@magpie-community/opencode-workbuddy-auth 0.1.40 → 0.1.42
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/index.mjs +150 -72
- package/package.json +1 -1
package/index.mjs
CHANGED
|
@@ -608,8 +608,13 @@ function isMessages(url, body) {
|
|
|
608
608
|
// unsigned thinking is body itself, byte for byte.
|
|
609
609
|
function withoutUnsigned(body) {
|
|
610
610
|
const b = chatOf(body)
|
|
611
|
+
return unsignedOn(b) ? JSON.stringify(b) : body
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// unsignedOn makes withoutUnsigned's change to a body's object; true when it made one
|
|
615
|
+
function unsignedOn(b) {
|
|
611
616
|
const msgs = b?.messages
|
|
612
|
-
if (!Array.isArray(msgs)) return
|
|
617
|
+
if (!Array.isArray(msgs)) return false
|
|
613
618
|
let changed = false
|
|
614
619
|
for (const m of msgs) {
|
|
615
620
|
if (m?.role !== "assistant" || !Array.isArray(m.content)) continue
|
|
@@ -618,7 +623,7 @@ function withoutUnsigned(body) {
|
|
|
618
623
|
changed = true
|
|
619
624
|
m.content = kept.length ? kept : [{ type: "text", text: "" }]
|
|
620
625
|
}
|
|
621
|
-
return changed
|
|
626
|
+
return changed
|
|
622
627
|
}
|
|
623
628
|
|
|
624
629
|
// withoutSignedThinking is a Messages body without any thinking block at
|
|
@@ -674,7 +679,10 @@ function sessionOf(headers) {
|
|
|
674
679
|
// already there — the caller's own, or another plugin's — is kept, and the
|
|
675
680
|
// message and request ids are the same one, as WorkBuddy's client has them.
|
|
676
681
|
function attend(headers, body, session) {
|
|
677
|
-
|
|
682
|
+
attendChat(headers, chatOf(body), session)
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
function attendChat(headers, chat, session) {
|
|
678
686
|
const conv = conversationKey(chat, session)
|
|
679
687
|
const msg = hex()
|
|
680
688
|
headers.set("X-Conversation-Message-ID", msg)
|
|
@@ -792,29 +800,34 @@ function withSystem(body) {
|
|
|
792
800
|
if (typeof body !== "string") return body
|
|
793
801
|
try {
|
|
794
802
|
const b = JSON.parse(body)
|
|
795
|
-
|
|
796
|
-
let changed = unflag(b.messages)
|
|
797
|
-
if (b.max_completion_tokens !== undefined) {
|
|
798
|
-
if (b.max_tokens === undefined) b.max_tokens = b.max_completion_tokens
|
|
799
|
-
delete b.max_completion_tokens
|
|
800
|
-
changed = true
|
|
801
|
-
}
|
|
802
|
-
for (const m of b.messages) {
|
|
803
|
-
if (m?.role !== "developer") continue
|
|
804
|
-
m.role = "system"
|
|
805
|
-
changed = true
|
|
806
|
-
}
|
|
807
|
-
if (asReasoning(b.messages)) changed = true
|
|
808
|
-
if (b.messages[0]?.role !== "system") {
|
|
809
|
-
b.messages.unshift({ role: "system", content: "You are a helpful assistant." })
|
|
810
|
-
changed = true
|
|
811
|
-
}
|
|
812
|
-
return changed ? JSON.stringify(b) : body
|
|
803
|
+
return systemOn(b) ? JSON.stringify(b) : body
|
|
813
804
|
} catch {
|
|
814
805
|
return body
|
|
815
806
|
}
|
|
816
807
|
}
|
|
817
808
|
|
|
809
|
+
// systemOn makes withSystem's changes to a chat's object; true when it made any
|
|
810
|
+
function systemOn(b) {
|
|
811
|
+
if (!Array.isArray(b?.messages) || !b.messages.length) return false
|
|
812
|
+
let changed = unflag(b.messages)
|
|
813
|
+
if (b.max_completion_tokens !== undefined) {
|
|
814
|
+
if (b.max_tokens === undefined) b.max_tokens = b.max_completion_tokens
|
|
815
|
+
delete b.max_completion_tokens
|
|
816
|
+
changed = true
|
|
817
|
+
}
|
|
818
|
+
for (const m of b.messages) {
|
|
819
|
+
if (m?.role !== "developer") continue
|
|
820
|
+
m.role = "system"
|
|
821
|
+
changed = true
|
|
822
|
+
}
|
|
823
|
+
if (asReasoning(b.messages)) changed = true
|
|
824
|
+
if (b.messages[0]?.role !== "system") {
|
|
825
|
+
b.messages.unshift({ role: "system", content: "You are a helpful assistant." })
|
|
826
|
+
changed = true
|
|
827
|
+
}
|
|
828
|
+
return changed
|
|
829
|
+
}
|
|
830
|
+
|
|
818
831
|
// WorkBuddy's channel check reads the system and assistant messages only:
|
|
819
832
|
// tried on 2026-10-21 with real accounts on both builds (glm-5.3,
|
|
820
833
|
// deepseek-v4.1-flash, gpt-5.5), each refused line was served in the user's
|
|
@@ -841,7 +854,11 @@ function textOf(content) {
|
|
|
841
854
|
// systemKey is the text of a chat's system messages, which a chat refused
|
|
842
855
|
// once is known by: the same agent's next chat goes with systemAsUser at once
|
|
843
856
|
function systemKey(body) {
|
|
844
|
-
|
|
857
|
+
return systemKeyOf(chatOf(body))
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
function systemKeyOf(chat) {
|
|
861
|
+
const msgs = chat?.messages
|
|
845
862
|
if (!Array.isArray(msgs)) return ""
|
|
846
863
|
const key = msgs.filter((m) => m?.role === "system").map((m) => textOf(m.content)).join("\n")
|
|
847
864
|
return key.trim() === DEFAULT_SYSTEM ? "" : key
|
|
@@ -849,15 +866,21 @@ function systemKey(body) {
|
|
|
849
866
|
|
|
850
867
|
function systemAsUser(body) {
|
|
851
868
|
const b = chatOf(body)
|
|
869
|
+
return systemAsUserOn(b) ? JSON.stringify(b) : null
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// systemAsUserOn makes systemAsUser's move on a chat's object; false when
|
|
873
|
+
// there's nothing to move
|
|
874
|
+
function systemAsUserOn(b) {
|
|
852
875
|
const msgs = b?.messages
|
|
853
|
-
if (!Array.isArray(msgs)) return
|
|
876
|
+
if (!Array.isArray(msgs)) return false
|
|
854
877
|
let i = 0
|
|
855
878
|
const lead = []
|
|
856
879
|
for (; i < msgs.length && msgs[i]?.role === "system"; i++) lead.push(textOf(msgs[i].content))
|
|
857
880
|
const said = lead.join("\n\n").trim()
|
|
858
881
|
const rest = msgs.slice(i).map((m) => (m?.role === "system" ? { ...m, role: "user" } : m))
|
|
859
882
|
const moved = said && said !== DEFAULT_SYSTEM
|
|
860
|
-
if (!moved && rest.every((m, k) => m === msgs[i + k])) return
|
|
883
|
+
if (!moved && rest.every((m, k) => m === msgs[i + k])) return false
|
|
861
884
|
if (moved) {
|
|
862
885
|
const text = INSTRUCTIONS[0] + said + INSTRUCTIONS[1]
|
|
863
886
|
const u = rest.findIndex((m) => m?.role === "user")
|
|
@@ -867,7 +890,7 @@ function systemAsUser(body) {
|
|
|
867
890
|
else rest[u] = { ...rest[u], content: text }
|
|
868
891
|
}
|
|
869
892
|
b.messages = [{ role: "system", content: DEFAULT_SYSTEM }, ...rest]
|
|
870
|
-
return
|
|
893
|
+
return true
|
|
871
894
|
}
|
|
872
895
|
|
|
873
896
|
// normalizeToolChoice converts the object form emitted by OpenAI clients to
|
|
@@ -877,42 +900,47 @@ function normalizeToolChoice(body) {
|
|
|
877
900
|
if (typeof body !== "string") return body
|
|
878
901
|
try {
|
|
879
902
|
const b = JSON.parse(body)
|
|
880
|
-
|
|
881
|
-
const choice = b.tool_choice
|
|
882
|
-
if (typeof choice === "string") {
|
|
883
|
-
if (choice.trim().toLowerCase() === "none") {
|
|
884
|
-
delete b.tool_choice
|
|
885
|
-
delete b.tools
|
|
886
|
-
delete b.functions
|
|
887
|
-
}
|
|
888
|
-
return JSON.stringify(b)
|
|
889
|
-
}
|
|
890
|
-
if (!choice || typeof choice !== "object" || Array.isArray(choice)) {
|
|
891
|
-
delete b.tool_choice
|
|
892
|
-
return JSON.stringify(b)
|
|
893
|
-
}
|
|
894
|
-
const type = typeof choice.type === "string" ? choice.type.trim().toLowerCase() : ""
|
|
895
|
-
if (type === "none") {
|
|
896
|
-
delete b.tool_choice
|
|
897
|
-
delete b.tools
|
|
898
|
-
delete b.functions
|
|
899
|
-
} else if (type === "auto" || type === "required") {
|
|
900
|
-
b.tool_choice = type
|
|
901
|
-
} else if (type === "function") {
|
|
902
|
-
const fn = choice.function && typeof choice.function === "object" && !Array.isArray(choice.function)
|
|
903
|
-
? choice.function
|
|
904
|
-
: undefined
|
|
905
|
-
const name = typeof fn?.name === "string" ? fn.name.trim() : ""
|
|
906
|
-
b.tool_choice = name || "auto"
|
|
907
|
-
} else {
|
|
908
|
-
delete b.tool_choice
|
|
909
|
-
}
|
|
910
|
-
return JSON.stringify(b)
|
|
903
|
+
return toolChoiceOn(b) ? JSON.stringify(b) : body
|
|
911
904
|
} catch {
|
|
912
905
|
return body
|
|
913
906
|
}
|
|
914
907
|
}
|
|
915
908
|
|
|
909
|
+
// toolChoiceOn makes normalizeToolChoice's change to a chat's object; true
|
|
910
|
+
// when it made one (a string choice other than none is WorkBuddy's already)
|
|
911
|
+
function toolChoiceOn(b) {
|
|
912
|
+
if (!b || typeof b !== "object" || Array.isArray(b) || !("tool_choice" in b)) return false
|
|
913
|
+
const choice = b.tool_choice
|
|
914
|
+
if (typeof choice === "string") {
|
|
915
|
+
if (choice.trim().toLowerCase() !== "none") return false
|
|
916
|
+
delete b.tool_choice
|
|
917
|
+
delete b.tools
|
|
918
|
+
delete b.functions
|
|
919
|
+
return true
|
|
920
|
+
}
|
|
921
|
+
if (!choice || typeof choice !== "object" || Array.isArray(choice)) {
|
|
922
|
+
delete b.tool_choice
|
|
923
|
+
return true
|
|
924
|
+
}
|
|
925
|
+
const type = typeof choice.type === "string" ? choice.type.trim().toLowerCase() : ""
|
|
926
|
+
if (type === "none") {
|
|
927
|
+
delete b.tool_choice
|
|
928
|
+
delete b.tools
|
|
929
|
+
delete b.functions
|
|
930
|
+
} else if (type === "auto" || type === "required") {
|
|
931
|
+
b.tool_choice = type
|
|
932
|
+
} else if (type === "function") {
|
|
933
|
+
const fn = choice.function && typeof choice.function === "object" && !Array.isArray(choice.function)
|
|
934
|
+
? choice.function
|
|
935
|
+
: undefined
|
|
936
|
+
const name = typeof fn?.name === "string" ? fn.name.trim() : ""
|
|
937
|
+
b.tool_choice = name || "auto"
|
|
938
|
+
} else {
|
|
939
|
+
delete b.tool_choice
|
|
940
|
+
}
|
|
941
|
+
return true
|
|
942
|
+
}
|
|
943
|
+
|
|
916
944
|
// ---- tools WorkBuddy takes ----------------------------------------------------
|
|
917
945
|
|
|
918
946
|
// WorkBuddy AI answers 400 code 11133 "Invalid request parameters" (the
|
|
@@ -1261,6 +1289,40 @@ function fitted(body) {
|
|
|
1261
1289
|
}
|
|
1262
1290
|
}
|
|
1263
1291
|
|
|
1292
|
+
// prepared is a request's body read once for every change the loader makes
|
|
1293
|
+
// to it and everything it reads off it (plugins#461): a long chat, 45 MiB
|
|
1294
|
+
// of history, was parsed six times on its way, once by each step, and
|
|
1295
|
+
// written out again whole by normalizeToolChoice even when nothing changed.
|
|
1296
|
+
// chat is the parsed body, changed whether any step changed it; the caller
|
|
1297
|
+
// writes it out once, when it did. Each step is the one its own function
|
|
1298
|
+
// above makes, in the same order.
|
|
1299
|
+
function prepared(body, native) {
|
|
1300
|
+
if (!native && (body instanceof ArrayBuffer || ArrayBuffer.isView(body))) body = new TextDecoder().decode(body)
|
|
1301
|
+
const none = { body, chat: null, names: new Map(), changed: false }
|
|
1302
|
+
if (typeof body !== "string") return none
|
|
1303
|
+
let chat
|
|
1304
|
+
try {
|
|
1305
|
+
chat = JSON.parse(body)
|
|
1306
|
+
} catch {
|
|
1307
|
+
return none
|
|
1308
|
+
}
|
|
1309
|
+
if (chat === null || typeof chat !== "object") return none
|
|
1310
|
+
if (native) return { body, chat, names: new Map(), changed: unsignedOn(chat) }
|
|
1311
|
+
try {
|
|
1312
|
+
let changed = systemOn(chat)
|
|
1313
|
+
const fit = fitTools(chat)
|
|
1314
|
+
if (fit.changed) changed = true
|
|
1315
|
+
if (toolChoiceOn(chat)) changed = true
|
|
1316
|
+
return { body, chat, names: fit.names, changed }
|
|
1317
|
+
} catch {
|
|
1318
|
+
// a shape a step can't take: the steps one by one, each keeping the
|
|
1319
|
+
// body as it was when it can't, as they always have
|
|
1320
|
+
const fit = fitted(withSystem(body))
|
|
1321
|
+
const out = normalizeToolChoice(fit.body)
|
|
1322
|
+
return { body: out, chat: chatOf(out), names: fit.names, changed: false }
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1264
1326
|
// namedBack gives the model's calls in a chat answer (one JSON body, or an
|
|
1265
1327
|
// SSE stream's chunks) the names the client gave its tools.
|
|
1266
1328
|
function namedBack(v, names) {
|
|
@@ -1738,14 +1800,31 @@ const compact = (n) => (Number.isInteger(n) ? String(n) : n.toFixed(2).replace(/
|
|
|
1738
1800
|
// as used or left beside the share (magpie#659). The free plan is said as a
|
|
1739
1801
|
// plan: "Free" alone beside the account read as the model being free, and
|
|
1740
1802
|
// routing as preferring it for that (#337).
|
|
1803
|
+
//
|
|
1804
|
+
// A package with no CycleTotalCapacity has no denominator: it is sent as
|
|
1805
|
+
// magpie's balance-shaped window (#3950), {balance, unit} with no used,
|
|
1806
|
+
// limit or aside, and routing ranks it by what is left. A literal "0" total
|
|
1807
|
+
// is still a total, not a wallet: it stays on the old path, so a lapsed or
|
|
1808
|
+
// placeholder package adds nothing. A literal "0" remainder is a reading
|
|
1809
|
+
// of nothing left, not an absent field; only a package with no
|
|
1810
|
+
// CycleRemainCapacity at all leaves the window out. TotalCredits is not a
|
|
1811
|
+
// fallback: an account with no packages at all keeps its unknown allowance,
|
|
1812
|
+
// as before, so its free models are not skipped.
|
|
1741
1813
|
function usageOf(sum, plan) {
|
|
1742
1814
|
const out = { plan: plan || (sum?.IsPaidUser ? "Pro" : "Free plan"), windows: [] }
|
|
1743
|
-
let total = 0, used = 0
|
|
1815
|
+
let total = 0, used = 0, left = 0, bags = 0
|
|
1744
1816
|
for (const p of sum?.Packages ?? []) {
|
|
1745
|
-
|
|
1746
|
-
|
|
1817
|
+
const hasTotal = p?.CycleTotalCapacity != null && p.CycleTotalCapacity !== ""
|
|
1818
|
+
if (hasTotal) {
|
|
1819
|
+
total += capacity(p.CycleTotalCapacity)
|
|
1820
|
+
used += capacity(p?.CycleUsedCapacity)
|
|
1821
|
+
} else if (p?.CycleRemainCapacity != null && p.CycleRemainCapacity !== "") {
|
|
1822
|
+
left += capacity(p.CycleRemainCapacity)
|
|
1823
|
+
bags++
|
|
1824
|
+
}
|
|
1747
1825
|
}
|
|
1748
1826
|
if (total > 0) out.windows.push({ name: "Credits", used: (100 * used) / total, display: `${compact(used)} / ${compact(total)}`, amount: used, limit: total, unit: "credits" })
|
|
1827
|
+
if (bags > 0 && (total === 0 || left > 0)) out.windows.push({ name: "Prepaid credits", balance: left, display: `${compact(left)} credits`, unit: "credits" })
|
|
1749
1828
|
return out
|
|
1750
1829
|
}
|
|
1751
1830
|
|
|
@@ -2285,16 +2364,13 @@ function makePlugin(site) {
|
|
|
2285
2364
|
// /v1/messages, where a chat is refused 11128.
|
|
2286
2365
|
const native = isMessages(req ? req.url : String(input), body)
|
|
2287
2366
|
let names = {}
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
} else {
|
|
2294
|
-
body = withoutUnsigned(body)
|
|
2295
|
-
}
|
|
2367
|
+
const p = prepared(body, native)
|
|
2368
|
+
let chat = p.chat
|
|
2369
|
+
let changed = p.changed
|
|
2370
|
+
body = p.body
|
|
2371
|
+
if (!native) names = p.names
|
|
2296
2372
|
chatHeaders(site, headers)
|
|
2297
|
-
|
|
2373
|
+
attendChat(headers, chat, session)
|
|
2298
2374
|
const post = (b) =>
|
|
2299
2375
|
fetch(req ? req.url : input, { ...init, method: init?.method ?? req?.method, headers, body: b }).catch((e) => unanswered(site, a, b, e, init?.signal))
|
|
2300
2376
|
// a 401 is answered once, on whichever try meets it: by a
|
|
@@ -2341,11 +2417,13 @@ function makePlugin(site) {
|
|
|
2341
2417
|
}
|
|
2342
2418
|
// a model known to turn tool results then the user's message away
|
|
2343
2419
|
// gets the empty reply between them up front (see endsAfterTools)
|
|
2344
|
-
const model =
|
|
2420
|
+
const model = chat?.model
|
|
2345
2421
|
// a system prompt WorkBuddy refused before goes as the user's
|
|
2346
2422
|
// words at once (see systemAsUser)
|
|
2347
|
-
const sysKey = native ? "" :
|
|
2348
|
-
if (sysKey && refusedSystems.has(sysKey)
|
|
2423
|
+
const sysKey = native ? "" : systemKeyOf(chat)
|
|
2424
|
+
if (sysKey && refusedSystems.has(sysKey) && systemAsUserOn(chat)) changed = true
|
|
2425
|
+
if (changed) body = JSON.stringify(chat)
|
|
2426
|
+
chat = null
|
|
2349
2427
|
const shaped = (b) => (!native && endsAfterTools.has(model) ? replyAfterTools(b) ?? b : b)
|
|
2350
2428
|
let res = await send(shaped(body))
|
|
2351
2429
|
// a signature WorkBuddy's own signing didn't make — a session
|
|
@@ -2575,4 +2653,4 @@ export const WorkBuddyAuthPlugin = makePlugin(SITES.workbuddy)
|
|
|
2575
2653
|
export const WorkBuddyAIAuthPlugin = makePlugin(SITES["workbuddy-ai"])
|
|
2576
2654
|
|
|
2577
2655
|
// for tests
|
|
2578
|
-
export const _internal = { FREE_ONLY, PRICE_TTL, keyAccount, remoteConfig, cycleOf, typeUnions, systemAsUser, systemKey, DEFAULT_SYSTEM, EDGE_READS, fitShape, replyAfterTools, AFTER_TOOLS_REFUSED, enterpriseUsageOf, notDesktop, textOnly, runsOutOf, packagesOf, endOf, enterpriseHeaders, enterpriseOf, liveModels, browserSignIn, withSystem, asReasoning, normalizeToolChoice, fitTools, fitted, renamed, unflagged, usageOf, desktopHeld, explained, REFUSED_HINT, RESTRICTED_HINT, EXHAUSTED_HINT, freeCredits, creditsOf, priceOf, fresh, SITES, renewing, renewed, SESSION, attend, turnKey, conversationKey, signatureOf, isMessages, withoutUnsigned, withoutSignedThinking, answeredBefore, nativeOf, modelOf, configModels, API_MESSAGES: NPM_MESSAGES, API_CHAT: NPM }
|
|
2656
|
+
export const _internal = { FREE_ONLY, PRICE_TTL, keyAccount, remoteConfig, cycleOf, typeUnions, systemAsUser, systemKey, DEFAULT_SYSTEM, EDGE_READS, fitShape, replyAfterTools, AFTER_TOOLS_REFUSED, enterpriseUsageOf, notDesktop, textOnly, runsOutOf, packagesOf, endOf, enterpriseHeaders, enterpriseOf, liveModels, browserSignIn, withSystem, asReasoning, normalizeToolChoice, fitTools, fitted, renamed, unflagged, usageOf, desktopHeld, explained, REFUSED_HINT, RESTRICTED_HINT, EXHAUSTED_HINT, freeCredits, creditsOf, priceOf, fresh, SITES, renewing, renewed, SESSION, attend, turnKey, conversationKey, signatureOf, isMessages, withoutUnsigned, withoutSignedThinking, prepared, answeredBefore, nativeOf, modelOf, configModels, API_MESSAGES: NPM_MESSAGES, API_CHAT: NPM }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@magpie-community/opencode-workbuddy-auth",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.42",
|
|
4
4
|
"description": "OpenCode provider plugins for WorkBuddy's plans: WorkBuddy (CodeBuddy, copilot.tencent.com) and WorkBuddy AI (www.workbuddy.ai)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.mjs",
|