@magpie-community/opencode-trae-auth 0.2.11 → 0.2.13
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 +14 -1
- package/index.mjs +218 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -107,6 +107,10 @@ agent's `POST https://trae-api-cn.mchost.guru/api/agent/v3/llm_utils_chat`:
|
|
|
107
107
|
Text under `message`, `delta` or an event with no name is the answer's
|
|
108
108
|
too.
|
|
109
109
|
- `token_usage`, `done` and `error` mark usage, the end and failures.
|
|
110
|
+
What `token_usage` says the prompt cache read and wrote
|
|
111
|
+
(`cache_read_input_tokens`, `cache_creation_input_tokens`) goes on under
|
|
112
|
+
those names, and `reasoning_tokens` as
|
|
113
|
+
`completion_tokens_details.reasoning_tokens`.
|
|
110
114
|
`done`'s `finish_reason` `length`/`max_tokens` is OpenAI's `length`.
|
|
111
115
|
- Queue events are ignored.
|
|
112
116
|
- A `{"reasoning_content": …}` object written into the text is reasoning,
|
|
@@ -124,7 +128,16 @@ agent's `POST https://trae-api-cn.mchost.guru/api/agent/v3/llm_utils_chat`:
|
|
|
124
128
|
- GLM also writes its own template, `<tool_call>name<arg_key>k</arg_key>
|
|
125
129
|
<arg_value>v</arg_value></tool_call>` (a string kept as written, other
|
|
126
130
|
types read as JSON), and sometimes a whole call into a native call's
|
|
127
|
-
name; both are read as the call.
|
|
131
|
+
name; both are read as the call. A name that holds the wrapper with
|
|
132
|
+
GLM's tags where its service stopped mid-call — `{"name":"bash",
|
|
133
|
+
"arguments":{"command</arg_key><arg_value>ls</arg_value>` — is read too:
|
|
134
|
+
the tags are the service's own handoff, so what stands between them is
|
|
135
|
+
the model's text (a quote left unescaped read as the quote it is, a
|
|
136
|
+
comma inside a value kept, a field's closing tag ending the value before
|
|
137
|
+
it), and the fields the native call did carry are kept beside the ones
|
|
138
|
+
the name's own text fills in. A name whose tool the request doesn't
|
|
139
|
+
have, or one no field can be read from, goes on as written, as any other
|
|
140
|
+
unknown tool does.
|
|
128
141
|
- A block is read as models get it wrong too: every quote escaped
|
|
129
142
|
(`{\"name\":…}`), quotes or backslashes in a string left unescaped, an
|
|
130
143
|
opener with the name as an attribute (`<tool_call name="bash">` with
|
package/index.mjs
CHANGED
|
@@ -992,13 +992,35 @@ const eventName = (s) =>
|
|
|
992
992
|
.toLowerCase()
|
|
993
993
|
.replace(/^_|_$/g, "")
|
|
994
994
|
|
|
995
|
-
//
|
|
995
|
+
// tokensOf maps Trae's token counts to OpenAI's. Trae's token_usage event
|
|
996
|
+
// (TRAE SOLO CN's ai-agent, TokenUsageEvent: name, prompt_tokens,
|
|
997
|
+
// completion_tokens, total_tokens, reasoning_tokens,
|
|
998
|
+
// cache_creation_input_tokens, cache_read_input_tokens,
|
|
999
|
+
// prompt_tokens_total, completion_tokens_total) says what the prompt cache
|
|
1000
|
+
// read and wrote under Anthropic's names. They go on under those names,
|
|
1001
|
+
// which magpie reads beside prompt_tokens as it reads a relay's in front of
|
|
1002
|
+
// Claude (whether prompt_tokens counts them too is judged there, by
|
|
1003
|
+
// whether it is at least as big); without them every request read as no
|
|
1004
|
+
// cache hit (01huadalang on magpie's Discord: K3, 53 calls, all 0)
|
|
996
1005
|
function tokensOf(u) {
|
|
997
1006
|
if (!u || typeof u !== "object") return null
|
|
998
|
-
const
|
|
999
|
-
const
|
|
1007
|
+
const n = (...v) => Number(v.find((x) => x != null) ?? 0) || 0
|
|
1008
|
+
const p = n(u.prompt_tokens, u.input_tokens, u.promptTokens, u.inputTokens)
|
|
1009
|
+
const c = n(u.completion_tokens, u.output_tokens, u.completionTokens, u.outputTokens)
|
|
1000
1010
|
if (!p && !c) return null
|
|
1001
|
-
|
|
1011
|
+
const out = { prompt_tokens: p, completion_tokens: c, total_tokens: n(u.total_tokens, u.totalTokens) || p + c }
|
|
1012
|
+
const read = n(u.prompt_tokens_details?.cached_tokens)
|
|
1013
|
+
const write = n(u.prompt_tokens_details?.cache_write_tokens)
|
|
1014
|
+
if (read || write) out.prompt_tokens_details = { cached_tokens: read, ...(write ? { cache_write_tokens: write } : {}) }
|
|
1015
|
+
else {
|
|
1016
|
+
const r = n(u.cache_read_input_tokens, u.cacheReadInputTokens)
|
|
1017
|
+
const w = n(u.cache_creation_input_tokens, u.cacheCreationInputTokens)
|
|
1018
|
+
if (r) out.cache_read_input_tokens = r
|
|
1019
|
+
if (w) out.cache_creation_input_tokens = w
|
|
1020
|
+
}
|
|
1021
|
+
const thought = n(u.reasoning_tokens, u.completion_tokens_details?.reasoning_tokens)
|
|
1022
|
+
if (thought) out.completion_tokens_details = { reasoning_tokens: thought }
|
|
1023
|
+
return out
|
|
1002
1024
|
}
|
|
1003
1025
|
|
|
1004
1026
|
let callSeq = 0
|
|
@@ -1471,6 +1493,165 @@ function glmCall(s, tools = []) {
|
|
|
1471
1493
|
return { name, arguments: args }
|
|
1472
1494
|
}
|
|
1473
1495
|
|
|
1496
|
+
// Trae reads a model's call for it and hands a call it could not read on as
|
|
1497
|
+
// the native call: the wrapper the client's prompt asked for, with GLM's own
|
|
1498
|
+
// template tags where the model wrote them and a field's close left behind
|
|
1499
|
+
// (``{"name":"bash","arguments":{"command</arg_key><arg_value>ls</arg_value>``).
|
|
1500
|
+
// The name then holds the call the model meant, and the call's own arguments
|
|
1501
|
+
// — where the model wrote them — hold the rest of it.
|
|
1502
|
+
const NAMECALL = /^\s*\{\s*"name"\s*:\s*"([^"\\]*)"\s*,\s*"(?:arguments|input|parameters)"\s*:\s*\{/
|
|
1503
|
+
// a wrapper the service put text in front of (`bash_abstract_command_0>{…}`):
|
|
1504
|
+
// the same shape, wherever it starts
|
|
1505
|
+
const NAMEHEAD = /\{\s*"name"\s*:\s*"([^"\\]*)"\s*,\s*"(?:arguments|input|parameters)"\s*:\s*\{/
|
|
1506
|
+
const NAMETAG = /<\/?arg_(?:key|value)\s*>/
|
|
1507
|
+
|
|
1508
|
+
// nameCall is the call a name holds when it is one the model wrote there
|
|
1509
|
+
// ({name, arguments}), null when it isn't: a name without the wrapper, one
|
|
1510
|
+
// whose tool the request doesn't have, or one no field could be read from.
|
|
1511
|
+
// The wrapper and the tags are the service's own handoff, so what stands
|
|
1512
|
+
// between them is the model's text as it wrote it — a quote it left
|
|
1513
|
+
// unescaped is read as the quote it is, the way repairedJSON reads a file's
|
|
1514
|
+
// content. A value is JSON where the tool's schema says the field is no
|
|
1515
|
+
// string and the text is one; else the text, cut where the value was cut.
|
|
1516
|
+
function nameCall(s, tools = []) {
|
|
1517
|
+
const text = String(s ?? "")
|
|
1518
|
+
// a wrapper the service put text in front of (`bash_abstract_command_0>{…}`)
|
|
1519
|
+
// is the call it holds, that text being no name the request has
|
|
1520
|
+
const at = text.indexOf("{")
|
|
1521
|
+
if (at < 0) return null
|
|
1522
|
+
const m = NAMECALL.exec(text.slice(at))
|
|
1523
|
+
if (!m) return null
|
|
1524
|
+
const name = m[1]
|
|
1525
|
+
const tool = tools.map((t) => t.function ?? t).find((t) => t?.name === name)
|
|
1526
|
+
if (!tool) return null
|
|
1527
|
+
const fields = nameFields(text.slice(at + m[0].length))
|
|
1528
|
+
const args = {}
|
|
1529
|
+
for (const [key, value] of fields) {
|
|
1530
|
+
// a field the schema says is no string is its JSON where it is one (a
|
|
1531
|
+
// number the model wrote, an object), else the text it is
|
|
1532
|
+
if (tool.parameters?.properties?.[key]?.type !== "string") {
|
|
1533
|
+
try {
|
|
1534
|
+
args[key] = JSON.parse(value.trim())
|
|
1535
|
+
continue
|
|
1536
|
+
} catch {}
|
|
1537
|
+
}
|
|
1538
|
+
args[key] = value
|
|
1539
|
+
}
|
|
1540
|
+
return Object.keys(args).length ? { name, arguments: args } : null
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
// nameFields reads a wrapper's body as its fields: a key, then its value.
|
|
1544
|
+
// A tag stands where the service cut a colon or a quote, so a value ends at
|
|
1545
|
+
// the tag that ends it, at the next key, or at the structure that closes it
|
|
1546
|
+
// — and at a closing tag (`</description>`) whose name is a field's, because
|
|
1547
|
+
// what follows such a tag is the service's own text, not the model's.
|
|
1548
|
+
// What none of those reached is no field's text and is left out, so a call
|
|
1549
|
+
// cut in the middle keeps what it has.
|
|
1550
|
+
function nameFields(body) {
|
|
1551
|
+
const out = []
|
|
1552
|
+
let i = 0
|
|
1553
|
+
for (let n = 0; n < 64 && i < body.length; n++) {
|
|
1554
|
+
// between fields: blanks, a comma, or the tags that stand for either
|
|
1555
|
+
i += (body.slice(i).match(/^[\s,]+|^<\/?[a-z_]+\s*>/) || [""])[0].length
|
|
1556
|
+
if (body[i] !== '"') break
|
|
1557
|
+
const key = nameString(body, i, () => true)
|
|
1558
|
+
i = key.i
|
|
1559
|
+
if (!key.text) break
|
|
1560
|
+
const colon = body.slice(i).match(/^\s*:\s*/)
|
|
1561
|
+
if (colon) i += colon[0].length
|
|
1562
|
+
// a value the service cut at its tag: what stands after the tag is the
|
|
1563
|
+
// model's text (limit</arg_key><arg_value>55)
|
|
1564
|
+
i += (body.slice(i).match(/^(?:<\/?[a-z_]+\s*>)+/) || [""])[0].length
|
|
1565
|
+
let value
|
|
1566
|
+
if (body[i] === '"') {
|
|
1567
|
+
const v = nameString(body, i, (rest) => endsValue(rest))
|
|
1568
|
+
value = v.text
|
|
1569
|
+
i = v.i
|
|
1570
|
+
if (!value && !v.closed) break // an open quote with nothing in it
|
|
1571
|
+
} else {
|
|
1572
|
+
const rest = body.slice(i)
|
|
1573
|
+
const stop = valueEnd(rest)
|
|
1574
|
+
value = rest.slice(0, stop).replace(/^[\s,:]+/, "")
|
|
1575
|
+
// the field's own close: the structure that follows it, and a quote
|
|
1576
|
+
// the service cut it at where an escape isn't what stands before it
|
|
1577
|
+
value = value.replace(/[\s,}\]]+$/, "").replace(/(?<!\\)"$/, "").replace(/[\s,}\]]+$/, "")
|
|
1578
|
+
i += stop
|
|
1579
|
+
// nothing introduced a value (no colon, no quote, no tag) and none
|
|
1580
|
+
// stands: the wrapper is cut at the key, and an empty call read out
|
|
1581
|
+
// of it would be a call the model never wrote
|
|
1582
|
+
if (!value) break
|
|
1583
|
+
}
|
|
1584
|
+
out.push([key.text, value])
|
|
1585
|
+
}
|
|
1586
|
+
return out
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
// endsValue is what may stand after a string's closing quote: the structure
|
|
1590
|
+
// that closes the field (`,`, `}`, `]`, the end), a tag that stands for one,
|
|
1591
|
+
// or the next field's key.
|
|
1592
|
+
function endsValue(rest) {
|
|
1593
|
+
if (/^\s*(?:[,}\]$]|<\/?[a-z_]+\s*>)/.test(rest)) return true
|
|
1594
|
+
return /^\s*"(?:[^"\\]|\\.)*"\s*(?::|<\/?[a-z_]+\s*>)/.test(rest)
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
// valueEnd is where an unquoted value runs out: the next key, a tag that
|
|
1598
|
+
// ends it, or the structure that closes the field. A comma inside it is the
|
|
1599
|
+
// model's own (sed -n 1,9p), so only a brace or a bracket closes a field.
|
|
1600
|
+
function valueEnd(rest) {
|
|
1601
|
+
const stops = [
|
|
1602
|
+
rest.search(/"(?:(?:[^"\\<>]*)"\s*(?::|,|\s*<\/?[a-z_]+\s*>)|[^"\\<>]*<\/?[a-z_]+\s*>)/),
|
|
1603
|
+
rest.search(/<\/?[a-z_]+\s*>/),
|
|
1604
|
+
rest.search(/[}\]]/),
|
|
1605
|
+
].filter((x) => x >= 0)
|
|
1606
|
+
return stops.length ? Math.min(...stops) : rest.length
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
// nameString reads the string at s[i] === '"', its escapes kept as written.
|
|
1610
|
+
// ends says, for what follows a quote, whether that quote ended the string:
|
|
1611
|
+
// a quote the model left unescaped inside a value is escaped as read on.
|
|
1612
|
+
// `closed` is whether the model's own quote ended it, where the read ran
|
|
1613
|
+
// into a tag or the end of the text.
|
|
1614
|
+
function nameString(s, i, ends) {
|
|
1615
|
+
i++ // the opening quote
|
|
1616
|
+
let src = ""
|
|
1617
|
+
while (i < s.length) {
|
|
1618
|
+
const ch = s[i]
|
|
1619
|
+
if (ch === "\\") {
|
|
1620
|
+
src += s[i] + (s[i + 1] ?? "")
|
|
1621
|
+
i += 2
|
|
1622
|
+
continue
|
|
1623
|
+
}
|
|
1624
|
+
if (ch === "<") {
|
|
1625
|
+
// a tag ends the run: the service cut the value at it, whether it is
|
|
1626
|
+
// one of its own (`</arg_value>`) or a field's (`</description>`),
|
|
1627
|
+
// for neither is text the model wrote
|
|
1628
|
+
if (/^<\/?[a-z_][\w-]*\s*>/.test(s.slice(i))) break
|
|
1629
|
+
}
|
|
1630
|
+
if (ch === '"') {
|
|
1631
|
+
if (ends(s.slice(i + 1))) {
|
|
1632
|
+
i++
|
|
1633
|
+
return { text: decode(src), i, closed: true }
|
|
1634
|
+
}
|
|
1635
|
+
src += '\\"'
|
|
1636
|
+
i++
|
|
1637
|
+
continue
|
|
1638
|
+
}
|
|
1639
|
+
src += ch
|
|
1640
|
+
i++
|
|
1641
|
+
}
|
|
1642
|
+
return { text: decode(src), i, closed: false }
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
// decode is a string's own source as the string: the escapes the model wrote
|
|
1646
|
+
// are the characters they stand for, a newline among them
|
|
1647
|
+
function decode(src) {
|
|
1648
|
+
try {
|
|
1649
|
+
return JSON.parse('"' + escapeRaw(src) + '"')
|
|
1650
|
+
} catch {
|
|
1651
|
+
return src
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1474
1655
|
// toolNamed is the request's own name for a tool a model named otherwise
|
|
1475
1656
|
// (Read for read, list-files for list_files); name itself when the request
|
|
1476
1657
|
// has none like it. A tool it has none of goes on as named: the agent tells
|
|
@@ -1662,12 +1843,21 @@ class NativeCalls {
|
|
|
1662
1843
|
}
|
|
1663
1844
|
take(tools = []) {
|
|
1664
1845
|
return this.calls.filter((c) => c.name).map(({ id, name, arguments: a }) => {
|
|
1665
|
-
// a name that holds the whole call, as
|
|
1666
|
-
// that call, its arguments the name's when it gave
|
|
1667
|
-
|
|
1846
|
+
// a name that holds the whole call, as the client's wrapper or in
|
|
1847
|
+
// GLM's template: that call, its arguments the name's when it gave
|
|
1848
|
+
// none of its own. The wrapper is read as JSON where it is whole —
|
|
1849
|
+
// its nesting is its own — and as the service's handoff where it was
|
|
1850
|
+
// left mid-call, a wrapper written with a space (`{ "name"`) whole
|
|
1851
|
+
// like any other
|
|
1852
|
+
const w = name.trimStart().startsWith("{") ? looseJSON(name) ?? nameCall(name, tools)
|
|
1853
|
+
: NAMEHEAD.test(name) ? nameCall(name, tools)
|
|
1854
|
+
: name.includes("<arg_key>") ? glmCall(name, tools) : null
|
|
1668
1855
|
if (w?.name) {
|
|
1669
1856
|
name = String(w.name)
|
|
1670
|
-
|
|
1857
|
+
// the name's own arguments are the model's; what the name read fills
|
|
1858
|
+
// the fields they left (the wrapper's text is the whole call where
|
|
1859
|
+
// Trae moved the arguments with it, and the rest where it did not)
|
|
1860
|
+
a = nameArgs(a, w)
|
|
1671
1861
|
}
|
|
1672
1862
|
const v = a && !done(a) ? looseJSON(a) : null
|
|
1673
1863
|
return { id: id || callId(), name, arguments: v ? JSON.stringify(v) : a || "{}" }
|
|
@@ -1675,6 +1865,25 @@ class NativeCalls {
|
|
|
1675
1865
|
}
|
|
1676
1866
|
}
|
|
1677
1867
|
|
|
1868
|
+
// nameArgs is the arguments of a call a name held: the native call's own
|
|
1869
|
+
// where it carried them, else the name's, and the two together where each
|
|
1870
|
+
// has a field the other hasn't — the service moves the arguments it read
|
|
1871
|
+
// into the native call and leaves the rest in the name. The name's side is
|
|
1872
|
+
// read the way main reads a whole name (`callArgs`: `arguments`, `input`,
|
|
1873
|
+
// `parameters`, arguments given as a string, or the flat fields), so what
|
|
1874
|
+
// main already read keeps reading the same. The two stand together only as
|
|
1875
|
+
// two sets of fields; a call's own arguments that are not that — text the
|
|
1876
|
+
// service cut, or a value that isn't a call's fields — go on as they stand,
|
|
1877
|
+
// which is what main passed on and what the agent can answer.
|
|
1878
|
+
function nameArgs(native, fromName) {
|
|
1879
|
+
const a = typeof native === "string" ? native.trim() : ""
|
|
1880
|
+
const named = callArgs(fromName)
|
|
1881
|
+
if (!a || a === "{}") return named
|
|
1882
|
+
const own = looseJSON(a), side = looseJSON(named)
|
|
1883
|
+
if (!own || !side || typeof own !== "object" || typeof side !== "object" || Array.isArray(own) || Array.isArray(side)) return a
|
|
1884
|
+
return JSON.stringify({ ...side, ...own })
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1678
1887
|
// resent says next is the call's whole arguments sent again after a
|
|
1679
1888
|
// partial snapshot, not a piece of them: Trae resends a snapshot whose
|
|
1680
1889
|
// punctuation isn't the first one's (− for -, ′ for '), so it doesn't
|
|
@@ -2474,4 +2683,4 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
2474
2683
|
export const TraeCNAuthPlugin = makePlugin(SITES["trae-cn"])
|
|
2475
2684
|
export const TraeGlobalAuthPlugin = makePlugin(SITES["trae-global"])
|
|
2476
2685
|
|
|
2477
|
-
export const _internal = { traeHost, accountOf, HOSTS: SITES["trae-cn"].hosts, MODELS: SITES["trae-cn"].models, TextTools, NativeCalls, looseJSON, glmCall, toolNamed, unwrapped, traeMessages, chatBody, credits, dollarUsageOf, whenOf, newDevice, proofOf, SITES, SESSION, conversationID }
|
|
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 }
|
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.13",
|
|
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",
|