agent-accelerator 0.1.0 → 0.2.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.
Files changed (45) hide show
  1. package/README.md +83 -112
  2. package/{SYSTEM_PROMPT_AGENT.md → examples/prompts/SYSTEM_PROMPT_AGENT.md} +1 -1
  3. package/package.json +14 -9
  4. package/src/agent/agent.ts +191 -71
  5. package/src/agent/context.ts +1 -0
  6. package/src/agent/delegation.ts +61 -12
  7. package/src/agent/loop.ts +71 -48
  8. package/src/data/README.md +6 -6
  9. package/src/index.ts +130 -45
  10. package/src/models/catalog-cache.ts +60 -9
  11. package/src/models/catalog.ts +53 -7
  12. package/src/providers/google.ts +926 -0
  13. package/src/providers/openai-compat.ts +1147 -0
  14. package/src/providers/openai.ts +959 -0
  15. package/src/providers/openrouter-responses.ts +949 -0
  16. package/src/providers/openrouter.ts +1037 -0
  17. package/src/{ai-sdk → providers}/registry.ts +43 -55
  18. package/src/providers.ts +490 -0
  19. package/src/streaming/sse-parser.ts +6 -4
  20. package/src/tools/executor.ts +19 -6
  21. package/src/tools/schema.ts +21 -11
  22. package/src/types/agent.ts +8 -1
  23. package/src/types/core.ts +1 -1
  24. package/src/types/message.ts +5 -0
  25. package/src/types/model.ts +3 -7
  26. package/src/types/provider-payloads.ts +2 -84
  27. package/src/types/tool.ts +6 -0
  28. package/src/update-models.ts +56 -0
  29. package/src/utils/cache.ts +1 -1
  30. package/src/utils/documents.ts +517 -0
  31. package/src/utils/env.ts +0 -7
  32. package/src/{ai-sdk → utils}/errors.ts +61 -2
  33. package/src/utils/headers.ts +10 -20
  34. package/src/utils/media.ts +5 -2
  35. package/src/utils/retry.ts +89 -0
  36. package/src/utils/serialization.ts +15 -0
  37. package/src/ai-sdk/converters.ts +0 -342
  38. package/src/ai-sdk/executor.ts +0 -454
  39. package/src/ai-sdk/index.ts +0 -55
  40. package/src/ai-sdk/model-provider.ts +0 -303
  41. package/src/ai-sdk/options.ts +0 -306
  42. package/src/ai-sdk/provider.ts +0 -415
  43. package/src/tokens/counter.ts +0 -136
  44. /package/{SYSTEM_PROMPT.md → examples/prompts/SYSTEM_PROMPT.md} +0 -0
  45. /package/{SYSTEM_PROMPT_TOOLS.md → examples/prompts/SYSTEM_PROMPT_TOOLS.md} +0 -0
@@ -1,303 +0,0 @@
1
- import type {
2
- Provider,
3
- ProviderId,
4
- ModelSpec,
5
- ProviderRequestOptions,
6
- ProviderGenerateResult,
7
- } from "../types/model.ts";
8
- import type { ProviderContext } from "../types/message.ts";
9
- import type { AssistantMessageEventStream } from "../streaming/event-stream.ts";
10
- import type { LanguageModelV4 } from "@ai-sdk/provider";
11
- import { getAiSdkModel } from "./provider.ts";
12
- import { executeAiSdkGenerate, executeAiSdkStream } from "./executor.ts";
13
- import { countTokens } from "../tokens/counter.ts";
14
- import { getModelFromCatalog, getModelsForProvider } from "../models/catalog.ts";
15
- import { getApiKey, getEnv } from "../utils/env.ts";
16
-
17
- /**
18
- * Permissive placeholder so custom endpoints never fail preflight:
19
- * allows every ThinkingLevel (none -> xhigh + dynamic).
20
- * @example `const spec = createGenericModelSpec("groq", "llama-3.3-70b-versatile");`
21
- */
22
- export function createGenericModelSpec(provider: string, modelId: string): ModelSpec {
23
- const clean = modelId.includes("/") ? modelId.split("/").slice(1).join("/") : modelId;
24
- return {
25
- id: clean,
26
- provider: provider as ProviderId,
27
- name: clean,
28
- contextWindow: 128000,
29
- maxOutputTokens: 8192,
30
- limit: { context: 128000, output: 8192 },
31
- cost: {},
32
- modalities: { input: ["text"], output: ["text"] },
33
- reasoning: true,
34
- reasoning_options: [
35
- { type: "toggle" },
36
- { type: "effort", values: ["minimal", "low", "medium", "high", "xhigh"] },
37
- { type: "budget_tokens", min: 0, max: 128000 },
38
- ],
39
- tool_call: true,
40
- capabilities: {
41
- supportsThinking: true,
42
- supportsThinkingLevel: true,
43
- supportsThinkingBudget: true,
44
- supportsReasoningToggle: true,
45
- supportsReasoningEffort: true,
46
- supportsImplicitCaching: true,
47
- supportsExplicitCaching: false,
48
- supportsLongCacheRetention: false,
49
- supportsParallelToolCalls: true,
50
- supportsStreaming: true,
51
- modalities: ["text"],
52
- },
53
- pricing: {},
54
- };
55
- }
56
-
57
- /**
58
- * Base provider class powered purely by Vercel AI SDK.
59
- */
60
- export class AiSdkBaseProvider implements Provider {
61
- readonly id: ProviderId;
62
- readonly name: string;
63
- readonly models: ModelSpec[] = [];
64
-
65
- /**
66
- * Creates a provider adapter around the Vercel AI SDK.
67
- *
68
- * @param id Stable provider prefix used in `provider/model` strings.
69
- * @param name Human-readable provider label.
70
- * @param models Optional catalog entries exposed by this provider.
71
- */
72
- constructor(id: ProviderId, name: string, models: ModelSpec[] = []) {
73
- this.id = id;
74
- this.name = name;
75
- this.models = models;
76
- }
77
-
78
- getModel(modelId: string): ModelSpec | undefined {
79
- const clean = modelId.includes("/") ? modelId.split("/").slice(1).join("/") : modelId;
80
- const fromCatalog =
81
- getModelFromCatalog(this.id, modelId) ||
82
- getModelFromCatalog(this.id, clean) ||
83
- this.models.find((m) => m.id === modelId || m.id === clean);
84
-
85
- if (fromCatalog) return fromCatalog;
86
- return createGenericModelSpec(this.id, clean);
87
- }
88
-
89
- protected cleanModelId(model: string | ModelSpec): string {
90
- const rawId = typeof model === "string" ? model : model.id;
91
- return rawId.replace(new RegExp(`^(${this.id}|models?)/`, "i"), "");
92
- }
93
-
94
- toAiSdkModel(modelId: string, options?: ProviderRequestOptions): LanguageModelV4 {
95
- const clean = this.cleanModelId(modelId);
96
- return getAiSdkModel(this.id, clean, {
97
- apiKey: options?.apiKey,
98
- baseUrl: options?.baseUrl,
99
- headers: options?.headers,
100
- env: options?.env,
101
- });
102
- }
103
-
104
- async generate(
105
- model: string | ModelSpec,
106
- context: ProviderContext,
107
- options?: ProviderRequestOptions
108
- ): Promise<ProviderGenerateResult> {
109
- const rawId = typeof model === "string" ? model : model.id;
110
- const clean = this.cleanModelId(rawId);
111
- const aiModel = this.toAiSdkModel(clean, options);
112
- return executeAiSdkGenerate(aiModel, this.id, clean, context, options);
113
- }
114
-
115
- stream(
116
- model: string | ModelSpec,
117
- context: ProviderContext,
118
- options?: ProviderRequestOptions
119
- ): AssistantMessageEventStream {
120
- const rawId = typeof model === "string" ? model : model.id;
121
- const clean = this.cleanModelId(rawId);
122
- const aiModel = this.toAiSdkModel(clean, options);
123
- return executeAiSdkStream(aiModel, this.id, clean, context, options);
124
- }
125
-
126
- async countTokens(
127
- _model: string | ModelSpec,
128
- context: ProviderContext
129
- ): Promise<number> {
130
- return countTokens(context);
131
- }
132
- }
133
-
134
- /**
135
- * Pure Vercel AI SDK Google provider.
136
- */
137
- export class GoogleAiSdkProvider extends AiSdkBaseProvider {
138
- /** Creates a Google AI Studio adapter with the supported Gemini catalog. */
139
- constructor() {
140
- const filtered = getModelsForProvider("google").filter(
141
- (m) => !m.id.toLowerCase().includes("gemini-2") && !m.id.toLowerCase().includes("gemini-1")
142
- );
143
- super("google", "Google AI Studio", filtered);
144
- }
145
-
146
- protected override cleanModelId(model: string | ModelSpec): string {
147
- const rawId = typeof model === "string" ? model : model.id;
148
- const clean = rawId.replace(/^(google\/|models\/)/, "");
149
- if (clean.toLowerCase().includes("gemini-2") || clean.toLowerCase().includes("gemini-1")) {
150
- throw new Error(
151
- `Gemini 2.x and 1.x models are not supported by the Google Provider. ` +
152
- `Only Gemini 3.x series models are supported (e.g. 'gemini-3.5-flash-lite', 'gemini-3.7-flash').`
153
- );
154
- }
155
- return clean;
156
- }
157
-
158
- }
159
-
160
- /**
161
- * Pure Vercel AI SDK OpenAI provider.
162
- */
163
- export class OpenAiAiSdkProvider extends AiSdkBaseProvider {
164
- /** Creates an OpenAI adapter backed by the AI SDK and the OpenAI catalog. */
165
- constructor() {
166
- super("openai", "OpenAI", getModelsForProvider("openai"));
167
- }
168
-
169
- protected override cleanModelId(model: string | ModelSpec): string {
170
- const rawId = typeof model === "string" ? model : model.id;
171
- return rawId.replace(/^(openai|models?)\//, "");
172
- }
173
- }
174
-
175
- /**
176
- * Pure Vercel AI SDK OpenCode provider.
177
- */
178
- export class OpenCodeAiSdkProvider extends AiSdkBaseProvider {
179
- /** Creates an OpenCode adapter backed by the AI SDK and OpenCode catalog. */
180
- constructor() {
181
- super("opencode", "OpenCode", getModelsForProvider("opencode"));
182
- }
183
-
184
- protected override cleanModelId(model: string | ModelSpec): string {
185
- const rawId = typeof model === "string" ? model : model.id;
186
- return rawId.replace(/^(opencode-go\/|opencode\/)/, "");
187
- }
188
- }
189
-
190
- /**
191
- * Pure Vercel AI SDK OpenRouter provider.
192
- */
193
- export class OpenRouterAiSdkProvider extends AiSdkBaseProvider {
194
- /** Creates an OpenRouter adapter backed by the AI SDK and OpenRouter catalog. */
195
- constructor() {
196
- super("openrouter", "OpenRouter", getModelsForProvider("openrouter"));
197
- }
198
-
199
- protected override cleanModelId(model: string | ModelSpec): string {
200
- const rawId = typeof model === "string" ? model : model.id;
201
- return rawId.replace(/^openrouter\//, "");
202
- }
203
- }
204
-
205
- export interface CustomProviderOptions {
206
- /** Human label, defaults to `${id} (OpenAI-compatible)` */
207
- name?: string;
208
- /** Static baseUrl — overrides env. Env `{PREFIX}_BASE_URL` still wins at runtime if set. */
209
- baseUrl?: string;
210
- /** Static apiKey — overrides env. Explicit per-call `apiKey` still wins. */
211
- apiKey?: string;
212
- /** Default baseUrl when no env is set. Defaults to OpenAI cloud. */
213
- defaultBaseUrl?: string;
214
- }
215
-
216
- /**
217
- * Pure Vercel AI SDK Custom/OpenAI-compatible provider.
218
- */
219
- export class CustomAiSdkProvider extends AiSdkBaseProvider {
220
- private readonly prefix: string;
221
- private readonly configuredBaseUrl?: string;
222
- private readonly configuredApiKey?: string;
223
-
224
- /**
225
- * Creates an OpenAI-compatible provider for any endpoint prefix.
226
- *
227
- * @param prefix Prefix used in model strings and environment variables,
228
- * such as `groq` for `groq/llama-3.3-70b`.
229
- * @param opts Optional endpoint, key, and display-name overrides.
230
- *
231
- * @example
232
- * ```ts
233
- * const groq = new CustomAiSdkProvider("groq", {
234
- * baseUrl: "https://api.groq.com/openai/v1",
235
- * apiKey: process.env.GROQ_API_KEY,
236
- * });
237
- * ```
238
- */
239
- constructor(prefix: string, opts?: CustomProviderOptions) {
240
- const norm = prefix.trim().toLowerCase().replace(/\/.*$/, "");
241
- super(norm as ProviderId, opts?.name ?? `${norm} (OpenAI-compatible)`);
242
- this.prefix = norm;
243
- this.configuredBaseUrl = opts?.baseUrl ?? opts?.defaultBaseUrl;
244
- this.configuredApiKey = opts?.apiKey;
245
- }
246
-
247
- override toAiSdkModel(modelId: string, options?: ProviderRequestOptions): LanguageModelV4 {
248
- const clean = this.cleanModelId(modelId);
249
- const envPrefix = this.prefix.toUpperCase().replace(/[^A-Z0-9]/g, "_");
250
-
251
- const apiKey =
252
- options?.apiKey ||
253
- this.configuredApiKey ||
254
- options?.env?.[`${envPrefix}_API_KEY`] ||
255
- options?.env?.[`${envPrefix}_BASE_API_KEY`] ||
256
- getEnv(`${envPrefix}_API_KEY`) ||
257
- getEnv(`${envPrefix}_BASE_API_KEY`) ||
258
- getEnv("OPENAI_BASE_API_KEY") ||
259
- getEnv("OPENAI_API_KEY") ||
260
- getApiKey(this.prefix, undefined, options?.env);
261
-
262
- const baseUrl =
263
- options?.baseUrl ||
264
- this.configuredBaseUrl ||
265
- options?.env?.[`${envPrefix}_BASE_URL`] ||
266
- options?.env?.[`${envPrefix}_BASEURL`] ||
267
- getEnv(`${envPrefix}_BASE_URL`) ||
268
- getEnv(`${envPrefix}_BASEURL`) ||
269
- getEnv("OPENAI_BASE_URL") ||
270
- "https://api.openai.com/v1";
271
-
272
- return getAiSdkModel(this.prefix, clean, {
273
- apiKey,
274
- baseUrl,
275
- headers: options?.headers,
276
- env: options?.env,
277
- });
278
- }
279
- }
280
-
281
- // Legacy class aliases so instanceof checks in tests & user code succeed seamlessly
282
- export {
283
- GoogleAiSdkProvider as GoogleAIStudioProvider,
284
- OpenAiAiSdkProvider as OpenAIProvider,
285
- OpenCodeAiSdkProvider as OpenCodeProvider,
286
- OpenRouterAiSdkProvider as OpenRouterProvider,
287
- CustomAiSdkProvider as OpenAICompatibleProvider,
288
- };
289
-
290
- /**
291
- * Creates a custom OpenAI-compatible provider backed by the Vercel AI SDK.
292
- * @example `const provider = createOpenAICompatibleProvider("groq", { baseUrl: "https://api.groq.com/openai/v1" });`
293
- */
294
- export function createOpenAICompatibleProvider(
295
- prefix: string,
296
- opts?: CustomProviderOptions
297
- ): CustomAiSdkProvider {
298
- return new CustomAiSdkProvider(prefix, opts);
299
- }
300
- /** Backward-compatible alias for {@link createOpenAICompatibleProvider}. @example `createCustomProvider("ollama")` */
301
- export const createCustomProvider = createOpenAICompatibleProvider;
302
- /** Backward-compatible class alias for {@link CustomAiSdkProvider}. @example `new CustomProvider("ollama")` */
303
- export const CustomProvider = CustomAiSdkProvider;
@@ -1,306 +0,0 @@
1
- import type { ThinkingConfig, CacheConfig, ServiceTier } from "../types/core.ts";
2
- import type { ProviderRequestOptions } from "../types/model.ts";
3
- import { buildSessionHeaders } from "../utils/headers.ts";
4
- import { toConciseProviderError } from "./errors.ts";
5
- import { getPromptCacheRetention } from "../utils/cache.ts";
6
-
7
- export type AiSdkReasoning = "provider-default" | "none" | "minimal" | "low" | "medium" | "high" | "xhigh";
8
-
9
- export type AiSdkToolChoice =
10
- | { type: "auto" }
11
- | { type: "none" }
12
- | { type: "required" }
13
- | { type: "tool"; toolName: string };
14
-
15
- export interface AiSdkCallOptions {
16
- reasoning?: AiSdkReasoning;
17
- toolChoice?: AiSdkToolChoice;
18
- providerOptions?: Record<string, Record<string, unknown>>;
19
- headers?: Record<string, string>;
20
- abortSignal?: AbortSignal;
21
- }
22
-
23
- /**
24
- * Maps Agent Accelerator thinkingLevel to Vercel AI SDK reasoning.
25
- */
26
- export function mapThinkingToReasoning(thinking?: ThinkingConfig): AiSdkReasoning | undefined {
27
- if (!thinking) return undefined;
28
- if (thinking.enabled === false) return "none";
29
- const level = thinking.level;
30
- if (!level) return undefined;
31
- if (level === "none") return "none";
32
- if (level === "dynamic") return "provider-default";
33
- if (level === "minimal" || level === "low" || level === "medium" || level === "high" || level === "xhigh") {
34
- return level;
35
- }
36
- return "provider-default";
37
- }
38
-
39
- /**
40
- * Resolves per-run thinking override without mutating agent config.
41
- */
42
- export function resolveEffectiveThinking(
43
- base?: ThinkingConfig,
44
- overrideLevel?: string
45
- ): ThinkingConfig | undefined {
46
- if (!overrideLevel) return base;
47
- if (overrideLevel === "none") return { enabled: false, level: "none", budgetTokens: 0 };
48
- if (overrideLevel === "dynamic") return { enabled: true, level: "dynamic", budgetTokens: -1 };
49
- return { enabled: true, level: overrideLevel as ThinkingConfig["level"] };
50
- }
51
-
52
- /**
53
- * Maps Agent Accelerator toolChoice to Vercel AI SDK toolChoice.
54
- */
55
- export function mapToolChoice(
56
- choice?: ProviderRequestOptions["toolChoice"]
57
- ): AiSdkToolChoice | undefined {
58
- if (!choice) return undefined;
59
- if (typeof choice === "string") {
60
- if (choice === "auto") return { type: "auto" };
61
- if (choice === "none") return { type: "none" };
62
- if (choice === "required") return { type: "required" };
63
- return undefined;
64
- }
65
- if (choice.type === "function") {
66
- const name = choice.function?.name;
67
- if (name) return { type: "tool", toolName: name };
68
- return { type: "required" };
69
- }
70
- return undefined;
71
- }
72
-
73
- function mergeProviderOptions(
74
- target: Record<string, Record<string, unknown>>,
75
- source: Record<string, Record<string, unknown>>
76
- ): void {
77
- for (const [key, value] of Object.entries(source)) {
78
- target[key] = { ...(target[key] ?? {}), ...value };
79
- }
80
- }
81
-
82
- /**
83
- * Maps thinking details to provider-specific providerOptions.
84
- */
85
- export function mapThinkingToProviderOptions(
86
- providerId: string,
87
- thinking?: ThinkingConfig
88
- ): Record<string, Record<string, unknown>> {
89
- if (!thinking || thinking.enabled === false) return {};
90
- const level = thinking.level;
91
- if (!level || level === "none" || level === "dynamic") return {};
92
- const norm = providerId.toLowerCase().trim();
93
-
94
- if (norm === "google" || norm === "gemini") {
95
- const googleLevel = level === "xhigh" ? "high" : level === "minimal" ? "minimal" : level;
96
- if (googleLevel === "minimal" || googleLevel === "low" || googleLevel === "medium" || googleLevel === "high") {
97
- return {
98
- google: {
99
- thinkingConfig: {
100
- thinkingLevel: googleLevel,
101
- includeThoughts: thinking.includeThoughts ?? true,
102
- },
103
- },
104
- };
105
- }
106
- return {};
107
- }
108
-
109
- if (norm === "openai") {
110
- if (level === "minimal" || level === "low" || level === "medium" || level === "high" || level === "xhigh") {
111
- return { openai: { reasoningEffort: level } };
112
- }
113
- return {};
114
- }
115
-
116
- return {};
117
- }
118
-
119
- /**
120
- * Providers known to tolerate OpenAI-style prompt-cache/service-tier body
121
- * params. Strict OpenAI-compatible endpoints (groq, ollama, …) 400 on unknown
122
- * properties like `promptCacheKey`, so anything off this list gets session
123
- * affinity via headers only (always harmless).
124
- */
125
- const PROMPT_CACHE_KEY_PROVIDERS = new Set([
126
- "openai",
127
- "openrouter",
128
- "opencode",
129
- "opencode-go",
130
- ]);
131
-
132
- /**
133
- * Maps serviceTier to provider-specific providerOptions.
134
- */
135
- export function mapServiceTierToProviderOptions(
136
- providerId: string,
137
- tier?: ServiceTier
138
- ): Record<string, Record<string, unknown>> {
139
- if (!tier) return {};
140
- const norm = providerId.toLowerCase().trim();
141
- if (norm === "google" || norm === "gemini") {
142
- return { google: { serviceTier: tier } };
143
- }
144
- if (norm === "openai") {
145
- return { openai: { serviceTier: tier } };
146
- }
147
- if (!PROMPT_CACHE_KEY_PROVIDERS.has(norm)) return {};
148
- return {
149
- openai: { serviceTier: tier },
150
- [norm]: { serviceTier: tier },
151
- };
152
- }
153
-
154
- /**
155
- * Maps cache config to provider-specific providerOptions.
156
- * retention short|medium|long maps to OpenCode prompt_cache_retention;
157
- * session affinity maps to OpenAI promptCacheKey; explicit Google
158
- * cachedContentId maps to google.cachedContent. Implicit prefix caching
159
- * itself needs no flag beyond a stable prompt plus session headers.
160
- */
161
- export function mapCacheToProviderOptions(
162
- providerId: string,
163
- cache?: CacheConfig,
164
- sessionId?: string
165
- ): Record<string, Record<string, unknown>> {
166
- const out: Record<string, Record<string, unknown>> = {};
167
- if (!cache && !sessionId) return out;
168
- const norm = providerId.toLowerCase().trim();
169
- const cachedContentId = cache?.cachedContentId;
170
- if (cachedContentId && (norm === "google" || norm === "gemini")) {
171
- mergeProviderOptions(out, { google: { cachedContent: cachedContentId } });
172
- }
173
- const affinity = sessionId || cache?.sessionId;
174
- if (affinity && PROMPT_CACHE_KEY_PROVIDERS.has(norm)) {
175
- mergeProviderOptions(out, { openai: { promptCacheKey: affinity } });
176
- if (norm !== "openai") {
177
- mergeProviderOptions(out, { [norm]: { promptCacheKey: affinity } });
178
- }
179
- }
180
- const retention = cache?.retention;
181
- if (retention && retention !== "implicit") {
182
- const promptCacheRetention = getPromptCacheRetention(retention, true);
183
- if (promptCacheRetention && (norm === "opencode" || norm === "opencode-go")) {
184
- mergeProviderOptions(out, {
185
- opencode: { prompt_cache_retention: promptCacheRetention },
186
- openai: { promptCacheRetention: promptCacheRetention },
187
- });
188
- }
189
- }
190
- return out;
191
- }
192
-
193
- /**
194
- * Builds Vercel AI SDK call options from Agent Accelerator request options.
195
- */
196
- export function buildAiSdkCallOptions(
197
- providerId: string,
198
- options?: ProviderRequestOptions
199
- ): AiSdkCallOptions {
200
- const reasoning = mapThinkingToReasoning(options?.thinking);
201
- const toolChoice = mapToolChoice(options?.toolChoice);
202
- const providerOptions: Record<string, Record<string, unknown>> = {};
203
- mergeProviderOptions(providerOptions, mapThinkingToProviderOptions(providerId, options?.thinking));
204
- mergeProviderOptions(providerOptions, mapServiceTierToProviderOptions(providerId, options?.serviceTier));
205
- mergeProviderOptions(providerOptions, mapCacheToProviderOptions(providerId, options?.cache, options?.sessionId));
206
-
207
- const headers: Record<string, string> = buildSessionHeaders(
208
- providerId,
209
- options?.cache,
210
- options?.headers,
211
- options?.sessionId
212
- );
213
-
214
- const result: AiSdkCallOptions = {};
215
- if (reasoning !== undefined) result.reasoning = reasoning;
216
- if (toolChoice !== undefined) result.toolChoice = toolChoice;
217
- if (Object.keys(providerOptions).length > 0) result.providerOptions = providerOptions;
218
- if (Object.keys(headers).length > 0) result.headers = headers;
219
- if (options?.signal) result.abortSignal = options.signal;
220
- return result;
221
- }
222
-
223
- /**
224
- * Checks whether a provider error is transient and safe to retry.
225
- */
226
- export function isAbortError(error: unknown): boolean {
227
- const err = error as { name?: unknown; message?: unknown } | null;
228
- if (!err || typeof err !== "object") return false;
229
- if ((err as { name?: unknown }).name === "AbortError") return true;
230
- return /abort|cancell?ed/i.test(String((err as { message?: unknown }).message ?? error));
231
- }
232
-
233
- export function isTransientAiSdkError(error: unknown): boolean {
234
- if (!error) return false;
235
- if (isAbortError(error)) return false;
236
- const err = error as Record<string, unknown>;
237
- if (err["transient"] === true) return true;
238
- const status = Number(
239
- (err["status"] as number | undefined) ??
240
- (err["statusCode"] as number | undefined) ??
241
- ((err["response"] as Record<string, unknown> | undefined)?.["status"] as number | undefined) ??
242
- ((err["lastError"] as Record<string, unknown> | undefined)?.["status"] as number | undefined) ??
243
- NaN
244
- );
245
- if ([408, 425, 429, 500, 502, 503, 504].includes(status)) return true;
246
- const code = String((err["code"] as string | undefined) ?? "").toUpperCase();
247
- if (["ECONNRESET", "ECONNREFUSED", "EAI_AGAIN", "UND_ERR_CONNECT_TIMEOUT", "EPIPE", "ETIMEDOUT"].includes(code)) {
248
- return true;
249
- }
250
- return /(?:timed?\s?out|temporar(?:y|ily)|connection reset|connection refused|service unavailable|rate limit|\b5\d\d\b|overloaded)/i.test(
251
- String((err["message"] as string | undefined) ?? error)
252
- );
253
- }
254
-
255
- function sleep(ms: number, signal?: AbortSignal): Promise<void> {
256
- if (ms <= 0) return Promise.resolve();
257
- return new Promise((resolve, reject) => {
258
- const timer = setTimeout(() => {
259
- signal?.removeEventListener("abort", onAbort);
260
- resolve();
261
- }, ms);
262
- const onAbort = () => {
263
- clearTimeout(timer);
264
- signal?.removeEventListener("abort", onAbort);
265
- reject(Object.assign(new Error("Operation aborted"), { name: "AbortError" }));
266
- };
267
- signal?.addEventListener("abort", onAbort, { once: true });
268
- if (signal?.aborted) onAbort();
269
- // Tag as AbortError so callers never retry it
270
- // (name assigned at reject site)
271
- });
272
- }
273
-
274
- /**
275
- * Runs a Vercel AI SDK call with bounded retries for transient failures.
276
- */
277
- export async function withAiSdkRetries<T>(
278
- fn: () => Promise<T> | PromiseLike<T>,
279
- options?: {
280
- maxRetries?: number;
281
- maxRetryDelayMs?: number;
282
- signal?: AbortSignal;
283
- label?: { providerId?: string; modelId?: string };
284
- }
285
- ): Promise<T> {
286
- const maxRetries = options?.maxRetries ?? 2;
287
- const maxDelay = options?.maxRetryDelayMs ?? 5000;
288
- const fail = (error: unknown): never => {
289
- if (options?.label?.providerId && options?.label?.modelId) {
290
- throw toConciseProviderError(error, options.label.providerId, options.label.modelId);
291
- }
292
- throw error;
293
- };
294
- let attempt = 0;
295
- for (;;) {
296
- try {
297
- return await fn();
298
- } catch (error) {
299
- if (options?.signal?.aborted || isAbortError(error)) throw error;
300
- if (attempt >= maxRetries || !isTransientAiSdkError(error)) fail(error);
301
- const delay = Math.min(maxDelay, 250 * 2 ** attempt + Math.random() * 100);
302
- attempt += 1;
303
- await sleep(delay, options?.signal);
304
- }
305
- }
306
- }