@magpie-community/opencode-trae-auth 0.2.5 → 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.
Files changed (3) hide show
  1. package/README.md +12 -0
  2. package/index.mjs +78 -7
  3. 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`,
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
- function chatBody(req, fn, modelName, most = 0, max = null) {
777
- const session = randomUUID()
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: session,
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)
@@ -2078,6 +2136,14 @@ const makePlugin = (site) => async ({ client }) => {
2078
2136
  models: { ...site.models, ...(was.models ?? {}) },
2079
2137
  }
2080
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
+ },
2081
2147
  provider: {
2082
2148
  id: site.id,
2083
2149
  // the account's own list, when Trae answers; else the list above
@@ -2156,11 +2222,16 @@ const makePlugin = (site) => async ({ client }) => {
2156
2222
  if (outer.aborted) ac.abort()
2157
2223
  else outer.addEventListener("abort", () => ac.abort(), { once: true })
2158
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)
2159
2230
  for (const fn of [...new Set(fns)]) {
2160
2231
  const res = await fetch(apiOf(a, site) + "/api/agent/v3/llm_utils_chat", {
2161
2232
  method: "POST",
2162
2233
  headers: ideHeaders(a, site, { Accept: "text/event-stream", "X-Request-ID": randomUUID() }),
2163
- 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)),
2164
2235
  signal: ac.signal,
2165
2236
  })
2166
2237
  if (!res.ok) {
@@ -2211,4 +2282,4 @@ const makePlugin = (site) => async ({ client }) => {
2211
2282
  export const TraeCNAuthPlugin = makePlugin(SITES["trae-cn"])
2212
2283
  export const TraeGlobalAuthPlugin = makePlugin(SITES["trae-global"])
2213
2284
 
2214
- 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.5",
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",