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

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 +4 -0
  2. package/index.mjs +52 -6
  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:
package/index.mjs CHANGED
@@ -687,6 +687,17 @@ const textOf = (c) => (typeof c === "string" ? c : blocksOf(c).filter((b) => b.t
687
687
  // blocks, an assistant's tool calls as tool_calls, results as tool turns by
688
688
  // tool_call_id. A tool turn holds text only, so the images a tool returned
689
689
  // follow in a user turn — the start of the user's own when one comes next.
690
+ //
691
+ // Qoder turns the whole request away when a tool turn answers no call of the
692
+ // turn before it ("Messages with role 'tool' must be a response to a
693
+ // preceding message with 'tool_calls'"), where another API takes the same
694
+ // conversation. A client's history reaches here with such a turn because it
695
+ // dropped the id: either the call lost it, so the result names one no call
696
+ // has, or the result kept none. Both are filled in rather than dropped — a
697
+ // call without an id is given one, and the results after it take the ids it
698
+ // went out with in order, which is the pairing a dropped id means. Only a
699
+ // result with no call of its turn left to answer becomes text in the user's
700
+ // own turn, so the words the tool returned stay with the conversation.
690
701
  function qoderMessages(msgs) {
691
702
  const out = []
692
703
  const names = {}
@@ -695,31 +706,64 @@ function qoderMessages(msgs) {
695
706
  if (seen.length) out.push({ role: "user", content: seen })
696
707
  seen = []
697
708
  }
698
- for (const m of msgs) {
709
+ // turn is the assistant turn being answered: the calls no result has
710
+ // answered yet, in the order they were made, and the ones a result after
711
+ // it names (kept), which a result whose id was lost must not take.
712
+ let turn = null
713
+ const asText = (txt, ims) => {
714
+ const said = txt.trim()
715
+ if (!said && !ims.length) return
716
+ if (seen.length) seen.push({ type: "text", text: "\n\n" })
717
+ if (said) seen.push({ type: "text", text: said })
718
+ seen.push(...ims)
719
+ }
720
+ for (const [i, m] of msgs.entries()) {
699
721
  if (m.role === "system" || m.role === "developer") continue
722
+ if (m.role === "user") turn = null
700
723
  if (m.role !== "user" && m.role !== "tool") showSeen()
701
724
  if (m.role === "tool") {
702
725
  let txt = textOf(m.content)
703
726
  const ims = Array.isArray(m.content) ? m.content.filter((p) => p?.type === "image_url").map(imageBlock).filter(Boolean) : []
727
+ const named = String(m.tool_call_id ?? "").trim()
728
+ // A result naming a call of this turn answers it; one whose id the
729
+ // client lost — none, or one no call here has — answers the earliest
730
+ // call still open that no other result names, which is the order the
731
+ // calls were made in.
732
+ const at = named ? turn?.open.findIndex((id) => id === named) ?? -1 : -1
733
+ const pick = at >= 0 ? at : turn ? turn.open.findIndex((id) => !turn.kept.has(id)) : -1
734
+ if (pick < 0) {
735
+ asText(txt, ims)
736
+ continue
737
+ }
738
+ const answered = turn.open.splice(pick, 1)[0]
704
739
  if (ims.length) {
705
- let of = "tool call " + m.tool_call_id
706
- const name = names[m.tool_call_id] || m.name
740
+ let of = "tool call " + answered
741
+ const name = names[answered] || m.name
707
742
  if (name) of = `${name} (${of})`
708
743
  seen.push({ type: "text", text: `[From the result of ${of}:]` }, ...ims)
709
744
  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
745
  txt = txt.trim() ? txt + "\n\n" + note : txt + note
711
746
  }
712
- out.push({ role: "tool", tool_call_id: m.tool_call_id, content: txt })
747
+ out.push({ role: "tool", tool_call_id: answered, content: txt })
713
748
  continue
714
749
  }
715
750
  if (m.role === "assistant" && m.tool_calls?.length) {
751
+ // a call the client sent without an id gets one, so its result can
752
+ // answer it instead of being turned into text
716
753
  const calls = m.tool_calls.map((c) => {
717
- const id = c.id || "call_" + hexID()
754
+ const id = String(c.id ?? "").trim() || "call_" + hexID()
718
755
  names[id] = c.function?.name
719
756
  const args = c.function?.arguments
720
757
  return { id, type: "function", function: { name: c.function?.name, arguments: typeof args === "string" ? args : JSON.stringify(args ?? {}) } }
721
758
  })
722
759
  out.push({ role: "assistant", content: textOf(m.content), tool_calls: calls })
760
+ const open = calls.map((c) => c.id)
761
+ const kept = new Set()
762
+ for (let j = i + 1; j < msgs.length && msgs[j].role === "tool"; j++) {
763
+ const id = String(msgs[j].tool_call_id ?? "").trim()
764
+ if (open.includes(id)) kept.add(id)
765
+ }
766
+ turn = { open, kept }
723
767
  continue
724
768
  }
725
769
  let blocks = blocksOf(m.content)
@@ -727,6 +771,8 @@ function qoderMessages(msgs) {
727
771
  if (!blocks.length) continue
728
772
  if (m.role === "user" && seen.length) [blocks, seen] = [[...seen, ...blocks], []]
729
773
  out.push({ role: m.role, content: blocks })
774
+ // a result after the assistant's own words answers no call before it
775
+ turn = null
730
776
  }
731
777
  showSeen()
732
778
  return out
@@ -1928,4 +1974,4 @@ export const QoderAuthPlugin = makePlugin(SITES.qoder)
1928
1974
  export const QoderCNAuthPlugin = makePlugin(SITES["qoder-cn"])
1929
1975
 
1930
1976
  // for tests
1931
- export const _internal = { campaignRuntime, campaignPage, campaignTarget, vpcInstance, vpcSite, parseUsage, gfmt, when, failure, modelInfo, qoderBody, SITES, decodeBody, events }
1977
+ 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.22",
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",