@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/index.js CHANGED
@@ -12300,6 +12300,15 @@ var MODELS_WITH_THINKING_MODE = ["kimi-k2.5", "kimi-k2-0324", "kimi-latest"];
12300
12300
  function needsResponsesApi(model) {
12301
12301
  return model.includes("codex") || model.startsWith("gpt-5") || model.startsWith("o4-") || model.startsWith("o3-");
12302
12302
  }
12303
+ function needsMaxCompletionTokens(model) {
12304
+ 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");
12305
+ }
12306
+ function buildMaxTokensParam(model, maxTokens) {
12307
+ if (needsMaxCompletionTokens(model)) {
12308
+ return { max_completion_tokens: maxTokens };
12309
+ }
12310
+ return { max_tokens: maxTokens };
12311
+ }
12303
12312
  var OpenAIProvider = class {
12304
12313
  id;
12305
12314
  name;
@@ -12376,9 +12385,10 @@ var OpenAIProvider = class {
12376
12385
  return withRetry(async () => {
12377
12386
  try {
12378
12387
  const supportsTemp = this.supportsTemperature(model);
12388
+ const maxTokens = options?.maxTokens ?? this.config.maxTokens ?? 8192;
12379
12389
  const response = await this.client.chat.completions.create({
12380
12390
  model,
12381
- max_tokens: options?.maxTokens ?? this.config.maxTokens ?? 8192,
12391
+ ...buildMaxTokensParam(model, maxTokens),
12382
12392
  messages: this.convertMessages(messages, options?.system),
12383
12393
  stop: options?.stopSequences,
12384
12394
  ...supportsTemp && {
@@ -12414,9 +12424,10 @@ var OpenAIProvider = class {
12414
12424
  try {
12415
12425
  const supportsTemp = this.supportsTemperature(model);
12416
12426
  const extraBody = this.getExtraBody(model);
12427
+ const maxTokens = options?.maxTokens ?? this.config.maxTokens ?? 8192;
12417
12428
  const requestParams = {
12418
12429
  model,
12419
- max_tokens: options?.maxTokens ?? this.config.maxTokens ?? 8192,
12430
+ ...buildMaxTokensParam(model, maxTokens),
12420
12431
  messages: this.convertMessages(messages, options?.system),
12421
12432
  tools: this.convertTools(options.tools),
12422
12433
  tool_choice: this.convertToolChoice(options.toolChoice)
@@ -12460,9 +12471,10 @@ var OpenAIProvider = class {
12460
12471
  }
12461
12472
  try {
12462
12473
  const supportsTemp = this.supportsTemperature(model);
12474
+ const maxTokens = options?.maxTokens ?? this.config.maxTokens ?? 8192;
12463
12475
  const stream = await this.client.chat.completions.create({
12464
12476
  model,
12465
- max_tokens: options?.maxTokens ?? this.config.maxTokens ?? 8192,
12477
+ ...buildMaxTokensParam(model, maxTokens),
12466
12478
  messages: this.convertMessages(messages, options?.system),
12467
12479
  stream: true,
12468
12480
  ...supportsTemp && { temperature: options?.temperature ?? this.config.temperature ?? 0 }
@@ -12496,9 +12508,10 @@ var OpenAIProvider = class {
12496
12508
  try {
12497
12509
  const supportsTemp = this.supportsTemperature(model);
12498
12510
  const extraBody = this.getExtraBody(model);
12511
+ const maxTokens = options?.maxTokens ?? this.config.maxTokens ?? 8192;
12499
12512
  const requestParams = {
12500
12513
  model,
12501
- max_tokens: options?.maxTokens ?? this.config.maxTokens ?? 8192,
12514
+ ...buildMaxTokensParam(model, maxTokens),
12502
12515
  messages: this.convertMessages(messages, options?.system),
12503
12516
  tools: this.convertTools(options.tools),
12504
12517
  tool_choice: this.convertToolChoice(options.toolChoice),
@@ -12527,7 +12540,7 @@ var OpenAIProvider = class {
12527
12540
  once: true
12528
12541
  });
12529
12542
  const providerName = this.name;
12530
- const parseArguments = (builder) => {
12543
+ const parseArguments2 = (builder) => {
12531
12544
  let input = {};
12532
12545
  try {
12533
12546
  input = builder.arguments ? JSON.parse(builder.arguments) : {};
@@ -12608,7 +12621,7 @@ var OpenAIProvider = class {
12608
12621
  toolCall: {
12609
12622
  id: builder.id,
12610
12623
  name: builder.name,
12611
- input: parseArguments(builder)
12624
+ input: parseArguments2(builder)
12612
12625
  }
12613
12626
  };
12614
12627
  }
@@ -12621,7 +12634,7 @@ var OpenAIProvider = class {
12621
12634
  toolCall: {
12622
12635
  id: builder.id,
12623
12636
  name: builder.name,
12624
- input: parseArguments(builder)
12637
+ input: parseArguments2(builder)
12625
12638
  }
12626
12639
  };
12627
12640
  }
@@ -12760,11 +12773,20 @@ var OpenAIProvider = class {
12760
12773
  } catch {
12761
12774
  try {
12762
12775
  const model = this.config.model || DEFAULT_MODEL2;
12763
- await this.client.chat.completions.create({
12764
- model,
12765
- messages: [{ role: "user", content: "Hi" }],
12766
- max_tokens: 1
12767
- });
12776
+ if (needsResponsesApi(model)) {
12777
+ await this.client.responses.create({
12778
+ model,
12779
+ input: [{ role: "user", content: [{ type: "input_text", text: "Hi" }] }],
12780
+ max_output_tokens: 1,
12781
+ store: false
12782
+ });
12783
+ } else {
12784
+ await this.client.chat.completions.create({
12785
+ model,
12786
+ messages: [{ role: "user", content: "Hi" }],
12787
+ ...buildMaxTokensParam(model, 1)
12788
+ });
12789
+ }
12768
12790
  return true;
12769
12791
  } catch {
12770
12792
  return false;
@@ -13623,6 +13645,7 @@ var CONTEXT_WINDOWS3 = {
13623
13645
  "gpt-5.2": 2e5,
13624
13646
  "gpt-5.1": 2e5
13625
13647
  };
13648
+ var STREAM_TIMEOUT_MS = 12e4;
13626
13649
  function parseJwtClaims(token) {
13627
13650
  const parts = token.split(".");
13628
13651
  if (parts.length !== 3 || !parts[1]) return void 0;
@@ -13638,12 +13661,28 @@ function extractAccountId(accessToken) {
13638
13661
  const auth = claims["https://api.openai.com/auth"];
13639
13662
  return claims["chatgpt_account_id"] || auth?.["chatgpt_account_id"] || claims["organizations"]?.[0]?.id;
13640
13663
  }
13664
+ function parseArguments(args) {
13665
+ try {
13666
+ return args ? JSON.parse(args) : {};
13667
+ } catch {
13668
+ try {
13669
+ if (args) {
13670
+ const repaired = jsonrepair(args);
13671
+ return JSON.parse(repaired);
13672
+ }
13673
+ } catch {
13674
+ console.error(`[Codex] Cannot parse tool arguments: ${args.slice(0, 200)}`);
13675
+ }
13676
+ return {};
13677
+ }
13678
+ }
13641
13679
  var CodexProvider = class {
13642
13680
  id = "codex";
13643
13681
  name = "OpenAI Codex (ChatGPT Plus/Pro)";
13644
13682
  config = {};
13645
13683
  accessToken = null;
13646
13684
  accountId;
13685
+ retryConfig = DEFAULT_RETRY_CONFIG;
13647
13686
  /**
13648
13687
  * Initialize the provider with OAuth tokens
13649
13688
  */
@@ -13728,166 +13767,466 @@ var CodexProvider = class {
13728
13767
  /**
13729
13768
  * Extract text content from a message
13730
13769
  */
13731
- extractTextContent(msg) {
13732
- if (typeof msg.content === "string") {
13733
- return msg.content;
13734
- }
13735
- if (Array.isArray(msg.content)) {
13736
- return msg.content.map((part) => {
13737
- if (part.type === "text") return part.text;
13738
- if (part.type === "tool_result") return `Tool result: ${JSON.stringify(part.content)}`;
13739
- return "";
13740
- }).join("\n");
13770
+ contentToString(content) {
13771
+ if (typeof content === "string") return content;
13772
+ if (Array.isArray(content)) {
13773
+ return content.filter((part) => part.type === "text").map((part) => part.text).join("\n");
13741
13774
  }
13742
13775
  return "";
13743
13776
  }
13744
13777
  /**
13745
- * Convert messages to Codex Responses API format
13746
- * Codex uses a different format than Chat Completions:
13747
- * {
13748
- * "input": [
13749
- * { "type": "message", "role": "developer|user", "content": [{ "type": "input_text", "text": "..." }] },
13750
- * { "type": "message", "role": "assistant", "content": [{ "type": "output_text", "text": "..." }] }
13751
- * ]
13752
- * }
13778
+ * Convert messages to Responses API input format.
13753
13779
  *
13754
- * IMPORTANT: User/developer messages use "input_text", assistant messages use "output_text"
13780
+ * Handles:
13781
+ * - system messages → extracted as instructions
13782
+ * - user text messages → { role: "user", content: "..." }
13783
+ * - user tool_result messages → function_call_output items
13784
+ * - assistant text → { role: "assistant", content: "..." }
13785
+ * - assistant tool_use → function_call items
13755
13786
  */
13756
- convertMessagesToResponsesFormat(messages) {
13757
- return messages.map((msg) => {
13758
- const text = this.extractTextContent(msg);
13759
- const role = msg.role === "system" ? "developer" : msg.role;
13760
- const contentType = msg.role === "assistant" ? "output_text" : "input_text";
13761
- return {
13762
- type: "message",
13763
- role,
13764
- content: [{ type: contentType, text }]
13765
- };
13766
- });
13787
+ convertToResponsesInput(messages, systemPrompt) {
13788
+ const input = [];
13789
+ let instructions = systemPrompt ?? null;
13790
+ for (const msg of messages) {
13791
+ if (msg.role === "system") {
13792
+ instructions = (instructions ? instructions + "\n\n" : "") + this.contentToString(msg.content);
13793
+ } else if (msg.role === "user") {
13794
+ if (Array.isArray(msg.content) && msg.content.some((b) => b.type === "tool_result")) {
13795
+ for (const block of msg.content) {
13796
+ if (block.type === "tool_result") {
13797
+ const tr = block;
13798
+ input.push({
13799
+ type: "function_call_output",
13800
+ call_id: tr.tool_use_id,
13801
+ output: tr.content
13802
+ });
13803
+ }
13804
+ }
13805
+ } else {
13806
+ input.push({
13807
+ role: "user",
13808
+ content: this.contentToString(msg.content)
13809
+ });
13810
+ }
13811
+ } else if (msg.role === "assistant") {
13812
+ if (typeof msg.content === "string") {
13813
+ input.push({ role: "assistant", content: msg.content });
13814
+ } else if (Array.isArray(msg.content)) {
13815
+ const textParts = [];
13816
+ for (const block of msg.content) {
13817
+ if (block.type === "text") {
13818
+ textParts.push(block.text);
13819
+ } else if (block.type === "tool_use") {
13820
+ if (textParts.length > 0) {
13821
+ input.push({ role: "assistant", content: textParts.join("") });
13822
+ textParts.length = 0;
13823
+ }
13824
+ const tu = block;
13825
+ input.push({
13826
+ type: "function_call",
13827
+ call_id: tu.id,
13828
+ name: tu.name,
13829
+ arguments: JSON.stringify(tu.input)
13830
+ });
13831
+ }
13832
+ }
13833
+ if (textParts.length > 0) {
13834
+ input.push({ role: "assistant", content: textParts.join("") });
13835
+ }
13836
+ }
13837
+ }
13838
+ }
13839
+ return { input, instructions };
13767
13840
  }
13768
13841
  /**
13769
- * Send a chat message using Codex Responses API format
13842
+ * Convert tool definitions to Responses API function tool format
13770
13843
  */
13771
- async chat(messages, options) {
13772
- const model = options?.model ?? this.config.model ?? DEFAULT_MODEL3;
13773
- const systemMsg = messages.find((m) => m.role === "system");
13774
- const instructions = systemMsg ? this.extractTextContent(systemMsg) : "You are a helpful coding assistant.";
13775
- const inputMessages = messages.filter((m) => m.role !== "system").map((msg) => this.convertMessagesToResponsesFormat([msg])[0]);
13844
+ convertTools(tools) {
13845
+ return tools.map((tool) => ({
13846
+ type: "function",
13847
+ name: tool.name,
13848
+ description: tool.description ?? void 0,
13849
+ parameters: tool.input_schema ?? null,
13850
+ strict: false
13851
+ }));
13852
+ }
13853
+ /**
13854
+ * Build the request body for the Codex Responses API
13855
+ */
13856
+ buildRequestBody(model, input, instructions, options) {
13776
13857
  const body = {
13777
13858
  model,
13778
- instructions,
13779
- input: inputMessages,
13780
- tools: [],
13859
+ input,
13860
+ instructions: instructions ?? "You are a helpful coding assistant.",
13861
+ max_output_tokens: options?.maxTokens ?? this.config.maxTokens ?? 8192,
13862
+ temperature: options?.temperature ?? this.config.temperature ?? 0,
13781
13863
  store: false,
13782
13864
  stream: true
13783
13865
  // Codex API requires streaming
13784
13866
  };
13785
- const response = await this.makeRequest(body);
13867
+ if (options?.tools && options.tools.length > 0) {
13868
+ body.tools = this.convertTools(options.tools);
13869
+ }
13870
+ return body;
13871
+ }
13872
+ /**
13873
+ * Read SSE stream and call handler for each parsed event.
13874
+ * Returns when stream ends.
13875
+ */
13876
+ async readSSEStream(response, onEvent) {
13786
13877
  if (!response.body) {
13787
- throw new ProviderError("No response body from Codex API", {
13788
- provider: this.id
13789
- });
13878
+ throw new ProviderError("No response body from Codex API", { provider: this.id });
13790
13879
  }
13791
13880
  const reader = response.body.getReader();
13792
13881
  const decoder = new TextDecoder();
13793
13882
  let buffer = "";
13794
- let content = "";
13795
- let responseId = `codex-${Date.now()}`;
13796
- let inputTokens = 0;
13797
- let outputTokens = 0;
13798
- let status = "completed";
13883
+ let lastActivityTime = Date.now();
13884
+ const timeoutController = new AbortController();
13885
+ const timeoutInterval = setInterval(() => {
13886
+ if (Date.now() - lastActivityTime > STREAM_TIMEOUT_MS) {
13887
+ clearInterval(timeoutInterval);
13888
+ timeoutController.abort();
13889
+ }
13890
+ }, 5e3);
13799
13891
  try {
13800
13892
  while (true) {
13893
+ if (timeoutController.signal.aborted) break;
13801
13894
  const { done, value } = await reader.read();
13802
13895
  if (done) break;
13896
+ lastActivityTime = Date.now();
13803
13897
  buffer += decoder.decode(value, { stream: true });
13804
13898
  const lines = buffer.split("\n");
13805
13899
  buffer = lines.pop() ?? "";
13806
13900
  for (const line of lines) {
13807
- if (line.startsWith("data: ")) {
13808
- const data = line.slice(6).trim();
13809
- if (!data || data === "[DONE]") continue;
13810
- try {
13811
- const parsed = JSON.parse(data);
13812
- if (parsed.id) {
13813
- responseId = parsed.id;
13814
- }
13815
- if (parsed.type === "response.output_text.delta" && parsed.delta) {
13816
- content += parsed.delta;
13817
- } else if (parsed.type === "response.completed" && parsed.response) {
13818
- if (parsed.response.usage) {
13819
- inputTokens = parsed.response.usage.input_tokens ?? 0;
13820
- outputTokens = parsed.response.usage.output_tokens ?? 0;
13821
- }
13822
- status = parsed.response.status ?? "completed";
13823
- } else if (parsed.type === "response.output_text.done" && parsed.text) {
13824
- content = parsed.text;
13825
- }
13826
- } catch {
13827
- }
13901
+ if (!line.startsWith("data: ")) continue;
13902
+ const data = line.slice(6).trim();
13903
+ if (!data || data === "[DONE]") continue;
13904
+ try {
13905
+ onEvent(JSON.parse(data));
13906
+ } catch {
13828
13907
  }
13829
13908
  }
13830
13909
  }
13831
13910
  } finally {
13911
+ clearInterval(timeoutInterval);
13832
13912
  reader.releaseLock();
13833
13913
  }
13834
- if (!content) {
13835
- throw new ProviderError("No response content from Codex API", {
13836
- provider: this.id
13837
- });
13914
+ if (timeoutController.signal.aborted) {
13915
+ throw new Error(
13916
+ `Stream timeout: No response from Codex API for ${STREAM_TIMEOUT_MS / 1e3}s`
13917
+ );
13838
13918
  }
13839
- const stopReason = status === "completed" ? "end_turn" : status === "incomplete" ? "max_tokens" : "end_turn";
13840
- return {
13841
- id: responseId,
13842
- content,
13843
- stopReason,
13844
- model,
13845
- usage: {
13846
- inputTokens,
13847
- outputTokens
13848
- }
13849
- };
13850
13919
  }
13851
13920
  /**
13852
- * Send a chat message with tool use
13853
- * Note: Codex Responses API tool support is complex; for now we delegate to chat()
13854
- * and return empty toolCalls. Full tool support can be added later.
13921
+ * Send a chat message using Codex Responses API format
13922
+ */
13923
+ async chat(messages, options) {
13924
+ return withRetry(async () => {
13925
+ const model = options?.model ?? this.config.model ?? DEFAULT_MODEL3;
13926
+ const { input, instructions } = this.convertToResponsesInput(messages, options?.system);
13927
+ const body = this.buildRequestBody(model, input, instructions, {
13928
+ maxTokens: options?.maxTokens,
13929
+ temperature: options?.temperature
13930
+ });
13931
+ const response = await this.makeRequest(body);
13932
+ let content = "";
13933
+ let responseId = `codex-${Date.now()}`;
13934
+ let inputTokens = 0;
13935
+ let outputTokens = 0;
13936
+ let status = "completed";
13937
+ await this.readSSEStream(response, (event) => {
13938
+ if (event.id) responseId = event.id;
13939
+ if (event.type === "response.output_text.delta" && event.delta) {
13940
+ content += event.delta;
13941
+ } else if (event.type === "response.output_text.done" && event.text) {
13942
+ content = event.text;
13943
+ } else if (event.type === "response.completed" && event.response) {
13944
+ const resp = event.response;
13945
+ const usage = resp.usage;
13946
+ if (usage) {
13947
+ inputTokens = usage.input_tokens ?? 0;
13948
+ outputTokens = usage.output_tokens ?? 0;
13949
+ }
13950
+ status = resp.status ?? "completed";
13951
+ }
13952
+ });
13953
+ const stopReason = status === "completed" ? "end_turn" : status === "incomplete" ? "max_tokens" : "end_turn";
13954
+ return {
13955
+ id: responseId,
13956
+ content,
13957
+ stopReason,
13958
+ model,
13959
+ usage: { inputTokens, outputTokens }
13960
+ };
13961
+ }, this.retryConfig);
13962
+ }
13963
+ /**
13964
+ * Send a chat message with tool use via Responses API
13855
13965
  */
13856
13966
  async chatWithTools(messages, options) {
13857
- const response = await this.chat(messages, options);
13858
- return {
13859
- ...response,
13860
- toolCalls: []
13861
- // Tools not yet supported in Codex provider
13862
- };
13967
+ return withRetry(async () => {
13968
+ const model = options?.model ?? this.config.model ?? DEFAULT_MODEL3;
13969
+ const { input, instructions } = this.convertToResponsesInput(messages, options?.system);
13970
+ const body = this.buildRequestBody(model, input, instructions, {
13971
+ tools: options.tools,
13972
+ maxTokens: options?.maxTokens
13973
+ });
13974
+ const response = await this.makeRequest(body);
13975
+ let content = "";
13976
+ let responseId = `codex-${Date.now()}`;
13977
+ let inputTokens = 0;
13978
+ let outputTokens = 0;
13979
+ const toolCalls = [];
13980
+ const fnCallBuilders = /* @__PURE__ */ new Map();
13981
+ await this.readSSEStream(response, (event) => {
13982
+ if (event.id) responseId = event.id;
13983
+ switch (event.type) {
13984
+ case "response.output_text.delta":
13985
+ content += event.delta ?? "";
13986
+ break;
13987
+ case "response.output_text.done":
13988
+ content = event.text ?? content;
13989
+ break;
13990
+ case "response.output_item.added": {
13991
+ const item = event.item;
13992
+ if (item.type === "function_call") {
13993
+ const itemKey = item.id ?? item.call_id;
13994
+ fnCallBuilders.set(itemKey, {
13995
+ callId: item.call_id,
13996
+ name: item.name,
13997
+ arguments: ""
13998
+ });
13999
+ }
14000
+ break;
14001
+ }
14002
+ case "response.function_call_arguments.delta": {
14003
+ const builder = fnCallBuilders.get(event.item_id);
14004
+ if (builder) builder.arguments += event.delta ?? "";
14005
+ break;
14006
+ }
14007
+ case "response.function_call_arguments.done": {
14008
+ const builder = fnCallBuilders.get(event.item_id);
14009
+ if (builder) {
14010
+ toolCalls.push({
14011
+ id: builder.callId,
14012
+ name: builder.name,
14013
+ input: parseArguments(event.arguments)
14014
+ });
14015
+ fnCallBuilders.delete(event.item_id);
14016
+ }
14017
+ break;
14018
+ }
14019
+ case "response.completed": {
14020
+ const resp = event.response;
14021
+ const usage = resp.usage;
14022
+ if (usage) {
14023
+ inputTokens = usage.input_tokens ?? 0;
14024
+ outputTokens = usage.output_tokens ?? 0;
14025
+ }
14026
+ for (const [, builder] of fnCallBuilders) {
14027
+ toolCalls.push({
14028
+ id: builder.callId,
14029
+ name: builder.name,
14030
+ input: parseArguments(builder.arguments)
14031
+ });
14032
+ }
14033
+ fnCallBuilders.clear();
14034
+ break;
14035
+ }
14036
+ }
14037
+ });
14038
+ return {
14039
+ id: responseId,
14040
+ content,
14041
+ stopReason: toolCalls.length > 0 ? "tool_use" : "end_turn",
14042
+ model,
14043
+ usage: { inputTokens, outputTokens },
14044
+ toolCalls
14045
+ };
14046
+ }, this.retryConfig);
13863
14047
  }
13864
14048
  /**
13865
- * Stream a chat response
13866
- * Note: True streaming with Codex Responses API is complex.
13867
- * For now, we make a non-streaming call and simulate streaming by emitting chunks.
14049
+ * Stream a chat response (no tools)
13868
14050
  */
13869
14051
  async *stream(messages, options) {
13870
- const response = await this.chat(messages, options);
13871
- if (response.content) {
13872
- const content = response.content;
13873
- const chunkSize = 20;
13874
- for (let i = 0; i < content.length; i += chunkSize) {
13875
- const chunk = content.slice(i, i + chunkSize);
13876
- yield { type: "text", text: chunk };
13877
- if (i + chunkSize < content.length) {
13878
- await new Promise((resolve3) => setTimeout(resolve3, 5));
14052
+ const model = options?.model ?? this.config.model ?? DEFAULT_MODEL3;
14053
+ const { input, instructions } = this.convertToResponsesInput(messages, options?.system);
14054
+ const body = this.buildRequestBody(model, input, instructions, {
14055
+ maxTokens: options?.maxTokens
14056
+ });
14057
+ const response = await this.makeRequest(body);
14058
+ if (!response.body) {
14059
+ throw new ProviderError("No response body from Codex API", { provider: this.id });
14060
+ }
14061
+ const reader = response.body.getReader();
14062
+ const decoder = new TextDecoder();
14063
+ let buffer = "";
14064
+ let lastActivityTime = Date.now();
14065
+ const timeoutController = new AbortController();
14066
+ const timeoutInterval = setInterval(() => {
14067
+ if (Date.now() - lastActivityTime > STREAM_TIMEOUT_MS) {
14068
+ clearInterval(timeoutInterval);
14069
+ timeoutController.abort();
14070
+ }
14071
+ }, 5e3);
14072
+ try {
14073
+ while (true) {
14074
+ if (timeoutController.signal.aborted) break;
14075
+ const { done, value } = await reader.read();
14076
+ if (done) break;
14077
+ lastActivityTime = Date.now();
14078
+ buffer += decoder.decode(value, { stream: true });
14079
+ const lines = buffer.split("\n");
14080
+ buffer = lines.pop() ?? "";
14081
+ for (const line of lines) {
14082
+ if (!line.startsWith("data: ")) continue;
14083
+ const data = line.slice(6).trim();
14084
+ if (!data || data === "[DONE]") continue;
14085
+ try {
14086
+ const event = JSON.parse(data);
14087
+ if (event.type === "response.output_text.delta" && event.delta) {
14088
+ yield { type: "text", text: event.delta };
14089
+ } else if (event.type === "response.completed") {
14090
+ yield { type: "done", stopReason: "end_turn" };
14091
+ }
14092
+ } catch {
14093
+ }
13879
14094
  }
13880
14095
  }
14096
+ } finally {
14097
+ clearInterval(timeoutInterval);
14098
+ reader.releaseLock();
14099
+ }
14100
+ if (timeoutController.signal.aborted) {
14101
+ throw new Error(
14102
+ `Stream timeout: No response from Codex API for ${STREAM_TIMEOUT_MS / 1e3}s`
14103
+ );
13881
14104
  }
13882
- yield { type: "done", stopReason: response.stopReason };
13883
14105
  }
13884
14106
  /**
13885
- * Stream a chat response with tool use
13886
- * Note: Tools and true streaming with Codex Responses API are not yet implemented.
13887
- * For now, we delegate to stream() which uses non-streaming under the hood.
14107
+ * Stream a chat response with tool use via Responses API.
14108
+ *
14109
+ * IMPORTANT: fnCallBuilders is keyed by output item ID (item.id), NOT by
14110
+ * call_id. The streaming events (function_call_arguments.delta/done) use
14111
+ * item_id which references the output item's id field, not call_id.
13888
14112
  */
13889
14113
  async *streamWithTools(messages, options) {
13890
- yield* this.stream(messages, options);
14114
+ const model = options?.model ?? this.config.model ?? DEFAULT_MODEL3;
14115
+ const { input, instructions } = this.convertToResponsesInput(messages, options?.system);
14116
+ const body = this.buildRequestBody(model, input, instructions, {
14117
+ tools: options.tools,
14118
+ maxTokens: options?.maxTokens
14119
+ });
14120
+ const response = await this.makeRequest(body);
14121
+ if (!response.body) {
14122
+ throw new ProviderError("No response body from Codex API", { provider: this.id });
14123
+ }
14124
+ const reader = response.body.getReader();
14125
+ const decoder = new TextDecoder();
14126
+ let buffer = "";
14127
+ const fnCallBuilders = /* @__PURE__ */ new Map();
14128
+ let lastActivityTime = Date.now();
14129
+ const timeoutController = new AbortController();
14130
+ const timeoutInterval = setInterval(() => {
14131
+ if (Date.now() - lastActivityTime > STREAM_TIMEOUT_MS) {
14132
+ clearInterval(timeoutInterval);
14133
+ timeoutController.abort();
14134
+ }
14135
+ }, 5e3);
14136
+ try {
14137
+ while (true) {
14138
+ if (timeoutController.signal.aborted) break;
14139
+ const { done, value } = await reader.read();
14140
+ if (done) break;
14141
+ lastActivityTime = Date.now();
14142
+ buffer += decoder.decode(value, { stream: true });
14143
+ const lines = buffer.split("\n");
14144
+ buffer = lines.pop() ?? "";
14145
+ for (const line of lines) {
14146
+ if (!line.startsWith("data: ")) continue;
14147
+ const data = line.slice(6).trim();
14148
+ if (!data || data === "[DONE]") continue;
14149
+ let event;
14150
+ try {
14151
+ event = JSON.parse(data);
14152
+ } catch {
14153
+ continue;
14154
+ }
14155
+ switch (event.type) {
14156
+ case "response.output_text.delta":
14157
+ yield { type: "text", text: event.delta ?? "" };
14158
+ break;
14159
+ case "response.output_item.added": {
14160
+ const item = event.item;
14161
+ if (item.type === "function_call") {
14162
+ const itemKey = item.id ?? item.call_id;
14163
+ fnCallBuilders.set(itemKey, {
14164
+ callId: item.call_id,
14165
+ name: item.name,
14166
+ arguments: ""
14167
+ });
14168
+ yield {
14169
+ type: "tool_use_start",
14170
+ toolCall: { id: item.call_id, name: item.name }
14171
+ };
14172
+ }
14173
+ break;
14174
+ }
14175
+ case "response.function_call_arguments.delta": {
14176
+ const builder = fnCallBuilders.get(event.item_id);
14177
+ if (builder) {
14178
+ builder.arguments += event.delta ?? "";
14179
+ }
14180
+ break;
14181
+ }
14182
+ case "response.function_call_arguments.done": {
14183
+ const builder = fnCallBuilders.get(event.item_id);
14184
+ if (builder) {
14185
+ yield {
14186
+ type: "tool_use_end",
14187
+ toolCall: {
14188
+ id: builder.callId,
14189
+ name: builder.name,
14190
+ input: parseArguments(event.arguments ?? builder.arguments)
14191
+ }
14192
+ };
14193
+ fnCallBuilders.delete(event.item_id);
14194
+ }
14195
+ break;
14196
+ }
14197
+ case "response.completed": {
14198
+ for (const [, builder] of fnCallBuilders) {
14199
+ yield {
14200
+ type: "tool_use_end",
14201
+ toolCall: {
14202
+ id: builder.callId,
14203
+ name: builder.name,
14204
+ input: parseArguments(builder.arguments)
14205
+ }
14206
+ };
14207
+ }
14208
+ fnCallBuilders.clear();
14209
+ const resp = event.response;
14210
+ const output = resp?.output ?? [];
14211
+ const hasToolCalls = output.some((i) => i.type === "function_call");
14212
+ yield {
14213
+ type: "done",
14214
+ stopReason: hasToolCalls ? "tool_use" : "end_turn"
14215
+ };
14216
+ break;
14217
+ }
14218
+ }
14219
+ }
14220
+ }
14221
+ } finally {
14222
+ clearInterval(timeoutInterval);
14223
+ reader.releaseLock();
14224
+ }
14225
+ if (timeoutController.signal.aborted) {
14226
+ throw new Error(
14227
+ `Stream timeout: No response from Codex API for ${STREAM_TIMEOUT_MS / 1e3}s`
14228
+ );
14229
+ }
13891
14230
  }
13892
14231
  };
13893
14232
  var CONTEXT_WINDOWS4 = {