@magpie-community/opencode-qoder-auth 0.2.4 → 0.2.6

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 +9 -1
  2. package/index.mjs +60 -4
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -62,6 +62,10 @@ itself.
62
62
  - The nearest level to the one asked, the higher one on a tie.
63
63
  - The model's default when none is asked.
64
64
  - The lowest level for "none", when the model can't turn thinking off.
65
+ - It picks the context window (`context_length`) from the model's own
66
+ windows (Qoder's Context setting: 200K, 400K, 1M): the default while the
67
+ request fits it, else the smallest that holds it, else the largest. A
68
+ larger window may cost more, so it is asked for only when needed.
65
69
  - It encodes the body with the client's codec and signs the call with the
66
70
  client's COSY envelope. The envelope carries:
67
71
  - the account, AES-encrypted, with the key wrapped by Qoder's RSA key;
@@ -89,7 +93,11 @@ itself.
89
93
  The `provider.models` hook reads the account's own list, as Qoder's client
90
94
  asks for it (`/algo/api/v2/model/list`). It keeps the enabled chat models
91
95
  and leaves out "auto" and "default", which route inside Qoder. It takes
92
- each model's reasoning levels from its `thinking_config`.
96
+ each model's reasoning levels from its `thinking_config`, and its context
97
+ from its `context_config`: the largest window it offers (Qoder's docs
98
+ give Qwen3.8-Flash 200K, 400K and 1M), not `max_input_tokens` (180K
99
+ there), which is only what a request naming no window gets. A model listed
100
+ with no windows keeps `max_input_tokens`.
93
101
 
94
102
  The `config` hook declares the list as it was on 2026-09-30:
95
103
 
package/index.mjs CHANGED
@@ -340,15 +340,51 @@ function rateOf(raw) {
340
340
  return { rate, rateWas }
341
341
  }
342
342
 
343
+ // windowsOf reads the context windows a listing entry offers, smallest
344
+ // first, and the one Qoder's client picks unless asked for another:
345
+ // context_config's entries ({"200k": {token_count: 200000, is_default:
346
+ // true}, "1m": {token_count: 1000000}, …}), else available_context_windows,
347
+ // as Qoder's client reads them (qodercli's catalog). Qoder's Context
348
+ // setting (200K / 400K / 1M) offers them on Qwen3.8-Flash and the rest;
349
+ // max_input_tokens (180K there) is only what a request that names none
350
+ // gets.
351
+ function windowsOf(raw) {
352
+ const n = (v) => (Number.isInteger(v) && v > 0 ? v : 0)
353
+ const windows = []
354
+ let def = 0
355
+ const cc = raw?.context_config ?? raw?.contextConfig
356
+ if (cc && typeof cc === "object" && !Array.isArray(cc)) {
357
+ for (const e of Object.values(cc)) {
358
+ const t = n(e?.token_count)
359
+ if (!t) continue
360
+ if (!windows.includes(t)) windows.push(t)
361
+ if (e.is_default === true || e.is_default === 1 || e.is_default === "1") def = t
362
+ }
363
+ }
364
+ if (!windows.length) {
365
+ const list = raw?.available_context_windows ?? raw?.availableContextWindows
366
+ for (const v of Array.isArray(list) ? list : []) if (n(v) && !windows.includes(v)) windows.push(v)
367
+ def = n(raw?.default_context_window ?? raw?.defaultContextWindow)
368
+ if (!windows.includes(def)) def = 0
369
+ }
370
+ windows.sort((a, b) => a - b)
371
+ return { windows, defaultWindow: def }
372
+ }
373
+
343
374
  // modelInfo reads one entry of the listing's "chat" array: its efforts and
344
- // default from thinking_config (is_reasoning alone when it has none).
375
+ // default from thinking_config (is_reasoning alone when it has none), and
376
+ // its context: the largest window it offers (windowsOf), since a request
377
+ // that needs more than the default is sent a larger one (windowFor).
345
378
  function modelInfo(raw) {
379
+ const { windows, defaultWindow } = windowsOf(raw)
346
380
  const m = {
347
381
  key: raw.key,
348
382
  source: raw.source ?? "",
349
383
  name: raw.display_name || raw.key,
350
384
  images: !!raw.is_vl,
351
- context: raw.max_input_tokens || 0,
385
+ context: windows.length ? windows[windows.length - 1] : raw.max_input_tokens || 0,
386
+ windows,
387
+ defaultWindow,
352
388
  thinks: !!raw.is_reasoning,
353
389
  alwaysThinks: false,
354
390
  efforts: Array.isArray(raw.reasoning_efforts) ? [...raw.reasoning_efforts] : [],
@@ -533,6 +569,25 @@ function qoderMessages(msgs) {
533
569
  return out
534
570
  }
535
571
 
572
+ // windowFor is the context window a request is sent (context_length) on a
573
+ // model that offers several: the one Qoder's client picks by default
574
+ // (max_input_tokens without one) while the request fits it, else the
575
+ // smallest that holds it, else the largest. A larger window may cost more
576
+ // (Qoder: changing the Context "may change the rate"), so it is asked for
577
+ // only when the conversation needs it. The request's size is counted
578
+ // generously, a token for every 3 bytes of its UTF-8 JSON, so a window is
579
+ // raised a little early rather than late, and the reply's max_tokens is
580
+ // counted in it: a window holds the prompt and the reply, so a
581
+ // conversation of 180K asking 32K for the reply is sent 400K, not the 200K
582
+ // it would overflow while magpie says the model takes 1M (yetone/magpie#700).
583
+ function windowFor(chat, m, reply = 0) {
584
+ const base = m.defaultWindow || (m.config?.max_input_tokens > 0 ? m.config.max_input_tokens : 0) || m.context
585
+ if (!m.windows?.length) return m.context
586
+ const need = Math.ceil(Buffer.byteLength(JSON.stringify({ m: chat.messages ?? [], t: chat.tools ?? [] })) / 3) + Math.max(0, reply)
587
+ if (base > 0 && need <= base) return base
588
+ return m.windows.find((w) => w >= need) ?? m.windows[m.windows.length - 1]
589
+ }
590
+
536
591
  // qoderBody is the plaintext agent_chat_generation takes for a chat
537
592
  // completion request, for model m.
538
593
  function qoderBody(chat, m) {
@@ -544,7 +599,8 @@ function qoderBody(chat, m) {
544
599
  const [thinking, effort] = effortFor(chat.reasoning_effort, m)
545
600
  const params = { enable_thinking: thinking, max_tokens: chat.max_completion_tokens || chat.max_tokens || 32000 }
546
601
  if (effort) params.reasoning_effort = effort
547
- if (m.context > 0) params.context_length = m.context
602
+ const window = windowFor(chat, m, params.max_tokens)
603
+ if (window > 0) params.context_length = window
548
604
  const body = {
549
605
  parameters: params,
550
606
  business: { product: "app", version: COSY_VERSION, type: "agent", id: hexID(), name: "magpie session", begin_at: Date.now(), stage: "start" },
@@ -1253,4 +1309,4 @@ export const QoderAuthPlugin = makePlugin(SITES.qoder)
1253
1309
  export const QoderCNAuthPlugin = makePlugin(SITES["qoder-cn"])
1254
1310
 
1255
1311
  // for tests
1256
- export const _internal = { parseUsage, gfmt, when, failure, modelInfo, SITES, decodeBody, events }
1312
+ export const _internal = { parseUsage, gfmt, when, failure, modelInfo, qoderBody, 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.4",
3
+ "version": "0.2.6",
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",