@magpie-community/opencode-qoder-auth 0.2.25 → 0.2.26
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 +13 -2
- package/index.mjs +66 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -91,6 +91,17 @@ itself.
|
|
|
91
91
|
answers no call before it. A result with no call left to answer goes
|
|
92
92
|
as text in the user's turn.
|
|
93
93
|
- Your tools become Qoder's native function tools.
|
|
94
|
+
- A call the client sent without an id gets one made from its turn, its
|
|
95
|
+
place and its arguments, so it goes out the same on every request.
|
|
96
|
+
- It marks the prompt for Qoder's cache as Qoder's own client does (since
|
|
97
|
+
0.2.26, #290): the last block of the last user turn carries
|
|
98
|
+
`cache_control: ephemeral` (nothing is marked when the last turn is a
|
|
99
|
+
tool's result), user turns carry their blocks as `contents` too, and each
|
|
100
|
+
request names its conversation in `session_id`. That id is a hash of
|
|
101
|
+
Codex's thread (`prompt_cache_key`) when magpie sends one, else of the
|
|
102
|
+
conversation's start, so every turn of one conversation names the same
|
|
103
|
+
session; the key itself never goes out. Each request has its own
|
|
104
|
+
`request_id`.
|
|
94
105
|
- The model's own configuration from Qoder's list goes with it.
|
|
95
106
|
- It picks the reasoning effort from the model's own levels:
|
|
96
107
|
- The nearest level to the one asked, the higher one on a tie.
|
|
@@ -257,8 +268,8 @@ the account's credentials.
|
|
|
257
268
|
|
|
258
269
|
- Install Qoder desktop, or sign in to Qoder CLI once, to make its native
|
|
259
270
|
runtime available. The plugin finds the desktop's `resources/umid`
|
|
260
|
-
(`Qoder.app` on macOS, `%LOCALAPPDATA%\Programs\Qoder`
|
|
261
|
-
`/opt/Qoder` on Linux) or the runtime the CLI caches in `~/.qoder/.bin`
|
|
271
|
+
(`Qoder.app` on macOS, `%LOCALAPPDATA%\Programs\Qoder` or, since
|
|
272
|
+
0.2.26, `%ProgramFiles%\Qoder` on Windows, `/opt/Qoder` on Linux) or the runtime the CLI caches in `~/.qoder/.bin`
|
|
262
273
|
(`runtime-info-<platform>-<arch>-*` on macOS and Linux,
|
|
263
274
|
`umid-win32-x64-*\runtime-info.exe` on Windows; `QODER_CONFIG_DIR`,
|
|
264
275
|
`QODER_CLI_HOME` and `QODER_CONFIG_DIR_NAME` move it as they move the
|
package/index.mjs
CHANGED
|
@@ -705,6 +705,16 @@ const textOf = (c) => (typeof c === "string" ? c : blocksOf(c).filter((b) => b.t
|
|
|
705
705
|
// went out with in order, which is the pairing a dropped id means. Only a
|
|
706
706
|
// result with no call of its turn left to answer becomes text in the user's
|
|
707
707
|
// own turn, so the words the tool returned stay with the conversation.
|
|
708
|
+
// callID is the id a call the client sent without one goes out with: the
|
|
709
|
+
// same on every turn of the conversation (#290), made from where the call
|
|
710
|
+
// is and what it says, so the history Qoder sees doesn't change under a
|
|
711
|
+
// prompt cache from one request to the next.
|
|
712
|
+
function callID(at, k, c) {
|
|
713
|
+
const args = c.function?.arguments
|
|
714
|
+
const said = typeof args === "string" ? args : JSON.stringify(args ?? {})
|
|
715
|
+
return "call_" + createHash("sha256").update(`${at}\0${k}\0${c.function?.name ?? ""}\0${said}`).digest("hex").slice(0, 32)
|
|
716
|
+
}
|
|
717
|
+
|
|
708
718
|
function qoderMessages(msgs) {
|
|
709
719
|
const out = []
|
|
710
720
|
const names = {}
|
|
@@ -757,8 +767,8 @@ function qoderMessages(msgs) {
|
|
|
757
767
|
if (m.role === "assistant" && m.tool_calls?.length) {
|
|
758
768
|
// a call the client sent without an id gets one, so its result can
|
|
759
769
|
// answer it instead of being turned into text
|
|
760
|
-
const calls = m.tool_calls.map((c) => {
|
|
761
|
-
const id = String(c.id ?? "").trim() ||
|
|
770
|
+
const calls = m.tool_calls.map((c, k) => {
|
|
771
|
+
const id = String(c.id ?? "").trim() || callID(i, k, c)
|
|
762
772
|
names[id] = c.function?.name
|
|
763
773
|
const args = c.function?.arguments
|
|
764
774
|
return { id, type: "function", function: { name: c.function?.name, arguments: typeof args === "string" ? args : JSON.stringify(args ?? {}) } }
|
|
@@ -804,6 +814,49 @@ function windowFor(chat, m, reply = 0) {
|
|
|
804
814
|
return m.windows.find((w) => w >= need) ?? m.windows[m.windows.length - 1]
|
|
805
815
|
}
|
|
806
816
|
|
|
817
|
+
// UNCACHED are the blocks Qoder's client never marks for the prompt cache.
|
|
818
|
+
const UNCACHED = new Set(["thinking", "redacted_thinking", "tool_use", "tool_result"])
|
|
819
|
+
|
|
820
|
+
// cacheMarked is the turns as Qoder's own client sends them for the prompt
|
|
821
|
+
// cache (#290): every user turn's blocks also as contents, and the last
|
|
822
|
+
// block of the last turn that isn't thinking or a tool's marked
|
|
823
|
+
// cache_control ephemeral. That is qoder-agent-sdk's (Qoder.app and
|
|
824
|
+
// qodercli 1.1.75 alike): the last message's last such block, and nothing
|
|
825
|
+
// when the last turn is a tool's. A model behind Qoder that caches only
|
|
826
|
+
// what is marked (Anthropic's) read none of a conversation without it,
|
|
827
|
+
// while one that caches on its own (Fast) did. The caller's turns are left
|
|
828
|
+
// as they were.
|
|
829
|
+
function cacheMarked(msgs) {
|
|
830
|
+
const out = msgs.map((m) => (m.role === "user" && Array.isArray(m.content) ? { ...m, contents: m.content } : m))
|
|
831
|
+
const last = out.at(-1)
|
|
832
|
+
if (last?.role !== "user" || !Array.isArray(last.content)) return out
|
|
833
|
+
const at = last.content.findLastIndex((b) => b?.type && !UNCACHED.has(b.type))
|
|
834
|
+
if (at < 0) return out
|
|
835
|
+
const blocks = last.content.slice()
|
|
836
|
+
blocks[at] = { ...blocks[at], cache_control: { type: "ephemeral" } }
|
|
837
|
+
out[out.length - 1] = { ...last, content: blocks, contents: blocks }
|
|
838
|
+
return out
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
// sessionOf is the session_id a request goes out under: one for every turn
|
|
842
|
+
// of a conversation and another for each conversation, as Qoder's client
|
|
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
|
+
// client's prompt_cache_key (Codex's thread), else its user, else the
|
|
846
|
+
// 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
|
+
function sessionOf(chat) {
|
|
849
|
+
let seed = String(chat.prompt_cache_key ?? "").trim() ? "key\0" + chat.prompt_cache_key : String(chat.user ?? "").trim() ? "user\0" + chat.user : ""
|
|
850
|
+
if (!seed) {
|
|
851
|
+
const msgs = chat.messages ?? []
|
|
852
|
+
const sys = msgs.filter((x) => x.role === "system" || x.role === "developer").map((x) => textOf(x.content))
|
|
853
|
+
const first = msgs.find((x) => x.role === "user")
|
|
854
|
+
seed = "chat\0" + JSON.stringify([sys, first?.content ?? ""])
|
|
855
|
+
}
|
|
856
|
+
const h = createHash("sha256").update(seed).digest("hex")
|
|
857
|
+
return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20, 32)}`
|
|
858
|
+
}
|
|
859
|
+
|
|
807
860
|
// qoderBody is the plaintext agent_chat_generation takes for a chat
|
|
808
861
|
// completion request, for model m.
|
|
809
862
|
function qoderBody(chat, m) {
|
|
@@ -817,7 +870,12 @@ function qoderBody(chat, m) {
|
|
|
817
870
|
if (effort) params.reasoning_effort = effort
|
|
818
871
|
const window = windowFor(chat, m, params.max_tokens)
|
|
819
872
|
if (window > 0) params.context_length = window
|
|
873
|
+
const request = randomUUID()
|
|
820
874
|
const body = {
|
|
875
|
+
request_id: request,
|
|
876
|
+
request_set_id: request,
|
|
877
|
+
chat_record_id: request,
|
|
878
|
+
session_id: sessionOf(chat),
|
|
821
879
|
parameters: params,
|
|
822
880
|
business: { product: "app", version: COSY_VERSION, type: "agent", id: hexID(), name: "magpie session", begin_at: Date.now(), stage: "start" },
|
|
823
881
|
agent_id: "agent_common",
|
|
@@ -825,7 +883,7 @@ function qoderBody(chat, m) {
|
|
|
825
883
|
session_type: "app",
|
|
826
884
|
model_config: m.config,
|
|
827
885
|
system: [sys],
|
|
828
|
-
messages: [{ role: "system", content: [sys] }, ...qoderMessages(chat.messages ?? [])],
|
|
886
|
+
messages: [{ role: "system", content: [sys] }, ...cacheMarked(qoderMessages(chat.messages ?? []))],
|
|
829
887
|
}
|
|
830
888
|
if (tools.length)
|
|
831
889
|
body.tools = tools.map((t) => ({
|
|
@@ -1312,7 +1370,8 @@ const CAMPAIGN_MACHINE_TTL = 60 * 60 * 1000 // Qoder's desktop client renews its
|
|
|
1312
1370
|
//
|
|
1313
1371
|
// Where each one keeps it (Qoder desktop 0.4.3's packages, qodercli 1.1.66):
|
|
1314
1372
|
// - desktop: <install>/resources/umid/runtime-info(.exe): Qoder.app on
|
|
1315
|
-
// macOS, %LOCALAPPDATA%\Programs\Qoder (the user installer)
|
|
1373
|
+
// macOS, %LOCALAPPDATA%\Programs\Qoder (the user installer) or
|
|
1374
|
+
// %ProgramFiles%\Qoder (a machine-wide install) on Windows,
|
|
1316
1375
|
// /opt/Qoder (the .deb and the .rpm) on Linux;
|
|
1317
1376
|
// - CLI: <config>/.bin, where <config> is QODER_CONFIG_DIR, else
|
|
1318
1377
|
// QODER_CLI_HOME (or the home folder) joined with QODER_CONFIG_DIR_NAME
|
|
@@ -1327,6 +1386,8 @@ async function campaignRuntime({ home = homedir(), platform = process.platform,
|
|
|
1327
1386
|
if (platform === "win32") {
|
|
1328
1387
|
const local = env.LOCALAPPDATA || join(home, "AppData", "Local")
|
|
1329
1388
|
paths.push(join(local, "Programs", "Qoder", "resources", "umid", name))
|
|
1389
|
+
// 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, "Qoder", "resources", "umid", name))
|
|
1330
1391
|
} else if (platform === "darwin") {
|
|
1331
1392
|
for (const dir of ["/Applications", join(home, "Applications")]) paths.push(join(dir, "Qoder.app", "Contents", "Resources", "umid", name))
|
|
1332
1393
|
} else if (platform === "linux") {
|
|
@@ -1993,4 +2054,4 @@ export const QoderAuthPlugin = makePlugin(SITES.qoder)
|
|
|
1993
2054
|
export const QoderCNAuthPlugin = makePlugin(SITES["qoder-cn"])
|
|
1994
2055
|
|
|
1995
2056
|
// for tests
|
|
1996
|
-
export const _internal = { campaignRuntime, campaignPage, campaignTarget, vpcInstance, vpcSite, parseUsage, gfmt, when, failure, modelInfo, qoderBody, qoderMessages, SITES, decodeBody, events }
|
|
2057
|
+
export const _internal = { campaignRuntime, campaignPage, campaignTarget, vpcInstance, vpcSite, parseUsage, gfmt, when, failure, modelInfo, qoderBody, qoderMessages, cacheMarked, sessionOf, SITES, decodeBody, events }
|
package/package.json
CHANGED