@magpie-community/opencode-workbuddy-auth 0.2.3 → 0.2.5
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 -0
- package/index.mjs +106 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -239,6 +239,20 @@ serves; a model that refused it once gets it that way from then on, until
|
|
|
239
239
|
the plugin restarts. A chat ending with the assistant's message (a prefill)
|
|
240
240
|
is refused as before.
|
|
241
241
|
|
|
242
|
+
WorkBuddy's safety filter can refuse a reply it has begun:
|
|
243
|
+
`deepseek-v4.1-flash` reasoned, said "Sorry, I can't respond to this
|
|
244
|
+
question.", and ended with `finish_reason: "content_filter"` and no output
|
|
245
|
+
tokens, all under a 200 (#540). Since 0.2.4 that line is taken out of a
|
|
246
|
+
streamed chat ending `content_filter`, so the reply says nothing before the
|
|
247
|
+
filter's stop: magpie hands such a refusal to the group's next member
|
|
248
|
+
instead of passing it to the agent as an answer. Only text that is still
|
|
249
|
+
the filter's line is held, so any other reply's words go as they come; the
|
|
250
|
+
same words ending `stop`, or the filter cutting off a reply after other
|
|
251
|
+
words, go through as they came.
|
|
252
|
+
copilot.tencent.com sends the line with a newline on each side
|
|
253
|
+
(`"\nSorry, I can't respond to this question.\n"`); since 0.2.5 white
|
|
254
|
+
space around it no longer lets it through.
|
|
255
|
+
|
|
242
256
|
## Models
|
|
243
257
|
|
|
244
258
|
When you are signed in, the models are the ones WorkBuddy's product config
|
package/index.mjs
CHANGED
|
@@ -1599,6 +1599,110 @@ function renamed(res, names) {
|
|
|
1599
1599
|
return new Response(body, { status: res.status, statusText: res.statusText, headers })
|
|
1600
1600
|
}
|
|
1601
1601
|
|
|
1602
|
+
// FILTER_SAYS is the text WorkBuddy's safety filter puts in a reply it ends
|
|
1603
|
+
// with finish_reason "content_filter": deepseek-v4.1-flash on
|
|
1604
|
+
// copilot.tencent.com reasoned, said this, and ended content_filter with
|
|
1605
|
+
// no output tokens counted (itspig, plugins #540).
|
|
1606
|
+
const FILTER_SAYS = ["Sorry, I can't respond to this question."]
|
|
1607
|
+
|
|
1608
|
+
// filterSaid tells whether text said so far may still be FILTER_SAYS, with
|
|
1609
|
+
// white space on either side: copilot.tencent.com sends it as
|
|
1610
|
+
// "\nSorry, I can't respond to this question.\n" (itspig, plugins #540).
|
|
1611
|
+
// Text that is only white space so far may still be it; text that goes on
|
|
1612
|
+
// past it is not.
|
|
1613
|
+
const filterSaid = (text) => {
|
|
1614
|
+
const t = text.replace(/’/g, "'").trim()
|
|
1615
|
+
return FILTER_SAYS.some((s) => s.startsWith(t))
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
// unsaid is a streamed chat whose reply WorkBuddy's filter refused with its
|
|
1619
|
+
// text (FILTER_SAYS) and finish_reason content_filter, with the text taken
|
|
1620
|
+
// out: a reply that says nothing before content_filter is the refusal magpie
|
|
1621
|
+
// asks the group's next member for, where a 200 that said something is an
|
|
1622
|
+
// answer the agent fails on ("The model refused to complete the request",
|
|
1623
|
+
// plugins #540). Only text that is still the filter's is held, so any other
|
|
1624
|
+
// reply's first words go as they come, and one that only begins like it is
|
|
1625
|
+
// let through at its first word that isn't, or at its end.
|
|
1626
|
+
function unsaid(res) {
|
|
1627
|
+
if (!res.body || !/event-stream/i.test(res.headers.get("content-type") ?? "")) return res
|
|
1628
|
+
const headers = new Headers(res.headers)
|
|
1629
|
+
headers.delete("content-length")
|
|
1630
|
+
headers.delete("content-encoding")
|
|
1631
|
+
const dec = new TextDecoder()
|
|
1632
|
+
const enc = new TextEncoder()
|
|
1633
|
+
let rest = ""
|
|
1634
|
+
let open = true // the reply may still be the filter's
|
|
1635
|
+
let said = ""
|
|
1636
|
+
let held = [] // events held while it may be
|
|
1637
|
+
const chunkOf = (ev) => {
|
|
1638
|
+
const m = /^data:\s?(.*)$/m.exec(ev)
|
|
1639
|
+
if (!m || m[1] === "[DONE]") return null
|
|
1640
|
+
try {
|
|
1641
|
+
const v = JSON.parse(m[1])
|
|
1642
|
+
return v && typeof v === "object" ? v : null
|
|
1643
|
+
} catch {
|
|
1644
|
+
return null
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
// without is ev with the filter's text taken out, or "" when nothing
|
|
1648
|
+
// else is in it
|
|
1649
|
+
const without = (ev) => {
|
|
1650
|
+
const v = chunkOf(ev)
|
|
1651
|
+
const c = v?.choices?.[0]
|
|
1652
|
+
if (!c?.delta || typeof c.delta.content !== "string") return ev
|
|
1653
|
+
delete c.delta.content
|
|
1654
|
+
const left = Object.keys(c.delta).filter((k) => k !== "role")
|
|
1655
|
+
if (!left.length && c.finish_reason == null && v.usage == null) return ""
|
|
1656
|
+
return `data: ${JSON.stringify(v)}`
|
|
1657
|
+
}
|
|
1658
|
+
const event = (ev) => {
|
|
1659
|
+
if (!open) return [ev]
|
|
1660
|
+
const c = chunkOf(ev)?.choices?.[0]
|
|
1661
|
+
const text = typeof c?.delta?.content === "string" ? c.delta.content : ""
|
|
1662
|
+
const finish = c?.finish_reason
|
|
1663
|
+
if (c?.delta?.tool_calls || (text && !filterSaid(said + text))) {
|
|
1664
|
+
open = false
|
|
1665
|
+
const out = [...held, ev]
|
|
1666
|
+
held = []
|
|
1667
|
+
return out
|
|
1668
|
+
}
|
|
1669
|
+
said += text
|
|
1670
|
+
if (finish === "content_filter" && said) {
|
|
1671
|
+
open = false
|
|
1672
|
+
const out = [...held, ev].map(without).filter(Boolean)
|
|
1673
|
+
held = []
|
|
1674
|
+
return out
|
|
1675
|
+
}
|
|
1676
|
+
if (finish) {
|
|
1677
|
+
open = false
|
|
1678
|
+
const out = [...held, ev]
|
|
1679
|
+
held = []
|
|
1680
|
+
return out
|
|
1681
|
+
}
|
|
1682
|
+
if (!said) return [ev] // reasoning, the role, before any text
|
|
1683
|
+
held.push(ev)
|
|
1684
|
+
return []
|
|
1685
|
+
}
|
|
1686
|
+
const body = res.body.pipeThrough(
|
|
1687
|
+
new TransformStream({
|
|
1688
|
+
transform(chunk, ctl) {
|
|
1689
|
+
rest = (rest + dec.decode(chunk, { stream: true })).replace(/\r\n/g, "\n")
|
|
1690
|
+
const evs = rest.split("\n\n")
|
|
1691
|
+
rest = evs.pop()
|
|
1692
|
+
const out = evs.flatMap(event)
|
|
1693
|
+
if (out.length) ctl.enqueue(enc.encode(out.map((e) => e + "\n\n").join("")))
|
|
1694
|
+
},
|
|
1695
|
+
flush(ctl) {
|
|
1696
|
+
rest += dec.decode()
|
|
1697
|
+
const out = held.map((e) => e + "\n\n").join("") + rest
|
|
1698
|
+
held = []
|
|
1699
|
+
if (out) ctl.enqueue(enc.encode(out))
|
|
1700
|
+
},
|
|
1701
|
+
}),
|
|
1702
|
+
)
|
|
1703
|
+
return new Response(body, { status: res.status, statusText: res.statusText, headers })
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1602
1706
|
// ---- signing in -------------------------------------------------------------
|
|
1603
1707
|
|
|
1604
1708
|
// poll asks path until WorkBuddy answers with data, retrying the codes
|
|
@@ -3439,7 +3543,7 @@ function makePlugin(site) {
|
|
|
3439
3543
|
}
|
|
3440
3544
|
if (ours.has(res)) return res
|
|
3441
3545
|
// renewed here: a mark the account had comes off
|
|
3442
|
-
return res.status >= 400 ? explained(res) : kept(native ? res : await renamed(res, names), renewed ? "renewed" : "kept")
|
|
3546
|
+
return res.status >= 400 ? explained(res) : kept(native ? res : unsaid(await renamed(res, names)), renewed ? "renewed" : "kept")
|
|
3443
3547
|
},
|
|
3444
3548
|
}
|
|
3445
3549
|
},
|
|
@@ -3652,4 +3756,4 @@ export const WorkBuddyAuthPlugin = makePlugin(SITES.workbuddy)
|
|
|
3652
3756
|
export const WorkBuddyAIAuthPlugin = makePlugin(SITES["workbuddy-ai"])
|
|
3653
3757
|
|
|
3654
3758
|
// for tests
|
|
3655
|
-
export const _internal = { idFor, replyAfterToolsOn, withDefaultEffort, defaultEffortOf, gzipped, FREE_ONLY, PRICE_TTL, keyAccount, remoteConfig, cycleOf, typeUnions, systemAsUser, systemKey, DEFAULT_SYSTEM, EDGE_READS, fitShape, replyAfterTools, AFTER_TOOLS_REFUSED, enterpriseUsageOf, accountName, 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, REFUSED_DOC, commaPaused, commaPausedOn, opened, freeCredits, creditsOf, priceOf, fresh, SITES, renewing, renewed, SESSION, attend, scopeOf, turnKey, conversationKey, signatureOf, isMessages, withoutUnsigned, withoutSignedThinking, prepared, answeredBefore, nativeOf, modelOf, configModels, freeWindowOf, tokensShort, freeWindowRows, freeWindowLedger, readLedgerTail, pinned, rowTime, rowAccount, ofAccount, windowOffset, windowReset, FREE_WINDOW_MODELS, FREE_WINDOW_SPAN, FREE_WINDOW_GAP, FREE_WINDOW_LEAD, checkinOf, API_MESSAGES: NPM_MESSAGES, API_CHAT: NPM }
|
|
3759
|
+
export const _internal = { idFor, replyAfterToolsOn, withDefaultEffort, defaultEffortOf, gzipped, FREE_ONLY, PRICE_TTL, keyAccount, remoteConfig, cycleOf, typeUnions, systemAsUser, systemKey, DEFAULT_SYSTEM, EDGE_READS, fitShape, replyAfterTools, AFTER_TOOLS_REFUSED, enterpriseUsageOf, accountName, notDesktop, textOnly, runsOutOf, packagesOf, endOf, enterpriseHeaders, enterpriseOf, liveModels, browserSignIn, withSystem, asReasoning, normalizeToolChoice, fitTools, fitted, renamed, unsaid, FILTER_SAYS, unflagged, usageOf, desktopHeld, explained, REFUSED_HINT, RESTRICTED_HINT, EXHAUSTED_HINT, REFUSED_DOC, commaPaused, commaPausedOn, opened, freeCredits, creditsOf, priceOf, fresh, SITES, renewing, renewed, SESSION, attend, scopeOf, turnKey, conversationKey, signatureOf, isMessages, withoutUnsigned, withoutSignedThinking, prepared, answeredBefore, nativeOf, modelOf, configModels, freeWindowOf, tokensShort, freeWindowRows, freeWindowLedger, readLedgerTail, pinned, rowTime, rowAccount, ofAccount, windowOffset, windowReset, FREE_WINDOW_MODELS, FREE_WINDOW_SPAN, FREE_WINDOW_GAP, FREE_WINDOW_LEAD, checkinOf, 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.2.
|
|
3
|
+
"version": "0.2.5",
|
|
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",
|