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.
@@ -760,17 +760,14 @@ function randomSentinel() {
760
760
  return `probe_sentinel_${randomBytes(12).toString("hex")}`;
761
761
  }
762
762
  /**
763
- * The transport's rejection code for an effort the model does not advertise
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
- * With the Qoder upstream this rejection is raised *locally* against the
767
- * discovery catalog before any network call: a probe therefore measures the
768
- * catalog's declaration as much as the endpoint behind it. That is a weaker
769
- * finding than the WorkBuddy-era one, and it is what the plan's Stage D will
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 queue.
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 — so any message
909
- * carrying those digits classifies AUTH no matter the structured code
910
- * travelling beside it. A queue answer quoting the upstream's 403 (in the
911
- * message prefix, the envelope status, or the body's `"code":"403"`) must not
912
- * reach that matcher as-is, or the user sees "API 密钥无效" for a throttle
913
- * (observed verbatim on 2026-09-26 23:30 and again at 23:40).
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, "throttled");
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 `RATE_LIMIT` code is never a re-auth candidate, **even when its status is
953
- * 401/403**: that is the upstream's queue answer (code 10605 / isQueued /
954
- * retryAfterSeconds in the body), which keeps the status but says the
955
- * credential was not the problem. Healing it exchanged job tokens against a
956
- * queue no token can jump (observed 2026-09-26) and, by consuming the retry,
957
- * suppressed the rate-limit retry that would have ridden out the window.
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 === "RATE_LIMIT") return false;
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
- if (!model?.reasoningEfforts?.some((candidate) => candidate.id === effort)) throw new QoderLlmError(`Qoder model "${options.model}" does not advertise reasoning effort "${effort}".`, "UNSUPPORTED_REASONING_EFFORT");
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
- getTodayDateString() {
2652
- const now = /* @__PURE__ */ new Date();
2653
- const utc8Time = new Date(now.getTime() + (now.getTimezoneOffset() + 480) * 6e4);
2654
- return `${utc8Time.getFullYear()}-${String(utc8Time.getMonth() + 1).padStart(2, "0")}-${String(utc8Time.getDate()).padStart(2, "0")}`;
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
- if (benefitCampaign.claimStatus === "CLAIMED") return {
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: today,
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
- /** HTTP status the shim answers each failure kind with. */
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 === "QUOTA" || failure.status === 402) return "quota_exceeded";
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
- * The transport validates `reasoningEffort` against the model's advertised
4327
- * efforts **locally** before any network call: a rejection therefore reads
4328
- * as an attributable 400 with code `UNSUPPORTED_REASONING_EFFORT`, and an
4329
- * accepted value costs exactly one streamed request, which this method ends
4330
- * after the first chunk arrives. A probe on this upstream measures the
4331
- * discovery catalog as much as the endpoint behind it — see the module docs
4332
- * in `probe.ts`.
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) return {
4372
- status: error.failure.status ?? defaultStatusForProbe(error.failure.code),
4373
- streamed: false,
4374
- errorCode: error.failure.code,
4375
- detail: error.failure.message.slice(0, 300)
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 { QODER_GLOBAL_AUTH_PATH as A, patTail as B, probeModel as C, FALLBACK_QODER_MODELS as D, qoderCatalogPath as E, QODER_STATUS_PATH as F, qoderMachineIdPath as G, qoderOwnAuthPath as H, QODER_AUTH_FILENAME as I, qoderPluginDataDir as K, QODER_PAT_ENV_CN as L, QODER_GLOBAL_STATUS_PATH as M, QODER_PROBE_PATH as N, QoderCatalog as O, QODER_SETTINGS_FACE_PATH as P, QODER_PAT_ENV_GLOBAL as R, PROBE_EFFORT_CANDIDATES as S, QoderCatalogStore as T, QODER_DATA_DIR_ENV as U, qoderCredentialIdentity as V, QODER_DATA_DIR_NAME as W, 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_GLOBAL_PROBE_PATH as j, QODER_AUTH_PATH as k, qoderHostHeartbeatPath as l, QoderUpstreamClient as m, GLOBAL_VARIANT as n, clearHostHeartbeat as o, KIND_STATUS as p, qoderStateDir as q, QODER_VARIANTS as r, isHeartbeatProcessAlive as s, CHINA_VARIANT as t, readHostHeartbeat as u, normalizeCredits as v, randomSentinel as w, getMachineId as x, validateApiKey as y, QoderCredentialStore as z };
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 };