doer-agent 0.8.7 → 0.8.8
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/dist/agent-skill-rpc.js
CHANGED
|
@@ -75,6 +75,9 @@ function resolveCodexModel(settings) {
|
|
|
75
75
|
if (providerKey === "zai") {
|
|
76
76
|
return settings.codex.providerModels.zai || "glm-5.2";
|
|
77
77
|
}
|
|
78
|
+
if (providerKey === "anthropic") {
|
|
79
|
+
return settings.codex.providerModels.anthropic || "claude-sonnet-4-6";
|
|
80
|
+
}
|
|
78
81
|
return settings.codex.providerModels[providerKey] || settings.codex.providerModels.openai || "gpt-5.5";
|
|
79
82
|
}
|
|
80
83
|
async function generateSkillViaCodex(args) {
|
|
@@ -16,10 +16,20 @@ function resolveCodexModel(settings) {
|
|
|
16
16
|
if (providerKey === "zai") {
|
|
17
17
|
return settings.codex.providerModels.zai || "glm-5.2";
|
|
18
18
|
}
|
|
19
|
+
if (providerKey === "anthropic") {
|
|
20
|
+
return settings.codex.providerModels.anthropic || "claude-sonnet-4-6";
|
|
21
|
+
}
|
|
19
22
|
return settings.codex.providerModels[providerKey] || settings.codex.providerModels.openai || "gpt-5.5";
|
|
20
23
|
}
|
|
21
24
|
function resolveCodexModelContextWindow(settings) {
|
|
22
|
-
|
|
25
|
+
if (settings.codex.modelProvider === "zai") {
|
|
26
|
+
return 258400;
|
|
27
|
+
}
|
|
28
|
+
if (settings.codex.modelProvider === "anthropic") {
|
|
29
|
+
const model = resolveCodexModel(settings);
|
|
30
|
+
return model.startsWith("claude-haiku-") ? 200000 : 1000000;
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
23
33
|
}
|
|
24
34
|
function buildModelProviderConfigArgs(settings) {
|
|
25
35
|
const provider = settings.codex.customProvider;
|
|
@@ -87,7 +97,9 @@ async function buildCodexAppServerArgs(args) {
|
|
|
87
97
|
}
|
|
88
98
|
async function resolveAppServerSettings(args) {
|
|
89
99
|
const provider = args.settings.codex.customProvider;
|
|
90
|
-
|
|
100
|
+
const providerId = args.settings.codex.modelProvider;
|
|
101
|
+
const gatewayProviderIds = new Set(["zai", "anthropic"]);
|
|
102
|
+
if (!providerId || provider?.id !== providerId || !gatewayProviderIds.has(providerId)) {
|
|
91
103
|
return { settings: args.settings, proxy: null };
|
|
92
104
|
}
|
|
93
105
|
const proxy = await startCodexChatBridge({
|
|
@@ -1,17 +1,40 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { createServer } from "node:http";
|
|
3
3
|
const DEFAULT_CHAT_BASE_URL = "https://api.z.ai/api/coding/paas/v4";
|
|
4
|
+
const DEFAULT_ANTHROPIC_BASE_URL = "https://api.anthropic.com/v1";
|
|
4
5
|
const CHAT_BRIDGE_ENV_KEY = "DOER_CODEX_CHAT_BRIDGE_API_KEY";
|
|
6
|
+
const ANTHROPIC_VERSION = "2023-06-01";
|
|
7
|
+
const PROVIDER_ADAPTERS = {
|
|
8
|
+
anthropic: {
|
|
9
|
+
kind: "anthropic_messages",
|
|
10
|
+
defaultBaseUrl: DEFAULT_ANTHROPIC_BASE_URL,
|
|
11
|
+
upstreamAuthHeaders: (apiKey) => ({
|
|
12
|
+
"x-api-key": apiKey,
|
|
13
|
+
"anthropic-version": ANTHROPIC_VERSION,
|
|
14
|
+
}),
|
|
15
|
+
},
|
|
16
|
+
default: {
|
|
17
|
+
kind: "chat_completions",
|
|
18
|
+
defaultBaseUrl: DEFAULT_CHAT_BASE_URL,
|
|
19
|
+
upstreamAuthHeaders: (apiKey) => ({
|
|
20
|
+
authorization: `Bearer ${apiKey}`,
|
|
21
|
+
}),
|
|
22
|
+
},
|
|
23
|
+
};
|
|
5
24
|
function normalizeBaseUrl(value) {
|
|
6
25
|
return value.replace(/\/+$/, "");
|
|
7
26
|
}
|
|
27
|
+
function resolveProviderAdapter(providerId) {
|
|
28
|
+
return PROVIDER_ADAPTERS[providerId?.trim().toLowerCase() || ""] ?? PROVIDER_ADAPTERS.default;
|
|
29
|
+
}
|
|
8
30
|
function resolveTargetBaseUrl(args) {
|
|
9
31
|
const providerId = args.providerId?.trim().toLowerCase();
|
|
10
32
|
const baseUrl = args.targetBaseUrl?.trim();
|
|
11
33
|
if (providerId === "zai" && (!baseUrl || !baseUrl.includes("/coding/paas/"))) {
|
|
12
34
|
return DEFAULT_CHAT_BASE_URL;
|
|
13
35
|
}
|
|
14
|
-
|
|
36
|
+
const adapter = resolveProviderAdapter(providerId);
|
|
37
|
+
return normalizeBaseUrl(baseUrl || adapter.defaultBaseUrl);
|
|
15
38
|
}
|
|
16
39
|
function stringValue(value) {
|
|
17
40
|
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
@@ -67,6 +90,15 @@ function contentToText(value) {
|
|
|
67
90
|
.filter(Boolean)
|
|
68
91
|
.join("\n");
|
|
69
92
|
}
|
|
93
|
+
function parseJsonObject(value) {
|
|
94
|
+
try {
|
|
95
|
+
const parsed = JSON.parse(value);
|
|
96
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return {};
|
|
100
|
+
}
|
|
101
|
+
}
|
|
70
102
|
function responsePartToChatPart(part) {
|
|
71
103
|
if (typeof part === "string") {
|
|
72
104
|
return part ? { type: "text", text: part } : null;
|
|
@@ -368,6 +400,160 @@ function buildChatRequest(body, stream, onLog) {
|
|
|
368
400
|
...(tools.length > 0 ? { tools } : {}),
|
|
369
401
|
};
|
|
370
402
|
}
|
|
403
|
+
function anthropicContentPartFromChatPart(part) {
|
|
404
|
+
const type = stringValue(part.type);
|
|
405
|
+
if (type === "text") {
|
|
406
|
+
const text = textValue(part.text);
|
|
407
|
+
return text ? { type: "text", text } : null;
|
|
408
|
+
}
|
|
409
|
+
if (type === "image_url") {
|
|
410
|
+
const rawImageUrl = recordValue(part.image_url);
|
|
411
|
+
const imageUrl = textValue(part.image_url) ?? textValue(rawImageUrl?.url);
|
|
412
|
+
const dataUrlMatch = imageUrl?.match(/^data:([^;,]+);base64,(.+)$/);
|
|
413
|
+
if (!dataUrlMatch) {
|
|
414
|
+
return null;
|
|
415
|
+
}
|
|
416
|
+
return {
|
|
417
|
+
type: "image",
|
|
418
|
+
source: {
|
|
419
|
+
type: "base64",
|
|
420
|
+
media_type: dataUrlMatch[1],
|
|
421
|
+
data: dataUrlMatch[2],
|
|
422
|
+
},
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
return null;
|
|
426
|
+
}
|
|
427
|
+
function anthropicContentFromChatContent(content) {
|
|
428
|
+
if (typeof content === "string") {
|
|
429
|
+
return content;
|
|
430
|
+
}
|
|
431
|
+
if (!Array.isArray(content)) {
|
|
432
|
+
return "";
|
|
433
|
+
}
|
|
434
|
+
const parts = content
|
|
435
|
+
.map((part) => recordValue(part))
|
|
436
|
+
.filter((part) => Boolean(part))
|
|
437
|
+
.map(anthropicContentPartFromChatPart)
|
|
438
|
+
.filter((part) => Boolean(part));
|
|
439
|
+
return parts.length > 0 ? parts : "";
|
|
440
|
+
}
|
|
441
|
+
function anthropicMessagesFromChatMessages(messages) {
|
|
442
|
+
const systemParts = [];
|
|
443
|
+
const anthropicMessages = [];
|
|
444
|
+
for (const message of messages) {
|
|
445
|
+
const role = stringValue(message.role);
|
|
446
|
+
if (role === "system" || role === "developer") {
|
|
447
|
+
const text = contentToText(message.content);
|
|
448
|
+
if (text) {
|
|
449
|
+
systemParts.push(text);
|
|
450
|
+
}
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
if (role === "tool") {
|
|
454
|
+
const toolUseId = stringValue(message.tool_call_id);
|
|
455
|
+
if (!toolUseId) {
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
anthropicMessages.push({
|
|
459
|
+
role: "user",
|
|
460
|
+
content: [{
|
|
461
|
+
type: "tool_result",
|
|
462
|
+
tool_use_id: toolUseId,
|
|
463
|
+
content: contentToText(message.content),
|
|
464
|
+
}],
|
|
465
|
+
});
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
if (role === "assistant") {
|
|
469
|
+
const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : [];
|
|
470
|
+
if (toolCalls.length > 0) {
|
|
471
|
+
const content = toolCalls
|
|
472
|
+
.map((toolCall) => {
|
|
473
|
+
const toolCallRecord = recordValue(toolCall);
|
|
474
|
+
const fn = recordValue(toolCallRecord?.function);
|
|
475
|
+
const name = stringValue(fn?.name);
|
|
476
|
+
if (!toolCallRecord || !fn || !name) {
|
|
477
|
+
return null;
|
|
478
|
+
}
|
|
479
|
+
return {
|
|
480
|
+
type: "tool_use",
|
|
481
|
+
id: stringValue(toolCallRecord.id) ?? `call_${randomUUID().replace(/-/g, "")}`,
|
|
482
|
+
name,
|
|
483
|
+
input: parseJsonObject(typeof fn.arguments === "string" ? fn.arguments : ""),
|
|
484
|
+
};
|
|
485
|
+
})
|
|
486
|
+
.filter((item) => item !== null);
|
|
487
|
+
if (content.length > 0) {
|
|
488
|
+
anthropicMessages.push({ role: "assistant", content });
|
|
489
|
+
}
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
anthropicMessages.push({ role: "assistant", content: anthropicContentFromChatContent(message.content) });
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
anthropicMessages.push({ role: "user", content: anthropicContentFromChatContent(message.content) });
|
|
496
|
+
}
|
|
497
|
+
return {
|
|
498
|
+
system: systemParts.length > 0 ? systemParts.join("\n\n") : null,
|
|
499
|
+
messages: anthropicMessages,
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
function anthropicToolsFromChatTools(tools) {
|
|
503
|
+
if (!Array.isArray(tools)) {
|
|
504
|
+
return [];
|
|
505
|
+
}
|
|
506
|
+
const converted = tools
|
|
507
|
+
.map((tool) => {
|
|
508
|
+
const record = recordValue(tool);
|
|
509
|
+
const fn = recordValue(record?.function);
|
|
510
|
+
const name = stringValue(fn?.name);
|
|
511
|
+
if (!fn || !name) {
|
|
512
|
+
return null;
|
|
513
|
+
}
|
|
514
|
+
return {
|
|
515
|
+
name,
|
|
516
|
+
description: stringValue(fn?.description) ?? "",
|
|
517
|
+
input_schema: schemaValue(fn?.parameters),
|
|
518
|
+
};
|
|
519
|
+
});
|
|
520
|
+
return converted.filter((tool) => tool !== null);
|
|
521
|
+
}
|
|
522
|
+
function anthropicToolChoiceFromChatToolChoice(value) {
|
|
523
|
+
const text = stringValue(value);
|
|
524
|
+
if (text === "auto") {
|
|
525
|
+
return { type: "auto" };
|
|
526
|
+
}
|
|
527
|
+
if (text === "required") {
|
|
528
|
+
return { type: "any" };
|
|
529
|
+
}
|
|
530
|
+
if (text === "none") {
|
|
531
|
+
return undefined;
|
|
532
|
+
}
|
|
533
|
+
const record = recordValue(value);
|
|
534
|
+
const fn = recordValue(record?.function);
|
|
535
|
+
const name = stringValue(fn?.name);
|
|
536
|
+
return name ? { type: "tool", name } : undefined;
|
|
537
|
+
}
|
|
538
|
+
function buildAnthropicRequest(body, stream, onLog) {
|
|
539
|
+
const chatRequest = buildChatRequest(body, stream, onLog);
|
|
540
|
+
const chatMessages = Array.isArray(chatRequest.messages) ? chatRequest.messages : [];
|
|
541
|
+
const converted = anthropicMessagesFromChatMessages(chatMessages);
|
|
542
|
+
const tools = anthropicToolsFromChatTools(chatRequest.tools);
|
|
543
|
+
const toolChoice = anthropicToolChoiceFromChatToolChoice(chatRequest.tool_choice);
|
|
544
|
+
return {
|
|
545
|
+
model: stringValue(chatRequest.model) ?? "claude-sonnet-4-6",
|
|
546
|
+
messages: converted.messages,
|
|
547
|
+
stream,
|
|
548
|
+
max_tokens: finiteNumber(chatRequest.max_tokens) ?? 4096,
|
|
549
|
+
...(converted.system ? { system: converted.system } : {}),
|
|
550
|
+
...(typeof chatRequest.temperature === "number" ? { temperature: chatRequest.temperature } : {}),
|
|
551
|
+
...(typeof chatRequest.top_p === "number" ? { top_p: chatRequest.top_p } : {}),
|
|
552
|
+
...(typeof chatRequest.stop === "string" || Array.isArray(chatRequest.stop) ? { stop_sequences: chatRequest.stop } : {}),
|
|
553
|
+
...(tools.length > 0 ? { tools } : {}),
|
|
554
|
+
...(toolChoice ? { tool_choice: toolChoice } : {}),
|
|
555
|
+
};
|
|
556
|
+
}
|
|
371
557
|
function upstreamErrorMessage(value) {
|
|
372
558
|
const body = recordValue(value);
|
|
373
559
|
const error = recordValue(body?.error);
|
|
@@ -472,6 +658,89 @@ function chatMessageToResponseOutput(message) {
|
|
|
472
658
|
content: [{ type: "output_text", text: contentToText(message.content), annotations: [] }],
|
|
473
659
|
}];
|
|
474
660
|
}
|
|
661
|
+
function anthropicUsageToResponseUsage(value) {
|
|
662
|
+
const usage = recordValue(value);
|
|
663
|
+
if (!usage) {
|
|
664
|
+
return null;
|
|
665
|
+
}
|
|
666
|
+
const inputTokens = finiteNumber(usage.input_tokens) ?? 0;
|
|
667
|
+
const outputTokens = finiteNumber(usage.output_tokens) ?? 0;
|
|
668
|
+
return {
|
|
669
|
+
input_tokens: inputTokens,
|
|
670
|
+
output_tokens: outputTokens,
|
|
671
|
+
total_tokens: inputTokens + outputTokens,
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
function anthropicContentToResponseOutput(content) {
|
|
675
|
+
const blocks = Array.isArray(content) ? content : [];
|
|
676
|
+
const output = [];
|
|
677
|
+
const text = blocks
|
|
678
|
+
.map((block) => {
|
|
679
|
+
const record = recordValue(block);
|
|
680
|
+
return stringValue(record?.type) === "text" ? textValue(record?.text) ?? "" : "";
|
|
681
|
+
})
|
|
682
|
+
.filter(Boolean)
|
|
683
|
+
.join("");
|
|
684
|
+
if (text) {
|
|
685
|
+
output.push({
|
|
686
|
+
id: `msg_${randomUUID().replace(/-/g, "")}`,
|
|
687
|
+
type: "message",
|
|
688
|
+
status: "completed",
|
|
689
|
+
role: "assistant",
|
|
690
|
+
content: [{ type: "output_text", text, annotations: [] }],
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
for (const block of blocks) {
|
|
694
|
+
const record = recordValue(block);
|
|
695
|
+
if (stringValue(record?.type) !== "tool_use") {
|
|
696
|
+
continue;
|
|
697
|
+
}
|
|
698
|
+
const name = stringValue(record?.name);
|
|
699
|
+
if (!name) {
|
|
700
|
+
continue;
|
|
701
|
+
}
|
|
702
|
+
const responseName = responsesFunctionCallNameFromChatName(name);
|
|
703
|
+
output.push({
|
|
704
|
+
type: "function_call",
|
|
705
|
+
id: stringValue(record?.id) ?? `fc_${randomUUID().replace(/-/g, "")}`,
|
|
706
|
+
call_id: stringValue(record?.id) ?? `call_${randomUUID().replace(/-/g, "")}`,
|
|
707
|
+
...responseName,
|
|
708
|
+
arguments: JSON.stringify(record?.input ?? {}),
|
|
709
|
+
status: "completed",
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
return output;
|
|
713
|
+
}
|
|
714
|
+
async function forwardAnthropicNonStreaming(args) {
|
|
715
|
+
const anthropicRequest = buildAnthropicRequest(args.body, false, args.onLog);
|
|
716
|
+
const responseMetadata = responseMetadataFromBody(args.body);
|
|
717
|
+
const upstream = await fetch(`${normalizeBaseUrl(args.targetBaseUrl)}/messages`, {
|
|
718
|
+
method: "POST",
|
|
719
|
+
headers: {
|
|
720
|
+
...args.upstreamHeaders,
|
|
721
|
+
"content-type": "application/json",
|
|
722
|
+
},
|
|
723
|
+
signal: args.signal,
|
|
724
|
+
body: JSON.stringify(anthropicRequest),
|
|
725
|
+
});
|
|
726
|
+
const upstreamBody = await upstream.json().catch(async () => ({ error: { message: await upstream.text() } }));
|
|
727
|
+
if (!upstream.ok) {
|
|
728
|
+
args.res.writeHead(upstream.status, { "content-type": "application/json" });
|
|
729
|
+
args.res.end(JSON.stringify({
|
|
730
|
+
error: {
|
|
731
|
+
message: upstreamErrorMessage(upstreamBody),
|
|
732
|
+
type: "server_error",
|
|
733
|
+
status: upstream.status,
|
|
734
|
+
upstream: recordValue(upstreamBody.error) ?? upstreamBody,
|
|
735
|
+
},
|
|
736
|
+
}));
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
const responseId = `resp_${randomUUID().replace(/-/g, "")}`;
|
|
740
|
+
const usage = anthropicUsageToResponseUsage(upstreamBody.usage);
|
|
741
|
+
args.res.writeHead(200, { "content-type": "application/json" });
|
|
742
|
+
args.res.end(JSON.stringify(responseBase(responseId, stringValue(anthropicRequest.model) ?? "claude-sonnet-4-6", anthropicContentToResponseOutput(upstreamBody.content), responseMetadata, usage)));
|
|
743
|
+
}
|
|
475
744
|
async function forwardNonStreaming(args) {
|
|
476
745
|
const chatRequest = buildChatRequest(args.body, false, args.onLog);
|
|
477
746
|
const responseMetadata = responseMetadataFromBody(args.body);
|
|
@@ -799,6 +1068,304 @@ async function forwardStreaming(args) {
|
|
|
799
1068
|
writeSse(args.res, "response.completed", { type: "response.completed", response: responseBase(responseId, model, outputItems, responseMetadata, streamUsage) });
|
|
800
1069
|
args.res.end();
|
|
801
1070
|
}
|
|
1071
|
+
async function forwardAnthropicStreaming(args) {
|
|
1072
|
+
const anthropicRequest = buildAnthropicRequest(args.body, true, args.onLog);
|
|
1073
|
+
const model = stringValue(anthropicRequest.model) ?? "claude-sonnet-4-6";
|
|
1074
|
+
const responseMetadata = responseMetadataFromBody(args.body);
|
|
1075
|
+
const responseId = `resp_${randomUUID().replace(/-/g, "")}`;
|
|
1076
|
+
const outputItems = [];
|
|
1077
|
+
const blocks = new Map();
|
|
1078
|
+
let streamUsage = null;
|
|
1079
|
+
args.res.writeHead(200, {
|
|
1080
|
+
"content-type": "text/event-stream; charset=utf-8",
|
|
1081
|
+
"cache-control": "no-cache",
|
|
1082
|
+
connection: "keep-alive",
|
|
1083
|
+
});
|
|
1084
|
+
writeSse(args.res, "response.created", { type: "response.created", response: responseBase(responseId, model, [], responseMetadata) });
|
|
1085
|
+
writeSse(args.res, "response.in_progress", { type: "response.in_progress", response: responseBase(responseId, model, [], responseMetadata, null, "in_progress") });
|
|
1086
|
+
const upstream = await fetch(`${normalizeBaseUrl(args.targetBaseUrl)}/messages`, {
|
|
1087
|
+
method: "POST",
|
|
1088
|
+
headers: {
|
|
1089
|
+
...args.upstreamHeaders,
|
|
1090
|
+
"content-type": "application/json",
|
|
1091
|
+
},
|
|
1092
|
+
signal: args.signal,
|
|
1093
|
+
body: JSON.stringify(anthropicRequest),
|
|
1094
|
+
});
|
|
1095
|
+
if (!upstream.ok || !upstream.body) {
|
|
1096
|
+
const errorText = await upstream.text();
|
|
1097
|
+
writeFailedSse({
|
|
1098
|
+
errorMessage: `Anthropic Messages upstream failed: ${upstream.status} ${errorText}`,
|
|
1099
|
+
model,
|
|
1100
|
+
res: args.res,
|
|
1101
|
+
responseId,
|
|
1102
|
+
responseMetadata,
|
|
1103
|
+
});
|
|
1104
|
+
args.res.end();
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
const reader = upstream.body.getReader();
|
|
1108
|
+
const decoder = new TextDecoder();
|
|
1109
|
+
let buffer = "";
|
|
1110
|
+
let upstreamDone = false;
|
|
1111
|
+
let streamFailed = false;
|
|
1112
|
+
let currentEvent = "";
|
|
1113
|
+
const processAnthropicEvent = (event, data) => {
|
|
1114
|
+
if (!data) {
|
|
1115
|
+
return false;
|
|
1116
|
+
}
|
|
1117
|
+
let parsed;
|
|
1118
|
+
try {
|
|
1119
|
+
parsed = JSON.parse(data);
|
|
1120
|
+
}
|
|
1121
|
+
catch (error) {
|
|
1122
|
+
writeFailedSse({
|
|
1123
|
+
errorMessage: `Failed to parse Anthropic stream chunk: ${error instanceof Error ? error.message : String(error)}`,
|
|
1124
|
+
model,
|
|
1125
|
+
res: args.res,
|
|
1126
|
+
responseId,
|
|
1127
|
+
responseMetadata,
|
|
1128
|
+
});
|
|
1129
|
+
args.res.end();
|
|
1130
|
+
streamFailed = true;
|
|
1131
|
+
upstreamDone = true;
|
|
1132
|
+
return true;
|
|
1133
|
+
}
|
|
1134
|
+
const type = stringValue(parsed.type) ?? event;
|
|
1135
|
+
if (type === "error") {
|
|
1136
|
+
writeFailedSse({
|
|
1137
|
+
errorMessage: upstreamErrorMessage(parsed),
|
|
1138
|
+
model,
|
|
1139
|
+
res: args.res,
|
|
1140
|
+
responseId,
|
|
1141
|
+
responseMetadata,
|
|
1142
|
+
});
|
|
1143
|
+
args.res.end();
|
|
1144
|
+
streamFailed = true;
|
|
1145
|
+
upstreamDone = true;
|
|
1146
|
+
return true;
|
|
1147
|
+
}
|
|
1148
|
+
if (type === "message_start") {
|
|
1149
|
+
const usage = anthropicUsageToResponseUsage(recordValue(parsed.message)?.usage);
|
|
1150
|
+
if (usage) {
|
|
1151
|
+
streamUsage = usage;
|
|
1152
|
+
}
|
|
1153
|
+
return false;
|
|
1154
|
+
}
|
|
1155
|
+
if (type === "message_delta") {
|
|
1156
|
+
const usage = anthropicUsageToResponseUsage(parsed.usage);
|
|
1157
|
+
if (usage) {
|
|
1158
|
+
streamUsage = {
|
|
1159
|
+
input_tokens: streamUsage?.input_tokens ?? usage.input_tokens,
|
|
1160
|
+
output_tokens: usage.output_tokens,
|
|
1161
|
+
total_tokens: (streamUsage?.input_tokens ?? usage.input_tokens) + usage.output_tokens,
|
|
1162
|
+
};
|
|
1163
|
+
}
|
|
1164
|
+
return false;
|
|
1165
|
+
}
|
|
1166
|
+
if (type === "content_block_start") {
|
|
1167
|
+
const index = finiteNumber(parsed.index) ?? blocks.size;
|
|
1168
|
+
const block = recordValue(parsed.content_block);
|
|
1169
|
+
const blockType = stringValue(block?.type);
|
|
1170
|
+
if (blockType === "text") {
|
|
1171
|
+
const itemId = `msg_${randomUUID().replace(/-/g, "")}`;
|
|
1172
|
+
blocks.set(index, {
|
|
1173
|
+
id: itemId,
|
|
1174
|
+
outputIndex: index,
|
|
1175
|
+
type: "text",
|
|
1176
|
+
text: "",
|
|
1177
|
+
toolId: "",
|
|
1178
|
+
toolName: "",
|
|
1179
|
+
toolInputJson: "",
|
|
1180
|
+
started: true,
|
|
1181
|
+
});
|
|
1182
|
+
writeSse(args.res, "response.output_item.added", {
|
|
1183
|
+
type: "response.output_item.added",
|
|
1184
|
+
output_index: index,
|
|
1185
|
+
item: { id: itemId, type: "message", status: "in_progress", role: "assistant", content: [] },
|
|
1186
|
+
});
|
|
1187
|
+
writeSse(args.res, "response.content_part.added", {
|
|
1188
|
+
type: "response.content_part.added",
|
|
1189
|
+
item_id: itemId,
|
|
1190
|
+
output_index: index,
|
|
1191
|
+
content_index: 0,
|
|
1192
|
+
part: { type: "output_text", text: "", annotations: [] },
|
|
1193
|
+
});
|
|
1194
|
+
}
|
|
1195
|
+
else if (blockType === "tool_use") {
|
|
1196
|
+
const toolId = stringValue(block?.id) ?? `call_${randomUUID().replace(/-/g, "")}`;
|
|
1197
|
+
const toolName = stringValue(block?.name) ?? "";
|
|
1198
|
+
const responseName = responsesFunctionCallNameFromChatName(toolName);
|
|
1199
|
+
const itemId = `fc_${toolId.replace(/[^a-zA-Z0-9_-]/g, "")}`;
|
|
1200
|
+
blocks.set(index, {
|
|
1201
|
+
id: itemId,
|
|
1202
|
+
outputIndex: index,
|
|
1203
|
+
type: "tool_use",
|
|
1204
|
+
text: "",
|
|
1205
|
+
toolId,
|
|
1206
|
+
toolName,
|
|
1207
|
+
toolInputJson: "",
|
|
1208
|
+
started: true,
|
|
1209
|
+
});
|
|
1210
|
+
writeSse(args.res, "response.output_item.added", {
|
|
1211
|
+
type: "response.output_item.added",
|
|
1212
|
+
output_index: index,
|
|
1213
|
+
item: {
|
|
1214
|
+
id: itemId,
|
|
1215
|
+
type: "function_call",
|
|
1216
|
+
status: "in_progress",
|
|
1217
|
+
call_id: toolId,
|
|
1218
|
+
...responseName,
|
|
1219
|
+
arguments: "",
|
|
1220
|
+
},
|
|
1221
|
+
});
|
|
1222
|
+
}
|
|
1223
|
+
return false;
|
|
1224
|
+
}
|
|
1225
|
+
if (type === "content_block_delta") {
|
|
1226
|
+
const index = finiteNumber(parsed.index) ?? 0;
|
|
1227
|
+
const block = blocks.get(index);
|
|
1228
|
+
const delta = recordValue(parsed.delta);
|
|
1229
|
+
if (!block || !delta) {
|
|
1230
|
+
return false;
|
|
1231
|
+
}
|
|
1232
|
+
const deltaType = stringValue(delta.type);
|
|
1233
|
+
if (block.type === "text" && deltaType === "text_delta") {
|
|
1234
|
+
const chunk = textValue(delta.text) ?? "";
|
|
1235
|
+
if (!chunk) {
|
|
1236
|
+
return false;
|
|
1237
|
+
}
|
|
1238
|
+
block.text += chunk;
|
|
1239
|
+
writeSse(args.res, "response.output_text.delta", {
|
|
1240
|
+
type: "response.output_text.delta",
|
|
1241
|
+
item_id: block.id,
|
|
1242
|
+
output_index: block.outputIndex,
|
|
1243
|
+
content_index: 0,
|
|
1244
|
+
delta: chunk,
|
|
1245
|
+
});
|
|
1246
|
+
}
|
|
1247
|
+
else if (block.type === "tool_use" && deltaType === "input_json_delta") {
|
|
1248
|
+
const chunk = textValue(delta.partial_json) ?? "";
|
|
1249
|
+
if (!chunk) {
|
|
1250
|
+
return false;
|
|
1251
|
+
}
|
|
1252
|
+
block.toolInputJson += chunk;
|
|
1253
|
+
writeSse(args.res, "response.function_call_arguments.delta", {
|
|
1254
|
+
type: "response.function_call_arguments.delta",
|
|
1255
|
+
item_id: block.id,
|
|
1256
|
+
output_index: block.outputIndex,
|
|
1257
|
+
delta: chunk,
|
|
1258
|
+
});
|
|
1259
|
+
}
|
|
1260
|
+
blocks.set(index, block);
|
|
1261
|
+
return false;
|
|
1262
|
+
}
|
|
1263
|
+
if (type === "content_block_stop") {
|
|
1264
|
+
const index = finiteNumber(parsed.index) ?? 0;
|
|
1265
|
+
const block = blocks.get(index);
|
|
1266
|
+
if (!block) {
|
|
1267
|
+
return false;
|
|
1268
|
+
}
|
|
1269
|
+
if (block.type === "text") {
|
|
1270
|
+
writeSse(args.res, "response.output_text.done", {
|
|
1271
|
+
type: "response.output_text.done",
|
|
1272
|
+
item_id: block.id,
|
|
1273
|
+
output_index: block.outputIndex,
|
|
1274
|
+
content_index: 0,
|
|
1275
|
+
text: block.text,
|
|
1276
|
+
});
|
|
1277
|
+
writeSse(args.res, "response.content_part.done", {
|
|
1278
|
+
type: "response.content_part.done",
|
|
1279
|
+
item_id: block.id,
|
|
1280
|
+
output_index: block.outputIndex,
|
|
1281
|
+
content_index: 0,
|
|
1282
|
+
part: { type: "output_text", text: block.text, annotations: [] },
|
|
1283
|
+
});
|
|
1284
|
+
const item = {
|
|
1285
|
+
id: block.id,
|
|
1286
|
+
type: "message",
|
|
1287
|
+
status: "completed",
|
|
1288
|
+
role: "assistant",
|
|
1289
|
+
content: [{ type: "output_text", text: block.text, annotations: [] }],
|
|
1290
|
+
};
|
|
1291
|
+
outputItems.push(item);
|
|
1292
|
+
writeSse(args.res, "response.output_item.done", { type: "response.output_item.done", output_index: block.outputIndex, item });
|
|
1293
|
+
}
|
|
1294
|
+
else {
|
|
1295
|
+
const responseName = responsesFunctionCallNameFromChatName(block.toolName);
|
|
1296
|
+
const item = {
|
|
1297
|
+
id: block.id,
|
|
1298
|
+
type: "function_call",
|
|
1299
|
+
status: "completed",
|
|
1300
|
+
call_id: block.toolId,
|
|
1301
|
+
...responseName,
|
|
1302
|
+
arguments: block.toolInputJson || "{}",
|
|
1303
|
+
};
|
|
1304
|
+
outputItems.push(item);
|
|
1305
|
+
writeSse(args.res, "response.function_call_arguments.done", {
|
|
1306
|
+
type: "response.function_call_arguments.done",
|
|
1307
|
+
item_id: block.id,
|
|
1308
|
+
output_index: block.outputIndex,
|
|
1309
|
+
arguments: block.toolInputJson || "{}",
|
|
1310
|
+
});
|
|
1311
|
+
writeSse(args.res, "response.output_item.done", { type: "response.output_item.done", output_index: block.outputIndex, item });
|
|
1312
|
+
}
|
|
1313
|
+
return false;
|
|
1314
|
+
}
|
|
1315
|
+
if (type === "message_stop") {
|
|
1316
|
+
upstreamDone = true;
|
|
1317
|
+
return true;
|
|
1318
|
+
}
|
|
1319
|
+
return false;
|
|
1320
|
+
};
|
|
1321
|
+
try {
|
|
1322
|
+
while (!upstreamDone) {
|
|
1323
|
+
const { done, value } = await reader.read();
|
|
1324
|
+
if (done)
|
|
1325
|
+
break;
|
|
1326
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1327
|
+
const lines = buffer.split(/\r?\n/);
|
|
1328
|
+
buffer = lines.pop() ?? "";
|
|
1329
|
+
for (const line of lines) {
|
|
1330
|
+
const trimmed = line.trim();
|
|
1331
|
+
if (!trimmed) {
|
|
1332
|
+
continue;
|
|
1333
|
+
}
|
|
1334
|
+
if (trimmed.startsWith("event:")) {
|
|
1335
|
+
currentEvent = trimmed.slice(6).trim();
|
|
1336
|
+
continue;
|
|
1337
|
+
}
|
|
1338
|
+
if (!trimmed.startsWith("data:")) {
|
|
1339
|
+
continue;
|
|
1340
|
+
}
|
|
1341
|
+
const data = trimmed.slice(5).trim();
|
|
1342
|
+
if (processAnthropicEvent(currentEvent, data)) {
|
|
1343
|
+
await reader.cancel().catch(() => undefined);
|
|
1344
|
+
break;
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
catch (error) {
|
|
1350
|
+
if (args.signal.aborted) {
|
|
1351
|
+
return;
|
|
1352
|
+
}
|
|
1353
|
+
writeFailedSse({
|
|
1354
|
+
errorMessage: `Anthropic Messages stream failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
1355
|
+
model,
|
|
1356
|
+
res: args.res,
|
|
1357
|
+
responseId,
|
|
1358
|
+
responseMetadata,
|
|
1359
|
+
});
|
|
1360
|
+
args.res.end();
|
|
1361
|
+
return;
|
|
1362
|
+
}
|
|
1363
|
+
if (streamFailed) {
|
|
1364
|
+
return;
|
|
1365
|
+
}
|
|
1366
|
+
writeSse(args.res, "response.completed", { type: "response.completed", response: responseBase(responseId, model, outputItems, responseMetadata, streamUsage) });
|
|
1367
|
+
args.res.end();
|
|
1368
|
+
}
|
|
802
1369
|
function requestAbortSignal(req, res) {
|
|
803
1370
|
const abortController = new AbortController();
|
|
804
1371
|
const abort = () => {
|
|
@@ -813,7 +1380,7 @@ function requestAbortSignal(req, res) {
|
|
|
813
1380
|
async function startResponsesBridge(args) {
|
|
814
1381
|
const targetBaseUrl = normalizeBaseUrl(args.targetBaseUrl);
|
|
815
1382
|
const expectedAuthorization = `Bearer ${args.localApiKey}`;
|
|
816
|
-
const
|
|
1383
|
+
const upstreamHeaders = args.adapter.upstreamAuthHeaders(args.upstreamApiKey);
|
|
817
1384
|
const server = createServer(async (req, res) => {
|
|
818
1385
|
try {
|
|
819
1386
|
if (req.method !== "POST" || !req.url?.replace(/\?.*$/, "").endsWith("/responses")) {
|
|
@@ -829,11 +1396,33 @@ async function startResponsesBridge(args) {
|
|
|
829
1396
|
}
|
|
830
1397
|
const body = await readRequestJson(req);
|
|
831
1398
|
const signal = requestAbortSignal(req, res);
|
|
832
|
-
if (
|
|
833
|
-
|
|
1399
|
+
if (args.adapter.kind === "anthropic_messages") {
|
|
1400
|
+
if (body.stream === true) {
|
|
1401
|
+
await forwardAnthropicStreaming({ body, res, signal, targetBaseUrl, upstreamHeaders, onLog: args.onLog });
|
|
1402
|
+
}
|
|
1403
|
+
else {
|
|
1404
|
+
await forwardAnthropicNonStreaming({ body, res, signal, targetBaseUrl, upstreamHeaders, onLog: args.onLog });
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
else if (body.stream === true) {
|
|
1408
|
+
await forwardStreaming({
|
|
1409
|
+
body,
|
|
1410
|
+
res,
|
|
1411
|
+
signal,
|
|
1412
|
+
targetBaseUrl,
|
|
1413
|
+
upstreamAuthorization: upstreamHeaders.authorization ?? "",
|
|
1414
|
+
onLog: args.onLog,
|
|
1415
|
+
});
|
|
834
1416
|
}
|
|
835
1417
|
else {
|
|
836
|
-
await forwardNonStreaming({
|
|
1418
|
+
await forwardNonStreaming({
|
|
1419
|
+
body,
|
|
1420
|
+
res,
|
|
1421
|
+
signal,
|
|
1422
|
+
targetBaseUrl,
|
|
1423
|
+
upstreamAuthorization: upstreamHeaders.authorization ?? "",
|
|
1424
|
+
onLog: args.onLog,
|
|
1425
|
+
});
|
|
837
1426
|
}
|
|
838
1427
|
}
|
|
839
1428
|
catch (error) {
|
|
@@ -870,14 +1459,16 @@ export async function startCodexChatBridge(args) {
|
|
|
870
1459
|
throw new Error(`${providerName} API key is required to start the Codex chat bridge`);
|
|
871
1460
|
}
|
|
872
1461
|
const targetBaseUrl = resolveTargetBaseUrl({ providerId: args.providerId, targetBaseUrl: args.targetBaseUrl });
|
|
1462
|
+
const adapter = resolveProviderAdapter(args.providerId);
|
|
873
1463
|
const apiKey = `sk-doer-chat-bridge-${randomUUID().replace(/-/g, "")}`;
|
|
874
1464
|
const bridge = await startResponsesBridge({
|
|
1465
|
+
adapter,
|
|
875
1466
|
localApiKey: apiKey,
|
|
876
1467
|
targetBaseUrl,
|
|
877
1468
|
upstreamApiKey: providerApiKey,
|
|
878
1469
|
onLog: args.onLog,
|
|
879
1470
|
});
|
|
880
|
-
args.onLog?.(`Codex
|
|
1471
|
+
args.onLog?.(`Codex provider gateway listening baseUrl=${bridge.baseUrl} target=${targetBaseUrl} provider=${providerName} adapter=${adapter.kind}`);
|
|
881
1472
|
return {
|
|
882
1473
|
baseUrl: bridge.baseUrl,
|
|
883
1474
|
envKey: CHAT_BRIDGE_ENV_KEY,
|
|
@@ -2,7 +2,7 @@ import assert from "node:assert/strict";
|
|
|
2
2
|
import { createServer } from "node:http";
|
|
3
3
|
import { test } from "node:test";
|
|
4
4
|
import { startCodexChatBridge } from "./codex-chat-bridge.js";
|
|
5
|
-
async function startBridgeFixture(handler) {
|
|
5
|
+
async function startBridgeFixture(handler, options) {
|
|
6
6
|
const capturedBodies = [];
|
|
7
7
|
const logs = [];
|
|
8
8
|
const upstream = createServer(async (req, res) => {
|
|
@@ -20,7 +20,7 @@ async function startBridgeFixture(handler) {
|
|
|
20
20
|
assert(address && typeof address !== "string");
|
|
21
21
|
const bridge = await startCodexChatBridge({
|
|
22
22
|
providerApiKey: "upstream-test-key",
|
|
23
|
-
providerId: "openai-compatible",
|
|
23
|
+
providerId: options?.providerId ?? "openai-compatible",
|
|
24
24
|
providerName: "test upstream",
|
|
25
25
|
targetBaseUrl: `http://127.0.0.1:${address.port}`,
|
|
26
26
|
onLog: (message) => logs.push(message),
|
|
@@ -344,6 +344,142 @@ test("returns function_call output for non-streaming Chat tool calls", async ()
|
|
|
344
344
|
await fixture.close();
|
|
345
345
|
}
|
|
346
346
|
});
|
|
347
|
+
test("maps Responses create request fields to Anthropic Messages", async () => {
|
|
348
|
+
const fixture = await startBridgeFixture(({ req, res }) => {
|
|
349
|
+
assert.equal(req.url, "/messages");
|
|
350
|
+
assert.equal(req.headers["x-api-key"], "upstream-test-key");
|
|
351
|
+
assert.equal(req.headers["anthropic-version"], "2023-06-01");
|
|
352
|
+
writeChatJson(res, {
|
|
353
|
+
content: [
|
|
354
|
+
{ type: "text", text: "claude mapped" },
|
|
355
|
+
{ type: "tool_use", id: "toolu_123", name: "mcp__doer_daemon__daemon_list", input: { includeStopped: true } },
|
|
356
|
+
],
|
|
357
|
+
usage: { input_tokens: 7, output_tokens: 3 },
|
|
358
|
+
});
|
|
359
|
+
}, { providerId: "anthropic" });
|
|
360
|
+
try {
|
|
361
|
+
const response = await responsesRequest({
|
|
362
|
+
bridge: fixture.bridge,
|
|
363
|
+
body: {
|
|
364
|
+
model: "claude-sonnet-4-6",
|
|
365
|
+
instructions: "system instruction",
|
|
366
|
+
input: [
|
|
367
|
+
{ role: "developer", content: [{ type: "input_text", text: "developer note" }] },
|
|
368
|
+
{
|
|
369
|
+
role: "user",
|
|
370
|
+
content: [
|
|
371
|
+
{ type: "input_text", text: "look at this" },
|
|
372
|
+
{ type: "input_image", image_url: "data:image/png;base64,AA==", detail: "high" },
|
|
373
|
+
],
|
|
374
|
+
},
|
|
375
|
+
{ type: "function_call", call_id: "call_123", name: "lookup", arguments: "{\"q\":\"doer\"}" },
|
|
376
|
+
{ type: "function_call_output", call_id: "call_123", output: "tool result" },
|
|
377
|
+
],
|
|
378
|
+
tools: [{
|
|
379
|
+
type: "namespace",
|
|
380
|
+
name: "doer_daemon",
|
|
381
|
+
tools: [{ name: "daemon_list", description: "List daemons", input_schema: { type: "object" } }],
|
|
382
|
+
}],
|
|
383
|
+
tool_choice: "required",
|
|
384
|
+
max_output_tokens: 64,
|
|
385
|
+
},
|
|
386
|
+
});
|
|
387
|
+
assert.equal(response.status, 200);
|
|
388
|
+
const anthropic = fixture.capturedBodies[0];
|
|
389
|
+
assert.equal(anthropic.model, "claude-sonnet-4-6");
|
|
390
|
+
assert.equal(anthropic.stream, false);
|
|
391
|
+
assert.equal(anthropic.max_tokens, 64);
|
|
392
|
+
assert.equal(anthropic.system, "system instruction\n\ndeveloper note");
|
|
393
|
+
assert.deepEqual(anthropic.tool_choice, { type: "any" });
|
|
394
|
+
assert.deepEqual(anthropic.tools, [{
|
|
395
|
+
name: "mcp__doer_daemon__daemon_list",
|
|
396
|
+
description: "Namespace: mcp__doer_daemon__. Original tool: daemon_list. List daemons",
|
|
397
|
+
input_schema: { type: "object" },
|
|
398
|
+
}]);
|
|
399
|
+
assert.deepEqual(anthropic.messages, [
|
|
400
|
+
{
|
|
401
|
+
role: "user",
|
|
402
|
+
content: [
|
|
403
|
+
{ type: "text", text: "look at this" },
|
|
404
|
+
{ type: "image", source: { type: "base64", media_type: "image/png", data: "AA==" } },
|
|
405
|
+
],
|
|
406
|
+
},
|
|
407
|
+
{
|
|
408
|
+
role: "assistant",
|
|
409
|
+
content: [{ type: "tool_use", id: "call_123", name: "lookup", input: { q: "doer" } }],
|
|
410
|
+
},
|
|
411
|
+
{
|
|
412
|
+
role: "user",
|
|
413
|
+
content: [{ type: "tool_result", tool_use_id: "call_123", content: "tool result" }],
|
|
414
|
+
},
|
|
415
|
+
]);
|
|
416
|
+
const body = await response.json();
|
|
417
|
+
assert.deepEqual(body.usage, { input_tokens: 7, output_tokens: 3, total_tokens: 10 });
|
|
418
|
+
const output = body.output;
|
|
419
|
+
assert.equal(output[0]?.type, "message");
|
|
420
|
+
assert.deepEqual(output[1], {
|
|
421
|
+
type: "function_call",
|
|
422
|
+
id: "toolu_123",
|
|
423
|
+
call_id: "toolu_123",
|
|
424
|
+
namespace: "mcp__doer_daemon",
|
|
425
|
+
name: "daemon_list",
|
|
426
|
+
arguments: "{\"includeStopped\":true}",
|
|
427
|
+
status: "completed",
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
finally {
|
|
431
|
+
await fixture.close();
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
test("streams Anthropic Messages text and tool_use as Responses events", async () => {
|
|
435
|
+
const fixture = await startBridgeFixture(({ res }) => {
|
|
436
|
+
writeChatSse(res, [
|
|
437
|
+
{ type: "message_start", message: { usage: { input_tokens: 5, output_tokens: 0 } } },
|
|
438
|
+
{ type: "content_block_start", index: 0, content_block: { type: "text", text: "" } },
|
|
439
|
+
{ type: "content_block_delta", index: 0, delta: { type: "text_delta", text: "hello" } },
|
|
440
|
+
{ type: "content_block_stop", index: 0 },
|
|
441
|
+
{ type: "content_block_start", index: 1, content_block: { type: "tool_use", id: "toolu_abc", name: "lookup", input: {} } },
|
|
442
|
+
{ type: "content_block_delta", index: 1, delta: { type: "input_json_delta", partial_json: "{\"q\"" } },
|
|
443
|
+
{ type: "content_block_delta", index: 1, delta: { type: "input_json_delta", partial_json: ":\"doer\"}" } },
|
|
444
|
+
{ type: "content_block_stop", index: 1 },
|
|
445
|
+
{ type: "message_delta", usage: { output_tokens: 4 } },
|
|
446
|
+
{ type: "message_stop" },
|
|
447
|
+
]);
|
|
448
|
+
}, { providerId: "anthropic" });
|
|
449
|
+
try {
|
|
450
|
+
const response = await responsesRequest({
|
|
451
|
+
bridge: fixture.bridge,
|
|
452
|
+
body: { model: "claude-sonnet-4-6", stream: true, input: "hello" },
|
|
453
|
+
});
|
|
454
|
+
const events = parseSse(await response.text());
|
|
455
|
+
assert.equal(response.status, 200);
|
|
456
|
+
assert.equal(fixture.capturedBodies[0]?.stream, true);
|
|
457
|
+
assert.equal(events.find((event) => event.event === "response.output_text.delta")?.data.delta, "hello");
|
|
458
|
+
assert.deepEqual(events.filter((event) => event.event === "response.function_call_arguments.delta").map((event) => event.data.delta), ["{\"q\"", ":\"doer\"}"]);
|
|
459
|
+
const completed = events.at(-1)?.data.response;
|
|
460
|
+
assert.deepEqual(completed.usage, { input_tokens: 5, output_tokens: 4, total_tokens: 9 });
|
|
461
|
+
assert.deepEqual(completed.output, [
|
|
462
|
+
{
|
|
463
|
+
id: completed.output[0]?.id,
|
|
464
|
+
type: "message",
|
|
465
|
+
status: "completed",
|
|
466
|
+
role: "assistant",
|
|
467
|
+
content: [{ type: "output_text", text: "hello", annotations: [] }],
|
|
468
|
+
},
|
|
469
|
+
{
|
|
470
|
+
id: "fc_toolu_abc",
|
|
471
|
+
type: "function_call",
|
|
472
|
+
status: "completed",
|
|
473
|
+
call_id: "toolu_abc",
|
|
474
|
+
name: "lookup",
|
|
475
|
+
arguments: "{\"q\":\"doer\"}",
|
|
476
|
+
},
|
|
477
|
+
]);
|
|
478
|
+
}
|
|
479
|
+
finally {
|
|
480
|
+
await fixture.close();
|
|
481
|
+
}
|
|
482
|
+
});
|
|
347
483
|
test("normalizes upstream errors for non-streaming requests", async () => {
|
|
348
484
|
const fixture = await startBridgeFixture(({ res }) => {
|
|
349
485
|
writeChatJson(res, { error: { message: "bad upstream", code: "bad_request" } }, 400);
|