@magpie-community/opencode-workbuddy-auth 0.1.11 → 0.1.12
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 +11 -0
- package/index.mjs +248 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -74,6 +74,17 @@ Each chat carries WorkBuddy's headers:
|
|
|
74
74
|
A chat that doesn't open with a system message is given WorkBuddy's
|
|
75
75
|
default one, as the app does.
|
|
76
76
|
|
|
77
|
+
A chat's tools are given shapes WorkBuddy's models take. Without them
|
|
78
|
+
WorkBuddy AI answers 400 "Invalid request parameters" (11133), DeepSeek's
|
|
79
|
+
models to each shape below and Kimi's, GPT's, Gemini's and GLM's to some:
|
|
80
|
+
- a tool's parameters with no `type`, empty, or a union (`anyOf`, `oneOf`,
|
|
81
|
+
`allOf`) at the root are an object, the union's fields merged;
|
|
82
|
+
- a `pattern`'s `\0` is `\u0000`, which matches the same;
|
|
83
|
+
- a tuple's `items` (a list) is one schema any of them fits;
|
|
84
|
+
- a tool's name with a character other than letters, digits, `_` and `-`
|
|
85
|
+
(`.`, `:`, `/`, a space, CJK) is sent with `_` there, and the model's
|
|
86
|
+
calls come back under the tool's own name.
|
|
87
|
+
|
|
77
88
|
## Models
|
|
78
89
|
|
|
79
90
|
When you are signed in, the models are the ones WorkBuddy's product config
|
package/index.mjs
CHANGED
|
@@ -468,6 +468,250 @@ function withSystem(body) {
|
|
|
468
468
|
}
|
|
469
469
|
}
|
|
470
470
|
|
|
471
|
+
// ---- tools WorkBuddy takes ----------------------------------------------------
|
|
472
|
+
|
|
473
|
+
// WorkBuddy AI answers 400 code 11133 "Invalid request parameters" (the
|
|
474
|
+
// model provider rejected them) to a chat whose tools it can't pass on to
|
|
475
|
+
// the model (wyh on magpie's Discord: ZCode and DeepSeek Harness on
|
|
476
|
+
// deepseek-v4.1-flash). Tried on the owner's account, 2026-10-08: DeepSeek
|
|
477
|
+
// turns away each of the shapes below, and Kimi, GPT, Gemini and GLM some
|
|
478
|
+
// of them; MiniMax none. fitTools gives each a shape every model takes.
|
|
479
|
+
// A tool's name may hold letters, digits, _ and -; another character
|
|
480
|
+
// (". : / space", CJK) is refused.
|
|
481
|
+
const NAME_OK = /^[A-Za-z0-9_-]+$/
|
|
482
|
+
|
|
483
|
+
// safeName is name with each character WorkBuddy refuses as _, kept apart
|
|
484
|
+
// from the names in taken.
|
|
485
|
+
function safeName(name, taken) {
|
|
486
|
+
const base = String(name).replace(/[^A-Za-z0-9_-]/g, "_") || "tool"
|
|
487
|
+
let n = base
|
|
488
|
+
for (let i = 2; taken.has(n); i++) n = `${base}_${i}`
|
|
489
|
+
taken.add(n)
|
|
490
|
+
return n
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// NUL_ESCAPE finds a regex's \0 (the NUL character), stepping over each \\
|
|
494
|
+
// (a backslash) so that a \\0 is left; \01 is an octal escape, also left.
|
|
495
|
+
const NUL_ESCAPE = /\\\\|\\0(?![0-9])/g
|
|
496
|
+
|
|
497
|
+
// fitSchema mends a schema in place, at every depth: a pattern's \0, which
|
|
498
|
+
// DeepSeek's validator says is "not a 'regex'", is \u0000, which matches
|
|
499
|
+
// the same (magpie's gateway does this for DeepSeek's own API,
|
|
500
|
+
// deepseekToolPatterns); a tuple's items (a list) become one schema any of
|
|
501
|
+
// them fits. It reports whether it changed anything.
|
|
502
|
+
function fitSchema(s, depth = 0) {
|
|
503
|
+
if (!s || typeof s !== "object" || depth > 64) return false
|
|
504
|
+
let changed = false
|
|
505
|
+
if (Array.isArray(s)) {
|
|
506
|
+
for (const v of s) if (fitSchema(v, depth + 1)) changed = true
|
|
507
|
+
return changed
|
|
508
|
+
}
|
|
509
|
+
if (typeof s.pattern === "string" && s.pattern.includes("\\0")) {
|
|
510
|
+
const p = s.pattern.replace(NUL_ESCAPE, (m) => (m === "\\\\" ? m : "\\u0000"))
|
|
511
|
+
if (p !== s.pattern) {
|
|
512
|
+
s.pattern = p
|
|
513
|
+
changed = true
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
if (Array.isArray(s.items)) {
|
|
517
|
+
const list = s.items.filter((v) => v && typeof v === "object")
|
|
518
|
+
s.items = list.length === 0 ? {} : list.length === 1 ? list[0] : { anyOf: list }
|
|
519
|
+
delete s.additionalItems
|
|
520
|
+
changed = true
|
|
521
|
+
}
|
|
522
|
+
for (const v of Object.values(s)) if (fitSchema(v, depth + 1)) changed = true
|
|
523
|
+
return changed
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// localRef is the schema a local $ref in root names ("#/$defs/x",
|
|
527
|
+
// "#/definitions/x"), or s itself.
|
|
528
|
+
function localRef(root, s) {
|
|
529
|
+
const ref = s?.$ref
|
|
530
|
+
if (typeof ref !== "string" || !ref.startsWith("#/")) return s
|
|
531
|
+
let at = root
|
|
532
|
+
for (const k of ref.slice(2).split("/")) at = at?.[k.replaceAll("~1", "/").replaceAll("~0", "~")]
|
|
533
|
+
return at && typeof at === "object" ? at : s
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// objectRoot makes a tool's parameters an object at the root, as magpie's
|
|
537
|
+
// gateway does for xAI (provider.ObjectRoot): allOf's branches are merged
|
|
538
|
+
// whole; of anyOf's and oneOf's object branches the properties are merged
|
|
539
|
+
// and a field each requires stays required. A root with no type, or an
|
|
540
|
+
// empty one, is an object. It reports whether it changed anything.
|
|
541
|
+
function objectRoot(ps, root = ps, depth = 0) {
|
|
542
|
+
const unions = ["allOf", "anyOf", "oneOf"].filter((k) => Array.isArray(ps[k]))
|
|
543
|
+
if (ps.type === "object" && !unions.length) return false
|
|
544
|
+
const props = ps.properties && typeof ps.properties === "object" ? ps.properties : {}
|
|
545
|
+
const required = Array.isArray(ps.required) ? [...ps.required] : []
|
|
546
|
+
for (const b0 of ps.allOf ?? []) {
|
|
547
|
+
let b = localRef(root, b0)
|
|
548
|
+
if (!b || typeof b !== "object") continue
|
|
549
|
+
if (depth < 8 && ["allOf", "anyOf", "oneOf"].some((k) => Array.isArray(b[k]))) objectRoot((b = structuredClone(b)), root, depth + 1)
|
|
550
|
+
for (const [k, v] of Object.entries(b.properties ?? {})) if (!(k in props)) props[k] = v
|
|
551
|
+
if (Array.isArray(b.required)) required.push(...b.required)
|
|
552
|
+
}
|
|
553
|
+
const branches = []
|
|
554
|
+
for (const key of ["anyOf", "oneOf"]) {
|
|
555
|
+
for (const b0 of ps[key] ?? []) {
|
|
556
|
+
let b = localRef(root, b0)
|
|
557
|
+
if (!b || typeof b !== "object") continue
|
|
558
|
+
if (depth < 8 && ["allOf", "anyOf", "oneOf"].some((k) => Array.isArray(b[k]))) objectRoot((b = structuredClone(b)), root, depth + 1)
|
|
559
|
+
if (b.type !== "object" && !b.properties) continue
|
|
560
|
+
branches.push(b)
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
const from = new Map()
|
|
564
|
+
for (const b of branches) {
|
|
565
|
+
for (const [k, v] of Object.entries(b.properties ?? {})) {
|
|
566
|
+
if (k in props) continue
|
|
567
|
+
const seen = from.get(k) ?? []
|
|
568
|
+
if (!seen.some((x) => JSON.stringify(x) === JSON.stringify(v))) seen.push(v)
|
|
569
|
+
from.set(k, seen)
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
for (const [k, list] of from) props[k] = list.length === 1 ? list[0] : { anyOf: list }
|
|
573
|
+
if (branches.length) {
|
|
574
|
+
for (const k of Object.keys(branches[0].properties ?? {})) {
|
|
575
|
+
if (branches.every((b) => Array.isArray(b.required) && b.required.includes(k))) required.push(k)
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
for (const k of unions) delete ps[k]
|
|
579
|
+
ps.type = "object"
|
|
580
|
+
ps.properties = props
|
|
581
|
+
const req = [...new Set(required)].filter((k) => typeof k === "string" && k in props)
|
|
582
|
+
if (req.length) ps.required = req
|
|
583
|
+
else delete ps.required
|
|
584
|
+
return true
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// fitTools mends b's function tools in place: their parameters (fitSchema,
|
|
588
|
+
// objectRoot) and names. It reports whether it changed anything, and the
|
|
589
|
+
// names it gave, each to the name the client knows (ours), so that the
|
|
590
|
+
// model's calls are named back. A call in the chat's history is named as
|
|
591
|
+
// its tool now is.
|
|
592
|
+
function fitTools(b) {
|
|
593
|
+
const names = new Map()
|
|
594
|
+
if (!Array.isArray(b?.tools) || !b.tools.length) return { changed: false, names }
|
|
595
|
+
let changed = false
|
|
596
|
+
const taken = new Set()
|
|
597
|
+
for (const t of b.tools) {
|
|
598
|
+
const n = t?.function?.name
|
|
599
|
+
if (typeof n === "string" && NAME_OK.test(n)) taken.add(n)
|
|
600
|
+
}
|
|
601
|
+
const theirs = new Map()
|
|
602
|
+
for (const t of b.tools) {
|
|
603
|
+
const fn = t?.type === "function" || t?.function ? t.function : null
|
|
604
|
+
if (!fn || typeof fn !== "object") continue
|
|
605
|
+
if (typeof fn.name === "string" && !NAME_OK.test(fn.name)) {
|
|
606
|
+
const safe = safeName(fn.name, taken)
|
|
607
|
+
names.set(safe, fn.name)
|
|
608
|
+
theirs.set(fn.name, safe)
|
|
609
|
+
fn.name = safe
|
|
610
|
+
changed = true
|
|
611
|
+
}
|
|
612
|
+
if (fn.parameters === undefined) continue
|
|
613
|
+
if (!fn.parameters || typeof fn.parameters !== "object" || Array.isArray(fn.parameters)) {
|
|
614
|
+
fn.parameters = { type: "object", properties: {} }
|
|
615
|
+
changed = true
|
|
616
|
+
continue
|
|
617
|
+
}
|
|
618
|
+
if (fitSchema(fn.parameters)) changed = true
|
|
619
|
+
if (objectRoot(fn.parameters)) changed = true
|
|
620
|
+
}
|
|
621
|
+
if (theirs.size && Array.isArray(b.messages)) {
|
|
622
|
+
for (const m of b.messages) {
|
|
623
|
+
for (const c of Array.isArray(m?.tool_calls) ? m.tool_calls : []) {
|
|
624
|
+
const safe = theirs.get(c?.function?.name)
|
|
625
|
+
if (safe) c.function.name = safe
|
|
626
|
+
}
|
|
627
|
+
if (m?.role === "tool" && theirs.has(m.name)) m.name = theirs.get(m.name)
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
const choice = b.tool_choice?.function
|
|
631
|
+
if (choice && theirs.has(choice.name)) choice.name = theirs.get(choice.name)
|
|
632
|
+
return { changed, names }
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// fitted is a chat body with its tools mended (fitTools), and the names
|
|
636
|
+
// given its tools, each to the client's own; a body that isn't a chat is
|
|
637
|
+
// as it was.
|
|
638
|
+
function fitted(body) {
|
|
639
|
+
if (typeof body !== "string") return { body, names: new Map() }
|
|
640
|
+
try {
|
|
641
|
+
const b = JSON.parse(body)
|
|
642
|
+
const { changed, names } = fitTools(b)
|
|
643
|
+
return { body: changed ? JSON.stringify(b) : body, names }
|
|
644
|
+
} catch {
|
|
645
|
+
return { body, names: new Map() }
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// namedBack gives the model's calls in a chat answer (one JSON body, or an
|
|
650
|
+
// SSE stream's chunks) the names the client gave its tools.
|
|
651
|
+
function namedBack(v, names) {
|
|
652
|
+
let changed = false
|
|
653
|
+
for (const c of Array.isArray(v?.choices) ? v.choices : []) {
|
|
654
|
+
for (const at of [c?.message, c?.delta]) {
|
|
655
|
+
for (const tc of Array.isArray(at?.tool_calls) ? at.tool_calls : []) {
|
|
656
|
+
const ours = names.get(tc?.function?.name)
|
|
657
|
+
if (ours) {
|
|
658
|
+
tc.function.name = ours
|
|
659
|
+
changed = true
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
return changed
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// renamed is res with namedBack on its body, read as it comes.
|
|
668
|
+
function renamed(res, names) {
|
|
669
|
+
if (!names.size || !res.body) return res
|
|
670
|
+
const headers = new Headers(res.headers)
|
|
671
|
+
headers.delete("content-length")
|
|
672
|
+
headers.delete("content-encoding")
|
|
673
|
+
const json = /json/i.test(res.headers.get("content-type") ?? "")
|
|
674
|
+
if (json) {
|
|
675
|
+
const out = res.text().then((text) => {
|
|
676
|
+
try {
|
|
677
|
+
const v = JSON.parse(text)
|
|
678
|
+
return namedBack(v, names) ? JSON.stringify(v) : text
|
|
679
|
+
} catch {
|
|
680
|
+
return text
|
|
681
|
+
}
|
|
682
|
+
})
|
|
683
|
+
return out.then((text) => new Response(text, { status: res.status, statusText: res.statusText, headers }))
|
|
684
|
+
}
|
|
685
|
+
const dec = new TextDecoder()
|
|
686
|
+
const enc = new TextEncoder()
|
|
687
|
+
let rest = ""
|
|
688
|
+
const line = (l) => {
|
|
689
|
+
const m = /^data:\s?(.*)$/.exec(l)
|
|
690
|
+
if (!m || m[1] === "[DONE]") return l
|
|
691
|
+
try {
|
|
692
|
+
const v = JSON.parse(m[1])
|
|
693
|
+
return namedBack(v, names) ? `data: ${JSON.stringify(v)}` : l
|
|
694
|
+
} catch {
|
|
695
|
+
return l
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
const body = res.body.pipeThrough(
|
|
699
|
+
new TransformStream({
|
|
700
|
+
transform(chunk, ctl) {
|
|
701
|
+
rest += dec.decode(chunk, { stream: true })
|
|
702
|
+
const lines = rest.split("\n")
|
|
703
|
+
rest = lines.pop()
|
|
704
|
+
if (lines.length) ctl.enqueue(enc.encode(lines.map(line).join("\n") + "\n"))
|
|
705
|
+
},
|
|
706
|
+
flush(ctl) {
|
|
707
|
+
rest += dec.decode()
|
|
708
|
+
if (rest) ctl.enqueue(enc.encode(line(rest)))
|
|
709
|
+
},
|
|
710
|
+
}),
|
|
711
|
+
)
|
|
712
|
+
return new Response(body, { status: res.status, statusText: res.statusText, headers })
|
|
713
|
+
}
|
|
714
|
+
|
|
471
715
|
// ---- signing in -------------------------------------------------------------
|
|
472
716
|
|
|
473
717
|
// poll asks path until WorkBuddy answers with data, retrying the codes
|
|
@@ -863,9 +1107,11 @@ function makePlugin(site) {
|
|
|
863
1107
|
let body = init?.body
|
|
864
1108
|
if (body === undefined && req) body = await req.clone().text()
|
|
865
1109
|
body = withSystem(body)
|
|
1110
|
+
const fit = fitted(body)
|
|
1111
|
+
body = fit.body
|
|
866
1112
|
attend(headers, body, session)
|
|
867
1113
|
const res = await fetch(req ? req.url : input, { ...init, method: init?.method ?? req?.method, headers, body })
|
|
868
|
-
return res.status >= 400 ? explained(res) : kept(res)
|
|
1114
|
+
return res.status >= 400 ? explained(res) : kept(await renamed(res, fit.names))
|
|
869
1115
|
},
|
|
870
1116
|
}
|
|
871
1117
|
},
|
|
@@ -970,4 +1216,4 @@ export const WorkBuddyAuthPlugin = makePlugin(SITES.workbuddy)
|
|
|
970
1216
|
export const WorkBuddyAIAuthPlugin = makePlugin(SITES["workbuddy-ai"])
|
|
971
1217
|
|
|
972
1218
|
// for tests
|
|
973
|
-
export const _internal = { withSystem, unflagged, usageOf, desktopHeld, explained, REFUSED_HINT, EXHAUSTED_HINT, freeCredits, creditsOf, fresh, SITES, renewing, renewed, SESSION, attend, turnKey, conversationKey, signatureOf }
|
|
1219
|
+
export const _internal = { withSystem, fitTools, fitted, renamed, unflagged, usageOf, desktopHeld, explained, REFUSED_HINT, EXHAUSTED_HINT, freeCredits, creditsOf, fresh, SITES, renewing, renewed, SESSION, attend, turnKey, conversationKey, signatureOf }
|
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.12",
|
|
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",
|