agent-accelerator 0.5.6 → 0.5.8
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 +10 -17
- package/package.json +1 -1
- package/src/agent/agent.ts +92 -57
- package/src/agent/architecture.md +25 -0
- package/src/agent/context.ts +7 -3
- package/src/agent/delegation.ts +30 -96
- package/src/agent/loop.ts +54 -56
- package/src/agent/subagent.ts +4 -4
- package/src/agent/worker.ts +1 -3
- package/src/architecture.md +28 -0
- package/src/index.ts +13 -80
- package/src/models/architecture.md +14 -0
- package/src/models/catalog-cache.ts +10 -7
- package/src/models/catalog.ts +20 -13
- package/src/providers/architecture.md +24 -0
- package/src/providers/google.ts +24 -4
- package/src/providers/openai-compat.ts +29 -7
- package/src/providers/openai.ts +22 -6
- package/src/providers/openrouter.ts +14 -2
- package/src/providers/registry.ts +1 -0
- package/src/providers/shared.ts +4 -4
- package/src/providers.ts +9 -53
- package/src/scheduler/architecture.md +22 -0
- package/src/scheduler/scheduler.ts +131 -56
- package/src/scheduler/store.ts +274 -54
- package/src/scheduler/time.ts +2 -0
- package/src/session/architecture.md +19 -0
- package/src/session/store.ts +37 -22
- package/src/session/usage.ts +1 -0
- package/src/streaming/architecture.md +13 -0
- package/src/streaming/event-stream.ts +16 -11
- package/src/telemetry-file.ts +74 -24
- package/src/telemetry.ts +3 -2
- package/src/tools/architecture.md +19 -0
- package/src/tools/executor.ts +14 -1
- package/src/tools/schema.ts +1 -0
- package/src/tools/subagent-result.ts +1 -0
- package/src/tools/tool.ts +8 -8
- package/src/types/agent.ts +22 -20
- package/src/types/architecture.md +20 -0
- package/src/types/response.ts +8 -8
- package/src/types/scheduler.ts +2 -2
- package/src/types/tool.ts +5 -5
- package/src/update-models.ts +2 -2
- package/src/utils/architecture.md +25 -0
- package/src/utils/base64.ts +2 -0
- package/src/utils/cache.ts +6 -4
- package/src/utils/durable.ts +78 -3
- package/src/utils/env.ts +1 -1
- package/src/utils/errors.ts +4 -0
- package/src/utils/headers.ts +7 -4
- package/src/utils/home.ts +1 -1
- package/src/utils/media.ts +1 -0
- package/src/utils/raw-capture.ts +3 -0
- package/src/utils/retry.ts +2 -1
- package/src/utils/safe-io.ts +4 -0
- package/src/utils/session.ts +10 -166
package/src/index.ts
CHANGED
|
@@ -1,28 +1,10 @@
|
|
|
1
1
|
import type { AssistantMessageEventStream as AssistantMessageEventStreamT } from "./streaming/event-stream.ts";
|
|
2
2
|
import type { AgentResponse as AgentResponseT } from "./types/response.ts";
|
|
3
3
|
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// AssistantMessageEventStream, document conversion, delegation primitives,
|
|
7
|
-
// provider registry, catalog, errors, and option/normalization helpers.
|
|
8
|
-
// Everything else below is a shim kept for compat and marked @deprecated
|
|
9
|
-
// toward a future `/internal` entry. Nothing is deleted (would break tests).
|
|
10
|
-
//
|
|
11
|
-
// Q-27 node split (deferred): this root entry pulls `session/store.ts`, which
|
|
12
|
-
// imports `node:fs` for session persistence. A future `/node` subpath will
|
|
13
|
-
// host the fs-backed persistence/telemetry surface; the root entry stays
|
|
14
|
-
// runtime-agnostic. Until then, browser/bundler consumers should import the
|
|
15
|
-
// documented core shims only.
|
|
16
|
-
//
|
|
17
|
-
// Q-51 one way to run: `run()` is canonical. `ask()` and `run/stream: true`
|
|
18
|
-
// remain for compat but are deprecated — prefer `run()` for single-shot and
|
|
19
|
-
// `stream()` for event iteration. `Run` (below) is the typed streaming handle.
|
|
4
|
+
// Public barrel: the one entry point of the package (`main`/`module`/`types`). `run()` is the canonical way to run an
|
|
5
|
+
// agent and `stream()` the canonical way to iterate events; `ask()` and `run(..., { stream: true })` remain as shorthands.
|
|
20
6
|
|
|
21
|
-
/**
|
|
22
|
-
* Typed streaming run handle (Q-51): the `AssistantMessageEventStream`
|
|
23
|
-
* returned by `stream()`/`run(stream:true)`, with its terminal `result()` and
|
|
24
|
-
* `cancel()` surfaced on the type for callers that only need the handle.
|
|
25
|
-
*/
|
|
7
|
+
/** The `AssistantMessageEventStream` returned by `stream()`/`run(stream:true)`, typed with its terminal `result()` and `cancel()`. */
|
|
26
8
|
export type Run = AssistantMessageEventStreamT & {
|
|
27
9
|
result: () => Promise<AgentResponseT>;
|
|
28
10
|
cancel: (reason?: unknown) => void;
|
|
@@ -33,7 +15,7 @@ export { Agent, SubAgentModelError } from "./agent/agent.ts";
|
|
|
33
15
|
/**
|
|
34
16
|
* @deprecated Compat alias: prefer composing a plain `Agent` and exposing it
|
|
35
17
|
* via `agentToTool()`/`buildAgentTools()` (or `subagents: [...]`). `SubAgent`
|
|
36
|
-
* stays for compat and receives no new features
|
|
18
|
+
* stays for compat and receives no new features.
|
|
37
19
|
*/
|
|
38
20
|
export { SubAgent, agentFromEnv } from "./agent/subagent.ts";
|
|
39
21
|
export type { SubAgentConfig } from "./agent/subagent.ts";
|
|
@@ -51,7 +33,7 @@ export type { CreateToolOptions } from "./tools/tool.ts";
|
|
|
51
33
|
export { zodToJsonSchema, cleanJsonSchema } from "./tools/schema.ts";
|
|
52
34
|
/**
|
|
53
35
|
* @deprecated Internal executor surface. Prefer `Agent` runs; for direct
|
|
54
|
-
* execution import from the future `/internal` entry
|
|
36
|
+
* execution import from the future `/internal` entry.
|
|
55
37
|
*/
|
|
56
38
|
export { executeToolCalls } from "./tools/executor.ts";
|
|
57
39
|
|
|
@@ -64,27 +46,11 @@ export {
|
|
|
64
46
|
createChildSessionId,
|
|
65
47
|
createFixedChildSessionId,
|
|
66
48
|
isSessionDescendant,
|
|
67
|
-
getSubAgentTrace,
|
|
68
|
-
getSubAgentTraceScoped,
|
|
69
|
-
getSubAgentTraceForSessions,
|
|
70
|
-
listSubAgentTraceIds,
|
|
71
|
-
listSubAgentTraceIdsForSessions,
|
|
72
|
-
subscribeToSubAgent,
|
|
73
|
-
subscribeToSubAgentForSessions,
|
|
74
|
-
appendSubAgentStep,
|
|
75
|
-
pruneTraces,
|
|
76
|
-
trimTraceSteps,
|
|
77
49
|
checkBudget,
|
|
78
50
|
chargeBudgetUsage,
|
|
79
51
|
reserveSpawn,
|
|
80
|
-
isStreamingLoop,
|
|
81
|
-
escapeXml as escapeXmlFromDelegation,
|
|
82
|
-
MAX_SUBAGENT_TRACES,
|
|
83
|
-
SUBAGENT_TRACE_TTL_MS,
|
|
84
|
-
MAX_SUBAGENT_STEPS_PER_TRACE,
|
|
85
|
-
SUBAGENT_SUBSCRIBE_TTL_MS,
|
|
86
52
|
} from "./agent/delegation.ts";
|
|
87
|
-
export type { AgentAsToolTarget, SubAgentTrace
|
|
53
|
+
export type { AgentAsToolTarget, SubAgentTrace } from "./agent/delegation.ts";
|
|
88
54
|
export type { WorkerRegistry, WorkerSpec, WorkerInfo, WorkerReply, WorkerCallOptions } from "./agent/workers.ts";
|
|
89
55
|
|
|
90
56
|
// Scheduled actions: dynamic sub-agents that run later and save their answer to a file
|
|
@@ -147,20 +113,12 @@ export { resolveEffectiveThinking } from "./agent/agent.ts";
|
|
|
147
113
|
export { withRetries, isTransientError } from "./utils/retry.ts";
|
|
148
114
|
export { toConciseProviderError, assertModalitiesSupported, assertNoVideoPartsOnResponses } from "./utils/errors.ts";
|
|
149
115
|
// Native provider implementations.
|
|
150
|
-
/** @deprecated The discontinued beta-Responses transport was removed
|
|
151
|
-
* directly for frozen wire-contract evidence only. It is wired nowhere and
|
|
152
|
-
* must not be extended or re-exported here (Q-46). */
|
|
153
116
|
export {
|
|
154
117
|
OpenAICompatibleChatProvider,
|
|
155
118
|
createOpenAICompatibleProvider,
|
|
156
119
|
createCustomProvider,
|
|
157
120
|
CustomProvider,
|
|
158
121
|
} from "./providers/openai-compat.ts";
|
|
159
|
-
/**
|
|
160
|
-
* @deprecated Compat alias of `OpenAICompatibleChatProvider` (canonical).
|
|
161
|
-
* Kept for back-compat only; new code uses the canonical name (Q-50).
|
|
162
|
-
*/
|
|
163
|
-
export { OpenAICompatibleChatProvider as OpenAICompatibleProvider } from "./providers/openai-compat.ts";
|
|
164
122
|
export type { CustomProviderOptions } from "./providers/openai-compat.ts";
|
|
165
123
|
|
|
166
124
|
// Canonical provider contract (provider-agnostic) + native adapters.
|
|
@@ -168,33 +126,16 @@ export {
|
|
|
168
126
|
GoogleInteractionsProvider,
|
|
169
127
|
clearInteractionChains,
|
|
170
128
|
} from "./providers/google.ts";
|
|
171
|
-
/**
|
|
172
|
-
* @deprecated Compat alias of `GoogleInteractionsProvider` (canonical).
|
|
173
|
-
* Kept for back-compat only; new code uses the canonical name (Q-50).
|
|
174
|
-
*/
|
|
175
|
-
export { GoogleInteractionsProvider as GoogleAIStudioProvider } from "./providers/google.ts";
|
|
176
129
|
export {
|
|
177
130
|
OpenRouterChatCompletionsProvider,
|
|
178
131
|
} from "./providers/openrouter.ts";
|
|
179
|
-
/**
|
|
180
|
-
* @deprecated Compat alias of `OpenRouterChatCompletionsProvider`
|
|
181
|
-
* (canonical). Kept for back-compat only (Q-50).
|
|
182
|
-
*/
|
|
183
|
-
export { OpenRouterChatCompletionsProvider as OpenRouterProvider } from "./providers/openrouter.ts";
|
|
184
|
-
// Discontinued beta-Responses transport, removed from core
|
|
185
|
-
// (was frozen migration evidence, wired nowhere): OpenRouterResponsesProvider.
|
|
186
132
|
export {
|
|
187
133
|
OpenAIResponsesProvider,
|
|
188
134
|
} from "./providers/openai.ts";
|
|
189
|
-
/**
|
|
190
|
-
* @deprecated Compat alias of `OpenAIResponsesProvider` (canonical).
|
|
191
|
-
* Kept for back-compat only; new code uses the canonical name (Q-50).
|
|
192
|
-
*/
|
|
193
|
-
export { OpenAIResponsesProvider as OpenAIProvider } from "./providers/openai.ts";
|
|
194
135
|
/**
|
|
195
136
|
* @deprecated Internal provider mappers/routing helpers. Prefer the
|
|
196
137
|
* provider-agnostic registry (`resolveModel`/`getProvider`); direct mapper
|
|
197
|
-
* use moves to the future `/internal` entry
|
|
138
|
+
* use moves to the future `/internal` entry.
|
|
198
139
|
*/
|
|
199
140
|
export {
|
|
200
141
|
emitProviderWarning,
|
|
@@ -205,9 +146,7 @@ export {
|
|
|
205
146
|
clearSessionRouting,
|
|
206
147
|
mapThinkingLevelToGoogle,
|
|
207
148
|
mapServiceTierToGoogle,
|
|
208
|
-
mapThinkingLevelToOpenRouter,
|
|
209
149
|
mapServiceTierToOpenRouter,
|
|
210
|
-
mapToolChoiceToOpenRouter,
|
|
211
150
|
mapThinkingLevelToOpenRouterChat,
|
|
212
151
|
mapToolChoiceToOpenRouterChat,
|
|
213
152
|
mapThinkingLevelToOpenAI,
|
|
@@ -230,7 +169,6 @@ export {
|
|
|
230
169
|
export type {
|
|
231
170
|
ProviderCapabilityStatus,
|
|
232
171
|
CanonicalToolChoiceMode,
|
|
233
|
-
OpenRouterToolChoice,
|
|
234
172
|
OpenRouterChatToolChoice,
|
|
235
173
|
OpenAIToolChoice,
|
|
236
174
|
ChatCompletionsResponseFormat,
|
|
@@ -261,17 +199,12 @@ export { clampCacheKey } from "./utils/cache.ts";
|
|
|
261
199
|
|
|
262
200
|
// Streaming & Events
|
|
263
201
|
export { AssistantMessageEventStream } from "./streaming/event-stream.ts";
|
|
264
|
-
/**
|
|
265
|
-
* @deprecated Internal SSE plumbing. Prefer `AssistantMessageEventStream`;
|
|
266
|
-
* direct parser use moves to the future `/internal` entry (Q-50).
|
|
267
|
-
*/
|
|
268
|
-
export { SSEParser } from "./streaming/sse-parser.ts";
|
|
269
202
|
export type { SteerEntry } from "./agent/loop.ts";
|
|
270
203
|
|
|
271
204
|
// Responses
|
|
272
205
|
export { AgentResponse } from "./types/response.ts";
|
|
273
206
|
|
|
274
|
-
// Core error taxonomy (
|
|
207
|
+
// Core error taxonomy (core surface)
|
|
275
208
|
export {
|
|
276
209
|
AgentAccelError,
|
|
277
210
|
ConfigError,
|
|
@@ -286,7 +219,7 @@ export {
|
|
|
286
219
|
formatErrorPlain,
|
|
287
220
|
} from "./types/errors.ts";
|
|
288
221
|
export type { AgentAccelErrorCode, AgentAccelErrorContext } from "./types/errors.ts";
|
|
289
|
-
// Normalized run limits (
|
|
222
|
+
// Normalized run limits (sub-agents inherit unless overridden)
|
|
290
223
|
export type { AgentRunLimits } from "./agent/agent.ts";
|
|
291
224
|
|
|
292
225
|
// Telemetry bus: warnings, anomalies and per-turn thinking traces (attach with `telemetryBus.onEvent`)
|
|
@@ -323,14 +256,14 @@ export type { SessionLimits, SessionPayloadStatus, PersistedAgentSession, Sessio
|
|
|
323
256
|
// Utilities
|
|
324
257
|
/**
|
|
325
258
|
* @deprecated Session-routing internals. Prefer agent-level `sessionId`;
|
|
326
|
-
* direct use moves to the future `/internal` entry
|
|
259
|
+
* direct use moves to the future `/internal` entry.
|
|
327
260
|
*/
|
|
328
|
-
export { createSessionId, hashSessionPart, createTrackingId, nowMs, newRunId, newTurnId, previewPrompt, parseModelRef
|
|
329
|
-
export type { ParsedModelRef
|
|
261
|
+
export { createSessionId, hashSessionPart, createTrackingId, nowMs, newRunId, newTurnId, previewPrompt, parseModelRef } from "./utils/session.ts";
|
|
262
|
+
export type { ParsedModelRef } from "./utils/session.ts";
|
|
330
263
|
/**
|
|
331
264
|
* @deprecated Raw env access. Prefer explicit `Agent` config (which wins over
|
|
332
265
|
* env) and `Agent.fromEnv()`/`agentFromEnv()`; direct use moves to the future
|
|
333
|
-
* `/internal` entry
|
|
266
|
+
* `/internal` entry.
|
|
334
267
|
*/
|
|
335
268
|
export { getApiKey, getEnv } from "./utils/env.ts";
|
|
336
269
|
export { buildSessionHeaders, setAttribution, getAttribution } from "./utils/headers.ts";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# src/models/
|
|
2
|
+
|
|
3
|
+
The model catalog: context limits, prices, modalities and reasoning options come from models.dev, not from code.
|
|
4
|
+
|
|
5
|
+
| File | What it is |
|
|
6
|
+
| :-- | :-- |
|
|
7
|
+
| `catalog-cache.ts` | Downloads and caches `models.dev/api.json` in `<home>/catalog/` (or `AGENT_CACHE_DIR`). Stale-while-revalidate: an expired catalog is served at once and refreshed in the background (8 s timeout, 10 min backoff after a failure); only a cold start waits. `AGENT_CATALOG_REFRESH=off` stops automatic refreshes. |
|
|
8
|
+
| `catalog.ts` | Lookups (`getModelFromCatalog`), `registerModel`, thinking validation (`validateModelThinking`), and the exported `GOOGLE_/OPENAI_/OPENROUTER_MODELS` arrays (rebuilt in place when the catalog changes). |
|
|
9
|
+
|
|
10
|
+
## Invariants
|
|
11
|
+
- Never hand-edit the cache file; register custom models with `registerModel()`. Verified modality fixes go in `MODALITY_OVERRIDES`.
|
|
12
|
+
- The global bare-id fallback applies to first-class providers and explicit bare-id lookups only. A custom prefix never borrows another provider's price or modalities: unknown stays unknown.
|
|
13
|
+
- A payload that is not a catalog (fewer than 3 providers with models) never overwrites the cache.
|
|
14
|
+
- Do not keep static copies of catalog data; use the lookups or the update listener.
|
|
@@ -9,13 +9,13 @@ import { writeFileDurable } from "../utils/durable.ts";
|
|
|
9
9
|
/** Default TTL: 12 Hours (in milliseconds). */
|
|
10
10
|
export const DEFAULT_CATALOG_TTL_MS = 12 * 60 * 60 * 1000;
|
|
11
11
|
|
|
12
|
-
/** Max accepted models.dev payload size: 15MB
|
|
12
|
+
/** Max accepted models.dev payload size: 15MB. Larger payloads are rejected before parse. */
|
|
13
13
|
export const MAX_CATALOG_PAYLOAD_BYTES = 15 * 1024 * 1024;
|
|
14
14
|
|
|
15
15
|
let globalCatalogTtlMs = DEFAULT_CATALOG_TTL_MS;
|
|
16
16
|
|
|
17
17
|
/** Sets the global model catalog cache TTL in milliseconds.
|
|
18
|
-
* Note
|
|
18
|
+
* Note: cache files carry their own `ttlMs`. A global `setCatalogTTL`
|
|
19
19
|
* does not rewrite in-flight file TTLs; it takes effect on the next refresh
|
|
20
20
|
* (fresh download) or when the cached file has no `ttlMs` of its own.
|
|
21
21
|
*/
|
|
@@ -64,11 +64,13 @@ export interface RefreshCatalogOptions {
|
|
|
64
64
|
type CatalogUpdateListener = () => void;
|
|
65
65
|
const updateListeners: Set<CatalogUpdateListener> = new Set();
|
|
66
66
|
|
|
67
|
+
/** Registers a callback run whenever the catalog changes; in: catalog.ts (cache invalidation), out: an unsubscribe function. */
|
|
67
68
|
export function registerCatalogUpdateListener(listener: CatalogUpdateListener): () => void {
|
|
68
69
|
updateListeners.add(listener);
|
|
69
70
|
return () => updateListeners.delete(listener);
|
|
70
71
|
}
|
|
71
72
|
|
|
73
|
+
// Runs every catalog-change listener, ignoring their errors; in: refresh and home change, out: the listeners.
|
|
72
74
|
function notifyUpdateListeners(): void {
|
|
73
75
|
for (const listener of updateListeners) {
|
|
74
76
|
try {
|
|
@@ -88,11 +90,12 @@ export function getCacheDir(): string {
|
|
|
88
90
|
return path.join(resolveHome(), "catalog");
|
|
89
91
|
}
|
|
90
92
|
|
|
91
|
-
/** Cache file path: `<cacheDir>/models.dev.json`, honoring AGENT_CACHE_DIR
|
|
93
|
+
/** Cache file path: `<cacheDir>/models.dev.json`, honoring AGENT_CACHE_DIR. */
|
|
92
94
|
export function getCacheFilePath(): string {
|
|
93
95
|
return path.join(getCacheDir(), "models.dev.json");
|
|
94
96
|
}
|
|
95
97
|
|
|
98
|
+
// Reads and parses a JSON file, null when missing or damaged; in: the cache loader, out: the parsed value.
|
|
96
99
|
function readJsonFileSync(filePath: string): any {
|
|
97
100
|
try {
|
|
98
101
|
if (!fs.existsSync(filePath)) return null;
|
|
@@ -108,7 +111,7 @@ function writeJsonFileSync(filePath: string, data: any): void {
|
|
|
108
111
|
writeFileDurable(filePath, JSON.stringify(data), { backup: false });
|
|
109
112
|
}
|
|
110
113
|
|
|
111
|
-
/** Verifies optional sha256 pin from MODELS_DEV_SHA256
|
|
114
|
+
/** Verifies optional sha256 pin from MODELS_DEV_SHA256. Returns false on mismatch. */
|
|
112
115
|
export function verifyCatalogSha256(text: string): boolean {
|
|
113
116
|
const pin = getEnv("MODELS_DEV_SHA256")?.trim().toLowerCase();
|
|
114
117
|
if (!pin) return true;
|
|
@@ -280,14 +283,14 @@ export async function refreshModelCatalog(options: RefreshCatalogOptions = {}):
|
|
|
280
283
|
throw new Error(`HTTP ${res.status} ${res.statusText}`);
|
|
281
284
|
}
|
|
282
285
|
|
|
283
|
-
//
|
|
286
|
+
// Size-cap before parse (check text length, reject >15MB).
|
|
284
287
|
const text = await res.text();
|
|
285
288
|
if (text.length > MAX_CATALOG_PAYLOAD_BYTES) {
|
|
286
289
|
clearTimeout(timer);
|
|
287
290
|
fallbackToDiskCache();
|
|
288
291
|
return getCatalogStatus();
|
|
289
292
|
}
|
|
290
|
-
//
|
|
293
|
+
// Optional sha256 pin via MODELS_DEV_SHA256.
|
|
291
294
|
if (!verifyCatalogSha256(text)) {
|
|
292
295
|
clearTimeout(timer);
|
|
293
296
|
fallbackToDiskCache();
|
|
@@ -331,7 +334,7 @@ export async function refreshModelCatalog(options: RefreshCatalogOptions = {}):
|
|
|
331
334
|
|
|
332
335
|
// Update in-memory state FIRST so a read-only filesystem still serves the
|
|
333
336
|
// fresh catalog for this process; disk persistence below is best-effort.
|
|
334
|
-
//
|
|
337
|
+
// A real download is NOT from cache.
|
|
335
338
|
activeCatalog = freshData;
|
|
336
339
|
activeFetchedAt = fetchedAt;
|
|
337
340
|
activeTtlMs = effectiveTtl;
|
package/src/models/catalog.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Model catalog
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* Model catalog: the single source of truth (models.dev) for context limits, prices, modalities and reasoning options.
|
|
3
|
+
* The data is cached in `<home>/catalog/models.dev.json` (or `AGENT_CACHE_DIR`) with a 12 hour TTL and refreshed in the
|
|
4
|
+
* background (see `catalog-cache.ts`). `GOOGLE_MODELS`, `OPENAI_MODELS` and `OPENROUTER_MODELS` are views of it,
|
|
5
|
+
* rebuilt in place whenever it changes.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import {
|
|
@@ -41,6 +41,7 @@ const MODALITY_OVERRIDES: Record<string, { input?: string[]; output?: string[] }
|
|
|
41
41
|
// 404 still surfaces as a one-line error.
|
|
42
42
|
};
|
|
43
43
|
|
|
44
|
+
// Applies the verified modality corrections over the catalog's input/output lists; in: mapModelSpec, out: ModelModalities.
|
|
44
45
|
function applyModalityOverrides(provider: string, modelId: string, base: ModelModalities): ModelModalities {
|
|
45
46
|
const hit =
|
|
46
47
|
MODALITY_OVERRIDES[`${provider}/${modelId}`] ??
|
|
@@ -65,6 +66,7 @@ function allowsGlobalFallback(providerInput: string, modelIdInput: string): bool
|
|
|
65
66
|
return p in PROVIDER_ALIASES || p === "gemini" || p === modelIdInput.trim().toLowerCase();
|
|
66
67
|
}
|
|
67
68
|
|
|
69
|
+
// Drops a leading `provider/` scope from a model id; in: the lookups, out: the bare id.
|
|
68
70
|
function normalizeModelIdForLookup(modelId: string): string {
|
|
69
71
|
if (modelId.includes("/")) {
|
|
70
72
|
const parts = modelId.split("/");
|
|
@@ -75,12 +77,12 @@ function normalizeModelIdForLookup(modelId: string): string {
|
|
|
75
77
|
|
|
76
78
|
// ---------------------------------------------------------------------------
|
|
77
79
|
// Internal cache — battle-tested: avoid repeated global scans (7487 models)
|
|
78
|
-
// Bound lookupCache to 1000 entries LRU
|
|
80
|
+
// Bound lookupCache to 1000 entries LRU: evict oldest on overflow.
|
|
79
81
|
// ---------------------------------------------------------------------------
|
|
80
82
|
const lookupCache = new Map<string, ModelSpec | undefined>();
|
|
81
83
|
const providerModelsCache = new Map<string, ModelSpec[]>();
|
|
82
84
|
|
|
83
|
-
/** Bounded LRU set for lookupCache
|
|
85
|
+
/** Bounded LRU set for lookupCache: keeps at most 1000 entries. */
|
|
84
86
|
function setLookupCache(key: string, value: ModelSpec | undefined): void {
|
|
85
87
|
if (lookupCache.has(key)) lookupCache.delete(key);
|
|
86
88
|
else if (lookupCache.size >= 1000) {
|
|
@@ -90,15 +92,16 @@ function setLookupCache(key: string, value: ModelSpec | undefined): void {
|
|
|
90
92
|
lookupCache.set(key, value);
|
|
91
93
|
}
|
|
92
94
|
|
|
93
|
-
// Custom-registered models
|
|
95
|
+
// Custom-registered models: checked first in getModelFromCatalog.
|
|
94
96
|
const customModelRegistry = new Map<string, ModelSpec>();
|
|
95
97
|
|
|
98
|
+
// Case-insensitive key of a registered custom model; in: registerModel and lookups, out: a string.
|
|
96
99
|
function customRegistryKey(provider: string, modelId: string): string {
|
|
97
100
|
return `${provider.toLowerCase()}::${modelId.toLowerCase()}`;
|
|
98
101
|
}
|
|
99
102
|
|
|
100
103
|
/**
|
|
101
|
-
* Registers a custom model spec that takes precedence over the catalog
|
|
104
|
+
* Registers a custom model spec that takes precedence over the catalog.
|
|
102
105
|
* Stored in a separate map, checked first in `getModelFromCatalog`.
|
|
103
106
|
*/
|
|
104
107
|
export function registerModel(spec: ModelSpec): void {
|
|
@@ -121,7 +124,7 @@ export function clearCustomModels(): void {
|
|
|
121
124
|
}
|
|
122
125
|
|
|
123
126
|
/**
|
|
124
|
-
* Returns where a model resolves from without touching frozen types
|
|
127
|
+
* Returns where a model resolves from without touching frozen types:
|
|
125
128
|
* `"override"` for custom-registered or MODALITY_OVERRIDES-covered models,
|
|
126
129
|
* `"catalog"` for plain catalog hits, `"unknown"` when unresolvable.
|
|
127
130
|
*/
|
|
@@ -141,6 +144,7 @@ export function getModelSource(provider: string, modelId: string): "catalog" | "
|
|
|
141
144
|
return getModelFromCatalog(provider, modelId) ? "catalog" : "unknown";
|
|
142
145
|
}
|
|
143
146
|
|
|
147
|
+
// Derives capability flags (thinking, caching, tools, structured output, modalities) from a raw catalog entry; in: mapModelSpec, out: ModelCapabilities.
|
|
144
148
|
function mapCapabilities(raw: any, cost: ModelCost, reasoning: boolean, toolCall: boolean, modalities: ModelModalities) {
|
|
145
149
|
return {
|
|
146
150
|
supportsThinking: reasoning,
|
|
@@ -158,6 +162,7 @@ function mapCapabilities(raw: any, cost: ModelCost, reasoning: boolean, toolCall
|
|
|
158
162
|
};
|
|
159
163
|
}
|
|
160
164
|
|
|
165
|
+
// Turns a raw models.dev entry into a ModelSpec with limits, cost, pricing and capabilities; in: the lookups, out: a ModelSpec.
|
|
161
166
|
function mapModelSpec(provider: string, modelId: string, raw: any): ModelSpec {
|
|
162
167
|
const limit: ModelLimit = raw.limit || { context: 128000, output: 8192 };
|
|
163
168
|
const cost: ModelCost = raw.cost || {};
|
|
@@ -214,6 +219,7 @@ interface IndexEntry {
|
|
|
214
219
|
|
|
215
220
|
let globalIndex: Map<string, IndexEntry> | null = null;
|
|
216
221
|
|
|
222
|
+
// The lazily built id-to-entry index across all providers, used only for first-class or bare-id fallbacks; in: getModelFromCatalog, out: the index.
|
|
217
223
|
function getGlobalIndex(): Map<string, IndexEntry> {
|
|
218
224
|
if (globalIndex) return globalIndex;
|
|
219
225
|
globalIndex = new Map();
|
|
@@ -250,7 +256,7 @@ export function getModelFromCatalog(providerInput: string, modelIdInput: string)
|
|
|
250
256
|
const modelId = modelIdInput.trim();
|
|
251
257
|
let result: ModelSpec | undefined;
|
|
252
258
|
|
|
253
|
-
//
|
|
259
|
+
// Custom-registered models win over the catalog.
|
|
254
260
|
const customHit =
|
|
255
261
|
customModelRegistry.get(customRegistryKey(provider, modelId)) ??
|
|
256
262
|
customModelRegistry.get(customRegistryKey(provider, normalizeModelIdForLookup(modelId)));
|
|
@@ -281,7 +287,7 @@ export function getModelFromCatalog(providerInput: string, modelIdInput: string)
|
|
|
281
287
|
return result;
|
|
282
288
|
}
|
|
283
289
|
}
|
|
284
|
-
//
|
|
290
|
+
// Tightened fuzzy match — exact, `endsWith("/"+modelId)` boundary,
|
|
285
291
|
// or normalized equality. The old loose `modelId.endsWith(key)` is removed
|
|
286
292
|
// (e.g. "gpt-4o-mini-extra" must not match "mini").
|
|
287
293
|
for (const [key, raw] of Object.entries(providerData.models as Record<string, any>)) {
|
|
@@ -557,7 +563,7 @@ export function getModelsForProvider(provider: string): ModelSpec[] {
|
|
|
557
563
|
out.push(mapModelSpec(alias as ProviderId, key, raw));
|
|
558
564
|
}
|
|
559
565
|
}
|
|
560
|
-
//
|
|
566
|
+
// Include custom-registered models for this provider/alias set.
|
|
561
567
|
for (const spec of customModelRegistry.values()) {
|
|
562
568
|
if (aliases.includes(String(spec.provider)) || String(spec.provider) === provider) {
|
|
563
569
|
if (!seen.has(spec.id)) {
|
|
@@ -567,7 +573,7 @@ export function getModelsForProvider(provider: string): ModelSpec[] {
|
|
|
567
573
|
}
|
|
568
574
|
}
|
|
569
575
|
// Also include our internal provider id mapping
|
|
570
|
-
//
|
|
576
|
+
// Return copies — cache the canonical array, hand out a fresh copy.
|
|
571
577
|
providerModelsCache.set(provider, out);
|
|
572
578
|
return [...out];
|
|
573
579
|
}
|
|
@@ -576,6 +582,7 @@ export const GOOGLE_MODELS: ModelSpec[] = [...getModelsForProvider("google")];
|
|
|
576
582
|
export const OPENAI_MODELS: ModelSpec[] = [...getModelsForProvider("openai")];
|
|
577
583
|
export const OPENROUTER_MODELS: ModelSpec[] = [...getModelsForProvider("openrouter")];
|
|
578
584
|
|
|
585
|
+
// Rebuilds the GOOGLE/OPENAI/OPENROUTER_MODELS arrays in place after a catalog change; in: the update listener, out: those exports.
|
|
579
586
|
function refreshExportedModels(): void {
|
|
580
587
|
GOOGLE_MODELS.length = 0;
|
|
581
588
|
GOOGLE_MODELS.push(...getModelsForProvider("google"));
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# src/providers/
|
|
2
|
+
|
|
3
|
+
All HTTP and wire parsing. Canonical mappers (thinking, cache, tool choice, structured output) live in `../providers.ts`, never here.
|
|
4
|
+
|
|
5
|
+
| File | Wire | Notes |
|
|
6
|
+
| :-- | :-- | :-- |
|
|
7
|
+
| `google.ts` | Google Interactions | Stateful chaining (TTL gate, self-heal on `not_found`), exact replay of raw `steps`, media consts at the top, Gemini 4 long decode as background interactions. |
|
|
8
|
+
| `openai.ts` | OpenAI Responses | Stateless (`store:false`), full history every turn. |
|
|
9
|
+
| `openrouter.ts` | OpenRouter Chat Completions | Every request streams (so it can be cancelled); `generate()` collects its own stream. |
|
|
10
|
+
| `openai-compat.ts` | OpenAI-compatible Chat Completions | Serves every custom prefix; strict subset of fields; no default base URL. |
|
|
11
|
+
| `registry.ts` | - | Routes `provider/model` to an adapter; the registry is immutable; `ModelProvider` helpers. |
|
|
12
|
+
| `shared.ts` | - | Helpers every adapter uses: signals and timers, error payloads, ids, retry-after, SSE scaffold (`linkStreamAbort`, `withAbortTimer`, `readSseData`, `incompleteStreamCheck`, `failStream`). |
|
|
13
|
+
|
|
14
|
+
## Contract (`Provider`, in `../types/model.ts`)
|
|
15
|
+
`generate(model, context, options)` returns a `ProviderGenerateResult`; `stream(...)` returns an `AssistantMessageEventStream` that ends with an `AgentResponse`; optional `isLongRunning()` tells the loop to skip its whole-call timeouts.
|
|
16
|
+
|
|
17
|
+
## Invariants
|
|
18
|
+
- Fix shared streaming behaviour in `shared.ts`, never per adapter. Event parsing and request building stay in each adapter.
|
|
19
|
+
- OpenRouter and compat chat chunk handlers are deliberately not merged.
|
|
20
|
+
- A custom prefix never gets another provider's key or base URL.
|
|
21
|
+
- A transient failure is retried only before the first byte of a stream.
|
|
22
|
+
- A stream that ends without a terminal event while a tool call is missing or cut fails as a retryable `incomplete_stream`.
|
|
23
|
+
- Adapters cancel a reader with `reader.cancel().catch(() => {})`, never a bare `void reader.cancel()`.
|
|
24
|
+
- To add a provider: implement `Provider` on native `fetch`, reuse `shared.ts`, add pure mappers to `../providers.ts`, register in `registry.ts`.
|
package/src/providers/google.ts
CHANGED
|
@@ -264,10 +264,12 @@ const PAID_CHAIN_TTL_MS = 54 * 24 * 3600 * 1000;
|
|
|
264
264
|
/** Last-turn total tokens per session for the rehydration invariant (Rule 3). */
|
|
265
265
|
const lastTurnTotals = new Map<string, number>();
|
|
266
266
|
|
|
267
|
+
// Storage tier for the chain TTL (`paid` only when set, else the safe `free`); in: ProviderRequestOptions.googleTier, out: chainTtlMs.
|
|
267
268
|
function resolveInteractionTier(options?: ProviderRequestOptions): "free" | "paid" {
|
|
268
269
|
return options?.googleTier === "paid" ? "paid" : "free";
|
|
269
270
|
}
|
|
270
271
|
|
|
272
|
+
// How long a chained interaction pointer is trusted locally (23 h free, 54 d paid); in: the tier, out: liveChainFor.
|
|
271
273
|
function chainTtlMs(options?: ProviderRequestOptions): number {
|
|
272
274
|
return resolveInteractionTier(options) === "paid" ? PAID_CHAIN_TTL_MS : FREE_CHAIN_TTL_MS;
|
|
273
275
|
}
|
|
@@ -280,11 +282,13 @@ function reportChainAnomaly(detail: { type: string; [k: string]: unknown }): voi
|
|
|
280
282
|
} catch {}
|
|
281
283
|
}
|
|
282
284
|
|
|
285
|
+
// The saved interaction pointer of a session, if any; in: liveChainFor, out: a ChainEntry or undefined.
|
|
283
286
|
function chainFor(sessionId: string | undefined): ChainEntry | undefined {
|
|
284
287
|
if (!sessionId) return undefined;
|
|
285
288
|
return interactionChains.get(sessionId);
|
|
286
289
|
}
|
|
287
290
|
|
|
291
|
+
// Saves the pointer of the interaction a turn just created; in: generate/stream, out: interactionChains.
|
|
288
292
|
function setChain(sessionId: string | undefined, entry: ChainEntry): void {
|
|
289
293
|
if (!sessionId) return;
|
|
290
294
|
interactionChains.set(sessionId, entry);
|
|
@@ -347,6 +351,7 @@ export function clearInteractionChains(sessionId?: string): void {
|
|
|
347
351
|
// Request building (canonical -> Interactions)
|
|
348
352
|
// ---------------------------------------------------------------------------
|
|
349
353
|
|
|
354
|
+
// Endpoint from the option, GOOGLE_BASE_URL, else the v1beta default; in: each request, out: the base URL without a trailing slash.
|
|
350
355
|
function resolveBaseUrl(options?: ProviderRequestOptions): string {
|
|
351
356
|
return (
|
|
352
357
|
options?.baseUrl ||
|
|
@@ -356,6 +361,7 @@ function resolveBaseUrl(options?: ProviderRequestOptions): string {
|
|
|
356
361
|
).replace(/\/+$/, "");
|
|
357
362
|
}
|
|
358
363
|
|
|
364
|
+
// API key from the option or env, or `unused` when custom headers carry the auth; in: each request, out: the key or undefined.
|
|
359
365
|
function resolveApiKey(options?: ProviderRequestOptions): string | undefined {
|
|
360
366
|
const key = getApiKey("google", options?.apiKey, options?.env);
|
|
361
367
|
if (key) return key;
|
|
@@ -363,6 +369,7 @@ function resolveApiKey(options?: ProviderRequestOptions): string | undefined {
|
|
|
363
369
|
return undefined;
|
|
364
370
|
}
|
|
365
371
|
|
|
372
|
+
// Strips a google/, gemini/ or models/ prefix; in: generate/stream/getModel, out: the bare model id.
|
|
366
373
|
function cleanModelId(model: string | ModelSpec): string {
|
|
367
374
|
const rawId = typeof model === "string" ? model : model.id;
|
|
368
375
|
return rawId.replace(/^(google\/|gemini\/|models\/)/i, "");
|
|
@@ -395,6 +402,7 @@ function throwIfFailedInteraction(interaction: InteractionObject, modelId: strin
|
|
|
395
402
|
throw toConciseProviderError(failure, "google", modelId);
|
|
396
403
|
}
|
|
397
404
|
|
|
405
|
+
// Canonical tool declarations as Interactions function tools with a Google-safe schema; in: options.tools, out: request `tools`.
|
|
398
406
|
function toGoogleTools(tools?: StandardToolDeclaration[]): Array<Record<string, unknown>> | undefined {
|
|
399
407
|
if (!tools || tools.length === 0) return undefined;
|
|
400
408
|
return tools.map((t) => ({
|
|
@@ -407,6 +415,7 @@ function toGoogleTools(tools?: StandardToolDeclaration[]): Array<Record<string,
|
|
|
407
415
|
}));
|
|
408
416
|
}
|
|
409
417
|
|
|
418
|
+
// Canonical tool choice as `generation_config.tool_choice.allowed_tools`; in: options.toolChoice + tool names, out: the config value.
|
|
410
419
|
function toGoogleToolChoice(
|
|
411
420
|
choice: ProviderRequestOptions["toolChoice"],
|
|
412
421
|
toolNames: string[]
|
|
@@ -500,6 +509,7 @@ function blockText(blocks: unknown): string {
|
|
|
500
509
|
return blocks.map((b) => (typeof b?.text === "string" ? b.text : "")).join("");
|
|
501
510
|
}
|
|
502
511
|
|
|
512
|
+
// A tool result as the text of a function_result block; in: functionResultStep, out: a string.
|
|
503
513
|
function resultToText(result: unknown): string {
|
|
504
514
|
if (typeof result === "string") return result;
|
|
505
515
|
return safeStringify(result);
|
|
@@ -599,7 +609,7 @@ async function fullHistorySteps(context: ProviderContext): Promise<InteractionIn
|
|
|
599
609
|
const turnStart = steps.length;
|
|
600
610
|
for (const part of m.content) {
|
|
601
611
|
if (part.type === "thinking" && part.thinking) {
|
|
602
|
-
//
|
|
612
|
+
// Preserve namespaced signatures only — foreign-provider
|
|
603
613
|
// signatures never echo here; untagged legacy echoes (compat).
|
|
604
614
|
// Absent signatures are omitted (never sent as empty strings).
|
|
605
615
|
const partProvider = (part as { thoughtSignatureProvider?: unknown }).thoughtSignatureProvider;
|
|
@@ -632,7 +642,7 @@ async function fullHistorySteps(context: ProviderContext): Promise<InteractionIn
|
|
|
632
642
|
} else if (m.thoughtSignature && !steps.slice(turnStart).some((s) => s.type === "thought")) {
|
|
633
643
|
// Preserve a signature-only thought so stateless chaining keeps
|
|
634
644
|
// reasoning continuity even when no summary text was retained.
|
|
635
|
-
//
|
|
645
|
+
// Only for google/untagged signatures.
|
|
636
646
|
const msgProvider = (m as { thoughtSignatureProvider?: unknown }).thoughtSignatureProvider;
|
|
637
647
|
if (shouldEchoSignature(msgProvider, "google")) {
|
|
638
648
|
steps.push({ type: "thought", signature: m.thoughtSignature });
|
|
@@ -654,6 +664,7 @@ async function fullHistorySteps(context: ProviderContext): Promise<InteractionIn
|
|
|
654
664
|
// Response mapping (Interactions -> canonical)
|
|
655
665
|
// ---------------------------------------------------------------------------
|
|
656
666
|
|
|
667
|
+
// Google usage as TokenUsage, folding thought tokens into output and clamping cache reads to input; in: an interaction or step.stop usage, out: TokenUsage.
|
|
657
668
|
function mapUsage(raw?: InteractionObject["usage"]): TokenUsage {
|
|
658
669
|
const input = raw?.total_input_tokens ?? 0;
|
|
659
670
|
// Google reports thought tokens separately from `total_output_tokens`, but bills them as output
|
|
@@ -677,6 +688,7 @@ function mapUsage(raw?: InteractionObject["usage"]): TokenUsage {
|
|
|
677
688
|
};
|
|
678
689
|
}
|
|
679
690
|
|
|
691
|
+
// Interaction status as a finish reason (requires_action = tool_calls, incomplete = length, failed = error); in: parseInteraction/stream, out: finishReason.
|
|
680
692
|
function mapFinishReason(status?: string): string {
|
|
681
693
|
if (status === "requires_action") return "tool_calls";
|
|
682
694
|
if (status === "incomplete") return "length";
|
|
@@ -684,6 +696,7 @@ function mapFinishReason(status?: string): string {
|
|
|
684
696
|
return "stop";
|
|
685
697
|
}
|
|
686
698
|
|
|
699
|
+
// Turns a finished Interaction into text, thinking, tool calls, usage and replayable steps, throwing on failed or cancelled; in: generate, out: ProviderGenerateResult.
|
|
687
700
|
function parseInteraction(
|
|
688
701
|
interaction: InteractionObject,
|
|
689
702
|
modelId: string,
|
|
@@ -767,6 +780,7 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
767
780
|
return getModelsForProvider("google");
|
|
768
781
|
}
|
|
769
782
|
|
|
783
|
+
// The catalog spec of a model, else a permissive generic one; in: the registry, out: a ModelSpec.
|
|
770
784
|
getModel(modelId: string): ModelSpec | undefined {
|
|
771
785
|
const clean = cleanModelId(modelId);
|
|
772
786
|
return (
|
|
@@ -777,6 +791,7 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
777
791
|
);
|
|
778
792
|
}
|
|
779
793
|
|
|
794
|
+
// Builds the Interactions request: generation config, tools, structured output and the input (latest turn on a live chain, full history on a fresh or switched one); in: generate/stream, out: the request body.
|
|
780
795
|
private buildBody(
|
|
781
796
|
modelId: string,
|
|
782
797
|
context: ProviderContext,
|
|
@@ -886,6 +901,7 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
886
901
|
})();
|
|
887
902
|
}
|
|
888
903
|
|
|
904
|
+
// Fetch options with JSON headers and the x-goog-api-key header; in: doFetch and stream, out: RequestInit.
|
|
889
905
|
private requestInit(
|
|
890
906
|
method: "POST" | "GET",
|
|
891
907
|
body: InteractionRequestBody | undefined,
|
|
@@ -894,7 +910,7 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
894
910
|
signal?: AbortSignal
|
|
895
911
|
): RequestInit {
|
|
896
912
|
const headers: Record<string, string> = { "Content-Type": "application/json", ...options?.headers };
|
|
897
|
-
//
|
|
913
|
+
// Key travels as the `x-goog-api-key` header (never `?key=` in the
|
|
898
914
|
// URL, which would leak into logs). Case-insensitive merge so a custom
|
|
899
915
|
// `X-Goog-Api-Key` never duplicates the canonical header. Proxy flows
|
|
900
916
|
// (`apiKey === "unused"`) preserve custom auth instead of overwriting.
|
|
@@ -902,6 +918,7 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
902
918
|
return { method, headers, ...(body ? { body: JSON.stringify(body) } : {}), signal };
|
|
903
919
|
}
|
|
904
920
|
|
|
921
|
+
// One HTTP call under the request timeout, returning status, headers and body text; in: fetchInteraction and cancelInteraction, out: the raw reply.
|
|
905
922
|
private async doFetch(
|
|
906
923
|
url: string,
|
|
907
924
|
method: "POST" | "GET",
|
|
@@ -924,6 +941,7 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
924
941
|
}
|
|
925
942
|
}
|
|
926
943
|
|
|
944
|
+
// Throws a concise provider error (status, code, Retry-After, key hint) for a non-2xx reply; in: every Google HTTP call, out: nothing or a thrown error.
|
|
927
945
|
private throwIfError(
|
|
928
946
|
status: number,
|
|
929
947
|
statusText: string,
|
|
@@ -992,6 +1010,7 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
992
1010
|
void this.doFetch(`${baseUrl}/interactions/${id}/cancel`, "POST", undefined, options, apiKey).catch(() => {});
|
|
993
1011
|
}
|
|
994
1012
|
|
|
1013
|
+
// Non-streaming turn: creates the interaction (polling it when it runs in the background), records the chain and self-heals once on a lost chain; in: runAgentLoop, out: ProviderGenerateResult.
|
|
995
1014
|
async generate(
|
|
996
1015
|
model: string | ModelSpec,
|
|
997
1016
|
context: ProviderContext,
|
|
@@ -1090,6 +1109,7 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
1090
1109
|
}
|
|
1091
1110
|
}
|
|
1092
1111
|
|
|
1112
|
+
// Streaming turn over SSE: assembles text, thinking, calls and exact replay steps, reconnects a dropped background stream and fails clean on a lost chain; in: streamAgentLoop, out: an AssistantMessageEventStream.
|
|
1093
1113
|
stream(
|
|
1094
1114
|
model: string | ModelSpec,
|
|
1095
1115
|
context: ProviderContext,
|
|
@@ -1372,7 +1392,7 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
1372
1392
|
noteProviderTurn(sessionId, "google");
|
|
1373
1393
|
checkTurnTokens(sessionId, usage);
|
|
1374
1394
|
|
|
1375
|
-
//
|
|
1395
|
+
// Terminal-state guard — fail `incomplete_stream` (retryable)
|
|
1376
1396
|
// instead of resolving empty success. Skipped for non-SSE payloads
|
|
1377
1397
|
// (JSON mocks) where no SSE frames were ever seen.
|
|
1378
1398
|
const hasTerminal = completedBody !== undefined || sawFinishReason || sawUsage;
|