@magpie-community/opencode-qoder-auth 0.2.18 → 0.2.20

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 +33 -0
  2. package/index.mjs +25 -9
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -247,6 +247,39 @@ International campaign discovery was checked with a real account on
247
247
  omitting either header hid them. This verification only queried the
248
248
  activity; a live claim was not performed.
249
249
 
250
+ ## Credits available, but a request is refused
251
+
252
+ A positive credit balance does not guarantee a queue slot or access to
253
+ every model. Use the model ids listed for that particular account and
254
+ region. Do not infer another account's permission from its balance.
255
+
256
+ Qoder code **10605**, including `RESOURCE_LIMIT`, `serviceAvailable` or
257
+ `isQueued` details, is a resource/queue answer. Keep its suggested retry
258
+ delay; repeatedly signing in does not free that queue. Since **0.2.18**
259
+ ([#185](https://github.com/magpie-community/plugins/pull/185)), a 10605 led
260
+ by a BOM or wrapped in an outer message is read as busy too; see
261
+ [#167](https://github.com/magpie-community/plugins/issues/167) for the broader report.
262
+ That parser fix does not remove the provider's limit or prove that every
263
+ live 401 had the same cause. A timeout likewise does not establish an
264
+ empty wallet or an expired login.
265
+
266
+ For a bounded offline reproduction, run `packages/qoder/busy.test.mjs`
267
+ from #185 in an isolated temporary HOME. It compares plain queue JSON,
268
+ BOM-prefixed JSON and known message wrappers, keeping explicit login and
269
+ credit failures distinct. Extracting a JSON body from a translated log
270
+ cannot recover the original wire bytes; synthetic wrappers demonstrate
271
+ parser behavior, not which wrapper a historical request actually used.
272
+ After selecting a local fix with magpie's `plugin use`, confirm which spec
273
+ serves `qoder` in `plugin list`; a patched checkout alone does not change
274
+ the running provider.
275
+
276
+
277
+ For native check-in, an existing `CLAIMED` result is not a new credit grant.
278
+ The host's recheck/cache behavior is tracked in
279
+ [magpie#2696](https://github.com/yetone/magpie/issues/2696). Verify a new
280
+ claim against the vendor record; a successful activity query alone is not
281
+ proof that credits arrived.
282
+
250
283
  ## Not here
251
284
 
252
285
  - Qoder's usage and quota display.
package/index.mjs CHANGED
@@ -1015,7 +1015,7 @@ async function* sseLines(body) {
1015
1015
  async function* events(body) {
1016
1016
  const a = new Splitter()
1017
1017
  let index = -1 // the tool call being given
1018
- let native = -1 // the native tool call being assembled
1018
+ const native = new Map() // upstream index -> call and its output index
1019
1019
  // one piece of the split text as an event: a call, text, or a call that
1020
1020
  // can't be read, which ends the turn as a 502
1021
1021
  const piece = (f) =>
@@ -1055,16 +1055,29 @@ async function* events(body) {
1055
1055
  // tool calls served the OpenAI way keep Qoder's id: a result answers it
1056
1056
  for (const tc of delta.tool_calls ?? []) {
1057
1057
  const i = Number(tc.index ?? 0)
1058
- if (i !== native) {
1059
- native = i
1058
+ const name = typeof tc.function?.name === "string" ? tc.function.name.trim() : ""
1059
+ const args = tc.function?.arguments ?? ""
1060
+ if (!tc.id && !name && !args) continue
1061
+ let call = native.get(i)
1062
+ if (!call) {
1063
+ call = { id: "", args: "" }
1064
+ native.set(i, call)
1065
+ }
1066
+ if (tc.id) call.id = tc.id
1067
+ if (call.index === undefined) {
1068
+ // An id or arguments can arrive before the name. Never emit an
1069
+ // unnamed tool: clients persist it and other providers reject it.
1070
+ call.args += args
1071
+ if (!name) continue
1060
1072
  for (const f of a.flush()) {
1061
1073
  const e = piece(f)
1062
1074
  yield e
1063
1075
  if (e.error) return
1064
1076
  }
1065
1077
  a.sawTool = true
1066
- yield { tool: { index: ++index, id: tc.id || "call_" + hexID(), name: tc.function?.name ?? "", args: tc.function?.arguments ?? "" } }
1067
- } else if (tc.function?.arguments) yield { args: { index, text: tc.function.arguments } }
1078
+ call.index = ++index
1079
+ yield { tool: { index: call.index, id: call.id || "call_" + hexID(), name, args: call.args } }
1080
+ } else if (args) yield { args: { index: call.index, text: args } }
1068
1081
  }
1069
1082
  fr = chunk?.choices?.[0]?.finish_reason || fr
1070
1083
  // the usage comes in a chunk of its own after the finish, with no
@@ -1085,6 +1098,10 @@ async function* events(body) {
1085
1098
  yield e
1086
1099
  if (e.error) return
1087
1100
  }
1101
+ if ([...native.values()].some((call) => call.index === undefined)) {
1102
+ yield { error: { status: 502, message: "the model's native tool call has no name; it is not sent on as a tool" } }
1103
+ return
1104
+ }
1088
1105
  yield { stop: a.sawTool ? "tool_calls" : fr === "length" ? "length" : "stop", usage }
1089
1106
  }
1090
1107
 
@@ -1111,7 +1128,7 @@ async function answer(res, chat) {
1111
1128
  if (e.text) msg.content += e.text
1112
1129
  if (e.reasoning) reasoning += e.reasoning
1113
1130
  if (e.tool) calls.push({ id: e.tool.id, type: "function", function: { name: e.tool.name, arguments: e.tool.args } })
1114
- if (e.args) calls[calls.length - 1].function.arguments += e.args.text
1131
+ if (e.args) calls[e.args.index].function.arguments += e.args.text
1115
1132
  if (e.stop) [stop, usage] = [e.stop, e.usage]
1116
1133
  }
1117
1134
  if (reasoning) msg.reasoning_content = reasoning
@@ -1708,9 +1725,8 @@ const makePlugin = (site) => async ({ client }) => {
1708
1725
  try {
1709
1726
  m = (await models(cred)).find((x) => x.key === chat.model) ?? (await models(cred, true)).find((x) => x.key === chat.model)
1710
1727
  } catch (e) {
1711
- // the built-in answered any failure to read the list 400
1712
- // (QoderModelOf), a refused one included, marking nothing
1713
- return errorResponse({ status: 400, message: e.message })
1728
+ if (e instanceof SignInGone || e?.expired) return signedInError(e)
1729
+ return errorResponse({ status: 502, message: e?.message ?? String(e) })
1714
1730
  }
1715
1731
  if (!m) return errorResponse({ status: 400, message: `unknown or disabled model "${chat.model}"` })
1716
1732
  const CHAT_URL = siteOf(site, cred).api + CHAT_PATH
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magpie-community/opencode-qoder-auth",
3
- "version": "0.2.18",
3
+ "version": "0.2.20",
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",