Files
openclaw/extensions/anthropic/openclaw.plugin.test.ts
Ayaan Zaidi 041938bc2f feat(ui): add a Claude CLI 200K/1M context-window switch to the model picker (#127951)
Adds a generic plugin-declared selectable-context-window surface mirroring thinkingLevels: ModelCatalogEntry.contextWindows + contextWindowDefault through catalog normalization and the gateway protocol, session validation on sessions.create/patch, and a 200K/1M switch inside the Control UI model picker for Claude CLI 5-series models. The Anthropic plugin owns the option mapping: explicit 1m → `[1m]` argv suffix, 200k → bare id + CLAUDE_CODE_DISABLE_1M_CONTEXT=1, omitted → bare id (shipped default argv). Run budgets follow the selection on both CLI and native paths, so a 200K session gets a matching auto-compact window instead of a silent 1M budget.

Review fixes landed in this PR: run-owner prepared-fact plumbing so ordinary replies honor the selection; atomic catalog overlay merge and normalization for the options/default tuple; one-owner tuple reads in the picker; sessions.create key-presence patch semantics; native-run budget capping.

Feature direction and in-picker switch shape by @obviyus (maintainer review).
2026-08-22 23:25:23 +05:30

158 lines
5.3 KiB
TypeScript

// Anthropic tests cover provider manifest model catalog behavior.
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
type AnthropicCatalogModel = {
id?: string;
name?: string;
reasoning?: boolean;
input?: string[];
mediaInput?: {
image?: {
maxSidePx?: number;
preferredSidePx?: number;
tokenMode?: string;
};
};
contextWindow?: number;
contextWindows?: Array<{ id: string; label: string; contextWindow: number }>;
contextWindowDefault?: string;
maxTokens?: number;
cost?: {
input?: number;
output?: number;
cacheRead?: number;
cacheWrite?: number;
};
thinkingLevelMap?: Record<string, string | null>;
status?: string;
replacedBy?: string;
compat?: { codeMode?: string };
};
type AnthropicManifest = {
modelCatalog?: {
providers?: {
anthropic?: { models?: AnthropicCatalogModel[] };
"claude-cli"?: { models?: AnthropicCatalogModel[] };
};
discovery?: Record<string, string>;
};
};
const manifest = JSON.parse(
readFileSync(new URL("./openclaw.plugin.json", import.meta.url), "utf8"),
) as AnthropicManifest;
const selectableContextWindowMetadata = {
contextWindows: [
{ id: "200k", label: "200K", contextWindow: 200_000 },
{ id: "1m", label: "1M", contextWindow: 1_000_000 },
],
contextWindowDefault: "1m",
};
describe("Anthropic plugin manifest", () => {
it("flags every static Anthropic API model as code-mode preferred", () => {
const models = manifest.modelCatalog?.providers?.anthropic?.models ?? [];
expect(models.length).toBeGreaterThan(0);
for (const model of models) {
expect(model.compat?.codeMode, model.id).toBe("preferred");
}
});
it("publishes the exact Claude Sonnet 5 API contract", () => {
const models = manifest.modelCatalog?.providers?.anthropic?.models ?? [];
expect(models.find((model) => model.id === "claude-sonnet-5")).toEqual({
id: "claude-sonnet-5",
name: "Claude Sonnet 5",
reasoning: true,
input: ["text", "image"],
mediaInput: {
image: { maxSidePx: 2576, preferredSidePx: 2576, tokenMode: "provider" },
},
cost: { input: 2, output: 10, cacheRead: 0.2, cacheWrite: 2.5 },
contextWindow: 1_000_000,
...selectableContextWindowMetadata,
maxTokens: 128_000,
thinkingLevelMap: { xhigh: "xhigh", max: "max" },
compat: { codeMode: "preferred" },
});
});
it("publishes the exact Claude Opus 5 API contract", () => {
const models = manifest.modelCatalog?.providers?.anthropic?.models ?? [];
expect(models.find((model) => model.id === "claude-opus-5")).toEqual({
id: "claude-opus-5",
name: "Claude Opus 5",
reasoning: true,
input: ["text", "image"],
mediaInput: {
image: { maxSidePx: 2576, preferredSidePx: 2576, tokenMode: "provider" },
},
cost: { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
contextWindow: 1_000_000,
...selectableContextWindowMetadata,
maxTokens: 128_000,
thinkingLevelMap: { xhigh: "xhigh", max: "max" },
compat: { codeMode: "preferred" },
});
// Opus 5's 1M window is the model default, so the CLI row is not clamped to 200k.
const cliModels = manifest.modelCatalog?.providers?.["claude-cli"]?.models ?? [];
expect(cliModels.find((model) => model.id === "claude-opus-5")).toMatchObject({
contextWindow: 1_000_000,
maxTokens: 128_000,
});
});
it("declares selectable context windows on both canonical Fable catalog rows", () => {
const providers = manifest.modelCatalog?.providers;
expect(
providers?.anthropic?.models?.find((model) => model.id === "claude-fable-5"),
).toMatchObject(selectableContextWindowMetadata);
expect(
providers?.["claude-cli"]?.models?.find((model) => model.id === "claude-fable-5"),
).toMatchObject(selectableContextWindowMetadata);
});
it("declares Opus 4.8 limits without overstating bare Claude CLI context", () => {
const models = manifest.modelCatalog?.providers?.anthropic?.models ?? [];
expect(models.find((model) => model.id === "claude-opus-4-8")).toMatchObject({
contextWindow: 1_000_000,
maxTokens: 128_000,
status: "deprecated",
replacedBy: "claude-opus-5",
});
const cliModels = manifest.modelCatalog?.providers?.["claude-cli"]?.models ?? [];
expect(cliModels.find((model) => model.id === "claude-opus-4-8")).toMatchObject({
contextWindow: 200_000,
maxTokens: 128_000,
status: "deprecated",
replacedBy: "claude-opus-5",
});
});
it("keeps only the dateless Claude Haiku 4.5 identifier in the static catalog", () => {
expect(manifest.modelCatalog?.discovery?.anthropic).toBe("refreshable");
const models = manifest.modelCatalog?.providers?.anthropic?.models ?? [];
expect(models.find((model) => model.id === "claude-haiku-4-5")).toEqual({
id: "claude-haiku-4-5",
name: "Claude Haiku 4.5",
reasoning: true,
input: ["text", "image"],
mediaInput: {
image: {
maxSidePx: 1568,
preferredSidePx: 1568,
tokenMode: "provider",
},
},
contextWindow: 200000,
maxTokens: 64000,
compat: { codeMode: "preferred" },
});
expect(models.find((model) => model.id === "claude-haiku-4-5-20251001")).toBeUndefined();
});
});