@bitkyc08/opencodex 2.81.0 → 2.83.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/ocx.mjs +166 -73
- package/gui/dist/assets/App-Ba8uG696.js +54 -0
- package/gui/dist/assets/{Tray-EwAOsP0S.js → Tray-IImbNPRy.js} +1 -1
- package/gui/dist/assets/{index-Brk6F3fn.css → index-BAbcj51J.css} +1 -1
- package/gui/dist/assets/{index-RsoaAxXY.js → index-BHRZdJuv.js} +13 -13
- package/gui/dist/assets/{quota-summary-D2lN8tDm.js → quota-summary-DiJO6sYY.js} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +1 -1
- package/src/adapters/cursor/catalog.ts +2 -2
- package/src/adapters/cursor/effort-map.ts +1 -1
- package/src/adapters/devin/live-models.ts +2 -2
- package/src/adapters/google-truncation.ts +22 -0
- package/src/adapters/google.ts +7 -3
- package/src/adapters/ollama-native.ts +18 -14
- package/src/adapters/openai-responses/compaction-search-history.ts +109 -0
- package/src/adapters/openai-responses/passthrough.ts +11 -4
- package/src/adapters/openai-responses/plaintext-reasoning-notice.ts +85 -0
- package/src/adapters/openai-responses/reasoning.ts +11 -1
- package/src/adapters/openai-responses/tool-output-recovery.ts +20 -0
- package/src/bridge/errors.ts +7 -1
- package/src/claude/agents-inject.ts +14 -3
- package/src/claude/context-windows.ts +22 -20
- package/src/claude/desktop-3p.ts +88 -12
- package/src/claude/desktop-picker.ts +1 -1
- package/src/claude/desktop-profile.ts +2 -1
- package/src/claude/gateway-cache.ts +18 -1
- package/src/claude/inbound.ts +77 -3
- package/src/claude/intercept/cli-catalog.ts +3 -0
- package/src/claude/intercept/picker-ca-persistence.ts +7 -6
- package/src/claude/intercept/picker-models.ts +14 -6
- package/src/claude/intercept/settings.ts +3 -0
- package/src/claude/long-context.ts +66 -0
- package/src/claude/model-info.ts +19 -16
- package/src/claude/outbound.ts +94 -9
- package/src/claude/subagent-model.ts +40 -11
- package/src/cli/account-auth.ts +20 -12
- package/src/cli/agent.ts +9 -5
- package/src/cli/aside-profile-recovery.ts +33 -0
- package/src/cli/capabilities-base.ts +5 -5
- package/src/cli/capabilities-integrations.ts +2 -2
- package/src/cli/cli-path-diagnostics.ts +132 -0
- package/src/cli/desktop-runtime-guidance.ts +65 -0
- package/src/cli/dispatch.ts +5 -0
- package/src/cli/doctor.ts +16 -3
- package/src/cli/index.ts +14 -9
- package/src/cli/integration-aside-sync.ts +2 -1
- package/src/cli/integrations.ts +16 -3
- package/src/cli/resolve.ts +26 -3
- package/src/cli/root.ts +4 -2
- package/src/cli/status.ts +64 -12
- package/src/cli/update-restart-child.ts +4 -2
- package/src/cli/update-restart-home.ts +28 -10
- package/src/cli/update-restart-service-record.ts +116 -0
- package/src/cli/update-restart-supervision.ts +96 -0
- package/src/cli/update-restart.ts +59 -13
- package/src/cli/v2.ts +27 -20
- package/src/cli/version-skew-notice.ts +77 -0
- package/src/clients/config-export/model-metadata.ts +16 -0
- package/src/clients/config-export/zcode-store.ts +46 -11
- package/src/clients/config-export/zcode.ts +3 -5
- package/src/clients/config-export.ts +1 -1
- package/src/clients/lazycodex.ts +1 -1
- package/src/clients/omo-role-models.ts +32 -7
- package/src/codex/auth-api/account-selection.ts +107 -0
- package/src/codex/auth-api/pool-mode-gate.ts +4 -5
- package/src/codex/auth-api/routes.ts +10 -49
- package/src/codex/auth-context.ts +28 -4
- package/src/codex/autostart-health.ts +12 -3
- package/src/codex/catalog/build-entries.ts +18 -0
- package/src/codex/catalog/bundled.ts +155 -26
- package/src/codex/catalog/combo-member.ts +3 -2
- package/src/codex/catalog/effort.ts +26 -5
- package/src/codex/catalog/gather-capture.ts +5 -0
- package/src/codex/catalog/model-hints.ts +15 -8
- package/src/codex/catalog/model-visibility.ts +3 -1
- package/src/codex/catalog/native-models.ts +1 -1
- package/src/codex/catalog/operator-tiers.ts +38 -0
- package/src/codex/catalog/provider-models.ts +68 -35
- package/src/codex/catalog/retained-sync.ts +7 -5
- package/src/codex/catalog/routed-gather.ts +15 -1
- package/src/codex/catalog-auto-refresh-sources.ts +3 -4
- package/src/codex/catalog-auto-refresh.ts +19 -0
- package/src/codex/catalog.ts +1 -1
- package/src/codex/codex-home-owner.ts +3 -0
- package/src/codex/config-write-lock.ts +333 -0
- package/src/codex/convergence.ts +2 -1
- package/src/codex/desktop-app/darwin.ts +1 -1
- package/src/codex/desktop-app/handoff.ts +1 -1
- package/src/codex/desktop-app/linux.ts +1 -1
- package/src/codex/desktop-app/lock.ts +1 -1
- package/src/codex/desktop-app/types.ts +1 -1
- package/src/codex/desktop-app/windows.ts +1 -1
- package/src/codex/desktop-app-restart.ts +1 -1
- package/src/codex/features.ts +257 -200
- package/src/codex/inject/config-write-section.ts +34 -0
- package/src/codex/inject/multi-agent-v2.ts +14 -6
- package/src/codex/inject/remove.ts +36 -11
- package/src/codex/inject/restore.ts +85 -20
- package/src/codex/inject-coordination.ts +15 -6
- package/src/codex/inject.ts +62 -20
- package/src/codex/journal.ts +62 -17
- package/src/codex/main-account-cache.ts +43 -0
- package/src/codex/main-account.ts +6 -7
- package/src/codex/paths.ts +2 -0
- package/src/codex/prepared-home.ts +18 -0
- package/src/codex/prompt-journal.ts +25 -12
- package/src/codex/prompt-layers.ts +49 -26
- package/src/codex/prompt-lock-claim.ts +134 -0
- package/src/codex/prompt-lock-evidence.ts +26 -0
- package/src/codex/prompt-lock-io.ts +16 -0
- package/src/codex/prompt-lock-owner.ts +130 -0
- package/src/codex/prompt-lock.ts +84 -53
- package/src/codex/routing/failure-window.ts +146 -0
- package/src/codex/routing/selection.ts +4 -1
- package/src/codex/routing.ts +13 -17
- package/src/codex/runtime.ts +106 -18
- package/src/codex/shim.ts +45 -19
- package/src/combos/jev-service-exchange.ts +281 -0
- package/src/combos/jev.ts +34 -220
- package/src/config/atomic-write.ts +1 -1
- package/src/config/codex-ws-reuse-setting.ts +10 -0
- package/src/config/live-reconcile.ts +17 -0
- package/src/config/load-degrade.ts +8 -0
- package/src/config/rebase-provenance.ts +11 -0
- package/src/config/schema/config-schema.ts +4 -0
- package/src/generated/compatibility-version.json +375 -227
- package/src/generated/model-metadata.ts +2 -2
- package/src/grok/inject.ts +276 -206
- package/src/grok/reset-coupon-ledger.ts +223 -57
- package/src/grok/reset-coupons.ts +13 -6
- package/src/grok/sync.ts +2 -1
- package/src/images/loop.ts +29 -11
- package/src/integrations/aside-profiles.ts +10 -3
- package/src/integrations/catalog-refresh.ts +18 -2
- package/src/integrations/cursor-detect.ts +1 -1
- package/src/integrations/writer.ts +9 -3
- package/src/lib/bun-path-runtime.d.mts +32 -0
- package/src/lib/bun-path-runtime.mjs +95 -0
- package/src/lib/bun-runtime-preflight.ts +140 -0
- package/src/lib/desktop-cli-handoff.d.mts +20 -0
- package/src/lib/desktop-cli-handoff.mjs +92 -0
- package/src/lib/desktop-cli-record.d.mts +29 -0
- package/src/lib/desktop-cli-record.mjs +102 -0
- package/src/lib/errors.ts +25 -0
- package/src/lib/redact.ts +2 -2
- package/src/lib/request-execution-budget.ts +8 -0
- package/src/lib/synced-state-location.ts +46 -6
- package/src/lib/test-home-guard.ts +87 -1
- package/src/lib/translator-budget.ts +17 -4
- package/src/lib/windows-atomic-replace.ts +38 -16
- package/src/lib/windows-elevation.ts +6 -0
- package/src/lib/windows-secret-acl.ts +92 -25
- package/src/lib/winsw.ts +2 -3
- package/src/oauth/anthropic-account-refusal.ts +39 -1
- package/src/oauth/anthropic-routing.ts +1 -1
- package/src/oauth/anthropic-send-ownership.ts +3 -3
- package/src/oauth/chatgpt.ts +113 -41
- package/src/oauth/index.ts +7 -3
- package/src/oauth/meta-muse-device.ts +1 -1
- package/src/oauth/store.ts +1 -1
- package/src/oauth/types.ts +1 -1
- package/src/providers/anthropic-fast.ts +1 -1
- package/src/providers/anthropic-reset-grant-ledger.ts +1 -1
- package/src/providers/azure-model-metadata.ts +115 -0
- package/src/providers/derive.ts +21 -0
- package/src/providers/devin-cli-authmode-migration.ts +1 -1
- package/src/providers/devin-provider-merge-migration.ts +1 -1
- package/src/providers/github-copilot-auto.ts +169 -0
- package/src/providers/model-presets.ts +1 -1
- package/src/providers/muse-subscription-usage.ts +1 -1
- package/src/providers/quota/vendor-probes-key.ts +127 -17
- package/src/providers/quota.ts +1 -1
- package/src/providers/registry/entries-core.ts +6 -6
- package/src/providers/registry/entries-extended.ts +4 -0
- package/src/providers/registry/model-seeds.ts +3 -3
- package/src/providers/xai-fast-model.ts +1 -1
- package/src/responses/reasoning-envelope.ts +29 -1
- package/src/responses/reasoning-replay-cache.ts +17 -1
- package/src/responses/spill-store.ts +15 -2
- package/src/responses/state/metrics.ts +8 -0
- package/src/responses/state/spill-failure.ts +12 -0
- package/src/responses/state/spill-queue.ts +12 -5
- package/src/responses/state.ts +18 -10
- package/src/responses/task-input.ts +17 -0
- package/src/server/background-lifecycle.ts +27 -0
- package/src/server/chat-completions.ts +1 -0
- package/src/server/chat-native.ts +28 -12
- package/src/server/claude-messages.ts +43 -6
- package/src/server/inbound-body-admission.ts +25 -7
- package/src/server/index/claude-intercept-lifecycle.ts +3 -1
- package/src/server/index/link-listener.ts +30 -17
- package/src/server/index/serve-options.ts +18 -4
- package/src/server/index/spend-ledger-lifecycle.ts +2 -1
- package/src/server/index/startup-warnings.ts +19 -0
- package/src/server/index.ts +2 -1
- package/src/server/lifecycle.ts +31 -4
- package/src/server/management/agent-settings-routes.ts +93 -32
- package/src/server/management/codex-agent-role-routes.ts +4 -2
- package/src/server/management/codex-prompt-routes.ts +1 -0
- package/src/server/management/codex-role-auto-assign.ts +13 -2
- package/src/server/management/config-routes.ts +59 -8
- package/src/server/management/context.ts +2 -2
- package/src/server/management/grok-coupon-routes.ts +277 -52
- package/src/server/management/link-routes.ts +42 -20
- package/src/server/management/route-registry.ts +2 -2
- package/src/server/management/shared.ts +3 -3
- package/src/server/messages-native-oauth.ts +9 -8
- package/src/server/messages-native.ts +49 -13
- package/src/server/messages-response-headers.ts +14 -0
- package/src/server/relay-eager.ts +2 -0
- package/src/server/relay.ts +18 -7
- package/src/server/request-log-conversation.ts +48 -5
- package/src/server/request-log-failure-attribution.ts +3 -0
- package/src/server/request-log-terminal-status.ts +186 -0
- package/src/server/request-log.ts +15 -89
- package/src/server/responses/adapter-continuation.ts +11 -3
- package/src/server/responses/adapter-dispatch.ts +99 -19
- package/src/server/responses/codex-ws-exchange.ts +17 -2
- package/src/server/responses/codex-ws-pool.ts +100 -13
- package/src/server/responses/codex-ws-wire.ts +9 -0
- package/src/server/responses/combo-requested-effort.ts +3 -1
- package/src/server/responses/compact.ts +3 -2
- package/src/server/responses/core-codex-account.ts +22 -1
- package/src/server/responses/core-combo-native.ts +20 -3
- package/src/server/responses/core-combo.ts +22 -14
- package/src/server/responses/core-lifetime.ts +3 -36
- package/src/server/responses/core-opaque-recovery.ts +5 -5
- package/src/server/responses/core-options.ts +5 -0
- package/src/server/responses/core-replay.ts +73 -0
- package/src/server/responses/core.ts +9 -9
- package/src/server/responses/empty-completion-guard.ts +18 -2
- package/src/server/responses/fetch-helpers.ts +26 -3
- package/src/server/responses/jev-model-invoke.ts +1 -1
- package/src/server/responses/passthrough-delivery.ts +24 -2
- package/src/server/responses/passthrough-dispatch.ts +134 -62
- package/src/server/responses/passthrough-execution.ts +3 -3
- package/src/server/responses/request-prepare.ts +4 -0
- package/src/server/responses/request-send-budget.ts +112 -49
- package/src/server/responses/request-transport.ts +125 -5
- package/src/server/responses/response-effects.ts +1 -0
- package/src/server/responses/run-turn-execution.ts +15 -5
- package/src/server/responses/sidecar-execution.ts +30 -16
- package/src/server/responses/sidecar-send-budget.ts +58 -0
- package/src/server/responses/spend-storage-error.ts +33 -0
- package/src/server/responses/ws-upstream.ts +19 -6
- package/src/server/startup-health-cache.ts +2 -1
- package/src/server/workflow-refusal.ts +3 -0
- package/src/service/cli.ts +20 -4
- package/src/service/desktop-command-guard.ts +32 -0
- package/src/service/desktop-startup.ts +66 -40
- package/src/service/desktop-supervision.d.mts +26 -0
- package/src/service/desktop-supervision.mjs +140 -0
- package/src/service/managing-cli.ts +1 -0
- package/src/service/orchestration.ts +46 -20
- package/src/service/repair.ts +25 -9
- package/src/service/state.ts +4 -3
- package/src/service/systemd.ts +17 -8
- package/src/service/windows-ops.ts +61 -22
- package/src/storage/policy-job.ts +5 -1
- package/src/storage/policy-worker.ts +3 -1
- package/src/storage/policy.ts +9 -0
- package/src/types/config.ts +21 -0
- package/src/types/request.ts +10 -0
- package/src/types/wire.ts +1 -0
- package/src/update/index.ts +57 -9
- package/src/update/restart-ownership.ts +10 -3
- package/src/update/runtime-ownership.d.mts +6 -0
- package/src/update/runtime-ownership.mjs +23 -7
- package/src/usage/expected-prices.ts +1 -1
- package/src/web-search/loop.ts +5 -2
- package/gui/dist/assets/App-7yhoys8a.js +0 -54
package/src/claude/inbound.ts
CHANGED
|
@@ -281,7 +281,7 @@ function userMessageToItems(content: unknown, input: Rec[], elide: SkillElisionC
|
|
|
281
281
|
pushUserMessage(input, pending);
|
|
282
282
|
}
|
|
283
283
|
|
|
284
|
-
function assistantMessageToItems(content: unknown, input: Rec[], budget: TranslatorBudget): void {
|
|
284
|
+
function assistantMessageToItems(content: unknown, input: Rec[], budget: TranslatorBudget, requestedModel: string, nativeReasoningReplay: Map<string, string>): void {
|
|
285
285
|
if (typeof content === "string") {
|
|
286
286
|
if (content.length > 0) input.push({ type: "message", role: "assistant", content: [{ type: "output_text", text: content }] });
|
|
287
287
|
return;
|
|
@@ -314,6 +314,24 @@ function assistantMessageToItems(content: unknown, input: Rec[], budget: Transla
|
|
|
314
314
|
const owned = decodeReasoningEnvelope(signature, budget);
|
|
315
315
|
if (!owned) throw new AnthropicRequestError("malformed ocxr1 reasoning signature");
|
|
316
316
|
if (Object.hasOwn(owned, "sig")) throw new AnthropicRequestError("OpenCodex reasoning continuity cannot be replayed as an Anthropic signature");
|
|
317
|
+
if (owned.nat) {
|
|
318
|
+
// The provider's own blob goes back only to the model that minted it; any other
|
|
319
|
+
// model gets the visible text alone, as before this envelope field existed.
|
|
320
|
+
const nat = owned.nat.model === requestedModel ? owned.nat : undefined;
|
|
321
|
+
if (nat) {
|
|
322
|
+
const previous = nativeReasoningReplay.get(nat.enc);
|
|
323
|
+
nativeReasoningReplay.set(nat.enc, previous === undefined || previous === nat.tag ? nat.tag : "");
|
|
324
|
+
}
|
|
325
|
+
if (nat) budget.chargeRetained(2 * nat.enc.length, { kind: "reasoning" });
|
|
326
|
+
if (thinking.length === 0 && !nat) break;
|
|
327
|
+
input.push({
|
|
328
|
+
type: "reasoning",
|
|
329
|
+
id: nat?.id ?? `rs_${crypto.randomUUID().replace(/-/g, "")}`,
|
|
330
|
+
summary: thinking.length > 0 ? [{ type: "summary_text", text: thinking }] : [],
|
|
331
|
+
...(nat ? { encrypted_content: nat.enc } : {}),
|
|
332
|
+
});
|
|
333
|
+
break;
|
|
334
|
+
}
|
|
317
335
|
}
|
|
318
336
|
const encrypted = signature.length === 0 ? undefined : signature.startsWith(OCX_REASONING_PREFIX) ? signature : encodeReasoningEnvelope({ sig: signature }, budget);
|
|
319
337
|
if (encrypted) budget.chargeRetained(2 * encrypted.length, { kind: "reasoning" });
|
|
@@ -355,6 +373,7 @@ export type ClaudeCacheKeySource = "metadata" | "system" | null;
|
|
|
355
373
|
export interface ClaudeInboundTranslation {
|
|
356
374
|
body: Rec;
|
|
357
375
|
cacheKeySource: ClaudeCacheKeySource;
|
|
376
|
+
nativeReasoningReplay: ReadonlyMap<string, string>;
|
|
358
377
|
}
|
|
359
378
|
|
|
360
379
|
/**
|
|
@@ -400,6 +419,7 @@ function translateAnthropicRequest(
|
|
|
400
419
|
}
|
|
401
420
|
|
|
402
421
|
const input: Rec[] = [];
|
|
422
|
+
const nativeReasoningReplay = new Map<string, string>();
|
|
403
423
|
const systemParts: string[] = [];
|
|
404
424
|
const topLevelSystem = systemToInstructions(raw.system);
|
|
405
425
|
if (topLevelSystem !== undefined) systemParts.push(topLevelSystem);
|
|
@@ -411,7 +431,7 @@ function translateAnthropicRequest(
|
|
|
411
431
|
for (const msg of raw.messages) {
|
|
412
432
|
if (!isRec(msg)) throw new AnthropicRequestError("each message must be an object");
|
|
413
433
|
if (msg.role === "user") userMessageToItems(msg.content, input, elide);
|
|
414
|
-
else if (msg.role === "assistant") assistantMessageToItems(msg.content, input, budget);
|
|
434
|
+
else if (msg.role === "assistant") assistantMessageToItems(msg.content, input, budget, raw.model, nativeReasoningReplay);
|
|
415
435
|
else if (msg.role === "system") {
|
|
416
436
|
const text = systemMessageText(msg.content);
|
|
417
437
|
// Keep it where the client put it. `developer` is first-class in the Responses
|
|
@@ -531,7 +551,61 @@ function translateAnthropicRequest(
|
|
|
531
551
|
reasoning.effort = effortForThinkingBudget(thinking.budget_tokens);
|
|
532
552
|
}
|
|
533
553
|
body.reasoning = reasoning;
|
|
554
|
+
// store:false returns a provider's encrypted reasoning only on request. The outbound
|
|
555
|
+
// envelope (`nat`) carries it to the client so the next turn can hand it back.
|
|
556
|
+
body.include = ["reasoning.encrypted_content"];
|
|
534
557
|
}
|
|
535
558
|
|
|
536
|
-
return { body, cacheKeySource };
|
|
559
|
+
return { body, cacheKeySource, nativeReasoningReplay };
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Remove proxy-owned native blobs before sending a Messages body to native Anthropic.
|
|
564
|
+
*
|
|
565
|
+
* Every decode, and every re-encode on top of it, is reserved against the request's translator
|
|
566
|
+
* budget first (eight bytes per code unit, as `decodeReasoningEnvelope` does), and every new
|
|
567
|
+
* signature is charged as a retained copy, so a body of near-limit envelopes cannot allocate past the per-turn bound.
|
|
568
|
+
* Throws TranslatorBudgetExceededError when it would; callers answer 413.
|
|
569
|
+
*/
|
|
570
|
+
export function nativeAnthropicProjection(body: Rec, budget: TranslatorBudget): Rec {
|
|
571
|
+
if (!Array.isArray(body.messages)) return body;
|
|
572
|
+
let changed = false;
|
|
573
|
+
const messages = body.messages.flatMap(message => {
|
|
574
|
+
if (!isRec(message) || message.role !== "assistant" || !Array.isArray(message.content)) return [message];
|
|
575
|
+
let localChanged = false;
|
|
576
|
+
const content = message.content.flatMap(block => {
|
|
577
|
+
if (!isRec(block) || block.type !== "thinking" || typeof block.signature !== "string"
|
|
578
|
+
|| !block.signature.startsWith(OCX_REASONING_PREFIX)) return [block];
|
|
579
|
+
// The decoded object stays alive while it is re-encoded, so its reservation is held until
|
|
580
|
+
// the new signature exists; the encode copies get their own reservation on top of it.
|
|
581
|
+
const decodeReservation = budget.reserveTransient(8 * block.signature.length, { kind: "reasoning" });
|
|
582
|
+
try {
|
|
583
|
+
let decoded: unknown;
|
|
584
|
+
try { decoded = JSON.parse(Buffer.from(block.signature.slice(OCX_REASONING_PREFIX.length), "base64").toString("utf8")); }
|
|
585
|
+
catch { decoded = undefined; }
|
|
586
|
+
if (!isRec(decoded)) { localChanged = true; return []; }
|
|
587
|
+
if (!Object.hasOwn(decoded, "nat")) return [block];
|
|
588
|
+
localChanged = true;
|
|
589
|
+
const { nat: _nat, ...rest } = decoded;
|
|
590
|
+
if (!rest.sig && !rest.red && !rest.krc && !(typeof rest.txt === "string" && rest.txt.length > 0)) return [];
|
|
591
|
+
// The re-encoded envelope is never larger than the decoded one (a field was removed), so a
|
|
592
|
+
// decode-sized reservation bounds the stringify, buffer and base64 copies before they exist.
|
|
593
|
+
const encodeReservation = budget.reserveTransient(8 * block.signature.length, { kind: "reasoning" });
|
|
594
|
+
let signature: string;
|
|
595
|
+
try {
|
|
596
|
+
signature = OCX_REASONING_PREFIX + Buffer.from(JSON.stringify(rest), "utf8").toString("base64");
|
|
597
|
+
} finally {
|
|
598
|
+
encodeReservation.release();
|
|
599
|
+
}
|
|
600
|
+
budget.chargeRetained(2 * signature.length, { kind: "reasoning" });
|
|
601
|
+
return [{ ...block, signature }];
|
|
602
|
+
} finally {
|
|
603
|
+
decodeReservation.release();
|
|
604
|
+
}
|
|
605
|
+
});
|
|
606
|
+
if (!localChanged) return [message];
|
|
607
|
+
changed = true;
|
|
608
|
+
return content.length ? [{ ...message, content }] : [];
|
|
609
|
+
});
|
|
610
|
+
return changed ? { ...body, messages } : body;
|
|
537
611
|
}
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { readdirSync, unlinkSync } from "node:fs";
|
|
16
16
|
import { join } from "node:path";
|
|
17
|
+
import { assertNotRealClaudeConfigUnderTest } from "../../lib/test-home-guard";
|
|
17
18
|
import type { ClaudeFirstPartyDesired } from "../first-party-settings";
|
|
18
19
|
import { classifyInterceptClient } from "./client-class";
|
|
19
20
|
import { BOOTSTRAP_MAX_DECODED_BYTES, injectPickerModels, type PickerModelEntry } from "./picker-bootstrap";
|
|
@@ -150,6 +151,8 @@ export async function rewriteCliCatalogResponse(
|
|
|
150
151
|
*/
|
|
151
152
|
export function invalidateClaudeCodeServedCatalog(claudeDir: string): number {
|
|
152
153
|
const dir = join(claudeDir, "cache", "model-catalog");
|
|
154
|
+
// Outside the catch below: an armed test process must not prune the real Claude cache (#6775).
|
|
155
|
+
assertNotRealClaudeConfigUnderTest(claudeDir, dir);
|
|
153
156
|
let names: string[];
|
|
154
157
|
try { names = readdirSync(dir); } catch { return 0; } // no-excuse-ok: catch -- no cache directory means nothing to drop.
|
|
155
158
|
let removed = 0;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { X509Certificate } from "node:crypto";
|
|
2
|
-
import { constants, closeSync, fstatSync, lstatSync, openSync, readSync, realpathSync, rmSync, writeFileSync, type
|
|
2
|
+
import { constants, closeSync, fstatSync, lstatSync, openSync, readSync, realpathSync, rmSync, writeFileSync, type BigIntStats } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import {
|
|
5
5
|
decodePickerCaCredential, openPickerCaCredential, pickerCaConfigId, readPickerCaCredential,
|
|
@@ -21,18 +21,19 @@ export function canonicalPickerConfigDir(configDir: string): string {
|
|
|
21
21
|
|
|
22
22
|
/** Public records still cannot follow symlinks or accept oversized/malformed recovery data. */
|
|
23
23
|
function readState(path: string, configId: string, initialization: boolean): Initialization | AuthorityMetadata | null {
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
// BigInt stats: a Windows file ID exceeds 2^53, so Number ino loses low bits and two files can compare equal.
|
|
25
|
+
let expected: BigIntStats;
|
|
26
|
+
try { expected = lstatSync(path, { bigint: true }); }
|
|
26
27
|
catch (error) { if ((error as NodeJS.ErrnoException).code === "ENOENT") return null; throw new Error("picker_ca_metadata_unsafe"); }
|
|
27
28
|
if (expected.isSymbolicLink()) throw new Error("picker_ca_metadata_unsafe");
|
|
28
29
|
let fd: number;
|
|
29
30
|
try { fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK); }
|
|
30
31
|
catch (error) { if ((error as NodeJS.ErrnoException).code === "ENOENT") return null; throw new Error("picker_ca_metadata_unsafe"); }
|
|
31
32
|
try {
|
|
32
|
-
const stat = fstatSync(fd);
|
|
33
|
+
const stat = fstatSync(fd, { bigint: true });
|
|
33
34
|
if (stat.dev !== expected.dev || stat.ino !== expected.ino) throw new Error("picker_ca_metadata_unsafe");
|
|
34
|
-
if (!stat.isFile() || stat.nlink !==
|
|
35
|
-
|| (process.platform !== "win32" && stat.uid !== process.getuid!())) throw new Error();
|
|
35
|
+
if (!stat.isFile() || stat.nlink !== 1n || stat.size > BigInt(MAX_PUBLIC_STATE_BYTES)
|
|
36
|
+
|| (process.platform !== "win32" && stat.uid !== BigInt(process.getuid!()))) throw new Error();
|
|
36
37
|
const bytes = Buffer.alloc(MAX_PUBLIC_STATE_BYTES + 1);
|
|
37
38
|
let used = 0;
|
|
38
39
|
while (used < bytes.length) {
|
|
@@ -7,7 +7,8 @@ import { dirname } from "node:path";
|
|
|
7
7
|
import { nativeOpenAiContextWindow, type NativeContextLimitsInput } from "../../codex/catalog";
|
|
8
8
|
import type { OcxClaudeDesktopProfile } from "../../types";
|
|
9
9
|
import { aliasForRoute, claudeCodeNativeAlias } from "../alias";
|
|
10
|
-
import { AUTO_CONTEXT_OFF, stripOneMillionMarker, withOneMillionMarker } from "../context-windows";
|
|
10
|
+
import { AUTO_CONTEXT_OFF, stripOneMillionMarker, UNPAIRED_AUTO_CONTEXT, withOneMillionMarker, type AutoContextMode } from "../context-windows";
|
|
11
|
+
import { isAnthropicClaudeRoute } from "../long-context";
|
|
11
12
|
import { activeDesktop3pAlias, displayModelId, resolveDesktop3pAlias, type Desktop3pRoutedModel } from "../desktop-3p";
|
|
12
13
|
import { reconcileDesktopProfile, renderDesktopProfile, type DesktopProfileModel } from "../desktop-profile";
|
|
13
14
|
import type { PickerModelEntry } from "./picker-bootstrap";
|
|
@@ -17,6 +18,8 @@ export interface PickerRouteInput {
|
|
|
17
18
|
routedModels: Desktop3pRoutedModel[];
|
|
18
19
|
profile?: OcxClaudeDesktopProfile;
|
|
19
20
|
nativeContextCap?: NativeContextLimitsInput;
|
|
21
|
+
/** resolveAutoContext(config.claudeCode); only its contextAccounting "200k" opt-in matters here. */
|
|
22
|
+
auto?: AutoContextMode;
|
|
20
23
|
}
|
|
21
24
|
|
|
22
25
|
/** Candidate routes in the gateway profile's order and with its labels. */
|
|
@@ -46,9 +49,14 @@ function splitRoute(route: string): { provider: string; id: string } {
|
|
|
46
49
|
return { provider: route.slice(0, slash), id: route.slice(slash + 1) };
|
|
47
50
|
}
|
|
48
51
|
|
|
49
|
-
/**
|
|
50
|
-
|
|
51
|
-
|
|
52
|
+
/**
|
|
53
|
+
* Desktop runners do not inherit the proxy's compaction env, so a long window (>= the default
|
|
54
|
+
* compact window) is marked on the strength of the `prompt is too long` recovery instead
|
|
55
|
+
* (devlog/_plan/261009_claude_1m_default/020). Real Anthropic models need a genuine 1M.
|
|
56
|
+
*/
|
|
57
|
+
function pickerSelector(alias: string, contextWindow: number | undefined, provider: string, id: string, auto?: AutoContextMode): string {
|
|
58
|
+
const mode = auto?.accounting200k ? auto : isAnthropicClaudeRoute(provider, id) ? AUTO_CONTEXT_OFF : UNPAIRED_AUTO_CONTEXT;
|
|
59
|
+
return withOneMillionMarker(alias, contextWindow === undefined ? {} : { [alias]: contextWindow }, mode)!;
|
|
52
60
|
}
|
|
53
61
|
|
|
54
62
|
export function buildPickerModels(input: PickerRouteInput): PickerModelEntry[] {
|
|
@@ -61,7 +69,7 @@ export function buildPickerModels(input: PickerRouteInput): PickerModelEntry[] {
|
|
|
61
69
|
const alias = provider === "native" ? claudeCodeNativeAlias(id) : aliasForRoute(provider, id);
|
|
62
70
|
if (!alias || seen.has(alias)) continue;
|
|
63
71
|
seen.add(alias);
|
|
64
|
-
out.push({ id: pickerSelector(alias, model.contextWindow), name: model.label,
|
|
72
|
+
out.push({ id: pickerSelector(alias, model.contextWindow, provider, id, input.auto), name: model.label,
|
|
65
73
|
...(model.contextWindow === undefined ? {} : { contextWindow: model.contextWindow }) });
|
|
66
74
|
}
|
|
67
75
|
return out;
|
|
@@ -81,7 +89,7 @@ export function buildCliPickerModels(input: PickerRouteInput): PickerModelEntry[
|
|
|
81
89
|
const alias = activeDesktop3pAlias(provider, id);
|
|
82
90
|
if (seen.has(alias) || resolveDesktop3pAlias(alias) !== model.route) continue;
|
|
83
91
|
seen.add(alias);
|
|
84
|
-
out.push({ id: pickerSelector(alias, model.contextWindow), name: model.label,
|
|
92
|
+
out.push({ id: pickerSelector(alias, model.contextWindow, provider, id, input.auto), name: model.label,
|
|
85
93
|
description: `opencodex · ${model.route}`, route: model.route,
|
|
86
94
|
...(model.contextWindow === undefined ? {} : { contextWindow: model.contextWindow }) });
|
|
87
95
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { mkdirSync, readFileSync } from "node:fs";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { atomicWriteFile } from "../../config/atomic-write";
|
|
4
|
+
import { assertNotRealClaudeConfigUnderTest } from "../../lib/test-home-guard";
|
|
4
5
|
import { claudeConfigDir } from "../auth-detect";
|
|
5
6
|
import { invalidateClaudeCodeServedCatalog } from "./cli-catalog";
|
|
6
7
|
|
|
@@ -112,6 +113,8 @@ export function inspectClaudeInterceptSettings(
|
|
|
112
113
|
}
|
|
113
114
|
|
|
114
115
|
function writeSettings(path: string, doc: SettingsDoc): void {
|
|
116
|
+
// An armed test process must not rewrite the real Claude settings.json (#6775).
|
|
117
|
+
assertNotRealClaudeConfigUnderTest(dirname(path), path);
|
|
115
118
|
mkdirSync(dirname(path), { recursive: true });
|
|
116
119
|
// The managed env embeds the proxy token, so the file must stay owner-only:
|
|
117
120
|
// atomicWriteFile applies the real NTFS ACL on Windows where chmod is a no-op.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Long-context thresholds shared by every Claude surface
|
|
3
|
+
* (devlog/_plan/261009_claude_1m_default/020_long_context_eligibility.md).
|
|
4
|
+
*
|
|
5
|
+
* A leaf module: desktop-3p.ts and desktop-profile.ts need the predicate, and context-windows.ts
|
|
6
|
+
* already imports desktop-3p.ts, so the predicate cannot live there.
|
|
7
|
+
*/
|
|
8
|
+
import { isAnthropicInstanceId } from "../providers/anthropic-instance-id";
|
|
9
|
+
|
|
10
|
+
export const ONE_MILLION = 1_000_000;
|
|
11
|
+
|
|
12
|
+
/** A window at or below this is Claude Code's own default accounting; widening it gains nothing. */
|
|
13
|
+
export const AUTO_CONTEXT_FLOOR = 200_000;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Auto-context defaults (devlog 260712 020, user-approved).
|
|
17
|
+
*
|
|
18
|
+
* The compact window is the token count at which Claude Code starts compacting, and it is
|
|
19
|
+
* also the floor `shouldMarkOneMillion` uses — a model may only carry the marker if it can
|
|
20
|
+
* host this window. 350,000 was chosen when the widest native row advertised 372,000.
|
|
21
|
+
*
|
|
22
|
+
* It now matches the auto-compaction limit the Codex catalog ships for the same models
|
|
23
|
+
* (`nativeAutoCompactLimit`: 829,800 against the 922,000 native window). Leaving the two
|
|
24
|
+
* apart meant one model compacting at 350k under Claude Code and at 829,800 under Codex.
|
|
25
|
+
* The value stays clear of the measured 922,000 ceiling by ~92k, so compaction still has
|
|
26
|
+
* room to run before the upstream refuses.
|
|
27
|
+
*/
|
|
28
|
+
export const AUTO_COMPACT_WINDOW_DEFAULT = 829_800;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Whether a window earns the 1M selection on a surface whose Claude Code runner may not inherit
|
|
32
|
+
* CLAUDE_CODE_AUTO_COMPACT_WINDOW (Desktop pickers, discovery, Desktop 3P, generated subagents).
|
|
33
|
+
* A window >= 1M always does. A smaller one does when it can host the default compact window, so
|
|
34
|
+
* a runner that has the variable compacts in time, and one that lacks it overflows into the
|
|
35
|
+
* `prompt is too long` envelope (anthropicErrorBody) and compacts reactively. The floor is fixed
|
|
36
|
+
* on purpose: a custom compact window must not re-admit a 372k route (#854).
|
|
37
|
+
*/
|
|
38
|
+
export function isLongContextWindow(window: number | undefined): boolean {
|
|
39
|
+
if (typeof window !== "number" || window <= 0) return false;
|
|
40
|
+
return window >= ONE_MILLION || (window > AUTO_CONTEXT_FLOOR && window >= AUTO_COMPACT_WINDOW_DEFAULT);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* A Claude model on an Anthropic-named route. Claude windows are 200k or a genuine 1M and a bare
|
|
45
|
+
* id rides the native passthrough, so the long-window rule never widens one: a Claude row listed
|
|
46
|
+
* under 1M is a capped one, and on a runner without the compaction env it keeps the 200k
|
|
47
|
+
* accounting it had before long windows were widened. Keyed on the model id as well, so a
|
|
48
|
+
* configured gateway that only shares the name `anthropic2` and serves other models keeps the
|
|
49
|
+
* long-window rule. A custom `anthropic2` gateway serving Claude below 1M is treated like a pool
|
|
50
|
+
* row on these surfaces, which is the pre-existing (unwidened) behavior.
|
|
51
|
+
*/
|
|
52
|
+
export function isAnthropicClaudeRoute(provider: string, modelId: string): boolean {
|
|
53
|
+
return isAnthropicInstanceId(provider) && modelId.startsWith("claude-");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Desktop 3P / picker / discovery / dashboard 1M eligibility for one provider route. */
|
|
57
|
+
export function claudeSurfaceSupportsOneMillion(provider: string, modelId: string, window: number | undefined): boolean {
|
|
58
|
+
if (isAnthropicClaudeRoute(provider, modelId)) return typeof window === "number" && window >= ONE_MILLION;
|
|
59
|
+
return isLongContextWindow(window);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Same, for a `provider/id` route string. */
|
|
63
|
+
export function routeSupportsOneMillion(route: string, window: number | undefined): boolean {
|
|
64
|
+
const slash = route.indexOf("/");
|
|
65
|
+
return claudeSurfaceSupportsOneMillion(route.slice(0, slash), route.slice(slash + 1), window);
|
|
66
|
+
}
|
package/src/claude/model-info.ts
CHANGED
|
@@ -19,7 +19,8 @@ import { orderForModelPicker, catalogModelEfforts, nativeEffortClamp, nativeOpen
|
|
|
19
19
|
import { claudeCodeAlias, claudeCodeNativeAlias } from "./alias";
|
|
20
20
|
import { cursorFastIdFor } from "../adapters/cursor/catalog";
|
|
21
21
|
import { desktop3pAlias } from "./desktop-3p";
|
|
22
|
-
import { AUTO_CONTEXT_OFF, type AutoContextMode } from "./context-windows";
|
|
22
|
+
import { AUTO_CONTEXT_OFF, shouldMarkOneMillion, UNPAIRED_AUTO_CONTEXT, type AutoContextMode } from "./context-windows";
|
|
23
|
+
import { isAnthropicClaudeRoute } from "./long-context";
|
|
23
24
|
|
|
24
25
|
const MODEL_INFO_CREATED_AT = "2026-01-01T00:00:00Z";
|
|
25
26
|
const ANTHROPIC_EFFORT_RUNGS = new Set(["low", "medium", "high", "xhigh", "max"]);
|
|
@@ -144,23 +145,25 @@ export function buildAnthropicModelInfos(
|
|
|
144
145
|
: aliasForRoute(m.provider, m.id);
|
|
145
146
|
}),
|
|
146
147
|
]);
|
|
147
|
-
// [1m] picker variant (devlog 260712 B1
|
|
148
|
-
// carrying the marker (2.1.207 binary: /\[1m\]/i
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
//
|
|
148
|
+
// [1m] picker variant (devlog 260712 B1; widened in devlog/_plan/261009_claude_1m_default/020):
|
|
149
|
+
// Claude Code accounts exactly 1M for ids carrying the marker (2.1.207 binary: /\[1m\]/i -> 1e6,
|
|
150
|
+
// compaction preserved). A discovery client may lack CLAUDE_CODE_AUTO_COMPACT_WINDOW, so the
|
|
151
|
+
// rule is the fixed unpaired one (long-context.ts), not the configurable launch compact window:
|
|
152
|
+
// a custom 350k window must not re-admit a 372k route (#854). The server's auto mode only turns
|
|
153
|
+
// widening off (autoContext: false / maxContextTokens), falling back to >= 1M. Base and Fast
|
|
154
|
+
// rows stay unmarked: this list is a choice list. Guards (audit R1#11): same dedupe set, never
|
|
155
|
+
// double-suffix.
|
|
156
|
+
const variantMode = auto.enabled ? UNPAIRED_AUTO_CONTEXT : AUTO_CONTEXT_OFF;
|
|
153
157
|
const push1mVariant = (
|
|
154
158
|
base: AnthropicModelInfo,
|
|
155
159
|
contextWindow: number | undefined,
|
|
156
160
|
maxInputTokens?: number,
|
|
157
161
|
selectorId?: string,
|
|
162
|
+
mode: AutoContextMode = variantMode,
|
|
158
163
|
) => {
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
|
|
162
|
-
// Claude Code over-fill it (the #854 defect).
|
|
163
|
-
if (contextWindow === undefined || contextWindow < ONE_MILLION) return;
|
|
164
|
+
// A window >= 1M always earns the row; a long window earns it under the unpaired rule,
|
|
165
|
+
// because an overflow is answered as `prompt is too long`, which Claude Code compacts on.
|
|
166
|
+
if (!shouldMarkOneMillion(contextWindow, mode)) return;
|
|
164
167
|
if (base.id.includes("[1m]")) return;
|
|
165
168
|
const id = selectorId ?? `${base.id}[1m]`;
|
|
166
169
|
if (seen.has(id)) return;
|
|
@@ -170,9 +173,9 @@ export function buildAnthropicModelInfos(
|
|
|
170
173
|
// (measured — see devlog/_plan/260817_native_gpt56_1m_context/001_measurement_evidence.md).
|
|
171
174
|
// Advertising the flat 1e6 there would invite mid-session context_length_exceeded, so the
|
|
172
175
|
// variant reports whichever of the two is smaller.
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
+
// A long window under 1M must not be advertised as 1e6 input either.
|
|
177
|
+
const ceiling = typeof maxInputTokens === "number" && maxInputTokens > 0 ? maxInputTokens : contextWindow;
|
|
178
|
+
const advertised = typeof ceiling === "number" && ceiling > 0 ? Math.min(ONE_MILLION, ceiling) : ONE_MILLION;
|
|
176
179
|
out.push({ ...base, id, display_name: `${base.display_name} · 1M`, max_input_tokens: advertised });
|
|
177
180
|
};
|
|
178
181
|
/**
|
|
@@ -251,7 +254,7 @@ export function buildAnthropicModelInfos(
|
|
|
251
254
|
&& listedModelId.startsWith("claude-fable-")
|
|
252
255
|
? `${claudeCodeNativeAlias(listedModelId)}[1m]`
|
|
253
256
|
: undefined;
|
|
254
|
-
push1mVariant(info, m.contextWindow, routedMaxInput, oneMillionSelector);
|
|
257
|
+
push1mVariant(info, m.contextWindow, routedMaxInput, oneMillionSelector, isAnthropicClaudeRoute(m.provider, m.id) ? AUTO_CONTEXT_OFF : variantMode);
|
|
255
258
|
// The whole model is passed, not a (provider, id) pair: a combo row lives in its own
|
|
256
259
|
// namespace with no config.providers entry, so the caller classifies it from the
|
|
257
260
|
// aggregated supportsServiceTier the row already carries.
|
package/src/claude/outbound.ts
CHANGED
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
type TranslatorBudget,
|
|
21
21
|
} from "../lib/translator-budget";
|
|
22
22
|
import { sseFieldOffset, sseFieldValue } from "../lib/sse-decoder";
|
|
23
|
-
import { decodeReasoningEnvelope, encodeReasoningEnvelope } from "../responses/reasoning-envelope";
|
|
23
|
+
import { decodeReasoningEnvelope, encodeReasoningEnvelope, OCX_REASONING_PREFIX } from "../responses/reasoning-envelope";
|
|
24
24
|
|
|
25
25
|
type Rec = Record<string, unknown>;
|
|
26
26
|
|
|
@@ -70,8 +70,67 @@ export function anthropicErrorType(status: number): string {
|
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Claude Code compacts reactively only when an oversized-prompt refusal is worded the way the
|
|
75
|
+
* Anthropic API words it: it looks for `prompt is too long` (or `input is too long for requested
|
|
76
|
+
* model`) in the message and reads `prompt is too long: <actual> tokens > <limit> maximum` for the
|
|
77
|
+
* gap (2.1.288 binary). Routed providers word the same refusal differently, so a
|
|
78
|
+
* context_length_exceeded envelope is rewritten into that form. Counts are carried over only when
|
|
79
|
+
* the upstream states both of them; otherwise the original text is prefixed and no number is
|
|
80
|
+
* guessed. Without this the client ends the turn on a fatal error instead of compacting
|
|
81
|
+
* (devlog/_plan/261009_claude_1m_default/010).
|
|
82
|
+
*/
|
|
83
|
+
const CLAUDE_PROMPT_TOO_LONG_RE = /prompt is too long|input is too long for requested model/i;
|
|
84
|
+
const STATED_CONTEXT_COUNTS_RE = /maximum context length is (\d+) tokens[\s\S]*?(?:resulted in|you requested) (\d+) tokens/i;
|
|
85
|
+
|
|
86
|
+
export function claudePromptTooLongMessage(message: string): string {
|
|
87
|
+
if (CLAUDE_PROMPT_TOO_LONG_RE.test(message)) return message;
|
|
88
|
+
const counts = STATED_CONTEXT_COUNTS_RE.exec(message);
|
|
89
|
+
if (counts) return `prompt is too long: ${counts[2]} tokens > ${counts[1]} maximum`;
|
|
90
|
+
return `prompt is too long: ${message}`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Provider wordings of an oversized-input refusal, for lanes that relay a provider's own error text. */
|
|
94
|
+
const CONTEXT_OVERFLOW_TEXT_RE = /prompt is too long|input is too long|context window|context length|maximum context|too many tokens|model token limit/i;
|
|
95
|
+
|
|
96
|
+
export function isContextOverflowText(message: string): boolean {
|
|
97
|
+
return CONTEXT_OVERFLOW_TEXT_RE.test(message);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* SSE payload rewrite for the native Messages lane: an `error` event that refuses an oversized
|
|
102
|
+
* input gets the Anthropic wording; every other payload passes through byte-identical. The type
|
|
103
|
+
* gate keeps a rate limit that happens to mention the context window untouched.
|
|
104
|
+
*/
|
|
105
|
+
export function claudeOverflowSsePayload(payload: string): string {
|
|
106
|
+
if (!payload.includes("\"error\"")) return payload;
|
|
107
|
+
let parsed: unknown;
|
|
108
|
+
try { parsed = JSON.parse(payload); } catch { return payload; }
|
|
109
|
+
if (!isRec(parsed) || parsed.type !== "error" || !isRec(parsed.error)) return payload;
|
|
110
|
+
const error = parsed.error;
|
|
111
|
+
const message = typeof error.message === "string" ? error.message : "";
|
|
112
|
+
const sized = error.type === "invalid_request_error" || error.type === "request_too_large";
|
|
113
|
+
if (!sized || isThroughputLimitText(message)) return payload;
|
|
114
|
+
if (!(error.code === "context_length_exceeded" || isContextOverflowText(message))) return payload;
|
|
115
|
+
const next = claudePromptTooLongMessage(message);
|
|
116
|
+
return next === message ? payload : JSON.stringify({ ...parsed, error: { ...error, message: next } });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* A throughput limit that the shared classifier files under context_length_exceeded (its "too many
|
|
121
|
+
* tokens" match also catches "too many tokens per minute"). Worded as an overflow it would make
|
|
122
|
+
* Claude Code compact a conversation that only needs to wait, so it keeps its own text.
|
|
123
|
+
*/
|
|
124
|
+
const THROUGHPUT_LIMIT_TEXT_RE = /\bper (?:second|minute|hour|day)\b|\brate limit|\bquota\b|\btpm\b/i;
|
|
125
|
+
|
|
126
|
+
export function isThroughputLimitText(message: string): boolean {
|
|
127
|
+
return THROUGHPUT_LIMIT_TEXT_RE.test(message);
|
|
128
|
+
}
|
|
129
|
+
|
|
73
130
|
export function anthropicErrorBody(status: number, message: string, type?: string, code?: string): Rec {
|
|
74
|
-
|
|
131
|
+
const overflow = code === "context_length_exceeded" && !isThroughputLimitText(message);
|
|
132
|
+
const text = overflow ? claudePromptTooLongMessage(message) : message;
|
|
133
|
+
return { type: "error", error: { type: type ?? anthropicErrorType(status), message: text, ...(code ? { code } : {}) } };
|
|
75
134
|
}
|
|
76
135
|
|
|
77
136
|
export function anthropicErrorResponse(status: number, message: string, type?: string, code?: string): Response {
|
|
@@ -280,6 +339,13 @@ interface OpenBlock {
|
|
|
280
339
|
thinkingBuf?: string;
|
|
281
340
|
thinkingBufBytes?: number;
|
|
282
341
|
reasoningSig?: string;
|
|
342
|
+
/** The provider's own encrypted reasoning for this item; signed into the block on close. */
|
|
343
|
+
reasoningNative?: { enc: string; id?: string };
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/** A provider-minted (not ocxr1) `encrypted_content`, or `""`. */
|
|
347
|
+
function nativeEncryptedReasoning(encrypted: string): string {
|
|
348
|
+
return encrypted.length > 0 && !encrypted.startsWith(OCX_REASONING_PREFIX) ? encrypted : "";
|
|
283
349
|
}
|
|
284
350
|
|
|
285
351
|
/** Streaming: Responses SSE bytes -> Anthropic Messages SSE bytes. */
|
|
@@ -294,6 +360,7 @@ export function responsesSseToAnthropicSse(
|
|
|
294
360
|
* upstream sent no confirmed usage before the first frame. See `messageSnapshot` (#4857).
|
|
295
361
|
*/
|
|
296
362
|
inputTokenFloor?: number;
|
|
363
|
+
nativeReasoningTagFor?: (blob: string) => string | undefined;
|
|
297
364
|
},
|
|
298
365
|
): ReadableStream<Uint8Array> {
|
|
299
366
|
const translatorBudget = opts.translatorBudget;
|
|
@@ -382,6 +449,13 @@ export function responsesSseToAnthropicSse(
|
|
|
382
449
|
open.webSearchArgsEmitted = true;
|
|
383
450
|
}
|
|
384
451
|
if (open.kind === "thinking") {
|
|
452
|
+
const native = open.reasoningNative;
|
|
453
|
+
const tag = native && opts.nativeReasoningTagFor?.(native.enc);
|
|
454
|
+
if (!open.thinkingBuf && !open.reasoningSig && !tag) {
|
|
455
|
+
releaseThinkingBuffer(open);
|
|
456
|
+
open = null;
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
385
459
|
// Delay the index and all thinking frames until closure so a matching
|
|
386
460
|
// done envelope can put its redacted blocks first. The existing buffer
|
|
387
461
|
// remains charged through signature emission, including queued frames.
|
|
@@ -396,7 +470,10 @@ export function responsesSseToAnthropicSse(
|
|
|
396
470
|
delta: { type: "thinking_delta", thinking: open.thinkingBuf },
|
|
397
471
|
});
|
|
398
472
|
}
|
|
399
|
-
const signature = open.reasoningSig ?? encodeReasoningEnvelope({
|
|
473
|
+
const signature = open.reasoningSig ?? encodeReasoningEnvelope({
|
|
474
|
+
txt: open.thinkingBuf ?? "",
|
|
475
|
+
...(tag ? { nat: { ...native, model, tag } } : {}),
|
|
476
|
+
}, translatorBudget);
|
|
400
477
|
emit("content_block_delta", {
|
|
401
478
|
type: "content_block_delta", index: open.index,
|
|
402
479
|
delta: { type: "signature_delta", signature },
|
|
@@ -684,11 +761,15 @@ export function responsesSseToAnthropicSse(
|
|
|
684
761
|
emit("content_block_start", { type: "content_block_start", index: idx, content_block: { type: "redacted_thinking", data } });
|
|
685
762
|
emit("content_block_stop", { type: "content_block_stop", index: idx });
|
|
686
763
|
}
|
|
687
|
-
|
|
764
|
+
// A provider's own blob is the only copy of its reasoning the client can hold, so
|
|
765
|
+
// it gets a thinking block even when no summary text arrived.
|
|
766
|
+
const native = nativeEncryptedReasoning(encrypted);
|
|
767
|
+
if ((env?.sig || native) && open?.kind !== "thinking") {
|
|
688
768
|
ensureBlock("thinking");
|
|
689
769
|
}
|
|
690
770
|
if (open?.kind === "thinking") {
|
|
691
771
|
if (env?.sig) open.reasoningSig = env.sig;
|
|
772
|
+
if (native) open.reasoningNative = typeof item.id === "string" && item.id.length > 0 ? { enc: native, id: item.id } : { enc: native };
|
|
692
773
|
closeOpenBlock();
|
|
693
774
|
}
|
|
694
775
|
}
|
|
@@ -881,7 +962,7 @@ export function responsesSseToAnthropicSse(
|
|
|
881
962
|
}
|
|
882
963
|
|
|
883
964
|
/** Non-streaming: /v1/responses JSON -> Anthropic message JSON. */
|
|
884
|
-
export function responsesJsonToAnthropicMessage(json: unknown, model: string, translatorBudget?: TranslatorBudget): Rec {
|
|
965
|
+
export function responsesJsonToAnthropicMessage(json: unknown, model: string, translatorBudget?: TranslatorBudget, nativeReasoningTagFor?: (blob: string) => string | undefined): Rec {
|
|
885
966
|
const body = isRec(json) ? json : {};
|
|
886
967
|
const output = Array.isArray(body.output) ? body.output : [];
|
|
887
968
|
const content: Rec[] = [];
|
|
@@ -917,10 +998,14 @@ export function responsesJsonToAnthropicMessage(json: unknown, model: string, tr
|
|
|
917
998
|
// Legacy combined envelopes place redacted blocks before the signed block,
|
|
918
999
|
// matching the Anthropic adapter. New bridge output uses separate items.
|
|
919
1000
|
for (const data of env?.red ?? []) content.push({ type: "redacted_thinking", data });
|
|
920
|
-
// env.txt may be locally hidden text. Do not expose it here
|
|
921
|
-
//
|
|
922
|
-
|
|
923
|
-
|
|
1001
|
+
// env.txt may be locally hidden text. Do not expose it here. A provider's own blob is
|
|
1002
|
+
// carried in the envelope so the next request can hand it back (see inbound.ts).
|
|
1003
|
+
const native = nativeEncryptedReasoning(encrypted);
|
|
1004
|
+
const tag = native && nativeReasoningTagFor?.(native);
|
|
1005
|
+
if (parts.length > 0 || env?.sig || tag) {
|
|
1006
|
+
const text = parts.join("\n\n");
|
|
1007
|
+
const nat = tag ? { enc: native, model, tag, ...(typeof raw.id === "string" && raw.id.length > 0 ? { id: raw.id } : {}) } : undefined;
|
|
1008
|
+
content.push({ type: "thinking", thinking: text, signature: env?.sig ?? encodeReasoningEnvelope({ txt: text, ...(nat ? { nat } : {}) }, translatorBudget) });
|
|
924
1009
|
}
|
|
925
1010
|
break;
|
|
926
1011
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { OcxConfig } from "../types";
|
|
2
2
|
import { claudeCodeAlias, claudeCodeNativeAlias, resolveAlias } from "./alias";
|
|
3
|
-
import { AUTO_CONTEXT_OFF, shouldMarkOneMillion, stripOneMillionMarker, withOneMillionMarker } from "./context-windows";
|
|
3
|
+
import { AUTO_CONTEXT_OFF, shouldMarkOneMillion, stripOneMillionMarker, UNPAIRED_AUTO_CONTEXT, withOneMillionMarker, type AutoContextMode } from "./context-windows";
|
|
4
|
+
import { isAnthropicClaudeRoute } from "./long-context";
|
|
4
5
|
import { hasOwnProvider } from "../config";
|
|
5
6
|
import { knownModelIdsForProvider } from "../router";
|
|
6
7
|
import { decodeRoutedModelIdOrThrow } from "../providers/slug-codec";
|
|
@@ -14,20 +15,43 @@ export { SAFE_AGENT_MODEL_ID } from "../config/subagent-models";
|
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Generated subagent defs cannot rely on the parent's auto-context compaction
|
|
17
|
-
* pairing, so their [1m] marker follows the
|
|
18
|
-
* the effective window (exact selector, then the
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
18
|
+
* pairing, so their [1m] marker follows the unpaired long-context rule
|
|
19
|
+
* (long-context.ts): mark when the effective window (exact selector, then the
|
|
20
|
+
* canonical [1m] form, then bare) is >= 1M or can host the default compact window,
|
|
21
|
+
* whose overflow Claude Code compacts on as `prompt is too long`; strip an inherited
|
|
22
|
+
* marker the window cannot carry; with no window information, keep the selector as it
|
|
23
|
+
* was. Genuine routed [1m] ids are preserved through the canonical-exact lookup.
|
|
24
|
+
* (#854; devlog/_plan/261009_claude_1m_default/020)
|
|
22
25
|
*/
|
|
23
|
-
|
|
26
|
+
/**
|
|
27
|
+
* The marking mode a subagent selector gets: real Anthropic models (a bare `claude-*` passthrough
|
|
28
|
+
* id, or a Claude row on either Anthropic pool) need a genuine 1M; everything else follows the
|
|
29
|
+
* unpaired long-context rule. Connected launches read windows from the published catalog, which
|
|
30
|
+
* does not drop capped Anthropic rows, so this cannot rely on the window map alone.
|
|
31
|
+
*/
|
|
32
|
+
function subagentSelectorMode(bare: string): AutoContextMode {
|
|
33
|
+
const route = resolveAlias(bare) ?? resolveDesktop3pAlias(bare) ?? bare;
|
|
34
|
+
const slash = route.indexOf("/");
|
|
35
|
+
const anthropic = slash < 0
|
|
36
|
+
? route.startsWith("claude-")
|
|
37
|
+
: isAnthropicClaudeRoute(route.slice(0, slash), route.slice(slash + 1));
|
|
38
|
+
return anthropic ? AUTO_CONTEXT_OFF : UNPAIRED_AUTO_CONTEXT;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* `accounting200k` (claudeCode.contextAccounting "200k") stops automatic marking only: an unmarked
|
|
43
|
+
* selector stays bare, while an explicit [1m] still follows its selector's safety rule.
|
|
44
|
+
*/
|
|
45
|
+
export function withSubagentContextMarker(selector: string, windows: Record<string, number>, accounting200k = false): string {
|
|
24
46
|
const bare = stripOneMillionMarker(selector);
|
|
25
47
|
const wasMarked = selector !== bare;
|
|
26
48
|
const canonicalExact = wasMarked ? `${bare}[1m]` : selector;
|
|
27
49
|
const authoritativeWindow = windows[selector] ?? windows[canonicalExact] ?? windows[bare];
|
|
28
50
|
if (typeof authoritativeWindow === "number" && authoritativeWindow > 0) {
|
|
29
|
-
|
|
30
|
-
|
|
51
|
+
if (accounting200k && !wasMarked) return bare;
|
|
52
|
+
const mode = subagentSelectorMode(bare);
|
|
53
|
+
return shouldMarkOneMillion(authoritativeWindow, mode)
|
|
54
|
+
? (withOneMillionMarker(selector, windows, mode) ?? selector)
|
|
31
55
|
: bare;
|
|
32
56
|
}
|
|
33
57
|
return wasMarked ? selector : bare;
|
|
@@ -56,6 +80,11 @@ function selectorIdentity(selector: string): string {
|
|
|
56
80
|
return fullSelectorIdentity(stripOneMillionMarker(selector));
|
|
57
81
|
}
|
|
58
82
|
|
|
83
|
+
/** Whether the config opted into 200k accounting (shared by roster, self and force markers). */
|
|
84
|
+
export function accountsAt200k(config: OcxConfig): boolean {
|
|
85
|
+
return config.claudeCode?.contextAccounting === "200k";
|
|
86
|
+
}
|
|
87
|
+
|
|
59
88
|
/** Resolve only currently exposed entries; retained roster rows are not exposure proof. */
|
|
60
89
|
export function resolveSubagentForceModel(config: OcxConfig, windows: Record<string, number>, available: SubagentForceExposure): string | null {
|
|
61
90
|
const entry = config.claudeCode?.subagentModelForce;
|
|
@@ -77,8 +106,8 @@ export function resolveSubagentForceModel(config: OcxConfig, windows: Record<str
|
|
|
77
106
|
const window = windows[parts.alias] ?? windows[`${bare}[1m]`] ?? windows[bare];
|
|
78
107
|
// Identity admission must not let an appended context promise ride on an
|
|
79
108
|
// ordinary exposed model. Exact catalog ids such as kimi/k3[1m] remain literal.
|
|
80
|
-
if (requestedMarker && !exactMarkedExposure && !(typeof window === "number" && Number.isFinite(window) && window
|
|
81
|
-
const marked = exactMarkedExposure ? parts.alias : withSubagentContextMarker(parts.alias, windows);
|
|
109
|
+
if (requestedMarker && !exactMarkedExposure && !(typeof window === "number" && Number.isFinite(window) && shouldMarkOneMillion(window, subagentSelectorMode(bare)))) return null;
|
|
110
|
+
const marked = exactMarkedExposure ? parts.alias : withSubagentContextMarker(parts.alias, windows, accountsAt200k(config));
|
|
82
111
|
// Bare Anthropic names are indistinguishable from the old CLI's fallback.
|
|
83
112
|
// Encode only the force selector; handlers restore native identity before
|
|
84
113
|
// their existing credential/model-map checks. Roster generation is unchanged.
|