@bitkyc08/opencodex 2.25.0 → 2.26.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/bin/ocx.mjs +59 -7
- package/gui/dist/assets/index-RL6b1bTV.js +102 -0
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/base.ts +18 -0
- package/src/adapters/client-fingerprint.ts +0 -2
- package/src/adapters/cursor/http1-bidi.ts +361 -0
- package/src/adapters/cursor/live-models.ts +117 -30
- package/src/adapters/cursor/live-transport.ts +137 -56
- package/src/adapters/cursor/native-exec-fs.ts +1 -1
- package/src/adapters/cursor/native-exec-network.ts +1 -1
- package/src/adapters/cursor/native-exec-shell.ts +0 -1
- package/src/adapters/cursor/protobuf-request.ts +89 -21
- package/src/adapters/cursor/tool-definitions.ts +18 -7
- package/src/adapters/cursor/tool-result-normalize.ts +92 -0
- package/src/adapters/cursor/transport.ts +7 -0
- package/src/adapters/cursor.ts +2 -0
- package/src/adapters/google-http.ts +38 -10
- package/src/adapters/google.ts +23 -3
- package/src/adapters/openai-chat.ts +38 -13
- package/src/adapters/openai-responses.ts +129 -9
- package/src/adapters/registry.ts +30 -1
- package/src/bridge.ts +50 -12
- package/src/chat/inbound.ts +1 -0
- package/src/claude/agents-inject.ts +3 -2
- package/src/cli/dispatch.ts +19 -6
- package/src/cli/help.ts +2 -1
- package/src/cli/integrations.ts +35 -0
- package/src/cli/minimax.ts +8 -2
- package/src/cli/registry.ts +13 -2
- package/src/clients/config-export.ts +120 -1
- package/src/codex/account-store.ts +17 -1
- package/src/codex/auth-api.ts +51 -17
- package/src/codex/catalog/effort.ts +8 -5
- package/src/codex/catalog/provider-fetch.ts +39 -27
- package/src/codex/catalog/sync.ts +30 -8
- package/src/codex/inject.ts +39 -13
- package/src/codex/main-account.ts +29 -1
- package/src/codex/plan-from-token.ts +140 -0
- package/src/codex/plan.ts +25 -0
- package/src/codex/prompt-journal.ts +6 -2
- package/src/codex/quota-rejection.ts +21 -7
- package/src/codex/refresh.ts +4 -2
- package/src/codex/sync.ts +106 -3
- package/src/codex/warmup.ts +187 -81
- package/src/config.ts +85 -46
- package/src/generated/compatibility-version.json +127 -83
- package/src/grok/inject.ts +1 -1
- package/src/images/loop.ts +1 -0
- package/src/integrations/registry.ts +7 -0
- package/src/lab/automation/config-persistence.ts +2 -2
- package/src/lab/automation/persistence.ts +2 -2
- package/src/lab/ledger/purge.ts +2 -2
- package/src/lab/subject/behavior-fingerprint.ts +2 -2
- package/src/lib/config-ownership.ts +5 -2
- package/src/lib/destination-policy.ts +18 -1
- package/src/lib/provider-outbound.ts +7 -0
- package/src/lib/redact.ts +11 -0
- package/src/lib/tool-argument-integers.ts +50 -6
- package/src/lib/upstream-http-version.ts +57 -0
- package/src/lib/upstream-retry.ts +2 -1
- package/src/lib/windows-atomic-replace.ts +155 -0
- package/src/lib/windows-service-wrappers.ts +72 -0
- package/src/oauth/google-antigravity.ts +2 -2
- package/src/oauth/index.ts +60 -8
- package/src/providers/antigravity-models.ts +58 -4
- package/src/providers/command-code-efforts.ts +36 -9
- package/src/providers/context-cap.ts +9 -0
- package/src/providers/derive.ts +4 -0
- package/src/providers/fastwire.ts +453 -0
- package/src/providers/registry.ts +21 -1
- package/src/providers/service-tier.ts +173 -89
- package/src/responses/parser.ts +25 -16
- package/src/responses/reasoning-replay-cache.ts +30 -0
- package/src/responses/thought-signature-replay.ts +74 -4
- package/src/router.ts +6 -0
- package/src/routing/compatibility/behavior.ts +27 -16
- package/src/server/index.ts +10 -1
- package/src/server/management/oauth-account-routes.ts +10 -2
- package/src/server/management/shared.ts +1 -1
- package/src/server/management/system-routes.ts +15 -0
- package/src/server/request-log.ts +57 -3
- package/src/server/responses/agent-task-recovery.ts +6 -1
- package/src/server/responses/core.ts +209 -40
- package/src/server/responses/fetch-helpers.ts +15 -36
- package/src/server/responses/responses-field-backfill.ts +173 -0
- package/src/server/responses-reasoning-summary-rewrite.ts +171 -0
- package/src/service.ts +38 -36
- package/src/storage/cleanup.ts +2 -2
- package/src/tray/windows.ts +3 -2
- package/src/types.ts +101 -14
- package/src/update/job.ts +9 -18
- package/src/update/transactional-install.d.mts +22 -0
- package/src/update/transactional-install.mjs +259 -0
- package/src/usage/cost.ts +34 -5
- package/src/usage/log.ts +74 -4
- package/src/vision/anthropic-describe.ts +10 -6
- package/src/web-search/anthropic-executor.ts +8 -6
- package/gui/dist/assets/index-DxJ7kXj9.js +0 -102
package/src/codex/warmup.ts
CHANGED
|
@@ -1,19 +1,18 @@
|
|
|
1
|
+
import { readBoundedResponseBody } from "../lib/bounded-body";
|
|
2
|
+
|
|
1
3
|
export class CodexWarmupError extends Error {
|
|
2
|
-
code: "http_status" | "missing_body" | "stream_failed" | "stream_incomplete" | "stream_error" | "invalid_sse" | "no_terminal" | "transport";
|
|
4
|
+
code: "http_status" | "missing_body" | "stream_failed" | "stream_incomplete" | "stream_error" | "stream_too_large" | "invalid_sse" | "no_terminal" | "transport";
|
|
3
5
|
status?: number;
|
|
4
|
-
/** Upstream error detail extracted from the response body (truncated to 512 chars). */
|
|
5
|
-
upstreamDetail?: string;
|
|
6
6
|
|
|
7
7
|
constructor(
|
|
8
8
|
code: CodexWarmupError["code"],
|
|
9
9
|
message = "Codex warmup failed",
|
|
10
|
-
options: { status?: number; cause?: unknown
|
|
10
|
+
options: { status?: number; cause?: unknown } = {},
|
|
11
11
|
) {
|
|
12
12
|
super(message);
|
|
13
13
|
this.name = "CodexWarmupError";
|
|
14
14
|
this.code = code;
|
|
15
15
|
this.status = options.status;
|
|
16
|
-
this.upstreamDetail = options.upstreamDetail;
|
|
17
16
|
if (options.cause !== undefined) this.cause = options.cause;
|
|
18
17
|
}
|
|
19
18
|
}
|
|
@@ -29,37 +28,31 @@ const CODEX_RESPONSES_URL = "https://chatgpt.com/backend-api/codex/responses";
|
|
|
29
28
|
const DEFAULT_MODEL = "gpt-5.4-mini";
|
|
30
29
|
const FALLBACK_MODELS = ["gpt-5.5"];
|
|
31
30
|
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
31
|
+
const MAX_TIMEOUT_MS = 0x7fff_ffff;
|
|
32
32
|
const MAX_ERROR_BODY_BYTES = 2048;
|
|
33
|
+
const MAX_WARMUP_STREAM_BYTES = 1024 * 1024;
|
|
33
34
|
|
|
34
|
-
/**
|
|
35
|
-
async function
|
|
35
|
+
/** Bound and release an upstream error body without exposing provider-controlled text. */
|
|
36
|
+
async function drainErrorBody(res: Response, signal: AbortSignal): Promise<void> {
|
|
36
37
|
try {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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.
|
|
38
|
+
await readBoundedResponseBody(res, {
|
|
39
|
+
signal,
|
|
40
|
+
maxBytes: MAX_ERROR_BODY_BYTES,
|
|
41
|
+
fatalUtf8: true,
|
|
42
|
+
});
|
|
43
|
+
} catch (error) {
|
|
44
|
+
if (signal.aborted) {
|
|
45
|
+
throw new CodexWarmupError("transport", "Codex warmup request failed", {
|
|
46
|
+
cause: error,
|
|
47
|
+
});
|
|
52
48
|
}
|
|
53
|
-
|
|
54
|
-
} catch {
|
|
55
|
-
return undefined;
|
|
49
|
+
// The bounded reader owns cancellation for oversized, invalid, or stalled bodies.
|
|
56
50
|
}
|
|
57
51
|
}
|
|
58
52
|
|
|
59
53
|
function safeWarmupReason(err: unknown): string {
|
|
60
54
|
if (err instanceof CodexWarmupError) {
|
|
61
|
-
|
|
62
|
-
return err.upstreamDetail ? `${base} — ${err.upstreamDetail}` : base;
|
|
55
|
+
return err.status ? `${err.code}:${err.status}` : err.code;
|
|
63
56
|
}
|
|
64
57
|
return "transport";
|
|
65
58
|
}
|
|
@@ -89,83 +82,196 @@ function parseSseFrame(frame: string): unknown | null {
|
|
|
89
82
|
}
|
|
90
83
|
}
|
|
91
84
|
|
|
92
|
-
async function drainWarmupSse(body: ReadableStream<Uint8Array
|
|
85
|
+
async function drainWarmupSse(body: ReadableStream<Uint8Array>, signal: AbortSignal): Promise<void> {
|
|
93
86
|
const reader = body.getReader();
|
|
94
87
|
const decoder = new TextDecoder();
|
|
95
|
-
let buffer =
|
|
88
|
+
let buffer = new Uint8Array(Math.min(MAX_WARMUP_STREAM_BYTES, 64 * 1024));
|
|
89
|
+
let bufferedBytes = 0;
|
|
90
|
+
let scanOffset = 0;
|
|
91
|
+
let bytesRead = 0;
|
|
92
|
+
const abortError = () => new CodexWarmupError("transport", "Codex warmup request failed", {
|
|
93
|
+
cause: signal.reason,
|
|
94
|
+
});
|
|
95
|
+
const cancelReader = () => {
|
|
96
|
+
try {
|
|
97
|
+
void reader.cancel(signal.reason).catch(() => {});
|
|
98
|
+
} catch {
|
|
99
|
+
// Some custom stream implementations throw synchronously from cancel().
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
// Fetch implementations usually error the response body when their signal is
|
|
103
|
+
// aborted, but a ReadableStream is not intrinsically coupled to that signal.
|
|
104
|
+
// Race only the currently pending read against a removable abort listener;
|
|
105
|
+
// Bun 1.3 can leave read() parked until a custom source's cancel promise
|
|
106
|
+
// settles, while a shared never-settled race promise would retain one handler
|
|
107
|
+
// per chunk. Cancellation remains best-effort and is never awaited.
|
|
108
|
+
const readWithSignal = (): Promise<Awaited<ReturnType<typeof reader.read>>> => {
|
|
109
|
+
if (signal.aborted) {
|
|
110
|
+
cancelReader();
|
|
111
|
+
return Promise.reject(abortError());
|
|
112
|
+
}
|
|
113
|
+
const read = reader.read();
|
|
114
|
+
void read.catch(() => {});
|
|
115
|
+
return new Promise((resolve, reject) => {
|
|
116
|
+
let settled = false;
|
|
117
|
+
const finish = (action: () => void) => {
|
|
118
|
+
if (settled) return;
|
|
119
|
+
settled = true;
|
|
120
|
+
signal.removeEventListener("abort", onAbort);
|
|
121
|
+
action();
|
|
122
|
+
};
|
|
123
|
+
const onAbort = () => {
|
|
124
|
+
cancelReader();
|
|
125
|
+
finish(() => reject(abortError()));
|
|
126
|
+
};
|
|
127
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
128
|
+
if (signal.aborted) {
|
|
129
|
+
onAbort();
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
read.then(
|
|
133
|
+
result => finish(() => resolve(result)),
|
|
134
|
+
error => finish(() => reject(error)),
|
|
135
|
+
);
|
|
136
|
+
});
|
|
137
|
+
};
|
|
138
|
+
const ensureCapacity = (requiredBytes: number) => {
|
|
139
|
+
if (requiredBytes <= buffer.byteLength) return;
|
|
140
|
+
const grown = new Uint8Array(Math.min(
|
|
141
|
+
MAX_WARMUP_STREAM_BYTES,
|
|
142
|
+
Math.max(requiredBytes, buffer.byteLength * 2),
|
|
143
|
+
));
|
|
144
|
+
grown.set(buffer.subarray(0, bufferedBytes));
|
|
145
|
+
buffer = grown;
|
|
146
|
+
};
|
|
147
|
+
const findFrameDelimiter = (start: number): { index: number; length: 2 | 3 | 4 } | undefined => {
|
|
148
|
+
for (let index = start; index < bufferedBytes - 1; index += 1) {
|
|
149
|
+
const firstLength = buffer[index] === 10
|
|
150
|
+
? 1
|
|
151
|
+
: buffer[index] === 13 && buffer[index + 1] === 10 ? 2 : 0;
|
|
152
|
+
if (firstLength === 0) continue;
|
|
153
|
+
const secondStart = index + firstLength;
|
|
154
|
+
const secondLength = buffer[secondStart] === 10
|
|
155
|
+
? 1
|
|
156
|
+
: buffer[secondStart] === 13 && buffer[secondStart + 1] === 10 ? 2 : 0;
|
|
157
|
+
if (secondLength > 0) return { index, length: (firstLength + secondLength) as 2 | 3 | 4 };
|
|
158
|
+
}
|
|
159
|
+
return undefined;
|
|
160
|
+
};
|
|
161
|
+
const acceptFrame = (frame: Uint8Array): boolean => {
|
|
162
|
+
const parsed = parseSseFrame(decoder.decode(frame));
|
|
163
|
+
const type = eventTypeFromData(parsed);
|
|
164
|
+
if (type === "response.completed") return true;
|
|
165
|
+
if (type === "response.failed") throw new CodexWarmupError("stream_failed");
|
|
166
|
+
if (type === "response.incomplete") throw new CodexWarmupError("stream_incomplete");
|
|
167
|
+
if (type === "error") throw new CodexWarmupError("stream_error");
|
|
168
|
+
return false;
|
|
169
|
+
};
|
|
96
170
|
|
|
97
171
|
try {
|
|
172
|
+
if (signal.aborted) throw abortError();
|
|
98
173
|
for (;;) {
|
|
99
|
-
const { done, value } = await
|
|
174
|
+
const { done, value } = await readWithSignal();
|
|
175
|
+
if (signal.aborted) throw abortError();
|
|
100
176
|
if (done) break;
|
|
101
|
-
|
|
177
|
+
if (value.byteLength > MAX_WARMUP_STREAM_BYTES - bytesRead) {
|
|
178
|
+
throw new CodexWarmupError("stream_too_large", "Codex warmup stream exceeded the size limit");
|
|
179
|
+
}
|
|
180
|
+
bytesRead += value.byteLength;
|
|
181
|
+
ensureCapacity(bufferedBytes + value.byteLength);
|
|
182
|
+
buffer.set(value, bufferedBytes);
|
|
183
|
+
bufferedBytes += value.byteLength;
|
|
102
184
|
|
|
185
|
+
let consumedBytes = 0;
|
|
103
186
|
for (;;) {
|
|
104
|
-
const
|
|
105
|
-
if (
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
187
|
+
const delimiter = findFrameDelimiter(scanOffset);
|
|
188
|
+
if (!delimiter) {
|
|
189
|
+
// A delimiter can start at most three bytes before the next chunk.
|
|
190
|
+
scanOffset = Math.max(consumedBytes, bufferedBytes - 3);
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
if (acceptFrame(buffer.subarray(consumedBytes, delimiter.index))) return;
|
|
194
|
+
consumedBytes = delimiter.index + delimiter.length;
|
|
195
|
+
scanOffset = consumedBytes;
|
|
196
|
+
}
|
|
197
|
+
if (consumedBytes > 0) {
|
|
198
|
+
buffer.copyWithin(0, consumedBytes, bufferedBytes);
|
|
199
|
+
bufferedBytes -= consumedBytes;
|
|
200
|
+
scanOffset = Math.max(0, scanOffset - consumedBytes);
|
|
115
201
|
}
|
|
116
202
|
}
|
|
117
203
|
|
|
118
|
-
if (buffer.
|
|
119
|
-
const parsed = parseSseFrame(buffer);
|
|
120
|
-
const type = eventTypeFromData(parsed);
|
|
121
|
-
if (type === "response.completed") return;
|
|
122
|
-
if (type === "response.failed") throw new CodexWarmupError("stream_failed");
|
|
123
|
-
if (type === "response.incomplete") throw new CodexWarmupError("stream_incomplete");
|
|
124
|
-
if (type === "error") throw new CodexWarmupError("stream_error");
|
|
125
|
-
}
|
|
204
|
+
if (bufferedBytes > 0 && acceptFrame(buffer.subarray(0, bufferedBytes))) return;
|
|
126
205
|
|
|
127
206
|
throw new CodexWarmupError("no_terminal", "Codex warmup ended before completion");
|
|
128
207
|
} finally {
|
|
129
|
-
|
|
208
|
+
try {
|
|
209
|
+
reader.releaseLock();
|
|
210
|
+
} catch {
|
|
211
|
+
// A hostile cancel promise may keep the final read locked after timeout.
|
|
212
|
+
}
|
|
130
213
|
}
|
|
131
214
|
}
|
|
132
215
|
|
|
133
216
|
async function tryWarmup(options: CodexWarmupOptions, model: string): Promise<void> {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
headers: {
|
|
139
|
-
Authorization: `Bearer ${options.accessToken}`,
|
|
140
|
-
"ChatGPT-Account-Id": options.chatgptAccountId,
|
|
141
|
-
"Content-Type": "application/json",
|
|
142
|
-
},
|
|
143
|
-
body: JSON.stringify({
|
|
144
|
-
model,
|
|
145
|
-
instructions: "Reply with OK.",
|
|
146
|
-
input: [{ type: "message", role: "user", content: [{ type: "input_text", text: "hi" }] }],
|
|
147
|
-
stream: true,
|
|
148
|
-
store: false,
|
|
149
|
-
}),
|
|
150
|
-
signal: AbortSignal.timeout(options.timeoutMs ?? DEFAULT_TIMEOUT_MS),
|
|
151
|
-
});
|
|
152
|
-
} catch (err) {
|
|
153
|
-
throw new CodexWarmupError("transport", "Codex warmup request failed", { cause: err });
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
if (!res.ok) {
|
|
157
|
-
const upstreamDetail = await readErrorDetail(res);
|
|
158
|
-
throw new CodexWarmupError("http_status", "Codex warmup was rejected", {
|
|
159
|
-
status: res.status,
|
|
160
|
-
upstreamDetail,
|
|
217
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
218
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 0 || timeoutMs > MAX_TIMEOUT_MS) {
|
|
219
|
+
throw new CodexWarmupError("transport", "Codex warmup request failed", {
|
|
220
|
+
cause: new RangeError("Codex warmup timeout is outside the supported range"),
|
|
161
221
|
});
|
|
162
222
|
}
|
|
163
|
-
|
|
223
|
+
// Bun 1.3 can leave AbortSignal.timeout() dormant while a custom response
|
|
224
|
+
// stream has a pending read. A ref'ed timer and explicit controller make the
|
|
225
|
+
// same deadline cover response headers and the full success/error body.
|
|
226
|
+
const deadline = new AbortController();
|
|
227
|
+
const signal = deadline.signal;
|
|
228
|
+
const timer = setTimeout(() => {
|
|
229
|
+
deadline.abort(new DOMException("Codex warmup timed out", "TimeoutError"));
|
|
230
|
+
}, timeoutMs);
|
|
164
231
|
|
|
165
232
|
try {
|
|
166
|
-
|
|
233
|
+
let res: Response;
|
|
234
|
+
try {
|
|
235
|
+
res = await fetch(CODEX_RESPONSES_URL, {
|
|
236
|
+
method: "POST",
|
|
237
|
+
headers: {
|
|
238
|
+
Authorization: `Bearer ${options.accessToken}`,
|
|
239
|
+
"ChatGPT-Account-Id": options.chatgptAccountId,
|
|
240
|
+
"Content-Type": "application/json",
|
|
241
|
+
},
|
|
242
|
+
body: JSON.stringify({
|
|
243
|
+
model,
|
|
244
|
+
instructions: "Reply with OK.",
|
|
245
|
+
input: [{ type: "message", role: "user", content: [{ type: "input_text", text: "hi" }] }],
|
|
246
|
+
stream: true,
|
|
247
|
+
store: false,
|
|
248
|
+
}),
|
|
249
|
+
signal,
|
|
250
|
+
});
|
|
251
|
+
} catch (err) {
|
|
252
|
+
throw new CodexWarmupError("transport", "Codex warmup request failed", { cause: err });
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (!res.ok) {
|
|
256
|
+
await drainErrorBody(res, signal);
|
|
257
|
+
throw new CodexWarmupError("http_status", "Codex warmup was rejected", {
|
|
258
|
+
status: res.status,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
const body = res.body;
|
|
262
|
+
if (!body) throw new CodexWarmupError("missing_body");
|
|
263
|
+
|
|
264
|
+
try {
|
|
265
|
+
await drainWarmupSse(body, signal);
|
|
266
|
+
} finally {
|
|
267
|
+
try {
|
|
268
|
+
void body.cancel().catch(() => {});
|
|
269
|
+
} catch {
|
|
270
|
+
// Some custom stream implementations throw synchronously from cancel().
|
|
271
|
+
}
|
|
272
|
+
}
|
|
167
273
|
} finally {
|
|
168
|
-
|
|
274
|
+
clearTimeout(timer);
|
|
169
275
|
}
|
|
170
276
|
}
|
|
171
277
|
|
package/src/config.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
2
|
import { createHash, randomUUID } from "node:crypto";
|
|
3
|
-
import { chmodSync, constants as fsConstants, copyFileSync, existsSync, linkSync, lstatSync, mkdirSync, readFileSync, realpathSync,
|
|
3
|
+
import { chmodSync, constants as fsConstants, copyFileSync, existsSync, linkSync, lstatSync, mkdirSync, readFileSync, realpathSync, truncateSync, unlinkSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { dirname, join, resolve } from "node:path";
|
|
6
6
|
import { Database } from "bun:sqlite";
|
|
@@ -65,9 +65,11 @@ import {
|
|
|
65
65
|
type OcxConfig,
|
|
66
66
|
type OcxApiKeyEntry,
|
|
67
67
|
type OcxProviderConfig,
|
|
68
|
+
type FastWire,
|
|
68
69
|
type ProviderCostOverlay,
|
|
69
70
|
} from "./types";
|
|
70
71
|
import { isCanonicalOpenAiForwardProvider, OPENAI_CODEX_PROVIDER_ID } from "./providers/openai-tiers";
|
|
72
|
+
import { fastWireDeclarationError, hasFastWireCapabilityConflict } from "./providers/fastwire";
|
|
71
73
|
import {
|
|
72
74
|
getProviderRegistryEntry,
|
|
73
75
|
providerMatchesRegistryTransport,
|
|
@@ -93,34 +95,13 @@ import { isHostedToolUnsupportedForModel } from "./responses/hosted-tool-policy"
|
|
|
93
95
|
|
|
94
96
|
let _atomicSeq = 0;
|
|
95
97
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
source: string,
|
|
104
|
-
destination: string,
|
|
105
|
-
io: AtomicRenameIO = {
|
|
106
|
-
platform: process.platform,
|
|
107
|
-
rename: renameSync,
|
|
108
|
-
sleep: Bun.sleepSync,
|
|
109
|
-
},
|
|
110
|
-
): void {
|
|
111
|
-
for (let attempt = 0; ; attempt += 1) {
|
|
112
|
-
try {
|
|
113
|
-
io.rename(source, destination);
|
|
114
|
-
return;
|
|
115
|
-
} catch (error) {
|
|
116
|
-
const code = (error as NodeJS.ErrnoException).code;
|
|
117
|
-
const transientWindowsError = io.platform === "win32"
|
|
118
|
-
&& (code === "EBUSY" || code === "EPERM" || code === "EACCES");
|
|
119
|
-
if (!transientWindowsError || attempt >= 2) throw error;
|
|
120
|
-
io.sleep(25 * (attempt + 1));
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
}
|
|
98
|
+
// The Windows-tolerant replace lives in lib/windows-atomic-replace: config-ownership
|
|
99
|
+
// is one of its callers and this module already imports config-ownership, so
|
|
100
|
+
// exporting it from here would close an import cycle. Re-exported because these
|
|
101
|
+
// names are part of this module's public surface and its callers.
|
|
102
|
+
export type { AtomicRenameIO } from "./lib/windows-atomic-replace";
|
|
103
|
+
export { renameAtomicFile } from "./lib/windows-atomic-replace";
|
|
104
|
+
import { renameAtomicFile, renameAtomicFileAsync } from "./lib/windows-atomic-replace";
|
|
124
105
|
|
|
125
106
|
/**
|
|
126
107
|
* Write a file atomically (temp + rename) so concurrent writers — e.g. `ocx stop` and the
|
|
@@ -284,21 +265,6 @@ export interface AtomicWriteAsyncTestSeam {
|
|
|
284
265
|
afterTempWrite?: (tempPath: string) => void | Promise<void>;
|
|
285
266
|
}
|
|
286
267
|
|
|
287
|
-
async function renameAtomicFileAsync(source: string, destination: string): Promise<void> {
|
|
288
|
-
for (let attempt = 0; ; attempt += 1) {
|
|
289
|
-
try {
|
|
290
|
-
renameSync(source, destination);
|
|
291
|
-
return;
|
|
292
|
-
} catch (error) {
|
|
293
|
-
const code = (error as NodeJS.ErrnoException).code;
|
|
294
|
-
const transientWindowsError = process.platform === "win32"
|
|
295
|
-
&& (code === "EBUSY" || code === "EPERM" || code === "EACCES");
|
|
296
|
-
if (!transientWindowsError || attempt >= 2) throw error;
|
|
297
|
-
await Bun.sleep(25 * (attempt + 1));
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
|
|
302
268
|
/**
|
|
303
269
|
* Async atomic write (#612): same temp+harden+rename and residual-temp policy as
|
|
304
270
|
* atomicWriteFile, but Windows ACL harden yields the event loop. Timeout memo is keyed
|
|
@@ -662,12 +628,14 @@ function resolveRuntimePortPath(): string {
|
|
|
662
628
|
}
|
|
663
629
|
|
|
664
630
|
const warnedConfigFallbacks = new Set<string>();
|
|
631
|
+
const warnedInheritedFastWireConflicts = new Set<string>();
|
|
665
632
|
let lastWarningReconciledGeneration = 0;
|
|
666
633
|
|
|
667
634
|
export function reconcileConfigWarningMemos(generation: number): number {
|
|
668
635
|
if (generation <= lastWarningReconciledGeneration) return 0;
|
|
669
|
-
const removed = warnedConfigFallbacks.size;
|
|
636
|
+
const removed = warnedConfigFallbacks.size + warnedInheritedFastWireConflicts.size;
|
|
670
637
|
warnedConfigFallbacks.clear();
|
|
638
|
+
warnedInheritedFastWireConflicts.clear();
|
|
671
639
|
lastWarningReconciledGeneration = generation;
|
|
672
640
|
return removed;
|
|
673
641
|
}
|
|
@@ -716,6 +684,16 @@ export function requestPacingConfigError(value: unknown): string | null {
|
|
|
716
684
|
return "requestPacing must contain enabled and a valid requestsPerMinute/minIntervalMs provider rule or model overrides";
|
|
717
685
|
}
|
|
718
686
|
|
|
687
|
+
const fastWireSchema = z.object({
|
|
688
|
+
kind: z.string(),
|
|
689
|
+
canonicalToWire: z.record(z.string().trim(), z.string().trim()),
|
|
690
|
+
foreignCallerTiers: z.string(),
|
|
691
|
+
betas: z.array(z.string().trim()).optional(),
|
|
692
|
+
}).strict().superRefine((fastWire, ctx) => {
|
|
693
|
+
const error = fastWireDeclarationError({ fastWire });
|
|
694
|
+
if (error) ctx.addIssue({ code: "custom", message: error });
|
|
695
|
+
}).transform(fastWire => fastWire as FastWire);
|
|
696
|
+
|
|
719
697
|
/**
|
|
720
698
|
* Zod schema for one provider entry: known fields are validated strictly while unknown
|
|
721
699
|
* fields pass through (preserved for runtime extensions).
|
|
@@ -731,6 +709,7 @@ const providerConfigSchema = z.object({
|
|
|
731
709
|
responsesPath: z.string().min(1).optional(),
|
|
732
710
|
statelessResponses: z.boolean().optional(),
|
|
733
711
|
requiresAdjacentResponsesToolResults: z.boolean().optional(),
|
|
712
|
+
fastWire: fastWireSchema.nullable().optional(),
|
|
734
713
|
supportsServiceTier: z.boolean().optional(),
|
|
735
714
|
modelSupportsServiceTier: z.record(z.string().min(1), z.boolean()).optional(),
|
|
736
715
|
preserveResponsesReasoningContent: z.boolean().optional(),
|
|
@@ -1416,6 +1395,13 @@ const configSchema = z.object({
|
|
|
1416
1395
|
});
|
|
1417
1396
|
}
|
|
1418
1397
|
const provider = config.providers[name];
|
|
1398
|
+
if (hasFastWireCapabilityConflict(provider)) {
|
|
1399
|
+
ctx.addIssue({
|
|
1400
|
+
code: "custom",
|
|
1401
|
+
path: ["providers", redactSecretString(name), "fastWire"],
|
|
1402
|
+
message: "fastWire=null conflicts with supportsServiceTier=true",
|
|
1403
|
+
});
|
|
1404
|
+
}
|
|
1419
1405
|
const openRouterRoutingError = openRouterRoutingConfigError(provider);
|
|
1420
1406
|
if (openRouterRoutingError) {
|
|
1421
1407
|
ctx.addIssue({
|
|
@@ -2154,6 +2140,52 @@ function warnDegradedNativeSubagentConfig(rawParsed: unknown, config: OcxConfig)
|
|
|
2154
2140
|
}
|
|
2155
2141
|
}
|
|
2156
2142
|
|
|
2143
|
+
/**
|
|
2144
|
+
* Registry metadata can gain service-tier capability after a config was written. An explicit
|
|
2145
|
+
* `fastWire: null` remains authoritative on load and on whole-document writes; rejecting either
|
|
2146
|
+
* would discard or lock access to unrelated providers and API keys. Direct contradictions within
|
|
2147
|
+
* one provider row remain schema errors through the outer config refinement, where the dynamic
|
|
2148
|
+
* provider name can be redacted before it reaches diagnostics.
|
|
2149
|
+
*/
|
|
2150
|
+
function inheritedFastWireConflictProviderNames(
|
|
2151
|
+
config: Pick<OcxConfig, "providers">,
|
|
2152
|
+
): string[] {
|
|
2153
|
+
const conflicts: string[] = [];
|
|
2154
|
+
for (const [name, provider] of Object.entries(config.providers)) {
|
|
2155
|
+
if (provider.fastWire !== null || provider.supportsServiceTier === false) continue;
|
|
2156
|
+
const registry = providerMatchesRegistryTransport(name, provider)
|
|
2157
|
+
? getProviderRegistryEntry(name)
|
|
2158
|
+
: undefined;
|
|
2159
|
+
if (!registry) continue;
|
|
2160
|
+
const effectiveProviderCapability = provider.supportsServiceTier ?? registry.supportsServiceTier;
|
|
2161
|
+
const effectiveModelCapabilities = {
|
|
2162
|
+
...(registry.modelSupportsServiceTier ?? {}),
|
|
2163
|
+
...(provider.modelSupportsServiceTier ?? {}),
|
|
2164
|
+
};
|
|
2165
|
+
if (
|
|
2166
|
+
effectiveProviderCapability === true
|
|
2167
|
+
|| Object.values(effectiveModelCapabilities).some(value => value === true)
|
|
2168
|
+
) {
|
|
2169
|
+
conflicts.push(name);
|
|
2170
|
+
}
|
|
2171
|
+
}
|
|
2172
|
+
return conflicts;
|
|
2173
|
+
}
|
|
2174
|
+
|
|
2175
|
+
function inheritedFastWireConflictWarning(name: string): string {
|
|
2176
|
+
return `providers.${redactSecretString(name)}.fastWire=null overrides service-tier capability inherited from the matching registry entry`;
|
|
2177
|
+
}
|
|
2178
|
+
|
|
2179
|
+
function warnInheritedFastWireConflicts(configPath: string, config: OcxConfig): void {
|
|
2180
|
+
const names = inheritedFastWireConflictProviderNames(config);
|
|
2181
|
+
if (names.length === 0 || warnedInheritedFastWireConflicts.has(configPath)) return;
|
|
2182
|
+
warnedInheritedFastWireConflicts.add(configPath);
|
|
2183
|
+
console.warn(
|
|
2184
|
+
`⚠️ config.json ${names.map(inheritedFastWireConflictWarning).join("; ")}. `
|
|
2185
|
+
+ "The persisted providers and API keys were preserved.",
|
|
2186
|
+
);
|
|
2187
|
+
}
|
|
2188
|
+
|
|
2157
2189
|
/**
|
|
2158
2190
|
* Load and validate config.json into an OcxConfig. Missing files reset to
|
|
2159
2191
|
* defaults and clear stale overlays. Broken existing files also fall back to
|
|
@@ -2178,6 +2210,7 @@ export function loadConfig(): OcxConfig {
|
|
|
2178
2210
|
const result = configSchema.safeParse(parsed);
|
|
2179
2211
|
if (result.success) {
|
|
2180
2212
|
const config = normalizeApiKeyIds(result.data as OcxConfig);
|
|
2213
|
+
warnInheritedFastWireConflicts(configPath, config);
|
|
2181
2214
|
warnDegradedStreamMode(parsed, config);
|
|
2182
2215
|
warnDegradedHostname(parsed, config);
|
|
2183
2216
|
warnDegradedApiKeys(parsed, config);
|
|
@@ -2202,6 +2235,7 @@ export function loadConfig(): OcxConfig {
|
|
|
2202
2235
|
if (retryResult.success) {
|
|
2203
2236
|
warnConfigRepaired(configPath, result.error);
|
|
2204
2237
|
const config = normalizeApiKeyIds(retryResult.data as OcxConfig);
|
|
2238
|
+
warnInheritedFastWireConflicts(configPath, config);
|
|
2205
2239
|
warnDegradedHostname(parsed, config);
|
|
2206
2240
|
warnDegradedApiKeys(parsed, config);
|
|
2207
2241
|
warnDegradedCodexAccountPriorities(parsed, config);
|
|
@@ -2221,6 +2255,7 @@ export function loadConfig(): OcxConfig {
|
|
|
2221
2255
|
{
|
|
2222
2256
|
warnDroppedConfigSections(configPath, salvaged.dropped, salvaged.issues);
|
|
2223
2257
|
const config = normalizeApiKeyIds(salvaged.parsed);
|
|
2258
|
+
warnInheritedFastWireConflicts(configPath, config);
|
|
2224
2259
|
warnDegradedHostname(parsed, config);
|
|
2225
2260
|
warnDegradedApiKeys(parsed, config);
|
|
2226
2261
|
warnDegradedCodexAccountPriorities(parsed, config);
|
|
@@ -2280,6 +2315,7 @@ function validFileConfigDiagnostics(config: OcxConfig, rawParsed: unknown): Conf
|
|
|
2280
2315
|
const rawEffort = rawClaudeSubagentEffort(rawParsed);
|
|
2281
2316
|
const normalized = normalizeClaudeSubagentEffort(normalizeNativeSubagentSync(config, rawParsed), rawParsed);
|
|
2282
2317
|
const warnings = configPlaceholderWarnings(normalized);
|
|
2318
|
+
warnings.push(...inheritedFastWireConflictProviderNames(normalized).map(inheritedFastWireConflictWarning));
|
|
2283
2319
|
warnings.push(...degradedCodexAccountPriorityWarnings(rawParsed, normalized));
|
|
2284
2320
|
if (rawEffort !== undefined && !isClaudeSubagentEffort(rawEffort)) {
|
|
2285
2321
|
warnings.push(`claudeCode.subagentEffort ignored: expected one of ${CLAUDE_SUBAGENT_EFFORTS.join(", ")}`);
|
|
@@ -2493,7 +2529,10 @@ export function validateConfigCandidate(value: unknown): { ok: true; config: Ocx
|
|
|
2493
2529
|
?? loopbackListenerPortError(value);
|
|
2494
2530
|
if (boundaryError) return { ok: false, error: boundaryError };
|
|
2495
2531
|
const result = configSchema.safeParse(value);
|
|
2496
|
-
if (result.success)
|
|
2532
|
+
if (result.success) {
|
|
2533
|
+
const config = normalizeApiKeyIds(result.data as OcxConfig);
|
|
2534
|
+
return { ok: true, config };
|
|
2535
|
+
}
|
|
2497
2536
|
return { ok: false, error: schemaDiagnosticsError(result.error) };
|
|
2498
2537
|
}
|
|
2499
2538
|
|