@magpie-community/opencode-qoder-auth 0.2.21 → 0.2.23

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.
Files changed (3) hide show
  1. package/README.md +7 -0
  2. package/index.mjs +60 -7
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -81,6 +81,10 @@ itself.
81
81
  - Messages become text and image blocks.
82
82
  - Tool calls and results become OpenAI tool turns. Images a tool returned
83
83
  follow in a user turn.
84
+ - A result whose id the client lost answers its turn's call in order
85
+ (since 0.2.22, #277): Qoder refuses a whole request with a result that
86
+ answers no call before it. A result with no call left to answer goes
87
+ as text in the user's turn.
84
88
  - Your tools become Qoder's native function tools.
85
89
  - The model's own configuration from Qoder's list goes with it.
86
90
  - It picks the reasoning effort from the model's own levels:
@@ -96,6 +100,9 @@ itself.
96
100
  - the account, AES-encrypted, with the key wrapped by Qoder's RSA key;
97
101
  - an MD5 signature;
98
102
  - the machine id and the client's headers.
103
+ - `Cosy-Version`: the worker runtime version that Qoder's desktop app
104
+ ships (1.1.69 in app 0.4.8, since 0.2.23, #281), because
105
+ `Cosy-ClientType` says the desktop app.
99
106
 
100
107
  **Reading the reply**
101
108
 
package/index.mjs CHANGED
@@ -43,7 +43,14 @@ import { join, resolve } from "node:path"
43
43
 
44
44
  const CHAT_PATH = "/algo/api/v2/service/pro/sse/agent_chat_generation?FetchKeys=llm_model_result&AgentId=agent_common&Encode=1"
45
45
  const MODELS_PATH = "/algo/api/v2/model/list?Encode=1"
46
- const COSY_VERSION = "1.1.49"
46
+ // The build the envelope claims. It has to be the one Qoder's desktop app
47
+ // talks through — the worker runtime bundled in the app — because the
48
+ // envelope also says Cosy-ClientType: 10 (desktop) and carries that app's
49
+ // machine identity; the CLI's channel publishes a different, later number
50
+ // that no desktop build runs. Taken from app 0.4.8 (build 79743374,
51
+ // 2026-10-14), whose transport config names 1.1.69. Raise it when the app
52
+ // moves, not when the CLI does.
53
+ const COSY_VERSION = "1.1.69"
47
54
  const SIGN_IN_TIMEOUT = 15 * 60 * 1000
48
55
  const REFRESH_LEAD = 5 * 60 * 1000 // a chat token this close to its end is refreshed before a request
49
56
  const RENEW_LEAD = 10 * 60 * 1000 // and this close, magpie renews it ahead of time (auth.refresh)
@@ -687,6 +694,17 @@ const textOf = (c) => (typeof c === "string" ? c : blocksOf(c).filter((b) => b.t
687
694
  // blocks, an assistant's tool calls as tool_calls, results as tool turns by
688
695
  // tool_call_id. A tool turn holds text only, so the images a tool returned
689
696
  // follow in a user turn — the start of the user's own when one comes next.
697
+ //
698
+ // Qoder turns the whole request away when a tool turn answers no call of the
699
+ // turn before it ("Messages with role 'tool' must be a response to a
700
+ // preceding message with 'tool_calls'"), where another API takes the same
701
+ // conversation. A client's history reaches here with such a turn because it
702
+ // dropped the id: either the call lost it, so the result names one no call
703
+ // has, or the result kept none. Both are filled in rather than dropped — a
704
+ // call without an id is given one, and the results after it take the ids it
705
+ // went out with in order, which is the pairing a dropped id means. Only a
706
+ // result with no call of its turn left to answer becomes text in the user's
707
+ // own turn, so the words the tool returned stay with the conversation.
690
708
  function qoderMessages(msgs) {
691
709
  const out = []
692
710
  const names = {}
@@ -695,31 +713,64 @@ function qoderMessages(msgs) {
695
713
  if (seen.length) out.push({ role: "user", content: seen })
696
714
  seen = []
697
715
  }
698
- for (const m of msgs) {
716
+ // turn is the assistant turn being answered: the calls no result has
717
+ // answered yet, in the order they were made, and the ones a result after
718
+ // it names (kept), which a result whose id was lost must not take.
719
+ let turn = null
720
+ const asText = (txt, ims) => {
721
+ const said = txt.trim()
722
+ if (!said && !ims.length) return
723
+ if (seen.length) seen.push({ type: "text", text: "\n\n" })
724
+ if (said) seen.push({ type: "text", text: said })
725
+ seen.push(...ims)
726
+ }
727
+ for (const [i, m] of msgs.entries()) {
699
728
  if (m.role === "system" || m.role === "developer") continue
729
+ if (m.role === "user") turn = null
700
730
  if (m.role !== "user" && m.role !== "tool") showSeen()
701
731
  if (m.role === "tool") {
702
732
  let txt = textOf(m.content)
703
733
  const ims = Array.isArray(m.content) ? m.content.filter((p) => p?.type === "image_url").map(imageBlock).filter(Boolean) : []
734
+ const named = String(m.tool_call_id ?? "").trim()
735
+ // A result naming a call of this turn answers it; one whose id the
736
+ // client lost — none, or one no call here has — answers the earliest
737
+ // call still open that no other result names, which is the order the
738
+ // calls were made in.
739
+ const at = named ? turn?.open.findIndex((id) => id === named) ?? -1 : -1
740
+ const pick = at >= 0 ? at : turn ? turn.open.findIndex((id) => !turn.kept.has(id)) : -1
741
+ if (pick < 0) {
742
+ asText(txt, ims)
743
+ continue
744
+ }
745
+ const answered = turn.open.splice(pick, 1)[0]
704
746
  if (ims.length) {
705
- let of = "tool call " + m.tool_call_id
706
- const name = names[m.tool_call_id] || m.name
747
+ let of = "tool call " + answered
748
+ const name = names[answered] || m.name
707
749
  if (name) of = `${name} (${of})`
708
750
  seen.push({ type: "text", text: `[From the result of ${of}:]` }, ...ims)
709
751
  const note = ims.length === 1 ? "[The tool returned an image; it follows in the next message.]" : `[The tool returned ${ims.length} images; they follow in the next message.]`
710
752
  txt = txt.trim() ? txt + "\n\n" + note : txt + note
711
753
  }
712
- out.push({ role: "tool", tool_call_id: m.tool_call_id, content: txt })
754
+ out.push({ role: "tool", tool_call_id: answered, content: txt })
713
755
  continue
714
756
  }
715
757
  if (m.role === "assistant" && m.tool_calls?.length) {
758
+ // a call the client sent without an id gets one, so its result can
759
+ // answer it instead of being turned into text
716
760
  const calls = m.tool_calls.map((c) => {
717
- const id = c.id || "call_" + hexID()
761
+ const id = String(c.id ?? "").trim() || "call_" + hexID()
718
762
  names[id] = c.function?.name
719
763
  const args = c.function?.arguments
720
764
  return { id, type: "function", function: { name: c.function?.name, arguments: typeof args === "string" ? args : JSON.stringify(args ?? {}) } }
721
765
  })
722
766
  out.push({ role: "assistant", content: textOf(m.content), tool_calls: calls })
767
+ const open = calls.map((c) => c.id)
768
+ const kept = new Set()
769
+ for (let j = i + 1; j < msgs.length && msgs[j].role === "tool"; j++) {
770
+ const id = String(msgs[j].tool_call_id ?? "").trim()
771
+ if (open.includes(id)) kept.add(id)
772
+ }
773
+ turn = { open, kept }
723
774
  continue
724
775
  }
725
776
  let blocks = blocksOf(m.content)
@@ -727,6 +778,8 @@ function qoderMessages(msgs) {
727
778
  if (!blocks.length) continue
728
779
  if (m.role === "user" && seen.length) [blocks, seen] = [[...seen, ...blocks], []]
729
780
  out.push({ role: m.role, content: blocks })
781
+ // a result after the assistant's own words answers no call before it
782
+ turn = null
730
783
  }
731
784
  showSeen()
732
785
  return out
@@ -1928,4 +1981,4 @@ export const QoderAuthPlugin = makePlugin(SITES.qoder)
1928
1981
  export const QoderCNAuthPlugin = makePlugin(SITES["qoder-cn"])
1929
1982
 
1930
1983
  // for tests
1931
- export const _internal = { campaignRuntime, campaignPage, campaignTarget, vpcInstance, vpcSite, parseUsage, gfmt, when, failure, modelInfo, qoderBody, SITES, decodeBody, events }
1984
+ export const _internal = { campaignRuntime, campaignPage, campaignTarget, vpcInstance, vpcSite, parseUsage, gfmt, when, failure, modelInfo, qoderBody, qoderMessages, SITES, decodeBody, events }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magpie-community/opencode-qoder-auth",
3
- "version": "0.2.21",
3
+ "version": "0.2.23",
4
4
  "description": "Qoder subscriptions in OpenCode and magpie: Qoder (qoder.com) and Qoder CN (qoder.cn)",
5
5
  "type": "module",
6
6
  "main": "./index.mjs",