dsh-plugin-subscription-oauth 0.0.0-stage → 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/LICENSE +21 -0
- package/README.md +413 -2
- package/cordis.patch.yml +13 -0
- package/lib/auth/device-flow.d.ts +112 -0
- package/lib/auth/device-flow.js +319 -0
- package/lib/auth/device-id.d.ts +31 -0
- package/lib/auth/device-id.js +58 -0
- package/lib/auth/flow.d.ts +33 -0
- package/lib/auth/flow.js +12 -0
- package/lib/auth/glm-flow.d.ts +99 -0
- package/lib/auth/glm-flow.js +194 -0
- package/lib/auth/headers.d.ts +61 -0
- package/lib/auth/headers.js +106 -0
- package/lib/auth/pkce-flow.d.ts +78 -0
- package/lib/auth/pkce-flow.js +205 -0
- package/lib/auth/provider-controller.d.ts +241 -0
- package/lib/auth/provider-controller.js +502 -0
- package/lib/auth/registry.d.ts +91 -0
- package/lib/auth/registry.js +149 -0
- package/lib/auth/rpc.d.ts +31 -0
- package/lib/auth/rpc.js +200 -0
- package/lib/auth/store.d.ts +99 -0
- package/lib/auth/store.js +213 -0
- package/lib/auth/tokens.d.ts +76 -0
- package/lib/auth/tokens.js +103 -0
- package/lib/client/SubscriptionsSection.d.ts +34 -0
- package/lib/client/SubscriptionsSection.js +742 -0
- package/lib/client/index.d.ts +36 -0
- package/lib/client/index.js +47 -0
- package/lib/client/locales.d.ts +187 -0
- package/lib/client/locales.js +185 -0
- package/lib/client/styles.d.ts +18 -0
- package/lib/client/styles.js +257 -0
- package/lib/client/subscriptions-rpc.d.ts +24 -0
- package/lib/client/subscriptions-rpc.js +44 -0
- package/lib/client.js +2078 -0
- package/lib/client.js.map +1 -0
- package/lib/constants.d.ts +79 -0
- package/lib/constants.js +91 -0
- package/lib/errors.d.ts +28 -0
- package/lib/errors.js +34 -0
- package/lib/http.d.ts +24 -0
- package/lib/http.js +98 -0
- package/lib/index.d.ts +57 -0
- package/lib/index.js +185 -0
- package/lib/protocol.d.ts +316 -0
- package/lib/protocol.js +29 -0
- package/lib/providers/antigravity-catalog.d.ts +53 -0
- package/lib/providers/antigravity-catalog.js +269 -0
- package/lib/providers/antigravity.d.ts +331 -0
- package/lib/providers/antigravity.js +821 -0
- package/lib/providers/auto.d.ts +151 -0
- package/lib/providers/auto.js +351 -0
- package/lib/providers/catalog.d.ts +69 -0
- package/lib/providers/catalog.js +78 -0
- package/lib/providers/common.d.ts +51 -0
- package/lib/providers/common.js +169 -0
- package/lib/providers/glm-provider.d.ts +41 -0
- package/lib/providers/glm-provider.js +165 -0
- package/lib/providers/glm.d.ts +159 -0
- package/lib/providers/glm.js +445 -0
- package/lib/providers/grok.d.ts +224 -0
- package/lib/providers/grok.js +700 -0
- package/lib/providers/kimi-provider.d.ts +45 -0
- package/lib/providers/kimi-provider.js +148 -0
- package/lib/providers/kimi.d.ts +151 -0
- package/lib/providers/kimi.js +400 -0
- package/lib/providers/responses-stream.d.ts +58 -0
- package/lib/providers/responses-stream.js +115 -0
- package/lib/providers/usage.d.ts +65 -0
- package/lib/providers/usage.js +309 -0
- package/lib/settings.d.ts +122 -0
- package/lib/settings.js +333 -0
- package/lib/translate/antigravity-schema.d.ts +24 -0
- package/lib/translate/antigravity-schema.js +174 -0
- package/lib/translate/antigravity.d.ts +184 -0
- package/lib/translate/antigravity.js +419 -0
- package/lib/translate/responses.d.ts +156 -0
- package/lib/translate/responses.js +395 -0
- package/lib/translate/sse.d.ts +27 -0
- package/lib/translate/sse.js +82 -0
- package/package.json +126 -4
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `kimi` provider route: Kimi Code model catalog, request assembly over
|
|
3
|
+
* the OpenAI Responses wire, and streaming.
|
|
4
|
+
*
|
|
5
|
+
* @module dsh-plugin-subscription-oauth/providers/kimi
|
|
6
|
+
*/
|
|
7
|
+
import { EMPTY_RESPONSE_CODE, errorChain, LlmAdapter, LlmError, ReasoningEffortId, } from '@deepseek-ai/dsh-llm';
|
|
8
|
+
import { KIMI_CODE_URL, KIMI_CONTEXT_WINDOW, KIMI_DEFAULT_MAX_TOKENS, KIMI_DEFAULT_MODEL, KIMI_LONG_CONTEXT_WINDOW, KIMI_MODELS_PATH, KIMI_PROVIDER_ID, KIMI_PROVIDER_NAME, KIMI_RESPONSES_PATH, } from '../constants.js';
|
|
9
|
+
import { proxiedFetch } from '../http.js';
|
|
10
|
+
import { kimiRequestHeaders } from '../auth/headers.js';
|
|
11
|
+
import { REAUTH_REQUIRED, ProviderError } from '../errors.js';
|
|
12
|
+
import { finiteInstant, nonEmpty } from '../auth/store.js';
|
|
13
|
+
import { idleWatchdog, httpLlmError, isMissingOrInvalidCredential, mapFetchFailure } from './common.js';
|
|
14
|
+
import { streamProviderResponses } from './responses-stream.js';
|
|
15
|
+
import { fallbackEntry, findEntry, resolveCatalog } from './catalog.js';
|
|
16
|
+
import { streamResponses, toResponsesInput, toResponsesTools } from '../translate/responses.js';
|
|
17
|
+
/**
|
|
18
|
+
* Coerce one decoded credential entry into a Kimi session.
|
|
19
|
+
* @param value - decoded JSON value for one account.
|
|
20
|
+
* @returns the session, or undefined when required fields are missing.
|
|
21
|
+
*/
|
|
22
|
+
export function parseKimiSession(value) {
|
|
23
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
24
|
+
return undefined;
|
|
25
|
+
const record = value;
|
|
26
|
+
const accessToken = nonEmpty(record.accessToken);
|
|
27
|
+
const oauthHost = nonEmpty(record.oauthHost);
|
|
28
|
+
const expiresAt = finiteInstant(record.expiresAt);
|
|
29
|
+
if (accessToken === undefined || oauthHost === undefined || expiresAt === undefined)
|
|
30
|
+
return undefined;
|
|
31
|
+
const refreshToken = nonEmpty(record.refreshToken);
|
|
32
|
+
const apiBase = nonEmpty(record.apiBase);
|
|
33
|
+
const accountId = nonEmpty(record.accountId);
|
|
34
|
+
const email = nonEmpty(record.email);
|
|
35
|
+
return {
|
|
36
|
+
accessToken,
|
|
37
|
+
expiresAt,
|
|
38
|
+
oauthHost,
|
|
39
|
+
...refreshToken === undefined ? {} : { refreshToken },
|
|
40
|
+
...apiBase === undefined ? {} : { apiBase },
|
|
41
|
+
...accountId === undefined ? {} : { accountId },
|
|
42
|
+
...email === undefined ? {} : { email },
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/** Harness effort ids normalised onto the ladder Kimi's endpoint accepts. */
|
|
46
|
+
const EFFORT_WIRE_MAP = {
|
|
47
|
+
none: 'none',
|
|
48
|
+
low: 'low',
|
|
49
|
+
medium: 'high',
|
|
50
|
+
high: 'high',
|
|
51
|
+
xhigh: 'max',
|
|
52
|
+
max: 'max',
|
|
53
|
+
};
|
|
54
|
+
/** Display metadata for the reasoning ladder shared by every live coding model. */
|
|
55
|
+
const EFFORT_LABELS = {
|
|
56
|
+
none: { name: 'Off', description: 'Answer without spending thinking tokens' },
|
|
57
|
+
low: { name: 'Low', description: 'Short chain of thought' },
|
|
58
|
+
medium: { name: 'Medium', description: 'Balanced thinking budget' },
|
|
59
|
+
high: { name: 'High', description: 'Deep chain of thought' },
|
|
60
|
+
xhigh: { name: 'Max', description: 'Highest thinking budget Kimi accepts' },
|
|
61
|
+
max: { name: 'Max', description: 'Highest thinking budget Kimi accepts' },
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Kimi Code's live coding catalog.
|
|
65
|
+
*
|
|
66
|
+
* `kimi-for-coding` is the stable subscription alias Moonshot re-points at each
|
|
67
|
+
* coding release; `k3` is the current flagship, with the `[1m]` suffix selecting
|
|
68
|
+
* its 1M-token window.
|
|
69
|
+
*/
|
|
70
|
+
export const KIMI_MODELS = [
|
|
71
|
+
{
|
|
72
|
+
id: 'kimi-for-coding',
|
|
73
|
+
name: 'Kimi for Coding',
|
|
74
|
+
description: 'Stable subscription alias — 1M-token window',
|
|
75
|
+
aliases: ['kimi-for-coding-highspeed', 'kimi-k2.7-code', 'kimi-k2.7-code-highspeed', 'kimi-k2.6', 'kimi-k2.5'],
|
|
76
|
+
contextWindow: KIMI_LONG_CONTEXT_WINDOW,
|
|
77
|
+
inputModalities: ['text', 'image'],
|
|
78
|
+
reasoningEfforts: ['low', 'medium', 'high', 'max'],
|
|
79
|
+
defaultEffort: 'max',
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
id: 'k3',
|
|
83
|
+
name: 'Kimi K3',
|
|
84
|
+
description: 'Flagship reasoning model',
|
|
85
|
+
aliases: ['kimi-k3'],
|
|
86
|
+
contextWindow: KIMI_CONTEXT_WINDOW,
|
|
87
|
+
inputModalities: ['text', 'image'],
|
|
88
|
+
reasoningEfforts: ['low', 'medium', 'high', 'max'],
|
|
89
|
+
defaultEffort: 'max',
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
id: 'k3[1m]',
|
|
93
|
+
name: 'Kimi K3 (1M)',
|
|
94
|
+
description: 'Flagship reasoning model with the 1M-token window',
|
|
95
|
+
contextWindow: KIMI_LONG_CONTEXT_WINDOW,
|
|
96
|
+
inputModalities: ['text', 'image'],
|
|
97
|
+
reasoningEfforts: ['low', 'medium', 'high', 'max'],
|
|
98
|
+
defaultEffort: 'max',
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
id: 'k3-256k',
|
|
102
|
+
name: 'Kimi K3 (256K)',
|
|
103
|
+
description: 'Flagship reasoning model pinned to the standard window',
|
|
104
|
+
contextWindow: KIMI_CONTEXT_WINDOW,
|
|
105
|
+
inputModalities: ['text', 'image'],
|
|
106
|
+
reasoningEfforts: ['low', 'medium', 'high', 'max'],
|
|
107
|
+
defaultEffort: 'max',
|
|
108
|
+
},
|
|
109
|
+
];
|
|
110
|
+
/** One `reasoning.efforts` entry for a resolved model. */
|
|
111
|
+
function effortInfo(effort) {
|
|
112
|
+
const label = EFFORT_LABELS[effort] ?? { name: effort, description: '' };
|
|
113
|
+
return { id: ReasoningEffortId(effort), name: label.name, description: label.description };
|
|
114
|
+
}
|
|
115
|
+
/** The reasoning block for a catalog entry, or undefined when it supports none. */
|
|
116
|
+
function reasoningInfo(entry) {
|
|
117
|
+
const efforts = entry.reasoningEfforts;
|
|
118
|
+
if (efforts === undefined || efforts.length === 0)
|
|
119
|
+
return undefined;
|
|
120
|
+
const defaultEffort = entry.defaultEffort ?? efforts[efforts.length - 1];
|
|
121
|
+
return {
|
|
122
|
+
efforts: efforts.map(effortInfo),
|
|
123
|
+
...defaultEffort === undefined ? {} : { defaultEffort: ReasoningEffortId(defaultEffort) },
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
/** Kimi Code adapter serving the `kimi` provider route. */
|
|
127
|
+
export class KimiAdapter extends LlmAdapter {
|
|
128
|
+
options;
|
|
129
|
+
/** Live catalog ids seen this session, used to accept renamed models. */
|
|
130
|
+
discovered;
|
|
131
|
+
/** Ids the user added by hand, cached so the sync catalog can name them. */
|
|
132
|
+
userAddedIds = [];
|
|
133
|
+
constructor(options) {
|
|
134
|
+
super();
|
|
135
|
+
this.options = options;
|
|
136
|
+
}
|
|
137
|
+
providerInfo(provider) {
|
|
138
|
+
return { id: provider, name: KIMI_PROVIDER_NAME };
|
|
139
|
+
}
|
|
140
|
+
providerRetryPolicy() {
|
|
141
|
+
// Kimi reports its own pacing through 429 `retry-after`; a local retry
|
|
142
|
+
// ladder would fight the account-level rate limiter.
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
145
|
+
async listModels(_provider) {
|
|
146
|
+
// Nothing to offer without a signed-in account: a model that cannot be called
|
|
147
|
+
// must not appear in the picker, and Auto must not try it.
|
|
148
|
+
if (await this.options.tokens.session() === undefined)
|
|
149
|
+
return [];
|
|
150
|
+
await this.syncUserModels();
|
|
151
|
+
const hidden = await this.hidden();
|
|
152
|
+
return resolveCatalog(this.catalog(), [], hidden).entries.map((entry) => ({
|
|
153
|
+
provider: KIMI_PROVIDER_ID,
|
|
154
|
+
id: entry.id,
|
|
155
|
+
name: entry.name,
|
|
156
|
+
...entry.description === undefined ? {} : { description: entry.description },
|
|
157
|
+
...entry.inputModalities === undefined ? {} : { inputModalities: entry.inputModalities },
|
|
158
|
+
}));
|
|
159
|
+
}
|
|
160
|
+
/** The ids the picker must currently omit. */
|
|
161
|
+
async hidden() {
|
|
162
|
+
if (this.options.hiddenModels === undefined)
|
|
163
|
+
return new Set();
|
|
164
|
+
try {
|
|
165
|
+
return await this.options.hiddenModels();
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
// A preferences read failure must not empty the picker.
|
|
169
|
+
this.options.onWarn?.(`model preferences unavailable (${errorChain(error)})`);
|
|
170
|
+
return new Set();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
async resolveModel(_provider, model) {
|
|
174
|
+
await this.syncUserModels();
|
|
175
|
+
const entry = this.entryFor(model);
|
|
176
|
+
if (entry === undefined) {
|
|
177
|
+
// An unknown id is still dispatchable: the endpoint accepts the id it
|
|
178
|
+
// reports, and a model added after this build should not be refused. It
|
|
179
|
+
// inherits the standard window and the shared reasoning ladder.
|
|
180
|
+
const template = this.entryFor(KIMI_DEFAULT_MODEL) ?? this.catalog()[0];
|
|
181
|
+
const reasoning = template === undefined ? undefined : reasoningInfo(template);
|
|
182
|
+
return {
|
|
183
|
+
provider: KIMI_PROVIDER_ID,
|
|
184
|
+
id: model,
|
|
185
|
+
name: model,
|
|
186
|
+
context: { contextWindow: KIMI_CONTEXT_WINDOW },
|
|
187
|
+
defaultMaxTokens: KIMI_DEFAULT_MAX_TOKENS,
|
|
188
|
+
...reasoning === undefined ? {} : { reasoning },
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
const reasoning = reasoningInfo(entry);
|
|
192
|
+
return {
|
|
193
|
+
provider: KIMI_PROVIDER_ID,
|
|
194
|
+
id: entry.id,
|
|
195
|
+
name: entry.name,
|
|
196
|
+
...entry.description === undefined ? {} : { description: entry.description },
|
|
197
|
+
...entry.inputModalities === undefined ? {} : { inputModalities: entry.inputModalities },
|
|
198
|
+
context: { contextWindow: entry.contextWindow },
|
|
199
|
+
defaultMaxTokens: KIMI_DEFAULT_MAX_TOKENS,
|
|
200
|
+
...reasoning === undefined ? {} : { reasoning },
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
stream(options) {
|
|
204
|
+
return streamProviderResponses(options, {
|
|
205
|
+
label: 'Kimi API',
|
|
206
|
+
tokens: this.options.tokens,
|
|
207
|
+
request: (call, model, accessToken, signal) => this.request(call, model, accessToken, signal),
|
|
208
|
+
streamIdleTimeoutMs: this.options.streamIdleTimeoutMs,
|
|
209
|
+
...this.options.onCredentialRejected === undefined
|
|
210
|
+
? {}
|
|
211
|
+
: { onCredentialRejected: this.options.onCredentialRejected },
|
|
212
|
+
onFallback: (from, to, reason) => {
|
|
213
|
+
this.options.onWarn?.(`kimi: model ${from} unavailable (${reason}); falling back to ${to}`);
|
|
214
|
+
},
|
|
215
|
+
describeFailure: (error) => this.refreshHint(error),
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
/** Translate an auth failure into the message a user can act on. */
|
|
219
|
+
refreshHint(error) {
|
|
220
|
+
if (error instanceof ProviderError && error.code === REAUTH_REQUIRED) {
|
|
221
|
+
return new LlmError(`Kimi Code login expired; sign in again from Settings → Subscriptions (${KIMI_CODE_URL})`, 'INVALID_CREDENTIAL', { status: 401 });
|
|
222
|
+
}
|
|
223
|
+
return error;
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* The catalog the route serves: its own entries, then the ids the user added
|
|
227
|
+
* by hand. The two are built separately so the hand-added check can look at
|
|
228
|
+
* the shipped list without recursing.
|
|
229
|
+
*/
|
|
230
|
+
catalog() {
|
|
231
|
+
return [...this.shippedCatalog(), ...this.addedEntries()];
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* The catalog to present: the shipped entries in their curated order and with
|
|
235
|
+
* their curated metadata, then any configured extra id, then any live id this
|
|
236
|
+
* build does not know.
|
|
237
|
+
*
|
|
238
|
+
* The live list never removes a shipped entry: the endpoint serves variants
|
|
239
|
+
* such as `k3[1m]` that its own `/models` listing omits, so absence there is
|
|
240
|
+
* not evidence of retirement.
|
|
241
|
+
*/
|
|
242
|
+
shippedCatalog() {
|
|
243
|
+
const extra = [];
|
|
244
|
+
const seen = new Set();
|
|
245
|
+
const append = (id, description) => {
|
|
246
|
+
if (seen.has(id) || this.entryFor(id) !== undefined)
|
|
247
|
+
return;
|
|
248
|
+
seen.add(id);
|
|
249
|
+
extra.push(fallbackEntry(id, description, KIMI_CONTEXT_WINDOW));
|
|
250
|
+
};
|
|
251
|
+
for (const id of this.options.extraModels ?? [])
|
|
252
|
+
append(id, 'Extra model from the plugin configuration');
|
|
253
|
+
for (const id of this.discovered ?? [])
|
|
254
|
+
append(id, 'Reported by the Kimi Code model list');
|
|
255
|
+
return extra.length === 0 ? KIMI_MODELS : [...KIMI_MODELS, ...extra];
|
|
256
|
+
}
|
|
257
|
+
/** Whether one shipped entry covers a live model id. */
|
|
258
|
+
entryMatches(entry, id) {
|
|
259
|
+
return entry.id === id || entry.aliases?.includes(id) === true;
|
|
260
|
+
}
|
|
261
|
+
/** Resolve a requested model id onto its catalog entry. */
|
|
262
|
+
entryFor(model) {
|
|
263
|
+
return findEntry(KIMI_MODELS, model);
|
|
264
|
+
}
|
|
265
|
+
/** Re-read the ids the user added by hand. */
|
|
266
|
+
async syncUserModels() {
|
|
267
|
+
if (this.options.userModels === undefined)
|
|
268
|
+
return;
|
|
269
|
+
try {
|
|
270
|
+
this.userAddedIds = await this.options.userModels();
|
|
271
|
+
}
|
|
272
|
+
catch (error) {
|
|
273
|
+
this.options.onWarn?.(`user-added models unavailable (${errorChain(error)})`);
|
|
274
|
+
this.userAddedIds = [];
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* The entries for ids the user added by hand, in the order they added them.
|
|
279
|
+
*
|
|
280
|
+
* Published without shipped metadata: this build has never seen the model, so
|
|
281
|
+
* the endpoint's own defaults are the honest description.
|
|
282
|
+
*/
|
|
283
|
+
addedEntries() {
|
|
284
|
+
if (this.userAddedIds.length === 0)
|
|
285
|
+
return [];
|
|
286
|
+
const known = new Set(this.shippedCatalog().map((entry) => entry.id));
|
|
287
|
+
return this.userAddedIds
|
|
288
|
+
.filter((id) => !known.has(id))
|
|
289
|
+
.map((id) => fallbackEntry(id, 'Added by hand in Settings', KIMI_CONTEXT_WINDOW));
|
|
290
|
+
}
|
|
291
|
+
/** Record the live catalog for the picker; failures keep the shipped list. */
|
|
292
|
+
discover(ids) {
|
|
293
|
+
this.discovered = ids;
|
|
294
|
+
}
|
|
295
|
+
/** The curated catalog, for the Settings page's model list. */
|
|
296
|
+
catalogEntries() {
|
|
297
|
+
return this.catalog();
|
|
298
|
+
}
|
|
299
|
+
/** What this route itself serves, without the ids added by hand. */
|
|
300
|
+
ownCatalogEntries() {
|
|
301
|
+
return this.shippedCatalog();
|
|
302
|
+
}
|
|
303
|
+
/** Build and send one request. */
|
|
304
|
+
async request(options, model, accessToken, signal) {
|
|
305
|
+
const resolveImage = this.imageResolver(signal);
|
|
306
|
+
const resolved = await toResponsesInput(options.messages, options.system, resolveImage);
|
|
307
|
+
const body = {
|
|
308
|
+
model,
|
|
309
|
+
...resolved.instructions === undefined ? {} : { instructions: resolved.instructions },
|
|
310
|
+
input: resolved.input,
|
|
311
|
+
...options.tools === undefined || options.tools.length === 0
|
|
312
|
+
? {}
|
|
313
|
+
: { tools: toResponsesTools(options.tools), tool_choice: 'auto', parallel_tool_calls: true },
|
|
314
|
+
...options.maxTokens === undefined ? {} : { max_output_tokens: options.maxTokens },
|
|
315
|
+
...options.reasoningEffort === undefined
|
|
316
|
+
? {}
|
|
317
|
+
: { reasoning: { effort: EFFORT_WIRE_MAP[String(options.reasoningEffort)] ?? String(options.reasoningEffort) } },
|
|
318
|
+
...options.sessionId === undefined ? {} : { prompt_cache_key: String(options.sessionId) },
|
|
319
|
+
// Ask for the encrypted reasoning payload so a later turn can replay the
|
|
320
|
+
// provider's own item rather than reconstructed text.
|
|
321
|
+
include: ['reasoning.encrypted_content'],
|
|
322
|
+
store: false,
|
|
323
|
+
stream: true,
|
|
324
|
+
};
|
|
325
|
+
// Kimi locks temperature and penalties on every subscription coding model,
|
|
326
|
+
// so a caller-supplied value is dropped rather than rejected upstream.
|
|
327
|
+
const fetchFn = this.options.fetchFn ?? proxiedFetch;
|
|
328
|
+
return fetchFn(`${this.options.apiBase}${KIMI_RESPONSES_PATH}`, {
|
|
329
|
+
method: 'POST',
|
|
330
|
+
headers: kimiRequestHeaders(await this.identity(), {
|
|
331
|
+
'authorization': `Bearer ${accessToken}`,
|
|
332
|
+
'accept': 'text/event-stream',
|
|
333
|
+
'content-type': 'application/json',
|
|
334
|
+
}),
|
|
335
|
+
body: JSON.stringify(body),
|
|
336
|
+
signal,
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
/** Client-identity headers for this machine, when configured. */
|
|
340
|
+
async identity() {
|
|
341
|
+
if (this.options.identityHeaders === undefined)
|
|
342
|
+
return {};
|
|
343
|
+
try {
|
|
344
|
+
return await this.options.identityHeaders();
|
|
345
|
+
}
|
|
346
|
+
catch (error) {
|
|
347
|
+
this.options.onWarn?.(`device identity unavailable (${errorChain(error)})`);
|
|
348
|
+
return {};
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
/** Resolve image blocks through the attachment service, when it is present. */
|
|
352
|
+
imageResolver(signal) {
|
|
353
|
+
const store = this.options.resolveAttachments?.();
|
|
354
|
+
if (store === undefined || store === null)
|
|
355
|
+
return undefined;
|
|
356
|
+
return async (ref) => {
|
|
357
|
+
try {
|
|
358
|
+
const stored = await store.readImage(ref, signal);
|
|
359
|
+
return { mediaType: ref.mediaType, base64: Buffer.from(stored.data).toString('base64') };
|
|
360
|
+
}
|
|
361
|
+
catch (error) {
|
|
362
|
+
this.options.onWarn?.(`image attachment unavailable (${errorChain(error)})`);
|
|
363
|
+
return undefined;
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
/** Read the live `/models` catalog, or undefined when unavailable. */
|
|
368
|
+
async fetchCatalog(accessToken, signal) {
|
|
369
|
+
if (!this.options.discovery)
|
|
370
|
+
return undefined;
|
|
371
|
+
const fetchFn = this.options.fetchFn ?? proxiedFetch;
|
|
372
|
+
try {
|
|
373
|
+
const response = await fetchFn(`${this.options.apiBase}${KIMI_MODELS_PATH}`, {
|
|
374
|
+
headers: kimiRequestHeaders(await this.identity(), { 'authorization': `Bearer ${accessToken}` }),
|
|
375
|
+
...signal === undefined ? {} : { signal },
|
|
376
|
+
});
|
|
377
|
+
if (!response.ok) {
|
|
378
|
+
if (!isMissingOrInvalidCredential(await httpLlmError(response, 'Kimi models'))) {
|
|
379
|
+
this.options.onWarn?.(`Kimi model discovery failed (HTTP ${response.status}); using the built-in catalog`);
|
|
380
|
+
}
|
|
381
|
+
return undefined;
|
|
382
|
+
}
|
|
383
|
+
const payload = await response.json();
|
|
384
|
+
const ids = (payload.data ?? [])
|
|
385
|
+
.map((entry) => entry.id)
|
|
386
|
+
.filter((id) => typeof id === 'string' && id.length > 0);
|
|
387
|
+
if (ids.length === 0) {
|
|
388
|
+
this.options.onWarn?.('Kimi model discovery returned an empty catalog; using the built-in catalog');
|
|
389
|
+
return undefined;
|
|
390
|
+
}
|
|
391
|
+
return ids;
|
|
392
|
+
}
|
|
393
|
+
catch (error) {
|
|
394
|
+
if (signal?.aborted === true)
|
|
395
|
+
return undefined;
|
|
396
|
+
this.options.onWarn?.(`Kimi model discovery failed; using the built-in catalog (${errorChain(error)})`);
|
|
397
|
+
return undefined;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shared request/stream dance for providers that speak the OpenAI Responses
|
|
3
|
+
* wire (Kimi Code, xAI/Grok).
|
|
4
|
+
*
|
|
5
|
+
* Everything route-specific stays with the route: building the URL, assembling
|
|
6
|
+
* the body, and its headers. What is shared is the part that is easy to get
|
|
7
|
+
* subtly wrong twice — resolving a usable token, retrying exactly once after a
|
|
8
|
+
* rejected credential, walking a model chain when one model is unavailable,
|
|
9
|
+
* mapping transport and HTTP failures, guarding an idle stream, and translating
|
|
10
|
+
* the SSE body into harness chunks.
|
|
11
|
+
*
|
|
12
|
+
* @module dsh-plugin-subscription-oauth/providers/responses-stream
|
|
13
|
+
*/
|
|
14
|
+
import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm';
|
|
15
|
+
import type { SessionBase } from '../auth/store.js';
|
|
16
|
+
import type { TokenManager } from '../auth/tokens.js';
|
|
17
|
+
/** Constructor inputs for {@link streamProviderResponses}. */
|
|
18
|
+
export interface ResponsesStreamInput<S extends SessionBase> {
|
|
19
|
+
/** Endpoint name used in failure messages, e.g. `Kimi API`. */
|
|
20
|
+
label: string;
|
|
21
|
+
/** Token manager owning refresh. */
|
|
22
|
+
tokens: TokenManager<S>;
|
|
23
|
+
/** Send one request for one model with the given access token. */
|
|
24
|
+
request(options: GenerateOptions, model: string, accessToken: string, signal: AbortSignal): Promise<Response>;
|
|
25
|
+
/** Inactivity budget before the stream is abandoned. */
|
|
26
|
+
streamIdleTimeoutMs: number;
|
|
27
|
+
/**
|
|
28
|
+
* The models to try, most preferred first, resolved lazily.
|
|
29
|
+
*
|
|
30
|
+
* Omitted means "only the requested model". `auto` supplies the user's whole
|
|
31
|
+
* ranked chain here; a concrete id supplies just itself.
|
|
32
|
+
*/
|
|
33
|
+
models?: () => Promise<readonly string[]>;
|
|
34
|
+
/** Called when a call proved the stored credential unusable. */
|
|
35
|
+
onCredentialRejected?: (account: string) => void;
|
|
36
|
+
/** Rewrite a thrown failure before it is mapped, e.g. to name the fix. */
|
|
37
|
+
describeFailure?(error: unknown): unknown;
|
|
38
|
+
/** Called when a model in the chain was skipped, with the reason. */
|
|
39
|
+
onFallback?: (from: string, to: string, reason: string) => void;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Run one model call over the Responses wire, walking a model chain.
|
|
43
|
+
*
|
|
44
|
+
* The chain is what `auto` means to a user: request the most preferred model,
|
|
45
|
+
* and — only while nothing has been streamed yet — fall through to the next one
|
|
46
|
+
* when the provider says that model cannot serve this request. Once any chunk has
|
|
47
|
+
* been produced the turn belongs to that model, so a later failure is reported
|
|
48
|
+
* rather than papered over by a second model's text.
|
|
49
|
+
*
|
|
50
|
+
* A 401/403 triggers exactly one forced token refresh and retry of the same
|
|
51
|
+
* model before the chain advances; if the retry is refused too, the credential is
|
|
52
|
+
* reported as dead so the caller can ask the user to sign in again.
|
|
53
|
+
*
|
|
54
|
+
* @param options - the fully assembled request for this call.
|
|
55
|
+
* @param input - the route's transport hooks and budgets.
|
|
56
|
+
* @yields harness stream chunks in contract order.
|
|
57
|
+
*/
|
|
58
|
+
export declare function streamProviderResponses<S extends SessionBase>(options: GenerateOptions, input: ResponsesStreamInput<S>): AsyncIterable<StreamChunk>;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shared request/stream dance for providers that speak the OpenAI Responses
|
|
3
|
+
* wire (Kimi Code, xAI/Grok).
|
|
4
|
+
*
|
|
5
|
+
* Everything route-specific stays with the route: building the URL, assembling
|
|
6
|
+
* the body, and its headers. What is shared is the part that is easy to get
|
|
7
|
+
* subtly wrong twice — resolving a usable token, retrying exactly once after a
|
|
8
|
+
* rejected credential, walking a model chain when one model is unavailable,
|
|
9
|
+
* mapping transport and HTTP failures, guarding an idle stream, and translating
|
|
10
|
+
* the SSE body into harness chunks.
|
|
11
|
+
*
|
|
12
|
+
* @module dsh-plugin-subscription-oauth/providers/responses-stream
|
|
13
|
+
*/
|
|
14
|
+
import { EMPTY_RESPONSE_CODE, LlmError } from '@deepseek-ai/dsh-llm';
|
|
15
|
+
import { streamResponses } from '../translate/responses.js';
|
|
16
|
+
import { httpLlmError, idleWatchdog, mapFetchFailure } from './common.js';
|
|
17
|
+
/**
|
|
18
|
+
* Whether a refusal says "this model cannot serve you now", as opposed to "this
|
|
19
|
+
* request is wrong".
|
|
20
|
+
*
|
|
21
|
+
* Only the first kind may be answered by another model. A 400 or an unparseable
|
|
22
|
+
* body is a bug in what this plugin sent: silently re-sending it elsewhere would
|
|
23
|
+
* hide the defect and produce an answer from a model the caller did not ask for.
|
|
24
|
+
*/
|
|
25
|
+
function isModelFallbackWorthy(status) {
|
|
26
|
+
// Gone or renamed model, exhausted allowance, rate limit, provider trouble.
|
|
27
|
+
return status === 402 || status === 404 || status === 408 || status === 429 || status >= 500;
|
|
28
|
+
}
|
|
29
|
+
/** One short reason for a log line. */
|
|
30
|
+
function reasonOf(error) {
|
|
31
|
+
if (error instanceof LlmError)
|
|
32
|
+
return `${error.failure.code}: ${error.message}`;
|
|
33
|
+
return error instanceof Error ? error.message : String(error);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Run one model call over the Responses wire, walking a model chain.
|
|
37
|
+
*
|
|
38
|
+
* The chain is what `auto` means to a user: request the most preferred model,
|
|
39
|
+
* and — only while nothing has been streamed yet — fall through to the next one
|
|
40
|
+
* when the provider says that model cannot serve this request. Once any chunk has
|
|
41
|
+
* been produced the turn belongs to that model, so a later failure is reported
|
|
42
|
+
* rather than papered over by a second model's text.
|
|
43
|
+
*
|
|
44
|
+
* A 401/403 triggers exactly one forced token refresh and retry of the same
|
|
45
|
+
* model before the chain advances; if the retry is refused too, the credential is
|
|
46
|
+
* reported as dead so the caller can ask the user to sign in again.
|
|
47
|
+
*
|
|
48
|
+
* @param options - the fully assembled request for this call.
|
|
49
|
+
* @param input - the route's transport hooks and budgets.
|
|
50
|
+
* @yields harness stream chunks in contract order.
|
|
51
|
+
*/
|
|
52
|
+
export async function* streamProviderResponses(options, input) {
|
|
53
|
+
const { label, tokens, request } = input;
|
|
54
|
+
const watchdog = idleWatchdog(options.signal, input.streamIdleTimeoutMs);
|
|
55
|
+
let lastError;
|
|
56
|
+
try {
|
|
57
|
+
// A chain is resolved once per call, so a settings change can never make the
|
|
58
|
+
// fallbacks disagree with the model the caller asked for.
|
|
59
|
+
const models = input.models === undefined ? [options.model] : await input.models();
|
|
60
|
+
if (models.length === 0) {
|
|
61
|
+
throw new LlmError(`${label} has no enabled model to dispatch to`, EMPTY_RESPONSE_CODE);
|
|
62
|
+
}
|
|
63
|
+
let token = await tokens.resolve();
|
|
64
|
+
for (const [index, model] of models.entries()) {
|
|
65
|
+
try {
|
|
66
|
+
let response = await request(options, model, token.accessToken, watchdog.signal);
|
|
67
|
+
if (response.status === 401 || response.status === 403) {
|
|
68
|
+
token = await tokens.resolve(token.account, true);
|
|
69
|
+
response = await request(options, model, token.accessToken, watchdog.signal);
|
|
70
|
+
if (response.status === 401 || response.status === 403) {
|
|
71
|
+
input.onCredentialRejected?.(token.account);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (!response.ok) {
|
|
75
|
+
const error = await httpLlmError(response, label);
|
|
76
|
+
if (models.length > 1 && isModelFallbackWorthy(response.status) && index < models.length - 1) {
|
|
77
|
+
const next = models[index + 1];
|
|
78
|
+
if (next !== undefined) {
|
|
79
|
+
lastError = error;
|
|
80
|
+
input.onFallback?.(model, next, reasonOf(error));
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
if (response.body === null) {
|
|
87
|
+
throw new LlmError(`${label} returned no response body`, EMPTY_RESPONSE_CODE);
|
|
88
|
+
}
|
|
89
|
+
yield* streamResponses(response.body, () => { watchdog.pulse(); });
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
// A transport failure on a chain is also worth another model.
|
|
94
|
+
const transport = error instanceof TypeError || error.name === 'AbortError';
|
|
95
|
+
if (transport && options.signal?.aborted !== true && index < models.length - 1) {
|
|
96
|
+
const next = models[index + 1];
|
|
97
|
+
if (next !== undefined) {
|
|
98
|
+
lastError = error;
|
|
99
|
+
input.onFallback?.(model, next, reasonOf(error));
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
throw input.describeFailure === undefined ? error : input.describeFailure(error);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
throw lastError ?? new LlmError(`${label} served none of the requested models`, EMPTY_RESPONSE_CODE);
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
const described = input.describeFailure === undefined ? error : input.describeFailure(error);
|
|
110
|
+
throw mapFetchFailure(label, described, watchdog, options.signal);
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
watchdog.stop();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan and allowance reads.
|
|
3
|
+
*
|
|
4
|
+
* Both providers state what a subscription still has left on their own
|
|
5
|
+
* endpoints, in their own shapes. This module normalises those shapes into the
|
|
6
|
+
* one {@link UsageRead} the Settings page renders, and nothing here ever throws
|
|
7
|
+
* for a merely unsupported or unavailable endpoint: a provider that states no
|
|
8
|
+
* usage simply yields `undefined`.
|
|
9
|
+
*
|
|
10
|
+
* @module dsh-plugin-subscription-oauth/providers/usage
|
|
11
|
+
*/
|
|
12
|
+
import type { UsageWindow } from '../protocol.js';
|
|
13
|
+
import type { FetchFn } from '../http.js';
|
|
14
|
+
/** Provider-neutral usage read, before the controller stamps it. */
|
|
15
|
+
export interface UsageRead {
|
|
16
|
+
plan?: string;
|
|
17
|
+
renewsAt?: number;
|
|
18
|
+
windows: readonly UsageWindow[];
|
|
19
|
+
/**
|
|
20
|
+
* Why the figures are not the provider's primary accounting.
|
|
21
|
+
*
|
|
22
|
+
* Set when a route had to publish a substitute — Antigravity's per-model quota
|
|
23
|
+
* when its account summary was unreachable — so the page can say so instead of
|
|
24
|
+
* presenting the substitute as the plan's own numbers.
|
|
25
|
+
*/
|
|
26
|
+
usageError?: string;
|
|
27
|
+
}
|
|
28
|
+
/** The `/usages` endpoint on the Kimi Code base. */
|
|
29
|
+
export declare const KIMI_USAGE_PATH = "/usages";
|
|
30
|
+
/** The Grok Build CLI chat proxy's billing endpoint. */
|
|
31
|
+
export declare const GROK_BILLING_URL = "https://cli-chat-proxy.grok.com/v1/billing?format=credits";
|
|
32
|
+
/**
|
|
33
|
+
* Read the Kimi Code plan's rolling windows.
|
|
34
|
+
*
|
|
35
|
+
* `GET <base>/usages` states a five-hour window and a weekly window, either of
|
|
36
|
+
* which may be absent depending on the plan. The payload also carries a
|
|
37
|
+
* whole-subscription `totalQuota`, which is deliberately not published as a third
|
|
38
|
+
* meter: what a Kimi Code plan is metered against is the rolling and weekly
|
|
39
|
+
* windows, and a bare credit total next to them reads as a second, contradictory
|
|
40
|
+
* allowance.
|
|
41
|
+
*
|
|
42
|
+
* @param input - endpoint base, bearer, and an injectable fetch.
|
|
43
|
+
* @returns the windows, or undefined when the endpoint states none.
|
|
44
|
+
*/
|
|
45
|
+
export declare function readKimiUsage(input: {
|
|
46
|
+
apiBase: string;
|
|
47
|
+
accessToken: string;
|
|
48
|
+
fetchFn?: FetchFn;
|
|
49
|
+
}): Promise<UsageRead | undefined>;
|
|
50
|
+
/** Read the subscription tier a Grok access token carries. */
|
|
51
|
+
export declare function grokTierName(accessToken: string): string | undefined;
|
|
52
|
+
/**
|
|
53
|
+
* Read the Grok (X Premium) plan's metered allowance.
|
|
54
|
+
*
|
|
55
|
+
* The CLI's own chat proxy states included-credit usage as a ready-made
|
|
56
|
+
* percentage plus the current period; an older shape states cent-valued totals
|
|
57
|
+
* from which the percentage is derived.
|
|
58
|
+
*
|
|
59
|
+
* @param input - bearer, and an injectable fetch.
|
|
60
|
+
* @returns the window, or undefined when the endpoint states none.
|
|
61
|
+
*/
|
|
62
|
+
export declare function readGrokUsage(input: {
|
|
63
|
+
accessToken: string;
|
|
64
|
+
fetchFn?: FetchFn;
|
|
65
|
+
}): Promise<UsageRead | undefined>;
|