@magpie-community/opencode-trae-auth 0.2.18 → 0.2.20
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 +57 -11
- package/index.mjs +317 -23
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -86,8 +86,9 @@ agent's `POST https://trae-api-cn.mchost.guru/api/agent/v3/llm_utils_chat`:
|
|
|
86
86
|
- The function is the one whose model list has the model, the first that
|
|
87
87
|
names a `__dev` model for it (in the order `chat_v3`, the classic IDE's;
|
|
88
88
|
`solo_work_lite`, SOLO's Work mode; `solo_agent`, the TRAE agent's;
|
|
89
|
-
`solo_agent_lite`). If Trae answers
|
|
90
|
-
the others once and remembers which
|
|
89
|
+
`solo_agent_lite`; `solo_coder`, SOLO's coder roster). If Trae answers
|
|
90
|
+
4001, 4023 or 1005, the plugin tries the others once and remembers which
|
|
91
|
+
one worked.
|
|
91
92
|
- `session_id` is the conversation's, as the IDE keeps one per conversation:
|
|
92
93
|
the session magpie (or OpenCode) names the request by, else the chat's
|
|
93
94
|
first user message, which every later turn repeats, together with the
|
|
@@ -165,6 +166,14 @@ agent's `POST https://trae-api-cn.mchost.guru/api/agent/v3/llm_utils_chat`:
|
|
|
165
166
|
or is cut short, is never sent on as the answer: the reply ends with an
|
|
166
167
|
error (a 502 without a stream), so the gateway can try again
|
|
167
168
|
(plugins#38). A `<tool_call>` in backticks or a code fence is text.
|
|
169
|
+
- Kimi and MiMo sometimes write a call in the tags they were trained on:
|
|
170
|
+
Qwen's `<function=name><parameter=k>v</parameter></function>` (bare or
|
|
171
|
+
inside `<tool_call>`), or Anthropic's `<function_calls>` around one or
|
|
172
|
+
more `<invoke name="…"><parameter name="k">v</parameter></invoke>`.
|
|
173
|
+
Each is read as a call, a parameter's value as JSON unless the tool's
|
|
174
|
+
schema says string, and one cut short is an error as above. These
|
|
175
|
+
tags are read only for a tool the request declared; for any other, or
|
|
176
|
+
in backticks or a code fence, they stay text.
|
|
168
177
|
- Calls from either source become OpenAI `tool_calls`. A name matching a
|
|
169
178
|
requested tool but for case or punctuation (`Read`) goes on as the
|
|
170
179
|
request's (`read`); any other goes on as written, so the agent can say
|
|
@@ -194,7 +203,10 @@ The model list is the account's own, from
|
|
|
194
203
|
TRAE SOLO CN asks it (`/api/ide/v1/get_detail_param`, one function at a
|
|
195
204
|
time, when the batch gives none). The lists are put together: SOLO and the
|
|
196
205
|
TRAE agent list models `chat_v3` doesn't (deepseek-v4.1-flash is the TRAE
|
|
197
|
-
agent's, `solo_agent`)
|
|
206
|
+
agent's, `solo_agent`), and SOLO's coder roster lists models no other chat
|
|
207
|
+
function does (`glm-5v-turbo`, `kimi-k2.5`, `minimax-m2.7`,
|
|
208
|
+
`qwen-3.6-plus`, each answering through `solo_coder`). Left out are the
|
|
209
|
+
IDE's helpers (`usage` other than
|
|
198
210
|
`chat_completion`: summary, fast_apply…), configs switched off and the
|
|
199
211
|
custom-model slots. Context is the list's `context_window_tokens.dev`,
|
|
200
212
|
output the `__dev` model's `max_tokens`. A model with a `__max` model and
|
|
@@ -214,14 +226,48 @@ served under — and asks that model, and its Max, through a list carrying
|
|
|
214
226
|
it. Trae renamed its official entries on 2026-10-09, leaving the name its
|
|
215
227
|
lists mostly agree on the only thing that tells a misnamed entry apart.
|
|
216
228
|
|
|
217
|
-
Reasoning: the models reason,
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
229
|
+
Reasoning: the models reason, and each entry of the account's list says
|
|
230
|
+
which levels it takes — its `reasoning_effort_config`, with the options
|
|
231
|
+
(`light`, `high`, `extra_high`) and the default. The request takes the same
|
|
232
|
+
names as `reasoning_effort`, so a level an agent picks is sent as Trae
|
|
233
|
+
spells it: the plugin offers each model's own levels as `variants`
|
|
234
|
+
(`low`/`high`/`xhigh` for `light`/`high`/`extra_high`), and sends nothing
|
|
235
|
+
for a level the model doesn't name, or for a model whose entry names none —
|
|
236
|
+
Trae then serves it at its own default. An entry naming levels this map
|
|
237
|
+
can't spell offers none, rather than being sent one Trae would reject. A
|
|
238
|
+
request that wants levels for a model the list hasn't named reads the list,
|
|
239
|
+
and that read is remembered for five minutes whether it produced an entry or
|
|
240
|
+
not: a list that fails, or one that doesn't name the model, costs one read
|
|
241
|
+
rather than one per request. (A read costs up to two 20s timeouts when Trae's
|
|
242
|
+
list endpoint is slow, while the chat itself needs no list to answer.) A read
|
|
243
|
+
in flight is shared, not skipped: requests that arrive together — a title
|
|
244
|
+
request beside a session's first turn, subagents — ride the one read and all
|
|
245
|
+
see what it named, so a Max asked that way is still sent under its base
|
|
246
|
+
model's name rather than the agent's own `-max` id. The host's own catalog
|
|
247
|
+
refresh rides the same read, so a refresh and a request never ask twice.
|
|
248
|
+
A request's read swallows an expiry (the chat can still answer); the
|
|
249
|
+
catalog's refresh raises it, so a lapsed account is still marked — including
|
|
250
|
+
when the refresh merely rides a read a request started, which is why the lapse
|
|
251
|
+
travels on the read's record instead of being thrown from it. The host's
|
|
252
|
+
refresh is strict, so it reads again rather than serving a cached lapse: a
|
|
253
|
+
re-sign-in is asked afresh instead of being told again that the account lapsed.
|
|
254
|
+
|
|
255
|
+
The entry kept for a model is the one the request is asked through, and so
|
|
256
|
+
the one whose levels the request is held to, so the lists' disagreement
|
|
257
|
+
about a model's levels matters: `computer_use_subagent`, for one, is named
|
|
258
|
+
with `light`/`high` by `solo_agent` and `solo_agent_lite` and with none by
|
|
259
|
+
`solo_work_lite`. The three signals that pick the entry — its `__dev`
|
|
260
|
+
model, its not carrying another model's name, its carrying the name the
|
|
261
|
+
lists agree on — decide first; of entries that tie on all three, one that
|
|
262
|
+
names levels is kept, so a model Trae serves with levels is not left
|
|
263
|
+
without them by a sibling entry that names none.
|
|
264
|
+
|
|
265
|
+
Every model it lists also says it reasons in its `capabilities`
|
|
266
|
+
(`reasoning: true`, with `toolcall`, `temperature`, `attachment` and the
|
|
267
|
+
input and output modalities), the fallback list's own capabilities kept
|
|
268
|
+
where it has an entry: magpie reads `capabilities.reasoning` alone for a
|
|
269
|
+
model a plugin lists, and a model without it is shown as one that doesn't
|
|
270
|
+
reason (0.2.4).
|
|
225
271
|
|
|
226
272
|
When no
|
|
227
273
|
list can be read, the plugin uses the realm's own fallback list. Trae CN's
|
package/index.mjs
CHANGED
|
@@ -70,8 +70,10 @@ const SITES = {
|
|
|
70
70
|
brand: "ASUS TUF Gaming A15 FA507RM_FA507RM",
|
|
71
71
|
// the IDE's chat functions: the classic IDE's agent, then SOLO's Work
|
|
72
72
|
// mode, then the TRAE agent (solo_agent, which has deepseek-v4.1-flash),
|
|
73
|
-
// then SOLO Lite's agent
|
|
74
|
-
|
|
73
|
+
// then SOLO Lite's agent, then SOLO's coder roster, which lists models no
|
|
74
|
+
// other chat function does (glm-5v-turbo, kimi-k2.5, minimax-m2.7,
|
|
75
|
+
// qwen-3.6-plus — each answers through it)
|
|
76
|
+
functions: ["chat_v3", "solo_work_lite", "solo_agent", "solo_agent_lite", "solo_coder"],
|
|
75
77
|
// what Trae CN's chat_v3 is known to serve; the live list replaces it
|
|
76
78
|
// (the MODEL defaults are spread in at the end of the module, once MODEL
|
|
77
79
|
// is defined, so a site's entry only names what differs)
|
|
@@ -172,7 +174,7 @@ const SITES = {
|
|
|
172
174
|
clientVersionCode: "20260917",
|
|
173
175
|
pluginVersion: "2.3.62834",
|
|
174
176
|
brand: "ASUS TUF Gaming A15 FA507RM_FA507RM",
|
|
175
|
-
functions: ["chat_v3", "solo_work_lite", "solo_agent", "solo_agent_lite"],
|
|
177
|
+
functions: ["chat_v3", "solo_work_lite", "solo_agent", "solo_agent_lite", "solo_coder"],
|
|
176
178
|
// what Trae Global's chat is known to serve (its catalog barely overlaps
|
|
177
179
|
// the CN one's); the live list replaces it
|
|
178
180
|
models: {
|
|
@@ -283,6 +285,13 @@ const SIGN_IN_TIMEOUT = 10 * 60 * 1000
|
|
|
283
285
|
const EARLY_MS = 2 * 60 * 1000 // a token this close to its end is renewed before a request
|
|
284
286
|
const LEAD_MS = 10 * 60 * 1000 // and this close, magpie renews it ahead of time (auth.refresh)
|
|
285
287
|
const DAY = 24 * 3600 * 1000
|
|
288
|
+
// REREAD_MS is how long a list read keeps its answer before a request that
|
|
289
|
+
// wants levels may read the list again: it holds whether the read produced an
|
|
290
|
+
// entry for the model or not, so a list answering badly (or one that doesn't
|
|
291
|
+
// name the model) costs one read, not one per request. A request path that
|
|
292
|
+
// reads on every attempt would otherwise pay two 20s timeouts on each turn —
|
|
293
|
+
// the branch costs the chat itself nothing to answer from.
|
|
294
|
+
const REREAD_MS = 5 * 60 * 1000
|
|
286
295
|
|
|
287
296
|
// a reply limit of 0 is one Trae doesn't say: magpie then takes models.dev's
|
|
288
297
|
// for the model (magpie's catalog), where a made-up 32K told agents a far
|
|
@@ -1168,7 +1177,12 @@ function sessionOf(headers) {
|
|
|
1168
1177
|
// tool result sent back, a retry, another function answering) shows up in the
|
|
1169
1178
|
// usage detail as several, each under no title at all. request_id stays a
|
|
1170
1179
|
// fresh id per request, being that request's own.
|
|
1171
|
-
|
|
1180
|
+
//
|
|
1181
|
+
// levels is the model's own levels (levelsOf of its entry): the effort the
|
|
1182
|
+
// request asks for is sent as the Trae name that model takes, and left out
|
|
1183
|
+
// when it takes none of that name — Trae rejects a level a model has no entry
|
|
1184
|
+
// for, and serves its default when none is asked.
|
|
1185
|
+
function chatBody(req, fn, modelName, most = 0, max = null, session = "", sees = false, levels = null) {
|
|
1172
1186
|
const config = max?.base ?? req.model
|
|
1173
1187
|
const body = {
|
|
1174
1188
|
messages: traeMessages(req, sees),
|
|
@@ -1180,6 +1194,8 @@ function chatBody(req, fn, modelName, most = 0, max = null, session = "", sees =
|
|
|
1180
1194
|
request_id: randomUUID(),
|
|
1181
1195
|
session_id: session || randomUUID(),
|
|
1182
1196
|
}
|
|
1197
|
+
const effort = effortOf(levels, req.reasoning_effort ?? req.reasoningEffort)
|
|
1198
|
+
if (effort) body.reasoning_effort = effort
|
|
1183
1199
|
const asked = req.max_completion_tokens ?? req.max_tokens
|
|
1184
1200
|
if (Number.isFinite(asked) && asked > 0) body.max_tokens = most > 0 ? Math.min(Math.floor(asked), most) : Math.floor(asked)
|
|
1185
1201
|
if (max?.window) {
|
|
@@ -1332,7 +1348,8 @@ class TextTools {
|
|
|
1332
1348
|
const r = this.buf.search(REASON_AT)
|
|
1333
1349
|
const sm = this.buf.match(STRAY_AT)
|
|
1334
1350
|
const s = sm ? sm.index : -1
|
|
1335
|
-
|
|
1351
|
+
const x = this.buf.search(XML_AT)
|
|
1352
|
+
if (s >= 0 && [i, d, r, x].every((y) => y < 0 || s < y)) {
|
|
1336
1353
|
// a close with no call open, with what GLM left before it
|
|
1337
1354
|
// (}}</tool_call>, magpie#1945): not the answer, dropped; in code,
|
|
1338
1355
|
// shown
|
|
@@ -1351,7 +1368,7 @@ class TextTools {
|
|
|
1351
1368
|
this.after = true
|
|
1352
1369
|
continue
|
|
1353
1370
|
}
|
|
1354
|
-
if (r >= 0 && (i < 0 || r < i) && (d < 0 || r < d)) {
|
|
1371
|
+
if (r >= 0 && (i < 0 || r < i) && (d < 0 || r < d) && (x < 0 || r < x)) {
|
|
1355
1372
|
const o = objectEnd(this.buf, r)
|
|
1356
1373
|
if (o === -1 && !end) {
|
|
1357
1374
|
// not whole yet: held, the text before it sent
|
|
@@ -1371,7 +1388,39 @@ class TextTools {
|
|
|
1371
1388
|
this.buf = this.buf.slice(r + 1)
|
|
1372
1389
|
continue
|
|
1373
1390
|
}
|
|
1374
|
-
if (i < 0 && d < 0) break
|
|
1391
|
+
if (i < 0 && d < 0 && x < 0) break
|
|
1392
|
+
if (x >= 0 && (i < 0 || x < i) && (d < 0 || x < d)) {
|
|
1393
|
+
// Anthropic's <function_calls><invoke name="…"> or Qwen's
|
|
1394
|
+
// <function=…>, as Kimi and MiMo write a call (01huadalang on
|
|
1395
|
+
// magpie's Discord, dsh): a call only for a tool the request
|
|
1396
|
+
// declared, and never in a code fence or backticks
|
|
1397
|
+
const head = this.buf.slice(0, x)
|
|
1398
|
+
const at = this.buf.slice(x)
|
|
1399
|
+
const opener = at.match(/^<[^<>]*>?/)[0]
|
|
1400
|
+
if (quoted(this.sent + text + head)) {
|
|
1401
|
+
text += head + opener
|
|
1402
|
+
this.buf = at.slice(opener.length)
|
|
1403
|
+
continue
|
|
1404
|
+
}
|
|
1405
|
+
const b = this.xmlCall(at, end)
|
|
1406
|
+
if (b.wait) break
|
|
1407
|
+
if (b.text) {
|
|
1408
|
+
text += head + opener
|
|
1409
|
+
this.buf = at.slice(opener.length)
|
|
1410
|
+
continue
|
|
1411
|
+
}
|
|
1412
|
+
text += head
|
|
1413
|
+
if (b.error) {
|
|
1414
|
+
this.buf = ""
|
|
1415
|
+
this.sent += text
|
|
1416
|
+
return { text, calls, reasoning, error: b.error }
|
|
1417
|
+
}
|
|
1418
|
+
calls.push(b.call)
|
|
1419
|
+
this.mark = this.sent.length + text.length
|
|
1420
|
+
this.buf = b.rest
|
|
1421
|
+
this.after = true
|
|
1422
|
+
continue
|
|
1423
|
+
}
|
|
1375
1424
|
if (d >= 0 && (i < 0 || d < i)) {
|
|
1376
1425
|
text += this.buf.slice(0, d)
|
|
1377
1426
|
this.buf = this.buf.slice(d)
|
|
@@ -1462,7 +1511,7 @@ class TextTools {
|
|
|
1462
1511
|
this.buf = ""
|
|
1463
1512
|
} else {
|
|
1464
1513
|
// keep back an opened block, or what may be the start of one
|
|
1465
|
-
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)
|
|
1514
|
+
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)
|
|
1466
1515
|
let keep = at.length ? this.buf.length - Math.min(...at) : 0
|
|
1467
1516
|
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 }
|
|
1468
1517
|
// and braces, brackets, quotes or a > a close may still come after
|
|
@@ -1475,6 +1524,60 @@ class TextTools {
|
|
|
1475
1524
|
return { text, calls, reasoning }
|
|
1476
1525
|
}
|
|
1477
1526
|
|
|
1527
|
+
// xmlCall reads a call written in Anthropic's tags (<function_calls>
|
|
1528
|
+
// around one or more <invoke name="…"><parameter name="k">v</parameter>
|
|
1529
|
+
// </invoke>) or Qwen's (<function=…><parameter=k>v</parameter>
|
|
1530
|
+
// </function>), s starting at its first tag: {call, rest}, {wait}
|
|
1531
|
+
// while it may still become one, {text} when it is no call (a tool the
|
|
1532
|
+
// request didn't declare, a tag named in prose), {error} for one cut
|
|
1533
|
+
// short. Each invoke is a call of its own; the tags between them are
|
|
1534
|
+
// dropped as the leftovers of one.
|
|
1535
|
+
xmlCall(s, end) {
|
|
1536
|
+
const w = s.match(/^<(?:antml:)?function_calls>/)
|
|
1537
|
+
if (w) {
|
|
1538
|
+
const rest = s.slice(w[0].length).replace(/^\s+/, "")
|
|
1539
|
+
if (!end && (!rest || XML_STARTS.some((k) => k.startsWith(rest)))) return { wait: true }
|
|
1540
|
+
return this.xmlInvoke(rest, end)
|
|
1541
|
+
}
|
|
1542
|
+
return this.xmlInvoke(s, end)
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
xmlInvoke(s, end) {
|
|
1546
|
+
let m = s.match(/^<(?:antml:)?invoke\s+name\s*=\s*"([^"<>]+)"\s*>/)
|
|
1547
|
+
let closeRe = /<\/(?:antml:)?invoke\s*>/
|
|
1548
|
+
if (!m) {
|
|
1549
|
+
m = s.match(/^<function=([^\s<>]+?)\s*>/)
|
|
1550
|
+
closeRe = /<\/function\s*>/
|
|
1551
|
+
}
|
|
1552
|
+
if (!m) {
|
|
1553
|
+
// an opener still being written
|
|
1554
|
+
if (!end && (XML_STARTS.some((k) => k.startsWith(s)) || /^<(?:antml:)?(?:invoke\s|function=)[^<>]{0,300}$/.test(s))) return { wait: true }
|
|
1555
|
+
return { text: true }
|
|
1556
|
+
}
|
|
1557
|
+
const declared = this.tools.map((t) => (t.function ?? t)?.name).filter(Boolean)
|
|
1558
|
+
const name = toolNamed(m[1].trim(), this.tools)
|
|
1559
|
+
if (!declared.includes(name)) return { text: true }
|
|
1560
|
+
const body = s.slice(m[0].length)
|
|
1561
|
+
const c = body.match(closeRe)
|
|
1562
|
+
const n = body.search(XML_NEXT)
|
|
1563
|
+
let stop = c ? c.index : -1
|
|
1564
|
+
if (n >= 0 && (stop < 0 || n < stop)) stop = n
|
|
1565
|
+
if (stop < 0) {
|
|
1566
|
+
if (!end) return { wait: true }
|
|
1567
|
+
stop = body.length
|
|
1568
|
+
}
|
|
1569
|
+
const whole = !!c && c.index === stop
|
|
1570
|
+
const region = body.slice(0, stop).replace(/<(\/?)antml:parameter\b/g, "<$1parameter")
|
|
1571
|
+
const p = paramArgs(region, name, this.tools)
|
|
1572
|
+
if (!whole && !p.closed) {
|
|
1573
|
+
// no parameter and no close: a tag named in passing; a parameter
|
|
1574
|
+
// left open: the call cut short (plugins#38)
|
|
1575
|
+
if (!/<parameter\b/.test(region)) return { text: true }
|
|
1576
|
+
return { error: CUT_SHORT }
|
|
1577
|
+
}
|
|
1578
|
+
return { call: { name, arguments: JSON.stringify(p.args) }, rest: whole ? body.slice(stop + c[0].length) : body.slice(stop) }
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1478
1581
|
// block reads what follows a <tool_call> opener: {call, rest} for a
|
|
1479
1582
|
// call, {wait} while it may still become one, {text} when it is no call
|
|
1480
1583
|
// at all, {error} for one that can't be read: cut short, or not JSON
|
|
@@ -1960,11 +2063,18 @@ const DSML_INVOKE = new RegExp(String.raw`^<\s*[||][\s||]*DSML[\s||]*invok
|
|
|
1960
2063
|
const DSML_INVOKE_AT = new RegExp(String.raw`<\s*[||][\s||]*DSML[\s||]*invoke\s+name\s*=`)
|
|
1961
2064
|
const DSML_CLOSE = new RegExp(String.raw`<\s*\/\s*[||][\s||]*DSML[\s||]*invoke\s*>`)
|
|
1962
2065
|
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")
|
|
2066
|
+
// Anthropic's and Qwen's call tags (xmlCall): where one starts, an
|
|
2067
|
+
// opener still being written, the openers a wrapper is followed by, and
|
|
2068
|
+
// the next call's opener
|
|
2069
|
+
const XML_AT = /<(?:antml:)?(?:function_calls>|invoke\s+name\s*=|function=)/
|
|
2070
|
+
const XML_HOLD = /<(?:antml:)?(?:invoke\s[^<>]{0,300}|function=[^<>]{0,300})$/
|
|
2071
|
+
const XML_STARTS = ["<invoke ", "<antml:invoke ", "<function="]
|
|
2072
|
+
const XML_NEXT = /<(?:antml:)?invoke\s+name\s*=|<function=/
|
|
1963
2073
|
// what is left after a call: blanks, our close, DeepSeek's tags but the
|
|
1964
|
-
// next call's
|
|
1965
|
-
const LEFTOVER = new RegExp(String.raw`^(?:\s+|<\/tool_?calls?>|<tool_?calls>|` + DSML + String.raw`(?![\s||]*invoke\b)[^<>]*>)+`)
|
|
2074
|
+
// next call's, the closes of Anthropic's and Qwen's
|
|
2075
|
+
const LEFTOVER = new RegExp(String.raw`^(?:\s+|<\/tool_?calls?>|<tool_?calls>|<\/(?:antml:)?(?:function_calls|invoke|function)>|` + DSML + String.raw`(?![\s||]*invoke\b)[^<>]*>)+`)
|
|
1966
2076
|
// what text may be the start of, so held back
|
|
1967
|
-
const MARKS = [OPEN, CLOSE, "<toolcall>", "</toolcall>", "<|DSML|", "<|DSML|", "</|DSML|", "</|DSML|", '{"reasoning_content"']
|
|
2077
|
+
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>"]
|
|
1968
2078
|
// reasoning a model wrote into its text as JSON
|
|
1969
2079
|
const REASON_AT = /\{\s*"reasoning_content"\s*:/
|
|
1970
2080
|
const MARK_MAX = Math.max(...MARKS.map((m) => m.length))
|
|
@@ -2456,6 +2566,55 @@ const maxModel = (...ms) => {
|
|
|
2456
2566
|
// deepseek-v4.1-flash in Max mode
|
|
2457
2567
|
const MAX = "-max"
|
|
2458
2568
|
|
|
2569
|
+
// ---- reasoning levels --------------------------------------------------------------
|
|
2570
|
+
//
|
|
2571
|
+
// A list entry carries a reasoning_effort_config: the levels its model takes
|
|
2572
|
+
// (light, high, extra_high) and the one it defaults to. The request takes the
|
|
2573
|
+
// same names, as reasoning_effort, so a level an agent picks is sent as Trae
|
|
2574
|
+
// spells it. OpenCode knows the three as low/high/xhigh, so the map goes both
|
|
2575
|
+
// ways.
|
|
2576
|
+
//
|
|
2577
|
+
// An entry without the config takes no level: Trae serves it at its own
|
|
2578
|
+
// default and nothing is sent. The plugin used to send none for every model,
|
|
2579
|
+
// which read as "Trae takes no level" — it takes them, per model, and an
|
|
2580
|
+
// entry that names none is the exception rather than the rule.
|
|
2581
|
+
const LEVELS = { light: "low", high: "high", extra_high: "xhigh" }
|
|
2582
|
+
|
|
2583
|
+
// levelsOf is an entry's levels in OpenCode's names, in Trae's order: [] when
|
|
2584
|
+
// the entry names none, null when it names ones this map can't spell (so a
|
|
2585
|
+
// caller can tell "takes no level" from "takes a level we can't name").
|
|
2586
|
+
const levelsOf = (m) => {
|
|
2587
|
+
const cfg = m?.reasoning_effort_config
|
|
2588
|
+
if (!cfg || typeof cfg !== "object") return []
|
|
2589
|
+
const options = Array.isArray(cfg.options) ? cfg.options : []
|
|
2590
|
+
const known = options.filter((o) => typeof o === "string" && LEVELS[o])
|
|
2591
|
+
return known.length ? known.map((o) => LEVELS[o]) : options.length ? null : []
|
|
2592
|
+
}
|
|
2593
|
+
|
|
2594
|
+
// variantsOf is a model's levels as OpenCode's variants. The value is the
|
|
2595
|
+
// effort OpenCode puts in the request body, so it names the level the way this
|
|
2596
|
+
// plugin's levels do: an agent in OpenCode writes `reasoning_effort` straight
|
|
2597
|
+
// from it, and effortOf below turns that name into Trae's. Naming Trae's
|
|
2598
|
+
// spelling here would make the body say "light", which is not one of the
|
|
2599
|
+
// levels this plugin offers, and the field would then be dropped. {} when the
|
|
2600
|
+
// model takes none.
|
|
2601
|
+
const variantsOf = (levels) => Object.fromEntries((levels ?? []).map((level) => [level, { reasoningEffort: level }]))
|
|
2602
|
+
|
|
2603
|
+
// effortOf is the name to send Trae for an effort the request carries, or
|
|
2604
|
+
// undefined when the model doesn't take that one: a level Trae has no entry for
|
|
2605
|
+
// is rejected, so nothing is sent instead. The effort arrives in either
|
|
2606
|
+
// spelling — this plugin's own (low/high/xhigh) from its variants, or Trae's
|
|
2607
|
+
// (light/extra_high) from a client that read them off the list — and both name
|
|
2608
|
+
// the same level.
|
|
2609
|
+
const effortOf = (levels, effort) => {
|
|
2610
|
+
if (typeof effort !== "string" || !levels) return undefined
|
|
2611
|
+
const mine = Object.values(LEVELS).includes(effort) ? effort
|
|
2612
|
+
: Object.prototype.hasOwnProperty.call(LEVELS, effort) ? LEVELS[effort]
|
|
2613
|
+
: undefined
|
|
2614
|
+
if (mine === undefined || !levels.includes(mine)) return undefined
|
|
2615
|
+
return Object.keys(LEVELS).find((k) => LEVELS[k] === mine)
|
|
2616
|
+
}
|
|
2617
|
+
|
|
2459
2618
|
// ---- usage ------------------------------------------------------------------------
|
|
2460
2619
|
|
|
2461
2620
|
// credits adds up the account's entitlement packs as the IDE shows them:
|
|
@@ -2516,6 +2675,88 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
2516
2675
|
const maxes = new Map()
|
|
2517
2676
|
// whether each model the list last read takes images (its multimodal)
|
|
2518
2677
|
const sight = new Map()
|
|
2678
|
+
// the entry the lists last gave each model, for what only the entry says:
|
|
2679
|
+
// the reasoning levels it takes (reasoning_effort_config)
|
|
2680
|
+
const liveEntry = new Map()
|
|
2681
|
+
const liveEntryOf = (id) => liveEntry.get(String(id)) ?? site.models?.[String(id)]?.entry ?? null
|
|
2682
|
+
// when each account's list was last read, and what that read returned:
|
|
2683
|
+
// {at, p, done, expired}. A read that answered, failed or hung all leave
|
|
2684
|
+
// their stamp, so a list that fails or leaves the model out costs one read
|
|
2685
|
+
// per REREAD_MS rather than one per request. The promise is kept beside the
|
|
2686
|
+
// stamp because a caller that arrives while a read is in flight must ride
|
|
2687
|
+
// THAT read, not start a second and not skip the answer it was about to
|
|
2688
|
+
// learn: agents send parallel requests at a session's start (a title request
|
|
2689
|
+
// beside the first turn, subagents), and a caller that skipped the entry a
|
|
2690
|
+
// read just produced would send a Max out under the agent's own id, which
|
|
2691
|
+
// Trae's list doesn't name.
|
|
2692
|
+
//
|
|
2693
|
+
// The record is the thing the promise resolves to, so a joiner sees the same
|
|
2694
|
+
// outcome the read settled on — including an expiry it hit — rather than the
|
|
2695
|
+
// bare `null` a failed read answers. One account per key; a read is that
|
|
2696
|
+
// account's.
|
|
2697
|
+
const readAt = new Map()
|
|
2698
|
+
// runRead starts the one read behind readList and answers its record. `p`
|
|
2699
|
+
// resolves to that record — not to the models — so a caller that joins a
|
|
2700
|
+
// read in flight sees the same outcome the read settled on, an expiry
|
|
2701
|
+
// included, rather than the bare null a failed read answers.
|
|
2702
|
+
const runRead = (a, who) => {
|
|
2703
|
+
const rec = { at: Date.now(), p: null, done: undefined, expired: null }
|
|
2704
|
+
// the record goes in BEFORE the read starts, so a caller arriving during it
|
|
2705
|
+
// finds `rec.p` and joins; `p` resolves to this same record
|
|
2706
|
+
readAt.set(who, rec)
|
|
2707
|
+
const body = (async () => {
|
|
2708
|
+
try {
|
|
2709
|
+
return await liveModels(a)
|
|
2710
|
+
} catch (e) {
|
|
2711
|
+
// a request's read swallows this (the chat can still answer); a caller
|
|
2712
|
+
// that must hear it reads `expired` off the record. Recorded on the
|
|
2713
|
+
// record, so a joiner hears it too.
|
|
2714
|
+
if (e instanceof Expired) rec.expired = e
|
|
2715
|
+
return null
|
|
2716
|
+
}
|
|
2717
|
+
})()
|
|
2718
|
+
// every rejection is claimed here, at creation: a read nobody awaits (the
|
|
2719
|
+
// host's refresh ending early, a caller that walked away) must not surface
|
|
2720
|
+
// as an unhandled rejection. `p` itself never rejects — the expiry rides on
|
|
2721
|
+
// the record instead.
|
|
2722
|
+
body.catch(() => {})
|
|
2723
|
+
rec.p = body.then((models) => {
|
|
2724
|
+
// only this read settles its own record: a later read may have replaced it
|
|
2725
|
+
if (readAt.get(who) === rec) {
|
|
2726
|
+
rec.at = Date.now()
|
|
2727
|
+
rec.done = models ?? []
|
|
2728
|
+
rec.p = null
|
|
2729
|
+
}
|
|
2730
|
+
return rec
|
|
2731
|
+
})
|
|
2732
|
+
rec.p.catch(() => {})
|
|
2733
|
+
return rec.p
|
|
2734
|
+
}
|
|
2735
|
+
// readList answers the models of the read this caller rode on (its own, or
|
|
2736
|
+
// the one in flight, whose record it waits for and then reads), and whether
|
|
2737
|
+
// that read found the account lapsed. The record is the account's, not the
|
|
2738
|
+
// token's: a re-sign-in is the same account's list, and the host's refresh is
|
|
2739
|
+
// strict, so it reads again rather than serving a cached lapse. A request
|
|
2740
|
+
// path only reaches here for a model it has no entry for, and that read is
|
|
2741
|
+
// the one that fills the entry in.
|
|
2742
|
+
//
|
|
2743
|
+
// The lapse is answered, not thrown: several callers can ride one read, and a
|
|
2744
|
+
// throw out of a shared promise surfaces once as an unhandled rejection for
|
|
2745
|
+
// the callers that did not expect it. Each caller decides — provider.models
|
|
2746
|
+
// raises it, a request path keeps going. `strict` only says whether a cached
|
|
2747
|
+
// lapse is worth reading again for (the host wants the truth; a request does
|
|
2748
|
+
// not pay a round trip to learn it).
|
|
2749
|
+
const readList = async (a, { strict = false } = {}) => {
|
|
2750
|
+
const who = a.uid || a.name || String(site.id)
|
|
2751
|
+
const was = readAt.get(who)
|
|
2752
|
+
let rec
|
|
2753
|
+
if (was === undefined) rec = await runRead(a, who)
|
|
2754
|
+
else if (was.p) rec = await was.p
|
|
2755
|
+
else if (was.done === undefined || Date.now() - was.at >= REREAD_MS) rec = await runRead(a, who)
|
|
2756
|
+
else if (strict && was.expired) rec = await runRead(a, who)
|
|
2757
|
+
else rec = was
|
|
2758
|
+
return { models: rec.done, expired: rec.expired }
|
|
2759
|
+
}
|
|
2519
2760
|
|
|
2520
2761
|
const save = async (auth) => {
|
|
2521
2762
|
try {
|
|
@@ -2786,8 +3027,16 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
2786
3027
|
}
|
|
2787
3028
|
// a function's entry is worth more when it names a __dev model, then
|
|
2788
3029
|
// when it isn't misnamed, then when it carries the name its lists agree
|
|
2789
|
-
// on; the first of the best is taken
|
|
2790
|
-
|
|
3030
|
+
// on; the first of the best is taken. Those three decide first, so the
|
|
3031
|
+
// entry kept is the one Trae's own client asks through.
|
|
3032
|
+
//
|
|
3033
|
+
// Last among equals comes whether the entry names reasoning levels: the
|
|
3034
|
+
// entry kept is the one the request is asked through, and so the one
|
|
3035
|
+
// whose reasoning_effort_config the request is held to (liveEntryOf). An
|
|
3036
|
+
// entry that names none while a sibling names some would leave the model
|
|
3037
|
+
// with no level to pick although Trae serves it with levels — doubling
|
|
3038
|
+
// the three keeps them decisive and lets this only break a tie.
|
|
3039
|
+
const worth = (fn, m) => ((devModel(m) ? 2 : 0) + (misnamed(fn, m) ? 0 : 1) + (nameOf(m) === agreed.get(String(m.config_name)) ? 1 : 0)) * 2 + (levelsOf(m)?.length ? 1 : 0)
|
|
2791
3040
|
const out = new Map()
|
|
2792
3041
|
for (const fn of site.functions) {
|
|
2793
3042
|
for (const m of lists[fn] ?? []) {
|
|
@@ -2799,6 +3048,7 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
2799
3048
|
}
|
|
2800
3049
|
for (const [id, { m, fn }] of out) {
|
|
2801
3050
|
listedBy.set(id, fn)
|
|
3051
|
+
liveEntry.set(id, m)
|
|
2802
3052
|
const sees = seesOf(m)
|
|
2803
3053
|
if (sees === null) sight.delete(id)
|
|
2804
3054
|
else sight.set(id, sees)
|
|
@@ -2822,7 +3072,17 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
2822
3072
|
modelNames.delete(id + MAX)
|
|
2823
3073
|
}
|
|
2824
3074
|
}
|
|
2825
|
-
|
|
3075
|
+
// this account's list has just answered: recorded, so a request that wants
|
|
3076
|
+
// levels for a model the list doesn't name waits REREAD_MS before reading
|
|
3077
|
+
// again, and a request arriving meanwhile joins this read instead of
|
|
3078
|
+
// asking — the catalog read a host makes counts as that read too
|
|
3079
|
+
const models = [...out.values()].map((x) => x.m)
|
|
3080
|
+
const who = a.uid || a.name || String(site.id)
|
|
3081
|
+
const at = readAt.get(who)
|
|
3082
|
+
// a readList run records itself; a read nobody recorded (a caller reaching
|
|
3083
|
+
// liveModels directly) is recorded here, unless a read is in flight
|
|
3084
|
+
if (at === undefined || !at.p) readAt.set(who, { at: Date.now(), p: null, done: models, expired: null })
|
|
3085
|
+
return models
|
|
2826
3086
|
}
|
|
2827
3087
|
|
|
2828
3088
|
// modelOf is a model of the account's list as OpenCode's provider
|
|
@@ -2860,9 +3120,13 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
2860
3120
|
input: sees === null ? inputs : { ...inputs, image: sees },
|
|
2861
3121
|
output: was.capabilities?.output ?? { text: true, image: false, audio: false, video: false, pdf: false },
|
|
2862
3122
|
},
|
|
2863
|
-
//
|
|
2864
|
-
//
|
|
2865
|
-
|
|
3123
|
+
// the levels this entry takes, as OpenCode's variants: Trae's own
|
|
3124
|
+
// reasoning_effort_config, which a model without one doesn't carry (it
|
|
3125
|
+
// is then served at Trae's default, with no level to pick). A config
|
|
3126
|
+
// model with an empty variants says nothing — magpie's host fills one in
|
|
3127
|
+
// for every model it hands over — so only a non-empty one is kept, or a
|
|
3128
|
+
// model in the fallback table would lose the levels its entry names.
|
|
3129
|
+
variants: Object.keys(was.variants ?? {}).length ? was.variants : variantsOf(levelsOf(m)),
|
|
2866
3130
|
}
|
|
2867
3131
|
}
|
|
2868
3132
|
|
|
@@ -2888,11 +3152,19 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
2888
3152
|
},
|
|
2889
3153
|
provider: {
|
|
2890
3154
|
id: site.id,
|
|
2891
|
-
// the account's own list, when Trae answers; else the list above
|
|
3155
|
+
// the account's own list, when Trae answers; else the list above.
|
|
3156
|
+
// A refresh rides readList's one read per REREAD_MS, so a request that
|
|
3157
|
+
// wants levels while the host is refreshing joins that read instead of
|
|
3158
|
+
// asking for the list a second time (and vice versa).
|
|
2892
3159
|
async models(provider, { auth }) {
|
|
2893
3160
|
if (auth?.type !== "oauth" || !auth.access) return provider.models
|
|
2894
3161
|
try {
|
|
2895
|
-
const ms = await
|
|
3162
|
+
const { models: ms, expired } = await readList(await fresh(async () => auth), { strict: true })
|
|
3163
|
+
// a lapse must reach the host here, so the account is marked: a
|
|
3164
|
+
// request path deliberately lets it pass instead. A fresh instance:
|
|
3165
|
+
// the recorded one already served as a rejection inside the read, and
|
|
3166
|
+
// re-throwing that same object carries its unhandled state with it
|
|
3167
|
+
if (expired) throw new Expired(expired.message)
|
|
2896
3168
|
if (!ms.length) return provider.models
|
|
2897
3169
|
const ids = new Set(ms.map((m) => String(m.config_name)))
|
|
2898
3170
|
return Object.fromEntries(ms.flatMap((m) => {
|
|
@@ -2949,8 +3221,12 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
2949
3221
|
let last
|
|
2950
3222
|
let named = modelNames.get(String(req.model))
|
|
2951
3223
|
if (!named && String(req.model).endsWith(MAX)) {
|
|
2952
|
-
// a Max asked before the list was read here: read it
|
|
2953
|
-
|
|
3224
|
+
// a Max asked before the list was read here: read it (or ride
|
|
3225
|
+
// the read already in flight), once per REREAD_MS — a list that
|
|
3226
|
+
// doesn't name this Max is not read for again on every request
|
|
3227
|
+
// (the chat can answer without it), and a Max the read does name
|
|
3228
|
+
// is asked through its base, not under this unlisted id
|
|
3229
|
+
await readList(a)
|
|
2954
3230
|
named = modelNames.get(String(req.model))
|
|
2955
3231
|
if (named) fns.unshift(named.fn)
|
|
2956
3232
|
}
|
|
@@ -2959,6 +3235,24 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
2959
3235
|
// sends them only to a model it was told takes them
|
|
2960
3236
|
const base = max?.base ?? String(req.model)
|
|
2961
3237
|
const sees = sight.get(base) ?? (site.models[base]?.modalities?.input?.includes("image") ?? true)
|
|
3238
|
+
// the levels the base model takes, from the entry its lists gave
|
|
3239
|
+
// it: what the request's effort is sent as, and left out when the
|
|
3240
|
+
// model has no entry for it. An entry naming levels this build
|
|
3241
|
+
// can't spell counts as no level here — levelsOf says null for it
|
|
3242
|
+
// so a caller can tell the two apart, and this one sends nothing
|
|
3243
|
+
// either way.
|
|
3244
|
+
//
|
|
3245
|
+
// The list is read once per REREAD_MS for a model it hasn't named,
|
|
3246
|
+
// and a read already in flight is shared rather than skipped: a
|
|
3247
|
+
// parallel request that skipped it would drop its level for that
|
|
3248
|
+
// turn. An answer that leaves the model out — a list that failed,
|
|
3249
|
+
// or one that simply doesn't name it — is remembered too, instead
|
|
3250
|
+
// of being asked for again on every request that carries an effort.
|
|
3251
|
+
let levels = levelsOf(liveEntryOf(base)) ?? []
|
|
3252
|
+
if (!liveEntry.has(base) && (req.reasoning_effort ?? req.reasoningEffort)) {
|
|
3253
|
+
await readList(a)
|
|
3254
|
+
levels = levelsOf(liveEntryOf(base)) ?? []
|
|
3255
|
+
}
|
|
2962
3256
|
// the answer's own signal: the caller's abort (a cancelled
|
|
2963
3257
|
// request) and the stream's cancel both stop Trae's fetch, so
|
|
2964
3258
|
// no read is left hanging on a connection the agent walked
|
|
@@ -2978,7 +3272,7 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
2978
3272
|
const res = await fetch(apiOf(a, site) + "/api/agent/v3/llm_utils_chat", {
|
|
2979
3273
|
method: "POST",
|
|
2980
3274
|
headers: ideHeaders(a, site, { Accept: "text/event-stream", "X-Request-ID": randomUUID() }),
|
|
2981
|
-
body: JSON.stringify(chatBody(req, fn, named?.fn === fn || max ? named.name : "", named?.fn === fn || max ? named.most : 0, max, session, sees)),
|
|
3275
|
+
body: JSON.stringify(chatBody(req, fn, named?.fn === fn || max ? named.name : "", named?.fn === fn || max ? named.most : 0, max, session, sees, levels)),
|
|
2982
3276
|
signal: ac.signal,
|
|
2983
3277
|
})
|
|
2984
3278
|
if (!res.ok) {
|
|
@@ -3029,4 +3323,4 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
3029
3323
|
export const TraeCNAuthPlugin = makePlugin(SITES["trae-cn"])
|
|
3030
3324
|
export const TraeGlobalAuthPlugin = makePlugin(SITES["trae-global"])
|
|
3031
3325
|
|
|
3032
|
-
export const _internal = { ownClient, clientStoragePaths, clientDevice, clientDecode, thisMachine, traeHost, accountOf, HOSTS: SITES["trae-cn"].hosts, MODELS: SITES["trae-cn"].models, TextTools, NativeCalls, looseJSON, glmCall, nameCall, toolNamed, unwrapped, traeMessages, chatBody, credits, dollarUsageOf, whenOf, newDevice, ugDevice, proofOf, SITES, SESSION, conversationID }
|
|
3326
|
+
export const _internal = { ownClient, clientStoragePaths, clientDevice, clientDecode, thisMachine, traeHost, accountOf, HOSTS: SITES["trae-cn"].hosts, MODELS: SITES["trae-cn"].models, TextTools, NativeCalls, looseJSON, glmCall, nameCall, toolNamed, unwrapped, traeMessages, chatBody, credits, dollarUsageOf, whenOf, newDevice, ugDevice, proofOf, SITES, SESSION, conversationID, LEVELS, levelsOf, variantsOf, effortOf }
|
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.20",
|
|
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",
|