agent-accelerator 0.1.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/LICENSE +21 -0
- package/README.md +1220 -0
- package/SYSTEM_PROMPT.md +14 -0
- package/SYSTEM_PROMPT_AGENT.md +34 -0
- package/SYSTEM_PROMPT_TOOLS.md +9 -0
- package/bunfig.toml +2 -0
- package/package.json +59 -0
- package/src/agent/agent.ts +615 -0
- package/src/agent/context.ts +161 -0
- package/src/agent/delegation.ts +481 -0
- package/src/agent/loop.ts +569 -0
- package/src/agent/subagent.ts +83 -0
- package/src/ai-sdk/converters.ts +342 -0
- package/src/ai-sdk/errors.ts +122 -0
- package/src/ai-sdk/executor.ts +454 -0
- package/src/ai-sdk/index.ts +55 -0
- package/src/ai-sdk/model-provider.ts +303 -0
- package/src/ai-sdk/options.ts +306 -0
- package/src/ai-sdk/provider.ts +415 -0
- package/src/ai-sdk/registry.ts +416 -0
- package/src/data/README.md +84 -0
- package/src/index.ts +190 -0
- package/src/models/catalog-cache.ts +273 -0
- package/src/models/catalog.ts +503 -0
- package/src/streaming/event-stream.ts +211 -0
- package/src/streaming/sse-parser.ts +97 -0
- package/src/tokens/counter.ts +136 -0
- package/src/tools/executor.ts +365 -0
- package/src/tools/schema.ts +221 -0
- package/src/tools/tool.ts +101 -0
- package/src/types/agent.ts +87 -0
- package/src/types/core.ts +86 -0
- package/src/types/message.ts +115 -0
- package/src/types/model.ts +212 -0
- package/src/types/provider-payloads.ts +434 -0
- package/src/types/response.ts +158 -0
- package/src/types/tool.ts +61 -0
- package/src/utils/base64.ts +27 -0
- package/src/utils/cache.ts +146 -0
- package/src/utils/env.ts +78 -0
- package/src/utils/headers.ts +110 -0
- package/src/utils/media.ts +137 -0
- package/src/utils/serialization.ts +91 -0
- package/src/utils/session.ts +26 -0
- package/src/utils/thought-signature.ts +27 -0
- package/tsconfig.json +31 -0
|
@@ -0,0 +1,303 @@
|
|
|
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;
|
|
@@ -0,0 +1,306 @@
|
|
|
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
|
+
}
|