@bitkyc08/opencodex 2.59.0 → 2.60.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/gui/dist/assets/index-BTuCbqQd.css +1 -0
- package/gui/dist/assets/index-DoBVdPHP.js +134 -0
- package/gui/dist/index.html +2 -2
- package/package.json +4 -1
- package/src/adapters/anthropic-image-codec.ts +16 -2
- package/src/adapters/anthropic-image-normalize.ts +49 -2
- package/src/adapters/anthropic.ts +4 -1
- package/src/adapters/base.ts +23 -0
- package/src/adapters/coding-agent/turn.ts +22 -2
- package/src/adapters/command-code.ts +50 -3
- package/src/adapters/cursor/checkpoint-store.ts +3 -0
- package/src/adapters/cursor/discovery.ts +11 -8
- package/src/adapters/cursor/live-transport.ts +26 -9
- package/src/adapters/cursor/request-builder.ts +7 -1
- package/src/adapters/cursor/transport.ts +19 -0
- package/src/adapters/cursor.ts +3 -1
- package/src/adapters/devin/cloud-direct/chat.ts +3 -1
- package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +42 -5
- package/src/adapters/devin.ts +50 -13
- package/src/adapters/google-antigravity-replay.ts +1 -1
- package/src/adapters/google-antigravity-wire.ts +29 -5
- package/src/adapters/google-http.ts +49 -10
- package/src/adapters/google-tool-schema.ts +595 -31
- package/src/adapters/google-wire-compiler.ts +93 -10
- package/src/adapters/google-wire-shape.ts +461 -0
- package/src/adapters/google.ts +37 -6
- package/src/adapters/openai-chat-images.ts +3 -1
- package/src/adapters/openai-chat.ts +5 -1
- package/src/adapters/openai-responses/image-gen.ts +8 -6
- package/src/adapters/openai-responses/passthrough.ts +15 -3
- package/src/adapters/openai-responses/reasoning.ts +7 -0
- package/src/adapters/opencode-go-additional-tools.ts +12 -2
- package/src/bridge/sse.ts +3 -151
- package/src/cli/account-extended.ts +4 -4
- package/src/cli/dispatch.ts +3 -3
- package/src/cli/doctor.ts +28 -9
- package/src/cli/hub.ts +3 -2
- package/src/cli/index.ts +7 -2
- package/src/cli/opencode.ts +2 -2
- package/src/cli/provider.ts +13 -1
- package/src/client/machine-api.ts +2 -2
- package/src/client/machine-listener.ts +2 -2
- package/src/client/runtime.ts +26 -2
- package/src/codex/account-store.ts +65 -0
- package/src/codex/auth-api/account-list.ts +19 -11
- package/src/codex/auth-api/pool-quota-probe.ts +30 -7
- package/src/codex/catalog/gather-capture.ts +21 -2
- package/src/codex/catalog/model-hints.ts +29 -28
- package/src/codex/catalog/parsing.ts +7 -0
- package/src/codex/catalog/provider-models.ts +19 -2
- package/src/codex/catalog/retained-sync.ts +22 -26
- package/src/codex/catalog/routed-gather.ts +19 -0
- package/src/codex/context-compat.ts +5 -2
- package/src/codex/desired-state.ts +4 -1
- package/src/codex/history-job.ts +6 -6
- package/src/codex/history-provider.ts +20 -166
- package/src/codex/history-rollout-read.ts +174 -0
- package/src/codex/internal/catalog-writer.ts +33 -1
- package/src/codex/model-cache.ts +47 -0
- package/src/codex/model-entitlements.ts +29 -10
- package/src/codex/observed-model-denials.ts +101 -8
- package/src/codex/prompt-text-probe.ts +9 -6
- package/src/codex/routing.ts +7 -1
- package/src/codex/shim.ts +1 -1
- package/src/codex/subagent-model-fallback.ts +22 -4
- package/src/combos/failover.ts +3 -0
- package/src/config/admitted-identity.ts +222 -0
- package/src/config/diagnostics.ts +22 -1
- package/src/config/feature-flags.ts +5 -0
- package/src/config/load-degrade.ts +18 -0
- package/src/config/proxy-env.ts +8 -2
- package/src/config/schema/compaction-triggers.ts +11 -0
- package/src/config/schema/config-schema.ts +4 -0
- package/src/config/schema/leaf-validators.ts +12 -0
- package/src/config.ts +2 -2
- package/src/generated/compatibility-version.json +249 -169
- package/src/grok/reset-coupons.ts +38 -19
- package/src/images/loop.ts +6 -1
- package/src/integrations/aside-profile-context.ts +37 -3
- package/src/integrations/aside-profile-journal.ts +68 -3
- package/src/integrations/aside-profiles.ts +128 -3
- package/src/integrations/mutation-plan.ts +815 -0
- package/src/integrations/writer.ts +85 -99
- package/src/lab/live/transport.ts +4 -0
- package/src/lab/live/types.ts +5 -0
- package/src/lab/subject/behavior-fingerprint.ts +1 -1
- package/src/lib/admin-secrets.ts +9 -1
- package/src/lib/debug-log-buffer.ts +6 -1
- package/src/lib/debug.ts +23 -0
- package/src/lib/errors.ts +79 -0
- package/src/lib/http-response-semantics.ts +57 -0
- package/src/lib/lab-live-pinned-sender.ts +26 -12
- package/src/lib/pinned-http.ts +142 -2
- package/src/lib/plain-data.ts +103 -0
- package/src/lib/process-control.ts +13 -5
- package/src/lib/provider-outbound.ts +50 -2
- package/src/lib/socks5-fetch.ts +136 -26
- package/src/lib/spend-ledger-owner.ts +364 -0
- package/src/lib/spend-reservation-ledger.ts +218 -27
- package/src/lib/windows-system-proxy.ts +16 -11
- package/src/oauth/callback-server.ts +4 -3
- package/src/oauth/generic-account-failover.ts +1 -0
- package/src/oauth/health.ts +12 -1
- package/src/oauth/index.ts +3 -107
- package/src/oauth/login-flow-state.ts +127 -0
- package/src/providers/derive.ts +34 -17
- package/src/providers/devin-cli-authmode-migration.ts +14 -10
- package/src/providers/key-failover.ts +66 -18
- package/src/providers/model-rename-migration.ts +55 -1
- package/src/providers/model-rename-startup.ts +7 -5
- package/src/providers/openai-virtual-models.ts +42 -2
- package/src/providers/quota/antigravity.ts +22 -2
- package/src/providers/quota/vendor-probes-key.ts +1 -1
- package/src/providers/registry/entries-core.ts +39 -17
- package/src/providers/registry/entries-extended.ts +35 -1
- package/src/providers/registry/model-ids.ts +168 -0
- package/src/providers/registry/model-seeds.ts +9 -0
- package/src/providers/registry/types.ts +2 -0
- package/src/providers/resolved-model-policy-merge.ts +167 -0
- package/src/providers/resolved-model-policy.ts +406 -0
- package/src/providers/stale-vision-classification-migration.ts +137 -0
- package/src/responses/apply-patch-envelope.ts +0 -12
- package/src/responses/freeform-wrapper-scan.ts +279 -0
- package/src/responses/legacy-dotted-tool-name-repair.ts +134 -0
- package/src/responses/progressive-freeform-input.ts +130 -0
- package/src/responses/reasoning-envelope.ts +30 -0
- package/src/responses/state.ts +5 -12
- package/src/responses/tool-name-aliases.ts +15 -1
- package/src/router.ts +91 -115
- package/src/routing/compatibility/behavior.ts +9 -0
- package/src/routing/compatibility/subject.ts +16 -1
- package/src/server/adapter-resolve.ts +9 -0
- package/src/server/auth-cors.ts +3 -0
- package/src/server/chat-completions.ts +5 -2
- package/src/server/claude-messages.ts +6 -3
- package/src/server/effort-row.ts +11 -3
- package/src/server/grok-responses-control-frame.ts +160 -1
- package/src/server/index/serve-options.ts +86 -29
- package/src/server/index/spend-ledger-lifecycle.ts +66 -0
- package/src/server/index/websocket-handler.ts +6 -1
- package/src/server/index.ts +16 -13
- package/src/server/management/aside-profile-routes.ts +266 -7
- package/src/server/management/config-routes.ts +18 -2
- package/src/server/management/context.ts +3 -0
- package/src/server/management/integration-routes.ts +287 -5
- package/src/server/management/metrics-routes.ts +20 -0
- package/src/server/management/model-rows.ts +224 -12
- package/src/server/management/route-registry.ts +14 -0
- package/src/server/management/shared.ts +10 -3
- package/src/server/management/system-restart.ts +7 -2
- package/src/server/management/system-routes.ts +2 -0
- package/src/server/management/usage-aggregate-cache.ts +4 -0
- package/src/server/management-api.ts +2 -0
- package/src/server/management-auth.ts +15 -1
- package/src/server/readiness.ts +29 -10
- package/src/server/relay-eager.ts +24 -2
- package/src/server/relay.ts +119 -10
- package/src/server/request-log.ts +55 -2
- package/src/server/request-metrics.ts +236 -0
- package/src/server/responses/adapter-continuation.ts +3 -3
- package/src/server/responses/adapter-dispatch.ts +11 -6
- package/src/server/responses/compact.ts +32 -10
- package/src/server/responses/compaction-routing.ts +111 -0
- package/src/server/responses/core-codex-account.ts +8 -3
- package/src/server/responses/core-combo.ts +7 -7
- package/src/server/responses/core-normalize.ts +6 -12
- package/src/server/responses/core-opaque-recovery.ts +1 -0
- package/src/server/responses/core-options.ts +4 -0
- package/src/server/responses/encrypted-payload.ts +20 -2
- package/src/server/responses/passthrough-delivery.ts +31 -14
- package/src/server/responses/passthrough-dispatch.ts +36 -9
- package/src/server/responses/policy-fallback.ts +5 -13
- package/src/server/responses/request-prepare.ts +67 -17
- package/src/server/responses/request-send-budget.ts +5 -1
- package/src/server/responses/request-sidecar-auth.ts +1 -1
- package/src/server/responses/request-transport.ts +2 -2
- package/src/server/responses/run-turn-execution.ts +25 -3
- package/src/server/responses/sidecar-execution.ts +17 -2
- package/src/server/responses/ws-upstream.ts +14 -27
- package/src/server/responses-custom-tool-repair.ts +27 -54
- package/src/server/responses-undeclared-tool-guard.ts +31 -1
- package/src/server/sse-payload-rewrite.ts +1 -1
- package/src/tray/windows-tray.ps1 +155 -3
- package/src/types/config.ts +11 -3
- package/src/types/provider.ts +18 -0
- package/src/types/request.ts +2 -0
- package/src/types/tools.ts +14 -0
- package/src/types.ts +1 -0
- package/src/vision/eligibility.ts +88 -9
- package/src/vision/plan.ts +34 -10
- package/src/web-search/executor.ts +41 -2
- package/src/web-search/loop.ts +6 -1
- package/gui/dist/assets/index-C5IebErG.js +0 -136
- package/gui/dist/assets/index-OESInAjC.css +0 -1
package/src/router.ts
CHANGED
|
@@ -15,10 +15,12 @@ import { assertProviderDestinationAllowed } from "./lib/destination-policy";
|
|
|
15
15
|
import { redactSecretString, redactUrlForLog } from "./lib/redact";
|
|
16
16
|
import {
|
|
17
17
|
PROVIDER_REGISTRY,
|
|
18
|
-
mergeRegistryStaticHeaders,
|
|
19
18
|
providerCodexAccountMode,
|
|
20
|
-
|
|
19
|
+
registryEntryForProviderDestination,
|
|
21
20
|
} from "./providers/registry";
|
|
21
|
+
// Imported from the module directly rather than through the registry facade, which is at its
|
|
22
|
+
// file-size cap.
|
|
23
|
+
import { registryModelIdKeys } from "./providers/registry/model-ids";
|
|
22
24
|
import { applyDirectReasoningEffortContracts, hasLegacyClinePassReasoningEfforts } from "./providers/derive";
|
|
23
25
|
import { cloneFastWire } from "./providers/fastwire";
|
|
24
26
|
import {
|
|
@@ -29,11 +31,10 @@ import {
|
|
|
29
31
|
isCanonicalOpenAiForwardProvider,
|
|
30
32
|
LEGACY_CHATGPT_PROVIDER_ID,
|
|
31
33
|
LEGACY_OPENAI_MULTI_PROVIDER_ID,
|
|
32
|
-
OPENAI_API_PROVIDER_ID,
|
|
33
34
|
OPENAI_CODEX_PROVIDER_ID,
|
|
34
35
|
} from "./providers/openai-tiers";
|
|
35
36
|
import { decodeRoutedModelIdOrThrow, encodeRoutedModelId } from "./providers/slug-codec";
|
|
36
|
-
import { resolveModelAlias } from "./providers/default-aliases";
|
|
37
|
+
import { effectiveProviderAliasDecision, resolveModelAlias } from "./providers/default-aliases";
|
|
37
38
|
import { resolveBlockedModelRedirect } from "./lib/shadow-call";
|
|
38
39
|
import { getStaleCached } from "./codex/model-cache";
|
|
39
40
|
import { codexAccountNamespaceEntries } from "./codex/account-namespaces";
|
|
@@ -46,6 +47,7 @@ import {
|
|
|
46
47
|
import { getRoutingProfile, resolvePolicyProfileId, POLICY_NAMESPACE } from "./routing/profile";
|
|
47
48
|
import { evaluatePolicyProfile, type PolicyRequestEvidence } from "./routing/evaluator";
|
|
48
49
|
import { assemblePolicyCandidateEvidence } from "./routing/compatibility/assemble";
|
|
50
|
+
import { resolveModelPolicy, type ResolvedModelPolicy } from "./providers/resolved-model-policy";
|
|
49
51
|
|
|
50
52
|
export class UnknownRoutingPolicyError extends Error {
|
|
51
53
|
constructor(readonly profileId: string) {
|
|
@@ -69,6 +71,8 @@ export interface RouteResult {
|
|
|
69
71
|
providerName: string;
|
|
70
72
|
provider: OcxProviderConfig;
|
|
71
73
|
modelId: string;
|
|
74
|
+
/** Immutable static policy for the current final wire model. */
|
|
75
|
+
staticPolicy: ResolvedModelPolicy;
|
|
72
76
|
/** Which deterministic routing path produced this route (RI-01). */
|
|
73
77
|
routeKind: RouteDecisionKind;
|
|
74
78
|
/** Stable wire reason code for the selected route (RI-01). */
|
|
@@ -83,6 +87,29 @@ export interface RouteResult {
|
|
|
83
87
|
routeDecision?: RouteDecisionTraceV1;
|
|
84
88
|
}
|
|
85
89
|
|
|
90
|
+
export function captureRouteStaticPolicy(
|
|
91
|
+
providerName: string,
|
|
92
|
+
modelId: string,
|
|
93
|
+
provider: OcxProviderConfig,
|
|
94
|
+
effectiveAlias?: string | null,
|
|
95
|
+
inboundWire: "responses" | "chat" | "anthropic" = "responses",
|
|
96
|
+
): ResolvedModelPolicy {
|
|
97
|
+
const registryEntry = PROVIDER_REGISTRY.find(entry => entry.id === providerName);
|
|
98
|
+
const transportMatchedRegistry = !!registryEntry
|
|
99
|
+
&& providerMatchesRegistryTransportWithStaticGuards(providerName, provider);
|
|
100
|
+
return resolveModelPolicy({
|
|
101
|
+
providerName,
|
|
102
|
+
modelId,
|
|
103
|
+
provider,
|
|
104
|
+
registryEntry,
|
|
105
|
+
transportMatchedRegistry,
|
|
106
|
+
inboundWire,
|
|
107
|
+
modelCapabilities: provider.modelCapabilities?.[modelId],
|
|
108
|
+
...(provider.authMode ? { effectiveAuth: { authMode: provider.authMode } } : {}),
|
|
109
|
+
...(effectiveAlias !== undefined ? { effectiveAlias } : {}),
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
86
113
|
const MODEL_PROVIDER_PATTERNS: Array<{ providerNames: string[]; prefixes: string[] }> = [
|
|
87
114
|
{
|
|
88
115
|
providerNames: ["anthropic"],
|
|
@@ -117,19 +144,11 @@ export function knownModelIdsForProvider(
|
|
|
117
144
|
: undefined;
|
|
118
145
|
for (const id of registry?.models ?? []) ids.add(id);
|
|
119
146
|
// Registry model-keyed hint maps double as known native ids (e.g. NVIDIA carries no
|
|
120
|
-
// static models list but names `moonshotai/kimi-k2.6` in its effort/window maps).
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
registry?.modelDefaultReasoningEfforts,
|
|
126
|
-
registry?.modelReasoningEffortMap,
|
|
127
|
-
registry?.modelMaxOutputTokens,
|
|
128
|
-
registry?.modelSupportsServiceTier,
|
|
129
|
-
registry?.modelSupportsVerbosity,
|
|
130
|
-
]) {
|
|
131
|
-
for (const id of Object.keys(map ?? {})) ids.add(id);
|
|
132
|
-
}
|
|
147
|
+
// static models list but names `moonshotai/kimi-k2.6` in its effort/window maps). Which
|
|
148
|
+
// maps count is classified by the registry itself rather than listed here, so an id declared
|
|
149
|
+
// only in a map this function forgot is no longer undecodable, and a new model-keyed field
|
|
150
|
+
// fails typecheck until its keys are given a meaning.
|
|
151
|
+
for (const id of registry ? registryModelIdKeys(registry) : []) ids.add(id);
|
|
133
152
|
for (const cached of getStaleCached(provName) ?? []) ids.add(cached.id);
|
|
134
153
|
for (const model of config?.customModels ?? []) {
|
|
135
154
|
if (model.provider === provName && model.modelId) ids.add(model.modelId);
|
|
@@ -137,66 +156,6 @@ export function knownModelIdsForProvider(
|
|
|
137
156
|
return [...ids];
|
|
138
157
|
}
|
|
139
158
|
|
|
140
|
-
// Merge registry-default effort maps under user values so built-in provider configs can
|
|
141
|
-
// carry real upstream aliases without a disk migration. User overrides win per-key.
|
|
142
|
-
function mergeRecord(
|
|
143
|
-
seed: Record<string, string> | undefined,
|
|
144
|
-
user: Record<string, string> | undefined,
|
|
145
|
-
): Record<string, string> | undefined {
|
|
146
|
-
if (!seed && !user) return undefined;
|
|
147
|
-
return { ...(seed ?? {}), ...(user ?? {}) };
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function mergeNestedRecord(
|
|
151
|
-
seed: Record<string, Record<string, string>> | undefined,
|
|
152
|
-
user: Record<string, Record<string, string>> | undefined,
|
|
153
|
-
): Record<string, Record<string, string>> | undefined {
|
|
154
|
-
if (!seed && !user) return undefined;
|
|
155
|
-
const out: Record<string, Record<string, string>> = {};
|
|
156
|
-
for (const [key, value] of Object.entries(seed ?? {})) out[key] = { ...value };
|
|
157
|
-
for (const [key, value] of Object.entries(user ?? {})) out[key] = { ...(out[key] ?? {}), ...value };
|
|
158
|
-
return out;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function mergeStringArray(
|
|
162
|
-
seed: string[] | undefined,
|
|
163
|
-
user: string[] | undefined,
|
|
164
|
-
): string[] | undefined {
|
|
165
|
-
if (!seed && !user) return undefined;
|
|
166
|
-
return [...new Set([...(seed ?? []), ...(user ?? [])])];
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function mergeRecordFill<T>(
|
|
170
|
-
seed: Record<string, T> | undefined,
|
|
171
|
-
user: Record<string, T> | undefined,
|
|
172
|
-
): Record<string, T> | undefined {
|
|
173
|
-
if (!seed && !user) return undefined;
|
|
174
|
-
return { ...(seed ?? {}), ...(user ?? {}) };
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function mergePositiveNumberCaps(
|
|
178
|
-
seed: Record<string, number> | undefined,
|
|
179
|
-
user: Record<string, number> | undefined,
|
|
180
|
-
): Record<string, number> | undefined {
|
|
181
|
-
if (!seed && !user) return undefined;
|
|
182
|
-
const out = { ...(seed ?? {}) };
|
|
183
|
-
for (const [key, value] of Object.entries(user ?? {})) {
|
|
184
|
-
out[key] = typeof out[key] === "number" ? Math.min(out[key]!, value) : value;
|
|
185
|
-
}
|
|
186
|
-
return out;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function mergeStringArrayRecord(
|
|
190
|
-
seed: Record<string, string[]> | undefined,
|
|
191
|
-
user: Record<string, string[]> | undefined,
|
|
192
|
-
): Record<string, string[]> | undefined {
|
|
193
|
-
if (!seed && !user) return undefined;
|
|
194
|
-
const out: Record<string, string[]> = {};
|
|
195
|
-
for (const [key, value] of Object.entries(seed ?? {})) out[key] = [...value];
|
|
196
|
-
for (const [key, value] of Object.entries(user ?? {})) out[key] = [...value];
|
|
197
|
-
return out;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
159
|
/** Same endpoint modulo surrounding space and trailing slashes — matches `matchBaseUrlChoice`. */
|
|
201
160
|
function isSameEndpoint(a: string, b: string): boolean {
|
|
202
161
|
return a.trim().replace(/\/+$/, "") === b.trim().replace(/\/+$/, "");
|
|
@@ -302,7 +261,20 @@ export function routedProviderConfig(providerName: string, provider: OcxProvider
|
|
|
302
261
|
const registryEntry = PROVIDER_REGISTRY.find(entry => entry.id === providerName);
|
|
303
262
|
if (!registryEntry || !providerMatchesRegistryTransportWithStaticGuards(providerName, provider)) {
|
|
304
263
|
assertProviderDestinationAllowed(providerName, provider);
|
|
305
|
-
|
|
264
|
+
// A row whose adapter no longer matches its registry entry still reaches the Responses
|
|
265
|
+
// adapter when one model opts in through `modelAdapters` — a Volcengine Coding Plan config
|
|
266
|
+
// saved on Chat, for instance. The replay-drop flag belongs to the DESTINATION rather than
|
|
267
|
+
// to the provider-wide wire, so it is filled here too; without it that continuation forwards
|
|
268
|
+
// the reasoning item the upstream answers 400 to. Destination matching refuses templated and
|
|
269
|
+
// overridable base URLs, so this cannot follow a retargeted row, and an explicit value wins.
|
|
270
|
+
const destination = registryEntryForProviderDestination(provider);
|
|
271
|
+
return {
|
|
272
|
+
...provider,
|
|
273
|
+
apiKey: usableResolvedApiKey(provider.apiKey),
|
|
274
|
+
...(provider.dropResponsesReasoningItems === undefined && destination?.dropResponsesReasoningItems !== undefined
|
|
275
|
+
? { dropResponsesReasoningItems: destination.dropResponsesReasoningItems }
|
|
276
|
+
: {}),
|
|
277
|
+
};
|
|
306
278
|
}
|
|
307
279
|
const resolvedApiKey = usableResolvedApiKey(provider.apiKey);
|
|
308
280
|
const staticModelCatalog = !providerSupportsLiveModelDiscovery(providerName, provider);
|
|
@@ -317,46 +289,42 @@ export function routedProviderConfig(providerName: string, provider: OcxProvider
|
|
|
317
289
|
: registryEntry.authKind === "forward" || registryEntry.authKind === "oauth"
|
|
318
290
|
? registryEntry.authKind
|
|
319
291
|
: provider.authMode === "forward" ? undefined : provider.authMode;
|
|
320
|
-
const
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
:
|
|
327
|
-
|
|
292
|
+
const staticPolicy = resolveModelPolicy({
|
|
293
|
+
providerName,
|
|
294
|
+
modelId: "__provider_static__",
|
|
295
|
+
provider,
|
|
296
|
+
registryEntry,
|
|
297
|
+
transportMatchedRegistry: true,
|
|
298
|
+
...(canonicalAuthMode ? { effectiveAuth: { authMode: canonicalAuthMode } } : {}),
|
|
299
|
+
}).provider;
|
|
300
|
+
const reasoningEffortMap = staticPolicy.reasoningEffortMap;
|
|
301
|
+
const modelReasoningEffortMap = staticPolicy.modelReasoningEffortMap;
|
|
302
|
+
const modelReasoningEfforts = staticPolicy.modelReasoningEfforts;
|
|
303
|
+
const modelDefaultReasoningEfforts = staticPolicy.modelDefaultReasoningEfforts;
|
|
304
|
+
const modelContextWindows = staticPolicy.modelContextWindows;
|
|
305
|
+
const modelInputModalities = staticPolicy.modelInputModalities;
|
|
328
306
|
// Registry static headers are documented as applying to every upstream request, so they are
|
|
329
307
|
// filled at resolve time rather than only at seed time: a config written before a header
|
|
330
308
|
// existed, or one carrying any header of its own, would otherwise never receive it. User
|
|
331
309
|
// headers win, matched case-insensitively so an override replaces rather than duplicates.
|
|
332
|
-
const headers =
|
|
333
|
-
const modelMaxInputTokens =
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
const
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
const
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
const
|
|
348
|
-
const
|
|
349
|
-
const
|
|
350
|
-
const noTopPModels = mergeStringArray(registryEntry.noTopPModels, provider.noTopPModels);
|
|
351
|
-
const noPenaltyModels = mergeStringArray(registryEntry.noPenaltyModels, provider.noPenaltyModels);
|
|
352
|
-
const noJsonSchemaModels = mergeStringArray(registryEntry.noJsonSchemaModels, provider.noJsonSchemaModels);
|
|
353
|
-
const autoToolChoiceOnlyModels = mergeStringArray(registryEntry.autoToolChoiceOnlyModels, provider.autoToolChoiceOnlyModels);
|
|
354
|
-
const preserveReasoningContentModels = mergeStringArray(registryEntry.preserveReasoningContentModels, provider.preserveReasoningContentModels);
|
|
355
|
-
const requiresReasoningPlaceholderModels = mergeStringArray(registryEntry.requiresReasoningPlaceholderModels, provider.requiresReasoningPlaceholderModels);
|
|
356
|
-
const reasoningSplitModels = mergeStringArray(registryEntry.reasoningSplitModels, provider.reasoningSplitModels);
|
|
357
|
-
const reasoningDetailsModels = mergeStringArray(registryEntry.reasoningDetailsModels, provider.reasoningDetailsModels);
|
|
358
|
-
const thinkingToggleModels = mergeStringArray(registryEntry.thinkingToggleModels, provider.thinkingToggleModels);
|
|
359
|
-
const thinkingBudgetModels = mergeStringArray(registryEntry.thinkingBudgetModels, provider.thinkingBudgetModels);
|
|
310
|
+
const headers = staticPolicy.headers;
|
|
311
|
+
const modelMaxInputTokens = staticPolicy.modelMaxInputTokens;
|
|
312
|
+
const modelMaxOutputTokens = staticPolicy.modelMaxOutputTokens;
|
|
313
|
+
const modelSupportsServiceTier = staticPolicy.modelSupportsServiceTier;
|
|
314
|
+
const modelSupportsVerbosity = staticPolicy.modelSupportsVerbosity;
|
|
315
|
+
const noVisionModels = staticPolicy.noVisionModels;
|
|
316
|
+
const noReasoningModels = staticPolicy.noReasoningModels;
|
|
317
|
+
const noTemperatureModels = staticPolicy.noTemperatureModels;
|
|
318
|
+
const noTopPModels = staticPolicy.noTopPModels;
|
|
319
|
+
const noPenaltyModels = staticPolicy.noPenaltyModels;
|
|
320
|
+
const noJsonSchemaModels = staticPolicy.noJsonSchemaModels;
|
|
321
|
+
const autoToolChoiceOnlyModels = staticPolicy.autoToolChoiceOnlyModels;
|
|
322
|
+
const preserveReasoningContentModels = staticPolicy.preserveReasoningContentModels;
|
|
323
|
+
const requiresReasoningPlaceholderModels = staticPolicy.requiresReasoningPlaceholderModels;
|
|
324
|
+
const reasoningSplitModels = staticPolicy.reasoningSplitModels;
|
|
325
|
+
const reasoningDetailsModels = staticPolicy.reasoningDetailsModels;
|
|
326
|
+
const thinkingToggleModels = staticPolicy.thinkingToggleModels;
|
|
327
|
+
const thinkingBudgetModels = staticPolicy.thinkingBudgetModels;
|
|
360
328
|
const registryBaseUrlIsTemplate = /\{[^}]*\}/.test(registryEntry.baseUrl);
|
|
361
329
|
const userBaseUrl = typeof provider.baseUrl === "string" ? provider.baseUrl.trim() : "";
|
|
362
330
|
const userBaseUrlIsResolved = userBaseUrl.length > 0 && !/\{[^}]*\}/.test(userBaseUrl);
|
|
@@ -417,6 +385,9 @@ export function routedProviderConfig(providerName: string, provider: OcxProvider
|
|
|
417
385
|
...(provider.preserveResponsesReasoningContent === undefined && registryEntry.preserveResponsesReasoningContent !== undefined
|
|
418
386
|
? { preserveResponsesReasoningContent: registryEntry.preserveResponsesReasoningContent }
|
|
419
387
|
: {}),
|
|
388
|
+
...(provider.dropResponsesReasoningItems === undefined && registryEntry.dropResponsesReasoningItems !== undefined
|
|
389
|
+
? { dropResponsesReasoningItems: registryEntry.dropResponsesReasoningItems }
|
|
390
|
+
: {}),
|
|
420
391
|
// The request path resolves through routedProviderConfig() and never calls
|
|
421
392
|
// enrichProviderFromRegistry(), so a saved provider row written before the
|
|
422
393
|
// registry learned this flag must be backfilled here or route.provider never
|
|
@@ -565,10 +536,15 @@ function routeResult(
|
|
|
565
536
|
const effectiveModelId = redirected ?? modelId;
|
|
566
537
|
const effectiveRouteReason = redirected ? "blocked-model-redirect" : routeReason;
|
|
567
538
|
const codexAccountMode = providerCodexAccountMode(providerName, provider);
|
|
539
|
+
const routedProvider = routedProviderConfig(providerName, provider);
|
|
540
|
+
const effectiveAlias = config
|
|
541
|
+
? effectiveProviderAliasDecision(providerName, provider, config)
|
|
542
|
+
: undefined;
|
|
568
543
|
return {
|
|
569
544
|
providerName,
|
|
570
|
-
provider:
|
|
545
|
+
provider: routedProvider,
|
|
571
546
|
modelId: effectiveModelId,
|
|
547
|
+
staticPolicy: captureRouteStaticPolicy(providerName, effectiveModelId, routedProvider, effectiveAlias),
|
|
572
548
|
routeKind,
|
|
573
549
|
routeReason: effectiveRouteReason,
|
|
574
550
|
...(codexAccountMode ? { codexAccountMode } : {}),
|
|
@@ -200,6 +200,12 @@ export function resolveProductionBehaviorValues(
|
|
|
200
200
|
: reasoningEfforts.length > 0 || Boolean(registryEntry?.reasoningEfforts),
|
|
201
201
|
),
|
|
202
202
|
"reasoning.efforts": behaviorRow("provider_config", reasoningEfforts),
|
|
203
|
+
// Whether the configured ladder above is what the WIRE uses, or only what the catalog
|
|
204
|
+
// advertises. An adapter shipping its own per-model table (command-code) otherwise wins for
|
|
205
|
+
// models it knows, and it also repairs a refused rung by replaying without the effort. Two
|
|
206
|
+
// routes that disagree about this send different bytes and recover differently, so they are
|
|
207
|
+
// not the same compatibility subject and must not share evidence.
|
|
208
|
+
"reasoning.effortsAuthoritative": behaviorRow("provider_config", effective.modelReasoningEffortsAuthoritative === true),
|
|
203
209
|
"reasoning.defaultEffort": behaviorRow("provider_config", defaultReasoningEffort),
|
|
204
210
|
"reasoning.effortMap": behaviorRow("provider_config", reasoningEffortMap),
|
|
205
211
|
"reasoning.wireFormat": behaviorRow("provider_config", effective.reasoningWireFormat ?? null),
|
|
@@ -209,6 +215,9 @@ export function resolveProductionBehaviorValues(
|
|
|
209
215
|
}),
|
|
210
216
|
"reasoning.replayMode": behaviorRow("provider_config", {
|
|
211
217
|
preserveResponses: effective.preserveResponsesReasoningContent === true,
|
|
218
|
+
// Dropping replayed reasoning items changes the continuation body, so two routes that
|
|
219
|
+
// disagree about it are not the same compatibility subject and must not share evidence.
|
|
220
|
+
dropResponses: effective.dropResponsesReasoningItems === true,
|
|
212
221
|
preserveContent: includesModel(effective.preserveReasoningContentModels, modelId),
|
|
213
222
|
placeholder: includesModel(effective.requiresReasoningPlaceholderModels, modelId),
|
|
214
223
|
}),
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type { OcxConfig, OcxProviderConfig } from "../../types";
|
|
2
2
|
import type { InboundWire } from "../../providers/registry";
|
|
3
|
+
import { getProviderRegistryEntry } from "../../providers/registry";
|
|
4
|
+
import { providerMatchesRegistryTransportWithStaticGuards } from "../../providers/static-model-discovery";
|
|
5
|
+
import { resolveModelPolicy } from "../../providers/resolved-model-policy";
|
|
3
6
|
import { subjectIdForSubject } from "../../lab/digest";
|
|
4
7
|
import { buildRouteSubjectV1 } from "../../lab/subject/route-subject";
|
|
5
8
|
import { buildProtocolSubjectV1 } from "../../lab/subject/protocol-subject";
|
|
@@ -65,7 +68,19 @@ export function resolveCompatibilitySubjectsForInboundWire(
|
|
|
65
68
|
inboundWire: InboundWire,
|
|
66
69
|
configDir?: string,
|
|
67
70
|
): ResolvedPolicyCompatibilitySubjects {
|
|
68
|
-
const
|
|
71
|
+
const registryEntry = getProviderRegistryEntry(providerName);
|
|
72
|
+
const staticPolicy = resolveModelPolicy({
|
|
73
|
+
providerName,
|
|
74
|
+
modelId,
|
|
75
|
+
provider: routed,
|
|
76
|
+
registryEntry,
|
|
77
|
+
transportMatchedRegistry: !!registryEntry
|
|
78
|
+
&& providerMatchesRegistryTransportWithStaticGuards(providerName, routed),
|
|
79
|
+
inboundWire,
|
|
80
|
+
modelCapabilities: routed.modelCapabilities?.[modelId],
|
|
81
|
+
...(routed.authMode ? { effectiveAuth: { authMode: routed.authMode } } : {}),
|
|
82
|
+
});
|
|
83
|
+
const effective = resolveWireProtocolOverride(providerName, modelId, routed, inboundWire, staticPolicy);
|
|
69
84
|
const baseUrl = typeof effective.baseUrl === "string" ? effective.baseUrl.trim() : "";
|
|
70
85
|
const adapter = effective.adapter ?? "openai-responses";
|
|
71
86
|
const inboundProtocol = inboundProtocolForWire(inboundWire);
|
|
@@ -3,6 +3,7 @@ import type { OcxProviderConfig } from "../types";
|
|
|
3
3
|
import { isWirePinnedModel, MODEL_ADAPTER_OVERRIDE_ALLOWED, pinnedWireAdapter } from "../types";
|
|
4
4
|
import { isCanonicalOpenAiForwardProvider } from "../providers/openai-tiers";
|
|
5
5
|
import { type InboundWire, providerModelWireDefault } from "../providers/registry";
|
|
6
|
+
import type { ResolvedModelPolicy } from "../providers/resolved-model-policy";
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Resolve the wire a single model should use: a hard pin first, then a configured
|
|
@@ -22,7 +23,15 @@ export function resolveWireProtocolOverride(
|
|
|
22
23
|
modelId: string,
|
|
23
24
|
providerConfig: OcxProviderConfig,
|
|
24
25
|
inbound: InboundWire = "responses",
|
|
26
|
+
staticPolicy?: ResolvedModelPolicy,
|
|
25
27
|
): OcxProviderConfig {
|
|
28
|
+
// RouteResult policy is recaptured for the original inbound protocol whenever routing selects
|
|
29
|
+
// or replaces a destination, so every downstream rebuild consumes the same adapter authority.
|
|
30
|
+
if (staticPolicy) {
|
|
31
|
+
return staticPolicy.model.adapter !== providerConfig.adapter
|
|
32
|
+
? { ...providerConfig, adapter: staticPolicy.model.adapter }
|
|
33
|
+
: providerConfig;
|
|
34
|
+
}
|
|
26
35
|
const pinned = pinnedWireAdapter(providerName, modelId);
|
|
27
36
|
if (pinned && providerConfig.adapter !== pinned) {
|
|
28
37
|
return { ...providerConfig, adapter: pinned };
|
package/src/server/auth-cors.ts
CHANGED
|
@@ -942,12 +942,15 @@ const PROVIDER_CONFIG_FIELD_POLICY = {
|
|
|
942
942
|
supportsServiceTier: "editor",
|
|
943
943
|
modelSupportsServiceTier: "editor",
|
|
944
944
|
preserveResponsesReasoningContent: "editor",
|
|
945
|
+
dropResponsesReasoningItems: "editor",
|
|
946
|
+
modelReasoningEffortsAuthoritative: "editor",
|
|
945
947
|
decodesNativeCompactionBlobs: "editor",
|
|
946
948
|
allowEncryptedV2AgentTasks: "editor",
|
|
947
949
|
allowPrivateNetwork: "editor",
|
|
948
950
|
upstreamHttpVersion: "editor",
|
|
949
951
|
upstreamWebsocket: "editor",
|
|
950
952
|
directGeminiWireRenames: "editor",
|
|
953
|
+
googleToolSchemaPolicy: "editor",
|
|
951
954
|
disabled: "editor",
|
|
952
955
|
codexAccountMode: "editor",
|
|
953
956
|
apiKey: "redacted",
|
|
@@ -23,7 +23,7 @@ import { classifyError, cyberPolicyErrorType, CYBER_POLICY_ERROR_CODE, isCyberPo
|
|
|
23
23
|
import { redactSecretString } from "../lib/redact";
|
|
24
24
|
import { resolveClientRetryAfter } from "../lib/retry-after";
|
|
25
25
|
import { estimateTokens } from "../lib/token-estimate";
|
|
26
|
-
import { NoEligiblePolicyCandidateError, UnknownRoutingPolicyError, routeModel } from "../router";
|
|
26
|
+
import { captureRouteStaticPolicy, NoEligiblePolicyCandidateError, UnknownRoutingPolicyError, routeModel } from "../router";
|
|
27
27
|
import { evidenceFromBody } from "../routing/request-evidence";
|
|
28
28
|
import { resolveWireProtocolOverride } from "./adapter-resolve";
|
|
29
29
|
import { resolveOpenCodeGoTransport } from "../providers/opencode-go-transport";
|
|
@@ -153,7 +153,10 @@ async function handleChatCompletionsWithBudget(
|
|
|
153
153
|
// Preserve the routed destination for Go recognition, then settle the wire before
|
|
154
154
|
// deriving protocol-scoped affinity. Recognition must not inspect the flipped adapter.
|
|
155
155
|
const routedProvider = route.provider;
|
|
156
|
-
|
|
156
|
+
route.staticPolicy = captureRouteStaticPolicy(
|
|
157
|
+
route.providerName, route.modelId, routedProvider, route.staticPolicy.effectiveAlias, "chat",
|
|
158
|
+
);
|
|
159
|
+
const wireProvider = resolveWireProtocolOverride(route.providerName, route.modelId, routedProvider, "chat", route.staticPolicy);
|
|
157
160
|
route.provider = resolveOpenCodeGoTransport(
|
|
158
161
|
wireProvider,
|
|
159
162
|
getOrAllocateRequestSessionLane(req),
|
|
@@ -29,7 +29,7 @@ import {
|
|
|
29
29
|
} from "../claude/outbound";
|
|
30
30
|
import { clearableDeadline, idleDeadline } from "../lib/abort";
|
|
31
31
|
import { estimateTokens } from "../lib/token-estimate";
|
|
32
|
-
import { NoEligiblePolicyCandidateError, UnknownRoutingPolicyError, routeModel } from "../router";
|
|
32
|
+
import { captureRouteStaticPolicy, NoEligiblePolicyCandidateError, UnknownRoutingPolicyError, routeModel } from "../router";
|
|
33
33
|
import { evidenceFromBody } from "../routing/request-evidence";
|
|
34
34
|
import { resolveWireProtocolOverride } from "./adapter-resolve";
|
|
35
35
|
import type { OcxConfig } from "../types";
|
|
@@ -420,7 +420,7 @@ async function anthropicNativePassthrough(
|
|
|
420
420
|
// count_tokens too — counts must match what the real send will contain, and the 32MB
|
|
421
421
|
// body cap applies to it equally. Non-message bodies pass through untouched.
|
|
422
422
|
if (Array.isArray(body.messages)) {
|
|
423
|
-
await normalizeAnthropicImages(body.messages);
|
|
423
|
+
await normalizeAnthropicImages(body.messages, { abortSignal: req.signal });
|
|
424
424
|
enforceAnthropicImageLimits(body.messages);
|
|
425
425
|
}
|
|
426
426
|
const headers = new Headers();
|
|
@@ -817,7 +817,10 @@ async function handleClaudeMessagesWithBudget(
|
|
|
817
817
|
const route = routeModel(config, internalBody.model as string, evidenceFromBody(internalBody));
|
|
818
818
|
// Settle the wire once so the sampling decision below reads the effective
|
|
819
819
|
// adapter rather than the provider-wide default (#404).
|
|
820
|
-
route.
|
|
820
|
+
route.staticPolicy = captureRouteStaticPolicy(
|
|
821
|
+
route.providerName, route.modelId, route.provider, route.staticPolicy.effectiveAlias, "anthropic",
|
|
822
|
+
);
|
|
823
|
+
route.provider = resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, "anthropic", route.staticPolicy);
|
|
821
824
|
logCtx.routeDecision = route.routeDecision;
|
|
822
825
|
if (route.provider.adapter === "openai-responses") {
|
|
823
826
|
delete internalBody.max_output_tokens;
|
package/src/server/effort-row.ts
CHANGED
|
@@ -30,7 +30,9 @@ export interface EffortRowOptions {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
export function isKnownId(knownIds: EffortRowKnownIds | undefined, id: string): boolean {
|
|
33
|
-
return typeof knownIds === "function"
|
|
33
|
+
return typeof knownIds === "function"
|
|
34
|
+
? knownIds(id)
|
|
35
|
+
: knownIds?.has(id) === true || knownIds?.has(id.toLowerCase()) === true;
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
export function effortRowId(baseId: string, effort: string): string {
|
|
@@ -43,6 +45,12 @@ export function effortRowId(baseId: string, effort: string): string {
|
|
|
43
45
|
*/
|
|
44
46
|
export function knownEffortRowIds(config: OcxConfig): Set<string> {
|
|
45
47
|
const ids = new Set<string>();
|
|
48
|
+
const addAlias = (id: string): void => {
|
|
49
|
+
ids.add(id);
|
|
50
|
+
// Model aliases are resolved case-insensitively. Preserve that contract here so an
|
|
51
|
+
// alias cannot be mistaken for a synthetic effort row before ordinary routing runs.
|
|
52
|
+
ids.add(id.toLowerCase());
|
|
53
|
+
};
|
|
46
54
|
for (const [providerName, provider] of Object.entries(config.providers)) {
|
|
47
55
|
const known = knownModelIdsForProvider(providerName, provider, config);
|
|
48
56
|
const namespaces = [providerName, provider.alias].filter((value): value is string => (
|
|
@@ -54,8 +62,8 @@ export function knownEffortRowIds(config: OcxConfig): Set<string> {
|
|
|
54
62
|
for (const namespace of namespaces) ids.add(`${namespace}/${id}`);
|
|
55
63
|
}
|
|
56
64
|
for (const alias of Object.values(provider.modelAliases ?? {})) {
|
|
57
|
-
|
|
58
|
-
for (const namespace of namespaces)
|
|
65
|
+
addAlias(alias);
|
|
66
|
+
for (const namespace of namespaces) addAlias(`${namespace}/${alias}`);
|
|
59
67
|
}
|
|
60
68
|
}
|
|
61
69
|
for (const [id, combo] of Object.entries(config.combos ?? {})) {
|
|
@@ -1,10 +1,137 @@
|
|
|
1
|
-
import { sseDataPayload, type SseBlockRewrite } from "./sse-payload-rewrite";
|
|
1
|
+
import { replaceSseDataPayload, sseDataPayload, type SseBlockRewrite } from "./sse-payload-rewrite";
|
|
2
2
|
|
|
3
3
|
const GROK_CONTROL_FRAME_TYPES: Record<string, true> = {
|
|
4
4
|
"codex.rate_limits": true,
|
|
5
5
|
"codex.response.metadata": true,
|
|
6
6
|
};
|
|
7
7
|
|
|
8
|
+
const GROK_RESPONSE_INTEGER_TIMESTAMP_FIELDS = ["created_at", "completed_at"] as const;
|
|
9
|
+
|
|
10
|
+
type JsonRange = { start: number; end: number };
|
|
11
|
+
|
|
12
|
+
function skipJsonWhitespace(payload: string, index: number): number {
|
|
13
|
+
while (index < payload.length && /[\t\n\r ]/.test(payload[index] ?? "")) index += 1;
|
|
14
|
+
return index;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function findJsonStringEnd(payload: string, start: number): number | undefined {
|
|
18
|
+
for (let index = start + 1; index < payload.length; index += 1) {
|
|
19
|
+
if (payload[index] === "\\") index += 1;
|
|
20
|
+
else if (payload[index] === '"') return index + 1;
|
|
21
|
+
}
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function findJsonValueEnd(payload: string, start: number): number | undefined {
|
|
26
|
+
if (payload[start] === '"') return findJsonStringEnd(payload, start);
|
|
27
|
+
if (payload[start] !== "{" && payload[start] !== "[") {
|
|
28
|
+
let index = start;
|
|
29
|
+
while (index < payload.length && !",}]".includes(payload[index] ?? "")) index += 1;
|
|
30
|
+
return index;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let depth = 0;
|
|
34
|
+
let inString = false;
|
|
35
|
+
for (let index = start; index < payload.length; index += 1) {
|
|
36
|
+
const char = payload[index];
|
|
37
|
+
if (inString) {
|
|
38
|
+
if (char === "\\") index += 1;
|
|
39
|
+
else if (char === '"') inString = false;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (char === '"') inString = true;
|
|
43
|
+
else if (char === "{" || char === "[") depth += 1;
|
|
44
|
+
else if (char === "}" || char === "]") {
|
|
45
|
+
depth -= 1;
|
|
46
|
+
if (depth === 0) return index + 1;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function findResponseObjectRange(payload: string): JsonRange | null {
|
|
53
|
+
let cursor = skipJsonWhitespace(payload, 0);
|
|
54
|
+
if (payload[cursor] !== "{") return null;
|
|
55
|
+
cursor += 1;
|
|
56
|
+
let responseRange: JsonRange | null = null;
|
|
57
|
+
|
|
58
|
+
while (cursor < payload.length) {
|
|
59
|
+
cursor = skipJsonWhitespace(payload, cursor);
|
|
60
|
+
if (payload[cursor] === "}") return responseRange;
|
|
61
|
+
if (payload[cursor] !== '"') return null;
|
|
62
|
+
const keyEnd = findJsonStringEnd(payload, cursor);
|
|
63
|
+
if (keyEnd === undefined) return null;
|
|
64
|
+
let key: unknown;
|
|
65
|
+
try {
|
|
66
|
+
key = JSON.parse(payload.slice(cursor, keyEnd));
|
|
67
|
+
} catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
cursor = skipJsonWhitespace(payload, keyEnd);
|
|
72
|
+
if (payload[cursor] !== ":") return null;
|
|
73
|
+
const valueStart = skipJsonWhitespace(payload, cursor + 1);
|
|
74
|
+
const valueEnd = findJsonValueEnd(payload, valueStart);
|
|
75
|
+
if (valueEnd === undefined) return null;
|
|
76
|
+
if (key === "response" && payload[valueStart] === "{") {
|
|
77
|
+
responseRange = { start: valueStart, end: valueEnd };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
cursor = skipJsonWhitespace(payload, valueEnd);
|
|
81
|
+
if (payload[cursor] === "}") return responseRange;
|
|
82
|
+
if (payload[cursor] !== ",") return null;
|
|
83
|
+
cursor += 1;
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function rewriteIntegralTimestamp(payload: string, field: string, value: number, event: unknown): string | null {
|
|
89
|
+
const responseRange = findResponseObjectRange(payload);
|
|
90
|
+
if (responseRange === null) return null;
|
|
91
|
+
let cursor = responseRange.start + 1;
|
|
92
|
+
let candidate: { start: number; length: number } | null = null;
|
|
93
|
+
while (cursor < responseRange.end) {
|
|
94
|
+
cursor = skipJsonWhitespace(payload, cursor);
|
|
95
|
+
if (payload[cursor] === "}") break;
|
|
96
|
+
if (payload[cursor] !== '"') return null;
|
|
97
|
+
const keyEnd = findJsonStringEnd(payload, cursor);
|
|
98
|
+
if (keyEnd === undefined) return null;
|
|
99
|
+
let key: unknown;
|
|
100
|
+
try {
|
|
101
|
+
key = JSON.parse(payload.slice(cursor, keyEnd));
|
|
102
|
+
} catch {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
cursor = skipJsonWhitespace(payload, keyEnd);
|
|
106
|
+
if (payload[cursor] !== ":") return null;
|
|
107
|
+
const valueStart = skipJsonWhitespace(payload, cursor + 1);
|
|
108
|
+
const valueEnd = findJsonValueEnd(payload, valueStart);
|
|
109
|
+
if (valueEnd === undefined || valueEnd > responseRange.end) return null;
|
|
110
|
+
const valueToken = payload.slice(valueStart, valueEnd).trimEnd();
|
|
111
|
+
if (
|
|
112
|
+
key === field &&
|
|
113
|
+
/^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$/.test(valueToken) &&
|
|
114
|
+
valueToken !== String(value) &&
|
|
115
|
+
Number(valueToken) === value
|
|
116
|
+
) {
|
|
117
|
+
if (candidate !== null) return null;
|
|
118
|
+
candidate = { start: valueStart, length: valueToken.length };
|
|
119
|
+
}
|
|
120
|
+
cursor = skipJsonWhitespace(payload, valueEnd);
|
|
121
|
+
if (payload[cursor] === "}") break;
|
|
122
|
+
if (payload[cursor] !== ",") return null;
|
|
123
|
+
cursor += 1;
|
|
124
|
+
}
|
|
125
|
+
if (candidate === null) return payload;
|
|
126
|
+
|
|
127
|
+
const rewritten = `${payload.slice(0, candidate.start)}${value}${payload.slice(candidate.start + candidate.length)}`;
|
|
128
|
+
try {
|
|
129
|
+
return JSON.stringify(JSON.parse(rewritten)) === JSON.stringify(event) ? rewritten : null;
|
|
130
|
+
} catch {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
8
135
|
/**
|
|
9
136
|
* Hide Codex-only control frames from Grok's strict Responses decoder.
|
|
10
137
|
*
|
|
@@ -41,3 +168,35 @@ export function createGrokResponsesControlFrameBlockRewrite(): SseBlockRewrite {
|
|
|
41
168
|
: [block];
|
|
42
169
|
};
|
|
43
170
|
}
|
|
171
|
+
|
|
172
|
+
/** Normalize safe integer response timestamps for Grok's strict Responses decoder. */
|
|
173
|
+
export function createGrokResponsesTimestampBlockRewrite(): SseBlockRewrite {
|
|
174
|
+
return (block) => {
|
|
175
|
+
const payload = sseDataPayload(block);
|
|
176
|
+
if (payload === null || payload === "[DONE]") return [block];
|
|
177
|
+
|
|
178
|
+
let event: unknown;
|
|
179
|
+
try {
|
|
180
|
+
event = JSON.parse(payload);
|
|
181
|
+
} catch {
|
|
182
|
+
return [block];
|
|
183
|
+
}
|
|
184
|
+
if (!event || typeof event !== "object" || Array.isArray(event)) return [block];
|
|
185
|
+
const eventRecord = event as Record<string, unknown>;
|
|
186
|
+
if (typeof eventRecord.type !== "string" || !eventRecord.type.startsWith("response.")) return [block];
|
|
187
|
+
if (!eventRecord.response || typeof eventRecord.response !== "object" || Array.isArray(eventRecord.response)) {
|
|
188
|
+
return [block];
|
|
189
|
+
}
|
|
190
|
+
const response = eventRecord.response as Record<string, unknown>;
|
|
191
|
+
let rewrittenPayload = payload;
|
|
192
|
+
for (const field of GROK_RESPONSE_INTEGER_TIMESTAMP_FIELDS) {
|
|
193
|
+
const value = response[field];
|
|
194
|
+
if (value === undefined) continue;
|
|
195
|
+
if (typeof value !== "number" || value < 0 || !Number.isSafeInteger(value)) return [block];
|
|
196
|
+
const rewritten = rewriteIntegralTimestamp(rewrittenPayload, field, value, event);
|
|
197
|
+
if (rewritten === null) return [block];
|
|
198
|
+
rewrittenPayload = rewritten;
|
|
199
|
+
}
|
|
200
|
+
return rewrittenPayload === payload ? [block] : [replaceSseDataPayload(block, rewrittenPayload)];
|
|
201
|
+
};
|
|
202
|
+
}
|