@corbat-tech/coco 2.12.0 → 2.13.1

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
@@ -1397,6 +1397,15 @@ var init_anthropic = __esm({
1397
1397
  function needsResponsesApi(model) {
1398
1398
  return model.includes("codex") || model.startsWith("gpt-5") || model.startsWith("o4-") || model.startsWith("o3-");
1399
1399
  }
1400
+ function needsMaxCompletionTokens(model) {
1401
+ return model.startsWith("o1") || model.startsWith("o3") || model.startsWith("o4") || model.startsWith("gpt-4o") || model.startsWith("gpt-4.1") || model.startsWith("gpt-5") || model.startsWith("chatgpt-4o");
1402
+ }
1403
+ function buildMaxTokensParam(model, maxTokens) {
1404
+ if (needsMaxCompletionTokens(model)) {
1405
+ return { max_completion_tokens: maxTokens };
1406
+ }
1407
+ return { max_tokens: maxTokens };
1408
+ }
1400
1409
  function createOpenAIProvider(config) {
1401
1410
  const provider = new OpenAIProvider();
1402
1411
  if (config) {
@@ -1605,9 +1614,10 @@ var init_openai = __esm({
1605
1614
  return withRetry(async () => {
1606
1615
  try {
1607
1616
  const supportsTemp = this.supportsTemperature(model);
1617
+ const maxTokens = options?.maxTokens ?? this.config.maxTokens ?? 8192;
1608
1618
  const response = await this.client.chat.completions.create({
1609
1619
  model,
1610
- max_tokens: options?.maxTokens ?? this.config.maxTokens ?? 8192,
1620
+ ...buildMaxTokensParam(model, maxTokens),
1611
1621
  messages: this.convertMessages(messages, options?.system),
1612
1622
  stop: options?.stopSequences,
1613
1623
  ...supportsTemp && {
@@ -1643,9 +1653,10 @@ var init_openai = __esm({
1643
1653
  try {
1644
1654
  const supportsTemp = this.supportsTemperature(model);
1645
1655
  const extraBody = this.getExtraBody(model);
1656
+ const maxTokens = options?.maxTokens ?? this.config.maxTokens ?? 8192;
1646
1657
  const requestParams = {
1647
1658
  model,
1648
- max_tokens: options?.maxTokens ?? this.config.maxTokens ?? 8192,
1659
+ ...buildMaxTokensParam(model, maxTokens),
1649
1660
  messages: this.convertMessages(messages, options?.system),
1650
1661
  tools: this.convertTools(options.tools),
1651
1662
  tool_choice: this.convertToolChoice(options.toolChoice)
@@ -1689,9 +1700,10 @@ var init_openai = __esm({
1689
1700
  }
1690
1701
  try {
1691
1702
  const supportsTemp = this.supportsTemperature(model);
1703
+ const maxTokens = options?.maxTokens ?? this.config.maxTokens ?? 8192;
1692
1704
  const stream = await this.client.chat.completions.create({
1693
1705
  model,
1694
- max_tokens: options?.maxTokens ?? this.config.maxTokens ?? 8192,
1706
+ ...buildMaxTokensParam(model, maxTokens),
1695
1707
  messages: this.convertMessages(messages, options?.system),
1696
1708
  stream: true,
1697
1709
  ...supportsTemp && { temperature: options?.temperature ?? this.config.temperature ?? 0 }
@@ -1725,9 +1737,10 @@ var init_openai = __esm({
1725
1737
  try {
1726
1738
  const supportsTemp = this.supportsTemperature(model);
1727
1739
  const extraBody = this.getExtraBody(model);
1740
+ const maxTokens = options?.maxTokens ?? this.config.maxTokens ?? 8192;
1728
1741
  const requestParams = {
1729
1742
  model,
1730
- max_tokens: options?.maxTokens ?? this.config.maxTokens ?? 8192,
1743
+ ...buildMaxTokensParam(model, maxTokens),
1731
1744
  messages: this.convertMessages(messages, options?.system),
1732
1745
  tools: this.convertTools(options.tools),
1733
1746
  tool_choice: this.convertToolChoice(options.toolChoice),
@@ -1756,7 +1769,7 @@ var init_openai = __esm({
1756
1769
  once: true
1757
1770
  });
1758
1771
  const providerName = this.name;
1759
- const parseArguments = (builder) => {
1772
+ const parseArguments2 = (builder) => {
1760
1773
  let input = {};
1761
1774
  try {
1762
1775
  input = builder.arguments ? JSON.parse(builder.arguments) : {};
@@ -1837,7 +1850,7 @@ var init_openai = __esm({
1837
1850
  toolCall: {
1838
1851
  id: builder.id,
1839
1852
  name: builder.name,
1840
- input: parseArguments(builder)
1853
+ input: parseArguments2(builder)
1841
1854
  }
1842
1855
  };
1843
1856
  }
@@ -1850,7 +1863,7 @@ var init_openai = __esm({
1850
1863
  toolCall: {
1851
1864
  id: builder.id,
1852
1865
  name: builder.name,
1853
- input: parseArguments(builder)
1866
+ input: parseArguments2(builder)
1854
1867
  }
1855
1868
  };
1856
1869
  }
@@ -1989,11 +2002,20 @@ var init_openai = __esm({
1989
2002
  } catch {
1990
2003
  try {
1991
2004
  const model = this.config.model || DEFAULT_MODEL2;
1992
- await this.client.chat.completions.create({
1993
- model,
1994
- messages: [{ role: "user", content: "Hi" }],
1995
- max_tokens: 1
1996
- });
2005
+ if (needsResponsesApi(model)) {
2006
+ await this.client.responses.create({
2007
+ model,
2008
+ input: [{ role: "user", content: [{ type: "input_text", text: "Hi" }] }],
2009
+ max_output_tokens: 1,
2010
+ store: false
2011
+ });
2012
+ } else {
2013
+ await this.client.chat.completions.create({
2014
+ model,
2015
+ messages: [{ role: "user", content: "Hi" }],
2016
+ ...buildMaxTokensParam(model, 1)
2017
+ });
2018
+ }
1997
2019
  return true;
1998
2020
  } catch {
1999
2021
  return false;
@@ -4061,8 +4083,6 @@ var init_auth = __esm({
4061
4083
  init_gcloud();
4062
4084
  }
4063
4085
  });
4064
-
4065
- // src/providers/codex.ts
4066
4086
  function parseJwtClaims(token) {
4067
4087
  const parts = token.split(".");
4068
4088
  if (parts.length !== 3 || !parts[1]) return void 0;
@@ -4078,6 +4098,21 @@ function extractAccountId(accessToken) {
4078
4098
  const auth = claims["https://api.openai.com/auth"];
4079
4099
  return claims["chatgpt_account_id"] || auth?.["chatgpt_account_id"] || claims["organizations"]?.[0]?.id;
4080
4100
  }
4101
+ function parseArguments(args) {
4102
+ try {
4103
+ return args ? JSON.parse(args) : {};
4104
+ } catch {
4105
+ try {
4106
+ if (args) {
4107
+ const repaired = jsonrepair(args);
4108
+ return JSON.parse(repaired);
4109
+ }
4110
+ } catch {
4111
+ console.error(`[Codex] Cannot parse tool arguments: ${args.slice(0, 200)}`);
4112
+ }
4113
+ return {};
4114
+ }
4115
+ }
4081
4116
  function createCodexProvider(config) {
4082
4117
  const provider = new CodexProvider();
4083
4118
  if (config) {
@@ -4086,11 +4121,12 @@ function createCodexProvider(config) {
4086
4121
  }
4087
4122
  return provider;
4088
4123
  }
4089
- var CODEX_API_ENDPOINT, DEFAULT_MODEL3, CONTEXT_WINDOWS3, CodexProvider;
4124
+ var CODEX_API_ENDPOINT, DEFAULT_MODEL3, CONTEXT_WINDOWS3, STREAM_TIMEOUT_MS, CodexProvider;
4090
4125
  var init_codex = __esm({
4091
4126
  "src/providers/codex.ts"() {
4092
4127
  init_errors();
4093
4128
  init_auth();
4129
+ init_retry();
4094
4130
  CODEX_API_ENDPOINT = "https://chatgpt.com/backend-api/codex/responses";
4095
4131
  DEFAULT_MODEL3 = "gpt-5.4-codex";
4096
4132
  CONTEXT_WINDOWS3 = {
@@ -4103,12 +4139,14 @@ var init_codex = __esm({
4103
4139
  "gpt-5.2": 2e5,
4104
4140
  "gpt-5.1": 2e5
4105
4141
  };
4142
+ STREAM_TIMEOUT_MS = 12e4;
4106
4143
  CodexProvider = class {
4107
4144
  id = "codex";
4108
4145
  name = "OpenAI Codex (ChatGPT Plus/Pro)";
4109
4146
  config = {};
4110
4147
  accessToken = null;
4111
4148
  accountId;
4149
+ retryConfig = DEFAULT_RETRY_CONFIG;
4112
4150
  /**
4113
4151
  * Initialize the provider with OAuth tokens
4114
4152
  */
@@ -4193,166 +4231,466 @@ var init_codex = __esm({
4193
4231
  /**
4194
4232
  * Extract text content from a message
4195
4233
  */
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");
4234
+ contentToString(content) {
4235
+ if (typeof content === "string") return content;
4236
+ if (Array.isArray(content)) {
4237
+ return content.filter((part) => part.type === "text").map((part) => part.text).join("\n");
4206
4238
  }
4207
4239
  return "";
4208
4240
  }
4209
4241
  /**
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
- * }
4242
+ * Convert messages to Responses API input format.
4218
4243
  *
4219
- * IMPORTANT: User/developer messages use "input_text", assistant messages use "output_text"
4244
+ * Handles:
4245
+ * - system messages → extracted as instructions
4246
+ * - user text messages → { role: "user", content: "..." }
4247
+ * - user tool_result messages → function_call_output items
4248
+ * - assistant text → { role: "assistant", content: "..." }
4249
+ * - assistant tool_use → function_call items
4220
4250
  */
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
- });
4251
+ convertToResponsesInput(messages, systemPrompt) {
4252
+ const input = [];
4253
+ let instructions = systemPrompt ?? null;
4254
+ for (const msg of messages) {
4255
+ if (msg.role === "system") {
4256
+ instructions = (instructions ? instructions + "\n\n" : "") + this.contentToString(msg.content);
4257
+ } else if (msg.role === "user") {
4258
+ if (Array.isArray(msg.content) && msg.content.some((b) => b.type === "tool_result")) {
4259
+ for (const block of msg.content) {
4260
+ if (block.type === "tool_result") {
4261
+ const tr = block;
4262
+ input.push({
4263
+ type: "function_call_output",
4264
+ call_id: tr.tool_use_id,
4265
+ output: tr.content
4266
+ });
4267
+ }
4268
+ }
4269
+ } else {
4270
+ input.push({
4271
+ role: "user",
4272
+ content: this.contentToString(msg.content)
4273
+ });
4274
+ }
4275
+ } else if (msg.role === "assistant") {
4276
+ if (typeof msg.content === "string") {
4277
+ input.push({ role: "assistant", content: msg.content });
4278
+ } else if (Array.isArray(msg.content)) {
4279
+ const textParts = [];
4280
+ for (const block of msg.content) {
4281
+ if (block.type === "text") {
4282
+ textParts.push(block.text);
4283
+ } else if (block.type === "tool_use") {
4284
+ if (textParts.length > 0) {
4285
+ input.push({ role: "assistant", content: textParts.join("") });
4286
+ textParts.length = 0;
4287
+ }
4288
+ const tu = block;
4289
+ input.push({
4290
+ type: "function_call",
4291
+ call_id: tu.id,
4292
+ name: tu.name,
4293
+ arguments: JSON.stringify(tu.input)
4294
+ });
4295
+ }
4296
+ }
4297
+ if (textParts.length > 0) {
4298
+ input.push({ role: "assistant", content: textParts.join("") });
4299
+ }
4300
+ }
4301
+ }
4302
+ }
4303
+ return { input, instructions };
4232
4304
  }
4233
4305
  /**
4234
- * Send a chat message using Codex Responses API format
4306
+ * Convert tool definitions to Responses API function tool format
4235
4307
  */
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]);
4308
+ convertTools(tools) {
4309
+ return tools.map((tool) => ({
4310
+ type: "function",
4311
+ name: tool.name,
4312
+ description: tool.description ?? void 0,
4313
+ parameters: tool.input_schema ?? null,
4314
+ strict: false
4315
+ }));
4316
+ }
4317
+ /**
4318
+ * Build the request body for the Codex Responses API
4319
+ */
4320
+ buildRequestBody(model, input, instructions, options) {
4241
4321
  const body = {
4242
4322
  model,
4243
- instructions,
4244
- input: inputMessages,
4245
- tools: [],
4323
+ input,
4324
+ instructions: instructions ?? "You are a helpful coding assistant.",
4325
+ max_output_tokens: options?.maxTokens ?? this.config.maxTokens ?? 8192,
4326
+ temperature: options?.temperature ?? this.config.temperature ?? 0,
4246
4327
  store: false,
4247
4328
  stream: true
4248
4329
  // Codex API requires streaming
4249
4330
  };
4250
- const response = await this.makeRequest(body);
4331
+ if (options?.tools && options.tools.length > 0) {
4332
+ body.tools = this.convertTools(options.tools);
4333
+ }
4334
+ return body;
4335
+ }
4336
+ /**
4337
+ * Read SSE stream and call handler for each parsed event.
4338
+ * Returns when stream ends.
4339
+ */
4340
+ async readSSEStream(response, onEvent) {
4251
4341
  if (!response.body) {
4252
- throw new ProviderError("No response body from Codex API", {
4253
- provider: this.id
4254
- });
4342
+ throw new ProviderError("No response body from Codex API", { provider: this.id });
4255
4343
  }
4256
4344
  const reader = response.body.getReader();
4257
4345
  const decoder = new TextDecoder();
4258
4346
  let buffer = "";
4259
- let content = "";
4260
- let responseId = `codex-${Date.now()}`;
4261
- let inputTokens = 0;
4262
- let outputTokens = 0;
4263
- let status = "completed";
4347
+ let lastActivityTime = Date.now();
4348
+ const timeoutController = new AbortController();
4349
+ const timeoutInterval = setInterval(() => {
4350
+ if (Date.now() - lastActivityTime > STREAM_TIMEOUT_MS) {
4351
+ clearInterval(timeoutInterval);
4352
+ timeoutController.abort();
4353
+ }
4354
+ }, 5e3);
4264
4355
  try {
4265
4356
  while (true) {
4357
+ if (timeoutController.signal.aborted) break;
4266
4358
  const { done, value } = await reader.read();
4267
4359
  if (done) break;
4360
+ lastActivityTime = Date.now();
4268
4361
  buffer += decoder.decode(value, { stream: true });
4269
4362
  const lines = buffer.split("\n");
4270
4363
  buffer = lines.pop() ?? "";
4271
4364
  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
- }
4365
+ if (!line.startsWith("data: ")) continue;
4366
+ const data = line.slice(6).trim();
4367
+ if (!data || data === "[DONE]") continue;
4368
+ try {
4369
+ onEvent(JSON.parse(data));
4370
+ } catch {
4293
4371
  }
4294
4372
  }
4295
4373
  }
4296
4374
  } finally {
4375
+ clearInterval(timeoutInterval);
4297
4376
  reader.releaseLock();
4298
4377
  }
4299
- if (!content) {
4300
- throw new ProviderError("No response content from Codex API", {
4301
- provider: this.id
4302
- });
4378
+ if (timeoutController.signal.aborted) {
4379
+ throw new Error(
4380
+ `Stream timeout: No response from Codex API for ${STREAM_TIMEOUT_MS / 1e3}s`
4381
+ );
4303
4382
  }
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
4383
  }
4316
4384
  /**
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.
4385
+ * Send a chat message using Codex Responses API format
4386
+ */
4387
+ async chat(messages, options) {
4388
+ return withRetry(async () => {
4389
+ const model = options?.model ?? this.config.model ?? DEFAULT_MODEL3;
4390
+ const { input, instructions } = this.convertToResponsesInput(messages, options?.system);
4391
+ const body = this.buildRequestBody(model, input, instructions, {
4392
+ maxTokens: options?.maxTokens,
4393
+ temperature: options?.temperature
4394
+ });
4395
+ const response = await this.makeRequest(body);
4396
+ let content = "";
4397
+ let responseId = `codex-${Date.now()}`;
4398
+ let inputTokens = 0;
4399
+ let outputTokens = 0;
4400
+ let status = "completed";
4401
+ await this.readSSEStream(response, (event) => {
4402
+ if (event.id) responseId = event.id;
4403
+ if (event.type === "response.output_text.delta" && event.delta) {
4404
+ content += event.delta;
4405
+ } else if (event.type === "response.output_text.done" && event.text) {
4406
+ content = event.text;
4407
+ } else if (event.type === "response.completed" && event.response) {
4408
+ const resp = event.response;
4409
+ const usage = resp.usage;
4410
+ if (usage) {
4411
+ inputTokens = usage.input_tokens ?? 0;
4412
+ outputTokens = usage.output_tokens ?? 0;
4413
+ }
4414
+ status = resp.status ?? "completed";
4415
+ }
4416
+ });
4417
+ const stopReason = status === "completed" ? "end_turn" : status === "incomplete" ? "max_tokens" : "end_turn";
4418
+ return {
4419
+ id: responseId,
4420
+ content,
4421
+ stopReason,
4422
+ model,
4423
+ usage: { inputTokens, outputTokens }
4424
+ };
4425
+ }, this.retryConfig);
4426
+ }
4427
+ /**
4428
+ * Send a chat message with tool use via Responses API
4320
4429
  */
4321
4430
  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
- };
4431
+ return withRetry(async () => {
4432
+ const model = options?.model ?? this.config.model ?? DEFAULT_MODEL3;
4433
+ const { input, instructions } = this.convertToResponsesInput(messages, options?.system);
4434
+ const body = this.buildRequestBody(model, input, instructions, {
4435
+ tools: options.tools,
4436
+ maxTokens: options?.maxTokens
4437
+ });
4438
+ const response = await this.makeRequest(body);
4439
+ let content = "";
4440
+ let responseId = `codex-${Date.now()}`;
4441
+ let inputTokens = 0;
4442
+ let outputTokens = 0;
4443
+ const toolCalls = [];
4444
+ const fnCallBuilders = /* @__PURE__ */ new Map();
4445
+ await this.readSSEStream(response, (event) => {
4446
+ if (event.id) responseId = event.id;
4447
+ switch (event.type) {
4448
+ case "response.output_text.delta":
4449
+ content += event.delta ?? "";
4450
+ break;
4451
+ case "response.output_text.done":
4452
+ content = event.text ?? content;
4453
+ break;
4454
+ case "response.output_item.added": {
4455
+ const item = event.item;
4456
+ if (item.type === "function_call") {
4457
+ const itemKey = item.id ?? item.call_id;
4458
+ fnCallBuilders.set(itemKey, {
4459
+ callId: item.call_id,
4460
+ name: item.name,
4461
+ arguments: ""
4462
+ });
4463
+ }
4464
+ break;
4465
+ }
4466
+ case "response.function_call_arguments.delta": {
4467
+ const builder = fnCallBuilders.get(event.item_id);
4468
+ if (builder) builder.arguments += event.delta ?? "";
4469
+ break;
4470
+ }
4471
+ case "response.function_call_arguments.done": {
4472
+ const builder = fnCallBuilders.get(event.item_id);
4473
+ if (builder) {
4474
+ toolCalls.push({
4475
+ id: builder.callId,
4476
+ name: builder.name,
4477
+ input: parseArguments(event.arguments)
4478
+ });
4479
+ fnCallBuilders.delete(event.item_id);
4480
+ }
4481
+ break;
4482
+ }
4483
+ case "response.completed": {
4484
+ const resp = event.response;
4485
+ const usage = resp.usage;
4486
+ if (usage) {
4487
+ inputTokens = usage.input_tokens ?? 0;
4488
+ outputTokens = usage.output_tokens ?? 0;
4489
+ }
4490
+ for (const [, builder] of fnCallBuilders) {
4491
+ toolCalls.push({
4492
+ id: builder.callId,
4493
+ name: builder.name,
4494
+ input: parseArguments(builder.arguments)
4495
+ });
4496
+ }
4497
+ fnCallBuilders.clear();
4498
+ break;
4499
+ }
4500
+ }
4501
+ });
4502
+ return {
4503
+ id: responseId,
4504
+ content,
4505
+ stopReason: toolCalls.length > 0 ? "tool_use" : "end_turn",
4506
+ model,
4507
+ usage: { inputTokens, outputTokens },
4508
+ toolCalls
4509
+ };
4510
+ }, this.retryConfig);
4328
4511
  }
4329
4512
  /**
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.
4513
+ * Stream a chat response (no tools)
4333
4514
  */
4334
4515
  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));
4516
+ const model = options?.model ?? this.config.model ?? DEFAULT_MODEL3;
4517
+ const { input, instructions } = this.convertToResponsesInput(messages, options?.system);
4518
+ const body = this.buildRequestBody(model, input, instructions, {
4519
+ maxTokens: options?.maxTokens
4520
+ });
4521
+ const response = await this.makeRequest(body);
4522
+ if (!response.body) {
4523
+ throw new ProviderError("No response body from Codex API", { provider: this.id });
4524
+ }
4525
+ const reader = response.body.getReader();
4526
+ const decoder = new TextDecoder();
4527
+ let buffer = "";
4528
+ let lastActivityTime = Date.now();
4529
+ const timeoutController = new AbortController();
4530
+ const timeoutInterval = setInterval(() => {
4531
+ if (Date.now() - lastActivityTime > STREAM_TIMEOUT_MS) {
4532
+ clearInterval(timeoutInterval);
4533
+ timeoutController.abort();
4534
+ }
4535
+ }, 5e3);
4536
+ try {
4537
+ while (true) {
4538
+ if (timeoutController.signal.aborted) break;
4539
+ const { done, value } = await reader.read();
4540
+ if (done) break;
4541
+ lastActivityTime = Date.now();
4542
+ buffer += decoder.decode(value, { stream: true });
4543
+ const lines = buffer.split("\n");
4544
+ buffer = lines.pop() ?? "";
4545
+ for (const line of lines) {
4546
+ if (!line.startsWith("data: ")) continue;
4547
+ const data = line.slice(6).trim();
4548
+ if (!data || data === "[DONE]") continue;
4549
+ try {
4550
+ const event = JSON.parse(data);
4551
+ if (event.type === "response.output_text.delta" && event.delta) {
4552
+ yield { type: "text", text: event.delta };
4553
+ } else if (event.type === "response.completed") {
4554
+ yield { type: "done", stopReason: "end_turn" };
4555
+ }
4556
+ } catch {
4557
+ }
4344
4558
  }
4345
4559
  }
4560
+ } finally {
4561
+ clearInterval(timeoutInterval);
4562
+ reader.releaseLock();
4563
+ }
4564
+ if (timeoutController.signal.aborted) {
4565
+ throw new Error(
4566
+ `Stream timeout: No response from Codex API for ${STREAM_TIMEOUT_MS / 1e3}s`
4567
+ );
4346
4568
  }
4347
- yield { type: "done", stopReason: response.stopReason };
4348
4569
  }
4349
4570
  /**
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.
4571
+ * Stream a chat response with tool use via Responses API.
4572
+ *
4573
+ * IMPORTANT: fnCallBuilders is keyed by output item ID (item.id), NOT by
4574
+ * call_id. The streaming events (function_call_arguments.delta/done) use
4575
+ * item_id which references the output item's id field, not call_id.
4353
4576
  */
4354
4577
  async *streamWithTools(messages, options) {
4355
- yield* this.stream(messages, options);
4578
+ const model = options?.model ?? this.config.model ?? DEFAULT_MODEL3;
4579
+ const { input, instructions } = this.convertToResponsesInput(messages, options?.system);
4580
+ const body = this.buildRequestBody(model, input, instructions, {
4581
+ tools: options.tools,
4582
+ maxTokens: options?.maxTokens
4583
+ });
4584
+ const response = await this.makeRequest(body);
4585
+ if (!response.body) {
4586
+ throw new ProviderError("No response body from Codex API", { provider: this.id });
4587
+ }
4588
+ const reader = response.body.getReader();
4589
+ const decoder = new TextDecoder();
4590
+ let buffer = "";
4591
+ const fnCallBuilders = /* @__PURE__ */ new Map();
4592
+ let lastActivityTime = Date.now();
4593
+ const timeoutController = new AbortController();
4594
+ const timeoutInterval = setInterval(() => {
4595
+ if (Date.now() - lastActivityTime > STREAM_TIMEOUT_MS) {
4596
+ clearInterval(timeoutInterval);
4597
+ timeoutController.abort();
4598
+ }
4599
+ }, 5e3);
4600
+ try {
4601
+ while (true) {
4602
+ if (timeoutController.signal.aborted) break;
4603
+ const { done, value } = await reader.read();
4604
+ if (done) break;
4605
+ lastActivityTime = Date.now();
4606
+ buffer += decoder.decode(value, { stream: true });
4607
+ const lines = buffer.split("\n");
4608
+ buffer = lines.pop() ?? "";
4609
+ for (const line of lines) {
4610
+ if (!line.startsWith("data: ")) continue;
4611
+ const data = line.slice(6).trim();
4612
+ if (!data || data === "[DONE]") continue;
4613
+ let event;
4614
+ try {
4615
+ event = JSON.parse(data);
4616
+ } catch {
4617
+ continue;
4618
+ }
4619
+ switch (event.type) {
4620
+ case "response.output_text.delta":
4621
+ yield { type: "text", text: event.delta ?? "" };
4622
+ break;
4623
+ case "response.output_item.added": {
4624
+ const item = event.item;
4625
+ if (item.type === "function_call") {
4626
+ const itemKey = item.id ?? item.call_id;
4627
+ fnCallBuilders.set(itemKey, {
4628
+ callId: item.call_id,
4629
+ name: item.name,
4630
+ arguments: ""
4631
+ });
4632
+ yield {
4633
+ type: "tool_use_start",
4634
+ toolCall: { id: item.call_id, name: item.name }
4635
+ };
4636
+ }
4637
+ break;
4638
+ }
4639
+ case "response.function_call_arguments.delta": {
4640
+ const builder = fnCallBuilders.get(event.item_id);
4641
+ if (builder) {
4642
+ builder.arguments += event.delta ?? "";
4643
+ }
4644
+ break;
4645
+ }
4646
+ case "response.function_call_arguments.done": {
4647
+ const builder = fnCallBuilders.get(event.item_id);
4648
+ if (builder) {
4649
+ yield {
4650
+ type: "tool_use_end",
4651
+ toolCall: {
4652
+ id: builder.callId,
4653
+ name: builder.name,
4654
+ input: parseArguments(event.arguments ?? builder.arguments)
4655
+ }
4656
+ };
4657
+ fnCallBuilders.delete(event.item_id);
4658
+ }
4659
+ break;
4660
+ }
4661
+ case "response.completed": {
4662
+ for (const [, builder] of fnCallBuilders) {
4663
+ yield {
4664
+ type: "tool_use_end",
4665
+ toolCall: {
4666
+ id: builder.callId,
4667
+ name: builder.name,
4668
+ input: parseArguments(builder.arguments)
4669
+ }
4670
+ };
4671
+ }
4672
+ fnCallBuilders.clear();
4673
+ const resp = event.response;
4674
+ const output = resp?.output ?? [];
4675
+ const hasToolCalls = output.some((i) => i.type === "function_call");
4676
+ yield {
4677
+ type: "done",
4678
+ stopReason: hasToolCalls ? "tool_use" : "end_turn"
4679
+ };
4680
+ break;
4681
+ }
4682
+ }
4683
+ }
4684
+ }
4685
+ } finally {
4686
+ clearInterval(timeoutInterval);
4687
+ reader.releaseLock();
4688
+ }
4689
+ if (timeoutController.signal.aborted) {
4690
+ throw new Error(
4691
+ `Stream timeout: No response from Codex API for ${STREAM_TIMEOUT_MS / 1e3}s`
4692
+ );
4693
+ }
4356
4694
  }
4357
4695
  };
4358
4696
  }