mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 05:16:23 -06:00
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 head06dd378ea3. - Required merge gates passed before the squash merge. Prepared head SHA:06dd378ea3Review: https://github.com/openclaw/openclaw/pull/83756#issuecomment-4482116499 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>
This commit is contained in:
@@ -48,6 +48,7 @@ Docs: https://docs.openclaw.ai
|
||||
### Fixes
|
||||
|
||||
- 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.
|
||||
- Providers/Anthropic: preserve native image input for current Claude model rows when stale local catalog data marks them text-only. (#83756) Thanks @TurboTheTurtle.
|
||||
- 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.
|
||||
- Outbound: resolve send-capable channel plugins from the active runtime registry when the pinned startup registry only has setup metadata. (#83733) Thanks @TurboTheTurtle.
|
||||
- Browser: enforce current-tab URL allowlist checks for `/act` evaluate/batch actions and `/highlight` routes while leaving tab-management actions unblocked. (#78523)
|
||||
|
||||
@@ -345,6 +345,28 @@ describe("anthropic provider replay hooks", () => {
|
||||
expect(resolved).toBeUndefined();
|
||||
});
|
||||
|
||||
it("normalizes stale text-only Claude vision rows to image-capable", async () => {
|
||||
const provider = await registerSingleProviderPlugin(anthropicPlugin);
|
||||
|
||||
const normalized = provider.normalizeResolvedModel?.({
|
||||
provider: "anthropic",
|
||||
modelId: "claude-sonnet-4-5",
|
||||
model: {
|
||||
id: "claude-sonnet-4-5",
|
||||
name: "Claude Sonnet 4.5",
|
||||
provider: "anthropic",
|
||||
api: "anthropic-messages",
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 200_000,
|
||||
maxTokens: 64_000,
|
||||
},
|
||||
} as never);
|
||||
|
||||
expect(normalized?.input).toEqual(["text", "image"]);
|
||||
});
|
||||
|
||||
it("normalizes exact claude opus 4.7 variants to 1M context", async () => {
|
||||
const provider = await registerSingleProviderPlugin(anthropicPlugin);
|
||||
|
||||
|
||||
@@ -63,11 +63,17 @@ const ANTHROPIC_SONNET_46_DOT_MODEL_ID = "claude-sonnet-4.6";
|
||||
const ANTHROPIC_SONNET_TEMPLATE_MODEL_IDS = ["claude-sonnet-4-5", "claude-sonnet-4.5"] as const;
|
||||
const ANTHROPIC_MODERN_MODEL_PREFIXES = [
|
||||
"claude-opus-4-7",
|
||||
"claude-opus-4.7",
|
||||
"claude-opus-4-6",
|
||||
"claude-opus-4.6",
|
||||
"claude-sonnet-4-6",
|
||||
"claude-sonnet-4.6",
|
||||
"claude-opus-4-5",
|
||||
"claude-opus-4.5",
|
||||
"claude-sonnet-4-5",
|
||||
"claude-sonnet-4.5",
|
||||
"claude-haiku-4-5",
|
||||
"claude-haiku-4.5",
|
||||
] as const;
|
||||
const ANTHROPIC_SETUP_TOKEN_NOTE_LINES = [
|
||||
"Anthropic setup-token auth is supported in OpenClaw.",
|
||||
@@ -370,6 +376,46 @@ function matchesAnthropicModernModel(modelId: string): boolean {
|
||||
return ANTHROPIC_MODERN_MODEL_PREFIXES.some((prefix) => lower.startsWith(prefix));
|
||||
}
|
||||
|
||||
function hasImageInput(input: unknown): boolean {
|
||||
return Array.isArray(input) && input.includes("image");
|
||||
}
|
||||
|
||||
function supportsAnthropicImageInput(modelId: string, modelName?: string): boolean {
|
||||
return [modelId, modelName]
|
||||
.filter((value): value is string => typeof value === "string")
|
||||
.some((candidate) => matchesAnthropicModernModel(candidate));
|
||||
}
|
||||
|
||||
function applyAnthropicImageInputCapability(params: {
|
||||
modelId: string;
|
||||
model: ProviderRuntimeModel;
|
||||
}): ProviderRuntimeModel | undefined {
|
||||
if (hasImageInput(params.model.input)) {
|
||||
return undefined;
|
||||
}
|
||||
if (!supportsAnthropicImageInput(params.modelId, params.model.name)) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
...params.model,
|
||||
input: ["text", "image"],
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeAnthropicResolvedModel(
|
||||
ctx: ProviderNormalizeResolvedModelContext,
|
||||
): ProviderRuntimeModel | undefined {
|
||||
const imageCapableModel = applyAnthropicImageInputCapability(ctx) ?? ctx.model;
|
||||
const contextWindowModel =
|
||||
applyAnthropicOpus47ContextWindow({
|
||||
config: ctx.config,
|
||||
provider: ctx.provider,
|
||||
modelId: ctx.modelId,
|
||||
model: imageCapableModel,
|
||||
}) ?? imageCapableModel;
|
||||
return contextWindowModel === ctx.model ? undefined : contextWindowModel;
|
||||
}
|
||||
|
||||
function buildAnthropicAuthDoctorHint(params: {
|
||||
config?: ProviderAuthContext["config"];
|
||||
store: AuthProfileStore;
|
||||
@@ -576,16 +622,21 @@ export function buildAnthropicProvider(): ProviderPlugin {
|
||||
if (!model) {
|
||||
return undefined;
|
||||
}
|
||||
const imageCapableModel =
|
||||
applyAnthropicImageInputCapability({
|
||||
modelId: ctx.modelId,
|
||||
model,
|
||||
}) ?? model;
|
||||
return (
|
||||
applyAnthropicOpus47ContextWindow({
|
||||
config: ctx.config,
|
||||
provider: ctx.provider,
|
||||
modelId: ctx.modelId,
|
||||
model,
|
||||
}) ?? model
|
||||
model: imageCapableModel,
|
||||
}) ?? imageCapableModel
|
||||
);
|
||||
},
|
||||
normalizeResolvedModel: (ctx) => applyAnthropicOpus47ContextWindow(ctx),
|
||||
normalizeResolvedModel: (ctx) => normalizeAnthropicResolvedModel(ctx),
|
||||
resolveSyntheticAuth: ({ provider }) =>
|
||||
normalizeLowercaseStringOrEmpty(provider) === CLAUDE_CLI_BACKEND_ID
|
||||
? resolveClaudeCliSyntheticAuth()
|
||||
|
||||
@@ -14,6 +14,20 @@ const GOOGLE_GEMINI_CLI_BASE_URL = "https://cloudcode-pa.googleapis.com";
|
||||
const DEFAULT_CONTEXT_WINDOW = 200_000;
|
||||
const DEFAULT_MAX_TOKENS = 8192;
|
||||
const OPENROUTER_FALLBACK_COST = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
||||
const ANTHROPIC_VISION_MODEL_PREFIXES = [
|
||||
"claude-opus-4-7",
|
||||
"claude-opus-4.7",
|
||||
"claude-opus-4-6",
|
||||
"claude-opus-4.6",
|
||||
"claude-sonnet-4-6",
|
||||
"claude-sonnet-4.6",
|
||||
"claude-opus-4-5",
|
||||
"claude-opus-4.5",
|
||||
"claude-sonnet-4-5",
|
||||
"claude-sonnet-4.5",
|
||||
"claude-haiku-4-5",
|
||||
"claude-haiku-4.5",
|
||||
] as const;
|
||||
|
||||
type ModelRegistryLike = {
|
||||
find: (provider: string, modelId: string) => unknown;
|
||||
@@ -91,6 +105,20 @@ function normalizeDynamicModel(params: { provider: string; model: ResolvedModelL
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
if (params.provider === "anthropic" || params.provider === "claude-cli") {
|
||||
const candidates = [params.model.id, params.model.name]
|
||||
.filter((value): value is string => typeof value === "string")
|
||||
.map((value) => lowercasePreservingWhitespace(value))
|
||||
.filter(Boolean);
|
||||
const isKnownVisionModel = candidates.some((candidate) =>
|
||||
ANTHROPIC_VISION_MODEL_PREFIXES.some((prefix) => candidate.startsWith(prefix)),
|
||||
);
|
||||
const hasImageInput = Array.isArray(params.model.input) && params.model.input.includes("image");
|
||||
if (isKnownVisionModel && !hasImageInput) {
|
||||
return { ...params.model, input: ["text", "image"] };
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
if (params.provider !== "openai-codex") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -1533,6 +1533,31 @@ describe("resolveModel", () => {
|
||||
expect(result.model?.input).toEqual(["text", "image"]);
|
||||
});
|
||||
|
||||
it("repairs stale text-only Anthropic fallback rows for Claude vision models", () => {
|
||||
const cfg = {
|
||||
models: {
|
||||
providers: {
|
||||
anthropic: {
|
||||
baseUrl: "https://api.anthropic.com",
|
||||
api: "anthropic-messages",
|
||||
models: [
|
||||
{
|
||||
...makeModel("claude-sonnet-4-5"),
|
||||
name: "claude-sonnet-4-5",
|
||||
api: "anthropic-messages",
|
||||
input: ["text"],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as OpenClawConfig;
|
||||
|
||||
const result = resolveModelForTest("anthropic", "claude-sonnet-4-5", "/tmp/agent", cfg);
|
||||
|
||||
expect(result.model?.input).toEqual(["text", "image"]);
|
||||
});
|
||||
|
||||
it("repairs stale text-only Foundry discovered rows for GPT-family models", () => {
|
||||
const cfg = {
|
||||
models: {
|
||||
|
||||
Reference in New Issue
Block a user