@magpie-community/opencode-workbuddy-auth 0.2.3 → 0.2.4
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 +102 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -239,6 +239,17 @@ 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
|
+
|
|
242
253
|
## Models
|
|
243
254
|
|
|
244
255
|
When you are signed in, the models are the ones WorkBuddy's product config
|
package/index.mjs
CHANGED
|
@@ -1599,6 +1599,106 @@ 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.
|
|
1609
|
+
const filterSaid = (text) => {
|
|
1610
|
+
const t = text.replace(/’/g, "'").trimStart()
|
|
1611
|
+
return t !== "" && FILTER_SAYS.some((s) => s.startsWith(t))
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
// unsaid is a streamed chat whose reply WorkBuddy's filter refused with its
|
|
1615
|
+
// text (FILTER_SAYS) and finish_reason content_filter, with the text taken
|
|
1616
|
+
// out: a reply that says nothing before content_filter is the refusal magpie
|
|
1617
|
+
// asks the group's next member for, where a 200 that said something is an
|
|
1618
|
+
// answer the agent fails on ("The model refused to complete the request",
|
|
1619
|
+
// plugins #540). Only text that is still the filter's is held, so any other
|
|
1620
|
+
// reply's first words go as they come, and one that only begins like it is
|
|
1621
|
+
// let through at its first word that isn't, or at its end.
|
|
1622
|
+
function unsaid(res) {
|
|
1623
|
+
if (!res.body || !/event-stream/i.test(res.headers.get("content-type") ?? "")) return res
|
|
1624
|
+
const headers = new Headers(res.headers)
|
|
1625
|
+
headers.delete("content-length")
|
|
1626
|
+
headers.delete("content-encoding")
|
|
1627
|
+
const dec = new TextDecoder()
|
|
1628
|
+
const enc = new TextEncoder()
|
|
1629
|
+
let rest = ""
|
|
1630
|
+
let open = true // the reply may still be the filter's
|
|
1631
|
+
let said = ""
|
|
1632
|
+
let held = [] // events held while it may be
|
|
1633
|
+
const chunkOf = (ev) => {
|
|
1634
|
+
const m = /^data:\s?(.*)$/m.exec(ev)
|
|
1635
|
+
if (!m || m[1] === "[DONE]") return null
|
|
1636
|
+
try {
|
|
1637
|
+
const v = JSON.parse(m[1])
|
|
1638
|
+
return v && typeof v === "object" ? v : null
|
|
1639
|
+
} catch {
|
|
1640
|
+
return null
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
// without is ev with the filter's text taken out, or "" when nothing
|
|
1644
|
+
// else is in it
|
|
1645
|
+
const without = (ev) => {
|
|
1646
|
+
const v = chunkOf(ev)
|
|
1647
|
+
const c = v?.choices?.[0]
|
|
1648
|
+
if (!c?.delta || typeof c.delta.content !== "string") return ev
|
|
1649
|
+
delete c.delta.content
|
|
1650
|
+
const left = Object.keys(c.delta).filter((k) => k !== "role")
|
|
1651
|
+
if (!left.length && c.finish_reason == null && v.usage == null) return ""
|
|
1652
|
+
return `data: ${JSON.stringify(v)}`
|
|
1653
|
+
}
|
|
1654
|
+
const event = (ev) => {
|
|
1655
|
+
if (!open) return [ev]
|
|
1656
|
+
const c = chunkOf(ev)?.choices?.[0]
|
|
1657
|
+
const text = typeof c?.delta?.content === "string" ? c.delta.content : ""
|
|
1658
|
+
const finish = c?.finish_reason
|
|
1659
|
+
if (c?.delta?.tool_calls || (text && !filterSaid(said + text))) {
|
|
1660
|
+
open = false
|
|
1661
|
+
const out = [...held, ev]
|
|
1662
|
+
held = []
|
|
1663
|
+
return out
|
|
1664
|
+
}
|
|
1665
|
+
said += text
|
|
1666
|
+
if (finish === "content_filter" && said) {
|
|
1667
|
+
open = false
|
|
1668
|
+
const out = [...held, ev].map(without).filter(Boolean)
|
|
1669
|
+
held = []
|
|
1670
|
+
return out
|
|
1671
|
+
}
|
|
1672
|
+
if (finish) {
|
|
1673
|
+
open = false
|
|
1674
|
+
const out = [...held, ev]
|
|
1675
|
+
held = []
|
|
1676
|
+
return out
|
|
1677
|
+
}
|
|
1678
|
+
if (!said) return [ev] // reasoning, the role, before any text
|
|
1679
|
+
held.push(ev)
|
|
1680
|
+
return []
|
|
1681
|
+
}
|
|
1682
|
+
const body = res.body.pipeThrough(
|
|
1683
|
+
new TransformStream({
|
|
1684
|
+
transform(chunk, ctl) {
|
|
1685
|
+
rest = (rest + dec.decode(chunk, { stream: true })).replace(/\r\n/g, "\n")
|
|
1686
|
+
const evs = rest.split("\n\n")
|
|
1687
|
+
rest = evs.pop()
|
|
1688
|
+
const out = evs.flatMap(event)
|
|
1689
|
+
if (out.length) ctl.enqueue(enc.encode(out.map((e) => e + "\n\n").join("")))
|
|
1690
|
+
},
|
|
1691
|
+
flush(ctl) {
|
|
1692
|
+
rest += dec.decode()
|
|
1693
|
+
const out = held.map((e) => e + "\n\n").join("") + rest
|
|
1694
|
+
held = []
|
|
1695
|
+
if (out) ctl.enqueue(enc.encode(out))
|
|
1696
|
+
},
|
|
1697
|
+
}),
|
|
1698
|
+
)
|
|
1699
|
+
return new Response(body, { status: res.status, statusText: res.statusText, headers })
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1602
1702
|
// ---- signing in -------------------------------------------------------------
|
|
1603
1703
|
|
|
1604
1704
|
// poll asks path until WorkBuddy answers with data, retrying the codes
|
|
@@ -3439,7 +3539,7 @@ function makePlugin(site) {
|
|
|
3439
3539
|
}
|
|
3440
3540
|
if (ours.has(res)) return res
|
|
3441
3541
|
// 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")
|
|
3542
|
+
return res.status >= 400 ? explained(res) : kept(native ? res : unsaid(await renamed(res, names)), renewed ? "renewed" : "kept")
|
|
3443
3543
|
},
|
|
3444
3544
|
}
|
|
3445
3545
|
},
|
|
@@ -3652,4 +3752,4 @@ export const WorkBuddyAuthPlugin = makePlugin(SITES.workbuddy)
|
|
|
3652
3752
|
export const WorkBuddyAIAuthPlugin = makePlugin(SITES["workbuddy-ai"])
|
|
3653
3753
|
|
|
3654
3754
|
// 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 }
|
|
3755
|
+
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.4",
|
|
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",
|