FN-7291: add Claude Sonnet 5 Anthropic support

Adds Claude Sonnet 5 to Anthropic model catalogs, pricing, and pi execution paths.

- Register supplemental direct Anthropic model metadata for Claude Sonnet 5 with deduping.
- Surface the model through dashboard model routes and engine session creation for non-Claude-CLI pi sessions.
- Add Claude CLI provider metadata, pricing coverage, tests, and a published package changeset.

Files changed:
 .changeset/fn-7291-sonnet-5-anthropic.md           |   7 ++
 packages/core/src/__tests__/model-pricing.test.ts  |  28 +++++
 packages/core/src/anthropic-models.ts              | 127 +++++++++++++++++++++
 packages/core/src/index.ts                         |   7 ++
 packages/core/src/model-pricing.ts                 |  13 ++-
 .../dashboard/src/__tests__/routes-auth.test.ts    |  67 +++++++++++
 packages/dashboard/src/routes.ts                   |   6 +-
 .../dashboard/src/routes/register-model-routes.ts  |   5 +-
 .../src/__tests__/pi-create-fn-agent.test.ts       |  67 +++++++++++
 .../src/cli-agent/adapters/__tests__/pi.test.ts    |  16 +++
 packages/engine/src/pi.ts                          |   2 +
 packages/pi-claude-cli/index.ts                    |  13 +++
 .../src/__tests__/process-manager.test.ts          |   4 +-
 .../pi-claude-cli/src/__tests__/provider.test.ts   |  31 ++++-
 14 files changed, 384 insertions(+), 9 deletions(-)

Fusion-Task-Id: FN-7291

Fusion-Task-Lineage: 6494fce8-4101-47f5-9137-306408fd7920

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-06-30 13:20:27 -07:00
parent 7e74fe3a9b
commit 2335a07620
14 changed files with 384 additions and 9 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": minor
---
summary: Add Claude Sonnet 5 across Anthropic model selection and execution paths.
category: feature
dev: Adds supplemental direct Anthropic and pi-claude-cli model metadata plus pricing for `claude-sonnet-5`.

View File

@@ -36,6 +36,25 @@ describe("model-pricing", () => {
expect(result.usd).toBeCloseTo(10.0, 2);
});
it("prices Claude Sonnet 5 for Anthropic and bare-model fallback", () => {
// claude-sonnet-5 introductory pricing: input $2/1M, output $10/1M,
// cache read $0.20/1M, 5m cache write $2.50/1M.
const usage = {
inputTokens: 1_000_000,
outputTokens: 200_000,
cachedTokens: 500_000,
cacheWriteTokens: 400_000,
};
const anthropic = costFor(usage, { provider: "anthropic", model: "claude-sonnet-5" });
expect(anthropic.unavailable).toBe(false);
expect(anthropic.usd).toBeCloseTo(5.1, 3);
const bare = costFor(usage, { model: "claude-sonnet-5" });
expect(bare.unavailable).toBe(false);
expect(bare.usd).toBeCloseTo(5.1, 3);
});
it("prices OpenAI Codex GPT-5 models instead of reporting unavailable", () => {
// gpt-5-codex: input $1.25/1M, output $10/1M.
// 1,000,000 input + 200,000 output = 1.25 + 2.00 = 3.25
@@ -187,6 +206,15 @@ describe("model-pricing", () => {
).toBe(MODEL_PRICING["openai-codex:gpt-5-codex"]);
});
it("resolves Claude Sonnet 5 by explicit provider and bare model", () => {
expect(
lookupPricing({ provider: " Anthropic ", model: " Claude-Sonnet-5 " }),
).toBe(MODEL_PRICING["anthropic:claude-sonnet-5"]);
expect(lookupPricing({ model: "claude-sonnet-5" })).toBe(
MODEL_PRICING["anthropic:claude-sonnet-5"],
);
});
it("falls back to a bare model id when provider is unset", () => {
expect(lookupPricing({ model: "gemini-2.5-pro" })).toBe(
MODEL_PRICING["google:gemini-2.5-pro"],

View File

@@ -0,0 +1,127 @@
type AnthropicModelInput = "text" | "image";
export const ANTHROPIC_PROVIDER_ID = "anthropic";
export const CLAUDE_SONNET_5_MODEL_ID = "claude-sonnet-5";
interface AnthropicModelRegistration {
id: string;
name: string;
reasoning: boolean;
input: AnthropicModelInput[];
cost: {
input: number;
output: number;
cacheRead: number;
cacheWrite: number;
};
contextWindow: number;
maxTokens: number;
compat?: Record<string, unknown>;
}
export interface AnthropicProviderRegistration {
name: string;
baseUrl: string;
apiKey: string;
api: "anthropic-messages";
models: AnthropicModelRegistration[];
}
/*
* FNXC:ModelCatalog 2026-06-30-12:22:
* Claude Sonnet 5 support must not depend on the installed pi-ai catalog version or on the Claude CLI provider. Keep this supplemental Anthropic registration shared by engine sessions and dashboard model routes, and dedupe by model id so upstream catalog catch-up does not create duplicate picker rows.
*/
export const SUPPLEMENTAL_ANTHROPIC_PROVIDER_REGISTRATION: AnthropicProviderRegistration = {
name: "Anthropic",
baseUrl: "https://api.anthropic.com/v1",
apiKey: "$ANTHROPIC_API_KEY",
api: "anthropic-messages",
models: [
{
id: CLAUDE_SONNET_5_MODEL_ID,
name: "Claude Sonnet 5",
reasoning: true,
input: ["text", "image"],
cost: {
input: 2,
output: 10,
cacheRead: 0.2,
cacheWrite: 2.5,
},
contextWindow: 1_000_000,
maxTokens: 128_000,
compat: {
supportsDeveloperRole: false,
},
},
],
};
type AnthropicModelLike = Partial<Omit<AnthropicModelRegistration, "name" | "compat">> & {
id: string;
name?: unknown;
provider?: string;
compat?: unknown;
};
interface AnthropicModelRegistryLike {
registerProvider(providerName: string, config: AnthropicProviderRegistration): void;
getAll?: () => AnthropicModelLike[];
}
type RegistryWithProviderState = AnthropicModelRegistryLike & {
registeredProviders?: Map<string, Partial<AnthropicProviderRegistration>>;
};
function toAnthropicModelRegistration(model: AnthropicModelLike): AnthropicModelRegistration {
const supplemental = SUPPLEMENTAL_ANTHROPIC_PROVIDER_REGISTRATION.models.find((entry) => entry.id === model.id);
return {
id: model.id,
name: String(model.name ?? supplemental?.name ?? model.id),
reasoning: model.reasoning ?? supplemental?.reasoning ?? false,
input: Array.isArray(model.input) ? model.input as AnthropicModelInput[] : supplemental?.input ?? ["text"],
cost: model.cost ?? supplemental?.cost ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: Number(model.contextWindow ?? supplemental?.contextWindow ?? 0),
maxTokens: Number(model.maxTokens ?? supplemental?.maxTokens ?? 0),
compat: typeof model.compat === "object" && model.compat !== null
? { ...(model.compat as Record<string, unknown>) }
: supplemental?.compat ? { ...supplemental.compat } : undefined,
};
}
function cloneAnthropicProviderRegistration(config: AnthropicProviderRegistration): AnthropicProviderRegistration {
return {
...config,
models: config.models.map((model) => toAnthropicModelRegistration(model)),
};
}
export function mergeSupplementalAnthropicModels(
modelRegistry: AnthropicModelRegistryLike,
logWarning: (message: string) => void = () => {},
): void {
try {
const registryWithState = modelRegistry as RegistryWithProviderState;
const registeredProvider = registryWithState.registeredProviders?.get(ANTHROPIC_PROVIDER_ID);
const registeredModels = registeredProvider?.models?.map((model) => toAnthropicModelRegistration(model)) ?? [];
const currentModels = registeredModels.length > 0
? registeredModels
: modelRegistry.getAll?.()
.filter((model) => model.provider === ANTHROPIC_PROVIDER_ID)
.map((model) => toAnthropicModelRegistration(model)) ?? [];
const currentModelIds = new Set(currentModels.map((model) => model.id));
const missingModels = SUPPLEMENTAL_ANTHROPIC_PROVIDER_REGISTRATION.models
.filter((model) => !currentModelIds.has(model.id));
if (missingModels.length === 0) return;
modelRegistry.registerProvider(ANTHROPIC_PROVIDER_ID, {
...cloneAnthropicProviderRegistration(SUPPLEMENTAL_ANTHROPIC_PROVIDER_REGISTRATION),
...registeredProvider,
models: [...currentModels, ...missingModels.map((model) => toAnthropicModelRegistration(model))],
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
logWarning(`Failed to merge supplemental ${ANTHROPIC_PROVIDER_ID} models: ${message}`);
}
}

View File

@@ -16,6 +16,13 @@ export type {
EntryPointBranchAssignment,
} from "./branch-assignment.js";
export { customProviderRegistryKey } from "./custom-provider-key.js";
export {
ANTHROPIC_PROVIDER_ID,
CLAUDE_SONNET_5_MODEL_ID,
SUPPLEMENTAL_ANTHROPIC_PROVIDER_REGISTRATION,
mergeSupplementalAnthropicModels,
} from "./anthropic-models.js";
export type { AnthropicProviderRegistration } from "./anthropic-models.js";
export { detectImageMimeFromBytes } from "./image-mime.js";
export type { DetectedImageMime } from "./image-mime.js";
export { redactSecrets } from "./redact-secrets.js";

View File

@@ -28,7 +28,7 @@
* The date the rates in {@link MODEL_PRICING} were last verified, ISO-8601.
* Bump this whenever you edit a rate. Surfaced in the UI as "prices as of".
*/
export const pricingAsOf = "2026-06-21";
export const pricingAsOf = "2026-06-30";
/**
* Pricing entries older than this (relative to a caller-supplied `now`) are
@@ -104,6 +104,17 @@ export interface CostResult {
export const MODEL_PRICING: Readonly<Record<string, ModelPricing>> = {
// ── Anthropic Claude ────────────────────────────────────────────────
// input / output / cacheRead(0.1×) / cacheWrite(1.25×, 5-min TTL)
/*
* FNXC:ModelCatalog 2026-06-30-12:10:
* Anthropic's docs publish Claude Sonnet 5 as the dateless pinned API ID `claude-sonnet-5`. Keep it in Fusion's hand-maintained support data before upstream pi-ai catalogs necessarily refresh so direct Anthropic runtime and cost surfaces can resolve it consistently.
*/
"anthropic:claude-sonnet-5": {
inputPer1M: 2,
outputPer1M: 10,
cacheReadPer1M: 0.2,
cacheWritePer1M: 2.5,
source: "platform.claude.com/docs/en/pricing#claude-sonnet-5-introductory-pricing",
},
"anthropic:claude-opus-4-8": {
inputPer1M: 5,
outputPer1M: 25,

View File

@@ -5,6 +5,7 @@ import express from "express";
import http from "node:http";
import { EventEmitter } from "node:events";
import { existsSync, mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import * as fsPromises from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { execFileSync } from "node:child_process";
@@ -328,6 +329,20 @@ function createMockModelRegistry(overrides: Partial<ModelRegistryLike> = {}): Mo
};
}
function createMutableModelRegistry(initialModels: Array<Record<string, any>>): ModelRegistryLike & { models: Array<Record<string, any>> } {
const registry = {
models: [...initialModels],
refresh: vi.fn(),
getAll: vi.fn(() => registry.models as any),
getAvailable: vi.fn(() => registry.models as any),
registerProvider: vi.fn((providerName: string, config: { models?: Array<Record<string, any>> }) => {
registry.models = registry.models.filter((model) => model.provider !== providerName);
registry.models.push(...(config.models ?? []).map((model) => ({ ...model, provider: providerName })));
}),
};
return registry;
}
describe("GET /models", () => {
let store: TaskStore;
@@ -383,6 +398,58 @@ describe("GET /models", () => {
expect(res.body.models).toEqual([]);
});
it("adds Claude Sonnet 5 for configured direct Anthropic users without relying on Claude CLI", async () => {
const modelRegistry = createMutableModelRegistry([
{ id: "claude-sonnet-4-5", name: "Claude Sonnet 4.5", provider: "anthropic", reasoning: true, contextWindow: 200000 },
{ id: "gpt-4o", name: "GPT-4o", provider: "openai", reasoning: false, contextWindow: 128000 },
]);
const res = await GET(buildApp(modelRegistry), "/api/models");
expect(res.status).toBe(200);
expect(res.body.models).toEqual(expect.arrayContaining([
expect.objectContaining({ provider: "anthropic", id: "claude-sonnet-5", name: "Claude Sonnet 5", reasoning: true, contextWindow: 1_000_000 }),
]));
expect(modelRegistry.registerProvider).toHaveBeenCalledWith("anthropic", expect.objectContaining({
models: expect.arrayContaining([expect.objectContaining({ id: "claude-sonnet-5" })]),
}));
});
it("does not expose Claude Sonnet 5 when direct Anthropic is not configured", async () => {
const readFileSpy = vi.spyOn(fsPromises, "readFile").mockImplementation(async (path: any) => {
const value = String(path);
if (value.endsWith("auth.json")) return "{}" as never;
if (value.endsWith("models.json")) return '{"providers":{}}' as never;
return "{}" as never;
});
try {
const modelRegistry = createMutableModelRegistry([
{ id: "claude-sonnet-4-5", name: "Claude Sonnet 4.5", provider: "anthropic", reasoning: true, contextWindow: 200000 },
]);
const res = await GET(buildApp(modelRegistry), "/api/models");
expect(res.status).toBe(200);
expect(res.body.models).toEqual([]);
expect(modelRegistry.models.some((model) => model.id === "claude-sonnet-5")).toBe(true);
} finally {
readFileSpy.mockRestore();
}
});
it("does not duplicate Claude Sonnet 5 when upstream registry already includes it", async () => {
const modelRegistry = createMutableModelRegistry([
{ id: "claude-sonnet-5", name: "Claude Sonnet 5 Upstream", provider: "anthropic", reasoning: true, contextWindow: 1_000_000, maxTokens: 128_000 },
]);
const res = await GET(buildApp(modelRegistry), "/api/models");
expect(res.status).toBe(200);
const sonnetFiveRows = res.body.models.filter((model: { provider: string; id: string }) => model.provider === "anthropic" && model.id === "claude-sonnet-5");
expect(sonnetFiveRows).toHaveLength(1);
expect(modelRegistry.registerProvider).not.toHaveBeenCalled();
});
// Regression guard: FN-2370's auto-resolved squash inverted this filter,
// emptying every model picker in the UI. The filter is small but the
// failure mode is silent and project-wide — keep these tests close to the

View File

@@ -13,7 +13,7 @@ import * as nodeFs from "node:fs";
import os from "node:os";
import v8 from "node:v8";
import type { TaskStore, ScheduleType, ActivityEventType, ModelPreset, RoutineTriggerType, McpServerDefinition } from "@fusion/core";
import type { AnthropicProviderRegistration, TaskStore, ScheduleType, ActivityEventType, ModelPreset, RoutineTriggerType, McpServerDefinition } from "@fusion/core";
import {
type Task,
type PiExtensionEntry,
@@ -206,6 +206,10 @@ export interface ModelRegistryLike {
refresh(): void;
/** Get models that have auth configured. */
getAvailable(): Array<{ id: string; name: string; provider: string; reasoning: boolean; contextWindow: number }>;
/** Optional pi ModelRegistry surface used for supplemental model registration. */
getAll?: () => Array<{ id: string; name?: string; provider: string; reasoning?: boolean; input?: string[]; cost?: { input: number; output: number; cacheRead: number; cacheWrite: number }; contextWindow?: number; maxTokens?: number; compat?: unknown }>;
/** Optional pi ModelRegistry surface used for supplemental model registration. */
registerProvider?: (providerName: string, config: AnthropicProviderRegistration) => void;
}
/**

View File

@@ -1,7 +1,7 @@
import { access, readFile } from "node:fs/promises";
import { homedir } from "node:os";
import { join } from "node:path";
import { customProviderRegistryKey, resolvePlanningSettingsModel } from "@fusion/core";
import { customProviderRegistryKey, mergeSupplementalAnthropicModels, resolvePlanningSettingsModel } from "@fusion/core";
import type { CustomProvider } from "@fusion/core";
import { ApiError } from "../api-error.js";
import type { ApiRouteRegistrar } from "./types.js";
@@ -129,6 +129,9 @@ export const registerModelRoutes: ApiRouteRegistrar = (ctx) => {
try {
options.modelRegistry.refresh();
if (options.modelRegistry.registerProvider) {
mergeSupplementalAnthropicModels(options.modelRegistry as Parameters<typeof mergeSupplementalAnthropicModels>[0], (message) => runtimeLogger.child("models").warn(message));
}
let models = options.modelRegistry.getAvailable().map((m) => ({
provider: m.provider,
id: m.id,

View File

@@ -1108,6 +1108,7 @@ describe("createFnAgent", () => {
readFileSyncMock.mockReturnValue("{}");
realpathSyncNativeMock.mockImplementation((path: PathLike) => String(path));
readCustomProvidersMock.mockReturnValue([]);
getAllMock.mockReturnValue([]);
findMock.mockImplementation((provider: string, modelId: string) => ({ provider, id: modelId }));
// #1675: re-establish default auth + session-id mock returns after clearAllMocks.
getApiKeyAndHeadersMock.mockResolvedValue({ ok: true, apiKey: undefined, headers: undefined });
@@ -1624,6 +1625,72 @@ describe("createFnAgent", () => {
});
});
it("resolves direct Anthropic Claude Sonnet 5 when the mocked registry initially lacks it", async () => {
getAllMock.mockReturnValueOnce([]);
findMock.mockImplementation((provider: string, modelId: string) => {
if (provider === "anthropic" && modelId === "claude-sonnet-5") {
const anthropicRegistration = registerProviderMock.mock.calls.find(([name]) => name === "anthropic")?.[1] as { models?: Array<{ id: string; name: string }> } | undefined;
const registeredModel = anthropicRegistration?.models?.find((model) => model.id === modelId);
return registeredModel ? { ...registeredModel, provider } : undefined;
}
return { provider, id: modelId };
});
const { createFnAgent } = await import("../pi.js");
const result = await createFnAgent({
cwd: "/tmp",
systemPrompt: "test",
tools: "readonly",
defaultProvider: "anthropic",
defaultModelId: "claude-sonnet-5",
});
expect(registerProviderMock).toHaveBeenCalledWith("anthropic", expect.objectContaining({
api: "anthropic-messages",
models: expect.arrayContaining([expect.objectContaining({
id: "claude-sonnet-5",
name: "Claude Sonnet 5",
contextWindow: 1_000_000,
maxTokens: 128_000,
})]),
}));
expect(createAgentSessionMock).toHaveBeenCalledWith(expect.objectContaining({
model: expect.objectContaining({ provider: "anthropic", id: "claude-sonnet-5" }),
}));
expect((result.session as { model?: unknown }).model).toEqual(expect.objectContaining({
provider: "anthropic",
id: "claude-sonnet-5",
}));
});
it("does not duplicate Claude Sonnet 5 when the Anthropic registry already has it", async () => {
getAllMock.mockReturnValue([
{
provider: "anthropic",
id: "claude-sonnet-5",
name: "Claude Sonnet 5 Upstream",
reasoning: true,
input: ["text", "image"],
cost: { input: 2, output: 10, cacheRead: 0.2, cacheWrite: 2.5 },
contextWindow: 1_000_000,
maxTokens: 128_000,
},
]);
findMock.mockImplementation((provider: string, modelId: string) => ({ provider, id: modelId }));
const { createFnAgent } = await import("../pi.js");
await createFnAgent({
cwd: "/tmp",
systemPrompt: "test",
tools: "readonly",
defaultProvider: "anthropic",
defaultModelId: "claude-sonnet-5",
});
const anthropicRegistrations = registerProviderMock.mock.calls.filter(([name]) => name === "anthropic");
expect(anthropicRegistrations).toHaveLength(0);
});
it("backfills the resolved model onto sessions that do not mirror it", async () => {
const session = {
prompt: vi.fn(),

View File

@@ -58,6 +58,22 @@ describe("piAdapter — buildLaunch", () => {
]);
});
it("forwards direct Anthropic Claude Sonnet 5 without Claude CLI routing", () => {
const spec = piAdapter.buildLaunch({
settings: { provider: "anthropic", model: "claude-sonnet-5", sessionDir: "/tmp/sess/pi" },
posture: null,
});
expect(spec.command).toBe("pi");
expect(spec.args).toEqual([
"--provider",
"anthropic",
"--model",
"claude-sonnet-5",
"--session-dir",
"/tmp/sess/pi",
]);
});
it("widens tool access ONLY when posture.autoApprove is true", () => {
const off = piAdapter.buildLaunch({ settings: {}, posture: { autoApprove: false } });
expect(off.args).not.toContain("--tools");

View File

@@ -43,6 +43,7 @@ import {
reconcileClaudeCliPaths,
reconcileDroidCliPaths,
mergeBuiltInZaiProviderModels,
mergeSupplementalAnthropicModels,
registerBuiltInZaiProvider,
resolvePiExtensionProjectRoot,
} from "@fusion/core";
@@ -2039,6 +2040,7 @@ export async function createFnAgent(options: AgentOptions): Promise<AgentResult>
}
}
modelRegistry.refresh();
mergeSupplementalAnthropicModels(modelRegistry, (message) => extensionsLog.warn(message));
// Build the pi built-in tool set. We deliberately do NOT use the bundled
// `createCodingTools` / `createReadOnlyTools` presets — they're missing

View File

@@ -190,6 +190,19 @@ export default function (pi: ExtensionAPI) {
// catalog catches up.
// https://platform.claude.com/docs/en/about-claude/models/overview
const extraModels: typeof catalogModels = [
/*
* FNXC:ModelCatalog 2026-06-30-12:31:
* The vendored Claude CLI provider has its own model list because it exposes `pi-claude-cli` independently from direct `anthropic`. Add Claude Sonnet 5 here as supplemental metadata so Claude CLI users can select it before the upstream pi-ai catalog catches up, while the dedupe below prevents duplicate rows after it does.
*/
{
id: "claude-sonnet-5",
name: "Claude Sonnet 5",
reasoning: true,
input: ["text", "image"],
cost: { input: 2, output: 10, cacheRead: 0.2, cacheWrite: 2.5 },
contextWindow: 1_000_000,
maxTokens: 128_000,
},
{
id: "claude-opus-4-7",
name: "Claude Opus 4.7",

View File

@@ -61,14 +61,14 @@ describe("buildClaudeSpawnArgs", () => {
});
it("builds args including model and optional session/mcp flags", () => {
const args = buildClaudeSpawnArgs("claude-sonnet-4-6", undefined, {
const args = buildClaudeSpawnArgs("claude-sonnet-5", undefined, {
resumeSessionId: "sess-1",
effort: "high",
mcpConfigPath: "/tmp/mcp.json",
});
expect(args).toContain("--model");
expect(args).toContain("claude-sonnet-4-6");
expect(args[args.indexOf("--model") + 1]).toBe("claude-sonnet-5");
expect(args).toContain("--resume");
expect(args).toContain("sess-1");
expect(args).toContain("--effort");

View File

@@ -127,6 +127,7 @@ describe("provider registration (default export)", () => {
const modelIds = new Set(config.models.map((m: { id: string }) => m.id));
for (const id of [
"claude-sonnet-5",
"claude-opus-4-7",
"claude-sonnet-4-6",
"claude-sonnet-4-5",
@@ -134,6 +135,15 @@ describe("provider registration (default export)", () => {
]) {
expect(modelIds.has(id)).toBe(true);
}
expect(config.models.find((m: { id: string }) => m.id === "claude-sonnet-5")).toMatchObject({
name: "Claude Sonnet 5",
reasoning: true,
input: ["text", "image"],
cost: { input: 2, output: 10, cacheRead: 0.2, cacheWrite: 2.5 },
contextWindow: 1_000_000,
maxTokens: 128_000,
});
});
it("deduplicates extra models when catalog already includes them", async () => {
@@ -143,6 +153,17 @@ describe("provider registration (default export)", () => {
getModelsMock.mockReturnValueOnce([
...mockModels,
{
id: "claude-sonnet-5",
name: "Claude Sonnet 5 Upstream",
api: "anthropic",
provider: "anthropic",
reasoning: true,
input: ["text", "image"],
cost: { input: 2, output: 10, cacheRead: 0.2, cacheWrite: 2.5 },
contextWindow: 1_000_000,
maxTokens: 128_000,
} as any,
{
id: "claude-sonnet-4-6",
name: "Claude Sonnet 4.6",
@@ -160,10 +181,12 @@ describe("provider registration (default export)", () => {
mod.default(mockPi);
const config = registerProvider.mock.calls[0][1];
const matches = config.models.filter(
(m: { id: string }) => m.id === "claude-sonnet-4-6",
);
expect(matches).toHaveLength(1);
for (const id of ["claude-sonnet-5", "claude-sonnet-4-6"]) {
const matches = config.models.filter(
(m: { id: string }) => m.id === id,
);
expect(matches).toHaveLength(1);
}
});
});