@assemble-dev/providers 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 +93 -0
- package/dist/index.cjs +1989 -0
- package/dist/index.d.cts +562 -0
- package/dist/index.d.ts +562 -0
- package/dist/index.js +1919 -0
- package/package.json +47 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1919 @@
|
|
|
1
|
+
// src/model-adapter.ts
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { JsonValueSchema } from "@assemble-dev/shared-types/json";
|
|
4
|
+
var ChatMessageRoleSchema = z.enum([
|
|
5
|
+
"system",
|
|
6
|
+
"user",
|
|
7
|
+
"assistant",
|
|
8
|
+
"tool"
|
|
9
|
+
]);
|
|
10
|
+
var MessageContentBlockSchema = z.discriminatedUnion("type", [
|
|
11
|
+
z.object({
|
|
12
|
+
type: z.literal("text"),
|
|
13
|
+
text: z.string()
|
|
14
|
+
}),
|
|
15
|
+
z.object({
|
|
16
|
+
type: z.literal("image"),
|
|
17
|
+
mediaType: z.string(),
|
|
18
|
+
dataBase64: z.string()
|
|
19
|
+
}),
|
|
20
|
+
z.object({
|
|
21
|
+
type: z.literal("document"),
|
|
22
|
+
mediaType: z.string(),
|
|
23
|
+
dataBase64: z.string()
|
|
24
|
+
})
|
|
25
|
+
]);
|
|
26
|
+
var ModelToolCallSchema = z.object({
|
|
27
|
+
id: z.string(),
|
|
28
|
+
toolName: z.string(),
|
|
29
|
+
input: JsonValueSchema
|
|
30
|
+
});
|
|
31
|
+
var ChatMessageSchema = z.object({
|
|
32
|
+
role: ChatMessageRoleSchema,
|
|
33
|
+
content: z.union([z.string(), z.array(MessageContentBlockSchema)]),
|
|
34
|
+
toolName: z.string().optional(),
|
|
35
|
+
toolCallId: z.string().optional(),
|
|
36
|
+
toolCalls: z.array(ModelToolCallSchema).optional()
|
|
37
|
+
});
|
|
38
|
+
var ChatStopReasonSchema = z.enum([
|
|
39
|
+
"end_turn",
|
|
40
|
+
"tool_use",
|
|
41
|
+
"max_tokens",
|
|
42
|
+
"stop_sequence"
|
|
43
|
+
]);
|
|
44
|
+
function extractMessageText(message) {
|
|
45
|
+
if (typeof message.content === "string") {
|
|
46
|
+
return message.content;
|
|
47
|
+
}
|
|
48
|
+
return message.content.filter((contentBlock) => contentBlock.type === "text").map((contentBlock) => contentBlock.text).join("");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// src/openai-adapter.ts
|
|
52
|
+
import { z as z3 } from "zod";
|
|
53
|
+
import { AppErrorCode as AppErrorCode7 } from "@assemble-dev/shared-types/errors";
|
|
54
|
+
import { JsonValueSchema as JsonValueSchema3 } from "@assemble-dev/shared-types/json";
|
|
55
|
+
import { AppError as AppError8 } from "@assemble-dev/shared-utils/errors";
|
|
56
|
+
|
|
57
|
+
// src/openai-request-mapping.ts
|
|
58
|
+
import { AppErrorCode } from "@assemble-dev/shared-types/errors";
|
|
59
|
+
import { AppError } from "@assemble-dev/shared-utils/errors";
|
|
60
|
+
function buildOpenAiRequestBody(model, input) {
|
|
61
|
+
const requestBody = {
|
|
62
|
+
model,
|
|
63
|
+
messages: input.messages.map(mapChatMessageToOpenAiRequestMessage)
|
|
64
|
+
};
|
|
65
|
+
if (input.temperature !== void 0) {
|
|
66
|
+
requestBody.temperature = input.temperature;
|
|
67
|
+
}
|
|
68
|
+
if (input.maxOutputTokens !== void 0) {
|
|
69
|
+
requestBody.max_tokens = input.maxOutputTokens;
|
|
70
|
+
}
|
|
71
|
+
if (input.requireJsonObjectResponse) {
|
|
72
|
+
requestBody.response_format = { type: "json_object" };
|
|
73
|
+
}
|
|
74
|
+
if (input.tools !== void 0 && input.tools.length > 0) {
|
|
75
|
+
requestBody.tools = input.tools.map(mapModelToolDefinitionToOpenAiTool);
|
|
76
|
+
}
|
|
77
|
+
if (input.toolChoice !== void 0) {
|
|
78
|
+
requestBody.tool_choice = mapModelToolChoiceToOpenAiToolChoice(
|
|
79
|
+
input.toolChoice
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
return requestBody;
|
|
83
|
+
}
|
|
84
|
+
function mapModelToolDefinitionToOpenAiTool(tool) {
|
|
85
|
+
return {
|
|
86
|
+
type: "function",
|
|
87
|
+
function: {
|
|
88
|
+
name: tool.name,
|
|
89
|
+
description: tool.description,
|
|
90
|
+
parameters: tool.inputJsonSchema
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function mapModelToolChoiceToOpenAiToolChoice(toolChoice) {
|
|
95
|
+
if (toolChoice === "auto" || toolChoice === "required" || toolChoice === "none") {
|
|
96
|
+
return toolChoice;
|
|
97
|
+
}
|
|
98
|
+
return { type: "function", function: { name: toolChoice.toolName } };
|
|
99
|
+
}
|
|
100
|
+
function mapChatMessageToOpenAiRequestMessage(message) {
|
|
101
|
+
if (message.role === "tool" && message.toolCallId !== void 0) {
|
|
102
|
+
return {
|
|
103
|
+
role: "tool",
|
|
104
|
+
tool_call_id: message.toolCallId,
|
|
105
|
+
content: extractMessageText(message)
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
if (message.role === "assistant" && message.toolCalls !== void 0 && message.toolCalls.length > 0) {
|
|
109
|
+
return mapAssistantToolCallMessageToOpenAiRequestMessage(message);
|
|
110
|
+
}
|
|
111
|
+
const requestMessage = {
|
|
112
|
+
role: message.role,
|
|
113
|
+
content: mapMessageContentToOpenAi(message)
|
|
114
|
+
};
|
|
115
|
+
if (message.toolName !== void 0) {
|
|
116
|
+
requestMessage.name = message.toolName;
|
|
117
|
+
}
|
|
118
|
+
return requestMessage;
|
|
119
|
+
}
|
|
120
|
+
function mapAssistantToolCallMessageToOpenAiRequestMessage(message) {
|
|
121
|
+
const assistantText = extractMessageText(message);
|
|
122
|
+
const toolCalls = message.toolCalls ?? [];
|
|
123
|
+
return {
|
|
124
|
+
role: "assistant",
|
|
125
|
+
content: assistantText.length > 0 ? assistantText : null,
|
|
126
|
+
tool_calls: toolCalls.map(mapModelToolCallToOpenAiRequestToolCall)
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
function mapModelToolCallToOpenAiRequestToolCall(toolCall) {
|
|
130
|
+
return {
|
|
131
|
+
id: toolCall.id,
|
|
132
|
+
type: "function",
|
|
133
|
+
function: {
|
|
134
|
+
name: toolCall.toolName,
|
|
135
|
+
arguments: JSON.stringify(toolCall.input)
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
function mapMessageContentToOpenAi(message) {
|
|
140
|
+
if (typeof message.content === "string") {
|
|
141
|
+
return message.content;
|
|
142
|
+
}
|
|
143
|
+
return message.content.map(mapMessageContentBlockToOpenAiContentPart);
|
|
144
|
+
}
|
|
145
|
+
function mapMessageContentBlockToOpenAiContentPart(contentBlock) {
|
|
146
|
+
if (contentBlock.type === "text") {
|
|
147
|
+
return { type: "text", text: contentBlock.text };
|
|
148
|
+
}
|
|
149
|
+
if (contentBlock.type === "image") {
|
|
150
|
+
return {
|
|
151
|
+
type: "image_url",
|
|
152
|
+
image_url: {
|
|
153
|
+
url: `data:${contentBlock.mediaType};base64,${contentBlock.dataBase64}`
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
throw new AppError({
|
|
158
|
+
code: AppErrorCode.ValidationFailed,
|
|
159
|
+
message: "OpenAI adapter does not support document content blocks in chat messages",
|
|
160
|
+
statusCode: 400
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// src/parse-structured-json.ts
|
|
165
|
+
import { AppErrorCode as AppErrorCode2 } from "@assemble-dev/shared-types/errors";
|
|
166
|
+
import { JsonValueSchema as JsonValueSchema2 } from "@assemble-dev/shared-types/json";
|
|
167
|
+
import { AppError as AppError2 } from "@assemble-dev/shared-utils/errors";
|
|
168
|
+
var markdownCodeFencePattern = /^```[A-Za-z]*\s*\n?([\s\S]*?)\n?```$/;
|
|
169
|
+
function parseStructuredJsonText(text, schema) {
|
|
170
|
+
return schema.parse(parseJsonText(stripMarkdownCodeFences(text)));
|
|
171
|
+
}
|
|
172
|
+
function stripMarkdownCodeFences(text) {
|
|
173
|
+
const trimmedText = text.trim();
|
|
174
|
+
const fenceMatch = markdownCodeFencePattern.exec(trimmedText);
|
|
175
|
+
if (fenceMatch?.[1] === void 0) {
|
|
176
|
+
return trimmedText;
|
|
177
|
+
}
|
|
178
|
+
return fenceMatch[1].trim();
|
|
179
|
+
}
|
|
180
|
+
function parseJsonText(text) {
|
|
181
|
+
try {
|
|
182
|
+
return JsonValueSchema2.parse(JSON.parse(text));
|
|
183
|
+
} catch (error) {
|
|
184
|
+
throw new AppError2({
|
|
185
|
+
code: AppErrorCode2.ValidationFailed,
|
|
186
|
+
message: "Structured output was not valid JSON",
|
|
187
|
+
statusCode: 422,
|
|
188
|
+
cause: error instanceof Error ? error : void 0
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// src/provider-api-key.ts
|
|
194
|
+
import { z as z2 } from "zod";
|
|
195
|
+
import { AppErrorCode as AppErrorCode3 } from "@assemble-dev/shared-types/errors";
|
|
196
|
+
import { AppError as AppError3 } from "@assemble-dev/shared-utils/errors";
|
|
197
|
+
import {
|
|
198
|
+
buildNamedApiKeyEnvVarName,
|
|
199
|
+
expectedProviderApiKeyNameFormat,
|
|
200
|
+
validateProviderApiKeyName
|
|
201
|
+
} from "@assemble-dev/shared-utils/provider-keys";
|
|
202
|
+
function resolveProviderApiKey(input) {
|
|
203
|
+
if (input.apiKey !== void 0 && input.apiKey.length > 0) {
|
|
204
|
+
return input.apiKey;
|
|
205
|
+
}
|
|
206
|
+
if (input.apiKeyName !== void 0) {
|
|
207
|
+
return resolveNamedProviderApiKeyFromEnv(input, input.apiKeyName);
|
|
208
|
+
}
|
|
209
|
+
return resolveBaseProviderApiKeyFromEnv(input);
|
|
210
|
+
}
|
|
211
|
+
function resolveNamedProviderApiKeyFromEnv(input, apiKeyName) {
|
|
212
|
+
if (!validateProviderApiKeyName(apiKeyName)) {
|
|
213
|
+
throw new AppError3({
|
|
214
|
+
code: AppErrorCode3.BadRequest,
|
|
215
|
+
message: `Invalid ${input.providerLabel} apiKeyName "${apiKeyName}". Expected ${expectedProviderApiKeyNameFormat}.`,
|
|
216
|
+
statusCode: 400
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
const envVarName = buildNamedApiKeyEnvVarName(input.baseEnvVarName, apiKeyName);
|
|
220
|
+
const envApiKey = readEnvironmentVariable(envVarName);
|
|
221
|
+
if (envApiKey === void 0 || envApiKey.length === 0) {
|
|
222
|
+
throw new AppError3({
|
|
223
|
+
code: AppErrorCode3.Unauthorized,
|
|
224
|
+
message: `${input.providerLabel} API key named "${apiKeyName}" is missing. Set the ${envVarName} environment variable or pass apiKey to ${input.adapterFunctionName}.`,
|
|
225
|
+
statusCode: 401
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
return envApiKey;
|
|
229
|
+
}
|
|
230
|
+
function resolveBaseProviderApiKeyFromEnv(input) {
|
|
231
|
+
const envApiKey = readEnvironmentVariable(input.baseEnvVarName);
|
|
232
|
+
if (envApiKey === void 0 || envApiKey.length === 0) {
|
|
233
|
+
throw new AppError3({
|
|
234
|
+
code: AppErrorCode3.Unauthorized,
|
|
235
|
+
message: `${input.providerLabel} API key is missing. Pass apiKey to ${input.adapterFunctionName} or set the ${input.baseEnvVarName} environment variable.`,
|
|
236
|
+
statusCode: 401
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
return envApiKey;
|
|
240
|
+
}
|
|
241
|
+
function readEnvironmentVariable(envVarName) {
|
|
242
|
+
const parsedEnv = z2.object({ [envVarName]: z2.string().optional() }).parse(process.env);
|
|
243
|
+
return parsedEnv[envVarName];
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// src/provider-cancellation.ts
|
|
247
|
+
import { AppErrorCode as AppErrorCode4 } from "@assemble-dev/shared-types/errors";
|
|
248
|
+
import { AppError as AppError4 } from "@assemble-dev/shared-utils/errors";
|
|
249
|
+
function isAbortError(error) {
|
|
250
|
+
return error.name === "AbortError";
|
|
251
|
+
}
|
|
252
|
+
function buildProviderCanceledError(providerLabel) {
|
|
253
|
+
return new AppError4({
|
|
254
|
+
code: AppErrorCode4.Canceled,
|
|
255
|
+
message: `${providerLabel} request was canceled`,
|
|
256
|
+
statusCode: 499
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
async function executeAbortableProviderFetch(providerLabel, performFetch) {
|
|
260
|
+
try {
|
|
261
|
+
return await performFetch();
|
|
262
|
+
} catch (error) {
|
|
263
|
+
if (error instanceof Error && isAbortError(error)) {
|
|
264
|
+
throw buildProviderCanceledError(providerLabel);
|
|
265
|
+
}
|
|
266
|
+
throw error;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// src/provider-fallback.ts
|
|
271
|
+
import { AppError as AppError6 } from "@assemble-dev/shared-utils/errors";
|
|
272
|
+
|
|
273
|
+
// src/provider-retry.ts
|
|
274
|
+
import { AppErrorCode as AppErrorCode5 } from "@assemble-dev/shared-types/errors";
|
|
275
|
+
import { AppError as AppError5 } from "@assemble-dev/shared-utils/errors";
|
|
276
|
+
var defaultProviderRetryMaxAttempts = 3;
|
|
277
|
+
var defaultProviderRetryInitialDelayMs = 500;
|
|
278
|
+
var defaultProviderRetryMaxDelayMs = 8e3;
|
|
279
|
+
var retryableAppErrorCodes = /* @__PURE__ */ new Set([
|
|
280
|
+
AppErrorCode5.RateLimited,
|
|
281
|
+
AppErrorCode5.ServiceUnavailable,
|
|
282
|
+
AppErrorCode5.InternalServerError
|
|
283
|
+
]);
|
|
284
|
+
function isRetryableProviderAppError(appError) {
|
|
285
|
+
return retryableAppErrorCodes.has(appError.code);
|
|
286
|
+
}
|
|
287
|
+
async function executeProviderCallWithRetry(retryOptions, executeCall, waitFor) {
|
|
288
|
+
if (retryOptions === void 0) {
|
|
289
|
+
return await executeCall();
|
|
290
|
+
}
|
|
291
|
+
const maxAttempts = retryOptions.maxAttempts ?? defaultProviderRetryMaxAttempts;
|
|
292
|
+
const waitForRetryDelay = waitFor ?? waitForMilliseconds;
|
|
293
|
+
for (let attemptNumber = 1; ; attemptNumber += 1) {
|
|
294
|
+
try {
|
|
295
|
+
return await executeCall();
|
|
296
|
+
} catch (error) {
|
|
297
|
+
if (error instanceof AppError5 && !isRetryableProviderAppError(error)) {
|
|
298
|
+
throw error;
|
|
299
|
+
}
|
|
300
|
+
if (error instanceof Error && isAbortError(error)) {
|
|
301
|
+
throw error;
|
|
302
|
+
}
|
|
303
|
+
if (attemptNumber >= maxAttempts) {
|
|
304
|
+
throw error;
|
|
305
|
+
}
|
|
306
|
+
await waitForRetryDelay(
|
|
307
|
+
calculateProviderRetryDelayMs(retryOptions, attemptNumber)
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
function calculateProviderRetryDelayMs(retryOptions, attemptNumber) {
|
|
313
|
+
const initialDelayMs = retryOptions.initialDelayMs ?? defaultProviderRetryInitialDelayMs;
|
|
314
|
+
const maxDelayMs = retryOptions.maxDelayMs ?? defaultProviderRetryMaxDelayMs;
|
|
315
|
+
return Math.min(initialDelayMs * 2 ** (attemptNumber - 1), maxDelayMs);
|
|
316
|
+
}
|
|
317
|
+
async function waitForMilliseconds(ms) {
|
|
318
|
+
await new Promise((resolve) => {
|
|
319
|
+
setTimeout(resolve, ms);
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// src/provider-fallback.ts
|
|
324
|
+
async function executeProviderCallWithModelFallback(fallbackModel, executePrimaryCall, executeFallbackCall) {
|
|
325
|
+
try {
|
|
326
|
+
return await executePrimaryCall();
|
|
327
|
+
} catch (error) {
|
|
328
|
+
if (fallbackModel === void 0) {
|
|
329
|
+
throw error;
|
|
330
|
+
}
|
|
331
|
+
if (error instanceof AppError6 && !isRetryableProviderAppError(error)) {
|
|
332
|
+
throw error;
|
|
333
|
+
}
|
|
334
|
+
if (error instanceof Error && isAbortError(error)) {
|
|
335
|
+
throw error;
|
|
336
|
+
}
|
|
337
|
+
return await executeFallbackCall(fallbackModel);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// src/provider-status-error.ts
|
|
342
|
+
import { AppErrorCode as AppErrorCode6 } from "@assemble-dev/shared-types/errors";
|
|
343
|
+
import { AppError as AppError7 } from "@assemble-dev/shared-utils/errors";
|
|
344
|
+
function buildProviderStatusError(providerLabel, providerStatus) {
|
|
345
|
+
const code = mapProviderStatusToAppErrorCode(providerStatus);
|
|
346
|
+
return new AppError7({
|
|
347
|
+
code,
|
|
348
|
+
message: `${providerLabel} request failed with status ${String(providerStatus)}`,
|
|
349
|
+
statusCode: mapAppErrorCodeToStatusCode(code),
|
|
350
|
+
details: { providerStatus }
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
function buildProviderInvalidResponseError(providerLabel, expectedShapeDescription) {
|
|
354
|
+
return new AppError7({
|
|
355
|
+
code: AppErrorCode6.ValidationFailed,
|
|
356
|
+
message: `${providerLabel} response did not match the expected ${expectedShapeDescription} shape`,
|
|
357
|
+
statusCode: 502
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
function mapProviderStatusToAppErrorCode(providerStatus) {
|
|
361
|
+
if (providerStatus === 400) {
|
|
362
|
+
return AppErrorCode6.BadRequest;
|
|
363
|
+
}
|
|
364
|
+
if (providerStatus === 401) {
|
|
365
|
+
return AppErrorCode6.Unauthorized;
|
|
366
|
+
}
|
|
367
|
+
if (providerStatus === 403) {
|
|
368
|
+
return AppErrorCode6.Forbidden;
|
|
369
|
+
}
|
|
370
|
+
if (providerStatus === 404) {
|
|
371
|
+
return AppErrorCode6.NotFound;
|
|
372
|
+
}
|
|
373
|
+
if (providerStatus === 429) {
|
|
374
|
+
return AppErrorCode6.RateLimited;
|
|
375
|
+
}
|
|
376
|
+
return AppErrorCode6.InternalServerError;
|
|
377
|
+
}
|
|
378
|
+
function mapAppErrorCodeToStatusCode(code) {
|
|
379
|
+
if (code === AppErrorCode6.BadRequest) {
|
|
380
|
+
return 400;
|
|
381
|
+
}
|
|
382
|
+
if (code === AppErrorCode6.Unauthorized) {
|
|
383
|
+
return 401;
|
|
384
|
+
}
|
|
385
|
+
if (code === AppErrorCode6.Forbidden) {
|
|
386
|
+
return 403;
|
|
387
|
+
}
|
|
388
|
+
if (code === AppErrorCode6.NotFound) {
|
|
389
|
+
return 404;
|
|
390
|
+
}
|
|
391
|
+
if (code === AppErrorCode6.RateLimited) {
|
|
392
|
+
return 429;
|
|
393
|
+
}
|
|
394
|
+
return 500;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// src/openai-adapter.ts
|
|
398
|
+
var defaultOpenAiBaseUrl = "https://api.openai.com/v1";
|
|
399
|
+
var OpenAiResponseToolCallSchema = z3.object({
|
|
400
|
+
id: z3.string(),
|
|
401
|
+
function: z3.object({
|
|
402
|
+
name: z3.string(),
|
|
403
|
+
arguments: z3.string()
|
|
404
|
+
})
|
|
405
|
+
});
|
|
406
|
+
var OpenAiChatCompletionResponseSchema = z3.object({
|
|
407
|
+
choices: z3.array(
|
|
408
|
+
z3.object({
|
|
409
|
+
message: z3.object({
|
|
410
|
+
content: z3.string().nullable(),
|
|
411
|
+
tool_calls: z3.array(OpenAiResponseToolCallSchema).optional()
|
|
412
|
+
}),
|
|
413
|
+
finish_reason: z3.string().nullable().optional()
|
|
414
|
+
})
|
|
415
|
+
).min(1),
|
|
416
|
+
usage: z3.object({
|
|
417
|
+
prompt_tokens: z3.number().int().nonnegative(),
|
|
418
|
+
completion_tokens: z3.number().int().nonnegative(),
|
|
419
|
+
total_tokens: z3.number().int().nonnegative()
|
|
420
|
+
})
|
|
421
|
+
});
|
|
422
|
+
var structuredOutputSystemMessage = {
|
|
423
|
+
role: "system",
|
|
424
|
+
content: "Respond with a single JSON object that satisfies the caller's requested structure. Output only valid JSON with no surrounding text."
|
|
425
|
+
};
|
|
426
|
+
function openai(options) {
|
|
427
|
+
return {
|
|
428
|
+
name: `openai:${options.model}`,
|
|
429
|
+
async generateChatCompletion(request) {
|
|
430
|
+
return await executeOpenAiAdapterCall(options, {
|
|
431
|
+
messages: request.messages,
|
|
432
|
+
temperature: request.temperature ?? options.temperature,
|
|
433
|
+
maxOutputTokens: request.maxOutputTokens ?? options.maxOutputTokens,
|
|
434
|
+
requireJsonObjectResponse: false,
|
|
435
|
+
tools: request.tools,
|
|
436
|
+
toolChoice: request.toolChoice,
|
|
437
|
+
abortSignal: request.abortSignal
|
|
438
|
+
});
|
|
439
|
+
},
|
|
440
|
+
async generateStructuredOutput(request) {
|
|
441
|
+
const completion = await executeOpenAiAdapterCall(options, {
|
|
442
|
+
messages: [structuredOutputSystemMessage, ...request.messages],
|
|
443
|
+
temperature: request.temperature ?? options.temperature,
|
|
444
|
+
maxOutputTokens: request.maxOutputTokens ?? options.maxOutputTokens,
|
|
445
|
+
requireJsonObjectResponse: true,
|
|
446
|
+
abortSignal: request.abortSignal
|
|
447
|
+
});
|
|
448
|
+
return {
|
|
449
|
+
value: parseStructuredJsonText(completion.content, request.schema),
|
|
450
|
+
tokenUsage: completion.tokenUsage,
|
|
451
|
+
latencyMs: completion.latencyMs
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
async function executeOpenAiAdapterCall(options, input) {
|
|
457
|
+
return await executeProviderCallWithModelFallback(
|
|
458
|
+
options.fallbackModel,
|
|
459
|
+
async () => await executeProviderCallWithRetry(
|
|
460
|
+
options.retry,
|
|
461
|
+
async () => await requestOpenAiChatCompletion(options, input)
|
|
462
|
+
),
|
|
463
|
+
async (fallbackModelId) => await requestOpenAiChatCompletion(
|
|
464
|
+
{ ...options, model: fallbackModelId },
|
|
465
|
+
input
|
|
466
|
+
)
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
async function requestOpenAiChatCompletion(options, input) {
|
|
470
|
+
const apiKey = resolveProviderApiKey({
|
|
471
|
+
providerLabel: "OpenAI",
|
|
472
|
+
adapterFunctionName: "openai()",
|
|
473
|
+
baseEnvVarName: "OPENAI_API_KEY",
|
|
474
|
+
apiKey: options.apiKey,
|
|
475
|
+
apiKeyName: options.apiKeyName
|
|
476
|
+
});
|
|
477
|
+
const fetchImplementation = options.fetchImplementation ?? fetch;
|
|
478
|
+
const baseUrl = options.baseURL ?? defaultOpenAiBaseUrl;
|
|
479
|
+
const requestBody = buildOpenAiRequestBody(options.model, input);
|
|
480
|
+
const startedAt = Date.now();
|
|
481
|
+
const response = await executeAbortableProviderFetch(
|
|
482
|
+
"OpenAI",
|
|
483
|
+
async () => await fetchImplementation(`${baseUrl}/chat/completions`, {
|
|
484
|
+
method: "POST",
|
|
485
|
+
headers: {
|
|
486
|
+
"Content-Type": "application/json",
|
|
487
|
+
Authorization: `Bearer ${apiKey}`
|
|
488
|
+
},
|
|
489
|
+
body: JSON.stringify(requestBody),
|
|
490
|
+
signal: input.abortSignal
|
|
491
|
+
})
|
|
492
|
+
);
|
|
493
|
+
const latencyMs = Date.now() - startedAt;
|
|
494
|
+
if (!response.ok) {
|
|
495
|
+
throw buildProviderStatusError("OpenAI", response.status);
|
|
496
|
+
}
|
|
497
|
+
const parseResult = OpenAiChatCompletionResponseSchema.safeParse(
|
|
498
|
+
await response.json()
|
|
499
|
+
);
|
|
500
|
+
if (!parseResult.success) {
|
|
501
|
+
throw buildProviderInvalidResponseError("OpenAI", "chat completion");
|
|
502
|
+
}
|
|
503
|
+
return mapOpenAiResponseToChatCompletionResult(parseResult.data, latencyMs);
|
|
504
|
+
}
|
|
505
|
+
function mapOpenAiResponseToChatCompletionResult(response, latencyMs) {
|
|
506
|
+
const toolCalls = extractOpenAiToolCalls(response);
|
|
507
|
+
const result = {
|
|
508
|
+
content: extractOpenAiMessageContent(response, toolCalls.length > 0),
|
|
509
|
+
tokenUsage: mapOpenAiUsageToTokenUsage(response),
|
|
510
|
+
latencyMs
|
|
511
|
+
};
|
|
512
|
+
const finishReason = response.choices[0]?.finish_reason;
|
|
513
|
+
if (toolCalls.length > 0) {
|
|
514
|
+
result.toolCalls = toolCalls;
|
|
515
|
+
}
|
|
516
|
+
if (finishReason !== null && finishReason !== void 0) {
|
|
517
|
+
result.stopReason = normalizeOpenAiFinishReason(finishReason);
|
|
518
|
+
}
|
|
519
|
+
return result;
|
|
520
|
+
}
|
|
521
|
+
function extractOpenAiToolCalls(response) {
|
|
522
|
+
const responseToolCalls = response.choices[0]?.message.tool_calls ?? [];
|
|
523
|
+
return responseToolCalls.map((responseToolCall) => ({
|
|
524
|
+
id: responseToolCall.id,
|
|
525
|
+
toolName: responseToolCall.function.name,
|
|
526
|
+
input: parseOpenAiToolArgumentsJson(responseToolCall.function.arguments)
|
|
527
|
+
}));
|
|
528
|
+
}
|
|
529
|
+
function parseOpenAiToolArgumentsJson(argumentsJsonText) {
|
|
530
|
+
if (argumentsJsonText.trim().length === 0) {
|
|
531
|
+
return {};
|
|
532
|
+
}
|
|
533
|
+
try {
|
|
534
|
+
return JsonValueSchema3.parse(JSON.parse(argumentsJsonText));
|
|
535
|
+
} catch (error) {
|
|
536
|
+
throw new AppError8({
|
|
537
|
+
code: AppErrorCode7.ValidationFailed,
|
|
538
|
+
message: "OpenAI tool call arguments were not valid JSON",
|
|
539
|
+
statusCode: 502,
|
|
540
|
+
cause: error instanceof Error ? error : void 0
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
function normalizeOpenAiFinishReason(finishReason) {
|
|
545
|
+
if (finishReason === "tool_calls") {
|
|
546
|
+
return "tool_use";
|
|
547
|
+
}
|
|
548
|
+
if (finishReason === "length") {
|
|
549
|
+
return "max_tokens";
|
|
550
|
+
}
|
|
551
|
+
return "end_turn";
|
|
552
|
+
}
|
|
553
|
+
function extractOpenAiMessageContent(response, allowEmptyContent) {
|
|
554
|
+
const content = response.choices[0]?.message.content;
|
|
555
|
+
if (content === void 0 || content === null) {
|
|
556
|
+
if (allowEmptyContent) {
|
|
557
|
+
return "";
|
|
558
|
+
}
|
|
559
|
+
throw new AppError8({
|
|
560
|
+
code: AppErrorCode7.ValidationFailed,
|
|
561
|
+
message: "OpenAI response contained no message content",
|
|
562
|
+
statusCode: 502
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
return content;
|
|
566
|
+
}
|
|
567
|
+
function mapOpenAiUsageToTokenUsage(response) {
|
|
568
|
+
return {
|
|
569
|
+
inputTokens: response.usage.prompt_tokens,
|
|
570
|
+
outputTokens: response.usage.completion_tokens,
|
|
571
|
+
totalTokens: response.usage.total_tokens
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// src/anthropic-request-mapping.ts
|
|
576
|
+
function buildAnthropicRequestBody(settings, input) {
|
|
577
|
+
const requestBody = {
|
|
578
|
+
model: settings.model,
|
|
579
|
+
max_tokens: input.maxOutputTokens ?? settings.defaultMaxOutputTokens,
|
|
580
|
+
messages: input.messages.filter((message) => message.role !== "system").map(mapChatMessageToAnthropicRequestMessage)
|
|
581
|
+
};
|
|
582
|
+
applyAnthropicSystemField(requestBody, settings, input);
|
|
583
|
+
applyAnthropicToolFields(requestBody, settings, input);
|
|
584
|
+
if (input.temperature !== void 0) {
|
|
585
|
+
requestBody.temperature = input.temperature;
|
|
586
|
+
}
|
|
587
|
+
if (settings.thinking !== void 0) {
|
|
588
|
+
requestBody.thinking = {
|
|
589
|
+
type: "enabled",
|
|
590
|
+
budget_tokens: settings.thinking.budgetTokens
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
return requestBody;
|
|
594
|
+
}
|
|
595
|
+
function applyAnthropicSystemField(requestBody, settings, input) {
|
|
596
|
+
const systemText = joinSystemMessageText(input.messages);
|
|
597
|
+
if (systemText.length === 0) {
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
if (settings.promptCaching !== "auto") {
|
|
601
|
+
requestBody.system = systemText;
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
requestBody.system = [
|
|
605
|
+
{ type: "text", text: systemText, cache_control: { type: "ephemeral" } }
|
|
606
|
+
];
|
|
607
|
+
}
|
|
608
|
+
function applyAnthropicToolFields(requestBody, settings, input) {
|
|
609
|
+
if (input.tools !== void 0 && input.tools.length > 0) {
|
|
610
|
+
requestBody.tools = mapModelToolDefinitionsToAnthropicTools(
|
|
611
|
+
input.tools,
|
|
612
|
+
settings.promptCaching
|
|
613
|
+
);
|
|
614
|
+
}
|
|
615
|
+
if (input.toolChoice !== void 0) {
|
|
616
|
+
requestBody.tool_choice = mapModelToolChoiceToAnthropicToolChoice(
|
|
617
|
+
input.toolChoice
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
function mapModelToolDefinitionsToAnthropicTools(tools, promptCaching) {
|
|
622
|
+
return tools.map((tool, toolIndex) => {
|
|
623
|
+
const anthropicTool = {
|
|
624
|
+
name: tool.name,
|
|
625
|
+
description: tool.description,
|
|
626
|
+
input_schema: tool.inputJsonSchema
|
|
627
|
+
};
|
|
628
|
+
if (promptCaching === "auto" && toolIndex === tools.length - 1) {
|
|
629
|
+
anthropicTool.cache_control = { type: "ephemeral" };
|
|
630
|
+
}
|
|
631
|
+
return anthropicTool;
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
function mapModelToolChoiceToAnthropicToolChoice(toolChoice) {
|
|
635
|
+
if (toolChoice === "auto") {
|
|
636
|
+
return { type: "auto" };
|
|
637
|
+
}
|
|
638
|
+
if (toolChoice === "required") {
|
|
639
|
+
return { type: "any" };
|
|
640
|
+
}
|
|
641
|
+
if (toolChoice === "none") {
|
|
642
|
+
return { type: "none" };
|
|
643
|
+
}
|
|
644
|
+
return { type: "tool", name: toolChoice.toolName };
|
|
645
|
+
}
|
|
646
|
+
function joinSystemMessageText(messages) {
|
|
647
|
+
return messages.filter((message) => message.role === "system").map(extractMessageText).join("\n\n");
|
|
648
|
+
}
|
|
649
|
+
function mapChatMessageToAnthropicRequestMessage(message) {
|
|
650
|
+
if (message.role === "assistant") {
|
|
651
|
+
return mapAssistantMessageToAnthropicRequestMessage(message);
|
|
652
|
+
}
|
|
653
|
+
if (message.role === "tool") {
|
|
654
|
+
return mapToolMessageToAnthropicRequestMessage(message);
|
|
655
|
+
}
|
|
656
|
+
return { role: "user", content: mapMessageContentToAnthropic(message) };
|
|
657
|
+
}
|
|
658
|
+
function mapAssistantMessageToAnthropicRequestMessage(message) {
|
|
659
|
+
if (message.toolCalls === void 0 || message.toolCalls.length === 0) {
|
|
660
|
+
return { role: "assistant", content: mapMessageContentToAnthropic(message) };
|
|
661
|
+
}
|
|
662
|
+
const contentBlocks = [];
|
|
663
|
+
const assistantText = extractMessageText(message);
|
|
664
|
+
if (assistantText.length > 0) {
|
|
665
|
+
contentBlocks.push({ type: "text", text: assistantText });
|
|
666
|
+
}
|
|
667
|
+
for (const toolCall of message.toolCalls) {
|
|
668
|
+
contentBlocks.push({
|
|
669
|
+
type: "tool_use",
|
|
670
|
+
id: toolCall.id,
|
|
671
|
+
name: toolCall.toolName,
|
|
672
|
+
input: toolCall.input
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
return { role: "assistant", content: contentBlocks };
|
|
676
|
+
}
|
|
677
|
+
function mapToolMessageToAnthropicRequestMessage(message) {
|
|
678
|
+
if (message.toolCallId !== void 0) {
|
|
679
|
+
return {
|
|
680
|
+
role: "user",
|
|
681
|
+
content: [
|
|
682
|
+
{
|
|
683
|
+
type: "tool_result",
|
|
684
|
+
tool_use_id: message.toolCallId,
|
|
685
|
+
content: extractMessageText(message)
|
|
686
|
+
}
|
|
687
|
+
]
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
return { role: "user", content: buildAnthropicToolResultText(message) };
|
|
691
|
+
}
|
|
692
|
+
function buildAnthropicToolResultText(message) {
|
|
693
|
+
const messageText = extractMessageText(message);
|
|
694
|
+
if (message.toolName === void 0) {
|
|
695
|
+
return `Tool result: ${messageText}`;
|
|
696
|
+
}
|
|
697
|
+
return `Tool result (${message.toolName}): ${messageText}`;
|
|
698
|
+
}
|
|
699
|
+
function mapMessageContentToAnthropic(message) {
|
|
700
|
+
if (typeof message.content === "string") {
|
|
701
|
+
return message.content;
|
|
702
|
+
}
|
|
703
|
+
return message.content.map(mapMessageContentBlockToAnthropic);
|
|
704
|
+
}
|
|
705
|
+
function mapMessageContentBlockToAnthropic(contentBlock) {
|
|
706
|
+
if (contentBlock.type === "text") {
|
|
707
|
+
return { type: "text", text: contentBlock.text };
|
|
708
|
+
}
|
|
709
|
+
return {
|
|
710
|
+
type: contentBlock.type,
|
|
711
|
+
source: {
|
|
712
|
+
type: "base64",
|
|
713
|
+
media_type: contentBlock.mediaType,
|
|
714
|
+
data: contentBlock.dataBase64
|
|
715
|
+
}
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// src/anthropic-response-parsing.ts
|
|
720
|
+
import { z as z4 } from "zod";
|
|
721
|
+
import { AppErrorCode as AppErrorCode8 } from "@assemble-dev/shared-types/errors";
|
|
722
|
+
import { JsonValueSchema as JsonValueSchema4 } from "@assemble-dev/shared-types/json";
|
|
723
|
+
import { AppError as AppError9 } from "@assemble-dev/shared-utils/errors";
|
|
724
|
+
var AnthropicUsageSchema = z4.object({
|
|
725
|
+
input_tokens: z4.number().int().nonnegative(),
|
|
726
|
+
output_tokens: z4.number().int().nonnegative(),
|
|
727
|
+
cache_creation_input_tokens: z4.number().int().nonnegative().optional(),
|
|
728
|
+
cache_read_input_tokens: z4.number().int().nonnegative().optional()
|
|
729
|
+
});
|
|
730
|
+
var AnthropicContentBlockSchema = z4.discriminatedUnion("type", [
|
|
731
|
+
z4.object({
|
|
732
|
+
type: z4.literal("text"),
|
|
733
|
+
text: z4.string()
|
|
734
|
+
}),
|
|
735
|
+
z4.object({
|
|
736
|
+
type: z4.literal("tool_use"),
|
|
737
|
+
id: z4.string(),
|
|
738
|
+
name: z4.string(),
|
|
739
|
+
input: JsonValueSchema4
|
|
740
|
+
}),
|
|
741
|
+
z4.object({
|
|
742
|
+
type: z4.literal("thinking"),
|
|
743
|
+
thinking: z4.string().optional(),
|
|
744
|
+
signature: z4.string().optional()
|
|
745
|
+
}),
|
|
746
|
+
z4.object({
|
|
747
|
+
type: z4.literal("redacted_thinking"),
|
|
748
|
+
data: z4.string().optional()
|
|
749
|
+
})
|
|
750
|
+
]);
|
|
751
|
+
var AnthropicMessagesResponseSchema = z4.object({
|
|
752
|
+
content: z4.array(AnthropicContentBlockSchema),
|
|
753
|
+
stop_reason: z4.string().nullable().optional(),
|
|
754
|
+
usage: AnthropicUsageSchema
|
|
755
|
+
});
|
|
756
|
+
function mapAnthropicResponseToChatCompletionResult(response, latencyMs) {
|
|
757
|
+
const toolCalls = extractAnthropicToolCalls(response);
|
|
758
|
+
const result = {
|
|
759
|
+
content: extractAnthropicTextContent(response, toolCalls.length > 0),
|
|
760
|
+
tokenUsage: mapAnthropicUsageToTokenUsage(response.usage),
|
|
761
|
+
latencyMs
|
|
762
|
+
};
|
|
763
|
+
if (toolCalls.length > 0) {
|
|
764
|
+
result.toolCalls = toolCalls;
|
|
765
|
+
}
|
|
766
|
+
if (response.stop_reason !== null && response.stop_reason !== void 0) {
|
|
767
|
+
result.stopReason = normalizeAnthropicStopReason(response.stop_reason);
|
|
768
|
+
}
|
|
769
|
+
return result;
|
|
770
|
+
}
|
|
771
|
+
function extractAnthropicToolCalls(response) {
|
|
772
|
+
return response.content.filter((contentBlock) => contentBlock.type === "tool_use").map((contentBlock) => ({
|
|
773
|
+
id: contentBlock.id,
|
|
774
|
+
toolName: contentBlock.name,
|
|
775
|
+
input: contentBlock.input
|
|
776
|
+
}));
|
|
777
|
+
}
|
|
778
|
+
function extractAnthropicTextContent(response, allowEmptyText) {
|
|
779
|
+
const textBlocks = response.content.filter(
|
|
780
|
+
(contentBlock) => contentBlock.type === "text"
|
|
781
|
+
);
|
|
782
|
+
if (textBlocks.length === 0 && !allowEmptyText) {
|
|
783
|
+
throw new AppError9({
|
|
784
|
+
code: AppErrorCode8.ValidationFailed,
|
|
785
|
+
message: "Anthropic response contained no text content",
|
|
786
|
+
statusCode: 502
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
return textBlocks.map((textBlock) => textBlock.text).join("");
|
|
790
|
+
}
|
|
791
|
+
function normalizeAnthropicStopReason(providerStopReason) {
|
|
792
|
+
if (providerStopReason === "tool_use") {
|
|
793
|
+
return "tool_use";
|
|
794
|
+
}
|
|
795
|
+
if (providerStopReason === "max_tokens") {
|
|
796
|
+
return "max_tokens";
|
|
797
|
+
}
|
|
798
|
+
if (providerStopReason === "stop_sequence") {
|
|
799
|
+
return "stop_sequence";
|
|
800
|
+
}
|
|
801
|
+
return "end_turn";
|
|
802
|
+
}
|
|
803
|
+
function mapAnthropicUsageToTokenUsage(usage) {
|
|
804
|
+
const cacheCreationInputTokens = usage.cache_creation_input_tokens;
|
|
805
|
+
const cacheReadInputTokens = usage.cache_read_input_tokens;
|
|
806
|
+
const tokenUsage = {
|
|
807
|
+
inputTokens: usage.input_tokens,
|
|
808
|
+
outputTokens: usage.output_tokens,
|
|
809
|
+
totalTokens: usage.input_tokens + usage.output_tokens + (cacheCreationInputTokens ?? 0) + (cacheReadInputTokens ?? 0)
|
|
810
|
+
};
|
|
811
|
+
if (cacheCreationInputTokens !== void 0) {
|
|
812
|
+
tokenUsage.cacheCreationInputTokens = cacheCreationInputTokens;
|
|
813
|
+
}
|
|
814
|
+
if (cacheReadInputTokens !== void 0) {
|
|
815
|
+
tokenUsage.cacheReadInputTokens = cacheReadInputTokens;
|
|
816
|
+
}
|
|
817
|
+
return tokenUsage;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// src/anthropic-stream.ts
|
|
821
|
+
import { AppErrorCode as AppErrorCode9 } from "@assemble-dev/shared-types/errors";
|
|
822
|
+
import { JsonValueSchema as JsonValueSchema5 } from "@assemble-dev/shared-types/json";
|
|
823
|
+
import { AppError as AppError10 } from "@assemble-dev/shared-utils/errors";
|
|
824
|
+
|
|
825
|
+
// src/anthropic-stream-events.ts
|
|
826
|
+
import { z as z5 } from "zod";
|
|
827
|
+
var AnthropicStreamEventSchema = z5.discriminatedUnion("type", [
|
|
828
|
+
z5.object({
|
|
829
|
+
type: z5.literal("message_start"),
|
|
830
|
+
message: z5.object({
|
|
831
|
+
usage: z5.object({
|
|
832
|
+
input_tokens: z5.number().int().nonnegative(),
|
|
833
|
+
output_tokens: z5.number().int().nonnegative().optional(),
|
|
834
|
+
cache_creation_input_tokens: z5.number().int().nonnegative().optional(),
|
|
835
|
+
cache_read_input_tokens: z5.number().int().nonnegative().optional()
|
|
836
|
+
})
|
|
837
|
+
})
|
|
838
|
+
}),
|
|
839
|
+
z5.object({
|
|
840
|
+
type: z5.literal("content_block_start"),
|
|
841
|
+
index: z5.number().int().nonnegative(),
|
|
842
|
+
content_block: z5.discriminatedUnion("type", [
|
|
843
|
+
z5.object({ type: z5.literal("text"), text: z5.string().optional() }),
|
|
844
|
+
z5.object({
|
|
845
|
+
type: z5.literal("tool_use"),
|
|
846
|
+
id: z5.string(),
|
|
847
|
+
name: z5.string()
|
|
848
|
+
})
|
|
849
|
+
])
|
|
850
|
+
}),
|
|
851
|
+
z5.object({
|
|
852
|
+
type: z5.literal("content_block_delta"),
|
|
853
|
+
index: z5.number().int().nonnegative(),
|
|
854
|
+
delta: z5.discriminatedUnion("type", [
|
|
855
|
+
z5.object({ type: z5.literal("text_delta"), text: z5.string() }),
|
|
856
|
+
z5.object({
|
|
857
|
+
type: z5.literal("input_json_delta"),
|
|
858
|
+
partial_json: z5.string()
|
|
859
|
+
})
|
|
860
|
+
])
|
|
861
|
+
}),
|
|
862
|
+
z5.object({ type: z5.literal("content_block_stop") }),
|
|
863
|
+
z5.object({
|
|
864
|
+
type: z5.literal("message_delta"),
|
|
865
|
+
delta: z5.object({ stop_reason: z5.string().nullable().optional() }),
|
|
866
|
+
usage: z5.object({ output_tokens: z5.number().int().nonnegative() }).optional()
|
|
867
|
+
}),
|
|
868
|
+
z5.object({ type: z5.literal("message_stop") }),
|
|
869
|
+
z5.object({ type: z5.literal("ping") }),
|
|
870
|
+
z5.object({
|
|
871
|
+
type: z5.literal("error"),
|
|
872
|
+
error: z5.object({ message: z5.string() })
|
|
873
|
+
})
|
|
874
|
+
]);
|
|
875
|
+
|
|
876
|
+
// src/anthropic-stream.ts
|
|
877
|
+
async function* streamAnthropicChatCompletion(input) {
|
|
878
|
+
const startedAt = Date.now();
|
|
879
|
+
const response = await establishAnthropicStreamResponse(input);
|
|
880
|
+
if (response.body === null) {
|
|
881
|
+
throw buildProviderInvalidResponseError("Anthropic", "stream body");
|
|
882
|
+
}
|
|
883
|
+
yield* parseAnthropicStreamEvents(response.body, startedAt, input.abortSignal);
|
|
884
|
+
}
|
|
885
|
+
async function establishAnthropicStreamResponse(input) {
|
|
886
|
+
return await executeProviderCallWithRetry(input.retry, async () => {
|
|
887
|
+
const response = await executeAbortableProviderFetch(
|
|
888
|
+
"Anthropic",
|
|
889
|
+
async () => await input.fetchImplementation(input.url, {
|
|
890
|
+
method: "POST",
|
|
891
|
+
headers: input.headers,
|
|
892
|
+
body: JSON.stringify(input.body),
|
|
893
|
+
signal: input.abortSignal
|
|
894
|
+
})
|
|
895
|
+
);
|
|
896
|
+
if (!response.ok) {
|
|
897
|
+
throw buildProviderStatusError("Anthropic", response.status);
|
|
898
|
+
}
|
|
899
|
+
return response;
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
async function* parseAnthropicStreamEvents(body, startedAt, abortSignal) {
|
|
903
|
+
const accumulator = createAnthropicStreamAccumulator();
|
|
904
|
+
for await (const dataLine of readServerSentEventDataLines(body, abortSignal)) {
|
|
905
|
+
const parsedEvent = AnthropicStreamEventSchema.safeParse(
|
|
906
|
+
JSON.parse(dataLine)
|
|
907
|
+
);
|
|
908
|
+
if (!parsedEvent.success) {
|
|
909
|
+
continue;
|
|
910
|
+
}
|
|
911
|
+
yield* applyAnthropicStreamEvent(parsedEvent.data, accumulator);
|
|
912
|
+
if (parsedEvent.data.type === "message_stop") {
|
|
913
|
+
yield {
|
|
914
|
+
type: "completed",
|
|
915
|
+
result: buildAnthropicStreamResult(accumulator, Date.now() - startedAt)
|
|
916
|
+
};
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
throw buildProviderInvalidResponseError("Anthropic", "stream termination");
|
|
921
|
+
}
|
|
922
|
+
async function* readServerSentEventDataLines(body, abortSignal) {
|
|
923
|
+
const reader = body.getReader();
|
|
924
|
+
const decoder = new TextDecoder();
|
|
925
|
+
let bufferedText = "";
|
|
926
|
+
try {
|
|
927
|
+
for (; ; ) {
|
|
928
|
+
if (abortSignal?.aborted === true) {
|
|
929
|
+
await reader.cancel();
|
|
930
|
+
throw buildProviderCanceledError("Anthropic");
|
|
931
|
+
}
|
|
932
|
+
const readResult = await reader.read();
|
|
933
|
+
if (readResult.done) {
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
bufferedText += decoder.decode(readResult.value, { stream: true });
|
|
937
|
+
const lines = bufferedText.split("\n");
|
|
938
|
+
bufferedText = lines.pop() ?? "";
|
|
939
|
+
for (const line of lines) {
|
|
940
|
+
if (line.startsWith("data:")) {
|
|
941
|
+
const dataText = line.slice("data:".length).trim();
|
|
942
|
+
if (dataText.length > 0) {
|
|
943
|
+
yield dataText;
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
} catch (error) {
|
|
949
|
+
if (error instanceof Error && isAbortError(error)) {
|
|
950
|
+
throw buildProviderCanceledError("Anthropic");
|
|
951
|
+
}
|
|
952
|
+
throw error;
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
function createAnthropicStreamAccumulator() {
|
|
956
|
+
return {
|
|
957
|
+
contentText: "",
|
|
958
|
+
toolCallsByBlockIndex: /* @__PURE__ */ new Map(),
|
|
959
|
+
inputTokens: 0,
|
|
960
|
+
outputTokens: 0
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
function* applyAnthropicStreamEvent(event, accumulator) {
|
|
964
|
+
if (event.type === "error") {
|
|
965
|
+
throw new AppError10({
|
|
966
|
+
code: AppErrorCode9.InternalServerError,
|
|
967
|
+
message: `Anthropic stream reported an error: ${event.error.message}`,
|
|
968
|
+
statusCode: 502
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
if (event.type === "message_start") {
|
|
972
|
+
applyAnthropicMessageStartUsage(event.message.usage, accumulator);
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
if (event.type === "content_block_start") {
|
|
976
|
+
yield* applyAnthropicContentBlockStart(event, accumulator);
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
if (event.type === "content_block_delta") {
|
|
980
|
+
yield* applyAnthropicContentBlockDelta(event, accumulator);
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
if (event.type === "message_delta") {
|
|
984
|
+
applyAnthropicMessageDelta(event, accumulator);
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
function applyAnthropicMessageStartUsage(usage, accumulator) {
|
|
988
|
+
accumulator.inputTokens = usage.input_tokens;
|
|
989
|
+
accumulator.outputTokens = usage.output_tokens ?? 0;
|
|
990
|
+
accumulator.cacheCreationInputTokens = usage.cache_creation_input_tokens;
|
|
991
|
+
accumulator.cacheReadInputTokens = usage.cache_read_input_tokens;
|
|
992
|
+
}
|
|
993
|
+
function* applyAnthropicContentBlockStart(event, accumulator) {
|
|
994
|
+
if (event.content_block.type === "text") {
|
|
995
|
+
const initialText = event.content_block.text ?? "";
|
|
996
|
+
if (initialText.length > 0) {
|
|
997
|
+
accumulator.contentText += initialText;
|
|
998
|
+
yield { type: "text_delta", text: initialText };
|
|
999
|
+
}
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
accumulator.toolCallsByBlockIndex.set(event.index, {
|
|
1003
|
+
id: event.content_block.id,
|
|
1004
|
+
toolName: event.content_block.name,
|
|
1005
|
+
inputJsonText: ""
|
|
1006
|
+
});
|
|
1007
|
+
yield {
|
|
1008
|
+
type: "tool_call_started",
|
|
1009
|
+
id: event.content_block.id,
|
|
1010
|
+
toolName: event.content_block.name
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
1013
|
+
function* applyAnthropicContentBlockDelta(event, accumulator) {
|
|
1014
|
+
if (event.delta.type === "text_delta") {
|
|
1015
|
+
accumulator.contentText += event.delta.text;
|
|
1016
|
+
yield { type: "text_delta", text: event.delta.text };
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
const toolCallAccumulation = accumulator.toolCallsByBlockIndex.get(
|
|
1020
|
+
event.index
|
|
1021
|
+
);
|
|
1022
|
+
if (toolCallAccumulation === void 0) {
|
|
1023
|
+
throw buildProviderInvalidResponseError(
|
|
1024
|
+
"Anthropic",
|
|
1025
|
+
"stream tool input delta"
|
|
1026
|
+
);
|
|
1027
|
+
}
|
|
1028
|
+
toolCallAccumulation.inputJsonText += event.delta.partial_json;
|
|
1029
|
+
yield {
|
|
1030
|
+
type: "tool_call_input_delta",
|
|
1031
|
+
id: toolCallAccumulation.id,
|
|
1032
|
+
inputJsonText: event.delta.partial_json
|
|
1033
|
+
};
|
|
1034
|
+
}
|
|
1035
|
+
function applyAnthropicMessageDelta(event, accumulator) {
|
|
1036
|
+
if (event.usage !== void 0) {
|
|
1037
|
+
accumulator.outputTokens = event.usage.output_tokens;
|
|
1038
|
+
}
|
|
1039
|
+
if (event.delta.stop_reason !== null && event.delta.stop_reason !== void 0) {
|
|
1040
|
+
accumulator.stopReason = normalizeAnthropicStopReason(
|
|
1041
|
+
event.delta.stop_reason
|
|
1042
|
+
);
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
function buildAnthropicStreamResult(accumulator, latencyMs) {
|
|
1046
|
+
const result = {
|
|
1047
|
+
content: accumulator.contentText,
|
|
1048
|
+
tokenUsage: mapAnthropicUsageToTokenUsage({
|
|
1049
|
+
input_tokens: accumulator.inputTokens,
|
|
1050
|
+
output_tokens: accumulator.outputTokens,
|
|
1051
|
+
cache_creation_input_tokens: accumulator.cacheCreationInputTokens,
|
|
1052
|
+
cache_read_input_tokens: accumulator.cacheReadInputTokens
|
|
1053
|
+
}),
|
|
1054
|
+
latencyMs
|
|
1055
|
+
};
|
|
1056
|
+
const toolCalls = [...accumulator.toolCallsByBlockIndex.values()].map(
|
|
1057
|
+
mapAccumulatedToolCallToModelToolCall
|
|
1058
|
+
);
|
|
1059
|
+
if (toolCalls.length > 0) {
|
|
1060
|
+
result.toolCalls = toolCalls;
|
|
1061
|
+
}
|
|
1062
|
+
if (accumulator.stopReason !== void 0) {
|
|
1063
|
+
result.stopReason = accumulator.stopReason;
|
|
1064
|
+
}
|
|
1065
|
+
return result;
|
|
1066
|
+
}
|
|
1067
|
+
function mapAccumulatedToolCallToModelToolCall(toolCallAccumulation) {
|
|
1068
|
+
return {
|
|
1069
|
+
id: toolCallAccumulation.id,
|
|
1070
|
+
toolName: toolCallAccumulation.toolName,
|
|
1071
|
+
input: parseAnthropicToolInputJson(toolCallAccumulation.inputJsonText)
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
1074
|
+
function parseAnthropicToolInputJson(inputJsonText) {
|
|
1075
|
+
if (inputJsonText.trim().length === 0) {
|
|
1076
|
+
return {};
|
|
1077
|
+
}
|
|
1078
|
+
try {
|
|
1079
|
+
return JsonValueSchema5.parse(JSON.parse(inputJsonText));
|
|
1080
|
+
} catch (error) {
|
|
1081
|
+
throw new AppError10({
|
|
1082
|
+
code: AppErrorCode9.ValidationFailed,
|
|
1083
|
+
message: "Anthropic tool input was not valid JSON",
|
|
1084
|
+
statusCode: 502,
|
|
1085
|
+
cause: error instanceof Error ? error : void 0
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
// src/anthropic-adapter.ts
|
|
1091
|
+
var defaultAnthropicBaseUrl = "https://api.anthropic.com";
|
|
1092
|
+
var defaultAnthropicMaxOutputTokens = 1024;
|
|
1093
|
+
var anthropicApiVersion = "2023-06-01";
|
|
1094
|
+
var structuredOutputSystemMessage2 = {
|
|
1095
|
+
role: "system",
|
|
1096
|
+
content: "Respond with a single JSON object that satisfies the caller's requested structure. Output only valid JSON with no surrounding text."
|
|
1097
|
+
};
|
|
1098
|
+
function anthropic(options) {
|
|
1099
|
+
return {
|
|
1100
|
+
name: `anthropic:${options.model}`,
|
|
1101
|
+
async generateChatCompletion(request) {
|
|
1102
|
+
return await executeAnthropicAdapterCall(
|
|
1103
|
+
options,
|
|
1104
|
+
buildAnthropicCallInput(options, request)
|
|
1105
|
+
);
|
|
1106
|
+
},
|
|
1107
|
+
async generateStructuredOutput(request) {
|
|
1108
|
+
const completion = await executeAnthropicAdapterCall(options, {
|
|
1109
|
+
messages: [structuredOutputSystemMessage2, ...request.messages],
|
|
1110
|
+
temperature: request.temperature ?? options.temperature,
|
|
1111
|
+
maxOutputTokens: request.maxOutputTokens ?? options.maxOutputTokens,
|
|
1112
|
+
abortSignal: request.abortSignal
|
|
1113
|
+
});
|
|
1114
|
+
return {
|
|
1115
|
+
value: parseStructuredJsonText(completion.content, request.schema),
|
|
1116
|
+
tokenUsage: completion.tokenUsage,
|
|
1117
|
+
latencyMs: completion.latencyMs
|
|
1118
|
+
};
|
|
1119
|
+
},
|
|
1120
|
+
streamChatCompletion(request) {
|
|
1121
|
+
return streamAnthropicChatCompletion(
|
|
1122
|
+
buildAnthropicStreamInput(options, request)
|
|
1123
|
+
);
|
|
1124
|
+
}
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1127
|
+
function buildAnthropicCallInput(options, request) {
|
|
1128
|
+
return {
|
|
1129
|
+
messages: request.messages,
|
|
1130
|
+
temperature: request.temperature ?? options.temperature,
|
|
1131
|
+
maxOutputTokens: request.maxOutputTokens ?? options.maxOutputTokens,
|
|
1132
|
+
tools: request.tools,
|
|
1133
|
+
toolChoice: request.toolChoice,
|
|
1134
|
+
abortSignal: request.abortSignal
|
|
1135
|
+
};
|
|
1136
|
+
}
|
|
1137
|
+
async function executeAnthropicAdapterCall(options, input) {
|
|
1138
|
+
return await executeProviderCallWithModelFallback(
|
|
1139
|
+
options.fallbackModel,
|
|
1140
|
+
async () => await executeProviderCallWithRetry(
|
|
1141
|
+
options.retry,
|
|
1142
|
+
async () => await requestAnthropicMessages(options, input)
|
|
1143
|
+
),
|
|
1144
|
+
async (fallbackModelId) => await requestAnthropicMessages(
|
|
1145
|
+
{ ...options, model: fallbackModelId },
|
|
1146
|
+
input
|
|
1147
|
+
)
|
|
1148
|
+
);
|
|
1149
|
+
}
|
|
1150
|
+
async function requestAnthropicMessages(options, input) {
|
|
1151
|
+
const requestBody = buildAnthropicRequestBody(
|
|
1152
|
+
buildAnthropicRequestSettings(options),
|
|
1153
|
+
input
|
|
1154
|
+
);
|
|
1155
|
+
const startedAt = Date.now();
|
|
1156
|
+
const response = await executeAbortableProviderFetch(
|
|
1157
|
+
"Anthropic",
|
|
1158
|
+
async () => await resolveAnthropicFetch(options)(
|
|
1159
|
+
`${resolveAnthropicBaseUrl(options)}/v1/messages`,
|
|
1160
|
+
{
|
|
1161
|
+
method: "POST",
|
|
1162
|
+
headers: buildAnthropicRequestHeaders(options),
|
|
1163
|
+
body: JSON.stringify(requestBody),
|
|
1164
|
+
signal: input.abortSignal
|
|
1165
|
+
}
|
|
1166
|
+
)
|
|
1167
|
+
);
|
|
1168
|
+
const latencyMs = Date.now() - startedAt;
|
|
1169
|
+
if (!response.ok) {
|
|
1170
|
+
throw buildProviderStatusError("Anthropic", response.status);
|
|
1171
|
+
}
|
|
1172
|
+
const parseResult = AnthropicMessagesResponseSchema.safeParse(
|
|
1173
|
+
await response.json()
|
|
1174
|
+
);
|
|
1175
|
+
if (!parseResult.success) {
|
|
1176
|
+
throw buildProviderInvalidResponseError("Anthropic", "messages");
|
|
1177
|
+
}
|
|
1178
|
+
return mapAnthropicResponseToChatCompletionResult(parseResult.data, latencyMs);
|
|
1179
|
+
}
|
|
1180
|
+
function buildAnthropicStreamInput(options, request) {
|
|
1181
|
+
const input = buildAnthropicCallInput(options, request);
|
|
1182
|
+
const requestBody = buildAnthropicRequestBody(
|
|
1183
|
+
buildAnthropicRequestSettings(options),
|
|
1184
|
+
input
|
|
1185
|
+
);
|
|
1186
|
+
return {
|
|
1187
|
+
url: `${resolveAnthropicBaseUrl(options)}/v1/messages`,
|
|
1188
|
+
headers: buildAnthropicRequestHeaders(options),
|
|
1189
|
+
body: { ...requestBody, stream: true },
|
|
1190
|
+
fetchImplementation: resolveAnthropicFetch(options),
|
|
1191
|
+
retry: options.retry,
|
|
1192
|
+
abortSignal: request.abortSignal
|
|
1193
|
+
};
|
|
1194
|
+
}
|
|
1195
|
+
function buildAnthropicRequestSettings(options) {
|
|
1196
|
+
return {
|
|
1197
|
+
model: options.model,
|
|
1198
|
+
defaultMaxOutputTokens: defaultAnthropicMaxOutputTokens,
|
|
1199
|
+
promptCaching: options.promptCaching,
|
|
1200
|
+
thinking: options.thinking
|
|
1201
|
+
};
|
|
1202
|
+
}
|
|
1203
|
+
function buildAnthropicRequestHeaders(options) {
|
|
1204
|
+
return {
|
|
1205
|
+
"content-type": "application/json",
|
|
1206
|
+
"x-api-key": resolveProviderApiKey({
|
|
1207
|
+
providerLabel: "Anthropic",
|
|
1208
|
+
adapterFunctionName: "anthropic()",
|
|
1209
|
+
baseEnvVarName: "ANTHROPIC_API_KEY",
|
|
1210
|
+
apiKey: options.apiKey,
|
|
1211
|
+
apiKeyName: options.apiKeyName
|
|
1212
|
+
}),
|
|
1213
|
+
"anthropic-version": anthropicApiVersion
|
|
1214
|
+
};
|
|
1215
|
+
}
|
|
1216
|
+
function resolveAnthropicBaseUrl(options) {
|
|
1217
|
+
return options.baseURL ?? defaultAnthropicBaseUrl;
|
|
1218
|
+
}
|
|
1219
|
+
function resolveAnthropicFetch(options) {
|
|
1220
|
+
return options.fetchImplementation ?? fetch;
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
// src/gemini-adapter.ts
|
|
1224
|
+
import { z as z6 } from "zod";
|
|
1225
|
+
import { AppErrorCode as AppErrorCode10 } from "@assemble-dev/shared-types/errors";
|
|
1226
|
+
import { AppError as AppError11 } from "@assemble-dev/shared-utils/errors";
|
|
1227
|
+
var defaultGeminiBaseUrl = "https://generativelanguage.googleapis.com";
|
|
1228
|
+
var GeminiGenerateContentResponseSchema = z6.object({
|
|
1229
|
+
candidates: z6.array(
|
|
1230
|
+
z6.object({
|
|
1231
|
+
content: z6.object({
|
|
1232
|
+
parts: z6.array(z6.object({ text: z6.string() }))
|
|
1233
|
+
})
|
|
1234
|
+
})
|
|
1235
|
+
).optional(),
|
|
1236
|
+
usageMetadata: z6.object({
|
|
1237
|
+
promptTokenCount: z6.number().int().nonnegative().optional(),
|
|
1238
|
+
candidatesTokenCount: z6.number().int().nonnegative().optional(),
|
|
1239
|
+
totalTokenCount: z6.number().int().nonnegative().optional()
|
|
1240
|
+
}).optional()
|
|
1241
|
+
});
|
|
1242
|
+
var structuredOutputSystemMessage3 = {
|
|
1243
|
+
role: "system",
|
|
1244
|
+
content: "Respond with a single JSON object that satisfies the caller's requested structure. Output only valid JSON with no surrounding text."
|
|
1245
|
+
};
|
|
1246
|
+
function gemini(options) {
|
|
1247
|
+
return {
|
|
1248
|
+
name: `gemini:${options.model}`,
|
|
1249
|
+
async generateChatCompletion(request) {
|
|
1250
|
+
ensureGeminiRequestHasNoTools(request);
|
|
1251
|
+
return await executeGeminiAdapterCall(options, {
|
|
1252
|
+
messages: request.messages,
|
|
1253
|
+
temperature: request.temperature ?? options.temperature,
|
|
1254
|
+
maxOutputTokens: request.maxOutputTokens ?? options.maxOutputTokens,
|
|
1255
|
+
requireJsonResponse: false,
|
|
1256
|
+
abortSignal: request.abortSignal
|
|
1257
|
+
});
|
|
1258
|
+
},
|
|
1259
|
+
async generateStructuredOutput(request) {
|
|
1260
|
+
const completion = await executeGeminiAdapterCall(options, {
|
|
1261
|
+
messages: [structuredOutputSystemMessage3, ...request.messages],
|
|
1262
|
+
temperature: request.temperature ?? options.temperature,
|
|
1263
|
+
maxOutputTokens: request.maxOutputTokens ?? options.maxOutputTokens,
|
|
1264
|
+
requireJsonResponse: true,
|
|
1265
|
+
abortSignal: request.abortSignal
|
|
1266
|
+
});
|
|
1267
|
+
return {
|
|
1268
|
+
value: parseStructuredJsonText(completion.content, request.schema),
|
|
1269
|
+
tokenUsage: completion.tokenUsage,
|
|
1270
|
+
latencyMs: completion.latencyMs
|
|
1271
|
+
};
|
|
1272
|
+
}
|
|
1273
|
+
};
|
|
1274
|
+
}
|
|
1275
|
+
async function executeGeminiAdapterCall(options, input) {
|
|
1276
|
+
return await executeProviderCallWithModelFallback(
|
|
1277
|
+
options.fallbackModel,
|
|
1278
|
+
async () => await executeProviderCallWithRetry(
|
|
1279
|
+
options.retry,
|
|
1280
|
+
async () => await requestGeminiGenerateContent(options, input)
|
|
1281
|
+
),
|
|
1282
|
+
async (fallbackModelId) => await requestGeminiGenerateContent(
|
|
1283
|
+
{ ...options, model: fallbackModelId },
|
|
1284
|
+
input
|
|
1285
|
+
)
|
|
1286
|
+
);
|
|
1287
|
+
}
|
|
1288
|
+
async function requestGeminiGenerateContent(options, input) {
|
|
1289
|
+
const apiKey = resolveProviderApiKey({
|
|
1290
|
+
providerLabel: "Gemini",
|
|
1291
|
+
adapterFunctionName: "gemini()",
|
|
1292
|
+
baseEnvVarName: "GOOGLE_API_KEY",
|
|
1293
|
+
apiKey: options.apiKey,
|
|
1294
|
+
apiKeyName: options.apiKeyName
|
|
1295
|
+
});
|
|
1296
|
+
const fetchImplementation = options.fetchImplementation ?? fetch;
|
|
1297
|
+
const baseUrl = options.baseURL ?? defaultGeminiBaseUrl;
|
|
1298
|
+
const requestBody = buildGeminiRequestBody(input);
|
|
1299
|
+
const requestUrl = `${baseUrl}/v1beta/models/${options.model}:generateContent`;
|
|
1300
|
+
const startedAt = Date.now();
|
|
1301
|
+
const response = await executeAbortableProviderFetch(
|
|
1302
|
+
"Gemini",
|
|
1303
|
+
async () => await fetchImplementation(requestUrl, {
|
|
1304
|
+
method: "POST",
|
|
1305
|
+
headers: {
|
|
1306
|
+
"content-type": "application/json",
|
|
1307
|
+
"x-goog-api-key": apiKey
|
|
1308
|
+
},
|
|
1309
|
+
body: JSON.stringify(requestBody),
|
|
1310
|
+
signal: input.abortSignal
|
|
1311
|
+
})
|
|
1312
|
+
);
|
|
1313
|
+
const latencyMs = Date.now() - startedAt;
|
|
1314
|
+
if (!response.ok) {
|
|
1315
|
+
throw buildProviderStatusError("Gemini", response.status);
|
|
1316
|
+
}
|
|
1317
|
+
const parseResult = GeminiGenerateContentResponseSchema.safeParse(
|
|
1318
|
+
await response.json()
|
|
1319
|
+
);
|
|
1320
|
+
if (!parseResult.success) {
|
|
1321
|
+
throw buildProviderInvalidResponseError("Gemini", "generateContent");
|
|
1322
|
+
}
|
|
1323
|
+
return {
|
|
1324
|
+
content: extractGeminiCandidateText(parseResult.data),
|
|
1325
|
+
tokenUsage: mapGeminiUsageToTokenUsage(parseResult.data),
|
|
1326
|
+
latencyMs
|
|
1327
|
+
};
|
|
1328
|
+
}
|
|
1329
|
+
function buildGeminiRequestBody(input) {
|
|
1330
|
+
const systemText = joinSystemMessageText2(input.messages);
|
|
1331
|
+
const generationConfig = buildGeminiGenerationConfig(input);
|
|
1332
|
+
const requestBody = {
|
|
1333
|
+
contents: input.messages.filter((message) => message.role !== "system").map(mapChatMessageToGeminiContent)
|
|
1334
|
+
};
|
|
1335
|
+
if (systemText.length > 0) {
|
|
1336
|
+
requestBody.systemInstruction = { parts: [{ text: systemText }] };
|
|
1337
|
+
}
|
|
1338
|
+
if (generationConfig !== void 0) {
|
|
1339
|
+
requestBody.generationConfig = generationConfig;
|
|
1340
|
+
}
|
|
1341
|
+
return requestBody;
|
|
1342
|
+
}
|
|
1343
|
+
function ensureGeminiRequestHasNoTools(request) {
|
|
1344
|
+
if (request.tools === void 0 || request.tools.length === 0) {
|
|
1345
|
+
return;
|
|
1346
|
+
}
|
|
1347
|
+
throw new AppError11({
|
|
1348
|
+
code: AppErrorCode10.ValidationFailed,
|
|
1349
|
+
message: "gemini adapter does not support model-driven tool use; remove tools from the request or use another adapter",
|
|
1350
|
+
statusCode: 400
|
|
1351
|
+
});
|
|
1352
|
+
}
|
|
1353
|
+
function joinSystemMessageText2(messages) {
|
|
1354
|
+
return messages.filter((message) => message.role === "system").map(extractMessageText).join("\n\n");
|
|
1355
|
+
}
|
|
1356
|
+
function mapChatMessageToGeminiContent(message) {
|
|
1357
|
+
const role = message.role === "assistant" ? "model" : "user";
|
|
1358
|
+
return { role, parts: mapMessageContentToGeminiParts(message) };
|
|
1359
|
+
}
|
|
1360
|
+
function mapMessageContentToGeminiParts(message) {
|
|
1361
|
+
if (typeof message.content === "string") {
|
|
1362
|
+
return [{ text: message.content }];
|
|
1363
|
+
}
|
|
1364
|
+
return message.content.map(mapMessageContentBlockToGeminiPart);
|
|
1365
|
+
}
|
|
1366
|
+
function mapMessageContentBlockToGeminiPart(contentBlock) {
|
|
1367
|
+
if (contentBlock.type === "text") {
|
|
1368
|
+
return { text: contentBlock.text };
|
|
1369
|
+
}
|
|
1370
|
+
return {
|
|
1371
|
+
inline_data: {
|
|
1372
|
+
mime_type: contentBlock.mediaType,
|
|
1373
|
+
data: contentBlock.dataBase64
|
|
1374
|
+
}
|
|
1375
|
+
};
|
|
1376
|
+
}
|
|
1377
|
+
function buildGeminiGenerationConfig(input) {
|
|
1378
|
+
const generationConfig = {};
|
|
1379
|
+
if (input.temperature !== void 0) {
|
|
1380
|
+
generationConfig.temperature = input.temperature;
|
|
1381
|
+
}
|
|
1382
|
+
if (input.maxOutputTokens !== void 0) {
|
|
1383
|
+
generationConfig.maxOutputTokens = input.maxOutputTokens;
|
|
1384
|
+
}
|
|
1385
|
+
if (input.requireJsonResponse) {
|
|
1386
|
+
generationConfig.responseMimeType = "application/json";
|
|
1387
|
+
}
|
|
1388
|
+
if (Object.keys(generationConfig).length === 0) {
|
|
1389
|
+
return void 0;
|
|
1390
|
+
}
|
|
1391
|
+
return generationConfig;
|
|
1392
|
+
}
|
|
1393
|
+
function extractGeminiCandidateText(response) {
|
|
1394
|
+
const firstCandidate = response.candidates?.[0];
|
|
1395
|
+
if (firstCandidate === void 0) {
|
|
1396
|
+
throw new AppError11({
|
|
1397
|
+
code: AppErrorCode10.ValidationFailed,
|
|
1398
|
+
message: "Gemini response contained no candidates",
|
|
1399
|
+
statusCode: 502
|
|
1400
|
+
});
|
|
1401
|
+
}
|
|
1402
|
+
return firstCandidate.content.parts.map((part) => part.text).join("");
|
|
1403
|
+
}
|
|
1404
|
+
function mapGeminiUsageToTokenUsage(response) {
|
|
1405
|
+
const inputTokens = response.usageMetadata?.promptTokenCount ?? 0;
|
|
1406
|
+
const outputTokens = response.usageMetadata?.candidatesTokenCount ?? 0;
|
|
1407
|
+
const totalTokens = response.usageMetadata?.totalTokenCount ?? inputTokens + outputTokens;
|
|
1408
|
+
return {
|
|
1409
|
+
inputTokens,
|
|
1410
|
+
outputTokens,
|
|
1411
|
+
totalTokens
|
|
1412
|
+
};
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
// src/mock-adapter.ts
|
|
1416
|
+
import { AppErrorCode as AppErrorCode11 } from "@assemble-dev/shared-types/errors";
|
|
1417
|
+
import { AppError as AppError12 } from "@assemble-dev/shared-utils/errors";
|
|
1418
|
+
var mockStreamChunkCount = 3;
|
|
1419
|
+
function mockModel(options = {}) {
|
|
1420
|
+
const remainingScriptedResponses = [...options.scriptedResponses ?? []];
|
|
1421
|
+
const adapterName = options.name ?? "mock";
|
|
1422
|
+
return {
|
|
1423
|
+
name: adapterName,
|
|
1424
|
+
async generateChatCompletion(request) {
|
|
1425
|
+
return await resolveMockChatCompletion(
|
|
1426
|
+
request,
|
|
1427
|
+
remainingScriptedResponses,
|
|
1428
|
+
options.respond
|
|
1429
|
+
);
|
|
1430
|
+
},
|
|
1431
|
+
async generateStructuredOutput(request) {
|
|
1432
|
+
const completion = await resolveMockChatCompletion(
|
|
1433
|
+
{
|
|
1434
|
+
messages: request.messages,
|
|
1435
|
+
temperature: request.temperature,
|
|
1436
|
+
maxOutputTokens: request.maxOutputTokens
|
|
1437
|
+
},
|
|
1438
|
+
remainingScriptedResponses,
|
|
1439
|
+
options.respond
|
|
1440
|
+
);
|
|
1441
|
+
return {
|
|
1442
|
+
value: parseStructuredJsonText(completion.content, request.schema),
|
|
1443
|
+
tokenUsage: completion.tokenUsage,
|
|
1444
|
+
latencyMs: completion.latencyMs
|
|
1445
|
+
};
|
|
1446
|
+
},
|
|
1447
|
+
streamChatCompletion(request) {
|
|
1448
|
+
return streamMockChatCompletion(
|
|
1449
|
+
request,
|
|
1450
|
+
remainingScriptedResponses,
|
|
1451
|
+
options.respond
|
|
1452
|
+
);
|
|
1453
|
+
}
|
|
1454
|
+
};
|
|
1455
|
+
}
|
|
1456
|
+
async function* streamMockChatCompletion(request, remainingScriptedResponses, respond) {
|
|
1457
|
+
const completion = await resolveMockChatCompletion(
|
|
1458
|
+
request,
|
|
1459
|
+
remainingScriptedResponses,
|
|
1460
|
+
respond
|
|
1461
|
+
);
|
|
1462
|
+
for (const chunkText of splitTextIntoStreamChunks(completion.content)) {
|
|
1463
|
+
yield { type: "text_delta", text: chunkText };
|
|
1464
|
+
}
|
|
1465
|
+
yield { type: "completed", result: completion };
|
|
1466
|
+
}
|
|
1467
|
+
function splitTextIntoStreamChunks(text) {
|
|
1468
|
+
if (text.length === 0) {
|
|
1469
|
+
return [];
|
|
1470
|
+
}
|
|
1471
|
+
const chunkSize = Math.max(1, Math.ceil(text.length / mockStreamChunkCount));
|
|
1472
|
+
const chunks = [];
|
|
1473
|
+
for (let startIndex = 0; startIndex < text.length; startIndex += chunkSize) {
|
|
1474
|
+
chunks.push(text.slice(startIndex, startIndex + chunkSize));
|
|
1475
|
+
}
|
|
1476
|
+
return chunks;
|
|
1477
|
+
}
|
|
1478
|
+
async function resolveMockChatCompletion(request, remainingScriptedResponses, respond) {
|
|
1479
|
+
const startedAt = Date.now();
|
|
1480
|
+
const content = await Promise.resolve(
|
|
1481
|
+
resolveNextMockResponseText(request, remainingScriptedResponses, respond)
|
|
1482
|
+
);
|
|
1483
|
+
return {
|
|
1484
|
+
content,
|
|
1485
|
+
tokenUsage: calculateMockTokenUsage(request.messages, content),
|
|
1486
|
+
latencyMs: Date.now() - startedAt
|
|
1487
|
+
};
|
|
1488
|
+
}
|
|
1489
|
+
function resolveNextMockResponseText(request, remainingScriptedResponses, respond) {
|
|
1490
|
+
const nextScriptedResponse = remainingScriptedResponses.shift();
|
|
1491
|
+
if (nextScriptedResponse !== void 0) {
|
|
1492
|
+
return nextScriptedResponse;
|
|
1493
|
+
}
|
|
1494
|
+
if (respond !== void 0) {
|
|
1495
|
+
return respond(request);
|
|
1496
|
+
}
|
|
1497
|
+
throw new AppError12({
|
|
1498
|
+
code: AppErrorCode11.BadRequest,
|
|
1499
|
+
message: "Mock model has no scripted responses left and no respond function was provided",
|
|
1500
|
+
statusCode: 400
|
|
1501
|
+
});
|
|
1502
|
+
}
|
|
1503
|
+
function calculateMockTokenUsage(messages, outputText) {
|
|
1504
|
+
const inputCharacterCount = messages.reduce(
|
|
1505
|
+
(characterCount, message) => characterCount + extractMessageText(message).length,
|
|
1506
|
+
0
|
|
1507
|
+
);
|
|
1508
|
+
const inputTokens = Math.max(1, Math.ceil(inputCharacterCount / 4));
|
|
1509
|
+
const outputTokens = Math.max(1, Math.ceil(outputText.length / 4));
|
|
1510
|
+
return {
|
|
1511
|
+
inputTokens,
|
|
1512
|
+
outputTokens,
|
|
1513
|
+
totalTokens: inputTokens + outputTokens
|
|
1514
|
+
};
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
// src/provider-pricing.ts
|
|
1518
|
+
var modelPricingByModelIdPrefix = {
|
|
1519
|
+
"claude-sonnet-4": {
|
|
1520
|
+
inputUsdPerMillionTokens: 3,
|
|
1521
|
+
outputUsdPerMillionTokens: 15,
|
|
1522
|
+
cacheWriteUsdPerMillionTokens: 3.75,
|
|
1523
|
+
cacheReadUsdPerMillionTokens: 0.3
|
|
1524
|
+
},
|
|
1525
|
+
"claude-haiku-4": {
|
|
1526
|
+
inputUsdPerMillionTokens: 1,
|
|
1527
|
+
outputUsdPerMillionTokens: 5,
|
|
1528
|
+
cacheWriteUsdPerMillionTokens: 1.25,
|
|
1529
|
+
cacheReadUsdPerMillionTokens: 0.1
|
|
1530
|
+
},
|
|
1531
|
+
"claude-opus-4": {
|
|
1532
|
+
inputUsdPerMillionTokens: 15,
|
|
1533
|
+
outputUsdPerMillionTokens: 75,
|
|
1534
|
+
cacheWriteUsdPerMillionTokens: 18.75,
|
|
1535
|
+
cacheReadUsdPerMillionTokens: 1.5
|
|
1536
|
+
},
|
|
1537
|
+
"gpt-4o-mini": {
|
|
1538
|
+
inputUsdPerMillionTokens: 0.15,
|
|
1539
|
+
outputUsdPerMillionTokens: 0.6
|
|
1540
|
+
},
|
|
1541
|
+
"gpt-4o": {
|
|
1542
|
+
inputUsdPerMillionTokens: 2.5,
|
|
1543
|
+
outputUsdPerMillionTokens: 10
|
|
1544
|
+
},
|
|
1545
|
+
"gemini-2.0-flash": {
|
|
1546
|
+
inputUsdPerMillionTokens: 0.1,
|
|
1547
|
+
outputUsdPerMillionTokens: 0.4
|
|
1548
|
+
}
|
|
1549
|
+
};
|
|
1550
|
+
function estimateModelCallCostUsd(modelName, tokenUsage) {
|
|
1551
|
+
const modelId = stripAdapterPrefixFromModelName(modelName);
|
|
1552
|
+
const pricing = findModelPricingByLongestPrefix(modelId);
|
|
1553
|
+
if (pricing === null) {
|
|
1554
|
+
return null;
|
|
1555
|
+
}
|
|
1556
|
+
return calculateModelCallCostUsd(pricing, tokenUsage);
|
|
1557
|
+
}
|
|
1558
|
+
function stripAdapterPrefixFromModelName(modelName) {
|
|
1559
|
+
const separatorIndex = modelName.indexOf(":");
|
|
1560
|
+
if (separatorIndex === -1) {
|
|
1561
|
+
return modelName;
|
|
1562
|
+
}
|
|
1563
|
+
return modelName.slice(separatorIndex + 1);
|
|
1564
|
+
}
|
|
1565
|
+
function findModelPricingByLongestPrefix(modelId) {
|
|
1566
|
+
const matchingPrefixes = Object.keys(modelPricingByModelIdPrefix).filter((modelIdPrefix) => modelId.startsWith(modelIdPrefix)).sort(
|
|
1567
|
+
(firstPrefix, secondPrefix) => secondPrefix.length - firstPrefix.length
|
|
1568
|
+
);
|
|
1569
|
+
const longestMatchingPrefix = matchingPrefixes[0];
|
|
1570
|
+
if (longestMatchingPrefix === void 0) {
|
|
1571
|
+
return null;
|
|
1572
|
+
}
|
|
1573
|
+
return modelPricingByModelIdPrefix[longestMatchingPrefix] ?? null;
|
|
1574
|
+
}
|
|
1575
|
+
function calculateModelCallCostUsd(pricing, tokenUsage) {
|
|
1576
|
+
const cacheWriteRate = pricing.cacheWriteUsdPerMillionTokens ?? pricing.inputUsdPerMillionTokens;
|
|
1577
|
+
const cacheReadRate = pricing.cacheReadUsdPerMillionTokens ?? pricing.inputUsdPerMillionTokens;
|
|
1578
|
+
const inputCostUsd = tokenUsage.inputTokens * pricing.inputUsdPerMillionTokens;
|
|
1579
|
+
const outputCostUsd = tokenUsage.outputTokens * pricing.outputUsdPerMillionTokens;
|
|
1580
|
+
const cacheWriteCostUsd = (tokenUsage.cacheCreationInputTokens ?? 0) * cacheWriteRate;
|
|
1581
|
+
const cacheReadCostUsd = (tokenUsage.cacheReadInputTokens ?? 0) * cacheReadRate;
|
|
1582
|
+
return (inputCostUsd + outputCostUsd + cacheWriteCostUsd + cacheReadCostUsd) / 1e6;
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
// src/anthropic-batch.ts
|
|
1586
|
+
import { AppErrorCode as AppErrorCode12 } from "@assemble-dev/shared-types/errors";
|
|
1587
|
+
import { JsonValueSchema as JsonValueSchema6 } from "@assemble-dev/shared-types/json";
|
|
1588
|
+
import { AppError as AppError13 } from "@assemble-dev/shared-utils/errors";
|
|
1589
|
+
|
|
1590
|
+
// src/anthropic-batch-types.ts
|
|
1591
|
+
import { z as z7 } from "zod";
|
|
1592
|
+
var AnthropicMessageBatchResponseSchema = z7.object({
|
|
1593
|
+
id: z7.string(),
|
|
1594
|
+
processing_status: z7.enum(["in_progress", "canceling", "ended"]),
|
|
1595
|
+
request_counts: z7.object({
|
|
1596
|
+
processing: z7.number().int().nonnegative(),
|
|
1597
|
+
succeeded: z7.number().int().nonnegative(),
|
|
1598
|
+
errored: z7.number().int().nonnegative(),
|
|
1599
|
+
canceled: z7.number().int().nonnegative(),
|
|
1600
|
+
expired: z7.number().int().nonnegative()
|
|
1601
|
+
}),
|
|
1602
|
+
results_url: z7.string().nullable(),
|
|
1603
|
+
created_at: z7.string(),
|
|
1604
|
+
ended_at: z7.string().nullable()
|
|
1605
|
+
});
|
|
1606
|
+
var AnthropicBatchContentBlockSchema = z7.discriminatedUnion("type", [
|
|
1607
|
+
z7.object({
|
|
1608
|
+
type: z7.literal("text"),
|
|
1609
|
+
text: z7.string()
|
|
1610
|
+
}),
|
|
1611
|
+
z7.object({
|
|
1612
|
+
type: z7.literal("thinking"),
|
|
1613
|
+
thinking: z7.string().optional(),
|
|
1614
|
+
signature: z7.string().optional()
|
|
1615
|
+
}),
|
|
1616
|
+
z7.object({
|
|
1617
|
+
type: z7.literal("redacted_thinking"),
|
|
1618
|
+
data: z7.string().optional()
|
|
1619
|
+
})
|
|
1620
|
+
]);
|
|
1621
|
+
var AnthropicBatchResultLineSchema = z7.object({
|
|
1622
|
+
custom_id: z7.string(),
|
|
1623
|
+
result: z7.discriminatedUnion("type", [
|
|
1624
|
+
z7.object({
|
|
1625
|
+
type: z7.literal("succeeded"),
|
|
1626
|
+
message: z7.object({
|
|
1627
|
+
content: z7.array(AnthropicBatchContentBlockSchema),
|
|
1628
|
+
usage: AnthropicUsageSchema
|
|
1629
|
+
})
|
|
1630
|
+
}),
|
|
1631
|
+
z7.object({
|
|
1632
|
+
type: z7.literal("errored"),
|
|
1633
|
+
error: z7.object({
|
|
1634
|
+
type: z7.string(),
|
|
1635
|
+
message: z7.string()
|
|
1636
|
+
})
|
|
1637
|
+
}),
|
|
1638
|
+
z7.object({ type: z7.literal("canceled") }),
|
|
1639
|
+
z7.object({ type: z7.literal("expired") })
|
|
1640
|
+
])
|
|
1641
|
+
});
|
|
1642
|
+
function mapAnthropicMessageBatchResponseToMessageBatch(response) {
|
|
1643
|
+
return {
|
|
1644
|
+
id: response.id,
|
|
1645
|
+
processingStatus: response.processing_status,
|
|
1646
|
+
requestCounts: {
|
|
1647
|
+
processing: response.request_counts.processing,
|
|
1648
|
+
succeeded: response.request_counts.succeeded,
|
|
1649
|
+
errored: response.request_counts.errored,
|
|
1650
|
+
canceled: response.request_counts.canceled,
|
|
1651
|
+
expired: response.request_counts.expired
|
|
1652
|
+
},
|
|
1653
|
+
resultsUrl: response.results_url,
|
|
1654
|
+
createdAt: response.created_at,
|
|
1655
|
+
endedAt: response.ended_at
|
|
1656
|
+
};
|
|
1657
|
+
}
|
|
1658
|
+
function mapAnthropicBatchResultLineToBatchResult(resultLine) {
|
|
1659
|
+
return {
|
|
1660
|
+
customId: resultLine.custom_id,
|
|
1661
|
+
outcome: mapAnthropicBatchResultToOutcome(resultLine.result)
|
|
1662
|
+
};
|
|
1663
|
+
}
|
|
1664
|
+
function mapAnthropicBatchResultToOutcome(result) {
|
|
1665
|
+
if (result.type === "succeeded") {
|
|
1666
|
+
return {
|
|
1667
|
+
type: "succeeded",
|
|
1668
|
+
content: joinAnthropicBatchTextBlocks(result.message.content),
|
|
1669
|
+
tokenUsage: mapAnthropicUsageToTokenUsage(result.message.usage)
|
|
1670
|
+
};
|
|
1671
|
+
}
|
|
1672
|
+
if (result.type === "errored") {
|
|
1673
|
+
return {
|
|
1674
|
+
type: "errored",
|
|
1675
|
+
errorType: result.error.type,
|
|
1676
|
+
errorMessage: result.error.message
|
|
1677
|
+
};
|
|
1678
|
+
}
|
|
1679
|
+
if (result.type === "canceled") {
|
|
1680
|
+
return { type: "canceled" };
|
|
1681
|
+
}
|
|
1682
|
+
return { type: "expired" };
|
|
1683
|
+
}
|
|
1684
|
+
function joinAnthropicBatchTextBlocks(contentBlocks) {
|
|
1685
|
+
return contentBlocks.filter((contentBlock) => contentBlock.type === "text").map((contentBlock) => contentBlock.text).join("");
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
// src/anthropic-batch.ts
|
|
1689
|
+
var defaultAnthropicBatchPollIntervalMs = 5e3;
|
|
1690
|
+
var defaultAnthropicBatchTimeoutMs = 36e5;
|
|
1691
|
+
var anthropicBatchApiVersion = "2023-06-01";
|
|
1692
|
+
function createAnthropicBatchClient(options) {
|
|
1693
|
+
return {
|
|
1694
|
+
async submitMessageBatch(requests) {
|
|
1695
|
+
return await submitAnthropicMessageBatch(options, requests);
|
|
1696
|
+
},
|
|
1697
|
+
async fetchMessageBatch(batchId) {
|
|
1698
|
+
return await fetchAnthropicMessageBatch(options, batchId);
|
|
1699
|
+
},
|
|
1700
|
+
async listMessageBatchResults(batchId) {
|
|
1701
|
+
return await listAnthropicMessageBatchResults(options, batchId);
|
|
1702
|
+
},
|
|
1703
|
+
async waitForMessageBatchCompletion(batchId, waitOptions) {
|
|
1704
|
+
return await waitForAnthropicMessageBatchCompletion(
|
|
1705
|
+
options,
|
|
1706
|
+
batchId,
|
|
1707
|
+
waitOptions
|
|
1708
|
+
);
|
|
1709
|
+
}
|
|
1710
|
+
};
|
|
1711
|
+
}
|
|
1712
|
+
function resolveAnthropicBatchRequestContext(options) {
|
|
1713
|
+
return {
|
|
1714
|
+
apiKey: resolveProviderApiKey({
|
|
1715
|
+
providerLabel: "Anthropic",
|
|
1716
|
+
adapterFunctionName: "createAnthropicBatchClient()",
|
|
1717
|
+
baseEnvVarName: "ANTHROPIC_API_KEY",
|
|
1718
|
+
apiKey: options.apiKey,
|
|
1719
|
+
apiKeyName: options.apiKeyName
|
|
1720
|
+
}),
|
|
1721
|
+
baseUrl: options.baseURL ?? defaultAnthropicBaseUrl,
|
|
1722
|
+
fetchImplementation: options.fetchImplementation ?? fetch
|
|
1723
|
+
};
|
|
1724
|
+
}
|
|
1725
|
+
function buildAnthropicBatchRequestHeaders(apiKey) {
|
|
1726
|
+
return {
|
|
1727
|
+
"content-type": "application/json",
|
|
1728
|
+
"x-api-key": apiKey,
|
|
1729
|
+
"anthropic-version": anthropicBatchApiVersion
|
|
1730
|
+
};
|
|
1731
|
+
}
|
|
1732
|
+
async function submitAnthropicMessageBatch(options, requests) {
|
|
1733
|
+
const context = resolveAnthropicBatchRequestContext(options);
|
|
1734
|
+
const response = await context.fetchImplementation(
|
|
1735
|
+
`${context.baseUrl}/v1/messages/batches`,
|
|
1736
|
+
{
|
|
1737
|
+
method: "POST",
|
|
1738
|
+
headers: buildAnthropicBatchRequestHeaders(context.apiKey),
|
|
1739
|
+
body: JSON.stringify({
|
|
1740
|
+
requests: requests.map(mapBatchMessageRequestToApiBatchRequest)
|
|
1741
|
+
})
|
|
1742
|
+
}
|
|
1743
|
+
);
|
|
1744
|
+
return await parseAnthropicMessageBatchHttpResponse(response);
|
|
1745
|
+
}
|
|
1746
|
+
async function fetchAnthropicMessageBatch(options, batchId) {
|
|
1747
|
+
const context = resolveAnthropicBatchRequestContext(options);
|
|
1748
|
+
const response = await context.fetchImplementation(
|
|
1749
|
+
`${context.baseUrl}/v1/messages/batches/${batchId}`,
|
|
1750
|
+
{
|
|
1751
|
+
method: "GET",
|
|
1752
|
+
headers: buildAnthropicBatchRequestHeaders(context.apiKey)
|
|
1753
|
+
}
|
|
1754
|
+
);
|
|
1755
|
+
return await parseAnthropicMessageBatchHttpResponse(response);
|
|
1756
|
+
}
|
|
1757
|
+
async function parseAnthropicMessageBatchHttpResponse(response) {
|
|
1758
|
+
if (!response.ok) {
|
|
1759
|
+
throw buildProviderStatusError("Anthropic", response.status);
|
|
1760
|
+
}
|
|
1761
|
+
const parseResult = AnthropicMessageBatchResponseSchema.safeParse(
|
|
1762
|
+
await response.json()
|
|
1763
|
+
);
|
|
1764
|
+
if (!parseResult.success) {
|
|
1765
|
+
throw buildProviderInvalidResponseError("Anthropic", "message batch");
|
|
1766
|
+
}
|
|
1767
|
+
return mapAnthropicMessageBatchResponseToMessageBatch(parseResult.data);
|
|
1768
|
+
}
|
|
1769
|
+
async function listAnthropicMessageBatchResults(options, batchId) {
|
|
1770
|
+
const batch = await fetchAnthropicMessageBatch(options, batchId);
|
|
1771
|
+
if (batch.resultsUrl === null) {
|
|
1772
|
+
throw new AppError13({
|
|
1773
|
+
code: AppErrorCode12.Conflict,
|
|
1774
|
+
message: `Anthropic message batch ${batchId} has no results because processing has not ended`,
|
|
1775
|
+
statusCode: 409,
|
|
1776
|
+
details: { batchId, processingStatus: batch.processingStatus }
|
|
1777
|
+
});
|
|
1778
|
+
}
|
|
1779
|
+
const context = resolveAnthropicBatchRequestContext(options);
|
|
1780
|
+
const response = await context.fetchImplementation(batch.resultsUrl, {
|
|
1781
|
+
method: "GET",
|
|
1782
|
+
headers: buildAnthropicBatchRequestHeaders(context.apiKey)
|
|
1783
|
+
});
|
|
1784
|
+
if (!response.ok) {
|
|
1785
|
+
throw buildProviderStatusError("Anthropic", response.status);
|
|
1786
|
+
}
|
|
1787
|
+
return parseAnthropicBatchResultsJsonl(await response.text());
|
|
1788
|
+
}
|
|
1789
|
+
function parseAnthropicBatchResultsJsonl(jsonlText) {
|
|
1790
|
+
return jsonlText.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).map(parseAnthropicBatchResultLine);
|
|
1791
|
+
}
|
|
1792
|
+
function parseAnthropicBatchResultLine(line) {
|
|
1793
|
+
const parseResult = AnthropicBatchResultLineSchema.safeParse(
|
|
1794
|
+
parseAnthropicBatchResultLineJson(line)
|
|
1795
|
+
);
|
|
1796
|
+
if (!parseResult.success) {
|
|
1797
|
+
throw buildProviderInvalidResponseError(
|
|
1798
|
+
"Anthropic",
|
|
1799
|
+
"message batch results"
|
|
1800
|
+
);
|
|
1801
|
+
}
|
|
1802
|
+
return mapAnthropicBatchResultLineToBatchResult(parseResult.data);
|
|
1803
|
+
}
|
|
1804
|
+
function parseAnthropicBatchResultLineJson(line) {
|
|
1805
|
+
try {
|
|
1806
|
+
return JsonValueSchema6.parse(JSON.parse(line));
|
|
1807
|
+
} catch {
|
|
1808
|
+
throw buildProviderInvalidResponseError(
|
|
1809
|
+
"Anthropic",
|
|
1810
|
+
"message batch results"
|
|
1811
|
+
);
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
async function waitForAnthropicMessageBatchCompletion(options, batchId, waitOptions) {
|
|
1815
|
+
const pollIntervalMs = waitOptions?.pollIntervalMs ?? defaultAnthropicBatchPollIntervalMs;
|
|
1816
|
+
const timeoutMs = waitOptions?.timeoutMs ?? defaultAnthropicBatchTimeoutMs;
|
|
1817
|
+
const waitFor = waitOptions?.waitFor ?? waitForMilliseconds;
|
|
1818
|
+
const startedAt = Date.now();
|
|
1819
|
+
let batch = await fetchAnthropicMessageBatch(options, batchId);
|
|
1820
|
+
while (batch.processingStatus !== "ended") {
|
|
1821
|
+
if (Date.now() - startedAt >= timeoutMs) {
|
|
1822
|
+
throw buildAnthropicBatchTimeoutError(batchId, timeoutMs);
|
|
1823
|
+
}
|
|
1824
|
+
await waitFor(pollIntervalMs);
|
|
1825
|
+
batch = await fetchAnthropicMessageBatch(options, batchId);
|
|
1826
|
+
}
|
|
1827
|
+
return batch;
|
|
1828
|
+
}
|
|
1829
|
+
function buildAnthropicBatchTimeoutError(batchId, timeoutMs) {
|
|
1830
|
+
return new AppError13({
|
|
1831
|
+
code: AppErrorCode12.ServiceUnavailable,
|
|
1832
|
+
message: `Anthropic message batch ${batchId} did not complete within ${String(timeoutMs)}ms`,
|
|
1833
|
+
statusCode: 504,
|
|
1834
|
+
details: { batchId, timeoutMs }
|
|
1835
|
+
});
|
|
1836
|
+
}
|
|
1837
|
+
function mapBatchMessageRequestToApiBatchRequest(request) {
|
|
1838
|
+
const params = {
|
|
1839
|
+
model: request.model,
|
|
1840
|
+
max_tokens: request.maxOutputTokens ?? defaultAnthropicMaxOutputTokens,
|
|
1841
|
+
messages: request.messages.filter((message) => message.role !== "system").map(mapChatMessageToBatchApiRequestMessage)
|
|
1842
|
+
};
|
|
1843
|
+
const systemText = joinBatchSystemMessageText(request.messages);
|
|
1844
|
+
if (systemText.length > 0) {
|
|
1845
|
+
params.system = systemText;
|
|
1846
|
+
}
|
|
1847
|
+
if (request.temperature !== void 0) {
|
|
1848
|
+
params.temperature = request.temperature;
|
|
1849
|
+
}
|
|
1850
|
+
return { custom_id: request.customId, params };
|
|
1851
|
+
}
|
|
1852
|
+
function joinBatchSystemMessageText(messages) {
|
|
1853
|
+
return messages.filter((message) => message.role === "system").map(extractMessageText).join("\n\n");
|
|
1854
|
+
}
|
|
1855
|
+
function mapChatMessageToBatchApiRequestMessage(message) {
|
|
1856
|
+
if (message.role === "assistant") {
|
|
1857
|
+
return { role: "assistant", content: extractMessageText(message) };
|
|
1858
|
+
}
|
|
1859
|
+
if (message.role === "tool") {
|
|
1860
|
+
return { role: "user", content: buildBatchToolResultText(message) };
|
|
1861
|
+
}
|
|
1862
|
+
return { role: "user", content: extractMessageText(message) };
|
|
1863
|
+
}
|
|
1864
|
+
function buildBatchToolResultText(message) {
|
|
1865
|
+
if (message.toolName === void 0) {
|
|
1866
|
+
return `Tool result: ${extractMessageText(message)}`;
|
|
1867
|
+
}
|
|
1868
|
+
return `Tool result (${message.toolName}): ${extractMessageText(message)}`;
|
|
1869
|
+
}
|
|
1870
|
+
export {
|
|
1871
|
+
AnthropicMessagesResponseSchema,
|
|
1872
|
+
AnthropicStreamEventSchema,
|
|
1873
|
+
ChatMessageRoleSchema,
|
|
1874
|
+
ChatMessageSchema,
|
|
1875
|
+
ChatStopReasonSchema,
|
|
1876
|
+
MessageContentBlockSchema,
|
|
1877
|
+
ModelToolCallSchema,
|
|
1878
|
+
anthropic,
|
|
1879
|
+
buildAnthropicRequestBody,
|
|
1880
|
+
buildOpenAiRequestBody,
|
|
1881
|
+
buildProviderCanceledError,
|
|
1882
|
+
calculateProviderRetryDelayMs,
|
|
1883
|
+
createAnthropicBatchClient,
|
|
1884
|
+
defaultAnthropicBaseUrl,
|
|
1885
|
+
defaultAnthropicBatchPollIntervalMs,
|
|
1886
|
+
defaultAnthropicBatchTimeoutMs,
|
|
1887
|
+
defaultAnthropicMaxOutputTokens,
|
|
1888
|
+
defaultGeminiBaseUrl,
|
|
1889
|
+
defaultOpenAiBaseUrl,
|
|
1890
|
+
defaultProviderRetryInitialDelayMs,
|
|
1891
|
+
defaultProviderRetryMaxAttempts,
|
|
1892
|
+
defaultProviderRetryMaxDelayMs,
|
|
1893
|
+
estimateModelCallCostUsd,
|
|
1894
|
+
executeAbortableProviderFetch,
|
|
1895
|
+
executeProviderCallWithModelFallback,
|
|
1896
|
+
executeProviderCallWithRetry,
|
|
1897
|
+
extractAnthropicTextContent,
|
|
1898
|
+
extractAnthropicToolCalls,
|
|
1899
|
+
extractMessageText,
|
|
1900
|
+
gemini,
|
|
1901
|
+
isAbortError,
|
|
1902
|
+
isRetryableProviderAppError,
|
|
1903
|
+
joinSystemMessageText,
|
|
1904
|
+
mapAnthropicResponseToChatCompletionResult,
|
|
1905
|
+
mapAnthropicUsageToTokenUsage,
|
|
1906
|
+
mapChatMessageToAnthropicRequestMessage,
|
|
1907
|
+
mapChatMessageToOpenAiRequestMessage,
|
|
1908
|
+
mapModelToolChoiceToAnthropicToolChoice,
|
|
1909
|
+
mapModelToolChoiceToOpenAiToolChoice,
|
|
1910
|
+
mapModelToolDefinitionToOpenAiTool,
|
|
1911
|
+
mapModelToolDefinitionsToAnthropicTools,
|
|
1912
|
+
mockModel,
|
|
1913
|
+
normalizeAnthropicStopReason,
|
|
1914
|
+
openai,
|
|
1915
|
+
parseAnthropicStreamEvents,
|
|
1916
|
+
parseAnthropicToolInputJson,
|
|
1917
|
+
resolveProviderApiKey,
|
|
1918
|
+
streamAnthropicChatCompletion
|
|
1919
|
+
};
|