@bitkyc08/opencodex 2.14.0 → 2.14.2

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 (60) hide show
  1. package/README.md +55 -0
  2. package/gui/dist/assets/index-DUCH59lJ.css +1 -0
  3. package/gui/dist/assets/index-DUyQeU1j.js +76 -0
  4. package/gui/dist/index.html +2 -2
  5. package/package.json +1 -1
  6. package/src/adapters/command-code.ts +46 -6
  7. package/src/adapters/cursor/request-builder.ts +54 -10
  8. package/src/adapters/cursor/tool-definitions.ts +24 -0
  9. package/src/adapters/kiro.ts +10 -1
  10. package/src/adapters/openai-chat-url.ts +11 -0
  11. package/src/adapters/openai-chat.ts +7 -4
  12. package/src/adapters/openai-responses-url.ts +14 -0
  13. package/src/adapters/openai-responses.ts +111 -2
  14. package/src/adapters/tool-catalog-nudge.ts +26 -4
  15. package/src/bridge.ts +50 -3
  16. package/src/cli/init.ts +4 -17
  17. package/src/codex/auth-api.ts +2 -74
  18. package/src/codex/catalog/effort.ts +2 -1
  19. package/src/codex/catalog/metadata.ts +62 -12
  20. package/src/codex/catalog/native-models.ts +27 -0
  21. package/src/codex/catalog/parsing.ts +27 -8
  22. package/src/codex/catalog/provider-fetch.ts +47 -5
  23. package/src/codex/catalog/sync.ts +31 -8
  24. package/src/codex/catalog.ts +1 -1
  25. package/src/codex/features.ts +14 -3
  26. package/src/codex/model-cache.ts +7 -1
  27. package/src/codex/native-main-claim.ts +13 -2
  28. package/src/config.ts +79 -4
  29. package/src/generated/compatibility-version.json +74 -46
  30. package/src/lab/ledger/store.ts +0 -18
  31. package/src/lab/subject/installation-salt.ts +13 -2
  32. package/src/lib/app-owned-memory-stores.ts +22 -0
  33. package/src/lib/tool-argument-integers.ts +158 -0
  34. package/src/oauth/nous.ts +58 -9
  35. package/src/providers/base-url-choices.ts +10 -0
  36. package/src/providers/command-code-efforts.ts +18 -0
  37. package/src/providers/model-rename-migration.ts +202 -0
  38. package/src/providers/model-rename-startup.ts +28 -0
  39. package/src/providers/openai-tier-startup.ts +31 -2
  40. package/src/providers/quota.ts +9 -2
  41. package/src/providers/registry.ts +17 -10
  42. package/src/responses/spill-store.ts +5 -1
  43. package/src/responses/state.ts +50 -2
  44. package/src/router.ts +12 -1
  45. package/src/server/index.ts +3 -2
  46. package/src/server/management/api-key-usage.ts +31 -5
  47. package/src/server/management/config-routes.ts +51 -16
  48. package/src/server/management/logs-usage-routes.ts +48 -10
  49. package/src/server/management/provider-routes.ts +2 -1
  50. package/src/server/management/usage-summary-cache.ts +7 -1
  51. package/src/server/responses/collaboration.ts +12 -2
  52. package/src/server/responses/core.ts +33 -17
  53. package/src/server/responses/fetch-helpers.ts +12 -1
  54. package/src/server/responses/ws-upstream.ts +199 -0
  55. package/src/server/startup-health-cache.ts +12 -0
  56. package/src/usage/log.ts +430 -12
  57. package/src/vision/index.ts +25 -4
  58. package/src/vision/timeout-bounds.ts +9 -0
  59. package/gui/dist/assets/index-BNVYzdn0.css +0 -1
  60. package/gui/dist/assets/index-Co12XTT-.js +0 -76
@@ -6,6 +6,7 @@ import {
6
6
  QWEN_CLOUD_BASE_URL_CHOICES, QWEN_CLOUD_TOKEN_PLAN_BASE_URL,
7
7
  ALIBABA_INTL_BASE_URL_CHOICES, ALIBABA_INTL_TOKEN_PLAN_BASE_URL,
8
8
  ALIBABA_CODING_BASE_URL_CHOICES, ALIBABA_CODING_INTL_BASE_URL,
9
+ MOONSHOT_BASE_URL_CHOICES, MOONSHOT_INTL_BASE_URL,
9
10
  } from "./base-url-choices";
10
11
  import {
11
12
  CURSOR_STATIC_MODELS,
@@ -950,8 +951,8 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
950
951
  // devlog/model_update/260709_model_refresh/001_xai_lineup.md.
951
952
  // grok-4.20-multi-agent-0309 is intentionally absent: the OAuth chat-completions
952
953
  // transport returns 400 ("Multi Agent requests are not allowed on chat completions").
953
- // 260813: grok-4.6 added per the new docs.x.ai/developers/grok-4-6 page; specs mirrored
954
- // from grok-4.5 until the official capability/pricing tables settle.
954
+ // 260813: grok-4.6 added per docs.x.ai/developers/grok-4-6. Context/vision still match
955
+ // grok-4.5; the reasoning ladder does not — 4.6 adds the documented xhigh rung.
955
956
  models: ["grok-4.6", "grok-4.5", "grok-4.3", "grok-4.20-0309-reasoning", "grok-4.20-0309-non-reasoning", "grok-build-0.1", "grok-composer-2.5-fast"],
956
957
  defaultModel: "grok-4.5",
957
958
  // Vision lineup per docs.x.ai model-capabilities/images/understanding: the grok-4.x chat
@@ -973,8 +974,11 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
973
974
  // (docs.x.ai prompt-caching/multi-turn, verified 2026-07-13 — devlog/_plan/260713_grok_caching).
974
975
  // Models that never emit reasoning simply have no thinking parts to replay (no-op).
975
976
  preserveReasoningContentModels: ["grok-4.6", "grok-4.5", "grok-4.3", "grok-4.20-0309-reasoning"],
976
- // grok-4.5 reasoning is always-on with low/medium/high control (no off tier upstream).
977
- modelReasoningEfforts: { "grok-4.6": ["low", "medium", "high"], "grok-4.5": ["low", "medium", "high"] },
977
+ // grok-4.5 reasoning is always-on with low/medium/high (no off tier, no xhigh).
978
+ // grok-4.6 adds xhigh per docs.x.ai/developers/model-capabilities/text/reasoning;
979
+ // xAI documents high as the upstream default.
980
+ modelReasoningEfforts: { "grok-4.6": ["low", "medium", "high", "xhigh"], "grok-4.5": ["low", "medium", "high"] },
981
+ modelDefaultReasoningEfforts: { "grok-4.6": "high" },
978
982
  modelContextWindows: {
979
983
  "grok-4.6": 500_000,
980
984
  "grok-4.5": 500_000,
@@ -1300,10 +1304,10 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
1300
1304
  modelContextWindows: CLINE_PASS_MODEL_CONTEXT_WINDOWS,
1301
1305
  modelInputModalities: CLINE_PASS_MODEL_INPUT_MODALITIES,
1302
1306
  noVisionModels: CLINE_PASS_TEXT_ONLY_MODELS,
1303
- // Only low and the `reasoning: { enabled, effort }` request shape have been accepted by a live
1304
- // ClinePass request. Neither wire detail is currently documented, so clamp higher Codex
1305
- // requests to the verified tier until the gateway documents or is live-probed more broadly.
1306
- reasoningEfforts: ["low"],
1307
+ // Live-probed 2026-08-13 across every static ClinePass model: the gateway accepts and
1308
+ // validates low/medium/high/xhigh/max, and rejects an invalid sentinel. Preserve the
1309
+ // caller's requested tier and let ClinePass own any backend-specific normalization.
1310
+ reasoningEfforts: ["low", "medium", "high", "xhigh", "max"],
1307
1311
  reasoningWireFormat: "gateway-object",
1308
1312
  preserveCustomDestination: true,
1309
1313
  note: "ClinePass subscription API. Uses a Cline API key and the full cline-pass/<model> upstream slug; quota is shared across the account's rolling 5-hour, weekly, and monthly limits.",
@@ -1789,7 +1793,7 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
1789
1793
  liveModels: true,
1790
1794
  preserveCustomDestination: true,
1791
1795
  // /v1/models is documented as callable authenticated or unauthenticated, so a 2xx catalog
1792
- // response cannot prove the supplied Bearer key is valid.
1796
+ // response cannot prove that the supplied Bearer key is valid.
1793
1797
  apiKeyValidation: "unknown",
1794
1798
  // Featherless documents tool calling, but not a provider-wide parallel tool-call contract.
1795
1799
  parallelToolCalls: false,
@@ -1870,7 +1874,9 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
1870
1874
  note: "Model data frozen pending Tier-2 entitlement proof",
1871
1875
  },
1872
1876
  {
1873
- id: "moonshot", label: "Moonshot (Kimi API)", baseUrl: "https://api.moonshot.ai/v1", adapter: "openai-chat", authKind: "key",
1877
+ id: "moonshot", label: "Moonshot (Kimi API)", baseUrl: MOONSHOT_INTL_BASE_URL, adapter: "openai-chat", authKind: "key",
1878
+ allowBaseUrlOverride: true,
1879
+ baseUrlChoices: MOONSHOT_BASE_URL_CHOICES,
1874
1880
  dashboardUrl: "https://platform.moonshot.ai/console/api-keys", defaultModel: "kimi-k2.7-code", jawcodeBundle: "moonshot",
1875
1881
  models: KIMI_API_MODELS,
1876
1882
  modelContextWindows: KIMI_API_MODEL_CONTEXT_WINDOWS,
@@ -1882,6 +1888,7 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
1882
1888
  noPenaltyModels: KIMI_API_MODELS,
1883
1889
  autoToolChoiceOnlyModels: ["kimi-k2.7-code", "kimi-k2.7-code-highspeed"],
1884
1890
  preserveReasoningContentModels: KIMI_API_MODELS,
1891
+ note: "International default (api.moonshot.ai). China accounts: choose China (.cn) or Custom for api.moonshot.cn.",
1885
1892
  },
1886
1893
  { id: "huggingface", label: "Hugging Face", baseUrl: "https://router.huggingface.co/v1", adapter: "openai-chat", authKind: "key", dashboardUrl: "https://huggingface.co/settings/tokens" },
1887
1894
  // 260715 NIM hardening (issue #126, devlog/_plan/260715_issue126_nim_kimi):
@@ -34,6 +34,7 @@ export interface ResponseSpillPayload {
34
34
  version: 1;
35
35
  responseId: string;
36
36
  createdAt: number;
37
+ clientThreadId?: string;
37
38
  items: unknown[];
38
39
  providers?: OcxProviderContinuationState;
39
40
  }
@@ -260,9 +261,11 @@ function validPayload(value: unknown, responseId: string): value is ResponseSpil
260
261
  if (!value || typeof value !== "object" || Array.isArray(value)) return false;
261
262
  const payload = value as Record<string, unknown>;
262
263
  const keys = Object.keys(payload);
263
- if (keys.some(key => !["version", "responseId", "createdAt", "items", "providers"].includes(key))) return false;
264
+ if (keys.some(key => !["version", "responseId", "createdAt", "clientThreadId", "items", "providers"].includes(key))) return false;
264
265
  if (payload.version !== 1 || payload.responseId !== responseId) return false;
265
266
  if (typeof payload.createdAt !== "number" || !Number.isFinite(payload.createdAt)) return false;
267
+ if (payload.clientThreadId !== undefined
268
+ && (typeof payload.clientThreadId !== "string" || payload.clientThreadId.trim().length === 0)) return false;
266
269
  if (!Array.isArray(payload.items)) return false;
267
270
  if (payload.providers !== undefined) {
268
271
  if (!payload.providers || typeof payload.providers !== "object" || Array.isArray(payload.providers)) return false;
@@ -284,6 +287,7 @@ export function writeResponseSpillDurably(
284
287
  version: 1,
285
288
  responseId,
286
289
  createdAt: state.createdAt,
290
+ ...(state.clientThreadId ? { clientThreadId: state.clientThreadId } : {}),
287
291
  items: state.items,
288
292
  ...(state.providers ? { providers: state.providers } : {}),
289
293
  };
@@ -37,6 +37,7 @@ const MAX_SNAPSHOT_REWRITE_ATTEMPTS = 4;
37
37
  interface ResidentResponseState {
38
38
  kind: "resident";
39
39
  createdAt: number;
40
+ clientThreadId?: string;
40
41
  items: unknown[];
41
42
  providers?: OcxProviderContinuationState;
42
43
  sizeBytes: number;
@@ -45,6 +46,7 @@ interface ResidentResponseState {
45
46
  interface SpilledResponseState {
46
47
  kind: "spill";
47
48
  createdAt: number;
49
+ clientThreadId?: string;
48
50
  providers?: OcxProviderContinuationState;
49
51
  spill: ResponseSpillRef;
50
52
  sizeBytes: number;
@@ -65,6 +67,7 @@ export type PreviousResponseReplayFailure = {
65
67
  };
66
68
 
67
69
  const states = new Map<string, StoredResponseState>();
70
+ const replayScopeMismatches = new WeakSet<object>();
68
71
  let storedResponseBytes = 0;
69
72
  let residentResponseBytes = 0;
70
73
  let oldestResidentId: string | undefined;
@@ -80,6 +83,7 @@ const spillCounters = { writes: 0, writeFailures: 0, readFailures: 0 };
80
83
  * snapshot files refused before parse.
81
84
  */
82
85
  const admissionCounters = { directSpills: 0, oversizedDrops: 0, snapshotOversizedRefusals: 0 };
86
+ let replayScopeMismatchDrops = 0;
83
87
 
84
88
  /** Test-only: admission-boundary counters (proves the new paths fire). */
85
89
  export function responseAdmissionCountersForTests(): Readonly<typeof admissionCounters> {
@@ -124,6 +128,7 @@ function measureResidentEntry(id: string, entry: ResidentInput): ResidentRespons
124
128
  const sizeBytes = serializedBytes({
125
129
  responseId: id,
126
130
  createdAt: entry.createdAt,
131
+ ...(entry.clientThreadId ? { clientThreadId: entry.clientThreadId } : {}),
127
132
  items: entry.items,
128
133
  ...(entry.providers ? { providers: entry.providers } : {}),
129
134
  });
@@ -224,6 +229,7 @@ function swapResidentForSpill(id: string, expected: ResidentResponseState, ref:
224
229
  const base: Omit<SpilledResponseState, "sizeBytes"> = {
225
230
  kind: "spill",
226
231
  createdAt: expected.createdAt,
232
+ ...(expected.clientThreadId ? { clientThreadId: expected.clientThreadId } : {}),
227
233
  ...(expected.providers ? { providers: expected.providers } : {}),
228
234
  spill: ref,
229
235
  };
@@ -244,12 +250,14 @@ function replaceSpillEntryAtomically(
244
250
  try {
245
251
  const ref = writeResponseSpillDurably(id, {
246
252
  createdAt: candidate.createdAt,
253
+ ...(candidate.clientThreadId ? { clientThreadId: candidate.clientThreadId } : {}),
247
254
  items: candidate.items,
248
255
  ...(candidate.providers ? { providers: candidate.providers } : {}),
249
256
  });
250
257
  const base: Omit<SpilledResponseState, "sizeBytes"> = {
251
258
  kind: "spill",
252
259
  createdAt: candidate.createdAt,
260
+ ...(candidate.clientThreadId ? { clientThreadId: candidate.clientThreadId } : {}),
253
261
  ...(candidate.providers ? { providers: candidate.providers } : {}),
254
262
  spill: ref,
255
263
  };
@@ -322,6 +330,7 @@ function admitOversizedCandidate(
322
330
  try {
323
331
  const ref = writeResponseSpillDurably(id, {
324
332
  createdAt: candidate.createdAt,
333
+ ...(candidate.clientThreadId ? { clientThreadId: candidate.clientThreadId } : {}),
325
334
  items: candidate.items,
326
335
  ...(candidate.providers ? { providers: candidate.providers } : {}),
327
336
  });
@@ -337,6 +346,7 @@ function admitOversizedCandidate(
337
346
  const base: Omit<SpilledResponseState, "sizeBytes"> = {
338
347
  kind: "spill",
339
348
  createdAt: candidate.createdAt,
349
+ ...(candidate.clientThreadId ? { clientThreadId: candidate.clientThreadId } : {}),
340
350
  ...(candidate.providers ? { providers: candidate.providers } : {}),
341
351
  spill: ref,
342
352
  };
@@ -385,6 +395,7 @@ function snapshotPath(): string {
385
395
 
386
396
  interface LegacySnapshotState {
387
397
  createdAt?: unknown;
398
+ clientThreadId?: unknown;
388
399
  items?: unknown;
389
400
  providers?: OcxProviderContinuationState;
390
401
  conversationId?: unknown;
@@ -405,11 +416,15 @@ function loadSnapshotEntry(id: string, value: unknown): void {
405
416
  if (!value || typeof value !== "object" || Array.isArray(value)) return;
406
417
  const rec = value as LegacySnapshotState & { kind?: unknown; spill?: unknown };
407
418
  if (typeof rec.createdAt !== "number" || !Number.isFinite(rec.createdAt)) return;
419
+ const clientThreadId = typeof rec.clientThreadId === "string" && rec.clientThreadId.trim().length > 0
420
+ ? rec.clientThreadId.trim()
421
+ : undefined;
408
422
  if (rec.kind === "spill") {
409
423
  if (!isSpillRef(rec.spill)) return;
410
424
  const base: Omit<SpilledResponseState, "sizeBytes"> = {
411
425
  kind: "spill",
412
426
  createdAt: rec.createdAt,
427
+ ...(clientThreadId ? { clientThreadId } : {}),
413
428
  ...(rec.providers ? { providers: rec.providers } : {}),
414
429
  spill: rec.spill,
415
430
  };
@@ -434,6 +449,7 @@ function loadSnapshotEntry(id: string, value: unknown): void {
434
449
  : undefined);
435
450
  const resident = measureResidentEntry(id, {
436
451
  createdAt: rec.createdAt,
452
+ ...(clientThreadId ? { clientThreadId } : {}),
437
453
  items: rec.items,
438
454
  ...(providers ? { providers } : {}),
439
455
  });
@@ -747,6 +763,7 @@ function pruneResponses(at = now()): void {
747
763
  try {
748
764
  const ref = writeResponseSpillDurably(oldestId, {
749
765
  createdAt: entry.createdAt,
766
+ ...(entry.clientThreadId ? { clientThreadId: entry.clientThreadId } : {}),
750
767
  items: entry.items,
751
768
  ...(entry.providers ? { providers: entry.providers } : {}),
752
769
  });
@@ -788,6 +805,7 @@ export function evictOldestResponseContinuationForBudget(): number {
788
805
  try {
789
806
  const ref = writeResponseSpillDurably(id, {
790
807
  createdAt: entry.createdAt,
808
+ ...(entry.clientThreadId ? { clientThreadId: entry.clientThreadId } : {}),
791
809
  items: entry.items,
792
810
  ...(entry.providers ? { providers: entry.providers } : {}),
793
811
  });
@@ -828,6 +846,7 @@ function materializeEntry(
828
846
  }
829
847
  const state = measureResidentEntry(id, {
830
848
  createdAt: result.payload.createdAt,
849
+ ...(result.payload.clientThreadId ? { clientThreadId: result.payload.clientThreadId } : {}),
831
850
  items: result.payload.items,
832
851
  ...(result.payload.providers ? { providers: result.payload.providers } : {}),
833
852
  });
@@ -840,7 +859,16 @@ function materializeEntry(
840
859
  return { ok: true, state };
841
860
  }
842
861
 
843
- export function expandPreviousResponseInput(body: unknown): unknown {
862
+ function normalizedClientThreadId(value: unknown): string | undefined {
863
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
864
+ }
865
+
866
+ function withoutPreviousResponseId(request: Record<string, unknown>): Record<string, unknown> {
867
+ const { previous_response_id: _previousResponseId, ...freshRequest } = request;
868
+ return freshRequest;
869
+ }
870
+
871
+ export function expandPreviousResponseInput(body: unknown, clientThreadId?: string): unknown {
844
872
  if (!body || typeof body !== "object" || Array.isArray(body)) return body;
845
873
  const request = body as Record<string, unknown>;
846
874
  const previousId = typeof request.previous_response_id === "string" ? request.previous_response_id : undefined;
@@ -854,6 +882,16 @@ export function expandPreviousResponseInput(body: unknown): unknown {
854
882
  replayFailures.set(request, materialized.failure);
855
883
  return body;
856
884
  }
885
+ const requestThreadId = normalizedClientThreadId(clientThreadId);
886
+ const storedThreadId = normalizedClientThreadId(materialized.state.clientThreadId);
887
+ // A Codex task must never inherit another task's continuation, nor a legacy unscoped entry.
888
+ // Unscoped callers retain backward-compatible replay only with other unscoped entries.
889
+ if (requestThreadId !== storedThreadId) {
890
+ const freshRequest = withoutPreviousResponseId(request);
891
+ replayScopeMismatches.add(freshRequest);
892
+ replayScopeMismatchDrops += 1;
893
+ return freshRequest;
894
+ }
857
895
  const expanded = {
858
896
  ...request,
859
897
  input: [...materialized.state.items, ...inputItems(request.input)],
@@ -873,6 +911,11 @@ export function previousResponseReplayPrefixLength(body: unknown): number {
873
911
  return replayedInputPrefixLengths.get(body) ?? 0;
874
912
  }
875
913
 
914
+ /** True when a stale or foreign previous_response_id was removed from this exact request body. */
915
+ export function previousResponseScopeMismatch(body: unknown): boolean {
916
+ return !!body && typeof body === "object" && replayScopeMismatches.has(body as object);
917
+ }
918
+
876
919
  export function previousResponseConversationId(responseId: string | undefined): string | undefined {
877
920
  return previousResponseProviderState(responseId)?.cursor?.conversationId;
878
921
  }
@@ -898,6 +941,7 @@ export interface ResponseStateMetrics {
898
941
  spillWrites: number;
899
942
  spillWriteFailures: number;
900
943
  spillReadFailures: number;
944
+ replayScopeMismatchDrops: number;
901
945
  }
902
946
 
903
947
  /**
@@ -940,6 +984,7 @@ export function responseStateMetrics(): ResponseStateMetrics {
940
984
  spillWrites: spillCounters.writes,
941
985
  spillWriteFailures: spillCounters.writeFailures,
942
986
  spillReadFailures: spillCounters.readFailures,
987
+ replayScopeMismatchDrops,
943
988
  };
944
989
  }
945
990
 
@@ -972,7 +1017,7 @@ export function rememberResponseState(
972
1017
  requestBody: unknown,
973
1018
  response: { id?: unknown; output?: unknown; status?: unknown; incomplete_details?: unknown },
974
1019
  providerState?: OcxProviderContinuationState | string,
975
- opts?: { force?: boolean },
1020
+ opts?: { force?: boolean; clientThreadId?: string },
976
1021
  ): void {
977
1022
  if (!requestBody || typeof requestBody !== "object" || Array.isArray(requestBody)) return;
978
1023
  const request = requestBody as Record<string, unknown>;
@@ -998,8 +1043,10 @@ export function rememberResponseState(
998
1043
  return !!item && typeof item === "object" && (item as { type?: unknown }).type === "function_call";
999
1044
  });
1000
1045
  }
1046
+ const clientThreadId = normalizedClientThreadId(opts?.clientThreadId);
1001
1047
  setResidentEntry(response.id, {
1002
1048
  createdAt: now(),
1049
+ ...(clientThreadId ? { clientThreadId } : {}),
1003
1050
  items: [...inputItems(request.input), ...response.output],
1004
1051
  // Always preserve the Cursor conversation id so the next tool-result turn can continue the SAME
1005
1052
  // Cursor conversation (multi-turn continuation). Separately track whether Cursor's own
@@ -1045,6 +1092,7 @@ export function clearResponseStateMemoryForTests(): void {
1045
1092
  spillCounters.writes = 0;
1046
1093
  spillCounters.writeFailures = 0;
1047
1094
  spillCounters.readFailures = 0;
1095
+ replayScopeMismatchDrops = 0;
1048
1096
  persistAttemptHookForTests = null;
1049
1097
  loaded = false;
1050
1098
  }
package/src/router.ts CHANGED
@@ -267,6 +267,14 @@ export function routedProviderConfig(providerName: string, provider: OcxProvider
267
267
  const modelReasoningEffortMap = mergeNestedRecord(registryEntry.modelReasoningEffortMap, provider.modelReasoningEffortMap);
268
268
  const modelReasoningEfforts = mergeStringArrayRecord(registryEntry.modelReasoningEfforts, provider.modelReasoningEfforts);
269
269
  const modelDefaultReasoningEfforts = mergeRecordFill(registryEntry.modelDefaultReasoningEfforts, provider.modelDefaultReasoningEfforts);
270
+ // Key-login used to persist this exact low-only ClinePass capability seed. Once the gateway's
271
+ // wider input ladder was live-verified, leaving that generated row untouched would keep old
272
+ // installs clamped forever. This branch is reached only after canonical transport matching, so
273
+ // same-named custom destinations and every other explicit ladder still retain user precedence.
274
+ const repairLegacyClinePassReasoningEfforts = providerName === "cline-pass"
275
+ && provider.reasoningWireFormat === "gateway-object"
276
+ && provider.reasoningEfforts?.length === 1
277
+ && provider.reasoningEfforts[0] === "low";
270
278
  const modelContextWindows = providerName === OPENAI_API_PROVIDER_ID
271
279
  ? mergePositiveNumberCaps(registryEntry.modelContextWindows, provider.modelContextWindows)
272
280
  : mergeRecordFill(registryEntry.modelContextWindows, provider.modelContextWindows);
@@ -342,7 +350,10 @@ export function routedProviderConfig(providerName: string, provider: OcxProvider
342
350
  ...(provider.project === undefined && registryEntry.project !== undefined ? { project: registryEntry.project } : {}),
343
351
  ...(provider.location === undefined && registryEntry.location !== undefined ? { location: registryEntry.location } : {}),
344
352
  ...(provider.contextWindow === undefined && registryEntry.contextWindow !== undefined ? { contextWindow: registryEntry.contextWindow } : {}),
345
- ...(provider.reasoningEfforts === undefined && registryEntry.reasoningEfforts !== undefined ? { reasoningEfforts: registryEntry.reasoningEfforts } : {}),
353
+ ...((provider.reasoningEfforts === undefined || repairLegacyClinePassReasoningEfforts)
354
+ && registryEntry.reasoningEfforts !== undefined
355
+ ? { reasoningEfforts: [...registryEntry.reasoningEfforts] }
356
+ : {}),
346
357
  ...(provider.escapeBuiltinToolNames === undefined && registryEntry.escapeBuiltinToolNames !== undefined ? { escapeBuiltinToolNames: registryEntry.escapeBuiltinToolNames } : {}),
347
358
  ...(provider.keyOptional === undefined && registryEntry.keyOptional !== undefined ? { keyOptional: registryEntry.keyOptional } : {}),
348
359
  ...(provider.modelSuffixBracketStrip === undefined && registryEntry.modelSuffixBracketStrip !== undefined ? { modelSuffixBracketStrip: registryEntry.modelSuffixBracketStrip } : {}),
@@ -53,6 +53,7 @@ import { loadLabAutomationPolicy } from "../lab/automation/persistence";
53
53
  import { createProductionLabRouteExecutor } from "../lib/lab-live-route-production";
54
54
  import { runOpenAiTierStartupMigration } from "../providers/openai-tier-startup";
55
55
  import { runAlibabaRegionStartupMigration } from "../providers/alibaba-region-startup";
56
+ import { runModelRenameStartupMigration } from "../providers/model-rename-startup";
56
57
  import { isCanonicalOpenAiForwardProvider, OPENAI_CODEX_PROVIDER_ID } from "../providers/openai-tiers";
57
58
  import { providerContextCap } from "../providers/context-cap";
58
59
  import { providerCodexAccountMode } from "../providers/registry";
@@ -485,12 +486,12 @@ export function warnAgentTaskRecoveryStartup(config: {
485
486
  if (config.agentTaskRecovery?.enabled !== true) return;
486
487
  console.warn("⚠️ Experimental encrypted V2 task recovery is enabled.");
487
488
  console.warn(" A scoped cache miss may send an additional authenticated request to ChatGPT and may consume quota or add latency; concurrent misses can share one request.");
488
- console.warn(" Recovered model output is retained only in a bounded in-memory cache; exact fidelity is not guaranteed and the path depends on undocumented backend behavior.");
489
+ console.warn(" Recovered plaintext assignment data is retained only in a bounded, process-local in-memory cache; exact fidelity is not guaranteed and the path depends on undocumented backend behavior.");
489
490
  }
490
491
 
491
492
  export function startServer(port?: number, deps: StartServerDeps = {}): Server<WsData> {
492
493
  const localAttestationSecret = deps.localAttestationSecret ?? createLocalAttestationSecret();
493
- const config = runAlibabaRegionStartupMigration(runOpenAiTierStartupMigration(loadConfig()));
494
+ const config = runModelRenameStartupMigration(runAlibabaRegionStartupMigration(runOpenAiTierStartupMigration(loadConfig())));
494
495
  warnAgentTaskRecoveryStartup(config);
495
496
  setLiveStateStoreConfig(config);
496
497
  applyProxyEnv(config);
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  currentUsageLogRevision,
3
3
  readUsageSnapshotForManagement,
4
- usageLogRevisionKey,
4
+ usageLogIdentityKey,
5
5
  type PersistedUsageEntry,
6
6
  } from "../../usage/log";
7
7
 
@@ -110,7 +110,7 @@ export function rollupApiKeyUsage(
110
110
  * create/rename/delete. The compact rollup is a handful of counters per key, so
111
111
  * caching it costs nothing; a new row changes the revision and invalidates it.
112
112
  */
113
- let rollupCache: { revisionKey: string; expiresAt: number; snapshot: ApiKeyUsageSnapshot } | null = null;
113
+ let rollupCache: { revisionKey: string; expiresAt: number; lastSeenSize?: number; snapshot: ApiKeyUsageSnapshot } | null = null;
114
114
 
115
115
  /**
116
116
  * The rollup is a function of the log AND of the clock: a request ages out of
@@ -136,6 +136,29 @@ export function clearApiKeyUsageCacheForTests(): void {
136
136
  * `attributionSince`. Key management working matters more than usage numbers
137
137
  * being present, and the GUI already treats an absent field as "no data".
138
138
  */
139
+ export function cacheApiKeyUsageFromSnapshot(
140
+ entries: PersistedUsageEntry[],
141
+ configuredIds: string[],
142
+ identityKey: string,
143
+ lastSeenSize: number,
144
+ truncated: boolean,
145
+ maxReadBytes: number | undefined,
146
+ now: number = Date.now(),
147
+ ): ApiKeyUsageSnapshot {
148
+ const idsKey = JSON.stringify([configuredIds, maxReadBytes]);
149
+ const rolled = {
150
+ ...rollupApiKeyUsage(entries, configuredIds, now),
151
+ ...(truncated ? { historyTruncated: true as const } : {}),
152
+ };
153
+ rollupCache = {
154
+ revisionKey: `${identityKey}|${idsKey}`,
155
+ expiresAt: now + ROLLUP_CACHE_TTL_MS,
156
+ lastSeenSize,
157
+ snapshot: rolled,
158
+ };
159
+ return rolled;
160
+ }
161
+
139
162
  export async function readApiKeyUsageRollup(configuredIds: string[], maxReadBytes?: number): Promise<ApiKeyUsageSnapshot> {
140
163
  // JSON rather than a joined string: ids are only validated as non-empty
141
164
  // strings, so `["a\0b","c"]` and `["a","b\0c"]` join to the same value and one
@@ -143,8 +166,10 @@ export async function readApiKeyUsageRollup(configuredIds: string[], maxReadByte
143
166
  const idsKey = JSON.stringify([configuredIds, maxReadBytes]);
144
167
  const now = Date.now();
145
168
  try {
146
- const observedKey = `${usageLogRevisionKey(currentUsageLogRevision())}|${idsKey}`;
147
- if (rollupCache?.revisionKey === observedKey && now < rollupCache.expiresAt) {
169
+ const observed = currentUsageLogRevision();
170
+ const observedKey = `${usageLogIdentityKey(observed)}|${idsKey}`;
171
+ const observedSize = observed?.size ?? 0;
172
+ if (rollupCache?.revisionKey === observedKey && now < rollupCache.expiresAt && observedSize >= (rollupCache.lastSeenSize ?? 0)) {
148
173
  return rollupCache.snapshot;
149
174
  }
150
175
 
@@ -154,8 +179,9 @@ export async function readApiKeyUsageRollup(configuredIds: string[], maxReadByte
154
179
  ...(snapshot.truncatedPrefixBytes > 0 || snapshot.entriesTruncated ? { historyTruncated: true as const } : {}),
155
180
  };
156
181
  rollupCache = {
157
- revisionKey: `${usageLogRevisionKey(snapshot.revision)}|${idsKey}`,
182
+ revisionKey: `${usageLogIdentityKey(snapshot.revision)}|${idsKey}`,
158
183
  expiresAt: now + ROLLUP_CACHE_TTL_MS,
184
+ lastSeenSize: snapshot.revision?.size ?? 0,
159
185
  snapshot: rolled,
160
186
  };
161
187
  return rolled;
@@ -54,7 +54,16 @@ import { stripCodexRuntimeProviderFields } from "../../codex/auth-context";
54
54
  import { getProviderRegistryEntry } from "../../providers/registry";
55
55
  import { VISION_REASONING_EFFORTS, isVisionReasoningEffort } from "../../reasoning-effort";
56
56
  import { normalizeVisionReasoningForModel } from "../../vision/reasoning";
57
- import { findAnthropicVisionProvider, resolveEffectiveVisionModel, resolveVisionBackend } from "../../vision";
57
+ import {
58
+ findAnthropicVisionProvider,
59
+ isValidVisionTimeoutMs,
60
+ MAX_VISION_TIMEOUT_MS,
61
+ MIN_VISION_TIMEOUT_MS,
62
+ resolveEffectiveVisionModel,
63
+ resolveMaxDescriptionsPerTurn,
64
+ resolveVisionBackend,
65
+ resolveVisionTimeoutMs,
66
+ } from "../../vision";
58
67
  import {
59
68
  visionCandidateRows,
60
69
  visionDescriberIsProvablyBlind,
@@ -108,6 +117,21 @@ async function sidecarVisionResponseSettings(config: OcxConfig): Promise<{
108
117
  return { model, reasoning, models };
109
118
  }
110
119
 
120
+ function publicVisionSidecarSettings(
121
+ config: OcxConfig,
122
+ vision: Awaited<ReturnType<typeof sidecarVisionResponseSettings>>,
123
+ ) {
124
+ const vs = config.visionSidecar ?? {};
125
+ return {
126
+ enabled: vs.enabled !== false,
127
+ model: vision.model,
128
+ backend: vs.backend,
129
+ reasoning: vision.reasoning,
130
+ maxDescriptionsPerTurn: resolveMaxDescriptionsPerTurn(vs.maxDescriptionsPerTurn),
131
+ timeoutMs: resolveVisionTimeoutMs(vs.timeoutMs),
132
+ };
133
+ }
134
+
111
135
  export async function handleConfigRoutes(ctx: ManagementContext): Promise<Response | null> {
112
136
  const { req, url, config, deps, convergeCodexCatalog, syncClaudeAgentDefsBestEffort } = ctx;
113
137
  if (url.pathname === "/api/config" && req.method === "GET") {
@@ -407,7 +431,6 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
407
431
 
408
432
  if (url.pathname === "/api/sidecar-settings" && req.method === "GET") {
409
433
  const ws = config.webSearchSidecar ?? {};
410
- const vs = config.visionSidecar ?? {};
411
434
  const vision = await sidecarVisionResponseSettings(config);
412
435
  return jsonResponse({
413
436
  webSearch: {
@@ -415,12 +438,7 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
415
438
  backend: ws.backend,
416
439
  streamRoutedModelOutput: ws.streamRoutedModelOutput === true,
417
440
  },
418
- vision: {
419
- model: vision.model,
420
- backend: vs.backend,
421
- reasoning: vision.reasoning,
422
- maxDescriptionsPerTurn: vs.maxDescriptionsPerTurn,
423
- },
441
+ vision: publicVisionSidecarSettings(config, vision),
424
442
  visionModels: vision.models,
425
443
  });
426
444
  }
@@ -435,7 +453,14 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
435
453
  if (raw.vision !== undefined && !isPlainRecord(raw.vision)) return jsonResponse({ error: "vision must be an object" }, 400);
436
454
  const body = raw as {
437
455
  webSearch?: { model?: unknown; backend?: unknown; reasoning?: unknown; streamRoutedModelOutput?: unknown };
438
- vision?: { model?: unknown; backend?: unknown; reasoning?: unknown; maxDescriptionsPerTurn?: unknown };
456
+ vision?: {
457
+ model?: unknown;
458
+ backend?: unknown;
459
+ reasoning?: unknown;
460
+ maxDescriptionsPerTurn?: unknown;
461
+ enabled?: unknown;
462
+ timeoutMs?: unknown;
463
+ };
439
464
  };
440
465
  if (body.webSearch && body.webSearch.backend !== undefined && body.webSearch.backend !== null
441
466
  && body.webSearch.backend !== "openai" && body.webSearch.backend !== "anthropic") {
@@ -455,6 +480,14 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
455
480
  || body.vision.maxDescriptionsPerTurn <= 0)) {
456
481
  return jsonResponse({ error: "vision.maxDescriptionsPerTurn must be a positive integer" }, 400);
457
482
  }
483
+ if (body.vision && body.vision.enabled !== undefined && typeof body.vision.enabled !== "boolean") {
484
+ return jsonResponse({ error: "vision.enabled must be a boolean" }, 400);
485
+ }
486
+ if (body.vision && body.vision.timeoutMs !== undefined && !isValidVisionTimeoutMs(body.vision.timeoutMs)) {
487
+ return jsonResponse({
488
+ error: `vision.timeoutMs must be an integer from ${MIN_VISION_TIMEOUT_MS} to ${MAX_VISION_TIMEOUT_MS}`,
489
+ }, 400);
490
+ }
458
491
  if (body.vision?.reasoning !== undefined && !isVisionReasoningEffort(body.vision.reasoning)) {
459
492
  return jsonResponse({ error: `vision.reasoning must be ${VISION_REASONING_EFFORTS.join(", ")}` }, 400);
460
493
  }
@@ -517,6 +550,14 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
517
550
  if (typeof body.vision.maxDescriptionsPerTurn === "number") {
518
551
  config.visionSidecar.maxDescriptionsPerTurn = body.vision.maxDescriptionsPerTurn;
519
552
  }
553
+ if (typeof body.vision.enabled === "boolean") {
554
+ // `true` is the default — drop the key so disable/re-enable does not rewrite the file.
555
+ if (body.vision.enabled) delete config.visionSidecar.enabled;
556
+ else config.visionSidecar.enabled = false;
557
+ }
558
+ if (typeof body.vision.timeoutMs === "number") {
559
+ config.visionSidecar.timeoutMs = body.vision.timeoutMs;
560
+ }
520
561
  if (visionReasoningTouched) {
521
562
  if (normalizedVisionReasoning === undefined) delete config.visionSidecar.reasoning;
522
563
  else config.visionSidecar.reasoning = normalizedVisionReasoning;
@@ -524,7 +565,6 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
524
565
  }
525
566
  saveConfigPreservingClaudeCode(config);
526
567
  const ws = config.webSearchSidecar ?? {};
527
- const vs = config.visionSidecar ?? {};
528
568
  const vision = await sidecarVisionResponseSettings(config);
529
569
  return jsonResponse({
530
570
  ok: true,
@@ -533,12 +573,7 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
533
573
  backend: ws.backend,
534
574
  streamRoutedModelOutput: ws.streamRoutedModelOutput === true,
535
575
  },
536
- vision: {
537
- model: vision.model,
538
- backend: vs.backend,
539
- reasoning: vision.reasoning,
540
- maxDescriptionsPerTurn: vs.maxDescriptionsPerTurn,
541
- },
576
+ vision: publicVisionSidecarSettings(config, vision),
542
577
  visionModels: vision.models,
543
578
  });
544
579
  }