@magpie-community/opencode-qoder-auth 0.2.28 → 0.2.31
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 +36 -6
- package/index.mjs +150 -35
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -98,10 +98,20 @@ itself.
|
|
|
98
98
|
`cache_control: ephemeral` (nothing is marked when the last turn is a
|
|
99
99
|
tool's result), user turns carry their blocks as `contents` too, and each
|
|
100
100
|
request names its conversation in `session_id`. That id is a hash of
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
`
|
|
101
|
+
the session the agent names (since 0.2.31, #330: DSH's
|
|
102
|
+
`x-deepseek-harness-session-id` and the other agents' own session
|
|
103
|
+
headers, which magpie hands the plugin's `chat.headers` hook, and
|
|
104
|
+
OpenCode's session), else of Codex's thread (`prompt_cache_key`) when
|
|
105
|
+
magpie sends one, else of the conversation's start, so every turn of one
|
|
106
|
+
conversation names the same session; the key itself never goes out.
|
|
107
|
+
Each request has its own `request_id` and `chat_record_id`.
|
|
108
|
+
- Every call that answers one user prompt goes out as one task, as Qoder's
|
|
109
|
+
own client sends it (since 0.2.31, #330): one `request_set_id`, which is
|
|
110
|
+
also `business.id`, and one `business.begin_at` for every call of the
|
|
111
|
+
tool loop, `business.stage` "start" until a call is answered and
|
|
112
|
+
"processing" after. A new user prompt in the session is a new task. The
|
|
113
|
+
plugin keeps at most 512 tasks, each for 2 hours after its last call.
|
|
114
|
+
Before 0.2.31 each call was a task of its own.
|
|
105
115
|
- The model's own configuration from Qoder's list goes with it.
|
|
106
116
|
- It picks the reasoning effort from the model's own levels:
|
|
107
117
|
- The nearest level to the one asked, the higher one on a tie.
|
|
@@ -285,8 +295,28 @@ the account's credentials.
|
|
|
285
295
|
international account gets no daily claim until Qoder is installed.
|
|
286
296
|
- A helper that is there but can't be read, or gives an invalid identity,
|
|
287
297
|
is `failed` and asked again later, rather than a false "no activity"
|
|
288
|
-
result.
|
|
289
|
-
|
|
298
|
+
result.
|
|
299
|
+
|
|
300
|
+
Qoder CN (since 0.2.30) sends its own device identity the same way when
|
|
301
|
+
Qoder CN is installed: its desktop 0.4.9 and CLI (`qoderclicn`) both ask
|
|
302
|
+
Qoder CN's `runtime-info` for environment `0` (the international one is
|
|
303
|
+
`3`) and send it on the campaigns pages (yetone/magpie#2911, #304).
|
|
304
|
+
|
|
305
|
+
- The plugin finds Qoder CN desktop's `resources/umid` (`Qoder CN.app` on
|
|
306
|
+
macOS, `%LOCALAPPDATA%\Programs\Qoder CN` or `%ProgramFiles%\Qoder CN`
|
|
307
|
+
on Windows, `/opt/Qoder-CN` on Linux) or the runtime its CLI caches in
|
|
308
|
+
`~/.qoder-cn/.bin` (`QODERCN_CONFIG_DIR`, `QODERCN_CLI_HOME` and
|
|
309
|
+
`QODERCN_CONFIG_DIR_NAME` move it). `QODERCN_RUNTIME_INFO` names
|
|
310
|
+
another location; `QODER_RUNTIME_INFO` is only the international one's.
|
|
311
|
+
- Without it the requests go out as before, as Qoder CN's desktop does
|
|
312
|
+
when it can't read the identity, and a claim the list still offers is
|
|
313
|
+
made. When the list then offers no daily claim, the check-in is
|
|
314
|
+
`ineligible` for the day with a message to install Qoder CN desktop (or
|
|
315
|
+
sign in to its CLI); a helper that is there but can't be read makes it
|
|
316
|
+
`failed`, asked again later.
|
|
317
|
+
- Not tried with a real Qoder CN account: whether Qoder CN now hides the
|
|
318
|
+
daily claim from a request without the identity, as the international
|
|
319
|
+
site does, is inferred from its clients, not seen.
|
|
290
320
|
|
|
291
321
|
- **List:** `GET <openapi>/sash/api/v1/me/campaigns`: `campaigns[]` with
|
|
292
322
|
`campaignId`, `actionType`, `claimStatus` (`CLAIMABLE`, `CLAIMED`),
|
package/index.mjs
CHANGED
|
@@ -841,12 +841,16 @@ function cacheMarked(msgs) {
|
|
|
841
841
|
// sessionOf is the session_id a request goes out under: one for every turn
|
|
842
842
|
// of a conversation and another for each conversation, as Qoder's client
|
|
843
843
|
// sends its own session's (#290). Qoder can keep a conversation where its
|
|
844
|
-
// prompt is cached only when its turns say they are one. It is the
|
|
845
|
-
//
|
|
844
|
+
// prompt is cached only when its turns say they are one. It is the session
|
|
845
|
+
// the agent named (named: DSH's x-deepseek-harness-session-id, OpenCode's
|
|
846
|
+
// session, as the chat.headers hook is told it, #330), else the client's
|
|
847
|
+
// prompt_cache_key (Codex's thread), else its user, else the
|
|
846
848
|
// conversation's opening (its system prompt and first user turn), hashed,
|
|
847
|
-
// so no key of the client's goes to Qoder as it is.
|
|
848
|
-
|
|
849
|
-
|
|
849
|
+
// so no key of the client's goes to Qoder as it is. DSH changes its system
|
|
850
|
+
// prompt mid-task (yetone/magpie#3298), so its own session is the one that
|
|
851
|
+
// holds for the whole task.
|
|
852
|
+
function sessionOf(chat, named = "") {
|
|
853
|
+
let seed = named ? "session\0" + named : String(chat.prompt_cache_key ?? "").trim() ? "key\0" + chat.prompt_cache_key : String(chat.user ?? "").trim() ? "user\0" + chat.user : ""
|
|
850
854
|
if (!seed) {
|
|
851
855
|
const msgs = chat.messages ?? []
|
|
852
856
|
const sys = msgs.filter((x) => x.role === "system" || x.role === "developer").map((x) => textOf(x.content))
|
|
@@ -857,9 +861,62 @@ function sessionOf(chat) {
|
|
|
857
861
|
return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20, 32)}`
|
|
858
862
|
}
|
|
859
863
|
|
|
864
|
+
// SESSION carries the session magpie (or OpenCode) names a request's
|
|
865
|
+
// conversation by, from the chat.headers hook to the loader's fetch. It
|
|
866
|
+
// goes no further: nothing of the request's headers is sent to Qoder.
|
|
867
|
+
const SESSION = "x-magpie-qoder-session"
|
|
868
|
+
|
|
869
|
+
// namedSession is the session a chat.headers hook is told, "" when it
|
|
870
|
+
// names none. magpie, with no session header from the agent, makes one of
|
|
871
|
+
// the conversation's first user message ("magpie-" and 24 hex digits):
|
|
872
|
+
// that names no session, and sessionOf's own hash is taken instead.
|
|
873
|
+
function namedSession(input) {
|
|
874
|
+
const s = String(input?.sessionID ?? "").trim()
|
|
875
|
+
if (!s || /^magpie-[0-9a-f]{24}$/.test(s)) return ""
|
|
876
|
+
return s.slice(0, 128)
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
// A task is what Qoder's client sends under one request_set_id and one
|
|
880
|
+
// business record (#330): every model call that answers one user prompt,
|
|
881
|
+
// the tool loop's turns included. qoder-agent-sdk (Qoder.app's
|
|
882
|
+
// qoder-worker-runtime, AgentLifecycle) makes the record once per prompt
|
|
883
|
+
// (business.id the prompt's requestSetId, begin_at when the prompt
|
|
884
|
+
// started, stage "start"), keeps it for each call of the loop, and moves
|
|
885
|
+
// stage to "processing" once a call is answered; only request_id and
|
|
886
|
+
// chat_record_id are new on every call. The plugin made a new record for
|
|
887
|
+
// every call, so a 265-call DSH task was 265 tasks to Qoder. A task here is
|
|
888
|
+
// a session's last user turn: the tool loop repeats it, and a new prompt
|
|
889
|
+
// is a new one. TASKS holds at most TASK_MAX of them, each for TASK_IDLE
|
|
890
|
+
// after its last call, so a long-running magpie doesn't grow with them.
|
|
891
|
+
const TASKS = new Map()
|
|
892
|
+
const TASK_MAX = 512
|
|
893
|
+
const TASK_IDLE = 2 * 60 * 60 * 1000
|
|
894
|
+
|
|
895
|
+
// taskOf is the task chat's call belongs to, in session sid: made on its
|
|
896
|
+
// first call, the same one on every call after.
|
|
897
|
+
function taskOf(sid, chat, now = Date.now()) {
|
|
898
|
+
const msgs = chat.messages ?? []
|
|
899
|
+
const at = msgs.findLastIndex((x) => x?.role === "user")
|
|
900
|
+
const key = createHash("sha256").update(JSON.stringify([sid, at, at < 0 ? null : msgs[at].content ?? null])).digest("hex")
|
|
901
|
+
for (const [k, t] of TASKS) {
|
|
902
|
+
if (now - t.used <= TASK_IDLE) break
|
|
903
|
+
TASKS.delete(k)
|
|
904
|
+
}
|
|
905
|
+
let t = TASKS.get(key)
|
|
906
|
+
if (t && now - t.used > TASK_IDLE) t = undefined
|
|
907
|
+
TASKS.delete(key)
|
|
908
|
+
t ??= { id: randomUUID(), begin: now, answered: false }
|
|
909
|
+
t.used = now
|
|
910
|
+
TASKS.set(key, t)
|
|
911
|
+
while (TASKS.size > TASK_MAX) TASKS.delete(TASKS.keys().next().value)
|
|
912
|
+
return t
|
|
913
|
+
}
|
|
914
|
+
|
|
860
915
|
// qoderBody is the plaintext agent_chat_generation takes for a chat
|
|
861
|
-
// completion request, for model m
|
|
862
|
-
|
|
916
|
+
// completion request, for model m, in the session the agent named (named,
|
|
917
|
+
// "" when none) and the task it is a call of (taskOf's, a new one when
|
|
918
|
+
// none is given).
|
|
919
|
+
function qoderBody(chat, m, named = "", task) {
|
|
863
920
|
const system = (chat.messages ?? []).filter((x) => x.role === "system" || x.role === "developer").map((x) => textOf(x.content)).filter(Boolean).join("\n\n")
|
|
864
921
|
const tools = chat.tool_choice !== "none" ? (chat.tools ?? []).filter((t) => t?.type === "function" && t.function?.name) : []
|
|
865
922
|
let sysText = system ? QODER_SYS + "\n\n" + system : QODER_SYS
|
|
@@ -871,13 +928,15 @@ function qoderBody(chat, m) {
|
|
|
871
928
|
const window = windowFor(chat, m, params.max_tokens)
|
|
872
929
|
if (window > 0) params.context_length = window
|
|
873
930
|
const request = randomUUID()
|
|
931
|
+
const session = sessionOf(chat, named)
|
|
932
|
+
task ??= taskOf(session, chat)
|
|
874
933
|
const body = {
|
|
875
934
|
request_id: request,
|
|
876
|
-
request_set_id:
|
|
935
|
+
request_set_id: task.id,
|
|
877
936
|
chat_record_id: request,
|
|
878
|
-
session_id:
|
|
937
|
+
session_id: session,
|
|
879
938
|
parameters: params,
|
|
880
|
-
business: { product: "app", version: COSY_VERSION, type: "agent", id:
|
|
939
|
+
business: { product: "app", version: COSY_VERSION, type: "agent", id: task.id, name: "magpie session", begin_at: task.begin, stage: task.answered ? "processing" : "start" },
|
|
881
940
|
agent_id: "agent_common",
|
|
882
941
|
task_id: "common",
|
|
883
942
|
session_type: "app",
|
|
@@ -1526,23 +1585,45 @@ const CAMPAIGN_MACHINE_TTL = 60 * 60 * 1000 // Qoder's desktop client renews its
|
|
|
1526
1585
|
// (or .qoder): a file runtime-info-<platform>-<arch>-<hash> on macOS and
|
|
1527
1586
|
// Linux, a folder umid-win32-<arch>-<hash> holding runtime-info.exe on
|
|
1528
1587
|
// Windows.
|
|
1529
|
-
|
|
1530
|
-
|
|
1588
|
+
//
|
|
1589
|
+
// Qoder CN keeps its own (Qoder CN desktop 0.4.9's packages, qoderclicn
|
|
1590
|
+
// 1.1.77): Qoder CN.app on macOS (its installer's expectedBundleName),
|
|
1591
|
+
// /opt/Qoder-CN on Linux (the .deb), Qoder CN on Windows (the user
|
|
1592
|
+
// installer's "Uninstall Qoder CN.exe", electron-builder's
|
|
1593
|
+
// Programs\<productName>); its CLI's config is QODERCN_CONFIG_DIR, else
|
|
1594
|
+
// QODERCN_CLI_HOME (or home) joined with QODERCN_CONFIG_DIR_NAME (or
|
|
1595
|
+
// .qoder-cn). Both ask the helper for environment 0, the international
|
|
1596
|
+
// ones for 3 (the desktop's `regionCode === "global" ? 3 : 0`, the CLI's
|
|
1597
|
+
// UMID environment ONLINE = 0, US_ONLINE = 3).
|
|
1598
|
+
//
|
|
1599
|
+
// required: international Qoder lists the daily credits only on a request
|
|
1600
|
+
// carrying the identity. Qoder CN's desktop sends it when it can read it
|
|
1601
|
+
// and goes on without it when it can't, and a Qoder CN account was offered
|
|
1602
|
+
// and claimed the credits without one (0.2.21), so there it is sent when
|
|
1603
|
+
// Qoder CN is installed and left out when it isn't.
|
|
1604
|
+
const RUNTIMES = {
|
|
1605
|
+
qoder: { name: "Qoder", environment: "3", override: "QODER_RUNTIME_INFO", app: "Qoder", linux: "Qoder", cli: "QODER_", dir: ".qoder", required: true },
|
|
1606
|
+
"qoder-cn": { name: "Qoder CN", environment: "0", override: "QODERCN_RUNTIME_INFO", app: "Qoder CN", linux: "Qoder-CN", cli: "QODERCN_", dir: ".qoder-cn", required: false },
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
async function campaignRuntime({ home = homedir(), platform = process.platform, arch = process.arch, env = process.env, opt = "/opt", runtime = RUNTIMES.qoder } = {}) {
|
|
1610
|
+
if (env[runtime.override]) return resolve(env[runtime.override])
|
|
1531
1611
|
const name = platform === "win32" ? "runtime-info.exe" : "runtime-info"
|
|
1532
1612
|
const isFile = async (path) => (await stat(path).catch(() => null))?.isFile() ?? false
|
|
1533
1613
|
const paths = []
|
|
1534
1614
|
if (platform === "win32") {
|
|
1535
1615
|
const local = env.LOCALAPPDATA || join(home, "AppData", "Local")
|
|
1536
|
-
paths.push(join(local, "Programs",
|
|
1616
|
+
paths.push(join(local, "Programs", runtime.app, "resources", "umid", name))
|
|
1537
1617
|
// the machine-wide installer, as VS Code's system setup places it
|
|
1538
|
-
for (const dir of [env.ProgramFiles || env.PROGRAMFILES, env.ProgramW6432]) if (dir) paths.push(join(dir,
|
|
1618
|
+
for (const dir of [env.ProgramFiles || env.PROGRAMFILES, env.ProgramW6432]) if (dir) paths.push(join(dir, runtime.app, "resources", "umid", name))
|
|
1539
1619
|
} else if (platform === "darwin") {
|
|
1540
|
-
for (const dir of ["/Applications", join(home, "Applications")]) paths.push(join(dir, "
|
|
1620
|
+
for (const dir of ["/Applications", join(home, "Applications")]) paths.push(join(dir, runtime.app + ".app", "Contents", "Resources", "umid", name))
|
|
1541
1621
|
} else if (platform === "linux") {
|
|
1542
|
-
paths.push(join(opt,
|
|
1622
|
+
paths.push(join(opt, runtime.linux, "resources", "umid", name))
|
|
1543
1623
|
}
|
|
1544
1624
|
for (const path of paths) if (await isFile(path)) return path
|
|
1545
|
-
const
|
|
1625
|
+
const cli = (key) => env[runtime.cli + key]
|
|
1626
|
+
const config = cli("CONFIG_DIR") || join(cli("CLI_HOME") || env.GEMINI_CLI_HOME || home, cli("CONFIG_DIR_NAME") || runtime.dir)
|
|
1546
1627
|
const cache = join(config, ".bin")
|
|
1547
1628
|
const entries = await readdir(cache, { withFileTypes: true }).catch(() => [])
|
|
1548
1629
|
const found = []
|
|
@@ -1556,7 +1637,7 @@ async function campaignRuntime({ home = homedir(), platform = process.platform,
|
|
|
1556
1637
|
if (path && (await isFile(path))) found.push({ path, time: (await stat(path)).mtimeMs })
|
|
1557
1638
|
}
|
|
1558
1639
|
if (found.length) return found.sort((a, b) => b.time - a.time)[0].path
|
|
1559
|
-
throw new RuntimeMissing()
|
|
1640
|
+
throw new RuntimeMissing(runtime)
|
|
1560
1641
|
}
|
|
1561
1642
|
|
|
1562
1643
|
// RuntimeMissing: no Qoder desktop or CLI on this machine to read the
|
|
@@ -1566,19 +1647,21 @@ async function campaignRuntime({ home = homedir(), platform = process.platform,
|
|
|
1566
1647
|
// never makes up. Asking again won't change that until the user installs
|
|
1567
1648
|
// Qoder, so the check-in says what to do once instead of "tries again later".
|
|
1568
1649
|
class RuntimeMissing extends Error {
|
|
1569
|
-
constructor() {
|
|
1650
|
+
constructor(runtime = RUNTIMES.qoder) {
|
|
1651
|
+
const { name, override } = runtime
|
|
1570
1652
|
super(
|
|
1571
|
-
|
|
1653
|
+
`${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`,
|
|
1572
1654
|
)
|
|
1573
1655
|
}
|
|
1574
1656
|
}
|
|
1575
1657
|
|
|
1576
|
-
// Read the SDK identity as Qoder does: environment 3 is international
|
|
1577
|
-
// Windows and macOS the uid goes over stdin, never into the
|
|
1578
|
-
|
|
1658
|
+
// Read the SDK identity as Qoder does: environment 3 is international, 0
|
|
1659
|
+
// Qoder CN; on Windows and macOS the uid goes over stdin, never into the
|
|
1660
|
+
// command line.
|
|
1661
|
+
async function campaignMachine(uid, runtime = RUNTIMES.qoder) {
|
|
1579
1662
|
if (!uid) throw new Error("Qoder check-in: device identity needs the account uid; sign in again")
|
|
1580
|
-
const file = await campaignRuntime()
|
|
1581
|
-
const args = process.platform === "linux" ? [
|
|
1663
|
+
const file = await campaignRuntime({ runtime })
|
|
1664
|
+
const args = process.platform === "linux" ? [runtime.environment] : [runtime.environment, "--account-stdin"]
|
|
1582
1665
|
const text = await new Promise((resolve, reject) => {
|
|
1583
1666
|
const child = execFile(file, args, { timeout: 20_000, windowsHide: true, maxBuffer: 1 << 20 }, (err, out) => {
|
|
1584
1667
|
if (err) reject(err)
|
|
@@ -1587,7 +1670,7 @@ async function campaignMachine(uid) {
|
|
|
1587
1670
|
child.stdin?.on("error", () => {})
|
|
1588
1671
|
child.stdin?.end(process.platform === "linux" ? undefined : JSON.stringify({ account: uid }) + "\n")
|
|
1589
1672
|
}).catch(() => {
|
|
1590
|
-
throw new Error(
|
|
1673
|
+
throw new Error(`${runtime.name} check-in: could not read device identity; restart or update ${runtime.name}, or check ${runtime.override}`)
|
|
1591
1674
|
})
|
|
1592
1675
|
try {
|
|
1593
1676
|
const identity = JSON.parse(text.trim().split(/\r?\n/, 1)[0])
|
|
@@ -1599,7 +1682,7 @@ async function campaignMachine(uid) {
|
|
|
1599
1682
|
}
|
|
1600
1683
|
return headers
|
|
1601
1684
|
} catch {
|
|
1602
|
-
throw new Error(
|
|
1685
|
+
throw new Error(`${runtime.name} check-in: invalid device identity from ${runtime.name}; restart or update ${runtime.name}`)
|
|
1603
1686
|
}
|
|
1604
1687
|
}
|
|
1605
1688
|
|
|
@@ -1754,12 +1837,13 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
1754
1837
|
const ID = site.id
|
|
1755
1838
|
// The SDK identity is account-specific. Share an in-flight read, refresh
|
|
1756
1839
|
// after an hour, and leave a failed read available for the next attempt.
|
|
1840
|
+
const runtime = RUNTIMES[ID]
|
|
1757
1841
|
const campaignMachines = new Map()
|
|
1758
1842
|
const machineForCampaign = async (uid) => {
|
|
1759
1843
|
const now = Date.now()
|
|
1760
1844
|
let entry = campaignMachines.get(uid)
|
|
1761
1845
|
if (!entry || now - entry.at >= CAMPAIGN_MACHINE_TTL) {
|
|
1762
|
-
entry = { at: now, promise: campaignMachine(uid) }
|
|
1846
|
+
entry = { at: now, promise: campaignMachine(uid, runtime) }
|
|
1763
1847
|
campaignMachines.set(uid, entry)
|
|
1764
1848
|
}
|
|
1765
1849
|
try {
|
|
@@ -1769,6 +1853,16 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
1769
1853
|
throw e
|
|
1770
1854
|
}
|
|
1771
1855
|
}
|
|
1856
|
+
// campaignIdentity: the headers a campaigns request carries, and why
|
|
1857
|
+
// there are none where the site goes on without them (Qoder CN).
|
|
1858
|
+
const campaignIdentity = async (uid) => {
|
|
1859
|
+
if (runtime.required) return { machine: await machineForCampaign(uid) }
|
|
1860
|
+
try {
|
|
1861
|
+
return { machine: await machineForCampaign(uid) }
|
|
1862
|
+
} catch (e) {
|
|
1863
|
+
return { machine: {}, unread: e }
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1772
1866
|
// serializes checking, rotating and saving tokens: a refresh token is
|
|
1773
1867
|
// spent once, so two refreshes would spend it twice
|
|
1774
1868
|
let lock = Promise.resolve()
|
|
@@ -1927,7 +2021,7 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
1927
2021
|
if (!url) return errorResponse({ status: 404, message: "only chat completions are served" })
|
|
1928
2022
|
const method = String(init.method ?? input?.method ?? "GET").toUpperCase()
|
|
1929
2023
|
try {
|
|
1930
|
-
const machine =
|
|
2024
|
+
const { machine } = await campaignIdentity(cred.uid)
|
|
1931
2025
|
// A PAT account has no device token: its job token signs the campaigns
|
|
1932
2026
|
// pages too, as it does the usage read. Nothing is rotated on that
|
|
1933
2027
|
// path — the refusal is the answer.
|
|
@@ -1956,7 +2050,7 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
1956
2050
|
const base = campaignTarget(site, siteOf(site, cred), site.openapi + CAMPAIGNS)
|
|
1957
2051
|
if (!base) return { outcome: "inactive", message: "no campaigns page for this account" }
|
|
1958
2052
|
try {
|
|
1959
|
-
const machine
|
|
2053
|
+
const { machine, unread } = await campaignIdentity(cred.uid)
|
|
1960
2054
|
// A PAT account has no device token: the campaigns pages take the job
|
|
1961
2055
|
// token the exchange gave, as the usage endpoint does. A live Qoder CN
|
|
1962
2056
|
// PAT account answered 200 to the list and CLAIMED to a claim, so the
|
|
@@ -1994,7 +2088,13 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
1994
2088
|
const amount = Number(done.benefit?.amount)
|
|
1995
2089
|
return { outcome: "done", ...(Number.isFinite(amount) && amount > 0 ? { credit: amount } : {}) }
|
|
1996
2090
|
}
|
|
1997
|
-
|
|
2091
|
+
if (campaigns.length) return { outcome: "failed", message: "campaigns: today's credits aren't offered yet" }
|
|
2092
|
+
// Asked without Qoder CN's identity and offered nothing: what an
|
|
2093
|
+
// international list says to a request without one, so say why
|
|
2094
|
+
// (no Qoder CN on this machine) or ask again (it didn't answer).
|
|
2095
|
+
if (unread instanceof RuntimeMissing) return { outcome: "ineligible", message: unread.message }
|
|
2096
|
+
if (unread) return { outcome: "failed", message: unread.message }
|
|
2097
|
+
return { outcome: "inactive" }
|
|
1998
2098
|
}
|
|
1999
2099
|
const claimRes = await call(base.replace(/\/$/, "") + "/" + encodeURIComponent(campaign.campaignId) + "/claim", "POST", "{}")
|
|
2000
2100
|
let claim = {}
|
|
@@ -2028,7 +2128,7 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
2028
2128
|
})
|
|
2029
2129
|
|
|
2030
2130
|
// ask is the answer to a chat completion on the live account cred.
|
|
2031
|
-
const ask = async (chat, cred, init) => {
|
|
2131
|
+
const ask = async (chat, cred, init, named = "") => {
|
|
2032
2132
|
let m
|
|
2033
2133
|
try {
|
|
2034
2134
|
m = (await models(cred)).find((x) => x.key === chat.model) ?? (await models(cred, true)).find((x) => x.key === chat.model)
|
|
@@ -2039,7 +2139,8 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
2039
2139
|
if (!m) return errorResponse({ status: 400, message: `unknown or disabled model "${chat.model}"` })
|
|
2040
2140
|
const api = siteOf(site, cred).api
|
|
2041
2141
|
const CHAT_URL = api + CHAT_PATH
|
|
2042
|
-
const
|
|
2142
|
+
const task = taskOf(sessionOf(chat, named), chat)
|
|
2143
|
+
const body = qoderBody(chat, m, named, task)
|
|
2043
2144
|
const wire = encodeBody(JSON.stringify(body))
|
|
2044
2145
|
// Qoder's queue is waited out (queueWait) and the same request, its
|
|
2045
2146
|
// request_set_id the one the queue knows, sent again
|
|
@@ -2061,6 +2162,8 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
2061
2162
|
return errorResponse({ status: 502, message: e.message })
|
|
2062
2163
|
}
|
|
2063
2164
|
const f = !res.ok ? failure(res.status, (await res.text()).slice(0, 1 << 20)) : open.first.value?.error
|
|
2165
|
+
// an answered call moves its task on, as Qoder's client moves it
|
|
2166
|
+
if (!f) task.answered = true
|
|
2064
2167
|
if (!f?.queue || waited >= QUEUE.recoveries) return f && !res.ok ? errorResponse(f) : answer(open, chat)
|
|
2065
2168
|
try {
|
|
2066
2169
|
if (!(await queueWait(api, cred, ask, f.queue, init.signal, deadline))) return errorResponse(f)
|
|
@@ -2071,6 +2174,16 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
2071
2174
|
}
|
|
2072
2175
|
|
|
2073
2176
|
return {
|
|
2177
|
+
// the session the request is part of, for session_id and its task
|
|
2178
|
+
// (#330): magpie names it here (DSH's x-deepseek-harness-session-id
|
|
2179
|
+
// among the agents' own), as OpenCode does, and the loader's fetch
|
|
2180
|
+
// reads it back. Every plugin's hook sees every provider's requests,
|
|
2181
|
+
// so it is put on this site's alone.
|
|
2182
|
+
async "chat.headers"(input, output) {
|
|
2183
|
+
if (input?.model?.providerID !== ID && input?.provider?.info?.id !== ID) return
|
|
2184
|
+
const s = namedSession(input)
|
|
2185
|
+
if (s) output.headers[SESSION] = s
|
|
2186
|
+
},
|
|
2074
2187
|
auth: {
|
|
2075
2188
|
provider: ID,
|
|
2076
2189
|
// magpie renews the chat token RENEW_LEAD before its end, once for
|
|
@@ -2123,7 +2236,9 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
2123
2236
|
} catch (e) {
|
|
2124
2237
|
return signedInError(e)
|
|
2125
2238
|
}
|
|
2126
|
-
|
|
2239
|
+
const h = new Headers(input instanceof Request ? input.headers : undefined)
|
|
2240
|
+
new Headers(init.headers ?? {}).forEach((v, k) => h.set(k, v))
|
|
2241
|
+
return signed(await ask(chat, cred, init, h.get(SESSION)?.trim().slice(0, 128) ?? ""), renewed(cred))
|
|
2127
2242
|
},
|
|
2128
2243
|
}
|
|
2129
2244
|
},
|
|
@@ -2233,4 +2348,4 @@ export const QoderAuthPlugin = makePlugin(SITES.qoder)
|
|
|
2233
2348
|
export const QoderCNAuthPlugin = makePlugin(SITES["qoder-cn"])
|
|
2234
2349
|
|
|
2235
2350
|
// for tests
|
|
2236
|
-
export const _internal = { campaignRuntime, campaignPage, campaignTarget, vpcInstance, vpcSite, parseUsage, gfmt, when, failure, modelInfo, qoderBody, qoderMessages, cacheMarked, sessionOf, SITES, decodeBody, events, QUEUE, queueState }
|
|
2351
|
+
export const _internal = { campaignRuntime, RUNTIMES, campaignPage, campaignTarget, vpcInstance, vpcSite, parseUsage, gfmt, when, failure, modelInfo, qoderBody, qoderMessages, cacheMarked, sessionOf, namedSession, taskOf, TASKS, TASK_MAX, TASK_IDLE, SESSION, SITES, decodeBody, events, QUEUE, queueState }
|
package/package.json
CHANGED