fix(server): hardcode Claude models with 1M context support
The SDK's supportedModels() API hides the 1M context variant behind a "default" alias and doesn't expose it as a selectable model. Replace dynamic SDK discovery with a hardcoded catalog that exposes both claude-opus-4-6[1m] (1M context) and claude-opus-4-6 (200k) as distinct models. Delete sdk-model-resolver which parsed SDK descriptions.
This commit is contained in:
@@ -1,5 +1,4 @@
|
|||||||
import { describe, expect, test, vi } from "vitest";
|
import { describe, expect, test, vi } from "vitest";
|
||||||
import type { ModelInfo } from "@anthropic-ai/claude-agent-sdk";
|
|
||||||
|
|
||||||
import { createTestLogger } from "../../../test-utils/test-logger.js";
|
import { createTestLogger } from "../../../test-utils/test-logger.js";
|
||||||
import { ClaudeAgentClient, convertClaudeHistoryEntry } from "./claude-agent.js";
|
import { ClaudeAgentClient, convertClaudeHistoryEntry } from "./claude-agent.js";
|
||||||
@@ -242,119 +241,23 @@ describe("convertClaudeHistoryEntry", () => {
|
|||||||
describe("ClaudeAgentClient.listModels", () => {
|
describe("ClaudeAgentClient.listModels", () => {
|
||||||
const logger = createTestLogger();
|
const logger = createTestLogger();
|
||||||
|
|
||||||
function createSupportedModelsQueryMock(models: ModelInfo[]) {
|
test("returns hardcoded claude models", async () => {
|
||||||
return {
|
|
||||||
supportedModels: vi.fn(async () => models),
|
|
||||||
return: vi.fn(async () => ({ done: true, value: undefined })),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
test("returns models with required fields", async () => {
|
|
||||||
const client = new ClaudeAgentClient({ logger });
|
const client = new ClaudeAgentClient({ logger });
|
||||||
const models = await client.listModels();
|
const models = await client.listModels();
|
||||||
|
|
||||||
expect(Array.isArray(models)).toBe(true);
|
expect(models.map((m) => m.id)).toEqual([
|
||||||
expect(models.length).toBeGreaterThan(0);
|
"claude-opus-4-6[1m]",
|
||||||
|
"claude-opus-4-6",
|
||||||
|
"claude-sonnet-4-6",
|
||||||
|
"claude-haiku-4-5",
|
||||||
|
]);
|
||||||
|
|
||||||
for (const model of models) {
|
for (const model of models) {
|
||||||
expect(model.provider).toBe("claude");
|
expect(model.provider).toBe("claude");
|
||||||
expect(typeof model.id).toBe("string");
|
|
||||||
expect(model.id.length).toBeGreaterThan(0);
|
|
||||||
expect(typeof model.label).toBe("string");
|
|
||||||
expect(model.label.length).toBeGreaterThan(0);
|
expect(model.label.length).toBeGreaterThan(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
const modelIds = models.map((model) => model.id);
|
const defaultModel = models.find((m) => m.isDefault);
|
||||||
expect(
|
expect(defaultModel?.id).toBe("claude-opus-4-6");
|
||||||
modelIds.some(
|
|
||||||
(id) =>
|
|
||||||
id.includes("claude") ||
|
|
||||||
id.includes("sonnet") ||
|
|
||||||
id.includes("opus") ||
|
|
||||||
id.includes("haiku"),
|
|
||||||
),
|
|
||||||
).toBe(true);
|
|
||||||
}, 60_000);
|
|
||||||
|
|
||||||
test("prefers provider-discovered Claude defaults and effort levels", async () => {
|
|
||||||
const queryMock = createSupportedModelsQueryMock([
|
|
||||||
{
|
|
||||||
value: "default",
|
|
||||||
displayName: "Default (recommended)",
|
|
||||||
description: "Sonnet 4.6 · Best for everyday tasks",
|
|
||||||
supportsEffort: true,
|
|
||||||
supportedEffortLevels: ["low", "medium", "high", "max"],
|
|
||||||
supportsAdaptiveThinking: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "opus",
|
|
||||||
displayName: "Opus",
|
|
||||||
description: "Opus 4.6 · Most capable for complex work",
|
|
||||||
supportsEffort: true,
|
|
||||||
supportedEffortLevels: ["low", "medium", "high", "max"],
|
|
||||||
supportsAdaptiveThinking: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "haiku",
|
|
||||||
displayName: "Haiku",
|
|
||||||
description: "Haiku 4.5 · Fastest for quick answers",
|
|
||||||
},
|
|
||||||
] satisfies ModelInfo[]);
|
|
||||||
const queryFactory = vi.fn(() => queryMock);
|
|
||||||
const client = new ClaudeAgentClient({
|
|
||||||
logger,
|
|
||||||
queryFactory: queryFactory as never,
|
|
||||||
});
|
|
||||||
|
|
||||||
const models = await client.listModels({ cwd: process.cwd() });
|
|
||||||
|
|
||||||
expect(queryFactory).toHaveBeenCalledTimes(1);
|
|
||||||
expect(queryMock.supportedModels).toHaveBeenCalledTimes(1);
|
|
||||||
expect(queryMock.return).toHaveBeenCalledTimes(1);
|
|
||||||
expect(models).toEqual([
|
|
||||||
expect.objectContaining({
|
|
||||||
id: "claude-sonnet-4-6",
|
|
||||||
isDefault: true,
|
|
||||||
label: "Sonnet 4.6",
|
|
||||||
thinkingOptions: [
|
|
||||||
{ id: "low", label: "Low" },
|
|
||||||
{ id: "medium", label: "Medium" },
|
|
||||||
{ id: "high", label: "High" },
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
expect.objectContaining({
|
|
||||||
id: "claude-opus-4-6",
|
|
||||||
label: "Opus 4.6",
|
|
||||||
}),
|
|
||||||
expect.objectContaining({
|
|
||||||
id: "claude-haiku-4-5",
|
|
||||||
label: "Haiku 4.5",
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("preserves SDK ids even when descriptions are weak", async () => {
|
|
||||||
const queryMock = createSupportedModelsQueryMock([
|
|
||||||
{
|
|
||||||
value: "default",
|
|
||||||
displayName: "Default (recommended)",
|
|
||||||
description: "Recommended model",
|
|
||||||
},
|
|
||||||
] satisfies ModelInfo[]);
|
|
||||||
const client = new ClaudeAgentClient({
|
|
||||||
logger,
|
|
||||||
queryFactory: vi.fn(() => queryMock) as never,
|
|
||||||
});
|
|
||||||
|
|
||||||
const models = await client.listModels({ cwd: process.cwd() });
|
|
||||||
|
|
||||||
expect(models).toEqual([
|
|
||||||
expect.objectContaining({
|
|
||||||
id: "default",
|
|
||||||
label: "Default (recommended)",
|
|
||||||
description: "Recommended model",
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
expect(queryMock.return).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
type AgentDefinition,
|
type AgentDefinition,
|
||||||
type CanUseTool,
|
type CanUseTool,
|
||||||
type McpServerConfig as ClaudeSdkMcpServerConfig,
|
type McpServerConfig as ClaudeSdkMcpServerConfig,
|
||||||
type ModelInfo,
|
|
||||||
type Options,
|
type Options,
|
||||||
type PermissionMode,
|
type PermissionMode,
|
||||||
type PermissionResult,
|
type PermissionResult,
|
||||||
@@ -34,9 +34,9 @@ import {
|
|||||||
mapTaskNotificationUserContentToToolCall,
|
mapTaskNotificationUserContentToToolCall,
|
||||||
} from "./claude/task-notification-tool-call.js";
|
} from "./claude/task-notification-tool-call.js";
|
||||||
import {
|
import {
|
||||||
normalizeClaudeModelIdFromText,
|
getClaudeModels,
|
||||||
resolveClaudeModelsFromSdkModels,
|
normalizeClaudeRuntimeModelId,
|
||||||
} from "./claude/sdk-model-resolver.js";
|
} from "./claude/claude-models.js";
|
||||||
import { parsePartialJsonObject } from "./claude/partial-json.js";
|
import { parsePartialJsonObject } from "./claude/partial-json.js";
|
||||||
import { ClaudeSidechainTracker } from "./claude/sidechain-tracker.js";
|
import { ClaudeSidechainTracker } from "./claude/sidechain-tracker.js";
|
||||||
|
|
||||||
@@ -239,10 +239,6 @@ function applyRuntimeSettingsToClaudeOptions(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function createEmptyClaudePrompt(): AsyncGenerator<SDKUserMessage, void, undefined> {
|
|
||||||
return (async function* empty() {})();
|
|
||||||
}
|
|
||||||
|
|
||||||
function isClaudeThinkingEffort(value: string | null | undefined): value is ClaudeThinkingEffort {
|
function isClaudeThinkingEffort(value: string | null | undefined): value is ClaudeThinkingEffort {
|
||||||
return value === "low" || value === "medium" || value === "high" || value === "max";
|
return value === "low" || value === "medium" || value === "high" || value === "max";
|
||||||
}
|
}
|
||||||
@@ -1050,33 +1046,8 @@ export class ClaudeAgentClient implements AgentClient {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async listModels(options?: ListModelsOptions): Promise<AgentModelDefinition[]> {
|
async listModels(_options?: ListModelsOptions): Promise<AgentModelDefinition[]> {
|
||||||
const claudeQuery = this.queryFactory({
|
return getClaudeModels();
|
||||||
prompt: createEmptyClaudePrompt(),
|
|
||||||
options: applyRuntimeSettingsToClaudeOptions(
|
|
||||||
{
|
|
||||||
cwd: options?.cwd ?? process.cwd(),
|
|
||||||
permissionMode: "plan",
|
|
||||||
includePartialMessages: false,
|
|
||||||
settingSources: CLAUDE_SETTING_SOURCES,
|
|
||||||
},
|
|
||||||
this.runtimeSettings,
|
|
||||||
),
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
const supportedModels = await claudeQuery.supportedModels();
|
|
||||||
return resolveClaudeModelsFromSdkModels(supportedModels as ModelInfo[]);
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.warn({ err: error }, "Failed to query Claude supportedModels()");
|
|
||||||
throw error;
|
|
||||||
} finally {
|
|
||||||
try {
|
|
||||||
await claudeQuery.return?.();
|
|
||||||
} catch {
|
|
||||||
// ignore control-plane shutdown errors
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2698,7 +2669,7 @@ class ClaudeAgentSession implements AgentSession {
|
|||||||
this.currentMode = message.permissionMode;
|
this.currentMode = message.permissionMode;
|
||||||
this.persistence = null;
|
this.persistence = null;
|
||||||
if (message.model) {
|
if (message.model) {
|
||||||
const normalizedRuntimeModel = normalizeClaudeModelIdFromText(message.model);
|
const normalizedRuntimeModel = normalizeClaudeRuntimeModelId(message.model);
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
{ runtimeModel: message.model, normalizedRuntimeModel },
|
{ runtimeModel: message.model, normalizedRuntimeModel },
|
||||||
"Captured runtime model from SDK init",
|
"Captured runtime model from SDK init",
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import { getClaudeModels, normalizeClaudeRuntimeModelId } from "./claude-models.js";
|
||||||
|
|
||||||
|
describe("getClaudeModels", () => {
|
||||||
|
it("returns all claude models", () => {
|
||||||
|
const models = getClaudeModels();
|
||||||
|
expect(models.map((m) => m.id)).toEqual([
|
||||||
|
"claude-opus-4-6[1m]",
|
||||||
|
"claude-opus-4-6",
|
||||||
|
"claude-sonnet-4-6",
|
||||||
|
"claude-haiku-4-5",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("marks exactly one model as default", () => {
|
||||||
|
const models = getClaudeModels();
|
||||||
|
const defaults = models.filter((m) => m.isDefault);
|
||||||
|
expect(defaults).toHaveLength(1);
|
||||||
|
expect(defaults[0]!.id).toBe("claude-opus-4-6");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns fresh copies each call", () => {
|
||||||
|
const a = getClaudeModels();
|
||||||
|
const b = getClaudeModels();
|
||||||
|
expect(a).not.toBe(b);
|
||||||
|
expect(a[0]).not.toBe(b[0]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("normalizeClaudeRuntimeModelId", () => {
|
||||||
|
it("returns exact match for known model IDs", () => {
|
||||||
|
expect(normalizeClaudeRuntimeModelId("claude-opus-4-6")).toBe("claude-opus-4-6");
|
||||||
|
expect(normalizeClaudeRuntimeModelId("claude-opus-4-6[1m]")).toBe("claude-opus-4-6[1m]");
|
||||||
|
expect(normalizeClaudeRuntimeModelId("claude-sonnet-4-6")).toBe("claude-sonnet-4-6");
|
||||||
|
expect(normalizeClaudeRuntimeModelId("claude-haiku-4-5")).toBe("claude-haiku-4-5");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("normalizes dated model IDs to base model", () => {
|
||||||
|
expect(normalizeClaudeRuntimeModelId("claude-opus-4-6-20260101")).toBe("claude-opus-4-6");
|
||||||
|
expect(normalizeClaudeRuntimeModelId("claude-sonnet-4-6-20260101")).toBe("claude-sonnet-4-6");
|
||||||
|
expect(normalizeClaudeRuntimeModelId("claude-haiku-4-5-20251001")).toBe("claude-haiku-4-5");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves [1m] suffix from runtime model strings", () => {
|
||||||
|
expect(normalizeClaudeRuntimeModelId("claude-opus-4-6[1m]")).toBe("claude-opus-4-6[1m]");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns null for empty/null/undefined", () => {
|
||||||
|
expect(normalizeClaudeRuntimeModelId(null)).toBeNull();
|
||||||
|
expect(normalizeClaudeRuntimeModelId(undefined)).toBeNull();
|
||||||
|
expect(normalizeClaudeRuntimeModelId("")).toBeNull();
|
||||||
|
expect(normalizeClaudeRuntimeModelId(" ")).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns null for unrecognized strings", () => {
|
||||||
|
expect(normalizeClaudeRuntimeModelId("gpt-5")).toBeNull();
|
||||||
|
expect(normalizeClaudeRuntimeModelId("random")).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import type { AgentModelDefinition } from "../../agent-sdk-types.js";
|
||||||
|
|
||||||
|
const CLAUDE_THINKING_OPTIONS = [
|
||||||
|
{ id: "low", label: "Low" },
|
||||||
|
{ id: "medium", label: "Medium" },
|
||||||
|
{ id: "high", label: "High" },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const CLAUDE_MODELS: AgentModelDefinition[] = [
|
||||||
|
{
|
||||||
|
provider: "claude",
|
||||||
|
id: "claude-opus-4-6[1m]",
|
||||||
|
label: "Opus 4.6 1M",
|
||||||
|
description: "Opus 4.6 with 1M context window",
|
||||||
|
thinkingOptions: [...CLAUDE_THINKING_OPTIONS],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provider: "claude",
|
||||||
|
id: "claude-opus-4-6",
|
||||||
|
label: "Opus 4.6",
|
||||||
|
description: "Opus 4.6 · Most capable for complex work",
|
||||||
|
isDefault: true,
|
||||||
|
thinkingOptions: [...CLAUDE_THINKING_OPTIONS],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provider: "claude",
|
||||||
|
id: "claude-sonnet-4-6",
|
||||||
|
label: "Sonnet 4.6",
|
||||||
|
description: "Sonnet 4.6 · Best for everyday tasks",
|
||||||
|
thinkingOptions: [...CLAUDE_THINKING_OPTIONS],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provider: "claude",
|
||||||
|
id: "claude-haiku-4-5",
|
||||||
|
label: "Haiku 4.5",
|
||||||
|
description: "Haiku 4.5 · Fastest for quick answers",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function getClaudeModels(): AgentModelDefinition[] {
|
||||||
|
return CLAUDE_MODELS.map((model) => ({ ...model }));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize a runtime model string (from SDK init message) to a known model ID.
|
||||||
|
* Handles the `[1m]` suffix that the SDK appends for 1M context sessions.
|
||||||
|
*/
|
||||||
|
export function normalizeClaudeRuntimeModelId(
|
||||||
|
value: string | null | undefined,
|
||||||
|
): string | null {
|
||||||
|
const trimmed = typeof value === "string" ? value.trim() : "";
|
||||||
|
if (!trimmed) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for exact match first (handles claude-opus-4-6[1m] directly)
|
||||||
|
if (CLAUDE_MODELS.some((model) => model.id === trimmed)) {
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match: claude-{family}-{major}-{minor}[1m]? possibly followed by a date suffix
|
||||||
|
const runtimeMatch = trimmed.match(
|
||||||
|
/(?:claude-)?(opus|sonnet|haiku)[-_ ]+(\d+)[-.](\d+)(\[1m\])?/i,
|
||||||
|
);
|
||||||
|
if (!runtimeMatch) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const family = runtimeMatch[1]!.toLowerCase();
|
||||||
|
const major = runtimeMatch[2]!;
|
||||||
|
const minor = runtimeMatch[3]!;
|
||||||
|
const suffix = runtimeMatch[4] ?? "";
|
||||||
|
return `claude-${family}-${major}-${minor}${suffix}`;
|
||||||
|
}
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
|
||||||
import type { ModelInfo } from "@anthropic-ai/claude-agent-sdk";
|
|
||||||
|
|
||||||
import {
|
|
||||||
parseClaudeSdkModelDescriptorForTest,
|
|
||||||
resolveClaudeModelsFromSdkModels,
|
|
||||||
} from "./sdk-model-resolver.js";
|
|
||||||
|
|
||||||
describe("resolveClaudeModelsFromSdkModels", () => {
|
|
||||||
const sdkModels: ModelInfo[] = [
|
|
||||||
{
|
|
||||||
value: "default",
|
|
||||||
displayName: "Default (recommended)",
|
|
||||||
description: "Sonnet 4.6 · Best for everyday tasks",
|
|
||||||
supportsEffort: true,
|
|
||||||
supportedEffortLevels: ["low", "medium", "high", "max"],
|
|
||||||
supportsAdaptiveThinking: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "opus",
|
|
||||||
displayName: "Opus",
|
|
||||||
description: "Opus 4.6 · Most capable for complex work",
|
|
||||||
supportsEffort: true,
|
|
||||||
supportedEffortLevels: ["low", "medium", "high", "max"],
|
|
||||||
supportsAdaptiveThinking: true,
|
|
||||||
supportsFastMode: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "sonnet",
|
|
||||||
displayName: "Sonnet",
|
|
||||||
description: "Sonnet 4.6 · Best for everyday tasks",
|
|
||||||
supportsEffort: true,
|
|
||||||
supportedEffortLevels: ["low", "medium", "high", "max"],
|
|
||||||
supportsAdaptiveThinking: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "haiku",
|
|
||||||
displayName: "Haiku",
|
|
||||||
description: "Haiku 4.5 · Fastest for quick answers",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
it("parses family and version from SDK descriptions", () => {
|
|
||||||
expect(parseClaudeSdkModelDescriptorForTest(sdkModels[0]!)).toEqual({
|
|
||||||
family: "sonnet",
|
|
||||||
version: "4.6",
|
|
||||||
});
|
|
||||||
expect(parseClaudeSdkModelDescriptorForTest(sdkModels[1]!)).toEqual({
|
|
||||||
family: "opus",
|
|
||||||
version: "4.6",
|
|
||||||
});
|
|
||||||
expect(parseClaudeSdkModelDescriptorForTest(sdkModels[2]!)).toEqual({
|
|
||||||
family: "sonnet",
|
|
||||||
version: "4.6",
|
|
||||||
});
|
|
||||||
expect(parseClaudeSdkModelDescriptorForTest(sdkModels[3]!)).toEqual({
|
|
||||||
family: "haiku",
|
|
||||||
version: "4.5",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("maps SDK models to parsed Claude model ids", () => {
|
|
||||||
const models = resolveClaudeModelsFromSdkModels(sdkModels);
|
|
||||||
|
|
||||||
expect(models).toEqual([
|
|
||||||
expect.objectContaining({
|
|
||||||
provider: "claude",
|
|
||||||
id: "claude-sonnet-4-6",
|
|
||||||
label: "Sonnet 4.6",
|
|
||||||
isDefault: true,
|
|
||||||
thinkingOptions: [
|
|
||||||
{ id: "low", label: "Low" },
|
|
||||||
{ id: "medium", label: "Medium" },
|
|
||||||
{ id: "high", label: "High" },
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
expect.objectContaining({
|
|
||||||
provider: "claude",
|
|
||||||
id: "claude-opus-4-6",
|
|
||||||
label: "Opus 4.6",
|
|
||||||
}),
|
|
||||||
expect.objectContaining({
|
|
||||||
provider: "claude",
|
|
||||||
id: "claude-haiku-4-5",
|
|
||||||
label: "Haiku 4.5",
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,145 +0,0 @@
|
|||||||
import type { ModelInfo } from "@anthropic-ai/claude-agent-sdk";
|
|
||||||
|
|
||||||
import type { AgentModelDefinition, AgentSelectOption } from "../../agent-sdk-types.js";
|
|
||||||
|
|
||||||
type ParsedClaudeSdkModelDescriptor = {
|
|
||||||
family: "opus" | "sonnet" | "haiku";
|
|
||||||
version: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Claude may advertise effort levels that are not usable for all account types.
|
|
||||||
const DISABLED_CLAUDE_THINKING_EFFORT_LEVELS: readonly string[] = ["max"];
|
|
||||||
|
|
||||||
function normalizeWhitespace(value: string): string {
|
|
||||||
return value.replace(/\s+/g, " ").trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
function titleCase(value: string): string {
|
|
||||||
return value.charAt(0).toUpperCase() + value.slice(1).toLowerCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeClaudeVersionId(version: string): string {
|
|
||||||
return version.replace(/\./g, "-");
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildClaudeModelId(parsed: ParsedClaudeSdkModelDescriptor): string {
|
|
||||||
return `claude-${parsed.family}-${normalizeClaudeVersionId(parsed.version)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseClaudeSdkDescriptor(model: ModelInfo): ParsedClaudeSdkModelDescriptor | null {
|
|
||||||
const description = normalizeWhitespace(model.description ?? "");
|
|
||||||
if (!description) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const match = description.match(/\b(opus|sonnet|haiku)\s+(\d+(?:\.\d+)*)\b/i);
|
|
||||||
if (!match) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const family = match[1].toLowerCase() as ParsedClaudeSdkModelDescriptor["family"];
|
|
||||||
const version = match[2]!;
|
|
||||||
return { family, version };
|
|
||||||
}
|
|
||||||
|
|
||||||
export function normalizeClaudeModelIdFromText(value: string | null | undefined): string | null {
|
|
||||||
const normalized = normalizeWhitespace(value ?? "");
|
|
||||||
if (!normalized) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const runtimeMatch = normalized.match(/\b(opus|sonnet|haiku)[-_ ]+(\d+(?:[-.]\d+)*)\b/i);
|
|
||||||
if (!runtimeMatch) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const family = runtimeMatch[1]!.toLowerCase() as ParsedClaudeSdkModelDescriptor["family"];
|
|
||||||
const version = runtimeMatch[2]!.replace(/-/g, ".");
|
|
||||||
return buildClaudeModelId({ family, version });
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildModelLabel(model: ModelInfo): string {
|
|
||||||
const parsed = parseClaudeSdkDescriptor(model);
|
|
||||||
if (!parsed) {
|
|
||||||
return normalizeWhitespace(model.displayName || model.value);
|
|
||||||
}
|
|
||||||
return `${titleCase(parsed.family)} ${parsed.version}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildThinkingOptions(model: ModelInfo): {
|
|
||||||
thinkingOptions?: AgentSelectOption[];
|
|
||||||
defaultThinkingOptionId?: string;
|
|
||||||
} {
|
|
||||||
const effortLevels = (model.supportedEffortLevels ?? []).filter(
|
|
||||||
(level) => !DISABLED_CLAUDE_THINKING_EFFORT_LEVELS.includes(level),
|
|
||||||
);
|
|
||||||
if (!model.supportsEffort || effortLevels.length === 0) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
const thinkingOptions: AgentSelectOption[] = effortLevels.map((level) => ({
|
|
||||||
id: level,
|
|
||||||
label: titleCase(level),
|
|
||||||
}));
|
|
||||||
|
|
||||||
return {
|
|
||||||
thinkingOptions,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function resolveClaudeModelsFromSdkModels(models: ModelInfo[]): AgentModelDefinition[] {
|
|
||||||
const resolved = new Map<string, AgentModelDefinition>();
|
|
||||||
|
|
||||||
for (const model of models) {
|
|
||||||
const thinking = buildThinkingOptions(model);
|
|
||||||
const parsed = parseClaudeSdkDescriptor(model);
|
|
||||||
const id = parsed ? buildClaudeModelId(parsed) : model.value;
|
|
||||||
const existing = resolved.get(id);
|
|
||||||
resolved.set(id, {
|
|
||||||
provider: "claude",
|
|
||||||
id,
|
|
||||||
label: buildModelLabel(model),
|
|
||||||
description: normalizeWhitespace(model.description ?? model.displayName ?? model.value),
|
|
||||||
isDefault:
|
|
||||||
existing?.isDefault === true || model.value.trim().toLowerCase() === "default" || undefined,
|
|
||||||
...(thinking.thinkingOptions || existing?.thinkingOptions
|
|
||||||
? { thinkingOptions: thinking.thinkingOptions ?? existing?.thinkingOptions }
|
|
||||||
: {}),
|
|
||||||
...(thinking.defaultThinkingOptionId || existing?.defaultThinkingOptionId
|
|
||||||
? {
|
|
||||||
defaultThinkingOptionId:
|
|
||||||
thinking.defaultThinkingOptionId ?? existing?.defaultThinkingOptionId,
|
|
||||||
}
|
|
||||||
: {}),
|
|
||||||
metadata: {
|
|
||||||
sdkValues: Array.from(
|
|
||||||
new Set([...(Array.isArray(existing?.metadata?.sdkValues) ? existing.metadata.sdkValues : []), model.value]),
|
|
||||||
),
|
|
||||||
sdkDisplayNames: Array.from(
|
|
||||||
new Set([
|
|
||||||
...(Array.isArray(existing?.metadata?.sdkDisplayNames) ? existing.metadata.sdkDisplayNames : []),
|
|
||||||
model.displayName,
|
|
||||||
].filter((entry): entry is string => typeof entry === "string" && entry.length > 0)),
|
|
||||||
),
|
|
||||||
sdkDescriptions: Array.from(
|
|
||||||
new Set([
|
|
||||||
...(Array.isArray(existing?.metadata?.sdkDescriptions) ? existing.metadata.sdkDescriptions : []),
|
|
||||||
model.description,
|
|
||||||
].filter((entry): entry is string => typeof entry === "string" && entry.length > 0)),
|
|
||||||
),
|
|
||||||
supportsEffort: model.supportsEffort === true,
|
|
||||||
supportedEffortLevels: model.supportedEffortLevels,
|
|
||||||
supportsAdaptiveThinking: model.supportsAdaptiveThinking === true,
|
|
||||||
supportsFastMode: model.supportsFastMode === true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return Array.from(resolved.values());
|
|
||||||
}
|
|
||||||
|
|
||||||
export function parseClaudeSdkModelDescriptorForTest(
|
|
||||||
model: ModelInfo,
|
|
||||||
): ParsedClaudeSdkModelDescriptor | null {
|
|
||||||
return parseClaudeSdkDescriptor(model);
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user