@bitkyc08/opencodex 2.26.0 → 2.28.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-RL6b1bTV.js → index-D2sP-biU.js} +14 -14
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +60 -1
- package/src/adapters/base.ts +16 -2
- package/src/adapters/command-code.ts +4 -3
- package/src/adapters/cursor/cursor-errors.ts +15 -0
- package/src/adapters/cursor/live-transport.ts +14 -1
- package/src/adapters/google-antigravity-replay.ts +16 -8
- package/src/adapters/google.ts +22 -5
- package/src/adapters/openai-chat.ts +189 -60
- package/src/adapters/openai-responses.ts +37 -0
- package/src/adapters/tool-catalog-nudge.ts +1 -1
- package/src/bridge.ts +11 -5
- package/src/cli/doctor.ts +76 -0
- package/src/cli/help.ts +2 -0
- package/src/cli/index.ts +19 -6
- package/src/cli/models.ts +13 -6
- package/src/codex/account-usability.ts +3 -0
- package/src/codex/app-server-processes.ts +269 -37
- package/src/codex/auth-api.ts +22 -5
- package/src/codex/auth-context.ts +108 -3
- package/src/codex/catalog/aggregation.ts +3 -0
- package/src/codex/catalog/metadata.ts +17 -3
- package/src/codex/catalog/native-models.ts +22 -14
- package/src/codex/catalog/parsing.ts +20 -3
- package/src/codex/catalog/provider-fetch.ts +8 -0
- package/src/codex/catalog/sync.ts +63 -15
- package/src/codex/convergence.ts +61 -13
- package/src/codex/log-guard/path-safety.ts +52 -3
- package/src/codex/model-entitlements.ts +353 -0
- package/src/codex/native-profile-startup.ts +100 -2
- package/src/codex/quota.ts +28 -3
- package/src/codex/routing.ts +14 -8
- package/src/codex/user-identity.ts +21 -1
- package/src/config/provider-name.ts +24 -0
- package/src/config.ts +11 -24
- package/src/generated/compatibility-version.json +110 -70
- package/src/images/loop.ts +11 -4
- package/src/lib/destination-policy.ts +47 -0
- package/src/lib/shadow-call.ts +15 -0
- package/src/lib/state-store-registrations.ts +8 -2
- package/src/oauth/index.ts +33 -5
- package/src/oauth/store.ts +11 -5
- package/src/providers/antigravity-models.ts +70 -5
- package/src/providers/derive.ts +12 -2
- package/src/providers/fastwire.ts +39 -8
- package/src/providers/quota.ts +9 -2
- package/src/providers/registry.ts +120 -6
- package/src/providers/service-tier.ts +50 -15
- package/src/responses/parser.ts +59 -11
- package/src/responses/state.ts +162 -5
- package/src/responses/tool-search-compat.ts +301 -0
- package/src/router.ts +17 -3
- package/src/routing/capability.ts +26 -9
- package/src/routing/compatibility/behavior.ts +44 -6
- package/src/routing/profile.ts +1 -1
- package/src/server/chat-native.ts +11 -2
- package/src/server/index.ts +59 -8
- package/src/server/management/agent-settings-routes.ts +16 -2
- package/src/server/management/shared.ts +3 -1
- package/src/server/request-log.ts +31 -0
- package/src/server/responses/collaboration.ts +34 -9
- package/src/server/responses/compact.ts +54 -7
- package/src/server/responses/core.ts +259 -43
- package/src/server/responses/input-admission.ts +7 -2
- package/src/server/responses/responses-field-backfill.ts +88 -6
- package/src/server/responses/terminal-guard.ts +10 -0
- package/src/server/responses-tool-search-repair.ts +217 -0
- package/src/server/system-env.ts +74 -5
- package/src/service-manager-probe.ts +99 -0
- package/src/service.ts +86 -6
- package/src/tray/windows.ts +25 -5
- package/src/types/accounts.ts +37 -0
- package/src/types/config.ts +818 -0
- package/src/types/provider.ts +521 -0
- package/src/types/request.ts +358 -0
- package/src/types/tools.ts +131 -0
- package/src/types/wire.ts +80 -0
- package/src/types.ts +103 -1883
- package/src/usage/cost.ts +37 -1
- package/src/usage/log.ts +4 -0
- package/src/web-search/loop.ts +11 -4
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
cursorModelReasoningEfforts,
|
|
18
18
|
} from "../adapters/cursor/discovery";
|
|
19
19
|
import { COMMAND_CODE_MODEL_REASONING_EFFORTS } from "./command-code-efforts";
|
|
20
|
+
import { isCanonicalOpenRouterTarget } from "./openrouter-routing";
|
|
20
21
|
|
|
21
22
|
export type ProviderAuthKind = "forward" | "oauth" | "key" | "local";
|
|
22
23
|
export type MetadataModelIdNormalize = "case-insensitive";
|
|
@@ -30,9 +31,15 @@ export type InboundWire = "responses" | "chat" | "anthropic";
|
|
|
30
31
|
|
|
31
32
|
/**
|
|
32
33
|
* A per-model wire default: a bare string applies to every inbound, while the object
|
|
33
|
-
* form
|
|
34
|
+
* form may scope the default to listed inbound protocols and authentication modes.
|
|
34
35
|
*/
|
|
35
|
-
export type ModelWireDefault = string | {
|
|
36
|
+
export type ModelWireDefault = string | {
|
|
37
|
+
wire: string;
|
|
38
|
+
inbound: readonly InboundWire[];
|
|
39
|
+
authModes?: readonly ProviderAuthKind[];
|
|
40
|
+
/** Whether this registry-selected route may relay a caller-owned service_tier. */
|
|
41
|
+
forwardCallerServiceTier?: boolean;
|
|
42
|
+
};
|
|
36
43
|
|
|
37
44
|
export interface ResponsesTerminalRepairPolicy {
|
|
38
45
|
/** Quiet time after a structurally complete output graph before synthesizing completion. */
|
|
@@ -217,6 +224,11 @@ export interface ProviderRegistryEntry {
|
|
|
217
224
|
supportsServiceTier?: boolean;
|
|
218
225
|
/** Registry default for exact model service-tier capability; explicit config keys win. */
|
|
219
226
|
modelSupportsServiceTier?: Record<string, boolean>;
|
|
227
|
+
/**
|
|
228
|
+
* Registry-only destination guard for `modelSupportsServiceTier`. This scopes vendor evidence
|
|
229
|
+
* without changing provider ownership, routing, authentication, or config validation.
|
|
230
|
+
*/
|
|
231
|
+
modelServiceTierCapabilityBaseUrlGuard?: (baseUrl: string) => boolean;
|
|
220
232
|
/** Registry default for plaintext reasoning replay; see `OcxProviderConfig.preserveResponsesReasoningContent`. Registry-only like `supportsServiceTier`. */
|
|
221
233
|
preserveResponsesReasoningContent?: boolean;
|
|
222
234
|
/** Registry defaults for per-model Codex reasoning propagation; explicit user keys win during enrichment. */
|
|
@@ -1011,6 +1023,24 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
1011
1023
|
// grok-4.5; the reasoning ladder does not — 4.6 adds the documented xhigh rung.
|
|
1012
1024
|
models: ["grok-4.6", "grok-4.5", "grok-4.3", "grok-4.20-0309-reasoning", "grok-4.20-0309-non-reasoning", "grok-build-0.1", "grok-composer-2.5-fast"],
|
|
1013
1025
|
defaultModel: "grok-4.5",
|
|
1026
|
+
// The current Grok CLI catalog declares both subscription models as native Responses
|
|
1027
|
+
// backends. Keep API-key and translated Chat/Anthropic callers on their existing wire;
|
|
1028
|
+
// Codex Responses traffic can relay xAI's SSE as it arrives instead of waiting for the
|
|
1029
|
+
// Chat Completions compatibility stream to flush at the end of a reasoning turn.
|
|
1030
|
+
modelWireDefaults: {
|
|
1031
|
+
"grok-4.6": {
|
|
1032
|
+
wire: "openai-responses",
|
|
1033
|
+
inbound: ["responses"],
|
|
1034
|
+
authModes: ["oauth"],
|
|
1035
|
+
forwardCallerServiceTier: false,
|
|
1036
|
+
},
|
|
1037
|
+
"grok-4.5": {
|
|
1038
|
+
wire: "openai-responses",
|
|
1039
|
+
inbound: ["responses"],
|
|
1040
|
+
authModes: ["oauth"],
|
|
1041
|
+
forwardCallerServiceTier: false,
|
|
1042
|
+
},
|
|
1043
|
+
},
|
|
1014
1044
|
// Vision lineup per docs.x.ai model-capabilities/images/understanding: the grok-4.x chat
|
|
1015
1045
|
// models accept image input (JPEG/PNG, URL or base64). Without this the catalog leaves
|
|
1016
1046
|
// inputModalities undefined, and deriveComboCatalogModel defaults an undefined member to
|
|
@@ -1077,6 +1107,7 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
1077
1107
|
adapter: "anthropic",
|
|
1078
1108
|
baseUrl: "https://api.anthropic.com",
|
|
1079
1109
|
authKind: "oauth",
|
|
1110
|
+
allowBaseUrlOverride: true,
|
|
1080
1111
|
featured: true,
|
|
1081
1112
|
oauthId: "anthropic",
|
|
1082
1113
|
jawcodeBundle: "anthropic",
|
|
@@ -1349,7 +1380,33 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
1349
1380
|
autoToolChoiceOnlyModels: ["kimi-k2.7-code"],
|
|
1350
1381
|
preserveReasoningContentModels: NEURALWATT_REASONING_HISTORY_MODELS,
|
|
1351
1382
|
},
|
|
1352
|
-
{
|
|
1383
|
+
{
|
|
1384
|
+
id: "openrouter",
|
|
1385
|
+
label: "OpenRouter",
|
|
1386
|
+
adapter: "openai-chat",
|
|
1387
|
+
baseUrl: "https://openrouter.ai/api/v1",
|
|
1388
|
+
authKind: "key",
|
|
1389
|
+
featured: true,
|
|
1390
|
+
dashboardUrl: "https://openrouter.ai/keys",
|
|
1391
|
+
jawcodeBundle: "openrouter",
|
|
1392
|
+
models: ["anthropic/claude-sonnet-5", ...OPENROUTER_GPT56_MODELS],
|
|
1393
|
+
modelContextWindows: {
|
|
1394
|
+
"anthropic/claude-sonnet-5": 1_000_000,
|
|
1395
|
+
...OPENROUTER_GPT56_CONTEXT_WINDOWS,
|
|
1396
|
+
},
|
|
1397
|
+
// OpenRouter documents priority support for OpenAI endpoints, but not Anthropic. Keep the
|
|
1398
|
+
// provider unclassified and opt in only the exact OpenAI-backed slugs we ship. These facts
|
|
1399
|
+
// belong only to the canonical destination; a same-named custom gateway is unknown to us.
|
|
1400
|
+
modelServiceTierCapabilityBaseUrlGuard: isCanonicalOpenRouterTarget,
|
|
1401
|
+
modelSupportsServiceTier: {
|
|
1402
|
+
"openai/gpt-5.6-sol": true,
|
|
1403
|
+
"openai/gpt-5.6-terra": true,
|
|
1404
|
+
"openai/gpt-5.6-luna": true,
|
|
1405
|
+
},
|
|
1406
|
+
// Deliberately no OpenRouter route pin: it bills the endpoint actually used and reports the
|
|
1407
|
+
// actual service_tier. B0 confirmation therefore owns downgrade safety. Forcing `only` plus
|
|
1408
|
+
// `allow_fallbacks:false` would turn a graceful priority-capacity fallback into a hard failure.
|
|
1409
|
+
},
|
|
1353
1410
|
{
|
|
1354
1411
|
// Primary sources checked 2026-08-02:
|
|
1355
1412
|
// - docs.cline.bot/getting-started/clinepass publishes this exact catalog and explicitly
|
|
@@ -1464,7 +1521,7 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
1464
1521
|
// 2026-07-10: defaultModel is frozen pending Vertex-specific Tier-2 evidence; Gemini API
|
|
1465
1522
|
// evidence from ai.google.dev does not establish Vertex publisher availability.
|
|
1466
1523
|
{ id: "google-vertex", label: "Google Vertex AI", adapter: "google", baseUrl: "https://aiplatform.googleapis.com", authKind: "key", dashboardUrl: "https://console.cloud.google.com/vertex-ai", defaultModel: "gemini-3-pro", googleMode: "vertex", jawcodeBundle: "google", extraMetadataAliases: ["gemini-vertex"] },
|
|
1467
|
-
{ id: "google-antigravity", label: "Google Antigravity", adapter: "google", baseUrl: "https://daily-cloudcode-pa.googleapis.com", authKind: "oauth", dashboardUrl: "https://antigravity.google", models: ANTIGRAVITY_MODELS, liveModels: true, defaultModel: "gemini-3.7-flash", modelContextWindows: ANTIGRAVITY_MODEL_CONTEXT_WINDOWS, modelInputModalities: ANTIGRAVITY_MODEL_INPUT_MODALITIES, modelReasoningEfforts: ANTIGRAVITY_MODEL_EFFORTS, googleMode: "cloud-code-assist", jawcodeBundle: "google", extraMetadataAliases: ["antigravity", "gemini-antigravity"] },
|
|
1524
|
+
{ id: "google-antigravity", label: "Google Antigravity", adapter: "google", baseUrl: "https://daily-cloudcode-pa.googleapis.com", authKind: "oauth", allowBaseUrlOverride: true, dashboardUrl: "https://antigravity.google", models: ANTIGRAVITY_MODELS, liveModels: true, defaultModel: "gemini-3.7-flash", modelContextWindows: ANTIGRAVITY_MODEL_CONTEXT_WINDOWS, modelInputModalities: ANTIGRAVITY_MODEL_INPUT_MODALITIES, modelReasoningEfforts: ANTIGRAVITY_MODEL_EFFORTS, googleMode: "cloud-code-assist", jawcodeBundle: "google", extraMetadataAliases: ["antigravity", "gemini-antigravity"] },
|
|
1468
1525
|
{ id: "azure-openai", label: "Azure OpenAI", adapter: "azure-openai", baseUrl: "https://{resource}.openai.azure.com/openai", authKind: "key", featured: true, dashboardUrl: "https://portal.azure.com" },
|
|
1469
1526
|
{ id: "ollama", label: "Ollama (local)", adapter: "openai-chat", baseUrl: "http://localhost:11434/v1", authKind: "local", allowPrivateNetworkByDefault: true, allowBaseUrlOverride: true, featured: true, note: "Local — key usually blank" },
|
|
1470
1527
|
{ id: "vllm", label: "vLLM (local)", adapter: "openai-chat", baseUrl: "http://localhost:8000/v1", authKind: "local", allowPrivateNetworkByDefault: true, allowBaseUrlOverride: true, featured: true, note: "Local — key usually blank" },
|
|
@@ -2403,6 +2460,16 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
2403
2460
|
note: "No key needed — public desktop tier. OpenCode currently advertises about 200 Big Pickle/free-model requests per 5 hours. The same Zen gateway can also short-window rate-limit free models at roughly 15-20 requests/minute, and may return generic 429s without Retry-After (opencodex synthesizes backoff only when that header is omitted). Free models are discovered live from Zen. Data use: per OpenCode's Zen docs (https://opencode.ai/docs/zen/), prompts sent to free models may be retained and used for training/improvement — do not send confidential material through this provider.",
|
|
2404
2461
|
dashboardUrl: "https://opencode.ai",
|
|
2405
2462
|
staticHeaders: {
|
|
2463
|
+
// Zen answers a bare runtime User-Agent (Bun/x.y.z) more aggressively than a client
|
|
2464
|
+
// that identifies itself, which is what the 429 in #2067 traced to. The value is
|
|
2465
|
+
// deliberately unversioned: a pinned "opencode-cli/<version>" is a claim about an
|
|
2466
|
+
// install we do not have and goes stale on the vendor's schedule, not ours.
|
|
2467
|
+
// Corroboration, not authority: OmniRoute — an independent open-source broker against
|
|
2468
|
+
// the same Zen upstream — defaults to exactly this pair (userAgent "opencode", client
|
|
2469
|
+
// "desktop") in open-sse/executors/opencode.ts, and got there by RETREATING from its
|
|
2470
|
+
// own earlier "opencode-cli/1.0.0" pin. An operator can still override either value
|
|
2471
|
+
// through the provider headers API; user headers win case-insensitively at route time.
|
|
2472
|
+
"User-Agent": "opencode",
|
|
2406
2473
|
"x-opencode-client": "desktop",
|
|
2407
2474
|
},
|
|
2408
2475
|
modelReasoningEfforts: Object.fromEntries(OPENCODE_FREE_DEEPSEEK_MODELS.map(id => [id, deepseekThinkingEffortsFor(id)])),
|
|
@@ -2470,6 +2537,10 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
2470
2537
|
// The gateway validates the ladder strictly and rejects anything above `high`.
|
|
2471
2538
|
reasoningEfforts: ["low", "medium", "high"],
|
|
2472
2539
|
reasoningEffortMap: { xhigh: "high", max: "high", ultra: "high" },
|
|
2540
|
+
// Live token-plan verification (#1927): the Pro route rejects image input while
|
|
2541
|
+
// mimo-v2.5 accepts it natively. Keep this provider-scoped so a hand-rolled
|
|
2542
|
+
// provider with the same id but another destination does not inherit the claim.
|
|
2543
|
+
noVisionModels: ["mimo-v2.5-pro"],
|
|
2473
2544
|
// A user may already have hand-rolled a provider under this id against a different host;
|
|
2474
2545
|
// without this, routedProviderConfig() would canonicalize their base URL onto ours and send
|
|
2475
2546
|
// their key somewhere they did not choose.
|
|
@@ -2555,6 +2626,45 @@ export function getProviderRegistryEntry(id: string): ProviderRegistryEntry | un
|
|
|
2555
2626
|
return PROVIDER_REGISTRY.find(entry => entry.id === id);
|
|
2556
2627
|
}
|
|
2557
2628
|
|
|
2629
|
+
/**
|
|
2630
|
+
* Merge a registry row's `staticHeaders` beneath a provider's own headers.
|
|
2631
|
+
*
|
|
2632
|
+
* The field is documented as "merged into every upstream request for this provider", but that
|
|
2633
|
+
* was only ever true for a freshly seeded config: `providerConfigSeed` copies the block once
|
|
2634
|
+
* (`derive.ts`), `enrichProviderFromCatalog` fills it only when the whole block is absent, and
|
|
2635
|
+
* nothing merged it at request time. So an install that predates a header — or that saved any
|
|
2636
|
+
* header of its own — never received the new one, which is exactly what #2067 would have
|
|
2637
|
+
* shipped for every existing opencode-free user.
|
|
2638
|
+
*
|
|
2639
|
+
* The comparison is case-insensitive on purpose. HTTP header names are case-insensitive, but a
|
|
2640
|
+
* plain object spread is not: merging a registry `User-Agent` over a user's `user-agent`
|
|
2641
|
+
* produces two entries that `Headers` serializes as one comma-joined value
|
|
2642
|
+
* ("opencode, custom-agent"), which is a corrupted request rather than an override. The user's
|
|
2643
|
+
* spelling and value both win; the registry only fills names the user has not spoken for.
|
|
2644
|
+
*/
|
|
2645
|
+
export function mergeRegistryStaticHeaders(
|
|
2646
|
+
staticHeaders: Record<string, string> | undefined,
|
|
2647
|
+
userHeaders: Record<string, string> | undefined,
|
|
2648
|
+
): Record<string, string> | undefined {
|
|
2649
|
+
if (!staticHeaders) return userHeaders;
|
|
2650
|
+
if (!userHeaders) return { ...staticHeaders };
|
|
2651
|
+
const claimed = new Set(Object.keys(userHeaders).map(name => name.toLowerCase()));
|
|
2652
|
+
const merged: Record<string, string> = { ...userHeaders };
|
|
2653
|
+
for (const [name, value] of Object.entries(staticHeaders)) {
|
|
2654
|
+
if (!claimed.has(name.toLowerCase())) merged[name] = value;
|
|
2655
|
+
}
|
|
2656
|
+
return merged;
|
|
2657
|
+
}
|
|
2658
|
+
|
|
2659
|
+
/** Whether this registry row's per-model service-tier evidence applies to one configured target. */
|
|
2660
|
+
export function registryModelServiceTierCapabilityApplies(
|
|
2661
|
+
entry: Pick<ProviderRegistryEntry, "modelServiceTierCapabilityBaseUrlGuard">,
|
|
2662
|
+
provider: Pick<OcxProviderConfig, "baseUrl">,
|
|
2663
|
+
): boolean {
|
|
2664
|
+
const guard = entry.modelServiceTierCapabilityBaseUrlGuard;
|
|
2665
|
+
return guard === undefined || guard(provider.baseUrl);
|
|
2666
|
+
}
|
|
2667
|
+
|
|
2558
2668
|
function normalizedProviderEndpoint(value: string): string {
|
|
2559
2669
|
const trimmed = value.trim();
|
|
2560
2670
|
try {
|
|
@@ -2634,8 +2744,12 @@ export function providerModelWireDefault(
|
|
|
2634
2744
|
if (!entry?.modelWireDefaults || !providerMatchesRegistryTransport(id, provider)) return undefined;
|
|
2635
2745
|
const declared = entry.modelWireDefaults[modelId.trim().toLowerCase()];
|
|
2636
2746
|
if (declared === undefined) return undefined;
|
|
2637
|
-
// A bare string applies to every inbound; the object form
|
|
2638
|
-
if (typeof declared !== "string"
|
|
2747
|
+
// A bare string applies to every inbound/auth mode; the object form may narrow either.
|
|
2748
|
+
if (typeof declared !== "string") {
|
|
2749
|
+
if (!declared.inbound.includes(inbound)) return undefined;
|
|
2750
|
+
const authMode = provider.authMode ?? entry.authKind;
|
|
2751
|
+
if (declared.authModes && !declared.authModes.includes(authMode)) return undefined;
|
|
2752
|
+
}
|
|
2639
2753
|
const wire = typeof declared === "string" ? declared : declared.wire;
|
|
2640
2754
|
return wire !== undefined && allowedWires.has(wire) ? wire : undefined;
|
|
2641
2755
|
}
|
|
@@ -4,6 +4,7 @@ import { isCanonicalOpenAiForwardProvider } from "./openai-tiers";
|
|
|
4
4
|
import {
|
|
5
5
|
getProviderRegistryEntry,
|
|
6
6
|
providerMatchesRegistryTransport,
|
|
7
|
+
registryModelServiceTierCapabilityApplies,
|
|
7
8
|
type InboundWire,
|
|
8
9
|
type ModelWireDefault,
|
|
9
10
|
} from "./registry";
|
|
@@ -44,7 +45,16 @@ function cloneRegistryWireDefaults(
|
|
|
44
45
|
for (const [modelId, declaration] of Object.entries(defaults)) {
|
|
45
46
|
clone[modelId.trim().toLowerCase()] = typeof declaration === "string"
|
|
46
47
|
? declaration
|
|
47
|
-
: Object.freeze({
|
|
48
|
+
: Object.freeze({
|
|
49
|
+
wire: declaration.wire,
|
|
50
|
+
inbound: Object.freeze([...declaration.inbound]),
|
|
51
|
+
...(declaration.authModes
|
|
52
|
+
? { authModes: Object.freeze([...declaration.authModes]) }
|
|
53
|
+
: {}),
|
|
54
|
+
...(declaration.forwardCallerServiceTier !== undefined
|
|
55
|
+
? { forwardCallerServiceTier: declaration.forwardCallerServiceTier }
|
|
56
|
+
: {}),
|
|
57
|
+
});
|
|
48
58
|
}
|
|
49
59
|
return Object.freeze(clone);
|
|
50
60
|
}
|
|
@@ -57,10 +67,17 @@ function buildFastPolicyAuthority(
|
|
|
57
67
|
providerName: string,
|
|
58
68
|
provider: ServiceTierCapabilityProvider,
|
|
59
69
|
registryTransportMatch: boolean,
|
|
70
|
+
capabilityProvider: ServiceTierCapabilityProvider = provider,
|
|
60
71
|
): FastPolicyAuthority {
|
|
61
72
|
const registry = registryTransportMatch ? getProviderRegistryEntry(providerName) : undefined;
|
|
73
|
+
const registryModelCapabilities = registry
|
|
74
|
+
&& registryModelServiceTierCapabilityApplies(registry, capabilityProvider)
|
|
75
|
+
? registry.modelSupportsServiceTier
|
|
76
|
+
: undefined;
|
|
77
|
+
const providerCapability = capabilityProvider.supportsServiceTier ?? registry?.supportsServiceTier;
|
|
62
78
|
const authority: FastPolicyAuthority = Object.freeze({
|
|
63
79
|
providerAdapter: provider.adapter,
|
|
80
|
+
providerAuthMode: provider.authMode ?? registry?.authKind ?? "key",
|
|
64
81
|
fastWireDeclaration: cloneFastWire(
|
|
65
82
|
provider.fastWire !== undefined ? provider.fastWire : registry?.fastWire,
|
|
66
83
|
{ freeze: true },
|
|
@@ -72,8 +89,11 @@ function buildFastPolicyAuthority(
|
|
|
72
89
|
provider.apiKeyTransport,
|
|
73
90
|
),
|
|
74
91
|
capability: Object.freeze({
|
|
75
|
-
...(
|
|
76
|
-
models: Object.freeze({
|
|
92
|
+
...(providerCapability !== undefined ? { provider: providerCapability } : {}),
|
|
93
|
+
models: Object.freeze({
|
|
94
|
+
...(registryModelCapabilities ?? {}),
|
|
95
|
+
...(capabilityProvider.modelSupportsServiceTier ?? {}),
|
|
96
|
+
}),
|
|
77
97
|
...(provider.chatServiceTier !== undefined ? { chatServiceTier: provider.chatServiceTier } : {}),
|
|
78
98
|
}),
|
|
79
99
|
modelAdapters: Object.freeze({ ...(provider.modelAdapters ?? {}) }),
|
|
@@ -87,8 +107,14 @@ export function captureFastPolicyAuthority(
|
|
|
87
107
|
providerName: string,
|
|
88
108
|
provider: ServiceTierCapabilityProvider,
|
|
89
109
|
registryTransportMatch: boolean,
|
|
110
|
+
capabilityProvider: ServiceTierCapabilityProvider = provider,
|
|
90
111
|
): FastPolicyAuthority {
|
|
91
|
-
const authority = buildFastPolicyAuthority(
|
|
112
|
+
const authority = buildFastPolicyAuthority(
|
|
113
|
+
providerName,
|
|
114
|
+
provider,
|
|
115
|
+
registryTransportMatch,
|
|
116
|
+
capabilityProvider,
|
|
117
|
+
);
|
|
92
118
|
if (Object.isFrozen(provider)) capturedFastPolicyAuthorities.set(provider, authority);
|
|
93
119
|
return authority;
|
|
94
120
|
}
|
|
@@ -106,12 +132,13 @@ export function captureServiceTierAdapterAuthority(
|
|
|
106
132
|
function authorityForProvider(
|
|
107
133
|
provider: ServiceTierCapabilityProvider,
|
|
108
134
|
providerName?: string,
|
|
135
|
+
capabilityProvider?: ServiceTierCapabilityProvider,
|
|
109
136
|
): FastPolicyAuthority {
|
|
110
137
|
// Preserve the legacy no-name short circuit: serviceTierSupportForModel() used the
|
|
111
138
|
// provider adapter directly when no provider identity was available, so no configured
|
|
112
139
|
// override, hard pin, or registry default may participate on this path in A1.
|
|
113
140
|
if (providerName === undefined) {
|
|
114
|
-
const authority = buildFastPolicyAuthority("", provider, false);
|
|
141
|
+
const authority = buildFastPolicyAuthority("", provider, false, capabilityProvider ?? provider);
|
|
115
142
|
return Object.freeze({
|
|
116
143
|
...authority,
|
|
117
144
|
modelAdapters: Object.freeze({}),
|
|
@@ -119,12 +146,17 @@ function authorityForProvider(
|
|
|
119
146
|
registryWireDefaults: Object.freeze({}),
|
|
120
147
|
});
|
|
121
148
|
}
|
|
122
|
-
const captured = Object.isFrozen(provider)
|
|
149
|
+
const captured = capabilityProvider === undefined && Object.isFrozen(provider)
|
|
123
150
|
? capturedFastPolicyAuthorities.get(provider)
|
|
124
151
|
: undefined;
|
|
125
152
|
if (captured) return captured;
|
|
126
153
|
const registryTransportMatch = providerMatchesRegistryTransport(providerName, provider);
|
|
127
|
-
const authority = buildFastPolicyAuthority(
|
|
154
|
+
const authority = buildFastPolicyAuthority(
|
|
155
|
+
providerName,
|
|
156
|
+
provider,
|
|
157
|
+
registryTransportMatch,
|
|
158
|
+
capabilityProvider ?? provider,
|
|
159
|
+
);
|
|
128
160
|
// Frozen provider snapshots cannot drift, so repeated catalog/runtime projections may safely
|
|
129
161
|
// reuse the registry lookup and detached declaration maps. Mutable configs still rebuild.
|
|
130
162
|
if (Object.isFrozen(provider)) capturedFastPolicyAuthorities.set(provider, authority);
|
|
@@ -137,8 +169,13 @@ export function fastPolicyForModel(
|
|
|
137
169
|
modelId: string,
|
|
138
170
|
providerName?: string,
|
|
139
171
|
inbound: InboundWire = "responses",
|
|
172
|
+
capabilityProvider?: ServiceTierCapabilityProvider,
|
|
140
173
|
): ResolvedFastPolicy {
|
|
141
|
-
return resolveFastPolicy(
|
|
174
|
+
return resolveFastPolicy(
|
|
175
|
+
authorityForProvider(provider, providerName, capabilityProvider),
|
|
176
|
+
modelId,
|
|
177
|
+
inbound,
|
|
178
|
+
);
|
|
142
179
|
}
|
|
143
180
|
|
|
144
181
|
/**
|
|
@@ -214,13 +251,11 @@ export function serviceTierSupportFromPolicy(
|
|
|
214
251
|
): boolean | undefined {
|
|
215
252
|
if (policy.eligibility === "eligible") return true;
|
|
216
253
|
if (policy.eligibility === "unclassified") {
|
|
217
|
-
//
|
|
218
|
-
//
|
|
219
|
-
//
|
|
220
|
-
//
|
|
221
|
-
|
|
222
|
-
// unknown, as does every unclassified Responses-wire route.
|
|
223
|
-
if (policy.adapter === "openai-chat" && !policy.forwardCallerTier) return false;
|
|
254
|
+
// An unclassified route that cannot forward a caller tier has definitive negative
|
|
255
|
+
// evidence even when its adapter can normally serialize service_tier. This covers both
|
|
256
|
+
// Chat routes without chatServiceTier and a registry default that explicitly closes a
|
|
257
|
+
// subscription gateway. Generic unclassified Responses routes still project unknown.
|
|
258
|
+
if (!policy.forwardCallerTier) return false;
|
|
224
259
|
return undefined;
|
|
225
260
|
}
|
|
226
261
|
return false;
|
package/src/responses/parser.ts
CHANGED
|
@@ -11,7 +11,7 @@ import type {
|
|
|
11
11
|
OcxToolCall,
|
|
12
12
|
OcxReasoningReplayScopeRef,
|
|
13
13
|
} from "../types";
|
|
14
|
-
import { namespacedToolName } from "../types";
|
|
14
|
+
import { namespacedToolName, toolChoiceCandidates } from "../types";
|
|
15
15
|
import { responsesRequestSchema } from "./schema";
|
|
16
16
|
import { providerMetadataFromResponsesFunctionCall } from "./provider-opaque-metadata";
|
|
17
17
|
import { lookupReplayThoughtSignature } from "./thought-signature-replay";
|
|
@@ -20,6 +20,7 @@ import { previousResponseReplayPrefixLength } from "./state";
|
|
|
20
20
|
import { decodeReasoningEnvelope } from "./reasoning-envelope";
|
|
21
21
|
import { extractHostedWebSearch, WEB_SEARCH_TOOL_NAME } from "../web-search/synthetic-tool";
|
|
22
22
|
import { extractHostedImageGeneration, IMAGE_GEN_TOOL_NAME } from "../images/synthetic-tool";
|
|
23
|
+
import { toolSearchDescription, toolSearchParameters } from "./tool-search-compat";
|
|
23
24
|
|
|
24
25
|
function isObj(v: unknown): v is Record<string, unknown> {
|
|
25
26
|
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
@@ -203,7 +204,7 @@ function buildTools(tools: unknown[] | undefined): OcxTool[] | undefined {
|
|
|
203
204
|
const ns = typeof t.name === "string" && !builtinFunctions ? t.name : undefined;
|
|
204
205
|
for (const inner of t.tools as unknown[]) {
|
|
205
206
|
if (isObj(inner) && inner.type === "function" && typeof inner.name === "string") pushFn(inner, ns);
|
|
206
|
-
else if (
|
|
207
|
+
else if (isObj(inner) && inner.type === "custom" && typeof inner.name === "string") pushCustom(inner, ns);
|
|
207
208
|
}
|
|
208
209
|
}
|
|
209
210
|
else if (t.type === "custom" && typeof t.name === "string") {
|
|
@@ -214,15 +215,8 @@ function buildTools(tools: unknown[] | undefined): OcxTool[] | undefined {
|
|
|
214
215
|
// Expose as a function so chat models can call it; the bridge relays it as a tool_search_call.
|
|
215
216
|
out.push({
|
|
216
217
|
name: "tool_search",
|
|
217
|
-
description: (t
|
|
218
|
-
parameters: (
|
|
219
|
-
type: "object",
|
|
220
|
-
properties: {
|
|
221
|
-
query: { type: "string", description: "Search query for tools to load." },
|
|
222
|
-
limit: { type: "number", description: "Maximum number of tools to return." },
|
|
223
|
-
},
|
|
224
|
-
required: ["query"],
|
|
225
|
-
}) as Record<string, unknown>,
|
|
218
|
+
description: toolSearchDescription(t),
|
|
219
|
+
parameters: normalizeParameters(toolSearchParameters(t)),
|
|
226
220
|
toolSearch: true,
|
|
227
221
|
});
|
|
228
222
|
}
|
|
@@ -328,6 +322,35 @@ function attachPendingReasoningToCallOwner(
|
|
|
328
322
|
|
|
329
323
|
const REASONING_EFFORTS = new Set(["none", "minimal", "low", "medium", "high", "xhigh", "max"]);
|
|
330
324
|
|
|
325
|
+
/**
|
|
326
|
+
* Namespace a custom tool was declared under, by its bare name.
|
|
327
|
+
*
|
|
328
|
+
* A `custom_tool_call` echoed back by the client carries only the bare name — the bridge
|
|
329
|
+
* emits `{"type":"custom_tool_call","name":"exec"}` even when the tool was declared as
|
|
330
|
+
* `mcp__functions__exec`. Without this lookup the namespace is lost on the return trip,
|
|
331
|
+
* and the adapters replay history through `namespacedToolName(namespace, name)`, which
|
|
332
|
+
* then produces a bare `exec` the provider may not have. Ordinary `function_call` items
|
|
333
|
+
* do not need this: they carry `namespace` on the wire.
|
|
334
|
+
*/
|
|
335
|
+
function customToolNamespaces(tools: unknown): Map<string, string> {
|
|
336
|
+
const out = new Map<string, string>();
|
|
337
|
+
if (!Array.isArray(tools)) return out;
|
|
338
|
+
for (const spec of tools) {
|
|
339
|
+
if (!isObj(spec) || spec.type !== "namespace" || !Array.isArray(spec.tools)) continue;
|
|
340
|
+
const namespace = typeof spec.name === "string" ? spec.name : undefined;
|
|
341
|
+
// Codex 0.147 groups ordinary client tools under the reserved `functions` namespace and
|
|
342
|
+
// buildTools deliberately flattens those without a namespace. Mirror that here, or the
|
|
343
|
+
// reconstruction would invent a namespace the request never advertised.
|
|
344
|
+
if (!namespace || namespace === "functions") continue;
|
|
345
|
+
for (const inner of spec.tools) {
|
|
346
|
+
if (!isObj(inner) || inner.type !== "custom" || typeof inner.name !== "string") continue;
|
|
347
|
+
// Ambiguous bare names are already rejected upstream, so first declaration wins.
|
|
348
|
+
if (!out.has(inner.name)) out.set(inner.name, namespace);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
return out;
|
|
352
|
+
}
|
|
353
|
+
|
|
331
354
|
export function parseRequest(
|
|
332
355
|
body: unknown,
|
|
333
356
|
parseOptions?: { replayCacheScope?: OcxReasoningReplayScopeRef },
|
|
@@ -341,6 +364,9 @@ export function parseRequest(
|
|
|
341
364
|
const data = parsed.data;
|
|
342
365
|
const now = Date.now();
|
|
343
366
|
const messages: OcxMessage[] = [];
|
|
367
|
+
// Built before the item loop: a custom_tool_call echoed back in `input` needs the
|
|
368
|
+
// namespace from the request's own tool catalog to survive the round trip.
|
|
369
|
+
const customToolNamespacesByName = customToolNamespaces(data.tools);
|
|
344
370
|
const systemPrompt: string[] = [];
|
|
345
371
|
// Responses reasoning siblings belong to the following assistant, including across call items.
|
|
346
372
|
// Keep them off the message list until that assistant arrives; turn boundaries clear the array.
|
|
@@ -574,10 +600,15 @@ export function parseRequest(
|
|
|
574
600
|
if (effectiveType === "custom_tool_call") {
|
|
575
601
|
const call = item as { id?: string; call_id: string; name: string; input: string };
|
|
576
602
|
const remembered = typeof call.call_id === "string" ? replayThoughtSignatureMetadata(call.call_id, replayCacheScope) : undefined;
|
|
603
|
+
// Reconstruct the namespace the request declared this tool under. The wire item
|
|
604
|
+
// carries only the bare name, so without this the round trip loses it and adapters
|
|
605
|
+
// replay the call as an unnamespaced tool the provider may not expose.
|
|
606
|
+
const customNamespace = customToolNamespacesByName.get(call.name);
|
|
577
607
|
const toolCall: OcxToolCall = {
|
|
578
608
|
type: "toolCall", id: call.call_id, name: call.name,
|
|
579
609
|
arguments: { input: call.input ?? "" },
|
|
580
610
|
customWireName: call.name,
|
|
611
|
+
...(customNamespace ? { namespace: customNamespace } : {}),
|
|
581
612
|
...(remembered ? { providerMetadata: remembered } : {}),
|
|
582
613
|
};
|
|
583
614
|
assistantHolderWithReasoning().content.push(toolCall);
|
|
@@ -691,6 +722,15 @@ export function parseRequest(
|
|
|
691
722
|
const declaredTools = buildTools(data.tools as unknown[] | undefined) ?? [];
|
|
692
723
|
const loadedTools = buildTools(loadedToolSpecs) ?? [];
|
|
693
724
|
const loadedToolNames = new Set(loadedTools.map(t => namespacedToolName(t.namespace, t.name)));
|
|
725
|
+
const wireOwners = new Map<string, OcxTool>();
|
|
726
|
+
for (const tool of [...declaredTools, ...loadedTools]) {
|
|
727
|
+
const wireName = namespacedToolName(tool.namespace, tool.name);
|
|
728
|
+
const previous = wireOwners.get(wireName);
|
|
729
|
+
if (previous && (previous.namespace !== tool.namespace || previous.name !== tool.name || previous.freeform !== tool.freeform || previous.toolSearch !== tool.toolSearch)) {
|
|
730
|
+
throw new Error(`ambiguous tool catalog: multiple logical tools map to wire name ${wireName}`);
|
|
731
|
+
}
|
|
732
|
+
wireOwners.set(wireName, tool);
|
|
733
|
+
}
|
|
694
734
|
const seenTools = new Set<string>();
|
|
695
735
|
const mergedTools = [...declaredTools, ...loadedTools]
|
|
696
736
|
.filter(t => {
|
|
@@ -716,6 +756,14 @@ export function parseRequest(
|
|
|
716
756
|
options.stopSequences = typeof data.stop === "string" ? [data.stop] : data.stop;
|
|
717
757
|
}
|
|
718
758
|
const tc = mapToolChoice(data.tool_choice);
|
|
759
|
+
if (tc && typeof tc === "object") {
|
|
760
|
+
const selectors = "allowedTools" in tc ? tc.allowedTools : [tc.name];
|
|
761
|
+
for (const selector of selectors) {
|
|
762
|
+
if (toolChoiceCandidates(mergedTools, selector).length > 1) {
|
|
763
|
+
throw new Error(`ambiguous tool_choice name: ${selector}`);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
}
|
|
719
767
|
if (tc !== undefined) options.toolChoice = tc;
|
|
720
768
|
if (data.parallel_tool_calls !== undefined) options.parallelToolCalls = data.parallel_tool_calls;
|
|
721
769
|
// Upstream codex-rs converts "ultra" to "max" at the inference boundary (core/src/client.rs
|