@astrosheep/pi-quota 0.11.0 → 0.12.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/README.md CHANGED
@@ -81,6 +81,28 @@ Pi model/auth → adapter → QuotaSnapshot → controller → footer / /usage /
81
81
 
82
82
  The extension publishes structured state on `quota-bar:state:v1`. Use `createQuotaExtension({ footer: false })` to consume it without this extension's footer. Normal queries are throttled, cancellable, timeout-bounded and backed off after errors. `/usage` forces a fresh query for every configured supported provider, then omits failed results from its default view. Switching provider discards old footer values immediately.
83
83
 
84
+ ## Standalone query layer
85
+
86
+ Non-Pi consumers (bridges, plugins, scripts) can import the query layer without pulling in the Pi extension or its peer dependencies:
87
+
88
+ ```ts
89
+ import {
90
+ AdapterRegistry, codexAdapter, createJsonClient, createPostJsonClient,
91
+ } from '@astrosheep/pi-quota/query';
92
+
93
+ const registry = new AdapterRegistry([codexAdapter]);
94
+ const snapshot = await registry.get('openai-codex')!.query({
95
+ provider: 'openai-codex',
96
+ signal: AbortSignal.timeout(10_000),
97
+ now: () => Date.now(),
98
+ getAuth: myAuthResolver, // you supply credentials
99
+ getJson: createJsonClient(),
100
+ postJson: createPostJsonClient(),
101
+ });
102
+ ```
103
+
104
+ The subpath exports every adapter, the registry/controller, option validators, HTTP/exec plumbing, and all query types. Peer dependencies are marked optional, so installing for the query layer alone stays light.
105
+
84
106
  ## Development
85
107
 
86
108
  Requires Node ≥22.18 and a compatible Pi installation.
package/package.json CHANGED
@@ -1,10 +1,15 @@
1
1
  {
2
2
  "name": "@astrosheep/pi-quota",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
7
7
  "type": "module",
8
+ "exports": {
9
+ ".": "./src/index.ts",
10
+ "./query": "./src/query/index.ts",
11
+ "./package.json": "./package.json"
12
+ },
8
13
  "description": "Composable quota bar with Codex, Kimi, OpenCode Go, new-api, DeepSeek, Sub2API and YesCode adapters",
9
14
  "keywords": [
10
15
  "pi-package"
@@ -31,6 +36,10 @@
31
36
  "@earendil-works/pi-coding-agent": "*",
32
37
  "@earendil-works/pi-tui": "*"
33
38
  },
39
+ "peerDependenciesMeta": {
40
+ "@earendil-works/pi-coding-agent": { "optional": true },
41
+ "@earendil-works/pi-tui": { "optional": true }
42
+ },
34
43
  "devDependencies": {
35
44
  "@types/node": "^22.0.0",
36
45
  "typescript": "^6.0.3"
package/src/bar/quota.ts CHANGED
@@ -3,6 +3,7 @@ import { duration, formatPercent, horizontalBar, plain, remainingTone, renderBar
3
3
  import type { BarElement, Paint, Span } from './bar.ts';
4
4
  import { allowanceLines, allowanceSpans, balanceLines, balanceSpans, money, spendLines } from './balance.ts';
5
5
  import type { QuotaErrorCode, QuotaState, QuotaWindow } from '../query/types.ts';
6
+ import type { FooterWindows } from '../query/settings.ts';
6
7
 
7
8
  const errorText: Record<QuotaErrorCode, string> = {
8
9
  auth: 'Sign in with /login',
@@ -25,7 +26,7 @@ function windowSpans(window: QuotaWindow, now: number): Span[] {
25
26
  }
26
27
 
27
28
  // Footer window cap: a positive count, or 'all' for every shared window.
28
- export type FooterWindows = number | 'all';
29
+ export type { FooterWindows };
29
30
 
30
31
  export function quotaElement(state: QuotaState, now = Date.now(), maxWindows: FooterWindows = 2): BarElement {
31
32
  const element: BarElement = { id: 'quota', spans: [] };
package/src/config.ts CHANGED
@@ -1,140 +1,11 @@
1
- import { createDeepSeekAdapter } from './query/deepseek.ts';
2
- import { createExecAdapter, validateExecOptions } from './query/exec.ts';
3
- import { createNewApiAdapter, validateNewApiOptions } from './query/new-api.ts';
4
- import { createOpenCodeGoAdapter } from './query/opencode-go.ts';
5
- import { createSub2ApiAdapter } from './query/sub2api.ts';
6
- import { createYesCodeAdapter, validateYesCodeOptions } from './query/yescode.ts';
7
- import type { NewApiOptions } from './query/new-api.ts';
8
- import type { YesCodeOptions } from './query/yescode.ts';
9
- import type { QuotaAdapter } from './query/types.ts';
10
- import type { FooterWindows } from './bar/quota.ts';
11
-
12
- export type ProviderQuotaConfig =
13
- | ({ adapter: 'new-api'; quotaPerUnit: number; currency: string } & Pick<NewApiOptions, 'dashboardAccessToken' | 'dashboardUserId'>)
14
- | ({ adapter: 'yescode' } & YesCodeOptions)
15
- | { adapter: 'exec'; command: string[]; label: string }
16
- | { adapter: 'alias'; of: string }
17
- | { adapter: 'deepseek' | 'sub2api' | 'opencode-go' };
18
- export interface QuotaConfig { providers: Record<string, ProviderQuotaConfig>; footerWindows?: FooterWindows }
19
-
20
- const DASHBOARD_KEYS = ['dashboardAccessToken', 'dashboardUserId'] as const;
21
-
22
- function parseDashboardOptions(item: Record<string, unknown>): Pick<NewApiOptions, 'dashboardAccessToken' | 'dashboardUserId'> {
23
- const result: Pick<NewApiOptions, 'dashboardAccessToken' | 'dashboardUserId'> = {};
24
- if (item.dashboardAccessToken !== undefined) {
25
- if (typeof item.dashboardAccessToken !== 'string' || item.dashboardAccessToken.trim() === '') throw new QuotaConfigError();
26
- result.dashboardAccessToken = item.dashboardAccessToken;
27
- }
28
- if (item.dashboardUserId !== undefined) {
29
- if (typeof item.dashboardUserId !== 'number'
30
- || !Number.isSafeInteger(item.dashboardUserId) || item.dashboardUserId <= 0) throw new QuotaConfigError();
31
- result.dashboardUserId = item.dashboardUserId;
32
- }
33
- return result;
34
- }
35
-
36
- function parseFooterWindows(value: unknown): FooterWindows {
37
- if (value === 'all') return value;
38
- if (typeof value === 'number' && Number.isSafeInteger(value) && value >= 1) return value;
39
- throw new QuotaConfigError();
40
- }
41
-
42
- export class QuotaConfigError extends Error {
43
- constructor() {
44
- // Settings may accidentally contain secrets. Never echo content/values.
45
- super('Invalid settings.json quotaUsage. Use providers with adapter "new-api", "deepseek", "sub2api", "opencode-go", "yescode", "exec" or "alias", and optionally footerWindows as a positive integer or "all".');
46
- }
47
- }
48
-
49
- function record(value: unknown): value is Record<string, unknown> {
50
- return value !== null && typeof value === 'object' && !Array.isArray(value);
51
- }
52
-
53
- export function parseQuotaConfig(value: unknown): QuotaConfig {
54
- if (!record(value) || Object.keys(value).some(key => key !== 'providers' && key !== 'footerWindows')
55
- || !record(value.providers) || Object.keys(value.providers).length > 64) throw new QuotaConfigError();
56
- const providers: Record<string, ProviderQuotaConfig> = Object.create(null);
57
- for (const [provider, item] of Object.entries(value.providers)) {
58
- if (!/^[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}$/.test(provider)
59
- || ['openai-codex', 'kimi-coding', 'opencode-go'].includes(provider)
60
- || !record(item)) throw new QuotaConfigError();
61
- if (item.adapter === 'deepseek') {
62
- if (Object.keys(item).some(key => key !== 'adapter')) throw new QuotaConfigError();
63
- providers[provider] = { adapter: 'deepseek' };
64
- continue;
65
- }
66
- if (item.adapter === 'sub2api' || item.adapter === 'opencode-go') {
67
- if (Object.keys(item).length !== 1) throw new QuotaConfigError();
68
- providers[provider] = { adapter: item.adapter };
69
- continue;
70
- }
71
- if (item.adapter === 'yescode') {
72
- if (Object.keys(item).some(key => !['adapter', 'username', 'password'].includes(key))
73
- || typeof item.username !== 'string' || typeof item.password !== 'string') throw new QuotaConfigError();
74
- try {
75
- providers[provider] = { adapter: 'yescode',
76
- ...validateYesCodeOptions({ username: item.username, password: item.password }) };
77
- } catch { throw new QuotaConfigError(); }
78
- continue;
79
- }
80
- if (item.adapter === 'exec') {
81
- if (Object.keys(item).some(key => !['adapter', 'command', 'label'].includes(key))) throw new QuotaConfigError();
82
- try {
83
- providers[provider] = { adapter: 'exec', ...validateExecOptions(item) };
84
- } catch { throw new QuotaConfigError(); }
85
- continue;
86
- }
87
- if (item.adapter === 'alias') {
88
- if (Object.keys(item).some(key => key !== 'adapter' && key !== 'of')
89
- || typeof item.of !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}$/.test(item.of)
90
- || item.of === provider) throw new QuotaConfigError();
91
- providers[provider] = { adapter: 'alias', of: item.of };
92
- continue;
93
- }
94
- if (item.adapter !== 'new-api'
95
- || Object.keys(item).some(key => !['adapter', 'quotaPerUnit', 'currency', ...DASHBOARD_KEYS].includes(key))
96
- || (item.quotaPerUnit !== undefined && typeof item.quotaPerUnit !== 'number')
97
- || (item.currency !== undefined && typeof item.currency !== 'string')) throw new QuotaConfigError();
98
- try {
99
- const settings = validateNewApiOptions({ quotaPerUnit: item.quotaPerUnit, currency: item.currency });
100
- providers[provider] = { adapter: 'new-api', ...settings, ...parseDashboardOptions(item) };
101
- } catch { throw new QuotaConfigError(); }
102
- }
103
- const config: QuotaConfig = { providers };
104
- if (value.footerWindows !== undefined) config.footerWindows = parseFooterWindows(value.footerWindows);
105
- return config;
106
- }
107
-
108
- export function loadQuotaFooterWindows(settings: unknown): FooterWindows | undefined {
109
- if (!record(settings) || settings.quotaUsage === undefined) return undefined;
110
- return parseQuotaConfig(settings.quotaUsage).footerWindows;
111
- }
112
-
113
- export function loadQuotaAdaptersFromSettings(settings: unknown): QuotaAdapter[] {
114
- if (!record(settings) || settings.quotaUsage === undefined) return [];
115
- const config = parseQuotaConfig(settings.quotaUsage);
116
- return Object.entries(config.providers).flatMap(([provider, options]) => {
117
- switch (options.adapter) {
118
- case 'alias': return []; // Aliases declare no adapter; extension.ts wires them.
119
- case 'deepseek': return createDeepSeekAdapter(provider);
120
- case 'sub2api': return createSub2ApiAdapter(provider);
121
- case 'opencode-go': return createOpenCodeGoAdapter(provider);
122
- case 'yescode': return createYesCodeAdapter(provider, options);
123
- case 'exec': return createExecAdapter(provider, options);
124
- case 'new-api': return createNewApiAdapter(provider, options);
125
- }
126
- });
127
- }
128
-
129
- export function loadQuotaAliasesFromSettings(settings: unknown): Record<string, string> {
130
- if (!record(settings) || settings.quotaUsage === undefined) return {};
131
- const config = parseQuotaConfig(settings.quotaUsage);
132
- const aliases: Record<string, string> = Object.create(null);
133
- for (const [provider, options] of Object.entries(config.providers)) {
134
- if (options.adapter === 'alias') aliases[provider] = options.of;
135
- }
136
- return aliases;
137
- }
138
-
139
- // Explicit adapter bindings from quotaUsage already have adapters registered; listing every
140
- // registered adapter is the boundary between quota providers and ordinary model providers.
1
+ // Back-compatible surface: the implementation lives in the Pi-free query layer
2
+ // (src/query/settings.ts) so non-Pi consumers can compose the same registry.
3
+ export {
4
+ composeQuotaRegistry,
5
+ loadQuotaAdaptersFromSettings,
6
+ loadQuotaAliasesFromSettings,
7
+ loadQuotaFooterWindows,
8
+ parseQuotaConfig,
9
+ QuotaConfigError,
10
+ } from './query/settings.ts';
11
+ export type { ComposedQuota, FooterWindows, ProviderQuotaConfig, QuotaConfig } from './query/settings.ts';
package/src/extension.ts CHANGED
@@ -3,14 +3,9 @@ import { renderFooter } from './bar/quota.ts';
3
3
  import type { FooterWindows } from './bar/quota.ts';
4
4
  import { captureAllUsage, USAGE_ENTRY, usageCardComponent, usageLines, compactUsageLines } from './bar/card.ts';
5
5
  import type { UsageCard } from './bar/card.ts';
6
- import { codexAdapter } from './query/codex.ts';
7
- import { createDeepSeekAdapter } from './query/deepseek.ts';
8
- import { kimiAdapter } from './query/kimi.ts';
9
- import { openCodeGoAdapter } from './query/opencode-go.ts';
10
6
  import { QuotaController } from './query/controller.ts';
11
7
  import { createJsonClient, createPostJsonClient } from './query/http.ts';
12
- import { AdapterRegistry } from './query/registry.ts';
13
- import { createAliasAdapter, createDanglingAliasAdapter } from './query/alias.ts';
8
+ import { composeQuotaRegistry } from './query/settings.ts';
14
9
  import type { ProviderAuth, QuotaAdapter, QuotaState } from './query/types.ts';
15
10
 
16
11
  export const STATUS_EVENT = 'quota-bar:state:v1';
@@ -33,21 +28,8 @@ export interface QuotaExtensionOptions {
33
28
 
34
29
  export function createQuotaExtension(options: QuotaExtensionOptions = {}) {
35
30
  return (pi: ExtensionAPI): void => {
36
- const explicitAdapters = options.adapters ?? [];
37
- const aliases = options.aliases ?? {};
38
- const explicitProviderIds = new Set([...explicitAdapters.map(adapter => adapter.provider), ...Object.keys(aliases)]);
39
31
  // Settings are deliberate user bindings, so they take precedence over defaults.
40
- // AdapterRegistry still detects an ambiguous duplicate among explicit adapters.
41
- const registry = new AdapterRegistry([
42
- codexAdapter, kimiAdapter, openCodeGoAdapter, createDeepSeekAdapter('deepseek'),
43
- ].filter(adapter => !explicitProviderIds.has(adapter.provider)).concat(explicitAdapters));
44
- // Alias providers share their target's account: register a delegating adapter
45
- // under the alias ID. One hop only — an alias of an alias never resolves, and
46
- // a dangling alias stays visible as an error instead of vanishing silently.
47
- for (const [alias, targetId] of Object.entries(aliases)) {
48
- const target = aliases[targetId] === undefined ? registry.get(targetId) : undefined;
49
- registry.register(target ? createAliasAdapter(alias, target) : createDanglingAliasAdapter(alias));
50
- }
32
+ const { registry, aliases } = composeQuotaRegistry({ adapters: options.adapters, aliases: options.aliases });
51
33
  let current: ExtensionContext | undefined;
52
34
  let selectedProvider: string | undefined;
53
35
  let timer: ReturnType<typeof setInterval> | undefined;
@@ -0,0 +1,23 @@
1
+ // Public entry for the standalone query layer — no Pi, terminal, or rendering
2
+ // dependencies. Consumers supply auth and transport through QueryContext.
3
+ export * from './types.ts';
4
+ export { AdapterRegistry, validateSnapshot } from './registry.ts';
5
+ export { QuotaController } from './controller.ts';
6
+ export { createJsonClient, createPostJsonClient, officialAuth, bearer, header } from './http.ts';
7
+ export { createAliasAdapter, createDanglingAliasAdapter } from './alias.ts';
8
+ export { createExecAdapter, validateExecOptions } from './exec.ts';
9
+ export type { ExecOptions } from './exec.ts';
10
+ export { codexAccountId, codexAdapter } from './codex.ts';
11
+ export { kimiAdapter } from './kimi.ts';
12
+ export { openCodeGoAdapter, createOpenCodeGoAdapter } from './opencode-go.ts';
13
+ export { createDeepSeekAdapter, DEEPSEEK_ORIGIN } from './deepseek.ts';
14
+ export { createNewApiAdapter, validateNewApiOptions } from './new-api.ts';
15
+ export type { NewApiOptions } from './new-api.ts';
16
+ export { createSub2ApiAdapter } from './sub2api.ts';
17
+ export { createYesCodeAdapter, validateYesCodeOptions, YESCODE_ORIGIN } from './yescode.ts';
18
+ export type { YesCodeOptions } from './yescode.ts';
19
+ export {
20
+ composeQuotaRegistry, loadQuotaAdaptersFromSettings, loadQuotaAliasesFromSettings,
21
+ loadQuotaFooterWindows, parseQuotaConfig, QuotaConfigError,
22
+ } from './settings.ts';
23
+ export type { ComposedQuota, FooterWindows, ProviderQuotaConfig, QuotaConfig } from './settings.ts';
@@ -0,0 +1,179 @@
1
+ import { createAliasAdapter, createDanglingAliasAdapter } from './alias.ts';
2
+ import { codexAdapter } from './codex.ts';
3
+ import { createDeepSeekAdapter } from './deepseek.ts';
4
+ import { createExecAdapter, validateExecOptions } from './exec.ts';
5
+ import { kimiAdapter } from './kimi.ts';
6
+ import { createNewApiAdapter, validateNewApiOptions } from './new-api.ts';
7
+ import { createOpenCodeGoAdapter, openCodeGoAdapter } from './opencode-go.ts';
8
+ import { AdapterRegistry } from './registry.ts';
9
+ import { createSub2ApiAdapter } from './sub2api.ts';
10
+ import { createYesCodeAdapter, validateYesCodeOptions } from './yescode.ts';
11
+ import type { NewApiOptions } from './new-api.ts';
12
+ import type { YesCodeOptions } from './yescode.ts';
13
+ import type { QuotaAdapter } from './types.ts';
14
+
15
+ export type FooterWindows = number | 'all';
16
+
17
+ export type ProviderQuotaConfig =
18
+ | ({ adapter: 'new-api'; quotaPerUnit: number; currency: string } & Pick<NewApiOptions, 'dashboardAccessToken' | 'dashboardUserId'>)
19
+ | ({ adapter: 'yescode' } & YesCodeOptions)
20
+ | { adapter: 'exec'; command: string[]; label: string }
21
+ | { adapter: 'alias'; of: string }
22
+ | { adapter: 'deepseek' | 'sub2api' | 'opencode-go' };
23
+ export interface QuotaConfig { providers: Record<string, ProviderQuotaConfig>; footerWindows?: FooterWindows }
24
+
25
+ const DASHBOARD_KEYS = ['dashboardAccessToken', 'dashboardUserId'] as const;
26
+
27
+ function parseDashboardOptions(item: Record<string, unknown>): Pick<NewApiOptions, 'dashboardAccessToken' | 'dashboardUserId'> {
28
+ const result: Pick<NewApiOptions, 'dashboardAccessToken' | 'dashboardUserId'> = {};
29
+ if (item.dashboardAccessToken !== undefined) {
30
+ if (typeof item.dashboardAccessToken !== 'string' || item.dashboardAccessToken.trim() === '') throw new QuotaConfigError();
31
+ result.dashboardAccessToken = item.dashboardAccessToken;
32
+ }
33
+ if (item.dashboardUserId !== undefined) {
34
+ if (typeof item.dashboardUserId !== 'number'
35
+ || !Number.isSafeInteger(item.dashboardUserId) || item.dashboardUserId <= 0) throw new QuotaConfigError();
36
+ result.dashboardUserId = item.dashboardUserId;
37
+ }
38
+ return result;
39
+ }
40
+
41
+ function parseFooterWindows(value: unknown): FooterWindows {
42
+ if (value === 'all') return value;
43
+ if (typeof value === 'number' && Number.isSafeInteger(value) && value >= 1) return value;
44
+ throw new QuotaConfigError();
45
+ }
46
+
47
+ export class QuotaConfigError extends Error {
48
+ constructor() {
49
+ // Settings may accidentally contain secrets. Never echo content/values.
50
+ super('Invalid settings.json quotaUsage. Use providers with adapter "new-api", "deepseek", "sub2api", "opencode-go", "yescode", "exec" or "alias", and optionally footerWindows as a positive integer or "all".');
51
+ }
52
+ }
53
+
54
+ function record(value: unknown): value is Record<string, unknown> {
55
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
56
+ }
57
+
58
+ export function parseQuotaConfig(value: unknown): QuotaConfig {
59
+ if (!record(value) || Object.keys(value).some(key => key !== 'providers' && key !== 'footerWindows')
60
+ || !record(value.providers) || Object.keys(value.providers).length > 64) throw new QuotaConfigError();
61
+ const providers: Record<string, ProviderQuotaConfig> = Object.create(null);
62
+ for (const [provider, item] of Object.entries(value.providers)) {
63
+ if (!/^[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}$/.test(provider)
64
+ || ['openai-codex', 'kimi-coding', 'opencode-go'].includes(provider)
65
+ || !record(item)) throw new QuotaConfigError();
66
+ if (item.adapter === 'deepseek') {
67
+ if (Object.keys(item).some(key => key !== 'adapter')) throw new QuotaConfigError();
68
+ providers[provider] = { adapter: 'deepseek' };
69
+ continue;
70
+ }
71
+ if (item.adapter === 'sub2api' || item.adapter === 'opencode-go') {
72
+ if (Object.keys(item).length !== 1) throw new QuotaConfigError();
73
+ providers[provider] = { adapter: item.adapter };
74
+ continue;
75
+ }
76
+ if (item.adapter === 'yescode') {
77
+ if (Object.keys(item).some(key => !['adapter', 'username', 'password'].includes(key))
78
+ || typeof item.username !== 'string' || typeof item.password !== 'string') throw new QuotaConfigError();
79
+ try {
80
+ providers[provider] = { adapter: 'yescode',
81
+ ...validateYesCodeOptions({ username: item.username, password: item.password }) };
82
+ } catch { throw new QuotaConfigError(); }
83
+ continue;
84
+ }
85
+ if (item.adapter === 'exec') {
86
+ if (Object.keys(item).some(key => !['adapter', 'command', 'label'].includes(key))) throw new QuotaConfigError();
87
+ try {
88
+ providers[provider] = { adapter: 'exec', ...validateExecOptions(item) };
89
+ } catch { throw new QuotaConfigError(); }
90
+ continue;
91
+ }
92
+ if (item.adapter === 'alias') {
93
+ if (Object.keys(item).some(key => key !== 'adapter' && key !== 'of')
94
+ || typeof item.of !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}$/.test(item.of)
95
+ || item.of === provider) throw new QuotaConfigError();
96
+ providers[provider] = { adapter: 'alias', of: item.of };
97
+ continue;
98
+ }
99
+ if (item.adapter !== 'new-api'
100
+ || Object.keys(item).some(key => !['adapter', 'quotaPerUnit', 'currency', ...DASHBOARD_KEYS].includes(key))
101
+ || (item.quotaPerUnit !== undefined && typeof item.quotaPerUnit !== 'number')
102
+ || (item.currency !== undefined && typeof item.currency !== 'string')) throw new QuotaConfigError();
103
+ try {
104
+ const settings = validateNewApiOptions({ quotaPerUnit: item.quotaPerUnit, currency: item.currency });
105
+ providers[provider] = { adapter: 'new-api', ...settings, ...parseDashboardOptions(item) };
106
+ } catch { throw new QuotaConfigError(); }
107
+ }
108
+ const config: QuotaConfig = { providers };
109
+ if (value.footerWindows !== undefined) config.footerWindows = parseFooterWindows(value.footerWindows);
110
+ return config;
111
+ }
112
+
113
+ export function loadQuotaFooterWindows(settings: unknown): FooterWindows | undefined {
114
+ if (!record(settings) || settings.quotaUsage === undefined) return undefined;
115
+ return parseQuotaConfig(settings.quotaUsage).footerWindows;
116
+ }
117
+
118
+ export function loadQuotaAdaptersFromSettings(settings: unknown): QuotaAdapter[] {
119
+ if (!record(settings) || settings.quotaUsage === undefined) return [];
120
+ const config = parseQuotaConfig(settings.quotaUsage);
121
+ return Object.entries(config.providers).flatMap(([provider, options]) => {
122
+ switch (options.adapter) {
123
+ case 'alias': return []; // Aliases declare no adapter; extension.ts wires them.
124
+ case 'deepseek': return createDeepSeekAdapter(provider);
125
+ case 'sub2api': return createSub2ApiAdapter(provider);
126
+ case 'opencode-go': return createOpenCodeGoAdapter(provider);
127
+ case 'yescode': return createYesCodeAdapter(provider, options);
128
+ case 'exec': return createExecAdapter(provider, options);
129
+ case 'new-api': return createNewApiAdapter(provider, options);
130
+ }
131
+ });
132
+ }
133
+
134
+ export function loadQuotaAliasesFromSettings(settings: unknown): Record<string, string> {
135
+ if (!record(settings) || settings.quotaUsage === undefined) return {};
136
+ const config = parseQuotaConfig(settings.quotaUsage);
137
+ const aliases: Record<string, string> = Object.create(null);
138
+ for (const [provider, options] of Object.entries(config.providers)) {
139
+ if (options.adapter === 'alias') aliases[provider] = options.of;
140
+ }
141
+ return aliases;
142
+ }
143
+
144
+ export interface ComposedQuota {
145
+ registry: AdapterRegistry;
146
+ aliases: Record<string, string>;
147
+ footerWindows: FooterWindows | undefined;
148
+ }
149
+
150
+ /**
151
+ * The full quota surface in one place: built-in adapters, settings-bound adapters
152
+ * and aliases wired exactly as the Pi extension does. Settings are deliberate user
153
+ * bindings, so they take precedence over defaults; AdapterRegistry still detects an
154
+ * ambiguous duplicate among explicit adapters. Alias providers share their target's
155
+ * account: one hop only, and a dangling alias stays visible as an error instead of
156
+ * vanishing silently.
157
+ */
158
+ export function composeQuotaRegistry(input: {
159
+ settings?: unknown;
160
+ adapters?: readonly QuotaAdapter[];
161
+ aliases?: Record<string, string>;
162
+ } = {}): ComposedQuota {
163
+ const settingsAdapters = input.settings === undefined ? [] : loadQuotaAdaptersFromSettings(input.settings);
164
+ const explicitAdapters = [...settingsAdapters, ...(input.adapters ?? [])];
165
+ const aliases = { ...loadQuotaAliasesFromSettings(input.settings ?? {}), ...(input.aliases ?? {}) };
166
+ const explicitProviderIds = new Set([...explicitAdapters.map(adapter => adapter.provider), ...Object.keys(aliases)]);
167
+ const registry = new AdapterRegistry([
168
+ codexAdapter, kimiAdapter, openCodeGoAdapter, createDeepSeekAdapter('deepseek'),
169
+ ].filter(adapter => !explicitProviderIds.has(adapter.provider)).concat(explicitAdapters));
170
+ for (const [alias, targetId] of Object.entries(aliases)) {
171
+ const target = aliases[targetId] === undefined ? registry.get(targetId) : undefined;
172
+ registry.register(target ? createAliasAdapter(alias, target) : createDanglingAliasAdapter(alias));
173
+ }
174
+ return {
175
+ registry,
176
+ aliases,
177
+ footerWindows: input.settings === undefined ? undefined : loadQuotaFooterWindows(input.settings),
178
+ };
179
+ }