dsh-qoder-connect 0.1.9 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/icon.svg +6 -0
- package/lib/bin.js +1 -1
- package/lib/index.d.ts +6 -0
- package/lib/index.js +10 -3
- package/lib/{variants-C8ezztjV.js → variants-DuEgaUad.js} +173 -60
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +123 -120
package/assets/icon.svg
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<rect x="1" y="1" width="34" height="34" rx="8" fill="url(#g)" />
|
|
3
|
+
<path d="M19.6 7.5 11 20h5.6l-1.4 8.5L24.5 16h-5.6l.7-8.5Z" fill="#FFFFFF"/>
|
|
4
|
+
<circle cx="25.5" cy="25.5" r="3.2" fill="#45D9E7"/>
|
|
5
|
+
<defs><linearGradient id="g" x1="18" y1="1" x2="18" y2="35" gradientUnits="userSpaceOnUse"><stop stop-color="#7CB7FF"/><stop offset="1" stop-color="#145AF3"/></linearGradient></defs>
|
|
6
|
+
</svg>
|
package/lib/bin.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { D as FALLBACK_QODER_MODELS, E as qoderCatalogPath, T as QoderCatalogStore, V as qoderCredentialIdentity, b as createQoderTransport, f as QODER_CONNECT_VERSION, i as variantFor, l as qoderHostHeartbeatPath, m as QoderUpstreamClient, r as QODER_VARIANTS, s as isHeartbeatProcessAlive, t as CHINA_VARIANT, u as readHostHeartbeat, y as validateApiKey, z as QoderCredentialStore } from "./variants-
|
|
2
|
+
import { D as FALLBACK_QODER_MODELS, E as qoderCatalogPath, T as QoderCatalogStore, V as qoderCredentialIdentity, b as createQoderTransport, f as QODER_CONNECT_VERSION, i as variantFor, l as qoderHostHeartbeatPath, m as QoderUpstreamClient, r as QODER_VARIANTS, s as isHeartbeatProcessAlive, t as CHINA_VARIANT, u as readHostHeartbeat, y as validateApiKey, z as QoderCredentialStore } from "./variants-DuEgaUad.js";
|
|
3
3
|
import { readFile } from "node:fs/promises";
|
|
4
4
|
import { realpathSync } from "node:fs";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
package/lib/index.d.ts
CHANGED
|
@@ -834,6 +834,12 @@ declare function classifyUpstreamError(status: number, body: string): UpstreamEr
|
|
|
834
834
|
* The transport's taxonomy (`src/qoder/errors.ts`) codes an HTTP 402 as
|
|
835
835
|
* `INVALID_REQUEST`, so the status gets the last say for the quota and auth
|
|
836
836
|
* families before the code-based defaults apply.
|
|
837
|
+
*
|
|
838
|
+
* `RATE_LIMIT` is tested before the auth family because the upstream's queue
|
|
839
|
+
* answer keeps its 401/403 status: a saturated qfmodel queue reads
|
|
840
|
+
* `code: RATE_LIMIT, status: 403` (body carries 10605/isQueued/
|
|
841
|
+
* retryAfterSeconds), and letting the status arm claim it reported a dead
|
|
842
|
+
* credential — which the host never retries — instead of a throttle it would.
|
|
837
843
|
*/
|
|
838
844
|
declare function kindFromQoderFailure(failure: {
|
|
839
845
|
code: string;
|
package/lib/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as QODER_GLOBAL_AUTH_PATH, B as patTail, C as probeModel, D as FALLBACK_QODER_MODELS, E as qoderCatalogPath, F as QODER_STATUS_PATH, G as qoderMachineIdPath, H as qoderOwnAuthPath, I as QODER_AUTH_FILENAME, K as qoderPluginDataDir, L as QODER_PAT_ENV_CN, M as QODER_GLOBAL_STATUS_PATH, N as QODER_PROBE_PATH, O as QoderCatalog, P as QODER_SETTINGS_FACE_PATH, R as QODER_PAT_ENV_GLOBAL, S as PROBE_EFFORT_CANDIDATES, T as QoderCatalogStore, U as QODER_DATA_DIR_ENV, V as qoderCredentialIdentity, W as QODER_DATA_DIR_NAME, _ 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, q as qoderStateDir, 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 QoderCredentialStore } from "./variants-
|
|
1
|
+
import { A as QODER_GLOBAL_AUTH_PATH, B as patTail, C as probeModel, D as FALLBACK_QODER_MODELS, E as qoderCatalogPath, F as QODER_STATUS_PATH, G as qoderMachineIdPath, H as qoderOwnAuthPath, I as QODER_AUTH_FILENAME, K as qoderPluginDataDir, L as QODER_PAT_ENV_CN, M as QODER_GLOBAL_STATUS_PATH, N as QODER_PROBE_PATH, O as QoderCatalog, P as QODER_SETTINGS_FACE_PATH, R as QODER_PAT_ENV_GLOBAL, S as PROBE_EFFORT_CANDIDATES, T as QoderCatalogStore, U as QODER_DATA_DIR_ENV, V as qoderCredentialIdentity, W as QODER_DATA_DIR_NAME, _ 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, q as qoderStateDir, 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 QoderCredentialStore } from "./variants-DuEgaUad.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";
|
|
@@ -628,7 +628,7 @@ function createQoderShim(options) {
|
|
|
628
628
|
req.on("close", () => controller.abort());
|
|
629
629
|
const result = await client.chatStream(raw, controller.signal);
|
|
630
630
|
if (!result.ok) {
|
|
631
|
-
writeOpenAIError(res, KIND_STATUS[result.kind], result.kind, `${providerId} upstream ${result.kind} (http ${result.
|
|
631
|
+
writeOpenAIError(res, KIND_STATUS[result.kind], result.kind, `${providerId} upstream ${result.kind} (http ${KIND_STATUS[result.kind]}): ${result.message.slice(0, 400)}`);
|
|
632
632
|
return;
|
|
633
633
|
}
|
|
634
634
|
res.writeHead(200, {
|
|
@@ -2113,9 +2113,16 @@ function jobTokenHintText(at) {
|
|
|
2113
2113
|
* fresh token is not necessarily an authorization problem at all, and the real
|
|
2114
2114
|
* reason travels in the failure message itself (see the SSE envelope body).
|
|
2115
2115
|
* Claiming "quota" or "revoked" here would be a guess presented as a finding.
|
|
2116
|
+
*
|
|
2117
|
+
* It also must not point the reader at "the error above": the failed heal does
|
|
2118
|
+
* not always belong to a chat whose failure renders a card in this
|
|
2119
|
+
* conversation. A heal inside the session-title request — same transport,
|
|
2120
|
+
* same shared notice state — fails silently there, so the row used to send
|
|
2121
|
+
* users looking for an error card that does not exist. The row states only
|
|
2122
|
+
* what is known: the retry ran, it did not recover, the request failed.
|
|
2116
2123
|
*/
|
|
2117
2124
|
function jobTokenRefreshFailedHintText(at) {
|
|
2118
|
-
return `jobToken 已重换但仍被上游拒绝(${new Date(at).toLocaleTimeString("zh-CN", { hour12: false })})—
|
|
2125
|
+
return `jobToken 已重换但仍被上游拒绝(${new Date(at).toLocaleTimeString("zh-CN", { hour12: false })})— 自愈未恢复,该请求已失败`;
|
|
2119
2126
|
}
|
|
2120
2127
|
/** Build one variant's stores, transport, and probe state. */
|
|
2121
2128
|
function createVariantRuntime(ctx, config, variant, current, identityOf) {
|
|
@@ -853,6 +853,125 @@ async function probeModel(options) {
|
|
|
853
853
|
};
|
|
854
854
|
}
|
|
855
855
|
//#endregion
|
|
856
|
+
//#region src/qoder/errors.ts
|
|
857
|
+
/**
|
|
858
|
+
* DSH-compatible LLM error representation.
|
|
859
|
+
*
|
|
860
|
+
* @module dsh-provider-qoder/qoder/errors
|
|
861
|
+
*/
|
|
862
|
+
var QoderLlmError = class extends LlmError {
|
|
863
|
+
constructor(message, code = "UNKNOWN_ERROR", options) {
|
|
864
|
+
super(message, code, options);
|
|
865
|
+
}
|
|
866
|
+
};
|
|
867
|
+
/** The queue/limit markers Qoder puts in a rejection body (observed 2026-09-26). */
|
|
868
|
+
const QUEUE_BODY_MARKERS = [
|
|
869
|
+
/"code"\s*:\s*"?10605"?/u,
|
|
870
|
+
/"isQueued"\s*:\s*true/u,
|
|
871
|
+
/"queueCount"/u,
|
|
872
|
+
/"serviceAvailable"\s*:\s*false/u
|
|
873
|
+
];
|
|
874
|
+
/**
|
|
875
|
+
* Whether an upstream rejection body is Qoder's **queue/limit** answer rather
|
|
876
|
+
* than an authorization failure.
|
|
877
|
+
*
|
|
878
|
+
* Qoder answers a saturated per-model queue (and a region whose service is not
|
|
879
|
+
* available) with an HTTP 401/403 — or a 200 SSE envelope carrying
|
|
880
|
+
* `statusCodeValue: 403` — whose body says `code: 10605`, `isQueued: true`,
|
|
881
|
+
* `queueCount`, `retryAfterSeconds` and sometimes `serviceAvailable: false`.
|
|
882
|
+
* Observed verbatim on 2026-09-26:
|
|
883
|
+
*
|
|
884
|
+
* {"code":"10605","message":"{\"isQueued\":true,\"modelKey\":\"qfmodel\",
|
|
885
|
+
* \"queueCount\":8887,\"queueType\":\"p3\",\"retryAfterSeconds\":30,
|
|
886
|
+
* \"serviceAvailable\":true,\"waitTime\":274}"}
|
|
887
|
+
*
|
|
888
|
+
* Reading that status as AUTH made the self-heal exchange fresh job tokens
|
|
889
|
+
* against a queue no token can jump, and — because the host retries
|
|
890
|
+
* `RATE_LIMIT` but deliberately not `AUTH` — it also suppressed the automatic
|
|
891
|
+
* retry that would have sailed through the 30-second window. The body is the
|
|
892
|
+
* only place the distinction is visible, so it gets the last say over status.
|
|
893
|
+
*/
|
|
894
|
+
function qoderQueueSignal(body) {
|
|
895
|
+
if (body === void 0 || body === "") return void 0;
|
|
896
|
+
const text = body.replace(/\\/gu, "");
|
|
897
|
+
if (!QUEUE_BODY_MARKERS.some((marker) => marker.test(text))) return void 0;
|
|
898
|
+
const match = /"retryAfterSeconds"\s*:\s*(\d+)/u.exec(text);
|
|
899
|
+
return match === null ? {} : { retryAfterMs: Number(match[1]) * 1e3 };
|
|
900
|
+
}
|
|
901
|
+
/**
|
|
902
|
+
* Replace status digits that read as authorization failure with a neutral
|
|
903
|
+
* word, for messages the upstream used to announce a queue.
|
|
904
|
+
*
|
|
905
|
+
* `dsh-llm-pi-ai`'s error classifier is a text matcher whose first test is
|
|
906
|
+
* `/\b(?:401|403)\b/` — ahead of its 429/rate-limit test — so any message
|
|
907
|
+
* carrying those digits classifies AUTH no matter the structured code
|
|
908
|
+
* travelling beside it. A queue answer quoting the upstream's 403 (in the
|
|
909
|
+
* message prefix, the envelope status, or the body's `"code":"403"`) must not
|
|
910
|
+
* reach that matcher as-is, or the user sees "API 密钥无效" for a throttle
|
|
911
|
+
* (observed verbatim on 2026-09-26 23:30 and again at 23:40).
|
|
912
|
+
*/
|
|
913
|
+
function deauthorizeDigits(message) {
|
|
914
|
+
return message.replace(/\b(?:401|403)\b/gu, "throttled");
|
|
915
|
+
}
|
|
916
|
+
function qoderHttpError(message, response, body) {
|
|
917
|
+
const { status } = response;
|
|
918
|
+
const headerRetryAfterMs = retryAfterMs(response.headers?.get("retry-after") ?? null);
|
|
919
|
+
const requestId = qoderRequestId(response.headers);
|
|
920
|
+
const queue = status === 401 || status === 403 || status === 429 ? qoderQueueSignal(body ?? message) : void 0;
|
|
921
|
+
if (queue !== void 0) {
|
|
922
|
+
const retryAfterMs = queue.retryAfterMs ?? headerRetryAfterMs;
|
|
923
|
+
return new QoderLlmError(deauthorizeDigits(message), "RATE_LIMIT", {
|
|
924
|
+
status,
|
|
925
|
+
...retryAfterMs === void 0 ? {} : { providerRetryAfterMs: retryAfterMs },
|
|
926
|
+
...requestId === void 0 ? {} : { requestId }
|
|
927
|
+
});
|
|
928
|
+
}
|
|
929
|
+
return new QoderLlmError(message, status === 401 || status === 403 ? "AUTH" : status === 408 ? "TIMEOUT" : status === 429 ? "RATE_LIMIT" : status >= 500 && status <= 599 ? "SERVER" : status >= 400 && status <= 499 ? "INVALID_REQUEST" : "PROVIDER_ERROR", {
|
|
930
|
+
status,
|
|
931
|
+
...headerRetryAfterMs === void 0 ? {} : { providerRetryAfterMs: headerRetryAfterMs },
|
|
932
|
+
...requestId === void 0 ? {} : { requestId }
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
function qoderRequestId(headers) {
|
|
936
|
+
const normalized = (headers?.get("x-request-id") ?? headers?.get("request-id") ?? headers?.get("x-amzn-requestid"))?.trim();
|
|
937
|
+
return normalized ? ProviderRequestId(normalized) : void 0;
|
|
938
|
+
}
|
|
939
|
+
/**
|
|
940
|
+
* Whether this failure is an upstream authorization rejection worth one
|
|
941
|
+
* re-auth retry.
|
|
942
|
+
*
|
|
943
|
+
* The job token the transport signs requests with is cached in memory; an
|
|
944
|
+
* upstream that invalidates it mid-lifetime (gateway rotation or a fault
|
|
945
|
+
* window) answers HTTP 401/403 before any payload is produced. That state is
|
|
946
|
+
* distinguishable from a genuinely revoked PAT only by trying a fresh
|
|
947
|
+
* exchange, so callers clear their credential cache and retry once before
|
|
948
|
+
* reporting `AUTH` to the user.
|
|
949
|
+
*
|
|
950
|
+
* A `RATE_LIMIT` code is never a re-auth candidate, **even when its status is
|
|
951
|
+
* 401/403**: that is the upstream's queue answer (code 10605 / isQueued /
|
|
952
|
+
* retryAfterSeconds in the body), which keeps the status but says the
|
|
953
|
+
* credential was not the problem. Healing it exchanged job tokens against a
|
|
954
|
+
* queue no token can jump (observed 2026-09-26) and, by consuming the retry,
|
|
955
|
+
* suppressed the rate-limit retry that would have ridden out the window.
|
|
956
|
+
*/
|
|
957
|
+
function isQoderAuthRejection(error) {
|
|
958
|
+
if (!(error instanceof LlmError)) return false;
|
|
959
|
+
if (error.code === "RATE_LIMIT") return false;
|
|
960
|
+
return error.code === "AUTH" || error.failure.status === 401 || error.failure.status === 403;
|
|
961
|
+
}
|
|
962
|
+
function retryAfterMs(value, nowMs = Date.now()) {
|
|
963
|
+
const normalized = value?.trim();
|
|
964
|
+
if (!normalized) return void 0;
|
|
965
|
+
if (/^\d+$/u.test(normalized)) {
|
|
966
|
+
const delayMs = Number(normalized) * 1e3;
|
|
967
|
+
return Number.isFinite(delayMs) && delayMs > 0 ? delayMs : void 0;
|
|
968
|
+
}
|
|
969
|
+
const retryAt = Date.parse(normalized);
|
|
970
|
+
if (Number.isNaN(retryAt)) return void 0;
|
|
971
|
+
const delayMs = retryAt - nowMs;
|
|
972
|
+
return delayMs > 0 ? delayMs : void 0;
|
|
973
|
+
}
|
|
974
|
+
//#endregion
|
|
856
975
|
//#region src/qoder/transport/endpoints.ts
|
|
857
976
|
const qoderRegionEndpoints = {
|
|
858
977
|
global: {
|
|
@@ -915,59 +1034,6 @@ function getQoderClaimCampaignUrl(region = "global", campaignId) {
|
|
|
915
1034
|
return `${openApiUrl}/sash/api/v1/me/campaigns/${encodeURIComponent(campaignId)}/claim`;
|
|
916
1035
|
}
|
|
917
1036
|
//#endregion
|
|
918
|
-
//#region src/qoder/errors.ts
|
|
919
|
-
/**
|
|
920
|
-
* DSH-compatible LLM error representation.
|
|
921
|
-
*
|
|
922
|
-
* @module dsh-provider-qoder/qoder/errors
|
|
923
|
-
*/
|
|
924
|
-
var QoderLlmError = class extends LlmError {
|
|
925
|
-
constructor(message, code = "UNKNOWN_ERROR", options) {
|
|
926
|
-
super(message, code, options);
|
|
927
|
-
}
|
|
928
|
-
};
|
|
929
|
-
function qoderHttpError(message, response) {
|
|
930
|
-
const { status } = response;
|
|
931
|
-
const code = status === 401 || status === 403 ? "AUTH" : status === 408 ? "TIMEOUT" : status === 429 ? "RATE_LIMIT" : status >= 500 && status <= 599 ? "SERVER" : status >= 400 && status <= 499 ? "INVALID_REQUEST" : "PROVIDER_ERROR";
|
|
932
|
-
const providerRetryAfterMs = retryAfterMs(response.headers?.get("retry-after") ?? null);
|
|
933
|
-
const requestId = qoderRequestId(response.headers);
|
|
934
|
-
return new QoderLlmError(message, code, {
|
|
935
|
-
status,
|
|
936
|
-
...providerRetryAfterMs === void 0 ? {} : { providerRetryAfterMs },
|
|
937
|
-
...requestId === void 0 ? {} : { requestId }
|
|
938
|
-
});
|
|
939
|
-
}
|
|
940
|
-
function qoderRequestId(headers) {
|
|
941
|
-
const normalized = (headers?.get("x-request-id") ?? headers?.get("request-id") ?? headers?.get("x-amzn-requestid"))?.trim();
|
|
942
|
-
return normalized ? ProviderRequestId(normalized) : void 0;
|
|
943
|
-
}
|
|
944
|
-
/**
|
|
945
|
-
* Whether this failure is an upstream authorization rejection worth one
|
|
946
|
-
* re-auth retry.
|
|
947
|
-
*
|
|
948
|
-
* The job token the transport signs requests with is cached in memory; an
|
|
949
|
-
* upstream that invalidates it mid-lifetime (gateway rotation or a fault
|
|
950
|
-
* window) answers HTTP 401/403 before any payload is produced. That state is
|
|
951
|
-
* distinguishable from a genuinely revoked PAT only by trying a fresh
|
|
952
|
-
* exchange, so callers clear their credential cache and retry once before
|
|
953
|
-
* reporting `AUTH` to the user.
|
|
954
|
-
*/
|
|
955
|
-
function isQoderAuthRejection(error) {
|
|
956
|
-
return error instanceof LlmError && (error.code === "AUTH" || error.failure.status === 401 || error.failure.status === 403);
|
|
957
|
-
}
|
|
958
|
-
function retryAfterMs(value, nowMs = Date.now()) {
|
|
959
|
-
const normalized = value?.trim();
|
|
960
|
-
if (!normalized) return void 0;
|
|
961
|
-
if (/^\d+$/u.test(normalized)) {
|
|
962
|
-
const delayMs = Number(normalized) * 1e3;
|
|
963
|
-
return Number.isFinite(delayMs) && delayMs > 0 ? delayMs : void 0;
|
|
964
|
-
}
|
|
965
|
-
const retryAt = Date.parse(normalized);
|
|
966
|
-
if (Number.isNaN(retryAt)) return void 0;
|
|
967
|
-
const delayMs = retryAt - nowMs;
|
|
968
|
-
return delayMs > 0 ? delayMs : void 0;
|
|
969
|
-
}
|
|
970
|
-
//#endregion
|
|
971
1037
|
//#region src/qoder/transport/machine-id.ts
|
|
972
1038
|
function defaultMachineIdPaths() {
|
|
973
1039
|
return [join(homedir(), ".qoder", ".auth", "machine_id"), join(homedir(), ".dsh", "qoder", "machine_id")];
|
|
@@ -1294,7 +1360,7 @@ async function openApiJsonRequest(fetchImpl, options) {
|
|
|
1294
1360
|
const text = await readLimitedText(response, response.ok ? defaultMaxJsonBytes : defaultMaxErrorBytes, `Qoder ${options.operation} response`);
|
|
1295
1361
|
if (!response.ok) {
|
|
1296
1362
|
options.logger?.error?.(`[Qoder ${options.operation}] Request failed`, redactLogPayload(text));
|
|
1297
|
-
throw qoderHttpError(`Failed to execute Qoder ${options.operation} with status ${response.status}.`, response);
|
|
1363
|
+
throw qoderHttpError(`Failed to execute Qoder ${options.operation} with status ${response.status}.`, response, text);
|
|
1298
1364
|
}
|
|
1299
1365
|
let data;
|
|
1300
1366
|
try {
|
|
@@ -1332,6 +1398,14 @@ async function waitForFlight(promise, signal) {
|
|
|
1332
1398
|
var QoderAuthService = class {
|
|
1333
1399
|
cache = /* @__PURE__ */ new Map();
|
|
1334
1400
|
inFlight = /* @__PURE__ */ new Map();
|
|
1401
|
+
/**
|
|
1402
|
+
* Self-heal exchanges in flight, keyed like the cache. Deliberately separate
|
|
1403
|
+
* from {@link inFlight}: that flight may be aborted by a departing
|
|
1404
|
+
* concurrent waiter (a quota poll, a catalog sweep) whose abort has nothing
|
|
1405
|
+
* to do with the heal, so healers must not share its controller. Concurrent
|
|
1406
|
+
* heals instead share this flight — see {@link exchangeFresh}.
|
|
1407
|
+
*/
|
|
1408
|
+
healFlights = /* @__PURE__ */ new Map();
|
|
1335
1409
|
fetchImpl;
|
|
1336
1410
|
timeoutMs;
|
|
1337
1411
|
resolveMachineId;
|
|
@@ -1354,10 +1428,31 @@ var QoderAuthService = class {
|
|
|
1354
1428
|
* (the quota poll, the catalog sweep), and a retry that joins it would then
|
|
1355
1429
|
* be cancelled by a path that has nothing to do with the chat. The result
|
|
1356
1430
|
* replaces the cache entry.
|
|
1431
|
+
*
|
|
1432
|
+
* Two heals for the same credential that overlap share one exchange instead
|
|
1433
|
+
* of racing: each racer's `clear` deleted what the previous one had just
|
|
1434
|
+
* cached, and its own `cache.set` overwrote the other's — so the cache could
|
|
1435
|
+
* end up naming a token the losing request was no longer using, and with an
|
|
1436
|
+
* upstream that retires the previous job token on a new exchange the racers
|
|
1437
|
+
* also invalidated each other's credentials (one chat recovering while a
|
|
1438
|
+
* concurrent one failed, exactly the 2026-09-26 22:36 observation). The
|
|
1439
|
+
* shared exchange runs on its own timeout signal, so one healer's abort —
|
|
1440
|
+
* its chat request went away — cannot cancel an exchange other healers, and
|
|
1441
|
+
* the future `getCredentials` callers about to read the refreshed cache, are
|
|
1442
|
+
* waiting on.
|
|
1357
1443
|
*/
|
|
1358
1444
|
async exchangeFresh(pat, signal) {
|
|
1445
|
+
const cacheKey = `${this.region}:${opaqueCredentialKey(pat)}`;
|
|
1446
|
+
const existing = this.healFlights.get(cacheKey);
|
|
1447
|
+
if (existing !== void 0 && !existing.settled) return waitForFlight(existing.promise, signal);
|
|
1359
1448
|
this.clear(pat);
|
|
1360
|
-
|
|
1449
|
+
const created = { settled: false };
|
|
1450
|
+
created.promise = this.exchangeAndResolve(pat, AbortSignal.timeout(this.timeoutMs)).finally(() => {
|
|
1451
|
+
created.settled = true;
|
|
1452
|
+
if (this.healFlights.get(cacheKey) === created) this.healFlights.delete(cacheKey);
|
|
1453
|
+
});
|
|
1454
|
+
this.healFlights.set(cacheKey, created);
|
|
1455
|
+
return waitForFlight(created.promise, signal);
|
|
1361
1456
|
}
|
|
1362
1457
|
async getCredentials(pat, signal) {
|
|
1363
1458
|
if (!pat || typeof pat !== "string") throw new QoderLlmError("Qoder Personal Access Token is missing or invalid. Configure Qoder in the Qoder settings page.", "MISSING_CREDENTIAL");
|
|
@@ -2793,7 +2888,7 @@ function parseEnvelope(rawData) {
|
|
|
2793
2888
|
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.");
|
|
2794
2889
|
if (envelope.statusCodeValue !== void 0 && envelope.statusCodeValue !== 200) {
|
|
2795
2890
|
const status = envelope.statusCodeValue;
|
|
2796
|
-
throw qoderHttpError(envelope.body ? `Qoder service returned upstream error status ${status}: ${envelope.body}` : `Qoder service returned upstream error status ${status}.`, { status });
|
|
2891
|
+
throw qoderHttpError(envelope.body ? `Qoder service returned upstream error status ${status}: ${envelope.body}` : `Qoder service returned upstream error status ${status}.`, { status }, envelope.body);
|
|
2797
2892
|
}
|
|
2798
2893
|
if (envelope.body !== void 0 && typeof envelope.body !== "string") throw malformed("Qoder SSE envelope has an invalid model body.");
|
|
2799
2894
|
return envelope;
|
|
@@ -3128,7 +3223,15 @@ async function* streamQoderChat(options, model, credentials, messages, dependenc
|
|
|
3128
3223
|
...reqId === void 0 ? {} : { requestId: reqId }
|
|
3129
3224
|
});
|
|
3130
3225
|
resetIdleTimer();
|
|
3131
|
-
if (!response.ok)
|
|
3226
|
+
if (!response.ok) {
|
|
3227
|
+
let errorBody;
|
|
3228
|
+
try {
|
|
3229
|
+
errorBody = await readLimitedText(response, defaultMaxErrorBytes, "Qoder chat error response");
|
|
3230
|
+
} catch {
|
|
3231
|
+
errorBody = void 0;
|
|
3232
|
+
}
|
|
3233
|
+
throw qoderHttpError(`Qoder upstream service returned HTTP ${response.status}.`, response, errorBody);
|
|
3234
|
+
}
|
|
3132
3235
|
if (!response.body) throw new QoderLlmError("Qoder response contains no readable body stream.", "EMPTY_RESPONSE");
|
|
3133
3236
|
let firstChunkDurationMs;
|
|
3134
3237
|
let tokenUsage;
|
|
@@ -3416,9 +3519,11 @@ var DefaultQoderTransport = class {
|
|
|
3416
3519
|
this.logger?.warn?.("[Qoder Stream] Chat rejected as unauthorized; exchanging a fresh job token and retrying once", { status: error.failure.status });
|
|
3417
3520
|
}
|
|
3418
3521
|
let lastRejection;
|
|
3522
|
+
let noticeAt;
|
|
3419
3523
|
for (let round = 0; round < 2; round++) {
|
|
3420
3524
|
const refreshed = await this.auth.exchangeFresh(pat, options.signal);
|
|
3421
3525
|
this.pendingRefreshAt = Date.now();
|
|
3526
|
+
noticeAt = this.pendingRefreshAt;
|
|
3422
3527
|
try {
|
|
3423
3528
|
yield* this.streamChat(options, model, refreshed, messages, () => {
|
|
3424
3529
|
this.onChatAccepted();
|
|
@@ -3442,7 +3547,7 @@ var DefaultQoderTransport = class {
|
|
|
3442
3547
|
}
|
|
3443
3548
|
}
|
|
3444
3549
|
}
|
|
3445
|
-
this.discardPendingRefreshNotice();
|
|
3550
|
+
if (this.pendingRefreshAt === noticeAt) this.discardPendingRefreshNotice();
|
|
3446
3551
|
if (!this.refreshFailureAnnounced) {
|
|
3447
3552
|
this.refreshFailureAnnounced = true;
|
|
3448
3553
|
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 });
|
|
@@ -3518,6 +3623,7 @@ function normalizeCredits(credits) {
|
|
|
3518
3623
|
*/
|
|
3519
3624
|
function classifyUpstreamError(status, body) {
|
|
3520
3625
|
const lower = body.toLowerCase();
|
|
3626
|
+
if (qoderQueueSignal(body) !== void 0) return "soft_rate";
|
|
3521
3627
|
if (status === 402) return "quota_exceeded";
|
|
3522
3628
|
if (status === 429) return "soft_rate";
|
|
3523
3629
|
if (status === 401 || status === 403) return "auth";
|
|
@@ -3551,11 +3657,18 @@ function classifyUpstreamError(status, body) {
|
|
|
3551
3657
|
* The transport's taxonomy (`src/qoder/errors.ts`) codes an HTTP 402 as
|
|
3552
3658
|
* `INVALID_REQUEST`, so the status gets the last say for the quota and auth
|
|
3553
3659
|
* families before the code-based defaults apply.
|
|
3660
|
+
*
|
|
3661
|
+
* `RATE_LIMIT` is tested before the auth family because the upstream's queue
|
|
3662
|
+
* answer keeps its 401/403 status: a saturated qfmodel queue reads
|
|
3663
|
+
* `code: RATE_LIMIT, status: 403` (body carries 10605/isQueued/
|
|
3664
|
+
* retryAfterSeconds), and letting the status arm claim it reported a dead
|
|
3665
|
+
* credential — which the host never retries — instead of a throttle it would.
|
|
3554
3666
|
*/
|
|
3555
3667
|
function kindFromQoderFailure(failure) {
|
|
3556
3668
|
if (failure.code === "MISSING_CREDENTIAL") return "missing_credential";
|
|
3669
|
+
if (failure.code === "RATE_LIMIT") return "soft_rate";
|
|
3557
3670
|
if (failure.code === "AUTH" || failure.status === 401 || failure.status === 403) return "auth";
|
|
3558
|
-
if (failure.
|
|
3671
|
+
if (failure.status === 429) return "soft_rate";
|
|
3559
3672
|
if (failure.code === "QUOTA" || failure.status === 402) return "quota_exceeded";
|
|
3560
3673
|
if (failure.code.startsWith("INVALID_") || failure.code.startsWith("UNSUPPORTED_") || failure.code === "ATTACHMENT") return "client";
|
|
3561
3674
|
return "server";
|
|
@@ -4275,7 +4388,7 @@ const DEFAULT_MAX_TOKENS = 32768;
|
|
|
4275
4388
|
const DEFAULT_CONTEXT_WINDOW = 18e4;
|
|
4276
4389
|
//#endregion
|
|
4277
4390
|
//#region src/version.ts
|
|
4278
|
-
const QODER_CONNECT_VERSION = "0.
|
|
4391
|
+
const QODER_CONNECT_VERSION = "0.2.0";
|
|
4279
4392
|
//#endregion
|
|
4280
4393
|
//#region src/host-heartbeat.ts
|
|
4281
4394
|
/**
|
package/locale/en.json
ADDED
package/locale/zh.json
ADDED
package/package.json
CHANGED
|
@@ -1,120 +1,123 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "dsh-qoder-connect",
|
|
3
|
-
"displayName": "DSH Qoder Connect",
|
|
4
|
-
"description": "将 Qoder(国内版 / 国际版)的模型接入 DeepSeek Harness — bring Qoder (CN & international) models into DeepSeek Harness.",
|
|
5
|
-
"version": "0.
|
|
6
|
-
"author": "corrinehu",
|
|
7
|
-
"keywords": [
|
|
8
|
-
"dsh-plugin",
|
|
9
|
-
"deepseek-harness",
|
|
10
|
-
"deepseek",
|
|
11
|
-
"qoder",
|
|
12
|
-
"model-provider"
|
|
13
|
-
],
|
|
14
|
-
"type": "module",
|
|
15
|
-
"engines": {
|
|
16
|
-
"dsh": ">=0.1.5-rc.2",
|
|
17
|
-
"node": "^22.19.0 || >=24.0.0"
|
|
18
|
-
},
|
|
19
|
-
"main": "lib/index.js",
|
|
20
|
-
"types": "lib/index.d.ts",
|
|
21
|
-
"bin": {
|
|
22
|
-
"dsh-qoder-connect": "lib/bin.js"
|
|
23
|
-
},
|
|
24
|
-
"scripts": {
|
|
25
|
-
"prepack": "npm run build",
|
|
26
|
-
"build": "tsdown",
|
|
27
|
-
"test": "node -r ./tests/sandbox-shim.cjs ./node_modules/vitest/vitest.mjs run",
|
|
28
|
-
"test:qoder": "node --test --test-isolation=none --experimental-strip-types tests/qoder/*.test.ts",
|
|
29
|
-
"typecheck": "tsc -p tsconfig.json && tsc -p tsconfig.client.json",
|
|
30
|
-
"check": "pnpm run typecheck && pnpm run test && pnpm run test:qoder && pnpm run build"
|
|
31
|
-
},
|
|
32
|
-
"exports": {
|
|
33
|
-
".": {
|
|
34
|
-
"types": "./lib/index.d.ts",
|
|
35
|
-
"default": "./lib/index.js"
|
|
36
|
-
},
|
|
37
|
-
"./client": "./lib/client.js",
|
|
38
|
-
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
39
|
-
"./package.json": "./package.json"
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
"README.
|
|
47
|
-
"
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
"
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
"@deepseek-ai/dsh-
|
|
65
|
-
"@deepseek-ai/dsh-
|
|
66
|
-
"@deepseek-ai/dsh-client-ui-
|
|
67
|
-
"@deepseek-ai/dsh-client-ui-
|
|
68
|
-
"@deepseek-ai/dsh-client-ui-
|
|
69
|
-
"@deepseek-ai/dsh-client-ui-
|
|
70
|
-
"@deepseek-ai/dsh-client-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
"@deepseek-ai/
|
|
79
|
-
"@deepseek-ai/dsh-
|
|
80
|
-
"@deepseek-ai/dsh-
|
|
81
|
-
"@deepseek-ai/dsh-
|
|
82
|
-
"@deepseek-ai/dsh-
|
|
83
|
-
"@deepseek-ai/dsh-
|
|
84
|
-
"@deepseek-ai/
|
|
85
|
-
"@
|
|
86
|
-
"
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
"@deepseek-ai/
|
|
92
|
-
"@deepseek-ai/dsh-
|
|
93
|
-
"@deepseek-ai/dsh-
|
|
94
|
-
"@deepseek-ai/dsh-client-
|
|
95
|
-
"@deepseek-ai/dsh-client-ui-
|
|
96
|
-
"@deepseek-ai/dsh-client-ui-
|
|
97
|
-
"@deepseek-ai/dsh-client-ui-
|
|
98
|
-
"@deepseek-ai/dsh-client-ui-
|
|
99
|
-
"@deepseek-ai/dsh-client-ui-
|
|
100
|
-
"@deepseek-ai/dsh-
|
|
101
|
-
"@deepseek-ai/dsh-
|
|
102
|
-
"@deepseek-ai/dsh-
|
|
103
|
-
"@deepseek-ai/dsh-
|
|
104
|
-
"@deepseek-ai/dsh-
|
|
105
|
-
"@deepseek-ai/dsh-
|
|
106
|
-
"@deepseek-ai/
|
|
107
|
-
"@
|
|
108
|
-
"@
|
|
109
|
-
"@
|
|
110
|
-
"@types/
|
|
111
|
-
"react": "
|
|
112
|
-
"react-test-renderer": "18.3.1",
|
|
113
|
-
"
|
|
114
|
-
"
|
|
115
|
-
"
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-qoder-connect",
|
|
3
|
+
"displayName": "DSH Qoder Connect",
|
|
4
|
+
"description": "将 Qoder(国内版 / 国际版)的模型接入 DeepSeek Harness — bring Qoder (CN & international) models into DeepSeek Harness.",
|
|
5
|
+
"version": "0.2.0",
|
|
6
|
+
"author": "corrinehu",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"dsh-plugin",
|
|
9
|
+
"deepseek-harness",
|
|
10
|
+
"deepseek",
|
|
11
|
+
"qoder",
|
|
12
|
+
"model-provider"
|
|
13
|
+
],
|
|
14
|
+
"type": "module",
|
|
15
|
+
"engines": {
|
|
16
|
+
"dsh": ">=0.1.5-rc.2",
|
|
17
|
+
"node": "^22.19.0 || >=24.0.0"
|
|
18
|
+
},
|
|
19
|
+
"main": "lib/index.js",
|
|
20
|
+
"types": "lib/index.d.ts",
|
|
21
|
+
"bin": {
|
|
22
|
+
"dsh-qoder-connect": "lib/bin.js"
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"prepack": "npm run build",
|
|
26
|
+
"build": "tsdown",
|
|
27
|
+
"test": "node -r ./tests/sandbox-shim.cjs ./node_modules/vitest/vitest.mjs run",
|
|
28
|
+
"test:qoder": "node --test --test-isolation=none --experimental-strip-types tests/qoder/*.test.ts",
|
|
29
|
+
"typecheck": "tsc -p tsconfig.json && tsc -p tsconfig.client.json",
|
|
30
|
+
"check": "pnpm run typecheck && pnpm run test && pnpm run test:qoder && pnpm run build"
|
|
31
|
+
},
|
|
32
|
+
"exports": {
|
|
33
|
+
".": {
|
|
34
|
+
"types": "./lib/index.d.ts",
|
|
35
|
+
"default": "./lib/index.js"
|
|
36
|
+
},
|
|
37
|
+
"./client": "./lib/client.js",
|
|
38
|
+
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
39
|
+
"./package.json": "./package.json",
|
|
40
|
+
"./locale/*.json": "./locale/*.json"
|
|
41
|
+
},
|
|
42
|
+
"files": [
|
|
43
|
+
"lib",
|
|
44
|
+
"assets",
|
|
45
|
+
"cordis.patch.yml",
|
|
46
|
+
"README.md",
|
|
47
|
+
"README.en.md",
|
|
48
|
+
"LICENSE",
|
|
49
|
+
"locale/*.json"
|
|
50
|
+
],
|
|
51
|
+
"license": "MIT",
|
|
52
|
+
"repository": {
|
|
53
|
+
"type": "git",
|
|
54
|
+
"url": "git+https://github.com/masknull/dsh-qoder-connect.git"
|
|
55
|
+
},
|
|
56
|
+
"homepage": "https://github.com/masknull/dsh-qoder-connect#readme",
|
|
57
|
+
"bugs": "https://github.com/masknull/dsh-qoder-connect/issues",
|
|
58
|
+
"dsh": {
|
|
59
|
+
"bundle": {
|
|
60
|
+
"patch": "./cordis.patch.yml"
|
|
61
|
+
},
|
|
62
|
+
"client": {
|
|
63
|
+
"inject": [
|
|
64
|
+
"@deepseek-ai/dsh-api-remotes",
|
|
65
|
+
"@deepseek-ai/dsh-api-session-controller",
|
|
66
|
+
"@deepseek-ai/dsh-client-ui-renderer",
|
|
67
|
+
"@deepseek-ai/dsh-client-ui-settings-plugins",
|
|
68
|
+
"@deepseek-ai/dsh-client-ui-settings",
|
|
69
|
+
"@deepseek-ai/dsh-client-ui-conversation",
|
|
70
|
+
"@deepseek-ai/dsh-client-ui-model-selection",
|
|
71
|
+
"@deepseek-ai/dsh-client-ui-sidebar",
|
|
72
|
+
"@deepseek-ai/dsh-client-locale"
|
|
73
|
+
],
|
|
74
|
+
"platform": "web"
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
"peerDependencies": {
|
|
78
|
+
"@deepseek-ai/cordis": "^4.0.2",
|
|
79
|
+
"@deepseek-ai/dsh-atomic-write": "^0.1.5-rc.1",
|
|
80
|
+
"@deepseek-ai/dsh-attachment": "^0.1.5-rc.1",
|
|
81
|
+
"@deepseek-ai/dsh-home-paths": "^0.1.5-rc.1",
|
|
82
|
+
"@deepseek-ai/dsh-host-webserver": "^0.1.5-rc.1",
|
|
83
|
+
"@deepseek-ai/dsh-llm": "^0.1.5-rc.1",
|
|
84
|
+
"@deepseek-ai/dsh-llm-pi-ai": "^0.1.5-rc.1",
|
|
85
|
+
"@deepseek-ai/dsh-settings": "^0.1.5-rc.1",
|
|
86
|
+
"@deepseek-ai/schemastery": "^3.18.2",
|
|
87
|
+
"@earendil-works/pi-ai": "^0.85.1",
|
|
88
|
+
"react": "^18.2.0"
|
|
89
|
+
},
|
|
90
|
+
"devDependencies": {
|
|
91
|
+
"@deepseek-ai/cordis": "^4.0.2",
|
|
92
|
+
"@deepseek-ai/dsh-atomic-write": "^0.1.5-rc.1",
|
|
93
|
+
"@deepseek-ai/dsh-attachment": "^0.1.5-rc.1",
|
|
94
|
+
"@deepseek-ai/dsh-client-locale": "^0.1.5-rc.1",
|
|
95
|
+
"@deepseek-ai/dsh-client-ui-conversation": "0.1.5-rc.1",
|
|
96
|
+
"@deepseek-ai/dsh-client-ui-model-selection": "0.1.5-rc.1",
|
|
97
|
+
"@deepseek-ai/dsh-client-ui-renderer": "^0.1.5-rc.1",
|
|
98
|
+
"@deepseek-ai/dsh-client-ui-settings": "^0.1.5-rc.1",
|
|
99
|
+
"@deepseek-ai/dsh-client-ui-settings-plugins": "^0.1.5-rc.1",
|
|
100
|
+
"@deepseek-ai/dsh-client-ui-sidebar": "^0.1.5-rc.1",
|
|
101
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.1.5-rc.1",
|
|
102
|
+
"@deepseek-ai/dsh-credentials": "^0.1.5-rc.1",
|
|
103
|
+
"@deepseek-ai/dsh-home-paths": "^0.1.5-rc.1",
|
|
104
|
+
"@deepseek-ai/dsh-host-webserver": "^0.1.5-rc.1",
|
|
105
|
+
"@deepseek-ai/dsh-llm": "^0.1.5-rc.1",
|
|
106
|
+
"@deepseek-ai/dsh-llm-pi-ai": "^0.1.5-rc.1",
|
|
107
|
+
"@deepseek-ai/dsh-settings": "^0.1.5-rc.1",
|
|
108
|
+
"@deepseek-ai/schemastery": "^3.18.2",
|
|
109
|
+
"@earendil-works/pi-ai": "^0.85.1",
|
|
110
|
+
"@types/node": "^22.20.0",
|
|
111
|
+
"@types/react": "~18.3.1",
|
|
112
|
+
"@types/react-test-renderer": "^18.3.1",
|
|
113
|
+
"react": "^18.2.0",
|
|
114
|
+
"react-test-renderer": "18.3.1",
|
|
115
|
+
"tsdown": "^0.22.2",
|
|
116
|
+
"typescript": "^5.9.3",
|
|
117
|
+
"vitest": "^4.1.8"
|
|
118
|
+
},
|
|
119
|
+
"dependencies": {
|
|
120
|
+
"js-yaml": "^4.1.0"
|
|
121
|
+
},
|
|
122
|
+
"icon": "./assets/icon.svg"
|
|
123
|
+
}
|