@bitkyc08/opencodex 2.6.32 → 2.7.1-preview.20260710

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.
Files changed (61) hide show
  1. package/README.ko.md +9 -5
  2. package/README.md +7 -4
  3. package/README.zh-CN.md +8 -4
  4. package/gui/dist/assets/index-BUAMcKFd.css +1 -0
  5. package/gui/dist/assets/index-KorpEKW8.js +34 -0
  6. package/gui/dist/index.html +2 -2
  7. package/package.json +1 -1
  8. package/src/adapters/anthropic.ts +62 -1
  9. package/src/adapters/cursor/cursor-errors.ts +28 -1
  10. package/src/adapters/cursor/discovery.ts +60 -10
  11. package/src/adapters/cursor/effort-map.ts +38 -7
  12. package/src/adapters/cursor/live-models.ts +3 -0
  13. package/src/adapters/cursor/live-transport.ts +136 -7
  14. package/src/adapters/cursor/protobuf-request.ts +24 -1
  15. package/src/adapters/cursor/request-builder.ts +6 -5
  16. package/src/adapters/cursor/transport-retry.ts +22 -3
  17. package/src/adapters/cursor.ts +2 -1
  18. package/src/adapters/openai-chat.ts +75 -26
  19. package/src/bridge.ts +42 -3
  20. package/src/cli/debug.ts +203 -0
  21. package/src/cli/doctor.ts +11 -0
  22. package/src/cli/help.ts +11 -0
  23. package/src/cli/index.ts +10 -0
  24. package/src/cli/v2.ts +131 -0
  25. package/src/codex/auth-api.ts +7 -3
  26. package/src/codex/catalog.ts +334 -31
  27. package/src/codex/data/upstream-models.json +830 -0
  28. package/src/codex/features.ts +178 -0
  29. package/src/codex/project-config-warnings.ts +388 -0
  30. package/src/codex/sync.ts +8 -0
  31. package/src/codex/warmup.ts +62 -7
  32. package/src/config.ts +7 -5
  33. package/src/lib/debug-log-buffer.ts +42 -0
  34. package/src/lib/debug-settings.ts +84 -0
  35. package/src/lib/debug.ts +18 -9
  36. package/src/lib/errors.ts +104 -1
  37. package/src/oauth/cursor.ts +35 -12
  38. package/src/oauth/store.ts +4 -3
  39. package/src/providers/derive.ts +8 -0
  40. package/src/providers/registry.ts +56 -21
  41. package/src/reasoning-effort.ts +37 -9
  42. package/src/responses/parser.ts +7 -2
  43. package/src/router.ts +5 -0
  44. package/src/server/adapter-resolve.ts +1 -1
  45. package/src/server/index.ts +27 -3
  46. package/src/server/management-api.ts +189 -7
  47. package/src/server/relay.ts +2 -2
  48. package/src/server/request-decompress.ts +8 -2
  49. package/src/server/request-log.ts +78 -0
  50. package/src/server/responses.ts +241 -9
  51. package/src/types.ts +34 -1
  52. package/src/usage/debug.ts +32 -5
  53. package/src/usage/summary.ts +6 -6
  54. package/src/vision/describe.ts +4 -0
  55. package/src/web-search/executor.ts +4 -0
  56. package/src/web-search/format-result.ts +11 -3
  57. package/src/web-search/index.ts +31 -2
  58. package/src/web-search/loop.ts +112 -61
  59. package/src/web-search/parse.ts +4 -1
  60. package/gui/dist/assets/index-ByGC8-Bm.css +0 -1
  61. package/gui/dist/assets/index-D_JZzI0r.js +0 -15
@@ -22,6 +22,8 @@ import {
22
22
  McpToolResultSchema,
23
23
  ModelDetailsSchema,
24
24
  ResumeActionSchema,
25
+ RequestContextSchema,
26
+ RequestContextEnvSchema,
25
27
  ThinkingMessageSchema,
26
28
  ToolCallSchema,
27
29
  UserMessageActionSchema,
@@ -39,6 +41,24 @@ import {
39
41
 
40
42
  const encoder = new TextEncoder();
41
43
 
44
+ /** Runtime timezone for protobuf RequestContextEnv (dynamic, never hardcoded). */
45
+ function runtimeTimeZone(): string {
46
+ try {
47
+ return Intl.DateTimeFormat().resolvedOptions().timeZone ?? "UTC";
48
+ } catch {
49
+ return "UTC";
50
+ }
51
+ }
52
+
53
+ /** Builds a RequestContext with env.timeZone populated dynamically. */
54
+ function buildRequestContext() {
55
+ return create(RequestContextSchema, {
56
+ env: create(RequestContextEnvSchema, {
57
+ timeZone: runtimeTimeZone(),
58
+ }),
59
+ });
60
+ }
61
+
42
62
  function jsonBlob(value: unknown): Uint8Array {
43
63
  return encoder.encode(JSON.stringify(value));
44
64
  }
@@ -307,11 +327,14 @@ export function encodeCursorRunRequest(request: CursorRunRequest): Uint8Array {
307
327
  text,
308
328
  messageId: crypto.randomUUID(),
309
329
  }),
330
+ requestContext: buildRequestContext(),
310
331
  }),
311
332
  }
312
333
  : {
313
334
  case: "resumeAction",
314
- value: create(ResumeActionSchema, {}),
335
+ value: create(ResumeActionSchema, {
336
+ requestContext: buildRequestContext(),
337
+ }),
315
338
  },
316
339
  });
317
340
 
@@ -8,17 +8,18 @@ import type {
8
8
  } from "../../types";
9
9
  import { namespacedToolName } from "../../types";
10
10
  import type { CursorRequestMessage, CursorRunRequest } from "./types";
11
+ import { cursorCodexToWireModelId } from "./discovery";
11
12
  import { cursorEffortSuffix } from "./effort-map";
12
13
 
13
14
  /**
14
15
  * Resolve a `cursor/<model>` selection + Codex reasoning effort to the actual Cursor model id. Cursor
15
- * encodes the effort as a per-model suffix (`claude-4.6-opus-high`); `cursorEffortSuffix` picks the
16
- * right tier for that specific model (top effort the model's top tier, e.g. `-max`/`-xhigh`) or
17
- * `undefined` for non-reasoning models like `composer-2.5`. A fully-qualified id (one that isn't a
18
- * known effort base) passes through unchanged.
16
+ * encodes the effort as a per-model suffix (`claude-4.6-opus-high`); `cursorEffortSuffix` picks the
17
+ * right tier for that specific model (literal pass-through, with rank clamp fallback) or
18
+ * `undefined` for non-reasoning models like `composer-2.5`. A fully-qualified id (one that isn't a
19
+ * known effort base) passes through unchanged.
19
20
  */
20
21
  function normalizeCursorModelId(modelId: string, reasoning?: string): string {
21
- const id = modelId.startsWith("cursor/") ? modelId.slice("cursor/".length) : modelId;
22
+ const id = cursorCodexToWireModelId(modelId);
22
23
  const suffix = cursorEffortSuffix(id, reasoning);
23
24
  return suffix ? `${id}-${suffix}` : id;
24
25
  }
@@ -1,6 +1,8 @@
1
1
  import type { CursorRunRequest, CursorServerMessage } from "./types";
2
2
  import type { CursorTransport, CursorTransportFactory, CursorTransportFactoryInput } from "./transport";
3
3
  import { abortError, sleepWithAbort } from "../../lib/upstream-retry";
4
+ import { debugProviderDiagnostic } from "../../lib/debug";
5
+ import { safeCursorErrorMessage } from "./cursor-errors";
4
6
 
5
7
  // Compat: historical name for the shared abortable sleep, kept for external callers.
6
8
  export { sleepWithAbort as abortAwareSleep } from "../../lib/upstream-retry";
@@ -20,6 +22,8 @@ export function isRetryableCursorError(err: unknown): boolean {
20
22
  const message = err instanceof Error ? err.message : typeof err === "string" ? err : "";
21
23
  const haystack = `${code} ${message}`.toLowerCase();
22
24
  if (/auth|unauthor|forbidden|invalid|permission|denied|not found|unsupported/.test(haystack)) return false;
25
+ if (/resource.exhausted|resource_exhausted|rate limit|too many requests|throttl/.test(haystack)) return false;
26
+ if (haystack.includes("nghttp2_cancel") || haystack.includes("stream suspended")) return false;
23
27
  return (
24
28
  haystack.includes("econnreset") ||
25
29
  haystack.includes("econnrefused") ||
@@ -27,7 +31,7 @@ export function isRetryableCursorError(err: unknown): boolean {
27
31
  haystack.includes("enetunreach") ||
28
32
  haystack.includes("eai_again") ||
29
33
  haystack.includes("goaway") ||
30
- haystack.includes("nghttp2") ||
34
+ (haystack.includes("nghttp2") && !haystack.includes("nghttp2_cancel")) ||
31
35
  haystack.includes("socket hang up") ||
32
36
  haystack.includes("connection reset") ||
33
37
  haystack.includes("unavailable") ||
@@ -83,8 +87,23 @@ export async function runCursorTurnWithRetry(
83
87
  !signal?.aborted &&
84
88
  requestUncommitted(transport) &&
85
89
  isRetryableCursorError(err);
86
- if (!canRetry) throw err;
87
- await sleepWithAbort(cursorRetryDelayMs(attempt), signal);
90
+ if (!canRetry) {
91
+ debugProviderDiagnostic("cursor", "no-retry", {
92
+ attempt,
93
+ reason: safeCursorErrorMessage(err instanceof Error ? err.message : String(err)),
94
+ emittedAny,
95
+ committed: !requestUncommitted(transport),
96
+ aborted: !!signal?.aborted,
97
+ });
98
+ throw err;
99
+ }
100
+ const backoffMs = cursorRetryDelayMs(attempt);
101
+ debugProviderDiagnostic("cursor", "retry", {
102
+ attempt,
103
+ reason: safeCursorErrorMessage(err instanceof Error ? err.message : String(err)),
104
+ backoffMs,
105
+ });
106
+ await sleepWithAbort(backoffMs, signal);
88
107
  } finally {
89
108
  await transport.close?.();
90
109
  }
@@ -1,7 +1,7 @@
1
1
  import type { AdapterEvent, OcxProviderConfig } from "../types";
2
2
  import type { ProviderAdapter } from "./base";
3
3
  import { cursorExecDeniedMessage } from "./cursor/exec-policy";
4
- import { safeCursorErrorMessage } from "./cursor/cursor-errors";
4
+ import { isCursorBenignCancelError, safeCursorErrorMessage } from "./cursor/cursor-errors";
5
5
  import { createCursorKvStore, type CursorKvStore } from "./cursor/kv-store";
6
6
  import { mapCursorServerMessage } from "./cursor/message-mapper";
7
7
  import { createCursorRequest, generatedCursorConversationId } from "./cursor/request-builder";
@@ -91,6 +91,7 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
91
91
  },
92
92
  );
93
93
  } catch (err) {
94
+ if (isCursorBenignCancelError(err)) return;
94
95
  const partialUsage = (err as { partialUsage?: import("../types").OcxUsage }).partialUsage;
95
96
  emit({ type: "error", message: safeCursorTransportError(err), ...(partialUsage ? { usage: partialUsage } : {}) });
96
97
  }
@@ -76,7 +76,9 @@ function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProviderCon
76
76
  type: "function",
77
77
  function: { name: namespacedToolName(tc.namespace, tc.name), arguments: JSON.stringify(tc.arguments) },
78
78
  }));
79
- if (!chatMsg.content) chatMsg.content = null;
79
+ // "" instead of null: strict validators (xAI: "Each message must have at least one
80
+ // content element", langchain#34140) reject content-less assistant history entries.
81
+ if (!chatMsg.content) chatMsg.content = "";
80
82
  }
81
83
  if (chatMsg.reasoning_content !== undefined && chatMsg.content === undefined && chatMsg.tool_calls === undefined) {
82
84
  chatMsg.content = "";
@@ -97,7 +99,7 @@ function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProviderCon
97
99
  const name = safeToolName(msg.toolName);
98
100
  out.push({
99
101
  role: "assistant",
100
- content: null,
102
+ content: "",
101
103
  tool_calls: [{
102
104
  id: toolCallId,
103
105
  type: "function",
@@ -166,6 +168,20 @@ function usageFromOpenAIChat(usage: Record<string, unknown> | undefined): OcxUsa
166
168
  };
167
169
  }
168
170
 
171
+ function thinkingBudgetForEffort(parsed: OcxParsedRequest, reasoningEffort: string): number | undefined {
172
+ if (parsed.options.reasoning === "minimal") return 0;
173
+ const maxBudget = parsed.options.maxOutputTokens ?? 32768;
174
+ const fractions: Record<string, number> = {
175
+ low: 0.20,
176
+ medium: 0.50,
177
+ high: 0.75,
178
+ xhigh: 0.90,
179
+ max: 1.0,
180
+ };
181
+ const fraction = fractions[reasoningEffort];
182
+ return fraction === undefined ? undefined : Math.max(1, Math.floor(maxBudget * fraction));
183
+ }
184
+
169
185
  export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAdapter {
170
186
  return {
171
187
  name: "openai-chat",
@@ -196,7 +212,10 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
196
212
  if (parsed.options.stopSequences !== undefined) body.stop = parsed.options.stopSequences;
197
213
  const reasoningEffort = mapReasoningEffort(provider, parsed.modelId, parsed.options.reasoning);
198
214
  if (reasoningEffort !== undefined) {
199
- if (modelInList(provider.thinkingToggleModels, parsed.modelId)) {
215
+ if (modelInList(provider.thinkingBudgetModels, parsed.modelId)) {
216
+ const budget = thinkingBudgetForEffort(parsed, reasoningEffort);
217
+ if (budget !== undefined) body.thinking_budget = budget;
218
+ } else if (modelInList(provider.thinkingToggleModels, parsed.modelId)) {
200
219
  // Vendor thinking-toggle wire (MiMo v2.x, GLM 5/5.1): the mapped value is the toggle
201
220
  // state, sent as `thinking: {type}` — these models ignore/reject reasoning_effort.
202
221
  if (reasoningEffort === "enabled" || reasoningEffort === "disabled") {
@@ -213,7 +232,15 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
213
232
  body.frequency_penalty = parsed.options.frequencyPenalty;
214
233
  }
215
234
 
216
- if (tools) body.parallel_tool_calls = false;
235
+ if (tools) {
236
+ // Default-ON for chat-completions providers (user decision 260709): the buffered
237
+ // parser assembles multi-call streams safely, so `parallelToolCalls: false` is the
238
+ // only per-provider opt-out; Codex's request bit can still force false per request.
239
+ // Rationale + provider evidence: devlog/_plan/260709_parallel_tool_calls.
240
+ body.parallel_tool_calls = provider.parallelToolCalls === false
241
+ ? false
242
+ : parsed.options.parallelToolCalls !== false;
243
+ }
217
244
  if (parsed.stream) {
218
245
  body.stream_options = { include_usage: true };
219
246
  }
@@ -235,8 +262,26 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
235
262
  const reader = response.body.getReader();
236
263
  const decoder = new TextDecoder();
237
264
  let buffer = "";
238
- let currentToolCallId = "";
239
- let currentToolCallName = "";
265
+ // Streamed tool calls are BUFFERED until a terminal signal, then flushed as atomic
266
+ // start/delta/end sequences. The bridge treats text/reasoning deltas as barriers that
267
+ // close an open tool-call item (bridge.ts closeCurrentToolCall on text_delta), so
268
+ // emitting calls incrementally would orphan later argument deltas whenever a provider
269
+ // interleaves content — and parallel tool calls (multiple ids, index-keyed continuation
270
+ // chunks, whole-chunk calls) cannot be represented live without overlapping sequences.
271
+ // Keyed by `index` (OpenAI wire standard), falling back to `id`, falling back to the
272
+ // last-seen call for providers that omit both on continuation chunks.
273
+ interface PendingToolCall { key: string; id: string; name: string; args: string }
274
+ const pendingToolCalls: PendingToolCall[] = [];
275
+ let toolCallSeq = 0;
276
+ const flushToolCalls = function* (): Generator<AdapterEvent> {
277
+ for (const call of pendingToolCalls) {
278
+ if (!call.id) call.id = `call_${++toolCallSeq}`;
279
+ yield { type: "tool_call_start", id: call.id, name: call.name };
280
+ if (call.args.length > 0) yield { type: "tool_call_delta", arguments: call.args };
281
+ yield { type: "tool_call_end" };
282
+ }
283
+ pendingToolCalls.length = 0;
284
+ };
240
285
  let pendingUsage: OcxUsage | undefined;
241
286
  // Track terminal signals so a socket EOF without any terminator can fail closed instead of
242
287
  // being reported as a clean completion (silent truncation). A graceful close is either an
@@ -252,10 +297,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
252
297
  if (!line.startsWith("data: ")) return "continue";
253
298
  const payload = line.slice(6).trim();
254
299
  if (payload === "[DONE]") {
255
- if (currentToolCallId) {
256
- yield { type: "tool_call_end" };
257
- currentToolCallId = "";
258
- }
300
+ yield* flushToolCalls();
259
301
  yield { type: "done", usage: pendingUsage };
260
302
  return "terminate";
261
303
  }
@@ -273,7 +315,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
273
315
  // classified response.failed (bridge case "error") — never a truncated completion.
274
316
  if (chunk.error) {
275
317
  const err = chunk.error as { message?: string } | undefined;
276
- if (currentToolCallId) yield { type: "tool_call_end" };
318
+ yield* flushToolCalls();
277
319
  yield { type: "error", message: err?.message ?? "upstream error" };
278
320
  return "terminate";
279
321
  }
@@ -302,25 +344,34 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
302
344
  yield { type: "reasoning_raw_delta", text: delta.reasoning_content };
303
345
  }
304
346
 
305
- const toolCalls = delta.tool_calls as { index: number; id?: string; function?: { name?: string; arguments?: string } }[] | undefined;
347
+ const toolCalls = delta.tool_calls as { index?: number; id?: string; function?: { name?: string; arguments?: string } }[] | undefined;
306
348
  if (toolCalls) {
307
349
  for (const tc of toolCalls) {
308
- if (tc.id && tc.id !== currentToolCallId) {
309
- if (currentToolCallId) yield { type: "tool_call_end" };
310
- currentToolCallId = tc.id;
311
- currentToolCallName = tc.function?.name ?? "";
312
- yield { type: "tool_call_start", id: tc.id, name: currentToolCallName };
313
- }
314
- if (tc.function?.arguments) {
315
- yield { type: "tool_call_delta", arguments: tc.function.arguments };
350
+ const key = typeof tc.index === "number"
351
+ ? `i:${tc.index}`
352
+ : tc.id
353
+ ? `id:${tc.id}`
354
+ : pendingToolCalls[pendingToolCalls.length - 1]?.key;
355
+ let call = key !== undefined ? pendingToolCalls.find(c => c.key === key) : undefined;
356
+ // Mixed keying rescue: a call opened under an index key must still absorb an
357
+ // id-only continuation for the same provider id (and vice versa) instead of
358
+ // splitting into two calls that share one call_id downstream.
359
+ if (!call && tc.id) call = pendingToolCalls.find(c => c.id === tc.id);
360
+ if (!call) {
361
+ call = { key: key ?? `seq:${pendingToolCalls.length}`, id: "", name: "", args: "" };
362
+ pendingToolCalls.push(call);
316
363
  }
364
+ if (tc.id && !call.id) call.id = tc.id;
365
+ if (tc.function?.name && !call.name) call.name = tc.function.name;
366
+ if (tc.function?.arguments) call.args += tc.function.arguments;
317
367
  }
318
368
  }
319
369
  }
320
370
 
321
- if (choices[0].finish_reason === "tool_calls" && currentToolCallId) {
322
- yield { type: "tool_call_end" };
323
- currentToolCallId = "";
371
+ // Any non-empty finish_reason ends the generation: flush assembled tool calls as
372
+ // atomic sequences (covers "tool_calls" AND providers that close tool turns with "stop").
373
+ if (typeof choices[0].finish_reason === "string" && choices[0].finish_reason) {
374
+ yield* flushToolCalls();
324
375
  }
325
376
  return "continue";
326
377
  };
@@ -347,9 +398,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
347
398
  if (buffer.length > 0) {
348
399
  if ((yield* handleDataLine(buffer)) === "terminate") return;
349
400
  }
350
- if (currentToolCallId) {
351
- yield { type: "tool_call_end" };
352
- }
401
+ yield* flushToolCalls();
353
402
  // Reader EOF. A graceful close shows at least one terminal signal: `[DONE]` (returns above),
354
403
  // a non-null finish_reason (sawFinish), or a trailing usage chunk (providers emit usage only
355
404
  // at end-of-generation). If NONE of those were seen, the stream was cut mid-flight — fail
package/src/bridge.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { AdapterEvent, OcxUsage } from "./types";
2
- import { classifyError, type OcxErrorPayload } from "./lib/errors";
2
+ import { adapterFailureFromMessage, classifyError, type OcxErrorPayload } from "./lib/errors";
3
3
  import { encodeCompactionSummary } from "./responses/compaction";
4
4
  import { encodeReasoningEnvelope, type ReasoningEnvelope } from "./responses/reasoning-envelope";
5
5
  import { usageDisplayTotalTokens, usageInputTokensWithCacheDetail } from "./usage/totals";
@@ -40,6 +40,8 @@ function responseError(status: number, type: string, message: string): OcxErrorP
40
40
  return classifyError(status, type, message);
41
41
  }
42
42
 
43
+ export { adapterFailureFromMessage } from "./lib/errors";
44
+
43
45
  /**
44
46
  * Build the native `WebSearchAction::Search` payload from the queries that ran. codex-rs prefers a
45
47
  * non-empty `query` over `queries` for the cell label, and only renders "<first> ..." when `query`
@@ -184,6 +186,7 @@ export function bridgeToResponsesSSE(
184
186
  if (currentMsg) closeCurrentMessage();
185
187
  if (currentReasoning) closeCurrentReasoning();
186
188
  if (currentRawReasoning) closeCurrentRawReasoning();
189
+ flushHiddenRawReasoning();
187
190
  if (currentToolCall) closeCurrentToolCall();
188
191
  if (currentWebSearch) closeCurrentWebSearch("failed", []);
189
192
  emit("response.incomplete", {
@@ -236,6 +239,23 @@ export function bridgeToResponsesSSE(
236
239
  finishedItems.push(item as OutputItem);
237
240
  outputIndex++;
238
241
  };
242
+ // hideThinkingSummary for RAW reasoning (openai-chat reasoning_content, kiro tags): no
243
+ // visible reasoning item is emitted — the app renders nothing, so tool cells keep grouping
244
+ // like native models — but the text still round-trips in a txt-only ocxr1 envelope so
245
+ // preserveReasoningContentModels replay (GLM interleaved thinking) keeps working. Direct
246
+ // encodeReasoningEnvelope: takeReasoningEnvelope's sig/red guard would drop txt-only.
247
+ let hiddenRawReasoningText = "";
248
+ const flushHiddenRawReasoning = () => {
249
+ if (!hiddenRawReasoningText) return;
250
+ const encrypted = encodeReasoningEnvelope({ txt: hiddenRawReasoningText });
251
+ hiddenRawReasoningText = "";
252
+ const itemId = `rs_${uuid()}`;
253
+ const item = { type: "reasoning", id: itemId, summary: [] as never[], encrypted_content: encrypted };
254
+ emit("response.output_item.added", { output_index: outputIndex, item });
255
+ emit("response.output_item.done", { output_index: outputIndex, item });
256
+ finishedItems.push(item as OutputItem);
257
+ outputIndex++;
258
+ };
239
259
  // Full assistant text of a compaction turn (across message boundaries) — becomes the
240
260
  // synthetic compaction item's payload on done.
241
261
  let compactionText = "";
@@ -392,6 +412,7 @@ export function bridgeToResponsesSSE(
392
412
  case "text_delta": {
393
413
  if (currentReasoning) closeCurrentReasoning();
394
414
  if (currentRawReasoning) closeCurrentRawReasoning();
415
+ flushHiddenRawReasoning();
395
416
  if (currentToolCall) closeCurrentToolCall();
396
417
  if (!currentMsg) {
397
418
  const itemId = `msg_${uuid()}`;
@@ -417,6 +438,7 @@ export function bridgeToResponsesSSE(
417
438
  if (options?.hideThinkingSummary) { hiddenThinkingText += event.thinking; break; }
418
439
  if (currentMsg) closeCurrentMessage();
419
440
  if (currentRawReasoning) closeCurrentRawReasoning();
441
+ flushHiddenRawReasoning();
420
442
  if (currentToolCall) closeCurrentToolCall();
421
443
  if (!currentReasoning) {
422
444
  const itemId = `rs_${uuid()}`;
@@ -448,6 +470,7 @@ export function bridgeToResponsesSSE(
448
470
  break;
449
471
  }
450
472
  case "reasoning_raw_delta": {
473
+ if (options?.hideThinkingSummary) { hiddenRawReasoningText += event.text; break; }
451
474
  if (currentMsg) closeCurrentMessage();
452
475
  if (currentReasoning) closeCurrentReasoning();
453
476
  if (currentToolCall) closeCurrentToolCall();
@@ -468,6 +491,7 @@ export function bridgeToResponsesSSE(
468
491
  if (currentMsg) closeCurrentMessage();
469
492
  if (currentReasoning) closeCurrentReasoning();
470
493
  if (currentRawReasoning) closeCurrentRawReasoning();
494
+ flushHiddenRawReasoning();
471
495
  if (currentToolCall) closeCurrentToolCall();
472
496
  const itemId = `fc_${uuid()}`;
473
497
  const mapped = toolNsMap?.get(event.name);
@@ -522,6 +546,7 @@ export function bridgeToResponsesSSE(
522
546
  if (currentMsg) closeCurrentMessage();
523
547
  if (currentReasoning) closeCurrentReasoning();
524
548
  if (currentRawReasoning) closeCurrentRawReasoning();
549
+ flushHiddenRawReasoning();
525
550
  if (currentToolCall) closeCurrentToolCall();
526
551
  if (currentWebSearch) closeCurrentWebSearch("completed", []);
527
552
  emit("response.output_item.added", {
@@ -556,6 +581,7 @@ export function bridgeToResponsesSSE(
556
581
  if (currentMsg) closeCurrentMessage();
557
582
  if (currentReasoning) closeCurrentReasoning();
558
583
  if (currentRawReasoning) closeCurrentRawReasoning();
584
+ flushHiddenRawReasoning();
559
585
  if (currentToolCall) closeCurrentToolCall();
560
586
  if (currentWebSearch) closeCurrentWebSearch("completed", []);
561
587
  // Redacted-only turns (or hidden thinking without a trailing signature event) still
@@ -584,16 +610,18 @@ export function bridgeToResponsesSSE(
584
610
  if (currentMsg) closeCurrentMessage();
585
611
  if (currentReasoning) closeCurrentReasoning();
586
612
  if (currentRawReasoning) closeCurrentRawReasoning();
613
+ flushHiddenRawReasoning();
587
614
  if (currentToolCall) closeCurrentToolCall();
588
615
  if (currentWebSearch) closeCurrentWebSearch("failed", []);
616
+ const failure = adapterFailureFromMessage(event.message);
589
617
  emit("response.failed", {
590
618
  response: {
591
619
  ...responseSnapshot("failed", finishedItems),
592
620
  // Partial consumption from a mid-stream upstream failure: surfaced so the request
593
621
  // log can record real tokens instead of usageStatus "unreported" with 0.
594
622
  ...(event.usage ? { usage: responsesUsage(event.usage) } : {}),
595
- error: responseError(502, "upstream_error", event.message),
596
- last_error: responseError(502, "upstream_error", event.message),
623
+ error: failure.error,
624
+ last_error: failure.error,
597
625
  },
598
626
  });
599
627
  reportTerminal("failed");
@@ -603,6 +631,7 @@ export function bridgeToResponsesSSE(
603
631
  }
604
632
  }
605
633
  } catch (err) {
634
+ flushHiddenRawReasoning();
606
635
  if (currentWebSearch) closeCurrentWebSearch("failed", []);
607
636
  emit("response.failed", {
608
637
  response: {
@@ -623,6 +652,7 @@ export function bridgeToResponsesSSE(
623
652
  if (currentMsg) closeCurrentMessage();
624
653
  if (currentReasoning) closeCurrentReasoning();
625
654
  if (currentRawReasoning) closeCurrentRawReasoning();
655
+ flushHiddenRawReasoning();
626
656
  if (currentToolCall) closeCurrentToolCall();
627
657
  if (currentWebSearch) closeCurrentWebSearch("failed", []);
628
658
  emit("response.incomplete", {
@@ -723,6 +753,15 @@ export function buildResponseJSON(
723
753
  };
724
754
  const flushRawReasoning = () => {
725
755
  if (!currentRawReasoning) return;
756
+ if (options?.hideThinkingSummary === true) {
757
+ // Same contract as the streaming path: no visible reasoning, txt-only envelope round-trip.
758
+ output.push({
759
+ type: "reasoning", id: `rs_${uuid()}`, summary: [],
760
+ encrypted_content: encodeReasoningEnvelope({ txt: currentRawReasoning }),
761
+ });
762
+ currentRawReasoning = "";
763
+ return;
764
+ }
726
765
  output.push({
727
766
  type: "reasoning", id: `rs_${uuid()}`, summary: [],
728
767
  content: [{ type: "reasoning_text", text: currentRawReasoning }],
@@ -0,0 +1,203 @@
1
+ import { findLiveProxy, probeHostname } from "../server/proxy-liveness";
2
+ import { DEBUG_ENV, type DebugSettingsView } from "../lib/debug-settings";
3
+ import { runningProxyUpdateHeaders } from "../oauth/login-cli";
4
+
5
+ type DebugScope = "provider" | "usage";
6
+
7
+ async function requireLiveProxy() {
8
+ const live = await findLiveProxy();
9
+ if (!live) {
10
+ console.error("Proxy is not running. Start it with: ocx start");
11
+ process.exit(1);
12
+ }
13
+ return live;
14
+ }
15
+
16
+ async function fetchDebugSettings(): Promise<DebugSettingsView> {
17
+ const live = await requireLiveProxy();
18
+ try {
19
+ const res = await fetch(`http://${probeHostname(live.hostname)}:${live.port}/api/debug`, {
20
+ headers: runningProxyUpdateHeaders(),
21
+ });
22
+ if (!res.ok) {
23
+ console.error(`Failed to read debug settings (${res.status})`);
24
+ process.exit(1);
25
+ }
26
+ return await res.json() as DebugSettingsView;
27
+ } catch (err) {
28
+ console.error(`Proxy is running but /api/debug is unreachable: ${err instanceof Error ? err.message : String(err)}`);
29
+ process.exit(1);
30
+ }
31
+ }
32
+
33
+ async function putDebugSettings(body: Record<string, unknown>): Promise<DebugSettingsView> {
34
+ const live = await requireLiveProxy();
35
+ const res = await fetch(`http://${probeHostname(live.hostname)}:${live.port}/api/debug`, {
36
+ method: "PUT",
37
+ headers: runningProxyUpdateHeaders(),
38
+ body: JSON.stringify(body),
39
+ });
40
+ if (!res.ok) {
41
+ const text = await res.text().catch(() => "");
42
+ console.error(`Failed to update debug settings (${res.status})${text ? `: ${text.slice(0, 200)}` : ""}`);
43
+ process.exit(1);
44
+ }
45
+ return await res.json() as DebugSettingsView;
46
+ }
47
+
48
+ function printScopeStatus(scope: DebugScope, view: DebugSettingsView): void {
49
+ if (scope === "provider") {
50
+ console.log(`Provider debug: ${view.enabled ? "ON" : "off"}`);
51
+ console.log(` env=${view.env.debug ? "on" : "off"}, runtime=${view.runtimeOverride.debug === undefined ? "env/default" : view.runtimeOverride.debug ? "on" : "off"}`);
52
+ console.log(" Tail: ocx debug provider logs [-f]");
53
+ } else {
54
+ console.log(`Usage debug: ${view.usage ? "ON" : "off"}`);
55
+ console.log(` env=${view.env.usage ? "on" : "off"}, runtime=${view.runtimeOverride.usage === undefined ? "env/default" : view.runtimeOverride.usage ? "on" : "off"}`);
56
+ console.log(" Tail: ocx debug usage logs [-f] (via running proxy API)");
57
+ }
58
+ }
59
+
60
+ function envDebugEnabled(): boolean {
61
+ return process.env.OCX_DEBUG === "1"
62
+ || process.env.OCX_DEBUG_FRAMES === "1";
63
+ }
64
+
65
+ async function printProviderLogs(follow: boolean): Promise<void> {
66
+ const live = await requireLiveProxy();
67
+ const base = `http://${probeHostname(live.hostname)}:${live.port}/api/debug/logs`;
68
+
69
+ let after = 0;
70
+ try {
71
+ const res = await fetch(`${base}?limit=500`, { headers: runningProxyUpdateHeaders() });
72
+ if (!res.ok) {
73
+ console.error(`Failed to read debug logs (${res.status})`);
74
+ process.exit(1);
75
+ }
76
+ const entries = await res.json() as { seq: number; line: string }[];
77
+ for (const entry of entries) console.log(entry.line);
78
+ if (entries.length > 0) after = entries[entries.length - 1]!.seq;
79
+ } catch (err) {
80
+ console.error(`Failed to read debug logs: ${err instanceof Error ? err.message : String(err)}`);
81
+ process.exit(1);
82
+ }
83
+
84
+ if (!follow) return;
85
+
86
+ while (true) {
87
+ await new Promise(resolve => setTimeout(resolve, 1000));
88
+ try {
89
+ const res = await fetch(`${base}?after=${after}&limit=500`, { headers: runningProxyUpdateHeaders() });
90
+ if (!res.ok) continue;
91
+ const entries = await res.json() as { seq: number; line: string }[];
92
+ for (const entry of entries) console.log(entry.line);
93
+ if (entries.length > 0) after = entries[entries.length - 1]!.seq;
94
+ } catch {
95
+ /* keep following */
96
+ }
97
+ }
98
+ }
99
+
100
+ async function printUsageLogs(follow: boolean): Promise<void> {
101
+ const live = await requireLiveProxy();
102
+ const base = `http://${probeHostname(live.hostname)}:${live.port}/api/debug/usage-logs`;
103
+
104
+ let after = 0;
105
+ try {
106
+ const res = await fetch(`${base}?limit=500`, { headers: runningProxyUpdateHeaders() });
107
+ if (!res.ok) {
108
+ console.error(`Failed to read usage debug logs (${res.status})`);
109
+ process.exit(1);
110
+ }
111
+ const entries = await res.json() as { seq: number; line: string }[];
112
+ for (const entry of entries) console.log(entry.line);
113
+ if (entries.length === 0) console.log("(empty — enable with: ocx debug usage on)");
114
+ if (entries.length > 0) after = entries[entries.length - 1]!.seq;
115
+ } catch (err) {
116
+ console.error(`Failed to read usage debug logs: ${err instanceof Error ? err.message : String(err)}`);
117
+ process.exit(1);
118
+ }
119
+
120
+ if (!follow) return;
121
+
122
+ while (true) {
123
+ await new Promise(resolve => setTimeout(resolve, 1000));
124
+ try {
125
+ const res = await fetch(`${base}?after=${after}&limit=500`, { headers: runningProxyUpdateHeaders() });
126
+ if (!res.ok) continue;
127
+ const entries = await res.json() as { seq: number; line: string }[];
128
+ for (const entry of entries) console.log(entry.line);
129
+ if (entries.length > 0) after = entries[entries.length - 1]!.seq;
130
+ } catch {
131
+ /* keep following */
132
+ }
133
+ }
134
+ }
135
+
136
+ async function handleScopeCommand(scope: DebugScope, actionArgv: string[]): Promise<void> {
137
+ const action = (actionArgv[0] ?? "status").trim().toLowerCase();
138
+
139
+ if (action === "on" || action === "off") {
140
+ const enabled = action === "on";
141
+ const body = scope === "provider" ? { debug: enabled } : { usage: enabled };
142
+ printScopeStatus(scope, await putDebugSettings(body));
143
+ console.log(`\n${scope} debug is now ${enabled ? "enabled" : "disabled"}.`);
144
+ return;
145
+ }
146
+
147
+ if (action === "status") {
148
+ printScopeStatus(scope, await fetchDebugSettings());
149
+ return;
150
+ }
151
+
152
+ if (action === "reset") {
153
+ const resetKey = scope === "provider" ? "provider" : "usage";
154
+ printScopeStatus(scope, await putDebugSettings({ reset: resetKey }));
155
+ console.log(`\nRuntime override cleared for ${scope}; effective value follows env again.`);
156
+ return;
157
+ }
158
+
159
+ if (action === "logs") {
160
+ const follow = actionArgv.slice(1).some(arg => arg === "-f" || arg === "--follow");
161
+ if (scope === "provider") await printProviderLogs(follow);
162
+ else await printUsageLogs(follow);
163
+ return;
164
+ }
165
+
166
+ console.error(`Usage: ocx debug ${scope} on|off|status|reset|logs [-f]`);
167
+ process.exit(1);
168
+ }
169
+
170
+ function printTopLevelHelp(): void {
171
+ console.log("Debug commands (proxy must be running):");
172
+ console.log("");
173
+ console.log(" ocx debug provider on|off|status|reset|logs [-f]");
174
+ console.log(" ocx debug usage on|off|status|reset|logs [-f]");
175
+ console.log("");
176
+ console.log("Env defaults on start:");
177
+ console.log(" provider → OCX_DEBUG=1 (legacy OCX_DEBUG_FRAMES still works)");
178
+ console.log(` usage → ${DEBUG_ENV.usage}=1`);
179
+ }
180
+
181
+ export async function handleDebugCommand(argv: string[]): Promise<void> {
182
+ const sub = (argv[0] ?? "").trim().toLowerCase();
183
+
184
+ if (sub === "provider" || sub === "usage") {
185
+ await handleScopeCommand(sub, argv.slice(1));
186
+ return;
187
+ }
188
+
189
+ if (sub === "" || sub === "help" || sub === "--help" || sub === "-h") {
190
+ const live = await findLiveProxy();
191
+ if (!live) {
192
+ console.log("Proxy is not running — env defaults for the next start:");
193
+ console.log(` provider → OCX_DEBUG = ${envDebugEnabled() ? "on" : "off"}`);
194
+ console.log(` usage → ${DEBUG_ENV.usage} = ${process.env[DEBUG_ENV.usage] === "1" ? "on" : "off"}`);
195
+ console.log("");
196
+ }
197
+ printTopLevelHelp();
198
+ return;
199
+ }
200
+
201
+ printTopLevelHelp();
202
+ process.exit(1);
203
+ }