Skip to content
Closed
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
84 changes: 48 additions & 36 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
},
"dependencies": {
"@anthropic-ai/claude-agent-sdk": "^0.2.117",
"@anthropic-ai/claude-code": "^2.1.198",
"@anthropic-ai/claude-code": "^2.1.257",
"cross-spawn": "7.0.6",
"jsonc-parser": "^3.3.1",
"libsql": "^0.5.29",
Expand Down
9 changes: 8 additions & 1 deletion src/__tests__/explicit-model-pins.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { describe, it, expect, mock, beforeEach } from "bun:test"

// ---------- unit: explicitModelPin + canonical bump ----------

const { explicitModelPin, CANONICAL_SONNET_MODEL, CANONICAL_OPUS_MODEL } = await import("../proxy/models")
const { explicitModelPin, CANONICAL_FABLE_MODEL, CANONICAL_SONNET_MODEL, CANONICAL_OPUS_MODEL } = await import("../proxy/models")

describe("explicitModelPin (#631)", () => {
it("pins fully-versioned sonnet ids", () => {
Expand All @@ -29,6 +29,7 @@ describe("explicitModelPin (#631)", () => {

it("routes mythos ids through the fable tier pin", () => {
expect(explicitModelPin("claude-mythos-5")).toEqual({ ANTHROPIC_DEFAULT_FABLE_MODEL: "claude-mythos-5" })
expect(explicitModelPin("claude-fable-5-1")).toEqual({ ANTHROPIC_DEFAULT_FABLE_MODEL: "claude-fable-5-1" })
expect(explicitModelPin("claude-fable-5")).toEqual({ ANTHROPIC_DEFAULT_FABLE_MODEL: "claude-fable-5" })
})

Expand Down Expand Up @@ -58,6 +59,12 @@ describe("canonical opus pin", () => {
})
})

describe("canonical fable pin", () => {
it("bare fable means the current Fable", () => {
expect(CANONICAL_FABLE_MODEL).toBe("claude-fable-5-1")
})
})

// ---------- integration: pins reach the SDK subprocess env ----------

let queryEnvs: Array<Record<string, string | undefined>> = []
Expand Down
21 changes: 12 additions & 9 deletions src/__tests__/openai.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1320,26 +1320,29 @@ describe("createSseTranslator", () => {
// ---------------------------------------------------------------------------

describe("buildModelList", () => {
it("returns 8 models", () => {
expect(buildModelList(true).length).toBe(8)
expect(buildModelList(false).length).toBe(8)
it("returns 9 models", () => {
expect(buildModelList(true).length).toBe(9)
expect(buildModelList(false).length).toBe(9)
})

it("includes sonnet-5, fable-5, opus-5, opus-4-6, opus-4-7, and opus-4-8 for UI pickers", () => {
it("includes current and legacy Fable plus the supported Sonnet and Opus models for UI pickers", () => {
const ids = buildModelList(true).map(m => m.id)
expect(ids).toContain("claude-sonnet-5")
expect(ids).toContain("claude-fable-5-1")
expect(ids).toContain("claude-fable-5")
expect(ids).toContain("claude-opus-5")
expect(ids).toContain("claude-opus-4-6")
expect(ids).toContain("claude-opus-4-7")
expect(ids).toContain("claude-opus-4-8")
})

it("Max subscription gets 1M context for fable, 200k otherwise", () => {
const fableMax = buildModelList(true).find(m => m.id === "claude-fable-5")!
const fableFree = buildModelList(false).find(m => m.id === "claude-fable-5")!
expect(fableMax.context_window).toBe(1_000_000)
expect(fableFree.context_window).toBe(200_000)
it("Max subscription gets 1M context for every Fable version, 200k otherwise", () => {
for (const id of ["claude-fable-5-1", "claude-fable-5"]) {
const fableMax = buildModelList(true).find(m => m.id === id)!
const fableFree = buildModelList(false).find(m => m.id === id)!
expect(fableMax.context_window).toBe(1_000_000)
expect(fableFree.context_window).toBe(200_000)
}
})

it("Max subscription gets 1M context for all opus variants, 200k for sonnet", () => {
Expand Down
1 change: 1 addition & 0 deletions src/__tests__/pricing-unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ describe("resolveModelPricing", () => {
it("resolves concrete API model IDs", () => {
expect(resolveModelPricing("claude-opus-4-8")).toMatchObject({ inputPerMTok: 5, outputPerMTok: 25 })
expect(resolveModelPricing("claude-sonnet-4-6")).toMatchObject({ inputPerMTok: 3 })
expect(resolveModelPricing("claude-fable-5-1")).toMatchObject({ inputPerMTok: 10 })
expect(resolveModelPricing("claude-fable-5")).toMatchObject({ inputPerMTok: 10 })
})

Expand Down
10 changes: 8 additions & 2 deletions src/__tests__/proxy-env-stripping.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ describe("SDK model pin injection (fixes #419)", () => {
const app = createTestApp()
// Bare alias: no envOverride kicks in, so the canonical pin is exercised.
await post(app, { ...BASIC_REQUEST, model: "sonnet" })
expect(capturedQueryOptions.env.ANTHROPIC_DEFAULT_FABLE_MODEL).toBe("claude-fable-5")
expect(capturedQueryOptions.env.ANTHROPIC_DEFAULT_FABLE_MODEL).toBe("claude-fable-5-1")
expect(capturedQueryOptions.env.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe("claude-opus-5")
expect(capturedQueryOptions.env.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe("claude-sonnet-5")
expect(capturedQueryOptions.env.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe("claude-haiku-4-5")
Expand All @@ -238,9 +238,15 @@ describe("SDK model pin injection (fixes #419)", () => {
expect(capturedQueryOptions.env.ANTHROPIC_DEFAULT_FABLE_MODEL).toBe("claude-fable-5")
})

it("explicit claude-fable-5-1 requests pin the SDK env to Fable 5.1", async () => {
const app = createTestApp()
await post(app, { ...BASIC_REQUEST, model: "claude-fable-5-1" })
expect(capturedQueryOptions.env.ANTHROPIC_DEFAULT_FABLE_MODEL).toBe("claude-fable-5-1")
})

// Mythos has no SDK alias of its own — it resolves through the fable alias,
// so an explicit claude-mythos-* request must pin ANTHROPIC_DEFAULT_FABLE_MODEL
// to the requested id (not Meridian's canonical claude-fable-5 pin).
// to the requested id (not Meridian's canonical claude-fable-5-1 pin).
it("explicit claude-mythos-5 requests pin the SDK env to mythos via the fable alias", async () => {
const app = createTestApp()
await post(app, { ...BASIC_REQUEST, model: "claude-mythos-5" })
Expand Down
1 change: 1 addition & 0 deletions src/__tests__/proxy-health-build.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ describe("/v1/models profile auth context", () => {
envOverrides: { CLAUDE_CONFIG_DIR: "/profiles/work" },
}])
expect(models.get("claude-opus-4-6")?.context_window).toBe(1_000_000)
expect(models.get("claude-fable-5-1")?.context_window).toBe(1_000_000)
expect(models.get("claude-fable-5")?.context_window).toBe(1_000_000)
})

Expand Down
2 changes: 1 addition & 1 deletion src/proxy/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export type ClaudeModel = "sonnet" | "sonnet[1m]" | "opus" | "opus[1m]" | "haiku
* override via MERIDIAN_DEFAULT_{TYPE}_MODEL (proxy-side) or
* ANTHROPIC_DEFAULT_{TYPE}_MODEL (shell env, wins over Meridian's pin).
*/
export const CANONICAL_FABLE_MODEL = "claude-fable-5"
export const CANONICAL_FABLE_MODEL = "claude-fable-5-1"
export const CANONICAL_OPUS_MODEL = "claude-opus-5"
export const CANONICAL_SONNET_MODEL = "claude-sonnet-5"
export const CANONICAL_HAIKU_MODEL = "claude-haiku-4-5"
Expand Down
9 changes: 9 additions & 0 deletions src/proxy/openai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1064,6 +1064,15 @@ export function buildModelList(extendedContextIncluded: boolean, now = Math.floo
context_window: extendedContextIncluded ? 1_000_000 : 200_000,
capabilities: FULL_CAPABILITIES,
},
{
id: "claude-fable-5-1",
object: "model",
created: now,
owned_by: "anthropic",
display_name: "Claude Fable 5.1",
context_window: extendedContextIncluded ? 1_000_000 : 200_000,
capabilities: FULL_CAPABILITIES,
},
{
id: "claude-fable-5",
object: "model",
Expand Down
Loading