@bitkyc08/opencodex 2.55.0 → 2.57.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/bin/ocx.mjs +10 -0
- package/gui/dist/assets/{index-BBOZWGB6.css → index-C5-RdDmD.css} +1 -1
- package/gui/dist/assets/{index-VuoiWj9J.js → index-Cz7CLdif.js} +21 -21
- package/gui/dist/index.html +2 -2
- package/package.json +4 -3
- package/src/adapters/base.ts +21 -0
- package/src/adapters/codebuddy/adapter.ts +2 -1
- package/src/adapters/codebuddy/scaffold-guard.ts +248 -0
- package/src/adapters/command-code.ts +1 -1
- package/src/adapters/cursor/envelope-echo.ts +8 -2
- package/src/adapters/cursor/transport-retry.ts +46 -1
- package/src/adapters/cursor.ts +4 -0
- package/src/adapters/google.ts +7 -7
- package/src/adapters/kiro/adapter.ts +42 -1
- package/src/adapters/kiro/payload.ts +17 -3
- package/src/adapters/kiro/reasoning.ts +70 -7
- package/src/adapters/kiro/stream.ts +8 -2
- package/src/adapters/kiro/wire.ts +2 -1
- package/src/adapters/kiro-events.ts +21 -13
- package/src/adapters/kiro-retry.ts +23 -4
- package/src/adapters/openai-chat/errors.ts +116 -0
- package/src/adapters/openai-chat/messages.ts +346 -0
- package/src/adapters/openai-chat/passthrough.ts +146 -0
- package/src/adapters/openai-chat/response-events.ts +117 -0
- package/src/adapters/openai-chat/tool-call-validation.ts +200 -0
- package/src/adapters/openai-chat/tool-name-registry.ts +166 -0
- package/src/adapters/openai-chat/tool-schema.ts +495 -0
- package/src/adapters/openai-chat/wire.ts +50 -0
- package/src/adapters/openai-chat.ts +40 -1452
- package/src/adapters/openai-responses/canonical-forward.ts +202 -0
- package/src/adapters/openai-responses/image-gen.ts +406 -0
- package/src/adapters/openai-responses/internal.ts +3 -0
- package/src/adapters/openai-responses/passthrough.ts +642 -0
- package/src/adapters/openai-responses/prompt-cache.ts +83 -0
- package/src/adapters/openai-responses/reasoning.ts +220 -0
- package/src/adapters/openai-responses/request-strips.ts +185 -0
- package/src/adapters/openai-responses/tool-output-recovery.ts +509 -0
- package/src/adapters/openai-responses/tool-schema.ts +293 -0
- package/src/adapters/openai-responses/web-search.ts +156 -0
- package/src/adapters/openai-responses.ts +4 -2625
- package/src/bridge/errors.ts +58 -0
- package/src/bridge/internal.ts +174 -0
- package/src/bridge/response-json.ts +630 -0
- package/src/bridge/sse.ts +1462 -0
- package/src/bridge.ts +5 -2204
- package/src/chat/inbound.ts +12 -1
- package/src/claude/desktop-profile.ts +66 -9
- package/src/claude/outbound.ts +18 -0
- package/src/cli/account-main.ts +1 -1
- package/src/cli/capabilities.ts +2 -2
- package/src/cli/combo.ts +10 -1
- package/src/cli/index.ts +48 -5
- package/src/cli/registry.ts +2 -1
- package/src/cli/system-command.ts +4 -4
- package/src/clients/config-export.ts +7 -3
- package/src/codex/account-label.ts +14 -3
- package/src/codex/account-lifecycle.ts +3 -0
- package/src/codex/account-store.ts +184 -35
- package/src/codex/account-usability.ts +21 -0
- package/src/codex/auth-api/account-list.ts +507 -0
- package/src/codex/auth-api/http.ts +32 -0
- package/src/codex/auth-api/login-flow.ts +566 -0
- package/src/codex/auth-api/login-state.ts +64 -0
- package/src/codex/auth-api/main-account-probe.ts +331 -0
- package/src/codex/auth-api/pool-mode-gate.ts +274 -0
- package/src/codex/auth-api/pool-quota-probe.ts +512 -0
- package/src/codex/auth-api/reset-credit-service.ts +431 -0
- package/src/codex/auth-api/routes.ts +425 -0
- package/src/codex/auth-api/runtime-config.ts +48 -0
- package/src/codex/auth-api.ts +27 -3118
- package/src/codex/auth-context.ts +252 -35
- package/src/codex/catalog/aggregation.ts +80 -1
- package/src/codex/catalog/auto-review.ts +507 -0
- package/src/codex/catalog/build-entries.ts +981 -0
- package/src/codex/catalog/combo-member.ts +375 -0
- package/src/codex/catalog/derive-entry.ts +229 -0
- package/src/codex/catalog/effort.ts +0 -1
- package/src/codex/catalog/gated-native-warn.ts +63 -0
- package/src/codex/catalog/gather-capture.ts +533 -0
- package/src/codex/catalog/model-hints.ts +691 -0
- package/src/codex/catalog/model-visibility.ts +305 -0
- package/src/codex/catalog/provider-fetch.ts +52 -2942
- package/src/codex/catalog/provider-models.ts +685 -0
- package/src/codex/catalog/remote.ts +30 -0
- package/src/codex/catalog/restore.ts +132 -0
- package/src/codex/catalog/retained-sync.ts +714 -0
- package/src/codex/catalog/routed-gather.ts +895 -0
- package/src/codex/catalog/subagent-roster.ts +176 -0
- package/src/codex/catalog/sync.ts +52 -2698
- package/src/codex/cli-install-provenance.ts +7 -1
- package/src/codex/convergence.ts +7 -2
- package/src/codex/desktop-app/types.ts +11 -2
- package/src/codex/desktop-app/windows.ts +5 -5
- package/src/codex/inject/config-toml.ts +563 -0
- package/src/codex/inject/remove.ts +192 -0
- package/src/codex/inject/restore.ts +567 -0
- package/src/codex/inject/routing-classify.ts +109 -0
- package/src/codex/inject/routing-target.ts +125 -0
- package/src/codex/inject.ts +89 -1444
- package/src/codex/lineage.ts +458 -0
- package/src/codex/model-entitlements.ts +152 -15
- package/src/codex/pool-refresh-backoff.ts +161 -0
- package/src/codex/quota-rejection.ts +104 -15
- package/src/codex/routing/active-account.ts +194 -0
- package/src/codex/routing/cache-affinity.ts +70 -0
- package/src/codex/routing/cooldown-math.ts +285 -0
- package/src/codex/routing/health-store.ts +402 -0
- package/src/codex/routing/probe-lease.ts +358 -0
- package/src/codex/routing/selection.ts +780 -0
- package/src/codex/routing/thread-affinity.ts +586 -0
- package/src/codex/routing/transient-hold-dispatch.ts +141 -0
- package/src/codex/routing.ts +370 -2271
- package/src/codex/shim-fingerprint.ts +223 -0
- package/src/codex/shim-inspect.ts +175 -0
- package/src/codex/shim-probe.ts +367 -0
- package/src/codex/shim-restore-lock.ts +169 -0
- package/src/codex/shim-state-file.ts +151 -0
- package/src/codex/shim-templates.ts +265 -0
- package/src/codex/shim.ts +48 -1268
- package/src/codex/warmup.ts +1 -1
- package/src/combos/failover.ts +85 -0
- package/src/combos/request.ts +17 -10
- package/src/combos/types.ts +23 -2
- package/src/config/diagnostics.ts +705 -0
- package/src/config/feature-flags.ts +55 -0
- package/src/config/live-reconcile.ts +403 -0
- package/src/config/load-degrade.ts +880 -0
- package/src/config/mutation-lock.ts +244 -0
- package/src/config/openai-tier-backup.ts +268 -0
- package/src/config/pending-teardown.ts +31 -0
- package/src/config/persist-unlocked.ts +92 -0
- package/src/config/proxy-env.ts +188 -0
- package/src/config/salvage.ts +244 -0
- package/src/config/schema/config-schema.ts +640 -0
- package/src/config/schema/leaf-validators.ts +855 -0
- package/src/config/warn-memo.ts +28 -0
- package/src/config.ts +234 -4481
- package/src/generated/compatibility-version.json +649 -121
- package/src/images/loop.ts +1 -1
- package/src/lib/errors.ts +17 -0
- package/src/lib/request-execution-budget.ts +198 -23
- package/src/lib/spend-reservation-ledger.ts +958 -0
- package/src/lib/state-store-registrations.ts +6 -2
- package/src/lib/test-home-guard.ts +85 -1
- package/src/lib/upstream-retry.ts +132 -21
- package/src/lib/windows-elevation.ts +76 -14
- package/src/lib/workflow-budget.ts +553 -30
- package/src/oauth/index.ts +2 -2
- package/src/oauth/key-providers.ts +2 -2
- package/src/providers/kiro-models.ts +4 -3
- package/src/providers/label.ts +19 -1
- package/src/providers/model-discovery.ts +16 -0
- package/src/providers/quota/account-cache.ts +441 -0
- package/src/providers/quota/antigravity.ts +295 -0
- package/src/providers/quota/report-cache.ts +320 -0
- package/src/providers/quota/vendor-probes-key.ts +1243 -0
- package/src/providers/quota/vendor-probes-oauth.ts +590 -0
- package/src/providers/quota.ts +324 -3079
- package/src/providers/registry/entries-core.ts +1228 -0
- package/src/providers/registry/entries-extended.ts +1213 -0
- package/src/providers/registry/model-seeds.ts +912 -0
- package/src/providers/registry/types.ts +352 -0
- package/src/providers/registry.ts +24 -3536
- package/src/responses/continuation-ownership.ts +29 -0
- package/src/responses/reasoning-envelope.ts +6 -3
- package/src/responses/state/replay-fingerprint.ts +80 -0
- package/src/responses/state/snapshot-codec.ts +104 -0
- package/src/responses/state/spill-failure.ts +118 -0
- package/src/responses/state/spill-queue.ts +665 -0
- package/src/responses/state/temp-recovery.ts +257 -0
- package/src/responses/state.ts +82 -1143
- package/src/routing/identity-domains.ts +456 -0
- package/src/routing/probe-lease.ts +613 -0
- package/src/server/chat-completions.ts +3 -1
- package/src/server/chat-native.ts +37 -9
- package/src/server/index/bounded-request.ts +88 -0
- package/src/server/index/live-sideband.ts +601 -0
- package/src/server/index/serve-options.ts +1766 -0
- package/src/server/index/startup-warnings.ts +213 -0
- package/src/server/index/websocket-handler.ts +339 -0
- package/src/server/index.ts +45 -2552
- package/src/server/inspection-tee.ts +107 -0
- package/src/server/live.ts +46 -1
- package/src/server/management/combo-routes.ts +10 -1
- package/src/server/management/route-registry.ts +26 -23
- package/src/server/management/shared.ts +8 -5
- package/src/server/management/workflow-budget-routes.ts +133 -0
- package/src/server/management-api.ts +12 -0
- package/src/server/relay-eager.ts +2 -0
- package/src/server/relay.ts +14 -19
- package/src/server/request-log-conversation.ts +9 -7
- package/src/server/request-log.ts +372 -4
- package/src/server/response-log-body.ts +153 -0
- package/src/server/responses/account-change-state.ts +307 -0
- package/src/server/responses/adapter-continuation.ts +540 -0
- package/src/server/responses/adapter-delivery.ts +208 -0
- package/src/server/responses/adapter-dispatch.ts +1042 -0
- package/src/server/responses/codex-ws-wire.ts +5 -0
- package/src/server/responses/collaboration.ts +74 -4
- package/src/server/responses/combo-session-recall.ts +68 -8
- package/src/server/responses/compact.ts +113 -17
- package/src/server/responses/completion-policy.ts +33 -0
- package/src/server/responses/core-auth.ts +529 -0
- package/src/server/responses/core-codex-account.ts +907 -0
- package/src/server/responses/core-combo-failure.ts +210 -0
- package/src/server/responses/core-combo.ts +787 -0
- package/src/server/responses/core-errors.ts +170 -0
- package/src/server/responses/core-lifetime.ts +95 -0
- package/src/server/responses/core-normalize.ts +350 -0
- package/src/server/responses/core-opaque-recovery.ts +380 -0
- package/src/server/responses/core-options.ts +159 -0
- package/src/server/responses/core-replay.ts +298 -0
- package/src/server/responses/core.ts +192 -8893
- package/src/server/responses/encrypted-payload.ts +0 -1
- package/src/server/responses/input-admission.ts +126 -6
- package/src/server/responses/passthrough-delivery.ts +869 -0
- package/src/server/responses/passthrough-dispatch.ts +1494 -0
- package/src/server/responses/passthrough-error.ts +38 -2
- package/src/server/responses/passthrough-execution.ts +54 -0
- package/src/server/responses/request-prepare.ts +1080 -0
- package/src/server/responses/request-send-budget.ts +259 -0
- package/src/server/responses/request-sidecar-auth.ts +149 -0
- package/src/server/responses/request-spend.ts +147 -0
- package/src/server/responses/request-transport.ts +803 -0
- package/src/server/responses/response-effects.ts +157 -0
- package/src/server/responses/run-turn-execution.ts +476 -0
- package/src/server/responses/sidecar-execution.ts +463 -0
- package/src/server/responses/terminal-guard.ts +65 -4
- package/src/server/responses-image-gen-repair.ts +1 -1
- package/src/server/responses-undeclared-tool-guard.ts +9 -5
- package/src/server/workflow-refusal.ts +84 -0
- package/src/service/windows-ops.ts +210 -16
- package/src/service/windows-scheduler.ts +28 -21
- package/src/service.ts +1 -1
- package/src/types/config.ts +34 -1
- package/src/types/request.ts +8 -5
- package/src/types/tools.ts +24 -0
- package/src/types.ts +2 -0
- package/src/update/index.ts +10 -0
- package/src/update/stop-contract.d.mts +1 -0
- package/src/update/stop-contract.mjs +19 -0
- package/src/update/stop-decision.d.mts +1 -1
- package/src/update/stop-decision.mjs +12 -3
- package/src/usage/log.ts +147 -1
- package/src/usage/summary.ts +171 -21
- package/src/vision/anthropic-describe.ts +1 -1
- package/src/vision/describe.ts +5 -5
- package/src/web-search/anthropic-executor.ts +1 -1
- package/src/web-search/exa-executor.ts +1 -1
- package/src/web-search/executor.ts +1 -1
- package/src/web-search/gemini-executor.ts +1 -1
- package/src/web-search/loop.ts +1 -1
- package/src/web-search/ollama-executor.ts +1 -1
- package/src/web-search/parse.ts +67 -14
- package/src/web-search/passthrough-bridge.ts +64 -31
- package/src/web-search/xai-executor.ts +1 -1
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
import { isNativeOpenAIChatTarget } from "./wire";
|
|
2
|
+
import { createOpenAIChatToolNameRegistry, type OpenAIChatToolNameRegistry } from "./tool-name-registry";
|
|
3
|
+
import { isXaiSchemaTarget, lookupLocalJsonPointer, normalizeXaiToolParameters } from "../xai-tool-schema";
|
|
4
|
+
import { stripResponsesOnlyEncryptedMarker, stripUnicodePropertyPatterns } from "../responses-tool-schema";
|
|
5
|
+
import { isAllowedToolChoice, resolveToolChoiceWireName, toolChoiceToolPredicate } from "../../types";
|
|
6
|
+
import type { OcxParsedRequest, OcxProviderConfig } from "../../types";
|
|
7
|
+
|
|
8
|
+
const ZEN_SCHEMA_MAP_KEYS = new Set(["properties", "$defs", "definitions"]);
|
|
9
|
+
const ZEN_DROPPED_SCHEMA_KEYS = new Set(["encrypted"]);
|
|
10
|
+
|
|
11
|
+
function sanitizeZenSchemaMap(value: unknown): unknown {
|
|
12
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return sanitizeZenToolParameters(value);
|
|
13
|
+
const out: Record<string, unknown> = {};
|
|
14
|
+
for (const [name, child] of Object.entries(value as Record<string, unknown>)) {
|
|
15
|
+
out[name] = sanitizeZenToolParameters(child);
|
|
16
|
+
}
|
|
17
|
+
return out;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function sanitizeZenToolParameters(value: unknown): unknown {
|
|
21
|
+
if (Array.isArray(value)) return value.map(sanitizeZenToolParameters);
|
|
22
|
+
if (!value || typeof value !== "object") return value;
|
|
23
|
+
const input = value as Record<string, unknown>;
|
|
24
|
+
const out: Record<string, unknown> = {};
|
|
25
|
+
for (const [key, child] of Object.entries(input)) {
|
|
26
|
+
if (ZEN_DROPPED_SCHEMA_KEYS.has(key)) continue;
|
|
27
|
+
if (key === "required" && Array.isArray(child) && child.length === 0) continue;
|
|
28
|
+
if (key === "type" && Array.isArray(child)) {
|
|
29
|
+
const nonNull = child.filter(entry => entry !== "null");
|
|
30
|
+
if (child.includes("null")) out.nullable = true;
|
|
31
|
+
if (nonNull.length > 0) out.type = nonNull[0];
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
out[key] = ZEN_SCHEMA_MAP_KEYS.has(key) ? sanitizeZenSchemaMap(child) : sanitizeZenToolParameters(child);
|
|
35
|
+
}
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function ensureZenRootObjectSchema(schema: unknown): Record<string, unknown> {
|
|
40
|
+
const obj = schema && typeof schema === "object" && !Array.isArray(schema)
|
|
41
|
+
? schema as Record<string, unknown>
|
|
42
|
+
: {};
|
|
43
|
+
const compositionKeys = ["oneOf", "anyOf", "allOf"] as const;
|
|
44
|
+
const hasComposition = compositionKeys.some(key => Array.isArray(obj[key]));
|
|
45
|
+
const rootType = obj.type;
|
|
46
|
+
const rootObjectType = rootType === "object" || (Array.isArray(rootType) && rootType.includes("object"));
|
|
47
|
+
if (!hasComposition) {
|
|
48
|
+
const base = sanitizeZenToolParameters(obj) as Record<string, unknown>;
|
|
49
|
+
return rootObjectType && base.type === "object" ? base : { ...base, type: "object" };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const props: Record<string, unknown> = {};
|
|
53
|
+
const required = new Set<string>();
|
|
54
|
+
if (obj.properties && typeof obj.properties === "object") {
|
|
55
|
+
Object.assign(props, sanitizeZenSchemaMap(obj.properties) as Record<string, unknown>);
|
|
56
|
+
}
|
|
57
|
+
if (Array.isArray(obj.required)) {
|
|
58
|
+
for (const entry of obj.required) if (typeof entry === "string") required.add(entry);
|
|
59
|
+
}
|
|
60
|
+
for (const key of compositionKeys) {
|
|
61
|
+
const variants = obj[key];
|
|
62
|
+
if (!Array.isArray(variants)) continue;
|
|
63
|
+
const mergeRequired = key === "allOf";
|
|
64
|
+
for (const variant of variants) {
|
|
65
|
+
if (!variant || typeof variant !== "object" || Array.isArray(variant)) continue;
|
|
66
|
+
const rec = variant as Record<string, unknown>;
|
|
67
|
+
if (rec.properties && typeof rec.properties === "object") {
|
|
68
|
+
Object.assign(props, sanitizeZenSchemaMap(rec.properties) as Record<string, unknown>);
|
|
69
|
+
}
|
|
70
|
+
if (mergeRequired && Array.isArray(rec.required)) {
|
|
71
|
+
for (const entry of rec.required) if (typeof entry === "string") required.add(entry);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const merged = sanitizeZenToolParameters(obj) as Record<string, unknown>;
|
|
77
|
+
delete merged.oneOf;
|
|
78
|
+
delete merged.anyOf;
|
|
79
|
+
delete merged.allOf;
|
|
80
|
+
merged.type = "object";
|
|
81
|
+
if (Object.keys(props).length > 0) merged.properties = props;
|
|
82
|
+
if (required.size > 0) merged.required = [...required];
|
|
83
|
+
return merged;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function shouldSanitizeZenToolParameters(provider: OcxProviderConfig): boolean {
|
|
87
|
+
const baseUrl = provider.baseUrl.replace(/\/+$/, "");
|
|
88
|
+
return baseUrl === "https://opencode.ai/zen/v1"
|
|
89
|
+
|| baseUrl === "https://opencode.ai/zen/go/v1";
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Azure Model Router (and Gemini-in-the-pool) 400s Codex MCP schemas whose root is a union. */
|
|
93
|
+
const AZURE_CHAT_FORBIDDEN_ROOT_KEYS = ["oneOf", "anyOf", "allOf", "enum", "const", "not"] as const;
|
|
94
|
+
|
|
95
|
+
function isAzureOpenAiChatTarget(provider: OcxProviderConfig): boolean {
|
|
96
|
+
try {
|
|
97
|
+
const host = new URL(provider.baseUrl).hostname.toLowerCase();
|
|
98
|
+
return host.endsWith(".openai.azure.com")
|
|
99
|
+
|| host.endsWith(".cognitiveservices.azure.com")
|
|
100
|
+
|| host.endsWith(".services.ai.azure.com")
|
|
101
|
+
|| host.endsWith(".ai.azure.com");
|
|
102
|
+
} catch {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Azure Foundry Model Router validates every function schema against the strictest model in
|
|
109
|
+
* the pool (Gemini-shaped): root must be {type:"object"} with no oneOf/anyOf/allOf/enum/
|
|
110
|
+
* const/not. Codex App MCP tools such as mcp__codex_app__automation_update ship a root
|
|
111
|
+
* union, which 400s the whole turn. Flatten like Zen, then strip leftover forbidden keys.
|
|
112
|
+
*/
|
|
113
|
+
function sanitizeAzureChatToolParameters(parameters: unknown): Record<string, unknown> {
|
|
114
|
+
const root = ensureZenRootObjectSchema(parameters);
|
|
115
|
+
for (const key of AZURE_CHAT_FORBIDDEN_ROOT_KEYS) delete root[key];
|
|
116
|
+
root.type = "object";
|
|
117
|
+
if (!root.properties || typeof root.properties !== "object" || Array.isArray(root.properties)) {
|
|
118
|
+
root.properties = {};
|
|
119
|
+
}
|
|
120
|
+
return root;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Moonshot validates function schemas against a draft-07 reading of `$ref`, where the
|
|
124
|
+
// keyword stands alone and siblings are ignored. It rejects the whole request rather
|
|
125
|
+
// than ignoring them: "not a valid moonshot flavored json schema ... when using $ref,
|
|
126
|
+
// type should be defined in the referenced schema instead of the parent schema".
|
|
127
|
+
const MOONSHOT_SCHEMA_HOSTNAMES = new Set([
|
|
128
|
+
"api.kimi.com",
|
|
129
|
+
"api.moonshot.ai",
|
|
130
|
+
"api.moonshot.cn",
|
|
131
|
+
]);
|
|
132
|
+
|
|
133
|
+
function isMoonshotSchemaTarget(provider: OcxProviderConfig): boolean {
|
|
134
|
+
try {
|
|
135
|
+
return MOONSHOT_SCHEMA_HOSTNAMES.has(new URL(provider.baseUrl).hostname);
|
|
136
|
+
} catch {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const VOLCENGINE_ARK_HOSTNAMES = new Set([
|
|
142
|
+
"ark.cn-beijing.volces.com",
|
|
143
|
+
"ark.ap-southeast.volces.com",
|
|
144
|
+
]);
|
|
145
|
+
|
|
146
|
+
export function isVolcengineArkPaygChatTarget(provider: OcxProviderConfig): boolean {
|
|
147
|
+
try {
|
|
148
|
+
const url = new URL(provider.baseUrl);
|
|
149
|
+
const pathname = url.pathname.replace(/\/+$/, "") || "/";
|
|
150
|
+
return VOLCENGINE_ARK_HOSTNAMES.has(url.hostname) && pathname === "/api/v3";
|
|
151
|
+
} catch {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function ensureRootObjectType(parameters: unknown): Record<string, unknown> {
|
|
157
|
+
if (!parameters || typeof parameters !== "object" || Array.isArray(parameters)) {
|
|
158
|
+
return { type: "object", properties: {} };
|
|
159
|
+
}
|
|
160
|
+
const obj = parameters as Record<string, unknown>;
|
|
161
|
+
if (obj.type === "object") return obj;
|
|
162
|
+
return { ...obj, type: "object" };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function isXaiObjectSchema(value: unknown): value is Record<string, unknown> {
|
|
166
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* JSON Schema 2020-12 makes `$ref` an in-place applicator: siblings stay in force and are
|
|
171
|
+
* combined with the referenced schema. Moonshot enforces the older draft-07 reading where
|
|
172
|
+
* `$ref` must stand alone, and 400s the entire request when a node carries both. Codex's own
|
|
173
|
+
* deferred tool catalog emits exactly that shape (zod-to-json-schema deduplicates into
|
|
174
|
+
* `$defs.__schema*` nodes that keep `type`/`minLength`/`format` beside the `$ref`), so the
|
|
175
|
+
* schema is not something a user can fix from configuration — see issue #2673.
|
|
176
|
+
*
|
|
177
|
+
* Inline the referenced schema underneath the node's own keywords, which is what 2020-12 says
|
|
178
|
+
* the node means, then drop `$ref`. Constraints reach the model instead of being stripped.
|
|
179
|
+
* The `$defs` bag is preserved: a bare `$ref` (no siblings) is already legal for Moonshot and
|
|
180
|
+
* is left pointing at its definition rather than expanded, which keeps recursive schemas finite.
|
|
181
|
+
*/
|
|
182
|
+
function moonshotRefTargetKeys(node: Record<string, unknown>): string[] {
|
|
183
|
+
return Object.keys(node).filter(key => key !== "$ref");
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Inlining duplicates the target, so a schema referencing one large definition from many
|
|
188
|
+
* sibling-carrying nodes can multiply. Bound the total expansions and fall back to a bare
|
|
189
|
+
* `$ref` once the budget is spent: still valid for Moonshot, just without the node's own
|
|
190
|
+
* narrowing keywords. Mirrors the node budget in google-tool-schema.ts.
|
|
191
|
+
*/
|
|
192
|
+
const MOONSHOT_MAX_REF_EXPANSIONS = 512;
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Expansion count alone does not bound the walk: a deeply nested ref-free schema, or one
|
|
196
|
+
* large definition repeated across many nodes, still recurses to exhaustion or amplifies the
|
|
197
|
+
* emitted output. Depth and node budgets close both, and mirror google-tool-schema.ts.
|
|
198
|
+
*/
|
|
199
|
+
const MOONSHOT_MAX_SCHEMA_DEPTH = 64;
|
|
200
|
+
const MOONSHOT_MAX_SCHEMA_NODES = 4_096;
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Assertion keywords whose meaning under a `$ref` is CONJUNCTION, not replacement. A node
|
|
204
|
+
* carrying `required: ["b"]` beside a target requiring `["a"]` means both are required;
|
|
205
|
+
* letting the sibling win emitted a schema that no longer described the tool.
|
|
206
|
+
*/
|
|
207
|
+
function unionRequired(target: unknown, sibling: unknown): unknown {
|
|
208
|
+
if (!Array.isArray(target) || !Array.isArray(sibling)) return sibling;
|
|
209
|
+
const seen = new Set<unknown>();
|
|
210
|
+
const out: unknown[] = [];
|
|
211
|
+
for (const name of [...target, ...sibling]) {
|
|
212
|
+
if (seen.has(name)) continue;
|
|
213
|
+
seen.add(name);
|
|
214
|
+
out.push(name);
|
|
215
|
+
}
|
|
216
|
+
return out;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Keywords whose values are DATA, not schemas.
|
|
221
|
+
*
|
|
222
|
+
* Recursing into them rewrote user data: an `enum` listing a literal object that happens
|
|
223
|
+
* to carry a `"$ref"` string had that key stripped as if it were a schema reference, so a
|
|
224
|
+
* value the tool declared as legal silently changed shape. These are copied through.
|
|
225
|
+
*/
|
|
226
|
+
const MOONSHOT_DATA_VALUED_KEYWORDS = new Set(["enum", "const", "default", "examples"]);
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Numeric assertions whose intersection is a bound, and which direction tightens.
|
|
230
|
+
*
|
|
231
|
+
* `$ref` under 2020-12 is an in-place applicator: the node and its target BOTH apply, so
|
|
232
|
+
* the emitted schema must be their INTERSECTION. The previous code overwrote the target
|
|
233
|
+
* with the node and called that "the narrower reading", which holds only when the node
|
|
234
|
+
* happens to be narrower. A node declaring `minLength: 1` beside a target declaring
|
|
235
|
+
* `minLength: 5` shipped `minLength: 1` - a contract weaker than either side asked for,
|
|
236
|
+
* emitted silently, which is the same failure mode the `required` composition fixed for
|
|
237
|
+
* set-valued keywords.
|
|
238
|
+
*
|
|
239
|
+
* "max" means the surviving value is the larger of the two (lower bounds), "min" the
|
|
240
|
+
* smaller (upper bounds). A keyword absent from this table keeps the overwrite: for
|
|
241
|
+
* `type`, `format`, `description` and friends there is no ordering to intersect along,
|
|
242
|
+
* and the node is the more specific statement.
|
|
243
|
+
*/
|
|
244
|
+
const MOONSHOT_BOUND_KEYWORDS: Record<string, "max" | "min"> = {
|
|
245
|
+
minLength: "max",
|
|
246
|
+
minItems: "max",
|
|
247
|
+
minProperties: "max",
|
|
248
|
+
minimum: "max",
|
|
249
|
+
exclusiveMinimum: "max",
|
|
250
|
+
maxLength: "min",
|
|
251
|
+
maxItems: "min",
|
|
252
|
+
maxProperties: "min",
|
|
253
|
+
maximum: "min",
|
|
254
|
+
exclusiveMaximum: "min",
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Intersect one numeric bound. Either side being absent or non-finite yields the other,
|
|
259
|
+
* because an unstated bound constrains nothing - returning `undefined` there would drop
|
|
260
|
+
* a constraint the remaining side genuinely made.
|
|
261
|
+
*/
|
|
262
|
+
function intersectBound(target: unknown, sibling: unknown, direction: "max" | "min"): unknown {
|
|
263
|
+
const a = typeof target === "number" && Number.isFinite(target) ? target : null;
|
|
264
|
+
const b = typeof sibling === "number" && Number.isFinite(sibling) ? sibling : null;
|
|
265
|
+
if (a === null) return b === null ? sibling : sibling;
|
|
266
|
+
if (b === null) return target;
|
|
267
|
+
return direction === "max" ? Math.max(a, b) : Math.min(a, b);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Compose two `properties` maps. A property named in BOTH the referenced target and the
|
|
272
|
+
* node is the same conjunction problem `required` had: letting the sibling win discards
|
|
273
|
+
* the target's constraints for that member. Merge the two member schemas so neither side
|
|
274
|
+
* loses its keywords. Shared member bounds are the same conjunction one level down,
|
|
275
|
+
* and nested object members recurse through this helper instead of replacing the target.
|
|
276
|
+
*/
|
|
277
|
+
function composeProperties(
|
|
278
|
+
target: Record<string, unknown>,
|
|
279
|
+
sibling: Record<string, unknown>,
|
|
280
|
+
): Record<string, unknown> {
|
|
281
|
+
const combined: Record<string, unknown> = Object.create(null) as Record<string, unknown>;
|
|
282
|
+
for (const [name, sub] of Object.entries(target)) combined[name] = sub;
|
|
283
|
+
for (const [name, sub] of Object.entries(sibling)) {
|
|
284
|
+
const existing = combined[name];
|
|
285
|
+
if (isXaiObjectSchema(existing) && isXaiObjectSchema(sub)) {
|
|
286
|
+
const member: Record<string, unknown> = Object.create(null) as Record<string, unknown>;
|
|
287
|
+
for (const [k, v] of Object.entries(existing)) member[k] = v;
|
|
288
|
+
for (const [k, v] of Object.entries(sub)) {
|
|
289
|
+
if (k === "required") {
|
|
290
|
+
member[k] = unionRequired(member[k], v);
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
if (k === "properties" && isXaiObjectSchema(member[k]) && isXaiObjectSchema(v)) {
|
|
294
|
+
member[k] = composeProperties(member[k] as Record<string, unknown>, v);
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
const boundDirection = MOONSHOT_BOUND_KEYWORDS[k];
|
|
298
|
+
if (boundDirection && k in member) {
|
|
299
|
+
member[k] = intersectBound(member[k], v, boundDirection);
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
member[k] = v;
|
|
303
|
+
}
|
|
304
|
+
combined[name] = member;
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
combined[name] = sub;
|
|
308
|
+
}
|
|
309
|
+
return combined;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
interface MoonshotNormalizeState {
|
|
313
|
+
activeRefs: Set<string>;
|
|
314
|
+
remainingExpansions: number;
|
|
315
|
+
remainingNodes: number;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function normalizeMoonshotSchemaNode(
|
|
319
|
+
node: unknown,
|
|
320
|
+
root: Record<string, unknown>,
|
|
321
|
+
state: MoonshotNormalizeState,
|
|
322
|
+
depth = 0,
|
|
323
|
+
): unknown {
|
|
324
|
+
if (Array.isArray(node)) {
|
|
325
|
+
if (depth >= MOONSHOT_MAX_SCHEMA_DEPTH) return [];
|
|
326
|
+
return node.map(item => normalizeMoonshotSchemaNode(item, root, state, depth + 1));
|
|
327
|
+
}
|
|
328
|
+
if (!isXaiObjectSchema(node)) return node;
|
|
329
|
+
|
|
330
|
+
// Fail closed for this node rather than emitting a partially weakened schema: an empty
|
|
331
|
+
// object is the one shape that asserts nothing it cannot back up.
|
|
332
|
+
if (depth >= MOONSHOT_MAX_SCHEMA_DEPTH || state.remainingNodes <= 0) return {};
|
|
333
|
+
state.remainingNodes -= 1;
|
|
334
|
+
|
|
335
|
+
const ref = node.$ref;
|
|
336
|
+
const hasSiblings = moonshotRefTargetKeys(node).length > 0;
|
|
337
|
+
|
|
338
|
+
if (typeof ref === "string" && hasSiblings) {
|
|
339
|
+
// A cycle cannot be inlined. Keeping the bare `$ref` is the lossy-but-valid fallback:
|
|
340
|
+
// Moonshot accepts it, and the alternative (dropping the ref) would erase the recursion.
|
|
341
|
+
if (state.activeRefs.has(ref) || state.remainingExpansions <= 0) return { $ref: ref };
|
|
342
|
+
|
|
343
|
+
const target = lookupLocalJsonPointer(root, ref);
|
|
344
|
+
if (isXaiObjectSchema(target)) {
|
|
345
|
+
state.remainingExpansions -= 1;
|
|
346
|
+
state.activeRefs.add(ref);
|
|
347
|
+
const resolvedTarget = normalizeMoonshotSchemaNode(target, root, state, depth + 1);
|
|
348
|
+
state.activeRefs.delete(ref);
|
|
349
|
+
const merged: Record<string, unknown> = Object.create(null) as Record<string, unknown>;
|
|
350
|
+
if (isXaiObjectSchema(resolvedTarget)) {
|
|
351
|
+
for (const [key, value] of Object.entries(resolvedTarget)) merged[key] = value;
|
|
352
|
+
}
|
|
353
|
+
// "Alongside the target" is conjunction, not replacement. For most keywords the node
|
|
354
|
+
// narrows the target and overwriting is the narrower reading, but `required` and
|
|
355
|
+
// `properties` are set-valued: letting the sibling win DROPPED the target's own
|
|
356
|
+
// members, so a tool requiring `a` beside a node requiring `b` shipped requiring only
|
|
357
|
+
// `b`. Those two compose; everything else keeps the narrowing overwrite.
|
|
358
|
+
for (const [key, value] of Object.entries(node)) {
|
|
359
|
+
if (key === "$ref") continue;
|
|
360
|
+
if (MOONSHOT_DATA_VALUED_KEYWORDS.has(key)) {
|
|
361
|
+
merged[key] = value;
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
const normalized = normalizeMoonshotSchemaNode(value, root, state, depth + 1);
|
|
365
|
+
if (key === "required") {
|
|
366
|
+
merged[key] = unionRequired(merged[key], normalized);
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
if (key === "properties" && isXaiObjectSchema(merged[key]) && isXaiObjectSchema(normalized)) {
|
|
370
|
+
merged[key] = composeProperties(merged[key] as Record<string, unknown>, normalized);
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
// Numeric bounds intersect rather than overwrite: both the node and its target
|
|
374
|
+
// apply, so the surviving bound is the stricter of the two in whichever direction
|
|
375
|
+
// that keyword tightens.
|
|
376
|
+
const boundDirection = MOONSHOT_BOUND_KEYWORDS[key];
|
|
377
|
+
if (boundDirection && key in merged) {
|
|
378
|
+
merged[key] = intersectBound(merged[key], normalized, boundDirection);
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
merged[key] = normalized;
|
|
382
|
+
}
|
|
383
|
+
return merged;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Unresolvable pointer: a remote ref, a malformed path, or a non-object target. Dropping
|
|
387
|
+
// the ref and keeping the siblings silently discards whatever the reference constrained,
|
|
388
|
+
// which is the one outcome we cannot detect downstream. A bare `$ref` is lossy in the
|
|
389
|
+
// other direction - it loses the node's own keywords - but it preserves the identity of
|
|
390
|
+
// what was asked for, and Moonshot accepts it.
|
|
391
|
+
return { $ref: ref };
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const out: Record<string, unknown> = Object.create(null) as Record<string, unknown>;
|
|
395
|
+
for (const [key, value] of Object.entries(node)) {
|
|
396
|
+
out[key] = key === "$ref" || MOONSHOT_DATA_VALUED_KEYWORDS.has(key)
|
|
397
|
+
? value
|
|
398
|
+
: normalizeMoonshotSchemaNode(value, root, state, depth + 1);
|
|
399
|
+
}
|
|
400
|
+
return out;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function normalizeMoonshotToolParameters(parameters: unknown): Record<string, unknown> {
|
|
404
|
+
const rooted = ensureRootObjectType(parameters);
|
|
405
|
+
const normalized = normalizeMoonshotSchemaNode(rooted, rooted, {
|
|
406
|
+
activeRefs: new Set<string>(),
|
|
407
|
+
remainingExpansions: MOONSHOT_MAX_REF_EXPANSIONS,
|
|
408
|
+
remainingNodes: MOONSHOT_MAX_SCHEMA_NODES,
|
|
409
|
+
});
|
|
410
|
+
return isXaiObjectSchema(normalized) ? normalized : rooted;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
export function toolsToChatFormat(
|
|
414
|
+
parsed: OcxParsedRequest,
|
|
415
|
+
provider: OcxProviderConfig,
|
|
416
|
+
registry: OpenAIChatToolNameRegistry = createOpenAIChatToolNameRegistry(parsed.context.tools),
|
|
417
|
+
): unknown[] | undefined {
|
|
418
|
+
if (!parsed.context.tools || parsed.context.tools.length === 0) return undefined;
|
|
419
|
+
const tools = parsed.context.tools.filter(toolChoiceToolPredicate(parsed.options.toolChoice, parsed.context.tools));
|
|
420
|
+
if (tools.length === 0) return undefined;
|
|
421
|
+
const xaiTarget = isXaiSchemaTarget(provider);
|
|
422
|
+
const moonshotTarget = !xaiTarget && isMoonshotSchemaTarget(provider);
|
|
423
|
+
const formatted = tools.flatMap(t => {
|
|
424
|
+
const normalized = xaiTarget
|
|
425
|
+
? normalizeXaiToolParameters(t.parameters)
|
|
426
|
+
: moonshotTarget
|
|
427
|
+
? normalizeMoonshotToolParameters(t.parameters)
|
|
428
|
+
: ensureRootObjectType(t.parameters);
|
|
429
|
+
const parameters = stripUnicodePropertyPatterns(stripResponsesOnlyEncryptedMarker(normalized));
|
|
430
|
+
|
|
431
|
+
if (parameters === undefined) return [];
|
|
432
|
+
return [{
|
|
433
|
+
type: "function",
|
|
434
|
+
function: {
|
|
435
|
+
name: registry.alias(t),
|
|
436
|
+
...(t.description ? { description: t.description } : {}),
|
|
437
|
+
parameters,
|
|
438
|
+
...(t.strict !== undefined ? { strict: t.strict } : {}),
|
|
439
|
+
},
|
|
440
|
+
}];
|
|
441
|
+
});
|
|
442
|
+
return formatted.length > 0 ? formatted : undefined;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
export function toolsToChatFormatForProvider(
|
|
446
|
+
parsed: OcxParsedRequest,
|
|
447
|
+
provider: OcxProviderConfig,
|
|
448
|
+
registry: OpenAIChatToolNameRegistry = createOpenAIChatToolNameRegistry(parsed.context.tools),
|
|
449
|
+
): unknown[] | undefined {
|
|
450
|
+
const base = toolsToChatFormat(parsed, provider, registry);
|
|
451
|
+
const azureChat = isAzureOpenAiChatTarget(provider);
|
|
452
|
+
const zenChat = shouldSanitizeZenToolParameters(provider);
|
|
453
|
+
if (!base || (!zenChat && !azureChat)) return base;
|
|
454
|
+
return base.map(tool => {
|
|
455
|
+
if (!tool || typeof tool !== "object") return tool;
|
|
456
|
+
const functionDef = (tool as { function?: Record<string, unknown> }).function;
|
|
457
|
+
if (!functionDef || typeof functionDef !== "object") return tool;
|
|
458
|
+
const parameters = azureChat
|
|
459
|
+
? sanitizeAzureChatToolParameters(functionDef.parameters ?? {})
|
|
460
|
+
: ensureZenRootObjectSchema(functionDef.parameters ?? {});
|
|
461
|
+
const nextFunction: Record<string, unknown> = { ...functionDef, parameters };
|
|
462
|
+
// strict: true plus a flattened schema is rejected by Gemini-in-the-pool routers.
|
|
463
|
+
if (azureChat) delete nextFunction.strict;
|
|
464
|
+
return {
|
|
465
|
+
...tool,
|
|
466
|
+
function: nextFunction,
|
|
467
|
+
};
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
export function toolChoiceToChatFormat(
|
|
472
|
+
tc: OcxParsedRequest["options"]["toolChoice"],
|
|
473
|
+
tools: OcxParsedRequest["context"]["tools"],
|
|
474
|
+
provider: OcxProviderConfig,
|
|
475
|
+
registry: OpenAIChatToolNameRegistry = createOpenAIChatToolNameRegistry(tools),
|
|
476
|
+
): unknown {
|
|
477
|
+
if (!tc) return undefined;
|
|
478
|
+
if (isAllowedToolChoice(tc)) {
|
|
479
|
+
if (tc.mode === "required" && tc.allowedTools.length === 1 && isNativeOpenAIChatTarget(provider)) {
|
|
480
|
+
return {
|
|
481
|
+
type: "function",
|
|
482
|
+
function: { name: registry.aliasWireName(resolveToolChoiceWireName(tools, tc.allowedTools[0])) },
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
return tc.mode === "required" ? "required" : "auto";
|
|
486
|
+
}
|
|
487
|
+
if (tc === "auto" || tc === "none" || tc === "required") return tc;
|
|
488
|
+
if ("name" in tc) {
|
|
489
|
+
return {
|
|
490
|
+
type: "function",
|
|
491
|
+
function: { name: registry.aliasWireName(resolveToolChoiceWireName(tools, tc.name)) },
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
return undefined;
|
|
495
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { agentRouterDefaultHeaders } from "../agentrouter";
|
|
2
|
+
import { openaiChatCompletionsUrl } from "../openai-chat-url";
|
|
3
|
+
import type { OcxProviderConfig } from "../../types";
|
|
4
|
+
|
|
5
|
+
// Providers may opt into stripping one trailing "[...]" group from the wire model id.
|
|
6
|
+
// Z.AI needs this because its OpenAI path rejects glm-5.2[1m] with 400 code 1211;
|
|
7
|
+
// unflagged OpenAI-compatible providers and the Anthropic adapter keep ids verbatim.
|
|
8
|
+
export function stripBracketedModelSuffix(modelId: string): string {
|
|
9
|
+
const suffixEnd = modelId.trimEnd().length;
|
|
10
|
+
if (suffixEnd === 0 || modelId[suffixEnd - 1] !== "]") return modelId;
|
|
11
|
+
|
|
12
|
+
let suffixStart = -1;
|
|
13
|
+
for (let i = suffixEnd - 2; i >= 0 && modelId[i] !== "]"; i--) {
|
|
14
|
+
if (modelId[i] === "[") suffixStart = i;
|
|
15
|
+
}
|
|
16
|
+
return suffixStart === -1 ? modelId : modelId.slice(0, suffixStart);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function openAIChatTransport(provider: OcxProviderConfig): {
|
|
20
|
+
url: string;
|
|
21
|
+
headers: Record<string, string>;
|
|
22
|
+
hasCredential: boolean;
|
|
23
|
+
} {
|
|
24
|
+
const hasCredential = typeof provider.apiKey === "string" && provider.apiKey.trim().length > 0;
|
|
25
|
+
if ((provider.authMode === "key" || provider.authMode === "oauth") && !provider.keyOptional && !hasCredential) {
|
|
26
|
+
throw new Error(`${provider.adapter} requires a non-empty credential (authMode: ${provider.authMode})`);
|
|
27
|
+
}
|
|
28
|
+
const headers: Record<string, string> = {
|
|
29
|
+
"Content-Type": "application/json",
|
|
30
|
+
...agentRouterDefaultHeaders(provider.baseUrl, provider.headers),
|
|
31
|
+
};
|
|
32
|
+
if (hasCredential) headers.Authorization = `Bearer ${provider.apiKey}`;
|
|
33
|
+
if (provider.headers) Object.assign(headers, provider.headers);
|
|
34
|
+
// A configured relative path wins, mirroring how the Responses adapter honours
|
|
35
|
+
// `responsesPath`. An upstream can serve both wires under different prefixes, and a
|
|
36
|
+
// per-model wire override only swaps the adapter, so without this the opted-in Chat
|
|
37
|
+
// request would be sent to the Responses base with `/chat/completions` appended.
|
|
38
|
+
const url = provider.chatCompletionsPath === undefined
|
|
39
|
+
? openaiChatCompletionsUrl(provider.baseUrl)
|
|
40
|
+
: `${provider.baseUrl.replace(/\/$/, "")}${provider.chatCompletionsPath}`;
|
|
41
|
+
return { url, headers, hasCredential };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function isNativeOpenAIChatTarget(provider: OcxProviderConfig): boolean {
|
|
45
|
+
try {
|
|
46
|
+
return new URL(provider.baseUrl).hostname === "api.openai.com";
|
|
47
|
+
} catch {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
}
|