@kenkaiiii/gg-ai 5.83.2 → 5.83.4

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.cjs CHANGED
@@ -36,6 +36,7 @@ __export(index_exports, {
36
36
  GGAIError: () => GGAIError,
37
37
  ProviderError: () => ProviderError,
38
38
  REDACTION_MARKER: () => REDACTED,
39
+ ResponseStalledError: () => ResponseStalledError,
39
40
  StreamResult: () => StreamResult,
40
41
  TOOL_CALL_NAME_RULES: () => TOOL_CALL_NAME_RULES,
41
42
  clampProviderContextImages: () => clampProviderContextImages,
@@ -205,6 +206,14 @@ var VideoUnsupportedError = class extends GGAIError {
205
206
  this.name = "VideoUnsupportedError";
206
207
  }
207
208
  };
209
+ var ResponseStalledError = class extends GGAIError {
210
+ inToolCall;
211
+ constructor(message, options) {
212
+ super(message, { source: "network", hint: options.hint, cause: options.cause });
213
+ this.name = "ResponseStalledError";
214
+ this.inToolCall = options.inToolCall;
215
+ }
216
+ };
208
217
  var ProviderError = class extends GGAIError {
209
218
  provider;
210
219
  statusCode;
@@ -340,6 +349,14 @@ function formatError(err) {
340
349
  guidance: err.hint ?? providerGuidance(err.provider, cleanMessage, err.statusCode)
341
350
  };
342
351
  }
352
+ if (err instanceof ResponseStalledError) {
353
+ return {
354
+ headline: "The response kept stalling before it finished.",
355
+ source: err.source,
356
+ message: err.message,
357
+ guidance: err.hint ?? "Retry once. If it keeps happening, choose another model."
358
+ };
359
+ }
343
360
  if (err instanceof GGAIError) {
344
361
  return finaliseBySource(err.source, err.message, err.requestId, err.hint);
345
362
  }
@@ -1751,9 +1768,43 @@ function parseToolArguments(argsJson) {
1751
1768
  }
1752
1769
  }
1753
1770
 
1771
+ // src/utils/provider-fetch.ts
1772
+ var import_undici = require("undici");
1773
+ var PROVIDER_TRANSPORT_TIMEOUT_MS = 30 * 6e4;
1774
+ function nodeUsesEnvProxy({
1775
+ env = process.env,
1776
+ execArgv = process.execArgv,
1777
+ nodeVersion = process.versions.node
1778
+ } = {}) {
1779
+ const flags = [...execArgv, ...(env.NODE_OPTIONS ?? "").split(/\s+/)];
1780
+ if (flags.includes("--use-env-proxy")) return true;
1781
+ if (env.NODE_USE_ENV_PROXY !== "1") return false;
1782
+ const [major = 0, minor = 0] = nodeVersion.split(".").map(Number);
1783
+ return major >= 24 || major === 22 && minor >= 21;
1784
+ }
1785
+ function createProviderFetch(timeoutMs, { useEnvProxy = nodeUsesEnvProxy() } = {}) {
1786
+ const timeouts = { headersTimeout: timeoutMs, bodyTimeout: timeoutMs };
1787
+ const dispatcher = useEnvProxy ? new import_undici.EnvHttpProxyAgent(timeouts) : new import_undici.Agent(timeouts);
1788
+ const builtinFetch = globalThis.fetch;
1789
+ const providerFetch2 = async (input, init2) => {
1790
+ if (globalThis.fetch !== builtinFetch || input instanceof Request || init2?.body instanceof FormData) {
1791
+ return globalThis.fetch(input, init2);
1792
+ }
1793
+ const response = await (0, import_undici.fetch)(input, {
1794
+ ...init2,
1795
+ dispatcher
1796
+ });
1797
+ return response;
1798
+ };
1799
+ return Object.assign(providerFetch2, { Response: globalThis.Response });
1800
+ }
1801
+ var providerFetch = createProviderFetch(
1802
+ PROVIDER_TRANSPORT_TIMEOUT_MS
1803
+ );
1804
+
1754
1805
  // src/providers/anthropic.ts
1755
1806
  var anthropicClientCache = /* @__PURE__ */ new Map();
1756
- var NON_STREAMING_REQUEST_TIMEOUT_MS = 6e5;
1807
+ var NON_STREAMING_REQUEST_TIMEOUT_MS = PROVIDER_TRANSPORT_TIMEOUT_MS;
1757
1808
  function fineGrainedToolStreamingEnabled() {
1758
1809
  const raw = process.env.GG_FINE_GRAINED_TOOL_STREAMING ?? process.env.CLAUDE_CODE_ENABLE_FINE_GRAINED_TOOL_STREAMING;
1759
1810
  if (!raw) return false;
@@ -1814,7 +1865,8 @@ function createClient(options) {
1814
1865
  const client = new import_sdk.default({
1815
1866
  ...isOAuth ? { apiKey: null, authToken: options.apiKey } : { apiKey: options.apiKey },
1816
1867
  ...options.baseUrl ? { baseURL: options.baseUrl } : {},
1817
- ...options.fetch ? { fetch: options.fetch } : {},
1868
+ // Default transport lifts undici's 300s header/body caps (see provider-fetch.ts).
1869
+ fetch: options.fetch ?? providerFetch,
1818
1870
  maxRetries: 0,
1819
1871
  ...isOAuth ? {
1820
1872
  defaultHeaders: {
@@ -2592,7 +2644,8 @@ function createClient2(options) {
2592
2644
  const client = new import_openai.default({
2593
2645
  apiKey: options.apiKey,
2594
2646
  ...options.baseUrl ? { baseURL: options.baseUrl } : {},
2595
- ...options.fetch ? { fetch: options.fetch } : {},
2647
+ // Default transport lifts undici's 300s header/body caps (see provider-fetch.ts).
2648
+ fetch: options.fetch ?? providerFetch,
2596
2649
  ...options.defaultHeaders ? { defaultHeaders: options.defaultHeaders } : {}
2597
2650
  });
2598
2651
  if (!options.fetch) {
@@ -2714,16 +2767,13 @@ async function* runStream2(options) {
2714
2767
  `
2715
2768
  );
2716
2769
  }
2717
- const requestHeaders = options.provider === "xai" ? {
2718
- headers: {
2719
- "x-grok-conv-id": normalizePromptCacheKey(options.promptCacheKey ?? "ggcoder")
2720
- }
2721
- } : {};
2722
2770
  if (!useStreaming) {
2723
2771
  try {
2724
2772
  const completion = await client.chat.completions.create(requestParams, {
2725
2773
  signal: options.signal ?? void 0,
2726
- ...requestHeaders
2774
+ // The whole reply arrives in one response, so the SDK's 10min default
2775
+ // would undercut the agent loop's 15min thinking cap for this call.
2776
+ timeout: PROVIDER_TRANSPORT_TIMEOUT_MS
2727
2777
  });
2728
2778
  yield* synthesizeEventsFromCompletion(completion, !!options.thinking, endpointKey);
2729
2779
  return completionToResponse(completion, endpointKey);
@@ -2734,8 +2784,7 @@ async function* runStream2(options) {
2734
2784
  let stream2;
2735
2785
  try {
2736
2786
  stream2 = await client.chat.completions.create(requestParams, {
2737
- signal: options.signal ?? void 0,
2738
- ...requestHeaders
2787
+ signal: options.signal ?? void 0
2739
2788
  });
2740
2789
  } catch (err) {
2741
2790
  throw toError2(err, providerName);
@@ -2776,6 +2825,8 @@ async function* runStream2(options) {
2776
2825
  thinkingAccum += reasoning.text;
2777
2826
  if (options.thinking) {
2778
2827
  yield { type: "thinking_delta", text: reasoning.text };
2828
+ } else {
2829
+ yield { type: "keepalive", reasoning: true };
2779
2830
  }
2780
2831
  }
2781
2832
  if (delta.content) {
@@ -3094,55 +3145,9 @@ function extractRequestIdFromMessage(message) {
3094
3145
  return match?.[1];
3095
3146
  }
3096
3147
 
3097
- // src/providers/openai-codex.ts
3098
- var DEFAULT_BASE_URL = "https://chatgpt.com/backend-api";
3099
- var CODEX_CLIENT_VERSION = "0.161.0";
3100
- var CODEX_REQUEST_COMPRESSION_MIN_BYTES = 16 * 1024;
3101
- var zstdInitPromise;
3102
- async function encodeCodexRequest(body) {
3103
- const json = JSON.stringify(body);
3104
- const raw = new TextEncoder().encode(json);
3105
- if (raw.byteLength < CODEX_REQUEST_COMPRESSION_MIN_BYTES) {
3106
- return {
3107
- body: json,
3108
- compressed: false,
3109
- rawBytes: raw.byteLength,
3110
- encodedBytes: raw.byteLength
3111
- };
3112
- }
3113
- try {
3114
- zstdInitPromise ??= zstd.init();
3115
- await zstdInitPromise;
3116
- const compressed = Uint8Array.from(zstd.compress(raw));
3117
- if (compressed.byteLength >= raw.byteLength) {
3118
- return {
3119
- body: json,
3120
- compressed: false,
3121
- rawBytes: raw.byteLength,
3122
- encodedBytes: raw.byteLength
3123
- };
3124
- }
3125
- return {
3126
- body: compressed,
3127
- compressed: true,
3128
- rawBytes: raw.byteLength,
3129
- encodedBytes: compressed.byteLength
3130
- };
3131
- } catch (error) {
3132
- providerDiag("codex_request_compression_failed", {
3133
- error: error instanceof Error ? error.message : String(error),
3134
- rawBytes: raw.byteLength
3135
- });
3136
- return {
3137
- body: json,
3138
- compressed: false,
3139
- rawBytes: raw.byteLength,
3140
- encodedBytes: raw.byteLength
3141
- };
3142
- }
3143
- }
3144
- function usesResponsesLite(model) {
3145
- return model.startsWith("gpt-5.6-") || model.startsWith("gpt-6-") || model.startsWith("gpt-6.");
3148
+ // src/providers/responses-stream.ts
3149
+ function isEncryptedReasoning(data) {
3150
+ return data.type === "reasoning" && typeof data.id === "string" && typeof data.encrypted_content === "string";
3146
3151
  }
3147
3152
  function outputTextKey(itemId, contentIndex) {
3148
3153
  return `${itemId ?? ""}:${contentIndex ?? 0}`;
@@ -3150,132 +3155,7 @@ function outputTextKey(itemId, contentIndex) {
3150
3155
  function isVisibleOutputItem(itemType) {
3151
3156
  return itemType === "message";
3152
3157
  }
3153
- function toCodexToolChoice(choice, tools) {
3154
- const resolved = choice ?? "auto";
3155
- if (typeof resolved === "object") {
3156
- throw new GGAIError(
3157
- `OpenAI Codex does not support selecting the named tool \`${resolved.name}\`; use auto, none, or required.`,
3158
- { source: "capability" }
3159
- );
3160
- }
3161
- if (resolved === "required" && !tools?.length) {
3162
- throw new GGAIError("OpenAI Codex cannot require a tool call when no tools are configured.", {
3163
- source: "capability"
3164
- });
3165
- }
3166
- return resolved;
3167
- }
3168
- function streamOpenAICodex(options) {
3169
- return new StreamResult(runStream3(options), options.signal);
3170
- }
3171
- async function* runStream3(options, retriedWithoutReasoning = false) {
3172
- const baseUrl = (options.baseUrl || DEFAULT_BASE_URL).replace(/\/+$/, "");
3173
- const url = `${baseUrl}/codex/responses`;
3174
- const downgradedImages = downgradeUnsupportedImages(options.messages, options.supportsImages);
3175
- const downgraded = downgradeUnsupportedVideos(downgradedImages, options.supportsVideo);
3176
- const { system, input } = toCodexInput(downgraded, { supportsImages: options.supportsImages });
3177
- const responsesLite = usesResponsesLite(options.model);
3178
- const liteShape = options.responsesLite ?? responsesLite;
3179
- const body = {
3180
- model: options.model,
3181
- store: false,
3182
- stream: true,
3183
- instructions: system,
3184
- input,
3185
- tool_choice: toCodexToolChoice(options.toolChoice, options.tools),
3186
- parallel_tool_calls: !liteShape,
3187
- include: ["reasoning.encrypted_content"]
3188
- };
3189
- if (options.tools?.length) {
3190
- body.tools = toCodexTools(options.tools, options.strictTools ?? true);
3191
- }
3192
- body.prompt_cache_key = normalizePromptCacheKey(options.promptCacheKey ?? "ggcoder");
3193
- if (options.temperature != null && !options.thinking) {
3194
- body.temperature = options.temperature;
3195
- }
3196
- if (options.speed === "fast") {
3197
- body.service_tier = "priority";
3198
- }
3199
- body.reasoning = {
3200
- // GPT-5.6/6 require at least low; older models still support thinking off.
3201
- // Apply the floor here for every caller, including one-off prompt rewrites.
3202
- // `ultra` is a client orchestration preset, not a Codex API effort.
3203
- effort: options.thinking ? toCodexReasoningEffort(options.thinking, options.model) : responsesLite ? "low" : "none",
3204
- summary: "auto",
3205
- ...liteShape ? { context: "all_turns" } : {}
3206
- };
3207
- if (responsesLite) {
3208
- body.text = { verbosity: "low" };
3209
- }
3210
- const headers = {
3211
- "Content-Type": "application/json",
3212
- Accept: "text/event-stream",
3213
- Authorization: `Bearer ${options.apiKey}`,
3214
- "OpenAI-Beta": "responses=experimental",
3215
- originator: responsesLite ? "codex_cli_rs" : "ggcoder",
3216
- "User-Agent": responsesLite ? `codex_cli_rs/${CODEX_CLIENT_VERSION}` : `ggcoder (${import_node_os.default.platform()} ${import_node_os.default.release()}; ${import_node_os.default.arch()})`,
3217
- ...responsesLite ? { version: CODEX_CLIENT_VERSION } : {},
3218
- ...liteShape ? { "X-OpenAI-Internal-Codex-Responses-Lite": "true" } : {}
3219
- };
3220
- if (options.accountId) {
3221
- headers["chatgpt-account-id"] = options.accountId;
3222
- }
3223
- if (options.transportSessionId) {
3224
- const transportSessionId = normalizePromptCacheKey(options.transportSessionId);
3225
- headers["session_id"] = transportSessionId;
3226
- headers["x-client-request-id"] = transportSessionId;
3227
- }
3228
- const encodedRequest = await encodeCodexRequest(
3229
- mergeProviderOptions(body, options.providerOptions)
3230
- );
3231
- if (encodedRequest.compressed) headers["Content-Encoding"] = "zstd";
3232
- providerDiag("codex_request_body", {
3233
- rawBytes: encodedRequest.rawBytes,
3234
- encodedBytes: encodedRequest.encodedBytes,
3235
- compressed: encodedRequest.compressed
3236
- });
3237
- const response = await fetch(url, {
3238
- method: "POST",
3239
- headers,
3240
- body: encodedRequest.body,
3241
- signal: options.signal
3242
- });
3243
- if (!response.ok) {
3244
- const text = await response.text().catch(() => "");
3245
- const parsed = parseCodexErrorBody(text, response.status);
3246
- const message = parsed.message ?? `Codex API returned HTTP ${response.status}.`;
3247
- const requestId = parsed.requestId ?? readHeader(response.headers, "x-request-id", "openai-request-id", "x-oai-request-id");
3248
- if (!retriedWithoutReasoning && !options.signal?.aborted && response.status === 400 && (parsed.errorObj?.code === "invalid_encrypted_content" || /encrypted content.*could not be (?:verified|decrypted|parsed)/i.test(message)) && options.messages.some(
3249
- (msg) => msg.role === "assistant" && Array.isArray(msg.content) && msg.content.some((part) => part.type === "raw" && isEncryptedReasoning(part.data))
3250
- )) {
3251
- providerDiag("codex_retry_without_encrypted_reasoning", { status: response.status });
3252
- const messages = options.messages.map(
3253
- (msg) => msg.role === "assistant" && Array.isArray(msg.content) ? {
3254
- ...msg,
3255
- content: msg.content.filter(
3256
- (part) => !(part.type === "raw" && isEncryptedReasoning(part.data))
3257
- )
3258
- } : msg
3259
- );
3260
- return yield* runStream3({ ...options, messages }, true);
3261
- }
3262
- const usageLimit = codexUsageLimitError(parsed.errorObj, response.status, requestId);
3263
- if (usageLimit) throw usageLimit;
3264
- let hint;
3265
- if (response.status === 400 && message === `The '${options.model}' model is not supported when using Codex with a ChatGPT account.`) {
3266
- hint = "This model is not available through your ChatGPT account. Choose another available model using the model selector.";
3267
- } else if (response.status === 404 && text.includes("does not exist")) {
3268
- hint = "This model is not in OpenAI's current catalog for your ChatGPT account. Choose another available model using the model selector.";
3269
- }
3270
- throw new ProviderError("openai", message, {
3271
- statusCode: response.status,
3272
- ...requestId ? { requestId } : {},
3273
- ...hint ? { hint } : {}
3274
- });
3275
- }
3276
- if (!response.body) {
3277
- throw new ProviderError("openai", "No response body from Codex API");
3278
- }
3158
+ async function* consumeResponsesEvents(events, ctx) {
3279
3159
  const contentParts = [];
3280
3160
  let textAccum = "";
3281
3161
  const toolCalls = /* @__PURE__ */ new Map();
@@ -3292,34 +3172,30 @@ async function* runStream3(options, retriedWithoutReasoning = false) {
3292
3172
  let thinkingTextEmitted = false;
3293
3173
  const diagStart = Date.now();
3294
3174
  const diagSeen = /* @__PURE__ */ new Set();
3295
- for await (const event of parseSSE(response.body)) {
3175
+ for await (const event of events) {
3296
3176
  const type = event.type;
3297
3177
  if (!type) continue;
3298
3178
  if (!diagSeen.has(type)) {
3299
3179
  diagSeen.add(type);
3300
- providerDiag("codex_event_first", { type, sinceStartMs: Date.now() - diagStart });
3180
+ providerDiag(`${ctx.diagPrefix}_event_first`, { type, sinceStartMs: Date.now() - diagStart });
3301
3181
  }
3302
3182
  if (type === "error") {
3303
3183
  const nested = event.error ?? void 0;
3304
- const message = nested?.message ?? event.message ?? "Codex stream emitted an error chunk without a message.";
3184
+ const message = nested?.message ?? event.message ?? `${ctx.label} stream emitted an error chunk without a message.`;
3305
3185
  const code = nested?.code ?? nested?.type ?? event.code ?? "server_error";
3306
3186
  const requestId = extractRequestIdFromMessage(message) ?? event.request_id;
3307
- const usageLimit = codexUsageLimitError(
3308
- nested ?? event,
3309
- void 0,
3310
- requestId
3311
- );
3187
+ const usageLimit = ctx.errorChunk?.(nested ?? event, requestId);
3312
3188
  if (usageLimit) throw usageLimit;
3313
- throw new ProviderError("openai", message, {
3189
+ throw new ProviderError(ctx.provider, message, {
3314
3190
  ...requestId != null ? { requestId } : {},
3315
3191
  ...code === "server_error" ? { statusCode: 500 } : {}
3316
3192
  });
3317
3193
  }
3318
3194
  if (type === "response.failed") {
3319
3195
  const nested = event.error;
3320
- const message = nested?.message ?? "Codex response failed.";
3196
+ const message = nested?.message ?? `${ctx.label} response failed.`;
3321
3197
  const requestId = extractRequestIdFromMessage(message) ?? event.request_id;
3322
- throw new ProviderError("openai", message, {
3198
+ throw new ProviderError(ctx.provider, message, {
3323
3199
  ...requestId != null ? { requestId } : {}
3324
3200
  });
3325
3201
  }
@@ -3368,15 +3244,17 @@ async function* runStream3(options, retriedWithoutReasoning = false) {
3368
3244
  }
3369
3245
  }
3370
3246
  if (type === "response.reasoning_summary_part.added") {
3371
- if (options.thinking && thinkingTextEmitted) {
3247
+ if (ctx.thinking && thinkingTextEmitted) {
3372
3248
  yield { type: "thinking_delta", text: "\n\n" };
3373
3249
  }
3374
3250
  }
3375
3251
  if (type === "response.reasoning_summary_text.delta" || type === "response.reasoning_summary.delta" || type === "response.reasoning_text.delta" || type === "response.reasoning.delta") {
3376
3252
  const delta = event.delta;
3377
- if (options.thinking && delta) {
3253
+ if (ctx.thinking && delta) {
3378
3254
  thinkingTextEmitted = true;
3379
3255
  yield { type: "thinking_delta", text: delta };
3256
+ } else if (delta) {
3257
+ yield { type: "keepalive", reasoning: true };
3380
3258
  }
3381
3259
  }
3382
3260
  if (type === "response.output_item.added") {
@@ -3386,8 +3264,8 @@ async function* runStream3(options, retriedWithoutReasoning = false) {
3386
3264
  if (itemId && itemType) {
3387
3265
  outputItemTypes.set(itemId, itemType);
3388
3266
  }
3389
- if (itemType === "reasoning" && options.thinking) {
3390
- yield { type: "thinking_delta", text: "" };
3267
+ if (itemType === "reasoning") {
3268
+ yield ctx.thinking ? { type: "thinking_delta", text: "" } : { type: "keepalive", reasoning: true };
3391
3269
  }
3392
3270
  if (itemId && itemType) {
3393
3271
  const pending = [...pendingOutputTextByPart.entries()].filter(([, pendingPart]) => pendingPart.itemId === itemId).sort(([, a], [, b]) => a.contentIndex - b.contentIndex);
@@ -3448,7 +3326,8 @@ async function* runStream3(options, retriedWithoutReasoning = false) {
3448
3326
  kind: "reasoning",
3449
3327
  part: {
3450
3328
  type: "raw",
3451
- data: { ...item, summary: Array.isArray(item.summary) ? item.summary : [] }
3329
+ data: { ...item, summary: Array.isArray(item.summary) ? item.summary : [] },
3330
+ ...ctx.reasoningOrigin ? { origin: ctx.reasoningOrigin } : {}
3452
3331
  }
3453
3332
  });
3454
3333
  }
@@ -3457,6 +3336,13 @@ async function* runStream3(options, retriedWithoutReasoning = false) {
3457
3336
  const callId = item.call_id;
3458
3337
  const itemId = item.id;
3459
3338
  const id = `${callId}|${itemId}`;
3339
+ if (!toolCalls.has(id) && typeof item.name === "string") {
3340
+ toolCalls.set(id, {
3341
+ id,
3342
+ name: item.name,
3343
+ argsJson: typeof item.arguments === "string" ? item.arguments : ""
3344
+ });
3345
+ }
3460
3346
  const tc = toolCalls.get(id);
3461
3347
  if (tc) {
3462
3348
  finishedToolCalls.add(id);
@@ -3482,6 +3368,23 @@ async function* runStream3(options, retriedWithoutReasoning = false) {
3482
3368
  } else {
3483
3369
  terminal = { status: "completed" };
3484
3370
  }
3371
+ if (Array.isArray(resp?.output)) {
3372
+ for (const raw of resp.output) {
3373
+ const item = raw;
3374
+ if (typeof item?.id !== "string" || typeof item.type !== "string") continue;
3375
+ if (!outputItemTypes.has(item.id)) outputItemTypes.set(item.id, item.type);
3376
+ }
3377
+ const pending = [...pendingOutputTextByPart.entries()].sort(
3378
+ ([, a], [, b]) => a.contentIndex - b.contentIndex
3379
+ );
3380
+ for (const [key, pendingPart] of pending) {
3381
+ if (!isVisibleOutputItem(outputItemTypes.get(pendingPart.itemId))) continue;
3382
+ pendingOutputTextByPart.delete(key);
3383
+ if (!pendingPart.text) continue;
3384
+ textAccum += pendingPart.text;
3385
+ yield { type: "text_delta", text: pendingPart.text };
3386
+ }
3387
+ }
3485
3388
  const usage = resp?.usage;
3486
3389
  if (usage) {
3487
3390
  cacheRead = usage.input_tokens_details?.cached_tokens ?? 0;
@@ -3492,23 +3395,30 @@ async function* runStream3(options, retriedWithoutReasoning = false) {
3492
3395
  }
3493
3396
  }
3494
3397
  if (!terminal) {
3495
- throw new ProviderError("openai", "Stream ended before completion (no response.completed).", {
3496
- statusCode: 504
3497
- });
3398
+ throw new ProviderError(
3399
+ ctx.provider,
3400
+ "Stream ended before completion (no response.completed).",
3401
+ {
3402
+ statusCode: 504
3403
+ }
3404
+ );
3498
3405
  }
3499
3406
  const droppedToolCalls = [...toolCalls.keys()].filter((id) => !finishedToolCalls.has(id)).length;
3500
3407
  if (terminal.status === "completed") {
3501
3408
  for (const [id, tc] of toolCalls) {
3502
3409
  if (!finishedToolCalls.has(id)) {
3503
3410
  throw new ProviderError(
3504
- "openai",
3505
- `Codex reply completed with an unfinished tool call: ${tc.name} (${id}).`,
3411
+ ctx.provider,
3412
+ `${ctx.label} reply completed with an unfinished tool call: ${tc.name} (${id}).`,
3506
3413
  { statusCode: 502 }
3507
3414
  );
3508
3415
  }
3509
3416
  }
3510
3417
  } else {
3511
- providerDiag("codex_incomplete", { reason: terminal.reason ?? null, droppedToolCalls });
3418
+ providerDiag(`${ctx.diagPrefix}_incomplete`, {
3419
+ reason: terminal.reason ?? null,
3420
+ droppedToolCalls
3421
+ });
3512
3422
  }
3513
3423
  const seenTool = /* @__PURE__ */ new Set();
3514
3424
  let textInserted = false;
@@ -3575,34 +3485,14 @@ function incompleteStopReason(reason) {
3575
3485
  if (reason === "content_filter") return "refusal";
3576
3486
  return "error";
3577
3487
  }
3578
- async function* parseSSE(body) {
3579
- for await (const event of readSseStream(body)) {
3580
- const data = event.data.trim();
3581
- if (!data || data === "[DONE]") continue;
3582
- try {
3583
- yield JSON.parse(data);
3584
- } catch {
3585
- }
3586
- }
3587
- }
3588
- function remapCodexId(id, idMap) {
3589
- const existing = idMap.get(id);
3590
- if (existing) return existing;
3591
- const withPrefix = id.startsWith("fc_") || id.startsWith("fc-") ? id : `fc_${id.replace(/^toolu_/, "")}`;
3592
- const sanitized = withPrefix.replace(/[^A-Za-z0-9_-]/g, "_");
3593
- let mapped = sanitized;
3594
- let suffix = 2;
3595
- const used = new Set(idMap.values());
3596
- while (used.has(mapped)) {
3597
- mapped = `${sanitized}_${suffix++}`;
3598
- }
3599
- idMap.set(id, mapped);
3600
- return mapped;
3601
- }
3602
- function isEncryptedReasoning(data) {
3603
- return data.type === "reasoning" && typeof data.id === "string" && typeof data.encrypted_content === "string";
3604
- }
3605
- function toCodexInput(messages, options) {
3488
+ function toResponsesInput(messages, options) {
3489
+ const mapId = options.mapId ?? ((id) => id);
3490
+ const assistantText = (text) => options.minimalAssistantItems ? { role: "assistant", content: text } : {
3491
+ type: "message",
3492
+ role: "assistant",
3493
+ content: [{ type: "output_text", text, annotations: [] }],
3494
+ status: "completed"
3495
+ };
3606
3496
  let system;
3607
3497
  const input = [];
3608
3498
  const idMap = /* @__PURE__ */ new Map();
@@ -3625,30 +3515,23 @@ function toCodexInput(messages, options) {
3625
3515
  }
3626
3516
  if (msg.role === "assistant") {
3627
3517
  if (typeof msg.content === "string") {
3628
- input.push({
3629
- type: "message",
3630
- role: "assistant",
3631
- content: [{ type: "output_text", text: msg.content, annotations: [] }],
3632
- status: "completed"
3633
- });
3518
+ input.push(assistantText(msg.content));
3634
3519
  continue;
3635
3520
  }
3636
3521
  for (const part of msg.content) {
3637
3522
  if (part.type === "raw" && isEncryptedReasoning(part.data)) {
3523
+ if (!options.keepReasoning(part)) continue;
3638
3524
  input.push(part.data);
3639
3525
  } else if (part.type === "text") {
3640
- input.push({
3641
- type: "message",
3642
- role: "assistant",
3643
- content: [{ type: "output_text", text: part.text, annotations: [] }],
3644
- status: "completed"
3645
- });
3526
+ input.push(assistantText(part.text));
3646
3527
  } else if (part.type === "tool_call") {
3647
- const [callId, itemId] = part.id.includes("|") ? part.id.split("|", 2) : [part.id, part.id];
3528
+ const native = part.id.includes("|");
3529
+ const [callId, itemId] = native ? part.id.split("|", 2) : [part.id, part.id];
3530
+ const sendItemId = !options.minimalAssistantItems || native && !!itemId && itemId !== "undefined";
3648
3531
  input.push({
3649
3532
  type: "function_call",
3650
- id: remapCodexId(itemId, idMap),
3651
- call_id: remapCodexId(callId, idMap),
3533
+ ...sendItemId ? { id: mapId(itemId ?? callId, idMap) } : {},
3534
+ call_id: mapId(callId, idMap),
3652
3535
  name: part.name,
3653
3536
  arguments: JSON.stringify(part.args)
3654
3537
  });
@@ -3663,10 +3546,10 @@ function toCodexInput(messages, options) {
3663
3546
  const text = toolResultText(result.content);
3664
3547
  input.push({
3665
3548
  type: "function_call_output",
3666
- call_id: remapCodexId(callId, idMap),
3549
+ call_id: mapId(callId, idMap),
3667
3550
  output: text.length > 0 ? text : "(see attached image)"
3668
3551
  });
3669
- if (options?.supportsImages !== false && Array.isArray(result.content)) {
3552
+ if (options.supportsImages !== false && Array.isArray(result.content)) {
3670
3553
  for (const block of result.content) {
3671
3554
  if (block.type === "image") toolImages.push(block);
3672
3555
  }
@@ -3690,6 +3573,221 @@ function toCodexInput(messages, options) {
3690
3573
  }
3691
3574
  return { system, input };
3692
3575
  }
3576
+
3577
+ // src/providers/openai-codex.ts
3578
+ var DEFAULT_BASE_URL = "https://chatgpt.com/backend-api";
3579
+ var CODEX_CLIENT_VERSION = "0.161.0";
3580
+ var CODEX_REQUEST_COMPRESSION_MIN_BYTES = 16 * 1024;
3581
+ var zstdInitPromise;
3582
+ async function encodeCodexRequest(body) {
3583
+ const json = JSON.stringify(body);
3584
+ const raw = new TextEncoder().encode(json);
3585
+ if (raw.byteLength < CODEX_REQUEST_COMPRESSION_MIN_BYTES) {
3586
+ return {
3587
+ body: json,
3588
+ compressed: false,
3589
+ rawBytes: raw.byteLength,
3590
+ encodedBytes: raw.byteLength
3591
+ };
3592
+ }
3593
+ try {
3594
+ zstdInitPromise ??= zstd.init();
3595
+ await zstdInitPromise;
3596
+ const compressed = Uint8Array.from(zstd.compress(raw));
3597
+ if (compressed.byteLength >= raw.byteLength) {
3598
+ return {
3599
+ body: json,
3600
+ compressed: false,
3601
+ rawBytes: raw.byteLength,
3602
+ encodedBytes: raw.byteLength
3603
+ };
3604
+ }
3605
+ return {
3606
+ body: compressed,
3607
+ compressed: true,
3608
+ rawBytes: raw.byteLength,
3609
+ encodedBytes: compressed.byteLength
3610
+ };
3611
+ } catch (error) {
3612
+ providerDiag("codex_request_compression_failed", {
3613
+ error: error instanceof Error ? error.message : String(error),
3614
+ rawBytes: raw.byteLength
3615
+ });
3616
+ return {
3617
+ body: json,
3618
+ compressed: false,
3619
+ rawBytes: raw.byteLength,
3620
+ encodedBytes: raw.byteLength
3621
+ };
3622
+ }
3623
+ }
3624
+ function usesResponsesLite(model) {
3625
+ return model.startsWith("gpt-5.6-") || model.startsWith("gpt-6-") || model.startsWith("gpt-6.");
3626
+ }
3627
+ function toCodexToolChoice(choice, tools) {
3628
+ const resolved = choice ?? "auto";
3629
+ if (typeof resolved === "object") {
3630
+ throw new GGAIError(
3631
+ `OpenAI Codex does not support selecting the named tool \`${resolved.name}\`; use auto, none, or required.`,
3632
+ { source: "capability" }
3633
+ );
3634
+ }
3635
+ if (resolved === "required" && !tools?.length) {
3636
+ throw new GGAIError("OpenAI Codex cannot require a tool call when no tools are configured.", {
3637
+ source: "capability"
3638
+ });
3639
+ }
3640
+ return resolved;
3641
+ }
3642
+ function streamOpenAICodex(options) {
3643
+ return new StreamResult(runStream3(options), options.signal);
3644
+ }
3645
+ async function* runStream3(options, retriedWithoutReasoning = false) {
3646
+ const baseUrl = (options.baseUrl || DEFAULT_BASE_URL).replace(/\/+$/, "");
3647
+ const url = `${baseUrl}/codex/responses`;
3648
+ const downgradedImages = downgradeUnsupportedImages(options.messages, options.supportsImages);
3649
+ const downgraded = downgradeUnsupportedVideos(downgradedImages, options.supportsVideo);
3650
+ const { system, input } = toResponsesInput(downgraded, {
3651
+ supportsImages: options.supportsImages,
3652
+ mapId: remapCodexId,
3653
+ // Stamped reasoning belongs to another transport (e.g. Grok's) and would
3654
+ // be rejected here; Codex's own items are never stamped.
3655
+ keepReasoning: (part) => part.origin === void 0
3656
+ });
3657
+ const responsesLite = usesResponsesLite(options.model);
3658
+ const liteShape = options.responsesLite ?? responsesLite;
3659
+ const body = {
3660
+ model: options.model,
3661
+ store: false,
3662
+ stream: true,
3663
+ instructions: system,
3664
+ input,
3665
+ tool_choice: toCodexToolChoice(options.toolChoice, options.tools),
3666
+ parallel_tool_calls: !liteShape,
3667
+ include: ["reasoning.encrypted_content"]
3668
+ };
3669
+ if (options.tools?.length) {
3670
+ body.tools = toCodexTools(options.tools, options.strictTools ?? true);
3671
+ }
3672
+ body.prompt_cache_key = normalizePromptCacheKey(options.promptCacheKey ?? "ggcoder");
3673
+ if (options.temperature != null && !options.thinking) {
3674
+ body.temperature = options.temperature;
3675
+ }
3676
+ if (options.speed === "fast") {
3677
+ body.service_tier = "priority";
3678
+ }
3679
+ body.reasoning = {
3680
+ // GPT-5.6/6 require at least low; older models still support thinking off.
3681
+ // Apply the floor here for every caller, including one-off prompt rewrites.
3682
+ // `ultra` is a client orchestration preset, not a Codex API effort.
3683
+ effort: options.thinking ? toCodexReasoningEffort(options.thinking, options.model) : responsesLite ? "low" : "none",
3684
+ summary: "auto",
3685
+ ...liteShape ? { context: "all_turns" } : {}
3686
+ };
3687
+ if (responsesLite) {
3688
+ body.text = { verbosity: "low" };
3689
+ }
3690
+ const headers = {
3691
+ "Content-Type": "application/json",
3692
+ Accept: "text/event-stream",
3693
+ Authorization: `Bearer ${options.apiKey}`,
3694
+ "OpenAI-Beta": "responses=experimental",
3695
+ originator: responsesLite ? "codex_cli_rs" : "ggcoder",
3696
+ "User-Agent": responsesLite ? `codex_cli_rs/${CODEX_CLIENT_VERSION}` : `ggcoder (${import_node_os.default.platform()} ${import_node_os.default.release()}; ${import_node_os.default.arch()})`,
3697
+ ...responsesLite ? { version: CODEX_CLIENT_VERSION } : {},
3698
+ ...liteShape ? { "X-OpenAI-Internal-Codex-Responses-Lite": "true" } : {}
3699
+ };
3700
+ if (options.accountId) {
3701
+ headers["chatgpt-account-id"] = options.accountId;
3702
+ }
3703
+ if (options.transportSessionId) {
3704
+ const transportSessionId = normalizePromptCacheKey(options.transportSessionId);
3705
+ headers["session_id"] = transportSessionId;
3706
+ headers["x-client-request-id"] = transportSessionId;
3707
+ }
3708
+ const encodedRequest = await encodeCodexRequest(
3709
+ mergeProviderOptions(body, options.providerOptions)
3710
+ );
3711
+ if (encodedRequest.compressed) headers["Content-Encoding"] = "zstd";
3712
+ providerDiag("codex_request_body", {
3713
+ rawBytes: encodedRequest.rawBytes,
3714
+ encodedBytes: encodedRequest.encodedBytes,
3715
+ compressed: encodedRequest.compressed
3716
+ });
3717
+ const response = await providerFetch(url, {
3718
+ method: "POST",
3719
+ headers,
3720
+ body: encodedRequest.body,
3721
+ signal: options.signal
3722
+ });
3723
+ if (!response.ok) {
3724
+ const text = await response.text().catch(() => "");
3725
+ const parsed = parseCodexErrorBody(text, response.status);
3726
+ const message = parsed.message ?? `Codex API returned HTTP ${response.status}.`;
3727
+ const requestId = parsed.requestId ?? readHeader(response.headers, "x-request-id", "openai-request-id", "x-oai-request-id");
3728
+ if (!retriedWithoutReasoning && !options.signal?.aborted && response.status === 400 && (parsed.errorObj?.code === "invalid_encrypted_content" || /encrypted content.*could not be (?:verified|decrypted|parsed)/i.test(message)) && options.messages.some(
3729
+ (msg) => msg.role === "assistant" && Array.isArray(msg.content) && msg.content.some((part) => part.type === "raw" && isEncryptedReasoning(part.data))
3730
+ )) {
3731
+ providerDiag("codex_retry_without_encrypted_reasoning", { status: response.status });
3732
+ const messages = options.messages.map(
3733
+ (msg) => msg.role === "assistant" && Array.isArray(msg.content) ? {
3734
+ ...msg,
3735
+ content: msg.content.filter(
3736
+ (part) => !(part.type === "raw" && isEncryptedReasoning(part.data))
3737
+ )
3738
+ } : msg
3739
+ );
3740
+ return yield* runStream3({ ...options, messages }, true);
3741
+ }
3742
+ const usageLimit = codexUsageLimitError(parsed.errorObj, response.status, requestId);
3743
+ if (usageLimit) throw usageLimit;
3744
+ let hint;
3745
+ if (response.status === 400 && message === `The '${options.model}' model is not supported when using Codex with a ChatGPT account.`) {
3746
+ hint = "This model is not available through your ChatGPT account. Choose another available model using the model selector.";
3747
+ } else if (response.status === 404 && text.includes("does not exist")) {
3748
+ hint = "This model is not in OpenAI's current catalog for your ChatGPT account. Choose another available model using the model selector.";
3749
+ }
3750
+ throw new ProviderError("openai", message, {
3751
+ statusCode: response.status,
3752
+ ...requestId ? { requestId } : {},
3753
+ ...hint ? { hint } : {}
3754
+ });
3755
+ }
3756
+ if (!response.body) {
3757
+ throw new ProviderError("openai", "No response body from Codex API");
3758
+ }
3759
+ return yield* consumeResponsesEvents(parseSSE(response.body), {
3760
+ provider: "openai",
3761
+ label: "Codex",
3762
+ diagPrefix: "codex",
3763
+ thinking: !!options.thinking,
3764
+ errorChunk: (errorObj, requestId) => codexUsageLimitError(errorObj, void 0, requestId)
3765
+ });
3766
+ }
3767
+ async function* parseSSE(body) {
3768
+ for await (const event of readSseStream(body)) {
3769
+ const data = event.data.trim();
3770
+ if (!data || data === "[DONE]") continue;
3771
+ try {
3772
+ yield JSON.parse(data);
3773
+ } catch {
3774
+ }
3775
+ }
3776
+ }
3777
+ function remapCodexId(id, idMap) {
3778
+ const existing = idMap.get(id);
3779
+ if (existing) return existing;
3780
+ const withPrefix = id.startsWith("fc_") || id.startsWith("fc-") ? id : `fc_${id.replace(/^toolu_/, "")}`;
3781
+ const sanitized = withPrefix.replace(/[^A-Za-z0-9_-]/g, "_");
3782
+ let mapped = sanitized;
3783
+ let suffix = 2;
3784
+ const used = new Set(idMap.values());
3785
+ while (used.has(mapped)) {
3786
+ mapped = `${sanitized}_${suffix++}`;
3787
+ }
3788
+ idMap.set(id, mapped);
3789
+ return mapped;
3790
+ }
3693
3791
  function toCodexTools(tools, strictTools) {
3694
3792
  return tools.map((tool) => {
3695
3793
  let parameters = resolveToolSchema(tool);
@@ -3754,6 +3852,136 @@ function codexUsageLimitError(errorObj, statusCode, requestId) {
3754
3852
  });
3755
3853
  }
3756
3854
 
3855
+ // src/providers/xai-responses.ts
3856
+ function streamXaiResponses(options) {
3857
+ return new StreamResult(runStream4(options), options.signal);
3858
+ }
3859
+ function xaiReasoningOrigin(model) {
3860
+ return `xai:${model}`;
3861
+ }
3862
+ async function* runStream4(options, dropReasoning = false) {
3863
+ const providerName = options.provider ?? "xai";
3864
+ const client = createClient2(options);
3865
+ const origin = xaiReasoningOrigin(options.model);
3866
+ const useStreaming = options.streaming !== false;
3867
+ const downgradedImages = downgradeUnsupportedImages(options.messages, options.supportsImages);
3868
+ const downgraded = downgradeUnsupportedVideos(downgradedImages, options.supportsVideo);
3869
+ const { system, input } = toResponsesInput(downgraded, {
3870
+ supportsImages: options.supportsImages,
3871
+ keepReasoning: (part) => !dropReasoning && part.origin === origin,
3872
+ minimalAssistantItems: true
3873
+ });
3874
+ const replaysReasoning = input.some(isReasoningItem);
3875
+ const params = {
3876
+ model: options.model,
3877
+ input: system ? [{ role: "system", content: system }, ...input] : input,
3878
+ store: false,
3879
+ stream: useStreaming,
3880
+ include: ["reasoning.encrypted_content"],
3881
+ // Responses' cache-routing key (Chat used the x-grok-conv-id header).
3882
+ prompt_cache_key: normalizePromptCacheKey(options.promptCacheKey ?? "ggcoder"),
3883
+ ...options.maxTokens ? { max_output_tokens: options.maxTokens } : {},
3884
+ // Grok reasoning can't be disabled; with thinking off the server default
3885
+ // (high on Grok 4.7) applies, exactly as on the Chat path.
3886
+ ...options.thinking ? { reasoning: { effort: toOpenAIReasoningEffort(options.thinking, options.model) } } : {},
3887
+ ...options.temperature != null && !options.thinking ? { temperature: options.temperature } : {},
3888
+ ...options.topP != null ? { top_p: options.topP } : {},
3889
+ ...options.tools?.length ? { tools: toXaiTools(options.tools) } : {},
3890
+ ...options.toolChoice && options.tools?.length ? { tool_choice: toXaiToolChoice(options.toolChoice) } : {}
3891
+ };
3892
+ const body = mergeProviderOptions(params, options.providerOptions);
3893
+ const requestOptions = {
3894
+ signal: options.signal ?? void 0,
3895
+ // A non-streamed reply arrives in one response; never undercut the agent
3896
+ // loop's own (up to 15min) cap with the SDK's 10min default.
3897
+ timeout: PROVIDER_TRANSPORT_TIMEOUT_MS,
3898
+ headers: {
3899
+ "x-grok-conv-id": normalizePromptCacheKey(options.promptCacheKey ?? "ggcoder")
3900
+ }
3901
+ };
3902
+ let events;
3903
+ try {
3904
+ if (useStreaming) {
3905
+ const stream2 = await client.responses.create(
3906
+ body,
3907
+ requestOptions
3908
+ );
3909
+ events = stream2;
3910
+ } else {
3911
+ const response = await client.responses.create(
3912
+ body,
3913
+ requestOptions
3914
+ );
3915
+ events = synthesizeResponseEvents(response);
3916
+ }
3917
+ } catch (err) {
3918
+ if (replaysReasoning && !dropReasoning && !options.signal?.aborted && err.status === 400) {
3919
+ providerDiag("xai_retry_without_encrypted_reasoning", { status: 400 });
3920
+ return yield* runStream4(options, true);
3921
+ }
3922
+ throw toError2(err, providerName);
3923
+ }
3924
+ try {
3925
+ return yield* consumeResponsesEvents(events, {
3926
+ provider: providerName,
3927
+ label: "Grok",
3928
+ diagPrefix: "xai",
3929
+ thinking: !!options.thinking,
3930
+ reasoningOrigin: origin
3931
+ });
3932
+ } catch (err) {
3933
+ throw toError2(err, providerName);
3934
+ }
3935
+ }
3936
+ function isReasoningItem(item) {
3937
+ return typeof item === "object" && item !== null && isEncryptedReasoning(item);
3938
+ }
3939
+ function toXaiTools(tools) {
3940
+ return tools.map((tool) => ({
3941
+ type: "function",
3942
+ name: tool.name,
3943
+ description: tool.description,
3944
+ parameters: resolveToolSchema(tool)
3945
+ }));
3946
+ }
3947
+ function toXaiToolChoice(choice) {
3948
+ return typeof choice === "object" ? { type: "function", name: choice.name } : choice;
3949
+ }
3950
+ async function* synthesizeResponseEvents(response) {
3951
+ const output = Array.isArray(response.output) ? response.output : [];
3952
+ for (const [index, raw] of output.entries()) {
3953
+ if (typeof raw !== "object" || raw === null) continue;
3954
+ const item = raw;
3955
+ const withId = typeof item.id === "string" ? item : { ...item, id: `item_${index}` };
3956
+ yield { type: "response.output_item.added", item: withId };
3957
+ if (withId.type === "message" && Array.isArray(withId.content)) {
3958
+ for (const [contentIndex, content] of withId.content.entries()) {
3959
+ const part = content;
3960
+ if (part?.type === "output_text" && typeof part.text === "string") {
3961
+ yield {
3962
+ type: "response.output_text.done",
3963
+ item_id: withId.id,
3964
+ content_index: contentIndex,
3965
+ text: part.text
3966
+ };
3967
+ }
3968
+ }
3969
+ }
3970
+ if (withId.type === "function_call") {
3971
+ yield {
3972
+ type: "response.function_call_arguments.done",
3973
+ item_id: withId.id,
3974
+ arguments: typeof withId.arguments === "string" ? withId.arguments : ""
3975
+ };
3976
+ }
3977
+ yield { type: "response.output_item.done", item: withId };
3978
+ }
3979
+ yield {
3980
+ type: response.status === "incomplete" ? "response.incomplete" : "response.completed",
3981
+ response
3982
+ };
3983
+ }
3984
+
3757
3985
  // src/providers/gemini.ts
3758
3986
  var DEFAULT_CODE_ASSIST_BASE_URL = "https://cloudcode-pa.googleapis.com";
3759
3987
  var CODE_ASSIST_API_VERSION = "v1internal";
@@ -4172,7 +4400,7 @@ async function sleep(ms, signal) {
4172
4400
  }
4173
4401
  async function fetchCodeAssist(plan, options) {
4174
4402
  try {
4175
- const response = await fetch(plan.url, {
4403
+ const response = await providerFetch(plan.url, {
4176
4404
  method: "POST",
4177
4405
  headers: plan.headers,
4178
4406
  body: JSON.stringify(plan.body),
@@ -4225,9 +4453,9 @@ async function fetchCodeAssistWithRetry(plan, options) {
4225
4453
  throw lastError ?? new ProviderError("gemini", "Gemini Code Assist request failed.");
4226
4454
  }
4227
4455
  function streamGemini(options) {
4228
- return new StreamResult(runStream4(options), options.signal);
4456
+ return new StreamResult(runStream5(options), options.signal);
4229
4457
  }
4230
- async function* runStream4(options) {
4458
+ async function* runStream5(options) {
4231
4459
  const useStreaming = options.streaming !== false;
4232
4460
  const method = useStreaming ? "streamGenerateContent" : "generateContent";
4233
4461
  const plan = buildRequestPlan(options, method);
@@ -5080,13 +5308,12 @@ providerRegistry.register("sakana", {
5080
5308
  })
5081
5309
  });
5082
5310
  providerRegistry.register("xai", {
5083
- // xAI's public API (console.x.ai key) is OpenAI-compatible — ride the Chat
5084
- // Completions transport like Moonshot/DeepSeek. Grok reasoning models take
5085
- // top-level `reasoning_effort` (low/medium/high/xhigh), which the shared thinking
5086
- // path already sends.
5311
+ // Both Grok routes ride xAI's Responses API (see providers/xai-responses.ts):
5312
+ // Chat Completions is deprecated for Grok reasoning models and can't carry
5313
+ // the encrypted reasoning that keeps long tool loops coherent.
5087
5314
  //
5088
5315
  // Subscription OAuth (SuperGrok / X Premium) routes to the Grok CLI chat proxy
5089
- // instead, which speaks the same Chat Completions wire but gates on Grok-CLI
5316
+ // instead, which serves the same `/v1/responses` surface but gates on Grok-CLI
5090
5317
  // client identity. Inject those headers centrally here — exactly as the Kimi
5091
5318
  // endpoint above — so EVERY stream (agent loop, compaction, title-gen,
5092
5319
  // sub-agents) is accepted rather than depending on each call site to thread
@@ -5100,7 +5327,7 @@ providerRegistry.register("xai", {
5100
5327
  "x-grok-model-override": options.model,
5101
5328
  ...options.defaultHeaders
5102
5329
  } : options.defaultHeaders;
5103
- return streamOpenAI({ ...options, baseUrl, defaultHeaders });
5330
+ return streamXaiResponses({ ...options, baseUrl, defaultHeaders });
5104
5331
  }
5105
5332
  });
5106
5333
  providerRegistry.register("minimax", {
@@ -5420,6 +5647,13 @@ function formatChatError(error) {
5420
5647
  "Wait for the reset, or use another provider."
5421
5648
  );
5422
5649
  }
5650
+ if (error instanceof ResponseStalledError) {
5651
+ return present(
5652
+ "network",
5653
+ "The response kept stalling",
5654
+ error.inToolCall ? "Ask for the change in smaller pieces, then try again." : "Try again, or choose another model if it keeps happening."
5655
+ );
5656
+ }
5423
5657
  if (base.source === "network") {
5424
5658
  return present(
5425
5659
  "network",
@@ -5801,6 +6035,7 @@ function palsuRerankScore(query, document) {
5801
6035
  GGAIError,
5802
6036
  ProviderError,
5803
6037
  REDACTION_MARKER,
6038
+ ResponseStalledError,
5804
6039
  StreamResult,
5805
6040
  TOOL_CALL_NAME_RULES,
5806
6041
  clampProviderContextImages,