@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.
- package/README.ko.md +9 -5
- package/README.md +7 -4
- package/README.zh-CN.md +8 -4
- package/gui/dist/assets/index-BUAMcKFd.css +1 -0
- package/gui/dist/assets/index-KorpEKW8.js +34 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +62 -1
- package/src/adapters/cursor/cursor-errors.ts +28 -1
- package/src/adapters/cursor/discovery.ts +60 -10
- package/src/adapters/cursor/effort-map.ts +38 -7
- package/src/adapters/cursor/live-models.ts +3 -0
- package/src/adapters/cursor/live-transport.ts +136 -7
- package/src/adapters/cursor/protobuf-request.ts +24 -1
- package/src/adapters/cursor/request-builder.ts +6 -5
- package/src/adapters/cursor/transport-retry.ts +22 -3
- package/src/adapters/cursor.ts +2 -1
- package/src/adapters/openai-chat.ts +75 -26
- package/src/bridge.ts +42 -3
- package/src/cli/debug.ts +203 -0
- package/src/cli/doctor.ts +11 -0
- package/src/cli/help.ts +11 -0
- package/src/cli/index.ts +10 -0
- package/src/cli/v2.ts +131 -0
- package/src/codex/auth-api.ts +7 -3
- package/src/codex/catalog.ts +334 -31
- package/src/codex/data/upstream-models.json +830 -0
- package/src/codex/features.ts +178 -0
- package/src/codex/project-config-warnings.ts +388 -0
- package/src/codex/sync.ts +8 -0
- package/src/codex/warmup.ts +62 -7
- package/src/config.ts +7 -5
- package/src/lib/debug-log-buffer.ts +42 -0
- package/src/lib/debug-settings.ts +84 -0
- package/src/lib/debug.ts +18 -9
- package/src/lib/errors.ts +104 -1
- package/src/oauth/cursor.ts +35 -12
- package/src/oauth/store.ts +4 -3
- package/src/providers/derive.ts +8 -0
- package/src/providers/registry.ts +56 -21
- package/src/reasoning-effort.ts +37 -9
- package/src/responses/parser.ts +7 -2
- package/src/router.ts +5 -0
- package/src/server/adapter-resolve.ts +1 -1
- package/src/server/index.ts +27 -3
- package/src/server/management-api.ts +189 -7
- package/src/server/relay.ts +2 -2
- package/src/server/request-decompress.ts +8 -2
- package/src/server/request-log.ts +78 -0
- package/src/server/responses.ts +241 -9
- package/src/types.ts +34 -1
- package/src/usage/debug.ts +32 -5
- package/src/usage/summary.ts +6 -6
- package/src/vision/describe.ts +4 -0
- package/src/web-search/executor.ts +4 -0
- package/src/web-search/format-result.ts +11 -3
- package/src/web-search/index.ts +31 -2
- package/src/web-search/loop.ts +112 -61
- package/src/web-search/parse.ts +4 -1
- package/gui/dist/assets/index-ByGC8-Bm.css +0 -1
- 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
|
-
|
|
16
|
-
* right tier for that specific model (
|
|
17
|
-
|
|
18
|
-
|
|
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 =
|
|
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)
|
|
87
|
-
|
|
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
|
}
|
package/src/adapters/cursor.ts
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
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.
|
|
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)
|
|
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
|
-
|
|
239
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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
|
-
|
|
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:
|
|
596
|
-
last_error:
|
|
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 }],
|
package/src/cli/debug.ts
ADDED
|
@@ -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
|
+
}
|