@magpie-community/opencode-workbuddy-auth 0.1.37 → 0.1.39
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 +22 -2
- package/index.mjs +125 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -164,8 +164,12 @@ refusal rests the account.
|
|
|
164
164
|
|
|
165
165
|
The loader also translates OpenAI-compatible request spellings WorkBuddy
|
|
166
166
|
doesn't take: `max_completion_tokens` becomes `max_tokens`, `developer`
|
|
167
|
-
messages become `system`,
|
|
168
|
-
string form
|
|
167
|
+
messages become `system`, an object `tool_choice` becomes WorkBuddy's
|
|
168
|
+
string form, and an earlier assistant turn's `reasoning_content` (or
|
|
169
|
+
`reasoning_details` text) goes as `reasoning`, the field WorkBuddy's own
|
|
170
|
+
client sends. WorkBuddy drops `reasoning_content`, so a client such as Grok
|
|
171
|
+
Build that sends it lost its earlier thinking; nothing is folded into the
|
|
172
|
+
message's content.
|
|
169
173
|
|
|
170
174
|
A chat's tools are given shapes WorkBuddy's models take. Without them
|
|
171
175
|
WorkBuddy AI answers 400 "Invalid request parameters" (11133), DeepSeek's
|
|
@@ -228,6 +232,22 @@ some hours of the day (Hy4 preview, free 23:00–8:00) is given as its full
|
|
|
228
232
|
price, never as free: magpie keeps the list until it asks again, so a list
|
|
229
233
|
asked for at night would say "free" all day (#121).
|
|
230
234
|
|
|
235
|
+
### Free models only
|
|
236
|
+
|
|
237
|
+
Each site (WorkBuddy, WorkBuddy AI) has a "Free models only" switch in its
|
|
238
|
+
provider settings in magpie, off by default (#430). When it is on:
|
|
239
|
+
|
|
240
|
+
- the list holds only the models the site serves at no cost now. A model free
|
|
241
|
+
only at night, or one with no price, counts as paid;
|
|
242
|
+
- a request for any other model is answered by the plugin with a 404
|
|
243
|
+
`model_not_found` and never sent, so no credits are spent. magpie rests
|
|
244
|
+
only that model and tries the next provider;
|
|
245
|
+
- if the price list can't be read, nothing is sent (503), since whether the
|
|
246
|
+
model is free is unknown. An API key WorkBuddy lists no prices for lists
|
|
247
|
+
no models.
|
|
248
|
+
|
|
249
|
+
The price list is read again at most once a minute for requests.
|
|
250
|
+
|
|
231
251
|
## Credits in magpie
|
|
232
252
|
|
|
233
253
|
magpie's usage card shows the account's credits from the resource summary.
|
package/index.mjs
CHANGED
|
@@ -592,6 +592,12 @@ function withoutSignedThinking(body) {
|
|
|
592
592
|
|
|
593
593
|
// chatOf is a chat's body as the object it is, null for one that isn't: a
|
|
594
594
|
// body that isn't text (bytes), or isn't JSON at all.
|
|
595
|
+
// bodyString is a body given as bytes, as text; anything else is null.
|
|
596
|
+
function bodyString(body) {
|
|
597
|
+
if (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) return new TextDecoder().decode(body)
|
|
598
|
+
return null
|
|
599
|
+
}
|
|
600
|
+
|
|
595
601
|
function chatOf(body) {
|
|
596
602
|
if (typeof body !== "string") return null
|
|
597
603
|
try {
|
|
@@ -699,6 +705,36 @@ function unflag(messages) {
|
|
|
699
705
|
// OpenAI-compatible client normally uses max_completion_tokens and may call a
|
|
700
706
|
// reasoning prompt developer; WorkBuddy's endpoint takes max_tokens and
|
|
701
707
|
// system instead.
|
|
708
|
+
// An earlier turn's thinking goes back as `reasoning`, the field WorkBuddy's
|
|
709
|
+
// own client (CodeBuddy's agents converter) sets on an assistant message.
|
|
710
|
+
// WorkBuddy drops `reasoning_content`, which Grok Build, DeepSeek-style
|
|
711
|
+
// clients and magpie's own replies use: measured 2026-10-27 on
|
|
712
|
+
// www.workbuddy.ai, a codeword in reasoning_content reached none of
|
|
713
|
+
// kimi-k2.6, kimi-k3, glm-5.3, deepseek-v4.1-flash and minimax-m3, while
|
|
714
|
+
// the same words as `reasoning` reached kimi-k2.6 and minimax-m3.
|
|
715
|
+
// OpenRouter-style reasoning_details are read for their text the same way.
|
|
716
|
+
function reasoningOf(m) {
|
|
717
|
+
if (typeof m.reasoning_content === "string" && m.reasoning_content) return m.reasoning_content
|
|
718
|
+
if (Array.isArray(m.reasoning_details)) {
|
|
719
|
+
const text = m.reasoning_details.map((d) => (typeof d?.text === "string" ? d.text : typeof d?.summary === "string" ? d.summary : "")).join("")
|
|
720
|
+
if (text) return text
|
|
721
|
+
}
|
|
722
|
+
return ""
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
function asReasoning(messages) {
|
|
726
|
+
let changed = false
|
|
727
|
+
for (const m of messages) {
|
|
728
|
+
if (m?.role !== "assistant" || (m.reasoning_content === undefined && m.reasoning_details === undefined)) continue
|
|
729
|
+
const text = reasoningOf(m)
|
|
730
|
+
if (text && !(typeof m.reasoning === "string" && m.reasoning)) m.reasoning = text
|
|
731
|
+
delete m.reasoning_content
|
|
732
|
+
delete m.reasoning_details
|
|
733
|
+
changed = true
|
|
734
|
+
}
|
|
735
|
+
return changed
|
|
736
|
+
}
|
|
737
|
+
|
|
702
738
|
function withSystem(body) {
|
|
703
739
|
// a body magpie or OpenCode hands over as bytes is read as the text it is
|
|
704
740
|
if (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) body = new TextDecoder().decode(body)
|
|
@@ -717,6 +753,7 @@ function withSystem(body) {
|
|
|
717
753
|
m.role = "system"
|
|
718
754
|
changed = true
|
|
719
755
|
}
|
|
756
|
+
if (asReasoning(b.messages)) changed = true
|
|
720
757
|
if (b.messages[0]?.role !== "system") {
|
|
721
758
|
b.messages.unshift({ role: "system", content: "You are a helpful assistant." })
|
|
722
759
|
changed = true
|
|
@@ -2027,6 +2064,29 @@ function replyAfterTools(body) {
|
|
|
2027
2064
|
return JSON.stringify(b)
|
|
2028
2065
|
}
|
|
2029
2066
|
|
|
2067
|
+
// FREE_ONLY is the switch that keeps an account off credits (#430): its
|
|
2068
|
+
// list holds only the models WorkBuddy serves at no cost (priceOf's free),
|
|
2069
|
+
// and a request for any other is answered here, never sent. A price that
|
|
2070
|
+
// can't be read is not free, and a list that can't be read sends nothing.
|
|
2071
|
+
const FREE_ONLY = "free_only"
|
|
2072
|
+
// PRICE_TTL is how long a list read for that check is used before it is
|
|
2073
|
+
// read again: a promotion that ends stops counting as free within it.
|
|
2074
|
+
const PRICE_TTL = 60_000
|
|
2075
|
+
|
|
2076
|
+
// notFree is the answer to a request the switch keeps from WorkBuddy. Its
|
|
2077
|
+
// words say the model, not the account, is what is turned away, so magpie
|
|
2078
|
+
// rests that model alone and asks another provider of it; they name no
|
|
2079
|
+
// quota or credit, which would rest the whole account as out of credit.
|
|
2080
|
+
function notFree(site, model, why) {
|
|
2081
|
+
const msg = why
|
|
2082
|
+
? `${site.name}'s model list couldn't be read (${why}), so whether model ${model || "(none)"} is free is unknown; with "Free models only" on it isn't sent`
|
|
2083
|
+
: `model ${model || "(none)"} is not available while "Free models only" is on: ${site.name} charges for it. Turn the switch off in ${site.name}'s provider settings to use it`
|
|
2084
|
+
return new Response(JSON.stringify({ error: { message: msg, type: "invalid_request_error", code: why ? "free_only_unknown" : "model_not_found" } }), {
|
|
2085
|
+
status: why ? 503 : 404,
|
|
2086
|
+
headers: { "content-type": "application/json", "X-Magpie-Sign-In": "kept" },
|
|
2087
|
+
})
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2030
2090
|
function makePlugin(site) {
|
|
2031
2091
|
// the models of this site that refused tool results then the user's
|
|
2032
2092
|
// message, for this run: their next chats go with replyAfterTools at once
|
|
@@ -2034,7 +2094,30 @@ function makePlugin(site) {
|
|
|
2034
2094
|
// the system prompts this run WorkBuddy refused as an unapproved channel:
|
|
2035
2095
|
// their next chats go with systemAsUser at once, not refused first
|
|
2036
2096
|
const refusedSystems = new Set()
|
|
2037
|
-
return async ({ client }) =>
|
|
2097
|
+
return async ({ client, magpie }) => {
|
|
2098
|
+
// each account's list as last read, for FREE_ONLY: {at, list}
|
|
2099
|
+
const lists = new Map()
|
|
2100
|
+
const listKey = (a) => `${a.uid ?? ""}|${a.enterpriseId ?? ""}|${a.source === "key" ? a.key : ""}`
|
|
2101
|
+
// listed is the account's live list, read again past PRICE_TTL
|
|
2102
|
+
const listed = async (a, fresh = false) => {
|
|
2103
|
+
const k = listKey(a)
|
|
2104
|
+
const was = lists.get(k)
|
|
2105
|
+
if (!fresh && was && Date.now() - was.at < PRICE_TTL) return was.list
|
|
2106
|
+
const list = await liveModels(site, a)
|
|
2107
|
+
lists.set(k, { at: Date.now(), list })
|
|
2108
|
+
if (lists.size > 64) lists.delete(lists.keys().next().value)
|
|
2109
|
+
return list
|
|
2110
|
+
}
|
|
2111
|
+
const freeOnly = () => magpie?.option?.(site.id, FREE_ONLY) === true
|
|
2112
|
+
// onlyFree is a list with the switch on: its free models, or a failure
|
|
2113
|
+
// when there are none to tell (magpie then keeps the list it had)
|
|
2114
|
+
const onlyFree = (ms) => {
|
|
2115
|
+
if (!freeOnly()) return ms
|
|
2116
|
+
const free = ms.filter((m) => m.free)
|
|
2117
|
+
if (!free.length) throw new Error(`${site.name} lists no free model, and "Free models only" is on`)
|
|
2118
|
+
return free
|
|
2119
|
+
}
|
|
2120
|
+
return {
|
|
2038
2121
|
config: async (config) => {
|
|
2039
2122
|
config.provider ??= {}
|
|
2040
2123
|
config.provider[site.id] ??= {}
|
|
@@ -2059,23 +2142,34 @@ function makePlugin(site) {
|
|
|
2059
2142
|
if (auth?.type === "api") {
|
|
2060
2143
|
// asked as WorkBuddy's own CLI asks with a key (remoteConfig)
|
|
2061
2144
|
const a = keyAccount(auth)
|
|
2062
|
-
if (!a || !remoteConfig(a.key))
|
|
2145
|
+
if (!a || !remoteConfig(a.key)) {
|
|
2146
|
+
if (freeOnly()) throw new Error(`${site.name} lists no prices for this key, and "Free models only" is on`)
|
|
2147
|
+
return provider.models
|
|
2148
|
+
}
|
|
2149
|
+
let ms
|
|
2063
2150
|
try {
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2151
|
+
ms = await listed(a, true)
|
|
2152
|
+
} catch (e) {
|
|
2153
|
+
if (freeOnly()) throw e
|
|
2067
2154
|
return provider.models
|
|
2068
2155
|
}
|
|
2156
|
+
if (!ms.length && !freeOnly()) return provider.models
|
|
2157
|
+
return Object.fromEntries(onlyFree(ms).map((m) => [m.id, modelOf(site, provider, m)]))
|
|
2069
2158
|
}
|
|
2070
2159
|
if (auth?.type !== "oauth" || !(auth.access || auth.source)) return provider.models
|
|
2160
|
+
let ms
|
|
2071
2161
|
try {
|
|
2072
2162
|
const a = await current(site, client, auth)
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2163
|
+
ms = await listed(a, true)
|
|
2164
|
+
} catch (e) {
|
|
2165
|
+
// with the switch on, a list without prices would offer models
|
|
2166
|
+
// that cost credits: the failure goes up, and magpie keeps the
|
|
2167
|
+
// list it had
|
|
2168
|
+
if (freeOnly()) throw e
|
|
2077
2169
|
return provider.models
|
|
2078
2170
|
}
|
|
2171
|
+
if (!ms.length && !freeOnly()) return provider.models
|
|
2172
|
+
return Object.fromEntries(onlyFree(ms).map((m) => [m.id, modelOf(site, provider, m)]))
|
|
2079
2173
|
},
|
|
2080
2174
|
},
|
|
2081
2175
|
auth: {
|
|
@@ -2087,6 +2181,13 @@ function makePlugin(site) {
|
|
|
2087
2181
|
// and is renewed as before.
|
|
2088
2182
|
refreshLead: LEAD_MS,
|
|
2089
2183
|
refresh: (auth) => refreshed(site, auth),
|
|
2184
|
+
options: [
|
|
2185
|
+
{
|
|
2186
|
+
key: FREE_ONLY,
|
|
2187
|
+
label: "Free models only",
|
|
2188
|
+
description: `Lists only the models ${site.name} serves at no cost, and turns away a request for any other here instead of sending it, so no credits are spent. A model free only at night counts as paid.`,
|
|
2189
|
+
},
|
|
2190
|
+
],
|
|
2090
2191
|
async loader(getAuth) {
|
|
2091
2192
|
const auth = await getAuth()
|
|
2092
2193
|
if (auth?.type !== "oauth" && auth?.type !== "api") return {}
|
|
@@ -2110,6 +2211,18 @@ function makePlugin(site) {
|
|
|
2110
2211
|
sign(site, a, headers)
|
|
2111
2212
|
let body = init?.body
|
|
2112
2213
|
if (body === undefined && req) body = await req.clone().text()
|
|
2214
|
+
// the switch is checked before anything is sent: a model not
|
|
2215
|
+
// free, or one whose price can't be read, never reaches WorkBuddy
|
|
2216
|
+
if (freeOnly()) {
|
|
2217
|
+
const model = chatOf(typeof body === "string" ? body : bodyString(body))?.model
|
|
2218
|
+
let list
|
|
2219
|
+
try {
|
|
2220
|
+
list = await listed(a)
|
|
2221
|
+
} catch (e) {
|
|
2222
|
+
return notFree(site, model, e?.message ?? String(e))
|
|
2223
|
+
}
|
|
2224
|
+
if (!list.some((m) => m.id === model && m.free)) return notFree(site, model)
|
|
2225
|
+
}
|
|
2113
2226
|
// An Anthropic request goes as the agent sent it: WorkBuddy
|
|
2114
2227
|
// serves these models on Messages itself, and the rewrites
|
|
2115
2228
|
// below (a default system message, the chat endpoint's
|
|
@@ -2304,7 +2417,8 @@ function makePlugin(site) {
|
|
|
2304
2417
|
},
|
|
2305
2418
|
],
|
|
2306
2419
|
},
|
|
2307
|
-
}
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2308
2422
|
}
|
|
2309
2423
|
|
|
2310
2424
|
// REFUSED_HINT is what the user can do about WorkBuddy's "Illegal API
|
|
@@ -2409,4 +2523,4 @@ export const WorkBuddyAuthPlugin = makePlugin(SITES.workbuddy)
|
|
|
2409
2523
|
export const WorkBuddyAIAuthPlugin = makePlugin(SITES["workbuddy-ai"])
|
|
2410
2524
|
|
|
2411
2525
|
// for tests
|
|
2412
|
-
export const _internal = { 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, 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 }
|
|
2526
|
+
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 }
|
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.39",
|
|
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",
|