doer-agent 0.8.6 → 0.8.7

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.
@@ -0,0 +1,898 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { createServer } from "node:http";
3
+ const DEFAULT_CHAT_BASE_URL = "https://api.z.ai/api/coding/paas/v4";
4
+ const CHAT_BRIDGE_ENV_KEY = "DOER_CODEX_CHAT_BRIDGE_API_KEY";
5
+ function normalizeBaseUrl(value) {
6
+ return value.replace(/\/+$/, "");
7
+ }
8
+ function resolveTargetBaseUrl(args) {
9
+ const providerId = args.providerId?.trim().toLowerCase();
10
+ const baseUrl = args.targetBaseUrl?.trim();
11
+ if (providerId === "zai" && (!baseUrl || !baseUrl.includes("/coding/paas/"))) {
12
+ return DEFAULT_CHAT_BASE_URL;
13
+ }
14
+ return normalizeBaseUrl(baseUrl || DEFAULT_CHAT_BASE_URL);
15
+ }
16
+ function stringValue(value) {
17
+ return typeof value === "string" && value.trim() ? value.trim() : null;
18
+ }
19
+ function textValue(value) {
20
+ return typeof value === "string" && value.length > 0 ? value : null;
21
+ }
22
+ function finiteNumber(value) {
23
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
24
+ }
25
+ function recordValue(value) {
26
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
27
+ }
28
+ function readRequestJson(req) {
29
+ return new Promise((resolve, reject) => {
30
+ const chunks = [];
31
+ req.on("data", (chunk) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
32
+ req.on("end", () => {
33
+ const raw = Buffer.concat(chunks).toString("utf8").trim();
34
+ if (!raw) {
35
+ resolve({});
36
+ return;
37
+ }
38
+ try {
39
+ const parsed = JSON.parse(raw);
40
+ resolve(parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {});
41
+ }
42
+ catch (error) {
43
+ reject(error);
44
+ }
45
+ });
46
+ req.on("error", reject);
47
+ });
48
+ }
49
+ function contentToText(value) {
50
+ if (typeof value === "string") {
51
+ return value;
52
+ }
53
+ if (!Array.isArray(value)) {
54
+ return "";
55
+ }
56
+ return value
57
+ .map((part) => {
58
+ if (typeof part === "string") {
59
+ return part;
60
+ }
61
+ if (!part || typeof part !== "object" || Array.isArray(part)) {
62
+ return "";
63
+ }
64
+ const record = part;
65
+ return textValue(record.text) ?? textValue(record.output_text) ?? "";
66
+ })
67
+ .filter(Boolean)
68
+ .join("\n");
69
+ }
70
+ function responsePartToChatPart(part) {
71
+ if (typeof part === "string") {
72
+ return part ? { type: "text", text: part } : null;
73
+ }
74
+ const record = recordValue(part);
75
+ if (!record) {
76
+ return null;
77
+ }
78
+ const type = stringValue(record.type);
79
+ if (type === "input_text" || type === "output_text" || type === "text") {
80
+ const text = textValue(record.text) ?? textValue(record.output_text);
81
+ return text ? { type: "text", text } : null;
82
+ }
83
+ if (type === "input_image" || type === "image_url") {
84
+ const rawImageUrl = recordValue(record.image_url);
85
+ const imageUrl = textValue(record.image_url) ?? textValue(rawImageUrl?.url) ?? textValue(record.url);
86
+ if (!imageUrl) {
87
+ return null;
88
+ }
89
+ const detail = stringValue(record.detail) ?? stringValue(rawImageUrl?.detail);
90
+ return {
91
+ type: "image_url",
92
+ image_url: {
93
+ url: imageUrl,
94
+ ...(detail ? { detail } : {}),
95
+ },
96
+ };
97
+ }
98
+ if (type === "input_file" || type === "file") {
99
+ const filename = stringValue(record.filename) ?? stringValue(record.name) ?? "attached file";
100
+ const fileId = stringValue(record.file_id);
101
+ const fileData = textValue(record.file_data);
102
+ const suffix = fileId ? ` file_id=${fileId}` : "";
103
+ return {
104
+ type: "text",
105
+ text: fileData ? `[attached file: ${filename}]\n${fileData}` : `[attached file: ${filename}${suffix}]`,
106
+ };
107
+ }
108
+ return null;
109
+ }
110
+ function contentToChatContent(value) {
111
+ if (typeof value === "string") {
112
+ return value;
113
+ }
114
+ if (!Array.isArray(value)) {
115
+ return "";
116
+ }
117
+ const parts = value.map(responsePartToChatPart).filter((part) => Boolean(part));
118
+ if (parts.length === 0) {
119
+ return "";
120
+ }
121
+ if (parts.every((part) => part.type === "text")) {
122
+ return parts.map((part) => textValue(part.text) ?? "").filter(Boolean).join("\n");
123
+ }
124
+ return parts;
125
+ }
126
+ function inputItemToMessages(item) {
127
+ if (!item || typeof item !== "object" || Array.isArray(item)) {
128
+ return [];
129
+ }
130
+ const record = item;
131
+ const type = stringValue(record.type);
132
+ const role = stringValue(record.role);
133
+ if (role === "user" || role === "assistant" || role === "system" || role === "developer") {
134
+ return [{
135
+ role: role === "developer" ? "system" : role,
136
+ content: contentToChatContent(record.content),
137
+ }];
138
+ }
139
+ if (type === "message") {
140
+ const messageRole = stringValue(record.role) ?? "user";
141
+ return [{
142
+ role: messageRole === "developer" ? "system" : messageRole,
143
+ content: contentToChatContent(record.content),
144
+ }];
145
+ }
146
+ if (type === "function_call") {
147
+ const name = stringValue(record.name);
148
+ const callId = stringValue(record.call_id) ?? stringValue(record.id) ?? `call_${randomUUID().replace(/-/g, "")}`;
149
+ if (!name) {
150
+ return [];
151
+ }
152
+ const namespace = stringValue(record.namespace);
153
+ return [{
154
+ role: "assistant",
155
+ content: null,
156
+ tool_calls: [{
157
+ id: callId,
158
+ type: "function",
159
+ function: {
160
+ name: chatNameFromResponsesFunctionCall(namespace, name),
161
+ arguments: typeof record.arguments === "string" ? record.arguments : "",
162
+ },
163
+ }],
164
+ }];
165
+ }
166
+ if (type === "function_call_output") {
167
+ const callId = stringValue(record.call_id);
168
+ if (!callId) {
169
+ return [];
170
+ }
171
+ return [{
172
+ role: "tool",
173
+ tool_call_id: callId,
174
+ content: typeof record.output === "string" ? record.output : JSON.stringify(record.output ?? ""),
175
+ }];
176
+ }
177
+ return [];
178
+ }
179
+ function inputToMessages(input) {
180
+ if (typeof input === "string") {
181
+ return [{ role: "user", content: input }];
182
+ }
183
+ if (!Array.isArray(input)) {
184
+ return [];
185
+ }
186
+ return input.flatMap(inputItemToMessages);
187
+ }
188
+ function schemaValue(value) {
189
+ return value && typeof value === "object" && !Array.isArray(value)
190
+ ? value
191
+ : { type: "object", properties: {} };
192
+ }
193
+ function chatFunctionTool(args) {
194
+ if (!/^[a-zA-Z0-9_-]+$/.test(args.name)) {
195
+ return null;
196
+ }
197
+ return {
198
+ type: "function",
199
+ function: {
200
+ name: args.name,
201
+ description: args.description,
202
+ parameters: schemaValue(args.parameters),
203
+ },
204
+ };
205
+ }
206
+ function namespaceToolChatName(namespace, name) {
207
+ if (!namespace) {
208
+ return name;
209
+ }
210
+ const prefix = namespace.startsWith("mcp__") ? namespace : `mcp__${namespace}`;
211
+ return `${prefix.endsWith("__") ? prefix : `${prefix}__`}${name}`;
212
+ }
213
+ function chatNameFromResponsesFunctionCall(namespace, name) {
214
+ if (!namespace) {
215
+ return name;
216
+ }
217
+ return `${namespace.endsWith("__") ? namespace : `${namespace}__`}${name}`;
218
+ }
219
+ function responsesFunctionCallNameFromChatName(name) {
220
+ if (!name.startsWith("mcp__")) {
221
+ return { name };
222
+ }
223
+ const separatorIndex = name.lastIndexOf("__");
224
+ if (separatorIndex <= "mcp__".length || separatorIndex >= name.length - 2) {
225
+ return { name };
226
+ }
227
+ return {
228
+ namespace: name.slice(0, separatorIndex),
229
+ name: name.slice(separatorIndex + 2),
230
+ };
231
+ }
232
+ function responsesToolToChatTools(tool, onLog) {
233
+ if (!tool || typeof tool !== "object" || Array.isArray(tool)) {
234
+ return [];
235
+ }
236
+ const record = tool;
237
+ const type = stringValue(record.type);
238
+ if (type === "namespace") {
239
+ const namespace = stringValue(record.name);
240
+ const namespaceDescription = stringValue(record.description);
241
+ const namespaceTools = Array.isArray(record.tools)
242
+ ? record.tools
243
+ : Array.isArray(record.functions)
244
+ ? record.functions
245
+ : [];
246
+ const tools = namespaceTools
247
+ .map((namespaceTool) => {
248
+ const toolRecord = recordValue(namespaceTool);
249
+ const name = stringValue(toolRecord?.name);
250
+ if (!name) {
251
+ return null;
252
+ }
253
+ const description = [
254
+ namespace ? `Namespace: ${namespaceToolChatName(namespace, "").replace(/__$/, "__")}. Original tool: ${name}.` : "",
255
+ stringValue(toolRecord?.description) ?? namespaceDescription ?? "",
256
+ ].filter(Boolean).join(" ");
257
+ return chatFunctionTool({
258
+ name: namespaceToolChatName(namespace, name),
259
+ description,
260
+ parameters: toolRecord?.parameters ?? toolRecord?.input_schema ?? toolRecord?.schema,
261
+ });
262
+ })
263
+ .filter((item) => Boolean(item));
264
+ if (tools.length === 0) {
265
+ onLog?.(`Codex chat bridge dropped unsupported Responses tool type=namespace`);
266
+ }
267
+ return tools;
268
+ }
269
+ if (type !== "function") {
270
+ if (type) {
271
+ onLog?.(`Codex chat bridge dropped unsupported Responses tool type=${type}`);
272
+ }
273
+ return [];
274
+ }
275
+ const name = stringValue(record.name);
276
+ if (!name) {
277
+ return [];
278
+ }
279
+ const chatTool = chatFunctionTool({
280
+ name,
281
+ description: stringValue(record.description) ?? "",
282
+ parameters: record.parameters,
283
+ });
284
+ return chatTool ? [chatTool] : [];
285
+ }
286
+ function chatToolChoiceFromResponses(value) {
287
+ const text = stringValue(value);
288
+ if (text === "auto" || text === "none" || text === "required") {
289
+ return text;
290
+ }
291
+ const record = recordValue(value);
292
+ if (!record) {
293
+ return undefined;
294
+ }
295
+ const type = stringValue(record.type);
296
+ const name = stringValue(record.name);
297
+ if (type === "function" && name) {
298
+ return { type: "function", function: { name } };
299
+ }
300
+ return undefined;
301
+ }
302
+ function chatResponseFormatFromResponsesText(value) {
303
+ const text = recordValue(value);
304
+ const format = recordValue(text?.format);
305
+ const type = stringValue(format?.type);
306
+ if (type === "json_object") {
307
+ return { type: "json_object" };
308
+ }
309
+ if (type !== "json_schema") {
310
+ return undefined;
311
+ }
312
+ const schema = recordValue(format?.schema) ?? recordValue(format?.json_schema);
313
+ if (!schema) {
314
+ return undefined;
315
+ }
316
+ const name = stringValue(format?.name) ?? "response";
317
+ return {
318
+ type: "json_schema",
319
+ json_schema: {
320
+ name,
321
+ schema,
322
+ ...(typeof format?.strict === "boolean" ? { strict: format.strict } : {}),
323
+ },
324
+ };
325
+ }
326
+ function responseMetadataFromBody(body) {
327
+ const tools = Array.isArray(body.tools) ? body.tools : [];
328
+ return {
329
+ instructions: stringValue(body.instructions),
330
+ maxOutputTokens: finiteNumber(body.max_output_tokens),
331
+ metadata: recordValue(body.metadata) ?? {},
332
+ parallelToolCalls: typeof body.parallel_tool_calls === "boolean" ? body.parallel_tool_calls : null,
333
+ previousResponseId: stringValue(body.previous_response_id),
334
+ temperature: finiteNumber(body.temperature),
335
+ text: recordValue(body.text) ?? { format: { type: "text" } },
336
+ toolChoice: body.tool_choice ?? "auto",
337
+ tools,
338
+ topP: finiteNumber(body.top_p),
339
+ truncation: stringValue(body.truncation),
340
+ user: stringValue(body.user),
341
+ };
342
+ }
343
+ function buildChatRequest(body, stream, onLog) {
344
+ const messages = inputToMessages(body.input);
345
+ const instructions = stringValue(body.instructions);
346
+ if (instructions) {
347
+ messages.unshift({ role: "system", content: instructions });
348
+ }
349
+ const tools = Array.isArray(body.tools) ? body.tools.flatMap((tool) => responsesToolToChatTools(tool, onLog)) : [];
350
+ const toolChoice = chatToolChoiceFromResponses(body.tool_choice);
351
+ const responseFormat = chatResponseFormatFromResponsesText(body.text);
352
+ return {
353
+ model: stringValue(body.model) ?? "glm-5.2",
354
+ messages,
355
+ stream,
356
+ ...(stream ? { stream_options: { include_usage: true } } : {}),
357
+ ...(typeof body.temperature === "number" ? { temperature: body.temperature } : {}),
358
+ ...(typeof body.top_p === "number" ? { top_p: body.top_p } : {}),
359
+ ...(typeof body.max_output_tokens === "number" ? { max_tokens: body.max_output_tokens } : {}),
360
+ ...(typeof body.presence_penalty === "number" ? { presence_penalty: body.presence_penalty } : {}),
361
+ ...(typeof body.frequency_penalty === "number" ? { frequency_penalty: body.frequency_penalty } : {}),
362
+ ...(typeof body.seed === "number" ? { seed: body.seed } : {}),
363
+ ...(typeof body.stop === "string" || Array.isArray(body.stop) ? { stop: body.stop } : {}),
364
+ ...(typeof body.parallel_tool_calls === "boolean" ? { parallel_tool_calls: body.parallel_tool_calls } : {}),
365
+ ...(typeof body.user === "string" ? { user: body.user } : {}),
366
+ ...(responseFormat ? { response_format: responseFormat } : {}),
367
+ ...(toolChoice ? { tool_choice: toolChoice } : {}),
368
+ ...(tools.length > 0 ? { tools } : {}),
369
+ };
370
+ }
371
+ function upstreamErrorMessage(value) {
372
+ const body = recordValue(value);
373
+ const error = recordValue(body?.error);
374
+ return stringValue(error?.message)
375
+ ?? stringValue(body?.message)
376
+ ?? (typeof value === "string" && value ? value : "Chat Completions upstream failed");
377
+ }
378
+ function streamDeltaText(delta) {
379
+ const content = delta.content;
380
+ if (typeof content === "string") {
381
+ return content;
382
+ }
383
+ if (Array.isArray(content)) {
384
+ return contentToText(content);
385
+ }
386
+ return "";
387
+ }
388
+ function writeSse(res, event, data) {
389
+ res.write(`event: ${event}\n`);
390
+ res.write(`data: ${JSON.stringify(data)}\n\n`);
391
+ }
392
+ function chatUsageToResponseUsage(value) {
393
+ const usage = recordValue(value);
394
+ if (!usage) {
395
+ return null;
396
+ }
397
+ const promptTokens = finiteNumber(usage.prompt_tokens) ?? 0;
398
+ const completionTokens = finiteNumber(usage.completion_tokens) ?? 0;
399
+ const totalTokens = finiteNumber(usage.total_tokens) ?? promptTokens + completionTokens;
400
+ const promptDetails = recordValue(usage.prompt_tokens_details);
401
+ const completionDetails = recordValue(usage.completion_tokens_details);
402
+ const cachedTokens = finiteNumber(promptDetails?.cached_tokens);
403
+ const reasoningTokens = finiteNumber(completionDetails?.reasoning_tokens);
404
+ return {
405
+ input_tokens: promptTokens,
406
+ ...(cachedTokens !== null ? { input_tokens_details: { cached_tokens: cachedTokens } } : {}),
407
+ output_tokens: completionTokens,
408
+ ...(reasoningTokens !== null ? { output_tokens_details: { reasoning_tokens: reasoningTokens } } : {}),
409
+ total_tokens: totalTokens,
410
+ };
411
+ }
412
+ function responseBase(responseId, model, output = [], metadata = responseMetadataFromBody({}), usage = null, status = "completed", error = null) {
413
+ return {
414
+ id: responseId,
415
+ object: "response",
416
+ created_at: Math.floor(Date.now() / 1000),
417
+ status,
418
+ error,
419
+ incomplete_details: null,
420
+ instructions: metadata.instructions,
421
+ max_output_tokens: metadata.maxOutputTokens,
422
+ model,
423
+ output,
424
+ parallel_tool_calls: metadata.parallelToolCalls ?? true,
425
+ previous_response_id: metadata.previousResponseId,
426
+ reasoning: null,
427
+ store: false,
428
+ temperature: metadata.temperature,
429
+ text: metadata.text,
430
+ tool_choice: metadata.toolChoice,
431
+ tools: metadata.tools,
432
+ top_p: metadata.topP,
433
+ truncation: metadata.truncation ?? "disabled",
434
+ usage,
435
+ user: metadata.user,
436
+ metadata: metadata.metadata,
437
+ };
438
+ }
439
+ function chatMessageToResponseOutput(message) {
440
+ const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : [];
441
+ if (toolCalls.length > 0) {
442
+ return toolCalls
443
+ .map((toolCall) => {
444
+ if (!toolCall || typeof toolCall !== "object" || Array.isArray(toolCall)) {
445
+ return null;
446
+ }
447
+ const record = toolCall;
448
+ const fn = record.function && typeof record.function === "object" && !Array.isArray(record.function)
449
+ ? record.function
450
+ : {};
451
+ const name = stringValue(fn.name);
452
+ if (!name) {
453
+ return null;
454
+ }
455
+ const responseName = responsesFunctionCallNameFromChatName(name);
456
+ return {
457
+ type: "function_call",
458
+ id: stringValue(record.id) ?? `fc_${randomUUID().replace(/-/g, "")}`,
459
+ call_id: stringValue(record.id) ?? `call_${randomUUID().replace(/-/g, "")}`,
460
+ ...responseName,
461
+ arguments: typeof fn.arguments === "string" ? fn.arguments : "",
462
+ status: "completed",
463
+ };
464
+ })
465
+ .filter(Boolean);
466
+ }
467
+ return [{
468
+ id: `msg_${randomUUID().replace(/-/g, "")}`,
469
+ type: "message",
470
+ status: "completed",
471
+ role: "assistant",
472
+ content: [{ type: "output_text", text: contentToText(message.content), annotations: [] }],
473
+ }];
474
+ }
475
+ async function forwardNonStreaming(args) {
476
+ const chatRequest = buildChatRequest(args.body, false, args.onLog);
477
+ const responseMetadata = responseMetadataFromBody(args.body);
478
+ const upstream = await fetch(`${normalizeBaseUrl(args.targetBaseUrl)}/chat/completions`, {
479
+ method: "POST",
480
+ headers: {
481
+ authorization: args.upstreamAuthorization,
482
+ "content-type": "application/json",
483
+ },
484
+ signal: args.signal,
485
+ body: JSON.stringify(chatRequest),
486
+ });
487
+ const upstreamBody = await upstream.json().catch(async () => ({ error: { message: await upstream.text() } }));
488
+ if (!upstream.ok) {
489
+ args.res.writeHead(upstream.status, { "content-type": "application/json" });
490
+ args.res.end(JSON.stringify({
491
+ error: {
492
+ message: upstreamErrorMessage(upstreamBody),
493
+ type: "server_error",
494
+ status: upstream.status,
495
+ upstream: recordValue(upstreamBody.error) ?? upstreamBody,
496
+ },
497
+ }));
498
+ return;
499
+ }
500
+ const choices = Array.isArray(upstreamBody.choices) ? upstreamBody.choices : [];
501
+ const firstChoice = choices[0] && typeof choices[0] === "object" ? choices[0] : {};
502
+ const message = firstChoice.message && typeof firstChoice.message === "object" ? firstChoice.message : {};
503
+ if (!message.content && typeof message.reasoning_content === "string") {
504
+ message.content = message.reasoning_content;
505
+ }
506
+ const responseId = `resp_${randomUUID().replace(/-/g, "")}`;
507
+ const usage = chatUsageToResponseUsage(upstreamBody.usage);
508
+ args.res.writeHead(200, { "content-type": "application/json" });
509
+ args.res.end(JSON.stringify(responseBase(responseId, stringValue(chatRequest.model) ?? "glm-5.2", chatMessageToResponseOutput(message), responseMetadata, usage)));
510
+ }
511
+ function parseStreamingToolCalls(delta) {
512
+ const toolCalls = Array.isArray(delta.tool_calls) ? delta.tool_calls : [];
513
+ return toolCalls
514
+ .map((toolCall) => {
515
+ if (!toolCall || typeof toolCall !== "object" || Array.isArray(toolCall)) {
516
+ return null;
517
+ }
518
+ const record = toolCall;
519
+ const fn = record.function && typeof record.function === "object" && !Array.isArray(record.function)
520
+ ? record.function
521
+ : {};
522
+ const id = stringValue(record.id) ?? `call_${randomUUID().replace(/-/g, "")}`;
523
+ return {
524
+ id,
525
+ itemId: `fc_${id.replace(/[^a-zA-Z0-9_-]/g, "")}`,
526
+ name: stringValue(fn.name) ?? "",
527
+ arguments: typeof fn.arguments === "string" ? fn.arguments : "",
528
+ outputIndex: typeof record.index === "number" ? record.index : 0,
529
+ started: false,
530
+ };
531
+ })
532
+ .filter((item) => Boolean(item));
533
+ }
534
+ function writeFailedSse(args) {
535
+ writeSse(args.res, "response.failed", {
536
+ type: "response.failed",
537
+ response: responseBase(args.responseId, args.model, [], args.responseMetadata, null, "failed", {
538
+ message: args.errorMessage,
539
+ type: "server_error",
540
+ }),
541
+ });
542
+ }
543
+ async function forwardStreaming(args) {
544
+ const chatRequest = buildChatRequest(args.body, true, args.onLog);
545
+ const model = stringValue(chatRequest.model) ?? "glm-5.2";
546
+ const responseMetadata = responseMetadataFromBody(args.body);
547
+ const responseId = `resp_${randomUUID().replace(/-/g, "")}`;
548
+ const messageId = `msg_${randomUUID().replace(/-/g, "")}`;
549
+ const outputItems = [];
550
+ let text = "";
551
+ let reasoningText = "";
552
+ let textStarted = false;
553
+ let streamUsage = null;
554
+ const tools = new Map();
555
+ args.res.writeHead(200, {
556
+ "content-type": "text/event-stream; charset=utf-8",
557
+ "cache-control": "no-cache",
558
+ connection: "keep-alive",
559
+ });
560
+ writeSse(args.res, "response.created", { type: "response.created", response: responseBase(responseId, model, [], responseMetadata) });
561
+ writeSse(args.res, "response.in_progress", { type: "response.in_progress", response: responseBase(responseId, model, [], responseMetadata, null, "in_progress") });
562
+ const upstream = await fetch(`${normalizeBaseUrl(args.targetBaseUrl)}/chat/completions`, {
563
+ method: "POST",
564
+ headers: {
565
+ authorization: args.upstreamAuthorization,
566
+ "content-type": "application/json",
567
+ },
568
+ signal: args.signal,
569
+ body: JSON.stringify(chatRequest),
570
+ });
571
+ if (!upstream.ok || !upstream.body) {
572
+ const errorText = await upstream.text();
573
+ writeFailedSse({
574
+ errorMessage: `Chat Completions upstream failed: ${upstream.status} ${errorText}`,
575
+ model,
576
+ res: args.res,
577
+ responseId,
578
+ responseMetadata,
579
+ });
580
+ args.res.end();
581
+ return;
582
+ }
583
+ const reader = upstream.body.getReader();
584
+ const decoder = new TextDecoder();
585
+ let buffer = "";
586
+ let upstreamDone = false;
587
+ let streamFailed = false;
588
+ const processDataLine = (data) => {
589
+ if (!data) {
590
+ return false;
591
+ }
592
+ if (data === "[DONE]") {
593
+ upstreamDone = true;
594
+ return true;
595
+ }
596
+ let parsed;
597
+ try {
598
+ parsed = JSON.parse(data);
599
+ }
600
+ catch (error) {
601
+ writeFailedSse({
602
+ errorMessage: `Failed to parse Chat Completions stream chunk: ${error instanceof Error ? error.message : String(error)}`,
603
+ model,
604
+ res: args.res,
605
+ responseId,
606
+ responseMetadata,
607
+ });
608
+ args.res.end();
609
+ upstreamDone = true;
610
+ streamFailed = true;
611
+ return true;
612
+ }
613
+ const usage = chatUsageToResponseUsage(parsed.usage);
614
+ if (usage) {
615
+ streamUsage = usage;
616
+ }
617
+ const choices = Array.isArray(parsed.choices) ? parsed.choices : [];
618
+ const firstChoice = choices[0] && typeof choices[0] === "object" ? choices[0] : {};
619
+ const delta = firstChoice.delta && typeof firstChoice.delta === "object" ? firstChoice.delta : {};
620
+ const message = firstChoice.message && typeof firstChoice.message === "object" ? firstChoice.message : {};
621
+ const effectiveDelta = Object.keys(delta).length > 0 ? delta : message;
622
+ const reasoningChunk = textValue(effectiveDelta.reasoning_content) ?? "";
623
+ if (reasoningChunk) {
624
+ reasoningText += reasoningChunk;
625
+ }
626
+ const chunk = streamDeltaText(effectiveDelta);
627
+ if (chunk) {
628
+ if (!textStarted) {
629
+ textStarted = true;
630
+ writeSse(args.res, "response.output_item.added", {
631
+ type: "response.output_item.added",
632
+ output_index: 0,
633
+ item: { id: messageId, type: "message", status: "in_progress", role: "assistant", content: [] },
634
+ });
635
+ writeSse(args.res, "response.content_part.added", {
636
+ type: "response.content_part.added",
637
+ item_id: messageId,
638
+ output_index: 0,
639
+ content_index: 0,
640
+ part: { type: "output_text", text: "", annotations: [] },
641
+ });
642
+ }
643
+ text += chunk;
644
+ writeSse(args.res, "response.output_text.delta", {
645
+ type: "response.output_text.delta",
646
+ item_id: messageId,
647
+ output_index: 0,
648
+ content_index: 0,
649
+ delta: chunk,
650
+ });
651
+ }
652
+ for (const partial of parseStreamingToolCalls(effectiveDelta)) {
653
+ const existing = tools.get(partial.id) ?? { ...partial, arguments: "" };
654
+ existing.name = partial.name || existing.name;
655
+ existing.arguments += partial.arguments;
656
+ if (!existing.started && existing.name) {
657
+ existing.started = true;
658
+ const outputIndex = tools.size + (textStarted ? 1 : 0);
659
+ existing.outputIndex = outputIndex;
660
+ const responseName = responsesFunctionCallNameFromChatName(existing.name);
661
+ writeSse(args.res, "response.output_item.added", {
662
+ type: "response.output_item.added",
663
+ output_index: outputIndex,
664
+ item: {
665
+ id: existing.itemId,
666
+ type: "function_call",
667
+ status: "in_progress",
668
+ call_id: existing.id,
669
+ ...responseName,
670
+ arguments: "",
671
+ },
672
+ });
673
+ }
674
+ if (partial.arguments) {
675
+ writeSse(args.res, "response.function_call_arguments.delta", {
676
+ type: "response.function_call_arguments.delta",
677
+ item_id: existing.itemId,
678
+ output_index: existing.outputIndex,
679
+ delta: partial.arguments,
680
+ });
681
+ }
682
+ tools.set(partial.id, existing);
683
+ }
684
+ return false;
685
+ };
686
+ try {
687
+ while (!upstreamDone) {
688
+ const { done, value } = await reader.read();
689
+ if (done)
690
+ break;
691
+ buffer += decoder.decode(value, { stream: true });
692
+ const lines = buffer.split(/\r?\n/);
693
+ buffer = lines.pop() ?? "";
694
+ for (const line of lines) {
695
+ const trimmed = line.trim();
696
+ if (!trimmed.startsWith("data:")) {
697
+ continue;
698
+ }
699
+ const data = trimmed.slice(5).trim();
700
+ if (processDataLine(data)) {
701
+ await reader.cancel().catch(() => undefined);
702
+ break;
703
+ }
704
+ }
705
+ }
706
+ const tail = buffer.trim();
707
+ if (!upstreamDone && tail.startsWith("data:")) {
708
+ processDataLine(tail.slice(5).trim());
709
+ }
710
+ }
711
+ catch (error) {
712
+ if (args.signal.aborted) {
713
+ return;
714
+ }
715
+ writeFailedSse({
716
+ errorMessage: `Chat Completions stream failed: ${error instanceof Error ? error.message : String(error)}`,
717
+ model,
718
+ res: args.res,
719
+ responseId,
720
+ responseMetadata,
721
+ });
722
+ args.res.end();
723
+ return;
724
+ }
725
+ if (streamFailed) {
726
+ return;
727
+ }
728
+ if (!textStarted && reasoningText) {
729
+ textStarted = true;
730
+ text = reasoningText;
731
+ writeSse(args.res, "response.output_item.added", {
732
+ type: "response.output_item.added",
733
+ output_index: 0,
734
+ item: { id: messageId, type: "message", status: "in_progress", role: "assistant", content: [] },
735
+ });
736
+ writeSse(args.res, "response.content_part.added", {
737
+ type: "response.content_part.added",
738
+ item_id: messageId,
739
+ output_index: 0,
740
+ content_index: 0,
741
+ part: { type: "output_text", text: "", annotations: [] },
742
+ });
743
+ writeSse(args.res, "response.output_text.delta", {
744
+ type: "response.output_text.delta",
745
+ item_id: messageId,
746
+ output_index: 0,
747
+ content_index: 0,
748
+ delta: text,
749
+ });
750
+ }
751
+ if (textStarted) {
752
+ writeSse(args.res, "response.output_text.done", {
753
+ type: "response.output_text.done",
754
+ item_id: messageId,
755
+ output_index: 0,
756
+ content_index: 0,
757
+ text,
758
+ });
759
+ writeSse(args.res, "response.content_part.done", {
760
+ type: "response.content_part.done",
761
+ item_id: messageId,
762
+ output_index: 0,
763
+ content_index: 0,
764
+ part: { type: "output_text", text, annotations: [] },
765
+ });
766
+ const item = {
767
+ id: messageId,
768
+ type: "message",
769
+ status: "completed",
770
+ role: "assistant",
771
+ content: [{ type: "output_text", text, annotations: [] }],
772
+ };
773
+ outputItems.push(item);
774
+ writeSse(args.res, "response.output_item.done", { type: "response.output_item.done", output_index: 0, item });
775
+ }
776
+ for (const tool of tools.values()) {
777
+ const responseName = responsesFunctionCallNameFromChatName(tool.name);
778
+ const item = {
779
+ id: tool.itemId,
780
+ type: "function_call",
781
+ status: "completed",
782
+ call_id: tool.id,
783
+ ...responseName,
784
+ arguments: tool.arguments,
785
+ };
786
+ outputItems.push(item);
787
+ writeSse(args.res, "response.function_call_arguments.done", {
788
+ type: "response.function_call_arguments.done",
789
+ item_id: tool.itemId,
790
+ output_index: tool.outputIndex,
791
+ arguments: tool.arguments,
792
+ });
793
+ writeSse(args.res, "response.output_item.done", {
794
+ type: "response.output_item.done",
795
+ output_index: tool.outputIndex,
796
+ item,
797
+ });
798
+ }
799
+ writeSse(args.res, "response.completed", { type: "response.completed", response: responseBase(responseId, model, outputItems, responseMetadata, streamUsage) });
800
+ args.res.end();
801
+ }
802
+ function requestAbortSignal(req, res) {
803
+ const abortController = new AbortController();
804
+ const abort = () => {
805
+ if (!res.writableEnded && !abortController.signal.aborted) {
806
+ abortController.abort();
807
+ }
808
+ };
809
+ req.once("aborted", abort);
810
+ res.once("close", abort);
811
+ return abortController.signal;
812
+ }
813
+ async function startResponsesBridge(args) {
814
+ const targetBaseUrl = normalizeBaseUrl(args.targetBaseUrl);
815
+ const expectedAuthorization = `Bearer ${args.localApiKey}`;
816
+ const upstreamAuthorization = `Bearer ${args.upstreamApiKey}`;
817
+ const server = createServer(async (req, res) => {
818
+ try {
819
+ if (req.method !== "POST" || !req.url?.replace(/\?.*$/, "").endsWith("/responses")) {
820
+ res.writeHead(404, { "content-type": "application/json" });
821
+ res.end(JSON.stringify({ error: { message: "Not Found" } }));
822
+ return;
823
+ }
824
+ const authorization = stringValue(req.headers.authorization) ?? "";
825
+ if (authorization !== expectedAuthorization) {
826
+ res.writeHead(401, { "content-type": "application/json" });
827
+ res.end(JSON.stringify({ error: { message: "Invalid Authorization header" } }));
828
+ return;
829
+ }
830
+ const body = await readRequestJson(req);
831
+ const signal = requestAbortSignal(req, res);
832
+ if (body.stream === true) {
833
+ await forwardStreaming({ body, res, signal, targetBaseUrl, upstreamAuthorization, onLog: args.onLog });
834
+ }
835
+ else {
836
+ await forwardNonStreaming({ body, res, signal, targetBaseUrl, upstreamAuthorization, onLog: args.onLog });
837
+ }
838
+ }
839
+ catch (error) {
840
+ if (error instanceof Error && error.name === "AbortError") {
841
+ return;
842
+ }
843
+ args.onLog?.(`Codex chat bridge failed: ${error instanceof Error ? error.message : String(error)}`);
844
+ if (!res.headersSent) {
845
+ res.writeHead(500, { "content-type": "application/json" });
846
+ }
847
+ res.end(JSON.stringify({ error: { message: error instanceof Error ? error.message : String(error) } }));
848
+ }
849
+ });
850
+ await new Promise((resolve, reject) => {
851
+ server.once("error", reject);
852
+ server.listen(0, "127.0.0.1", () => {
853
+ server.off("error", reject);
854
+ resolve();
855
+ });
856
+ });
857
+ const address = server.address();
858
+ if (!address || typeof address === "string") {
859
+ throw new Error("Failed to start Codex chat bridge");
860
+ }
861
+ return {
862
+ baseUrl: `http://127.0.0.1:${address.port}`,
863
+ stop: () => stopServer(server),
864
+ };
865
+ }
866
+ export async function startCodexChatBridge(args) {
867
+ const providerName = args.providerName?.trim() || "Chat Completions provider";
868
+ const providerApiKey = args.providerApiKey.trim();
869
+ if (!providerApiKey) {
870
+ throw new Error(`${providerName} API key is required to start the Codex chat bridge`);
871
+ }
872
+ const targetBaseUrl = resolveTargetBaseUrl({ providerId: args.providerId, targetBaseUrl: args.targetBaseUrl });
873
+ const apiKey = `sk-doer-chat-bridge-${randomUUID().replace(/-/g, "")}`;
874
+ const bridge = await startResponsesBridge({
875
+ localApiKey: apiKey,
876
+ targetBaseUrl,
877
+ upstreamApiKey: providerApiKey,
878
+ onLog: args.onLog,
879
+ });
880
+ args.onLog?.(`Codex chat bridge listening baseUrl=${bridge.baseUrl} target=${targetBaseUrl} provider=${providerName}`);
881
+ return {
882
+ baseUrl: bridge.baseUrl,
883
+ envKey: CHAT_BRIDGE_ENV_KEY,
884
+ apiKey,
885
+ stop: bridge.stop,
886
+ };
887
+ }
888
+ function stopServer(server) {
889
+ return new Promise((resolve, reject) => {
890
+ server.close((error) => {
891
+ if (error) {
892
+ reject(error);
893
+ return;
894
+ }
895
+ resolve();
896
+ });
897
+ });
898
+ }