mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-26 12:26:38 -06:00
fix(anthropic): preserve configured Claude image capability
This commit is contained in:
@@ -89,6 +89,7 @@ Docs: https://docs.openclaw.ai
|
||||
- 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.
|
||||
- Skills: refresh existing session skill snapshots when watched skill roots change, so changed extra skill directories take effect without starting a new session. Fixes #83782. (#83800) Thanks @hclsys.
|
||||
- Providers/Anthropic: preserve native image input for current Claude model rows when stale local catalog data marks them text-only. (#83756) Thanks @TurboTheTurtle.
|
||||
- Providers/Anthropic: preserve Claude 4 image capability when configured model refs resolve through a stale local catalog row. (#83756) Thanks @TurboTheTurtle.
|
||||
- Providers/DeepSeek: normalize MCP tool schemas with `anyOf`/`oneOf` unions before normal and compaction requests reach DeepSeek, preventing union-shaped parameters from being rejected. (#83766) 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.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildInlineProviderModels } from "./model.inline-provider.js";
|
||||
import { buildInlineProviderModels, resolveProviderModelInput } from "./model.inline-provider.js";
|
||||
import { makeModel } from "./model.test-harness.js";
|
||||
|
||||
describe("buildInlineProviderModels", () => {
|
||||
@@ -261,3 +261,16 @@ describe("buildInlineProviderModels", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveProviderModelInput", () => {
|
||||
it("keeps configured Anthropic model input unchanged before provider-owned normalization", () => {
|
||||
expect(
|
||||
resolveProviderModelInput({
|
||||
provider: "anthropic",
|
||||
modelId: "claude-sonnet-4-5",
|
||||
modelName: "Claude Sonnet 4.5",
|
||||
input: ["text"],
|
||||
}),
|
||||
).toEqual(["text"]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -32,6 +32,19 @@ const loadProviderIndexCatalogRowsForList = vi.fn<() => Array<Record<string, unk
|
||||
const hasProviderStaticCatalogForFilter = vi.fn().mockResolvedValue(false);
|
||||
const shouldSuppressBuiltInModel = vi.fn().mockReturnValue(false);
|
||||
const shouldSuppressBuiltInModelFromManifest = vi.fn().mockReturnValue(false);
|
||||
const normalizeProviderResolvedModelWithPlugin = vi.hoisted(() =>
|
||||
vi.fn(({ context }) => {
|
||||
if (
|
||||
context?.provider === "anthropic" &&
|
||||
context?.modelId === "claude-sonnet-4-5" &&
|
||||
Array.isArray(context?.model?.input) &&
|
||||
!context.model.input.includes("image")
|
||||
) {
|
||||
return { ...context.model, input: ["text", "image"] };
|
||||
}
|
||||
return undefined;
|
||||
}),
|
||||
);
|
||||
const modelRegistryState = {
|
||||
models: [] as Array<Record<string, unknown>>,
|
||||
available: [] as Array<Record<string, unknown>>,
|
||||
@@ -124,6 +137,14 @@ vi.mock("../agents/pi-model-discovery.js", () => {
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../plugins/provider-runtime.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../plugins/provider-runtime.js")>();
|
||||
return {
|
||||
...actual,
|
||||
normalizeProviderResolvedModelWithPlugin,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../plugins/synthetic-auth.runtime.js", () => ({
|
||||
resolveRuntimeSyntheticAuthProviderRefs: () => [],
|
||||
}));
|
||||
@@ -232,6 +253,7 @@ beforeEach(() => {
|
||||
hasProviderStaticCatalogForFilter.mockResolvedValue(false);
|
||||
shouldSuppressBuiltInModel.mockReset();
|
||||
shouldSuppressBuiltInModel.mockReturnValue(false);
|
||||
normalizeProviderResolvedModelWithPlugin.mockClear();
|
||||
readConfigFileSnapshotForWrite.mockClear();
|
||||
readConfigFileSnapshotForWrite.mockResolvedValue({
|
||||
snapshot: { valid: false, resolved: {} },
|
||||
@@ -448,6 +470,29 @@ describe("models list/status", () => {
|
||||
expect(runtimeLogText(runtime)).toBe("openrouter/hunter-alpha");
|
||||
});
|
||||
|
||||
it("models list configured fallback marks stale Anthropic Claude 4 refs image-capable", async () => {
|
||||
getRuntimeConfig.mockReturnValue({
|
||||
agents: { defaults: { model: "anthropic/claude-sonnet-4-5" } },
|
||||
});
|
||||
const runtime = makeRuntime();
|
||||
|
||||
await modelsListCommand({ json: true }, runtime);
|
||||
|
||||
const payload = parseJsonLog(runtime);
|
||||
expect(payload.models[0]).toMatchObject({
|
||||
key: "anthropic/claude-sonnet-4-5",
|
||||
input: "text+image",
|
||||
});
|
||||
expect(normalizeProviderResolvedModelWithPlugin).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
provider: "anthropic",
|
||||
context: expect.objectContaining({
|
||||
modelId: "claude-sonnet-4-5",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it.each(["z.ai", "Z.AI", "z-ai"] as const)(
|
||||
"models list provider filter normalizes %s alias",
|
||||
async (provider) => {
|
||||
|
||||
@@ -166,6 +166,7 @@ export async function modelsListCommand(
|
||||
},
|
||||
skipRuntimeModelSuppression,
|
||||
metadataSnapshot,
|
||||
workspaceDir,
|
||||
});
|
||||
const rows: ModelRow[] = [];
|
||||
|
||||
|
||||
@@ -10,6 +10,8 @@ import type { ModelDefinitionConfig, ModelProviderConfig } from "../../config/ty
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import type { NormalizedModelCatalogRow } from "../../model-catalog/index.js";
|
||||
import type { PluginMetadataSnapshot } from "../../plugins/plugin-metadata-snapshot.types.js";
|
||||
import type { ProviderRuntimeModel } from "../../plugins/provider-runtime-model.types.js";
|
||||
import { normalizeProviderResolvedModelWithPlugin } from "../../plugins/provider-runtime.js";
|
||||
import { createLazyImportLoader } from "../../shared/lazy-promise.js";
|
||||
import type { ModelListAuthIndex } from "./list.auth-index.js";
|
||||
import type { ListRowModel } from "./list.model-row.js";
|
||||
@@ -37,6 +39,7 @@ export type RowBuilderContext = {
|
||||
filter: RowFilter;
|
||||
skipRuntimeModelSuppression?: boolean;
|
||||
metadataSnapshot?: PluginMetadataSnapshot;
|
||||
workspaceDir?: string;
|
||||
};
|
||||
|
||||
const modelCatalogModuleLoader = createLazyImportLoader<ModelCatalogModule>(
|
||||
@@ -116,6 +119,38 @@ function shouldSuppressListModel(params: {
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeListRowWithProviderPlugin(params: {
|
||||
model: ListRowModel;
|
||||
context: RowBuilderContext;
|
||||
}): ListRowModel {
|
||||
const normalized = normalizeProviderResolvedModelWithPlugin({
|
||||
provider: params.model.provider,
|
||||
config: params.context.cfg,
|
||||
workspaceDir: params.context.workspaceDir,
|
||||
context: {
|
||||
config: params.context.cfg,
|
||||
agentDir: params.context.agentDir,
|
||||
workspaceDir: params.context.workspaceDir,
|
||||
provider: params.model.provider,
|
||||
modelId: params.model.id,
|
||||
model: params.model as ProviderRuntimeModel,
|
||||
},
|
||||
});
|
||||
if (!normalized) {
|
||||
return params.model;
|
||||
}
|
||||
return {
|
||||
...params.model,
|
||||
id: normalized.id,
|
||||
name: normalized.name,
|
||||
provider: normalized.provider,
|
||||
baseUrl: normalized.baseUrl,
|
||||
input: toListRowInput(normalized.input),
|
||||
contextWindow: normalized.contextWindow,
|
||||
contextTokens: normalized.contextTokens,
|
||||
};
|
||||
}
|
||||
|
||||
async function appendVisibleRow(params: {
|
||||
rows: ModelRow[];
|
||||
model: ListRowModel;
|
||||
@@ -131,15 +166,19 @@ async function appendVisibleRow(params: {
|
||||
if (!matchesRowFilter(params.context.filter, params.model)) {
|
||||
return false;
|
||||
}
|
||||
const normalizedModel = normalizeListRowWithProviderPlugin({
|
||||
model: params.model,
|
||||
context: params.context,
|
||||
});
|
||||
if (
|
||||
!params.skipSuppression &&
|
||||
shouldSuppressListModel({ model: params.model, context: params.context })
|
||||
shouldSuppressListModel({ model: normalizedModel, context: params.context })
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
params.rows.push(
|
||||
await buildRow({
|
||||
model: params.model,
|
||||
model: normalizedModel,
|
||||
key: params.key,
|
||||
context: params.context,
|
||||
allowProviderAvailabilityFallback: params.allowProviderAvailabilityFallback,
|
||||
@@ -487,7 +526,7 @@ export async function appendConfiguredRows(params: {
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const model =
|
||||
const resolvedModel =
|
||||
params.modelRegistry && resolveModelWithRegistry
|
||||
? resolveModelWithRegistry({
|
||||
provider: entry.ref.provider,
|
||||
@@ -496,6 +535,9 @@ export async function appendConfiguredRows(params: {
|
||||
cfg: params.context.cfg,
|
||||
})
|
||||
: toFallbackConfiguredListModel(entry, params.context.cfg);
|
||||
const model = resolvedModel
|
||||
? normalizeListRowWithProviderPlugin({ model: resolvedModel, context: params.context })
|
||||
: resolvedModel;
|
||||
if (params.context.filter.local && model && !isLocalBaseUrl(model.baseUrl ?? "")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user