@magpie-community/opencode-workbuddy-auth 0.1.37 → 0.1.38
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 +16 -0
- package/index.mjs +94 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -228,6 +228,22 @@ some hours of the day (Hy4 preview, free 23:00–8:00) is given as its full
|
|
|
228
228
|
price, never as free: magpie keeps the list until it asks again, so a list
|
|
229
229
|
asked for at night would say "free" all day (#121).
|
|
230
230
|
|
|
231
|
+
### Free models only
|
|
232
|
+
|
|
233
|
+
Each site (WorkBuddy, WorkBuddy AI) has a "Free models only" switch in its
|
|
234
|
+
provider settings in magpie, off by default (#430). When it is on:
|
|
235
|
+
|
|
236
|
+
- the list holds only the models the site serves at no cost now. A model free
|
|
237
|
+
only at night, or one with no price, counts as paid;
|
|
238
|
+
- a request for any other model is answered by the plugin with a 404
|
|
239
|
+
`model_not_found` and never sent, so no credits are spent. magpie rests
|
|
240
|
+
only that model and tries the next provider;
|
|
241
|
+
- if the price list can't be read, nothing is sent (503), since whether the
|
|
242
|
+
model is free is unknown. An API key WorkBuddy lists no prices for lists
|
|
243
|
+
no models.
|
|
244
|
+
|
|
245
|
+
The price list is read again at most once a minute for requests.
|
|
246
|
+
|
|
231
247
|
## Credits in magpie
|
|
232
248
|
|
|
233
249
|
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 {
|
|
@@ -2027,6 +2033,29 @@ function replyAfterTools(body) {
|
|
|
2027
2033
|
return JSON.stringify(b)
|
|
2028
2034
|
}
|
|
2029
2035
|
|
|
2036
|
+
// FREE_ONLY is the switch that keeps an account off credits (#430): its
|
|
2037
|
+
// list holds only the models WorkBuddy serves at no cost (priceOf's free),
|
|
2038
|
+
// and a request for any other is answered here, never sent. A price that
|
|
2039
|
+
// can't be read is not free, and a list that can't be read sends nothing.
|
|
2040
|
+
const FREE_ONLY = "free_only"
|
|
2041
|
+
// PRICE_TTL is how long a list read for that check is used before it is
|
|
2042
|
+
// read again: a promotion that ends stops counting as free within it.
|
|
2043
|
+
const PRICE_TTL = 60_000
|
|
2044
|
+
|
|
2045
|
+
// notFree is the answer to a request the switch keeps from WorkBuddy. Its
|
|
2046
|
+
// words say the model, not the account, is what is turned away, so magpie
|
|
2047
|
+
// rests that model alone and asks another provider of it; they name no
|
|
2048
|
+
// quota or credit, which would rest the whole account as out of credit.
|
|
2049
|
+
function notFree(site, model, why) {
|
|
2050
|
+
const msg = why
|
|
2051
|
+
? `${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`
|
|
2052
|
+
: `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`
|
|
2053
|
+
return new Response(JSON.stringify({ error: { message: msg, type: "invalid_request_error", code: why ? "free_only_unknown" : "model_not_found" } }), {
|
|
2054
|
+
status: why ? 503 : 404,
|
|
2055
|
+
headers: { "content-type": "application/json", "X-Magpie-Sign-In": "kept" },
|
|
2056
|
+
})
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2030
2059
|
function makePlugin(site) {
|
|
2031
2060
|
// the models of this site that refused tool results then the user's
|
|
2032
2061
|
// message, for this run: their next chats go with replyAfterTools at once
|
|
@@ -2034,7 +2063,30 @@ function makePlugin(site) {
|
|
|
2034
2063
|
// the system prompts this run WorkBuddy refused as an unapproved channel:
|
|
2035
2064
|
// their next chats go with systemAsUser at once, not refused first
|
|
2036
2065
|
const refusedSystems = new Set()
|
|
2037
|
-
return async ({ client }) =>
|
|
2066
|
+
return async ({ client, magpie }) => {
|
|
2067
|
+
// each account's list as last read, for FREE_ONLY: {at, list}
|
|
2068
|
+
const lists = new Map()
|
|
2069
|
+
const listKey = (a) => `${a.uid ?? ""}|${a.enterpriseId ?? ""}|${a.source === "key" ? a.key : ""}`
|
|
2070
|
+
// listed is the account's live list, read again past PRICE_TTL
|
|
2071
|
+
const listed = async (a, fresh = false) => {
|
|
2072
|
+
const k = listKey(a)
|
|
2073
|
+
const was = lists.get(k)
|
|
2074
|
+
if (!fresh && was && Date.now() - was.at < PRICE_TTL) return was.list
|
|
2075
|
+
const list = await liveModels(site, a)
|
|
2076
|
+
lists.set(k, { at: Date.now(), list })
|
|
2077
|
+
if (lists.size > 64) lists.delete(lists.keys().next().value)
|
|
2078
|
+
return list
|
|
2079
|
+
}
|
|
2080
|
+
const freeOnly = () => magpie?.option?.(site.id, FREE_ONLY) === true
|
|
2081
|
+
// onlyFree is a list with the switch on: its free models, or a failure
|
|
2082
|
+
// when there are none to tell (magpie then keeps the list it had)
|
|
2083
|
+
const onlyFree = (ms) => {
|
|
2084
|
+
if (!freeOnly()) return ms
|
|
2085
|
+
const free = ms.filter((m) => m.free)
|
|
2086
|
+
if (!free.length) throw new Error(`${site.name} lists no free model, and "Free models only" is on`)
|
|
2087
|
+
return free
|
|
2088
|
+
}
|
|
2089
|
+
return {
|
|
2038
2090
|
config: async (config) => {
|
|
2039
2091
|
config.provider ??= {}
|
|
2040
2092
|
config.provider[site.id] ??= {}
|
|
@@ -2059,23 +2111,34 @@ function makePlugin(site) {
|
|
|
2059
2111
|
if (auth?.type === "api") {
|
|
2060
2112
|
// asked as WorkBuddy's own CLI asks with a key (remoteConfig)
|
|
2061
2113
|
const a = keyAccount(auth)
|
|
2062
|
-
if (!a || !remoteConfig(a.key))
|
|
2114
|
+
if (!a || !remoteConfig(a.key)) {
|
|
2115
|
+
if (freeOnly()) throw new Error(`${site.name} lists no prices for this key, and "Free models only" is on`)
|
|
2116
|
+
return provider.models
|
|
2117
|
+
}
|
|
2118
|
+
let ms
|
|
2063
2119
|
try {
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2120
|
+
ms = await listed(a, true)
|
|
2121
|
+
} catch (e) {
|
|
2122
|
+
if (freeOnly()) throw e
|
|
2067
2123
|
return provider.models
|
|
2068
2124
|
}
|
|
2125
|
+
if (!ms.length && !freeOnly()) return provider.models
|
|
2126
|
+
return Object.fromEntries(onlyFree(ms).map((m) => [m.id, modelOf(site, provider, m)]))
|
|
2069
2127
|
}
|
|
2070
2128
|
if (auth?.type !== "oauth" || !(auth.access || auth.source)) return provider.models
|
|
2129
|
+
let ms
|
|
2071
2130
|
try {
|
|
2072
2131
|
const a = await current(site, client, auth)
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2132
|
+
ms = await listed(a, true)
|
|
2133
|
+
} catch (e) {
|
|
2134
|
+
// with the switch on, a list without prices would offer models
|
|
2135
|
+
// that cost credits: the failure goes up, and magpie keeps the
|
|
2136
|
+
// list it had
|
|
2137
|
+
if (freeOnly()) throw e
|
|
2077
2138
|
return provider.models
|
|
2078
2139
|
}
|
|
2140
|
+
if (!ms.length && !freeOnly()) return provider.models
|
|
2141
|
+
return Object.fromEntries(onlyFree(ms).map((m) => [m.id, modelOf(site, provider, m)]))
|
|
2079
2142
|
},
|
|
2080
2143
|
},
|
|
2081
2144
|
auth: {
|
|
@@ -2087,6 +2150,13 @@ function makePlugin(site) {
|
|
|
2087
2150
|
// and is renewed as before.
|
|
2088
2151
|
refreshLead: LEAD_MS,
|
|
2089
2152
|
refresh: (auth) => refreshed(site, auth),
|
|
2153
|
+
options: [
|
|
2154
|
+
{
|
|
2155
|
+
key: FREE_ONLY,
|
|
2156
|
+
label: "Free models only",
|
|
2157
|
+
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.`,
|
|
2158
|
+
},
|
|
2159
|
+
],
|
|
2090
2160
|
async loader(getAuth) {
|
|
2091
2161
|
const auth = await getAuth()
|
|
2092
2162
|
if (auth?.type !== "oauth" && auth?.type !== "api") return {}
|
|
@@ -2110,6 +2180,18 @@ function makePlugin(site) {
|
|
|
2110
2180
|
sign(site, a, headers)
|
|
2111
2181
|
let body = init?.body
|
|
2112
2182
|
if (body === undefined && req) body = await req.clone().text()
|
|
2183
|
+
// the switch is checked before anything is sent: a model not
|
|
2184
|
+
// free, or one whose price can't be read, never reaches WorkBuddy
|
|
2185
|
+
if (freeOnly()) {
|
|
2186
|
+
const model = chatOf(typeof body === "string" ? body : bodyString(body))?.model
|
|
2187
|
+
let list
|
|
2188
|
+
try {
|
|
2189
|
+
list = await listed(a)
|
|
2190
|
+
} catch (e) {
|
|
2191
|
+
return notFree(site, model, e?.message ?? String(e))
|
|
2192
|
+
}
|
|
2193
|
+
if (!list.some((m) => m.id === model && m.free)) return notFree(site, model)
|
|
2194
|
+
}
|
|
2113
2195
|
// An Anthropic request goes as the agent sent it: WorkBuddy
|
|
2114
2196
|
// serves these models on Messages itself, and the rewrites
|
|
2115
2197
|
// below (a default system message, the chat endpoint's
|
|
@@ -2304,7 +2386,8 @@ function makePlugin(site) {
|
|
|
2304
2386
|
},
|
|
2305
2387
|
],
|
|
2306
2388
|
},
|
|
2307
|
-
}
|
|
2389
|
+
}
|
|
2390
|
+
}
|
|
2308
2391
|
}
|
|
2309
2392
|
|
|
2310
2393
|
// REFUSED_HINT is what the user can do about WorkBuddy's "Illegal API
|
|
@@ -2409,4 +2492,4 @@ export const WorkBuddyAuthPlugin = makePlugin(SITES.workbuddy)
|
|
|
2409
2492
|
export const WorkBuddyAIAuthPlugin = makePlugin(SITES["workbuddy-ai"])
|
|
2410
2493
|
|
|
2411
2494
|
// 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 }
|
|
2495
|
+
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, 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.38",
|
|
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",
|