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,342 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
LanguageModelV4Prompt,
|
|
3
|
+
LanguageModelV4FunctionTool,
|
|
4
|
+
LanguageModelV4FinishReason,
|
|
5
|
+
} from "@ai-sdk/provider";
|
|
6
|
+
import type { ProviderContext, ContentPart, Message } from "../types/message.ts";
|
|
7
|
+
import type { StandardToolDeclaration, ToolCallRecord } from "../types/tool.ts";
|
|
8
|
+
import type { ProviderGenerateResult, ProviderRequestOptions } from "../types/model.ts";
|
|
9
|
+
import type { TokenUsage } from "../types/core.ts";
|
|
10
|
+
import { normalizeMediaInput } from "../utils/media.ts";
|
|
11
|
+
import { base64ToBytes } from "../utils/base64.ts";
|
|
12
|
+
import { safeStringify } from "../utils/serialization.ts";
|
|
13
|
+
import { stripSchemaForGoogle } from "../tools/schema.ts";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Converts Agent Accelerator tools to Vercel AI SDK tools format.
|
|
17
|
+
*/
|
|
18
|
+
export function toAiSdkTools(
|
|
19
|
+
tools?: StandardToolDeclaration[],
|
|
20
|
+
providerId?: string
|
|
21
|
+
): LanguageModelV4FunctionTool[] | undefined {
|
|
22
|
+
if (!tools || tools.length === 0) return undefined;
|
|
23
|
+
const norm = providerId?.toLowerCase().trim();
|
|
24
|
+
|
|
25
|
+
return tools.map((t) => {
|
|
26
|
+
const raw = (t.parameters || { type: "object", properties: {} }) as Record<string, unknown>;
|
|
27
|
+
const inputSchema = norm === "google" || norm === "gemini" ? (stripSchemaForGoogle(raw) as any) : (raw as any);
|
|
28
|
+
return {
|
|
29
|
+
type: "function",
|
|
30
|
+
name: t.name,
|
|
31
|
+
description: t.description,
|
|
32
|
+
inputSchema,
|
|
33
|
+
...(t.strict !== undefined ? { strict: t.strict } : {}),
|
|
34
|
+
};
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function guessFilename(input: unknown, mimeType: string): string | undefined {
|
|
39
|
+
if (typeof input !== "string" || input.startsWith("data:") || input.startsWith("http")) return undefined;
|
|
40
|
+
const base = input.split(/[\\/]/).pop()?.split("?")[0]?.trim();
|
|
41
|
+
if (base && base.includes(".")) return base;
|
|
42
|
+
const ext = mimeType.split("/")[1]?.split(";")[0]?.trim();
|
|
43
|
+
return ext ? `file.${ext}` : undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Providers known to accept echoed reasoning in history. Vercel serializes
|
|
48
|
+
* prompt `reasoning` parts to `reasoning_content`, which strict
|
|
49
|
+
* OpenAI-compatible endpoints (groq, ollama, …) reject with 400 — so off-list
|
|
50
|
+
* providers get history without the thinking trace (tool calls preserved).
|
|
51
|
+
*/
|
|
52
|
+
const REASONING_HISTORY_PROVIDERS = new Set([
|
|
53
|
+
"google",
|
|
54
|
+
"gemini",
|
|
55
|
+
"openai",
|
|
56
|
+
"openrouter",
|
|
57
|
+
"opencode",
|
|
58
|
+
"opencode-go",
|
|
59
|
+
]);
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Converts Agent Accelerator ProviderContext to Vercel AI SDK LanguageModelV4Prompt.
|
|
63
|
+
*/
|
|
64
|
+
export async function toAiSdkPrompt(
|
|
65
|
+
context: ProviderContext,
|
|
66
|
+
providerId?: string
|
|
67
|
+
): Promise<LanguageModelV4Prompt> {
|
|
68
|
+
const prompt: LanguageModelV4Prompt = [];
|
|
69
|
+
|
|
70
|
+
if (context.systemPrompt) {
|
|
71
|
+
prompt.push({
|
|
72
|
+
role: "system",
|
|
73
|
+
content: context.systemPrompt,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
for (const msg of context.messages) {
|
|
78
|
+
if (typeof msg.content === "string") {
|
|
79
|
+
if (msg.role === "system") {
|
|
80
|
+
prompt.push({ role: "system", content: msg.content });
|
|
81
|
+
} else if (msg.role === "assistant") {
|
|
82
|
+
prompt.push({
|
|
83
|
+
role: "assistant",
|
|
84
|
+
content: [{ type: "text", text: msg.content }],
|
|
85
|
+
});
|
|
86
|
+
} else if (msg.role === "tool") {
|
|
87
|
+
prompt.push({
|
|
88
|
+
role: "tool",
|
|
89
|
+
content: [
|
|
90
|
+
{
|
|
91
|
+
type: "tool-result",
|
|
92
|
+
toolCallId: (msg as any).tool_call_id || (msg as any).id || "call_0",
|
|
93
|
+
toolName: (msg as any).name || "unknown",
|
|
94
|
+
output: { type: "text", value: msg.content },
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
});
|
|
98
|
+
} else {
|
|
99
|
+
prompt.push({
|
|
100
|
+
role: "user",
|
|
101
|
+
content: [{ type: "text", text: msg.content }],
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
} else if (Array.isArray(msg.content)) {
|
|
105
|
+
if (msg.role === "assistant") {
|
|
106
|
+
const assistantParts: Array<any> = [];
|
|
107
|
+
for (const part of msg.content) {
|
|
108
|
+
if (part.type === "text" && part.text) {
|
|
109
|
+
assistantParts.push({ type: "text", text: part.text });
|
|
110
|
+
} else if (part.type === "thinking" && part.thinking) {
|
|
111
|
+
const keepReasoning =
|
|
112
|
+
!providerId || REASONING_HISTORY_PROVIDERS.has(providerId.toLowerCase().trim());
|
|
113
|
+
if (keepReasoning) {
|
|
114
|
+
assistantParts.push({ type: "reasoning", text: part.thinking });
|
|
115
|
+
}
|
|
116
|
+
} else if (part.type === "tool_call") {
|
|
117
|
+
assistantParts.push({
|
|
118
|
+
type: "tool-call",
|
|
119
|
+
toolCallId: part.id,
|
|
120
|
+
toolName: part.name,
|
|
121
|
+
input: part.arguments || {},
|
|
122
|
+
providerMetadata: part.thoughtSignature
|
|
123
|
+
? { google: { thoughtSignature: part.thoughtSignature } }
|
|
124
|
+
: undefined,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (assistantParts.length > 0) {
|
|
129
|
+
prompt.push({ role: "assistant", content: assistantParts });
|
|
130
|
+
}
|
|
131
|
+
} else if (msg.role === "tool") {
|
|
132
|
+
const toolParts: Array<any> = [];
|
|
133
|
+
for (const part of msg.content) {
|
|
134
|
+
if (part.type === "tool_result") {
|
|
135
|
+
toolParts.push({
|
|
136
|
+
type: "tool-result",
|
|
137
|
+
toolCallId: part.id,
|
|
138
|
+
toolName: part.name,
|
|
139
|
+
output: {
|
|
140
|
+
type: "text",
|
|
141
|
+
value: typeof part.result === "string" ? part.result : safeStringify(part.result),
|
|
142
|
+
},
|
|
143
|
+
isError: part.isError,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (toolParts.length > 0) {
|
|
148
|
+
prompt.push({ role: "tool", content: toolParts });
|
|
149
|
+
}
|
|
150
|
+
} else {
|
|
151
|
+
const userParts: Array<any> = [];
|
|
152
|
+
for (const part of msg.content) {
|
|
153
|
+
if (part.type === "text" && part.text) {
|
|
154
|
+
userParts.push({ type: "text", text: part.text });
|
|
155
|
+
} else if (
|
|
156
|
+
part.type === "image" ||
|
|
157
|
+
part.type === "audio" ||
|
|
158
|
+
part.type === "video" ||
|
|
159
|
+
part.type === "file"
|
|
160
|
+
) {
|
|
161
|
+
// Vercel V4 has no `image` part: every media part is a `file` part
|
|
162
|
+
// with a tagged data payload. Images use image/* media types so all
|
|
163
|
+
// providers route them to their native image input.
|
|
164
|
+
const raw = (part as any).image ?? (part as any).audio ?? (part as any).video ?? (part as any).file;
|
|
165
|
+
const norm = await normalizeMediaInput(raw, (part as any).mimeType);
|
|
166
|
+
const bytes = base64ToBytes(norm.base64Data);
|
|
167
|
+
const filename =
|
|
168
|
+
(part as any).filename ?? guessFilename(raw, norm.mimeType);
|
|
169
|
+
userParts.push({
|
|
170
|
+
type: "file",
|
|
171
|
+
mediaType: norm.mimeType,
|
|
172
|
+
data: { type: "data", data: bytes },
|
|
173
|
+
...(filename ? { filename } : {}),
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (userParts.length > 0) {
|
|
178
|
+
prompt.push({ role: "user", content: userParts });
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return prompt;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Extracts normalized TokenUsage from Vercel AI SDK usage structure.
|
|
189
|
+
*/
|
|
190
|
+
export function extractAiSdkUsage(rawUsage?: any): TokenUsage {
|
|
191
|
+
if (!rawUsage) {
|
|
192
|
+
return { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const input =
|
|
196
|
+
rawUsage.inputTokens?.total ??
|
|
197
|
+
rawUsage.prompt_tokens ??
|
|
198
|
+
rawUsage.input_tokens ??
|
|
199
|
+
0;
|
|
200
|
+
|
|
201
|
+
const output =
|
|
202
|
+
rawUsage.outputTokens?.total ??
|
|
203
|
+
rawUsage.completion_tokens ??
|
|
204
|
+
rawUsage.output_tokens ??
|
|
205
|
+
0;
|
|
206
|
+
|
|
207
|
+
const total =
|
|
208
|
+
rawUsage.total_tokens ??
|
|
209
|
+
rawUsage.totalTokens ??
|
|
210
|
+
input + output;
|
|
211
|
+
|
|
212
|
+
const cached =
|
|
213
|
+
rawUsage.inputTokens?.cacheRead ??
|
|
214
|
+
rawUsage.prompt_tokens_details?.cached_tokens ??
|
|
215
|
+
rawUsage.cached_tokens ??
|
|
216
|
+
0;
|
|
217
|
+
|
|
218
|
+
const cacheWrite =
|
|
219
|
+
rawUsage.inputTokens?.cacheWrite ??
|
|
220
|
+
rawUsage.prompt_tokens_details?.cache_write_tokens ??
|
|
221
|
+
rawUsage.cache_write_tokens ??
|
|
222
|
+
0;
|
|
223
|
+
|
|
224
|
+
const thinking =
|
|
225
|
+
rawUsage.outputTokens?.reasoning ??
|
|
226
|
+
rawUsage.completion_tokens_details?.reasoning_tokens ??
|
|
227
|
+
rawUsage.reasoning_tokens ??
|
|
228
|
+
0;
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
inputTokens: input,
|
|
232
|
+
outputTokens: output,
|
|
233
|
+
totalTokens: total,
|
|
234
|
+
cachedTokens: cached,
|
|
235
|
+
cacheReadTokens: cached,
|
|
236
|
+
cacheWriteTokens: cacheWrite,
|
|
237
|
+
thinkingTokens: thinking,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Converts Vercel AI SDK doGenerate result into Agent Accelerator ProviderGenerateResult.
|
|
243
|
+
*/
|
|
244
|
+
export function fromAiSdkGenerateResult(
|
|
245
|
+
aiResult: any,
|
|
246
|
+
providerId: string,
|
|
247
|
+
modelId: string,
|
|
248
|
+
durationMs: number
|
|
249
|
+
): ProviderGenerateResult {
|
|
250
|
+
let text = "";
|
|
251
|
+
let thinking: string | undefined;
|
|
252
|
+
let thoughtSignature: string | undefined;
|
|
253
|
+
const toolCalls: ToolCallRecord[] = [];
|
|
254
|
+
|
|
255
|
+
if (aiResult.content && Array.isArray(aiResult.content)) {
|
|
256
|
+
for (const part of aiResult.content) {
|
|
257
|
+
if (part.type === "text" && part.text) {
|
|
258
|
+
text += part.text;
|
|
259
|
+
} else if (part.type === "reasoning" && part.text) {
|
|
260
|
+
thinking = (thinking ? thinking + "\n" : "") + part.text;
|
|
261
|
+
const sig =
|
|
262
|
+
part.providerMetadata?.google?.thoughtSignature ||
|
|
263
|
+
part.providerMetadata?.custom?.thoughtSignature;
|
|
264
|
+
if (sig && typeof sig === "string") {
|
|
265
|
+
thoughtSignature = sig;
|
|
266
|
+
}
|
|
267
|
+
} else if (part.type === "tool-call") {
|
|
268
|
+
let args: Record<string, unknown> = {};
|
|
269
|
+
if (typeof part.input === "string") {
|
|
270
|
+
try {
|
|
271
|
+
args = JSON.parse(part.input);
|
|
272
|
+
} catch {
|
|
273
|
+
args = { raw: part.input };
|
|
274
|
+
}
|
|
275
|
+
} else if (typeof part.input === "object" && part.input !== null) {
|
|
276
|
+
args = part.input as Record<string, unknown>;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const sig =
|
|
280
|
+
part.providerMetadata?.google?.thoughtSignature ||
|
|
281
|
+
part.providerMetadata?.custom?.thoughtSignature;
|
|
282
|
+
|
|
283
|
+
toolCalls.push({
|
|
284
|
+
id: part.toolCallId || `call_${Math.random().toString(36).slice(2, 9)}`,
|
|
285
|
+
name: part.toolName,
|
|
286
|
+
arguments: args,
|
|
287
|
+
rawArguments: typeof part.input === "string" ? part.input : JSON.stringify(part.input || {}),
|
|
288
|
+
thoughtSignature: typeof sig === "string" ? sig : undefined,
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
if (sig && !thoughtSignature) {
|
|
292
|
+
thoughtSignature = sig;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Fallback / extraction from raw response body if Vercel AI SDK parts omitted custom metadata
|
|
299
|
+
const rawBody = aiResult.response?.body;
|
|
300
|
+
if (rawBody && typeof rawBody === "object") {
|
|
301
|
+
const rawChoice = rawBody.choices?.[0];
|
|
302
|
+
const rawMsg = rawChoice?.message;
|
|
303
|
+
if (!thinking && rawMsg?.reasoning_content) {
|
|
304
|
+
thinking = rawMsg.reasoning_content;
|
|
305
|
+
}
|
|
306
|
+
if (rawMsg?.tool_calls && Array.isArray(rawMsg.tool_calls)) {
|
|
307
|
+
for (let i = 0; i < rawMsg.tool_calls.length; i++) {
|
|
308
|
+
const rawTc = rawMsg.tool_calls[i];
|
|
309
|
+
const extraSig = rawTc?.extra_content?.google?.thought_signature;
|
|
310
|
+
if (extraSig && toolCalls[i]) {
|
|
311
|
+
toolCalls[i]!.thoughtSignature = extraSig;
|
|
312
|
+
if (!thoughtSignature) thoughtSignature = extraSig;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const usage = extractAiSdkUsage(aiResult.usage ?? rawBody?.usage);
|
|
319
|
+
const finishReason =
|
|
320
|
+
typeof aiResult.finishReason === "object" && aiResult.finishReason !== null
|
|
321
|
+
? aiResult.finishReason.raw || aiResult.finishReason.unified || "stop"
|
|
322
|
+
: String(aiResult.finishReason || "stop");
|
|
323
|
+
|
|
324
|
+
const responseId = aiResult.response?.id || rawBody?.id;
|
|
325
|
+
|
|
326
|
+
return {
|
|
327
|
+
text,
|
|
328
|
+
thinking,
|
|
329
|
+
thoughtSignature,
|
|
330
|
+
toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
|
|
331
|
+
usage,
|
|
332
|
+
finishReason,
|
|
333
|
+
responseId,
|
|
334
|
+
model: modelId,
|
|
335
|
+
provider: providerId as any,
|
|
336
|
+
raw: {
|
|
337
|
+
request: aiResult.request,
|
|
338
|
+
response: aiResult.response,
|
|
339
|
+
},
|
|
340
|
+
durationMs,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import type { ProviderContext } from "../types/message.ts";
|
|
2
|
+
import { getModelFromCatalog } from "../models/catalog.ts";
|
|
3
|
+
|
|
4
|
+
const CONCISE = "__agentAccelConcise";
|
|
5
|
+
|
|
6
|
+
function readStatus(error: any): number | undefined {
|
|
7
|
+
const s =
|
|
8
|
+
error?.statusCode ??
|
|
9
|
+
error?.status ??
|
|
10
|
+
error?.response?.status ??
|
|
11
|
+
error?.lastError?.statusCode ??
|
|
12
|
+
error?.lastError?.status;
|
|
13
|
+
const n = Number(s);
|
|
14
|
+
return Number.isFinite(n) && n > 0 ? n : undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function readUrl(error: any): string {
|
|
18
|
+
const raw =
|
|
19
|
+
(typeof error?.url === "string" && error.url) ||
|
|
20
|
+
(typeof error?.request?.url === "string" && error.request.url) ||
|
|
21
|
+
"";
|
|
22
|
+
return raw.split("?")[0];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function readProviderMessage(error: any): string {
|
|
26
|
+
const data = error?.data;
|
|
27
|
+
if (typeof data?.error?.message === "string" && data.error.message.trim()) return data.error.message.trim();
|
|
28
|
+
if (typeof data?.error === "string" && data.error.trim()) return data.error.trim();
|
|
29
|
+
if (typeof data?.message === "string" && data.message.trim()) return data.message.trim();
|
|
30
|
+
const body = error?.responseBody;
|
|
31
|
+
if (typeof body === "string" && body.trim()) {
|
|
32
|
+
try {
|
|
33
|
+
const parsed = JSON.parse(body);
|
|
34
|
+
if (typeof parsed?.error?.message === "string" && parsed.error.message.trim()) return parsed.error.message.trim();
|
|
35
|
+
if (typeof parsed?.message === "string" && parsed.message.trim()) return parsed.message.trim();
|
|
36
|
+
} catch {}
|
|
37
|
+
if (body.length <= 300) return body.trim();
|
|
38
|
+
}
|
|
39
|
+
return String(error?.message ?? error).slice(0, 300);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function displayModel(providerId: string, modelId: string): string {
|
|
43
|
+
if (modelId.toLowerCase().startsWith(`${providerId.toLowerCase()}/`)) return modelId;
|
|
44
|
+
return `${providerId}/${modelId}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Collapses a Vercel `APICallError` (which carries the full request/response
|
|
49
|
+
* dump as enumerable props) into a one-line actionable error. Raw details stay
|
|
50
|
+
* available but non-enumerable, so runtime dumps stay small.
|
|
51
|
+
*/
|
|
52
|
+
export function toConciseProviderError(error: unknown, providerId: string, modelId: string): Error {
|
|
53
|
+
if (error instanceof Error && (error as any)[CONCISE]) return error;
|
|
54
|
+
const err = error as any;
|
|
55
|
+
const status = readStatus(err);
|
|
56
|
+
const url = readUrl(err);
|
|
57
|
+
const detail = readProviderMessage(err);
|
|
58
|
+
const concise = new Error(
|
|
59
|
+
`[${displayModel(providerId, modelId)}] request failed${status ? ` (${status})` : ""}: ${detail}`
|
|
60
|
+
);
|
|
61
|
+
concise.name = "AgentAccelProviderError";
|
|
62
|
+
Object.defineProperties(concise, {
|
|
63
|
+
[CONCISE]: { value: true, enumerable: false },
|
|
64
|
+
statusCode: { value: status, enumerable: false },
|
|
65
|
+
provider: { value: providerId, enumerable: false },
|
|
66
|
+
model: { value: modelId, enumerable: false },
|
|
67
|
+
url: { value: url, enumerable: false },
|
|
68
|
+
responseBody: {
|
|
69
|
+
value: typeof err?.responseBody === "string" ? err.responseBody.slice(0, 500) : undefined,
|
|
70
|
+
enumerable: false,
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
return concise;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const PART_TO_MODALITY: Record<string, "image" | "audio" | "video" | "pdf"> = {
|
|
77
|
+
image: "image",
|
|
78
|
+
audio: "audio",
|
|
79
|
+
video: "video",
|
|
80
|
+
file: "pdf",
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Fails fast — before any network call — when the catalog knows the model
|
|
85
|
+
* lacks a requested input modality (image/audio/video/document). Unknown
|
|
86
|
+
* models (custom providers, dynamic routers) are skipped: the provider
|
|
87
|
+
* verdict stands and surfaces via toConciseProviderError instead.
|
|
88
|
+
*/
|
|
89
|
+
export function assertModalitiesSupported(
|
|
90
|
+
context: ProviderContext,
|
|
91
|
+
providerId: string,
|
|
92
|
+
modelId: string
|
|
93
|
+
): void {
|
|
94
|
+
const needed = new Set<string>();
|
|
95
|
+
for (const msg of context.messages) {
|
|
96
|
+
if (!Array.isArray((msg as any)?.content)) continue;
|
|
97
|
+
for (const part of (msg as any).content) {
|
|
98
|
+
const mapped = PART_TO_MODALITY[(part as any)?.type];
|
|
99
|
+
if (mapped) needed.add(mapped);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (needed.size === 0) return;
|
|
103
|
+
let supported: readonly string[] | undefined;
|
|
104
|
+
try {
|
|
105
|
+
supported = getModelFromCatalog(providerId, modelId)?.modalities?.input;
|
|
106
|
+
} catch {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (!supported) return;
|
|
110
|
+
const missing = [...needed].filter((k) => !supported.includes(k));
|
|
111
|
+
if (missing.length === 0) return;
|
|
112
|
+
const err = new Error(
|
|
113
|
+
`[${displayModel(providerId, modelId)}] unsupported ${missing.join("+")} input (supports: ${supported.join(", ") || "text"}). Use a capable model or drop the ${missing.join("+")} part.`
|
|
114
|
+
);
|
|
115
|
+
err.name = "AgentAccelProviderError";
|
|
116
|
+
Object.defineProperties(err, {
|
|
117
|
+
[CONCISE]: { value: true, enumerable: false },
|
|
118
|
+
provider: { value: providerId, enumerable: false },
|
|
119
|
+
model: { value: modelId, enumerable: false },
|
|
120
|
+
});
|
|
121
|
+
throw err;
|
|
122
|
+
}
|