@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
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The telemetry vocabulary both the proxy and the dashboard read.
|
|
3
|
+
*
|
|
4
|
+
* This module has NO imports, and that is its entire job. The dashboard is a separate TypeScript
|
|
5
|
+
* project with `erasableSyntaxOnly`, and a type-only import still pulls the imported file's whole
|
|
6
|
+
* import graph into that project. Importing these names from `./log` therefore dragged
|
|
7
|
+
* `node:fs`, `node:crypto` and the config barrel into the browser build, where a parameter
|
|
8
|
+
* property in `src/config/atomic-write.ts` fails to compile. The names below are the ones a
|
|
9
|
+
* browser legitimately needs, so they live where a browser can reach them.
|
|
10
|
+
*
|
|
11
|
+
* Anything added here must stay free of imports. A contract that acquires a dependency stops
|
|
12
|
+
* being a contract.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Recovery kinds recorded per attempt in the usage log; the dashboard renders localized labels
|
|
17
|
+
* for these wire values.
|
|
18
|
+
*
|
|
19
|
+
* The roster is the single statement of this vocabulary and the type is derived from it. It was
|
|
20
|
+
* written twice once -- as a union and as the read-back whitelist -- and the two are not
|
|
21
|
+
* interchangeable: a member added only to the union compiles, is written to disk, and is dropped
|
|
22
|
+
* on the next read, so the row loses the field that says why it recovered. One declaration cannot
|
|
23
|
+
* drift from itself, and the dashboard now reads this one rather than keeping a third copy.
|
|
24
|
+
*/
|
|
25
|
+
export const ATTEMPT_RECOVERY_KIND_ROSTER = Object.freeze([
|
|
26
|
+
"transient-5xx",
|
|
27
|
+
"connection-reset",
|
|
28
|
+
"oauth-401",
|
|
29
|
+
"key-401",
|
|
30
|
+
"key-429",
|
|
31
|
+
"rate-limit-429",
|
|
32
|
+
"anthropic-oauth-429",
|
|
33
|
+
"oauth-account-429",
|
|
34
|
+
"image-413",
|
|
35
|
+
"console-go-upload-retry",
|
|
36
|
+
"opaque-blob-rejection",
|
|
37
|
+
"empty-completion",
|
|
38
|
+
"reasoning-effort-downgrade",
|
|
39
|
+
] as const);
|
|
40
|
+
|
|
41
|
+
export type AttemptRecoveryKind = typeof ATTEMPT_RECOVERY_KIND_ROSTER[number];
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Why a recovery this request was otherwise willing to make did not happen.
|
|
45
|
+
*
|
|
46
|
+
* Recorded separately from `recoveryKinds` and from `sendCount` because the question it answers
|
|
47
|
+
* is different from either. A log showing one physical send and no recovery kind used to be
|
|
48
|
+
* ambiguous: nothing was eligible, or something was and the send budget withheld it. Those need
|
|
49
|
+
* opposite follow-ups and the second was invisible (#5044).
|
|
50
|
+
*
|
|
51
|
+
* `sendCount` deliberately does not move for these. A refused attempt is not a physical send, and
|
|
52
|
+
* inflating the count to signal the refusal would corrupt the one number that means "requests this
|
|
53
|
+
* proxy actually made".
|
|
54
|
+
*/
|
|
55
|
+
export const ATTEMPT_RECOVERY_WITHHELD_ROSTER = Object.freeze([
|
|
56
|
+
"retry-send-budget",
|
|
57
|
+
"rotation-send-budget",
|
|
58
|
+
] as const);
|
|
59
|
+
|
|
60
|
+
export type AttemptRecoveryWithheld = typeof ATTEMPT_RECOVERY_WITHHELD_ROSTER[number];
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* How far a failed exchange got, ordered by how much the DOWNSTREAM CLIENT observed.
|
|
64
|
+
*
|
|
65
|
+
* The order is by client observation rather than by upstream progress, because the question it
|
|
66
|
+
* answers is whether resending can duplicate something the caller already saw. An upstream that
|
|
67
|
+
* completed a turn we never relayed has committed nothing downstream; an upstream that emitted
|
|
68
|
+
* one token has.
|
|
69
|
+
*
|
|
70
|
+
* The roster lives here rather than beside the resend tables for the reason stated at the top of
|
|
71
|
+
* this file: the dashboard renders a label per member, and reaching the table module for the
|
|
72
|
+
* names would drag its import graph into the browser project. `src/lib/request-failure-model.ts`
|
|
73
|
+
* re-exports it, so every existing importer keeps its path and there is still exactly one
|
|
74
|
+
* declaration.
|
|
75
|
+
*/
|
|
76
|
+
export const REQUEST_FAILURE_STAGES = Object.freeze([
|
|
77
|
+
/** No response head exists. Whether the origin began the turn is not known from the stage alone. */
|
|
78
|
+
"pre-header",
|
|
79
|
+
/** A status line and headers exist, and no protocol body event has been parsed yet. */
|
|
80
|
+
"headers-only",
|
|
81
|
+
/** The protocol body began with control events only -- `response.created`, quota frames. */
|
|
82
|
+
"protocol-prelude",
|
|
83
|
+
/** At least one output-bearing event reached the caller. */
|
|
84
|
+
"semantic-output",
|
|
85
|
+
/** A tool call or other externally visible effect was emitted. */
|
|
86
|
+
"side-effect",
|
|
87
|
+
/** A terminal event settled the turn after its answer reached the caller. */
|
|
88
|
+
"terminal",
|
|
89
|
+
] as const);
|
|
90
|
+
|
|
91
|
+
export type RequestFailureStage = typeof REQUEST_FAILURE_STAGES[number];
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Why the request failed, as one closed dictionary for every layer.
|
|
95
|
+
*
|
|
96
|
+
* Bounded on purpose: these are wire values a maintainer reads and a metric labels by, never a
|
|
97
|
+
* credential, an account identifier, an upstream body or prompt content. That bound is what lets
|
|
98
|
+
* the value be a Prometheus label and a grouping key without a masking pass -- a closed roster
|
|
99
|
+
* has nothing to mask.
|
|
100
|
+
*/
|
|
101
|
+
export const REQUEST_FAILURE_CAUSES = Object.freeze([
|
|
102
|
+
/** The bytes provably never reached the origin: connect refused, DNS failure, TLS handshake. */
|
|
103
|
+
"transport-unsent",
|
|
104
|
+
/** The bytes left and the connection died before a head. The origin may be running the turn. */
|
|
105
|
+
"transport-ambiguous",
|
|
106
|
+
/** The origin answered that it would not start the turn now: 503, overloaded, backpressure. */
|
|
107
|
+
"upstream-declined",
|
|
108
|
+
/** A 429 rate limit. Capacity is momentarily gone; waiting is the remedy. */
|
|
109
|
+
"rate-limit",
|
|
110
|
+
/** Plan or credit quota is gone. Waiting out a retry window does not help; the account must change. */
|
|
111
|
+
"quota-exhausted",
|
|
112
|
+
/** Credentials were rejected: 401, 403 on identity. */
|
|
113
|
+
"credential-rejected",
|
|
114
|
+
/** The origin evaluated the content and refused it. Identical bytes get the identical refusal. */
|
|
115
|
+
"policy-refusal",
|
|
116
|
+
/**
|
|
117
|
+
* The origin rejected a request PARAMETER rather than the content: an unsupported reasoning
|
|
118
|
+
* effort, an unknown field. Distinct from `policy-refusal` because the remedy is opposite --
|
|
119
|
+
* the same content succeeds once the parameter is adjusted.
|
|
120
|
+
*/
|
|
121
|
+
"parameter-rejected",
|
|
122
|
+
/** Opaque replay state was rejected as unverifiable. Only a request without it can succeed. */
|
|
123
|
+
"ciphertext-refusal",
|
|
124
|
+
/**
|
|
125
|
+
* The payload exceeded a size the origin accepts. A smaller rebuild of the same turn can
|
|
126
|
+
* succeed, which is why this is not the same answer as `payload-rejected`.
|
|
127
|
+
*/
|
|
128
|
+
"payload-too-large",
|
|
129
|
+
/** The payload was rejected on its merits: unsupported media, malformed part. No repair helps. */
|
|
130
|
+
"payload-rejected",
|
|
131
|
+
/**
|
|
132
|
+
* The origin returned a server-side fault. Whether it had already begun the turn is not
|
|
133
|
+
* knowable from the status, so this is the honest classification for the mixed 5xx set the
|
|
134
|
+
* transient layer retries: 503 really did decline, 500 may not have.
|
|
135
|
+
*/
|
|
136
|
+
"upstream-fault",
|
|
137
|
+
/** The turn settled carrying no usable output. */
|
|
138
|
+
"empty-output",
|
|
139
|
+
/** The caller went away. */
|
|
140
|
+
"client-cancelled",
|
|
141
|
+
/** This proxy refused before dispatch: send budget, route policy, replay refusal. */
|
|
142
|
+
"local-refusal",
|
|
143
|
+
] as const);
|
|
144
|
+
|
|
145
|
+
export type RequestFailureCause = typeof REQUEST_FAILURE_CAUSES[number];
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Whether this proxy may send the request again. Derived at READ time from the stage and the
|
|
149
|
+
* cause and never persisted, so a stored row cannot carry a verdict that the current table
|
|
150
|
+
* would no longer reach.
|
|
151
|
+
*
|
|
152
|
+
* Every refusal names WHY it refused, because the three reasons need different operator
|
|
153
|
+
* responses and used to arrive as one undifferentiated "no retry".
|
|
154
|
+
*/
|
|
155
|
+
export const RESEND_PERMISSIONS = Object.freeze([
|
|
156
|
+
/** The same request may be sent again. */
|
|
157
|
+
"permitted",
|
|
158
|
+
/** Only a modified request may be sent: rotated credential, stripped ciphertext. */
|
|
159
|
+
"permitted-after-repair",
|
|
160
|
+
/** Upstream execution state is unknown. No AUTOMATIC resend. */
|
|
161
|
+
"refused-ambiguous",
|
|
162
|
+
/** The caller already observed output or an externally visible effect. */
|
|
163
|
+
"refused-committed",
|
|
164
|
+
/** Identical bytes would get the identical answer. */
|
|
165
|
+
"refused-futile",
|
|
166
|
+
] as const);
|
|
167
|
+
|
|
168
|
+
export type ResendPermission = typeof RESEND_PERMISSIONS[number];
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Where a terminal or failure was observed on the wire.
|
|
172
|
+
*
|
|
173
|
+
* Declared here because three modules read it as a closed set -- the durable row's validator,
|
|
174
|
+
* the failure attribution and the failure fingerprint -- and a fourth restatement in a test is
|
|
175
|
+
* how a member added later leaves an "exhaustive" cross product green without exercising it.
|
|
176
|
+
*/
|
|
177
|
+
export const REQUEST_TRANSPORT_PHASES = Object.freeze([
|
|
178
|
+
"pre_headers",
|
|
179
|
+
"mid_stream",
|
|
180
|
+
"terminal_sse",
|
|
181
|
+
] as const);
|
|
182
|
+
|
|
183
|
+
export type RequestTransportPhase = typeof REQUEST_TRANSPORT_PHASES[number];
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* What an attempt actually delivered, as five bounded counts (#3983).
|
|
187
|
+
*
|
|
188
|
+
* #3983 wanted these signals and emitted one debug line per event to get them. That is a second
|
|
189
|
+
* durable record: `emitDebugLine` writes the in-process ring AND stderr, and stderr is redirected
|
|
190
|
+
* to the service log under both launchd and systemd, so an installed service ends up with a
|
|
191
|
+
* per-event history beside the ledger, carrying its own retention, sequencing and identity. It
|
|
192
|
+
* also fingerprinted each payload under a process-global random key, which makes every repeated
|
|
193
|
+
* prompt fragment, tool name and error message correlatable for the process lifetime.
|
|
194
|
+
*
|
|
195
|
+
* Counts answer the same questions -- a missing terminal, adapter-to-client loss, empty output,
|
|
196
|
+
* partial output size -- and cannot carry content at all. They ride the attempt, so they inherit
|
|
197
|
+
* the ledger's normalization, masking and retention rather than acquiring their own.
|
|
198
|
+
*
|
|
199
|
+
* Counted where the event is DELIVERED, not where it is read. An adapter event the client never
|
|
200
|
+
* received is exactly the discrepancy worth seeing, and counting both ends at the reader would
|
|
201
|
+
* make the two numbers equal by construction.
|
|
202
|
+
*/
|
|
203
|
+
export interface AttemptDeliverySummary {
|
|
204
|
+
/** Events this attempt's adapter produced. */
|
|
205
|
+
adapterEvents: number;
|
|
206
|
+
/** Frames that reached the client transport, after a successful enqueue. */
|
|
207
|
+
relayedEvents: number;
|
|
208
|
+
/** UTF-8 bytes of output-bearing delta actually relayed. Never the content itself. */
|
|
209
|
+
semanticBytes: number;
|
|
210
|
+
/** Externally visible effects relayed: a tool call or a search call starting. */
|
|
211
|
+
sideEffectEvents: number;
|
|
212
|
+
/** Terminal frames relayed. Zero on a delivered stream is the missing-terminal signal. */
|
|
213
|
+
terminalEvents: number;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* What one logical request spent upstream, decomposed by how much of it is explained.
|
|
218
|
+
*
|
|
219
|
+
* The counting half of the durable spend record, without the routing detail that sits beside it.
|
|
220
|
+
* Every surface that reports a send total reads these three numbers and none of them recomputes a
|
|
221
|
+
* total of its own -- a recomputed total is how the exporter and the dashboard ended up reporting
|
|
222
|
+
* different send counts for the same request.
|
|
223
|
+
*/
|
|
224
|
+
export interface RequestSpendTotals {
|
|
225
|
+
/** Physical upstream sends summed across every attempt, combo children included. */
|
|
226
|
+
sends: number;
|
|
227
|
+
/** Sends whose attempt reached a terminal status, so the spend has a known outcome. */
|
|
228
|
+
settled: number;
|
|
229
|
+
/**
|
|
230
|
+
* Sends charged with no terminal outcome behind them: an attempt abandoned mid-flight, or a
|
|
231
|
+
* budget charge no attempt row ever accounted for. Never folded into `settled` -- an unexplained
|
|
232
|
+
* send is the exact quantity this record exists to make visible.
|
|
233
|
+
*/
|
|
234
|
+
unresolved: number;
|
|
235
|
+
/** Model sends the request execution budget charged. Absent when no budget was attached. */
|
|
236
|
+
reserved?: number;
|
|
237
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { cacheTokensFromUsage, usageAttributions } from "./summary";
|
|
2
|
+
import type { PersistedUsageEntry } from "./log";
|
|
3
|
+
import { usageDisplayTotalTokens } from "./totals";
|
|
4
|
+
|
|
5
|
+
export type TimelineMetric = "total" | "input" | "output" | "cached";
|
|
6
|
+
export type TimelineAggregation = "sum" | "average" | "max";
|
|
7
|
+
export type TimelineGrouping = "model" | "modelAccount";
|
|
8
|
+
export const TIMELINE_HOURS = [6, 24, 72, 168] as const;
|
|
9
|
+
|
|
10
|
+
export interface TimelineQuery {
|
|
11
|
+
hours: typeof TIMELINE_HOURS[number];
|
|
12
|
+
bucketMinutes: number;
|
|
13
|
+
metric: TimelineMetric;
|
|
14
|
+
aggregation: TimelineAggregation;
|
|
15
|
+
grouping: TimelineGrouping;
|
|
16
|
+
models: string[] | null;
|
|
17
|
+
hiddenProviders: string[];
|
|
18
|
+
now: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface TimelineSeries {
|
|
22
|
+
id: string;
|
|
23
|
+
provider: string;
|
|
24
|
+
model: string;
|
|
25
|
+
accountLogLabel?: string;
|
|
26
|
+
total: number;
|
|
27
|
+
points: number[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface UsageTimeline {
|
|
31
|
+
appliedFilters: { models: string[] | null; hiddenProviders: string[] };
|
|
32
|
+
start: number;
|
|
33
|
+
end: number;
|
|
34
|
+
bucketSeconds: number;
|
|
35
|
+
buckets: number;
|
|
36
|
+
metric: TimelineMetric;
|
|
37
|
+
aggregation: TimelineAggregation;
|
|
38
|
+
grouping: TimelineGrouping;
|
|
39
|
+
series: TimelineSeries[];
|
|
40
|
+
availableModels: string[];
|
|
41
|
+
missingMeasurements: number;
|
|
42
|
+
truncated: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const METRICS: readonly TimelineMetric[] = ["total", "input", "output", "cached"];
|
|
46
|
+
const AGGREGATIONS: readonly TimelineAggregation[] = ["sum", "average", "max"];
|
|
47
|
+
const GROUPINGS: readonly TimelineGrouping[] = ["model", "modelAccount"];
|
|
48
|
+
|
|
49
|
+
function enumValue<T extends string>(value: string | null, values: readonly T[], fallback: T): T | { error: string } {
|
|
50
|
+
if (value === null || value === "") return fallback;
|
|
51
|
+
return values.includes(value as T) ? value as T : { error: `invalid value for parameter: ${value}` };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function isTimelineModelId(value: unknown): value is string {
|
|
55
|
+
return typeof value === "string" && /^[^/\s]+\/\S+$/.test(value);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function parseModels(raw: string | null): string[] | null | { error: string } {
|
|
59
|
+
if (raw === null || raw.trim() === "") return null;
|
|
60
|
+
const models = raw.split(",").map(model => model.trim());
|
|
61
|
+
if (models.length > 100) return { error: "models must contain at most 100 identifiers" };
|
|
62
|
+
if (models.some(model => !isTimelineModelId(model))) {
|
|
63
|
+
return { error: "models must contain provider/model identifiers" };
|
|
64
|
+
}
|
|
65
|
+
return [...new Set(models)];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function parseTimelineQuery(params: URLSearchParams, now: number): TimelineQuery | { error: string } {
|
|
69
|
+
const rawHours = params.get("hours") ?? "24";
|
|
70
|
+
const hoursNumber = Number(rawHours);
|
|
71
|
+
if (!TIMELINE_HOURS.includes(hoursNumber as typeof TIMELINE_HOURS[number])) {
|
|
72
|
+
return { error: "hours must be one of 6, 24, 72, 168" };
|
|
73
|
+
}
|
|
74
|
+
const bucketMinutes = Number(params.get("bucketMinutes") ?? "60");
|
|
75
|
+
if (!Number.isInteger(bucketMinutes) || bucketMinutes < 1 || bucketMinutes > 1440) {
|
|
76
|
+
return { error: "bucketMinutes must be an integer from 1 through 1440" };
|
|
77
|
+
}
|
|
78
|
+
const buckets = Math.ceil(hoursNumber * 60 / bucketMinutes);
|
|
79
|
+
if (buckets > 2000) return { error: "timeline bucket count must not exceed 2000" };
|
|
80
|
+
const metric = enumValue(params.get("metric"), METRICS, "total");
|
|
81
|
+
if (typeof metric !== "string") return metric;
|
|
82
|
+
const aggregation = enumValue(params.get("aggregation"), AGGREGATIONS, "sum");
|
|
83
|
+
if (typeof aggregation !== "string") return aggregation;
|
|
84
|
+
const grouping = enumValue(params.get("grouping"), GROUPINGS, "model");
|
|
85
|
+
if (typeof grouping !== "string") return grouping;
|
|
86
|
+
const models = parseModels(params.get("models"));
|
|
87
|
+
if (typeof models === "object" && models !== null && "error" in models) return models;
|
|
88
|
+
const hiddenProviders = params.getAll("hiddenProvider");
|
|
89
|
+
if (hiddenProviders.length > 100 || hiddenProviders.some(value => !value || /\s/.test(value))) {
|
|
90
|
+
return { error: "hiddenProvider must contain at most 100 nonblank provider names" };
|
|
91
|
+
}
|
|
92
|
+
if (!Number.isFinite(now)) return { error: "now must be finite" };
|
|
93
|
+
return {
|
|
94
|
+
hours: hoursNumber as TimelineQuery["hours"],
|
|
95
|
+
bucketMinutes,
|
|
96
|
+
metric,
|
|
97
|
+
aggregation,
|
|
98
|
+
grouping,
|
|
99
|
+
models: models as string[] | null,
|
|
100
|
+
hiddenProviders: [...new Set(hiddenProviders)].sort(),
|
|
101
|
+
now,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
interface SeriesState {
|
|
106
|
+
provider: string;
|
|
107
|
+
model: string;
|
|
108
|
+
accountLogLabel?: string;
|
|
109
|
+
points: number[];
|
|
110
|
+
requests: Map<number, Map<string, number>>;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function metricValue(metric: TimelineMetric, attribution: ReturnType<typeof usageAttributions>[number]): number | undefined {
|
|
114
|
+
if (metric === "total") return usageDisplayTotalTokens(attribution.usage, attribution.totalTokens);
|
|
115
|
+
if (metric === "input") return attribution.usage?.inputTokens;
|
|
116
|
+
if (metric === "output") return attribution.usage?.outputTokens;
|
|
117
|
+
return cacheTokensFromUsage(attribution.usage).read;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function createTimelineAccumulator(query: TimelineQuery): { add(entry: PersistedUsageEntry): void; finish(): UsageTimeline } {
|
|
121
|
+
const bucketSeconds = query.bucketMinutes * 60;
|
|
122
|
+
const buckets = Math.ceil(query.hours * 60 / query.bucketMinutes);
|
|
123
|
+
const end = (Math.floor(query.now / 1000 / bucketSeconds) + 1) * bucketSeconds;
|
|
124
|
+
const start = end - buckets * bucketSeconds;
|
|
125
|
+
const startMs = start * 1000;
|
|
126
|
+
const endMs = end * 1000;
|
|
127
|
+
const series = new Map<string, SeriesState>();
|
|
128
|
+
const availableModels = new Set<string>();
|
|
129
|
+
const hiddenProviders = new Set(query.hiddenProviders);
|
|
130
|
+
let missingMeasurements = 0;
|
|
131
|
+
|
|
132
|
+
function add(entry: PersistedUsageEntry): void {
|
|
133
|
+
if (entry.timestamp < startMs || entry.timestamp >= endMs) return;
|
|
134
|
+
const bucket = Math.floor((entry.timestamp - startMs) / (bucketSeconds * 1000));
|
|
135
|
+
if (bucket < 0 || bucket >= buckets) return;
|
|
136
|
+
for (const attribution of usageAttributions(entry)) {
|
|
137
|
+
if (hiddenProviders.has(attribution.provider)) continue;
|
|
138
|
+
const modelId = `${attribution.provider}/${attribution.model}`;
|
|
139
|
+
availableModels.add(modelId);
|
|
140
|
+
if (query.models && !query.models.includes(modelId)) continue;
|
|
141
|
+
const id = query.grouping === "model"
|
|
142
|
+
? modelId
|
|
143
|
+
: `${modelId} · ${attribution.accountLogLabel ?? "unknown"}`;
|
|
144
|
+
let state = series.get(id);
|
|
145
|
+
if (!state) {
|
|
146
|
+
state = {
|
|
147
|
+
provider: attribution.provider,
|
|
148
|
+
model: attribution.model,
|
|
149
|
+
...(query.grouping === "modelAccount" ? { accountLogLabel: attribution.accountLogLabel ?? "unknown" } : {}),
|
|
150
|
+
points: Array<number>(buckets).fill(0),
|
|
151
|
+
requests: new Map(),
|
|
152
|
+
};
|
|
153
|
+
series.set(id, state);
|
|
154
|
+
}
|
|
155
|
+
const value = metricValue(query.metric, attribution);
|
|
156
|
+
if (value === undefined) {
|
|
157
|
+
missingMeasurements += 1;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (query.aggregation === "sum") {
|
|
161
|
+
state.points[bucket] = (state.points[bucket] ?? 0) + value;
|
|
162
|
+
} else {
|
|
163
|
+
let requests = state.requests.get(bucket);
|
|
164
|
+
if (!requests) {
|
|
165
|
+
requests = new Map();
|
|
166
|
+
state.requests.set(bucket, requests);
|
|
167
|
+
}
|
|
168
|
+
requests.set(attribution.requestId, (requests.get(attribution.requestId) ?? 0) + value);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function finish(): UsageTimeline {
|
|
174
|
+
const rows = [...series].map(([id, state]): { row: TimelineSeries; state: SeriesState } => {
|
|
175
|
+
if (query.aggregation !== "sum") {
|
|
176
|
+
for (const [bucket, requests] of state.requests) {
|
|
177
|
+
const values = [...requests.values()];
|
|
178
|
+
state.points[bucket] = query.aggregation === "max"
|
|
179
|
+
? Math.max(...values)
|
|
180
|
+
: values.reduce((sum, value) => sum + value, 0) / values.length;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
const total = state.points.reduce((sum, value) => sum + value, 0);
|
|
184
|
+
return {
|
|
185
|
+
row: {
|
|
186
|
+
id,
|
|
187
|
+
provider: state.provider,
|
|
188
|
+
model: state.model,
|
|
189
|
+
...(state.accountLogLabel !== undefined ? { accountLogLabel: state.accountLogLabel } : {}),
|
|
190
|
+
total,
|
|
191
|
+
points: state.points,
|
|
192
|
+
},
|
|
193
|
+
state,
|
|
194
|
+
};
|
|
195
|
+
}).sort((left, right) => right.row.total - left.row.total || left.row.id.localeCompare(right.row.id));
|
|
196
|
+
const kept = (rows.length > 24 ? rows.slice(0, 23) : rows).map(({ row }) => row);
|
|
197
|
+
if (rows.length > 24) {
|
|
198
|
+
const otherPoints = Array<number>(buckets).fill(0);
|
|
199
|
+
const folded = rows.slice(23);
|
|
200
|
+
if (query.aggregation === "sum") {
|
|
201
|
+
for (const { row } of folded) {
|
|
202
|
+
for (let index = 0; index < buckets; index += 1) otherPoints[index] = (otherPoints[index] ?? 0) + (row.points[index] ?? 0);
|
|
203
|
+
}
|
|
204
|
+
} else {
|
|
205
|
+
for (let index = 0; index < buckets; index += 1) {
|
|
206
|
+
const values = folded.flatMap(({ state }) => [...(state.requests.get(index)?.values() ?? [])]);
|
|
207
|
+
if (values.length > 0) {
|
|
208
|
+
otherPoints[index] = query.aggregation === "max"
|
|
209
|
+
? Math.max(...values)
|
|
210
|
+
: values.reduce((sum, value) => sum + value, 0) / values.length;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
kept.push({ id: "other", provider: "", model: "other", total: otherPoints.reduce((sum, value) => sum + value, 0), points: otherPoints });
|
|
215
|
+
}
|
|
216
|
+
return {
|
|
217
|
+
appliedFilters: {
|
|
218
|
+
models: query.models === null ? null : [...new Set(query.models)].sort(),
|
|
219
|
+
hiddenProviders: [...hiddenProviders].sort(),
|
|
220
|
+
},
|
|
221
|
+
start,
|
|
222
|
+
end,
|
|
223
|
+
bucketSeconds,
|
|
224
|
+
buckets,
|
|
225
|
+
metric: query.metric,
|
|
226
|
+
aggregation: query.aggregation,
|
|
227
|
+
grouping: query.grouping,
|
|
228
|
+
series: kept,
|
|
229
|
+
availableModels: [...availableModels].sort(),
|
|
230
|
+
missingMeasurements,
|
|
231
|
+
truncated: false,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return { add, finish };
|
|
236
|
+
}
|
|
@@ -35,6 +35,54 @@ import { isCanonicalOpenAiForwardProvider } from "../providers/openai-tiers-dest
|
|
|
35
35
|
*/
|
|
36
36
|
export type VisionSidecarBackend = "openai" | "anthropic" | "routed";
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Input modalities an explicit custom row declares for one routed model.
|
|
40
|
+
*
|
|
41
|
+
* A custom row is the operator's own definition of that model, so its declaration outranks the
|
|
42
|
+
* provider-level hints the predicates below read. Without this the two halves of one config
|
|
43
|
+
* disagreed in production: the catalog overlay in `src/codex/catalog/routed-gather.ts` copies
|
|
44
|
+
* `customModels[].inputModalities` onto the row and the dashboard showed "text, image", while
|
|
45
|
+
* the request path consulted only `providers[].noVisionModels` / `modelInputModalities` and
|
|
46
|
+
* stripped the image before dispatch. A model the operator had declared image-capable therefore
|
|
47
|
+
* received an omission marker instead of its attachment.
|
|
48
|
+
*
|
|
49
|
+
* Precedence, highest first: `modelCapabilities` (the documented per-model capability
|
|
50
|
+
* declaration), then this custom-row declaration, then `noVisionModels`, then
|
|
51
|
+
* `modelInputModalities`, then registry/vendor metadata. `modelCapabilities` stays on top
|
|
52
|
+
* because it is the dedicated capability axis and the CLI `--text-only` flag writes it; when the
|
|
53
|
+
* two explicit forms contradict each other the more specific axis wins.
|
|
54
|
+
*
|
|
55
|
+
* Matching is exact on the routed identity — provider name and native model id, the pair the
|
|
56
|
+
* custom-model API keys rows by. A row that declares no modalities returns `undefined` rather
|
|
57
|
+
* than `["text"]`, so it stays silent instead of turning into a text-only claim.
|
|
58
|
+
*/
|
|
59
|
+
export function customRowInputModalities(
|
|
60
|
+
config: Pick<OcxConfig, "customModels">,
|
|
61
|
+
providerName: string,
|
|
62
|
+
modelId: string,
|
|
63
|
+
): string[] | undefined {
|
|
64
|
+
for (const row of config.customModels ?? []) {
|
|
65
|
+
if (row.provider !== providerName || row.modelId !== modelId) continue;
|
|
66
|
+
if (Array.isArray(row.inputModalities) && row.inputModalities.length > 0) {
|
|
67
|
+
return [...row.inputModalities];
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The custom row's verdict for one model, in the shape the predicates need:
|
|
75
|
+
* `true`/`false` when the row declares modalities, `undefined` when it declares none.
|
|
76
|
+
*/
|
|
77
|
+
function customRowAcceptsImageInput(
|
|
78
|
+
config: Pick<OcxConfig, "customModels">,
|
|
79
|
+
providerName: string,
|
|
80
|
+
modelId: string,
|
|
81
|
+
): boolean | undefined {
|
|
82
|
+
const declared = customRowInputModalities(config, providerName, modelId);
|
|
83
|
+
return declared === undefined ? undefined : declared.includes("image");
|
|
84
|
+
}
|
|
85
|
+
|
|
38
86
|
/** The two sides every deployment has; also the empty-auth fallback set. */
|
|
39
87
|
export type UniversalVisionBackend = "openai" | "anthropic";
|
|
40
88
|
|
|
@@ -60,6 +108,14 @@ export interface VisionCandidateModel {
|
|
|
60
108
|
native?: boolean;
|
|
61
109
|
}
|
|
62
110
|
|
|
111
|
+
/**
|
|
112
|
+
* The config slice the capability predicates need. `customModels` is optional so provider-only
|
|
113
|
+
* callers and unit tests keep compiling; a caller that omits it is simply silent about custom
|
|
114
|
+
* rows rather than wrong about them.
|
|
115
|
+
*/
|
|
116
|
+
export type VisionCapabilityConfig =
|
|
117
|
+
Pick<OcxConfig, "providers"> & { customModels?: OcxConfig["customModels"] };
|
|
118
|
+
|
|
63
119
|
export interface VisionModelOption {
|
|
64
120
|
value: string;
|
|
65
121
|
label: string;
|
|
@@ -142,20 +198,31 @@ function enrichedProviderForVision(
|
|
|
142
198
|
}
|
|
143
199
|
|
|
144
200
|
function isVisionSidecarConsumerWithCache(
|
|
145
|
-
config:
|
|
201
|
+
config: VisionCapabilityConfig,
|
|
146
202
|
providerName: string,
|
|
147
203
|
modelId: string,
|
|
148
204
|
cache: EnrichedProviderCache,
|
|
149
205
|
): boolean {
|
|
150
206
|
const provider = enrichedProviderForVision(config, providerName, cache);
|
|
151
|
-
|
|
207
|
+
if (provider === undefined) return false;
|
|
208
|
+
// Documented precedence, highest first: the dedicated per-model capability axis, then the
|
|
209
|
+
// operator's own custom row, then the provider-level hints. Reading the custom row ahead of
|
|
210
|
+
// `modelCapabilities` would let it override the more specific axis.
|
|
211
|
+
const capabilityDeclared = Object.hasOwn(provider.modelCapabilities ?? {}, modelId)
|
|
212
|
+
? provider.modelCapabilities?.[modelId]?.inputModalities : undefined;
|
|
213
|
+
if (capabilityDeclared !== undefined) {
|
|
214
|
+
return capabilityDeclared.includes("text") && !capabilityDeclared.includes("image");
|
|
215
|
+
}
|
|
216
|
+
const customDeclared = customRowInputModalities(config, providerName, modelId);
|
|
217
|
+
if (customDeclared !== undefined) return customDeclared.includes("text") && !customDeclared.includes("image");
|
|
218
|
+
return isModelVisionSidecarConsumer(provider, modelId);
|
|
152
219
|
}
|
|
153
220
|
|
|
154
221
|
/**
|
|
155
222
|
* Is this model listed as one the sidecar describes FOR? Such a model cannot be
|
|
156
223
|
* the describer, and its advertised modalities are untrustworthy.
|
|
157
224
|
*/
|
|
158
|
-
export function isVisionSidecarConsumer(config:
|
|
225
|
+
export function isVisionSidecarConsumer(config: VisionCapabilityConfig, providerName: string, modelId: string): boolean {
|
|
159
226
|
return isVisionSidecarConsumerWithCache(config, providerName, modelId, new Map());
|
|
160
227
|
}
|
|
161
228
|
|
|
@@ -165,23 +232,30 @@ export function isVisionSidecarConsumer(config: Pick<OcxConfig, "providers">, pr
|
|
|
165
232
|
* which callers treat as eligible.
|
|
166
233
|
*/
|
|
167
234
|
export function modelAcceptsImageInput(
|
|
168
|
-
config:
|
|
235
|
+
config: VisionCapabilityConfig,
|
|
169
236
|
candidate: VisionCandidateModel,
|
|
170
237
|
): boolean | undefined {
|
|
171
238
|
return modelAcceptsImageInputWithCache(config, candidate, new Map());
|
|
172
239
|
}
|
|
173
240
|
|
|
174
241
|
function modelAcceptsImageInputWithCache(
|
|
175
|
-
config:
|
|
242
|
+
config: VisionCapabilityConfig,
|
|
176
243
|
candidate: VisionCandidateModel,
|
|
177
244
|
cache: EnrichedProviderCache,
|
|
178
245
|
): boolean | undefined {
|
|
179
246
|
if (candidate.native === true || (candidate.provider === "openai" && SUPPORTED_NATIVE_OPENAI_SLUGS.has(candidate.id))) {
|
|
180
247
|
const nativeProvider = enrichedProviderForVision(config, candidate.provider, cache);
|
|
181
|
-
if (nativeProvider && isModelVisionSidecarConsumer(nativeProvider, candidate.id)) return false;
|
|
182
248
|
const declared = Object.hasOwn(nativeProvider?.modelCapabilities ?? {}, candidate.id)
|
|
183
249
|
? nativeProvider?.modelCapabilities?.[candidate.id]?.inputModalities : undefined;
|
|
184
250
|
if (declared !== undefined) return declared.includes("image");
|
|
251
|
+
// The catalog already lets an explicit custom row replace the native modality list for the
|
|
252
|
+
// same slug; consult it here too or the row would advertise what the request path ignores.
|
|
253
|
+
const fromCustomRow = customRowAcceptsImageInput(config, candidate.provider, candidate.id);
|
|
254
|
+
if (fromCustomRow !== undefined) return fromCustomRow;
|
|
255
|
+
// The sidecar hints come after both explicit declarations, matching the documented order.
|
|
256
|
+
// Ahead of them a `noVisionModels` membership would short-circuit to text-only before the
|
|
257
|
+
// operator's own row was read.
|
|
258
|
+
if (nativeProvider && isModelVisionSidecarConsumer(nativeProvider, candidate.id)) return false;
|
|
185
259
|
return advertisesImageInput(nativeInputModalities(candidate.id)) ?? true;
|
|
186
260
|
}
|
|
187
261
|
if (isVisionSidecarConsumerWithCache(config, candidate.provider, candidate.id, cache)) return false;
|
|
@@ -189,6 +263,11 @@ function modelAcceptsImageInputWithCache(
|
|
|
189
263
|
const declared = Object.hasOwn(provider?.modelCapabilities ?? {}, candidate.id)
|
|
190
264
|
? provider?.modelCapabilities?.[candidate.id]?.inputModalities : undefined;
|
|
191
265
|
if (declared !== undefined) return declared.includes("image");
|
|
266
|
+
// Ahead of the provider's own hints: this row is the operator's definition of this exact model,
|
|
267
|
+
// and the catalog overlay reads the same field. Behind modelCapabilities, which is the dedicated
|
|
268
|
+
// capability axis the CLI `--text-only` writes.
|
|
269
|
+
const fromCustomRow = customRowAcceptsImageInput(config, candidate.provider, candidate.id);
|
|
270
|
+
if (fromCustomRow !== undefined) return fromCustomRow;
|
|
192
271
|
const configuredModalities = provider ? modelRecordValue(provider.modelInputModalities, candidate.id) : undefined;
|
|
193
272
|
const fromConfiguredModalities = advertisesImageInput(configuredModalities);
|
|
194
273
|
if (fromConfiguredModalities !== undefined) return fromConfiguredModalities;
|
|
@@ -206,14 +285,14 @@ function modelAcceptsImageInputWithCache(
|
|
|
206
285
|
|
|
207
286
|
/** Eligible = not a sidecar consumer, and not positively known to be text-only. */
|
|
208
287
|
export function isVisionEligibleModel(
|
|
209
|
-
config:
|
|
288
|
+
config: VisionCapabilityConfig,
|
|
210
289
|
candidate: VisionCandidateModel,
|
|
211
290
|
): boolean {
|
|
212
291
|
return isVisionEligibleModelWithCache(config, candidate, new Map());
|
|
213
292
|
}
|
|
214
293
|
|
|
215
294
|
function isVisionEligibleModelWithCache(
|
|
216
|
-
config:
|
|
295
|
+
config: VisionCapabilityConfig,
|
|
217
296
|
candidate: VisionCandidateModel,
|
|
218
297
|
cache: EnrichedProviderCache,
|
|
219
298
|
): boolean {
|
|
@@ -274,7 +353,7 @@ function baselineCandidate(
|
|
|
274
353
|
* backend, and only `value` reaches the client, so first-wins costs nothing.
|
|
275
354
|
*/
|
|
276
355
|
export function visionEligibleModelOptions(
|
|
277
|
-
config:
|
|
356
|
+
config: VisionCapabilityConfig,
|
|
278
357
|
candidates: readonly VisionCandidateModel[],
|
|
279
358
|
enabledBackends: readonly VisionSidecarBackend[],
|
|
280
359
|
anthropicProviderName?: string,
|