dsh-subscriptions 0.1.5 → 0.2.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 +31 -5
- package/lib/auth/rpc.d.ts +73 -2
- package/lib/auth/rpc.js +53 -3
- package/lib/client/ExternalUsageCards.js +1 -1
- package/lib/client/OllamaCard.d.ts +13 -0
- package/lib/client/OllamaCard.js +76 -0
- package/lib/client/OllamaSettingsManager.d.ts +24 -0
- package/lib/client/OllamaSettingsManager.js +246 -0
- package/lib/client/ProviderModelEditor.js +4 -2
- package/lib/client/SubscriptionUsageBadge.d.ts +1 -1
- package/lib/client/SubscriptionUsageBadge.js +3 -2
- package/lib/client/SubscriptionsSection.d.ts +2 -0
- package/lib/client/SubscriptionsSection.js +2 -1
- package/lib/client/UsageMeter.js +11 -7
- package/lib/client/locales.d.ts +68 -0
- package/lib/client/locales.js +77 -0
- package/lib/client/ollama-draft.d.ts +229 -0
- package/lib/client/ollama-draft.js +246 -0
- package/lib/client/usage-pace.d.ts +7 -0
- package/lib/client/usage-pace.js +15 -0
- package/lib/client.js +1385 -244
- package/lib/client.js.map +1 -1
- package/lib/index.d.ts +25 -0
- package/lib/index.js +3191 -2133
- package/lib/provider-settings.d.ts +2 -1
- package/lib/provider-settings.js +6 -4
- package/lib/providers/claude.d.ts +14 -1
- package/lib/providers/claude.js +19 -7
- package/lib/providers/common.d.ts +14 -0
- package/lib/providers/common.js +48 -0
- package/lib/providers/external-usage-controller.d.ts +9 -2
- package/lib/providers/external-usage-controller.js +19 -5
- package/lib/providers/external-usage.d.ts +8 -0
- package/lib/providers/external-usage.js +92 -8
- package/lib/providers/ollama-settings.d.ts +90 -0
- package/lib/providers/ollama-settings.js +197 -0
- package/lib/providers/ollama-web.d.ts +46 -0
- package/lib/providers/ollama-web.js +213 -0
- package/lib/providers/ollama.d.ts +107 -0
- package/lib/providers/ollama.js +545 -0
- package/package.json +1 -1
|
@@ -32,7 +32,8 @@ export declare class ProviderSettingsStore {
|
|
|
32
32
|
constructor(path?: string);
|
|
33
33
|
get(provider: ProviderId): ProviderPreferences;
|
|
34
34
|
visible(provider: ProviderId, model: string): boolean;
|
|
35
|
-
|
|
35
|
+
/** The user's local context-window override for one model; absent follows the provider. */
|
|
36
|
+
contextWindow(model: string, provider?: ProviderId): number | undefined;
|
|
36
37
|
/** Creation-time policy survives restarts and never changes an existing session. */
|
|
37
38
|
toolEnabled(provider: ProviderId, tool: SubscriptionTool, createdAt?: number): boolean;
|
|
38
39
|
set(provider: ProviderId, input: unknown): Promise<void>;
|
package/lib/provider-settings.js
CHANGED
|
@@ -51,8 +51,9 @@ export function validatePreferences(provider, input) {
|
|
|
51
51
|
result.visibleModels = [...new Set(raw.visibleModels)];
|
|
52
52
|
}
|
|
53
53
|
if (raw.contextWindows !== undefined) {
|
|
54
|
-
if (provider !== 'codex')
|
|
55
|
-
throw new Error('context window overrides are
|
|
54
|
+
if (provider !== 'codex' && provider !== 'claude') {
|
|
55
|
+
throw new Error('context window overrides are supported only for Codex and Claude');
|
|
56
|
+
}
|
|
56
57
|
if (!raw.contextWindows || typeof raw.contextWindows !== 'object' || Array.isArray(raw.contextWindows)) {
|
|
57
58
|
throw new Error('contextWindows must be a model-to-token map');
|
|
58
59
|
}
|
|
@@ -110,8 +111,9 @@ export class ProviderSettingsStore {
|
|
|
110
111
|
visible(provider, model) {
|
|
111
112
|
return this.current.providers[provider]?.visibleModels?.includes(model) ?? true;
|
|
112
113
|
}
|
|
113
|
-
|
|
114
|
-
|
|
114
|
+
/** The user's local context-window override for one model; absent follows the provider. */
|
|
115
|
+
contextWindow(model, provider = 'codex') {
|
|
116
|
+
const windows = this.current.providers[provider]?.contextWindows;
|
|
115
117
|
return windows && Object.hasOwn(windows, model) ? windows[model] : undefined;
|
|
116
118
|
}
|
|
117
119
|
/** Creation-time policy survives restarts and never changes an existing session. */
|
|
@@ -127,6 +127,12 @@ export interface ClaudeAdapterOptions {
|
|
|
127
127
|
* the provider's own default.
|
|
128
128
|
*/
|
|
129
129
|
defaultEffortOf?: (model: string) => string | undefined;
|
|
130
|
+
/**
|
|
131
|
+
* The user's local context-window override for one model (the Settings
|
|
132
|
+
* editor). Returns undefined to follow the advertised window. The result
|
|
133
|
+
* is capped at that window: Anthropic advertises no separate maximum.
|
|
134
|
+
*/
|
|
135
|
+
contextWindowOf?: (model: string) => number | undefined;
|
|
130
136
|
/** Resolves the Claude Code version to present (npm-backed); absent means the local floor. */
|
|
131
137
|
resolveCliVersion?: () => Promise<string>;
|
|
132
138
|
/** Cache breakpoint lifetime; absent means the API's five-minute default. */
|
|
@@ -183,7 +189,14 @@ export declare class ClaudeAdapter extends LlmAdapter {
|
|
|
183
189
|
listOwnModels(provider: string, account?: string, signal?: AbortSignal): Promise<readonly LlmModelInfo[]>;
|
|
184
190
|
resolveModel(provider: string, model: string): Promise<LlmResolvedModelInfo>;
|
|
185
191
|
/** Capability resolution of the provider's own models (the pool resolves members here). */
|
|
186
|
-
resolveOwnModel(provider: string, model: string): Promise<LlmResolvedModelInfo>;
|
|
192
|
+
resolveOwnModel(provider: string, model: string, account?: string): Promise<LlmResolvedModelInfo>;
|
|
193
|
+
/** The advertised window is both the default and the ceiling for a local override. */
|
|
194
|
+
private contextLimitsFor;
|
|
195
|
+
/** Account-specific bounds the Settings editor shows next to a model's override input. */
|
|
196
|
+
contextLimits(model: string, account?: string): Promise<{
|
|
197
|
+
default: number;
|
|
198
|
+
max: number;
|
|
199
|
+
}>;
|
|
187
200
|
stream(options: GenerateOptions): AsyncIterable<StreamChunk>;
|
|
188
201
|
/** Pool seam: stream through one specific account instead of the default. */
|
|
189
202
|
streamAccount(options: GenerateOptions, account: string): AsyncIterable<StreamChunk>;
|
package/lib/providers/claude.js
CHANGED
|
@@ -600,10 +600,14 @@ export class ClaudeAdapter extends LlmAdapter {
|
|
|
600
600
|
}
|
|
601
601
|
return cache;
|
|
602
602
|
}
|
|
603
|
-
async discovered(model) {
|
|
603
|
+
async discovered(model, account) {
|
|
604
604
|
if (!this.options.discovery)
|
|
605
605
|
return undefined;
|
|
606
|
-
|
|
606
|
+
// A named account is looked up alone: its window must not be borrowed
|
|
607
|
+
// from another account's catalog.
|
|
608
|
+
const accounts = account === undefined
|
|
609
|
+
? (await this.options.tokens.list()).map(entry => entry.key)
|
|
610
|
+
: [account];
|
|
607
611
|
return discoverAcrossAccounts(accounts, async (account) => {
|
|
608
612
|
const catalog = await this.catalogFor(account);
|
|
609
613
|
const models = await catalog.resolve(() => this.fetchCatalog(account));
|
|
@@ -674,22 +678,30 @@ export class ClaudeAdapter extends LlmAdapter {
|
|
|
674
678
|
return this.resolveOwnModel(provider, model);
|
|
675
679
|
}
|
|
676
680
|
/** Capability resolution of the provider's own models (the pool resolves members here). */
|
|
677
|
-
async resolveOwnModel(provider, model) {
|
|
678
|
-
const disc = await this.discovered(model);
|
|
681
|
+
async resolveOwnModel(provider, model, account) {
|
|
682
|
+
const disc = await this.discovered(model, account);
|
|
679
683
|
const configured = this.options.models.find(entry => entry.id === model);
|
|
684
|
+
const limits = this.contextLimitsFor(disc, configured);
|
|
680
685
|
const reasoning = mergeReasoning(this.options.defaultEffortOf?.(model), disc?.reasoning);
|
|
681
686
|
return {
|
|
682
687
|
provider,
|
|
683
688
|
id: model,
|
|
684
689
|
name: disc?.name ?? configured?.name ?? model,
|
|
685
690
|
inputModalities: configured?.inputModalities ?? CLAUDE_MODALITIES,
|
|
686
|
-
context: {
|
|
687
|
-
contextWindow: disc?.contextWindow ?? configured?.contextWindow ?? CLAUDE_CONTEXT_WINDOW,
|
|
688
|
-
},
|
|
691
|
+
context: { contextWindow: Math.min(this.options.contextWindowOf?.(model) ?? limits.default, limits.max) },
|
|
689
692
|
defaultMaxTokens: claudeMaxTokens(configured, disc),
|
|
690
693
|
...(reasoning === undefined ? {} : { reasoning }),
|
|
691
694
|
};
|
|
692
695
|
}
|
|
696
|
+
/** The advertised window is both the default and the ceiling for a local override. */
|
|
697
|
+
contextLimitsFor(disc, configured) {
|
|
698
|
+
const advertised = disc?.contextWindow ?? configured?.contextWindow ?? CLAUDE_CONTEXT_WINDOW;
|
|
699
|
+
return { default: advertised, max: advertised };
|
|
700
|
+
}
|
|
701
|
+
/** Account-specific bounds the Settings editor shows next to a model's override input. */
|
|
702
|
+
async contextLimits(model, account) {
|
|
703
|
+
return this.contextLimitsFor(await this.discovered(model, account), this.options.models.find(entry => entry.id === model));
|
|
704
|
+
}
|
|
693
705
|
async *stream(options) {
|
|
694
706
|
const pool = this.options.pool?.();
|
|
695
707
|
if (pool !== undefined && await pool.owns(options.provider, options.model)) {
|
|
@@ -46,6 +46,13 @@ export declare function validateModels(models: readonly ModelEntry[], label: str
|
|
|
46
46
|
* @throws SyntaxError with the local origin and a fixed omission marker.
|
|
47
47
|
*/
|
|
48
48
|
export declare function parseProviderJson<T = unknown>(input: Response | string, origin: string): Promise<T>;
|
|
49
|
+
/**
|
|
50
|
+
* Read a bounded JSON body; oversized or malformed payloads fail loudly.
|
|
51
|
+
* Bytes accumulate per chunk and the stream is cancelled past the cap, so
|
|
52
|
+
* the bound limits memory even when the body has no end; a declared
|
|
53
|
+
* over-cap `content-length` fails before the first read.
|
|
54
|
+
*/
|
|
55
|
+
export declare function readBoundedJson(response: Response, label: string, maxBytes: number): Promise<unknown>;
|
|
49
56
|
/** Optional per-call hooks {@link httpLlmError} uses to read a rate-limit window. */
|
|
50
57
|
export interface HttpLlmErrorOptions {
|
|
51
58
|
/**
|
|
@@ -212,6 +219,13 @@ export interface UsageWindow {
|
|
|
212
219
|
windowDurationMs?: number;
|
|
213
220
|
/** Provider-verified fixed interval; permits deriving start from end and duration. */
|
|
214
221
|
fixedWindow?: boolean;
|
|
222
|
+
/**
|
|
223
|
+
* Display-only start the reader assumed (kind or label based, clamped
|
|
224
|
+
* to now). The time marker may ride it; pace and color must never —
|
|
225
|
+
* it is not evidence of when the window opened. Mutually exclusive
|
|
226
|
+
* with `startsAt`.
|
|
227
|
+
*/
|
|
228
|
+
displayStartsAt?: number;
|
|
215
229
|
}
|
|
216
230
|
/** One banked reset credit (Codex), or one grant containing resets (Claude). */
|
|
217
231
|
export interface ResetCredit {
|
package/lib/providers/common.js
CHANGED
|
@@ -83,6 +83,54 @@ export async function parseProviderJson(input, origin) {
|
|
|
83
83
|
throw new SyntaxError(providerDiagnostic(`${origin}: invalid JSON`, true));
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* Read a bounded JSON body; oversized or malformed payloads fail loudly.
|
|
88
|
+
* Bytes accumulate per chunk and the stream is cancelled past the cap, so
|
|
89
|
+
* the bound limits memory even when the body has no end; a declared
|
|
90
|
+
* over-cap `content-length` fails before the first read.
|
|
91
|
+
*/
|
|
92
|
+
export async function readBoundedJson(response, label, maxBytes) {
|
|
93
|
+
const declared = Number(response.headers.get('content-length'));
|
|
94
|
+
if (Number.isFinite(declared) && declared > maxBytes) {
|
|
95
|
+
// Stop the socket too: the headers arrived, but the body must never flow.
|
|
96
|
+
await response.body?.cancel().catch(() => undefined);
|
|
97
|
+
throw new Error(`${label} response exceeds ${String(maxBytes)} bytes`);
|
|
98
|
+
}
|
|
99
|
+
if (response.body === null)
|
|
100
|
+
throw new Error(`${label} response has no body`);
|
|
101
|
+
const reader = response.body.getReader();
|
|
102
|
+
const decoder = new TextDecoder();
|
|
103
|
+
let bytes = 0;
|
|
104
|
+
let text = '';
|
|
105
|
+
try {
|
|
106
|
+
for (;;) {
|
|
107
|
+
const chunk = await reader.read();
|
|
108
|
+
if (chunk.done)
|
|
109
|
+
break;
|
|
110
|
+
bytes += chunk.value.byteLength;
|
|
111
|
+
if (bytes > maxBytes) {
|
|
112
|
+
// Cancel before throwing so the live stream does not linger behind
|
|
113
|
+
// a rejection the caller already stopped listening to.
|
|
114
|
+
await reader.cancel().catch(() => undefined);
|
|
115
|
+
throw new Error(`${label} response exceeds ${String(maxBytes)} bytes`);
|
|
116
|
+
}
|
|
117
|
+
text += decoder.decode(chunk.value, { stream: true });
|
|
118
|
+
}
|
|
119
|
+
text += decoder.decode();
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
try {
|
|
123
|
+
reader.releaseLock();
|
|
124
|
+
}
|
|
125
|
+
catch { /* best effort */ }
|
|
126
|
+
}
|
|
127
|
+
try {
|
|
128
|
+
return JSON.parse(text);
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
throw new Error(`${label} response is not JSON`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
86
134
|
/**
|
|
87
135
|
* Build an LlmError from a non-2xx provider response, mapping the status to a
|
|
88
136
|
* stable code and, for a rate-limited request, the disclosed reset instant to
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ProviderUsage } from './common.js';
|
|
2
2
|
/** Usage-only sources whose model routes are supplied by the DSH base install. */
|
|
3
|
-
export type ExternalUsageSource = 'opencode-go' | 'kimi-code' | 'minimax' | 'minimax-cn';
|
|
3
|
+
export type ExternalUsageSource = 'opencode-go' | 'kimi-code' | 'minimax' | 'minimax-cn' | 'ollama-cloud';
|
|
4
4
|
export declare const EXTERNAL_USAGE_SOURCES: readonly ExternalUsageSource[];
|
|
5
5
|
export interface ExternalUsageStatus {
|
|
6
6
|
configured: boolean;
|
|
@@ -8,11 +8,18 @@ export interface ExternalUsageStatus {
|
|
|
8
8
|
type ResolveCredential = (name: string) => Promise<{
|
|
9
9
|
value: string;
|
|
10
10
|
} | undefined>;
|
|
11
|
+
/**
|
|
12
|
+
* A setting the controller reads per call. The function form keeps the Ollama
|
|
13
|
+
* reference and base URL live: the Settings card can change them after the
|
|
14
|
+
* controller was constructed.
|
|
15
|
+
*/
|
|
16
|
+
type LiveValue = string | (() => string);
|
|
11
17
|
export declare class ExternalUsageController {
|
|
12
18
|
private readonly resolveCredential;
|
|
13
19
|
private readonly http;
|
|
14
20
|
private readonly refs;
|
|
15
|
-
|
|
21
|
+
private readonly ollamaBaseURL;
|
|
22
|
+
constructor(resolveCredential: ResolveCredential, http?: typeof fetch, refs?: Partial<Record<ExternalUsageSource, LiveValue>>, ollamaBaseURL?: LiveValue);
|
|
16
23
|
status(): Promise<Record<ExternalUsageSource, ExternalUsageStatus>>;
|
|
17
24
|
usage(source: ExternalUsageSource, signal?: AbortSignal): Promise<ProviderUsage>;
|
|
18
25
|
}
|
|
@@ -1,35 +1,49 @@
|
|
|
1
1
|
import { fetchMiniMaxUsage } from './minimax-usage.js';
|
|
2
|
-
import { fetchKimiCodeUsage, fetchOpenCodeGoUsage } from './external-usage.js';
|
|
3
|
-
|
|
2
|
+
import { fetchKimiCodeUsage, fetchOllamaUsage, fetchOpenCodeGoUsage } from './external-usage.js';
|
|
3
|
+
import { DEFAULT_OLLAMA_API_KEY_REF, DEFAULT_OLLAMA_BASE_URL } from './ollama.js';
|
|
4
|
+
export const EXTERNAL_USAGE_SOURCES = ['opencode-go', 'kimi-code', 'minimax', 'minimax-cn', 'ollama-cloud'];
|
|
5
|
+
function valueOf(value) {
|
|
6
|
+
return typeof value === 'function' ? value() : value;
|
|
7
|
+
}
|
|
4
8
|
const DEFAULT_REFS = {
|
|
5
9
|
'opencode-go': 'OPENCODE_GO_API_KEY',
|
|
6
10
|
'kimi-code': 'KIMI_CODING_API_KEY',
|
|
7
11
|
'minimax': 'MINIMAX_API_KEY',
|
|
8
12
|
'minimax-cn': 'MINIMAX_CN_API_KEY',
|
|
13
|
+
'ollama-cloud': DEFAULT_OLLAMA_API_KEY_REF,
|
|
9
14
|
};
|
|
10
15
|
export class ExternalUsageController {
|
|
11
16
|
resolveCredential;
|
|
12
17
|
http;
|
|
13
18
|
refs;
|
|
14
|
-
|
|
19
|
+
ollamaBaseURL;
|
|
20
|
+
constructor(resolveCredential, http = fetch, refs = {}, ollamaBaseURL = DEFAULT_OLLAMA_BASE_URL) {
|
|
15
21
|
this.resolveCredential = resolveCredential;
|
|
16
22
|
this.http = http;
|
|
17
23
|
this.refs = { ...DEFAULT_REFS, ...refs };
|
|
24
|
+
this.ollamaBaseURL = ollamaBaseURL;
|
|
18
25
|
}
|
|
19
26
|
async status() {
|
|
20
27
|
const values = await Promise.all(EXTERNAL_USAGE_SOURCES.map(async (source) => {
|
|
21
|
-
const value = await this.resolveCredential(this.refs[source]);
|
|
28
|
+
const value = await this.resolveCredential(valueOf(this.refs[source]));
|
|
22
29
|
return typeof value?.value === 'string' && value.value.length > 0;
|
|
23
30
|
}));
|
|
24
31
|
return Object.fromEntries(EXTERNAL_USAGE_SOURCES.map((source, i) => [source, { configured: values[i] ?? false }]));
|
|
25
32
|
}
|
|
26
33
|
async usage(source, signal) {
|
|
27
|
-
|
|
34
|
+
// Snapshot the endpoint facts before the first await: a save that
|
|
35
|
+
// switches the ref or host mid-flight must not send the old
|
|
36
|
+
// reference's key to the new host.
|
|
37
|
+
const ref = valueOf(this.refs[source]);
|
|
38
|
+
const ollamaBase = valueOf(this.ollamaBaseURL);
|
|
39
|
+
const credential = await this.resolveCredential(ref);
|
|
28
40
|
if (credential?.value === undefined || credential.value.length === 0) {
|
|
29
41
|
throw new Error(`${source} API key is not configured`);
|
|
30
42
|
}
|
|
31
43
|
if (source === 'minimax' || source === 'minimax-cn')
|
|
32
44
|
return fetchMiniMaxUsage(credential.value, source === 'minimax-cn' ? 'cn' : 'global', this.http, signal);
|
|
45
|
+
if (source === 'ollama-cloud')
|
|
46
|
+
return fetchOllamaUsage(credential.value, ollamaBase, this.http, signal);
|
|
33
47
|
return source === 'opencode-go'
|
|
34
48
|
? fetchOpenCodeGoUsage(credential.value, this.http, signal)
|
|
35
49
|
: fetchKimiCodeUsage(credential.value, this.http, signal);
|
|
@@ -5,4 +5,12 @@ type HttpFetch = typeof fetch;
|
|
|
5
5
|
export declare function fetchOpenCodeGoUsage(apiKey: string, http?: HttpFetch, signal?: AbortSignal): Promise<ProviderUsage>;
|
|
6
6
|
/** Read official CLI count windows and retain compatibility with older ratio pools. */
|
|
7
7
|
export declare function fetchKimiCodeUsage(apiKey: string, http?: HttpFetch, signal?: AbortSignal): Promise<ProviderUsage>;
|
|
8
|
+
/**
|
|
9
|
+
* Read Ollama Cloud quota from `GET {base}/balance`. The Cloud has no
|
|
10
|
+
* `limits` object: quota is the included allowance against its remaining
|
|
11
|
+
* balance (`included.allowance_usd`, `included.balance_usd`) over the
|
|
12
|
+
* reported period. A 404 means a self-hosted endpoint without the
|
|
13
|
+
* surface: not an error, the card renders an unsupported note instead.
|
|
14
|
+
*/
|
|
15
|
+
export declare function fetchOllamaUsage(apiKey: string, baseURL: string, http?: HttpFetch, signal?: AbortSignal): Promise<ProviderUsage>;
|
|
8
16
|
export {};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** Usage readers for subscriptions whose model adapters are built into DSH. */
|
|
2
|
-
import {
|
|
2
|
+
import { attributionHeaders } from '@deepseek-ai/dsh-llm';
|
|
3
|
+
import { parseProviderJson, readBoundedJson } from './common.js';
|
|
3
4
|
function numeric(value) {
|
|
4
5
|
const n = typeof value === 'string' && value.trim() !== '' ? Number(value) : value;
|
|
5
6
|
return typeof n === 'number' && Number.isFinite(n) ? n : undefined;
|
|
@@ -59,6 +60,12 @@ export async function fetchOpenCodeGoUsage(apiKey, http = fetch, signal) {
|
|
|
59
60
|
if (usedPercent === undefined)
|
|
60
61
|
continue;
|
|
61
62
|
const resetsAt = resetTime(value?.resetsAt);
|
|
63
|
+
// Monthly has no interval fields. Both key providers' monthly pools
|
|
64
|
+
// read the same display assumption: a 30-day span back from the
|
|
65
|
+
// reset, clamped to now. Only the marker may ride it — pace never.
|
|
66
|
+
const monthlyDisplay = field === 'monthly' && resetsAt !== undefined
|
|
67
|
+
? { displayStartsAt: Math.min(resetsAt - 30 * 86_400_000, Date.now()) }
|
|
68
|
+
: {};
|
|
62
69
|
windows.push({
|
|
63
70
|
kind,
|
|
64
71
|
...(scope === undefined ? {} : { scope }),
|
|
@@ -67,6 +74,7 @@ export async function fetchOpenCodeGoUsage(apiKey, http = fetch, signal) {
|
|
|
67
74
|
...(field === 'rolling' ? { fixedWindow: true, windowDurationMs: 18_000_000 }
|
|
68
75
|
: field === 'weekly' ? { fixedWindow: true, windowDurationMs: 604_800_000 } : {}),
|
|
69
76
|
...(resetsAt === undefined ? {} : { resetsAt }),
|
|
77
|
+
...monthlyDisplay,
|
|
70
78
|
});
|
|
71
79
|
}
|
|
72
80
|
if (windows.length === 0)
|
|
@@ -79,11 +87,13 @@ export async function fetchKimiCodeUsage(apiKey, http = fetch, signal) {
|
|
|
79
87
|
if (body === undefined)
|
|
80
88
|
throw new Error('Kimi Code usage response is not an object');
|
|
81
89
|
const modern = [];
|
|
90
|
+
// Only the top-level `usage` object is the summary the official CLI labels
|
|
91
|
+
// "Weekly limit"; an anonymous `limits[]` entry is "Limit #n" there.
|
|
82
92
|
const rows = [
|
|
83
|
-
...(record(body.usage) ? [{ detail: body.usage }] : []),
|
|
84
|
-
...(Array.isArray(body.limits) ? body.limits : []),
|
|
93
|
+
...(record(body.usage) ? [{ raw: { detail: body.usage }, summary: true }] : []),
|
|
94
|
+
...(Array.isArray(body.limits) ? body.limits.map(raw => ({ raw, summary: false })) : []),
|
|
85
95
|
];
|
|
86
|
-
for (const raw of rows) {
|
|
96
|
+
for (const { raw, summary } of rows) {
|
|
87
97
|
const item = record(raw);
|
|
88
98
|
if (!item)
|
|
89
99
|
continue;
|
|
@@ -103,16 +113,34 @@ export async function fetchKimiCodeUsage(apiKey, http = fetch, signal) {
|
|
|
103
113
|
: unit.includes('DAY') ? 86_400_000 : unit.includes('SECOND') ? 1000 : undefined;
|
|
104
114
|
const durationMs = duration === undefined || duration <= 0 || multiplier === undefined
|
|
105
115
|
? undefined : duration * multiplier;
|
|
106
|
-
const kind = durationMs === 18_000_000 ? 'session'
|
|
116
|
+
const kind = durationMs === 18_000_000 ? 'session'
|
|
117
|
+
: durationMs === 604_800_000 ? 'weekly'
|
|
118
|
+
// No period fields at all: the official CLI labels the top-level
|
|
119
|
+
// summary the weekly limit, so use that kind rather than 'other'.
|
|
120
|
+
: durationMs === undefined && unit === '' ? 'weekly' : 'other';
|
|
107
121
|
const resetsAt = kimiReset(detail);
|
|
108
122
|
const scope = detail.name ?? detail.title ?? item.name ?? item.title;
|
|
123
|
+
// A vendor-declared span anchors the display marker at
|
|
124
|
+
// min(reset − span, now); pace stays unverified (rolling semantics
|
|
125
|
+
// are not established), so the assumed instant rides displayStartsAt.
|
|
126
|
+
// The top-level summary without a usable period (absent, zero, or no
|
|
127
|
+
// unit) is read as the weekly limit (above), so its marker assumes seven
|
|
128
|
+
// days. The official CLI
|
|
129
|
+
// gives only the label, not a span; the span rests on a live account
|
|
130
|
+
// whose summary reset matched `usages.limit_7d` to within a second.
|
|
131
|
+
// An anonymous `limits[]` entry gets no such assumption. Display only.
|
|
132
|
+
const markerSpan = durationMs ?? (summary && kind === 'weekly' ? 604_800_000 : undefined);
|
|
133
|
+
const displayStart = resetsAt !== undefined && markerSpan !== undefined
|
|
134
|
+
? { displayStartsAt: Math.min(resetsAt - markerSpan, Date.now()) }
|
|
135
|
+
: {};
|
|
109
136
|
modern.push({ kind, usedPercent,
|
|
110
137
|
...(typeof scope === 'string' ? { scope } : {}),
|
|
111
138
|
...(resetsAt === undefined ? {} : { resetsAt }),
|
|
112
139
|
...(durationMs === undefined ? {} : { windowDurationMs: durationMs }),
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
...(
|
|
140
|
+
// A whole seven-day bucket reset, not a rolling span, only where the
|
|
141
|
+
// endpoint actually said 7 DAY.
|
|
142
|
+
...(durationMs === 604_800_000 ? { fixedWindow: true } : {}),
|
|
143
|
+
...displayStart,
|
|
116
144
|
});
|
|
117
145
|
}
|
|
118
146
|
if (modern.length)
|
|
@@ -131,13 +159,69 @@ export async function fetchKimiCodeUsage(apiKey, http = fetch, signal) {
|
|
|
131
159
|
if (typeof ratio !== 'number' || !Number.isFinite(ratio) || ratio < 0 || ratio > 1)
|
|
132
160
|
continue;
|
|
133
161
|
const resetsAt = resetTime(value?.reset_time ?? value?.resetAt);
|
|
162
|
+
// The monthly total resets on the subscription's cycle, not the
|
|
163
|
+
// calendar month; the display marker assumes ~30 days back. It only
|
|
164
|
+
// moves the marker — pace never rides it.
|
|
165
|
+
const monthlyDisplay = field === 'limit_month_total' && resetsAt !== undefined
|
|
166
|
+
? { displayStartsAt: Math.min(resetsAt - 30 * 86_400_000, Date.now()) }
|
|
167
|
+
: {};
|
|
134
168
|
windows.push({
|
|
135
169
|
kind,
|
|
136
170
|
...(scope === undefined ? {} : { scope }),
|
|
137
171
|
usedPercent: ratio * 100,
|
|
138
172
|
...(field === 'limit_7d' ? { fixedWindow: true, windowDurationMs: 604_800_000 } : {}),
|
|
139
173
|
...(resetsAt === undefined ? {} : { resetsAt }),
|
|
174
|
+
...monthlyDisplay,
|
|
140
175
|
});
|
|
141
176
|
}
|
|
142
177
|
return { supported: true, windows, plan: 'Kimi Code' };
|
|
143
178
|
}
|
|
179
|
+
/**
|
|
180
|
+
* Read Ollama Cloud quota from `GET {base}/balance`. The Cloud has no
|
|
181
|
+
* `limits` object: quota is the included allowance against its remaining
|
|
182
|
+
* balance (`included.allowance_usd`, `included.balance_usd`) over the
|
|
183
|
+
* reported period. A 404 means a self-hosted endpoint without the
|
|
184
|
+
* surface: not an error, the card renders an unsupported note instead.
|
|
185
|
+
*/
|
|
186
|
+
export async function fetchOllamaUsage(apiKey, baseURL, http = fetch, signal) {
|
|
187
|
+
const combined = signal === undefined
|
|
188
|
+
? AbortSignal.timeout(15_000)
|
|
189
|
+
: AbortSignal.any([AbortSignal.timeout(15_000), signal]);
|
|
190
|
+
const response = await http(`${baseURL.replace(/\/+$/, '')}/balance`, {
|
|
191
|
+
headers: {
|
|
192
|
+
accept: 'application/json',
|
|
193
|
+
authorization: `Bearer ${apiKey}`,
|
|
194
|
+
...attributionHeaders(),
|
|
195
|
+
},
|
|
196
|
+
redirect: 'error',
|
|
197
|
+
signal: combined,
|
|
198
|
+
});
|
|
199
|
+
if (signal?.aborted)
|
|
200
|
+
throw signal.reason;
|
|
201
|
+
if (response.status === 404)
|
|
202
|
+
return { supported: false };
|
|
203
|
+
if (!response.ok)
|
|
204
|
+
throw new Error(`Usage endpoint returned HTTP ${response.status}`);
|
|
205
|
+
const body = record(await readBoundedJson(response, 'usage', 1 * 1024 * 1024));
|
|
206
|
+
const included = record(body?.included);
|
|
207
|
+
const allowance = numeric(included?.allowance_usd);
|
|
208
|
+
const balance = numeric(included?.balance_usd);
|
|
209
|
+
if (included === undefined || allowance === undefined || allowance <= 0 || balance === undefined) {
|
|
210
|
+
throw new Error('Ollama balance response has no usable included allowance');
|
|
211
|
+
}
|
|
212
|
+
const period = record(included.period);
|
|
213
|
+
const resetsAt = period === undefined ? undefined : resetTime(period.until);
|
|
214
|
+
const startsAt = period === undefined ? undefined : resetTime(period.from);
|
|
215
|
+
return {
|
|
216
|
+
supported: true,
|
|
217
|
+
windows: [{
|
|
218
|
+
kind: 'other',
|
|
219
|
+
scope: 'Credits',
|
|
220
|
+
// An overdrawn balance stays visible instead of dropping the window.
|
|
221
|
+
usedPercent: (allowance - balance) / allowance * 100,
|
|
222
|
+
...(resetsAt === undefined ? {} : { resetsAt }),
|
|
223
|
+
...(startsAt === undefined ? {} : { startsAt }),
|
|
224
|
+
}],
|
|
225
|
+
plan: 'Ollama Cloud',
|
|
226
|
+
};
|
|
227
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One model the Ollama Cloud route serves. The route ships no catalog of its
|
|
3
|
+
* own: this list is the catalog, in the order the picker shows it. Adoption
|
|
4
|
+
* fills the capability fields from the endpoint's `/api/tags` + `/api/show`
|
|
5
|
+
* reply; every field except `id` is optional so a hand-written row still
|
|
6
|
+
* works.
|
|
7
|
+
*/
|
|
8
|
+
export interface OllamaModelSetting {
|
|
9
|
+
/** Wire model id; must be non-empty and unique within the list. */
|
|
10
|
+
id: string;
|
|
11
|
+
/** Selector label; defaults to the id. */
|
|
12
|
+
name?: string;
|
|
13
|
+
/** Known combined request/response context capacity in tokens. */
|
|
14
|
+
contextWindow?: number;
|
|
15
|
+
/**
|
|
16
|
+
* The user's local context-window choice. Unlike `contextWindow`, which
|
|
17
|
+
* snapshots what the endpoint advertised, this records only an explicit
|
|
18
|
+
* edit: absent follows the endpoint. The runtime caps it at the advertised
|
|
19
|
+
* window and it never widens the API's real capacity.
|
|
20
|
+
*/
|
|
21
|
+
contextOverride?: number;
|
|
22
|
+
/** Per-request output cap; defaults to the route's fallback. */
|
|
23
|
+
maxTokens?: number;
|
|
24
|
+
/** The endpoint advertised the `vision` capability. */
|
|
25
|
+
vision?: boolean;
|
|
26
|
+
/** The endpoint advertised the `thinking` capability. */
|
|
27
|
+
thinking?: boolean;
|
|
28
|
+
/** Advertised reasoning efforts, in picker order; empty means non-reasoning. */
|
|
29
|
+
efforts?: string[];
|
|
30
|
+
/** Effort preselected when the session picks none; must be one of `efforts`. */
|
|
31
|
+
defaultEffort?: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Durable, non-secret Ollama Cloud configuration. Every field is optional and
|
|
35
|
+
* its absence means "no override": the effective value then falls back to the
|
|
36
|
+
* plugin config and, last, to the built-in default. The API key itself never
|
|
37
|
+
* appears here; `apiKeyRef` only names a DSH credential.
|
|
38
|
+
*/
|
|
39
|
+
export interface OllamaSettings {
|
|
40
|
+
/** The route's catalog, in picker order. `[]` hides every model. */
|
|
41
|
+
models?: OllamaModelSetting[];
|
|
42
|
+
/** Register the web search/fetch providers; the config value is the default. */
|
|
43
|
+
web?: boolean;
|
|
44
|
+
/** Native base URL; the config value is the default. */
|
|
45
|
+
baseURL?: string;
|
|
46
|
+
/** Credential reference naming the API key; the config value is the default. */
|
|
47
|
+
apiKeyRef?: string;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Accept only an absolute http(s) base URL with no credentials, query, or
|
|
51
|
+
* fragment, and return its trimmed form. The route appends `/tags`, `/show`,
|
|
52
|
+
* and `/v1/chat/completions` to this value, so anything else would build a URL
|
|
53
|
+
* that cannot work — and a non-official host receives the referenced
|
|
54
|
+
* credential, which the dialog tells the user.
|
|
55
|
+
* @param value - the candidate URL.
|
|
56
|
+
* @returns the trimmed URL to store.
|
|
57
|
+
* @throws when the value is not a usable absolute http(s) URL.
|
|
58
|
+
*/
|
|
59
|
+
export declare function validateOllamaBaseURL(value: unknown): string;
|
|
60
|
+
/**
|
|
61
|
+
* Validate one settings document, dropping unknown keys and duplicate model
|
|
62
|
+
* ids (first wins, matching the Cursor store's visible-model handling).
|
|
63
|
+
* @param input - the parsed store document or an RPC payload.
|
|
64
|
+
* @throws when a known field has the wrong type or an unusable value.
|
|
65
|
+
*/
|
|
66
|
+
export declare function validateOllamaSettings(input: unknown): OllamaSettings;
|
|
67
|
+
/**
|
|
68
|
+
* Durable Ollama Cloud configuration under the DSH home directory. Writes are
|
|
69
|
+
* serialized and atomic (temp file with mode 0600, then rename), so a crash
|
|
70
|
+
* never leaves a half-written document and a concurrent writer cannot lose an
|
|
71
|
+
* earlier save.
|
|
72
|
+
*/
|
|
73
|
+
export declare class OllamaSettingsStore {
|
|
74
|
+
readonly path: string;
|
|
75
|
+
private readonly onCorrupt?;
|
|
76
|
+
private current;
|
|
77
|
+
private writes;
|
|
78
|
+
/** Set when the stored file existed but was unreadable or invalid. */
|
|
79
|
+
private corrupt;
|
|
80
|
+
constructor(path?: string, onCorrupt?: ((message: string) => void) | undefined);
|
|
81
|
+
/** Why the stored file is ignored, when it is. */
|
|
82
|
+
corruption(): string | undefined;
|
|
83
|
+
get(): OllamaSettings;
|
|
84
|
+
/** The stored catalog, or `undefined` when the store carries no override. */
|
|
85
|
+
models(): readonly OllamaModelSetting[] | undefined;
|
|
86
|
+
web(): boolean | undefined;
|
|
87
|
+
baseURL(): string | undefined;
|
|
88
|
+
apiKeyRef(): string | undefined;
|
|
89
|
+
set(input: unknown): Promise<void>;
|
|
90
|
+
}
|