Skip to content

Commit 10e6e27

Browse files
andyliuhydro13
andauthored
fix(models): guard optional model input capabilities (#42096)
Merged via squash. Prepared head SHA: d398fa0 Co-authored-by: andyliu <[email protected]> Co-authored-by: hydro13 <[email protected]> Reviewed-by: @hydro13
1 parent 144c1b8 commit 10e6e27

File tree

4 files changed

+47
-4
lines changed

4 files changed

+47
-4
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ Docs: https://docs.openclaw.ai
9696
- Telegram/final preview delivery followup: keep ambiguous missing-`message_id` finals only when a preview was already visible, while first-preview/no-id cases still fall back so Telegram users do not lose the final reply. (#41932) thanks @hougangdev.
9797
- Agents/Azure OpenAI Responses: include the `azure-openai` provider in the Responses API store override so Azure OpenAI multi-turn cron jobs and embedded agent runs no longer fail with HTTP 400 "store is set to false". (#42934, fixes #42800) Thanks @ademczuk.
9898
- Agents/context pruning: prune image-only tool results during soft-trim, align context-pruning coverage with the new tool-result contract, and extend historical image cleanup to the same screenshot-heavy session path. (#43045) Thanks @MoerAI.
99+
- fix(models): guard optional model.input capability checks (#42096) thanks @andyliu
99100

100101
## 2026.3.8
101102

src/agents/model-scan.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -326,12 +326,12 @@ async function probeImage(
326326
}
327327

328328
function ensureImageInput(model: OpenAIModel): OpenAIModel {
329-
if (model.input.includes("image")) {
329+
if (model.input?.includes("image")) {
330330
return model;
331331
}
332332
return {
333333
...model,
334-
input: Array.from(new Set([...model.input, "image"])),
334+
input: Array.from(new Set([...(model.input ?? []), "image"])),
335335
};
336336
}
337337

@@ -472,7 +472,7 @@ export async function scanOpenRouterModels(
472472
};
473473

474474
const toolResult = await probeTool(model, apiKey, timeoutMs);
475-
const imageResult = model.input.includes("image")
475+
const imageResult = model.input?.includes("image")
476476
? await probeImage(ensureImageInput(model), apiKey, timeoutMs)
477477
: { ok: false, latencyMs: null, skipped: true };
478478

src/agents/pi-embedded-runner/model.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,42 @@ describe("buildInlineProviderModels", () => {
202202
});
203203

204204
describe("resolveModel", () => {
205+
it("defaults model input to text when discovery omits input", () => {
206+
mockDiscoveredModel({
207+
provider: "custom",
208+
modelId: "missing-input",
209+
templateModel: {
210+
id: "missing-input",
211+
name: "missing-input",
212+
api: "openai-completions",
213+
provider: "custom",
214+
baseUrl: "http://localhost:9999",
215+
reasoning: false,
216+
// NOTE: deliberately omit input to simulate buggy/custom catalogs.
217+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
218+
contextWindow: 8192,
219+
maxTokens: 1024,
220+
},
221+
});
222+
223+
const result = resolveModel("custom", "missing-input", "/tmp/agent", {
224+
models: {
225+
providers: {
226+
custom: {
227+
baseUrl: "http://localhost:9999",
228+
api: "openai-completions",
229+
// Intentionally keep this minimal — the discovered model provides the rest.
230+
models: [{ id: "missing-input", name: "missing-input" }],
231+
},
232+
},
233+
},
234+
} as unknown as OpenClawConfig);
235+
236+
expect(result.error).toBeUndefined();
237+
expect(Array.isArray(result.model?.input)).toBe(true);
238+
expect(result.model?.input).toEqual(["text"]);
239+
});
240+
205241
it("includes provider baseUrl in fallback model", () => {
206242
const cfg = {
207243
models: {

src/agents/pi-embedded-runner/model.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,12 +93,18 @@ function applyConfiguredProviderOverrides(params: {
9393
headers: discoveredHeaders,
9494
};
9595
}
96+
const resolvedInput = configuredModel?.input ?? discoveredModel.input;
97+
const normalizedInput =
98+
Array.isArray(resolvedInput) && resolvedInput.length > 0
99+
? resolvedInput.filter((item) => item === "text" || item === "image")
100+
: (["text"] as Array<"text" | "image">);
101+
96102
return {
97103
...discoveredModel,
98104
api: configuredModel?.api ?? providerConfig.api ?? discoveredModel.api,
99105
baseUrl: providerConfig.baseUrl ?? discoveredModel.baseUrl,
100106
reasoning: configuredModel?.reasoning ?? discoveredModel.reasoning,
101-
input: configuredModel?.input ?? discoveredModel.input,
107+
input: normalizedInput,
102108
cost: configuredModel?.cost ?? discoveredModel.cost,
103109
contextWindow: configuredModel?.contextWindow ?? discoveredModel.contextWindow,
104110
maxTokens: configuredModel?.maxTokens ?? discoveredModel.maxTokens,

0 commit comments

Comments
 (0)