@bitkyc08/opencodex 2.42.0 → 2.43.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS_INSTALL.md +2 -2
- package/README.md +31 -0
- package/bin/ocx.mjs +10 -1
- package/gui/dist/assets/index-DS1NE4Jn.css +1 -0
- package/gui/dist/assets/index-Djowl68T.js +112 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic-image-codec.ts +304 -0
- package/src/adapters/anthropic-image-normalize.ts +8 -298
- package/src/adapters/anthropic.ts +30 -7
- package/src/adapters/command-code.ts +7 -5
- package/src/adapters/cursor/desktop-executor-contract.ts +15 -0
- package/src/adapters/cursor/images.ts +36 -6
- package/src/adapters/cursor/live-transport.ts +7 -2
- package/src/adapters/cursor/native-exec-desktop.ts +2 -15
- package/src/adapters/cursor/protobuf-request.ts +13 -3
- package/src/adapters/cursor/tool-definitions.ts +5 -670
- package/src/adapters/cursor/tool-guidance.ts +236 -0
- package/src/adapters/cursor/tool-naming.ts +252 -0
- package/src/adapters/cursor/tool-schemas.ts +195 -0
- package/src/adapters/cursor/types.ts +6 -3
- package/src/adapters/exec-tool-result-normalize.ts +1 -1
- package/src/adapters/google-errors.ts +9 -1
- package/src/adapters/google.ts +1 -0
- package/src/adapters/kiro-calibration.ts +181 -0
- package/src/adapters/kiro.ts +135 -3
- package/src/adapters/openai-responses.ts +216 -26
- package/src/adapters/responses-code-mode.ts +59 -0
- package/src/adapters/tool-catalog-nudge.ts +1 -1
- package/src/adapters/xai-schema-analysis.ts +86 -0
- package/src/adapters/xai-tool-schema.ts +2 -87
- package/src/adapters/xai-web-search.ts +1 -1
- package/src/bridge.ts +22 -10
- package/src/chat/inbound.ts +11 -3
- package/src/claude/inbound-content-options.ts +60 -0
- package/src/claude/inbound-model-options.ts +142 -0
- package/src/claude/inbound-records.ts +7 -0
- package/src/claude/inbound.ts +10 -202
- package/src/claude/model-info.ts +45 -0
- package/src/cli/account-auth.ts +21 -6
- package/src/cli/capabilities.ts +13 -2
- package/src/cli/claude.ts +232 -39
- package/src/cli/config-command.ts +9 -1
- package/src/cli/dispatch.ts +5 -1
- package/src/cli/doctor.ts +10 -0
- package/src/cli/effort.ts +372 -0
- package/src/cli/export-command.ts +3 -9
- package/src/cli/help.ts +1 -0
- package/src/cli/index.ts +13 -0
- package/src/cli/init.ts +4 -0
- package/src/cli/model-selection-guidance.ts +30 -0
- package/src/cli/models-runtime.ts +3 -2
- package/src/cli/models.ts +8 -3
- package/src/cli/opencode.ts +4 -1
- package/src/cli/provider-runtime.ts +65 -0
- package/src/cli/provider.ts +8 -0
- package/src/cli/registry.ts +16 -2
- package/src/cli/runtime-api.ts +3 -1
- package/src/cli/star-prompt.ts +22 -6
- package/src/cli/status-probes.ts +168 -0
- package/src/cli/status.ts +5 -168
- package/src/clients/config-export/constants.ts +69 -0
- package/src/clients/config-export/contracts.ts +154 -0
- package/src/clients/config-export/dsh.ts +132 -0
- package/src/clients/config-export/fast-models.ts +29 -0
- package/src/clients/config-export/mcode.ts +83 -0
- package/src/clients/config-export/model-metadata.ts +108 -0
- package/src/clients/config-export/omp.ts +104 -0
- package/src/clients/config-export/zcode.ts +92 -0
- package/src/clients/config-export.ts +18 -710
- package/src/codex/account-lifecycle.ts +20 -3
- package/src/codex/account-usability.ts +2 -0
- package/src/codex/auth-api.ts +170 -23
- package/src/codex/auth-context.ts +328 -24
- package/src/codex/catalog/effort.ts +30 -4
- package/src/codex/catalog/metadata.ts +39 -9
- package/src/codex/catalog/native-models.ts +53 -12
- package/src/codex/catalog/parsing.ts +119 -5
- package/src/codex/catalog/provider-fetch.ts +150 -20
- package/src/codex/catalog/reserve.ts +52 -0
- package/src/codex/catalog/sync.ts +89 -16
- package/src/codex/convergence-types.ts +1 -0
- package/src/codex/convergence.ts +2 -0
- package/src/codex/data/upstream-models.json +1 -1
- package/src/codex/forward-transport-headers.ts +25 -0
- package/src/codex/inject.ts +3 -28
- package/src/codex/legacy-config-keys.ts +68 -0
- package/src/codex/log-guard/inspect-schema.ts +137 -0
- package/src/codex/log-guard/inspect.ts +2 -134
- package/src/codex/loopback-target.ts +54 -0
- package/src/codex/main-account-cache.ts +63 -1
- package/src/codex/main-account-hard-lock.ts +52 -0
- package/src/codex/main-account.ts +3 -1
- package/src/codex/management-convergence.ts +3 -0
- package/src/codex/model-entitlements.ts +54 -4
- package/src/codex/project-config-warnings.ts +92 -2
- package/src/codex/prompt-layers/encoding.ts +80 -0
- package/src/codex/prompt-layers/paths.ts +54 -0
- package/src/codex/prompt-layers/revision.ts +55 -0
- package/src/codex/prompt-layers/toml-edit.ts +163 -0
- package/src/codex/prompt-layers/toml-read.ts +181 -0
- package/src/codex/prompt-layers.ts +14 -520
- package/src/codex/quota-auto-refresh-state.ts +16 -0
- package/src/codex/quota-auto-refresh.ts +219 -0
- package/src/codex/quota-types.ts +51 -0
- package/src/codex/quota.ts +252 -93
- package/src/codex/reserve-availability.ts +177 -0
- package/src/codex/routing.ts +28 -9
- package/src/codex/shim.ts +53 -11
- package/src/codex/subagent-model-fallback.ts +23 -3
- package/src/combos/failover.ts +125 -7
- package/src/combos/identifiers.ts +89 -0
- package/src/combos/index.ts +4 -0
- package/src/combos/resolve.ts +80 -9
- package/src/combos/types.ts +20 -93
- package/src/config/subagent-models.ts +24 -0
- package/src/config.ts +156 -13
- package/src/generated/compatibility-version.json +454 -166
- package/src/generated/model-metadata.ts +1 -1
- package/src/integrations/journal.ts +65 -4
- package/src/integrations/store.ts +5 -0
- package/src/lab/events/limits.ts +4 -0
- package/src/lib/destination-policy.ts +31 -2
- package/src/lib/errors.ts +39 -0
- package/src/lib/provider-outbound.ts +69 -3
- package/src/lib/proxy-env.ts +22 -0
- package/src/lib/redact-folding.ts +176 -0
- package/src/lib/redact.ts +2 -175
- package/src/lib/state-store-sweeper.ts +20 -6
- package/src/lib/token-estimate.ts +94 -27
- package/src/lib/windows-user-principal.ts +53 -5
- package/src/oauth/anthropic-routing.ts +99 -3
- package/src/oauth/generic-account-failover.ts +36 -13
- package/src/oauth/index.ts +127 -26
- package/src/oauth/login-cli.ts +5 -0
- package/src/oauth/meta-muse.ts +117 -15
- package/src/oauth/pool-settings-capability.ts +15 -4
- package/src/providers/api-keys.ts +8 -10
- package/src/providers/default-aliases.ts +39 -0
- package/src/providers/derive.ts +10 -2
- package/src/providers/fastwire.ts +36 -7
- package/src/providers/initial-model-selection-runtime.ts +90 -0
- package/src/providers/initial-model-selection.ts +120 -0
- package/src/providers/key-failover.ts +134 -54
- package/src/providers/key-store.ts +11 -1
- package/src/providers/label.ts +1 -1
- package/src/providers/model-discovery.ts +76 -0
- package/src/providers/model-rename-startup.ts +72 -8
- package/src/providers/openai-sidecar.ts +17 -5
- package/src/providers/openai-tiers-destination.ts +102 -0
- package/src/providers/openai-tiers.ts +2 -99
- package/src/providers/opencode-go-transport.ts +41 -0
- package/src/providers/quota-key-accounts.ts +141 -0
- package/src/providers/quota-types.ts +9 -0
- package/src/providers/quota.ts +535 -104
- package/src/providers/registry.ts +60 -17
- package/src/providers/xai-responses-opt-in.ts +31 -5
- package/src/quota/reset-activation.ts +81 -0
- package/src/quota/reset-detector.ts +305 -0
- package/src/quota/reset-notify-config.ts +162 -0
- package/src/quota/reset-observer.ts +125 -0
- package/src/quota/reset-poller.ts +160 -0
- package/src/quota/reset-seen-store.ts +385 -0
- package/src/quota/reset-sinks.ts +199 -0
- package/src/quota/window-mapping.ts +106 -0
- package/src/responses/apply-patch-envelope.ts +46 -0
- package/src/responses/code-mode-helper-compat.ts +39 -1
- package/src/responses/custom-tool-compat.ts +10 -4
- package/src/responses/hosted-tool-policy.ts +12 -4
- package/src/responses/parser-content.ts +133 -0
- package/src/responses/parser-text-format.ts +24 -0
- package/src/responses/parser-tools.ts +188 -0
- package/src/responses/parser.ts +3 -326
- package/src/responses/state.ts +124 -28
- package/src/router.ts +48 -13
- package/src/routing/analytics.ts +1 -0
- package/src/routing/capability.ts +17 -4
- package/src/server/auth-cors.ts +7 -1
- package/src/server/background-lifecycle.ts +23 -1
- package/src/server/chat-completions.ts +25 -3
- package/src/server/claude-messages.ts +62 -5
- package/src/server/effort-row.ts +1 -1
- package/src/server/fast-row.ts +295 -0
- package/src/server/gui-static.ts +30 -4
- package/src/server/index.ts +107 -21
- package/src/server/management/agent-settings-routes.ts +2 -2
- package/src/server/management/combo-routes.ts +37 -9
- package/src/server/management/config-routes.ts +93 -2
- package/src/server/management/integration-routes.ts +108 -0
- package/src/server/management/model-routes.ts +13 -3
- package/src/server/management/model-rows.ts +20 -1
- package/src/server/management/native-integration-routes.ts +4 -1
- package/src/server/management/oauth-account-routes.ts +38 -10
- package/src/server/management/provider-routes.ts +34 -3
- package/src/server/management/quota-reset-routes.ts +57 -0
- package/src/server/management/route-registry.ts +7 -4
- package/src/server/management/shared.ts +19 -5
- package/src/server/management/system-routes.ts +3 -2
- package/src/server/management-api.ts +14 -2
- package/src/server/ports.ts +12 -2
- package/src/server/relay-eager.ts +38 -23
- package/src/server/relay.ts +4 -0
- package/src/server/request-log.ts +6 -0
- package/src/server/responses/codex-ws-correlation.ts +65 -0
- package/src/server/responses/codex-ws-exchange.ts +261 -0
- package/src/server/responses/codex-ws-metadata.ts +134 -0
- package/src/server/responses/codex-ws-pool.ts +162 -0
- package/src/server/responses/codex-ws-request.ts +87 -0
- package/src/server/responses/codex-ws-session.ts +93 -0
- package/src/server/responses/codex-ws-wire.ts +144 -0
- package/src/server/responses/collaboration.ts +41 -1
- package/src/server/responses/compact.ts +105 -12
- package/src/server/responses/core.ts +476 -57
- package/src/server/responses/fetch-helpers.ts +10 -3
- package/src/server/responses/input-admission.ts +16 -9
- package/src/server/responses/responses-field-backfill.ts +1 -1
- package/src/server/responses/ws-upstream.ts +34 -318
- package/src/server/responses-custom-tool-repair.ts +20 -4
- package/src/server/responses-undeclared-tool-guard.ts +100 -8
- package/src/server/safe-response-headers.ts +23 -0
- package/src/server/search.ts +9 -0
- package/src/server/subagent-models-startup.ts +27 -0
- package/src/server/system-env-shell.ts +238 -0
- package/src/server/system-env.ts +7 -234
- package/src/server/ws-bridge.ts +3 -25
- package/src/server/xai-responses-startup.ts +21 -0
- package/src/service-manager-probe.ts +1 -1
- package/src/service.ts +55 -16
- package/src/types/config.ts +108 -12
- package/src/types/provider.ts +36 -7
- package/src/types/tools.ts +26 -1
- package/src/types.ts +2 -0
- package/src/update/notify.ts +8 -2
- package/src/usage/cost.ts +38 -28
- package/src/usage/expected-prices.ts +34 -15
- package/src/usage/log.ts +2 -0
- package/src/usage/model-identity.ts +26 -0
- package/src/usage/summary.ts +15 -1
- package/src/vision/describe.ts +6 -0
- package/src/vision/image-rewrite.ts +108 -0
- package/src/vision/index.ts +19 -306
- package/src/vision/plan.ts +205 -0
- package/src/web-search/executor.ts +6 -0
- package/src/web-search/index.ts +8 -1
- package/gui/dist/assets/index-BU1tE0sr.js +0 -112
- package/gui/dist/assets/index-DL9-iS6J.css +0 -1
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 {
|
|
@@ -277,9 +277,12 @@ export function bridgeToResponsesSSE(
|
|
|
277
277
|
toolName: string,
|
|
278
278
|
namespace?: string,
|
|
279
279
|
codeModeHelperName?: string,
|
|
280
|
-
): string =>
|
|
281
|
-
|
|
282
|
-
|
|
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
|
+
};
|
|
283
286
|
// Best-effort unwrap of a PARTIAL freeform arg buffer for live input streaming
|
|
284
287
|
// (`response.custom_tool_call_input.delta` — codex-rs uses it for UI preview only;
|
|
285
288
|
// the completed custom_tool_call item stays authoritative). Compact `{"input":"...`
|
|
@@ -1172,7 +1175,13 @@ export function bridgeToResponsesSSE(
|
|
|
1172
1175
|
if (!FREEFORM_WRAP_PREFIX.startsWith(currentToolCall.args)) {
|
|
1173
1176
|
const full = freeformPartialInput(currentToolCall.args);
|
|
1174
1177
|
const emitted = currentToolCall.inputEmitted ?? "";
|
|
1175
|
-
|
|
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) {
|
|
1176
1185
|
emit("response.custom_tool_call_input.delta", {
|
|
1177
1186
|
item_id: currentToolCall.itemId, output_index: currentToolCall.outputIndex,
|
|
1178
1187
|
delta: full.slice(emitted.length),
|
|
@@ -1666,9 +1675,12 @@ function buildResponseJSONWithBudget(
|
|
|
1666
1675
|
toolName: string,
|
|
1667
1676
|
namespace?: string,
|
|
1668
1677
|
codeModeHelperName?: string,
|
|
1669
|
-
): string =>
|
|
1670
|
-
|
|
1671
|
-
|
|
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
|
+
};
|
|
1672
1684
|
const parseArgsObj = (args: string): Record<string, unknown> => {
|
|
1673
1685
|
try { const o = JSON.parse(args); return o && typeof o === "object" ? o : {}; } catch { return {}; }
|
|
1674
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
|
+
}
|
package/src/claude/inbound.ts
CHANGED
|
@@ -10,130 +10,24 @@
|
|
|
10
10
|
* - top_k is accepted and silently dropped (no Responses equivalent, CCR parity).
|
|
11
11
|
*/
|
|
12
12
|
import type { OcxClaudeCodeConfig } from "../types";
|
|
13
|
-
import { isAnthropicOutputSchema } from "../adapters/anthropic-output-schema";
|
|
14
|
-
import { resolveAlias } from "./alias";
|
|
15
|
-
import { stripOneMillionMarker } from "./context-windows";
|
|
16
|
-
import { resolveDesktop3pAlias } from "./desktop-3p";
|
|
17
|
-
import { isClaudeWebSearchToolName } from "./outbound";
|
|
18
13
|
import { createHash } from "node:crypto";
|
|
19
14
|
|
|
20
|
-
export
|
|
15
|
+
export { AnthropicRequestError } from "./inbound-records";
|
|
16
|
+
export { resolveInboundModel, effortForThinkingBudget, effortFromOutputConfig, extractOcxRouteDirective, extractOcxEffortDirective } from "./inbound-model-options";
|
|
17
|
+
import { AnthropicRequestError, isRec, type Rec } from "./inbound-records";
|
|
18
|
+
import { resolveInboundModel, effortForThinkingBudget, effortFromOutputConfig, formatFromOutputConfig } from "./inbound-model-options";
|
|
19
|
+
import { systemToInstructions, toolsToResponses, toolChoiceToResponses } from "./inbound-content-options";
|
|
21
20
|
|
|
22
|
-
type Rec = Record<string, unknown>;
|
|
23
21
|
|
|
24
|
-
function isRec(v: unknown): v is Rec {
|
|
25
|
-
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function isClaudeClassifierModel(model: string): boolean {
|
|
29
|
-
const stripped = model.replace(/-\d{8}$/, "");
|
|
30
|
-
return /^claude-opus-[45]/.test(stripped);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Explicitly configured classifier route for Claude Code Auto Mode safety checks (#1697).
|
|
35
|
-
*
|
|
36
|
-
* Only OPERATOR-DECLARED targets are used: `classifierModel`, then the ordered
|
|
37
|
-
* `classifierFallbacks`. Both are qualified `provider/model` strings the operator chose, so
|
|
38
|
-
* routing them crosses no boundary the operator did not ask for.
|
|
39
|
-
*
|
|
40
|
-
* Deliberately NOT here: inferring a provider from `claudeCode.model`. That value is the
|
|
41
|
-
* injected/default config slot, not the provider the live session actually selected, so it goes
|
|
42
|
-
* stale the moment the user changes the model picker -- and acting on it would silently move a
|
|
43
|
-
* classifier turn onto a provider with its own privacy and billing consequences. Live session
|
|
44
|
-
* affinity needs the request/session state this function does not have; it is tracked as
|
|
45
|
-
* follow-up work rather than approximated from static config.
|
|
46
|
-
*/
|
|
47
|
-
function configuredClassifierRoute(cc?: OcxClaudeCodeConfig): string | undefined {
|
|
48
|
-
const explicit = typeof cc?.classifierModel === "string" ? cc.classifierModel.trim() : "";
|
|
49
|
-
if (explicit.length > 0) return explicit;
|
|
50
|
-
if (Array.isArray(cc?.classifierFallbacks)) {
|
|
51
|
-
for (const candidate of cc.classifierFallbacks) {
|
|
52
|
-
if (typeof candidate === "string" && candidate.trim().length > 0) return candidate.trim();
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
return undefined;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/** Alias first, then modelMap: exact id, then date-suffix-stripped (`-\d{8}$`), then classifier affinity/config, else passthrough. */
|
|
59
|
-
export function resolveInboundModel(model: string, cc?: OcxClaudeCodeConfig): string {
|
|
60
|
-
// Defensive: Desktop/CLI strip the [1m] context-variant marker client-side, but a
|
|
61
|
-
// leaking build must not break alias decode (devlog 138 — the 1M signal is the
|
|
62
|
-
// anthropic-beta header, never the id). Case-insensitive: the CLI matches /\[1m\]/i.
|
|
63
|
-
model = stripOneMillionMarker(model);
|
|
64
|
-
const aliased = resolveAlias(model);
|
|
65
|
-
if (aliased) return aliased;
|
|
66
|
-
// Desktop 3P aliases: claude-opus-4-{code} → provider/model route key
|
|
67
|
-
const desktop3p = resolveDesktop3pAlias(model);
|
|
68
|
-
if (desktop3p) {
|
|
69
|
-
// Native pseudo-provider returns bare slug; routed returns provider/model
|
|
70
|
-
const sep = desktop3p.indexOf("/");
|
|
71
|
-
if (sep > 0 && desktop3p.slice(0, sep) === "native") return desktop3p.slice(sep + 1);
|
|
72
|
-
return desktop3p;
|
|
73
|
-
}
|
|
74
|
-
const map = cc?.modelMap ?? {};
|
|
75
|
-
const exact = map[model];
|
|
76
|
-
if (typeof exact === "string" && exact.length > 0) return exact;
|
|
77
|
-
const stripped = model.replace(/-\d{8}$/, "");
|
|
78
|
-
const dateless = map[stripped];
|
|
79
|
-
if (typeof dateless === "string" && dateless.length > 0) return dateless;
|
|
80
|
-
|
|
81
|
-
// Claude Code Auto Mode classifier routing (#1697). Bare classifier checks such as
|
|
82
|
-
// `claude-opus-5` carry no provider, so without this they fall through to defaultProvider --
|
|
83
|
-
// which may not speak Anthropic at all. Only an operator-declared target is used.
|
|
84
|
-
if (isClaudeClassifierModel(model)) {
|
|
85
|
-
const configured = configuredClassifierRoute(cc);
|
|
86
|
-
if (configured) return configured;
|
|
87
|
-
}
|
|
88
|
-
return model;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/** budget_tokens ladder -> Responses reasoning effort (003: real API min is 1024; never forward raw). */
|
|
92
|
-
export function effortForThinkingBudget(budget: number): string {
|
|
93
|
-
if (budget <= 4096) return "low";
|
|
94
|
-
if (budget <= 16384) return "medium";
|
|
95
|
-
return "high";
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Adaptive-thinking wire (devlog 080): Claude Code /effort sends
|
|
100
|
-
* `thinking:{type:"adaptive"}` + `output_config:{effort:"..."}` (verified by local
|
|
101
|
-
* capture of claude 2.1.207 and CLIProxyAPI#1540). Forward the level verbatim when it
|
|
102
|
-
* is a known Responses effort; unknown strings are dropped so downstream defaults win.
|
|
103
|
-
*/
|
|
104
|
-
const OUTPUT_CONFIG_EFFORTS = new Set(["minimal", "low", "medium", "high", "xhigh", "max", "ultra"]);
|
|
105
|
-
export function effortFromOutputConfig(outputConfig: unknown): string | undefined {
|
|
106
|
-
if (!isRec(outputConfig)) return undefined;
|
|
107
|
-
const effort = outputConfig.effort;
|
|
108
|
-
return typeof effort === "string" && OUTPUT_CONFIG_EFFORTS.has(effort) ? effort : undefined;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function formatFromOutputConfig(outputConfig: unknown): Rec | undefined {
|
|
112
|
-
if (!isRec(outputConfig) || !isRec(outputConfig.format)) return undefined;
|
|
113
|
-
const format = outputConfig.format;
|
|
114
|
-
if (
|
|
115
|
-
format.type !== "json_schema"
|
|
116
|
-
|| !isRec(format.schema)
|
|
117
|
-
|| !isAnthropicOutputSchema(format.schema)
|
|
118
|
-
) return undefined;
|
|
119
|
-
return { type: "json_schema", name: "response", schema: format.schema };
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function systemToInstructions(system: unknown): string | undefined {
|
|
123
|
-
if (typeof system === "string") return system.length > 0 ? system : undefined;
|
|
124
|
-
if (Array.isArray(system)) {
|
|
125
|
-
const parts: string[] = [];
|
|
126
|
-
for (const block of system) {
|
|
127
|
-
if (isRec(block) && block.type === "text" && typeof block.text === "string") parts.push(block.text);
|
|
128
|
-
}
|
|
129
|
-
return parts.length > 0 ? parts.join("\n\n") : undefined;
|
|
130
|
-
}
|
|
131
|
-
return undefined;
|
|
132
|
-
}
|
|
133
22
|
|
|
134
23
|
function imageBlockToInputImage(block: Rec): Rec | null {
|
|
135
24
|
const source = block.source;
|
|
136
25
|
if (!isRec(source)) return null;
|
|
26
|
+
if (source.type === "file") {
|
|
27
|
+
throw new AnthropicRequestError(
|
|
28
|
+
"File-backed images require native Anthropic passthrough; use base64 or URL images on translated routes.",
|
|
29
|
+
);
|
|
30
|
+
}
|
|
137
31
|
if (source.type === "base64" && typeof source.data === "string") {
|
|
138
32
|
const media = typeof source.media_type === "string" ? source.media_type : "image/png";
|
|
139
33
|
return { type: "input_image", image_url: `data:${media};base64,${source.data}` };
|
|
@@ -194,47 +88,6 @@ export function effectiveBlockedSkillNames(cc?: Pick<OcxClaudeCodeConfig, "block
|
|
|
194
88
|
.filter(name => name.length > 0))];
|
|
195
89
|
}
|
|
196
90
|
|
|
197
|
-
/**
|
|
198
|
-
* ocx-route directive (devlog 072): injected agent-definition bodies carry
|
|
199
|
-
* `<!-- ocx-route: <model> -->` because Claude Code 2.1.207 ignores custom
|
|
200
|
-
* gateway ids in agent frontmatter (live-proven fallback to sonnet). The body
|
|
201
|
-
* rides the subagent's system prompt, so the proxy re-routes here. Only the
|
|
202
|
-
* FIRST directive wins; the scan is bounded to the system field.
|
|
203
|
-
*/
|
|
204
|
-
const OCX_ROUTE_RE = /<!--\s*ocx-route:\s*([^\s]+)\s*-->/;
|
|
205
|
-
const OCX_EFFORT_RE = /<!--\s*ocx-effort:\s*(low|medium|high|xhigh|max)\s*-->/;
|
|
206
|
-
|
|
207
|
-
function systemText(body: unknown): string | null {
|
|
208
|
-
if (!isRec(body)) return null;
|
|
209
|
-
const system = body.system;
|
|
210
|
-
if (typeof system === "string") return system || null;
|
|
211
|
-
if (!Array.isArray(system)) return null;
|
|
212
|
-
const text = system
|
|
213
|
-
.filter((b): b is Rec => isRec(b) && b.type === "text" && typeof b.text === "string")
|
|
214
|
-
.map(b => b.text as string)
|
|
215
|
-
.join("\n");
|
|
216
|
-
return text || null;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
export function extractOcxRouteDirective(body: unknown): string | null {
|
|
220
|
-
const text = systemText(body);
|
|
221
|
-
if (!text) return null;
|
|
222
|
-
const match = OCX_ROUTE_RE.exec(text);
|
|
223
|
-
return match ? match[1]! : null;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* Claude Code 2.1.220 collapses custom-agent frontmatter `effort: max` and
|
|
228
|
-
* `effort: xhigh` into the legacy `thinking.budget_tokens` shape. Preserve the
|
|
229
|
-
* exact generated-agent setting through the same trusted system-body channel as
|
|
230
|
-
* ocx-route so the inbound translator can restore `output_config.effort`.
|
|
231
|
-
*/
|
|
232
|
-
export function extractOcxEffortDirective(body: unknown): NonNullable<OcxClaudeCodeConfig["subagentEffort"]> | null {
|
|
233
|
-
const text = systemText(body);
|
|
234
|
-
if (!text) return null;
|
|
235
|
-
const match = OCX_EFFORT_RE.exec(text);
|
|
236
|
-
return match ? match[1] as NonNullable<OcxClaudeCodeConfig["subagentEffort"]> : null;
|
|
237
|
-
}
|
|
238
91
|
|
|
239
92
|
/** Injected-skill payloads below this size are never stubbed (not worth it). */
|
|
240
93
|
const SKILL_ELISION_MIN_CHARS = 10_000;
|
|
@@ -391,51 +244,6 @@ function assistantMessageToItems(content: unknown, input: Rec[]): void {
|
|
|
391
244
|
flush();
|
|
392
245
|
}
|
|
393
246
|
|
|
394
|
-
function toolsToResponses(tools: unknown): Rec[] | undefined {
|
|
395
|
-
if (!Array.isArray(tools) || tools.length === 0) return undefined;
|
|
396
|
-
const out: Rec[] = [];
|
|
397
|
-
for (const raw of tools) {
|
|
398
|
-
if (!isRec(raw)) continue;
|
|
399
|
-
const type = typeof raw.type === "string" ? raw.type : "";
|
|
400
|
-
if (type.startsWith("web_search")) {
|
|
401
|
-
out.push({ type: "web_search" }); // hosted sidecar path
|
|
402
|
-
continue;
|
|
403
|
-
}
|
|
404
|
-
if (typeof raw.name === "string" && raw.name.length > 0 && isRec(raw.input_schema)) {
|
|
405
|
-
out.push({
|
|
406
|
-
type: "function",
|
|
407
|
-
name: raw.name,
|
|
408
|
-
...(typeof raw.description === "string" ? { description: raw.description } : {}),
|
|
409
|
-
parameters: raw.input_schema as Record<string, unknown>,
|
|
410
|
-
});
|
|
411
|
-
continue;
|
|
412
|
-
}
|
|
413
|
-
// Other server tools (bash_*, text_editor_*, ...) have no routed equivalent: drop.
|
|
414
|
-
}
|
|
415
|
-
return out.length > 0 ? out : undefined;
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
function toolChoiceToResponses(choice: unknown, body: Rec): void {
|
|
419
|
-
if (!isRec(choice)) return;
|
|
420
|
-
if (choice.disable_parallel_tool_use === true) body.parallel_tool_calls = false;
|
|
421
|
-
switch (choice.type) {
|
|
422
|
-
case "auto": body.tool_choice = "auto"; break;
|
|
423
|
-
case "none": body.tool_choice = "none"; break;
|
|
424
|
-
case "any": body.tool_choice = "required"; break;
|
|
425
|
-
case "tool":
|
|
426
|
-
if (typeof choice.name !== "string" || choice.name.length === 0) {
|
|
427
|
-
throw new AnthropicRequestError("tool_choice.tool requires a name");
|
|
428
|
-
}
|
|
429
|
-
// Anthropic represents hosted WebSearch as a named tool choice, while
|
|
430
|
-
// Responses requires the choice type to match the hosted declaration.
|
|
431
|
-
// Preserve forced-tool intent rather than weakening it to `auto`.
|
|
432
|
-
body.tool_choice = isClaudeWebSearchToolName(choice.name)
|
|
433
|
-
? { type: "web_search" }
|
|
434
|
-
: { type: "function", name: choice.name };
|
|
435
|
-
break;
|
|
436
|
-
default: break;
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
247
|
|
|
440
248
|
/** Recursive canonical JSON (keys sorted at every depth) — stable cache-cohort input. */
|
|
441
249
|
function canonicalJson(value: unknown): string {
|
package/src/claude/model-info.ts
CHANGED
|
@@ -111,9 +111,32 @@ export function buildAnthropicModelInfos(
|
|
|
111
111
|
aliasForRoute: (provider: string, modelId: string) => string = desktop3pAlias,
|
|
112
112
|
nativeContextCap?: NativeContextLimitsInput,
|
|
113
113
|
fastMode?: boolean,
|
|
114
|
+
// Presence is the feature gate: the caller passes undefined when `fastRows` is off, so a
|
|
115
|
+
// default install publishes nothing. The predicate answers ELIGIBILITY, not enablement.
|
|
116
|
+
fastRows?: (model: CatalogModel | { provider: string; id: string }) => boolean,
|
|
114
117
|
): AnthropicModelInfo[] {
|
|
115
118
|
const out: AnthropicModelInfo[] = [];
|
|
116
119
|
const seen = new Set<string>();
|
|
120
|
+
// Every id the loops below will really emit, computed BEFORE either runs. `seen` alone is
|
|
121
|
+
// not enough: it grows as they run, so whether a synthetic id collided with a real one
|
|
122
|
+
// would depend on iteration order. With both `foo` and a real `foo--fast` in the roster,
|
|
123
|
+
// the synthetic id for `foo` IS the real model's id, and whichever ran first would win it.
|
|
124
|
+
const realDiscoveryIds = new Set<string>([
|
|
125
|
+
...nativeSlugs.map(slug => (
|
|
126
|
+
idStyle === "readable" ? claudeCodeNativeAlias(slug) : aliasForRoute("native", slug)
|
|
127
|
+
)),
|
|
128
|
+
...routedModels.map(m => {
|
|
129
|
+
// The same asymmetry the routed loop applies: readable uses the LISTED id, so a
|
|
130
|
+
// fastMode-rewritten Cursor row is counted under the id it is really published as,
|
|
131
|
+
// while Desktop 3P hashes the RAW id.
|
|
132
|
+
const listed = fastMode === true && m.provider === "cursor" && idStyle === "readable"
|
|
133
|
+
? cursorFastIdFor(m.id) ?? m.id
|
|
134
|
+
: m.id;
|
|
135
|
+
return idStyle === "readable"
|
|
136
|
+
? claudeCodeAlias(m.provider, listed)
|
|
137
|
+
: aliasForRoute(m.provider, m.id);
|
|
138
|
+
}),
|
|
139
|
+
]);
|
|
117
140
|
// [1m] picker variant (devlog 260712 B1): Claude Code accounts exactly 1M for ids
|
|
118
141
|
// carrying the marker (2.1.207 binary: /\[1m\]/i → 1e6, compaction preserved), so
|
|
119
142
|
// ONLY models with an authoritative >=1M window get a second selectable row —
|
|
@@ -140,6 +163,21 @@ export function buildAnthropicModelInfos(
|
|
|
140
163
|
: ONE_MILLION;
|
|
141
164
|
out.push({ ...base, id, display_name: `${base.display_name} · 1M`, max_input_tokens: advertised });
|
|
142
165
|
};
|
|
166
|
+
/**
|
|
167
|
+
* Publish a Fast sibling beside a row, following `push1mVariant` rather than the
|
|
168
|
+
* `fastMode` rewrite below: `fastMode` is a global switch with no per-request choice, so
|
|
169
|
+
* it REPLACES the listed id, while a selector has to leave the default pickable beside it.
|
|
170
|
+
*
|
|
171
|
+
* Because it only ADDS a row, it is safe for the Desktop 3P hashed style too — the
|
|
172
|
+
* exclusion `fastMode` needs exists because rewriting a hash strands a saved selection.
|
|
173
|
+
*/
|
|
174
|
+
const pushFastVariant = (base: AnthropicModelInfo) => {
|
|
175
|
+
const id = `${base.id}--fast`;
|
|
176
|
+
// A real model always wins its own id, whatever the iteration order.
|
|
177
|
+
if (realDiscoveryIds.has(id) || seen.has(id)) return;
|
|
178
|
+
seen.add(id);
|
|
179
|
+
out.push({ ...base, id, display_name: `${base.display_name} · Fast` });
|
|
180
|
+
};
|
|
143
181
|
for (const slug of nativeSlugs) {
|
|
144
182
|
const id = idStyle === "readable" ? claudeCodeNativeAlias(slug) : aliasForRoute("native", slug);
|
|
145
183
|
if (seen.has(id)) continue;
|
|
@@ -151,6 +189,9 @@ export function buildAnthropicModelInfos(
|
|
|
151
189
|
const info = modelInfo(id, `${slug} (native)`, nativeEffectiveLadder(slug), true, nativeMaxInput ?? nativeWindow);
|
|
152
190
|
out.push(info);
|
|
153
191
|
push1mVariant(info, nativeWindow, nativeMaxInput);
|
|
192
|
+
// Natives too, not only routed rows: gpt-5.6-sol is the flagship Fast model, and
|
|
193
|
+
// omitting it would leave this surface without the model the feature exists for.
|
|
194
|
+
if (fastRows?.({ provider: "native", id: slug }) === true) pushFastVariant(info);
|
|
154
195
|
}
|
|
155
196
|
for (const m of routedModels) {
|
|
156
197
|
// Global Fast has no toggle on this surface, so the fast identity is what gets listed —
|
|
@@ -180,6 +221,10 @@ export function buildAnthropicModelInfos(
|
|
|
180
221
|
// Anthropic passthrough guard (audit 021 #3): never auto-widen canonical claude
|
|
181
222
|
// routes — only a genuine >=1M window earns the variant row there.
|
|
182
223
|
push1mVariant(info, m.contextWindow, routedMaxInput);
|
|
224
|
+
// The whole model is passed, not a (provider, id) pair: a combo row lives in its own
|
|
225
|
+
// namespace with no config.providers entry, so the caller classifies it from the
|
|
226
|
+
// aggregated supportsServiceTier the row already carries.
|
|
227
|
+
if (fastRows?.(m) === true) pushFastVariant(info);
|
|
183
228
|
}
|
|
184
229
|
return out;
|
|
185
230
|
}
|