@magpie-community/opencode-trae-auth 0.2.19 → 0.2.21
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 +55 -14
- package/index.mjs +227 -22
- 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
|
|
@@ -202,7 +203,10 @@ The model list is the account's own, from
|
|
|
202
203
|
TRAE SOLO CN asks it (`/api/ide/v1/get_detail_param`, one function at a
|
|
203
204
|
time, when the batch gives none). The lists are put together: SOLO and the
|
|
204
205
|
TRAE agent list models `chat_v3` doesn't (deepseek-v4.1-flash is the TRAE
|
|
205
|
-
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
|
|
206
210
|
`chat_completion`: summary, fast_apply…), configs switched off and the
|
|
207
211
|
custom-model slots. Context is the list's `context_window_tokens.dev`,
|
|
208
212
|
output the `__dev` model's `max_tokens`. A model with a `__max` model and
|
|
@@ -222,14 +226,48 @@ served under — and asks that model, and its Max, through a list carrying
|
|
|
222
226
|
it. Trae renamed its official entries on 2026-10-09, leaving the name its
|
|
223
227
|
lists mostly agree on the only thing that tells a misnamed entry apart.
|
|
224
228
|
|
|
225
|
-
Reasoning: the models reason,
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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).
|
|
233
271
|
|
|
234
272
|
When no
|
|
235
273
|
list can be read, the plugin uses the realm's own fallback list. Trae CN's
|
|
@@ -369,9 +407,12 @@ answered it 9074 even with the client's headers.
|
|
|
369
407
|
in today, and that holds for the day. No device id is ever changed or
|
|
370
408
|
rotated to get round a 9095 or a 9074, and no account is given another
|
|
371
409
|
account's device or its own client's device unless the client is signed
|
|
372
|
-
in as that account. The credit a claim granted is its `credits
|
|
373
|
-
its `extra_credits
|
|
374
|
-
|
|
410
|
+
in as that account. The credit a claim granted is its `credits`, or
|
|
411
|
+
its `extra_credits` when it names only that; Trae CN's claim usually
|
|
412
|
+
names neither (`{"code":0,"message":"success"}`), and then the status
|
|
413
|
+
answer's `credits` is the credit. The two are never added: the status
|
|
414
|
+
carries `extra_credits` beside `credits` (100 and 100 on a real
|
|
415
|
+
account) while the day gives 100.
|
|
375
416
|
|
|
376
417
|
Only `api.trae.cn`'s `/trae/api/` pages are sent this way; any other URL
|
|
377
418
|
that isn't a chat request is still refused (400). Needs 0.1.4 or later.
|
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) {
|
|
@@ -2550,6 +2566,55 @@ const maxModel = (...ms) => {
|
|
|
2550
2566
|
// deepseek-v4.1-flash in Max mode
|
|
2551
2567
|
const MAX = "-max"
|
|
2552
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
|
+
|
|
2553
2618
|
// ---- usage ------------------------------------------------------------------------
|
|
2554
2619
|
|
|
2555
2620
|
// credits adds up the account's entitlement packs as the IDE shows them:
|
|
@@ -2610,6 +2675,88 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
2610
2675
|
const maxes = new Map()
|
|
2611
2676
|
// whether each model the list last read takes images (its multimodal)
|
|
2612
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
|
+
}
|
|
2613
2760
|
|
|
2614
2761
|
const save = async (auth) => {
|
|
2615
2762
|
try {
|
|
@@ -2755,13 +2902,18 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
2755
2902
|
if (String(ce.code) === "9074") return { outcome: "ineligible", message: `${ce.message} — check in in the Trae CN app` }
|
|
2756
2903
|
if (claim.status !== 200 || (ce.code && String(ce.code) !== "0")) return { outcome: "failed", message: ce.message || statusLine(claim.status) }
|
|
2757
2904
|
if (!answer || typeof answer !== "object" || Array.isArray(answer)) return { outcome: "failed", message: `check-in claim: unreadable reply ${claimText.trim().slice(0, 120) || "(empty)"}` }
|
|
2758
|
-
//
|
|
2759
|
-
//
|
|
2760
|
-
//
|
|
2905
|
+
// The credit is the day's credits, never credits plus extra_credits.
|
|
2906
|
+
// Trae CN's real claim answers {"code":0,"message":"success"}, with no
|
|
2907
|
+
// credit at all (plugins#194), so the status's credits stand in. Its
|
|
2908
|
+
// status carries extra_credits beside credits, at the same 100 on a
|
|
2909
|
+
// real account (yetone/magpie#808: credits 100, extra_credits 100),
|
|
2910
|
+
// while the account gets 100: adding the two showed 200 (zhujiu on
|
|
2911
|
+
// magpie's Discord, 10-25). A claim that names its own credit is
|
|
2912
|
+
// taken first; one naming extra_credits alone is that (#260). Either
|
|
2913
|
+
// may be absent, and Number(undefined) is NaN, which reads as none.
|
|
2761
2914
|
const n = (v) => (Number.isFinite(Number(v)) ? Number(v) : 0)
|
|
2762
|
-
const credit = n(answer.credits)
|
|
2763
|
-
|
|
2764
|
-
return { outcome: "claimed", ...(credit > 0 ? { credit } : fallback > 0 ? { credit: fallback } : {}) }
|
|
2915
|
+
const credit = n(answer.credits) || n(answer.extra_credits) || n(body.credits)
|
|
2916
|
+
return { outcome: "claimed", ...(credit > 0 ? { credit } : {}) }
|
|
2765
2917
|
}
|
|
2766
2918
|
|
|
2767
2919
|
const usage = async (getAuth) => {
|
|
@@ -2880,8 +3032,16 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
2880
3032
|
}
|
|
2881
3033
|
// a function's entry is worth more when it names a __dev model, then
|
|
2882
3034
|
// when it isn't misnamed, then when it carries the name its lists agree
|
|
2883
|
-
// on; the first of the best is taken
|
|
2884
|
-
|
|
3035
|
+
// on; the first of the best is taken. Those three decide first, so the
|
|
3036
|
+
// entry kept is the one Trae's own client asks through.
|
|
3037
|
+
//
|
|
3038
|
+
// Last among equals comes whether the entry names reasoning levels: the
|
|
3039
|
+
// entry kept is the one the request is asked through, and so the one
|
|
3040
|
+
// whose reasoning_effort_config the request is held to (liveEntryOf). An
|
|
3041
|
+
// entry that names none while a sibling names some would leave the model
|
|
3042
|
+
// with no level to pick although Trae serves it with levels — doubling
|
|
3043
|
+
// the three keeps them decisive and lets this only break a tie.
|
|
3044
|
+
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)
|
|
2885
3045
|
const out = new Map()
|
|
2886
3046
|
for (const fn of site.functions) {
|
|
2887
3047
|
for (const m of lists[fn] ?? []) {
|
|
@@ -2893,6 +3053,7 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
2893
3053
|
}
|
|
2894
3054
|
for (const [id, { m, fn }] of out) {
|
|
2895
3055
|
listedBy.set(id, fn)
|
|
3056
|
+
liveEntry.set(id, m)
|
|
2896
3057
|
const sees = seesOf(m)
|
|
2897
3058
|
if (sees === null) sight.delete(id)
|
|
2898
3059
|
else sight.set(id, sees)
|
|
@@ -2916,7 +3077,17 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
2916
3077
|
modelNames.delete(id + MAX)
|
|
2917
3078
|
}
|
|
2918
3079
|
}
|
|
2919
|
-
|
|
3080
|
+
// this account's list has just answered: recorded, so a request that wants
|
|
3081
|
+
// levels for a model the list doesn't name waits REREAD_MS before reading
|
|
3082
|
+
// again, and a request arriving meanwhile joins this read instead of
|
|
3083
|
+
// asking — the catalog read a host makes counts as that read too
|
|
3084
|
+
const models = [...out.values()].map((x) => x.m)
|
|
3085
|
+
const who = a.uid || a.name || String(site.id)
|
|
3086
|
+
const at = readAt.get(who)
|
|
3087
|
+
// a readList run records itself; a read nobody recorded (a caller reaching
|
|
3088
|
+
// liveModels directly) is recorded here, unless a read is in flight
|
|
3089
|
+
if (at === undefined || !at.p) readAt.set(who, { at: Date.now(), p: null, done: models, expired: null })
|
|
3090
|
+
return models
|
|
2920
3091
|
}
|
|
2921
3092
|
|
|
2922
3093
|
// modelOf is a model of the account's list as OpenCode's provider
|
|
@@ -2954,9 +3125,13 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
2954
3125
|
input: sees === null ? inputs : { ...inputs, image: sees },
|
|
2955
3126
|
output: was.capabilities?.output ?? { text: true, image: false, audio: false, video: false, pdf: false },
|
|
2956
3127
|
},
|
|
2957
|
-
//
|
|
2958
|
-
//
|
|
2959
|
-
|
|
3128
|
+
// the levels this entry takes, as OpenCode's variants: Trae's own
|
|
3129
|
+
// reasoning_effort_config, which a model without one doesn't carry (it
|
|
3130
|
+
// is then served at Trae's default, with no level to pick). A config
|
|
3131
|
+
// model with an empty variants says nothing — magpie's host fills one in
|
|
3132
|
+
// for every model it hands over — so only a non-empty one is kept, or a
|
|
3133
|
+
// model in the fallback table would lose the levels its entry names.
|
|
3134
|
+
variants: Object.keys(was.variants ?? {}).length ? was.variants : variantsOf(levelsOf(m)),
|
|
2960
3135
|
}
|
|
2961
3136
|
}
|
|
2962
3137
|
|
|
@@ -2982,11 +3157,19 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
2982
3157
|
},
|
|
2983
3158
|
provider: {
|
|
2984
3159
|
id: site.id,
|
|
2985
|
-
// the account's own list, when Trae answers; else the list above
|
|
3160
|
+
// the account's own list, when Trae answers; else the list above.
|
|
3161
|
+
// A refresh rides readList's one read per REREAD_MS, so a request that
|
|
3162
|
+
// wants levels while the host is refreshing joins that read instead of
|
|
3163
|
+
// asking for the list a second time (and vice versa).
|
|
2986
3164
|
async models(provider, { auth }) {
|
|
2987
3165
|
if (auth?.type !== "oauth" || !auth.access) return provider.models
|
|
2988
3166
|
try {
|
|
2989
|
-
const ms = await
|
|
3167
|
+
const { models: ms, expired } = await readList(await fresh(async () => auth), { strict: true })
|
|
3168
|
+
// a lapse must reach the host here, so the account is marked: a
|
|
3169
|
+
// request path deliberately lets it pass instead. A fresh instance:
|
|
3170
|
+
// the recorded one already served as a rejection inside the read, and
|
|
3171
|
+
// re-throwing that same object carries its unhandled state with it
|
|
3172
|
+
if (expired) throw new Expired(expired.message)
|
|
2990
3173
|
if (!ms.length) return provider.models
|
|
2991
3174
|
const ids = new Set(ms.map((m) => String(m.config_name)))
|
|
2992
3175
|
return Object.fromEntries(ms.flatMap((m) => {
|
|
@@ -3043,8 +3226,12 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
3043
3226
|
let last
|
|
3044
3227
|
let named = modelNames.get(String(req.model))
|
|
3045
3228
|
if (!named && String(req.model).endsWith(MAX)) {
|
|
3046
|
-
// a Max asked before the list was read here: read it
|
|
3047
|
-
|
|
3229
|
+
// a Max asked before the list was read here: read it (or ride
|
|
3230
|
+
// the read already in flight), once per REREAD_MS — a list that
|
|
3231
|
+
// doesn't name this Max is not read for again on every request
|
|
3232
|
+
// (the chat can answer without it), and a Max the read does name
|
|
3233
|
+
// is asked through its base, not under this unlisted id
|
|
3234
|
+
await readList(a)
|
|
3048
3235
|
named = modelNames.get(String(req.model))
|
|
3049
3236
|
if (named) fns.unshift(named.fn)
|
|
3050
3237
|
}
|
|
@@ -3053,6 +3240,24 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
3053
3240
|
// sends them only to a model it was told takes them
|
|
3054
3241
|
const base = max?.base ?? String(req.model)
|
|
3055
3242
|
const sees = sight.get(base) ?? (site.models[base]?.modalities?.input?.includes("image") ?? true)
|
|
3243
|
+
// the levels the base model takes, from the entry its lists gave
|
|
3244
|
+
// it: what the request's effort is sent as, and left out when the
|
|
3245
|
+
// model has no entry for it. An entry naming levels this build
|
|
3246
|
+
// can't spell counts as no level here — levelsOf says null for it
|
|
3247
|
+
// so a caller can tell the two apart, and this one sends nothing
|
|
3248
|
+
// either way.
|
|
3249
|
+
//
|
|
3250
|
+
// The list is read once per REREAD_MS for a model it hasn't named,
|
|
3251
|
+
// and a read already in flight is shared rather than skipped: a
|
|
3252
|
+
// parallel request that skipped it would drop its level for that
|
|
3253
|
+
// turn. An answer that leaves the model out — a list that failed,
|
|
3254
|
+
// or one that simply doesn't name it — is remembered too, instead
|
|
3255
|
+
// of being asked for again on every request that carries an effort.
|
|
3256
|
+
let levels = levelsOf(liveEntryOf(base)) ?? []
|
|
3257
|
+
if (!liveEntry.has(base) && (req.reasoning_effort ?? req.reasoningEffort)) {
|
|
3258
|
+
await readList(a)
|
|
3259
|
+
levels = levelsOf(liveEntryOf(base)) ?? []
|
|
3260
|
+
}
|
|
3056
3261
|
// the answer's own signal: the caller's abort (a cancelled
|
|
3057
3262
|
// request) and the stream's cancel both stop Trae's fetch, so
|
|
3058
3263
|
// no read is left hanging on a connection the agent walked
|
|
@@ -3072,7 +3277,7 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
3072
3277
|
const res = await fetch(apiOf(a, site) + "/api/agent/v3/llm_utils_chat", {
|
|
3073
3278
|
method: "POST",
|
|
3074
3279
|
headers: ideHeaders(a, site, { Accept: "text/event-stream", "X-Request-ID": randomUUID() }),
|
|
3075
|
-
body: JSON.stringify(chatBody(req, fn, named?.fn === fn || max ? named.name : "", named?.fn === fn || max ? named.most : 0, max, session, sees)),
|
|
3280
|
+
body: JSON.stringify(chatBody(req, fn, named?.fn === fn || max ? named.name : "", named?.fn === fn || max ? named.most : 0, max, session, sees, levels)),
|
|
3076
3281
|
signal: ac.signal,
|
|
3077
3282
|
})
|
|
3078
3283
|
if (!res.ok) {
|
|
@@ -3123,4 +3328,4 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
3123
3328
|
export const TraeCNAuthPlugin = makePlugin(SITES["trae-cn"])
|
|
3124
3329
|
export const TraeGlobalAuthPlugin = makePlugin(SITES["trae-global"])
|
|
3125
3330
|
|
|
3126
|
-
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 }
|
|
3331
|
+
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.21",
|
|
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",
|