@magpie-community/opencode-qoder-auth 0.2.12 → 0.2.14

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.
Files changed (3) hide show
  1. package/README.md +14 -6
  2. package/index.mjs +76 -9
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -83,25 +83,33 @@ itself.
83
83
 
84
84
  | What Qoder says | What the request returns |
85
85
  |---|---|
86
- | A refused sign-in | 401 |
87
- | A quota | 429 |
86
+ | A refused sign-in (no code, or code 105) | 401 |
87
+ | A quota (codes 110, 112–119, 122) | 429 |
88
+ | Busy or queued for the model (code 10605) | 503, with `Retry-After` from Qoder's `retryAfterSeconds` (30s when it gives none); the sign-in is untouched |
89
+ | A refusal of the request itself (codes 406, 416, 430, 80411, 80412, 100400–100403) | 400, with Qoder's words |
88
90
  | A failure before the answer starts | Qoder's status |
89
91
  | A failure after the answer has started | The stream ends with an error |
90
92
 
91
93
  ## Models
92
94
 
93
95
  The `provider.models` hook reads the account's own list, as Qoder's client
94
- asks for it (`/algo/api/v2/model/list`). It keeps the enabled chat models
95
- and leaves out "auto" and "default", which route inside Qoder. It takes
96
+ asks for it (`/algo/api/v2/model/list`). It keeps the enabled models of the
97
+ listing's `app` scene, the list Qoder's app shows and the one a request
98
+ names (`session_type: "app"`), and adds any model only the `chat` scene
99
+ still lists after the model it follows there, so a model picked before
100
+ stays served (since 0.2.13, #97: the app listed Fast, 0.8×, where chat
101
+ lists Performance, 1.1×). It leaves out "auto" and "default", which route
102
+ inside Qoder. It takes
96
103
  each model's reasoning levels from its `thinking_config`, and its context
97
104
  from its `context_config`: the largest window it offers (Qoder's docs
98
105
  give Qwen3.8-Flash 200K, 400K and 1M), not `max_input_tokens` (180K
99
106
  there), which is only what a request naming no window gets. A model listed
100
107
  with no windows keeps `max_input_tokens`.
101
108
 
102
- The `config` hook declares the list as it was on 2026-09-30:
109
+ The `config` hook declares the list as it was on 2026-09-30, with the
110
+ app's Fast from 2026-10-12:
103
111
 
104
- - Ultimate, Performance, Efficient
112
+ - Ultimate, Fast, Performance, Efficient
105
113
  - Sonus, Cantus
106
114
  - Qwen3.8-Max, Qwen3.8-Flash, Qwen3.7-Max, Qwen3.7-Plus
107
115
  - Kimi-K3, Kimi-K2.8-Preview
package/index.mjs CHANGED
@@ -340,9 +340,11 @@ async function refreshJob(site, refresh, user) {
340
340
  const EFFORT_ORDER = ["minimal", "low", "medium", "high", "xhigh", "max"]
341
341
  const EFF5 = ["low", "medium", "high", "xhigh", "max"]
342
342
 
343
- // Qoder's list when the account's can't be asked (as it was on 2026-09-30).
343
+ // Qoder's list when the account's can't be asked (as it was on 2026-09-30,
344
+ // with the app's Fast from 2026-10-12).
344
345
  const MODELS = [
345
346
  { id: "ultimate", name: "Ultimate", context: 1_000_000, efforts: EFF5 },
347
+ { id: "fast", name: "Fast", context: 1_000_000 },
346
348
  { id: "performance", name: "Performance", context: 1_000_000, efforts: EFF5 },
347
349
  { id: "efficient", name: "Efficient", context: 200_000 },
348
350
  { id: "smodel", name: "Sonus", context: 180_000, efforts: EFF5 },
@@ -485,10 +487,34 @@ function modelInfo(raw) {
485
487
  return m
486
488
  }
487
489
 
488
- // modelInfos is the listing's enabled chat models an agent can pick: "auto"
490
+ // sceneModels is the models an agent can pick from the listing: the "app"
491
+ // scene's, which is what Qoder's app shows and what a request names
492
+ // (qoderBody's product and session_type "app"), in its order, then any
493
+ // "chat" model the app no longer lists, after the model it followed there,
494
+ // so a pick made before stays served. On 2026-10-12 the app listed Fast
495
+ // (fast, 0.8×) where Performance (1.1×) had been, and chat still listed
496
+ // Performance (magpie-community/plugins#97). A listing with only one scene
497
+ // gives that one. Each key once: the app's entry wins.
498
+ function sceneModels(listing) {
499
+ const app = Array.isArray(listing?.app) ? listing.app : []
500
+ const chat = Array.isArray(listing?.chat) ? listing.chat : []
501
+ const out = [...app]
502
+ let after = -1
503
+ for (const m of chat) {
504
+ const i = out.findIndex((x) => x?.key === m?.key)
505
+ if (i >= 0) {
506
+ after = i
507
+ continue
508
+ }
509
+ out.splice(++after, 0, m)
510
+ }
511
+ return out
512
+ }
513
+
514
+ // modelInfos is the listing's enabled models an agent can pick: "auto"
489
515
  // and "default" route inside Qoder and aren't one model.
490
516
  function modelInfos(listing) {
491
- return (listing?.chat ?? [])
517
+ return sceneModels(listing)
492
518
  .filter((m) => m?.enable && !["", "auto", "default"].includes(String(m.key ?? "").trim()))
493
519
  .map(modelInfo)
494
520
  }
@@ -694,19 +720,55 @@ function qoderBody(chat, m) {
694
720
  return body
695
721
  }
696
722
 
697
- // failure is the status and message for a Qoder failure: a refused sign-in
698
- // asks for another, a quota is a 429.
723
+ // failure is the status and message for a Qoder failure. Qoder's own code
724
+ // says what it is, read as Qoder's client reads it (QoderErrorCode in the
725
+ // app's qoder-agent-sdk worker): only 105 (loginExpired) is a lapsed
726
+ // sign-in there, so a 401/403 that carries another code isn't one here
727
+ // either (#98: a queued 10605 came as a 403 and read as "sign in again").
728
+ // A refused chat with no code keeps the built-in's 401, and a quota is a
729
+ // 429.
730
+ const QUEUED = "10605"
731
+ // the codes Qoder's client counts as a plan's usage or credits used up
732
+ const QUOTA_CODES = new Set(["110", "112", "113", "114", "115", "116", "117", "118", "119", "122"])
733
+ // refusals of this request, not of the account: sensitive content, not
734
+ // supported, input too long, too many media, a custom model's service
735
+ const REQUEST_CODES = new Set(["406", "416", "430", "80411", "80412", "100400", "100401", "100403"])
736
+ const BUSY_WAIT = 30
737
+
699
738
  function failure(status, text) {
700
739
  if (status < 400 || status > 599) status = 502
701
740
  let msg = String(text ?? "").trim()
741
+ let code = ""
742
+ let queue
702
743
  try {
703
744
  const j = JSON.parse(text)
704
- if (j?.message) msg = j.message
745
+ code = j?.code == null ? "" : String(j.code)
746
+ if (j?.message) msg = String(j.message)
705
747
  // details is a JSON string, or the object itself (gjson reads either)
706
748
  const d = typeof j?.details === "string" ? JSON.parse(j.details) : j?.details
707
749
  if (d?.error?.message) msg += ": " + d.error.message
750
+ // the queue's state: the message itself, as a JSON string, or a field
751
+ queue = j?.queue && typeof j.queue === "object" ? j.queue : undefined
752
+ if (!queue && typeof j?.message === "string" && j.message.trim().startsWith("{")) {
753
+ const q = JSON.parse(j.message)
754
+ if (q && typeof q === "object" && ("isQueued" in q || "serviceAvailable" in q || "retryAfterSeconds" in q)) queue = q
755
+ }
708
756
  } catch {}
709
757
  msg ||= STATUS_CODES[status] ?? `HTTP ${status}`
758
+ // Qoder's queue for the model: busy for now, with when to ask again. The
759
+ // sign-in is fine (the same token reads the models and the usage), so it
760
+ // is a 503 with Retry-After, which magpie asks again after, and never
761
+ // touches the account's sign-in.
762
+ if (code === QUEUED || queue?.isQueued === true || queue?.serviceAvailable === false) {
763
+ const n = Number(queue?.retryAfterSeconds)
764
+ const wait = Number.isFinite(n) && n > 0 ? Math.ceil(n) : BUSY_WAIT
765
+ const model = queue?.modelKey ? ` for ${queue.modelKey}` : ""
766
+ const tier = queue?.queueType ? ` (this plan's queue: ${queue.queueType})` : ""
767
+ const what = queue?.isQueued === true ? "queued the request" : "is busy"
768
+ return { status: 503, message: `Qoder ${what}${model}${tier}; try again in ${wait}s`, retryAfter: wait, signIn: "" }
769
+ }
770
+ if (QUOTA_CODES.has(code)) return { status: 429, message: "usage limit reached: " + msg, signIn: "" }
771
+ if (REQUEST_CODES.has(code)) return { status: 400, message: `Qoder refused the request (${code}): ${msg}`, signIn: "" }
710
772
  // a refused chat is the built-in's 401, which never marked the account
711
773
  // lapsed: only a refused refresh did (errorResponse says kept). Qoder's
712
774
  // own words go with it: a 401 right after signing in isn't a lapsed
@@ -721,11 +783,16 @@ function failure(status, text) {
721
783
 
722
784
  // errorResponse is a failure as OpenAI's API gives it. signIn is what it
723
785
  // means for the account (magpie's X-Magpie-Sign-In): a 401 keeps it unless
724
- // told expired, since the built-in marked only a refused refresh.
725
- const errorResponse = ({ status, message, signIn = status === 401 ? "kept" : undefined }) =>
786
+ // told expired, since the built-in marked only a refused refresh. retryAfter
787
+ // (seconds) is sent as Retry-After.
788
+ const errorResponse = ({ status, message, signIn = status === 401 ? "kept" : undefined, retryAfter }) =>
726
789
  new Response(JSON.stringify({ error: { message, type: "qoder_error", code: status } }), {
727
790
  status,
728
- headers: { "Content-Type": "application/json", ...(signIn ? { "X-Magpie-Sign-In": signIn } : {}) },
791
+ headers: {
792
+ "Content-Type": "application/json",
793
+ ...(signIn ? { "X-Magpie-Sign-In": signIn } : {}),
794
+ ...(retryAfter > 0 ? { "Retry-After": String(retryAfter) } : {}),
795
+ },
729
796
  })
730
797
 
731
798
  // ---- the reply ----------------------------------------------------------------
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magpie-community/opencode-qoder-auth",
3
- "version": "0.2.12",
3
+ "version": "0.2.14",
4
4
  "description": "Qoder subscriptions in OpenCode and magpie: Qoder (qoder.com) and Qoder CN (qoder.cn)",
5
5
  "type": "module",
6
6
  "main": "./index.mjs",