@magpie-community/opencode-trae-auth 0.2.4 → 0.2.6
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 +20 -0
- package/index.mjs +332 -32
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -69,6 +69,18 @@ agent's `POST https://trae-api-cn.mchost.guru/api/agent/v3/llm_utils_chat`:
|
|
|
69
69
|
`solo_work_lite`, SOLO's Work mode; `solo_agent`, the TRAE agent's;
|
|
70
70
|
`solo_agent_lite`). If Trae answers 4001, 4023 or 1005, the plugin tries
|
|
71
71
|
the others once and remembers which one worked.
|
|
72
|
+
- `session_id` is the conversation's, as the IDE keeps one per conversation:
|
|
73
|
+
the session magpie (or OpenCode) names the request by, else the chat's
|
|
74
|
+
first user message, which every later turn repeats, together with the
|
|
75
|
+
account the request is made as. Trae counts a conversation by it, so a
|
|
76
|
+
fresh one per request made one user send that takes several steps (a tool
|
|
77
|
+
result sent back, a retry, another function answering) count as a
|
|
78
|
+
conversation of its own — several rows in the usage detail, each under no
|
|
79
|
+
title. One conversation answered by two accounts (magpie failing over, or
|
|
80
|
+
rotating, inside it) is named twice then, as two conversations, so Trae
|
|
81
|
+
never reads one account's conversation as another's; that costs the two
|
|
82
|
+
halves of such a conversation their single row. `request_id` is the
|
|
83
|
+
request's own, fresh on every one.
|
|
72
84
|
- **Headers:** TRAE SOLO CN 0.1.69's (`x-ide-version`/`x-app-version`
|
|
73
85
|
0.1.69, version code 20260917; Trae offers a model only to clients new
|
|
74
86
|
enough for it): `Authorization: Cloud-IDE-JWT …`, `X-Cloudide-Token`,
|
|
@@ -96,6 +108,14 @@ agent's `POST https://trae-api-cn.mchost.guru/api/agent/v3/llm_utils_chat`:
|
|
|
96
108
|
<arg_value>v</arg_value></tool_call>` (a string kept as written, other
|
|
97
109
|
types read as JSON), and sometimes a whole call into a native call's
|
|
98
110
|
name; both are read as the call.
|
|
111
|
+
- A block is read as models get it wrong too: every quote escaped
|
|
112
|
+
(`{\"name\":…}`), quotes or backslashes in a string left unescaped, an
|
|
113
|
+
opener with the name as an attribute (`<tool_call name="bash">` with
|
|
114
|
+
`<parameter name="k">v</parameter>`), or JSON that is whole when the
|
|
115
|
+
stream ends without `</tool_call>`. A block that still can't be read,
|
|
116
|
+
or is cut short, is never sent on as the answer: the reply ends with an
|
|
117
|
+
error (a 502 without a stream), so the gateway can try again
|
|
118
|
+
(plugins#38). A `<tool_call>` in backticks or a code fence is text.
|
|
99
119
|
- Calls from either source become OpenAI `tool_calls`. A name matching a
|
|
100
120
|
requested tool but for case or punctuation (`Read`) goes on as the
|
|
101
121
|
request's (`read`); any other goes on as written, so the agent can say
|
package/index.mjs
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
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 { generateKeyPairSync, randomBytes, randomUUID, randomInt, sign } from "node:crypto"
|
|
12
|
+
import { createHash, generateKeyPairSync, randomBytes, randomUUID, randomInt, sign } from "node:crypto"
|
|
13
13
|
import { createServer, STATUS_CODES } from "node:http"
|
|
14
14
|
|
|
15
15
|
const SITES = {
|
|
@@ -767,14 +767,72 @@ const nativeTools = (tools) =>
|
|
|
767
767
|
return { type: "function", function: { name: f.name, description: f.description ?? "", parameters: typeof f.parameters === "string" ? f.parameters : JSON.stringify(f.parameters ?? {}) } }
|
|
768
768
|
})
|
|
769
769
|
|
|
770
|
+
// SESSION carries the conversation magpie (or OpenCode) names a request by,
|
|
771
|
+
// from the chat.headers hook to the loader's fetch. It goes no further.
|
|
772
|
+
const SESSION = "x-magpie-trae-session"
|
|
773
|
+
|
|
774
|
+
// SALT is what the id is derived with, so it says nothing of the words it was
|
|
775
|
+
// made from. New in each process, as Trae's own session ids are one session's
|
|
776
|
+
// own within a run.
|
|
777
|
+
const SALT = randomBytes(16).toString("hex")
|
|
778
|
+
|
|
779
|
+
// conversationID is the conversation a chat belongs to, as the UUID the
|
|
780
|
+
// request's session_id wants: the session magpie or OpenCode names (its
|
|
781
|
+
// chat.headers hook), else the chat's first user message, which every later
|
|
782
|
+
// turn of the conversation repeats. "" when neither names one — a body that
|
|
783
|
+
// isn't a chat, or one with no user message in it — and the caller then makes
|
|
784
|
+
// a fresh id per request, as before: a made-up conversation of the request's
|
|
785
|
+
// own is the very splitting this is here to end.
|
|
786
|
+
//
|
|
787
|
+
// who is the account the request is made as (its uid, else its name), and it
|
|
788
|
+
// is part of what the id is made of, so one conversation answered by two
|
|
789
|
+
// accounts (magpie failing over, or rotating, inside it) is two conversations
|
|
790
|
+
// as Trae is concerned — an id a fresh random one never linked, and one that
|
|
791
|
+
// would let Trae read one conversation as another's. The cost is that such a
|
|
792
|
+
// conversation's two halves are counted apart again, which is what it did
|
|
793
|
+
// before this named one at all.
|
|
794
|
+
function conversationID(chat, session, who = "") {
|
|
795
|
+
const account = String(who ?? "").trim()
|
|
796
|
+
if (session) return uuidOf("magpie-trae-session|" + account + "|" + session)
|
|
797
|
+
const first = (Array.isArray(chat?.messages) ? chat.messages : []).find((m) => m?.role === "user")
|
|
798
|
+
const text = partText(first?.content ?? "")
|
|
799
|
+
return text.trim() ? uuidOf("magpie-trae-first|" + account + "|" + text) : ""
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// uuidOf is a version 4 UUID made of a hash's first 16 bytes: the shape
|
|
803
|
+
// randomUUID() returns, since that is what session_id carried before.
|
|
804
|
+
function uuidOf(s) {
|
|
805
|
+
const b = Buffer.from(createHash("sha256").update(SALT + "|" + s).digest().subarray(0, 16))
|
|
806
|
+
b[6] = (b[6] & 0x0f) | 0x40
|
|
807
|
+
b[8] = (b[8] & 0x3f) | 0x80
|
|
808
|
+
const h = b.toString("hex")
|
|
809
|
+
return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20)}`
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// sessionOf takes the session a chat.headers hook named off the headers, with
|
|
813
|
+
// the header: it goes no further than here. Its length is capped as magpie's
|
|
814
|
+
// own session ids are, the id being derived from it anyway.
|
|
815
|
+
function sessionOf(headers) {
|
|
816
|
+
const s = String(headers.get(SESSION) ?? "").trim()
|
|
817
|
+
headers.delete(SESSION)
|
|
818
|
+
return s.slice(0, 128)
|
|
819
|
+
}
|
|
820
|
+
|
|
770
821
|
// chatBody is the request for llm_utils_chat; modelName is the model the
|
|
771
822
|
// function's list names for the config (its __dev one), when it named one,
|
|
772
823
|
// and most the max_tokens it gives that model (0: none given), which a
|
|
773
824
|
// request asking more is held to. A Max model (max: {base, window}) asks
|
|
774
825
|
// the config base by its __max model, max_tokens always set, and gives
|
|
775
826
|
// the prompt the room the window has beside it, as the IDE's Max mode does.
|
|
776
|
-
|
|
777
|
-
|
|
827
|
+
//
|
|
828
|
+
// session is the conversation's id, made by conversationID, so every request
|
|
829
|
+
// of one conversation carries one: Trae counts a conversation by the session
|
|
830
|
+
// the requests name, and a request naming a new session each time is counted
|
|
831
|
+
// as a conversation of its own — one user send that takes several steps (a
|
|
832
|
+
// tool result sent back, a retry, another function answering) shows up in the
|
|
833
|
+
// usage detail as several, each under no title at all. request_id stays a
|
|
834
|
+
// fresh id per request, being that request's own.
|
|
835
|
+
function chatBody(req, fn, modelName, most = 0, max = null, session = "") {
|
|
778
836
|
const config = max?.base ?? req.model
|
|
779
837
|
const body = {
|
|
780
838
|
messages: traeMessages(req),
|
|
@@ -783,8 +841,8 @@ function chatBody(req, fn, modelName, most = 0, max = null) {
|
|
|
783
841
|
model: config,
|
|
784
842
|
...(modelName ? { model_name: modelName } : {}),
|
|
785
843
|
stream: true, // Trae answers in SSE either way
|
|
786
|
-
request_id:
|
|
787
|
-
session_id: session,
|
|
844
|
+
request_id: randomUUID(),
|
|
845
|
+
session_id: session || randomUUID(),
|
|
788
846
|
}
|
|
789
847
|
const asked = req.max_completion_tokens ?? req.max_tokens
|
|
790
848
|
if (Number.isFinite(asked) && asked > 0) body.max_tokens = most > 0 ? Math.min(Math.floor(asked), most) : Math.floor(asked)
|
|
@@ -889,6 +947,8 @@ class TextTools {
|
|
|
889
947
|
this.buf = ""
|
|
890
948
|
this.after = false
|
|
891
949
|
this.tools = tools
|
|
950
|
+
this.sent = "" // the text given out so far, to tell a block in code from a call
|
|
951
|
+
this.bad = new Set() // where the waiting block's JSON was tried and not read, not tried again
|
|
892
952
|
}
|
|
893
953
|
push(s, end = false) {
|
|
894
954
|
this.buf += s
|
|
@@ -907,7 +967,8 @@ class TextTools {
|
|
|
907
967
|
}
|
|
908
968
|
this.after = false
|
|
909
969
|
}
|
|
910
|
-
const
|
|
970
|
+
const om = this.buf.match(OPEN_AT)
|
|
971
|
+
const i = om ? om.index : -1
|
|
911
972
|
const d = this.buf.search(DSML_AT)
|
|
912
973
|
const r = this.buf.search(REASON_AT)
|
|
913
974
|
if (r >= 0 && (i < 0 || r < i) && (d < 0 || r < d)) {
|
|
@@ -965,53 +1026,271 @@ class TextTools {
|
|
|
965
1026
|
this.after = true
|
|
966
1027
|
continue
|
|
967
1028
|
}
|
|
968
|
-
const
|
|
969
|
-
const
|
|
970
|
-
if (
|
|
1029
|
+
const head = this.buf.slice(0, i)
|
|
1030
|
+
const body = this.buf.slice(i + om[0].length)
|
|
1031
|
+
if (quoted(this.sent + text + head)) {
|
|
1032
|
+
// a <tool_call> in backticks or a code fence is prose about one
|
|
1033
|
+
text += head + om[0]
|
|
1034
|
+
this.buf = body
|
|
1035
|
+
continue
|
|
1036
|
+
}
|
|
1037
|
+
const named = om[1]?.match(/\bname\s*=\s*"([^"]+)"/)?.[1]
|
|
1038
|
+
const inner = body.trimStart()
|
|
1039
|
+
if (!named && (inner.startsWith("<") || (end && !inner))) {
|
|
971
1040
|
// no JSON in it: DeepSeek's own invoke, or only the closes of one
|
|
972
1041
|
// (magpie#823 again); the opener is dropped and what follows read
|
|
973
1042
|
// as it is
|
|
974
|
-
text +=
|
|
1043
|
+
text += head
|
|
975
1044
|
this.buf = inner
|
|
976
1045
|
this.after = true
|
|
977
1046
|
continue
|
|
978
1047
|
}
|
|
979
|
-
const
|
|
980
|
-
if (
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
1048
|
+
const b = this.block(body, named, end)
|
|
1049
|
+
if (b.wait) break
|
|
1050
|
+
this.bad.clear()
|
|
1051
|
+
if (b.text) {
|
|
1052
|
+
// not a call: the opener is text, and what follows read on
|
|
1053
|
+
text += head + om[0]
|
|
1054
|
+
this.buf = body
|
|
985
1055
|
continue
|
|
986
1056
|
}
|
|
987
|
-
|
|
988
|
-
if (
|
|
989
|
-
//
|
|
990
|
-
|
|
991
|
-
|
|
1057
|
+
text += head
|
|
1058
|
+
if (b.error) {
|
|
1059
|
+
// a call that can't be read is not the answer (plugins#38): the
|
|
1060
|
+
// turn ends as a failure, so the gateway can try it again
|
|
1061
|
+
this.buf = ""
|
|
1062
|
+
this.sent += text
|
|
1063
|
+
return { text, calls, reasoning, error: b.error }
|
|
992
1064
|
}
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
this.
|
|
996
|
-
const w = looseJSON(raw) ?? glmCall(raw, this.tools) ?? {}
|
|
997
|
-
if (w.name) {
|
|
998
|
-
calls.push({ name: String(w.name), arguments: callArgs(w) })
|
|
999
|
-
this.after = true
|
|
1000
|
-
} else text += OPEN + raw + CLOSE
|
|
1065
|
+
calls.push(b.call)
|
|
1066
|
+
this.buf = b.rest
|
|
1067
|
+
this.after = true
|
|
1001
1068
|
}
|
|
1002
1069
|
if (end) {
|
|
1003
1070
|
text += this.buf
|
|
1004
1071
|
this.buf = ""
|
|
1005
1072
|
} else {
|
|
1006
1073
|
// keep back an opened block, or what may be the start of one
|
|
1007
|
-
const at = [this.buf.
|
|
1074
|
+
const at = [this.buf.search(OPEN_AT), this.buf.search(OPEN_HOLD), this.buf.search(DSML_AT), this.buf.search(REASON_AT)].filter((x) => x >= 0)
|
|
1008
1075
|
let keep = at.length ? this.buf.length - Math.min(...at) : 0
|
|
1009
1076
|
if (!keep) for (let k = Math.min(MARK_MAX - 1, this.buf.length); k > 0; k--) if (MARKS.some((m) => m.startsWith(this.buf.slice(-k)))) { keep = k; break }
|
|
1010
1077
|
text += this.buf.slice(0, this.buf.length - keep)
|
|
1011
1078
|
this.buf = this.buf.slice(this.buf.length - keep)
|
|
1012
1079
|
}
|
|
1080
|
+
this.sent += text
|
|
1013
1081
|
return { text, calls, reasoning }
|
|
1014
1082
|
}
|
|
1083
|
+
|
|
1084
|
+
// block reads what follows a <tool_call> opener: {call, rest} for a
|
|
1085
|
+
// call, {wait} while it may still become one, {text} when it is no call
|
|
1086
|
+
// at all, {error} for one that can't be read: cut short, or not JSON
|
|
1087
|
+
// even as models get it wrong (plugins#38). named is the name an opener
|
|
1088
|
+
// gave as <tool_call name="…">.
|
|
1089
|
+
block(body, named, end) {
|
|
1090
|
+
if (!end && !body.trim()) return { wait: true }
|
|
1091
|
+
const close = body.indexOf(CLOSE)
|
|
1092
|
+
if (named) {
|
|
1093
|
+
if (close < 0 && !end) return { wait: true }
|
|
1094
|
+
const region = close >= 0 ? body.slice(0, close) : body
|
|
1095
|
+
const rest = close >= 0 ? body.slice(close + CLOSE.length) : ""
|
|
1096
|
+
if (region.trimStart().startsWith("{")) {
|
|
1097
|
+
const v = repairedJSON(region.trim())
|
|
1098
|
+
if (!v) return { error: close >= 0 ? UNREADABLE : CUT_SHORT }
|
|
1099
|
+
return { call: v.name ? { name: String(v.name), arguments: callArgs(v) } : { name: named, arguments: JSON.stringify(v) }, rest }
|
|
1100
|
+
}
|
|
1101
|
+
if (region.includes("<arg_key>")) {
|
|
1102
|
+
const w = glmCall(named + region, this.tools)
|
|
1103
|
+
return w ? { call: { name: w.name, arguments: callArgs(w) }, rest } : { error: close >= 0 ? UNREADABLE : CUT_SHORT }
|
|
1104
|
+
}
|
|
1105
|
+
const p = paramArgs(region, named, this.tools)
|
|
1106
|
+
// cut short at the end: whole only when each parameter was closed
|
|
1107
|
+
if (close < 0 && !p.closed) return { error: CUT_SHORT }
|
|
1108
|
+
return { call: { name: named, arguments: JSON.stringify(p.args) }, rest }
|
|
1109
|
+
}
|
|
1110
|
+
if (body.trimStart().startsWith("{")) {
|
|
1111
|
+
const ph = body.match(PARAM_HEAD)
|
|
1112
|
+
if (ph) {
|
|
1113
|
+
// DeepSeek's half-and-half: the name as JSON, then its parameters
|
|
1114
|
+
// (magpie#917)
|
|
1115
|
+
if (close < 0 && !end) return { wait: true }
|
|
1116
|
+
const region = body.slice(ph[0].length, close >= 0 ? close : body.length)
|
|
1117
|
+
const p = paramArgs(region, ph[1], this.tools)
|
|
1118
|
+
if (close < 0 && !p.closed) return { error: CUT_SHORT }
|
|
1119
|
+
return { call: { name: ph[1], arguments: JSON.stringify(p.args) }, rest: close >= 0 ? body.slice(close + CLOSE.length) : "" }
|
|
1120
|
+
}
|
|
1121
|
+
const o = objectEnd(body, 0)
|
|
1122
|
+
if (o > 0 && !this.bad.has(":" + o)) {
|
|
1123
|
+
const v = callJSON(body.slice(0, o))
|
|
1124
|
+
// a name alone may have its parameters still to come
|
|
1125
|
+
if (v && !end && Object.keys(v).length === 1 && "<parameter".startsWith(body.slice(o).replace(/^\s*>?\s*/, ""))) return { wait: true }
|
|
1126
|
+
if (v) return { call: { name: String(v.name), arguments: callArgs(v) }, rest: body.slice(o) }
|
|
1127
|
+
this.bad.add(":" + o)
|
|
1128
|
+
}
|
|
1129
|
+
// not whole, or not JSON as it stands: read up to a close (a file's
|
|
1130
|
+
// content may hold one), or at the end all of it
|
|
1131
|
+
for (let j = close; j >= 0; j = body.indexOf(CLOSE, j + 1)) {
|
|
1132
|
+
if (this.bad.has("<" + j)) continue
|
|
1133
|
+
const v = callJSON(body.slice(0, j))
|
|
1134
|
+
if (v) return { call: { name: String(v.name), arguments: callArgs(v) }, rest: body.slice(j + CLOSE.length) }
|
|
1135
|
+
this.bad.add("<" + j)
|
|
1136
|
+
}
|
|
1137
|
+
if (!end) return { wait: true }
|
|
1138
|
+
const v = callJSON(body)
|
|
1139
|
+
if (v) return { call: { name: String(v.name), arguments: callArgs(v) }, rest: "" }
|
|
1140
|
+
return { error: close >= 0 ? UNREADABLE : CUT_SHORT }
|
|
1141
|
+
}
|
|
1142
|
+
// GLM's own (a name, then <arg_key>/<arg_value> pairs), or text
|
|
1143
|
+
if (!/^\s*[\w.:-]{1,128}\s*(?:<|$)/.test(body)) return { text: true }
|
|
1144
|
+
if (close < 0 && !end) return { wait: true }
|
|
1145
|
+
const region = close >= 0 ? body.slice(0, close) : body
|
|
1146
|
+
const w = glmCall(region, this.tools)
|
|
1147
|
+
// GLM's call, cut short at the end, is still the call
|
|
1148
|
+
if (w) return { call: { name: w.name, arguments: callArgs(w) }, rest: close >= 0 ? body.slice(close + CLOSE.length) : "" }
|
|
1149
|
+
if (/^\s*[\w.:-]{1,128}\s*<arg_key>/.test(body)) return { error: close >= 0 ? UNREADABLE : CUT_SHORT }
|
|
1150
|
+
return { text: true }
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
const CUT_SHORT = "the model's tool call, written as text, was cut short; it is not sent on as the answer"
|
|
1155
|
+
const UNREADABLE = "the model's tool call, written as text, can't be read; it is not sent on as the answer"
|
|
1156
|
+
|
|
1157
|
+
// an opener: <tool_call>, one with attributes (<tool_call name="bash">),
|
|
1158
|
+
// or <tool_call right before its JSON; and one still being written
|
|
1159
|
+
const OPEN_AT = /<tool_call(?:(\s[^<>{}]*)?>|(?=\s*\{))/
|
|
1160
|
+
const OPEN_HOLD = /<tool_call(?:\s[^<>{}\n]{0,200})?$/
|
|
1161
|
+
// DeepSeek's half-and-half call: {"name":"x"} (or its brace or > left
|
|
1162
|
+
// out), then <parameter name="k">v</parameter> or Qwen's <parameter=k>
|
|
1163
|
+
const PARAM_HEAD = /^\s*\{\s*"name"\s*:\s*"([^"]+)"\s*\}?\s*>?\s*(?=<parameter\b)/
|
|
1164
|
+
const PARAM_OPEN = /<parameter(?:\s+name\s*=\s*"([^"]+)"|=([^\s>]+))\s*>/g
|
|
1165
|
+
|
|
1166
|
+
// quoted says text ends inside a code fence or inline code: what comes
|
|
1167
|
+
// next is shown, not done
|
|
1168
|
+
function quoted(text) {
|
|
1169
|
+
if ((text.match(/```/g)?.length ?? 0) % 2) return true
|
|
1170
|
+
const line = text.slice(text.lastIndexOf("\n") + 1).replace(/```/g, "")
|
|
1171
|
+
return (line.match(/`/g)?.length ?? 0) % 2 === 1
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
// paramArgs reads <parameter name="k">v</parameter> (or <parameter=k>)
|
|
1175
|
+
// arguments; a value runs to its close, the next parameter or the end. A
|
|
1176
|
+
// value is its text where the tool's schema says string (or it isn't
|
|
1177
|
+
// JSON), else its JSON. closed says each one had its close.
|
|
1178
|
+
function paramArgs(s, name, tools = []) {
|
|
1179
|
+
const props = (tools.find((t) => (t.function ?? t)?.name === name)?.function ?? {}).parameters?.properties ?? {}
|
|
1180
|
+
const opens = [...s.matchAll(PARAM_OPEN)]
|
|
1181
|
+
const args = {}
|
|
1182
|
+
let closed = opens.length > 0
|
|
1183
|
+
opens.forEach((m, k) => {
|
|
1184
|
+
const from = m.index + m[0].length
|
|
1185
|
+
const to = k + 1 < opens.length ? opens[k + 1].index : s.length
|
|
1186
|
+
let v = s.slice(from, to)
|
|
1187
|
+
const c = v.indexOf("</parameter>")
|
|
1188
|
+
if (c >= 0) v = v.slice(0, c)
|
|
1189
|
+
else closed = false
|
|
1190
|
+
v = v.replace(/^\r?\n|\r?\n$/g, "")
|
|
1191
|
+
const key = m[1] ?? m[2]
|
|
1192
|
+
if (props[key]?.type === "string") {
|
|
1193
|
+
args[key] = v
|
|
1194
|
+
return
|
|
1195
|
+
}
|
|
1196
|
+
try {
|
|
1197
|
+
args[key] = JSON.parse(v.trim())
|
|
1198
|
+
} catch {
|
|
1199
|
+
args[key] = v
|
|
1200
|
+
}
|
|
1201
|
+
})
|
|
1202
|
+
return { args, closed }
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
// callJSON is a block's call ({name, …}) as a model may get its JSON
|
|
1206
|
+
// wrong, null when it isn't one even so
|
|
1207
|
+
function callJSON(s) {
|
|
1208
|
+
const v = repairedJSON(s)
|
|
1209
|
+
return v && typeof v.name === "string" && v.name ? v : null
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
// repairedJSON reads a JSON object as looseJSON does, and else as models
|
|
1213
|
+
// get it wrong (plugins#38): every quote escaped ({\"name\":…}), a
|
|
1214
|
+
// backslash that escapes nothing JSON knows (\_ \| C:\Users in a file's
|
|
1215
|
+
// content), or quotes inside a string left unescaped ("官方" in a write's
|
|
1216
|
+
// content)
|
|
1217
|
+
function repairedJSON(s) {
|
|
1218
|
+
const v = looseJSON(s)
|
|
1219
|
+
if (v) return v
|
|
1220
|
+
const t = escapeRaw(s)
|
|
1221
|
+
const tries = []
|
|
1222
|
+
if (/^\s*\{\s*\\"/.test(s)) {
|
|
1223
|
+
try {
|
|
1224
|
+
const u = JSON.parse('"' + s.trim().replace(/[\n\r\t]/g, (c) => (c === "\n" ? "\\n" : c === "\r" ? "\\r" : "\\t")) + '"')
|
|
1225
|
+
tries.push(u, escapeRaw(u))
|
|
1226
|
+
} catch {}
|
|
1227
|
+
tries.push(s.replace(/\\"/g, '"'))
|
|
1228
|
+
}
|
|
1229
|
+
tries.push(fixEscapes(t), quoteFix(fixEscapes(t)))
|
|
1230
|
+
for (const x of tries) {
|
|
1231
|
+
try {
|
|
1232
|
+
const v = JSON.parse(x)
|
|
1233
|
+
if (v && typeof v === "object" && !Array.isArray(v)) return v
|
|
1234
|
+
} catch {}
|
|
1235
|
+
}
|
|
1236
|
+
return null
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
// fixEscapes makes a backslash that escapes nothing JSON knows a backslash
|
|
1240
|
+
// of its own (\' is the quote it meant)
|
|
1241
|
+
function fixEscapes(s) {
|
|
1242
|
+
return s.replace(/\\(u[0-9a-fA-F]{4}|["\\/bfnrt])|\\'|\\/g, (m, ok) => (ok ? m : m === "\\'" ? "'" : "\\\\"))
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
// quoteFix escapes the quotes inside strings a model left unescaped: a
|
|
1246
|
+
// quote ends a key only before its colon, and a value only before what
|
|
1247
|
+
// may follow the last of one (closing braces or brackets, then the end or
|
|
1248
|
+
// a comma and the next key or value)
|
|
1249
|
+
function quoteFix(s) {
|
|
1250
|
+
let out = ""
|
|
1251
|
+
const stack = []
|
|
1252
|
+
let key = false
|
|
1253
|
+
for (let i = 0; i < s.length; ) {
|
|
1254
|
+
const ch = s[i]
|
|
1255
|
+
if (ch !== '"') {
|
|
1256
|
+
if (ch === "{") {
|
|
1257
|
+
stack.push("{")
|
|
1258
|
+
key = true
|
|
1259
|
+
} else if (ch === "[") stack.push("[")
|
|
1260
|
+
else if (ch === "}" || ch === "]") stack.pop()
|
|
1261
|
+
else if (ch === ",") key = stack.at(-1) === "{"
|
|
1262
|
+
else if (ch === ":") key = false
|
|
1263
|
+
out += ch
|
|
1264
|
+
i++
|
|
1265
|
+
continue
|
|
1266
|
+
}
|
|
1267
|
+
const isKey = key && stack.at(-1) === "{"
|
|
1268
|
+
const inArray = stack.at(-1) === "["
|
|
1269
|
+
out += ch
|
|
1270
|
+
i++
|
|
1271
|
+
for (; i < s.length; i++) {
|
|
1272
|
+
const c = s[i]
|
|
1273
|
+
if (c === "\\") {
|
|
1274
|
+
out += c + (s[i + 1] ?? "")
|
|
1275
|
+
i++
|
|
1276
|
+
continue
|
|
1277
|
+
}
|
|
1278
|
+
if (c !== '"') {
|
|
1279
|
+
out += c
|
|
1280
|
+
continue
|
|
1281
|
+
}
|
|
1282
|
+
const after = s.slice(i + 1)
|
|
1283
|
+
const ends = isKey ? /^\s*:/.test(after) : (inArray ? /^\s*(?:[}\]]\s*)*(?:$|,\s*["{[\d\-tfn])/ : /^\s*(?:[}\]]\s*)*(?:$|,\s*")/).test(after)
|
|
1284
|
+
if (ends) {
|
|
1285
|
+
out += c
|
|
1286
|
+
i++
|
|
1287
|
+
break
|
|
1288
|
+
}
|
|
1289
|
+
out += '\\"'
|
|
1290
|
+
}
|
|
1291
|
+
key = false
|
|
1292
|
+
}
|
|
1293
|
+
return out
|
|
1015
1294
|
}
|
|
1016
1295
|
|
|
1017
1296
|
// GLM's call template: the tool's name, then each argument as
|
|
@@ -1328,6 +1607,10 @@ async function* parts(events, tools = []) {
|
|
|
1328
1607
|
if (t) yield { reasoning: t }
|
|
1329
1608
|
if (r.text) yield { text: r.text }
|
|
1330
1609
|
for (const c of r.calls) yield call({ ...c, id: callId() })
|
|
1610
|
+
if (r.error) {
|
|
1611
|
+
yield { error: r.error, code: null }
|
|
1612
|
+
return
|
|
1613
|
+
}
|
|
1331
1614
|
}
|
|
1332
1615
|
for (const tc of Array.isArray(d.tool_calls) ? d.tool_calls : []) nc.push(tc)
|
|
1333
1616
|
const u = tokensOf(d.usage)
|
|
@@ -1339,6 +1622,10 @@ async function* parts(events, tools = []) {
|
|
|
1339
1622
|
if (t) yield { reasoning: t }
|
|
1340
1623
|
if (r.text) yield { text: r.text }
|
|
1341
1624
|
for (const c of r.calls) yield call({ ...c, id: callId() })
|
|
1625
|
+
if (r.error) {
|
|
1626
|
+
yield { error: r.error, code: null }
|
|
1627
|
+
return
|
|
1628
|
+
}
|
|
1342
1629
|
for (const c of nc.take(tools)) yield call(c)
|
|
1343
1630
|
if (finish) yield { finish }
|
|
1344
1631
|
}
|
|
@@ -1849,6 +2136,14 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
1849
2136
|
models: { ...site.models, ...(was.models ?? {}) },
|
|
1850
2137
|
}
|
|
1851
2138
|
},
|
|
2139
|
+
// the conversation's id, which the requests below carry as their
|
|
2140
|
+
// session_id: magpie's (or OpenCode's) session for this request, when it
|
|
2141
|
+
// names one
|
|
2142
|
+
"chat.headers": async (input, output) => {
|
|
2143
|
+
const id = input?.model?.providerID ?? input?.provider?.info?.id
|
|
2144
|
+
if (id !== site.id || !input?.sessionID) return
|
|
2145
|
+
output.headers[SESSION] = String(input.sessionID).slice(0, 128)
|
|
2146
|
+
},
|
|
1852
2147
|
provider: {
|
|
1853
2148
|
id: site.id,
|
|
1854
2149
|
// the account's own list, when Trae answers; else the list above
|
|
@@ -1927,11 +2222,16 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
1927
2222
|
if (outer.aborted) ac.abort()
|
|
1928
2223
|
else outer.addEventListener("abort", () => ac.abort(), { once: true })
|
|
1929
2224
|
}
|
|
2225
|
+
// the conversation this request belongs to, from the chat.headers
|
|
2226
|
+
// hook (magpie's session) or the chat's own first user message, as
|
|
2227
|
+
// the account answering it, so a failover inside one conversation
|
|
2228
|
+
// is not one Trae reads as another's
|
|
2229
|
+
const session = conversationID(req, sessionOf(new Headers(init.headers ?? r0?.headers ?? {})), who)
|
|
1930
2230
|
for (const fn of [...new Set(fns)]) {
|
|
1931
2231
|
const res = await fetch(apiOf(a, site) + "/api/agent/v3/llm_utils_chat", {
|
|
1932
2232
|
method: "POST",
|
|
1933
2233
|
headers: ideHeaders(a, site, { Accept: "text/event-stream", "X-Request-ID": randomUUID() }),
|
|
1934
|
-
body: JSON.stringify(chatBody(req, fn, named?.fn === fn || max ? named.name : "", named?.fn === fn || max ? named.most : 0, max)),
|
|
2234
|
+
body: JSON.stringify(chatBody(req, fn, named?.fn === fn || max ? named.name : "", named?.fn === fn || max ? named.most : 0, max, session)),
|
|
1935
2235
|
signal: ac.signal,
|
|
1936
2236
|
})
|
|
1937
2237
|
if (!res.ok) {
|
|
@@ -1982,4 +2282,4 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
1982
2282
|
export const TraeCNAuthPlugin = makePlugin(SITES["trae-cn"])
|
|
1983
2283
|
export const TraeGlobalAuthPlugin = makePlugin(SITES["trae-global"])
|
|
1984
2284
|
|
|
1985
|
-
export const _internal = { HOSTS: SITES["trae-cn"].hosts, MODELS: SITES["trae-cn"].models, TextTools, NativeCalls, looseJSON, glmCall, toolNamed, traeMessages, chatBody, credits, dollarUsageOf, whenOf, newDevice, proofOf, SITES }
|
|
2285
|
+
export const _internal = { HOSTS: SITES["trae-cn"].hosts, MODELS: SITES["trae-cn"].models, TextTools, NativeCalls, looseJSON, glmCall, toolNamed, 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.6",
|
|
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",
|