dsh-plugin-subscription-usage 0.1.0 → 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 +18 -6
- package/lib/index.d.ts +1 -1
- package/lib/index.js +194 -5
- package/lib/providers/antigravity.d.ts +1 -1
- package/lib/providers/antigravity.js +1 -1
- package/lib/providers/grok.js +1 -1
- package/lib/providers/rate-limit.d.ts +1 -1
- package/lib/providers/rate-limit.js +1 -1
- package/lib/tools/registration.d.ts +1 -0
- package/lib/tools/registration.js +1 -0
- package/lib/tools/usage.d.ts +62 -0
- package/lib/tools/usage.js +89 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
A DeepSeek Harness plugin that uses your ChatGPT (Codex), Claude, Grok, GitHub Copilot and Google Antigravity subscriptions as LLM providers, and shows how fast you are using each limit.
|
|
4
4
|
|
|
5
|
-
It replaces `dsh-plugin-subscriptions`. Both register the `codex` adapter, so enabling both fails with `DUPLICATE_ADAPTER`. Disable the other one first.
|
|
6
|
-
|
|
7
5
|
## What you get
|
|
8
6
|
|
|
9
7
|
- **Models page.** Each subscription is a provider row where you sign in, manage accounts, choose models and enable tools.
|
|
@@ -11,6 +9,24 @@ It replaces `dsh-plugin-subscriptions`. Both register the `codex` adapter, so en
|
|
|
11
9
|
- **Composer gauge.** The current model's provider, plus any you pinned, shows under the composer.
|
|
12
10
|
- **API key usage.** Z.ai, Ollama Cloud and Kimi Code usage is read with an API key you paste on the Usage page. These providers are usage-only. The plugin does not route requests to them.
|
|
13
11
|
|
|
12
|
+
## Screenshots
|
|
13
|
+
|
|
14
|
+
Usage page in Settings, with pace forecasts and pins:
|
|
15
|
+
|
|
16
|
+

|
|
17
|
+
|
|
18
|
+
Models page, where each subscription is a provider row:
|
|
19
|
+
|
|
20
|
+

|
|
21
|
+
|
|
22
|
+
Usage pop-up in the composer on a new session:
|
|
23
|
+
|
|
24
|
+

|
|
25
|
+
|
|
26
|
+
The same pop-up inside a chat:
|
|
27
|
+
|
|
28
|
+

|
|
29
|
+
|
|
14
30
|
## Install
|
|
15
31
|
|
|
16
32
|
```bash
|
|
@@ -30,10 +46,6 @@ Restart DSH, then open Settings, Models.
|
|
|
30
46
|
- Copilot, Z.ai, Ollama Cloud and Kimi Code usage comes from endpoints the vendors do not document. They can change without notice, and the card then shows "Usage unavailable".
|
|
31
47
|
- A plain Moonshot key does not work for Kimi Code usage.
|
|
32
48
|
|
|
33
|
-
## Credits
|
|
34
|
-
|
|
35
|
-
Built on [V1ki/dsh-plugin-subscriptions](https://github.com/V1ki/dsh-plugin-subscriptions) (MIT). The sign-in, provider, pool and translation code under `src/auth`, `src/providers`, `src/translate` and `src/tools` comes from it. The Usage page, key providers and the Models UI are new.
|
|
36
|
-
|
|
37
49
|
## License
|
|
38
50
|
|
|
39
51
|
MIT
|
package/lib/index.d.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* RPC channel the web Settings page uses to run the logins. The token store
|
|
5
5
|
* lives at `~/.dsh/plugins/subscriptions/auth.json`; the channel registers only when
|
|
6
6
|
* a host `connection` service exists, so headless compositions load fine.
|
|
7
|
-
* @module dsh-plugin-
|
|
7
|
+
* @module dsh-plugin-subscription-usage
|
|
8
8
|
*/
|
|
9
9
|
import type { Context } from '@deepseek-ai/cordis';
|
|
10
10
|
import z from '@deepseek-ai/schemastery';
|
package/lib/index.js
CHANGED
|
@@ -8856,7 +8856,8 @@ var ImageAccountPool = class {
|
|
|
8856
8856
|
const TOOL_ALIASES = {
|
|
8857
8857
|
x_search: "dsh_subscriptions_x_search",
|
|
8858
8858
|
video_generate: "dsh_subscriptions_video_generate",
|
|
8859
|
-
image_generate: "dsh_subscriptions_image_generate"
|
|
8859
|
+
image_generate: "dsh_subscriptions_image_generate",
|
|
8860
|
+
subscription_usage: "dsh_subscriptions_subscription_usage"
|
|
8860
8861
|
};
|
|
8861
8862
|
/** Register a tool under its canonical name, then a plugin-scoped alias. */
|
|
8862
8863
|
function registerWithAlias(registry, definition, warn = (message) => console.warn(message)) {
|
|
@@ -8883,6 +8884,162 @@ function registerWithAlias(registry, definition, warn = (message) => console.war
|
|
|
8883
8884
|
}
|
|
8884
8885
|
}
|
|
8885
8886
|
|
|
8887
|
+
//#endregion
|
|
8888
|
+
//#region src/usage/pace.ts
|
|
8889
|
+
/** Below this share of elapsed window a burst says little about the whole window. */
|
|
8890
|
+
const MIN_FORECAST_ELAPSED = .2;
|
|
8891
|
+
/** Below this usage the projection is noise. */
|
|
8892
|
+
const MIN_FORECAST_USED_PERCENT = 3;
|
|
8893
|
+
/** Projected use above this percent turns a window `tight`. */
|
|
8894
|
+
const TIGHT_PROJECTED_PERCENT = 85;
|
|
8895
|
+
/**
|
|
8896
|
+
* Compute the pace of one window at `now`.
|
|
8897
|
+
* @param window - the provider-reported window.
|
|
8898
|
+
* @param now - epoch milliseconds.
|
|
8899
|
+
* @returns the pace; `elapsed` is the share of the window passed, 0 to 1.
|
|
8900
|
+
*/
|
|
8901
|
+
function pace(window, now) {
|
|
8902
|
+
const used = window.usedPercent;
|
|
8903
|
+
if (!Number.isFinite(used)) return { state: "unknown" };
|
|
8904
|
+
const usedPercent = Math.max(0, Math.min(100, used));
|
|
8905
|
+
if (usedPercent >= 100) return {
|
|
8906
|
+
state: "out",
|
|
8907
|
+
usedPercent
|
|
8908
|
+
};
|
|
8909
|
+
const { durationMs: durationMs$1, resetsAt } = window;
|
|
8910
|
+
if (durationMs$1 === void 0 || resetsAt === void 0 || !(durationMs$1 > 0) || !Number.isFinite(resetsAt)) return {
|
|
8911
|
+
state: "calm",
|
|
8912
|
+
usedPercent
|
|
8913
|
+
};
|
|
8914
|
+
const elapsed = Math.min(1, Math.max(0, 1 - (resetsAt - now) / durationMs$1));
|
|
8915
|
+
if (elapsed < MIN_FORECAST_ELAPSED || usedPercent < MIN_FORECAST_USED_PERCENT) return {
|
|
8916
|
+
state: "calm",
|
|
8917
|
+
usedPercent,
|
|
8918
|
+
elapsed
|
|
8919
|
+
};
|
|
8920
|
+
const projectedPercent = usedPercent / elapsed;
|
|
8921
|
+
if (projectedPercent > 100) {
|
|
8922
|
+
const ratePerMs = usedPercent / (elapsed * durationMs$1);
|
|
8923
|
+
return {
|
|
8924
|
+
state: "over",
|
|
8925
|
+
usedPercent,
|
|
8926
|
+
elapsed,
|
|
8927
|
+
projectedPercent,
|
|
8928
|
+
emptyAt: now + (100 - usedPercent) / ratePerMs
|
|
8929
|
+
};
|
|
8930
|
+
}
|
|
8931
|
+
if (projectedPercent > TIGHT_PROJECTED_PERCENT) return {
|
|
8932
|
+
state: "tight",
|
|
8933
|
+
usedPercent,
|
|
8934
|
+
elapsed,
|
|
8935
|
+
projectedPercent
|
|
8936
|
+
};
|
|
8937
|
+
return {
|
|
8938
|
+
state: "calm",
|
|
8939
|
+
usedPercent,
|
|
8940
|
+
elapsed,
|
|
8941
|
+
projectedPercent
|
|
8942
|
+
};
|
|
8943
|
+
}
|
|
8944
|
+
|
|
8945
|
+
//#endregion
|
|
8946
|
+
//#region src/tools/usage.ts
|
|
8947
|
+
function iso(ms) {
|
|
8948
|
+
return ms !== void 0 && Number.isFinite(ms) ? new Date(ms).toISOString() : void 0;
|
|
8949
|
+
}
|
|
8950
|
+
function reportWindows(usage, now) {
|
|
8951
|
+
return (usage.windows ?? []).map((window) => {
|
|
8952
|
+
const state = pace(window, now);
|
|
8953
|
+
const used = Number.isFinite(window.usedPercent) ? Math.max(0, Math.min(100, window.usedPercent)) : 0;
|
|
8954
|
+
const resetsAt = iso(window.resetsAt);
|
|
8955
|
+
const emptyAt = state.state === "over" ? iso(state.emptyAt) : void 0;
|
|
8956
|
+
return {
|
|
8957
|
+
kind: window.kind,
|
|
8958
|
+
...window.scope === void 0 ? {} : { scope: window.scope },
|
|
8959
|
+
usedPercent: Math.round(used),
|
|
8960
|
+
leftPercent: 100 - Math.round(used),
|
|
8961
|
+
...resetsAt === void 0 ? {} : { resetsAt },
|
|
8962
|
+
pace: state.state,
|
|
8963
|
+
...emptyAt === void 0 ? {} : { emptyAt }
|
|
8964
|
+
};
|
|
8965
|
+
});
|
|
8966
|
+
}
|
|
8967
|
+
/**
|
|
8968
|
+
* Collect usage for the requested provider, or for every signed-in provider.
|
|
8969
|
+
* One provider's failure becomes an `error` on its own entry.
|
|
8970
|
+
*/
|
|
8971
|
+
async function buildUsageReport(options, args, signal) {
|
|
8972
|
+
const now = (options.now ?? Date.now)();
|
|
8973
|
+
const all = await options.source.targets();
|
|
8974
|
+
const known = [...new Set(all.map((target) => target.provider))];
|
|
8975
|
+
if (args.provider !== void 0 && !known.includes(args.provider)) throw new Error(`subscription_usage: no signed-in provider "${args.provider}". Available: ${known.length > 0 ? known.join(", ") : "none"}`);
|
|
8976
|
+
const targets = args.provider === void 0 ? all : all.filter((target) => target.provider === args.provider);
|
|
8977
|
+
if (targets.length === 0) return {
|
|
8978
|
+
providers: [],
|
|
8979
|
+
note: "No subscription or usage key is set up."
|
|
8980
|
+
};
|
|
8981
|
+
return { providers: await Promise.all(targets.map(async (target) => {
|
|
8982
|
+
const base = {
|
|
8983
|
+
provider: target.provider,
|
|
8984
|
+
account: target.label
|
|
8985
|
+
};
|
|
8986
|
+
try {
|
|
8987
|
+
const usage = await options.source.read(target, args.refresh === true, signal);
|
|
8988
|
+
if (!usage.supported) return {
|
|
8989
|
+
...base,
|
|
8990
|
+
note: "This provider reports no usage."
|
|
8991
|
+
};
|
|
8992
|
+
const fetchedAt = iso(usage.fetchedAt);
|
|
8993
|
+
return {
|
|
8994
|
+
...base,
|
|
8995
|
+
...usage.plan === void 0 ? {} : { plan: usage.plan },
|
|
8996
|
+
windows: reportWindows(usage, now),
|
|
8997
|
+
...usage.stale === true || usage.refreshFailed === true ? { stale: true } : {},
|
|
8998
|
+
...fetchedAt === void 0 ? {} : { fetchedAt }
|
|
8999
|
+
};
|
|
9000
|
+
} catch (error) {
|
|
9001
|
+
return {
|
|
9002
|
+
...base,
|
|
9003
|
+
error: error instanceof Error ? error.message : String(error)
|
|
9004
|
+
};
|
|
9005
|
+
}
|
|
9006
|
+
})) };
|
|
9007
|
+
}
|
|
9008
|
+
/** Build the `subscription_usage` tool definition. */
|
|
9009
|
+
function createUsageTool(options) {
|
|
9010
|
+
return defineTool({
|
|
9011
|
+
name: "subscription_usage",
|
|
9012
|
+
description: "Read how much of each AI subscription limit is used (ChatGPT Codex, Claude, Grok, Copilot, Antigravity, Z.ai, Ollama Cloud, Kimi Code). Returns per provider the plan, each window's percent used and left, the reset time, and a pace state: calm, tight, over (will run out before reset, with emptyAt) or out. Call it before long, critical or heavy work, and prefer a provider whose pace is calm. Read-only. It returns cached data unless refresh is true.",
|
|
9013
|
+
parameters: {
|
|
9014
|
+
provider: {
|
|
9015
|
+
type: "string",
|
|
9016
|
+
description: "Limit the answer to one provider id, such as codex or claude. Omit for all."
|
|
9017
|
+
},
|
|
9018
|
+
refresh: {
|
|
9019
|
+
type: "boolean",
|
|
9020
|
+
description: "Fetch fresh numbers instead of the cached snapshot."
|
|
9021
|
+
}
|
|
9022
|
+
},
|
|
9023
|
+
output: {
|
|
9024
|
+
schema: { type: "string" },
|
|
9025
|
+
render: (_args, value) => [{
|
|
9026
|
+
type: "text",
|
|
9027
|
+
text: value
|
|
9028
|
+
}]
|
|
9029
|
+
},
|
|
9030
|
+
presentCall: (args) => ({
|
|
9031
|
+
card: "generic",
|
|
9032
|
+
title: args.provider === void 0 ? "subscription_usage" : `subscription_usage: ${args.provider}`,
|
|
9033
|
+
kind: "search"
|
|
9034
|
+
}),
|
|
9035
|
+
async execute(args, exec) {
|
|
9036
|
+
return JSON.stringify(await buildUsageReport(options, args, exec.signal));
|
|
9037
|
+
}
|
|
9038
|
+
});
|
|
9039
|
+
}
|
|
9040
|
+
/** The system prompt reminder, kept short so it costs little on every step. */
|
|
9041
|
+
const USAGE_PROMPT_REMINDER = "Before long, critical or heavy work on a subscription model, call subscription_usage to check how much of the limit is left, and prefer a provider whose pace is calm.";
|
|
9042
|
+
|
|
8886
9043
|
//#endregion
|
|
8887
9044
|
//#region src/providers/pool-usage.ts
|
|
8888
9045
|
/** Near-full members are tried only after members below this fill level. */
|
|
@@ -9244,7 +9401,7 @@ async function grokFlow() {
|
|
|
9244
9401
|
state,
|
|
9245
9402
|
nonce,
|
|
9246
9403
|
plan: "generic",
|
|
9247
|
-
referrer: "dsh-plugin-
|
|
9404
|
+
referrer: "dsh-plugin-subscription-usage"
|
|
9248
9405
|
});
|
|
9249
9406
|
return `${discovery.authorizationEndpoint}?${params.toString()}`;
|
|
9250
9407
|
}
|
|
@@ -10464,7 +10621,7 @@ const ANTIGRAVITY_TOKEN_URL = "https://oauth2.googleapis.com/token";
|
|
|
10464
10621
|
const ANTIGRAVITY_USERINFO_URL = "https://www.googleapis.com/oauth2/v2/userinfo";
|
|
10465
10622
|
const ANTIGRAVITY_DEFAULT_BASE_URL = "https://daily-cloudcode-pa.googleapis.com";
|
|
10466
10623
|
const ANTIGRAVITY_PROD_BASE_URL = "https://cloudcode-pa.googleapis.com";
|
|
10467
|
-
const ANTIGRAVITY_DEFAULT_USER_AGENT = "antigravity/1.104.0 dsh-plugin-
|
|
10624
|
+
const ANTIGRAVITY_DEFAULT_USER_AGENT = "antigravity/1.104.0 dsh-plugin-subscription-usage";
|
|
10468
10625
|
const ANTIGRAVITY_PREEMPT_MS = 5 * 6e4;
|
|
10469
10626
|
const ANTIGRAVITY_CALLBACK_PATH = "/oauth-callback";
|
|
10470
10627
|
const ANTIGRAVITY_CONTEXT_WINDOW = 1024e3;
|
|
@@ -13049,13 +13206,14 @@ function apply(ctx, config) {
|
|
|
13049
13206
|
if (!codexTokens) throw new BadRequest("Codex unavailable");
|
|
13050
13207
|
await consumeCodexResetCredit(await codexTokens.session(account), creditId, requestId, proxiedFetch, signal);
|
|
13051
13208
|
}, (account) => usageTracker.invalidate("codex", account));
|
|
13052
|
-
|
|
13209
|
+
const authController = new SubscriptionsAuthController(flows, deviceFlows, authChanged, resolveAttachments, usageFetchers, void 0, usageTracker, config.antigravity, {
|
|
13053
13210
|
...providers.includes("codex") ? { codex: config.codexClientVersion === void 0 ? presentedVersion(codexVersion) : async () => ({
|
|
13054
13211
|
version: config.codexClientVersion,
|
|
13055
13212
|
source: "config"
|
|
13056
13213
|
}) } : {},
|
|
13057
13214
|
...providers.includes("claude") ? { claude: presentedVersion(claudeVersion) } : {}
|
|
13058
|
-
}, resetRedemption, (provider, account) => poolHealth?.clear(provider, account), usage, keyUsage)
|
|
13215
|
+
}, resetRedemption, (provider, account) => poolHealth?.clear(provider, account), usage, keyUsage);
|
|
13216
|
+
registerAuthRpc(ctx, authController, speed, {
|
|
13059
13217
|
get: () => proxyGetConfig(),
|
|
13060
13218
|
set: (input) => proxySetConfig(input),
|
|
13061
13219
|
test: (payload) => proxyTestConnection(payload.url, payload.proxy)
|
|
@@ -13173,12 +13331,43 @@ function apply(ctx, config) {
|
|
|
13173
13331
|
}));
|
|
13174
13332
|
});
|
|
13175
13333
|
}
|
|
13334
|
+
const usageTargets = async () => {
|
|
13335
|
+
const targets = [];
|
|
13336
|
+
for (const provider of PROVIDER_IDS) {
|
|
13337
|
+
if (!authController.hasUsage(provider)) continue;
|
|
13338
|
+
const status = await authController.status(provider).catch(() => void 0);
|
|
13339
|
+
const account = status?.accounts.find((entry) => entry.key === status.servingAccount) ?? status?.accounts.find((entry) => entry.isDefault) ?? status?.accounts[0];
|
|
13340
|
+
if (account !== void 0) targets.push({
|
|
13341
|
+
provider,
|
|
13342
|
+
key: account.key,
|
|
13343
|
+
label: account.account ?? account.key
|
|
13344
|
+
});
|
|
13345
|
+
}
|
|
13346
|
+
for (const view of await authController.keyProviders()) if (view.configured) targets.push({
|
|
13347
|
+
provider: view.id,
|
|
13348
|
+
key: KEY_ACCOUNT,
|
|
13349
|
+
label: view.name
|
|
13350
|
+
});
|
|
13351
|
+
return targets;
|
|
13352
|
+
};
|
|
13353
|
+
ctx.inject(["systemPrompt"], (promptCtx) => {
|
|
13354
|
+
promptCtx.systemPrompt.section({
|
|
13355
|
+
name: "subscription-usage:reminder",
|
|
13356
|
+
order: 5e3,
|
|
13357
|
+
text: USAGE_PROMPT_REMINDER
|
|
13358
|
+
});
|
|
13359
|
+
});
|
|
13176
13360
|
ctx.inject(["tools"], (toolsCtx) => {
|
|
13177
13361
|
const registeredNames = /* @__PURE__ */ new Map();
|
|
13178
13362
|
if (grokTokens !== void 0) for (const definition of [createXSearchTool({ tokens: grokTokens }), createVideoGenerateTool({ tokens: grokTokens })]) {
|
|
13179
13363
|
const result = registerWithAlias(toolsCtx.tools, definition);
|
|
13180
13364
|
if (result !== void 0) registeredNames.set(definition.name, result.name);
|
|
13181
13365
|
}
|
|
13366
|
+
const usageResult = registerWithAlias(toolsCtx.tools, createUsageTool({ source: {
|
|
13367
|
+
targets: usageTargets,
|
|
13368
|
+
read: (target, refresh, signal) => authController.usage(target.provider, target.key, signal, refresh, refresh ? "manual" : "poll")
|
|
13369
|
+
} }));
|
|
13370
|
+
if (usageResult !== void 0) registeredNames.set("subscription_usage", usageResult.name);
|
|
13182
13371
|
if (codexTokens !== void 0 || grokTokens !== void 0) {
|
|
13183
13372
|
const result = registerWithAlias(toolsCtx.tools, createImageGenerateTool({
|
|
13184
13373
|
imagePool,
|
|
@@ -18,7 +18,7 @@ export declare const ANTIGRAVITY_TOKEN_URL = "https://oauth2.googleapis.com/toke
|
|
|
18
18
|
export declare const ANTIGRAVITY_USERINFO_URL = "https://www.googleapis.com/oauth2/v2/userinfo";
|
|
19
19
|
export declare const ANTIGRAVITY_DEFAULT_BASE_URL = "https://daily-cloudcode-pa.googleapis.com";
|
|
20
20
|
export declare const ANTIGRAVITY_PROD_BASE_URL = "https://cloudcode-pa.googleapis.com";
|
|
21
|
-
export declare const ANTIGRAVITY_DEFAULT_USER_AGENT = "antigravity/1.104.0 dsh-plugin-
|
|
21
|
+
export declare const ANTIGRAVITY_DEFAULT_USER_AGENT = "antigravity/1.104.0 dsh-plugin-subscription-usage";
|
|
22
22
|
export declare const ANTIGRAVITY_PREEMPT_MS: number;
|
|
23
23
|
/** Antigravity, not Gemini CLI, OAuth scopes from the local reference clients. */
|
|
24
24
|
export declare const ANTIGRAVITY_SCOPES: readonly ["openid", "https://www.googleapis.com/auth/cloud-platform", "https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/userinfo.profile", "https://www.googleapis.com/auth/cclog", "https://www.googleapis.com/auth/experimentsandconfigs"];
|
|
@@ -17,7 +17,7 @@ export const ANTIGRAVITY_TOKEN_URL = 'https://oauth2.googleapis.com/token';
|
|
|
17
17
|
export const ANTIGRAVITY_USERINFO_URL = 'https://www.googleapis.com/oauth2/v2/userinfo';
|
|
18
18
|
export const ANTIGRAVITY_DEFAULT_BASE_URL = 'https://daily-cloudcode-pa.googleapis.com';
|
|
19
19
|
export const ANTIGRAVITY_PROD_BASE_URL = 'https://cloudcode-pa.googleapis.com';
|
|
20
|
-
export const ANTIGRAVITY_DEFAULT_USER_AGENT = 'antigravity/1.104.0 dsh-plugin-
|
|
20
|
+
export const ANTIGRAVITY_DEFAULT_USER_AGENT = 'antigravity/1.104.0 dsh-plugin-subscription-usage';
|
|
21
21
|
export const ANTIGRAVITY_PREEMPT_MS = 5 * 60_000;
|
|
22
22
|
const ANTIGRAVITY_CALLBACK_PATH = '/oauth-callback';
|
|
23
23
|
const ANTIGRAVITY_CONTEXT_WINDOW = 1_024_000;
|
package/lib/providers/grok.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* adapter contributes one {@link RateLimitResetReader} built from the parsing
|
|
14
14
|
* primitives here.
|
|
15
15
|
*
|
|
16
|
-
* @module dsh-plugin-
|
|
16
|
+
* @module dsh-plugin-subscription-usage/providers/rate-limit
|
|
17
17
|
*/
|
|
18
18
|
import type { ResolvedRetryPolicy } from '@deepseek-ai/dsh-llm';
|
|
19
19
|
/**
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* adapter contributes one {@link RateLimitResetReader} built from the parsing
|
|
14
14
|
* primitives here.
|
|
15
15
|
*
|
|
16
|
-
* @module dsh-plugin-
|
|
16
|
+
* @module dsh-plugin-subscription-usage/providers/rate-limit
|
|
17
17
|
*/
|
|
18
18
|
import { resolveRetryPolicy } from '@deepseek-ai/dsh-llm';
|
|
19
19
|
/**
|
|
@@ -4,6 +4,7 @@ export declare const TOOL_ALIASES: {
|
|
|
4
4
|
readonly x_search: "dsh_subscriptions_x_search";
|
|
5
5
|
readonly video_generate: "dsh_subscriptions_video_generate";
|
|
6
6
|
readonly image_generate: "dsh_subscriptions_image_generate";
|
|
7
|
+
readonly subscription_usage: "dsh_subscriptions_subscription_usage";
|
|
7
8
|
};
|
|
8
9
|
export interface ToolRegistry {
|
|
9
10
|
register(definition: ToolDefinition): () => void;
|
|
@@ -3,6 +3,7 @@ export const TOOL_ALIASES = {
|
|
|
3
3
|
x_search: 'dsh_subscriptions_x_search',
|
|
4
4
|
video_generate: 'dsh_subscriptions_video_generate',
|
|
5
5
|
image_generate: 'dsh_subscriptions_image_generate',
|
|
6
|
+
subscription_usage: 'dsh_subscriptions_subscription_usage',
|
|
6
7
|
};
|
|
7
8
|
/** Register a tool under its canonical name, then a plugin-scoped alias. */
|
|
8
9
|
export function registerWithAlias(registry, definition, warn = message => console.warn(message)) {
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `subscription_usage` tool: lets the agent read how much of each
|
|
3
|
+
* subscription limit is used before it starts long or heavy work. Read-only;
|
|
4
|
+
* it reuses the usage snapshots the Settings page shows.
|
|
5
|
+
*/
|
|
6
|
+
import type { ToolDefinition } from '@deepseek-ai/dsh-tools';
|
|
7
|
+
import type { ProviderUsage } from '../providers/common.js';
|
|
8
|
+
/** One signed-in account the tool may report on. */
|
|
9
|
+
export interface UsageTarget {
|
|
10
|
+
provider: string;
|
|
11
|
+
/** Account key passed to `read`. */
|
|
12
|
+
key: string;
|
|
13
|
+
/** Label safe to show the model: an email or login, never a secret. */
|
|
14
|
+
label: string;
|
|
15
|
+
}
|
|
16
|
+
/** Where the tool gets accounts and snapshots from. */
|
|
17
|
+
export interface UsageToolSource {
|
|
18
|
+
/** One account per provider that has a usage endpoint. */
|
|
19
|
+
targets(): Promise<UsageTarget[]>;
|
|
20
|
+
read(target: UsageTarget, refresh: boolean, signal: AbortSignal): Promise<ProviderUsage>;
|
|
21
|
+
}
|
|
22
|
+
export interface UsageToolOptions {
|
|
23
|
+
source: UsageToolSource;
|
|
24
|
+
/** Clock, injectable for tests. */
|
|
25
|
+
now?: () => number;
|
|
26
|
+
}
|
|
27
|
+
interface WindowReport {
|
|
28
|
+
kind: string;
|
|
29
|
+
scope?: string;
|
|
30
|
+
usedPercent: number;
|
|
31
|
+
leftPercent: number;
|
|
32
|
+
resetsAt?: string;
|
|
33
|
+
pace: string;
|
|
34
|
+
emptyAt?: string;
|
|
35
|
+
}
|
|
36
|
+
interface ProviderReport {
|
|
37
|
+
provider: string;
|
|
38
|
+
account: string;
|
|
39
|
+
plan?: string;
|
|
40
|
+
windows?: WindowReport[];
|
|
41
|
+
stale?: boolean;
|
|
42
|
+
fetchedAt?: string;
|
|
43
|
+
note?: string;
|
|
44
|
+
error?: string;
|
|
45
|
+
}
|
|
46
|
+
export interface UsageReport {
|
|
47
|
+
providers: ProviderReport[];
|
|
48
|
+
note?: string;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Collect usage for the requested provider, or for every signed-in provider.
|
|
52
|
+
* One provider's failure becomes an `error` on its own entry.
|
|
53
|
+
*/
|
|
54
|
+
export declare function buildUsageReport(options: UsageToolOptions, args: {
|
|
55
|
+
provider?: string;
|
|
56
|
+
refresh?: boolean;
|
|
57
|
+
}, signal: AbortSignal): Promise<UsageReport>;
|
|
58
|
+
/** Build the `subscription_usage` tool definition. */
|
|
59
|
+
export declare function createUsageTool(options: UsageToolOptions): ToolDefinition;
|
|
60
|
+
/** The system prompt reminder, kept short so it costs little on every step. */
|
|
61
|
+
export declare const USAGE_PROMPT_REMINDER = "Before long, critical or heavy work on a subscription model, call subscription_usage to check how much of the limit is left, and prefer a provider whose pace is calm.";
|
|
62
|
+
export {};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `subscription_usage` tool: lets the agent read how much of each
|
|
3
|
+
* subscription limit is used before it starts long or heavy work. Read-only;
|
|
4
|
+
* it reuses the usage snapshots the Settings page shows.
|
|
5
|
+
*/
|
|
6
|
+
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
7
|
+
import { pace } from '../usage/pace.js';
|
|
8
|
+
function iso(ms) {
|
|
9
|
+
return ms !== undefined && Number.isFinite(ms) ? new Date(ms).toISOString() : undefined;
|
|
10
|
+
}
|
|
11
|
+
function reportWindows(usage, now) {
|
|
12
|
+
return (usage.windows ?? []).map(window => {
|
|
13
|
+
const state = pace(window, now);
|
|
14
|
+
const used = Number.isFinite(window.usedPercent) ? Math.max(0, Math.min(100, window.usedPercent)) : 0;
|
|
15
|
+
const resetsAt = iso(window.resetsAt);
|
|
16
|
+
const emptyAt = state.state === 'over' ? iso(state.emptyAt) : undefined;
|
|
17
|
+
return {
|
|
18
|
+
kind: window.kind,
|
|
19
|
+
...window.scope === undefined ? {} : { scope: window.scope },
|
|
20
|
+
usedPercent: Math.round(used),
|
|
21
|
+
leftPercent: 100 - Math.round(used),
|
|
22
|
+
...resetsAt === undefined ? {} : { resetsAt },
|
|
23
|
+
pace: state.state,
|
|
24
|
+
...emptyAt === undefined ? {} : { emptyAt },
|
|
25
|
+
};
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Collect usage for the requested provider, or for every signed-in provider.
|
|
30
|
+
* One provider's failure becomes an `error` on its own entry.
|
|
31
|
+
*/
|
|
32
|
+
export async function buildUsageReport(options, args, signal) {
|
|
33
|
+
const now = (options.now ?? Date.now)();
|
|
34
|
+
const all = await options.source.targets();
|
|
35
|
+
const known = [...new Set(all.map(target => target.provider))];
|
|
36
|
+
if (args.provider !== undefined && !known.includes(args.provider)) {
|
|
37
|
+
throw new Error(`subscription_usage: no signed-in provider "${args.provider}". Available: ${known.length > 0 ? known.join(', ') : 'none'}`);
|
|
38
|
+
}
|
|
39
|
+
const targets = args.provider === undefined ? all : all.filter(target => target.provider === args.provider);
|
|
40
|
+
if (targets.length === 0)
|
|
41
|
+
return { providers: [], note: 'No subscription or usage key is set up.' };
|
|
42
|
+
const providers = await Promise.all(targets.map(async (target) => {
|
|
43
|
+
const base = { provider: target.provider, account: target.label };
|
|
44
|
+
try {
|
|
45
|
+
const usage = await options.source.read(target, args.refresh === true, signal);
|
|
46
|
+
if (!usage.supported)
|
|
47
|
+
return { ...base, note: 'This provider reports no usage.' };
|
|
48
|
+
const fetchedAt = iso(usage.fetchedAt);
|
|
49
|
+
return {
|
|
50
|
+
...base,
|
|
51
|
+
...usage.plan === undefined ? {} : { plan: usage.plan },
|
|
52
|
+
windows: reportWindows(usage, now),
|
|
53
|
+
...usage.stale === true || usage.refreshFailed === true ? { stale: true } : {},
|
|
54
|
+
...fetchedAt === undefined ? {} : { fetchedAt },
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
return { ...base, error: error instanceof Error ? error.message : String(error) };
|
|
59
|
+
}
|
|
60
|
+
}));
|
|
61
|
+
return { providers };
|
|
62
|
+
}
|
|
63
|
+
/** Build the `subscription_usage` tool definition. */
|
|
64
|
+
export function createUsageTool(options) {
|
|
65
|
+
return defineTool({
|
|
66
|
+
name: 'subscription_usage',
|
|
67
|
+
description: 'Read how much of each AI subscription limit is used (ChatGPT Codex, Claude, Grok, Copilot, Antigravity, Z.ai, Ollama Cloud, Kimi Code). '
|
|
68
|
+
+ 'Returns per provider the plan, each window\'s percent used and left, the reset time, and a pace state: calm, tight, over (will run out before reset, with emptyAt) or out. '
|
|
69
|
+
+ 'Call it before long, critical or heavy work, and prefer a provider whose pace is calm. Read-only. It returns cached data unless refresh is true.',
|
|
70
|
+
parameters: {
|
|
71
|
+
provider: { type: 'string', description: 'Limit the answer to one provider id, such as codex or claude. Omit for all.' },
|
|
72
|
+
refresh: { type: 'boolean', description: 'Fetch fresh numbers instead of the cached snapshot.' },
|
|
73
|
+
},
|
|
74
|
+
output: {
|
|
75
|
+
schema: { type: 'string' },
|
|
76
|
+
render: (_args, value) => [{ type: 'text', text: value }],
|
|
77
|
+
},
|
|
78
|
+
presentCall: args => ({
|
|
79
|
+
card: 'generic',
|
|
80
|
+
title: args.provider === undefined ? 'subscription_usage' : `subscription_usage: ${args.provider}`,
|
|
81
|
+
kind: 'search',
|
|
82
|
+
}),
|
|
83
|
+
async execute(args, exec) {
|
|
84
|
+
return JSON.stringify(await buildUsageReport(options, args, exec.signal));
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
/** The system prompt reminder, kept short so it costs little on every step. */
|
|
89
|
+
export const USAGE_PROMPT_REMINDER = 'Before long, critical or heavy work on a subscription model, call subscription_usage to check how much of the limit is left, and prefer a provider whose pace is calm.';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-subscription-usage",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "ChatGPT (Codex), Claude, Grok, GitHub Copilot and Google Antigravity subscriptions as rows on the DSH Models page, with pace-aware usage meters",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Parth Patel <pp542004@gmail.com>",
|