ドキュメント · プラグイン
プラグインを書く
プラグインはサービスにサインインし、そのサービスへのリクエストを送ります。magpie はそれを、すべてのエージェントが使えるプロバイダに変えます。複数アカウント、フェイルオーバー、使用量表示なども備わります。magpie は OpenCode のプロバイダプラグインを動かすので、OpenCode 向けに書いたプラグインはそのままここで動きます。プロバイダを登録する pi のパッケージも動きます(pi パッケージを参照)。このページでは、プラグイン作者に必要なこと、つまり magpie が呼び出すフック、その戻り値を magpie がどう扱うか、そしてプラグインのテスト・公開・掲載の方法を説明します。
configはプロバイダとそのデフォルトのモデルを宣言します。authはサインイン方法と、各リクエストを送るfetchを持つloaderを提供します。provider.modelsはアカウントごとのモデルを列挙します。auth.usageはプランをどれだけ使ったかを返します。magpie 独自のフックで、OpenCode は無視します。auth.refreshはアカウントのトークンを期限切れ前に、一度にひとつずつ更新します。これも magpie 独自のフックです。
fetch に渡して、応答をストリームで返します。5 分でプラグインを作る
この例は架空のコーディングプラン Acme にサインインします。Acme は API キーを受け取り、OpenAI 互換のエンドポイントを持ち、アカウントごとのモデルと使用量を返します。実際のプラグインとの違いは主に、サインインの方法とリクエストへの署名の仕方です。この例はすべて magpie v0.1.676 と、Acme の API のローカルな代用品で実際に動かしています。
{
"name": "opencode-acme-auth",
"version": "0.1.0",
"description": "OpenCode provider plugin: Acme Coding Plan",
"type": "module",
"main": "./index.mjs",
"files": ["index.mjs", "README.md"],
"keywords": ["opencode", "opencode-plugin", "acme"],
"license": "MIT",
"magpie": { "maxConcurrency": 4 }
}
const PROVIDER = "acme" const BASE = "https://api.acme.example/v1" export const AcmeAuthPlugin = async ({ client }) => ({ // the provider and the models it has before anyone signs in config: async (cfg) => { cfg.provider ??= {} cfg.provider[PROVIDER] ??= { name: "Acme", npm: "@ai-sdk/openai-compatible", // chat completions api: BASE, models: { "acme-coder": { name: "Acme Coder", limit: { context: 200000, output: 32000 }, tool_call: true }, }, } }, auth: { provider: PROVIDER, methods: [{ type: "api", label: "Acme API key", placeholder: "acme-…" }], // what every request of this account is sent with async loader(getAuth) { const auth = await getAuth() if (auth?.type !== "api") return {} return { baseURL: BASE, async fetch(input, init) { const headers = new Headers(init?.headers) headers.set("authorization", `Bearer ${auth.key}`) headers.set("x-acme-client", "opencode-acme-auth/0.1.0") return fetch(input, { ...init, headers }) }, } }, // magpie's own hook: how much of the plan is used async usage(getAuth) { const auth = await getAuth() const res = await fetch(`${BASE}/usage`, { headers: { authorization: `Bearer ${auth.key}` } }) if (!res.ok) return { error: `Acme answered ${res.status}`, windows: [] } const u = await res.json() return { plan: u.plan, windows: [{ name: "5 hours", used: u.percent, resetsAt: u.resets_at, span: 5 * 3600 }], } }, }, // the models this account has, asked of Acme provider: { id: PROVIDER, async models(provider, { auth } = {}) { if (auth?.type !== "api") return provider.models const res = await fetch(`${BASE}/models`, { headers: { authorization: `Bearer ${auth.key}` } }) if (!res.ok) return provider.models // magpie keeps the list it had const { data } = await res.json() const base = provider.models["acme-coder"] return Object.fromEntries(data.map((m) => [m.id, { ...base, id: m.id, name: m.name ?? m.id }])) }, }, })
フォルダを magpie に追加し、サインインして試してみます:
magpie plugin add ./opencode-acme-auth # a folder is loaded where it is; nothing is installed magpie plugin login acme # asks for the key magpie plugin # acme (Acme, 2 models) signed in magpie provider test acme # ✓ chat 7 ms · acme-coder magpie quota # acme · Pro · 5h 37% ↻59m
これでエージェントは、ほかのプロバイダと同じように acme/acme-coder としてそのモデルを使えます。アプリでは プラグイン を開き、プラグインを追加 をクリックしてフォルダを指定します。その後、プロバイダの行からサインインします。
HOME で magpie を動かしてください。magpie がプラグインを動かす仕組み
- Bun。magpie は最初にプラグインが追加されたときに Bun をダウンロードし、すべてのプラグインをその上で動かします。Bun は TypeScript をそのまま実行するので、エントリは
index.tsでも構いません。 - ひとつのホストプロセス。有効なプラグインはすべて、ひとつの Bun プロセスに読み込まれます。このプロセスは最初に必要になったときに起動します。magpie とホストは stdin と stdout で、1 行にひとつの JSON メッセージをやり取りします。プラグインが出力するものは、
console.logやprocess.stdoutも含めてすべて stderr に送られ、magpie はplugin: …としてログに記録します。 - インストール。
- npm パッケージは
bun addでインストールされ、OpenCode と同じくインストールスクリプトは実行されません。postinstallでのビルドが必要な依存は動きません。 - git リポジトリは Bun が取得します:
github:owner/repo[#ref]、git+https://…、owner/repo。 - フォルダやファイルへのパスは、その場所から読み込まれます。
- npm パッケージは
- 再起動。プラグインの追加・削除・更新・有効化・無効化でホストが再起動します。古いホストでストリーム中の応答は先に最後まで流れます。ターミナルの
magpieコマンドはそれぞれ専用のホストを起動するので、フォルダのプラグインは常にその時点の内容で読み込まれます。アプリでは、プラグインをオフにしてからオンにすると編集内容が読み込まれます。 - プロキシ。プラグインがプロバイダやアカウントのために行う fetch は、ユーザーがそのプロバイダやアカウントに設定したプロキシを経由します。設定がなければ magpie 自身のプロキシを経由します(設定が直接接続の場合を除く)。ループバックアドレスはプロキシを通りません。独自の
proxyオプションを渡す fetch はそのままにされます。 - 更新。
- コミュニティのパッケージ(
@magpie-community/*)は、magpie の起動から少し後に自動で更新され、その後は 6 時間ごとに更新されます。 - そのほかのパッケージは、新しいバージョンがあると プラグイン にドットが表示され、ユーザーがクリックか
magpie plugin updateで更新します。 - バージョンを固定した指定(
name@1.2.3)はそのバージョンのままです。
- コミュニティのパッケージ(
magpie が設定フォルダ(~/.config/magpie または $XDG_CONFIG_HOME/magpie)に保存するもの:
| ファイル | 内容 |
|---|---|
plugins.json | 追加したプラグイン:{"plugins": [{"spec", "off"?, "options"?}], "config"?}。options はプラグインの第 2 引数で、OpenCode の ["name", {…}] が渡すものと同じです。config はプラグインに渡される OpenCode の設定です。 |
plugins/ | インストールされた npm と git のパッケージ。独立した Bun プロジェクトです。 |
plugin-auth.json | サインイン情報(モード 600)。OpenCode の auth.json と同じ形式なので、両者の間でサインインを移せます。 |
plugin-providers.json | プラグインが最後に列挙したプロバイダとモデル。magpie はこれを使い、プラグインに問い合わせずに起動できます。 |
パッケージ
magpie がどこから読み込むか
magpie はプラグインのサーバー側を OpenCode と同じ場所から探し、プラグインをエクスポートしている最初のファイルを読み込みます:
package.jsonのexports["./server"]、次にmain、次にexports["."](文字列ならexports)。- それがなければ
index.ts、index.tsx、index.js、index.mjs、index.cjs。
単一ファイルへのパスなら、そのファイルを読み込みます。
モジュールがエクスポートするもの
export default { id, server }(OpenCode v1):serverがプラグインです。- それ以外の場合、モジュールがエクスポートするすべての関数がプラグインとして呼び出されます。エクスポートされた
{ server }オブジェクトも同様です。
ですから、エントリファイルからヘルパー関数をエクスポートしないでください。テストに必要なものはオブジェクトにまとめましょう。コミュニティのプラグインは export const _internal = { … } としています。
関数
async (input, options) => hooks // input: { client, project, directory, worktree, serverUrl, $, experimental_workspace } // options: the entry's "options" in plugins.json, else undefined // hooks: { config?, auth?, provider?, "chat.headers"? }
複数の関数をエクスポートすれば、ひとつのプラグインで複数のプロバイダを返せます。それぞれのフックは独立しています。同じプロバイダに対して 2 つのプラグインが auth フックを持つ場合、OpenCode と同じく最後に読み込まれたものが勝ちます。
package.json の magpie 用フィールド
OpenCode は magpie オブジェクトを無視するので、OpenCode プラグインに追加しても安全です。
| フィールド | 意味 |
|---|---|
magpie.icon | プロバイダの画像:https:// URL または data:image/… URI、最大 1 MB。SVG、PNG など。 |
magpie.maxConcurrency | 各アカウントが同時に受けるリクエスト数。1 から 1000 の整数。ユーザーがプロバイダに設定した上限が優先されます。 |
同じフィールドは auth フックにも設定でき(auth.icon、auth.maxConcurrency)、そちらが package.json より優先されます。
プロバイダとモデル
プロバイダを宣言する
プロバイダの id は auth.provider です。models.dev にその id があれば、magpie は OpenCode と同じくそのエントリから始めます。なければ config フックでプロバイダを宣言します。ユーザーが plugins.json で設定したプロバイダを上書きしないよう、??= を使ってください:
config: async (cfg) => {
cfg.provider ??= {}
cfg.provider["acme"] ??= {
name: "Acme",
npm: "@ai-sdk/anthropic", // the API its models speak (table below)
api: "https://api.acme.example/v1", // where requests go, unless the loader says otherwise
models: {
"acme-large": {
name: "Acme Large",
id: "acme-large-2026-09", // the id the API takes, when it isn't the key
limit: { context: 400000, output: 64000 },
reasoning: true,
tool_call: true,
modalities: { input: ["text", "image"] },
cost: { input: 3, output: 15, cache_read: 0.3 }, // US$ per million tokens; leave out for a plan
variants: { low: {}, medium: {}, high: {} }, // reasoning levels
},
},
}
}
モデルには、プロバイダとは別の API やアドレスを使うための provider: { npm, api }、headers、options、そして非表示にするための disabled: true も指定できます。
モデルが話す API
モデルまたはプロバイダの npm で指定した AI SDK パッケージが API を決めます。それによって、あなたの fetch が受け取るリクエストの形が決まります。エージェントが何を送ってきても、magpie のゲートウェイがすでに変換済みです:
| npm | API | ベース以降のパス |
|---|---|---|
@ai-sdk/anthropic, @ai-sdk/google-vertex/anthropic | Anthropic Messages | /messages |
@ai-sdk/openai, @ai-sdk/azure | OpenAI Responses | /responses |
@ai-sdk/google | Gemini | /models/<id>:streamGenerateContent?alt=sse |
@ai-sdk/github-copilot | GPT-5 以降(gpt-5-mini を除く)は Responses、それ以外は chat completions | /responses または /chat/completions |
それ以外すべて(@ai-sdk/openai-compatible, xai, groq, deepseek, …) | Chat completions | /chat/completions |
ベースは次の順で決まります:
- loader の
baseURL; - モデルの
provider.api; - プロバイダの
api; - AI SDK パッケージ自身のデフォルト(
https://api.anthropic.com/v1、https://api.openai.com/v1、…)。
本文の model はモデルの API id です。id があればそれ、なければキーです。
アカウントごとのモデル
モデルがアカウントによって変わる場合は、provider フックを追加します:
provider: {
id: "acme",
async models(provider, { auth }) {
// provider.models: what models.dev and the config hook give, keyed by id
// auth: this account's sign-in, as plugin-auth.json keeps it
return { "acme-large": { ...provider.models["acme-large"], name: "Acme Large" } }
},
}
- magpie はこれをアカウントごとに 1 回、そのアカウントのスコープで、そのプロキシを通して呼び出します。各アカウントは自分に列挙されたモデルだけを扱うので、小さいプランのアカウントに、持っていないモデルが送られることはありません。
- モデルは OpenCode の
Modelです:id、name;api: {id, url, npm};limit: {context, input?, output};capabilities: {reasoning, toolcall, attachment, input: {text, image, …}};cost: {input, output, cache: {read, write}};variants、release_date、status。
provider.modelsのモデルを展開していくつかのフィールドを変えるのが簡単です。statusが"deprecated"のモデルは表示されません。 - magpie は、OpenCode が無視する次のフィールドもモデルから読み取ります:
free: true:プランの枠を消費せずに使えます。rate:1 リクエストが消費するクレジットの倍率。数値(0.5)でも、ベンダーの表記("x0.5")でも構いません。rateWas:現在実施中の割引の前の倍率。
- モデルが推論する場合、
variantsのキーがそのモデルの推論レベルになります。
ベンダーに接続できないとき
フックが次のいずれかをした場合、magpie は一覧を取得できなかった組み込みサブスクリプションと同じく、すでに持っている一覧を保持します:
- fetch が失敗した後、
provider.modelsを変更せずに返す; - 例外を投げる;
list[Symbol.for("magpie.fellBack")] = trueを付けた独自の一覧を返す。
失敗の原因がサインインなら、signIn: "expired" を付けたエラーを投げてください。するとアカウントは再サインインが必要と記録されます(サインイン状態を参照)。
サインイン
auth フックは { provider, methods, loader, usage?, refresh?, refreshLead?, icon?, maxConcurrency? } です。各メソッドはユーザーが選べるサインイン方法です。アプリではメソッドがボタンになり、ターミナルでは magpie plugin login <id> [<method>] がどれを使うか尋ねます。
API キー
{ type: "api", label: "Acme API key", placeholder: "acme-…" }
labelはキー入力欄の見出しです。「API key」とだけ書いたラベルは「Acme API key」と表示されます。placeholderは magpie 独自のフィールドで、入力欄の中のヒントです。authorize関数がなければ、キーはそのまま{type: "api", key, metadata?}として保存されます。promptsへの回答はmetadataに入ります。authorize(inputs)があれば、プラグイン自身がキーを検証または交換します。戻り値は{type: "success", key, metadata?}または{type: "failed", error?}です。
ブラウザやデバイスでのサインイン
{
type: "oauth",
label: "Sign in with Acme",
async authorize(inputs) {
const d = await startDeviceCode()
return {
url: d.verification_uri, // magpie opens it
instructions: `Enter the code ${d.user_code}`, // and shows this
method: "auto", // "auto": callback() resolves on its own
async callback() { // "code": callback(code), the code the user pastes
const t = await pollForToken(d)
if (!t) return { type: "failed", error: "The code expired" }
return { type: "success", refresh: t.refresh_token, access: t.access_token,
expires: Date.now() + t.expires_in * 1000, accountId: t.email }
},
}
},
}
refreshを含む成功は{type: "oauth", refresh, access, expires, …}として保存され、返したほかのフィールド(accountId、enterpriseUrl、独自のもの)もすべて一緒に保持されます。keyを含む成功は API キーとして保存されます。- 結果に
provider: "other-id"を付けると、そのプラグインがサインインする別のプロバイダにサインイン情報を保存します。 - 失敗時の
errorは magpie 独自のフィールドで、ユーザーに理由(最大 500 文字)が表示されます。なければ、サインインに失敗したことだけが表示されます。 inputsはpromptsを持つメソッドにだけ渡されます。prompts のないメソッドには、OpenCode の TUI が呼ぶときと同じくundefinedが渡ります。空のオブジェクトは OpenCode の CLI の呼び方で、その場合ターミナルで質問するプラグインもあります。
サインイン前の質問
メソッドの prompts は authorize の前に順番に尋ねられます。それぞれ条件を満たすときだけです:
prompts: [
{ type: "select", key: "region", message: "Region",
options: [{ label: "Global", value: "global" }, { label: "China", value: "cn", hint: "acme.cn" }] },
{ type: "text", key: "team", message: "Team id", placeholder: "t-…",
when: { key: "region", op: "eq", value: "global" }, // or condition: (inputs) => boolean
validate: (v) => (/^t-/.test(v) ? undefined : "Starts with t-") },
]
アカウント
- 保存場所。ひとつのプロバイダに複数回サインインできます。最初のアカウントは OpenCode と同じくプロバイダの id で保存されます。それ以外は
id#xxxxxxで保存されます。 - ユーザーが見分ける方法。magpie はアカウントを
accountId、なければmetadata.email、なければemailで名付けます。どれかを保存してください。メールアドレスがあればそれが最適です。 - 再サインイン。新しいサインインは、既存のアカウントと
accountIdが同じならそれを置き換えます。両方にuid(名前が重複しうるベンダーでの、ベンダー自身の id)もある場合は、それも一致する必要があります。id がなければ、同じシークレットで一致を判断します。それ以外は新しいアカウントになります。 - スコープ。magpie があるアカウントのために行う呼び出しは、すべてそのアカウントのスコープで実行されます:
getAuth()はそのアカウントを返します。client.auth.set({ path: { id: "acme" }, body })はそのアカウントに保存します。
- リフレッシュ。
auth.refreshを提供すれば、magpie がトークンを更新します(サインインの更新を参照)。そうでなければ、トークンが切れたときにfetchの中でリフレッシュし、client.auth.setでオブジェクト全体を保存してください。渡したものが保存内容を置き換え、マージはされません。
サインインの更新
magpie 0.1.684 以降、プラグインは magpie 独自のフック auth.refresh でトークンの更新を magpie に任せられます(OpenCode はこれを無視します):
auth: {
provider: "acme",
methods: [ … ],
// optional: how long before expires to renew (ms), 5 minutes by default
refreshLead: 5 * 60 * 1000,
async refresh(auth, provider) { // auth is the account's saved sign-in
const t = await exchange(auth.refresh)
if (t.error === "invalid_grant")
throw Object.assign(new Error("Acme refused the sign-in"), { signIn: "expired" })
return { access: t.access_token, refresh: t.refresh_token ?? auth.refresh,
expires: Date.now() + t.expires_in * 1000 }
},
loader: async (getAuth) => ({ … }), // getAuth() already gives the renewed token
}
- 実行されるタイミング。
expiresまでの残りがrefreshLead未満の OAuth アカウントについて、そのアカウントの loader、provider.models、auth.usage、またはリクエストの前に実行されます。expiresが 0 またはないアカウントは更新されません。 - 一度にひとつ。トークンの更新が必要だと気づいたリクエストは、ひとつの更新を待ちます。リフレッシュトークンを一度しか使えないベンダーに、同じトークンが二度送られることはありません。待つのは最大 15 秒で、それより長くかかった更新も終わった時点で保存されます。
- 保存されるもの。返したフィールドはアカウントの古いフィールドの上に保持され(
client.auth.setと違ってマージされます)、アカウントの記録は消去されます。更新するものがなければ何も返さないでください。 - 失敗したとき。アカウントはそのままで、リクエストは古いトークンで送られ、結果はベンダーの応答次第になります。
signIn: "expired"を付けたエラーはアカウントに記録を付け(サインイン状態を参照)、再サインインされるまで magpie は再試行しません。それ以外のエラーは 30 秒後に再試行されます。 - OpenCode と古い magpie。どちらもこのフックを無視します。プラグインがそちらでも動くなら、
fetch内のチェックも残してください。magpie 上ではトークンが新しいので何もしません。
リクエスト
loader(getAuth, provider) はアカウントごとに 1 回実行され、サインインが変わると再実行されます。OpenCode が AI SDK に渡すものを返します:
| フィールド | magpie での扱い |
|---|---|
baseURL | リクエストの送り先。モデルやプロバイダの api より優先されます。 |
apiKey | AI SDK パッケージと同じ方法で送られます:Anthropic なら x-api-key、Google なら x-goog-api-key、それ以外は Authorization: Bearer。API キーでのサインインでは、保存されたキーがデフォルトです。 |
headers | すべてのリクエストに追加されます。 |
fetch | リクエストを送ります。なければ magpie のホストは標準の fetch を使います。 |
あなたの fetch(input, init) が受け取るリクエスト:
inputは完全な URL です:ベースに、モデルの API のパスを足したものです。init.methodは通常POSTです。init.headersは次の順に設定され、後のものが前のものを上書きします:apiKeyによるキーのヘッダー;- loader の
headers; - ゲートウェイのリクエストヘッダー;
chat.headersフックが追加するもの。
init.bodyはその API の形式の JSON 文字列です(UTF-8 テキストでなければバイト列)。中のmodelは API id で、streamはエージェントが要求したとおりです。init.signalはエージェントがリクエストを諦めると中断されます。
Response を返してください。そのステータス、ヘッダー、本文はそのまま、チャンクごとにストリームで返されます。ストリーミングの本文を書き換える場合は、ヘッダーから content-length と content-encoding を取り除いてください。
失敗とフェイルオーバー
- 2xx 以外の応答はベンダーの応答です。ゲートウェイはほかのプロバイダと同じように扱います:レート制限やサーバーエラーならリクエストを次のアカウントやモデルに移し、401 ならアカウントに記録を付けます(サインイン状態を参照)。
- 例外を投げるとリクエストは失敗し、フェイルオーバーが次に進みます。
- 可能なら API 自身のエラー形式で応答してください。エージェントがベンダーのメッセージを表示できます。
chat.headers
OpenCode の chat.headers フックはリクエストごとに実行されます。その input.sessionID は magpie の会話 id で、会話のターンをまたいで同じままです。ベンダーのセッションヘッダーやキャッシュヘッダーに使ってください:
"chat.headers": async (input, output) => { if (input.model.providerID === "acme") output.headers["x-acme-session"] = input.sessionID }
サインイン状態
magpie は、ベンダーにサインインを拒否されたアカウントに記録を付けます。その記録をユーザーに表示し、再び使えるようになるまでそのアカウントには送信しません。デフォルトでは 401 で記録が付き、成功すると消えます。プラグインは X-Magpie-Sign-In レスポンスヘッダーで、応答の意味をより正確に伝えられます。このヘッダーは応答が先に渡される前に magpie が取り除きます:
| 値 | 意味 |
|---|---|
expired | ステータスにかかわらず、サインインが拒否された(そのために 403 や 502 を返すベンダー)。 |
kept | アカウントをそのままにする(別の理由による 401)。 |
renewed | サインインが更新された:リクエストがその後どうなっても記録を消す。 |
fetch や provider.models から signIn: "expired" を付けたエラーを投げても同じ効果があります:
throw Object.assign(new Error("Acme refused the sign-in; sign in again"), { signIn: "expired" })
使用量
auth.usage(getAuth, provider) は magpie 独自のフックです。アカウントの枠をどれだけ使ったかを返します。アカウントのスコープで実行されるので、ここでリフレッシュしたトークンはそのアカウントに保存されます。magpie はその結果を プロバイダ のアカウント、magpie quota、ゲートウェイの GET /v1/magpie/quotas に表示します。フィールドはすべて省略可能です:
{
plan: "Pro",
user: "ada@example.com", // the account as the vendor names it
until: "2026-11-01T00:00:00Z", // the plan's end
renew: "auto", // or "off"
balance: "$12.40",
error: "", // shown in place of the windows
windows: [{
name: "5 hours",
used: 37, // percent; past 100 when overspent
resetsAt: "2026-10-03T18:00:00Z", // or resetSecs: 3600
span: 18000, // seconds the window runs
display: "370 / 1000 credits",
model: "opus", // counts only models whose ids hold this word
models: [], notModels: [], // or exactly these ids, or all but these
aside: false, // true: using it up doesn't stop the account
}],
resets: { count: 2, until: "…", byWindow: false, fiveHour: 1, weekly: 1 }, // resets the account may spend
signIn: "kept", // as X-Magpie-Sign-In
}
時刻は ISO 文字列か、秒またはミリ秒のエポック数値です。signIn がない場合、再サインインを求める error はアカウントに記録を付け、問題なく読み取れれば記録は消えます。
プラグインに渡されるもの
magpie は OpenCode の PluginInput の内容を、OpenCode サーバーなしで意味をなす範囲でプラグインに渡します:
| フィールド | magpie では |
|---|---|
client.auth.set / .remove | スコープ内のアカウントのサインインを保存または削除します。 |
client.app.log | magpie のログに plugin [level]: [service] message として書き込みます。 |
client.tui.showToast | magpie のログに書き込まれます。 |
client.config.get | すべてのプラグインの config フックを経た後の設定。 |
そのほかの client.* | { data: undefined } を返します。 |
directory, worktree | magpie の設定フォルダ。ファイルの保存場所に使えます。 |
project | { id: "magpie", worktree }。 |
$ | Bun のシェル。 |
serverUrl | プレースホルダーです。背後に OpenCode サーバーはありません。 |
magpie が呼び出すフックは config、auth(そのメソッド、loader、usage)、provider.models、chat.headers です。プラグインのほかのフック(tool、event、chat.params、…)は呼び出されません。ツールはエージェント自身が実行します。
プロバイダ id
- エージェントはプラグインのモデルを
<id>/<model>と呼びます。id はプラグインのauth.providerです。 - その id が magpie ですでに使われている場合(組み込みサブスクリプション、プリセット(
google、openai、anthropic、…)、magpie自身)、プラグインのプロバイダは<id>-pluginという名前になります。自分だけの id を選んでください。 - 例外は、magpie の非推奨の組み込みサブスクリプション向けのコミュニティプラグインです。これらは組み込みの id(
cursor、zed、…)を使い、ユーザーがプラグインに移行するとそれを引き継ぐので、エージェントは変更なしで動き続けます。
テスト
magpie はサンドボックスで動かしましょう。普段の magpie、そのエージェントやサインインに触れずに済みます:
sb=$(mktemp -d)
m() { env HOME=$sb XDG_CONFIG_HOME=$sb/.config XDG_CACHE_HOME=$sb/.cache MAGPIE_ADDR=127.0.0.1:3499 magpie "$@"; }
m plugin add ./opencode-acme-auth </dev/null
m plugin --json # what the plugin lists: methods, accounts, models, load errors
m plugin login acme # each method, each prompt
m provider test acme # one tiny request per API
m provider test acme acme-coder # or to the models named
m quota # the usage hook
m plugin logout acme
- 読み込みに失敗したプラグインは、
magpie pluginに理由が表示されます。 - フックのエラーとプラグインが出力したものはすべて、
plugin: …の行として出力に含まれます。 - ユニットテストには、
_internalに置いた関数に対してbun testを実行してください。 - コミュニティリポジトリにはいくつかスクリプトがあります:
bun scripts/check.mjsは各パッケージを magpie と同じように読み込み、フックをチェックします。scripts/try.sh <name>は上のようなサンドボックスで実行します。
公開と掲載
- npm。パッケージを通常どおり公開します。ユーザーは名前(
magpie plugin add opencode-acme-auth)、バージョン(…@0.2.0)、git、フォルダから追加できます。 - 検索。プラグイン の検索は npm に
<query> opencodeを問い合わせます。名前かキーワードにopencodeと、auth、plugin、providerのいずれかを含むパッケージが表示されます。["opencode", "opencode-plugin", "<vendor>"]のようなキーワードで条件を満たせます。あわせてkeywords:pi-package <query>も問い合わせ、名前かキーワードにauthかproviderを含む pi パッケージを表示します。 - README。magpie のプラグインページには npm の README が表示されます。何にサインインするのか、どうやって、サインイン情報をどこに保存するのか、どのモデルがあるのかを書いてください。
- 一覧。プラグイン は、コミュニティリポジトリの
registry.jsonにあるプラグインをおすすめします。magpie は最大で 6 時間ごとにこれを取得し、取得できないときのためのコピーを内蔵しています。掲載してほしい場合は、項目を追加するプルリクエストを送ってください:registry.json{ "package": "opencode-acme-auth", "name": "Acme", "icon": "https://acme.example/icon.svg", "providers": ["acme"], "summary": { "en": "Your Acme Coding Plan.", "zh": "使用你的 Acme Coding Plan。" } }MAGPIE_PLUGIN_MARKET=<url>は magpie に別の一覧を参照させます(自分の一覧の確認に便利です)。offにすると内蔵のコピーだけを使います。 - コミュニティのパッケージ。magpie-community/plugins に貢献するには:
packages/<name>に置き、名前は@magpie-community/opencode-<name>-authとします。本当に必要でない限り、ランタイムの依存は持たせないでください。- バージョンを上げて
mainにプッシュすると公開されます。 - これらのパッケージは、どの magpie でも自動で更新されます。
pi パッケージ
magpie は pi のパッケージも動かします。pi.registerProvider でプロバイダを登録する pi 拡張は、何も変えずに pi と同じように magpie で動き、ほかのプラグインと同じ方法で追加できます(magpie plugin add pi-antigravity)。
- pi のパッケージとみなす条件。
package.jsonにpiマニフェスト("pi": { "extensions": [...] })があるか、pi-packageキーワードがあるか、dependenciesかpeerDependenciesに@earendil-works/pi-coding-agent(または旧名の@mariozechner/pi-coding-agent)があること。単体のファイルは pi を import していれば該当します。 - 読み込み。magpie はパッケージの隣に pi をインストールし、pi 自身のローダーで同じプラグインホストに読み込みます。登録されたプロバイダは magpie のプロバイダになり、モデルは pi がそれぞれに挙げるもの(pi がアカウントごとの一覧を取得できるときはその一覧)です。コマンド、ツール、レンダラー、イベントハンドラは使いません。
- サインイン。OAuth のログインは pi と同じように進みます。質問(
onPrompt)は magpie のサインインの中で聞かれ、ページ(onAuth)はブラウザで開き、refreshTokenで更新されます。パッケージがctx.uiで出すダイアログ(select、confirm、input、editor)やctx.ui.customに渡すターミナルコンポーネントも、magpie の質問として表示されます。API キーを受け付けるプロバイダにはキーの入力欄が出ます。サインインごとにアカウントになるので、pi のプロバイダも複数アカウントを持てます。 - pi のファイル。pi の設定、
auth.json(pi と同じ形で各プロバイダの最初のアカウント)、パッケージが書くファイルは、magpie の設定ディレクトリのpi/に置かれ、PI_CODING_AGENT_DIRを設定しない限りユーザーの~/.piには触れません。アカウント自体はほかのプラグインと同じくplugin-auth.jsonに保存されます。 - リクエスト。pi のプロバイダのモデルは Anthropic Messages を受け取ります。ゲートウェイがエージェントのリクエストを変換し、magpie が pi を通してストリーミングし、応答の思考・署名・ツール呼び出しはそのまま保たれます。ベンダーのエラーはステータス付きで返るので、フェイルオーバーもほかのプラグインと同じく働きます。
- id。pi のプロバイダの id もほかのプラグインと同じ規則です。プロバイダ id を参照してください。
ホストプロトコル
プラグインを書くのにこの節は不要です。magpie 自体の開発や、ホストのデバッグをする人向けです。magpie はホストの stdin に {id, method, params} を 1 行にひとつずつ書き込みます。ホストは {id, result} または {id, error: {message}} で応答します。
| メソッド | 動作 |
|---|---|
init | プラグインを読み込み、その config フックを実行します。ほかのすべての呼び出しはこれを待ちます。 |
providers | 各プロバイダとそのメソッド、アカウント、アカウントごとのモデル。 |
prompt, validate | メソッドの次の質問、回答のチェック。 |
authorize, callback, apiKey | サインイン。 |
load | アカウントに対する loader のオプション。 |
fetch | 1 件のリクエスト。{id, event: "head", status, headers}、次に {id, event: "chunk", data}(base64)、最後に {id, result: null} をストリームで返します。{method: "abort", params: {id}} で取り消せます。 |
usage, check | アカウントの使用量、リクエストと同じ方法でのアカウントの確認。 |
import, take, signOut, reload | アカウントの取り込みと取り出し、削除、loader の再実行。 |
ホストは独自のイベントも送ります:log、toast、auth(サインインが保存またはリフレッシュされた)、signIn(アカウントのサインイン状態、前述のとおり)。ソースは internal/plugin/host.js です。
よくある質問
OpenCode のプラグインは magpie でそのまま動きますか?
はい。auth、config、provider、chat.headers を通してサインインとリクエストを行っていれば動きます。magpie が追加するもの(auth.usage、auth.refresh、magpie.icon、maxConcurrency、free、rate、X-Magpie-Sign-In)は OpenCode が無視するので、ひとつのパッケージで両方に対応できます。
pi のパッケージは magpie で動きますか?
はい。pi.registerProvider でサインインとモデルを提供していれば動きます。magpie が使うのは登録されたプロバイダだけです。pi パッケージを参照してください。pi-package キーワードを付ければ検索で見つかります。
プロバイダの名前が …-plugin になるのはなぜですか?
その id が magpie ですでに使われているからです。プロバイダ idを参照してください。
ヘルパー関数の一部がプラグインとして実行されます。
エントリモジュールがエクスポートする関数はすべてプラグインとして呼び出されます。プラグイン関数だけをエクスポートするか、export default { id, server } を使ってください。
npm の依存を使えますか?
はい。プラグインと一緒にインストールされますが、インストールスクリプトは実行されません。Bun の fetch、crypto、fs、$ でたいていは足ります。
プラグインからベンダーの CLI を実行できますか?
はい。Bun の $ か child_process を使います。いくつかのコミュニティプラグインはベンダー自身の CLI でサインインします。CLI が入っていないマシンもあると想定し、エラーでインストール方法を伝えてください。
プラグインにはユーザーの作業のどこまでが見えますか?
プラグインが送るリクエストに含まれるものすべて、つまり会話、ツールとその結果です。ユーザーが指定した送り先にだけ送り、ログには残さないでください。
編集内容を magpie に反映させるには?
ターミナルの magpie コマンドは、フォルダをその時点の内容で読み込みます。アプリでは、プラグイン でプラグインをオフにしてからオンにしてください。
質問や、共有したいプラグインがありますか? Discord で聞くか、magpie-community/plugins に issue を立ててください。