@magpie-community/opencode-qoder-auth 0.2.25 → 0.2.27
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 +23 -5
- package/index.mjs +84 -6
- 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
|
|
@@ -266,9 +277,16 @@ the account's credentials.
|
|
|
266
277
|
- For another installation location, set `QODER_RUNTIME_INFO` to the
|
|
267
278
|
absolute path of Qoder's installed `runtime-info` executable
|
|
268
279
|
(`runtime-info.exe` on Windows), with its accompanying SDK files.
|
|
269
|
-
-
|
|
270
|
-
|
|
271
|
-
|
|
280
|
+
- With no helper anywhere (a machine that never had Qoder desktop or
|
|
281
|
+
Qoder CLI), the check-in is `ineligible` for the day, with a message
|
|
282
|
+
saying to install one (or set `QODER_RUNTIME_INFO`); magpie asks again
|
|
283
|
+
the next day rather than every 30 minutes (#304). The identity comes
|
|
284
|
+
only from Qoder's own helper: the plugin never makes one up, so such an
|
|
285
|
+
international account gets no daily claim until Qoder is installed.
|
|
286
|
+
- A helper that is there but can't be read, or gives an invalid identity,
|
|
287
|
+
is `failed` and asked again later, rather than a false "no activity"
|
|
288
|
+
result. Qoder CN keeps its existing requests and does not need this
|
|
289
|
+
local helper.
|
|
272
290
|
|
|
273
291
|
- **List:** `GET <openapi>/sash/api/v1/me/campaigns`: `campaigns[]` with
|
|
274
292
|
`campaignId`, `actionType`, `claimStatus` (`CLAIMABLE`, `CLAIMED`),
|
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") {
|
|
@@ -1347,7 +1408,21 @@ async function campaignRuntime({ home = homedir(), platform = process.platform,
|
|
|
1347
1408
|
if (path && (await isFile(path))) found.push({ path, time: (await stat(path)).mtimeMs })
|
|
1348
1409
|
}
|
|
1349
1410
|
if (found.length) return found.sort((a, b) => b.time - a.time)[0].path
|
|
1350
|
-
throw new
|
|
1411
|
+
throw new RuntimeMissing()
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
// RuntimeMissing: no Qoder desktop or CLI on this machine to read the
|
|
1415
|
+
// device identity from. Qoder offers the daily credits only on a request
|
|
1416
|
+
// carrying it (without it the list answers 200 with no CLAIM_BENEFIT), and
|
|
1417
|
+
// the identity is the output of Qoder's own signed helper, which the plugin
|
|
1418
|
+
// never makes up. Asking again won't change that until the user installs
|
|
1419
|
+
// Qoder, so the check-in says what to do once instead of "tries again later".
|
|
1420
|
+
class RuntimeMissing extends Error {
|
|
1421
|
+
constructor() {
|
|
1422
|
+
super(
|
|
1423
|
+
"Qoder check-in: device identity runtime not found. Qoder gives the daily credits only to a device its own app identifies, and this machine has neither Qoder desktop nor Qoder CLI to read that identity from. Install Qoder desktop (or install Qoder CLI and sign in to it once), or set QODER_RUNTIME_INFO to its runtime-info executable; magpie checks in again the next day",
|
|
1424
|
+
)
|
|
1425
|
+
}
|
|
1351
1426
|
}
|
|
1352
1427
|
|
|
1353
1428
|
// Read the SDK identity as Qoder does: environment 3 is international; on
|
|
@@ -1790,6 +1865,9 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
1790
1865
|
}
|
|
1791
1866
|
return { outcome: "failed", message: `claim: ${said || "unknown answer"}` }
|
|
1792
1867
|
} catch (e) {
|
|
1868
|
+
// no Qoder on this machine: an answer for the day, not a failure
|
|
1869
|
+
// magpie would ask again every 30 minutes (plugins#304)
|
|
1870
|
+
if (e instanceof RuntimeMissing) return { outcome: "ineligible", message: e.message }
|
|
1793
1871
|
return { outcome: "failed", message: e?.message ?? String(e), ...(e?.expired ? { signIn: "expired" } : {}) }
|
|
1794
1872
|
}
|
|
1795
1873
|
}
|
|
@@ -1993,4 +2071,4 @@ export const QoderAuthPlugin = makePlugin(SITES.qoder)
|
|
|
1993
2071
|
export const QoderCNAuthPlugin = makePlugin(SITES["qoder-cn"])
|
|
1994
2072
|
|
|
1995
2073
|
// for tests
|
|
1996
|
-
export const _internal = { campaignRuntime, campaignPage, campaignTarget, vpcInstance, vpcSite, parseUsage, gfmt, when, failure, modelInfo, qoderBody, qoderMessages, SITES, decodeBody, events }
|
|
2074
|
+
export const _internal = { campaignRuntime, campaignPage, campaignTarget, vpcInstance, vpcSite, parseUsage, gfmt, when, failure, modelInfo, qoderBody, qoderMessages, cacheMarked, sessionOf, SITES, decodeBody, events }
|
package/package.json
CHANGED