@narumitw/pi-usage 0.60.3 → 0.60.6
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/README.md +42 -321
- package/dist/index.ts +106 -18
- package/dist/index.ts.map +4 -4
- package/docs/operations.md +31 -0
- package/docs/providers.md +271 -0
- package/package.json +6 -5
- package/src/core.ts +4 -0
- package/src/providers/zai-errors.ts +94 -0
- package/src/providers/zai.ts +5 -4
- package/src/query.ts +8 -0
- package/src/types.ts +6 -0
- package/src/usage.ts +1 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Usage queries and reset redemption
|
|
2
|
+
|
|
3
|
+
[Back to README](../README.md#-commands)
|
|
4
|
+
|
|
5
|
+
## Choose a provider target
|
|
6
|
+
|
|
7
|
+
A target is the provider-owned account, organization, project, team, or workspace used for one usage query.
|
|
8
|
+
Providers without target discovery query immediately; a single returned target is selected automatically without saving settings.
|
|
9
|
+
When several targets exist, `/usage` remembers an explicit selection by provider and reuses it only while it remains in a fresh listing.
|
|
10
|
+
A missing remembered target reports **Selection required** instead of querying another target silently.
|
|
11
|
+
Use **Select <target>…** for the current provider or **Change <target>…** for a ready current or individually viewed provider.
|
|
12
|
+
|
|
13
|
+
Viewing another provider may open one target prompt after that provider is queried lazily.
|
|
14
|
+
Cancelling changes nothing.
|
|
15
|
+
Authentication and target membership are revalidated before saving an explicit selection, then resolved again before billing is queried.
|
|
16
|
+
**View all configured providers…** never opens nested target prompts; view an unresolved provider individually to select its target.
|
|
17
|
+
Background refresh also remains non-interactive and reports `selection required` until `/usage` completes the choice.
|
|
18
|
+
Fireworks accounts are the first implementation of this provider-neutral flow.
|
|
19
|
+
|
|
20
|
+
For credentials, endpoints, and provider-specific meanings, see the [provider reference](./providers.md).
|
|
21
|
+
For selection storage and migration, see [Settings](../README.md#-settings).
|
|
22
|
+
|
|
23
|
+
## Redeem a Codex usage reset
|
|
24
|
+
|
|
25
|
+
For the current OpenAI Codex OAuth account, **Redeem usage limit reset…** checks fresh earned-reset details, lets you choose a reset, and previews its exact effect before confirmation.
|
|
26
|
+
Custom or proxy origins are rejected before mutation.
|
|
27
|
+
**No, go back** is the safe default, and cancellation before confirmation sends no mutation.
|
|
28
|
+
|
|
29
|
+
After confirmation, the reset cannot be cancelled from its progress view; session replacement or shutdown still aborts owned work.
|
|
30
|
+
A transport failure offers **Try again** with the same redemption request ID so the backend can handle an uncertain retry idempotently.
|
|
31
|
+
Successful, already-completed, not-needed, and no-credit outcomes are reported separately, then usage and the statusline refresh for the still-current account.
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
# Pi Usage provider reference
|
|
2
|
+
|
|
3
|
+
[Back to README](../README.md)
|
|
4
|
+
|
|
5
|
+
This reference preserves each provider's endpoint, authentication boundary, billing semantics, and contract evidence.
|
|
6
|
+
The README contains the capability overview and shared security requirements.
|
|
7
|
+
|
|
8
|
+
- [OpenAI Codex](#openai-codex)
|
|
9
|
+
- [Kimi For Coding](#kimi-for-coding)
|
|
10
|
+
- [Moonshot AI](#moonshot-ai-api-balance)
|
|
11
|
+
- [MiniMax](#minimax-token-plan-and-api-balance)
|
|
12
|
+
- [GitHub Copilot](#github-copilot)
|
|
13
|
+
- [OpenRouter](#openrouter)
|
|
14
|
+
- [DeepSeek](#deepseek-api-balance)
|
|
15
|
+
- [Fireworks](#fireworks-api-spend)
|
|
16
|
+
- [Vercel AI Gateway](#vercel-ai-gateway-credits)
|
|
17
|
+
- [Baseten](#baseten-model-apis-spend)
|
|
18
|
+
- [OpenCode Go](#opencode-go-zen)
|
|
19
|
+
- [xAI](#xai-consumer-subscriptions)
|
|
20
|
+
- [Z.AI](#zai-glm-coding-plan)
|
|
21
|
+
|
|
22
|
+
## 📋 Provider semantics
|
|
23
|
+
|
|
24
|
+
### OpenAI Codex
|
|
25
|
+
|
|
26
|
+
- Provider ID: `openai-codex`
|
|
27
|
+
- Semantics: ChatGPT consumer subscription limits
|
|
28
|
+
- Source: the Codex usage and earned-reset endpoints using Pi's resolved runtime authorization
|
|
29
|
+
- Displayed data: returned duration-based windows, resets, credits, earned usage-limit resets, and additional model buckets
|
|
30
|
+
- Reset mutation: `POST /wham/rate-limit-reset-credits/consume` with a unique redemption request ID and, when available, the selected opaque credit ID
|
|
31
|
+
- Statusline examples: `codex 59% ↻ 2h30m 61% ↻ 2d15m`, `codex fast 59% ↻ 2h30m`, or `codex spark 100% ↻ 2h30m`. Set `codexStatusResetCountdown` to `false` for the legacy `5h` and `wk` labels.
|
|
32
|
+
|
|
33
|
+
The statusline selects a returned bucket that matches the current Codex model when one is available.
|
|
34
|
+
Unlike `pi-codex-usage`, this successor intentionally has no Codex CLI fallback because the CLI may be logged into a different account than Pi's active runtime account.
|
|
35
|
+
|
|
36
|
+
Reset redemption is available only when Codex is the current provider.
|
|
37
|
+
Pi's freshly resolved access token must exactly match an OAuth credential from Pi's stored login or a compatible credential source.
|
|
38
|
+
`pi-usage` forwards only the bearer authorization and matching `chatgpt-account-id` to the official ChatGPT origin.
|
|
39
|
+
API-key credentials, configured-but-not-current Codex accounts, account changes during the flow, and custom/proxy origins fail before mutation.
|
|
40
|
+
Backend-provided titles and descriptions are sanitized for terminal display.
|
|
41
|
+
Opaque credit and account IDs are never shown or persisted by the extension.
|
|
42
|
+
|
|
43
|
+
### Kimi For Coding
|
|
44
|
+
|
|
45
|
+
- Provider ID: `kimi-coding`
|
|
46
|
+
- Semantics: Kimi Coding Plan request windows plus a separate Extra Usage booster wallet
|
|
47
|
+
- Source: `GET https://api.kimi.com/coding/v1/usages` using Pi's freshly resolved runtime Bearer credential
|
|
48
|
+
- Displayed plan data: the weekly summary, returned sub-windows, used and remaining request percentages, and valid reset times
|
|
49
|
+
- Displayed wallet data: balance, monthly spend, and monthly charge limit
|
|
50
|
+
- Statusline examples: `kimi 99% 5h 96% wk` or `kimi 95% 1d`
|
|
51
|
+
|
|
52
|
+
Both Pi API-key credentials and Pi OAuth credentials are accepted because current Pi resolves each form as Bearer authorization for the same official Kimi inference origin.
|
|
53
|
+
The extension queries the fixed usage endpoint only when both the selected model origin and the effective resolved-auth origin are `https://api.kimi.com`.
|
|
54
|
+
Custom and proxy origins fail before network access, redirects are rejected, and the credential is never sent to an override from Kimi Code's environment-specific development path.
|
|
55
|
+
|
|
56
|
+
Plan buckets remain integer request counts and are rendered with their source-defined windows.
|
|
57
|
+
Unknown units, duplicate windows, missing counts, invalid timestamps, and malformed rows remain unavailable rather than receiving guessed semantics.
|
|
58
|
+
Booster-wallet `amount` and `amountLeft` values use Kimi's first-party conversion of 1,000,000 fixed-point units per cent, while monthly values already arrive in cents.
|
|
59
|
+
Wallet values retain their currency and stay separate from plan requests and percentages in reports and the statusline.
|
|
60
|
+
Wallet fields remain unavailable unless the response supplies one consistent currency; missing monthly values are omitted, and an enabled zero cap is shown as zero.
|
|
61
|
+
|
|
62
|
+
The contract was revalidated on 2026-08-27 against [Pi `c49906ec77788625aacbdc53ebca6fbe65bd20f5`](https://github.com/earendil-works/pi/tree/c49906ec77788625aacbdc53ebca6fbe65bd20f5), including [`kimi-coding.ts`](https://github.com/earendil-works/pi/blob/c49906ec77788625aacbdc53ebca6fbe65bd20f5/packages/ai/src/providers/kimi-coding.ts) and [`auth/oauth/kimi-coding.ts`](https://github.com/earendil-works/pi/blob/c49906ec77788625aacbdc53ebca6fbe65bd20f5/packages/ai/src/auth/oauth/kimi-coding.ts).
|
|
63
|
+
It was also revalidated against [Kimi Code `676e4d82240855044fe809fea89ce1dbe8e512cf`](https://github.com/MoonshotAI/kimi-code/tree/676e4d82240855044fe809fea89ce1dbe8e512cf), including [`managed-usage.ts`](https://github.com/MoonshotAI/kimi-code/blob/676e4d82240855044fe809fea89ce1dbe8e512cf/packages/oauth/src/managed-usage.ts) and its [tests](https://github.com/MoonshotAI/kimi-code/blob/676e4d82240855044fe809fea89ce1dbe8e512cf/packages/oauth/test/managed-usage.test.ts).
|
|
64
|
+
The pinned Pi source at `e86823096c5bad39e1ca282ec24bc5eb9bec745b` has no changes in either reviewed Kimi file at the selected revision.
|
|
65
|
+
The pinned Kimi managed-usage source at `cd7c97b377a77f7ae1b9d541cafe314e986ec074` is an ancestor of that selected revision and has no changes in the reviewed source or tests.
|
|
66
|
+
|
|
67
|
+
### Moonshot AI API balance
|
|
68
|
+
|
|
69
|
+
- Provider IDs: `moonshotai` and `moonshotai-cn`
|
|
70
|
+
- Semantics: current API account balance, not Kimi For Coding subscription usage
|
|
71
|
+
- Global source: `GET https://api.moonshot.ai/v1/users/me/balance`
|
|
72
|
+
- China source: `GET https://api.moonshot.cn/v1/users/me/balance`
|
|
73
|
+
- Displayed data: available, voucher, and cash balance in USD for Global or CNY for China
|
|
74
|
+
- Statusline examples: `moonshot USD 49.58894` or `moonshot CNY 49.58894`
|
|
75
|
+
|
|
76
|
+
Each endpoint uses Pi's resolved inference Bearer key for the matching region.
|
|
77
|
+
Pi maps both built-in providers to `MOONSHOT_API_KEY`, so that shared environment credential is eligible only for the currently selected region.
|
|
78
|
+
Querying the sibling region requires a provider-specific stored, runtime, or `models.json` credential.
|
|
79
|
+
The extension rejects custom, proxy, and cross-region origins before network access and refuses redirects.
|
|
80
|
+
Available and voucher balances must be nonnegative, while cash balance may be negative when the account owes money.
|
|
81
|
+
The endpoint does not provide historical spend, token totals, quota windows, or reset times.
|
|
82
|
+
These API-platform balances are independent from the `kimi-coding` subscription and booster wallet.
|
|
83
|
+
|
|
84
|
+
The contracts were verified on 2026-08-30 against the official [Global balance reference](https://platform.kimi.ai/docs/api/balance), [China balance reference](https://platform.moonshot.cn/docs/api/balance), and first-party [`MoonshotAI-Cookbook` balance client and DTO](https://github.com/MoonshotAI/MoonshotAI-Cookbook/tree/25a9e46d2391dd4817d28ab980dac69eb59b582c/examples/golang_demo).
|
|
85
|
+
|
|
86
|
+
### MiniMax Token Plan and API balance
|
|
87
|
+
|
|
88
|
+
- Provider IDs: `minimax` and `minimax-cn`
|
|
89
|
+
- Token Plan source: `GET {region-api-root}/v1/token_plan/remains`
|
|
90
|
+
- Pay-as-you-go source: `GET {region-api-root}/account/query_balance`
|
|
91
|
+
- Region API roots: `https://api.minimax.io` and `https://api.minimaxi.com`
|
|
92
|
+
- Statusline examples: `minimax 15% 5h 80% wk` or `minimax USD 98.00001`
|
|
93
|
+
|
|
94
|
+
Pi's resolved MiniMax API key selects exactly one endpoint before network access.
|
|
95
|
+
Keys with the first-party `sk-api-` prefix query pay-as-you-go balance; other MiniMax API keys query Token Plan quota.
|
|
96
|
+
The extension never probes both endpoints with one credential.
|
|
97
|
+
Token Plan reports preserve provider rows, rolling and weekly windows, counts, reset times, unlimited status, and first-party handling for legacy versus current `*_usage_count` semantics.
|
|
98
|
+
Pay-as-you-go reports keep available, cash, voucher, credit, and owed amounts separate in USD for Global or CNY for China.
|
|
99
|
+
Custom, proxy, and cross-region origins fail before network access, and redirects are rejected.
|
|
100
|
+
|
|
101
|
+
The contract was verified on 2026-08-30 against MiniMax's [Token Plan FAQ](https://platform.minimax.io/docs/token-plan/faq#how-to-check-token-plan-usage) and the first-party [`MiniMax-AI/cli`](https://github.com/MiniMax-AI/cli/tree/b78eccea80a0f9692e186d98906cff26931464f3), including endpoint selection, response types, quota normalization, and SDK tests.
|
|
102
|
+
|
|
103
|
+
### GitHub Copilot
|
|
104
|
+
|
|
105
|
+
- Provider ID: `github-copilot`
|
|
106
|
+
- Semantics: the allowance reported for the active Copilot plan
|
|
107
|
+
- Allowance labels: AI credits for usage-based billing, premium requests for legacy annual billing, or chat requests for Copilot Free
|
|
108
|
+
- Source: GitHub's undocumented `GET /copilot_internal/user` endpoint
|
|
109
|
+
- Displayed data: entitlement, remaining allowance, percentage, reset time, plan, and any additional usage beyond the included allowance
|
|
110
|
+
- Statusline examples: `copilot credits 1200/1500 80%`, `copilot 245/300 82%`, or `copilot chat 40/50 80%`
|
|
111
|
+
|
|
112
|
+
GitHub's quota endpoint requires the original GitHub OAuth token rather than the short-lived Copilot inference token exposed by runtime auth.
|
|
113
|
+
`pi-usage` supports Copilot accounts created through Pi's `/login` flow and named accounts offered by a compatible `oauth:credential-source:v1` owner.
|
|
114
|
+
It uses a candidate only when its short-lived access token exactly matches the freshly resolved active runtime credential.
|
|
115
|
+
Duplicate equivalent candidates are harmless, while conflicting matches fail closed without choosing by extension load order.
|
|
116
|
+
API-key credentials, account mismatches, GitHub Enterprise accounts, and proxy/custom provider origins fail closed.
|
|
117
|
+
The detailed report follows the endpoint's `token_based_billing` marker so AI credits are not mislabeled as legacy premium requests.
|
|
118
|
+
It reports overage without treating a negative included balance as malformed.
|
|
119
|
+
|
|
120
|
+
### OpenRouter
|
|
121
|
+
|
|
122
|
+
- Provider ID: `openrouter`
|
|
123
|
+
- Semantics: API-key spend and per-key credit limits—not consumer subscription quota
|
|
124
|
+
- Source: OpenRouter's documented [`GET /api/v1/key`](https://openrouter.ai/docs/api/api-reference/api-keys/get-current-api-key) endpoint using Pi's resolved inference API key
|
|
125
|
+
- Displayed data: key label when safely returned, optional per-key limit and remaining amount, reset period, and daily/weekly/monthly/all-time spend
|
|
126
|
+
- Statusline examples: `openrouter $74.50 left` or `openrouter $25.50 used`
|
|
127
|
+
|
|
128
|
+
The extension does not call OpenRouter's account-level `/credits` endpoint because that operation requires a separate management key.
|
|
129
|
+
OpenRouter documents the distinction between credit and rate limits in its [API limits guide](https://openrouter.ai/docs/api_reference/limits).
|
|
130
|
+
|
|
131
|
+
### DeepSeek API balance
|
|
132
|
+
|
|
133
|
+
- Provider ID: `deepseek`
|
|
134
|
+
- Semantics: current API account balance, not historical usage or quota
|
|
135
|
+
- Source: documented `GET https://api.deepseek.com/user/balance` using Pi's freshly resolved runtime API key
|
|
136
|
+
- Displayed data: whether API calls are available plus separate total, granted, and topped-up balances for each returned CNY or USD currency
|
|
137
|
+
- Statusline examples: `deepseek CNY 110.00` or `deepseek CNY 110.00 · USD 20.00`
|
|
138
|
+
|
|
139
|
+
The extension queries the fixed balance endpoint only when the selected model origin is `https://api.deepseek.com` and any resolved-auth origin override, when present, has the same official origin.
|
|
140
|
+
Pi's built-in DeepSeek API-key resolver does not attach a redundant auth origin, so the validated model origin remains authoritative when no override exists.
|
|
141
|
+
Custom and proxy origins fail before network access, redirects are rejected, and only the resolved Bearer credential is forwarded from Pi's runtime auth.
|
|
142
|
+
Monetary decimal strings remain exact from the response through display.
|
|
143
|
+
CNY and USD stay separate and are never converted or added together.
|
|
144
|
+
The balance endpoint does not provide historical spend, request windows, reset times, or aggregate token usage, so `pi-usage` does not claim those DeepSeek capabilities.
|
|
145
|
+
|
|
146
|
+
The contract was verified on 2026-08-28 against [DeepSeek's Get User Balance documentation](https://api-docs.deepseek.com/api/get-user-balance) and Pi's [`deepseek.ts`](https://github.com/earendil-works/pi/blob/c49906ec77788625aacbdc53ebca6fbe65bd20f5/packages/ai/src/providers/deepseek.ts) at `c49906ec77788625aacbdc53ebca6fbe65bd20f5`.
|
|
147
|
+
DeepSeek Harness `cd5ef8148158c3a752a658978873241fdf8e2bbc` reports only per-request model token usage and does not provide account balance data.
|
|
148
|
+
|
|
149
|
+
### Fireworks API spend
|
|
150
|
+
|
|
151
|
+
- Provider ID: `fireworks`
|
|
152
|
+
- Semantics: rated 30-day account spend, not credit balance or spend-cap quota
|
|
153
|
+
- Source: documented `GET https://api.fireworks.ai/v1/accounts` account discovery and `GET https://api.fireworks.ai/v1/accounts/{account_id}/billing/summary` rated costs using Pi's resolved inference API key
|
|
154
|
+
- Displayed data: exact rated spend per currency with serverless, dedicated-deployment, and training subtotals for the trailing 30 days
|
|
155
|
+
- Statusline example: `fireworks USD 12.345678901`
|
|
156
|
+
|
|
157
|
+
The extension queries the fixed endpoints only when the selected model origin is `https://api.fireworks.ai` and any resolved-auth origin override, when present, has the same official origin.
|
|
158
|
+
The account slug is discovered through the documented account listing.
|
|
159
|
+
One visible account is selected automatically; several visible accounts use the remembered selection or ask through `/usage`, and a disappeared selection returns **Selection required** without a billing request.
|
|
160
|
+
Monetary `units` and `nanos` values are summed exactly with integer arithmetic and stay exact through display.
|
|
161
|
+
Fireworks does not expose credit balance, spend caps, per-window quota, or reset times through its API, so `pi-usage` does not claim those Fireworks capabilities; the web console remains the authoritative balance source.
|
|
162
|
+
Rated line items may differ from the final invoice once credits or adjustments are applied.
|
|
163
|
+
|
|
164
|
+
The contract was verified on 2026-07-31 against Fireworks' [Usage & Cost Breakdown](https://docs.fireworks.ai/accounts/exporting-usage-and-costs), [Get billing summary](https://docs.fireworks.ai/api-reference/get-billing-summary), and [List Accounts](https://docs.fireworks.ai/api-reference/list-accounts) API references.
|
|
165
|
+
|
|
166
|
+
### Vercel AI Gateway credits
|
|
167
|
+
|
|
168
|
+
- Provider ID: `vercel-ai-gateway`
|
|
169
|
+
- Semantics: current team credit balance and lifetime spend, not rate-limit quota
|
|
170
|
+
- Source: documented `GET https://ai-gateway.vercel.sh/v1/credits` using Pi's resolved AI Gateway API key
|
|
171
|
+
- Displayed data: exact decimal-string credit balance and lifetime spend in USD
|
|
172
|
+
- Statusline example: `vercel USD 95.50 left`
|
|
173
|
+
|
|
174
|
+
The extension queries the fixed endpoint only when the selected model origin and any resolved-auth origin are `https://ai-gateway.vercel.sh`.
|
|
175
|
+
Custom and proxy origins fail before network access, redirects are rejected, and only the resolved Bearer credential is forwarded.
|
|
176
|
+
The credits endpoint does not provide reset times, request-rate counters, or date-window usage, so `pi-usage` does not claim those capabilities.
|
|
177
|
+
Vercel's separate Custom Reporting API is limited to eligible paid plans and is intentionally outside this first integration.
|
|
178
|
+
|
|
179
|
+
The contract was verified on 2026-08-30 against Vercel's [REST API Reference](https://vercel.com/docs/ai-gateway/sdks-and-apis/rest-api#check-credit-balance) and the first-party [`vercel/ai` Gateway implementation](https://github.com/vercel/ai/blob/69428b1f8b037e4d118fb4853428d5c4e620493c/packages/gateway/src/gateway-fetch-metadata.ts).
|
|
180
|
+
|
|
181
|
+
### Baseten Model APIs spend
|
|
182
|
+
|
|
183
|
+
- Provider ID: `baseten`
|
|
184
|
+
- Semantics: organization-wide Model APIs spend, not per-key quota or account balance
|
|
185
|
+
- Source: `GET https://api.baseten.co/v1/billing/usage_summary` using Pi's resolved Baseten API key
|
|
186
|
+
- Displayed data: trailing 30-day gross usage, credits used, and net subtotal in USD
|
|
187
|
+
- Statusline example: `baseten USD 166.15 net`
|
|
188
|
+
|
|
189
|
+
The extension intentionally ignores Dedicated deployment and Training categories because they do not represent Pi's Model APIs provider usage.
|
|
190
|
+
The query window is a precise trailing 30 days and stays below the endpoint's 31-day maximum.
|
|
191
|
+
The fixed Management API endpoint is queried only for an official `https://inference.baseten.co` model and an official resolved-auth origin.
|
|
192
|
+
Custom and proxy origins fail before network access, redirects are rejected, and only the resolved Bearer credential is forwarded.
|
|
193
|
+
An empty `model_apis_usage` result is reported as no Model APIs usage rather than zero account-wide spend.
|
|
194
|
+
|
|
195
|
+
The contract was verified on 2026-08-30 against Baseten's [Billing and usage](https://docs.baseten.co/organization/billing#view-usage), [Model APIs usage](https://docs.baseten.co/inference/model-apis/pricing-and-limits#usage), first-party [`baseten-go` Management OpenAPI](https://github.com/basetenlabs/baseten-go/blob/f028e27beb4bde106d984833313c055ddd6fefa4/internal/tools/apigen/specs/management.json), and [`baseten-cli` billing behavior](https://github.com/basetenlabs/baseten-cli/blob/e3d002b465f49a7295ea44b5988dbfeb8197896d/internal/cmd/command.org.go).
|
|
196
|
+
|
|
197
|
+
### OpenCode Go (Zen)
|
|
198
|
+
|
|
199
|
+
- Provider ID: `opencode-go`
|
|
200
|
+
- Semantics: OpenCode Zen plan usage windows—rolling, weekly, and monthly
|
|
201
|
+
- Source: `GET https://opencode.ai/zen/go/v1/usage` using Pi's resolved inference API key
|
|
202
|
+
- Displayed data: used percentage and reset time for each window
|
|
203
|
+
- Status handling: `rate-limited` windows remain visible, while unknown statuses become unavailable notes
|
|
204
|
+
- Statusline examples: `zen 0% r 4% w 2% m`
|
|
205
|
+
|
|
206
|
+
The fixed endpoint is queried only when the OpenCode Go model uses the official `https://opencode.ai` origin.
|
|
207
|
+
When resolved provider auth includes a base URL, that URL must use the same origin.
|
|
208
|
+
Other origins fail before the credential is sent.
|
|
209
|
+
|
|
210
|
+
### xAI consumer subscriptions
|
|
211
|
+
|
|
212
|
+
- Provider ID: `xai`
|
|
213
|
+
- Semantics: consumer subscription allowance and credits, not xAI API-team billing
|
|
214
|
+
- Identity route: `GET https://cli-chat-proxy.grok.com/v1/user?include=subscription`
|
|
215
|
+
- Billing route: `GET https://cli-chat-proxy.grok.com/v1/billing?format=credits`
|
|
216
|
+
- Displayed data: included allowance or legacy monetary limit, period and reset, on-demand spend and cap, prepaid balance, and a sanitized optional plan tier
|
|
217
|
+
- Statusline: not published; xAI is queried only through an explicit `/usage` action
|
|
218
|
+
|
|
219
|
+
The adapter accepts only the official Pi inference origin `https://api.x.ai` and a freshly resolved bearer that exactly matches one complete Pi OAuth credential.
|
|
220
|
+
Pi's reviewed OAuth scope is `openid profile email offline_access grok-cli:access api:access`.
|
|
221
|
+
The adapter rejects `XAI_API_KEY`, duplicate or conflicting OAuth candidates, account mismatches, and incomplete OAuth records.
|
|
222
|
+
It also rejects custom or proxy-resolved origins before consumer-proxy access.
|
|
223
|
+
API-key users can review API-team spend through [console.x.ai](https://console.x.ai/) instead.
|
|
224
|
+
The public Management API requires a separate management key and team ID and is intentionally outside this runtime-credential integration.
|
|
225
|
+
|
|
226
|
+
The identity response supplies a transient proxy-canonical `userId` that is validated and sent as `x-userid` only on the billing request.
|
|
227
|
+
The extension sends the matched bearer as `Authorization` plus Grok Build's source-defined non-secret `X-XAI-Token-Auth`, client-version, and interactive client-mode headers.
|
|
228
|
+
It does not read Grok Build files, device state, names, email, or other profile fields.
|
|
229
|
+
Responses are body-bounded, redirects are rejected, raw identity and billing payloads are not retained, and secrets are redacted from errors.
|
|
230
|
+
Included allowance, on-demand usage, and prepaid balance remain distinct because they represent different billing concepts.
|
|
231
|
+
|
|
232
|
+
The current official Grok Build implementation is the ground truth for the xAI integration contract.
|
|
233
|
+
The implementation contract was verified against these first-party revisions:
|
|
234
|
+
|
|
235
|
+
- Pi [`providers/xai.ts`](https://github.com/earendil-works/pi/blob/e86823096c5bad39e1ca282ec24bc5eb9bec745b/packages/ai/src/providers/xai.ts) and [`auth/oauth/xai.ts`](https://github.com/earendil-works/pi/blob/e86823096c5bad39e1ca282ec24bc5eb9bec745b/packages/ai/src/auth/oauth/xai.ts) at `e868230`, revalidated byte-for-byte for those files at [`ccfe79e`](https://github.com/earendil-works/pi/tree/ccfe79ed238674f760c986e3a61493aab794000a).
|
|
236
|
+
- Grok Build [`UserInfo`](https://github.com/xai-org/grok-build/blob/9684fa3cdbf2995e30ea8b9b637f1db008f144fc/crates/codegen/xai-grok-shell/src/auth/model.rs), [`subscription_check.rs`](https://github.com/xai-org/grok-build/blob/9684fa3cdbf2995e30ea8b9b637f1db008f144fc/crates/codegen/xai-grok-shell/src/agent/subscription_check.rs), [`billing.rs`](https://github.com/xai-org/grok-build/blob/9684fa3cdbf2995e30ea8b9b637f1db008f144fc/crates/codegen/xai-grok-shell/src/extensions/billing.rs), [`auth/config.rs`](https://github.com/xai-org/grok-build/blob/9684fa3cdbf2995e30ea8b9b637f1db008f144fc/crates/codegen/xai-grok-shell/src/auth/config.rs), [`xai-grok-http`](https://github.com/xai-org/grok-build/blob/9684fa3cdbf2995e30ea8b9b637f1db008f144fc/crates/codegen/xai-grok-http/src/lib.rs), and [`xai-grok-version`](https://github.com/xai-org/grok-build/blob/9684fa3cdbf2995e30ea8b9b637f1db008f144fc/crates/codegen/xai-grok-version/Cargo.toml) at `9684fa3`.
|
|
237
|
+
- [xAI Management API team billing boundary at `723dd2a`](https://github.com/xai-org/xai-proto/blob/723dd2aa22d17be35617463837dc47cda008d90e/proto/xai/management_api/v1/billing.proto).
|
|
238
|
+
|
|
239
|
+
The approved 2026-08-27 protocol smoke used only Pi's OAuth bearer and read no Grok-local files.
|
|
240
|
+
A disposable or maintainer account received HTTP 200 without redirects from both routes.
|
|
241
|
+
The implementation also sends the non-secret client headers present on both routes in current Grok Build source, with `x-userid` added only for billing.
|
|
242
|
+
The sanitized identity shape contained a string `userId` and nullable `subscriptionTier`.
|
|
243
|
+
The billing shape contained a `config` object with period and distinct on-demand and prepaid wrappers, without retaining field values.
|
|
244
|
+
|
|
245
|
+
xAI identity and billing requests occur only after an explicit current, configured-provider, or all-provider `/usage` action.
|
|
246
|
+
|
|
247
|
+
### Z.AI (GLM Coding Plan)
|
|
248
|
+
|
|
249
|
+
- Provider ID: `zai` and `zai-coding-cn`
|
|
250
|
+
- Semantics: GLM Coding Plan quota windows—the rolling 5-hour and weekly plan-usage windows plus the monthly MCP allowance
|
|
251
|
+
- Source: the undocumented `GET {origin}/api/monitor/usage/quota/limit` endpoint also used by Z.AI's official coding plugin, plus the undocumented `GET {origin}/api/biz/subscription/list` plan endpoint
|
|
252
|
+
- Allowed origins: the model base URL must resolve to `https://api.z.ai` or `https://open.bigmodel.cn`
|
|
253
|
+
- Displayed data: explicit used and remaining values, reset times, provider-reported per-tool MCP details, and the plan name with its renewal date
|
|
254
|
+
- Percentage-only windows remain percent-based
|
|
255
|
+
- Statusline: publishes remaining plan percentages such as `zai 87% 5h 76% wk`; monthly MCP details remain available through `/usage`
|
|
256
|
+
|
|
257
|
+
The monitor endpoint is not a published API contract and may return legacy `TOKENS_LIMIT` or newer `CREDIT_LIMIT` window names.
|
|
258
|
+
The extension classifies both forms by the provider's window unit and does not label provider-reported counts as tokens or calls.
|
|
259
|
+
The quota monitor expects a raw API key without a `Bearer` prefix.
|
|
260
|
+
The extension removes that prefix from resolved authorization before sending it to the monitor endpoint.
|
|
261
|
+
Fingerprinting and redaction keep using the original resolved credential.
|
|
262
|
+
Quota errors use fixed English hints for the [documented Z.AI business error codes](https://docs.z.ai/api-reference/api-code), accepting nested `error.code` and the monitor endpoint's top-level `code` as strings or numbers.
|
|
263
|
+
An explicit nested code takes precedence; when it is absent, the top-level code is used.
|
|
264
|
+
For example, `1113` reports insufficient balance or no resource package, and `1309` reports an expired GLM Coding Plan.
|
|
265
|
+
When a failed HTTP response has no business error code, the hint uses its HTTP status; unknown business codes use a generic API failure message.
|
|
266
|
+
The monitor's top-level `code: 500` is not a documented business code and is not treated as HTTP 500 or proof that the credential has no Coding Plan.
|
|
267
|
+
Error classification never matches or echoes provider `msg` or `error.message` text, which may contain credentials.
|
|
268
|
+
Quota errors and missing or malformed quota data remain query failures with a statusline error hint, a 30-second request backoff, and scheduled retries; they no longer clear the statusline as unsupported.
|
|
269
|
+
An accepted Z.AI query failure discards the matching cached report, so the next turn after backoff expires retries instead of restoring stale usage.
|
|
270
|
+
The plan endpoint only contributes the plan name and renewal date; when it is unavailable or fails, the quota windows remain reported and the plan note falls back to the quota response's plan level.
|
|
271
|
+
Only the official `api.z.ai` and `open.bigmodel.cn` origins are queried; other origins fail before sending the credential.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@narumitw/pi-usage",
|
|
3
|
-
"version": "0.60.
|
|
3
|
+
"version": "0.60.6",
|
|
4
4
|
"description": "Pi extension that shows current-account usage and DeepSeek API balance for supported providers.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
"files": [
|
|
36
36
|
"src",
|
|
37
37
|
"dist",
|
|
38
|
+
"docs",
|
|
38
39
|
"README.md",
|
|
39
40
|
"LICENSE"
|
|
40
41
|
],
|
|
@@ -57,10 +58,10 @@
|
|
|
57
58
|
},
|
|
58
59
|
"devDependencies": {
|
|
59
60
|
"@biomejs/biome": "2.5.11",
|
|
60
|
-
"@earendil-works/pi-ai": "0.
|
|
61
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
62
|
-
"@earendil-works/pi-tui": "0.
|
|
63
|
-
"@types/node": "26.4.
|
|
61
|
+
"@earendil-works/pi-ai": "0.85.0",
|
|
62
|
+
"@earendil-works/pi-coding-agent": "0.85.0",
|
|
63
|
+
"@earendil-works/pi-tui": "0.85.0",
|
|
64
|
+
"@types/node": "26.4.1",
|
|
64
65
|
"esbuild": "0.28.2",
|
|
65
66
|
"typescript": "7.0.2"
|
|
66
67
|
},
|
package/src/core.ts
CHANGED
|
@@ -36,6 +36,10 @@ export class UsageCache {
|
|
|
36
36
|
this.entries.set(key, { createdAt: now, report });
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
delete(providerId: string, fingerprint: string): void {
|
|
40
|
+
this.entries.delete(cacheKey(providerId, fingerprint));
|
|
41
|
+
}
|
|
42
|
+
|
|
39
43
|
clearProvider(providerId: string): void {
|
|
40
44
|
for (const key of this.entries.keys()) {
|
|
41
45
|
if (key.startsWith(`${providerId}:`)) this.entries.delete(key);
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// Fixed messages from https://docs.z.ai/api-reference/api-code. Never interpolate provider text:
|
|
2
|
+
// error messages and their template parameters can contain credentials or terminal controls.
|
|
3
|
+
const ERROR_MESSAGES: Readonly<Record<string, string>> = {
|
|
4
|
+
"1000": "Authentication failed. Check your API key.",
|
|
5
|
+
"1001": "Authentication header missing. Check your API key.",
|
|
6
|
+
"1003": "Authentication token expired. Obtain a new token.",
|
|
7
|
+
"1005": "Two-factor authentication required.",
|
|
8
|
+
"1113": "Insufficient balance or no resource package. Recharge your account.",
|
|
9
|
+
"1200": "API call error. Try again later.",
|
|
10
|
+
"1210": "Invalid API parameter. Check the API documentation.",
|
|
11
|
+
"1211": "Unknown model. Check the model ID.",
|
|
12
|
+
"1212": "This model does not support the requested method.",
|
|
13
|
+
"1213": "A required parameter is missing.",
|
|
14
|
+
"1214": "Invalid parameter. Check the API documentation.",
|
|
15
|
+
"1215": "Conflicting parameters. Check the API documentation.",
|
|
16
|
+
"1220": "Access denied. Check your permissions.",
|
|
17
|
+
"1221": "This API has been taken offline.",
|
|
18
|
+
"1222": "This API does not exist.",
|
|
19
|
+
"1230": "API processing error. Try again later.",
|
|
20
|
+
"1234": "Network error. Try again later.",
|
|
21
|
+
"1261": "Prompt too long. Reduce the input length.",
|
|
22
|
+
"1301": "Content rejected by the safety policy.",
|
|
23
|
+
"1302": "Request rate limit reached. Try again later.",
|
|
24
|
+
"1305": "Service overloaded. Try again later.",
|
|
25
|
+
"1308": "Usage limit reached. Wait for the quota reset.",
|
|
26
|
+
"1309": "GLM Coding Plan expired. Renew your subscription.",
|
|
27
|
+
"1310": "Weekly or monthly limit exhausted. Wait for the quota reset.",
|
|
28
|
+
"1311": "Your subscription does not include this model.",
|
|
29
|
+
"1313": "Request frequency restricted by the Fair Usage Policy. Contact support.",
|
|
30
|
+
"1314": "Enterprise package expired. Contact your administrator.",
|
|
31
|
+
"1315": "This API key requires an enterprise coding package scenario. Replace the key.",
|
|
32
|
+
"1316": "5-hour limit reached; insufficient balance for extra usage. Wait for reset.",
|
|
33
|
+
"1317": "7-day limit reached; insufficient balance for extra usage. Wait for reset.",
|
|
34
|
+
"1318": "5-hour limit reached; extra usage blocked by monthly spend limit.",
|
|
35
|
+
"1319": "7-day limit reached; extra usage blocked by monthly spend limit.",
|
|
36
|
+
"1320": "5-hour limit reached; extra usage blocked by monthly spend limit.",
|
|
37
|
+
"1321": "7-day limit reached; extra usage blocked by monthly spend limit.",
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const HTTP_MESSAGES: Readonly<Record<number, string>> = {
|
|
41
|
+
400: "Invalid request. Check the API documentation.",
|
|
42
|
+
401: "Authentication failed. Check your API key.",
|
|
43
|
+
403: "Access denied. Check your permissions.",
|
|
44
|
+
429: "Request or usage limit reached. Try again later.",
|
|
45
|
+
500: "Internal error. Try again later.",
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export function zaiPayloadError(payload: unknown): string | undefined {
|
|
49
|
+
const object = asObject(payload);
|
|
50
|
+
if (!object) return undefined;
|
|
51
|
+
const nested = asObject(object.error);
|
|
52
|
+
// Missing nested metadata must not hide a top-level code; explicit nested values retain priority.
|
|
53
|
+
const rawCode = nested?.code === undefined ? object.code : nested.code;
|
|
54
|
+
const code = errorCode(rawCode);
|
|
55
|
+
if (
|
|
56
|
+
object.error === undefined &&
|
|
57
|
+
object.success !== false &&
|
|
58
|
+
(rawCode === undefined || code === "0" || code === "200")
|
|
59
|
+
) {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
return code && code !== "0" && code !== "200"
|
|
63
|
+
? `Z.AI ${code}: ${ERROR_MESSAGES[code] ?? "API request failed."}`
|
|
64
|
+
: "Z.AI: API request failed.";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function zaiResponseError(status: number, text: string): string | undefined {
|
|
68
|
+
let payload: unknown;
|
|
69
|
+
try {
|
|
70
|
+
payload = JSON.parse(text) as unknown;
|
|
71
|
+
} catch {
|
|
72
|
+
// Do not expose JSON parser excerpts, response bodies, or statusText.
|
|
73
|
+
if (status >= 200 && status < 300) return "Z.AI: Invalid JSON response.";
|
|
74
|
+
}
|
|
75
|
+
const error = zaiPayloadError(payload);
|
|
76
|
+
if (error) return error;
|
|
77
|
+
if (status < 200 || status >= 300) {
|
|
78
|
+
return `Z.AI HTTP ${status}: ${HTTP_MESSAGES[status] ?? "API request failed."}`;
|
|
79
|
+
}
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function errorCode(value: unknown): string | undefined {
|
|
84
|
+
if (typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= 9999) {
|
|
85
|
+
return String(value);
|
|
86
|
+
}
|
|
87
|
+
return typeof value === "string" && /^(?:0|[1-9]\d{0,3})$/u.test(value) ? value : undefined;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function asObject(value: unknown): Record<string, unknown> | undefined {
|
|
91
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
92
|
+
? (value as Record<string, unknown>)
|
|
93
|
+
: undefined;
|
|
94
|
+
}
|
package/src/providers/zai.ts
CHANGED
|
@@ -8,6 +8,8 @@ import type {
|
|
|
8
8
|
ZaiSubscriptionPayload,
|
|
9
9
|
} from "../types.js";
|
|
10
10
|
|
|
11
|
+
import { zaiPayloadError } from "./zai-errors.js";
|
|
12
|
+
|
|
11
13
|
const FIVE_HOUR_WINDOW_MINUTES = 300;
|
|
12
14
|
const WEEKLY_WINDOW_MINUTES = 10_080;
|
|
13
15
|
|
|
@@ -18,6 +20,8 @@ export function normalizeZaiQuotaPayload(
|
|
|
18
20
|
capturedAt: number,
|
|
19
21
|
plan?: ZaiPlanInfo,
|
|
20
22
|
): UsageReport {
|
|
23
|
+
const error = zaiPayloadError(payload);
|
|
24
|
+
if (error) throw new Error(error);
|
|
21
25
|
const data = asObject(payload.data);
|
|
22
26
|
if (!data) throw new Error("Z.AI quota response data was not an object.");
|
|
23
27
|
const limits = Array.isArray(data.limits) ? (data.limits as unknown[]) : [];
|
|
@@ -81,10 +85,7 @@ export function normalizeZaiQuotaPayload(
|
|
|
81
85
|
export function normalizeZaiSubscriptionPayload(
|
|
82
86
|
payload: ZaiSubscriptionPayload,
|
|
83
87
|
): ZaiPlanInfo | undefined {
|
|
84
|
-
if (payload
|
|
85
|
-
if (typeof payload.code === "number" && payload.code !== 0 && payload.code !== 200) {
|
|
86
|
-
return undefined;
|
|
87
|
-
}
|
|
88
|
+
if (zaiPayloadError(payload)) return undefined;
|
|
88
89
|
if (!Array.isArray(payload.data)) return undefined;
|
|
89
90
|
|
|
90
91
|
const candidates: Array<{
|
package/src/query.ts
CHANGED
|
@@ -24,6 +24,7 @@ import { normalizeOpenRouterKeyPayload } from "./providers/openrouter.js";
|
|
|
24
24
|
import { normalizeVercelAIGatewayCreditsPayload } from "./providers/vercel-ai-gateway.js";
|
|
25
25
|
import { normalizeXaiBillingPayload } from "./providers/xai.js";
|
|
26
26
|
import { normalizeZaiQuotaPayload, normalizeZaiSubscriptionPayload } from "./providers/zai.js";
|
|
27
|
+
import { zaiResponseError } from "./providers/zai-errors.js";
|
|
27
28
|
import type {
|
|
28
29
|
BasetenBillingUsagePayload,
|
|
29
30
|
CodexBackendPayload,
|
|
@@ -261,6 +262,7 @@ export const SUPPORTED_ADAPTERS: readonly UsageProviderAdapter[] = [
|
|
|
261
262
|
{
|
|
262
263
|
id: "zai",
|
|
263
264
|
displayName: "Z.AI",
|
|
265
|
+
invalidateCacheOnFailure: true,
|
|
264
266
|
semantics: { kind: "consumer-subscription", label: "GLM Coding Plan usage" },
|
|
265
267
|
async query(auth, signal, timeoutMs, guard) {
|
|
266
268
|
return queryZaiUsage("zai", "Z.AI", auth, signal, timeoutMs, guard);
|
|
@@ -269,6 +271,7 @@ export const SUPPORTED_ADAPTERS: readonly UsageProviderAdapter[] = [
|
|
|
269
271
|
{
|
|
270
272
|
id: "zai-coding-cn",
|
|
271
273
|
displayName: "Z.AI Coding CN",
|
|
274
|
+
invalidateCacheOnFailure: true,
|
|
272
275
|
semantics: { kind: "consumer-subscription", label: "GLM Coding Plan usage" },
|
|
273
276
|
async query(auth, signal, timeoutMs, guard) {
|
|
274
277
|
return queryZaiUsage("zai-coding-cn", "Z.AI Coding CN", auth, signal, timeoutMs, guard);
|
|
@@ -627,6 +630,7 @@ export async function fetchProviderJson(
|
|
|
627
630
|
body?: Record<string, unknown>;
|
|
628
631
|
redirect?: RequestRedirect;
|
|
629
632
|
userAgent?: boolean;
|
|
633
|
+
responseError?: (status: number, text: string) => string | undefined;
|
|
630
634
|
} = {},
|
|
631
635
|
): Promise<Record<string, unknown>> {
|
|
632
636
|
const controller = new AbortController();
|
|
@@ -665,6 +669,8 @@ export async function fetchProviderJson(
|
|
|
665
669
|
);
|
|
666
670
|
if (controller.signal.aborted)
|
|
667
671
|
throw Object.assign(new Error("Usage query aborted."), { name: "AbortError" });
|
|
672
|
+
const responseError = request.responseError?.(response.status, text);
|
|
673
|
+
if (responseError) throw new Error(responseError);
|
|
668
674
|
if (!response.ok) {
|
|
669
675
|
throw new Error(
|
|
670
676
|
`${description} returned ${response.status} ${response.statusText}: ${redactUsageError(text, auth.secrets)}`,
|
|
@@ -1101,6 +1107,7 @@ async function queryZaiUsage(
|
|
|
1101
1107
|
signal,
|
|
1102
1108
|
remainingTimeout(timeoutMs, startedAt, `fetching ${providerName} quota`),
|
|
1103
1109
|
`${providerName} quota endpoint`,
|
|
1110
|
+
{ responseError: zaiResponseError },
|
|
1104
1111
|
)) as ZaiQuotaPayload;
|
|
1105
1112
|
await guard();
|
|
1106
1113
|
const planTimeoutMs = timeoutMs - (Date.now() - startedAt);
|
|
@@ -1125,6 +1132,7 @@ async function fetchZaiPlan(
|
|
|
1125
1132
|
signal,
|
|
1126
1133
|
timeoutMs,
|
|
1127
1134
|
`${providerName} plan endpoint`,
|
|
1135
|
+
{ responseError: zaiResponseError },
|
|
1128
1136
|
)) as ZaiSubscriptionPayload;
|
|
1129
1137
|
return normalizeZaiSubscriptionPayload(payload);
|
|
1130
1138
|
} catch (error) {
|
package/src/types.ts
CHANGED
|
@@ -91,6 +91,8 @@ export interface UsageProviderAdapter {
|
|
|
91
91
|
displayName: string;
|
|
92
92
|
semantics: UsageSemantics;
|
|
93
93
|
publishesStatusline?: boolean;
|
|
94
|
+
/** Invalidate the matching ready report when the latest query fails; default preserves it. */
|
|
95
|
+
invalidateCacheOnFailure?: boolean;
|
|
94
96
|
targets?: UsageTargetResolver;
|
|
95
97
|
query(
|
|
96
98
|
auth: ResolvedUsageAuth,
|
|
@@ -191,10 +193,14 @@ export type OpenCodeZenPayload = {
|
|
|
191
193
|
};
|
|
192
194
|
|
|
193
195
|
export type ZaiQuotaPayload = {
|
|
196
|
+
error?: unknown;
|
|
197
|
+
code?: unknown;
|
|
198
|
+
success?: unknown;
|
|
194
199
|
data?: unknown;
|
|
195
200
|
};
|
|
196
201
|
|
|
197
202
|
export type ZaiSubscriptionPayload = {
|
|
203
|
+
error?: unknown;
|
|
198
204
|
code?: unknown;
|
|
199
205
|
success?: unknown;
|
|
200
206
|
data?: unknown;
|
package/src/usage.ts
CHANGED
|
@@ -484,6 +484,7 @@ export default function usageExtension(
|
|
|
484
484
|
if (failure.until <= now) failureBackoff.delete(key);
|
|
485
485
|
}
|
|
486
486
|
if (queryId === undefined || latestQueries.get(failureKey) === queryId) {
|
|
487
|
+
if (adapter.invalidateCacheOnFailure) cache.delete(adapter.id, queryFingerprint);
|
|
487
488
|
setBoundedMap(
|
|
488
489
|
failureBackoff,
|
|
489
490
|
failureKey,
|