@juspay/neurolink 12.17.3 → 12.18.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/CHANGELOG.md +3 -7
- package/dist/browser/neurolink.min.js +405 -404
- package/dist/neurolink.js +27 -2
- package/dist/providers/googleAiStudio/client.js +10 -0
- package/dist/proxy/proxyContextPreflight.js +85 -23
- package/dist/proxy/proxyHistoryTruncation.d.ts +24 -0
- package/dist/proxy/proxyHistoryTruncation.js +171 -0
- package/dist/proxy/vertexAnthropicFallback.d.ts +82 -0
- package/dist/proxy/vertexAnthropicFallback.js +339 -0
- package/dist/server/routes/claudeProxyRoutes.js +39 -0
- package/dist/types/proxy.d.ts +22 -0
- package/dist/types/proxyContext.d.ts +19 -1
- package/docs-site/static/search-index.json +1 -1
- package/package.json +3 -2
package/dist/neurolink.js
CHANGED
|
@@ -4470,6 +4470,14 @@ Current user's request: ${currentInput}`;
|
|
|
4470
4470
|
// remains false for backwards-compat with existing listeners that only
|
|
4471
4471
|
// branch on the boolean.
|
|
4472
4472
|
const aborted = isAbortError(error);
|
|
4473
|
+
// A native path that already emitted its own `generation:end` for this
|
|
4474
|
+
// failure marks the error, because on the failure path it has no result
|
|
4475
|
+
// object to mark. Re-emitting here would double every native failure.
|
|
4476
|
+
if (error &&
|
|
4477
|
+
typeof error === "object" &&
|
|
4478
|
+
error._generationEndEmitted) {
|
|
4479
|
+
return;
|
|
4480
|
+
}
|
|
4473
4481
|
try {
|
|
4474
4482
|
this.emitter.emit("generation:end", {
|
|
4475
4483
|
provider: errProvider,
|
|
@@ -6501,10 +6509,27 @@ Current user's request: ${currentInput}`;
|
|
|
6501
6509
|
// naming the one provider that failed is clearer than the generic
|
|
6502
6510
|
// multi-provider wording. The multi-provider prefix is left untouched
|
|
6503
6511
|
// so existing message-matching suites keep passing.
|
|
6512
|
+
// Both wrappers replace the provider's error with a new one, discarding
|
|
6513
|
+
// anything attached to it. `_generationEndEmitted` has to survive: lose it
|
|
6514
|
+
// and emitGenerateErrorEvent emits a second event for a failure the native
|
|
6515
|
+
// provider already reported. Returns the error rather than throwing so each
|
|
6516
|
+
// call site keeps its own `throw` — a never-returning helper reads better
|
|
6517
|
+
// but tsconfig.cli.json will not accept it as terminating the enclosing
|
|
6518
|
+
// function (TS2366). `cause` is set because the original error and its
|
|
6519
|
+
// stack were simply being thrown away.
|
|
6520
|
+
const wrapProviderError = (message) => {
|
|
6521
|
+
const wrapped = new Error(message, { cause: lastError });
|
|
6522
|
+
if (lastError &&
|
|
6523
|
+
lastError._generationEndEmitted) {
|
|
6524
|
+
wrapped._generationEndEmitted =
|
|
6525
|
+
true;
|
|
6526
|
+
}
|
|
6527
|
+
return wrapped;
|
|
6528
|
+
};
|
|
6504
6529
|
if (providersToTry.length === 1) {
|
|
6505
|
-
throw
|
|
6530
|
+
throw wrapProviderError(`Provider ${providersToTry[0]} failed: ${lastError?.message || "Unknown error"}`);
|
|
6506
6531
|
}
|
|
6507
|
-
throw
|
|
6532
|
+
throw wrapProviderError(`Failed to generate text with all providers. Last error: ${lastError?.message || "Unknown error"}`);
|
|
6508
6533
|
}
|
|
6509
6534
|
/**
|
|
6510
6535
|
* Create tool-aware system prompt that informs AI about available tools
|
|
@@ -1364,6 +1364,16 @@ export class GoogleAIStudioProvider extends BaseProvider {
|
|
|
1364
1364
|
result._generationEndEmitted =
|
|
1365
1365
|
true;
|
|
1366
1366
|
}
|
|
1367
|
+
// The failure path has no result to carry that flag — `result` is null —
|
|
1368
|
+
// so mark the error instead. Without this the provider's own
|
|
1369
|
+
// `generation:end` (success: false) and the SDK's emitGenerateErrorEvent
|
|
1370
|
+
// both fire for one failed native generate, double-counting every failure
|
|
1371
|
+
// in analytics and as two Pipeline B spans. The success path above was
|
|
1372
|
+
// already deduplicated; only the catch branch was missed.
|
|
1373
|
+
if (!success && error && typeof error === "object") {
|
|
1374
|
+
error._generationEndEmitted =
|
|
1375
|
+
true;
|
|
1376
|
+
}
|
|
1367
1377
|
emitter.emit("generation:end", {
|
|
1368
1378
|
provider: this.providerName,
|
|
1369
1379
|
responseTime: Date.now() - startTime,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { getRuntimeContextWindow, getRuntimeOutputCeiling, } from "../constants/contextWindows.js";
|
|
2
2
|
import { estimateTokens, IMAGE_TOKEN_ESTIMATE, } from "../utils/tokenEstimation.js";
|
|
3
|
+
import { truncateHistoryForBudget } from "./proxyHistoryTruncation.js";
|
|
3
4
|
/** Local refusal before spending a provider request; never silently truncates. */
|
|
4
5
|
export class ProxyContextPreflightError extends Error {
|
|
5
6
|
code;
|
|
@@ -65,13 +66,32 @@ export function parseProxyContextPolicy(raw) {
|
|
|
65
66
|
throw new ProxyContextPreflightError("invalid_context_policy", "models must be a bounded map of provider/model limits");
|
|
66
67
|
}
|
|
67
68
|
for (const [key, limit] of Object.entries(value.models)) {
|
|
69
|
+
// Read each budget into a local before comparing: narrowing from the
|
|
70
|
+
// `positive` predicate does not survive several `||` operands when it is
|
|
71
|
+
// applied to a property access, and the looser compiler flags used by the
|
|
72
|
+
// package build then see `unknown` on one side of `>=`.
|
|
73
|
+
const contextWindow = record(limit) ? limit.contextWindow : undefined;
|
|
74
|
+
const maxOutputTokens = record(limit) ? limit.maxOutputTokens : undefined;
|
|
75
|
+
const compactAt = record(limit) ? limit.compactAtTokens : undefined;
|
|
76
|
+
const compactTo = record(limit) ? limit.compactToTokens : undefined;
|
|
68
77
|
if (!key.includes("/") ||
|
|
69
78
|
!record(limit) ||
|
|
70
|
-
!positive(
|
|
71
|
-
(
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
79
|
+
!positive(contextWindow) ||
|
|
80
|
+
(maxOutputTokens !== undefined && !positive(maxOutputTokens)) ||
|
|
81
|
+
(compactAt !== undefined &&
|
|
82
|
+
(!positive(compactAt) || compactAt >= contextWindow)) ||
|
|
83
|
+
(compactTo !== undefined &&
|
|
84
|
+
(!positive(compactTo) ||
|
|
85
|
+
!positive(compactAt) ||
|
|
86
|
+
compactTo >= compactAt)) ||
|
|
87
|
+
(compactAt !== undefined && compactTo === undefined) ||
|
|
88
|
+
Object.keys(limit).some((k) => ![
|
|
89
|
+
"contextWindow",
|
|
90
|
+
"maxOutputTokens",
|
|
91
|
+
"compactAtTokens",
|
|
92
|
+
"compactToTokens",
|
|
93
|
+
].includes(k))) {
|
|
94
|
+
throw new ProxyContextPreflightError("invalid_context_policy", "Each model needs an explicit provider/model key, a positive contextWindow/output limit, and a compaction target below its trigger");
|
|
75
95
|
}
|
|
76
96
|
}
|
|
77
97
|
}
|
|
@@ -234,16 +254,21 @@ export function prepareProxyRequestContext(args) {
|
|
|
234
254
|
: {}),
|
|
235
255
|
};
|
|
236
256
|
}
|
|
237
|
-
|
|
238
|
-
|
|
257
|
+
let wire = body;
|
|
258
|
+
// Fixed cost and history are tracked separately: truncation can remove the
|
|
259
|
+
// only image in a request, and a sticky flag would then keep the ceiling
|
|
260
|
+
// check disabled for a request that no longer carries any media.
|
|
261
|
+
const fixedState = { multimodal: false };
|
|
262
|
+
let historyState = { multimodal: false };
|
|
263
|
+
const isMultimodal = () => fixedState.multimodal || historyState.multimodal;
|
|
239
264
|
const provider = args.provider === "codex" ? "openai" : args.provider;
|
|
240
|
-
const toolsTokensEstimate = estimateValue(wire.tools, provider,
|
|
265
|
+
const toolsTokensEstimate = estimateValue(wire.tools, provider, fixedState);
|
|
241
266
|
const instructionsTokensEstimate = [
|
|
242
267
|
wire.instructions,
|
|
243
268
|
wire.system,
|
|
244
269
|
wire.systemPrompt,
|
|
245
270
|
wire.systemInstruction,
|
|
246
|
-
].reduce((sum, value) => sum + estimateValue(value, provider,
|
|
271
|
+
].reduce((sum, value) => sum + estimateValue(value, provider, fixedState), 0);
|
|
247
272
|
const schemaTokensEstimate = [
|
|
248
273
|
wire.response_format,
|
|
249
274
|
wire.output_config,
|
|
@@ -256,19 +281,53 @@ export function prepareProxyRequestContext(args) {
|
|
|
256
281
|
record(wire.generationConfig)
|
|
257
282
|
? wire.generationConfig.responseJsonSchema
|
|
258
283
|
: undefined,
|
|
259
|
-
].reduce((sum, value) => sum + estimateValue(value, provider,
|
|
260
|
-
const
|
|
284
|
+
].reduce((sum, value) => sum + estimateValue(value, provider, fixedState), 0);
|
|
285
|
+
const estimate = (value) => estimateValue(value, provider, historyState);
|
|
286
|
+
const fixedTokensEstimate = 24 +
|
|
261
287
|
schemaTokensEstimate +
|
|
262
288
|
toolsTokensEstimate +
|
|
263
|
-
instructionsTokensEstimate
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
289
|
+
instructionsTokensEstimate;
|
|
290
|
+
const historyTokensEstimate = () => [
|
|
291
|
+
wire.messages,
|
|
292
|
+
wire.input,
|
|
293
|
+
wire.contents,
|
|
294
|
+
wire.conversationMessages,
|
|
295
|
+
wire.prompt,
|
|
296
|
+
].reduce((sum, value) => sum + estimate(value), 0);
|
|
297
|
+
let inputTokensEstimate = fixedTokensEstimate + historyTokensEstimate();
|
|
271
298
|
const configured = policy.models?.[`${args.provider}/${args.model}`];
|
|
299
|
+
// Cost control: reduce history before the refusal checks below, so those see
|
|
300
|
+
// the reduced estimate and a bounded request is dispatched instead of a
|
|
301
|
+
// rejected one. Runs on every dispatch path because every path lands here.
|
|
302
|
+
let historyModified = false;
|
|
303
|
+
let historyUnitsRemoved = 0;
|
|
304
|
+
const inputTokensBeforeTruncation = inputTokensEstimate;
|
|
305
|
+
if (configured?.compactAtTokens !== undefined &&
|
|
306
|
+
configured.compactToTokens !== undefined &&
|
|
307
|
+
inputTokensEstimate > configured.compactAtTokens) {
|
|
308
|
+
const truncated = truncateHistoryForBudget({
|
|
309
|
+
body,
|
|
310
|
+
inputTokensEstimate,
|
|
311
|
+
targetTokens: configured.compactToTokens,
|
|
312
|
+
estimate,
|
|
313
|
+
});
|
|
314
|
+
if (truncated.historyModified) {
|
|
315
|
+
body = truncated.body;
|
|
316
|
+
wire = body;
|
|
317
|
+
historyModified = true;
|
|
318
|
+
historyUnitsRemoved = truncated.unitsRemoved;
|
|
319
|
+
historyState = { multimodal: false };
|
|
320
|
+
inputTokensEstimate = fixedTokensEstimate + historyTokensEstimate();
|
|
321
|
+
}
|
|
322
|
+
// The design requires a typed local failure rather than a silent dispatch
|
|
323
|
+
// above the configured bound: fixed context alone, or the one unit that
|
|
324
|
+
// must be retained, can exceed the target with nothing left to remove.
|
|
325
|
+
if (inputTokensEstimate > configured.compactToTokens) {
|
|
326
|
+
throw new ProxyContextPreflightError("proxy_context_compaction_failed", `Compaction could not bring ${args.provider}/${args.model} within ` +
|
|
327
|
+
`${configured.compactToTokens} tokens; ${inputTokensEstimate} remain ` +
|
|
328
|
+
`after removing every removable unit`);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
272
331
|
const discovered = getRuntimeContextWindow(args.provider, args.model);
|
|
273
332
|
const contextWindow = configured?.contextWindow ?? discovered;
|
|
274
333
|
const outputCeiling = configured?.maxOutputTokens ??
|
|
@@ -306,10 +365,13 @@ export function prepareProxyRequestContext(args) {
|
|
|
306
365
|
? "discovered"
|
|
307
366
|
: "unknown",
|
|
308
367
|
tokenCountSource: "estimated",
|
|
309
|
-
multimodalEstimate:
|
|
368
|
+
multimodalEstimate: isMultimodal(),
|
|
310
369
|
originalToolCount: toolCount(tools),
|
|
311
370
|
retainedToolCount: toolCount(wire.tools),
|
|
312
|
-
historyModified
|
|
371
|
+
historyModified,
|
|
372
|
+
...(historyModified
|
|
373
|
+
? { historyUnitsRemoved, inputTokensBeforeTruncation }
|
|
374
|
+
: {}),
|
|
313
375
|
};
|
|
314
376
|
if (policy.maxInputTokens !== undefined &&
|
|
315
377
|
inputTokensEstimate > policy.maxInputTokens) {
|
|
@@ -323,9 +385,9 @@ export function prepareProxyRequestContext(args) {
|
|
|
323
385
|
}
|
|
324
386
|
if (contextWindow !== undefined &&
|
|
325
387
|
policy.enforceDiscoveredLimits !== false &&
|
|
326
|
-
!
|
|
388
|
+
!isMultimodal() &&
|
|
327
389
|
inputTokensEstimate + outputTokensReserve > contextWindow) {
|
|
328
|
-
throw new ProxyContextPreflightError("proxy_context_window_exceeded",
|
|
390
|
+
throw new ProxyContextPreflightError("proxy_context_window_exceeded", `Estimated input plus output/reasoning reserve exceeds the serving model context window; ${historyModified ? "history was truncated and still does not fit" : "no history was truncated"}`, evidence);
|
|
329
391
|
}
|
|
330
392
|
return {
|
|
331
393
|
body,
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic pre-dispatch history truncation.
|
|
3
|
+
*
|
|
4
|
+
* Runs inside the shared context preflight, so one implementation bounds input
|
|
5
|
+
* cost on every dispatch path: native Anthropic, the translated/SDK fallback
|
|
6
|
+
* that serves Vertex, and native or bridged Codex.
|
|
7
|
+
*
|
|
8
|
+
* Deliberately provider-free. Summarizing history would add an outbound model
|
|
9
|
+
* call to the very requests this exists to make cheaper, so reduction is pure
|
|
10
|
+
* removal of the oldest complete units.
|
|
11
|
+
*/
|
|
12
|
+
import type { ProxyHistoryEstimate, ProxyHistoryTruncationResult } from "../types/index.js";
|
|
13
|
+
/**
|
|
14
|
+
* Remove the oldest complete units until the estimate reaches `targetTokens`.
|
|
15
|
+
*
|
|
16
|
+
* Returns the body unchanged when there is nothing safe to remove. The newest
|
|
17
|
+
* unit is always retained: it carries the turn being answered.
|
|
18
|
+
*/
|
|
19
|
+
export declare function truncateHistoryForBudget<T extends object>(args: {
|
|
20
|
+
body: T;
|
|
21
|
+
inputTokensEstimate: number;
|
|
22
|
+
targetTokens: number;
|
|
23
|
+
estimate: ProxyHistoryEstimate;
|
|
24
|
+
}): ProxyHistoryTruncationResult<T>;
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic pre-dispatch history truncation.
|
|
3
|
+
*
|
|
4
|
+
* Runs inside the shared context preflight, so one implementation bounds input
|
|
5
|
+
* cost on every dispatch path: native Anthropic, the translated/SDK fallback
|
|
6
|
+
* that serves Vertex, and native or bridged Codex.
|
|
7
|
+
*
|
|
8
|
+
* Deliberately provider-free. Summarizing history would add an outbound model
|
|
9
|
+
* call to the very requests this exists to make cheaper, so reduction is pure
|
|
10
|
+
* removal of the oldest complete units.
|
|
11
|
+
*/
|
|
12
|
+
function record(value) {
|
|
13
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* A turn that cannot legally open a history.
|
|
17
|
+
*
|
|
18
|
+
* The Messages API rejects a conversation whose first message is not from the
|
|
19
|
+
* user, so truncation that stops on an assistant turn would trade a local
|
|
20
|
+
* refusal for an upstream 400. Items with no role (Codex `function_call` and
|
|
21
|
+
* `function_call_output`) are not turns and are left alone.
|
|
22
|
+
*/
|
|
23
|
+
function isNonUserTurn(item) {
|
|
24
|
+
return record(item) && typeof item.role === "string" && item.role !== "user";
|
|
25
|
+
}
|
|
26
|
+
function blocksOf(item) {
|
|
27
|
+
return Array.isArray(item.content) ? item.content : [item];
|
|
28
|
+
}
|
|
29
|
+
/** Tool-call ids this item opens. */
|
|
30
|
+
function definedCallIds(item) {
|
|
31
|
+
if (!record(item)) {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
const ids = [];
|
|
35
|
+
if (item.type === "function_call" && typeof item.call_id === "string") {
|
|
36
|
+
ids.push(item.call_id);
|
|
37
|
+
}
|
|
38
|
+
for (const block of blocksOf(item)) {
|
|
39
|
+
if (record(block) &&
|
|
40
|
+
block.type === "tool_use" &&
|
|
41
|
+
typeof block.id === "string") {
|
|
42
|
+
ids.push(block.id);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return ids;
|
|
46
|
+
}
|
|
47
|
+
/** Tool-call ids this item answers. */
|
|
48
|
+
function referencedCallIds(item) {
|
|
49
|
+
if (!record(item)) {
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
const ids = [];
|
|
53
|
+
if (item.type === "function_call_output" &&
|
|
54
|
+
typeof item.call_id === "string") {
|
|
55
|
+
ids.push(item.call_id);
|
|
56
|
+
}
|
|
57
|
+
for (const block of blocksOf(item)) {
|
|
58
|
+
if (record(block) &&
|
|
59
|
+
block.type === "tool_result" &&
|
|
60
|
+
typeof block.tool_use_id === "string") {
|
|
61
|
+
ids.push(block.tool_use_id);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return ids;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Group items so a tool call and every answer to it stay in one unit.
|
|
68
|
+
*
|
|
69
|
+
* A unit stays open while any call it opened is still unanswered, so removing
|
|
70
|
+
* whole units can never strand a `tool_result` or a `function_call_output`.
|
|
71
|
+
*/
|
|
72
|
+
function groupIntoUnits(items) {
|
|
73
|
+
const units = [];
|
|
74
|
+
let current = [];
|
|
75
|
+
const awaiting = new Set();
|
|
76
|
+
for (let index = 0; index < items.length; index += 1) {
|
|
77
|
+
const item = items[index];
|
|
78
|
+
current.push(index);
|
|
79
|
+
for (const id of referencedCallIds(item)) {
|
|
80
|
+
awaiting.delete(id);
|
|
81
|
+
}
|
|
82
|
+
for (const id of definedCallIds(item)) {
|
|
83
|
+
awaiting.add(id);
|
|
84
|
+
}
|
|
85
|
+
if (awaiting.size === 0) {
|
|
86
|
+
units.push(current);
|
|
87
|
+
current = [];
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (current.length > 0) {
|
|
91
|
+
// An unanswered trailing call still belongs with the turn that opened it.
|
|
92
|
+
units.push(current);
|
|
93
|
+
}
|
|
94
|
+
return units;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Pick the history container.
|
|
98
|
+
*
|
|
99
|
+
* `input` is overloaded across paths: the translated/SDK shape carries the
|
|
100
|
+
* current turn there as an object, while Codex carries the item array. Only an
|
|
101
|
+
* array is history, so the object form is never touched.
|
|
102
|
+
*/
|
|
103
|
+
function historyFieldOf(wire) {
|
|
104
|
+
if (Array.isArray(wire.messages)) {
|
|
105
|
+
return "messages";
|
|
106
|
+
}
|
|
107
|
+
if (Array.isArray(wire.conversationMessages)) {
|
|
108
|
+
return "conversationMessages";
|
|
109
|
+
}
|
|
110
|
+
if (Array.isArray(wire.input)) {
|
|
111
|
+
return "input";
|
|
112
|
+
}
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Remove the oldest complete units until the estimate reaches `targetTokens`.
|
|
117
|
+
*
|
|
118
|
+
* Returns the body unchanged when there is nothing safe to remove. The newest
|
|
119
|
+
* unit is always retained: it carries the turn being answered.
|
|
120
|
+
*/
|
|
121
|
+
export function truncateHistoryForBudget(args) {
|
|
122
|
+
const unchanged = {
|
|
123
|
+
body: args.body,
|
|
124
|
+
historyModified: false,
|
|
125
|
+
unitsRemoved: 0,
|
|
126
|
+
itemsRemoved: 0,
|
|
127
|
+
};
|
|
128
|
+
const wire = args.body;
|
|
129
|
+
const field = historyFieldOf(wire);
|
|
130
|
+
if (field === undefined) {
|
|
131
|
+
return unchanged;
|
|
132
|
+
}
|
|
133
|
+
const items = wire[field];
|
|
134
|
+
const units = groupIntoUnits(items);
|
|
135
|
+
if (units.length <= 1) {
|
|
136
|
+
return unchanged;
|
|
137
|
+
}
|
|
138
|
+
// Everything outside the history container — tools, instructions, schema —
|
|
139
|
+
// is fixed cost that truncation must never touch, so only unit costs are
|
|
140
|
+
// subtracted from the running projection.
|
|
141
|
+
const unitTokens = units.map((unit) => args.estimate(unit.map((index) => items[index])));
|
|
142
|
+
// The translated shape keeps the current turn in object-valued `input`, so
|
|
143
|
+
// every `conversationMessages` unit is prior history and all of them may go.
|
|
144
|
+
// The other shapes carry the current turn in the array itself, so the newest
|
|
145
|
+
// unit there is the turn being answered and must survive.
|
|
146
|
+
const removable = field === "conversationMessages" ? units.length : units.length - 1;
|
|
147
|
+
let removedUnits = 0;
|
|
148
|
+
let projected = args.inputTokensEstimate;
|
|
149
|
+
while (removedUnits < removable && projected > args.targetTokens) {
|
|
150
|
+
projected -= unitTokens[removedUnits];
|
|
151
|
+
removedUnits += 1;
|
|
152
|
+
}
|
|
153
|
+
if (removedUnits === 0) {
|
|
154
|
+
return unchanged;
|
|
155
|
+
}
|
|
156
|
+
// Advance by whole units so the kept history opens on a user turn without
|
|
157
|
+
// stranding a tool call from its answer.
|
|
158
|
+
while (removedUnits < removable &&
|
|
159
|
+
isNonUserTurn(items[units[removedUnits][0]])) {
|
|
160
|
+
removedUnits += 1;
|
|
161
|
+
}
|
|
162
|
+
const keptIndices = units.slice(removedUnits).flat();
|
|
163
|
+
const kept = keptIndices.map((index) => items[index]);
|
|
164
|
+
const itemsRemoved = items.length - kept.length;
|
|
165
|
+
return {
|
|
166
|
+
body: { ...args.body, [field]: kept },
|
|
167
|
+
historyModified: true,
|
|
168
|
+
unitsRemoved: removedUnits,
|
|
169
|
+
itemsRemoved,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native Claude-to-Vertex passthrough.
|
|
3
|
+
*
|
|
4
|
+
* Claude on Vertex speaks the Anthropic Messages API, so a fallback to it needs
|
|
5
|
+
* no translation at all — the client's body goes upstream essentially verbatim
|
|
6
|
+
* and the SSE comes back in the shape the client already expects.
|
|
7
|
+
*
|
|
8
|
+
* This exists because the SDK/translation path cannot carry an agentic turn:
|
|
9
|
+
* it runs its own tool loop, and a request whose tools have no executor (every
|
|
10
|
+
* proxied request, since the caller executes them) ends with the pending tool
|
|
11
|
+
* call discarded and a step-cap apology returned as assistant text. Tools,
|
|
12
|
+
* `tool_use`, `tool_result`, `cache_control` and thinking blocks all survive
|
|
13
|
+
* here because nothing reshapes them.
|
|
14
|
+
*/
|
|
15
|
+
import type { UsageContext, VertexPassthroughTerminal, VertexAnthropicPassthroughRequest, VertexAnthropicPassthroughTarget } from "../types/index.js";
|
|
16
|
+
/**
|
|
17
|
+
* Translate `output_config` for Vertex.
|
|
18
|
+
*
|
|
19
|
+
* An effort level Vertex knows passes through; one it does not is mapped onto
|
|
20
|
+
* the nearest level it does. A level with no mapping at all loses only the
|
|
21
|
+
* `effort` key, never the sibling settings beside it.
|
|
22
|
+
*/
|
|
23
|
+
export declare function mapVertexOutputConfig(value: unknown): Record<string, unknown> | undefined;
|
|
24
|
+
/** Strip what Vertex rejects and pin the API version it requires. */
|
|
25
|
+
export declare function buildVertexAnthropicPayload(body: Readonly<Record<string, unknown>>): Record<string, unknown>;
|
|
26
|
+
/**
|
|
27
|
+
* Publisher endpoint for a Claude model on Vertex.
|
|
28
|
+
*
|
|
29
|
+
* Streaming and non-streaming are different verbs on the same resource, so the
|
|
30
|
+
* caller's `stream` flag selects the verb rather than changing the body.
|
|
31
|
+
*/
|
|
32
|
+
export declare function buildVertexAnthropicUrl(target: VertexAnthropicPassthroughTarget): string;
|
|
33
|
+
/**
|
|
34
|
+
* Send one Anthropic-shaped request to Vertex and return the upstream response
|
|
35
|
+
* untouched, so the caller can stream its bytes straight to the client.
|
|
36
|
+
*/
|
|
37
|
+
export declare function dispatchVertexAnthropicPassthrough(request: VertexAnthropicPassthroughRequest): Promise<Response>;
|
|
38
|
+
/** Claude models are the only publisher models this passthrough addresses. */
|
|
39
|
+
export declare function isVertexAnthropicModel(model: string): boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Resolve where Vertex traffic is addressed, using the same environment the
|
|
42
|
+
* Vertex provider reads so a fallback lands on the project and region the rest
|
|
43
|
+
* of the process already uses.
|
|
44
|
+
*/
|
|
45
|
+
export declare function resolveVertexTarget(): {
|
|
46
|
+
projectId: string;
|
|
47
|
+
location: string;
|
|
48
|
+
} | undefined;
|
|
49
|
+
/**
|
|
50
|
+
* Run one Claude-on-Vertex fallback attempt and hand back a response the route
|
|
51
|
+
* can serve directly.
|
|
52
|
+
*
|
|
53
|
+
* A non-2xx upstream throws so the caller's attempt loop records the failure
|
|
54
|
+
* and moves to the next hop, exactly as the other providers behave.
|
|
55
|
+
*/
|
|
56
|
+
export declare function executeVertexAnthropicFallback(args: {
|
|
57
|
+
body: Readonly<Record<string, unknown>>;
|
|
58
|
+
model: string;
|
|
59
|
+
signal?: AbortSignal;
|
|
60
|
+
/** Fires exactly once on every exit, so the hop is finalized like any other. */
|
|
61
|
+
onTerminal?: (terminal: VertexPassthroughTerminal) => void;
|
|
62
|
+
}): Promise<Response>;
|
|
63
|
+
/**
|
|
64
|
+
* Read usage out of a non-streaming Anthropic reply.
|
|
65
|
+
*
|
|
66
|
+
* `rawPredict` returns a single `{"type":"message","usage":{...}}` object, which
|
|
67
|
+
* carries the whole turn's counts at once rather than splitting them across
|
|
68
|
+
* `message_start` and `message_delta`.
|
|
69
|
+
*/
|
|
70
|
+
export declare function readJsonUsage(text: string): UsageContext;
|
|
71
|
+
/**
|
|
72
|
+
* Forward a stream unchanged while observing its usage events.
|
|
73
|
+
*
|
|
74
|
+
* The bytes are passed through untouched — parsing happens on a copy of each
|
|
75
|
+
* chunk — so the client sees exactly what Vertex sent.
|
|
76
|
+
*
|
|
77
|
+
* A transform's `flush` only runs on a clean end, which would leave a stream
|
|
78
|
+
* error or a client disconnect with no terminal record at all. An explicit
|
|
79
|
+
* reader loop is used instead, so `onTerminal` fires exactly once on every
|
|
80
|
+
* exit: normal end, upstream failure, and cancellation.
|
|
81
|
+
*/
|
|
82
|
+
export declare function observeVertexUsage(body: ReadableStream<Uint8Array>, onTerminal: (terminal: VertexPassthroughTerminal) => void): ReadableStream<Uint8Array>;
|