agent-accelerator 0.5.6 → 0.5.7

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.
Files changed (45) hide show
  1. package/README.md +6 -13
  2. package/package.json +1 -1
  3. package/src/agent/agent.ts +16 -18
  4. package/src/agent/architecture.md +25 -0
  5. package/src/agent/context.ts +4 -0
  6. package/src/agent/delegation.ts +18 -84
  7. package/src/agent/loop.ts +13 -15
  8. package/src/agent/worker.ts +1 -3
  9. package/src/architecture.md +28 -0
  10. package/src/index.ts +6 -73
  11. package/src/models/architecture.md +14 -0
  12. package/src/models/catalog-cache.ts +3 -0
  13. package/src/models/catalog.ts +7 -0
  14. package/src/providers/architecture.md +24 -0
  15. package/src/providers/google.ts +20 -0
  16. package/src/providers/openai-compat.ts +22 -0
  17. package/src/providers/openai.ts +17 -1
  18. package/src/providers/openrouter.ts +12 -0
  19. package/src/providers/registry.ts +1 -0
  20. package/src/providers.ts +9 -53
  21. package/src/scheduler/architecture.md +19 -0
  22. package/src/scheduler/scheduler.ts +21 -0
  23. package/src/scheduler/store.ts +6 -0
  24. package/src/scheduler/time.ts +2 -0
  25. package/src/session/architecture.md +19 -0
  26. package/src/session/store.ts +15 -0
  27. package/src/session/usage.ts +1 -0
  28. package/src/streaming/architecture.md +13 -0
  29. package/src/streaming/event-stream.ts +5 -0
  30. package/src/telemetry.ts +1 -0
  31. package/src/tools/architecture.md +19 -0
  32. package/src/tools/executor.ts +13 -0
  33. package/src/tools/schema.ts +1 -0
  34. package/src/tools/subagent-result.ts +1 -0
  35. package/src/types/architecture.md +20 -0
  36. package/src/utils/architecture.md +25 -0
  37. package/src/utils/base64.ts +2 -0
  38. package/src/utils/cache.ts +2 -0
  39. package/src/utils/errors.ts +4 -0
  40. package/src/utils/headers.ts +4 -1
  41. package/src/utils/media.ts +1 -0
  42. package/src/utils/raw-capture.ts +3 -0
  43. package/src/utils/retry.ts +1 -0
  44. package/src/utils/safe-io.ts +4 -0
  45. package/src/utils/session.ts +5 -161
@@ -0,0 +1,28 @@
1
+ # src/
2
+
3
+ The whole SDK. `index.ts` is the only public entry (`main`/`module`/`types`); everything else is reached through it.
4
+
5
+ | File / folder | What it is |
6
+ | :-- | :-- |
7
+ | `index.ts` | The public barrel. Adding or removing an export is a public API change. |
8
+ | `telemetry.ts` | The in-process event bus (`telemetryBus`). Imports nothing from the SDK, so any module may use it. |
9
+ | `telemetry-file.ts` | Opt-in NDJSON sink for the bus (`<home>/telemetry/<date>.ndjson`). |
10
+ | `update-models.ts` | CLI behind `bun run update-models` (refreshes the model catalog cache). |
11
+ | `providers.ts` | Provider-agnostic mappers (thinking, cache, tool choice, structured output, generation params). No HTTP. |
12
+ | `agent/` | `Agent`, the model-to-tools loop, workers, delegation. |
13
+ | `providers/` | One adapter per wire (Google, OpenAI, OpenRouter, OpenAI-compatible), the registry, shared helpers. |
14
+ | `models/` | The model catalog (models.dev) and its cache. |
15
+ | `tools/` | `tool()`, schemas, the executor. |
16
+ | `streaming/` | The event stream and the SSE parser. |
17
+ | `session/` | Persistence (snapshot + journal), usage accounting, tool logs. |
18
+ | `scheduler/` | Scheduled actions. |
19
+ | `types/` | Shared contracts (no logic except small helpers). |
20
+ | `utils/` | Small shared helpers (durable writes, home, env, media, retry, safe I/O). |
21
+
22
+ ## Rules for every file here
23
+ - Imports use `.ts` extensions; type-only imports use `import type`.
24
+ - No `console.*`: warnings and traces go to `telemetryBus` (exceptions: the `update-models.ts` CLI and one compat `console.warn` in `providers.ts`).
25
+ - All state is written under the one home folder (`utils/home.ts`). Files are replaced through `utils/durable.ts`; the append-only ones are the session journal and tool logs (fsynced per line) and the opt-in telemetry file (not fsynced).
26
+ - Provider differences never leave `providers/` and `providers.ts`.
27
+ - Every function has one comment line above it (how it works, where input comes from, where output goes).
28
+ - Change a public option, export, env var or telemetry code: update `README.md` and `docs/` in the same change.
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
- // Q-50 public API budget: ~25 core runtime exports + compat/deprecation shims.
5
- // Core = Agent, tool, SubAgent (deprecated alias), AgentResponse,
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;
@@ -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, StreamingLoopHandle } from "./agent/delegation.ts";
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,29 +126,12 @@ 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
@@ -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,11 +199,6 @@ 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
@@ -325,8 +258,8 @@ export type { SessionLimits, SessionPayloadStatus, PersistedAgentSession, Sessio
325
258
  * @deprecated Session-routing internals. Prefer agent-level `sessionId`;
326
259
  * direct use moves to the future `/internal` entry (Q-50).
327
260
  */
328
- export { createSessionId, hashSessionPart, createTrackingId, nowMs, newRunId, newTurnId, previewPrompt, parseModelRef, resolveAgentConfig } from "./utils/session.ts";
329
- export type { ParsedModelRef, ResolvedAgentConfig } from "./utils/session.ts";
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
@@ -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.
@@ -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 {
@@ -93,6 +95,7 @@ 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;
@@ -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("/");
@@ -93,6 +95,7 @@ function setLookupCache(key: string, value: ModelSpec | undefined): void {
93
95
  // Custom-registered models (Q-33): 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
  }
@@ -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();
@@ -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`.
@@ -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);
@@ -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,
@@ -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,
@@ -192,10 +192,12 @@ interface ChatResponse {
192
192
  // Request building (canonical -> Chat Completions)
193
193
  // ---------------------------------------------------------------------------
194
194
 
195
+ // Env-variable prefix of a custom provider (`my-llm` becomes `MY_LLM`); in: key and URL lookups, out: the prefix.
195
196
  function envPrefixOf(prefix: string): string {
196
197
  return prefix.toUpperCase().replace(/[^A-Z0-9]/g, "_");
197
198
  }
198
199
 
200
+ // Endpoint from the option, the provider's configuration or {PREFIX}_BASE_URL; throws ConfigError when none (never another vendor's URL); in: each request, out: the base URL.
199
201
  function resolveBaseUrl(
200
202
  prefix: string,
201
203
  configuredBaseUrl: string | undefined,
@@ -221,6 +223,7 @@ function resolveBaseUrl(
221
223
  return resolved.replace(/\/+$/, "");
222
224
  }
223
225
 
226
+ // Key from the option, the provider's configuration or {PREFIX}_API_KEY, or `unused` when custom headers carry the auth; in: each request, out: the key or undefined.
224
227
  function resolveApiKey(
225
228
  prefix: string,
226
229
  configuredApiKey: string | undefined,
@@ -282,6 +285,7 @@ function cleanModelId(prefix: string, model: string | ModelSpec): string {
282
285
  return rawId.replace(new RegExp(`^${escaped}/`, "i"), "");
283
286
  }
284
287
 
288
+ // Canonical tool declarations as Chat Completions function tools; in: options.tools, out: request `tools`.
285
289
  function toCompatTools(tools?: StandardToolDeclaration[]): Array<Record<string, unknown>> | undefined {
286
290
  if (!tools || tools.length === 0) return undefined;
287
291
  return tools.map((t) => ({
@@ -309,6 +313,7 @@ function warnThinkingDropped(prefix: string, thinking: ThinkingConfig | undefine
309
313
  });
310
314
  }
311
315
 
316
+ // `wav` or `mp3`, the only formats Chat audio input takes here; in: contentPartsToBlocks, out: input_audio.format.
312
317
  function audioFormatFor(mimeType?: string): string {
313
318
  const mime = (mimeType || "").toLowerCase();
314
319
  if (mime.includes("wav")) return "wav";
@@ -354,6 +359,7 @@ function assertNoVideoOrFileParts(
354
359
  throw err;
355
360
  }
356
361
 
362
+ // Text, image (link or data URL) and audio parts as Chat content blocks; in: a user message's parts, out: fullHistoryMessages.
357
363
  async function contentPartsToBlocks(parts: ContentPart[]): Promise<ChatContentPart[]> {
358
364
  const blocks: ChatContentPart[] = [];
359
365
  for (const part of parts) {
@@ -388,6 +394,7 @@ async function contentPartsToBlocks(parts: ContentPart[]): Promise<ChatContentPa
388
394
  return blocks;
389
395
  }
390
396
 
397
+ // A tool result as the text of a tool message; in: fullHistoryMessages, out: a string.
391
398
  function resultToOutput(result: unknown): string {
392
399
  if (typeof result === "string") return result;
393
400
  return safeStringify(result);
@@ -502,6 +509,7 @@ async function fullHistoryMessages(context: ProviderContext): Promise<ChatMessag
502
509
  // Response mapping (Chat Completions -> canonical)
503
510
  // ---------------------------------------------------------------------------
504
511
 
512
+ // Chat usage as TokenUsage, clamping cache reads to input; in: a reply or the last stream chunk, out: TokenUsage.
505
513
  function mapUsage(raw?: ChatResponse["usage"]): TokenUsage {
506
514
  const input = raw?.prompt_tokens ?? 0;
507
515
  const output = raw?.completion_tokens ?? 0;
@@ -517,12 +525,14 @@ function mapUsage(raw?: ChatResponse["usage"]): TokenUsage {
517
525
  cacheWriteTokens: raw?.prompt_tokens_details?.cache_write_tokens ?? 0,
518
526
  thinkingTokens: raw?.completion_tokens_details?.reasoning_tokens ?? 0,
519
527
  };
528
+ // `cost` is an OpenRouter extension. A generic endpoint that sends it may use 0 for "not computed", so only a positive value counts as known (OpenRouter's adapter accepts an explicit 0).
520
529
  if (typeof raw?.cost === "number" && raw.cost > 0) {
521
530
  usage.cost = { totalCost: raw.cost };
522
531
  }
523
532
  return usage;
524
533
  }
525
534
 
535
+ // The Gemini thought signature a proxy put on a tool call, if any; in: parseResponse, out: a string or undefined.
526
536
  function extractSignature(rawTc: {
527
537
  extra_content?: { google?: { thought_signature?: string } };
528
538
  }): string | undefined {
@@ -536,6 +546,7 @@ function withCompatSignature(sig: string | undefined): { thoughtSignature?: stri
536
546
  return { thoughtSignature: sig, thoughtSignatureProvider: "google" };
537
547
  }
538
548
 
549
+ // Throws a concise error for an HTTP 200 reply that carries only `error`; in: parseResponse, out: a thrown error.
539
550
  function throwResponseError(
540
551
  response: ChatResponse,
541
552
  prefix: string,
@@ -550,6 +561,7 @@ function throwResponseError(
550
561
  throw toConciseProviderError(failure, prefix, modelId);
551
562
  }
552
563
 
564
+ // Turns a Chat Completions JSON reply into text, thinking, tool calls and usage; in: generate, out: ProviderGenerateResult.
553
565
  function parseResponse(
554
566
  response: ChatResponse,
555
567
  prefix: string,
@@ -664,6 +676,7 @@ export class OpenAICompatibleChatProvider implements Provider {
664
676
  this.configuredApiKey = opts?.apiKey;
665
677
  }
666
678
 
679
+ // The catalog spec of a model, else a permissive generic one; in: the registry, out: a ModelSpec.
667
680
  getModel(modelId: string): ModelSpec | undefined {
668
681
  const clean = cleanModelId(this.prefix, modelId);
669
682
  return (
@@ -674,14 +687,17 @@ export class OpenAICompatibleChatProvider implements Provider {
674
687
  );
675
688
  }
676
689
 
690
+ // This provider's endpoint for one request; in: generate/stream, out: resolveBaseUrl.
677
691
  private baseUrl(options?: ProviderRequestOptions): string {
678
692
  return resolveBaseUrl(this.prefix, this.configuredBaseUrl, options);
679
693
  }
680
694
 
695
+ // This provider's key for one request; in: requireApiKey, out: resolveApiKey.
681
696
  private apiKey(options?: ProviderRequestOptions): string | undefined {
682
697
  return resolveApiKey(this.prefix, this.configuredApiKey, options);
683
698
  }
684
699
 
700
+ // The key, where a missing one only throws for non-local endpoints (loopback and LAN may omit it); in: generate/stream, out: the key or undefined.
685
701
  private requireApiKey(baseUrl: string, options?: ProviderRequestOptions): string | undefined {
686
702
  const key = this.apiKey(options);
687
703
  if (!key && !isLocalEndpoint(baseUrl)) {
@@ -692,6 +708,7 @@ export class OpenAICompatibleChatProvider implements Provider {
692
708
  return key;
693
709
  }
694
710
 
711
+ // Builds the Chat request (messages, tools, tool_choice, generation controls, response_format, stream usage) and warns+drops what a strict endpoint cannot take; in: generate/stream, out: the body.
695
712
  private async buildBody(
696
713
  modelId: string,
697
714
  context: ProviderContext,
@@ -788,6 +805,7 @@ export class OpenAICompatibleChatProvider implements Provider {
788
805
  return body;
789
806
  }
790
807
 
808
+ // Fetch options with session-affinity headers (unless opted out), JSON content type and the Bearer key; in: doFetch and stream, out: RequestInit.
791
809
  private requestInit(
792
810
  body: ChatRequestBody,
793
811
  options: ProviderRequestOptions | undefined,
@@ -823,6 +841,7 @@ export class OpenAICompatibleChatProvider implements Provider {
823
841
  return { method: "POST", headers, body: JSON.stringify(body), signal };
824
842
  }
825
843
 
844
+ // One HTTP call under the request timeout, returning status, headers and body text; in: generate, out: the raw reply.
826
845
  private async doFetch(
827
846
  url: string,
828
847
  body: ChatRequestBody,
@@ -845,6 +864,7 @@ export class OpenAICompatibleChatProvider implements Provider {
845
864
  }
846
865
  }
847
866
 
867
+ // Throws a concise provider error (status, code, Retry-After, key hint) for a non-2xx reply; in: generate and stream, out: nothing or a thrown error.
848
868
  private throwIfError(
849
869
  status: number,
850
870
  url: string,
@@ -873,6 +893,7 @@ export class OpenAICompatibleChatProvider implements Provider {
873
893
  throw toConciseProviderError(err, this.prefix, modelId);
874
894
  }
875
895
 
896
+ // Non-streaming turn as one retried JSON request; in: runAgentLoop, out: ProviderGenerateResult.
876
897
  async generate(
877
898
  model: string | ModelSpec,
878
899
  context: ProviderContext,
@@ -945,6 +966,7 @@ export class OpenAICompatibleChatProvider implements Provider {
945
966
  }
946
967
  }
947
968
 
969
+ // Streaming turn over SSE: assembles text, reasoning, tool calls and usage, retrying only before the first byte; in: streamAgentLoop, out: an AssistantMessageEventStream.
948
970
  stream(
949
971
  model: string | ModelSpec,
950
972
  context: ProviderContext,