Files
magpie/internal/plugin/testdata/fake/index.js
T
yetone a999770531 plugins: a plugin's subscription does what a built-in's does, in seven places it didn't (the user: 要保证插件订阅 provider 跟内置的订阅 provider所有逻辑都要一模一样) — an installed plugin's provider id is kept for it signed in or not, as a built-in's is: a provider of the user's own isn't saved on it, renamed onto it or imported as it (freeID gives id-2). A plugin's account whose plan lacks the model is tried after the ones that list it, as a Codex Free account behind a Plus, by the list the plugin told for it. Cursor's plugin has fast mode where the built-in Cursor has it: a model with a -fast one is saved :fast in a group and sent service_tier "priority", one without is refused. Removing a plugin's provider hides it as a built-in's is, its accounts kept and shown again from Hidden or by signing in, rather than signing every account out ("stays signed in" said so already). magpie accounts lists, adds, switches and forgets a plugin's accounts by its provider's id or name. Grok's plugin beside the built-in Grok (grok-plugin) searches by itself as Grok does. A plugin's plan is named as its built-in's ("Cursor Pro", "SuperGrok"), and a plugin's resets are shown but have no Use button or Auto-use, which spent Codex's reset of the same user. TestPluginIDReservedSignedOut, TestPluginAccountLackingTheModelGoesLast, TestCursorPluginGroupFast, TestAccountsOfAPlugin, TestGrokPluginSearchesItself, plugin-plan and codex-auto-reset fail without the change; TestPluginSignIn and TestPluginAccounts now expect a removed plugin hidden. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (TestClaudeSignedOut failed once under load, passes alone); the gateway, provider and plugin suites and TestAccountsOfAPlugin pass on Windows, the plugin tests on Linux.
2026-10-01 20:26:55 +08:00

147 lines
7.3 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
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
},
})