doer-agent 0.8.5 → 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,685 @@
1
+ import { spawn } from "node:child_process";
2
+ import { randomUUID } from "node:crypto";
3
+ import { createServer } from "node:http";
4
+ import { mkdir, rm, writeFile } from "node:fs/promises";
5
+ import path from "node:path";
6
+ const ZAI_CODING_BASE_URL = "https://api.z.ai/api/coding/paas/v4";
7
+ const LITELLM_ENV_KEY = "LITELLM_API_KEY";
8
+ function normalizeBaseUrl(value) {
9
+ return value.replace(/\/+$/, "");
10
+ }
11
+ function stringValue(value) {
12
+ return typeof value === "string" && value.trim() ? value.trim() : null;
13
+ }
14
+ function yamlString(value) {
15
+ return JSON.stringify(value);
16
+ }
17
+ function readRequestJson(req) {
18
+ return new Promise((resolve, reject) => {
19
+ const chunks = [];
20
+ req.on("data", (chunk) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
21
+ req.on("end", () => {
22
+ const raw = Buffer.concat(chunks).toString("utf8").trim();
23
+ if (!raw) {
24
+ resolve({});
25
+ return;
26
+ }
27
+ try {
28
+ const parsed = JSON.parse(raw);
29
+ resolve(parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {});
30
+ }
31
+ catch (error) {
32
+ reject(error);
33
+ }
34
+ });
35
+ req.on("error", reject);
36
+ });
37
+ }
38
+ function contentToText(value) {
39
+ if (typeof value === "string") {
40
+ return value;
41
+ }
42
+ if (!Array.isArray(value)) {
43
+ return "";
44
+ }
45
+ return value
46
+ .map((part) => {
47
+ if (typeof part === "string") {
48
+ return part;
49
+ }
50
+ if (!part || typeof part !== "object" || Array.isArray(part)) {
51
+ return "";
52
+ }
53
+ const record = part;
54
+ return stringValue(record.text) ?? stringValue(record.output_text) ?? "";
55
+ })
56
+ .filter(Boolean)
57
+ .join("\n");
58
+ }
59
+ function inputItemToMessages(item) {
60
+ if (!item || typeof item !== "object" || Array.isArray(item)) {
61
+ return [];
62
+ }
63
+ const record = item;
64
+ const type = stringValue(record.type);
65
+ const role = stringValue(record.role);
66
+ if (role === "user" || role === "assistant" || role === "system" || role === "developer") {
67
+ return [{
68
+ role: role === "developer" ? "system" : role,
69
+ content: contentToText(record.content),
70
+ }];
71
+ }
72
+ if (type === "message") {
73
+ const messageRole = stringValue(record.role) ?? "user";
74
+ return [{
75
+ role: messageRole === "developer" ? "system" : messageRole,
76
+ content: contentToText(record.content),
77
+ }];
78
+ }
79
+ if (type === "function_call") {
80
+ const name = stringValue(record.name);
81
+ const callId = stringValue(record.call_id) ?? stringValue(record.id) ?? `call_${randomUUID().replace(/-/g, "")}`;
82
+ if (!name) {
83
+ return [];
84
+ }
85
+ return [{
86
+ role: "assistant",
87
+ content: null,
88
+ tool_calls: [{
89
+ id: callId,
90
+ type: "function",
91
+ function: {
92
+ name,
93
+ arguments: typeof record.arguments === "string" ? record.arguments : "",
94
+ },
95
+ }],
96
+ }];
97
+ }
98
+ if (type === "function_call_output") {
99
+ const callId = stringValue(record.call_id);
100
+ if (!callId) {
101
+ return [];
102
+ }
103
+ return [{
104
+ role: "tool",
105
+ tool_call_id: callId,
106
+ content: typeof record.output === "string" ? record.output : JSON.stringify(record.output ?? ""),
107
+ }];
108
+ }
109
+ return [];
110
+ }
111
+ function inputToMessages(input) {
112
+ if (typeof input === "string") {
113
+ return [{ role: "user", content: input }];
114
+ }
115
+ if (!Array.isArray(input)) {
116
+ return [];
117
+ }
118
+ return input.flatMap(inputItemToMessages);
119
+ }
120
+ function responsesToolToChatTool(tool) {
121
+ if (!tool || typeof tool !== "object" || Array.isArray(tool)) {
122
+ return null;
123
+ }
124
+ const record = tool;
125
+ const type = stringValue(record.type);
126
+ if (type !== "function") {
127
+ return null;
128
+ }
129
+ const name = stringValue(record.name);
130
+ if (!name) {
131
+ return null;
132
+ }
133
+ return {
134
+ type: "function",
135
+ function: {
136
+ name,
137
+ description: stringValue(record.description) ?? "",
138
+ parameters: record.parameters && typeof record.parameters === "object" ? record.parameters : { type: "object", properties: {} },
139
+ },
140
+ };
141
+ }
142
+ function buildChatRequest(body, stream) {
143
+ const messages = inputToMessages(body.input);
144
+ const instructions = stringValue(body.instructions);
145
+ if (instructions) {
146
+ messages.unshift({ role: "system", content: instructions });
147
+ }
148
+ const tools = Array.isArray(body.tools) ? body.tools.map(responsesToolToChatTool).filter(Boolean) : [];
149
+ return {
150
+ model: stringValue(body.model) ?? "glm-5.2",
151
+ messages,
152
+ stream,
153
+ ...(typeof body.temperature === "number" ? { temperature: body.temperature } : {}),
154
+ ...(typeof body.top_p === "number" ? { top_p: body.top_p } : {}),
155
+ ...(typeof body.max_output_tokens === "number" ? { max_tokens: body.max_output_tokens } : {}),
156
+ ...(tools.length > 0 ? { tools } : {}),
157
+ };
158
+ }
159
+ function writeSse(res, event, data) {
160
+ res.write(`event: ${event}\n`);
161
+ res.write(`data: ${JSON.stringify(data)}\n\n`);
162
+ }
163
+ function responseBase(responseId, model, output = []) {
164
+ return {
165
+ id: responseId,
166
+ object: "response",
167
+ created_at: Math.floor(Date.now() / 1000),
168
+ status: "completed",
169
+ error: null,
170
+ incomplete_details: null,
171
+ instructions: null,
172
+ max_output_tokens: null,
173
+ model,
174
+ output,
175
+ parallel_tool_calls: true,
176
+ previous_response_id: null,
177
+ reasoning: null,
178
+ store: false,
179
+ temperature: null,
180
+ text: { format: { type: "text" } },
181
+ tool_choice: "auto",
182
+ tools: [],
183
+ top_p: null,
184
+ truncation: "disabled",
185
+ usage: null,
186
+ user: null,
187
+ metadata: {},
188
+ };
189
+ }
190
+ function chatMessageToResponseOutput(message) {
191
+ const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : [];
192
+ if (toolCalls.length > 0) {
193
+ return toolCalls
194
+ .map((toolCall) => {
195
+ if (!toolCall || typeof toolCall !== "object" || Array.isArray(toolCall)) {
196
+ return null;
197
+ }
198
+ const record = toolCall;
199
+ const fn = record.function && typeof record.function === "object" && !Array.isArray(record.function)
200
+ ? record.function
201
+ : {};
202
+ const name = stringValue(fn.name);
203
+ if (!name) {
204
+ return null;
205
+ }
206
+ return {
207
+ type: "function_call",
208
+ id: stringValue(record.id) ?? `fc_${randomUUID().replace(/-/g, "")}`,
209
+ call_id: stringValue(record.id) ?? `call_${randomUUID().replace(/-/g, "")}`,
210
+ name,
211
+ arguments: typeof fn.arguments === "string" ? fn.arguments : "",
212
+ status: "completed",
213
+ };
214
+ })
215
+ .filter(Boolean);
216
+ }
217
+ return [{
218
+ id: `msg_${randomUUID().replace(/-/g, "")}`,
219
+ type: "message",
220
+ status: "completed",
221
+ role: "assistant",
222
+ content: [{ type: "output_text", text: contentToText(message.content), annotations: [] }],
223
+ }];
224
+ }
225
+ async function forwardNonStreaming(args) {
226
+ const chatRequest = buildChatRequest(args.body, false);
227
+ const upstream = await fetch(`${normalizeBaseUrl(args.targetBaseUrl)}/chat/completions`, {
228
+ method: "POST",
229
+ headers: {
230
+ authorization: args.authorization,
231
+ "content-type": "application/json",
232
+ },
233
+ body: JSON.stringify(chatRequest),
234
+ });
235
+ const upstreamBody = await upstream.json().catch(async () => ({ error: { message: await upstream.text() } }));
236
+ if (!upstream.ok) {
237
+ args.res.writeHead(upstream.status, { "content-type": "application/json" });
238
+ args.res.end(JSON.stringify(upstreamBody));
239
+ return;
240
+ }
241
+ const choices = Array.isArray(upstreamBody.choices) ? upstreamBody.choices : [];
242
+ const firstChoice = choices[0] && typeof choices[0] === "object" ? choices[0] : {};
243
+ const message = firstChoice.message && typeof firstChoice.message === "object" ? firstChoice.message : {};
244
+ if (!message.content && typeof message.reasoning_content === "string") {
245
+ message.content = message.reasoning_content;
246
+ }
247
+ const responseId = `resp_${randomUUID().replace(/-/g, "")}`;
248
+ args.res.writeHead(200, { "content-type": "application/json" });
249
+ args.res.end(JSON.stringify(responseBase(responseId, stringValue(chatRequest.model) ?? "glm-5.2", chatMessageToResponseOutput(message))));
250
+ }
251
+ function parseStreamingToolCalls(delta) {
252
+ const toolCalls = Array.isArray(delta.tool_calls) ? delta.tool_calls : [];
253
+ return toolCalls
254
+ .map((toolCall) => {
255
+ if (!toolCall || typeof toolCall !== "object" || Array.isArray(toolCall)) {
256
+ return null;
257
+ }
258
+ const record = toolCall;
259
+ const fn = record.function && typeof record.function === "object" && !Array.isArray(record.function)
260
+ ? record.function
261
+ : {};
262
+ const id = stringValue(record.id) ?? `call_${randomUUID().replace(/-/g, "")}`;
263
+ return {
264
+ id,
265
+ itemId: `fc_${id.replace(/[^a-zA-Z0-9_-]/g, "")}`,
266
+ name: stringValue(fn.name) ?? "",
267
+ arguments: typeof fn.arguments === "string" ? fn.arguments : "",
268
+ outputIndex: typeof record.index === "number" ? record.index : 0,
269
+ started: false,
270
+ };
271
+ })
272
+ .filter((item) => Boolean(item));
273
+ }
274
+ async function forwardStreaming(args) {
275
+ const chatRequest = buildChatRequest(args.body, true);
276
+ const model = stringValue(chatRequest.model) ?? "glm-5.2";
277
+ const responseId = `resp_${randomUUID().replace(/-/g, "")}`;
278
+ const messageId = `msg_${randomUUID().replace(/-/g, "")}`;
279
+ const outputItems = [];
280
+ let text = "";
281
+ let reasoningText = "";
282
+ let textStarted = false;
283
+ const tools = new Map();
284
+ args.res.writeHead(200, {
285
+ "content-type": "text/event-stream; charset=utf-8",
286
+ "cache-control": "no-cache",
287
+ connection: "keep-alive",
288
+ });
289
+ writeSse(args.res, "response.created", { type: "response.created", response: responseBase(responseId, model, []) });
290
+ writeSse(args.res, "response.in_progress", { type: "response.in_progress", response: { ...responseBase(responseId, model, []), status: "in_progress" } });
291
+ const upstream = await fetch(`${normalizeBaseUrl(args.targetBaseUrl)}/chat/completions`, {
292
+ method: "POST",
293
+ headers: {
294
+ authorization: args.authorization,
295
+ "content-type": "application/json",
296
+ },
297
+ body: JSON.stringify(chatRequest),
298
+ });
299
+ if (!upstream.ok || !upstream.body) {
300
+ const errorText = await upstream.text();
301
+ const message = `LiteLLM chat/completions failed: ${upstream.status} ${errorText}`;
302
+ writeSse(args.res, "response.failed", {
303
+ type: "response.failed",
304
+ response: { ...responseBase(responseId, model, []), status: "failed", error: { message, type: "server_error" } },
305
+ });
306
+ args.res.end();
307
+ return;
308
+ }
309
+ const reader = upstream.body.getReader();
310
+ const decoder = new TextDecoder();
311
+ let buffer = "";
312
+ while (true) {
313
+ const { done, value } = await reader.read();
314
+ if (done)
315
+ break;
316
+ buffer += decoder.decode(value, { stream: true });
317
+ const lines = buffer.split(/\r?\n/);
318
+ buffer = lines.pop() ?? "";
319
+ for (const line of lines) {
320
+ const trimmed = line.trim();
321
+ if (!trimmed.startsWith("data:")) {
322
+ continue;
323
+ }
324
+ const data = trimmed.slice(5).trim();
325
+ if (!data || data === "[DONE]") {
326
+ continue;
327
+ }
328
+ const parsed = JSON.parse(data);
329
+ const choices = Array.isArray(parsed.choices) ? parsed.choices : [];
330
+ const firstChoice = choices[0] && typeof choices[0] === "object" ? choices[0] : {};
331
+ const delta = firstChoice.delta && typeof firstChoice.delta === "object" ? firstChoice.delta : {};
332
+ const reasoningChunk = stringValue(delta.reasoning_content) ?? "";
333
+ if (reasoningChunk) {
334
+ reasoningText += reasoningChunk;
335
+ }
336
+ const chunk = stringValue(delta.content) ?? "";
337
+ if (chunk) {
338
+ if (!textStarted) {
339
+ textStarted = true;
340
+ writeSse(args.res, "response.output_item.added", {
341
+ type: "response.output_item.added",
342
+ output_index: 0,
343
+ item: { id: messageId, type: "message", status: "in_progress", role: "assistant", content: [] },
344
+ });
345
+ writeSse(args.res, "response.content_part.added", {
346
+ type: "response.content_part.added",
347
+ item_id: messageId,
348
+ output_index: 0,
349
+ content_index: 0,
350
+ part: { type: "output_text", text: "", annotations: [] },
351
+ });
352
+ }
353
+ text += chunk;
354
+ writeSse(args.res, "response.output_text.delta", {
355
+ type: "response.output_text.delta",
356
+ item_id: messageId,
357
+ output_index: 0,
358
+ content_index: 0,
359
+ delta: chunk,
360
+ });
361
+ }
362
+ for (const partial of parseStreamingToolCalls(delta)) {
363
+ const existing = tools.get(partial.id) ?? partial;
364
+ existing.name = partial.name || existing.name;
365
+ existing.arguments += partial.arguments;
366
+ if (!existing.started && existing.name) {
367
+ existing.started = true;
368
+ const outputIndex = tools.size + (textStarted ? 1 : 0);
369
+ existing.outputIndex = outputIndex;
370
+ writeSse(args.res, "response.output_item.added", {
371
+ type: "response.output_item.added",
372
+ output_index: outputIndex,
373
+ item: {
374
+ id: existing.itemId,
375
+ type: "function_call",
376
+ status: "in_progress",
377
+ call_id: existing.id,
378
+ name: existing.name,
379
+ arguments: "",
380
+ },
381
+ });
382
+ }
383
+ if (partial.arguments) {
384
+ writeSse(args.res, "response.function_call_arguments.delta", {
385
+ type: "response.function_call_arguments.delta",
386
+ item_id: existing.itemId,
387
+ output_index: existing.outputIndex,
388
+ delta: partial.arguments,
389
+ });
390
+ }
391
+ tools.set(partial.id, existing);
392
+ }
393
+ }
394
+ }
395
+ if (!textStarted && reasoningText) {
396
+ textStarted = true;
397
+ text = reasoningText;
398
+ writeSse(args.res, "response.output_item.added", {
399
+ type: "response.output_item.added",
400
+ output_index: 0,
401
+ item: { id: messageId, type: "message", status: "in_progress", role: "assistant", content: [] },
402
+ });
403
+ writeSse(args.res, "response.content_part.added", {
404
+ type: "response.content_part.added",
405
+ item_id: messageId,
406
+ output_index: 0,
407
+ content_index: 0,
408
+ part: { type: "output_text", text: "", annotations: [] },
409
+ });
410
+ writeSse(args.res, "response.output_text.delta", {
411
+ type: "response.output_text.delta",
412
+ item_id: messageId,
413
+ output_index: 0,
414
+ content_index: 0,
415
+ delta: text,
416
+ });
417
+ }
418
+ if (textStarted) {
419
+ writeSse(args.res, "response.output_text.done", {
420
+ type: "response.output_text.done",
421
+ item_id: messageId,
422
+ output_index: 0,
423
+ content_index: 0,
424
+ text,
425
+ });
426
+ writeSse(args.res, "response.content_part.done", {
427
+ type: "response.content_part.done",
428
+ item_id: messageId,
429
+ output_index: 0,
430
+ content_index: 0,
431
+ part: { type: "output_text", text, annotations: [] },
432
+ });
433
+ const item = {
434
+ id: messageId,
435
+ type: "message",
436
+ status: "completed",
437
+ role: "assistant",
438
+ content: [{ type: "output_text", text, annotations: [] }],
439
+ };
440
+ outputItems.push(item);
441
+ writeSse(args.res, "response.output_item.done", { type: "response.output_item.done", output_index: 0, item });
442
+ }
443
+ for (const tool of tools.values()) {
444
+ const item = {
445
+ id: tool.itemId,
446
+ type: "function_call",
447
+ status: "completed",
448
+ call_id: tool.id,
449
+ name: tool.name,
450
+ arguments: tool.arguments,
451
+ };
452
+ outputItems.push(item);
453
+ writeSse(args.res, "response.function_call_arguments.done", {
454
+ type: "response.function_call_arguments.done",
455
+ item_id: tool.itemId,
456
+ output_index: tool.outputIndex,
457
+ arguments: tool.arguments,
458
+ });
459
+ writeSse(args.res, "response.output_item.done", {
460
+ type: "response.output_item.done",
461
+ output_index: tool.outputIndex,
462
+ item,
463
+ });
464
+ }
465
+ writeSse(args.res, "response.completed", { type: "response.completed", response: responseBase(responseId, model, outputItems) });
466
+ args.res.end();
467
+ }
468
+ async function startResponsesBridge(args) {
469
+ const targetBaseUrl = normalizeBaseUrl(args.targetBaseUrl);
470
+ const server = createServer(async (req, res) => {
471
+ try {
472
+ if (req.method !== "POST" || !req.url?.replace(/\?.*$/, "").endsWith("/responses")) {
473
+ res.writeHead(404, { "content-type": "application/json" });
474
+ res.end(JSON.stringify({ error: { message: "Not Found" } }));
475
+ return;
476
+ }
477
+ const authorization = stringValue(req.headers.authorization) ?? "";
478
+ if (!authorization) {
479
+ res.writeHead(401, { "content-type": "application/json" });
480
+ res.end(JSON.stringify({ error: { message: "Missing Authorization header" } }));
481
+ return;
482
+ }
483
+ const body = await readRequestJson(req);
484
+ if (body.stream === false) {
485
+ await forwardNonStreaming({ authorization, body, res, targetBaseUrl });
486
+ }
487
+ else {
488
+ await forwardStreaming({ authorization, body, res, targetBaseUrl });
489
+ }
490
+ }
491
+ catch (error) {
492
+ args.onLog?.(`LiteLLM responses bridge failed: ${error instanceof Error ? error.message : String(error)}`);
493
+ if (!res.headersSent) {
494
+ res.writeHead(500, { "content-type": "application/json" });
495
+ }
496
+ res.end(JSON.stringify({ error: { message: error instanceof Error ? error.message : String(error) } }));
497
+ }
498
+ });
499
+ await new Promise((resolve, reject) => {
500
+ server.once("error", reject);
501
+ server.listen(0, "127.0.0.1", () => {
502
+ server.off("error", reject);
503
+ resolve();
504
+ });
505
+ });
506
+ const address = server.address();
507
+ if (!address || typeof address === "string") {
508
+ throw new Error("Failed to start LiteLLM responses bridge");
509
+ }
510
+ return {
511
+ baseUrl: `http://127.0.0.1:${address.port}`,
512
+ stop: () => stopServer(server),
513
+ };
514
+ }
515
+ function resolveRuntimeDir(workspaceRoot) {
516
+ return path.resolve(workspaceRoot, ".doer-agent", "runtime", "litellm");
517
+ }
518
+ function buildLiteLlmConfig(model) {
519
+ return [
520
+ "model_list:",
521
+ " - model_name: " + yamlString(model),
522
+ " litellm_params:",
523
+ " model: " + yamlString(`openai/${model}`),
524
+ " api_base: " + yamlString(ZAI_CODING_BASE_URL),
525
+ " api_key: os.environ/ZAI_API_KEY",
526
+ " drop_params: true",
527
+ "litellm_settings:",
528
+ " drop_params: true",
529
+ "general_settings:",
530
+ " master_key: os.environ/LITELLM_MASTER_KEY",
531
+ "",
532
+ ].join("\n");
533
+ }
534
+ async function getFreePort() {
535
+ const server = createServer();
536
+ await new Promise((resolve, reject) => {
537
+ server.once("error", reject);
538
+ server.listen(0, "127.0.0.1", () => {
539
+ server.off("error", reject);
540
+ resolve();
541
+ });
542
+ });
543
+ const address = server.address();
544
+ await stopServer(server);
545
+ if (!address || typeof address === "string") {
546
+ throw new Error("Failed to allocate local port");
547
+ }
548
+ return address.port;
549
+ }
550
+ async function waitForLiteLlm(port, child) {
551
+ const deadline = Date.now() + 45_000;
552
+ let lastError = "";
553
+ while (Date.now() < deadline) {
554
+ if (child.exitCode !== null) {
555
+ throw new Error(`LiteLLM exited before startup code=${child.exitCode}`);
556
+ }
557
+ try {
558
+ const response = await fetch(`http://127.0.0.1:${port}/health/readiness`);
559
+ if (response.ok) {
560
+ return;
561
+ }
562
+ lastError = `${response.status} ${await response.text().catch(() => "")}`;
563
+ }
564
+ catch (error) {
565
+ lastError = error instanceof Error ? error.message : String(error);
566
+ }
567
+ await new Promise((resolve) => setTimeout(resolve, 500));
568
+ }
569
+ throw new Error(`Timed out waiting for LiteLLM startup${lastError ? `: ${lastError}` : ""}`);
570
+ }
571
+ function signalProcessGroup(pid, signal) {
572
+ if (!pid || process.platform === "win32") {
573
+ return false;
574
+ }
575
+ try {
576
+ process.kill(-pid, signal);
577
+ return true;
578
+ }
579
+ catch {
580
+ return false;
581
+ }
582
+ }
583
+ async function stopChild(child) {
584
+ if (child.exitCode !== null || child.killed) {
585
+ return;
586
+ }
587
+ const pid = child.pid;
588
+ signalProcessGroup(pid, "SIGTERM") || child.kill("SIGTERM");
589
+ const exited = await new Promise((resolve) => {
590
+ const timer = setTimeout(() => resolve(false), 5_000);
591
+ child.once("exit", () => {
592
+ clearTimeout(timer);
593
+ resolve(true);
594
+ });
595
+ });
596
+ if (!exited) {
597
+ signalProcessGroup(pid, "SIGKILL") || child.kill("SIGKILL");
598
+ }
599
+ }
600
+ export async function startLitellmResponsesProxy(args) {
601
+ const model = args.model.trim() || "glm-5.2";
602
+ const zaiApiKey = args.zaiApiKey.trim();
603
+ if (!zaiApiKey) {
604
+ throw new Error("Z.AI API key is required to start LiteLLM");
605
+ }
606
+ const runtimeDir = resolveRuntimeDir(args.workspaceRoot);
607
+ await mkdir(runtimeDir, { recursive: true });
608
+ const instanceId = `${process.pid}-${randomUUID().slice(0, 8)}`;
609
+ const configPath = path.join(runtimeDir, `config-${instanceId}.yaml`);
610
+ await writeFile(configPath, buildLiteLlmConfig(model), "utf8");
611
+ const port = await getFreePort();
612
+ const apiKey = `sk-doer-litellm-${randomUUID().replace(/-/g, "")}`;
613
+ const child = spawn("uvx", [
614
+ "--from",
615
+ "litellm[proxy]",
616
+ "litellm",
617
+ "--config",
618
+ configPath,
619
+ "--host",
620
+ "127.0.0.1",
621
+ "--port",
622
+ String(port),
623
+ ], {
624
+ cwd: path.resolve(args.workspaceRoot),
625
+ detached: process.platform !== "win32",
626
+ env: {
627
+ ...process.env,
628
+ ZAI_API_KEY: zaiApiKey,
629
+ LITELLM_MASTER_KEY: apiKey,
630
+ },
631
+ stdio: ["ignore", "pipe", "pipe"],
632
+ });
633
+ child.stdout?.setEncoding("utf8");
634
+ child.stderr?.setEncoding("utf8");
635
+ child.stdout?.on("data", (chunk) => {
636
+ const message = chunk.trim();
637
+ if (message) {
638
+ args.onLog?.(`[litellm] ${message}`);
639
+ }
640
+ });
641
+ child.stderr?.on("data", (chunk) => {
642
+ const message = chunk.trim();
643
+ if (message) {
644
+ args.onLog?.(`[litellm] ${message}`);
645
+ }
646
+ });
647
+ try {
648
+ await waitForLiteLlm(port, child);
649
+ }
650
+ catch (error) {
651
+ await stopChild(child).catch(() => undefined);
652
+ await rm(configPath, { force: true }).catch(() => undefined);
653
+ throw error;
654
+ }
655
+ const bridge = await startResponsesBridge({
656
+ targetBaseUrl: `http://127.0.0.1:${port}/v1`,
657
+ onLog: args.onLog,
658
+ });
659
+ args.onLog?.(`LiteLLM responses bridge listening baseUrl=${bridge.baseUrl} litellm=http://127.0.0.1:${port}/v1 model=${model}`);
660
+ return {
661
+ baseUrl: bridge.baseUrl,
662
+ envKey: LITELLM_ENV_KEY,
663
+ apiKey,
664
+ stop: async () => {
665
+ await bridge.stop().catch((error) => {
666
+ args.onLog?.(`failed to stop LiteLLM responses bridge: ${error instanceof Error ? error.message : String(error)}`);
667
+ });
668
+ await stopChild(child).catch((error) => {
669
+ args.onLog?.(`failed to stop LiteLLM: ${error instanceof Error ? error.message : String(error)}`);
670
+ });
671
+ await rm(configPath, { force: true }).catch(() => undefined);
672
+ },
673
+ };
674
+ }
675
+ function stopServer(server) {
676
+ return new Promise((resolve, reject) => {
677
+ server.close((error) => {
678
+ if (error) {
679
+ reject(error);
680
+ return;
681
+ }
682
+ resolve();
683
+ });
684
+ });
685
+ }