@bitkyc08/opencodex 2.6.32 → 2.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +9 -5
- package/README.md +7 -4
- package/README.zh-CN.md +8 -4
- package/gui/dist/assets/index-BGdxwydf.js +34 -0
- package/gui/dist/assets/index-DANCQ2Jt.css +1 -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 +56 -10
- package/src/adapters/cursor/effort-map.ts +35 -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 -6
- 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 +32 -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 +168 -7
- package/src/server/relay.ts +2 -2
- package/src/server/request-log.ts +78 -0
- package/src/server/responses.ts +209 -0
- package/src/types.ts +28 -1
- package/src/usage/debug.ts +32 -5
- package/src/usage/summary.ts +6 -6
- package/src/web-search/index.ts +1 -1
- package/gui/dist/assets/index-ByGC8-Bm.css +0 -1
- package/gui/dist/assets/index-D_JZzI0r.js +0 -15
package/src/codex/warmup.ts
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
export class CodexWarmupError extends Error {
|
|
2
2
|
code: "http_status" | "missing_body" | "stream_failed" | "stream_incomplete" | "stream_error" | "invalid_sse" | "no_terminal" | "transport";
|
|
3
3
|
status?: number;
|
|
4
|
+
/** Upstream error detail extracted from the response body (truncated to 512 chars). */
|
|
5
|
+
upstreamDetail?: string;
|
|
4
6
|
|
|
5
7
|
constructor(
|
|
6
8
|
code: CodexWarmupError["code"],
|
|
7
9
|
message = "Codex warmup failed",
|
|
8
|
-
options: { status?: number; cause?: unknown } = {},
|
|
10
|
+
options: { status?: number; cause?: unknown; upstreamDetail?: string } = {},
|
|
9
11
|
) {
|
|
10
12
|
super(message);
|
|
11
13
|
this.name = "CodexWarmupError";
|
|
12
14
|
this.code = code;
|
|
13
15
|
this.status = options.status;
|
|
16
|
+
this.upstreamDetail = options.upstreamDetail;
|
|
14
17
|
if (options.cause !== undefined) this.cause = options.cause;
|
|
15
18
|
}
|
|
16
19
|
}
|
|
@@ -24,11 +27,39 @@ export interface CodexWarmupOptions {
|
|
|
24
27
|
|
|
25
28
|
const CODEX_RESPONSES_URL = "https://chatgpt.com/backend-api/codex/responses";
|
|
26
29
|
const DEFAULT_MODEL = "gpt-5.4-mini";
|
|
30
|
+
const FALLBACK_MODELS = ["gpt-5.5"];
|
|
27
31
|
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
32
|
+
const MAX_ERROR_BODY_BYTES = 2048;
|
|
33
|
+
|
|
34
|
+
/** Read the first MAX_ERROR_BODY_BYTES of a response body and extract an error message. */
|
|
35
|
+
async function readErrorDetail(res: Response): Promise<string | undefined> {
|
|
36
|
+
try {
|
|
37
|
+
const text = await res.text();
|
|
38
|
+
const trimmed = text.slice(0, MAX_ERROR_BODY_BYTES);
|
|
39
|
+
try {
|
|
40
|
+
const json = JSON.parse(trimmed) as Record<string, unknown>;
|
|
41
|
+
// ChatGPT backend error shape: { error: { message: "..." } } or { detail: "..." }
|
|
42
|
+
const nested = json.error;
|
|
43
|
+
if (nested && typeof nested === "object" && typeof (nested as Record<string, unknown>).message === "string") {
|
|
44
|
+
return ((nested as Record<string, unknown>).message as string).slice(0, 512);
|
|
45
|
+
}
|
|
46
|
+
if (typeof json.detail === "string") return json.detail.slice(0, 512);
|
|
47
|
+
if (typeof json.error === "string") return (json.error as string).slice(0, 512);
|
|
48
|
+
if (typeof json.message === "string") return json.message.slice(0, 512);
|
|
49
|
+
} catch {
|
|
50
|
+
// Non-JSON response body may contain sensitive data (tokens, credentials).
|
|
51
|
+
// Only surface structured error messages, never raw text.
|
|
52
|
+
}
|
|
53
|
+
return undefined;
|
|
54
|
+
} catch {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
28
58
|
|
|
29
59
|
function safeWarmupReason(err: unknown): string {
|
|
30
60
|
if (err instanceof CodexWarmupError) {
|
|
31
|
-
|
|
61
|
+
const base = err.status ? `${err.code}:${err.status}` : err.code;
|
|
62
|
+
return err.upstreamDetail ? `${base} — ${err.upstreamDetail}` : base;
|
|
32
63
|
}
|
|
33
64
|
return "transport";
|
|
34
65
|
}
|
|
@@ -99,7 +130,7 @@ async function drainWarmupSse(body: ReadableStream<Uint8Array>): Promise<void> {
|
|
|
99
130
|
}
|
|
100
131
|
}
|
|
101
132
|
|
|
102
|
-
|
|
133
|
+
async function tryWarmup(options: CodexWarmupOptions, model: string): Promise<void> {
|
|
103
134
|
let res: Response;
|
|
104
135
|
try {
|
|
105
136
|
res = await fetch(CODEX_RESPONSES_URL, {
|
|
@@ -110,7 +141,7 @@ export async function warmCodexAccount(options: CodexWarmupOptions): Promise<voi
|
|
|
110
141
|
"Content-Type": "application/json",
|
|
111
142
|
},
|
|
112
143
|
body: JSON.stringify({
|
|
113
|
-
model
|
|
144
|
+
model,
|
|
114
145
|
instructions: "Reply with OK.",
|
|
115
146
|
input: "hi",
|
|
116
147
|
stream: true,
|
|
@@ -123,8 +154,11 @@ export async function warmCodexAccount(options: CodexWarmupOptions): Promise<voi
|
|
|
123
154
|
}
|
|
124
155
|
|
|
125
156
|
if (!res.ok) {
|
|
126
|
-
await res
|
|
127
|
-
throw new CodexWarmupError("http_status", "Codex warmup was rejected", {
|
|
157
|
+
const upstreamDetail = await readErrorDetail(res);
|
|
158
|
+
throw new CodexWarmupError("http_status", "Codex warmup was rejected", {
|
|
159
|
+
status: res.status,
|
|
160
|
+
upstreamDetail,
|
|
161
|
+
});
|
|
128
162
|
}
|
|
129
163
|
if (!res.body) throw new CodexWarmupError("missing_body");
|
|
130
164
|
|
|
@@ -135,3 +169,25 @@ export async function warmCodexAccount(options: CodexWarmupOptions): Promise<voi
|
|
|
135
169
|
}
|
|
136
170
|
}
|
|
137
171
|
|
|
172
|
+
export async function warmCodexAccount(options: CodexWarmupOptions): Promise<void> {
|
|
173
|
+
const primaryModel = options.model?.trim() || DEFAULT_MODEL;
|
|
174
|
+
try {
|
|
175
|
+
await tryWarmup(options, primaryModel);
|
|
176
|
+
return;
|
|
177
|
+
} catch (err) {
|
|
178
|
+
// Retry with fallback models on 400 (model may not be available for this account).
|
|
179
|
+
if (!(err instanceof CodexWarmupError) || err.status !== 400) throw err;
|
|
180
|
+
let lastErr = err;
|
|
181
|
+
for (const fallback of FALLBACK_MODELS) {
|
|
182
|
+
if (fallback === primaryModel) continue;
|
|
183
|
+
try {
|
|
184
|
+
await tryWarmup(options, fallback);
|
|
185
|
+
return;
|
|
186
|
+
} catch (retryErr) {
|
|
187
|
+
if (retryErr instanceof CodexWarmupError) lastErr = retryErr;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
throw lastErr;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
package/src/config.ts
CHANGED
|
@@ -149,12 +149,14 @@ const configSchema = z.object({
|
|
|
149
149
|
|
|
150
150
|
/**
|
|
151
151
|
* Default featured subagent models (native GPT) seeded on a fresh install and when `subagentModels`
|
|
152
|
-
* is unset. Codex's spawn_agent advertises the first 5 featured catalog entries
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
152
|
+
* is unset. Codex's spawn_agent advertises the first 5 featured catalog entries, so this seed is a
|
|
153
|
+
* deliberate 5-list: frontier gpt-5.5 first, the gpt-5.6 preview trio, and gpt-5.4-mini as the cheap
|
|
154
|
+
* tier. gpt-5.4 / gpt-5.3-codex-spark stay selectable in the GUI's available list. The user can
|
|
155
|
+
* remove any in the GUI — once they set the list (even to []), it is respected, so removals persist
|
|
156
|
+
* (start-up only seeds the UNSET case). Kept to ids ChatGPT accepts; the start-up seed prefers the
|
|
157
|
+
* live catalog's native slugs.
|
|
156
158
|
*/
|
|
157
|
-
export const DEFAULT_SUBAGENT_MODELS = ["gpt-5.5", "gpt-5.
|
|
159
|
+
export const DEFAULT_SUBAGENT_MODELS = ["gpt-5.5", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", "gpt-5.4-mini"];
|
|
158
160
|
|
|
159
161
|
export function getConfigDir(): string {
|
|
160
162
|
return resolveConfigDir();
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/** In-memory ring buffer of debug log lines for `ocx debug logs` / GUI tailing. */
|
|
2
|
+
|
|
3
|
+
export interface DebugLogEntry {
|
|
4
|
+
/** Monotonic cursor for pagination; survives same-millisecond bursts. */
|
|
5
|
+
seq: number;
|
|
6
|
+
at: number;
|
|
7
|
+
line: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const MAX_LINES = 2_000;
|
|
11
|
+
const buffer: DebugLogEntry[] = [];
|
|
12
|
+
const listeners = new Set<(entry: DebugLogEntry) => void>();
|
|
13
|
+
let nextSeq = 1;
|
|
14
|
+
|
|
15
|
+
export function appendDebugLogLine(line: string): void {
|
|
16
|
+
const entry: DebugLogEntry = { seq: nextSeq++, at: Date.now(), line };
|
|
17
|
+
buffer.push(entry);
|
|
18
|
+
if (buffer.length > MAX_LINES) buffer.splice(0, buffer.length - MAX_LINES);
|
|
19
|
+
for (const listener of listeners) {
|
|
20
|
+
try { listener(entry); } catch { /* listeners must not break logging */ }
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function getDebugLogEntries(options?: { after?: number; limit?: number }): DebugLogEntry[] {
|
|
25
|
+
const after = options?.after ?? 0;
|
|
26
|
+
const limit = options?.limit ?? 500;
|
|
27
|
+
const filtered = after > 0 ? buffer.filter(entry => entry.seq > after) : buffer;
|
|
28
|
+
if (filtered.length <= limit) return filtered;
|
|
29
|
+
return filtered.slice(-limit);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function subscribeDebugLogEntries(listener: (entry: DebugLogEntry) => void): () => void {
|
|
33
|
+
listeners.add(listener);
|
|
34
|
+
return () => listeners.delete(listener);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Test isolation. */
|
|
38
|
+
export function resetDebugLogBufferForTests(): void {
|
|
39
|
+
buffer.length = 0;
|
|
40
|
+
listeners.clear();
|
|
41
|
+
nextSeq = 1;
|
|
42
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime-controllable debug flags.
|
|
3
|
+
* Provider debug: `ocx debug provider on|off|status|reset|logs [-f]` (or OCX_DEBUG=1 on start).
|
|
4
|
+
* Usage capture: `ocx debug usage on|off|status|reset|logs [-f]` (or OPENCODEX_USAGE_DEBUG=1).
|
|
5
|
+
* `/api/debug` and `ocx debug` override env defaults without restart.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export const DEBUG_ENV = {
|
|
9
|
+
debug: "OCX_DEBUG",
|
|
10
|
+
usage: "OPENCODEX_USAGE_DEBUG",
|
|
11
|
+
} as const;
|
|
12
|
+
|
|
13
|
+
/** Legacy env var that still enables provider debug logging. */
|
|
14
|
+
const LEGACY_DEBUG_ENV = ["OCX_DEBUG_FRAMES"] as const;
|
|
15
|
+
|
|
16
|
+
export type DebugFlag = keyof typeof DEBUG_ENV;
|
|
17
|
+
|
|
18
|
+
export interface DebugSettingsView {
|
|
19
|
+
enabled: boolean;
|
|
20
|
+
usage: boolean;
|
|
21
|
+
runtimeOverride: Partial<Record<DebugFlag, boolean>>;
|
|
22
|
+
env: Record<DebugFlag, boolean>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const runtimeOverride: Partial<Record<DebugFlag, boolean>> = {};
|
|
26
|
+
|
|
27
|
+
function envFlag(name: string): boolean {
|
|
28
|
+
return process.env[name] === "1";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function legacyDebugEnvEnabled(): boolean {
|
|
32
|
+
return LEGACY_DEBUG_ENV.some(name => envFlag(name));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function isDebugEnabled(): boolean {
|
|
36
|
+
if (runtimeOverride.debug !== undefined) return runtimeOverride.debug;
|
|
37
|
+
return envFlag(DEBUG_ENV.debug) || legacyDebugEnvEnabled();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** @deprecated Use isDebugEnabled(). */
|
|
41
|
+
export function isFramesDebugEnabled(): boolean {
|
|
42
|
+
return isDebugEnabled();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function isUsageDebugEnabled(): boolean {
|
|
46
|
+
if (runtimeOverride.usage !== undefined) return runtimeOverride.usage;
|
|
47
|
+
return envFlag(DEBUG_ENV.usage);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function getDebugSettings(): DebugSettingsView {
|
|
51
|
+
return {
|
|
52
|
+
enabled: isDebugEnabled(),
|
|
53
|
+
usage: isUsageDebugEnabled(),
|
|
54
|
+
runtimeOverride: { ...runtimeOverride },
|
|
55
|
+
env: {
|
|
56
|
+
debug: envFlag(DEBUG_ENV.debug) || legacyDebugEnvEnabled(),
|
|
57
|
+
usage: envFlag(DEBUG_ENV.usage),
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function setDebugSettings(partial: Partial<Record<DebugFlag, boolean>>): DebugSettingsView {
|
|
63
|
+
for (const key of ["debug", "usage"] as const) {
|
|
64
|
+
if (partial[key] !== undefined) runtimeOverride[key] = partial[key];
|
|
65
|
+
}
|
|
66
|
+
return getDebugSettings();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function clearDebugSetting(flag: DebugFlag): DebugSettingsView {
|
|
70
|
+
delete runtimeOverride[flag];
|
|
71
|
+
return getDebugSettings();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function clearDebugSettings(): DebugSettingsView {
|
|
75
|
+
for (const key of ["debug", "usage"] as const) {
|
|
76
|
+
delete runtimeOverride[key];
|
|
77
|
+
}
|
|
78
|
+
return getDebugSettings();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Test isolation: drop runtime overrides only (env vars untouched). */
|
|
82
|
+
export function resetDebugSettingsForTests(): void {
|
|
83
|
+
clearDebugSettings();
|
|
84
|
+
}
|
package/src/lib/debug.ts
CHANGED
|
@@ -1,21 +1,30 @@
|
|
|
1
|
+
import { appendDebugLogLine } from "./debug-log-buffer";
|
|
2
|
+
import { isDebugEnabled } from "./debug-settings";
|
|
1
3
|
import { redactSecrets } from "./redact";
|
|
2
4
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
function emitDebugLine(line: string): void {
|
|
6
|
+
if (!isDebugEnabled()) return;
|
|
7
|
+
try {
|
|
8
|
+
appendDebugLogLine(line);
|
|
9
|
+
console.error(line);
|
|
10
|
+
} catch {
|
|
11
|
+
/* diagnostics must never affect request handling */
|
|
12
|
+
}
|
|
8
13
|
}
|
|
9
14
|
|
|
15
|
+
// Opt-in provider diagnostics. Streaming adapters stay quiet unless provider debug is on
|
|
16
|
+
// (`ocx debug provider on`, GUI Logs toggle, or OCX_DEBUG=1). Tail with `ocx debug provider logs -f`.
|
|
17
|
+
|
|
10
18
|
export function debugDroppedFrame(adapter: string, payload: string): void {
|
|
11
|
-
if (!
|
|
12
|
-
|
|
19
|
+
if (!isDebugEnabled()) return;
|
|
20
|
+
emitDebugLine(`[ocx:frame-drop] ${adapter}: dropped malformed upstream frame (payload redacted, bytes=${payload.length})`);
|
|
13
21
|
}
|
|
14
22
|
|
|
23
|
+
/** Provider-agnostic diagnostic logging: `[ocx:<adapter>:<event>] {...}`. */
|
|
15
24
|
export function debugProviderDiagnostic(adapter: string, event: string, details: Record<string, unknown>): void {
|
|
16
|
-
if (!
|
|
25
|
+
if (!isDebugEnabled()) return;
|
|
17
26
|
try {
|
|
18
|
-
|
|
27
|
+
emitDebugLine(`[ocx:${adapter}:${event}] ${JSON.stringify(redactSecrets(details))}`);
|
|
19
28
|
} catch {
|
|
20
29
|
/* diagnostics must never affect request handling */
|
|
21
30
|
}
|
package/src/lib/errors.ts
CHANGED
|
@@ -30,7 +30,10 @@ export function classifyError(status: number, type: string, message: string): Oc
|
|
|
30
30
|
text.includes("rate limit") ||
|
|
31
31
|
text.includes("rate limited") ||
|
|
32
32
|
text.includes("too many requests") ||
|
|
33
|
-
text.includes("
|
|
33
|
+
text.includes("resource_exhausted") ||
|
|
34
|
+
text.includes("resource exhausted") ||
|
|
35
|
+
text.includes("throttlingexception") ||
|
|
36
|
+
text.includes("throttling")
|
|
34
37
|
) {
|
|
35
38
|
return { message, type: "rate_limit_error", code: "rate_limit_exceeded" };
|
|
36
39
|
}
|
|
@@ -81,3 +84,103 @@ export function classifyError(status: number, type: string, message: string): Oc
|
|
|
81
84
|
}
|
|
82
85
|
return { message, type, code: type || null };
|
|
83
86
|
}
|
|
87
|
+
|
|
88
|
+
/** Best-effort parse of a retry delay embedded in an upstream error message. */
|
|
89
|
+
export function parseRetryAfterFromMessage(message: string): number | undefined {
|
|
90
|
+
const patterns = [
|
|
91
|
+
/try again in (\d+(?:\.\d+)?)\s*s(?:ec(?:ond)?s?)?/i,
|
|
92
|
+
/retry after (\d+(?:\.\d+)?)\s*s(?:ec(?:ond)?s?)?/i,
|
|
93
|
+
/retry[- ]after[:\s]+(\d+)/i,
|
|
94
|
+
];
|
|
95
|
+
for (const pattern of patterns) {
|
|
96
|
+
const match = message.match(pattern);
|
|
97
|
+
if (!match?.[1]) continue;
|
|
98
|
+
const seconds = Number.parseFloat(match[1]);
|
|
99
|
+
if (Number.isFinite(seconds) && seconds > 0) return Math.ceil(seconds);
|
|
100
|
+
}
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Infer HTTP status from adapter terminal error text (provider-agnostic keyword matching). */
|
|
105
|
+
export function inferHttpStatusFromAdapterMessage(message: string): number {
|
|
106
|
+
const lower = message.toLowerCase();
|
|
107
|
+
if (
|
|
108
|
+
lower.includes("resource_exhausted") ||
|
|
109
|
+
lower.includes("resource exhausted") ||
|
|
110
|
+
lower.includes("rate limit") ||
|
|
111
|
+
lower.includes("too many requests") ||
|
|
112
|
+
lower.includes("throttling")
|
|
113
|
+
) return 429;
|
|
114
|
+
if (
|
|
115
|
+
lower.includes("unauthenticated") ||
|
|
116
|
+
lower.includes("unauthorized") ||
|
|
117
|
+
lower.includes("permission_denied") ||
|
|
118
|
+
lower.includes("permission denied") ||
|
|
119
|
+
lower.includes("forbidden") ||
|
|
120
|
+
lower.includes("invalid token") ||
|
|
121
|
+
lower.includes("expired token") ||
|
|
122
|
+
lower.includes("authentication") ||
|
|
123
|
+
lower.includes("access denied")
|
|
124
|
+
) return 401;
|
|
125
|
+
if (
|
|
126
|
+
lower.includes("unavailable") ||
|
|
127
|
+
lower.includes("overloaded") ||
|
|
128
|
+
lower.includes("temporarily") ||
|
|
129
|
+
lower.includes("server is busy")
|
|
130
|
+
) return 503;
|
|
131
|
+
if (
|
|
132
|
+
lower.includes("invalid") ||
|
|
133
|
+
lower.includes("not found") ||
|
|
134
|
+
lower.includes("unsupported") ||
|
|
135
|
+
lower.includes("malformed") ||
|
|
136
|
+
lower.includes("unimplemented")
|
|
137
|
+
) return 400;
|
|
138
|
+
if (
|
|
139
|
+
lower.includes("timed out") ||
|
|
140
|
+
lower.includes("timeout") ||
|
|
141
|
+
lower.includes("etimedout") ||
|
|
142
|
+
lower.includes("deadline")
|
|
143
|
+
) return 504;
|
|
144
|
+
return 502;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Map an adapter terminal error message to HTTP status + classified Codex error payload. */
|
|
148
|
+
export function adapterFailureFromMessage(message: string): { httpStatus: number; error: OcxErrorPayload } {
|
|
149
|
+
const httpStatus = inferHttpStatusFromAdapterMessage(message);
|
|
150
|
+
let finalMessage = message;
|
|
151
|
+
const retryAfterSeconds = parseRetryAfterFromMessage(message);
|
|
152
|
+
if (retryAfterSeconds && !/please try again in /i.test(message)) {
|
|
153
|
+
finalMessage = `${message} Please try again in ${retryAfterSeconds}s.`;
|
|
154
|
+
}
|
|
155
|
+
const errorType = httpStatus === 429
|
|
156
|
+
? "rate_limit_error"
|
|
157
|
+
: httpStatus === 401
|
|
158
|
+
? "authentication_error"
|
|
159
|
+
: httpStatus === 503 || httpStatus === 504
|
|
160
|
+
? "server_error"
|
|
161
|
+
: httpStatus === 400
|
|
162
|
+
? "invalid_request_error"
|
|
163
|
+
: "upstream_error";
|
|
164
|
+
return {
|
|
165
|
+
httpStatus,
|
|
166
|
+
error: classifyError(httpStatus, errorType, finalMessage),
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Map a terminal Responses error object to the HTTP status we record in /api/logs. */
|
|
171
|
+
export function httpStatusFromTerminalError(error: {
|
|
172
|
+
type?: string;
|
|
173
|
+
code?: string | null;
|
|
174
|
+
message?: string;
|
|
175
|
+
} | undefined): number {
|
|
176
|
+
if (!error) return 502;
|
|
177
|
+
if (error.type === "rate_limit_error" || error.code === "rate_limit_exceeded") return 429;
|
|
178
|
+
if (error.type === "authentication_error" || error.code === "invalid_api_key") return 401;
|
|
179
|
+
if (error.type === "insufficient_quota" || error.code === "insufficient_quota") return 429;
|
|
180
|
+
if (error.type === "server_error" && error.code === "server_is_overloaded") return 503;
|
|
181
|
+
if (error.type === "invalid_request_error") return 400;
|
|
182
|
+
if (error.type === "proxy_error") return 500;
|
|
183
|
+
const message = error.message ?? "";
|
|
184
|
+
if (message) return inferHttpStatusFromAdapterMessage(message);
|
|
185
|
+
return 502;
|
|
186
|
+
}
|
package/src/oauth/cursor.ts
CHANGED
|
@@ -32,6 +32,37 @@ export interface CursorAuthParams {
|
|
|
32
32
|
loginUrl: string;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
interface CursorJwtPayload {
|
|
36
|
+
sub?: unknown;
|
|
37
|
+
email?: unknown;
|
|
38
|
+
exp?: unknown;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function decodeCursorJwtPayload(token: string): CursorJwtPayload | undefined {
|
|
42
|
+
const parts = token.split(".");
|
|
43
|
+
const payload = parts[1];
|
|
44
|
+
if (parts.length !== 3 || !payload) return undefined;
|
|
45
|
+
try {
|
|
46
|
+
return JSON.parse(Buffer.from(payload, "base64url").toString("utf-8")) as CursorJwtPayload;
|
|
47
|
+
} catch {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Build OAuthCredentials from Cursor tokens, extracting stable identity from JWT `sub` for multiauth. */
|
|
53
|
+
export function credentialsFromCursorTokens(accessToken: string, refreshToken: string): OAuthCredentials {
|
|
54
|
+
const payload = decodeCursorJwtPayload(accessToken) ?? decodeCursorJwtPayload(refreshToken);
|
|
55
|
+
const accountId = typeof payload?.sub === "string" && payload.sub.length > 0 ? payload.sub : undefined;
|
|
56
|
+
const email = typeof payload?.email === "string" && payload.email.length > 0 ? payload.email.toLowerCase() : undefined;
|
|
57
|
+
return {
|
|
58
|
+
access: accessToken,
|
|
59
|
+
refresh: refreshToken,
|
|
60
|
+
expires: getTokenExpiry(accessToken),
|
|
61
|
+
...(accountId ? { accountId } : {}),
|
|
62
|
+
...(email ? { email } : {}),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
35
66
|
/** Generate PKCE params + the cursor.com deep-link login URL (challenge only — never the verifier). */
|
|
36
67
|
export async function generateCursorAuthParams(): Promise<CursorAuthParams> {
|
|
37
68
|
const { verifier, challenge } = await generatePKCE();
|
|
@@ -113,7 +144,7 @@ export async function loginCursor(
|
|
|
113
144
|
ctrl.onAuth?.({ url: loginUrl, instructions: "Approve the Cursor login in your browser, then return here." });
|
|
114
145
|
ctrl.onProgress?.("Waiting for Cursor login approval…");
|
|
115
146
|
const { accessToken, refreshToken } = await pollCursorAuth(uuid, verifier, ctrl.signal, pollBaseDelayMs);
|
|
116
|
-
return
|
|
147
|
+
return credentialsFromCursorTokens(accessToken, refreshToken);
|
|
117
148
|
}
|
|
118
149
|
|
|
119
150
|
function isRetryableRefreshStatus(status: number): boolean {
|
|
@@ -160,7 +191,7 @@ export async function refreshCursorToken(refresh: string, signal?: AbortSignal):
|
|
|
160
191
|
if (response.ok) {
|
|
161
192
|
const data = (await response.json()) as { accessToken?: string; refreshToken?: string };
|
|
162
193
|
if (!data.accessToken) throw new Error("Cursor refresh response missing access token");
|
|
163
|
-
return
|
|
194
|
+
return credentialsFromCursorTokens(data.accessToken, data.refreshToken || refresh);
|
|
164
195
|
}
|
|
165
196
|
if (!isRetryableRefreshStatus(response.status) || attempt === REFRESH_ATTEMPTS - 1) {
|
|
166
197
|
throw new Error(`Cursor token refresh failed: ${response.status}`);
|
|
@@ -174,15 +205,7 @@ export async function refreshCursorToken(refresh: string, signal?: AbortSignal):
|
|
|
174
205
|
|
|
175
206
|
/** Resolve a token's expiry (epoch ms) from its JWT `exp`, minus a 5-minute skew; ~1h fallback. */
|
|
176
207
|
export function getTokenExpiry(token: string): number {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
const payload = parts.length === 3 ? parts[1] : undefined;
|
|
180
|
-
if (payload) {
|
|
181
|
-
const decoded = JSON.parse(Buffer.from(payload, "base64url").toString("utf-8")) as { exp?: number };
|
|
182
|
-
if (typeof decoded.exp === "number") return decoded.exp * 1000 - EXPIRY_SKEW_MS;
|
|
183
|
-
}
|
|
184
|
-
} catch {
|
|
185
|
-
// fall through to the fixed fallback below
|
|
186
|
-
}
|
|
208
|
+
const decoded = decodeCursorJwtPayload(token);
|
|
209
|
+
if (typeof decoded?.exp === "number") return decoded.exp * 1000 - EXPIRY_SKEW_MS;
|
|
187
210
|
return Date.now() + FALLBACK_TTL_MS;
|
|
188
211
|
}
|
package/src/oauth/store.ts
CHANGED
|
@@ -10,9 +10,10 @@
|
|
|
10
10
|
* Exceptions:
|
|
11
11
|
* - `chatgpt` stays single-slot (always replaced): codex-auth-api uses it as a scratch slot
|
|
12
12
|
* for Codex pool logins, which have their own ledger (codex-accounts.json).
|
|
13
|
-
* - Credentials without identity (no accountId/email — kimi, kiro
|
|
14
|
-
*
|
|
15
|
-
*
|
|
13
|
+
* - Credentials without identity (no accountId/email — kimi, kiro) replace the active slot
|
|
14
|
+
* instead of appending: their refresh tokens rotate, so a derived id would duplicate the
|
|
15
|
+
* same human on every re-login. Cursor login extracts JWT `sub` as accountId so multiauth
|
|
16
|
+
* can append distinct accounts.
|
|
16
17
|
*/
|
|
17
18
|
import { createHash } from "node:crypto";
|
|
18
19
|
import { copyFileSync, existsSync, mkdirSync, readFileSync, chmodSync } from "node:fs";
|
package/src/providers/derive.ts
CHANGED
|
@@ -23,6 +23,8 @@ export interface DerivedKeyLoginProvider {
|
|
|
23
23
|
noPenaltyModels?: string[];
|
|
24
24
|
autoToolChoiceOnlyModels?: string[];
|
|
25
25
|
preserveReasoningContentModels?: string[];
|
|
26
|
+
thinkingToggleModels?: string[];
|
|
27
|
+
thinkingBudgetModels?: string[];
|
|
26
28
|
escapeBuiltinToolNames?: boolean;
|
|
27
29
|
}
|
|
28
30
|
|
|
@@ -80,9 +82,11 @@ export function providerConfigSeed(entry: ProviderRegistryEntry): OcxProviderCon
|
|
|
80
82
|
...(entry.noTemperatureModels ? { noTemperatureModels: [...entry.noTemperatureModels] } : {}),
|
|
81
83
|
...(entry.noTopPModels ? { noTopPModels: [...entry.noTopPModels] } : {}),
|
|
82
84
|
...(entry.noPenaltyModels ? { noPenaltyModels: [...entry.noPenaltyModels] } : {}),
|
|
85
|
+
...(entry.parallelToolCalls !== undefined ? { parallelToolCalls: entry.parallelToolCalls } : {}),
|
|
83
86
|
...(entry.autoToolChoiceOnlyModels ? { autoToolChoiceOnlyModels: [...entry.autoToolChoiceOnlyModels] } : {}),
|
|
84
87
|
...(entry.preserveReasoningContentModels ? { preserveReasoningContentModels: [...entry.preserveReasoningContentModels] } : {}),
|
|
85
88
|
...(entry.thinkingToggleModels ? { thinkingToggleModels: [...entry.thinkingToggleModels] } : {}),
|
|
89
|
+
...(entry.thinkingBudgetModels ? { thinkingBudgetModels: [...entry.thinkingBudgetModels] } : {}),
|
|
86
90
|
...(entry.escapeBuiltinToolNames !== undefined ? { escapeBuiltinToolNames: entry.escapeBuiltinToolNames } : {}),
|
|
87
91
|
...(entry.googleMode ? { googleMode: entry.googleMode } : {}),
|
|
88
92
|
...(entry.project ? { project: entry.project } : {}),
|
|
@@ -117,6 +121,8 @@ export function deriveKeyLoginMap(): Record<string, DerivedKeyLoginProvider> {
|
|
|
117
121
|
...(entry.noPenaltyModels ? { noPenaltyModels: [...entry.noPenaltyModels] } : {}),
|
|
118
122
|
...(entry.autoToolChoiceOnlyModels ? { autoToolChoiceOnlyModels: [...entry.autoToolChoiceOnlyModels] } : {}),
|
|
119
123
|
...(entry.preserveReasoningContentModels ? { preserveReasoningContentModels: [...entry.preserveReasoningContentModels] } : {}),
|
|
124
|
+
...(entry.thinkingToggleModels ? { thinkingToggleModels: [...entry.thinkingToggleModels] } : {}),
|
|
125
|
+
...(entry.thinkingBudgetModels ? { thinkingBudgetModels: [...entry.thinkingBudgetModels] } : {}),
|
|
120
126
|
...(entry.escapeBuiltinToolNames !== undefined ? { escapeBuiltinToolNames: entry.escapeBuiltinToolNames } : {}),
|
|
121
127
|
...(entry.googleMode ? { googleMode: entry.googleMode } : {}),
|
|
122
128
|
...(entry.project ? { project: entry.project } : {}),
|
|
@@ -177,9 +183,11 @@ export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig
|
|
|
177
183
|
if (!prov.noTemperatureModels && seed.noTemperatureModels) prov.noTemperatureModels = [...seed.noTemperatureModels];
|
|
178
184
|
if (!prov.noTopPModels && seed.noTopPModels) prov.noTopPModels = [...seed.noTopPModels];
|
|
179
185
|
if (!prov.noPenaltyModels && seed.noPenaltyModels) prov.noPenaltyModels = [...seed.noPenaltyModels];
|
|
186
|
+
if (prov.parallelToolCalls === undefined && seed.parallelToolCalls !== undefined) prov.parallelToolCalls = seed.parallelToolCalls;
|
|
180
187
|
if (!prov.autoToolChoiceOnlyModels && seed.autoToolChoiceOnlyModels) prov.autoToolChoiceOnlyModels = [...seed.autoToolChoiceOnlyModels];
|
|
181
188
|
if (!prov.preserveReasoningContentModels && seed.preserveReasoningContentModels) prov.preserveReasoningContentModels = [...seed.preserveReasoningContentModels];
|
|
182
189
|
if (!prov.thinkingToggleModels && seed.thinkingToggleModels) prov.thinkingToggleModels = [...seed.thinkingToggleModels];
|
|
190
|
+
if (!prov.thinkingBudgetModels && seed.thinkingBudgetModels) prov.thinkingBudgetModels = [...seed.thinkingBudgetModels];
|
|
183
191
|
if (prov.escapeBuiltinToolNames === undefined && seed.escapeBuiltinToolNames !== undefined) prov.escapeBuiltinToolNames = seed.escapeBuiltinToolNames;
|
|
184
192
|
}
|
|
185
193
|
|