dsh-qoder-connect 0.1.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.en.md +147 -0
- package/README.md +148 -0
- package/assets/1.png +0 -0
- package/assets/2.png +0 -0
- package/assets/3.png +0 -0
- package/assets/4.png +0 -0
- package/assets/5.png +0 -0
- package/assets/6.png +0 -0
- package/cordis.patch.yml +6 -0
- package/lib/bin.d.ts +6 -0
- package/lib/bin.js +291 -0
- package/lib/client.js +3545 -0
- package/lib/index.d.ts +1371 -0
- package/lib/index.js +1961 -0
- package/lib/variants-jbFhMKO8.js +4082 -0
- package/package.json +116 -0
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,1371 @@
|
|
|
1
|
+
import z from "@deepseek-ai/schemastery";
|
|
2
|
+
import "@earendil-works/pi-ai";
|
|
3
|
+
import { GenerateOptions, StreamChunk } from "@deepseek-ai/dsh-llm";
|
|
4
|
+
import { PiAiAdapter } from "@deepseek-ai/dsh-llm-pi-ai";
|
|
5
|
+
import { IncomingMessage, ServerResponse } from "node:http";
|
|
6
|
+
import { Context } from "@deepseek-ai/cordis";
|
|
7
|
+
import { SettingsNamespace } from "@deepseek-ai/dsh-settings";
|
|
8
|
+
import { AttachmentStore } from "@deepseek-ai/dsh-attachment";
|
|
9
|
+
//#region src/paths.d.ts
|
|
10
|
+
/**
|
|
11
|
+
* The plugin's data directory — the ONE place every file the plugin owns
|
|
12
|
+
* lives.
|
|
13
|
+
*
|
|
14
|
+
* Layout: `<profile>/.dsh-qoder-connect/state/` (the profile discovered
|
|
15
|
+
* the same way the credential store always did). Everything — PAT files,
|
|
16
|
+
* saved catalogs, probe records, the host heartbeat, and the machine-id
|
|
17
|
+
* seed — writes there, so a profile directory never collects loose
|
|
18
|
+
* `.qoder-*` files and the whole plugin's footprint is one folder.
|
|
19
|
+
*
|
|
20
|
+
* Fallbacks, in order: `DSH_QODER_DATA_DIR` env override → the discovered
|
|
21
|
+
* profile → the Harness home (a checkout running its own tests, or a host
|
|
22
|
+
* loading the plugin from outside any profile).
|
|
23
|
+
*
|
|
24
|
+
* Split out of `auth.ts` so catalog/probe/heartbeat stores can import the
|
|
25
|
+
* directory without pulling in the credential code (and its upstream
|
|
26
|
+
* dependency) — these modules stay leaf-light on purpose.
|
|
27
|
+
*
|
|
28
|
+
* @module dsh-qoder-connect/paths
|
|
29
|
+
*/
|
|
30
|
+
/** The per-profile directory the plugin's data folder lives under. */
|
|
31
|
+
declare const QODER_DATA_DIR_NAME = ".dsh-qoder-connect";
|
|
32
|
+
/** Environment override for the whole data directory. */
|
|
33
|
+
declare const QODER_DATA_DIR_ENV = "DSH_QODER_DATA_DIR";
|
|
34
|
+
/**
|
|
35
|
+
* The plugin's data directory: `<profile>/.dsh-qoder-connect`.
|
|
36
|
+
*
|
|
37
|
+
* Falls back to the Harness home when no profile can be discovered — a
|
|
38
|
+
* checkout running its own tests, or a host that loads the plugin from
|
|
39
|
+
* outside a profile — so the plugin always has somewhere to write, and
|
|
40
|
+
* `DSH_QODER_DATA_DIR` overrides either way.
|
|
41
|
+
*/
|
|
42
|
+
declare function qoderPluginDataDir(): string;
|
|
43
|
+
//#endregion
|
|
44
|
+
//#region src/qoder/region.d.ts
|
|
45
|
+
/** Qoder upstream deployment selected for one immutable transport instance. */
|
|
46
|
+
type QoderRegion = 'global' | 'china';
|
|
47
|
+
//#endregion
|
|
48
|
+
//#region src/status-paths.d.ts
|
|
49
|
+
/** The two Qoder product variants this plugin serves, by provider id. */
|
|
50
|
+
type QoderVariantId = 'qoder' | 'qoder-global';
|
|
51
|
+
/** Plugin-owned status endpoint consumed by its browser half. */
|
|
52
|
+
declare const QODER_STATUS_PATH = "/plugins/dsh-qoder-connect/status";
|
|
53
|
+
/**
|
|
54
|
+
* Plugin-owned probe control endpoint.
|
|
55
|
+
*
|
|
56
|
+
* Separate from the status route because it accepts writes: the status route's
|
|
57
|
+
* loopback Host/Origin guard protects against a DNS-rebinding *page*, which is
|
|
58
|
+
* not the same as authorizing a state-changing action. This route therefore
|
|
59
|
+
* also requires the in-process key the browser half receives with the status
|
|
60
|
+
* document.
|
|
61
|
+
*/
|
|
62
|
+
declare const QODER_PROBE_PATH = "/plugins/dsh-qoder-connect/probe";
|
|
63
|
+
/**
|
|
64
|
+
* Plugin-owned PAT endpoint, one per variant.
|
|
65
|
+
*
|
|
66
|
+
* A POST here saves (validates and persists) a Personal Access Token, or
|
|
67
|
+
* clears the stored one. Unlike the read-only status GET it is a write —
|
|
68
|
+
* it persists a credential — so it also requires the in-process key the
|
|
69
|
+
* browser half receives with the status document.
|
|
70
|
+
*/
|
|
71
|
+
declare const QODER_AUTH_PATH = "/plugins/dsh-qoder-connect/auth";
|
|
72
|
+
/**
|
|
73
|
+
* The international (Qoder Global) variant's own triple of routes.
|
|
74
|
+
*
|
|
75
|
+
* Kept as separate constants rather than a computed suffix so both halves
|
|
76
|
+
* reference literal strings: the browser bundle and the host bundle are built
|
|
77
|
+
* independently, and a shared expression is one build-config drift away from
|
|
78
|
+
* the desk asking a route the host never mounted.
|
|
79
|
+
*/
|
|
80
|
+
declare const QODER_GLOBAL_STATUS_PATH = "/plugins/dsh-qoder-connect/global/status";
|
|
81
|
+
declare const QODER_GLOBAL_PROBE_PATH = "/plugins/dsh-qoder-connect/global/probe";
|
|
82
|
+
declare const QODER_GLOBAL_AUTH_PATH = "/plugins/dsh-qoder-connect/global/auth";
|
|
83
|
+
/**
|
|
84
|
+
* One action the PAT route accepts.
|
|
85
|
+
*
|
|
86
|
+
* `save-pat` validates the pasted token against the variant's region and, only
|
|
87
|
+
* on success, persists it; `clear` removes the stored credential. There is no
|
|
88
|
+
* device flow and no credential import: the token is the whole auth surface.
|
|
89
|
+
*/
|
|
90
|
+
type QoderAuthAction = 'save-pat' | 'clear';
|
|
91
|
+
/** Request body accepted by the PAT route. */
|
|
92
|
+
interface QoderAuthRequest {
|
|
93
|
+
action: QoderAuthAction;
|
|
94
|
+
/** The token to validate and store; required for `save-pat`. */
|
|
95
|
+
pat?: string;
|
|
96
|
+
}
|
|
97
|
+
/** Where the credential in effect came from. */
|
|
98
|
+
type QoderCredentialSource = 'card' | 'env' | 'cli';
|
|
99
|
+
/**
|
|
100
|
+
* Redacted summary of one variant's PAT.
|
|
101
|
+
*
|
|
102
|
+
* Never the token itself: the browser half displays where the credential came
|
|
103
|
+
* from, when it was saved, and the last four characters — enough to tell two
|
|
104
|
+
* tokens apart, not enough to misuse one.
|
|
105
|
+
*/
|
|
106
|
+
interface QoderPatSummary {
|
|
107
|
+
source: QoderCredentialSource;
|
|
108
|
+
/** When the stored token was saved, epoch milliseconds; absent for env fallback. */
|
|
109
|
+
savedAtMs?: number;
|
|
110
|
+
/** Last four characters of the token. */
|
|
111
|
+
patTail?: string;
|
|
112
|
+
}
|
|
113
|
+
/** One model's recorded probe observation, as the card displays it. */
|
|
114
|
+
interface QoderWebProbeModel {
|
|
115
|
+
id: string;
|
|
116
|
+
name: string;
|
|
117
|
+
/** `validating` results carry efforts; the other states never do. */
|
|
118
|
+
validation: 'validating' | 'non-validating' | 'unknown';
|
|
119
|
+
efforts: readonly string[];
|
|
120
|
+
probedAt: number;
|
|
121
|
+
}
|
|
122
|
+
/** Probe section of the status document. */
|
|
123
|
+
interface QoderWebProbeSection {
|
|
124
|
+
/** Whether the user has authorized probing. */
|
|
125
|
+
consent: boolean;
|
|
126
|
+
/** Whether a sweep is in flight right now. */
|
|
127
|
+
running: boolean;
|
|
128
|
+
/** Models the user could probe by hand (undeclared yet reasoning-capable). */
|
|
129
|
+
candidates: readonly string[];
|
|
130
|
+
/** Recorded observations. */
|
|
131
|
+
results: readonly QoderWebProbeModel[];
|
|
132
|
+
}
|
|
133
|
+
/** Action requested from the probe control route. */
|
|
134
|
+
interface QoderProbeAction {
|
|
135
|
+
/**
|
|
136
|
+
* `probe` spends credit on one model; `clear` drops recorded observations;
|
|
137
|
+
* `refresh` re-reads the credential and re-fetches the model catalog;
|
|
138
|
+
* `set-maximum-context-window` persists the card's maximum-window
|
|
139
|
+
* preference (one per variant section).
|
|
140
|
+
*
|
|
141
|
+
* All are writes, which is why they share this route's in-process key
|
|
142
|
+
* and loopback guards rather than the read-only status GET.
|
|
143
|
+
*/
|
|
144
|
+
action: 'probe' | 'clear' | 'refresh' | 'set-maximum-context-window';
|
|
145
|
+
/** Target model id; required for `probe`. */
|
|
146
|
+
model?: string;
|
|
147
|
+
/** Requested value for `set-maximum-context-window`. */
|
|
148
|
+
enabled?: boolean;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Where the models a card is currently showing came from.
|
|
152
|
+
*
|
|
153
|
+
* The plan requires the card to distinguish a live catalog from the built-in
|
|
154
|
+
* fallback, and to say when the last attempt failed — otherwise a stale list is
|
|
155
|
+
* indistinguishable from an offline one, and a user cannot tell whether the
|
|
156
|
+
* models they see still match the upstream.
|
|
157
|
+
*/
|
|
158
|
+
interface QoderWebCatalog {
|
|
159
|
+
/**
|
|
160
|
+
* Where the models on screen came from, in degradation order:
|
|
161
|
+
* `live` (fetched now) → `saved` (this account's last successful fetch,
|
|
162
|
+
* restored after a restart or a failed fetch) → `fallback` (the roster
|
|
163
|
+
* compiled into the plugin). The card distinguishes them because "stale" and
|
|
164
|
+
* "offline with a saved list" are different situations for the user.
|
|
165
|
+
*/
|
|
166
|
+
source: 'live' | 'saved' | 'fallback';
|
|
167
|
+
/** When the live catalog last succeeded, epoch ms. */
|
|
168
|
+
fetchedAt?: number;
|
|
169
|
+
/** Why the most recent fetch failed, when it did, redacted for display. */
|
|
170
|
+
error?: string;
|
|
171
|
+
}
|
|
172
|
+
/** One quota package and its remaining credit. */
|
|
173
|
+
interface QoderWebCreditAccount {
|
|
174
|
+
packageName: string;
|
|
175
|
+
remain: number;
|
|
176
|
+
size: number;
|
|
177
|
+
unlimited?: true;
|
|
178
|
+
/**
|
|
179
|
+
* When the package's credit expires, carried through in whatever form the
|
|
180
|
+
* upstream reported it (an ISO string or a numeric epoch-millisecond value
|
|
181
|
+
* normalized to an ISO string). Absent when the upstream reported no
|
|
182
|
+
* expiry — rendered as "no expiry", never guessed into a date.
|
|
183
|
+
*/
|
|
184
|
+
packageEndTime?: string;
|
|
185
|
+
}
|
|
186
|
+
/** Aggregated credit answer rendered by the plugin card. */
|
|
187
|
+
interface QoderWebCredits {
|
|
188
|
+
/**
|
|
189
|
+
* Usage percentage across the account (0..100), taken from the Qoder quota
|
|
190
|
+
* answer's `userQuota.percentage`. The card renders its headline from this.
|
|
191
|
+
*/
|
|
192
|
+
total: number;
|
|
193
|
+
/** Summed per-package totals — the denominator of the dashboard's bar. */
|
|
194
|
+
totalSize?: number;
|
|
195
|
+
accounts: readonly QoderWebCreditAccount[];
|
|
196
|
+
/** The account's quota is uncapped; renderers test this flag first. */
|
|
197
|
+
unlimited?: true;
|
|
198
|
+
/** When the current quota cycle ends, verbatim from the upstream. */
|
|
199
|
+
cycleResetTime?: string;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* One catalog model as the status document snapshots it.
|
|
203
|
+
*
|
|
204
|
+
* A plain-Qoder mirror of `src/qoder/catalog.ts`'s `QoderCatalogModel` (the
|
|
205
|
+
* WorkBuddy-era billing/promotion/free fields are gone): the browser half
|
|
206
|
+
* renders reasoning, price factor, and context capacity from these fields and
|
|
207
|
+
* nothing else. Field names follow the Qoder catalog vocabulary so the two
|
|
208
|
+
* halves cannot drift on spelling.
|
|
209
|
+
*/
|
|
210
|
+
interface QoderCatalogModelSnapshot {
|
|
211
|
+
id: string;
|
|
212
|
+
name: string;
|
|
213
|
+
/** Effective input budget in tokens. */
|
|
214
|
+
contextWindow?: number;
|
|
215
|
+
/** The upstream's preferred window before a larger one is selected. */
|
|
216
|
+
defaultContextWindow?: number;
|
|
217
|
+
/** Selectable window sizes the upstream declares, ascending. */
|
|
218
|
+
supportedContextWindows?: readonly number[];
|
|
219
|
+
/** Whether the model reasons at all. */
|
|
220
|
+
isReasoning?: boolean;
|
|
221
|
+
/** Effort ids the model advertises (wire spellings). */
|
|
222
|
+
reasoningEfforts?: readonly string[];
|
|
223
|
+
/** The model's default effort id, when the upstream names one. */
|
|
224
|
+
defaultReasoningEffort?: string;
|
|
225
|
+
/** Price multiplier vs the baseline (1 = base rate); absent means unknown. */
|
|
226
|
+
priceFactor?: number;
|
|
227
|
+
/** Whether the model accepts image input. */
|
|
228
|
+
supportsImages?: boolean;
|
|
229
|
+
/** Upstream provenance marker (`system` / `user`), when reported. */
|
|
230
|
+
source?: string;
|
|
231
|
+
}
|
|
232
|
+
/** The JSON document the plugin card renders. */
|
|
233
|
+
type QoderWebStatus = {
|
|
234
|
+
status: 'signed-in';
|
|
235
|
+
/** Which upstream region the stored token belongs to. */
|
|
236
|
+
region?: QoderRegion;
|
|
237
|
+
/** Redacted summary of the PAT in effect. */
|
|
238
|
+
pat?: QoderPatSummary;
|
|
239
|
+
credits?: QoderWebCredits;
|
|
240
|
+
creditsError?: string;
|
|
241
|
+
/** The models the plugin serves, as catalog snapshots. */
|
|
242
|
+
models?: readonly QoderCatalogModelSnapshot[];
|
|
243
|
+
/** Where those models came from, and whether the last fetch failed. */
|
|
244
|
+
catalog?: QoderWebCatalog;
|
|
245
|
+
/** Reasoning-effort probe state, consent, and recorded observations. */
|
|
246
|
+
probe?: QoderWebProbeSection;
|
|
247
|
+
/** Card preference selecting larger declared context windows. */
|
|
248
|
+
useMaximumContextWindow?: boolean;
|
|
249
|
+
/**
|
|
250
|
+
* In-process key authorizing probe control writes. Handed to the card with
|
|
251
|
+
* the status document (the card is same-origin and already had to pass the
|
|
252
|
+
* loopback guard); it is never persisted and rotates per process.
|
|
253
|
+
*/
|
|
254
|
+
probeKey?: string;
|
|
255
|
+
/**
|
|
256
|
+
* In-process key authorizing PAT writes, including clearing. Travels with
|
|
257
|
+
* the document for the same reason `probeKey` does.
|
|
258
|
+
*/
|
|
259
|
+
authKey?: string;
|
|
260
|
+
} | {
|
|
261
|
+
/**
|
|
262
|
+
* No usable token, and the card may save one.
|
|
263
|
+
*
|
|
264
|
+
* Its own arm rather than an optional field on the signed-in document,
|
|
265
|
+
* because the PAT key below is exactly what a signed-out card needs and a
|
|
266
|
+
* signed-in one does not: the two states ask for different actions, and a
|
|
267
|
+
* single arm would let a card offer clear and save at once.
|
|
268
|
+
*/
|
|
269
|
+
status: 'signed-out';
|
|
270
|
+
/**
|
|
271
|
+
* Why no credential is usable, when that is diagnosable rather than simply
|
|
272
|
+
* "nobody signed in" — a stale WorkBuddy-era credential file being the
|
|
273
|
+
* case that matters. The card renders it in place of the generic hint.
|
|
274
|
+
*/
|
|
275
|
+
reason?: string;
|
|
276
|
+
/**
|
|
277
|
+
* In-process key authorizing PAT writes; see the signed-in arm's
|
|
278
|
+
* `probeKey` for why it travels with the document.
|
|
279
|
+
*/
|
|
280
|
+
authKey?: string;
|
|
281
|
+
} | {
|
|
282
|
+
status: 'error';
|
|
283
|
+
message: string;
|
|
284
|
+
};
|
|
285
|
+
//#endregion
|
|
286
|
+
//#region src/variants.d.ts
|
|
287
|
+
/** One Qoder product variant. */
|
|
288
|
+
interface QoderVariant {
|
|
289
|
+
/** Provider id registered with DSH, e.g. `qoder-global`. */
|
|
290
|
+
id: QoderVariantId;
|
|
291
|
+
/** Model-group heading and card title stem, e.g. `Qoder Global`. */
|
|
292
|
+
displayName: string;
|
|
293
|
+
/** Product name as users know it, for diagnostics and error copy. */
|
|
294
|
+
appName: string;
|
|
295
|
+
/** Which upstream region this variant's transport talks to. */
|
|
296
|
+
region: QoderRegion;
|
|
297
|
+
/** Basename of the plugin-owned credential file under the data dir. */
|
|
298
|
+
ownFilename: string;
|
|
299
|
+
/** Basename of the plugin-owned probe-record file under the data dir. */
|
|
300
|
+
probeFilename: string;
|
|
301
|
+
/**
|
|
302
|
+
* Basename of the plugin-owned saved-catalog file under the data dir.
|
|
303
|
+
*
|
|
304
|
+
* One per variant, like the probe records: the two deployments disagree about
|
|
305
|
+
* rates, windows, and even which models exist for a shared id, so a catalog
|
|
306
|
+
* saved from one must never be served as the other's.
|
|
307
|
+
*/
|
|
308
|
+
catalogFilename: string;
|
|
309
|
+
/** Same-origin status route consumed by this variant's card. */
|
|
310
|
+
statusPath: string;
|
|
311
|
+
/** Same-origin probe-control route consumed by this variant's card. */
|
|
312
|
+
probePath: string;
|
|
313
|
+
/** Same-origin PAT route consumed by this variant's card. */
|
|
314
|
+
authPath: string;
|
|
315
|
+
}
|
|
316
|
+
/** China Qoder first: the no-suffix arm keeps the plugin's primary routes. */
|
|
317
|
+
declare const QODER_VARIANTS: readonly QoderVariant[];
|
|
318
|
+
/** The China variant; the plugin's default and compatibility anchor. */
|
|
319
|
+
declare const CHINA_VARIANT: QoderVariant;
|
|
320
|
+
/** The international variant. */
|
|
321
|
+
declare const GLOBAL_VARIANT: QoderVariant;
|
|
322
|
+
/** Look up a variant by provider id. */
|
|
323
|
+
declare function variantFor(id: string): QoderVariant | undefined;
|
|
324
|
+
//#endregion
|
|
325
|
+
//#region src/auth.d.ts
|
|
326
|
+
/** Environment fallback for the global arm when no file exists. */
|
|
327
|
+
declare const QODER_PAT_ENV_GLOBAL = "QODER_PERSONAL_ACCESS_TOKEN";
|
|
328
|
+
/** Environment fallback for the China arm when no file exists. */
|
|
329
|
+
declare const QODER_PAT_ENV_CN = "QODER_CN_PERSONAL_ACCESS_TOKEN";
|
|
330
|
+
/** Basename of the China variant's credential file inside the data directory. */
|
|
331
|
+
declare const QODER_AUTH_FILENAME = ".qoder-auth.json";
|
|
332
|
+
/** Minimal logger surface the plugin context already provides. */
|
|
333
|
+
interface AuthLogger {
|
|
334
|
+
warn?(...args: unknown[]): void;
|
|
335
|
+
}
|
|
336
|
+
/** Runtime credential, timestamps in epoch milliseconds. */
|
|
337
|
+
interface QoderCredential {
|
|
338
|
+
pat: string;
|
|
339
|
+
region: QoderRegion;
|
|
340
|
+
/** When the stored token was saved; absent for environment-sourced tokens. */
|
|
341
|
+
savedAtMs?: number;
|
|
342
|
+
source: QoderCredentialSource;
|
|
343
|
+
}
|
|
344
|
+
/** Read-only sign-in summary for status, the PAT route, and doctor output. */
|
|
345
|
+
interface QoderAuthStatus {
|
|
346
|
+
state: 'configured' | 'missing';
|
|
347
|
+
/** Redacted summary of the token in effect, when there is one. */
|
|
348
|
+
pat?: QoderPatSummary;
|
|
349
|
+
/** Which upstream region the token is used against. */
|
|
350
|
+
region?: QoderRegion;
|
|
351
|
+
/** Why no credential is usable, when diagnosable (a stale legacy file). */
|
|
352
|
+
reason?: string;
|
|
353
|
+
/** Absolute path of the credential file this store owns. */
|
|
354
|
+
filePath: string;
|
|
355
|
+
}
|
|
356
|
+
/** Constructor options. */
|
|
357
|
+
interface QoderStoreOptions {
|
|
358
|
+
variant: QoderVariant;
|
|
359
|
+
/** Explicit plugin-owned credential path, defaulting under the data dir. */
|
|
360
|
+
ownPath?: string;
|
|
361
|
+
/** Logger for the legacy-file warning; optional so tests stay quiet. */
|
|
362
|
+
logger?: AuthLogger;
|
|
363
|
+
}
|
|
364
|
+
/** Last four characters of a token, for display. */
|
|
365
|
+
declare function patTail(pat: string): string | undefined;
|
|
366
|
+
/**
|
|
367
|
+
* Stable identity key for a credential, used by probe records and saved
|
|
368
|
+
* catalogs.
|
|
369
|
+
*
|
|
370
|
+
* A PAT is a bearer secret, so the key is its one-way hash rather than any
|
|
371
|
+
* suffix of it: surfaces that show the key (cards, JSON diagnostics) leak
|
|
372
|
+
* nothing usable, while two reads of the same token still correlate locally.
|
|
373
|
+
*/
|
|
374
|
+
declare function qoderCredentialIdentity(credential: Pick<QoderCredential, 'pat'>): string;
|
|
375
|
+
declare class QoderCredentialStore {
|
|
376
|
+
private readonly variant;
|
|
377
|
+
private readonly ownPath;
|
|
378
|
+
private readonly logger;
|
|
379
|
+
/** The legacy-file warning is emitted once per process, not per read. */
|
|
380
|
+
private legacyWarned;
|
|
381
|
+
constructor(options: QoderStoreOptions);
|
|
382
|
+
/** Absolute path of this variant's credential file, for display and tests. */
|
|
383
|
+
ownAuthPath(): string;
|
|
384
|
+
/**
|
|
385
|
+
* The credential currently in effect, or `undefined` when there is none.
|
|
386
|
+
*
|
|
387
|
+
* Resolution order: the plugin-owned file, then the arm's environment
|
|
388
|
+
* variable (only when no file exists — a saved token always wins over a
|
|
389
|
+
* stray env), then nothing. There is no refresh and no expiry check: a PAT
|
|
390
|
+
* lives until revoked.
|
|
391
|
+
*/
|
|
392
|
+
current(): Promise<QoderCredential | undefined>;
|
|
393
|
+
/**
|
|
394
|
+
* The credential, or a thrown `MISSING_CREDENTIAL` error.
|
|
395
|
+
*
|
|
396
|
+
* Callers that serve requests (the shim, the catalog sweep) use this so the
|
|
397
|
+
* absence of a token surfaces as the same classified failure the transport
|
|
398
|
+
* itself raises when a saved token turns out empty.
|
|
399
|
+
*/
|
|
400
|
+
resolve(): Promise<QoderCredential>;
|
|
401
|
+
/** The token in effect as a bare string, rejecting when there is none. */
|
|
402
|
+
patPromise(): Promise<string>;
|
|
403
|
+
/**
|
|
404
|
+
* Persist a token for this variant. The caller is responsible for having
|
|
405
|
+
* validated it against the region first (`validateApiKey` upstream).
|
|
406
|
+
*/
|
|
407
|
+
save(pat: string, source?: QoderCredentialSource): Promise<QoderCredential>;
|
|
408
|
+
/** Remove the stored credential. A missing file is not an error. */
|
|
409
|
+
clear(): Promise<void>;
|
|
410
|
+
/** Alias kept for call-site readability (`logout` is just clearing). */
|
|
411
|
+
logout(): Promise<void>;
|
|
412
|
+
/** Redacted summary for the card, the PAT route, and doctor output. */
|
|
413
|
+
status(): Promise<QoderAuthStatus>;
|
|
414
|
+
/** Set when a read found a stale WorkBuddy-era file; consumed once. */
|
|
415
|
+
private pendingLegacyReason;
|
|
416
|
+
private fromEnvironment;
|
|
417
|
+
private readOwn;
|
|
418
|
+
private writeOwn;
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* The plugin-owned credential path for one variant's default filename.
|
|
422
|
+
* Kept as a free function for the CLI, which builds no store per display line.
|
|
423
|
+
*/
|
|
424
|
+
declare function qoderOwnAuthPath(variant: QoderVariant): string;
|
|
425
|
+
//#endregion
|
|
426
|
+
//#region src/catalog.d.ts
|
|
427
|
+
/**
|
|
428
|
+
* Qoder model catalog: a static fallback roster captured from the transport's
|
|
429
|
+
* built-in defaults, replaced by the upstream's dynamic answer once it loads.
|
|
430
|
+
*
|
|
431
|
+
* The row types moved here from the WorkBuddy-era upstream module: they are
|
|
432
|
+
* the plugin's own model vocabulary (what the catalog stores, the adapter
|
|
433
|
+
* exposes, and the card renders), while `QoderCatalogModel` in
|
|
434
|
+
* `src/qoder/catalog.ts` is the upstream's. `upstream.ts` translates one into
|
|
435
|
+
* the other.
|
|
436
|
+
*
|
|
437
|
+
* @module dsh-qoder-connect/catalog
|
|
438
|
+
*/
|
|
439
|
+
/** One model entry the adapter exposes. */
|
|
440
|
+
interface QoderModelInfo {
|
|
441
|
+
id: string;
|
|
442
|
+
name: string;
|
|
443
|
+
/** Effective input budget in tokens. */
|
|
444
|
+
contextWindow: number;
|
|
445
|
+
/** The upstream's preferred window before a larger one is selected. */
|
|
446
|
+
defaultContextWindow?: number;
|
|
447
|
+
/** Selectable window sizes the upstream declares, ascending. */
|
|
448
|
+
supportedContextWindows?: readonly number[];
|
|
449
|
+
/** Per-request output cap. */
|
|
450
|
+
maxTokens: number;
|
|
451
|
+
/** Whether the model accepts image input. */
|
|
452
|
+
supportsImages: boolean;
|
|
453
|
+
/** Reasoning metadata; absent means the upstream declared none. */
|
|
454
|
+
reasoning?: QoderModelReasoning;
|
|
455
|
+
billing: QoderModelBilling;
|
|
456
|
+
/** Upstream provenance marker (`system` / `user`), when reported. */
|
|
457
|
+
source?: string;
|
|
458
|
+
}
|
|
459
|
+
/** What the upstream declares about one model's reasoning. */
|
|
460
|
+
interface QoderModelReasoning {
|
|
461
|
+
supports: boolean;
|
|
462
|
+
/** Effort ids the model advertises, in canonical order. */
|
|
463
|
+
supportedEfforts?: readonly string[];
|
|
464
|
+
/** The model's default effort id, when the upstream names one. */
|
|
465
|
+
defaultEffort?: string;
|
|
466
|
+
/** Whether thinking can be switched off; Qoder's catalog never says so. */
|
|
467
|
+
canDisableThinking: boolean;
|
|
468
|
+
}
|
|
469
|
+
/** The price the catalog reports for one model. */
|
|
470
|
+
interface QoderModelBilling {
|
|
471
|
+
/** Rate label, spelled `x<n>` (a multiplier on the base rate). */
|
|
472
|
+
credits?: string;
|
|
473
|
+
/** Whether the model is free; the `x0` marker and nothing else sets this. */
|
|
474
|
+
free: boolean;
|
|
475
|
+
/** The upstream disclosed no rate; the card shows "rate unknown". */
|
|
476
|
+
rateUnknown?: boolean;
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* The built-in roster, transcribed from the transport's own `defaultModels`
|
|
480
|
+
* (`src/qoder/catalog.ts`) — the Qoder model keys both regions start from
|
|
481
|
+
* before the first successful discovery: `cmodel`, `auto`, `ultimate`,
|
|
482
|
+
* `performance`, `efficient`, `lite`.
|
|
483
|
+
*
|
|
484
|
+
* It exists so the provider registers with a usable catalog while the first
|
|
485
|
+
* fetch is in flight or the endpoint is unreachable, and it is deliberately
|
|
486
|
+
* *not* a promise about the upstream's current state: the discovery answer
|
|
487
|
+
* replaces it at startup. No per-region roster is invented here — the
|
|
488
|
+
* transport ships one shared default set, and this mirror keeps the same
|
|
489
|
+
* discipline. Reasoning and rate metadata are absent because the defaults
|
|
490
|
+
* declare none; those rows offer no thinking control until the upstream says
|
|
491
|
+
* otherwise.
|
|
492
|
+
*/
|
|
493
|
+
declare const FALLBACK_QODER_MODELS: readonly QoderModelInfo[];
|
|
494
|
+
/**
|
|
495
|
+
* Mutable catalog shared by the shim's `/v1/models` and the adapter.
|
|
496
|
+
*
|
|
497
|
+
* Visibility is separate from content. A variant with no usable credential
|
|
498
|
+
* must expose *no* models rather than a fallback roster: the DSH model picker
|
|
499
|
+
* drops an empty group, so an empty catalog is exactly how a provider hides
|
|
500
|
+
* without touching registration. Serving the fallback to a signed-out user
|
|
501
|
+
* instead offers models that can only fail (the transport throws
|
|
502
|
+
* `MISSING_CREDENTIAL` on the first message), which is worse than showing
|
|
503
|
+
* nothing.
|
|
504
|
+
*
|
|
505
|
+
* The flag defaults to visible so a directly-constructed catalog behaves as
|
|
506
|
+
* it always has; the plugin runtime applies the credential gate.
|
|
507
|
+
*/
|
|
508
|
+
declare class QoderCatalog {
|
|
509
|
+
private models;
|
|
510
|
+
private visible;
|
|
511
|
+
private useMaximumContextWindow;
|
|
512
|
+
/** Per-model window overrides (model id → tokens); an override wins over the preference. */
|
|
513
|
+
private modelContextWindows;
|
|
514
|
+
constructor(initial?: readonly QoderModelInfo[]);
|
|
515
|
+
/** Current entries; empty while the variant has no usable credential. */
|
|
516
|
+
current(): readonly QoderModelInfo[];
|
|
517
|
+
/** Replace the list; callers invalidate their adapter snapshot after this. */
|
|
518
|
+
set(models: readonly QoderModelInfo[]): void;
|
|
519
|
+
/** Whether this variant's models are exposed at all. */
|
|
520
|
+
isVisible(): boolean;
|
|
521
|
+
/**
|
|
522
|
+
* Show or hide the whole catalog. Returns whether the value changed, so the
|
|
523
|
+
* caller can skip an invalidation that would re-render an identical list.
|
|
524
|
+
*/
|
|
525
|
+
setVisible(visible: boolean): boolean;
|
|
526
|
+
/** Select the largest declared window where the upstream offers one. */
|
|
527
|
+
setUseMaximumContextWindow(useMaximum: boolean): boolean;
|
|
528
|
+
/**
|
|
529
|
+
* Replace the per-model window overrides wholesale. Returns whether the map
|
|
530
|
+
* changed, so the caller can skip an invalidation over an identical write.
|
|
531
|
+
*/
|
|
532
|
+
setModelContextWindows(modelContextWindows: Readonly<Record<string, number>>): boolean;
|
|
533
|
+
/**
|
|
534
|
+
* Models to fall back to when the upstream fetch fails; ignores
|
|
535
|
+
* visibility, because the caller asking for the fallback already knows the
|
|
536
|
+
* credential state.
|
|
537
|
+
*/
|
|
538
|
+
fallback(): readonly QoderModelInfo[];
|
|
539
|
+
}
|
|
540
|
+
//#endregion
|
|
541
|
+
//#region src/probe.d.ts
|
|
542
|
+
/**
|
|
543
|
+
* The canonical values a probe tests, in a fixed order.
|
|
544
|
+
*
|
|
545
|
+
* `minimal` is absent: it appears in no upstream vocabulary. `off` is absent
|
|
546
|
+
* by policy — disabling thinking is a separate capability the upstream must
|
|
547
|
+
* declare through `canDisableThinking`, never something probing may infer.
|
|
548
|
+
*/
|
|
549
|
+
declare const PROBE_EFFORT_CANDIDATES: readonly QoderEffort[];
|
|
550
|
+
/** Sentinel generator; injectable so tests get deterministic values. */
|
|
551
|
+
type SentinelFactory = () => string;
|
|
552
|
+
/** Default sentinel: unmistakably non-canonical, different on every call. */
|
|
553
|
+
declare function randomSentinel(): string;
|
|
554
|
+
/**
|
|
555
|
+
* One response as the probe sees it, split into the only distinctions the
|
|
556
|
+
* attribution rule needs.
|
|
557
|
+
*/
|
|
558
|
+
interface ProbeAttempt {
|
|
559
|
+
/** HTTP status, or 0 for a transport failure. */
|
|
560
|
+
status: number;
|
|
561
|
+
/** True when a parseable SSE event arrived. */
|
|
562
|
+
streamed: boolean;
|
|
563
|
+
/** `extError.code` from a JSON error body, when present. */
|
|
564
|
+
errorCode?: string;
|
|
565
|
+
/** Free-form detail for logs; never shown as a capability claim. */
|
|
566
|
+
detail?: string;
|
|
567
|
+
}
|
|
568
|
+
/** How one attempt is performed; the caller owns credentials and HTTP. */
|
|
569
|
+
type ProbeSender = (effort: string | undefined, signal: AbortSignal) => Promise<ProbeAttempt>;
|
|
570
|
+
/** The outcome of probing one model. */
|
|
571
|
+
type ProbeOutcome = {
|
|
572
|
+
validation: 'validating';
|
|
573
|
+
efforts: readonly QoderEffort[];
|
|
574
|
+
requests: number;
|
|
575
|
+
} | {
|
|
576
|
+
validation: 'non-validating';
|
|
577
|
+
efforts: readonly [];
|
|
578
|
+
requests: number;
|
|
579
|
+
} | {
|
|
580
|
+
validation: 'unknown';
|
|
581
|
+
efforts: readonly [];
|
|
582
|
+
requests: number;
|
|
583
|
+
reason: string;
|
|
584
|
+
};
|
|
585
|
+
/**
|
|
586
|
+
* Probe one model.
|
|
587
|
+
*
|
|
588
|
+
* `options.candidates` exists so tests can shorten the sweep; production always
|
|
589
|
+
* uses {@link PROBE_EFFORT_CANDIDATES}.
|
|
590
|
+
*/
|
|
591
|
+
declare function probeModel(options: {
|
|
592
|
+
send: ProbeSender;
|
|
593
|
+
sentinel?: SentinelFactory;
|
|
594
|
+
candidates?: readonly QoderEffort[];
|
|
595
|
+
timeoutMs?: number;
|
|
596
|
+
}): Promise<ProbeOutcome>;
|
|
597
|
+
//#endregion
|
|
598
|
+
//#region src/qoder/catalog.d.ts
|
|
599
|
+
/** Browser-safe Qoder model catalog types and built-in fallback entries. */
|
|
600
|
+
interface QoderCatalogModel {
|
|
601
|
+
id: string;
|
|
602
|
+
name: string;
|
|
603
|
+
description?: string;
|
|
604
|
+
/** Effective DSH input budget, which may be smaller than the provider default tier. */
|
|
605
|
+
contextWindow?: number;
|
|
606
|
+
/** Largest known capacity, independent of the effective input budget. */
|
|
607
|
+
maxContextWindow?: number;
|
|
608
|
+
maxTokens?: number;
|
|
609
|
+
source?: string;
|
|
610
|
+
isReasoning?: boolean;
|
|
611
|
+
supportsEffort?: boolean;
|
|
612
|
+
reasoningEfforts?: Array<{
|
|
613
|
+
id: string;
|
|
614
|
+
name: string;
|
|
615
|
+
description?: string;
|
|
616
|
+
}>;
|
|
617
|
+
defaultReasoningEffort?: string;
|
|
618
|
+
priceFactor?: number;
|
|
619
|
+
contextOptions?: Record<string, {
|
|
620
|
+
tokenCount?: number;
|
|
621
|
+
isDefault?: boolean;
|
|
622
|
+
}>;
|
|
623
|
+
supportsImages?: boolean;
|
|
624
|
+
}
|
|
625
|
+
//#endregion
|
|
626
|
+
//#region src/qoder/account.d.ts
|
|
627
|
+
/** Browser-safe Qoder subscriber account and quota types. */
|
|
628
|
+
interface QoderSubscriberProfile {
|
|
629
|
+
id: string;
|
|
630
|
+
name: string;
|
|
631
|
+
email: string;
|
|
632
|
+
}
|
|
633
|
+
interface QoderQuota {
|
|
634
|
+
total: number;
|
|
635
|
+
used: number;
|
|
636
|
+
remaining: number;
|
|
637
|
+
percentage: number;
|
|
638
|
+
unit: string;
|
|
639
|
+
}
|
|
640
|
+
interface QoderQuotaUsage {
|
|
641
|
+
userQuota?: QoderQuota | undefined;
|
|
642
|
+
orgResourcePackage?: QoderQuota | undefined;
|
|
643
|
+
totalUsagePercentage?: number | undefined;
|
|
644
|
+
isQuotaExceeded?: boolean | undefined;
|
|
645
|
+
expiresAt?: string | undefined;
|
|
646
|
+
raw?: unknown;
|
|
647
|
+
}
|
|
648
|
+
interface QoderSubscriberOrganization {
|
|
649
|
+
orgId: string;
|
|
650
|
+
orgName: string;
|
|
651
|
+
roleName?: string | undefined;
|
|
652
|
+
isSuspended?: boolean;
|
|
653
|
+
canManageSubscriptions?: boolean;
|
|
654
|
+
resourcePackageFeatureEnabled?: boolean;
|
|
655
|
+
}
|
|
656
|
+
interface QoderSubscriberFeatureAllowed {
|
|
657
|
+
quest?: boolean;
|
|
658
|
+
wiki?: boolean;
|
|
659
|
+
codeReview?: boolean;
|
|
660
|
+
}
|
|
661
|
+
interface QoderSubscriberPlan {
|
|
662
|
+
userType: string;
|
|
663
|
+
planTierName: string;
|
|
664
|
+
planTier?: string;
|
|
665
|
+
isPersonalVersion: boolean;
|
|
666
|
+
isHighestTier?: boolean;
|
|
667
|
+
isRenewed?: boolean;
|
|
668
|
+
startDate?: string;
|
|
669
|
+
endDate?: string;
|
|
670
|
+
organization?: QoderSubscriberOrganization;
|
|
671
|
+
featureAllowed?: QoderSubscriberFeatureAllowed;
|
|
672
|
+
raw?: unknown;
|
|
673
|
+
}
|
|
674
|
+
interface QoderSubscriberStatus {
|
|
675
|
+
allowByok: number;
|
|
676
|
+
teamAllowByok?: number;
|
|
677
|
+
isPrivacyPolicyModifiable?: boolean;
|
|
678
|
+
raw?: unknown;
|
|
679
|
+
}
|
|
680
|
+
interface QoderAccountInfo {
|
|
681
|
+
profile: QoderSubscriberProfile;
|
|
682
|
+
usage?: QoderQuotaUsage;
|
|
683
|
+
plan?: QoderSubscriberPlan;
|
|
684
|
+
status?: QoderSubscriberStatus;
|
|
685
|
+
updatedAt: string;
|
|
686
|
+
}
|
|
687
|
+
//#endregion
|
|
688
|
+
//#region src/qoder/transport/index.d.ts
|
|
689
|
+
interface QoderTransport {
|
|
690
|
+
stream(options: GenerateOptions, model?: QoderCatalogModel): AsyncIterable<StreamChunk>;
|
|
691
|
+
discoverModels(signal?: AbortSignal): Promise<readonly QoderCatalogModel[]>;
|
|
692
|
+
readAccount(options?: {
|
|
693
|
+
force?: boolean | undefined;
|
|
694
|
+
signal?: AbortSignal | undefined;
|
|
695
|
+
}): Promise<QoderAccountInfo>;
|
|
696
|
+
}
|
|
697
|
+
//#endregion
|
|
698
|
+
//#region src/upstream.d.ts
|
|
699
|
+
/**
|
|
700
|
+
* The reasoning-effort vocabulary the probe sweeps and the effort map speak.
|
|
701
|
+
* Mirrors the canonical order of `src/qoder/catalog.ts` (`low` … `max`).
|
|
702
|
+
* `minimal` and `off` are deliberately absent: `off` is a separate capability
|
|
703
|
+
* the upstream must declare, never something probing may infer.
|
|
704
|
+
*/
|
|
705
|
+
type QoderEffort = 'low' | 'medium' | 'high' | 'xhigh' | 'max';
|
|
706
|
+
/** How the shim should present an upstream failure to its OpenAI client. */
|
|
707
|
+
type UpstreamErrorKind = 'missing_credential' | 'auth' | 'soft_rate' | 'quota_exceeded' | 'server' | 'client';
|
|
708
|
+
/** HTTP status the shim answers each failure kind with. */
|
|
709
|
+
declare const KIND_STATUS: Readonly<Record<UpstreamErrorKind, number>>;
|
|
710
|
+
/** The chat-completions answer: a streaming `Response`, or a classified failure. */
|
|
711
|
+
type QoderChatResult = {
|
|
712
|
+
ok: true;
|
|
713
|
+
response: Response;
|
|
714
|
+
} | {
|
|
715
|
+
ok: false;
|
|
716
|
+
status: number;
|
|
717
|
+
kind: UpstreamErrorKind;
|
|
718
|
+
message: string;
|
|
719
|
+
};
|
|
720
|
+
/** Provenance of the last successful catalog fetch. */
|
|
721
|
+
interface QoderCatalogFetch {
|
|
722
|
+
fetchedAtMs: number;
|
|
723
|
+
source: string;
|
|
724
|
+
}
|
|
725
|
+
/** One quota package and its remaining credit, as the card renders it. */
|
|
726
|
+
interface QoderCreditAccount {
|
|
727
|
+
packageName: string;
|
|
728
|
+
remain: number;
|
|
729
|
+
size: number;
|
|
730
|
+
unlimited?: true;
|
|
731
|
+
/** Expiry of this package, verbatim from the upstream (ISO string). */
|
|
732
|
+
packageEndTime?: string;
|
|
733
|
+
}
|
|
734
|
+
/** The aggregated credit answer the card renders. */
|
|
735
|
+
interface QoderCredits {
|
|
736
|
+
/** Usage percentage of the personal quota (0..100). */
|
|
737
|
+
total: number;
|
|
738
|
+
/** Summed per-package totals — the denominator of the dashboard's bar. */
|
|
739
|
+
totalSize?: number;
|
|
740
|
+
accounts: readonly QoderCreditAccount[];
|
|
741
|
+
/** The account's quota is uncapped; renderers test this flag first. */
|
|
742
|
+
unlimited?: true;
|
|
743
|
+
/** When the current quota cycle ends, verbatim from the upstream. */
|
|
744
|
+
cycleResetTime?: string;
|
|
745
|
+
}
|
|
746
|
+
/**
|
|
747
|
+
* Normalize a billing rate label for display.
|
|
748
|
+
*
|
|
749
|
+
* Qoder reports a `priceFactor` number; the host spells it `x<n>` (and the
|
|
750
|
+
* WorkBuddy-era saved files sometimes carried a trailing " credits"). The card
|
|
751
|
+
* wants one shape: a trimmed multiplier without the suffix.
|
|
752
|
+
*/
|
|
753
|
+
declare function normalizeCredits(credits: string | undefined): string | undefined;
|
|
754
|
+
/**
|
|
755
|
+
* Classify a raw HTTP upstream failure from status and a body excerpt.
|
|
756
|
+
*
|
|
757
|
+
* Status first, then phrases: a throttling body often also says "quota
|
|
758
|
+
* exceeded", and reading that as an exhausted balance parks a healthy account
|
|
759
|
+
* until the next billing day instead of retrying shortly.
|
|
760
|
+
*/
|
|
761
|
+
declare function classifyUpstreamError(status: number, body: string): UpstreamErrorKind;
|
|
762
|
+
/**
|
|
763
|
+
* Map one structured transport failure onto the shim's error kind.
|
|
764
|
+
*
|
|
765
|
+
* The transport's taxonomy (`src/qoder/errors.ts`) codes an HTTP 402 as
|
|
766
|
+
* `INVALID_REQUEST`, so the status gets the last say for the quota and auth
|
|
767
|
+
* families before the code-based defaults apply.
|
|
768
|
+
*/
|
|
769
|
+
declare function kindFromQoderFailure(failure: {
|
|
770
|
+
code: string;
|
|
771
|
+
status?: number | undefined;
|
|
772
|
+
}): UpstreamErrorKind;
|
|
773
|
+
/** Constructor options for {@link QoderUpstreamClient}. */
|
|
774
|
+
interface QoderUpstreamClientOptions {
|
|
775
|
+
/** Which upstream region this client's transport talks to. */
|
|
776
|
+
region: QoderRegion;
|
|
777
|
+
/** Provider id stamped into translated requests. */
|
|
778
|
+
providerId: string;
|
|
779
|
+
/** Resolves the current Personal Access Token (rejects when none). */
|
|
780
|
+
getPat: () => Promise<string>;
|
|
781
|
+
/** The transport this client drives. */
|
|
782
|
+
transport: QoderTransport;
|
|
783
|
+
/**
|
|
784
|
+
* The attachment service's image commit path. Without it, any request
|
|
785
|
+
* carrying image parts fails as a client error — the Qoder wire only ever
|
|
786
|
+
* transports durable references.
|
|
787
|
+
*/
|
|
788
|
+
attachments?: Pick<AttachmentStore, 'saveImage'> | undefined;
|
|
789
|
+
/**
|
|
790
|
+
* Extra catalog lookup used when a requested model is not one this client
|
|
791
|
+
* fetched itself. The transport validates effort and image support against
|
|
792
|
+
* the model row, so a stream must carry it whenever one is known.
|
|
793
|
+
*/
|
|
794
|
+
catalogProvider?: ((id: string) => QoderCatalogModel | undefined) | undefined;
|
|
795
|
+
}
|
|
796
|
+
declare class QoderUpstreamClient {
|
|
797
|
+
private readonly region;
|
|
798
|
+
private readonly providerId;
|
|
799
|
+
private readonly getPat;
|
|
800
|
+
private readonly transport;
|
|
801
|
+
private readonly attachments;
|
|
802
|
+
private readonly externalCatalog;
|
|
803
|
+
/** The raw discovery rows of the last successful `fetchModels`, by id. */
|
|
804
|
+
private readonly discovered;
|
|
805
|
+
/** Provenance of the last successful catalog fetch, for the status card. */
|
|
806
|
+
lastCatalog: QoderCatalogFetch | undefined;
|
|
807
|
+
constructor(options: QoderUpstreamClientOptions);
|
|
808
|
+
/** The region this client's transport serves. */
|
|
809
|
+
get clientRegion(): QoderRegion;
|
|
810
|
+
/**
|
|
811
|
+
* The catalog row for one model id: this client's own discovery answers
|
|
812
|
+
* first (it is the same data the upstream catalog call produced), and an
|
|
813
|
+
* externally supplied provider — the plugin's fallback roster, say — fills
|
|
814
|
+
* ids the discovery never listed.
|
|
815
|
+
*/
|
|
816
|
+
catalogModelFor(id: string): QoderCatalogModel | undefined;
|
|
817
|
+
/**
|
|
818
|
+
* Discover this variant's models and translate them into plugin rows.
|
|
819
|
+
*
|
|
820
|
+
* The raw rows stay addressable via {@link catalogModelFor}: a chat request
|
|
821
|
+
* for a discovered model must hand the transport its row, because effort
|
|
822
|
+
* support, image capability, and the output cap are all validated against
|
|
823
|
+
* it. The last successful fetch's provenance lands in {@link lastCatalog}
|
|
824
|
+
* for the status card.
|
|
825
|
+
*/
|
|
826
|
+
fetchModels(signal?: AbortSignal): Promise<QoderModelInfo[]>;
|
|
827
|
+
/**
|
|
828
|
+
* The subscriber's quota answer, mapped onto the card's credit shape.
|
|
829
|
+
*
|
|
830
|
+
* Qoder reports one personal quota and an optional organization resource
|
|
831
|
+
* package; each becomes a package row the card already knows how to draw.
|
|
832
|
+
* An unbounded personal allowance (`total === 0`, not exceeded) is the
|
|
833
|
+
* account's "unlimited" state.
|
|
834
|
+
*/
|
|
835
|
+
fetchCredits(signal?: AbortSignal): Promise<QoderCredits>;
|
|
836
|
+
/**
|
|
837
|
+
* Stream one translated chat-completions request.
|
|
838
|
+
*
|
|
839
|
+
* The first transport chunk is pulled before the response is constructed:
|
|
840
|
+
* everything the transport can reject without touching the network — a
|
|
841
|
+
* missing or empty credential, an effort the model does not advertise, an
|
|
842
|
+
* image on a text-only model — must still be able to answer as a real HTTP
|
|
843
|
+
* status, not as a 200 whose first frame is an error. Once streaming has
|
|
844
|
+
* begun, failures degrade to the error-frame-then-`[DONE]` form the
|
|
845
|
+
* {@link ChunkEncoder} documents.
|
|
846
|
+
*/
|
|
847
|
+
chatStream(bodyJson: string, signal?: AbortSignal): Promise<QoderChatResult>;
|
|
848
|
+
/** Drive the SSE body from an already-started chunk iterator. */
|
|
849
|
+
private bodyStream;
|
|
850
|
+
/**
|
|
851
|
+
* One minimal reasoning-effort request, as the probe sweep needs it.
|
|
852
|
+
*
|
|
853
|
+
* The transport validates `reasoningEffort` against the model's advertised
|
|
854
|
+
* efforts **locally** before any network call: a rejection therefore reads
|
|
855
|
+
* as an attributable 400 with code `UNSUPPORTED_REASONING_EFFORT`, and an
|
|
856
|
+
* accepted value costs exactly one streamed request, which this method ends
|
|
857
|
+
* after the first chunk arrives. A probe on this upstream measures the
|
|
858
|
+
* discovery catalog as much as the endpoint behind it — see the module docs
|
|
859
|
+
* in `probe.ts`.
|
|
860
|
+
*/
|
|
861
|
+
probeEffort(model: string, effort: string | undefined, signal: AbortSignal): Promise<ProbeAttempt>;
|
|
862
|
+
}
|
|
863
|
+
/**
|
|
864
|
+
* Translate one Qoder catalog row into the plugin's model-info shape.
|
|
865
|
+
*
|
|
866
|
+
* The Qoder vocabulary carries less than the WorkBuddy one did — no badges,
|
|
867
|
+
* no promotions, no free/credits strings — so the billing section reports the
|
|
868
|
+
* price factor only, spelled `x<n>`, and says so honestly when the upstream
|
|
869
|
+
* reported nothing (`rateUnknown`). Context capacity comes from the effective
|
|
870
|
+
* window, with the declared options kept for the card and the maximum-window
|
|
871
|
+
* preference; a row with no capacity at all takes the transport's own default.
|
|
872
|
+
*/
|
|
873
|
+
declare function modelInfoOf(model: QoderCatalogModel): QoderModelInfo;
|
|
874
|
+
//#endregion
|
|
875
|
+
//#region src/probe-store.d.ts
|
|
876
|
+
/** Basename of the China variant's probe record inside the plugin's state dir. */
|
|
877
|
+
declare const QODER_PROBE_FILENAME = ".qoder-probe.json";
|
|
878
|
+
/**
|
|
879
|
+
* Whether the model's effort parameter is actually validated.
|
|
880
|
+
*
|
|
881
|
+
* - `validating`: the upstream rejected an unknown sentinel value, so a
|
|
882
|
+
* per-level answer is meaningful.
|
|
883
|
+
* - `non-validating`: the upstream accepted the sentinel, so it ignores or
|
|
884
|
+
* loosely coerces the parameter and no per-level answer can be trusted.
|
|
885
|
+
* - `unknown`: baseline or sentinel failed for an unrelated reason (auth,
|
|
886
|
+
* rate limit, transport, ambiguous error body). Not a negative claim.
|
|
887
|
+
*/
|
|
888
|
+
type QoderProbeValidation = 'validating' | 'non-validating' | 'unknown';
|
|
889
|
+
/** One model's recorded observation. */
|
|
890
|
+
interface QoderProbeRecord {
|
|
891
|
+
/** Fingerprint of the catalog row this observation was made against. */
|
|
892
|
+
fingerprint: string;
|
|
893
|
+
validation: QoderProbeValidation;
|
|
894
|
+
/** Efforts verified as accepted; only ever non-empty for `validating`. */
|
|
895
|
+
efforts: readonly QoderEffort[];
|
|
896
|
+
/** When the probe ran, epoch milliseconds. */
|
|
897
|
+
probedAtMs: number;
|
|
898
|
+
/** Plugin version that produced the record. */
|
|
899
|
+
pluginVersion: string;
|
|
900
|
+
/**
|
|
901
|
+
* The account this observation was made under, as a one-way hash of the
|
|
902
|
+
* credential in effect.
|
|
903
|
+
*
|
|
904
|
+
* An effort set is a fact about one account's entitlement as much as about
|
|
905
|
+
* the model: the same model id can accept different levels under a different
|
|
906
|
+
* subscription. Without this a record outlived the account that produced it,
|
|
907
|
+
* so signing out and in as someone else inherited the previous account's
|
|
908
|
+
* detected levels. Records written before this field existed carry no
|
|
909
|
+
* identity and are therefore never reused.
|
|
910
|
+
*/
|
|
911
|
+
account?: string;
|
|
912
|
+
}
|
|
913
|
+
/**
|
|
914
|
+
* Plugin-owned probe record path inside the plugin's state directory.
|
|
915
|
+
*
|
|
916
|
+
* One file per variant. The two regions serve overlapping model ids with
|
|
917
|
+
* different entitlements, and {@link fingerprintModel} covers only
|
|
918
|
+
* `id`/`reasoning`/`supportsImages` — never the provider — so a single shared
|
|
919
|
+
* file would let one variant's observation answer for the other. The paths
|
|
920
|
+
* differ; the format does not.
|
|
921
|
+
*/
|
|
922
|
+
declare function qoderProbePath(filename?: string): string;
|
|
923
|
+
/**
|
|
924
|
+
* Fingerprint the catalog fields a probe depends on.
|
|
925
|
+
*
|
|
926
|
+
* Deliberately excludes display-only fields (`name`, `billing`, `contextWindow`)
|
|
927
|
+
* so a rename or a rate relabel does not throw away a valid observation, and
|
|
928
|
+
* deliberately includes the whole reasoning object so any change to the
|
|
929
|
+
* declared shape re-probes.
|
|
930
|
+
*/
|
|
931
|
+
declare function fingerprintModel(info: QoderModelInfo): string;
|
|
932
|
+
/** Options for {@link QoderProbeStore}. */
|
|
933
|
+
interface QoderProbeStoreOptions {
|
|
934
|
+
/** Explicit state-file path, overriding the `$DSH_HOME` default. */
|
|
935
|
+
path?: string;
|
|
936
|
+
/** Observation lifetime; defaults to 14 days. */
|
|
937
|
+
ttlMs?: number;
|
|
938
|
+
/** Plugin version stamped into new records. */
|
|
939
|
+
pluginVersion: string;
|
|
940
|
+
/** Clock injection for tests. */
|
|
941
|
+
now?: () => number;
|
|
942
|
+
}
|
|
943
|
+
/**
|
|
944
|
+
* The plugin's probe records: read once, written atomically, never trusted
|
|
945
|
+
* across a fingerprint change or past the TTL.
|
|
946
|
+
*/
|
|
947
|
+
declare class QoderProbeStore {
|
|
948
|
+
private readonly path;
|
|
949
|
+
private readonly ttlMs;
|
|
950
|
+
private readonly pluginVersion;
|
|
951
|
+
private readonly now;
|
|
952
|
+
private records;
|
|
953
|
+
constructor(options: QoderProbeStoreOptions | string);
|
|
954
|
+
/** Resolved state-file path, for the CLI and tests. */
|
|
955
|
+
filePath(): string;
|
|
956
|
+
private load;
|
|
957
|
+
/**
|
|
958
|
+
* The usable record for a model, or `undefined` when there is none, it is
|
|
959
|
+
* expired, it was taken against a different catalog row, or it belongs to a
|
|
960
|
+
* different account.
|
|
961
|
+
*
|
|
962
|
+
* @param account - the account in effect, as `uid:enterpriseId`. Records are
|
|
963
|
+
* only returned for the account that produced them.
|
|
964
|
+
*/
|
|
965
|
+
get(modelId: string, fingerprint: string, account: string): QoderProbeRecord | undefined;
|
|
966
|
+
/**
|
|
967
|
+
* Store one observation. Only a decisive answer (`validating` /
|
|
968
|
+
* `non-validating`) replaces an existing decisive record: a transient
|
|
969
|
+
* `unknown` must not erase knowledge the user already paid for.
|
|
970
|
+
*/
|
|
971
|
+
set(modelId: string, record: QoderProbeRecord): void;
|
|
972
|
+
/** Drop every record; used by the card's explicit "clear" action. */
|
|
973
|
+
clear(): void;
|
|
974
|
+
/** Every record currently held, for status display. */
|
|
975
|
+
all(): Readonly<Record<string, QoderProbeRecord>>;
|
|
976
|
+
/** Build a record stamped with this store's clock, version, and account. */
|
|
977
|
+
record(fingerprint: string, validation: QoderProbeValidation, efforts: readonly QoderEffort[], account: string): QoderProbeRecord;
|
|
978
|
+
/**
|
|
979
|
+
* Write through a temporary file and rename, so a crash mid-write cannot
|
|
980
|
+
* leave a half-parsed document that reads as "no records" and silently drops
|
|
981
|
+
* every observation.
|
|
982
|
+
*/
|
|
983
|
+
private persist;
|
|
984
|
+
}
|
|
985
|
+
//#endregion
|
|
986
|
+
//#region src/shim.d.ts
|
|
987
|
+
/** Minimal logger surface the plugin context already provides. */
|
|
988
|
+
interface ShimLogger {
|
|
989
|
+
warn(...args: unknown[]): void;
|
|
990
|
+
error(...args: unknown[]): void;
|
|
991
|
+
}
|
|
992
|
+
/** What the plugin needs from a running shim. */
|
|
993
|
+
interface QoderShim {
|
|
994
|
+
/** Resolves once the listener is up; rejects if listening failed. */
|
|
995
|
+
ready: Promise<void>;
|
|
996
|
+
/** The shim origin, e.g. `http://127.0.0.1:39271`; valid after ready. */
|
|
997
|
+
baseUrl(): string;
|
|
998
|
+
/**
|
|
999
|
+
* The per-process shared secret the plugin's own client must carry as
|
|
1000
|
+
* `Authorization: Bearer <token>`. Lives only in memory; the adapter
|
|
1001
|
+
* resolves this instead of the upstream credential, because the shim
|
|
1002
|
+
* resolves the real credential itself via the store.
|
|
1003
|
+
*/
|
|
1004
|
+
token(): string;
|
|
1005
|
+
/** Stop serving and destroy open connections. */
|
|
1006
|
+
close(): Promise<void>;
|
|
1007
|
+
}
|
|
1008
|
+
/** Constructor dependencies. */
|
|
1009
|
+
interface QoderShimOptions {
|
|
1010
|
+
store: QoderCredentialStore;
|
|
1011
|
+
client: Pick<QoderUpstreamClient, 'chatStream'>;
|
|
1012
|
+
catalog: QoderCatalog;
|
|
1013
|
+
/** Provider id reported as each model's `owned_by`; one per variant. */
|
|
1014
|
+
providerId: string;
|
|
1015
|
+
logger?: ShimLogger;
|
|
1016
|
+
}
|
|
1017
|
+
/**
|
|
1018
|
+
* Start the loopback endpoint. Requests carry any bearer; the loopback bind
|
|
1019
|
+
* is the boundary, and the upstream credential comes from the store alone.
|
|
1020
|
+
*/
|
|
1021
|
+
declare function createQoderShim(options: QoderShimOptions): QoderShim;
|
|
1022
|
+
//#endregion
|
|
1023
|
+
//#region src/adapter.d.ts
|
|
1024
|
+
/** Default provider route owned by this bundle (the China variant). */
|
|
1025
|
+
declare const QODER_PROVIDER = "qoder";
|
|
1026
|
+
/** Provider idle ceiling while one stream read is outstanding. */
|
|
1027
|
+
declare const QODER_STREAM_IDLE_TIMEOUT_MS = 300000;
|
|
1028
|
+
/** Constructor dependencies. */
|
|
1029
|
+
interface QoderAdapterOptions {
|
|
1030
|
+
providerId?: string;
|
|
1031
|
+
displayName?: string;
|
|
1032
|
+
shim: QoderShim;
|
|
1033
|
+
store: QoderCredentialStore;
|
|
1034
|
+
catalog: QoderCatalog;
|
|
1035
|
+
/** Resolve the durable attachment service at request time, when present. */
|
|
1036
|
+
resolveAttachments?: () => AttachmentStore | undefined;
|
|
1037
|
+
/**
|
|
1038
|
+
* Look up a local probe observation for a model. Consulted only for rows the
|
|
1039
|
+
* upstream left undeclared; absent means declared-set-only behavior.
|
|
1040
|
+
*/
|
|
1041
|
+
observe?: (modelId: string) => QoderProbeRecord | undefined;
|
|
1042
|
+
}
|
|
1043
|
+
/** What {@link createQoderAdapter} hands back. */
|
|
1044
|
+
interface QoderAdapter {
|
|
1045
|
+
adapter: PiAiAdapter;
|
|
1046
|
+
/** Rebuild the adapter's provider snapshot; call after a catalog update. */
|
|
1047
|
+
invalidate: () => void;
|
|
1048
|
+
}
|
|
1049
|
+
/**
|
|
1050
|
+
* Assemble the adapter. The provider's `getModels` reads the live catalog,
|
|
1051
|
+
* and every model's `baseUrl` is re-resolved per read so the shim's
|
|
1052
|
+
* ephemeral port applies from the first snapshot after startup.
|
|
1053
|
+
*
|
|
1054
|
+
* The profile is constructed by hand rather than through dsh-llm-pi-ai's
|
|
1055
|
+
* internal `resolveProfiles()`: that helper is not part of the package's
|
|
1056
|
+
* public export surface (root entry, `lib/` deep imports blocked by the
|
|
1057
|
+
* exports map, `src/` not shipped), so hand-assembly is the only supported
|
|
1058
|
+
* path and every newly required field must be adopted here explicitly —
|
|
1059
|
+
* `modelErrors` since 0.1.5-alpha.2 (#12).
|
|
1060
|
+
*/
|
|
1061
|
+
declare function createQoderAdapter(options: QoderAdapterOptions): QoderAdapter;
|
|
1062
|
+
//#endregion
|
|
1063
|
+
//#region src/catalog-store.d.ts
|
|
1064
|
+
/** One saved catalog: the account it belonged to, and the models it listed. */
|
|
1065
|
+
interface SavedCatalog {
|
|
1066
|
+
/** The credential-identity key the catalog was fetched for. */
|
|
1067
|
+
account: string;
|
|
1068
|
+
/** Which answer served, so one region's roster is never used as the other's. */
|
|
1069
|
+
source: string;
|
|
1070
|
+
/** When the fetch succeeded, epoch milliseconds. */
|
|
1071
|
+
fetchedAtMs: number;
|
|
1072
|
+
models: readonly QoderModelInfo[];
|
|
1073
|
+
}
|
|
1074
|
+
/** Plugin-owned saved-catalog path inside the plugin's state directory. */
|
|
1075
|
+
declare function qoderCatalogPath(filename: string): string;
|
|
1076
|
+
/** Options for {@link QoderCatalogStore}. */
|
|
1077
|
+
interface QoderCatalogStoreOptions {
|
|
1078
|
+
/** Explicit state-file path, overriding the `$DSH_HOME` default. */
|
|
1079
|
+
path?: string;
|
|
1080
|
+
}
|
|
1081
|
+
/**
|
|
1082
|
+
* The last successful catalog per account, read once and written atomically.
|
|
1083
|
+
*
|
|
1084
|
+
* Malformed content reads as "nothing saved" rather than throwing: this file
|
|
1085
|
+
* is an optimization for the offline and first-seconds cases, and a corrupt one
|
|
1086
|
+
* must never be able to stop the plugin from serving models.
|
|
1087
|
+
*/
|
|
1088
|
+
declare class QoderCatalogStore {
|
|
1089
|
+
private readonly path;
|
|
1090
|
+
private entries;
|
|
1091
|
+
constructor(options?: QoderCatalogStoreOptions | string);
|
|
1092
|
+
/** Resolved state-file path, for the CLI and tests. */
|
|
1093
|
+
filePath(): string;
|
|
1094
|
+
private load;
|
|
1095
|
+
/** The saved catalog for one account, or `undefined` when there is none. */
|
|
1096
|
+
get(account: string): SavedCatalog | undefined;
|
|
1097
|
+
/**
|
|
1098
|
+
* Remember a catalog for an account, replacing whatever was saved before.
|
|
1099
|
+
*
|
|
1100
|
+
* A failed write is swallowed: the plugin has already served these models,
|
|
1101
|
+
* and losing the *memory* of them is not worth surfacing.
|
|
1102
|
+
*/
|
|
1103
|
+
set(account: string, catalog: Omit<SavedCatalog, 'account'>): void;
|
|
1104
|
+
/** Forget one account's catalog — used when that account signs out. */
|
|
1105
|
+
delete(account: string): void;
|
|
1106
|
+
private persist;
|
|
1107
|
+
}
|
|
1108
|
+
//#endregion
|
|
1109
|
+
//#region src/probe-service.d.ts
|
|
1110
|
+
/** What the caller learns about a completed probe. */
|
|
1111
|
+
type QoderProbeStatus = {
|
|
1112
|
+
state: 'ok';
|
|
1113
|
+
validation: QoderProbeRecord['validation'];
|
|
1114
|
+
efforts: readonly string[];
|
|
1115
|
+
requests: number;
|
|
1116
|
+
} | {
|
|
1117
|
+
state: 'unavailable';
|
|
1118
|
+
reason: string;
|
|
1119
|
+
};
|
|
1120
|
+
/** Options for {@link QoderProbeService}. */
|
|
1121
|
+
interface QoderProbeServiceOptions {
|
|
1122
|
+
store: QoderProbeStore;
|
|
1123
|
+
catalog: QoderCatalog;
|
|
1124
|
+
credentials: QoderCredentialStore;
|
|
1125
|
+
client: QoderUpstreamClient;
|
|
1126
|
+
/** Whether probing is permitted at all; consulted before every sweep. */
|
|
1127
|
+
consent: () => boolean;
|
|
1128
|
+
/**
|
|
1129
|
+
* The account currently in effect, as a one-way hash of the credential, or
|
|
1130
|
+
* `undefined` while signed out.
|
|
1131
|
+
*
|
|
1132
|
+
* Records are read and written against this identity, and it is re-checked
|
|
1133
|
+
* after the sweep finishes: an observation produced under account A must not
|
|
1134
|
+
* be stored once account B is in effect, however long the probe took. The
|
|
1135
|
+
* caller's `clear()` on an account switch is not sufficient on its own,
|
|
1136
|
+
* because an in-flight probe completes *after* that clear.
|
|
1137
|
+
*/
|
|
1138
|
+
account: () => string | undefined;
|
|
1139
|
+
sentinel?: SentinelFactory;
|
|
1140
|
+
/** Injectable for tests; defaults to the live upstream sender. */
|
|
1141
|
+
send?: (modelId: string) => ProbeSender;
|
|
1142
|
+
}
|
|
1143
|
+
/**
|
|
1144
|
+
* Serial probe runner. One instance is shared by the manual API and any
|
|
1145
|
+
* future automatic trigger, so the two can never overlap.
|
|
1146
|
+
*/
|
|
1147
|
+
declare class QoderProbeService {
|
|
1148
|
+
private readonly options;
|
|
1149
|
+
private queue;
|
|
1150
|
+
private readonly pending;
|
|
1151
|
+
private running;
|
|
1152
|
+
constructor(options: QoderProbeServiceOptions);
|
|
1153
|
+
/** Whether a sweep is in flight right now. */
|
|
1154
|
+
isRunning(): boolean;
|
|
1155
|
+
/**
|
|
1156
|
+
* The record the adapter may use for this model, or `undefined`.
|
|
1157
|
+
*
|
|
1158
|
+
* Applies the plan's precedence (§5): a declared set always wins, so a model
|
|
1159
|
+
* that declares `supportedEfforts` is never answered from an observation.
|
|
1160
|
+
*/
|
|
1161
|
+
recordFor(modelId: string): QoderProbeRecord | undefined;
|
|
1162
|
+
/**
|
|
1163
|
+
* Probe one model, serially.
|
|
1164
|
+
*
|
|
1165
|
+
* The authenticated manual route supplies one-request consent after UI
|
|
1166
|
+
* confirmation. Other callers must pass the configured consent gate.
|
|
1167
|
+
* Manual consent never changes the automatic-probing configuration.
|
|
1168
|
+
* Explicit requests bypass historical results, but share an ongoing run.
|
|
1169
|
+
*/
|
|
1170
|
+
probe(modelId: string, manualConsent?: boolean): Promise<QoderProbeStatus>;
|
|
1171
|
+
}
|
|
1172
|
+
//#endregion
|
|
1173
|
+
//#region src/auth-route.d.ts
|
|
1174
|
+
/** Outcome of a save attempt: stored, or refused with a stable reason. */
|
|
1175
|
+
type QoderAuthSaveResult = {
|
|
1176
|
+
ok: true;
|
|
1177
|
+
status: QoderAuthStatus;
|
|
1178
|
+
} | {
|
|
1179
|
+
ok: false;
|
|
1180
|
+
error: 'qoder_invalid_pat' | 'qoder_missing_pat';
|
|
1181
|
+
};
|
|
1182
|
+
/** Constructor dependencies. */
|
|
1183
|
+
interface QoderAuthRouteOptions {
|
|
1184
|
+
/**
|
|
1185
|
+
* Validate one pasted token against this variant's region and, only on
|
|
1186
|
+
* success, persist it. The route never decides validity itself.
|
|
1187
|
+
*/
|
|
1188
|
+
save: (pat: string) => Promise<QoderAuthSaveResult>;
|
|
1189
|
+
/** Remove the stored credential. */
|
|
1190
|
+
clear: () => Promise<void>;
|
|
1191
|
+
/**
|
|
1192
|
+
* Route path to mount. Defaults to the China variant's path so callers that
|
|
1193
|
+
* only serve it keep their behaviour; the global variant passes its own.
|
|
1194
|
+
*/
|
|
1195
|
+
path?: string;
|
|
1196
|
+
}
|
|
1197
|
+
/** Mint the per-process PAT control key. */
|
|
1198
|
+
declare function createAuthKey(): string;
|
|
1199
|
+
/**
|
|
1200
|
+
* The PAT route's handler, extracted so tests can mount it on a bare server
|
|
1201
|
+
* with a known key.
|
|
1202
|
+
*
|
|
1203
|
+
* @param deps - the save/clear operations for one variant.
|
|
1204
|
+
* @param key - the in-process control key this route requires.
|
|
1205
|
+
* @returns the Node request handler.
|
|
1206
|
+
*/
|
|
1207
|
+
declare function qoderAuthHandler(deps: QoderAuthRouteOptions, key: string): (req: IncomingMessage, res: ServerResponse) => Promise<void>;
|
|
1208
|
+
/** Mount the POST PAT route on an optional webServer context. */
|
|
1209
|
+
declare function registerQoderAuthRoute(ctx: Context, deps: QoderAuthRouteOptions, key: string): void;
|
|
1210
|
+
//#endregion
|
|
1211
|
+
//#region src/host-heartbeat.d.ts
|
|
1212
|
+
/**
|
|
1213
|
+
* Host-side heartbeat: a small JSON file written into the plugin's data
|
|
1214
|
+
* directory once the `qoder` providers are registered. The status CLI reads it
|
|
1215
|
+
* to report whether the host bundle is alive, independent of the browser card.
|
|
1216
|
+
*
|
|
1217
|
+
* The browser (client) bundle cannot write files; its health is reported
|
|
1218
|
+
* only through `console.error` on failure (see `src/client/index.tsx`).
|
|
1219
|
+
* This asymmetry is intentional: the host is the load-bearing half, and
|
|
1220
|
+
* a missing heartbeat unambiguously means the host never started.
|
|
1221
|
+
*
|
|
1222
|
+
* @module dsh-qoder-connect/host-heartbeat
|
|
1223
|
+
*/
|
|
1224
|
+
/** Basename of the host heartbeat file inside the plugin's state directory. */
|
|
1225
|
+
declare const QODER_HOST_HEARTBEAT_FILENAME = ".qoder-host-heartbeat.json";
|
|
1226
|
+
/** Current on-disk heartbeat format; readers reject others. */
|
|
1227
|
+
declare const HEARTBEAT_FORMAT_VERSION = 1;
|
|
1228
|
+
/** On-disk shape of the heartbeat. */
|
|
1229
|
+
interface QoderHostHeartbeat {
|
|
1230
|
+
version: typeof HEARTBEAT_FORMAT_VERSION;
|
|
1231
|
+
package: 'dsh-qoder-connect';
|
|
1232
|
+
pluginVersion: string;
|
|
1233
|
+
/** Epoch milliseconds when the host registered the provider. */
|
|
1234
|
+
registeredAt: number;
|
|
1235
|
+
/** Host process PID, to distinguish a stale heartbeat after a crash. */
|
|
1236
|
+
pid: number;
|
|
1237
|
+
}
|
|
1238
|
+
/** Absolute path of the host heartbeat file. */
|
|
1239
|
+
declare function qoderHostHeartbeatPath(): string;
|
|
1240
|
+
/** Remove the heartbeat on plugin disposal so a stale file does not linger. */
|
|
1241
|
+
declare function clearHostHeartbeat(): Promise<void>;
|
|
1242
|
+
/** Read and validate the heartbeat; returns `undefined` when absent or malformed. */
|
|
1243
|
+
declare function readHostHeartbeat(): Promise<QoderHostHeartbeat | undefined>;
|
|
1244
|
+
/**
|
|
1245
|
+
* Absolute start time (epoch ms) of the process holding `pid`, or `undefined`
|
|
1246
|
+
* when it cannot be determined (no such PID, platform lacks a readable source).
|
|
1247
|
+
*
|
|
1248
|
+
* - macOS / Linux: `ps -o lstart=` prints a local-time "EEE MMM DD HH:MM:SS YYYY";
|
|
1249
|
+
* `Date.parse` resolves it against the local clock, which matches how
|
|
1250
|
+
* `registeredAt` (a `Date.now()` absolute value) is expressed.
|
|
1251
|
+
* - Windows: WMI `CreationDate` is UTC (`YYYYMMDDHHMMSS.mmm+zzzz`); parsed with
|
|
1252
|
+
* `Date.UTC`, again comparable to `registeredAt`.
|
|
1253
|
+
*
|
|
1254
|
+
* Failures return `undefined` so callers can fall back to plain PID liveness
|
|
1255
|
+
* rather than mis-report a running host as dead.
|
|
1256
|
+
*/
|
|
1257
|
+
declare function processStartTimeMs(pid: number): number | undefined;
|
|
1258
|
+
/**
|
|
1259
|
+
* Whether the heartbeat's PID is still alive *and* still the same process that
|
|
1260
|
+
* registered it. A stale heartbeat (host crashed without clearing the file)
|
|
1261
|
+
* is distinguished from a live host by two checks:
|
|
1262
|
+
*
|
|
1263
|
+
* 1. `process.kill(pid, 0)` — the PID exists (signal 0 tests existence).
|
|
1264
|
+
* 2. The process holding that PID started at or before `registeredAt`. A host
|
|
1265
|
+
* that registered the heartbeat must have been started before writing it,
|
|
1266
|
+
* so `start <= registeredAt`; a recycled PID belongs to an unrelated process
|
|
1267
|
+
* started after the host died, so `start > registeredAt` correctly reads dead.
|
|
1268
|
+
*
|
|
1269
|
+
* PID-only detection is not enough: after a crash the OS may hand the same PID
|
|
1270
|
+
* to an unrelated process, and the un-cleared stale heartbeat would otherwise
|
|
1271
|
+
* produce a false "Host running". When the process start time cannot be read
|
|
1272
|
+
* (e.g. unsupported platform) the check degrades to plain PID liveness.
|
|
1273
|
+
*/
|
|
1274
|
+
declare function isHeartbeatProcessAlive(heartbeat: QoderHostHeartbeat): boolean;
|
|
1275
|
+
//#endregion
|
|
1276
|
+
//#region src/index.d.ts
|
|
1277
|
+
/** Stable Cordis plugin name. */
|
|
1278
|
+
declare const name = "llm-qoder";
|
|
1279
|
+
/** The model registry required before the provider can register. */
|
|
1280
|
+
declare const inject: string[];
|
|
1281
|
+
/**
|
|
1282
|
+
* Settings namespace owning the China card's section.
|
|
1283
|
+
*
|
|
1284
|
+
* DSH 0.1.2 dropped the `settingsNamespace()` branding function: a namespace is
|
|
1285
|
+
* now a nominal string, validated by the type system where it is used rather
|
|
1286
|
+
* than at runtime by a function call. The brand is compile-time only, so this
|
|
1287
|
+
* stays the plain string it always was — every comparison, descriptor lookup,
|
|
1288
|
+
* and `dsh` config file still sees `'qoder'`. It is cast once here so the
|
|
1289
|
+
* public constant carries the seam's type without pulling the brand helper
|
|
1290
|
+
* into this package (upstream DSH plugins, `dsh-llm-pi-ai` included, pass
|
|
1291
|
+
* their namespaces as plain string literals).
|
|
1292
|
+
*/
|
|
1293
|
+
declare const QODER_SETTINGS_NS: SettingsNamespace;
|
|
1294
|
+
/**
|
|
1295
|
+
* Settings namespace owning the global card's section.
|
|
1296
|
+
*
|
|
1297
|
+
* One namespace per card, not one shared: the settings Plugins tab dispatches a
|
|
1298
|
+
* card by rendering `settings.plugin.item` with `entryKey = ns` for each
|
|
1299
|
+
* namespace the Host serves, and skips an entry whose key names no served
|
|
1300
|
+
* namespace. With a single installed section, the global card registers
|
|
1301
|
+
* into the slot but is never rendered — the card list is built from the Host's
|
|
1302
|
+
* sections, not from the slot's entries. Each card therefore needs its own
|
|
1303
|
+
* installed section whose namespace equals the card's slot key.
|
|
1304
|
+
*/
|
|
1305
|
+
declare const QODER_GLOBAL_SETTINGS_NS: SettingsNamespace;
|
|
1306
|
+
/**
|
|
1307
|
+
* Settings namespace owning the shared quota-card section.
|
|
1308
|
+
*
|
|
1309
|
+
* One card above the two variant cards configures both sidebar quota widgets
|
|
1310
|
+
* (China and global) from a single place, so its toggles cannot live in
|
|
1311
|
+
* either variant's section — they are per-variant fields on a cross-variant
|
|
1312
|
+
* card. The Plugins tab dispatches by namespace, so this section is what makes
|
|
1313
|
+
* that card render (see {@link QODER_GLOBAL_SETTINGS_NS} for the mechanism).
|
|
1314
|
+
*/
|
|
1315
|
+
declare const QODER_QUOTA_SETTINGS_NS: SettingsNamespace;
|
|
1316
|
+
/** Plugin configuration. */
|
|
1317
|
+
interface Config {
|
|
1318
|
+
/**
|
|
1319
|
+
* Whether the user has authorized sending probe requests about reasoning
|
|
1320
|
+
* efforts. Off by default: a probe spends real credit, so nothing is sent
|
|
1321
|
+
* until the user explicitly agrees.
|
|
1322
|
+
*/
|
|
1323
|
+
probeConsent?: boolean;
|
|
1324
|
+
/** Use the largest context window the global catalog explicitly offers. */
|
|
1325
|
+
useMaximumContextWindow?: boolean;
|
|
1326
|
+
/** The China variant's own maximum-window preference, persisted in its section. */
|
|
1327
|
+
useMaximumContextWindowCN?: boolean;
|
|
1328
|
+
/**
|
|
1329
|
+
* Per-model context-window overrides for the global variant (model id →
|
|
1330
|
+
* tokens). Kept for schema compatibility; the card no longer writes
|
|
1331
|
+
* per-model overrides because the upstream honours only the default and
|
|
1332
|
+
* the maximum, nothing between.
|
|
1333
|
+
*/
|
|
1334
|
+
modelContextWindows?: Record<string, number>;
|
|
1335
|
+
/** The China variant's per-model overrides; see {@link Config.modelContextWindows}. */
|
|
1336
|
+
modelContextWindowsCN?: Record<string, number>;
|
|
1337
|
+
/** Show the China variant's sidebar quota card. */
|
|
1338
|
+
sidebarQuotaCN?: boolean;
|
|
1339
|
+
/** Show the global variant's sidebar quota card. */
|
|
1340
|
+
sidebarQuotaGlobal?: boolean;
|
|
1341
|
+
/**
|
|
1342
|
+
* Sidebar quota refresh interval in milliseconds. One shared value (both
|
|
1343
|
+
* cards poll on it) because the two widgets hit the same rate-limited
|
|
1344
|
+
* billing family; the floor guards against a typo hammering the quota
|
|
1345
|
+
* endpoint, which serves no cache.
|
|
1346
|
+
*/
|
|
1347
|
+
quotaPollMs?: number;
|
|
1348
|
+
}
|
|
1349
|
+
/**
|
|
1350
|
+
* Quota poll interval: default 5 minutes, floor 1 minute. The status route
|
|
1351
|
+
* performs a live upstream billing call per request with no cache, so an
|
|
1352
|
+
* aggressively small interval translates directly into upstream load; the
|
|
1353
|
+
* floor is the smallest value the UI offers rather than a silent clamp —
|
|
1354
|
+
* smaller staged values fail Host validation and refuse to save.
|
|
1355
|
+
*/
|
|
1356
|
+
declare const QUOTA_POLL_DEFAULT_MS = 300000;
|
|
1357
|
+
declare const QUOTA_POLL_MIN_MS = 60000;
|
|
1358
|
+
declare const Config: z<Config>;
|
|
1359
|
+
/**
|
|
1360
|
+
* Start both variants: their loopback endpoints, the `qoder` and
|
|
1361
|
+
* `qoder-global` providers, their configuration cards, and their
|
|
1362
|
+
* credential-driven catalog lifecycles.
|
|
1363
|
+
*
|
|
1364
|
+
* Each variant registers unconditionally; what varies is whether its catalog is
|
|
1365
|
+
* *visible*. An empty catalog is how DSH hides a model group (the host filters
|
|
1366
|
+
* out groups with no models), which keeps a token saved after startup working
|
|
1367
|
+
* without re-registering the provider.
|
|
1368
|
+
*/
|
|
1369
|
+
declare function apply(ctx: Context, config: Config): void;
|
|
1370
|
+
//#endregion
|
|
1371
|
+
export { CHINA_VARIANT, Config, FALLBACK_QODER_MODELS, GLOBAL_VARIANT, KIND_STATUS, PROBE_EFFORT_CANDIDATES, type ProbeAttempt, type ProbeOutcome, type ProbeSender, QODER_AUTH_FILENAME, QODER_AUTH_PATH, QODER_DATA_DIR_ENV, QODER_DATA_DIR_NAME, QODER_GLOBAL_AUTH_PATH, QODER_GLOBAL_PROBE_PATH, QODER_GLOBAL_SETTINGS_NS, QODER_GLOBAL_STATUS_PATH, QODER_HOST_HEARTBEAT_FILENAME, QODER_PAT_ENV_CN, QODER_PAT_ENV_GLOBAL, QODER_PROBE_FILENAME, QODER_PROBE_PATH, QODER_PROVIDER, QODER_QUOTA_SETTINGS_NS, QODER_SETTINGS_NS, QODER_STATUS_PATH, QODER_STREAM_IDLE_TIMEOUT_MS, QODER_VARIANTS, QUOTA_POLL_DEFAULT_MS, QUOTA_POLL_MIN_MS, type QoderAdapter, type QoderAuthRequest, type QoderAuthRouteOptions, type QoderAuthSaveResult, type QoderAuthStatus, QoderCatalog, type QoderCatalogFetch, type QoderCatalogModelSnapshot, QoderCatalogStore, type QoderCatalogStoreOptions, type QoderChatResult, type QoderCredential, QoderCredentialStore, type QoderCreditAccount, type QoderCredits, type QoderEffort, type QoderHostHeartbeat, type QoderModelBilling, type QoderModelInfo, type QoderModelReasoning, type QoderPatSummary, type QoderProbeAction, type QoderProbeRecord, QoderProbeService, type QoderProbeStatus, QoderProbeStore, type QoderProbeValidation, type QoderShim, QoderUpstreamClient, type QoderVariant, type QoderVariantId, type QoderWebCatalog, type QoderWebCreditAccount, type QoderWebCredits, type QoderWebProbeModel, type QoderWebProbeSection, type QoderWebStatus, type ShimLogger, type UpstreamErrorKind, apply, classifyUpstreamError, clearHostHeartbeat, createAuthKey, createQoderAdapter, createQoderShim, fingerprintModel, inject, isHeartbeatProcessAlive, kindFromQoderFailure, modelInfoOf, name, normalizeCredits, patTail, probeModel, processStartTimeMs, qoderAuthHandler, qoderCatalogPath, qoderCredentialIdentity, qoderHostHeartbeatPath, qoderOwnAuthPath, qoderPluginDataDir, qoderProbePath, randomSentinel, readHostHeartbeat, registerQoderAuthRoute, variantFor };
|