@magpie-community/opencode-qoder-auth 0.2.30 → 0.2.32
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 +21 -4
- package/index.mjs +99 -15
- 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.
|
|
@@ -286,6 +296,13 @@ the account's credentials.
|
|
|
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
298
|
result.
|
|
299
|
+
- With the identity read and sent, a list with no daily claim is
|
|
300
|
+
`inactive`, with a message (since 0.2.32) saying Qoder offered this
|
|
301
|
+
device's identity no daily credits campaign, that Qoder decides that per
|
|
302
|
+
device, and that the credits can be claimed in Qoder desktop when it
|
|
303
|
+
shows them (#304: a Linux CLI identity was offered none while the same
|
|
304
|
+
account's desktop identity was). The plugin never sends another
|
|
305
|
+
device's identity.
|
|
289
306
|
|
|
290
307
|
Qoder CN (since 0.2.30) sends its own device identity the same way when
|
|
291
308
|
Qoder CN is installed: its desktop 0.4.9 and CLI (`qoderclicn`) both ask
|
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",
|
|
@@ -1596,6 +1655,12 @@ class RuntimeMissing extends Error {
|
|
|
1596
1655
|
}
|
|
1597
1656
|
}
|
|
1598
1657
|
|
|
1658
|
+
// noCampaign is why a check-in asked with this machine's own device
|
|
1659
|
+
// identity found nothing to claim: all the list says is that it offered
|
|
1660
|
+
// that identity no daily credits, not why.
|
|
1661
|
+
const noCampaign = (name) =>
|
|
1662
|
+
`${name} check-in: ${name} offered this device's identity, read from this machine's own ${name} runtime-info, no daily credits campaign right now. ${name} decides per device whether to offer it; if ${name} desktop shows today's credits, claim them there`
|
|
1663
|
+
|
|
1599
1664
|
// Read the SDK identity as Qoder does: environment 3 is international, 0
|
|
1600
1665
|
// Qoder CN; on Windows and macOS the uid goes over stdin, never into the
|
|
1601
1666
|
// command line.
|
|
@@ -2035,7 +2100,11 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
2035
2100
|
// (no Qoder CN on this machine) or ask again (it didn't answer).
|
|
2036
2101
|
if (unread instanceof RuntimeMissing) return { outcome: "ineligible", message: unread.message }
|
|
2037
2102
|
if (unread) return { outcome: "failed", message: unread.message }
|
|
2038
|
-
|
|
2103
|
+
// Asked with this machine's own identity and still offered nothing:
|
|
2104
|
+
// Qoder decides per device (plugins#304, a Linux CLI identity was
|
|
2105
|
+
// offered none while the same account's desktop identity was), so
|
|
2106
|
+
// say that much, and where the credits can still be claimed.
|
|
2107
|
+
return { outcome: "inactive", message: noCampaign(site.name) }
|
|
2039
2108
|
}
|
|
2040
2109
|
const claimRes = await call(base.replace(/\/$/, "") + "/" + encodeURIComponent(campaign.campaignId) + "/claim", "POST", "{}")
|
|
2041
2110
|
let claim = {}
|
|
@@ -2069,7 +2138,7 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
2069
2138
|
})
|
|
2070
2139
|
|
|
2071
2140
|
// ask is the answer to a chat completion on the live account cred.
|
|
2072
|
-
const ask = async (chat, cred, init) => {
|
|
2141
|
+
const ask = async (chat, cred, init, named = "") => {
|
|
2073
2142
|
let m
|
|
2074
2143
|
try {
|
|
2075
2144
|
m = (await models(cred)).find((x) => x.key === chat.model) ?? (await models(cred, true)).find((x) => x.key === chat.model)
|
|
@@ -2080,7 +2149,8 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
2080
2149
|
if (!m) return errorResponse({ status: 400, message: `unknown or disabled model "${chat.model}"` })
|
|
2081
2150
|
const api = siteOf(site, cred).api
|
|
2082
2151
|
const CHAT_URL = api + CHAT_PATH
|
|
2083
|
-
const
|
|
2152
|
+
const task = taskOf(sessionOf(chat, named), chat)
|
|
2153
|
+
const body = qoderBody(chat, m, named, task)
|
|
2084
2154
|
const wire = encodeBody(JSON.stringify(body))
|
|
2085
2155
|
// Qoder's queue is waited out (queueWait) and the same request, its
|
|
2086
2156
|
// request_set_id the one the queue knows, sent again
|
|
@@ -2102,6 +2172,8 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
2102
2172
|
return errorResponse({ status: 502, message: e.message })
|
|
2103
2173
|
}
|
|
2104
2174
|
const f = !res.ok ? failure(res.status, (await res.text()).slice(0, 1 << 20)) : open.first.value?.error
|
|
2175
|
+
// an answered call moves its task on, as Qoder's client moves it
|
|
2176
|
+
if (!f) task.answered = true
|
|
2105
2177
|
if (!f?.queue || waited >= QUEUE.recoveries) return f && !res.ok ? errorResponse(f) : answer(open, chat)
|
|
2106
2178
|
try {
|
|
2107
2179
|
if (!(await queueWait(api, cred, ask, f.queue, init.signal, deadline))) return errorResponse(f)
|
|
@@ -2112,6 +2184,16 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
2112
2184
|
}
|
|
2113
2185
|
|
|
2114
2186
|
return {
|
|
2187
|
+
// the session the request is part of, for session_id and its task
|
|
2188
|
+
// (#330): magpie names it here (DSH's x-deepseek-harness-session-id
|
|
2189
|
+
// among the agents' own), as OpenCode does, and the loader's fetch
|
|
2190
|
+
// reads it back. Every plugin's hook sees every provider's requests,
|
|
2191
|
+
// so it is put on this site's alone.
|
|
2192
|
+
async "chat.headers"(input, output) {
|
|
2193
|
+
if (input?.model?.providerID !== ID && input?.provider?.info?.id !== ID) return
|
|
2194
|
+
const s = namedSession(input)
|
|
2195
|
+
if (s) output.headers[SESSION] = s
|
|
2196
|
+
},
|
|
2115
2197
|
auth: {
|
|
2116
2198
|
provider: ID,
|
|
2117
2199
|
// magpie renews the chat token RENEW_LEAD before its end, once for
|
|
@@ -2164,7 +2246,9 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
2164
2246
|
} catch (e) {
|
|
2165
2247
|
return signedInError(e)
|
|
2166
2248
|
}
|
|
2167
|
-
|
|
2249
|
+
const h = new Headers(input instanceof Request ? input.headers : undefined)
|
|
2250
|
+
new Headers(init.headers ?? {}).forEach((v, k) => h.set(k, v))
|
|
2251
|
+
return signed(await ask(chat, cred, init, h.get(SESSION)?.trim().slice(0, 128) ?? ""), renewed(cred))
|
|
2168
2252
|
},
|
|
2169
2253
|
}
|
|
2170
2254
|
},
|
|
@@ -2274,4 +2358,4 @@ export const QoderAuthPlugin = makePlugin(SITES.qoder)
|
|
|
2274
2358
|
export const QoderCNAuthPlugin = makePlugin(SITES["qoder-cn"])
|
|
2275
2359
|
|
|
2276
2360
|
// for tests
|
|
2277
|
-
export const _internal = { campaignRuntime, RUNTIMES, campaignPage, campaignTarget, vpcInstance, vpcSite, parseUsage, gfmt, when, failure, modelInfo, qoderBody, qoderMessages, cacheMarked, sessionOf, SITES, decodeBody, events, QUEUE, queueState }
|
|
2361
|
+
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