Skip to content

Commit 98cc6df

Browse files
TurboTheTurtleclawsweeper[bot]Takhoffman
authored
fix(anthropic): preserve Claude image capability (#83756)
Summary: - The PR adds Anthropic Claude 4.x image-capability normalization for stale text-only resolved model rows, regression tests for provider and fallback model resolution, and a changelog entry. - Reproducibility: yes. for source-level reproduction: current main gates native images on model.input includi ... s text-only. I did not run the command locally because this review was constrained to read-only inspection. Automerge notes: - PR branch already contained follow-up commit before automerge: fix(anthropic): preserve Claude image capability Validation: - ClawSweeper review passed for head 06dd378. - Required merge gates passed before the squash merge. Prepared head SHA: 06dd378 Review: #83756 (comment) Co-authored-by: Andy Ye <35905412+TurboTheTurtle@users.noreply.github.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: takhoffman Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
1 parent 83c225b commit 98cc6df

5 files changed

Lines changed: 130 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ Docs: https://docs.openclaw.ai
4848
### Fixes
4949

5050
- Agents/subagents: keep collect-mode announce queues batching unresolved-origin items with compatible same-route messages and resume collection after a true cross-channel drain when a later compatible batch remains. Fixes #83577.
51+
- Providers/Anthropic: preserve native image input for current Claude model rows when stale local catalog data marks them text-only. (#83756) Thanks @TurboTheTurtle.
5152
- Control UI: render live tool progress from session-scoped `session.tool` Gateway events so externally started runs show their tool cards in the active session. (#83734) Thanks @TurboTheTurtle.
5253
- Outbound: resolve send-capable channel plugins from the active runtime registry when the pinned startup registry only has setup metadata. (#83733) Thanks @TurboTheTurtle.
5354
- Browser: enforce current-tab URL allowlist checks for `/act` evaluate/batch actions and `/highlight` routes while leaving tab-management actions unblocked. (#78523)

extensions/anthropic/index.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,28 @@ describe("anthropic provider replay hooks", () => {
345345
expect(resolved).toBeUndefined();
346346
});
347347

348+
it("normalizes stale text-only Claude vision rows to image-capable", async () => {
349+
const provider = await registerSingleProviderPlugin(anthropicPlugin);
350+
351+
const normalized = provider.normalizeResolvedModel?.({
352+
provider: "anthropic",
353+
modelId: "claude-sonnet-4-5",
354+
model: {
355+
id: "claude-sonnet-4-5",
356+
name: "Claude Sonnet 4.5",
357+
provider: "anthropic",
358+
api: "anthropic-messages",
359+
reasoning: true,
360+
input: ["text"],
361+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
362+
contextWindow: 200_000,
363+
maxTokens: 64_000,
364+
},
365+
} as never);
366+
367+
expect(normalized?.input).toEqual(["text", "image"]);
368+
});
369+
348370
it("normalizes exact claude opus 4.7 variants to 1M context", async () => {
349371
const provider = await registerSingleProviderPlugin(anthropicPlugin);
350372

extensions/anthropic/register.runtime.ts

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,11 +63,17 @@ const ANTHROPIC_SONNET_46_DOT_MODEL_ID = "claude-sonnet-4.6";
6363
const ANTHROPIC_SONNET_TEMPLATE_MODEL_IDS = ["claude-sonnet-4-5", "claude-sonnet-4.5"] as const;
6464
const ANTHROPIC_MODERN_MODEL_PREFIXES = [
6565
"claude-opus-4-7",
66+
"claude-opus-4.7",
6667
"claude-opus-4-6",
68+
"claude-opus-4.6",
6769
"claude-sonnet-4-6",
70+
"claude-sonnet-4.6",
6871
"claude-opus-4-5",
72+
"claude-opus-4.5",
6973
"claude-sonnet-4-5",
74+
"claude-sonnet-4.5",
7075
"claude-haiku-4-5",
76+
"claude-haiku-4.5",
7177
] as const;
7278
const ANTHROPIC_SETUP_TOKEN_NOTE_LINES = [
7379
"Anthropic setup-token auth is supported in OpenClaw.",
@@ -370,6 +376,46 @@ function matchesAnthropicModernModel(modelId: string): boolean {
370376
return ANTHROPIC_MODERN_MODEL_PREFIXES.some((prefix) => lower.startsWith(prefix));
371377
}
372378

379+
function hasImageInput(input: unknown): boolean {
380+
return Array.isArray(input) && input.includes("image");
381+
}
382+
383+
function supportsAnthropicImageInput(modelId: string, modelName?: string): boolean {
384+
return [modelId, modelName]
385+
.filter((value): value is string => typeof value === "string")
386+
.some((candidate) => matchesAnthropicModernModel(candidate));
387+
}
388+
389+
function applyAnthropicImageInputCapability(params: {
390+
modelId: string;
391+
model: ProviderRuntimeModel;
392+
}): ProviderRuntimeModel | undefined {
393+
if (hasImageInput(params.model.input)) {
394+
return undefined;
395+
}
396+
if (!supportsAnthropicImageInput(params.modelId, params.model.name)) {
397+
return undefined;
398+
}
399+
return {
400+
...params.model,
401+
input: ["text", "image"],
402+
};
403+
}
404+
405+
function normalizeAnthropicResolvedModel(
406+
ctx: ProviderNormalizeResolvedModelContext,
407+
): ProviderRuntimeModel | undefined {
408+
const imageCapableModel = applyAnthropicImageInputCapability(ctx) ?? ctx.model;
409+
const contextWindowModel =
410+
applyAnthropicOpus47ContextWindow({
411+
config: ctx.config,
412+
provider: ctx.provider,
413+
modelId: ctx.modelId,
414+
model: imageCapableModel,
415+
}) ?? imageCapableModel;
416+
return contextWindowModel === ctx.model ? undefined : contextWindowModel;
417+
}
418+
373419
function buildAnthropicAuthDoctorHint(params: {
374420
config?: ProviderAuthContext["config"];
375421
store: AuthProfileStore;
@@ -576,16 +622,21 @@ export function buildAnthropicProvider(): ProviderPlugin {
576622
if (!model) {
577623
return undefined;
578624
}
625+
const imageCapableModel =
626+
applyAnthropicImageInputCapability({
627+
modelId: ctx.modelId,
628+
model,
629+
}) ?? model;
579630
return (
580631
applyAnthropicOpus47ContextWindow({
581632
config: ctx.config,
582633
provider: ctx.provider,
583634
modelId: ctx.modelId,
584-
model,
585-
}) ?? model
635+
model: imageCapableModel,
636+
}) ?? imageCapableModel
586637
);
587638
},
588-
normalizeResolvedModel: (ctx) => applyAnthropicOpus47ContextWindow(ctx),
639+
normalizeResolvedModel: (ctx) => normalizeAnthropicResolvedModel(ctx),
589640
resolveSyntheticAuth: ({ provider }) =>
590641
normalizeLowercaseStringOrEmpty(provider) === CLAUDE_CLI_BACKEND_ID
591642
? resolveClaudeCliSyntheticAuth()

src/agents/pi-embedded-runner/model.provider-runtime.test-support.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,20 @@ const GOOGLE_GEMINI_CLI_BASE_URL = "https://cloudcode-pa.googleapis.com";
1414
const DEFAULT_CONTEXT_WINDOW = 200_000;
1515
const DEFAULT_MAX_TOKENS = 8192;
1616
const OPENROUTER_FALLBACK_COST = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
17+
const ANTHROPIC_VISION_MODEL_PREFIXES = [
18+
"claude-opus-4-7",
19+
"claude-opus-4.7",
20+
"claude-opus-4-6",
21+
"claude-opus-4.6",
22+
"claude-sonnet-4-6",
23+
"claude-sonnet-4.6",
24+
"claude-opus-4-5",
25+
"claude-opus-4.5",
26+
"claude-sonnet-4-5",
27+
"claude-sonnet-4.5",
28+
"claude-haiku-4-5",
29+
"claude-haiku-4.5",
30+
] as const;
1731

1832
type ModelRegistryLike = {
1933
find: (provider: string, modelId: string) => unknown;
@@ -91,6 +105,20 @@ function normalizeDynamicModel(params: { provider: string; model: ResolvedModelL
91105
}
92106
return undefined;
93107
}
108+
if (params.provider === "anthropic" || params.provider === "claude-cli") {
109+
const candidates = [params.model.id, params.model.name]
110+
.filter((value): value is string => typeof value === "string")
111+
.map((value) => lowercasePreservingWhitespace(value))
112+
.filter(Boolean);
113+
const isKnownVisionModel = candidates.some((candidate) =>
114+
ANTHROPIC_VISION_MODEL_PREFIXES.some((prefix) => candidate.startsWith(prefix)),
115+
);
116+
const hasImageInput = Array.isArray(params.model.input) && params.model.input.includes("image");
117+
if (isKnownVisionModel && !hasImageInput) {
118+
return { ...params.model, input: ["text", "image"] };
119+
}
120+
return undefined;
121+
}
94122
if (params.provider !== "openai-codex") {
95123
return undefined;
96124
}

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1533,6 +1533,31 @@ describe("resolveModel", () => {
15331533
expect(result.model?.input).toEqual(["text", "image"]);
15341534
});
15351535

1536+
it("repairs stale text-only Anthropic fallback rows for Claude vision models", () => {
1537+
const cfg = {
1538+
models: {
1539+
providers: {
1540+
anthropic: {
1541+
baseUrl: "https://api.anthropic.com",
1542+
api: "anthropic-messages",
1543+
models: [
1544+
{
1545+
...makeModel("claude-sonnet-4-5"),
1546+
name: "claude-sonnet-4-5",
1547+
api: "anthropic-messages",
1548+
input: ["text"],
1549+
},
1550+
],
1551+
},
1552+
},
1553+
},
1554+
} as unknown as OpenClawConfig;
1555+
1556+
const result = resolveModelForTest("anthropic", "claude-sonnet-4-5", "/tmp/agent", cfg);
1557+
1558+
expect(result.model?.input).toEqual(["text", "image"]);
1559+
});
1560+
15361561
it("repairs stale text-only Foundry discovered rows for GPT-family models", () => {
15371562
const cfg = {
15381563
models: {

0 commit comments

Comments
 (0)