@bitkyc08/opencodex 2.28.0 → 2.29.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 (90) hide show
  1. package/README.md +9 -1
  2. package/gui/dist/assets/index-BNESwCzn.js +102 -0
  3. package/gui/dist/assets/index-CH7ncHCC.css +1 -0
  4. package/gui/dist/index.html +2 -2
  5. package/package.json +1 -1
  6. package/src/adapters/base.ts +3 -1
  7. package/src/adapters/cursor/checkpoint-store.ts +303 -0
  8. package/src/adapters/cursor/discovery.ts +25 -0
  9. package/src/adapters/cursor/live-transport.ts +19 -2
  10. package/src/adapters/cursor/native-exec.ts +47 -2
  11. package/src/adapters/cursor/protobuf-request.ts +80 -23
  12. package/src/adapters/cursor/request-builder.ts +119 -5
  13. package/src/adapters/cursor/transport.ts +5 -0
  14. package/src/adapters/cursor/types.ts +13 -0
  15. package/src/adapters/cursor.ts +109 -3
  16. package/src/adapters/google-antigravity-replay.ts +31 -4
  17. package/src/adapters/google.ts +323 -38
  18. package/src/adapters/openai-chat.ts +19 -0
  19. package/src/adapters/openai-responses.ts +218 -37
  20. package/src/bridge.ts +38 -20
  21. package/src/claude/desktop-3p.ts +15 -6
  22. package/src/cli/agent.ts +44 -1
  23. package/src/cli/claude-agent-startup-sync.ts +73 -0
  24. package/src/cli/dispatch.ts +24 -1
  25. package/src/cli/ensure-desired-integrations.ts +152 -0
  26. package/src/cli/help.ts +1 -1
  27. package/src/cli/index.ts +40 -38
  28. package/src/cli/integrations.ts +1 -1
  29. package/src/cli/registry.ts +2 -2
  30. package/src/clients/config-export.ts +119 -20
  31. package/src/codex/affinity-debug.ts +162 -0
  32. package/src/codex/inject.ts +46 -14
  33. package/src/codex/journal.ts +22 -8
  34. package/src/config.ts +1 -0
  35. package/src/generated/compatibility-version.json +134 -66
  36. package/src/integrations/mutation-flight.ts +71 -0
  37. package/src/integrations/owned-refresh.ts +74 -0
  38. package/src/integrations/registry.ts +25 -2
  39. package/src/integrations/writer.ts +32 -1
  40. package/src/lab/public/signature.ts +25 -1
  41. package/src/lab/subject/behavior-fingerprint.ts +1 -1
  42. package/src/lib/redact.ts +2 -2
  43. package/src/oauth/log.ts +3 -1
  44. package/src/providers/derive.ts +9 -0
  45. package/src/providers/fastwire.ts +24 -18
  46. package/src/providers/openai-tiers.ts +60 -1
  47. package/src/providers/registry.ts +14 -7
  48. package/src/providers/xai-responses-opt-in.ts +15 -0
  49. package/src/responses/compaction.ts +18 -0
  50. package/src/responses/custom-tool-compat.ts +70 -5
  51. package/src/responses/namespace-tool-compat.ts +356 -0
  52. package/src/responses/parser.ts +2 -2
  53. package/src/responses/provider-continuation.ts +98 -0
  54. package/src/responses/reasoning-replay-cache.ts +125 -7
  55. package/src/responses/spill-store.ts +6 -1
  56. package/src/responses/state.ts +11 -0
  57. package/src/router.ts +14 -0
  58. package/src/routing/compatibility/behavior.ts +1 -0
  59. package/src/server/auth-cors.ts +4 -0
  60. package/src/server/management/agent-settings-routes.ts +57 -9
  61. package/src/server/management/config-routes.ts +134 -15
  62. package/src/server/management/integration-routes.ts +8 -55
  63. package/src/server/management/model-routes.ts +23 -1
  64. package/src/server/management/provider-routes.ts +22 -0
  65. package/src/server/management/vision-sidecar-options.ts +18 -11
  66. package/src/server/management/web-search-sidecar-options.ts +120 -0
  67. package/src/server/responses/core.ts +588 -78
  68. package/src/server/responses/responses-field-backfill.ts +7 -11
  69. package/src/server/responses/terminal-guard.ts +22 -11
  70. package/src/server/responses-custom-tool-repair.ts +3 -1
  71. package/src/server/responses-reasoning-summary-rewrite.ts +7 -0
  72. package/src/server/responses-tool-search-repair.ts +64 -14
  73. package/src/sidecar/auth.ts +92 -0
  74. package/src/sidecar/candidates.ts +83 -0
  75. package/src/types/config.ts +26 -5
  76. package/src/types/provider.ts +18 -0
  77. package/src/types/request.ts +26 -0
  78. package/src/types.ts +1 -0
  79. package/src/usage/log.ts +2 -0
  80. package/src/vision/index.ts +8 -9
  81. package/src/web-search/backends.ts +108 -0
  82. package/src/web-search/exa-executor.ts +88 -0
  83. package/src/web-search/gemini-executor.ts +141 -0
  84. package/src/web-search/index.ts +139 -12
  85. package/src/web-search/loop.ts +45 -5
  86. package/src/web-search/parse.ts +34 -23
  87. package/src/web-search/sources.ts +60 -0
  88. package/src/web-search/xai-executor.ts +219 -0
  89. package/gui/dist/assets/index-D2sP-biU.js +0 -102
  90. package/gui/dist/assets/index-DQsMZzI5.css +0 -1
@@ -27,6 +27,12 @@ import {
27
27
  type WriteRefused,
28
28
  } from "../../integrations/writer";
29
29
  import { IntegrationWriterLockBusyError, type IntegrationWriterLockSeams } from "../../integrations/writer-lock";
30
+ import {
31
+ INTEGRATION_MUTATION_TERMINAL_MS,
32
+ IntegrationMutationBusyError,
33
+ runIntegrationMutationFlight,
34
+ setIntegrationMutationFlightTestHook,
35
+ } from "../../integrations/mutation-flight";
30
36
  import { jsonResponse } from "../auth-cors";
31
37
  import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
32
38
  import type { ManagementContext } from "./context";
@@ -34,8 +40,7 @@ import { loadExportModels } from "./model-rows";
34
40
 
35
41
 
36
42
  const INTEGRATION_ROUTE_PREFIX = "/api/client-integrations/";
37
- const INTEGRATION_MUTATION_JOIN_MS = 120_000;
38
- export const INTEGRATION_MUTATION_TERMINAL_MS = 10 * 60_000;
43
+ export { INTEGRATION_MUTATION_TERMINAL_MS };
39
44
 
40
45
  type IntegrationStateRecord = Awaited<ReturnType<typeof readIntegrationState>>;
41
46
  type ApplyResult = Awaited<ReturnType<typeof applyIntegrationCoordinated>>;
@@ -81,19 +86,6 @@ export interface IntegrationRestoreBody {
81
86
  confirmDrift?: boolean;
82
87
  }
83
88
 
84
- interface IntegrationMutationFlight {
85
- key: string;
86
- startedAt: number;
87
- promise: Promise<unknown>;
88
- }
89
-
90
- class IntegrationMutationBusyError extends Error {
91
- constructor(readonly clientId: IntegrationClientId) {
92
- super("integration_mutation_busy");
93
- }
94
- }
95
-
96
- const integrationMutationFlights = new Map<IntegrationClientId, IntegrationMutationFlight>();
97
89
  let integrationMutationTestHooks: {
98
90
  io?: IntegrationIO;
99
91
  lockSeams?: IntegrationWriterLockSeams;
@@ -145,45 +137,6 @@ function decodeClientPath(pathname: string): string | null {
145
137
  }
146
138
  }
147
139
 
148
- function runIntegrationMutationFlight<T>(
149
- clientId: IntegrationClientId,
150
- key: string,
151
- now: () => number,
152
- operation: () => Promise<T>,
153
- ): Promise<T> {
154
- const startedAt = now();
155
- const current = integrationMutationFlights.get(clientId);
156
- if (current) {
157
- const age = startedAt - current.startedAt;
158
- if (current.key === key && age < INTEGRATION_MUTATION_JOIN_MS) {
159
- return current.promise as Promise<T>;
160
- }
161
- if (age <= INTEGRATION_MUTATION_TERMINAL_MS) {
162
- return Promise.reject(new IntegrationMutationBusyError(clientId));
163
- }
164
- if (integrationMutationFlights.get(clientId) === current) {
165
- integrationMutationFlights.delete(clientId);
166
- }
167
- }
168
-
169
- const flight: IntegrationMutationFlight = {
170
- key,
171
- startedAt,
172
- promise: Promise.resolve(),
173
- };
174
- const run = async (): Promise<unknown> => operation();
175
- flight.promise = (integrationMutationTestHooks?.run
176
- ? integrationMutationTestHooks.run(run)
177
- : run()
178
- ).finally(() => {
179
- if (integrationMutationFlights.get(clientId) === flight) {
180
- integrationMutationFlights.delete(clientId);
181
- }
182
- });
183
- integrationMutationFlights.set(clientId, flight);
184
- return flight.promise as Promise<T>;
185
- }
186
-
187
140
  export function setIntegrationMutationFlightTestHooks(
188
141
  hooks: {
189
142
  io?: IntegrationIO;
@@ -194,7 +147,7 @@ export function setIntegrationMutationFlightTestHooks(
194
147
  } | null,
195
148
  ): void {
196
149
  integrationMutationTestHooks = hooks;
197
- integrationMutationFlights.clear();
150
+ setIntegrationMutationFlightTestHook(hooks?.run ?? null);
198
151
  // Path overrides are part of the same isolation contract: clearing flights
199
152
  // while leaving a temp home bound would let the next suite write real files.
200
153
  if (hooks === null) integrationPathTestHooks = null;
@@ -129,6 +129,7 @@ import type { PersistedUsageAttempt } from "../../usage/log";
129
129
  import { isAllowedRequestOrigin, jsonResponse, providerManagementConfigError, publicProviderBaseUrl, safeConfigDTO, corsHeaders } from "../auth-cors";
130
130
  import { applySystemEnvToggle } from "../system-env";
131
131
  import {
132
+ ClientPathError,
132
133
  EXPORT_CLIENTS,
133
134
  EXPORT_CLIENT_IDS,
134
135
  OPENCODE_PROVIDER_ID,
@@ -204,6 +205,27 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
204
205
  );
205
206
  }
206
207
  const spec = EXPORT_CLIENTS[requested];
208
+ // Resolved before the catalog load on purpose. A refused override is a
209
+ // property of the request, not of the catalog: validating it afterwards
210
+ // let a busy or failing catalog answer 503 first, so a user with a
211
+ // relative override never saw the message that says how to fix it — and
212
+ // the route did the enumeration work anyway for input it was going to
213
+ // reject.
214
+ let destination: string;
215
+ try {
216
+ destination = spec.destination(process.env);
217
+ } catch (error) {
218
+ // A client's own environment override can name a path the resolver
219
+ // refuses — a relative value, which this process and the client would
220
+ // resolve against different working directories. That is a
221
+ // user-correctable configuration error, not a server fault, so it leaves
222
+ // this boundary as a bounded 400 instead of escaping handleManagementAPI
223
+ // as a generic 500 and stripping the message that says how to fix it.
224
+ // `integrations/state.ts` and `integrations/writer.ts` already catch the
225
+ // same error on their paths; this route was the one that did not.
226
+ if (!(error instanceof ClientPathError)) throw error;
227
+ return jsonResponse({ error: error.message }, 400, req, config);
228
+ }
207
229
  let models: ExportModel[];
208
230
  try {
209
231
  // The ONE loader every export surface uses. It carries the visibility
@@ -231,7 +253,7 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
231
253
  return jsonResponse({
232
254
  client: spec.id,
233
255
  filename: spec.filename,
234
- destination: spec.destination(process.env),
256
+ destination,
235
257
  apiKeyEnv: spec.apiKeyEnv,
236
258
  exportHint: spec.exportHint,
237
259
  // The client's own format and the exact bytes for it. The GUI previously
@@ -82,6 +82,10 @@ import {
82
82
  LOCAL_PROVIDER_RELOAD_PATH,
83
83
  } from "../../lib/local-provider-reload-contract";
84
84
  import { refreshUserCostOverlays } from "../../usage/user-cost-overlays";
85
+ import {
86
+ XAI_RESPONSES_OPT_IN_MODELS,
87
+ xaiResponsesOptInState,
88
+ } from "../../providers/xai-responses-opt-in";
85
89
 
86
90
  import { isPlainRecord, parseDebugLogQuery, tokPerSecondResult, unavailableCostReason, costResult, requestLogDto, stripRegistryOnlyStaticHeaders, fetchAllModels } from "./shared";
87
91
  import type { MetricUnavailableReason, TokPerSecondResult, CostEstimateReason, CostResult, MetricSource } from "./shared";
@@ -182,6 +186,20 @@ function applyProviderPatchFields(
182
186
  next.liveModels = rawBody.liveModels;
183
187
  touched = true;
184
188
  }
189
+ if (Object.hasOwn(rawBody, "xaiResponsesOptIn")) {
190
+ if (name !== "xai") return { error: "xaiResponsesOptIn is valid only for provider xai" };
191
+ if (typeof rawBody.xaiResponsesOptIn !== "boolean") {
192
+ return { error: "xaiResponsesOptIn must be a boolean" };
193
+ }
194
+ const modelAdapters = { ...(next.modelAdapters ?? {}) };
195
+ for (const model of XAI_RESPONSES_OPT_IN_MODELS) {
196
+ if (rawBody.xaiResponsesOptIn) modelAdapters[model] = "openai-responses";
197
+ else delete modelAdapters[model];
198
+ }
199
+ if (Object.keys(modelAdapters).length > 0) next.modelAdapters = modelAdapters;
200
+ else delete next.modelAdapters;
201
+ touched = true;
202
+ }
185
203
  if (Object.hasOwn(rawBody, "requestPacing")) {
186
204
  const value = rawBody.requestPacing;
187
205
  if (value === null) {
@@ -394,6 +412,7 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
394
412
  apiKeyTransport: p.apiKeyTransport,
395
413
  disabled: p.disabled === true,
396
414
  codexAccountMode: providerCodexAccountMode(name, p),
415
+ ...(name === "xai" ? { xaiResponsesOptInState: xaiResponsesOptInState(p) } : {}),
397
416
  discovery: p.liveModels === false ? undefined : getProviderDiscoveryStatus(name),
398
417
  })));
399
418
  }
@@ -694,6 +713,9 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
694
713
  name,
695
714
  disabled: config.providers[name]!.disabled === true,
696
715
  hasApiKey: !!config.providers[name]!.apiKey,
716
+ ...(name === "xai"
717
+ ? { xaiResponsesOptInState: xaiResponsesOptInState(config.providers[name]!) }
718
+ : {}),
697
719
  catalogRefresh,
698
720
  });
699
721
  }
@@ -17,7 +17,8 @@ import {
17
17
  type VisionSidecarBackend,
18
18
  } from "../../vision/eligibility";
19
19
  import { listOpenAiForwardSidecarCandidates } from "../../providers/openai-sidecar";
20
- import { listManagementModelRows } from "./model-rows";
20
+ import { pickerVisibleSidecarCandidates } from "../../sidecar/candidates";
21
+ import { resolveSidecarAuth } from "../../sidecar/auth";
21
22
 
22
23
  /**
23
24
  * Backends whose executor could actually run: openai forward, anthropic OAuth.
@@ -41,17 +42,23 @@ export function enabledVisionBackends(
41
42
  return backends.length > 0 ? backends : ["openai", "anthropic"];
42
43
  }
43
44
 
44
- /** Visible catalog rows in the shape the eligibility predicate consumes. */
45
+ /**
46
+ * Visible catalog rows in the shape the eligibility predicate consumes.
47
+ * Sourced from the unified picker set (#2188): picker-visible rows ∪ auth
48
+ * slots, UNFILTERED. Rule 2 (− provably text-only) belongs to the OPTIONS
49
+ * path only (visionEligibleModelOptions already applies it). The PUT gate
50
+ * consumes this list as EVIDENCE: a picker row proving an id text-only is
51
+ * exactly what visionDescriberIsProvablyBlind needs to reject that id, so
52
+ * pre-filtering here would deaden the gate (review F1: reject → allow flip).
53
+ */
45
54
  export async function visionCandidateRows(config: OcxConfig): Promise<VisionCandidateModel[]> {
46
- let rows: Awaited<ReturnType<typeof listManagementModelRows>> = [];
47
- // A catalog outage must not 500 the settings route nor reject a write; with []
48
- // the option list degrades to the baselines, which is the intended floor.
49
- try { rows = await listManagementModelRows(config); } catch { rows = []; }
50
- return rows.filter(row => row.disabled !== true).map(row => ({
51
- provider: row.provider,
52
- id: row.id,
53
- ...(row.inputModalities ? { inputModalities: row.inputModalities } : {}),
54
- ...(row.native ? { native: true } : {}),
55
+ const auth = resolveSidecarAuth(config);
56
+ const all = await pickerVisibleSidecarCandidates(config, auth);
57
+ return all.map(candidate => ({
58
+ provider: candidate.provider,
59
+ id: candidate.id,
60
+ ...(candidate.inputModalities ? { inputModalities: candidate.inputModalities } : {}),
61
+ ...(candidate.native ? { native: true } : {}),
55
62
  }));
56
63
  }
57
64
 
@@ -0,0 +1,120 @@
1
+ /**
2
+ * The one place that decides which models the management API offers as the
3
+ * web-search sidecar, and which it refuses to persist (#2188).
4
+ *
5
+ * Two routes write `webSearchSidecar.model` — `PUT /api/sidecar-settings`
6
+ * and the `webSearchSidecar` override in `PUT /api/claude-code`. They share
7
+ * this module for the same reason the vision routes share
8
+ * vision-sidecar-options.ts: a gate on one route and a stale copy on the
9
+ * other is the same as no gate at all. `ocx config set` raw JSON writes
10
+ * bypass management gates by design (operator escape hatch, same as vision).
11
+ *
12
+ * Unlike vision's provably-blind gate (reject only on positive proof), the
13
+ * web-search gate is MEMBERSHIP: the executor set is closed and known, so an
14
+ * id outside (candidates ∪ auth slots) can never run and is refused.
15
+ */
16
+ import type { OcxConfig } from "../../types";
17
+ import { AUTH_SLOT_MODELS, resolveSidecarAuth } from "../../sidecar/auth";
18
+ import { pickerVisibleSidecarCandidates, type SidecarCandidate } from "../../sidecar/candidates";
19
+ import { WEB_SEARCH_BACKENDS, webSearchSidecarCandidates } from "../../web-search/backends";
20
+ import type { WebSearchBackendId } from "../../web-search/index";
21
+
22
+ // The full configured union. Rows only ever materialize for backends whose
23
+ // executor admits the candidate, so inert arms (gemini/exa without config)
24
+ // simply never produce rows.
25
+ export type WebSearchBackend = WebSearchBackendId;
26
+
27
+ export interface WebSearchCandidateRow extends SidecarCandidate {
28
+ /** Executor backend that admitted this exact candidate row. */
29
+ backend: WebSearchBackend;
30
+ }
31
+
32
+ export interface WebSearchModelOption {
33
+ value: string;
34
+ label: string;
35
+ /** Exact runnable pair represented by this picker row. */
36
+ backend: WebSearchBackend;
37
+ model: string;
38
+ /** True when the row is an auth-slot entitlement rather than a picker row. */
39
+ authSlot?: boolean;
40
+ }
41
+
42
+ /** The candidate rows the web-search executors can actually run right now. */
43
+ export async function webSearchCandidateRows(config: OcxConfig): Promise<WebSearchCandidateRow[]> {
44
+ const auth = resolveSidecarAuth(config);
45
+ const all = await pickerVisibleSidecarCandidates(config, auth);
46
+ return webSearchSidecarCandidates(config, auth, all).flatMap(candidate => {
47
+ const descriptor = WEB_SEARCH_BACKENDS.find(entry =>
48
+ entry.isActive(auth, config) && entry.eligibleModel(candidate, auth));
49
+ return descriptor ? [{ ...candidate, backend: descriptor.backend }] : [];
50
+ });
51
+ }
52
+
53
+ /**
54
+ * The GUI/CLI option list. The persisted model is display-grandfathered so an
55
+ * operator can SEE a now-illegal setting in the picker (parity with the vision
56
+ * GET); new writes of such an id are still rejected by the gate below.
57
+ */
58
+ export function webSearchModelOptionsFrom(
59
+ config: Pick<OcxConfig, "webSearchSidecar">,
60
+ candidates: readonly WebSearchCandidateRow[],
61
+ ): WebSearchModelOption[] {
62
+ const byValue = new Map<string, WebSearchModelOption>();
63
+ for (const candidate of candidates) {
64
+ if (byValue.has(candidate.id)) continue;
65
+ byValue.set(candidate.id, {
66
+ value: candidate.id,
67
+ label: candidate.id,
68
+ backend: candidate.backend,
69
+ model: candidate.id,
70
+ ...(candidate.authSlot ? { authSlot: true } : {}),
71
+ });
72
+ }
73
+ const persisted = config.webSearchSidecar?.model;
74
+ if (persisted && !byValue.has(persisted)) {
75
+ byValue.set(persisted, {
76
+ value: persisted,
77
+ label: persisted,
78
+ backend: config.webSearchSidecar?.backend ?? "openai",
79
+ model: persisted,
80
+ });
81
+ }
82
+ return [...byValue.values()].sort((a, b) => a.value.localeCompare(b.value));
83
+ }
84
+
85
+ /**
86
+ * Membership gate: reject when the id is neither a runnable candidate nor an
87
+ * auth-slot model. Auth slots pass even when the matching login is currently
88
+ * absent — the slot is a legal setting whose executor simply is not live yet,
89
+ * and refusing it would make settings order-dependent on login state.
90
+ */
91
+ export function webSearchModelIsRejected(
92
+ backend: WebSearchBackend,
93
+ requested: string,
94
+ candidates: readonly WebSearchCandidateRow[],
95
+ ): boolean {
96
+ if (requested === AUTH_SLOT_MODELS.codex) return backend !== "openai";
97
+ if (requested === AUTH_SLOT_MODELS.anthropic) return backend !== "anthropic";
98
+ return !candidates.some(candidate => candidate.backend === backend && candidate.id === requested);
99
+ }
100
+
101
+ /** Uniform 400 payload naming the filter, mirroring visionDescriberRejection's shape. */
102
+ export function webSearchModelRejection(
103
+ field: string,
104
+ backend: WebSearchBackend,
105
+ requested: string,
106
+ candidates: readonly WebSearchCandidateRow[],
107
+ ): { error: string; allowedModels: string[] } {
108
+ const allowed = new Set(candidates
109
+ .filter(candidate => candidate.backend === backend)
110
+ .map(candidate => candidate.id));
111
+ // Preserve the response contract: allowedModels lists both always-configurable
112
+ // auth-slot ids even though the pair gate below binds each to its own backend.
113
+ allowed.add(AUTH_SLOT_MODELS.codex);
114
+ allowed.add(AUTH_SLOT_MODELS.anthropic);
115
+ return {
116
+ error: `${field}: backend/model pair "${backend}/${requested}" is not a web-search sidecar candidate — ` +
117
+ "the model must be picker-visible and runnable by that executor-backed backend (or its auth-slot model)",
118
+ allowedModels: [...allowed].sort(),
119
+ };
120
+ }