@bitkyc08/opencodex 2.18.2 → 2.20.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-DF_UFrGS.css +1 -0
- package/gui/dist/assets/index-DSK3S5HY.js +76 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/client-fingerprint.ts +14 -10
- package/src/adapters/google-antigravity-wire.ts +4 -3
- package/src/adapters/google.ts +1 -1
- package/src/adapters/mimo-free.ts +17 -0
- package/src/adapters/openai-chat.ts +367 -32
- package/src/adapters/registry.ts +144 -0
- package/src/chat/inbound.ts +37 -8
- package/src/cli/claude.ts +2 -1
- 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/codex/shim.ts +100 -5
- 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 +239 -87
- package/src/images/loop.ts +11 -1
- 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/errors.ts +27 -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 +9 -3
- 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/spill-store.ts +20 -1
- package/src/responses/state.ts +159 -3
- 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 +8 -0
- package/src/server/chat-completions.ts +74 -34
- package/src/server/chat-native-sse.ts +331 -0
- package/src/server/chat-native.ts +371 -0
- package/src/server/effort-policy.ts +18 -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 +278 -48
- package/src/server/responses/empty-completion-guard.ts +276 -0
- package/src/server/responses/fetch-helpers.ts +35 -4
- package/src/server/responses/input-admission.ts +169 -0
- 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/service-manager-probe.ts +2 -3
- package/src/types.ts +76 -6
- 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/vision/index.ts +21 -4
- package/src/web-search/index.ts +2 -1
- package/gui/dist/assets/index-CXI1262_.js +0 -76
- package/gui/dist/assets/index-DUCH59lJ.css +0 -1
package/gui/dist/index.html
CHANGED
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
} catch (e) {}
|
|
17
17
|
})();
|
|
18
18
|
</script>
|
|
19
|
-
<script type="module" crossorigin src="/assets/index-
|
|
20
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
19
|
+
<script type="module" crossorigin src="/assets/index-DSK3S5HY.js"></script>
|
|
20
|
+
<link rel="stylesheet" crossorigin href="/assets/index-DF_UFrGS.css">
|
|
21
21
|
</head>
|
|
22
22
|
<body>
|
|
23
23
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -40,20 +40,24 @@ export function claudeCodeSessionId(token: string | undefined): string {
|
|
|
40
40
|
return `${h.slice(0, 8)}-${h.slice(8, 12)}-4${h.slice(13, 16)}-${variant}${h.slice(17, 20)}-${h.slice(20, 32)}`;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
// ── Antigravity
|
|
44
|
-
/** Pinned fallback Antigravity
|
|
45
|
-
export const
|
|
46
|
-
const
|
|
47
|
-
const
|
|
43
|
+
// ── Antigravity IDE ──
|
|
44
|
+
/** Pinned fallback Antigravity IDE language-server version (matches the bundled LS 2.5.5). */
|
|
45
|
+
export const ANTIGRAVITY_IDE_VERSION = "2.5.5";
|
|
46
|
+
const ANTIGRAVITY_IDE_CLIENT_NAME = "aidev_client";
|
|
47
|
+
const ANTIGRAVITY_IDE_PLATFORM = "windows/amd64";
|
|
48
48
|
/** Secondary Google API client UA the Antigravity client library reports. */
|
|
49
49
|
export const ANTIGRAVITY_GOOG_API_CLIENT_UA = "google-api-nodejs-client/10.3.0";
|
|
50
50
|
|
|
51
51
|
/**
|
|
52
|
-
* The real Antigravity
|
|
53
|
-
* `antigravity/
|
|
52
|
+
* The real Antigravity IDE User-Agent, e.g.
|
|
53
|
+
* `antigravity/ide/2.5.5 (aidev_client; os_type=windows; arch=amd64)`.
|
|
54
|
+
*
|
|
55
|
+
* Must be the IDE client family, NOT `antigravity/cli/...`: the Cloud Code Assist backend gates
|
|
56
|
+
* newer agent models (e.g. `gemini-3.7-flash`) by User-Agent and answers 404 NOT_FOUND to
|
|
57
|
+
* CLI-shaped UAs even with a valid OAuth token. Only `antigravity/ide/<ver>` unlocks them.
|
|
54
58
|
* A `GOOGLE_ANTIGRAVITY_USER_AGENT` override (set by the caller) takes precedence upstream.
|
|
55
59
|
*/
|
|
56
|
-
export function antigravityUserAgent(version =
|
|
57
|
-
const [osType, arch] =
|
|
58
|
-
return `antigravity/
|
|
60
|
+
export function antigravityUserAgent(version = ANTIGRAVITY_IDE_VERSION): string {
|
|
61
|
+
const [osType, arch] = ANTIGRAVITY_IDE_PLATFORM.split("/");
|
|
62
|
+
return `antigravity/ide/${version} (${ANTIGRAVITY_IDE_CLIENT_NAME}; os_type=${osType}; arch=${arch})`;
|
|
59
63
|
}
|
|
@@ -3,10 +3,11 @@ import type { OcxContentPart, OcxParsedRequest } from "../types";
|
|
|
3
3
|
import { antigravityUserAgent } from "./client-fingerprint";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Antigravity request User-Agent. Mirrors the real Antigravity
|
|
7
|
-
* (`antigravity/
|
|
6
|
+
* Antigravity request User-Agent. Mirrors the real Antigravity IDE UA
|
|
7
|
+
* (`antigravity/ide/{ver} (aidev_client; os_type=windows; arch=amd64)`) so the request fingerprint
|
|
8
8
|
* matches the OAuth credential — the prior literal `"antigravity"` was a giveaway no real client
|
|
9
|
-
* sends.
|
|
9
|
+
* sends. The IDE client family is also required to unlock newer agent models (the backend 404s
|
|
10
|
+
* CLI-shaped UAs for `gemini-3.7-*`). A `GOOGLE_ANTIGRAVITY_USER_AGENT` override still wins.
|
|
10
11
|
*/
|
|
11
12
|
export const ANTIGRAVITY_REQUEST_UA = process.env.GOOGLE_ANTIGRAVITY_USER_AGENT || antigravityUserAgent();
|
|
12
13
|
|
package/src/adapters/google.ts
CHANGED
|
@@ -469,7 +469,7 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
469
469
|
const envelope = {
|
|
470
470
|
model: wireModelId,
|
|
471
471
|
// The envelope's `userAgent` field is a protocol constant ("antigravity"), distinct from
|
|
472
|
-
// the HTTP `User-Agent` header (the real
|
|
472
|
+
// the HTTP `User-Agent` header (the real IDE UA). CLIProxyAPI `geminiToAntigravity` hardcodes
|
|
473
473
|
// the body field; only the header carries the versioned client string.
|
|
474
474
|
userAgent: "antigravity",
|
|
475
475
|
requestType: "agent",
|
|
@@ -40,6 +40,18 @@ function randomUserAgent(): string {
|
|
|
40
40
|
return USER_AGENTS[Math.floor(Math.random() * USER_AGENTS.length)]!;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
function isCanonicalMimoFreeEndpoint(baseUrl: string): boolean {
|
|
44
|
+
try {
|
|
45
|
+
const actual = new URL(baseUrl.trim());
|
|
46
|
+
const expected = new URL(MIMO_CHAT_URL);
|
|
47
|
+
actual.pathname = actual.pathname.replace(/\/+$/, "") || "/";
|
|
48
|
+
expected.pathname = expected.pathname.replace(/\/+$/, "") || "/";
|
|
49
|
+
return actual.toString().replace(/\/$/, "") === expected.toString().replace(/\/$/, "");
|
|
50
|
+
} catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
43
55
|
/**
|
|
44
56
|
* Anonymous per-install client id for the bootstrap `client` field. A random UUID
|
|
45
57
|
* persisted under the config dir (OPENCODEX_HOME-aware) — deliberately NOT derived
|
|
@@ -194,6 +206,11 @@ export function injectMimoSystemMarker(body: unknown): unknown {
|
|
|
194
206
|
* On 401/403, flushes the JWT cache and retries once via fetchResponse.
|
|
195
207
|
*/
|
|
196
208
|
export function createMimoFreeAdapter(provider: OcxProviderConfig): ProviderAdapter {
|
|
209
|
+
if (!isCanonicalMimoFreeEndpoint(provider.baseUrl)) {
|
|
210
|
+
throw new Error(
|
|
211
|
+
"The mimo-free adapter only supports the canonical Xiaomi MiMo Free endpoint. Use openai-chat for a custom endpoint.",
|
|
212
|
+
);
|
|
213
|
+
}
|
|
197
214
|
const base = createOpenAIChatAdapter(provider);
|
|
198
215
|
// Per-adapter session-affinity id (random, per process instance).
|
|
199
216
|
const sessionId = `ses_${Math.random().toString(36).slice(2, 26)}`;
|
|
@@ -12,6 +12,7 @@ import { identifyRoutedModel } from "./identity";
|
|
|
12
12
|
import { peekReasoningForCall } from "../responses/reasoning-replay-cache";
|
|
13
13
|
import { buildNonOpenAIToolCatalogNudgeForTools, shouldInjectNonOpenAIToolCatalogNudge } from "./tool-catalog-nudge";
|
|
14
14
|
import { openRouterProviderPayload, resolveOpenRouterRouting } from "../providers/openrouter-routing";
|
|
15
|
+
import { canSerializeServiceTierForChatModel } from "../providers/service-tier";
|
|
15
16
|
import { openaiChatCompletionsUrl } from "./openai-chat-url";
|
|
16
17
|
import {
|
|
17
18
|
isTranslatorBudgetExceededError,
|
|
@@ -34,6 +35,122 @@ export function stripBracketedModelSuffix(modelId: string): string {
|
|
|
34
35
|
return suffixStart === -1 ? modelId : modelId.slice(0, suffixStart);
|
|
35
36
|
}
|
|
36
37
|
|
|
38
|
+
const CHAT_PASSTHROUGH_FIELDS = [
|
|
39
|
+
"audio",
|
|
40
|
+
"frequency_penalty",
|
|
41
|
+
"logit_bias",
|
|
42
|
+
"logprobs",
|
|
43
|
+
"max_completion_tokens",
|
|
44
|
+
"max_tokens",
|
|
45
|
+
"metadata",
|
|
46
|
+
"modalities",
|
|
47
|
+
"n",
|
|
48
|
+
"prediction",
|
|
49
|
+
"presence_penalty",
|
|
50
|
+
"reasoning_effort",
|
|
51
|
+
"response_format",
|
|
52
|
+
"seed",
|
|
53
|
+
"stop",
|
|
54
|
+
"store",
|
|
55
|
+
"temperature",
|
|
56
|
+
"tool_choice",
|
|
57
|
+
"tools",
|
|
58
|
+
"top_logprobs",
|
|
59
|
+
"top_p",
|
|
60
|
+
"user",
|
|
61
|
+
"web_search_options",
|
|
62
|
+
] as const;
|
|
63
|
+
|
|
64
|
+
function openAIChatTransport(provider: OcxProviderConfig): {
|
|
65
|
+
url: string;
|
|
66
|
+
headers: Record<string, string>;
|
|
67
|
+
hasCredential: boolean;
|
|
68
|
+
} {
|
|
69
|
+
const hasCredential = typeof provider.apiKey === "string" && provider.apiKey.trim().length > 0;
|
|
70
|
+
if ((provider.authMode === "key" || provider.authMode === "oauth") && !provider.keyOptional && !hasCredential) {
|
|
71
|
+
throw new Error(`${provider.adapter} requires a non-empty credential (authMode: ${provider.authMode})`);
|
|
72
|
+
}
|
|
73
|
+
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
|
74
|
+
if (hasCredential) headers.Authorization = `Bearer ${provider.apiKey}`;
|
|
75
|
+
if (provider.headers) Object.assign(headers, provider.headers);
|
|
76
|
+
return { url: openaiChatCompletionsUrl(provider.baseUrl), headers, hasCredential };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Build a provider request from an inbound Chat Completions body without translating it
|
|
81
|
+
* through the Responses contract. This is deliberately a whitelist: Chat-only caller
|
|
82
|
+
* fields retain their exact wire representation, while provider capability gates remain
|
|
83
|
+
* centralized beside the ordinary openai-chat adapter.
|
|
84
|
+
*/
|
|
85
|
+
export function buildOpenAIChatPassthroughRequest(
|
|
86
|
+
provider: OcxProviderConfig,
|
|
87
|
+
rawBody: Record<string, unknown>,
|
|
88
|
+
modelId: string,
|
|
89
|
+
stream: boolean,
|
|
90
|
+
): AdapterRequest {
|
|
91
|
+
const { url, headers, hasCredential } = openAIChatTransport(provider);
|
|
92
|
+
|
|
93
|
+
const body: Record<string, unknown> = {
|
|
94
|
+
model: provider.modelSuffixBracketStrip ? stripBracketedModelSuffix(modelId) : modelId,
|
|
95
|
+
messages: rawBody.messages,
|
|
96
|
+
stream,
|
|
97
|
+
};
|
|
98
|
+
for (const field of CHAT_PASSTHROUGH_FIELDS) {
|
|
99
|
+
if (rawBody[field] !== undefined) body[field] = rawBody[field];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (modelInList(provider.noTemperatureModels, modelId)) delete body.temperature;
|
|
103
|
+
if (modelInList(provider.noTopPModels, modelId)) delete body.top_p;
|
|
104
|
+
if (modelInList(provider.noPenaltyModels, modelId)) {
|
|
105
|
+
delete body.presence_penalty;
|
|
106
|
+
delete body.frequency_penalty;
|
|
107
|
+
}
|
|
108
|
+
if (modelInList(provider.noStructuredOutputModels, modelId)) delete body.response_format;
|
|
109
|
+
|
|
110
|
+
if (provider.chatServiceTier && rawBody.service_tier !== undefined) {
|
|
111
|
+
body.service_tier = rawBody.service_tier;
|
|
112
|
+
}
|
|
113
|
+
if (provider.promptCacheKey && rawBody.prompt_cache_key !== undefined) {
|
|
114
|
+
body.prompt_cache_key = rawBody.prompt_cache_key;
|
|
115
|
+
}
|
|
116
|
+
if (Array.isArray(rawBody.tools) && rawBody.tools.length > 0) {
|
|
117
|
+
if (provider.parallelToolCalls === true) {
|
|
118
|
+
body.parallel_tool_calls = rawBody.parallel_tool_calls !== false;
|
|
119
|
+
} else if (provider.parallelToolCalls === false
|
|
120
|
+
&& (provider.baseUrl === "https://integrate.api.nvidia.com/v1" || provider.pinParallelToolCallsFalse === true)) {
|
|
121
|
+
body.parallel_tool_calls = false;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (stream) {
|
|
125
|
+
const callerOptions = rawBody.stream_options !== null
|
|
126
|
+
&& typeof rawBody.stream_options === "object"
|
|
127
|
+
&& !Array.isArray(rawBody.stream_options)
|
|
128
|
+
? rawBody.stream_options as Record<string, unknown>
|
|
129
|
+
: {};
|
|
130
|
+
body.stream_options = { ...callerOptions, include_usage: true };
|
|
131
|
+
} else if (rawBody.stream_options !== undefined) {
|
|
132
|
+
body.stream_options = rawBody.stream_options;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const bodyJson = JSON.stringify(body);
|
|
136
|
+
|
|
137
|
+
if (isDebugEnabled()) {
|
|
138
|
+
let host = "upstream";
|
|
139
|
+
try { host = new URL(url).host; } catch { /* keep fallback */ }
|
|
140
|
+
debugProviderDiagnostic("openai-chat", "passthrough-request", {
|
|
141
|
+
host,
|
|
142
|
+
model: body.model,
|
|
143
|
+
stream,
|
|
144
|
+
messageCount: Array.isArray(body.messages) ? body.messages.length : 0,
|
|
145
|
+
toolCount: Array.isArray(body.tools) ? body.tools.length : 0,
|
|
146
|
+
hasCredential,
|
|
147
|
+
bodyBytes: new TextEncoder().encode(bodyJson).length,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return { url, method: "POST", headers, body: bodyJson };
|
|
152
|
+
}
|
|
153
|
+
|
|
37
154
|
// 260715 (issue #126): surface upstream error detail through the web-search sidecar loop.
|
|
38
155
|
// loop.ts only appends a suffix to "Provider error N" when the adapter exposes
|
|
39
156
|
// formatErrorBody; without it, strict OpenAI-compatible backends (NVIDIA NIM pydantic
|
|
@@ -658,11 +775,11 @@ function shouldSanitizeZenToolParameters(provider: OcxProviderConfig): boolean {
|
|
|
658
775
|
|| baseUrl === "https://opencode.ai/zen/go/v1";
|
|
659
776
|
}
|
|
660
777
|
|
|
661
|
-
const XAI_SCHEMA_BASE_URLS = new Set(["api.x.ai", "cli-chat-proxy.grok.com"]);
|
|
662
|
-
|
|
663
778
|
function isXaiSchemaTarget(provider: OcxProviderConfig): boolean {
|
|
664
779
|
try {
|
|
665
|
-
|
|
780
|
+
// Public api.x.ai accepts native root object unions. Only the Grok CLI proxy
|
|
781
|
+
// 400s on a root oneOf/anyOf, so flattening/omitting is scoped to that host.
|
|
782
|
+
return new URL(provider.baseUrl).hostname === "cli-chat-proxy.grok.com";
|
|
666
783
|
} catch {
|
|
667
784
|
return false;
|
|
668
785
|
}
|
|
@@ -696,36 +813,262 @@ function ensureRootObjectType(parameters: unknown): Record<string, unknown> {
|
|
|
696
813
|
return { ...obj, type: "object" };
|
|
697
814
|
}
|
|
698
815
|
|
|
816
|
+
function isXaiObjectSchema(value: unknown): value is Record<string, unknown> {
|
|
817
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
function stringRequiredFields(value: unknown): string[] {
|
|
821
|
+
return Array.isArray(value)
|
|
822
|
+
? value.filter((item): item is string => typeof item === "string")
|
|
823
|
+
: [];
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
/** Variant keys the merger can keep. Anything else is refused, not silently dropped. */
|
|
827
|
+
const XAI_VARIANT_MERGE_KEYS = new Set([
|
|
828
|
+
"type",
|
|
829
|
+
"properties",
|
|
830
|
+
"required",
|
|
831
|
+
"additionalProperties",
|
|
832
|
+
"description",
|
|
833
|
+
"title",
|
|
834
|
+
"$comment",
|
|
835
|
+
"$defs",
|
|
836
|
+
"definitions",
|
|
837
|
+
]);
|
|
838
|
+
|
|
839
|
+
function decodeJsonPointerToken(token: string): string {
|
|
840
|
+
return token.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
function lookupLocalJsonPointer(root: unknown, ref: string): unknown {
|
|
844
|
+
if (ref === "#" || ref === "#/") return root;
|
|
845
|
+
if (!ref.startsWith("#/")) return undefined;
|
|
846
|
+
let current: unknown = root;
|
|
847
|
+
for (const token of ref.slice(2).split("/").map(decodeJsonPointerToken)) {
|
|
848
|
+
if (!isXaiObjectSchema(current) || !Object.hasOwn(current, token)) return undefined;
|
|
849
|
+
current = current[token];
|
|
850
|
+
}
|
|
851
|
+
return current;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
/** Resolve local `#/` `$ref`s. Unresolvable or cyclic refs return undefined. */
|
|
855
|
+
function resolveXaiSchemaRefs(
|
|
856
|
+
schema: unknown,
|
|
857
|
+
root: Record<string, unknown>,
|
|
858
|
+
stack: Set<string> = new Set(),
|
|
859
|
+
): unknown | undefined {
|
|
860
|
+
if (!isXaiObjectSchema(schema)) return schema;
|
|
861
|
+
if (typeof schema.$ref === "string") {
|
|
862
|
+
const ref = schema.$ref;
|
|
863
|
+
if (stack.has(ref)) return undefined;
|
|
864
|
+
const target = lookupLocalJsonPointer(root, ref);
|
|
865
|
+
if (target === undefined) return undefined;
|
|
866
|
+
stack.add(ref);
|
|
867
|
+
const resolvedTarget = resolveXaiSchemaRefs(target, root, stack);
|
|
868
|
+
stack.delete(ref);
|
|
869
|
+
if (resolvedTarget === undefined) return undefined;
|
|
870
|
+
const rest: Record<string, unknown> = { ...schema };
|
|
871
|
+
delete rest.$ref;
|
|
872
|
+
if (Object.keys(rest).length === 0) return resolvedTarget;
|
|
873
|
+
const resolvedRest = resolveXaiSchemaRefs(rest, root, stack);
|
|
874
|
+
if (resolvedRest === undefined || !isXaiObjectSchema(resolvedTarget) || !isXaiObjectSchema(resolvedRest)) {
|
|
875
|
+
return undefined;
|
|
876
|
+
}
|
|
877
|
+
return composeXaiObjectSchemas(resolvedTarget, resolvedRest);
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
const resolved: Record<string, unknown> = {};
|
|
881
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
882
|
+
if ((key === "oneOf" || key === "anyOf") && Array.isArray(value)) {
|
|
883
|
+
const items: unknown[] = [];
|
|
884
|
+
for (const item of value) {
|
|
885
|
+
const next = resolveXaiSchemaRefs(item, root, stack);
|
|
886
|
+
if (next === undefined) return undefined;
|
|
887
|
+
items.push(next);
|
|
888
|
+
}
|
|
889
|
+
resolved[key] = items;
|
|
890
|
+
continue;
|
|
891
|
+
}
|
|
892
|
+
if (key === "properties" && isXaiObjectSchema(value)) {
|
|
893
|
+
const properties: Record<string, unknown> = {};
|
|
894
|
+
for (const [name, property] of Object.entries(value)) {
|
|
895
|
+
const next = resolveXaiSchemaRefs(property, root, stack);
|
|
896
|
+
if (next === undefined) return undefined;
|
|
897
|
+
properties[name] = next;
|
|
898
|
+
}
|
|
899
|
+
resolved[key] = properties;
|
|
900
|
+
continue;
|
|
901
|
+
}
|
|
902
|
+
resolved[key] = value;
|
|
903
|
+
}
|
|
904
|
+
return resolved;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
function xaiVariantIsConcreteObject(variant: Record<string, unknown>): boolean {
|
|
908
|
+
if (variant.type !== undefined && variant.type !== "object") return false;
|
|
909
|
+
return Object.keys(variant).every(key => XAI_VARIANT_MERGE_KEYS.has(key));
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
function variantProperties(variant: Record<string, unknown>): Record<string, unknown> {
|
|
913
|
+
return isXaiObjectSchema(variant.properties) ? variant.properties : {};
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
/**
|
|
917
|
+
* Independent per-property anyOf is lossless only when every property name exists
|
|
918
|
+
* on every variant (absence is meaningful under xAI's default additionalProperties:
|
|
919
|
+
* false, and promoting a branch-local key also tightens explicit-true variants)
|
|
920
|
+
* and at most one of those shared properties has a conflicting schema.
|
|
921
|
+
*/
|
|
922
|
+
function xaiPropertyMergeIsLossless(variants: Record<string, unknown>[]): boolean {
|
|
923
|
+
const names = new Set<string>();
|
|
924
|
+
const props = variants.map(variant => {
|
|
925
|
+
const properties = variantProperties(variant);
|
|
926
|
+
for (const name of Object.keys(properties)) names.add(name);
|
|
927
|
+
return properties;
|
|
928
|
+
});
|
|
929
|
+
let schemaConflicts = 0;
|
|
930
|
+
for (const name of names) {
|
|
931
|
+
const values = props.map(property => property[name]);
|
|
932
|
+
if (values.some(value => value === undefined)) return false;
|
|
933
|
+
if (values.some(value => JSON.stringify(value) !== JSON.stringify(values[0]))) schemaConflicts += 1;
|
|
934
|
+
}
|
|
935
|
+
return schemaConflicts <= 1;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
function xaiRequiredSetsMatch(variants: Record<string, unknown>[]): boolean {
|
|
939
|
+
const serialized = variants.map(variant => [...stringRequiredFields(variant.required)].sort().join("\0"));
|
|
940
|
+
return serialized.every(value => value === serialized[0]);
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
function mergeXaiAdditionalProperties(
|
|
944
|
+
variants: Record<string, unknown>[],
|
|
945
|
+
): { ok: true; value?: unknown } | { ok: false } {
|
|
946
|
+
const values = variants.map(variant => variant.additionalProperties);
|
|
947
|
+
const explicit = values.filter(value => value !== undefined);
|
|
948
|
+
if (explicit.length === 0) return { ok: true };
|
|
949
|
+
if (explicit.length !== values.length) return { ok: false };
|
|
950
|
+
const hasFalse = explicit.some(value => value === false);
|
|
951
|
+
const permissive = explicit.filter(value => value !== false);
|
|
952
|
+
if (hasFalse && permissive.length > 0) return { ok: false };
|
|
953
|
+
if (hasFalse) return { ok: true, value: false };
|
|
954
|
+
const unique: unknown[] = [];
|
|
955
|
+
const seen = new Set<string>();
|
|
956
|
+
for (const value of permissive) {
|
|
957
|
+
const key = JSON.stringify(value);
|
|
958
|
+
if (seen.has(key)) continue;
|
|
959
|
+
seen.add(key);
|
|
960
|
+
unique.push(value);
|
|
961
|
+
}
|
|
962
|
+
if (unique.length !== 1) return { ok: false };
|
|
963
|
+
return { ok: true, value: unique[0] };
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
/** Compose root siblings into a branch so properties/required are not overwritten. */
|
|
967
|
+
function composeXaiObjectSchemas(
|
|
968
|
+
inherited: Record<string, unknown>,
|
|
969
|
+
branch: Record<string, unknown>,
|
|
970
|
+
): Record<string, unknown> {
|
|
971
|
+
const composed: Record<string, unknown> = { ...inherited, ...branch };
|
|
972
|
+
const inheritedProps = isXaiObjectSchema(inherited.properties) ? inherited.properties : undefined;
|
|
973
|
+
const branchProps = isXaiObjectSchema(branch.properties) ? branch.properties : undefined;
|
|
974
|
+
if (inheritedProps || branchProps) {
|
|
975
|
+
const properties: Record<string, unknown> = { ...(inheritedProps ?? {}) };
|
|
976
|
+
for (const [name, value] of Object.entries(branchProps ?? {})) {
|
|
977
|
+
const inheritedValue = inheritedProps?.[name];
|
|
978
|
+
properties[name] = inheritedValue !== undefined && JSON.stringify(inheritedValue) !== JSON.stringify(value)
|
|
979
|
+
? { allOf: [inheritedValue, value] }
|
|
980
|
+
: value;
|
|
981
|
+
}
|
|
982
|
+
composed.properties = properties;
|
|
983
|
+
}
|
|
984
|
+
const required = [...new Set([
|
|
985
|
+
...stringRequiredFields(inherited.required),
|
|
986
|
+
...stringRequiredFields(branch.required),
|
|
987
|
+
])];
|
|
988
|
+
if (required.length > 0) composed.required = required;
|
|
989
|
+
else delete composed.required;
|
|
990
|
+
return composed;
|
|
991
|
+
}
|
|
992
|
+
|
|
699
993
|
function expandXaiRootObjectSchemas(schema: unknown): Record<string, unknown>[] | undefined {
|
|
700
|
-
if (!
|
|
701
|
-
const
|
|
702
|
-
const compositionKey = ["oneOf", "anyOf"].find(key => Array.isArray(obj[key]));
|
|
994
|
+
if (!isXaiObjectSchema(schema)) return undefined;
|
|
995
|
+
const compositionKey = ["oneOf", "anyOf"].find(key => Array.isArray(schema[key]));
|
|
703
996
|
if (!compositionKey) {
|
|
704
|
-
if (
|
|
705
|
-
return [{ ...
|
|
997
|
+
if (schema.type !== undefined && schema.type !== "object") return undefined;
|
|
998
|
+
return [{ ...schema, type: "object" }];
|
|
706
999
|
}
|
|
707
1000
|
|
|
708
|
-
const siblings = Object.fromEntries(Object.entries(
|
|
709
|
-
const branches =
|
|
1001
|
+
const siblings = Object.fromEntries(Object.entries(schema).filter(([key]) => key !== compositionKey));
|
|
1002
|
+
const branches = schema[compositionKey];
|
|
710
1003
|
if (!Array.isArray(branches)) return undefined;
|
|
711
1004
|
const expanded: Record<string, unknown>[] = [];
|
|
712
1005
|
for (const branch of branches) {
|
|
713
1006
|
const variants = expandXaiRootObjectSchemas(branch);
|
|
714
1007
|
if (!variants) return undefined;
|
|
715
|
-
for (const variant of variants) expanded.push(
|
|
1008
|
+
for (const variant of variants) expanded.push(composeXaiObjectSchemas(siblings, variant));
|
|
716
1009
|
}
|
|
717
1010
|
return expanded.length > 0 ? expanded : undefined;
|
|
718
1011
|
}
|
|
719
1012
|
|
|
1013
|
+
function mergeXaiPropertySchemas(values: unknown[]): unknown {
|
|
1014
|
+
const unique: unknown[] = [];
|
|
1015
|
+
const serialized = new Set<string>();
|
|
1016
|
+
for (const value of values) {
|
|
1017
|
+
const key = JSON.stringify(value);
|
|
1018
|
+
if (serialized.has(key)) continue;
|
|
1019
|
+
serialized.add(key);
|
|
1020
|
+
unique.push(value);
|
|
1021
|
+
}
|
|
1022
|
+
return unique.length === 1 ? unique[0] : { anyOf: unique };
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
/**
|
|
1026
|
+
* The Grok CLI proxy rejects a function parameter schema whose root remains oneOf/anyOf.
|
|
1027
|
+
* Flatten only when the merge is lossless: local $refs resolve, every variant is a concrete
|
|
1028
|
+
* object whose keys we can preserve, required sets match, additionalProperties does not change
|
|
1029
|
+
* meaning, every property name exists on every variant, and at most one property schema
|
|
1030
|
+
* differs. Otherwise omit the tool rather than emit a weaker schema.
|
|
1031
|
+
*/
|
|
720
1032
|
function normalizeXaiToolParameters(parameters: unknown): Record<string, unknown> | undefined {
|
|
721
|
-
|
|
1033
|
+
if (!isXaiObjectSchema(parameters)) return undefined;
|
|
1034
|
+
const resolved = resolveXaiSchemaRefs(parameters, parameters);
|
|
1035
|
+
if (!isXaiObjectSchema(resolved)) return undefined;
|
|
1036
|
+
const variants = expandXaiRootObjectSchemas(resolved);
|
|
722
1037
|
if (!variants) return undefined;
|
|
723
|
-
if (variants.length === 1)
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
const
|
|
728
|
-
|
|
1038
|
+
if (variants.length === 1) {
|
|
1039
|
+
return xaiVariantIsConcreteObject(variants[0]) ? variants[0] : undefined;
|
|
1040
|
+
}
|
|
1041
|
+
if (!variants.every(xaiVariantIsConcreteObject) || !xaiRequiredSetsMatch(variants)) return undefined;
|
|
1042
|
+
const additionalProperties = mergeXaiAdditionalProperties(variants);
|
|
1043
|
+
if (!additionalProperties.ok) return undefined;
|
|
1044
|
+
if (!xaiPropertyMergeIsLossless(variants)) return undefined;
|
|
1045
|
+
|
|
1046
|
+
const metadata = Object.fromEntries(Object.entries(resolved).filter(([key]) => key !== "oneOf" && key !== "anyOf" && key !== "type"));
|
|
1047
|
+
delete metadata.properties;
|
|
1048
|
+
delete metadata.required;
|
|
1049
|
+
delete metadata.additionalProperties;
|
|
1050
|
+
|
|
1051
|
+
const propertyValues = new Map<string, unknown[]>();
|
|
1052
|
+
for (const variant of variants) {
|
|
1053
|
+
if (!variant.properties || typeof variant.properties !== "object" || Array.isArray(variant.properties)) continue;
|
|
1054
|
+
for (const [name, value] of Object.entries(variant.properties as Record<string, unknown>)) {
|
|
1055
|
+
const values = propertyValues.get(name) ?? [];
|
|
1056
|
+
values.push(value);
|
|
1057
|
+
propertyValues.set(name, values);
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
const properties = Object.fromEntries(
|
|
1061
|
+
[...propertyValues].map(([name, values]) => [name, mergeXaiPropertySchemas(values)]),
|
|
1062
|
+
);
|
|
1063
|
+
const required = stringRequiredFields(variants[0]?.required);
|
|
1064
|
+
|
|
1065
|
+
return {
|
|
1066
|
+
...metadata,
|
|
1067
|
+
type: "object",
|
|
1068
|
+
properties,
|
|
1069
|
+
...(required.length > 0 ? { required } : {}),
|
|
1070
|
+
...("value" in additionalProperties ? { additionalProperties: additionalProperties.value } : {}),
|
|
1071
|
+
};
|
|
729
1072
|
}
|
|
730
1073
|
|
|
731
1074
|
function toolsToChatFormat(parsed: OcxParsedRequest, provider: OcxProviderConfig): unknown[] | undefined {
|
|
@@ -825,10 +1168,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
825
1168
|
formatErrorBody: formatOpenAIChatErrorBody,
|
|
826
1169
|
|
|
827
1170
|
buildRequest(parsed: OcxParsedRequest) {
|
|
828
|
-
const
|
|
829
|
-
if ((provider.authMode === "key" || provider.authMode === "oauth") && !provider.keyOptional && !hasCredential) {
|
|
830
|
-
throw new Error(`${provider.adapter} requires a non-empty credential (authMode: ${provider.authMode})`);
|
|
831
|
-
}
|
|
1171
|
+
const { url, headers, hasCredential } = openAIChatTransport(provider);
|
|
832
1172
|
|
|
833
1173
|
const messages = messagesToChatFormat(parsed, provider);
|
|
834
1174
|
const tools = toolsToChatFormatForProvider(parsed, provider);
|
|
@@ -844,11 +1184,11 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
844
1184
|
// an explicit value here makes the Responses parser's serviceTier projection ineffective.
|
|
845
1185
|
//
|
|
846
1186
|
// Opt-in, like `prompt_cache_key` directly below: `service_tier` is an OpenAI-specific
|
|
847
|
-
// extension and 66 registry providers share this adapter
|
|
848
|
-
//
|
|
849
|
-
//
|
|
850
|
-
|
|
851
|
-
|
|
1187
|
+
// extension and 66 registry providers share this adapter. A provider-wide Chat opt-in
|
|
1188
|
+
// authorizes undeclared models; an exact model declaration can authorize or deny one
|
|
1189
|
+
// model. Provider-level false remains fail-closed.
|
|
1190
|
+
if (canSerializeServiceTierForChatModel(provider, parsed.modelId)
|
|
1191
|
+
&& parsed.options.serviceTier !== undefined) {
|
|
852
1192
|
body.service_tier = parsed.options.serviceTier;
|
|
853
1193
|
}
|
|
854
1194
|
if (modelInList(provider.reasoningSplitModels, parsed.modelId)) body.reasoning_split = true;
|
|
@@ -981,11 +1321,6 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
981
1321
|
}
|
|
982
1322
|
if (parsed.stream) body.stream_options = { include_usage: true };
|
|
983
1323
|
|
|
984
|
-
const url = openaiChatCompletionsUrl(provider.baseUrl);
|
|
985
|
-
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
|
986
|
-
if (hasCredential) headers["Authorization"] = `Bearer ${provider.apiKey}`;
|
|
987
|
-
if (provider.headers) Object.assign(headers, provider.headers);
|
|
988
|
-
|
|
989
1324
|
const bodyJson = JSON.stringify(body);
|
|
990
1325
|
if (isDebugEnabled()) {
|
|
991
1326
|
let host = "upstream";
|