dsh-plugin-subscription-oauth 0.13.0 → 0.15.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/lib/protocol.d.ts CHANGED
@@ -12,7 +12,7 @@
12
12
  /** Prefix of every RPC method this plugin serves. */
13
13
  export declare const RPC_PREFIX = "subscription-oauth.";
14
14
  /** Endpoints the Settings page may call. */
15
- export declare const RPC_ENDPOINTS: readonly ["status", "login", "cancel", "logout", "setDefault", "refresh", "discover", "setModelVisible", "setModelOrder", "addModel", "removeModel", "usage", "reload", "auto-state", "setAutoOn", "deepseek-balance", "ccswitch-balance", "model-variants"];
15
+ export declare const RPC_ENDPOINTS: readonly ["status", "login", "cancel", "logout", "setDefault", "refresh", "discover", "setModelVisible", "setModelOrder", "addModel", "removeModel", "usage", "reload", "auto-state", "setAutoOn", "checkin", "deepseek-balance", "ccswitch-balance", "model-variants"];
16
16
  /** One endpoint name. */
17
17
  export type RpcEndpoint = typeof RPC_ENDPOINTS[number];
18
18
  /** Client → host envelope. */
@@ -146,6 +146,13 @@ export interface ProviderUsage {
146
146
  renewsAt?: number;
147
147
  /** Metered allowances, most relevant first. */
148
148
  windows: readonly UsageWindow[];
149
+ /** Daily check-in state, when the provider states one. */
150
+ checkin?: {
151
+ active: boolean;
152
+ todayCheckedIn: boolean;
153
+ streakDays: number;
154
+ todayCredit: number;
155
+ };
149
156
  /** Epoch milliseconds these figures were read. */
150
157
  fetchedAt: number;
151
158
  /**
@@ -323,6 +330,10 @@ export interface RpcMap {
323
330
  payload: SetAutoOnRequest;
324
331
  value: AutoStateResult;
325
332
  };
333
+ checkin: {
334
+ payload: CheckinRequest;
335
+ value: CheckinResult;
336
+ };
326
337
  'deepseek-balance': {
327
338
  payload: DeepSeekBalanceRequest;
328
339
  value: DeepSeekBalanceResult;
@@ -398,6 +409,17 @@ export interface SetAutoOnRequest {
398
409
  export interface AutoStateResult {
399
410
  enabled: boolean;
400
411
  }
412
+ /** Payload of `checkin`: which account claims today's reward. */
413
+ export interface CheckinRequest {
414
+ provider: string;
415
+ account: string;
416
+ }
417
+ /** Result of `checkin`: the credit granted and the streak after claiming. */
418
+ export interface CheckinResult {
419
+ credit: number;
420
+ streakDays: number;
421
+ already: boolean;
422
+ }
401
423
  /** Payload of `deepseek-balance`. */
402
424
  export interface DeepSeekBalanceRequest {
403
425
  /** Client metadata the host's account service expects. */
package/lib/protocol.js CHANGED
@@ -28,6 +28,7 @@ export const RPC_ENDPOINTS = [
28
28
  'reload',
29
29
  'auto-state',
30
30
  'setAutoOn',
31
+ 'checkin',
31
32
  'deepseek-balance',
32
33
  'ccswitch-balance',
33
34
  'model-variants',
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Per-account credit spend tracking for the WorkBuddy route.
3
+ *
4
+ * The gateway states the real billed figure for a turn — `usage.credit` on the last
5
+ * usage frame of the stream — so the spent total is a sum of those, never an estimate
6
+ * from tokens and a rate. Spends accumulate per account in a small JSON file beside the
7
+ * credentials, and fold with the live packages' remaining allowance into one precise
8
+ * spent-against-limit window.
9
+ *
10
+ * @module dsh-plugin-subscription-oauth/providers/workbuddy-credits
11
+ */
12
+ /** One account's accumulated spend. */
13
+ interface AccountSpend {
14
+ spent: number;
15
+ calls: number;
16
+ }
17
+ /** The spend ledger, keyed by account id. */
18
+ export type SpendLedger = Record<string, AccountSpend>;
19
+ /** Read the ledger, tolerating a missing or corrupt file. */
20
+ export declare function readSpendLedger(file: string): Promise<SpendLedger>;
21
+ /** Add one turn's billed credit to an account's spend, returning the new totals. */
22
+ export declare function recordSpend(file: string, account: string, credit: number): Promise<AccountSpend>;
23
+ export {};
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Per-account credit spend tracking for the WorkBuddy route.
3
+ *
4
+ * The gateway states the real billed figure for a turn — `usage.credit` on the last
5
+ * usage frame of the stream — so the spent total is a sum of those, never an estimate
6
+ * from tokens and a rate. Spends accumulate per account in a small JSON file beside the
7
+ * credentials, and fold with the live packages' remaining allowance into one precise
8
+ * spent-against-limit window.
9
+ *
10
+ * @module dsh-plugin-subscription-oauth/providers/workbuddy-credits
11
+ */
12
+ import { readFile, writeFile, mkdir } from 'node:fs/promises';
13
+ import path from 'node:path';
14
+ /** Read the ledger, tolerating a missing or corrupt file. */
15
+ export async function readSpendLedger(file) {
16
+ try {
17
+ const raw = JSON.parse(await readFile(file, 'utf8'));
18
+ if (typeof raw !== 'object' || raw === null || Array.isArray(raw))
19
+ return {};
20
+ const out = {};
21
+ for (const [key, value] of Object.entries(raw)) {
22
+ if (typeof value !== 'object' || value === null)
23
+ continue;
24
+ const record = value;
25
+ const spent = typeof record.spent === 'number' && Number.isFinite(record.spent) ? record.spent : 0;
26
+ const calls = typeof record.calls === 'number' && Number.isFinite(record.calls) ? record.calls : 0;
27
+ out[key] = { spent: Math.max(0, spent), calls: Math.max(0, calls) };
28
+ }
29
+ return out;
30
+ }
31
+ catch {
32
+ return {};
33
+ }
34
+ }
35
+ /** Add one turn's billed credit to an account's spend, returning the new totals. */
36
+ export async function recordSpend(file, account, credit) {
37
+ const ledger = await readSpendLedger(file);
38
+ const previous = ledger[account] ?? { spent: 0, calls: 0 };
39
+ const next = {
40
+ spent: Math.round((previous.spent + Math.max(0, credit)) * 10000) / 10000,
41
+ calls: previous.calls + 1,
42
+ };
43
+ ledger[account] = next;
44
+ try {
45
+ await mkdir(path.dirname(file), { recursive: true });
46
+ await writeFile(file, JSON.stringify(ledger, null, 2), 'utf8');
47
+ }
48
+ catch {
49
+ // A failed write keeps the in-memory figure; the next turn retries.
50
+ }
51
+ return next;
52
+ }
@@ -8,7 +8,9 @@ import { REAUTH_REQUIRED, ProviderError } from '../errors.js';
8
8
  import { TokenStore } from '../auth/store.js';
9
9
  import { TokenManager } from '../auth/tokens.js';
10
10
  import { LOGIN_CANCELLED_MESSAGE } from '../auth/device-flow.js';
11
- import { finishWorkBuddySignIn, parseWorkBuddySession, readWorkBuddyUsage, refreshWorkBuddyToken, startWorkBuddySignIn, workbuddyRefreshable, workbuddyStale, WORKBUDDY_EARLY_MS, } from '../auth/workbuddy-flow.js';
11
+ import path from 'node:path';
12
+ import { readSpendLedger } from './workbuddy-credits.js';
13
+ import { finishWorkBuddySignIn, parseWorkBuddySession, readWorkBuddyUsage, refreshWorkBuddyToken, startWorkBuddySignIn, workbuddyRefreshable, workbuddyStale, claimWorkBuddyCheckin, WORKBUDDY_EARLY_MS, } from '../auth/workbuddy-flow.js';
12
14
  import { WorkBuddyAdapter, WORKBUDDY_CODE_URL, WORKBUDDY_CATALOG, WORKBUDDY_DEFAULT_MODEL, WORKBUDDY_PROVIDER_ID, WORKBUDDY_PROVIDER_NAME, } from './workbuddy.js';
13
15
  import { ProviderController } from '../auth/provider-controller.js';
14
16
  import { proxiedFetch } from '../http.js';
@@ -129,9 +131,11 @@ export function createWorkBuddyProvider(options) {
129
131
  }
130
132
  },
131
133
  });
134
+ const spendFile = path.join(path.dirname(options.credentialFile), 'workbuddy-spend.json');
132
135
  const adapter = new WorkBuddyAdapter({
133
136
  streamIdleTimeoutMs: options.streamIdleTimeoutMs,
134
137
  tokens,
138
+ spendFile,
135
139
  ...options.onWarn === undefined ? {} : { onWarn: options.onWarn },
136
140
  hiddenModels: () => options.preferences.hiddenIds(WORKBUDDY_PROVIDER_ID),
137
141
  userModels: () => options.preferences.extraIds(WORKBUDDY_PROVIDER_ID),
@@ -164,8 +168,10 @@ export function createWorkBuddyProvider(options) {
164
168
  const held = await store.get(account);
165
169
  if (held === undefined)
166
170
  return undefined;
167
- const read = await readWorkBuddyUsage(tokenOf(held), held.accountId, fetchFn);
168
- return { plan: read.plan, windows: read.windows };
171
+ const ledger = await readSpendLedger(spendFile);
172
+ const spend = ledger[account];
173
+ const read = await readWorkBuddyUsage(tokenOf(held), held.accountId, fetchFn, spend === undefined ? undefined : { spent: spend.spent });
174
+ return { plan: read.plan, windows: read.windows, ...read.checkin === undefined ? {} : { checkin: read.checkin } };
169
175
  },
170
176
  details: () => [
171
177
  { key: 'provider', value: WORKBUDDY_PROVIDER_NAME },
@@ -201,5 +207,21 @@ export function createWorkBuddyProvider(options) {
201
207
  removeModel: (id) => controller.removeModel(id),
202
208
  usage: (force) => controller.usage(force),
203
209
  reload: () => controller.reload(),
210
+ checkin: async (account) => {
211
+ const held = await store.get(account);
212
+ if (held === undefined)
213
+ throw new ProviderError('WorkBuddy account not signed in', REAUTH_REQUIRED);
214
+ try {
215
+ const claim = await claimWorkBuddyCheckin(tokenOf(held), fetchFn);
216
+ return { credit: claim.credit, streakDays: claim.streakDays, already: claim.credit === 0 };
217
+ }
218
+ catch (error) {
219
+ const message = error instanceof Error ? error.message : String(error);
220
+ // An already-claimed check-in is a normal answer, not a failure.
221
+ if (/already|已签到/i.test(message))
222
+ return { credit: 0, streakDays: 0, already: true };
223
+ throw new ProviderError(`WorkBuddy check-in failed: ${message}`, REAUTH_REQUIRED);
224
+ }
225
+ },
204
226
  };
205
227
  }
@@ -43,6 +43,8 @@ export interface WorkBuddyAdapterOptions {
43
43
  onWarn?: (message: string) => void;
44
44
  hiddenModels?: () => Promise<ReadonlySet<string>>;
45
45
  userModels?: () => Promise<readonly string[]>;
46
+ /** Where per-account credit spends accumulate; beside the credential file. */
47
+ spendFile?: string;
46
48
  fetchFn?: FetchFn;
47
49
  }
48
50
  /** The WorkBuddy China wire adapter serving the `workbuddy` route. */
@@ -60,6 +62,8 @@ export declare class WorkBuddyAdapter extends LlmAdapter {
60
62
  resolveModel(_provider: string, model: string): Promise<LlmResolvedModelInfo>;
61
63
  providerRetryPolicy(): undefined;
62
64
  stream(options: GenerateOptions): AsyncIterable<StreamChunk>;
65
+ /** The current turn's billed credit, read off the last usage frame. */
66
+ private turnCredit;
63
67
  /** The stored session's fields as the token the request signs with. */
64
68
  private tokenOf;
65
69
  private requestBody;
@@ -13,6 +13,7 @@ import { idleWatchdog, mapFetchFailure } from './common.js';
13
13
  import { proxiedFetch } from '../http.js';
14
14
  import { findEntry } from './catalog.js';
15
15
  import { WORKBUDDY_ENDPOINT, WORKBUDDY_UA_VERSION, workbuddyHeaders } from '../auth/workbuddy-flow.js';
16
+ import { recordSpend } from './workbuddy-credits.js';
16
17
  /** The `workbuddy` provider route key (China build). */
17
18
  export const WORKBUDDY_PROVIDER_ID = 'workbuddy';
18
19
  /** Human name for the route and its cards. */
@@ -277,6 +278,9 @@ export class WorkBuddyAdapter extends LlmAdapter {
277
278
  // Capture the tool-call history so unsafe tool names in past turns can be renamed to
278
279
  // the safe ones the model now sees, keeping the conversation consistent.
279
280
  this.historyMessages = options.messages.filter((message) => message.role === 'assistant' || message.role === 'tool');
281
+ // The last usage frame carries the turn's real billed credit; record it so the spend
282
+ // total is a sum of real figures, not an estimate.
283
+ this.turnCredit = 0;
280
284
  try {
281
285
  const body = this.requestBody(options);
282
286
  const response = await this.fetchFn(WORKBUDDY_API, {
@@ -308,8 +312,13 @@ export class WorkBuddyAdapter extends LlmAdapter {
308
312
  }
309
313
  finally {
310
314
  watchdog.stop();
315
+ if (this.turnCredit > 0 && this.options.spendFile !== undefined) {
316
+ void recordSpend(this.options.spendFile, held.account, this.turnCredit).catch(() => { });
317
+ }
311
318
  }
312
319
  }
320
+ /** The current turn's billed credit, read off the last usage frame. */
321
+ turnCredit = 0;
313
322
  /** The stored session's fields as the token the request signs with. */
314
323
  tokenOf(session) {
315
324
  return {
@@ -564,6 +573,11 @@ export class WorkBuddyAdapter extends LlmAdapter {
564
573
  if (usage !== undefined && usage !== null) {
565
574
  const input = usage.prompt_tokens ?? 0;
566
575
  const output = usage.completion_tokens ?? 0;
576
+ // The last usage frame carries the real billed credit; keep the latest, so a
577
+ // mid-stream retry does not double-count.
578
+ if (typeof usage.credit === 'number' && Number.isFinite(usage.credit)) {
579
+ this.turnCredit = usage.credit;
580
+ }
567
581
  yield {
568
582
  type: 'usage',
569
583
  usage: { inputTokens: input, outputTokens: output, totalTokens: usage.total_tokens ?? input + output },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-subscription-oauth",
3
- "version": "0.13.0",
3
+ "version": "0.15.0",
4
4
  "description": "Use subscription accounts as DeepSeek Harness LLM providers — Kimi Code (OAuth device flow), Grok / X Premium (OIDC + PKCE), Google Antigravity (Google OAuth + Cloud Code Assist) and GLM / BigModel (ZCode's browser sign-in) — with per-model visibility, drag-to-reorder Auto fallback and plan quotas from the web Settings page",
5
5
  "license": "MIT",
6
6
  "author": "EricFL1998",