@magpie-community/opencode-trae-auth 0.2.13 → 0.2.15

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 +30 -3
  2. package/index.mjs +203 -25
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -151,7 +151,15 @@ agent's `POST https://trae-api-cn.mchost.guru/api/agent/v3/llm_utils_chat`:
151
151
  request's (`read`); any other goes on as written, so the agent can say
152
152
  the tool doesn't exist.
153
153
  - Earlier calls and their results go back in as text.
154
- - Images aren't sent.
154
+ - Images: Trae's lists mark each model `multimodal` true or false (the
155
+ Doubao-Seed models, Kimi K3 and K2.6, Qwen 3.7 Plus and MiniMax M3 take
156
+ images; GLM-5 and DeepSeek V4 don't). A model marked true is listed
157
+ as one that takes images, and an image sent to it goes in the
158
+ message's content as an OpenAI `image_url` part beside the text, as
159
+ TRAE's own agent sends one (plugins#161). A model marked false gets
160
+ "[an image, which this model can't see]" in its place. A model whose
161
+ list hasn't been read is sent its images, unless the fallback list
162
+ says it takes none.
155
163
 
156
164
  The model list is the account's own, from
157
165
  `/api/ide/v1/batch_get_detail_param`, every function's list in one ask, as
@@ -288,10 +296,29 @@ answered it 9074 even with the client's headers.
288
296
  `checked_in`, `credits`.
289
297
  - **Claim:** `POST api.trae.cn/trae/api/v2/ug/checkin_credits/claim`,
290
298
  the same, only while it is on and today's isn't in.
299
+ - **The device, when Trae CN's own client is here (0.2.14):** the
300
+ account this machine's Trae CN desktop client is signed in as checks in
301
+ on the client's own device, the one Trae's device service registered:
302
+ `x-device-id` is the id in the client's
303
+ `globalStorage/storage.json` key `iCubeAuthInfo://icube-dc:<id>`,
304
+ `x-device-type` and `x-os-version` are this machine's, and no
305
+ `x-device-brand` is sent. The account is matched by the `userId` in the
306
+ client's sign-in (`iCubeAuthInfo://icube.cloudide`, plain JSON or the
307
+ client's fixed codec, as dingminhua/dsh-connect-trae reads it). The file
308
+ is only read: nothing in it is written, and the client's token is never
309
+ used. On one account the client's claim went through while the
310
+ plugin's, on its own device, got 9074 (yetone/magpie#808, #1920); that
311
+ this header is what changes it hasn't been tried on a real account
312
+ yet. Every other account keeps its own device, and a file that can't be
313
+ read or made out, or holds no single device, changes nothing. Files:
314
+ macOS `~/Library/Application Support/Trae CN/…`, Windows
315
+ `%APPDATA%\Trae CN\…` (or `trae-cn`), Linux `~/.config/trae-cn/…`
316
+ (or `Trae CN`; not seen on a real Linux install).
291
317
  - **What comes back:** Trae's answer as it is. A 401 or code 1001 marks
292
318
  the account for a new sign-in. Code 9095 means this device has checked
293
- in today; nothing sends another device id to get round it, nor to get
294
- round a 9074.
319
+ in today. No device id is ever made up, changed or rotated to get round
320
+ a 9095 or a 9074, and no account is given a device that isn't its
321
+ own client's.
295
322
 
296
323
  Only `api.trae.cn`'s `/trae/api/` pages are sent this way; any other URL
297
324
  that isn't a chat request is still refused (400). Needs 0.1.4 or later.
package/index.mjs CHANGED
@@ -9,8 +9,15 @@
9
9
  // The account's fetch also sends Trae CN's own pages on api.trae.cn
10
10
  // (/trae/api/…) as the account, for magpie's daily check-in (每日签到:
11
11
  // /trae/api/v2/ug/checkin_credits/status, then /claim).
12
- import { createHash, generateKeyPairSync, randomBytes, randomUUID, randomInt, sign } from "node:crypto"
12
+ import { createDecipheriv, createHash, generateKeyPairSync, randomBytes, randomUUID, randomInt, sign } from "node:crypto"
13
+ import { readFile } from "node:fs/promises"
13
14
  import { createServer, STATUS_CODES } from "node:http"
15
+ import { homedir, release, version as osVersion } from "node:os"
16
+ import { join } from "node:path"
17
+
18
+ // SEES is a fallback model that takes images, as Trae's lists mark it
19
+ // (multimodal: true)
20
+ const SEES = { attachment: true, modalities: { input: ["text", "image"], output: ["text"] } }
14
21
 
15
22
  const SITES = {
16
23
  // Trae CN (trae.cn): the authorization page sends the browser back to a
@@ -71,8 +78,8 @@ const SITES = {
71
78
  models: {
72
79
  "glm-5.2": { name: "GLM-5.2", limit: { context: 200_000, output: 32_000 } },
73
80
  "glm-5": { name: "GLM-5" },
74
- "kimi-k2.6": { name: "Kimi K2.6", limit: { context: 256_000, output: 0 } },
75
- "qwen-3.7-plus": { name: "Qwen 3.7 Plus" },
81
+ "kimi-k2.6": { name: "Kimi K2.6", limit: { context: 256_000, output: 0 }, ...SEES },
82
+ "qwen-3.7-plus": { name: "Qwen 3.7 Plus", ...SEES },
76
83
  "DeepSeek-V4-Pro": { name: "DeepSeek V4 Pro" },
77
84
  "DeepSeek-V4-Flash": { name: "DeepSeek V4 Flash" },
78
85
  },
@@ -739,9 +746,12 @@ const ugUA = (site) =>
739
746
  // ugHeaders are the headers Trae CN's IDE (3.3.104) sends a growth page
740
747
  // with, and only those: its JWT, then its device (fb: x-device-id,
741
748
  // x-device-brand, x-device-type, x-os-version, x-app-version), not the
742
- // chat's tokens and versions.
743
- function ugHeaders(a, site, extra = {}) {
744
- return {
749
+ // chat's tokens and versions. own is this machine's Trae CN client, when
750
+ // it is signed in as this very account (ownClient): its device id and
751
+ // this machine's OS go instead of the account's, and no brand is named
752
+ // (fb leaves it out when it has no model to name).
753
+ function ugHeaders(a, site, extra = {}, own) {
754
+ const h = {
745
755
  "Content-Type": "application/json",
746
756
  Authorization: `Cloud-IDE-JWT ${a.token}`,
747
757
  "User-Agent": ugUA(site),
@@ -750,8 +760,121 @@ function ugHeaders(a, site, extra = {}) {
750
760
  "x-device-type": "Windows",
751
761
  "x-os-version": "10.0.22631",
752
762
  "x-app-version": site.clientVersion,
753
- ...extra,
754
763
  }
764
+ if (own) {
765
+ h["x-device-id"] = own.deviceId
766
+ h["x-device-type"] = own.type
767
+ h["x-os-version"] = own.os
768
+ delete h["x-device-brand"]
769
+ }
770
+ return { ...h, ...extra }
771
+ }
772
+
773
+ // ---- this machine's own Trae CN client ----------------------------------------------
774
+
775
+ // Trae CN's desktop client keeps its state in a VS Code-style
776
+ // globalStorage/storage.json. The device id Trae's device service gave
777
+ // it is in a key's name, iCubeAuthInfo://icube-dc:<digits> (15 or 16
778
+ // digits seen); its sign-in is the value of iCubeAuthInfo://icube.cloudide,
779
+ // a JSON envelope (userId, token, …) stored as is or obfuscated with the
780
+ // client's fixed AES-128-CBC codec. Seen in real clients' files
781
+ // (yetone/magpie#808: the same id in the client's own did= query;
782
+ // yetone/magpie#1705) and read the same way by dingminhua/dsh-connect-trae
783
+ // (MIT), whose codec this is.
784
+ const DC_KEY = "iCubeAuthInfo://icube-dc:"
785
+ const AUTH_KEY = "iCubeAuthInfo://icube.cloudide"
786
+
787
+ // ownClient.paths are where storage.json may be, in order; tests replace it
788
+ const ownClient = {
789
+ paths: () => clientStoragePaths(process.platform, homedir(), process.env),
790
+ }
791
+
792
+ function clientStoragePaths(platform, home, env) {
793
+ const file = (root, name) => join(root, name, "User", "globalStorage", "storage.json")
794
+ if (platform === "darwin") return [file(join(home, "Library", "Application Support"), "Trae CN")]
795
+ if (platform === "win32") {
796
+ const roots = [env.APPDATA, join(home, "AppData", "Roaming")].filter((r, i, all) => r && all.indexOf(r) === i)
797
+ return roots.flatMap((r) => ["Trae CN", "trae-cn"].map((n) => file(r, n)))
798
+ }
799
+ if (platform === "linux") return ["trae-cn", "Trae CN"].map((n) => file(env.XDG_CONFIG_HOME || join(home, ".config"), n))
800
+ return []
801
+ }
802
+
803
+ // clientDevice is the device id of this machine's Trae CN client when the
804
+ // client is signed in as uid, else undefined. It only reads: nothing of
805
+ // the client's is written, kept or sent but that id, and the client's own
806
+ // token is read past, never used. A file it can't read or make out, or one
807
+ // with no single device, names no device, and the account keeps its own.
808
+ // Never an id made up or changed: an account the client isn't signed in
809
+ // as never gets the client's id.
810
+ async function clientDevice(uid) {
811
+ if (!uid) return undefined
812
+ for (const p of ownClient.paths()) {
813
+ let text
814
+ try {
815
+ text = await readFile(p, "utf8")
816
+ } catch {
817
+ continue // not installed here
818
+ }
819
+ try {
820
+ const doc = JSON.parse(text)
821
+ const ids = Object.keys(doc ?? {}).filter((k) => k.startsWith(DC_KEY)).map((k) => k.slice(DC_KEY.length))
822
+ if (ids.length !== 1 || !/^\d{8,20}$/.test(ids[0])) return undefined
823
+ const who = clientAuth(doc[AUTH_KEY])?.userId
824
+ return who != null && String(who) === String(uid) ? ids[0] : undefined
825
+ } catch {
826
+ return undefined
827
+ }
828
+ }
829
+ return undefined
830
+ }
831
+
832
+ // clientAuth is the client's sign-in envelope, as stored or decoded
833
+ function clientAuth(v) {
834
+ if (typeof v !== "string" || !v.trim()) return undefined
835
+ const t = v.trim()
836
+ return JSON.parse(t.startsWith("{") ? t : clientDecode(t))
837
+ }
838
+
839
+ const xorOf = (a, b) => Buffer.from(a.map((x, i) => x ^ b[i]))
840
+ const CODEC = {
841
+ "746305100000": xorOf(
842
+ [82, 9, 106, 213, 48, 54, 165, 56, 191, 64, 163, 158, 129, 243, 215, 251, 124, 227, 57, 130, 155, 47, 255, 135, 52, 142, 67, 68, 196, 222, 233, 203, 84, 123, 148, 50, 166, 194, 35, 61, 238, 76, 149, 11, 66, 250, 195, 78, 8, 46, 161, 102, 40, 217, 36, 178, 118, 91, 162, 73, 109, 139, 209, 37],
843
+ [31, 221, 168, 51, 136, 7, 199, 49, 177, 18, 16, 89, 39, 128, 236, 95, 96, 81, 127, 169, 25, 181, 74, 13, 45, 229, 122, 159, 147, 201, 156, 239, 160, 224, 59, 77, 174, 42, 245, 176, 200, 235, 187, 60, 131, 83, 153, 97, 23, 43, 4, 126, 186, 119, 214, 38, 225, 105, 20, 99, 85, 33, 12, 125],
844
+ ),
845
+ "123920200203": xorOf(
846
+ [191, 192, 216, 250, 122, 246, 220, 97, 31, 254, 98, 27, 8, 72, 71, 176, 135, 99, 96, 18, 127, 101, 203, 104, 211, 102, 191, 125, 37, 72, 150, 156, 51, 229, 121, 35, 17, 153, 141, 177, 110, 131, 150, 128, 172, 255, 254, 6, 18, 140, 55, 62, 236, 249, 135, 64, 135, 12, 117, 4, 89, 149, 168, 209],
847
+ [246, 204, 26, 232, 232, 70, 129, 109, 223, 146, 169, 242, 23, 241, 105, 145, 50, 196, 165, 42, 254, 120, 3, 54, 244, 207, 209, 85, 53, 6, 138, 106, 175, 148, 31, 204, 186, 186, 165, 182, 87, 142, 49, 10, 39, 110, 26, 154, 86, 56, 173, 125, 18, 64, 198, 225, 99, 99, 83, 82, 191, 134, 76, 170],
848
+ ),
849
+ }
850
+
851
+ // clientDecode undoes the client's codec: a 6-byte header naming the salt,
852
+ // 32 random bytes, then AES-128-CBC under sha512(sha512(random) + salt);
853
+ // the plaintext is led by its own sha512, which is checked
854
+ function clientDecode(b64) {
855
+ const buf = Buffer.from(b64, "base64")
856
+ const salt = CODEC[buf.subarray(0, 6).toString("hex")]
857
+ if (!salt || buf.length <= 102) throw new Error("not the client's codec")
858
+ const key = createHash("sha512").update(Buffer.concat([createHash("sha512").update(buf.subarray(6, 38)).digest(), salt])).digest()
859
+ const d = createDecipheriv("aes-128-cbc", key.subarray(0, 16), key.subarray(16, 32))
860
+ const out = Buffer.concat([d.update(buf.subarray(38)), d.final()])
861
+ const body = out.subarray(64)
862
+ if (out.length < 64 || !createHash("sha512").update(body).digest().equals(out.subarray(0, 64))) throw new Error("the client's value doesn't check")
863
+ return body.toString("utf8")
864
+ }
865
+
866
+ // thisMachine is how the client names this machine's OS: its type, and
867
+ // the OS's own version (Windows' product name, as "Windows 11 Home China"
868
+ // was seen, else its build; elsewhere the kernel's release)
869
+ function thisMachine(platform = process.platform) {
870
+ if (platform === "win32") {
871
+ let v = ""
872
+ try {
873
+ v = osVersion()
874
+ } catch {}
875
+ return { type: "windows", os: /^Windows \d/.test(v) ? v : `Windows ${release()}` }
876
+ }
877
+ return { type: platform === "darwin" ? "mac" : platform, os: `${platform === "darwin" ? "macOS" : platform} ${release()}` }
755
878
  }
756
879
 
757
880
  // ugPage: url is one of Trae's growth pages (the check-in)
@@ -796,15 +919,36 @@ const partText = (content) => {
796
919
  .join("\n")
797
920
  }
798
921
 
799
- // traeMessages is the conversation as Trae's chat takes it
800
- function traeMessages(req) {
922
+ // imageOf is an image part of a message as Trae's chat takes one: OpenAI's
923
+ // image_url part, {url} in it, which is what TRAE's ai-agent sends
924
+ // (LLMRawMultiContent's image_url, LLMRawChatImageURL) and what the
925
+ // open-source relays pass llm_utils_chat unchanged. null for a part that
926
+ // isn't an image, or one with no url.
927
+ function imageOf(p) {
928
+ if (p?.type !== "image_url" && p?.type !== "input_image") return null
929
+ const url = typeof p.image_url === "string" ? p.image_url : p.image_url?.url ?? p.url
930
+ return typeof url === "string" && url ? { type: "image_url", image_url: { url } } : null
931
+ }
932
+
933
+ // partsOf is a message's text and, for a model that sees, its images apart
934
+ // from it; a model that doesn't is told in the text that one was there
935
+ const partsOf = (content, sees) => {
936
+ if (!sees || !Array.isArray(content)) return { text: partText(content), images: [] }
937
+ const images = content.map(imageOf).filter(Boolean)
938
+ return { text: partText(content.filter((p) => !imageOf(p))), images }
939
+ }
940
+
941
+ // traeMessages is the conversation as Trae's chat takes it. sees is whether
942
+ // the model takes images: Trae's list says so of each model (multimodal),
943
+ // and a model it says doesn't is sent the placeholder, not the image.
944
+ function traeMessages(req, sees = false) {
801
945
  const tools = Array.isArray(req.tools) && req.tools.length && req.tool_choice !== "none" ? req.tools : []
802
946
  const names = new Map()
803
947
  const out = []
804
948
  if (tools.length) out.push({ role: "system", text: toolPrompt(tools, req.tool_choice, req.parallel_tool_calls) })
805
949
  for (const m of req.messages ?? []) {
806
950
  const role = m.role === "developer" ? "system" : m.role
807
- let text = partText(m.content)
951
+ let { text, images } = partsOf(m.content, sees)
808
952
  if (role === "assistant") {
809
953
  const calls = (m.tool_calls ?? []).map((c) => {
810
954
  names.set(c.id, c.function?.name)
@@ -817,20 +961,24 @@ function traeMessages(req) {
817
961
  text = [text, ...calls].filter(Boolean).join("\n")
818
962
  out.push({ role: "assistant", text })
819
963
  } else if (role === "tool") {
820
- out.push({ role: "user", text: `Result of ${names.get(m.tool_call_id) || m.name || "the tool"} (call ${m.tool_call_id ?? "?"}):\n${text}` })
964
+ out.push({ role: "user", text: `Result of ${names.get(m.tool_call_id) || m.name || "the tool"} (call ${m.tool_call_id ?? "?"}):\n${text}`, images })
821
965
  } else {
822
- out.push({ role: role === "system" ? "system" : "user", text })
966
+ out.push({ role: role === "system" ? "system" : "user", text, images })
823
967
  }
824
968
  }
825
- // Trae takes no empty message, nor two of one role in a row
969
+ // Trae takes no empty message, nor two of one role in a row; a message of
970
+ // an image alone is not empty
826
971
  const merged = []
827
972
  for (const m of out) {
828
- if (!m.text) continue
973
+ const images = m.images ?? []
974
+ if (!m.text && !images.length) continue
829
975
  const last = merged.at(-1)
830
- if (last && last.role === m.role && m.role !== "system") last.text += "\n\n" + m.text
831
- else merged.push({ ...m })
976
+ if (last && last.role === m.role && m.role !== "system") {
977
+ last.text = [last.text, m.text].filter(Boolean).join("\n\n")
978
+ last.images = [...last.images, ...images]
979
+ } else merged.push({ role: m.role, text: m.text, images })
832
980
  }
833
- return merged.map((m) => ({ role: m.role, content: [{ type: "text", text: m.text }] }))
981
+ return merged.map((m) => ({ role: m.role, content: [...(m.text ? [{ type: "text", text: m.text }] : []), ...m.images] }))
834
982
  }
835
983
 
836
984
  // nativeTools are the tools as Trae names them: parameters a JSON string
@@ -905,10 +1053,10 @@ function sessionOf(headers) {
905
1053
  // tool result sent back, a retry, another function answering) shows up in the
906
1054
  // usage detail as several, each under no title at all. request_id stays a
907
1055
  // fresh id per request, being that request's own.
908
- function chatBody(req, fn, modelName, most = 0, max = null, session = "") {
1056
+ function chatBody(req, fn, modelName, most = 0, max = null, session = "", sees = false) {
909
1057
  const config = max?.base ?? req.model
910
1058
  const body = {
911
- messages: traeMessages(req),
1059
+ messages: traeMessages(req, sees),
912
1060
  function: fn,
913
1061
  config_name: config,
914
1062
  model: config,
@@ -2151,6 +2299,12 @@ function chatModel(m, site) {
2151
2299
  return true
2152
2300
  }
2153
2301
 
2302
+ // seesOf is whether a list's entry says its model takes images: Trae's
2303
+ // lists mark each one multimodal true or false (Doubao-Seed, Kimi K3,
2304
+ // Qwen 3.7 Plus take them; GLM-5 and DeepSeek V4 don't). null when the
2305
+ // entry doesn't say.
2306
+ const seesOf = (m) => (typeof m?.multimodal === "boolean" ? m.multimodal : typeof m?.display_config?.multimodal === "boolean" ? m.display_config.multimodal : null)
2307
+
2154
2308
  // devModel is the model an entry serves requests with: its __dev one
2155
2309
  const devModel = (m) => (Array.isArray(m?.model_detail_list) ? m.model_detail_list : []).find((d) => /__dev$/.test(String(d?.model_name ?? "")))
2156
2310
 
@@ -2233,6 +2387,8 @@ const makePlugin = (site) => async ({ client }) => {
2233
2387
  const modelNames = new Map()
2234
2388
  // the Max each model the list last read has (null: none)
2235
2389
  const maxes = new Map()
2390
+ // whether each model the list last read takes images (its multimodal)
2391
+ const sight = new Map()
2236
2392
 
2237
2393
  const save = async (auth) => {
2238
2394
  try {
@@ -2328,9 +2484,16 @@ const makePlugin = (site) => async ({ client }) => {
2328
2484
  const ug = ugPage(url)
2329
2485
  let sent = method === "GET" || method === "HEAD" ? undefined : typeof body === "string" ? body : "{}"
2330
2486
  if (ug && sent !== undefined && /^\s*(\{\s*\})?\s*$/.test(sent)) sent = JSON.stringify({ req_source: 1 })
2487
+ // the account this machine's own Trae CN client is signed in as checks
2488
+ // in on the client's device, the one Trae registered: on one account
2489
+ // the client's own claim went through while the plugin's, on a device
2490
+ // it had made up, came back 9074 (yetone/magpie#808, #1920). Every
2491
+ // other account keeps its own device; none is given the client's.
2492
+ const deviceId = ug && site.id === "trae-cn" ? await clientDevice(a.uid) : undefined
2493
+ const own = deviceId ? { deviceId, ...thisMachine() } : undefined
2331
2494
  const res = await fetch(url, {
2332
2495
  method,
2333
- headers: ug ? ugHeaders(a, site, { Accept: "application/json" }) : ideHeaders(a, site, { Accept: "application/json" }),
2496
+ headers: ug ? ugHeaders(a, site, { Accept: "application/json" }, own) : ideHeaders(a, site, { Accept: "application/json" }),
2334
2497
  body: sent,
2335
2498
  signal: signal ?? AbortSignal.timeout(20_000),
2336
2499
  })
@@ -2469,6 +2632,9 @@ const makePlugin = (site) => async ({ client }) => {
2469
2632
  }
2470
2633
  for (const [id, { m, fn }] of out) {
2471
2634
  listedBy.set(id, fn)
2635
+ const sees = seesOf(m)
2636
+ if (sees === null) sight.delete(id)
2637
+ else sight.set(id, sees)
2472
2638
  const dev = devModel(m)
2473
2639
  if (dev) modelNames.set(id, { fn, name: dev.model_name, most: Number(dev.max_tokens) || 0 })
2474
2640
  else modelNames.delete(id)
@@ -2507,16 +2673,24 @@ const makePlugin = (site) => async ({ client }) => {
2507
2673
  const out = Number(devModel(m)?.max_tokens) || was.limit?.output || 0
2508
2674
  const name = m.display_config?.display_name || m.display_name || m.display_model_name || was.name || id
2509
2675
  const input = (k) => (MODEL.modalities?.input ?? []).includes(k)
2676
+ // the list's multimodal says whether the model takes images, over the
2677
+ // fallback list's word and MODEL's
2678
+ const sees = seesOf(m)
2679
+ const inputs = was.capabilities?.input ?? { text: true, image: input("image"), audio: input("audio"), video: input("video"), pdf: input("pdf") }
2680
+ const sight = sees === null ? {} : {
2681
+ modalities: { input: [...new Set([...((was.modalities ?? MODEL.modalities).input ?? ["text"]).filter((k) => k !== "image"), ...(sees ? ["image"] : [])])], output: (was.modalities ?? MODEL.modalities).output ?? ["text"] },
2682
+ attachment: sees,
2683
+ }
2510
2684
  return {
2511
- ...MODEL, ...was, id, providerID: site.id, name: String(name), limit: { context: ctx, output: out },
2685
+ ...MODEL, ...was, ...sight, id, providerID: site.id, name: String(name), limit: { context: ctx, output: out },
2512
2686
  api: was.api ?? { id, url: site.hosts.api, npm: "@ai-sdk/openai-compatible" },
2513
2687
  capabilities: {
2514
2688
  ...(was.capabilities ?? {}),
2515
2689
  reasoning: was.capabilities?.reasoning ?? MODEL.reasoning,
2516
2690
  temperature: was.capabilities?.temperature ?? MODEL.temperature,
2517
- attachment: was.capabilities?.attachment ?? MODEL.attachment,
2691
+ attachment: sees ?? was.capabilities?.attachment ?? MODEL.attachment,
2518
2692
  toolcall: was.capabilities?.toolcall ?? MODEL.tool_call,
2519
- input: was.capabilities?.input ?? { text: true, image: input("image"), audio: input("audio"), video: input("video"), pdf: input("pdf") },
2693
+ input: sees === null ? inputs : { ...inputs, image: sees },
2520
2694
  output: was.capabilities?.output ?? { text: true, image: false, audio: false, video: false, pdf: false },
2521
2695
  },
2522
2696
  // Trae's request takes no effort or thinking level, so a model has
@@ -2613,6 +2787,10 @@ const makePlugin = (site) => async ({ client }) => {
2613
2787
  if (named) fns.unshift(named.fn)
2614
2788
  }
2615
2789
  const max = named?.max ?? null
2790
+ // a model the list hasn't said of is sent its images: the agent
2791
+ // sends them only to a model it was told takes them
2792
+ const base = max?.base ?? String(req.model)
2793
+ const sees = sight.get(base) ?? (site.models[base]?.modalities?.input?.includes("image") ?? true)
2616
2794
  // the answer's own signal: the caller's abort (a cancelled
2617
2795
  // request) and the stream's cancel both stop Trae's fetch, so
2618
2796
  // no read is left hanging on a connection the agent walked
@@ -2632,7 +2810,7 @@ const makePlugin = (site) => async ({ client }) => {
2632
2810
  const res = await fetch(apiOf(a, site) + "/api/agent/v3/llm_utils_chat", {
2633
2811
  method: "POST",
2634
2812
  headers: ideHeaders(a, site, { Accept: "text/event-stream", "X-Request-ID": randomUUID() }),
2635
- body: JSON.stringify(chatBody(req, fn, named?.fn === fn || max ? named.name : "", named?.fn === fn || max ? named.most : 0, max, session)),
2813
+ body: JSON.stringify(chatBody(req, fn, named?.fn === fn || max ? named.name : "", named?.fn === fn || max ? named.most : 0, max, session, sees)),
2636
2814
  signal: ac.signal,
2637
2815
  })
2638
2816
  if (!res.ok) {
@@ -2683,4 +2861,4 @@ const makePlugin = (site) => async ({ client }) => {
2683
2861
  export const TraeCNAuthPlugin = makePlugin(SITES["trae-cn"])
2684
2862
  export const TraeGlobalAuthPlugin = makePlugin(SITES["trae-global"])
2685
2863
 
2686
- export const _internal = { 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, proofOf, SITES, SESSION, conversationID }
2864
+ 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, proofOf, SITES, SESSION, conversationID }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magpie-community/opencode-trae-auth",
3
- "version": "0.2.13",
3
+ "version": "0.2.15",
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",