@magpie-community/opencode-trae-auth 0.1.10 → 0.1.12

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 (2) hide show
  1. package/index.mjs +132 -15
  2. package/package.json +1 -1
package/index.mjs CHANGED
@@ -39,12 +39,15 @@ const DAY = 24 * 3600 * 1000
39
39
  // then SOLO Lite's agent
40
40
  const FUNCTIONS = ["chat_v3", "solo_work_lite", "solo_agent", "solo_agent_lite"]
41
41
 
42
- const MODEL = { attachment: false, tool_call: true, reasoning: true, temperature: true, limit: { context: 128_000, output: 32_000 }, modalities: { input: ["text"], output: ["text"] } }
42
+ // a reply limit of 0 is one Trae doesn't say: magpie then takes models.dev's
43
+ // for the model (magpie's catalog), where a made-up 32K told agents a far
44
+ // smaller one than DeepSeek V4.1 Flash's (ARNO on magpie's Discord)
45
+ const MODEL = { attachment: false, tool_call: true, reasoning: true, temperature: true, limit: { context: 128_000, output: 0 }, modalities: { input: ["text"], output: ["text"] } }
43
46
  // what Trae CN's chat_v3 is known to serve; the live list replaces it
44
47
  const MODELS = {
45
48
  "glm-5.2": { name: "GLM-5.2", ...MODEL, limit: { context: 200_000, output: 32_000 } },
46
49
  "glm-5": { name: "GLM-5", ...MODEL },
47
- "kimi-k2.6": { name: "Kimi K2.6", ...MODEL, limit: { context: 256_000, output: 32_000 } },
50
+ "kimi-k2.6": { name: "Kimi K2.6", ...MODEL, limit: { context: 256_000, output: 0 } },
48
51
  "qwen-3.7-plus": { name: "Qwen 3.7 Plus", ...MODEL },
49
52
  "DeepSeek-V4-Pro": { name: "DeepSeek V4 Pro", ...MODEL },
50
53
  "DeepSeek-V4-Flash": { name: "DeepSeek V4 Flash", ...MODEL },
@@ -444,8 +447,10 @@ const nativeTools = (tools) =>
444
447
  })
445
448
 
446
449
  // chatBody is the request for llm_utils_chat; modelName is the model the
447
- // function's list names for the config (its __dev one), when it named one
448
- function chatBody(req, fn, modelName) {
450
+ // function's list names for the config (its __dev one), when it named one,
451
+ // and most the max_tokens it gives that model (0: none given), which a
452
+ // request asking more is held to
453
+ function chatBody(req, fn, modelName, most = 0) {
449
454
  const session = randomUUID()
450
455
  const body = {
451
456
  messages: traeMessages(req),
@@ -458,7 +463,7 @@ function chatBody(req, fn, modelName) {
458
463
  session_id: session,
459
464
  }
460
465
  const max = req.max_completion_tokens ?? req.max_tokens
461
- if (Number.isFinite(max) && max > 0) body.max_tokens = Math.floor(max)
466
+ if (Number.isFinite(max) && max > 0) body.max_tokens = most > 0 ? Math.min(Math.floor(max), most) : Math.floor(max)
462
467
  if (typeof req.temperature === "number") body.temperature = req.temperature
463
468
  if (Array.isArray(req.tools) && req.tools.length && req.tool_choice !== "none") {
464
469
  body.tools = nativeTools(req.tools)
@@ -534,25 +539,79 @@ const callId = () => "call_" + randomBytes(12).toString("hex") + (callSeq++).toS
534
539
 
535
540
  // TextTools splits the model's text into what the agent sees and the tool
536
541
  // calls it wrote as blocks, holding back the start of a block until it
537
- // is whole
542
+ // is whole. A block is ours (<tool_call>{…}</tool_call>), its JSON taken
543
+ // once whole even when DeepSeek closes it with its own tags in place of
544
+ // </tool_call>, or DeepSeek's (<|DSML|invoke name="…">, magpie#823); the
545
+ // tags left around a call are dropped.
538
546
  class TextTools {
539
547
  constructor() {
540
548
  this.buf = ""
549
+ this.after = false
541
550
  }
542
551
  push(s, end = false) {
543
552
  this.buf += s
544
553
  let text = ""
545
554
  const calls = []
546
555
  for (;;) {
556
+ if (this.after) {
557
+ const m = this.buf.match(LEFTOVER)
558
+ if (m) this.buf = this.buf.slice(m[0].length)
559
+ if (!this.buf || (!end && MARKS.some((k) => k.startsWith(this.buf)))) break
560
+ this.after = false
561
+ }
547
562
  const i = this.buf.indexOf(OPEN)
548
- if (i < 0) break
563
+ const d = this.buf.search(DSML_AT)
564
+ if (i < 0 && d < 0) break
565
+ if (d >= 0 && (i < 0 || d < i)) {
566
+ text += this.buf.slice(0, d)
567
+ this.buf = this.buf.slice(d)
568
+ const tag = this.buf.match(DSML_TAG)
569
+ if (!tag) {
570
+ // a tag not closed yet; at the end, or with another tag after it,
571
+ // a stray one dropped
572
+ const next = this.buf.indexOf("<", 1)
573
+ if (next < 0 && !end) break
574
+ this.buf = next < 0 ? "" : this.buf.slice(next)
575
+ continue
576
+ }
577
+ const inv = tag[0].match(DSML_INVOKE)
578
+ if (!inv) {
579
+ // function_calls, a parameter's close, any other tag: dropped,
580
+ // with the blanks after it
581
+ this.buf = this.buf.slice(tag[0].length)
582
+ this.after = true
583
+ continue
584
+ }
585
+ const body = this.buf.slice(tag[0].length)
586
+ const c = body.match(DSML_CLOSE)
587
+ const n = body.search(DSML_INVOKE_AT)
588
+ let stop = c ? c.index : n >= 0 ? n : -1
589
+ if (n >= 0 && n < stop) stop = n
590
+ if (stop < 0) {
591
+ if (!end) break
592
+ stop = body.length
593
+ }
594
+ calls.push({ name: inv[1], arguments: dsmlArgs(body.slice(0, stop)) })
595
+ this.buf = body.slice(c && c.index === stop ? stop + c[0].length : stop)
596
+ this.after = true
597
+ continue
598
+ }
599
+ const o = objectEnd(this.buf, i + OPEN.length)
600
+ const v = o > 0 ? looseJSON(this.buf.slice(i + OPEN.length, o)) : null
601
+ if (v?.name) {
602
+ text += this.buf.slice(0, i)
603
+ calls.push({ name: String(v.name), arguments: callArgs(v) })
604
+ this.buf = this.buf.slice(o)
605
+ this.after = true
606
+ continue
607
+ }
549
608
  const j = this.buf.indexOf(CLOSE, i + OPEN.length)
550
609
  if (j < 0) break
551
610
  text += this.buf.slice(0, i)
552
611
  const raw = this.buf.slice(i + OPEN.length, j).trim()
553
612
  this.buf = this.buf.slice(j + CLOSE.length)
554
- const v = looseJSON(raw) ?? {}
555
- if (v.name) calls.push({ name: String(v.name), arguments: callArgs(v) })
613
+ const w = looseJSON(raw) ?? {}
614
+ if (w.name) calls.push({ name: String(w.name), arguments: callArgs(w) })
556
615
  else text += OPEN + raw + CLOSE
557
616
  }
558
617
  if (end) {
@@ -560,9 +619,9 @@ class TextTools {
560
619
  this.buf = ""
561
620
  } else {
562
621
  // keep back an opened block, or what may be the start of one
563
- const i = this.buf.indexOf(OPEN)
564
- let keep = i >= 0 ? this.buf.length - i : 0
565
- if (!keep) for (let k = Math.min(OPEN.length - 1, this.buf.length); k > 0; k--) if (OPEN.startsWith(this.buf.slice(-k))) { keep = k; break }
622
+ const at = [this.buf.indexOf(OPEN), this.buf.search(DSML_AT)].filter((x) => x >= 0)
623
+ let keep = at.length ? this.buf.length - Math.min(...at) : 0
624
+ if (!keep) for (let k = Math.min(MARK_MAX - 1, this.buf.length); k > 0; k--) if (MARKS.some((m) => m.startsWith(this.buf.slice(-k)))) { keep = k; break }
566
625
  text += this.buf.slice(0, this.buf.length - keep)
567
626
  this.buf = this.buf.slice(this.buf.length - keep)
568
627
  }
@@ -570,6 +629,64 @@ class TextTools {
570
629
  }
571
630
  }
572
631
 
632
+ // DeepSeek's tags, with its full-width bar or a plain one
633
+ const DSML = String.raw`<\s*\/?\s*[||][\s||]*DSML[\s||]*`
634
+ const DSML_AT = new RegExp(DSML)
635
+ const DSML_TAG = new RegExp("^" + DSML + "[^<>]*>")
636
+ const DSML_INVOKE = new RegExp(String.raw`^<\s*[||][\s||]*DSML[\s||]*invoke\s+name\s*=\s*"([^"]+)"\s*>`)
637
+ const DSML_INVOKE_AT = new RegExp(String.raw`<\s*[||][\s||]*DSML[\s||]*invoke\s+name\s*=`)
638
+ const DSML_CLOSE = new RegExp(String.raw`<\s*\/\s*[||][\s||]*DSML[\s||]*invoke\s*>`)
639
+ const DSML_PARAM = new RegExp(String.raw`<\s*[||][\s||]*DSML[\s||]*parameter\s+name\s*=\s*"([^"]+)"((?:\s+\w+\s*=\s*"[^"]*")*)\s*>([\s\S]*?)` + DSML + String.raw`parameter\s*>`, "g")
640
+ // what is left after a call: blanks, our close, DeepSeek's tags but the
641
+ // next call's
642
+ const LEFTOVER = new RegExp(String.raw`^(?:\s+|<\/tool_calls?>|<tool_calls>|` + DSML + String.raw`(?![\s||]*invoke\b)[^<>]*>)+`)
643
+ // what text may be the start of, so held back
644
+ const MARKS = [OPEN, CLOSE, "<|DSML|", "<|DSML|", "</|DSML|", "</|DSML|"]
645
+ const MARK_MAX = Math.max(...MARKS.map((m) => m.length))
646
+
647
+ // dsmlArgs is the arguments of a DSML invoke as JSON: a parameter marked
648
+ // string="true" is its text, any other its JSON (its text when not JSON)
649
+ function dsmlArgs(body) {
650
+ const args = {}
651
+ for (const p of body.matchAll(DSML_PARAM)) {
652
+ if (/string\s*=\s*"true"/.test(p[2])) {
653
+ args[p[1]] = p[3]
654
+ continue
655
+ }
656
+ try {
657
+ args[p[1]] = JSON.parse(p[3].trim())
658
+ } catch {
659
+ args[p[1]] = p[3]
660
+ }
661
+ }
662
+ return JSON.stringify(args)
663
+ }
664
+
665
+ // objectEnd is where the JSON object starting at from (after blanks) ends:
666
+ // past its closing brace, -1 when it isn't whole yet, -2 when there's none
667
+ function objectEnd(s, from) {
668
+ let k = from
669
+ while (k < s.length && /\s/.test(s[k])) k++
670
+ if (k >= s.length) return -1
671
+ if (s[k] !== "{") return -2
672
+ let depth = 0
673
+ let str = false
674
+ let esc = false
675
+ for (; k < s.length; k++) {
676
+ const ch = s[k]
677
+ if (str) {
678
+ if (esc) esc = false
679
+ else if (ch === "\\") esc = true
680
+ else if (ch === '"') str = false
681
+ continue
682
+ }
683
+ if (ch === '"') str = true
684
+ else if (ch === "{") depth++
685
+ else if (ch === "}" && --depth === 0) return k + 1
686
+ }
687
+ return -1
688
+ }
689
+
573
690
  // looseJSON reads a JSON object as a model writes one: a string holding a
574
691
  // raw newline or tab (a file's content written out, #799) is read as if
575
692
  // they were escaped. null when it isn't one even so.
@@ -1085,7 +1202,7 @@ export const TraeCNAuthPlugin = async ({ client }) => {
1085
1202
  for (const [id, { m, fn }] of out) {
1086
1203
  listedBy.set(id, fn)
1087
1204
  const dev = devModel(m)
1088
- if (dev) modelNames.set(id, { fn, name: dev.model_name })
1205
+ if (dev) modelNames.set(id, { fn, name: dev.model_name, most: Number(dev.max_tokens) || 0 })
1089
1206
  else modelNames.delete(id)
1090
1207
  }
1091
1208
  return [...out.values()].map((x) => x.m)
@@ -1096,7 +1213,7 @@ export const TraeCNAuthPlugin = async ({ client }) => {
1096
1213
  const was = provider.models?.[id] ?? {}
1097
1214
  // context_window_tokens: dev is what a request gets, max only in Max mode
1098
1215
  const ctx = Number(m.context_window_tokens?.dev ?? m.context_window_size?.max?.[0] ?? m.context_window_size?.max ?? m.context_window_tokens?.max ?? m.prompt_max_tokens) || was.limit?.context || MODEL.limit.context
1099
- const out = Number(devModel(m)?.max_tokens) || was.limit?.output || MODEL.limit.output
1216
+ const out = Number(devModel(m)?.max_tokens) || was.limit?.output || 0
1100
1217
  const name = m.display_config?.display_name || m.display_name || m.display_model_name || was.name || id
1101
1218
  return { ...MODEL, ...was, id, providerID: ID, name: String(name), limit: { context: ctx, output: out }, api: was.api ?? { id, url: HOSTS.api, npm: "@ai-sdk/openai-compatible" } }
1102
1219
  }
@@ -1169,7 +1286,7 @@ export const TraeCNAuthPlugin = async ({ client }) => {
1169
1286
  const res = await fetch(apiOf(a) + "/api/agent/v3/llm_utils_chat", {
1170
1287
  method: "POST",
1171
1288
  headers: ideHeaders(a, { Accept: "text/event-stream", "X-Request-ID": randomUUID() }),
1172
- body: JSON.stringify(chatBody(req, fn, named?.fn === fn ? named.name : "")),
1289
+ body: JSON.stringify(chatBody(req, fn, named?.fn === fn ? named.name : "", named?.fn === fn ? named.most : 0)),
1173
1290
  signal: init.signal ?? r0?.signal,
1174
1291
  })
1175
1292
  if (!res.ok) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magpie-community/opencode-trae-auth",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "description": "OpenCode provider plugin for Trae CN (trae.cn): your Trae CN account's models, free tier included, with Trae's own browser sign-in",
5
5
  "type": "module",
6
6
  "main": "./index.mjs",