Skip to content

Commit 31c269f

Browse files
fix(tools): honor config apiKey in media tool preflight (#85570)
Summary: - The branch adds a config-aware tool auth helper, routes image/PDF/media generation preflight and list selection through it, threads `workspaceDir`, and adds focused regression tests plus a changelog entry. - Reproducibility: yes. by source inspection. Current main gates affected media/PDF/generation preflight paths on env/profile auth while the runtime auth contract already accepts usable `models.providers.*.apiKey`. Automerge notes: - PR branch already contained follow-up commit before automerge: fix(tools): fall back to config apiKey in capability preflight - PR branch already contained follow-up commit before automerge: fix(tools): honor config apiKey in media tool preflight - PR branch already contained follow-up commit before automerge: fix(clawsweeper): address review for automerge-openclaw-openclaw-8557… Validation: - ClawSweeper review passed for head b8c9242. - Required merge gates passed before the squash merge. Prepared head SHA: b8c9242 Review: #85570 (comment) Co-authored-by: Mason Huang <masonxhuang@tencent.com> Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com> Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com> Approved-by: hxy91819 Co-authored-by: hxy91819 <8814856+hxy91819@users.noreply.github.com>
1 parent b4f62c9 commit 31c269f

16 files changed

Lines changed: 247 additions & 14 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ Docs: https://docs.openclaw.ai
4747

4848
### Fixes
4949

50+
- Agents/tools: honor configured custom provider API keys when deciding whether media, image-generation, video-generation, music-generation, and PDF tools are available. (#85570)
5051
- Windows installer: fail Git checkout installs when `pnpm install` or `pnpm build` fails instead of writing a wrapper to a missing CLI build.
5152
- Sessions: surface previous-transcript archive failures during `/new` rotation so disk rename errors are logged instead of silently hiding stranded transcript files. Fixes #81984. (#85586, from #82081) Thanks @0xghost42.
5253
- TUI/agents: mirror internal-ui message-tool replies into final chat output so message-tool-only agents remain visible in `openclaw tui`. Fixes #85538. Thanks @danpolasek.

src/agents/tools/image-generate-tool.actions.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ export function summarizeImageGenerationCapabilities(provider: ImageGenerationPr
6464

6565
export function createImageGenerateListActionResult(params: {
6666
cfg?: OpenClawConfig;
67+
workspaceDir?: string;
6768
agentDir?: string;
6869
authStore?: AuthProfileStore;
6970
}): ImageGenerateActionResult {
@@ -73,6 +74,7 @@ export function createImageGenerateListActionResult(params: {
7374
providers,
7475
emptyText: "No image-generation providers are registered.",
7576
cfg: params.cfg,
77+
workspaceDir: params.workspaceDir,
7678
agentDir: params.agentDir,
7779
authStore: params.authStore,
7880
listModes: listSupportedImageGenerationModes,

src/agents/tools/image-generate-tool.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,11 +208,13 @@ const ImageGenerateToolSchema = Type.Object({
208208

209209
export function resolveImageGenerationModelConfigForTool(params: {
210210
cfg?: OpenClawConfig;
211+
workspaceDir?: string;
211212
agentDir?: string;
212213
authStore?: AuthProfileStore;
213214
}): ToolModelConfig | null {
214215
return resolveCapabilityModelConfigForTool({
215216
cfg: params.cfg,
217+
workspaceDir: params.workspaceDir,
216218
agentDir: params.agentDir,
217219
authStore: params.authStore,
218220
modelConfig: params.cfg?.agents?.defaults?.imageGenerationModel,
@@ -806,6 +808,7 @@ export function createImageGenerateTool(options?: {
806808
if (action === "list") {
807809
return createImageGenerateListActionResult({
808810
cfg,
811+
workspaceDir: options?.workspaceDir,
809812
agentDir: options?.agentDir,
810813
authStore: options?.authProfileStore,
811814
});
@@ -816,6 +819,7 @@ export function createImageGenerateTool(options?: {
816819

817820
const imageGenerationModelConfig = resolveImageGenerationModelConfigForTool({
818821
cfg,
822+
workspaceDir: options?.workspaceDir,
819823
agentDir: options?.agentDir,
820824
authStore: options?.authProfileStore,
821825
});

src/agents/tools/image-tool.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,11 @@ vi.mock("../auth-profiles.js", () => ({
121121
}));
122122

123123
vi.mock("../model-auth.js", () => ({
124+
hasUsableCustomProviderApiKey: (cfg?: OpenClawConfig, provider?: string) => {
125+
const providerConfig = cfg?.models?.providers?.[provider ?? ""];
126+
const apiKey = providerConfig?.apiKey;
127+
return typeof apiKey === "string" && apiKey.trim().length > 0;
128+
},
124129
resolveEnvApiKey: (provider: string) => {
125130
const envVarByProvider: Record<string, string[]> = {
126131
anthropic: ["ANTHROPIC_API_KEY", "ANTHROPIC_OAUTH_TOKEN"],
@@ -1060,6 +1065,30 @@ describe("image tool implicit imageModel config", () => {
10601065
});
10611066
});
10621067

1068+
it("pairs a custom provider when config declares its api key", async () => {
1069+
await withTempAgentDir(async (agentDir) => {
1070+
const cfg: OpenClawConfig = {
1071+
agents: { defaults: { model: { primary: "hatchery-qwen3.6-plus/text-1" } } },
1072+
models: {
1073+
providers: {
1074+
"hatchery-qwen3.6-plus": {
1075+
baseUrl: "https://example.com",
1076+
apiKey: "sk-configured", // pragma: allowlist secret
1077+
models: [
1078+
makeModelDefinition("text-1", ["text"]),
1079+
makeModelDefinition("qwen3.6-plus", ["text", "image"]),
1080+
],
1081+
},
1082+
},
1083+
},
1084+
};
1085+
expect(resolveImageModelConfigForTool({ cfg, agentDir })).toEqual({
1086+
primary: "hatchery-qwen3.6-plus/qwen3.6-plus",
1087+
});
1088+
expect(typeof createImageTool({ config: cfg, agentDir })?.execute).toBe("function");
1089+
});
1090+
});
1091+
10631092
it("does not double-prefix custom provider model IDs that already include the provider", async () => {
10641093
await withTempAgentDir(async (agentDir) => {
10651094
await writeAuthProfiles(agentDir, {

src/agents/tools/image-tool.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,8 @@ export function resolveImageModelConfigForTool(params: {
239239

240240
return buildToolModelConfigFromCandidates({
241241
explicit,
242+
cfg: params.cfg,
243+
workspaceDir: params.workspaceDir,
242244
agentDir: params.agentDir,
243245
authStore: params.authStore,
244246
candidates: [...primaryAliasCandidates, ...primaryCandidates, ...remainingAutoCandidates],

src/agents/tools/media-generate-tool-actions-shared.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ export function createMediaGenerateProviderListActionResult<
4545
providers: TProvider[];
4646
emptyText: string;
4747
cfg?: OpenClawConfig;
48+
workspaceDir?: string;
4849
agentDir?: string;
4950
authStore?: AuthProfileStore;
5051
listModes: (provider: TProvider) => string[];
@@ -72,6 +73,7 @@ export function createMediaGenerateProviderListActionResult<
7273
providers: params.providers,
7374
provider,
7475
cfg: params.cfg,
76+
workspaceDir: params.workspaceDir,
7577
agentDir: params.agentDir,
7678
authStore: params.authStore,
7779
}),

src/agents/tools/media-tool-shared.test.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { pathToFileURL } from "node:url";
33
import { afterEach, describe, expect, it, vi } from "vitest";
44
import {
55
hasGenerationToolAvailability,
6+
isCapabilityProviderConfigured,
7+
resolveCapabilityModelConfigForTool,
68
resolveMediaToolLocalRoots,
79
resolveModelFromRegistry,
810
} from "./media-tool-shared.js";
@@ -104,6 +106,61 @@ describe("resolveModelFromRegistry", () => {
104106
});
105107

106108
describe("hasGenerationToolAvailability", () => {
109+
it("accepts config-backed custom provider auth for generation providers", () => {
110+
const cfg = {
111+
models: {
112+
providers: {
113+
"custom-image": {
114+
baseUrl: "https://example.com/v1",
115+
apiKey: "sk-configured", // pragma: allowlist secret
116+
models: [],
117+
},
118+
},
119+
},
120+
};
121+
122+
expect(
123+
hasGenerationToolAvailability({
124+
providerKey: "imageGenerationProviders",
125+
cfg,
126+
providers: [{ id: "custom-image", defaultModel: "workflow" }],
127+
}),
128+
).toBe(true);
129+
});
130+
131+
it("preserves a provider-specific not-configured result over generic config auth", () => {
132+
const cfg = {
133+
models: {
134+
providers: {
135+
"workflow-image": {
136+
baseUrl: "https://example.com/v1",
137+
apiKey: "sk-configured", // pragma: allowlist secret
138+
models: [],
139+
},
140+
},
141+
},
142+
};
143+
const provider = {
144+
id: "workflow-image",
145+
defaultModel: "workflow",
146+
isConfigured: () => false,
147+
};
148+
149+
expect(
150+
isCapabilityProviderConfigured({
151+
providers: [provider],
152+
provider,
153+
cfg,
154+
}),
155+
).toBe(false);
156+
expect(
157+
resolveCapabilityModelConfigForTool({
158+
cfg,
159+
providers: [provider],
160+
}),
161+
).toBeNull();
162+
});
163+
107164
it("allows generation tools for runtime providers configured without auth", () => {
108165
expect(
109166
hasGenerationToolAvailability({

src/agents/tools/media-tool-shared.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import {
2727
import {
2828
buildToolModelConfigFromCandidates,
2929
coerceToolModelConfig,
30-
hasAuthForProvider,
30+
hasProviderAuthForTool,
3131
hasToolModelConfig,
3232
resolveDefaultModelRef,
3333
type ToolModelConfig,
@@ -192,6 +192,7 @@ export function isCapabilityProviderConfigured<T extends CapabilityProvider>(par
192192
provider?: T;
193193
providerId?: string;
194194
cfg?: OpenClawConfig;
195+
workspaceDir?: string;
195196
agentDir?: string;
196197
authStore?: AuthProfileStore;
197198
}): boolean {
@@ -203,8 +204,10 @@ export function isCapabilityProviderConfigured<T extends CapabilityProvider>(par
203204
});
204205
if (!provider) {
205206
return params.providerId
206-
? hasAuthForProvider({
207+
? hasProviderAuthForTool({
207208
provider: params.providerId,
209+
cfg: params.cfg,
210+
workspaceDir: params.workspaceDir,
208211
agentDir: params.agentDir,
209212
authStore: params.authStore,
210213
})
@@ -216,8 +219,10 @@ export function isCapabilityProviderConfigured<T extends CapabilityProvider>(par
216219
agentDir: params.agentDir,
217220
});
218221
}
219-
return hasAuthForProvider({
222+
return hasProviderAuthForTool({
220223
provider: provider.id,
224+
cfg: params.cfg,
225+
workspaceDir: params.workspaceDir,
221226
agentDir: params.agentDir,
222227
authStore: params.authStore,
223228
});
@@ -251,6 +256,7 @@ export function resolveSelectedCapabilityProvider<T extends CapabilityProvider>(
251256

252257
function resolveCapabilityModelCandidatesForTool(params: {
253258
cfg?: OpenClawConfig;
259+
workspaceDir?: string;
254260
agentDir?: string;
255261
authStore?: AuthProfileStore;
256262
providers: CapabilityProvider[];
@@ -267,6 +273,7 @@ function resolveCapabilityModelCandidatesForTool(params: {
267273
providers: params.providers,
268274
provider,
269275
cfg: params.cfg,
276+
workspaceDir: params.workspaceDir,
270277
agentDir: params.agentDir,
271278
authStore: params.authStore,
272279
})
@@ -309,6 +316,7 @@ function resolveCapabilityModelCandidatesForTool(params: {
309316

310317
export function resolveCapabilityModelConfigForTool(params: {
311318
cfg?: OpenClawConfig;
319+
workspaceDir?: string;
312320
agentDir?: string;
313321
authStore?: AuthProfileStore;
314322
modelConfig?: AgentModelConfig;
@@ -326,10 +334,13 @@ export function resolveCapabilityModelConfigForTool(params: {
326334
};
327335
return buildToolModelConfigFromCandidates({
328336
explicit,
337+
cfg: params.cfg,
338+
workspaceDir: params.workspaceDir,
329339
agentDir: params.agentDir,
330340
authStore: params.authStore,
331341
candidates: resolveCapabilityModelCandidatesForTool({
332342
cfg: params.cfg,
343+
workspaceDir: params.workspaceDir,
333344
agentDir: params.agentDir,
334345
authStore: params.authStore,
335346
providers: getProviders(),
@@ -339,6 +350,7 @@ export function resolveCapabilityModelConfigForTool(params: {
339350
providers: getProviders(),
340351
providerId,
341352
cfg: params.cfg,
353+
workspaceDir: params.workspaceDir,
342354
agentDir: params.agentDir,
343355
authStore: params.authStore,
344356
}),
@@ -367,6 +379,7 @@ export function hasGenerationToolAvailability(params: {
367379
providers,
368380
provider,
369381
cfg: params.cfg,
382+
workspaceDir: params.workspaceDir,
370383
agentDir: params.agentDir,
371384
authStore: params.authStore,
372385
}),
@@ -396,8 +409,10 @@ export function hasGenerationToolAvailability(params: {
396409
contract: params.providerKey,
397410
config: params.cfg,
398411
}).some((providerId) =>
399-
hasAuthForProvider({
412+
hasProviderAuthForTool({
400413
provider: providerId,
414+
cfg: params.cfg,
415+
workspaceDir: params.workspaceDir,
401416
agentDir: params.agentDir,
402417
authStore: params.authStore,
403418
}),
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { afterEach, describe, expect, it, vi } from "vitest";
2+
import type { OpenClawConfig } from "../../config/config.js";
3+
import { hasProviderAuthForTool } from "./model-config.helpers.js";
4+
5+
describe("hasProviderAuthForTool", () => {
6+
afterEach(() => {
7+
vi.unstubAllEnvs();
8+
});
9+
10+
it("accepts config-backed custom provider auth", () => {
11+
const cfg = {
12+
models: {
13+
providers: {
14+
hatchery: {
15+
baseUrl: "https://example.com/v1",
16+
apiKey: "sk-configured", // pragma: allowlist secret
17+
models: [],
18+
},
19+
},
20+
},
21+
} as OpenClawConfig;
22+
23+
expect(hasProviderAuthForTool({ provider: "hatchery", cfg })).toBe(true);
24+
});
25+
26+
it("keeps auth-store profiles as valid tool auth", () => {
27+
expect(
28+
hasProviderAuthForTool({
29+
provider: "hatchery",
30+
authStore: {
31+
version: 1,
32+
profiles: {
33+
"hatchery:default": {
34+
provider: "hatchery",
35+
type: "api_key",
36+
key: "sk-profile", // pragma: allowlist secret
37+
},
38+
},
39+
},
40+
}),
41+
).toBe(true);
42+
});
43+
44+
it("rejects providers without config, env, or profile auth", () => {
45+
expect(hasProviderAuthForTool({ provider: "unconfigured-provider" })).toBe(false);
46+
});
47+
});

0 commit comments

Comments
 (0)