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:
Mohamed Boudra
2026-09-22 20:18:00 +02:00
committed by GitHub
co-authored by Leonardo G. Urci Sebastian Galindo Rodrigo
parent c8a7667a03
commit aeb98f813a
4 changed files with 106 additions and 23 deletions
+6 -1
View File
@@ -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);