mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
Show Claude Ultra Code where supported (#1872)
* fix(providers): show Claude Ultra Code by capability Expose Ultra Code for xhigh-capable Claude models and keep first-party Claude model metadata in one manifest so future model updates do not need scattered feature lists. * fix(providers): preserve Claude fast mode aliases Normalize versioned Claude model IDs through the manifest before checking fast-mode support so dated Opus IDs keep the same feature surface as their base model. * fix(providers): keep Claude fast mode first-party Anchor Claude model normalization so custom settings models that merely contain first-party IDs do not inherit manifest-only fast mode. * fix(providers): preserve Claude runtime aliases Keep Fast Mode capability checks strict to manifest IDs while allowing provider-form Claude runtime model strings to resolve known model metadata.
This commit is contained in:
@@ -18,6 +18,8 @@ Implement the `AgentClient` and `AgentSession` interfaces from `agent-sdk-types.
|
||||
|
||||
Existing direct providers: `claude` (in `providers/claude/agent.ts`), `codex` (`codex-app-server-agent.ts`), `opencode` (`opencode-agent.ts`), `pi` (`providers/pi/agent.ts`), and `omp` (a Pi-compatible built-in backed by the Pi adapter). The dev-only `mock` provider (`mock-load-test-agent.ts`) is also direct.
|
||||
|
||||
Claude first-party model metadata lives in `packages/server/src/server/agent/providers/claude/model-manifest.ts`. When adding or updating a Claude model, update that manifest only; the model picker thinking options and Claude-specific feature gates are derived from the manifest. Do not add model-specific Claude capability lists in feature code.
|
||||
|
||||
Paseo tools are not implemented as MCP tools internally. They live in a shared tool catalog under `packages/server/src/server/agent/tools/`; MCP is only the fallback adapter. A provider that can register runtime tools directly should set `supportsNativePaseoTools: true` and consume `launchContext.paseoTools` in `createSession`/`resumeSession`. When native tools are present, `AgentManager` strips the internal Paseo MCP server from the provider launch config so the provider does not receive the same tools twice. Providers that only know MCP should keep `supportsMcpServers: true` and let the daemon inject `/mcp/agents`.
|
||||
|
||||
Pi is a process-backed provider. Paseo requires the user to have the `pi` binary installed and talks to it through `pi --mode rpc`; the server package does not embed Pi's SDK/runtime packages.
|
||||
|
||||
@@ -438,7 +438,7 @@ describe("ClaudeAgentClient.fetchCatalog", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("exposes Ultracode only on Claude models that support it", async () => {
|
||||
test("exposes Ultra Code on xhigh-capable Claude models", async () => {
|
||||
const emptyConfigDir = await fs.mkdtemp(path.join(os.tmpdir(), "paseo-claude-models-empty-"));
|
||||
try {
|
||||
const client = new ClaudeAgentClient({
|
||||
@@ -459,8 +459,9 @@ describe("ClaudeAgentClient.fetchCatalog", () => {
|
||||
expect(getThinkingIds("claude-opus-4-8[1m]")).toContain("ultracode");
|
||||
expect(getThinkingIds("claude-opus-4-8")).toContain("ultracode");
|
||||
expect(getThinkingIds("claude-sonnet-5")).toContain("xhigh");
|
||||
expect(getThinkingIds("claude-sonnet-5")).not.toContain("ultracode");
|
||||
expect(getThinkingIds("claude-opus-4-7")).not.toContain("ultracode");
|
||||
expect(getThinkingIds("claude-sonnet-5")).toContain("ultracode");
|
||||
expect(getThinkingIds("claude-opus-4-7[1m]")).toContain("ultracode");
|
||||
expect(getThinkingIds("claude-opus-4-7")).toContain("ultracode");
|
||||
expect(getThinkingIds("claude-sonnet-4-6")).not.toContain("ultracode");
|
||||
} finally {
|
||||
await fs.rm(emptyConfigDir, { recursive: true, force: true });
|
||||
@@ -597,6 +598,30 @@ describe("ClaudeAgentSession features", () => {
|
||||
}),
|
||||
).resolves.toEqual([expect.objectContaining({ id: "fast_mode", value: false })]);
|
||||
|
||||
await expect(
|
||||
client.listFeatures({
|
||||
provider: "claude",
|
||||
cwd: process.cwd(),
|
||||
model: "claude-opus-4-8[1m]",
|
||||
}),
|
||||
).resolves.toEqual([expect.objectContaining({ id: "fast_mode", value: false })]);
|
||||
|
||||
await expect(
|
||||
client.listFeatures({
|
||||
provider: "claude",
|
||||
cwd: process.cwd(),
|
||||
model: "claude-opus-4-8-20260101",
|
||||
}),
|
||||
).resolves.toEqual([expect.objectContaining({ id: "fast_mode", value: false })]);
|
||||
|
||||
await expect(
|
||||
client.listFeatures({
|
||||
provider: "claude",
|
||||
cwd: process.cwd(),
|
||||
model: "openrouter/anthropic/claude-opus-4-8",
|
||||
}),
|
||||
).resolves.toEqual([]);
|
||||
|
||||
await expect(
|
||||
client.listFeatures({
|
||||
provider: "claude",
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
getClaudeModelsWithSettings,
|
||||
normalizeClaudeRuntimeModelId,
|
||||
} from "./models.js";
|
||||
import { CLAUDE_ULTRACODE_THINKING_OPTION_ID } from "./model-manifest.js";
|
||||
import { parsePartialJsonObject } from "./partial-json.js";
|
||||
import { ClaudeSidechainTracker } from "./sidechain-tracker.js";
|
||||
import { buildClaudeFeatures, claudeModelSupportsFastMode } from "./feature-definitions.js";
|
||||
@@ -373,7 +374,7 @@ interface ClaudeAgentSessionOptions {
|
||||
}
|
||||
|
||||
type ClaudeThinkingEffort = "low" | "medium" | "high" | "xhigh" | "max";
|
||||
type ClaudeThinkingOption = ClaudeThinkingEffort | "ultracode";
|
||||
type ClaudeThinkingOption = ClaudeThinkingEffort | typeof CLAUDE_ULTRACODE_THINKING_OPTION_ID;
|
||||
|
||||
function resolvePathEnvKey(): "Path" | "PATH" | null {
|
||||
if (process.env["Path"] !== undefined) return "Path";
|
||||
@@ -421,7 +422,7 @@ function isClaudeThinkingEffort(value: string | null | undefined): value is Clau
|
||||
}
|
||||
|
||||
function isClaudeThinkingOption(value: string | null | undefined): value is ClaudeThinkingOption {
|
||||
return value === "ultracode" || isClaudeThinkingEffort(value);
|
||||
return value === CLAUDE_ULTRACODE_THINKING_OPTION_ID || isClaudeThinkingEffort(value);
|
||||
}
|
||||
|
||||
interface ClaudeOptionsLogSummary {
|
||||
@@ -2864,7 +2865,7 @@ class ClaudeAgentSession implements AgentSession {
|
||||
this.config.thinkingOptionId && this.config.thinkingOptionId !== "default"
|
||||
? this.config.thinkingOptionId
|
||||
: undefined;
|
||||
if (thinkingOptionId === "ultracode") {
|
||||
if (thinkingOptionId === CLAUDE_ULTRACODE_THINKING_OPTION_ID) {
|
||||
return { thinking: { type: "adaptive" }, effort: "xhigh", ultracode: true };
|
||||
}
|
||||
if (thinkingOptionId && isClaudeThinkingEffort(thinkingOptionId)) {
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import type { AgentFeature, AgentFeatureToggle } from "../../agent-sdk-types.js";
|
||||
|
||||
const CLAUDE_FAST_MODE_SUPPORTED_MODEL_PREFIXES = [
|
||||
"claude-opus-4-8",
|
||||
"claude-opus-4-7",
|
||||
"claude-opus-4-6",
|
||||
] as const;
|
||||
import { claudeManifestModelSupportsFastMode } from "./model-manifest.js";
|
||||
|
||||
export const CLAUDE_FAST_MODE_FEATURE: Omit<AgentFeatureToggle, "value"> = {
|
||||
type: "toggle",
|
||||
@@ -15,20 +10,8 @@ export const CLAUDE_FAST_MODE_FEATURE: Omit<AgentFeatureToggle, "value"> = {
|
||||
icon: "zap",
|
||||
};
|
||||
|
||||
function normalizeClaudeModelId(modelId: string | null | undefined): string | null {
|
||||
const normalized = typeof modelId === "string" ? modelId.trim() : "";
|
||||
return normalized.length > 0 ? normalized : null;
|
||||
}
|
||||
|
||||
export function claudeModelSupportsFastMode(modelId: string | null | undefined): boolean {
|
||||
const normalizedModelId = normalizeClaudeModelId(modelId);
|
||||
if (!normalizedModelId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return CLAUDE_FAST_MODE_SUPPORTED_MODEL_PREFIXES.some(
|
||||
(prefix) => normalizedModelId === prefix || normalizedModelId.startsWith(`${prefix}[`),
|
||||
);
|
||||
return claudeManifestModelSupportsFastMode(modelId);
|
||||
}
|
||||
|
||||
export function buildClaudeFeatures(input: {
|
||||
|
||||
@@ -0,0 +1,282 @@
|
||||
import type { AgentModelDefinition, AgentSelectOption } from "../../agent-sdk-types.js";
|
||||
|
||||
type ClaudeEffortLevel = "low" | "medium" | "high" | "xhigh" | "max";
|
||||
|
||||
interface ClaudeModelManifestEntry {
|
||||
id: string;
|
||||
label: string;
|
||||
description: string;
|
||||
isDefault?: boolean;
|
||||
contextWindowMaxTokens?: number;
|
||||
effortLevels?: readonly ClaudeEffortLevel[];
|
||||
supportsFastMode?: boolean;
|
||||
}
|
||||
|
||||
const CLAUDE_EFFORT_LEVELS = {
|
||||
standard: ["low", "medium", "high", "max"],
|
||||
xhigh: ["low", "medium", "high", "xhigh", "max"],
|
||||
} as const satisfies Record<string, readonly ClaudeEffortLevel[]>;
|
||||
|
||||
const CLAUDE_EFFORT_LABELS = {
|
||||
low: "Low",
|
||||
medium: "Medium",
|
||||
high: "High",
|
||||
xhigh: "Extra High",
|
||||
max: "Max",
|
||||
} as const satisfies Record<ClaudeEffortLevel, string>;
|
||||
|
||||
export const CLAUDE_ULTRACODE_THINKING_OPTION_ID = "ultracode";
|
||||
|
||||
export const CLAUDE_MODEL_MANIFEST = [
|
||||
{
|
||||
id: "claude-fable-5",
|
||||
label: "Fable 5",
|
||||
description: "Fable 5 · Most powerful model",
|
||||
contextWindowMaxTokens: 1_000_000,
|
||||
effortLevels: CLAUDE_EFFORT_LEVELS.xhigh,
|
||||
},
|
||||
{
|
||||
id: "claude-opus-4-8[1m]",
|
||||
label: "Opus 4.8 1M",
|
||||
description: "Opus 4.8 with 1M context window",
|
||||
contextWindowMaxTokens: 1_000_000,
|
||||
effortLevels: CLAUDE_EFFORT_LEVELS.xhigh,
|
||||
supportsFastMode: true,
|
||||
},
|
||||
{
|
||||
id: "claude-opus-4-8",
|
||||
label: "Opus 4.8",
|
||||
description: "Opus 4.8 · Latest release",
|
||||
isDefault: true,
|
||||
contextWindowMaxTokens: 200_000,
|
||||
effortLevels: CLAUDE_EFFORT_LEVELS.xhigh,
|
||||
supportsFastMode: true,
|
||||
},
|
||||
{
|
||||
id: "claude-sonnet-5",
|
||||
label: "Sonnet 5",
|
||||
description: "Sonnet 5 · Best for everyday tasks",
|
||||
contextWindowMaxTokens: 1_000_000,
|
||||
effortLevels: CLAUDE_EFFORT_LEVELS.xhigh,
|
||||
},
|
||||
{
|
||||
id: "claude-opus-4-7[1m]",
|
||||
label: "Opus 4.7 1M",
|
||||
description: "Opus 4.7 with 1M context window",
|
||||
contextWindowMaxTokens: 1_000_000,
|
||||
effortLevels: CLAUDE_EFFORT_LEVELS.xhigh,
|
||||
supportsFastMode: true,
|
||||
},
|
||||
{
|
||||
id: "claude-opus-4-7",
|
||||
label: "Opus 4.7",
|
||||
description: "Opus 4.7 · Previous release",
|
||||
contextWindowMaxTokens: 200_000,
|
||||
effortLevels: CLAUDE_EFFORT_LEVELS.xhigh,
|
||||
supportsFastMode: true,
|
||||
},
|
||||
{
|
||||
id: "claude-opus-4-6[1m]",
|
||||
label: "Opus 4.6 1M",
|
||||
description: "Opus 4.6 with 1M context window",
|
||||
contextWindowMaxTokens: 1_000_000,
|
||||
effortLevels: CLAUDE_EFFORT_LEVELS.standard,
|
||||
supportsFastMode: true,
|
||||
},
|
||||
{
|
||||
id: "claude-opus-4-6",
|
||||
label: "Opus 4.6",
|
||||
description: "Opus 4.6 · Most capable for complex work",
|
||||
contextWindowMaxTokens: 200_000,
|
||||
effortLevels: CLAUDE_EFFORT_LEVELS.standard,
|
||||
supportsFastMode: true,
|
||||
},
|
||||
{
|
||||
id: "claude-sonnet-4-6[1m]",
|
||||
label: "Sonnet 4.6 1M",
|
||||
description: "Sonnet 4.6 with 1M context window",
|
||||
contextWindowMaxTokens: 1_000_000,
|
||||
effortLevels: CLAUDE_EFFORT_LEVELS.standard,
|
||||
},
|
||||
{
|
||||
id: "claude-sonnet-4-6",
|
||||
label: "Sonnet 4.6",
|
||||
description: "Sonnet 4.6 · Best for everyday tasks",
|
||||
contextWindowMaxTokens: 200_000,
|
||||
effortLevels: CLAUDE_EFFORT_LEVELS.standard,
|
||||
},
|
||||
{
|
||||
id: "claude-haiku-4-5",
|
||||
label: "Haiku 4.5",
|
||||
description: "Haiku 4.5 · Fastest for quick answers",
|
||||
contextWindowMaxTokens: 200_000,
|
||||
},
|
||||
] as const satisfies readonly ClaudeModelManifestEntry[];
|
||||
|
||||
function buildThinkingOptions(
|
||||
effortLevels: readonly ClaudeEffortLevel[] | undefined,
|
||||
): AgentSelectOption[] | undefined {
|
||||
if (!effortLevels) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const options: AgentSelectOption[] = effortLevels.map((id) => ({
|
||||
id,
|
||||
label: CLAUDE_EFFORT_LABELS[id],
|
||||
}));
|
||||
|
||||
if (effortLevels.includes("xhigh")) {
|
||||
options.push({ id: CLAUDE_ULTRACODE_THINKING_OPTION_ID, label: "Ultra Code" });
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
export function getClaudeManifestModels(): AgentModelDefinition[] {
|
||||
return CLAUDE_MODEL_MANIFEST.map((model) => {
|
||||
const thinkingOptions = buildThinkingOptions(
|
||||
"effortLevels" in model ? model.effortLevels : undefined,
|
||||
);
|
||||
return {
|
||||
provider: "claude",
|
||||
id: model.id,
|
||||
label: model.label,
|
||||
description: model.description,
|
||||
...("isDefault" in model && model.isDefault ? { isDefault: true } : {}),
|
||||
...(model.contextWindowMaxTokens !== undefined
|
||||
? { contextWindowMaxTokens: model.contextWindowMaxTokens }
|
||||
: {}),
|
||||
...(thinkingOptions ? { thinkingOptions } : {}),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function isClaudeManifestModelId(modelId: string): boolean {
|
||||
return CLAUDE_MODEL_MANIFEST.some((model) => model.id === modelId);
|
||||
}
|
||||
|
||||
export function claudeManifestModelSupportsFastMode(modelId: string | null | undefined): boolean {
|
||||
const normalizedModelId = normalizeClaudeManifestModelId(modelId);
|
||||
if (!normalizedModelId) {
|
||||
return false;
|
||||
}
|
||||
return CLAUDE_MODEL_MANIFEST.some(
|
||||
(model) =>
|
||||
model.id === normalizedModelId &&
|
||||
"supportsFastMode" in model &&
|
||||
model.supportsFastMode === true,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize first-party Claude model IDs for manifest capability checks. Provider-prefixed
|
||||
* runtime IDs intentionally use normalizeClaudeRuntimeModelId instead.
|
||||
*/
|
||||
export function normalizeClaudeManifestModelId(value: string | null | undefined): string | null {
|
||||
const trimmed = typeof value === "string" ? value.trim() : "";
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isClaudeManifestModelId(trimmed)) {
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
const singleSegmentMatch = trimmed.match(
|
||||
/^(?:claude[-_ ])?(fable|opus|sonnet|haiku)[-_ ]+(\d+)(\[1m\])?(?:[-_ ]+\d{8})?$/i,
|
||||
);
|
||||
if (singleSegmentMatch) {
|
||||
return normalizeSingleSegmentClaudeModelId(
|
||||
singleSegmentMatch[1],
|
||||
singleSegmentMatch[2],
|
||||
trimmed.toLowerCase().includes("[1m]"),
|
||||
);
|
||||
}
|
||||
|
||||
const runtimeMatch = trimmed.match(
|
||||
/^(?:claude[-_ ])?(opus|sonnet|haiku)[-_ ]+(\d+)[-.](\d+)(\[1m\])?(?:[-_ ]+\d{8})?$/i,
|
||||
);
|
||||
if (!runtimeMatch) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return normalizeMajorMinorClaudeModelId(
|
||||
runtimeMatch[1],
|
||||
runtimeMatch[2],
|
||||
runtimeMatch[3],
|
||||
Boolean(runtimeMatch[4]),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a Claude Code runtime/config model string to a known manifest ID.
|
||||
* Runtime metadata may include provider prefixes such as Bedrock model IDs; feature
|
||||
* gates should use normalizeClaudeManifestModelId instead.
|
||||
*/
|
||||
export function normalizeClaudeRuntimeModelId(value: string | null | undefined): string | null {
|
||||
const normalizedManifestModelId = normalizeClaudeManifestModelId(value);
|
||||
if (normalizedManifestModelId) {
|
||||
return normalizedManifestModelId;
|
||||
}
|
||||
|
||||
const trimmed = typeof value === "string" ? value.trim() : "";
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const singleSegmentMatch = trimmed.match(
|
||||
/claude[-_ ](fable|opus|sonnet|haiku)[-_ ]+(\d+)(\[1m\])?/i,
|
||||
);
|
||||
if (singleSegmentMatch) {
|
||||
const normalizedModelId = normalizeSingleSegmentClaudeModelId(
|
||||
singleSegmentMatch[1],
|
||||
singleSegmentMatch[2],
|
||||
Boolean(singleSegmentMatch[3]),
|
||||
);
|
||||
if (normalizedModelId) {
|
||||
return normalizedModelId;
|
||||
}
|
||||
}
|
||||
|
||||
const runtimeMatch = trimmed.match(
|
||||
/claude[-_ ](opus|sonnet|haiku)[-_ ]+(\d+)[-.](\d+)(\[1m\])?/i,
|
||||
);
|
||||
if (!runtimeMatch) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return normalizeMajorMinorClaudeModelId(
|
||||
runtimeMatch[1],
|
||||
runtimeMatch[2],
|
||||
runtimeMatch[3],
|
||||
Boolean(runtimeMatch[4]),
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeSingleSegmentClaudeModelId(
|
||||
familyValue: string,
|
||||
major: string,
|
||||
hasOneMillionContext: boolean,
|
||||
): string | null {
|
||||
const family = familyValue.toLowerCase();
|
||||
const suffix = hasOneMillionContext ? "[1m]" : "";
|
||||
const candidates = [`claude-${family}-${major}${suffix}`, `claude-${family}-${major}`];
|
||||
for (const candidate of candidates) {
|
||||
if (isClaudeManifestModelId(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function normalizeMajorMinorClaudeModelId(
|
||||
familyValue: string,
|
||||
major: string,
|
||||
minor: string,
|
||||
hasOneMillionContext: boolean,
|
||||
): string | null {
|
||||
const family = familyValue.toLowerCase();
|
||||
const suffix = hasOneMillionContext ? "[1m]" : "";
|
||||
const candidate = `claude-${family}-${major}-${minor}${suffix}`;
|
||||
return isClaudeManifestModelId(candidate) ? candidate : null;
|
||||
}
|
||||
@@ -5,6 +5,11 @@ import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { createTestLogger } from "../../../../test-utils/test-logger.js";
|
||||
import { ClaudeAgentClient } from "./agent.js";
|
||||
import {
|
||||
CLAUDE_ULTRACODE_THINKING_OPTION_ID,
|
||||
claudeManifestModelSupportsFastMode,
|
||||
normalizeClaudeManifestModelId,
|
||||
} from "./model-manifest.js";
|
||||
import { findClaudeModel, getClaudeModels, normalizeClaudeRuntimeModelId } from "./models.js";
|
||||
|
||||
const createdClaudeConfigDirs: string[] = [];
|
||||
@@ -78,6 +83,41 @@ describe("getClaudeModels", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("derives thinking options from model effort capabilities", () => {
|
||||
const models = new Map(getClaudeModels().map((model) => [model.id, model]));
|
||||
|
||||
expect(models.get("claude-sonnet-5")?.thinkingOptions?.map((option) => option.id)).toEqual([
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"xhigh",
|
||||
"max",
|
||||
CLAUDE_ULTRACODE_THINKING_OPTION_ID,
|
||||
]);
|
||||
expect(
|
||||
models
|
||||
.get("claude-sonnet-5")
|
||||
?.thinkingOptions?.find((option) => option.id === CLAUDE_ULTRACODE_THINKING_OPTION_ID)
|
||||
?.label,
|
||||
).toBe("Ultra Code");
|
||||
|
||||
expect(models.get("claude-opus-4-7")?.thinkingOptions?.map((option) => option.id)).toEqual([
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"xhigh",
|
||||
"max",
|
||||
CLAUDE_ULTRACODE_THINKING_OPTION_ID,
|
||||
]);
|
||||
expect(models.get("claude-sonnet-4-6")?.thinkingOptions?.map((option) => option.id)).toEqual([
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"max",
|
||||
]);
|
||||
expect(models.get("claude-haiku-4-5")?.thinkingOptions).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns fresh copies each call", () => {
|
||||
const a = getClaudeModels();
|
||||
const b = getClaudeModels();
|
||||
@@ -254,11 +294,34 @@ describe("normalizeClaudeRuntimeModelId", () => {
|
||||
expect(normalizeClaudeRuntimeModelId("gpt-5")).toBeNull();
|
||||
expect(normalizeClaudeRuntimeModelId("random")).toBeNull();
|
||||
});
|
||||
|
||||
it("normalizes provider-form runtime model strings", () => {
|
||||
expect(normalizeClaudeRuntimeModelId("openrouter/anthropic/claude-opus-4-8")).toBe(
|
||||
"claude-opus-4-8",
|
||||
);
|
||||
expect(normalizeClaudeRuntimeModelId("us.anthropic.claude-opus-4-8[1m]")).toBe(
|
||||
"claude-opus-4-8[1m]",
|
||||
);
|
||||
expect(normalizeClaudeRuntimeModelId("us.anthropic.claude-opus-4-8-20260101")).toBe(
|
||||
"claude-opus-4-8",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("findClaudeModel", () => {
|
||||
it("resolves runtime model IDs to catalog entries", () => {
|
||||
expect(findClaudeModel("claude-sonnet-5-20260101")?.id).toBe("claude-sonnet-5");
|
||||
expect(findClaudeModel("claude-sonnet-5[1m]")?.contextWindowMaxTokens).toBe(1_000_000);
|
||||
expect(findClaudeModel("us.anthropic.claude-opus-4-8[1m]")?.contextWindowMaxTokens).toBe(
|
||||
1_000_000,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("claudeManifestModelSupportsFastMode", () => {
|
||||
it("keeps fast mode strict to first-party manifest model IDs", () => {
|
||||
expect(normalizeClaudeManifestModelId("openrouter/anthropic/claude-opus-4-8")).toBeNull();
|
||||
expect(claudeManifestModelSupportsFastMode("openrouter/anthropic/claude-opus-4-8")).toBe(false);
|
||||
expect(claudeManifestModelSupportsFastMode("claude-opus-4-8-20260101")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,117 +4,10 @@ import * as path from "node:path";
|
||||
import type { Logger } from "pino";
|
||||
|
||||
import type { AgentModelDefinition } from "../../agent-sdk-types.js";
|
||||
|
||||
const CLAUDE_THINKING_OPTIONS = [
|
||||
{ id: "low", label: "Low" },
|
||||
{ id: "medium", label: "Medium" },
|
||||
{ id: "high", label: "High" },
|
||||
{ id: "max", label: "Max" },
|
||||
] as const;
|
||||
|
||||
const CLAUDE_EXTENDED_THINKING_OPTIONS = [
|
||||
{ id: "low", label: "Low" },
|
||||
{ id: "medium", label: "Medium" },
|
||||
{ id: "high", label: "High" },
|
||||
{ id: "xhigh", label: "Extra High" },
|
||||
{ id: "max", label: "Max" },
|
||||
] as const;
|
||||
|
||||
const CLAUDE_ULTRACODE_THINKING_OPTIONS = [
|
||||
...CLAUDE_EXTENDED_THINKING_OPTIONS,
|
||||
{ id: "ultracode", label: "Ultracode" },
|
||||
] as const;
|
||||
|
||||
const CLAUDE_MODELS: AgentModelDefinition[] = [
|
||||
{
|
||||
provider: "claude",
|
||||
id: "claude-fable-5",
|
||||
label: "Fable 5",
|
||||
description: "Fable 5 · Most powerful model",
|
||||
contextWindowMaxTokens: 1_000_000,
|
||||
thinkingOptions: [...CLAUDE_ULTRACODE_THINKING_OPTIONS],
|
||||
},
|
||||
{
|
||||
provider: "claude",
|
||||
id: "claude-opus-4-8[1m]",
|
||||
label: "Opus 4.8 1M",
|
||||
description: "Opus 4.8 with 1M context window",
|
||||
contextWindowMaxTokens: 1_000_000,
|
||||
thinkingOptions: [...CLAUDE_ULTRACODE_THINKING_OPTIONS],
|
||||
},
|
||||
{
|
||||
provider: "claude",
|
||||
id: "claude-opus-4-8",
|
||||
label: "Opus 4.8",
|
||||
description: "Opus 4.8 · Latest release",
|
||||
isDefault: true,
|
||||
contextWindowMaxTokens: 200_000,
|
||||
thinkingOptions: [...CLAUDE_ULTRACODE_THINKING_OPTIONS],
|
||||
},
|
||||
{
|
||||
provider: "claude",
|
||||
id: "claude-sonnet-5",
|
||||
label: "Sonnet 5",
|
||||
description: "Sonnet 5 · Best for everyday tasks",
|
||||
contextWindowMaxTokens: 1_000_000,
|
||||
thinkingOptions: [...CLAUDE_EXTENDED_THINKING_OPTIONS],
|
||||
},
|
||||
{
|
||||
provider: "claude",
|
||||
id: "claude-opus-4-7[1m]",
|
||||
label: "Opus 4.7 1M",
|
||||
description: "Opus 4.7 with 1M context window",
|
||||
contextWindowMaxTokens: 1_000_000,
|
||||
thinkingOptions: [...CLAUDE_EXTENDED_THINKING_OPTIONS],
|
||||
},
|
||||
{
|
||||
provider: "claude",
|
||||
id: "claude-opus-4-7",
|
||||
label: "Opus 4.7",
|
||||
description: "Opus 4.7 · Previous release",
|
||||
contextWindowMaxTokens: 200_000,
|
||||
thinkingOptions: [...CLAUDE_EXTENDED_THINKING_OPTIONS],
|
||||
},
|
||||
{
|
||||
provider: "claude",
|
||||
id: "claude-opus-4-6[1m]",
|
||||
label: "Opus 4.6 1M",
|
||||
description: "Opus 4.6 with 1M context window",
|
||||
contextWindowMaxTokens: 1_000_000,
|
||||
thinkingOptions: [...CLAUDE_THINKING_OPTIONS],
|
||||
},
|
||||
{
|
||||
provider: "claude",
|
||||
id: "claude-opus-4-6",
|
||||
label: "Opus 4.6",
|
||||
description: "Opus 4.6 · Most capable for complex work",
|
||||
contextWindowMaxTokens: 200_000,
|
||||
thinkingOptions: [...CLAUDE_THINKING_OPTIONS],
|
||||
},
|
||||
{
|
||||
provider: "claude",
|
||||
id: "claude-sonnet-4-6[1m]",
|
||||
label: "Sonnet 4.6 1M",
|
||||
description: "Sonnet 4.6 with 1M context window",
|
||||
contextWindowMaxTokens: 1_000_000,
|
||||
thinkingOptions: [...CLAUDE_THINKING_OPTIONS],
|
||||
},
|
||||
{
|
||||
provider: "claude",
|
||||
id: "claude-sonnet-4-6",
|
||||
label: "Sonnet 4.6",
|
||||
description: "Sonnet 4.6 · Best for everyday tasks",
|
||||
contextWindowMaxTokens: 200_000,
|
||||
thinkingOptions: [...CLAUDE_THINKING_OPTIONS],
|
||||
},
|
||||
{
|
||||
provider: "claude",
|
||||
id: "claude-haiku-4-5",
|
||||
label: "Haiku 4.5",
|
||||
description: "Haiku 4.5 · Fastest for quick answers",
|
||||
contextWindowMaxTokens: 200_000,
|
||||
},
|
||||
];
|
||||
import {
|
||||
getClaudeManifestModels,
|
||||
normalizeClaudeRuntimeModelId as normalizeClaudeManifestRuntimeModelId,
|
||||
} from "./model-manifest.js";
|
||||
|
||||
const CLAUDE_SETTINGS_MODEL_ENV_KEYS = [
|
||||
"ANTHROPIC_MODEL",
|
||||
@@ -125,7 +18,7 @@ const CLAUDE_SETTINGS_MODEL_ENV_KEYS = [
|
||||
] as const;
|
||||
|
||||
export function getClaudeModels(): AgentModelDefinition[] {
|
||||
return CLAUDE_MODELS.map((model) => ({ ...model }));
|
||||
return getClaudeManifestModels();
|
||||
}
|
||||
|
||||
export function findClaudeModel(
|
||||
@@ -135,7 +28,7 @@ export function findClaudeModel(
|
||||
if (!normalizedModelId) {
|
||||
return undefined;
|
||||
}
|
||||
return CLAUDE_MODELS.find((model) => model.id === normalizedModelId);
|
||||
return getClaudeModels().find((model) => model.id === normalizedModelId);
|
||||
}
|
||||
|
||||
export async function getClaudeModelsWithSettings(
|
||||
@@ -236,42 +129,5 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
* 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 single-segment major-version model names such as claude-fable-5
|
||||
// and claude-sonnet-5, including dated runtime strings from the SDK.
|
||||
const singleSegmentMatch = trimmed.match(/(?:claude-)?(fable|opus|sonnet|haiku)[-_ ]+(\d+)/i);
|
||||
if (singleSegmentMatch) {
|
||||
const family = singleSegmentMatch[1].toLowerCase();
|
||||
const major = singleSegmentMatch[2];
|
||||
const suffix = trimmed.toLowerCase().includes("[1m]") ? "[1m]" : "";
|
||||
const candidates = [`claude-${family}-${major}${suffix}`, `claude-${family}-${major}`];
|
||||
for (const candidate of candidates) {
|
||||
if (CLAUDE_MODELS.some((model) => model.id === candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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}`;
|
||||
return normalizeClaudeManifestRuntimeModelId(value);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user