Moody-Sin: the Routing page showed a WorkBuddy account as "已用 71%" only, which for a plan counted in credits says little. A quota window now carries its own count when the vendor counts in amounts — amount of limit, in unit (QuotaWindow, QuotaSpan for quota --json and /v1/magpie/quotas, and the routing trace's Weighed, from the window its share is of: Allowance.Count). WorkBuddy's built-in fills it from the resource summary, and @magpie-community/opencode-workbuddy-auth 0.1.8 tells it from auth.usage (amount / limit / unit, passed through host.js); the workbuddy mover's min is 0.1.8. The Routing page's stage and its list of what each account did, the Usage page's meter, the menu bar's tooltip and the TUI say "3,550 / 5,000 credits · 71% used", or "1,450 / 5,000 credits · 29% left" with Left on (积分 in Chinese); a window counted in shares alone reads as before. quota-credits.test.cjs (WebKit, en/zh, used/left, Routing and Usage pages) fails on the old assets and passes; TestQuotaWindowCount, TestAllowanceCount, TestTraceTellsTheCount, TestPluginUsage (the count through the host) and TestWorkBuddyAccounts cover the Go side. Tried with the real WorkBuddy and WorkBuddy AI accounts in a sandbox magpie: moved onto the local plugin, /api/usage/quotas gives 33.29 / 3900 and 13.47 / 2250 credits and the Usage page reads "33.29 / 3,900 积分 · 已用 0.9%", "3,866.71 / 3,900 积分 · 剩余 99.1%" after a click on the count; not moved, the built-in gives the same for WorkBuddy. The Routing page with two real accounts of one provider wasn't tried (one account each). go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass.
151 lines
7.7 KiB
JavaScript
151 lines
7.7 KiB
JavaScript
// A plugin as OpenCode's are written, signing in to a made-up provider
|
|
// whose requests go to $FAKE_BASE; $FAKE_ID names it another's id, as a
|
|
// built-in's plugin does.
|
|
const ID = process.env.FAKE_ID || "fakeco"
|
|
|
|
// fakeWho is who a team signs in as: team@fake, and a team named name/uid
|
|
// is name@fake with that uid, as WorkBuddy's plugin keeps one
|
|
function fakeWho(team) {
|
|
const [name, uid] = (team ?? "me").split("/")
|
|
return uid ? { accountId: name + "@fake", uid } : { accountId: name + "@fake" }
|
|
}
|
|
|
|
export const FakePlugin = async ({ client }) => ({
|
|
config: async (cfg) => {
|
|
cfg.provider = cfg.provider ?? {}
|
|
cfg.provider[ID] = {
|
|
name: "FakeCo",
|
|
npm: "@ai-sdk/openai-compatible",
|
|
api: "https://fake.invalid/v1",
|
|
models: {
|
|
"fake-1": { name: "Fake One", limit: { context: 1000, output: 100 } },
|
|
"fake-claude": { name: "Fake Claude", provider: { npm: "@ai-sdk/anthropic" }, modalities: { input: ["text", "image"], output: ["text"] }, limit: { context: 2000, output: 200 } },
|
|
"fake-gemini": { name: "Fake Gemini", provider: { npm: "@ai-sdk/google" }, reasoning: true, modalities: { input: ["text"], output: ["text"] }, limit: { context: 3000, output: 300 } },
|
|
},
|
|
}
|
|
// $FAKE_RESPONSES: one model more, on OpenAI's Responses (Grok's)
|
|
// $FAKE_FAST: fake-1 has a fast one, as a Cursor model has its -fast
|
|
if (process.env.FAKE_FAST) cfg.provider[ID].models["fake-1-fast"] = { name: "Fake One Fast", limit: { context: 1000, output: 100 } }
|
|
if (process.env.FAKE_RESPONSES) cfg.provider[ID].models["fake-resp"] = { name: "Fake Responses", provider: { npm: "@ai-sdk/openai" }, limit: { context: 4000, output: 400 } }
|
|
},
|
|
auth: {
|
|
provider: ID,
|
|
methods: [
|
|
{ type: "api", label: "API key" },
|
|
{
|
|
type: "oauth",
|
|
label: "Browser",
|
|
prompts: [
|
|
{ type: "select", key: "where", message: "Where?", options: [{ label: "Home", value: "home" }, { label: "Work", value: "work" }] },
|
|
{ type: "text", key: "team", message: "Team?", when: { key: "where", op: "eq", value: "work" }, validate: (v) => (v ? undefined : "Required") },
|
|
],
|
|
authorize: async (inputs) => ({
|
|
url: "https://fake.invalid/auth?where=" + inputs.where,
|
|
instructions: "Paste the code",
|
|
method: "code",
|
|
callback: async (code) =>
|
|
code === "good"
|
|
? { type: "success", refresh: "r-" + (inputs.team ?? "none"), access: "stale", expires: 0, ...fakeWho(inputs.team) }
|
|
: code === "expired"
|
|
? { type: "failed", error: "the sign-in page expired" }
|
|
: { type: "failed" },
|
|
}),
|
|
},
|
|
],
|
|
loader: async (getAuth, provider) => ({
|
|
apiKey: "dummy",
|
|
baseURL: process.env.FAKE_BASE,
|
|
async fetch(url, init) {
|
|
let a = await getAuth()
|
|
// "r-revoked": the vendor turned the refresh away, said as Zed's says it
|
|
if (a.type === "oauth" && a.refresh === "r-revoked") throw Object.assign(new Error("FakeCo turned the sign-in away"), { signIn: "expired" })
|
|
if (a.type === "oauth" && a.expires < Date.now()) {
|
|
a = { ...a, access: "fresh-" + a.refresh, expires: Date.now() + 3600e3 }
|
|
await client.auth.set({ path: { id: ID }, body: a })
|
|
}
|
|
const h = new Headers(init.headers)
|
|
h.set("authorization", "Bearer " + (a.type === "oauth" ? a.access : a.key))
|
|
h.set("x-models", Object.keys(provider.models).sort().join(","))
|
|
h.set("x-body-type", typeof init.body)
|
|
return fetch(url, { ...init, headers: h })
|
|
},
|
|
}),
|
|
// magpie's: the account's allowance; "full@fake" has used its five
|
|
// hours, which count only fake-claude
|
|
usage: async (getAuth) => {
|
|
const a = await getAuth()
|
|
// $FAKE_USAGE: the plan is what the vendor's page there says
|
|
if (process.env.FAKE_USAGE) {
|
|
const r = await fetch(process.env.FAKE_USAGE)
|
|
return { plan: await r.text() }
|
|
}
|
|
if (a.type !== "oauth") return { error: "an API key has no plan" }
|
|
if (a.refresh === "r-gone") return { error: `${a.accountId}: the FakeCo sign-in has expired — sign in again` }
|
|
if (a.refresh === "r-offline") throw new Error("fetch failed")
|
|
// the vendor unreachable, said as Zed's plugin says it
|
|
if (a.refresh === "r-unreachable") {
|
|
try {
|
|
await fetch("http://127.0.0.1:9/me")
|
|
} catch (e) {
|
|
return { error: e?.message ?? String(e), signIn: "kept" }
|
|
}
|
|
}
|
|
if (a.refresh === "r-kept") return { error: "sign in again to see usage", signIn: "kept" }
|
|
if (a.refresh === "r-renewed") return { error: "usage is down", signIn: "renewed" }
|
|
const full = a.accountId === "full@fake"
|
|
return {
|
|
plan: "Fake Pro",
|
|
user: full ? "Full@Fake.example" : undefined,
|
|
until: "2030-01-02T03:04:05Z",
|
|
renew: "auto",
|
|
resets: full ? { count: 3, byWindow: true, fiveHour: 2, weekly: 1 } : undefined,
|
|
windows: [
|
|
{ name: "5 hours", used: full ? 100 : 25, resetsAt: Date.now() + 3600e3, span: 5 * 3600, models: ["fake-claude"] },
|
|
{ name: "Week", used: 10, resetsAt: Math.floor(Date.now() / 1000) + 86400, span: 7 * 86400, amount: 120, limit: 1200, unit: "credits" },
|
|
{ name: "Extra", used: 250, display: "$2.50", aside: true },
|
|
],
|
|
}
|
|
},
|
|
},
|
|
// the models an account has: refused for a dead one; a "rot-" sign-in
|
|
// spends its refresh token asking, as a rotating one does
|
|
provider: {
|
|
id: ID,
|
|
models: async (p, { auth }) => {
|
|
if (auth?.refresh === "r-dead" || auth?.key === "dead") throw new Error("the vendor refused the sign-in")
|
|
if (auth?.refresh === "r-models-gone") throw Object.assign(new Error("the vendor refused the sign-in"), { signIn: "expired" })
|
|
// a sign-in past its time, said in words only, as Grok's plugin says it
|
|
if (auth?.refresh === "r-models-expired") throw new Error("FakeCo's sign-in has expired; run `fake login`")
|
|
// the vendor unreachable: the list kept, as Zed's plugin keeps it
|
|
if (auth?.refresh === "r-unreachable") await fetch("http://127.0.0.1:9/models").catch(() => {})
|
|
if (auth?.type === "oauth" && auth.refresh?.startsWith("rot-")) {
|
|
await client.auth.set({ path: { id: ID }, body: { ...auth, refresh: auth.refresh + "+" } })
|
|
}
|
|
// fake-1 costs the plan nothing, as a WorkBuddy model of x0.00 credits
|
|
p.models["fake-1"].free = true
|
|
// fake-claude's credits are a number, discounted; fake-gemini's as
|
|
// WorkBuddy's picker writes them
|
|
if (p.models["fake-claude"]) Object.assign(p.models["fake-claude"], { rate: 0.5, rateWas: 1 })
|
|
if (p.models["fake-gemini"]) p.models["fake-gemini"].rate = "x0.03"
|
|
if (auth?.key === "few") return { "fake-1": p.models["fake-1"] }
|
|
// $FAKE_MODELS: the vendor's list, whose answer names one more model
|
|
if (process.env.FAKE_MODELS && auth) {
|
|
const r = await fetch(process.env.FAKE_MODELS).then((r) => r.text()).catch((e) => {
|
|
// $FAKE_MODELS_THROW: the hook throws, as Cursor's, Grok's and Devin's do
|
|
if (process.env.FAKE_MODELS_THROW === "1") throw e
|
|
return null
|
|
})
|
|
// "own": it hands back a table of its own, saying it fell back, as
|
|
// Command Code's Go and ZCode do
|
|
if (r === null && process.env.FAKE_MODELS_THROW === "own")
|
|
return { "fake-1": { ...p.models["fake-1"] }, [Symbol.for("magpie.fellBack")]: true }
|
|
if (r) p.models["fake-" + r] = { ...p.models["fake-1"], id: "fake-" + r, name: r }
|
|
}
|
|
return p.models
|
|
},
|
|
},
|
|
"chat.headers": async (input, output) => {
|
|
if (input.model.providerID === ID) output.headers["x-plugin-model"] = input.model.id
|
|
},
|
|
})
|