@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.
- package/README.md +14 -6
- package/index.mjs +76 -9
- 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
|
|
95
|
-
|
|
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
|
-
//
|
|
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
|
|
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
|
|
698
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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: {
|
|
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