@bitkyc08/opencodex 2.55.0 → 2.57.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/ocx.mjs +10 -0
- package/gui/dist/assets/{index-BBOZWGB6.css → index-C5-RdDmD.css} +1 -1
- package/gui/dist/assets/{index-VuoiWj9J.js → index-Cz7CLdif.js} +21 -21
- package/gui/dist/index.html +2 -2
- package/package.json +4 -3
- package/src/adapters/base.ts +21 -0
- package/src/adapters/codebuddy/adapter.ts +2 -1
- package/src/adapters/codebuddy/scaffold-guard.ts +248 -0
- package/src/adapters/command-code.ts +1 -1
- package/src/adapters/cursor/envelope-echo.ts +8 -2
- package/src/adapters/cursor/transport-retry.ts +46 -1
- package/src/adapters/cursor.ts +4 -0
- package/src/adapters/google.ts +7 -7
- package/src/adapters/kiro/adapter.ts +42 -1
- package/src/adapters/kiro/payload.ts +17 -3
- package/src/adapters/kiro/reasoning.ts +70 -7
- package/src/adapters/kiro/stream.ts +8 -2
- package/src/adapters/kiro/wire.ts +2 -1
- package/src/adapters/kiro-events.ts +21 -13
- package/src/adapters/kiro-retry.ts +23 -4
- package/src/adapters/openai-chat/errors.ts +116 -0
- package/src/adapters/openai-chat/messages.ts +346 -0
- package/src/adapters/openai-chat/passthrough.ts +146 -0
- package/src/adapters/openai-chat/response-events.ts +117 -0
- package/src/adapters/openai-chat/tool-call-validation.ts +200 -0
- package/src/adapters/openai-chat/tool-name-registry.ts +166 -0
- package/src/adapters/openai-chat/tool-schema.ts +495 -0
- package/src/adapters/openai-chat/wire.ts +50 -0
- package/src/adapters/openai-chat.ts +40 -1452
- package/src/adapters/openai-responses/canonical-forward.ts +202 -0
- package/src/adapters/openai-responses/image-gen.ts +406 -0
- package/src/adapters/openai-responses/internal.ts +3 -0
- package/src/adapters/openai-responses/passthrough.ts +642 -0
- package/src/adapters/openai-responses/prompt-cache.ts +83 -0
- package/src/adapters/openai-responses/reasoning.ts +220 -0
- package/src/adapters/openai-responses/request-strips.ts +185 -0
- package/src/adapters/openai-responses/tool-output-recovery.ts +509 -0
- package/src/adapters/openai-responses/tool-schema.ts +293 -0
- package/src/adapters/openai-responses/web-search.ts +156 -0
- package/src/adapters/openai-responses.ts +4 -2625
- package/src/bridge/errors.ts +58 -0
- package/src/bridge/internal.ts +174 -0
- package/src/bridge/response-json.ts +630 -0
- package/src/bridge/sse.ts +1462 -0
- package/src/bridge.ts +5 -2204
- package/src/chat/inbound.ts +12 -1
- package/src/claude/desktop-profile.ts +66 -9
- package/src/claude/outbound.ts +18 -0
- package/src/cli/account-main.ts +1 -1
- package/src/cli/capabilities.ts +2 -2
- package/src/cli/combo.ts +10 -1
- package/src/cli/index.ts +48 -5
- package/src/cli/registry.ts +2 -1
- package/src/cli/system-command.ts +4 -4
- package/src/clients/config-export.ts +7 -3
- package/src/codex/account-label.ts +14 -3
- package/src/codex/account-lifecycle.ts +3 -0
- package/src/codex/account-store.ts +184 -35
- package/src/codex/account-usability.ts +21 -0
- package/src/codex/auth-api/account-list.ts +507 -0
- package/src/codex/auth-api/http.ts +32 -0
- package/src/codex/auth-api/login-flow.ts +566 -0
- package/src/codex/auth-api/login-state.ts +64 -0
- package/src/codex/auth-api/main-account-probe.ts +331 -0
- package/src/codex/auth-api/pool-mode-gate.ts +274 -0
- package/src/codex/auth-api/pool-quota-probe.ts +512 -0
- package/src/codex/auth-api/reset-credit-service.ts +431 -0
- package/src/codex/auth-api/routes.ts +425 -0
- package/src/codex/auth-api/runtime-config.ts +48 -0
- package/src/codex/auth-api.ts +27 -3118
- package/src/codex/auth-context.ts +252 -35
- package/src/codex/catalog/aggregation.ts +80 -1
- package/src/codex/catalog/auto-review.ts +507 -0
- package/src/codex/catalog/build-entries.ts +981 -0
- package/src/codex/catalog/combo-member.ts +375 -0
- package/src/codex/catalog/derive-entry.ts +229 -0
- package/src/codex/catalog/effort.ts +0 -1
- package/src/codex/catalog/gated-native-warn.ts +63 -0
- package/src/codex/catalog/gather-capture.ts +533 -0
- package/src/codex/catalog/model-hints.ts +691 -0
- package/src/codex/catalog/model-visibility.ts +305 -0
- package/src/codex/catalog/provider-fetch.ts +52 -2942
- package/src/codex/catalog/provider-models.ts +685 -0
- package/src/codex/catalog/remote.ts +30 -0
- package/src/codex/catalog/restore.ts +132 -0
- package/src/codex/catalog/retained-sync.ts +714 -0
- package/src/codex/catalog/routed-gather.ts +895 -0
- package/src/codex/catalog/subagent-roster.ts +176 -0
- package/src/codex/catalog/sync.ts +52 -2698
- package/src/codex/cli-install-provenance.ts +7 -1
- package/src/codex/convergence.ts +7 -2
- package/src/codex/desktop-app/types.ts +11 -2
- package/src/codex/desktop-app/windows.ts +5 -5
- package/src/codex/inject/config-toml.ts +563 -0
- package/src/codex/inject/remove.ts +192 -0
- package/src/codex/inject/restore.ts +567 -0
- package/src/codex/inject/routing-classify.ts +109 -0
- package/src/codex/inject/routing-target.ts +125 -0
- package/src/codex/inject.ts +89 -1444
- package/src/codex/lineage.ts +458 -0
- package/src/codex/model-entitlements.ts +152 -15
- package/src/codex/pool-refresh-backoff.ts +161 -0
- package/src/codex/quota-rejection.ts +104 -15
- package/src/codex/routing/active-account.ts +194 -0
- package/src/codex/routing/cache-affinity.ts +70 -0
- package/src/codex/routing/cooldown-math.ts +285 -0
- package/src/codex/routing/health-store.ts +402 -0
- package/src/codex/routing/probe-lease.ts +358 -0
- package/src/codex/routing/selection.ts +780 -0
- package/src/codex/routing/thread-affinity.ts +586 -0
- package/src/codex/routing/transient-hold-dispatch.ts +141 -0
- package/src/codex/routing.ts +370 -2271
- package/src/codex/shim-fingerprint.ts +223 -0
- package/src/codex/shim-inspect.ts +175 -0
- package/src/codex/shim-probe.ts +367 -0
- package/src/codex/shim-restore-lock.ts +169 -0
- package/src/codex/shim-state-file.ts +151 -0
- package/src/codex/shim-templates.ts +265 -0
- package/src/codex/shim.ts +48 -1268
- package/src/codex/warmup.ts +1 -1
- package/src/combos/failover.ts +85 -0
- package/src/combos/request.ts +17 -10
- package/src/combos/types.ts +23 -2
- package/src/config/diagnostics.ts +705 -0
- package/src/config/feature-flags.ts +55 -0
- package/src/config/live-reconcile.ts +403 -0
- package/src/config/load-degrade.ts +880 -0
- package/src/config/mutation-lock.ts +244 -0
- package/src/config/openai-tier-backup.ts +268 -0
- package/src/config/pending-teardown.ts +31 -0
- package/src/config/persist-unlocked.ts +92 -0
- package/src/config/proxy-env.ts +188 -0
- package/src/config/salvage.ts +244 -0
- package/src/config/schema/config-schema.ts +640 -0
- package/src/config/schema/leaf-validators.ts +855 -0
- package/src/config/warn-memo.ts +28 -0
- package/src/config.ts +234 -4481
- package/src/generated/compatibility-version.json +649 -121
- package/src/images/loop.ts +1 -1
- package/src/lib/errors.ts +17 -0
- package/src/lib/request-execution-budget.ts +198 -23
- package/src/lib/spend-reservation-ledger.ts +958 -0
- package/src/lib/state-store-registrations.ts +6 -2
- package/src/lib/test-home-guard.ts +85 -1
- package/src/lib/upstream-retry.ts +132 -21
- package/src/lib/windows-elevation.ts +76 -14
- package/src/lib/workflow-budget.ts +553 -30
- package/src/oauth/index.ts +2 -2
- package/src/oauth/key-providers.ts +2 -2
- package/src/providers/kiro-models.ts +4 -3
- package/src/providers/label.ts +19 -1
- package/src/providers/model-discovery.ts +16 -0
- package/src/providers/quota/account-cache.ts +441 -0
- package/src/providers/quota/antigravity.ts +295 -0
- package/src/providers/quota/report-cache.ts +320 -0
- package/src/providers/quota/vendor-probes-key.ts +1243 -0
- package/src/providers/quota/vendor-probes-oauth.ts +590 -0
- package/src/providers/quota.ts +324 -3079
- package/src/providers/registry/entries-core.ts +1228 -0
- package/src/providers/registry/entries-extended.ts +1213 -0
- package/src/providers/registry/model-seeds.ts +912 -0
- package/src/providers/registry/types.ts +352 -0
- package/src/providers/registry.ts +24 -3536
- package/src/responses/continuation-ownership.ts +29 -0
- package/src/responses/reasoning-envelope.ts +6 -3
- package/src/responses/state/replay-fingerprint.ts +80 -0
- package/src/responses/state/snapshot-codec.ts +104 -0
- package/src/responses/state/spill-failure.ts +118 -0
- package/src/responses/state/spill-queue.ts +665 -0
- package/src/responses/state/temp-recovery.ts +257 -0
- package/src/responses/state.ts +82 -1143
- package/src/routing/identity-domains.ts +456 -0
- package/src/routing/probe-lease.ts +613 -0
- package/src/server/chat-completions.ts +3 -1
- package/src/server/chat-native.ts +37 -9
- package/src/server/index/bounded-request.ts +88 -0
- package/src/server/index/live-sideband.ts +601 -0
- package/src/server/index/serve-options.ts +1766 -0
- package/src/server/index/startup-warnings.ts +213 -0
- package/src/server/index/websocket-handler.ts +339 -0
- package/src/server/index.ts +45 -2552
- package/src/server/inspection-tee.ts +107 -0
- package/src/server/live.ts +46 -1
- package/src/server/management/combo-routes.ts +10 -1
- package/src/server/management/route-registry.ts +26 -23
- package/src/server/management/shared.ts +8 -5
- package/src/server/management/workflow-budget-routes.ts +133 -0
- package/src/server/management-api.ts +12 -0
- package/src/server/relay-eager.ts +2 -0
- package/src/server/relay.ts +14 -19
- package/src/server/request-log-conversation.ts +9 -7
- package/src/server/request-log.ts +372 -4
- package/src/server/response-log-body.ts +153 -0
- package/src/server/responses/account-change-state.ts +307 -0
- package/src/server/responses/adapter-continuation.ts +540 -0
- package/src/server/responses/adapter-delivery.ts +208 -0
- package/src/server/responses/adapter-dispatch.ts +1042 -0
- package/src/server/responses/codex-ws-wire.ts +5 -0
- package/src/server/responses/collaboration.ts +74 -4
- package/src/server/responses/combo-session-recall.ts +68 -8
- package/src/server/responses/compact.ts +113 -17
- package/src/server/responses/completion-policy.ts +33 -0
- package/src/server/responses/core-auth.ts +529 -0
- package/src/server/responses/core-codex-account.ts +907 -0
- package/src/server/responses/core-combo-failure.ts +210 -0
- package/src/server/responses/core-combo.ts +787 -0
- package/src/server/responses/core-errors.ts +170 -0
- package/src/server/responses/core-lifetime.ts +95 -0
- package/src/server/responses/core-normalize.ts +350 -0
- package/src/server/responses/core-opaque-recovery.ts +380 -0
- package/src/server/responses/core-options.ts +159 -0
- package/src/server/responses/core-replay.ts +298 -0
- package/src/server/responses/core.ts +192 -8893
- package/src/server/responses/encrypted-payload.ts +0 -1
- package/src/server/responses/input-admission.ts +126 -6
- package/src/server/responses/passthrough-delivery.ts +869 -0
- package/src/server/responses/passthrough-dispatch.ts +1494 -0
- package/src/server/responses/passthrough-error.ts +38 -2
- package/src/server/responses/passthrough-execution.ts +54 -0
- package/src/server/responses/request-prepare.ts +1080 -0
- package/src/server/responses/request-send-budget.ts +259 -0
- package/src/server/responses/request-sidecar-auth.ts +149 -0
- package/src/server/responses/request-spend.ts +147 -0
- package/src/server/responses/request-transport.ts +803 -0
- package/src/server/responses/response-effects.ts +157 -0
- package/src/server/responses/run-turn-execution.ts +476 -0
- package/src/server/responses/sidecar-execution.ts +463 -0
- package/src/server/responses/terminal-guard.ts +65 -4
- package/src/server/responses-image-gen-repair.ts +1 -1
- package/src/server/responses-undeclared-tool-guard.ts +9 -5
- package/src/server/workflow-refusal.ts +84 -0
- package/src/service/windows-ops.ts +210 -16
- package/src/service/windows-scheduler.ts +28 -21
- package/src/service.ts +1 -1
- package/src/types/config.ts +34 -1
- package/src/types/request.ts +8 -5
- package/src/types/tools.ts +24 -0
- package/src/types.ts +2 -0
- package/src/update/index.ts +10 -0
- package/src/update/stop-contract.d.mts +1 -0
- package/src/update/stop-contract.mjs +19 -0
- package/src/update/stop-decision.d.mts +1 -1
- package/src/update/stop-decision.mjs +12 -3
- package/src/usage/log.ts +147 -1
- package/src/usage/summary.ts +171 -21
- package/src/vision/anthropic-describe.ts +1 -1
- package/src/vision/describe.ts +5 -5
- package/src/web-search/anthropic-executor.ts +1 -1
- package/src/web-search/exa-executor.ts +1 -1
- package/src/web-search/executor.ts +1 -1
- package/src/web-search/gemini-executor.ts +1 -1
- package/src/web-search/loop.ts +1 -1
- package/src/web-search/ollama-executor.ts +1 -1
- package/src/web-search/parse.ts +67 -14
- package/src/web-search/passthrough-bridge.ts +64 -31
- package/src/web-search/xai-executor.ts +1 -1
|
@@ -0,0 +1,1243 @@
|
|
|
1
|
+
import { resolveProviderApiKey } from "../key-store";
|
|
2
|
+
import { getProviderRegistryEntry, registryEntryForProviderDestination } from "../registry";
|
|
3
|
+
import { isCanonicalOllamaCloudUrl } from "../../adapters/ollama-native-url";
|
|
4
|
+
import { QUOTA_JSON_READ_FAILURE, asRecord, normalizePercent, normalizeResetAt, readQuotaJson, REQUEST_TIMEOUT_MS, toFiniteNumber } from "../quota-wire";
|
|
5
|
+
import {
|
|
6
|
+
AUTHORITATIVE_EMPTY_QUOTA,
|
|
7
|
+
hasQuotaRows,
|
|
8
|
+
keyReport,
|
|
9
|
+
report,
|
|
10
|
+
TERMINAL_QUOTA_FAILURE,
|
|
11
|
+
type ProviderQuotaProbeResult,
|
|
12
|
+
type ProviderQuotaReport,
|
|
13
|
+
} from "./report-cache";
|
|
14
|
+
import { getTokenForAccountQuotaProbe } from "./account-cache";
|
|
15
|
+
import type { AccountQuotaMode, ProviderQuota, ProviderQuotaCreditsUsd } from "../quota-types";
|
|
16
|
+
import type { OcxProviderConfig } from "../../types";
|
|
17
|
+
|
|
18
|
+
const KIMI_CODE_BASE_URL = "https://api.kimi.com/coding/v1";
|
|
19
|
+
const KIMI_CODE_USAGE_URL = `${KIMI_CODE_BASE_URL}/usages`;
|
|
20
|
+
const COMMAND_CODE_BASE_URL = "https://api.commandcode.ai";
|
|
21
|
+
const COMMAND_CODE_WHOAMI_URL = `${COMMAND_CODE_BASE_URL}/alpha/whoami`;
|
|
22
|
+
const COMMAND_CODE_CREDITS_URL = `${COMMAND_CODE_BASE_URL}/alpha/billing/credits`;
|
|
23
|
+
const COMMAND_CODE_SUBSCRIPTIONS_URL = `${COMMAND_CODE_BASE_URL}/alpha/billing/subscriptions`;
|
|
24
|
+
const COMMAND_CODE_USAGE_URL = `${COMMAND_CODE_BASE_URL}/alpha/usage/summary`;
|
|
25
|
+
const A6API_BASE_URL = "https://api.a6api.com";
|
|
26
|
+
const OPENCODE_GO_BASE_URL = "https://opencode.ai/zen/go/v1";
|
|
27
|
+
const OPENCODE_GO_USAGE_URL = `${OPENCODE_GO_BASE_URL}/usage`;
|
|
28
|
+
const OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1";
|
|
29
|
+
const DEEPSEEK_BASE_URL = "https://api.deepseek.com";
|
|
30
|
+
const CLINE_BASE_URL = "https://api.cline.bot";
|
|
31
|
+
const OLLAMA_CLOUD_BASE_URL = "https://ollama.com";
|
|
32
|
+
const OLLAMA_CLOUD_USAGE_URL = `${OLLAMA_CLOUD_BASE_URL}/api/usage`;
|
|
33
|
+
const ZAI_BASE_URL = "https://api.z.ai";
|
|
34
|
+
const ZAI_CN_BASE_URL = "https://open.bigmodel.cn";
|
|
35
|
+
const MINIMAX_REMAINS_URL = "https://www.minimax.io/v1/token_plan/remains";
|
|
36
|
+
const MOONSHOT_BASE_URL = "https://api.moonshot.ai/v1";
|
|
37
|
+
const VENICE_BASE_URL = "https://api.venice.ai/api/v1";
|
|
38
|
+
const SYNTHETIC_BASE_URL = "https://api.synthetic.new/v2";
|
|
39
|
+
const DEEPINFRA_BASE_URL = "https://api.deepinfra.com";
|
|
40
|
+
const NEURALWATT_BASE_URL = "https://api.neuralwatt.com/v1";
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
function isCanonicalA6apiBaseUrl(baseUrl: string): boolean {
|
|
44
|
+
const normalized = normalizedBaseUrl(baseUrl);
|
|
45
|
+
return normalized === A6API_BASE_URL || normalized === `${A6API_BASE_URL}/v1`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isCanonicalOpenCodeGoBaseUrl(baseUrl: string): boolean {
|
|
49
|
+
return normalizedBaseUrl(baseUrl) === OPENCODE_GO_BASE_URL;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function isCanonicalOpenRouterBaseUrl(baseUrl: string): boolean {
|
|
53
|
+
const normalized = normalizedBaseUrl(baseUrl);
|
|
54
|
+
return normalized === OPENROUTER_BASE_URL;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function isCanonicalDeepSeekBaseUrl(baseUrl: string): boolean {
|
|
58
|
+
const normalized = normalizedBaseUrl(baseUrl);
|
|
59
|
+
return normalized === DEEPSEEK_BASE_URL || normalized === `${DEEPSEEK_BASE_URL}/v1`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function isCanonicalClineBaseUrl(baseUrl: string): boolean {
|
|
63
|
+
const normalized = normalizedBaseUrl(baseUrl);
|
|
64
|
+
return normalized === CLINE_BASE_URL || normalized === `${CLINE_BASE_URL}/api/v1`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function isCanonicalOllamaCloudBaseUrl(baseUrl?: string): boolean {
|
|
68
|
+
if (!baseUrl) return false;
|
|
69
|
+
try {
|
|
70
|
+
return isCanonicalOllamaCloudUrl(baseUrl);
|
|
71
|
+
} catch {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function zaiQuotaMonitorHost(baseUrl: string): string | null {
|
|
77
|
+
// Admission and destination selection must share one mapping: admitting a new
|
|
78
|
+
// international wire must never fall through to the CN host/authentication scheme.
|
|
79
|
+
switch (normalizedBaseUrl(baseUrl)) {
|
|
80
|
+
case ZAI_BASE_URL:
|
|
81
|
+
case `${ZAI_BASE_URL}/api/coding/paas/v4`:
|
|
82
|
+
case `${ZAI_BASE_URL}/api/anthropic`:
|
|
83
|
+
case `${ZAI_BASE_URL}/api/v1`:
|
|
84
|
+
return ZAI_BASE_URL;
|
|
85
|
+
case ZAI_CN_BASE_URL:
|
|
86
|
+
case `${ZAI_CN_BASE_URL}/api/coding/paas/v4`:
|
|
87
|
+
case `${ZAI_CN_BASE_URL}/api/v1`:
|
|
88
|
+
return ZAI_CN_BASE_URL;
|
|
89
|
+
default:
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function isCanonicalZaiBaseUrl(baseUrl: string): boolean {
|
|
95
|
+
return zaiQuotaMonitorHost(baseUrl) !== null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function isCanonicalMinimaxBaseUrl(baseUrl: string): boolean {
|
|
99
|
+
const normalized = normalizedBaseUrl(baseUrl);
|
|
100
|
+
return normalized === "https://api.minimax.io/v1" || normalized === "https://api.minimaxi.com/v1";
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function isCanonicalMoonshotBaseUrl(baseUrl: string): boolean {
|
|
104
|
+
const normalized = normalizedBaseUrl(baseUrl);
|
|
105
|
+
return normalized === MOONSHOT_BASE_URL || normalized === "https://api.moonshot.cn/v1";
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function isCanonicalVeniceBaseUrl(baseUrl: string): boolean {
|
|
109
|
+
return normalizedBaseUrl(baseUrl) === VENICE_BASE_URL;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function isCanonicalSyntheticBaseUrl(baseUrl: string): boolean {
|
|
113
|
+
const normalized = normalizedBaseUrl(baseUrl);
|
|
114
|
+
return normalized === SYNTHETIC_BASE_URL || normalized === "https://api.synthetic.new/openai/v1";
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function isCanonicalDeepInfraBaseUrl(baseUrl: string): boolean {
|
|
118
|
+
const normalized = normalizedBaseUrl(baseUrl);
|
|
119
|
+
return normalized === DEEPINFRA_BASE_URL || normalized === `${DEEPINFRA_BASE_URL}/v1/openai`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function isCanonicalNeuralwattBaseUrl(baseUrl: string): boolean {
|
|
123
|
+
return normalizedBaseUrl(baseUrl) === NEURALWATT_BASE_URL;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function a6apiPayload(value: unknown): Record<string, unknown> | null {
|
|
127
|
+
const body = asRecord(value);
|
|
128
|
+
return asRecord(body?.data) ?? body;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function firstFinite(record: Record<string, unknown> | null, names: string[]): number | undefined {
|
|
132
|
+
if (!record) return undefined;
|
|
133
|
+
for (const name of names) {
|
|
134
|
+
const value = toFiniteNumber(record[name]);
|
|
135
|
+
if (value !== undefined) return value;
|
|
136
|
+
}
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function fetchA6apiQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
|
|
141
|
+
// Never send a configured API key to a lookalike host or through a redirect.
|
|
142
|
+
if (!isCanonicalA6apiBaseUrl(config.baseUrl)) return null;
|
|
143
|
+
const apiKey = resolveProviderApiKey(config.apiKey)?.trim();
|
|
144
|
+
if (!apiKey) return null;
|
|
145
|
+
const headers = { Accept: "application/json", Authorization: `Bearer ${apiKey}` } as const;
|
|
146
|
+
const [subscriptionResponse, tokenResponse] = await Promise.all([
|
|
147
|
+
fetch(`${A6API_BASE_URL}/dashboard/billing/subscription`, {
|
|
148
|
+
headers, redirect: "error", signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
149
|
+
}),
|
|
150
|
+
fetch(`${A6API_BASE_URL}/api/usage/token/`, {
|
|
151
|
+
headers, redirect: "error", signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
152
|
+
}),
|
|
153
|
+
]);
|
|
154
|
+
if (!subscriptionResponse.ok || !tokenResponse.ok) {
|
|
155
|
+
const statuses = [subscriptionResponse.status, tokenResponse.status];
|
|
156
|
+
// 408/429 are transient (timeout/throttle), not invalid-account signals: keep the
|
|
157
|
+
// last-good row like 5xx/network failures. 401/403 (bad key) and 404 (contract change)
|
|
158
|
+
// stay terminal.
|
|
159
|
+
return statuses.some(status => status >= 400 && status < 500 && status !== 429 && status !== 408)
|
|
160
|
+
? TERMINAL_QUOTA_FAILURE
|
|
161
|
+
: null;
|
|
162
|
+
}
|
|
163
|
+
const [subscriptionBody, tokenBody] = await Promise.all([
|
|
164
|
+
readQuotaJson(subscriptionResponse),
|
|
165
|
+
readQuotaJson(tokenResponse),
|
|
166
|
+
]);
|
|
167
|
+
if (subscriptionBody === QUOTA_JSON_READ_FAILURE || tokenBody === QUOTA_JSON_READ_FAILURE) return null;
|
|
168
|
+
const subscription = a6apiPayload(subscriptionBody);
|
|
169
|
+
const token = a6apiPayload(tokenBody);
|
|
170
|
+
const unlimited = token?.unlimited_quota === true
|
|
171
|
+
|| token?.unlimited_quota === 1
|
|
172
|
+
|| token?.unlimited_quota === "true";
|
|
173
|
+
const normalizedExpiry = normalizeResetAt(token?.expires_at);
|
|
174
|
+
const expiry = normalizedExpiry && normalizedExpiry > 0
|
|
175
|
+
? { expiresAt: normalizedExpiry }
|
|
176
|
+
: {};
|
|
177
|
+
if (unlimited) {
|
|
178
|
+
// Every row is an API-credit constraint on inference, so the display quota is also
|
|
179
|
+
// the routing projection. Passing it explicitly is the opt-in.
|
|
180
|
+
const quota: ProviderQuota = {
|
|
181
|
+
creditsUsd: {
|
|
182
|
+
used: 0,
|
|
183
|
+
limit: 0,
|
|
184
|
+
remaining: 0,
|
|
185
|
+
percent: 0,
|
|
186
|
+
unlimited: true,
|
|
187
|
+
...expiry,
|
|
188
|
+
},
|
|
189
|
+
customWindows: [{ label: "Unlimited API credits", percent: 0 }],
|
|
190
|
+
updatedAt: Date.now(),
|
|
191
|
+
};
|
|
192
|
+
return keyReport(provider, "a6api:billing", quota, config, apiKey, quota);
|
|
193
|
+
}
|
|
194
|
+
const limitUsd = firstFinite(subscription, ["hard_limit_usd"]);
|
|
195
|
+
const grantedUnits = firstFinite(token, ["total_granted"]);
|
|
196
|
+
const usedUnits = firstFinite(token, ["total_used"]);
|
|
197
|
+
const availableUnits = firstFinite(token, ["total_available"]);
|
|
198
|
+
const reconciledUnits = usedUnits !== undefined && availableUnits !== undefined
|
|
199
|
+
? usedUnits + availableUnits
|
|
200
|
+
: undefined;
|
|
201
|
+
const reconciliationTolerance = grantedUnits !== undefined
|
|
202
|
+
? Math.abs(grantedUnits) * 1e-9
|
|
203
|
+
: 0;
|
|
204
|
+
if (limitUsd === undefined || grantedUnits === undefined || usedUnits === undefined
|
|
205
|
+
|| availableUnits === undefined || limitUsd <= 0 || grantedUnits <= 0
|
|
206
|
+
|| usedUnits < 0 || availableUnits < 0
|
|
207
|
+
|| reconciledUnits === undefined
|
|
208
|
+
|| Math.abs(reconciledUnits - grantedUnits) > reconciliationTolerance) return TERMINAL_QUOTA_FAILURE;
|
|
209
|
+
const usdPerUnit = limitUsd / grantedUnits;
|
|
210
|
+
const usedUsd = usedUnits * usdPerUnit;
|
|
211
|
+
const remainingUsd = Math.max(0, availableUnits * usdPerUnit);
|
|
212
|
+
const percent = normalizePercent((usedUsd / limitUsd) * 100);
|
|
213
|
+
if (percent === undefined) return TERMINAL_QUOTA_FAILURE;
|
|
214
|
+
const label = `API credits ($${remainingUsd.toFixed(2)} of $${limitUsd.toFixed(2)} remaining)`;
|
|
215
|
+
const quota: ProviderQuota = {
|
|
216
|
+
creditsUsd: {
|
|
217
|
+
used: usedUsd,
|
|
218
|
+
limit: limitUsd,
|
|
219
|
+
remaining: remainingUsd,
|
|
220
|
+
percent,
|
|
221
|
+
...expiry,
|
|
222
|
+
},
|
|
223
|
+
customWindows: [{ label, percent }],
|
|
224
|
+
updatedAt: Date.now(),
|
|
225
|
+
};
|
|
226
|
+
// The credit balance funds inference itself, so display and routing scope agree.
|
|
227
|
+
return keyReport(provider, "a6api:billing", quota, config, apiKey, quota);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function parseOpenCodeGoUsageWindow(value: unknown): { percent: number; resetAt?: number } | null {
|
|
231
|
+
const row = asRecord(value);
|
|
232
|
+
if (!row) return null;
|
|
233
|
+
const percent = normalizePercent(row.percent);
|
|
234
|
+
if (percent === undefined) return null;
|
|
235
|
+
const resetAt = normalizeResetAt(row.resetsAt);
|
|
236
|
+
return { percent, ...(resetAt !== undefined ? { resetAt } : {}) };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async function fetchOpenCodeGoQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
|
|
240
|
+
// Never send a configured API key when the provider destination is not the built-in Go endpoint.
|
|
241
|
+
if (!isCanonicalOpenCodeGoBaseUrl(config.baseUrl)) return null;
|
|
242
|
+
const apiKey = resolveProviderApiKey(config.apiKey)?.trim();
|
|
243
|
+
if (!apiKey) return null;
|
|
244
|
+
const response = await fetch(OPENCODE_GO_USAGE_URL, {
|
|
245
|
+
headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
|
|
246
|
+
redirect: "error",
|
|
247
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
248
|
+
});
|
|
249
|
+
if (!response.ok) {
|
|
250
|
+
return response.status >= 400 && response.status < 500 && response.status !== 408 && response.status !== 429
|
|
251
|
+
? TERMINAL_QUOTA_FAILURE
|
|
252
|
+
: null;
|
|
253
|
+
}
|
|
254
|
+
const body = asRecord(await readQuotaJson(response));
|
|
255
|
+
const usage = asRecord(body?.usage);
|
|
256
|
+
if (!usage) return null;
|
|
257
|
+
const rolling = parseOpenCodeGoUsageWindow(usage.rolling);
|
|
258
|
+
const weekly = parseOpenCodeGoUsageWindow(usage.weekly);
|
|
259
|
+
const monthly = parseOpenCodeGoUsageWindow(usage.monthly);
|
|
260
|
+
const quota: ProviderQuota = {
|
|
261
|
+
...(rolling ? {
|
|
262
|
+
fiveHourPercent: rolling.percent,
|
|
263
|
+
...(rolling.resetAt !== undefined ? { fiveHourResetAt: rolling.resetAt } : {}),
|
|
264
|
+
} : {}),
|
|
265
|
+
...(weekly ? {
|
|
266
|
+
weeklyPercent: weekly.percent,
|
|
267
|
+
...(weekly.resetAt !== undefined ? { weeklyResetAt: weekly.resetAt } : {}),
|
|
268
|
+
} : {}),
|
|
269
|
+
...(monthly ? {
|
|
270
|
+
monthlyPercent: monthly.percent,
|
|
271
|
+
...(monthly.resetAt !== undefined ? { monthlyResetAt: monthly.resetAt } : {}),
|
|
272
|
+
} : {}),
|
|
273
|
+
updatedAt: Date.now(),
|
|
274
|
+
};
|
|
275
|
+
return keyReport(provider, "opencode-go:usage", quota, config, apiKey, quota);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* OpenRouter `GET /api/v1/key` — the key's own credit balance and optional
|
|
280
|
+
* per-key spending cap. `limit` is the configured cap (absent = uncapped);
|
|
281
|
+
* `usage` is lifetime spend; `limit_remaining` is what is left of the cap.
|
|
282
|
+
* When no cap is set there is no hard limit to meter against, so no bar is
|
|
283
|
+
* produced — the provider falls back to its documented reference.
|
|
284
|
+
*/
|
|
285
|
+
async function fetchOpenRouterQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
|
|
286
|
+
// Never send a configured API key to a lookalike host or through a redirect.
|
|
287
|
+
if (!isCanonicalOpenRouterBaseUrl(config.baseUrl)) return null;
|
|
288
|
+
const apiKey = resolveProviderApiKey(config.apiKey)?.trim();
|
|
289
|
+
if (!apiKey) return null;
|
|
290
|
+
const response = await fetch(`${OPENROUTER_BASE_URL}/key`, {
|
|
291
|
+
headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
|
|
292
|
+
redirect: "error",
|
|
293
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
294
|
+
});
|
|
295
|
+
if (!response.ok) {
|
|
296
|
+
return response.status >= 400 && response.status < 500 && response.status !== 408 && response.status !== 429
|
|
297
|
+
? TERMINAL_QUOTA_FAILURE
|
|
298
|
+
: null;
|
|
299
|
+
}
|
|
300
|
+
const body = asRecord(await readQuotaJson(response));
|
|
301
|
+
const data = asRecord(body?.data) ?? body;
|
|
302
|
+
if (!data) return null;
|
|
303
|
+
const limit = toFiniteNumber(data.limit);
|
|
304
|
+
const limitRemaining = toFiniteNumber(data.limit_remaining);
|
|
305
|
+
const usage = toFiniteNumber(data.usage);
|
|
306
|
+
// A successful no-cap response is a DELIBERATE change, not a transient
|
|
307
|
+
// failure: the old capped row must be dropped, not preserved as last-good.
|
|
308
|
+
if (limit === undefined || limit <= 0) return TERMINAL_QUOTA_FAILURE;
|
|
309
|
+
// Prefer the authoritative remaining-cap value when present: `usage` is
|
|
310
|
+
// lifetime accumulated spend and overstates a reset or re-capped key.
|
|
311
|
+
const used = limitRemaining !== undefined
|
|
312
|
+
? Math.max(0, limit - limitRemaining)
|
|
313
|
+
: usage !== undefined && usage >= 0 ? usage : undefined;
|
|
314
|
+
if (used === undefined) return null;
|
|
315
|
+
const percent = normalizePercent((used / limit) * 100);
|
|
316
|
+
if (percent === undefined) return null;
|
|
317
|
+
const remaining = Math.max(0, limit - used);
|
|
318
|
+
const label = `API credits ($${remaining.toFixed(2)} of $${limit.toFixed(2)} remaining)`;
|
|
319
|
+
// The per-key spending cap stops every request this credential can make, so the
|
|
320
|
+
// whole report is inference-wide routing evidence.
|
|
321
|
+
const quota: ProviderQuota = {
|
|
322
|
+
customWindows: [{ label, percent }],
|
|
323
|
+
updatedAt: Date.now(),
|
|
324
|
+
};
|
|
325
|
+
return keyReport(provider, "openrouter:key-info", quota, config, apiKey, quota);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* DeepSeek `GET /user/balance` — the account's granted + topped-up credit
|
|
330
|
+
* balance. The payload places `total_balance` / `granted_balance` inside
|
|
331
|
+
* entries of `balance_infos` (one row per currency); the row for the account's
|
|
332
|
+
* currency is selected by preference. `granted_balance` is a CURRENT balance
|
|
333
|
+
* component, not the original grant ceiling, so no consumed percentage is
|
|
334
|
+
* fabricated — the balance is reported as a balance-only window.
|
|
335
|
+
*/
|
|
336
|
+
async function fetchDeepSeekQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
|
|
337
|
+
if (!isCanonicalDeepSeekBaseUrl(config.baseUrl)) return null;
|
|
338
|
+
const apiKey = resolveProviderApiKey(config.apiKey)?.trim();
|
|
339
|
+
if (!apiKey) return null;
|
|
340
|
+
const response = await fetch(`${DEEPSEEK_BASE_URL}/user/balance`, {
|
|
341
|
+
headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
|
|
342
|
+
redirect: "error",
|
|
343
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
344
|
+
});
|
|
345
|
+
if (!response.ok) {
|
|
346
|
+
return response.status >= 400 && response.status < 500 && response.status !== 408 && response.status !== 429
|
|
347
|
+
? TERMINAL_QUOTA_FAILURE
|
|
348
|
+
: null;
|
|
349
|
+
}
|
|
350
|
+
const body = asRecord(await readQuotaJson(response));
|
|
351
|
+
// The payload nests balances under `balance_infos` rows keyed by currency;
|
|
352
|
+
// prefer a USD row, then CNY, then the first row that parses.
|
|
353
|
+
const infos = Array.isArray(body?.balance_infos) ? body.balance_infos as unknown[] : null;
|
|
354
|
+
const rows = infos
|
|
355
|
+
? infos.map((raw): Record<string, unknown> | null => asRecord(raw)).filter((r): r is Record<string, unknown> => r !== null)
|
|
356
|
+
: [];
|
|
357
|
+
const pick = (currency: string): Record<string, unknown> | null =>
|
|
358
|
+
rows.find(row => String(row.currency ?? "").toUpperCase() === currency) ?? null;
|
|
359
|
+
const preferred = pick("USD") ?? pick("CNY") ?? rows[0] ?? null;
|
|
360
|
+
if (!preferred) return null;
|
|
361
|
+
const totalBalance = toFiniteNumber(preferred.total_balance);
|
|
362
|
+
const grantedBalance = toFiniteNumber(preferred.granted_balance);
|
|
363
|
+
const toppedUp = toFiniteNumber(preferred.topped_up_balance);
|
|
364
|
+
const balance = totalBalance ?? grantedBalance ?? toppedUp;
|
|
365
|
+
if (balance === undefined || balance < 0) return null;
|
|
366
|
+
const label = grantedBalance !== undefined && grantedBalance > 0
|
|
367
|
+
? `API balance ($${balance.toFixed(2)} total, $${grantedBalance.toFixed(2)} granted)`
|
|
368
|
+
: `API balance ($${balance.toFixed(2)})`;
|
|
369
|
+
return report(provider, "deepseek:balance", {
|
|
370
|
+
customWindows: [{ label, percent: 0 }],
|
|
371
|
+
updatedAt: Date.now(),
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* ClinePass `GET /api/v1/users/me/plan/usage-limits` — the subscription's
|
|
377
|
+
* rolling five-hour, weekly, and monthly utilization, matching the existing
|
|
378
|
+
* ProviderQuota windows directly. The endpoint 404s (or returns a null plan)
|
|
379
|
+
* for accounts without an active ClinePass, which is a no-report, not an error.
|
|
380
|
+
*/
|
|
381
|
+
async function fetchClineQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
|
|
382
|
+
if (!isCanonicalClineBaseUrl(config.baseUrl)) return null;
|
|
383
|
+
const apiKey = resolveProviderApiKey(config.apiKey)?.trim();
|
|
384
|
+
if (!apiKey) return null;
|
|
385
|
+
const response = await fetch(`${CLINE_BASE_URL}/api/v1/users/me/plan/usage-limits`, {
|
|
386
|
+
headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
|
|
387
|
+
redirect: "error",
|
|
388
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
389
|
+
});
|
|
390
|
+
if (!response.ok) {
|
|
391
|
+
// 404 = no active plan; a plain "no plan" is a no-report, everything else
|
|
392
|
+
// 4xx (except 408/429) is a credential/contract problem.
|
|
393
|
+
if (response.status === 404) return null;
|
|
394
|
+
return response.status >= 400 && response.status < 500 && response.status !== 408 && response.status !== 429
|
|
395
|
+
? TERMINAL_QUOTA_FAILURE
|
|
396
|
+
: null;
|
|
397
|
+
}
|
|
398
|
+
const body = asRecord(await readQuotaJson(response));
|
|
399
|
+
const data = asRecord(body?.data) ?? body;
|
|
400
|
+
const limits = Array.isArray(data?.limits) ? data.limits : null;
|
|
401
|
+
if (!limits) return null;
|
|
402
|
+
const quota: ProviderQuota = { updatedAt: Date.now() };
|
|
403
|
+
let windows = 0;
|
|
404
|
+
for (const raw of limits) {
|
|
405
|
+
const row = asRecord(raw);
|
|
406
|
+
if (!row) continue;
|
|
407
|
+
const percent = normalizePercent(row.percentUsed);
|
|
408
|
+
if (percent === undefined) continue;
|
|
409
|
+
const resetAt = normalizeResetAt(row.resetsAt);
|
|
410
|
+
if (row.type === "five_hour") {
|
|
411
|
+
quota.fiveHourPercent = percent;
|
|
412
|
+
if (resetAt !== undefined) quota.fiveHourResetAt = resetAt;
|
|
413
|
+
windows += 1;
|
|
414
|
+
} else if (row.type === "weekly") {
|
|
415
|
+
quota.weeklyPercent = percent;
|
|
416
|
+
if (resetAt !== undefined) quota.weeklyResetAt = resetAt;
|
|
417
|
+
windows += 1;
|
|
418
|
+
} else if (row.type === "monthly") {
|
|
419
|
+
quota.monthlyPercent = percent;
|
|
420
|
+
if (resetAt !== undefined) quota.monthlyResetAt = resetAt;
|
|
421
|
+
windows += 1;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return windows > 0 ? keyReport(provider, "cline:plan-usage-limits", quota, config, apiKey, quota) : null;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Ollama Cloud `GET https://ollama.com/api/usage` — returns account usage.
|
|
429
|
+
* Legacy plans report rolling 5-hour `limits.session.usage` and 7-day
|
|
430
|
+
* `limits.weekly.usage`. Migrated monthly-credit plans report
|
|
431
|
+
* `limits.monthly.usage`. `usage` values are normalized fractions (0..1).
|
|
432
|
+
*/
|
|
433
|
+
function parseOllamaPercent(usageValue: unknown): number | undefined {
|
|
434
|
+
const usage = toFiniteNumber(usageValue);
|
|
435
|
+
if (usage === undefined || usage < 0) return undefined;
|
|
436
|
+
const percent = Math.round(usage * 10000) / 100;
|
|
437
|
+
return normalizePercent(percent);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
export function parseOllamaCloudQuota(body: Record<string, unknown> | null): ProviderQuota | null {
|
|
441
|
+
if (!body) return null;
|
|
442
|
+
const limits = asRecord(body.limits);
|
|
443
|
+
if (!limits) return null;
|
|
444
|
+
|
|
445
|
+
const quota: ProviderQuota = { updatedAt: Date.now() };
|
|
446
|
+
let windows = 0;
|
|
447
|
+
|
|
448
|
+
const session = asRecord(limits.session);
|
|
449
|
+
if (session) {
|
|
450
|
+
const percent = parseOllamaPercent(session.usage);
|
|
451
|
+
if (percent !== undefined) {
|
|
452
|
+
quota.fiveHourPercent = percent;
|
|
453
|
+
windows += 1;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const weekly = asRecord(limits.weekly);
|
|
458
|
+
if (weekly) {
|
|
459
|
+
const percent = parseOllamaPercent(weekly.usage);
|
|
460
|
+
if (percent !== undefined) {
|
|
461
|
+
quota.weeklyPercent = percent;
|
|
462
|
+
windows += 1;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const monthly = asRecord(limits.monthly);
|
|
467
|
+
if (monthly) {
|
|
468
|
+
const percent = parseOllamaPercent(monthly.usage);
|
|
469
|
+
if (percent !== undefined) {
|
|
470
|
+
quota.monthlyPercent = percent;
|
|
471
|
+
windows += 1;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
return windows > 0 ? quota : null;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
async function fetchOllamaCloudQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
|
|
479
|
+
const effectiveBaseUrl = config.baseUrl ?? getProviderRegistryEntry(provider)?.baseUrl ?? "";
|
|
480
|
+
if (!isCanonicalOllamaCloudBaseUrl(effectiveBaseUrl)) return null;
|
|
481
|
+
const apiKey = resolveProviderApiKey(config.apiKey)?.trim();
|
|
482
|
+
if (!apiKey) return null;
|
|
483
|
+
const response = await fetch(OLLAMA_CLOUD_USAGE_URL, {
|
|
484
|
+
headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
|
|
485
|
+
redirect: "error",
|
|
486
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
487
|
+
});
|
|
488
|
+
if (!response.ok) {
|
|
489
|
+
if (response.status === 404) return null;
|
|
490
|
+
return response.status >= 400 && response.status < 500 && response.status !== 408 && response.status !== 429
|
|
491
|
+
? TERMINAL_QUOTA_FAILURE
|
|
492
|
+
: null;
|
|
493
|
+
}
|
|
494
|
+
const body = asRecord(await readQuotaJson(response));
|
|
495
|
+
const quota = parseOllamaCloudQuota(body);
|
|
496
|
+
return quota ? keyReport(provider, "ollama-cloud:usage", quota, config, apiKey, quota) : null;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Z.AI GLM Coding Plan `GET /api/monitor/usage/quota/limit` — the coding-plan
|
|
501
|
+
* limits arrive as a `limits` array of `TOKENS_LIMIT` (newer plans call the
|
|
502
|
+
* same rows `CREDIT_LIMIT`) and `TIME_LIMIT` rows. `TOKENS_LIMIT`/`CREDIT_LIMIT`
|
|
503
|
+
* rows carry the window length as `unit`/`number`: unit 3 is hours (number 5 →
|
|
504
|
+
* the rolling five-hour window), unit 6 is weeks (number 1 → the weekly
|
|
505
|
+
* window). Every row's `percentage` is the consumed share (falling
|
|
506
|
+
* back to `currentValue`/`usage` when absent) and `nextResetTime` (unix ms)
|
|
507
|
+
* the window reset.
|
|
508
|
+
*
|
|
509
|
+
* `TIME_LIMIT` rows are deliberately ignored (issue #1168). They are the shared
|
|
510
|
+
* monthly MCP *call* allowance for Web Search / Web Reader / Zread — not a
|
|
511
|
+
* model-token budget — and `ProviderQuota.monthlyPercent` is consumed as a
|
|
512
|
+
* model-capacity signal: `headroomOf()` in `src/oauth/account-quota-rank.ts`
|
|
513
|
+
* takes the MAX across every window, so a user who spent their MCP search
|
|
514
|
+
* allowance would be ranked as having no model capacity left, and the dashboard
|
|
515
|
+
* would draw a full monthly bar for a plan whose model tokens are untouched.
|
|
516
|
+
* A payload carrying only `TIME_LIMIT` rows therefore reports no quota at all,
|
|
517
|
+
* which is the honest answer rather than a fabricated one.
|
|
518
|
+
*/
|
|
519
|
+
export function parseZaiQuotaLimits(data: Record<string, unknown> | null): ProviderQuota | null {
|
|
520
|
+
const limits = Array.isArray(data?.limits) ? data.limits as unknown[] : null;
|
|
521
|
+
if (!limits) return null;
|
|
522
|
+
const quota: ProviderQuota = { updatedAt: Date.now() };
|
|
523
|
+
let windows = 0;
|
|
524
|
+
for (const raw of limits) {
|
|
525
|
+
const row = asRecord(raw);
|
|
526
|
+
if (!row) continue;
|
|
527
|
+
// Gate on row type before deriving a percentage: an MCP row must not even
|
|
528
|
+
// contribute a parsed value to a model-quota report.
|
|
529
|
+
if (row.type !== "TOKENS_LIMIT" && row.type !== "CREDIT_LIMIT") continue;
|
|
530
|
+
const resetAt = normalizeResetAt(row.nextResetTime);
|
|
531
|
+
let percent = normalizePercent(row.percentage);
|
|
532
|
+
if (percent === undefined) {
|
|
533
|
+
const used = toFiniteNumber(row.currentValue);
|
|
534
|
+
const total = toFiniteNumber(row.usage);
|
|
535
|
+
if (used !== undefined && total !== undefined && total > 0) {
|
|
536
|
+
percent = normalizePercent((used / total) * 100);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
if (percent === undefined) continue;
|
|
540
|
+
const unit = toFiniteNumber(row.unit);
|
|
541
|
+
const number = toFiniteNumber(row.number);
|
|
542
|
+
if (unit === 3 && number === 5) {
|
|
543
|
+
quota.fiveHourPercent = percent;
|
|
544
|
+
if (resetAt !== undefined) quota.fiveHourResetAt = resetAt;
|
|
545
|
+
windows += 1;
|
|
546
|
+
} else if (unit === 6 && number === 1) {
|
|
547
|
+
quota.weeklyPercent = percent;
|
|
548
|
+
if (resetAt !== undefined) quota.weeklyResetAt = resetAt;
|
|
549
|
+
windows += 1;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
return windows > 0 ? quota : null;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Legacy Z.AI payload shape: percent fields with window identifiers directly on
|
|
557
|
+
* the data object (optionally nested under `quota`). Kept as a fallback so
|
|
558
|
+
* older responses keep rendering when the `limits` array is absent.
|
|
559
|
+
*/
|
|
560
|
+
function parseZaiQuotaLegacyFields(data: Record<string, unknown> | null): ProviderQuota | null {
|
|
561
|
+
if (!data) return null;
|
|
562
|
+
const quota: ProviderQuota = { updatedAt: Date.now() };
|
|
563
|
+
let windows = 0;
|
|
564
|
+
const percentAt = (key: string): number | undefined => {
|
|
565
|
+
const value = normalizePercent(data[key]);
|
|
566
|
+
if (value !== undefined) return value;
|
|
567
|
+
const nested = asRecord(data.quota);
|
|
568
|
+
return nested ? normalizePercent(nested[key]) : undefined;
|
|
569
|
+
};
|
|
570
|
+
const fiveHour = percentAt("fiveHourPercent") ?? percentAt("fiveHourUsage") ?? percentAt("fiveHourUsed");
|
|
571
|
+
const weekly = percentAt("weeklyPercent") ?? percentAt("weeklyUsage") ?? percentAt("weeklyUsed");
|
|
572
|
+
const monthly = percentAt("monthlyPercent") ?? percentAt("mcpPercent") ?? percentAt("monthlyMCPUsage");
|
|
573
|
+
if (fiveHour !== undefined) {
|
|
574
|
+
quota.fiveHourPercent = fiveHour;
|
|
575
|
+
windows += 1;
|
|
576
|
+
}
|
|
577
|
+
if (weekly !== undefined) {
|
|
578
|
+
quota.weeklyPercent = weekly;
|
|
579
|
+
windows += 1;
|
|
580
|
+
}
|
|
581
|
+
if (monthly !== undefined) {
|
|
582
|
+
quota.monthlyPercent = monthly;
|
|
583
|
+
windows += 1;
|
|
584
|
+
}
|
|
585
|
+
return windows > 0 ? quota : null;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Fetches the Z.AI GLM Coding Plan quota — on whichever region the provider
|
|
590
|
+
* points at (api.z.ai or open.bigmodel.cn). The `limits` array shape is
|
|
591
|
+
* preferred; older field-name payloads fall back to the legacy parser.
|
|
592
|
+
*
|
|
593
|
+
* Authentication differs by host (issue #1168). `api.z.ai` takes the API key as
|
|
594
|
+
* a Bearer token per Z.AI's API reference; `open.bigmodel.cn` expects the key
|
|
595
|
+
* directly in `Authorization` with no scheme prefix and answers a Bearer header
|
|
596
|
+
* with an auth error, which is why BigModel Coding Plan quota never rendered.
|
|
597
|
+
* The host is already canonicalized by `isCanonicalZaiBaseUrl` above and
|
|
598
|
+
* `redirect: "error"` stays set, so the bare key cannot travel to a lookalike
|
|
599
|
+
* host or follow a redirect off-origin.
|
|
600
|
+
*/
|
|
601
|
+
async function fetchZaiQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
|
|
602
|
+
const monitorHost = zaiQuotaMonitorHost(config.baseUrl);
|
|
603
|
+
if (!monitorHost) return null;
|
|
604
|
+
const apiKey = resolveProviderApiKey(config.apiKey)?.trim();
|
|
605
|
+
if (!apiKey) return null;
|
|
606
|
+
const authorization = monitorHost === ZAI_CN_BASE_URL ? apiKey : `Bearer ${apiKey}`;
|
|
607
|
+
const response = await fetch(`${monitorHost}/api/monitor/usage/quota/limit`, {
|
|
608
|
+
headers: { Accept: "application/json", Authorization: authorization },
|
|
609
|
+
redirect: "error",
|
|
610
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
611
|
+
});
|
|
612
|
+
if (!response.ok) {
|
|
613
|
+
return response.status >= 400 && response.status < 500 && response.status !== 408 && response.status !== 429
|
|
614
|
+
? TERMINAL_QUOTA_FAILURE
|
|
615
|
+
: null;
|
|
616
|
+
}
|
|
617
|
+
const body = asRecord(await readQuotaJson(response));
|
|
618
|
+
if (!body || body.success === false) return null;
|
|
619
|
+
const data = asRecord(body.data) ?? body;
|
|
620
|
+
if (Array.isArray(data?.limits)) {
|
|
621
|
+
const quota = parseZaiQuotaLimits(data);
|
|
622
|
+
// A well-formed `limits[]` we fully understood is authoritative even when it yields no
|
|
623
|
+
// model window — for example a plan reporting only the monthly MCP `TIME_LIMIT` row.
|
|
624
|
+
// Returning `null` here would preserve the previous token windows for up to 30 minutes
|
|
625
|
+
// and keep quota-aware routing acting on a report the provider has already superseded.
|
|
626
|
+
return quota
|
|
627
|
+
? keyReport(provider, "zai:quota-limit", quota, config, apiKey, quota)
|
|
628
|
+
: AUTHORITATIVE_EMPTY_QUOTA;
|
|
629
|
+
}
|
|
630
|
+
const legacy = parseZaiQuotaLegacyFields(data);
|
|
631
|
+
if (!legacy) return null;
|
|
632
|
+
// The legacy monthly figure also carries MCP usage; it is display evidence, not
|
|
633
|
+
// proof that model inference is unavailable. Modern TOKEN_LIMIT rows above are scoped.
|
|
634
|
+
const inferenceQuota = { ...legacy };
|
|
635
|
+
delete inferenceQuota.monthlyPercent;
|
|
636
|
+
delete inferenceQuota.monthlyResetAt;
|
|
637
|
+
return keyReport(provider, "zai:quota-limit", legacy, config, apiKey, inferenceQuota);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* MiniMax Token Plan `GET /v1/token_plan/remains` — the subscription's
|
|
642
|
+
* remaining quota as a countdown-time value (ms). The endpoint does not expose
|
|
643
|
+
* the plan's total duration, so no percentage is fabricated from a presumed
|
|
644
|
+
* window: the remaining time is reported as a duration-only window. When the
|
|
645
|
+
* API supplies a total (`total_time` / `plan_duration_ms`), a consumed share
|
|
646
|
+
* is derived from it. Region selects the host: `minimax` → www.minimax.io,
|
|
647
|
+
* `minimax-cn` → api.minimaxi.com.
|
|
648
|
+
*/
|
|
649
|
+
async function fetchMinimaxQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
|
|
650
|
+
if (!isCanonicalMinimaxBaseUrl(config.baseUrl)) return null;
|
|
651
|
+
const apiKey = resolveProviderApiKey(config.apiKey)?.trim();
|
|
652
|
+
if (!apiKey) return null;
|
|
653
|
+
const cnHost = normalizedBaseUrl(config.baseUrl)?.startsWith("https://api.minimaxi.com");
|
|
654
|
+
const remainsUrl = cnHost ? "https://api.minimaxi.com/v1/token_plan/remains" : MINIMAX_REMAINS_URL;
|
|
655
|
+
const response = await fetch(remainsUrl, {
|
|
656
|
+
headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
|
|
657
|
+
redirect: "error",
|
|
658
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
659
|
+
});
|
|
660
|
+
if (!response.ok) {
|
|
661
|
+
return response.status >= 400 && response.status < 500 && response.status !== 408 && response.status !== 429
|
|
662
|
+
? TERMINAL_QUOTA_FAILURE
|
|
663
|
+
: null;
|
|
664
|
+
}
|
|
665
|
+
const body = asRecord(await readQuotaJson(response));
|
|
666
|
+
if (!body || body.success === false) return null;
|
|
667
|
+
const data = asRecord(body.data) ?? body;
|
|
668
|
+
const remainsMs = toFiniteNumber(data.remains_time ?? data.remainsTime);
|
|
669
|
+
if (remainsMs === undefined || remainsMs < 0) return null;
|
|
670
|
+
const hours = Math.floor(remainsMs / 3_600_000);
|
|
671
|
+
const label = `Token Plan remaining (${hours}h)`;
|
|
672
|
+
// Only derive a consumed share when the API actually reports the plan total;
|
|
673
|
+
// a presumed window (e.g. 30 days) would fabricate utilization. A valid
|
|
674
|
+
// response that omits the total after a prior refresh had it is a DELIBERATE
|
|
675
|
+
// contract change — the old row must be dropped (terminal), not preserved as
|
|
676
|
+
// a transient last-good.
|
|
677
|
+
const totalMs = toFiniteNumber(data.total_time ?? data.plan_duration_ms ?? data.total_duration_ms);
|
|
678
|
+
if (totalMs === undefined || totalMs <= 0) return TERMINAL_QUOTA_FAILURE;
|
|
679
|
+
const consumed = Math.max(0, totalMs - remainsMs);
|
|
680
|
+
const percent = normalizePercent((consumed / totalMs) * 100);
|
|
681
|
+
if (percent === undefined) return null;
|
|
682
|
+
return report(provider, "minimax:token-plan-remains", {
|
|
683
|
+
customWindows: [{ label, percent }],
|
|
684
|
+
updatedAt: Date.now(),
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/**
|
|
689
|
+
* Moonshot/Kimi `GET /v1/users/me/balance` — the account's available balance
|
|
690
|
+
* (voucher + cash). Renders a single balance window against the sum of
|
|
691
|
+
* voucher + cash when positive (there is no per-window rate limit to meter).
|
|
692
|
+
*/
|
|
693
|
+
async function fetchMoonshotQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
|
|
694
|
+
if (!isCanonicalMoonshotBaseUrl(config.baseUrl)) return null;
|
|
695
|
+
const apiKey = resolveProviderApiKey(config.apiKey)?.trim();
|
|
696
|
+
if (!apiKey) return null;
|
|
697
|
+
const host = normalizedBaseUrl(config.baseUrl)?.startsWith("https://api.moonshot.cn") ? "https://api.moonshot.cn/v1" : MOONSHOT_BASE_URL;
|
|
698
|
+
const response = await fetch(`${host}/users/me/balance`, {
|
|
699
|
+
headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
|
|
700
|
+
redirect: "error",
|
|
701
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
702
|
+
});
|
|
703
|
+
if (!response.ok) {
|
|
704
|
+
return response.status >= 400 && response.status < 500 && response.status !== 408 && response.status !== 429
|
|
705
|
+
? TERMINAL_QUOTA_FAILURE
|
|
706
|
+
: null;
|
|
707
|
+
}
|
|
708
|
+
const body = asRecord(await readQuotaJson(response));
|
|
709
|
+
const data = asRecord(body?.data) ?? body;
|
|
710
|
+
if (!data) return null;
|
|
711
|
+
const available = toFiniteNumber(data.available_balance);
|
|
712
|
+
const voucher = toFiniteNumber(data.voucher_balance);
|
|
713
|
+
const cash = toFiniteNumber(data.cash_balance);
|
|
714
|
+
if (available === undefined || available < 0) return null;
|
|
715
|
+
// Moonshot exposes no per-window quota ceiling, only a balance — report it
|
|
716
|
+
// as a balance-only window (percent 0) rather than a fabricated utilization.
|
|
717
|
+
// Currency is host-scoped: China platform (api.moonshot.cn) bills in CNY;
|
|
718
|
+
// the international platform (api.moonshot.ai) bills in USD. Do not force
|
|
719
|
+
// either side into the other unit — the number is correct, only the unit
|
|
720
|
+
// must match the host.
|
|
721
|
+
const isChinaHost = host.startsWith("https://api.moonshot.cn");
|
|
722
|
+
const money = (n: number) => isChinaHost ? `¥${n.toFixed(2)}` : `$${n.toFixed(2)}`;
|
|
723
|
+
const unit = isChinaHost ? "CNY" : "USD";
|
|
724
|
+
const label = voucher !== undefined && cash !== undefined
|
|
725
|
+
? `Balance (${money(available)} ${unit} available, ${money(voucher)} voucher)`
|
|
726
|
+
: `Balance (${money(available)} ${unit} available)`;
|
|
727
|
+
return report(provider, "moonshot:balance", {
|
|
728
|
+
customWindows: [{ label, percent: 0 }],
|
|
729
|
+
updatedAt: Date.now(),
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* Venice `GET /api/v1/billing/balance` — DIEM (native credits) or USD balance.
|
|
735
|
+
* Shows the remaining balance; epoch allocation progress when present.
|
|
736
|
+
*/
|
|
737
|
+
async function fetchVeniceQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
|
|
738
|
+
if (!isCanonicalVeniceBaseUrl(config.baseUrl)) return null;
|
|
739
|
+
const apiKey = resolveProviderApiKey(config.apiKey)?.trim();
|
|
740
|
+
if (!apiKey) return null;
|
|
741
|
+
const response = await fetch(`${VENICE_BASE_URL}/billing/balance`, {
|
|
742
|
+
headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
|
|
743
|
+
redirect: "error",
|
|
744
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
745
|
+
});
|
|
746
|
+
if (!response.ok) {
|
|
747
|
+
return response.status >= 400 && response.status < 500 && response.status !== 408 && response.status !== 429
|
|
748
|
+
? TERMINAL_QUOTA_FAILURE
|
|
749
|
+
: null;
|
|
750
|
+
}
|
|
751
|
+
const body = asRecord(await readQuotaJson(response));
|
|
752
|
+
const data = asRecord(body?.data) ?? body;
|
|
753
|
+
if (!data) return null;
|
|
754
|
+
const diemBalance = toFiniteNumber(data.balance);
|
|
755
|
+
const usdBalance = toFiniteNumber(data.balance_usd);
|
|
756
|
+
const epochUsed = toFiniteNumber(data.diem_epoch_used);
|
|
757
|
+
const epochAllocated = toFiniteNumber(data.diem_epoch_allocated);
|
|
758
|
+
if (diemBalance === undefined && usdBalance === undefined) return null;
|
|
759
|
+
const label = diemBalance !== undefined
|
|
760
|
+
? `DIEM balance (${Math.round(diemBalance)})`
|
|
761
|
+
: `USD balance ($${usdBalance?.toFixed(2) ?? "?"})`;
|
|
762
|
+
if (epochAllocated !== undefined && epochAllocated > 0 && epochUsed !== undefined) {
|
|
763
|
+
const percent = normalizePercent((epochUsed / epochAllocated) * 100);
|
|
764
|
+
if (percent === undefined) return null;
|
|
765
|
+
return report(provider, "venice:billing-balance", {
|
|
766
|
+
customWindows: [{ label, percent }],
|
|
767
|
+
updatedAt: Date.now(),
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
return report(provider, "venice:billing-balance", {
|
|
771
|
+
customWindows: [{ label, percent: 0 }],
|
|
772
|
+
updatedAt: Date.now(),
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
/**
|
|
777
|
+
* Synthetic `GET /v2/quotas` — the known quota lanes (rolling 5-hour,
|
|
778
|
+
* weekly token, search-hourly) mapped onto the quota windows.
|
|
779
|
+
*/
|
|
780
|
+
async function fetchSyntheticQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
|
|
781
|
+
if (!isCanonicalSyntheticBaseUrl(config.baseUrl)) return null;
|
|
782
|
+
const apiKey = resolveProviderApiKey(config.apiKey)?.trim();
|
|
783
|
+
if (!apiKey) return null;
|
|
784
|
+
const response = await fetch(`${SYNTHETIC_BASE_URL}/quotas`, {
|
|
785
|
+
headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
|
|
786
|
+
redirect: "error",
|
|
787
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
788
|
+
});
|
|
789
|
+
if (!response.ok) {
|
|
790
|
+
return response.status >= 400 && response.status < 500 && response.status !== 408 && response.status !== 429
|
|
791
|
+
? TERMINAL_QUOTA_FAILURE
|
|
792
|
+
: null;
|
|
793
|
+
}
|
|
794
|
+
const body = asRecord(await readQuotaJson(response));
|
|
795
|
+
const data = asRecord(body?.data) ?? body;
|
|
796
|
+
const quota: ProviderQuota = { updatedAt: Date.now() };
|
|
797
|
+
let windows = 0;
|
|
798
|
+
const percentAt = (key: string): number | undefined => {
|
|
799
|
+
const value = normalizePercent(data?.[key]);
|
|
800
|
+
if (value !== undefined) return value;
|
|
801
|
+
const nested = asRecord(data?.quota) ?? asRecord(data?.quotas);
|
|
802
|
+
return nested ? normalizePercent(nested[key]) : undefined;
|
|
803
|
+
};
|
|
804
|
+
const fiveHour = percentAt("rollingFiveHourLimit");
|
|
805
|
+
const weekly = percentAt("weeklyTokenLimit");
|
|
806
|
+
if (fiveHour !== undefined) {
|
|
807
|
+
quota.fiveHourPercent = fiveHour;
|
|
808
|
+
windows += 1;
|
|
809
|
+
}
|
|
810
|
+
if (weekly !== undefined) {
|
|
811
|
+
quota.weeklyPercent = weekly;
|
|
812
|
+
windows += 1;
|
|
813
|
+
}
|
|
814
|
+
const search = asRecord(data?.search);
|
|
815
|
+
const searchHourly = search ? normalizePercent(search.hourly) : undefined;
|
|
816
|
+
if (searchHourly !== undefined) {
|
|
817
|
+
quota.customWindows = [...(quota.customWindows ?? []), { label: "Search hourly", percent: searchHourly }];
|
|
818
|
+
windows += 1;
|
|
819
|
+
}
|
|
820
|
+
const inferenceQuota = { ...quota };
|
|
821
|
+
delete inferenceQuota.customWindows; // search.hourly does not constrain model inference.
|
|
822
|
+
return windows > 0 ? keyReport(provider, "synthetic:quotas", quota, config, apiKey, inferenceQuota) : null;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
/**
|
|
826
|
+
* DeepInfra `GET /payment/checklist?compute_owed=true` — prepaid balance,
|
|
827
|
+
* recent spend, spending limit, and suspension state. Renders a balance
|
|
828
|
+
* window (prepaid funds are a negative `stripe_balance` → positive available).
|
|
829
|
+
*/
|
|
830
|
+
async function fetchDeepInfraQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
|
|
831
|
+
if (!isCanonicalDeepInfraBaseUrl(config.baseUrl)) return null;
|
|
832
|
+
const apiKey = resolveProviderApiKey(config.apiKey)?.trim();
|
|
833
|
+
if (!apiKey) return null;
|
|
834
|
+
const response = await fetch(`${DEEPINFRA_BASE_URL}/payment/checklist?compute_owed=true`, {
|
|
835
|
+
headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
|
|
836
|
+
redirect: "error",
|
|
837
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
838
|
+
});
|
|
839
|
+
if (!response.ok) {
|
|
840
|
+
return response.status >= 400 && response.status < 500 && response.status !== 408 && response.status !== 429
|
|
841
|
+
? TERMINAL_QUOTA_FAILURE
|
|
842
|
+
: null;
|
|
843
|
+
}
|
|
844
|
+
const body = asRecord(await readQuotaJson(response));
|
|
845
|
+
const data = asRecord(body?.data) ?? body;
|
|
846
|
+
if (!data) return null;
|
|
847
|
+
const stripeBalance = toFiniteNumber(data.stripe_balance);
|
|
848
|
+
const spendLimit = toFiniteNumber(data.spending_limit);
|
|
849
|
+
const total = toFiniteNumber(data.total_amount_due);
|
|
850
|
+
if (stripeBalance === undefined) return null;
|
|
851
|
+
// Prepaid funds are negative; a positive value is money owed.
|
|
852
|
+
const available = stripeBalance < 0 ? -stripeBalance : 0;
|
|
853
|
+
if (spendLimit !== undefined && spendLimit > 0) {
|
|
854
|
+
const spent = total !== undefined && total > 0 ? total : Math.max(0, spendLimit - available);
|
|
855
|
+
const percent = normalizePercent((spent / spendLimit) * 100);
|
|
856
|
+
if (percent === undefined) return null;
|
|
857
|
+
return report(provider, "deepinfra:billing-checklist", {
|
|
858
|
+
customWindows: [{ label: `Billing cycle spend ($${spent.toFixed(2)} of $${spendLimit.toFixed(2)})`, percent }],
|
|
859
|
+
updatedAt: Date.now(),
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
return report(provider, "deepinfra:billing-checklist", {
|
|
863
|
+
customWindows: [{ label: `Prepaid balance ($${available.toFixed(2)})`, percent: 0 }],
|
|
864
|
+
updatedAt: Date.now(),
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
/**
|
|
869
|
+
* Neuralwatt `GET /v1/quota` — subscription kWh usage (primary window) and
|
|
870
|
+
* prepaid USD credit balance (secondary).
|
|
871
|
+
*/
|
|
872
|
+
async function fetchNeuralwattQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
|
|
873
|
+
if (!isCanonicalNeuralwattBaseUrl(config.baseUrl)) return null;
|
|
874
|
+
const apiKey = resolveProviderApiKey(config.apiKey)?.trim();
|
|
875
|
+
if (!apiKey) return null;
|
|
876
|
+
const response = await fetch(`${NEURALWATT_BASE_URL}/quota`, {
|
|
877
|
+
headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
|
|
878
|
+
redirect: "error",
|
|
879
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
880
|
+
});
|
|
881
|
+
if (!response.ok) {
|
|
882
|
+
return response.status >= 400 && response.status < 500 && response.status !== 408 && response.status !== 429
|
|
883
|
+
? TERMINAL_QUOTA_FAILURE
|
|
884
|
+
: null;
|
|
885
|
+
}
|
|
886
|
+
const body = asRecord(await readQuotaJson(response));
|
|
887
|
+
const data = asRecord(body?.data) ?? body;
|
|
888
|
+
const quota: ProviderQuota = { updatedAt: Date.now() };
|
|
889
|
+
let windows = 0;
|
|
890
|
+
const subscription = asRecord(data?.subscription);
|
|
891
|
+
const kwhUsed = subscription ? toFiniteNumber(subscription.kwh_used) : undefined;
|
|
892
|
+
const kwhIncluded = subscription ? toFiniteNumber(subscription.kwh_included) : undefined;
|
|
893
|
+
if (kwhUsed !== undefined && kwhIncluded !== undefined && kwhIncluded > 0) {
|
|
894
|
+
const percent = normalizePercent((kwhUsed / kwhIncluded) * 100);
|
|
895
|
+
if (percent !== undefined) {
|
|
896
|
+
quota.fiveHourPercent = percent;
|
|
897
|
+
const periodEnd = subscription ? normalizeResetAt(subscription.current_period_end) : undefined;
|
|
898
|
+
if (periodEnd !== undefined) quota.fiveHourResetAt = periodEnd;
|
|
899
|
+
windows += 1;
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
const balance = asRecord(data?.balance);
|
|
903
|
+
const totalCredits = balance ? toFiniteNumber(balance.total_credits_usd) : undefined;
|
|
904
|
+
const remainingCredits = balance ? toFiniteNumber(balance.credits_remaining_usd) : undefined;
|
|
905
|
+
if (totalCredits !== undefined && totalCredits > 0 && remainingCredits !== undefined) {
|
|
906
|
+
// Utilization is CONSUMED credits, not the remaining share.
|
|
907
|
+
const used = Math.max(0, totalCredits - remainingCredits);
|
|
908
|
+
const percent = normalizePercent((used / totalCredits) * 100);
|
|
909
|
+
if (percent !== undefined) {
|
|
910
|
+
quota.customWindows = [...(quota.customWindows ?? []), { label: "Prepaid credits", percent }];
|
|
911
|
+
windows += 1;
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
return windows > 0 ? report(provider, "neuralwatt:quota", quota) : null;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
|
|
918
|
+
function normalizedBaseUrl(value: string): string | null {
|
|
919
|
+
try {
|
|
920
|
+
const url = new URL(value);
|
|
921
|
+
if (url.username || url.password || url.search || url.hash) return null;
|
|
922
|
+
return `${url.origin.toLowerCase()}${url.pathname.replace(/\/+$/, "")}`;
|
|
923
|
+
} catch {
|
|
924
|
+
return null;
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
function quotaResetAt(row: Record<string, unknown>): number | undefined {
|
|
929
|
+
return normalizeResetAt(row.resetTime ?? row.resetAt ?? row.reset_time ?? row.reset_at);
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
export function isCanonicalKimiCodeBaseUrl(baseUrl: string): boolean {
|
|
933
|
+
return normalizedBaseUrl(baseUrl) === KIMI_CODE_BASE_URL;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
export function isCanonicalCommandCodeBaseUrl(baseUrl: string): boolean {
|
|
937
|
+
const normalized = normalizedBaseUrl(baseUrl);
|
|
938
|
+
// OAuth preset points at the API root; the Provider-API preset at /provider/v1.
|
|
939
|
+
return normalized === COMMAND_CODE_BASE_URL || normalized === `${COMMAND_CODE_BASE_URL}/provider/v1`;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
/** Prefer the nested `data` shell when the outer object is only an envelope. */
|
|
943
|
+
function unwrapKimiQuotaPayload(value: unknown): Record<string, unknown> | null {
|
|
944
|
+
const body = asRecord(value);
|
|
945
|
+
if (!body) return null;
|
|
946
|
+
const nested = asRecord(body.data);
|
|
947
|
+
if (!nested) return body;
|
|
948
|
+
// A null/non-usable outer field is a placeholder, not data — an envelope like
|
|
949
|
+
// { usage: null, data: { usage: {...} } } must still unwrap to the nested payload.
|
|
950
|
+
const usable = (field: unknown): boolean => field !== undefined && field !== null;
|
|
951
|
+
const outerHasUsage = usable(body.usage) || usable(body.limits) || usable(body.totalQuota);
|
|
952
|
+
const nestedHasUsage = usable(nested.usage) || usable(nested.limits) || usable(nested.totalQuota);
|
|
953
|
+
return !outerHasUsage && nestedHasUsage ? nested : body;
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
function kimiLimitLabel(item: Record<string, unknown>, detail: Record<string, unknown>): string {
|
|
957
|
+
return [item.name, item.title, item.scope, detail.name, detail.title]
|
|
958
|
+
.filter((value): value is string => typeof value === "string")
|
|
959
|
+
.join(" ")
|
|
960
|
+
.toLowerCase();
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
function parseKimiQuotaRow(value: unknown, resetFallback?: Record<string, unknown>): { percent: number; resetAt?: number } | null {
|
|
964
|
+
const row = asRecord(value);
|
|
965
|
+
if (!row) return null;
|
|
966
|
+
const resetAt = quotaResetAt(row) ?? (resetFallback ? quotaResetAt(resetFallback) : undefined);
|
|
967
|
+
const limit = toFiniteNumber(row.limit);
|
|
968
|
+
if (limit !== undefined && limit > 0) {
|
|
969
|
+
let used = toFiniteNumber(row.used);
|
|
970
|
+
if (used === undefined) {
|
|
971
|
+
const remaining = toFiniteNumber(row.remaining);
|
|
972
|
+
if (remaining !== undefined) used = limit - remaining;
|
|
973
|
+
}
|
|
974
|
+
if (used !== undefined) {
|
|
975
|
+
const percent = normalizePercent((used / limit) * 100);
|
|
976
|
+
if (percent !== undefined) return { percent, ...(resetAt !== undefined ? { resetAt } : {}) };
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
// Some payloads expose utilisation directly when limit/used arithmetic is absent.
|
|
980
|
+
const direct = normalizePercent(row.utilization ?? row.percent ?? row.usedPercent ?? row.used_percent);
|
|
981
|
+
return direct === undefined ? null : { percent: direct, ...(resetAt !== undefined ? { resetAt } : {}) };
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
function isKimiFiveHourLimit(item: Record<string, unknown>, detail: Record<string, unknown>, window: Record<string, unknown>): boolean {
|
|
985
|
+
const duration = toFiniteNumber(window.duration ?? item.duration ?? detail.duration);
|
|
986
|
+
const unit = String(window.timeUnit ?? item.timeUnit ?? detail.timeUnit ?? "").toUpperCase();
|
|
987
|
+
if ((unit.includes("MINUTE") && duration === 300) || (unit.includes("HOUR") && duration === 5)) return true;
|
|
988
|
+
return /(^|\b)5\s*(?:h|hour)/.test(kimiLimitLabel(item, detail));
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
function isKimiWeeklyLimit(item: Record<string, unknown>, detail: Record<string, unknown>, window: Record<string, unknown>): boolean {
|
|
992
|
+
const duration = toFiniteNumber(window.duration ?? item.duration ?? detail.duration);
|
|
993
|
+
const unit = String(window.timeUnit ?? item.timeUnit ?? detail.timeUnit ?? "").toUpperCase();
|
|
994
|
+
if ((unit.includes("DAY") && duration === 7) || (unit.includes("HOUR") && duration === 168)) return true;
|
|
995
|
+
return /weekly|7\s*(?:d|day)/.test(kimiLimitLabel(item, detail));
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
function parseKimiQuotaPayload(value: unknown): ProviderQuota | null {
|
|
999
|
+
const body = unwrapKimiQuotaPayload(value);
|
|
1000
|
+
if (!body) return null;
|
|
1001
|
+
let weekly = parseKimiQuotaRow(body.usage);
|
|
1002
|
+
const total = parseKimiQuotaRow(body.totalQuota);
|
|
1003
|
+
let fiveHour: { percent: number; resetAt?: number } | null = null;
|
|
1004
|
+
if (Array.isArray(body.limits)) {
|
|
1005
|
+
for (const rawItem of body.limits) {
|
|
1006
|
+
const item = asRecord(rawItem);
|
|
1007
|
+
if (!item) continue;
|
|
1008
|
+
const detail = asRecord(item.detail) ?? item;
|
|
1009
|
+
const window = asRecord(item.window) ?? {};
|
|
1010
|
+
if (!fiveHour && isKimiFiveHourLimit(item, detail, window)) {
|
|
1011
|
+
fiveHour = parseKimiQuotaRow(detail, window);
|
|
1012
|
+
}
|
|
1013
|
+
if (!weekly && isKimiWeeklyLimit(item, detail, window)) {
|
|
1014
|
+
weekly = parseKimiQuotaRow(detail, window);
|
|
1015
|
+
}
|
|
1016
|
+
if (fiveHour && weekly) break;
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
const quota: ProviderQuota = {
|
|
1020
|
+
...(fiveHour ? {
|
|
1021
|
+
fiveHourPercent: fiveHour.percent,
|
|
1022
|
+
...(fiveHour.resetAt !== undefined ? { fiveHourResetAt: fiveHour.resetAt } : {}),
|
|
1023
|
+
} : {}),
|
|
1024
|
+
...(weekly ? {
|
|
1025
|
+
weeklyPercent: weekly.percent,
|
|
1026
|
+
...(weekly.resetAt !== undefined ? { weeklyResetAt: weekly.resetAt } : {}),
|
|
1027
|
+
} : {}),
|
|
1028
|
+
...(total ? { customWindows: [{ label: "Total subscription credits", percent: total.percent, ...(total.resetAt !== undefined ? { resetAt: total.resetAt } : {}) }] } : {}),
|
|
1029
|
+
updatedAt: Date.now(),
|
|
1030
|
+
};
|
|
1031
|
+
return hasQuotaRows(quota) ? quota : null;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
async function resolveKimiQuotaBearer(config: OcxProviderConfig, accountId?: string): Promise<string | null> {
|
|
1035
|
+
if (config.authMode === "oauth") {
|
|
1036
|
+
try {
|
|
1037
|
+
return accountId ? await getTokenForAccountQuotaProbe("kimi", accountId) : null;
|
|
1038
|
+
} catch {
|
|
1039
|
+
return null;
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
// ACTIVE key only: silently walking apiKeyPool when the primary env reference is
|
|
1043
|
+
// unresolved would render a quota bar for a DIFFERENT account than the one routing
|
|
1044
|
+
// requests — a wrong meter is worse than no meter.
|
|
1045
|
+
const primary = resolveProviderApiKey(config.apiKey)?.trim();
|
|
1046
|
+
return primary || null;
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
export async function fetchKimiQuota(provider: string, config: OcxProviderConfig, accessToken: string): Promise<ProviderQuotaReport | null> {
|
|
1050
|
+
// Never release credentials to a user-edited or lookalike provider host.
|
|
1051
|
+
if (!isCanonicalKimiCodeBaseUrl(config.baseUrl)) return null;
|
|
1052
|
+
if (!accessToken) return null;
|
|
1053
|
+
const response = await fetch(KIMI_CODE_USAGE_URL, {
|
|
1054
|
+
headers: { Accept: "application/json", Authorization: `Bearer ${accessToken}` },
|
|
1055
|
+
redirect: "error",
|
|
1056
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
1057
|
+
});
|
|
1058
|
+
if (!response.ok) return null;
|
|
1059
|
+
const quota = parseKimiQuotaPayload(await readQuotaJson(response));
|
|
1060
|
+
return quota ? keyReport(provider, "kimi:usages", quota, config, accessToken, quota) : null;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
/**
|
|
1064
|
+
* Command Code rolling window: `{ cap, used, resetAt }` off /alpha/billing/credits,
|
|
1065
|
+
* normalized to a percent with an optional reset timestamp.
|
|
1066
|
+
*/
|
|
1067
|
+
function parseCommandCodeWindow(value: unknown): { percent: number; resetAt?: number } | null {
|
|
1068
|
+
const row = asRecord(value);
|
|
1069
|
+
if (!row) return null;
|
|
1070
|
+
const cap = toFiniteNumber(row.cap);
|
|
1071
|
+
const used = toFiniteNumber(row.used);
|
|
1072
|
+
if (cap === undefined || used === undefined || cap <= 0 || used < 0) return null;
|
|
1073
|
+
const percent = normalizePercent((used / cap) * 100);
|
|
1074
|
+
if (percent === undefined) return null;
|
|
1075
|
+
const resetAt = quotaResetAt(row);
|
|
1076
|
+
return { percent, ...(resetAt !== undefined ? { resetAt } : {}) };
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
/** Soft-fail GET returning a parsed record, or null when unavailable. */
|
|
1080
|
+
async function fetchCommandCodeJson(url: string, bearer: string): Promise<Record<string, unknown> | null> {
|
|
1081
|
+
try {
|
|
1082
|
+
const response = await fetch(url, {
|
|
1083
|
+
headers: { Accept: "application/json", Authorization: `Bearer ${bearer}` },
|
|
1084
|
+
redirect: "error",
|
|
1085
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
1086
|
+
});
|
|
1087
|
+
if (!response.ok) return null;
|
|
1088
|
+
return asRecord(await readQuotaJson(response));
|
|
1089
|
+
} catch {
|
|
1090
|
+
return null;
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
/**
|
|
1095
|
+
* Soft-fail period spend (used) against the remaining credit pools → creditsUsd.
|
|
1096
|
+
* Period scoping: `since=<currentPeriodStart>` keeps spend aligned with the
|
|
1097
|
+
* pools' billing cycle, and `currentPeriodEnd` becomes expiresAt.
|
|
1098
|
+
*/
|
|
1099
|
+
async function fetchCommandCodeSpend(
|
|
1100
|
+
bearer: string,
|
|
1101
|
+
credits: Record<string, unknown> | null,
|
|
1102
|
+
orgQuery: string,
|
|
1103
|
+
): Promise<ProviderQuotaCreditsUsd | undefined> {
|
|
1104
|
+
if (!credits) return undefined;
|
|
1105
|
+
const subscriptionBody = await fetchCommandCodeJson(`${COMMAND_CODE_SUBSCRIPTIONS_URL}${orgQuery}`, bearer);
|
|
1106
|
+
const subscription = asRecord(subscriptionBody?.data) ?? subscriptionBody;
|
|
1107
|
+
const periodStart = typeof subscription?.currentPeriodStart === "string" ? subscription.currentPeriodStart.trim() : "";
|
|
1108
|
+
// Unscoped /usage/summary is lifetime spend; mixing it with current-cycle
|
|
1109
|
+
// remaining pools produces a wrong percent. Omit creditsUsd until a period exists.
|
|
1110
|
+
if (!periodStart) return undefined;
|
|
1111
|
+
const sinceQuery = `${orgQuery ? "&" : "?"}since=${encodeURIComponent(periodStart)}`;
|
|
1112
|
+
const expiresAt = normalizeResetAt(subscription?.currentPeriodEnd);
|
|
1113
|
+
const summaryBody = await fetchCommandCodeJson(`${COMMAND_CODE_USAGE_URL}${orgQuery}${sinceQuery}`, bearer);
|
|
1114
|
+
const summary = asRecord(summaryBody?.data) ?? summaryBody;
|
|
1115
|
+
const used = toFiniteNumber(summary?.totalCost) ?? toFiniteNumber(summary?.totalMonthlyCredits);
|
|
1116
|
+
if (used === undefined || used < 0) return undefined;
|
|
1117
|
+
const pools = [credits.monthlyCredits, credits.purchasedCredits, credits.freeCredits]
|
|
1118
|
+
.map(value => toFiniteNumber(value))
|
|
1119
|
+
.filter((value): value is number => value !== undefined);
|
|
1120
|
+
// Field presence is what separates a real balance from absent data: an exhausted
|
|
1121
|
+
// all-zero account still reports remaining=0, while no remaining-credit field at
|
|
1122
|
+
// all means there is nothing to meter.
|
|
1123
|
+
if (pools.length === 0) return undefined;
|
|
1124
|
+
const remaining = pools.reduce((sum, value) => sum + Math.max(0, value ?? 0), 0);
|
|
1125
|
+
const limit = used + remaining;
|
|
1126
|
+
const percent = normalizePercent(limit > 0 ? (used / limit) * 100 : 0);
|
|
1127
|
+
// Purchased credits roll over past the subscription period end, so an expiry is
|
|
1128
|
+
// only truthful when the aggregate contains no non-expiring purchased pool.
|
|
1129
|
+
const purchased = toFiniteNumber(credits.purchasedCredits) ?? 0;
|
|
1130
|
+
return percent === undefined
|
|
1131
|
+
? undefined
|
|
1132
|
+
: {
|
|
1133
|
+
used,
|
|
1134
|
+
limit,
|
|
1135
|
+
remaining,
|
|
1136
|
+
percent,
|
|
1137
|
+
...(expiresAt !== undefined && purchased <= 0 ? { expiresAt } : {}),
|
|
1138
|
+
};
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
/** OAuth access token or ACTIVE Provider-API key for the Command Code quota probe. */
|
|
1142
|
+
async function resolveCommandCodeQuotaBearer(config: OcxProviderConfig, accountId?: string): Promise<string | null> {
|
|
1143
|
+
if (config.authMode === "oauth") {
|
|
1144
|
+
try {
|
|
1145
|
+
return accountId ? await getTokenForAccountQuotaProbe("command-code", accountId) : null;
|
|
1146
|
+
} catch {
|
|
1147
|
+
return null;
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
// ACTIVE key only: a quota bar for a different account than the one routing
|
|
1151
|
+
// requests is a wrong meter, not a helpful one.
|
|
1152
|
+
return resolveProviderApiKey(config.apiKey)?.trim() || null;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
/**
|
|
1156
|
+
* Command Code `GET /alpha/billing/credits` — the same Bearer surface the CLI's
|
|
1157
|
+
* usage view uses (windowLimits.fiveHour / windowLimits.weekly), plus soft
|
|
1158
|
+
* whoami (team orgId scoping) and subscription-scoped spend for creditsUsd.
|
|
1159
|
+
*/
|
|
1160
|
+
export async function fetchCommandCodeQuota(provider: string, config: OcxProviderConfig, bearer: string): Promise<ProviderQuotaProbeResult> {
|
|
1161
|
+
// Never release credentials to a user-edited or lookalike provider host.
|
|
1162
|
+
if (!isCanonicalCommandCodeBaseUrl(config.baseUrl)) return null;
|
|
1163
|
+
if (!bearer) return null;
|
|
1164
|
+
const whoamiBody = await fetchCommandCodeJson(COMMAND_CODE_WHOAMI_URL, bearer);
|
|
1165
|
+
const whoami = asRecord(whoamiBody?.data) ?? whoamiBody;
|
|
1166
|
+
const org = asRecord(whoami?.org);
|
|
1167
|
+
const orgId = typeof org?.id === "string" && org.id.trim() ? org.id.trim() : null;
|
|
1168
|
+
const orgQuery = orgId ? `?orgId=${encodeURIComponent(orgId)}` : "";
|
|
1169
|
+
const response = await fetch(`${COMMAND_CODE_CREDITS_URL}${orgQuery}`, {
|
|
1170
|
+
headers: { Accept: "application/json", Authorization: `Bearer ${bearer}` },
|
|
1171
|
+
redirect: "error",
|
|
1172
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
1173
|
+
});
|
|
1174
|
+
if (!response.ok) {
|
|
1175
|
+
return response.status >= 400 && response.status < 500 && response.status !== 408 && response.status !== 429
|
|
1176
|
+
? TERMINAL_QUOTA_FAILURE
|
|
1177
|
+
: null;
|
|
1178
|
+
}
|
|
1179
|
+
const raw = asRecord(await readQuotaJson(response));
|
|
1180
|
+
const body = asRecord(raw?.data) ?? raw;
|
|
1181
|
+
const credits = asRecord(body?.credits);
|
|
1182
|
+
const limits = asRecord(body?.windowLimits);
|
|
1183
|
+
if (!credits && !limits) return null;
|
|
1184
|
+
const fiveHour = parseCommandCodeWindow(limits?.fiveHour);
|
|
1185
|
+
const weekly = parseCommandCodeWindow(limits?.weekly);
|
|
1186
|
+
const creditsUsd = await fetchCommandCodeSpend(bearer, credits, orgQuery);
|
|
1187
|
+
const quota: ProviderQuota = {
|
|
1188
|
+
...(fiveHour ? {
|
|
1189
|
+
fiveHourPercent: fiveHour.percent,
|
|
1190
|
+
...(fiveHour.resetAt !== undefined ? { fiveHourResetAt: fiveHour.resetAt } : {}),
|
|
1191
|
+
} : {}),
|
|
1192
|
+
...(weekly ? {
|
|
1193
|
+
weeklyPercent: weekly.percent,
|
|
1194
|
+
...(weekly.resetAt !== undefined ? { weeklyResetAt: weekly.resetAt } : {}),
|
|
1195
|
+
} : {}),
|
|
1196
|
+
...(creditsUsd ? { creditsUsd } : {}),
|
|
1197
|
+
updatedAt: Date.now(),
|
|
1198
|
+
};
|
|
1199
|
+
// Rolling windows and the credit balance both gate inference on this bearer.
|
|
1200
|
+
return keyReport(provider, "command-code:credits", quota, config, bearer, quota);
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
|
|
1204
|
+
type KeyQuotaReader = (name: string, provider: OcxProviderConfig) => Promise<ProviderQuotaProbeResult>;
|
|
1205
|
+
|
|
1206
|
+
/** Same selector drives cheap capabilities and uncached reads; never resolves credentials. */
|
|
1207
|
+
export function keyQuotaReaderForProvider(name: string, provider: OcxProviderConfig): KeyQuotaReader | null {
|
|
1208
|
+
if (provider.disabled === true || (provider.authMode ?? "key") !== "key") return null;
|
|
1209
|
+
if (isCanonicalKimiCodeBaseUrl(provider.baseUrl)) {
|
|
1210
|
+
return async (id, config) => {
|
|
1211
|
+
const bearer = await resolveKimiQuotaBearer(config);
|
|
1212
|
+
return bearer ? fetchKimiQuota(id, config, bearer) : null;
|
|
1213
|
+
};
|
|
1214
|
+
}
|
|
1215
|
+
if (name === "commandcode" && isCanonicalCommandCodeBaseUrl(provider.baseUrl)) {
|
|
1216
|
+
return async (id, config) => {
|
|
1217
|
+
const bearer = await resolveCommandCodeQuotaBearer(config);
|
|
1218
|
+
return bearer ? fetchCommandCodeQuota(id, config, bearer) : null;
|
|
1219
|
+
};
|
|
1220
|
+
}
|
|
1221
|
+
if (registryEntryForProviderDestination(provider)?.id === "opencode-go") return fetchOpenCodeGoQuota;
|
|
1222
|
+
if (isCanonicalA6apiBaseUrl(provider.baseUrl)) return fetchA6apiQuota;
|
|
1223
|
+
if (name === "openrouter" && isCanonicalOpenRouterBaseUrl(provider.baseUrl)) return fetchOpenRouterQuota;
|
|
1224
|
+
if (name === "deepseek" && isCanonicalDeepSeekBaseUrl(provider.baseUrl)) return fetchDeepSeekQuota;
|
|
1225
|
+
if (name === "cline-pass" && isCanonicalClineBaseUrl(provider.baseUrl)) return fetchClineQuota;
|
|
1226
|
+
if (isCanonicalOllamaCloudBaseUrl(provider.baseUrl ?? getProviderRegistryEntry(name)?.baseUrl)) return fetchOllamaCloudQuota;
|
|
1227
|
+
// #4201: the Responses preset is the same domestic GLM Coding Plan subscription on the OpenAI
|
|
1228
|
+
// Responses wire, so it reads the same monitor endpoint. Eligibility stays a name list AND the
|
|
1229
|
+
// canonical-URL guard: the guard is what keeps BigModel's bare-key Authorization from reaching a
|
|
1230
|
+
// lookalike host, so a same-named custom destination still dispatches nothing.
|
|
1231
|
+
if (["zai", "glm", "glm-cn", "zhipu-bigmodel-coding", "zhipu-bigmodel-responses"].includes(name) && isCanonicalZaiBaseUrl(provider.baseUrl)) return fetchZaiQuota;
|
|
1232
|
+
if (["minimax", "minimax-cn"].includes(name) && isCanonicalMinimaxBaseUrl(provider.baseUrl)) return fetchMinimaxQuota;
|
|
1233
|
+
if (name === "moonshot" && isCanonicalMoonshotBaseUrl(provider.baseUrl)) return fetchMoonshotQuota;
|
|
1234
|
+
if (name === "venice" && isCanonicalVeniceBaseUrl(provider.baseUrl)) return fetchVeniceQuota;
|
|
1235
|
+
if (name === "synthetic" && isCanonicalSyntheticBaseUrl(provider.baseUrl)) return fetchSyntheticQuota;
|
|
1236
|
+
if (name === "deepinfra" && isCanonicalDeepInfraBaseUrl(provider.baseUrl)) return fetchDeepInfraQuota;
|
|
1237
|
+
if (name === "neuralwatt" && isCanonicalNeuralwattBaseUrl(provider.baseUrl)) return fetchNeuralwattQuota;
|
|
1238
|
+
return null;
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
export function providerApiKeyQuotaMode(name: string, provider: OcxProviderConfig): AccountQuotaMode {
|
|
1242
|
+
return keyQuotaReaderForProvider(name, provider) ? "probe" : "unsupported";
|
|
1243
|
+
}
|