dsh-plugin-subscription-oauth 0.5.0 → 0.6.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/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
  ];
@@ -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 { moonshotToolParameters } 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.
@@ -310,7 +311,7 @@ export class KimiAdapter extends LlmAdapter {
310
311
  input: resolved.input,
311
312
  ...options.tools === undefined || options.tools.length === 0
312
313
  ? {}
313
- : { tools: toResponsesTools(options.tools), tool_choice: 'auto', parallel_tool_calls: true },
314
+ : { tools: toResponsesTools(options.tools, moonshotToolParameters), tool_choice: 'auto', parallel_tool_calls: true },
314
315
  ...options.maxTokens === undefined ? {} : { max_output_tokens: options.maxTokens },
315
316
  ...options.reasoningEffort === undefined
316
317
  ? {}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Tool-schema preparation for the Moonshot wire (Kimi Code).
3
+ *
4
+ * Moonshot validates `tools[].function.parameters` with its own JSON-Schema
5
+ * reader, and that reader cannot settle a `$ref` that carries sibling
6
+ * keywords: instead of intersecting the two, it re-expands the node, so the
7
+ * whole request is refused with
8
+ * `detected infinite recursion without termination condition`. The harness
9
+ * forwards tool schemas exactly as their owner declared them, and the MCP
10
+ * tools REA registers use that shape heavily —
11
+ * `{"$ref": "#/$defs/__schema12", "description": "..."}` — 153 of 170 tools
12
+ * carried at least one such node, and the first one Moonshot reached killed
13
+ * every Kimi request that included a tool.
14
+ *
15
+ * A `$ref` that stands alone is accepted, and so is keeping `$defs` for it, so
16
+ * only the offending nodes are rewritten: the referenced schema is inlined
17
+ * under the node's own keywords (JSON Schema 2020-12 applies both, and the
18
+ * node's own keywords win on a conflict), which leaves the wire schema with
19
+ * standalone references only. Resolution is bounded by the reference stack, so
20
+ * a real cycle ends by dropping the `$ref` and keeping the sibling keywords
21
+ * rather than recursing.
22
+ *
23
+ * @module dsh-plugin-subscription-oauth/translate/moonshot-schema
24
+ */
25
+ /**
26
+ * Prepare one tool's parameters for Moonshot.
27
+ *
28
+ * @param parameters - the tool's declared argument schema.
29
+ * @returns a detached schema whose `$ref` nodes carry no sibling keywords.
30
+ */
31
+ export declare function moonshotToolParameters(parameters: Record<string, unknown>): Record<string, unknown>;
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Tool-schema preparation for the Moonshot wire (Kimi Code).
3
+ *
4
+ * Moonshot validates `tools[].function.parameters` with its own JSON-Schema
5
+ * reader, and that reader cannot settle a `$ref` that carries sibling
6
+ * keywords: instead of intersecting the two, it re-expands the node, so the
7
+ * whole request is refused with
8
+ * `detected infinite recursion without termination condition`. The harness
9
+ * forwards tool schemas exactly as their owner declared them, and the MCP
10
+ * tools REA registers use that shape heavily —
11
+ * `{"$ref": "#/$defs/__schema12", "description": "..."}` — 153 of 170 tools
12
+ * carried at least one such node, and the first one Moonshot reached killed
13
+ * every Kimi request that included a tool.
14
+ *
15
+ * A `$ref` that stands alone is accepted, and so is keeping `$defs` for it, so
16
+ * only the offending nodes are rewritten: the referenced schema is inlined
17
+ * under the node's own keywords (JSON Schema 2020-12 applies both, and the
18
+ * node's own keywords win on a conflict), which leaves the wire schema with
19
+ * standalone references only. Resolution is bounded by the reference stack, so
20
+ * a real cycle ends by dropping the `$ref` and keeping the sibling keywords
21
+ * rather than recursing.
22
+ *
23
+ * @module dsh-plugin-subscription-oauth/translate/moonshot-schema
24
+ */
25
+ /** True for a plain object usable as a schema node. */
26
+ function object(value) {
27
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
28
+ }
29
+ /** Follow one local JSON pointer, or undefined when it does not resolve. */
30
+ function pointed(root, ref) {
31
+ if (!ref.startsWith('#/'))
32
+ return undefined;
33
+ let node = root;
34
+ for (const segment of ref.slice(2).split('/')) {
35
+ const key = segment.replaceAll('~1', '/').replaceAll('~0', '~');
36
+ if (Array.isArray(node))
37
+ node = node[Number(key)];
38
+ else if (object(node) && Object.hasOwn(node, key))
39
+ node = node[key];
40
+ else
41
+ return undefined;
42
+ }
43
+ return node;
44
+ }
45
+ /**
46
+ * Rewrite one node so every `$ref` left on the wire stands alone.
47
+ *
48
+ * @param node - the node to rewrite.
49
+ * @param root - the tool's root schema, which local pointers resolve against.
50
+ * @param refs - pointers already being resolved on the path to this node.
51
+ * @returns the rewritten node.
52
+ */
53
+ function inline(node, root, refs) {
54
+ if (Array.isArray(node))
55
+ return node.map((item) => inline(item, root, refs));
56
+ if (!object(node))
57
+ return node;
58
+ const { $ref, ...siblings } = node;
59
+ if (typeof $ref !== 'string') {
60
+ return Object.fromEntries(Object.entries(node).map(([key, value]) => [key, inline(value, root, refs)]));
61
+ }
62
+ // A standalone reference is exactly what Moonshot expects, so it stays as is
63
+ // and keeps its `$defs` entry.
64
+ if (Object.keys(siblings).length === 0)
65
+ return { $ref };
66
+ // A repeated pointer, or one that is not a local object, cannot be inlined:
67
+ // the sibling keywords are all that is left to terminate on.
68
+ const target = refs.has($ref) ? undefined : pointed(root, $ref);
69
+ return object(target)
70
+ ? inline({ ...target, ...siblings }, root, new Set([...refs, $ref]))
71
+ : inline(siblings, root, refs);
72
+ }
73
+ /**
74
+ * Prepare one tool's parameters for Moonshot.
75
+ *
76
+ * @param parameters - the tool's declared argument schema.
77
+ * @returns a detached schema whose `$ref` nodes carry no sibling keywords.
78
+ */
79
+ export function moonshotToolParameters(parameters) {
80
+ return inline(parameters, parameters, new Set());
81
+ }
@@ -53,9 +53,11 @@ export declare function toResponsesInput(messages: readonly RequestMessage[], sy
53
53
  * than letting the endpoint normalize every schema into strict mode.
54
54
  *
55
55
  * @param tools - the request's tool schemas.
56
+ * @param prepare - an endpoint-specific rewrite of one parameters schema, for
57
+ * a route whose JSON-Schema reader refuses the schema as declared.
56
58
  * @returns `tools` array entries.
57
59
  */
58
- export declare function toResponsesTools(tools: readonly ToolSchema[]): Record<string, unknown>[];
60
+ export declare function toResponsesTools(tools: readonly ToolSchema[], prepare?: (parameters: Record<string, unknown>) => Record<string, unknown>): Record<string, unknown>[];
59
61
  /** The `usage` object of a completed response. */
60
62
  export interface ResponsesUsage {
61
63
  input_tokens?: number;
@@ -124,14 +124,16 @@ export async function toResponsesInput(messages, system, resolveImage) {
124
124
  * than letting the endpoint normalize every schema into strict mode.
125
125
  *
126
126
  * @param tools - the request's tool schemas.
127
+ * @param prepare - an endpoint-specific rewrite of one parameters schema, for
128
+ * a route whose JSON-Schema reader refuses the schema as declared.
127
129
  * @returns `tools` array entries.
128
130
  */
129
- export function toResponsesTools(tools) {
131
+ export function toResponsesTools(tools, prepare) {
130
132
  return tools.map((tool) => ({
131
133
  type: 'function',
132
134
  name: tool.name,
133
135
  description: tool.description,
134
- parameters: tool.parameters,
136
+ parameters: prepare === undefined ? tool.parameters : prepare(tool.parameters),
135
137
  strict: false,
136
138
  }));
137
139
  }
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.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",