@magpie-community/opencode-qoder-auth 0.2.30 → 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 +14 -4
- package/index.mjs +88 -14
- 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.
|
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",
|
|
@@ -2069,7 +2128,7 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
2069
2128
|
})
|
|
2070
2129
|
|
|
2071
2130
|
// ask is the answer to a chat completion on the live account cred.
|
|
2072
|
-
const ask = async (chat, cred, init) => {
|
|
2131
|
+
const ask = async (chat, cred, init, named = "") => {
|
|
2073
2132
|
let m
|
|
2074
2133
|
try {
|
|
2075
2134
|
m = (await models(cred)).find((x) => x.key === chat.model) ?? (await models(cred, true)).find((x) => x.key === chat.model)
|
|
@@ -2080,7 +2139,8 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
2080
2139
|
if (!m) return errorResponse({ status: 400, message: `unknown or disabled model "${chat.model}"` })
|
|
2081
2140
|
const api = siteOf(site, cred).api
|
|
2082
2141
|
const CHAT_URL = api + CHAT_PATH
|
|
2083
|
-
const
|
|
2142
|
+
const task = taskOf(sessionOf(chat, named), chat)
|
|
2143
|
+
const body = qoderBody(chat, m, named, task)
|
|
2084
2144
|
const wire = encodeBody(JSON.stringify(body))
|
|
2085
2145
|
// Qoder's queue is waited out (queueWait) and the same request, its
|
|
2086
2146
|
// request_set_id the one the queue knows, sent again
|
|
@@ -2102,6 +2162,8 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
2102
2162
|
return errorResponse({ status: 502, message: e.message })
|
|
2103
2163
|
}
|
|
2104
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
|
|
2105
2167
|
if (!f?.queue || waited >= QUEUE.recoveries) return f && !res.ok ? errorResponse(f) : answer(open, chat)
|
|
2106
2168
|
try {
|
|
2107
2169
|
if (!(await queueWait(api, cred, ask, f.queue, init.signal, deadline))) return errorResponse(f)
|
|
@@ -2112,6 +2174,16 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
2112
2174
|
}
|
|
2113
2175
|
|
|
2114
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
|
+
},
|
|
2115
2187
|
auth: {
|
|
2116
2188
|
provider: ID,
|
|
2117
2189
|
// magpie renews the chat token RENEW_LEAD before its end, once for
|
|
@@ -2164,7 +2236,9 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
2164
2236
|
} catch (e) {
|
|
2165
2237
|
return signedInError(e)
|
|
2166
2238
|
}
|
|
2167
|
-
|
|
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))
|
|
2168
2242
|
},
|
|
2169
2243
|
}
|
|
2170
2244
|
},
|
|
@@ -2274,4 +2348,4 @@ export const QoderAuthPlugin = makePlugin(SITES.qoder)
|
|
|
2274
2348
|
export const QoderCNAuthPlugin = makePlugin(SITES["qoder-cn"])
|
|
2275
2349
|
|
|
2276
2350
|
// 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 }
|
|
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