dsh-qoder-connect 0.1.1 → 0.1.4
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.md +148 -148
- package/lib/bin.js +1 -1
- package/lib/client.js +1154 -1215
- package/lib/index.js +92 -19
- package/lib/{variants-CQg-9veJ.js → variants-CSeJqJpR.js} +98 -18
- package/package.json +2 -2
package/lib/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as QODER_GLOBAL_AUTH_PATH, B as qoderCredentialIdentity, C as probeModel, D as FALLBACK_QODER_MODELS, E as qoderCatalogPath, F as QODER_AUTH_FILENAME, G as qoderPluginDataDir, H as QODER_DATA_DIR_ENV, I as QODER_PAT_ENV_CN, K as qoderStateDir, L as QODER_PAT_ENV_GLOBAL, M as QODER_GLOBAL_STATUS_PATH, N as QODER_PROBE_PATH, O as QoderCatalog, P as QODER_STATUS_PATH, R as QoderCredentialStore, S as PROBE_EFFORT_CANDIDATES, T as QoderCatalogStore, U as QODER_DATA_DIR_NAME, V as qoderOwnAuthPath, W as qoderMachineIdPath, _ as modelInfoOf, a as QODER_HOST_HEARTBEAT_FILENAME, b as createQoderTransport, c as processStartTimeMs, d as writeHostHeartbeat, f as QODER_CONNECT_VERSION, g as kindFromQoderFailure, h as classifyUpstreamError, i as variantFor, j as QODER_GLOBAL_PROBE_PATH, k as QODER_AUTH_PATH, l as qoderHostHeartbeatPath, m as QoderUpstreamClient, n as GLOBAL_VARIANT, o as clearHostHeartbeat, p as KIND_STATUS, r as QODER_VARIANTS, s as isHeartbeatProcessAlive, t as CHINA_VARIANT, u as readHostHeartbeat, v as normalizeCredits, w as randomSentinel, x as getMachineId, y as validateApiKey, z as patTail } from "./variants-
|
|
1
|
+
import { A as QODER_GLOBAL_AUTH_PATH, B as qoderCredentialIdentity, C as probeModel, D as FALLBACK_QODER_MODELS, E as qoderCatalogPath, F as QODER_AUTH_FILENAME, G as qoderPluginDataDir, H as QODER_DATA_DIR_ENV, I as QODER_PAT_ENV_CN, K as qoderStateDir, L as QODER_PAT_ENV_GLOBAL, M as QODER_GLOBAL_STATUS_PATH, N as QODER_PROBE_PATH, O as QoderCatalog, P as QODER_STATUS_PATH, R as QoderCredentialStore, S as PROBE_EFFORT_CANDIDATES, T as QoderCatalogStore, U as QODER_DATA_DIR_NAME, V as qoderOwnAuthPath, W as qoderMachineIdPath, _ as modelInfoOf, a as QODER_HOST_HEARTBEAT_FILENAME, b as createQoderTransport, c as processStartTimeMs, d as writeHostHeartbeat, f as QODER_CONNECT_VERSION, g as kindFromQoderFailure, h as classifyUpstreamError, i as variantFor, j as QODER_GLOBAL_PROBE_PATH, k as QODER_AUTH_PATH, l as qoderHostHeartbeatPath, m as QoderUpstreamClient, n as GLOBAL_VARIANT, o as clearHostHeartbeat, p as KIND_STATUS, r as QODER_VARIANTS, s as isHeartbeatProcessAlive, t as CHINA_VARIANT, u as readHostHeartbeat, v as normalizeCredits, w as randomSentinel, x as getMachineId, y as validateApiKey, z as patTail } from "./variants-CSeJqJpR.js";
|
|
2
2
|
import z from "@deepseek-ai/schemastery";
|
|
3
3
|
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
|
|
4
4
|
import { dirname, join, resolve } from "node:path";
|
|
@@ -539,7 +539,13 @@ function createQoderShim(options) {
|
|
|
539
539
|
return timingSafeEqual(a, b);
|
|
540
540
|
}
|
|
541
541
|
const server = createServer((req, res) => {
|
|
542
|
-
handle(req, res)
|
|
542
|
+
handle(req, res).catch((error) => {
|
|
543
|
+
logger?.warn("dsh-qoder-connect: loopback request failed", error);
|
|
544
|
+
try {
|
|
545
|
+
if (!res.headersSent) writeOpenAIError(res, 500, "internal", "loopback request failed");
|
|
546
|
+
else res.end();
|
|
547
|
+
} catch {}
|
|
548
|
+
});
|
|
543
549
|
});
|
|
544
550
|
const ready = new Promise((resolve, reject) => {
|
|
545
551
|
server.once("listening", () => resolve());
|
|
@@ -1284,16 +1290,42 @@ function emitJobTokenHint(at, text) {
|
|
|
1284
1290
|
const agent = lastRunningAgent;
|
|
1285
1291
|
if (agent?.session === void 0) return;
|
|
1286
1292
|
lastRefreshAt.set(String(agent.id), at);
|
|
1293
|
+
appendHintRow(agent, "success", text);
|
|
1294
|
+
}
|
|
1295
|
+
/**
|
|
1296
|
+
* Print the failed-heal row into the conversation whose request triggered it.
|
|
1297
|
+
*
|
|
1298
|
+
* The mirror of {@link emitJobTokenHint}: the self-heal ran, exchanged a fresh
|
|
1299
|
+
* job token, retried, and the upstream still refused. Without this row that
|
|
1300
|
+
* outcome was silent — the user saw only the resulting authorization failure
|
|
1301
|
+
* and could not tell that recovery had already been attempted and lost.
|
|
1302
|
+
*
|
|
1303
|
+
* Rate limited by the transport (once per unresolved outage), not here: this
|
|
1304
|
+
* module stays a pure printer so the dedupe policy lives with the state that
|
|
1305
|
+
* knows when an outage ends.
|
|
1306
|
+
*
|
|
1307
|
+
* @param at - When the failed heal was recorded.
|
|
1308
|
+
* @param text - The row's summary line.
|
|
1309
|
+
*/
|
|
1310
|
+
function emitJobTokenRefreshFailedHint(at, text) {
|
|
1311
|
+
const agent = lastRunningAgent;
|
|
1312
|
+
if (agent?.session === void 0) return;
|
|
1313
|
+
appendHintRow(agent, "error", text);
|
|
1314
|
+
}
|
|
1315
|
+
/** Append one log-only command pair carrying `text` as its outcome. */
|
|
1316
|
+
function appendHintRow(agent, kind, text) {
|
|
1317
|
+
const session = agent.session;
|
|
1318
|
+
if (session === void 0) return;
|
|
1287
1319
|
const commandId = `cmd-qoder-hint-${Date.now().toString(36)}-${++hintSeq}`;
|
|
1288
1320
|
try {
|
|
1289
|
-
|
|
1321
|
+
session.append("command/run", {
|
|
1290
1322
|
commandId,
|
|
1291
1323
|
name: JOB_TOKEN_HINT_NAME,
|
|
1292
1324
|
source: { kind: "user" }
|
|
1293
1325
|
});
|
|
1294
|
-
|
|
1326
|
+
session.append("command/done", {
|
|
1295
1327
|
commandId,
|
|
1296
|
-
kind
|
|
1328
|
+
kind,
|
|
1297
1329
|
text
|
|
1298
1330
|
});
|
|
1299
1331
|
} catch {}
|
|
@@ -1469,6 +1501,17 @@ function fallbackFor(_variant) {
|
|
|
1469
1501
|
function jobTokenHintText(at) {
|
|
1470
1502
|
return `jobToken 已自动刷新(${new Date(at).toLocaleTimeString("zh-CN", { hour12: false })})— 旧令牌被上游拒绝,已自动重换并恢复`;
|
|
1471
1503
|
}
|
|
1504
|
+
/**
|
|
1505
|
+
* The failed-heal row's summary line.
|
|
1506
|
+
*
|
|
1507
|
+
* Deliberately does not name a cause: the upstream rejection that survived a
|
|
1508
|
+
* fresh token is not necessarily an authorization problem at all, and the real
|
|
1509
|
+
* reason travels in the failure message itself (see the SSE envelope body).
|
|
1510
|
+
* Claiming "quota" or "revoked" here would be a guess presented as a finding.
|
|
1511
|
+
*/
|
|
1512
|
+
function jobTokenRefreshFailedHintText(at) {
|
|
1513
|
+
return `jobToken 已重换但仍被上游拒绝(${new Date(at).toLocaleTimeString("zh-CN", { hour12: false })})— 自愈未恢复,请查看上方错误详情`;
|
|
1514
|
+
}
|
|
1472
1515
|
/** Build one variant's stores, transport, and probe state. */
|
|
1473
1516
|
function createVariantRuntime(ctx, config, variant, current, identityOf) {
|
|
1474
1517
|
const store = new QoderCredentialStore({
|
|
@@ -1484,6 +1527,10 @@ function createVariantRuntime(ctx, config, variant, current, identityOf) {
|
|
|
1484
1527
|
jobTokenRefreshedAt = info.at;
|
|
1485
1528
|
ctx.logger.warn(`dsh-qoder-connect: ${variant.displayName} job token was auto-refreshed after an upstream rejection`);
|
|
1486
1529
|
emitJobTokenHint(info.at, jobTokenHintText(info.at));
|
|
1530
|
+
},
|
|
1531
|
+
onJobTokenRefreshFailed: (info) => {
|
|
1532
|
+
ctx.logger.warn(`dsh-qoder-connect: ${variant.displayName} job token refresh did not recover the chat (upstream status ${info.status ?? "unknown"})`);
|
|
1533
|
+
emitJobTokenRefreshFailedHint(info.at, jobTokenRefreshFailedHintText(info.at));
|
|
1487
1534
|
}
|
|
1488
1535
|
});
|
|
1489
1536
|
const client = new QoderUpstreamClient({
|
|
@@ -1582,6 +1629,21 @@ function probeSection(runtime, consent) {
|
|
|
1582
1629
|
};
|
|
1583
1630
|
}
|
|
1584
1631
|
/**
|
|
1632
|
+
* Run a detached promise without letting its failure kill the host.
|
|
1633
|
+
*
|
|
1634
|
+
* Node terminates the whole process on an unhandled rejection (exit code 1),
|
|
1635
|
+
* which the desktop shell reports as the backend having "exited unexpectedly"
|
|
1636
|
+
* — so a failing heartbeat write, loopback close, or background sweep in this
|
|
1637
|
+
* plugin would take the entire Harness down with it. Every fire-and-forget
|
|
1638
|
+
* call therefore carries a handler; a failure is a diagnostic, never a reason
|
|
1639
|
+
* for the host to die.
|
|
1640
|
+
*/
|
|
1641
|
+
function detach(ctx, work, what) {
|
|
1642
|
+
work.catch((error) => {
|
|
1643
|
+
ctx.logger.warn(`dsh-qoder-connect: ${what} failed`, error);
|
|
1644
|
+
});
|
|
1645
|
+
}
|
|
1646
|
+
/**
|
|
1585
1647
|
* Start one variant: its loopback endpoint, provider registration, and
|
|
1586
1648
|
* configuration-card wiring.
|
|
1587
1649
|
*
|
|
@@ -1641,18 +1703,18 @@ async function startVariant(ctx, runtime) {
|
|
|
1641
1703
|
ctx.effect(() => () => {
|
|
1642
1704
|
releaseAdapter?.();
|
|
1643
1705
|
releaseDirectory?.();
|
|
1644
|
-
shim.close();
|
|
1706
|
+
detach(ctx, shim.close(), "loopback endpoint close");
|
|
1645
1707
|
});
|
|
1646
1708
|
} catch {
|
|
1647
1709
|
releaseAdapter?.();
|
|
1648
1710
|
releaseDirectory?.();
|
|
1649
|
-
shim.close();
|
|
1711
|
+
detach(ctx, shim.close(), "loopback endpoint close");
|
|
1650
1712
|
}
|
|
1651
1713
|
runtime.registered = true;
|
|
1652
1714
|
return true;
|
|
1653
1715
|
} catch (error) {
|
|
1654
1716
|
ctx.logger.error(`dsh-qoder-connect: ${variant.displayName} provider registration failed`, error);
|
|
1655
|
-
shim.close();
|
|
1717
|
+
detach(ctx, shim.close(), "loopback endpoint close");
|
|
1656
1718
|
return false;
|
|
1657
1719
|
}
|
|
1658
1720
|
}
|
|
@@ -1906,7 +1968,7 @@ function apply(ctx, config) {
|
|
|
1906
1968
|
stopped = true;
|
|
1907
1969
|
for (const timer of timers) clearInterval(timer);
|
|
1908
1970
|
timers.length = 0;
|
|
1909
|
-
clearHostHeartbeat();
|
|
1971
|
+
detach(ctx, clearHostHeartbeat(), "host heartbeat cleanup");
|
|
1910
1972
|
});
|
|
1911
1973
|
/**
|
|
1912
1974
|
* Fetch one variant's catalog for the current credential.
|
|
@@ -1967,11 +2029,15 @@ function apply(ctx, config) {
|
|
|
1967
2029
|
runtime.catalogSource = "live";
|
|
1968
2030
|
runtime.catalogFetchedAtMs = runtime.client.lastCatalog?.fetchedAtMs ?? Date.now();
|
|
1969
2031
|
runtime.catalogError = void 0;
|
|
1970
|
-
if (lastIdentities.get(runtime.variant.id) === identity)
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
2032
|
+
if (lastIdentities.get(runtime.variant.id) === identity) try {
|
|
2033
|
+
runtime.savedCatalogs.set(identity, {
|
|
2034
|
+
source: runtime.client.lastCatalog?.source ?? "unknown",
|
|
2035
|
+
fetchedAtMs: runtime.client.lastCatalog?.fetchedAtMs ?? Date.now(),
|
|
2036
|
+
models: [...models]
|
|
2037
|
+
});
|
|
2038
|
+
} catch (error) {
|
|
2039
|
+
ctx.logger.warn(`dsh-qoder-connect: ${runtime.variant.displayName} catalog could not be saved for this account`, error);
|
|
2040
|
+
}
|
|
1975
2041
|
runtime.invalidate();
|
|
1976
2042
|
})().finally(() => {
|
|
1977
2043
|
if (runtime.inflightFetch?.promise === run) runtime.inflightFetch = void 0;
|
|
@@ -2022,16 +2088,23 @@ function apply(ctx, config) {
|
|
|
2022
2088
|
const syncAll = async () => {
|
|
2023
2089
|
for (const runtime of runtimes) await syncVariant(runtime);
|
|
2024
2090
|
};
|
|
2025
|
-
|
|
2091
|
+
/**
|
|
2092
|
+
* Start both variants, then begin the credential sweep.
|
|
2093
|
+
*
|
|
2094
|
+
* The chain carries its own failure handler: without one, a rejection here
|
|
2095
|
+
* would be an unhandled rejection — which Node turns into process
|
|
2096
|
+
* termination, taking the whole Harness down over one plugin's startup.
|
|
2097
|
+
*/
|
|
2098
|
+
detach(ctx, Promise.all(runtimes.map(async (runtime) => startVariant(ctx, runtime))).then(() => {
|
|
2026
2099
|
if (stopped) return;
|
|
2027
|
-
if (runtimes.some((runtime) => runtime.registered)) writeHostHeartbeat();
|
|
2028
|
-
syncAll();
|
|
2100
|
+
if (runtimes.some((runtime) => runtime.registered)) detach(ctx, writeHostHeartbeat(), "host heartbeat write");
|
|
2101
|
+
detach(ctx, syncAll(), "credential sweep");
|
|
2029
2102
|
const timer = setInterval(() => {
|
|
2030
|
-
syncAll();
|
|
2103
|
+
detach(ctx, syncAll(), "credential sweep");
|
|
2031
2104
|
}, credentialPollMs());
|
|
2032
2105
|
timer.unref?.();
|
|
2033
2106
|
timers.push(timer);
|
|
2034
|
-
});
|
|
2107
|
+
}), "variant startup");
|
|
2035
2108
|
}
|
|
2036
2109
|
//#endregion
|
|
2037
2110
|
export { CHINA_VARIANT, Config, FALLBACK_QODER_MODELS, 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, 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 };
|
|
@@ -2415,27 +2415,38 @@ var QoderUsageReader = class {
|
|
|
2415
2415
|
* Re-arranges standard Base64 chunks and translates characters against
|
|
2416
2416
|
* a custom alphabet to satisfy Qoder upstream WAF requirements.
|
|
2417
2417
|
*
|
|
2418
|
+
* The translation runs through a byte table applied to a Buffer instead of a
|
|
2419
|
+
* per-character string loop. The loop form (`out += char` plus
|
|
2420
|
+
* `alphabet.indexOf(char)`) costs ~800 ms on a 1 MB body and runs on the main
|
|
2421
|
+
* thread once per model request, which saturated the Harness event loop as
|
|
2422
|
+
* soon as a few large-context sessions were in flight. The table form is
|
|
2423
|
+
* byte-for-byte identical (the input is always Base64, i.e. ASCII) and about
|
|
2424
|
+
* 24x faster.
|
|
2425
|
+
*
|
|
2418
2426
|
* @module dsh-provider-qoder/qoder/transport/wire/encoding
|
|
2419
2427
|
*/
|
|
2420
2428
|
const qoderCustomAlphabet = "_doRTgHZBKcGVjlvpC,@aFSx#DPuNJme&i*MzLOEn)sUrthbf%Y^w.(kIQyXqWA!";
|
|
2421
2429
|
const qoderStdAlphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
2430
|
+
/**
|
|
2431
|
+
* Byte translation table: every byte maps to itself unless it is a Base64
|
|
2432
|
+
* alphabet character (→ the custom alphabet) or the `=` pad (→ `$`).
|
|
2433
|
+
*/
|
|
2434
|
+
const encodeTable = (() => {
|
|
2435
|
+
const table = /* @__PURE__ */ new Uint8Array(256);
|
|
2436
|
+
for (let index = 0; index < 256; index++) table[index] = index;
|
|
2437
|
+
for (let index = 0; index < 64; index++) table[qoderStdAlphabet.charCodeAt(index)] = qoderCustomAlphabet.charCodeAt(index);
|
|
2438
|
+
table["=".charCodeAt(0)] = "$".charCodeAt(0);
|
|
2439
|
+
return table;
|
|
2440
|
+
})();
|
|
2422
2441
|
function qoderEncodeBody(plaintext) {
|
|
2423
2442
|
const std = Buffer.isBuffer(plaintext) ? plaintext.toString("base64") : Buffer.from(plaintext).toString("base64");
|
|
2424
2443
|
const n = std.length;
|
|
2425
2444
|
const a = Math.floor(n / 3);
|
|
2426
2445
|
const rearranged = std.slice(n - a) + std.slice(a, n - a) + std.slice(0, a);
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
if (c === "=") out += "$";
|
|
2432
|
-
else {
|
|
2433
|
-
const idx = qoderStdAlphabet.indexOf(c);
|
|
2434
|
-
if (idx >= 0) out += qoderCustomAlphabet[idx];
|
|
2435
|
-
else out += c;
|
|
2436
|
-
}
|
|
2437
|
-
}
|
|
2438
|
-
return out;
|
|
2446
|
+
const source = Buffer.from(rearranged, "latin1");
|
|
2447
|
+
const target = Buffer.allocUnsafe(n);
|
|
2448
|
+
for (let index = 0; index < n; index++) target[index] = encodeTable[source[index] ?? 0] ?? 0;
|
|
2449
|
+
return target.toString("latin1");
|
|
2439
2450
|
}
|
|
2440
2451
|
//#endregion
|
|
2441
2452
|
//#region src/qoder/transport/wire/thinking.ts
|
|
@@ -2612,7 +2623,10 @@ function parseEnvelope(rawData) {
|
|
|
2612
2623
|
const envelope = value;
|
|
2613
2624
|
if (envelope.statusCodeValue !== void 0 && typeof envelope.statusCodeValue !== "number") throw malformed("Qoder SSE envelope has an invalid status code.");
|
|
2614
2625
|
if (envelope.statusCodeValue !== void 0 && (!Number.isInteger(envelope.statusCodeValue) || envelope.statusCodeValue < 100 || envelope.statusCodeValue > 599)) throw malformed("Qoder SSE envelope has an invalid status code.");
|
|
2615
|
-
if (envelope.statusCodeValue !== void 0 && envelope.statusCodeValue !== 200)
|
|
2626
|
+
if (envelope.statusCodeValue !== void 0 && envelope.statusCodeValue !== 200) {
|
|
2627
|
+
const status = envelope.statusCodeValue;
|
|
2628
|
+
throw qoderHttpError(envelope.body ? `Qoder service returned upstream error status ${status}: ${envelope.body}` : `Qoder service returned upstream error status ${status}.`, { status });
|
|
2629
|
+
}
|
|
2616
2630
|
if (envelope.body !== void 0 && typeof envelope.body !== "string") throw malformed("Qoder SSE envelope has an invalid model body.");
|
|
2617
2631
|
return envelope;
|
|
2618
2632
|
}
|
|
@@ -3019,6 +3033,18 @@ async function* streamQoderChat(options, model, credentials, messages, dependenc
|
|
|
3019
3033
|
requestController.abort("request complete");
|
|
3020
3034
|
}
|
|
3021
3035
|
}
|
|
3036
|
+
/**
|
|
3037
|
+
* How long a rotated-token notice may wait for an accepting chat.
|
|
3038
|
+
*
|
|
3039
|
+
* The heal's own retry can lose a race with a transient fault and be rescued
|
|
3040
|
+
* by a later attempt, so the notice is deferred until some chat is accepted.
|
|
3041
|
+
* That deferral has to be bounded: an unbounded one let a rotation from
|
|
3042
|
+
* minutes earlier surface as a fresh-looking row whose timestamp named a
|
|
3043
|
+
* moment the user could not connect to the row appearing now. Five minutes
|
|
3044
|
+
* comfortably covers the host's own retry cycle (its backoff caps at 10s)
|
|
3045
|
+
* while keeping the row adjacent to the event it describes.
|
|
3046
|
+
*/
|
|
3047
|
+
const jobTokenNoticeMaxAgeMs = 3e5;
|
|
3022
3048
|
function aborted(message) {
|
|
3023
3049
|
return new QoderLlmError(message, "ABORTED");
|
|
3024
3050
|
}
|
|
@@ -3037,6 +3063,7 @@ var DefaultQoderTransport = class {
|
|
|
3037
3063
|
imageUploader;
|
|
3038
3064
|
preserveThinking;
|
|
3039
3065
|
onJobTokenRefreshed;
|
|
3066
|
+
onJobTokenRefreshFailed;
|
|
3040
3067
|
/**
|
|
3041
3068
|
* Set when the self-heal exchanged a fresh job token, cleared when a chat
|
|
3042
3069
|
* afterwards succeeds. The notice reports "the token was rotated because the
|
|
@@ -3045,6 +3072,16 @@ var DefaultQoderTransport = class {
|
|
|
3045
3072
|
* transient upstream timeout and be rescued by a later attempt.
|
|
3046
3073
|
*/
|
|
3047
3074
|
pendingRefreshAt;
|
|
3075
|
+
/**
|
|
3076
|
+
* Whether the current unresolved heal failure has already been reported.
|
|
3077
|
+
*
|
|
3078
|
+
* One upstream rejection can outlive many host-level retries (an observed
|
|
3079
|
+
* storm ran 59 of them across 75 steps), and the transport re-heals inside
|
|
3080
|
+
* every one of them. Without this latch the failure notice would print once
|
|
3081
|
+
* per retry; with it, the user is told once per outage. Cleared as soon as
|
|
3082
|
+
* any chat is accepted, so the next outage announces itself again.
|
|
3083
|
+
*/
|
|
3084
|
+
refreshFailureAnnounced = false;
|
|
3048
3085
|
constructor(options) {
|
|
3049
3086
|
this.region = options.region;
|
|
3050
3087
|
this.resolvePat = options.resolvePat;
|
|
@@ -3056,6 +3093,7 @@ var DefaultQoderTransport = class {
|
|
|
3056
3093
|
this.attachments = options.attachments;
|
|
3057
3094
|
this.preserveThinking = options.preserveThinking;
|
|
3058
3095
|
this.onJobTokenRefreshed = options.onJobTokenRefreshed;
|
|
3096
|
+
this.onJobTokenRefreshFailed = options.onJobTokenRefreshFailed;
|
|
3059
3097
|
this.auth = new QoderAuthService({
|
|
3060
3098
|
fetch: this.fetchImpl,
|
|
3061
3099
|
logger: this.logger,
|
|
@@ -3096,12 +3134,35 @@ var DefaultQoderTransport = class {
|
|
|
3096
3134
|
if (this.pendingRefreshAt === void 0) return;
|
|
3097
3135
|
const at = this.pendingRefreshAt;
|
|
3098
3136
|
this.pendingRefreshAt = void 0;
|
|
3137
|
+
if (Date.now() - at > jobTokenNoticeMaxAgeMs) return;
|
|
3099
3138
|
this.logger?.warn?.("[Qoder Stream] Job token was auto-refreshed after an upstream rejection; the chat has recovered");
|
|
3100
3139
|
this.onJobTokenRefreshed?.({
|
|
3101
3140
|
region: this.region,
|
|
3102
3141
|
at
|
|
3103
3142
|
});
|
|
3104
3143
|
}
|
|
3144
|
+
/**
|
|
3145
|
+
* Drop a pending rotation notice whose heal did not rescue anything.
|
|
3146
|
+
*
|
|
3147
|
+
* The heal's own retry was rejected too, so "the rotation fixed it" is not
|
|
3148
|
+
* what happened. Leaving the notice pending made a LATER, unrelated chat
|
|
3149
|
+
* acceptance flush it — printing a success row that contradicts the failure
|
|
3150
|
+
* row already shown, stamped with the old rotation time.
|
|
3151
|
+
*/
|
|
3152
|
+
discardPendingRefreshNotice() {
|
|
3153
|
+
this.pendingRefreshAt = void 0;
|
|
3154
|
+
}
|
|
3155
|
+
/**
|
|
3156
|
+
* Note that the upstream accepted a chat, ending any unresolved heal failure.
|
|
3157
|
+
*
|
|
3158
|
+
* `streamChat` calls this the moment its first chunk arrives — the only point
|
|
3159
|
+
* at which "the credential was accepted" is actually known. A rejected chat
|
|
3160
|
+
* throws before that, so this never fires on a failing attempt.
|
|
3161
|
+
*/
|
|
3162
|
+
onChatAccepted() {
|
|
3163
|
+
this.refreshFailureAnnounced = false;
|
|
3164
|
+
this.flushPendingRefreshNotice();
|
|
3165
|
+
}
|
|
3105
3166
|
async discoverModels(signal) {
|
|
3106
3167
|
const pat = await this.requirePat(signal);
|
|
3107
3168
|
const key = opaqueCredentialKey(pat);
|
|
@@ -3167,7 +3228,7 @@ var DefaultQoderTransport = class {
|
|
|
3167
3228
|
const chatCredentials = await this.auth.getCredentials(pat, options.signal);
|
|
3168
3229
|
try {
|
|
3169
3230
|
yield* this.streamChat(options, model, chatCredentials, messages, () => {
|
|
3170
|
-
this.
|
|
3231
|
+
this.onChatAccepted();
|
|
3171
3232
|
});
|
|
3172
3233
|
return;
|
|
3173
3234
|
} catch (error) {
|
|
@@ -3180,7 +3241,7 @@ var DefaultQoderTransport = class {
|
|
|
3180
3241
|
this.pendingRefreshAt = Date.now();
|
|
3181
3242
|
try {
|
|
3182
3243
|
yield* this.streamChat(options, model, refreshed, messages, () => {
|
|
3183
|
-
this.
|
|
3244
|
+
this.onChatAccepted();
|
|
3184
3245
|
});
|
|
3185
3246
|
return;
|
|
3186
3247
|
} catch (error) {
|
|
@@ -3201,6 +3262,16 @@ var DefaultQoderTransport = class {
|
|
|
3201
3262
|
}
|
|
3202
3263
|
}
|
|
3203
3264
|
}
|
|
3265
|
+
this.discardPendingRefreshNotice();
|
|
3266
|
+
if (!this.refreshFailureAnnounced) {
|
|
3267
|
+
this.refreshFailureAnnounced = true;
|
|
3268
|
+
this.logger?.warn?.("[Qoder Stream] Job token refresh did not recover the chat; the upstream still rejects it", { status: lastRejection instanceof LlmError ? lastRejection.failure.status : void 0 });
|
|
3269
|
+
this.onJobTokenRefreshFailed?.({
|
|
3270
|
+
region: this.region,
|
|
3271
|
+
at: Date.now(),
|
|
3272
|
+
...lastRejection instanceof LlmError && lastRejection.failure.status !== void 0 ? { status: lastRejection.failure.status } : {}
|
|
3273
|
+
});
|
|
3274
|
+
}
|
|
3204
3275
|
throw lastRejection;
|
|
3205
3276
|
}
|
|
3206
3277
|
};
|
|
@@ -3607,14 +3678,23 @@ var ChunkEncoder = class {
|
|
|
3607
3678
|
}] })];
|
|
3608
3679
|
}
|
|
3609
3680
|
case "usage": {
|
|
3610
|
-
const
|
|
3681
|
+
const cacheRead = chunk.usage.cacheReadTokens;
|
|
3682
|
+
const cacheWrite = chunk.usage.cacheWriteTokens;
|
|
3683
|
+
const prompt = chunk.usage.inputTokens + (cacheRead ?? 0) + (cacheWrite ?? 0);
|
|
3611
3684
|
const completion = chunk.usage.outputTokens;
|
|
3685
|
+
const reasoning = chunk.usage.reasoningTokens;
|
|
3686
|
+
const details = cacheRead === void 0 && cacheWrite === void 0 ? void 0 : {
|
|
3687
|
+
...cacheRead === void 0 ? {} : { cached_tokens: cacheRead },
|
|
3688
|
+
...cacheWrite === void 0 ? {} : { cache_write_tokens: cacheWrite }
|
|
3689
|
+
};
|
|
3612
3690
|
return [this.frame({
|
|
3613
3691
|
choices: [],
|
|
3614
3692
|
usage: {
|
|
3615
3693
|
prompt_tokens: prompt,
|
|
3616
3694
|
completion_tokens: completion,
|
|
3617
|
-
total_tokens: chunk.usage.totalTokens ?? prompt + completion
|
|
3695
|
+
total_tokens: chunk.usage.totalTokens ?? prompt + completion,
|
|
3696
|
+
...details === void 0 ? {} : { prompt_tokens_details: details },
|
|
3697
|
+
...reasoning === void 0 ? {} : { completion_tokens_details: { reasoning_tokens: reasoning } }
|
|
3618
3698
|
}
|
|
3619
3699
|
})];
|
|
3620
3700
|
}
|
|
@@ -4002,7 +4082,7 @@ const DEFAULT_MAX_TOKENS = 32768;
|
|
|
4002
4082
|
const DEFAULT_CONTEXT_WINDOW = 18e4;
|
|
4003
4083
|
//#endregion
|
|
4004
4084
|
//#region src/version.ts
|
|
4005
|
-
const QODER_CONNECT_VERSION = "0.1.
|
|
4085
|
+
const QODER_CONNECT_VERSION = "0.1.4";
|
|
4006
4086
|
//#endregion
|
|
4007
4087
|
//#region src/host-heartbeat.ts
|
|
4008
4088
|
/**
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "dsh-qoder-connect",
|
|
3
3
|
"displayName": "DSH Qoder Connect",
|
|
4
4
|
"description": "将 Qoder(国内版 / 国际版)的模型接入 DeepSeek Harness — bring Qoder (CN & international) models into DeepSeek Harness.",
|
|
5
|
-
"version": "0.1.
|
|
5
|
+
"version": "0.1.4",
|
|
6
6
|
"author": "corrinehu",
|
|
7
7
|
"keywords": [
|
|
8
8
|
"dsh-plugin",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"scripts": {
|
|
24
24
|
"prepack": "npm run build",
|
|
25
25
|
"build": "tsdown",
|
|
26
|
-
"test": "vitest run",
|
|
26
|
+
"test": "node -r ./tests/sandbox-shim.cjs ./node_modules/vitest/vitest.mjs run",
|
|
27
27
|
"test:qoder": "node --test --test-isolation=none --experimental-strip-types tests/qoder/*.test.ts",
|
|
28
28
|
"typecheck": "tsc -p tsconfig.json && tsc -p tsconfig.client.json",
|
|
29
29
|
"check": "pnpm run typecheck && pnpm run test && pnpm run test:qoder && pnpm run build"
|