@magpie-community/opencode-trae-auth 0.2.17 → 0.2.19
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 +48 -10
- package/index.mjs +245 -48
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -122,11 +122,28 @@ agent's `POST https://trae-api-cn.mchost.guru/api/agent/v3/llm_utils_chat`:
|
|
|
122
122
|
that fails (an `error` event, or Trae's stream breaking off) ends with the
|
|
123
123
|
error and `[DONE]`, with no finish.
|
|
124
124
|
- **Tools:**
|
|
125
|
-
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
125
|
+
- The tools go natively in `tools`, with `parameters` as a JSON string.
|
|
126
|
+
A past call goes back as the turn Trae made it: an assistant
|
|
127
|
+
`tool_calls` message carrying it (each call
|
|
128
|
+
`{id, type: "function", function_call: {name, arguments}}`) and a
|
|
129
|
+
`tool` message after it answering it. Trae answers such a history with
|
|
130
|
+
native calls; asked for its calls in text instead — a system prompt
|
|
131
|
+
describing a `<tool_call>` block, earlier calls written out as one —
|
|
132
|
+
models write them as blocks, and a block the stream cuts short is an
|
|
133
|
+
error (plugins#38). Trae takes no two messages of one role in a row,
|
|
134
|
+
so an assistant turn's text is folded into the calls turn after it
|
|
135
|
+
and consecutive calls turns join one `tool_calls` list.
|
|
136
|
+
- A call with no name is dropped, and the `tool` result answering it
|
|
137
|
+
with it: Trae requires the name, and an empty `tool_calls` list with a
|
|
138
|
+
result naming no call is not a turn it takes.
|
|
139
|
+
- A result with no text goes as `(no output)` (`(the result's image
|
|
140
|
+
follows)` when it had only an image): Trae refuses the whole request
|
|
141
|
+
with 4027 when a result's text is empty, reading the calls turn as
|
|
142
|
+
unanswered (measured on Trae CN in #251). A result answering no call
|
|
143
|
+
the history sends goes back as text in a `user` turn.
|
|
144
|
+
- A request that names no tools to Trae (none sent, or `tool_choice:
|
|
145
|
+
"none"`, such as a summary) sends its past calls and results as text,
|
|
146
|
+
as before: every live run of the native turns carried the tools.
|
|
130
147
|
- GLM also writes its own template, `<tool_call>name<arg_key>k</arg_key>
|
|
131
148
|
<arg_value>v</arg_value></tool_call>` (a string kept as written, other
|
|
132
149
|
types read as JSON), and sometimes a whole call into a native call's
|
|
@@ -148,11 +165,21 @@ agent's `POST https://trae-api-cn.mchost.guru/api/agent/v3/llm_utils_chat`:
|
|
|
148
165
|
or is cut short, is never sent on as the answer: the reply ends with an
|
|
149
166
|
error (a 502 without a stream), so the gateway can try again
|
|
150
167
|
(plugins#38). A `<tool_call>` in backticks or a code fence is text.
|
|
168
|
+
- Kimi and MiMo sometimes write a call in the tags they were trained on:
|
|
169
|
+
Qwen's `<function=name><parameter=k>v</parameter></function>` (bare or
|
|
170
|
+
inside `<tool_call>`), or Anthropic's `<function_calls>` around one or
|
|
171
|
+
more `<invoke name="…"><parameter name="k">v</parameter></invoke>`.
|
|
172
|
+
Each is read as a call, a parameter's value as JSON unless the tool's
|
|
173
|
+
schema says string, and one cut short is an error as above. These
|
|
174
|
+
tags are read only for a tool the request declared; for any other, or
|
|
175
|
+
in backticks or a code fence, they stay text.
|
|
151
176
|
- Calls from either source become OpenAI `tool_calls`. A name matching a
|
|
152
177
|
requested tool but for case or punctuation (`Read`) goes on as the
|
|
153
178
|
request's (`read`); any other goes on as written, so the agent can say
|
|
154
179
|
the tool doesn't exist.
|
|
155
|
-
-
|
|
180
|
+
- Trae requires the message answering a call to follow the call: the
|
|
181
|
+
results of parallel calls go out as consecutive `tool` messages, with
|
|
182
|
+
nothing between them, or the later ones lose their answer (11148).
|
|
156
183
|
- Images: Trae's lists mark each model `multimodal` true or false (the
|
|
157
184
|
Doubao-Seed models, Kimi K3 and K2.6, Qwen 3.7 Plus and MiniMax M3 take
|
|
158
185
|
images; GLM-5 and DeepSeek V4 don't). A model marked true is listed
|
|
@@ -162,6 +189,13 @@ agent's `POST https://trae-api-cn.mchost.guru/api/agent/v3/llm_utils_chat`:
|
|
|
162
189
|
"[an image, which this model can't see]" in its place. A model whose
|
|
163
190
|
list hasn't been read is sent its images, unless the fallback list
|
|
164
191
|
says it takes none.
|
|
192
|
+
A tool result's images go in a `user` message after the whole run of
|
|
193
|
+
results, not in the `tool` turn: measured on the live endpoint, an
|
|
194
|
+
`image_url` part in a `tool` turn reaches Kimi K2.6 but not
|
|
195
|
+
Doubao-Seed (it answered "Blue"/"Black" to a red image), while the
|
|
196
|
+
user-turn placement is read by both. The results of one turn stay
|
|
197
|
+
consecutive — nothing may go between them (11148) — so the images
|
|
198
|
+
wait for the last of them.
|
|
165
199
|
|
|
166
200
|
The model list is the account's own, from
|
|
167
201
|
`/api/ide/v1/batch_get_detail_param`, every function's list in one ask, as
|
|
@@ -283,9 +317,11 @@ Trae CN gives credits for a daily check-in (每日签到); Trae Global has no
|
|
|
283
317
|
such page, so this is CN only. magpie can press it
|
|
284
318
|
once a day for each account (Settings, or the switch on the usage card),
|
|
285
319
|
through `auth.checkin`: `claimed` when Trae grants the credits, `done`
|
|
286
|
-
when the status
|
|
287
|
-
`
|
|
288
|
-
|
|
320
|
+
when the status says today's is in already, `inactive` when the page is
|
|
321
|
+
disabled, `ineligible` when the claim answers 9095 (this device has
|
|
322
|
+
checked in today) or 9074 (to be done in Trae CN's own app; not asked
|
|
323
|
+
again that day), and `failed` for a turned-away sign-in or any other
|
|
324
|
+
answer, which is tried again later that day.
|
|
289
325
|
It asks Trae CN's own pages through
|
|
290
326
|
this plugin's fetch, which sends them as the account, as Trae CN's IDE
|
|
291
327
|
(3.3.104) sends them: its Cloud-IDE-JWT (renewed first when near its
|
|
@@ -333,7 +369,9 @@ answered it 9074 even with the client's headers.
|
|
|
333
369
|
in today, and that holds for the day. No device id is ever changed or
|
|
334
370
|
rotated to get round a 9095 or a 9074, and no account is given another
|
|
335
371
|
account's device or its own client's device unless the client is signed
|
|
336
|
-
in as that account.
|
|
372
|
+
in as that account. The credit a claim granted is its `credits` plus
|
|
373
|
+
its `extra_credits`, either alone; the status answer's is the fallback
|
|
374
|
+
when the claim says none.
|
|
337
375
|
|
|
338
376
|
Only `api.trae.cn`'s `/trae/api/` pages are sent this way; any other URL
|
|
339
377
|
that isn't a chat request is still refused (400). Needs 0.1.4 or later.
|
package/index.mjs
CHANGED
|
@@ -821,7 +821,16 @@ function clientStoragePaths(platform, home, env) {
|
|
|
821
821
|
const file = (root, name) => join(root, name, "User", "globalStorage", "storage.json")
|
|
822
822
|
if (platform === "darwin") return [file(join(home, "Library", "Application Support"), "Trae CN")]
|
|
823
823
|
if (platform === "win32") {
|
|
824
|
-
|
|
824
|
+
// the two roots are often the same folder: APPDATA and the home's own
|
|
825
|
+
// AppData\Roaming. They are compared as one system spells them, so a
|
|
826
|
+
// difference of separator or case alone doesn't read the same file twice
|
|
827
|
+
const spell = (r) => String(r ?? "").replace(/[\\/]+/g, "\\").replace(/\\+$/, "").toLowerCase()
|
|
828
|
+
const roots = []
|
|
829
|
+
for (const r of [env.APPDATA, join(home, "AppData", "Roaming")]) {
|
|
830
|
+
if (!r) continue
|
|
831
|
+
if (roots.some((was) => spell(was) === spell(r))) continue
|
|
832
|
+
roots.push(r)
|
|
833
|
+
}
|
|
825
834
|
return roots.flatMap((r) => ["Trae CN", "trae-cn"].map((n) => file(r, n)))
|
|
826
835
|
}
|
|
827
836
|
if (platform === "linux") return ["trae-cn", "Trae CN"].map((n) => file(env.XDG_CONFIG_HOME || join(home, ".config"), n))
|
|
@@ -914,29 +923,13 @@ const ugPage = (url) => {
|
|
|
914
923
|
}
|
|
915
924
|
}
|
|
916
925
|
|
|
917
|
-
//
|
|
918
|
-
//
|
|
919
|
-
//
|
|
920
|
-
//
|
|
926
|
+
// A model that writes a tool call as text still writes it, mostly GLM and
|
|
927
|
+
// DeepSeek: the blocks are read as the calls they mean (the tools also go
|
|
928
|
+
// natively in the body, and a past call is a native turn, so an answer is
|
|
929
|
+
// usually native; the text form is read for when it is not).
|
|
921
930
|
const OPEN = "<tool_call>"
|
|
922
931
|
const CLOSE = "</tool_call>"
|
|
923
932
|
|
|
924
|
-
function toolPrompt(tools, choice, parallel) {
|
|
925
|
-
const defs = tools.map((t) => {
|
|
926
|
-
const f = t.function ?? t
|
|
927
|
-
return { name: f.name, description: f.description ?? "", parameters: f.parameters ?? {} }
|
|
928
|
-
})
|
|
929
|
-
const lines = [
|
|
930
|
-
"You can call the client's tools, listed below as JSON. They run on the user's machine, not on yours: use no built-in or server tool to reach the user's files.",
|
|
931
|
-
`To call a tool, write one block per call and nothing after it: ${OPEN}{"name":"tool_name","arguments":{...}}${CLOSE}. Use an exact tool name and fill arguments from its schema. Then stop and wait: the result comes back in the next message.`,
|
|
932
|
-
"A call is not done until its result comes back; don't say it is, and don't repeat a call whose result you have.",
|
|
933
|
-
]
|
|
934
|
-
if (parallel === false) lines.push("Call at most one tool at a time.")
|
|
935
|
-
if (choice === "required") lines.push("Call a tool before answering.")
|
|
936
|
-
else if (choice && typeof choice === "object" && (choice.function?.name || choice.name)) lines.push(`Call the tool ${choice.function?.name || choice.name} before answering.`)
|
|
937
|
-
lines.push("Tools:", JSON.stringify(defs))
|
|
938
|
-
return lines.join("\n")
|
|
939
|
-
}
|
|
940
933
|
|
|
941
934
|
const partText = (content) => {
|
|
942
935
|
if (typeof content === "string") return content
|
|
@@ -970,43 +963,137 @@ const partsOf = (content, sees) => {
|
|
|
970
963
|
// the model takes images: Trae's list says so of each model (multimodal),
|
|
971
964
|
// and a model it says doesn't is sent the placeholder, not the image.
|
|
972
965
|
function traeMessages(req, sees = false) {
|
|
973
|
-
|
|
966
|
+
// native is whether the body names the tools to Trae (chatBody's own
|
|
967
|
+
// test). Past calls go back as Trae's own turns only then: every live
|
|
968
|
+
// run of that shape carried the tools, and a request without them (a
|
|
969
|
+
// summary, tool_choice "none") keeps the text form it always had
|
|
970
|
+
const native = Array.isArray(req.tools) && req.tools.length > 0 && req.tool_choice !== "none"
|
|
974
971
|
const names = new Map()
|
|
975
972
|
const out = []
|
|
976
|
-
|
|
973
|
+
// dropped is every call id a turn carried but Trae cannot be sent: an
|
|
974
|
+
// assistant turn whose calls all lost their name takes its results with it
|
|
975
|
+
const dropped = new Set()
|
|
976
|
+
// images of tool results, held until the run of results ends: Trae
|
|
977
|
+
// takes no message between a call's results, so they cannot go after
|
|
978
|
+
// each one — one user message once they are all in
|
|
979
|
+
const pending = []
|
|
980
|
+
const flushImages = () => {
|
|
981
|
+
if (pending.length) out.push({ role: "user", images: pending.splice(0) })
|
|
982
|
+
}
|
|
977
983
|
for (const m of req.messages ?? []) {
|
|
978
984
|
const role = m.role === "developer" ? "system" : m.role
|
|
979
985
|
let { text, images } = partsOf(m.content, sees)
|
|
980
|
-
if (role === "assistant") {
|
|
981
|
-
const calls =
|
|
982
|
-
names.set(c.id, c.function?.name)
|
|
986
|
+
if (!native && role === "assistant" && Array.isArray(m.tool_calls) && m.tool_calls.length) {
|
|
987
|
+
const calls = m.tool_calls.map((c) => {
|
|
988
|
+
names.set(String(c.id ?? ""), c.function?.name)
|
|
983
989
|
let args = c.function?.arguments ?? "{}"
|
|
984
990
|
try {
|
|
985
991
|
args = JSON.parse(args)
|
|
986
992
|
} catch {}
|
|
987
993
|
return OPEN + JSON.stringify({ name: c.function?.name, arguments: args }) + CLOSE
|
|
988
994
|
})
|
|
989
|
-
text
|
|
990
|
-
|
|
995
|
+
out.push({ role: "assistant", text: [text, ...calls].filter(Boolean).join("\n") })
|
|
996
|
+
continue
|
|
997
|
+
}
|
|
998
|
+
if (role === "assistant" && Array.isArray(m.tool_calls) && m.tool_calls.length) {
|
|
999
|
+
// a past call goes back as the turn Trae made it: an assistant
|
|
1000
|
+
// tool_calls message, its result in the tool message after it. Trae
|
|
1001
|
+
// answers such a history with native calls; written out as a
|
|
1002
|
+
// <tool_call> block in the text it answers in blocks too, and a block
|
|
1003
|
+
// the stream cuts short ends the turn as an error (plugins#38).
|
|
1004
|
+
const calls = []
|
|
1005
|
+
for (const c of m.tool_calls) {
|
|
1006
|
+
const name = String(c.function?.name ?? c.name ?? "").trim()
|
|
1007
|
+
// a call Trae cannot be sent is dropped here; its result goes too
|
|
1008
|
+
// (an empty tool_calls list and a result naming no call are not a
|
|
1009
|
+
// turn Trae takes)
|
|
1010
|
+
if (!name) {
|
|
1011
|
+
if (c.id != null) dropped.add(String(c.id))
|
|
1012
|
+
continue
|
|
1013
|
+
}
|
|
1014
|
+
names.set(String(c.id ?? ""), name)
|
|
1015
|
+
let args = c.function?.arguments ?? c.arguments ?? "{}"
|
|
1016
|
+
if (typeof args !== "string") {
|
|
1017
|
+
try {
|
|
1018
|
+
args = JSON.stringify(args)
|
|
1019
|
+
} catch {
|
|
1020
|
+
args = "{}"
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
calls.push({ id: String(c.id ?? "call_" + (names.size + 1)), type: "function", function_call: { name, arguments: args } })
|
|
1024
|
+
}
|
|
1025
|
+
if (calls.length) out.push({ role: "assistant", text, images, calls })
|
|
1026
|
+
else if (text || images.length) out.push({ role: "assistant", text, images })
|
|
1027
|
+
continue
|
|
1028
|
+
}
|
|
1029
|
+
if (role === "assistant") {
|
|
1030
|
+
flushImages()
|
|
1031
|
+
out.push({ role: "assistant", text, images })
|
|
991
1032
|
} else if (role === "tool") {
|
|
992
|
-
|
|
1033
|
+
const id = String(m.tool_call_id ?? "")
|
|
1034
|
+
if (dropped.has(id)) continue
|
|
1035
|
+
// a result answering no call this history sends (the text form, or
|
|
1036
|
+
// a call cut away before it) is no turn Trae takes: it goes as text
|
|
1037
|
+
if (!native || !names.has(id)) {
|
|
1038
|
+
flushImages()
|
|
1039
|
+
out.push({ role: "user", text: `Result of ${names.get(id) || m.name || "the tool"} (call ${m.tool_call_id ?? "?"}):\n${text}`, images })
|
|
1040
|
+
continue
|
|
1041
|
+
}
|
|
1042
|
+
// Trae's own result turn: the call it answers, its text as parts
|
|
1043
|
+
// (an empty one for a result with no text). Its images wait: a
|
|
1044
|
+
// tool turn's image_url reaches some models (Kimi) and not others
|
|
1045
|
+
// (Doubao-Seed), so they go in a user message after the whole run
|
|
1046
|
+
// of results, which every multimodal model reads
|
|
1047
|
+
if (images.length) pending.push(...images)
|
|
1048
|
+
// Trae refuses a result with no text (4027: the calls turn reads as
|
|
1049
|
+
// unanswered), so one that printed nothing says so (plugins#251)
|
|
1050
|
+
out.push({ role: "tool", tool_call_id: id, text: text || (images.length ? "(the result's image follows)" : "(no output)") })
|
|
993
1051
|
} else {
|
|
1052
|
+
flushImages()
|
|
994
1053
|
out.push({ role: role === "system" ? "system" : "user", text, images })
|
|
995
1054
|
}
|
|
996
1055
|
}
|
|
997
|
-
|
|
998
|
-
// an
|
|
1056
|
+
flushImages()
|
|
1057
|
+
// Trae takes no two messages of one role in a row, so an assistant turn is
|
|
1058
|
+
// folded into the turn after it: text into the calls turn it introduces
|
|
1059
|
+
// (agents send "let me read" and then the call, and parallel calls split
|
|
1060
|
+
// over two assistant turns), calls joined into one list. A message of an
|
|
1061
|
+
// image alone is not empty; tool results are turns of their own, each
|
|
1062
|
+
// answering its own call.
|
|
999
1063
|
const merged = []
|
|
1000
1064
|
for (const m of out) {
|
|
1001
1065
|
const images = m.images ?? []
|
|
1066
|
+
if (m.role === "tool") {
|
|
1067
|
+
merged.push(m)
|
|
1068
|
+
continue
|
|
1069
|
+
}
|
|
1070
|
+
if (m.role === "assistant") {
|
|
1071
|
+
const last = merged.at(-1)
|
|
1072
|
+
if (last && last.role === "assistant") {
|
|
1073
|
+
// fold: text and images join, calls append to the same list
|
|
1074
|
+
last.text = [last.text, m.text].filter(Boolean).join("\n\n")
|
|
1075
|
+
last.images = [...(last.images ?? []), ...images]
|
|
1076
|
+
if (m.calls) last.calls = [...(last.calls ?? []), ...m.calls]
|
|
1077
|
+
if (!last.text && !last.images.length && !last.calls) merged.pop()
|
|
1078
|
+
continue
|
|
1079
|
+
}
|
|
1080
|
+
if (!m.text && !images.length && !m.calls) continue
|
|
1081
|
+
merged.push({ role: "assistant", text: m.text, images, ...(m.calls ? { calls: m.calls } : {}) })
|
|
1082
|
+
continue
|
|
1083
|
+
}
|
|
1002
1084
|
if (!m.text && !images.length) continue
|
|
1003
1085
|
const last = merged.at(-1)
|
|
1004
1086
|
if (last && last.role === m.role && m.role !== "system") {
|
|
1005
1087
|
last.text = [last.text, m.text].filter(Boolean).join("\n\n")
|
|
1006
|
-
last.images = [...last.images, ...images]
|
|
1088
|
+
last.images = [...(last.images ?? []), ...images]
|
|
1007
1089
|
} else merged.push({ role: m.role, text: m.text, images })
|
|
1008
1090
|
}
|
|
1009
|
-
return merged.map((m) =>
|
|
1091
|
+
return merged.map((m) => {
|
|
1092
|
+
const content = [...(m.text ? [{ type: "text", text: m.text }] : []), ...(m.images ?? [])]
|
|
1093
|
+
if (m.calls) return { role: "assistant", content: content.length ? content : null, tool_calls: m.calls }
|
|
1094
|
+
if (m.role === "tool") return { role: "tool", tool_call_id: m.tool_call_id, content } // results of one turn stay consecutive: nothing may go between them (11148)
|
|
1095
|
+
return { role: m.role, content }
|
|
1096
|
+
})
|
|
1010
1097
|
}
|
|
1011
1098
|
|
|
1012
1099
|
// nativeTools are the tools as Trae names them: parameters a JSON string
|
|
@@ -1245,7 +1332,8 @@ class TextTools {
|
|
|
1245
1332
|
const r = this.buf.search(REASON_AT)
|
|
1246
1333
|
const sm = this.buf.match(STRAY_AT)
|
|
1247
1334
|
const s = sm ? sm.index : -1
|
|
1248
|
-
|
|
1335
|
+
const x = this.buf.search(XML_AT)
|
|
1336
|
+
if (s >= 0 && [i, d, r, x].every((y) => y < 0 || s < y)) {
|
|
1249
1337
|
// a close with no call open, with what GLM left before it
|
|
1250
1338
|
// (}}</tool_call>, magpie#1945): not the answer, dropped; in code,
|
|
1251
1339
|
// shown
|
|
@@ -1264,7 +1352,7 @@ class TextTools {
|
|
|
1264
1352
|
this.after = true
|
|
1265
1353
|
continue
|
|
1266
1354
|
}
|
|
1267
|
-
if (r >= 0 && (i < 0 || r < i) && (d < 0 || r < d)) {
|
|
1355
|
+
if (r >= 0 && (i < 0 || r < i) && (d < 0 || r < d) && (x < 0 || r < x)) {
|
|
1268
1356
|
const o = objectEnd(this.buf, r)
|
|
1269
1357
|
if (o === -1 && !end) {
|
|
1270
1358
|
// not whole yet: held, the text before it sent
|
|
@@ -1284,7 +1372,39 @@ class TextTools {
|
|
|
1284
1372
|
this.buf = this.buf.slice(r + 1)
|
|
1285
1373
|
continue
|
|
1286
1374
|
}
|
|
1287
|
-
if (i < 0 && d < 0) break
|
|
1375
|
+
if (i < 0 && d < 0 && x < 0) break
|
|
1376
|
+
if (x >= 0 && (i < 0 || x < i) && (d < 0 || x < d)) {
|
|
1377
|
+
// Anthropic's <function_calls><invoke name="…"> or Qwen's
|
|
1378
|
+
// <function=…>, as Kimi and MiMo write a call (01huadalang on
|
|
1379
|
+
// magpie's Discord, dsh): a call only for a tool the request
|
|
1380
|
+
// declared, and never in a code fence or backticks
|
|
1381
|
+
const head = this.buf.slice(0, x)
|
|
1382
|
+
const at = this.buf.slice(x)
|
|
1383
|
+
const opener = at.match(/^<[^<>]*>?/)[0]
|
|
1384
|
+
if (quoted(this.sent + text + head)) {
|
|
1385
|
+
text += head + opener
|
|
1386
|
+
this.buf = at.slice(opener.length)
|
|
1387
|
+
continue
|
|
1388
|
+
}
|
|
1389
|
+
const b = this.xmlCall(at, end)
|
|
1390
|
+
if (b.wait) break
|
|
1391
|
+
if (b.text) {
|
|
1392
|
+
text += head + opener
|
|
1393
|
+
this.buf = at.slice(opener.length)
|
|
1394
|
+
continue
|
|
1395
|
+
}
|
|
1396
|
+
text += head
|
|
1397
|
+
if (b.error) {
|
|
1398
|
+
this.buf = ""
|
|
1399
|
+
this.sent += text
|
|
1400
|
+
return { text, calls, reasoning, error: b.error }
|
|
1401
|
+
}
|
|
1402
|
+
calls.push(b.call)
|
|
1403
|
+
this.mark = this.sent.length + text.length
|
|
1404
|
+
this.buf = b.rest
|
|
1405
|
+
this.after = true
|
|
1406
|
+
continue
|
|
1407
|
+
}
|
|
1288
1408
|
if (d >= 0 && (i < 0 || d < i)) {
|
|
1289
1409
|
text += this.buf.slice(0, d)
|
|
1290
1410
|
this.buf = this.buf.slice(d)
|
|
@@ -1375,7 +1495,7 @@ class TextTools {
|
|
|
1375
1495
|
this.buf = ""
|
|
1376
1496
|
} else {
|
|
1377
1497
|
// keep back an opened block, or what may be the start of one
|
|
1378
|
-
const at = [this.buf.search(OPEN_AT), this.buf.search(OPEN_HOLD), this.buf.search(DSML_AT), this.buf.search(REASON_AT)].filter((x) => x >= 0)
|
|
1498
|
+
const at = [this.buf.search(OPEN_AT), this.buf.search(OPEN_HOLD), this.buf.search(DSML_AT), this.buf.search(REASON_AT), this.buf.search(XML_AT), this.buf.search(XML_HOLD)].filter((x) => x >= 0)
|
|
1379
1499
|
let keep = at.length ? this.buf.length - Math.min(...at) : 0
|
|
1380
1500
|
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 }
|
|
1381
1501
|
// and braces, brackets, quotes or a > a close may still come after
|
|
@@ -1388,6 +1508,60 @@ class TextTools {
|
|
|
1388
1508
|
return { text, calls, reasoning }
|
|
1389
1509
|
}
|
|
1390
1510
|
|
|
1511
|
+
// xmlCall reads a call written in Anthropic's tags (<function_calls>
|
|
1512
|
+
// around one or more <invoke name="…"><parameter name="k">v</parameter>
|
|
1513
|
+
// </invoke>) or Qwen's (<function=…><parameter=k>v</parameter>
|
|
1514
|
+
// </function>), s starting at its first tag: {call, rest}, {wait}
|
|
1515
|
+
// while it may still become one, {text} when it is no call (a tool the
|
|
1516
|
+
// request didn't declare, a tag named in prose), {error} for one cut
|
|
1517
|
+
// short. Each invoke is a call of its own; the tags between them are
|
|
1518
|
+
// dropped as the leftovers of one.
|
|
1519
|
+
xmlCall(s, end) {
|
|
1520
|
+
const w = s.match(/^<(?:antml:)?function_calls>/)
|
|
1521
|
+
if (w) {
|
|
1522
|
+
const rest = s.slice(w[0].length).replace(/^\s+/, "")
|
|
1523
|
+
if (!end && (!rest || XML_STARTS.some((k) => k.startsWith(rest)))) return { wait: true }
|
|
1524
|
+
return this.xmlInvoke(rest, end)
|
|
1525
|
+
}
|
|
1526
|
+
return this.xmlInvoke(s, end)
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
xmlInvoke(s, end) {
|
|
1530
|
+
let m = s.match(/^<(?:antml:)?invoke\s+name\s*=\s*"([^"<>]+)"\s*>/)
|
|
1531
|
+
let closeRe = /<\/(?:antml:)?invoke\s*>/
|
|
1532
|
+
if (!m) {
|
|
1533
|
+
m = s.match(/^<function=([^\s<>]+?)\s*>/)
|
|
1534
|
+
closeRe = /<\/function\s*>/
|
|
1535
|
+
}
|
|
1536
|
+
if (!m) {
|
|
1537
|
+
// an opener still being written
|
|
1538
|
+
if (!end && (XML_STARTS.some((k) => k.startsWith(s)) || /^<(?:antml:)?(?:invoke\s|function=)[^<>]{0,300}$/.test(s))) return { wait: true }
|
|
1539
|
+
return { text: true }
|
|
1540
|
+
}
|
|
1541
|
+
const declared = this.tools.map((t) => (t.function ?? t)?.name).filter(Boolean)
|
|
1542
|
+
const name = toolNamed(m[1].trim(), this.tools)
|
|
1543
|
+
if (!declared.includes(name)) return { text: true }
|
|
1544
|
+
const body = s.slice(m[0].length)
|
|
1545
|
+
const c = body.match(closeRe)
|
|
1546
|
+
const n = body.search(XML_NEXT)
|
|
1547
|
+
let stop = c ? c.index : -1
|
|
1548
|
+
if (n >= 0 && (stop < 0 || n < stop)) stop = n
|
|
1549
|
+
if (stop < 0) {
|
|
1550
|
+
if (!end) return { wait: true }
|
|
1551
|
+
stop = body.length
|
|
1552
|
+
}
|
|
1553
|
+
const whole = !!c && c.index === stop
|
|
1554
|
+
const region = body.slice(0, stop).replace(/<(\/?)antml:parameter\b/g, "<$1parameter")
|
|
1555
|
+
const p = paramArgs(region, name, this.tools)
|
|
1556
|
+
if (!whole && !p.closed) {
|
|
1557
|
+
// no parameter and no close: a tag named in passing; a parameter
|
|
1558
|
+
// left open: the call cut short (plugins#38)
|
|
1559
|
+
if (!/<parameter\b/.test(region)) return { text: true }
|
|
1560
|
+
return { error: CUT_SHORT }
|
|
1561
|
+
}
|
|
1562
|
+
return { call: { name, arguments: JSON.stringify(p.args) }, rest: whole ? body.slice(stop + c[0].length) : body.slice(stop) }
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1391
1565
|
// block reads what follows a <tool_call> opener: {call, rest} for a
|
|
1392
1566
|
// call, {wait} while it may still become one, {text} when it is no call
|
|
1393
1567
|
// at all, {error} for one that can't be read: cut short, or not JSON
|
|
@@ -1873,11 +2047,18 @@ const DSML_INVOKE = new RegExp(String.raw`^<\s*[||][\s||]*DSML[\s||]*invok
|
|
|
1873
2047
|
const DSML_INVOKE_AT = new RegExp(String.raw`<\s*[||][\s||]*DSML[\s||]*invoke\s+name\s*=`)
|
|
1874
2048
|
const DSML_CLOSE = new RegExp(String.raw`<\s*\/\s*[||][\s||]*DSML[\s||]*invoke\s*>`)
|
|
1875
2049
|
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")
|
|
2050
|
+
// Anthropic's and Qwen's call tags (xmlCall): where one starts, an
|
|
2051
|
+
// opener still being written, the openers a wrapper is followed by, and
|
|
2052
|
+
// the next call's opener
|
|
2053
|
+
const XML_AT = /<(?:antml:)?(?:function_calls>|invoke\s+name\s*=|function=)/
|
|
2054
|
+
const XML_HOLD = /<(?:antml:)?(?:invoke\s[^<>]{0,300}|function=[^<>]{0,300})$/
|
|
2055
|
+
const XML_STARTS = ["<invoke ", "<antml:invoke ", "<function="]
|
|
2056
|
+
const XML_NEXT = /<(?:antml:)?invoke\s+name\s*=|<function=/
|
|
1876
2057
|
// what is left after a call: blanks, our close, DeepSeek's tags but the
|
|
1877
|
-
// next call's
|
|
1878
|
-
const LEFTOVER = new RegExp(String.raw`^(?:\s+|<\/tool_?calls?>|<tool_?calls>|` + DSML + String.raw`(?![\s||]*invoke\b)[^<>]*>)+`)
|
|
2058
|
+
// next call's, the closes of Anthropic's and Qwen's
|
|
2059
|
+
const LEFTOVER = new RegExp(String.raw`^(?:\s+|<\/tool_?calls?>|<tool_?calls>|<\/(?:antml:)?(?:function_calls|invoke|function)>|` + DSML + String.raw`(?![\s||]*invoke\b)[^<>]*>)+`)
|
|
1879
2060
|
// what text may be the start of, so held back
|
|
1880
|
-
const MARKS = [OPEN, CLOSE, "<toolcall>", "</toolcall>", "<|DSML|", "<|DSML|", "</|DSML|", "</|DSML|", '{"reasoning_content"']
|
|
2061
|
+
const MARKS = [OPEN, CLOSE, "<toolcall>", "</toolcall>", "<|DSML|", "<|DSML|", "</|DSML|", "</|DSML|", '{"reasoning_content"', "<function_calls>", "</function_calls>", "<antml:function_calls>", "</antml:function_calls>", "<invoke ", "<antml:invoke ", "</invoke>", "</antml:invoke>", "<function=", "</function>"]
|
|
1881
2062
|
// reasoning a model wrote into its text as JSON
|
|
1882
2063
|
const REASON_AT = /\{\s*"reasoning_content"\s*:/
|
|
1883
2064
|
const MARK_MAX = Math.max(...MARKS.map((m) => m.length))
|
|
@@ -2008,7 +2189,13 @@ class NativeCalls {
|
|
|
2008
2189
|
this.calls.push({ ...c })
|
|
2009
2190
|
return
|
|
2010
2191
|
}
|
|
2192
|
+
// a name comes as the arguments do: a snapshot that grows. A name that
|
|
2193
|
+
// starts with what was read is that snapshot further along, and takes
|
|
2194
|
+
// its place — a wrapper cut short at a tag, then the whole call again.
|
|
2195
|
+
// A name that doesn't start with it is a piece of the name's own, or
|
|
2196
|
+
// one no snapshot explains, and the first is kept as it was
|
|
2011
2197
|
if (!o.name) o.name = c.name
|
|
2198
|
+
else if (c.name && c.name.startsWith(o.name) && c.name !== o.name) o.name = c.name
|
|
2012
2199
|
if (c.index !== null && o.index === null) o.index = c.index
|
|
2013
2200
|
const prev = o.arguments
|
|
2014
2201
|
const next = c.arguments
|
|
@@ -2135,7 +2322,9 @@ async function* parts(events, tools = []) {
|
|
|
2135
2322
|
if (data === "[DONE]") break
|
|
2136
2323
|
if (name === "error" || (name !== "output" && d.code && d.message && !d.response && !d.content)) {
|
|
2137
2324
|
const e = errorOf(d)
|
|
2138
|
-
|
|
2325
|
+
// the realm answering is named by the caller, which knows it; a bare
|
|
2326
|
+
// word here reads as neither realm's
|
|
2327
|
+
yield { error: e.message || "Trae returned an error", code: e.code }
|
|
2139
2328
|
return
|
|
2140
2329
|
}
|
|
2141
2330
|
if (name === "token_usage") {
|
|
@@ -2151,9 +2340,12 @@ async function* parts(events, tools = []) {
|
|
|
2151
2340
|
}
|
|
2152
2341
|
if (!ANSWER.has(name)) continue
|
|
2153
2342
|
const delta = d.delta && typeof d.delta === "object" ? d.delta : {}
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2343
|
+
// the reasoning's own fields, in the order Trae has put it under: an
|
|
2344
|
+
// empty one is no answer, so the next is asked (an event carrying both
|
|
2345
|
+
// reasoning_content: "" and reasoning: "…" says the thought in the second)
|
|
2346
|
+
const said = [d.reasoning_content, d.reasoning, delta.reasoning_content, delta.reasoning].find((v) => typeof v === "string" && v) ?? ""
|
|
2347
|
+
if (said) {
|
|
2348
|
+
const r = th.add(said)
|
|
2157
2349
|
if (r) yield { reasoning: r }
|
|
2158
2350
|
}
|
|
2159
2351
|
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 : ""
|
|
@@ -2255,8 +2447,9 @@ async function openai(req, it, abort, site) {
|
|
|
2255
2447
|
// a reply that failed (an error event, or Trae's stream breaking off)
|
|
2256
2448
|
// ends with the error and [DONE], not a finish that says it stopped; the
|
|
2257
2449
|
// stream is closed whatever happens, and an agent that goes away stops
|
|
2258
|
-
// the reading of Trae's
|
|
2259
|
-
|
|
2450
|
+
// the reading of Trae's. The realm answering names itself, so a Global
|
|
2451
|
+
// account's failure reads as Trae Global's
|
|
2452
|
+
const error = (message, code = null) => enc.encode(`data: ${JSON.stringify({ error: { message: `${site.realm}: ${message}`, type: "api_error", code } })}\n\n`)
|
|
2260
2453
|
let gone = false
|
|
2261
2454
|
const stream = new ReadableStream({
|
|
2262
2455
|
async start(ctl) {
|
|
@@ -2562,8 +2755,12 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
2562
2755
|
if (String(ce.code) === "9074") return { outcome: "ineligible", message: `${ce.message} — check in in the Trae CN app` }
|
|
2563
2756
|
if (claim.status !== 200 || (ce.code && String(ce.code) !== "0")) return { outcome: "failed", message: ce.message || statusLine(claim.status) }
|
|
2564
2757
|
if (!answer || typeof answer !== "object" || Array.isArray(answer)) return { outcome: "failed", message: `check-in claim: unreadable reply ${claimText.trim().slice(0, 120) || "(empty)"}` }
|
|
2565
|
-
|
|
2566
|
-
|
|
2758
|
+
// a claim may answer extra_credits with no credits beside it, and either
|
|
2759
|
+
// may be absent: Number(undefined) is NaN, and NaN + n is NaN, which
|
|
2760
|
+
// reads as no credit at all
|
|
2761
|
+
const n = (v) => (Number.isFinite(Number(v)) ? Number(v) : 0)
|
|
2762
|
+
const credit = n(answer.credits) + n(answer.extra_credits)
|
|
2763
|
+
const fallback = n(body.credits) + n(body.extra_credits)
|
|
2567
2764
|
return { outcome: "claimed", ...(credit > 0 ? { credit } : fallback > 0 ? { credit: fallback } : {}) }
|
|
2568
2765
|
}
|
|
2569
2766
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@magpie-community/opencode-trae-auth",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.19",
|
|
4
4
|
"description": "OpenCode provider plugins for Trae: Trae CN (trae.cn) and Trae Global (trae.ai), free tier included, with Trae's own browser sign-in",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.mjs",
|