dsh-qoder-connect 0.2.6 → 0.2.8
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/README.en.md +33 -8
- package/README.md +33 -8
- package/assets/3.png +0 -0
- package/assets/5.png +0 -0
- package/assets/6.png +0 -0
- package/assets/7.png +0 -0
- package/assets/8.png +0 -0
- package/lib/bin.js +109 -13
- package/lib/client.js +301 -117
- package/lib/index.d.ts +152 -22
- package/lib/index.js +369 -398
- package/lib/{variants-COxX6r1j.js → variants-Dl1AbWre.js} +474 -51
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { A as
|
|
1
|
+
import { $ as configurePluginPaths, A as probeModel, B as QODER_PROBE_PATH, C as normalizeCredits, D as utc8WallClock, E as formatUtc8Date, F as QoderCatalog, G as QODER_PAT_ENV_GLOBAL, H as QODER_STATUS_PATH, I as QODER_AUTH_PATH, J as qoderCredentialId, K as QoderCredentialStore, L as QODER_GLOBAL_AUTH_PATH, M as QoderCatalogStore, N as qoderCatalogPath, O as getMachineId, P as FALLBACK_QODER_MODELS, Q as QODER_DATA_DIR_NAME, R as QODER_GLOBAL_PROBE_PATH, S as modelInfoOf, T as createQoderTransport, U as QODER_AUTH_FILENAME, V as QODER_SETTINGS_FACE_PATH, W as QODER_PAT_ENV_CN, X as qoderOwnAuthPath, Y as qoderCredentialIdentity, Z as QODER_DATA_DIR_ENV, _ as qoderProbePath, a as QODER_HOST_HEARTBEAT_FILENAME, b as classifyUpstreamError, c as processStartTimeMs, d as writeHostHeartbeat, et as qoderMachineIdPath, f as QODER_CONNECT_VERSION, g as newestFirst, h as fingerprintModel, i as variantFor, j as randomSentinel, k as PROBE_EFFORT_CANDIDATES, l as qoderHostHeartbeatPath, m as QoderProbeStore, n as GLOBAL_VARIANT, o as clearHostHeartbeat, p as QODER_PROBE_FILENAME, q as patTail, r as QODER_VARIANTS, s as isHeartbeatProcessAlive, t as CHINA_VARIANT, tt as qoderPluginDataDir, u as readHostHeartbeat, v as KIND_STATUS, w as validateApiKey, x as kindFromQoderFailure, y as QoderUpstreamClient, z as QODER_GLOBAL_STATUS_PATH } from "./variants-Dl1AbWre.js";
|
|
2
2
|
import z from "@deepseek-ai/schemastery";
|
|
3
|
-
import {
|
|
4
|
-
import { dirname, join
|
|
3
|
+
import { randomBytes, timingSafeEqual } from "node:crypto";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
5
|
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
6
6
|
import { createProvider } from "@earendil-works/pi-ai";
|
|
7
7
|
import { openAICompletionsApi } from "@earendil-works/pi-ai/api/openai-completions.lazy";
|
|
@@ -60,7 +60,8 @@ function originIsLoopback(origin) {
|
|
|
60
60
|
//#endregion
|
|
61
61
|
//#region src/auth-route.ts
|
|
62
62
|
/**
|
|
63
|
-
* PAT route: saves (validate-then-persist) or clears
|
|
63
|
+
* PAT route: saves (validate-then-persist), switches, removes, or clears the
|
|
64
|
+
* tokens of one variant.
|
|
64
65
|
*
|
|
65
66
|
* The state-changing endpoints the plugin exposes share one guard shape — a
|
|
66
67
|
* loopback Host and Origin, plus the in-process key the card receives with its
|
|
@@ -111,9 +112,20 @@ function parseRequest(text) {
|
|
|
111
112
|
if (action === "save-pat") {
|
|
112
113
|
const pat = wrapped["pat"];
|
|
113
114
|
if (typeof pat !== "string" || pat.trim() === "") return void 0;
|
|
115
|
+
const label = wrapped["label"];
|
|
116
|
+
if (label !== void 0 && (typeof label !== "string" || label.length > 200)) return void 0;
|
|
114
117
|
return {
|
|
115
118
|
action,
|
|
116
|
-
pat
|
|
119
|
+
pat,
|
|
120
|
+
...label === void 0 ? {} : { label }
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
if (action === "activate" || action === "remove") {
|
|
124
|
+
const id = wrapped["id"];
|
|
125
|
+
if (typeof id !== "string" || id.trim() === "") return void 0;
|
|
126
|
+
return {
|
|
127
|
+
action,
|
|
128
|
+
id: id.trim()
|
|
117
129
|
};
|
|
118
130
|
}
|
|
119
131
|
}
|
|
@@ -143,7 +155,7 @@ function keyMatches$1(expected, presented) {
|
|
|
143
155
|
* The PAT route's handler, extracted so tests can mount it on a bare server
|
|
144
156
|
* with a known key.
|
|
145
157
|
*
|
|
146
|
-
* @param deps - the save/clear operations for one variant.
|
|
158
|
+
* @param deps - the save/activate/remove/clear operations for one variant.
|
|
147
159
|
* @param key - the in-process control key this route requires.
|
|
148
160
|
* @returns the Node request handler.
|
|
149
161
|
*/
|
|
@@ -177,7 +189,15 @@ function qoderAuthHandler(deps, key) {
|
|
|
177
189
|
json$2(res, 200, { ok: true });
|
|
178
190
|
return;
|
|
179
191
|
}
|
|
180
|
-
|
|
192
|
+
if (request.action === "activate") {
|
|
193
|
+
json$2(res, 200, await deps.activate(request.id));
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (request.action === "remove") {
|
|
197
|
+
json$2(res, 200, await deps.remove(request.id));
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
json$2(res, 200, await deps.save(request.pat, request.label));
|
|
181
201
|
} catch (error) {
|
|
182
202
|
json$2(res, 200, {
|
|
183
203
|
ok: false,
|
|
@@ -201,6 +221,245 @@ function registerQoderAuthRoute(ctx, deps, key) {
|
|
|
201
221
|
}, "dsh-qoder-connect: PAT route");
|
|
202
222
|
}
|
|
203
223
|
//#endregion
|
|
224
|
+
//#region src/shim.ts
|
|
225
|
+
/**
|
|
226
|
+
* Loopback OpenAI-compatible endpoint. The pi-ai provider points here; the
|
|
227
|
+
* shim guards the local boundary and hands the raw JSON body to the upstream
|
|
228
|
+
* client, which translates it onto the Qoder transport. It binds 127.0.0.1
|
|
229
|
+
* only and never serves another interface.
|
|
230
|
+
*
|
|
231
|
+
* Inbound hardening: the loopback bind alone is not a trust boundary (any
|
|
232
|
+
* local process or a DNS-rebinding page can reach 127.0.0.1), so every
|
|
233
|
+
* request must carry a loopback Host header, browser-sent Origins must be
|
|
234
|
+
* loopback, chat POSTs must be application/json, and the Authorization
|
|
235
|
+
* header must carry the shim's per-process shared secret. The plugin's
|
|
236
|
+
* own client satisfies all four by construction; local attackers cannot
|
|
237
|
+
* read the secret out of the plugin process's memory.
|
|
238
|
+
*
|
|
239
|
+
* @module dsh-qoder-connect/shim
|
|
240
|
+
*/
|
|
241
|
+
const REQUEST_BODY_LIMIT = 67108864;
|
|
242
|
+
/** Chat-completion POSTs must carry a JSON body type (simple-request CSRF drops here). */
|
|
243
|
+
function isJsonContentType(req) {
|
|
244
|
+
const type = req.headers["content-type"];
|
|
245
|
+
return typeof type === "string" && type.trim().toLowerCase().startsWith("application/json");
|
|
246
|
+
}
|
|
247
|
+
function writeJson(res, status, body, headers = {}) {
|
|
248
|
+
const payload = JSON.stringify(body);
|
|
249
|
+
res.writeHead(status, {
|
|
250
|
+
"Content-Type": "application/json",
|
|
251
|
+
"Content-Length": Buffer.byteLength(payload),
|
|
252
|
+
...headers
|
|
253
|
+
});
|
|
254
|
+
res.end(payload);
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* The prefix the shim stamps onto a failure message carrying an upstream retry
|
|
258
|
+
* hint.
|
|
259
|
+
*
|
|
260
|
+
* The hint has to cross two text-only boundaries to reach the harness retry
|
|
261
|
+
* policy: pi-ai flattens an HTTP failure to its message and the adapter may only
|
|
262
|
+
* rewrite that message. So the marker is written here, in front of the 400-byte
|
|
263
|
+
* excerpt (never inside it, where truncation could cut it off), and read back by
|
|
264
|
+
* {@link retryAfterFromMessage}.
|
|
265
|
+
*/
|
|
266
|
+
const RETRY_AFTER_TAG = /\[retry-after=(\d+)s\]/u;
|
|
267
|
+
/**
|
|
268
|
+
* The upstream's retry hint, in milliseconds, recovered from a shim failure
|
|
269
|
+
* message.
|
|
270
|
+
*
|
|
271
|
+
* The tagged form is the contract this plugin writes itself. The untagged
|
|
272
|
+
* `retryAfterSeconds` form is the fallback for a message the transport built
|
|
273
|
+
* before the tag existed (an installed older shim, or a caller that composed the
|
|
274
|
+
* message itself), and is the same field {@link qoderQueueSignal} reads.
|
|
275
|
+
*/
|
|
276
|
+
function retryAfterFromMessage(message) {
|
|
277
|
+
const tagged = RETRY_AFTER_TAG.exec(message);
|
|
278
|
+
if (tagged !== null) {
|
|
279
|
+
const seconds = Number(tagged[1]);
|
|
280
|
+
return Number.isFinite(seconds) && seconds > 0 ? seconds * 1e3 : void 0;
|
|
281
|
+
}
|
|
282
|
+
const body = /"retryAfterSeconds"\s*:\s*(\d+)/u.exec(message);
|
|
283
|
+
if (body !== null) {
|
|
284
|
+
const seconds = Number(body[1]);
|
|
285
|
+
return Number.isFinite(seconds) && seconds > 0 ? seconds * 1e3 : void 0;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Seconds an OpenAI `Retry-After` header carries: the ceiling of the hint, so a
|
|
290
|
+
* client that honors it never returns before the upstream's window closed.
|
|
291
|
+
*/
|
|
292
|
+
function retryAfterSeconds(retryAfterMs) {
|
|
293
|
+
return Math.max(1, Math.ceil(retryAfterMs / 1e3));
|
|
294
|
+
}
|
|
295
|
+
function writeOpenAIError(res, status, kind, message, retryAfterMs) {
|
|
296
|
+
const hint = typeof retryAfterMs === "number" && Number.isFinite(retryAfterMs) && retryAfterMs > 0 ? Math.floor(retryAfterMs) : void 0;
|
|
297
|
+
if (hint === void 0) {
|
|
298
|
+
writeJson(res, status, { error: {
|
|
299
|
+
message,
|
|
300
|
+
type: kind,
|
|
301
|
+
code: kind
|
|
302
|
+
} });
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
const seconds = retryAfterSeconds(hint);
|
|
306
|
+
writeJson(res, status, { error: {
|
|
307
|
+
message: `[retry-after=${seconds}s] ${message}`,
|
|
308
|
+
type: kind,
|
|
309
|
+
code: kind
|
|
310
|
+
} }, {
|
|
311
|
+
"Retry-After": String(seconds),
|
|
312
|
+
"retry-after-ms": String(hint)
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
/** Read a request body with a size cap; over-limit bodies fail the request. */
|
|
316
|
+
function readBody$1(req) {
|
|
317
|
+
return new Promise((resolve, reject) => {
|
|
318
|
+
const chunks = [];
|
|
319
|
+
let size = 0;
|
|
320
|
+
req.on("data", (chunk) => {
|
|
321
|
+
size += chunk.length;
|
|
322
|
+
if (size > REQUEST_BODY_LIMIT) {
|
|
323
|
+
reject(/* @__PURE__ */ new Error("request body too large"));
|
|
324
|
+
req.destroy();
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
chunks.push(chunk);
|
|
328
|
+
});
|
|
329
|
+
req.on("end", () => resolve(Buffer.concat(chunks)));
|
|
330
|
+
req.on("error", reject);
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Start the loopback endpoint. Requests carry any bearer; the loopback bind
|
|
335
|
+
* is the boundary, and the upstream credential comes from the store alone.
|
|
336
|
+
*/
|
|
337
|
+
function createQoderShim(options) {
|
|
338
|
+
const { store, client, catalog } = options;
|
|
339
|
+
const logger = options.logger;
|
|
340
|
+
const providerId = options.providerId;
|
|
341
|
+
const SHARED_SECRET = randomBytes(32).toString("base64url");
|
|
342
|
+
/** Constant-time bearer check; absent or mismatched bearers are rejected. */
|
|
343
|
+
function bearerOk(req) {
|
|
344
|
+
const header = req.headers.authorization;
|
|
345
|
+
if (typeof header !== "string") return false;
|
|
346
|
+
const match = /^Bearer\s+(.+)$/i.exec(header.trim());
|
|
347
|
+
if (match === null) return false;
|
|
348
|
+
const presented = match[1];
|
|
349
|
+
const expected = SHARED_SECRET;
|
|
350
|
+
const a = Buffer.from(presented);
|
|
351
|
+
const b = Buffer.from(expected);
|
|
352
|
+
if (a.length !== b.length) return false;
|
|
353
|
+
return timingSafeEqual(a, b);
|
|
354
|
+
}
|
|
355
|
+
const server = createServer((req, res) => {
|
|
356
|
+
handle(req, res).catch((error) => {
|
|
357
|
+
logger?.warn("dsh-qoder-connect: loopback request failed", error);
|
|
358
|
+
try {
|
|
359
|
+
if (!res.headersSent) writeOpenAIError(res, 500, "internal", "loopback request failed");
|
|
360
|
+
else res.end();
|
|
361
|
+
} catch {}
|
|
362
|
+
});
|
|
363
|
+
});
|
|
364
|
+
const ready = new Promise((resolve, reject) => {
|
|
365
|
+
server.once("listening", () => resolve());
|
|
366
|
+
server.once("error", reject);
|
|
367
|
+
});
|
|
368
|
+
server.listen(0, "127.0.0.1");
|
|
369
|
+
const baseUrl = () => {
|
|
370
|
+
const address = server.address();
|
|
371
|
+
if (address === null || typeof address === "string") throw new Error(`${providerId} shim has no listening address`);
|
|
372
|
+
return `http://127.0.0.1:${address.port}`;
|
|
373
|
+
};
|
|
374
|
+
async function handle(req, res) {
|
|
375
|
+
try {
|
|
376
|
+
if (!hostIsLoopback(req.headers.host)) {
|
|
377
|
+
writeOpenAIError(res, 403, "host_not_allowed", "Host header must name the loopback interface");
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
if (!originIsLoopback(req.headers.origin)) {
|
|
381
|
+
writeOpenAIError(res, 403, "origin_not_allowed", "Origin must be a loopback origin");
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
if (!bearerOk(req)) {
|
|
385
|
+
writeOpenAIError(res, 401, "unauthorized", "missing or invalid Authorization bearer");
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
const url = req.url ?? "/";
|
|
389
|
+
if (req.method === "GET" && (url === "/healthz" || url === "/healthz/")) {
|
|
390
|
+
writeJson(res, 200, { ok: true });
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
if (req.method === "GET" && (url === "/v1/models" || url === "/v1/models/")) {
|
|
394
|
+
writeJson(res, 200, {
|
|
395
|
+
object: "list",
|
|
396
|
+
data: catalog.current().map((model) => ({
|
|
397
|
+
id: model.id,
|
|
398
|
+
object: "model",
|
|
399
|
+
created: 0,
|
|
400
|
+
owned_by: providerId
|
|
401
|
+
}))
|
|
402
|
+
});
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
if (req.method === "POST" && (url === "/v1/chat/completions" || url === "/v1/chat/completions/")) {
|
|
406
|
+
await chatCompletions(req, res);
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
writeOpenAIError(res, 404, "not_found", `no such route: ${req.method} ${url}`);
|
|
410
|
+
} catch (error) {
|
|
411
|
+
if (!res.headersSent) writeOpenAIError(res, 500, "internal", String(error));
|
|
412
|
+
else res.end();
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
async function chatCompletions(req, res) {
|
|
416
|
+
if (!isJsonContentType(req)) {
|
|
417
|
+
writeOpenAIError(res, 415, "unsupported_media_type", "Content-Type must be application/json");
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
try {
|
|
421
|
+
await store.resolve();
|
|
422
|
+
} catch (error) {
|
|
423
|
+
writeOpenAIError(res, KIND_STATUS.missing_credential, "not_signed_in", String(error));
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
const raw = (await readBody$1(req)).toString("utf8");
|
|
427
|
+
const controller = new AbortController();
|
|
428
|
+
req.on("close", () => controller.abort());
|
|
429
|
+
const result = await client.chatStream(raw, controller.signal);
|
|
430
|
+
if (!result.ok) {
|
|
431
|
+
writeOpenAIError(res, KIND_STATUS[result.kind], result.kind, `${providerId} upstream ${result.kind} (http ${KIND_STATUS[result.kind]}): ${result.message.slice(0, 400)}`, result.retryAfterMs);
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
res.writeHead(200, {
|
|
435
|
+
"Content-Type": "text/event-stream",
|
|
436
|
+
"Cache-Control": "no-cache",
|
|
437
|
+
"Connection": "keep-alive",
|
|
438
|
+
"X-Accel-Buffering": "no"
|
|
439
|
+
});
|
|
440
|
+
let sawDone = false;
|
|
441
|
+
const body = Readable.fromWeb(result.response.body);
|
|
442
|
+
body.on("data", (chunk) => {
|
|
443
|
+
if (chunk.includes("[DONE]")) sawDone = true;
|
|
444
|
+
});
|
|
445
|
+
body.on("error", (error) => {
|
|
446
|
+
logger?.warn("dsh-qoder-connect: upstream stream failed mid-flight", error);
|
|
447
|
+
if (!sawDone && res.writable) res.end("data: [DONE]\n\n");
|
|
448
|
+
});
|
|
449
|
+
body.pipe(res);
|
|
450
|
+
}
|
|
451
|
+
return {
|
|
452
|
+
ready,
|
|
453
|
+
baseUrl,
|
|
454
|
+
token: () => SHARED_SECRET,
|
|
455
|
+
close: () => new Promise((resolve, reject) => {
|
|
456
|
+
server.close(() => resolve());
|
|
457
|
+
server.closeAllConnections();
|
|
458
|
+
server.once("error", reject);
|
|
459
|
+
})
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
//#endregion
|
|
204
463
|
//#region src/adapter.ts
|
|
205
464
|
/**
|
|
206
465
|
* The Qoder pi-ai provider: one loopback-backed adapter per variant route,
|
|
@@ -472,6 +731,64 @@ var QoderPiAiAdapter = class QoderPiAiAdapter extends PiAiAdapter {
|
|
|
472
731
|
};
|
|
473
732
|
}
|
|
474
733
|
/**
|
|
734
|
+
* Carry the upstream's retry hint onto the finish failure.
|
|
735
|
+
*
|
|
736
|
+
* `dsh-llm-pi-ai` maps a failed pi-ai turn onto `{ message, code }` before the
|
|
737
|
+
* chunk reaches the loop: the HTTP status and headers pi-ai's own error object
|
|
738
|
+
* still holds are flattened away, so `failure.providerRetryAfterMs` — the one
|
|
739
|
+
* field the harness retry policy honors — never arrives. The policy then falls
|
|
740
|
+
* back to its local backoff, which is capped (20s in the report that prompted
|
|
741
|
+
* this) below Qoder's 30-second queue answer, so every retry hits the same
|
|
742
|
+
* closed window.
|
|
743
|
+
*
|
|
744
|
+
* The shim stamps `[retry-after=<n>s]` in front of its error message for this
|
|
745
|
+
* layer to recover; see `retryAfterFromMessage`. Rewriting only the field,
|
|
746
|
+
* never the message, keeps the text a human reads unchanged.
|
|
747
|
+
*
|
|
748
|
+
* Both entry points are wrapped because `PiAiAdapter` has two of them and the
|
|
749
|
+
* harness uses the other one: `prepareCall()` returns
|
|
750
|
+
* `stream: (options) => this.streamWithSnapshot(options, snapshot)`, which
|
|
751
|
+
* never calls `this.stream`. Overriding `stream` alone is dead code on every
|
|
752
|
+
* harness request (verified against a live session log: the finish failure
|
|
753
|
+
* arrived as bare `{ message, code }` while a direct `adapter.stream()` call
|
|
754
|
+
* carried the hint).
|
|
755
|
+
*/
|
|
756
|
+
async *stream(options) {
|
|
757
|
+
yield* this.withRetryHint(super.stream(options));
|
|
758
|
+
}
|
|
759
|
+
async prepareCall(provider, model, signal) {
|
|
760
|
+
const prepared = await super.prepareCall(provider, model, signal);
|
|
761
|
+
return {
|
|
762
|
+
...prepared,
|
|
763
|
+
stream: (options) => this.withRetryHint(prepared.stream(options))
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
/** Re-stamp `failure.providerRetryAfterMs` on every terminal failure chunk. */
|
|
767
|
+
async *withRetryHint(inner) {
|
|
768
|
+
for await (const chunk of inner) {
|
|
769
|
+
if (chunk.type === "finish") {
|
|
770
|
+
const { reason } = chunk;
|
|
771
|
+
if ((reason.kind === "error" || reason.kind === "aborted") && reason.failure.providerRetryAfterMs === void 0) {
|
|
772
|
+
const retryAfterMs = retryAfterFromMessage(reason.failure.message);
|
|
773
|
+
if (retryAfterMs !== void 0) {
|
|
774
|
+
yield {
|
|
775
|
+
...chunk,
|
|
776
|
+
reason: {
|
|
777
|
+
...reason,
|
|
778
|
+
failure: {
|
|
779
|
+
...reason.failure,
|
|
780
|
+
providerRetryAfterMs: retryAfterMs
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
};
|
|
784
|
+
continue;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
yield chunk;
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
/**
|
|
475
792
|
* Resolve the request-image projection target for one route.
|
|
476
793
|
*
|
|
477
794
|
* `dsh-llm` 0.2.1 added `LlmAdapter.imageRequestTarget()` and its runtime calls
|
|
@@ -497,389 +814,6 @@ var QoderPiAiAdapter = class QoderPiAiAdapter extends PiAiAdapter {
|
|
|
497
814
|
}
|
|
498
815
|
};
|
|
499
816
|
//#endregion
|
|
500
|
-
//#region src/shim.ts
|
|
501
|
-
/**
|
|
502
|
-
* Loopback OpenAI-compatible endpoint. The pi-ai provider points here; the
|
|
503
|
-
* shim guards the local boundary and hands the raw JSON body to the upstream
|
|
504
|
-
* client, which translates it onto the Qoder transport. It binds 127.0.0.1
|
|
505
|
-
* only and never serves another interface.
|
|
506
|
-
*
|
|
507
|
-
* Inbound hardening: the loopback bind alone is not a trust boundary (any
|
|
508
|
-
* local process or a DNS-rebinding page can reach 127.0.0.1), so every
|
|
509
|
-
* request must carry a loopback Host header, browser-sent Origins must be
|
|
510
|
-
* loopback, chat POSTs must be application/json, and the Authorization
|
|
511
|
-
* header must carry the shim's per-process shared secret. The plugin's
|
|
512
|
-
* own client satisfies all four by construction; local attackers cannot
|
|
513
|
-
* read the secret out of the plugin process's memory.
|
|
514
|
-
*
|
|
515
|
-
* @module dsh-qoder-connect/shim
|
|
516
|
-
*/
|
|
517
|
-
const REQUEST_BODY_LIMIT = 67108864;
|
|
518
|
-
/** Chat-completion POSTs must carry a JSON body type (simple-request CSRF drops here). */
|
|
519
|
-
function isJsonContentType(req) {
|
|
520
|
-
const type = req.headers["content-type"];
|
|
521
|
-
return typeof type === "string" && type.trim().toLowerCase().startsWith("application/json");
|
|
522
|
-
}
|
|
523
|
-
function writeJson(res, status, body) {
|
|
524
|
-
const payload = JSON.stringify(body);
|
|
525
|
-
res.writeHead(status, {
|
|
526
|
-
"Content-Type": "application/json",
|
|
527
|
-
"Content-Length": Buffer.byteLength(payload)
|
|
528
|
-
});
|
|
529
|
-
res.end(payload);
|
|
530
|
-
}
|
|
531
|
-
function writeOpenAIError(res, status, kind, message) {
|
|
532
|
-
writeJson(res, status, { error: {
|
|
533
|
-
message,
|
|
534
|
-
type: kind,
|
|
535
|
-
code: kind
|
|
536
|
-
} });
|
|
537
|
-
}
|
|
538
|
-
/** Read a request body with a size cap; over-limit bodies fail the request. */
|
|
539
|
-
function readBody$1(req) {
|
|
540
|
-
return new Promise((resolve, reject) => {
|
|
541
|
-
const chunks = [];
|
|
542
|
-
let size = 0;
|
|
543
|
-
req.on("data", (chunk) => {
|
|
544
|
-
size += chunk.length;
|
|
545
|
-
if (size > REQUEST_BODY_LIMIT) {
|
|
546
|
-
reject(/* @__PURE__ */ new Error("request body too large"));
|
|
547
|
-
req.destroy();
|
|
548
|
-
return;
|
|
549
|
-
}
|
|
550
|
-
chunks.push(chunk);
|
|
551
|
-
});
|
|
552
|
-
req.on("end", () => resolve(Buffer.concat(chunks)));
|
|
553
|
-
req.on("error", reject);
|
|
554
|
-
});
|
|
555
|
-
}
|
|
556
|
-
/**
|
|
557
|
-
* Start the loopback endpoint. Requests carry any bearer; the loopback bind
|
|
558
|
-
* is the boundary, and the upstream credential comes from the store alone.
|
|
559
|
-
*/
|
|
560
|
-
function createQoderShim(options) {
|
|
561
|
-
const { store, client, catalog } = options;
|
|
562
|
-
const logger = options.logger;
|
|
563
|
-
const providerId = options.providerId;
|
|
564
|
-
const SHARED_SECRET = randomBytes(32).toString("base64url");
|
|
565
|
-
/** Constant-time bearer check; absent or mismatched bearers are rejected. */
|
|
566
|
-
function bearerOk(req) {
|
|
567
|
-
const header = req.headers.authorization;
|
|
568
|
-
if (typeof header !== "string") return false;
|
|
569
|
-
const match = /^Bearer\s+(.+)$/i.exec(header.trim());
|
|
570
|
-
if (match === null) return false;
|
|
571
|
-
const presented = match[1];
|
|
572
|
-
const expected = SHARED_SECRET;
|
|
573
|
-
const a = Buffer.from(presented);
|
|
574
|
-
const b = Buffer.from(expected);
|
|
575
|
-
if (a.length !== b.length) return false;
|
|
576
|
-
return timingSafeEqual(a, b);
|
|
577
|
-
}
|
|
578
|
-
const server = createServer((req, res) => {
|
|
579
|
-
handle(req, res).catch((error) => {
|
|
580
|
-
logger?.warn("dsh-qoder-connect: loopback request failed", error);
|
|
581
|
-
try {
|
|
582
|
-
if (!res.headersSent) writeOpenAIError(res, 500, "internal", "loopback request failed");
|
|
583
|
-
else res.end();
|
|
584
|
-
} catch {}
|
|
585
|
-
});
|
|
586
|
-
});
|
|
587
|
-
const ready = new Promise((resolve, reject) => {
|
|
588
|
-
server.once("listening", () => resolve());
|
|
589
|
-
server.once("error", reject);
|
|
590
|
-
});
|
|
591
|
-
server.listen(0, "127.0.0.1");
|
|
592
|
-
const baseUrl = () => {
|
|
593
|
-
const address = server.address();
|
|
594
|
-
if (address === null || typeof address === "string") throw new Error(`${providerId} shim has no listening address`);
|
|
595
|
-
return `http://127.0.0.1:${address.port}`;
|
|
596
|
-
};
|
|
597
|
-
async function handle(req, res) {
|
|
598
|
-
try {
|
|
599
|
-
if (!hostIsLoopback(req.headers.host)) {
|
|
600
|
-
writeOpenAIError(res, 403, "host_not_allowed", "Host header must name the loopback interface");
|
|
601
|
-
return;
|
|
602
|
-
}
|
|
603
|
-
if (!originIsLoopback(req.headers.origin)) {
|
|
604
|
-
writeOpenAIError(res, 403, "origin_not_allowed", "Origin must be a loopback origin");
|
|
605
|
-
return;
|
|
606
|
-
}
|
|
607
|
-
if (!bearerOk(req)) {
|
|
608
|
-
writeOpenAIError(res, 401, "unauthorized", "missing or invalid Authorization bearer");
|
|
609
|
-
return;
|
|
610
|
-
}
|
|
611
|
-
const url = req.url ?? "/";
|
|
612
|
-
if (req.method === "GET" && (url === "/healthz" || url === "/healthz/")) {
|
|
613
|
-
writeJson(res, 200, { ok: true });
|
|
614
|
-
return;
|
|
615
|
-
}
|
|
616
|
-
if (req.method === "GET" && (url === "/v1/models" || url === "/v1/models/")) {
|
|
617
|
-
writeJson(res, 200, {
|
|
618
|
-
object: "list",
|
|
619
|
-
data: catalog.current().map((model) => ({
|
|
620
|
-
id: model.id,
|
|
621
|
-
object: "model",
|
|
622
|
-
created: 0,
|
|
623
|
-
owned_by: providerId
|
|
624
|
-
}))
|
|
625
|
-
});
|
|
626
|
-
return;
|
|
627
|
-
}
|
|
628
|
-
if (req.method === "POST" && (url === "/v1/chat/completions" || url === "/v1/chat/completions/")) {
|
|
629
|
-
await chatCompletions(req, res);
|
|
630
|
-
return;
|
|
631
|
-
}
|
|
632
|
-
writeOpenAIError(res, 404, "not_found", `no such route: ${req.method} ${url}`);
|
|
633
|
-
} catch (error) {
|
|
634
|
-
if (!res.headersSent) writeOpenAIError(res, 500, "internal", String(error));
|
|
635
|
-
else res.end();
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
async function chatCompletions(req, res) {
|
|
639
|
-
if (!isJsonContentType(req)) {
|
|
640
|
-
writeOpenAIError(res, 415, "unsupported_media_type", "Content-Type must be application/json");
|
|
641
|
-
return;
|
|
642
|
-
}
|
|
643
|
-
try {
|
|
644
|
-
await store.resolve();
|
|
645
|
-
} catch (error) {
|
|
646
|
-
writeOpenAIError(res, KIND_STATUS.missing_credential, "not_signed_in", String(error));
|
|
647
|
-
return;
|
|
648
|
-
}
|
|
649
|
-
const raw = (await readBody$1(req)).toString("utf8");
|
|
650
|
-
const controller = new AbortController();
|
|
651
|
-
req.on("close", () => controller.abort());
|
|
652
|
-
const result = await client.chatStream(raw, controller.signal);
|
|
653
|
-
if (!result.ok) {
|
|
654
|
-
writeOpenAIError(res, KIND_STATUS[result.kind], result.kind, `${providerId} upstream ${result.kind} (http ${KIND_STATUS[result.kind]}): ${result.message.slice(0, 400)}`);
|
|
655
|
-
return;
|
|
656
|
-
}
|
|
657
|
-
res.writeHead(200, {
|
|
658
|
-
"Content-Type": "text/event-stream",
|
|
659
|
-
"Cache-Control": "no-cache",
|
|
660
|
-
"Connection": "keep-alive",
|
|
661
|
-
"X-Accel-Buffering": "no"
|
|
662
|
-
});
|
|
663
|
-
let sawDone = false;
|
|
664
|
-
const body = Readable.fromWeb(result.response.body);
|
|
665
|
-
body.on("data", (chunk) => {
|
|
666
|
-
if (chunk.includes("[DONE]")) sawDone = true;
|
|
667
|
-
});
|
|
668
|
-
body.on("error", (error) => {
|
|
669
|
-
logger?.warn("dsh-qoder-connect: upstream stream failed mid-flight", error);
|
|
670
|
-
if (!sawDone && res.writable) res.end("data: [DONE]\n\n");
|
|
671
|
-
});
|
|
672
|
-
body.pipe(res);
|
|
673
|
-
}
|
|
674
|
-
return {
|
|
675
|
-
ready,
|
|
676
|
-
baseUrl,
|
|
677
|
-
token: () => SHARED_SECRET,
|
|
678
|
-
close: () => new Promise((resolve, reject) => {
|
|
679
|
-
server.close(() => resolve());
|
|
680
|
-
server.closeAllConnections();
|
|
681
|
-
server.once("error", reject);
|
|
682
|
-
})
|
|
683
|
-
};
|
|
684
|
-
}
|
|
685
|
-
//#endregion
|
|
686
|
-
//#region src/probe-store.ts
|
|
687
|
-
/**
|
|
688
|
-
* Local record of reasoning-effort probes.
|
|
689
|
-
*
|
|
690
|
-
* What this stores is an *observation*, never a claim about the upstream: a
|
|
691
|
-
* model's row is only consulted when the catalog carries no explicit
|
|
692
|
-
* `supportedEfforts` set, and it always loses to a declared set. The plan this
|
|
693
|
-
* implements (`docs/reasoning-effort-probe-plan.md` §5) requires that a result
|
|
694
|
-
* is invalidated whenever the model's catalog row changes, so every record
|
|
695
|
-
* carries a fingerprint of the fields the probe depended on.
|
|
696
|
-
*
|
|
697
|
-
* The file lives in the plugin's own state directory, never beside the
|
|
698
|
-
* product's files, and carries no token, prompt, or response body — only
|
|
699
|
-
* model ids, effort spellings, and timestamps.
|
|
700
|
-
*
|
|
701
|
-
* @module dsh-qoder-connect/probe-store
|
|
702
|
-
*/
|
|
703
|
-
/** Basename of the China variant's probe record inside the plugin's state dir. */
|
|
704
|
-
const QODER_PROBE_FILENAME = ".qoder-probe.json";
|
|
705
|
-
/** On-disk format this reader accepts; other versions are discarded. */
|
|
706
|
-
const PROBE_FORMAT_VERSION = 1;
|
|
707
|
-
/**
|
|
708
|
-
* How long an observation stays usable. Conservative on purpose: the plan's
|
|
709
|
-
* whole argument is that upstream metadata moves fast, so a result that has
|
|
710
|
-
* outlived its fingerprint's usefulness should not quietly keep granting a
|
|
711
|
-
* picker entry.
|
|
712
|
-
*/
|
|
713
|
-
const DEFAULT_TTL_MS = 12096e5;
|
|
714
|
-
/**
|
|
715
|
-
* Plugin-owned probe record path inside the plugin's state directory.
|
|
716
|
-
*
|
|
717
|
-
* One file per variant. The two regions serve overlapping model ids with
|
|
718
|
-
* different entitlements, and {@link fingerprintModel} covers only
|
|
719
|
-
* `id`/`reasoning`/`supportsImages` — never the provider — so a single shared
|
|
720
|
-
* file would let one variant's observation answer for the other. The paths
|
|
721
|
-
* differ; the format does not.
|
|
722
|
-
*/
|
|
723
|
-
function qoderProbePath(filename = QODER_PROBE_FILENAME) {
|
|
724
|
-
return join(qoderStateDir(), filename);
|
|
725
|
-
}
|
|
726
|
-
/**
|
|
727
|
-
* Fingerprint the catalog fields a probe depends on.
|
|
728
|
-
*
|
|
729
|
-
* Deliberately excludes display-only fields (`name`, `billing`, `contextWindow`)
|
|
730
|
-
* so a rename or a rate relabel does not throw away a valid observation, and
|
|
731
|
-
* deliberately includes the whole reasoning object so any change to the
|
|
732
|
-
* declared shape re-probes.
|
|
733
|
-
*/
|
|
734
|
-
function fingerprintModel(info) {
|
|
735
|
-
const basis = JSON.stringify({
|
|
736
|
-
id: info.id,
|
|
737
|
-
reasoning: info.reasoning ?? null,
|
|
738
|
-
supportsImages: info.supportsImages ?? null
|
|
739
|
-
});
|
|
740
|
-
return createHash("sha256").update(basis).digest("hex").slice(0, 16);
|
|
741
|
-
}
|
|
742
|
-
/** Read-and-validate the documents on disk; anything malformed reads as empty. */
|
|
743
|
-
function readDocument(path) {
|
|
744
|
-
if (!existsSync(path)) return void 0;
|
|
745
|
-
let parsed;
|
|
746
|
-
try {
|
|
747
|
-
parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
748
|
-
} catch {
|
|
749
|
-
return;
|
|
750
|
-
}
|
|
751
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return void 0;
|
|
752
|
-
const wrapped = parsed;
|
|
753
|
-
if (wrapped["version"] !== PROBE_FORMAT_VERSION) return void 0;
|
|
754
|
-
const records = wrapped["records"];
|
|
755
|
-
if (typeof records !== "object" || records === null || Array.isArray(records)) return void 0;
|
|
756
|
-
return parsed;
|
|
757
|
-
}
|
|
758
|
-
/** One record's shape check; a bad row is dropped rather than trusted. */
|
|
759
|
-
function isRecord(value) {
|
|
760
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
761
|
-
const wrapped = value;
|
|
762
|
-
const validation = wrapped["validation"];
|
|
763
|
-
if (validation !== "validating" && validation !== "non-validating" && validation !== "unknown") return false;
|
|
764
|
-
if (typeof wrapped["fingerprint"] !== "string") return false;
|
|
765
|
-
if (typeof wrapped["probedAtMs"] !== "number" || !Number.isFinite(wrapped["probedAtMs"])) return false;
|
|
766
|
-
if (typeof wrapped["pluginVersion"] !== "string") return false;
|
|
767
|
-
const efforts = wrapped["efforts"];
|
|
768
|
-
if (!Array.isArray(efforts) || efforts.some((effort) => typeof effort !== "string")) return false;
|
|
769
|
-
return true;
|
|
770
|
-
}
|
|
771
|
-
/**
|
|
772
|
-
* The plugin's probe records: read once, written atomically, never trusted
|
|
773
|
-
* across a fingerprint change or past the TTL.
|
|
774
|
-
*/
|
|
775
|
-
var QoderProbeStore = class {
|
|
776
|
-
path;
|
|
777
|
-
ttlMs;
|
|
778
|
-
pluginVersion;
|
|
779
|
-
now;
|
|
780
|
-
records;
|
|
781
|
-
constructor(options) {
|
|
782
|
-
const opts = typeof options === "string" ? {
|
|
783
|
-
path: options,
|
|
784
|
-
pluginVersion: "0.0.0"
|
|
785
|
-
} : options;
|
|
786
|
-
this.path = opts.path ?? qoderProbePath();
|
|
787
|
-
this.ttlMs = opts.ttlMs ?? DEFAULT_TTL_MS;
|
|
788
|
-
this.pluginVersion = opts.pluginVersion;
|
|
789
|
-
this.now = opts.now ?? (() => Date.now());
|
|
790
|
-
}
|
|
791
|
-
/** Resolved state-file path, for the CLI and tests. */
|
|
792
|
-
filePath() {
|
|
793
|
-
return this.path;
|
|
794
|
-
}
|
|
795
|
-
load() {
|
|
796
|
-
if (this.records === void 0) {
|
|
797
|
-
const document = readDocument(this.path);
|
|
798
|
-
const records = {};
|
|
799
|
-
for (const [id, record] of Object.entries(document?.records ?? {})) if (isRecord(record)) records[id] = record;
|
|
800
|
-
this.records = records;
|
|
801
|
-
}
|
|
802
|
-
return this.records;
|
|
803
|
-
}
|
|
804
|
-
/**
|
|
805
|
-
* The usable record for a model, or `undefined` when there is none, it is
|
|
806
|
-
* expired, it was taken against a different catalog row, or it belongs to a
|
|
807
|
-
* different account.
|
|
808
|
-
*
|
|
809
|
-
* @param account - the account in effect, as `uid:enterpriseId`. Records are
|
|
810
|
-
* only returned for the account that produced them.
|
|
811
|
-
*/
|
|
812
|
-
get(modelId, fingerprint, account) {
|
|
813
|
-
const record = this.load()[modelId];
|
|
814
|
-
if (record === void 0) return void 0;
|
|
815
|
-
if (record.fingerprint !== fingerprint) return void 0;
|
|
816
|
-
if (record.account !== account) return void 0;
|
|
817
|
-
if (this.now() - record.probedAtMs > this.ttlMs) return void 0;
|
|
818
|
-
return record;
|
|
819
|
-
}
|
|
820
|
-
/**
|
|
821
|
-
* Store one observation. Only a decisive answer (`validating` /
|
|
822
|
-
* `non-validating`) replaces an existing decisive record: a transient
|
|
823
|
-
* `unknown` must not erase knowledge the user already paid for.
|
|
824
|
-
*/
|
|
825
|
-
set(modelId, record) {
|
|
826
|
-
const records = this.load();
|
|
827
|
-
const existing = records[modelId];
|
|
828
|
-
if (record.validation === "unknown" && existing !== void 0 && existing.fingerprint === record.fingerprint && existing.validation !== "unknown") return;
|
|
829
|
-
records[modelId] = record;
|
|
830
|
-
this.persist();
|
|
831
|
-
}
|
|
832
|
-
/** Drop every record; used by the card's explicit "clear" action. */
|
|
833
|
-
clear() {
|
|
834
|
-
this.records = {};
|
|
835
|
-
this.persist();
|
|
836
|
-
}
|
|
837
|
-
/** Every record currently held, for status display. */
|
|
838
|
-
all() {
|
|
839
|
-
return { ...this.load() };
|
|
840
|
-
}
|
|
841
|
-
/** Build a record stamped with this store's clock, version, and account. */
|
|
842
|
-
record(fingerprint, validation, efforts, account) {
|
|
843
|
-
return {
|
|
844
|
-
fingerprint,
|
|
845
|
-
validation,
|
|
846
|
-
efforts: validation === "validating" ? [...efforts] : [],
|
|
847
|
-
probedAtMs: this.now(),
|
|
848
|
-
pluginVersion: this.pluginVersion,
|
|
849
|
-
account
|
|
850
|
-
};
|
|
851
|
-
}
|
|
852
|
-
/**
|
|
853
|
-
* Write through a temporary file and rename, so a crash mid-write cannot
|
|
854
|
-
* leave a half-parsed document that reads as "no records" and silently drops
|
|
855
|
-
* every observation.
|
|
856
|
-
*/
|
|
857
|
-
persist() {
|
|
858
|
-
const directory = dirname(this.path);
|
|
859
|
-
try {
|
|
860
|
-
if (!existsSync(directory)) mkdirSync(directory, { recursive: true });
|
|
861
|
-
const document = {
|
|
862
|
-
version: PROBE_FORMAT_VERSION,
|
|
863
|
-
records: this.load()
|
|
864
|
-
};
|
|
865
|
-
const temporary = resolve(`${this.path}.tmp`);
|
|
866
|
-
writeFileSync(temporary, `${JSON.stringify(document, null, 2)}\n`, { mode: 384 });
|
|
867
|
-
renameSync(temporary, this.path);
|
|
868
|
-
} catch {}
|
|
869
|
-
}
|
|
870
|
-
};
|
|
871
|
-
/**
|
|
872
|
-
* Order observations newest-first for display.
|
|
873
|
-
*
|
|
874
|
-
* The store keeps insertion order so the file reads chronologically, but the
|
|
875
|
-
* card wants the most recent detection at the top: a sweep the user just ran
|
|
876
|
-
* should not appear below every earlier one, which is what appending to an
|
|
877
|
-
* insertion-ordered list does.
|
|
878
|
-
*/
|
|
879
|
-
function newestFirst(records) {
|
|
880
|
-
return [...records].sort((a, b) => b.probedAt - a.probedAt);
|
|
881
|
-
}
|
|
882
|
-
//#endregion
|
|
883
817
|
//#region src/probe-service.ts
|
|
884
818
|
/**
|
|
885
819
|
* Serial probe runner. One instance is shared by the manual API and any
|
|
@@ -1070,6 +1004,8 @@ async function qoderWebStatus(deps) {
|
|
|
1070
1004
|
status: "signed-in",
|
|
1071
1005
|
...authStatus.region === void 0 ? {} : { region: authStatus.region },
|
|
1072
1006
|
...authStatus.pat === void 0 ? {} : { pat: authStatus.pat },
|
|
1007
|
+
...authStatus.profiles === void 0 ? {} : { patProfiles: authStatus.profiles },
|
|
1008
|
+
...authStatus.activeId === void 0 ? {} : { activePatId: authStatus.activeId },
|
|
1073
1009
|
...deps.authKey === void 0 ? {} : { authKey: deps.authKey }
|
|
1074
1010
|
};
|
|
1075
1011
|
const modelsField = deps.models().map(modelSnapshot);
|
|
@@ -2687,10 +2623,7 @@ function apply(ctx, config) {
|
|
|
2687
2623
|
runtime.catalogGeneration += 1;
|
|
2688
2624
|
runtime.inflightFetch?.controller.abort();
|
|
2689
2625
|
runtime.inflightFetch = void 0;
|
|
2690
|
-
if (hadCredential && known !== identity)
|
|
2691
|
-
runtime.probeStore.clear();
|
|
2692
|
-
runtime.invalidate();
|
|
2693
|
-
}
|
|
2626
|
+
if (hadCredential && known !== identity) runtime.invalidate();
|
|
2694
2627
|
if (identity === void 0) {
|
|
2695
2628
|
if (known !== void 0) runtime.savedCatalogs.delete(known);
|
|
2696
2629
|
runtime.catalog.set(runtime.fallback);
|
|
@@ -2765,13 +2698,13 @@ function apply(ctx, config) {
|
|
|
2765
2698
|
});
|
|
2766
2699
|
registerQoderAuthRoute(webCtx, {
|
|
2767
2700
|
path: runtime.variant.authPath,
|
|
2768
|
-
save: async (pat) => {
|
|
2701
|
+
save: async (pat, label) => {
|
|
2769
2702
|
if (!await validateApiKey(pat, runtime.variant.region)) return {
|
|
2770
2703
|
ok: false,
|
|
2771
2704
|
error: "qoder_invalid_pat"
|
|
2772
2705
|
};
|
|
2773
|
-
const
|
|
2774
|
-
const identity = credentialIdentity(
|
|
2706
|
+
const entry = await runtime.store.add(pat, label);
|
|
2707
|
+
const identity = credentialIdentity(entry);
|
|
2775
2708
|
adoptIdentity(runtime, identity);
|
|
2776
2709
|
await fetchCatalog(runtime, identity);
|
|
2777
2710
|
return {
|
|
@@ -2779,7 +2712,45 @@ function apply(ctx, config) {
|
|
|
2779
2712
|
status: await runtime.store.status()
|
|
2780
2713
|
};
|
|
2781
2714
|
},
|
|
2715
|
+
activate: async (id) => {
|
|
2716
|
+
const entry = await runtime.store.activate(id);
|
|
2717
|
+
if (entry === void 0) return {
|
|
2718
|
+
ok: false,
|
|
2719
|
+
error: "qoder_unknown_pat"
|
|
2720
|
+
};
|
|
2721
|
+
const identity = credentialIdentity(entry);
|
|
2722
|
+
adoptIdentity(runtime, identity);
|
|
2723
|
+
detach(ctx, fetchCatalog(runtime, identity), `${runtime.variant.id} catalog refresh after switch`);
|
|
2724
|
+
return {
|
|
2725
|
+
ok: true,
|
|
2726
|
+
status: await runtime.store.status()
|
|
2727
|
+
};
|
|
2728
|
+
},
|
|
2729
|
+
remove: async (id) => {
|
|
2730
|
+
const removed = await runtime.store.remove(id);
|
|
2731
|
+
if (removed === void 0) return {
|
|
2732
|
+
ok: false,
|
|
2733
|
+
error: "qoder_unknown_pat"
|
|
2734
|
+
};
|
|
2735
|
+
const finished = credentialIdentity(removed);
|
|
2736
|
+
runtime.probeStore.clearFor(finished);
|
|
2737
|
+
runtime.savedCatalogs.delete(finished);
|
|
2738
|
+
const active = await runtime.store.current().catch(() => void 0);
|
|
2739
|
+
const identity = active === void 0 ? void 0 : credentialIdentity(active);
|
|
2740
|
+
adoptIdentity(runtime, identity);
|
|
2741
|
+
if (identity !== void 0) detach(ctx, fetchCatalog(runtime, identity), `${runtime.variant.id} catalog refresh after removal`);
|
|
2742
|
+
return {
|
|
2743
|
+
ok: true,
|
|
2744
|
+
status: await runtime.store.status()
|
|
2745
|
+
};
|
|
2746
|
+
},
|
|
2782
2747
|
clear: async () => {
|
|
2748
|
+
const saved = await runtime.store.entries().catch(() => []);
|
|
2749
|
+
for (const entry of saved) {
|
|
2750
|
+
const identity = credentialIdentity(entry);
|
|
2751
|
+
runtime.probeStore.clearFor(identity);
|
|
2752
|
+
runtime.savedCatalogs.delete(identity);
|
|
2753
|
+
}
|
|
2783
2754
|
await runtime.store.clear();
|
|
2784
2755
|
adoptIdentity(runtime, void 0);
|
|
2785
2756
|
}
|
|
@@ -3097,4 +3068,4 @@ function apply(ctx, config) {
|
|
|
3097
3068
|
}), "variant startup");
|
|
3098
3069
|
}
|
|
3099
3070
|
//#endregion
|
|
3100
|
-
export { CHINA_VARIANT, CN_SECTION_KEYS, Config, FALLBACK_QODER_MODELS, GLOBAL_SECTION_KEYS, GLOBAL_VARIANT, KIND_STATUS, PROBE_EFFORT_CANDIDATES, 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, QUOTA_SECTION_KEYS, QoderCatalog, QoderCatalogStore, QoderCredentialStore, QoderProbeService, QoderProbeStore, QoderUpstreamClient, 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 };
|
|
3071
|
+
export { CHINA_VARIANT, CN_SECTION_KEYS, Config, FALLBACK_QODER_MODELS, GLOBAL_SECTION_KEYS, GLOBAL_VARIANT, KIND_STATUS, PROBE_EFFORT_CANDIDATES, 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, QUOTA_SECTION_KEYS, QoderCatalog, QoderCatalogStore, QoderCredentialStore, QoderProbeService, QoderProbeStore, QoderUpstreamClient, apply, classifyUpstreamError, clearHostHeartbeat, createAuthKey, createQoderAdapter, createQoderShim, fingerprintModel, inject, isHeartbeatProcessAlive, kindFromQoderFailure, modelInfoOf, name, normalizeCredits, patTail, probeModel, processStartTimeMs, qoderAuthHandler, qoderCatalogPath, qoderCredentialId, qoderCredentialIdentity, qoderHostHeartbeatPath, qoderOwnAuthPath, qoderPluginDataDir, qoderProbePath, randomSentinel, readHostHeartbeat, registerQoderAuthRoute, variantFor };
|