@magpie-community/opencode-qoder-auth 0.2.27 → 0.2.30
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 +37 -3
- package/index.mjs +245 -42
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -139,7 +139,7 @@ itself.
|
|
|
139
139
|
|---|---|
|
|
140
140
|
| A refused sign-in (no code, or code 105) | 401 |
|
|
141
141
|
| A quota (codes 110, 112–119, 122) | 429 |
|
|
142
|
-
| Busy or queued for the model (code 10605) |
|
|
142
|
+
| Busy or queued for the model (code 10605) | Since 0.2.28 the request waits in Qoder's queue as Qoder's client does (below), then is sent again; past the wait, 503 with `Retry-After` from Qoder's `retryAfterSeconds` (30s when it gives none). The sign-in is untouched |
|
|
143
143
|
| A refusal of the request itself (codes 406, 416, 430, 80411, 80412, 100400–100403) | 400, with Qoder's words |
|
|
144
144
|
| "Error in upstream response", the model's own vendor behind Qoder failing, which Qoder sends as a 400 (since 0.2.24) | 502, so magpie asks again or another account; the sign-in is untouched |
|
|
145
145
|
| A failure before the answer starts | Qoder's status |
|
|
@@ -285,8 +285,28 @@ the account's credentials.
|
|
|
285
285
|
international account gets no daily claim until Qoder is installed.
|
|
286
286
|
- A helper that is there but can't be read, or gives an invalid identity,
|
|
287
287
|
is `failed` and asked again later, rather than a false "no activity"
|
|
288
|
-
result.
|
|
289
|
-
|
|
288
|
+
result.
|
|
289
|
+
|
|
290
|
+
Qoder CN (since 0.2.30) sends its own device identity the same way when
|
|
291
|
+
Qoder CN is installed: its desktop 0.4.9 and CLI (`qoderclicn`) both ask
|
|
292
|
+
Qoder CN's `runtime-info` for environment `0` (the international one is
|
|
293
|
+
`3`) and send it on the campaigns pages (yetone/magpie#2911, #304).
|
|
294
|
+
|
|
295
|
+
- The plugin finds Qoder CN desktop's `resources/umid` (`Qoder CN.app` on
|
|
296
|
+
macOS, `%LOCALAPPDATA%\Programs\Qoder CN` or `%ProgramFiles%\Qoder CN`
|
|
297
|
+
on Windows, `/opt/Qoder-CN` on Linux) or the runtime its CLI caches in
|
|
298
|
+
`~/.qoder-cn/.bin` (`QODERCN_CONFIG_DIR`, `QODERCN_CLI_HOME` and
|
|
299
|
+
`QODERCN_CONFIG_DIR_NAME` move it). `QODERCN_RUNTIME_INFO` names
|
|
300
|
+
another location; `QODER_RUNTIME_INFO` is only the international one's.
|
|
301
|
+
- Without it the requests go out as before, as Qoder CN's desktop does
|
|
302
|
+
when it can't read the identity, and a claim the list still offers is
|
|
303
|
+
made. When the list then offers no daily claim, the check-in is
|
|
304
|
+
`ineligible` for the day with a message to install Qoder CN desktop (or
|
|
305
|
+
sign in to its CLI); a helper that is there but can't be read makes it
|
|
306
|
+
`failed`, asked again later.
|
|
307
|
+
- Not tried with a real Qoder CN account: whether Qoder CN now hides the
|
|
308
|
+
daily claim from a request without the identity, as the international
|
|
309
|
+
site does, is inferred from its clients, not seen.
|
|
290
310
|
|
|
291
311
|
- **List:** `GET <openapi>/sash/api/v1/me/campaigns`: `campaigns[]` with
|
|
292
312
|
`campaignId`, `actionType`, `claimStatus` (`CLAIMABLE`, `CLAIMED`),
|
|
@@ -319,6 +339,20 @@ That parser fix does not remove the provider's limit or prove that every
|
|
|
319
339
|
live 401 had the same cause. A timeout likewise does not establish an
|
|
320
340
|
empty wallet or an expired login.
|
|
321
341
|
|
|
342
|
+
Since **0.2.28** a queued request waits its turn as Qoder's own client
|
|
343
|
+
(Qoder.app 0.4.3's agent worker) waits it: it asks
|
|
344
|
+
`/api/v2/service/ask/queue/status` with the request's `requestSetId`, the
|
|
345
|
+
model and the plan's `queueType`, at once and then every
|
|
346
|
+
`retryAfterSeconds` (held to 0.5–30s, 30s when unsaid), and once
|
|
347
|
+
`isQueued` is false it waits that state's `retryAfterSeconds` and sends the
|
|
348
|
+
same request again (up to 10 times). Qoder's client waits up to an hour;
|
|
349
|
+
the plugin waits at most 4 minutes from the request's start, since nothing
|
|
350
|
+
reaches the agent meanwhile (magpie keeps a stream alive with comments for
|
|
351
|
+
5 minutes of silence). Three failed status asks in a row, a 404, or a
|
|
352
|
+
cancelled request end the wait early. Qoder's client also reports the end
|
|
353
|
+
of its turn to `/api/v2/service/ask/finish`; the plugin doesn't, and
|
|
354
|
+
Qoder releases that lease on its own TTL. `queue.test.mjs` has the wait.
|
|
355
|
+
|
|
322
356
|
For a bounded offline reproduction, run `packages/qoder/busy.test.mjs`
|
|
323
357
|
from #185 in an isolated temporary HOME. It compares plain queue JSON,
|
|
324
358
|
BOM-prefixed JSON and known message wrappers, keeping explicit login and
|
package/index.mjs
CHANGED
|
@@ -956,7 +956,7 @@ function failure(status, text) {
|
|
|
956
956
|
const model = queue?.modelKey ? ` for ${queue.modelKey}` : ""
|
|
957
957
|
const tier = queue?.queueType ? ` (this plan's queue: ${queue.queueType})` : ""
|
|
958
958
|
const what = queue?.isQueued === true ? "queued the request" : "is busy"
|
|
959
|
-
return { status: 503, message: `Qoder ${what}${model}${tier}; try again in ${wait}s`, retryAfter: wait, signIn: "" }
|
|
959
|
+
return { status: 503, message: `Qoder ${what}${model}${tier}; try again in ${wait}s`, retryAfter: wait, signIn: "", queue: queue ?? {} }
|
|
960
960
|
}
|
|
961
961
|
if (QUOTA_CODES.has(code)) return { status: 429, message: "usage limit reached: " + msg, signIn: "" }
|
|
962
962
|
if (REQUEST_CODES.has(code)) return { status: 400, message: `Qoder refused the request (${code}): ${msg}`, signIn: "" }
|
|
@@ -987,6 +987,148 @@ const errorResponse = ({ status, message, signIn = status === 401 ? "kept" : und
|
|
|
987
987
|
},
|
|
988
988
|
})
|
|
989
989
|
|
|
990
|
+
// ---- Qoder's queue -------------------------------------------------------------
|
|
991
|
+
|
|
992
|
+
// A chat Qoder answers with its queue code (10605) is waited out as Qoder's
|
|
993
|
+
// own client waits it (qoder-agent-sdk's worker in Qoder.app 0.4.3, its
|
|
994
|
+
// "[ModelQueue]" path): it asks GET /api/v2/service/ask/queue/status with
|
|
995
|
+
// the request's requestSetId, the model and the plan's queueType, at once
|
|
996
|
+
// and then every retryAfterSeconds (held to 0.5s..30s, 30s when unsaid),
|
|
997
|
+
// while the state says isQueued or serviceAvailable false. Once isQueued is
|
|
998
|
+
// false it waits that state's retryAfterSeconds, if any, and sends the same
|
|
999
|
+
// request again, request_set_id and all, up to 10 times. Three failed asks
|
|
1000
|
+
// in a row, a 404 (no queue there) or a state with neither field end the
|
|
1001
|
+
// wait. Qoder's client waits up to an hour; here the whole of it is held
|
|
1002
|
+
// under QUEUE.budget from the request's start, since nothing reaches the
|
|
1003
|
+
// agent meanwhile: magpie keeps a stream's agent alive with comments for 5
|
|
1004
|
+
// minutes of a vendor's silence, and then leaves it to the agent's own
|
|
1005
|
+
// timeout. Past the budget it is the 503 with Retry-After it was before, so
|
|
1006
|
+
// magpie may ask another account. Qoder's client also tells
|
|
1007
|
+
// /api/v2/service/ask/finish when its turn ends; that is left out, and its
|
|
1008
|
+
// own log says Qoder's TTL releases the lease when finish is not heard.
|
|
1009
|
+
const QUEUE_STATUS_PATH = "/algo/api/v2/service/ask/queue/status"
|
|
1010
|
+
const QUEUE = {
|
|
1011
|
+
budget: 4 * 60 * 1000, // the longest a request is held, its first answer's wait included
|
|
1012
|
+
shortest: 500,
|
|
1013
|
+
longest: 30_000,
|
|
1014
|
+
unsaid: 30_000, // a pause when the queue gives no retryAfterSeconds
|
|
1015
|
+
recoveries: 10, // the queue waited out this many times, at most, for one request
|
|
1016
|
+
failures: 3, // status asks failing in a row that end the wait
|
|
1017
|
+
ask: 30_000, // the longest one status ask may take
|
|
1018
|
+
now: () => Date.now(),
|
|
1019
|
+
pause: (ms, signal) =>
|
|
1020
|
+
new Promise((ok, no) => {
|
|
1021
|
+
if (signal?.aborted) return no(signal.reason ?? new DOMException("aborted", "AbortError"))
|
|
1022
|
+
const t = setTimeout(() => {
|
|
1023
|
+
signal?.removeEventListener("abort", stop)
|
|
1024
|
+
ok()
|
|
1025
|
+
}, ms)
|
|
1026
|
+
const stop = () => {
|
|
1027
|
+
clearTimeout(t)
|
|
1028
|
+
no(signal.reason ?? new DOMException("aborted", "AbortError"))
|
|
1029
|
+
}
|
|
1030
|
+
signal?.addEventListener("abort", stop, { once: true })
|
|
1031
|
+
}),
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
// queuePause is how long to wait before asking the queue again: the
|
|
1035
|
+
// queue's retryAfterSeconds held to 0.5s..30s, or 30s when it said none.
|
|
1036
|
+
function queuePause(seconds) {
|
|
1037
|
+
const n = Number(seconds)
|
|
1038
|
+
const ms = Number.isFinite(n) ? n * 1000 : QUEUE.unsaid
|
|
1039
|
+
return Math.min(QUEUE.longest, Math.max(QUEUE.shortest, ms))
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
class QueueEnd extends Error {}
|
|
1043
|
+
|
|
1044
|
+
// queueState is the queue's state in a status reply, found as Qoder's
|
|
1045
|
+
// client finds it: the object, or one under data/result/message/body, as
|
|
1046
|
+
// an object or a JSON string, that says isQueued or serviceAvailable.
|
|
1047
|
+
function queueState(text) {
|
|
1048
|
+
let root
|
|
1049
|
+
try {
|
|
1050
|
+
root = JSON.parse(text)
|
|
1051
|
+
} catch {
|
|
1052
|
+
return undefined
|
|
1053
|
+
}
|
|
1054
|
+
const todo = [root]
|
|
1055
|
+
const seen = new Set()
|
|
1056
|
+
while (todo.length) {
|
|
1057
|
+
const o = todo.shift()
|
|
1058
|
+
if (!o || typeof o !== "object" || Array.isArray(o) || seen.has(o)) continue
|
|
1059
|
+
seen.add(o)
|
|
1060
|
+
const flag = (v) => (typeof v === "boolean" ? v : v === "true" ? true : v === "false" ? false : undefined)
|
|
1061
|
+
const isQueued = flag(o.isQueued)
|
|
1062
|
+
const serviceAvailable = flag(o.serviceAvailable)
|
|
1063
|
+
if (isQueued !== undefined || serviceAvailable !== undefined) {
|
|
1064
|
+
const num = (v) => (v == null || v === "" || !Number.isFinite(Number(v)) ? undefined : Number(v))
|
|
1065
|
+
const str = (v) => (typeof v === "string" && v ? v : undefined)
|
|
1066
|
+
return { isQueued, serviceAvailable, modelKey: str(o.modelKey), queueType: str(o.queueType), retryAfterSeconds: num(o.retryAfterSeconds) }
|
|
1067
|
+
}
|
|
1068
|
+
for (const k of ["data", "result", "message", "body"]) {
|
|
1069
|
+
const v = o[k]
|
|
1070
|
+
if (v && typeof v === "object") todo.push(v)
|
|
1071
|
+
else if (typeof v === "string")
|
|
1072
|
+
try {
|
|
1073
|
+
todo.push(JSON.parse(v))
|
|
1074
|
+
} catch {}
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
return undefined
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
// queueStatus asks Qoder's queue where the request stands.
|
|
1081
|
+
async function queueStatus(api, cred, { requestSetId, modelKey, queueType, requestId, sessionId }, signal, deadline) {
|
|
1082
|
+
const q = new URLSearchParams({ requestSetId, modelKey })
|
|
1083
|
+
if (queueType) q.set("queueType", queueType)
|
|
1084
|
+
const url = `${api}${QUEUE_STATUS_PATH}?${q}`
|
|
1085
|
+
const headers = { ...cosyHeaders(url, cred, ""), "X-Request-ID": requestId, "X-Session-ID": sessionId }
|
|
1086
|
+
delete headers["Content-Type"]
|
|
1087
|
+
const left = Math.min(QUEUE.ask, deadline - QUEUE.now())
|
|
1088
|
+
if (left <= 0) throw new QueueEnd("the queue's wait ran out")
|
|
1089
|
+
const res = await fetch(url, { headers, signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(left)]) : AbortSignal.timeout(left) })
|
|
1090
|
+
const text = await res.text()
|
|
1091
|
+
if (res.status === 404) throw new QueueEnd("Qoder has no queue status here")
|
|
1092
|
+
if (!res.ok) throw new Error(`queue status: HTTP ${res.status}`)
|
|
1093
|
+
const st = queueState(text)
|
|
1094
|
+
if (!st) throw new QueueEnd("the queue status said nothing of the queue")
|
|
1095
|
+
return st
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
// queueWait waits in Qoder's queue for the request, from the state its
|
|
1099
|
+
// refusal gave, until the queue lets it through (true) or the wait ends
|
|
1100
|
+
// (false). A cancelled request throws.
|
|
1101
|
+
async function queueWait(api, cred, ask, first, signal, deadline) {
|
|
1102
|
+
let q = first ?? {}
|
|
1103
|
+
let asked = false
|
|
1104
|
+
let failed = 0
|
|
1105
|
+
for (;;) {
|
|
1106
|
+
if (signal?.aborted) throw signal.reason ?? new DOMException("aborted", "AbortError")
|
|
1107
|
+
if (QUEUE.now() >= deadline) return false
|
|
1108
|
+
if (asked) await QUEUE.pause(Math.min(queuePause(q.retryAfterSeconds), Math.max(0, deadline - QUEUE.now())), signal)
|
|
1109
|
+
asked = true
|
|
1110
|
+
if (QUEUE.now() >= deadline) return false
|
|
1111
|
+
let st
|
|
1112
|
+
try {
|
|
1113
|
+
st = await queueStatus(api, cred, { ...ask, modelKey: q.modelKey ?? ask.modelKey, queueType: q.queueType ?? ask.queueType }, signal, deadline)
|
|
1114
|
+
} catch (e) {
|
|
1115
|
+
if (signal?.aborted) throw signal.reason ?? e
|
|
1116
|
+
if (e instanceof QueueEnd) return false
|
|
1117
|
+
if (++failed >= QUEUE.failures) return false
|
|
1118
|
+
q = { ...q, retryAfterSeconds: undefined }
|
|
1119
|
+
continue
|
|
1120
|
+
}
|
|
1121
|
+
failed = 0
|
|
1122
|
+
q = { ...st, modelKey: st.modelKey ?? q.modelKey, queueType: st.queueType ?? q.queueType }
|
|
1123
|
+
if (st.isQueued === false) {
|
|
1124
|
+
if (st.retryAfterSeconds > 0) await QUEUE.pause(Math.min(queuePause(st.retryAfterSeconds), Math.max(0, deadline - QUEUE.now())), signal)
|
|
1125
|
+
return QUEUE.now() < deadline
|
|
1126
|
+
}
|
|
1127
|
+
if (st.serviceAvailable === false || st.isQueued === true) continue
|
|
1128
|
+
return false
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
|
|
990
1132
|
// ---- the reply ----------------------------------------------------------------
|
|
991
1133
|
|
|
992
1134
|
const CALL_OPEN = "\x3ctool_call\x3e"
|
|
@@ -1232,11 +1374,17 @@ async function* events(body) {
|
|
|
1232
1374
|
// answer turns Qoder's reply into the chat completion OpenAI's API gives.
|
|
1233
1375
|
// A failure before the answer keeps its status; one after it ends the
|
|
1234
1376
|
// stream with an error.
|
|
1235
|
-
|
|
1377
|
+
// opened is a chat reply's events and the first of them, read before
|
|
1378
|
+
// anything is answered: a refusal in the stream (Qoder's queue among them)
|
|
1379
|
+
// comes first.
|
|
1380
|
+
async function opened(res) {
|
|
1381
|
+
const it = events(res.body)
|
|
1382
|
+
return { it, first: await it.next() }
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
async function answer({ it, first }, chat) {
|
|
1236
1386
|
const id = "chatcmpl-" + hexID()
|
|
1237
1387
|
const created = Math.floor(Date.now() / 1000)
|
|
1238
|
-
const it = events(res.body)
|
|
1239
|
-
const first = await it.next()
|
|
1240
1388
|
if (first.value?.error) return errorResponse(first.value.error)
|
|
1241
1389
|
if (first.done) return errorResponse({ status: 502, message: "Qoder ended without an answer" })
|
|
1242
1390
|
|
|
@@ -1378,23 +1526,45 @@ const CAMPAIGN_MACHINE_TTL = 60 * 60 * 1000 // Qoder's desktop client renews its
|
|
|
1378
1526
|
// (or .qoder): a file runtime-info-<platform>-<arch>-<hash> on macOS and
|
|
1379
1527
|
// Linux, a folder umid-win32-<arch>-<hash> holding runtime-info.exe on
|
|
1380
1528
|
// Windows.
|
|
1381
|
-
|
|
1382
|
-
|
|
1529
|
+
//
|
|
1530
|
+
// Qoder CN keeps its own (Qoder CN desktop 0.4.9's packages, qoderclicn
|
|
1531
|
+
// 1.1.77): Qoder CN.app on macOS (its installer's expectedBundleName),
|
|
1532
|
+
// /opt/Qoder-CN on Linux (the .deb), Qoder CN on Windows (the user
|
|
1533
|
+
// installer's "Uninstall Qoder CN.exe", electron-builder's
|
|
1534
|
+
// Programs\<productName>); its CLI's config is QODERCN_CONFIG_DIR, else
|
|
1535
|
+
// QODERCN_CLI_HOME (or home) joined with QODERCN_CONFIG_DIR_NAME (or
|
|
1536
|
+
// .qoder-cn). Both ask the helper for environment 0, the international
|
|
1537
|
+
// ones for 3 (the desktop's `regionCode === "global" ? 3 : 0`, the CLI's
|
|
1538
|
+
// UMID environment ONLINE = 0, US_ONLINE = 3).
|
|
1539
|
+
//
|
|
1540
|
+
// required: international Qoder lists the daily credits only on a request
|
|
1541
|
+
// carrying the identity. Qoder CN's desktop sends it when it can read it
|
|
1542
|
+
// and goes on without it when it can't, and a Qoder CN account was offered
|
|
1543
|
+
// and claimed the credits without one (0.2.21), so there it is sent when
|
|
1544
|
+
// Qoder CN is installed and left out when it isn't.
|
|
1545
|
+
const RUNTIMES = {
|
|
1546
|
+
qoder: { name: "Qoder", environment: "3", override: "QODER_RUNTIME_INFO", app: "Qoder", linux: "Qoder", cli: "QODER_", dir: ".qoder", required: true },
|
|
1547
|
+
"qoder-cn": { name: "Qoder CN", environment: "0", override: "QODERCN_RUNTIME_INFO", app: "Qoder CN", linux: "Qoder-CN", cli: "QODERCN_", dir: ".qoder-cn", required: false },
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
async function campaignRuntime({ home = homedir(), platform = process.platform, arch = process.arch, env = process.env, opt = "/opt", runtime = RUNTIMES.qoder } = {}) {
|
|
1551
|
+
if (env[runtime.override]) return resolve(env[runtime.override])
|
|
1383
1552
|
const name = platform === "win32" ? "runtime-info.exe" : "runtime-info"
|
|
1384
1553
|
const isFile = async (path) => (await stat(path).catch(() => null))?.isFile() ?? false
|
|
1385
1554
|
const paths = []
|
|
1386
1555
|
if (platform === "win32") {
|
|
1387
1556
|
const local = env.LOCALAPPDATA || join(home, "AppData", "Local")
|
|
1388
|
-
paths.push(join(local, "Programs",
|
|
1557
|
+
paths.push(join(local, "Programs", runtime.app, "resources", "umid", name))
|
|
1389
1558
|
// the machine-wide installer, as VS Code's system setup places it
|
|
1390
|
-
for (const dir of [env.ProgramFiles || env.PROGRAMFILES, env.ProgramW6432]) if (dir) paths.push(join(dir,
|
|
1559
|
+
for (const dir of [env.ProgramFiles || env.PROGRAMFILES, env.ProgramW6432]) if (dir) paths.push(join(dir, runtime.app, "resources", "umid", name))
|
|
1391
1560
|
} else if (platform === "darwin") {
|
|
1392
|
-
for (const dir of ["/Applications", join(home, "Applications")]) paths.push(join(dir, "
|
|
1561
|
+
for (const dir of ["/Applications", join(home, "Applications")]) paths.push(join(dir, runtime.app + ".app", "Contents", "Resources", "umid", name))
|
|
1393
1562
|
} else if (platform === "linux") {
|
|
1394
|
-
paths.push(join(opt,
|
|
1563
|
+
paths.push(join(opt, runtime.linux, "resources", "umid", name))
|
|
1395
1564
|
}
|
|
1396
1565
|
for (const path of paths) if (await isFile(path)) return path
|
|
1397
|
-
const
|
|
1566
|
+
const cli = (key) => env[runtime.cli + key]
|
|
1567
|
+
const config = cli("CONFIG_DIR") || join(cli("CLI_HOME") || env.GEMINI_CLI_HOME || home, cli("CONFIG_DIR_NAME") || runtime.dir)
|
|
1398
1568
|
const cache = join(config, ".bin")
|
|
1399
1569
|
const entries = await readdir(cache, { withFileTypes: true }).catch(() => [])
|
|
1400
1570
|
const found = []
|
|
@@ -1408,7 +1578,7 @@ async function campaignRuntime({ home = homedir(), platform = process.platform,
|
|
|
1408
1578
|
if (path && (await isFile(path))) found.push({ path, time: (await stat(path)).mtimeMs })
|
|
1409
1579
|
}
|
|
1410
1580
|
if (found.length) return found.sort((a, b) => b.time - a.time)[0].path
|
|
1411
|
-
throw new RuntimeMissing()
|
|
1581
|
+
throw new RuntimeMissing(runtime)
|
|
1412
1582
|
}
|
|
1413
1583
|
|
|
1414
1584
|
// RuntimeMissing: no Qoder desktop or CLI on this machine to read the
|
|
@@ -1418,19 +1588,21 @@ async function campaignRuntime({ home = homedir(), platform = process.platform,
|
|
|
1418
1588
|
// never makes up. Asking again won't change that until the user installs
|
|
1419
1589
|
// Qoder, so the check-in says what to do once instead of "tries again later".
|
|
1420
1590
|
class RuntimeMissing extends Error {
|
|
1421
|
-
constructor() {
|
|
1591
|
+
constructor(runtime = RUNTIMES.qoder) {
|
|
1592
|
+
const { name, override } = runtime
|
|
1422
1593
|
super(
|
|
1423
|
-
|
|
1594
|
+
`${name} check-in: device identity runtime not found. ${name} gives the daily credits only to a device its own app identifies, and this machine has neither ${name} desktop nor ${name} CLI to read that identity from. Install ${name} desktop (or install ${name} CLI and sign in to it once), or set ${override} to its runtime-info executable; magpie checks in again the next day`,
|
|
1424
1595
|
)
|
|
1425
1596
|
}
|
|
1426
1597
|
}
|
|
1427
1598
|
|
|
1428
|
-
// Read the SDK identity as Qoder does: environment 3 is international
|
|
1429
|
-
// Windows and macOS the uid goes over stdin, never into the
|
|
1430
|
-
|
|
1599
|
+
// Read the SDK identity as Qoder does: environment 3 is international, 0
|
|
1600
|
+
// Qoder CN; on Windows and macOS the uid goes over stdin, never into the
|
|
1601
|
+
// command line.
|
|
1602
|
+
async function campaignMachine(uid, runtime = RUNTIMES.qoder) {
|
|
1431
1603
|
if (!uid) throw new Error("Qoder check-in: device identity needs the account uid; sign in again")
|
|
1432
|
-
const file = await campaignRuntime()
|
|
1433
|
-
const args = process.platform === "linux" ? [
|
|
1604
|
+
const file = await campaignRuntime({ runtime })
|
|
1605
|
+
const args = process.platform === "linux" ? [runtime.environment] : [runtime.environment, "--account-stdin"]
|
|
1434
1606
|
const text = await new Promise((resolve, reject) => {
|
|
1435
1607
|
const child = execFile(file, args, { timeout: 20_000, windowsHide: true, maxBuffer: 1 << 20 }, (err, out) => {
|
|
1436
1608
|
if (err) reject(err)
|
|
@@ -1439,7 +1611,7 @@ async function campaignMachine(uid) {
|
|
|
1439
1611
|
child.stdin?.on("error", () => {})
|
|
1440
1612
|
child.stdin?.end(process.platform === "linux" ? undefined : JSON.stringify({ account: uid }) + "\n")
|
|
1441
1613
|
}).catch(() => {
|
|
1442
|
-
throw new Error(
|
|
1614
|
+
throw new Error(`${runtime.name} check-in: could not read device identity; restart or update ${runtime.name}, or check ${runtime.override}`)
|
|
1443
1615
|
})
|
|
1444
1616
|
try {
|
|
1445
1617
|
const identity = JSON.parse(text.trim().split(/\r?\n/, 1)[0])
|
|
@@ -1451,7 +1623,7 @@ async function campaignMachine(uid) {
|
|
|
1451
1623
|
}
|
|
1452
1624
|
return headers
|
|
1453
1625
|
} catch {
|
|
1454
|
-
throw new Error(
|
|
1626
|
+
throw new Error(`${runtime.name} check-in: invalid device identity from ${runtime.name}; restart or update ${runtime.name}`)
|
|
1455
1627
|
}
|
|
1456
1628
|
}
|
|
1457
1629
|
|
|
@@ -1606,12 +1778,13 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
1606
1778
|
const ID = site.id
|
|
1607
1779
|
// The SDK identity is account-specific. Share an in-flight read, refresh
|
|
1608
1780
|
// after an hour, and leave a failed read available for the next attempt.
|
|
1781
|
+
const runtime = RUNTIMES[ID]
|
|
1609
1782
|
const campaignMachines = new Map()
|
|
1610
1783
|
const machineForCampaign = async (uid) => {
|
|
1611
1784
|
const now = Date.now()
|
|
1612
1785
|
let entry = campaignMachines.get(uid)
|
|
1613
1786
|
if (!entry || now - entry.at >= CAMPAIGN_MACHINE_TTL) {
|
|
1614
|
-
entry = { at: now, promise: campaignMachine(uid) }
|
|
1787
|
+
entry = { at: now, promise: campaignMachine(uid, runtime) }
|
|
1615
1788
|
campaignMachines.set(uid, entry)
|
|
1616
1789
|
}
|
|
1617
1790
|
try {
|
|
@@ -1621,6 +1794,16 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
1621
1794
|
throw e
|
|
1622
1795
|
}
|
|
1623
1796
|
}
|
|
1797
|
+
// campaignIdentity: the headers a campaigns request carries, and why
|
|
1798
|
+
// there are none where the site goes on without them (Qoder CN).
|
|
1799
|
+
const campaignIdentity = async (uid) => {
|
|
1800
|
+
if (runtime.required) return { machine: await machineForCampaign(uid) }
|
|
1801
|
+
try {
|
|
1802
|
+
return { machine: await machineForCampaign(uid) }
|
|
1803
|
+
} catch (e) {
|
|
1804
|
+
return { machine: {}, unread: e }
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1624
1807
|
// serializes checking, rotating and saving tokens: a refresh token is
|
|
1625
1808
|
// spent once, so two refreshes would spend it twice
|
|
1626
1809
|
let lock = Promise.resolve()
|
|
@@ -1779,7 +1962,7 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
1779
1962
|
if (!url) return errorResponse({ status: 404, message: "only chat completions are served" })
|
|
1780
1963
|
const method = String(init.method ?? input?.method ?? "GET").toUpperCase()
|
|
1781
1964
|
try {
|
|
1782
|
-
const machine =
|
|
1965
|
+
const { machine } = await campaignIdentity(cred.uid)
|
|
1783
1966
|
// A PAT account has no device token: its job token signs the campaigns
|
|
1784
1967
|
// pages too, as it does the usage read. Nothing is rotated on that
|
|
1785
1968
|
// path — the refusal is the answer.
|
|
@@ -1808,7 +1991,7 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
1808
1991
|
const base = campaignTarget(site, siteOf(site, cred), site.openapi + CAMPAIGNS)
|
|
1809
1992
|
if (!base) return { outcome: "inactive", message: "no campaigns page for this account" }
|
|
1810
1993
|
try {
|
|
1811
|
-
const machine
|
|
1994
|
+
const { machine, unread } = await campaignIdentity(cred.uid)
|
|
1812
1995
|
// A PAT account has no device token: the campaigns pages take the job
|
|
1813
1996
|
// token the exchange gave, as the usage endpoint does. A live Qoder CN
|
|
1814
1997
|
// PAT account answered 200 to the list and CLAIMED to a claim, so the
|
|
@@ -1846,7 +2029,13 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
1846
2029
|
const amount = Number(done.benefit?.amount)
|
|
1847
2030
|
return { outcome: "done", ...(Number.isFinite(amount) && amount > 0 ? { credit: amount } : {}) }
|
|
1848
2031
|
}
|
|
1849
|
-
|
|
2032
|
+
if (campaigns.length) return { outcome: "failed", message: "campaigns: today's credits aren't offered yet" }
|
|
2033
|
+
// Asked without Qoder CN's identity and offered nothing: what an
|
|
2034
|
+
// international list says to a request without one, so say why
|
|
2035
|
+
// (no Qoder CN on this machine) or ask again (it didn't answer).
|
|
2036
|
+
if (unread instanceof RuntimeMissing) return { outcome: "ineligible", message: unread.message }
|
|
2037
|
+
if (unread) return { outcome: "failed", message: unread.message }
|
|
2038
|
+
return { outcome: "inactive" }
|
|
1850
2039
|
}
|
|
1851
2040
|
const claimRes = await call(base.replace(/\/$/, "") + "/" + encodeURIComponent(campaign.campaignId) + "/claim", "POST", "{}")
|
|
1852
2041
|
let claim = {}
|
|
@@ -1889,23 +2078,37 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
1889
2078
|
return errorResponse({ status: 502, message: e?.message ?? String(e) })
|
|
1890
2079
|
}
|
|
1891
2080
|
if (!m) return errorResponse({ status: 400, message: `unknown or disabled model "${chat.model}"` })
|
|
1892
|
-
const
|
|
1893
|
-
const
|
|
1894
|
-
const
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
2081
|
+
const api = siteOf(site, cred).api
|
|
2082
|
+
const CHAT_URL = api + CHAT_PATH
|
|
2083
|
+
const body = qoderBody(chat, m)
|
|
2084
|
+
const wire = encodeBody(JSON.stringify(body))
|
|
2085
|
+
// Qoder's queue is waited out (queueWait) and the same request, its
|
|
2086
|
+
// request_set_id the one the queue knows, sent again
|
|
2087
|
+
const deadline = QUEUE.now() + QUEUE.budget
|
|
2088
|
+
const ask = { requestSetId: body.request_set_id, requestId: body.request_id, sessionId: body.session_id, modelKey: m.key }
|
|
2089
|
+
for (let waited = 0; ; waited++) {
|
|
2090
|
+
const headers = {
|
|
2091
|
+
...cosyHeaders(CHAT_URL, cred, wire),
|
|
2092
|
+
Accept: "text/event-stream",
|
|
2093
|
+
"Cache-Control": "no-cache",
|
|
2094
|
+
"X-Model-Key": m.key,
|
|
2095
|
+
"X-Model-Source": m.source,
|
|
2096
|
+
}
|
|
2097
|
+
let res, open
|
|
2098
|
+
try {
|
|
2099
|
+
res = await fetch(CHAT_URL, { method: "POST", headers, body: wire, signal: init.signal })
|
|
2100
|
+
if (res.ok) open = await opened(res)
|
|
2101
|
+
} catch (e) {
|
|
2102
|
+
return errorResponse({ status: 502, message: e.message })
|
|
2103
|
+
}
|
|
2104
|
+
const f = !res.ok ? failure(res.status, (await res.text()).slice(0, 1 << 20)) : open.first.value?.error
|
|
2105
|
+
if (!f?.queue || waited >= QUEUE.recoveries) return f && !res.ok ? errorResponse(f) : answer(open, chat)
|
|
2106
|
+
try {
|
|
2107
|
+
if (!(await queueWait(api, cred, ask, f.queue, init.signal, deadline))) return errorResponse(f)
|
|
2108
|
+
} catch (e) {
|
|
2109
|
+
return errorResponse({ status: 502, message: e?.message ?? String(e) })
|
|
2110
|
+
}
|
|
1906
2111
|
}
|
|
1907
|
-
if (!res.ok) return errorResponse(failure(res.status, (await res.text()).slice(0, 1 << 20)))
|
|
1908
|
-
return answer(res, chat)
|
|
1909
2112
|
}
|
|
1910
2113
|
|
|
1911
2114
|
return {
|
|
@@ -2071,4 +2274,4 @@ export const QoderAuthPlugin = makePlugin(SITES.qoder)
|
|
|
2071
2274
|
export const QoderCNAuthPlugin = makePlugin(SITES["qoder-cn"])
|
|
2072
2275
|
|
|
2073
2276
|
// for tests
|
|
2074
|
-
export const _internal = { campaignRuntime, campaignPage, campaignTarget, vpcInstance, vpcSite, parseUsage, gfmt, when, failure, modelInfo, qoderBody, qoderMessages, cacheMarked, sessionOf, SITES, decodeBody, events }
|
|
2277
|
+
export const _internal = { campaignRuntime, RUNTIMES, campaignPage, campaignTarget, vpcInstance, vpcSite, parseUsage, gfmt, when, failure, modelInfo, qoderBody, qoderMessages, cacheMarked, sessionOf, SITES, decodeBody, events, QUEUE, queueState }
|
package/package.json
CHANGED