mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-31 21:52:54 +00:00
feat(claude): add Opus 5.5 with its model capabilities (#5200)
feat(claude): add Opus 5.5 with its model capabilities Combine the catalog and runtime identity work from #5191 with the per-model effort default from #5197. Claude Code 2.1.280 introduces Opus 5.5 with 1M context, medium effort, and always-on thinking. Resolve prefixed minor model releases before their major version so Opus 5.5 retains its identity and capabilities after a turn. Co-authored-by: Leonardo G. Urci <leonardoguimaraesurci@gmail.com> Co-authored-by: Sebastian Galindo <seb@jexhq.com> Co-authored-by: Rodrigo <rodrigo.parisusao25201@gmail.com>
This commit is contained in:
co-authored by
Leonardo G. Urci
Sebastian Galindo
Rodrigo
parent
c8a7667a03
commit
aeb98f813a
@@ -52,10 +52,15 @@ interface ProviderDiagnostic {
|
||||
}
|
||||
|
||||
const EXPECTED_CLAUDE_MODELS = [
|
||||
{
|
||||
id: "claude-opus-5-5",
|
||||
model: "Opus 5.5",
|
||||
descriptionFragment: "Latest release",
|
||||
},
|
||||
{
|
||||
id: "claude-opus-5",
|
||||
model: "Opus 5",
|
||||
descriptionFragment: "Latest release",
|
||||
descriptionFragment: "Previous release",
|
||||
},
|
||||
{
|
||||
id: "claude-fable-5-1",
|
||||
|
||||
@@ -476,7 +476,7 @@ describe("ClaudeAgentClient.fetchCatalog", () => {
|
||||
force: false,
|
||||
});
|
||||
|
||||
expect(models.find((model) => model.isDefault)?.id).toBe("claude-opus-5");
|
||||
expect(models.find((model) => model.isDefault)?.id).toBe("claude-opus-5-5");
|
||||
expect(models.map((model) => model.id)).toContain("claude-fable-5");
|
||||
} finally {
|
||||
await fs.rm(emptyConfigDir, { recursive: true, force: true });
|
||||
|
||||
@@ -11,6 +11,7 @@ interface ClaudeModelManifestEntry {
|
||||
minimumClaudeCodeVersion?: string;
|
||||
contextWindowMaxTokens?: number;
|
||||
effortLevels?: readonly ClaudeEffortLevel[];
|
||||
defaultThinkingOptionId?: ClaudeEffortLevel;
|
||||
supportsThinkingDisabled?: boolean;
|
||||
supportsFastMode?: boolean;
|
||||
}
|
||||
@@ -34,10 +35,21 @@ export const CLAUDE_DISABLED_THINKING_OPTION_ID = "off";
|
||||
export const CLAUDE_ULTRACODE_THINKING_OPTION_ID = "ultracode";
|
||||
|
||||
export const CLAUDE_MODEL_MANIFEST = [
|
||||
{
|
||||
id: "claude-opus-5-5",
|
||||
label: "Opus 5.5",
|
||||
description: "Opus 5.5 · Latest release",
|
||||
defaultPriority: 3,
|
||||
minimumClaudeCodeVersion: "2.1.280",
|
||||
defaultThinkingOptionId: "medium",
|
||||
contextWindowMaxTokens: 1_000_000,
|
||||
effortLevels: CLAUDE_EFFORT_LEVELS.xhigh,
|
||||
supportsFastMode: true,
|
||||
},
|
||||
{
|
||||
id: "claude-opus-5",
|
||||
label: "Opus 5",
|
||||
description: "Opus 5 · Latest release",
|
||||
description: "Opus 5 · Previous release",
|
||||
defaultPriority: 2,
|
||||
minimumClaudeCodeVersion: "2.1.219",
|
||||
contextWindowMaxTokens: 1_000_000,
|
||||
@@ -157,9 +169,14 @@ export const CLAUDE_MODEL_MANIFEST = [
|
||||
},
|
||||
] as const satisfies readonly ClaudeModelManifestEntry[];
|
||||
|
||||
function getDefaultThinkingOptionId(model: ClaudeModelManifestEntry): ClaudeEffortLevel {
|
||||
return model.defaultThinkingOptionId ?? CLAUDE_DEFAULT_THINKING_OPTION_ID;
|
||||
}
|
||||
|
||||
function buildThinkingOptions(
|
||||
effortLevels: readonly ClaudeEffortLevel[] | undefined,
|
||||
supportsThinkingDisabled: boolean,
|
||||
defaultThinkingOptionId: ClaudeEffortLevel,
|
||||
): AgentSelectOption[] | undefined {
|
||||
if (!effortLevels) {
|
||||
return undefined;
|
||||
@@ -170,7 +187,7 @@ function buildThinkingOptions(
|
||||
...effortLevels.map((id) => ({
|
||||
id,
|
||||
label: CLAUDE_EFFORT_LABELS[id],
|
||||
...(id === CLAUDE_DEFAULT_THINKING_OPTION_ID ? { isDefault: true } : {}),
|
||||
...(id === defaultThinkingOptionId ? { isDefault: true } : {}),
|
||||
})),
|
||||
];
|
||||
|
||||
@@ -196,6 +213,7 @@ export function getClaudeManifestModels(claudeCodeVersion?: string): AgentModelD
|
||||
const thinkingOptions = buildThinkingOptions(
|
||||
model.effortLevels,
|
||||
model.supportsThinkingDisabled === true,
|
||||
getDefaultThinkingOptionId(model),
|
||||
);
|
||||
const definition: AgentModelDefinition = {
|
||||
provider: "claude",
|
||||
@@ -214,7 +232,7 @@ export function getClaudeManifestModels(claudeCodeVersion?: string): AgentModelD
|
||||
}
|
||||
if (thinkingOptions) {
|
||||
definition.thinkingOptions = thinkingOptions;
|
||||
definition.defaultThinkingOptionId = CLAUDE_DEFAULT_THINKING_OPTION_ID;
|
||||
definition.defaultThinkingOptionId = getDefaultThinkingOptionId(model);
|
||||
}
|
||||
definitions.push(definition);
|
||||
if (!("aliases" in model) || !model.aliases) {
|
||||
@@ -286,7 +304,7 @@ export function resolveClaudeDisabledThinkingForModel(
|
||||
supported:
|
||||
!!model && "supportsThinkingDisabled" in model && model.supportsThinkingDisabled === true,
|
||||
fallbackThinkingOptionId:
|
||||
model && "effortLevels" in model ? CLAUDE_DEFAULT_THINKING_OPTION_ID : undefined,
|
||||
model && "effortLevels" in model ? getDefaultThinkingOptionId(model) : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -350,7 +368,9 @@ export function normalizeClaudeManifestModelId(value: string | null | undefined)
|
||||
/**
|
||||
* 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.
|
||||
* gates should use normalizeClaudeManifestModelId instead. The prefixed matches are
|
||||
* unanchored, so major-minor runs first: "claude-opus-5-5" would otherwise stop at the
|
||||
* "claude-opus-5" entry.
|
||||
*/
|
||||
export function normalizeClaudeRuntimeModelId(value: string | null | undefined): string | null {
|
||||
const normalizedManifestModelId = normalizeClaudeManifestModelId(value);
|
||||
@@ -363,13 +383,14 @@ export function normalizeClaudeRuntimeModelId(value: string | null | undefined):
|
||||
return null;
|
||||
}
|
||||
|
||||
const singleSegmentMatch = trimmed.match(
|
||||
/claude[-_ ](fable|opus|sonnet|haiku)[-_ ]+(\d+)(\[1m\])?/i,
|
||||
const runtimeMatch = trimmed.match(
|
||||
/claude[-_ ](fable|opus|sonnet|haiku)[-_ ]+(\d+)[-.](\d+)(\[1m\])?/i,
|
||||
);
|
||||
if (singleSegmentMatch) {
|
||||
const normalizedModelId = normalizeSingleSegmentClaudeModelId(
|
||||
singleSegmentMatch[1],
|
||||
singleSegmentMatch[2],
|
||||
if (runtimeMatch) {
|
||||
const normalizedModelId = normalizeMajorMinorClaudeModelId(
|
||||
runtimeMatch[1],
|
||||
runtimeMatch[2],
|
||||
runtimeMatch[3],
|
||||
trimmed.toLowerCase().includes("[1m]"),
|
||||
);
|
||||
if (normalizedModelId) {
|
||||
@@ -377,17 +398,16 @@ export function normalizeClaudeRuntimeModelId(value: string | null | undefined):
|
||||
}
|
||||
}
|
||||
|
||||
const runtimeMatch = trimmed.match(
|
||||
/claude[-_ ](fable|opus|sonnet|haiku)[-_ ]+(\d+)[-.](\d+)(\[1m\])?/i,
|
||||
const singleSegmentMatch = trimmed.match(
|
||||
/claude[-_ ](fable|opus|sonnet|haiku)[-_ ]+(\d+)(\[1m\])?/i,
|
||||
);
|
||||
if (!runtimeMatch) {
|
||||
if (!singleSegmentMatch) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return normalizeMajorMinorClaudeModelId(
|
||||
runtimeMatch[1],
|
||||
runtimeMatch[2],
|
||||
runtimeMatch[3],
|
||||
return normalizeSingleSegmentClaudeModelId(
|
||||
singleSegmentMatch[1],
|
||||
singleSegmentMatch[2],
|
||||
trimmed.toLowerCase().includes("[1m]"),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ async function createClaudeConfigDirWithRawSettings(settings: string): Promise<s
|
||||
return configDir;
|
||||
}
|
||||
|
||||
function createCatalogClient(claudeCodeVersion = "2.1.219"): ClaudeAgentClient {
|
||||
function createCatalogClient(claudeCodeVersion = "2.1.280"): ClaudeAgentClient {
|
||||
return new ClaudeAgentClient({
|
||||
logger: createTestLogger(),
|
||||
resolveVersion: async () => claudeCodeVersion,
|
||||
@@ -50,6 +50,7 @@ describe("getClaudeModels", () => {
|
||||
it("returns all claude models", () => {
|
||||
const models = getClaudeModels();
|
||||
expect(models.map((m) => m.id)).toEqual([
|
||||
"claude-opus-5-5",
|
||||
"claude-opus-5",
|
||||
"claude-fable-5-1",
|
||||
"claude-fable-5",
|
||||
@@ -72,7 +73,7 @@ describe("getClaudeModels", () => {
|
||||
const models = getClaudeModels();
|
||||
const defaults = models.filter((m) => m.isDefault);
|
||||
expect(defaults).toHaveLength(1);
|
||||
expect(defaults[0].id).toBe("claude-opus-5");
|
||||
expect(defaults[0].id).toBe("claude-opus-5-5");
|
||||
});
|
||||
|
||||
it("defines context window sizes in the catalog", () => {
|
||||
@@ -82,6 +83,7 @@ describe("getClaudeModels", () => {
|
||||
|
||||
expect(contextWindows).toEqual(
|
||||
new Map([
|
||||
["claude-opus-5-5", 1_000_000],
|
||||
["claude-opus-5", 1_000_000],
|
||||
["claude-fable-5-1", 1_000_000],
|
||||
["claude-fable-5", 1_000_000],
|
||||
@@ -109,6 +111,11 @@ describe("getClaudeModels", () => {
|
||||
|
||||
expect(getClaudeModels("2.1.168").map((model) => model.id)).not.toContain("claude-fable-5");
|
||||
expect(getClaudeModels("2.1.169").map((model) => model.id)).toContain("claude-fable-5");
|
||||
|
||||
expect(getClaudeModels("2.1.279").map((model) => model.id)).not.toContain("claude-opus-5-5");
|
||||
expect(getClaudeModels("2.1.279").find((model) => model.isDefault)?.id).toBe("claude-opus-5");
|
||||
expect(getClaudeModels("2.1.280").map((model) => model.id)).toContain("claude-opus-5-5");
|
||||
expect(getClaudeModels("2.1.280").find((model) => model.isDefault)?.id).toBe("claude-opus-5-5");
|
||||
});
|
||||
|
||||
it("derives thinking options from model effort capabilities", () => {
|
||||
@@ -169,6 +176,8 @@ describe("getClaudeModels", () => {
|
||||
});
|
||||
|
||||
it.each([
|
||||
["claude-opus-5-5", false, "medium"],
|
||||
["claude-opus-5-5-20260401", false, "medium"],
|
||||
["claude-opus-5", true, "high"],
|
||||
["claude-opus-5-20260724", true, "high"],
|
||||
["claude-sonnet-5", true, "high"],
|
||||
@@ -417,6 +426,19 @@ describe("normalizeClaudeRuntimeModelId", () => {
|
||||
"claude-opus-4-8",
|
||||
);
|
||||
});
|
||||
|
||||
it("does not collapse a prefixed minor release onto the major it extends", () => {
|
||||
expect(normalizeClaudeRuntimeModelId("anthropic/claude-opus-5-5")).toBe("claude-opus-5-5");
|
||||
expect(normalizeClaudeRuntimeModelId("us.anthropic.claude-opus-5-5-20260401-v1:0")).toBe(
|
||||
"claude-opus-5-5",
|
||||
);
|
||||
expect(normalizeClaudeRuntimeModelId("openrouter/anthropic/claude-fable-5-1")).toBe(
|
||||
"claude-fable-5-1",
|
||||
);
|
||||
expect(normalizeClaudeRuntimeModelId("us.anthropic.claude-opus-5-20260724-v1:0")).toBe(
|
||||
"claude-opus-5",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseClaudeCodeVersion", () => {
|
||||
@@ -438,7 +460,7 @@ describe("findClaudeModel", () => {
|
||||
describe("Claude Opus 5 catalog", () => {
|
||||
it("offers a single Opus 5 entry with a 1M context window", () => {
|
||||
const opus5Models = getClaudeModels()
|
||||
.filter((model) => model.id.startsWith("claude-opus-5"))
|
||||
.filter((model) => /^claude-opus-5(\[1m\])?$/.test(model.id))
|
||||
.map(({ id, label, contextWindowMaxTokens }) => ({ id, label, contextWindowMaxTokens }));
|
||||
|
||||
expect(opus5Models).toEqual([
|
||||
@@ -515,6 +537,41 @@ describe("Claude Fable 5.1 catalog", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Claude Opus 5.5 catalog", () => {
|
||||
it("offers one Opus 5.5 entry with a 1M context window", () => {
|
||||
const opus55Models = getClaudeModels()
|
||||
.filter((model) => model.id.startsWith("claude-opus-5-5"))
|
||||
.map(({ id, label, contextWindowMaxTokens }) => ({ id, label, contextWindowMaxTokens }));
|
||||
|
||||
expect(opus55Models).toEqual([
|
||||
{ id: "claude-opus-5-5", label: "Opus 5.5", contextWindowMaxTokens: 1_000_000 },
|
||||
]);
|
||||
});
|
||||
|
||||
it("offers every effort level except off, because Opus 5.5 cannot disable thinking", () => {
|
||||
const opus55 = getClaudeModels().find((model) => model.id === "claude-opus-5-5");
|
||||
|
||||
expect(opus55?.thinkingOptions?.map((option) => option.id)).toEqual([
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"xhigh",
|
||||
"max",
|
||||
CLAUDE_ULTRACODE_THINKING_OPTION_ID,
|
||||
]);
|
||||
expect(opus55?.defaultThinkingOptionId).toBe("medium");
|
||||
expect(
|
||||
opus55?.thinkingOptions?.filter((option) => option.isDefault).map((option) => option.id),
|
||||
).toEqual(["medium"]);
|
||||
});
|
||||
|
||||
it("resolves suffixed and dated Opus 5.5 IDs to the catalog entry", () => {
|
||||
expect(findClaudeModel("claude-opus-5-5[1m]")?.id).toBe("claude-opus-5-5");
|
||||
expect(findClaudeModel("claude-opus-5-5-20260401")?.id).toBe("claude-opus-5-5");
|
||||
expect(findClaudeModel("claude-opus-5-5-20260401[1m]")?.id).toBe("claude-opus-5-5");
|
||||
});
|
||||
});
|
||||
|
||||
describe("claudeManifestModelSupportsFastMode", () => {
|
||||
it("keeps fast mode strict to first-party manifest model IDs", () => {
|
||||
expect(normalizeClaudeManifestModelId("openrouter/anthropic/claude-opus-4-8")).toBeNull();
|
||||
@@ -523,6 +580,7 @@ describe("claudeManifestModelSupportsFastMode", () => {
|
||||
});
|
||||
|
||||
it("supports fast mode on Opus 5 but not on other Claude 5 models", () => {
|
||||
expect(claudeManifestModelSupportsFastMode("claude-opus-5-5")).toBe(true);
|
||||
expect(claudeManifestModelSupportsFastMode("claude-opus-5")).toBe(true);
|
||||
expect(claudeManifestModelSupportsFastMode("claude-sonnet-5")).toBe(false);
|
||||
expect(claudeManifestModelSupportsFastMode("claude-fable-5")).toBe(false);
|
||||
|
||||
Reference in New Issue
Block a user