dsh-qoder-connect 0.2.2 → 0.2.3-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.en.md +177 -177
- package/README.md +178 -178
- package/cordis.patch.yml +6 -6
- package/lib/bin.js +1 -1
- package/lib/client.js +13 -6
- package/lib/index.d.ts +23 -9
- package/lib/index.js +13 -17
- package/lib/{variants-CFI6-cGn.js → variants-UX2MxZaz.js} +269 -52
- package/package.json +120 -120
|
@@ -760,17 +760,14 @@ function randomSentinel() {
|
|
|
760
760
|
return `probe_sentinel_${randomBytes(12).toString("hex")}`;
|
|
761
761
|
}
|
|
762
762
|
/**
|
|
763
|
-
* The
|
|
764
|
-
* (`src/qoder/transport/wire/serialize.ts`
|
|
763
|
+
* The code a refused effort arrives under, wherever it was refused
|
|
764
|
+
* (`src/qoder/transport/wire/serialize.ts` for a declared row, `probeEffort`
|
|
765
|
+
* for a refusal the endpoint issued itself).
|
|
765
766
|
*
|
|
766
|
-
*
|
|
767
|
-
*
|
|
768
|
-
*
|
|
769
|
-
*
|
|
770
|
-
* weigh; the sequence below still distinguishes "advertises and validates"
|
|
771
|
-
* (per-level refusals after an accepted baseline) from "accepts anything the
|
|
772
|
-
* catalog does not name" — the sentinel answers that question against the
|
|
773
|
-
* same vocabulary the request path will later enforce.
|
|
767
|
+
* Only a row that declares efforts can be refused locally. An undeclared row —
|
|
768
|
+
* the only kind a sweep is offered for — sends every candidate to the wire and
|
|
769
|
+
* lets the sentinel measure the endpoint rather than this plugin's own reading
|
|
770
|
+
* of the catalog.
|
|
774
771
|
*/
|
|
775
772
|
const INVALID_EFFORT_CODE = "UNSUPPORTED_REASONING_EFFORT";
|
|
776
773
|
/** Whether an attempt is an attributable rejection of the effort value. */
|
|
@@ -855,6 +852,94 @@ async function probeModel(options) {
|
|
|
855
852
|
};
|
|
856
853
|
}
|
|
857
854
|
//#endregion
|
|
855
|
+
//#region src/qoder/codes.ts
|
|
856
|
+
const table = (family, rows) => rows.map(([code, name]) => ({
|
|
857
|
+
code,
|
|
858
|
+
name,
|
|
859
|
+
family
|
|
860
|
+
}));
|
|
861
|
+
/**
|
|
862
|
+
* The documented code table.
|
|
863
|
+
*
|
|
864
|
+
* Source: Qoder's *Errors and error codes* reference
|
|
865
|
+
* (https://docs.qoder.com/cli/sdk/errors), "Result error codes" — the same
|
|
866
|
+
* reference also states the rule this module exists to serve: "Authentication,
|
|
867
|
+
* quota, policy, input, and configuration errors require a change before
|
|
868
|
+
* retrying."
|
|
869
|
+
*
|
|
870
|
+
* Scope note on evidence. That page documents the Agent SDK / qodercli surface,
|
|
871
|
+
* not the loopback HTTP endpoint this plugin talks to. Two of its codes are
|
|
872
|
+
* confirmed on our own path, with matching semantics:
|
|
873
|
+
*
|
|
874
|
+
* - `10605` — observed verbatim: `{"code":"10605","message":"{\"isQueued\":true,
|
|
875
|
+
* ...\"retryAfterSeconds\":30}"}` (see `qoderQueueSignal`).
|
|
876
|
+
* - `110` — reported in dsh-qoder-connect#10 and reproduced by injecting the
|
|
877
|
+
* reported body through this transport: `{"code":"110","message":"Billing
|
|
878
|
+
* daily count exceeded"}`.
|
|
879
|
+
*
|
|
880
|
+
* The rest are carried as the upstream's documented vocabulary. They are read
|
|
881
|
+
* only to identify the family; an undocumented code is never guessed at, so a
|
|
882
|
+
* body this table does not recognize keeps whatever routing the HTTP status
|
|
883
|
+
* already gives it.
|
|
884
|
+
*/
|
|
885
|
+
const QODER_RESULT_CODES = Object.freeze([
|
|
886
|
+
...table("auth", [["105", "Login or access token expired"]]),
|
|
887
|
+
...table("quota", [
|
|
888
|
+
["110", "Daily usage limit reached"],
|
|
889
|
+
["113", "Usage quota exhausted"],
|
|
890
|
+
["114", "Free-trial account limit reached"],
|
|
891
|
+
["115", "Free-user quota reached"],
|
|
892
|
+
["116", "Team administrator Credits exhausted"],
|
|
893
|
+
["117", "Team member Credits exhausted"],
|
|
894
|
+
["118", "Personal Credits exhausted"],
|
|
895
|
+
["119", "Free usage limit for the selected model reached"],
|
|
896
|
+
["122", "Billing-group Credits limit reached"]
|
|
897
|
+
]),
|
|
898
|
+
...table("request", [
|
|
899
|
+
["406", "Request blocked because of sensitive content or model refusal"],
|
|
900
|
+
["416", "Requested range or request shape is not satisfiable"],
|
|
901
|
+
["430", "Requested capability is not supported"],
|
|
902
|
+
["47902", "Maximum Agent turns reached"],
|
|
903
|
+
["48716", "A Hook blocked Agent execution"],
|
|
904
|
+
["80411", "Input content is too long"],
|
|
905
|
+
["80412", "Too many images or documents"]
|
|
906
|
+
]),
|
|
907
|
+
...table("runtime", [
|
|
908
|
+
["500", "Request or network failure"],
|
|
909
|
+
["10408", "Request timed out"],
|
|
910
|
+
["10500", "Model service internal error"],
|
|
911
|
+
["10605", "Model request is queued"]
|
|
912
|
+
])
|
|
913
|
+
]);
|
|
914
|
+
const byCode = new Map(QODER_RESULT_CODES.map((entry) => [entry.code, entry]));
|
|
915
|
+
/**
|
|
916
|
+
* The documented result code a rejection body names, if it names one.
|
|
917
|
+
*
|
|
918
|
+
* Qoder nests: a queue answer arrives as `{"code":"10605","message":"{\"isQueued\":
|
|
919
|
+
* true,...}"}`, so the inner payload is escaped JSON and a marker can sit at any
|
|
920
|
+
* depth. Dropping the ASCII backslashes keeps every layer readable at once — the
|
|
921
|
+
* same trick `qoderQueueSignal` uses, and for the same reason; no marker this
|
|
922
|
+
* module looks for depends on a backslash.
|
|
923
|
+
*
|
|
924
|
+
* Only a *quoted* `code` key counts. Qoder also mirrors raw HTTP statuses into a
|
|
925
|
+
* `code` field (`"code":"429"`, `"code":403`), and a message that merely mentions
|
|
926
|
+
* a code in prose must not be read as one; both stay unrecognized because neither
|
|
927
|
+
* names a documented result code.
|
|
928
|
+
*
|
|
929
|
+
* @param body - the upstream error body, or the formatted message when the body
|
|
930
|
+
* was only embedded in it.
|
|
931
|
+
* @returns the matching table entry, or `undefined` when the body names no
|
|
932
|
+
* documented code — never a guess.
|
|
933
|
+
*/
|
|
934
|
+
function qoderResultCode(body) {
|
|
935
|
+
if (body === void 0 || body === "") return void 0;
|
|
936
|
+
const text = body.replace(/\\/gu, "");
|
|
937
|
+
for (const match of text.matchAll(/"(?:error_)?code"\s*:\s*"?(\d+)"?/gu)) {
|
|
938
|
+
const entry = byCode.get(match[1]);
|
|
939
|
+
if (entry !== void 0) return entry;
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
//#endregion
|
|
858
943
|
//#region src/qoder/errors.ts
|
|
859
944
|
/**
|
|
860
945
|
* DSH-compatible LLM error representation.
|
|
@@ -902,18 +987,25 @@ function qoderQueueSignal(body) {
|
|
|
902
987
|
}
|
|
903
988
|
/**
|
|
904
989
|
* Replace status digits that read as authorization failure with a neutral
|
|
905
|
-
* word, for messages the upstream used to announce a
|
|
990
|
+
* word, for messages the upstream used to announce something other than a dead
|
|
991
|
+
* credential.
|
|
906
992
|
*
|
|
907
993
|
* `dsh-llm-pi-ai`'s error classifier is a text matcher whose first test is
|
|
908
|
-
* `/\b(?:401|403)\b/` — ahead of its 429/rate-limit test
|
|
909
|
-
* carrying those digits classifies AUTH no matter
|
|
910
|
-
* travelling beside it.
|
|
911
|
-
*
|
|
912
|
-
*
|
|
913
|
-
*
|
|
994
|
+
* `/\b(?:401|403)\b/` — ahead of its 429/rate-limit test and ahead of every
|
|
995
|
+
* quota test — so any message carrying those digits classifies AUTH no matter
|
|
996
|
+
* the structured code travelling beside it. An answer that is really a
|
|
997
|
+
* throttle or an exhausted quota must not reach that matcher as-is, or the user
|
|
998
|
+
* sees "API 密钥无效" for a limit that clears on its own (observed verbatim on
|
|
999
|
+
* 2026-09-26 23:30 and again at 23:40 for the queue; reproduced 2026-10-08 for
|
|
1000
|
+
* the daily counter reported in #10).
|
|
1001
|
+
*
|
|
1002
|
+
* @param message - the message to neutralize.
|
|
1003
|
+
* @param replacement - the neutral word substituted for the digits. `throttled`
|
|
1004
|
+
* for the queue answer, whose wording users and tests already know; `limited`
|
|
1005
|
+
* for a quota answer, which is not a throttle.
|
|
914
1006
|
*/
|
|
915
|
-
function deauthorizeDigits(message) {
|
|
916
|
-
return message.replace(/\b(?:401|403)\b/gu,
|
|
1007
|
+
function deauthorizeDigits(message, replacement = "throttled") {
|
|
1008
|
+
return message.replace(/\b(?:401|403)\b/gu, replacement);
|
|
917
1009
|
}
|
|
918
1010
|
function qoderHttpError(message, response, body) {
|
|
919
1011
|
const { status } = response;
|
|
@@ -928,6 +1020,15 @@ function qoderHttpError(message, response, body) {
|
|
|
928
1020
|
...requestId === void 0 ? {} : { requestId }
|
|
929
1021
|
});
|
|
930
1022
|
}
|
|
1023
|
+
const documented = qoderResultCode(body ?? message);
|
|
1024
|
+
if (documented?.family === "quota") return new QoderLlmError(deauthorizeDigits(message, "limited"), "QUOTA", {
|
|
1025
|
+
status,
|
|
1026
|
+
...requestId === void 0 ? {} : { requestId }
|
|
1027
|
+
});
|
|
1028
|
+
if (documented?.family === "request" && (status === 401 || status === 403)) return new QoderLlmError(deauthorizeDigits(message, "rejected"), "INVALID_REQUEST", {
|
|
1029
|
+
status,
|
|
1030
|
+
...requestId === void 0 ? {} : { requestId }
|
|
1031
|
+
});
|
|
931
1032
|
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", {
|
|
932
1033
|
status,
|
|
933
1034
|
...headerRetryAfterMs === void 0 ? {} : { providerRetryAfterMs: headerRetryAfterMs },
|
|
@@ -939,6 +1040,24 @@ function qoderRequestId(headers) {
|
|
|
939
1040
|
return normalized ? ProviderRequestId(normalized) : void 0;
|
|
940
1041
|
}
|
|
941
1042
|
/**
|
|
1043
|
+
* Codes that keep the upstream's 401/403 status without being credential
|
|
1044
|
+
* rejections, so the status arm of {@link isQoderAuthRejection} must not claim
|
|
1045
|
+
* them.
|
|
1046
|
+
*
|
|
1047
|
+
* `RATE_LIMIT` is the queue answer (code 10605 / isQueued / retryAfterSeconds)
|
|
1048
|
+
* and `QUOTA`/`ACCOUNT_QUOTA` is a spent usage window (#10, e.g. code 110 with
|
|
1049
|
+
* `Billing daily count exceeded`). Both arrive as 401/403 and neither is fixed
|
|
1050
|
+
* by a fresh job token — the queue cannot be jumped and a quota cannot be
|
|
1051
|
+
* exchanged away — so healing either one spends an exchange and suppresses the
|
|
1052
|
+
* retry that would actually help (the queue) or reports a failure notice for a
|
|
1053
|
+
* condition that clears on its own (the quota).
|
|
1054
|
+
*/
|
|
1055
|
+
const NON_AUTH_STATUS_CODES = [
|
|
1056
|
+
"RATE_LIMIT",
|
|
1057
|
+
"QUOTA",
|
|
1058
|
+
"ACCOUNT_QUOTA"
|
|
1059
|
+
];
|
|
1060
|
+
/**
|
|
942
1061
|
* Whether this failure is an upstream authorization rejection worth one
|
|
943
1062
|
* re-auth retry.
|
|
944
1063
|
*
|
|
@@ -949,16 +1068,16 @@ function qoderRequestId(headers) {
|
|
|
949
1068
|
* exchange, so callers clear their credential cache and retry once before
|
|
950
1069
|
* reporting `AUTH` to the user.
|
|
951
1070
|
*
|
|
952
|
-
* A
|
|
953
|
-
* 401/403**:
|
|
954
|
-
*
|
|
955
|
-
*
|
|
956
|
-
*
|
|
957
|
-
*
|
|
1071
|
+
* A code in {@link NON_AUTH_STATUS_CODES} is never a re-auth candidate, **even
|
|
1072
|
+
* when its status is 401/403**: those answers keep the status while saying the
|
|
1073
|
+
* credential was not the problem. Healing one exchanged job tokens against a
|
|
1074
|
+
* condition no token can change (observed 2026-09-26 for the queue, and again
|
|
1075
|
+
* 2026-10-08 for the daily counter), and by consuming the retry it suppressed
|
|
1076
|
+
* the rate-limit retry that would have ridden out the window.
|
|
958
1077
|
*/
|
|
959
1078
|
function isQoderAuthRejection(error) {
|
|
960
1079
|
if (!(error instanceof LlmError)) return false;
|
|
961
|
-
if (error.code
|
|
1080
|
+
if (NON_AUTH_STATUS_CODES.includes(error.code)) return false;
|
|
962
1081
|
return error.code === "AUTH" || error.failure.status === 401 || error.failure.status === 403;
|
|
963
1082
|
}
|
|
964
1083
|
function retryAfterMs(value, nowMs = Date.now()) {
|
|
@@ -1960,7 +2079,8 @@ function stableChatRecordId(model, messages, tools, maxTokens) {
|
|
|
1960
2079
|
function validateQoderRequestShape(options, model) {
|
|
1961
2080
|
if (options.reasoningEffort !== void 0) {
|
|
1962
2081
|
const effort = String(options.reasoningEffort);
|
|
1963
|
-
|
|
2082
|
+
const declared = model?.reasoningEfforts;
|
|
2083
|
+
if (declared !== void 0 && declared.length > 0 && !declared.some((candidate) => candidate.id === effort)) throw new QoderLlmError(`Qoder model "${options.model}" does not advertise reasoning effort "${effort}".`, "UNSUPPORTED_REASONING_EFFORT");
|
|
1964
2084
|
}
|
|
1965
2085
|
if (options.messages.some((message) => contentHasImage(message.content)) && model?.supportsImages !== true) throw new QoderLlmError(`Qoder model "${options.model}" does not advertise image input.`, "UNSUPPORTED_CONTENT");
|
|
1966
2086
|
validateMessageShapes(options.messages);
|
|
@@ -2598,9 +2718,38 @@ var QoderUsageReader = class {
|
|
|
2598
2718
|
}
|
|
2599
2719
|
}
|
|
2600
2720
|
};
|
|
2721
|
+
/**
|
|
2722
|
+
* The UTC+8 wall clock of an instant, as a Date whose *local* fields hold it.
|
|
2723
|
+
*
|
|
2724
|
+
* Shifting by the host's own offset cancels the host timezone out, so the
|
|
2725
|
+
* result reads the same on a UTC+8 machine and on a UTC-5 one. Callers must
|
|
2726
|
+
* therefore use the local getters (`getHours`, `getFullYear`, …) on it.
|
|
2727
|
+
*/
|
|
2728
|
+
function utc8WallClock(atMs) {
|
|
2729
|
+
const d = new Date(atMs);
|
|
2730
|
+
return new Date(d.getTime() + (d.getTimezoneOffset() + 480) * 6e4);
|
|
2731
|
+
}
|
|
2732
|
+
/**
|
|
2733
|
+
* Format an epoch-ms instant as its UTC+8 calendar date, `YYYY-MM-DD`.
|
|
2734
|
+
*/
|
|
2735
|
+
function formatUtc8Date(atMs) {
|
|
2736
|
+
const utc8 = utc8WallClock(atMs);
|
|
2737
|
+
return `${utc8.getFullYear()}-${String(utc8.getMonth() + 1).padStart(2, "0")}-${String(utc8.getDate()).padStart(2, "0")}`;
|
|
2738
|
+
}
|
|
2601
2739
|
//#endregion
|
|
2602
2740
|
//#region src/qoder/transport/checkin.ts
|
|
2741
|
+
/** Qoder campaign inquiry and daily benefit check-in service. */
|
|
2603
2742
|
const defaultCheckInTimeoutMs = 15e3;
|
|
2743
|
+
/**
|
|
2744
|
+
* The longest span still recognised as one daily benefit window.
|
|
2745
|
+
*
|
|
2746
|
+
* Qoder's daily window runs 10:00 → 09:59 the next morning (86_340 s). The
|
|
2747
|
+
* slack above 24h absorbs a clock or rounding difference; a genuinely longer
|
|
2748
|
+
* campaign is *not* a daily window, and its start day says nothing about which
|
|
2749
|
+
* day one claim of it belongs to, so it must fall back to the calendar day
|
|
2750
|
+
* rather than mislabel every day of its run.
|
|
2751
|
+
*/
|
|
2752
|
+
const DAILY_WINDOW_MAX_MS = 864e5;
|
|
2604
2753
|
var QoderCheckInService = class {
|
|
2605
2754
|
authService;
|
|
2606
2755
|
fetchImpl;
|
|
@@ -2648,14 +2797,37 @@ var QoderCheckInService = class {
|
|
|
2648
2797
|
logCategory: "campaigns.claim"
|
|
2649
2798
|
});
|
|
2650
2799
|
}
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2800
|
+
/**
|
|
2801
|
+
* Attribute the campaign the upstream is serving to a day, or say why it
|
|
2802
|
+
* cannot be attributed to today.
|
|
2803
|
+
*
|
|
2804
|
+
* The daily benefit window is 10:00 UTC+8 → 09:59 the next morning, so a
|
|
2805
|
+
* calendar date and the window's own date agree for only fourteen hours of
|
|
2806
|
+
* every twenty-four. Treating them as the same thing is what recorded a
|
|
2807
|
+
* pre-10:00 read of the *previous* window's `CLAIMED` against today.
|
|
2808
|
+
*/
|
|
2809
|
+
classifyCampaignWindow(campaign, nowMs, today) {
|
|
2810
|
+
const startAt = campaign.startAt;
|
|
2811
|
+
if (typeof startAt !== "number" || !Number.isFinite(startAt) || startAt <= 0) return { kind: "unknown" };
|
|
2812
|
+
const endAt = campaign.endAt;
|
|
2813
|
+
if (typeof endAt === "number" && Number.isFinite(endAt)) {
|
|
2814
|
+
const spanMs = (endAt - startAt) * 1e3;
|
|
2815
|
+
if (spanMs <= 0 || spanMs > DAILY_WINDOW_MAX_MS) return { kind: "unknown" };
|
|
2816
|
+
}
|
|
2817
|
+
const startMs = startAt * 1e3;
|
|
2818
|
+
if (startMs > nowMs) return { kind: "not-open" };
|
|
2819
|
+
const date = formatUtc8Date(startMs);
|
|
2820
|
+
return date === today ? {
|
|
2821
|
+
kind: "open",
|
|
2822
|
+
date
|
|
2823
|
+
} : {
|
|
2824
|
+
kind: "earlier",
|
|
2825
|
+
date
|
|
2826
|
+
};
|
|
2655
2827
|
}
|
|
2656
2828
|
async checkIn(pat, signal) {
|
|
2657
|
-
const today = this.getTodayDateString();
|
|
2658
2829
|
const nowMs = Date.now();
|
|
2830
|
+
const today = formatUtc8Date(nowMs);
|
|
2659
2831
|
if (!pat || typeof pat !== "string" || pat.trim() === "") return {
|
|
2660
2832
|
variantId: this.variantId,
|
|
2661
2833
|
date: today,
|
|
@@ -2672,10 +2844,20 @@ var QoderCheckInService = class {
|
|
|
2672
2844
|
status: "no-campaign",
|
|
2673
2845
|
message: "No claimable benefit campaign found for this region"
|
|
2674
2846
|
};
|
|
2675
|
-
|
|
2847
|
+
const campaignWindow = this.classifyCampaignWindow(benefitCampaign, nowMs, today);
|
|
2848
|
+
if (campaignWindow.kind === "not-open" || campaignWindow.kind === "earlier") return {
|
|
2676
2849
|
variantId: this.variantId,
|
|
2677
2850
|
date: today,
|
|
2678
2851
|
timestamp: nowMs,
|
|
2852
|
+
status: "no-campaign",
|
|
2853
|
+
campaignKey: benefitCampaign.campaignKey,
|
|
2854
|
+
message: campaignWindow.kind === "not-open" ? "Today's benefit window has not opened yet" : `Upstream is still serving the benefit window of ${campaignWindow.date}`
|
|
2855
|
+
};
|
|
2856
|
+
const windowDate = campaignWindow.kind === "open" ? campaignWindow.date : today;
|
|
2857
|
+
if (benefitCampaign.claimStatus === "CLAIMED") return {
|
|
2858
|
+
variantId: this.variantId,
|
|
2859
|
+
date: windowDate,
|
|
2860
|
+
timestamp: nowMs,
|
|
2679
2861
|
status: "already-claimed",
|
|
2680
2862
|
campaignKey: benefitCampaign.campaignKey,
|
|
2681
2863
|
amount: benefitCampaign.benefit?.amount ?? 100,
|
|
@@ -2687,7 +2869,7 @@ var QoderCheckInService = class {
|
|
|
2687
2869
|
const amount = claimResult.benefit?.amount ?? benefitCampaign.benefit?.amount ?? 100;
|
|
2688
2870
|
if (isClaimed) return {
|
|
2689
2871
|
variantId: this.variantId,
|
|
2690
|
-
date:
|
|
2872
|
+
date: windowDate,
|
|
2691
2873
|
timestamp: nowMs,
|
|
2692
2874
|
status: replayed ? "already-claimed" : "claimed",
|
|
2693
2875
|
amount,
|
|
@@ -3650,12 +3832,20 @@ function createQoderTransport(options) {
|
|
|
3650
3832
|
*
|
|
3651
3833
|
* @module dsh-qoder-connect/upstream
|
|
3652
3834
|
*/
|
|
3653
|
-
/**
|
|
3835
|
+
/**
|
|
3836
|
+
* HTTP status the shim answers each failure kind with.
|
|
3837
|
+
*
|
|
3838
|
+
* `timeout` is 408 and deliberately not 504: the harness's pi-ai classifier tests
|
|
3839
|
+
* `/\b5\d\d\b/` *before* its `timeout` test, so a 5xx status would be read as
|
|
3840
|
+
* SERVER no matter what the message says. 408 is also the honest answer for a
|
|
3841
|
+
* request the upstream never completed.
|
|
3842
|
+
*/
|
|
3654
3843
|
const KIND_STATUS = {
|
|
3655
3844
|
missing_credential: 401,
|
|
3656
3845
|
auth: 401,
|
|
3657
3846
|
soft_rate: 429,
|
|
3658
3847
|
quota_exceeded: 402,
|
|
3848
|
+
timeout: 408,
|
|
3659
3849
|
server: 502,
|
|
3660
3850
|
client: 400
|
|
3661
3851
|
};
|
|
@@ -3681,6 +3871,9 @@ function normalizeCredits(credits) {
|
|
|
3681
3871
|
function classifyUpstreamError(status, body) {
|
|
3682
3872
|
const lower = body.toLowerCase();
|
|
3683
3873
|
if (qoderQueueSignal(body) !== void 0) return "soft_rate";
|
|
3874
|
+
const documented = qoderResultCode(body);
|
|
3875
|
+
if (documented?.family === "quota") return "quota_exceeded";
|
|
3876
|
+
if (documented?.family === "request") return "client";
|
|
3684
3877
|
if (status === 402) return "quota_exceeded";
|
|
3685
3878
|
if (status === 429) return "soft_rate";
|
|
3686
3879
|
if (status === 401 || status === 403) return "auth";
|
|
@@ -3705,6 +3898,7 @@ function classifyUpstreamError(status, body) {
|
|
|
3705
3898
|
"forbidden",
|
|
3706
3899
|
"not authenticated"
|
|
3707
3900
|
]) if (lower.includes(marker)) return "auth";
|
|
3901
|
+
if (status === 408) return "timeout";
|
|
3708
3902
|
if (status === 0 || status >= 500) return "server";
|
|
3709
3903
|
return "client";
|
|
3710
3904
|
}
|
|
@@ -3720,14 +3914,22 @@ function classifyUpstreamError(status, body) {
|
|
|
3720
3914
|
* `code: RATE_LIMIT, status: 403` (body carries 10605/isQueued/
|
|
3721
3915
|
* retryAfterSeconds), and letting the status arm claim it reported a dead
|
|
3722
3916
|
* credential — which the host never retries — instead of a throttle it would.
|
|
3917
|
+
*
|
|
3918
|
+
* `QUOTA` and the request family are tested before the auth family for the same
|
|
3919
|
+
* reason: both keep the upstream's 401/403 (see `qoderHttpError`), and the status
|
|
3920
|
+
* arm would call a spent daily window or a refused request a credential
|
|
3921
|
+
* rejection. `status === 402` is tested before the request arm so that a 402 the
|
|
3922
|
+
* transport coded `INVALID_REQUEST` (its 4xx fallback) still reads as a quota.
|
|
3723
3923
|
*/
|
|
3724
3924
|
function kindFromQoderFailure(failure) {
|
|
3725
3925
|
if (failure.code === "MISSING_CREDENTIAL") return "missing_credential";
|
|
3726
3926
|
if (failure.code === "RATE_LIMIT") return "soft_rate";
|
|
3927
|
+
if (failure.code === "QUOTA" || failure.code === "ACCOUNT_QUOTA") return "quota_exceeded";
|
|
3928
|
+
if (failure.status === 402) return "quota_exceeded";
|
|
3929
|
+
if (failure.code.startsWith("INVALID_") || failure.code.startsWith("UNSUPPORTED_") || failure.code === "ATTACHMENT") return "client";
|
|
3727
3930
|
if (failure.code === "AUTH" || failure.status === 401 || failure.status === 403) return "auth";
|
|
3728
3931
|
if (failure.status === 429) return "soft_rate";
|
|
3729
|
-
if (failure.code === "
|
|
3730
|
-
if (failure.code.startsWith("INVALID_") || failure.code.startsWith("UNSUPPORTED_") || failure.code === "ATTACHMENT") return "client";
|
|
3932
|
+
if (failure.code === "TIMEOUT") return "timeout";
|
|
3731
3933
|
return "server";
|
|
3732
3934
|
}
|
|
3733
3935
|
/** Whether a Personal Access Token works against one region's endpoints. */
|
|
@@ -4323,13 +4525,14 @@ var QoderUpstreamClient = class {
|
|
|
4323
4525
|
/**
|
|
4324
4526
|
* One minimal reasoning-effort request, as the probe sweep needs it.
|
|
4325
4527
|
*
|
|
4326
|
-
*
|
|
4327
|
-
*
|
|
4328
|
-
*
|
|
4329
|
-
*
|
|
4330
|
-
*
|
|
4331
|
-
*
|
|
4332
|
-
*
|
|
4528
|
+
* A row that declares efforts is still checked locally, so an unlisted value
|
|
4529
|
+
* costs no request. A row that declares none — the only kind detection is
|
|
4530
|
+
* offered for — sends the value to the upstream and waits for its answer,
|
|
4531
|
+
* because the endpoint is the only authority on spellings the catalog never
|
|
4532
|
+
* lists. Either refusal is reported as an attributable 400 with code
|
|
4533
|
+
* `UNSUPPORTED_REASONING_EFFORT`, and an accepted value costs exactly one
|
|
4534
|
+
* streamed request, which this method ends after the first chunk arrives —
|
|
4535
|
+
* see the module docs in `probe.ts`.
|
|
4333
4536
|
*/
|
|
4334
4537
|
async probeEffort(model, effort, signal) {
|
|
4335
4538
|
try {
|
|
@@ -4368,12 +4571,15 @@ var QoderUpstreamClient = class {
|
|
|
4368
4571
|
await iterator.return?.();
|
|
4369
4572
|
}
|
|
4370
4573
|
} catch (error) {
|
|
4371
|
-
if (error instanceof LlmError)
|
|
4372
|
-
status
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
4574
|
+
if (error instanceof LlmError) {
|
|
4575
|
+
const status = error.failure.status ?? defaultStatusForProbe(error.failure.code);
|
|
4576
|
+
return {
|
|
4577
|
+
status,
|
|
4578
|
+
streamed: false,
|
|
4579
|
+
errorCode: status === 400 && blamesReasoningEffort(error.failure.message) ? "UNSUPPORTED_REASONING_EFFORT" : error.failure.code,
|
|
4580
|
+
detail: error.failure.message.slice(0, 300)
|
|
4581
|
+
};
|
|
4582
|
+
}
|
|
4377
4583
|
return {
|
|
4378
4584
|
status: 0,
|
|
4379
4585
|
streamed: false,
|
|
@@ -4382,6 +4588,17 @@ var QoderUpstreamClient = class {
|
|
|
4382
4588
|
}
|
|
4383
4589
|
}
|
|
4384
4590
|
};
|
|
4591
|
+
/**
|
|
4592
|
+
* Whether an upstream 400 blames the `reasoning_effort` value it was sent.
|
|
4593
|
+
*
|
|
4594
|
+
* Qoder answers an impossible effort with a `provider_error` body whose nested
|
|
4595
|
+
* error names `invalid_parameter_error` and quotes `reasoning_effort`. Both
|
|
4596
|
+
* markers are required: a 400 naming only one of them is some other parameter
|
|
4597
|
+
* problem, and the sweep must not record it as a rejected level.
|
|
4598
|
+
*/
|
|
4599
|
+
function blamesReasoningEffort(message) {
|
|
4600
|
+
return message.includes("invalid_parameter_error") && message.includes("reasoning_effort");
|
|
4601
|
+
}
|
|
4385
4602
|
/** Statuses {@link probeEffort} reports for failures the transport raised pre-network. */
|
|
4386
4603
|
function defaultStatusForProbe(code) {
|
|
4387
4604
|
switch (code) {
|
|
@@ -4445,7 +4662,7 @@ const DEFAULT_MAX_TOKENS = 32768;
|
|
|
4445
4662
|
const DEFAULT_CONTEXT_WINDOW = 18e4;
|
|
4446
4663
|
//#endregion
|
|
4447
4664
|
//#region src/version.ts
|
|
4448
|
-
const QODER_CONNECT_VERSION = "0.2.2";
|
|
4665
|
+
const QODER_CONNECT_VERSION = "0.2.3-alpha.2";
|
|
4449
4666
|
//#endregion
|
|
4450
4667
|
//#region src/host-heartbeat.ts
|
|
4451
4668
|
/**
|
|
@@ -4642,4 +4859,4 @@ function variantFor(id) {
|
|
|
4642
4859
|
return QODER_VARIANTS.find((variant) => variant.id === id);
|
|
4643
4860
|
}
|
|
4644
4861
|
//#endregion
|
|
4645
|
-
export {
|
|
4862
|
+
export { QoderCatalog as A, QODER_PAT_ENV_GLOBAL as B, getMachineId as C, QoderCatalogStore as D, randomSentinel as E, QODER_PROBE_PATH as F, QODER_DATA_DIR_ENV as G, patTail as H, QODER_SETTINGS_FACE_PATH as I, qoderPluginDataDir as J, QODER_DATA_DIR_NAME as K, QODER_STATUS_PATH as L, QODER_GLOBAL_AUTH_PATH as M, QODER_GLOBAL_PROBE_PATH as N, qoderCatalogPath as O, QODER_GLOBAL_STATUS_PATH as P, QODER_AUTH_FILENAME as R, utc8WallClock as S, probeModel as T, qoderCredentialIdentity as U, QoderCredentialStore as V, qoderOwnAuthPath as W, qoderStateDir as Y, modelInfoOf as _, QODER_HOST_HEARTBEAT_FILENAME as a, createQoderTransport as b, processStartTimeMs as c, writeHostHeartbeat as d, QODER_CONNECT_VERSION as f, kindFromQoderFailure as g, classifyUpstreamError as h, variantFor as i, QODER_AUTH_PATH as j, FALLBACK_QODER_MODELS as k, qoderHostHeartbeatPath as l, QoderUpstreamClient as m, GLOBAL_VARIANT as n, clearHostHeartbeat as o, KIND_STATUS as p, qoderMachineIdPath as q, QODER_VARIANTS as r, isHeartbeatProcessAlive as s, CHINA_VARIANT as t, readHostHeartbeat as u, normalizeCredits as v, PROBE_EFFORT_CANDIDATES as w, formatUtc8Date as x, validateApiKey as y, QODER_PAT_ENV_CN as z };
|