@bitkyc08/opencodex 2.86.0 → 2.87.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/gui/dist/assets/App-ZfYbLHkB.js +58 -0
- package/gui/dist/assets/App-wT3mXqFz.css +1 -0
- package/gui/dist/assets/{Tray-DyR9yBee.js → Tray-DCd-A1az.js} +1 -1
- package/gui/dist/assets/index-CfYDMj34.js +94 -0
- package/gui/dist/assets/index-DPeB9HpV.css +1 -0
- package/gui/dist/assets/quota-summary-ChvmKGOk.js +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic/beta-allowlist.ts +9 -8
- package/src/adapters/anthropic/beta-body-pairs.ts +93 -0
- package/src/adapters/anthropic/passthrough.ts +7 -23
- package/src/adapters/anthropic-progress.ts +84 -0
- package/src/adapters/anthropic.ts +108 -21
- package/src/adapters/base.ts +2 -0
- package/src/adapters/claude-cli/adapter.ts +111 -23
- package/src/adapters/claude-cli/profiles.ts +2 -1
- package/src/adapters/claude-cli/stable-replay.ts +108 -0
- package/src/adapters/claude-cli/usage-admission.ts +230 -0
- package/src/adapters/coding-agent/protocol.ts +1 -1
- package/src/adapters/coding-agent/turn.ts +18 -2
- package/src/adapters/dsh-account.ts +97 -0
- package/src/adapters/google.ts +35 -8
- package/src/adapters/openai-chat/messages.ts +4 -3
- package/src/adapters/openai-chat/passthrough.ts +8 -2
- package/src/adapters/openai-chat/reasoning-replay.ts +52 -0
- package/src/adapters/openai-responses/compaction-search-history.ts +14 -7
- package/src/adapters/openai-responses/passthrough.ts +57 -125
- package/src/adapters/openai-responses/request-strips.ts +25 -3
- package/src/adapters/openai-responses/transport.ts +81 -0
- package/src/adapters/qoder/adapter.ts +4 -2
- package/src/adapters/registry.ts +5 -0
- package/src/adapters/zed.ts +26 -1
- package/src/advisor/activity.ts +55 -0
- package/src/advisor/budget.ts +62 -0
- package/src/advisor/consult.ts +4 -1
- package/src/advisor/context.ts +16 -4
- package/src/advisor/evidence-tool.ts +21 -0
- package/src/advisor/evidence.ts +117 -0
- package/src/advisor/runtime.ts +79 -31
- package/src/advisor/settings.ts +11 -2
- package/src/bridge/errors.ts +19 -1
- package/src/bridge/internal.ts +3 -1
- package/src/bridge/response-json.ts +18 -3
- package/src/bridge/sse.ts +58 -8
- package/src/bridge/terminal-refusal.ts +59 -0
- package/src/claude/desktop-3p-library.ts +32 -7
- package/src/claude/desktop-3p.ts +55 -13
- package/src/claude/desktop-first-party.ts +1 -1
- package/src/claude/desktop-picker-platform.ts +4 -0
- package/src/claude/desktop-picker.ts +15 -8
- package/src/claude/desktop-system-proxy.ts +50 -4
- package/src/claude/inbound-content-options.ts +11 -0
- package/src/claude/inbound.ts +33 -0
- package/src/claude/intercept/picker-ca-startup.ts +2 -1
- package/src/claude/intercept/picker-runtime.ts +6 -5
- package/src/claude/intercept/picker-trust-windows.ts +338 -0
- package/src/claude/intercept/picker-trust.ts +19 -0
- package/src/claude/intercept/port-policy.ts +30 -0
- package/src/claude/intercept/runtime.ts +97 -94
- package/src/claude/outbound.ts +38 -13
- package/src/cli/account-api.ts +1 -0
- package/src/cli/account-policy-dto.ts +6 -0
- package/src/cli/account-policy.ts +34 -5
- package/src/cli/account-preservation.ts +44 -0
- package/src/cli/account.ts +8 -2
- package/src/cli/advisor.ts +17 -3
- package/src/cli/capabilities-accounts.ts +20 -1
- package/src/cli/capabilities-agents-routing.ts +5 -2
- package/src/cli/capabilities-base.ts +38 -10
- package/src/cli/capabilities-observe-system.ts +63 -3
- package/src/cli/capabilities-provider-models.ts +11 -0
- package/src/cli/codex-cli-update.ts +151 -41
- package/src/cli/codex-provider.ts +44 -0
- package/src/cli/combo-input.ts +10 -1
- package/src/cli/combo-result.ts +1 -1
- package/src/cli/combo.ts +2 -1
- package/src/cli/cross-home-owner.ts +17 -14
- package/src/cli/dispatch.ts +25 -4
- package/src/cli/doctor.ts +6 -1
- package/src/cli/ensure-desktop-cold-start.ts +60 -7
- package/src/cli/help.ts +3 -2
- package/src/cli/index.ts +1 -1
- package/src/cli/integration-plan-dto.ts +1 -0
- package/src/cli/log-history.ts +172 -0
- package/src/cli/models-runtime-subcommands.ts +1 -0
- package/src/cli/models-runtime.ts +72 -8
- package/src/cli/observe-stream.ts +2 -0
- package/src/cli/observe.ts +16 -4
- package/src/cli/provider.ts +27 -5
- package/src/cli/registry.ts +28 -8
- package/src/cli/runtime-api.ts +5 -3
- package/src/cli/system-command.ts +40 -8
- package/src/cli/uninstall-client-state.ts +32 -0
- package/src/cli/usage-compare.ts +309 -0
- package/src/cli/usage-model-search.ts +4 -4
- package/src/cli/usage-report.ts +2 -2
- package/src/cli/v2.ts +1 -1
- package/src/client/machine-listener.ts +1 -3
- package/src/client/runtime.ts +1 -0
- package/src/clients/client-path.ts +51 -0
- package/src/clients/config-export/contracts.ts +2 -1
- package/src/clients/config-export.ts +18 -44
- package/src/clients/gentle-shell-path.ts +87 -0
- package/src/codex/account-auto-switch.ts +2 -0
- package/src/codex/account-identity.ts +29 -0
- package/src/codex/account-label.ts +1 -1
- package/src/codex/account-lifecycle.ts +21 -5
- package/src/codex/account-preservation.ts +28 -0
- package/src/codex/account-use-remaining.ts +23 -0
- package/src/codex/app-server-processes.ts +97 -7
- package/src/codex/app-server-restart-service.ts +55 -4
- package/src/codex/auth-api/account-list.ts +4 -0
- package/src/codex/auth-api/account-pause-group.ts +6 -50
- package/src/codex/auth-api/account-preservation.ts +57 -0
- package/src/codex/auth-api/routes.ts +8 -0
- package/src/codex/auth-api/use-remaining-route.ts +36 -0
- package/src/codex/auth-api.ts +0 -1
- package/src/codex/auth-context.ts +2 -2
- package/src/codex/catalog/auto-review.ts +20 -3
- package/src/codex/catalog/build-entries.ts +9 -1
- package/src/codex/catalog/combo-member.ts +14 -5
- package/src/codex/catalog/configured-tiers.ts +1 -1
- package/src/codex/catalog/gather-capture.ts +2 -0
- package/src/codex/catalog/metadata.ts +11 -9
- package/src/codex/catalog/model-hints.ts +21 -16
- package/src/codex/catalog/model-visibility.ts +3 -1
- package/src/codex/catalog/parsing.ts +30 -86
- package/src/codex/catalog/provider-models.ts +36 -34
- package/src/codex/catalog/routed-gather.ts +12 -13
- package/src/codex/cli-install-provenance.ts +105 -17
- package/src/codex/cli-update-plan.ts +545 -0
- package/src/codex/config-write-lock.ts +23 -2
- package/src/codex/desktop-app/picker-freshness.ts +96 -0
- package/src/codex/desktop-app-restart.ts +21 -0
- package/src/codex/desktop-compatibility/installed-build.ts +24 -6
- package/src/codex/desktop-compatibility/routing-preflight.ts +1 -1
- package/src/codex/desktop-compatibility/runtime.ts +12 -3
- package/src/codex/desktop-compatibility/windows-key-protection.ts +115 -9
- package/src/codex/features.ts +85 -45
- package/src/codex/inject/history-failure.ts +27 -0
- package/src/codex/inject/restore.ts +24 -2
- package/src/codex/inject/routing-classify.ts +4 -3
- package/src/codex/inject.ts +18 -27
- package/src/codex/main-account-hard-lock.ts +30 -1
- package/src/codex/native-profile-startup.ts +1 -1
- package/src/codex/ocx-compaction-history.ts +12 -10
- package/src/codex/paths.ts +86 -1
- package/src/codex/provider-routing.ts +174 -0
- package/src/codex/refresh.ts +2 -2
- package/src/codex/routing/health-store.ts +1 -1
- package/src/codex/routing/selection.ts +4 -3
- package/src/codex/routing/thread-affinity.ts +1 -1
- package/src/codex/signed-in-identity.ts +43 -0
- package/src/codex/subagent-model-fallback.ts +1 -1
- package/src/codex/sync.ts +6 -1
- package/src/codex/user-identity.ts +16 -5
- package/src/codex/windows-local-appdata.ts +84 -0
- package/src/combos/failover.ts +6 -1
- package/src/combos/index.ts +1 -0
- package/src/combos/resolve.ts +46 -10
- package/src/combos/types.ts +15 -0
- package/src/config/diagnostics.ts +33 -6
- package/src/config/live-reconcile.ts +38 -2
- package/src/config/load-degrade.ts +17 -0
- package/src/config/opencode-wire-upgrade.ts +54 -0
- package/src/config/opencode-zen-upgrade.ts +52 -0
- package/src/config/owner-registry.ts +22 -17
- package/src/config/provider-validation.ts +45 -2
- package/src/config/schema/config-schema.ts +22 -0
- package/src/config/schema/leaf-validators.ts +38 -9
- package/src/config/schema/main-account-hard-lock-fallback.ts +26 -0
- package/src/config/schema/native-reasoning-retention.ts +26 -0
- package/src/config.ts +2 -0
- package/src/generated/compatibility-version.json +608 -284
- package/src/grok/sync.ts +1 -1
- package/src/images/loop.ts +46 -8
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/mutation-plan.ts +1 -0
- package/src/integrations/registry.ts +8 -0
- package/src/lib/codex-restart-contract.ts +15 -0
- package/src/lib/desktop-upstream-tunnel.ts +18 -53
- package/src/lib/empty-tool-catalog.ts +62 -0
- package/src/lib/errors.ts +19 -3
- package/src/lib/gcp-adc.ts +96 -9
- package/src/lib/gcp-credential-input.ts +12 -0
- package/src/lib/lab-live-route-production.ts +5 -0
- package/src/lib/opaque-proxy-tunnel.ts +217 -0
- package/src/lib/redact.ts +2 -2
- package/src/oauth/account-quota-rank.ts +69 -2
- package/src/oauth/anthropic-account-refusal.ts +20 -2
- package/src/oauth/anthropic-oauth-definitions.ts +3 -1
- package/src/oauth/anthropic-routing.ts +1 -21
- package/src/oauth/anthropic.ts +38 -7
- package/src/oauth/callback-server.ts +27 -1
- package/src/oauth/chatgpt-device.ts +1 -1
- package/src/oauth/chatgpt-shared.ts +83 -0
- package/src/oauth/chatgpt.ts +8 -72
- package/src/oauth/dsh.ts +203 -0
- package/src/oauth/generic-account-failover.ts +33 -7
- package/src/oauth/generic-session-affinity.ts +125 -0
- package/src/oauth/index.ts +184 -109
- package/src/oauth/local-token-detect.ts +21 -1
- package/src/oauth/login-cli-errors.ts +44 -0
- package/src/oauth/login-cli.ts +69 -7
- package/src/oauth/login-flow-state.ts +371 -23
- package/src/oauth/meta-muse-errors.ts +41 -0
- package/src/oauth/meta-muse-keychain.ts +52 -0
- package/src/oauth/meta-muse.ts +51 -84
- package/src/oauth/model-discovery-auth.ts +3 -0
- package/src/oauth/pool-settings-capability.ts +4 -2
- package/src/oauth/refresh-flight-retirement.ts +48 -0
- package/src/oauth/session-affinity-key.ts +23 -0
- package/src/oauth/store-anthropic-instance.ts +9 -2
- package/src/oauth/store.ts +12 -5
- package/src/protocols/encoders/adapter-events.ts +5 -4
- package/src/protocols/encoders/messages.ts +9 -6
- package/src/protocols/provider-summary.ts +2 -1
- package/src/providers/api-key-cooldowns.ts +49 -0
- package/src/providers/api-key-resolve.ts +11 -6
- package/src/providers/api-key-response-ownership.ts +57 -0
- package/src/providers/api-key-selection.ts +31 -6
- package/src/providers/api-keys.ts +214 -3
- package/src/providers/claude-cli-usage.ts +96 -0
- package/src/providers/dsh-account-destination.ts +31 -0
- package/src/providers/dsh-account-models.ts +41 -0
- package/src/providers/gcp-credential-cleanup.ts +59 -0
- package/src/providers/key-failover.ts +54 -50
- package/src/providers/key-store.ts +7 -2
- package/src/providers/model-rename-fields.ts +3 -0
- package/src/providers/model-rename-migration.ts +2 -2
- package/src/providers/openai-tiers-destination.ts +11 -2
- package/src/providers/openai-tiers.ts +5 -12
- package/src/providers/opencode-go-transport.ts +3 -1
- package/src/providers/quota/account-cache.ts +6 -5
- package/src/providers/quota/account-row.ts +34 -0
- package/src/providers/quota/antigravity.ts +28 -7
- package/src/providers/quota/dsh-account.ts +70 -0
- package/src/providers/quota/vendor-probes-oauth.ts +1 -93
- package/src/providers/quota-types.ts +2 -0
- package/src/providers/quota.ts +14 -20
- package/src/providers/registry/entries-core.ts +37 -8
- package/src/providers/registry/types.ts +1 -1
- package/src/providers/request-pacing.ts +77 -19
- package/src/providers/routed-config.ts +339 -0
- package/src/providers/service-tier.ts +2 -1
- package/src/quota/window-mapping.ts +5 -2
- package/src/responses/anthropic-progress.ts +34 -0
- package/src/responses/assistant-replay.ts +35 -0
- package/src/responses/compaction-history-framing.ts +65 -0
- package/src/responses/compaction.ts +6 -1
- package/src/responses/incomplete-reason.ts +31 -0
- package/src/responses/parser.ts +50 -5
- package/src/responses/reasoning-envelope.ts +4 -0
- package/src/responses/reasoning-replay-cache.ts +24 -2
- package/src/responses/reasoning-retention.ts +173 -0
- package/src/responses/retained-compaction.ts +46 -0
- package/src/responses/schema.ts +23 -0
- package/src/router.ts +8 -321
- package/src/routing/health.ts +37 -22
- package/src/routing/history/indexer.ts +6 -5
- package/src/routing/history/ingest.ts +6 -2
- package/src/routing/profile.ts +1 -1
- package/src/server/admission-model-scope.ts +17 -0
- package/src/server/auth-cors.ts +11 -0
- package/src/server/chat-completions.ts +7 -1
- package/src/server/chat-native-reasoning.ts +123 -0
- package/src/server/chat-native-sse.ts +110 -7
- package/src/server/chat-native.ts +76 -17
- package/src/server/claude-messages.ts +55 -414
- package/src/server/effort-policy.ts +1 -1
- package/src/server/images.ts +15 -2
- package/src/server/index/model-alias-id.ts +36 -0
- package/src/server/index/optional-listeners.ts +2 -0
- package/src/server/index/serve-options.ts +9 -1
- package/src/server/index/startup-warnings.ts +2 -0
- package/src/server/index.ts +1 -1
- package/src/server/inference/advisor-diagnostics.ts +31 -0
- package/src/server/management/advisor-routes.ts +15 -4
- package/src/server/management/claude-desktop-picker-routes.ts +1 -1
- package/src/server/management/combo-routes.ts +176 -50
- package/src/server/management/companion-routes.ts +1 -1
- package/src/server/management/config-routes.ts +75 -4
- package/src/server/management/cursor-integration-routes.ts +1 -1
- package/src/server/management/model-routes.ts +11 -6
- package/src/server/management/model-rows.ts +14 -0
- package/src/server/management/native-integration-routes.ts +2 -7
- package/src/server/management/native-reasoning-retention-routes.ts +41 -0
- package/src/server/management/oauth-account-routes.ts +87 -14
- package/src/server/management/provider-gcp-credential-routes.ts +29 -0
- package/src/server/management/provider-key-pacing-routes.ts +31 -0
- package/src/server/management/provider-overwrite-carry.ts +7 -0
- package/src/server/management/provider-routes.ts +25 -30
- package/src/server/management/route-registry.ts +10 -1
- package/src/server/management-api.ts +3 -1
- package/src/server/messages-native-tool-guard.ts +185 -0
- package/src/server/messages-native.ts +101 -21
- package/src/server/messages-shared.ts +431 -0
- package/src/server/relay.ts +1 -1
- package/src/server/request-log-terminal-status.ts +1 -1
- package/src/server/request-log.ts +6 -1
- package/src/server/responses/account-change-state.ts +8 -2
- package/src/server/responses/adapter-continuation.ts +13 -1
- package/src/server/responses/adapter-delivery.ts +19 -6
- package/src/server/responses/adapter-dispatch.ts +19 -2
- package/src/server/responses/advisor-retention.ts +117 -0
- package/src/server/responses/advisor-slot.ts +73 -41
- package/src/server/responses/codex-ws-wire.ts +15 -8
- package/src/server/responses/compact.ts +303 -125
- package/src/server/responses/compaction-recovery.ts +18 -10
- package/src/server/responses/compaction-retention.ts +61 -0
- package/src/server/responses/context-overflow.ts +11 -6
- package/src/server/responses/core-auth.ts +6 -0
- package/src/server/responses/core-codex-account.ts +11 -1
- package/src/server/responses/core-combo-failure.ts +2 -0
- package/src/server/responses/core-combo.ts +120 -36
- package/src/server/responses/core-errors.ts +3 -3
- package/src/server/responses/core-lifetime.ts +5 -2
- package/src/server/responses/core-opaque-recovery.ts +35 -0
- package/src/server/responses/core-options.ts +5 -0
- package/src/server/responses/core-pipeline.ts +141 -0
- package/src/server/responses/core-replay.ts +18 -2
- package/src/server/responses/core.ts +2 -115
- package/src/server/responses/fetch-helpers.ts +73 -19
- package/src/server/responses/main-hard-lock-routing.ts +109 -0
- package/src/server/responses/native-response-control.ts +4 -2
- package/src/server/responses/passthrough-delivery.ts +3 -1
- package/src/server/responses/passthrough-dispatch.ts +15 -13
- package/src/server/responses/request-prepare.ts +76 -12
- package/src/server/responses/request-sidecar-auth.ts +2 -2
- package/src/server/responses/request-transport.ts +104 -27
- package/src/server/responses/response-effects.ts +1 -0
- package/src/server/responses/run-turn-execution.ts +96 -30
- package/src/server/responses/sidecar-execution.ts +20 -2
- package/src/server/responses/terminal-guard.ts +2 -16
- package/src/service/auth-preflight.ts +71 -0
- package/src/service/cli.ts +4 -2
- package/src/service/definition.ts +97 -0
- package/src/service/guards.ts +1 -61
- package/src/service/health-sentinel-linux.ts +112 -0
- package/src/service/health-sentinel-main.ts +76 -57
- package/src/service/health.ts +5 -89
- package/src/service/launchd.ts +27 -7
- package/src/service/logging.ts +72 -9
- package/src/service/repair.ts +2 -1
- package/src/service/state.ts +7 -8
- package/src/service/systemd.ts +1 -1
- package/src/service/windows-guarded-manager.ts +2 -1
- package/src/service/windows-ops.ts +1 -1
- package/src/service/windows-scheduler.ts +2 -7
- package/src/service/windows-taskxml.ts +2 -2
- package/src/service/windows-wscript.ts +14 -0
- package/src/service.ts +4 -2
- package/src/sidecar/auth.ts +2 -0
- package/src/types/config.ts +64 -3
- package/src/types/opencode-go.ts +61 -0
- package/src/types/opencode-zen.ts +29 -0
- package/src/types/provider.ts +32 -6
- package/src/types/request.ts +23 -2
- package/src/types/wire.ts +29 -37
- package/src/types.ts +1 -0
- package/src/update/npm-invocation.mjs +12 -2
- package/src/usage/cost.ts +48 -12
- package/src/usage/expected-prices.ts +3 -0
- package/src/usage/ledger-retention.ts +2 -1
- package/src/usage/ledger-scanner.ts +6 -5
- package/src/usage/log.ts +17 -8
- package/src/usage/summary.ts +22 -9
- package/src/usage/user-cost-overlays.ts +14 -19
- package/src/usage/user-cost-validation.ts +38 -0
- package/src/vision/eligibility.ts +13 -0
- package/src/web-search/loop.ts +55 -5
- package/src/web-search/progress-stream.ts +4 -1
- package/src/web-search/run-turn-loop.ts +9 -1
- package/gui/dist/assets/App-BjlttVLy.js +0 -58
- package/gui/dist/assets/App-CSJvmjNs.css +0 -1
- package/gui/dist/assets/index-BB6uK6tU.css +0 -1
- package/gui/dist/assets/index-BMssP4k3.js +0 -94
- package/gui/dist/assets/quota-summary-D8M1bc_R.js +0 -1
package/src/usage/cost.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
resolveMetadataProvider,
|
|
16
16
|
} from "../generated/model-metadata";
|
|
17
17
|
import type { AttemptTierOutcome, OcxUsage } from "../types";
|
|
18
|
+
import type { PromptLengthPricing } from "../types/provider";
|
|
18
19
|
import { canonicalFastTierMarker } from "../providers/fastwire";
|
|
19
20
|
import { baseProviderLabel } from "../providers/label";
|
|
20
21
|
import type { PersistedUsageAttempt, UsageStatus } from "./log";
|
|
@@ -79,6 +80,7 @@ export interface MatchedPrice {
|
|
|
79
80
|
modelId: string;
|
|
80
81
|
jawcodeProvider?: string;
|
|
81
82
|
cost4: Cost4;
|
|
83
|
+
promptLengthPricing?: PromptLengthPricing;
|
|
82
84
|
source: "jawcode" | "expected" | "user";
|
|
83
85
|
sourceRef?: string;
|
|
84
86
|
verifiedAt?: string;
|
|
@@ -97,6 +99,8 @@ export interface AttemptCostEstimate {
|
|
|
97
99
|
priorityMultiplier?: number;
|
|
98
100
|
/** Set when the published long-context rate was applied (#908). */
|
|
99
101
|
contextTier?: ContextTierName;
|
|
102
|
+
/** User absolute rates selected for this attempt, separate from catalog tiers. */
|
|
103
|
+
customThresholdApplied?: true;
|
|
100
104
|
/** The numeric estimate is a known floor because the exact Priority price is unavailable. */
|
|
101
105
|
priorityLowerBound?: boolean;
|
|
102
106
|
}
|
|
@@ -111,6 +115,8 @@ export interface CostEstimate {
|
|
|
111
115
|
priorityMultiplier?: number;
|
|
112
116
|
/** Set when any priced attempt used the published long-context rate (#908). */
|
|
113
117
|
contextTier?: ContextTierName;
|
|
118
|
+
/** At least one request/attempt selected the user threshold rates. */
|
|
119
|
+
customThresholdApplied?: true;
|
|
114
120
|
/** The aggregate is a known floor because every priced attempt is a lower bound. */
|
|
115
121
|
priorityLowerBound?: boolean;
|
|
116
122
|
}
|
|
@@ -343,6 +349,7 @@ function userOverlayMatch(
|
|
|
343
349
|
modelId,
|
|
344
350
|
cost4: overlay.cost4,
|
|
345
351
|
source: "user",
|
|
352
|
+
...(overlay.promptLengthPricing ? { promptLengthPricing: overlay.promptLengthPricing } : {}),
|
|
346
353
|
sourceRef: overlay.source,
|
|
347
354
|
verifiedAt: overlay.verifiedAt,
|
|
348
355
|
status: "verified",
|
|
@@ -397,6 +404,15 @@ function vendorPrefixedCost(modelId: string): ReturnType<typeof findVendorCostBy
|
|
|
397
404
|
return normalize(found.provider) === normalize(claimedVendor) ? found : undefined;
|
|
398
405
|
}
|
|
399
406
|
|
|
407
|
+
/**
|
|
408
|
+
* Only measured usage is priced. An `unreported` or `unsupported` row can still carry tokens — a
|
|
409
|
+
* combo whose first leg failed without usage keeps the second leg's — and pricing them reports an
|
|
410
|
+
* understated cost as if it were complete.
|
|
411
|
+
*/
|
|
412
|
+
export function isMeasuredStatus(usageStatus: UsageStatus): boolean {
|
|
413
|
+
return usageStatus === "reported" || usageStatus === "estimated";
|
|
414
|
+
}
|
|
415
|
+
|
|
400
416
|
function isEstimated(usage: OcxUsage, usageStatus: UsageStatus, priceStatus: ExpectedPriceStatus | "verified"): boolean {
|
|
401
417
|
return usage.estimated === true || usageStatus === "estimated" || priceStatus === "verified-derived";
|
|
402
418
|
}
|
|
@@ -475,8 +491,8 @@ function isConfirmedFast(tier?: ServiceTierInput): boolean {
|
|
|
475
491
|
}
|
|
476
492
|
|
|
477
493
|
/**
|
|
478
|
-
*
|
|
479
|
-
*
|
|
494
|
+
* Select user prompt-length rates, or apply the published context rule in automatic mode.
|
|
495
|
+
* Returns [effectiveCost4, catalogTier, priorityLowerBound, customThresholdApplied].
|
|
480
496
|
*
|
|
481
497
|
* `rawInputTokens` MUST be `usage.inputTokens` (total prompt size), never the
|
|
482
498
|
* normalized billable input — normalization subtracts cache read/write, so a
|
|
@@ -486,13 +502,27 @@ function isConfirmedFast(tier?: ServiceTierInput): boolean {
|
|
|
486
502
|
* API rows can stack the bands. xAI publishes neither a combined rate nor an exclusion,
|
|
487
503
|
* so its long-context rate remains the known lower bound instead of inventing a stacked multiplier.
|
|
488
504
|
*/
|
|
489
|
-
function
|
|
505
|
+
function applyPromptLengthPricing(
|
|
490
506
|
cost4: Cost4,
|
|
491
507
|
provider: string,
|
|
492
508
|
modelId: string,
|
|
493
509
|
rawInputTokens: number | undefined,
|
|
494
510
|
tier?: ServiceTierInput,
|
|
495
|
-
|
|
511
|
+
policy?: PromptLengthPricing,
|
|
512
|
+
): [Cost4, ContextTierName | undefined, boolean, true?] {
|
|
513
|
+
// A user policy replaces the catalog rule on BOTH sides of its threshold.
|
|
514
|
+
// Keep custom provenance separate while honoring the published Priority relation.
|
|
515
|
+
if (policy?.mode === "flat") return [cost4, undefined, false];
|
|
516
|
+
if (policy?.mode === "custom") {
|
|
517
|
+
const crossed = rawInputTokens !== undefined && Number.isFinite(rawInputTokens)
|
|
518
|
+
&& (policy.comparison === "gte"
|
|
519
|
+
? rawInputTokens >= policy.thresholdTokens : rawInputTokens > policy.thresholdTokens);
|
|
520
|
+
if (!crossed) return [cost4, undefined, false];
|
|
521
|
+
const relation = findContextTier(provider, modelId)?.confirmedPriorityRelation;
|
|
522
|
+
const confirmedFast = isConfirmedFast(tier);
|
|
523
|
+
if (confirmedFast && relation === "exclusive") return [cost4, undefined, false];
|
|
524
|
+
return [policy.rates, undefined, confirmedFast && relation === "lower-bound", true];
|
|
525
|
+
}
|
|
496
526
|
if (rawInputTokens === undefined) return [cost4, undefined, false];
|
|
497
527
|
const rule = findContextTier(provider, modelId);
|
|
498
528
|
if (!rule || !isLongContext(rule, rawInputTokens)) return [cost4, undefined, false];
|
|
@@ -520,9 +550,12 @@ function applyPriorityMultiplier(
|
|
|
520
550
|
modelId: string,
|
|
521
551
|
serviceTier?: ServiceTierInput,
|
|
522
552
|
contextTier?: ContextTierName,
|
|
553
|
+
customThresholdApplied?: true,
|
|
523
554
|
): [Cost4, number] {
|
|
524
555
|
if (canonicalFastTierMarker(tierScalar(serviceTier)) !== "priority") return [cost4, 1];
|
|
525
556
|
if (contextTier && findContextTier(provider, modelId)?.confirmedPriorityRelation !== "stack") return [cost4, 1];
|
|
557
|
+
if (customThresholdApplied && isConfirmedFast(serviceTier)
|
|
558
|
+
&& findContextTier(provider, modelId)?.confirmedPriorityRelation === "lower-bound") return [cost4, 1];
|
|
526
559
|
const rule = findPriorityPricingRule(provider, modelId);
|
|
527
560
|
if (rule?.requiresResponseConfirmation && !isConfirmedFast(serviceTier)) return [cost4, 1];
|
|
528
561
|
const multiplier = rule?.multiplier ?? 1;
|
|
@@ -568,7 +601,7 @@ export function estimateAttemptCost(
|
|
|
568
601
|
serviceTier?: ServiceTierInput,
|
|
569
602
|
userOverlays: readonly ExpectedPriceOverlay[] = activeUserCostOverlays(),
|
|
570
603
|
): AttemptCostEstimate | null {
|
|
571
|
-
if (!attempt.usage) return null;
|
|
604
|
+
if (!isMeasuredStatus(attempt.usageStatus) || !attempt.usage) return null;
|
|
572
605
|
const tokens = normalizeCostTokens(attempt.usage);
|
|
573
606
|
if (!tokens) return null;
|
|
574
607
|
const price = resolveMatchedPrice(attempt.provider, attempt.model, overlays, userOverlays, attempt);
|
|
@@ -576,11 +609,11 @@ export function estimateAttemptCost(
|
|
|
576
609
|
const attemptServiceTier = attempt.tierOutcome
|
|
577
610
|
? serviceTierContextFromOutcome(attempt.tierOutcome)
|
|
578
611
|
: serviceTier;
|
|
579
|
-
const [tieredCost4, contextTier, contextPriorityLowerBound] =
|
|
580
|
-
price.cost4, price.provider, attempt.model, attempt.usage.inputTokens, attemptServiceTier,
|
|
612
|
+
const [tieredCost4, contextTier, contextPriorityLowerBound, customThresholdApplied] = applyPromptLengthPricing(
|
|
613
|
+
price.cost4, price.provider, attempt.model, attempt.usage.inputTokens, attemptServiceTier, price.promptLengthPricing,
|
|
581
614
|
);
|
|
582
615
|
const [effectiveCost4, multiplier] = applyPriorityMultiplier(
|
|
583
|
-
tieredCost4, price.provider, attempt.model, attemptServiceTier, contextTier,
|
|
616
|
+
tieredCost4, price.provider, attempt.model, attemptServiceTier, contextTier, customThresholdApplied,
|
|
584
617
|
);
|
|
585
618
|
const priorityLowerBound = contextPriorityLowerBound
|
|
586
619
|
|| isOpenRouterPriorityLowerBound(price.provider, attempt.tierOutcome);
|
|
@@ -594,6 +627,7 @@ export function estimateAttemptCost(
|
|
|
594
627
|
estimated: isEstimated(attempt.usage, attempt.usageStatus, price.status),
|
|
595
628
|
...(multiplier !== 1 ? { priorityMultiplier: multiplier } : {}),
|
|
596
629
|
...(contextTier ? { contextTier } : {}),
|
|
630
|
+
...(customThresholdApplied ? { customThresholdApplied } : {}),
|
|
597
631
|
...(priorityLowerBound ? { priorityLowerBound: true } : {}),
|
|
598
632
|
};
|
|
599
633
|
}
|
|
@@ -637,6 +671,7 @@ export function estimateComboCost(
|
|
|
637
671
|
? { priorityMultiplier: estimates.find(est => est.priorityMultiplier)?.priorityMultiplier }
|
|
638
672
|
: {}),
|
|
639
673
|
...(estimates.some(est => est.contextTier) ? { contextTier: "long" as const } : {}),
|
|
674
|
+
...(estimates.some(est => est.customThresholdApplied) ? { customThresholdApplied: true as const } : {}),
|
|
640
675
|
...(estimates.every(est => est.priorityLowerBound === true)
|
|
641
676
|
? { priorityLowerBound: true as const }
|
|
642
677
|
: {}),
|
|
@@ -656,16 +691,16 @@ export function estimateRequestCost(
|
|
|
656
691
|
overlays: readonly ExpectedPriceOverlay[] = EXPECTED_PRICE_OVERLAYS,
|
|
657
692
|
userOverlays: readonly ExpectedPriceOverlay[] = activeUserCostOverlays(),
|
|
658
693
|
): CostEstimate | null {
|
|
659
|
-
if (!input.usage) return null;
|
|
694
|
+
if (!isMeasuredStatus(input.usageStatus) || !input.usage) return null;
|
|
660
695
|
const tokens = normalizeCostTokens(input.usage);
|
|
661
696
|
if (!tokens) return null;
|
|
662
697
|
const price = resolveMatchedPrice(input.provider, input.model, overlays, userOverlays, input);
|
|
663
698
|
if (!price) return null;
|
|
664
|
-
const [tieredCost4, contextTier, contextPriorityLowerBound] =
|
|
665
|
-
price.cost4, price.provider, input.model, input.usage.inputTokens, input.serviceTier,
|
|
699
|
+
const [tieredCost4, contextTier, contextPriorityLowerBound, customThresholdApplied] = applyPromptLengthPricing(
|
|
700
|
+
price.cost4, price.provider, input.model, input.usage.inputTokens, input.serviceTier, price.promptLengthPricing,
|
|
666
701
|
);
|
|
667
702
|
const [effectiveCost4, multiplier] = applyPriorityMultiplier(
|
|
668
|
-
tieredCost4, price.provider, input.model, input.serviceTier, contextTier,
|
|
703
|
+
tieredCost4, price.provider, input.model, input.serviceTier, contextTier, customThresholdApplied,
|
|
669
704
|
);
|
|
670
705
|
const priorityLowerBound = contextPriorityLowerBound || isOpenRouterPriorityLowerBound(
|
|
671
706
|
price.provider,
|
|
@@ -678,6 +713,7 @@ export function estimateRequestCost(
|
|
|
678
713
|
estimated: isEstimated(input.usage, input.usageStatus, price.status),
|
|
679
714
|
...(multiplier !== 1 ? { priorityMultiplier: multiplier } : {}),
|
|
680
715
|
...(contextTier ? { contextTier } : {}),
|
|
716
|
+
...(customThresholdApplied ? { customThresholdApplied } : {}),
|
|
681
717
|
...(priorityLowerBound ? { priorityLowerBound: true } : {}),
|
|
682
718
|
};
|
|
683
719
|
}
|
|
@@ -10,6 +10,7 @@ import { isAnthropicInstanceId } from "../providers/anthropic-instance-id";
|
|
|
10
10
|
export function pricingFamilyProvider(provider: string): string {
|
|
11
11
|
return isAnthropicInstanceId(provider) ? "anthropic" : provider;
|
|
12
12
|
}
|
|
13
|
+
import type { PromptLengthPricing } from "../types/provider";
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Expected-price overlay for models whose jawcode cost rows are missing or all-zero
|
|
@@ -46,6 +47,8 @@ export interface ExpectedPriceOverlay {
|
|
|
46
47
|
provider: string;
|
|
47
48
|
modelId: string;
|
|
48
49
|
cost4: Cost4;
|
|
50
|
+
/** User-owned context policy; catalog rows leave this absent. */
|
|
51
|
+
promptLengthPricing?: PromptLengthPricing;
|
|
49
52
|
source: string;
|
|
50
53
|
verifiedAt: string;
|
|
51
54
|
status: ExpectedPriceStatus;
|
|
@@ -22,6 +22,7 @@ import { closeSync, fstatSync, openSync, readSync, writeSync } from "node:fs";
|
|
|
22
22
|
import { atomicWriteFileStreamed } from "../config/atomic-write";
|
|
23
23
|
import {
|
|
24
24
|
currentUsageLogRevision,
|
|
25
|
+
exactFileIdentity,
|
|
25
26
|
usageLogIdentityKey,
|
|
26
27
|
usageLogPath,
|
|
27
28
|
usageLogRevisionKey,
|
|
@@ -127,7 +128,7 @@ export function enforceUsageLedgerSizeLimit(
|
|
|
127
128
|
let sourceClosed = false;
|
|
128
129
|
try {
|
|
129
130
|
const opened = fstatSync(sourceFd);
|
|
130
|
-
if (Number(opened.size) !== captured.size ||
|
|
131
|
+
if (Number(opened.size) !== captured.size || exactFileIdentity(sourceFd).ino !== captured.ino) {
|
|
131
132
|
return { kind: "deferred", currentBytes: captured.size, reason: "revision-changed" };
|
|
132
133
|
}
|
|
133
134
|
const start = firstRowBoundary(sourceFd, Math.max(0, captured.size - target), captured.size);
|
|
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { closeSync, fstatSync, openSync, readSync } from "node:fs";
|
|
3
3
|
import {
|
|
4
4
|
currentUsageLogRevision,
|
|
5
|
+
exactFileIdentity,
|
|
5
6
|
normalizePersistedUsageRow,
|
|
6
7
|
usageLogIdentityKey,
|
|
7
8
|
usageLogPath,
|
|
@@ -63,13 +64,13 @@ export class UsageLedgerRebuildRequiredError extends Error {
|
|
|
63
64
|
|
|
64
65
|
function revisionFromStat(
|
|
65
66
|
path: string,
|
|
67
|
+
fd: number,
|
|
66
68
|
stat: ReturnType<typeof fstatSync>,
|
|
67
69
|
): UsageLogRevision {
|
|
68
70
|
if (!stat.isFile()) throw new Error("usage log is not a regular file");
|
|
69
71
|
return {
|
|
70
72
|
path,
|
|
71
|
-
|
|
72
|
-
ino: Number(stat.ino),
|
|
73
|
+
...exactFileIdentity(fd),
|
|
73
74
|
birthtimeMs: Number(stat.birthtimeMs),
|
|
74
75
|
size: Number(stat.size),
|
|
75
76
|
mtimeMs: Number(stat.mtimeMs),
|
|
@@ -276,7 +277,7 @@ export async function scanUsageLedgerCooperatively(
|
|
|
276
277
|
let pendingLineBytes = 0;
|
|
277
278
|
|
|
278
279
|
try {
|
|
279
|
-
const openedRevision = revisionFromStat(path, fstatSync(fd));
|
|
280
|
+
const openedRevision = revisionFromStat(path, fd, fstatSync(fd));
|
|
280
281
|
const scanEnd = openedRevision.size;
|
|
281
282
|
const openedIdentityKey = usageLogIdentityKey(openedRevision);
|
|
282
283
|
if (options.expectedIdentityKey && options.expectedIdentityKey !== openedIdentityKey) {
|
|
@@ -391,7 +392,7 @@ export async function scanUsageLedgerCooperatively(
|
|
|
391
392
|
}
|
|
392
393
|
|
|
393
394
|
throwIfAborted(options.signal);
|
|
394
|
-
const endingRevision = revisionFromStat(path, fstatSync(fd));
|
|
395
|
+
const endingRevision = revisionFromStat(path, fd, fstatSync(fd));
|
|
395
396
|
if (usageLogIdentityKey(endingRevision) !== openedIdentityKey) {
|
|
396
397
|
throw rebuildRequired("identity_mismatch");
|
|
397
398
|
}
|
|
@@ -417,7 +418,7 @@ export async function scanUsageLedgerCooperatively(
|
|
|
417
418
|
line,
|
|
418
419
|
options.signal,
|
|
419
420
|
);
|
|
420
|
-
const verifiedFdRevision = revisionFromStat(path, fstatSync(fd));
|
|
421
|
+
const verifiedFdRevision = revisionFromStat(path, fd, fstatSync(fd));
|
|
421
422
|
const verifiedPathRevision = currentUsageLogRevision();
|
|
422
423
|
if (capturedDigest !== verifiedDigest) throw rebuildRequired("content_changed");
|
|
423
424
|
if (!verifiedPathRevision
|
package/src/usage/log.ts
CHANGED
|
@@ -1163,8 +1163,12 @@ export function appendUsageEntry(entry: PersistedUsageEntry): void {
|
|
|
1163
1163
|
|
|
1164
1164
|
export type UsageLogRevision = {
|
|
1165
1165
|
path: string;
|
|
1166
|
-
|
|
1167
|
-
|
|
1166
|
+
/**
|
|
1167
|
+
* Exact decimal device and file id. NTFS file ids carry a sequence number in their
|
|
1168
|
+
* high bits and routinely exceed 2^53, so a Number would let two files compare equal.
|
|
1169
|
+
*/
|
|
1170
|
+
dev: string;
|
|
1171
|
+
ino: string;
|
|
1168
1172
|
birthtimeMs: number;
|
|
1169
1173
|
size: number;
|
|
1170
1174
|
mtimeMs: number;
|
|
@@ -1367,12 +1371,17 @@ function readExactly(fd: number, length: number, position: number): Buffer | nul
|
|
|
1367
1371
|
return output;
|
|
1368
1372
|
}
|
|
1369
1373
|
|
|
1370
|
-
|
|
1374
|
+
/** Exact dev/ino of an open file, read through bigint stats so large ids keep every bit. */
|
|
1375
|
+
export function exactFileIdentity(fd: number): { dev: string; ino: string } {
|
|
1376
|
+
const exact = fstatSync(fd, { bigint: true });
|
|
1377
|
+
return { dev: exact.dev.toString(), ino: exact.ino.toString() };
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
function usageLogRevision(path: string, fd: number, stat: ReturnType<typeof fstatSync>): UsageLogRevision {
|
|
1371
1381
|
if (!stat.isFile()) throw new Error("usage log is not a regular file");
|
|
1372
1382
|
return {
|
|
1373
1383
|
path,
|
|
1374
|
-
|
|
1375
|
-
ino: Number(stat.ino),
|
|
1384
|
+
...exactFileIdentity(fd),
|
|
1376
1385
|
birthtimeMs: Number(stat.birthtimeMs),
|
|
1377
1386
|
size: Number(stat.size),
|
|
1378
1387
|
mtimeMs: Number(stat.mtimeMs),
|
|
@@ -1405,7 +1414,7 @@ export function currentUsageLogRevision(): UsageLogRevision | null {
|
|
|
1405
1414
|
let fd: number | undefined;
|
|
1406
1415
|
try {
|
|
1407
1416
|
fd = openSync(path, "r");
|
|
1408
|
-
return usageLogRevision(path, fstatSync(fd));
|
|
1417
|
+
return usageLogRevision(path, fd, fstatSync(fd));
|
|
1409
1418
|
} finally {
|
|
1410
1419
|
if (fd !== undefined) closeSync(fd);
|
|
1411
1420
|
}
|
|
@@ -1530,7 +1539,7 @@ async function readUsageEntriesFullCooperatively(
|
|
|
1530
1539
|
if (prefixDigest === null) throw new Error("usage log changed while it was being read");
|
|
1531
1540
|
return {
|
|
1532
1541
|
entries: parsed.entries,
|
|
1533
|
-
revision: usageLogRevision(path, stat),
|
|
1542
|
+
revision: usageLogRevision(path, fd, stat),
|
|
1534
1543
|
truncatedPrefixBytes,
|
|
1535
1544
|
entriesTruncated: parsed.entriesDropped > 0,
|
|
1536
1545
|
entriesDropped: parsed.entriesDropped,
|
|
@@ -1562,7 +1571,7 @@ async function readUsageEntriesIncrementally(
|
|
|
1562
1571
|
try {
|
|
1563
1572
|
fd = openSync(path, "r");
|
|
1564
1573
|
const stat = fstatSync(fd);
|
|
1565
|
-
const revision = usageLogRevision(path, stat);
|
|
1574
|
+
const revision = usageLogRevision(path, fd, stat);
|
|
1566
1575
|
if (usageLogIdentityKey(revision) !== retained.identityKey) return null;
|
|
1567
1576
|
const size = Number(stat.size);
|
|
1568
1577
|
// A shrink means truncation or replacement-in-place; the retained rows may no
|
package/src/usage/summary.ts
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
type PersistedUsageEntry,
|
|
11
11
|
type UsageStatus,
|
|
12
12
|
} from "./log";
|
|
13
|
-
import { type AttemptCostEstimate, type CostEstimate, estimateAttemptCost, estimateRequestCost, serviceTierContext, type ServiceTierContext, tokensPerSecond } from "./cost";
|
|
13
|
+
import { type AttemptCostEstimate, type CostEstimate, estimateAttemptCost, estimateRequestCost, isMeasuredStatus, serviceTierContext, type ServiceTierContext, tokensPerSecond } from "./cost";
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* Canonical range members. The warm-up loop in the management usage route
|
|
@@ -303,7 +303,10 @@ export function computeEntryCost(entry: PersistedUsageEntry): EntryCostInfo {
|
|
|
303
303
|
isPriced = true;
|
|
304
304
|
}
|
|
305
305
|
}
|
|
306
|
-
|
|
306
|
+
// An unmeasured leg that may have generated billed tokens leaves the total unknown, not short.
|
|
307
|
+
const unknownCost = entry.attempts.some((attempt, index) =>
|
|
308
|
+
!attemptEstimates[index] && (!attempt.usage || !isMeasuredStatus(attempt.usageStatus)) && !spentNothing(attempt));
|
|
309
|
+
return { tier, estimate: null, attemptEstimates, costTotal, isPriced: isPriced && !unknownCost };
|
|
307
310
|
}
|
|
308
311
|
const estimate = estimateRequestCost({
|
|
309
312
|
...usageModelPriceOptions(entry, entry),
|
|
@@ -428,10 +431,6 @@ function blankTotals(): UsageSummaryTotals {
|
|
|
428
431
|
};
|
|
429
432
|
}
|
|
430
433
|
|
|
431
|
-
function isMeasuredStatus(status: UsageStatus): boolean {
|
|
432
|
-
return status === "reported" || status === "estimated";
|
|
433
|
-
}
|
|
434
|
-
|
|
435
434
|
export interface UsageAttribution {
|
|
436
435
|
requestId: string;
|
|
437
436
|
provider: string;
|
|
@@ -814,14 +813,28 @@ function blankRequestCounts(): UsageRequestCounts {
|
|
|
814
813
|
};
|
|
815
814
|
}
|
|
816
815
|
|
|
816
|
+
/**
|
|
817
|
+
* A usage-less attempt that provably cost nothing: it never reached upstream, or upstream refused it
|
|
818
|
+
* with an HTTP error before any output (providers do not bill a pre-generation 429/401/5xx). One
|
|
819
|
+
* whose stream began, aborted, or ended without a status may have generated billed tokens, so it
|
|
820
|
+
* stays an unknown cost. A request made only of such zeros carries neither fact and counts unpriced.
|
|
821
|
+
*/
|
|
822
|
+
function spentNothing(attempt: NonNullable<PersistedUsageEntry["attempts"]>[number] | undefined): boolean {
|
|
823
|
+
if (!attempt || attempt.usage) return false;
|
|
824
|
+
if (attempt.sendCount === 0 || attempt.locallyAnswered === true) return true;
|
|
825
|
+
return attempt.status >= 400 && attempt.streamAborted !== true && attempt.firstOutputMs === undefined;
|
|
826
|
+
}
|
|
827
|
+
|
|
817
828
|
function bumpRequestCounts(counts: UsageRequestCounts, facts: number, amount = 1): void {
|
|
818
829
|
counts.requests += amount;
|
|
819
830
|
const status = statusFromRequestFacts(facts);
|
|
820
831
|
if (isMeasuredStatus(status)) counts.measuredRequests += amount;
|
|
821
832
|
if (status === "reported") counts.reportedRequests += amount;
|
|
822
833
|
else if (status === "estimated") counts.estimatedRequests += amount;
|
|
823
|
-
|
|
824
|
-
|
|
834
|
+
// Unpriced dominates: one unpriceable part makes the request's cost unknown, and counting it
|
|
835
|
+
// on both sides overstated price coverage.
|
|
836
|
+
if ((facts & REQUEST_PRICED) !== 0 && (facts & REQUEST_UNPRICED) === 0) counts.pricedRequests += amount;
|
|
837
|
+
else counts.unpricedRequests += amount;
|
|
825
838
|
}
|
|
826
839
|
|
|
827
840
|
function mergeRequestCounts(target: UsageRequestCounts, source: UsageRequestCounts): void {
|
|
@@ -1557,7 +1570,7 @@ class StreamingUsageSummaryAccumulator implements UsageSummaryAccumulator {
|
|
|
1557
1570
|
: costInfo.estimate;
|
|
1558
1571
|
const ordinal = this.nextOrdinal++;
|
|
1559
1572
|
const facts = requestStatusFact(attribution.usageStatus)
|
|
1560
|
-
| (estimate ? REQUEST_PRICED : REQUEST_UNPRICED);
|
|
1573
|
+
| (estimate ? REQUEST_PRICED : spentNothing(entry.attempts?.[index]) ? 0 : REQUEST_UNPRICED);
|
|
1561
1574
|
const modelKey = this.addModelAttribution(partition, attribution, estimate, ordinal);
|
|
1562
1575
|
modelFacts.set(modelKey, (modelFacts.get(modelKey) ?? 0) | facts);
|
|
1563
1576
|
if (this.mode === "row-unique") {
|
|
@@ -18,8 +18,10 @@
|
|
|
18
18
|
*
|
|
19
19
|
* Display-time estimation only — these rows never affect billing.
|
|
20
20
|
*/
|
|
21
|
-
import type { OcxConfig, OcxProviderConfig, ProviderCostOverlay } from "../types";
|
|
22
|
-
import {
|
|
21
|
+
import type { OcxConfig, OcxProviderConfig, ProviderCostOverlay, PromptLengthPricing } from "../types";
|
|
22
|
+
import type { ExpectedPriceOverlay } from "./expected-prices";
|
|
23
|
+
import { validCost4 } from "./user-cost-validation";
|
|
24
|
+
export { COST4_RATE_KEYS, isValidCost4Rate, validCost4, isValidPromptLengthPricing } from "./user-cost-validation";
|
|
23
25
|
import { redactSecretString } from "../lib/redact";
|
|
24
26
|
import { isSelectableCodexPoolAccount, MAIN_CODEX_ACCOUNT_ID } from "../codex/account-id";
|
|
25
27
|
import { codexAccountLogLabel } from "../codex/account-label";
|
|
@@ -236,22 +238,13 @@ export function resetPreservedDiskOnlyProvidersForTests(): void {
|
|
|
236
238
|
preservationOwnerStates.clear();
|
|
237
239
|
}
|
|
238
240
|
|
|
239
|
-
/**
|
|
240
|
-
export
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
return
|
|
245
|
-
|
|
246
|
-
&& rate >= 0
|
|
247
|
-
&& rate <= MAX_COST4_RATE;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
/** True when `value` is a complete cost entry: all four rates are non-negative finite numbers. */
|
|
251
|
-
function validCost4(value: unknown): value is ProviderCostOverlay {
|
|
252
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
253
|
-
const entry = value as Record<string, unknown>;
|
|
254
|
-
return COST4_RATE_KEYS.every(key => isValidCost4Rate(entry[key]));
|
|
241
|
+
/** Copy a validated override; automatic is represented by absence. */
|
|
242
|
+
export function copyPromptLengthPricing(policy: PromptLengthPricing): PromptLengthPricing | undefined {
|
|
243
|
+
if (policy.mode === "automatic") return undefined;
|
|
244
|
+
if (policy.mode !== "custom") return { mode: policy.mode };
|
|
245
|
+
const { input, output, cacheRead, cacheWrite } = policy.rates;
|
|
246
|
+
return { mode: "custom", thresholdTokens: policy.thresholdTokens, comparison: policy.comparison,
|
|
247
|
+
rates: { input, output, cacheRead, cacheWrite } };
|
|
255
248
|
}
|
|
256
249
|
|
|
257
250
|
/**
|
|
@@ -273,10 +266,11 @@ export function refreshUserCostOverlays(config: OcxConfig): void {
|
|
|
273
266
|
if (!costs || typeof costs !== "object" || Array.isArray(costs)) continue;
|
|
274
267
|
for (const [modelId, cost4] of Object.entries(costs)) {
|
|
275
268
|
if (!modelId.trim() || !validCost4(cost4)) continue;
|
|
269
|
+
const policy = cost4.promptLengthPricing && copyPromptLengthPricing(cost4.promptLengthPricing);
|
|
276
270
|
rows.push({
|
|
277
271
|
provider: providerName,
|
|
278
272
|
modelId,
|
|
279
|
-
// Copy
|
|
273
|
+
// Copy only validated rates and policy: a hand-edited row may
|
|
280
274
|
// carry extra properties (e.g. a misplaced apiKey) that must never
|
|
281
275
|
// reach display estimates or /api/logs through the registry.
|
|
282
276
|
cost4: {
|
|
@@ -285,6 +279,7 @@ export function refreshUserCostOverlays(config: OcxConfig): void {
|
|
|
285
279
|
cacheRead: cost4.cacheRead,
|
|
286
280
|
cacheWrite: cost4.cacheWrite,
|
|
287
281
|
},
|
|
282
|
+
...(policy ? { promptLengthPricing: policy } : {}),
|
|
288
283
|
// Display provenance only: redact token-shaped provider/model ids so
|
|
289
284
|
// the source string can never echo a pasted key. Matching still uses
|
|
290
285
|
// the raw fields, and the change-detection signature below MUST keep
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** Pure model-cost validation shared by config, migration, and display lifting. */
|
|
2
|
+
import type { ProviderCostOverlay, PromptLengthPricing } from "../types";
|
|
3
|
+
import { MAX_COST4_RATE } from "./expected-prices";
|
|
4
|
+
|
|
5
|
+
/** Exact four fields accepted for a user-configured cost tuple. */
|
|
6
|
+
export const COST4_RATE_KEYS = ["input", "output", "cacheRead", "cacheWrite"] as const;
|
|
7
|
+
|
|
8
|
+
/** Shared per-rate predicate used by config validation, display sanitization, and runtime lifting. */
|
|
9
|
+
export function isValidCost4Rate(rate: unknown): rate is number {
|
|
10
|
+
return typeof rate === "number"
|
|
11
|
+
&& Number.isFinite(rate)
|
|
12
|
+
&& rate >= 0
|
|
13
|
+
&& rate <= MAX_COST4_RATE;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Complete base rates with a valid optional policy; extra top-level fields are never copied. */
|
|
17
|
+
export function validCost4(value: unknown): value is ProviderCostOverlay {
|
|
18
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
19
|
+
const entry = value as Record<string, unknown>;
|
|
20
|
+
return COST4_RATE_KEYS.every(key => Object.hasOwn(entry, key) && isValidCost4Rate(entry[key]))
|
|
21
|
+
&& (!Object.hasOwn(entry, "promptLengthPricing") || isValidPromptLengthPricing(entry.promptLengthPricing));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Strict nested policy boundary; diagnostics never echo untrusted policy fields. */
|
|
25
|
+
export function isValidPromptLengthPricing(value: unknown): value is PromptLengthPricing {
|
|
26
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
27
|
+
const policy = value as Record<string, unknown>;
|
|
28
|
+
if (!Object.hasOwn(policy, "mode")) return false;
|
|
29
|
+
if (policy.mode === "automatic" || policy.mode === "flat") return Object.keys(policy).length === 1;
|
|
30
|
+
if (policy.mode !== "custom" || Object.keys(policy).length !== 4
|
|
31
|
+
|| !["mode", "thresholdTokens", "comparison", "rates"].every(key => Object.hasOwn(policy, key))
|
|
32
|
+
|| typeof policy.thresholdTokens !== "number" || !Number.isSafeInteger(policy.thresholdTokens)
|
|
33
|
+
|| policy.thresholdTokens <= 0 || (policy.comparison !== "gt" && policy.comparison !== "gte")) return false;
|
|
34
|
+
const rates = policy.rates;
|
|
35
|
+
return !!rates && typeof rates === "object" && !Array.isArray(rates)
|
|
36
|
+
&& Object.keys(rates).length === COST4_RATE_KEYS.length
|
|
37
|
+
&& COST4_RATE_KEYS.every(key => Object.hasOwn(rates, key) && isValidCost4Rate((rates as Record<string, unknown>)[key]));
|
|
38
|
+
}
|
|
@@ -226,6 +226,19 @@ export function isVisionSidecarConsumer(config: VisionCapabilityConfig, provider
|
|
|
226
226
|
return isVisionSidecarConsumerWithCache(config, providerName, modelId, new Map());
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
+
/**
|
|
230
|
+
* `isVisionSidecarConsumer` for many rows of one config. Rows of the same provider share one
|
|
231
|
+
* registry enrichment instead of rebuilding it per row, which matters for `/api/models`
|
|
232
|
+
* listings with thousands of rows. Bind it to a single config snapshot; it does not observe
|
|
233
|
+
* provider changes made after the first lookup.
|
|
234
|
+
*/
|
|
235
|
+
export function visionSidecarConsumerClassifier(
|
|
236
|
+
config: VisionCapabilityConfig,
|
|
237
|
+
): (providerName: string, modelId: string) => boolean {
|
|
238
|
+
const cache: EnrichedProviderCache = new Map();
|
|
239
|
+
return (providerName, modelId) => isVisionSidecarConsumerWithCache(config, providerName, modelId, cache);
|
|
240
|
+
}
|
|
241
|
+
|
|
229
242
|
/**
|
|
230
243
|
* Can this model accept an image on the wire? Sources are consulted in descending
|
|
231
244
|
* trustworthiness; the first that speaks wins. `undefined` means nothing knows,
|