@corbat-tech/coco 2.12.0 → 2.13.0

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/cli/index.js CHANGED
@@ -1756,7 +1756,7 @@ var init_openai = __esm({
1756
1756
  once: true
1757
1757
  });
1758
1758
  const providerName = this.name;
1759
- const parseArguments = (builder) => {
1759
+ const parseArguments2 = (builder) => {
1760
1760
  let input = {};
1761
1761
  try {
1762
1762
  input = builder.arguments ? JSON.parse(builder.arguments) : {};
@@ -1837,7 +1837,7 @@ var init_openai = __esm({
1837
1837
  toolCall: {
1838
1838
  id: builder.id,
1839
1839
  name: builder.name,
1840
- input: parseArguments(builder)
1840
+ input: parseArguments2(builder)
1841
1841
  }
1842
1842
  };
1843
1843
  }
@@ -1850,7 +1850,7 @@ var init_openai = __esm({
1850
1850
  toolCall: {
1851
1851
  id: builder.id,
1852
1852
  name: builder.name,
1853
- input: parseArguments(builder)
1853
+ input: parseArguments2(builder)
1854
1854
  }
1855
1855
  };
1856
1856
  }
@@ -4061,8 +4061,6 @@ var init_auth = __esm({
4061
4061
  init_gcloud();
4062
4062
  }
4063
4063
  });
4064
-
4065
- // src/providers/codex.ts
4066
4064
  function parseJwtClaims(token) {
4067
4065
  const parts = token.split(".");
4068
4066
  if (parts.length !== 3 || !parts[1]) return void 0;
@@ -4078,6 +4076,21 @@ function extractAccountId(accessToken) {
4078
4076
  const auth = claims["https://api.openai.com/auth"];
4079
4077
  return claims["chatgpt_account_id"] || auth?.["chatgpt_account_id"] || claims["organizations"]?.[0]?.id;
4080
4078
  }
4079
+ function parseArguments(args) {
4080
+ try {
4081
+ return args ? JSON.parse(args) : {};
4082
+ } catch {
4083
+ try {
4084
+ if (args) {
4085
+ const repaired = jsonrepair(args);
4086
+ return JSON.parse(repaired);
4087
+ }
4088
+ } catch {
4089
+ console.error(`[Codex] Cannot parse tool arguments: ${args.slice(0, 200)}`);
4090
+ }
4091
+ return {};
4092
+ }
4093
+ }
4081
4094
  function createCodexProvider(config) {
4082
4095
  const provider = new CodexProvider();
4083
4096
  if (config) {
@@ -4086,11 +4099,12 @@ function createCodexProvider(config) {
4086
4099
  }
4087
4100
  return provider;
4088
4101
  }
4089
- var CODEX_API_ENDPOINT, DEFAULT_MODEL3, CONTEXT_WINDOWS3, CodexProvider;
4102
+ var CODEX_API_ENDPOINT, DEFAULT_MODEL3, CONTEXT_WINDOWS3, STREAM_TIMEOUT_MS, CodexProvider;
4090
4103
  var init_codex = __esm({
4091
4104
  "src/providers/codex.ts"() {
4092
4105
  init_errors();
4093
4106
  init_auth();
4107
+ init_retry();
4094
4108
  CODEX_API_ENDPOINT = "https://chatgpt.com/backend-api/codex/responses";
4095
4109
  DEFAULT_MODEL3 = "gpt-5.4-codex";
4096
4110
  CONTEXT_WINDOWS3 = {
@@ -4103,12 +4117,14 @@ var init_codex = __esm({
4103
4117
  "gpt-5.2": 2e5,
4104
4118
  "gpt-5.1": 2e5
4105
4119
  };
4120
+ STREAM_TIMEOUT_MS = 12e4;
4106
4121
  CodexProvider = class {
4107
4122
  id = "codex";
4108
4123
  name = "OpenAI Codex (ChatGPT Plus/Pro)";
4109
4124
  config = {};
4110
4125
  accessToken = null;
4111
4126
  accountId;
4127
+ retryConfig = DEFAULT_RETRY_CONFIG;
4112
4128
  /**
4113
4129
  * Initialize the provider with OAuth tokens
4114
4130
  */
@@ -4193,166 +4209,466 @@ var init_codex = __esm({
4193
4209
  /**
4194
4210
  * Extract text content from a message
4195
4211
  */
4196
- extractTextContent(msg) {
4197
- if (typeof msg.content === "string") {
4198
- return msg.content;
4199
- }
4200
- if (Array.isArray(msg.content)) {
4201
- return msg.content.map((part) => {
4202
- if (part.type === "text") return part.text;
4203
- if (part.type === "tool_result") return `Tool result: ${JSON.stringify(part.content)}`;
4204
- return "";
4205
- }).join("\n");
4212
+ contentToString(content) {
4213
+ if (typeof content === "string") return content;
4214
+ if (Array.isArray(content)) {
4215
+ return content.filter((part) => part.type === "text").map((part) => part.text).join("\n");
4206
4216
  }
4207
4217
  return "";
4208
4218
  }
4209
4219
  /**
4210
- * Convert messages to Codex Responses API format
4211
- * Codex uses a different format than Chat Completions:
4212
- * {
4213
- * "input": [
4214
- * { "type": "message", "role": "developer|user", "content": [{ "type": "input_text", "text": "..." }] },
4215
- * { "type": "message", "role": "assistant", "content": [{ "type": "output_text", "text": "..." }] }
4216
- * ]
4217
- * }
4220
+ * Convert messages to Responses API input format.
4218
4221
  *
4219
- * IMPORTANT: User/developer messages use "input_text", assistant messages use "output_text"
4222
+ * Handles:
4223
+ * - system messages → extracted as instructions
4224
+ * - user text messages → { role: "user", content: "..." }
4225
+ * - user tool_result messages → function_call_output items
4226
+ * - assistant text → { role: "assistant", content: "..." }
4227
+ * - assistant tool_use → function_call items
4220
4228
  */
4221
- convertMessagesToResponsesFormat(messages) {
4222
- return messages.map((msg) => {
4223
- const text13 = this.extractTextContent(msg);
4224
- const role = msg.role === "system" ? "developer" : msg.role;
4225
- const contentType = msg.role === "assistant" ? "output_text" : "input_text";
4226
- return {
4227
- type: "message",
4228
- role,
4229
- content: [{ type: contentType, text: text13 }]
4230
- };
4231
- });
4229
+ convertToResponsesInput(messages, systemPrompt) {
4230
+ const input = [];
4231
+ let instructions = systemPrompt ?? null;
4232
+ for (const msg of messages) {
4233
+ if (msg.role === "system") {
4234
+ instructions = (instructions ? instructions + "\n\n" : "") + this.contentToString(msg.content);
4235
+ } else if (msg.role === "user") {
4236
+ if (Array.isArray(msg.content) && msg.content.some((b) => b.type === "tool_result")) {
4237
+ for (const block of msg.content) {
4238
+ if (block.type === "tool_result") {
4239
+ const tr = block;
4240
+ input.push({
4241
+ type: "function_call_output",
4242
+ call_id: tr.tool_use_id,
4243
+ output: tr.content
4244
+ });
4245
+ }
4246
+ }
4247
+ } else {
4248
+ input.push({
4249
+ role: "user",
4250
+ content: this.contentToString(msg.content)
4251
+ });
4252
+ }
4253
+ } else if (msg.role === "assistant") {
4254
+ if (typeof msg.content === "string") {
4255
+ input.push({ role: "assistant", content: msg.content });
4256
+ } else if (Array.isArray(msg.content)) {
4257
+ const textParts = [];
4258
+ for (const block of msg.content) {
4259
+ if (block.type === "text") {
4260
+ textParts.push(block.text);
4261
+ } else if (block.type === "tool_use") {
4262
+ if (textParts.length > 0) {
4263
+ input.push({ role: "assistant", content: textParts.join("") });
4264
+ textParts.length = 0;
4265
+ }
4266
+ const tu = block;
4267
+ input.push({
4268
+ type: "function_call",
4269
+ call_id: tu.id,
4270
+ name: tu.name,
4271
+ arguments: JSON.stringify(tu.input)
4272
+ });
4273
+ }
4274
+ }
4275
+ if (textParts.length > 0) {
4276
+ input.push({ role: "assistant", content: textParts.join("") });
4277
+ }
4278
+ }
4279
+ }
4280
+ }
4281
+ return { input, instructions };
4232
4282
  }
4233
4283
  /**
4234
- * Send a chat message using Codex Responses API format
4284
+ * Convert tool definitions to Responses API function tool format
4235
4285
  */
4236
- async chat(messages, options) {
4237
- const model = options?.model ?? this.config.model ?? DEFAULT_MODEL3;
4238
- const systemMsg = messages.find((m) => m.role === "system");
4239
- const instructions = systemMsg ? this.extractTextContent(systemMsg) : "You are a helpful coding assistant.";
4240
- const inputMessages = messages.filter((m) => m.role !== "system").map((msg) => this.convertMessagesToResponsesFormat([msg])[0]);
4286
+ convertTools(tools) {
4287
+ return tools.map((tool) => ({
4288
+ type: "function",
4289
+ name: tool.name,
4290
+ description: tool.description ?? void 0,
4291
+ parameters: tool.input_schema ?? null,
4292
+ strict: false
4293
+ }));
4294
+ }
4295
+ /**
4296
+ * Build the request body for the Codex Responses API
4297
+ */
4298
+ buildRequestBody(model, input, instructions, options) {
4241
4299
  const body = {
4242
4300
  model,
4243
- instructions,
4244
- input: inputMessages,
4245
- tools: [],
4301
+ input,
4302
+ instructions: instructions ?? "You are a helpful coding assistant.",
4303
+ max_output_tokens: options?.maxTokens ?? this.config.maxTokens ?? 8192,
4304
+ temperature: options?.temperature ?? this.config.temperature ?? 0,
4246
4305
  store: false,
4247
4306
  stream: true
4248
4307
  // Codex API requires streaming
4249
4308
  };
4250
- const response = await this.makeRequest(body);
4309
+ if (options?.tools && options.tools.length > 0) {
4310
+ body.tools = this.convertTools(options.tools);
4311
+ }
4312
+ return body;
4313
+ }
4314
+ /**
4315
+ * Read SSE stream and call handler for each parsed event.
4316
+ * Returns when stream ends.
4317
+ */
4318
+ async readSSEStream(response, onEvent) {
4251
4319
  if (!response.body) {
4252
- throw new ProviderError("No response body from Codex API", {
4253
- provider: this.id
4254
- });
4320
+ throw new ProviderError("No response body from Codex API", { provider: this.id });
4255
4321
  }
4256
4322
  const reader = response.body.getReader();
4257
4323
  const decoder = new TextDecoder();
4258
4324
  let buffer = "";
4259
- let content = "";
4260
- let responseId = `codex-${Date.now()}`;
4261
- let inputTokens = 0;
4262
- let outputTokens = 0;
4263
- let status = "completed";
4325
+ let lastActivityTime = Date.now();
4326
+ const timeoutController = new AbortController();
4327
+ const timeoutInterval = setInterval(() => {
4328
+ if (Date.now() - lastActivityTime > STREAM_TIMEOUT_MS) {
4329
+ clearInterval(timeoutInterval);
4330
+ timeoutController.abort();
4331
+ }
4332
+ }, 5e3);
4264
4333
  try {
4265
4334
  while (true) {
4335
+ if (timeoutController.signal.aborted) break;
4266
4336
  const { done, value } = await reader.read();
4267
4337
  if (done) break;
4338
+ lastActivityTime = Date.now();
4268
4339
  buffer += decoder.decode(value, { stream: true });
4269
4340
  const lines = buffer.split("\n");
4270
4341
  buffer = lines.pop() ?? "";
4271
4342
  for (const line of lines) {
4272
- if (line.startsWith("data: ")) {
4273
- const data = line.slice(6).trim();
4274
- if (!data || data === "[DONE]") continue;
4275
- try {
4276
- const parsed = JSON.parse(data);
4277
- if (parsed.id) {
4278
- responseId = parsed.id;
4279
- }
4280
- if (parsed.type === "response.output_text.delta" && parsed.delta) {
4281
- content += parsed.delta;
4282
- } else if (parsed.type === "response.completed" && parsed.response) {
4283
- if (parsed.response.usage) {
4284
- inputTokens = parsed.response.usage.input_tokens ?? 0;
4285
- outputTokens = parsed.response.usage.output_tokens ?? 0;
4286
- }
4287
- status = parsed.response.status ?? "completed";
4288
- } else if (parsed.type === "response.output_text.done" && parsed.text) {
4289
- content = parsed.text;
4290
- }
4291
- } catch {
4292
- }
4343
+ if (!line.startsWith("data: ")) continue;
4344
+ const data = line.slice(6).trim();
4345
+ if (!data || data === "[DONE]") continue;
4346
+ try {
4347
+ onEvent(JSON.parse(data));
4348
+ } catch {
4293
4349
  }
4294
4350
  }
4295
4351
  }
4296
4352
  } finally {
4353
+ clearInterval(timeoutInterval);
4297
4354
  reader.releaseLock();
4298
4355
  }
4299
- if (!content) {
4300
- throw new ProviderError("No response content from Codex API", {
4301
- provider: this.id
4302
- });
4356
+ if (timeoutController.signal.aborted) {
4357
+ throw new Error(
4358
+ `Stream timeout: No response from Codex API for ${STREAM_TIMEOUT_MS / 1e3}s`
4359
+ );
4303
4360
  }
4304
- const stopReason = status === "completed" ? "end_turn" : status === "incomplete" ? "max_tokens" : "end_turn";
4305
- return {
4306
- id: responseId,
4307
- content,
4308
- stopReason,
4309
- model,
4310
- usage: {
4311
- inputTokens,
4312
- outputTokens
4313
- }
4314
- };
4315
4361
  }
4316
4362
  /**
4317
- * Send a chat message with tool use
4318
- * Note: Codex Responses API tool support is complex; for now we delegate to chat()
4319
- * and return empty toolCalls. Full tool support can be added later.
4363
+ * Send a chat message using Codex Responses API format
4364
+ */
4365
+ async chat(messages, options) {
4366
+ return withRetry(async () => {
4367
+ const model = options?.model ?? this.config.model ?? DEFAULT_MODEL3;
4368
+ const { input, instructions } = this.convertToResponsesInput(messages, options?.system);
4369
+ const body = this.buildRequestBody(model, input, instructions, {
4370
+ maxTokens: options?.maxTokens,
4371
+ temperature: options?.temperature
4372
+ });
4373
+ const response = await this.makeRequest(body);
4374
+ let content = "";
4375
+ let responseId = `codex-${Date.now()}`;
4376
+ let inputTokens = 0;
4377
+ let outputTokens = 0;
4378
+ let status = "completed";
4379
+ await this.readSSEStream(response, (event) => {
4380
+ if (event.id) responseId = event.id;
4381
+ if (event.type === "response.output_text.delta" && event.delta) {
4382
+ content += event.delta;
4383
+ } else if (event.type === "response.output_text.done" && event.text) {
4384
+ content = event.text;
4385
+ } else if (event.type === "response.completed" && event.response) {
4386
+ const resp = event.response;
4387
+ const usage = resp.usage;
4388
+ if (usage) {
4389
+ inputTokens = usage.input_tokens ?? 0;
4390
+ outputTokens = usage.output_tokens ?? 0;
4391
+ }
4392
+ status = resp.status ?? "completed";
4393
+ }
4394
+ });
4395
+ const stopReason = status === "completed" ? "end_turn" : status === "incomplete" ? "max_tokens" : "end_turn";
4396
+ return {
4397
+ id: responseId,
4398
+ content,
4399
+ stopReason,
4400
+ model,
4401
+ usage: { inputTokens, outputTokens }
4402
+ };
4403
+ }, this.retryConfig);
4404
+ }
4405
+ /**
4406
+ * Send a chat message with tool use via Responses API
4320
4407
  */
4321
4408
  async chatWithTools(messages, options) {
4322
- const response = await this.chat(messages, options);
4323
- return {
4324
- ...response,
4325
- toolCalls: []
4326
- // Tools not yet supported in Codex provider
4327
- };
4409
+ return withRetry(async () => {
4410
+ const model = options?.model ?? this.config.model ?? DEFAULT_MODEL3;
4411
+ const { input, instructions } = this.convertToResponsesInput(messages, options?.system);
4412
+ const body = this.buildRequestBody(model, input, instructions, {
4413
+ tools: options.tools,
4414
+ maxTokens: options?.maxTokens
4415
+ });
4416
+ const response = await this.makeRequest(body);
4417
+ let content = "";
4418
+ let responseId = `codex-${Date.now()}`;
4419
+ let inputTokens = 0;
4420
+ let outputTokens = 0;
4421
+ const toolCalls = [];
4422
+ const fnCallBuilders = /* @__PURE__ */ new Map();
4423
+ await this.readSSEStream(response, (event) => {
4424
+ if (event.id) responseId = event.id;
4425
+ switch (event.type) {
4426
+ case "response.output_text.delta":
4427
+ content += event.delta ?? "";
4428
+ break;
4429
+ case "response.output_text.done":
4430
+ content = event.text ?? content;
4431
+ break;
4432
+ case "response.output_item.added": {
4433
+ const item = event.item;
4434
+ if (item.type === "function_call") {
4435
+ const itemKey = item.id ?? item.call_id;
4436
+ fnCallBuilders.set(itemKey, {
4437
+ callId: item.call_id,
4438
+ name: item.name,
4439
+ arguments: ""
4440
+ });
4441
+ }
4442
+ break;
4443
+ }
4444
+ case "response.function_call_arguments.delta": {
4445
+ const builder = fnCallBuilders.get(event.item_id);
4446
+ if (builder) builder.arguments += event.delta ?? "";
4447
+ break;
4448
+ }
4449
+ case "response.function_call_arguments.done": {
4450
+ const builder = fnCallBuilders.get(event.item_id);
4451
+ if (builder) {
4452
+ toolCalls.push({
4453
+ id: builder.callId,
4454
+ name: builder.name,
4455
+ input: parseArguments(event.arguments)
4456
+ });
4457
+ fnCallBuilders.delete(event.item_id);
4458
+ }
4459
+ break;
4460
+ }
4461
+ case "response.completed": {
4462
+ const resp = event.response;
4463
+ const usage = resp.usage;
4464
+ if (usage) {
4465
+ inputTokens = usage.input_tokens ?? 0;
4466
+ outputTokens = usage.output_tokens ?? 0;
4467
+ }
4468
+ for (const [, builder] of fnCallBuilders) {
4469
+ toolCalls.push({
4470
+ id: builder.callId,
4471
+ name: builder.name,
4472
+ input: parseArguments(builder.arguments)
4473
+ });
4474
+ }
4475
+ fnCallBuilders.clear();
4476
+ break;
4477
+ }
4478
+ }
4479
+ });
4480
+ return {
4481
+ id: responseId,
4482
+ content,
4483
+ stopReason: toolCalls.length > 0 ? "tool_use" : "end_turn",
4484
+ model,
4485
+ usage: { inputTokens, outputTokens },
4486
+ toolCalls
4487
+ };
4488
+ }, this.retryConfig);
4328
4489
  }
4329
4490
  /**
4330
- * Stream a chat response
4331
- * Note: True streaming with Codex Responses API is complex.
4332
- * For now, we make a non-streaming call and simulate streaming by emitting chunks.
4491
+ * Stream a chat response (no tools)
4333
4492
  */
4334
4493
  async *stream(messages, options) {
4335
- const response = await this.chat(messages, options);
4336
- if (response.content) {
4337
- const content = response.content;
4338
- const chunkSize = 20;
4339
- for (let i = 0; i < content.length; i += chunkSize) {
4340
- const chunk = content.slice(i, i + chunkSize);
4341
- yield { type: "text", text: chunk };
4342
- if (i + chunkSize < content.length) {
4343
- await new Promise((resolve4) => setTimeout(resolve4, 5));
4494
+ const model = options?.model ?? this.config.model ?? DEFAULT_MODEL3;
4495
+ const { input, instructions } = this.convertToResponsesInput(messages, options?.system);
4496
+ const body = this.buildRequestBody(model, input, instructions, {
4497
+ maxTokens: options?.maxTokens
4498
+ });
4499
+ const response = await this.makeRequest(body);
4500
+ if (!response.body) {
4501
+ throw new ProviderError("No response body from Codex API", { provider: this.id });
4502
+ }
4503
+ const reader = response.body.getReader();
4504
+ const decoder = new TextDecoder();
4505
+ let buffer = "";
4506
+ let lastActivityTime = Date.now();
4507
+ const timeoutController = new AbortController();
4508
+ const timeoutInterval = setInterval(() => {
4509
+ if (Date.now() - lastActivityTime > STREAM_TIMEOUT_MS) {
4510
+ clearInterval(timeoutInterval);
4511
+ timeoutController.abort();
4512
+ }
4513
+ }, 5e3);
4514
+ try {
4515
+ while (true) {
4516
+ if (timeoutController.signal.aborted) break;
4517
+ const { done, value } = await reader.read();
4518
+ if (done) break;
4519
+ lastActivityTime = Date.now();
4520
+ buffer += decoder.decode(value, { stream: true });
4521
+ const lines = buffer.split("\n");
4522
+ buffer = lines.pop() ?? "";
4523
+ for (const line of lines) {
4524
+ if (!line.startsWith("data: ")) continue;
4525
+ const data = line.slice(6).trim();
4526
+ if (!data || data === "[DONE]") continue;
4527
+ try {
4528
+ const event = JSON.parse(data);
4529
+ if (event.type === "response.output_text.delta" && event.delta) {
4530
+ yield { type: "text", text: event.delta };
4531
+ } else if (event.type === "response.completed") {
4532
+ yield { type: "done", stopReason: "end_turn" };
4533
+ }
4534
+ } catch {
4535
+ }
4344
4536
  }
4345
4537
  }
4538
+ } finally {
4539
+ clearInterval(timeoutInterval);
4540
+ reader.releaseLock();
4541
+ }
4542
+ if (timeoutController.signal.aborted) {
4543
+ throw new Error(
4544
+ `Stream timeout: No response from Codex API for ${STREAM_TIMEOUT_MS / 1e3}s`
4545
+ );
4346
4546
  }
4347
- yield { type: "done", stopReason: response.stopReason };
4348
4547
  }
4349
4548
  /**
4350
- * Stream a chat response with tool use
4351
- * Note: Tools and true streaming with Codex Responses API are not yet implemented.
4352
- * For now, we delegate to stream() which uses non-streaming under the hood.
4549
+ * Stream a chat response with tool use via Responses API.
4550
+ *
4551
+ * IMPORTANT: fnCallBuilders is keyed by output item ID (item.id), NOT by
4552
+ * call_id. The streaming events (function_call_arguments.delta/done) use
4553
+ * item_id which references the output item's id field, not call_id.
4353
4554
  */
4354
4555
  async *streamWithTools(messages, options) {
4355
- yield* this.stream(messages, options);
4556
+ const model = options?.model ?? this.config.model ?? DEFAULT_MODEL3;
4557
+ const { input, instructions } = this.convertToResponsesInput(messages, options?.system);
4558
+ const body = this.buildRequestBody(model, input, instructions, {
4559
+ tools: options.tools,
4560
+ maxTokens: options?.maxTokens
4561
+ });
4562
+ const response = await this.makeRequest(body);
4563
+ if (!response.body) {
4564
+ throw new ProviderError("No response body from Codex API", { provider: this.id });
4565
+ }
4566
+ const reader = response.body.getReader();
4567
+ const decoder = new TextDecoder();
4568
+ let buffer = "";
4569
+ const fnCallBuilders = /* @__PURE__ */ new Map();
4570
+ let lastActivityTime = Date.now();
4571
+ const timeoutController = new AbortController();
4572
+ const timeoutInterval = setInterval(() => {
4573
+ if (Date.now() - lastActivityTime > STREAM_TIMEOUT_MS) {
4574
+ clearInterval(timeoutInterval);
4575
+ timeoutController.abort();
4576
+ }
4577
+ }, 5e3);
4578
+ try {
4579
+ while (true) {
4580
+ if (timeoutController.signal.aborted) break;
4581
+ const { done, value } = await reader.read();
4582
+ if (done) break;
4583
+ lastActivityTime = Date.now();
4584
+ buffer += decoder.decode(value, { stream: true });
4585
+ const lines = buffer.split("\n");
4586
+ buffer = lines.pop() ?? "";
4587
+ for (const line of lines) {
4588
+ if (!line.startsWith("data: ")) continue;
4589
+ const data = line.slice(6).trim();
4590
+ if (!data || data === "[DONE]") continue;
4591
+ let event;
4592
+ try {
4593
+ event = JSON.parse(data);
4594
+ } catch {
4595
+ continue;
4596
+ }
4597
+ switch (event.type) {
4598
+ case "response.output_text.delta":
4599
+ yield { type: "text", text: event.delta ?? "" };
4600
+ break;
4601
+ case "response.output_item.added": {
4602
+ const item = event.item;
4603
+ if (item.type === "function_call") {
4604
+ const itemKey = item.id ?? item.call_id;
4605
+ fnCallBuilders.set(itemKey, {
4606
+ callId: item.call_id,
4607
+ name: item.name,
4608
+ arguments: ""
4609
+ });
4610
+ yield {
4611
+ type: "tool_use_start",
4612
+ toolCall: { id: item.call_id, name: item.name }
4613
+ };
4614
+ }
4615
+ break;
4616
+ }
4617
+ case "response.function_call_arguments.delta": {
4618
+ const builder = fnCallBuilders.get(event.item_id);
4619
+ if (builder) {
4620
+ builder.arguments += event.delta ?? "";
4621
+ }
4622
+ break;
4623
+ }
4624
+ case "response.function_call_arguments.done": {
4625
+ const builder = fnCallBuilders.get(event.item_id);
4626
+ if (builder) {
4627
+ yield {
4628
+ type: "tool_use_end",
4629
+ toolCall: {
4630
+ id: builder.callId,
4631
+ name: builder.name,
4632
+ input: parseArguments(event.arguments ?? builder.arguments)
4633
+ }
4634
+ };
4635
+ fnCallBuilders.delete(event.item_id);
4636
+ }
4637
+ break;
4638
+ }
4639
+ case "response.completed": {
4640
+ for (const [, builder] of fnCallBuilders) {
4641
+ yield {
4642
+ type: "tool_use_end",
4643
+ toolCall: {
4644
+ id: builder.callId,
4645
+ name: builder.name,
4646
+ input: parseArguments(builder.arguments)
4647
+ }
4648
+ };
4649
+ }
4650
+ fnCallBuilders.clear();
4651
+ const resp = event.response;
4652
+ const output = resp?.output ?? [];
4653
+ const hasToolCalls = output.some((i) => i.type === "function_call");
4654
+ yield {
4655
+ type: "done",
4656
+ stopReason: hasToolCalls ? "tool_use" : "end_turn"
4657
+ };
4658
+ break;
4659
+ }
4660
+ }
4661
+ }
4662
+ }
4663
+ } finally {
4664
+ clearInterval(timeoutInterval);
4665
+ reader.releaseLock();
4666
+ }
4667
+ if (timeoutController.signal.aborted) {
4668
+ throw new Error(
4669
+ `Stream timeout: No response from Codex API for ${STREAM_TIMEOUT_MS / 1e3}s`
4670
+ );
4671
+ }
4356
4672
  }
4357
4673
  };
4358
4674
  }