auto-model-router 0.23.0 → 0.24.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.
- package/.omp-plugin/marketplace.json +2 -2
- package/package.json +1 -1
- package/src/catalog/composite.ts +14 -3
- package/src/catalog/static-catalog.ts +11 -3
- package/src/config/schema.ts +4 -2
- package/src/config/types.ts +18 -4
- package/src/config/upstreams.ts +2 -0
- package/src/router/candidates.ts +9 -7
- package/src/server/providers.ts +2 -2
- package/src/upstream/anthropic.ts +28 -2
- package/test/upstreams.test.ts +43 -0
|
@@ -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.
|
|
10
|
+
"version": "0.24.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.
|
|
17
|
+
"version": "0.24.0",
|
|
18
18
|
"author": {
|
|
19
19
|
"name": "drewappling",
|
|
20
20
|
"email": "drewappling@gmail.com"
|
package/package.json
CHANGED
package/src/catalog/composite.ts
CHANGED
|
@@ -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?: {
|
|
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
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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}`,
|
package/src/config/schema.ts
CHANGED
|
@@ -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
|
|
package/src/config/types.ts
CHANGED
|
@@ -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
|
-
/**
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
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
|
|
package/src/config/upstreams.ts
CHANGED
|
@@ -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
|
}
|
package/src/router/candidates.ts
CHANGED
|
@@ -357,12 +357,14 @@ export function buildCandidates(args: BuildCandidatesArgs): { candidates: Candid
|
|
|
357
357
|
filters.escalationCostWeight > 0 && args.escalationUsdPerPromptToken !== undefined
|
|
358
358
|
? filters.escalationCostWeight * escalationRate * args.escalationUsdPerPromptToken * features.promptTokens
|
|
359
359
|
: 0;
|
|
360
|
-
// Provider bias: an Ollama plan's included credits
|
|
361
|
-
// spent, so an operator may value
|
|
362
|
-
// ledger still records list price.
|
|
363
|
-
// The snapshot carries the LIVE bias (credit-aware
|
|
364
|
-
// value is the fallback for snapshots built without one.
|
|
365
|
-
const providerBias =
|
|
360
|
+
// Provider bias: capacity already paid for — an Ollama plan's included credits, a
|
|
361
|
+
// Claude Pro/Max subscription — is money already spent, so an operator may value it
|
|
362
|
+
// below list price in ranking. The ledger still records list price.
|
|
363
|
+
// The snapshot carries the LIVE bias (credit-aware, and each named upstream's own);
|
|
364
|
+
// the static config value is the fallback for snapshots built without one.
|
|
365
|
+
const providerBias =
|
|
366
|
+
snapshot.providerBias?.[model.provider] ??
|
|
367
|
+
(model.provider === "ollama" ? cfg.ollama.costBias : (cfg.upstreams.find((u) => u.id === model.provider)?.costBias ?? 1));
|
|
366
368
|
const effectiveUsd = (fc.expectedUsd / Math.max(trustScore, 0.5) + escalationUsd) * latencyMult * providerBias;
|
|
367
369
|
// Score is assigned in a SECOND PASS below: both qualityNormalization and
|
|
368
370
|
// capabilityFloorUsd are properties of the candidate SET, not of one
|
|
@@ -381,7 +383,7 @@ export function buildCandidates(args: BuildCandidatesArgs): { candidates: Candid
|
|
|
381
383
|
if (escalationUsd > 0) {
|
|
382
384
|
reasons.push(`escalation risk +$${escalationUsd.toFixed(6)} (rate ${(escalationRate * 100).toFixed(2)}% × measured retry cost)`);
|
|
383
385
|
}
|
|
384
|
-
if (providerBias !== 1) reasons.push(`provider bias ×${providerBias} (ollama.costBias, plan credits remaining)`);
|
|
386
|
+
if (providerBias !== 1) reasons.push(`provider bias ×${providerBias} (${model.provider === "ollama" ? "ollama.costBias, plan credits remaining" : `${model.provider}.costBias, capacity already paid for`})`);
|
|
385
387
|
if (latencyMult > 1 && latency !== null) {
|
|
386
388
|
reasons.push(
|
|
387
389
|
`latency penalty ×${latencyMult.toFixed(2)} (ttft ${Math.round(latency.ttftMs)}ms, ${latency.tokensPerSec.toFixed(0)} tok/s over ${latency.samples} samples)`,
|
package/src/server/providers.ts
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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) {
|
package/test/upstreams.test.ts
CHANGED
|
@@ -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", () => {
|