@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.
Files changed (55) hide show
  1. package/README.ko.md +37 -7
  2. package/README.md +52 -11
  3. package/README.zh-CN.md +36 -7
  4. package/bin/ocx.mjs +5 -3
  5. package/gui/dist/assets/index-BzhyTAco.js +40 -0
  6. package/gui/dist/assets/index-Dq3eZ1cU.css +1 -0
  7. package/gui/dist/index.html +2 -2
  8. package/gui/dist/provider-icons/opencode.svg +1 -1
  9. package/package.json +5 -2
  10. package/src/adapters/anthropic-image-normalize.ts +70 -29
  11. package/src/adapters/cursor/transport-retry.ts +20 -1
  12. package/src/adapters/run-turn-queue.ts +40 -0
  13. package/src/codex/auth-api.ts +10 -1
  14. package/src/codex/auth-context.ts +33 -7
  15. package/src/codex/catalog.ts +357 -23
  16. package/src/codex/routing.ts +10 -4
  17. package/src/combos/failover.ts +102 -0
  18. package/src/combos/index.ts +37 -0
  19. package/src/combos/request.ts +31 -0
  20. package/src/combos/resolve.ts +171 -0
  21. package/src/combos/types.ts +203 -0
  22. package/src/config.ts +280 -11
  23. package/src/lib/errors.ts +86 -24
  24. package/src/lib/upstream-retry.ts +8 -4
  25. package/src/oauth/index.ts +7 -1
  26. package/src/oauth/key-providers.ts +2 -32
  27. package/src/oauth/login-cli.ts +4 -3
  28. package/src/oauth/token-guardian.ts +38 -3
  29. package/src/providers/derive.ts +27 -2
  30. package/src/providers/kiro-models.ts +8 -3
  31. package/src/providers/label.ts +3 -1
  32. package/src/providers/openai-sidecar.ts +94 -0
  33. package/src/providers/openai-tier-startup.ts +27 -0
  34. package/src/providers/openai-tiers.ts +283 -0
  35. package/src/providers/openai-virtual-models.ts +82 -0
  36. package/src/providers/quota.ts +344 -24
  37. package/src/providers/registry.ts +112 -20
  38. package/src/reasoning-effort.ts +12 -11
  39. package/src/router.ts +80 -36
  40. package/src/server/auth-cors.ts +85 -9
  41. package/src/server/images.ts +31 -75
  42. package/src/server/index.ts +45 -86
  43. package/src/server/management-api.ts +273 -21
  44. package/src/server/request-log.ts +221 -20
  45. package/src/server/responses.ts +594 -75
  46. package/src/server/search.ts +22 -37
  47. package/src/types.ts +49 -1
  48. package/src/update/index.ts +50 -6
  49. package/src/update/job.ts +21 -4
  50. package/src/usage/log.ts +124 -1
  51. package/src/usage/summary.ts +147 -56
  52. package/src/vision/index.ts +20 -19
  53. package/src/web-search/index.ts +15 -17
  54. package/gui/dist/assets/index-Bk_GgFrh.css +0 -1
  55. 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 { httpStatusFromTerminalError as httpStatusFromClassifiedTerminalError } from "../lib/errors";
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 || status === 403) return "invalid_api_key";
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) logCtx.usage = 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
- // Estimated-usage detection prefers the route ADAPTER: configured provider names
380
- // ("cursor-mykey") broke the old exact-name match and cursor rows logged as
381
- // accurately "reported" (devlog 130 B2).
382
- const finalUsage = usageForFinalLog(logCtx.providerAdapter ?? logCtx.provider, logCtx.usage);
383
- const usageFallback = !finalUsage && typeof logCtx.usageLogInputTokens === "number"
384
- ? { inputTokens: logCtx.usageLogInputTokens, outputTokens: 0, estimated: true }
385
- : undefined;
386
- const loggedUsage = finalUsage && typeof logCtx.usageLogInputTokens === "number"
387
- ? { ...finalUsage, inputTokens: Math.max(finalUsage.inputTokens, logCtx.usageLogInputTokens), estimated: true }
388
- : (finalUsage ?? usageFallback);
389
- const usageStatus = usageStatusForFinalLog(loggedUsage);
390
- const totalTokens = usageTotalTokens(loggedUsage);
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) filtered = filtered.filter(entry => entry.provider === 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
+ }