@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.
- package/README.md +30 -3
- package/index.mjs +203 -25
- 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
|
|
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
|
|
294
|
-
|
|
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
|
-
|
|
744
|
-
|
|
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
|
-
//
|
|
800
|
-
|
|
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 =
|
|
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
|
-
|
|
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")
|
|
831
|
-
|
|
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:
|
|
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.
|
|
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",
|