@bitkyc08/opencodex 2.7.23 → 2.7.24
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 +37 -7
- package/README.md +52 -11
- package/README.zh-CN.md +36 -7
- package/bin/ocx.mjs +5 -3
- package/gui/dist/assets/index-BzhyTAco.js +40 -0
- package/gui/dist/assets/index-Dq3eZ1cU.css +1 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/opencode.svg +1 -1
- package/package.json +5 -2
- package/src/adapters/anthropic-image-normalize.ts +70 -29
- package/src/adapters/cursor/transport-retry.ts +20 -1
- package/src/adapters/run-turn-queue.ts +40 -0
- package/src/codex/auth-api.ts +10 -1
- package/src/codex/auth-context.ts +33 -7
- package/src/codex/catalog.ts +357 -23
- package/src/codex/routing.ts +10 -4
- package/src/combos/failover.ts +102 -0
- package/src/combos/index.ts +37 -0
- package/src/combos/request.ts +31 -0
- package/src/combos/resolve.ts +171 -0
- package/src/combos/types.ts +203 -0
- package/src/config.ts +280 -11
- package/src/lib/errors.ts +86 -24
- package/src/lib/upstream-retry.ts +8 -4
- package/src/oauth/index.ts +7 -1
- package/src/oauth/key-providers.ts +2 -32
- package/src/oauth/login-cli.ts +4 -3
- package/src/oauth/token-guardian.ts +38 -3
- package/src/providers/derive.ts +27 -2
- package/src/providers/kiro-models.ts +8 -3
- package/src/providers/label.ts +3 -1
- package/src/providers/openai-sidecar.ts +94 -0
- package/src/providers/openai-tier-startup.ts +27 -0
- package/src/providers/openai-tiers.ts +283 -0
- package/src/providers/openai-virtual-models.ts +82 -0
- package/src/providers/quota.ts +344 -24
- package/src/providers/registry.ts +112 -20
- package/src/reasoning-effort.ts +12 -11
- package/src/router.ts +80 -36
- package/src/server/auth-cors.ts +85 -9
- package/src/server/images.ts +31 -75
- package/src/server/index.ts +45 -86
- package/src/server/management-api.ts +273 -21
- package/src/server/request-log.ts +221 -20
- package/src/server/responses.ts +594 -75
- package/src/server/search.ts +22 -37
- package/src/types.ts +49 -1
- package/src/update/index.ts +50 -6
- package/src/update/job.ts +21 -4
- package/src/usage/log.ts +124 -1
- package/src/usage/summary.ts +147 -56
- package/src/vision/index.ts +20 -19
- package/src/web-search/index.ts +15 -17
- package/gui/dist/assets/index-Bk_GgFrh.css +0 -1
- package/gui/dist/assets/index-DQjt6Hly.js +0 -40
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import type { ResponsesTerminalStatus } from "../bridge";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
classifyError,
|
|
5
|
+
httpStatusFromTerminalError as httpStatusFromClassifiedTerminalError,
|
|
6
|
+
} from "../lib/errors";
|
|
4
7
|
import { CODEX_CONFIG_PATH, readRootTomlString } from "../codex/paths";
|
|
5
8
|
import { readCodexCatalogPath } from "../codex/catalog";
|
|
6
9
|
import type { OcxUsage } from "../types";
|
|
@@ -10,6 +13,8 @@ import {
|
|
|
10
13
|
usageForFinalLog,
|
|
11
14
|
usageStatusForFinalLog,
|
|
12
15
|
usageTotalTokens,
|
|
16
|
+
type AttemptRecoveryKind,
|
|
17
|
+
type PersistedUsageAttempt,
|
|
13
18
|
type UsageStatus,
|
|
14
19
|
} from "../usage/log";
|
|
15
20
|
import {
|
|
@@ -35,6 +40,11 @@ export interface RequestLogContext {
|
|
|
35
40
|
resolvedModel?: string;
|
|
36
41
|
usage?: OcxUsage;
|
|
37
42
|
usageLogInputTokens?: number;
|
|
43
|
+
attempts?: PersistedUsageAttempt[];
|
|
44
|
+
/** Internal mutable final attempt; omitted from RequestLogEntry/JSONL. */
|
|
45
|
+
activeAttempt?: PersistedUsageAttempt;
|
|
46
|
+
/** Internal wall-clock origin for the committed final attempt; never persisted. */
|
|
47
|
+
activeAttemptStartedAt?: number;
|
|
38
48
|
usageDebugBodyKind?: UsageDebugBodyKind;
|
|
39
49
|
usageDebugBodySample?: string;
|
|
40
50
|
usageDebugContentType?: string;
|
|
@@ -74,6 +84,7 @@ export interface RequestLogEntry {
|
|
|
74
84
|
usageStatus: UsageStatus;
|
|
75
85
|
usage?: OcxUsage;
|
|
76
86
|
totalTokens?: number;
|
|
87
|
+
attempts?: PersistedUsageAttempt[];
|
|
77
88
|
}
|
|
78
89
|
|
|
79
90
|
const requestLog: RequestLogEntry[] = [];
|
|
@@ -102,11 +113,13 @@ export function addRequestLog(entry: RequestLogEntry) {
|
|
|
102
113
|
model: entry.model,
|
|
103
114
|
...(entry.surface === "claude" ? { surface: entry.surface } : {}),
|
|
104
115
|
...(entry.resolvedModel ? { resolvedModel: entry.resolvedModel } : {}),
|
|
116
|
+
...(entry.requestedModel ? { requestedModel: entry.requestedModel } : {}),
|
|
105
117
|
status: entry.status,
|
|
106
118
|
durationMs: entry.durationMs,
|
|
107
119
|
usageStatus: entry.usageStatus,
|
|
108
120
|
...(entry.usage ? { usage: entry.usage } : {}),
|
|
109
121
|
...(entry.totalTokens !== undefined ? { totalTokens: entry.totalTokens } : {}),
|
|
122
|
+
...(entry.attempts?.length ? { attempts: entry.attempts } : {}),
|
|
110
123
|
...failureDiagnostics,
|
|
111
124
|
});
|
|
112
125
|
} catch {
|
|
@@ -119,10 +132,19 @@ export function nextRequestLogId(timestamp = Date.now()): string {
|
|
|
119
132
|
return `ocx-${timestamp.toString(36)}-${requestLogSeq.toString(36)}`;
|
|
120
133
|
}
|
|
121
134
|
|
|
122
|
-
export function requestLogErrorCode(status: number): string | undefined {
|
|
135
|
+
export function requestLogErrorCode(status: number, upstreamError?: string): string | undefined {
|
|
123
136
|
if (status >= 200 && status < 400) return undefined;
|
|
124
137
|
if (status === 400 || status === 409) return "invalid_request_error";
|
|
125
|
-
if (status === 401
|
|
138
|
+
if (status === 401) return "invalid_api_key";
|
|
139
|
+
if (status === 403) {
|
|
140
|
+
// Prefer message-aware codes (e.g. Ollama Cloud subscription gates) over a blunt
|
|
141
|
+
// invalid_api_key — 403 usually means authenticated but not allowed.
|
|
142
|
+
if (upstreamError?.trim()) {
|
|
143
|
+
const code = classifyError(403, "upstream_error", upstreamError).code;
|
|
144
|
+
if (code) return code;
|
|
145
|
+
}
|
|
146
|
+
return "permission_denied";
|
|
147
|
+
}
|
|
126
148
|
if (status === 429) return "rate_limit_exceeded";
|
|
127
149
|
if (status === 499) return "client_closed_request";
|
|
128
150
|
if (status === 503) return "server_is_overloaded";
|
|
@@ -179,7 +201,10 @@ export function applyResponseLogMetadata(logCtx: RequestLogContext, payload: unk
|
|
|
179
201
|
const serviceTier = (source as { service_tier?: unknown }).service_tier;
|
|
180
202
|
if (typeof serviceTier === "string" && serviceTier.trim()) logCtx.responseServiceTier = serviceTier;
|
|
181
203
|
const usage = usageFromResponsesPayload((source as { usage?: unknown }).usage);
|
|
182
|
-
if (usage)
|
|
204
|
+
if (usage) {
|
|
205
|
+
logCtx.usage = usage;
|
|
206
|
+
if (logCtx.activeAttempt) logCtx.activeAttempt.usage = usage;
|
|
207
|
+
}
|
|
183
208
|
}
|
|
184
209
|
|
|
185
210
|
export function usageFromResponsesPayload(usage: unknown): OcxUsage | undefined {
|
|
@@ -375,24 +400,36 @@ export function addFinalRequestLog(
|
|
|
375
400
|
meta?: Pick<RequestLogEntry, "terminalStatus" | "closeReason">,
|
|
376
401
|
addLog: (entry: RequestLogEntry) => void = addRequestLog,
|
|
377
402
|
): void {
|
|
378
|
-
const errorCode = requestLogErrorCode(status);
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
403
|
+
const errorCode = requestLogErrorCode(status, logCtx.upstreamError);
|
|
404
|
+
if (logCtx.activeAttempt) {
|
|
405
|
+
finishRequestAttempt(
|
|
406
|
+
logCtx.activeAttempt,
|
|
407
|
+
status,
|
|
408
|
+
Date.now() - (logCtx.activeAttemptStartedAt ?? start),
|
|
409
|
+
logCtx.usage,
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
const existing = finalizedUsage(
|
|
413
|
+
logCtx.providerAdapter ?? logCtx.provider,
|
|
414
|
+
logCtx.usage,
|
|
415
|
+
logCtx.usageLogInputTokens,
|
|
416
|
+
);
|
|
417
|
+
const attempts = logCtx.attempts?.map(attempt => ({
|
|
418
|
+
...attempt,
|
|
419
|
+
recoveryKinds: [...attempt.recoveryKinds],
|
|
420
|
+
...(attempt.usage ? { usage: { ...attempt.usage } } : {}),
|
|
421
|
+
}));
|
|
422
|
+
const isCombo = (logCtx.requestedModel ?? "").startsWith("combo/")
|
|
423
|
+
&& (attempts?.length ?? 0) > 0;
|
|
424
|
+
const aggregate = isCombo ? aggregateAttemptUsage(attempts ?? []) : null;
|
|
425
|
+
const loggedUsage = aggregate?.usage ?? existing.usage;
|
|
426
|
+
const usageStatus = aggregate?.status ?? existing.status;
|
|
427
|
+
const totalTokens = aggregate?.totalTokens ?? existing.totalTokens;
|
|
391
428
|
addLog({
|
|
392
429
|
requestId,
|
|
393
430
|
timestamp: start,
|
|
394
|
-
model: logCtx.model,
|
|
395
|
-
provider: logCtx.provider,
|
|
431
|
+
model: isCombo ? logCtx.requestedModel! : logCtx.model,
|
|
432
|
+
provider: isCombo ? "combo" : logCtx.provider,
|
|
396
433
|
...(logCtx.surface ? { surface: logCtx.surface } : {}),
|
|
397
434
|
...(logCtx.requestedModel ? { requestedModel: logCtx.requestedModel } : {}),
|
|
398
435
|
...(logCtx.requestedEffort ? { requestedEffort: logCtx.requestedEffort } : {}),
|
|
@@ -412,6 +449,7 @@ export function addFinalRequestLog(
|
|
|
412
449
|
usageStatus,
|
|
413
450
|
...(loggedUsage ? { usage: loggedUsage } : {}),
|
|
414
451
|
...(totalTokens !== undefined ? { totalTokens } : {}),
|
|
452
|
+
...(attempts?.length ? { attempts } : {}),
|
|
415
453
|
});
|
|
416
454
|
if (isUsageDebugEnabled()) {
|
|
417
455
|
appendUsageDebug({
|
|
@@ -431,7 +469,10 @@ export function addFinalRequestLog(
|
|
|
431
469
|
export function filterRequestLogs(logs: RequestLogEntry[], params: URLSearchParams): RequestLogEntry[] {
|
|
432
470
|
let filtered = logs;
|
|
433
471
|
const provider = params.get("provider")?.trim();
|
|
434
|
-
if (provider)
|
|
472
|
+
if (provider) {
|
|
473
|
+
filtered = filtered.filter(entry => entry.provider === provider
|
|
474
|
+
|| entry.attempts?.some(attempt => attempt.provider === provider));
|
|
475
|
+
}
|
|
435
476
|
const status = params.get("status")?.trim().toLowerCase();
|
|
436
477
|
if (status) {
|
|
437
478
|
filtered = /^[1-5]xx$/.test(status)
|
|
@@ -446,4 +487,164 @@ export function filterRequestLogs(logs: RequestLogEntry[], params: URLSearchPara
|
|
|
446
487
|
return filtered;
|
|
447
488
|
}
|
|
448
489
|
|
|
490
|
+
interface FinalizedUsageResult {
|
|
491
|
+
usage?: OcxUsage;
|
|
492
|
+
status: UsageStatus;
|
|
493
|
+
totalTokens?: number;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function finalizedUsage(
|
|
497
|
+
adapter: string,
|
|
498
|
+
usage: OcxUsage | undefined,
|
|
499
|
+
inputTokenEstimate: number | undefined,
|
|
500
|
+
): FinalizedUsageResult {
|
|
501
|
+
const estimate = typeof inputTokenEstimate === "number"
|
|
502
|
+
&& Number.isFinite(inputTokenEstimate)
|
|
503
|
+
&& inputTokenEstimate >= 0
|
|
504
|
+
? inputTokenEstimate
|
|
505
|
+
: undefined;
|
|
506
|
+
const finalUsage = usageForFinalLog(adapter, usage);
|
|
507
|
+
const usageFallback = !finalUsage && estimate !== undefined
|
|
508
|
+
? { inputTokens: estimate, outputTokens: 0, estimated: true }
|
|
509
|
+
: undefined;
|
|
510
|
+
const loggedUsage = finalUsage && estimate !== undefined
|
|
511
|
+
? {
|
|
512
|
+
...finalUsage,
|
|
513
|
+
inputTokens: Math.max(finalUsage.inputTokens, estimate),
|
|
514
|
+
estimated: true,
|
|
515
|
+
}
|
|
516
|
+
: (finalUsage ?? usageFallback);
|
|
517
|
+
const totalTokens = usageTotalTokens(loggedUsage);
|
|
518
|
+
return {
|
|
519
|
+
status: usageStatusForFinalLog(loggedUsage),
|
|
520
|
+
...(loggedUsage ? { usage: loggedUsage } : {}),
|
|
521
|
+
...(totalTokens !== undefined ? { totalTokens } : {}),
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
export function beginRequestAttempt(
|
|
526
|
+
ordinal: number,
|
|
527
|
+
provider: string,
|
|
528
|
+
model: string,
|
|
529
|
+
adapter: string,
|
|
530
|
+
): PersistedUsageAttempt {
|
|
531
|
+
return {
|
|
532
|
+
ordinal,
|
|
533
|
+
provider,
|
|
534
|
+
model,
|
|
535
|
+
adapter,
|
|
536
|
+
status: 0,
|
|
537
|
+
durationMs: 0,
|
|
538
|
+
sendCount: 0,
|
|
539
|
+
recoveryKinds: [],
|
|
540
|
+
usageStatus: "unreported",
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
export function sealRequestAttemptIdentity(
|
|
545
|
+
attempt: PersistedUsageAttempt | undefined,
|
|
546
|
+
provider: string,
|
|
547
|
+
adapter: string,
|
|
548
|
+
): void {
|
|
549
|
+
if (!attempt) return;
|
|
550
|
+
attempt.provider = provider;
|
|
551
|
+
attempt.adapter = adapter;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
export function noteAttemptSend(
|
|
555
|
+
attempt: PersistedUsageAttempt | undefined,
|
|
556
|
+
inputTokenEstimate: number | undefined,
|
|
557
|
+
recovery?: AttemptRecoveryKind,
|
|
558
|
+
): void {
|
|
559
|
+
if (!attempt) return;
|
|
560
|
+
attempt.sendCount += 1;
|
|
561
|
+
if (typeof inputTokenEstimate === "number"
|
|
562
|
+
&& Number.isFinite(inputTokenEstimate)
|
|
563
|
+
&& inputTokenEstimate >= 0) {
|
|
564
|
+
attempt.inputTokenEstimate = inputTokenEstimate;
|
|
565
|
+
}
|
|
566
|
+
if (recovery && !attempt.recoveryKinds.includes(recovery)) {
|
|
567
|
+
attempt.recoveryKinds.push(recovery);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
export function finishRequestAttempt(
|
|
572
|
+
attempt: PersistedUsageAttempt,
|
|
573
|
+
status: number,
|
|
574
|
+
durationMs: number,
|
|
575
|
+
usage?: OcxUsage,
|
|
576
|
+
): PersistedUsageAttempt {
|
|
577
|
+
const finalized = finalizedUsage(
|
|
578
|
+
attempt.adapter,
|
|
579
|
+
usage ?? attempt.usage,
|
|
580
|
+
attempt.inputTokenEstimate,
|
|
581
|
+
);
|
|
582
|
+
attempt.status = status;
|
|
583
|
+
attempt.durationMs = Math.max(0, durationMs);
|
|
584
|
+
attempt.usageStatus = finalized.status;
|
|
585
|
+
if (finalized.usage) attempt.usage = finalized.usage;
|
|
586
|
+
else delete attempt.usage;
|
|
587
|
+
if (finalized.totalTokens !== undefined) attempt.totalTokens = finalized.totalTokens;
|
|
588
|
+
else delete attempt.totalTokens;
|
|
589
|
+
const errorCode = requestLogErrorCode(status);
|
|
590
|
+
if (errorCode) attempt.errorCode = errorCode;
|
|
591
|
+
else delete attempt.errorCode;
|
|
592
|
+
return attempt;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
export function aggregateAttemptUsage(
|
|
596
|
+
attempts: readonly PersistedUsageAttempt[],
|
|
597
|
+
): FinalizedUsageResult {
|
|
598
|
+
const status: UsageStatus = attempts.length > 0
|
|
599
|
+
&& attempts.every(attempt => attempt.usageStatus === "unsupported")
|
|
600
|
+
? "unsupported"
|
|
601
|
+
: attempts.some(attempt => (
|
|
602
|
+
attempt.usageStatus === "unreported" || attempt.usageStatus === "unsupported"
|
|
603
|
+
))
|
|
604
|
+
? "unreported"
|
|
605
|
+
: attempts.some(attempt => attempt.usageStatus === "estimated")
|
|
606
|
+
? "estimated"
|
|
607
|
+
: attempts.length > 0
|
|
608
|
+
? "reported"
|
|
609
|
+
: "unreported";
|
|
610
|
+
|
|
611
|
+
const usages = attempts.flatMap(attempt => attempt.usage ? [attempt.usage] : []);
|
|
612
|
+
if (usages.length === 0) return { status };
|
|
613
|
+
|
|
614
|
+
const sumOptional = (
|
|
615
|
+
key: "cachedInputTokens" | "cacheReadInputTokens" | "cacheCreationInputTokens"
|
|
616
|
+
| "reasoningOutputTokens",
|
|
617
|
+
): number | undefined => {
|
|
618
|
+
const present = usages.flatMap(usage => (
|
|
619
|
+
typeof usage[key] === "number" ? [usage[key] as number] : []
|
|
620
|
+
));
|
|
621
|
+
return present.length > 0 ? present.reduce((sum, value) => sum + value, 0) : undefined;
|
|
622
|
+
};
|
|
623
|
+
const cachedInputTokens = sumOptional("cachedInputTokens");
|
|
624
|
+
const cacheReadInputTokens = sumOptional("cacheReadInputTokens");
|
|
625
|
+
const cacheCreationInputTokens = sumOptional("cacheCreationInputTokens");
|
|
626
|
+
const reasoningOutputTokens = sumOptional("reasoningOutputTokens");
|
|
627
|
+
const totalTokens = usages.reduce(
|
|
628
|
+
(sum, usage) => sum + (usageTotalTokens(usage) ?? 0),
|
|
629
|
+
0,
|
|
630
|
+
);
|
|
631
|
+
const aggregate: OcxUsage = {
|
|
632
|
+
inputTokens: usages.reduce((sum, usage) => sum + usage.inputTokens, 0),
|
|
633
|
+
outputTokens: usages.reduce((sum, usage) => sum + usage.outputTokens, 0),
|
|
634
|
+
totalTokens,
|
|
635
|
+
...(cachedInputTokens !== undefined ? { cachedInputTokens } : {}),
|
|
636
|
+
...(cacheReadInputTokens !== undefined ? { cacheReadInputTokens } : {}),
|
|
637
|
+
...(cacheCreationInputTokens !== undefined ? { cacheCreationInputTokens } : {}),
|
|
638
|
+
...(reasoningOutputTokens !== undefined ? { reasoningOutputTokens } : {}),
|
|
639
|
+
...(status === "estimated" ? { estimated: true } : {}),
|
|
640
|
+
};
|
|
641
|
+
return { usage: aggregate, status, totalTokens };
|
|
642
|
+
}
|
|
643
|
+
|
|
449
644
|
export function getRequestLogEntries(): RequestLogEntry[] { return requestLog; }
|
|
645
|
+
|
|
646
|
+
/** Test-only process-state reset for isolated integration harnesses. */
|
|
647
|
+
export function clearRequestLogsForTests(): void {
|
|
648
|
+
requestLog.length = 0;
|
|
649
|
+
requestLogSeq = 0;
|
|
650
|
+
}
|