@juspay/neurolink 12.20.1 → 12.20.2
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/CHANGELOG.md +2 -2
- package/dist/browser/neurolink.min.js +371 -371
- package/dist/proxy/codexFallback.d.ts +27 -9
- package/dist/proxy/codexFallback.js +84 -13
- package/dist/proxy/openaiFormat.js +8 -1
- package/dist/proxy/vertexAnthropicFallback.d.ts +17 -1
- package/dist/proxy/vertexAnthropicFallback.js +25 -1
- package/dist/server/routes/claudeProxyRoutes.d.ts +17 -0
- package/dist/server/routes/claudeProxyRoutes.js +53 -5
- package/dist/types/providers.d.ts +8 -0
- package/dist/types/proxy.d.ts +20 -4
- package/dist/utils/anthropicCacheBreakpoints.d.ts +22 -1
- package/dist/utils/anthropicCacheBreakpoints.js +113 -0
- package/dist/utils/pricing.js +69 -2
- package/package.json +1 -1
|
@@ -23,21 +23,39 @@ export declare class CodexFallbackStreamError extends Error {
|
|
|
23
23
|
constructor(payload: Record<string, unknown>, eventType: string, usage?: CodexFallbackResult["usage"]);
|
|
24
24
|
}
|
|
25
25
|
/**
|
|
26
|
-
*
|
|
26
|
+
* Routing key that pins a request to the cache already holding its prefix.
|
|
27
27
|
*
|
|
28
28
|
* Anthropic caching is explicit — the client marks breakpoints and the provider
|
|
29
29
|
* honours them. OpenAI caching is automatic: a prefix is reused only when the
|
|
30
30
|
* request lands on infrastructure already holding it, and `prompt_cache_key` is
|
|
31
|
-
* what pins a
|
|
32
|
-
*
|
|
33
|
-
*
|
|
31
|
+
* what pins a request to one. Without it this hop takes its chances on routing,
|
|
32
|
+
* which is the worst case for a fallback that arrives in bursts separated by
|
|
33
|
+
* long gaps.
|
|
34
34
|
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
35
|
+
* The key must therefore describe the *prefix*, not the caller. Keying it by
|
|
36
|
+
* session id gives every session a private namespace, so N agents that share
|
|
37
|
+
* one system prompt and tool set each pay that shared prefix in full rather
|
|
38
|
+
* than paying it once — the cost is linear in fan-out width and invisible,
|
|
39
|
+
* because each request looks individually well-behaved. Across 42 captured
|
|
40
|
+
* fallback bodies the prefixes collapse to 10 distinct values, the largest
|
|
41
|
+
* shared by 12 requests belonging to different sessions.
|
|
42
|
+
*
|
|
43
|
+
* So derive the key from the prefix itself: the instructions and the tool
|
|
44
|
+
* declarations, which are exactly the leading bytes the upstream caches and
|
|
45
|
+
* the only part stable across a conversation's turns. Identical prefixes then
|
|
46
|
+
* share one cache across sessions and subagents, a prefix that genuinely
|
|
47
|
+
* differs still gets its own key so nothing unrelated contends, and turns
|
|
48
|
+
* within one session keep sharing a key because neither input changes between
|
|
49
|
+
* them.
|
|
50
|
+
*
|
|
51
|
+
* A request carrying neither instructions nor tools has no prefix worth
|
|
52
|
+
* pinning, and falls back to the session id so its turns at least stay
|
|
53
|
+
* together. With neither available the field is omitted rather than filled
|
|
54
|
+
* with something shared: a key common to every request would pin unrelated
|
|
40
55
|
* prefixes onto one cache instead of separating them.
|
|
56
|
+
*
|
|
57
|
+
* The digest covers only content already sent upstream in `instructions` and
|
|
58
|
+
* `tools`; no account, device or session identifier reaches the wire.
|
|
41
59
|
*/
|
|
42
60
|
export declare function codexPromptCacheKey(body: ClaudeRequest): string | undefined;
|
|
43
61
|
/** Convert a Claude Messages request into the ChatGPT Codex Responses shape. */
|
|
@@ -237,23 +237,95 @@ function convertClaudeMessage(role, content) {
|
|
|
237
237
|
return input;
|
|
238
238
|
}
|
|
239
239
|
/**
|
|
240
|
-
*
|
|
240
|
+
* Routing key that pins a request to the cache already holding its prefix.
|
|
241
241
|
*
|
|
242
242
|
* Anthropic caching is explicit — the client marks breakpoints and the provider
|
|
243
243
|
* honours them. OpenAI caching is automatic: a prefix is reused only when the
|
|
244
244
|
* request lands on infrastructure already holding it, and `prompt_cache_key` is
|
|
245
|
-
* what pins a
|
|
246
|
-
*
|
|
247
|
-
*
|
|
245
|
+
* what pins a request to one. Without it this hop takes its chances on routing,
|
|
246
|
+
* which is the worst case for a fallback that arrives in bursts separated by
|
|
247
|
+
* long gaps.
|
|
248
248
|
*
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
*
|
|
249
|
+
* The key must therefore describe the *prefix*, not the caller. Keying it by
|
|
250
|
+
* session id gives every session a private namespace, so N agents that share
|
|
251
|
+
* one system prompt and tool set each pay that shared prefix in full rather
|
|
252
|
+
* than paying it once — the cost is linear in fan-out width and invisible,
|
|
253
|
+
* because each request looks individually well-behaved. Across 42 captured
|
|
254
|
+
* fallback bodies the prefixes collapse to 10 distinct values, the largest
|
|
255
|
+
* shared by 12 requests belonging to different sessions.
|
|
256
|
+
*
|
|
257
|
+
* So derive the key from the prefix itself: the instructions and the tool
|
|
258
|
+
* declarations, which are exactly the leading bytes the upstream caches and
|
|
259
|
+
* the only part stable across a conversation's turns. Identical prefixes then
|
|
260
|
+
* share one cache across sessions and subagents, a prefix that genuinely
|
|
261
|
+
* differs still gets its own key so nothing unrelated contends, and turns
|
|
262
|
+
* within one session keep sharing a key because neither input changes between
|
|
263
|
+
* them.
|
|
264
|
+
*
|
|
265
|
+
* A request carrying neither instructions nor tools has no prefix worth
|
|
266
|
+
* pinning, and falls back to the session id so its turns at least stay
|
|
267
|
+
* together. With neither available the field is omitted rather than filled
|
|
268
|
+
* with something shared: a key common to every request would pin unrelated
|
|
254
269
|
* prefixes onto one cache instead of separating them.
|
|
270
|
+
*
|
|
271
|
+
* The digest covers only content already sent upstream in `instructions` and
|
|
272
|
+
* `tools`; no account, device or session identifier reaches the wire.
|
|
255
273
|
*/
|
|
256
274
|
export function codexPromptCacheKey(body) {
|
|
275
|
+
const prefix = codexCachePrefix(body);
|
|
276
|
+
if (prefix !== undefined) {
|
|
277
|
+
return createHash("sha256").update(prefix).digest("hex");
|
|
278
|
+
}
|
|
279
|
+
const sessionId = codexSessionId(body);
|
|
280
|
+
if (sessionId !== undefined) {
|
|
281
|
+
return createHash("sha256").update(sessionId).digest("hex");
|
|
282
|
+
}
|
|
283
|
+
return undefined;
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* How much of the instructions the key covers.
|
|
287
|
+
*
|
|
288
|
+
* Claude Code's system prompt is not byte-stable: measured across live 16,722
|
|
289
|
+
* character prompts, the first 14,768 characters (88.3%) are identical and the
|
|
290
|
+
* divergence sits in a trailing session-memory section that is rewritten every
|
|
291
|
+
* turn. Hashing the whole thing therefore produces a fresh key per request —
|
|
292
|
+
* the opposite of pinning. Hashing a bounded head instead tracks the part the
|
|
293
|
+
* upstream can actually reuse, since its cache is built forward from the first
|
|
294
|
+
* token.
|
|
295
|
+
*
|
|
296
|
+
* 8192 is below the observed divergence point with margin, and the error is
|
|
297
|
+
* asymmetric: too long only costs sharing, while too short pins genuinely
|
|
298
|
+
* unrelated prefixes onto one cache where they evict each other.
|
|
299
|
+
*
|
|
300
|
+
* The bound only helps a prompt long enough to have a tail beyond it. A
|
|
301
|
+
* shorter prompt is hashed whole, so a churning section inside it would move
|
|
302
|
+
* the key every turn — where keying by session used to hold still. Measured on
|
|
303
|
+
* the same corpus, that does not happen: 85 of 92 prompts carrying a system
|
|
304
|
+
* block sit under this bound, and within a session they resolve to a handful
|
|
305
|
+
* of keys rather than one per request (28 requests to 4 keys, 17 to 2, 31 to
|
|
306
|
+
* 2) — those are distinct agents, not one agent churning. Identifying the
|
|
307
|
+
* stable section structurally would remove the assumption, and is worth doing
|
|
308
|
+
* if a prompt ever proves otherwise.
|
|
309
|
+
*/
|
|
310
|
+
const CACHE_KEY_PREFIX_CHARS = 8192;
|
|
311
|
+
/**
|
|
312
|
+
* The leading, turn-stable bytes of a Codex request: a bounded head of the
|
|
313
|
+
* instructions plus the tool names, which distinguish one agent's tool surface
|
|
314
|
+
* from another's when their instructions open identically. Descriptions and
|
|
315
|
+
* schemas are deliberately left out — they sit behind the instructions in the
|
|
316
|
+
* prefix, so two requests agreeing on this much already share a cacheable head
|
|
317
|
+
* worth routing together.
|
|
318
|
+
*/
|
|
319
|
+
function codexCachePrefix(body) {
|
|
320
|
+
const head = (buildSystemInstructions(body) ?? "").slice(0, CACHE_KEY_PREFIX_CHARS);
|
|
321
|
+
const toolNames = (body.tools ?? []).map((tool) => tool.name).join("\u0000");
|
|
322
|
+
if (!head && !toolNames) {
|
|
323
|
+
return undefined;
|
|
324
|
+
}
|
|
325
|
+
return `${head}\u0001${toolNames}`;
|
|
326
|
+
}
|
|
327
|
+
/** Claude Code carries its session id inside `metadata.user_id`. */
|
|
328
|
+
function codexSessionId(body) {
|
|
257
329
|
const raw = body.metadata?.user_id;
|
|
258
330
|
if (typeof raw !== "string" || raw.length === 0) {
|
|
259
331
|
return undefined;
|
|
@@ -270,10 +342,9 @@ export function codexPromptCacheKey(body) {
|
|
|
270
342
|
}
|
|
271
343
|
const fields = parsed;
|
|
272
344
|
const sessionId = fields.session_id ?? fields.parent_session_id;
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
return createHash("sha256").update(sessionId).digest("hex");
|
|
345
|
+
return typeof sessionId === "string" && sessionId.length > 0
|
|
346
|
+
? sessionId
|
|
347
|
+
: undefined;
|
|
277
348
|
}
|
|
278
349
|
/** Convert a Claude Messages request into the ChatGPT Codex Responses shape. */
|
|
279
350
|
export function convertClaudeRequestToCodex(body, model, reasoningEffort) {
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import { jsonSchema, tool } from "../utils/tool.js";
|
|
12
12
|
import { randomBytes } from "crypto";
|
|
13
13
|
import { normalizeJsonSchemaObject } from "../utils/schemaConversion.js";
|
|
14
|
+
import { applyClaudeRequestCacheBreakpoints } from "../utils/anthropicCacheBreakpoints.js";
|
|
14
15
|
// ---------------------------------------------------------------------------
|
|
15
16
|
// Helpers
|
|
16
17
|
// ---------------------------------------------------------------------------
|
|
@@ -603,7 +604,13 @@ export function convertOpenAIToClaudeRequest(openai) {
|
|
|
603
604
|
? openai.stop
|
|
604
605
|
: [openai.stop];
|
|
605
606
|
}
|
|
606
|
-
|
|
607
|
+
// An OpenAI-shaped caller has no way to ask for caching: its provider does it
|
|
608
|
+
// automatically from the first token, so the format carries no equivalent of
|
|
609
|
+
// `cache_control`. Translating faithfully therefore produces a request
|
|
610
|
+
// Anthropic caches at 0% on every turn — and a caller that arrives here after
|
|
611
|
+
// its own pool ran dry would burn the replacement quota faster than the one
|
|
612
|
+
// it was spared. Synthesise the breakpoints the caller could not express.
|
|
613
|
+
return applyClaudeRequestCacheBreakpoints(result);
|
|
607
614
|
}
|
|
608
615
|
/**
|
|
609
616
|
* Convert a non-streaming Claude Messages response to an OpenAI Chat
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* `tool_use`, `tool_result`, `cache_control` and thinking blocks all survive
|
|
13
13
|
* here because nothing reshapes them.
|
|
14
14
|
*/
|
|
15
|
-
import type { UsageContext, VertexPassthroughTerminal, VertexAnthropicPassthroughRequest, VertexAnthropicPassthroughTarget } from "../types/index.js";
|
|
15
|
+
import type { VertexAccessTokenProvider, UsageContext, VertexPassthroughTerminal, VertexAnthropicPassthroughRequest, VertexAnthropicPassthroughTarget } from "../types/index.js";
|
|
16
16
|
/**
|
|
17
17
|
* Translate `output_config` for Vertex.
|
|
18
18
|
*
|
|
@@ -29,6 +29,22 @@ export declare function buildVertexAnthropicPayload(body: Readonly<Record<string
|
|
|
29
29
|
* caller's `stream` flag selects the verb rather than changing the body.
|
|
30
30
|
*/
|
|
31
31
|
export declare function buildVertexAnthropicUrl(target: VertexAnthropicPassthroughTarget): string;
|
|
32
|
+
/**
|
|
33
|
+
* Replace the Google credential lookup, for tests only.
|
|
34
|
+
*
|
|
35
|
+
* Without this seam the two-leg fallback chain cannot be driven end to end.
|
|
36
|
+
* The rest of the hop is reachable — `dispatchVertexAnthropicPassthrough`
|
|
37
|
+
* calls the global `fetch`, which a test can replace — but the credential
|
|
38
|
+
* lookup goes through gaxios, and gaxios resolves its transport to
|
|
39
|
+
* `(await import("node-fetch")).default` rather than `globalThis.fetch`. A
|
|
40
|
+
* test that swaps the global therefore cannot intercept the token exchange,
|
|
41
|
+
* and on a machine with no Application Default Credentials the leg throws
|
|
42
|
+
* before it ever reaches the model. That left the one path that bills in real
|
|
43
|
+
* currency with no end-to-end coverage at all.
|
|
44
|
+
*
|
|
45
|
+
* Pass `undefined` to restore the real credential lookup.
|
|
46
|
+
*/
|
|
47
|
+
export declare function setVertexAccessTokenProviderForTests(provider: VertexAccessTokenProvider | undefined): void;
|
|
32
48
|
/**
|
|
33
49
|
* Send one Anthropic-shaped request to Vertex and return the upstream response
|
|
34
50
|
* untouched, so the caller can stream its bytes straight to the client.
|
|
@@ -135,12 +135,36 @@ function auth() {
|
|
|
135
135
|
cachedAuth ??= new GoogleAuth({ scopes: [CLOUD_PLATFORM_SCOPE] });
|
|
136
136
|
return cachedAuth;
|
|
137
137
|
}
|
|
138
|
+
let accessTokenProviderForTests;
|
|
139
|
+
async function vertexAccessToken() {
|
|
140
|
+
return accessTokenProviderForTests
|
|
141
|
+
? accessTokenProviderForTests()
|
|
142
|
+
: auth().getAccessToken();
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Replace the Google credential lookup, for tests only.
|
|
146
|
+
*
|
|
147
|
+
* Without this seam the two-leg fallback chain cannot be driven end to end.
|
|
148
|
+
* The rest of the hop is reachable — `dispatchVertexAnthropicPassthrough`
|
|
149
|
+
* calls the global `fetch`, which a test can replace — but the credential
|
|
150
|
+
* lookup goes through gaxios, and gaxios resolves its transport to
|
|
151
|
+
* `(await import("node-fetch")).default` rather than `globalThis.fetch`. A
|
|
152
|
+
* test that swaps the global therefore cannot intercept the token exchange,
|
|
153
|
+
* and on a machine with no Application Default Credentials the leg throws
|
|
154
|
+
* before it ever reaches the model. That left the one path that bills in real
|
|
155
|
+
* currency with no end-to-end coverage at all.
|
|
156
|
+
*
|
|
157
|
+
* Pass `undefined` to restore the real credential lookup.
|
|
158
|
+
*/
|
|
159
|
+
export function setVertexAccessTokenProviderForTests(provider) {
|
|
160
|
+
accessTokenProviderForTests = provider;
|
|
161
|
+
}
|
|
138
162
|
/**
|
|
139
163
|
* Send one Anthropic-shaped request to Vertex and return the upstream response
|
|
140
164
|
* untouched, so the caller can stream its bytes straight to the client.
|
|
141
165
|
*/
|
|
142
166
|
export async function dispatchVertexAnthropicPassthrough(request) {
|
|
143
|
-
const token = await
|
|
167
|
+
const token = await vertexAccessToken();
|
|
144
168
|
if (!token) {
|
|
145
169
|
throw new Error("Vertex passthrough could not obtain a Google access token; " +
|
|
146
170
|
"check Application Default Credentials for this project");
|
|
@@ -175,6 +175,22 @@ declare function trackUpstreamReadableStream(source: ReadableStream<Uint8Array>)
|
|
|
175
175
|
* diagnostics the operator wants.
|
|
176
176
|
*/
|
|
177
177
|
declare function redactHeadersForBorrower(headers: Record<string, string>): Record<string, string>;
|
|
178
|
+
/**
|
|
179
|
+
* The model that actually served a fallback turn.
|
|
180
|
+
*
|
|
181
|
+
* One request can leave more than one leg's model in metadata: the Codex leg
|
|
182
|
+
* records its model on the failure path before the chain moves on, and the
|
|
183
|
+
* configured chain tries Codex first. Choosing by key precedence therefore
|
|
184
|
+
* billed a Vertex-served turn as the Codex model — the exact misattribution
|
|
185
|
+
* this file's pricing fix exists to prevent. The account type that reached
|
|
186
|
+
* `logFinalRequest` is the only authoritative signal for which leg answered,
|
|
187
|
+
* so select on that and ignore the other leg's leftovers.
|
|
188
|
+
*/
|
|
189
|
+
export declare function resolveServedFallbackModel(metadata: {
|
|
190
|
+
codexFallbackModel?: unknown;
|
|
191
|
+
vertexFallbackModel?: unknown;
|
|
192
|
+
}, accountType: string | undefined): string | undefined;
|
|
193
|
+
declare function clearClaudeFallbackTerminalEvidence(ctx: ServerContext): void;
|
|
178
194
|
declare function executeClaudeFallbackTranslation(args: {
|
|
179
195
|
ctx: ServerContext;
|
|
180
196
|
body: ClaudeRequest;
|
|
@@ -462,6 +478,7 @@ export declare function isTransientHttpFailure(status: number, errBody: string):
|
|
|
462
478
|
export declare function isUpstreamOverload(status: number, errBody: string): boolean;
|
|
463
479
|
export declare function redactProviderErrorMessage(message: string): string;
|
|
464
480
|
export declare const __testHooks: {
|
|
481
|
+
clearClaudeFallbackTerminalEvidence: typeof clearClaudeFallbackTerminalEvidence;
|
|
465
482
|
normalizeQuotaForAccounts: typeof normalizeQuotaForAccounts;
|
|
466
483
|
resolveHomeIndex: typeof resolveHomeIndex;
|
|
467
484
|
maybeResetPrimaryToHome: typeof maybeResetPrimaryToHome;
|
|
@@ -100,6 +100,16 @@ function resolveRequestLogAccountIdentity(accountLabel, accountType) {
|
|
|
100
100
|
provider: "anthropic",
|
|
101
101
|
};
|
|
102
102
|
}
|
|
103
|
+
if (accountType === "vertex") {
|
|
104
|
+
// A native fallback leg has no OAuth login — the account IS the served
|
|
105
|
+
// model, e.g. "vertex/claude-opus-4-6". Without this branch `provider`
|
|
106
|
+
// stays unset, `annotateRequestPricing` falls back to
|
|
107
|
+
// "openai-compatible", and that resolves to a cross-provider scan which
|
|
108
|
+
// prices the turn from the *requested* alias in the Anthropic table
|
|
109
|
+
// instead of the served model in the Vertex one — a 2.5x under-report on
|
|
110
|
+
// the only leg that bills in real currency.
|
|
111
|
+
return { accountKey: accountLabel, provider: "vertex" };
|
|
112
|
+
}
|
|
103
113
|
return {};
|
|
104
114
|
}
|
|
105
115
|
// ---------------------------------------------------------------------------
|
|
@@ -3567,11 +3577,29 @@ async function loadClaudeProxyAccounts(args) {
|
|
|
3567
3577
|
isClaudeClientRequest: isLikelyClaudeClient(clientHeaders, clientSnapshotBody),
|
|
3568
3578
|
};
|
|
3569
3579
|
}
|
|
3580
|
+
/**
|
|
3581
|
+
* The model that actually served a fallback turn.
|
|
3582
|
+
*
|
|
3583
|
+
* One request can leave more than one leg's model in metadata: the Codex leg
|
|
3584
|
+
* records its model on the failure path before the chain moves on, and the
|
|
3585
|
+
* configured chain tries Codex first. Choosing by key precedence therefore
|
|
3586
|
+
* billed a Vertex-served turn as the Codex model — the exact misattribution
|
|
3587
|
+
* this file's pricing fix exists to prevent. The account type that reached
|
|
3588
|
+
* `logFinalRequest` is the only authoritative signal for which leg answered,
|
|
3589
|
+
* so select on that and ignore the other leg's leftovers.
|
|
3590
|
+
*/
|
|
3591
|
+
export function resolveServedFallbackModel(metadata, accountType) {
|
|
3592
|
+
const served = accountType === "vertex"
|
|
3593
|
+
? metadata.vertexFallbackModel
|
|
3594
|
+
: metadata.codexFallbackModel;
|
|
3595
|
+
return typeof served === "string" && served.length > 0 ? served : undefined;
|
|
3596
|
+
}
|
|
3570
3597
|
function clearClaudeFallbackTerminalEvidence(ctx) {
|
|
3571
3598
|
for (const key of [
|
|
3572
3599
|
"codexFallbackAccount",
|
|
3573
3600
|
"codexFallbackAccountType",
|
|
3574
3601
|
"codexFallbackModel",
|
|
3602
|
+
"vertexFallbackModel",
|
|
3575
3603
|
"codexFallbackFailureUsage",
|
|
3576
3604
|
"codexFallbackDeferredFailure",
|
|
3577
3605
|
"sdkFallbackFailure",
|
|
@@ -4054,6 +4082,19 @@ async function tryConfiguredClaudeFallbackChain(args) {
|
|
|
4054
4082
|
// the caller's pending tool call, which strands every agentic turn.
|
|
4055
4083
|
const vertexModel = fallback.model;
|
|
4056
4084
|
const vertexAccount = `vertex/${vertexModel}`;
|
|
4085
|
+
// A Codex leg that already failed left its own terminal evidence
|
|
4086
|
+
// behind — its model, account, deferred-failure flag and failure
|
|
4087
|
+
// usage. Every one of those is read again when this turn finalizes:
|
|
4088
|
+
// the model would be logged and priced in place of the Vertex one,
|
|
4089
|
+
// the deferred-failure branch would re-attribute the tracer to
|
|
4090
|
+
// "openai" after the Vertex leg set it, and the failure usage would
|
|
4091
|
+
// merge its reasoning tokens into this turn's counts. Start the leg
|
|
4092
|
+
// clean, the same way a fresh upstream dispatch does.
|
|
4093
|
+
clearClaudeFallbackTerminalEvidence(ctx);
|
|
4094
|
+
// Billing follows the model that served the turn. The request log
|
|
4095
|
+
// otherwise records `body.model` — the alias the client asked for —
|
|
4096
|
+
// and prices Opus-on-Vertex at Sonnet rates.
|
|
4097
|
+
ctx.metadata.vertexFallbackModel = vertexModel;
|
|
4057
4098
|
response = await executeVertexAnthropicFallback({
|
|
4058
4099
|
body,
|
|
4059
4100
|
model: vertexModel,
|
|
@@ -6212,18 +6253,24 @@ function createClaudeRequestRuntimeContext(args) {
|
|
|
6212
6253
|
recordFinalSuccess(finalAccountLabel, finalAccountType);
|
|
6213
6254
|
}
|
|
6214
6255
|
const traceCtx = tracer?.getTraceContext();
|
|
6256
|
+
const servedModel = resolveServedFallbackModel(ctx.metadata, finalAccountType);
|
|
6215
6257
|
logRequest({
|
|
6216
6258
|
timestamp: new Date().toISOString(),
|
|
6217
6259
|
requestId: ctx.requestId,
|
|
6218
6260
|
method: ctx.method,
|
|
6219
6261
|
path: ctx.path,
|
|
6220
|
-
model:
|
|
6221
|
-
|
|
6222
|
-
: body.model,
|
|
6223
|
-
...(typeof ctx.metadata.codexFallbackModel === "string"
|
|
6262
|
+
model: servedModel ?? body.model,
|
|
6263
|
+
...(servedModel
|
|
6224
6264
|
? {
|
|
6225
6265
|
requestedModel: body.model,
|
|
6226
|
-
|
|
6266
|
+
// Codex reports input inclusive of cached tokens; the Vertex
|
|
6267
|
+
// Anthropic wire reports them separately, exactly as the native
|
|
6268
|
+
// path does, so the flag stays Codex-only. Keyed off the serving
|
|
6269
|
+
// account for the same reason `servedModel` is: a failed Codex leg
|
|
6270
|
+
// leaves its model behind on a turn Vertex went on to serve.
|
|
6271
|
+
...(finalAccountType === "vertex"
|
|
6272
|
+
? {}
|
|
6273
|
+
: { inputIncludesCachedTokens: false }),
|
|
6227
6274
|
}
|
|
6228
6275
|
: {}),
|
|
6229
6276
|
...(ctx.metadata.contextPreflight
|
|
@@ -8777,6 +8824,7 @@ export function redactProviderErrorMessage(message) {
|
|
|
8777
8824
|
// spinning up a full proxy. Keep this surface small.
|
|
8778
8825
|
// ---------------------------------------------------------------------------
|
|
8779
8826
|
export const __testHooks = {
|
|
8827
|
+
clearClaudeFallbackTerminalEvidence,
|
|
8780
8828
|
normalizeQuotaForAccounts,
|
|
8781
8829
|
resolveHomeIndex,
|
|
8782
8830
|
maybeResetPrimaryToHome,
|
|
@@ -2281,6 +2281,14 @@ export type VertexAnthropicTool = {
|
|
|
2281
2281
|
};
|
|
2282
2282
|
cache_control?: VertexAnthropicCacheControl;
|
|
2283
2283
|
};
|
|
2284
|
+
/**
|
|
2285
|
+
* Supplies the bearer token for a Vertex publisher call.
|
|
2286
|
+
*
|
|
2287
|
+
* Exists so a test can stand in for the Google credential lookup: gaxios
|
|
2288
|
+
* resolves its transport to node-fetch rather than `globalThis.fetch`, so a
|
|
2289
|
+
* test that swaps the global cannot intercept the token exchange.
|
|
2290
|
+
*/
|
|
2291
|
+
export type VertexAccessTokenProvider = () => Promise<string | null | undefined>;
|
|
2284
2292
|
/** Input to `applyVertexAnthropicCacheBreakpoints`. */
|
|
2285
2293
|
export type VertexAnthropicCacheInput = {
|
|
2286
2294
|
system?: string;
|
package/dist/types/proxy.d.ts
CHANGED
|
@@ -42,10 +42,27 @@ export type ModelRouterInterface = {
|
|
|
42
42
|
getModelMappings?: () => ModelMapping[];
|
|
43
43
|
getPassthroughModels?: () => string[];
|
|
44
44
|
};
|
|
45
|
+
/**
|
|
46
|
+
* Anthropic prompt-cache breakpoint marker.
|
|
47
|
+
*
|
|
48
|
+
* Anthropic caching is explicit: the prefix up to each marker is cached, and a
|
|
49
|
+
* request carrying none is cached not at all. Four markers is the hard ceiling.
|
|
50
|
+
*/
|
|
51
|
+
export type ClaudeCacheControl = {
|
|
52
|
+
type: "ephemeral";
|
|
53
|
+
/**
|
|
54
|
+
* How long the marked prefix stays cached. The API accepts a 5-minute or a
|
|
55
|
+
* 1-hour window, priced at 1.25x and 2x base input respectively; omitting it
|
|
56
|
+
* means 5 minutes. Without this field a caller cannot express the 1-hour
|
|
57
|
+
* marker at all.
|
|
58
|
+
*/
|
|
59
|
+
ttl?: "5m" | "1h";
|
|
60
|
+
};
|
|
45
61
|
/** A single text block in a Claude content array. */
|
|
46
62
|
export type ClaudeTextBlock = {
|
|
47
63
|
type: "text";
|
|
48
64
|
text: string;
|
|
65
|
+
cache_control?: ClaudeCacheControl;
|
|
49
66
|
};
|
|
50
67
|
/** A single image block in a Claude content array. */
|
|
51
68
|
export type ClaudeImageBlock = {
|
|
@@ -69,6 +86,7 @@ export type ClaudeToolResultBlock = {
|
|
|
69
86
|
type: "tool_result";
|
|
70
87
|
tool_use_id: string;
|
|
71
88
|
content: string | ClaudeContentBlock[];
|
|
89
|
+
cache_control?: ClaudeCacheControl;
|
|
72
90
|
};
|
|
73
91
|
/** A thinking/reasoning block in a Claude content array. */
|
|
74
92
|
export type ClaudeThinkingBlock = {
|
|
@@ -86,6 +104,7 @@ export type ClaudeTool = {
|
|
|
86
104
|
name: string;
|
|
87
105
|
description?: string;
|
|
88
106
|
input_schema: Record<string, unknown>;
|
|
107
|
+
cache_control?: ClaudeCacheControl;
|
|
89
108
|
};
|
|
90
109
|
/** Metadata attached to a Claude Messages API request. */
|
|
91
110
|
export type ClaudeMetadata = {
|
|
@@ -99,10 +118,7 @@ export type ClaudeRequest = {
|
|
|
99
118
|
model: string;
|
|
100
119
|
messages: ClaudeMessage[];
|
|
101
120
|
max_tokens: number;
|
|
102
|
-
system?: string |
|
|
103
|
-
type: "text";
|
|
104
|
-
text: string;
|
|
105
|
-
}>;
|
|
121
|
+
system?: string | ClaudeTextBlock[];
|
|
106
122
|
temperature?: number;
|
|
107
123
|
top_p?: number;
|
|
108
124
|
top_k?: number;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { VertexAnthropicCacheInput, VertexAnthropicCacheOutput, VertexAnthropicMessage } from "../types/index.js";
|
|
1
|
+
import type { ClaudeRequest, VertexAnthropicCacheInput, VertexAnthropicCacheOutput, VertexAnthropicMessage } from "../types/index.js";
|
|
2
2
|
/** Anthropic allows at most four `cache_control` breakpoints per request. */
|
|
3
3
|
export declare const ANTHROPIC_MAX_CACHE_BREAKPOINTS = 4;
|
|
4
4
|
/**
|
|
@@ -41,3 +41,24 @@ export declare function countAnthropicCacheMarkers(input: {
|
|
|
41
41
|
* the input array is cloned, never mutated.
|
|
42
42
|
*/
|
|
43
43
|
export declare function applyAnthropicHistoryCacheBreakpoints(input: ReadonlyArray<VertexAnthropicMessage>, budget: number): VertexAnthropicMessage[];
|
|
44
|
+
/**
|
|
45
|
+
* Breakpoint placement for a Claude Messages request the proxy built itself,
|
|
46
|
+
* rather than one a client authored.
|
|
47
|
+
*
|
|
48
|
+
* A request translated from another wire format arrives with no markers at
|
|
49
|
+
* all, because the format it came from has no way to express one: OpenAI-shaped
|
|
50
|
+
* callers rely on automatic prefix caching, which Anthropic does not do. A
|
|
51
|
+
* faithful translation is therefore cached at 0% on every turn, and a caller
|
|
52
|
+
* that fell back here from an exhausted pool burns the replacement quota faster
|
|
53
|
+
* than the one it was spared.
|
|
54
|
+
*
|
|
55
|
+
* Placement follows Anthropic's `tools → system → messages` rendering: one
|
|
56
|
+
* marker on the last system block covers tools *and* system in a single
|
|
57
|
+
* breakpoint, and the rest roll along the tail of the history so a growing
|
|
58
|
+
* conversation keeps its settled prefix cached. Markers the caller supplied are
|
|
59
|
+
* counted first and never displaced — this only spends what is left of the
|
|
60
|
+
* four.
|
|
61
|
+
*
|
|
62
|
+
* Pure: inputs are cloned, never mutated.
|
|
63
|
+
*/
|
|
64
|
+
export declare function applyClaudeRequestCacheBreakpoints(request: ClaudeRequest): ClaudeRequest;
|
|
@@ -148,3 +148,116 @@ function markLastContentBlock(messages, i) {
|
|
|
148
148
|
messages[i] = { ...message, content };
|
|
149
149
|
return true;
|
|
150
150
|
}
|
|
151
|
+
/**
|
|
152
|
+
* Breakpoint placement for a Claude Messages request the proxy built itself,
|
|
153
|
+
* rather than one a client authored.
|
|
154
|
+
*
|
|
155
|
+
* A request translated from another wire format arrives with no markers at
|
|
156
|
+
* all, because the format it came from has no way to express one: OpenAI-shaped
|
|
157
|
+
* callers rely on automatic prefix caching, which Anthropic does not do. A
|
|
158
|
+
* faithful translation is therefore cached at 0% on every turn, and a caller
|
|
159
|
+
* that fell back here from an exhausted pool burns the replacement quota faster
|
|
160
|
+
* than the one it was spared.
|
|
161
|
+
*
|
|
162
|
+
* Placement follows Anthropic's `tools → system → messages` rendering: one
|
|
163
|
+
* marker on the last system block covers tools *and* system in a single
|
|
164
|
+
* breakpoint, and the rest roll along the tail of the history so a growing
|
|
165
|
+
* conversation keeps its settled prefix cached. Markers the caller supplied are
|
|
166
|
+
* counted first and never displaced — this only spends what is left of the
|
|
167
|
+
* four.
|
|
168
|
+
*
|
|
169
|
+
* Pure: inputs are cloned, never mutated.
|
|
170
|
+
*/
|
|
171
|
+
export function applyClaudeRequestCacheBreakpoints(request) {
|
|
172
|
+
const existing = countAnthropicCacheMarkers({
|
|
173
|
+
system: request.system,
|
|
174
|
+
tools: request.tools,
|
|
175
|
+
messages: [],
|
|
176
|
+
});
|
|
177
|
+
const inMessages = request.messages.reduce((n, m) => n +
|
|
178
|
+
(Array.isArray(m.content)
|
|
179
|
+
? m.content.filter((b) => "cache_control" in b && b.cache_control)
|
|
180
|
+
.length
|
|
181
|
+
: 0), 0);
|
|
182
|
+
let budget = MAX_BREAKPOINTS - existing - inMessages;
|
|
183
|
+
if (budget <= 0) {
|
|
184
|
+
return request;
|
|
185
|
+
}
|
|
186
|
+
const out = { ...request };
|
|
187
|
+
// One marker on the stable head. The last system block is the better anchor
|
|
188
|
+
// than the last tool, because system renders after tools and so a single
|
|
189
|
+
// marker there caches both.
|
|
190
|
+
if (Array.isArray(out.system) && out.system.length > 0 && existing === 0) {
|
|
191
|
+
const system = out.system.map((b) => ({ ...b }));
|
|
192
|
+
system[system.length - 1] = {
|
|
193
|
+
...system[system.length - 1],
|
|
194
|
+
cache_control: CLAUDE_EPHEMERAL,
|
|
195
|
+
};
|
|
196
|
+
out.system = system;
|
|
197
|
+
budget--;
|
|
198
|
+
}
|
|
199
|
+
else if ((!out.system || (Array.isArray(out.system) && out.system.length === 0)) &&
|
|
200
|
+
out.tools &&
|
|
201
|
+
out.tools.length > 0 &&
|
|
202
|
+
existing === 0) {
|
|
203
|
+
const tools = out.tools.map((t) => ({ ...t }));
|
|
204
|
+
tools[tools.length - 1] = {
|
|
205
|
+
...tools[tools.length - 1],
|
|
206
|
+
cache_control: CLAUDE_EPHEMERAL,
|
|
207
|
+
};
|
|
208
|
+
out.tools = tools;
|
|
209
|
+
budget--;
|
|
210
|
+
}
|
|
211
|
+
if (budget > 0) {
|
|
212
|
+
out.messages = applyClaudeHistoryBreakpoints(out.messages, budget);
|
|
213
|
+
}
|
|
214
|
+
return out;
|
|
215
|
+
}
|
|
216
|
+
const CLAUDE_EPHEMERAL = { type: "ephemeral" };
|
|
217
|
+
/**
|
|
218
|
+
* Roll `budget` markers backwards along the history, marking the last content
|
|
219
|
+
* block of each message that can carry one. Only text and tool_result blocks
|
|
220
|
+
* accept `cache_control`; a message ending in anything else is skipped without
|
|
221
|
+
* spending budget.
|
|
222
|
+
*
|
|
223
|
+
* A message whose content is a plain string is promoted to the single text
|
|
224
|
+
* block the marker has to live on. Skipping strings instead would have left an
|
|
225
|
+
* ordinary conversation with no history breakpoint at all, since the OpenAI
|
|
226
|
+
* bridge emits every plain-text turn as a string — which is precisely the
|
|
227
|
+
* growing history this is here to keep cached.
|
|
228
|
+
*/
|
|
229
|
+
function applyClaudeHistoryBreakpoints(input, budget) {
|
|
230
|
+
const messages = input.map((m) => ({ ...m }));
|
|
231
|
+
let remaining = Math.max(0, Math.min(budget, MAX_BREAKPOINTS));
|
|
232
|
+
for (let i = messages.length - 1; i >= 0 && remaining > 0; i--) {
|
|
233
|
+
const content = messages[i].content;
|
|
234
|
+
if (typeof content === "string") {
|
|
235
|
+
if (content.length === 0) {
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
messages[i] = {
|
|
239
|
+
...messages[i],
|
|
240
|
+
content: [
|
|
241
|
+
{ type: "text", text: content, cache_control: CLAUDE_EPHEMERAL },
|
|
242
|
+
],
|
|
243
|
+
};
|
|
244
|
+
remaining--;
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
if (!Array.isArray(content) || content.length === 0) {
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
const last = content[content.length - 1];
|
|
251
|
+
if (last.type !== "text" && last.type !== "tool_result") {
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
if (last.cache_control) {
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
const blocks = content.map((b) => ({ ...b }));
|
|
258
|
+
blocks[blocks.length - 1] = { ...last, cache_control: CLAUDE_EPHEMERAL };
|
|
259
|
+
messages[i] = { ...messages[i], content: blocks };
|
|
260
|
+
remaining--;
|
|
261
|
+
}
|
|
262
|
+
return messages;
|
|
263
|
+
}
|