@bitkyc08/opencodex 2.25.0 → 2.26.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 (99) hide show
  1. package/bin/ocx.mjs +59 -7
  2. package/gui/dist/assets/index-RL6b1bTV.js +102 -0
  3. package/gui/dist/index.html +1 -1
  4. package/package.json +1 -1
  5. package/src/adapters/base.ts +18 -0
  6. package/src/adapters/client-fingerprint.ts +0 -2
  7. package/src/adapters/cursor/http1-bidi.ts +361 -0
  8. package/src/adapters/cursor/live-models.ts +117 -30
  9. package/src/adapters/cursor/live-transport.ts +137 -56
  10. package/src/adapters/cursor/native-exec-fs.ts +1 -1
  11. package/src/adapters/cursor/native-exec-network.ts +1 -1
  12. package/src/adapters/cursor/native-exec-shell.ts +0 -1
  13. package/src/adapters/cursor/protobuf-request.ts +89 -21
  14. package/src/adapters/cursor/tool-definitions.ts +18 -7
  15. package/src/adapters/cursor/tool-result-normalize.ts +92 -0
  16. package/src/adapters/cursor/transport.ts +7 -0
  17. package/src/adapters/cursor.ts +2 -0
  18. package/src/adapters/google-http.ts +38 -10
  19. package/src/adapters/google.ts +23 -3
  20. package/src/adapters/openai-chat.ts +38 -13
  21. package/src/adapters/openai-responses.ts +129 -9
  22. package/src/adapters/registry.ts +30 -1
  23. package/src/bridge.ts +50 -12
  24. package/src/chat/inbound.ts +1 -0
  25. package/src/claude/agents-inject.ts +3 -2
  26. package/src/cli/dispatch.ts +19 -6
  27. package/src/cli/help.ts +2 -1
  28. package/src/cli/integrations.ts +35 -0
  29. package/src/cli/minimax.ts +8 -2
  30. package/src/cli/registry.ts +13 -2
  31. package/src/clients/config-export.ts +120 -1
  32. package/src/codex/account-store.ts +17 -1
  33. package/src/codex/auth-api.ts +51 -17
  34. package/src/codex/catalog/effort.ts +8 -5
  35. package/src/codex/catalog/provider-fetch.ts +39 -27
  36. package/src/codex/catalog/sync.ts +30 -8
  37. package/src/codex/inject.ts +39 -13
  38. package/src/codex/main-account.ts +29 -1
  39. package/src/codex/plan-from-token.ts +140 -0
  40. package/src/codex/plan.ts +25 -0
  41. package/src/codex/prompt-journal.ts +6 -2
  42. package/src/codex/quota-rejection.ts +21 -7
  43. package/src/codex/refresh.ts +4 -2
  44. package/src/codex/sync.ts +106 -3
  45. package/src/codex/warmup.ts +187 -81
  46. package/src/config.ts +85 -46
  47. package/src/generated/compatibility-version.json +127 -83
  48. package/src/grok/inject.ts +1 -1
  49. package/src/images/loop.ts +1 -0
  50. package/src/integrations/registry.ts +7 -0
  51. package/src/lab/automation/config-persistence.ts +2 -2
  52. package/src/lab/automation/persistence.ts +2 -2
  53. package/src/lab/ledger/purge.ts +2 -2
  54. package/src/lab/subject/behavior-fingerprint.ts +2 -2
  55. package/src/lib/config-ownership.ts +5 -2
  56. package/src/lib/destination-policy.ts +18 -1
  57. package/src/lib/provider-outbound.ts +7 -0
  58. package/src/lib/redact.ts +11 -0
  59. package/src/lib/tool-argument-integers.ts +50 -6
  60. package/src/lib/upstream-http-version.ts +57 -0
  61. package/src/lib/upstream-retry.ts +2 -1
  62. package/src/lib/windows-atomic-replace.ts +155 -0
  63. package/src/lib/windows-service-wrappers.ts +72 -0
  64. package/src/oauth/google-antigravity.ts +2 -2
  65. package/src/oauth/index.ts +60 -8
  66. package/src/providers/antigravity-models.ts +58 -4
  67. package/src/providers/command-code-efforts.ts +36 -9
  68. package/src/providers/context-cap.ts +9 -0
  69. package/src/providers/derive.ts +4 -0
  70. package/src/providers/fastwire.ts +453 -0
  71. package/src/providers/registry.ts +21 -1
  72. package/src/providers/service-tier.ts +173 -89
  73. package/src/responses/parser.ts +25 -16
  74. package/src/responses/reasoning-replay-cache.ts +30 -0
  75. package/src/responses/thought-signature-replay.ts +74 -4
  76. package/src/router.ts +6 -0
  77. package/src/routing/compatibility/behavior.ts +27 -16
  78. package/src/server/index.ts +10 -1
  79. package/src/server/management/oauth-account-routes.ts +10 -2
  80. package/src/server/management/shared.ts +1 -1
  81. package/src/server/management/system-routes.ts +15 -0
  82. package/src/server/request-log.ts +57 -3
  83. package/src/server/responses/agent-task-recovery.ts +6 -1
  84. package/src/server/responses/core.ts +209 -40
  85. package/src/server/responses/fetch-helpers.ts +15 -36
  86. package/src/server/responses/responses-field-backfill.ts +173 -0
  87. package/src/server/responses-reasoning-summary-rewrite.ts +171 -0
  88. package/src/service.ts +38 -36
  89. package/src/storage/cleanup.ts +2 -2
  90. package/src/tray/windows.ts +3 -2
  91. package/src/types.ts +101 -14
  92. package/src/update/job.ts +9 -18
  93. package/src/update/transactional-install.d.mts +22 -0
  94. package/src/update/transactional-install.mjs +259 -0
  95. package/src/usage/cost.ts +34 -5
  96. package/src/usage/log.ts +74 -4
  97. package/src/vision/anthropic-describe.ts +10 -6
  98. package/src/web-search/anthropic-executor.ts +8 -6
  99. package/gui/dist/assets/index-DxJ7kXj9.js +0 -102
@@ -1,116 +1,190 @@
1
1
  import type { OcxProviderConfig } from "../types";
2
- import { MODEL_ADAPTER_OVERRIDE_ALLOWED } from "../types";
3
- import { getProviderRegistryEntry, providerModelWireDefault, type InboundWire } from "./registry";
2
+ import { captureWireAdapterHardPins } from "../types";
3
+ import { isCanonicalOpenAiForwardProvider } from "./openai-tiers";
4
+ import {
5
+ getProviderRegistryEntry,
6
+ providerMatchesRegistryTransport,
7
+ type InboundWire,
8
+ type ModelWireDefault,
9
+ } from "./registry";
10
+ import {
11
+ cloneFastWire,
12
+ resolveFastPolicy,
13
+ resolveProviderAuthTransport,
14
+ type FastPolicyAuthority,
15
+ type ResolvedFastPolicy,
16
+ } from "./fastwire";
4
17
 
5
18
  /** OpenAI-compatible adapters that can carry the standard `service_tier` field. */
6
19
  export const SERVICE_TIER_ADAPTERS = new Set(["openai-chat", "openai-responses"]);
7
20
 
8
- export type CapturedServiceTierAdapterAuthority = Readonly<Record<string, string>>;
21
+ /** @deprecated A1 evolves this snapshot into the complete FastPolicyAuthority. */
22
+ export type CapturedServiceTierAdapterAuthority = FastPolicyAuthority;
9
23
 
10
- const capturedAdapterAuthority = new WeakMap<object, CapturedServiceTierAdapterAuthority>();
24
+ const capturedFastPolicyAuthorities = new WeakMap<object, FastPolicyAuthority>();
11
25
 
12
26
  type ServiceTierCapabilityProvider = Pick<
13
27
  OcxProviderConfig,
14
- "adapter" | "supportsServiceTier" | "modelSupportsServiceTier" | "modelAdapters" | "baseUrl" | "authMode" | "chatServiceTier"
28
+ | "adapter"
29
+ | "supportsServiceTier"
30
+ | "modelSupportsServiceTier"
31
+ | "modelAdapters"
32
+ | "baseUrl"
33
+ | "authMode"
34
+ | "apiKeyTransport"
35
+ | "chatServiceTier"
36
+ | "fastWire"
15
37
  >;
16
38
 
39
+ function cloneRegistryWireDefaults(
40
+ defaults: Readonly<Record<string, ModelWireDefault>> | undefined,
41
+ ): Readonly<Record<string, ModelWireDefault>> {
42
+ if (!defaults) return Object.freeze({});
43
+ const clone: Record<string, ModelWireDefault> = {};
44
+ for (const [modelId, declaration] of Object.entries(defaults)) {
45
+ clone[modelId.trim().toLowerCase()] = typeof declaration === "string"
46
+ ? declaration
47
+ : Object.freeze({ wire: declaration.wire, inbound: Object.freeze([...declaration.inbound]) });
48
+ }
49
+ return Object.freeze(clone);
50
+ }
51
+
17
52
  /**
18
- * Read a model map by exact model identity. Service-tier capability is deliberately
19
- * stricter than the older model metadata maps: a family key or a colon-qualified
20
- * fallback must not silently advertise Fast for a sibling model that was never verified.
21
- * A case-insensitive exact match keeps hand-edited ids consistent with the other maps
22
- * without widening the model scope.
53
+ * Capture every registry-owned input before an asynchronous catalog flight begins.
54
+ * The resolver itself is pure and never reads the live provider registry.
23
55
  */
24
- function exactModelValue<T>(
25
- record: Record<string, T> | undefined,
26
- modelId: string,
27
- ): T | undefined {
28
- if (!record) return undefined;
29
- if (Object.prototype.hasOwnProperty.call(record, modelId)) return record[modelId];
30
- const folded = modelId.toLowerCase();
31
- for (const [key, value] of Object.entries(record)) {
32
- if (key.toLowerCase() === folded) return value;
56
+ function buildFastPolicyAuthority(
57
+ providerName: string,
58
+ provider: ServiceTierCapabilityProvider,
59
+ registryTransportMatch: boolean,
60
+ ): FastPolicyAuthority {
61
+ const registry = registryTransportMatch ? getProviderRegistryEntry(providerName) : undefined;
62
+ const authority: FastPolicyAuthority = Object.freeze({
63
+ providerAdapter: provider.adapter,
64
+ fastWireDeclaration: cloneFastWire(
65
+ provider.fastWire !== undefined ? provider.fastWire : registry?.fastWire,
66
+ { freeze: true },
67
+ ),
68
+ modelWireOverrideAllowed: !isCanonicalOpenAiForwardProvider(provider as OcxProviderConfig),
69
+ authTransport: resolveProviderAuthTransport(
70
+ provider.adapter,
71
+ provider.authMode ?? registry?.authKind ?? "key",
72
+ provider.apiKeyTransport,
73
+ ),
74
+ capability: Object.freeze({
75
+ ...(provider.supportsServiceTier !== undefined ? { provider: provider.supportsServiceTier } : {}),
76
+ models: Object.freeze({ ...(provider.modelSupportsServiceTier ?? {}) }),
77
+ ...(provider.chatServiceTier !== undefined ? { chatServiceTier: provider.chatServiceTier } : {}),
78
+ }),
79
+ modelAdapters: Object.freeze({ ...(provider.modelAdapters ?? {}) }),
80
+ hardPins: captureWireAdapterHardPins(providerName),
81
+ registryWireDefaults: cloneRegistryWireDefaults(registry?.modelWireDefaults),
82
+ });
83
+ return authority;
84
+ }
85
+
86
+ export function captureFastPolicyAuthority(
87
+ providerName: string,
88
+ provider: ServiceTierCapabilityProvider,
89
+ registryTransportMatch: boolean,
90
+ ): FastPolicyAuthority {
91
+ const authority = buildFastPolicyAuthority(providerName, provider, registryTransportMatch);
92
+ if (Object.isFrozen(provider)) capturedFastPolicyAuthorities.set(provider, authority);
93
+ return authority;
94
+ }
95
+
96
+ /** @deprecated Use captureFastPolicyAuthority. The legacy inbound argument is now snapshot data. */
97
+ export function captureServiceTierAdapterAuthority(
98
+ providerName: string,
99
+ provider: ServiceTierCapabilityProvider,
100
+ registryTransportMatch: boolean,
101
+ _inbound: InboundWire = "responses",
102
+ ): FastPolicyAuthority {
103
+ return captureFastPolicyAuthority(providerName, provider, registryTransportMatch);
104
+ }
105
+
106
+ function authorityForProvider(
107
+ provider: ServiceTierCapabilityProvider,
108
+ providerName?: string,
109
+ ): FastPolicyAuthority {
110
+ // Preserve the legacy no-name short circuit: serviceTierSupportForModel() used the
111
+ // provider adapter directly when no provider identity was available, so no configured
112
+ // override, hard pin, or registry default may participate on this path in A1.
113
+ if (providerName === undefined) {
114
+ const authority = buildFastPolicyAuthority("", provider, false);
115
+ return Object.freeze({
116
+ ...authority,
117
+ modelAdapters: Object.freeze({}),
118
+ hardPins: Object.freeze({}),
119
+ registryWireDefaults: Object.freeze({}),
120
+ });
33
121
  }
34
- return undefined;
122
+ const captured = Object.isFrozen(provider)
123
+ ? capturedFastPolicyAuthorities.get(provider)
124
+ : undefined;
125
+ if (captured) return captured;
126
+ const registryTransportMatch = providerMatchesRegistryTransport(providerName, provider);
127
+ const authority = buildFastPolicyAuthority(providerName, provider, registryTransportMatch);
128
+ // Frozen provider snapshots cannot drift, so repeated catalog/runtime projections may safely
129
+ // reuse the registry lookup and detached declaration maps. Mutable configs still rebuild.
130
+ if (Object.isFrozen(provider)) capturedFastPolicyAuthorities.set(provider, authority);
131
+ return authority;
132
+ }
133
+
134
+ /** Resolve the pure Fast policy for a provider/model pair. */
135
+ export function fastPolicyForModel(
136
+ provider: ServiceTierCapabilityProvider,
137
+ modelId: string,
138
+ providerName?: string,
139
+ inbound: InboundWire = "responses",
140
+ ): ResolvedFastPolicy {
141
+ return resolveFastPolicy(authorityForProvider(provider, providerName), modelId, inbound);
35
142
  }
36
143
 
37
144
  /**
38
- * Resolve the declared provider/model capability. An explicit provider-level false is a
39
- * fail-closed boundary and cannot be reopened by a model map. Otherwise an exact model
40
- * declaration wins over the provider default, including an explicit false. The resolver is
41
- * provider-local: the caller must first resolve the final provider, so identical bare model ids
42
- * on two providers cannot share capability state.
145
+ * Resolve the declared provider/model capability without applying wire availability.
146
+ * Kept as a public compatibility helper for callers that need the pure tri-state.
43
147
  */
44
148
  export function supportsServiceTierForModel(
45
149
  provider: Pick<OcxProviderConfig, "supportsServiceTier" | "modelSupportsServiceTier">,
46
150
  modelId: string,
47
151
  ): boolean | undefined {
48
- if (provider.supportsServiceTier === false) return false;
49
- return exactModelValue(provider.modelSupportsServiceTier, modelId)
50
- ?? provider.supportsServiceTier;
152
+ const authority: FastPolicyAuthority = {
153
+ providerAdapter: "openai-responses",
154
+ fastWireDeclaration: undefined,
155
+ modelWireOverrideAllowed: true,
156
+ authTransport: "authorization_bearer",
157
+ capability: {
158
+ ...(provider.supportsServiceTier !== undefined ? { provider: provider.supportsServiceTier } : {}),
159
+ models: provider.modelSupportsServiceTier ?? {},
160
+ },
161
+ modelAdapters: {},
162
+ hardPins: {},
163
+ registryWireDefaults: {},
164
+ };
165
+ return resolveFastPolicy(authority, modelId).capability;
51
166
  }
52
167
 
53
- /** Whether the Chat serializer may emit a tier for this exact model. */
54
- export function canSerializeServiceTierForChatModel(
168
+ /** Whether a Chat route may forward an arbitrary caller tier rather than canonical Fast. */
169
+ export function canForwardForeignServiceTierForChatModel(
55
170
  provider: Pick<OcxProviderConfig, "supportsServiceTier" | "modelSupportsServiceTier" | "chatServiceTier">,
56
171
  modelId: string,
57
172
  ): boolean {
58
- const exact = exactModelValue(provider.modelSupportsServiceTier, modelId);
59
- if (provider.supportsServiceTier === false || exact === false) return false;
60
- return provider.chatServiceTier === true || exact === true;
173
+ const capability = supportsServiceTierForModel(provider, modelId);
174
+ return capability !== false && provider.chatServiceTier === true;
61
175
  }
62
176
 
63
- /** Capture registry-owned model wire defaults before an asynchronous catalog flight begins. */
64
- export function captureServiceTierAdapterAuthority(
65
- providerName: string,
66
- provider: Pick<OcxProviderConfig, "adapter" | "baseUrl" | "authMode">,
67
- registryTransportMatch: boolean,
68
- inbound: InboundWire = "responses",
69
- ): CapturedServiceTierAdapterAuthority {
70
- const authority: Record<string, string> = {};
71
- const defaults = registryTransportMatch
72
- ? getProviderRegistryEntry(providerName)?.modelWireDefaults
73
- : undefined;
74
- for (const modelId of Object.keys(defaults ?? {})) {
75
- const adapter = providerModelWireDefault(
76
- providerName,
77
- provider,
78
- modelId,
79
- MODEL_ADAPTER_OVERRIDE_ALLOWED,
80
- inbound,
81
- );
82
- if (adapter !== undefined) authority[modelId.trim().toLowerCase()] = adapter;
83
- }
84
- const frozen = Object.freeze(authority);
85
- capturedAdapterAuthority.set(provider, frozen);
86
- return frozen;
87
- }
88
-
89
- /** Resolve an explicit model wire override for catalog-time capability projection. */
177
+ /** Final adapter selected by the Fast policy's four-level wire resolver. */
90
178
  export function serviceTierAdapterForModel(
91
179
  providerName: string,
92
- provider: Pick<OcxProviderConfig, "adapter" | "baseUrl" | "authMode" | "modelAdapters">,
180
+ provider: ServiceTierCapabilityProvider,
93
181
  modelId: string,
94
182
  inbound: InboundWire = "responses",
95
183
  ): string {
96
- // Keep this lookup identical to resolveWireProtocolOverride(): configured model-adapter
97
- // entries are exact-case keys, while registry defaults intentionally normalize ids there.
98
- const configured = provider.modelAdapters?.[modelId];
99
- if (configured !== undefined && MODEL_ADAPTER_OVERRIDE_ALLOWED.has(configured)) return configured;
100
- const captured = capturedAdapterAuthority.get(provider);
101
- if (captured !== undefined) {
102
- return captured[modelId.trim().toLowerCase()] ?? provider.adapter;
103
- }
104
- return providerModelWireDefault(
105
- providerName,
106
- provider,
107
- modelId,
108
- MODEL_ADAPTER_OVERRIDE_ALLOWED,
109
- inbound,
110
- ) ?? provider.adapter;
184
+ return fastPolicyForModel(provider, modelId, providerName, inbound).adapter;
111
185
  }
112
186
 
113
- /** Whether the final provider/model pair can actually publish/send OpenAI service tiers. */
187
+ /** Whether the final provider/model pair can publish/send OpenAI service tiers. */
114
188
  export function canForwardServiceTierForModel(
115
189
  provider: ServiceTierCapabilityProvider,
116
190
  modelId: string,
@@ -121,9 +195,8 @@ export function canForwardServiceTierForModel(
121
195
  }
122
196
 
123
197
  /**
124
- * Return the tri-state capability after resolving the model's final wire adapter.
125
- * `false` means either an explicit provider/model denial or an adapter that cannot carry the
126
- * field; `undefined` keeps the existing conservative contract for an unclassified OpenAI wire.
198
+ * Compatibility projection for catalog, routing, and fingerprint consumers. The new
199
+ * resolver carries richer eligibility internally while preserving the old tri-state bytes.
127
200
  */
128
201
  export function serviceTierSupportForModel(
129
202
  provider: ServiceTierCapabilityProvider,
@@ -131,13 +204,24 @@ export function serviceTierSupportForModel(
131
204
  providerName?: string,
132
205
  inbound: InboundWire = "responses",
133
206
  ): boolean | undefined {
134
- const adapter = providerName === undefined
135
- ? provider.adapter
136
- : serviceTierAdapterForModel(providerName, provider, modelId, inbound);
137
- if (!SERVICE_TIER_ADAPTERS.has(adapter)) return false;
138
- // Treat the Chat serializer decision as authoritative so catalog metadata, routing
139
- // evidence, fast-mode injection, and caller-tier stripping cannot claim support that the
140
- // final request builder will omit. A provider-wide false and an exact false stay closed.
141
- if (adapter === "openai-chat" && !canSerializeServiceTierForChatModel(provider, modelId)) return false;
142
- return supportsServiceTierForModel(provider, modelId);
207
+ const policy = fastPolicyForModel(provider, modelId, providerName, inbound);
208
+ return serviceTierSupportFromPolicy(policy);
209
+ }
210
+
211
+ /** Compatibility projection shared by catalog, routing, and request logging. */
212
+ export function serviceTierSupportFromPolicy(
213
+ policy: Pick<ResolvedFastPolicy, "eligibility" | "adapter" | "forwardCallerTier">,
214
+ ): boolean | undefined {
215
+ if (policy.eligibility === "eligible") return true;
216
+ if (policy.eligibility === "unclassified") {
217
+ // B1 regression guard: an unclassified chat-wire route whose final adapter will not
218
+ // forward any tier cannot serialize service_tier, so projecting "unknown" would let
219
+ // require.serviceTier: "unsupported" routing stop matching groq/ollama-class providers
220
+ // that main projected as false. Chat + no forwarding stays a definitive false; a
221
+ // chat route with chatServiceTier: true (forwarding allowed) keeps the historical
222
+ // unknown, as does every unclassified Responses-wire route.
223
+ if (policy.adapter === "openai-chat" && !policy.forwardCallerTier) return false;
224
+ return undefined;
225
+ }
226
+ return false;
143
227
  }
@@ -169,6 +169,23 @@ function buildTools(tools: unknown[] | undefined): OcxTool[] | undefined {
169
169
  if (namespace) tool.namespace = namespace;
170
170
  out.push(tool);
171
171
  };
172
+ const pushCustom = (t: Record<string, unknown>, namespace?: string) => {
173
+ // Freeform custom tools are lowered to a single string `input` because chat models cannot
174
+ // emit Responses grammar payloads directly. Keep tool-specific input guidance scoped to the
175
+ // tool that owns it: leaking apply_patch syntax into `exec` or another freeform tool teaches
176
+ // routed models that the nested helper name is itself a callable top-level tool.
177
+ const inputDescription = t.name === "apply_patch"
178
+ ? "Raw tool input. For apply_patch, begin exactly with `*** Begin Patch` (no trailing `***`), then use its standard patch envelope."
179
+ : "Raw freeform input for this tool.";
180
+ const tool: OcxTool = {
181
+ name: t.name as string,
182
+ description: (t.description as string) ?? "",
183
+ parameters: { type: "object", properties: { input: { type: "string", description: inputDescription } }, required: ["input"] },
184
+ freeform: true,
185
+ };
186
+ if (namespace) tool.namespace = namespace;
187
+ out.push(tool);
188
+ };
172
189
  for (const t of tools) {
173
190
  if (!isObj(t)) continue;
174
191
  if (t.type === "function" && isObj(t.function) && typeof t.function.name === "string" && t.function.name.length > 0) {
@@ -178,27 +195,19 @@ function buildTools(tools: unknown[] | undefined): OcxTool[] | undefined {
178
195
  if (t.type === "function" && typeof t.name === "string") {
179
196
  pushFn(t);
180
197
  } else if (t.type === "namespace" && Array.isArray(t.tools)) {
181
- // MCP tools arrive grouped under a namespace tool; flatten the inner function tools so
182
- // chat-completions models receive them (round-trip restores the namespace in the bridge).
183
- const ns = typeof t.name === "string" ? t.name : undefined;
198
+ // Codex 0.147 groups its ordinary client tools under the reserved `functions` namespace,
199
+ // including freeform custom tools such as code-mode `exec`. Those children are still
200
+ // top-level Responses tools, so flatten them without a namespace. Other namespace groups
201
+ // are MCP-style and keep their namespace for round-trip routing.
202
+ const builtinFunctions = t.name === "functions";
203
+ const ns = typeof t.name === "string" && !builtinFunctions ? t.name : undefined;
184
204
  for (const inner of t.tools as unknown[]) {
185
205
  if (isObj(inner) && inner.type === "function" && typeof inner.name === "string") pushFn(inner, ns);
206
+ else if (builtinFunctions && isObj(inner) && inner.type === "custom" && typeof inner.name === "string") pushCustom(inner);
186
207
  }
187
208
  }
188
209
  else if (t.type === "custom" && typeof t.name === "string") {
189
- // Freeform custom tools are lowered to a single string `input` because chat models cannot
190
- // emit Responses grammar payloads directly. Keep tool-specific input guidance scoped to the
191
- // tool that owns it: leaking apply_patch syntax into `exec` or another freeform tool teaches
192
- // routed models that the nested helper name is itself a callable top-level tool.
193
- const inputDescription = t.name === "apply_patch"
194
- ? "Raw tool input. For apply_patch, begin exactly with `*** Begin Patch` (no trailing `***`), then use its standard patch envelope."
195
- : "Raw freeform input for this tool.";
196
- out.push({
197
- name: t.name,
198
- description: (t.description as string) ?? "",
199
- parameters: { type: "object", properties: { input: { type: "string", description: inputDescription } }, required: ["input"] },
200
- freeform: true,
201
- });
210
+ pushCustom(t);
202
211
  }
203
212
  else if (t.type === "tool_search") {
204
213
  // Client-executed tool discovery — the gateway to deferred tools (subagents, extra MCP tools).
@@ -123,6 +123,36 @@ export function durableReplayDestinationIdentity(baseUrl: string | undefined): s
123
123
  return `destination:${createHash("sha256").update("destination\0").update(canonical).digest("hex")}`;
124
124
  }
125
125
 
126
+ /**
127
+ * Restart-stable credential identity for the DURABLE thought-signature store (#1926).
128
+ *
129
+ * Unlike the destination, credential material may be secret (an API key), so a plain
130
+ * unsalted digest would turn the store file into an offline verifier for candidate keys.
131
+ * The identity is therefore an HMAC under a random salt persisted NEXT TO the store: the
132
+ * salt is not a secret escrow (it holds no credential material) but it makes every digest
133
+ * useless outside this installation. Full 256-bit output — no truncation.
134
+ *
135
+ * OAuth accounts use the persisted account-slot id (not the rotating token/generation):
136
+ * relinking a slot to a different upstream account keeps the id, but the upstream then
137
+ * validates signatures against the new credential and rejects stale ones — the same
138
+ * fail-closed backstop the destination identity relies on. Credential-scoped header
139
+ * overrides participate so two provider entries sharing one key but different
140
+ * authorization headers stay distinct, mirroring the process-local identity.
141
+ */
142
+ export function durableReplayCredentialIdentity(
143
+ kind: "key" | "oauth" | "codex",
144
+ material: string | undefined,
145
+ headers: Record<string, string> | undefined,
146
+ salt: Buffer | undefined,
147
+ ): string | undefined {
148
+ if (!nonEmpty(material) || !salt || salt.length < 16) return undefined;
149
+ const overrides = credentialHeaderOverrides(headers);
150
+ return `credential:${createHmac("sha256", salt)
151
+ .update(`credential\0${kind}\0`)
152
+ .update(JSON.stringify([material, overrides]))
153
+ .digest("hex")}`;
154
+ }
155
+
126
156
  /** Produce a non-reversible process-local identity for credential material. */
127
157
  export function reasoningReplayCredentialIdentity(
128
158
  kind: "key" | "oauth" | "codex",
@@ -20,18 +20,23 @@
20
20
  * destination, adapter, model and credential — so a signature can only ever be replayed into
21
21
  * the turn that produced it.
22
22
  */
23
- import { readFileSync } from "node:fs";
23
+ import { chmodSync, readFileSync, writeFileSync } from "node:fs";
24
+ import { randomBytes } from "node:crypto";
24
25
  import { join } from "node:path";
25
26
  import { atomicWriteFileAsync, getConfigDir } from "../config";
26
27
  import type { OcxProviderOpaqueToolCallMetadata, OcxReasoningReplayScopeRef } from "../types";
27
28
  import { isCarryableSignature, responsesExtraContentFromProviderMetadata } from "./provider-opaque-metadata";
28
29
 
29
30
  const STORE_FILE_NAME = "thought-signature-replay.json";
31
+ const SALT_FILE_NAME = "thought-signature-replay.salt";
30
32
  /**
31
- * Bumped whenever `keyFor` changes shape. v3 added the durable destination identity, so a
32
- * v2 file's keys can never match and are dropped on load instead of aging out invisibly.
33
+ * Bumped whenever `keyFor` changes shape. v3 added the durable destination identity; v4
34
+ * added the salted durable credential identity (#1926), so a v3 file's keys can never
35
+ * match and are dropped on load instead of aging out invisibly. v3 rows carried no
36
+ * credential information, so they are not upgradable — the next Gemini turn re-accumulates
37
+ * its signatures (bounded, best-effort loss identical to the pre-store status quo).
33
38
  */
34
- const STORE_VERSION = 3;
39
+ const STORE_VERSION = 4;
35
40
 
36
41
  /** Bound on remembered entries; real signatures are a few hundred bytes, so this stays small. */
37
42
  const MAX_ENTRIES = 16_384;
@@ -64,6 +69,45 @@ function storePath(): string {
64
69
  return join(getConfigDir(), STORE_FILE_NAME);
65
70
  }
66
71
 
72
+ function saltPath(): string {
73
+ return join(getConfigDir(), SALT_FILE_NAME);
74
+ }
75
+
76
+ let cachedSalt: Buffer | undefined;
77
+ let saltLoaded = false;
78
+
79
+ /**
80
+ * Installation-local salt for the durable credential identity (#1926). Created once and
81
+ * persisted beside the store; losing it invalidates every stored key (the entries then
82
+ * never match and age out), which is safe — signatures re-accumulate per turn.
83
+ */
84
+ export function thoughtSignatureReplaySalt(): Buffer | undefined {
85
+ if (saltLoaded) return cachedSalt;
86
+ saltLoaded = true;
87
+ try {
88
+ const raw = readFileSync(saltPath());
89
+ if (raw.length >= 16) {
90
+ // Re-assert owner-only permissions on every load: a pre-existing file may have
91
+ // been created before this guard or loosened by external tooling.
92
+ try { chmodSync(saltPath(), 0o600); } catch { /* best effort on exotic filesystems */ }
93
+ cachedSalt = raw;
94
+ return cachedSalt;
95
+ }
96
+ } catch {
97
+ // fall through to mint
98
+ }
99
+ try {
100
+ const minted = randomBytes(32);
101
+ writeFileSync(saltPath(), minted, { mode: 0o600 });
102
+ cachedSalt = minted;
103
+ } catch {
104
+ // Unwritable config dir: no durable credential identity this process; the durable
105
+ // store fails closed (keyFor returns undefined) rather than keying under a shared id.
106
+ cachedSalt = undefined;
107
+ }
108
+ return cachedSalt;
109
+ }
110
+
67
111
  function nonEmpty(value: unknown): value is string {
68
112
  return typeof value === "string" && value.trim().length > 0;
69
113
  }
@@ -84,6 +128,10 @@ function keyFor(callId: string, scope: OcxReasoningReplayScopeRef | undefined):
84
128
  || !nonEmpty(identity?.providerName)
85
129
  || !nonEmpty(identity?.adapterName)
86
130
  || !nonEmpty(identity?.modelId)
131
+ // v4 (#1926): a missing durable credential identity means we cannot isolate this
132
+ // entry per credential across restarts. Refusing the key is the fail-closed choice —
133
+ // "credential:unknown" would let two different credentials share one durable slot.
134
+ || !nonEmpty(identity?.credentialDurableIdentity)
87
135
  ) return undefined;
88
136
  return JSON.stringify([
89
137
  scope.clientThreadId,
@@ -93,6 +141,7 @@ function keyFor(callId: string, scope: OcxReasoningReplayScopeRef | undefined):
93
141
  // reasoning cache's randomBytes-keyed HMAC cannot. Without it, one provider NAME
94
142
  // serving two endpoints shares signatures across both.
95
143
  identity.providerDestinationDurableIdentity ?? "destination:unknown",
144
+ identity.credentialDurableIdentity,
96
145
  identity.adapterName,
97
146
  identity.modelId,
98
147
  callId,
@@ -265,6 +314,8 @@ export function resetThoughtSignatureReplayForTests(): void {
265
314
  totalBytes = 0;
266
315
  loaded = false;
267
316
  persistChain = Promise.resolve();
317
+ cachedSalt = undefined;
318
+ saltLoaded = false;
268
319
  }
269
320
 
270
321
  export function thoughtSignatureReplayCountForTests(): number {
@@ -275,3 +326,22 @@ export function thoughtSignatureReplayCountForTests(): number {
275
326
  export function flushThoughtSignatureReplayForTests(): Promise<void> {
276
327
  return persistChain;
277
328
  }
329
+
330
+ /**
331
+ * Bounded commit barrier (#1926 gap 2). All durable writes serialize onto
332
+ * `persistChain`, so awaiting it (with a cap) before a turn's terminal frame becomes
333
+ * externally visible bounds the restart window in which a handed-out signature could
334
+ * be lost. Bounded BEST EFFORT: on timeout the turn proceeds — availability wins, and
335
+ * the miss cost is one turn's re-accumulation (the pre-store status quo). The
336
+ * in-memory map is updated synchronously at remember() time, so within one process
337
+ * lifetime replay never races this barrier at all.
338
+ */
339
+ export function awaitThoughtSignatureDurability(capMs = 250): Promise<void> {
340
+ let timer: ReturnType<typeof setTimeout> | undefined;
341
+ const cap = new Promise<void>(resolve => {
342
+ timer = setTimeout(resolve, capMs);
343
+ });
344
+ return Promise.race([persistChain, cap]).then(() => {
345
+ if (timer !== undefined) clearTimeout(timer);
346
+ });
347
+ }
package/src/router.ts CHANGED
@@ -13,6 +13,7 @@ import { assertProviderDestinationAllowed } from "./lib/destination-policy";
13
13
  import { redactSecretString, redactUrlForLog } from "./lib/redact";
14
14
  import { PROVIDER_REGISTRY, providerCodexAccountMode } from "./providers/registry";
15
15
  import { applyDirectReasoningEffortContracts, hasLegacyClinePassReasoningEfforts } from "./providers/derive";
16
+ import { cloneFastWire } from "./providers/fastwire";
16
17
  import {
17
18
  providerMatchesRegistryTransportWithStaticGuards,
18
19
  providerSupportsLiveModelDiscovery,
@@ -333,6 +334,11 @@ export function routedProviderConfig(providerName: string, provider: OcxProvider
333
334
  && registryEntry.requiresAdjacentResponsesToolResults !== undefined
334
335
  ? { requiresAdjacentResponsesToolResults: registryEntry.requiresAdjacentResponsesToolResults }
335
336
  : {}),
337
+ ...(provider.fastWire === undefined && registryEntry.fastWire !== undefined
338
+ ? {
339
+ fastWire: cloneFastWire(registryEntry.fastWire),
340
+ }
341
+ : {}),
336
342
  ...(provider.supportsServiceTier === undefined && registryEntry.supportsServiceTier !== undefined
337
343
  ? { supportsServiceTier: registryEntry.supportsServiceTier }
338
344
  : {}),
@@ -1,6 +1,8 @@
1
+ import { modelInList } from "../../types";
1
2
  import type { OcxConfig, OcxProviderConfig } from "../../types";
2
- import { PROVIDER_REGISTRY, type ProviderAuthKind } from "../../providers/registry";
3
- import { serviceTierSupportForModel } from "../../providers/service-tier";
3
+ import { PROVIDER_REGISTRY } from "../../providers/registry";
4
+ import { fastPolicyForModel, serviceTierSupportForModel } from "../../providers/service-tier";
5
+ import { resolveProviderAuthTransport } from "../../providers/fastwire";
4
6
  import { localFingerprint } from "../../lab/digest";
5
7
  import type { LabBehaviorSource, LabBehaviorValues } from "../../lab/live/types";
6
8
 
@@ -36,8 +38,17 @@ function behaviorRow(source: LabBehaviorSource, value: unknown) {
36
38
  return { source, value };
37
39
  }
38
40
 
41
+ /**
42
+ * Membership for the provider's `no*Models`-style lists.
43
+ *
44
+ * Delegates to modelInList so the report matches the wire: every runtime gate these
45
+ * rows describe (openai-chat's sampling/reasoning/tool-choice gates, reasoning-effort's
46
+ * noReasoningModels) matches through modelInList, which also accepts a bare entry for a
47
+ * tagged id. ollama-cloud serves `gpt-oss:120b` and lists the bare `gpt-oss`, so an
48
+ * exact-only check here reported "temperature is sent" on a request that omits it.
49
+ */
39
50
  function includesModel(list: string[] | undefined, modelId: string): boolean {
40
- return Array.isArray(list) && list.includes(modelId);
51
+ return modelInList(list, modelId);
41
52
  }
42
53
 
43
54
  function modelValue<T>(map: Record<string, T> | undefined, modelId: string): T | undefined {
@@ -57,18 +68,6 @@ function nonCredentialHeaderDigest(
57
68
  return localFingerprint("nonCredentialHeaders", rows, installationSalt);
58
69
  }
59
70
 
60
- function authTransportFor(
61
- effective: OcxProviderConfig,
62
- adapter: string,
63
- mode: ProviderAuthKind,
64
- ): string {
65
- if (mode === "oauth") return "oauth_bearer";
66
- if (mode === "forward") return "forwarded_authorization";
67
- if (mode === "local") return "none";
68
- if (adapter === "anthropic" && effective.apiKeyTransport !== "bearer") return "x_api_key";
69
- return "authorization_bearer";
70
- }
71
-
72
71
  function effectiveOpenRouterRouting(effective: OcxProviderConfig, modelId: string) {
73
72
  return effective.modelOpenRouterRouting?.[modelId] ?? effective.openRouterRouting;
74
73
  }
@@ -104,6 +103,7 @@ export function resolveProductionBehaviorValues(
104
103
  const project = typeof effective.project === "string" && effective.project ? effective.project : null;
105
104
  const location = typeof effective.location === "string" && effective.location ? effective.location : null;
106
105
  const nativeLocalExec = effective.nativeLocalExec === "on" || effective.unsafeAllowNativeLocalExec === true;
106
+ const fastPolicy = fastPolicyForModel(effective, modelId, providerName);
107
107
 
108
108
  const values: LabBehaviorValues = {
109
109
  "wire.adapter": behaviorRow("provider_config", adapter),
@@ -115,12 +115,23 @@ export function resolveProductionBehaviorValues(
115
115
  effective.modelSuffixBracketStrip === true ? "bracket_strip" : "none",
116
116
  ),
117
117
  "auth.mode": behaviorRow("provider_config", authMode),
118
- "auth.transport": behaviorRow("provider_config", authTransportFor(effective, adapter, authMode)),
118
+ "auth.transport": behaviorRow(
119
+ "provider_config",
120
+ resolveProviderAuthTransport(adapter, authMode, effective.apiKeyTransport),
121
+ ),
119
122
  "responses.stateful": behaviorRow("provider_config", effective.statelessResponses !== true),
120
123
  "responses.serviceTier": behaviorRow(
121
124
  "provider_config",
122
125
  serviceTierSupportForModel(effective, modelId, providerName) ?? null,
123
126
  ),
127
+ "responses.fastWireKind": behaviorRow(
128
+ "provider_config",
129
+ fastPolicy.fastWire?.kind ?? null,
130
+ ),
131
+ "responses.fastWireValue": behaviorRow(
132
+ "provider_config",
133
+ fastPolicy.fastWire?.canonicalToWire.priority ?? null,
134
+ ),
124
135
  "responses.snapshotRepair": behaviorRow("provider_config", effective.responsesSnapshotRepair === true),
125
136
  "responses.itemIdRepair": behaviorRow("provider_config", effective.responsesItemIdRepair ?? null),
126
137
  "limits.contextWindow": behaviorRow(