@corbat-tech/coco 2.11.1 → 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/index.js CHANGED
@@ -189,7 +189,7 @@ function getDefaultModel(provider) {
189
189
  case "anthropic":
190
190
  return process.env["ANTHROPIC_MODEL"] ?? "claude-opus-4-6";
191
191
  case "openai":
192
- return process.env["OPENAI_MODEL"] ?? "gpt-5.3-codex";
192
+ return process.env["OPENAI_MODEL"] ?? "gpt-5.4-codex";
193
193
  case "gemini":
194
194
  return process.env["GEMINI_MODEL"] ?? "gemini-3.1-pro-preview";
195
195
  case "kimi":
@@ -201,7 +201,7 @@ function getDefaultModel(provider) {
201
201
  case "ollama":
202
202
  return process.env["OLLAMA_MODEL"] ?? "llama3.1";
203
203
  case "codex":
204
- return process.env["CODEX_MODEL"] ?? "gpt-5.3-codex";
204
+ return process.env["CODEX_MODEL"] ?? "gpt-5.4-codex";
205
205
  case "copilot":
206
206
  return process.env["COPILOT_MODEL"] ?? "claude-sonnet-4.6";
207
207
  case "groq":
@@ -219,7 +219,7 @@ function getDefaultModel(provider) {
219
219
  case "qwen":
220
220
  return process.env["QWEN_MODEL"] ?? "qwen-coder-plus";
221
221
  default:
222
- return "gpt-5.3-codex";
222
+ return "gpt-5.4-codex";
223
223
  }
224
224
  }
225
225
  function getDefaultProvider() {
@@ -12192,7 +12192,7 @@ function createKimiCodeProvider(config) {
12192
12192
  }
12193
12193
  return provider;
12194
12194
  }
12195
- var DEFAULT_MODEL2 = "gpt-5.3-codex";
12195
+ var DEFAULT_MODEL2 = "gpt-5.4-codex";
12196
12196
  var CONTEXT_WINDOWS2 = {
12197
12197
  // OpenAI models
12198
12198
  "gpt-4o": 128e3,
@@ -12215,6 +12215,7 @@ var CONTEXT_WINDOWS2 = {
12215
12215
  "gpt-5.2-instant": 4e5,
12216
12216
  "gpt-5.2-pro": 4e5,
12217
12217
  "gpt-5.3-codex": 4e5,
12218
+ "gpt-5.4-codex": 4e5,
12218
12219
  // Kimi/Moonshot models
12219
12220
  "kimi-k2.5": 262144,
12220
12221
  "kimi-k2-0324": 131072,
@@ -12271,7 +12272,7 @@ var CONTEXT_WINDOWS2 = {
12271
12272
  "microsoft/Phi-4": 16384,
12272
12273
  // OpenRouter model IDs
12273
12274
  "anthropic/claude-opus-4-6": 2e5,
12274
- "openai/gpt-5.3-codex": 4e5,
12275
+ "openai/gpt-5.4-codex": 4e5,
12275
12276
  "google/gemini-3-flash-preview": 1e6,
12276
12277
  "meta-llama/llama-3.3-70b-instruct": 128e3
12277
12278
  };
@@ -12526,7 +12527,7 @@ var OpenAIProvider = class {
12526
12527
  once: true
12527
12528
  });
12528
12529
  const providerName = this.name;
12529
- const parseArguments = (builder) => {
12530
+ const parseArguments2 = (builder) => {
12530
12531
  let input = {};
12531
12532
  try {
12532
12533
  input = builder.arguments ? JSON.parse(builder.arguments) : {};
@@ -12607,7 +12608,7 @@ var OpenAIProvider = class {
12607
12608
  toolCall: {
12608
12609
  id: builder.id,
12609
12610
  name: builder.name,
12610
- input: parseArguments(builder)
12611
+ input: parseArguments2(builder)
12611
12612
  }
12612
12613
  };
12613
12614
  }
@@ -12620,7 +12621,7 @@ var OpenAIProvider = class {
12620
12621
  toolCall: {
12621
12622
  id: builder.id,
12622
12623
  name: builder.name,
12623
- input: parseArguments(builder)
12624
+ input: parseArguments2(builder)
12624
12625
  }
12625
12626
  };
12626
12627
  }
@@ -13611,8 +13612,9 @@ async function getCachedADCToken() {
13611
13612
 
13612
13613
  // src/providers/codex.ts
13613
13614
  var CODEX_API_ENDPOINT = "https://chatgpt.com/backend-api/codex/responses";
13614
- var DEFAULT_MODEL3 = "gpt-5.3-codex";
13615
+ var DEFAULT_MODEL3 = "gpt-5.4-codex";
13615
13616
  var CONTEXT_WINDOWS3 = {
13617
+ "gpt-5.4-codex": 2e5,
13616
13618
  "gpt-5.3-codex": 2e5,
13617
13619
  "gpt-5.2-codex": 2e5,
13618
13620
  "gpt-5-codex": 2e5,
@@ -13621,6 +13623,7 @@ var CONTEXT_WINDOWS3 = {
13621
13623
  "gpt-5.2": 2e5,
13622
13624
  "gpt-5.1": 2e5
13623
13625
  };
13626
+ var STREAM_TIMEOUT_MS = 12e4;
13624
13627
  function parseJwtClaims(token) {
13625
13628
  const parts = token.split(".");
13626
13629
  if (parts.length !== 3 || !parts[1]) return void 0;
@@ -13636,12 +13639,28 @@ function extractAccountId(accessToken) {
13636
13639
  const auth = claims["https://api.openai.com/auth"];
13637
13640
  return claims["chatgpt_account_id"] || auth?.["chatgpt_account_id"] || claims["organizations"]?.[0]?.id;
13638
13641
  }
13642
+ function parseArguments(args) {
13643
+ try {
13644
+ return args ? JSON.parse(args) : {};
13645
+ } catch {
13646
+ try {
13647
+ if (args) {
13648
+ const repaired = jsonrepair(args);
13649
+ return JSON.parse(repaired);
13650
+ }
13651
+ } catch {
13652
+ console.error(`[Codex] Cannot parse tool arguments: ${args.slice(0, 200)}`);
13653
+ }
13654
+ return {};
13655
+ }
13656
+ }
13639
13657
  var CodexProvider = class {
13640
13658
  id = "codex";
13641
13659
  name = "OpenAI Codex (ChatGPT Plus/Pro)";
13642
13660
  config = {};
13643
13661
  accessToken = null;
13644
13662
  accountId;
13663
+ retryConfig = DEFAULT_RETRY_CONFIG;
13645
13664
  /**
13646
13665
  * Initialize the provider with OAuth tokens
13647
13666
  */
@@ -13726,166 +13745,466 @@ var CodexProvider = class {
13726
13745
  /**
13727
13746
  * Extract text content from a message
13728
13747
  */
13729
- extractTextContent(msg) {
13730
- if (typeof msg.content === "string") {
13731
- return msg.content;
13732
- }
13733
- if (Array.isArray(msg.content)) {
13734
- return msg.content.map((part) => {
13735
- if (part.type === "text") return part.text;
13736
- if (part.type === "tool_result") return `Tool result: ${JSON.stringify(part.content)}`;
13737
- return "";
13738
- }).join("\n");
13748
+ contentToString(content) {
13749
+ if (typeof content === "string") return content;
13750
+ if (Array.isArray(content)) {
13751
+ return content.filter((part) => part.type === "text").map((part) => part.text).join("\n");
13739
13752
  }
13740
13753
  return "";
13741
13754
  }
13742
13755
  /**
13743
- * Convert messages to Codex Responses API format
13744
- * Codex uses a different format than Chat Completions:
13745
- * {
13746
- * "input": [
13747
- * { "type": "message", "role": "developer|user", "content": [{ "type": "input_text", "text": "..." }] },
13748
- * { "type": "message", "role": "assistant", "content": [{ "type": "output_text", "text": "..." }] }
13749
- * ]
13750
- * }
13756
+ * Convert messages to Responses API input format.
13751
13757
  *
13752
- * IMPORTANT: User/developer messages use "input_text", assistant messages use "output_text"
13758
+ * Handles:
13759
+ * - system messages → extracted as instructions
13760
+ * - user text messages → { role: "user", content: "..." }
13761
+ * - user tool_result messages → function_call_output items
13762
+ * - assistant text → { role: "assistant", content: "..." }
13763
+ * - assistant tool_use → function_call items
13753
13764
  */
13754
- convertMessagesToResponsesFormat(messages) {
13755
- return messages.map((msg) => {
13756
- const text = this.extractTextContent(msg);
13757
- const role = msg.role === "system" ? "developer" : msg.role;
13758
- const contentType = msg.role === "assistant" ? "output_text" : "input_text";
13759
- return {
13760
- type: "message",
13761
- role,
13762
- content: [{ type: contentType, text }]
13763
- };
13764
- });
13765
+ convertToResponsesInput(messages, systemPrompt) {
13766
+ const input = [];
13767
+ let instructions = systemPrompt ?? null;
13768
+ for (const msg of messages) {
13769
+ if (msg.role === "system") {
13770
+ instructions = (instructions ? instructions + "\n\n" : "") + this.contentToString(msg.content);
13771
+ } else if (msg.role === "user") {
13772
+ if (Array.isArray(msg.content) && msg.content.some((b) => b.type === "tool_result")) {
13773
+ for (const block of msg.content) {
13774
+ if (block.type === "tool_result") {
13775
+ const tr = block;
13776
+ input.push({
13777
+ type: "function_call_output",
13778
+ call_id: tr.tool_use_id,
13779
+ output: tr.content
13780
+ });
13781
+ }
13782
+ }
13783
+ } else {
13784
+ input.push({
13785
+ role: "user",
13786
+ content: this.contentToString(msg.content)
13787
+ });
13788
+ }
13789
+ } else if (msg.role === "assistant") {
13790
+ if (typeof msg.content === "string") {
13791
+ input.push({ role: "assistant", content: msg.content });
13792
+ } else if (Array.isArray(msg.content)) {
13793
+ const textParts = [];
13794
+ for (const block of msg.content) {
13795
+ if (block.type === "text") {
13796
+ textParts.push(block.text);
13797
+ } else if (block.type === "tool_use") {
13798
+ if (textParts.length > 0) {
13799
+ input.push({ role: "assistant", content: textParts.join("") });
13800
+ textParts.length = 0;
13801
+ }
13802
+ const tu = block;
13803
+ input.push({
13804
+ type: "function_call",
13805
+ call_id: tu.id,
13806
+ name: tu.name,
13807
+ arguments: JSON.stringify(tu.input)
13808
+ });
13809
+ }
13810
+ }
13811
+ if (textParts.length > 0) {
13812
+ input.push({ role: "assistant", content: textParts.join("") });
13813
+ }
13814
+ }
13815
+ }
13816
+ }
13817
+ return { input, instructions };
13765
13818
  }
13766
13819
  /**
13767
- * Send a chat message using Codex Responses API format
13820
+ * Convert tool definitions to Responses API function tool format
13768
13821
  */
13769
- async chat(messages, options) {
13770
- const model = options?.model ?? this.config.model ?? DEFAULT_MODEL3;
13771
- const systemMsg = messages.find((m) => m.role === "system");
13772
- const instructions = systemMsg ? this.extractTextContent(systemMsg) : "You are a helpful coding assistant.";
13773
- const inputMessages = messages.filter((m) => m.role !== "system").map((msg) => this.convertMessagesToResponsesFormat([msg])[0]);
13822
+ convertTools(tools) {
13823
+ return tools.map((tool) => ({
13824
+ type: "function",
13825
+ name: tool.name,
13826
+ description: tool.description ?? void 0,
13827
+ parameters: tool.input_schema ?? null,
13828
+ strict: false
13829
+ }));
13830
+ }
13831
+ /**
13832
+ * Build the request body for the Codex Responses API
13833
+ */
13834
+ buildRequestBody(model, input, instructions, options) {
13774
13835
  const body = {
13775
13836
  model,
13776
- instructions,
13777
- input: inputMessages,
13778
- tools: [],
13837
+ input,
13838
+ instructions: instructions ?? "You are a helpful coding assistant.",
13839
+ max_output_tokens: options?.maxTokens ?? this.config.maxTokens ?? 8192,
13840
+ temperature: options?.temperature ?? this.config.temperature ?? 0,
13779
13841
  store: false,
13780
13842
  stream: true
13781
13843
  // Codex API requires streaming
13782
13844
  };
13783
- const response = await this.makeRequest(body);
13845
+ if (options?.tools && options.tools.length > 0) {
13846
+ body.tools = this.convertTools(options.tools);
13847
+ }
13848
+ return body;
13849
+ }
13850
+ /**
13851
+ * Read SSE stream and call handler for each parsed event.
13852
+ * Returns when stream ends.
13853
+ */
13854
+ async readSSEStream(response, onEvent) {
13784
13855
  if (!response.body) {
13785
- throw new ProviderError("No response body from Codex API", {
13786
- provider: this.id
13787
- });
13856
+ throw new ProviderError("No response body from Codex API", { provider: this.id });
13788
13857
  }
13789
13858
  const reader = response.body.getReader();
13790
13859
  const decoder = new TextDecoder();
13791
13860
  let buffer = "";
13792
- let content = "";
13793
- let responseId = `codex-${Date.now()}`;
13794
- let inputTokens = 0;
13795
- let outputTokens = 0;
13796
- let status = "completed";
13861
+ let lastActivityTime = Date.now();
13862
+ const timeoutController = new AbortController();
13863
+ const timeoutInterval = setInterval(() => {
13864
+ if (Date.now() - lastActivityTime > STREAM_TIMEOUT_MS) {
13865
+ clearInterval(timeoutInterval);
13866
+ timeoutController.abort();
13867
+ }
13868
+ }, 5e3);
13797
13869
  try {
13798
13870
  while (true) {
13871
+ if (timeoutController.signal.aborted) break;
13799
13872
  const { done, value } = await reader.read();
13800
13873
  if (done) break;
13874
+ lastActivityTime = Date.now();
13801
13875
  buffer += decoder.decode(value, { stream: true });
13802
13876
  const lines = buffer.split("\n");
13803
13877
  buffer = lines.pop() ?? "";
13804
13878
  for (const line of lines) {
13805
- if (line.startsWith("data: ")) {
13806
- const data = line.slice(6).trim();
13807
- if (!data || data === "[DONE]") continue;
13808
- try {
13809
- const parsed = JSON.parse(data);
13810
- if (parsed.id) {
13811
- responseId = parsed.id;
13812
- }
13813
- if (parsed.type === "response.output_text.delta" && parsed.delta) {
13814
- content += parsed.delta;
13815
- } else if (parsed.type === "response.completed" && parsed.response) {
13816
- if (parsed.response.usage) {
13817
- inputTokens = parsed.response.usage.input_tokens ?? 0;
13818
- outputTokens = parsed.response.usage.output_tokens ?? 0;
13819
- }
13820
- status = parsed.response.status ?? "completed";
13821
- } else if (parsed.type === "response.output_text.done" && parsed.text) {
13822
- content = parsed.text;
13823
- }
13824
- } catch {
13825
- }
13879
+ if (!line.startsWith("data: ")) continue;
13880
+ const data = line.slice(6).trim();
13881
+ if (!data || data === "[DONE]") continue;
13882
+ try {
13883
+ onEvent(JSON.parse(data));
13884
+ } catch {
13826
13885
  }
13827
13886
  }
13828
13887
  }
13829
13888
  } finally {
13889
+ clearInterval(timeoutInterval);
13830
13890
  reader.releaseLock();
13831
13891
  }
13832
- if (!content) {
13833
- throw new ProviderError("No response content from Codex API", {
13834
- provider: this.id
13835
- });
13892
+ if (timeoutController.signal.aborted) {
13893
+ throw new Error(
13894
+ `Stream timeout: No response from Codex API for ${STREAM_TIMEOUT_MS / 1e3}s`
13895
+ );
13836
13896
  }
13837
- const stopReason = status === "completed" ? "end_turn" : status === "incomplete" ? "max_tokens" : "end_turn";
13838
- return {
13839
- id: responseId,
13840
- content,
13841
- stopReason,
13842
- model,
13843
- usage: {
13844
- inputTokens,
13845
- outputTokens
13846
- }
13847
- };
13848
13897
  }
13849
13898
  /**
13850
- * Send a chat message with tool use
13851
- * Note: Codex Responses API tool support is complex; for now we delegate to chat()
13852
- * and return empty toolCalls. Full tool support can be added later.
13899
+ * Send a chat message using Codex Responses API format
13900
+ */
13901
+ async chat(messages, options) {
13902
+ return withRetry(async () => {
13903
+ const model = options?.model ?? this.config.model ?? DEFAULT_MODEL3;
13904
+ const { input, instructions } = this.convertToResponsesInput(messages, options?.system);
13905
+ const body = this.buildRequestBody(model, input, instructions, {
13906
+ maxTokens: options?.maxTokens,
13907
+ temperature: options?.temperature
13908
+ });
13909
+ const response = await this.makeRequest(body);
13910
+ let content = "";
13911
+ let responseId = `codex-${Date.now()}`;
13912
+ let inputTokens = 0;
13913
+ let outputTokens = 0;
13914
+ let status = "completed";
13915
+ await this.readSSEStream(response, (event) => {
13916
+ if (event.id) responseId = event.id;
13917
+ if (event.type === "response.output_text.delta" && event.delta) {
13918
+ content += event.delta;
13919
+ } else if (event.type === "response.output_text.done" && event.text) {
13920
+ content = event.text;
13921
+ } else if (event.type === "response.completed" && event.response) {
13922
+ const resp = event.response;
13923
+ const usage = resp.usage;
13924
+ if (usage) {
13925
+ inputTokens = usage.input_tokens ?? 0;
13926
+ outputTokens = usage.output_tokens ?? 0;
13927
+ }
13928
+ status = resp.status ?? "completed";
13929
+ }
13930
+ });
13931
+ const stopReason = status === "completed" ? "end_turn" : status === "incomplete" ? "max_tokens" : "end_turn";
13932
+ return {
13933
+ id: responseId,
13934
+ content,
13935
+ stopReason,
13936
+ model,
13937
+ usage: { inputTokens, outputTokens }
13938
+ };
13939
+ }, this.retryConfig);
13940
+ }
13941
+ /**
13942
+ * Send a chat message with tool use via Responses API
13853
13943
  */
13854
13944
  async chatWithTools(messages, options) {
13855
- const response = await this.chat(messages, options);
13856
- return {
13857
- ...response,
13858
- toolCalls: []
13859
- // Tools not yet supported in Codex provider
13860
- };
13945
+ return withRetry(async () => {
13946
+ const model = options?.model ?? this.config.model ?? DEFAULT_MODEL3;
13947
+ const { input, instructions } = this.convertToResponsesInput(messages, options?.system);
13948
+ const body = this.buildRequestBody(model, input, instructions, {
13949
+ tools: options.tools,
13950
+ maxTokens: options?.maxTokens
13951
+ });
13952
+ const response = await this.makeRequest(body);
13953
+ let content = "";
13954
+ let responseId = `codex-${Date.now()}`;
13955
+ let inputTokens = 0;
13956
+ let outputTokens = 0;
13957
+ const toolCalls = [];
13958
+ const fnCallBuilders = /* @__PURE__ */ new Map();
13959
+ await this.readSSEStream(response, (event) => {
13960
+ if (event.id) responseId = event.id;
13961
+ switch (event.type) {
13962
+ case "response.output_text.delta":
13963
+ content += event.delta ?? "";
13964
+ break;
13965
+ case "response.output_text.done":
13966
+ content = event.text ?? content;
13967
+ break;
13968
+ case "response.output_item.added": {
13969
+ const item = event.item;
13970
+ if (item.type === "function_call") {
13971
+ const itemKey = item.id ?? item.call_id;
13972
+ fnCallBuilders.set(itemKey, {
13973
+ callId: item.call_id,
13974
+ name: item.name,
13975
+ arguments: ""
13976
+ });
13977
+ }
13978
+ break;
13979
+ }
13980
+ case "response.function_call_arguments.delta": {
13981
+ const builder = fnCallBuilders.get(event.item_id);
13982
+ if (builder) builder.arguments += event.delta ?? "";
13983
+ break;
13984
+ }
13985
+ case "response.function_call_arguments.done": {
13986
+ const builder = fnCallBuilders.get(event.item_id);
13987
+ if (builder) {
13988
+ toolCalls.push({
13989
+ id: builder.callId,
13990
+ name: builder.name,
13991
+ input: parseArguments(event.arguments)
13992
+ });
13993
+ fnCallBuilders.delete(event.item_id);
13994
+ }
13995
+ break;
13996
+ }
13997
+ case "response.completed": {
13998
+ const resp = event.response;
13999
+ const usage = resp.usage;
14000
+ if (usage) {
14001
+ inputTokens = usage.input_tokens ?? 0;
14002
+ outputTokens = usage.output_tokens ?? 0;
14003
+ }
14004
+ for (const [, builder] of fnCallBuilders) {
14005
+ toolCalls.push({
14006
+ id: builder.callId,
14007
+ name: builder.name,
14008
+ input: parseArguments(builder.arguments)
14009
+ });
14010
+ }
14011
+ fnCallBuilders.clear();
14012
+ break;
14013
+ }
14014
+ }
14015
+ });
14016
+ return {
14017
+ id: responseId,
14018
+ content,
14019
+ stopReason: toolCalls.length > 0 ? "tool_use" : "end_turn",
14020
+ model,
14021
+ usage: { inputTokens, outputTokens },
14022
+ toolCalls
14023
+ };
14024
+ }, this.retryConfig);
13861
14025
  }
13862
14026
  /**
13863
- * Stream a chat response
13864
- * Note: True streaming with Codex Responses API is complex.
13865
- * For now, we make a non-streaming call and simulate streaming by emitting chunks.
14027
+ * Stream a chat response (no tools)
13866
14028
  */
13867
14029
  async *stream(messages, options) {
13868
- const response = await this.chat(messages, options);
13869
- if (response.content) {
13870
- const content = response.content;
13871
- const chunkSize = 20;
13872
- for (let i = 0; i < content.length; i += chunkSize) {
13873
- const chunk = content.slice(i, i + chunkSize);
13874
- yield { type: "text", text: chunk };
13875
- if (i + chunkSize < content.length) {
13876
- await new Promise((resolve3) => setTimeout(resolve3, 5));
14030
+ const model = options?.model ?? this.config.model ?? DEFAULT_MODEL3;
14031
+ const { input, instructions } = this.convertToResponsesInput(messages, options?.system);
14032
+ const body = this.buildRequestBody(model, input, instructions, {
14033
+ maxTokens: options?.maxTokens
14034
+ });
14035
+ const response = await this.makeRequest(body);
14036
+ if (!response.body) {
14037
+ throw new ProviderError("No response body from Codex API", { provider: this.id });
14038
+ }
14039
+ const reader = response.body.getReader();
14040
+ const decoder = new TextDecoder();
14041
+ let buffer = "";
14042
+ let lastActivityTime = Date.now();
14043
+ const timeoutController = new AbortController();
14044
+ const timeoutInterval = setInterval(() => {
14045
+ if (Date.now() - lastActivityTime > STREAM_TIMEOUT_MS) {
14046
+ clearInterval(timeoutInterval);
14047
+ timeoutController.abort();
14048
+ }
14049
+ }, 5e3);
14050
+ try {
14051
+ while (true) {
14052
+ if (timeoutController.signal.aborted) break;
14053
+ const { done, value } = await reader.read();
14054
+ if (done) break;
14055
+ lastActivityTime = Date.now();
14056
+ buffer += decoder.decode(value, { stream: true });
14057
+ const lines = buffer.split("\n");
14058
+ buffer = lines.pop() ?? "";
14059
+ for (const line of lines) {
14060
+ if (!line.startsWith("data: ")) continue;
14061
+ const data = line.slice(6).trim();
14062
+ if (!data || data === "[DONE]") continue;
14063
+ try {
14064
+ const event = JSON.parse(data);
14065
+ if (event.type === "response.output_text.delta" && event.delta) {
14066
+ yield { type: "text", text: event.delta };
14067
+ } else if (event.type === "response.completed") {
14068
+ yield { type: "done", stopReason: "end_turn" };
14069
+ }
14070
+ } catch {
14071
+ }
13877
14072
  }
13878
14073
  }
14074
+ } finally {
14075
+ clearInterval(timeoutInterval);
14076
+ reader.releaseLock();
14077
+ }
14078
+ if (timeoutController.signal.aborted) {
14079
+ throw new Error(
14080
+ `Stream timeout: No response from Codex API for ${STREAM_TIMEOUT_MS / 1e3}s`
14081
+ );
13879
14082
  }
13880
- yield { type: "done", stopReason: response.stopReason };
13881
14083
  }
13882
14084
  /**
13883
- * Stream a chat response with tool use
13884
- * Note: Tools and true streaming with Codex Responses API are not yet implemented.
13885
- * For now, we delegate to stream() which uses non-streaming under the hood.
14085
+ * Stream a chat response with tool use via Responses API.
14086
+ *
14087
+ * IMPORTANT: fnCallBuilders is keyed by output item ID (item.id), NOT by
14088
+ * call_id. The streaming events (function_call_arguments.delta/done) use
14089
+ * item_id which references the output item's id field, not call_id.
13886
14090
  */
13887
14091
  async *streamWithTools(messages, options) {
13888
- yield* this.stream(messages, options);
14092
+ const model = options?.model ?? this.config.model ?? DEFAULT_MODEL3;
14093
+ const { input, instructions } = this.convertToResponsesInput(messages, options?.system);
14094
+ const body = this.buildRequestBody(model, input, instructions, {
14095
+ tools: options.tools,
14096
+ maxTokens: options?.maxTokens
14097
+ });
14098
+ const response = await this.makeRequest(body);
14099
+ if (!response.body) {
14100
+ throw new ProviderError("No response body from Codex API", { provider: this.id });
14101
+ }
14102
+ const reader = response.body.getReader();
14103
+ const decoder = new TextDecoder();
14104
+ let buffer = "";
14105
+ const fnCallBuilders = /* @__PURE__ */ new Map();
14106
+ let lastActivityTime = Date.now();
14107
+ const timeoutController = new AbortController();
14108
+ const timeoutInterval = setInterval(() => {
14109
+ if (Date.now() - lastActivityTime > STREAM_TIMEOUT_MS) {
14110
+ clearInterval(timeoutInterval);
14111
+ timeoutController.abort();
14112
+ }
14113
+ }, 5e3);
14114
+ try {
14115
+ while (true) {
14116
+ if (timeoutController.signal.aborted) break;
14117
+ const { done, value } = await reader.read();
14118
+ if (done) break;
14119
+ lastActivityTime = Date.now();
14120
+ buffer += decoder.decode(value, { stream: true });
14121
+ const lines = buffer.split("\n");
14122
+ buffer = lines.pop() ?? "";
14123
+ for (const line of lines) {
14124
+ if (!line.startsWith("data: ")) continue;
14125
+ const data = line.slice(6).trim();
14126
+ if (!data || data === "[DONE]") continue;
14127
+ let event;
14128
+ try {
14129
+ event = JSON.parse(data);
14130
+ } catch {
14131
+ continue;
14132
+ }
14133
+ switch (event.type) {
14134
+ case "response.output_text.delta":
14135
+ yield { type: "text", text: event.delta ?? "" };
14136
+ break;
14137
+ case "response.output_item.added": {
14138
+ const item = event.item;
14139
+ if (item.type === "function_call") {
14140
+ const itemKey = item.id ?? item.call_id;
14141
+ fnCallBuilders.set(itemKey, {
14142
+ callId: item.call_id,
14143
+ name: item.name,
14144
+ arguments: ""
14145
+ });
14146
+ yield {
14147
+ type: "tool_use_start",
14148
+ toolCall: { id: item.call_id, name: item.name }
14149
+ };
14150
+ }
14151
+ break;
14152
+ }
14153
+ case "response.function_call_arguments.delta": {
14154
+ const builder = fnCallBuilders.get(event.item_id);
14155
+ if (builder) {
14156
+ builder.arguments += event.delta ?? "";
14157
+ }
14158
+ break;
14159
+ }
14160
+ case "response.function_call_arguments.done": {
14161
+ const builder = fnCallBuilders.get(event.item_id);
14162
+ if (builder) {
14163
+ yield {
14164
+ type: "tool_use_end",
14165
+ toolCall: {
14166
+ id: builder.callId,
14167
+ name: builder.name,
14168
+ input: parseArguments(event.arguments ?? builder.arguments)
14169
+ }
14170
+ };
14171
+ fnCallBuilders.delete(event.item_id);
14172
+ }
14173
+ break;
14174
+ }
14175
+ case "response.completed": {
14176
+ for (const [, builder] of fnCallBuilders) {
14177
+ yield {
14178
+ type: "tool_use_end",
14179
+ toolCall: {
14180
+ id: builder.callId,
14181
+ name: builder.name,
14182
+ input: parseArguments(builder.arguments)
14183
+ }
14184
+ };
14185
+ }
14186
+ fnCallBuilders.clear();
14187
+ const resp = event.response;
14188
+ const output = resp?.output ?? [];
14189
+ const hasToolCalls = output.some((i) => i.type === "function_call");
14190
+ yield {
14191
+ type: "done",
14192
+ stopReason: hasToolCalls ? "tool_use" : "end_turn"
14193
+ };
14194
+ break;
14195
+ }
14196
+ }
14197
+ }
14198
+ }
14199
+ } finally {
14200
+ clearInterval(timeoutInterval);
14201
+ reader.releaseLock();
14202
+ }
14203
+ if (timeoutController.signal.aborted) {
14204
+ throw new Error(
14205
+ `Stream timeout: No response from Codex API for ${STREAM_TIMEOUT_MS / 1e3}s`
14206
+ );
14207
+ }
13889
14208
  }
13890
14209
  };
13891
14210
  var CONTEXT_WINDOWS4 = {
@@ -13898,6 +14217,7 @@ var CONTEXT_WINDOWS4 = {
13898
14217
  // OpenAI models — chat/completions
13899
14218
  "gpt-4.1": 1048576,
13900
14219
  // OpenAI models — /responses API (Codex/GPT-5+)
14220
+ "gpt-5.4-codex": 4e5,
13901
14221
  "gpt-5.3-codex": 4e5,
13902
14222
  "gpt-5.2-codex": 4e5,
13903
14223
  "gpt-5.1-codex-max": 4e5,