dsh-plugin-subscription-oauth 0.6.0 → 0.7.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/auth/ccswitch.d.ts +26 -1
- package/lib/auth/ccswitch.js +60 -3
- package/lib/auth/rpc.d.ts +2 -0
- package/lib/auth/rpc.js +13 -7
- package/lib/index.d.ts +12 -6
- package/lib/index.js +29 -8
- package/lib/providers/kimi.d.ts +8 -0
- package/lib/providers/kimi.js +27 -2
- package/lib/translate/moonshot-schema.d.ts +42 -19
- package/lib/translate/moonshot-schema.js +94 -33
- package/lib/translate/responses.d.ts +1 -3
- package/lib/translate/responses.js +2 -4
- package/package.json +1 -1
package/lib/auth/ccswitch.d.ts
CHANGED
|
@@ -24,13 +24,38 @@ export interface CcSwitchBalance {
|
|
|
24
24
|
/** Whether the account reports itself as usable. */
|
|
25
25
|
available: boolean;
|
|
26
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* Query the balance endpoint some OpenAI-compatible providers expose at `{base}/v1/usage`.
|
|
29
|
+
*
|
|
30
|
+
* huanling is one: it answers `GET {base}/v1/usage` with `{ remaining, unit, … }`, which is
|
|
31
|
+
* the figure cc-switch shows for it. This is the provider's own shape, not cc-switch's
|
|
32
|
+
* `balance_infos`, so it is read separately. The base has any trailing `/v1` stripped before
|
|
33
|
+
* the path is added, so both `https://api.huanling.icu` and `https://api.huanling.icu/v1`
|
|
34
|
+
* resolve to the same `{base}/v1/usage`.
|
|
35
|
+
*
|
|
36
|
+
* @param input - the provider's API key and base URL.
|
|
37
|
+
* @returns the balance, or undefined when the provider did not answer with one.
|
|
38
|
+
*/
|
|
39
|
+
export declare function queryUsageBalance(input: {
|
|
40
|
+
apiKey: string;
|
|
41
|
+
baseUrl: string;
|
|
42
|
+
fetchFn?: FetchFn;
|
|
43
|
+
}): Promise<CcSwitchBalance | undefined>;
|
|
27
44
|
/**
|
|
28
45
|
* Query one provider's balance the way cc-switch does.
|
|
29
46
|
*
|
|
30
|
-
*
|
|
47
|
+
* cc-switch reads a DeepSeek-compatible provider's balance with `GET <base>/user/balance`
|
|
48
|
+
* and a bearer API key. For the official DeepSeek that base is `api.deepseek.com`, so the
|
|
49
|
+
* path is effectively hardcoded there; for a provider the user added through a custom
|
|
50
|
+
* endpoint (like huanling), the same path is read against that provider's own base URL,
|
|
51
|
+
* which is the only place its balance can live.
|
|
52
|
+
*
|
|
53
|
+
* @param input - the provider's API key and base URL.
|
|
31
54
|
* @returns the balance, or undefined when the provider did not answer with one.
|
|
32
55
|
*/
|
|
33
56
|
export declare function queryCcSwitchBalance(input: {
|
|
34
57
|
apiKey: string;
|
|
58
|
+
/** The provider's own base URL; `https://api.deepseek.com` for the official one. */
|
|
59
|
+
baseUrl: string;
|
|
35
60
|
fetchFn?: FetchFn;
|
|
36
61
|
}): Promise<CcSwitchBalance | undefined>;
|
package/lib/auth/ccswitch.js
CHANGED
|
@@ -23,18 +23,75 @@ function num(value) {
|
|
|
23
23
|
return value.trim();
|
|
24
24
|
return undefined;
|
|
25
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* Query the balance endpoint some OpenAI-compatible providers expose at `{base}/v1/usage`.
|
|
28
|
+
*
|
|
29
|
+
* huanling is one: it answers `GET {base}/v1/usage` with `{ remaining, unit, … }`, which is
|
|
30
|
+
* the figure cc-switch shows for it. This is the provider's own shape, not cc-switch's
|
|
31
|
+
* `balance_infos`, so it is read separately. The base has any trailing `/v1` stripped before
|
|
32
|
+
* the path is added, so both `https://api.huanling.icu` and `https://api.huanling.icu/v1`
|
|
33
|
+
* resolve to the same `{base}/v1/usage`.
|
|
34
|
+
*
|
|
35
|
+
* @param input - the provider's API key and base URL.
|
|
36
|
+
* @returns the balance, or undefined when the provider did not answer with one.
|
|
37
|
+
*/
|
|
38
|
+
export async function queryUsageBalance(input) {
|
|
39
|
+
const fetchFn = input.fetchFn ?? proxiedFetch;
|
|
40
|
+
const base = input.baseUrl.replace(/\/+$/, '').replace(/\/v1$/i, '');
|
|
41
|
+
const url = `${base}/v1/usage`;
|
|
42
|
+
let response;
|
|
43
|
+
try {
|
|
44
|
+
response = await fetchFn(url, {
|
|
45
|
+
headers: { authorization: `Bearer ${input.apiKey}`, accept: 'application/json' },
|
|
46
|
+
signal: AbortSignal.timeout(15_000),
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
if (!response.ok)
|
|
53
|
+
return undefined;
|
|
54
|
+
let payload;
|
|
55
|
+
try {
|
|
56
|
+
payload = await response.json();
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
if (typeof payload !== 'object' || payload === null)
|
|
62
|
+
return undefined;
|
|
63
|
+
const record = payload;
|
|
64
|
+
const total = num(record.remaining);
|
|
65
|
+
if (total === undefined)
|
|
66
|
+
return undefined;
|
|
67
|
+
const currency = typeof record.unit === 'string' && record.unit.trim() !== '' ? record.unit.trim() : 'USD';
|
|
68
|
+
return {
|
|
69
|
+
currency,
|
|
70
|
+
total,
|
|
71
|
+
available: record.isValid !== false && record.is_active !== false,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
26
74
|
/**
|
|
27
75
|
* Query one provider's balance the way cc-switch does.
|
|
28
76
|
*
|
|
29
|
-
*
|
|
77
|
+
* cc-switch reads a DeepSeek-compatible provider's balance with `GET <base>/user/balance`
|
|
78
|
+
* and a bearer API key. For the official DeepSeek that base is `api.deepseek.com`, so the
|
|
79
|
+
* path is effectively hardcoded there; for a provider the user added through a custom
|
|
80
|
+
* endpoint (like huanling), the same path is read against that provider's own base URL,
|
|
81
|
+
* which is the only place its balance can live.
|
|
82
|
+
*
|
|
83
|
+
* @param input - the provider's API key and base URL.
|
|
30
84
|
* @returns the balance, or undefined when the provider did not answer with one.
|
|
31
85
|
*/
|
|
32
86
|
export async function queryCcSwitchBalance(input) {
|
|
33
87
|
const fetchFn = input.fetchFn ?? proxiedFetch;
|
|
88
|
+
// Resolve `<base>/user/balance` without doubling or dropping the path: a base that
|
|
89
|
+
// already ends in `/v1` keeps it, since that is the path the OpenAI-compatible wire
|
|
90
|
+
// uses and the balance endpoint lives beside it.
|
|
91
|
+
const url = `${input.baseUrl.replace(/\/+$/, '')}/user/balance`;
|
|
34
92
|
let response;
|
|
35
93
|
try {
|
|
36
|
-
|
|
37
|
-
response = await fetchFn('https://api.deepseek.com/user/balance', {
|
|
94
|
+
response = await fetchFn(url, {
|
|
38
95
|
headers: { authorization: `Bearer ${input.apiKey}`, accept: 'application/json' },
|
|
39
96
|
signal: AbortSignal.timeout(15_000),
|
|
40
97
|
});
|
package/lib/auth/rpc.d.ts
CHANGED
|
@@ -16,6 +16,8 @@ export type CredentialResolver = (ref: string) => Promise<string | undefined>;
|
|
|
16
16
|
export interface BalanceContext {
|
|
17
17
|
/** The credential reference each provider id's balance query uses. */
|
|
18
18
|
readonly credentialRefs: Readonly<Record<string, string>>;
|
|
19
|
+
/** The base URL each provider id's balance query reads, when it is not DeepSeek's own. */
|
|
20
|
+
readonly baseUrls: Readonly<Record<string, string>>;
|
|
19
21
|
/** Resolve one credential reference to its key. */
|
|
20
22
|
readonly resolveCredential?: CredentialResolver;
|
|
21
23
|
/**
|
package/lib/auth/rpc.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { BAD_REQUEST, ProviderError } from '../errors.js';
|
|
2
|
-
import { queryCcSwitchBalance } from './ccswitch.js';
|
|
2
|
+
import { queryCcSwitchBalance, queryUsageBalance } from './ccswitch.js';
|
|
3
3
|
import { readDeepSeekBalance } from './deepseek-balance.js';
|
|
4
4
|
import { RPC_ENDPOINTS, RPC_PREFIX } from '../protocol.js';
|
|
5
5
|
/** Wrap a value as a successful envelope result. */
|
|
@@ -165,11 +165,12 @@ async function runEndpoint(registry, endpoint, payload, balance) {
|
|
|
165
165
|
return await readDeepSeekBalance(balance?.resolveDeepSeekAccount?.(), client);
|
|
166
166
|
}
|
|
167
167
|
case 'ccswitch-balance': {
|
|
168
|
-
// A non-subscription provider's balance, by cc-switch's query
|
|
169
|
-
// the provider; the configured map turns that into
|
|
170
|
-
// provider's profile uses
|
|
171
|
-
//
|
|
172
|
-
//
|
|
168
|
+
// A non-subscription provider's balance, by cc-switch's query against the provider's
|
|
169
|
+
// own base URL. The caller names the provider; the configured map turns that into
|
|
170
|
+
// the credential reference the provider's profile uses and the base URL that profile
|
|
171
|
+
// connects to, and the host's credential service resolves the reference. No entry,
|
|
172
|
+
// no key, or a key the provider does not answer for means no balance — never a
|
|
173
|
+
// fabricated zero.
|
|
173
174
|
const provider = requiredProvider(payload, endpoint);
|
|
174
175
|
const credentialRef = balance?.credentialRefs[provider];
|
|
175
176
|
if (credentialRef === undefined || balance?.resolveCredential === undefined)
|
|
@@ -177,7 +178,12 @@ async function runEndpoint(registry, endpoint, payload, balance) {
|
|
|
177
178
|
const apiKey = await balance.resolveCredential(credentialRef);
|
|
178
179
|
if (apiKey === undefined)
|
|
179
180
|
return undefined;
|
|
180
|
-
|
|
181
|
+
const baseUrl = balance?.baseUrls[provider] ?? 'https://api.deepseek.com';
|
|
182
|
+
// A provider that answers the OpenAI-compatible `/v1/usage` (the shape cc-switch's
|
|
183
|
+
// custom template reads, `{ remaining, unit }`) is asked that first; one that
|
|
184
|
+
// answers the DeepSeek `balance_infos` shape is asked that as a fallback.
|
|
185
|
+
return await queryUsageBalance({ apiKey, baseUrl })
|
|
186
|
+
?? await queryCcSwitchBalance({ apiKey, baseUrl });
|
|
181
187
|
}
|
|
182
188
|
case 'reload':
|
|
183
189
|
return registry.reload();
|
package/lib/index.d.ts
CHANGED
|
@@ -45,16 +45,22 @@ export interface Config {
|
|
|
45
45
|
*/
|
|
46
46
|
auto?: boolean;
|
|
47
47
|
/**
|
|
48
|
-
* The
|
|
49
|
-
* provider id.
|
|
48
|
+
* The balance source a non-subscription provider uses, by provider id.
|
|
50
49
|
*
|
|
51
50
|
* A provider that is not a subscription this plugin serves has no usage windows to
|
|
52
51
|
* meter, so the chip shows its balance instead — read by cc-switch's query, which needs
|
|
53
|
-
* the provider's API key
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
52
|
+
* the provider's API key and its own base URL (the balance endpoint lives beside the
|
|
53
|
+
* provider's OpenAI-compatible wire, not on DeepSeek's). Each entry names the
|
|
54
|
+
* credential reference the provider's profile uses (`apiKeyEnv`) and the base URL that
|
|
55
|
+
* profile connects to; the host's credential service resolves the reference. A provider
|
|
56
|
+
* with no entry, or whose key does not resolve, gets no balance and the chip stays
|
|
57
|
+
* silent.
|
|
57
58
|
*/
|
|
59
|
+
balanceSources?: Record<string, {
|
|
60
|
+
credentialRef: string;
|
|
61
|
+
baseUrl: string;
|
|
62
|
+
}>;
|
|
63
|
+
/** @deprecated Use {@link balanceSources}, which carries the base URL too. */
|
|
58
64
|
balanceCredentialRefs?: Record<string, string>;
|
|
59
65
|
}
|
|
60
66
|
/** Configuration schema, with the config file's own defaults applied. */
|
package/lib/index.js
CHANGED
|
@@ -42,6 +42,8 @@ export const Config = z.object({
|
|
|
42
42
|
defaultModel: z.string().default(KIMI_DEFAULT_MODEL),
|
|
43
43
|
extraModels: z.array(z.string()).default([]),
|
|
44
44
|
auto: z.boolean().default(true),
|
|
45
|
+
balanceSources: z.dict(z.object({ credentialRef: z.string(), baseUrl: z.string() })).default({}),
|
|
46
|
+
balanceCredentialRefs: z.dict(z.string()).default({}),
|
|
45
47
|
});
|
|
46
48
|
/**
|
|
47
49
|
* Register every configured provider route, its credential store, and — when
|
|
@@ -188,13 +190,18 @@ export function apply(ctx, config = {}) {
|
|
|
188
190
|
handles.set(route.id, handle);
|
|
189
191
|
ctx.effect(() => () => handle(), `${name}: ${route.id} provider route`);
|
|
190
192
|
}
|
|
191
|
-
// A non-subscription provider's balance is read by cc-switch's query
|
|
192
|
-
// provider's
|
|
193
|
-
//
|
|
194
|
-
//
|
|
195
|
-
//
|
|
196
|
-
|
|
197
|
-
const credentialRefs = config.balanceCredentialRefs
|
|
193
|
+
// A non-subscription provider's balance is read by cc-switch's query against the
|
|
194
|
+
// provider's own base URL (the balance endpoint lives beside its OpenAI-compatible
|
|
195
|
+
// wire, not on DeepSeek's), which needs the provider's API key. Each configured source
|
|
196
|
+
// names the credential reference the provider's profile uses (`apiKeyEnv`) and the base
|
|
197
|
+
// URL that profile connects to; the host's credential service resolves the reference.
|
|
198
|
+
const balanceSources = config.balanceSources ?? {};
|
|
199
|
+
const credentialRefs = { ...config.balanceCredentialRefs };
|
|
200
|
+
const baseUrls = {};
|
|
201
|
+
for (const [provider, source] of Object.entries(balanceSources)) {
|
|
202
|
+
credentialRefs[provider] = source.credentialRef;
|
|
203
|
+
baseUrls[provider] = source.baseUrl;
|
|
204
|
+
}
|
|
198
205
|
// The DeepSeek account's balance is read on the node half through the host's
|
|
199
206
|
// `deepseekAccount` service; the browser reaches it through the plugin's own RPC, the
|
|
200
207
|
// same channel as the rest of the chip's data. The service is read lazily, per request
|
|
@@ -202,6 +209,7 @@ export function apply(ctx, config = {}) {
|
|
|
202
209
|
// without it still serves every other endpoint, and the balance endpoint answers nothing.
|
|
203
210
|
const balanceContext = {
|
|
204
211
|
credentialRefs,
|
|
212
|
+
baseUrls,
|
|
205
213
|
resolveDeepSeekAccount: () => {
|
|
206
214
|
try {
|
|
207
215
|
return ctx.get('deepseekAccount');
|
|
@@ -213,7 +221,20 @@ export function apply(ctx, config = {}) {
|
|
|
213
221
|
resolveCredential: async (ref) => {
|
|
214
222
|
try {
|
|
215
223
|
const credentials = ctx.get('credentials');
|
|
216
|
-
|
|
224
|
+
const hit = await credentials?.resolve?.(ref);
|
|
225
|
+
// The resolve answer is `{ value, source }`; an env-var source may name the var
|
|
226
|
+
// rather than carry the value, so both are considered before giving up.
|
|
227
|
+
if (typeof hit === 'object' && hit !== null) {
|
|
228
|
+
const record = hit;
|
|
229
|
+
if (typeof record.value === 'string' && record.value.trim() !== '')
|
|
230
|
+
return record.value;
|
|
231
|
+
if (typeof record.source === 'string') {
|
|
232
|
+
const fromEnv = process.env[record.source];
|
|
233
|
+
if (typeof fromEnv === 'string' && fromEnv.trim() !== '')
|
|
234
|
+
return fromEnv;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return undefined;
|
|
217
238
|
}
|
|
218
239
|
catch {
|
|
219
240
|
return undefined;
|
package/lib/providers/kimi.d.ts
CHANGED
|
@@ -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[]>;
|
package/lib/providers/kimi.js
CHANGED
|
@@ -14,7 +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 {
|
|
17
|
+
import { prepareMoonshotTool } from '../translate/moonshot-schema.js';
|
|
18
18
|
/**
|
|
19
19
|
* Coerce one decoded credential entry into a Kimi session.
|
|
20
20
|
* @param value - decoded JSON value for one account.
|
|
@@ -131,10 +131,31 @@ export class KimiAdapter extends LlmAdapter {
|
|
|
131
131
|
discovered;
|
|
132
132
|
/** Ids the user added by hand, cached so the sync catalog can name them. */
|
|
133
133
|
userAddedIds = [];
|
|
134
|
+
/** Tools already reported as flattened, so each is stated once. */
|
|
135
|
+
flattenedTools = new Set();
|
|
134
136
|
constructor(options) {
|
|
135
137
|
super();
|
|
136
138
|
this.options = options;
|
|
137
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
|
+
}
|
|
138
159
|
providerInfo(provider) {
|
|
139
160
|
return { id: provider, name: KIMI_PROVIDER_NAME };
|
|
140
161
|
}
|
|
@@ -311,7 +332,11 @@ export class KimiAdapter extends LlmAdapter {
|
|
|
311
332
|
input: resolved.input,
|
|
312
333
|
...options.tools === undefined || options.tools.length === 0
|
|
313
334
|
? {}
|
|
314
|
-
: {
|
|
335
|
+
: {
|
|
336
|
+
tools: toResponsesTools(this.moonshotTools(options.tools)),
|
|
337
|
+
tool_choice: 'auto',
|
|
338
|
+
parallel_tool_calls: true,
|
|
339
|
+
},
|
|
315
340
|
...options.maxTokens === undefined ? {} : { max_output_tokens: options.maxTokens },
|
|
316
341
|
...options.reasoningEffort === undefined
|
|
317
342
|
? {}
|
|
@@ -1,31 +1,54 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Tool-schema preparation for the Moonshot wire (Kimi Code).
|
|
3
3
|
*
|
|
4
|
-
* Moonshot validates `tools[].function.parameters` with its own
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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.
|
|
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`.
|
|
14
10
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
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.
|
|
22
30
|
*
|
|
23
31
|
* @module dsh-plugin-subscription-oauth/translate/moonshot-schema
|
|
24
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
|
+
}
|
|
25
48
|
/**
|
|
26
49
|
* Prepare one tool's parameters for Moonshot.
|
|
27
50
|
*
|
|
28
51
|
* @param parameters - the tool's declared argument schema.
|
|
29
|
-
* @returns
|
|
52
|
+
* @returns the schema to send and what it had to give up to fit the endpoint.
|
|
30
53
|
*/
|
|
31
|
-
export declare function
|
|
54
|
+
export declare function prepareMoonshotTool(parameters: Record<string, unknown>): MoonshotToolSchema;
|
|
@@ -1,27 +1,47 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Tool-schema preparation for the Moonshot wire (Kimi Code).
|
|
3
3
|
*
|
|
4
|
-
* Moonshot validates `tools[].function.parameters` with its own
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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.
|
|
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`.
|
|
14
10
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
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.
|
|
22
30
|
*
|
|
23
31
|
* @module dsh-plugin-subscription-oauth/translate/moonshot-schema
|
|
24
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;
|
|
25
45
|
/** True for a plain object usable as a schema node. */
|
|
26
46
|
function object(value) {
|
|
27
47
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
@@ -43,7 +63,7 @@ function pointed(root, ref) {
|
|
|
43
63
|
return node;
|
|
44
64
|
}
|
|
45
65
|
/**
|
|
46
|
-
*
|
|
66
|
+
* Replace every reference reachable from one node with what it names.
|
|
47
67
|
*
|
|
48
68
|
* @param node - the node to rewrite.
|
|
49
69
|
* @param root - the tool's root schema, which local pointers resolve against.
|
|
@@ -56,26 +76,67 @@ function inline(node, root, refs) {
|
|
|
56
76
|
if (!object(node))
|
|
57
77
|
return node;
|
|
58
78
|
const { $ref, ...siblings } = node;
|
|
59
|
-
if (typeof $ref
|
|
60
|
-
|
|
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);
|
|
61
86
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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));
|
|
72
124
|
}
|
|
73
125
|
/**
|
|
74
126
|
* Prepare one tool's parameters for Moonshot.
|
|
75
127
|
*
|
|
76
128
|
* @param parameters - the tool's declared argument schema.
|
|
77
|
-
* @returns
|
|
129
|
+
* @returns the schema to send and what it had to give up to fit the endpoint.
|
|
78
130
|
*/
|
|
79
|
-
export function
|
|
80
|
-
|
|
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' };
|
|
81
142
|
}
|
|
@@ -53,11 +53,9 @@ 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.
|
|
58
56
|
* @returns `tools` array entries.
|
|
59
57
|
*/
|
|
60
|
-
export declare function toResponsesTools(tools: readonly ToolSchema[]
|
|
58
|
+
export declare function toResponsesTools(tools: readonly ToolSchema[]): Record<string, unknown>[];
|
|
61
59
|
/** The `usage` object of a completed response. */
|
|
62
60
|
export interface ResponsesUsage {
|
|
63
61
|
input_tokens?: number;
|
|
@@ -124,16 +124,14 @@ 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.
|
|
129
127
|
* @returns `tools` array entries.
|
|
130
128
|
*/
|
|
131
|
-
export function toResponsesTools(tools
|
|
129
|
+
export function toResponsesTools(tools) {
|
|
132
130
|
return tools.map((tool) => ({
|
|
133
131
|
type: 'function',
|
|
134
132
|
name: tool.name,
|
|
135
133
|
description: tool.description,
|
|
136
|
-
parameters:
|
|
134
|
+
parameters: tool.parameters,
|
|
137
135
|
strict: false,
|
|
138
136
|
}));
|
|
139
137
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-subscription-oauth",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.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",
|