@magpie-community/opencode-trae-auth 0.2.14 → 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 +9 -1
- package/index.mjs +70 -18
- 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
|
package/index.mjs
CHANGED
|
@@ -15,6 +15,10 @@ import { createServer, STATUS_CODES } from "node:http"
|
|
|
15
15
|
import { homedir, release, version as osVersion } from "node:os"
|
|
16
16
|
import { join } from "node:path"
|
|
17
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"] } }
|
|
21
|
+
|
|
18
22
|
const SITES = {
|
|
19
23
|
// Trae CN (trae.cn): the authorization page sends the browser back to a
|
|
20
24
|
// callback on 127.0.0.1 with the account's Cloud-IDE-JWT and refresh
|
|
@@ -74,8 +78,8 @@ const SITES = {
|
|
|
74
78
|
models: {
|
|
75
79
|
"glm-5.2": { name: "GLM-5.2", limit: { context: 200_000, output: 32_000 } },
|
|
76
80
|
"glm-5": { name: "GLM-5" },
|
|
77
|
-
"kimi-k2.6": { name: "Kimi K2.6", limit: { context: 256_000, output: 0 } },
|
|
78
|
-
"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 },
|
|
79
83
|
"DeepSeek-V4-Pro": { name: "DeepSeek V4 Pro" },
|
|
80
84
|
"DeepSeek-V4-Flash": { name: "DeepSeek V4 Flash" },
|
|
81
85
|
},
|
|
@@ -915,15 +919,36 @@ const partText = (content) => {
|
|
|
915
919
|
.join("\n")
|
|
916
920
|
}
|
|
917
921
|
|
|
918
|
-
//
|
|
919
|
-
|
|
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) {
|
|
920
945
|
const tools = Array.isArray(req.tools) && req.tools.length && req.tool_choice !== "none" ? req.tools : []
|
|
921
946
|
const names = new Map()
|
|
922
947
|
const out = []
|
|
923
948
|
if (tools.length) out.push({ role: "system", text: toolPrompt(tools, req.tool_choice, req.parallel_tool_calls) })
|
|
924
949
|
for (const m of req.messages ?? []) {
|
|
925
950
|
const role = m.role === "developer" ? "system" : m.role
|
|
926
|
-
let text =
|
|
951
|
+
let { text, images } = partsOf(m.content, sees)
|
|
927
952
|
if (role === "assistant") {
|
|
928
953
|
const calls = (m.tool_calls ?? []).map((c) => {
|
|
929
954
|
names.set(c.id, c.function?.name)
|
|
@@ -936,20 +961,24 @@ function traeMessages(req) {
|
|
|
936
961
|
text = [text, ...calls].filter(Boolean).join("\n")
|
|
937
962
|
out.push({ role: "assistant", text })
|
|
938
963
|
} else if (role === "tool") {
|
|
939
|
-
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 })
|
|
940
965
|
} else {
|
|
941
|
-
out.push({ role: role === "system" ? "system" : "user", text })
|
|
966
|
+
out.push({ role: role === "system" ? "system" : "user", text, images })
|
|
942
967
|
}
|
|
943
968
|
}
|
|
944
|
-
// 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
|
|
945
971
|
const merged = []
|
|
946
972
|
for (const m of out) {
|
|
947
|
-
|
|
973
|
+
const images = m.images ?? []
|
|
974
|
+
if (!m.text && !images.length) continue
|
|
948
975
|
const last = merged.at(-1)
|
|
949
|
-
if (last && last.role === m.role && m.role !== "system")
|
|
950
|
-
|
|
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 })
|
|
951
980
|
}
|
|
952
|
-
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] }))
|
|
953
982
|
}
|
|
954
983
|
|
|
955
984
|
// nativeTools are the tools as Trae names them: parameters a JSON string
|
|
@@ -1024,10 +1053,10 @@ function sessionOf(headers) {
|
|
|
1024
1053
|
// tool result sent back, a retry, another function answering) shows up in the
|
|
1025
1054
|
// usage detail as several, each under no title at all. request_id stays a
|
|
1026
1055
|
// fresh id per request, being that request's own.
|
|
1027
|
-
function chatBody(req, fn, modelName, most = 0, max = null, session = "") {
|
|
1056
|
+
function chatBody(req, fn, modelName, most = 0, max = null, session = "", sees = false) {
|
|
1028
1057
|
const config = max?.base ?? req.model
|
|
1029
1058
|
const body = {
|
|
1030
|
-
messages: traeMessages(req),
|
|
1059
|
+
messages: traeMessages(req, sees),
|
|
1031
1060
|
function: fn,
|
|
1032
1061
|
config_name: config,
|
|
1033
1062
|
model: config,
|
|
@@ -2270,6 +2299,12 @@ function chatModel(m, site) {
|
|
|
2270
2299
|
return true
|
|
2271
2300
|
}
|
|
2272
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
|
+
|
|
2273
2308
|
// devModel is the model an entry serves requests with: its __dev one
|
|
2274
2309
|
const devModel = (m) => (Array.isArray(m?.model_detail_list) ? m.model_detail_list : []).find((d) => /__dev$/.test(String(d?.model_name ?? "")))
|
|
2275
2310
|
|
|
@@ -2352,6 +2387,8 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
2352
2387
|
const modelNames = new Map()
|
|
2353
2388
|
// the Max each model the list last read has (null: none)
|
|
2354
2389
|
const maxes = new Map()
|
|
2390
|
+
// whether each model the list last read takes images (its multimodal)
|
|
2391
|
+
const sight = new Map()
|
|
2355
2392
|
|
|
2356
2393
|
const save = async (auth) => {
|
|
2357
2394
|
try {
|
|
@@ -2595,6 +2632,9 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
2595
2632
|
}
|
|
2596
2633
|
for (const [id, { m, fn }] of out) {
|
|
2597
2634
|
listedBy.set(id, fn)
|
|
2635
|
+
const sees = seesOf(m)
|
|
2636
|
+
if (sees === null) sight.delete(id)
|
|
2637
|
+
else sight.set(id, sees)
|
|
2598
2638
|
const dev = devModel(m)
|
|
2599
2639
|
if (dev) modelNames.set(id, { fn, name: dev.model_name, most: Number(dev.max_tokens) || 0 })
|
|
2600
2640
|
else modelNames.delete(id)
|
|
@@ -2633,16 +2673,24 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
2633
2673
|
const out = Number(devModel(m)?.max_tokens) || was.limit?.output || 0
|
|
2634
2674
|
const name = m.display_config?.display_name || m.display_name || m.display_model_name || was.name || id
|
|
2635
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
|
+
}
|
|
2636
2684
|
return {
|
|
2637
|
-
...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 },
|
|
2638
2686
|
api: was.api ?? { id, url: site.hosts.api, npm: "@ai-sdk/openai-compatible" },
|
|
2639
2687
|
capabilities: {
|
|
2640
2688
|
...(was.capabilities ?? {}),
|
|
2641
2689
|
reasoning: was.capabilities?.reasoning ?? MODEL.reasoning,
|
|
2642
2690
|
temperature: was.capabilities?.temperature ?? MODEL.temperature,
|
|
2643
|
-
attachment: was.capabilities?.attachment ?? MODEL.attachment,
|
|
2691
|
+
attachment: sees ?? was.capabilities?.attachment ?? MODEL.attachment,
|
|
2644
2692
|
toolcall: was.capabilities?.toolcall ?? MODEL.tool_call,
|
|
2645
|
-
input:
|
|
2693
|
+
input: sees === null ? inputs : { ...inputs, image: sees },
|
|
2646
2694
|
output: was.capabilities?.output ?? { text: true, image: false, audio: false, video: false, pdf: false },
|
|
2647
2695
|
},
|
|
2648
2696
|
// Trae's request takes no effort or thinking level, so a model has
|
|
@@ -2739,6 +2787,10 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
2739
2787
|
if (named) fns.unshift(named.fn)
|
|
2740
2788
|
}
|
|
2741
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)
|
|
2742
2794
|
// the answer's own signal: the caller's abort (a cancelled
|
|
2743
2795
|
// request) and the stream's cancel both stop Trae's fetch, so
|
|
2744
2796
|
// no read is left hanging on a connection the agent walked
|
|
@@ -2758,7 +2810,7 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
2758
2810
|
const res = await fetch(apiOf(a, site) + "/api/agent/v3/llm_utils_chat", {
|
|
2759
2811
|
method: "POST",
|
|
2760
2812
|
headers: ideHeaders(a, site, { Accept: "text/event-stream", "X-Request-ID": randomUUID() }),
|
|
2761
|
-
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)),
|
|
2762
2814
|
signal: ac.signal,
|
|
2763
2815
|
})
|
|
2764
2816
|
if (!res.ok) {
|
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",
|