doer-agent 0.8.7 → 0.8.9

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.
@@ -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
- return settings.codex.modelProvider === "zai" ? 258400 : null;
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
- if (args.settings.codex.modelProvider !== "zai" || provider?.id !== "zai") {
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
- return normalizeBaseUrl(baseUrl || DEFAULT_CHAT_BASE_URL);
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 upstreamAuthorization = `Bearer ${args.upstreamApiKey}`;
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 (body.stream === true) {
833
- await forwardStreaming({ body, res, signal, targetBaseUrl, upstreamAuthorization, onLog: args.onLog });
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({ body, res, signal, targetBaseUrl, upstreamAuthorization, onLog: args.onLog });
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 chat bridge listening baseUrl=${bridge.baseUrl} target=${targetBaseUrl} provider=${providerName}`);
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "doer-agent",
3
- "version": "0.8.7",
3
+ "version": "0.8.9",
4
4
  "description": "Reverse-polling agent runtime for doer",
5
5
  "type": "module",
6
6
  "main": "dist/agent.js",
@@ -26,8 +26,8 @@
26
26
  },
27
27
  "dependencies": {
28
28
  "@modelcontextprotocol/sdk": "^1.29.0",
29
- "@openai/codex": "^0.141.0",
30
- "@openai/codex-sdk": "^0.141.0",
29
+ "@openai/codex": "^0.144.1",
30
+ "@openai/codex-sdk": "^0.144.1",
31
31
  "diff": "^9.0.0",
32
32
  "nats": "^2.29.3",
33
33
  "tar": "^7.5.15"