@bitkyc08/opencodex 2.42.0 → 2.43.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/AGENTS_INSTALL.md +2 -2
- package/README.md +31 -0
- package/bin/ocx.mjs +10 -1
- package/gui/dist/assets/index-DS1NE4Jn.css +1 -0
- package/gui/dist/assets/index-Djowl68T.js +112 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic-image-codec.ts +304 -0
- package/src/adapters/anthropic-image-normalize.ts +8 -298
- package/src/adapters/anthropic.ts +30 -7
- package/src/adapters/command-code.ts +7 -5
- package/src/adapters/cursor/desktop-executor-contract.ts +15 -0
- package/src/adapters/cursor/images.ts +36 -6
- package/src/adapters/cursor/live-transport.ts +7 -2
- package/src/adapters/cursor/native-exec-desktop.ts +2 -15
- package/src/adapters/cursor/protobuf-request.ts +13 -3
- package/src/adapters/cursor/tool-definitions.ts +5 -670
- package/src/adapters/cursor/tool-guidance.ts +236 -0
- package/src/adapters/cursor/tool-naming.ts +252 -0
- package/src/adapters/cursor/tool-schemas.ts +195 -0
- package/src/adapters/cursor/types.ts +6 -3
- package/src/adapters/exec-tool-result-normalize.ts +1 -1
- package/src/adapters/google-errors.ts +9 -1
- package/src/adapters/google.ts +1 -0
- package/src/adapters/kiro-calibration.ts +181 -0
- package/src/adapters/kiro.ts +135 -3
- package/src/adapters/openai-responses.ts +216 -26
- package/src/adapters/responses-code-mode.ts +59 -0
- package/src/adapters/tool-catalog-nudge.ts +1 -1
- package/src/adapters/xai-schema-analysis.ts +86 -0
- package/src/adapters/xai-tool-schema.ts +2 -87
- package/src/adapters/xai-web-search.ts +1 -1
- package/src/bridge.ts +22 -10
- package/src/chat/inbound.ts +11 -3
- package/src/claude/inbound-content-options.ts +60 -0
- package/src/claude/inbound-model-options.ts +142 -0
- package/src/claude/inbound-records.ts +7 -0
- package/src/claude/inbound.ts +10 -202
- package/src/claude/model-info.ts +45 -0
- package/src/cli/account-auth.ts +21 -6
- package/src/cli/capabilities.ts +13 -2
- package/src/cli/claude.ts +232 -39
- package/src/cli/config-command.ts +9 -1
- package/src/cli/dispatch.ts +5 -1
- package/src/cli/doctor.ts +10 -0
- package/src/cli/effort.ts +372 -0
- package/src/cli/export-command.ts +3 -9
- package/src/cli/help.ts +1 -0
- package/src/cli/index.ts +13 -0
- package/src/cli/init.ts +4 -0
- package/src/cli/model-selection-guidance.ts +30 -0
- package/src/cli/models-runtime.ts +3 -2
- package/src/cli/models.ts +8 -3
- package/src/cli/opencode.ts +4 -1
- package/src/cli/provider-runtime.ts +65 -0
- package/src/cli/provider.ts +8 -0
- package/src/cli/registry.ts +16 -2
- package/src/cli/runtime-api.ts +3 -1
- package/src/cli/star-prompt.ts +22 -6
- package/src/cli/status-probes.ts +168 -0
- package/src/cli/status.ts +5 -168
- package/src/clients/config-export/constants.ts +69 -0
- package/src/clients/config-export/contracts.ts +154 -0
- package/src/clients/config-export/dsh.ts +132 -0
- package/src/clients/config-export/fast-models.ts +29 -0
- package/src/clients/config-export/mcode.ts +83 -0
- package/src/clients/config-export/model-metadata.ts +108 -0
- package/src/clients/config-export/omp.ts +104 -0
- package/src/clients/config-export/zcode.ts +92 -0
- package/src/clients/config-export.ts +18 -710
- package/src/codex/account-lifecycle.ts +20 -3
- package/src/codex/account-usability.ts +2 -0
- package/src/codex/auth-api.ts +170 -23
- package/src/codex/auth-context.ts +328 -24
- package/src/codex/catalog/effort.ts +30 -4
- package/src/codex/catalog/metadata.ts +39 -9
- package/src/codex/catalog/native-models.ts +53 -12
- package/src/codex/catalog/parsing.ts +119 -5
- package/src/codex/catalog/provider-fetch.ts +150 -20
- package/src/codex/catalog/reserve.ts +52 -0
- package/src/codex/catalog/sync.ts +89 -16
- package/src/codex/convergence-types.ts +1 -0
- package/src/codex/convergence.ts +2 -0
- package/src/codex/data/upstream-models.json +1 -1
- package/src/codex/forward-transport-headers.ts +25 -0
- package/src/codex/inject.ts +3 -28
- package/src/codex/legacy-config-keys.ts +68 -0
- package/src/codex/log-guard/inspect-schema.ts +137 -0
- package/src/codex/log-guard/inspect.ts +2 -134
- package/src/codex/loopback-target.ts +54 -0
- package/src/codex/main-account-cache.ts +63 -1
- package/src/codex/main-account-hard-lock.ts +52 -0
- package/src/codex/main-account.ts +3 -1
- package/src/codex/management-convergence.ts +3 -0
- package/src/codex/model-entitlements.ts +54 -4
- package/src/codex/project-config-warnings.ts +92 -2
- package/src/codex/prompt-layers/encoding.ts +80 -0
- package/src/codex/prompt-layers/paths.ts +54 -0
- package/src/codex/prompt-layers/revision.ts +55 -0
- package/src/codex/prompt-layers/toml-edit.ts +163 -0
- package/src/codex/prompt-layers/toml-read.ts +181 -0
- package/src/codex/prompt-layers.ts +14 -520
- package/src/codex/quota-auto-refresh-state.ts +16 -0
- package/src/codex/quota-auto-refresh.ts +219 -0
- package/src/codex/quota-types.ts +51 -0
- package/src/codex/quota.ts +252 -93
- package/src/codex/reserve-availability.ts +177 -0
- package/src/codex/routing.ts +28 -9
- package/src/codex/shim.ts +53 -11
- package/src/codex/subagent-model-fallback.ts +23 -3
- package/src/combos/failover.ts +125 -7
- package/src/combos/identifiers.ts +89 -0
- package/src/combos/index.ts +4 -0
- package/src/combos/resolve.ts +80 -9
- package/src/combos/types.ts +20 -93
- package/src/config/subagent-models.ts +24 -0
- package/src/config.ts +156 -13
- package/src/generated/compatibility-version.json +454 -166
- package/src/generated/model-metadata.ts +1 -1
- package/src/integrations/journal.ts +65 -4
- package/src/integrations/store.ts +5 -0
- package/src/lab/events/limits.ts +4 -0
- package/src/lib/destination-policy.ts +31 -2
- package/src/lib/errors.ts +39 -0
- package/src/lib/provider-outbound.ts +69 -3
- package/src/lib/proxy-env.ts +22 -0
- package/src/lib/redact-folding.ts +176 -0
- package/src/lib/redact.ts +2 -175
- package/src/lib/state-store-sweeper.ts +20 -6
- package/src/lib/token-estimate.ts +94 -27
- package/src/lib/windows-user-principal.ts +53 -5
- package/src/oauth/anthropic-routing.ts +99 -3
- package/src/oauth/generic-account-failover.ts +36 -13
- package/src/oauth/index.ts +127 -26
- package/src/oauth/login-cli.ts +5 -0
- package/src/oauth/meta-muse.ts +117 -15
- package/src/oauth/pool-settings-capability.ts +15 -4
- package/src/providers/api-keys.ts +8 -10
- package/src/providers/default-aliases.ts +39 -0
- package/src/providers/derive.ts +10 -2
- package/src/providers/fastwire.ts +36 -7
- package/src/providers/initial-model-selection-runtime.ts +90 -0
- package/src/providers/initial-model-selection.ts +120 -0
- package/src/providers/key-failover.ts +134 -54
- package/src/providers/key-store.ts +11 -1
- package/src/providers/label.ts +1 -1
- package/src/providers/model-discovery.ts +76 -0
- package/src/providers/model-rename-startup.ts +72 -8
- package/src/providers/openai-sidecar.ts +17 -5
- package/src/providers/openai-tiers-destination.ts +102 -0
- package/src/providers/openai-tiers.ts +2 -99
- package/src/providers/opencode-go-transport.ts +41 -0
- package/src/providers/quota-key-accounts.ts +141 -0
- package/src/providers/quota-types.ts +9 -0
- package/src/providers/quota.ts +535 -104
- package/src/providers/registry.ts +60 -17
- package/src/providers/xai-responses-opt-in.ts +31 -5
- package/src/quota/reset-activation.ts +81 -0
- package/src/quota/reset-detector.ts +305 -0
- package/src/quota/reset-notify-config.ts +162 -0
- package/src/quota/reset-observer.ts +125 -0
- package/src/quota/reset-poller.ts +160 -0
- package/src/quota/reset-seen-store.ts +385 -0
- package/src/quota/reset-sinks.ts +199 -0
- package/src/quota/window-mapping.ts +106 -0
- package/src/responses/apply-patch-envelope.ts +46 -0
- package/src/responses/code-mode-helper-compat.ts +39 -1
- package/src/responses/custom-tool-compat.ts +10 -4
- package/src/responses/hosted-tool-policy.ts +12 -4
- package/src/responses/parser-content.ts +133 -0
- package/src/responses/parser-text-format.ts +24 -0
- package/src/responses/parser-tools.ts +188 -0
- package/src/responses/parser.ts +3 -326
- package/src/responses/state.ts +124 -28
- package/src/router.ts +48 -13
- package/src/routing/analytics.ts +1 -0
- package/src/routing/capability.ts +17 -4
- package/src/server/auth-cors.ts +7 -1
- package/src/server/background-lifecycle.ts +23 -1
- package/src/server/chat-completions.ts +25 -3
- package/src/server/claude-messages.ts +62 -5
- package/src/server/effort-row.ts +1 -1
- package/src/server/fast-row.ts +295 -0
- package/src/server/gui-static.ts +30 -4
- package/src/server/index.ts +107 -21
- package/src/server/management/agent-settings-routes.ts +2 -2
- package/src/server/management/combo-routes.ts +37 -9
- package/src/server/management/config-routes.ts +93 -2
- package/src/server/management/integration-routes.ts +108 -0
- package/src/server/management/model-routes.ts +13 -3
- package/src/server/management/model-rows.ts +20 -1
- package/src/server/management/native-integration-routes.ts +4 -1
- package/src/server/management/oauth-account-routes.ts +38 -10
- package/src/server/management/provider-routes.ts +34 -3
- package/src/server/management/quota-reset-routes.ts +57 -0
- package/src/server/management/route-registry.ts +7 -4
- package/src/server/management/shared.ts +19 -5
- package/src/server/management/system-routes.ts +3 -2
- package/src/server/management-api.ts +14 -2
- package/src/server/ports.ts +12 -2
- package/src/server/relay-eager.ts +38 -23
- package/src/server/relay.ts +4 -0
- package/src/server/request-log.ts +6 -0
- package/src/server/responses/codex-ws-correlation.ts +65 -0
- package/src/server/responses/codex-ws-exchange.ts +261 -0
- package/src/server/responses/codex-ws-metadata.ts +134 -0
- package/src/server/responses/codex-ws-pool.ts +162 -0
- package/src/server/responses/codex-ws-request.ts +87 -0
- package/src/server/responses/codex-ws-session.ts +93 -0
- package/src/server/responses/codex-ws-wire.ts +144 -0
- package/src/server/responses/collaboration.ts +41 -1
- package/src/server/responses/compact.ts +105 -12
- package/src/server/responses/core.ts +476 -57
- package/src/server/responses/fetch-helpers.ts +10 -3
- package/src/server/responses/input-admission.ts +16 -9
- package/src/server/responses/responses-field-backfill.ts +1 -1
- package/src/server/responses/ws-upstream.ts +34 -318
- package/src/server/responses-custom-tool-repair.ts +20 -4
- package/src/server/responses-undeclared-tool-guard.ts +100 -8
- package/src/server/safe-response-headers.ts +23 -0
- package/src/server/search.ts +9 -0
- package/src/server/subagent-models-startup.ts +27 -0
- package/src/server/system-env-shell.ts +238 -0
- package/src/server/system-env.ts +7 -234
- package/src/server/ws-bridge.ts +3 -25
- package/src/server/xai-responses-startup.ts +21 -0
- package/src/service-manager-probe.ts +1 -1
- package/src/service.ts +55 -16
- package/src/types/config.ts +108 -12
- package/src/types/provider.ts +36 -7
- package/src/types/tools.ts +26 -1
- package/src/types.ts +2 -0
- package/src/update/notify.ts +8 -2
- package/src/usage/cost.ts +38 -28
- package/src/usage/expected-prices.ts +34 -15
- package/src/usage/log.ts +2 -0
- package/src/usage/model-identity.ts +26 -0
- package/src/usage/summary.ts +15 -1
- package/src/vision/describe.ts +6 -0
- package/src/vision/image-rewrite.ts +108 -0
- package/src/vision/index.ts +19 -306
- package/src/vision/plan.ts +205 -0
- package/src/web-search/executor.ts +6 -0
- package/src/web-search/index.ts +8 -1
- package/gui/dist/assets/index-BU1tE0sr.js +0 -112
- package/gui/dist/assets/index-DL9-iS6J.css +0 -1
|
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import type { IncomingMeta, ProviderAdapter } from "./base";
|
|
3
3
|
import { namespacedToolName, type AdapterEvent, type OcxParsedRequest, type OcxProviderConfig, type OcxUsage, type TierDecision } from "../types";
|
|
4
4
|
import { catalogModelSupportsReasoningSummaries } from "../codex/catalog";
|
|
5
|
+
import { applyCodexRoutingHint, CODEX_RESPONSES_LITE_HEADER, CODEX_ROUTING_HINT_HEADER } from "../codex/forward-transport-headers";
|
|
5
6
|
import { COMPACT_PROMPT, compactionItemToText, decodeCompactionSummary, isCompactionItemType } from "../responses/compaction";
|
|
6
7
|
import { collectResponsesToolGroups } from "../responses/tool-groups";
|
|
7
8
|
import { isHostedToolUnsupportedForModel } from "../responses/hosted-tool-policy";
|
|
@@ -20,6 +21,7 @@ import { rewriteRoutedCustomToolsForUpstream } from "../responses/custom-tool-co
|
|
|
20
21
|
import { rewriteRoutedToolSearchForUpstream } from "../responses/tool-search-compat";
|
|
21
22
|
import { rewriteRoutedNamespaceToolsForUpstream } from "../responses/namespace-tool-compat";
|
|
22
23
|
import { openaiResponsesUrl } from "./openai-responses-url";
|
|
24
|
+
import { normalizeResponsesCodeMode } from "./responses-code-mode";
|
|
23
25
|
import { injectXaiResponsesXSearch, normalizeXaiResponsesWebSearch } from "./xai-web-search";
|
|
24
26
|
import { EMPTY_TOOL_OUTPUT_ANNOTATION, isWhitespaceOnlyTextPartArray } from "./empty-tool-output-annotation";
|
|
25
27
|
import {
|
|
@@ -51,6 +53,7 @@ export const FORWARD_HEADERS = [
|
|
|
51
53
|
"x-oai-attestation",
|
|
52
54
|
"x-openai-subagent",
|
|
53
55
|
"x-responsesapi-include-timing-metrics",
|
|
56
|
+
CODEX_RESPONSES_LITE_HEADER,
|
|
54
57
|
];
|
|
55
58
|
|
|
56
59
|
/**
|
|
@@ -187,7 +190,7 @@ const CANONICAL_ONLY_TOOL_FIELDS: readonly { field: string; toolTypes?: Readonly
|
|
|
187
190
|
// OWNERSHIP: official OpenAI API-key traffic and unclassified gateways ACCEPT this field, so
|
|
188
191
|
// it is only stripped when the provider capability denies it (supportsOpenAiWebSearchToolFields
|
|
189
192
|
// === false), matching stripOpenAiOnlyWebSearchFields; see
|
|
190
|
-
// tests/responses-routed-web-search-fields.test.ts.
|
|
193
|
+
// tests/responses/responses-routed-web-search-fields.test.ts.
|
|
191
194
|
{ field: "external_web_access", toolTypes: new Set(["web_search", "web_search_preview"]), capabilityGated: true },
|
|
192
195
|
// Deferred-discovery marker. `activateDeferredTool` clears it only for tools a `tool_search_output`
|
|
193
196
|
// already loaded, so a still-deferred declaration — including one promoted out of a namespace
|
|
@@ -906,6 +909,61 @@ function toolOutputText(output: unknown): string {
|
|
|
906
909
|
}).filter(Boolean).join("\n");
|
|
907
910
|
}
|
|
908
911
|
|
|
912
|
+
/** True when an output can be losslessly represented as user-message content. */
|
|
913
|
+
function isRepairableToolOutput(output: unknown): output is string | Record<string, unknown>[] {
|
|
914
|
+
if (typeof output === "string") return true;
|
|
915
|
+
if (!Array.isArray(output)) return false;
|
|
916
|
+
return output.every(part => {
|
|
917
|
+
if (!isPlainObject(part)) return false;
|
|
918
|
+
if (typeof part.type !== "string") return false;
|
|
919
|
+
if (["output_text", "text", "input_text"].includes(part.type)) {
|
|
920
|
+
return typeof part.text === "string";
|
|
921
|
+
}
|
|
922
|
+
if (part.type === "refusal") return typeof part.refusal === "string";
|
|
923
|
+
if (part.type === "encrypted_content") return typeof part.encrypted_content === "string";
|
|
924
|
+
if (part.type !== "input_image") return false;
|
|
925
|
+
const imageUrl = part.image_url;
|
|
926
|
+
const fileId = part.file_id;
|
|
927
|
+
const imageUrlIsString = typeof imageUrl === "string";
|
|
928
|
+
const fileIdIsString = typeof fileId === "string";
|
|
929
|
+
const hasUsableSource = (imageUrlIsString && imageUrl.length > 0)
|
|
930
|
+
|| (fileIdIsString && fileId.length > 0);
|
|
931
|
+
const validSource = hasUsableSource
|
|
932
|
+
&& (part.image_url === undefined || imageUrlIsString)
|
|
933
|
+
&& (part.file_id === undefined || fileIdIsString);
|
|
934
|
+
const validDetail = part.detail === undefined
|
|
935
|
+
|| (typeof part.detail === "string"
|
|
936
|
+
&& ["auto", "low", "high", "original"].includes(part.detail));
|
|
937
|
+
return validSource && validDetail;
|
|
938
|
+
});
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
/** Convert orphaned tool output to user-message content without discarding valid images. */
|
|
942
|
+
function orphanedToolOutputContent(output: unknown, callId = ""): Record<string, unknown>[] {
|
|
943
|
+
const marker = `[tool output for ${callId || "unknown call"}]`;
|
|
944
|
+
if (typeof output !== "string" && !Array.isArray(output)) {
|
|
945
|
+
return [{ type: "input_text", text: marker }];
|
|
946
|
+
}
|
|
947
|
+
if (!Array.isArray(output)) {
|
|
948
|
+
return [{ type: "input_text", text: `${marker}\n${toolOutputText(output)}` }];
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
const content: Record<string, unknown>[] = [{ type: "input_text", text: marker }];
|
|
952
|
+
for (const part of output) {
|
|
953
|
+
if (!isPlainObject(part)) continue;
|
|
954
|
+
if (part.type === "input_image") {
|
|
955
|
+
content.push(part);
|
|
956
|
+
} else if (part.type === "encrypted_content" && typeof part.encrypted_content === "string") {
|
|
957
|
+
content.push({ type: "input_text", text: "[encrypted content omitted]" });
|
|
958
|
+
} else if (typeof part.text === "string") {
|
|
959
|
+
content.push({ type: "input_text", text: part.text });
|
|
960
|
+
} else if (part.type === "refusal" && typeof part.refusal === "string") {
|
|
961
|
+
content.push({ type: "input_text", text: `[refusal] ${part.refusal}` });
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
return content;
|
|
965
|
+
}
|
|
966
|
+
|
|
909
967
|
/** True when a Responses tool output item is present but carries no usable content. */
|
|
910
968
|
function isToolOutputEmpty(output: unknown): boolean {
|
|
911
969
|
if (typeof output === "string") return output.trim() === "";
|
|
@@ -940,6 +998,32 @@ function annotateEmptyResponsesToolOutputs(body: unknown, enabled: boolean): unk
|
|
|
940
998
|
return changed ? { ...body, input } : body;
|
|
941
999
|
}
|
|
942
1000
|
|
|
1001
|
+
/**
|
|
1002
|
+
* Preserve the text of structurally invalid tool-output items before they reach a strict
|
|
1003
|
+
* Responses parser. Stateful destinations may legitimately receive an output whose matching
|
|
1004
|
+
* call lives behind `previous_response_id`, so ordinary orphan repair cannot run universally.
|
|
1005
|
+
* A missing or empty `call_id`, however, cannot identify stored state on any destination.
|
|
1006
|
+
*/
|
|
1007
|
+
function repairUnidentifiedToolOutputItems(body: unknown): unknown {
|
|
1008
|
+
if (!isPlainObject(body) || !Array.isArray(body.input)) return body;
|
|
1009
|
+
let changed = false;
|
|
1010
|
+
const input = body.input.map(item => {
|
|
1011
|
+
if (!isPlainObject(item)
|
|
1012
|
+
|| (item.type !== "function_call_output" && item.type !== "custom_tool_call_output")
|
|
1013
|
+
|| (typeof item.call_id === "string" && item.call_id.length > 0)) {
|
|
1014
|
+
return item;
|
|
1015
|
+
}
|
|
1016
|
+
if (!isRepairableToolOutput(item.output)) return item;
|
|
1017
|
+
changed = true;
|
|
1018
|
+
return {
|
|
1019
|
+
type: "message",
|
|
1020
|
+
role: "user",
|
|
1021
|
+
content: orphanedToolOutputContent(item.output),
|
|
1022
|
+
};
|
|
1023
|
+
});
|
|
1024
|
+
return changed ? { ...body, input } : body;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
943
1027
|
/**
|
|
944
1028
|
* Repair a forward-mode input array whose continuation context was lost. When the replay
|
|
945
1029
|
* expansion misses (proxy restart, unrecorded prior turn), previous_response_id is stripped
|
|
@@ -1060,12 +1144,17 @@ function repairOrphanedInputItems(body: unknown, dropReasoning: boolean, synthes
|
|
|
1060
1144
|
flushPendingSyntheticOutputs();
|
|
1061
1145
|
const callId = typeof item.call_id === "string" ? item.call_id : "";
|
|
1062
1146
|
const paired = isFnOutput ? functionCallIds.has(callId) : customCallIds.has(callId);
|
|
1063
|
-
|
|
1147
|
+
const usableOutput = isRepairableToolOutput(item.output);
|
|
1148
|
+
// A known orphan call is still useful as a labeled user message even when its output is
|
|
1149
|
+
// incomplete. With no call id and no output, preserve the invalid item so validation fails
|
|
1150
|
+
// closed rather than pretending any tool result exists.
|
|
1151
|
+
const knownNullOutput = callId.length > 0 && item.output == null;
|
|
1152
|
+
if (!paired && (knownNullOutput || usableOutput)) {
|
|
1064
1153
|
changed = true;
|
|
1065
1154
|
repaired.push({
|
|
1066
1155
|
type: "message",
|
|
1067
1156
|
role: "user",
|
|
1068
|
-
content:
|
|
1157
|
+
content: orphanedToolOutputContent(item.output, callId),
|
|
1069
1158
|
});
|
|
1070
1159
|
continue;
|
|
1071
1160
|
}
|
|
@@ -1893,14 +1982,67 @@ function normalizeImageGenClientTools(body: unknown): unknown {
|
|
|
1893
1982
|
* carries a tool the upstream model 400s on. No-op (returns the original reference) when nothing
|
|
1894
1983
|
* matches, keeping the common path allocation-free.
|
|
1895
1984
|
*/
|
|
1896
|
-
function stripUnsupportedHostedTools(body: unknown): unknown {
|
|
1897
|
-
if (!isPlainObject(body)
|
|
1985
|
+
function stripUnsupportedHostedTools(body: unknown, provider: Pick<OcxProviderConfig, "baseUrl">): unknown {
|
|
1986
|
+
if (!isPlainObject(body)) return body;
|
|
1898
1987
|
const model = typeof body.model === "string" ? body.model : "";
|
|
1899
|
-
const
|
|
1900
|
-
const
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1988
|
+
const filterTools = (tools: unknown[]): unknown[] => {
|
|
1989
|
+
const filtered = tools.filter(t => {
|
|
1990
|
+
const type = isPlainObject(t) && typeof t.type === "string" ? t.type : undefined;
|
|
1991
|
+
return !type || !isHostedToolUnsupportedForModel(model, type, provider.baseUrl);
|
|
1992
|
+
});
|
|
1993
|
+
return filtered.length === tools.length ? tools : filtered;
|
|
1994
|
+
};
|
|
1995
|
+
|
|
1996
|
+
let next: Record<string, unknown> = body;
|
|
1997
|
+
let changed = false;
|
|
1998
|
+
if (Array.isArray(body.tools)) {
|
|
1999
|
+
const tools = filterTools(body.tools);
|
|
2000
|
+
if (tools !== body.tools) {
|
|
2001
|
+
next = { ...next, tools };
|
|
2002
|
+
changed = true;
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
if (Array.isArray(body.input)) {
|
|
2006
|
+
let inputChanged = false;
|
|
2007
|
+
const input = body.input.map(item => {
|
|
2008
|
+
if (!isPlainObject(item) || item.type !== "additional_tools" || !Array.isArray(item.tools)) return item;
|
|
2009
|
+
const tools = filterTools(item.tools);
|
|
2010
|
+
if (tools === item.tools) return item;
|
|
2011
|
+
inputChanged = true;
|
|
2012
|
+
return { ...item, tools };
|
|
2013
|
+
});
|
|
2014
|
+
if (inputChanged) {
|
|
2015
|
+
next = { ...next, input };
|
|
2016
|
+
changed = true;
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
const toolChoice = next.tool_choice;
|
|
2021
|
+
if (isPlainObject(toolChoice) && toolChoice.type === "allowed_tools" && Array.isArray(toolChoice.tools)) {
|
|
2022
|
+
const tools = filterTools(toolChoice.tools);
|
|
2023
|
+
if (tools !== toolChoice.tools) {
|
|
2024
|
+
next = { ...next, tool_choice: tools.length > 0 ? { ...toolChoice, tools } : "none" };
|
|
2025
|
+
changed = true;
|
|
2026
|
+
}
|
|
2027
|
+
} else if (
|
|
2028
|
+
isPlainObject(toolChoice)
|
|
2029
|
+
&& typeof toolChoice.type === "string"
|
|
2030
|
+
&& isHostedToolUnsupportedForModel(model, toolChoice.type, provider.baseUrl)
|
|
2031
|
+
) {
|
|
2032
|
+
next = { ...next, tool_choice: "none" };
|
|
2033
|
+
changed = true;
|
|
2034
|
+
} else if (changed && toolChoice === "required") {
|
|
2035
|
+
const hasDeclaredTools = (Array.isArray(next.tools) && next.tools.length > 0)
|
|
2036
|
+
|| (Array.isArray(next.input) && next.input.some(item =>
|
|
2037
|
+
isPlainObject(item)
|
|
2038
|
+
&& item.type === "additional_tools"
|
|
2039
|
+
&& Array.isArray(item.tools)
|
|
2040
|
+
&& item.tools.length > 0));
|
|
2041
|
+
if (!hasDeclaredTools) {
|
|
2042
|
+
next = { ...next, tool_choice: "none" };
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
return changed ? next : body;
|
|
1904
2046
|
}
|
|
1905
2047
|
|
|
1906
2048
|
/**
|
|
@@ -1966,10 +2108,10 @@ export function stripOpenAiOnlyWebSearchFields(body: unknown): unknown {
|
|
|
1966
2108
|
}
|
|
1967
2109
|
|
|
1968
2110
|
/**
|
|
1969
|
-
* Muse Spark ids whose Responses gateway refuses
|
|
2111
|
+
* Muse Spark ids whose Responses gateway refuses provider-specific fields on a plain
|
|
1970
2112
|
* `web_search` tool. Membership, not equality: 1.3 shipped 2026-09-02 as the
|
|
1971
2113
|
* same-shaped successor to 1.2 on the same Zen wire, and an equality check would
|
|
1972
|
-
* have let a Codex-emitted `web_search`
|
|
2114
|
+
* have let a Codex-emitted `web_search` body reach the
|
|
1973
2115
|
* gateway and come back 400 for every request the moment 1.3 was selected.
|
|
1974
2116
|
*/
|
|
1975
2117
|
const MUSE_SPARK_WEB_SEARCH_STRICT_MODELS = new Set([
|
|
@@ -1977,26 +2119,51 @@ const MUSE_SPARK_WEB_SEARCH_STRICT_MODELS = new Set([
|
|
|
1977
2119
|
"muse-spark-1.2-contributor",
|
|
1978
2120
|
]);
|
|
1979
2121
|
|
|
2122
|
+
const MUSE_SPARK_WEB_SEARCH_STRICT_RESPONSE_URLS = new Set([
|
|
2123
|
+
"https://opencode.ai/zen/v1/responses",
|
|
2124
|
+
"https://opencode.ai/zen/go/v1/responses",
|
|
2125
|
+
]);
|
|
2126
|
+
|
|
2127
|
+
const MUSE_SPARK_UNSUPPORTED_WEB_SEARCH_FIELDS = [
|
|
2128
|
+
"search_content_types",
|
|
2129
|
+
"indexed_web_access",
|
|
2130
|
+
] as const;
|
|
2131
|
+
|
|
1980
2132
|
/**
|
|
1981
|
-
* OpenCode Zen / Go Muse Spark Responses gateway refuses
|
|
1982
|
-
*
|
|
1983
|
-
*
|
|
1984
|
-
*
|
|
1985
|
-
*
|
|
1986
|
-
*
|
|
1987
|
-
* keeping the tool type and every other accepted option intact.
|
|
2133
|
+
* OpenCode Zen / Go Muse Spark Responses gateway refuses a short list of Codex
|
|
2134
|
+
* `web_search` fields. `web_search_preview` keeps its accepted shape, and Luna
|
|
2135
|
+
* remains untouched. Match the exact effective request URL; malformed, credentialed,
|
|
2136
|
+
* or parameterized destinations keep their original body instead of assuming this
|
|
2137
|
+
* gateway contract. Keep the rejected names together so a newly identified field is
|
|
2138
|
+
* a one-line compatibility update rather than another bespoke rewrite.
|
|
1988
2139
|
*/
|
|
1989
|
-
function stripMuseSparkUnsupportedWebSearchFields(
|
|
2140
|
+
function stripMuseSparkUnsupportedWebSearchFields(
|
|
2141
|
+
body: unknown,
|
|
2142
|
+
modelId: unknown,
|
|
2143
|
+
responseUrl: string,
|
|
2144
|
+
): unknown {
|
|
1990
2145
|
if (!isPlainObject(body)) return body;
|
|
1991
2146
|
if (typeof modelId !== "string") return body;
|
|
1992
2147
|
if (!MUSE_SPARK_WEB_SEARCH_STRICT_MODELS.has(modelId.trim().toLowerCase())) return body;
|
|
2148
|
+
let destination: string;
|
|
2149
|
+
try {
|
|
2150
|
+
const url = new URL(responseUrl);
|
|
2151
|
+
if (url.username || url.password || url.search || url.hash) return body;
|
|
2152
|
+
destination = `${url.origin.toLowerCase()}${url.pathname.replace(/\/+$/, "")}`;
|
|
2153
|
+
} catch {
|
|
2154
|
+
return body;
|
|
2155
|
+
}
|
|
2156
|
+
if (!MUSE_SPARK_WEB_SEARCH_STRICT_RESPONSE_URLS.has(destination)) return body;
|
|
1993
2157
|
|
|
1994
2158
|
const rewriteTools = (tools: unknown[]): { tools: unknown[]; changed: boolean } => {
|
|
1995
2159
|
let changed = false;
|
|
1996
2160
|
const rewritten = tools.map(tool => {
|
|
1997
2161
|
if (!isPlainObject(tool) || tool.type !== "web_search") return tool;
|
|
1998
|
-
if (!Object.hasOwn(tool,
|
|
1999
|
-
|
|
2162
|
+
if (!MUSE_SPARK_UNSUPPORTED_WEB_SEARCH_FIELDS.some(field => Object.hasOwn(tool, field))) {
|
|
2163
|
+
return tool;
|
|
2164
|
+
}
|
|
2165
|
+
const rest = { ...tool };
|
|
2166
|
+
for (const field of MUSE_SPARK_UNSUPPORTED_WEB_SEARCH_FIELDS) delete rest[field];
|
|
2000
2167
|
changed = true;
|
|
2001
2168
|
return rest;
|
|
2002
2169
|
});
|
|
@@ -2152,7 +2319,14 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
|
|
|
2152
2319
|
if (mayForwardCallerCredentials) {
|
|
2153
2320
|
for (const h of FORWARD_HEADERS) {
|
|
2154
2321
|
const v = incoming?.headers.get(h);
|
|
2155
|
-
if (v)
|
|
2322
|
+
if (v) {
|
|
2323
|
+
if (h === CODEX_RESPONSES_LITE_HEADER) {
|
|
2324
|
+
for (const name of Object.keys(headers)) {
|
|
2325
|
+
if (name.toLowerCase() === h) delete headers[name];
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
headers[h] = v; // …so genuine forwarded fields win.
|
|
2329
|
+
}
|
|
2156
2330
|
}
|
|
2157
2331
|
}
|
|
2158
2332
|
const override = runtimeProvider._codexAccountOverride;
|
|
@@ -2265,18 +2439,22 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
|
|
|
2265
2439
|
if (provider.supportsOpenAiWebSearchToolFields === false) {
|
|
2266
2440
|
outBody = stripOpenAiOnlyWebSearchFields(outBody);
|
|
2267
2441
|
}
|
|
2268
|
-
outBody = stripMuseSparkUnsupportedWebSearchFields(outBody, parsed.modelId);
|
|
2442
|
+
outBody = stripMuseSparkUnsupportedWebSearchFields(outBody, parsed.modelId, url);
|
|
2269
2443
|
// Last, so promoted namespace children are also cleared of Codex-private fields.
|
|
2270
2444
|
outBody = stripCanonicalOnlyToolFields(outBody, provider.supportsOpenAiWebSearchToolFields === false);
|
|
2271
2445
|
}
|
|
2272
2446
|
// Same predicate as the routedCompaction gate in handleResponses(): an authMode check would
|
|
2273
2447
|
// let a noncanonical custom forward provider skip this rewrite while the server still routes
|
|
2274
2448
|
// it as a summarizer turn (#422). The compaction body build removes the tool surface and must
|
|
2275
|
-
// therefore be the last routed transform
|
|
2276
|
-
//
|
|
2449
|
+
// therefore be the last routed transform that may depend on those declarations. Structural
|
|
2450
|
+
// sanitizers below can still run after it.
|
|
2451
|
+
outBody = normalizeResponsesCodeMode(outBody, parsed, provider);
|
|
2277
2452
|
if (parsed._compactionRequest === true && !isCanonicalOpenAiForwardProvider(provider)) {
|
|
2278
2453
|
outBody = buildRoutedCompactionBody(outBody);
|
|
2279
2454
|
}
|
|
2455
|
+
// Run after routed compaction so nested input_image parts are replaced before a malformed
|
|
2456
|
+
// tool output is flattened to text and can no longer be inspected structurally.
|
|
2457
|
+
outBody = repairUnidentifiedToolOutputItems(outBody);
|
|
2280
2458
|
const threadServingIdentityChanged = parsed._stripReasoningEncryptedContent === true;
|
|
2281
2459
|
const sanitizedBody = normalizeToolSchemas(
|
|
2282
2460
|
stripSparkCompatibility(
|
|
@@ -2296,6 +2474,7 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
|
|
|
2296
2474
|
stripEncryptedContent: threadServingIdentityChanged,
|
|
2297
2475
|
},
|
|
2298
2476
|
),
|
|
2477
|
+
provider,
|
|
2299
2478
|
),
|
|
2300
2479
|
),
|
|
2301
2480
|
),
|
|
@@ -2312,6 +2491,17 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
|
|
|
2312
2491
|
provider,
|
|
2313
2492
|
parsed.modelId,
|
|
2314
2493
|
);
|
|
2494
|
+
if (isCanonicalOpenAiForwardProvider(provider)) {
|
|
2495
|
+
const routingHeaders = new Headers(headers);
|
|
2496
|
+
applyCodexRoutingHint(routingHeaders, finalBody);
|
|
2497
|
+
// Static headers may use mixed casing. Remove every stale spelling
|
|
2498
|
+
// without normalizing unrelated headers returned by this adapter.
|
|
2499
|
+
for (const name of Object.keys(headers)) {
|
|
2500
|
+
if (name.toLowerCase() === CODEX_ROUTING_HINT_HEADER) delete headers[name];
|
|
2501
|
+
}
|
|
2502
|
+
const hint = routingHeaders.get(CODEX_ROUTING_HINT_HEADER);
|
|
2503
|
+
if (hint !== null) headers[CODEX_ROUTING_HINT_HEADER] = hint;
|
|
2504
|
+
}
|
|
2315
2505
|
const actualServiceTier = isPlainObject(finalBody) && typeof finalBody.service_tier === "string"
|
|
2316
2506
|
? finalBody.service_tier
|
|
2317
2507
|
: null;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { toolChoiceToolPredicate, type OcxParsedRequest, type OcxProviderConfig } from "../types";
|
|
2
|
+
import { isOpenAiOperatedResponsesDestination } from "../providers/openai-tiers";
|
|
3
|
+
import { CODE_MODE_RESULT_ECHO_SENTENCE, normalizeEmptyExecToolResultText } from "./exec-tool-result-normalize";
|
|
4
|
+
import { isBareShellBridgeTool, isCodexCodeModeExecTool } from "./tool-catalog-nudge";
|
|
5
|
+
|
|
6
|
+
function record(value: unknown): value is Record<string, unknown> {
|
|
7
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Inspect the whole result, not just its empty header: later text or media is real output. */
|
|
11
|
+
function textOnlyOutput(output: unknown): string | undefined {
|
|
12
|
+
if (typeof output === "string") return output;
|
|
13
|
+
if (!Array.isArray(output)) return undefined;
|
|
14
|
+
if (!output.every(part => record(part)
|
|
15
|
+
&& ["text", "input_text", "output_text"].includes(String(part.type))
|
|
16
|
+
&& typeof part.text === "string")) return undefined;
|
|
17
|
+
return output.map(part => part.text).join("\n");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function withExecInputGuidance(tool: unknown): unknown {
|
|
21
|
+
if (!record(tool) || tool.type !== "function" || tool.name !== "exec" || tool.namespace !== undefined) return tool;
|
|
22
|
+
if (!record(tool.parameters) || !record(tool.parameters.properties) || !record(tool.parameters.properties.input)) return tool;
|
|
23
|
+
return { ...tool, parameters: { ...tool.parameters, properties: {
|
|
24
|
+
...tool.parameters.properties,
|
|
25
|
+
input: {
|
|
26
|
+
...tool.parameters.properties.input,
|
|
27
|
+
description: `JavaScript source for unified exec; do not provide a bare shell command. ${CODE_MODE_RESULT_ECHO_SENTENCE}`,
|
|
28
|
+
},
|
|
29
|
+
} } };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Native routed Responses needs the same first-call/output contract as translated adapters. */
|
|
33
|
+
export function normalizeResponsesCodeMode(body: unknown, parsed: OcxParsedRequest, provider: OcxProviderConfig): unknown {
|
|
34
|
+
if (!record(body) || parsed._compactionRequest || isOpenAiOperatedResponsesDestination(provider)) return body;
|
|
35
|
+
const visible = parsed.context.tools?.filter(toolChoiceToolPredicate(parsed.options.toolChoice, parsed.context.tools));
|
|
36
|
+
if (!visible?.some(isCodexCodeModeExecTool) || visible.some(isBareShellBridgeTool)) return body;
|
|
37
|
+
const instructions = typeof body.instructions === "string" ? body.instructions : "";
|
|
38
|
+
const input = Array.isArray(body.input) ? body.input : undefined;
|
|
39
|
+
const execCalls = new Set(input?.filter(item => record(item)
|
|
40
|
+
&& (item.type === "function_call" || item.type === "custom_tool_call")
|
|
41
|
+
&& item.name === "exec" && item.namespace === undefined && typeof item.call_id === "string")
|
|
42
|
+
.map(item => item.call_id));
|
|
43
|
+
return {
|
|
44
|
+
...body,
|
|
45
|
+
instructions: instructions.includes(CODE_MODE_RESULT_ECHO_SENTENCE)
|
|
46
|
+
? instructions : [instructions, CODE_MODE_RESULT_ECHO_SENTENCE].filter(Boolean).join("\n\n"),
|
|
47
|
+
...(Array.isArray(body.tools) ? { tools: body.tools.map(withExecInputGuidance) } : {}),
|
|
48
|
+
...(input ? { input: input.map(item => {
|
|
49
|
+
if (!record(item)) return item;
|
|
50
|
+
if (item.type === "additional_tools" && Array.isArray(item.tools)) {
|
|
51
|
+
return { ...item, tools: item.tools.map(withExecInputGuidance) };
|
|
52
|
+
}
|
|
53
|
+
if ((item.type !== "function_call_output" && item.type !== "custom_tool_call_output") || !execCalls.has(item.call_id)) return item;
|
|
54
|
+
const text = textOnlyOutput(item.output);
|
|
55
|
+
const normalized = text === undefined ? undefined : normalizeEmptyExecToolResultText(text, { toolName: "exec" });
|
|
56
|
+
return normalized === undefined ? item : { ...item, output: normalized };
|
|
57
|
+
}) } : {}),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -121,7 +121,7 @@ export function buildNonOpenAIToolCatalogNudgeFromNames(
|
|
|
121
121
|
"Call only listed names with their listed argument keys; do not invent, translate, or rename tools.",
|
|
122
122
|
"Names mentioned only in instructions, tool descriptions, argument descriptions, or nested helper APIs are not additional top-level tools.",
|
|
123
123
|
verifiedCodeModeExecName
|
|
124
|
-
? "`" + verifiedCodeModeExecName + "` is Codex code mode: its body is JavaScript evaluated in a V8 isolate. Nested helpers are called INSIDE that body as `await tools.<name>(...)`, for example `await tools.exec_command({cmd: \"ls\"})` or `await tools.codex_app__list_threads({})`. Absence from the top-level catalog or from `" + verifiedCodeModeExecName + "`'s description is not absence: deferred helpers stay callable on `tools.<name>`. Discover them from the isolate global `ALL_TOOLS`, not `tools.ALL_TOOLS`. Do not skip an available nested helper because it is omitted from the listed top-level names. " + CODE_MODE_RESULT_ECHO_SENTENCE + " Nested `tools.apply_patch(input)` is host-executed: the string must begin exactly with `*** Begin Patch` and end with `*** End Patch
|
|
124
|
+
? "`" + verifiedCodeModeExecName + "` is Codex code mode: its body is JavaScript evaluated in a V8 isolate. Nested helpers are called INSIDE that body as `await tools.<name>(...)`, for example `await tools.exec_command({cmd: \"ls\"})` or `await tools.codex_app__list_threads({})`. Absence from the top-level catalog or from `" + verifiedCodeModeExecName + "`'s description is not absence: deferred helpers stay callable on `tools.<name>`. Discover them from the isolate global `ALL_TOOLS`, not `tools.ALL_TOOLS`. Do not skip an available nested helper because it is omitted from the listed top-level names. " + CODE_MODE_RESULT_ECHO_SENTENCE + " Nested `tools.apply_patch(input)` is host-executed: the string must begin exactly with `*** Begin Patch` and end with `*** End Patch`, each marker line being three asterisks, one space, the two words, then end of line with no further asterisks. OpenCodex does not rewrite JavaScript inside exec, so extra asterisks on a marker line are rejected by Codex before the file is touched."
|
|
125
125
|
: "If a listed tool exposes nested helpers such as a tools.* API, call the listed parent tool and use those helpers only inside that tool's input.",
|
|
126
126
|
unavailableNeighborNames.length > 0
|
|
127
127
|
? "Do not use neighboring-agent tool names " + quoteNames(unavailableNeighborNames) + " unless this turn's catalog lists those exact names."
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
export function isSchemaObject(value: unknown): value is Record<string, unknown> {
|
|
2
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function decodeJsonPointerToken(token: string): string {
|
|
6
|
+
return token.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Resolve a local `#/`-rooted JSON Pointer against `root`; undefined when it does not resolve. */
|
|
10
|
+
export function lookupLocalJsonPointer(root: unknown, ref: string): unknown {
|
|
11
|
+
if (ref === "#" || ref === "#/") return root;
|
|
12
|
+
if (!ref.startsWith("#/")) return undefined;
|
|
13
|
+
let current: unknown = root;
|
|
14
|
+
for (const token of ref.slice(2).split("/").map(decodeJsonPointerToken)) {
|
|
15
|
+
if (!isSchemaObject(current) || !Object.hasOwn(current, token)) return undefined;
|
|
16
|
+
current = current[token];
|
|
17
|
+
}
|
|
18
|
+
return current;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Values a schema pins through `const`/`enum`, or undefined when it pins none. */
|
|
22
|
+
function xaiLiteralValues(schema: unknown): unknown[] | undefined {
|
|
23
|
+
if (!isSchemaObject(schema)) return undefined;
|
|
24
|
+
if (Object.hasOwn(schema, "const")) return [schema.const];
|
|
25
|
+
if (Array.isArray(schema.enum)) return schema.enum;
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** JSON type name for a literal, so it can be compared against a `type` keyword. */
|
|
30
|
+
function xaiJsonTypeOf(value: unknown): string {
|
|
31
|
+
if (value === null) return "null";
|
|
32
|
+
if (Array.isArray(value)) return "array";
|
|
33
|
+
if (typeof value === "string") return "string";
|
|
34
|
+
if (typeof value === "boolean") return "boolean";
|
|
35
|
+
if (typeof value === "number") return Number.isInteger(value) ? "integer" : "number";
|
|
36
|
+
return "object";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Types a schema declares, or undefined when it constrains none. */
|
|
40
|
+
function xaiDeclaredTypes(schema: unknown): Set<string> | undefined {
|
|
41
|
+
if (!isSchemaObject(schema)) return undefined;
|
|
42
|
+
const type = schema.type;
|
|
43
|
+
if (typeof type === "string") return new Set([type]);
|
|
44
|
+
if (Array.isArray(type) && type.every(item => typeof item === "string")) return new Set(type as string[]);
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** `integer` is a subset of `number`, so those two names overlap rather than exclude. */
|
|
49
|
+
function xaiTypesOverlap(left: string, right: string): boolean {
|
|
50
|
+
if (left === right) return true;
|
|
51
|
+
return (left === "integer" && right === "number") || (left === "number" && right === "integer");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Conservative mutual-exclusion test: true only when no instance can satisfy both schemas.
|
|
56
|
+
* Proof comes from disjoint literal sets or disjoint declared types; anything it cannot prove
|
|
57
|
+
* is reported as overlapping so the caller refuses the merge instead of widening the schema.
|
|
58
|
+
*/
|
|
59
|
+
function xaiSchemasAreProvablyDisjoint(left: unknown, right: unknown): boolean {
|
|
60
|
+
const leftValues = xaiLiteralValues(left);
|
|
61
|
+
const rightValues = xaiLiteralValues(right);
|
|
62
|
+
if (leftValues && rightValues) {
|
|
63
|
+
const seen = new Set(rightValues.map(value => JSON.stringify(value)));
|
|
64
|
+
return leftValues.every(value => !seen.has(JSON.stringify(value)));
|
|
65
|
+
}
|
|
66
|
+
const leftTypes = xaiDeclaredTypes(left);
|
|
67
|
+
const rightTypes = xaiDeclaredTypes(right);
|
|
68
|
+
const literalsExcludedByTypes = (values: unknown[], types: Set<string>): boolean =>
|
|
69
|
+
values.every(value => ![...types].some(type => xaiTypesOverlap(xaiJsonTypeOf(value), type)));
|
|
70
|
+
if (leftValues && rightTypes) return literalsExcludedByTypes(leftValues, rightTypes);
|
|
71
|
+
if (rightValues && leftTypes) return literalsExcludedByTypes(rightValues, leftTypes);
|
|
72
|
+
if (leftTypes && rightTypes) {
|
|
73
|
+
return ![...leftTypes].some(leftType => [...rightTypes].some(rightType => xaiTypesOverlap(leftType, rightType)));
|
|
74
|
+
}
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Every pair provably disjoint, so a union over them accepts each instance exactly once. */
|
|
79
|
+
export function xaiSchemasArePairwiseDisjoint(schemas: unknown[]): boolean {
|
|
80
|
+
for (let i = 0; i < schemas.length; i += 1) {
|
|
81
|
+
for (let j = i + 1; j < schemas.length; j += 1) {
|
|
82
|
+
if (!xaiSchemasAreProvablyDisjoint(schemas[i], schemas[j])) return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import type { OcxProviderConfig } from "../types";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
5
|
-
}
|
|
2
|
+
export { lookupLocalJsonPointer } from "./xai-schema-analysis";
|
|
3
|
+
import { isSchemaObject, lookupLocalJsonPointer, xaiSchemasArePairwiseDisjoint } from "./xai-schema-analysis";
|
|
6
4
|
|
|
7
5
|
export function isXaiSchemaTarget(provider: Pick<OcxProviderConfig, "baseUrl">): boolean {
|
|
8
6
|
try {
|
|
@@ -58,22 +56,6 @@ function createXaiSchemaBudget(): XaiSchemaBudget {
|
|
|
58
56
|
return { remainingNodes: XAI_MAX_SCHEMA_NODES, remainingVariants: XAI_MAX_ROOT_VARIANTS };
|
|
59
57
|
}
|
|
60
58
|
|
|
61
|
-
function decodeJsonPointerToken(token: string): string {
|
|
62
|
-
return token.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/** Resolve a local `#/`-rooted JSON Pointer against `root`; undefined when it does not resolve. */
|
|
66
|
-
export function lookupLocalJsonPointer(root: unknown, ref: string): unknown {
|
|
67
|
-
if (ref === "#" || ref === "#/") return root;
|
|
68
|
-
if (!ref.startsWith("#/")) return undefined;
|
|
69
|
-
let current: unknown = root;
|
|
70
|
-
for (const token of ref.slice(2).split("/").map(decodeJsonPointerToken)) {
|
|
71
|
-
if (!isSchemaObject(current) || !Object.hasOwn(current, token)) return undefined;
|
|
72
|
-
current = current[token];
|
|
73
|
-
}
|
|
74
|
-
return current;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
59
|
/** Resolve local `#/` `$ref`s. Unresolvable, cyclic, or over-budget refs return undefined. */
|
|
78
60
|
function resolveXaiSchemaRefs(
|
|
79
61
|
schema: unknown,
|
|
@@ -168,73 +150,6 @@ function xaiRequiredSetsMatch(variants: Record<string, unknown>[]): boolean {
|
|
|
168
150
|
return serialized.every(value => value === serialized[0]);
|
|
169
151
|
}
|
|
170
152
|
|
|
171
|
-
/** Values a schema pins through `const`/`enum`, or undefined when it pins none. */
|
|
172
|
-
function xaiLiteralValues(schema: unknown): unknown[] | undefined {
|
|
173
|
-
if (!isSchemaObject(schema)) return undefined;
|
|
174
|
-
if (Object.hasOwn(schema, "const")) return [schema.const];
|
|
175
|
-
if (Array.isArray(schema.enum)) return schema.enum;
|
|
176
|
-
return undefined;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/** JSON type name for a literal, so it can be compared against a `type` keyword. */
|
|
180
|
-
function xaiJsonTypeOf(value: unknown): string {
|
|
181
|
-
if (value === null) return "null";
|
|
182
|
-
if (Array.isArray(value)) return "array";
|
|
183
|
-
if (typeof value === "string") return "string";
|
|
184
|
-
if (typeof value === "boolean") return "boolean";
|
|
185
|
-
if (typeof value === "number") return Number.isInteger(value) ? "integer" : "number";
|
|
186
|
-
return "object";
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/** Types a schema declares, or undefined when it constrains none. */
|
|
190
|
-
function xaiDeclaredTypes(schema: unknown): Set<string> | undefined {
|
|
191
|
-
if (!isSchemaObject(schema)) return undefined;
|
|
192
|
-
const type = schema.type;
|
|
193
|
-
if (typeof type === "string") return new Set([type]);
|
|
194
|
-
if (Array.isArray(type) && type.every(item => typeof item === "string")) return new Set(type as string[]);
|
|
195
|
-
return undefined;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/** `integer` is a subset of `number`, so those two names overlap rather than exclude. */
|
|
199
|
-
function xaiTypesOverlap(left: string, right: string): boolean {
|
|
200
|
-
if (left === right) return true;
|
|
201
|
-
return (left === "integer" && right === "number") || (left === "number" && right === "integer");
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* Conservative mutual-exclusion test: true only when no instance can satisfy both schemas.
|
|
206
|
-
* Proof comes from disjoint literal sets or disjoint declared types; anything it cannot prove
|
|
207
|
-
* is reported as overlapping so the caller refuses the merge instead of widening the schema.
|
|
208
|
-
*/
|
|
209
|
-
function xaiSchemasAreProvablyDisjoint(left: unknown, right: unknown): boolean {
|
|
210
|
-
const leftValues = xaiLiteralValues(left);
|
|
211
|
-
const rightValues = xaiLiteralValues(right);
|
|
212
|
-
if (leftValues && rightValues) {
|
|
213
|
-
const seen = new Set(rightValues.map(value => JSON.stringify(value)));
|
|
214
|
-
return leftValues.every(value => !seen.has(JSON.stringify(value)));
|
|
215
|
-
}
|
|
216
|
-
const leftTypes = xaiDeclaredTypes(left);
|
|
217
|
-
const rightTypes = xaiDeclaredTypes(right);
|
|
218
|
-
const literalsExcludedByTypes = (values: unknown[], types: Set<string>): boolean =>
|
|
219
|
-
values.every(value => ![...types].some(type => xaiTypesOverlap(xaiJsonTypeOf(value), type)));
|
|
220
|
-
if (leftValues && rightTypes) return literalsExcludedByTypes(leftValues, rightTypes);
|
|
221
|
-
if (rightValues && leftTypes) return literalsExcludedByTypes(rightValues, leftTypes);
|
|
222
|
-
if (leftTypes && rightTypes) {
|
|
223
|
-
return ![...leftTypes].some(leftType => [...rightTypes].some(rightType => xaiTypesOverlap(leftType, rightType)));
|
|
224
|
-
}
|
|
225
|
-
return false;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
/** Every pair provably disjoint, so a union over them accepts each instance exactly once. */
|
|
229
|
-
function xaiSchemasArePairwiseDisjoint(schemas: unknown[]): boolean {
|
|
230
|
-
for (let i = 0; i < schemas.length; i += 1) {
|
|
231
|
-
for (let j = i + 1; j < schemas.length; j += 1) {
|
|
232
|
-
if (!xaiSchemasAreProvablyDisjoint(schemas[i], schemas[j])) return false;
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
return true;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
153
|
/** Deduplicate schemas by serialized shape, preserving first-seen order. */
|
|
239
154
|
function uniqueXaiSchemas(values: unknown[]): unknown[] {
|
|
240
155
|
const unique: unknown[] = [];
|
|
@@ -55,7 +55,7 @@ function normalizeToolGroup(tools: unknown[]): ToolGroupRewrite {
|
|
|
55
55
|
// `search_context_size` 400s, while `user_location`, `search_content_types`, `filters` and
|
|
56
56
|
// `enable_image_search` are all accepted. Deleting the accepted ones was a silent capability
|
|
57
57
|
// loss, and it contradicted the sibling layer, whose own probe note already records
|
|
58
|
-
// user_location/filters as accepted (tests/responses-routed-web-search-fields.test.ts).
|
|
58
|
+
// user_location/filters as accepted (tests/responses/responses-routed-web-search-fields.test.ts).
|
|
59
59
|
delete next.external_web_access;
|
|
60
60
|
delete next.search_context_size;
|
|
61
61
|
if (enableImageSearch && !Object.hasOwn(next, "enable_image_search")) {
|