@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.
- package/bin/ocx.mjs +59 -7
- package/gui/dist/assets/index-RL6b1bTV.js +102 -0
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/base.ts +18 -0
- package/src/adapters/client-fingerprint.ts +0 -2
- package/src/adapters/cursor/http1-bidi.ts +361 -0
- package/src/adapters/cursor/live-models.ts +117 -30
- package/src/adapters/cursor/live-transport.ts +137 -56
- package/src/adapters/cursor/native-exec-fs.ts +1 -1
- package/src/adapters/cursor/native-exec-network.ts +1 -1
- package/src/adapters/cursor/native-exec-shell.ts +0 -1
- package/src/adapters/cursor/protobuf-request.ts +89 -21
- package/src/adapters/cursor/tool-definitions.ts +18 -7
- package/src/adapters/cursor/tool-result-normalize.ts +92 -0
- package/src/adapters/cursor/transport.ts +7 -0
- package/src/adapters/cursor.ts +2 -0
- package/src/adapters/google-http.ts +38 -10
- package/src/adapters/google.ts +23 -3
- package/src/adapters/openai-chat.ts +38 -13
- package/src/adapters/openai-responses.ts +129 -9
- package/src/adapters/registry.ts +30 -1
- package/src/bridge.ts +50 -12
- package/src/chat/inbound.ts +1 -0
- package/src/claude/agents-inject.ts +3 -2
- package/src/cli/dispatch.ts +19 -6
- package/src/cli/help.ts +2 -1
- package/src/cli/integrations.ts +35 -0
- package/src/cli/minimax.ts +8 -2
- package/src/cli/registry.ts +13 -2
- package/src/clients/config-export.ts +120 -1
- package/src/codex/account-store.ts +17 -1
- package/src/codex/auth-api.ts +51 -17
- package/src/codex/catalog/effort.ts +8 -5
- package/src/codex/catalog/provider-fetch.ts +39 -27
- package/src/codex/catalog/sync.ts +30 -8
- package/src/codex/inject.ts +39 -13
- package/src/codex/main-account.ts +29 -1
- package/src/codex/plan-from-token.ts +140 -0
- package/src/codex/plan.ts +25 -0
- package/src/codex/prompt-journal.ts +6 -2
- package/src/codex/quota-rejection.ts +21 -7
- package/src/codex/refresh.ts +4 -2
- package/src/codex/sync.ts +106 -3
- package/src/codex/warmup.ts +187 -81
- package/src/config.ts +85 -46
- package/src/generated/compatibility-version.json +127 -83
- package/src/grok/inject.ts +1 -1
- package/src/images/loop.ts +1 -0
- package/src/integrations/registry.ts +7 -0
- package/src/lab/automation/config-persistence.ts +2 -2
- package/src/lab/automation/persistence.ts +2 -2
- package/src/lab/ledger/purge.ts +2 -2
- package/src/lab/subject/behavior-fingerprint.ts +2 -2
- package/src/lib/config-ownership.ts +5 -2
- package/src/lib/destination-policy.ts +18 -1
- package/src/lib/provider-outbound.ts +7 -0
- package/src/lib/redact.ts +11 -0
- package/src/lib/tool-argument-integers.ts +50 -6
- package/src/lib/upstream-http-version.ts +57 -0
- package/src/lib/upstream-retry.ts +2 -1
- package/src/lib/windows-atomic-replace.ts +155 -0
- package/src/lib/windows-service-wrappers.ts +72 -0
- package/src/oauth/google-antigravity.ts +2 -2
- package/src/oauth/index.ts +60 -8
- package/src/providers/antigravity-models.ts +58 -4
- package/src/providers/command-code-efforts.ts +36 -9
- package/src/providers/context-cap.ts +9 -0
- package/src/providers/derive.ts +4 -0
- package/src/providers/fastwire.ts +453 -0
- package/src/providers/registry.ts +21 -1
- package/src/providers/service-tier.ts +173 -89
- package/src/responses/parser.ts +25 -16
- package/src/responses/reasoning-replay-cache.ts +30 -0
- package/src/responses/thought-signature-replay.ts +74 -4
- package/src/router.ts +6 -0
- package/src/routing/compatibility/behavior.ts +27 -16
- package/src/server/index.ts +10 -1
- package/src/server/management/oauth-account-routes.ts +10 -2
- package/src/server/management/shared.ts +1 -1
- package/src/server/management/system-routes.ts +15 -0
- package/src/server/request-log.ts +57 -3
- package/src/server/responses/agent-task-recovery.ts +6 -1
- package/src/server/responses/core.ts +209 -40
- package/src/server/responses/fetch-helpers.ts +15 -36
- package/src/server/responses/responses-field-backfill.ts +173 -0
- package/src/server/responses-reasoning-summary-rewrite.ts +171 -0
- package/src/service.ts +38 -36
- package/src/storage/cleanup.ts +2 -2
- package/src/tray/windows.ts +3 -2
- package/src/types.ts +101 -14
- package/src/update/job.ts +9 -18
- package/src/update/transactional-install.d.mts +22 -0
- package/src/update/transactional-install.mjs +259 -0
- package/src/usage/cost.ts +34 -5
- package/src/usage/log.ts +74 -4
- package/src/vision/anthropic-describe.ts +10 -6
- package/src/web-search/anthropic-executor.ts +8 -6
- package/gui/dist/assets/index-DxJ7kXj9.js +0 -102
|
@@ -1,116 +1,190 @@
|
|
|
1
1
|
import type { OcxProviderConfig } from "../types";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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
|
-
|
|
21
|
+
/** @deprecated A1 evolves this snapshot into the complete FastPolicyAuthority. */
|
|
22
|
+
export type CapturedServiceTierAdapterAuthority = FastPolicyAuthority;
|
|
9
23
|
|
|
10
|
-
const
|
|
24
|
+
const capturedFastPolicyAuthorities = new WeakMap<object, FastPolicyAuthority>();
|
|
11
25
|
|
|
12
26
|
type ServiceTierCapabilityProvider = Pick<
|
|
13
27
|
OcxProviderConfig,
|
|
14
|
-
|
|
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
|
-
*
|
|
19
|
-
*
|
|
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
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
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
|
|
39
|
-
*
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
|
54
|
-
export function
|
|
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
|
|
59
|
-
|
|
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
|
-
/**
|
|
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:
|
|
180
|
+
provider: ServiceTierCapabilityProvider,
|
|
93
181
|
modelId: string,
|
|
94
182
|
inbound: InboundWire = "responses",
|
|
95
183
|
): string {
|
|
96
|
-
|
|
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
|
|
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
|
-
*
|
|
125
|
-
*
|
|
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
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
}
|
package/src/responses/parser.ts
CHANGED
|
@@ -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
|
-
//
|
|
182
|
-
//
|
|
183
|
-
|
|
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
|
-
|
|
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
|
|
32
|
-
*
|
|
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 =
|
|
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
|
|
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
|
|
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(
|
|
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(
|