@bitkyc08/opencodex 2.44.0 → 2.45.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/gui/dist/assets/index-CCfD72yq.js +115 -0
- package/gui/dist/assets/index-J96sug5C.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/openai-responses.ts +14 -0
- package/src/chat/outbound.ts +286 -35
- package/src/claude/compatibility.ts +192 -0
- package/src/claude/model-info.ts +14 -2
- package/src/cli/account-extended.ts +24 -1
- package/src/cli/init.ts +70 -13
- package/src/codex/catalog/provider-fetch.ts +38 -9
- package/src/codex/catalog/sync.ts +34 -2
- package/src/codex/catalog.ts +1 -1
- package/src/config/initialize.ts +132 -0
- package/src/config/rebase-provenance.ts +26 -0
- package/src/config.ts +50 -1
- package/src/generated/compatibility-version.json +41 -29
- package/src/lib/windows-secret-acl.ts +8 -4
- package/src/providers/quota.ts +26 -15
- package/src/responses/state.ts +48 -3
- package/src/server/chat-completions.ts +32 -36
- package/src/server/chat-native-sse.ts +23 -3
- package/src/server/chat-native.ts +15 -8
- package/src/server/claude-messages.ts +27 -0
- package/src/server/index.ts +5 -1
- package/src/server/management/agent-settings-routes.ts +101 -14
- package/src/server/management/logs-usage-routes.ts +9 -2
- package/src/server/request-log-cursor.ts +84 -0
- package/src/server/request-log.ts +46 -3
- package/src/server/responses/agent-task-recovery.ts +50 -14
- package/src/server/responses/codex-ws-exchange.ts +79 -0
- package/src/server/responses/compact.ts +39 -33
- package/src/server/responses/core.ts +43 -27
- package/src/storage/cleanup.ts +49 -35
- package/src/types/config.ts +4 -0
- package/src/usage/log.ts +22 -0
- package/gui/dist/assets/index-B7_K1Hsj.js +0 -115
- package/gui/dist/assets/index-ltx3L-WS.css +0 -1
package/src/responses/state.ts
CHANGED
|
@@ -169,7 +169,10 @@ async function snapshotOnDiskMatches(path: string, payload: string, payloadBytes
|
|
|
169
169
|
return false;
|
|
170
170
|
}
|
|
171
171
|
}
|
|
172
|
-
const spillCounters = {
|
|
172
|
+
const spillCounters = {
|
|
173
|
+
writes: 0, writeFailures: 0, readFailures: 0,
|
|
174
|
+
aclRetryReturnedTimeouts: 0, aclTimeoutMemoRefusals: 0,
|
|
175
|
+
};
|
|
173
176
|
|
|
174
177
|
export type ResponseSpillWriteFailureCode =
|
|
175
178
|
| "EACLRETRYEXHAUSTED"
|
|
@@ -184,9 +187,14 @@ export type ResponseSpillWriteFailureCode =
|
|
|
184
187
|
|
|
185
188
|
export type ResponseSpillWriteStatus = "initial" | "healthy" | "degraded";
|
|
186
189
|
|
|
190
|
+
export type ResponseSpillWriteFailureOrigin =
|
|
191
|
+
| "retry_returned_timeout"
|
|
192
|
+
| "timeout_memo_refusal";
|
|
193
|
+
|
|
187
194
|
interface ResponseSpillWriteHealth {
|
|
188
195
|
consecutiveFailures: number;
|
|
189
196
|
lastFailureCode: ResponseSpillWriteFailureCode | null;
|
|
197
|
+
lastFailureOrigin: ResponseSpillWriteFailureOrigin | null;
|
|
190
198
|
lastFailureAt: number | null;
|
|
191
199
|
lastSuccessAt: number | null;
|
|
192
200
|
}
|
|
@@ -194,6 +202,7 @@ interface ResponseSpillWriteHealth {
|
|
|
194
202
|
const spillWriteHealth: ResponseSpillWriteHealth = {
|
|
195
203
|
consecutiveFailures: 0,
|
|
196
204
|
lastFailureCode: null,
|
|
205
|
+
lastFailureOrigin: null,
|
|
197
206
|
lastFailureAt: null,
|
|
198
207
|
lastSuccessAt: null,
|
|
199
208
|
};
|
|
@@ -226,6 +235,20 @@ function classifySpillWriteFailure(error: unknown): ResponseSpillWriteFailureCod
|
|
|
226
235
|
return "EUNKNOWN";
|
|
227
236
|
}
|
|
228
237
|
|
|
238
|
+
/** The spill writer preserves ACL errors in cause; only a fixed memo marker is diagnostic. */
|
|
239
|
+
function spillAclMemoRefusalOrigin(error: unknown): "timeout_memo_refusal" | null {
|
|
240
|
+
let cursor = error;
|
|
241
|
+
for (let depth = 0; depth < 4 && cursor && typeof cursor === "object"; depth += 1) {
|
|
242
|
+
const record = cursor as { code?: unknown; aclFailureOrigin?: unknown; cause?: unknown };
|
|
243
|
+
if ((record.code === "ETIMEDOUT" || record.code === "EACLRETRYEXHAUSTED")
|
|
244
|
+
&& record.aclFailureOrigin === "timeout_memo_refusal") {
|
|
245
|
+
return "timeout_memo_refusal";
|
|
246
|
+
}
|
|
247
|
+
cursor = record.cause;
|
|
248
|
+
}
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
|
|
229
252
|
function noteSpillWriteSuccess(): void {
|
|
230
253
|
spillCounters.writes += 1;
|
|
231
254
|
spillWriteHealth.consecutiveFailures = 0;
|
|
@@ -235,11 +258,20 @@ function noteSpillWriteSuccess(): void {
|
|
|
235
258
|
function noteSpillWriteFailure(
|
|
236
259
|
error: unknown,
|
|
237
260
|
override?: ResponseSpillWriteFailureCode,
|
|
261
|
+
retryOrigin: ResponseSpillWriteFailureOrigin | null = null,
|
|
238
262
|
): void {
|
|
263
|
+
const code = override ?? classifySpillWriteFailure(error);
|
|
264
|
+
const origin = code === "ETIMEDOUT" || code === "EACLRETRYEXHAUSTED"
|
|
265
|
+
? spillAclMemoRefusalOrigin(error) ?? retryOrigin
|
|
266
|
+
: null;
|
|
239
267
|
spillCounters.writeFailures += 1;
|
|
240
268
|
spillWriteHealth.consecutiveFailures += 1;
|
|
241
|
-
spillWriteHealth.lastFailureCode =
|
|
269
|
+
spillWriteHealth.lastFailureCode = code;
|
|
270
|
+
spillWriteHealth.lastFailureOrigin = origin;
|
|
242
271
|
spillWriteHealth.lastFailureAt = now();
|
|
272
|
+
// Count terminal publications, not ACL calls or a transient first attempt.
|
|
273
|
+
if (origin === "retry_returned_timeout") spillCounters.aclRetryReturnedTimeouts += 1;
|
|
274
|
+
else if (origin === "timeout_memo_refusal") spillCounters.aclTimeoutMemoRefusals += 1;
|
|
243
275
|
}
|
|
244
276
|
/**
|
|
245
277
|
* Admission-boundary observability (test-visible). directSpills: oversized
|
|
@@ -418,6 +450,7 @@ async function runPendingResponseSpill(job: PendingResponseSpill): Promise<void>
|
|
|
418
450
|
const candidate = job.candidate;
|
|
419
451
|
let ref: ResponseSpillRef | null = null;
|
|
420
452
|
let exhaustedAclRetry = false;
|
|
453
|
+
let aclRetryFailureOrigin: ResponseSpillWriteFailureOrigin | null = null;
|
|
421
454
|
try {
|
|
422
455
|
const state = spillPayloadForResident(candidate);
|
|
423
456
|
try {
|
|
@@ -437,6 +470,9 @@ async function runPendingResponseSpill(job: PendingResponseSpill): Promise<void>
|
|
|
437
470
|
});
|
|
438
471
|
} catch (retryError) {
|
|
439
472
|
exhaustedAclRetry = isAclTimeout(retryError);
|
|
473
|
+
// A returned timeout can also mean an exhausted budget before the next OS command.
|
|
474
|
+
aclRetryFailureOrigin = spillAclMemoRefusalOrigin(retryError)
|
|
475
|
+
?? (exhaustedAclRetry ? "retry_returned_timeout" : null);
|
|
440
476
|
throw retryError;
|
|
441
477
|
}
|
|
442
478
|
}
|
|
@@ -460,7 +496,7 @@ async function runPendingResponseSpill(job: PendingResponseSpill): Promise<void>
|
|
|
460
496
|
} catch (error) {
|
|
461
497
|
if (ref) deleteResponseSpill(ref);
|
|
462
498
|
if (states.get(job.id) === candidate && !job.cancelled) {
|
|
463
|
-
noteSpillWriteFailure(error, exhaustedAclRetry ? "EACLRETRYEXHAUSTED" : undefined);
|
|
499
|
+
noteSpillWriteFailure(error, exhaustedAclRetry ? "EACLRETRYEXHAUSTED" : undefined, aclRetryFailureOrigin);
|
|
464
500
|
replaceWithSpillFailure(job.id, candidate);
|
|
465
501
|
deferSupersededSpill(job.supersededSpill);
|
|
466
502
|
}
|
|
@@ -2188,6 +2224,9 @@ export interface ResponseStateMetrics {
|
|
|
2188
2224
|
spillWriteStatus: ResponseSpillWriteStatus;
|
|
2189
2225
|
spillWriteConsecutiveFailures: number;
|
|
2190
2226
|
spillLastWriteFailureCode: ResponseSpillWriteFailureCode | null;
|
|
2227
|
+
spillLastWriteFailureOrigin: ResponseSpillWriteFailureOrigin | null;
|
|
2228
|
+
spillAclRetryReturnedTimeouts: number;
|
|
2229
|
+
spillAclTimeoutMemoRefusals: number;
|
|
2191
2230
|
spillLastWriteFailureAt: number | null;
|
|
2192
2231
|
spillLastWriteSuccessAt: number | null;
|
|
2193
2232
|
spillReadFailures: number;
|
|
@@ -2240,6 +2279,9 @@ export function responseStateMetrics(): ResponseStateMetrics {
|
|
|
2240
2279
|
: "initial",
|
|
2241
2280
|
spillWriteConsecutiveFailures: spillWriteHealth.consecutiveFailures,
|
|
2242
2281
|
spillLastWriteFailureCode: spillWriteHealth.lastFailureCode,
|
|
2282
|
+
spillLastWriteFailureOrigin: spillWriteHealth.lastFailureOrigin,
|
|
2283
|
+
spillAclRetryReturnedTimeouts: spillCounters.aclRetryReturnedTimeouts,
|
|
2284
|
+
spillAclTimeoutMemoRefusals: spillCounters.aclTimeoutMemoRefusals,
|
|
2243
2285
|
spillLastWriteFailureAt: spillWriteHealth.lastFailureAt,
|
|
2244
2286
|
spillLastWriteSuccessAt: spillWriteHealth.lastSuccessAt,
|
|
2245
2287
|
spillReadFailures: spillCounters.readFailures,
|
|
@@ -2360,8 +2402,11 @@ export function clearResponseStateMemoryForTests(): void {
|
|
|
2360
2402
|
spillCounters.writes = 0;
|
|
2361
2403
|
spillCounters.writeFailures = 0;
|
|
2362
2404
|
spillCounters.readFailures = 0;
|
|
2405
|
+
spillCounters.aclRetryReturnedTimeouts = 0;
|
|
2406
|
+
spillCounters.aclTimeoutMemoRefusals = 0;
|
|
2363
2407
|
spillWriteHealth.consecutiveFailures = 0;
|
|
2364
2408
|
spillWriteHealth.lastFailureCode = null;
|
|
2409
|
+
spillWriteHealth.lastFailureOrigin = null;
|
|
2365
2410
|
spillWriteHealth.lastFailureAt = null;
|
|
2366
2411
|
spillWriteHealth.lastSuccessAt = null;
|
|
2367
2412
|
replayScopeMismatchDrops = 0;
|
|
@@ -46,6 +46,7 @@ import {
|
|
|
46
46
|
type TranslatorBudget,
|
|
47
47
|
} from "../lib/translator-budget";
|
|
48
48
|
import { handleNativeChatCompletions, isNativeChatRouteEligible } from "./chat-native";
|
|
49
|
+
import { jsonCompletionSse } from "./chat-native-sse";
|
|
49
50
|
import { parseRequestEffortRowId } from "./effort-row";
|
|
50
51
|
import { parseSyntheticRowId } from "./fast-row";
|
|
51
52
|
import { isCanonicalOpenAiForwardProvider } from "../providers/openai-tiers";
|
|
@@ -79,6 +80,14 @@ export async function handleChatCompletions(
|
|
|
79
80
|
);
|
|
80
81
|
} catch (error) {
|
|
81
82
|
translatorBudget.dispose();
|
|
83
|
+
if (isTranslatorBudgetExceededError(error)) {
|
|
84
|
+
if (logIds) addFinalRequestLog(logIds.requestId, logIds.start, logCtx, 502, { closeReason: "non_stream" });
|
|
85
|
+
return chatCompletionsErrorResponse(502, "upstream translation buffer exceeded the safe limit", "upstream_error", "translation_buffer_limit");
|
|
86
|
+
}
|
|
87
|
+
if (isChatCompletionsStreamError(error)) {
|
|
88
|
+
if (logIds) addFinalRequestLog(logIds.requestId, logIds.start, logCtx, error.status, { closeReason: "non_stream" });
|
|
89
|
+
return chatCompletionsErrorResponse(error.status, error.message, error.type, error.code);
|
|
90
|
+
}
|
|
82
91
|
throw error;
|
|
83
92
|
}
|
|
84
93
|
}
|
|
@@ -277,7 +286,7 @@ async function handleChatCompletionsWithBudget(
|
|
|
277
286
|
});
|
|
278
287
|
|
|
279
288
|
let nativeLogged = false;
|
|
280
|
-
const finalizeNativeLog = (status: number, meta: { terminalStatus?: RequestLogEntry["terminalStatus"]; closeReason: "terminal" | "client_cancel" }) => {
|
|
289
|
+
const finalizeNativeLog = (status: number, meta: { terminalStatus?: RequestLogEntry["terminalStatus"]; closeReason: "terminal" | "client_cancel" | "non_stream" }) => {
|
|
281
290
|
if (!logIds || nativeLogged) return;
|
|
282
291
|
nativeLogged = true;
|
|
283
292
|
addFinalRequestLog(logIds.requestId, logIds.start, logCtx, status, meta);
|
|
@@ -378,11 +387,14 @@ async function handleChatCompletionsWithBudget(
|
|
|
378
387
|
: rewritten;
|
|
379
388
|
}
|
|
380
389
|
|
|
381
|
-
const
|
|
390
|
+
const contentType = upstream.headers.get("content-type") ?? "";
|
|
391
|
+
// JSON is not complete for the client until its Chat projection succeeds.
|
|
392
|
+
// Logging the upstream JSON body here would persist 200 before a later
|
|
393
|
+
// conversion/serialization error, double-counting both the request and usage.
|
|
394
|
+
const response = logIds && contentType.includes("text/event-stream")
|
|
382
395
|
? responseWithDeferredRequestLog(upstream, logIds.requestId, logIds.start, logCtx)
|
|
383
396
|
: upstream;
|
|
384
397
|
|
|
385
|
-
const contentType = response.headers.get("content-type") ?? "";
|
|
386
398
|
if (contentType.includes("text/event-stream") && response.body) {
|
|
387
399
|
const chatSse = responsesSseToChatCompletionsSse(response.body, requestedModel, { translatorBudget });
|
|
388
400
|
if (stream) {
|
|
@@ -416,11 +428,15 @@ async function handleChatCompletionsWithBudget(
|
|
|
416
428
|
}
|
|
417
429
|
|
|
418
430
|
// Defensive: JSON despite stream:true.
|
|
431
|
+
const finishJson = (result: Response): Response => {
|
|
432
|
+
finalizeNativeLog(result.status, { closeReason: "non_stream" });
|
|
433
|
+
return result;
|
|
434
|
+
};
|
|
419
435
|
let json: unknown;
|
|
420
436
|
try {
|
|
421
437
|
json = await response.json();
|
|
422
438
|
} catch {
|
|
423
|
-
return chatCompletionsErrorResponse(502, "internal replay returned a non-JSON response", "server_error");
|
|
439
|
+
return finishJson(chatCompletionsErrorResponse(502, "internal replay returned a non-JSON response", "server_error"));
|
|
424
440
|
}
|
|
425
441
|
const status = (json as Rec)?.status;
|
|
426
442
|
if (status === "failed") {
|
|
@@ -438,44 +454,24 @@ async function handleChatCompletionsWithBudget(
|
|
|
438
454
|
classified.code = "model_not_found";
|
|
439
455
|
classified.type = "invalid_request_error";
|
|
440
456
|
}
|
|
441
|
-
return chatCompletionsErrorResponse(
|
|
457
|
+
return finishJson(chatCompletionsErrorResponse(
|
|
442
458
|
classified.code === "translation_buffer_limit"
|
|
443
459
|
? 502
|
|
444
460
|
: isCyberPolicyCode(classified.code) ? 400 : 502,
|
|
445
461
|
message,
|
|
446
462
|
classified.type,
|
|
447
463
|
classified.code,
|
|
448
|
-
);
|
|
464
|
+
));
|
|
449
465
|
}
|
|
450
|
-
const completion = responsesJsonToChatCompletion(json, requestedModel);
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
// Streaming client + JSON upstream: synthesize a minimal Chat Completions stream.
|
|
459
|
-
const encoder = new TextEncoder();
|
|
460
|
-
const id = typeof completion.id === "string" ? completion.id : `chatcmpl-${Date.now()}`;
|
|
461
|
-
const created = typeof completion.created === "number" ? completion.created : Math.floor(Date.now() / 1000);
|
|
462
|
-
const message = isRec((completion.choices as Rec[] | undefined)?.[0])
|
|
463
|
-
? ((completion.choices as Rec[])[0] as Rec).message as Rec | undefined
|
|
464
|
-
: undefined;
|
|
465
|
-
const content = message && typeof message.content === "string" ? message.content : "";
|
|
466
|
-
const frames = [
|
|
467
|
-
`data: ${JSON.stringify({ id, object: "chat.completion.chunk", created, model: requestedModel, choices: [{ index: 0, delta: { role: "assistant", content: "" }, finish_reason: null }] })}\n\n`,
|
|
468
|
-
...(content
|
|
469
|
-
? [`data: ${JSON.stringify({ id, object: "chat.completion.chunk", created, model: requestedModel, choices: [{ index: 0, delta: { content }, finish_reason: null }] })}\n\n`]
|
|
470
|
-
: []),
|
|
471
|
-
`data: ${JSON.stringify({ id, object: "chat.completion.chunk", created, model: requestedModel, choices: [{ index: 0, delta: {}, finish_reason: "stop" }], usage: completion.usage })}\n\n`,
|
|
472
|
-
"data: [DONE]\n\n",
|
|
473
|
-
];
|
|
474
|
-
return new Response(encoder.encode(frames.join("")), {
|
|
466
|
+
const completion = responsesJsonToChatCompletion(json, requestedModel, translatorBudget);
|
|
467
|
+
const body = stream
|
|
468
|
+
? jsonCompletionSse(completion, requestedModel, translatorBudget)
|
|
469
|
+
: JSON.stringify(completion);
|
|
470
|
+
if (!stream) translatorBudget.chargeRetained(Buffer.byteLength(body) * 2, { kind: "live_transient" });
|
|
471
|
+
return finishJson(new Response(body, {
|
|
475
472
|
status: 200,
|
|
476
|
-
headers:
|
|
477
|
-
"Content-Type": "text/event-stream; charset=utf-8",
|
|
478
|
-
"
|
|
479
|
-
|
|
480
|
-
});
|
|
473
|
+
headers: stream
|
|
474
|
+
? { "Content-Type": "text/event-stream; charset=utf-8", "Cache-Control": "no-cache", Connection: "keep-alive" }
|
|
475
|
+
: { "Content-Type": "application/json" },
|
|
476
|
+
}));
|
|
481
477
|
}
|
|
@@ -61,7 +61,7 @@ function normalizedChunk(value: Rec, requestedModel: string): Rec {
|
|
|
61
61
|
};
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
export function jsonCompletionSse(value: Rec, requestedModel: string): string {
|
|
64
|
+
export function jsonCompletionSse(value: Rec, requestedModel: string, budget?: TranslatorBudget): string {
|
|
65
65
|
const id = typeof value.id === "string" ? value.id : `chatcmpl-${crypto.randomUUID().replace(/-/g, "").slice(0, 24)}`;
|
|
66
66
|
const created = typeof value.created === "number" ? value.created : Math.floor(Date.now() / 1000);
|
|
67
67
|
const model = requestedModel;
|
|
@@ -77,11 +77,12 @@ export function jsonCompletionSse(value: Rec, requestedModel: string): string {
|
|
|
77
77
|
}];
|
|
78
78
|
const delta: Rec = {};
|
|
79
79
|
if (typeof message.content === "string" && message.content.length > 0) delta.content = message.content;
|
|
80
|
+
if (typeof message.refusal === "string") delta.refusal = message.refusal;
|
|
80
81
|
if (typeof message.reasoning_content === "string" && message.reasoning_content.length > 0) {
|
|
81
82
|
delta.reasoning_content = message.reasoning_content;
|
|
82
83
|
}
|
|
83
84
|
if (Array.isArray(message.tool_calls) && message.tool_calls.length > 0) {
|
|
84
|
-
delta.tool_calls = message.tool_calls.map((tool, index) =>
|
|
85
|
+
delta.tool_calls = message.tool_calls.filter(isRec).map((tool, index) => ({ ...tool, index }));
|
|
85
86
|
}
|
|
86
87
|
if (Object.keys(delta).length > 0) {
|
|
87
88
|
frames.push({ id, object: "chat.completion.chunk", created, model, choices: [{ index: 0, delta, finish_reason: null }] });
|
|
@@ -94,7 +95,26 @@ export function jsonCompletionSse(value: Rec, requestedModel: string): string {
|
|
|
94
95
|
choices: [{ index: 0, delta: {}, finish_reason: typeof choice.finish_reason === "string" ? choice.finish_reason : "stop" }],
|
|
95
96
|
...(value.usage !== undefined ? { usage: value.usage } : {}),
|
|
96
97
|
});
|
|
97
|
-
|
|
98
|
+
// Keep the frame strings charged while the joined body is allocated. The final
|
|
99
|
+
// string and Response's UTF-8 body coexist until response ownership ends.
|
|
100
|
+
const scope = { kind: "live_transient" as const };
|
|
101
|
+
let frameBytes = 0;
|
|
102
|
+
const serialized: string[] = [];
|
|
103
|
+
try {
|
|
104
|
+
for (const frame of frames) {
|
|
105
|
+
const text = `data: ${JSON.stringify(frame)}\n\n`;
|
|
106
|
+
const bytes = Buffer.byteLength(text);
|
|
107
|
+
budget?.chargeRetained(bytes, scope);
|
|
108
|
+
frameBytes += bytes;
|
|
109
|
+
serialized.push(text);
|
|
110
|
+
}
|
|
111
|
+
const done = "data: [DONE]\n\n";
|
|
112
|
+
const outputBytes = frameBytes + Buffer.byteLength(done);
|
|
113
|
+
budget?.chargeRetained(outputBytes * 2, scope);
|
|
114
|
+
return serialized.join("") + done;
|
|
115
|
+
} finally {
|
|
116
|
+
budget?.releaseRetained(frameBytes, scope);
|
|
117
|
+
}
|
|
98
118
|
}
|
|
99
119
|
|
|
100
120
|
interface NativeChatSseOptions {
|
|
@@ -502,15 +502,22 @@ export async function handleNativeChatCompletions(options: HandleNativeChatOptio
|
|
|
502
502
|
attempt.usage = usage;
|
|
503
503
|
}
|
|
504
504
|
if (logIds) recordFirstOutput(logCtx, logIds.start);
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
505
|
+
try {
|
|
506
|
+
const serialized = requestedStream
|
|
507
|
+
? jsonCompletionSse(completion, requestedModel, translatorBudget)
|
|
508
|
+
: JSON.stringify(completion);
|
|
509
|
+
if (!requestedStream) translatorBudget.chargeRetained(Buffer.byteLength(serialized) * 2, { kind: "live_transient" });
|
|
510
|
+
finishLog(200);
|
|
511
|
+
return new Response(serialized, {
|
|
508
512
|
status: 200,
|
|
509
|
-
headers:
|
|
513
|
+
headers: requestedStream
|
|
514
|
+
? { "Content-Type": "text/event-stream; charset=utf-8", "Cache-Control": "no-cache" }
|
|
515
|
+
: { "Content-Type": "application/json" },
|
|
510
516
|
});
|
|
517
|
+
} catch (error) {
|
|
518
|
+
if (isTranslatorBudgetExceededError(error)) {
|
|
519
|
+
return fail(502, "upstream translation buffer exceeded the safe limit", "upstream_error", "translation_buffer_limit");
|
|
520
|
+
}
|
|
521
|
+
throw error;
|
|
511
522
|
}
|
|
512
|
-
return new Response(JSON.stringify(completion), {
|
|
513
|
-
status: 200,
|
|
514
|
-
headers: { "Content-Type": "application/json" },
|
|
515
|
-
});
|
|
516
523
|
}
|
|
@@ -16,6 +16,7 @@ import { resolveAlias, claudeCodeNativeAlias } from "../claude/alias";
|
|
|
16
16
|
import { recordDesktopRequest } from "../claude/desktop-health";
|
|
17
17
|
import { stripOneMillionMarker } from "../claude/context-windows";
|
|
18
18
|
import { captureClaudeInbound } from "../claude/inbound-debug";
|
|
19
|
+
import { analyzeClaudeCompatibility, isClaudeCompatibilityMode } from "../claude/compatibility";
|
|
19
20
|
import { isTransientUpstreamStatus } from "../lib/upstream-retry";
|
|
20
21
|
import { resolveClientRetryAfter } from "../lib/retry-after";
|
|
21
22
|
import {
|
|
@@ -719,6 +720,32 @@ async function handleClaudeMessagesWithBudget(
|
|
|
719
720
|
if (!effortRow && !fastRow && isRec(anthropicBody) && wantsNativePassthrough(req, config, requestPolicy, anthropicBody.model)) {
|
|
720
721
|
return await anthropicNativePassthrough(req, config, logCtx, logIds, anthropicBody, "/v1/messages");
|
|
721
722
|
}
|
|
723
|
+
// Capture source semantics before effort rewriting or translation drops fields.
|
|
724
|
+
// This policy is uniform across translated targets, including later fallback attempts.
|
|
725
|
+
const compatibilityMode: unknown = config.claudeCode?.compatibility;
|
|
726
|
+
if (compatibilityMode !== undefined) {
|
|
727
|
+
if (!isClaudeCompatibilityMode(compatibilityMode)) {
|
|
728
|
+
logCtx.errorCode = "claude_compatibility_configuration";
|
|
729
|
+
if (logIds) addFinalRequestLog(logIds.requestId, logIds.start, logCtx, 503, { closeReason: "non_stream" });
|
|
730
|
+
return anthropicErrorResponse(503, "Invalid claudeCode.compatibility setting", "api_error");
|
|
731
|
+
}
|
|
732
|
+
const compatibility = analyzeClaudeCompatibility(anthropicBody, {
|
|
733
|
+
mode: compatibilityMode,
|
|
734
|
+
anthropicBeta: req.headers.get("anthropic-beta") ?? undefined,
|
|
735
|
+
});
|
|
736
|
+
if (compatibility.decision === "reject") {
|
|
737
|
+
logCtx.errorCode = "claude_compatibility_unsupported";
|
|
738
|
+
if (logIds) addFinalRequestLog(logIds.requestId, logIds.start, logCtx, 400, { closeReason: "non_stream" });
|
|
739
|
+
return anthropicErrorResponse(400, compatibility.reason!, "invalid_request_error");
|
|
740
|
+
}
|
|
741
|
+
if (compatibility.decision === "shadow") {
|
|
742
|
+
logCtx.claudeCompatibility = {
|
|
743
|
+
decision: "shadow",
|
|
744
|
+
featureCodes: compatibility.featureCodes,
|
|
745
|
+
reason: compatibility.reason,
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
}
|
|
722
749
|
if (isRec(anthropicBody) && effortOverride) {
|
|
723
750
|
anthropicBody.output_config = {
|
|
724
751
|
...(isRec(anthropicBody.output_config) ? anthropicBody.output_config : {}),
|
package/src/server/index.ts
CHANGED
|
@@ -1528,6 +1528,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
1528
1528
|
? nativeFastEligible(model.id)
|
|
1529
1529
|
: catalogRowFastEligible(model)
|
|
1530
1530
|
: undefined,
|
|
1531
|
+
{ modelPickerOrder: config.modelPickerOrder, featured: config.subagentModels },
|
|
1531
1532
|
);
|
|
1532
1533
|
return jsonResponse({ data }, 200, req, policy);
|
|
1533
1534
|
}
|
|
@@ -1562,6 +1563,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
1562
1563
|
accountNativeSlugs,
|
|
1563
1564
|
accountNativeSlugsBySelector,
|
|
1564
1565
|
config.keepNativeChatGptOnV1 === true,
|
|
1566
|
+
config.modelPickerOrder,
|
|
1565
1567
|
);
|
|
1566
1568
|
return jsonResponse({
|
|
1567
1569
|
models: applyNativeVisibility(
|
|
@@ -1763,7 +1765,9 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
1763
1765
|
return runAdmittedHttpTurn(req, policy, async turnAdmissionLease => {
|
|
1764
1766
|
let response: Response;
|
|
1765
1767
|
try {
|
|
1766
|
-
response = await handleResponsesCompact(req, config, logCtx, turnAdmissionLease, admission
|
|
1768
|
+
response = await handleResponsesCompact(req, config, logCtx, turnAdmissionLease, admission, {
|
|
1769
|
+
onRequestBodyRead: () => disableResponsesRequestTimeout(req, requestServer),
|
|
1770
|
+
});
|
|
1767
1771
|
} catch {
|
|
1768
1772
|
response = formatErrorResponse(500, "server_error", "Unexpected compact request failure");
|
|
1769
1773
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import type { CatalogModel } from "../../codex/catalog";
|
|
4
|
-
import { catalogModelSlug, invalidateCodexModelsCache, nativeContextLimits, nativeModelRows, uniqueCatalogModelsForPublicList } from "../../codex/catalog";
|
|
4
|
+
import { catalogModelSlug, filterCatalogVisibleModels, invalidateCodexModelsCache, nativeContextLimits, nativeModelRows, uniqueCatalogModelsForPublicList } from "../../codex/catalog";
|
|
5
|
+
import { captureConfigTopLevelRollback, parsedConfigRebaseDeletionKeys, projectConfigRebaseProvenance } from "../../config/rebase-provenance";
|
|
5
6
|
import {
|
|
6
7
|
DEFAULT_SUBAGENT_MODELS,
|
|
7
8
|
codexAutoStartEnabled,
|
|
@@ -622,10 +623,10 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
622
623
|
return jsonResponse({ ok: true, effortCap: config.effortCap ?? null, subagentEffortCap: config.subagentEffortCap ?? null });
|
|
623
624
|
}
|
|
624
625
|
|
|
625
|
-
//
|
|
626
|
-
// first
|
|
626
|
+
// Featured roster and saved picker order are separate settings. Native Codex advertises
|
|
627
|
+
// the first five eligible visible rows by display priority; OCX guidance uses natural ranks.
|
|
627
628
|
if (url.pathname === "/api/subagent-models" && req.method === "GET") {
|
|
628
|
-
const models = await fetchAllModels(config);
|
|
629
|
+
const models = await (deps.fetchAllModels ?? fetchAllModels)(config);
|
|
629
630
|
const disabled = new Set(config.disabledModels ?? []);
|
|
630
631
|
// Native gpt (passthrough) are also valid subagent picks — they're picker-visible models in the
|
|
631
632
|
// catalog, just buried by priority. List them first so the user can feature them over routed.
|
|
@@ -657,19 +658,105 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
657
658
|
// in-memory catalog than the one on disk.
|
|
658
659
|
const { collectCodexAppServerCatalogState } = await import("../../codex/app-server-processes");
|
|
659
660
|
const catalogState = collectCodexAppServerCatalogState();
|
|
660
|
-
return jsonResponse({
|
|
661
|
+
return jsonResponse({
|
|
662
|
+
chosen, available, catalogState,
|
|
663
|
+
pickerAvailable: [...new Set(filterCatalogVisibleModels(models, config).map(catalogModelSlug).filter(slug => slug.includes("/")))],
|
|
664
|
+
pickerOrder: config.modelPickerOrder ?? [],
|
|
665
|
+
pickerOrderMode: config.modelPickerOrderMode ?? null,
|
|
666
|
+
});
|
|
661
667
|
}
|
|
662
668
|
if (url.pathname === "/api/subagent-models" && req.method === "PUT") {
|
|
663
|
-
let
|
|
664
|
-
try {
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
const
|
|
668
|
-
|
|
669
|
+
let rawBody: unknown;
|
|
670
|
+
try { rawBody = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
|
|
671
|
+
if (!isPlainRecord(rawBody)) return jsonResponse({ error: "JSON body must be an object" }, 400);
|
|
672
|
+
const body = rawBody as { models?: unknown; pickerOrder?: unknown; pickerOrderMode?: unknown };
|
|
673
|
+
const updatesRoster = body.models !== undefined;
|
|
674
|
+
const updatesPicker = body.pickerOrder !== undefined;
|
|
675
|
+
if (!updatesRoster && !updatesPicker) return jsonResponse({ error: "models or pickerOrder is required" }, 400);
|
|
676
|
+
let chosen: string[] | undefined;
|
|
677
|
+
if (updatesRoster) {
|
|
678
|
+
if (!Array.isArray(body.models) || body.models.some(model => typeof model !== "string")) {
|
|
679
|
+
return jsonResponse({ error: "models must be an array of strings" }, 400);
|
|
680
|
+
}
|
|
681
|
+
// Keep the original valid roster contract: no discovery validation, trimming or deduping.
|
|
682
|
+
chosen = body.models.slice(0, 5);
|
|
683
|
+
}
|
|
684
|
+
const mode = body.pickerOrderMode;
|
|
685
|
+
if (mode !== undefined && (!updatesPicker || (mode !== null
|
|
686
|
+
&& mode !== "alphabetical" && mode !== "provider" && mode !== "most-used"))) {
|
|
687
|
+
return jsonResponse({ error: "pickerOrderMode requires pickerOrder and must be alphabetical, provider, most-used, or null" }, 400);
|
|
688
|
+
}
|
|
689
|
+
let pickerOrder: string[] | undefined;
|
|
690
|
+
if (updatesPicker) {
|
|
691
|
+
if (body.pickerOrder !== null && (!Array.isArray(body.pickerOrder)
|
|
692
|
+
|| body.pickerOrder.some(model => typeof model !== "string" || model.trim() === ""))) {
|
|
693
|
+
return jsonResponse({ error: "pickerOrder must be an array of non-empty routed model ids, or null" }, 400);
|
|
694
|
+
}
|
|
695
|
+
pickerOrder = body.pickerOrder === null ? [] : (body.pickerOrder as string[]).map(model => model.trim());
|
|
696
|
+
if (new Set(pickerOrder).size !== pickerOrder.length) {
|
|
697
|
+
return jsonResponse({ error: "pickerOrder must not contain duplicate ids" }, 400);
|
|
698
|
+
}
|
|
699
|
+
if (pickerOrder.length > 0) {
|
|
700
|
+
const models = await (deps.fetchAllModels ?? fetchAllModels)(config);
|
|
701
|
+
// Evaluate visibility AFTER discovery: a concurrent visibility write may have completed.
|
|
702
|
+
const visible = new Set(filterCatalogVisibleModels(models, config).map(catalogModelSlug).filter(slug => slug.includes("/")));
|
|
703
|
+
if (pickerOrder.some(model => !visible.has(model))) {
|
|
704
|
+
return jsonResponse({ error: "pickerOrder must contain each visible routed model at most once" }, 400);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// Everything above can await. From this snapshot through persistence there is no yield.
|
|
710
|
+
// Stage deletion intent before adopting the touched fields through the canonical
|
|
711
|
+
// live deletion owner. A failed save restores both fields and pending intent.
|
|
712
|
+
if (updatesPicker && config.configRebaseProvenance !== undefined
|
|
713
|
+
&& parsedConfigRebaseDeletionKeys(config) === null) {
|
|
714
|
+
// A newer provenance format must not silently discard this clear's intent on rebase.
|
|
715
|
+
return jsonResponse({ error: "unsupported config deletion provenance" }, 409);
|
|
716
|
+
}
|
|
717
|
+
const draft = { ...projectConfigRebaseProvenance(config) };
|
|
718
|
+
if (chosen !== undefined) draft.subagentModels = chosen;
|
|
719
|
+
if (pickerOrder !== undefined) {
|
|
720
|
+
if (pickerOrder.length === 0) {
|
|
721
|
+
deleteConfigTopLevelKey(draft, "modelPickerOrder");
|
|
722
|
+
deleteConfigTopLevelKey(draft, "modelPickerOrderMode");
|
|
723
|
+
} else {
|
|
724
|
+
draft.modelPickerOrder = pickerOrder;
|
|
725
|
+
if (mode === "alphabetical" || mode === "provider" || mode === "most-used") draft.modelPickerOrderMode = mode;
|
|
726
|
+
else deleteConfigTopLevelKey(draft, "modelPickerOrderMode");
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
const projected = projectConfigRebaseProvenance(draft);
|
|
730
|
+
const touched = [
|
|
731
|
+
...(updatesRoster ? ["subagentModels" as const] : []),
|
|
732
|
+
...(updatesPicker ? ["modelPickerOrder" as const, "modelPickerOrderMode" as const] : []),
|
|
733
|
+
"configRebaseProvenance" as const,
|
|
734
|
+
];
|
|
735
|
+
const rollback = captureConfigTopLevelRollback(config, touched);
|
|
736
|
+
try {
|
|
737
|
+
for (const key of touched) {
|
|
738
|
+
if (Object.hasOwn(projected, key)) Object.defineProperty(config, key, {
|
|
739
|
+
value: projected[key], writable: true, enumerable: true, configurable: true,
|
|
740
|
+
});
|
|
741
|
+
else deleteConfigTopLevelKey(config, key);
|
|
742
|
+
}
|
|
743
|
+
(deps.saveConfigPreservingClaudeCode ?? saveConfigPreservingClaudeCode)(config);
|
|
744
|
+
} catch (error) {
|
|
745
|
+
rollback();
|
|
746
|
+
throw error;
|
|
747
|
+
}
|
|
748
|
+
// Capture the result before convergence yields to another settings mutation.
|
|
749
|
+
const saved = {
|
|
750
|
+
applied: [...(config.subagentModels ?? [])],
|
|
751
|
+
pickerOrder: [...(config.modelPickerOrder ?? [])],
|
|
752
|
+
pickerOrderMode: config.modelPickerOrderMode ?? null,
|
|
753
|
+
};
|
|
669
754
|
const catalogRefresh = await convergeCodexCatalog();
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
755
|
+
if (updatesRoster) {
|
|
756
|
+
await syncClaudeAgentDefsBestEffort();
|
|
757
|
+
await autoApplyDesktopBestEffort();
|
|
758
|
+
}
|
|
759
|
+
return jsonResponse({ ok: true, ...saved, catalogRefresh });
|
|
673
760
|
}
|
|
674
761
|
|
|
675
762
|
// Priority-ordered subagent model fallback chain for quota-aware spawn routing.
|
|
@@ -66,6 +66,7 @@ import {
|
|
|
66
66
|
import type { OcxClaudeCodeConfig, OcxConfig, OcxCustomModel, OcxProviderConfig } from "../../types";
|
|
67
67
|
import { drainAndShutdown } from "../lifecycle";
|
|
68
68
|
import { filterRequestLogs, filteredRequestLogCount, getRequestLogEntries, type RequestLogEntry } from "../request-log";
|
|
69
|
+
import { decodeRequestLogCursor, selectRequestLogPoll } from "../request-log-cursor";
|
|
69
70
|
import { estimateComboCost, estimateRequestCost, normalizeCostTokens, tokensPerSecond } from "../../usage/cost";
|
|
70
71
|
import { userCostOverlayVersion } from "../../usage/user-cost-overlays";
|
|
71
72
|
import type { PersistedUsageAttempt } from "../../usage/log";
|
|
@@ -106,14 +107,20 @@ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Res
|
|
|
106
107
|
const { req, url, config, deps, syncClaudeAgentDefsBestEffort } = ctx;
|
|
107
108
|
|
|
108
109
|
if (url.pathname === "/api/logs" && req.method === "GET") {
|
|
110
|
+
const rawCursor = url.searchParams.get("cursor");
|
|
111
|
+
const cursor = rawCursor === null ? null : decodeRequestLogCursor(rawCursor);
|
|
112
|
+
if (rawCursor !== null && cursor === null) {
|
|
113
|
+
return jsonResponse({ error: { code: "invalid_cursor", message: "invalid cursor" } }, 400);
|
|
114
|
+
}
|
|
109
115
|
const all = getRequestLogEntries();
|
|
110
116
|
const total = filteredRequestLogCount(all, url.searchParams);
|
|
111
|
-
const logs = filterRequestLogs(all, url.searchParams);
|
|
117
|
+
const logs = filterRequestLogs(all, url.searchParams).map(requestLogDto);
|
|
118
|
+
const poll = selectRequestLogPoll(logs, url.searchParams, cursor);
|
|
112
119
|
return jsonResponse({
|
|
113
120
|
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
114
121
|
generatedAt: Date.now(),
|
|
115
122
|
total,
|
|
116
|
-
|
|
123
|
+
...poll,
|
|
117
124
|
});
|
|
118
125
|
}
|
|
119
126
|
|