Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
# Port plan — Zoo PR #555 → `feature/zoo-555-add-fable-5-support-across-anthropic-providers`

> **For the executor (read first).** Do the steps in order. Do not improvise or
> refactor beyond what is written (YAGNI). Every code block is already adapted to
> this repo. This repo is **Tumble Code**: never introduce the strings "Roo" or
> "Zoo" in user-facing text. The model id `claude-fable-5` is an **internal model
> id**, not branding — keep it verbatim.

---

## 0. Context

- **Upstream:** Zoo PR #555 — "Add Fable 5 support across Anthropic providers" (commit `cc2654521`).
- **What it does:** Registers the `claude-fable-5` model across every Anthropic-family
provider path — direct Anthropic, Bedrock, Vertex, OpenRouter, Requesty, and the
Vercel AI Gateway — including model metadata, the adaptive-thinking guard, and the
`supportsTemperature: false` plumbing so temperature is omitted for this model.
- **Model facts (mirror upstream, reviewed in PR #555):** context window `1_000_000`;
direct Anthropic `maxTokens` `128_000` (overridden to 8k when reasoning effort is
off); Bedrock/Vertex `maxTokens` `8192`; prices in=$10 / out=$50 / cacheWrite=$12.5 /
cacheRead=$1 per M; `supportsImages`, `supportsPromptCache`, `supportsReasoningBudget`,
`supportsReasoningBinary` all true; `supportsTemperature: false`. Uses the same
adaptive-thinking contract as Opus 4.7/4.8.
- **Why we want it:** new top-tier Claude model; our providers have no `claude-fable-5`
entry yet (verified: `grep claude-fable-5` empty across `packages/types/src/providers/*`
and `src/api/providers/*`). Low risk, high value — matches our model-support cadence.
- **Adaptations vs. the raw upstream diff (IMPORTANT):**
1. **Branding:** upstream edits a bedrock comment to say "Zoo Code UI"; our fork already
reads `// display: "summarized" surfaces thinking content in the UI.`
([bedrock.ts:439](../src/api/providers/bedrock.ts#L439)). Keep "the UI" — do NOT
introduce "Zoo Code". The Fable-5 mentions in the doc comment are optional polish.
2. **model-params.ts:** upstream adds the `supportsTemperature === false` guard to the
generic **`else`** branch. Our `format === "anthropic"` branch already has that guard
([model-params.ts:151](../src/api/transform/model-params.ts#L151)); the `else` branch
(our lines 179-187) does NOT. Apply the guard to the `else` branch only.
3. Dependencies `getAnthropicProviderReasoning` / `AnthropicProviderReasoningParams`
already exist in [reasoning.ts:61](../src/api/transform/reasoning.ts#L61) — the
requesty refactor applies cleanly.
- **Original authors — credit them:**

```text
Co-authored-by: T <taltas@users.noreply.github.com>
Co-authored-by: Elliott de Launay <edelauna@gmail.com>
```

## 1. Preconditions

- [ ] Branch `feature/zoo-555-...` created off `main`.
- [ ] No `claude-fable-5` entry exists yet in any provider type/handler.
- [ ] `getAnthropicProviderReasoning` exported from `src/api/transform/reasoning.ts`.

## 2. Source edits

### Edit A — `packages/types/src/providers/anthropic.ts`
Insert a `"claude-fable-5"` entry into `anthropicModels` (before `claude-opus-4-5-20251101`):
maxTokens 128_000, contextWindow 1_000_000, images/cache true, in 10 / out 50 /
cacheWrite 12.5 / cacheRead 1, supportsReasoningBudget/Binary true, supportsTemperature
false, with the upstream description.

### Edit B — `packages/types/src/providers/bedrock.ts`
- Add `"anthropic.claude-fable-5"` to `bedrockModels` (maxTokens 8192, contextWindow 1M,
cache true + `minTokensPerCachePoint`/`maxCachePoints`/`cachableFields`, same prices,
reasoning flags, supportsTemperature false, description).
- Append `"anthropic.claude-fable-5"` to `BEDROCK_GLOBAL_INFERENCE_MODEL_IDS` with the
cross-region comment line.

### Edit C — `packages/types/src/providers/vertex.ts`
Add `"claude-fable-5"` to `vertexModels` (maxTokens 8192, contextWindow 1M, etc.).

### Edit D — `packages/types/src/providers/openrouter.ts`
Add `"anthropic/claude-fable-5"` to both `OPEN_ROUTER_PROMPT_CACHING_MODELS` and
`OPEN_ROUTER_REASONING_BUDGET_MODELS`.

### Edit E — `packages/types/src/providers/vercel-ai-gateway.ts`
Add `"anthropic/claude-fable-5"` to both `VERCEL_AI_GATEWAY_PROMPT_CACHING_MODELS` and
`VERCEL_AI_GATEWAY_VISION_AND_TOOLS_MODELS`.

### Edit F — `src/api/providers/anthropic.ts`
Add `case "claude-fable-5":` to the two switch statements (after `claude-opus-4-8`).

### Edit G — `src/api/providers/bedrock.ts`
Add `baseModelId.includes("fable-5") ||` to the `isAdaptiveThinkingModel` guard. Optional:
mention Fable 5 in the adjacent doc comments — but keep "the UI" wording (no "Zoo Code").

### Edit H — `src/api/providers/requesty.ts`
Swap import to `{ AnthropicProviderReasoningParams, getAnthropicProviderReasoning }`,
change the two `thinking?: AnthropicReasoningParams` to `AnthropicProviderReasoningParams`,
and in `getModel()` compute `reasoning = getAnthropicProviderReasoning({ model: info,
reasoningBudget: params.reasoningBudget, settings: this.options })` and return
`{ id, info, ...params, reasoning }`.

### Edit I — `src/api/providers/vercel-ai-gateway.ts`
Gate temperature on `info.supportsTemperature !== false && this.supportsTemperature(modelId)`
in both `createMessage` and `completePrompt`.

### Edit J — `src/api/providers/fetchers/openrouter.ts`
Add the `anthropic/claude-fable-5` block setting maxTokens + reasoningBinary + temperature.

### Edit K — `src/api/providers/fetchers/requesty.ts`
Add the `anthropic/claude-fable-5` override block (reasoning flags + supportsTemperature false).

### Edit L — `src/api/providers/fetchers/vercel-ai-gateway.ts`
Add the `anthropic/claude-fable-5` → `supportsTemperature = false` block.

### Edit M — `src/api/transform/model-params.ts`
In the **`else`** branch only, prepend `if (model.supportsTemperature === false) {
params.temperature = undefined }` and drop the stale 2-line OpenRouter TODO.

### Edit N — `src/api/providers/anthropic-vertex.ts` (fork-specific; NOT in Zoo's diff)
Zoo's PR did not touch this file because Zoo's Vertex handler already routed
adaptive-binary models through `getAnthropicProviderReasoning` (from an earlier PR).
**Our fork never got that change** — our `getModel()` returned `params.reasoning`
from `getModelParams` (`getAnthropicReasoning`), which always emits
`{ type: "enabled", budget_tokens }`. So Fable 5 (and Opus 4.7/4.8) on Vertex sent
the wrong thinking config. Root cause proven via the new vertex adaptive test failing
with `{ type: "enabled", budget_tokens: 8192 }` instead of `{ type: "adaptive" }`.
Fix: import `getAnthropicProviderReasoning`; in `getModel()` compute
`thinking = getAnthropicProviderReasoning({ model: info, reasoningBudget:
params.reasoningBudget, reasoningEffort: params.reasoningEffort, settings: this.options })`
and return `{ ...params, reasoning: thinking }`. Because `{ type: "adaptive" }` is not in
the SDK's `ThinkingConfigParam` union, change the `createMessage`/`completePrompt` `params`
declarations from a `: Anthropic.Messages.MessageCreateParams…` annotation to an
`as Anthropic.Messages.MessageCreateParams…` cast (mirrors `AnthropicHandler`).

## 3. Tests (port from upstream, adapt anchors)

Add the Fable-5 cases to: `anthropic.spec.ts`, `anthropic-vertex.spec.ts`, `bedrock.spec.ts`,
`requesty.spec.ts`, `vercel-ai-gateway.spec.ts`, `fetchers/__tests__/openrouter.spec.ts`,
`fetchers/__tests__/vercel-ai-gateway.spec.ts`, `transform/__tests__/model-params.spec.ts`,
`shared/__tests__/api.spec.ts`, and create new `fetchers/__tests__/requesty.spec.ts`.

## 4. Out of scope
- No "Zoo Code" branding. No TTS/router/cloud. Internal id `claude-fable-5` stays.

## 5. Verify
- `pnpm --filter @roo-code/types check-types` clean.
- `cd src && npx vitest run api/providers/__tests__/anthropic.spec.ts api/providers/__tests__/bedrock.spec.ts api/providers/__tests__/anthropic-vertex.spec.ts api/providers/__tests__/requesty.spec.ts api/providers/__tests__/vercel-ai-gateway.spec.ts api/providers/fetchers/__tests__/openrouter.spec.ts api/providers/fetchers/__tests__/requesty.spec.ts api/providers/fetchers/__tests__/vercel-ai-gateway.spec.ts api/transform/__tests__/model-params.spec.ts shared/__tests__/api.spec.ts` all green.

## 6. Acceptance
- [ ] All new Fable-5 tests pass; touched suites green.
- [ ] No "Roo"/"Zoo" user-facing strings introduced.

## 7. Record
```bash
node .claude/skills/zoo-port/scripts/zoo-prs.mjs record --pr 555 --status ported \
--branch feature/zoo-555-add-fable-5-support-across-anthropic-providers \
--plan ai_plans/2026-06-17_zoo-555-add-fable-5-support-across-anthropic-providers.md
```
17 changes: 17 additions & 0 deletions packages/types/src/providers/anthropic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,23 @@ export const anthropicModels = {
supportsReasoningBinary: true,
supportsTemperature: false,
},
"claude-fable-5": {
maxTokens: 128_000, // Overridden to 8k if `enableReasoningEffort` is false.
contextWindow: 1_000_000,
supportsImages: true,
supportsPromptCache: true,
inputPrice: 10.0, // $10 per million input tokens
outputPrice: 50.0, // $50 per million output tokens
cacheWritesPrice: 12.5, // $12.50 per million tokens
cacheReadsPrice: 1.0, // $1.00 per million tokens
// Fable 5 uses the same adaptive-thinking / binary-toggle convention as
// Opus 4.7+ on the direct Anthropic provider path.
supportsReasoningBudget: true,
supportsReasoningBinary: true,
supportsTemperature: false,
description:
"Claude Fable 5 is Anthropic's most capable widely released model for the most demanding reasoning and long-horizon agentic work.",
},
"claude-opus-4-5-20251101": {
maxTokens: 32_000, // Overridden to 8k if `enableReasoningEffort` is false.
contextWindow: 200_000,
Expand Down
20 changes: 20 additions & 0 deletions packages/types/src/providers/bedrock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,24 @@ export const bedrockModels = {
},
],
},
"anthropic.claude-fable-5": {
maxTokens: 8192,
contextWindow: 1_000_000,
supportsImages: true,
supportsPromptCache: true,
supportsReasoningBudget: true,
supportsReasoningBinary: true,
supportsTemperature: false,
inputPrice: 10.0,
outputPrice: 50.0,
cacheWritesPrice: 12.5,
cacheReadsPrice: 1.0,
minTokensPerCachePoint: 1024,
maxCachePoints: 4,
cachableFields: ["system", "messages", "tools"],
description:
"Claude Fable 5 is Anthropic's most capable widely released model for the most demanding reasoning and long-horizon agentic work.",
},
"anthropic.claude-opus-4-5-20251101-v1:0": {
maxTokens: 8192,
contextWindow: 200_000,
Expand Down Expand Up @@ -588,6 +606,7 @@ export const BEDROCK_1M_CONTEXT_MODEL_IDS = [
// - Claude Opus 4.5
// - Claude Opus 4.6
// - Claude Opus 4.7
// - Claude Fable 5 (cross-region inference only — can only be used through an inference profile)
export const BEDROCK_GLOBAL_INFERENCE_MODEL_IDS = [
"anthropic.claude-sonnet-4-20250514-v1:0",
"anthropic.claude-sonnet-4-5-20250929-v1:0",
Expand All @@ -597,6 +616,7 @@ export const BEDROCK_GLOBAL_INFERENCE_MODEL_IDS = [
"anthropic.claude-opus-4-6-v1",
"anthropic.claude-opus-4-7",
"anthropic.claude-opus-4-8",
"anthropic.claude-fable-5",
] as const

// Amazon Bedrock Service Tier types
Expand Down
2 changes: 2 additions & 0 deletions packages/types/src/providers/openrouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export const OPEN_ROUTER_PROMPT_CACHING_MODELS = new Set([
"anthropic/claude-opus-4.1",
"anthropic/claude-opus-4.5",
"anthropic/claude-opus-4.6",
"anthropic/claude-fable-5",
"anthropic/claude-haiku-4.5",
"google/gemini-2.5-flash-preview",
"google/gemini-2.5-flash-preview:thinking",
Expand Down Expand Up @@ -74,6 +75,7 @@ export const OPEN_ROUTER_REASONING_BUDGET_MODELS = new Set([
"anthropic/claude-opus-4.1",
"anthropic/claude-opus-4.5",
"anthropic/claude-opus-4.6",
"anthropic/claude-fable-5",
"anthropic/claude-sonnet-4",
"anthropic/claude-sonnet-4.5",
"anthropic/claude-sonnet-4.6",
Expand Down
2 changes: 2 additions & 0 deletions packages/types/src/providers/vercel-ai-gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const VERCEL_AI_GATEWAY_PROMPT_CACHING_MODELS = new Set([
"anthropic/claude-opus-4.1",
"anthropic/claude-opus-4.5",
"anthropic/claude-opus-4.6",
"anthropic/claude-fable-5",
"anthropic/claude-sonnet-4",
"anthropic/claude-sonnet-4.6",
"openai/gpt-4.1",
Expand Down Expand Up @@ -55,6 +56,7 @@ export const VERCEL_AI_GATEWAY_VISION_AND_TOOLS_MODELS = new Set([
"anthropic/claude-opus-4.1",
"anthropic/claude-opus-4.5",
"anthropic/claude-opus-4.6",
"anthropic/claude-fable-5",
"anthropic/claude-sonnet-4",
"anthropic/claude-sonnet-4.6",
"google/gemini-1.5-flash",
Expand Down
15 changes: 15 additions & 0 deletions packages/types/src/providers/vertex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,21 @@ export const vertexModels = {
},
],
},
"claude-fable-5": {
maxTokens: 8192,
contextWindow: 1_000_000,
supportsImages: true,
supportsPromptCache: true,
inputPrice: 10.0,
outputPrice: 50.0,
cacheWritesPrice: 12.5,
cacheReadsPrice: 1.0,
supportsReasoningBudget: true,
supportsReasoningBinary: true,
supportsTemperature: false,
description:
"Claude Fable 5 is Anthropic's most capable widely released model for the most demanding reasoning and long-horizon agentic work.",
},
"claude-opus-4-5@20251101": {
maxTokens: 8192,
contextWindow: 200_000,
Expand Down
46 changes: 46 additions & 0 deletions src/api/providers/__tests__/anthropic-vertex.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -945,6 +945,23 @@ describe("VertexHandler", () => {
expect(model.betas).toContain("context-1m-2025-08-07")
})

it("should return Claude Fable 5 model info", () => {
const handler = new AnthropicVertexHandler({
apiModelId: "claude-fable-5",
vertexProjectId: "test-project",
vertexRegion: "us-central1",
})

const model = handler.getModel()
expect(model.id).toBe("claude-fable-5")
expect(model.info.maxTokens).toBe(8192)
expect(model.info.contextWindow).toBe(1_000_000)
expect(model.info.supportsReasoningBinary).toBe(true)
expect(model.info.supportsReasoningBudget).toBe(true)
expect(model.info.supportsPromptCache).toBe(true)
expect(model.info.supportsTemperature).toBe(false)
})

it("should not enable 1M context when flag is disabled", () => {
const handler = new AnthropicVertexHandler({
apiModelId: VERTEX_1M_CONTEXT_MODEL_IDS[0],
Expand Down Expand Up @@ -1161,6 +1178,35 @@ describe("VertexHandler", () => {
undefined,
)
})

it("should use adaptive thinking for Claude Fable 5", async () => {
const fableHandler = new AnthropicVertexHandler({
apiModelId: "claude-fable-5",
vertexProjectId: "test-project",
vertexRegion: "us-central1",
enableReasoningEffort: true,
})

const mockCreate = vitest.fn().mockImplementation(async () => ({
async *[Symbol.asyncIterator]() {
yield { type: "message_start", message: { usage: { input_tokens: 10, output_tokens: 5 } } }
},
}))
;(fableHandler["client"].messages as any).create = mockCreate

await fableHandler.createMessage("You are a helpful assistant", [{ role: "user", content: "Hello" }]).next()

expect(mockCreate).toHaveBeenCalledWith(
expect.objectContaining({
thinking: { type: "adaptive" },
}),
undefined,
)

const request = mockCreate.mock.calls[0][0]
expect(request.thinking).not.toHaveProperty("budget_tokens")
expect(request.temperature).toBeUndefined()
})
})

describe("native tool calling", () => {
Expand Down
44 changes: 44 additions & 0 deletions src/api/providers/__tests__/anthropic.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,33 @@ describe("AnthropicHandler", () => {
expect(requestBody?.max_tokens).toBe(32768)
})

it("should use adaptive thinking for Claude Fable 5 when reasoning is enabled", async () => {
const fableHandler = new AnthropicHandler({
apiKey: "test-api-key",
apiModelId: "claude-fable-5",
enableReasoningEffort: true,
modelMaxTokens: 32768,
})

const stream = fableHandler.createMessage(systemPrompt, [
{
role: "user",
content: [{ type: "text" as const, text: "Hello" }],
},
])

for await (const _chunk of stream) {
// Consume stream
}

const requestBody = mockCreate.mock.calls[mockCreate.mock.calls.length - 1]?.[0]
const requestOptions = mockCreate.mock.calls[mockCreate.mock.calls.length - 1]?.[1]
expect(requestBody?.thinking).toEqual({ type: "adaptive" })
expect(requestBody?.temperature).toBeUndefined()
expect(requestBody?.max_tokens).toBe(32768)
expect(requestOptions?.headers?.["anthropic-beta"]).toContain("prompt-caching-2024-07-31")
})

it("should not require the 1M context beta header for Claude Opus 4.8", async () => {
const opus48Handler = new AnthropicHandler({
apiKey: "test-api-key",
Expand Down Expand Up @@ -473,6 +500,23 @@ describe("AnthropicHandler", () => {
expect(model.reasoningBudget).toBeUndefined()
})

it("should handle Claude Fable 5 model correctly", () => {
const handler = new AnthropicHandler({
apiKey: "test-api-key",
apiModelId: "claude-fable-5",
})
const model = handler.getModel()
expect(model.id).toBe("claude-fable-5")
expect(model.info.maxTokens).toBe(128000)
expect(model.info.contextWindow).toBe(1000000)
expect(model.maxTokens).toBe(8192)
expect(model.info.supportsReasoningBinary).toBe(true)
expect(model.info.supportsReasoningBudget).toBe(true)
expect(model.info.supportsPromptCache).toBe(true)
expect(model.info.supportsTemperature).toBe(false)
expect(model.reasoningBudget).toBeUndefined()
})

it("should enable 1M context for Claude 4.5 Sonnet when beta flag is set", () => {
const handler = new AnthropicHandler({
apiKey: "test-api-key",
Expand Down
Loading
Loading