@esankhan3/anvil-agent-core 0.3.0 → 0.4.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/README.md +1 -1
- package/dist/__tests__/gemini-adapter-recording.test.d.ts +11 -0
- package/dist/__tests__/gemini-adapter-recording.test.d.ts.map +1 -0
- package/dist/__tests__/gemini-adapter-recording.test.js +108 -0
- package/dist/__tests__/gemini-adapter-recording.test.js.map +1 -0
- package/dist/__tests__/legacy-adapter-language-model.test.d.ts +11 -0
- package/dist/__tests__/legacy-adapter-language-model.test.d.ts.map +1 -0
- package/dist/__tests__/legacy-adapter-language-model.test.js +102 -0
- package/dist/__tests__/legacy-adapter-language-model.test.js.map +1 -0
- package/dist/__tests__/ollama-adapter-recording.test.d.ts +15 -0
- package/dist/__tests__/ollama-adapter-recording.test.d.ts.map +1 -0
- package/dist/__tests__/ollama-adapter-recording.test.js +128 -0
- package/dist/__tests__/ollama-adapter-recording.test.js.map +1 -0
- package/dist/__tests__/openrouter-prefill.integration.test.d.ts +19 -0
- package/dist/__tests__/openrouter-prefill.integration.test.d.ts.map +1 -0
- package/dist/__tests__/openrouter-prefill.integration.test.js +410 -0
- package/dist/__tests__/openrouter-prefill.integration.test.js.map +1 -0
- package/dist/__tests__/prefill-truncate.test.d.ts +8 -0
- package/dist/__tests__/prefill-truncate.test.d.ts.map +1 -0
- package/dist/__tests__/prefill-truncate.test.js +75 -0
- package/dist/__tests__/prefill-truncate.test.js.map +1 -0
- package/dist/__tests__/router-circuit-breaker.test.js +1 -1
- package/dist/__tests__/router-circuit-breaker.test.js.map +1 -1
- package/dist/__tests__/router-classify-unified.test.d.ts +12 -0
- package/dist/__tests__/router-classify-unified.test.d.ts.map +1 -0
- package/dist/__tests__/router-classify-unified.test.js +108 -0
- package/dist/__tests__/router-classify-unified.test.js.map +1 -0
- package/dist/__tests__/router-fallback.test.js +1 -1
- package/dist/__tests__/router-fallback.test.js.map +1 -1
- package/dist/__tests__/router-model-registry.test.js +24 -0
- package/dist/__tests__/router-model-registry.test.js.map +1 -1
- package/dist/__tests__/router-retry.test.js +3 -5
- package/dist/__tests__/router-retry.test.js.map +1 -1
- package/dist/__tests__/router-runagent.test.d.ts +14 -0
- package/dist/__tests__/router-runagent.test.d.ts.map +1 -0
- package/dist/__tests__/router-runagent.test.js +95 -0
- package/dist/__tests__/router-runagent.test.js.map +1 -0
- package/dist/__tests__/router-scaffold.test.js +2 -1
- package/dist/__tests__/router-scaffold.test.js.map +1 -1
- package/dist/__tests__/router-spend-ledger.test.js +1 -1
- package/dist/__tests__/router-spend-ledger.test.js.map +1 -1
- package/dist/__tests__/router-stream-fallback.test.d.ts +13 -0
- package/dist/__tests__/router-stream-fallback.test.d.ts.map +1 -0
- package/dist/__tests__/router-stream-fallback.test.js +129 -0
- package/dist/__tests__/router-stream-fallback.test.js.map +1 -0
- package/dist/__tests__/router-telemetry.test.js +3 -1
- package/dist/__tests__/router-telemetry.test.js.map +1 -1
- package/dist/__tests__/util/mock-upstream.d.ts +77 -0
- package/dist/__tests__/util/mock-upstream.d.ts.map +1 -0
- package/dist/__tests__/util/mock-upstream.js +219 -0
- package/dist/__tests__/util/mock-upstream.js.map +1 -0
- package/dist/adk-adapter.d.ts.map +1 -1
- package/dist/adk-adapter.js +51 -0
- package/dist/adk-adapter.js.map +1 -1
- package/dist/agent/session/adapter.d.ts +15 -0
- package/dist/agent/session/adapter.d.ts.map +1 -1
- package/dist/agent/session/adapter.js +3 -0
- package/dist/agent/session/adapter.js.map +1 -1
- package/dist/agent/session/index.d.ts +1 -0
- package/dist/agent/session/index.d.ts.map +1 -1
- package/dist/agent/session/index.js +1 -0
- package/dist/agent/session/index.js.map +1 -1
- package/dist/agent/session/language-model-bridge.d.ts.map +1 -1
- package/dist/agent/session/language-model-bridge.js +22 -0
- package/dist/agent/session/language-model-bridge.js.map +1 -1
- package/dist/agent/session/legacy-adapter-language-model.d.ts +27 -0
- package/dist/agent/session/legacy-adapter-language-model.d.ts.map +1 -0
- package/dist/agent/session/legacy-adapter-language-model.js +221 -0
- package/dist/agent/session/legacy-adapter-language-model.js.map +1 -0
- package/dist/agent/session/types.d.ts +20 -0
- package/dist/agent/session/types.d.ts.map +1 -1
- package/dist/agent/session/types.js.map +1 -1
- package/dist/claude-adapter.d.ts.map +1 -1
- package/dist/claude-adapter.js +27 -0
- package/dist/claude-adapter.js.map +1 -1
- package/dist/gemini-adapter.d.ts.map +1 -1
- package/dist/gemini-adapter.js +46 -0
- package/dist/gemini-adapter.js.map +1 -1
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/ollama-adapter.d.ts.map +1 -1
- package/dist/ollama-adapter.js +118 -8
- package/dist/ollama-adapter.js.map +1 -1
- package/dist/openrouter-adapter.d.ts.map +1 -1
- package/dist/openrouter-adapter.js +289 -15
- package/dist/openrouter-adapter.js.map +1 -1
- package/dist/prefill/strip.d.ts +49 -0
- package/dist/prefill/strip.d.ts.map +1 -0
- package/dist/prefill/strip.js +93 -0
- package/dist/prefill/strip.js.map +1 -0
- package/dist/prefill/translate.d.ts +55 -0
- package/dist/prefill/translate.d.ts.map +1 -0
- package/dist/prefill/translate.js +162 -0
- package/dist/prefill/translate.js.map +1 -0
- package/dist/prefill/truncate.d.ts +62 -0
- package/dist/prefill/truncate.d.ts.map +1 -0
- package/dist/prefill/truncate.js +77 -0
- package/dist/prefill/truncate.js.map +1 -0
- package/dist/router/errors.d.ts +24 -7
- package/dist/router/errors.d.ts.map +1 -1
- package/dist/router/errors.js +71 -14
- package/dist/router/errors.js.map +1 -1
- package/dist/router/index.d.ts +3 -2
- package/dist/router/index.d.ts.map +1 -1
- package/dist/router/index.js +2 -1
- package/dist/router/index.js.map +1 -1
- package/dist/router/model-registry.d.ts +22 -0
- package/dist/router/model-registry.d.ts.map +1 -1
- package/dist/router/model-registry.js +64 -1
- package/dist/router/model-registry.js.map +1 -1
- package/dist/router/provider-registry-resolver.d.ts +25 -0
- package/dist/router/provider-registry-resolver.d.ts.map +1 -0
- package/dist/router/provider-registry-resolver.js +84 -0
- package/dist/router/provider-registry-resolver.js.map +1 -0
- package/dist/router/retry.d.ts +3 -0
- package/dist/router/retry.d.ts.map +1 -1
- package/dist/router/retry.js +4 -1
- package/dist/router/retry.js.map +1 -1
- package/dist/router/router.d.ts +79 -1
- package/dist/router/router.d.ts.map +1 -1
- package/dist/router/router.js +256 -2
- package/dist/router/router.js.map +1 -1
- package/dist/router/types.d.ts +1 -1
- package/dist/router/types.d.ts.map +1 -1
- package/dist/router/types.js +1 -0
- package/dist/router/types.js.map +1 -1
- package/dist/turn-recorder/__tests__/turn-recorder.test.d.ts +20 -0
- package/dist/turn-recorder/__tests__/turn-recorder.test.d.ts.map +1 -0
- package/dist/turn-recorder/__tests__/turn-recorder.test.js +282 -0
- package/dist/turn-recorder/__tests__/turn-recorder.test.js.map +1 -0
- package/dist/turn-recorder/hash.d.ts +10 -0
- package/dist/turn-recorder/hash.d.ts.map +1 -0
- package/dist/turn-recorder/hash.js +35 -0
- package/dist/turn-recorder/hash.js.map +1 -0
- package/dist/turn-recorder/index.d.ts +119 -0
- package/dist/turn-recorder/index.d.ts.map +1 -0
- package/dist/turn-recorder/index.js +326 -0
- package/dist/turn-recorder/index.js.map +1 -0
- package/dist/turn-recorder/types.d.ts +227 -0
- package/dist/turn-recorder/types.d.ts.map +1 -0
- package/dist/turn-recorder/types.js +20 -0
- package/dist/turn-recorder/types.js.map +1 -0
- package/dist/types.d.ts +35 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-provider strip policy (v2 ADR §2.3.1).
|
|
3
|
+
*
|
|
4
|
+
* Vendor-specific message blocks DO NOT survive a chain-fallback swap
|
|
5
|
+
* across providers. `stripForTarget` removes fields the target provider
|
|
6
|
+
* would reject (or silently misinterpret) when the conversation
|
|
7
|
+
* originated on a different vendor.
|
|
8
|
+
*
|
|
9
|
+
* The matrix (§2.3.1):
|
|
10
|
+
* - reasoning_details[] / reasoning — OpenRouter reasoning models;
|
|
11
|
+
* strip when source !== target (Anthropic + OpenAI reject them).
|
|
12
|
+
* - cache_control — Anthropic; strip when target is non-Anthropic.
|
|
13
|
+
* - prompt_cache_key — OpenAI; strip when target is non-OpenAI.
|
|
14
|
+
*
|
|
15
|
+
* Same-vendor swaps short-circuit: nothing is stripped (hot path), and
|
|
16
|
+
* they additionally qualify for the §5.1 prefix-cache fast-path.
|
|
17
|
+
*
|
|
18
|
+
* The function is shape-agnostic: it walks each message object and
|
|
19
|
+
* deletes the known vendor keys (including inside content-block arrays
|
|
20
|
+
* for Anthropic's `cache_control`). It never mutates the input — every
|
|
21
|
+
* message is shallow-cloned before a key is removed.
|
|
22
|
+
*/
|
|
23
|
+
/** Anthropic family: native `messages` API consumers.
|
|
24
|
+
*
|
|
25
|
+
* NOTE on 'adk': ADK is provider-agnostic at runtime (claude-via-adk =
|
|
26
|
+
* Anthropic wire; gemini-via-adk = Google wire). Per ADR §2.8, ADK
|
|
27
|
+
* prefill is materialized via `Session.appendEvent`, NOT through
|
|
28
|
+
* `materializePrefill`/`stripForTarget` — so this predicate is NOT
|
|
29
|
+
* exercised for ADK targets in the H2 path. When ADK is ported (H4),
|
|
30
|
+
* its wire format must be detected from the underlying model id, not
|
|
31
|
+
* assumed Anthropic here. Kept in the Anthropic bucket only so the
|
|
32
|
+
* §5.1 same-vendor cache fast-path can recognise `claude`↔`claude`;
|
|
33
|
+
* revisit when ADK joins the prefill path. */
|
|
34
|
+
function isAnthropic(p) {
|
|
35
|
+
return p === 'adk' || p === 'claude';
|
|
36
|
+
}
|
|
37
|
+
/** OpenAI-compatible chat-completions consumers. */
|
|
38
|
+
function isOpenAICompat(p) {
|
|
39
|
+
return p === 'openai' || p === 'openrouter' || p === 'opencode' || p === 'ollama';
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Return a new message array safe to send to `targetProvider` given it
|
|
43
|
+
* originated on `sourceProvider`. Same-vendor → returned as a shallow
|
|
44
|
+
* copy untouched.
|
|
45
|
+
*/
|
|
46
|
+
export function stripForTarget(messages, ctx) {
|
|
47
|
+
if (ctx.sourceProvider === ctx.targetProvider) {
|
|
48
|
+
return messages.slice();
|
|
49
|
+
}
|
|
50
|
+
const dropReasoning = !isOpenRouterFamily(ctx.targetProvider);
|
|
51
|
+
const dropCacheControl = !isAnthropic(ctx.targetProvider);
|
|
52
|
+
const dropPromptCacheKey = !isOpenAIExact(ctx.targetProvider);
|
|
53
|
+
return messages.map((raw) => {
|
|
54
|
+
if (!raw || typeof raw !== 'object')
|
|
55
|
+
return raw;
|
|
56
|
+
const msg = { ...raw };
|
|
57
|
+
if (dropReasoning) {
|
|
58
|
+
delete msg.reasoning;
|
|
59
|
+
delete msg.reasoning_details;
|
|
60
|
+
}
|
|
61
|
+
if (dropPromptCacheKey) {
|
|
62
|
+
delete msg.prompt_cache_key;
|
|
63
|
+
}
|
|
64
|
+
if (dropCacheControl) {
|
|
65
|
+
// top-level marker
|
|
66
|
+
delete msg.cache_control;
|
|
67
|
+
// Anthropic puts cache_control inside content-block arrays.
|
|
68
|
+
if (Array.isArray(msg.content)) {
|
|
69
|
+
msg.content = msg.content.map((block) => {
|
|
70
|
+
if (block && typeof block === 'object' && 'cache_control' in block) {
|
|
71
|
+
const clone = { ...block };
|
|
72
|
+
delete clone.cache_control;
|
|
73
|
+
return clone;
|
|
74
|
+
}
|
|
75
|
+
return block;
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return msg;
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
/** OpenRouter (and OpenCode, which proxies OpenRouter-style models) are
|
|
83
|
+
* the only consumers that accept echoed reasoning_details. */
|
|
84
|
+
function isOpenRouterFamily(p) {
|
|
85
|
+
return p === 'openrouter' || p === 'opencode';
|
|
86
|
+
}
|
|
87
|
+
/** Exact OpenAI — the only consumer that honors `prompt_cache_key`. */
|
|
88
|
+
function isOpenAIExact(p) {
|
|
89
|
+
return p === 'openai';
|
|
90
|
+
}
|
|
91
|
+
// Re-export the family predicates for the translate layer + tests.
|
|
92
|
+
export { isAnthropic, isOpenAICompat };
|
|
93
|
+
//# sourceMappingURL=strip.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"strip.js","sourceRoot":"","sources":["../../src/prefill/strip.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AASH;;;;;;;;;;8CAU8C;AAC9C,SAAS,WAAW,CAAC,CAAe;IAClC,OAAO,CAAC,KAAK,KAAK,IAAI,CAAC,KAAK,QAAQ,CAAC;AACvC,CAAC;AAED,oDAAoD;AACpD,SAAS,cAAc,CAAC,CAAe;IACrC,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,KAAK,YAAY,IAAI,CAAC,KAAK,UAAU,IAAI,CAAC,KAAK,QAAQ,CAAC;AACpF,CAAC;AAID;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,QAA4B,EAAE,GAAiB;IAC5E,IAAI,GAAG,CAAC,cAAc,KAAK,GAAG,CAAC,cAAc,EAAE,CAAC;QAC9C,OAAO,QAAQ,CAAC,KAAK,EAAE,CAAC;IAC1B,CAAC;IAED,MAAM,aAAa,GAAG,CAAC,kBAAkB,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAC9D,MAAM,gBAAgB,GAAG,CAAC,WAAW,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAC1D,MAAM,kBAAkB,GAAG,CAAC,aAAa,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAE9D,OAAO,QAAQ,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE;QAC1B,IAAI,CAAC,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ;YAAE,OAAO,GAAG,CAAC;QAChD,MAAM,GAAG,GAAG,EAAE,GAAI,GAAoB,EAAE,CAAC;QAEzC,IAAI,aAAa,EAAE,CAAC;YAClB,OAAO,GAAG,CAAC,SAAS,CAAC;YACrB,OAAO,GAAG,CAAC,iBAAiB,CAAC;QAC/B,CAAC;QACD,IAAI,kBAAkB,EAAE,CAAC;YACvB,OAAO,GAAG,CAAC,gBAAgB,CAAC;QAC9B,CAAC;QACD,IAAI,gBAAgB,EAAE,CAAC;YACrB,mBAAmB;YACnB,OAAO,GAAG,CAAC,aAAa,CAAC;YACzB,4DAA4D;YAC5D,IAAI,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC;gBAC/B,GAAG,CAAC,OAAO,GAAI,GAAG,CAAC,OAAqB,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE;oBACrD,IAAI,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,eAAe,IAAK,KAAgB,EAAE,CAAC;wBAC/E,MAAM,KAAK,GAAG,EAAE,GAAI,KAAiC,EAAE,CAAC;wBACxD,OAAO,KAAK,CAAC,aAAa,CAAC;wBAC3B,OAAO,KAAK,CAAC;oBACf,CAAC;oBACD,OAAO,KAAK,CAAC;gBACf,CAAC,CAAC,CAAC;YACL,CAAC;QACH,CAAC;QACD,OAAO,GAAG,CAAC;IACb,CAAC,CAAC,CAAC;AACL,CAAC;AAED;+DAC+D;AAC/D,SAAS,kBAAkB,CAAC,CAAe;IACzC,OAAO,CAAC,KAAK,YAAY,IAAI,CAAC,KAAK,UAAU,CAAC;AAChD,CAAC;AAED,uEAAuE;AACvE,SAAS,aAAa,CAAC,CAAe;IACpC,OAAO,CAAC,KAAK,QAAQ,CAAC;AACxB,CAAC;AAED,mEAAmE;AACnE,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,CAAC"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool-result + prefill shape translation (v2 ADR §2.3.2 / §2.3).
|
|
3
|
+
*
|
|
4
|
+
* The durable log stores tool results in a neutral shape
|
|
5
|
+
* (`NeutralToolResult`). When a chain-fallback swap re-injects a prior
|
|
6
|
+
* model's turn into a NEW provider, the adapter materializes that turn
|
|
7
|
+
* into the target provider's wire format. This module owns that
|
|
8
|
+
* translation.
|
|
9
|
+
*
|
|
10
|
+
* - Anthropic native:
|
|
11
|
+
* tool_use → { role: 'assistant', content: [{ type: 'tool_use', id, name, input }] }
|
|
12
|
+
* result → { role: 'user', content: [{ type: 'tool_result', tool_use_id, content, is_error }] }
|
|
13
|
+
* - OpenAI-compat (openrouter, opencode, openai, ollama):
|
|
14
|
+
* tool_use → { role: 'assistant', tool_calls: [{ id, type:'function', function:{ name, arguments } }] }
|
|
15
|
+
* result → { role: 'tool', tool_call_id, content: stringify }
|
|
16
|
+
* - ADK: materialized via Session.appendEvent inside the adk adapter
|
|
17
|
+
* (functionCall + functionResponse events) — NOT via this module.
|
|
18
|
+
*
|
|
19
|
+
* `materializePrefill` turns a `Prefill` into the ordered list of wire
|
|
20
|
+
* messages to splice into the request BEFORE the continuation: for each
|
|
21
|
+
* recorded tool use, an assistant(tool_call) + tool(result) pair, then
|
|
22
|
+
* a trailing assistant message carrying the partial text the prior
|
|
23
|
+
* model streamed. The target model continues from that trailing
|
|
24
|
+
* assistant message (verified empirically — Anthropic + OpenAI-compat
|
|
25
|
+
* both honor a trailing assistant message as a continuation prefix).
|
|
26
|
+
*/
|
|
27
|
+
import type { ProviderName } from '../types.js';
|
|
28
|
+
import type { NeutralToolResult, Prefill, PrefillTurn } from '../turn-recorder/types.js';
|
|
29
|
+
export interface TranslatedToolResult {
|
|
30
|
+
/** Target-specific wire object the adapter appends to messages[]. */
|
|
31
|
+
message: unknown;
|
|
32
|
+
}
|
|
33
|
+
/** Convert a `NeutralToolResult` to the target provider's result message. */
|
|
34
|
+
export declare function translateToolResult(result: NeutralToolResult, target: ProviderName): TranslatedToolResult;
|
|
35
|
+
/**
|
|
36
|
+
* Build the ordered wire messages that re-present a `Prefill` to the
|
|
37
|
+
* target provider. The caller splices these onto the end of its
|
|
38
|
+
* `messages[]` (after system + user), then lets the model continue.
|
|
39
|
+
*
|
|
40
|
+
* Ordering: each recorded tool use becomes an assistant(tool_call) +
|
|
41
|
+
* result pair, in recorded order; finally a trailing assistant message
|
|
42
|
+
* carrying the partial text (only when non-empty — an empty trailing
|
|
43
|
+
* assistant message is rejected by some providers).
|
|
44
|
+
*/
|
|
45
|
+
export declare function materializePrefill(prefill: Prefill, target: ProviderName): unknown[];
|
|
46
|
+
/**
|
|
47
|
+
* §Tier 2 — materialize a list of COMPLETED prior turns into the target
|
|
48
|
+
* provider's wire format, spliced BEFORE the new user message so a stateful
|
|
49
|
+
* (non-claude) session resume re-presents the full conversation. Each turn
|
|
50
|
+
* emits, in order: its opening user message (when `userPrompt` is set — the
|
|
51
|
+
* first turn of a phase), then one combined assistant(text+tool_calls), then
|
|
52
|
+
* its tool results. Empty list → [] (START phase / no prior turns).
|
|
53
|
+
*/
|
|
54
|
+
export declare function materializePriorTurns(turns: PrefillTurn[], target: ProviderName): unknown[];
|
|
55
|
+
//# sourceMappingURL=translate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"translate.d.ts","sourceRoot":"","sources":["../../src/prefill/translate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,KAAK,EAAE,iBAAiB,EAAE,OAAO,EAAE,WAAW,EAAkB,MAAM,2BAA2B,CAAC;AAGzG,MAAM,WAAW,oBAAoB;IACnC,qEAAqE;IACrE,OAAO,EAAE,OAAO,CAAC;CAClB;AAMD,6EAA6E;AAC7E,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,iBAAiB,EACzB,MAAM,EAAE,YAAY,GACnB,oBAAoB,CA6BtB;AA6BD;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,YAAY,GAAG,OAAO,EAAE,CAUpF;AAsCD;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,WAAW,EAAE,EAAE,MAAM,EAAE,YAAY,GAAG,OAAO,EAAE,CAe3F"}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool-result + prefill shape translation (v2 ADR §2.3.2 / §2.3).
|
|
3
|
+
*
|
|
4
|
+
* The durable log stores tool results in a neutral shape
|
|
5
|
+
* (`NeutralToolResult`). When a chain-fallback swap re-injects a prior
|
|
6
|
+
* model's turn into a NEW provider, the adapter materializes that turn
|
|
7
|
+
* into the target provider's wire format. This module owns that
|
|
8
|
+
* translation.
|
|
9
|
+
*
|
|
10
|
+
* - Anthropic native:
|
|
11
|
+
* tool_use → { role: 'assistant', content: [{ type: 'tool_use', id, name, input }] }
|
|
12
|
+
* result → { role: 'user', content: [{ type: 'tool_result', tool_use_id, content, is_error }] }
|
|
13
|
+
* - OpenAI-compat (openrouter, opencode, openai, ollama):
|
|
14
|
+
* tool_use → { role: 'assistant', tool_calls: [{ id, type:'function', function:{ name, arguments } }] }
|
|
15
|
+
* result → { role: 'tool', tool_call_id, content: stringify }
|
|
16
|
+
* - ADK: materialized via Session.appendEvent inside the adk adapter
|
|
17
|
+
* (functionCall + functionResponse events) — NOT via this module.
|
|
18
|
+
*
|
|
19
|
+
* `materializePrefill` turns a `Prefill` into the ordered list of wire
|
|
20
|
+
* messages to splice into the request BEFORE the continuation: for each
|
|
21
|
+
* recorded tool use, an assistant(tool_call) + tool(result) pair, then
|
|
22
|
+
* a trailing assistant message carrying the partial text the prior
|
|
23
|
+
* model streamed. The target model continues from that trailing
|
|
24
|
+
* assistant message (verified empirically — Anthropic + OpenAI-compat
|
|
25
|
+
* both honor a trailing assistant message as a continuation prefix).
|
|
26
|
+
*/
|
|
27
|
+
import { isAnthropic } from './strip.js';
|
|
28
|
+
function stringifyContent(content) {
|
|
29
|
+
return typeof content === 'string' ? content : JSON.stringify(content ?? null);
|
|
30
|
+
}
|
|
31
|
+
/** Convert a `NeutralToolResult` to the target provider's result message. */
|
|
32
|
+
export function translateToolResult(result, target) {
|
|
33
|
+
// Defensive guard (review finding): an empty tool_use_id / tool_call_id
|
|
34
|
+
// makes the upstream reject with "tool_use_id does not reference a known
|
|
35
|
+
// tool_use". The ADK path can produce `id ?? ''`; fail loud here rather
|
|
36
|
+
// than ship a malformed request.
|
|
37
|
+
if (!result.toolUseId) {
|
|
38
|
+
throw new Error('translateToolResult: NeutralToolResult.toolUseId is empty — cannot bind tool result to a tool_use');
|
|
39
|
+
}
|
|
40
|
+
if (isAnthropic(target)) {
|
|
41
|
+
return {
|
|
42
|
+
message: {
|
|
43
|
+
role: 'user',
|
|
44
|
+
content: [{
|
|
45
|
+
type: 'tool_result',
|
|
46
|
+
tool_use_id: result.toolUseId,
|
|
47
|
+
content: stringifyContent(result.content),
|
|
48
|
+
is_error: !result.ok,
|
|
49
|
+
}],
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
// OpenAI-compatible default.
|
|
54
|
+
return {
|
|
55
|
+
message: {
|
|
56
|
+
role: 'tool',
|
|
57
|
+
tool_call_id: result.toolUseId,
|
|
58
|
+
content: stringifyContent(result.content),
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/** The assistant message that re-presents a recorded tool_use to the
|
|
63
|
+
* target so the following tool-result message is well-formed. */
|
|
64
|
+
function translateToolUseAssistant(use, target) {
|
|
65
|
+
if (isAnthropic(target)) {
|
|
66
|
+
return {
|
|
67
|
+
role: 'assistant',
|
|
68
|
+
content: [{ type: 'tool_use', id: use.id, name: use.name, input: use.input ?? {} }],
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
// Empty string, not null: an assistant message that only carries a
|
|
73
|
+
// tool_call has no text preamble. While most OpenAI-compat upstreams
|
|
74
|
+
// accept `content: null` here, `''` is universally valid and removes
|
|
75
|
+
// any ambiguity across the openai/openrouter/opencode/ollama set.
|
|
76
|
+
role: 'assistant',
|
|
77
|
+
content: '',
|
|
78
|
+
tool_calls: [{
|
|
79
|
+
id: use.id,
|
|
80
|
+
type: 'function',
|
|
81
|
+
function: { name: use.name, arguments: JSON.stringify(use.input ?? {}) },
|
|
82
|
+
}],
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Build the ordered wire messages that re-present a `Prefill` to the
|
|
87
|
+
* target provider. The caller splices these onto the end of its
|
|
88
|
+
* `messages[]` (after system + user), then lets the model continue.
|
|
89
|
+
*
|
|
90
|
+
* Ordering: each recorded tool use becomes an assistant(tool_call) +
|
|
91
|
+
* result pair, in recorded order; finally a trailing assistant message
|
|
92
|
+
* carrying the partial text (only when non-empty — an empty trailing
|
|
93
|
+
* assistant message is rejected by some providers).
|
|
94
|
+
*/
|
|
95
|
+
export function materializePrefill(prefill, target) {
|
|
96
|
+
const out = [];
|
|
97
|
+
for (const use of prefill.toolUses) {
|
|
98
|
+
out.push(translateToolUseAssistant({ id: use.id, name: use.name, input: use.input }, target));
|
|
99
|
+
out.push(translateToolResult(use.result, target).message);
|
|
100
|
+
}
|
|
101
|
+
if (prefill.text.length > 0) {
|
|
102
|
+
out.push({ role: 'assistant', content: prefill.text });
|
|
103
|
+
}
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* One COMPLETED prior turn as a single assistant message (text + tool_calls
|
|
108
|
+
* combined) followed by its tool-result messages. UNLIKE `materializePrefill`
|
|
109
|
+
* — which keeps the partial text as a separate TRAILING assistant message so
|
|
110
|
+
* the live model continues from it — a completed turn's text and tool calls
|
|
111
|
+
* belong to one wire message. Emitting them split would put a phase's
|
|
112
|
+
* tool-loop turns back-to-back as consecutive assistant messages (no
|
|
113
|
+
* intervening tool/user), which OpenAI-compat upstreams reject or silently
|
|
114
|
+
* merge. The combined shape reproduces the exact tool-loop wire sequence:
|
|
115
|
+
* assistant(text,tool_calls) → tool(result)* → (next turn).
|
|
116
|
+
*/
|
|
117
|
+
function combinedAssistantMessage(text, toolUses, target) {
|
|
118
|
+
if (isAnthropic(target)) {
|
|
119
|
+
const content = [];
|
|
120
|
+
if (text.length > 0)
|
|
121
|
+
content.push({ type: 'text', text });
|
|
122
|
+
for (const u of toolUses) {
|
|
123
|
+
content.push({ type: 'tool_use', id: u.id, name: u.name, input: u.input ?? {} });
|
|
124
|
+
}
|
|
125
|
+
return { role: 'assistant', content };
|
|
126
|
+
}
|
|
127
|
+
// OpenAI-compat: text + tool_calls live on ONE assistant message.
|
|
128
|
+
const msg = { role: 'assistant', content: text };
|
|
129
|
+
if (toolUses.length > 0) {
|
|
130
|
+
msg.tool_calls = toolUses.map((u) => ({
|
|
131
|
+
id: u.id,
|
|
132
|
+
type: 'function',
|
|
133
|
+
function: { name: u.name, arguments: JSON.stringify(u.input ?? {}) },
|
|
134
|
+
}));
|
|
135
|
+
}
|
|
136
|
+
return msg;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* §Tier 2 — materialize a list of COMPLETED prior turns into the target
|
|
140
|
+
* provider's wire format, spliced BEFORE the new user message so a stateful
|
|
141
|
+
* (non-claude) session resume re-presents the full conversation. Each turn
|
|
142
|
+
* emits, in order: its opening user message (when `userPrompt` is set — the
|
|
143
|
+
* first turn of a phase), then one combined assistant(text+tool_calls), then
|
|
144
|
+
* its tool results. Empty list → [] (START phase / no prior turns).
|
|
145
|
+
*/
|
|
146
|
+
export function materializePriorTurns(turns, target) {
|
|
147
|
+
const out = [];
|
|
148
|
+
for (const turn of turns) {
|
|
149
|
+
if (typeof turn.userPrompt === 'string' && turn.userPrompt.length > 0) {
|
|
150
|
+
out.push({ role: 'user', content: turn.userPrompt });
|
|
151
|
+
}
|
|
152
|
+
// A completed turn always carries text and/or tools; guard the empty case.
|
|
153
|
+
if (turn.text.length > 0 || turn.toolUses.length > 0) {
|
|
154
|
+
out.push(combinedAssistantMessage(turn.text, turn.toolUses, target));
|
|
155
|
+
}
|
|
156
|
+
for (const use of turn.toolUses) {
|
|
157
|
+
out.push(translateToolResult(use.result, target).message);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return out;
|
|
161
|
+
}
|
|
162
|
+
//# sourceMappingURL=translate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"translate.js","sourceRoot":"","sources":["../../src/prefill/translate.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* §2.3.3 context-window truncation policy.
|
|
3
|
+
*
|
|
4
|
+
* `prefill.text` (the partial assistant continuation) plus the recorded
|
|
5
|
+
* tool history may push the resuming request over the TARGET model's
|
|
6
|
+
* input window even though it fit on the source model. Before the chain
|
|
7
|
+
* walker hands a prefill to the next attempt, it runs this gate:
|
|
8
|
+
*
|
|
9
|
+
* budget = target.maxInputTokens − margin(8K) − baseTokens
|
|
10
|
+
* − sourceTokens − sum(toolUseTokens)
|
|
11
|
+
*
|
|
12
|
+
* 1. If the partial text alone won't fit (budget-before-tools < 0),
|
|
13
|
+
* the prefill CANNOT be served → return undefined. The walker then
|
|
14
|
+
* retries the next model WITHOUT a prefill (a clean re-run beats a
|
|
15
|
+
* failed run, per §2.3.3 step 3).
|
|
16
|
+
* 2. Otherwise drop the OLDEST tool_use/result pairs (from the start)
|
|
17
|
+
* until the tool history fits, preserving the most-recent tools and
|
|
18
|
+
* the trailing partial text. Those dropped tools already executed on
|
|
19
|
+
* the source model; the resuming model simply won't see them in its
|
|
20
|
+
* re-presented history.
|
|
21
|
+
*
|
|
22
|
+
* `maxInputTokens` comes from `cost.ts`'s model-prices table; a model
|
|
23
|
+
* missing from the table conservatively assumes 32K (§2.3.3 step 4).
|
|
24
|
+
*
|
|
25
|
+
* Token counts are estimates (default ~chars/4) — exact tokenization is
|
|
26
|
+
* deferred (§ ADR open question). The 8K margin absorbs the slack; if the
|
|
27
|
+
* estimate is too aggressive the walker just drops an extra tool pair,
|
|
28
|
+
* which is a slower chain walk, not a correctness loss.
|
|
29
|
+
*/
|
|
30
|
+
import type { Prefill } from '../turn-recorder/types.js';
|
|
31
|
+
/** Conservative floor when the model is absent from the price table. */
|
|
32
|
+
export declare const DEFAULT_MAX_INPUT_TOKENS = 32000;
|
|
33
|
+
/** Headroom reserved below the model's max input window. */
|
|
34
|
+
export declare const DEFAULT_MARGIN_TOKENS = 8000;
|
|
35
|
+
export interface TruncatePrefillArgs {
|
|
36
|
+
prefill: Prefill;
|
|
37
|
+
/** Model id the prefill is about to be served to. */
|
|
38
|
+
targetModel: string;
|
|
39
|
+
/**
|
|
40
|
+
* Tokens already committed to the request before the prefill is
|
|
41
|
+
* spliced (system prompt + the user turn). Best-effort; default 0.
|
|
42
|
+
* Counted against the budget so a large system prompt + big prefill
|
|
43
|
+
* don't silently blow the window.
|
|
44
|
+
*/
|
|
45
|
+
baseTokens?: number;
|
|
46
|
+
/** Headroom under the model's max input window. Default 8000. */
|
|
47
|
+
marginTokens?: number;
|
|
48
|
+
/** Estimator over an arbitrary string. Default ≈ chars/4. */
|
|
49
|
+
estimateTokens?: (text: string) => number;
|
|
50
|
+
/**
|
|
51
|
+
* Override the target's max-input-token lookup (tests / models the
|
|
52
|
+
* cost table can't price). Default reads `cost.ts`.
|
|
53
|
+
*/
|
|
54
|
+
maxInputTokensFor?: (model: string) => number | undefined;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Returns the (possibly tool-trimmed) prefill that fits the target
|
|
58
|
+
* model's input budget, or `undefined` when even the bare partial text
|
|
59
|
+
* won't fit — signalling the walker to retry without a prefill.
|
|
60
|
+
*/
|
|
61
|
+
export declare function truncatePrefillForBudget(args: TruncatePrefillArgs): Prefill | undefined;
|
|
62
|
+
//# sourceMappingURL=truncate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"truncate.d.ts","sourceRoot":"","sources":["../../src/prefill/truncate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAGH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,2BAA2B,CAAC;AAEzD,wEAAwE;AACxE,eAAO,MAAM,wBAAwB,QAAS,CAAC;AAC/C,4DAA4D;AAC5D,eAAO,MAAM,qBAAqB,OAAQ,CAAC;AAE3C,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,OAAO,CAAC;IACjB,qDAAqD;IACrD,WAAW,EAAE,MAAM,CAAC;IACpB;;;;;OAKG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iEAAiE;IACjE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,6DAA6D;IAC7D,cAAc,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IAC1C;;;OAGG;IACH,iBAAiB,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAC;CAC3D;AAeD;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,mBAAmB,GAAG,OAAO,GAAG,SAAS,CAiCvF"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* §2.3.3 context-window truncation policy.
|
|
3
|
+
*
|
|
4
|
+
* `prefill.text` (the partial assistant continuation) plus the recorded
|
|
5
|
+
* tool history may push the resuming request over the TARGET model's
|
|
6
|
+
* input window even though it fit on the source model. Before the chain
|
|
7
|
+
* walker hands a prefill to the next attempt, it runs this gate:
|
|
8
|
+
*
|
|
9
|
+
* budget = target.maxInputTokens − margin(8K) − baseTokens
|
|
10
|
+
* − sourceTokens − sum(toolUseTokens)
|
|
11
|
+
*
|
|
12
|
+
* 1. If the partial text alone won't fit (budget-before-tools < 0),
|
|
13
|
+
* the prefill CANNOT be served → return undefined. The walker then
|
|
14
|
+
* retries the next model WITHOUT a prefill (a clean re-run beats a
|
|
15
|
+
* failed run, per §2.3.3 step 3).
|
|
16
|
+
* 2. Otherwise drop the OLDEST tool_use/result pairs (from the start)
|
|
17
|
+
* until the tool history fits, preserving the most-recent tools and
|
|
18
|
+
* the trailing partial text. Those dropped tools already executed on
|
|
19
|
+
* the source model; the resuming model simply won't see them in its
|
|
20
|
+
* re-presented history.
|
|
21
|
+
*
|
|
22
|
+
* `maxInputTokens` comes from `cost.ts`'s model-prices table; a model
|
|
23
|
+
* missing from the table conservatively assumes 32K (§2.3.3 step 4).
|
|
24
|
+
*
|
|
25
|
+
* Token counts are estimates (default ~chars/4) — exact tokenization is
|
|
26
|
+
* deferred (§ ADR open question). The 8K margin absorbs the slack; if the
|
|
27
|
+
* estimate is too aggressive the walker just drops an extra tool pair,
|
|
28
|
+
* which is a slower chain walk, not a correctness loss.
|
|
29
|
+
*/
|
|
30
|
+
import { getDetailedPricing } from '../cost.js';
|
|
31
|
+
/** Conservative floor when the model is absent from the price table. */
|
|
32
|
+
export const DEFAULT_MAX_INPUT_TOKENS = 32_000;
|
|
33
|
+
/** Headroom reserved below the model's max input window. */
|
|
34
|
+
export const DEFAULT_MARGIN_TOKENS = 8_000;
|
|
35
|
+
const defaultEstimate = (text) => Math.ceil(text.length / 4);
|
|
36
|
+
/** Rough token cost of re-presenting one recorded tool_use/result pair. */
|
|
37
|
+
function toolUseTokens(tu, estimate) {
|
|
38
|
+
const content = typeof tu.result?.content === 'string'
|
|
39
|
+
? tu.result.content
|
|
40
|
+
: JSON.stringify(tu.result?.content ?? '');
|
|
41
|
+
return estimate(`${tu.name}${JSON.stringify(tu.input ?? {})}${content}`);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Returns the (possibly tool-trimmed) prefill that fits the target
|
|
45
|
+
* model's input budget, or `undefined` when even the bare partial text
|
|
46
|
+
* won't fit — signalling the walker to retry without a prefill.
|
|
47
|
+
*/
|
|
48
|
+
export function truncatePrefillForBudget(args) {
|
|
49
|
+
const estimate = args.estimateTokens ?? defaultEstimate;
|
|
50
|
+
const margin = args.marginTokens ?? DEFAULT_MARGIN_TOKENS;
|
|
51
|
+
const baseTokens = args.baseTokens ?? 0;
|
|
52
|
+
const maxInput = (args.maxInputTokensFor
|
|
53
|
+
? args.maxInputTokensFor(args.targetModel)
|
|
54
|
+
: getDetailedPricing(args.targetModel)?.maxInputTokens)
|
|
55
|
+
?? DEFAULT_MAX_INPUT_TOKENS;
|
|
56
|
+
// Budget remaining for the tool history AFTER the fixed costs (system +
|
|
57
|
+
// user + margin + the partial-text reinjection). If this is already
|
|
58
|
+
// negative the partial alone overflows the window → cannot serve.
|
|
59
|
+
const budgetForTools = maxInput - margin - baseTokens - args.prefill.sourceTokens;
|
|
60
|
+
if (budgetForTools < 0)
|
|
61
|
+
return undefined;
|
|
62
|
+
// Per-tool token estimates, oldest-first (prefill.toolUses preserves
|
|
63
|
+
// recording order). Drop from the FRONT until the sum fits.
|
|
64
|
+
const tools = args.prefill.toolUses.slice();
|
|
65
|
+
let toolSum = tools.reduce((acc, tu) => acc + toolUseTokens(tu, estimate), 0);
|
|
66
|
+
if (toolSum <= budgetForTools) {
|
|
67
|
+
return args.prefill; // fits as-is
|
|
68
|
+
}
|
|
69
|
+
while (tools.length > 0 && toolSum > budgetForTools) {
|
|
70
|
+
const dropped = tools.shift();
|
|
71
|
+
toolSum -= toolUseTokens(dropped, estimate);
|
|
72
|
+
}
|
|
73
|
+
// budgetForTools >= 0 guarantees the empty-tools case fits; return the
|
|
74
|
+
// trimmed prefill (which may have zero tools but always keeps the text).
|
|
75
|
+
return { ...args.prefill, toolUses: tools };
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=truncate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"truncate.js","sourceRoot":"","sources":["../../src/prefill/truncate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAGhD,wEAAwE;AACxE,MAAM,CAAC,MAAM,wBAAwB,GAAG,MAAM,CAAC;AAC/C,4DAA4D;AAC5D,MAAM,CAAC,MAAM,qBAAqB,GAAG,KAAK,CAAC;AAwB3C,MAAM,eAAe,GAAG,CAAC,IAAY,EAAU,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;AAE7E,2EAA2E;AAC3E,SAAS,aAAa,CACpB,EAA+B,EAC/B,QAA+B;IAE/B,MAAM,OAAO,GAAG,OAAO,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,QAAQ;QACpD,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC,OAAO;QACnB,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,IAAI,EAAE,CAAC,CAAC;IAC7C,OAAO,QAAQ,CAAC,GAAG,EAAE,CAAC,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC,KAAK,IAAI,EAAE,CAAC,GAAG,OAAO,EAAE,CAAC,CAAC;AAC3E,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,wBAAwB,CAAC,IAAyB;IAChE,MAAM,QAAQ,GAAG,IAAI,CAAC,cAAc,IAAI,eAAe,CAAC;IACxD,MAAM,MAAM,GAAG,IAAI,CAAC,YAAY,IAAI,qBAAqB,CAAC;IAC1D,MAAM,UAAU,GAAG,IAAI,CAAC,UAAU,IAAI,CAAC,CAAC;IAExC,MAAM,QAAQ,GAAG,CAAC,IAAI,CAAC,iBAAiB;QACtC,CAAC,CAAC,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,WAAW,CAAC;QAC1C,CAAC,CAAC,kBAAkB,CAAC,IAAI,CAAC,WAAW,CAAC,EAAE,cAAc,CAAC;WACpD,wBAAwB,CAAC;IAE9B,wEAAwE;IACxE,oEAAoE;IACpE,kEAAkE;IAClE,MAAM,cAAc,GAAG,QAAQ,GAAG,MAAM,GAAG,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,YAAY,CAAC;IAClF,IAAI,cAAc,GAAG,CAAC;QAAE,OAAO,SAAS,CAAC;IAEzC,qEAAqE;IACrE,4DAA4D;IAC5D,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC;IAC5C,IAAI,OAAO,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,EAAE,EAAE,CAAC,GAAG,GAAG,aAAa,CAAC,EAAE,EAAE,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC;IAE9E,IAAI,OAAO,IAAI,cAAc,EAAE,CAAC;QAC9B,OAAO,IAAI,CAAC,OAAO,CAAC,CAAC,aAAa;IACpC,CAAC;IAED,OAAO,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,OAAO,GAAG,cAAc,EAAE,CAAC;QACpD,MAAM,OAAO,GAAG,KAAK,CAAC,KAAK,EAAG,CAAC;QAC/B,OAAO,IAAI,aAAa,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;IAC9C,CAAC;IAED,uEAAuE;IACvE,yEAAyE;IACzE,OAAO,EAAE,GAAG,IAAI,CAAC,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;AAC9C,CAAC"}
|
package/dist/router/errors.d.ts
CHANGED
|
@@ -19,18 +19,35 @@ export declare class RouterError extends Error {
|
|
|
19
19
|
});
|
|
20
20
|
}
|
|
21
21
|
/**
|
|
22
|
-
* Map a raw error to an `ErrorClass`.
|
|
22
|
+
* Map a raw error to an `ErrorClass`. This is the single source of truth for
|
|
23
|
+
* error classification across the whole stack — `LlmRouter.invoke` (single
|
|
24
|
+
* shot), `LlmRouter.invokeStream` (streaming), and `LlmRouter.runAgent`
|
|
25
|
+
* (agentic chain walk) all route through here.
|
|
23
26
|
*
|
|
24
27
|
* Recognizes:
|
|
25
|
-
* -
|
|
26
|
-
* - 5xx → server_5xx
|
|
27
|
-
* -
|
|
28
|
-
* -
|
|
29
|
-
* -
|
|
30
|
-
* -
|
|
28
|
+
* - network (status 0 / fetch failed / ECONNRESET / EPIPE / abort / timeout) → timeout
|
|
29
|
+
* - 429 → rate_limit; 5xx → server_5xx; 401/403 → auth
|
|
30
|
+
* - 400 + content/safety hints → content_policy; other 400 → invalid_request
|
|
31
|
+
* - body: phantom-model / hard-billing → model_unavailable
|
|
32
|
+
* - body: quota / rate-limit / overloaded / resource-exhausted → rate_limit
|
|
33
|
+
* - body: auth/permission → auth
|
|
34
|
+
* - explicit `.retryable === true` with no better signal → timeout (fall-back eligible)
|
|
31
35
|
* - otherwise → unknown
|
|
32
36
|
*/
|
|
33
37
|
export declare function classifyError(err: unknown): ErrorClass;
|
|
38
|
+
/**
|
|
39
|
+
* Terminal classes never trigger cross-model fallback (a different model from
|
|
40
|
+
* the same wall won't help) and never count toward the circuit breaker.
|
|
41
|
+
*/
|
|
42
|
+
export declare function isTerminalErrorClass(cls: ErrorClass): boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Fall-back-eligible classes: a transient/provider-side failure where trying a
|
|
45
|
+
* DIFFERENT model in the chain is the right move. This is the unified
|
|
46
|
+
* replacement for the former binary `isRetryableUpstreamError` duck-type. Note
|
|
47
|
+
* `unknown` is intentionally excluded — a generic error is more likely a bug
|
|
48
|
+
* than a provider fault, so it should surface rather than burn the chain.
|
|
49
|
+
*/
|
|
50
|
+
export declare function isFallbackEligibleErrorClass(cls: ErrorClass): boolean;
|
|
34
51
|
/**
|
|
35
52
|
* Parse a `Retry-After` header value (seconds-since-now or HTTP-date)
|
|
36
53
|
* into a millisecond delay. Returns `null` if absent or unparseable.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/router/errors.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE3D;;;GAGG;AACH,qBAAa,WAAY,SAAQ,KAAK;IACpC,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC,YAAY,CAAC,CAAC;IAC/C,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC;gBAGrB,OAAO,EAAE,MAAM,EACf,IAAI,EAAE;QAAE,QAAQ,EAAE,aAAa,CAAC,YAAY,CAAC,CAAC;QAAC,KAAK,CAAC,EAAE,KAAK,CAAA;KAAE;CAOjE;
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/router/errors.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE3D;;;GAGG;AACH,qBAAa,WAAY,SAAQ,KAAK;IACpC,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC,YAAY,CAAC,CAAC;IAC/C,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC;gBAGrB,OAAO,EAAE,MAAM,EACf,IAAI,EAAE;QAAE,QAAQ,EAAE,aAAa,CAAC,YAAY,CAAC,CAAC;QAAC,KAAK,CAAC,EAAE,KAAK,CAAA;KAAE;CAOjE;AAkCD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,OAAO,GAAG,UAAU,CAqEtD;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,UAAU,GAAG,OAAO,CAE7D;AAED;;;;;;GAMG;AACH,wBAAgB,4BAA4B,CAAC,GAAG,EAAE,UAAU,GAAG,OAAO,CAKrE;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,GAAG,MAAM,GAAG,IAAI,CAY7F"}
|