@magpie-community/opencode-trae-auth 0.1.13 → 0.1.15
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 +31 -4
- package/index.mjs +264 -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,21 @@ 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 model 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`; the two can come from different functions' lists (chat_v3
|
|
87
|
+
can name the `__max` model, a SOLO list the windows). It asks the
|
|
88
|
+
`__max` model through the function that names it, with `max_tokens` set
|
|
89
|
+
and `user_message_context.model_info.prompt_max_tokens` the window less
|
|
90
|
+
it, as Max mode does.
|
|
91
|
+
|
|
92
|
+
Reasoning: the models reason, but Trae's request takes no effort or
|
|
93
|
+
thinking level, so the plugin lists no variants and a level an agent
|
|
94
|
+
picks does nothing.
|
|
95
|
+
|
|
96
|
+
When no
|
|
70
97
|
list can be read, the plugin uses the models Trae CN's `chat_v3` is
|
|
71
98
|
known to serve: GLM-5.2, GLM-5, Kimi K2.6, Qwen 3.7 Plus, DeepSeek V4 Pro
|
|
72
99
|
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,14 +550,21 @@ 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) {
|
|
@@ -566,6 +580,27 @@ class TextTools {
|
|
|
566
580
|
}
|
|
567
581
|
const i = this.buf.indexOf(OPEN)
|
|
568
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
|
+
}
|
|
569
604
|
if (i < 0 && d < 0) break
|
|
570
605
|
if (d >= 0 && (i < 0 || d < i)) {
|
|
571
606
|
text += this.buf.slice(0, d)
|
|
@@ -620,30 +655,81 @@ class TextTools {
|
|
|
620
655
|
this.after = true
|
|
621
656
|
continue
|
|
622
657
|
}
|
|
623
|
-
|
|
624
|
-
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
|
+
}
|
|
625
664
|
text += this.buf.slice(0, i)
|
|
626
665
|
const raw = this.buf.slice(i + OPEN.length, j).trim()
|
|
627
666
|
this.buf = this.buf.slice(j + CLOSE.length)
|
|
628
|
-
const w = looseJSON(raw) ?? {}
|
|
629
|
-
if (w.name)
|
|
630
|
-
|
|
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
|
|
631
672
|
}
|
|
632
673
|
if (end) {
|
|
633
674
|
text += this.buf
|
|
634
675
|
this.buf = ""
|
|
635
676
|
} else {
|
|
636
677
|
// keep back an opened block, or what may be the start of one
|
|
637
|
-
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)
|
|
638
679
|
let keep = at.length ? this.buf.length - Math.min(...at) : 0
|
|
639
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 }
|
|
640
681
|
text += this.buf.slice(0, this.buf.length - keep)
|
|
641
682
|
this.buf = this.buf.slice(this.buf.length - keep)
|
|
642
683
|
}
|
|
643
|
-
return { text, calls }
|
|
684
|
+
return { text, calls, reasoning }
|
|
644
685
|
}
|
|
645
686
|
}
|
|
646
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
|
+
}
|
|
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
|
|
731
|
+
}
|
|
732
|
+
|
|
647
733
|
// DeepSeek's tags, with its full-width bar or a plain one
|
|
648
734
|
const DSML = String.raw`<\s*\/?\s*[||][\s||]*DSML[\s||]*`
|
|
649
735
|
const DSML_AT = new RegExp(DSML)
|
|
@@ -656,7 +742,9 @@ const DSML_PARAM = new RegExp(String.raw`<\s*[||][\s||]*DSML[\s||]*paramet
|
|
|
656
742
|
// next call's
|
|
657
743
|
const LEFTOVER = new RegExp(String.raw`^(?:\s+|<\/tool_calls?>|<tool_calls>|` + DSML + String.raw`(?![\s||]*invoke\b)[^<>]*>)+`)
|
|
658
744
|
// what text may be the start of, so held back
|
|
659
|
-
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*:/
|
|
660
748
|
const MARK_MAX = Math.max(...MARKS.map((m) => m.length))
|
|
661
749
|
|
|
662
750
|
// dsmlArgs is the arguments of a DSML invoke as JSON: a parameter marked
|
|
@@ -793,8 +881,15 @@ class NativeCalls {
|
|
|
793
881
|
if (next.startsWith(prev) || done(prev)) o.arguments = next // the arguments so far
|
|
794
882
|
else o.arguments = prev + next // a piece
|
|
795
883
|
}
|
|
796
|
-
take() {
|
|
884
|
+
take(tools = []) {
|
|
797
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
|
+
}
|
|
798
893
|
const v = a && !done(a) ? looseJSON(a) : null
|
|
799
894
|
return { id: id || callId(), name, arguments: v ? JSON.stringify(v) : a || "{}" }
|
|
800
895
|
})
|
|
@@ -812,11 +907,48 @@ function done(a) {
|
|
|
812
907
|
}
|
|
813
908
|
}
|
|
814
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
|
+
|
|
815
943
|
// parts turns Trae's events into what the answer is made of: text,
|
|
816
|
-
// reasoning, tool calls, the token counts, the finish, or an error.
|
|
817
|
-
|
|
818
|
-
|
|
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)
|
|
819
948
|
const nc = new NativeCalls()
|
|
949
|
+
const th = new Thoughts()
|
|
950
|
+
const call = (c) => ({ call: { ...c, name: toolNamed(c.name, tools) } })
|
|
951
|
+
let finish = ""
|
|
820
952
|
for await (const { event, data } of events) {
|
|
821
953
|
const name = eventName(event)
|
|
822
954
|
const d = data && typeof data === "object" ? data : {}
|
|
@@ -834,27 +966,38 @@ async function* parts(events) {
|
|
|
834
966
|
if (name === "done" || name === "response_done" || name === "stream_done") {
|
|
835
967
|
const u = tokensOf(d.usage)
|
|
836
968
|
if (u) yield { usage: u }
|
|
969
|
+
finish = finishOf(d) || finish
|
|
837
970
|
break
|
|
838
971
|
}
|
|
839
|
-
if (name
|
|
840
|
-
const
|
|
841
|
-
|
|
842
|
-
|
|
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 : ""
|
|
843
980
|
// the IDE's own progress notes, not the model's
|
|
844
981
|
if (/^(Building prompt:|Completed building prompt)/.test(text)) text = ""
|
|
845
982
|
if (text) {
|
|
846
983
|
const r = tt.push(text)
|
|
984
|
+
const t = r.reasoning && th.add(r.reasoning)
|
|
985
|
+
if (t) yield { reasoning: t }
|
|
847
986
|
if (r.text) yield { text: r.text }
|
|
848
|
-
for (const c of r.calls) yield
|
|
987
|
+
for (const c of r.calls) yield call({ ...c, id: callId() })
|
|
849
988
|
}
|
|
850
989
|
for (const tc of Array.isArray(d.tool_calls) ? d.tool_calls : []) nc.push(tc)
|
|
851
990
|
const u = tokensOf(d.usage)
|
|
852
991
|
if (u) yield { usage: u }
|
|
992
|
+
finish = finishOf(d) || finish
|
|
853
993
|
}
|
|
854
994
|
const r = tt.push("", true)
|
|
995
|
+
const t = r.reasoning && th.add(r.reasoning)
|
|
996
|
+
if (t) yield { reasoning: t }
|
|
855
997
|
if (r.text) yield { text: r.text }
|
|
856
|
-
for (const c of r.calls) yield
|
|
857
|
-
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 }
|
|
858
1001
|
}
|
|
859
1002
|
|
|
860
1003
|
// first reads events up to the first that is the answer's, so an answer
|
|
@@ -899,45 +1042,71 @@ async function openai(req, it) {
|
|
|
899
1042
|
let reasoning = ""
|
|
900
1043
|
const calls = []
|
|
901
1044
|
let usage = null
|
|
1045
|
+
let finish = ""
|
|
902
1046
|
for await (const p of it) {
|
|
903
1047
|
if (p.error) return failure(502, p.code, p.error)
|
|
904
1048
|
if (p.text) text += p.text
|
|
905
1049
|
if (p.reasoning) reasoning += p.reasoning
|
|
906
1050
|
if (p.call) calls.push(p.call)
|
|
907
1051
|
if (p.usage) usage = p.usage
|
|
1052
|
+
if (p.finish) finish = p.finish
|
|
908
1053
|
}
|
|
909
1054
|
const message = { role: "assistant", content: text || (calls.length ? null : "") }
|
|
910
1055
|
if (reasoning) message.reasoning_content = reasoning
|
|
911
1056
|
if (calls.length) message.tool_calls = calls.map((c) => ({ id: c.id, type: "function", function: { name: c.name, arguments: c.arguments } }))
|
|
912
|
-
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" }] }
|
|
913
1058
|
if (usage) body.usage = usage
|
|
914
1059
|
return new Response(JSON.stringify(body), { status: 200, headers: { "content-type": "application/json" } })
|
|
915
1060
|
}
|
|
916
1061
|
const enc = new TextEncoder()
|
|
917
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
|
|
918
1069
|
const stream = new ReadableStream({
|
|
919
1070
|
async start(ctl) {
|
|
920
1071
|
let calls = 0
|
|
921
1072
|
let usage = null
|
|
922
|
-
|
|
1073
|
+
let finish = ""
|
|
1074
|
+
let failed = false
|
|
1075
|
+
const put = (b) => {
|
|
1076
|
+
if (!gone) ctl.enqueue(b)
|
|
1077
|
+
}
|
|
923
1078
|
try {
|
|
1079
|
+
put(chunk({ role: "assistant", content: "" }))
|
|
924
1080
|
for await (const p of it) {
|
|
1081
|
+
if (gone) break
|
|
925
1082
|
if (p.error) {
|
|
926
|
-
|
|
1083
|
+
put(error(p.error, p.code ?? null))
|
|
1084
|
+
failed = true
|
|
927
1085
|
break
|
|
928
1086
|
}
|
|
929
|
-
if (p.text)
|
|
930
|
-
if (p.reasoning)
|
|
931
|
-
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 } }] }))
|
|
932
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`))
|
|
933
1096
|
}
|
|
934
|
-
ctl.enqueue(chunk({}, calls ? "tool_calls" : "stop"))
|
|
935
|
-
if (usage) ctl.enqueue(enc.encode(`data: ${JSON.stringify({ id, object: "chat.completion.chunk", created, model, choices: [], usage })}\n\n`))
|
|
936
|
-
ctl.enqueue(enc.encode("data: [DONE]\n\n"))
|
|
937
1097
|
} catch (e) {
|
|
938
|
-
|
|
1098
|
+
try {
|
|
1099
|
+
put(error(e?.message ?? e))
|
|
1100
|
+
} catch {}
|
|
939
1101
|
}
|
|
940
|
-
|
|
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?.()
|
|
941
1110
|
},
|
|
942
1111
|
})
|
|
943
1112
|
return new Response(stream, { status: 200, headers: { "content-type": "text/event-stream", "cache-control": "no-cache" } })
|
|
@@ -964,6 +1133,27 @@ function chatModel(m) {
|
|
|
964
1133
|
// devModel is the model an entry serves requests with: its __dev one
|
|
965
1134
|
const devModel = (m) => (Array.isArray(m?.model_detail_list) ? m.model_detail_list : []).find((d) => /__dev$/.test(String(d?.model_name ?? "")))
|
|
966
1135
|
|
|
1136
|
+
// maxModel is a model's Max mode, from the entries its lists give it (the
|
|
1137
|
+
// one it is asked through first): the first __max model one names, and the
|
|
1138
|
+
// window context_window_tokens.max gives it, when that is bigger than dev's.
|
|
1139
|
+
// The lists needn't agree: chat_v3 can name the __max model while the SOLO
|
|
1140
|
+
// list the model is asked through names only __dev, and the window can be
|
|
1141
|
+
// another entry's again. A model with no __max, or no bigger window, has no
|
|
1142
|
+
// Max.
|
|
1143
|
+
const maxModel = (...ms) => {
|
|
1144
|
+
const details = (m) => (Array.isArray(m?.model_detail_list) ? m.model_detail_list : [])
|
|
1145
|
+
const at = ms.find((m) => details(m).some((d) => /__max$/.test(String(d?.model_name ?? ""))))
|
|
1146
|
+
if (!at) return null
|
|
1147
|
+
const d = details(at).find((d) => /__max$/.test(String(d?.model_name ?? "")))
|
|
1148
|
+
const first = (k) => [at, ...ms].map((m) => Number(m?.context_window_tokens?.[k]) || 0).find((n) => n > 0) || 0
|
|
1149
|
+
const window = first("max")
|
|
1150
|
+
return window > first("dev") ? { name: String(d.model_name), most: Number(d.max_tokens) || 0, window, entry: at } : null
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
// MAX is the suffix of a model's Max: deepseek-v4.1-flash-max is
|
|
1154
|
+
// deepseek-v4.1-flash in Max mode
|
|
1155
|
+
const MAX = "-max"
|
|
1156
|
+
|
|
967
1157
|
// ---- usage ------------------------------------------------------------------------
|
|
968
1158
|
|
|
969
1159
|
// credits adds up the account's entitlement packs as the IDE shows them:
|
|
@@ -1003,6 +1193,8 @@ export const TraeCNAuthPlugin = async ({ client }) => {
|
|
|
1003
1193
|
// model it names there
|
|
1004
1194
|
const listedBy = new Map()
|
|
1005
1195
|
const modelNames = new Map()
|
|
1196
|
+
// the Max each model the list last read has (null: none)
|
|
1197
|
+
const maxes = new Map()
|
|
1006
1198
|
|
|
1007
1199
|
const save = async (auth) => {
|
|
1008
1200
|
try {
|
|
@@ -1219,6 +1411,19 @@ export const TraeCNAuthPlugin = async ({ client }) => {
|
|
|
1219
1411
|
const dev = devModel(m)
|
|
1220
1412
|
if (dev) modelNames.set(id, { fn, name: dev.model_name, most: Number(dev.max_tokens) || 0 })
|
|
1221
1413
|
else modelNames.delete(id)
|
|
1414
|
+
if (out.has(id + MAX)) continue
|
|
1415
|
+
// its Max is asked through the function whose entry names the __max model
|
|
1416
|
+
const entries = [{ m, fn }, ...FUNCTIONS.flatMap((f) => (lists[f] ?? []).filter((o) => o !== m && chatModel(o) && String(o.config_name) === id).map((o) => ({ m: o, fn: f })))]
|
|
1417
|
+
const max = maxModel(...entries.map((e) => e.m))
|
|
1418
|
+
maxes.set(id, max)
|
|
1419
|
+
if (max) {
|
|
1420
|
+
const by = entries.find((e) => e.m === max.entry).fn
|
|
1421
|
+
listedBy.set(id + MAX, by)
|
|
1422
|
+
modelNames.set(id + MAX, { fn: by, name: max.name, most: max.most, max: { base: id, window: max.window } })
|
|
1423
|
+
} else {
|
|
1424
|
+
listedBy.delete(id + MAX)
|
|
1425
|
+
modelNames.delete(id + MAX)
|
|
1426
|
+
}
|
|
1222
1427
|
}
|
|
1223
1428
|
return [...out.values()].map((x) => x.m)
|
|
1224
1429
|
}
|
|
@@ -1253,7 +1458,16 @@ export const TraeCNAuthPlugin = async ({ client }) => {
|
|
|
1253
1458
|
try {
|
|
1254
1459
|
const ms = await liveModels(await fresh(async () => auth))
|
|
1255
1460
|
if (!ms.length) return provider.models
|
|
1256
|
-
|
|
1461
|
+
const ids = new Set(ms.map((m) => String(m.config_name)))
|
|
1462
|
+
return Object.fromEntries(ms.flatMap((m) => {
|
|
1463
|
+
const id = String(m.config_name)
|
|
1464
|
+
const own = modelOf(provider, m)
|
|
1465
|
+
const max = maxes.get(id)
|
|
1466
|
+
if (!max || ids.has(id + MAX)) return [[id, own]]
|
|
1467
|
+
// its Max, a model of its own: the window and output Max mode gives
|
|
1468
|
+
const was = provider.models?.[id + MAX] ?? {}
|
|
1469
|
+
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 } }]]
|
|
1470
|
+
}))
|
|
1257
1471
|
} catch (e) {
|
|
1258
1472
|
if (e instanceof Expired) throw e
|
|
1259
1473
|
return provider.models
|
|
@@ -1296,12 +1510,19 @@ export const TraeCNAuthPlugin = async ({ client }) => {
|
|
|
1296
1510
|
// served this account last, then the rest
|
|
1297
1511
|
const fns = [...new Set([listedBy.get(String(req.model)), fnOf.get(who), ...FUNCTIONS].filter(Boolean))]
|
|
1298
1512
|
let last
|
|
1299
|
-
|
|
1300
|
-
|
|
1513
|
+
let named = modelNames.get(String(req.model))
|
|
1514
|
+
if (!named && String(req.model).endsWith(MAX)) {
|
|
1515
|
+
// a Max asked before the list was read here: read it
|
|
1516
|
+
await liveModels(a).catch(() => {})
|
|
1517
|
+
named = modelNames.get(String(req.model))
|
|
1518
|
+
if (named) fns.unshift(named.fn)
|
|
1519
|
+
}
|
|
1520
|
+
const max = named?.max ?? null
|
|
1521
|
+
for (const fn of [...new Set(fns)]) {
|
|
1301
1522
|
const res = await fetch(apiOf(a) + "/api/agent/v3/llm_utils_chat", {
|
|
1302
1523
|
method: "POST",
|
|
1303
1524
|
headers: ideHeaders(a, { Accept: "text/event-stream", "X-Request-ID": randomUUID() }),
|
|
1304
|
-
body: JSON.stringify(chatBody(req, fn, named?.fn === fn ? named.name : "", named?.fn === fn ? named.most : 0)),
|
|
1525
|
+
body: JSON.stringify(chatBody(req, fn, named?.fn === fn || max ? named.name : "", named?.fn === fn || max ? named.most : 0, max)),
|
|
1305
1526
|
signal: init.signal ?? r0?.signal,
|
|
1306
1527
|
})
|
|
1307
1528
|
if (!res.ok) {
|
|
@@ -1319,7 +1540,7 @@ export const TraeCNAuthPlugin = async ({ client }) => {
|
|
|
1319
1540
|
if (wrongFunction(e.code)) continue
|
|
1320
1541
|
break
|
|
1321
1542
|
}
|
|
1322
|
-
const it = parts(sse(res.body))[Symbol.asyncIterator]()
|
|
1543
|
+
const it = parts(sse(res.body), Array.isArray(req.tools) ? req.tools : [])[Symbol.asyncIterator]()
|
|
1323
1544
|
const { held, done } = await first(it)
|
|
1324
1545
|
const err = held.find((p) => p.error)
|
|
1325
1546
|
if (err) {
|
|
@@ -1343,4 +1564,4 @@ export const TraeCNAuthPlugin = async ({ client }) => {
|
|
|
1343
1564
|
}
|
|
1344
1565
|
|
|
1345
1566
|
// for tests
|
|
1346
|
-
export const _internal = { HOSTS, MODELS, TextTools, NativeCalls, looseJSON, traeMessages, chatBody, credits, whenOf, newDevice }
|
|
1567
|
+
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.15",
|
|
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",
|