@magpie-community/opencode-trae-auth 0.2.19 → 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.
Files changed (3) hide show
  1. package/README.md +49 -11
  2. package/index.mjs +216 -16
  3. 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 4001, 4023 or 1005, the plugin tries
90
- the others once and remembers which one worked.
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`). Left out are the IDE's helpers (`usage` other than
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, but Trae's request takes no effort or
226
- thinking level, so the plugin lists no variants and a level an agent
227
- picks does nothing. Every model it lists says so in its `capabilities`
228
- (`reasoning: true`, with `toolcall`, `temperature`, `attachment`, the
229
- input and output modalities and an empty `variants`), the fallback
230
- list's own capabilities kept where it has an entry: magpie reads
231
- `capabilities.reasoning` alone for a model a plugin lists, and a model
232
- without it is shown as one that doesn't reason (0.2.4).
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
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
- functions: ["chat_v3", "solo_work_lite", "solo_agent", "solo_agent_lite"],
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
- function chatBody(req, fn, modelName, most = 0, max = null, session = "", sees = false) {
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 {
@@ -2880,8 +3027,16 @@ const makePlugin = (site) => async ({ client }) => {
2880
3027
  }
2881
3028
  // a function's entry is worth more when it names a __dev model, then
2882
3029
  // when it isn't misnamed, then when it carries the name its lists agree
2883
- // on; the first of the best is taken
2884
- const worth = (fn, m) => (devModel(m) ? 2 : 0) + (misnamed(fn, m) ? 0 : 1) + (nameOf(m) === agreed.get(String(m.config_name)) ? 1 : 0)
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)
2885
3040
  const out = new Map()
2886
3041
  for (const fn of site.functions) {
2887
3042
  for (const m of lists[fn] ?? []) {
@@ -2893,6 +3048,7 @@ const makePlugin = (site) => async ({ client }) => {
2893
3048
  }
2894
3049
  for (const [id, { m, fn }] of out) {
2895
3050
  listedBy.set(id, fn)
3051
+ liveEntry.set(id, m)
2896
3052
  const sees = seesOf(m)
2897
3053
  if (sees === null) sight.delete(id)
2898
3054
  else sight.set(id, sees)
@@ -2916,7 +3072,17 @@ const makePlugin = (site) => async ({ client }) => {
2916
3072
  modelNames.delete(id + MAX)
2917
3073
  }
2918
3074
  }
2919
- return [...out.values()].map((x) => x.m)
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
2920
3086
  }
2921
3087
 
2922
3088
  // modelOf is a model of the account's list as OpenCode's provider
@@ -2954,9 +3120,13 @@ const makePlugin = (site) => async ({ client }) => {
2954
3120
  input: sees === null ? inputs : { ...inputs, image: sees },
2955
3121
  output: was.capabilities?.output ?? { text: true, image: false, audio: false, video: false, pdf: false },
2956
3122
  },
2957
- // Trae's request takes no effort or thinking level, so a model has
2958
- // none to pick from (see the README's Reasoning)
2959
- variants: was.variants ?? {},
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)),
2960
3130
  }
2961
3131
  }
2962
3132
 
@@ -2982,11 +3152,19 @@ const makePlugin = (site) => async ({ client }) => {
2982
3152
  },
2983
3153
  provider: {
2984
3154
  id: site.id,
2985
- // 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).
2986
3159
  async models(provider, { auth }) {
2987
3160
  if (auth?.type !== "oauth" || !auth.access) return provider.models
2988
3161
  try {
2989
- const ms = await liveModels(await fresh(async () => auth))
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)
2990
3168
  if (!ms.length) return provider.models
2991
3169
  const ids = new Set(ms.map((m) => String(m.config_name)))
2992
3170
  return Object.fromEntries(ms.flatMap((m) => {
@@ -3043,8 +3221,12 @@ const makePlugin = (site) => async ({ client }) => {
3043
3221
  let last
3044
3222
  let named = modelNames.get(String(req.model))
3045
3223
  if (!named && String(req.model).endsWith(MAX)) {
3046
- // a Max asked before the list was read here: read it
3047
- await liveModels(a).catch(() => {})
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)
3048
3230
  named = modelNames.get(String(req.model))
3049
3231
  if (named) fns.unshift(named.fn)
3050
3232
  }
@@ -3053,6 +3235,24 @@ const makePlugin = (site) => async ({ client }) => {
3053
3235
  // sends them only to a model it was told takes them
3054
3236
  const base = max?.base ?? String(req.model)
3055
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
+ }
3056
3256
  // the answer's own signal: the caller's abort (a cancelled
3057
3257
  // request) and the stream's cancel both stop Trae's fetch, so
3058
3258
  // no read is left hanging on a connection the agent walked
@@ -3072,7 +3272,7 @@ const makePlugin = (site) => async ({ client }) => {
3072
3272
  const res = await fetch(apiOf(a, site) + "/api/agent/v3/llm_utils_chat", {
3073
3273
  method: "POST",
3074
3274
  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)),
3275
+ body: JSON.stringify(chatBody(req, fn, named?.fn === fn || max ? named.name : "", named?.fn === fn || max ? named.most : 0, max, session, sees, levels)),
3076
3276
  signal: ac.signal,
3077
3277
  })
3078
3278
  if (!res.ok) {
@@ -3123,4 +3323,4 @@ const makePlugin = (site) => async ({ client }) => {
3123
3323
  export const TraeCNAuthPlugin = makePlugin(SITES["trae-cn"])
3124
3324
  export const TraeGlobalAuthPlugin = makePlugin(SITES["trae-global"])
3125
3325
 
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 }
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.19",
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",