@magpie-community/opencode-trae-auth 0.1.12 → 0.1.14
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/README.md +29 -4
- package/index.mjs +262 -43
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -44,17 +44,31 @@ agent's `POST https://trae-api-cn.mchost.guru/api/agent/v3/llm_utils_chat`:
|
|
|
44
44
|
`x-app-id`, the device headers and `x-uid`.
|
|
45
45
|
- **Answer:** Trae always answers in its own SSE events:
|
|
46
46
|
- `output` gives `response`/`content` and `reasoning_content`/`reasoning`.
|
|
47
|
+
Text under `message`, `delta` or an event with no name is the answer's
|
|
48
|
+
too.
|
|
47
49
|
- `token_usage`, `done` and `error` mark usage, the end and failures.
|
|
50
|
+
`done`'s `finish_reason` `length`/`max_tokens` is OpenAI's `length`.
|
|
48
51
|
- Queue events are ignored.
|
|
52
|
+
- A `{"reasoning_content": …}` object written into the text is reasoning,
|
|
53
|
+
and reasoning Trae sends again is sent once.
|
|
49
54
|
|
|
50
|
-
The plugin turns these into an OpenAI stream or a single body.
|
|
55
|
+
The plugin turns these into an OpenAI stream or a single body. A reply
|
|
56
|
+
that fails (an `error` event, or Trae's stream breaking off) ends with the
|
|
57
|
+
error and `[DONE]`, with no finish.
|
|
51
58
|
- **Tools:**
|
|
52
59
|
- Trae's chat has no turn for a tool call or its result, so the tools are
|
|
53
60
|
named twice:
|
|
54
61
|
- natively in `tools`, with `parameters` as a JSON string;
|
|
55
62
|
- in a system prompt that asks for each call as a
|
|
56
63
|
`<tool_call>{"name", "arguments"}</tool_call>` block.
|
|
57
|
-
-
|
|
64
|
+
- GLM also writes its own template, `<tool_call>name<arg_key>k</arg_key>
|
|
65
|
+
<arg_value>v</arg_value></tool_call>` (a string kept as written, other
|
|
66
|
+
types read as JSON), and sometimes a whole call into a native call's
|
|
67
|
+
name; both are read as the call.
|
|
68
|
+
- Calls from either source become OpenAI `tool_calls`. A name matching a
|
|
69
|
+
requested tool but for case or punctuation (`Read`) goes on as the
|
|
70
|
+
request's (`read`); any other goes on as written, so the agent can say
|
|
71
|
+
the tool doesn't exist.
|
|
58
72
|
- Earlier calls and their results go back in as text.
|
|
59
73
|
- Images aren't sent.
|
|
60
74
|
|
|
@@ -65,8 +79,19 @@ time, when the batch gives none). The lists are put together: SOLO and the
|
|
|
65
79
|
TRAE agent list models `chat_v3` doesn't (deepseek-v4.1-flash is the TRAE
|
|
66
80
|
agent's, `solo_agent`). Left out are the IDE's helpers (`usage` other than
|
|
67
81
|
`chat_completion`: summary, fast_apply…), configs switched off and the
|
|
68
|
-
custom-model slots. Context is the list's `context_window_tokens.dev
|
|
69
|
-
|
|
82
|
+
custom-model slots. Context is the list's `context_window_tokens.dev`,
|
|
83
|
+
output the `__dev` model's `max_tokens`. A config with a `__max` model and
|
|
84
|
+
a bigger `context_window_tokens.max` is listed again as its Max,
|
|
85
|
+
`<id>-max` ("… (Max)"), with that window and the `__max` model's
|
|
86
|
+
`max_tokens`; it asks the config's `__max` model, with `max_tokens` set
|
|
87
|
+
and `user_message_context.model_info.prompt_max_tokens` the window less
|
|
88
|
+
it, as Max mode does.
|
|
89
|
+
|
|
90
|
+
Reasoning: the models reason, but Trae's request takes no effort or
|
|
91
|
+
thinking level, so the plugin lists no variants and a level an agent
|
|
92
|
+
picks does nothing.
|
|
93
|
+
|
|
94
|
+
When no
|
|
70
95
|
list can be read, the plugin uses the models Trae CN's `chat_v3` is
|
|
71
96
|
known to serve: GLM-5.2, GLM-5, Kimi K2.6, Qwen 3.7 Plus, DeepSeek V4 Pro
|
|
72
97
|
and DeepSeek V4 Flash.
|
package/index.mjs
CHANGED
|
@@ -449,21 +449,28 @@ const nativeTools = (tools) =>
|
|
|
449
449
|
// chatBody is the request for llm_utils_chat; modelName is the model the
|
|
450
450
|
// function's list names for the config (its __dev one), when it named one,
|
|
451
451
|
// and most the max_tokens it gives that model (0: none given), which a
|
|
452
|
-
// request asking more is held to
|
|
453
|
-
|
|
452
|
+
// request asking more is held to. A Max model (max: {base, window}) asks
|
|
453
|
+
// the config base by its __max model, max_tokens always set, and gives
|
|
454
|
+
// the prompt the room the window has beside it, as the IDE's Max mode does.
|
|
455
|
+
function chatBody(req, fn, modelName, most = 0, max = null) {
|
|
454
456
|
const session = randomUUID()
|
|
457
|
+
const config = max?.base ?? req.model
|
|
455
458
|
const body = {
|
|
456
459
|
messages: traeMessages(req),
|
|
457
460
|
function: fn,
|
|
458
|
-
config_name:
|
|
459
|
-
model:
|
|
461
|
+
config_name: config,
|
|
462
|
+
model: config,
|
|
460
463
|
...(modelName ? { model_name: modelName } : {}),
|
|
461
464
|
stream: true, // Trae answers in SSE either way
|
|
462
465
|
request_id: session,
|
|
463
466
|
session_id: session,
|
|
464
467
|
}
|
|
465
|
-
const
|
|
466
|
-
if (Number.isFinite(
|
|
468
|
+
const asked = req.max_completion_tokens ?? req.max_tokens
|
|
469
|
+
if (Number.isFinite(asked) && asked > 0) body.max_tokens = most > 0 ? Math.min(Math.floor(asked), most) : Math.floor(asked)
|
|
470
|
+
if (max?.window) {
|
|
471
|
+
if (!body.max_tokens && most > 0) body.max_tokens = most
|
|
472
|
+
body.user_message_context = { model_info: { prompt_max_tokens: max.window - (body.max_tokens || 0) } }
|
|
473
|
+
}
|
|
467
474
|
if (typeof req.temperature === "number") body.temperature = req.temperature
|
|
468
475
|
if (Array.isArray(req.tools) && req.tools.length && req.tool_choice !== "none") {
|
|
469
476
|
body.tools = nativeTools(req.tools)
|
|
@@ -543,24 +550,57 @@ const callId = () => "call_" + randomBytes(12).toString("hex") + (callSeq++).toS
|
|
|
543
550
|
// once whole even when DeepSeek closes it with its own tags in place of
|
|
544
551
|
// </tool_call>, or DeepSeek's (<|DSML|invoke name="…">, magpie#823); the
|
|
545
552
|
// tags left around a call are dropped.
|
|
553
|
+
//
|
|
554
|
+
// GLM writes a call as its own template has it, <tool_call>name
|
|
555
|
+
// <arg_key>k</arg_key><arg_value>v</arg_value></tool_call>, which is read
|
|
556
|
+
// too; and a {"reasoning_content": …} object in the text is the model's
|
|
557
|
+
// reasoning, not its answer (歧路亡羊 on magpie's Discord).
|
|
546
558
|
class TextTools {
|
|
547
|
-
constructor() {
|
|
559
|
+
constructor(tools = []) {
|
|
548
560
|
this.buf = ""
|
|
549
561
|
this.after = false
|
|
562
|
+
this.tools = tools
|
|
550
563
|
}
|
|
551
564
|
push(s, end = false) {
|
|
552
565
|
this.buf += s
|
|
553
566
|
let text = ""
|
|
567
|
+
let reasoning = ""
|
|
554
568
|
const calls = []
|
|
555
569
|
for (;;) {
|
|
556
570
|
if (this.after) {
|
|
557
571
|
const m = this.buf.match(LEFTOVER)
|
|
558
572
|
if (m) this.buf = this.buf.slice(m[0].length)
|
|
559
573
|
if (!this.buf || (!end && MARKS.some((k) => k.startsWith(this.buf)))) break
|
|
574
|
+
// a tag cut short at the end (</), after the others: dropped too
|
|
575
|
+
if (end && this.buf.length > 1 && MARKS.some((k) => k.startsWith(this.buf.trimEnd()))) {
|
|
576
|
+
this.buf = ""
|
|
577
|
+
break
|
|
578
|
+
}
|
|
560
579
|
this.after = false
|
|
561
580
|
}
|
|
562
581
|
const i = this.buf.indexOf(OPEN)
|
|
563
582
|
const d = this.buf.search(DSML_AT)
|
|
583
|
+
const r = this.buf.search(REASON_AT)
|
|
584
|
+
if (r >= 0 && (i < 0 || r < i) && (d < 0 || r < d)) {
|
|
585
|
+
const o = objectEnd(this.buf, r)
|
|
586
|
+
if (o === -1 && !end) {
|
|
587
|
+
// not whole yet: held, the text before it sent
|
|
588
|
+
text += this.buf.slice(0, r)
|
|
589
|
+
this.buf = this.buf.slice(r)
|
|
590
|
+
break
|
|
591
|
+
}
|
|
592
|
+
const v = o > 0 ? looseJSON(this.buf.slice(r, o)) : null
|
|
593
|
+
if (v && typeof v.reasoning_content === "string") {
|
|
594
|
+
text += this.buf.slice(0, r)
|
|
595
|
+
reasoning += v.reasoning_content
|
|
596
|
+
this.buf = this.buf.slice(o)
|
|
597
|
+
continue
|
|
598
|
+
}
|
|
599
|
+
// not one after all: text, and what follows read on
|
|
600
|
+
text += this.buf.slice(0, r + 1)
|
|
601
|
+
this.buf = this.buf.slice(r + 1)
|
|
602
|
+
continue
|
|
603
|
+
}
|
|
564
604
|
if (i < 0 && d < 0) break
|
|
565
605
|
if (d >= 0 && (i < 0 || d < i)) {
|
|
566
606
|
text += this.buf.slice(0, d)
|
|
@@ -597,6 +637,16 @@ class TextTools {
|
|
|
597
637
|
continue
|
|
598
638
|
}
|
|
599
639
|
const o = objectEnd(this.buf, i + OPEN.length)
|
|
640
|
+
const inner = this.buf.slice(i + OPEN.length).trimStart()
|
|
641
|
+
if (inner.startsWith("<") || (end && !inner)) {
|
|
642
|
+
// no JSON in it: DeepSeek's own invoke, or only the closes of one
|
|
643
|
+
// (magpie#823 again); the opener is dropped and what follows read
|
|
644
|
+
// as it is
|
|
645
|
+
text += this.buf.slice(0, i)
|
|
646
|
+
this.buf = inner
|
|
647
|
+
this.after = true
|
|
648
|
+
continue
|
|
649
|
+
}
|
|
600
650
|
const v = o > 0 ? looseJSON(this.buf.slice(i + OPEN.length, o)) : null
|
|
601
651
|
if (v?.name) {
|
|
602
652
|
text += this.buf.slice(0, i)
|
|
@@ -605,28 +655,79 @@ class TextTools {
|
|
|
605
655
|
this.after = true
|
|
606
656
|
continue
|
|
607
657
|
}
|
|
608
|
-
|
|
609
|
-
if (j < 0)
|
|
658
|
+
let j = this.buf.indexOf(CLOSE, i + OPEN.length)
|
|
659
|
+
if (j < 0) {
|
|
660
|
+
// GLM's call, cut short at the end, is still the call
|
|
661
|
+
if (!end || !glmCall(this.buf.slice(i + OPEN.length), this.tools)) break
|
|
662
|
+
j = this.buf.length
|
|
663
|
+
}
|
|
610
664
|
text += this.buf.slice(0, i)
|
|
611
665
|
const raw = this.buf.slice(i + OPEN.length, j).trim()
|
|
612
666
|
this.buf = this.buf.slice(j + CLOSE.length)
|
|
613
|
-
const w = looseJSON(raw) ?? {}
|
|
614
|
-
if (w.name)
|
|
615
|
-
|
|
667
|
+
const w = looseJSON(raw) ?? glmCall(raw, this.tools) ?? {}
|
|
668
|
+
if (w.name) {
|
|
669
|
+
calls.push({ name: String(w.name), arguments: callArgs(w) })
|
|
670
|
+
this.after = true
|
|
671
|
+
} else text += OPEN + raw + CLOSE
|
|
616
672
|
}
|
|
617
673
|
if (end) {
|
|
618
674
|
text += this.buf
|
|
619
675
|
this.buf = ""
|
|
620
676
|
} else {
|
|
621
677
|
// keep back an opened block, or what may be the start of one
|
|
622
|
-
const at = [this.buf.indexOf(OPEN), this.buf.search(DSML_AT)].filter((x) => x >= 0)
|
|
678
|
+
const at = [this.buf.indexOf(OPEN), this.buf.search(DSML_AT), this.buf.search(REASON_AT)].filter((x) => x >= 0)
|
|
623
679
|
let keep = at.length ? this.buf.length - Math.min(...at) : 0
|
|
624
680
|
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 }
|
|
625
681
|
text += this.buf.slice(0, this.buf.length - keep)
|
|
626
682
|
this.buf = this.buf.slice(this.buf.length - keep)
|
|
627
683
|
}
|
|
628
|
-
return { text, calls }
|
|
684
|
+
return { text, calls, reasoning }
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// GLM's call template: the tool's name, then each argument as
|
|
689
|
+
// <arg_key>k</arg_key><arg_value>v</arg_value>
|
|
690
|
+
const GLM_ARG = /<arg_key>\s*([\s\S]*?)\s*<\/arg_key>\s*<arg_value>([\s\S]*?)<\/arg_value>/g
|
|
691
|
+
|
|
692
|
+
// glmCall is a call GLM wrote in its template ({name, arguments}), null
|
|
693
|
+
// when s isn't one. A value is its text where the tool's schema says
|
|
694
|
+
// string (or the text isn't JSON), else its JSON, as GLM's template writes
|
|
695
|
+
// a value that isn't a string. A name alone is a call only to a tool the
|
|
696
|
+
// request has.
|
|
697
|
+
function glmCall(s, tools = []) {
|
|
698
|
+
s = String(s ?? "").replace(/<\/tool_call>\s*$/, "")
|
|
699
|
+
const k = s.indexOf("<arg_key>")
|
|
700
|
+
const name = (k < 0 ? s : s.slice(0, k)).trim()
|
|
701
|
+
if (!/^[\w.:-]{1,128}$/.test(name)) return null
|
|
702
|
+
if (k < 0 && !tools.some((t) => (t.function ?? t)?.name === name)) return null
|
|
703
|
+
const props = (tools.find((t) => (t.function ?? t)?.name === name)?.function ?? {}).parameters?.properties ?? {}
|
|
704
|
+
const args = {}
|
|
705
|
+
for (const m of s.matchAll(GLM_ARG)) {
|
|
706
|
+
const raw = m[2]
|
|
707
|
+
if (props[m[1]]?.type === "string") {
|
|
708
|
+
args[m[1]] = raw
|
|
709
|
+
continue
|
|
710
|
+
}
|
|
711
|
+
try {
|
|
712
|
+
args[m[1]] = JSON.parse(raw.trim())
|
|
713
|
+
} catch {
|
|
714
|
+
args[m[1]] = raw
|
|
715
|
+
}
|
|
629
716
|
}
|
|
717
|
+
if (k >= 0 && !Object.keys(args).length) return null
|
|
718
|
+
return { name, arguments: args }
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// toolNamed is the request's own name for a tool a model named otherwise
|
|
722
|
+
// (Read for read, list-files for list_files); name itself when the request
|
|
723
|
+
// has none like it. A tool it has none of goes on as named: the agent tells
|
|
724
|
+
// the model there's no such tool, which it can act on, where a call
|
|
725
|
+
// dropped here would end the turn with nothing.
|
|
726
|
+
function toolNamed(name, tools) {
|
|
727
|
+
const names = tools.map((t) => (t.function ?? t)?.name).filter(Boolean)
|
|
728
|
+
if (!names.length || names.includes(name)) return name
|
|
729
|
+
const norm = (n) => String(n).toLowerCase().replace(/[^a-z0-9]/g, "")
|
|
730
|
+
return names.find((n) => norm(n) === norm(name)) ?? name
|
|
630
731
|
}
|
|
631
732
|
|
|
632
733
|
// DeepSeek's tags, with its full-width bar or a plain one
|
|
@@ -641,7 +742,9 @@ const DSML_PARAM = new RegExp(String.raw`<\s*[||][\s||]*DSML[\s||]*paramet
|
|
|
641
742
|
// next call's
|
|
642
743
|
const LEFTOVER = new RegExp(String.raw`^(?:\s+|<\/tool_calls?>|<tool_calls>|` + DSML + String.raw`(?![\s||]*invoke\b)[^<>]*>)+`)
|
|
643
744
|
// what text may be the start of, so held back
|
|
644
|
-
const MARKS = [OPEN, CLOSE, "<|DSML|", "<|DSML|", "</|DSML|", "</|DSML|"]
|
|
745
|
+
const MARKS = [OPEN, CLOSE, "<|DSML|", "<|DSML|", "</|DSML|", "</|DSML|", '{"reasoning_content"']
|
|
746
|
+
// reasoning a model wrote into its text as JSON
|
|
747
|
+
const REASON_AT = /\{\s*"reasoning_content"\s*:/
|
|
645
748
|
const MARK_MAX = Math.max(...MARKS.map((m) => m.length))
|
|
646
749
|
|
|
647
750
|
// dsmlArgs is the arguments of a DSML invoke as JSON: a parameter marked
|
|
@@ -778,8 +881,15 @@ class NativeCalls {
|
|
|
778
881
|
if (next.startsWith(prev) || done(prev)) o.arguments = next // the arguments so far
|
|
779
882
|
else o.arguments = prev + next // a piece
|
|
780
883
|
}
|
|
781
|
-
take() {
|
|
884
|
+
take(tools = []) {
|
|
782
885
|
return this.calls.filter((c) => c.name).map(({ id, name, arguments: a }) => {
|
|
886
|
+
// a name that holds the whole call, as JSON or in GLM's template:
|
|
887
|
+
// that call, its arguments the name's when it gave none of its own
|
|
888
|
+
const w = name.trimStart().startsWith("{") ? looseJSON(name) : name.includes("<arg_key>") ? glmCall(name, tools) : null
|
|
889
|
+
if (w?.name) {
|
|
890
|
+
name = String(w.name)
|
|
891
|
+
if (!a || a.trim() === "{}") a = callArgs(w)
|
|
892
|
+
}
|
|
783
893
|
const v = a && !done(a) ? looseJSON(a) : null
|
|
784
894
|
return { id: id || callId(), name, arguments: v ? JSON.stringify(v) : a || "{}" }
|
|
785
895
|
})
|
|
@@ -797,11 +907,48 @@ function done(a) {
|
|
|
797
907
|
}
|
|
798
908
|
}
|
|
799
909
|
|
|
910
|
+
// the events that carry the answer: output and message, and the names
|
|
911
|
+
// Trae has put a reply's text under otherwise (delta, or none at all);
|
|
912
|
+
// any other is queueing, metadata or timing
|
|
913
|
+
const ANSWER = new Set(["", "output", "message", "delta", "content", "text", "answer", "chunk", "output_delta", "message_delta"])
|
|
914
|
+
|
|
915
|
+
// finishOf is OpenAI's finish_reason for the one Trae's last event gives,
|
|
916
|
+
// "" for none it says or one it has no word for
|
|
917
|
+
function finishOf(d) {
|
|
918
|
+
const f = String(d?.finish_reason ?? d?.stop_reason ?? d?.finishReason ?? "").toLowerCase()
|
|
919
|
+
if (["length", "max_tokens", "max_output_tokens", "max_length"].includes(f)) return "length"
|
|
920
|
+
if (f === "content_filter" || f === "sensitive") return "content_filter"
|
|
921
|
+
return ""
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
// Thoughts is the reasoning sent on so far: a piece Trae sends again (the
|
|
925
|
+
// same one twice in a row, or the reasoning so far, again or with more
|
|
926
|
+
// after it) sends only what is new
|
|
927
|
+
class Thoughts {
|
|
928
|
+
constructor() {
|
|
929
|
+
this.all = ""
|
|
930
|
+
this.last = ""
|
|
931
|
+
}
|
|
932
|
+
add(r) {
|
|
933
|
+
const last = this.last
|
|
934
|
+
this.last = r
|
|
935
|
+
if (r === last && r.length >= 8) return ""
|
|
936
|
+
if (this.all.length >= 16 && r.length >= 16 && this.all.endsWith(r)) return ""
|
|
937
|
+
if (this.all.length >= 16 && r.startsWith(this.all)) r = r.slice(this.all.length)
|
|
938
|
+
this.all += r
|
|
939
|
+
return r
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
|
|
800
943
|
// parts turns Trae's events into what the answer is made of: text,
|
|
801
|
-
// reasoning, tool calls, the token counts, the finish, or an error.
|
|
802
|
-
|
|
803
|
-
|
|
944
|
+
// reasoning, tool calls, the token counts, the finish, or an error. tools
|
|
945
|
+
// are the request's, a call to one named otherwise going on under its name.
|
|
946
|
+
async function* parts(events, tools = []) {
|
|
947
|
+
const tt = new TextTools(tools)
|
|
804
948
|
const nc = new NativeCalls()
|
|
949
|
+
const th = new Thoughts()
|
|
950
|
+
const call = (c) => ({ call: { ...c, name: toolNamed(c.name, tools) } })
|
|
951
|
+
let finish = ""
|
|
805
952
|
for await (const { event, data } of events) {
|
|
806
953
|
const name = eventName(event)
|
|
807
954
|
const d = data && typeof data === "object" ? data : {}
|
|
@@ -819,27 +966,38 @@ async function* parts(events) {
|
|
|
819
966
|
if (name === "done" || name === "response_done" || name === "stream_done") {
|
|
820
967
|
const u = tokensOf(d.usage)
|
|
821
968
|
if (u) yield { usage: u }
|
|
969
|
+
finish = finishOf(d) || finish
|
|
822
970
|
break
|
|
823
971
|
}
|
|
824
|
-
if (name
|
|
825
|
-
const
|
|
826
|
-
|
|
827
|
-
|
|
972
|
+
if (!ANSWER.has(name)) continue
|
|
973
|
+
const delta = d.delta && typeof d.delta === "object" ? d.delta : {}
|
|
974
|
+
const reasoning = d.reasoning_content ?? d.reasoning ?? delta.reasoning_content ?? ""
|
|
975
|
+
if (typeof reasoning === "string" && reasoning) {
|
|
976
|
+
const r = th.add(reasoning)
|
|
977
|
+
if (r) yield { reasoning: r }
|
|
978
|
+
}
|
|
979
|
+
let text = typeof d.response === "string" ? d.response : typeof d.content === "string" ? d.content : typeof delta.content === "string" ? delta.content : typeof d.delta === "string" ? d.delta : ""
|
|
828
980
|
// the IDE's own progress notes, not the model's
|
|
829
981
|
if (/^(Building prompt:|Completed building prompt)/.test(text)) text = ""
|
|
830
982
|
if (text) {
|
|
831
983
|
const r = tt.push(text)
|
|
984
|
+
const t = r.reasoning && th.add(r.reasoning)
|
|
985
|
+
if (t) yield { reasoning: t }
|
|
832
986
|
if (r.text) yield { text: r.text }
|
|
833
|
-
for (const c of r.calls) yield
|
|
987
|
+
for (const c of r.calls) yield call({ ...c, id: callId() })
|
|
834
988
|
}
|
|
835
989
|
for (const tc of Array.isArray(d.tool_calls) ? d.tool_calls : []) nc.push(tc)
|
|
836
990
|
const u = tokensOf(d.usage)
|
|
837
991
|
if (u) yield { usage: u }
|
|
992
|
+
finish = finishOf(d) || finish
|
|
838
993
|
}
|
|
839
994
|
const r = tt.push("", true)
|
|
995
|
+
const t = r.reasoning && th.add(r.reasoning)
|
|
996
|
+
if (t) yield { reasoning: t }
|
|
840
997
|
if (r.text) yield { text: r.text }
|
|
841
|
-
for (const c of r.calls) yield
|
|
842
|
-
for (const c of nc.take()) yield
|
|
998
|
+
for (const c of r.calls) yield call({ ...c, id: callId() })
|
|
999
|
+
for (const c of nc.take(tools)) yield call(c)
|
|
1000
|
+
if (finish) yield { finish }
|
|
843
1001
|
}
|
|
844
1002
|
|
|
845
1003
|
// first reads events up to the first that is the answer's, so an answer
|
|
@@ -884,45 +1042,71 @@ async function openai(req, it) {
|
|
|
884
1042
|
let reasoning = ""
|
|
885
1043
|
const calls = []
|
|
886
1044
|
let usage = null
|
|
1045
|
+
let finish = ""
|
|
887
1046
|
for await (const p of it) {
|
|
888
1047
|
if (p.error) return failure(502, p.code, p.error)
|
|
889
1048
|
if (p.text) text += p.text
|
|
890
1049
|
if (p.reasoning) reasoning += p.reasoning
|
|
891
1050
|
if (p.call) calls.push(p.call)
|
|
892
1051
|
if (p.usage) usage = p.usage
|
|
1052
|
+
if (p.finish) finish = p.finish
|
|
893
1053
|
}
|
|
894
1054
|
const message = { role: "assistant", content: text || (calls.length ? null : "") }
|
|
895
1055
|
if (reasoning) message.reasoning_content = reasoning
|
|
896
1056
|
if (calls.length) message.tool_calls = calls.map((c) => ({ id: c.id, type: "function", function: { name: c.name, arguments: c.arguments } }))
|
|
897
|
-
const body = { id, object: "chat.completion", created, model, choices: [{ index: 0, message, finish_reason: calls.length ? "tool_calls" : "stop" }] }
|
|
1057
|
+
const body = { id, object: "chat.completion", created, model, choices: [{ index: 0, message, finish_reason: calls.length ? "tool_calls" : finish || "stop" }] }
|
|
898
1058
|
if (usage) body.usage = usage
|
|
899
1059
|
return new Response(JSON.stringify(body), { status: 200, headers: { "content-type": "application/json" } })
|
|
900
1060
|
}
|
|
901
1061
|
const enc = new TextEncoder()
|
|
902
1062
|
const chunk = (delta, finish = null, extra = {}) => enc.encode(`data: ${JSON.stringify({ id, object: "chat.completion.chunk", created, model, choices: [{ index: 0, delta, finish_reason: finish }], ...extra })}\n\n`)
|
|
1063
|
+
// a reply that failed (an error event, or Trae's stream breaking off)
|
|
1064
|
+
// ends with the error and [DONE], not a finish that says it stopped; the
|
|
1065
|
+
// stream is closed whatever happens, and an agent that goes away stops
|
|
1066
|
+
// the reading of Trae's
|
|
1067
|
+
const error = (message, code = null) => enc.encode(`data: ${JSON.stringify({ error: { message: `Trae CN: ${message}`, type: "api_error", code } })}\n\n`)
|
|
1068
|
+
let gone = false
|
|
903
1069
|
const stream = new ReadableStream({
|
|
904
1070
|
async start(ctl) {
|
|
905
1071
|
let calls = 0
|
|
906
1072
|
let usage = null
|
|
907
|
-
|
|
1073
|
+
let finish = ""
|
|
1074
|
+
let failed = false
|
|
1075
|
+
const put = (b) => {
|
|
1076
|
+
if (!gone) ctl.enqueue(b)
|
|
1077
|
+
}
|
|
908
1078
|
try {
|
|
1079
|
+
put(chunk({ role: "assistant", content: "" }))
|
|
909
1080
|
for await (const p of it) {
|
|
1081
|
+
if (gone) break
|
|
910
1082
|
if (p.error) {
|
|
911
|
-
|
|
1083
|
+
put(error(p.error, p.code ?? null))
|
|
1084
|
+
failed = true
|
|
912
1085
|
break
|
|
913
1086
|
}
|
|
914
|
-
if (p.text)
|
|
915
|
-
if (p.reasoning)
|
|
916
|
-
if (p.call)
|
|
1087
|
+
if (p.text) put(chunk({ content: p.text }))
|
|
1088
|
+
if (p.reasoning) put(chunk({ reasoning_content: p.reasoning }))
|
|
1089
|
+
if (p.call) put(chunk({ tool_calls: [{ index: calls++, id: p.call.id, type: "function", function: { name: p.call.name, arguments: p.call.arguments } }] }))
|
|
917
1090
|
if (p.usage) usage = p.usage
|
|
1091
|
+
if (p.finish) finish = p.finish
|
|
1092
|
+
}
|
|
1093
|
+
if (!failed) {
|
|
1094
|
+
put(chunk({}, calls ? "tool_calls" : finish || "stop"))
|
|
1095
|
+
if (usage) put(enc.encode(`data: ${JSON.stringify({ id, object: "chat.completion.chunk", created, model, choices: [], usage })}\n\n`))
|
|
918
1096
|
}
|
|
919
|
-
ctl.enqueue(chunk({}, calls ? "tool_calls" : "stop"))
|
|
920
|
-
if (usage) ctl.enqueue(enc.encode(`data: ${JSON.stringify({ id, object: "chat.completion.chunk", created, model, choices: [], usage })}\n\n`))
|
|
921
|
-
ctl.enqueue(enc.encode("data: [DONE]\n\n"))
|
|
922
1097
|
} catch (e) {
|
|
923
|
-
|
|
1098
|
+
try {
|
|
1099
|
+
put(error(e?.message ?? e))
|
|
1100
|
+
} catch {}
|
|
924
1101
|
}
|
|
925
|
-
|
|
1102
|
+
try {
|
|
1103
|
+
put(enc.encode("data: [DONE]\n\n"))
|
|
1104
|
+
if (!gone) ctl.close()
|
|
1105
|
+
} catch {}
|
|
1106
|
+
},
|
|
1107
|
+
cancel() {
|
|
1108
|
+
gone = true
|
|
1109
|
+
it.return?.()
|
|
926
1110
|
},
|
|
927
1111
|
})
|
|
928
1112
|
return new Response(stream, { status: 200, headers: { "content-type": "text/event-stream", "cache-control": "no-cache" } })
|
|
@@ -949,6 +1133,20 @@ function chatModel(m) {
|
|
|
949
1133
|
// devModel is the model an entry serves requests with: its __dev one
|
|
950
1134
|
const devModel = (m) => (Array.isArray(m?.model_detail_list) ? m.model_detail_list : []).find((d) => /__dev$/.test(String(d?.model_name ?? "")))
|
|
951
1135
|
|
|
1136
|
+
// maxModel is a config's Max mode: its __max model and the window
|
|
1137
|
+
// context_window_tokens.max gives it, when that is bigger than dev's; a
|
|
1138
|
+
// config without one has no Max
|
|
1139
|
+
const maxModel = (m) => {
|
|
1140
|
+
const d = (Array.isArray(m?.model_detail_list) ? m.model_detail_list : []).find((d) => /__max$/.test(String(d?.model_name ?? "")))
|
|
1141
|
+
const window = Number(m?.context_window_tokens?.max) || 0
|
|
1142
|
+
const dev = Number(m?.context_window_tokens?.dev) || 0
|
|
1143
|
+
return d && window > dev ? { name: String(d.model_name), most: Number(d.max_tokens) || 0, window } : null
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
// MAX is the suffix of a model's Max: deepseek-v4.1-flash-max is
|
|
1147
|
+
// deepseek-v4.1-flash in Max mode
|
|
1148
|
+
const MAX = "-max"
|
|
1149
|
+
|
|
952
1150
|
// ---- usage ------------------------------------------------------------------------
|
|
953
1151
|
|
|
954
1152
|
// credits adds up the account's entitlement packs as the IDE shows them:
|
|
@@ -1204,6 +1402,11 @@ export const TraeCNAuthPlugin = async ({ client }) => {
|
|
|
1204
1402
|
const dev = devModel(m)
|
|
1205
1403
|
if (dev) modelNames.set(id, { fn, name: dev.model_name, most: Number(dev.max_tokens) || 0 })
|
|
1206
1404
|
else modelNames.delete(id)
|
|
1405
|
+
const max = maxModel(m)
|
|
1406
|
+
if (max && !out.has(id + MAX)) {
|
|
1407
|
+
listedBy.set(id + MAX, fn)
|
|
1408
|
+
modelNames.set(id + MAX, { fn, name: max.name, most: max.most, max: { base: id, window: max.window } })
|
|
1409
|
+
}
|
|
1207
1410
|
}
|
|
1208
1411
|
return [...out.values()].map((x) => x.m)
|
|
1209
1412
|
}
|
|
@@ -1238,7 +1441,16 @@ export const TraeCNAuthPlugin = async ({ client }) => {
|
|
|
1238
1441
|
try {
|
|
1239
1442
|
const ms = await liveModels(await fresh(async () => auth))
|
|
1240
1443
|
if (!ms.length) return provider.models
|
|
1241
|
-
|
|
1444
|
+
const ids = new Set(ms.map((m) => String(m.config_name)))
|
|
1445
|
+
return Object.fromEntries(ms.flatMap((m) => {
|
|
1446
|
+
const id = String(m.config_name)
|
|
1447
|
+
const own = modelOf(provider, m)
|
|
1448
|
+
const max = maxModel(m)
|
|
1449
|
+
if (!max || ids.has(id + MAX)) return [[id, own]]
|
|
1450
|
+
// its Max, a model of its own: the window and output Max mode gives
|
|
1451
|
+
const was = provider.models?.[id + MAX] ?? {}
|
|
1452
|
+
return [[id, own], [id + MAX, { ...own, ...was, id: id + MAX, name: own.name + " (Max)", limit: { context: max.window, output: max.most || own.limit.output }, api: { ...own.api, id: id + MAX } }]]
|
|
1453
|
+
}))
|
|
1242
1454
|
} catch (e) {
|
|
1243
1455
|
if (e instanceof Expired) throw e
|
|
1244
1456
|
return provider.models
|
|
@@ -1281,12 +1493,19 @@ export const TraeCNAuthPlugin = async ({ client }) => {
|
|
|
1281
1493
|
// served this account last, then the rest
|
|
1282
1494
|
const fns = [...new Set([listedBy.get(String(req.model)), fnOf.get(who), ...FUNCTIONS].filter(Boolean))]
|
|
1283
1495
|
let last
|
|
1284
|
-
|
|
1285
|
-
|
|
1496
|
+
let named = modelNames.get(String(req.model))
|
|
1497
|
+
if (!named && String(req.model).endsWith(MAX)) {
|
|
1498
|
+
// a Max asked before the list was read here: read it
|
|
1499
|
+
await liveModels(a).catch(() => {})
|
|
1500
|
+
named = modelNames.get(String(req.model))
|
|
1501
|
+
if (named) fns.unshift(named.fn)
|
|
1502
|
+
}
|
|
1503
|
+
const max = named?.max ?? null
|
|
1504
|
+
for (const fn of [...new Set(fns)]) {
|
|
1286
1505
|
const res = await fetch(apiOf(a) + "/api/agent/v3/llm_utils_chat", {
|
|
1287
1506
|
method: "POST",
|
|
1288
1507
|
headers: ideHeaders(a, { Accept: "text/event-stream", "X-Request-ID": randomUUID() }),
|
|
1289
|
-
body: JSON.stringify(chatBody(req, fn, named?.fn === fn ? named.name : "", named?.fn === fn ? named.most : 0)),
|
|
1508
|
+
body: JSON.stringify(chatBody(req, fn, named?.fn === fn || max ? named.name : "", named?.fn === fn || max ? named.most : 0, max)),
|
|
1290
1509
|
signal: init.signal ?? r0?.signal,
|
|
1291
1510
|
})
|
|
1292
1511
|
if (!res.ok) {
|
|
@@ -1304,7 +1523,7 @@ export const TraeCNAuthPlugin = async ({ client }) => {
|
|
|
1304
1523
|
if (wrongFunction(e.code)) continue
|
|
1305
1524
|
break
|
|
1306
1525
|
}
|
|
1307
|
-
const it = parts(sse(res.body))[Symbol.asyncIterator]()
|
|
1526
|
+
const it = parts(sse(res.body), Array.isArray(req.tools) ? req.tools : [])[Symbol.asyncIterator]()
|
|
1308
1527
|
const { held, done } = await first(it)
|
|
1309
1528
|
const err = held.find((p) => p.error)
|
|
1310
1529
|
if (err) {
|
|
@@ -1328,4 +1547,4 @@ export const TraeCNAuthPlugin = async ({ client }) => {
|
|
|
1328
1547
|
}
|
|
1329
1548
|
|
|
1330
1549
|
// for tests
|
|
1331
|
-
export const _internal = { HOSTS, MODELS, TextTools, NativeCalls, looseJSON, traeMessages, chatBody, credits, whenOf, newDevice }
|
|
1550
|
+
export const _internal = { HOSTS, MODELS, TextTools, NativeCalls, looseJSON, glmCall, toolNamed, traeMessages, chatBody, credits, whenOf, newDevice }
|
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.14",
|
|
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",
|