agent-accelerator 0.5.5 → 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.
- package/README.md +16 -17
- package/package.json +1 -1
- package/src/agent/agent.ts +16 -18
- package/src/agent/architecture.md +25 -0
- package/src/agent/context.ts +4 -0
- package/src/agent/delegation.ts +18 -84
- package/src/agent/loop.ts +14 -16
- package/src/agent/worker.ts +1 -3
- package/src/architecture.md +28 -0
- package/src/index.ts +6 -73
- package/src/models/architecture.md +14 -0
- package/src/models/catalog-cache.ts +3 -0
- package/src/models/catalog.ts +7 -0
- package/src/providers/architecture.md +24 -0
- package/src/providers/google.ts +26 -39
- package/src/providers/openai-compat.ts +53 -104
- package/src/providers/openai.ts +47 -104
- package/src/providers/openrouter.ts +114 -175
- package/src/providers/registry.ts +1 -0
- package/src/providers/shared.ts +134 -5
- package/src/providers.ts +30 -59
- package/src/scheduler/architecture.md +19 -0
- package/src/scheduler/scheduler.ts +21 -0
- package/src/scheduler/store.ts +6 -0
- package/src/scheduler/time.ts +2 -0
- package/src/session/architecture.md +19 -0
- package/src/session/store.ts +18 -3
- package/src/session/usage.ts +1 -0
- package/src/streaming/architecture.md +13 -0
- package/src/streaming/event-stream.ts +5 -0
- package/src/telemetry.ts +1 -0
- package/src/tools/architecture.md +19 -0
- package/src/tools/executor.ts +13 -0
- package/src/tools/schema.ts +1 -0
- package/src/tools/subagent-result.ts +1 -0
- package/src/types/architecture.md +20 -0
- package/src/types/message.ts +1 -1
- package/src/utils/architecture.md +25 -0
- package/src/utils/base64.ts +2 -0
- package/src/utils/cache.ts +2 -0
- package/src/utils/errors.ts +8 -1
- package/src/utils/headers.ts +4 -1
- package/src/utils/media.ts +11 -1
- package/src/utils/raw-capture.ts +3 -0
- package/src/utils/retry.ts +1 -0
- package/src/utils/safe-io.ts +4 -0
- package/src/utils/session.ts +5 -161
package/src/agent/worker.ts
CHANGED
|
@@ -3,12 +3,9 @@ import type { SubAgentStep } from "../types/response.ts";
|
|
|
3
3
|
import type { AgentConfig } from "../types/agent.ts";
|
|
4
4
|
import type { Agent } from "./agent.ts";
|
|
5
5
|
import { normalizeToolName } from "../tools/executor.ts";
|
|
6
|
-
import { escapeXml } from "../utils/serialization.ts";
|
|
7
6
|
import { getModel, getSubModel } from "../utils/env.ts";
|
|
8
7
|
import { resolveModel } from "../providers/registry.ts";
|
|
9
8
|
|
|
10
|
-
export { escapeXml };
|
|
11
|
-
|
|
12
9
|
/** Minimal live-trace shape shared by fixed + dynamic workers (structural subset of `SubAgentTrace`). */
|
|
13
10
|
export interface LiveWorkerTrace {
|
|
14
11
|
trackingId: string;
|
|
@@ -200,6 +197,7 @@ export function grantWorkerTools(
|
|
|
200
197
|
return grantedTools;
|
|
201
198
|
}
|
|
202
199
|
|
|
200
|
+
// The provider id a model string resolves to, or undefined when it cannot be resolved; in: resolveWorkerCredentials, out: the provider match check.
|
|
203
201
|
function providerOf(modelStr: string | unknown | undefined): string | undefined {
|
|
204
202
|
if (!modelStr) return undefined;
|
|
205
203
|
try {
|
|
@@ -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
|
-
//
|
|
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;
|
|
@@ -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,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
|
|
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
|
|
@@ -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;
|
package/src/models/catalog.ts
CHANGED
|
@@ -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`.
|
package/src/providers/google.ts
CHANGED
|
@@ -58,6 +58,8 @@ import {
|
|
|
58
58
|
retryOptionsFrom,
|
|
59
59
|
headersToRecord,
|
|
60
60
|
parseRetryAfterMs,
|
|
61
|
+
linkStreamAbort,
|
|
62
|
+
withAbortTimer,
|
|
61
63
|
} from "./shared.ts";
|
|
62
64
|
import { telemetryBus } from "../telemetry.ts";
|
|
63
65
|
import { createGenericModelSpec } from "../models/catalog.ts";
|
|
@@ -262,10 +264,12 @@ const PAID_CHAIN_TTL_MS = 54 * 24 * 3600 * 1000;
|
|
|
262
264
|
/** Last-turn total tokens per session for the rehydration invariant (Rule 3). */
|
|
263
265
|
const lastTurnTotals = new Map<string, number>();
|
|
264
266
|
|
|
267
|
+
// Storage tier for the chain TTL (`paid` only when set, else the safe `free`); in: ProviderRequestOptions.googleTier, out: chainTtlMs.
|
|
265
268
|
function resolveInteractionTier(options?: ProviderRequestOptions): "free" | "paid" {
|
|
266
269
|
return options?.googleTier === "paid" ? "paid" : "free";
|
|
267
270
|
}
|
|
268
271
|
|
|
272
|
+
// How long a chained interaction pointer is trusted locally (23 h free, 54 d paid); in: the tier, out: liveChainFor.
|
|
269
273
|
function chainTtlMs(options?: ProviderRequestOptions): number {
|
|
270
274
|
return resolveInteractionTier(options) === "paid" ? PAID_CHAIN_TTL_MS : FREE_CHAIN_TTL_MS;
|
|
271
275
|
}
|
|
@@ -278,11 +282,13 @@ function reportChainAnomaly(detail: { type: string; [k: string]: unknown }): voi
|
|
|
278
282
|
} catch {}
|
|
279
283
|
}
|
|
280
284
|
|
|
285
|
+
// The saved interaction pointer of a session, if any; in: liveChainFor, out: a ChainEntry or undefined.
|
|
281
286
|
function chainFor(sessionId: string | undefined): ChainEntry | undefined {
|
|
282
287
|
if (!sessionId) return undefined;
|
|
283
288
|
return interactionChains.get(sessionId);
|
|
284
289
|
}
|
|
285
290
|
|
|
291
|
+
// Saves the pointer of the interaction a turn just created; in: generate/stream, out: interactionChains.
|
|
286
292
|
function setChain(sessionId: string | undefined, entry: ChainEntry): void {
|
|
287
293
|
if (!sessionId) return;
|
|
288
294
|
interactionChains.set(sessionId, entry);
|
|
@@ -345,6 +351,7 @@ export function clearInteractionChains(sessionId?: string): void {
|
|
|
345
351
|
// Request building (canonical -> Interactions)
|
|
346
352
|
// ---------------------------------------------------------------------------
|
|
347
353
|
|
|
354
|
+
// Endpoint from the option, GOOGLE_BASE_URL, else the v1beta default; in: each request, out: the base URL without a trailing slash.
|
|
348
355
|
function resolveBaseUrl(options?: ProviderRequestOptions): string {
|
|
349
356
|
return (
|
|
350
357
|
options?.baseUrl ||
|
|
@@ -354,6 +361,7 @@ function resolveBaseUrl(options?: ProviderRequestOptions): string {
|
|
|
354
361
|
).replace(/\/+$/, "");
|
|
355
362
|
}
|
|
356
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.
|
|
357
365
|
function resolveApiKey(options?: ProviderRequestOptions): string | undefined {
|
|
358
366
|
const key = getApiKey("google", options?.apiKey, options?.env);
|
|
359
367
|
if (key) return key;
|
|
@@ -361,6 +369,7 @@ function resolveApiKey(options?: ProviderRequestOptions): string | undefined {
|
|
|
361
369
|
return undefined;
|
|
362
370
|
}
|
|
363
371
|
|
|
372
|
+
// Strips a google/, gemini/ or models/ prefix; in: generate/stream/getModel, out: the bare model id.
|
|
364
373
|
function cleanModelId(model: string | ModelSpec): string {
|
|
365
374
|
const rawId = typeof model === "string" ? model : model.id;
|
|
366
375
|
return rawId.replace(/^(google\/|gemini\/|models\/)/i, "");
|
|
@@ -393,6 +402,7 @@ function throwIfFailedInteraction(interaction: InteractionObject, modelId: strin
|
|
|
393
402
|
throw toConciseProviderError(failure, "google", modelId);
|
|
394
403
|
}
|
|
395
404
|
|
|
405
|
+
// Canonical tool declarations as Interactions function tools with a Google-safe schema; in: options.tools, out: request `tools`.
|
|
396
406
|
function toGoogleTools(tools?: StandardToolDeclaration[]): Array<Record<string, unknown>> | undefined {
|
|
397
407
|
if (!tools || tools.length === 0) return undefined;
|
|
398
408
|
return tools.map((t) => ({
|
|
@@ -405,6 +415,7 @@ function toGoogleTools(tools?: StandardToolDeclaration[]): Array<Record<string,
|
|
|
405
415
|
}));
|
|
406
416
|
}
|
|
407
417
|
|
|
418
|
+
// Canonical tool choice as `generation_config.tool_choice.allowed_tools`; in: options.toolChoice + tool names, out: the config value.
|
|
408
419
|
function toGoogleToolChoice(
|
|
409
420
|
choice: ProviderRequestOptions["toolChoice"],
|
|
410
421
|
toolNames: string[]
|
|
@@ -498,6 +509,7 @@ function blockText(blocks: unknown): string {
|
|
|
498
509
|
return blocks.map((b) => (typeof b?.text === "string" ? b.text : "")).join("");
|
|
499
510
|
}
|
|
500
511
|
|
|
512
|
+
// A tool result as the text of a function_result block; in: functionResultStep, out: a string.
|
|
501
513
|
function resultToText(result: unknown): string {
|
|
502
514
|
if (typeof result === "string") return result;
|
|
503
515
|
return safeStringify(result);
|
|
@@ -652,6 +664,7 @@ async function fullHistorySteps(context: ProviderContext): Promise<InteractionIn
|
|
|
652
664
|
// Response mapping (Interactions -> canonical)
|
|
653
665
|
// ---------------------------------------------------------------------------
|
|
654
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.
|
|
655
668
|
function mapUsage(raw?: InteractionObject["usage"]): TokenUsage {
|
|
656
669
|
const input = raw?.total_input_tokens ?? 0;
|
|
657
670
|
// Google reports thought tokens separately from `total_output_tokens`, but bills them as output
|
|
@@ -675,6 +688,7 @@ function mapUsage(raw?: InteractionObject["usage"]): TokenUsage {
|
|
|
675
688
|
};
|
|
676
689
|
}
|
|
677
690
|
|
|
691
|
+
// Interaction status as a finish reason (requires_action = tool_calls, incomplete = length, failed = error); in: parseInteraction/stream, out: finishReason.
|
|
678
692
|
function mapFinishReason(status?: string): string {
|
|
679
693
|
if (status === "requires_action") return "tool_calls";
|
|
680
694
|
if (status === "incomplete") return "length";
|
|
@@ -682,6 +696,7 @@ function mapFinishReason(status?: string): string {
|
|
|
682
696
|
return "stop";
|
|
683
697
|
}
|
|
684
698
|
|
|
699
|
+
// Turns a finished Interaction into text, thinking, tool calls, usage and replayable steps, throwing on failed or cancelled; in: generate, out: ProviderGenerateResult.
|
|
685
700
|
function parseInteraction(
|
|
686
701
|
interaction: InteractionObject,
|
|
687
702
|
modelId: string,
|
|
@@ -765,6 +780,7 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
765
780
|
return getModelsForProvider("google");
|
|
766
781
|
}
|
|
767
782
|
|
|
783
|
+
// The catalog spec of a model, else a permissive generic one; in: the registry, out: a ModelSpec.
|
|
768
784
|
getModel(modelId: string): ModelSpec | undefined {
|
|
769
785
|
const clean = cleanModelId(modelId);
|
|
770
786
|
return (
|
|
@@ -775,6 +791,7 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
775
791
|
);
|
|
776
792
|
}
|
|
777
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.
|
|
778
795
|
private buildBody(
|
|
779
796
|
modelId: string,
|
|
780
797
|
context: ProviderContext,
|
|
@@ -884,6 +901,7 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
884
901
|
})();
|
|
885
902
|
}
|
|
886
903
|
|
|
904
|
+
// Fetch options with JSON headers and the x-goog-api-key header; in: doFetch and stream, out: RequestInit.
|
|
887
905
|
private requestInit(
|
|
888
906
|
method: "POST" | "GET",
|
|
889
907
|
body: InteractionRequestBody | undefined,
|
|
@@ -900,6 +918,7 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
900
918
|
return { method, headers, ...(body ? { body: JSON.stringify(body) } : {}), signal };
|
|
901
919
|
}
|
|
902
920
|
|
|
921
|
+
// One HTTP call under the request timeout, returning status, headers and body text; in: fetchInteraction and cancelInteraction, out: the raw reply.
|
|
903
922
|
private async doFetch(
|
|
904
923
|
url: string,
|
|
905
924
|
method: "POST" | "GET",
|
|
@@ -922,6 +941,7 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
922
941
|
}
|
|
923
942
|
}
|
|
924
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.
|
|
925
945
|
private throwIfError(
|
|
926
946
|
status: number,
|
|
927
947
|
statusText: string,
|
|
@@ -990,6 +1010,7 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
990
1010
|
void this.doFetch(`${baseUrl}/interactions/${id}/cancel`, "POST", undefined, options, apiKey).catch(() => {});
|
|
991
1011
|
}
|
|
992
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.
|
|
993
1014
|
async generate(
|
|
994
1015
|
model: string | ModelSpec,
|
|
995
1016
|
context: ProviderContext,
|
|
@@ -1088,6 +1109,7 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
1088
1109
|
}
|
|
1089
1110
|
}
|
|
1090
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.
|
|
1091
1113
|
stream(
|
|
1092
1114
|
model: string | ModelSpec,
|
|
1093
1115
|
context: ProviderContext,
|
|
@@ -1097,23 +1119,7 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
1097
1119
|
const startTime = Date.now();
|
|
1098
1120
|
const clean = cleanModelId(model);
|
|
1099
1121
|
|
|
1100
|
-
const linked =
|
|
1101
|
-
const forwardUserAbort = () => {
|
|
1102
|
-
try {
|
|
1103
|
-
linked.abort((options?.signal as { reason?: unknown })?.reason);
|
|
1104
|
-
} catch {
|
|
1105
|
-
try {
|
|
1106
|
-
linked.abort();
|
|
1107
|
-
} catch {}
|
|
1108
|
-
}
|
|
1109
|
-
};
|
|
1110
|
-
if (options?.signal?.aborted) forwardUserAbort();
|
|
1111
|
-
else options?.signal?.addEventListener("abort", forwardUserAbort, { once: true });
|
|
1112
|
-
const removeStreamCancel = eventStream.onCancel(() => {
|
|
1113
|
-
try {
|
|
1114
|
-
linked.abort();
|
|
1115
|
-
} catch {}
|
|
1116
|
-
});
|
|
1122
|
+
const { linked, release } = linkStreamAbort(eventStream, options?.signal);
|
|
1117
1123
|
|
|
1118
1124
|
(async () => {
|
|
1119
1125
|
// Hoisted for the catch-block self-heal (consts inside try are not
|
|
@@ -1159,21 +1165,7 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
1159
1165
|
// (429/5xx/network) are retried before the first byte; a live stream is never retried.
|
|
1160
1166
|
const open = (target: string, method: "POST" | "GET"): Promise<Response> =>
|
|
1161
1167
|
openStreamResponse({
|
|
1162
|
-
fetchOnce:
|
|
1163
|
-
const timer =
|
|
1164
|
-
streamTimeoutMs && streamTimeoutMs > 0
|
|
1165
|
-
? setTimeout(() => {
|
|
1166
|
-
try {
|
|
1167
|
-
linked.abort();
|
|
1168
|
-
} catch {}
|
|
1169
|
-
}, streamTimeoutMs)
|
|
1170
|
-
: undefined;
|
|
1171
|
-
try {
|
|
1172
|
-
return await (options?.fetch ?? fetch)(target, this.requestInit(method, method === "POST" ? body : undefined, options, apiKey, linked.signal));
|
|
1173
|
-
} finally {
|
|
1174
|
-
if (timer) clearTimeout(timer);
|
|
1175
|
-
}
|
|
1176
|
-
},
|
|
1168
|
+
fetchOnce: () => withAbortTimer(linked, streamTimeoutMs, () => (options?.fetch ?? fetch)(target, this.requestInit(method, method === "POST" ? body : undefined, options, apiKey, linked.signal))),
|
|
1177
1169
|
onHttpError: (r, text) => this.throwIfError(r.status, r.statusText, target, text, clean, headersToRecord(r.headers)),
|
|
1178
1170
|
noBodyError: () => toConciseProviderError(new Error("Google streaming response had no body"), "google", clean),
|
|
1179
1171
|
retry: retryOptionsFrom(options),
|
|
@@ -1210,7 +1202,7 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
1210
1202
|
() => {
|
|
1211
1203
|
aborted = true;
|
|
1212
1204
|
try {
|
|
1213
|
-
void reader?.cancel();
|
|
1205
|
+
void reader?.cancel().catch(() => {}); // an aborted body is already errored: its cancel() rejects, and nobody else listens
|
|
1214
1206
|
} catch {}
|
|
1215
1207
|
},
|
|
1216
1208
|
{ once: true }
|
|
@@ -1465,12 +1457,7 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
1465
1457
|
eventStream.fail(raw);
|
|
1466
1458
|
}
|
|
1467
1459
|
} finally {
|
|
1468
|
-
|
|
1469
|
-
options?.signal?.removeEventListener("abort", forwardUserAbort);
|
|
1470
|
-
} catch {}
|
|
1471
|
-
try {
|
|
1472
|
-
removeStreamCancel();
|
|
1473
|
-
} catch {}
|
|
1460
|
+
release();
|
|
1474
1461
|
}
|
|
1475
1462
|
})();
|
|
1476
1463
|
|