@bitkyc08/opencodex 2.59.0 → 2.61.0-preview.20260922
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS_INSTALL.md +64 -0
- package/README.md +28 -1
- package/bin/ocx.mjs +382 -209
- package/gui/dist/assets/App-E64Rzjap.js +50 -0
- package/gui/dist/assets/Tray-_nfzD8k4.js +1 -0
- package/gui/dist/assets/index-DpdfZWMK.js +86 -0
- package/gui/dist/assets/index-_bpvxJu0.css +1 -0
- package/gui/dist/assets/usage-companion-chart-DtoK7T6h.js +1 -0
- package/gui/dist/favicon.png +0 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/stepfun-color.svg +1 -0
- package/package.json +8 -1
- package/src/adapters/anthropic-image-codec.ts +16 -2
- package/src/adapters/anthropic-image-normalize.ts +49 -2
- package/src/adapters/anthropic.ts +20 -1
- package/src/adapters/base.ts +23 -0
- package/src/adapters/coding-agent/protocol.ts +36 -6
- package/src/adapters/coding-agent/turn.ts +32 -4
- package/src/adapters/command-code.ts +52 -4
- package/src/adapters/cursor/catalog.ts +51 -7
- package/src/adapters/cursor/checkpoint-store.ts +3 -0
- package/src/adapters/cursor/discovery.ts +11 -8
- package/src/adapters/cursor/live-transport.ts +26 -9
- package/src/adapters/cursor/protobuf-request.ts +6 -3
- package/src/adapters/cursor/request-builder.ts +20 -4
- package/src/adapters/cursor/transport.ts +19 -0
- package/src/adapters/cursor.ts +14 -3
- package/src/adapters/declaration-carrier.ts +45 -0
- package/src/adapters/devin/cloud-direct/chat.ts +3 -1
- package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +42 -5
- package/src/adapters/devin.ts +125 -36
- package/src/adapters/google-antigravity-replay.ts +1 -1
- package/src/adapters/google-antigravity-wire.ts +34 -7
- package/src/adapters/google-errors.ts +7 -1
- package/src/adapters/google-http.ts +49 -10
- package/src/adapters/google-tool-schema.ts +595 -31
- package/src/adapters/google-wire-compiler.ts +93 -10
- package/src/adapters/google-wire-shape.ts +461 -0
- package/src/adapters/google.ts +66 -11
- package/src/adapters/image.ts +4 -1
- package/src/adapters/input-media-guard.ts +21 -9
- package/src/adapters/kiro/usage.ts +3 -2
- package/src/adapters/kiro-tool-fallback.ts +1 -1
- package/src/adapters/ollama-native.ts +6 -0
- package/src/adapters/openai-chat/developer-role.ts +61 -0
- package/src/adapters/openai-chat/messages.ts +46 -27
- package/src/adapters/openai-chat/parallel-tool-calls.ts +32 -0
- package/src/adapters/openai-chat/passthrough.ts +33 -9
- package/src/adapters/openai-chat/reasoning-wire.ts +89 -0
- package/src/adapters/openai-chat-images.ts +3 -1
- package/src/adapters/openai-chat.ts +23 -58
- package/src/adapters/openai-responses/image-gen.ts +8 -6
- package/src/adapters/openai-responses/passthrough.ts +17 -3
- package/src/adapters/openai-responses/reasoning.ts +7 -0
- package/src/adapters/opencode-go-additional-tools.ts +12 -2
- package/src/adapters/registry.ts +3 -2
- package/src/adapters/run-turn-queue.ts +178 -29
- package/src/adapters/xai-web-search.ts +16 -1
- package/src/bridge/errors.ts +8 -2
- package/src/bridge/response-json.ts +9 -1
- package/src/bridge/sse.ts +13 -151
- package/src/chat/inbound.ts +141 -5
- package/src/claude/desktop-3p.ts +7 -1
- package/src/claude/desktop-first-party.ts +183 -0
- package/src/claude/desktop-gateway-state.ts +41 -0
- package/src/claude/inbound-content-options.ts +6 -0
- package/src/claude/inbound.ts +32 -6
- package/src/claude/intercept/connect-proxy.ts +179 -0
- package/src/claude/intercept/listener.ts +122 -0
- package/src/claude/intercept/local-ca.ts +298 -0
- package/src/claude/intercept/runtime.ts +98 -0
- package/src/claude/intercept/settings.ts +189 -0
- package/src/cli/access.ts +87 -0
- package/src/cli/account-auth.ts +19 -0
- package/src/cli/account-extended.ts +4 -4
- package/src/cli/capabilities.ts +31 -0
- package/src/cli/claude-desktop.ts +206 -16
- package/src/cli/codex-shim-autorestore.ts +3 -0
- package/src/cli/companion.ts +56 -0
- package/src/cli/dispatch.ts +46 -7
- package/src/cli/doctor.ts +28 -9
- package/src/cli/ensure-desired-integrations.ts +43 -5
- package/src/cli/help.ts +7 -9
- package/src/cli/hub.ts +3 -2
- package/src/cli/index.ts +203 -59
- package/src/cli/init.ts +8 -0
- package/src/cli/integrations.ts +7 -1
- package/src/cli/opencode.ts +2 -2
- package/src/cli/provider.ts +13 -1
- package/src/cli/registry.ts +41 -2
- package/src/cli/resolve.ts +230 -0
- package/src/cli/root.ts +24 -1
- package/src/cli/start-ownership-publication.ts +56 -0
- package/src/cli/status-probes.ts +2 -18
- package/src/cli/status.ts +62 -0
- package/src/cli/stop-report.ts +143 -0
- package/src/cli/uninstall-plan.ts +9 -0
- package/src/client/machine-api.ts +2 -2
- package/src/client/machine-listener.ts +4 -7
- package/src/client/runtime.ts +26 -2
- package/src/clients/aside-profiles.ts +4 -0
- package/src/clients/config-export/zcode-store.ts +157 -0
- package/src/clients/config-export.ts +36 -0
- package/src/codex/account-store.ts +65 -0
- package/src/codex/app-server-processes.ts +72 -40
- package/src/codex/auth-api/account-list.ts +19 -11
- package/src/codex/auth-api/login-flow.ts +6 -1
- package/src/codex/auth-api/pool-quota-probe.ts +30 -7
- package/src/codex/autostart-health.ts +28 -0
- package/src/codex/catalog/build-entries.ts +2 -2
- package/src/codex/catalog/effort.ts +3 -3
- package/src/codex/catalog/gather-capture.ts +21 -2
- package/src/codex/catalog/model-hints.ts +29 -28
- package/src/codex/catalog/parsing.ts +7 -0
- package/src/codex/catalog/provider-models.ts +43 -17
- package/src/codex/catalog/retained-sync.ts +24 -28
- package/src/codex/catalog/routed-gather.ts +19 -0
- package/src/codex/context-compat.ts +5 -2
- package/src/codex/convergence.ts +2 -2
- package/src/codex/desired-state.ts +4 -1
- package/src/codex/history-job.ts +6 -6
- package/src/codex/history-provider.ts +31 -166
- package/src/codex/history-rollout-read.ts +174 -0
- package/src/codex/inject/config-toml.ts +41 -6
- package/src/codex/inject/paginated-openai-compat.ts +90 -0
- package/src/codex/inject.ts +18 -15
- package/src/codex/injected-marker.ts +18 -0
- package/src/codex/internal/catalog-writer.ts +33 -1
- package/src/codex/main-account.ts +6 -0
- package/src/codex/model-cache.ts +99 -6
- package/src/codex/model-entitlement-admission.ts +59 -0
- package/src/codex/model-entitlements.ts +116 -54
- package/src/codex/native-main-admission.ts +83 -0
- package/src/codex/observed-model-denials.ts +101 -8
- package/src/codex/prompt-text-probe.ts +9 -6
- package/src/codex/routing/health-store.ts +39 -0
- package/src/codex/routing/selection.ts +37 -1
- package/src/codex/routing.ts +12 -42
- package/src/codex/shim-templates.ts +29 -3
- package/src/codex/shim.ts +1 -1
- package/src/codex/subagent-model-fallback.ts +22 -4
- package/src/combos/failover.ts +3 -0
- package/src/companion/settings.ts +132 -0
- package/src/config/admitted-identity.ts +222 -0
- package/src/config/atomic-write.ts +117 -5
- package/src/config/diagnostics.ts +22 -1
- package/src/config/feature-flags.ts +5 -0
- package/src/config/load-degrade.ts +52 -7
- package/src/config/process-state.ts +1 -1
- package/src/config/proxy-env.ts +8 -2
- package/src/config/schema/compaction-triggers.ts +11 -0
- package/src/config/schema/config-schema.ts +31 -1
- package/src/config/schema/leaf-validators.ts +59 -0
- package/src/config.ts +3 -3
- package/src/generated/compatibility-version.json +604 -280
- package/src/grok/reset-coupons.ts +38 -19
- package/src/images/loop.ts +6 -1
- package/src/integrations/aside-profile-context.ts +37 -3
- package/src/integrations/aside-profile-journal.ts +68 -3
- package/src/integrations/aside-profiles.ts +128 -3
- package/src/integrations/config-io.ts +44 -10
- package/src/integrations/merge.ts +120 -13
- package/src/integrations/mutation-plan.ts +921 -0
- package/src/integrations/registry.ts +38 -0
- package/src/integrations/state.ts +78 -45
- package/src/integrations/target.ts +208 -0
- package/src/integrations/writer.ts +134 -110
- package/src/lab/conformance/fixture-provider.ts +5 -0
- package/src/lab/live/transport.ts +4 -0
- package/src/lab/live/types.ts +5 -0
- package/src/lab/subject/behavior-fingerprint.ts +1 -1
- package/src/lib/admin-secrets.ts +9 -1
- package/src/lib/browser-launch-notice.ts +59 -0
- package/src/lib/bun-runtime.ts +6 -2
- package/src/lib/debug-log-buffer.ts +6 -1
- package/src/lib/debug.ts +63 -0
- package/src/lib/errors.ts +79 -0
- package/src/lib/http-response-semantics.ts +57 -0
- package/src/lib/lab-live-pinned-sender.ts +26 -12
- package/src/lib/open-url.ts +51 -7
- package/src/lib/package-tree-integrity.ts +2 -1
- package/src/lib/package-version.ts +8 -0
- package/src/lib/pinned-http.ts +142 -2
- package/src/lib/plain-data.ts +103 -0
- package/src/lib/process-control.ts +13 -5
- package/src/lib/provider-egress.ts +310 -0
- package/src/lib/provider-outbound.ts +109 -16
- package/src/lib/proxy-env.ts +82 -7
- package/src/lib/request-execution-budget.ts +72 -0
- package/src/lib/request-failure-attribution.ts +183 -0
- package/src/lib/request-failure-model.ts +236 -0
- package/src/lib/request-resend-gate.ts +138 -0
- package/src/lib/socks5-fetch.ts +136 -26
- package/src/lib/spend-ledger-owner.ts +364 -0
- package/src/lib/spend-reservation-ledger.ts +218 -27
- package/src/lib/standalone.ts +16 -0
- package/src/lib/upstream-retry.ts +167 -16
- package/src/lib/windows-system-proxy.ts +16 -11
- package/src/lib/winsw.ts +2 -2
- package/src/oauth/callback-server.ts +4 -3
- package/src/oauth/generic-account-failover.ts +1 -0
- package/src/oauth/health.ts +12 -1
- package/src/oauth/index.ts +27 -108
- package/src/oauth/login-cli.ts +80 -29
- package/src/oauth/login-flow-state.ts +127 -0
- package/src/providers/api-key-resolve.ts +133 -0
- package/src/providers/api-key-selection.ts +5 -1
- package/src/providers/derive.ts +34 -17
- package/src/providers/devin-cli-authmode-migration.ts +14 -10
- package/src/providers/key-failover.ts +97 -19
- package/src/providers/key-store.ts +34 -110
- package/src/providers/model-rename-fields.ts +147 -0
- package/src/providers/model-rename-migration.ts +179 -38
- package/src/providers/model-rename-startup.ts +7 -5
- package/src/providers/openai-virtual-models.ts +42 -2
- package/src/providers/quota/antigravity.ts +22 -2
- package/src/providers/quota/vendor-probes-key.ts +38 -23
- package/src/providers/reasoning-metadata.ts +43 -18
- package/src/providers/registry/entries-core.ts +47 -20
- package/src/providers/registry/entries-extended.ts +63 -4
- package/src/providers/registry/model-ids.ts +168 -0
- package/src/providers/registry/model-seeds.ts +56 -10
- package/src/providers/registry/types.ts +2 -0
- package/src/providers/resolved-model-policy-merge.ts +167 -0
- package/src/providers/resolved-model-policy.ts +406 -0
- package/src/providers/stale-vision-classification-migration.ts +137 -0
- package/src/providers/xai-transport.ts +12 -1
- package/src/reasoning-effort.ts +8 -0
- package/src/responses/apply-patch-envelope.ts +0 -12
- package/src/responses/freeform-wrapper-scan.ts +279 -0
- package/src/responses/function-call-compat.ts +38 -1
- package/src/responses/inline-document.ts +65 -0
- package/src/responses/input-media.ts +42 -8
- package/src/responses/legacy-dotted-tool-name-repair.ts +134 -0
- package/src/responses/muse-tool-name-alias.ts +19 -0
- package/src/responses/parser-content.ts +8 -2
- package/src/responses/parser-tools.ts +3 -0
- package/src/responses/parser.ts +3 -1
- package/src/responses/progressive-freeform-input.ts +130 -0
- package/src/responses/reasoning-envelope.ts +30 -0
- package/src/responses/schema.ts +3 -0
- package/src/responses/state.ts +5 -12
- package/src/responses/tool-name-aliases.ts +15 -1
- package/src/router.ts +108 -117
- package/src/routing/compatibility/behavior.ts +9 -0
- package/src/routing/compatibility/subject.ts +16 -1
- package/src/server/adapter-resolve.ts +9 -0
- package/src/server/admission-model-scope.ts +219 -0
- package/src/server/audio-live.ts +9 -3
- package/src/server/audio-upstream.ts +18 -0
- package/src/server/auth-cors.ts +29 -0
- package/src/server/chat-completions.ts +60 -4
- package/src/server/chat-native.ts +19 -4
- package/src/server/claude-messages.ts +61 -20
- package/src/server/effort-row.ts +11 -3
- package/src/server/grok-responses-control-frame.ts +160 -1
- package/src/server/grok-responses-snapshot-repair.ts +113 -11
- package/src/server/gui-freshness.ts +103 -0
- package/src/server/gui-static.ts +7 -9
- package/src/server/images.ts +59 -6
- package/src/server/index/claude-intercept-lifecycle.ts +49 -0
- package/src/server/index/serve-options.ts +142 -39
- package/src/server/index/spend-ledger-lifecycle.ts +92 -0
- package/src/server/index/startup-warnings.ts +24 -0
- package/src/server/index/websocket-handler.ts +6 -1
- package/src/server/index.ts +34 -38
- package/src/server/lifecycle.ts +4 -4
- package/src/server/live-call-bindings.ts +6 -0
- package/src/server/live.ts +88 -3
- package/src/server/management/agent-settings-routes.ts +121 -36
- package/src/server/management/aside-profile-routes.ts +266 -7
- package/src/server/management/companion-routes.ts +77 -0
- package/src/server/management/config-routes.ts +18 -2
- package/src/server/management/context.ts +3 -0
- package/src/server/management/integration-routes.ts +287 -5
- package/src/server/management/logs-usage-routes.ts +19 -0
- package/src/server/management/metrics-routes.ts +20 -0
- package/src/server/management/model-rows.ts +224 -12
- package/src/server/management/native-integration-routes.ts +103 -6
- package/src/server/management/oauth-account-routes.ts +45 -7
- package/src/server/management/route-registry.ts +20 -0
- package/src/server/management/shared.ts +28 -4
- package/src/server/management/system-restart.ts +7 -2
- package/src/server/management/system-routes.ts +2 -0
- package/src/server/management/usage-aggregate-cache.ts +4 -0
- package/src/server/management/usage-timeline-routes.ts +44 -0
- package/src/server/management-api.ts +10 -9
- package/src/server/management-auth.ts +15 -1
- package/src/server/proxy-liveness.ts +75 -0
- package/src/server/readiness.ts +29 -10
- package/src/server/relay-eager.ts +24 -2
- package/src/server/relay.ts +138 -12
- package/src/server/request-log-failure-attribution.ts +99 -0
- package/src/server/request-log.ts +169 -2
- package/src/server/request-metrics.ts +298 -0
- package/src/server/responses/adapter-continuation.ts +3 -3
- package/src/server/responses/adapter-dispatch.ts +11 -6
- package/src/server/responses/codex-ws-wire.ts +34 -8
- package/src/server/responses/combo-stream-preflight.ts +168 -6
- package/src/server/responses/compact.ts +43 -10
- package/src/server/responses/compaction-routing.ts +111 -0
- package/src/server/responses/core-codex-account.ts +8 -3
- package/src/server/responses/core-combo.ts +7 -7
- package/src/server/responses/core-normalize.ts +6 -12
- package/src/server/responses/core-opaque-recovery.ts +91 -0
- package/src/server/responses/core-options.ts +4 -0
- package/src/server/responses/encrypted-payload.ts +20 -2
- package/src/server/responses/fetch-helpers.ts +124 -8
- package/src/server/responses/input-admission.ts +10 -0
- package/src/server/responses/passthrough-delivery.ts +45 -15
- package/src/server/responses/passthrough-dispatch.ts +215 -44
- package/src/server/responses/passthrough-error.ts +27 -8
- package/src/server/responses/policy-fallback.ts +5 -13
- package/src/server/responses/request-prepare.ts +109 -18
- package/src/server/responses/request-send-budget.ts +17 -1
- package/src/server/responses/request-sidecar-auth.ts +1 -1
- package/src/server/responses/request-transport.ts +26 -6
- package/src/server/responses/reset-replay.ts +108 -0
- package/src/server/responses/run-turn-execution.ts +25 -3
- package/src/server/responses/sidecar-execution.ts +17 -2
- package/src/server/responses/ws-upstream.ts +14 -27
- package/src/server/responses-custom-tool-repair.ts +27 -54
- package/src/server/responses-request-tool-scope.ts +214 -0
- package/src/server/responses-undeclared-tool-guard.ts +35 -2
- package/src/server/search.ts +25 -1
- package/src/server/sse-payload-rewrite.ts +1 -1
- package/src/server/usage-ledger-retention.ts +73 -0
- package/src/service/cli.ts +48 -2
- package/src/service/health.ts +3 -2
- package/src/service/install-state-contract.d.mts +27 -0
- package/src/service/install-state-contract.mjs +34 -0
- package/src/service/launchd.ts +1 -1
- package/src/service/orchestration.ts +2 -4
- package/src/service/ownership-compatibility.ts +164 -0
- package/src/service/ownership-mutation-lease.d.mts +32 -0
- package/src/service/ownership-mutation-lease.mjs +211 -0
- package/src/service/repair.ts +45 -1
- package/src/service/state-lock.ts +269 -0
- package/src/service/state-record.d.mts +36 -0
- package/src/service/state-record.mjs +138 -0
- package/src/service/state.ts +582 -68
- package/src/service/windows-taskxml.ts +11 -10
- package/src/service.ts +7 -3
- package/src/tray/windows-tray.ps1 +156 -4
- package/src/types/config.ts +48 -3
- package/src/types/provider.ts +91 -0
- package/src/types/request.ts +30 -2
- package/src/types/tools.ts +33 -0
- package/src/types.ts +4 -0
- package/src/update/index.ts +207 -63
- package/src/update/job.ts +9 -5
- package/src/update/ownership-transaction.ts +47 -0
- package/src/update/restart-ownership.ts +54 -0
- package/src/update/runtime-ownership.d.mts +40 -0
- package/src/update/runtime-ownership.mjs +122 -0
- package/src/usage/attempt-delivery.ts +198 -0
- package/src/usage/cache-diagnostic.ts +305 -0
- package/src/usage/failure-fingerprint.ts +118 -0
- package/src/usage/failure-projection-cache.ts +174 -0
- package/src/usage/failure-projection.ts +174 -0
- package/src/usage/ledger-retention.ts +165 -0
- package/src/usage/log.ts +126 -79
- package/src/usage/request-outcome.ts +150 -0
- package/src/usage/retention-contract.ts +28 -0
- package/src/usage/summary.ts +2 -2
- package/src/usage/telemetry-contract.ts +237 -0
- package/src/usage/timeline.ts +236 -0
- package/src/vision/eligibility.ts +88 -9
- package/src/vision/plan.ts +34 -10
- package/src/web-search/alpha-search.ts +21 -1
- package/src/web-search/executor.ts +41 -2
- package/src/web-search/loop.ts +6 -1
- package/gui/dist/assets/index-C5IebErG.js +0 -136
- package/gui/dist/assets/index-OESInAjC.css +0 -1
|
@@ -7,6 +7,12 @@
|
|
|
7
7
|
* unchanged. The Responses output (SSE or JSON) is converted back to Anthropic shape.
|
|
8
8
|
*/
|
|
9
9
|
import { FORWARD_HEADERS } from "../adapters/openai-responses";
|
|
10
|
+
import {
|
|
11
|
+
admissionModelDeniedResponse,
|
|
12
|
+
AdmissionModelDeniedError,
|
|
13
|
+
assertRouteAllowedByScope,
|
|
14
|
+
resolveAdmissionModelScope,
|
|
15
|
+
} from "./admission-model-scope";
|
|
10
16
|
import { jsonUtf8Bytes } from "../lib/json-byte-size";
|
|
11
17
|
import { sseFieldValue } from "../lib/sse-decoder";
|
|
12
18
|
import { enforceAnthropicImageLimits, sniffImageDimensions } from "../adapters/anthropic-image-guard";
|
|
@@ -18,7 +24,14 @@ import { recordDesktopRequest } from "../claude/desktop-health";
|
|
|
18
24
|
import { stripOneMillionMarker } from "../claude/context-windows";
|
|
19
25
|
import { captureClaudeInbound } from "../claude/inbound-debug";
|
|
20
26
|
import { analyzeClaudeCompatibility, isClaudeCompatibilityMode } from "../claude/compatibility";
|
|
21
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
applyReplayRefusalClientHeaders,
|
|
29
|
+
carryReplayRefusal,
|
|
30
|
+
isReplayRefusalResponse,
|
|
31
|
+
isTransientUpstreamStatus,
|
|
32
|
+
REPLAY_REFUSED_STATUS,
|
|
33
|
+
UPSTREAM_RESET_REPLAY_REFUSED_CODE,
|
|
34
|
+
} from "../lib/upstream-retry";
|
|
22
35
|
import { resolveClientRetryAfter } from "../lib/retry-after";
|
|
23
36
|
import {
|
|
24
37
|
anthropicErrorBody,
|
|
@@ -29,7 +42,7 @@ import {
|
|
|
29
42
|
} from "../claude/outbound";
|
|
30
43
|
import { clearableDeadline, idleDeadline } from "../lib/abort";
|
|
31
44
|
import { estimateTokens } from "../lib/token-estimate";
|
|
32
|
-
import { NoEligiblePolicyCandidateError, UnknownRoutingPolicyError, routeModel } from "../router";
|
|
45
|
+
import { captureRouteStaticPolicy, NoEligiblePolicyCandidateError, UnknownRoutingPolicyError, routeModel } from "../router";
|
|
33
46
|
import { evidenceFromBody } from "../routing/request-evidence";
|
|
34
47
|
import { resolveWireProtocolOverride } from "./adapter-resolve";
|
|
35
48
|
import type { OcxConfig } from "../types";
|
|
@@ -420,7 +433,7 @@ async function anthropicNativePassthrough(
|
|
|
420
433
|
// count_tokens too — counts must match what the real send will contain, and the 32MB
|
|
421
434
|
// body cap applies to it equally. Non-message bodies pass through untouched.
|
|
422
435
|
if (Array.isArray(body.messages)) {
|
|
423
|
-
await normalizeAnthropicImages(body.messages);
|
|
436
|
+
await normalizeAnthropicImages(body.messages, { abortSignal: req.signal });
|
|
424
437
|
enforceAnthropicImageLimits(body.messages);
|
|
425
438
|
}
|
|
426
439
|
const headers = new Headers();
|
|
@@ -815,9 +828,19 @@ async function handleClaudeMessagesWithBudget(
|
|
|
815
828
|
// verified live 2026-07-11). Strip them for that route; routed providers keep them.
|
|
816
829
|
try {
|
|
817
830
|
const route = routeModel(config, internalBody.model as string, evidenceFromBody(internalBody));
|
|
831
|
+
// Same reason as the native Chat lane: this route can be sent from here, so
|
|
832
|
+
// the key's scope is applied before the wire is settled.
|
|
833
|
+
assertRouteAllowedByScope(
|
|
834
|
+
resolveAdmissionModelScope(config, logIds?.admission),
|
|
835
|
+
String(internalBody.model ?? ""),
|
|
836
|
+
route,
|
|
837
|
+
);
|
|
818
838
|
// Settle the wire once so the sampling decision below reads the effective
|
|
819
839
|
// adapter rather than the provider-wide default (#404).
|
|
820
|
-
route.
|
|
840
|
+
route.staticPolicy = captureRouteStaticPolicy(
|
|
841
|
+
route.providerName, route.modelId, route.provider, route.staticPolicy.effectiveAlias, "anthropic",
|
|
842
|
+
);
|
|
843
|
+
route.provider = resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, "anthropic", route.staticPolicy);
|
|
821
844
|
logCtx.routeDecision = route.routeDecision;
|
|
822
845
|
if (route.provider.adapter === "openai-responses") {
|
|
823
846
|
delete internalBody.max_output_tokens;
|
|
@@ -844,6 +867,11 @@ async function handleClaudeMessagesWithBudget(
|
|
|
844
867
|
if (ladder !== undefined && ladder.length === 0) delete internalBody.reasoning;
|
|
845
868
|
}
|
|
846
869
|
} catch (err) {
|
|
870
|
+
if (err instanceof AdmissionModelDeniedError) {
|
|
871
|
+
logCtx.requestedModel = requestedModel;
|
|
872
|
+
if (logIds) addFinalRequestLog(logIds.requestId, logIds.start, logCtx, 403, { closeReason: "non_stream" });
|
|
873
|
+
return admissionModelDeniedResponse(err);
|
|
874
|
+
}
|
|
847
875
|
if (err instanceof UnknownRoutingPolicyError) {
|
|
848
876
|
logCtx.requestedModel = requestedModel;
|
|
849
877
|
if (logIds) addFinalRequestLog(logIds.requestId, logIds.start, logCtx, 404, { closeReason: "non_stream" });
|
|
@@ -958,6 +986,9 @@ async function handleClaudeMessagesWithBudget(
|
|
|
958
986
|
const response = logIds ? responseWithDeferredRequestLog(upstream, logIds.requestId, logIds.start, logCtx) : upstream;
|
|
959
987
|
|
|
960
988
|
if (!response.ok) {
|
|
989
|
+
// Read the shared provenance verdict before consuming and re-wrapping the body. A refusal
|
|
990
|
+
// and an ordinary provider rate limit are both 429, so the status cannot distinguish them.
|
|
991
|
+
const replayRefusal = isReplayRefusalResponse(response);
|
|
961
992
|
// Re-shape the OpenAI-style error envelope into the Anthropic one, preserving status.
|
|
962
993
|
let message = `upstream error (${response.status})`;
|
|
963
994
|
try {
|
|
@@ -972,14 +1003,16 @@ async function handleClaudeMessagesWithBudget(
|
|
|
972
1003
|
}
|
|
973
1004
|
} catch { /* keep fallback message */ }
|
|
974
1005
|
const upstreamRetryAfter = response.headers.get("retry-after");
|
|
975
|
-
const retryAfter =
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
1006
|
+
const retryAfter = replayRefusal
|
|
1007
|
+
? undefined
|
|
1008
|
+
: resolveClientRetryAfter({
|
|
1009
|
+
status: response.status,
|
|
1010
|
+
message,
|
|
1011
|
+
upstreamRetryAfter,
|
|
1012
|
+
})
|
|
1013
|
+
// Instant-retry "0" is a valid client directive but rejected by cooldown parsers.
|
|
1014
|
+
// Preserve it so it still wins over the transient "2" fallback (claude-529 mapping).
|
|
1015
|
+
?? (upstreamRetryAfter?.trim() === "0" ? "0" : undefined);
|
|
983
1016
|
// Transient upstream 5xx (already retried pre-stream, 010): reclassify as Anthropic
|
|
984
1017
|
// 529 overloaded_error so the Claude Code client applies its built-in backoff retry
|
|
985
1018
|
// instead of dying on a fatal api_error (260716 sol-builder incident). The request
|
|
@@ -990,16 +1023,24 @@ async function handleClaudeMessagesWithBudget(
|
|
|
990
1023
|
const nativeMainFence = response.status === 503
|
|
991
1024
|
&& upstreamRetryAfter?.trim() === "1"
|
|
992
1025
|
&& message === CODEX_MAIN_PROFILE_MAINTENANCE_MESSAGE;
|
|
993
|
-
const transient = !nativeMainFence && isTransientUpstreamStatus(response.status);
|
|
994
|
-
const outStatus =
|
|
995
|
-
|
|
1026
|
+
const transient = !replayRefusal && !nativeMainFence && isTransientUpstreamStatus(response.status);
|
|
1027
|
+
const outStatus = replayRefusal
|
|
1028
|
+
? REPLAY_REFUSED_STATUS
|
|
1029
|
+
: nativeMainFence ? 503 : transient ? 529 : response.status;
|
|
1030
|
+
const outHeaders = new Headers({ "Content-Type": "application/json" });
|
|
1031
|
+
if (retryAfter) outHeaders.set("Retry-After", retryAfter);
|
|
1032
|
+
else if (transient) outHeaders.set("Retry-After", "2");
|
|
1033
|
+
if (replayRefusal) applyReplayRefusalClientHeaders(outHeaders);
|
|
1034
|
+
const out = new Response(JSON.stringify(anthropicErrorBody(
|
|
1035
|
+
outStatus,
|
|
1036
|
+
message,
|
|
1037
|
+
undefined,
|
|
1038
|
+
replayRefusal ? UPSTREAM_RESET_REPLAY_REFUSED_CODE : undefined,
|
|
1039
|
+
)), {
|
|
996
1040
|
status: outStatus,
|
|
997
|
-
headers:
|
|
998
|
-
"Content-Type": "application/json",
|
|
999
|
-
...(retryAfter ? { "Retry-After": retryAfter } : (transient ? { "Retry-After": "2" } : {})),
|
|
1000
|
-
},
|
|
1041
|
+
headers: outHeaders,
|
|
1001
1042
|
});
|
|
1002
|
-
return out;
|
|
1043
|
+
return carryReplayRefusal(response, out);
|
|
1003
1044
|
}
|
|
1004
1045
|
|
|
1005
1046
|
const contentType = response.headers.get("content-type") ?? "";
|
package/src/server/effort-row.ts
CHANGED
|
@@ -30,7 +30,9 @@ export interface EffortRowOptions {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
export function isKnownId(knownIds: EffortRowKnownIds | undefined, id: string): boolean {
|
|
33
|
-
return typeof knownIds === "function"
|
|
33
|
+
return typeof knownIds === "function"
|
|
34
|
+
? knownIds(id)
|
|
35
|
+
: knownIds?.has(id) === true || knownIds?.has(id.toLowerCase()) === true;
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
export function effortRowId(baseId: string, effort: string): string {
|
|
@@ -43,6 +45,12 @@ export function effortRowId(baseId: string, effort: string): string {
|
|
|
43
45
|
*/
|
|
44
46
|
export function knownEffortRowIds(config: OcxConfig): Set<string> {
|
|
45
47
|
const ids = new Set<string>();
|
|
48
|
+
const addAlias = (id: string): void => {
|
|
49
|
+
ids.add(id);
|
|
50
|
+
// Model aliases are resolved case-insensitively. Preserve that contract here so an
|
|
51
|
+
// alias cannot be mistaken for a synthetic effort row before ordinary routing runs.
|
|
52
|
+
ids.add(id.toLowerCase());
|
|
53
|
+
};
|
|
46
54
|
for (const [providerName, provider] of Object.entries(config.providers)) {
|
|
47
55
|
const known = knownModelIdsForProvider(providerName, provider, config);
|
|
48
56
|
const namespaces = [providerName, provider.alias].filter((value): value is string => (
|
|
@@ -54,8 +62,8 @@ export function knownEffortRowIds(config: OcxConfig): Set<string> {
|
|
|
54
62
|
for (const namespace of namespaces) ids.add(`${namespace}/${id}`);
|
|
55
63
|
}
|
|
56
64
|
for (const alias of Object.values(provider.modelAliases ?? {})) {
|
|
57
|
-
|
|
58
|
-
for (const namespace of namespaces)
|
|
65
|
+
addAlias(alias);
|
|
66
|
+
for (const namespace of namespaces) addAlias(`${namespace}/${alias}`);
|
|
59
67
|
}
|
|
60
68
|
}
|
|
61
69
|
for (const [id, combo] of Object.entries(config.combos ?? {})) {
|
|
@@ -1,10 +1,137 @@
|
|
|
1
|
-
import { sseDataPayload, type SseBlockRewrite } from "./sse-payload-rewrite";
|
|
1
|
+
import { replaceSseDataPayload, sseDataPayload, type SseBlockRewrite } from "./sse-payload-rewrite";
|
|
2
2
|
|
|
3
3
|
const GROK_CONTROL_FRAME_TYPES: Record<string, true> = {
|
|
4
4
|
"codex.rate_limits": true,
|
|
5
5
|
"codex.response.metadata": true,
|
|
6
6
|
};
|
|
7
7
|
|
|
8
|
+
const GROK_RESPONSE_INTEGER_TIMESTAMP_FIELDS = ["created_at", "completed_at"] as const;
|
|
9
|
+
|
|
10
|
+
type JsonRange = { start: number; end: number };
|
|
11
|
+
|
|
12
|
+
function skipJsonWhitespace(payload: string, index: number): number {
|
|
13
|
+
while (index < payload.length && /[\t\n\r ]/.test(payload[index] ?? "")) index += 1;
|
|
14
|
+
return index;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function findJsonStringEnd(payload: string, start: number): number | undefined {
|
|
18
|
+
for (let index = start + 1; index < payload.length; index += 1) {
|
|
19
|
+
if (payload[index] === "\\") index += 1;
|
|
20
|
+
else if (payload[index] === '"') return index + 1;
|
|
21
|
+
}
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function findJsonValueEnd(payload: string, start: number): number | undefined {
|
|
26
|
+
if (payload[start] === '"') return findJsonStringEnd(payload, start);
|
|
27
|
+
if (payload[start] !== "{" && payload[start] !== "[") {
|
|
28
|
+
let index = start;
|
|
29
|
+
while (index < payload.length && !",}]".includes(payload[index] ?? "")) index += 1;
|
|
30
|
+
return index;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let depth = 0;
|
|
34
|
+
let inString = false;
|
|
35
|
+
for (let index = start; index < payload.length; index += 1) {
|
|
36
|
+
const char = payload[index];
|
|
37
|
+
if (inString) {
|
|
38
|
+
if (char === "\\") index += 1;
|
|
39
|
+
else if (char === '"') inString = false;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (char === '"') inString = true;
|
|
43
|
+
else if (char === "{" || char === "[") depth += 1;
|
|
44
|
+
else if (char === "}" || char === "]") {
|
|
45
|
+
depth -= 1;
|
|
46
|
+
if (depth === 0) return index + 1;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function findResponseObjectRange(payload: string): JsonRange | null {
|
|
53
|
+
let cursor = skipJsonWhitespace(payload, 0);
|
|
54
|
+
if (payload[cursor] !== "{") return null;
|
|
55
|
+
cursor += 1;
|
|
56
|
+
let responseRange: JsonRange | null = null;
|
|
57
|
+
|
|
58
|
+
while (cursor < payload.length) {
|
|
59
|
+
cursor = skipJsonWhitespace(payload, cursor);
|
|
60
|
+
if (payload[cursor] === "}") return responseRange;
|
|
61
|
+
if (payload[cursor] !== '"') return null;
|
|
62
|
+
const keyEnd = findJsonStringEnd(payload, cursor);
|
|
63
|
+
if (keyEnd === undefined) return null;
|
|
64
|
+
let key: unknown;
|
|
65
|
+
try {
|
|
66
|
+
key = JSON.parse(payload.slice(cursor, keyEnd));
|
|
67
|
+
} catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
cursor = skipJsonWhitespace(payload, keyEnd);
|
|
72
|
+
if (payload[cursor] !== ":") return null;
|
|
73
|
+
const valueStart = skipJsonWhitespace(payload, cursor + 1);
|
|
74
|
+
const valueEnd = findJsonValueEnd(payload, valueStart);
|
|
75
|
+
if (valueEnd === undefined) return null;
|
|
76
|
+
if (key === "response" && payload[valueStart] === "{") {
|
|
77
|
+
responseRange = { start: valueStart, end: valueEnd };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
cursor = skipJsonWhitespace(payload, valueEnd);
|
|
81
|
+
if (payload[cursor] === "}") return responseRange;
|
|
82
|
+
if (payload[cursor] !== ",") return null;
|
|
83
|
+
cursor += 1;
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function rewriteIntegralTimestamp(payload: string, field: string, value: number, event: unknown): string | null {
|
|
89
|
+
const responseRange = findResponseObjectRange(payload);
|
|
90
|
+
if (responseRange === null) return null;
|
|
91
|
+
let cursor = responseRange.start + 1;
|
|
92
|
+
let candidate: { start: number; length: number } | null = null;
|
|
93
|
+
while (cursor < responseRange.end) {
|
|
94
|
+
cursor = skipJsonWhitespace(payload, cursor);
|
|
95
|
+
if (payload[cursor] === "}") break;
|
|
96
|
+
if (payload[cursor] !== '"') return null;
|
|
97
|
+
const keyEnd = findJsonStringEnd(payload, cursor);
|
|
98
|
+
if (keyEnd === undefined) return null;
|
|
99
|
+
let key: unknown;
|
|
100
|
+
try {
|
|
101
|
+
key = JSON.parse(payload.slice(cursor, keyEnd));
|
|
102
|
+
} catch {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
cursor = skipJsonWhitespace(payload, keyEnd);
|
|
106
|
+
if (payload[cursor] !== ":") return null;
|
|
107
|
+
const valueStart = skipJsonWhitespace(payload, cursor + 1);
|
|
108
|
+
const valueEnd = findJsonValueEnd(payload, valueStart);
|
|
109
|
+
if (valueEnd === undefined || valueEnd > responseRange.end) return null;
|
|
110
|
+
const valueToken = payload.slice(valueStart, valueEnd).trimEnd();
|
|
111
|
+
if (
|
|
112
|
+
key === field &&
|
|
113
|
+
/^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$/.test(valueToken) &&
|
|
114
|
+
valueToken !== String(value) &&
|
|
115
|
+
Number(valueToken) === value
|
|
116
|
+
) {
|
|
117
|
+
if (candidate !== null) return null;
|
|
118
|
+
candidate = { start: valueStart, length: valueToken.length };
|
|
119
|
+
}
|
|
120
|
+
cursor = skipJsonWhitespace(payload, valueEnd);
|
|
121
|
+
if (payload[cursor] === "}") break;
|
|
122
|
+
if (payload[cursor] !== ",") return null;
|
|
123
|
+
cursor += 1;
|
|
124
|
+
}
|
|
125
|
+
if (candidate === null) return payload;
|
|
126
|
+
|
|
127
|
+
const rewritten = `${payload.slice(0, candidate.start)}${value}${payload.slice(candidate.start + candidate.length)}`;
|
|
128
|
+
try {
|
|
129
|
+
return JSON.stringify(JSON.parse(rewritten)) === JSON.stringify(event) ? rewritten : null;
|
|
130
|
+
} catch {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
8
135
|
/**
|
|
9
136
|
* Hide Codex-only control frames from Grok's strict Responses decoder.
|
|
10
137
|
*
|
|
@@ -41,3 +168,35 @@ export function createGrokResponsesControlFrameBlockRewrite(): SseBlockRewrite {
|
|
|
41
168
|
: [block];
|
|
42
169
|
};
|
|
43
170
|
}
|
|
171
|
+
|
|
172
|
+
/** Normalize safe integer response timestamps for Grok's strict Responses decoder. */
|
|
173
|
+
export function createGrokResponsesTimestampBlockRewrite(): SseBlockRewrite {
|
|
174
|
+
return (block) => {
|
|
175
|
+
const payload = sseDataPayload(block);
|
|
176
|
+
if (payload === null || payload === "[DONE]") return [block];
|
|
177
|
+
|
|
178
|
+
let event: unknown;
|
|
179
|
+
try {
|
|
180
|
+
event = JSON.parse(payload);
|
|
181
|
+
} catch {
|
|
182
|
+
return [block];
|
|
183
|
+
}
|
|
184
|
+
if (!event || typeof event !== "object" || Array.isArray(event)) return [block];
|
|
185
|
+
const eventRecord = event as Record<string, unknown>;
|
|
186
|
+
if (typeof eventRecord.type !== "string" || !eventRecord.type.startsWith("response.")) return [block];
|
|
187
|
+
if (!eventRecord.response || typeof eventRecord.response !== "object" || Array.isArray(eventRecord.response)) {
|
|
188
|
+
return [block];
|
|
189
|
+
}
|
|
190
|
+
const response = eventRecord.response as Record<string, unknown>;
|
|
191
|
+
let rewrittenPayload = payload;
|
|
192
|
+
for (const field of GROK_RESPONSE_INTEGER_TIMESTAMP_FIELDS) {
|
|
193
|
+
const value = response[field];
|
|
194
|
+
if (value === undefined) continue;
|
|
195
|
+
if (typeof value !== "number" || value < 0 || !Number.isSafeInteger(value)) return [block];
|
|
196
|
+
const rewritten = rewriteIntegralTimestamp(rewrittenPayload, field, value, event);
|
|
197
|
+
if (rewritten === null) return [block];
|
|
198
|
+
rewrittenPayload = rewritten;
|
|
199
|
+
}
|
|
200
|
+
return rewrittenPayload === payload ? [block] : [replaceSseDataPayload(block, rewrittenPayload)];
|
|
201
|
+
};
|
|
202
|
+
}
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
/** Strict terminal reconstruction selected by the Grok compatibility marker. */
|
|
2
2
|
import type { TranslatorBudget } from "../lib/translator-budget";
|
|
3
3
|
import { MAX_COMPLETED_OUTPUT_ITEMS, MAX_COMPLETED_OUTPUT_ITEM_SOURCE_BYTES } from "./relay";
|
|
4
|
-
import { sseDataPayload, type SseBlockRewrite } from "./sse-payload-rewrite";
|
|
4
|
+
import { replaceSseDataPayload, sseDataPayload, type SseBlockRewrite } from "./sse-payload-rewrite";
|
|
5
5
|
import { isPlainObject, jsonBlock, type RetainedOutputItem } from "./responses-snapshot-codec";
|
|
6
|
+
import {
|
|
7
|
+
requestToolScope,
|
|
8
|
+
type RequestToolScope,
|
|
9
|
+
type RequestToolScopeCorrespondence,
|
|
10
|
+
} from "./responses-request-tool-scope";
|
|
6
11
|
|
|
7
12
|
type SparseTerminalOpenItem = {
|
|
8
13
|
type: string;
|
|
@@ -16,6 +21,20 @@ type SparseTerminalCompletedItem = RetainedOutputItem & {
|
|
|
16
21
|
|
|
17
22
|
const MAX_GROK_OPEN_ITEM_IDENTITY_BYTES = MAX_COMPLETED_OUTPUT_ITEM_SOURCE_BYTES;
|
|
18
23
|
|
|
24
|
+
/** Terminal event this repair publishes when it refused to reconstruct a call faithfully. */
|
|
25
|
+
export const GROK_REFUSED_TERMINAL_EVENT_TYPE = "response.incomplete";
|
|
26
|
+
|
|
27
|
+
/** `incomplete_details.reason` carried by that terminal. */
|
|
28
|
+
export const GROK_FORBIDDEN_TOOL_CALL_REASON = "forbidden_tool_call";
|
|
29
|
+
|
|
30
|
+
/** An upstream-supplied name reaches the terminal message; keep it bounded. */
|
|
31
|
+
const MAX_REPORTED_TOOL_NAME_CHARS = 100;
|
|
32
|
+
|
|
33
|
+
export function forbiddenToolCallMessage(name: string): string {
|
|
34
|
+
return `routed provider called "${name.slice(0, MAX_REPORTED_TOOL_NAME_CHARS)}", `
|
|
35
|
+
+ "which this request's tool selection excludes; the reconstructed output omits that call";
|
|
36
|
+
}
|
|
37
|
+
|
|
19
38
|
const GROK_TERMINAL_OUTPUT_ITEM_TYPES = new Set([
|
|
20
39
|
"message",
|
|
21
40
|
"reasoning",
|
|
@@ -167,6 +186,45 @@ function plausibleGrokOpenItem(
|
|
|
167
186
|
};
|
|
168
187
|
}
|
|
169
188
|
|
|
189
|
+
/**
|
|
190
|
+
* Publish the refusal on the terminal itself rather than as a silent omission.
|
|
191
|
+
*
|
|
192
|
+
* The ordinary reconstruction replaces only the data payload's `output`, so its event name still
|
|
193
|
+
* describes the payload. A refusal does not: the turn no longer completed the way the upstream
|
|
194
|
+
* said it did, so the event line moves with the status instead of leaving a client to read a
|
|
195
|
+
* clean finish off an unchanged `event: response.completed`.
|
|
196
|
+
*/
|
|
197
|
+
function refusedTerminalBlock(
|
|
198
|
+
block: string,
|
|
199
|
+
parsed: Record<string, unknown>,
|
|
200
|
+
response: Record<string, unknown>,
|
|
201
|
+
output: readonly Record<string, unknown>[],
|
|
202
|
+
refusedName: string | undefined,
|
|
203
|
+
): string {
|
|
204
|
+
const payload = JSON.stringify({
|
|
205
|
+
...parsed,
|
|
206
|
+
type: GROK_REFUSED_TERMINAL_EVENT_TYPE,
|
|
207
|
+
response: {
|
|
208
|
+
...response,
|
|
209
|
+
status: "incomplete",
|
|
210
|
+
output,
|
|
211
|
+
incomplete_details: {
|
|
212
|
+
reason: GROK_FORBIDDEN_TOOL_CALL_REASON,
|
|
213
|
+
...(refusedName === undefined ? {} : { message: forbiddenToolCallMessage(refusedName) }),
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
const rewritten = replaceSseDataPayload(block, payload);
|
|
218
|
+
const newline = block.includes("\r\n") ? "\r\n" : "\n";
|
|
219
|
+
let eventRewritten = false;
|
|
220
|
+
const lines = rewritten.split(/\r?\n/).map(line => {
|
|
221
|
+
if (eventRewritten || !line.startsWith("event:")) return line;
|
|
222
|
+
eventRewritten = true;
|
|
223
|
+
return `event: ${GROK_REFUSED_TERMINAL_EVENT_TYPE}`;
|
|
224
|
+
});
|
|
225
|
+
return lines.join(newline);
|
|
226
|
+
}
|
|
227
|
+
|
|
170
228
|
/**
|
|
171
229
|
* Narrow client repair for grok-build's Responses consumer.
|
|
172
230
|
*
|
|
@@ -176,12 +234,26 @@ function plausibleGrokOpenItem(
|
|
|
176
234
|
* empty output. Reconstruct only from real, unique, contiguous, bounded done
|
|
177
235
|
* events whose raw semantics are already valid. Any ambiguity stays byte-level
|
|
178
236
|
* fail-closed; the provider-opt-in snapshot repair above is unchanged.
|
|
237
|
+
*
|
|
238
|
+
* The terminal this publishes is one the upstream never sent, so it carries only what the final
|
|
239
|
+
* outbound request still authorized. A client call outside that request's tool selection is left
|
|
240
|
+
* out and the terminal says so explicitly. The declaration guard downstream answers the other
|
|
241
|
+
* half of the question — whether a name was declared at all — and keeps policing the raw stream,
|
|
242
|
+
* which this rewrite never edits.
|
|
179
243
|
*/
|
|
180
244
|
export function createGrokResponsesSparseTerminalBlockRewrite(
|
|
181
245
|
budget?: TranslatorBudget,
|
|
246
|
+
outboundRequestBody?: unknown,
|
|
247
|
+
toolIdentityCorrespondence?: RequestToolScopeCorrespondence,
|
|
182
248
|
): SseBlockRewrite {
|
|
249
|
+
const toolScope: RequestToolScope | undefined = requestToolScope(
|
|
250
|
+
outboundRequestBody,
|
|
251
|
+
toolIdentityCorrespondence,
|
|
252
|
+
);
|
|
183
253
|
const openItems = new Map<number, SparseTerminalOpenItem>();
|
|
184
254
|
const completedItems = new Map<number, SparseTerminalCompletedItem>();
|
|
255
|
+
const withheldIndices = new Set<number>();
|
|
256
|
+
let withheldToolName: string | undefined;
|
|
185
257
|
let aggregateItemBytes = 0;
|
|
186
258
|
let aggregateOpenItemBytes = 0;
|
|
187
259
|
let tainted = false;
|
|
@@ -194,6 +266,8 @@ export function createGrokResponsesSparseTerminalBlockRewrite(
|
|
|
194
266
|
}
|
|
195
267
|
openItems.clear();
|
|
196
268
|
completedItems.clear();
|
|
269
|
+
withheldIndices.clear();
|
|
270
|
+
withheldToolName = undefined;
|
|
197
271
|
aggregateItemBytes = 0;
|
|
198
272
|
aggregateOpenItemBytes = 0;
|
|
199
273
|
hasVisibleOutput = false;
|
|
@@ -217,7 +291,7 @@ export function createGrokResponsesSparseTerminalBlockRewrite(
|
|
|
217
291
|
if (tainted) return;
|
|
218
292
|
const sourceBytes = Buffer.byteLength(JSON.stringify(item), "utf8");
|
|
219
293
|
if (sourceBytes > MAX_COMPLETED_OUTPUT_ITEM_SOURCE_BYTES
|
|
220
|
-
|| completedItems.size >= MAX_COMPLETED_OUTPUT_ITEMS
|
|
294
|
+
|| completedItems.size + withheldIndices.size >= MAX_COMPLETED_OUTPUT_ITEMS
|
|
221
295
|
|| aggregateItemBytes + sourceBytes > MAX_COMPLETED_OUTPUT_ITEM_SOURCE_BYTES) {
|
|
222
296
|
taintAndRelease();
|
|
223
297
|
return;
|
|
@@ -228,6 +302,24 @@ export function createGrokResponsesSparseTerminalBlockRewrite(
|
|
|
228
302
|
hasVisibleOutput = hasVisibleOutput || visibleToGrok;
|
|
229
303
|
};
|
|
230
304
|
|
|
305
|
+
/**
|
|
306
|
+
* Record the position of a call this request forbade without retaining the item.
|
|
307
|
+
*
|
|
308
|
+
* Only the offending item is dropped. Tainting here instead would discard the assistant text
|
|
309
|
+
* that arrived in the same turn and leave the client the empty terminal this repair exists to
|
|
310
|
+
* fix, which punishes the caller for the provider's overreach. The index is kept so the
|
|
311
|
+
* contiguity proof below still covers the whole output.
|
|
312
|
+
*/
|
|
313
|
+
const withholdForbiddenCall = (index: number, name: string): void => {
|
|
314
|
+
if (tainted) return;
|
|
315
|
+
if (completedItems.size + withheldIndices.size >= MAX_COMPLETED_OUTPUT_ITEMS) {
|
|
316
|
+
taintAndRelease();
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
withheldIndices.add(index);
|
|
320
|
+
withheldToolName ??= name.slice(0, MAX_REPORTED_TOOL_NAME_CHARS);
|
|
321
|
+
};
|
|
322
|
+
|
|
231
323
|
const closeOpenItem = (index: number): void => {
|
|
232
324
|
const open = openItems.get(index);
|
|
233
325
|
if (!open) return;
|
|
@@ -265,6 +357,7 @@ export function createGrokResponsesSparseTerminalBlockRewrite(
|
|
|
265
357
|
const open = isPlainObject(parsed.item) ? plausibleGrokOpenItem(parsed.item) : null;
|
|
266
358
|
if (outputIndex === undefined || !open
|
|
267
359
|
|| openItems.has(outputIndex) || completedItems.has(outputIndex)
|
|
360
|
+
|| withheldIndices.has(outputIndex)
|
|
268
361
|
|| openItems.size >= MAX_COMPLETED_OUTPUT_ITEMS) {
|
|
269
362
|
taintAndRelease();
|
|
270
363
|
} else if (!tainted) {
|
|
@@ -284,7 +377,8 @@ export function createGrokResponsesSparseTerminalBlockRewrite(
|
|
|
284
377
|
if (type === "response.output_item.done") {
|
|
285
378
|
const item = isPlainObject(parsed.item) ? parsed.item : null;
|
|
286
379
|
const proof = item ? trustedGrokCompletedItem(item) : null;
|
|
287
|
-
if (outputIndex === undefined || !proof
|
|
380
|
+
if (outputIndex === undefined || !proof
|
|
381
|
+
|| completedItems.has(outputIndex) || withheldIndices.has(outputIndex)) {
|
|
288
382
|
taintAndRelease();
|
|
289
383
|
return [block];
|
|
290
384
|
}
|
|
@@ -295,7 +389,12 @@ export function createGrokResponsesSparseTerminalBlockRewrite(
|
|
|
295
389
|
return [block];
|
|
296
390
|
}
|
|
297
391
|
closeOpenItem(outputIndex);
|
|
298
|
-
|
|
392
|
+
const forbidden = toolScope?.forbiddenClientToolCallName(item!);
|
|
393
|
+
if (forbidden === undefined) {
|
|
394
|
+
retainCompletedItem(outputIndex, item!, proof.visibleToGrok);
|
|
395
|
+
} else {
|
|
396
|
+
withholdForbiddenCall(outputIndex, forbidden);
|
|
397
|
+
}
|
|
299
398
|
return [block];
|
|
300
399
|
}
|
|
301
400
|
|
|
@@ -312,14 +411,18 @@ export function createGrokResponsesSparseTerminalBlockRewrite(
|
|
|
312
411
|
const outputIsAuthoritative = Array.isArray(output) && output.length > 0;
|
|
313
412
|
const outputIsSparse = !("output" in response)
|
|
314
413
|
|| (Array.isArray(output) && output.length === 0);
|
|
414
|
+
// A withheld call is a reason to publish on its own: the refusal has to reach the client
|
|
415
|
+
// even when nothing visible survived it, or the turn ends as an ordinary empty finish.
|
|
416
|
+
const refused = withheldIndices.size > 0;
|
|
315
417
|
if (!outputIsAuthoritative && outputIsSparse && terminalStatusConsistent
|
|
316
|
-
&&
|
|
418
|
+
&& openItems.size === 0 && (hasVisibleOutput || refused)) {
|
|
317
419
|
const ordered = [...completedItems.entries()].sort(([left], [right]) => left - right);
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
420
|
+
const positions = [...completedItems.keys(), ...withheldIndices].sort((left, right) => left - right);
|
|
421
|
+
if (positions.length > 0 && positions.every((index, position) => index === position)) {
|
|
422
|
+
const rebuilt = ordered.map(([, retained]) => retained.item);
|
|
423
|
+
out = refused
|
|
424
|
+
? refusedTerminalBlock(block, parsed, response, rebuilt, withheldToolName)
|
|
425
|
+
: jsonBlock({ ...parsed, response: { ...response, output: rebuilt } });
|
|
323
426
|
}
|
|
324
427
|
}
|
|
325
428
|
}
|
|
@@ -330,4 +433,3 @@ export function createGrokResponsesSparseTerminalBlockRewrite(
|
|
|
330
433
|
rewrite.dispose = reset;
|
|
331
434
|
return rewrite;
|
|
332
435
|
}
|
|
333
|
-
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Whether the served dashboard bundle predates the sources it was built from.
|
|
6
|
+
*
|
|
7
|
+
* The dashboard is a build artifact: `AGENTS.md` records that `gui/` is served from `gui/dist`,
|
|
8
|
+
* so a checkout that moves forward without `bun run build:gui` keeps serving the previous bundle.
|
|
9
|
+
* Nothing fails when that happens. The proxy answers, the page loads, and every feature added since
|
|
10
|
+
* the last build is simply absent — which reads as the feature being broken rather than unbuilt.
|
|
11
|
+
* A five-day-old bundle hid the entire menu-bar and widget section of the Usage page this way.
|
|
12
|
+
*
|
|
13
|
+
* This reports; it never rebuilds. A proxy that compiled a frontend while starting would trade a
|
|
14
|
+
* silent staleness for a slow, surprising start, and the rebuild belongs to whoever moved the
|
|
15
|
+
* checkout.
|
|
16
|
+
*/
|
|
17
|
+
export interface GuiBundleFreshness {
|
|
18
|
+
/** Absolute path of the served bundle, or null when no bundle was found. */
|
|
19
|
+
bundlePath: string | null;
|
|
20
|
+
/** Newest mtime under the bundle, in epoch milliseconds. */
|
|
21
|
+
bundleModifiedMs: number | null;
|
|
22
|
+
/** Newest mtime under the GUI sources, in epoch milliseconds. */
|
|
23
|
+
sourceModifiedMs: number | null;
|
|
24
|
+
/** True only when both sides are known and the sources are strictly newer. */
|
|
25
|
+
stale: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Directory entries that never carry meaningful build input or output timestamps. */
|
|
29
|
+
const IGNORED_DIRECTORIES = new Set(["node_modules", ".git", ".vite", ".cache"]);
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Newest mtime beneath `root`, or null when the tree is missing or empty.
|
|
33
|
+
*
|
|
34
|
+
* Walking is bounded by `maxEntries` because this runs on a status path: a pathological tree must
|
|
35
|
+
* cost a predictable amount rather than stalling the command that reports on it. Hitting the bound
|
|
36
|
+
* yields the newest value seen so far, which can only make the comparison more conservative.
|
|
37
|
+
*/
|
|
38
|
+
export function newestModifiedMs(root: string, maxEntries = 20_000): number | null {
|
|
39
|
+
if (!existsSync(root)) return null;
|
|
40
|
+
let newest: number | null = null;
|
|
41
|
+
let seen = 0;
|
|
42
|
+
const queue: string[] = [root];
|
|
43
|
+
while (queue.length > 0) {
|
|
44
|
+
const current = queue.pop()!;
|
|
45
|
+
let entries;
|
|
46
|
+
try {
|
|
47
|
+
entries = readdirSync(current, { withFileTypes: true });
|
|
48
|
+
} catch {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
for (const entry of entries) {
|
|
52
|
+
if (seen >= maxEntries) return newest;
|
|
53
|
+
if (entry.isSymbolicLink()) continue;
|
|
54
|
+
const full = join(current, entry.name);
|
|
55
|
+
if (entry.isDirectory()) {
|
|
56
|
+
if (IGNORED_DIRECTORIES.has(entry.name)) continue;
|
|
57
|
+
queue.push(full);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (!entry.isFile()) continue;
|
|
61
|
+
seen += 1;
|
|
62
|
+
try {
|
|
63
|
+
const mtime = statSync(full).mtimeMs;
|
|
64
|
+
if (newest === null || mtime > newest) newest = mtime;
|
|
65
|
+
} catch {
|
|
66
|
+
// A file that vanished mid-walk cannot make the bundle look fresher than it is.
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return newest;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Compare the built bundle against its sources.
|
|
75
|
+
*
|
|
76
|
+
* Unknown is not stale. A packaged install has no `gui/src` beside it, and a missing bundle is a
|
|
77
|
+
* different condition with its own message elsewhere; neither should produce a rebuild warning.
|
|
78
|
+
*/
|
|
79
|
+
export function inspectGuiBundleFreshness(input: {
|
|
80
|
+
bundlePath: string | null;
|
|
81
|
+
sourcePath: string;
|
|
82
|
+
}): GuiBundleFreshness {
|
|
83
|
+
const bundleModifiedMs = input.bundlePath === null ? null : newestModifiedMs(input.bundlePath);
|
|
84
|
+
const sourceModifiedMs = newestModifiedMs(input.sourcePath);
|
|
85
|
+
const stale = bundleModifiedMs !== null
|
|
86
|
+
&& sourceModifiedMs !== null
|
|
87
|
+
&& sourceModifiedMs > bundleModifiedMs;
|
|
88
|
+
return {
|
|
89
|
+
bundlePath: input.bundlePath,
|
|
90
|
+
bundleModifiedMs,
|
|
91
|
+
sourceModifiedMs,
|
|
92
|
+
stale,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Operator-facing lines for a stale bundle; empty when the bundle is current or unknown. */
|
|
97
|
+
export function staleGuiBundleLines(freshness: GuiBundleFreshness): string[] {
|
|
98
|
+
if (!freshness.stale) return [];
|
|
99
|
+
return [
|
|
100
|
+
"Dashboard bundle is older than the GUI sources, so the page is missing everything built since.",
|
|
101
|
+
"Rebuild it with: bun run build:gui",
|
|
102
|
+
];
|
|
103
|
+
}
|