@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.
- package/index.mjs +132 -15
- 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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
555
|
-
if (
|
|
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
|
|
564
|
-
let keep =
|
|
565
|
-
if (!keep) for (let k = Math.min(
|
|
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 ||
|
|
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.
|
|
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",
|