dsh-plugin-subscription-oauth 0.5.0 → 0.6.1

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/index.d.ts CHANGED
@@ -44,6 +44,18 @@ export interface Config {
44
44
  * the other routes. Defaults to true.
45
45
  */
46
46
  auto?: boolean;
47
+ /**
48
+ * The credential reference a non-subscription provider's balance query uses, by
49
+ * provider id.
50
+ *
51
+ * A provider that is not a subscription this plugin serves has no usage windows to
52
+ * meter, so the chip shows its balance instead — read by cc-switch's query, which needs
53
+ * the provider's API key. The provider's profile names that key by reference
54
+ * (`apiKeyEnv`), and the host's credential service resolves the name; this map is the
55
+ * one place a provider id is tied to that reference. A provider with no entry, or
56
+ * whose key does not resolve, gets no balance and the chip stays silent.
57
+ */
58
+ balanceCredentialRefs?: Record<string, string>;
47
59
  }
48
60
  /** Configuration schema, with the config file's own defaults applied. */
49
61
  export declare const Config: z<Config>;
package/lib/index.js CHANGED
@@ -188,7 +188,39 @@ export function apply(ctx, config = {}) {
188
188
  handles.set(route.id, handle);
189
189
  ctx.effect(() => () => handle(), `${name}: ${route.id} provider route`);
190
190
  }
191
- registerSubscriptionRpc(ctx, registry);
191
+ // A non-subscription provider's balance is read by cc-switch's query, which needs the
192
+ // provider's API key. The provider's profile names that key by reference (`apiKeyEnv`),
193
+ // and the configured map ties a provider id to that reference; the host's credential
194
+ // service resolves it. The map lives in server config because that is where the rest
195
+ // of the plugin's wiring is, and the client learns a provider's balance by id, never
196
+ // by its credential reference.
197
+ const credentialRefs = config.balanceCredentialRefs ?? {};
198
+ // The DeepSeek account's balance is read on the node half through the host's
199
+ // `deepseekAccount` service; the browser reaches it through the plugin's own RPC, the
200
+ // same channel as the rest of the chip's data. The service is read lazily, per request
201
+ // (not once in apply), because it may register after this plugin applies; a composition
202
+ // without it still serves every other endpoint, and the balance endpoint answers nothing.
203
+ const balanceContext = {
204
+ credentialRefs,
205
+ resolveDeepSeekAccount: () => {
206
+ try {
207
+ return ctx.get('deepseekAccount');
208
+ }
209
+ catch {
210
+ return undefined;
211
+ }
212
+ },
213
+ resolveCredential: async (ref) => {
214
+ try {
215
+ const credentials = ctx.get('credentials');
216
+ return (await credentials?.resolve?.(ref))?.value;
217
+ }
218
+ catch {
219
+ return undefined;
220
+ }
221
+ },
222
+ };
223
+ registerSubscriptionRpc(ctx, registry, balanceContext);
192
224
  // Warm each route's live catalog for an install that is already signed in.
193
225
  for (const id of registry.ids()) {
194
226
  void registry.discover(id, false).catch((error) => {
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", "ccswitch-balance"];
15
+ export declare const RPC_ENDPOINTS: readonly ["status", "login", "cancel", "logout", "setDefault", "refresh", "discover", "setModelVisible", "setModelOrder", "addModel", "removeModel", "usage", "reload", "deepseek-balance", "ccswitch-balance"];
16
16
  /** One endpoint name. */
17
17
  export type RpcEndpoint = typeof RPC_ENDPOINTS[number];
18
18
  /** Client → host envelope. */
@@ -313,6 +313,10 @@ export interface RpcMap {
313
313
  payload: undefined;
314
314
  value: SubscriptionStatus;
315
315
  };
316
+ 'deepseek-balance': {
317
+ payload: DeepSeekBalanceRequest;
318
+ value: DeepSeekBalanceResult;
319
+ };
316
320
  'ccswitch-balance': {
317
321
  payload: CcSwitchBalanceRequest;
318
322
  value: CcSwitchBalanceResult;
@@ -343,10 +347,36 @@ export type BalanceResult =
343
347
  | {
344
348
  ok: false;
345
349
  };
350
+ /** Payload of `deepseek-balance`. */
351
+ export interface DeepSeekBalanceRequest {
352
+ /** Client metadata the host's account service expects. */
353
+ version?: string;
354
+ locale?: string;
355
+ timezoneOffsetSeconds?: number;
356
+ }
357
+ /** One currency's wallet, as the host's account service reports it. */
358
+ export interface DeepSeekWallet {
359
+ /** ISO currency the balance is denominated in. */
360
+ currency: string;
361
+ /** Remaining balance, as a decimal string. */
362
+ balance: string;
363
+ }
364
+ /**
365
+ * Result of `deepseek-balance`.
366
+ *
367
+ * `undefined` means the account is signed out or the read failed, so a chip stays
368
+ * silent rather than showing a wrong figure.
369
+ */
370
+ export type DeepSeekBalanceResult = {
371
+ /** Normal (topped-up) wallets, one per currency. */
372
+ wallets: readonly DeepSeekWallet[];
373
+ /** Bonus / promotional wallets, one per currency. */
374
+ bonusWallets: readonly DeepSeekWallet[];
375
+ } | undefined;
346
376
  /** Payload of `ccswitch-balance`. */
347
377
  export interface CcSwitchBalanceRequest {
348
- /** The credential reference the provider's profile names (its apiKeyEnv). */
349
- credentialRef: string;
378
+ /** The provider whose balance to read; the server maps it to its credential reference. */
379
+ provider: string;
350
380
  }
351
381
  /**
352
382
  * Result of `ccswitch-balance`.
package/lib/protocol.js CHANGED
@@ -26,5 +26,6 @@ export const RPC_ENDPOINTS = [
26
26
  'removeModel',
27
27
  'usage',
28
28
  'reload',
29
+ 'deepseek-balance',
29
30
  'ccswitch-balance',
30
31
  ];
@@ -95,7 +95,15 @@ export declare class KimiAdapter extends LlmAdapter {
95
95
  private discovered;
96
96
  /** Ids the user added by hand, cached so the sync catalog can name them. */
97
97
  private userAddedIds;
98
+ /** Tools already reported as flattened, so each is stated once. */
99
+ private readonly flattenedTools;
98
100
  constructor(options: KimiAdapterOptions);
101
+ /**
102
+ * Tool schemas in the shape Moonshot accepts: no `$ref`, and under its size
103
+ * ceiling. A declaration too large to send keeps its arguments, and that is
104
+ * reported once per tool rather than on every turn.
105
+ */
106
+ private moonshotTools;
99
107
  providerInfo(provider: string): LlmProviderInfo;
100
108
  providerRetryPolicy(): undefined;
101
109
  listModels(_provider: string): Promise<readonly LlmModelInfo[]>;
@@ -14,6 +14,7 @@ import { idleWatchdog, httpLlmError, isMissingOrInvalidCredential, mapFetchFailu
14
14
  import { streamProviderResponses } from './responses-stream.js';
15
15
  import { fallbackEntry, findEntry, resolveCatalog } from './catalog.js';
16
16
  import { streamResponses, toResponsesInput, toResponsesTools } from '../translate/responses.js';
17
+ import { prepareMoonshotTool } from '../translate/moonshot-schema.js';
17
18
  /**
18
19
  * Coerce one decoded credential entry into a Kimi session.
19
20
  * @param value - decoded JSON value for one account.
@@ -130,10 +131,31 @@ export class KimiAdapter extends LlmAdapter {
130
131
  discovered;
131
132
  /** Ids the user added by hand, cached so the sync catalog can name them. */
132
133
  userAddedIds = [];
134
+ /** Tools already reported as flattened, so each is stated once. */
135
+ flattenedTools = new Set();
133
136
  constructor(options) {
134
137
  super();
135
138
  this.options = options;
136
139
  }
140
+ /**
141
+ * Tool schemas in the shape Moonshot accepts: no `$ref`, and under its size
142
+ * ceiling. A declaration too large to send keeps its arguments, and that is
143
+ * reported once per tool rather than on every turn.
144
+ */
145
+ moonshotTools(tools) {
146
+ return tools.map((tool) => {
147
+ const prepared = prepareMoonshotTool(tool.parameters);
148
+ if (prepared.preparedAs !== 'as-declared' && !this.flattenedTools.has(tool.name)) {
149
+ this.flattenedTools.add(tool.name);
150
+ this.options.onWarn?.(prepared.preparedAs === 'flattened'
151
+ ? `kimi: ${tool.name} declares an argument schema larger than the endpoint accepts; `
152
+ + 'its arguments are described without their nested structure'
153
+ : `kimi: ${tool.name} declares an argument schema larger than the endpoint accepts; `
154
+ + 'it is offered as an unrestricted object');
155
+ }
156
+ return { ...tool, parameters: prepared.parameters };
157
+ });
158
+ }
137
159
  providerInfo(provider) {
138
160
  return { id: provider, name: KIMI_PROVIDER_NAME };
139
161
  }
@@ -310,7 +332,11 @@ export class KimiAdapter extends LlmAdapter {
310
332
  input: resolved.input,
311
333
  ...options.tools === undefined || options.tools.length === 0
312
334
  ? {}
313
- : { tools: toResponsesTools(options.tools), tool_choice: 'auto', parallel_tool_calls: true },
335
+ : {
336
+ tools: toResponsesTools(this.moonshotTools(options.tools)),
337
+ tool_choice: 'auto',
338
+ parallel_tool_calls: true,
339
+ },
314
340
  ...options.maxTokens === undefined ? {} : { max_output_tokens: options.maxTokens },
315
341
  ...options.reasoningEffort === undefined
316
342
  ? {}
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Tool-schema preparation for the Moonshot wire (Kimi Code).
3
+ *
4
+ * Moonshot validates `tools[].function.parameters` with its own reader, and
5
+ * that reader expands every `$ref` before it measures or checks the schema. The
6
+ * harness forwards each tool's schema exactly as its owner declared it, and
7
+ * every MCP tool REA registers holds its shapes in a `$defs` block that its
8
+ * properties reference, so Kimi refused the whole request with
9
+ * `tools.function.parameters is not a valid moonshot flavored json schema`.
10
+ *
11
+ * Live probes against `api.kimi.com` measured both edges of that reader:
12
+ *
13
+ * - a `$ref` whose target itself holds `$ref`s is refused as
14
+ * `detected infinite recursion without termination condition` — the failing
15
+ * tool reported the node `properties.actions` and, once that node was
16
+ * inlined so its reference stood alone, the deeper `properties.actions.items`.
17
+ * A one-level reference is accepted; a chain is not.
18
+ * - the expanded schema has a size ceiling between 80,081 B (accepted) and
19
+ * 124,389 B (refused). `compare_web_captures` declares 57,175 B of shared
20
+ * definitions but expands past 153,000 B, so it was refused even as declared.
21
+ *
22
+ * So this module sends no references at all — every local pointer is replaced
23
+ * by the definition it names, and the document keywords that only existed to
24
+ * hold them (`$defs`, `$schema`, `$id`, `definitions`) are dropped. A pointer
25
+ * that re-enters itself cannot be inlined, and there the referencing node keeps
26
+ * its own keywords instead of recursing. A tool whose expanded schema still
27
+ * exceeds the ceiling is flattened in steps — the arguments with their prose,
28
+ * then the arguments alone, then an unrestricted object — so that one oversized
29
+ * tool costs its own detail rather than the whole request.
30
+ *
31
+ * @module dsh-plugin-subscription-oauth/translate/moonshot-schema
32
+ */
33
+ /** How much of a declaration a schema too large to send had to give up. */
34
+ export type MoonshotPreparation =
35
+ /** Sent as declared, with its references inlined. */
36
+ 'as-declared'
37
+ /** Only the arguments: names, types, choices, `required`, and prose while it fits. */
38
+ | 'flattened'
39
+ /** An unrestricted object, because even the arguments did not fit. */
40
+ | 'permissive';
41
+ /** One prepared tool schema together with what had to be done to it. */
42
+ export interface MoonshotToolSchema {
43
+ /** The schema to send. */
44
+ parameters: Record<string, unknown>;
45
+ /** What the declaration had to give up to fit. */
46
+ preparedAs: MoonshotPreparation;
47
+ }
48
+ /**
49
+ * Prepare one tool's parameters for Moonshot.
50
+ *
51
+ * @param parameters - the tool's declared argument schema.
52
+ * @returns the schema to send and what it had to give up to fit the endpoint.
53
+ */
54
+ export declare function prepareMoonshotTool(parameters: Record<string, unknown>): MoonshotToolSchema;
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Tool-schema preparation for the Moonshot wire (Kimi Code).
3
+ *
4
+ * Moonshot validates `tools[].function.parameters` with its own reader, and
5
+ * that reader expands every `$ref` before it measures or checks the schema. The
6
+ * harness forwards each tool's schema exactly as its owner declared it, and
7
+ * every MCP tool REA registers holds its shapes in a `$defs` block that its
8
+ * properties reference, so Kimi refused the whole request with
9
+ * `tools.function.parameters is not a valid moonshot flavored json schema`.
10
+ *
11
+ * Live probes against `api.kimi.com` measured both edges of that reader:
12
+ *
13
+ * - a `$ref` whose target itself holds `$ref`s is refused as
14
+ * `detected infinite recursion without termination condition` — the failing
15
+ * tool reported the node `properties.actions` and, once that node was
16
+ * inlined so its reference stood alone, the deeper `properties.actions.items`.
17
+ * A one-level reference is accepted; a chain is not.
18
+ * - the expanded schema has a size ceiling between 80,081 B (accepted) and
19
+ * 124,389 B (refused). `compare_web_captures` declares 57,175 B of shared
20
+ * definitions but expands past 153,000 B, so it was refused even as declared.
21
+ *
22
+ * So this module sends no references at all — every local pointer is replaced
23
+ * by the definition it names, and the document keywords that only existed to
24
+ * hold them (`$defs`, `$schema`, `$id`, `definitions`) are dropped. A pointer
25
+ * that re-enters itself cannot be inlined, and there the referencing node keeps
26
+ * its own keywords instead of recursing. A tool whose expanded schema still
27
+ * exceeds the ceiling is flattened in steps — the arguments with their prose,
28
+ * then the arguments alone, then an unrestricted object — so that one oversized
29
+ * tool costs its own detail rather than the whole request.
30
+ *
31
+ * @module dsh-plugin-subscription-oauth/translate/moonshot-schema
32
+ */
33
+ /** Keywords that describe the schema document rather than the values it accepts. */
34
+ const DOCUMENT_KEYS = new Set(['$schema', '$id', '$defs', 'definitions']);
35
+ /** Root keywords a flattened schema keeps. */
36
+ const ROOT_KEYS = ['type', 'description', 'required', 'additionalProperties', 'minProperties', 'maxProperties'];
37
+ /**
38
+ * Largest expanded schema the endpoint is asked to take, in bytes.
39
+ *
40
+ * Measured rather than assumed: 80,081 B was accepted and 124,389 B was refused
41
+ * with `At path 'root': schema exceeds maximum allowed size`. This sits below
42
+ * both so a schema this plugin sends has room to spare.
43
+ */
44
+ const SIZE_BUDGET = 64 * 1024;
45
+ /** True for a plain object usable as a schema node. */
46
+ function object(value) {
47
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
48
+ }
49
+ /** Follow one local JSON pointer, or undefined when it does not resolve. */
50
+ function pointed(root, ref) {
51
+ if (!ref.startsWith('#/'))
52
+ return undefined;
53
+ let node = root;
54
+ for (const segment of ref.slice(2).split('/')) {
55
+ const key = segment.replaceAll('~1', '/').replaceAll('~0', '~');
56
+ if (Array.isArray(node))
57
+ node = node[Number(key)];
58
+ else if (object(node) && Object.hasOwn(node, key))
59
+ node = node[key];
60
+ else
61
+ return undefined;
62
+ }
63
+ return node;
64
+ }
65
+ /**
66
+ * Replace every reference reachable from one node with what it names.
67
+ *
68
+ * @param node - the node to rewrite.
69
+ * @param root - the tool's root schema, which local pointers resolve against.
70
+ * @param refs - pointers already being resolved on the path to this node.
71
+ * @returns the rewritten node.
72
+ */
73
+ function inline(node, root, refs) {
74
+ if (Array.isArray(node))
75
+ return node.map((item) => inline(item, root, refs));
76
+ if (!object(node))
77
+ return node;
78
+ const { $ref, ...siblings } = node;
79
+ if (typeof $ref === 'string') {
80
+ // A repeated pointer, or one that names nothing local, cannot be inlined:
81
+ // the node's own keywords are all that is left.
82
+ const target = refs.has($ref) ? undefined : pointed(root, $ref);
83
+ return object(target)
84
+ ? inline({ ...target, ...siblings }, root, new Set([...refs, $ref]))
85
+ : inline(siblings, root, refs);
86
+ }
87
+ return Object.fromEntries(Object.entries(node)
88
+ .filter(([key]) => !DOCUMENT_KEYS.has(key))
89
+ .map(([key, value]) => [key, inline(value, root, refs)]));
90
+ }
91
+ /**
92
+ * Keep only what a model needs to call the tool: the root's own keywords, and
93
+ * each argument's name, type, value choices and — while the budget allows —
94
+ * its description.
95
+ *
96
+ * @param schema - an already inlined schema.
97
+ * @param prose - whether descriptions are worth their bytes.
98
+ * @returns the flattened schema.
99
+ */
100
+ function flatten(schema, prose) {
101
+ const out = {};
102
+ for (const key of ROOT_KEYS) {
103
+ if (Object.hasOwn(schema, key) && (prose || key !== 'description'))
104
+ out[key] = schema[key];
105
+ }
106
+ if (!object(schema.properties))
107
+ return out;
108
+ out.properties = Object.fromEntries(Object.entries(schema.properties).map(([name, property]) => {
109
+ const node = object(property) ? property : {};
110
+ const reduced = {};
111
+ if (typeof node.type === 'string')
112
+ reduced.type = node.type;
113
+ if (Array.isArray(node.enum))
114
+ reduced.enum = node.enum;
115
+ if (prose && typeof node.description === 'string')
116
+ reduced.description = node.description;
117
+ return [name, reduced];
118
+ }));
119
+ return out;
120
+ }
121
+ /** Bytes the endpoint measures once it has expanded every reference. */
122
+ function expandedBytes(schema) {
123
+ return Buffer.byteLength(JSON.stringify(schema));
124
+ }
125
+ /**
126
+ * Prepare one tool's parameters for Moonshot.
127
+ *
128
+ * @param parameters - the tool's declared argument schema.
129
+ * @returns the schema to send and what it had to give up to fit the endpoint.
130
+ */
131
+ export function prepareMoonshotTool(parameters) {
132
+ const inlined = inline(parameters, parameters, new Set());
133
+ if (expandedBytes(inlined) <= SIZE_BUDGET)
134
+ return { parameters: inlined, preparedAs: 'as-declared' };
135
+ const documented = flatten(inlined, true);
136
+ if (expandedBytes(documented) <= SIZE_BUDGET)
137
+ return { parameters: documented, preparedAs: 'flattened' };
138
+ const bare = flatten(inlined, false);
139
+ if (expandedBytes(bare) <= SIZE_BUDGET)
140
+ return { parameters: bare, preparedAs: 'flattened' };
141
+ return { parameters: { type: 'object', additionalProperties: true }, preparedAs: 'permissive' };
142
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-subscription-oauth",
3
- "version": "0.5.0",
3
+ "version": "0.6.1",
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",