@magpie-community/opencode-qoder-auth 0.2.24 → 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 +19 -3
- package/index.mjs +70 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -22,6 +22,11 @@ hosts; [its section](#qoder-cn) says what differs.
|
|
|
22
22
|
3. The device token is traded for a job token, and the account's email and
|
|
23
23
|
name are read.
|
|
24
24
|
|
|
25
|
+
The page is asked for no `redirect_uri`, as Qoder's CLI asks for none, so
|
|
26
|
+
the sign-in ends in the browser tab even when Qoder.app is installed
|
|
27
|
+
(before 0.2.25 it sent the desktop app's `qoder-app://`, which handed the
|
|
28
|
+
page to Qoder.app).
|
|
29
|
+
|
|
25
30
|
**Sign in with Qoder PAT** (and **Sign in with Qoder CN PAT**) skips the
|
|
26
31
|
device flow: a Personal Access Token from `<site>/account/integrations` is
|
|
27
32
|
posted to `/api/v1/jobToken/exchange` (`personal_token`), which answers a
|
|
@@ -86,6 +91,17 @@ itself.
|
|
|
86
91
|
answers no call before it. A result with no call left to answer goes
|
|
87
92
|
as text in the user's turn.
|
|
88
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`.
|
|
89
105
|
- The model's own configuration from Qoder's list goes with it.
|
|
90
106
|
- It picks the reasoning effort from the model's own levels:
|
|
91
107
|
- The nearest level to the one asked, the higher one on a tie.
|
|
@@ -178,7 +194,7 @@ Qoder CN's own CLI builds in for its `cn` build (`@qodercn-ai/qoderclicn`
|
|
|
178
194
|
| Accounts: poll, tokens, user info, usage | openapi.qoder.sh | openapi.qoder.com.cn |
|
|
179
195
|
| Models and chat | api3.qoder.sh | gateway.qoder.com.cn |
|
|
180
196
|
| Device-flow client id | Qoder's desktop client's | Qoder CN's CLI's (production) |
|
|
181
|
-
| `redirect_uri` on the sign-in page |
|
|
197
|
+
| `redirect_uri` on the sign-in page | none, as Qoder's CLI sends (since 0.2.25) | none, as the CLI sends |
|
|
182
198
|
|
|
183
199
|
Qoder CN's CLI chats with the device token itself and never makes a job
|
|
184
200
|
token. The plugin asks qoder.cn for a job token first, as it does on
|
|
@@ -252,8 +268,8 @@ the account's credentials.
|
|
|
252
268
|
|
|
253
269
|
- Install Qoder desktop, or sign in to Qoder CLI once, to make its native
|
|
254
270
|
runtime available. The plugin finds the desktop's `resources/umid`
|
|
255
|
-
(`Qoder.app` on macOS, `%LOCALAPPDATA%\Programs\Qoder`
|
|
256
|
-
`/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`
|
|
257
273
|
(`runtime-info-<platform>-<arch>-*` on macOS and Linux,
|
|
258
274
|
`umid-win32-x64-*\runtime-info.exe` on Windows; `QODER_CONFIG_DIR`,
|
|
259
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") {
|
|
@@ -1504,7 +1565,10 @@ const SITES = {
|
|
|
1504
1565
|
web: "https://qoder.com",
|
|
1505
1566
|
openapi: "https://openapi.qoder.sh",
|
|
1506
1567
|
api: "https://api3.qoder.sh",
|
|
1507
|
-
|
|
1568
|
+
// none, as Qoder's CLI (@qoder-ai/qodercli 1.1.75) sends: the desktop
|
|
1569
|
+
// app's qoder-app:// hands the finished page to Qoder.app, when it is
|
|
1570
|
+
// installed, instead of leaving it in the tab while this polls (#276)
|
|
1571
|
+
redirect: "",
|
|
1508
1572
|
models: MODELS,
|
|
1509
1573
|
},
|
|
1510
1574
|
"qoder-cn": {
|
|
@@ -1990,4 +2054,4 @@ export const QoderAuthPlugin = makePlugin(SITES.qoder)
|
|
|
1990
2054
|
export const QoderCNAuthPlugin = makePlugin(SITES["qoder-cn"])
|
|
1991
2055
|
|
|
1992
2056
|
// for tests
|
|
1993
|
-
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