@bitkyc08/opencodex 2.19.0 → 2.21.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-BOFeam5a.js +76 -0
- package/gui/dist/assets/index-Xq49CY8F.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +2 -28
- package/src/adapters/google.ts +31 -3
- package/src/adapters/mimo-free.ts +17 -0
- package/src/adapters/openai-chat.ts +392 -40
- package/src/adapters/registry.ts +144 -0
- package/src/adapters/responses-tool-schema.ts +67 -0
- package/src/bridge.ts +15 -2
- package/src/chat/inbound.ts +13 -7
- package/src/claude/gateway-cache.ts +41 -4
- package/src/cli/claude.ts +3 -2
- package/src/cli/combo.ts +3 -0
- package/src/cli/dispatch.ts +8 -0
- package/src/cli/export-command.ts +2 -2
- package/src/cli/help.ts +2 -0
- package/src/cli/index.ts +3 -2
- package/src/cli/lab.ts +135 -1
- package/src/cli/minimax.ts +491 -0
- package/src/cli/models-runtime.ts +22 -1
- package/src/cli/models.ts +67 -2
- package/src/cli/opencode.ts +2 -1
- package/src/cli/registry.ts +22 -2
- package/src/clients/config-export.ts +125 -7
- package/src/codex/app-server-processes.ts +57 -2
- package/src/codex/app-server-restart-service.ts +232 -0
- package/src/codex/catalog/aggregation.ts +10 -1
- package/src/codex/catalog/effort.ts +15 -3
- package/src/codex/catalog/parsing.ts +3 -1
- package/src/codex/catalog/provider-fetch.ts +45 -5
- package/src/codex/catalog/sync.ts +74 -4
- package/src/codex/convergence.ts +2 -0
- package/src/combos/index.ts +1 -0
- package/src/combos/request.ts +30 -0
- package/src/combos/types.ts +6 -0
- package/src/config.ts +52 -0
- package/src/generated/compatibility-version.json +241 -85
- package/src/images/loop.ts +26 -6
- package/src/integrations/registry.ts +7 -0
- package/src/lab/conformance/jcs.ts +42 -2
- package/src/lab/conformance/negative-controls.ts +6 -2
- package/src/lab/conformance/runner.ts +16 -5
- package/src/lab/fabric/observe.ts +49 -14
- package/src/lab/index.ts +16 -0
- package/src/lab/ledger/purge.ts +152 -83
- package/src/lab/ledger/store.ts +168 -54
- package/src/lab/observe/from-conformance.ts +8 -6
- package/src/lab/observe/from-live.ts +8 -2
- package/src/lab/paths.ts +23 -0
- package/src/lab/public/bundle.ts +217 -0
- package/src/lab/public/community-authority.ts +175 -0
- package/src/lab/public/community-files.ts +29 -0
- package/src/lab/public/community.ts +479 -0
- package/src/lab/public/file-safety.ts +155 -0
- package/src/lab/public/ids.ts +26 -0
- package/src/lab/public/index.ts +16 -0
- package/src/lab/public/mutation-lock.ts +424 -0
- package/src/lab/public/operator.ts +353 -0
- package/src/lab/public/origin-purge.ts +79 -0
- package/src/lab/public/origin.ts +203 -0
- package/src/lab/public/privacy.ts +143 -0
- package/src/lab/public/private-file.ts +261 -0
- package/src/lab/public/project.ts +124 -0
- package/src/lab/public/purge-test-fault.ts +21 -0
- package/src/lab/public/purge.ts +223 -0
- package/src/lab/public/registry.ts +44 -0
- package/src/lab/public/revocation.ts +252 -0
- package/src/lab/public/signature.ts +219 -0
- package/src/lab/public/storage.ts +105 -0
- package/src/lab/public/strict-json.ts +206 -0
- package/src/lab/public/time.ts +26 -0
- package/src/lab/public/types.ts +172 -0
- package/src/lab/public/validate.ts +391 -0
- package/src/lib/codex-restart-contract.ts +120 -0
- package/src/lib/lab-activation.ts +109 -47
- package/src/lib/lab-live-pinned-sender.ts +16 -5
- package/src/lib/pinned-http.ts +70 -16
- package/src/lib/self-launch-argv.ts +15 -0
- package/src/lib/state-store-registrations.ts +2 -0
- package/src/lib/upstream-reachability.ts +4 -0
- package/src/lib/windows-elevation.ts +10 -1
- package/src/providers/derive.ts +24 -4
- package/src/providers/registry.ts +14 -4
- package/src/providers/request-pacing.ts +310 -0
- package/src/providers/service-tier.ts +143 -0
- package/src/providers/static-model-discovery.ts +86 -0
- package/src/reasoning-effort.ts +27 -1
- package/src/responses/custom-tool-compat.ts +4 -1
- package/src/responses/parser.ts +7 -1
- package/src/responses/provider-opaque-metadata.ts +73 -0
- package/src/responses/schema.ts +6 -0
- package/src/router.ts +23 -6
- package/src/routing/capability.ts +4 -2
- package/src/routing/compatibility/behavior.ts +5 -1
- package/src/server/adapter-resolve.ts +2 -32
- package/src/server/auth-cors.ts +50 -6
- package/src/server/chat-completions.ts +74 -36
- package/src/server/chat-native-sse.ts +331 -0
- package/src/server/chat-native.ts +371 -0
- package/src/server/management/combo-routes.ts +16 -2
- package/src/server/management/config-routes.ts +6 -4
- package/src/server/management/context.ts +17 -0
- package/src/server/management/lab-routes.ts +181 -19
- package/src/server/management/model-routes.ts +76 -2
- package/src/server/management/model-rows.ts +8 -0
- package/src/server/management/provider-capability-config.ts +48 -0
- package/src/server/management/provider-routes.ts +76 -4
- package/src/server/management/system-restart.ts +4 -2
- package/src/server/management/system-routes.ts +38 -0
- package/src/server/relay.ts +17 -3
- package/src/server/responses/compact.ts +4 -1
- package/src/server/responses/core.ts +257 -46
- package/src/server/responses/empty-completion-guard.ts +276 -0
- package/src/server/responses/fetch-helpers.ts +35 -4
- package/src/server/responses/pacing-overload.ts +13 -0
- package/src/server/responses/policy-fallback.ts +16 -2
- package/src/server/responses/terminal-guard.ts +1 -1
- package/src/server/responses/upstream-error.ts +5 -0
- package/src/server/responses.ts +17 -2
- package/src/server/system-env.ts +1 -1
- package/src/types.ts +84 -5
- package/src/update/index.ts +6 -5
- package/src/update/job.ts +5 -6
- package/src/update/notify.ts +5 -3
- package/src/usage/log.ts +11 -1
- package/src/web-search/loop.ts +21 -5
- package/gui/dist/assets/index-CQ7bIKee.css +0 -1
- package/gui/dist/assets/index-D_JUZLEC.js +0 -76
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider-opaque tool-call metadata across the Responses boundary (issue #1735).
|
|
3
|
+
*
|
|
4
|
+
* Gemini issues a `thoughtSignature` on the exact part that carries a function call, and the
|
|
5
|
+
* next request is only valid if that signature comes back on the part rebuilt from that same
|
|
6
|
+
* call. Every synthetic loop in this proxy (web search, images, continuation replay) tears a
|
|
7
|
+
* tool call down into id/name/arguments and builds a fresh one, which silently dropped the
|
|
8
|
+
* signature and left only the same-process replay cache to paper over it. History replay and
|
|
9
|
+
* `previous_response_id` had no cache to fall back on.
|
|
10
|
+
*
|
|
11
|
+
* This module is the single seam where that metadata crosses into and out of the Responses
|
|
12
|
+
* wire, so a loop that rebuilds a call only has to carry one field instead of knowing about
|
|
13
|
+
* any provider. Values are treated as opaque: never parsed, merged, re-encoded, or synthesized.
|
|
14
|
+
*/
|
|
15
|
+
import type { OcxProviderOpaqueToolCallMetadata } from "../types";
|
|
16
|
+
|
|
17
|
+
/** Wire shape: `extra_content.google.thought_signature` on a Responses function_call item. */
|
|
18
|
+
interface ResponsesExtraContent {
|
|
19
|
+
google?: { thought_signature?: unknown };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function isObj(value: unknown): value is Record<string, unknown> {
|
|
23
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Same ceiling the Antigravity replay cache already enforces on a stored signature. An opaque
|
|
28
|
+
* token this large is not a real signature, and accepting it would let a caller push unbounded
|
|
29
|
+
* state through history replay.
|
|
30
|
+
*/
|
|
31
|
+
const MAX_SIGNATURE_BYTES = 64 * 1024;
|
|
32
|
+
|
|
33
|
+
function isCarryableSignature(value: unknown): value is string {
|
|
34
|
+
if (typeof value !== "string" || value.length === 0) return false;
|
|
35
|
+
// Cheap length pre-check: UTF-8 is at most 3 bytes per UTF-16 code unit for the BMP, so this
|
|
36
|
+
// skips the encode for the overwhelmingly common short case.
|
|
37
|
+
if (value.length <= MAX_SIGNATURE_BYTES / 3) return true;
|
|
38
|
+
return Buffer.byteLength(value, "utf8") <= MAX_SIGNATURE_BYTES;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Read provider metadata off an inbound Responses function_call item. */
|
|
42
|
+
export function providerMetadataFromResponsesFunctionCall(
|
|
43
|
+
item: { extra_content?: unknown } | undefined,
|
|
44
|
+
): OcxProviderOpaqueToolCallMetadata | undefined {
|
|
45
|
+
const extra = item?.extra_content;
|
|
46
|
+
if (!isObj(extra)) return undefined;
|
|
47
|
+
const google = (extra as ResponsesExtraContent).google;
|
|
48
|
+
if (!isObj(google)) return undefined;
|
|
49
|
+
const signature = google.thought_signature;
|
|
50
|
+
if (!isCarryableSignature(signature)) return undefined;
|
|
51
|
+
return { google: { thoughtSignature: signature } };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Serialize provider metadata onto an outbound Responses function_call item. */
|
|
55
|
+
export function responsesExtraContentFromProviderMetadata(
|
|
56
|
+
metadata: OcxProviderOpaqueToolCallMetadata | undefined,
|
|
57
|
+
): { extra_content: { google: { thought_signature: string } } } | undefined {
|
|
58
|
+
const signature = metadata?.google?.thoughtSignature;
|
|
59
|
+
if (!isCarryableSignature(signature)) return undefined;
|
|
60
|
+
return { extra_content: { google: { thought_signature: signature } } };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Copy metadata for a rebuilt tool call. A signature belongs to one specific part, so a loop
|
|
65
|
+
* that fans one model response into several calls must copy per call and never share or merge.
|
|
66
|
+
*/
|
|
67
|
+
export function cloneProviderOpaqueToolCallMetadata(
|
|
68
|
+
metadata: OcxProviderOpaqueToolCallMetadata | undefined,
|
|
69
|
+
): OcxProviderOpaqueToolCallMetadata | undefined {
|
|
70
|
+
const signature = metadata?.google?.thoughtSignature;
|
|
71
|
+
if (!isCarryableSignature(signature)) return undefined;
|
|
72
|
+
return { google: { thoughtSignature: signature } };
|
|
73
|
+
}
|
package/src/responses/schema.ts
CHANGED
|
@@ -64,6 +64,12 @@ const functionCallItemSchema = z.object({
|
|
|
64
64
|
name: z.string().min(1),
|
|
65
65
|
namespace: z.string().optional(),
|
|
66
66
|
arguments: z.string().optional(),
|
|
67
|
+
// Provider-opaque metadata that must survive the round trip verbatim (issue #1735). The shape
|
|
68
|
+
// is bounded on purpose: only the one nested key we round-trip is modeled, so an unexpected
|
|
69
|
+
// payload cannot ride through as arbitrary passthrough state.
|
|
70
|
+
extra_content: z.object({
|
|
71
|
+
google: z.object({ thought_signature: z.string().optional() }).optional(),
|
|
72
|
+
}).optional(),
|
|
67
73
|
});
|
|
68
74
|
const functionCallOutputItemSchema = z.object({
|
|
69
75
|
type: z.literal("function_call_output"),
|
package/src/router.ts
CHANGED
|
@@ -11,8 +11,12 @@ import type { NormalizedComboConfig } from "./combos/types";
|
|
|
11
11
|
import { hasOwnProvider, resolveEnvValue } from "./config";
|
|
12
12
|
import { assertProviderDestinationAllowed } from "./lib/destination-policy";
|
|
13
13
|
import { redactSecretString, redactUrlForLog } from "./lib/redact";
|
|
14
|
-
import { PROVIDER_REGISTRY, providerCodexAccountMode
|
|
14
|
+
import { PROVIDER_REGISTRY, providerCodexAccountMode } from "./providers/registry";
|
|
15
15
|
import { applyDirectReasoningEffortContracts } from "./providers/derive";
|
|
16
|
+
import {
|
|
17
|
+
providerMatchesRegistryTransportWithStaticGuards,
|
|
18
|
+
providerSupportsLiveModelDiscovery,
|
|
19
|
+
} from "./providers/static-model-discovery";
|
|
16
20
|
import {
|
|
17
21
|
isCanonicalOpenAiForwardProvider,
|
|
18
22
|
LEGACY_CHATGPT_PROVIDER_ID,
|
|
@@ -86,7 +90,7 @@ const MODEL_PROVIDER_PATTERNS: Array<{ providerNames: string[]; prefixes: string
|
|
|
86
90
|
export function knownModelIdsForProvider(provName: string, prov: OcxProviderConfig): string[] {
|
|
87
91
|
const ids = new Set<string>();
|
|
88
92
|
for (const id of prov.models ?? []) ids.add(id);
|
|
89
|
-
const registry =
|
|
93
|
+
const registry = providerMatchesRegistryTransportWithStaticGuards(provName, prov)
|
|
90
94
|
? PROVIDER_REGISTRY.find(entry => entry.id === provName)
|
|
91
95
|
: undefined;
|
|
92
96
|
for (const id of registry?.models ?? []) ids.add(id);
|
|
@@ -99,6 +103,7 @@ export function knownModelIdsForProvider(provName: string, prov: OcxProviderConf
|
|
|
99
103
|
registry?.modelDefaultReasoningEfforts,
|
|
100
104
|
registry?.modelReasoningEffortMap,
|
|
101
105
|
registry?.modelMaxOutputTokens,
|
|
106
|
+
registry?.modelSupportsServiceTier,
|
|
102
107
|
]) {
|
|
103
108
|
for (const id of Object.keys(map ?? {})) ids.add(id);
|
|
104
109
|
}
|
|
@@ -249,20 +254,26 @@ function usableResolvedApiKey(apiKey: string | undefined): string | undefined {
|
|
|
249
254
|
|
|
250
255
|
export function routedProviderConfig(providerName: string, provider: OcxProviderConfig): OcxProviderConfig {
|
|
251
256
|
const registryEntry = PROVIDER_REGISTRY.find(entry => entry.id === providerName);
|
|
252
|
-
if (!registryEntry || !
|
|
257
|
+
if (!registryEntry || !providerMatchesRegistryTransportWithStaticGuards(providerName, provider)) {
|
|
253
258
|
assertProviderDestinationAllowed(providerName, provider);
|
|
254
259
|
return { ...provider, apiKey: usableResolvedApiKey(provider.apiKey) };
|
|
255
260
|
}
|
|
256
261
|
const resolvedApiKey = usableResolvedApiKey(provider.apiKey);
|
|
262
|
+
const staticModelCatalog = !providerSupportsLiveModelDiscovery(providerName, provider);
|
|
263
|
+
const repairLegacyMimoFreeAuth = providerName === "mimo-free"
|
|
264
|
+
&& staticModelCatalog
|
|
265
|
+
&& (provider.authMode === undefined || provider.authMode === "local");
|
|
257
266
|
const explicitKeyOverride = registryEntry.authKind === "oauth"
|
|
258
267
|
&& registryEntry.allowKeyAuthOverride === true
|
|
259
268
|
&& provider.authMode === "key"
|
|
260
269
|
&& resolvedApiKey !== undefined;
|
|
261
270
|
const canonicalAuthMode = explicitKeyOverride
|
|
262
271
|
? "key"
|
|
263
|
-
:
|
|
264
|
-
?
|
|
265
|
-
|
|
272
|
+
: repairLegacyMimoFreeAuth
|
|
273
|
+
? "key"
|
|
274
|
+
: registryEntry.authKind === "forward" || registryEntry.authKind === "oauth"
|
|
275
|
+
? registryEntry.authKind
|
|
276
|
+
: provider.authMode === "forward" ? undefined : provider.authMode;
|
|
266
277
|
const reasoningEffortMap = mergeRecord(registryEntry.reasoningEffortMap, provider.reasoningEffortMap);
|
|
267
278
|
const modelReasoningEffortMap = mergeNestedRecord(registryEntry.modelReasoningEffortMap, provider.modelReasoningEffortMap);
|
|
268
279
|
const modelReasoningEfforts = mergeStringArrayRecord(registryEntry.modelReasoningEfforts, provider.modelReasoningEfforts);
|
|
@@ -283,6 +294,10 @@ export function routedProviderConfig(providerName: string, provider: OcxProvider
|
|
|
283
294
|
? mergePositiveNumberCaps(registryEntry.modelMaxInputTokens, provider.modelMaxInputTokens)
|
|
284
295
|
: mergeRecordFill(registryEntry.modelMaxInputTokens, provider.modelMaxInputTokens);
|
|
285
296
|
const modelMaxOutputTokens = mergeRecordFill(registryEntry.modelMaxOutputTokens, provider.modelMaxOutputTokens);
|
|
297
|
+
const modelSupportsServiceTier = mergeRecordFill(
|
|
298
|
+
registryEntry.modelSupportsServiceTier,
|
|
299
|
+
provider.modelSupportsServiceTier,
|
|
300
|
+
);
|
|
286
301
|
const noVisionModels = mergeStringArray(registryEntry.noVisionModels, provider.noVisionModels);
|
|
287
302
|
const noReasoningModels = mergeStringArray(registryEntry.noReasoningModels, provider.noReasoningModels);
|
|
288
303
|
const noTemperatureModels = mergeStringArray(registryEntry.noTemperatureModels, provider.noTemperatureModels);
|
|
@@ -343,6 +358,7 @@ export function routedProviderConfig(providerName: string, provider: OcxProvider
|
|
|
343
358
|
: {}),
|
|
344
359
|
authMode: canonicalAuthMode,
|
|
345
360
|
apiKey: resolvedApiKey,
|
|
361
|
+
...(staticModelCatalog ? { liveModels: false } : {}),
|
|
346
362
|
// Backfill the Google wire mode + Vertex project/location from the registry when the user
|
|
347
363
|
// config omits them, so a minimal `google-vertex`/`google-antigravity` entry still routes
|
|
348
364
|
// through the correct branch (CCA/Vertex) instead of falling back to AI Studio.
|
|
@@ -372,6 +388,7 @@ export function routedProviderConfig(providerName: string, provider: OcxProvider
|
|
|
372
388
|
...(modelInputModalities ? { modelInputModalities } : {}),
|
|
373
389
|
...(modelMaxInputTokens ? { modelMaxInputTokens } : {}),
|
|
374
390
|
...(modelMaxOutputTokens ? { modelMaxOutputTokens } : {}),
|
|
391
|
+
...(modelSupportsServiceTier ? { modelSupportsServiceTier } : {}),
|
|
375
392
|
...(modelReasoningEfforts ? { modelReasoningEfforts } : {}),
|
|
376
393
|
...(modelDefaultReasoningEfforts ? { modelDefaultReasoningEfforts } : {}),
|
|
377
394
|
...(reasoningEffortMap ? { reasoningEffortMap } : {}),
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import type { OcxConfig } from "../types";
|
|
14
14
|
import { isCanonicalOpenAiForwardProvider, OPENAI_CODEX_PROVIDER_ID } from "../providers/openai-tiers";
|
|
15
|
+
import { serviceTierSupportForModel } from "../providers/service-tier";
|
|
15
16
|
import { applyProviderContextCap, providerContextCap } from "../providers/context-cap";
|
|
16
17
|
import { PROVIDER_REGISTRY } from "../providers/registry";
|
|
17
18
|
import {
|
|
@@ -185,8 +186,9 @@ export function candidateCapabilityEvidence(
|
|
|
185
186
|
?? catalogRow?.reasoningEfforts
|
|
186
187
|
?? (isNative ? nativeReasoningEfforts(modelId) : undefined);
|
|
187
188
|
|
|
188
|
-
const tierSupport = provider
|
|
189
|
-
|
|
189
|
+
const tierSupport = provider
|
|
190
|
+
? serviceTierSupportForModel(provider, modelId, providerName)
|
|
191
|
+
: registryEntry ? serviceTierSupportForModel(registryEntry, modelId, providerName) : undefined;
|
|
190
192
|
const serviceTier = tierSupport === true
|
|
191
193
|
? "supported"
|
|
192
194
|
: tierSupport === false ? "unsupported" : "unknown";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { OcxConfig, OcxProviderConfig } from "../../types";
|
|
2
2
|
import { PROVIDER_REGISTRY, type ProviderAuthKind } from "../../providers/registry";
|
|
3
|
+
import { serviceTierSupportForModel } from "../../providers/service-tier";
|
|
3
4
|
import { localFingerprint } from "../../lab/digest";
|
|
4
5
|
import type { LabBehaviorSource, LabBehaviorValues } from "../../lab/live/types";
|
|
5
6
|
|
|
@@ -116,7 +117,10 @@ export function resolveProductionBehaviorValues(
|
|
|
116
117
|
"auth.mode": behaviorRow("provider_config", authMode),
|
|
117
118
|
"auth.transport": behaviorRow("provider_config", authTransportFor(effective, adapter, authMode)),
|
|
118
119
|
"responses.stateful": behaviorRow("provider_config", effective.statelessResponses !== true),
|
|
119
|
-
"responses.serviceTier": behaviorRow(
|
|
120
|
+
"responses.serviceTier": behaviorRow(
|
|
121
|
+
"provider_config",
|
|
122
|
+
serviceTierSupportForModel(effective, modelId, providerName) ?? null,
|
|
123
|
+
),
|
|
120
124
|
"responses.snapshotRepair": behaviorRow("provider_config", effective.responsesSnapshotRepair === true),
|
|
121
125
|
"responses.itemIdRepair": behaviorRow("provider_config", effective.responsesItemIdRepair ?? null),
|
|
122
126
|
"limits.contextWindow": behaviorRow(
|
|
@@ -1,12 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { createAzureAdapter } from "../adapters/azure";
|
|
3
|
-
import { createCursorAdapter } from "../adapters/cursor";
|
|
4
|
-
import { createGoogleAdapter } from "../adapters/google";
|
|
5
|
-
import { createKiroAdapter } from "../adapters/kiro";
|
|
6
|
-
import { createMimoFreeAdapter } from "../adapters/mimo-free";
|
|
7
|
-
import { createOpenAIChatAdapter } from "../adapters/openai-chat";
|
|
8
|
-
import { createCommandCodeAdapter } from "../adapters/command-code";
|
|
9
|
-
import { createResponsesPassthroughAdapter } from "../adapters/openai-responses";
|
|
1
|
+
import { createRegisteredAdapter } from "../adapters/registry";
|
|
10
2
|
import type { OcxProviderConfig } from "../types";
|
|
11
3
|
import { isWirePinnedModel, MODEL_ADAPTER_OVERRIDE_ALLOWED, pinnedWireAdapter } from "../types";
|
|
12
4
|
import { isCanonicalOpenAiForwardProvider } from "../providers/openai-tiers";
|
|
@@ -57,27 +49,5 @@ export function resolveWireProtocolOverride(
|
|
|
57
49
|
|
|
58
50
|
/** Build the provider adapter for a resolved provider config. */
|
|
59
51
|
export function resolveAdapter(providerConfig: OcxProviderConfig, cacheRetention?: "none" | "short" | "long") {
|
|
60
|
-
|
|
61
|
-
case "command-code":
|
|
62
|
-
return createCommandCodeAdapter(providerConfig);
|
|
63
|
-
case "openai-chat":
|
|
64
|
-
return createOpenAIChatAdapter(providerConfig);
|
|
65
|
-
case "anthropic":
|
|
66
|
-
return createAnthropicAdapter(providerConfig, cacheRetention);
|
|
67
|
-
case "openai-responses":
|
|
68
|
-
return createResponsesPassthroughAdapter(providerConfig);
|
|
69
|
-
case "google":
|
|
70
|
-
return createGoogleAdapter(providerConfig);
|
|
71
|
-
case "kiro":
|
|
72
|
-
return createKiroAdapter(providerConfig);
|
|
73
|
-
case "azure":
|
|
74
|
-
case "azure-openai":
|
|
75
|
-
return createAzureAdapter(providerConfig);
|
|
76
|
-
case "cursor":
|
|
77
|
-
return createCursorAdapter(providerConfig);
|
|
78
|
-
case "mimo-free":
|
|
79
|
-
return createMimoFreeAdapter(providerConfig);
|
|
80
|
-
default:
|
|
81
|
-
throw new Error(`Unknown adapter: ${providerConfig.adapter}`);
|
|
82
|
-
}
|
|
52
|
+
return createRegisteredAdapter(providerConfig, { cacheRetention });
|
|
83
53
|
}
|
package/src/server/auth-cors.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
providerModelCostsConfigError,
|
|
15
15
|
reasoningSummaryDeliveryRecordConfigError,
|
|
16
16
|
retryOn429PolicyConfigError,
|
|
17
|
+
requestPacingConfigError,
|
|
17
18
|
sanitizeModelCostsForDisplay,
|
|
18
19
|
} from "../config";
|
|
19
20
|
import { providerDestinationConfigError } from "../lib/destination-policy";
|
|
@@ -139,17 +140,53 @@ export function browserSecurityHeaders(): Record<string, string> {
|
|
|
139
140
|
};
|
|
140
141
|
}
|
|
141
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Baseline data-plane request headers. ChatGPT-Account-Id is required for browser/Electron
|
|
145
|
+
* ChatGPT & Codex App voice preflights (direct forward auth matches the bearer to this account
|
|
146
|
+
* id). The OpenAI-Alpha .. X-OAI-Attestation block covers GPT-Live voice protocol headers
|
|
147
|
+
* relayed by the /v1/live call-create path.
|
|
148
|
+
*/
|
|
149
|
+
const STATIC_ALLOWED_REQUEST_HEADERS =
|
|
150
|
+
"Content-Type, Authorization, X-OpenCodex-API-Key, X-Api-Key, Anthropic-Version, Anthropic-Beta, ChatGPT-Account-Id, OpenAI-Alpha, X-Session-Id, Session-Id, Thread-Id, Originator, X-OAI-Attestation";
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* A fixed allow-list cannot enumerate vendor telemetry headers: the OpenAI and Anthropic
|
|
154
|
+
* browser SDKs send `X-Stainless-*` describing runtime and retry state, and the browser blocks
|
|
155
|
+
* the real request when the preflight omits even one of them (#1773).
|
|
156
|
+
*
|
|
157
|
+
* Echo what an already-allowed origin asked for, and fall back to the static list otherwise.
|
|
158
|
+
* The echo is deliberately gated on the origin check that ran first: this widens which headers
|
|
159
|
+
* an admitted caller may send, never which origins are admitted, and it grants nothing to an
|
|
160
|
+
* origin that would have been rejected anyway. Authentication is unchanged — the preflight
|
|
161
|
+
* itself carries no credential and produces no auth or account-pool side effect.
|
|
162
|
+
*/
|
|
163
|
+
function allowedRequestHeaders(req?: Request): string {
|
|
164
|
+
const requested = req?.headers.get("Access-Control-Request-Headers")?.trim();
|
|
165
|
+
if (!requested) return STATIC_ALLOWED_REQUEST_HEADERS;
|
|
166
|
+
const seen = new Set(STATIC_ALLOWED_REQUEST_HEADERS.split(",").map(h => h.trim().toLowerCase()));
|
|
167
|
+
const extra: string[] = [];
|
|
168
|
+
for (const raw of requested.split(",")) {
|
|
169
|
+
const name = raw.trim();
|
|
170
|
+
// Header names are case-insensitive on the wire, so normalize before de-duplicating;
|
|
171
|
+
// echo the caller's spelling for the ones we add.
|
|
172
|
+
if (!name || seen.has(name.toLowerCase())) continue;
|
|
173
|
+
seen.add(name.toLowerCase());
|
|
174
|
+
extra.push(name);
|
|
175
|
+
}
|
|
176
|
+
return extra.length === 0 ? STATIC_ALLOWED_REQUEST_HEADERS : `${STATIC_ALLOWED_REQUEST_HEADERS}, ${extra.join(", ")}`;
|
|
177
|
+
}
|
|
178
|
+
|
|
142
179
|
export function corsHeaders(req?: Request, config?: RequestPolicyView): Record<string, string> {
|
|
143
180
|
const origin = req?.headers.get("Origin");
|
|
144
|
-
const
|
|
181
|
+
const originAllowed = Boolean(origin && req && config && isAllowedRequestOrigin(req, config));
|
|
182
|
+
const allowOrigin = originAllowed && origin ? origin : _corsOrigin;
|
|
145
183
|
return {
|
|
146
184
|
"Access-Control-Allow-Origin": allowOrigin,
|
|
147
185
|
"Access-Control-Allow-Methods": "GET, POST, PUT, PATCH, DELETE, OPTIONS",
|
|
148
|
-
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
"
|
|
152
|
-
"Vary": "Origin",
|
|
186
|
+
"Access-Control-Allow-Headers": allowedRequestHeaders(originAllowed ? req : undefined),
|
|
187
|
+
// A response that varies by the request's headers must say so, or a shared cache can
|
|
188
|
+
// replay one client's allow-list to a client that asked for different headers.
|
|
189
|
+
"Vary": "Origin, Access-Control-Request-Headers",
|
|
153
190
|
...browserSecurityHeaders(),
|
|
154
191
|
};
|
|
155
192
|
}
|
|
@@ -453,6 +490,8 @@ export function providerManagementConfigError(name: unknown, provider: unknown):
|
|
|
453
490
|
// modelCosts is a user-owned display overlay, not part of the canonical
|
|
454
491
|
// forward seed; it is validated separately below (providerModelCostsConfigError).
|
|
455
492
|
delete canonicalCandidate.modelCosts;
|
|
493
|
+
// requestPacing is a user-owned transport overlay, not part of the canonical seed.
|
|
494
|
+
delete canonicalCandidate.requestPacing;
|
|
456
495
|
const canonical = seed && sameCanonicalProviderSeed(canonicalCandidate, seed);
|
|
457
496
|
if (!canonical) {
|
|
458
497
|
return `provider ${name} must equal the canonical built-in provider seed`;
|
|
@@ -477,6 +516,10 @@ export function providerManagementConfigError(name: unknown, provider: unknown):
|
|
|
477
516
|
// it before it reaches the management API response.
|
|
478
517
|
return `provider ${JSON.stringify(redactSecretString(name))} ${retryOn429Error}`;
|
|
479
518
|
}
|
|
519
|
+
const requestPacingError = requestPacingConfigError(raw.requestPacing);
|
|
520
|
+
if (requestPacingError) {
|
|
521
|
+
return `provider ${JSON.stringify(redactSecretString(name))} ${requestPacingError}`;
|
|
522
|
+
}
|
|
480
523
|
const modelCostsError = providerModelCostsConfigError(raw.modelCosts);
|
|
481
524
|
if (modelCostsError) {
|
|
482
525
|
// The provider name is caller-controlled and can be token-shaped; redact and JSON-escape
|
|
@@ -580,6 +623,7 @@ export function safeConfigDTO(config: OcxConfig): unknown {
|
|
|
580
623
|
"keyOptional",
|
|
581
624
|
"freeTier",
|
|
582
625
|
"liveModels",
|
|
626
|
+
"requestPacing",
|
|
583
627
|
"models",
|
|
584
628
|
"contextWindow",
|
|
585
629
|
"modelContextWindows",
|
|
@@ -2,11 +2,15 @@
|
|
|
2
2
|
* OpenAI Chat Completions inbound (/v1/chat/completions) for GitHub Copilot App
|
|
3
3
|
* and other OpenAI-compatible clients.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* Ordinary openai-chat routes send directly on the Chat Completions wire. Routes
|
|
6
|
+
* that need Responses-only behavior keep the Chat -> Responses -> Chat bridge.
|
|
7
7
|
*/
|
|
8
8
|
import { FORWARD_HEADERS } from "../adapters/openai-responses";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
assertChatCompletionsRoutingBody,
|
|
11
|
+
ChatCompletionsRequestError,
|
|
12
|
+
chatCompletionsToResponsesBody,
|
|
13
|
+
} from "../chat/inbound";
|
|
10
14
|
import {
|
|
11
15
|
chatCompletionsErrorResponse,
|
|
12
16
|
collectChatCompletion,
|
|
@@ -40,6 +44,7 @@ import {
|
|
|
40
44
|
isTranslatorBudgetExceededError,
|
|
41
45
|
type TranslatorBudget,
|
|
42
46
|
} from "../lib/translator-budget";
|
|
47
|
+
import { handleNativeChatCompletions, isNativeChatRouteEligible } from "./chat-native";
|
|
43
48
|
|
|
44
49
|
type Rec = Record<string, unknown>;
|
|
45
50
|
|
|
@@ -80,11 +85,11 @@ async function handleChatCompletionsWithBudget(
|
|
|
80
85
|
translatorBudget: TranslatorBudget,
|
|
81
86
|
logIds?: { requestId: string; start: number; turnAdmissionLease?: AdmissionLease },
|
|
82
87
|
): Promise<Response> {
|
|
83
|
-
let chatBody:
|
|
84
|
-
let internalBody: Rec;
|
|
88
|
+
let chatBody: Rec;
|
|
85
89
|
try {
|
|
86
|
-
|
|
87
|
-
|
|
90
|
+
const rawBody = await readChatBody(req, translatorBudget);
|
|
91
|
+
assertChatCompletionsRoutingBody(rawBody);
|
|
92
|
+
chatBody = rawBody;
|
|
88
93
|
} catch (err) {
|
|
89
94
|
const overflow = isTranslatorBudgetExceededError(err);
|
|
90
95
|
const status = overflow ? 413 : err instanceof ChatCompletionsRequestError ? 400 : 500;
|
|
@@ -97,20 +102,17 @@ async function handleChatCompletionsWithBudget(
|
|
|
97
102
|
);
|
|
98
103
|
}
|
|
99
104
|
|
|
100
|
-
const requestedModel =
|
|
101
|
-
const stream =
|
|
105
|
+
const requestedModel = chatBody.model as string;
|
|
106
|
+
const stream = chatBody.stream === true;
|
|
102
107
|
// Best-effort Grok attribution: the managed fence stamps this header on every model
|
|
103
108
|
// it registers (extra_headers, sent verbatim by upstream Grok). Dashboard usage
|
|
104
109
|
// bucketing only — never an auth or billing signal.
|
|
105
110
|
if (req.headers.get("x-opencodex-grok") === "1") logCtx.surface = "grok";
|
|
106
|
-
// Routed adapters only support streamed turns; always stream internally and fold
|
|
107
|
-
// for non-streaming clients.
|
|
108
|
-
internalBody.stream = true;
|
|
109
|
-
|
|
110
|
-
let nativeRoute = false;
|
|
111
111
|
let directRoute = false;
|
|
112
|
+
let settledRoute: ReturnType<typeof routeModel> | null = null;
|
|
113
|
+
let chatNativeRoute: ReturnType<typeof routeModel> | null = null;
|
|
112
114
|
try {
|
|
113
|
-
const route = routeModel(config,
|
|
115
|
+
const route = routeModel(config, requestedModel, evidenceFromBody(chatBody));
|
|
114
116
|
// Settle the wire once so every branch below reads the adapter this model will
|
|
115
117
|
// actually use, not the provider-wide default (#404).
|
|
116
118
|
route.provider = resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, "chat");
|
|
@@ -119,33 +121,17 @@ async function handleChatCompletionsWithBudget(
|
|
|
119
121
|
logCtx.requestedModel = requestedModel;
|
|
120
122
|
logCtx.provider = route.providerName;
|
|
121
123
|
logCtx.routeDecision = route.routeDecision;
|
|
124
|
+
settledRoute = route;
|
|
122
125
|
if (route.provider.adapter === "openai-responses") {
|
|
123
|
-
nativeRoute = true;
|
|
124
126
|
directRoute = route.codexAccountMode === "direct";
|
|
125
|
-
// ChatGPT backend rejects store:true and unsupported sampling knobs.
|
|
126
|
-
internalBody.store = false;
|
|
127
|
-
delete internalBody.max_output_tokens;
|
|
128
|
-
delete internalBody.temperature;
|
|
129
|
-
delete internalBody.top_p;
|
|
130
|
-
delete internalBody.stop;
|
|
131
|
-
delete internalBody.user;
|
|
132
|
-
} else if (internalBody.store === undefined) {
|
|
133
|
-
internalBody.store = false;
|
|
134
127
|
}
|
|
135
128
|
if (route.provider.adapter === "cursor" || route.provider.adapter === "kiro") {
|
|
136
|
-
const raw = chatBody as Rec;
|
|
137
129
|
const parts: string[] = [];
|
|
138
|
-
if (
|
|
139
|
-
if (
|
|
130
|
+
if (chatBody.messages !== undefined) parts.push(JSON.stringify(chatBody.messages));
|
|
131
|
+
if (chatBody.tools !== undefined) parts.push(JSON.stringify(chatBody.tools));
|
|
140
132
|
logCtx.usageLogInputTokens = Math.max(1, estimateTokens(parts.join("\n"), requestedModel));
|
|
141
133
|
}
|
|
142
|
-
if (
|
|
143
|
-
const { stripEmptyLadderEffort, supportedLadderFor } = await import("./effort-policy");
|
|
144
|
-
const ladder = supportedLadderFor({ provider: route.provider, modelId: route.modelId });
|
|
145
|
-
const next = stripEmptyLadderEffort(internalBody.reasoning, ladder);
|
|
146
|
-
if (next === undefined) delete internalBody.reasoning;
|
|
147
|
-
else internalBody.reasoning = next;
|
|
148
|
-
}
|
|
134
|
+
if (isNativeChatRouteEligible(route, chatBody)) chatNativeRoute = route;
|
|
149
135
|
} catch (err) {
|
|
150
136
|
if (err instanceof NoEligiblePolicyCandidateError) {
|
|
151
137
|
logCtx.routeDecision = err.trace;
|
|
@@ -154,7 +140,59 @@ async function handleChatCompletionsWithBudget(
|
|
|
154
140
|
}
|
|
155
141
|
/* unknown model: let handleResponses shape the 404 */
|
|
156
142
|
}
|
|
157
|
-
|
|
143
|
+
|
|
144
|
+
if (chatNativeRoute) {
|
|
145
|
+
return handleNativeChatCompletions({
|
|
146
|
+
req,
|
|
147
|
+
config,
|
|
148
|
+
logCtx,
|
|
149
|
+
...(logIds ? { logIds } : {}),
|
|
150
|
+
route: chatNativeRoute,
|
|
151
|
+
chatBody,
|
|
152
|
+
requestedModel,
|
|
153
|
+
requestedStream: stream,
|
|
154
|
+
translatorBudget,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
let internalBody: Rec;
|
|
159
|
+
try {
|
|
160
|
+
// Validate the full Chat boundary after routing. Native Chat keeps `chatBody` as
|
|
161
|
+
// its wire source; this Responses projection is used only by the fallback path.
|
|
162
|
+
internalBody = chatCompletionsToResponsesBody(chatBody);
|
|
163
|
+
} catch (err) {
|
|
164
|
+
const overflow = isTranslatorBudgetExceededError(err);
|
|
165
|
+
const status = overflow ? 413 : err instanceof ChatCompletionsRequestError ? 400 : 500;
|
|
166
|
+
if (logIds) addFinalRequestLog(logIds.requestId, logIds.start, logCtx, status, { closeReason: "non_stream" });
|
|
167
|
+
return chatCompletionsErrorResponse(
|
|
168
|
+
status,
|
|
169
|
+
overflow ? "request translation buffer exceeded the safe limit" : err instanceof Error ? err.message : String(err),
|
|
170
|
+
overflow ? "request_too_large" : undefined,
|
|
171
|
+
overflow ? "translation_buffer_limit" : undefined,
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Routed adapters only support streamed turns; always stream internally and fold
|
|
176
|
+
// for non-streaming clients. Native Chat uses the caller's original stream bit.
|
|
177
|
+
internalBody.stream = true;
|
|
178
|
+
if (settledRoute?.provider.adapter === "openai-responses") {
|
|
179
|
+
// ChatGPT backend rejects store:true and unsupported sampling knobs.
|
|
180
|
+
internalBody.store = false;
|
|
181
|
+
delete internalBody.max_output_tokens;
|
|
182
|
+
delete internalBody.temperature;
|
|
183
|
+
delete internalBody.top_p;
|
|
184
|
+
delete internalBody.stop;
|
|
185
|
+
delete internalBody.user;
|
|
186
|
+
} else if (internalBody.store === undefined) {
|
|
187
|
+
internalBody.store = false;
|
|
188
|
+
}
|
|
189
|
+
if (settledRoute && internalBody.reasoning !== undefined) {
|
|
190
|
+
const { stripEmptyLadderEffort, supportedLadderFor } = await import("./effort-policy");
|
|
191
|
+
const ladder = supportedLadderFor({ provider: settledRoute.provider, modelId: settledRoute.modelId });
|
|
192
|
+
const next = stripEmptyLadderEffort(internalBody.reasoning, ladder);
|
|
193
|
+
if (next === undefined) delete internalBody.reasoning;
|
|
194
|
+
else internalBody.reasoning = next;
|
|
195
|
+
}
|
|
158
196
|
|
|
159
197
|
const headers = new Headers({ "content-type": "application/json" });
|
|
160
198
|
for (const name of FORWARD_HEADERS) {
|