@bitkyc08/opencodex 2.20.0 → 2.22.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 (64) hide show
  1. package/AGENTS_INSTALL.md +32 -0
  2. package/README.md +1 -1
  3. package/gui/dist/assets/{index-DSK3S5HY.js → index-ClEcVlFO.js} +43 -17
  4. package/gui/dist/assets/index-DQsMZzI5.css +1 -0
  5. package/gui/dist/index.html +2 -2
  6. package/package.json +1 -1
  7. package/src/adapters/anthropic.ts +2 -28
  8. package/src/adapters/google.ts +31 -3
  9. package/src/adapters/openai-chat.ts +25 -8
  10. package/src/adapters/responses-tool-schema.ts +67 -0
  11. package/src/bridge.ts +15 -2
  12. package/src/claude/agents-inject.ts +2 -2
  13. package/src/claude/gateway-cache.ts +41 -4
  14. package/src/cli/claude.ts +1 -1
  15. package/src/cli/codex-log-guard-doctor.ts +103 -0
  16. package/src/cli/dispatch.ts +7 -1
  17. package/src/cli/help.ts +1 -1
  18. package/src/cli/models.ts +16 -6
  19. package/src/cli/observe.ts +38 -2
  20. package/src/cli/registry.ts +2 -1
  21. package/src/cli/v2.ts +34 -1
  22. package/src/codex/app-server-processes.ts +46 -26
  23. package/src/codex/catalog/effort.ts +49 -1
  24. package/src/codex/catalog/parsing.ts +64 -4
  25. package/src/codex/catalog/provider-fetch.ts +12 -0
  26. package/src/codex/catalog/sync.ts +14 -1
  27. package/src/codex/convergence.ts +2 -0
  28. package/src/codex/inject.ts +3 -3
  29. package/src/codex/log-guard/inspect.ts +506 -0
  30. package/src/codex/log-guard/lock.ts +150 -0
  31. package/src/codex/log-guard/maintenance.ts +403 -0
  32. package/src/codex/log-guard/path-safety.ts +39 -0
  33. package/src/codex/log-guard/policy.ts +44 -0
  34. package/src/codex/log-guard/processes.ts +205 -0
  35. package/src/codex/log-guard/protection.ts +489 -0
  36. package/src/codex/log-guard/sqlite-errors.ts +9 -0
  37. package/src/codex/paths.ts +5 -0
  38. package/src/codex/plugins-doctor.ts +1 -1
  39. package/src/codex/project-config-warnings.ts +2 -2
  40. package/src/generated/compatibility-version.json +93 -45
  41. package/src/images/loop.ts +15 -5
  42. package/src/providers/antigravity-models.ts +11 -1
  43. package/src/providers/model-discovery.ts +94 -6
  44. package/src/providers/quota.ts +159 -0
  45. package/src/providers/registry.ts +20 -1
  46. package/src/providers/slug-codec.ts +29 -0
  47. package/src/responses/custom-tool-compat.ts +4 -1
  48. package/src/responses/parser.ts +7 -1
  49. package/src/responses/provider-opaque-metadata.ts +73 -0
  50. package/src/responses/schema.ts +6 -0
  51. package/src/router.ts +12 -4
  52. package/src/routing/capability.ts +32 -17
  53. package/src/server/auth-cors.ts +42 -6
  54. package/src/server/index.ts +1 -0
  55. package/src/server/management/agent-settings-routes.ts +20 -2
  56. package/src/server/management/context.ts +15 -0
  57. package/src/server/management/model-routes.ts +12 -3
  58. package/src/server/management/storage-log-guard-routes.ts +186 -0
  59. package/src/server/management-api.ts +3 -1
  60. package/src/server/responses/core.ts +13 -0
  61. package/src/server/system-env.ts +1 -1
  62. package/src/types.ts +24 -2
  63. package/src/web-search/loop.ts +21 -5
  64. package/gui/dist/assets/index-DF_UFrGS.css +0 -1
@@ -232,6 +232,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
232
232
  agentsMaxThreadsConflict: enabled && hasAgentsMaxThreads(),
233
233
  maxConcurrentThreadsPerSession: getLogicalMaxThreads(),
234
234
  multiAgentMode: config.multiAgentMode ?? "default",
235
+ keepNativeChatGptOnV1: config.keepNativeChatGptOnV1 === true,
235
236
  agentsEnabled: getAgentsEnabled(),
236
237
  agentsMaxDepth: getAgentsMaxDepth(),
237
238
  subagentDeveloperInstructions: getSubagentDeveloperInstructions(),
@@ -246,6 +247,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
246
247
  enabled?: unknown;
247
248
  maxConcurrentThreadsPerSession?: unknown;
248
249
  multiAgentMode?: unknown;
250
+ keepNativeChatGptOnV1?: unknown;
249
251
  agentsEnabled?: unknown;
250
252
  agentsMaxDepth?: unknown;
251
253
  subagentDeveloperInstructions?: unknown;
@@ -255,17 +257,21 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
255
257
  const wantsFlag = body.enabled !== undefined;
256
258
  const wantsThreads = body.maxConcurrentThreadsPerSession !== undefined;
257
259
  const wantsMode = body.multiAgentMode !== undefined;
260
+ const wantsKeepNative = body.keepNativeChatGptOnV1 !== undefined;
258
261
  const wantsAgentsEnabled = body.agentsEnabled !== undefined;
259
262
  const wantsMaxDepth = body.agentsMaxDepth !== undefined;
260
263
  const wantsSubagentInstructions = body.subagentDeveloperInstructions !== undefined;
261
264
  const wantsModeHintText = body.multiAgentModeHintText !== undefined;
262
- if (!wantsFlag && !wantsThreads && !wantsMode && !wantsAgentsEnabled && !wantsMaxDepth && !wantsSubagentInstructions && !wantsModeHintText) {
263
- return jsonResponse({ error: "body must set enabled, multiAgentMode, maxConcurrentThreadsPerSession, agentsEnabled, agentsMaxDepth, subagentDeveloperInstructions, and/or multiAgentModeHintText" }, 400);
265
+ if (!wantsFlag && !wantsThreads && !wantsMode && !wantsKeepNative && !wantsAgentsEnabled && !wantsMaxDepth && !wantsSubagentInstructions && !wantsModeHintText) {
266
+ return jsonResponse({ error: "body must set enabled, multiAgentMode, keepNativeChatGptOnV1, maxConcurrentThreadsPerSession, agentsEnabled, agentsMaxDepth, subagentDeveloperInstructions, and/or multiAgentModeHintText" }, 400);
264
267
  }
265
268
  if (wantsFlag && typeof body.enabled !== "boolean") return jsonResponse({ error: "body.enabled must be a boolean" }, 400);
266
269
  if (wantsMode && body.multiAgentMode !== "v1" && body.multiAgentMode !== "default" && body.multiAgentMode !== "v2") {
267
270
  return jsonResponse({ error: "body.multiAgentMode must be 'v1', 'default', or 'v2'" }, 400);
268
271
  }
272
+ if (wantsKeepNative && typeof body.keepNativeChatGptOnV1 !== "boolean") {
273
+ return jsonResponse({ error: "body.keepNativeChatGptOnV1 must be a boolean" }, 400);
274
+ }
269
275
  if (wantsThreads && (typeof body.maxConcurrentThreadsPerSession !== "number" || !Number.isInteger(body.maxConcurrentThreadsPerSession) || body.maxConcurrentThreadsPerSession < 1)) {
270
276
  return jsonResponse({ error: "body.maxConcurrentThreadsPerSession must be an integer >= 1" }, 400);
271
277
  }
@@ -333,6 +339,17 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
333
339
  saveConfigPreservingClaudeCode(config);
334
340
  warnings.push(`Multi-agent mode set to '${mode}'. Applies to new sessions.`);
335
341
  }
342
+ if (wantsKeepNative) {
343
+ if (body.keepNativeChatGptOnV1 === true) config.keepNativeChatGptOnV1 = true;
344
+ else delete config.keepNativeChatGptOnV1;
345
+ saveConfigPreservingClaudeCode(config);
346
+ const effectiveMode = mode ?? config.multiAgentMode ?? "default";
347
+ warnings.push(body.keepNativeChatGptOnV1 === true
348
+ ? (effectiveMode === "v2"
349
+ ? "ChatGPT-native models stay on v1 while other models use v2. Applies to new sessions."
350
+ : "keepNativeChatGptOnV1 is stored but inactive until multi-agent mode is v2. Applies to new sessions.")
351
+ : "ChatGPT-native models follow the selected v1/v2/base surface. Applies to new sessions.");
352
+ }
336
353
  // New-key scalar writes: each writer is individually atomic, so apply them in
337
354
  // sequence after the transition. A failure here is a persistence failure (the
338
355
  // writers' ok:false result or a throw from the underlying atomic write helper),
@@ -373,6 +390,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
373
390
  agentsMaxThreadsConflict: enabled && hasAgentsMaxThreads(),
374
391
  maxConcurrentThreadsPerSession: getLogicalMaxThreads(),
375
392
  multiAgentMode: config.multiAgentMode ?? "default",
393
+ keepNativeChatGptOnV1: config.keepNativeChatGptOnV1 === true,
376
394
  agentsEnabled: getAgentsEnabled(),
377
395
  agentsMaxDepth: getAgentsMaxDepth(),
378
396
  subagentDeveloperInstructions: getSubagentDeveloperInstructions(),
@@ -1,5 +1,7 @@
1
1
  import type { OcxConfig } from "../../types";
2
2
  import type { NativeProfileApiDeps } from "../../codex/native-profile-api";
3
+ import type { CodexLogGuardProtectionDeps } from "../../codex/log-guard/protection";
4
+ import type { CodexLogGuardMaintenanceDeps } from "../../codex/log-guard/maintenance";
3
5
  import type { StartupHealth } from "../../codex/autostart-health";
4
6
  import type { StartupInstallAction } from "../startup-action-control";
5
7
  import type { ManagementPrincipal } from "../management-auth";
@@ -70,6 +72,19 @@ export interface ManagementApiDeps {
70
72
  performRestart: typeof performCodexRestart;
71
73
  };
72
74
  nativeProfileApi?: NativeProfileApiDeps;
75
+ /**
76
+ * Log Guard mutation seam. Production leaves this unset and therefore uses the
77
+ * owner-verified process enumerator, trusted L namespace and real config store.
78
+ * Route tests inject all three so they cannot depend on local Codex processes
79
+ * or create lock/config state outside the fixture.
80
+ */
81
+ codexLogGuardProtectionDeps?: CodexLogGuardProtectionDeps;
82
+ /**
83
+ * Log Guard maintenance seam. Production reuses the same fail-closed process
84
+ * enumerator and L namespace as Protect; route tests keep all maintenance
85
+ * state inside their temporary Codex home.
86
+ */
87
+ codexLogGuardMaintenanceDeps?: CodexLogGuardMaintenanceDeps;
73
88
  }
74
89
 
75
90
 
@@ -97,7 +97,8 @@ import { providerDestinationResolvedError } from "../../lib/destination-policy";
97
97
  import { enrichProviderFromCatalog, listKeyLoginProviders } from "../../oauth/key-providers";
98
98
  import { deriveProviderPresets } from "../../providers/derive";
99
99
  import { providerCodexAccountMode } from "../../providers/registry";
100
- import { routedSlug, slugEquals } from "../../providers/slug-codec";
100
+ import { encodedModelIdCollides, routedSlug, slugEquals } from "../../providers/slug-codec";
101
+ import { knownModelIdsForProvider } from "../../router";
101
102
  import { COMBO_NAMESPACE, comboDisabledModelSelectors, comboModelId, preservesPhysicalComboProvider } from "../../combos";
102
103
  import { clearProviderQuotaCache, fetchProviderQuotaReports } from "../../providers/quota";
103
104
  import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
@@ -376,7 +377,6 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
376
377
  const provider = typeof body.provider === "string" ? body.provider.trim() : "";
377
378
  const modelId = typeof body.modelId === "string" ? body.modelId.trim() : "";
378
379
  if (!provider || !modelId) return jsonResponse({ error: "provider and modelId are required" }, 400);
379
- if (modelId.includes("/")) return jsonResponse({ error: "modelId must not contain /" }, 400);
380
380
  if (!isValidProviderName(provider)) return jsonResponse({ error: "invalid provider name" }, 400);
381
381
  if (!hasOwnProvider(config.providers, provider)) return jsonResponse({ error: "provider not configured" }, 404);
382
382
  const displayName = typeof body.displayName === "string" && body.displayName.trim() ? body.displayName.trim() : undefined;
@@ -394,6 +394,10 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
394
394
  if (existing.some(cm => routedSlug(cm.provider, cm.modelId) === newSlug)) {
395
395
  return jsonResponse({ error: "duplicate model" }, 409);
396
396
  }
397
+ const known = knownModelIdsForProvider(provider, config.providers[provider], config);
398
+ if (encodedModelIdCollides(modelId, known)) {
399
+ return jsonResponse({ error: "ambiguous model id" }, 409);
400
+ }
397
401
  const entry: OcxCustomModel = {
398
402
  id: randomUUID(),
399
403
  provider,
@@ -422,7 +426,6 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
422
426
  if (idx === -1) return jsonResponse({ error: "not found" }, 404);
423
427
  const cm = { ...list[idx] };
424
428
  if (typeof body.modelId === "string" && body.modelId.trim()) {
425
- if (body.modelId.includes("/")) return jsonResponse({ error: "modelId must not contain /" }, 400);
426
429
  cm.modelId = body.modelId.trim();
427
430
  }
428
431
  if (body.displayName !== undefined) {
@@ -469,6 +472,12 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
469
472
  if (list.some((other, i) => i !== idx && routedSlug(other.provider, other.modelId) === updatedSlug)) {
470
473
  return jsonResponse({ error: "duplicate model" }, 409);
471
474
  }
475
+ const known = knownModelIdsForProvider(cm.provider, config.providers[cm.provider], {
476
+ customModels: list.filter((_, i) => i !== idx),
477
+ });
478
+ if (encodedModelIdCollides(cm.modelId, known)) {
479
+ return jsonResponse({ error: "ambiguous model id" }, 409);
480
+ }
472
481
  list[idx] = cm;
473
482
  config.customModels = list;
474
483
  persistConfig(config);
@@ -0,0 +1,186 @@
1
+ import { resolveCodexHomeDir } from "../../codex/home";
2
+ import {
3
+ compactCodexLogs,
4
+ type CodexLogGuardCompactionResult,
5
+ } from "../../codex/log-guard/maintenance";
6
+ import {
7
+ getCodexLogGuardProtectionStatus,
8
+ protectCodexLogs,
9
+ repairCodexLogGuardProtection,
10
+ unprotectCodexLogs,
11
+ type CodexLogGuardMutationResult,
12
+ type CodexLogGuardStatus,
13
+ } from "../../codex/log-guard/protection";
14
+ import { scanStorage } from "../../storage/scanner";
15
+ import { jsonResponse } from "../auth-cors";
16
+ import {
17
+ managementBodyTooLargeResponse,
18
+ readManagementJsonBody,
19
+ } from "./body";
20
+ import type { ManagementContext } from "./context";
21
+
22
+ const INSPECTION_FAILED_MESSAGE = "Codex log inspection failed";
23
+
24
+ function inspectionUnavailable(report: CodexLogGuardStatus): boolean {
25
+ return report.schema.state === "unavailable";
26
+ }
27
+
28
+ function mutationStatus(result: CodexLogGuardMutationResult): number {
29
+ if (result.ok) return 200;
30
+ switch (result.error) {
31
+ case "process_enumeration_failed":
32
+ return 503;
33
+ case "codex_running":
34
+ case "busy":
35
+ case "unsupported_schema":
36
+ case "trigger_collision":
37
+ case "unsafe_path":
38
+ return 409;
39
+ case "database_error":
40
+ case "config_write_failed":
41
+ return 500;
42
+ }
43
+ }
44
+
45
+ function compactStatus(result: CodexLogGuardCompactionResult): number {
46
+ if (result.ok) return 200;
47
+ switch (result.error) {
48
+ case "process_enumeration_failed":
49
+ return 503;
50
+ case "codex_running":
51
+ case "busy":
52
+ case "unsupported_schema":
53
+ case "auto_vacuum_not_incremental":
54
+ case "unsafe_path":
55
+ case "integrity_check_failed":
56
+ return 409;
57
+ case "database_error":
58
+ return 500;
59
+ }
60
+ }
61
+
62
+ function mutationResponse(
63
+ result: CodexLogGuardMutationResult,
64
+ ctx: ManagementContext,
65
+ ): Response {
66
+ return result.ok
67
+ ? jsonResponse(result.status, 200, ctx.req, ctx.config)
68
+ : jsonResponse({ error: result.error }, mutationStatus(result), ctx.req, ctx.config);
69
+ }
70
+
71
+ function compactResponse(
72
+ result: CodexLogGuardCompactionResult,
73
+ ctx: ManagementContext,
74
+ ): Response {
75
+ if (result.ok) {
76
+ return jsonResponse({ report: result.report }, 200, ctx.req, ctx.config);
77
+ }
78
+ return jsonResponse(
79
+ result.error === "integrity_check_failed"
80
+ ? { error: result.error, phase: result.phase }
81
+ : { error: result.error },
82
+ compactStatus(result),
83
+ ctx.req,
84
+ ctx.config,
85
+ );
86
+ }
87
+
88
+ async function readProtectMode(ctx: ManagementContext): Promise<"compat" | "quiet" | Response> {
89
+ let body: unknown;
90
+ try {
91
+ body = await readManagementJsonBody(ctx.req);
92
+ } catch (error) {
93
+ const tooLarge = managementBodyTooLargeResponse(error, ctx.req, ctx.config);
94
+ if (tooLarge) return tooLarge;
95
+ return jsonResponse({ error: "invalid_request" }, 400, ctx.req, ctx.config);
96
+ }
97
+ if (!body || typeof body !== "object" || Array.isArray(body)) {
98
+ return jsonResponse({ error: "invalid_request" }, 400, ctx.req, ctx.config);
99
+ }
100
+ const mode = (body as Record<string, unknown>).mode;
101
+ if (mode !== "compat" && mode !== "quiet") {
102
+ return jsonResponse({ error: "invalid_mode" }, 400, ctx.req, ctx.config);
103
+ }
104
+ return mode;
105
+ }
106
+
107
+ /** Codex Log Guard diagnostics plus explicit protection and maintenance mutations. */
108
+ export async function handleStorageLogGuardRoutes(ctx: ManagementContext): Promise<Response | null> {
109
+ const { req, url, config, deps } = ctx;
110
+ const protectionDeps = deps.codexLogGuardProtectionDeps;
111
+
112
+ if (url.pathname === "/api/storage/codex-logs") {
113
+ if (req.method !== "GET") return null;
114
+ try {
115
+ const report = getCodexLogGuardProtectionStatus(protectionDeps);
116
+ if (inspectionUnavailable(report)) {
117
+ return jsonResponse({ error: "inspect_failed", message: INSPECTION_FAILED_MESSAGE }, 500, req, config);
118
+ }
119
+ return jsonResponse(report, 200, req, config);
120
+ } catch {
121
+ return jsonResponse({
122
+ error: "inspect_failed",
123
+ message: INSPECTION_FAILED_MESSAGE,
124
+ }, 500, req, config);
125
+ }
126
+ }
127
+
128
+ if (url.pathname === "/api/storage/codex-logs/protect") {
129
+ if (req.method !== "POST") return null;
130
+ const mode = await readProtectMode(ctx);
131
+ if (mode instanceof Response) return mode;
132
+ return mutationResponse(protectCodexLogs(mode, protectionDeps), ctx);
133
+ }
134
+
135
+ if (url.pathname === "/api/storage/codex-logs/unprotect") {
136
+ if (req.method !== "POST") return null;
137
+ return mutationResponse(unprotectCodexLogs(protectionDeps), ctx);
138
+ }
139
+
140
+ if (url.pathname === "/api/storage/codex-logs/repair") {
141
+ if (req.method !== "POST") return null;
142
+ return mutationResponse(repairCodexLogGuardProtection(protectionDeps), ctx);
143
+ }
144
+
145
+ if (url.pathname === "/api/storage/codex-logs/compact") {
146
+ if (req.method !== "POST") return null;
147
+ return compactResponse(compactCodexLogs(deps.codexLogGuardMaintenanceDeps), ctx);
148
+ }
149
+
150
+ if (url.pathname !== "/api/storage" || req.method !== "GET") return null;
151
+
152
+ // Keep the existing CODEX_HOME scan as the primary storage contract. The Log Guard
153
+ // report is attached separately so an external sqlite_home is visible without being
154
+ // silently folded into CODEX_HOME totals.
155
+ let storage;
156
+ try {
157
+ storage = scanStorage();
158
+ } catch {
159
+ const fallback = {
160
+ codexHome: resolveCodexHomeDir(),
161
+ generatedAt: Date.now(),
162
+ total: { bytes: 0, fileCount: 0 },
163
+ buckets: [],
164
+ error: "scan_failed",
165
+ };
166
+ try {
167
+ const report = getCodexLogGuardProtectionStatus(protectionDeps);
168
+ return inspectionUnavailable(report)
169
+ ? jsonResponse({ ...fallback, codexLogs: null, codexLogsError: "inspect_failed" }, 200, req, config)
170
+ : jsonResponse({ ...fallback, codexLogs: report }, 200, req, config);
171
+ } catch {
172
+ return jsonResponse({ ...fallback, codexLogs: null, codexLogsError: "inspect_failed" }, 200, req, config);
173
+ }
174
+ }
175
+
176
+ try {
177
+ const report = getCodexLogGuardProtectionStatus(protectionDeps);
178
+ return inspectionUnavailable(report)
179
+ ? jsonResponse({ ...storage, codexLogs: null, codexLogsError: "inspect_failed" }, 200, req, config)
180
+ : jsonResponse({ ...storage, codexLogs: report }, 200, req, config);
181
+ } catch {
182
+ // Log Guard inspection is auxiliary to the existing Storage page. A config/path
183
+ // resolution failure must not take the legacy read-only storage report down with it.
184
+ return jsonResponse({ ...storage, codexLogs: null, codexLogsError: "inspect_failed" }, 200, req, config);
185
+ }
186
+ }
@@ -59,6 +59,7 @@ import { applySystemEnvToggle } from "./system-env";
59
59
  import type { ManagementApiDeps } from "./management/context";
60
60
  import { handleConfigRoutes } from "./management/config-routes";
61
61
  import { handleLogsUsageRoutes } from "./management/logs-usage-routes";
62
+ import { handleStorageLogGuardRoutes } from "./management/storage-log-guard-routes";
62
63
  import { handleRequestHistoryRoutes } from "./management/request-history-routes";
63
64
  import { handleRoutingAnalyticsRoutes } from "./management/routing-analytics-routes";
64
65
  import { handleProviderRoutes } from "./management/provider-routes";
@@ -209,6 +210,7 @@ export async function handleManagementAPI(
209
210
  let routed: Response | null;
210
211
  try {
211
212
  routed = (await handleConfigRoutes(ctx))
213
+ ?? (await handleStorageLogGuardRoutes(ctx))
212
214
  ?? (await handleLogsUsageRoutes(ctx))
213
215
  ?? (await handleRequestHistoryRoutes(ctx))
214
216
  ?? (await handleRoutingAnalyticsRoutes(ctx))
@@ -300,4 +302,4 @@ export async function handleManagementAPI(
300
302
  }
301
303
 
302
304
 
303
- export { buildClaudeDesktopState, fetchAllModels } from "./management/shared";
305
+ export { buildClaudeDesktopState, fetchAllModels } from "./management/shared";
@@ -1080,6 +1080,19 @@ async function applyFinalRouteRequestNormalization(args: {
1080
1080
  }
1081
1081
  }
1082
1082
 
1083
+ // Generic Responses clients (e.g. AI-SDK apps) omit `store`, but the canonical
1084
+ // forward Codex backend rejects a native request without an explicit store:false.
1085
+ // Default it only there — every other Responses upstream (key-auth providers and
1086
+ // custom forward gateways) intentionally keeps the omitted-store server-side
1087
+ // default for previous_response_id reuse — and never override an explicit value.
1088
+ if (
1089
+ isCanonicalOpenAiForwardProvider(route.provider)
1090
+ && parsed._rawBody && typeof parsed._rawBody === "object"
1091
+ && (parsed._rawBody as Record<string, unknown>).store === undefined
1092
+ ) {
1093
+ (parsed._rawBody as Record<string, unknown>).store = false;
1094
+ }
1095
+
1083
1096
  // Final selected model before virtual wire-model rewriting (Pro aliases).
1084
1097
  const finalSelectedModelId = route.modelId;
1085
1098
 
@@ -335,7 +335,7 @@ export async function injectSystemEnv(port: number, config: OcxConfig): Promise<
335
335
  // without a token — keep it in sync with this proxy's /v1/models. Best-effort.
336
336
  try {
337
337
  const { refreshGatewayModelCacheFromProxy } = await import("../claude/gateway-cache");
338
- await refreshGatewayModelCacheFromProxy(port);
338
+ await refreshGatewayModelCacheFromProxy(port, { admissionConfig: config });
339
339
  } catch { /* best-effort */ }
340
340
 
341
341
  // Roster agent definitions (devlog 070): same launch-time sync for plain `claude`.
package/src/types.ts CHANGED
@@ -180,10 +180,27 @@ export interface OcxToolCall {
180
180
  arguments: Record<string, unknown>;
181
181
  customWireName?: string;
182
182
  thoughtSignature?: string;
183
+ /**
184
+ * Provider-issued opaque metadata that must survive the whole round trip unchanged
185
+ * (issue #1735). A signed Gemini part is only valid when its signature comes back on the
186
+ * SAME part it was issued for, so this travels with the individual tool call rather than
187
+ * being matched by name/arguments after the fact.
188
+ */
189
+ providerMetadata?: OcxProviderOpaqueToolCallMetadata;
183
190
  /** MCP namespace (e.g. "mcp__context7") when this call targets a namespaced tool. */
184
191
  namespace?: string;
185
192
  }
186
193
 
194
+ /**
195
+ * Opaque, provider-scoped tool-call metadata. Values are never parsed, merged, re-encoded, or
196
+ * synthesized — they are carried verbatim or not at all.
197
+ */
198
+ export interface OcxProviderOpaqueToolCallMetadata {
199
+ google?: {
200
+ thoughtSignature?: string;
201
+ };
202
+ }
203
+
187
204
  export type OcxAssistantContentPart = OcxTextContent | OcxThinkingContent | OcxToolCall;
188
205
 
189
206
  export interface OcxTool {
@@ -328,7 +345,7 @@ export type AdapterEvent =
328
345
  // Never rendered — it only rides the reasoning item's envelope so the next request can replay it.
329
346
  | { type: "kiro_redacted_reasoning"; data: string }
330
347
  | { type: "reasoning_raw_delta"; text: string }
331
- | { type: "tool_call_start"; id: string; name: string }
348
+ | { type: "tool_call_start"; id: string; name: string; providerMetadata?: OcxProviderOpaqueToolCallMetadata }
332
349
  | { type: "tool_call_delta"; arguments: string }
333
350
  | { type: "tool_call_end" }
334
351
  /** Internal boundary between a guarded first pass and its one-shot continuation. */
@@ -579,7 +596,7 @@ export interface OcxCustomModel {
579
596
  id: string;
580
597
  /** 프로바이더 키 (기존 providers[name]) */
581
598
  provider: string;
582
- /** 모델 슬러그 (프로바이더 접두사 없는 bare id) */
599
+ /** Native provider model id; slashes are allowed and encoded for Codex as provider/<hyphenated-id>. */
583
600
  modelId: string;
584
601
  /** 인간 가독 표시명 (선택, 슬래시 불가) */
585
602
  displayName?: string;
@@ -808,6 +825,11 @@ export interface OcxConfig {
808
825
  * - "v2": force ALL models to v2 surface (override upstream pins)
809
826
  */
810
827
  multiAgentMode?: "v1" | "default" | "v2";
828
+ /**
829
+ * When `multiAgentMode` is `"v2"`, keep ChatGPT-native catalog rows on v1.
830
+ * Routed parents get v2 tools; Sol/Terra can still spawn Grok/Claude (issue #92).
831
+ */
832
+ keepNativeChatGptOnV1?: boolean;
811
833
  /** Experimental, default-off ChatGPT recovery for encrypted V2 routed tasks. */
812
834
  agentTaskRecovery?: {
813
835
  enabled?: boolean;
@@ -1,6 +1,7 @@
1
1
  import type { AdapterRequest, IncomingMeta, ProviderAdapter } from "../adapters/base";
2
- import type { AdapterEvent, OcxMessage, OcxParsedRequest, OcxProviderConfig, OcxThinkingContent, OcxUsage, RateLimitRetryPolicy } from "../types";
2
+ import type { AdapterEvent, OcxMessage, OcxParsedRequest, OcxProviderConfig, OcxProviderOpaqueToolCallMetadata, OcxThinkingContent, OcxUsage, RateLimitRetryPolicy } from "../types";
3
3
  import { namespacedToolName, toolChoiceToolPredicate } from "../types";
4
+ import { cloneProviderOpaqueToolCallMetadata } from "../responses/provider-opaque-metadata";
4
5
  import type { AttemptRecoveryKind } from "../usage/log";
5
6
  import { bridgeToResponsesSSE } from "../bridge";
6
7
  import { runWebSearch, type SidecarOutcome, type SidecarOutcomeRecorder, type SidecarSettings } from "./executor";
@@ -32,6 +33,11 @@ interface WebSearchCall {
32
33
  // empty array means the model called the tool with neither `query` nor `queries` (handled as an
33
34
  // empty-query placeholder).
34
35
  queries: string[];
36
+ /**
37
+ * Provider-opaque metadata from the originating part (issue #1735). Stored PER CALL so a
38
+ * signature can never migrate to a different call when the model batches several.
39
+ */
40
+ providerMetadata?: OcxProviderOpaqueToolCallMetadata;
35
41
  }
36
42
 
37
43
  /**
@@ -69,7 +75,7 @@ export function scanEventsForWebSearch(events: AdapterEvent[]): {
69
75
  const passthrough: AdapterEvent[] = [];
70
76
  let hasRealToolCall = false;
71
77
  let hasMalformedToolCall = false;
72
- let pending: { name: string; id: string; argsBuf: string; closed: boolean; events: AdapterEvent[] } | null = null;
78
+ let pending: { name: string; id: string; argsBuf: string; closed: boolean; events: AdapterEvent[]; providerMetadata?: OcxProviderOpaqueToolCallMetadata } | null = null;
73
79
  const isBlank = (value: string): boolean => value.trim().length === 0;
74
80
  const flushPending = (): void => {
75
81
  // A pending call that never saw tool_call_end is structurally malformed.
@@ -84,7 +90,7 @@ export function scanEventsForWebSearch(events: AdapterEvent[]): {
84
90
  if (e.type === "tool_call_start") {
85
91
  flushPending();
86
92
  if (isBlank(e.id) || isBlank(e.name)) hasMalformedToolCall = true;
87
- pending = { name: e.name, id: e.id, argsBuf: "", closed: false, events: [e] };
93
+ pending = { name: e.name, id: e.id, argsBuf: "", closed: false, events: [e], providerMetadata: e.providerMetadata };
88
94
  } else if (e.type === "tool_call_delta") {
89
95
  // Orphan delta (no open call) is malformed.
90
96
  if (!pending) hasMalformedToolCall = true;
@@ -100,7 +106,7 @@ export function scanEventsForWebSearch(events: AdapterEvent[]): {
100
106
  pending.events.push(e);
101
107
  pending.closed = true;
102
108
  if (pending.name === WEB_SEARCH_TOOL_NAME) {
103
- calls.push({ id: pending.id, queries: parseQueries(pending.argsBuf) });
109
+ calls.push({ id: pending.id, queries: parseQueries(pending.argsBuf), providerMetadata: pending.providerMetadata });
104
110
  } else {
105
111
  passthrough.push(...pending.events);
106
112
  if (!isBlank(pending.id) && !isBlank(pending.name)) hasRealToolCall = true;
@@ -678,7 +684,17 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
678
684
  // Signed thinking must precede tool_use on replay (Anthropic extended thinking), and
679
685
  // unsigned raw reasoning has to ride along for providers that require it back (#688).
680
686
  ...precedingThinking,
681
- { type: "toolCall" as const, id: call.id, name: WEB_SEARCH_TOOL_NAME, arguments: callArgs },
687
+ {
688
+ type: "toolCall" as const,
689
+ id: call.id,
690
+ name: WEB_SEARCH_TOOL_NAME,
691
+ arguments: callArgs,
692
+ // Re-attach the signature to the rebuilt call so a sidecar turn keeps Gemini
693
+ // reasoning continuity instead of relying on the same-process replay cache.
694
+ ...(cloneProviderOpaqueToolCallMetadata(call.providerMetadata)
695
+ ? { providerMetadata: cloneProviderOpaqueToolCallMetadata(call.providerMetadata) }
696
+ : {}),
697
+ },
682
698
  ],
683
699
  timestamp: now,
684
700
  });