openclaw-autoproxy 1.0.2 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -159
- package/README.zh-CN.md +127 -0
- package/dist/gateway/anthropic-compat.js +841 -0
- package/dist/gateway/config.js +16 -0
- package/dist/gateway/model-load-metrics.js +114 -0
- package/dist/gateway/proxy.js +324 -19
- package/dist/gateway/server-http.js +12 -2
- package/dist/gateway/server.impl.js +1 -1
- package/package.json +2 -1
- package/src/gateway/anthropic-compat.ts +1085 -0
- package/src/gateway/config.ts +29 -0
- package/src/gateway/model-load-metrics.ts +166 -0
- package/src/gateway/proxy.ts +443 -25
- package/src/gateway/server-http.ts +16 -2
- package/src/gateway/server.impl.ts +1 -1
- package/openclaw-autoproxy-1.0.1.tgz +0 -0
|
@@ -0,0 +1,1085 @@
|
|
|
1
|
+
import { Transform } from "node:stream";
|
|
2
|
+
|
|
3
|
+
type JsonRecord = Record<string, unknown>;
|
|
4
|
+
|
|
5
|
+
export interface AnthropicCompatRequestTransformResult {
|
|
6
|
+
requestPath: string;
|
|
7
|
+
body: JsonRecord;
|
|
8
|
+
responseFormat: "anthropic-messages" | null;
|
|
9
|
+
error?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function isRecord(value: unknown): value is JsonRecord {
|
|
13
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function parsePathnameAndSearch(requestPath: string): { pathname: string; search: string } {
|
|
17
|
+
try {
|
|
18
|
+
const parsed = new URL(requestPath, "http://localhost");
|
|
19
|
+
return {
|
|
20
|
+
pathname: parsed.pathname,
|
|
21
|
+
search: parsed.search,
|
|
22
|
+
};
|
|
23
|
+
} catch {
|
|
24
|
+
const [pathnamePart, ...searchParts] = requestPath.split("?");
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
pathname: pathnamePart || "/",
|
|
28
|
+
search: searchParts.length > 0 ? `?${searchParts.join("?")}` : "",
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function isAnthropicMessagesPath(pathname: string): boolean {
|
|
34
|
+
return pathname === "/v1/messages";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isNativeAnthropicMessagesUpstream(upstreamUrl: string): boolean {
|
|
38
|
+
try {
|
|
39
|
+
const pathname = new URL(upstreamUrl).pathname.replace(/\/+$/, "");
|
|
40
|
+
return pathname.endsWith("/v1/messages");
|
|
41
|
+
} catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function stringifyToolResultContent(content: unknown): string {
|
|
47
|
+
if (typeof content === "string") {
|
|
48
|
+
return content;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (Array.isArray(content)) {
|
|
52
|
+
const textParts = content
|
|
53
|
+
.filter((item) => isRecord(item) && item.type === "text" && typeof item.text === "string")
|
|
54
|
+
.map((item) => String((item as JsonRecord).text));
|
|
55
|
+
|
|
56
|
+
if (textParts.length === content.length) {
|
|
57
|
+
return textParts.join("\n\n");
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
return JSON.stringify(content ?? "");
|
|
63
|
+
} catch {
|
|
64
|
+
return String(content ?? "");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function normalizeSystemPrompt(system: unknown): { value: string | null; error?: string } {
|
|
69
|
+
if (system === undefined || system === null) {
|
|
70
|
+
return { value: null };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (typeof system === "string") {
|
|
74
|
+
return { value: system };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!Array.isArray(system)) {
|
|
78
|
+
return {
|
|
79
|
+
value: null,
|
|
80
|
+
error: 'Anthropic request field "system" must be a string or an array of text blocks.',
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const textParts: string[] = [];
|
|
85
|
+
|
|
86
|
+
for (const block of system) {
|
|
87
|
+
if (!isRecord(block) || block.type !== "text" || typeof block.text !== "string") {
|
|
88
|
+
return {
|
|
89
|
+
value: null,
|
|
90
|
+
error:
|
|
91
|
+
'Anthropic request field "system" only supports text blocks when routed to an OpenAI-style upstream.',
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
textParts.push(block.text);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return { value: textParts.join("\n\n") };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function appendUserContentBlocks(content: unknown, target: JsonRecord[]): string | null {
|
|
102
|
+
if (typeof content === "string") {
|
|
103
|
+
target.push({
|
|
104
|
+
role: "user",
|
|
105
|
+
content,
|
|
106
|
+
});
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!Array.isArray(content)) {
|
|
111
|
+
return 'Anthropic user message content must be a string or an array of content blocks.';
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const pendingText: string[] = [];
|
|
115
|
+
|
|
116
|
+
const flushText = () => {
|
|
117
|
+
if (pendingText.length === 0) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
target.push({
|
|
122
|
+
role: "user",
|
|
123
|
+
content: pendingText.join("\n\n"),
|
|
124
|
+
});
|
|
125
|
+
pendingText.length = 0;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
for (const block of content) {
|
|
129
|
+
if (!isRecord(block) || typeof block.type !== "string") {
|
|
130
|
+
return 'Anthropic user message content contains an invalid block.';
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (block.type === "text") {
|
|
134
|
+
if (typeof block.text !== "string") {
|
|
135
|
+
return 'Anthropic text content blocks must include a string "text" field.';
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
pendingText.push(block.text);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (block.type === "tool_result") {
|
|
143
|
+
const toolUseId = typeof block.tool_use_id === "string" ? block.tool_use_id.trim() : "";
|
|
144
|
+
|
|
145
|
+
if (!toolUseId) {
|
|
146
|
+
return 'Anthropic tool_result blocks must include a non-empty "tool_use_id" field.';
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
flushText();
|
|
150
|
+
target.push({
|
|
151
|
+
role: "tool",
|
|
152
|
+
tool_call_id: toolUseId,
|
|
153
|
+
content: stringifyToolResultContent(block.content),
|
|
154
|
+
});
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return `Anthropic user content block type "${block.type}" is not supported for OpenAI-style upstream routes.`;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
flushText();
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function appendAssistantContentBlocks(content: unknown, target: JsonRecord[]): string | null {
|
|
166
|
+
if (typeof content === "string") {
|
|
167
|
+
target.push({
|
|
168
|
+
role: "assistant",
|
|
169
|
+
content,
|
|
170
|
+
});
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (!Array.isArray(content)) {
|
|
175
|
+
return 'Anthropic assistant message content must be a string or an array of content blocks.';
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const textParts: string[] = [];
|
|
179
|
+
const toolCalls: JsonRecord[] = [];
|
|
180
|
+
|
|
181
|
+
for (const block of content) {
|
|
182
|
+
if (!isRecord(block) || typeof block.type !== "string") {
|
|
183
|
+
return 'Anthropic assistant message content contains an invalid block.';
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (block.type === "text") {
|
|
187
|
+
if (typeof block.text !== "string") {
|
|
188
|
+
return 'Anthropic text content blocks must include a string "text" field.';
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
textParts.push(block.text);
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (block.type === "tool_use") {
|
|
196
|
+
const toolId = typeof block.id === "string" && block.id.trim()
|
|
197
|
+
? block.id.trim()
|
|
198
|
+
: `toolu_${Date.now()}_${toolCalls.length}`;
|
|
199
|
+
const toolName = typeof block.name === "string" ? block.name.trim() : "";
|
|
200
|
+
|
|
201
|
+
if (!toolName) {
|
|
202
|
+
return 'Anthropic tool_use blocks must include a non-empty "name" field.';
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
toolCalls.push({
|
|
206
|
+
id: toolId,
|
|
207
|
+
type: "function",
|
|
208
|
+
function: {
|
|
209
|
+
name: toolName,
|
|
210
|
+
arguments: JSON.stringify(block.input ?? {}),
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (block.type === "thinking" || block.type === "redacted_thinking") {
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return `Anthropic assistant content block type "${block.type}" is not supported for OpenAI-style upstream routes.`;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (textParts.length === 0 && toolCalls.length === 0) {
|
|
224
|
+
target.push({
|
|
225
|
+
role: "assistant",
|
|
226
|
+
content: "",
|
|
227
|
+
});
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
target.push({
|
|
232
|
+
role: "assistant",
|
|
233
|
+
content: textParts.length > 0 ? textParts.join("\n\n") : null,
|
|
234
|
+
...(toolCalls.length > 0 ? { tool_calls: toolCalls } : {}),
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function convertAnthropicTools(tools: unknown): { value?: JsonRecord[]; error?: string } {
|
|
241
|
+
if (tools === undefined) {
|
|
242
|
+
return { value: undefined };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (!Array.isArray(tools)) {
|
|
246
|
+
return { error: 'Anthropic request field "tools" must be an array.' };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const converted: JsonRecord[] = [];
|
|
250
|
+
|
|
251
|
+
for (const tool of tools) {
|
|
252
|
+
if (!isRecord(tool)) {
|
|
253
|
+
return { error: 'Anthropic request field "tools" contains an invalid entry.' };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const name = typeof tool.name === "string" ? tool.name.trim() : "";
|
|
257
|
+
|
|
258
|
+
if (!name) {
|
|
259
|
+
return { error: 'Anthropic tool entries must include a non-empty "name" field.' };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const inputSchema = tool.input_schema;
|
|
263
|
+
|
|
264
|
+
if (!isRecord(inputSchema)) {
|
|
265
|
+
return {
|
|
266
|
+
error:
|
|
267
|
+
'Anthropic tool entries must include an object "input_schema" field when routed to an OpenAI-style upstream.',
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const functionDefinition: JsonRecord = {
|
|
272
|
+
name,
|
|
273
|
+
parameters: inputSchema,
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
if (typeof tool.description === "string" && tool.description.trim()) {
|
|
277
|
+
functionDefinition.description = tool.description;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
converted.push({
|
|
281
|
+
type: "function",
|
|
282
|
+
function: functionDefinition,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return { value: converted };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function convertAnthropicToolChoice(toolChoice: unknown): { value?: unknown; error?: string } {
|
|
290
|
+
if (toolChoice === undefined) {
|
|
291
|
+
return { value: undefined };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (typeof toolChoice === "string") {
|
|
295
|
+
if (toolChoice === "auto" || toolChoice === "none" || toolChoice === "required") {
|
|
296
|
+
return { value: toolChoice };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return { error: `Unsupported Anthropic tool choice string "${toolChoice}".` };
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (!isRecord(toolChoice) || typeof toolChoice.type !== "string") {
|
|
303
|
+
return { error: 'Anthropic request field "tool_choice" must be a string or an object with a "type" field.' };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (toolChoice.type === "auto") {
|
|
307
|
+
return { value: "auto" };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (toolChoice.type === "any") {
|
|
311
|
+
return { value: "required" };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (toolChoice.type === "none") {
|
|
315
|
+
return { value: "none" };
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (toolChoice.type === "tool") {
|
|
319
|
+
const toolName = typeof toolChoice.name === "string" ? toolChoice.name.trim() : "";
|
|
320
|
+
|
|
321
|
+
if (!toolName) {
|
|
322
|
+
return { error: 'Anthropic tool_choice.type="tool" requires a non-empty "name" field.' };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return {
|
|
326
|
+
value: {
|
|
327
|
+
type: "function",
|
|
328
|
+
function: {
|
|
329
|
+
name: toolName,
|
|
330
|
+
},
|
|
331
|
+
},
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return { error: `Unsupported Anthropic tool choice type "${toolChoice.type}".` };
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function convertAnthropicMessagesRequest(body: JsonRecord): { value?: JsonRecord; error?: string } {
|
|
339
|
+
if (typeof body.model !== "string" || !body.model.trim()) {
|
|
340
|
+
return { error: 'Anthropic request field "model" is required.' };
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (!Array.isArray(body.messages)) {
|
|
344
|
+
return { error: 'Anthropic request field "messages" must be an array.' };
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const systemPrompt = normalizeSystemPrompt(body.system);
|
|
348
|
+
|
|
349
|
+
if (systemPrompt.error) {
|
|
350
|
+
return { error: systemPrompt.error };
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const messages: JsonRecord[] = [];
|
|
354
|
+
|
|
355
|
+
if (systemPrompt.value) {
|
|
356
|
+
messages.push({
|
|
357
|
+
role: "system",
|
|
358
|
+
content: systemPrompt.value,
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
for (const message of body.messages) {
|
|
363
|
+
if (!isRecord(message) || typeof message.role !== "string") {
|
|
364
|
+
return { error: 'Anthropic request field "messages" contains an invalid entry.' };
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (message.role === "user") {
|
|
368
|
+
const error = appendUserContentBlocks(message.content, messages);
|
|
369
|
+
|
|
370
|
+
if (error) {
|
|
371
|
+
return { error };
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (message.role === "assistant") {
|
|
378
|
+
const error = appendAssistantContentBlocks(message.content, messages);
|
|
379
|
+
|
|
380
|
+
if (error) {
|
|
381
|
+
return { error };
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return { error: `Anthropic message role "${message.role}" is not supported.` };
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const tools = convertAnthropicTools(body.tools);
|
|
391
|
+
|
|
392
|
+
if (tools.error) {
|
|
393
|
+
return { error: tools.error };
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const toolChoice = convertAnthropicToolChoice(body.tool_choice);
|
|
397
|
+
|
|
398
|
+
if (toolChoice.error) {
|
|
399
|
+
return { error: toolChoice.error };
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const converted: JsonRecord = {
|
|
403
|
+
model: body.model,
|
|
404
|
+
messages,
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
if (typeof body.max_tokens === "number") {
|
|
408
|
+
converted.max_tokens = body.max_tokens;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (typeof body.temperature === "number") {
|
|
412
|
+
converted.temperature = body.temperature;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (typeof body.top_p === "number") {
|
|
416
|
+
converted.top_p = body.top_p;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (typeof body.stream === "boolean") {
|
|
420
|
+
converted.stream = body.stream;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (Array.isArray(body.stop_sequences)) {
|
|
424
|
+
const stop = body.stop_sequences.filter((item) => typeof item === "string");
|
|
425
|
+
|
|
426
|
+
if (stop.length > 0) {
|
|
427
|
+
converted.stop = stop;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (tools.value && tools.value.length > 0) {
|
|
432
|
+
converted.tools = tools.value;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (toolChoice.value !== undefined) {
|
|
436
|
+
converted.tool_choice = toolChoice.value;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
return { value: converted };
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function extractOpenAiTextContent(content: unknown): string | null {
|
|
443
|
+
if (typeof content === "string") {
|
|
444
|
+
return content;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (!Array.isArray(content)) {
|
|
448
|
+
return null;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const textParts: string[] = [];
|
|
452
|
+
|
|
453
|
+
for (const item of content) {
|
|
454
|
+
if (typeof item === "string") {
|
|
455
|
+
textParts.push(item);
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (isRecord(item) && item.type === "text" && typeof item.text === "string") {
|
|
460
|
+
textParts.push(item.text);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
return textParts.join("\n\n");
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function parseOpenAiToolArguments(argumentsValue: unknown): unknown {
|
|
468
|
+
if (typeof argumentsValue !== "string") {
|
|
469
|
+
return isRecord(argumentsValue) || Array.isArray(argumentsValue) ? argumentsValue : {};
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
try {
|
|
473
|
+
return JSON.parse(argumentsValue);
|
|
474
|
+
} catch {
|
|
475
|
+
return {
|
|
476
|
+
_raw: argumentsValue,
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function mapOpenAiFinishReason(finishReason: unknown, hasToolUse: boolean): string {
|
|
482
|
+
if (finishReason === "tool_calls" || hasToolUse) {
|
|
483
|
+
return "tool_use";
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
if (finishReason === "length") {
|
|
487
|
+
return "max_tokens";
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
return "end_turn";
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function parseUsageValue(value: unknown): number {
|
|
494
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function extractErrorMessage(payload: unknown, fallbackStatus: number): string {
|
|
498
|
+
if (isRecord(payload)) {
|
|
499
|
+
if (typeof payload.message === "string" && payload.message.trim()) {
|
|
500
|
+
return payload.message;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
if (isRecord(payload.error) && typeof payload.error.message === "string" && payload.error.message.trim()) {
|
|
504
|
+
return payload.error.message;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
if (Array.isArray(payload.detail)) {
|
|
508
|
+
const details = payload.detail
|
|
509
|
+
.map((item) => {
|
|
510
|
+
if (typeof item === "string") {
|
|
511
|
+
return item;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
if (!isRecord(item)) {
|
|
515
|
+
return null;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const location = Array.isArray(item.loc)
|
|
519
|
+
? item.loc.map((part) => String(part)).join(".")
|
|
520
|
+
: null;
|
|
521
|
+
const message = typeof item.msg === "string" ? item.msg : null;
|
|
522
|
+
|
|
523
|
+
if (location && message) {
|
|
524
|
+
return `${location}: ${message}`;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
if (message) {
|
|
528
|
+
return message;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
return null;
|
|
532
|
+
})
|
|
533
|
+
.filter((item): item is string => Boolean(item));
|
|
534
|
+
|
|
535
|
+
if (details.length > 0) {
|
|
536
|
+
return details.join("\n");
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
try {
|
|
542
|
+
const serialized = JSON.stringify(payload);
|
|
543
|
+
return serialized.length > 0
|
|
544
|
+
? serialized
|
|
545
|
+
: `Upstream request failed with status ${fallbackStatus}.`;
|
|
546
|
+
} catch {
|
|
547
|
+
return `Upstream request failed with status ${fallbackStatus}.`;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function formatSseEvent(event: string, payload: unknown): string {
|
|
552
|
+
return `event: ${event}\ndata: ${JSON.stringify(payload)}\n\n`;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
interface AnthropicToolStreamState {
|
|
556
|
+
blockIndex: number;
|
|
557
|
+
id: string;
|
|
558
|
+
name: string;
|
|
559
|
+
open: boolean;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
interface AnthropicStreamState {
|
|
563
|
+
inputTokens: number;
|
|
564
|
+
messageId: string | null;
|
|
565
|
+
messageStarted: boolean;
|
|
566
|
+
messageStopped: boolean;
|
|
567
|
+
model: string | null;
|
|
568
|
+
nextBlockIndex: number;
|
|
569
|
+
openTextBlockIndex: number | null;
|
|
570
|
+
outputTokens: number;
|
|
571
|
+
stopReason: string | null;
|
|
572
|
+
sawToolUse: boolean;
|
|
573
|
+
toolBlocks: Map<number, AnthropicToolStreamState>;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function updateStreamUsage(state: AnthropicStreamState, payload: unknown): void {
|
|
577
|
+
if (!isRecord(payload) || !isRecord(payload.usage)) {
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
const promptTokens = parseUsageValue(payload.usage.prompt_tokens);
|
|
582
|
+
const completionTokens = parseUsageValue(payload.usage.completion_tokens);
|
|
583
|
+
|
|
584
|
+
if (promptTokens > 0) {
|
|
585
|
+
state.inputTokens = promptTokens;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
if (completionTokens >= 0) {
|
|
589
|
+
state.outputTokens = completionTokens;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function updateMessageIdentity(
|
|
594
|
+
state: AnthropicStreamState,
|
|
595
|
+
payload: unknown,
|
|
596
|
+
fallbackModel: string | null,
|
|
597
|
+
): void {
|
|
598
|
+
if (!isRecord(payload)) {
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
if (!state.messageId && typeof payload.id === "string" && payload.id.trim()) {
|
|
603
|
+
state.messageId = payload.id;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
if (!state.model && typeof payload.model === "string" && payload.model.trim()) {
|
|
607
|
+
state.model = payload.model;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
if (!state.model && fallbackModel) {
|
|
611
|
+
state.model = fallbackModel;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function ensureAnthropicMessageStart(state: AnthropicStreamState): string[] {
|
|
616
|
+
if (state.messageStarted) {
|
|
617
|
+
return [];
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
state.messageStarted = true;
|
|
621
|
+
|
|
622
|
+
return [
|
|
623
|
+
formatSseEvent("message_start", {
|
|
624
|
+
type: "message_start",
|
|
625
|
+
message: {
|
|
626
|
+
id: state.messageId ?? `msg_${Date.now()}`,
|
|
627
|
+
type: "message",
|
|
628
|
+
role: "assistant",
|
|
629
|
+
model: state.model ?? "",
|
|
630
|
+
content: [],
|
|
631
|
+
stop_reason: null,
|
|
632
|
+
stop_sequence: null,
|
|
633
|
+
usage: {
|
|
634
|
+
input_tokens: state.inputTokens,
|
|
635
|
+
output_tokens: 0,
|
|
636
|
+
},
|
|
637
|
+
},
|
|
638
|
+
}),
|
|
639
|
+
];
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
function closeAnthropicTextBlock(state: AnthropicStreamState): string[] {
|
|
643
|
+
if (state.openTextBlockIndex === null) {
|
|
644
|
+
return [];
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
const blockIndex = state.openTextBlockIndex;
|
|
648
|
+
state.openTextBlockIndex = null;
|
|
649
|
+
|
|
650
|
+
return [
|
|
651
|
+
formatSseEvent("content_block_stop", {
|
|
652
|
+
type: "content_block_stop",
|
|
653
|
+
index: blockIndex,
|
|
654
|
+
}),
|
|
655
|
+
];
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function closeAnthropicToolBlocks(state: AnthropicStreamState): string[] {
|
|
659
|
+
const events: string[] = [];
|
|
660
|
+
|
|
661
|
+
for (const block of [...state.toolBlocks.values()].sort((left, right) => left.blockIndex - right.blockIndex)) {
|
|
662
|
+
if (!block.open) {
|
|
663
|
+
continue;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
block.open = false;
|
|
667
|
+
events.push(
|
|
668
|
+
formatSseEvent("content_block_stop", {
|
|
669
|
+
type: "content_block_stop",
|
|
670
|
+
index: block.blockIndex,
|
|
671
|
+
}),
|
|
672
|
+
);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
return events;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function ensureAnthropicTextBlock(state: AnthropicStreamState): { events: string[]; blockIndex: number } {
|
|
679
|
+
if (state.openTextBlockIndex !== null) {
|
|
680
|
+
return {
|
|
681
|
+
events: [],
|
|
682
|
+
blockIndex: state.openTextBlockIndex,
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
const blockIndex = state.nextBlockIndex;
|
|
687
|
+
state.nextBlockIndex += 1;
|
|
688
|
+
state.openTextBlockIndex = blockIndex;
|
|
689
|
+
|
|
690
|
+
return {
|
|
691
|
+
blockIndex,
|
|
692
|
+
events: [
|
|
693
|
+
formatSseEvent("content_block_start", {
|
|
694
|
+
type: "content_block_start",
|
|
695
|
+
index: blockIndex,
|
|
696
|
+
content_block: {
|
|
697
|
+
type: "text",
|
|
698
|
+
text: "",
|
|
699
|
+
},
|
|
700
|
+
}),
|
|
701
|
+
],
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
function ensureAnthropicToolBlock(
|
|
706
|
+
state: AnthropicStreamState,
|
|
707
|
+
toolIndex: number,
|
|
708
|
+
toolCall: JsonRecord,
|
|
709
|
+
): { events: string[]; blockIndex: number } {
|
|
710
|
+
let block = state.toolBlocks.get(toolIndex);
|
|
711
|
+
const functionRecord = isRecord(toolCall.function) ? toolCall.function : {};
|
|
712
|
+
const nextId = typeof toolCall.id === "string" && toolCall.id.trim()
|
|
713
|
+
? toolCall.id.trim()
|
|
714
|
+
: block?.id ?? `toolu_${Date.now()}_${toolIndex}`;
|
|
715
|
+
const nextName = typeof functionRecord.name === "string" && functionRecord.name.trim()
|
|
716
|
+
? functionRecord.name.trim()
|
|
717
|
+
: block?.name ?? `tool_${toolIndex}`;
|
|
718
|
+
|
|
719
|
+
if (!block) {
|
|
720
|
+
block = {
|
|
721
|
+
blockIndex: state.nextBlockIndex,
|
|
722
|
+
id: nextId,
|
|
723
|
+
name: nextName,
|
|
724
|
+
open: false,
|
|
725
|
+
};
|
|
726
|
+
state.nextBlockIndex += 1;
|
|
727
|
+
state.toolBlocks.set(toolIndex, block);
|
|
728
|
+
} else {
|
|
729
|
+
block.id = nextId;
|
|
730
|
+
block.name = nextName;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
if (block.open) {
|
|
734
|
+
return {
|
|
735
|
+
events: [],
|
|
736
|
+
blockIndex: block.blockIndex,
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
block.open = true;
|
|
741
|
+
state.sawToolUse = true;
|
|
742
|
+
|
|
743
|
+
return {
|
|
744
|
+
blockIndex: block.blockIndex,
|
|
745
|
+
events: [
|
|
746
|
+
formatSseEvent("content_block_start", {
|
|
747
|
+
type: "content_block_start",
|
|
748
|
+
index: block.blockIndex,
|
|
749
|
+
content_block: {
|
|
750
|
+
type: "tool_use",
|
|
751
|
+
id: block.id,
|
|
752
|
+
name: block.name,
|
|
753
|
+
input: {},
|
|
754
|
+
},
|
|
755
|
+
}),
|
|
756
|
+
],
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
function finalizeAnthropicMessageStream(state: AnthropicStreamState): string[] {
|
|
761
|
+
if (!state.messageStarted || state.messageStopped) {
|
|
762
|
+
return [];
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
const events = [
|
|
766
|
+
...closeAnthropicTextBlock(state),
|
|
767
|
+
...closeAnthropicToolBlocks(state),
|
|
768
|
+
formatSseEvent("message_delta", {
|
|
769
|
+
type: "message_delta",
|
|
770
|
+
delta: {
|
|
771
|
+
stop_reason: state.stopReason ?? (state.sawToolUse ? "tool_use" : "end_turn"),
|
|
772
|
+
stop_sequence: null,
|
|
773
|
+
},
|
|
774
|
+
usage: {
|
|
775
|
+
output_tokens: state.outputTokens,
|
|
776
|
+
},
|
|
777
|
+
}),
|
|
778
|
+
formatSseEvent("message_stop", {
|
|
779
|
+
type: "message_stop",
|
|
780
|
+
}),
|
|
781
|
+
];
|
|
782
|
+
|
|
783
|
+
state.messageStopped = true;
|
|
784
|
+
return events;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
function parseSseData(frame: string): string | null {
|
|
788
|
+
const dataLines = frame
|
|
789
|
+
.split("\n")
|
|
790
|
+
.filter((line) => line.startsWith("data:"))
|
|
791
|
+
.map((line) => line.slice(5).trimStart());
|
|
792
|
+
|
|
793
|
+
if (dataLines.length === 0) {
|
|
794
|
+
return null;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
return dataLines.join("\n");
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
function transformOpenAiStreamPayloadToAnthropicEvents(params: {
|
|
801
|
+
fallbackModel: string | null;
|
|
802
|
+
payload: unknown;
|
|
803
|
+
state: AnthropicStreamState;
|
|
804
|
+
}): string[] {
|
|
805
|
+
const { payload, state, fallbackModel } = params;
|
|
806
|
+
updateStreamUsage(state, payload);
|
|
807
|
+
updateMessageIdentity(state, payload, fallbackModel);
|
|
808
|
+
|
|
809
|
+
const events = ensureAnthropicMessageStart(state);
|
|
810
|
+
|
|
811
|
+
if (!isRecord(payload) || !Array.isArray(payload.choices)) {
|
|
812
|
+
return events;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
for (const choice of payload.choices) {
|
|
816
|
+
if (!isRecord(choice) || !isRecord(choice.delta)) {
|
|
817
|
+
continue;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
const delta = choice.delta;
|
|
821
|
+
|
|
822
|
+
if (typeof delta.content === "string" && delta.content.length > 0) {
|
|
823
|
+
events.push(...closeAnthropicToolBlocks(state));
|
|
824
|
+
const textBlock = ensureAnthropicTextBlock(state);
|
|
825
|
+
events.push(...textBlock.events);
|
|
826
|
+
events.push(
|
|
827
|
+
formatSseEvent("content_block_delta", {
|
|
828
|
+
type: "content_block_delta",
|
|
829
|
+
index: textBlock.blockIndex,
|
|
830
|
+
delta: {
|
|
831
|
+
type: "text_delta",
|
|
832
|
+
text: delta.content,
|
|
833
|
+
},
|
|
834
|
+
}),
|
|
835
|
+
);
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
if (Array.isArray(delta.tool_calls)) {
|
|
839
|
+
events.push(...closeAnthropicTextBlock(state));
|
|
840
|
+
|
|
841
|
+
for (const [fallbackIndex, entry] of delta.tool_calls.entries()) {
|
|
842
|
+
if (!isRecord(entry)) {
|
|
843
|
+
continue;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
const toolIndex = typeof entry.index === "number" ? entry.index : fallbackIndex;
|
|
847
|
+
const toolBlock = ensureAnthropicToolBlock(state, toolIndex, entry);
|
|
848
|
+
events.push(...toolBlock.events);
|
|
849
|
+
|
|
850
|
+
const functionRecord = isRecord(entry.function) ? entry.function : {};
|
|
851
|
+
const partialJson = typeof functionRecord.arguments === "string" ? functionRecord.arguments : "";
|
|
852
|
+
|
|
853
|
+
if (partialJson.length > 0) {
|
|
854
|
+
events.push(
|
|
855
|
+
formatSseEvent("content_block_delta", {
|
|
856
|
+
type: "content_block_delta",
|
|
857
|
+
index: toolBlock.blockIndex,
|
|
858
|
+
delta: {
|
|
859
|
+
type: "input_json_delta",
|
|
860
|
+
partial_json: partialJson,
|
|
861
|
+
},
|
|
862
|
+
}),
|
|
863
|
+
);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
if (choice.finish_reason !== null && choice.finish_reason !== undefined) {
|
|
869
|
+
state.stopReason = mapOpenAiFinishReason(choice.finish_reason, state.sawToolUse);
|
|
870
|
+
events.push(...closeAnthropicTextBlock(state));
|
|
871
|
+
events.push(...closeAnthropicToolBlocks(state));
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
return events;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
export function createAnthropicMessagesEventStreamTransformer(
|
|
879
|
+
fallbackModel: string | null,
|
|
880
|
+
): Transform {
|
|
881
|
+
const state: AnthropicStreamState = {
|
|
882
|
+
inputTokens: 0,
|
|
883
|
+
messageId: null,
|
|
884
|
+
messageStarted: false,
|
|
885
|
+
messageStopped: false,
|
|
886
|
+
model: fallbackModel,
|
|
887
|
+
nextBlockIndex: 0,
|
|
888
|
+
openTextBlockIndex: null,
|
|
889
|
+
outputTokens: 0,
|
|
890
|
+
sawToolUse: false,
|
|
891
|
+
stopReason: null,
|
|
892
|
+
toolBlocks: new Map(),
|
|
893
|
+
};
|
|
894
|
+
|
|
895
|
+
let buffer = "";
|
|
896
|
+
|
|
897
|
+
return new Transform({
|
|
898
|
+
transform(chunk, _encoding, callback) {
|
|
899
|
+
try {
|
|
900
|
+
buffer += chunk.toString("utf8").replace(/\r\n/g, "\n");
|
|
901
|
+
|
|
902
|
+
while (true) {
|
|
903
|
+
const delimiterIndex = buffer.indexOf("\n\n");
|
|
904
|
+
|
|
905
|
+
if (delimiterIndex === -1) {
|
|
906
|
+
break;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
const frame = buffer.slice(0, delimiterIndex);
|
|
910
|
+
buffer = buffer.slice(delimiterIndex + 2);
|
|
911
|
+
const data = parseSseData(frame);
|
|
912
|
+
|
|
913
|
+
if (!data) {
|
|
914
|
+
continue;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
if (data === "[DONE]") {
|
|
918
|
+
this.push(finalizeAnthropicMessageStream(state).join(""));
|
|
919
|
+
continue;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
try {
|
|
923
|
+
const payload = JSON.parse(data);
|
|
924
|
+
const events = transformOpenAiStreamPayloadToAnthropicEvents({
|
|
925
|
+
fallbackModel,
|
|
926
|
+
payload,
|
|
927
|
+
state,
|
|
928
|
+
});
|
|
929
|
+
|
|
930
|
+
if (events.length > 0) {
|
|
931
|
+
this.push(events.join(""));
|
|
932
|
+
}
|
|
933
|
+
} catch {
|
|
934
|
+
// Ignore malformed SSE payload chunks and continue streaming.
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
callback();
|
|
939
|
+
} catch (error) {
|
|
940
|
+
callback(error as Error);
|
|
941
|
+
}
|
|
942
|
+
},
|
|
943
|
+
flush(callback) {
|
|
944
|
+
try {
|
|
945
|
+
this.push(finalizeAnthropicMessageStream(state).join(""));
|
|
946
|
+
callback();
|
|
947
|
+
} catch (error) {
|
|
948
|
+
callback(error as Error);
|
|
949
|
+
}
|
|
950
|
+
},
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
export function maybeTransformAnthropicMessagesRequest(params: {
|
|
955
|
+
requestPath: string;
|
|
956
|
+
upstreamUrl: string;
|
|
957
|
+
body: JsonRecord;
|
|
958
|
+
}): AnthropicCompatRequestTransformResult {
|
|
959
|
+
const { pathname, search } = parsePathnameAndSearch(params.requestPath);
|
|
960
|
+
|
|
961
|
+
if (!isAnthropicMessagesPath(pathname)) {
|
|
962
|
+
return {
|
|
963
|
+
requestPath: params.requestPath,
|
|
964
|
+
body: params.body,
|
|
965
|
+
responseFormat: null,
|
|
966
|
+
};
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
if (isNativeAnthropicMessagesUpstream(params.upstreamUrl)) {
|
|
970
|
+
return {
|
|
971
|
+
requestPath: params.requestPath,
|
|
972
|
+
body: params.body,
|
|
973
|
+
responseFormat: null,
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
const converted = convertAnthropicMessagesRequest(params.body);
|
|
978
|
+
|
|
979
|
+
if (converted.error || !converted.value) {
|
|
980
|
+
return {
|
|
981
|
+
requestPath: params.requestPath,
|
|
982
|
+
body: params.body,
|
|
983
|
+
responseFormat: null,
|
|
984
|
+
error:
|
|
985
|
+
converted.error ??
|
|
986
|
+
'Gateway failed to translate the Anthropic request for the selected OpenAI-style upstream route.',
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
return {
|
|
991
|
+
requestPath: `/v1/chat/completions${search}`,
|
|
992
|
+
body: converted.value,
|
|
993
|
+
responseFormat: "anthropic-messages",
|
|
994
|
+
};
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
export function transformOpenAiChatCompletionToAnthropicMessage(
|
|
998
|
+
payload: unknown,
|
|
999
|
+
fallbackModel: string | null,
|
|
1000
|
+
): { value?: JsonRecord; error?: string } {
|
|
1001
|
+
if (!isRecord(payload) || !Array.isArray(payload.choices) || payload.choices.length === 0) {
|
|
1002
|
+
return { error: 'Upstream response is not a valid OpenAI chat completion payload.' };
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
const firstChoice = payload.choices[0];
|
|
1006
|
+
|
|
1007
|
+
if (!isRecord(firstChoice) || !isRecord(firstChoice.message)) {
|
|
1008
|
+
return { error: 'Upstream response does not include a chat completion message.' };
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
const message = firstChoice.message;
|
|
1012
|
+
const content: JsonRecord[] = [];
|
|
1013
|
+
const text = extractOpenAiTextContent(message.content);
|
|
1014
|
+
const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : [];
|
|
1015
|
+
|
|
1016
|
+
if (typeof text === "string" && text.length > 0) {
|
|
1017
|
+
content.push({
|
|
1018
|
+
type: "text",
|
|
1019
|
+
text,
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
for (const toolCall of toolCalls) {
|
|
1024
|
+
if (!isRecord(toolCall) || !isRecord(toolCall.function)) {
|
|
1025
|
+
continue;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
const toolId = typeof toolCall.id === "string" && toolCall.id.trim()
|
|
1029
|
+
? toolCall.id.trim()
|
|
1030
|
+
: `toolu_${Date.now()}_${content.length}`;
|
|
1031
|
+
const toolName = typeof toolCall.function.name === "string" ? toolCall.function.name.trim() : "";
|
|
1032
|
+
|
|
1033
|
+
if (!toolName) {
|
|
1034
|
+
continue;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
content.push({
|
|
1038
|
+
type: "tool_use",
|
|
1039
|
+
id: toolId,
|
|
1040
|
+
name: toolName,
|
|
1041
|
+
input: parseOpenAiToolArguments(toolCall.function.arguments),
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
if (content.length === 0) {
|
|
1046
|
+
content.push({
|
|
1047
|
+
type: "text",
|
|
1048
|
+
text: text ?? "",
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
const usage = isRecord(payload.usage) ? payload.usage : {};
|
|
1053
|
+
const resolvedModel = typeof payload.model === "string" && payload.model.trim()
|
|
1054
|
+
? payload.model
|
|
1055
|
+
: fallbackModel ?? "";
|
|
1056
|
+
|
|
1057
|
+
return {
|
|
1058
|
+
value: {
|
|
1059
|
+
id:
|
|
1060
|
+
typeof payload.id === "string" && payload.id.trim()
|
|
1061
|
+
? payload.id
|
|
1062
|
+
: `msg_${Date.now()}`,
|
|
1063
|
+
type: "message",
|
|
1064
|
+
role: "assistant",
|
|
1065
|
+
content,
|
|
1066
|
+
model: resolvedModel,
|
|
1067
|
+
stop_reason: mapOpenAiFinishReason(firstChoice.finish_reason, toolCalls.length > 0),
|
|
1068
|
+
stop_sequence: null,
|
|
1069
|
+
usage: {
|
|
1070
|
+
input_tokens: parseUsageValue(usage.prompt_tokens),
|
|
1071
|
+
output_tokens: parseUsageValue(usage.completion_tokens),
|
|
1072
|
+
},
|
|
1073
|
+
},
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
export function transformUpstreamErrorToAnthropicError(payload: unknown, statusCode: number): JsonRecord {
|
|
1078
|
+
return {
|
|
1079
|
+
type: "error",
|
|
1080
|
+
error: {
|
|
1081
|
+
type: statusCode >= 500 ? "api_error" : "invalid_request_error",
|
|
1082
|
+
message: extractErrorMessage(payload, statusCode),
|
|
1083
|
+
},
|
|
1084
|
+
};
|
|
1085
|
+
}
|