@magpie-community/opencode-trae-auth 0.1.1 → 0.1.3

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 +21 -11
  2. package/index.mjs +103 -23
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -31,14 +31,17 @@ agent's `POST https://trae-api-cn.mchost.guru/api/agent/v3/llm_utils_chat`:
31
31
 
32
32
  - **Request:**
33
33
  - The messages go as `{role, content: [{type: "text", text}]}`.
34
- - The model goes as `config_name` and `model`.
35
- - The function is the one whose model list has the model (`chat_v3`, the
36
- classic IDE's, first; else `solo_work_lite`, SOLO's). If Trae answers
37
- 4001, 4023 or 1005, the plugin tries the other once and remembers which
38
- one worked.
39
- - **Headers:** the IDE's: `Authorization: Cloud-IDE-JWT …`,
40
- `X-Cloudide-Token`, `x-app-id`, `x-ide-version`, the device headers and
41
- `x-uid`.
34
+ - The model goes as `config_name` and `model`, and as `model_name` the
35
+ `__dev` model the function's list names for it, when it names one.
36
+ - The function is the one whose model list has the model, the first that
37
+ names a `__dev` model for it (in the order `chat_v3`, the classic IDE's;
38
+ `solo_work_lite`, SOLO's Work mode; `solo_agent`, the TRAE agent's;
39
+ `solo_agent_lite`). If Trae answers 4001, 4023 or 1005, the plugin tries
40
+ the others once and remembers which one worked.
41
+ - **Headers:** TRAE SOLO CN 0.1.69's (`x-ide-version`/`x-app-version`
42
+ 0.1.69, version code 20260917; Trae offers a model only to clients new
43
+ enough for it): `Authorization: Cloud-IDE-JWT …`, `X-Cloudide-Token`,
44
+ `x-app-id`, the device headers and `x-uid`.
42
45
  - **Answer:** Trae always answers in its own SSE events:
43
46
  - `output` gives `response`/`content` and `reasoning_content`/`reasoning`.
44
47
  - `token_usage`, `done` and `error` mark usage, the end and failures.
@@ -55,9 +58,16 @@ agent's `POST https://trae-api-cn.mchost.guru/api/agent/v3/llm_utils_chat`:
55
58
  - Earlier calls and their results go back in as text.
56
59
  - Images aren't sent.
57
60
 
58
- The model list is the account's own, from `/api/ide/v1/get_detail_param`,
59
- asked for both functions and put together: SOLO lists models `chat_v3`
60
- doesn't (DeepSeek-V4.1-Flash). When neither can be read, the plugin uses the models Trae CN's `chat_v3` is
61
+ The model list is the account's own, from
62
+ `/api/ide/v1/batch_get_detail_param`, every function's list in one ask, as
63
+ TRAE SOLO CN asks it (`/api/ide/v1/get_detail_param`, one function at a
64
+ time, when the batch gives none). The lists are put together: SOLO and the
65
+ TRAE agent list models `chat_v3` doesn't (deepseek-v4.1-flash is the TRAE
66
+ agent's, `solo_agent`). Left out are the IDE's helpers (`usage` other than
67
+ `chat_completion`: summary, fast_apply…), configs switched off and the
68
+ custom-model slots. Context is the list's `context_window_tokens.dev`
69
+ (`max` is Max mode's), output the `__dev` model's `max_tokens`. When no
70
+ list can be read, the plugin uses the models Trae CN's `chat_v3` is
61
71
  known to serve: GLM-5.2, GLM-5, Kimi K2.6, Qwen 3.7 Plus, DeepSeek V4 Pro
62
72
  and DeepSeek V4 Flash.
63
73
 
package/index.mjs CHANGED
@@ -18,16 +18,23 @@ const HOSTS = {
18
18
  }
19
19
  const CLIENT_ID = "ono9krqynydwx5" // Trae CN's IDE
20
20
  const APP_ID = "6eefa01c-1036-4c7e-9ca5-d891f63bfcd8"
21
- const IDE_VERSION = "3.3.65"
21
+ const IDE_VERSION = "3.3.65" // named to the authorization page
22
22
  const IDE_VERSION_CODE = "20260401"
23
+ // the client the model host is told it serves: TRAE SOLO CN 0.1.69, as its
24
+ // ai-agent names itself (same app id). Trae offers a model only to clients
25
+ // new enough for it, and an April IDE was offered no deepseek-v4.1-flash.
26
+ const CLIENT_VERSION = "0.1.69"
27
+ const CLIENT_VERSION_CODE = "20260917"
23
28
  const PLUGIN_VERSION = "2.3.24254"
24
29
  const BRAND = "ASUS TUF Gaming A15 FA507RM_FA507RM"
25
30
  const SIGN_IN_TIMEOUT = 10 * 60 * 1000
26
31
  const EARLY_MS = 2 * 60 * 1000 // a token this close to its end is renewed before a request
27
32
  const LEAD_MS = 10 * 60 * 1000 // and this close, magpie renews it ahead of time (auth.refresh)
28
33
  const DAY = 24 * 3600 * 1000
29
- // the IDE's chat functions: the classic IDE's agent, then SOLO's
30
- const FUNCTIONS = ["chat_v3", "solo_work_lite"]
34
+ // the IDE's chat functions: the classic IDE's agent, then SOLO's Work
35
+ // mode, then the TRAE agent (solo_agent, which has deepseek-v4.1-flash),
36
+ // then SOLO Lite's agent
37
+ const FUNCTIONS = ["chat_v3", "solo_work_lite", "solo_agent", "solo_agent_lite"]
31
38
 
32
39
  const MODEL = { attachment: false, tool_call: true, reasoning: true, temperature: true, limit: { context: 128_000, output: 32_000 }, modalities: { input: ["text"], output: ["text"] } }
33
40
  // what Trae CN's chat_v3 is known to serve; the live list replaces it
@@ -330,8 +337,10 @@ function ideHeaders(a, extra = {}) {
330
337
  "x-device-id": a.deviceId,
331
338
  "x-machine-id": a.machineId,
332
339
  "x-request-id": randomUUID(),
333
- "x-ide-version": IDE_VERSION,
334
- "x-ide-version-code": IDE_VERSION_CODE,
340
+ "x-app-version": CLIENT_VERSION,
341
+ "x-app-version-code": CLIENT_VERSION_CODE,
342
+ "x-ide-version": CLIENT_VERSION,
343
+ "x-ide-version-code": CLIENT_VERSION_CODE,
335
344
  "x-ide-version-type": "stable",
336
345
  "x-device-cpu": "AMD",
337
346
  "x-device-brand": BRAND,
@@ -419,13 +428,16 @@ const nativeTools = (tools) =>
419
428
  return { type: "function", function: { name: f.name, description: f.description ?? "", parameters: typeof f.parameters === "string" ? f.parameters : JSON.stringify(f.parameters ?? {}) } }
420
429
  })
421
430
 
422
- function chatBody(req, fn) {
431
+ // chatBody is the request for llm_utils_chat; modelName is the model the
432
+ // function's list names for the config (its __dev one), when it named one
433
+ function chatBody(req, fn, modelName) {
423
434
  const session = randomUUID()
424
435
  const body = {
425
436
  messages: traeMessages(req),
426
437
  function: fn,
427
438
  config_name: req.model,
428
439
  model: req.model,
440
+ ...(modelName ? { model_name: modelName } : {}),
429
441
  stream: true, // Trae answers in SSE either way
430
442
  request_id: session,
431
443
  session_id: session,
@@ -689,9 +701,27 @@ async function openai(req, it) {
689
701
  return new Response(stream, { status: 200, headers: { "content-type": "text/event-stream", "cache-control": "no-cache" } })
690
702
  }
691
703
 
692
- // a model or function this account's chat_v3 doesn't take: SOLO's may
704
+ // a model or function this account's chat_v3 doesn't take: another may
693
705
  const wrongFunction = (code) => ["4001", "4023", "1005"].includes(String(code))
694
706
 
707
+ // ---- the model lists ------------------------------------------------------------
708
+
709
+ // chatModel: an entry of a function's list that is a model to chat with.
710
+ // The lists also hold the IDE's own helpers (summary, fast_apply, title
711
+ // generation: usage other than chat_completion), configs switched off, and
712
+ // the slots of custom models, which need a provider bound in the IDE.
713
+ function chatModel(m) {
714
+ const id = String(m?.config_name ?? "")
715
+ if (!id || /^custom_model/i.test(id)) return false
716
+ if (m.usage && m.usage !== "chat_completion") return false
717
+ if (m.config_switch === false) return false
718
+ if (m.is_custom_model === true || m.display_config?.is_custom_model === true) return false
719
+ return true
720
+ }
721
+
722
+ // devModel is the model an entry serves requests with: its __dev one
723
+ const devModel = (m) => (Array.isArray(m?.model_detail_list) ? m.model_detail_list : []).find((d) => /__dev$/.test(String(d?.model_name ?? "")))
724
+
695
725
  // ---- usage ------------------------------------------------------------------------
696
726
 
697
727
  // credits adds up the account's entitlement packs as the IDE shows them:
@@ -727,8 +757,10 @@ export const TraeCNAuthPlugin = async ({ client }) => {
727
757
  const renewed = new Map()
728
758
  // the chat function that served each account last
729
759
  const fnOf = new Map()
730
- // the first chat function whose model list has each model
760
+ // the chat function whose model list has each model, and the __dev
761
+ // model it names there
731
762
  const listedBy = new Map()
763
+ const modelNames = new Map()
732
764
 
733
765
  const save = async (auth) => {
734
766
  try {
@@ -816,6 +848,27 @@ export const TraeCNAuthPlugin = async ({ client }) => {
816
848
  return out
817
849
  }
818
850
 
851
+ // batchLists is every chat function's model list in one ask, as TRAE
852
+ // SOLO CN's ai-agent asks (batch_get_detail_param): {function: entries}.
853
+ // null when Trae answers it with no lists.
854
+ const batchLists = async (a) => {
855
+ const r = await post(a, apiOf(a) + "/api/ide/v1/batch_get_detail_param", {
856
+ functions: FUNCTIONS, agent_type: "", current_config_info: { config_name: "", is_custom_model: false },
857
+ mode_type: 0, access_type: 0, ab_force_vids: "", ab_autotest_advanced_mode: 0, show_custom_model: true,
858
+ })
859
+ if (r.status === 401 || lapsedCode(errorOf(r.v).code)) throw new Expired("Trae CN's sign-in has expired; sign in again")
860
+ if (r.status !== 200) throw new Error(`Trae CN models: ${statusLine(r.status)}`)
861
+ const groups = r.v.function_configs ?? r.v.data?.function_configs
862
+ if (!Array.isArray(groups)) return null
863
+ const out = {}
864
+ for (const g of groups) {
865
+ const fn = String(g?.function ?? "")
866
+ if (!FUNCTIONS.includes(fn) || !Array.isArray(g.config_info_list)) continue
867
+ out[fn] = [...(out[fn] ?? []), ...g.config_info_list.filter((m) => m?.config_name)]
868
+ }
869
+ return Object.keys(out).length ? out : null
870
+ }
871
+
819
872
  // listOf is one function's model list, as the IDE asks for it
820
873
  const listOf = async (a, fn) => {
821
874
  const r = await post(a, apiOf(a) + "/api/ide/v1/get_detail_param", {
@@ -828,32 +881,58 @@ export const TraeCNAuthPlugin = async ({ client }) => {
828
881
  }
829
882
 
830
883
  // liveModels is the account's model list: every chat function's, since
831
- // SOLO lists models the classic IDE's chat_v3 doesn't (DeepSeek-V4.1-Flash
832
- // came to SOLO first, yetone/magpie#681); a model listed by one function
884
+ // SOLO lists models the classic IDE's chat_v3 doesn't (deepseek-v4.1-flash
885
+ // came to the TRAE agent's first, yetone/magpie#681); a model listed by one function
833
886
  // only is asked through that one. One function's list failing leaves the
834
887
  // others'; all failing is the error.
888
+ //
889
+ // The lists come from batch_get_detail_param, as TRAE SOLO CN asks them;
890
+ // when that gives none, from get_detail_param, one function at a time.
891
+ // A model several functions list is asked through the first whose entry
892
+ // names a __dev model (one Trae serves), else the first.
835
893
  const liveModels = async (a) => {
836
- const got = await Promise.allSettled(FUNCTIONS.map((fn) => listOf(a, fn)))
837
- const expired = got.find((g) => g.status === "rejected" && g.reason instanceof Expired)
838
- if (expired) throw expired.reason
839
- if (got.every((g) => g.status === "rejected")) throw got[0].reason
894
+ let lists = null
895
+ try {
896
+ lists = await batchLists(a)
897
+ } catch (e) {
898
+ if (e instanceof Expired) throw e
899
+ }
900
+ if (!lists) {
901
+ const got = await Promise.allSettled(FUNCTIONS.map((fn) => listOf(a, fn)))
902
+ const expired = got.find((g) => g.status === "rejected" && g.reason instanceof Expired)
903
+ if (expired) throw expired.reason
904
+ if (got.every((g) => g.status === "rejected")) throw got[0].reason
905
+ lists = {}
906
+ got.forEach((g, i) => {
907
+ if (g.status === "fulfilled") lists[FUNCTIONS[i]] = g.value
908
+ })
909
+ }
840
910
  const out = new Map()
841
- got.forEach((g, i) => {
842
- if (g.status !== "fulfilled") return
843
- for (const m of g.value) {
911
+ for (const fn of FUNCTIONS) {
912
+ for (const m of lists[fn] ?? []) {
913
+ if (!chatModel(m)) continue
844
914
  const id = String(m.config_name)
845
- if (!out.has(id)) out.set(id, { m, fn: FUNCTIONS[i] })
915
+ const was = out.get(id)
916
+ if (!was || (!devModel(was.m) && devModel(m))) out.set(id, { m, fn })
846
917
  }
847
- })
848
- for (const [id, { fn }] of out) listedBy.set(id, fn)
918
+ }
919
+ for (const [id, { m, fn }] of out) {
920
+ listedBy.set(id, fn)
921
+ const dev = devModel(m)
922
+ if (dev) modelNames.set(id, { fn, name: dev.model_name })
923
+ else modelNames.delete(id)
924
+ }
849
925
  return [...out.values()].map((x) => x.m)
850
926
  }
851
927
 
852
928
  const modelOf = (provider, m) => {
853
929
  const id = String(m.config_name)
854
930
  const was = provider.models?.[id] ?? {}
855
- const ctx = Number(m.context_window_size?.max?.[0] ?? m.context_window_size?.max ?? m.context_window_tokens?.max ?? m.prompt_max_tokens) || was.limit?.context || MODEL.limit.context
856
- return { ...MODEL, ...was, id, providerID: ID, name: String(m.display_name || m.display_model_name || was.name || id), limit: { context: ctx, output: was.limit?.output ?? MODEL.limit.output }, api: was.api ?? { id, url: HOSTS.api, npm: "@ai-sdk/openai-compatible" } }
931
+ // context_window_tokens: dev is what a request gets, max only in Max mode
932
+ const ctx = Number(m.context_window_tokens?.dev ?? m.context_window_size?.max?.[0] ?? m.context_window_size?.max ?? m.context_window_tokens?.max ?? m.prompt_max_tokens) || was.limit?.context || MODEL.limit.context
933
+ const out = Number(devModel(m)?.max_tokens) || was.limit?.output || MODEL.limit.output
934
+ const name = m.display_config?.display_name || m.display_name || m.display_model_name || was.name || id
935
+ return { ...MODEL, ...was, id, providerID: ID, name: String(name), limit: { context: ctx, output: out }, api: was.api ?? { id, url: HOSTS.api, npm: "@ai-sdk/openai-compatible" } }
857
936
  }
858
937
 
859
938
  return {
@@ -917,11 +996,12 @@ export const TraeCNAuthPlugin = async ({ client }) => {
917
996
  // served this account last, then the rest
918
997
  const fns = [...new Set([listedBy.get(String(req.model)), fnOf.get(who), ...FUNCTIONS].filter(Boolean))]
919
998
  let last
999
+ const named = modelNames.get(String(req.model))
920
1000
  for (const fn of fns) {
921
1001
  const res = await fetch(apiOf(a) + "/api/agent/v3/llm_utils_chat", {
922
1002
  method: "POST",
923
1003
  headers: ideHeaders(a, { Accept: "text/event-stream", "X-Request-ID": randomUUID() }),
924
- body: JSON.stringify(chatBody(req, fn)),
1004
+ body: JSON.stringify(chatBody(req, fn, named?.fn === fn ? named.name : "")),
925
1005
  signal: init.signal ?? r0?.signal,
926
1006
  })
927
1007
  if (!res.ok) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magpie-community/opencode-trae-auth",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "OpenCode provider plugin for Trae CN (trae.cn): your Trae CN account's models, free tier included, with Trae's own browser sign-in",
5
5
  "type": "module",
6
6
  "main": "./index.mjs",