auto-model-router 0.23.0 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,14 +7,14 @@
7
7
  },
8
8
  "metadata": {
9
9
  "description": "auto-model-router: a local cost/complexity-aware model router for Oh My Pi, backed by OpenRouter",
10
- "version": "0.23.0",
10
+ "version": "0.25.0",
11
11
  "pluginRoot": "."
12
12
  },
13
13
  "plugins": [
14
14
  {
15
15
  "name": "auto-model-router",
16
16
  "description": "Local cost/complexity-aware model router for Oh My Pi, backed by OpenRouter. Runs in-process, routes per turn by price and task complexity, with budget caps, mid-stream escalation, and cache-aware hysteresis.",
17
- "version": "0.23.0",
17
+ "version": "0.25.0",
18
18
  "author": {
19
19
  "name": "drewappling",
20
20
  "email": "drewappling@gmail.com"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "auto-model-router",
3
- "version": "0.23.0",
3
+ "version": "0.25.0",
4
4
  "private": false,
5
5
  "description": "Local cost/complexity-aware model router for Oh My Pi, backed by OpenRouter",
6
6
  "type": "module",
@@ -35,7 +35,12 @@ export interface CompositeBias {
35
35
  /** False when OpenRouter cannot dispatch (no key): its models are listed for metadata only, never served. Default true. */
36
36
  serveOpenRouter?: () => boolean;
37
37
  /** Named upstreams' models, built from the OpenRouter models (twins) and filtered by each upstream's breaker. */
38
- named?: { models(openrouter: readonly CatalogModel[]): readonly CatalogModel[]; serving(id: string): boolean };
38
+ named?: {
39
+ models(openrouter: readonly CatalogModel[]): readonly CatalogModel[];
40
+ serving(id: string): boolean;
41
+ /** That upstream's `costBias`, so prepaid capacity ranks below list price. 1 when it has none. */
42
+ bias?(id: string): number;
43
+ };
39
44
  }
40
45
 
41
46
  /** Shared empty list, so a deployment without named upstreams keeps the merged snapshot's identity. */
@@ -81,8 +86,14 @@ export function createCompositeCatalog(
81
86
  merged = serveBase ? mergeSnapshots(base, available ? models : []) : { ...base, models: available ? [...models] : [] };
82
87
  if (named.length > 0) merged = { ...merged, models: [...merged.models, ...named] };
83
88
  // A fresh object either way once anything changed; stamp the live bias so
84
- // candidate scoring reads it off the snapshot it is ranking.
85
- merged = { ...merged, providerBias: { ollama: providerBias } };
89
+ // candidate scoring reads it off the snapshot it is ranking. Each named upstream
90
+ // adds its own, so prepaid capacity ranks below list price without being free.
91
+ const biases: Record<string, number> = { ollama: providerBias };
92
+ for (const m of named) {
93
+ const b = bias.named?.bias?.(m.provider) ?? 1;
94
+ if (b !== 1) biases[m.provider] = b;
95
+ }
96
+ merged = { ...merged, providerBias: biases };
86
97
  return merged;
87
98
  }
88
99
 
@@ -29,9 +29,17 @@ export function buildUpstreamModels(entry: UpstreamEntry, openrouter: readonly C
29
29
  const twin = (m.twin !== undefined ? bySlug.get(m.twin) : undefined) ?? twins.get(normalizeModelKey(m.id)) ?? null;
30
30
  const modalities: Modality[] = ["text"];
31
31
  if (m.vision ?? twin?.inputModalities.includes("image") ?? false) modalities.push("image");
32
- const price: CatalogModel["price"] = { prompt: m.input / 1e6, completion: m.output / 1e6 };
33
- if (m.cachedInput !== undefined) price.cacheRead = m.cachedInput / 1e6;
34
- if (m.cacheWrite !== undefined) price.cacheWrite = m.cacheWrite / 1e6;
32
+ // An omitted price means "whatever the twin costs": a subscription sells the same
33
+ // weights per token on OpenRouter, so the twin is the only honest figure. Falling
34
+ // back to zero instead would win every tier outright and silence the quality floors.
35
+ const price: CatalogModel["price"] = {
36
+ prompt: m.input !== undefined ? m.input / 1e6 : (twin?.price.prompt ?? 0),
37
+ completion: m.output !== undefined ? m.output / 1e6 : (twin?.price.completion ?? 0),
38
+ };
39
+ const cacheRead = m.cachedInput !== undefined ? m.cachedInput / 1e6 : twin?.price.cacheRead;
40
+ const cacheWrite = m.cacheWrite !== undefined ? m.cacheWrite / 1e6 : twin?.price.cacheWrite;
41
+ if (cacheRead !== undefined) price.cacheRead = cacheRead;
42
+ if (cacheWrite !== undefined) price.cacheWrite = cacheWrite;
35
43
  const model: CatalogModel = {
36
44
  slug: `${entry.id}/${m.id}`,
37
45
  canonicalSlug: `${entry.id}/${m.id}`,
@@ -64,8 +64,8 @@ export const RESERVED_UPSTREAM_IDS: readonly string[] = ["openrouter", "ollama",
64
64
  const upstreamModel = z.strictObject({
65
65
  id: z.string().min(1),
66
66
  name: z.string().optional(),
67
- input: z.number().nonnegative(),
68
- output: z.number().nonnegative(),
67
+ input: z.number().nonnegative().optional(),
68
+ output: z.number().nonnegative().optional(),
69
69
  cachedInput: z.number().nonnegative().optional(),
70
70
  cacheWrite: z.number().nonnegative().optional(),
71
71
  contextLength: z.number().int().positive().optional(),
@@ -87,11 +87,13 @@ const upstream = z.strictObject({
87
87
  enabled: z.boolean().optional(),
88
88
  baseUrl: z.string().min(1),
89
89
  apiKey: z.string().optional(),
90
+ auth: z.enum(["api-key", "oauth-bearer"]).optional(),
90
91
  apiVersion: z.string().optional(),
91
92
  headers: z.record(z.string(), z.string()).optional(),
92
93
  timeoutMs: z.number().positive().optional(),
93
94
  rateLimitCooldownMs: z.number().nonnegative().optional(),
94
95
  quotaCooldownMs: z.number().nonnegative().optional(),
96
+ costBias: z.number().positive().max(1).optional(),
95
97
  models: z.array(upstreamModel),
96
98
  });
97
99
 
@@ -906,10 +906,15 @@ export interface UpstreamModelConfig {
906
906
  /** The model id the provider knows (an Azure deployment name for `azure`). The catalog slug is `<upstream id>/<id>`. */
907
907
  id: string;
908
908
  name?: string;
909
- /** USD per million prompt tokens. */
910
- input: number;
911
- /** USD per million completion tokens. */
912
- output: number;
909
+ /**
910
+ * USD per million prompt tokens. Absent ⇒ the OpenRouter twin's price, which is what
911
+ * a subscription upstream wants: the same weights are sold per token there, so the
912
+ * twin is the only honest figure for what a turn is worth. Pair it with `costBias`
913
+ * to rank prepaid capacity below list without pretending it is free.
914
+ */
915
+ input?: number;
916
+ /** USD per million completion tokens. Absent ⇒ the OpenRouter twin's. */
917
+ output?: number;
913
918
  /** USD per million cached prompt tokens, when the provider discounts them. */
914
919
  cachedInput?: number;
915
920
  /** USD per million prompt tokens written to cache (Anthropic). */
@@ -943,11 +948,20 @@ export interface UpstreamEntry {
943
948
  apiKey: string;
944
949
  /** Azure only: the `api-version` query parameter. */
945
950
  apiVersion: string;
951
+ /** `api-key` sends `x-api-key`; `oauth-bearer` sends `Authorization: Bearer` plus the Claude OAuth beta headers (a Claude Pro/Max subscription token). */
952
+ auth: "api-key" | "oauth-bearer";
946
953
  /** Extra request headers, e.g. a gateway's own auth. */
947
954
  headers: Record<string, string>;
948
955
  timeoutMs: number;
949
956
  rateLimitCooldownMs: number;
950
957
  quotaCooldownMs: number;
958
+ /**
959
+ * Ranking multiplier on this upstream's forecast cost, like `ollama.costBias`: below 1
960
+ * prefers capacity that is already paid for (a Claude Pro/Max subscription) without
961
+ * pricing it at zero, which would win every tier and make quality floors meaningless.
962
+ * Ranking only — the ledger still records the price the catalog carries. Default 1.
963
+ */
964
+ costBias: number;
951
965
  models: UpstreamModelConfig[];
952
966
  }
953
967
 
@@ -17,9 +17,11 @@ export function completeUpstreamEntry(raw: Record<string, unknown>): UpstreamEnt
17
17
  apiKey: typeof raw.apiKey === "string" ? raw.apiKey : "",
18
18
  apiVersion: typeof raw.apiVersion === "string" ? raw.apiVersion : "2024-10-21",
19
19
  headers: (raw.headers as Record<string, string> | undefined) ?? {},
20
+ auth: raw.auth === "oauth-bearer" ? "oauth-bearer" : "api-key",
20
21
  timeoutMs: typeof raw.timeoutMs === "number" ? raw.timeoutMs : 600_000,
21
22
  rateLimitCooldownMs: typeof raw.rateLimitCooldownMs === "number" ? raw.rateLimitCooldownMs : 60_000,
22
23
  quotaCooldownMs: typeof raw.quotaCooldownMs === "number" ? raw.quotaCooldownMs : 15 * 60_000,
24
+ costBias: typeof raw.costBias === "number" && raw.costBias > 0 ? raw.costBias : 1,
23
25
  models: (raw.models as UpstreamEntry["models"] | undefined) ?? [],
24
26
  };
25
27
  }
@@ -254,20 +254,33 @@ export function buildCandidates(args: BuildCandidatesArgs): { candidates: Candid
254
254
  // Price ceilings at the ACTUAL prompt size: long-context overrides can
255
255
  // push a model over the ceiling exactly when conversations get long.
256
256
  // Catalog prices are per-token; ceilings are per million tokens.
257
+ //
258
+ // The ceiling is compared against the BIASED price, because that is what the turn
259
+ // actually costs this deployment: capacity already paid for — an Ollama plan's
260
+ // included credits, a Claude Pro/Max subscription — carries the list price of the
261
+ // twin it is priced from, and a subscription model at $2-5/Mtok list would be
262
+ // thrown out here on every cheap tier before `costBias` was ever consulted. That
263
+ // made the bias silently inert: setting it to 0.00001 changed no decision at all.
257
264
  const price = priceAt(model, Math.max(1, features.promptTokens));
258
- if (!relaxPrice && priceCeiling !== undefined && price.prompt * 1e6 > priceCeiling) {
265
+ const providerBias =
266
+ snapshot.providerBias?.[model.provider] ??
267
+ (model.provider === "ollama" ? cfg.ollama.costBias : (cfg.upstreams.find((u) => u.id === model.provider)?.costBias ?? 1));
268
+ const biasedPrompt = price.prompt * providerBias;
269
+ const biasedCompletion = price.completion * providerBias;
270
+ const biasNote = providerBias === 1 ? "" : ` (×${providerBias} bias on $${(price.prompt * 1e6).toFixed(2)} list)`;
271
+ if (!relaxPrice && priceCeiling !== undefined && biasedPrompt * 1e6 > priceCeiling) {
259
272
  rejected.push({
260
273
  slug,
261
274
  reason: "over_price_ceiling",
262
- detail: `input $${(price.prompt * 1e6).toFixed(2)}/Mtok > ceiling $${priceCeiling.toFixed(2)}`,
275
+ detail: `input $${(biasedPrompt * 1e6).toFixed(2)}/Mtok > ceiling $${priceCeiling.toFixed(2)}${biasNote}`,
263
276
  });
264
277
  continue;
265
278
  }
266
- if (!relaxPrice && tierCfg.maxOutputPerMtok !== undefined && price.completion * 1e6 > tierCfg.maxOutputPerMtok) {
279
+ if (!relaxPrice && tierCfg.maxOutputPerMtok !== undefined && biasedCompletion * 1e6 > tierCfg.maxOutputPerMtok) {
267
280
  rejected.push({
268
281
  slug,
269
282
  reason: "over_price_ceiling",
270
- detail: `output $${(price.completion * 1e6).toFixed(2)}/Mtok > ceiling $${tierCfg.maxOutputPerMtok}`,
283
+ detail: `output $${(biasedCompletion * 1e6).toFixed(2)}/Mtok > ceiling $${tierCfg.maxOutputPerMtok}${biasNote}`,
271
284
  });
272
285
  continue;
273
286
  }
@@ -357,12 +370,9 @@ export function buildCandidates(args: BuildCandidatesArgs): { candidates: Candid
357
370
  filters.escalationCostWeight > 0 && args.escalationUsdPerPromptToken !== undefined
358
371
  ? filters.escalationCostWeight * escalationRate * args.escalationUsdPerPromptToken * features.promptTokens
359
372
  : 0;
360
- // Provider bias: an Ollama plan's included credits are money already
361
- // spent, so an operator may value them below list price in ranking. The
362
- // ledger still records list price.
363
- // The snapshot carries the LIVE bias (credit-aware); the static config
364
- // value is the fallback for snapshots built without one.
365
- const providerBias = snapshot.providerBias?.[model.provider] ?? (model.provider === "ollama" ? cfg.ollama.costBias : 1);
373
+ // `providerBias` is computed with the price ceilings above capacity already paid for
374
+ // is money already spent, so it is valued below list in ranking AND against the
375
+ // ceilings. The ledger still records list price either way.
366
376
  const effectiveUsd = (fc.expectedUsd / Math.max(trustScore, 0.5) + escalationUsd) * latencyMult * providerBias;
367
377
  // Score is assigned in a SECOND PASS below: both qualityNormalization and
368
378
  // capabilityFloorUsd are properties of the candidate SET, not of one
@@ -381,7 +391,7 @@ export function buildCandidates(args: BuildCandidatesArgs): { candidates: Candid
381
391
  if (escalationUsd > 0) {
382
392
  reasons.push(`escalation risk +$${escalationUsd.toFixed(6)} (rate ${(escalationRate * 100).toFixed(2)}% × measured retry cost)`);
383
393
  }
384
- if (providerBias !== 1) reasons.push(`provider bias ×${providerBias} (ollama.costBias, plan credits remaining)`);
394
+ if (providerBias !== 1) reasons.push(`provider bias ×${providerBias} (${model.provider === "ollama" ? "ollama.costBias, plan credits remaining" : `${model.provider}.costBias, capacity already paid for`})`);
385
395
  if (latencyMult > 1 && latency !== null) {
386
396
  reasons.push(
387
397
  `latency penalty ×${latencyMult.toFixed(2)} (ttft ${Math.round(latency.ttftMs)}ms, ${latency.tokensPerSec.toFixed(0)} tok/s over ${latency.samples} samples)`,
@@ -77,7 +77,7 @@ export function createProviders(cfg: RouterConfig, db: Database, log: Logger = c
77
77
  };
78
78
  const namedServingOne = (id: string): boolean => {
79
79
  const entry = cfg.upstreams.find((u) => u.id === id);
80
- if (entry === undefined || !entry.enabled || entry.apiKey === "" && entry.kind !== "openai") return entry !== undefined && entry.enabled && (named(id)?.available() ?? false);
80
+ if (entry === undefined || !entry.enabled || (entry.apiKey === "" && entry.kind !== "openai" && entry.auth !== "oauth-bearer")) return entry !== undefined && entry.enabled && (named(id)?.available() ?? false);
81
81
  return named(id)?.available() ?? false;
82
82
  };
83
83
  const namedServing = (): string[] => cfg.upstreams.filter((u) => u.enabled && namedServingOne(u.id)).map((u) => u.id);
@@ -91,7 +91,7 @@ export function createProviders(cfg: RouterConfig, db: Database, log: Logger = c
91
91
  usage: ollamaUsage,
92
92
  live: () => ({ costBias: cfg.ollama.costBias, biasUntilUsage: cfg.ollama.biasUntilUsage }),
93
93
  serveOpenRouter: () => cfg.openrouter.apiKey !== "",
94
- named: { models: (base) => staticCatalog.get(base), serving: namedServingOne },
94
+ named: { models: (base) => staticCatalog.get(base), serving: namedServingOne, bias: (id) => cfg.upstreams.find((u) => u.id === id)?.costBias ?? 1 },
95
95
  }),
96
96
  ollama,
97
97
  ollamaServing,
@@ -355,6 +355,27 @@ function transportError(id: string, err: unknown): UpstreamError {
355
355
  return new UpstreamError("network", 0, err instanceof Error ? err.message : String(err), true);
356
356
  }
357
357
 
358
+ /**
359
+ * Anthropic accepts a Pro/Max subscription token only on Claude Code's own traffic: unless
360
+ * the FIRST system block carries this exact line, the API answers 429 `rate_limit_error`
361
+ * with the message "Error" — a refusal wearing a quota's clothes, not a real rate limit,
362
+ * which the breaker would otherwise read as "slow down" and cool the upstream off.
363
+ *
364
+ * Claude Code sends it itself. Every other client — an OpenAI-wire caller, the router's own
365
+ * classifier and digest chores — would be refused, so an `oauth-bearer` upstream adds it
366
+ * when it is missing. It goes in as its own leading block rather than being merged into the
367
+ * caller's text, because that is the shape Claude Code sends and it keeps the caller's
368
+ * `cache_control` markers attached to the blocks they were written for.
369
+ */
370
+ const CLAUDE_CODE_IDENTITY = "You are Claude Code, Anthropic's official CLI for Claude.";
371
+
372
+ function withClaudeCodeIdentity(body: Record<string, unknown>): Record<string, unknown> {
373
+ const system = Array.isArray(body.system) ? (body.system as Block[]) : [];
374
+ const first = asRec(system[0]);
375
+ if (first !== null && typeof first.text === "string" && first.text.startsWith(CLAUDE_CODE_IDENTITY)) return body;
376
+ return { ...body, system: [{ type: "text", text: CLAUDE_CODE_IDENTITY }, ...system] };
377
+ }
378
+
358
379
  export function createAnthropicClient(cfg: RouterConfig, id: string, fetchImpl: FetchLike = fetch): NamedUpstreamClient {
359
380
  const lookup = upstreamLookup(cfg, id);
360
381
  const log = createLogger(cfg.logLevel);
@@ -379,7 +400,8 @@ export function createAnthropicClient(cfg: RouterConfig, id: string, fetchImpl:
379
400
  const { modelId, model } = modelInfo(e, slug);
380
401
  const opts: AnthropicBodyOptions = { modelId, supportsReasoning: model?.supportsReasoning ?? false };
381
402
  if (model?.maxCompletionTokens !== undefined) opts.maxCompletionTokens = model.maxCompletionTokens;
382
- return { rendered: toAnthropicBody(body, opts), servedSlug: `${id}/${modelId}` };
403
+ const rendered = toAnthropicBody(body, opts);
404
+ return { rendered: e.auth === "oauth-bearer" ? withClaudeCodeIdentity(rendered) : rendered, servedSlug: `${id}/${modelId}` };
383
405
  }
384
406
  function composeSignal(e: UpstreamEntry, caller: AbortSignal | undefined): AbortSignal | null {
385
407
  const timeout = e.timeoutMs > 0 ? AbortSignal.timeout(e.timeoutMs) : null;
@@ -388,7 +410,11 @@ export function createAnthropicClient(cfg: RouterConfig, id: string, fetchImpl:
388
410
  }
389
411
  async function post(e: UpstreamEntry, body: Record<string, unknown>, signal: AbortSignal | undefined): Promise<Response> {
390
412
  const headers: Record<string, string> = { "content-type": "application/json", "anthropic-version": ANTHROPIC_VERSION, ...e.headers };
391
- if (e.apiKey !== "") headers["x-api-key"] = e.apiKey;
413
+ if (e.auth === "oauth-bearer") {
414
+ // A Claude Pro/Max subscription token: Bearer auth at the first-party API, with the OAuth beta. No per-token cost is reported.
415
+ headers["authorization"] = `Bearer ${e.apiKey}`;
416
+ headers["anthropic-beta"] = e.headers["anthropic-beta"] ?? "oauth-2025-04-20,claude-code-20250219";
417
+ } else if (e.apiKey !== "") headers["x-api-key"] = e.apiKey;
392
418
  try {
393
419
  return await fetchImpl(`${e.baseUrl.replace(/\/+$/, "")}/v1/messages`, { method: "POST", headers, body: JSON.stringify(body), signal: composeSignal(e, signal) });
394
420
  } catch (err) {
@@ -358,6 +358,22 @@ describe("adaptive price ceilings", () => {
358
358
  expect(on.candidates.map((c) => c.model.slug)).not.toContain("a/4");
359
359
  expect(on.rejected.some((r) => r.slug === "a/4" && r.reason === "over_price_ceiling")).toBe(true);
360
360
  });
361
+
362
+ test("a price ceiling judges the BIASED price, so prepaid capacity is not thrown out on list", () => {
363
+ // A subscription upstream inherits its OpenRouter twin's list price ($4/Mtok here) and is
364
+ // discounted by `costBias` because the capacity is already paid for. Judging the ceiling on
365
+ // list threw it out before the bias was ever read, which made the bias entirely inert.
366
+ const snap = snapshot(priced);
367
+ const biased = { ...snap, providerBias: { [snap.models[0]!.provider]: 0.1 } };
368
+ const run = (s: typeof snap) =>
369
+ buildCandidates({ req, features, tier: "moderate", task: "coding", snapshot: s, ledger: null, cfg: { ...BASE, adaptivePriceCeilings: true }, expectedCompletionTokens: 512, warmSlug: null });
370
+ // Unbiased: the band tightens moderate to $3 and a/4 is over it.
371
+ expect(run(snap).rejected.some((r) => r.slug === "a/4" && r.reason === "over_price_ceiling")).toBe(true);
372
+ // Biased ×0.1: $4 list is $0.40 to this deployment, so it clears the same ceiling.
373
+ const on = run(biased);
374
+ expect(on.candidates.map((c) => c.model.slug)).toContain("a/4");
375
+ expect(on.rejected.some((r) => r.slug === "a/4")).toBe(false);
376
+ });
361
377
  });
362
378
 
363
379
  describe("quality normalization and capability floor (benchmark findings 4/6)", () => {
@@ -100,6 +100,23 @@ describe("static catalog", () => {
100
100
  expect(unknown!.tokenizer).toBe("Claude");
101
101
  });
102
102
 
103
+ test("a subscription model with no price of its own inherits the twin's, so it never ranks as free", () => {
104
+ // A Pro/Max subscription publishes no per-token rates, and pricing it at zero would beat
105
+ // every model in every tier outright. The twin sells the same weights, so it is the rate.
106
+ const e = entry({ id: "anthropic-subscription", kind: "anthropic", costBias: 0.1, models: [{ id: "claude-sonnet-4-20250514", twin: "anthropic/claude-sonnet-4" }] });
107
+ const [sub] = buildUpstreamModels(e, twins);
108
+ const twin = twins.find((m) => m.slug === "anthropic/claude-sonnet-4")!;
109
+ expect(sub!.price.prompt).toBe(twin.price.prompt);
110
+ expect(sub!.price.completion).toBe(twin.price.completion);
111
+ expect(sub!.price.prompt).toBeGreaterThan(0);
112
+ expect(sub!.isFree).toBe(false);
113
+ // The discount is a RANKING bias on the entry, never a rewrite of the recorded price.
114
+ expect(e.costBias).toBe(0.1);
115
+ // An explicit zero still means zero: a self-hosted server is genuinely free.
116
+ const free = entry({ id: "vllm", kind: "openai", models: [{ id: "local", input: 0, output: 0 }] });
117
+ expect(buildUpstreamModels(free, twins)[0]!.price).toEqual({ prompt: 0, completion: 0 });
118
+ });
119
+
103
120
  test("the source rebuilds only when the entries or the OpenRouter models change", () => {
104
121
  const cfg = cfgWith([entry({ id: "vllm", kind: "openai" })]);
105
122
  const src = createStaticCatalogSource(cfg);
@@ -349,6 +366,32 @@ describe("the Anthropic client", () => {
349
366
  expect(classifyAnthropicStatus("a", 400, { error: { message: "prompt is too long: 250000 tokens" } })).toMatchObject({ kind: "context_length", retryable: false });
350
367
  expect(classifyAnthropicStatus("a", 401, {})).toMatchObject({ kind: "auth", retryable: false });
351
368
  });
369
+
370
+ test("a subscription upstream: OAuth bearer instead of x-api-key, and the Claude Code identity leads the system blocks", async () => {
371
+ // Anthropic answers a Pro/Max token 429 "Error" unless the first system block says this,
372
+ // so the literal is the wire contract and is spelled out here rather than imported.
373
+ const identity = "You are Claude Code, Anthropic's official CLI for Claude.";
374
+ let headers: Record<string, string> = {};
375
+ let sent: Record<string, unknown> = {};
376
+ const fetchImpl = async (_u: string, init?: RequestInit): Promise<Response> => {
377
+ headers = init?.headers as Record<string, string>;
378
+ sent = JSON.parse(String(init?.body)) as Record<string, unknown>;
379
+ return sse([`event: message_stop${NL}data: {"type":"message_stop"}`]);
380
+ };
381
+ const cfg = cfgWith([entry({ id: "sub", kind: "anthropic", auth: "oauth-bearer", baseUrl: "https://api.anthropic.com", apiKey: "sk-ant-oat01-x", models: [{ id: "claude-sonnet-5", input: 0, output: 0 }] })]);
382
+ const drain = async (messages: unknown[]): Promise<void> => {
383
+ const d = await createAnthropicClient(cfg, "sub", fetchImpl).dispatch({ body: { model: "sub/claude-sonnet-5", messages, max_tokens: 16 }, sessionId: "s", signal: new AbortController().signal });
384
+ for await (const c of d.chunks) void c;
385
+ };
386
+ await drain([{ role: "user", content: "ping" }]);
387
+ expect(headers.authorization).toBe("Bearer sk-ant-oat01-x");
388
+ expect(headers["anthropic-beta"]).toContain("oauth-2025-04-20");
389
+ expect(headers["x-api-key"]).toBeUndefined();
390
+ expect(sent.system).toEqual([{ type: "text", text: identity }]);
391
+ // A caller that already identifies as Claude Code keeps its own block; it is not said twice.
392
+ await drain([{ role: "system", content: `${identity} Be brief.` }, { role: "user", content: "ping" }]);
393
+ expect(sent.system).toEqual([{ type: "text", text: `${identity} Be brief.` }]);
394
+ });
352
395
  });
353
396
 
354
397
  describe("the composite catalog with named upstreams", () => {