@magpie-community/opencode-qoder-auth 0.1.1 → 0.1.3
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/index.mjs +104 -41
- package/package.json +1 -1
package/index.mjs
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
// from magpie's internal/qoder, which ported it from CLIProxyAPI's qoder
|
|
17
17
|
// support (https://github.com/ufec/CLIProxyAPI, MIT).
|
|
18
18
|
import { createCipheriv, createHash, publicEncrypt, randomBytes, randomUUID, constants } from "node:crypto"
|
|
19
|
+
import { STATUS_CODES } from "node:http"
|
|
19
20
|
|
|
20
21
|
const ID = "qoder"
|
|
21
22
|
const CLIENT_ID = "732aef47-9cf2-46a2-95fe-4cebb5d0d1fa"
|
|
@@ -213,14 +214,24 @@ async function deviceSignIn() {
|
|
|
213
214
|
}
|
|
214
215
|
}
|
|
215
216
|
|
|
216
|
-
|
|
217
|
+
// SignInGone is a sign-in that can't sign a request: a 401. Only Qoder
|
|
218
|
+
// refusing the refresh token marks the account lapsed (expired), as the
|
|
219
|
+
// built-in's qoderRefreshFailed did; no refresh token, or no account,
|
|
220
|
+
// answered 401 there without marking one.
|
|
221
|
+
class SignInGone extends Error {
|
|
222
|
+
constructor(message, expired = false) {
|
|
223
|
+
super(message)
|
|
224
|
+
this.expired = expired
|
|
225
|
+
}
|
|
226
|
+
}
|
|
217
227
|
|
|
218
228
|
// refreshJob trades the job token's refresh token for a new pair; the old
|
|
219
229
|
// one is spent.
|
|
220
|
-
async function refreshJob(refresh) {
|
|
230
|
+
async function refreshJob(refresh, user) {
|
|
221
231
|
if (!String(refresh ?? "").trim()) throw new SignInGone("Qoder: the sign-in lapsed; sign in again")
|
|
222
232
|
const r = await openapi("/api/v1/jobToken/refresh", { method: "POST", body: { refresh_token: refresh } })
|
|
223
|
-
if (r.status === 401 || r.status === 403)
|
|
233
|
+
if (r.status === 401 || r.status === 403)
|
|
234
|
+
throw new SignInGone(`${user}'s Qoder sign-in has expired — sign in again (qoder job token refresh: status ${r.status})`, true)
|
|
224
235
|
if (r.status !== 200) throw new Error(`Qoder job token refresh: status ${r.status}`)
|
|
225
236
|
if (!String(r.json?.token ?? "").trim() || !String(r.json?.refresh_token ?? "").trim())
|
|
226
237
|
throw new Error("Qoder job token refresh: incomplete token pair")
|
|
@@ -252,6 +263,15 @@ const MODELS = [
|
|
|
252
263
|
{ id: "mmodel", name: "MiniMax-M3", context: 180_000 },
|
|
253
264
|
].map((m) => ({ images: true, ...m }))
|
|
254
265
|
|
|
266
|
+
// freeOf reads whether the listing marks a model free, as Qoder's client
|
|
267
|
+
// reads it (internal/qoder/models.go): is_free true, or a price_factor (the
|
|
268
|
+
// credits a request costs, as a multiple) of 0, either in snake or camel case.
|
|
269
|
+
function freeOf(raw) {
|
|
270
|
+
const is = raw.is_free ?? raw.isFree
|
|
271
|
+
const price = raw.price_factor ?? raw.priceFactor
|
|
272
|
+
return is === true || (typeof price === "number" && price === 0)
|
|
273
|
+
}
|
|
274
|
+
|
|
255
275
|
// modelInfo reads one entry of the listing's "chat" array: its efforts and
|
|
256
276
|
// default from thinking_config (is_reasoning alone when it has none).
|
|
257
277
|
function modelInfo(raw) {
|
|
@@ -265,6 +285,7 @@ function modelInfo(raw) {
|
|
|
265
285
|
alwaysThinks: false,
|
|
266
286
|
efforts: Array.isArray(raw.reasoning_efforts) ? [...raw.reasoning_efforts] : [],
|
|
267
287
|
defaultEffort: "",
|
|
288
|
+
free: freeOf(raw),
|
|
268
289
|
config: raw,
|
|
269
290
|
}
|
|
270
291
|
const tc = raw.thinking_config
|
|
@@ -331,6 +352,8 @@ function runtimeModel(m) {
|
|
|
331
352
|
},
|
|
332
353
|
release_date: "",
|
|
333
354
|
variants: Object.fromEntries((m.efforts ?? []).map((e) => [e, { reasoningEffort: e }])),
|
|
355
|
+
// magpie's own: served at no cost to the plan's credits
|
|
356
|
+
free: !!m.free,
|
|
334
357
|
}
|
|
335
358
|
}
|
|
336
359
|
|
|
@@ -474,17 +497,26 @@ function failure(status, text) {
|
|
|
474
497
|
try {
|
|
475
498
|
const j = JSON.parse(text)
|
|
476
499
|
if (j?.message) msg = j.message
|
|
477
|
-
|
|
500
|
+
// details is a JSON string, or the object itself (gjson reads either)
|
|
501
|
+
const d = typeof j?.details === "string" ? JSON.parse(j.details) : j?.details
|
|
478
502
|
if (d?.error?.message) msg += ": " + d.error.message
|
|
479
503
|
} catch {}
|
|
480
|
-
msg ||= `HTTP ${status}`
|
|
481
|
-
|
|
504
|
+
msg ||= STATUS_CODES[status] ?? `HTTP ${status}`
|
|
505
|
+
// a refused chat is the built-in's 401, which never marked the account
|
|
506
|
+
// lapsed: only a refused refresh did (errorResponse says kept)
|
|
507
|
+
if (status === 401 || status === 403) return { status: 401, message: "the sign-in lapsed — sign in again" }
|
|
482
508
|
if (status === 429 || msg.toLowerCase().includes("quota")) return { status: 429, message: "usage limit reached: " + msg }
|
|
483
|
-
return { status, message:
|
|
509
|
+
return { status, message: msg }
|
|
484
510
|
}
|
|
485
511
|
|
|
486
|
-
|
|
487
|
-
|
|
512
|
+
// errorResponse is a failure as OpenAI's API gives it. signIn is what it
|
|
513
|
+
// means for the account (magpie's X-Magpie-Sign-In): a 401 keeps it unless
|
|
514
|
+
// told expired, since the built-in marked only a refused refresh.
|
|
515
|
+
const errorResponse = ({ status, message, signIn = status === 401 ? "kept" : undefined }) =>
|
|
516
|
+
new Response(JSON.stringify({ error: { message, type: "qoder_error", code: status } }), {
|
|
517
|
+
status,
|
|
518
|
+
headers: { "Content-Type": "application/json", ...(signIn ? { "X-Magpie-Sign-In": signIn } : {}) },
|
|
519
|
+
})
|
|
488
520
|
|
|
489
521
|
// ---- the reply ----------------------------------------------------------------
|
|
490
522
|
|
|
@@ -727,6 +759,17 @@ async function bodyText(input, init) {
|
|
|
727
759
|
return typeof b === "string" ? b : new TextDecoder().decode(b)
|
|
728
760
|
}
|
|
729
761
|
|
|
762
|
+
// signed is res saying what it means for the sign-in, as the built-in's
|
|
763
|
+
// answer did: a renewed job token cleared the mark whatever came of the
|
|
764
|
+
// request; without one, a success left the mark as it was.
|
|
765
|
+
function signed(res, renewed) {
|
|
766
|
+
const said = renewed ? "renewed" : res.ok ? "kept" : null
|
|
767
|
+
if (!said) return res
|
|
768
|
+
const headers = new Headers(res.headers)
|
|
769
|
+
headers.set("X-Magpie-Sign-In", said)
|
|
770
|
+
return new Response(res.body, { status: res.status, statusText: res.statusText, headers })
|
|
771
|
+
}
|
|
772
|
+
|
|
730
773
|
// ---- usage --------------------------------------------------------------------
|
|
731
774
|
//
|
|
732
775
|
// The account's allowance as magpie's built-in Qoder account shows it
|
|
@@ -848,6 +891,9 @@ export async function QoderAuthPlugin({ client }) {
|
|
|
848
891
|
}
|
|
849
892
|
// each account's listing, the model configs a request carries
|
|
850
893
|
const listings = new Map()
|
|
894
|
+
// the accounts fresh renewed: the built-in took the lapse mark off on a
|
|
895
|
+
// renewed job token (qoderPersist), whatever the request then met
|
|
896
|
+
const renewals = new WeakSet()
|
|
851
897
|
|
|
852
898
|
// fresh is the account with a live job token, refreshed and saved near
|
|
853
899
|
// its end.
|
|
@@ -856,9 +902,10 @@ export async function QoderAuthPlugin({ client }) {
|
|
|
856
902
|
const a = await getAuth()
|
|
857
903
|
if (a?.type !== "oauth" || !a.access || !a.uid) throw new SignInGone("Qoder: not signed in")
|
|
858
904
|
if (a.expires - Date.now() > REFRESH_LEAD) return a
|
|
859
|
-
const jt = await refreshJob(a.refresh)
|
|
905
|
+
const jt = await refreshJob(a.refresh, a.accountId || a.email || a.uid)
|
|
860
906
|
const next = { ...a, access: jt.token, refresh: jt.refresh_token, expires: expiresAt(jt) }
|
|
861
907
|
await client.auth.set({ path: { id: ID }, body: next })
|
|
908
|
+
renewals.add(next)
|
|
862
909
|
return next
|
|
863
910
|
})
|
|
864
911
|
|
|
@@ -882,10 +929,15 @@ export async function QoderAuthPlugin({ client }) {
|
|
|
882
929
|
return dt.token
|
|
883
930
|
})
|
|
884
931
|
|
|
885
|
-
// usage is the account's allowance, magpie's own hook
|
|
932
|
+
// usage is the account's allowance, magpie's own hook. signIn is what the read means for the sign-in, as the built-in's did:
|
|
933
|
+
// a refused job refresh marks it, a renewed one clears it, and nothing
|
|
934
|
+
// else does — not a clean read, nor a refused device token, which serves
|
|
935
|
+
// only the account pages
|
|
886
936
|
const usage = async (getAuth) => {
|
|
937
|
+
let signIn = "kept"
|
|
887
938
|
try {
|
|
888
939
|
const cred = await fresh(getAuth)
|
|
940
|
+
if (renewals.has(cred)) signIn = "renewed"
|
|
889
941
|
let env
|
|
890
942
|
try {
|
|
891
943
|
env = await fetchUsage(cred.deviceToken)
|
|
@@ -893,14 +945,47 @@ export async function QoderAuthPlugin({ client }) {
|
|
|
893
945
|
if (!(e instanceof UsageStatus) || (e.status !== 401 && e.status !== 403)) throw e
|
|
894
946
|
env = await fetchUsage(await deviceToken(getAuth, cred.deviceToken))
|
|
895
947
|
}
|
|
896
|
-
return parseUsage(env)
|
|
948
|
+
return { ...parseUsage(env), signIn }
|
|
897
949
|
} catch (e) {
|
|
898
|
-
return { error: e?.message ?? String(e) }
|
|
950
|
+
return { error: e?.message ?? String(e), signIn: e?.expired ? "expired" : signIn }
|
|
899
951
|
}
|
|
900
952
|
}
|
|
901
953
|
|
|
902
954
|
const signedInError = (e) =>
|
|
903
|
-
errorResponse(
|
|
955
|
+
errorResponse({
|
|
956
|
+
status: e instanceof SignInGone ? 401 : 502,
|
|
957
|
+
message: String(e?.message ?? e).replace(/^Qoder: /, ""),
|
|
958
|
+
signIn: e?.expired ? "expired" : undefined,
|
|
959
|
+
})
|
|
960
|
+
|
|
961
|
+
// ask is the answer to a chat completion on the live account cred.
|
|
962
|
+
const ask = async (chat, cred, init) => {
|
|
963
|
+
let m
|
|
964
|
+
try {
|
|
965
|
+
m = (await models(cred)).find((x) => x.key === chat.model) ?? (await models(cred, true)).find((x) => x.key === chat.model)
|
|
966
|
+
} catch (e) {
|
|
967
|
+
// the built-in answered any failure to read the list 400
|
|
968
|
+
// (QoderModelOf), a refused one included, marking nothing
|
|
969
|
+
return errorResponse({ status: 400, message: e.message })
|
|
970
|
+
}
|
|
971
|
+
if (!m) return errorResponse({ status: 400, message: `unknown or disabled model "${chat.model}"` })
|
|
972
|
+
const wire = encodeBody(JSON.stringify(qoderBody(chat, m)))
|
|
973
|
+
const headers = {
|
|
974
|
+
...cosyHeaders(CHAT_URL, cred, wire),
|
|
975
|
+
Accept: "text/event-stream",
|
|
976
|
+
"Cache-Control": "no-cache",
|
|
977
|
+
"X-Model-Key": m.key,
|
|
978
|
+
"X-Model-Source": m.source,
|
|
979
|
+
}
|
|
980
|
+
let res
|
|
981
|
+
try {
|
|
982
|
+
res = await fetch(CHAT_URL, { method: "POST", headers, body: wire, signal: init.signal })
|
|
983
|
+
} catch (e) {
|
|
984
|
+
return errorResponse({ status: 502, message: e.message })
|
|
985
|
+
}
|
|
986
|
+
if (!res.ok) return errorResponse(failure(res.status, (await res.text()).slice(0, 1 << 20)))
|
|
987
|
+
return answer(res, chat)
|
|
988
|
+
}
|
|
904
989
|
|
|
905
990
|
return {
|
|
906
991
|
auth: {
|
|
@@ -915,12 +1000,12 @@ export async function QoderAuthPlugin({ client }) {
|
|
|
915
1000
|
async fetch(input, init = {}) {
|
|
916
1001
|
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url
|
|
917
1002
|
if (!/\/chat\/completions$/.test(new URL(url).pathname))
|
|
918
|
-
return errorResponse({ status: 404, message: "
|
|
1003
|
+
return errorResponse({ status: 404, message: "only chat completions are served" })
|
|
919
1004
|
let chat
|
|
920
1005
|
try {
|
|
921
1006
|
chat = JSON.parse(await bodyText(input, init))
|
|
922
1007
|
} catch {
|
|
923
|
-
return errorResponse({ status: 400, message: "
|
|
1008
|
+
return errorResponse({ status: 400, message: "a request that isn't JSON" })
|
|
924
1009
|
}
|
|
925
1010
|
let cred
|
|
926
1011
|
try {
|
|
@@ -928,29 +1013,7 @@ export async function QoderAuthPlugin({ client }) {
|
|
|
928
1013
|
} catch (e) {
|
|
929
1014
|
return signedInError(e)
|
|
930
1015
|
}
|
|
931
|
-
|
|
932
|
-
try {
|
|
933
|
-
m = (await models(cred)).find((x) => x.key === chat.model) ?? (await models(cred, true)).find((x) => x.key === chat.model)
|
|
934
|
-
} catch (e) {
|
|
935
|
-
return errorResponse({ status: 502, message: e.message })
|
|
936
|
-
}
|
|
937
|
-
if (!m) return errorResponse({ status: 400, message: `Qoder: unknown or disabled model "${chat.model}"` })
|
|
938
|
-
const wire = encodeBody(JSON.stringify(qoderBody(chat, m)))
|
|
939
|
-
const headers = {
|
|
940
|
-
...cosyHeaders(CHAT_URL, cred, wire),
|
|
941
|
-
Accept: "text/event-stream",
|
|
942
|
-
"Cache-Control": "no-cache",
|
|
943
|
-
"X-Model-Key": m.key,
|
|
944
|
-
"X-Model-Source": m.source,
|
|
945
|
-
}
|
|
946
|
-
let res
|
|
947
|
-
try {
|
|
948
|
-
res = await fetch(CHAT_URL, { method: "POST", headers, body: wire, signal: init.signal })
|
|
949
|
-
} catch (e) {
|
|
950
|
-
return errorResponse({ status: 502, message: "Qoder: " + e.message })
|
|
951
|
-
}
|
|
952
|
-
if (!res.ok) return errorResponse(failure(res.status, (await res.text()).slice(0, 1 << 20)))
|
|
953
|
-
return answer(res, chat)
|
|
1016
|
+
return signed(await ask(chat, cred, init), renewals.has(cred))
|
|
954
1017
|
},
|
|
955
1018
|
}
|
|
956
1019
|
},
|
|
@@ -978,7 +1041,7 @@ export async function QoderAuthPlugin({ client }) {
|
|
|
978
1041
|
const ms = await models(cred, true)
|
|
979
1042
|
if (!ms.length) return provider.models
|
|
980
1043
|
return Object.fromEntries(
|
|
981
|
-
ms.map((m) => [m.key, runtimeModel({ id: m.key, name: m.name, context: m.context, images: m.images, efforts: m.thinks ? m.efforts : [] })]),
|
|
1044
|
+
ms.map((m) => [m.key, runtimeModel({ id: m.key, name: m.name, context: m.context, images: m.images, efforts: m.thinks ? m.efforts : [], free: m.free })]),
|
|
982
1045
|
)
|
|
983
1046
|
} catch {
|
|
984
1047
|
return provider.models
|
|
@@ -989,4 +1052,4 @@ export async function QoderAuthPlugin({ client }) {
|
|
|
989
1052
|
}
|
|
990
1053
|
|
|
991
1054
|
// for tests
|
|
992
|
-
export const _internal = { parseUsage, gfmt, when }
|
|
1055
|
+
export const _internal = { parseUsage, gfmt, when, failure, modelInfo }
|