@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.
Files changed (3) hide show
  1. package/README.md +11 -0
  2. package/index.mjs +102 -2
  3. 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",
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",