@bitkyc08/opencodex 2.14.1 → 2.15.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.
Files changed (49) hide show
  1. package/gui/dist/assets/index-B5T5ADgY.js +76 -0
  2. package/gui/dist/assets/{index-DWhX3yMp.css → index-DUCH59lJ.css} +1 -1
  3. package/gui/dist/index.html +2 -2
  4. package/package.json +1 -1
  5. package/src/adapters/command-code.ts +15 -4
  6. package/src/adapters/cursor/effort-map.ts +4 -5
  7. package/src/adapters/cursor/request-builder.ts +55 -11
  8. package/src/adapters/cursor/tool-definitions.ts +24 -0
  9. package/src/adapters/kiro.ts +10 -1
  10. package/src/adapters/openai-chat.ts +5 -3
  11. package/src/adapters/openai-responses.ts +109 -0
  12. package/src/adapters/tool-catalog-nudge.ts +26 -4
  13. package/src/bridge.ts +50 -3
  14. package/src/cli/init.ts +4 -17
  15. package/src/codex/catalog/effort.ts +2 -1
  16. package/src/codex/catalog/metadata.ts +62 -12
  17. package/src/codex/catalog/native-models.ts +27 -0
  18. package/src/codex/catalog/parsing.ts +17 -2
  19. package/src/codex/catalog/provider-fetch.ts +47 -5
  20. package/src/codex/catalog/sync.ts +21 -7
  21. package/src/codex/catalog.ts +1 -1
  22. package/src/config.ts +79 -4
  23. package/src/generated/compatibility-version.json +54 -42
  24. package/src/generated/model-metadata.ts +1 -1
  25. package/src/lib/app-owned-memory-stores.ts +22 -0
  26. package/src/lib/tool-argument-integers.ts +158 -0
  27. package/src/oauth/index.ts +3 -0
  28. package/src/oauth/nous.ts +58 -9
  29. package/src/providers/antigravity-models.ts +93 -28
  30. package/src/providers/base-url-choices.ts +10 -0
  31. package/src/providers/command-code-efforts.ts +18 -0
  32. package/src/providers/model-rename-migration.ts +255 -0
  33. package/src/providers/model-rename-startup.ts +28 -0
  34. package/src/providers/openai-tier-startup.ts +31 -2
  35. package/src/providers/quota.ts +9 -2
  36. package/src/providers/registry.ts +13 -6
  37. package/src/responses/spill-store.ts +5 -1
  38. package/src/responses/state.ts +50 -2
  39. package/src/server/index.ts +2 -1
  40. package/src/server/management/api-key-usage.ts +31 -5
  41. package/src/server/management/logs-usage-routes.ts +48 -10
  42. package/src/server/management/provider-routes.ts +2 -1
  43. package/src/server/management/usage-summary-cache.ts +7 -1
  44. package/src/server/responses/collaboration.ts +12 -2
  45. package/src/server/responses/core.ts +33 -16
  46. package/src/server/startup-health-cache.ts +12 -0
  47. package/src/usage/expected-prices.ts +13 -0
  48. package/src/usage/log.ts +430 -12
  49. package/gui/dist/assets/index-DuaUVm_d.js +0 -76
@@ -102,18 +102,28 @@ import type { TranslatorBudget } from "../../lib/translator-budget";
102
102
 
103
103
  export function buildToolBridgeMaps(parsed: OcxParsedRequest, budget?: TranslatorBudget): {
104
104
  toolNsMap: Map<string, { namespace: string; name: string }>;
105
+ declaredToolNames: Set<string>;
106
+ /** Declared parameter schema per request-visible tool name (#1611 integer repair). */
107
+ toolParameterSchemas: Map<string, Record<string, unknown>>;
105
108
  freeformToolNames: Set<string>;
106
109
  toolSearchToolNames: Set<string>;
107
110
  } {
108
111
  const toolNsMap = new Map<string, { namespace: string; name: string }>();
112
+ const declaredToolNames = new Set<string>();
113
+ const toolParameterSchemas = new Map<string, Record<string, unknown>>();
109
114
  const freeformToolNames = new Set<string>();
110
115
  const toolSearchToolNames = new Set<string>();
111
116
  const toolAllowed = toolChoiceToolPredicate(parsed.options.toolChoice);
112
117
  for (const t of parsed.context.tools ?? []) {
113
118
  // Upstream output is untrusted: only restore calls for tools the caller authorized.
114
119
  if (!toolAllowed(t)) continue;
120
+ const wireName = namespacedToolName(t.namespace, t.name);
121
+ budget?.chargeRetained(new TextEncoder().encode(wireName).byteLength, { kind: "retained_collectors" });
122
+ declaredToolNames.add(wireName);
123
+ // Retained by reference (the schema is already resident in parsed.context.tools),
124
+ // so this adds a map entry rather than a copy of every tool's parameters.
125
+ if (t.parameters && typeof t.parameters === "object") toolParameterSchemas.set(wireName, t.parameters);
115
126
  if (t.namespace) {
116
- const wireName = namespacedToolName(t.namespace, t.name);
117
127
  budget?.chargeRetained(new TextEncoder().encode(JSON.stringify([wireName, t.namespace, t.name])).byteLength, { kind: "retained_collectors" });
118
128
  toolNsMap.set(wireName, { namespace: t.namespace, name: t.name });
119
129
  }
@@ -126,7 +136,7 @@ export function buildToolBridgeMaps(parsed: OcxParsedRequest, budget?: Translato
126
136
  toolSearchToolNames.add(t.name);
127
137
  }
128
138
  }
129
- return { toolNsMap, freeformToolNames, toolSearchToolNames };
139
+ return { toolNsMap, declaredToolNames, toolParameterSchemas, freeformToolNames, toolSearchToolNames };
130
140
  }
131
141
 
132
142
 
@@ -22,6 +22,7 @@ import {
22
22
  markBodyNonPersistable,
23
23
  previousResponseProviderState,
24
24
  previousResponseReplayFailure,
25
+ previousResponseScopeMismatch,
25
26
  rememberResponseState,
26
27
  } from "../../responses/state";
27
28
  import {
@@ -1503,8 +1504,12 @@ async function handleResponsesInner(
1503
1504
  let unreadableEncryptedAgentTask = hasUnreadableEncryptedAgentTask(
1504
1505
  (body as { input?: unknown } | undefined)?.input,
1505
1506
  );
1507
+ const inboundClientThreadId = req.headers.get("x-codex-parent-thread-id")?.trim() || undefined;
1506
1508
  const originalBody = body;
1507
- body = expandPreviousResponseInput(body);
1509
+ body = expandPreviousResponseInput(body, inboundClientThreadId);
1510
+ if (previousResponseScopeMismatch(body)) {
1511
+ console.warn("[opencodex] dropped a previous_response_id with a mismatched client task scope; continuing fresh");
1512
+ }
1508
1513
  if (previousResponseReplayFailure(body)) {
1509
1514
  return formatErrorResponse(
1510
1515
  400,
@@ -1512,7 +1517,8 @@ async function handleResponsesInner(
1512
1517
  "Continuation state is unavailable or corrupt; resend the full conversation without previous_response_id.",
1513
1518
  );
1514
1519
  }
1515
- const previousResponseInputExpanded = body !== originalBody;
1520
+ const previousResponseInputExpanded = body !== originalBody
1521
+ && typeof (body as { previous_response_id?: unknown }).previous_response_id === "string";
1516
1522
 
1517
1523
  // Spawn-message compatibility (both directions): agent_message task payloads ride in
1518
1524
  // encrypted_content slots as plaintext. Rewrite them to input_text on the RAW body BEFORE
@@ -1529,7 +1535,7 @@ async function handleResponsesInner(
1529
1535
  );
1530
1536
  }
1531
1537
 
1532
- let parsed;
1538
+ let parsed: OcxParsedRequest;
1533
1539
  let toolBridgeMaps: ReturnType<typeof buildToolBridgeMaps>;
1534
1540
  try {
1535
1541
  parsed = parseRequest(body);
@@ -1537,10 +1543,9 @@ async function handleResponsesInner(
1537
1543
  if (previousResponseInputExpanded) parsed._previousResponseInputExpanded = true;
1538
1544
  parsed._providerContinuation = previousResponseProviderState(parsed.previousResponseId);
1539
1545
  parsed._cursorConversationId = parsed._providerContinuation?.cursor?.conversationId;
1540
- const clientThreadId = req.headers.get("x-codex-parent-thread-id")?.trim();
1541
- if (clientThreadId) {
1542
- parsed._clientThreadId = clientThreadId;
1543
- parsed._reasoningReplayScope = { clientThreadId };
1546
+ if (inboundClientThreadId) {
1547
+ parsed._clientThreadId = inboundClientThreadId;
1548
+ parsed._reasoningReplayScope = { clientThreadId: inboundClientThreadId };
1544
1549
  }
1545
1550
  } catch (err) {
1546
1551
  if (isTranslatorBudgetExceededError(err)) {
@@ -1550,6 +1555,10 @@ async function handleResponsesInner(
1550
1555
  }
1551
1556
  return formatErrorResponse(400, "invalid_request_error", err instanceof Error ? err.message : String(err));
1552
1557
  }
1558
+ const responseStateOptions = (force = false): { force?: boolean; clientThreadId?: string } => ({
1559
+ ...(force ? { force: true } : {}),
1560
+ ...(parsed._clientThreadId ? { clientThreadId: parsed._clientThreadId } : {}),
1561
+ });
1553
1562
  // Prefer a pre-populated id (routed Claude) over Responses headers that may be
1554
1563
  // absent or synthetically injected (session_id from prompt_cache_key).
1555
1564
  if (!logCtx.conversationId) {
@@ -2137,7 +2146,7 @@ async function handleResponsesInner(
2137
2146
  && (!parsed.previousResponseId || parsed._previousResponseInputExpanded === true);
2138
2147
  const rememberPassthroughResponse = passthroughRecordEligible
2139
2148
  ? (response: { id?: unknown; output?: unknown; status?: unknown }) =>
2140
- rememberResponseState(parsed._rawBody, response, undefined, { force: true })
2149
+ rememberResponseState(parsed._rawBody, response, undefined, responseStateOptions(true))
2141
2150
  : undefined;
2142
2151
  if (parsed.previousResponseId && !parsed._previousResponseInputExpanded) {
2143
2152
  console.warn(
@@ -2962,7 +2971,7 @@ async function handleResponsesInner(
2962
2971
  parsed._rawBody,
2963
2972
  response,
2964
2973
  continuationStateForResponse(providerState),
2965
- adapterNeedsForcedContinuation(adapter.name) ? { force: true } : undefined,
2974
+ responseStateOptions(adapterNeedsForcedContinuation(adapter.name)),
2966
2975
  ),
2967
2976
  });
2968
2977
  if (imgResponse.body) {
@@ -3075,7 +3084,7 @@ async function handleResponsesInner(
3075
3084
  }
3076
3085
  };
3077
3086
 
3078
- const { toolNsMap, freeformToolNames, toolSearchToolNames } = toolBridgeMaps;
3087
+ const { toolNsMap, declaredToolNames, toolParameterSchemas, freeformToolNames, toolSearchToolNames } = toolBridgeMaps;
3079
3088
  if (parsed.stream) {
3080
3089
  void runTurn();
3081
3090
  let eventSource: AsyncIterable<AdapterEvent> = queue.stream();
@@ -3101,6 +3110,8 @@ async function handleResponsesInner(
3101
3110
  ...(options.forceEmptyResponseId ? { responseId: "" } : {}),
3102
3111
  stallTimeoutSec: config.stallTimeoutSec,
3103
3112
  hideThinkingSummary: parsed.options.hideThinkingSummary,
3113
+ declaredToolNames,
3114
+ toolParameterSchemas,
3104
3115
  ...(options.onFirstOutput ? { onFirstOutput: options.onFirstOutput } : {}),
3105
3116
  ...(routedCompaction ? { compaction: true } : {}),
3106
3117
  onUsage: usage => {
@@ -3118,7 +3129,7 @@ async function handleResponsesInner(
3118
3129
  parsed._rawBody,
3119
3130
  response,
3120
3131
  continuationStateForResponse(providerState),
3121
- adapterNeedsForcedContinuation(adapter.name) ? { force: true } : undefined,
3132
+ responseStateOptions(adapterNeedsForcedContinuation(adapter.name)),
3122
3133
  ),
3123
3134
  }),
3124
3135
  },
@@ -3147,6 +3158,8 @@ async function handleResponsesInner(
3147
3158
  replayCacheScope: parsed._reasoningReplayScope,
3148
3159
  hideThinkingSummary: parsed.options.hideThinkingSummary,
3149
3160
  toolNsMap,
3161
+ declaredToolNames,
3162
+ toolParameterSchemas,
3150
3163
  freeformToolNames,
3151
3164
  toolSearchToolNames,
3152
3165
  ...(routedCompaction ? { compaction: true } : {}),
@@ -3164,7 +3177,7 @@ async function handleResponsesInner(
3164
3177
  parsed._rawBody,
3165
3178
  json,
3166
3179
  continuationStateForResponse(providerState),
3167
- adapterNeedsForcedContinuation(adapter.name) ? { force: true } : undefined,
3180
+ responseStateOptions(adapterNeedsForcedContinuation(adapter.name)),
3168
3181
  );
3169
3182
  }
3170
3183
  return new Response(JSON.stringify(json), { headers: { "Content-Type": "application/json" } });
@@ -3835,7 +3848,7 @@ async function handleResponsesInner(
3835
3848
  continuation: fetchTerminalGuardContinuation,
3836
3849
  })
3837
3850
  : initialEventStream;
3838
- const { toolNsMap, freeformToolNames, toolSearchToolNames } = toolBridgeMaps;
3851
+ const { toolNsMap, declaredToolNames, toolParameterSchemas, freeformToolNames, toolSearchToolNames } = toolBridgeMaps;
3839
3852
  const sseStream = bridgeToResponsesSSE(
3840
3853
  eventStream, parsed._responseModelId ?? parsed.modelId, toolNsMap, freeformToolNames, toolSearchToolNames,
3841
3854
  () => upstream.abort(), 2_000,
@@ -3845,6 +3858,8 @@ async function handleResponsesInner(
3845
3858
  ...(options.forceEmptyResponseId ? { responseId: "" } : {}),
3846
3859
  stallTimeoutSec: config.stallTimeoutSec,
3847
3860
  hideThinkingSummary: parsed.options.hideThinkingSummary,
3861
+ declaredToolNames,
3862
+ toolParameterSchemas,
3848
3863
  ...(options.onFirstOutput ? { onFirstOutput: options.onFirstOutput } : {}),
3849
3864
  ...(routedCompaction ? { compaction: true } : {}),
3850
3865
  onUsage: usage => {
@@ -3864,7 +3879,7 @@ async function handleResponsesInner(
3864
3879
  parsed._rawBody,
3865
3880
  response,
3866
3881
  continuationStateForResponse(providerState),
3867
- activeAdapter.name === "kiro" ? { force: true } : undefined,
3882
+ responseStateOptions(activeAdapter.name === "kiro"),
3868
3883
  ),
3869
3884
  }),
3870
3885
  },
@@ -3895,13 +3910,15 @@ async function handleResponsesInner(
3895
3910
  } finally {
3896
3911
  cleanupUpstreamAbort();
3897
3912
  }
3898
- const { toolNsMap, freeformToolNames, toolSearchToolNames } = toolBridgeMaps;
3913
+ const { toolNsMap, declaredToolNames, toolParameterSchemas, freeformToolNames, toolSearchToolNames } = toolBridgeMaps;
3899
3914
  let providerState: OcxProviderContinuationState | undefined;
3900
3915
  const json = buildResponseJSON(events, parsed._responseModelId ?? parsed.modelId, {
3901
3916
  translatorBudget,
3902
3917
  replayCacheScope: parsed._reasoningReplayScope,
3903
3918
  hideThinkingSummary: parsed.options.hideThinkingSummary,
3904
3919
  toolNsMap,
3920
+ declaredToolNames,
3921
+ toolParameterSchemas,
3905
3922
  freeformToolNames,
3906
3923
  toolSearchToolNames,
3907
3924
  ...(routedCompaction ? { compaction: true } : {}),
@@ -3920,7 +3937,7 @@ async function handleResponsesInner(
3920
3937
  parsed._rawBody,
3921
3938
  json,
3922
3939
  continuationStateForResponse(providerState),
3923
- activeAdapter.name === "kiro" ? { force: true } : undefined,
3940
+ responseStateOptions(activeAdapter.name === "kiro"),
3924
3941
  );
3925
3942
  }
3926
3943
  return new Response(JSON.stringify(json), { headers: { "Content-Type": "application/json" } });
@@ -10,6 +10,7 @@ import { truncateRetainedUtf8 } from "../lib/admission";
10
10
 
11
11
  const CACHE_TTL_MS = 30_000;
12
12
  const PROBE_TIMEOUT_MS = 5_000;
13
+ const INITIAL_PROBE_WAIT_MS = 5_500;
13
14
  const MAX_DIAGNOSTIC_VALUE_BYTES = 8 * 1024;
14
15
  let cached: { timestamp: number; value: StartupHealth } | null = null;
15
16
  let inflight: Promise<StartupHealth> | null = null;
@@ -109,6 +110,17 @@ function refreshInBackground(config: Pick<OcxConfig, "codexAutoStart">): void {
109
110
  export async function getCachedStartupHealth(config: Pick<OcxConfig, "codexAutoStart">): Promise<StartupHealth> {
110
111
  if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) return cached.value;
111
112
  refreshInBackground(config);
113
+ // An expired or empty read is an explicit protection check. Wait for the
114
+ // isolated probe instead of presenting a synthetic failure while that probe
115
+ // is still running. The probe remains child-process isolated and hard-capped
116
+ // at 5s; stale state is returned only if that bounded probe cannot settle.
117
+ if (inflight) {
118
+ const settled = await Promise.race([
119
+ inflight,
120
+ new Promise<null>(resolve => setTimeout(() => resolve(null), INITIAL_PROBE_WAIT_MS)),
121
+ ]);
122
+ if (settled) return settled;
123
+ }
112
124
  return cached ? markStartupHealthDiagnosticStale(cached.value) : conservativeFallback(config);
113
125
  }
114
126
 
@@ -52,6 +52,10 @@ const DAYBREAK_RED: Cost4 = { input: 12.5, output: 75, cacheRead: 1.25, cacheWri
52
52
  const GPT56_TERRA: Cost4 = { input: 2, output: 12, cacheRead: 0.2, cacheWrite: 2.5 };
53
53
  const GPT56_LUNA: Cost4 = { input: 0.2, output: 1.2, cacheRead: 0.02, cacheWrite: 0.25 };
54
54
  const GEMINI_36_FLASH: Cost4 = { input: 1.5, output: 7.5, cacheRead: 0.15, cacheWrite: 0 };
55
+ // Gemini 3.7 Flash launch promotion: Google publishes $0.75 in / $3.75 out per 1M
56
+ // through 2026-12-31, stepping up to $1.50 / $7.50 on 2027-01-01. Revisit this row
57
+ // then — the promotional rate is dated on the pricing page, not open-ended.
58
+ const GEMINI_37_FLASH: Cost4 = { input: 0.75, output: 3.75, cacheRead: 0.075, cacheWrite: 0 };
55
59
  const MINIMAX_M21_HIGHSPEED: Cost4 = { input: 0.6, output: 2.4, cacheRead: 0.03, cacheWrite: 0.375 };
56
60
  const KIMI_K3: Cost4 = { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3 };
57
61
  const KIMI_K27_CODE: Cost4 = { input: 0.95, output: 4, cacheRead: 0.19, cacheWrite: 0.95 };
@@ -70,6 +74,7 @@ const CLAUDE_OPUS_5_DERIVED_SOURCE =
70
74
  const ANTHROPIC_PRICING = "https://platform.claude.com/docs/en/about-claude/pricing (official; 5m cache-write tier)";
71
75
 
72
76
  const GEMINI_PRICING = "https://ai.google.dev/gemini-api/docs/pricing (2026-07-22); cacheWrite=0: storage is billed per-hour, not per-token";
77
+ const GEMINI_37_PRICING = "https://ai.google.dev/gemini-api/docs/pricing (2026-08-14); promotional rate through 2026-12-31, rises to 1.50/7.50 on 2027-01-01; cacheWrite=0: storage is billed per-hour, not per-token";
73
78
  const MINIMAX_PRICING = "https://platform.minimax.io/docs/guides/pricing-paygo";
74
79
  const OPENAI_GPT56_PRICING = "https://developers.openai.com/api/docs/pricing";
75
80
  const DEEPSEEK_PRICING = "https://api-docs.deepseek.com/quick_start/pricing-details-usd; V4 Flash alias transition scheduled 2026-07-24 — re-verify after";
@@ -102,6 +107,12 @@ export const EXPECTED_PRICE_OVERLAYS: readonly ExpectedPriceOverlay[] = [
102
107
  // Google Antigravity effort-suffix variants — derived from the verified base-model
103
108
  // price (Google does not publish per-suffix prices; Agent inference bills at the
104
109
  // base model's standard rate per the official Billing FAQ).
110
+ // 3.7 Flash rides CCA, whose billing equivalence to the Developer API list price is
111
+ // not published, so this is `verified-derived` rather than `verified`: the number is
112
+ // proven, the claim that Antigravity charges it is inferred.
113
+ { provider: "google-antigravity", modelId: "gemini-3.7-flash", cost4: GEMINI_37_FLASH, source: `derived: Gemini 3.7 Flash promotional rate through 2026-12-31 ${GEMINI_37_PRICING}`, verifiedAt: "2026-08-14", status: "verified-derived" },
114
+ // Retained after the 3.6 retirement: historical usage.jsonl rows still carry these
115
+ // ids, and dropping the row would silently zero the cost of requests already made.
105
116
  { provider: "google-antigravity", modelId: "gemini-3.6-flash", cost4: GEMINI_36_FLASH, source: `collapsed base ID ${GEMINI_PRICING}`, verifiedAt: "2026-07-22", status: "verified" },
106
117
  { provider: "google-antigravity", modelId: "gemini-3.1-pro", cost4: GEMINI_31_PRO, source: `collapsed base ID ${GEMINI_PRICING}`, verifiedAt: "2026-07-22", status: "verified" },
107
118
  // OpenAI GPT-5.6 `-pro` virtual selections. The virtual resolver keeps the SELECTED id in
@@ -130,6 +141,8 @@ export const EXPECTED_PRICE_OVERLAYS: readonly ExpectedPriceOverlay[] = [
130
141
  { provider: "google-antigravity", modelId: "gemini-3-flash-agent", cost4: GEMINI_36_FLASH, source: `compat alias -> gemini-3.6-flash-high ${GEMINI_PRICING}`, verifiedAt: "2026-07-22", status: "verified-derived" },
131
142
  // Direct Google Gemini API current model (verified — published table).
132
143
  { provider: "google", modelId: "gemini-3.6-flash", cost4: GEMINI_36_FLASH, source: GEMINI_PRICING, verifiedAt: "2026-07-22", status: "verified" },
144
+ // Developer API row: the price IS published for this surface, so `verified`.
145
+ { provider: "google", modelId: "gemini-3.7-flash", cost4: GEMINI_37_FLASH, source: GEMINI_37_PRICING, verifiedAt: "2026-08-14", status: "verified" },
133
146
  { provider: "google-antigravity", modelId: "gemini-3.1-pro-preview", cost4: GEMINI_31_PRO, source: GEMINI_PRICING, verifiedAt: "2026-07-20", status: "verified" },
134
147
  // Antigravity-bundled third-party models — derived from the underlying vendor's
135
148
  // official API price (Antigravity itself bills via subscription quota).