@bitkyc08/opencodex 2.41.0 → 2.43.0-preview.20260906
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-VGeQEZ_v.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 +54 -24
- 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/identity.ts +8 -2
- package/src/adapters/kiro-calibration.ts +181 -0
- package/src/adapters/kiro.ts +135 -3
- package/src/adapters/openai-responses.ts +259 -29
- 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 +47 -13
- 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 +49 -9
- package/src/cli/account-extended.ts +7 -1
- package/src/cli/capabilities.ts +15 -4
- 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/observe.ts +3 -1
- 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 +272 -32
- package/src/codex/auth-context.ts +328 -24
- package/src/codex/catalog/effort.ts +44 -5
- package/src/codex/catalog/metadata.ts +149 -14
- package/src/codex/catalog/native-models.ts +116 -4
- package/src/codex/catalog/parsing.ts +122 -8
- package/src/codex/catalog/provider-fetch.ts +154 -23
- package/src/codex/catalog/reserve.ts +52 -0
- package/src/codex/catalog/sync.ts +89 -16
- package/src/codex/catalog.ts +1 -1
- package/src/codex/convergence-types.ts +1 -0
- package/src/codex/convergence.ts +2 -0
- package/src/codex/data/upstream-models.json +169 -0
- package/src/codex/forward-transport-headers.ts +25 -0
- package/src/codex/inject.ts +99 -34
- package/src/codex/injected-marker.ts +30 -4
- package/src/codex/journal.ts +14 -0
- 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 +474 -178
- 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/account-quota-rank.ts +40 -1
- package/src/oauth/anthropic-routing.ts +99 -3
- package/src/oauth/chatgpt-device.ts +187 -0
- package/src/oauth/chatgpt.ts +31 -4
- package/src/oauth/generic-account-failover.ts +36 -13
- package/src/oauth/index.ts +140 -29
- package/src/oauth/log.ts +3 -0
- 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/muse-subscription-usage.ts +95 -0
- 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 +625 -98
- 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 +122 -28
- package/src/server/live.ts +18 -4
- 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 +45 -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 +510 -57
- package/src/server/responses/empty-completion-guard.ts +4 -0
- 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/request.ts +8 -0
- 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-B2YjLA-i.css +0 -1
- package/gui/dist/assets/index-aPup8CKb.js +0 -112
|
@@ -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")) {
|
package/src/bridge.ts
CHANGED
|
@@ -16,9 +16,9 @@ import {
|
|
|
16
16
|
type OcxErrorPayload,
|
|
17
17
|
} from "./lib/errors";
|
|
18
18
|
import { redactSecretString } from "./lib/redact";
|
|
19
|
-
import { repairFreeformToolInput } from "./responses/apply-patch-envelope";
|
|
19
|
+
import { mayBecomePatchEnvelope, repairFreeformToolInput } from "./responses/apply-patch-envelope";
|
|
20
20
|
import { encodeCompactionSummary } from "./responses/compaction";
|
|
21
|
-
import { compileCodeModeHelperInput } from "./responses/code-mode-helper-compat";
|
|
21
|
+
import { compileCodeModeHelperInput, resolveCodeModeHelperName } from "./responses/code-mode-helper-compat";
|
|
22
22
|
import { isTruncatedStopReason, truncationReasonFor } from "./responses/truncated-stop-reason";
|
|
23
23
|
import { encodeReasoningEnvelope, type ReasoningEnvelope } from "./responses/reasoning-envelope";
|
|
24
24
|
import { rememberReasoningForCall } from "./responses/reasoning-replay-cache";
|
|
@@ -33,7 +33,7 @@ import {
|
|
|
33
33
|
stripCitationMarkers,
|
|
34
34
|
type CitationMarkerFilter,
|
|
35
35
|
} from "./responses/citation-markers";
|
|
36
|
-
import { normalizeDeclaredToolName } from "./types";
|
|
36
|
+
import { declaresCodeModeExec, normalizeDeclaredToolName } from "./types";
|
|
37
37
|
import { usageDisplayTotalTokens } from "./usage/totals";
|
|
38
38
|
import { appendSafeWebSearchSource, safeWebSearchSources } from "./web-search/sources";
|
|
39
39
|
import {
|
|
@@ -59,6 +59,10 @@ function sseEvent(name: string, data: Record<string, unknown>): string {
|
|
|
59
59
|
return `event: ${name}\ndata: ${JSON.stringify(data)}\n\n`;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
63
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
64
|
+
}
|
|
65
|
+
|
|
62
66
|
function responsesUsage(usage: OcxUsage | undefined): Record<string, unknown> {
|
|
63
67
|
// input_tokens_details / output_tokens_details are ALWAYS emitted (zero defaults):
|
|
64
68
|
// strict Responses clients deserialize them as required fields — grok-build's pinned
|
|
@@ -80,7 +84,24 @@ function responsesUsage(usage: OcxUsage | undefined): Record<string, unknown> {
|
|
|
80
84
|
const inputTokens = usage.contextTotalTokens !== undefined
|
|
81
85
|
? Math.max(0, usage.contextTotalTokens - usage.outputTokens)
|
|
82
86
|
: usage.inputTokens;
|
|
87
|
+
// openai/codex#41980 parity: unknown upstream usage fields (subscription metadata, future
|
|
88
|
+
// counters) pass through the rebuild. Normalized values stay authoritative for the known
|
|
89
|
+
// keys (they are derived from the same raw values, so this never disagrees with upstream).
|
|
90
|
+
const raw: Record<string, unknown> = usage.rawUsage ?? {};
|
|
91
|
+
// cache_write_tokens is a KNOWN key: it is emitted only from the validated normalized
|
|
92
|
+
// value below, never copied through raw (an unknown-shaped value must not leak into the
|
|
93
|
+
// normalized contract).
|
|
94
|
+
const rawInputDetails = isRecord(raw.input_tokens_details)
|
|
95
|
+
? Object.fromEntries(Object.entries(raw.input_tokens_details as Record<string, unknown>)
|
|
96
|
+
.filter(([key]) => key !== "cache_write_tokens"))
|
|
97
|
+
: {} as Record<string, unknown>;
|
|
98
|
+
const rawOutputDetails = isRecord(raw.output_tokens_details)
|
|
99
|
+
? raw.output_tokens_details as Record<string, unknown>
|
|
100
|
+
: {} as Record<string, unknown>;
|
|
83
101
|
const out: Record<string, unknown> = {
|
|
102
|
+
...Object.fromEntries(Object.entries(raw).filter(([key]) =>
|
|
103
|
+
key !== "input_tokens" && key !== "output_tokens" && key !== "total_tokens"
|
|
104
|
+
&& key !== "input_tokens_details" && key !== "output_tokens_details")),
|
|
84
105
|
input_tokens: inputTokens,
|
|
85
106
|
output_tokens: usage.outputTokens,
|
|
86
107
|
total_tokens: usage.contextTotalTokens !== undefined
|
|
@@ -90,18 +111,19 @@ function responsesUsage(usage: OcxUsage | undefined): Record<string, unknown> {
|
|
|
90
111
|
// cached_tokens carries cache READS only, matching OpenAI semantics, and is always present
|
|
91
112
|
// (zero default) for strict clients. Clamp to inputTokens so a provider's absolute
|
|
92
113
|
// checkpoint can never report more cache reads than input.
|
|
93
|
-
const inputDetails: Record<string,
|
|
114
|
+
const inputDetails: Record<string, unknown> = {
|
|
115
|
+
...rawInputDetails,
|
|
94
116
|
cached_tokens: Math.min(usage.cachedInputTokens ?? 0, inputTokens),
|
|
95
117
|
};
|
|
96
118
|
if (usage.cacheCreationInputTokens !== undefined) {
|
|
97
|
-
const cacheRead = inputDetails.cached_tokens
|
|
119
|
+
const cacheRead = typeof inputDetails.cached_tokens === "number" ? inputDetails.cached_tokens : 0;
|
|
98
120
|
inputDetails.cache_write_tokens = Math.min(
|
|
99
121
|
usage.cacheCreationInputTokens,
|
|
100
122
|
Math.max(0, inputTokens - cacheRead),
|
|
101
123
|
);
|
|
102
124
|
}
|
|
103
125
|
out.input_tokens_details = inputDetails;
|
|
104
|
-
out.output_tokens_details = { reasoning_tokens: usage.reasoningOutputTokens ?? 0 };
|
|
126
|
+
out.output_tokens_details = { ...rawOutputDetails, reasoning_tokens: usage.reasoningOutputTokens ?? 0 };
|
|
105
127
|
return out;
|
|
106
128
|
}
|
|
107
129
|
|
|
@@ -255,9 +277,12 @@ export function bridgeToResponsesSSE(
|
|
|
255
277
|
toolName: string,
|
|
256
278
|
namespace?: string,
|
|
257
279
|
codeModeHelperName?: string,
|
|
258
|
-
): string =>
|
|
259
|
-
|
|
260
|
-
|
|
280
|
+
): string => {
|
|
281
|
+
const helper = resolveCodeModeHelperName(codeModeHelperName, toolName, args, namespace, options?.declaredToolNames);
|
|
282
|
+
return helper
|
|
283
|
+
? compileCodeModeHelperInput(args, helper)
|
|
284
|
+
: repairFreeformToolInput(args, toolName, namespace);
|
|
285
|
+
};
|
|
261
286
|
// Best-effort unwrap of a PARTIAL freeform arg buffer for live input streaming
|
|
262
287
|
// (`response.custom_tool_call_input.delta` — codex-rs uses it for UI preview only;
|
|
263
288
|
// the completed custom_tool_call item stays authoritative). Compact `{"input":"...`
|
|
@@ -1150,7 +1175,13 @@ export function bridgeToResponsesSSE(
|
|
|
1150
1175
|
if (!FREEFORM_WRAP_PREFIX.startsWith(currentToolCall.args)) {
|
|
1151
1176
|
const full = freeformPartialInput(currentToolCall.args);
|
|
1152
1177
|
const emitted = currentToolCall.inputEmitted ?? "";
|
|
1153
|
-
|
|
1178
|
+
// Also hold a buffer that could still become a complete patch envelope:
|
|
1179
|
+
// at completion such a body is recompiled into an apply_patch helper call,
|
|
1180
|
+
// and streaming the envelope bytes first would be that same rewind.
|
|
1181
|
+
const mayCompile = declaresCodeModeExec(options?.declaredToolNames)
|
|
1182
|
+
&& !currentToolCall.namespace
|
|
1183
|
+
&& currentToolCall.name === "exec";
|
|
1184
|
+
if (!(mayCompile && mayBecomePatchEnvelope(full)) && full.startsWith(emitted) && full.length > emitted.length) {
|
|
1154
1185
|
emit("response.custom_tool_call_input.delta", {
|
|
1155
1186
|
item_id: currentToolCall.itemId, output_index: currentToolCall.outputIndex,
|
|
1156
1187
|
delta: full.slice(emitted.length),
|
|
@@ -1644,9 +1675,12 @@ function buildResponseJSONWithBudget(
|
|
|
1644
1675
|
toolName: string,
|
|
1645
1676
|
namespace?: string,
|
|
1646
1677
|
codeModeHelperName?: string,
|
|
1647
|
-
): string =>
|
|
1648
|
-
|
|
1649
|
-
|
|
1678
|
+
): string => {
|
|
1679
|
+
const helper = resolveCodeModeHelperName(codeModeHelperName, toolName, args, namespace, options?.declaredToolNames);
|
|
1680
|
+
return helper
|
|
1681
|
+
? compileCodeModeHelperInput(args, helper)
|
|
1682
|
+
: repairFreeformToolInput(args, toolName, namespace);
|
|
1683
|
+
};
|
|
1650
1684
|
const parseArgsObj = (args: string): Record<string, unknown> => {
|
|
1651
1685
|
try { const o = JSON.parse(args); return o && typeof o === "object" ? o : {}; } catch { return {}; }
|
|
1652
1686
|
};
|
package/src/chat/inbound.ts
CHANGED
|
@@ -73,13 +73,18 @@ function userContentToBlocks(content: unknown): Rec[] {
|
|
|
73
73
|
continue;
|
|
74
74
|
}
|
|
75
75
|
if (!isRec(raw)) continue;
|
|
76
|
-
if ((raw.type === "text" || raw.type === "input_text") && typeof raw.text === "string") {
|
|
76
|
+
if ((raw.type === "text" || raw.type === "input_text" || raw.type === "output_text") && typeof raw.text === "string") {
|
|
77
77
|
blocks.push({ type: "input_text", text: raw.text });
|
|
78
78
|
continue;
|
|
79
79
|
}
|
|
80
80
|
const imageUrl = imageUrlFromPart(raw);
|
|
81
81
|
if (imageUrl) {
|
|
82
|
-
|
|
82
|
+
const detail = isRec(raw.image_url) ? raw.image_url.detail : raw.detail;
|
|
83
|
+
blocks.push({
|
|
84
|
+
type: "input_image",
|
|
85
|
+
image_url: imageUrl,
|
|
86
|
+
...(detail === "auto" || detail === "low" || detail === "high" ? { detail } : {}),
|
|
87
|
+
});
|
|
83
88
|
continue;
|
|
84
89
|
}
|
|
85
90
|
const videoUrl = videoUrlFromPart(raw);
|
|
@@ -275,7 +280,10 @@ export function chatCompletionsToResponsesBody(raw: unknown): Rec {
|
|
|
275
280
|
: typeof msg.tool_use_id === "string" ? msg.tool_use_id
|
|
276
281
|
: "";
|
|
277
282
|
if (!callId) throw new ChatCompletionsRequestError("tool messages require tool_call_id");
|
|
278
|
-
const
|
|
283
|
+
const blocks = userContentToBlocks(msg.content);
|
|
284
|
+
const output = blocks.some(part => part.type === "input_image")
|
|
285
|
+
? blocks.filter(part => part.type === "input_text" || part.type === "input_image")
|
|
286
|
+
: contentToText(msg.content);
|
|
279
287
|
input.push({ type: "function_call_output", call_id: callId, output });
|
|
280
288
|
break;
|
|
281
289
|
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { isClaudeWebSearchToolName } from "./outbound";
|
|
2
|
+
import { AnthropicRequestError, isRec, type Rec } from "./inbound-records";
|
|
3
|
+
|
|
4
|
+
export function systemToInstructions(system: unknown): string | undefined {
|
|
5
|
+
if (typeof system === "string") return system.length > 0 ? system : undefined;
|
|
6
|
+
if (Array.isArray(system)) {
|
|
7
|
+
const parts: string[] = [];
|
|
8
|
+
for (const block of system) {
|
|
9
|
+
if (isRec(block) && block.type === "text" && typeof block.text === "string") parts.push(block.text);
|
|
10
|
+
}
|
|
11
|
+
return parts.length > 0 ? parts.join("\n\n") : undefined;
|
|
12
|
+
}
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function toolsToResponses(tools: unknown): Rec[] | undefined {
|
|
17
|
+
if (!Array.isArray(tools) || tools.length === 0) return undefined;
|
|
18
|
+
const out: Rec[] = [];
|
|
19
|
+
for (const raw of tools) {
|
|
20
|
+
if (!isRec(raw)) continue;
|
|
21
|
+
const type = typeof raw.type === "string" ? raw.type : "";
|
|
22
|
+
if (type.startsWith("web_search")) {
|
|
23
|
+
out.push({ type: "web_search" }); // hosted sidecar path
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (typeof raw.name === "string" && raw.name.length > 0 && isRec(raw.input_schema)) {
|
|
27
|
+
out.push({
|
|
28
|
+
type: "function",
|
|
29
|
+
name: raw.name,
|
|
30
|
+
...(typeof raw.description === "string" ? { description: raw.description } : {}),
|
|
31
|
+
parameters: raw.input_schema as Record<string, unknown>,
|
|
32
|
+
});
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
// Other server tools (bash_*, text_editor_*, ...) have no routed equivalent: drop.
|
|
36
|
+
}
|
|
37
|
+
return out.length > 0 ? out : undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function toolChoiceToResponses(choice: unknown, body: Rec): void {
|
|
41
|
+
if (!isRec(choice)) return;
|
|
42
|
+
if (choice.disable_parallel_tool_use === true) body.parallel_tool_calls = false;
|
|
43
|
+
switch (choice.type) {
|
|
44
|
+
case "auto": body.tool_choice = "auto"; break;
|
|
45
|
+
case "none": body.tool_choice = "none"; break;
|
|
46
|
+
case "any": body.tool_choice = "required"; break;
|
|
47
|
+
case "tool":
|
|
48
|
+
if (typeof choice.name !== "string" || choice.name.length === 0) {
|
|
49
|
+
throw new AnthropicRequestError("tool_choice.tool requires a name");
|
|
50
|
+
}
|
|
51
|
+
// Anthropic represents hosted WebSearch as a named tool choice, while
|
|
52
|
+
// Responses requires the choice type to match the hosted declaration.
|
|
53
|
+
// Preserve forced-tool intent rather than weakening it to `auto`.
|
|
54
|
+
body.tool_choice = isClaudeWebSearchToolName(choice.name)
|
|
55
|
+
? { type: "web_search" }
|
|
56
|
+
: { type: "function", name: choice.name };
|
|
57
|
+
break;
|
|
58
|
+
default: break;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import type { OcxClaudeCodeConfig } from "../types";
|
|
2
|
+
import { isAnthropicOutputSchema } from "../adapters/anthropic-output-schema";
|
|
3
|
+
import { resolveAlias } from "./alias";
|
|
4
|
+
import { stripOneMillionMarker } from "./context-windows";
|
|
5
|
+
import { resolveDesktop3pAlias } from "./desktop-3p";
|
|
6
|
+
import { isRec, type Rec } from "./inbound-records";
|
|
7
|
+
|
|
8
|
+
function isClaudeClassifierModel(model: string): boolean {
|
|
9
|
+
const stripped = model.replace(/-\d{8}$/, "");
|
|
10
|
+
return /^claude-opus-[45]/.test(stripped);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Explicitly configured classifier route for Claude Code Auto Mode safety checks (#1697).
|
|
15
|
+
*
|
|
16
|
+
* Only OPERATOR-DECLARED targets are used: `classifierModel`, then the ordered
|
|
17
|
+
* `classifierFallbacks`. Both are qualified `provider/model` strings the operator chose, so
|
|
18
|
+
* routing them crosses no boundary the operator did not ask for.
|
|
19
|
+
*
|
|
20
|
+
* Deliberately NOT here: inferring a provider from `claudeCode.model`. That value is the
|
|
21
|
+
* injected/default config slot, not the provider the live session actually selected, so it goes
|
|
22
|
+
* stale the moment the user changes the model picker -- and acting on it would silently move a
|
|
23
|
+
* classifier turn onto a provider with its own privacy and billing consequences. Live session
|
|
24
|
+
* affinity needs the request/session state this function does not have; it is tracked as
|
|
25
|
+
* follow-up work rather than approximated from static config.
|
|
26
|
+
*/
|
|
27
|
+
function configuredClassifierRoute(cc?: OcxClaudeCodeConfig): string | undefined {
|
|
28
|
+
const explicit = typeof cc?.classifierModel === "string" ? cc.classifierModel.trim() : "";
|
|
29
|
+
if (explicit.length > 0) return explicit;
|
|
30
|
+
if (Array.isArray(cc?.classifierFallbacks)) {
|
|
31
|
+
for (const candidate of cc.classifierFallbacks) {
|
|
32
|
+
if (typeof candidate === "string" && candidate.trim().length > 0) return candidate.trim();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Alias first, then modelMap: exact id, then date-suffix-stripped (`-\d{8}$`), then classifier affinity/config, else passthrough. */
|
|
39
|
+
export function resolveInboundModel(model: string, cc?: OcxClaudeCodeConfig): string {
|
|
40
|
+
// Defensive: Desktop/CLI strip the [1m] context-variant marker client-side, but a
|
|
41
|
+
// leaking build must not break alias decode (devlog 138 — the 1M signal is the
|
|
42
|
+
// anthropic-beta header, never the id). Case-insensitive: the CLI matches /\[1m\]/i.
|
|
43
|
+
model = stripOneMillionMarker(model);
|
|
44
|
+
const aliased = resolveAlias(model);
|
|
45
|
+
if (aliased) return aliased;
|
|
46
|
+
// Desktop 3P aliases: claude-opus-4-{code} → provider/model route key
|
|
47
|
+
const desktop3p = resolveDesktop3pAlias(model);
|
|
48
|
+
if (desktop3p) {
|
|
49
|
+
// Native pseudo-provider returns bare slug; routed returns provider/model
|
|
50
|
+
const sep = desktop3p.indexOf("/");
|
|
51
|
+
if (sep > 0 && desktop3p.slice(0, sep) === "native") return desktop3p.slice(sep + 1);
|
|
52
|
+
return desktop3p;
|
|
53
|
+
}
|
|
54
|
+
const map = cc?.modelMap ?? {};
|
|
55
|
+
const exact = map[model];
|
|
56
|
+
if (typeof exact === "string" && exact.length > 0) return exact;
|
|
57
|
+
const stripped = model.replace(/-\d{8}$/, "");
|
|
58
|
+
const dateless = map[stripped];
|
|
59
|
+
if (typeof dateless === "string" && dateless.length > 0) return dateless;
|
|
60
|
+
|
|
61
|
+
// Claude Code Auto Mode classifier routing (#1697). Bare classifier checks such as
|
|
62
|
+
// `claude-opus-5` carry no provider, so without this they fall through to defaultProvider --
|
|
63
|
+
// which may not speak Anthropic at all. Only an operator-declared target is used.
|
|
64
|
+
if (isClaudeClassifierModel(model)) {
|
|
65
|
+
const configured = configuredClassifierRoute(cc);
|
|
66
|
+
if (configured) return configured;
|
|
67
|
+
}
|
|
68
|
+
return model;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** budget_tokens ladder -> Responses reasoning effort (003: real API min is 1024; never forward raw). */
|
|
72
|
+
export function effortForThinkingBudget(budget: number): string {
|
|
73
|
+
if (budget <= 4096) return "low";
|
|
74
|
+
if (budget <= 16384) return "medium";
|
|
75
|
+
return "high";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Adaptive-thinking wire (devlog 080): Claude Code /effort sends
|
|
80
|
+
* `thinking:{type:"adaptive"}` + `output_config:{effort:"..."}` (verified by local
|
|
81
|
+
* capture of claude 2.1.207 and CLIProxyAPI#1540). Forward the level verbatim when it
|
|
82
|
+
* is a known Responses effort; unknown strings are dropped so downstream defaults win.
|
|
83
|
+
*/
|
|
84
|
+
const OUTPUT_CONFIG_EFFORTS = new Set(["minimal", "low", "medium", "high", "xhigh", "max", "ultra"]);
|
|
85
|
+
export function effortFromOutputConfig(outputConfig: unknown): string | undefined {
|
|
86
|
+
if (!isRec(outputConfig)) return undefined;
|
|
87
|
+
const effort = outputConfig.effort;
|
|
88
|
+
return typeof effort === "string" && OUTPUT_CONFIG_EFFORTS.has(effort) ? effort : undefined;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function formatFromOutputConfig(outputConfig: unknown): Rec | undefined {
|
|
92
|
+
if (!isRec(outputConfig) || !isRec(outputConfig.format)) return undefined;
|
|
93
|
+
const format = outputConfig.format;
|
|
94
|
+
if (
|
|
95
|
+
format.type !== "json_schema"
|
|
96
|
+
|| !isRec(format.schema)
|
|
97
|
+
|| !isAnthropicOutputSchema(format.schema)
|
|
98
|
+
) return undefined;
|
|
99
|
+
return { type: "json_schema", name: "response", schema: format.schema };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* ocx-route directive (devlog 072): injected agent-definition bodies carry
|
|
104
|
+
* `<!-- ocx-route: <model> -->` because Claude Code 2.1.207 ignores custom
|
|
105
|
+
* gateway ids in agent frontmatter (live-proven fallback to sonnet). The body
|
|
106
|
+
* rides the subagent's system prompt, so the proxy re-routes here. Only the
|
|
107
|
+
* FIRST directive wins; the scan is bounded to the system field.
|
|
108
|
+
*/
|
|
109
|
+
const OCX_ROUTE_RE = /<!--\s*ocx-route:\s*([^\s]+)\s*-->/;
|
|
110
|
+
const OCX_EFFORT_RE = /<!--\s*ocx-effort:\s*(low|medium|high|xhigh|max)\s*-->/;
|
|
111
|
+
|
|
112
|
+
function systemText(body: unknown): string | null {
|
|
113
|
+
if (!isRec(body)) return null;
|
|
114
|
+
const system = body.system;
|
|
115
|
+
if (typeof system === "string") return system || null;
|
|
116
|
+
if (!Array.isArray(system)) return null;
|
|
117
|
+
const text = system
|
|
118
|
+
.filter((b): b is Rec => isRec(b) && b.type === "text" && typeof b.text === "string")
|
|
119
|
+
.map(b => b.text as string)
|
|
120
|
+
.join("\n");
|
|
121
|
+
return text || null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function extractOcxRouteDirective(body: unknown): string | null {
|
|
125
|
+
const text = systemText(body);
|
|
126
|
+
if (!text) return null;
|
|
127
|
+
const match = OCX_ROUTE_RE.exec(text);
|
|
128
|
+
return match ? match[1]! : null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Claude Code 2.1.220 collapses custom-agent frontmatter `effort: max` and
|
|
133
|
+
* `effort: xhigh` into the legacy `thinking.budget_tokens` shape. Preserve the
|
|
134
|
+
* exact generated-agent setting through the same trusted system-body channel as
|
|
135
|
+
* ocx-route so the inbound translator can restore `output_config.effort`.
|
|
136
|
+
*/
|
|
137
|
+
export function extractOcxEffortDirective(body: unknown): NonNullable<OcxClaudeCodeConfig["subagentEffort"]> | null {
|
|
138
|
+
const text = systemText(body);
|
|
139
|
+
if (!text) return null;
|
|
140
|
+
const match = OCX_EFFORT_RE.exec(text);
|
|
141
|
+
return match ? match[1] as NonNullable<OcxClaudeCodeConfig["subagentEffort"]> : null;
|
|
142
|
+
}
|