agent-accelerator 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +174 -123
- package/{SYSTEM_PROMPT_AGENT.md → examples/prompts/SYSTEM_PROMPT_AGENT.md} +1 -1
- package/package.json +14 -9
- package/src/agent/agent.ts +385 -73
- package/src/agent/context.ts +1 -0
- package/src/agent/delegation.ts +443 -30
- package/src/agent/loop.ts +183 -53
- package/src/data/README.md +6 -6
- package/src/index.ts +163 -46
- package/src/models/catalog-cache.ts +60 -9
- package/src/models/catalog.ts +55 -8
- package/src/providers/google.ts +926 -0
- package/src/providers/openai-compat.ts +1147 -0
- package/src/providers/openai.ts +959 -0
- package/src/providers/openrouter-responses.ts +949 -0
- package/src/providers/openrouter.ts +1037 -0
- package/src/{ai-sdk → providers}/registry.ts +43 -55
- package/src/providers.ts +492 -0
- package/src/session/store.ts +806 -0
- package/src/streaming/sse-parser.ts +6 -4
- package/src/tools/executor.ts +23 -7
- package/src/tools/schema.ts +21 -11
- package/src/types/agent.ts +33 -2
- package/src/types/core.ts +3 -2
- package/src/types/message.ts +5 -0
- package/src/types/model.ts +3 -7
- package/src/types/provider-payloads.ts +2 -84
- package/src/types/response.ts +24 -0
- package/src/types/tool.ts +8 -0
- package/src/update-models.ts +56 -0
- package/src/utils/cache.ts +1 -1
- package/src/utils/documents.ts +517 -0
- package/src/utils/env.ts +0 -7
- package/src/{ai-sdk → utils}/errors.ts +61 -2
- package/src/utils/headers.ts +10 -20
- package/src/utils/media.ts +5 -2
- package/src/utils/retry.ts +89 -0
- package/src/utils/serialization.ts +15 -0
- package/src/utils/session.ts +36 -0
- package/src/ai-sdk/converters.ts +0 -342
- package/src/ai-sdk/executor.ts +0 -454
- package/src/ai-sdk/index.ts +0 -55
- package/src/ai-sdk/model-provider.ts +0 -303
- package/src/ai-sdk/options.ts +0 -306
- package/src/ai-sdk/provider.ts +0 -415
- package/src/tokens/counter.ts +0 -136
- /package/{SYSTEM_PROMPT.md → examples/prompts/SYSTEM_PROMPT.md} +0 -0
- /package/{SYSTEM_PROMPT_TOOLS.md → examples/prompts/SYSTEM_PROMPT_TOOLS.md} +0 -0
package/src/providers.ts
ADDED
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical provider contract for Agent Accelerator.
|
|
3
|
+
*
|
|
4
|
+
* This module is provider-agnostic: it defines how canonical Agent Accelerator
|
|
5
|
+
* capabilities map onto provider concepts WITHOUT containing any
|
|
6
|
+
* provider-specific HTTP, endpoints, headers, or response parsing. Those live
|
|
7
|
+
* in `src/providers/<provider>.ts` (e.g. `src/providers/google.ts`).
|
|
8
|
+
*
|
|
9
|
+
* Rule: OpenAI, OpenRouter, and future providers must be addable here without
|
|
10
|
+
* redesigning this abstraction — only new per-provider mappers/adapters.
|
|
11
|
+
*/
|
|
12
|
+
import type { CacheConfig, ServiceTier, ThinkingLevel } from "./types/core.ts";
|
|
13
|
+
|
|
14
|
+
/** Classification of a canonical capability on a given provider. */
|
|
15
|
+
export type ProviderCapabilityStatus =
|
|
16
|
+
| "NATIVE"
|
|
17
|
+
| "TRANSLATED"
|
|
18
|
+
| "AUTOMATIC"
|
|
19
|
+
| "EMULATED"
|
|
20
|
+
| "UNSUPPORTED";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Single warning mechanism for provider capability differences.
|
|
24
|
+
*
|
|
25
|
+
* There is no pre-existing logger in `src/` (only CLI logs in
|
|
26
|
+
* `update-models.ts`), so this `console.warn` line IS the mechanism — do not
|
|
27
|
+
* introduce a second one. Used when a requested capability cannot be applied
|
|
28
|
+
* as-is (e.g. Google explicit cache retention) so it never silently
|
|
29
|
+
* disappears.
|
|
30
|
+
*
|
|
31
|
+
* Warnings are deduped per provider+capability+requested: agent loops call
|
|
32
|
+
* mappers once per turn, and repeating the same warning every turn is noise.
|
|
33
|
+
* A repeated identical request logs once per process.
|
|
34
|
+
*
|
|
35
|
+
* @example `emitProviderWarning({ provider: "google", capability: "cache retention", requested: "high", reason: "...", fallback: "..." })`
|
|
36
|
+
*/
|
|
37
|
+
const emittedWarnings = new Set<string>();
|
|
38
|
+
|
|
39
|
+
export function emitProviderWarning(options: {
|
|
40
|
+
provider: string;
|
|
41
|
+
capability: string;
|
|
42
|
+
requested?: string;
|
|
43
|
+
reason: string;
|
|
44
|
+
fallback: string;
|
|
45
|
+
}): void {
|
|
46
|
+
const key = `${options.provider}::${options.capability}::${options.requested ?? ""}`;
|
|
47
|
+
if (emittedWarnings.has(key)) return;
|
|
48
|
+
emittedWarnings.add(key);
|
|
49
|
+
const requested = options.requested ? ` (requested: "${options.requested}")` : "";
|
|
50
|
+
// Leading newline: warnings fire at the start of a turn's request build,
|
|
51
|
+
// when the previous turn's streamed text may have left the terminal cursor
|
|
52
|
+
// mid-line. Without it the warning glues onto streamed output.
|
|
53
|
+
console.warn(
|
|
54
|
+
`\n[Agent Accelerator] WARNING [${options.provider}] ${options.capability}${requested}: ${options.reason} Using ${options.fallback} instead.`
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Clears deduped-warning state (mainly for tests). */
|
|
59
|
+
export function clearEmittedWarnings(): void {
|
|
60
|
+
emittedWarnings.clear();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
// Per-session provider routing state (canonical conversation stays agnostic)
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
interface SessionRoutingState {
|
|
68
|
+
/** Provider id that served the last turn in this session. */
|
|
69
|
+
lastProvider?: string;
|
|
70
|
+
/** True once a session mixed providers and must stay on explicit history. */
|
|
71
|
+
mixedProviders?: boolean;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const sessionRouting = new Map<string, SessionRoutingState>();
|
|
75
|
+
|
|
76
|
+
/** Records which provider served a turn so adapters can detect switches. */
|
|
77
|
+
export function noteProviderTurn(sessionId: string | undefined, providerId: string): void {
|
|
78
|
+
if (!sessionId) return;
|
|
79
|
+
const state = sessionRouting.get(sessionId) ?? {};
|
|
80
|
+
if (state.lastProvider && state.lastProvider !== providerId) {
|
|
81
|
+
state.mixedProviders = true;
|
|
82
|
+
}
|
|
83
|
+
state.lastProvider = providerId;
|
|
84
|
+
sessionRouting.set(sessionId, state);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Returns the provider id that served the previous turn in this session. */
|
|
88
|
+
export function lastProviderFor(sessionId: string | undefined): string | undefined {
|
|
89
|
+
if (!sessionId) return undefined;
|
|
90
|
+
return sessionRouting.get(sessionId)?.lastProvider;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** True when this session already mixed providers (history must be explicit). */
|
|
94
|
+
export function isMixedProviderSession(sessionId: string | undefined): boolean {
|
|
95
|
+
if (!sessionId) return false;
|
|
96
|
+
return sessionRouting.get(sessionId)?.mixedProviders === true;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Clears routing state for a session (mainly for tests). */
|
|
100
|
+
export function clearSessionRouting(sessionId?: string): void {
|
|
101
|
+
if (sessionId) sessionRouting.delete(sessionId);
|
|
102
|
+
else sessionRouting.clear();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
// Canonical capability mappers (pure, no I/O)
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Maps a canonical ThinkingLevel onto Google Interactions `thinking_level`.
|
|
111
|
+
* Google values: minimal|low|medium|high (no off switch, no xhigh/max).
|
|
112
|
+
*/
|
|
113
|
+
export function mapThinkingLevelToGoogle(
|
|
114
|
+
level: ThinkingLevel | string | undefined
|
|
115
|
+
): { thinkingLevel?: string } {
|
|
116
|
+
if (!level) return {};
|
|
117
|
+
const norm = String(level).toLowerCase().trim();
|
|
118
|
+
if (norm === "dynamic") return {}; // server dynamic default (NATIVE)
|
|
119
|
+
if (norm === "none") {
|
|
120
|
+
emitProviderWarning({
|
|
121
|
+
provider: "google",
|
|
122
|
+
capability: "thinking level",
|
|
123
|
+
requested: String(level),
|
|
124
|
+
reason: "the Interactions API has no disable/off level — thought steps are always present.",
|
|
125
|
+
fallback: "the server default (omit thinking_level)",
|
|
126
|
+
});
|
|
127
|
+
return {};
|
|
128
|
+
}
|
|
129
|
+
if (norm === "xhigh" || norm === "max") {
|
|
130
|
+
emitProviderWarning({
|
|
131
|
+
provider: "google",
|
|
132
|
+
capability: "thinking level",
|
|
133
|
+
requested: String(level),
|
|
134
|
+
reason: "Google's maximum thinking level is high.",
|
|
135
|
+
fallback: "thinking_level high",
|
|
136
|
+
});
|
|
137
|
+
return { thinkingLevel: "high" };
|
|
138
|
+
}
|
|
139
|
+
if (norm === "minimal" || norm === "low" || norm === "medium" || norm === "high") {
|
|
140
|
+
return { thinkingLevel: norm };
|
|
141
|
+
}
|
|
142
|
+
return {};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Maps canonical ServiceTier onto Google `service_tier` (omit = standard). */
|
|
146
|
+
export function mapServiceTierToGoogle(tier: ServiceTier | undefined): "flex" | "priority" | undefined {
|
|
147
|
+
if (tier === "flex" || tier === "priority") return tier;
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Maps a canonical ThinkingLevel onto OpenRouter Chat Completions
|
|
153
|
+
* `reasoning.effort`.
|
|
154
|
+
*
|
|
155
|
+
* Completions documents `reasoning_effort: xhigh|high|medium|low|minimal|none`
|
|
156
|
+
* (parameters.md) and live probes accept `xhigh` verbatim (`03`), so — unlike
|
|
157
|
+
* the discontinued Responses skin, which clamped `xhigh` — everything passes
|
|
158
|
+
* through. `max` rides the same verbatim path. `dynamic` omits (server default).
|
|
159
|
+
*/
|
|
160
|
+
export function mapThinkingLevelToOpenRouterChat(
|
|
161
|
+
level: ThinkingLevel | string | undefined
|
|
162
|
+
): { effort?: string } {
|
|
163
|
+
if (!level) return {};
|
|
164
|
+
const norm = String(level).toLowerCase().trim();
|
|
165
|
+
if (norm === "dynamic") return {}; // server default
|
|
166
|
+
if (
|
|
167
|
+
norm === "none" ||
|
|
168
|
+
norm === "minimal" ||
|
|
169
|
+
norm === "low" ||
|
|
170
|
+
norm === "medium" ||
|
|
171
|
+
norm === "high" ||
|
|
172
|
+
norm === "xhigh" ||
|
|
173
|
+
norm === "max"
|
|
174
|
+
) {
|
|
175
|
+
return { effort: norm };
|
|
176
|
+
}
|
|
177
|
+
return {};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Maps a canonical ThinkingLevel onto OpenRouter Responses `reasoning.effort`.
|
|
182
|
+
* Documented efforts: minimal|low|medium|high (server default medium).
|
|
183
|
+
* Wire-validated extras: `none` disables reasoning output; `xhigh`/`max` are
|
|
184
|
+
* echoed but are NOT documented levels, so they clamp to `high` with a warning.
|
|
185
|
+
*
|
|
186
|
+
* @deprecated The Responses skin is discontinued for OpenRouter
|
|
187
|
+
* (`src/providers/openrouter-responses.ts`, archived). Use
|
|
188
|
+
* {@link mapThinkingLevelToOpenRouterChat} with the stable Chat Completions
|
|
189
|
+
* transport instead.
|
|
190
|
+
*/
|
|
191
|
+
export function mapThinkingLevelToOpenRouter(
|
|
192
|
+
level: ThinkingLevel | string | undefined
|
|
193
|
+
): { effort?: string } {
|
|
194
|
+
if (!level) return {};
|
|
195
|
+
const norm = String(level).toLowerCase().trim();
|
|
196
|
+
if (norm === "dynamic") return {}; // server default (medium)
|
|
197
|
+
if (norm === "none") return { effort: "none" };
|
|
198
|
+
if (norm === "xhigh" || norm === "max") {
|
|
199
|
+
emitProviderWarning({
|
|
200
|
+
provider: "openrouter",
|
|
201
|
+
capability: "thinking level",
|
|
202
|
+
requested: String(level),
|
|
203
|
+
reason: "OpenRouter documents reasoning efforts up to high.",
|
|
204
|
+
fallback: "reasoning effort high",
|
|
205
|
+
});
|
|
206
|
+
return { effort: "high" };
|
|
207
|
+
}
|
|
208
|
+
if (norm === "minimal" || norm === "low" || norm === "medium" || norm === "high") {
|
|
209
|
+
return { effort: norm };
|
|
210
|
+
}
|
|
211
|
+
return {};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Maps canonical ServiceTier onto OpenRouter `service_tier` (omit = auto). */
|
|
215
|
+
export function mapServiceTierToOpenRouter(tier: ServiceTier | undefined): "flex" | "priority" | undefined {
|
|
216
|
+
if (tier === "flex" || tier === "priority") return tier;
|
|
217
|
+
return undefined;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Applies canonical cache config for OpenRouter. Session affinity flows via
|
|
222
|
+
* top-level body `session_id` (sent by the adapter) plus the `x-session-id`
|
|
223
|
+
* header fallback; there is no retention body primitive, so explicit
|
|
224
|
+
* retention/cachedContentId are UNSUPPORTED: warn and drop.
|
|
225
|
+
*/
|
|
226
|
+
export function applyCacheForOpenRouter(
|
|
227
|
+
cache: CacheConfig | undefined,
|
|
228
|
+
modelRef: string
|
|
229
|
+
): void {
|
|
230
|
+
if (!cache) return;
|
|
231
|
+
if (cache.retention && cache.retention !== "implicit") {
|
|
232
|
+
emitProviderWarning({
|
|
233
|
+
provider: "openrouter",
|
|
234
|
+
capability: "cache retention",
|
|
235
|
+
requested: `${cache.retention} (${modelRef})`,
|
|
236
|
+
reason: "OpenRouter documents no retention control on the stable Chat Completions endpoint.",
|
|
237
|
+
fallback: "default gateway caching with headers-only session affinity (no retention payload is sent)",
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
if (cache.cachedContentId) {
|
|
241
|
+
emitProviderWarning({
|
|
242
|
+
provider: "openrouter",
|
|
243
|
+
capability: "explicit cached content",
|
|
244
|
+
requested: cache.cachedContentId,
|
|
245
|
+
reason: "cached content references do not exist on the Responses API.",
|
|
246
|
+
fallback: "full-history sends instead (cachedContentId is ignored)",
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Applies canonical cache config for custom OpenAI-compatible endpoints.
|
|
253
|
+
* Affinity is headers-only (`x-session-id`): unknown body properties make
|
|
254
|
+
* strict endpoints (groq, ollama, …) fail, so nothing cache-related is ever
|
|
255
|
+
* placed in the body. Explicit retention/cachedContentId are UNSUPPORTED:
|
|
256
|
+
* warn and drop.
|
|
257
|
+
*/
|
|
258
|
+
export function applyCacheForCustom(
|
|
259
|
+
prefix: string,
|
|
260
|
+
cache: CacheConfig | undefined,
|
|
261
|
+
modelRef: string
|
|
262
|
+
): void {
|
|
263
|
+
if (!cache) return;
|
|
264
|
+
if (cache.retention && cache.retention !== "implicit") {
|
|
265
|
+
emitProviderWarning({
|
|
266
|
+
provider: prefix,
|
|
267
|
+
capability: "cache retention",
|
|
268
|
+
requested: `${cache.retention} (${modelRef})`,
|
|
269
|
+
reason: "custom OpenAI-compatible endpoints define no retention control, and strict endpoints reject unknown body properties.",
|
|
270
|
+
fallback: "default caching with headers-only session affinity (no retention payload is sent)",
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
if (cache.cachedContentId) {
|
|
274
|
+
emitProviderWarning({
|
|
275
|
+
provider: prefix,
|
|
276
|
+
capability: "explicit cached content",
|
|
277
|
+
requested: cache.cachedContentId,
|
|
278
|
+
reason: "cached content references do not exist on OpenAI-compatible endpoints.",
|
|
279
|
+
fallback: "full-history sends instead (cachedContentId is ignored)",
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** OpenRouter Chat Completions `tool_choice` wire values. */
|
|
285
|
+
export type OpenRouterChatToolChoice =
|
|
286
|
+
| "auto"
|
|
287
|
+
| "none"
|
|
288
|
+
| "required"
|
|
289
|
+
| { type: "function"; function: { name: string } };
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Maps canonical toolChoice onto Chat Completions `tool_choice`.
|
|
293
|
+
* `auto` is the server default (omitted); `required` is documented
|
|
294
|
+
* (parameters.md) and accepted live (`05`); a function pin passes through
|
|
295
|
+
* verbatim. Note the shape differs from the discontinued Responses skin
|
|
296
|
+
* (`{type:function,name}`): completions nests the name under `function`.
|
|
297
|
+
*/
|
|
298
|
+
export function mapToolChoiceToOpenRouterChat(
|
|
299
|
+
choice: "auto" | "none" | "required" | { type: "function"; function: { name: string } } | undefined
|
|
300
|
+
): OpenRouterChatToolChoice | undefined {
|
|
301
|
+
if (!choice || choice === "auto") return undefined;
|
|
302
|
+
if (choice === "none" || choice === "required") return choice;
|
|
303
|
+
const name = choice.function?.name;
|
|
304
|
+
if (name) return { type: "function", function: { name } };
|
|
305
|
+
return undefined;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** OpenRouter Responses `tool_choice` wire values.
|
|
309
|
+
* @deprecated Discontinued skin; see {@link OpenRouterChatToolChoice}. */
|
|
310
|
+
export type OpenRouterToolChoice = "auto" | "none" | "required" | { type: "function"; name: string };
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Maps canonical toolChoice onto OpenRouter Responses `tool_choice`.
|
|
314
|
+
* `auto` is the server default (omitted); `required` is natively supported
|
|
315
|
+
* (validated); a function pin passes through verbatim.
|
|
316
|
+
*/
|
|
317
|
+
export function mapToolChoiceToOpenRouter(
|
|
318
|
+
choice: "auto" | "none" | "required" | { type: "function"; function: { name: string } } | undefined
|
|
319
|
+
): OpenRouterToolChoice | undefined {
|
|
320
|
+
if (!choice || choice === "auto") return undefined;
|
|
321
|
+
if (choice === "none" || choice === "required") return choice;
|
|
322
|
+
const name = choice.function?.name;
|
|
323
|
+
if (name) return { type: "function", name };
|
|
324
|
+
return undefined;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Maps a canonical ThinkingLevel onto OpenAI Responses `reasoning.effort`.
|
|
329
|
+
* Documented efforts: none|minimal|low|medium|high|xhigh|max (server default
|
|
330
|
+
* medium). Unlike OpenRouter (which clamps xhigh), OpenAI documents xhigh
|
|
331
|
+
* and max natively so they pass through verbatim. `dynamic` omits (server default).
|
|
332
|
+
*/
|
|
333
|
+
export function mapThinkingLevelToOpenAI(
|
|
334
|
+
level: ThinkingLevel | string | undefined
|
|
335
|
+
): { effort?: string } {
|
|
336
|
+
if (!level) return {};
|
|
337
|
+
const norm = String(level).toLowerCase().trim();
|
|
338
|
+
if (norm === "dynamic") return {}; // server default
|
|
339
|
+
if (
|
|
340
|
+
norm === "none" ||
|
|
341
|
+
norm === "minimal" ||
|
|
342
|
+
norm === "low" ||
|
|
343
|
+
norm === "medium" ||
|
|
344
|
+
norm === "high" ||
|
|
345
|
+
norm === "xhigh" ||
|
|
346
|
+
norm === "max"
|
|
347
|
+
) {
|
|
348
|
+
return { effort: norm };
|
|
349
|
+
}
|
|
350
|
+
return {};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** Maps canonical ServiceTier onto OpenAI `service_tier` (omit = auto). */
|
|
354
|
+
export function mapServiceTierToOpenAI(tier: ServiceTier | undefined): "flex" | "priority" | undefined {
|
|
355
|
+
if (tier === "flex" || tier === "priority") return tier;
|
|
356
|
+
return undefined;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Applies canonical cache config for OpenAI. Session affinity flows via
|
|
361
|
+
* `prompt_cache_key` (handled by the adapter); explicit retention control
|
|
362
|
+
* lives in `prompt_cache_options` (gpt-5.6+ explicit breakpoints) which is
|
|
363
|
+
* out of scope for the most-important subset, so retention/cachedContentId
|
|
364
|
+
* are UNSUPPORTED: warn and drop. `prompt_cache_retention` is deprecated.
|
|
365
|
+
*/
|
|
366
|
+
export function applyCacheForOpenAI(
|
|
367
|
+
cache: CacheConfig | undefined,
|
|
368
|
+
modelRef: string
|
|
369
|
+
): void {
|
|
370
|
+
if (!cache) return;
|
|
371
|
+
if (cache.retention && cache.retention !== "implicit") {
|
|
372
|
+
emitProviderWarning({
|
|
373
|
+
provider: "openai",
|
|
374
|
+
capability: "cache retention",
|
|
375
|
+
requested: `${cache.retention} (${modelRef})`,
|
|
376
|
+
reason: "the native Responses adapter uses prompt_cache_key affinity only; explicit retention modes are out of scope.",
|
|
377
|
+
fallback: "default caching with prompt_cache_key affinity (no retention payload is sent)",
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
if (cache.cachedContentId) {
|
|
381
|
+
emitProviderWarning({
|
|
382
|
+
provider: "openai",
|
|
383
|
+
capability: "explicit cached content",
|
|
384
|
+
requested: cache.cachedContentId,
|
|
385
|
+
reason: "cached content references do not exist on the Responses API.",
|
|
386
|
+
fallback: "full-history sends instead (cachedContentId is ignored)",
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/** OpenAI Responses `tool_choice` wire values (most-important subset). */
|
|
392
|
+
export type OpenAIToolChoice = "auto" | "none" | "required" | { type: "function"; name: string };
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Maps canonical toolChoice onto OpenAI Responses `tool_choice`.
|
|
396
|
+
* `auto` is the server default (omitted); `required` forces one or more
|
|
397
|
+
* calls; a function pin passes through verbatim. Built-in/MCP/allowed_tools
|
|
398
|
+
* variants are out of scope and never emitted.
|
|
399
|
+
*/
|
|
400
|
+
export function mapToolChoiceToOpenAI(
|
|
401
|
+
choice: "auto" | "none" | "required" | { type: "function"; function: { name: string } } | undefined
|
|
402
|
+
): OpenAIToolChoice | undefined {
|
|
403
|
+
if (!choice || choice === "auto") return undefined;
|
|
404
|
+
if (choice === "none" || choice === "required") return choice;
|
|
405
|
+
const name = choice.function?.name;
|
|
406
|
+
if (name) return { type: "function", name };
|
|
407
|
+
return undefined;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Applies canonical cache config for Google. The Interactions API supports
|
|
412
|
+
* implicit/automatic caching ONLY — there is no cache payload to send.
|
|
413
|
+
* Explicit retention/cachedContentId are UNSUPPORTED: warn and drop.
|
|
414
|
+
*/
|
|
415
|
+
export function applyCacheForGoogle(
|
|
416
|
+
cache: CacheConfig | undefined,
|
|
417
|
+
modelRef: string
|
|
418
|
+
): void {
|
|
419
|
+
if (!cache) return;
|
|
420
|
+
if (cache.retention && cache.retention !== "implicit") {
|
|
421
|
+
emitProviderWarning({
|
|
422
|
+
provider: "google",
|
|
423
|
+
capability: "cache retention",
|
|
424
|
+
requested: `${cache.retention} (${modelRef})`,
|
|
425
|
+
reason: "the Interactions API does not support user-defined explicit cache retention.",
|
|
426
|
+
fallback: "Google automatic/implicit caching (no cache payload is sent)",
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
if (cache.cachedContentId) {
|
|
430
|
+
emitProviderWarning({
|
|
431
|
+
provider: "google",
|
|
432
|
+
capability: "explicit cached content",
|
|
433
|
+
requested: cache.cachedContentId,
|
|
434
|
+
reason: "cachedContents resources do not exist on the Interactions API.",
|
|
435
|
+
fallback: "stateful interaction chaining / stateless history instead (cachedContentId is ignored)",
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/** Normalized tool-choice mode shared by provider adapters. */
|
|
441
|
+
export type CanonicalToolChoiceMode = "auto" | "any" | "none";
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Normalizes canonical toolChoice into a provider-neutral mode + optional
|
|
445
|
+
* pinned tool name. Google renders this as
|
|
446
|
+
* `{ allowed_tools: { mode, tools? } }` in its own adapter.
|
|
447
|
+
*/
|
|
448
|
+
export function normalizeToolChoice(
|
|
449
|
+
choice: "auto" | "none" | "required" | { type: "function"; function: { name: string } } | undefined,
|
|
450
|
+
availableToolNames: string[]
|
|
451
|
+
): { mode: CanonicalToolChoiceMode; tools?: string[] } | undefined {
|
|
452
|
+
if (!choice) return undefined;
|
|
453
|
+
if (typeof choice === "string") {
|
|
454
|
+
if (choice === "auto") return undefined; // server default
|
|
455
|
+
if (choice === "none") return { mode: "none" };
|
|
456
|
+
if (choice === "required") {
|
|
457
|
+
return availableToolNames.length > 0 ? { mode: "any", tools: availableToolNames } : { mode: "any" };
|
|
458
|
+
}
|
|
459
|
+
return undefined;
|
|
460
|
+
}
|
|
461
|
+
const name = choice.function?.name;
|
|
462
|
+
if (name) return { mode: "any", tools: [name] };
|
|
463
|
+
return { mode: "any" };
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
467
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Reconstructs tool arguments from streamed chunks.
|
|
472
|
+
*
|
|
473
|
+
* Wire behavior varies across providers: some send the complete JSON in a
|
|
474
|
+
* single delta after an empty initial payload, others genuinely split
|
|
475
|
+
* partials across start + deltas. Concatenating blindly can yield
|
|
476
|
+
* `"{}{...}"` (unparseable), so candidates are tried in order and the first
|
|
477
|
+
* chunk that parses to a plain object wins.
|
|
478
|
+
*
|
|
479
|
+
* @example `parseStreamedToolArguments("{}", '{"location":"Paris"}')`
|
|
480
|
+
*/
|
|
481
|
+
export function parseStreamedToolArguments(startText: string, deltaText: string): Record<string, unknown> {
|
|
482
|
+
const candidates = [startText + deltaText, deltaText, startText].filter((c) => c && c.trim());
|
|
483
|
+
for (const candidate of candidates) {
|
|
484
|
+
try {
|
|
485
|
+
const parsed: unknown = JSON.parse(candidate);
|
|
486
|
+
if (isPlainObject(parsed)) return parsed;
|
|
487
|
+
} catch {
|
|
488
|
+
// try next candidate
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
return { raw: startText + deltaText };
|
|
492
|
+
}
|