Files
magpie/internal/plugin/testdata/fake/index.js
T
yetone 33acb6bca0 providers: each model's credit rate shows by it in the provider's model list and an agent's model picker, Qoder's 0.5× and WorkBuddy's 0.03×, a discount's price before it struck through (01huadalang on Discord: qcoder 或者 workbuddy 能不能实时抓取或者按一段时间抓取能展示列表模型的积分消耗倍率,这样不用打开软件查看,能选择到便宜的模型) — magpie read each model's price only to tell free from not: Qoder's price_factor (06584e15) and WorkBuddy's "credits" ("x0.03", wbFreeCredits), so the lists said FREE or nothing, and picking a cheap model meant opening Qoder or WorkBuddy. catalog.Model now keeps the price, Rate (the credits a request costs, as a multiple; 0 is not listed, or Free) and RateWas (the price before a discount running now), and carries both through provider.Entry, agent.Option and the providers page's models. Qoder (and Qoder CN): Rate is the price_factor; RateWas an active promotion's before_promotion_price_factor, else an original_price_factor above the price (Qwen3.8-Flash: free, 0.1× struck through), and a promotion's 0 that isn't free is its price before times its discount_factor. WorkBuddy and WorkBuddy AI (the CodeBuddy plan; both builds read the same /v3/config): Rate is the credits (wbCredits; x0.00 stays FREE, an unreadable or infinite rate is none). A plugin's model gives it as it gives free: rate and rateWas on the model its provider.models hook lists, a number (0.5) or as the vendor writes it ("x0.03", "0.5×"), which host.js reads (rateOf) into plugin.Model.Rate/RateWas and pluginCatalog keeps; the moved Qoder and WorkBuddy users get it once their plugins send it. The badge is small and grey (the context badge's colours, no uppercase), beside FREE where that shows, its title saying what a request costs (每次请求消耗 0.5× 积分;折扣前 / 限时免费,原价). Rates are as of the provider's last list: a built-in's is fetched at start-up when it has none, on Refresh and when its key changes, a plugin's when its plugins are asked again (start-up, a sign-in, a plugin change); nothing asks on a timer. Kiro, Cursor, ZCode and Devin list no per-model rate magpie reads, so they show none. TestParseModelsRate (qoder) and TestWBCreditsRate (provider) are new and don't build without the change; TestFakePlugin now checks a plugin's rate given as a number and as "x0.03" (0 for both with host.js not reading them). model-rate.test.cjs (WebKit, en and zh: the chips' badges and titles, struck-through prices, grey not green, no stripe, a click picks and scrolls nothing, the picker's badges) fails without the change and passes with it; click-scroll, models-free-only, model-pick and favorite-star pass in WebKit. No request was made to Qoder, WorkBuddy or any vendor. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME, go test ./internal/gui pass.
2026-10-02 22:48:45 +08:00

151 lines
7.6 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 },
{ 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
},
})