@bitkyc08/opencodex 2.64.0 → 2.65.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/README.md +28 -25
- package/bin/ocx.mjs +16 -2
- package/gui/dist/assets/App-8NMiZxT0.css +1 -0
- package/gui/dist/assets/App-DGLte6IR.js +50 -0
- package/gui/dist/assets/Tray-D4bA1vff.js +1 -0
- package/gui/dist/assets/{index-DiBRuK-d.css → index--EWgGQvZ.css} +1 -1
- package/gui/dist/assets/{index-SggB6t3z.js → index-Bi1K37Wl.js} +12 -12
- package/gui/dist/assets/tray-data-BZXhB3qz.js +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -4
- package/src/adapters/anthropic.ts +30 -1
- package/src/adapters/claude-cli/adapter.ts +218 -0
- package/src/adapters/claude-cli/profiles.ts +38 -0
- package/src/adapters/codebuddy/profiles.ts +2 -0
- package/src/adapters/coding-agent/profile.ts +12 -4
- package/src/adapters/coding-agent/turn.ts +16 -6
- package/src/adapters/command-code-tool-text.ts +133 -23
- package/src/adapters/cursor/catalog.ts +4 -8
- package/src/adapters/cursor/discovery.ts +10 -5
- package/src/adapters/cursor/effort-map.ts +2 -6
- package/src/adapters/cursor/envelope-echo.ts +10 -5
- package/src/adapters/cursor/request-builder.ts +9 -4
- package/src/adapters/cursor/thread-continuity.ts +53 -0
- package/src/adapters/cursor.ts +29 -13
- package/src/adapters/devin/cloud-direct/chat.ts +3 -1
- package/src/adapters/google-tool-schema.ts +26 -3
- package/src/adapters/identity.ts +214 -2
- package/src/adapters/openai-chat/passthrough.ts +1 -0
- package/src/adapters/openai-chat/serialized-tool-call-content.ts +569 -0
- package/src/adapters/openai-chat.ts +43 -15
- package/src/adapters/openai-responses/canonical-forward.ts +17 -10
- package/src/adapters/openai-responses/passthrough.ts +19 -4
- package/src/adapters/openai-responses/request-strips.ts +25 -0
- package/src/adapters/openai-responses/web-search.ts +22 -4
- package/src/adapters/qoder/profiles.ts +2 -0
- package/src/adapters/registry.ts +8 -0
- package/src/adapters/run-turn-queue.ts +7 -0
- package/src/bridge/response-json.ts +6 -4
- package/src/bridge/sse.ts +7 -5
- package/src/claude/alias.ts +73 -26
- package/src/claude/context-windows.ts +15 -6
- package/src/claude/desktop-3p-library.ts +3 -2
- package/src/claude/desktop-3p.ts +1 -1
- package/src/claude/desktop-first-party.ts +63 -22
- package/src/claude/desktop-picker-profile.ts +302 -0
- package/src/claude/desktop-picker.ts +365 -0
- package/src/claude/desktop-risk.ts +20 -0
- package/src/claude/gateway-cache.ts +13 -4
- package/src/claude/inbound.ts +5 -2
- package/src/claude/intercept/connect-proxy.ts +106 -29
- package/src/claude/intercept/local-ca.ts +76 -18
- package/src/claude/intercept/model-bindings.ts +145 -0
- package/src/claude/intercept/picker-bootstrap.ts +141 -0
- package/src/claude/intercept/picker-ca.ts +126 -0
- package/src/claude/intercept/picker-listener.ts +213 -0
- package/src/claude/intercept/picker-models.ts +101 -0
- package/src/claude/intercept/picker-runtime.ts +330 -0
- package/src/claude/intercept/picker-trust.ts +125 -0
- package/src/claude/intercept/runtime.ts +149 -2
- package/src/claude/model-info.ts +18 -4
- package/src/cli/agent.ts +16 -2
- package/src/cli/capabilities.ts +71 -2
- package/src/cli/claude-desktop.ts +319 -33
- package/src/cli/claude.ts +15 -7
- package/src/cli/codex-shim-autorestore.ts +1 -0
- package/src/cli/dispatch.ts +29 -23
- package/src/cli/effort.ts +16 -6
- package/src/cli/ensure-desired-integrations.ts +29 -4
- package/src/cli/ready.ts +1 -1
- package/src/cli/registry.ts +8 -0
- package/src/cli/restart-scope.ts +9 -1
- package/src/cli/runtime-api.ts +17 -0
- package/src/cli/system-command.ts +11 -12
- package/src/client/connect.ts +12 -2
- package/src/client/state.ts +39 -1
- package/src/clients/config-export.ts +57 -9
- package/src/codex/auth-context.ts +51 -11
- package/src/codex/catalog/derive-entry.ts +5 -5
- package/src/codex/catalog/gather-capture.ts +25 -6
- package/src/codex/catalog/metadata.ts +3 -3
- package/src/codex/catalog/parsing.ts +39 -1
- package/src/codex/catalog/provider-models.ts +8 -6
- package/src/codex/catalog/retained-sync.ts +17 -7
- package/src/codex/catalog/sync.ts +2 -2
- package/src/codex/desktop-app-restart.ts +8 -0
- package/src/codex/desktop-switches.ts +9 -1
- package/src/codex/home.ts +43 -4
- package/src/codex/inject/config-toml.ts +137 -0
- package/src/codex/inject/plan.ts +23 -0
- package/src/codex/inject/remove.ts +21 -1
- package/src/codex/inject.ts +63 -8
- package/src/codex/journal.ts +36 -0
- package/src/codex/main-account-hard-lock.ts +14 -1
- package/src/codex/main-account-policy-wait.ts +109 -0
- package/src/codex/native-profile-startup.ts +24 -6
- package/src/codex/native-residue.ts +28 -10
- package/src/codex/quota-types.ts +8 -1
- package/src/codex/quota.ts +49 -7
- package/src/codex/runtime.ts +31 -10
- package/src/combos/failover.ts +7 -6
- package/src/combos/resolve.ts +91 -14
- package/src/combos/types.ts +18 -1
- package/src/config/load-degrade.ts +2 -0
- package/src/config/schema/config-schema.ts +21 -2
- package/src/config/schema/leaf-validators.ts +6 -3
- package/src/config/subagent-models.ts +3 -1
- package/src/generated/compatibility-version.json +275 -163
- package/src/images/artifacts.ts +14 -0
- package/src/integrations/owned-refresh.ts +1 -1
- package/src/integrations/ownership-policy.ts +32 -1
- package/src/integrations/state.ts +3 -1
- package/src/integrations/writer.ts +9 -0
- package/src/lib/config-ownership.ts +3 -0
- package/src/lib/errors.ts +87 -0
- package/src/lib/package-tree-integrity.ts +1 -1
- package/src/lib/request-execution-budget.ts +42 -14
- package/src/lib/request-resend-gate.ts +4 -3
- package/src/lib/retry-delay.ts +7 -1
- package/src/lib/tool-envelope-echo-filter.ts +236 -0
- package/src/lib/upstream-retry.ts +20 -12
- package/src/oauth/index.ts +1 -0
- package/src/oauth/kiro-credentials.ts +14 -1
- package/src/oauth/login-cli.ts +1 -0
- package/src/providers/derive.ts +4 -0
- package/src/providers/fast-opt-in.ts +31 -0
- package/src/providers/model-rename-fields.ts +2 -0
- package/src/providers/provider-id-rewrite.ts +2 -0
- package/src/providers/quota/vendor-probes-key.ts +8 -2
- package/src/providers/registry/entries-core.ts +45 -2
- package/src/providers/registry/entries-extended.ts +99 -12
- package/src/providers/registry/model-ids.ts +2 -0
- package/src/providers/registry/types.ts +7 -1
- package/src/providers/resolved-model-policy.ts +11 -4
- package/src/providers/service-tier.ts +10 -3
- package/src/responses/code-mode-helper-compat.ts +3 -0
- package/src/responses/parser.ts +45 -3
- package/src/router.ts +6 -0
- package/src/server/auth-cors.ts +8 -0
- package/src/server/chat-completions.ts +3 -1
- package/src/server/claude-messages.ts +69 -14
- package/src/server/grok-upstream-envelope-echo.ts +120 -0
- package/src/server/index/claude-intercept-lifecycle.ts +24 -0
- package/src/server/index/serve-options.ts +2 -2
- package/src/server/index.ts +7 -0
- package/src/server/management/agent-settings-routes.ts +204 -112
- package/src/server/management/claude-desktop-picker-routes.ts +128 -0
- package/src/server/management/combo-routes.ts +59 -2
- package/src/server/management/config-routes.ts +81 -39
- package/src/server/management/context.ts +3 -0
- package/src/server/management/native-integration-routes.ts +169 -93
- package/src/server/management/provider-routes.ts +46 -4
- package/src/server/management/route-registry.ts +10 -0
- package/src/server/management/routing-profile-routes.ts +9 -0
- package/src/server/management/sidebar-routes.ts +52 -0
- package/src/server/management-api.ts +2 -0
- package/src/server/proxy-liveness.ts +38 -3
- package/src/server/request-log-account-rotation.ts +53 -0
- package/src/server/request-log.ts +3 -16
- package/src/server/responses/adapter-dispatch.ts +17 -1
- package/src/server/responses/agent-task-recovery.ts +49 -26
- package/src/server/responses/codex-ws-exchange.ts +18 -7
- package/src/server/responses/codex-ws-wire.ts +27 -4
- package/src/server/responses/core-combo.ts +21 -3
- package/src/server/responses/core-normalize.ts +7 -0
- package/src/server/responses/core-opaque-recovery.ts +3 -2
- package/src/server/responses/encrypted-payload.ts +15 -14
- package/src/server/responses/fetch-helpers.ts +1 -1
- package/src/server/responses/input-admission.ts +10 -6
- package/src/server/responses/native-response-control.ts +2 -2
- package/src/server/responses/passthrough-delivery.ts +182 -12
- package/src/server/responses/passthrough-dispatch.ts +102 -44
- package/src/server/responses/policy-fallback.ts +3 -0
- package/src/server/responses/policy-refusal.ts +67 -0
- package/src/server/responses/request-send-budget.ts +4 -0
- package/src/server/responses/run-turn-execution.ts +42 -11
- package/src/server/responses/ws-upstream.ts +2 -1
- package/src/server/system-env-shell.ts +0 -1
- package/src/server/system-env.ts +25 -4
- package/src/service/cli.ts +34 -3
- package/src/tray/assets/opencodex-tray-offline-update.ico +0 -0
- package/src/tray/assets/opencodex-tray-online-update.ico +0 -0
- package/src/tray/assets/opencodex-tray-warning-update.ico +0 -0
- package/src/tray/windows-tray.ps1 +188 -7
- package/src/tray/windows.ts +23 -2
- package/src/types/config.ts +52 -12
- package/src/types/provider.ts +22 -5
- package/src/types/tools.ts +11 -5
- package/src/types.ts +1 -0
- package/src/update/async-check.ts +109 -0
- package/src/update/badge.ts +11 -17
- package/src/update/check-types.ts +9 -0
- package/src/update/desktop-badge.ts +102 -0
- package/src/update/index.ts +48 -8
- package/src/update/install-detection.d.mts +29 -2
- package/src/update/install-detection.mjs +213 -7
- package/src/update/job.ts +17 -12
- package/src/update/notify.ts +16 -11
- package/src/update/pnpm-owner-worker.ts +12 -0
- package/src/update/refresh-scheduler.ts +141 -0
- package/src/usage/cost.ts +31 -4
- package/src/usage/expected-prices.ts +57 -0
- package/assets/download-linux.svg +0 -10
- package/assets/download-macos.svg +0 -10
- package/assets/download-windows.svg +0 -10
- package/gui/dist/assets/App-CpuDF3ci.js +0 -50
- package/gui/dist/assets/App-I5AnaSLh.css +0 -1
- package/gui/dist/assets/Tray-B4uEIa1O.js +0 -1
- package/gui/dist/assets/usage-companion-chart-DLbKOJml.js +0 -1
package/src/responses/parser.ts
CHANGED
|
@@ -18,6 +18,7 @@ import { lookupReplayThoughtSignature } from "./thought-signature-replay";
|
|
|
18
18
|
import { compactionItemToText, isCompactionItemType } from "./compaction";
|
|
19
19
|
import { previousResponseReplayPrefixLength } from "./state";
|
|
20
20
|
import { decodeReasoningEnvelope } from "./reasoning-envelope";
|
|
21
|
+
import { hasRoutedIdentity, nameRoutedIdentity } from "../adapters/identity";
|
|
21
22
|
import { extractHostedWebSearch, WEB_SEARCH_TOOL_NAME } from "../web-search/synthetic-tool";
|
|
22
23
|
import { buildImageTool, extractHostedImageGeneration, IMAGE_GEN_TOOL_NAME } from "../images/synthetic-tool";
|
|
23
24
|
import { toolSearchDescription, toolSearchParameters } from "./tool-search-compat";
|
|
@@ -121,6 +122,33 @@ export function hasValidatedActiveReasoningEffort(options: Pick<OcxRequestOption
|
|
|
121
122
|
}
|
|
122
123
|
|
|
123
124
|
|
|
125
|
+
/**
|
|
126
|
+
* Name this request's destination in an instruction text inherited from a stored session block.
|
|
127
|
+
*
|
|
128
|
+
* Returns the input unchanged when it carries no sentence of ours, which is every request that has
|
|
129
|
+
* not gone through a sub-agent spawn. The catalog block is model-neutral on disk (#5217) and some
|
|
130
|
+
* routed adapters build their own system text instead of calling `identifyRoutedModel`, so both the
|
|
131
|
+
* neutral line and a sentence naming an earlier model are handled here — request time is the first
|
|
132
|
+
* point where the destination model is known.
|
|
133
|
+
*/
|
|
134
|
+
function nameDestinationText(text: string, modelId: string): string {
|
|
135
|
+
return hasRoutedIdentity(text) ? nameRoutedIdentity(text, modelId) : text;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function nameDestinationContent(
|
|
139
|
+
content: string | OcxContentPart[],
|
|
140
|
+
modelId: string,
|
|
141
|
+
): string | OcxContentPart[] {
|
|
142
|
+
if (typeof content === "string") return nameDestinationText(content, modelId);
|
|
143
|
+
let changed = false;
|
|
144
|
+
const parts = content.map((part) => {
|
|
145
|
+
if (part.type !== "text" || !hasRoutedIdentity(part.text)) return part;
|
|
146
|
+
changed = true;
|
|
147
|
+
return { ...part, text: nameRoutedIdentity(part.text, modelId) };
|
|
148
|
+
});
|
|
149
|
+
return changed ? parts : content;
|
|
150
|
+
}
|
|
151
|
+
|
|
124
152
|
export function parseRequest(
|
|
125
153
|
body: unknown,
|
|
126
154
|
parseOptions?: { replayCacheScope?: OcxReasoningReplayScopeRef },
|
|
@@ -168,7 +196,10 @@ export function parseRequest(
|
|
|
168
196
|
let continuationConversationMessageIndex: number | undefined;
|
|
169
197
|
|
|
170
198
|
if (typeof data.instructions === "string" && data.instructions.length > 0) {
|
|
171
|
-
|
|
199
|
+
// #5217: this is the stored session instruction block. A sub-agent spawned on a DIFFERENT model
|
|
200
|
+
// receives the parent's copy verbatim, so the identity sentence inside it names the parent
|
|
201
|
+
// unless it is renamed here, where the destination model is known.
|
|
202
|
+
systemPrompt.push(nameDestinationText(data.instructions, data.model));
|
|
172
203
|
}
|
|
173
204
|
|
|
174
205
|
if (typeof data.input === "string") {
|
|
@@ -274,14 +305,25 @@ export function parseRequest(
|
|
|
274
305
|
const flat = typeof text === "string"
|
|
275
306
|
? text
|
|
276
307
|
: text.map(p => (p.type === "text" || p.type === "document" ? p.text : "")).join("");
|
|
277
|
-
|
|
308
|
+
// #5217: a system-role item is instruction text, exactly like `instructions` and a
|
|
309
|
+
// developer item, so it needs the same request-time naming — it lands in the system
|
|
310
|
+
// block verbatim, and Codex replays the parent's copy to a sub-agent on another model.
|
|
311
|
+
if (flat.length > 0) systemPrompt.push(nameDestinationText(flat, data.model));
|
|
278
312
|
break;
|
|
279
313
|
}
|
|
280
314
|
case "user":
|
|
281
315
|
case "developer": {
|
|
282
316
|
pendingReasoning.length = 0;
|
|
283
317
|
const content = inputContentParts(msg.content);
|
|
284
|
-
messages.push({
|
|
318
|
+
messages.push({
|
|
319
|
+
role: msg.role,
|
|
320
|
+
// #5217: Codex replays the PARENT session's instruction block as the worker's
|
|
321
|
+
// developer message, so a sub-agent on another model inherits an identity sentence
|
|
322
|
+
// naming the parent. Only this proxy's own sentence is rewritten, and only on a
|
|
323
|
+
// developer item; user turns are the caller's content and stay byte-identical.
|
|
324
|
+
content: msg.role === "developer" ? nameDestinationContent(content, data.model) : content,
|
|
325
|
+
timestamp: now,
|
|
326
|
+
});
|
|
285
327
|
break;
|
|
286
328
|
}
|
|
287
329
|
case "assistant": {
|
package/src/router.ts
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
import { registryModelIdKeys } from "./providers/registry/model-ids";
|
|
27
27
|
import { applyDirectReasoningEffortContracts, hasLegacyClinePassReasoningEfforts } from "./providers/derive";
|
|
28
28
|
import { cloneFastWire } from "./providers/fastwire";
|
|
29
|
+
import { fastSwitchOff } from "./providers/fast-opt-in";
|
|
29
30
|
import {
|
|
30
31
|
providerMatchesRegistryTransportWithStaticGuards,
|
|
31
32
|
providerSupportsLiveModelDiscovery,
|
|
@@ -339,6 +340,7 @@ export function routedProviderConfig(providerName: string, provider: OcxProvider
|
|
|
339
340
|
const noReasoningModels = staticPolicy.noReasoningModels;
|
|
340
341
|
const noTemperatureModels = staticPolicy.noTemperatureModels;
|
|
341
342
|
const noTopPModels = staticPolicy.noTopPModels;
|
|
343
|
+
const noStopModels = staticPolicy.noStopModels;
|
|
342
344
|
const noPenaltyModels = staticPolicy.noPenaltyModels;
|
|
343
345
|
const noJsonSchemaModels = staticPolicy.noJsonSchemaModels;
|
|
344
346
|
const autoToolChoiceOnlyModels = staticPolicy.autoToolChoiceOnlyModels;
|
|
@@ -392,6 +394,9 @@ export function routedProviderConfig(providerName: string, provider: OcxProvider
|
|
|
392
394
|
...(provider.supportsServiceTier === undefined && registryEntry.supportsServiceTier !== undefined
|
|
393
395
|
? { supportsServiceTier: registryEntry.supportsServiceTier }
|
|
394
396
|
: {}),
|
|
397
|
+
// An off Fast switch is a provider-wide denial on the runtime provider, so a Fast policy
|
|
398
|
+
// resolved without the provider name still refuses (providerFastSwitchOff).
|
|
399
|
+
...(fastSwitchOff(provider, registryEntry) ? { supportsServiceTier: false } : {}),
|
|
395
400
|
// Registry-only web-search capability: without this backfill a saved provider row reaches
|
|
396
401
|
// the Responses adapter with the flag `undefined`, so the capability gate added in #2262
|
|
397
402
|
// reads "unclassified" and forwards Codex's OpenAI-only `web_search` config fields. xAI
|
|
@@ -482,6 +487,7 @@ export function routedProviderConfig(providerName: string, provider: OcxProvider
|
|
|
482
487
|
...(noReasoningModels ? { noReasoningModels } : {}),
|
|
483
488
|
...(noTemperatureModels ? { noTemperatureModels } : {}),
|
|
484
489
|
...(noTopPModels ? { noTopPModels } : {}),
|
|
490
|
+
...(noStopModels ? { noStopModels } : {}),
|
|
485
491
|
...(noPenaltyModels ? { noPenaltyModels } : {}),
|
|
486
492
|
...(noJsonSchemaModels ? { noJsonSchemaModels } : {}),
|
|
487
493
|
...(autoToolChoiceOnlyModels ? { autoToolChoiceOnlyModels } : {}),
|
package/src/server/auth-cors.ts
CHANGED
|
@@ -744,6 +744,12 @@ export function providerManagementConfigError(
|
|
|
744
744
|
// validation and then rejected by the seed comparison, so canonical OpenAI could never
|
|
745
745
|
// set OR clear it — the value was admitted and then refused in the same request.
|
|
746
746
|
delete canonicalCandidate.annotateEmptyToolOutputs;
|
|
747
|
+
// Canonical ChatGPT keeps WebSocket as the default, but an operator may
|
|
748
|
+
// select the existing HTTP/SSE path without changing its auth or endpoint.
|
|
749
|
+
if (raw.upstreamWebsocket !== undefined) {
|
|
750
|
+
if (raw.upstreamWebsocket !== false) return "provider openai upstreamWebsocket must be false or omitted";
|
|
751
|
+
delete canonicalCandidate.upstreamWebsocket;
|
|
752
|
+
}
|
|
747
753
|
const canonical = seed && (options?.allowOperatorOverlays
|
|
748
754
|
? matchesCanonicalProviderSeed(canonicalCandidate, seed)
|
|
749
755
|
: sameCanonicalProviderSeed(canonicalCandidate, seed));
|
|
@@ -950,6 +956,7 @@ const PROVIDER_CONFIG_FIELD_POLICY = {
|
|
|
950
956
|
mcpMaxResultBytes: "editor",
|
|
951
957
|
modelAdapters: "editor",
|
|
952
958
|
fastWire: "editor",
|
|
959
|
+
fastEnabled: "editor",
|
|
953
960
|
baseUrl: "editor",
|
|
954
961
|
responsesPath: "editor",
|
|
955
962
|
chatCompletionsPath: "editor",
|
|
@@ -1042,6 +1049,7 @@ const PROVIDER_CONFIG_FIELD_POLICY = {
|
|
|
1042
1049
|
noReasoningModels: "editor",
|
|
1043
1050
|
noTemperatureModels: "editor",
|
|
1044
1051
|
noTopPModels: "editor",
|
|
1052
|
+
noStopModels: "editor",
|
|
1045
1053
|
noPenaltyModels: "editor",
|
|
1046
1054
|
noStructuredOutputModels: "editor",
|
|
1047
1055
|
noJsonSchemaModels: "editor",
|
|
@@ -328,7 +328,9 @@ async function handleChatCompletionsWithBudget(
|
|
|
328
328
|
// This enrichment is optional for routed/non-main providers. If native main
|
|
329
329
|
// is fenced, omit it and let auth-context reject only a final physical-main
|
|
330
330
|
// selection while healthy pool/provider routes continue.
|
|
331
|
-
|
|
331
|
+
const isCanonicalPool = settledRoute && isCanonicalOpenAiForwardProvider(settledRoute.provider)
|
|
332
|
+
&& settledRoute.codexAccountMode === "pool";
|
|
333
|
+
if (!isCanonicalPool && tryClaimNativeMainProfileForTurn(logIds?.turnAdmissionLease)) {
|
|
332
334
|
try {
|
|
333
335
|
const { getMainAccountToken } = await import("../codex/main-account");
|
|
334
336
|
const token = getMainAccountToken();
|
|
@@ -17,12 +17,14 @@ import { jsonUtf8Bytes } from "../lib/json-byte-size";
|
|
|
17
17
|
import { sseFieldValue } from "../lib/sse-decoder";
|
|
18
18
|
import { enforceAnthropicImageLimits, sniffImageDimensions } from "../adapters/anthropic-image-guard";
|
|
19
19
|
import { normalizeAnthropicImages } from "../adapters/anthropic-image-normalize";
|
|
20
|
+
import { createToolCallIdAllocator } from "../adapters/tool-call-id";
|
|
20
21
|
import { AnthropicRequestError, DesktopModelMappingUnavailableError, anthropicToResponsesTranslation, extractOcxEffortDirective, extractOcxRouteDirective, resolveInboundModel, type ClaudeCacheKeySource } from "../claude/inbound";
|
|
21
22
|
import { isKnownDesktop3pModelId, resolveDesktop3pAlias } from "../claude/desktop-3p";
|
|
22
|
-
import { resolveAlias, claudeCodeNativeAlias } from "../claude/alias";
|
|
23
|
+
import { resolveAlias, claudeCodeNativeAlias, legacyAliasForNative } from "../claude/alias";
|
|
23
24
|
import { recordDesktopRequest } from "../claude/desktop-health";
|
|
24
25
|
import { stripOneMillionMarker } from "../claude/context-windows";
|
|
25
26
|
import { captureClaudeInbound } from "../claude/inbound-debug";
|
|
27
|
+
import { claudeCodeForIngress } from "../claude/intercept/model-bindings";
|
|
26
28
|
import { analyzeClaudeCompatibility, isClaudeCompatibilityMode } from "../claude/compatibility";
|
|
27
29
|
import {
|
|
28
30
|
applyReplayRefusalClientHeaders,
|
|
@@ -85,6 +87,11 @@ import {
|
|
|
85
87
|
|
|
86
88
|
type Rec = Record<string, unknown>;
|
|
87
89
|
|
|
90
|
+
/** Which listener a Claude Messages request arrived on. Only the intercept ingress honours bindings. */
|
|
91
|
+
export interface ClaudeIngressOptions {
|
|
92
|
+
claudeIntercept?: boolean;
|
|
93
|
+
}
|
|
94
|
+
|
|
88
95
|
/**
|
|
89
96
|
* Decode a Claude selector that may carry the fast marker.
|
|
90
97
|
*
|
|
@@ -112,7 +119,8 @@ function decodeClaudeFastSelector(raw: string, cc?: OcxConfig["claudeCode"]): st
|
|
|
112
119
|
function decodeFablePickerAlias(raw: string, cc?: OcxConfig["claudeCode"]): string {
|
|
113
120
|
const decoded = resolveInboundModel(raw, cc);
|
|
114
121
|
if (!decoded.startsWith("claude-fable-")) return raw;
|
|
115
|
-
|
|
122
|
+
// A picker value saved before the ocx-claude spelling keeps the native passthrough too.
|
|
123
|
+
return claudeCodeNativeAlias(decoded) === raw || legacyAliasForNative(decoded) === raw ? decoded : raw;
|
|
116
124
|
}
|
|
117
125
|
|
|
118
126
|
function isRec(v: unknown): v is Rec {
|
|
@@ -195,8 +203,9 @@ function wantsNativePassthrough(
|
|
|
195
203
|
config: OcxConfig,
|
|
196
204
|
requestPolicy: RequestPolicyView,
|
|
197
205
|
model: unknown,
|
|
206
|
+
cc: OcxConfig["claudeCode"] = config.claudeCode,
|
|
198
207
|
): model is string {
|
|
199
|
-
if (
|
|
208
|
+
if (cc?.nativePassthrough === false) return false;
|
|
200
209
|
if (typeof model !== "string" || !/^(claude|anthropic)/i.test(model)) return false;
|
|
201
210
|
// Authorization and x-api-key both belong to the upstream on this branch. An exposed listener
|
|
202
211
|
// therefore requires the dedicated admission header even though the routed Messages surface
|
|
@@ -207,7 +216,8 @@ function wantsNativePassthrough(
|
|
|
207
216
|
}
|
|
208
217
|
if (!hasAnthropicNativeCredential(req, config)) return false;
|
|
209
218
|
// An alias or modelMap hit means the user asked for a ROUTED model: translate instead.
|
|
210
|
-
|
|
219
|
+
// `cc` carries first-party intercept bindings for requests on the claude-intercept ingress.
|
|
220
|
+
return resolveInboundModel(model, cc) === model;
|
|
211
221
|
}
|
|
212
222
|
|
|
213
223
|
function shouldForwardNativeHeader(name: string, value: string, config: OcxConfig): boolean {
|
|
@@ -406,6 +416,44 @@ export function tapAnthropicSseForLog(
|
|
|
406
416
|
});
|
|
407
417
|
}
|
|
408
418
|
|
|
419
|
+
/**
|
|
420
|
+
* `tool_use.id` / `tool_result.tool_use_id` must match Anthropic's wire contract
|
|
421
|
+
* (`^[a-zA-Z0-9_-]+$`, <=64 chars). Third-party models mint other shapes — Devin's
|
|
422
|
+
* swe-2 emits `Bash:0#<hex>` — and a session history carrying them 400s the moment it
|
|
423
|
+
* is switched to a native Anthropic model ("messages.N.content.M.tool_use.id: String
|
|
424
|
+
* should match pattern"). The adapter path normalizes these via
|
|
425
|
+
* adapters/tool-call-id.ts (#1780); this passthrough bypasses that adapter, so the same
|
|
426
|
+
* allocator runs here. Stateless per request: conforming ids pass through byte-identical
|
|
427
|
+
* (prompt-cache keys untouched), rewritten ids keep call/result pairing stable.
|
|
428
|
+
* An empty id has no representable wire form, and forwarding `""` is what Anthropic
|
|
429
|
+
* rejects (#1767), so the request fails locally with a 400 before any upstream fetch.
|
|
430
|
+
*/
|
|
431
|
+
function sanitizePassthroughToolCallIds(messages: unknown[]): void {
|
|
432
|
+
const blocks: Rec[] = [];
|
|
433
|
+
for (const message of messages) {
|
|
434
|
+
if (!isRec(message) || !Array.isArray(message.content)) continue;
|
|
435
|
+
for (const block of message.content) if (isRec(block)) blocks.push(block);
|
|
436
|
+
}
|
|
437
|
+
const fieldOf = (block: Rec): "id" | "tool_use_id" | undefined => {
|
|
438
|
+
if (typeof block.type !== "string") return undefined;
|
|
439
|
+
if (block.type.endsWith("tool_use")) return "id";
|
|
440
|
+
if (block.type.endsWith("tool_result")) return "tool_use_id";
|
|
441
|
+
return undefined;
|
|
442
|
+
};
|
|
443
|
+
const callIds = createToolCallIdAllocator();
|
|
444
|
+
for (const block of blocks) {
|
|
445
|
+
const field = fieldOf(block);
|
|
446
|
+
if (field && typeof block[field] === "string") callIds.reserve(block[field] as string);
|
|
447
|
+
}
|
|
448
|
+
for (const block of blocks) {
|
|
449
|
+
const field = fieldOf(block);
|
|
450
|
+
if (!field || typeof block[field] !== "string") continue;
|
|
451
|
+
const wire = callIds.allocate(block[field] as string);
|
|
452
|
+
if (wire === undefined) throw new AnthropicRequestError(`${block.type} block has an empty ${field}`);
|
|
453
|
+
block[field] = wire;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
409
457
|
async function anthropicNativePassthrough(
|
|
410
458
|
req: Request,
|
|
411
459
|
config: OcxConfig,
|
|
@@ -435,6 +483,7 @@ async function anthropicNativePassthrough(
|
|
|
435
483
|
if (Array.isArray(body.messages)) {
|
|
436
484
|
await normalizeAnthropicImages(body.messages, { abortSignal: req.signal });
|
|
437
485
|
enforceAnthropicImageLimits(body.messages);
|
|
486
|
+
sanitizePassthroughToolCallIds(body.messages);
|
|
438
487
|
}
|
|
439
488
|
const headers = new Headers();
|
|
440
489
|
req.headers.forEach((value, name) => {
|
|
@@ -637,11 +686,12 @@ export async function handleClaudeMessages(
|
|
|
637
686
|
logCtx: RequestLogContext,
|
|
638
687
|
logIds?: { requestId: string; start: number; turnAdmissionLease?: AdmissionLease; admission?: DataPlaneAdmission },
|
|
639
688
|
requestPolicy: RequestPolicyView = config,
|
|
689
|
+
ingress: ClaudeIngressOptions = {},
|
|
640
690
|
): Promise<Response> {
|
|
641
691
|
const translatorBudget = createTranslatorBudget();
|
|
642
692
|
try {
|
|
643
693
|
return finalizeTranslatorBudgetResponse(
|
|
644
|
-
await handleClaudeMessagesWithBudget(req, config, logCtx, translatorBudget, logIds, requestPolicy),
|
|
694
|
+
await handleClaudeMessagesWithBudget(req, config, logCtx, translatorBudget, logIds, requestPolicy, ingress),
|
|
645
695
|
translatorBudget,
|
|
646
696
|
);
|
|
647
697
|
} catch (error) {
|
|
@@ -663,6 +713,7 @@ async function handleClaudeMessagesWithBudget(
|
|
|
663
713
|
translatorBudget: TranslatorBudget,
|
|
664
714
|
logIds?: { requestId: string; start: number; turnAdmissionLease?: AdmissionLease; admission?: DataPlaneAdmission },
|
|
665
715
|
requestPolicy: RequestPolicyView = config,
|
|
716
|
+
ingress: ClaudeIngressOptions = {},
|
|
666
717
|
): Promise<Response> {
|
|
667
718
|
logCtx.surface = "claude";
|
|
668
719
|
const disabled = claudeInboundDisabled(config);
|
|
@@ -670,6 +721,8 @@ async function handleClaudeMessagesWithBudget(
|
|
|
670
721
|
if (logIds) addFinalRequestLog(logIds.requestId, logIds.start, logCtx, 403, { closeReason: "non_stream" });
|
|
671
722
|
return disabled;
|
|
672
723
|
}
|
|
724
|
+
// Model resolution reads this view; every other claudeCode setting keeps reading `config`.
|
|
725
|
+
const cc = claudeCodeForIngress(config.claudeCode, ingress.claudeIntercept === true);
|
|
673
726
|
|
|
674
727
|
let anthropicBody: unknown;
|
|
675
728
|
let internalBody: Rec;
|
|
@@ -698,7 +751,7 @@ async function handleClaudeMessagesWithBudget(
|
|
|
698
751
|
}
|
|
699
752
|
}
|
|
700
753
|
if (isRec(anthropicBody) && typeof anthropicBody.model === "string") {
|
|
701
|
-
anthropicBody.model = decodeFablePickerAlias(anthropicBody.model,
|
|
754
|
+
anthropicBody.model = decodeFablePickerAlias(anthropicBody.model, cc);
|
|
702
755
|
}
|
|
703
756
|
if (isRec(anthropicBody) && typeof anthropicBody.model === "string") {
|
|
704
757
|
requestedModel = anthropicBody.model;
|
|
@@ -709,7 +762,7 @@ async function handleClaudeMessagesWithBudget(
|
|
|
709
762
|
({ fastRow, effortRow } = parseSyntheticRowId(
|
|
710
763
|
requestedModel,
|
|
711
764
|
config,
|
|
712
|
-
() => decodeClaudeFastSelector(requestedModel,
|
|
765
|
+
() => decodeClaudeFastSelector(requestedModel, cc),
|
|
713
766
|
));
|
|
714
767
|
if (effortRow) {
|
|
715
768
|
anthropicBody.model = effortRow.baseId;
|
|
@@ -723,7 +776,7 @@ async function handleClaudeMessagesWithBudget(
|
|
|
723
776
|
"messages",
|
|
724
777
|
anthropicBody,
|
|
725
778
|
isRec(anthropicBody) && typeof anthropicBody.model === "string"
|
|
726
|
-
? resolveInboundModel(anthropicBody.model,
|
|
779
|
+
? resolveInboundModel(anthropicBody.model, cc)
|
|
727
780
|
: undefined,
|
|
728
781
|
req.headers.get("anthropic-beta") ?? undefined,
|
|
729
782
|
);
|
|
@@ -744,7 +797,7 @@ async function handleClaudeMessagesWithBudget(
|
|
|
744
797
|
// caller's body with the caller's credential and never runs the Anthropic adapter, so the
|
|
745
798
|
// proxy-owned `speed` + beta (anthropic-speed wire) and its usage.speed observation would be
|
|
746
799
|
// silently skipped. Translation reaches the adapter, which owns both.
|
|
747
|
-
if (!effortRow && !fastRow && isRec(anthropicBody) && wantsNativePassthrough(req, config, requestPolicy, anthropicBody.model)) {
|
|
800
|
+
if (!effortRow && !fastRow && isRec(anthropicBody) && wantsNativePassthrough(req, config, requestPolicy, anthropicBody.model, cc)) {
|
|
748
801
|
return await anthropicNativePassthrough(req, config, logCtx, logIds, anthropicBody, "/v1/messages");
|
|
749
802
|
}
|
|
750
803
|
// Capture source semantics before effort rewriting or translation drops fields.
|
|
@@ -780,7 +833,7 @@ async function handleClaudeMessagesWithBudget(
|
|
|
780
833
|
};
|
|
781
834
|
delete anthropicBody.thinking;
|
|
782
835
|
}
|
|
783
|
-
const translation = anthropicToResponsesTranslation(anthropicBody,
|
|
836
|
+
const translation = anthropicToResponsesTranslation(anthropicBody, cc, translatorBudget);
|
|
784
837
|
internalBody = translation.body;
|
|
785
838
|
// The Anthropic translator builds its body from model/input/store/stream plus sampling
|
|
786
839
|
// fields only, so the caller intent is applied to the TRANSLATED body rather than the
|
|
@@ -1212,9 +1265,11 @@ export async function handleClaudeCountTokens(
|
|
|
1212
1265
|
req: Request,
|
|
1213
1266
|
config: OcxConfig,
|
|
1214
1267
|
requestPolicy: RequestPolicyView = config,
|
|
1268
|
+
ingress: ClaudeIngressOptions = {},
|
|
1215
1269
|
): Promise<Response> {
|
|
1216
1270
|
const disabled = claudeInboundDisabled(config);
|
|
1217
1271
|
if (disabled) return disabled;
|
|
1272
|
+
const cc = claudeCodeForIngress(config.claudeCode, ingress.claudeIntercept === true);
|
|
1218
1273
|
|
|
1219
1274
|
let body: unknown;
|
|
1220
1275
|
const translatorBudget = createTranslatorBudget();
|
|
@@ -1246,20 +1301,20 @@ export async function handleClaudeCountTokens(
|
|
|
1246
1301
|
model = stripOneMillionMarker(countRoute);
|
|
1247
1302
|
raw.model = model;
|
|
1248
1303
|
}
|
|
1249
|
-
model = decodeFablePickerAlias(model,
|
|
1304
|
+
model = decodeFablePickerAlias(model, cc);
|
|
1250
1305
|
raw.model = model;
|
|
1251
1306
|
// Fast-only: count_tokens never parsed an effort row, so it must not start. It returns a
|
|
1252
1307
|
// token estimate and sends no tier, so only the IDENTITY is corrected - without this the
|
|
1253
1308
|
// synthetic id reaches native passthrough as a model Anthropic has never heard of.
|
|
1254
1309
|
const countFastRow = parseFastOnlyRowId(
|
|
1255
|
-
config, () => decodeClaudeFastSelector(model,
|
|
1310
|
+
config, () => decodeClaudeFastSelector(model, cc),
|
|
1256
1311
|
);
|
|
1257
1312
|
if (countFastRow) {
|
|
1258
1313
|
model = countFastRow.baseId;
|
|
1259
1314
|
raw.model = model;
|
|
1260
1315
|
}
|
|
1261
|
-
captureClaudeInbound("count_tokens", raw, resolveInboundModel(model,
|
|
1262
|
-
if (wantsNativePassthrough(req, config, requestPolicy, model)) {
|
|
1316
|
+
captureClaudeInbound("count_tokens", raw, resolveInboundModel(model, cc), req.headers.get("anthropic-beta") ?? undefined);
|
|
1317
|
+
if (wantsNativePassthrough(req, config, requestPolicy, model, cc)) {
|
|
1263
1318
|
return await anthropicNativePassthrough(req, config, { model, provider: "anthropic-native", surface: "claude" }, undefined, raw, "/v1/messages/count_tokens");
|
|
1264
1319
|
}
|
|
1265
1320
|
const inputTokens = estimateClaudeRequestTokens(raw, model);
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { ToolEnvelopeEchoFilter, stripToolEnvelopeEcho } from "../lib/tool-envelope-echo-filter";
|
|
2
|
+
import { replaceSseDataPayload, sseDataPayload, type SseBlockRewrite } from "./sse-payload-rewrite";
|
|
3
|
+
|
|
4
|
+
function record(value: unknown): value is Record<string, unknown> {
|
|
5
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function outputText(value: unknown, replacement?: (index: number, content: number, text: string) => string): void {
|
|
9
|
+
if (!record(value) || !Array.isArray(value.output)) return;
|
|
10
|
+
value.output.forEach((item: unknown, index: number) => {
|
|
11
|
+
if (!record(item) || !Array.isArray(item.content)) return;
|
|
12
|
+
item.content.forEach((part: unknown, content: number) => {
|
|
13
|
+
if (record(part) && part.type === "output_text" && typeof part.text === "string") {
|
|
14
|
+
part.text = replacement?.(index, content, part.text) ?? stripToolEnvelopeEcho(part.text);
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The echo only appears when the model has seen a replayed tool envelope: a tool call or tool output
|
|
22
|
+
* in this request's input, or a stored conversation continued through previous_response_id (its history
|
|
23
|
+
* lives upstream and may hold tool results). A first turn with neither is never filtered, so a
|
|
24
|
+
* legitimate answer that starts a line with "[Tool Result]" reaches the client intact.
|
|
25
|
+
*/
|
|
26
|
+
export function responsesRequestMayReplayToolOutput(rawBody: unknown): boolean {
|
|
27
|
+
if (!record(rawBody)) return false;
|
|
28
|
+
if (typeof rawBody.previous_response_id === "string" && rawBody.previous_response_id !== "") return true;
|
|
29
|
+
if (!Array.isArray(rawBody.input)) return false;
|
|
30
|
+
// A tool call counts too: xAI's paired tool-result repair answers a dangling call with a synthetic
|
|
31
|
+
// output on the outbound request, so the model sees a tool result the caller never sent.
|
|
32
|
+
return rawBody.input.some(item => record(item) && typeof item.type === "string" && /_call(_output)?$/.test(item.type));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Completed non-streaming Responses bodies use the same line-aware marker rule. */
|
|
36
|
+
export function stripGrokUpstreamEnvelopeEchoFromResponsesJson(json: string): string {
|
|
37
|
+
try {
|
|
38
|
+
const value = JSON.parse(json) as unknown;
|
|
39
|
+
outputText(value);
|
|
40
|
+
return JSON.stringify(value);
|
|
41
|
+
} catch {
|
|
42
|
+
return json;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
type TextState = {
|
|
47
|
+
filter: ToolEnvelopeEchoFilter;
|
|
48
|
+
delivered: string;
|
|
49
|
+
lastDeltaBlock: string;
|
|
50
|
+
lastDeltaEvent: Record<string, unknown>;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/** Keep xAI text deltas, done events, terminal snapshots and replay state in agreement. */
|
|
54
|
+
export function createGrokUpstreamEnvelopeEchoBlockRewrite(
|
|
55
|
+
onCompletedResponse?: (response: Record<string, unknown>) => void,
|
|
56
|
+
): SseBlockRewrite {
|
|
57
|
+
const states = new Map<string, TextState>();
|
|
58
|
+
const key = (output: unknown, content: unknown): string =>
|
|
59
|
+
`${typeof output === "number" ? output : 0}:${typeof content === "number" ? content : 0}`;
|
|
60
|
+
const flush = (state: TextState): string[] => {
|
|
61
|
+
const suffix = state.filter.finish();
|
|
62
|
+
if (!suffix) return [];
|
|
63
|
+
state.delivered += suffix;
|
|
64
|
+
return [replaceSseDataPayload(state.lastDeltaBlock, JSON.stringify({ ...state.lastDeltaEvent, delta: suffix }))];
|
|
65
|
+
};
|
|
66
|
+
return (block) => {
|
|
67
|
+
const payload = sseDataPayload(block);
|
|
68
|
+
if (payload === null) return [block];
|
|
69
|
+
if (payload === "[DONE]") return [...[...states.values()].flatMap(flush), block];
|
|
70
|
+
let event: unknown;
|
|
71
|
+
try { event = JSON.parse(payload); } catch { return [block]; }
|
|
72
|
+
if (!record(event) || typeof event.type !== "string") return [block];
|
|
73
|
+
if (event.type === "response.output_text.delta" && typeof event.delta === "string") {
|
|
74
|
+
const id = key(event.output_index, event.content_index);
|
|
75
|
+
let state = states.get(id);
|
|
76
|
+
if (!state) {
|
|
77
|
+
state = { filter: new ToolEnvelopeEchoFilter(), delivered: "", lastDeltaBlock: block, lastDeltaEvent: event };
|
|
78
|
+
states.set(id, state);
|
|
79
|
+
}
|
|
80
|
+
state.lastDeltaBlock = block;
|
|
81
|
+
state.lastDeltaEvent = event;
|
|
82
|
+
const delta = state.filter.feed(event.delta);
|
|
83
|
+
state.delivered += delta;
|
|
84
|
+
if (!delta) return [];
|
|
85
|
+
return [replaceSseDataPayload(block, JSON.stringify({ ...event, delta }))];
|
|
86
|
+
}
|
|
87
|
+
if (event.type === "response.output_text.done") {
|
|
88
|
+
const state = states.get(key(event.output_index, event.content_index));
|
|
89
|
+
if (!state) {
|
|
90
|
+
if (typeof event.text !== "string") return [block];
|
|
91
|
+
event.text = stripToolEnvelopeEcho(event.text);
|
|
92
|
+
return [replaceSseDataPayload(block, JSON.stringify(event))];
|
|
93
|
+
}
|
|
94
|
+
const pending = flush(state);
|
|
95
|
+
event.text = state.delivered;
|
|
96
|
+
return [...pending, replaceSseDataPayload(block, JSON.stringify(event))];
|
|
97
|
+
}
|
|
98
|
+
if (event.type === "response.output_item.done" && record(event.item)) {
|
|
99
|
+
const index = typeof event.output_index === "number" ? event.output_index : 0;
|
|
100
|
+
const pending: string[] = [];
|
|
101
|
+
if (Array.isArray(event.item.content)) {
|
|
102
|
+
event.item.content.forEach((part: unknown, content: number) => {
|
|
103
|
+
if (!record(part) || part.type !== "output_text" || typeof part.text !== "string") return;
|
|
104
|
+
const state = states.get(key(index, content));
|
|
105
|
+
if (state) { pending.push(...flush(state)); part.text = state.delivered; }
|
|
106
|
+
else part.text = stripToolEnvelopeEcho(part.text);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
return [...pending, replaceSseDataPayload(block, JSON.stringify(event))];
|
|
110
|
+
}
|
|
111
|
+
if (event.type === "response.completed" && record(event.response)) {
|
|
112
|
+
const pending = [...states.values()].flatMap(flush);
|
|
113
|
+
outputText(event.response, (index, content, text) =>
|
|
114
|
+
states.get(key(index, content))?.delivered ?? stripToolEnvelopeEcho(text));
|
|
115
|
+
onCompletedResponse?.(event.response);
|
|
116
|
+
return [...pending, replaceSseDataPayload(block, JSON.stringify(event))];
|
|
117
|
+
}
|
|
118
|
+
return [block];
|
|
119
|
+
};
|
|
120
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Server } from "bun";
|
|
2
|
+
import type { PickerRouteInput } from "../../claude/intercept/picker-models";
|
|
2
3
|
import {
|
|
3
4
|
startClaudeIntercept,
|
|
4
5
|
type ClaudeInterceptHandle,
|
|
@@ -18,6 +19,28 @@ export interface ClaudeInterceptLifecycle<T> {
|
|
|
18
19
|
stop(): Promise<void>;
|
|
19
20
|
}
|
|
20
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Routes for Desktop's Code-tab picker: the same inputs `/api/sync` gives the gateway profile
|
|
24
|
+
* (src/server/management/config-routes.ts), read from the persisted config at call time. Dynamic
|
|
25
|
+
* imports keep the catalog and discovery off the synchronous startup path.
|
|
26
|
+
*/
|
|
27
|
+
export async function loadPickerRoutesFromCatalog(): Promise<PickerRouteInput> {
|
|
28
|
+
const [{ loadConfig }, { fetchAllModels }, catalog] = await Promise.all([
|
|
29
|
+
import("../../config"),
|
|
30
|
+
import("../management-api"),
|
|
31
|
+
import("../../codex/catalog"),
|
|
32
|
+
]);
|
|
33
|
+
const config = loadConfig();
|
|
34
|
+
const models = await fetchAllModels(config);
|
|
35
|
+
return {
|
|
36
|
+
nativeSlugs: [...catalog.desktopVisibleNativeSlugs(config)],
|
|
37
|
+
routedModels: catalog.filterCatalogVisibleModels(models, config)
|
|
38
|
+
.map(model => ({ provider: model.provider, id: model.id, contextWindow: model.contextWindow })),
|
|
39
|
+
...(config.claudeCode?.desktopProfile ? { profile: config.claudeCode.desktopProfile } : {}),
|
|
40
|
+
nativeContextCap: catalog.nativeContextLimits(config),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
21
44
|
export function createClaudeInterceptLifecycle<T>(): ClaudeInterceptLifecycle<T> {
|
|
22
45
|
let listener: Server<T> | null = null;
|
|
23
46
|
let pending: Promise<ClaudeInterceptHandle<T> | null> = Promise.resolve(null);
|
|
@@ -27,6 +50,7 @@ export function createClaudeInterceptLifecycle<T>(): ClaudeInterceptLifecycle<T>
|
|
|
27
50
|
const dispatch = options.dispatch;
|
|
28
51
|
pending = startClaudeIntercept<T>({
|
|
29
52
|
...options,
|
|
53
|
+
loadPickerRoutes: options.loadPickerRoutes ?? loadPickerRoutesFromCatalog,
|
|
30
54
|
dispatch: (req, requestServer) => {
|
|
31
55
|
listener ??= requestServer;
|
|
32
56
|
return dispatch(req, requestServer);
|
|
@@ -1475,7 +1475,7 @@ export function createServeOptions(ctx: ServeOptionsContext) {
|
|
|
1475
1475
|
return withCors(anthropicErrorResponse(403, "cross-origin data-plane request blocked", "permission_error"), req, policy);
|
|
1476
1476
|
}
|
|
1477
1477
|
return runAdmittedHttpTurn(req, policy, async () => withCors(
|
|
1478
|
-
await handleClaudeCountTokens(req, config, policy),
|
|
1478
|
+
await handleClaudeCountTokens(req, config, policy, { claudeIntercept: ingress === "claude-intercept" }),
|
|
1479
1479
|
req,
|
|
1480
1480
|
policy,
|
|
1481
1481
|
));
|
|
@@ -1506,7 +1506,7 @@ export function createServeOptions(ctx: ServeOptionsContext) {
|
|
|
1506
1506
|
// pre-translation stream + native passthrough callbacks) — do not re-wrap the
|
|
1507
1507
|
// translated Anthropic stream here.
|
|
1508
1508
|
return runAdmittedHttpTurn(req, policy, async turnAdmissionLease => withCors(
|
|
1509
|
-
await handleClaudeMessages(req, config, logCtx, { requestId, start, turnAdmissionLease, admission }, policy),
|
|
1509
|
+
await handleClaudeMessages(req, config, logCtx, { requestId, start, turnAdmissionLease, admission }, policy, { claudeIntercept: ingress === "claude-intercept" }),
|
|
1510
1510
|
req,
|
|
1511
1511
|
policy,
|
|
1512
1512
|
), { requestId, start, logCtx });
|
package/src/server/index.ts
CHANGED
|
@@ -61,6 +61,7 @@ import {
|
|
|
61
61
|
registerDefaultAppOwnedObservedBuffers,
|
|
62
62
|
} from "../lib/app-owned-memory-stores";
|
|
63
63
|
import { acquireServerBackgroundLifecycle } from "./background-lifecycle";
|
|
64
|
+
import { startPackageRefresh, stopPackageRefresh } from "../update/refresh-scheduler";
|
|
64
65
|
import { activateLab, labActivationRequired } from "../lib/lab-activation";
|
|
65
66
|
import { runOpenAiTierStartupMigration } from "../providers/openai-tier-startup";
|
|
66
67
|
import { runAlibabaRegionStartupMigration } from "../providers/alibaba-region-startup";
|
|
@@ -746,6 +747,7 @@ function startServerWithSpendLedgerOwner(port: number | undefined, deps: StartSe
|
|
|
746
747
|
const nativeStop = server.stop.bind(server);
|
|
747
748
|
const loopbackListenerRef = loopbackServer;
|
|
748
749
|
const managementIngressRef = managementIngressServer;
|
|
750
|
+
let packageRefreshStopped = false;
|
|
749
751
|
Object.defineProperty(server, "stop", {
|
|
750
752
|
configurable: true,
|
|
751
753
|
value: async (closeActiveConnections?: boolean): Promise<void> => {
|
|
@@ -754,6 +756,10 @@ function startServerWithSpendLedgerOwner(port: number | undefined, deps: StartSe
|
|
|
754
756
|
// Disarm the package-tree restart timer before listener teardown: a queued
|
|
755
757
|
// replacement callback must not call acceptSystemRestart() after stop() has
|
|
756
758
|
// begun, or it would schedule a drain-and-restart on a stopped server.
|
|
759
|
+
if (!packageRefreshStopped) {
|
|
760
|
+
packageRefreshStopped = true;
|
|
761
|
+
stopPackageRefresh();
|
|
762
|
+
}
|
|
757
763
|
packageTreeIntegrity.dispose();
|
|
758
764
|
// The orchestration lives in `runListenerShutdown` so its two competing properties —
|
|
759
765
|
// cleanup completes, failure propagates — are testable without a live socket.
|
|
@@ -874,6 +880,7 @@ function startServerWithSpendLedgerOwner(port: number | undefined, deps: StartSe
|
|
|
874
880
|
});
|
|
875
881
|
}
|
|
876
882
|
|
|
883
|
+
startPackageRefresh();
|
|
877
884
|
return server;
|
|
878
885
|
}
|
|
879
886
|
|