ドキュメント · プラグイン
プラグインを書く
プラグインはサービスにサインインし、そのサービスへのリクエストを送ります。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 の起動から少し後に自動で更新され、その後は 1 時間ごとに更新されます。 - そのほかのパッケージは、新しいバージョンがあると プラグイン にドットが表示され、サイドバーの インストール済み に「1 件のアップデート」と出ます。このときプラグインを開くとインストール済みが開き、新しいバージョンのあるプラグインが上部に すべてアップデート と並んで表示されます。ユーザーはクリックか
magpie plugin updateで更新します。 - バージョンを固定した指定(
name@1.2.3)は、ユーザーが更新するまでそのバージョンのままです。… にアップデート、すべてアップデート、magpie plugin updateは最新版をインストールし、固定を外します。 - git リポジトリには比べるバージョンがありません。ブランチやタグ(
#refがなければデフォルトブランチ)が Bun のインストールしたコミットと違うときだけ、アップデートが表示されます。GitHub のものは GitHub の API に、ほかのホストはgit ls-remoteで、最大 10 分に 1 回問い合わせ、1 時間ごとのチェックでも問い合わせます。コミットに固定した指定にはアップデートはありません。行にはインストールしたコミットが表示されます。
- コミュニティのパッケージ(
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 | プラグイン自身の画像。「発見」のカード(npm から、GitHub のものはリポジトリから読み込み)と「インストール済み」の行に表示され、自分の画像を持たないプロバイダにも使われます:https:// URL または data:image/… URI、最大 1 MB。SVG、PNG など。 |
magpie.maxConcurrency | 各アカウントが同時に受けるリクエスト数。1 から 1000 の整数。ユーザーがプロバイダに設定した上限が優先されます。 |
同じフィールドは auth フックにも設定でき(auth.icon、auth.maxConcurrency)、そちらが package.json より優先されます。複数のプロバイダにサインインするプラグインは、プロバイダごとに auth.icon を設定し、magpie.icon をプラグイン自身のものにできます。
プロバイダとモデル
プロバイダを宣言する
プロバイダの 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 も指定できます。
自分でウェブ検索する上流
プラグインが Anthropic や OpenAI 自身の API(ホスト型のウェブ検索ツールを自分で実行する)の前に立つ場合は、config フックでプロバイダに searches: true を設定します。するとクライアントのウェブ検索(Claude Code の WebSearch、Codex の web_search)は、そのプロバイダの Anthropic または Responses のモデルでは送られたまま fetch に届き、magpie 自身の検索を通りません。Chat のモデルは変わりません。magpie が他のモデルのために自動でこれを使って検索することはありませんが、設定で検索モデルとして指定できます。
判定 API(System One)
プラグインは自分のプロバイダ向けに TypeSafe の System One API を提供でき、そのモデルをルーティンググループの分類器にできます。config フックでプロバイダに decide: true を、会話ではなく質問に答えるモデルごとに decides: true を設定します(config フックでも models フックのリストでも構いません)。すると magpie は各質問を POST {baseURL}/systemone としてローダーの fetch に送ります。アカウントのサインイン情報は他のリクエストと同じく付きます。本文は {"model", "questions", "state"}、返答は {"model", "answers": {"<質問>": {"choice", "score", "confidence"}}} です。
decides を付けたモデルと jev-* という名前のモデルは分類器として提示され、エージェントには見せません。それらへのチャットリクエストは拒否されます。プロバイダに decide: true がなければ decides は無視され、どのモデルも普通にチャットに使えます。
モデルが話す 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:現在実施中の割引の前の倍率。
serviceTiers(modelsフックが返すモデルに):ベンダーがそのモデルを提供するサービス階層を id で、たとえば["priority", "ultrafast"]。Codex のモデル一覧に載り(priorityは Fast)、そのどれかを指定した Chat または Responses のリクエストはservice_tier付きでfetchに届きます。magpie がプロトコルを変換するリクエストでは、モデルが挙げていない階層は送られません。- モデルが推論する場合、
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-") },
]
type: "password" の質問は text と同じですが、入力が表示されません。アプリではマスクされた入力欄、ターミナルの magpie plugin login ではエコーなしです。トークンや個人アクセストークン(PAT)に使います。magpie 独自の型で、OpenCode にあるのは text と select だけです。
アカウント
- 保存場所。ひとつのプロバイダに複数回サインインできます。最初のアカウントは 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",
amount: 370, limit: 1000, unit: "credits", // its own count, when it counts amounts
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
runsOutAt: "2026-10-05T00:00:00Z", runsOut: 120, // when the soonest credits left are lost, and how many
}],
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、…)を使い、ユーザーがプラグインに移行するとそれを引き継ぐので、エージェントは変更なしで動き続けます。 - インストール済みの 2 つのプラグインが同じ id にサインインする場合(公開プラグインと自分のコピーなど)、提供するのは 1 つだけです。選んだ方、選んでいなければ後から追加した方です。どちらも プラグイン → インストール済み に残り、各行にどちらが提供しているかが表示されます。もう一方の行の … に使う、または
magpie plugin use <名前> <id>で切り替えられます。
テスト
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。" } }"kind": "middleware"を書き、providersは書きません。ゲートウェイミドルウェア の下に表示されます。ほかの種類も同じで、"kind": "tools"は エージェント向けツール、"kind": "page"は ページ、"kind": "agent"は エージェント の下に表示されます。MAGPIE_PLUGIN_MARKET=<url>は magpie に別の一覧を参照させます(自分の一覧の確認に便利です)。offにすると内蔵のコピーだけを使います。 - GitHub のトピック。GitHub で
magpie-pluginトピックを付けたリポジトリは、プラグイン → 発見 → 非公式 · GitHub に、スターの多い順に、誰もレビューしていないものとして表示されます。package.jsonはリポジトリのルートに置いてください。repositoryがそのリポジトリを指す形で npm に公開されていれば npm からインストールされ、そうでなければリポジトリの現在のコードがインストールされます。そのため読み込むファイルはコミットしておく必要があり、distを公開時にしかビルドしないリポジトリは表示されません。フォーク、アーカイブ済みのリポジトリ、magpie-community 自身のものも表示されません。magpie が GitHub に問い合わせるのは最大で 10 分に 1 回なので、トピックを付けたリポジトリは数分で表示されます。カードにはpackage.jsonの"magpie": { "icon": "…" }で指定したアイコンが表示され、ない場合はパズルのアイコンになります。 - コミュニティのパッケージ。magpie-community/plugins に貢献するには:
packages/<name>に置き、名前はプロバイダなら@magpie-community/opencode-<name>-auth、ミドルウェアやルーティング戦略なら@magpie-community/middleware-<name>、ツールなら@magpie-community/tools-<name>、ページなら@magpie-community/page-<name>、エージェントなら@magpie-community/agent-<name>とします。本当に必要でない限り、ランタイムの依存は持たせないでください。- バージョンを上げて
mainにプッシュすると公開されます。 - これらのパッケージは、どの magpie でも自動で更新されます。
非同期ゲートウェイリクエストフック
プラグインはエージェントのリクエストをルーティングする前に I/O を待てます。package.json に "magpie": { "hooks": "./hooks.js" } を宣言するか、*.hooks.js / .mjs / .ts / .mts を追加し(Bun は TypeScript をビルドなしで実行します)、名前付きの非同期 onRequest(body, ctx) をエクスポートします。専用パッケージに OpenCode main/exports は不要です。
任意の非同期 onResponse(reply, ctx) は出力完了後にベストエフォートの通知を受け取ります。応答やライブストリームは変更せず、ツール宣言と実行ループはクライアントが担当します。
Bun でリクエストごとに一度、JS ゲートウェイミドルウェアの前に実行し、Codex 自身の ChatGPT サインインを含むすべてのプロバイダーに適用します。body はエージェント API のオブジェクトです。ctx は protocol、model、stream、path、agent、session、cwd、query、options、signal を持ちます。query は最後のユーザーテキストで、ツール結果と画像を除きます。ない識別情報は空で、Authorization は渡しません。
オブジェクトを返すと置き換え、undefined は元を保ちます。モデル、ストリーミング、クライアントのツールやツール選択を変えたフックは取り消され、失敗として扱われ、そのリクエストでは onResponse が呼ばれません。コピーを渡すので失敗や undefined の直接変更は破棄します。成功した変更は有効な順に渡ります。各フックは既定で 1 秒です。gatewayTimeoutMs は 100〜10000 ミリ秒の整数です。全体は合計予算を 2〜10 秒に制限し、起動と RPC を含めて 11 秒です。設定 → プラグインでオプションと呼び出し回数、時間、失敗、最新の結果を確認できます。ctx.report(code, bytes) は ready/empty/unbound/unauthorized/unavailable/invalid/skipped とバイト数のみを受け付けます。私的なテキストは保存せず、再読み込みで統計をリセットします。fetch に ctx.signal を使ってください。例外、不正な戻り値、タイムアウト、起動中のホストは元を保ちます。Bun はホスト権限で動くので信頼できるコードを使ってください。
magpie plugin add ./my-plugin で追加し、magpie plugin options my-plugin '{"serverUrl":"http://127.0.0.1:8000"}' で設定します。on/off と設定変更は再読み込みします。ソースだけの変更は設定の再読み込みが必要です。内部のタイトルや分類リクエストは対象外です。
ゲートウェイミドルウェア
プラグインはゲートウェイミドルウェアにもなれます。エージェントが magpie のゲートウェイに送るすべてのリクエストに対して、ルーティングの前に実行され、戻ってくる応答に対しても実行されるコードです。モデルに別名を付ける、システムプロンプトに追記する、リクエストを断る、モデルの出力を書き換えたり隠したりする、通ったものを独自に数える、といったことができます。プラグインのものかどうかに関係なく、すべてのプロバイダで動きます。
// alias.middleware.js export const events = ["message_start"] export function onRequest(body, ctx) { ctx.state.asked = body.model if (body.model === "fast") body.model = "deepseek/deepseek-chat" if ((ctx.options.blocked ?? []).includes(body.model)) return ctx.reject(403, "not this model") return body } export function onEvent(ev, ctx) { ev.message.model = ctx.state.asked // the agent sees the name it asked for return ev }
magpie plugin add ./alias.middleware.js # または package.json に "magpie": { "middleware": "./mw.js" } を書いたパッケージ
すぐ使えるミドルウェア
コミュニティリポジトリには、そのまま追加できるミドルウェアがあります。多くは New API がチャネルに対して行うことで、同じ JSON を受け取ります。プラグイン → 発見 → ゲートウェイミドルウェア にあり、そこのカードにはプロバイダかミドルウェアかが表示されます。
| パッケージ | すること |
|---|---|
param-override | New API の param_override:リクエストのフィールドを設定・削除・移動・コピー・書き換えします。特定のモデルや条件に絞ることも、リクエストを断ることもできます。New API からコピーした param_override はそのまま動きます。 |
model-map | New API の model_mapping:モデルを別の名前で送ります。名前か /パターン/ で指定します。応答にはエージェントが指定したモデル名が入ります。 |
system-prompt | すべてのリクエスト(または特定のエージェントやモデルのもの)に自分のシステムプロンプトを、各 API の決まった場所に加えます。リクエストに独自のものがあれば、override を有効にしない限りそのままにします。 |
word-guard | New API の禁止語フィルタ:ユーザーが送る内容にある語やパターンを断るか伏せ字にします。応答も伏せ字にできます。 |
think-tags | 応答本文の先頭にある <think>…</think> を取り除くか、Chat の応答の reasoning_content を本文に入れます(New API の thinking_to_content)。 |
antigravity-cloak | Antigravity が Claude Code と Agent SDK のシステムプロンプトを断る理由になる語を、Antigravity に送る分だけで分割します。他のプロバイダには元のプロンプトのまま送ります。 |
magpie plugin add @magpie-community/middleware-model-map
magpie plugin options model-map '{"mapping": {"fast": "deepseek/deepseek-chat"}}'
オプション
ミドルウェアは設定を ctx.options から読みます。これは plugins.json のそのエントリの options です。プラグイン → インストール済み でミドルウェアの オプション ボタンを押すとその行で JSON として編集でき、magpie plugin options <名前> '<json>' でも設定できます(off で消去)。名前はパッケージ名、spec、短い名前のどれでもかまいません:@magpie-community/middleware-model-map なら model-map です。次のリクエストから反映されます。パッケージは package.json の "magpie": { "options": { … } } でオプションの例を示せます。未設定のときエディタはそれから始まりますが、保存するまでミドルウェアはオプションなしで動きます。
動き方
ミドルウェアは Bun では動きません。ゲートウェイ自身のプロセスの中で、Go で書かれた JavaScript エンジン moejs によって動きます。応答は数千のイベントとしてストリームされ、各イベントがミドルウェアの処理を待つからです。1 イベントでのフック呼び出しは約 1 マイクロ秒で済みますが、Bun との往復だと数十マイクロ秒かかります。各ミドルウェアのコンパイルは一度だけで、リクエストごとに専用のランタイムを使います。そのためリクエストは並行に処理され、同じリクエストのフック同士は ctx.state を共有します。
- 宣言のしかた。
package.jsonに"magpie": { "middleware": "./mw.js" }と書くか、*.middleware.js(または.mjs)という名前の単一ファイルにします。mainもexportsもないパッケージはミドルウェア専用で、プラグインホストは読み込みません。両方あるパッケージはプロバイダプラグインとミドルウェアを兼ねます。 - インポート。プラグイン内のファイルだけを相対パスで読み込めます。ここに
node_modulesはないので、使う依存はファイルにバンドルしてください。 - 使えるもの。標準の JavaScript と
console.log(magpie のログに出ます。1 分あたり最大 60 行)。タイマー、fetch、Node の API はありません。フックはasyncにできますが、待つ対象はありません。 - 変更の反映。magpie はファイルと
plugins.jsonを 1 秒ごとに確認し、変わっていればミドルウェアを読み込み直します。
フック
各フックは名前付きエクスポートか、export default { … } のメンバーです。本文とイベントはプロバイダではなく、エージェント自身の API の形式(ctx.protocol)です。送り出すオブジェクトを返すか、何も返さなければ元のまま送られます。
| フック | 渡されるもの | 戻り値 |
|---|---|---|
onRequest(body, ctx) | ルーティング前のリクエスト本文。model を変えると別の宛先にルーティングされます。 | 本文、または何も返さない。ctx.reject(status, message) はリクエストを断り、そのエラーをエージェントの API の形式で返します(ステータスを省くと 403)。 |
onProvider(body, ctx) | あるプロバイダに送る直前のリクエスト本文。ctx.provider にはその id、name、account(サブスクリプションの種類。claude や antigravity など。API キーにはありません)と model があります。リクエストを試すプロバイダごとに呼ばれ、毎回 onRequest が残した本文を受け取るので、あるプロバイダ向けの変更は他のプロバイダに届きません。 | 本文、または何も返さない。この時点でルーティングは済んでいるので、拒否も行き先の変更もできません。 |
onEvent(event, ctx) | ストリーム応答の各イベントの data: JSON。 | イベント、何も返さない、またはイベントを落とす null。 |
onResponse(body, ctx) | ストリームでない応答の全体。ctx.status がそのステータスです。 | 本文、または何も返さない。 |
export const events = ["content_block_delta"] と書くと、onEvent はその名前のイベントだけで呼ばれます(Anthropic Messages と Responses のイベントには名前があります)。Chat Completions と Gemini のイベントには名前がないので、それらでは events を書かないでください。1 つのイベントは応答の一部なので、1 つの単語が 2 つのイベントに分かれることがあります。data: [DONE] の行とコメントはフックを通らずにそのまま流れます。圧縮された応答もそのまま流れます。
ctx には次のものがあります:protocol(anthropic・chat・responses・gemini)、エージェントが指定した model と stream、path、magpie が把握している場合の agent、options(そのプラグインのオプション)、そしてリクエストごとのオブジェクト state。
フックが失敗したとき
フックの持ち時間は、リクエスト、プロバイダへの 1 回の試行、または応答全体で 250 ms、1 イベントで 50 ms、読み込みで 1 秒です。例外を投げたフック、時間切れになったフック、想定外の値を返したフックは、なかったものとして扱われます。つまりリクエストやイベントは元のまま送られます。設定 → プラグインには、ミドルウェアごとのフック、呼び出し回数、1 回あたりの平均時間、失敗回数と直近のエラーが表示されます。フックが見るのは、エージェントが送ったものと受け取るものそのものです。送信時は magpie のマスキングより前、受信時はマスク解除の後です。magpie 自身のリクエスト(スレッドのタイトル、ルーターの分類器)はミドルウェアを通りません。
ルーティングと使用量の行
同じモジュールは strategies と onUsage もエクスポートできます。そのファイルは *.gateway.js と名付けてもよく、パッケージなら "magpie": { "gateway": "./gw.js" } と書けます。
- ルーティング。
strategies[name]はそれぞれ<プラグイン>/<name>という名前のルーティングになり(パッケージの短い名前、または.gateway.jsを除いたファイル名)、プラグインがオンの間、プロバイダ・プール・グループのルーティング選択に並びます。関数(pool, seats, options)かorderを持つオブジェクトで、席を試す順番をインデックスで返します。各席にはkey、account、known、learns、used、soon(枠が更新される時刻、ms)、pace、weight、resting、tokens(最近送った量)があります。持ち時間は 20 ms で、例外・時間切れ・それ以外の値のときは一覧の順に試します。オフにすると、それでルーティングするプロバイダやグループも一覧の順になります。 - 使用量。
onUsage(row, options)は各リクエストの終了後に使用量の行を受け取ります:プロバイダ、アカウントとキー、モデル、トークン、ms、ttftMs、ステータス、エラーの種類、コスト。本文・プロンプト・エラーの文面は含みません。別に動くのでリクエストが待つことはなく、処理中にキューがあふれた行は破棄して数えます。持ち時間は 250 ms です。
// spread.gateway.js export const strategies = { "fewest-tokens": (pool, seats) => seats.map((_, i) => i).sort((a, b) => (seats[a].resting - seats[b].resting) || (seats[a].tokens - seats[b].tokens)), } let slow = 0 export function onUsage(row, options) { if (row.ms > (options.slowMs ?? 30000)) { slow++ console.log(`slow: ${row.provider} ${row.model} ${row.ms} ms (${slow} so far)`) } }
magpie plugin add ./spread.gateway.js magpie group set fast routing=spread/fewest-tokens
すべてのエージェントのツール
プラグインは OpenCode の tool フックでツールを提供でき、magpie はそれを OpenCode だけでなくすべてのエージェントに提供します。ツールは magpie 自身の MCP サーバー /mcp/magpie/tools(Streamable HTTP)になります。MCP サーバーを使えるエージェントにこのアドレスを追加すると、そのモデルはオンのプラグインが提供するすべてのツールを呼び出せます。名前は <プラグイン>_<ツール> で、プラグインの短い名前が先頭に付きます(下の例では weather-tools_now)。
import { tool } from "@opencode-ai/plugin" export const WeatherTools = async () => ({ tool: { now: tool({ description: "ある都市の今の天気。", args: { city: tool.schema.string().describe("都市名(例: Berlin)") }, async execute({ city }, ctx) { const r = await fetch(`https://wttr.in/${encodeURIComponent(city)}?format=3`, { signal: ctx.abort }) return await r.text() // テキスト。オブジェクトは JSON で返ります }, }), }, })
magpie plugin add ./weather-tools
claude mcp add --transport http magpie-tools http://127.0.0.1:3425/mcp/magpie/tools # Claude Code。ほかのエージェントも同じアドレスを使います
設定 → プラグインには各プラグインのツールがこの名前で表示され、サーバーのアドレスをコピーできます。args は zod のフィールド定義(tool.schema は zod)、zod オブジェクト、または JSON Schema オブジェクトです。エージェントにはこれが示され、magpie は execute の前に呼び出しの引数をこれで検証します。引数が正しくなければ、その引数を示すエラーを返します。ctx は OpenCode と同じで、agent は呼び出したエージェントです。ツールの実行時間は 55 秒までで、時間切れになると ctx.abort が発火します。
このサーバーは組み込みプラグイン プラグインのツール が提供します。オンのプラグインがツールを提供している間はオンのままで、オフのときはツールが一覧に出ず、呼び出しにはオフであることが返ります。ゲートウェイキーでの呼び出しはそのキーの上限に数えられます。ツールはプロバイダープラグインと同じくあなたの権限でプラグインホスト内で動き、結果は呼び出したエージェントにだけ返ります。
エージェントプラグイン
プラグインはエージェントを追加することもできます。magpie に組み込みの設定がないエージェントです。そのエージェントがモデルをどこに書くか、magpie のゲートウェイを使うために他に何を書くかを伝えます。するとエージェントは magpie 自身のものと同じようにエージェントページに並びます。その行で magpie のモデルを選ぶと、magpie はモデルをゲートウェイのアドレスとエージェント専用のキーと一緒に設定へ書き込み、スイッチを切るとすべてユーザーの元の状態に戻します。下の例は Aider を追加します。
// agent.js(package.json の "magpie": { "agent": "./agent.js" } で指定) export const agent = { id: "aider", name: "Aider", bin: "aider", config: "~/.aider.conf.yml", // magpie が編集するファイル model: "model", // その中のモデルのキー prefix: "openai/", // magpie のモデル id の前に付ける ua: ["aider", "litellm"], notice: "Aider reads its settings when it starts: restart it to use the new model.", } export function connect({ gateway, model }) { return { "openai-api-base": gateway.v1, "openai-api-key": gateway.key, "show-model-warnings": false, } }
magpie plugin add ./agent-aider # 単一の *.agent.js ファイルでも可
agent エクスポート
| フィールド | 意味 |
|---|---|
id | エージェントページ、使用量、CLI で使う id。小文字、数字、.、_、- のみ。magpie 組み込みのエージェントの id(codex、claude など)は拒否されます。プラグインはエージェントを追加するもので、置き換えるものではありません。 |
name | ページでの表示名。省略すると id。 |
config | magpie が編集するファイル。~ と $VAR は展開されます。なければ作成します。 |
format | ファイル名から分からないときの json、yaml、env(.json/.jsonc、.yaml/.yml、.env)。TOML はまだ対応していません。 |
model | ファイル内のモデルのキー。JSON と YAML ではドット区切りのパス(llm.model)。モデルをエージェント自身で、connect が書くモデルの中から選ぶ場合(Raycast など)は省略します。その行は接続スイッチだけになってモデルの選択欄は出ず、connect(このプラグインには必須)は model なしで呼ばれます。 |
prefix | magpie のモデル id の前に書くもの。LiteLLM 経由で OpenAI 互換エンドポイントを使うエージェントなら openai/。id 自体はゲートウェイが受け付ける <プロバイダー>/<モデル> です。 |
bin、dir | エージェントがインストール済みかを示す PATH 上のコマンドとフォルダー。 |
ua | キーなしで来たリクエストを使用量で名付けるための、エージェントの User-Agent に含まれる語。 |
icon | 行のアイコン。アイコン名、https:// または data: URL。省略するとプラグインのアイコン、それもなければ汎用のもの。 |
notice | モデルを変えた後に表示する案内。組み込みのエージェントと同じです。 |
library | ライブラリが渡すものをエージェントがどこで読むか:instructions(毎回の会話の前に読む Markdown ファイル)、mcp(MCP サーバーのファイル)とその書き方を表す mcpFormat(どのエージェントのファイルと同じ形か:alma, antigravity, claude, cline, codex, commandcode, copilot, crush, cursor, devin, gemini, goose, grok, hermes, kimi, omp, opencode, zed。claude は {"mcpServers": {…}})、skills(SKILL.md のフォルダを置くフォルダ)。どれも省略でき、~ と $VAR は展開されます:library: { instructions: "~/.toy/AGENTS.md", mcp: "~/.toy/mcp.json", mcpFormat: "claude", skills: "~/.toy/skills" }。書かなければライブラリからは何も渡されず、渡そうとするとプラグインが書くべきキーを示して断られます。 |
connect
connect(input) はモデルの横に書き込む他のキーを、キーのパスと値で返します。async でも構いません。受け取るのは gateway(url、/v1 で終わる URL の v1、エージェント専用のキー key。リクエストはその id で数えられます)、model(id、name、context、output、images、efforts、そして /v1/models と同じ名前の native_endpoints(リクエストが変換されずにそのまま通るゲートウェイのパス、例えば ["/v1/chat/completions"])と pricing(1 トークンあたりの USD、10 進の文字列:prompt、completion、input_cache_read、input_cache_write。分からないものは含まれず、空や 0 にはなりません))、models(magpie がエージェントに出すすべてのモデル、同じ形)、agent(id、config)です。connect がないプラグインはモデルだけを書きます。magpie のモデルが変わると connect がもう一度呼ばれるので、書いたモデル一覧は最新のまま保たれます。
magpie がすること
- 書き込み。各キーをその場で書き換え、ユーザーのコメントや他のキーは残します。最初に書く前に、各キーの元の値を型ごと(数値は数値のまま)取っておきます。
- 元に戻す。エージェント自身のモデルを選び直すか、スイッチを切ると、すべてのキーを元の値に戻し、なかったキーは取り除きます。ファイルを丸ごと上書きすることはありません。
- 接続の判定。magpie のキーを入れたキーがまだそのキーを持っている間、行は接続中と表示します。後から手で変えられたキーは、組み込みのエージェントと同じく行に表示されます。
- 実行のされ方。ミドルウェアと同じく、magpie 自身のプロセスの moejs で動きます。インポートはプラグイン内のファイルのみ、Node API はなく、
console.logは magpie のログに出ます。読み込みとconnectはそれぞれ 1 秒まで。モジュールが読み込めない、またはconnectが例外を投げたプラグインは、プラグインページの行に理由を表示し、何も書き込みません。 - ライブラリ。
libraryがあれば、組み込みのエージェントと同じくライブラリの中身を受け取ります:指示はファイル内の magpie の目印の行の間に、MCP サーバーはユーザー自身のものと同じファイルに(ユーザーが足したフィールドは残ります)、スキルはリンクかコピーでフォルダに入ります。外すときは magpie が書いた分だけを取り除き、元の内容は magpie のバックアップに残ります。mcpFormatがリストにない、またはmcpにmcpFormatがないと、プラグインは読み込まれません。 - 宣言。
package.jsonの"magpie": { "agent": "./agent.js" }、または*.agent.js(または.mjs)という単一ファイル。1 つのパッケージがプロバイダー、ミドルウェア、エージェントを兼ねられます。プラグインマーケットではエージェントのエントリーに"kind": "agent"を付け、エージェントの下に並びます。コミュニティのものは@magpie-community/agent-<name>と名付けます。
ウィンドウのページ
プラグインは magpie のウィンドウに自分のページを追加できます。オンにすると、サイドバーのプラグイングループの、magpie 自身のページの後ろにタブが出ます。ページはプラグインのフォルダにある HTML、CSS、JavaScript だけで、Bun もビルドも要りません。下の例は今日の使用量を表示します。
{
"name": "magpie-usage-glance",
"version": "0.1.0",
"magpie": {
"page": {
"title": "Usage glance", "file": "page/index.html", "icon": "page/icon.svg",
"titles": { "zh": "用量速览", "ja": "使用量の概要" }
},
"permissions": ["usage:summary"]
}
}
<!doctype html>
<link rel="stylesheet" href="style.css">
<script src="_magpie/bridge.js"></script> <!-- served beside every page -->
<script src="app.js" defer></script>
<p id="calls">…</p>
await magpie.ready // the window's theme, painted const s = await magpie.read("usage:summary", { period: "today" }) document.getElementById("calls").textContent = `${s.calls} calls` const opts = await magpie.options.get() // this plugin's own options await magpie.options.set({ ...opts, hideCost: true })
プラグインページの行には、オンにする前からページと読めるものが表示されます。page.file と page.icon はプラグイン内のパスです。ページのファイルは file のフォルダにあるものだけで、外を指すパスやリンクは配信されません。1 つのパッケージがページとプロバイダ、ミドルウェア、エージェントを兼ねることもできます。ページだけのプラグインはプラグインホストを起動しません。page.titles(パネルでは titles)は、言語コード(zh、zh-TW、ja、de)ごとのタブ名です。ほかの言語では title を使います。
ページがスタイルを付けていないところは、最初から magpie の見た目になります。bridge がフォント、色、見出し、ボタン、入力欄、表のスタイルシートを重みなし(:where())で加えるので、ページ自身のルールが常に勝ちます。自分のスタイルでは --magpie-* 変数を使うとウィンドウに揃います。
ページが頼めること
| フィールド / 呼び出し | 返すもの |
|---|---|
magpie.ready | ウィンドウが応答すると、ウィンドウのテーマが得られます。それまでページは magpie と何も話していません。10 秒以内に話さないページは応答なしと表示されます。 |
magpie.theme, magpie.onTheme(fn) | { dark, lang, tokens }。bridge は <html> に data-theme、lang、color-scheme と --magpie-bg、--magpie-fg、--magpie-accent なども設定し、変わるたびに設定し直します。 |
magpie.options.get(), .set(o) | plugins.json にあるそのプラグイン自身のオプションです。ほかのプラグインのものは扱えません。set はオブジェクトを受け取り、保存されたオプションを返します。呼べるのは 1 秒に 1 回までです。 |
magpie.read(name, params) | magpie.permissions で宣言した読み取りです。それ以外はウィンドウが拒否し、magpie ももう一度拒否します。usage:summary(period:today、7d、30d)は呼び出し数、エラー数、トークン、費用と上位 20 モデルを、providers:list は各プロバイダの id と名前を返します。キー、アカウント、サインイン、リクエスト本文を返す読み取りはありません。 |
ページにできないこと
- ページはスクリプトだけを許したサンドボックスのフレーム(
sandbox="allow-scripts"、Content-Security-Policyでも同じ)で、独自のオリジンで動きます。ウィンドウにも、magpie の cookie にも、ウィンドウのストレージにも届きません。 - ポリシーで読み込めるのは自分のファイルだけです。
connect-src 'none'なのでfetch、WebSocket、beacon は使えず、フォームやフレームもありません。スクリプトはファイル(<script src>)にする必要があり、インラインは使えません。インラインのスタイルとdata:の画像は使えます。 - このようなフレームが送れるリクエスト(
Origin: null、フレームからのリクエスト、ほかのサイトのリクエスト)は magpie の API がすべて拒否するので、ポリシーがなくてもページは API に入れません。ファイルは/_magpie/plugin/<token>/から配信され、token は magpie を起動するたびに変わります。 - それでもページは自分をほかのサイトへ移動できます。そのサイトがフレームを持つことになるので、読み取りは秘密でないものだけを返します。ページが読めるものは公開されたものとして扱ってください。
- ページが捕まえなかったエラーは、その場所にこのページは動作を停止しましたと再試行を表示します。ページ自身のスクリプトのエラーはページには「Script error.」としか届かないので、詳しくはウィンドウのコンソールを見てください。
magpie 自身のページに入るパネル
プラグインは、magpie 自身が描くページにパネルを加えることもできます。パネルはそれぞれプラグインのフォルダーにある小さなページで、ブリッジ、読み取り、オプション、サンドボックスはページと同じです。タイトルの見出しの下のカードに収まり、高さは描いた内容に合わせて決まります。
{
"name": "magpie-provider-notes",
"version": "0.1.0",
"magpie": {
"panels": [
{ "slot": "provider", "file": "ui/notes.html", "title": "Notes" },
{ "slot": "usage", "file": "ui/spend.html", "title": "Top models today" }
],
"permissions": ["usage:summary"]
}
}
slot | パネルが入る場所 | magpie.context |
|---|---|---|
agents | 「エージェント」ページの末尾 | {} |
usage | 使用量の「概要」の末尾 | {} |
settings | 設定の「プラグイン」タブ(プラグインがそこにパネルを置いているときだけ表示) | {} |
provider | 保存した各プロバイダーの編集画面 | { provider: { id, name } } |
パネルは 1 つのプラグインにつき 8 個まで。パネルの高さは <html> に合わせて決まる(最大 2400px)ので、ページ自身に高さを任せ、html や body に固定の高さを付けないでください。パネルがエラーを出すか 10 秒以内に応答しないと、その場所に「{name} は動作を停止しました」と「再試行」が表示されます。周りのページと隣のパネルはそのまま動き続けます。
組み込みページの代わりになる
ページは magpie 自身のページの代わりになれます。magpie.page に "replaces" を加え、sessions、library、routing、usage のどれかを指定します。そのタブが組み込みページのあった場所に入り、そのページへのリンクはプラグインのページを開きます。2 つのプラグインが同じページを指定したときは、プラグイン一覧で先にあるほうが代わりになり、もう一方は独立したページになります。プラグインをオフにすると、組み込みページが元どおりに戻ります。
"page": { "title": "My library", "file": "page/index.html", "replaces": "library" }
magpie のメニューに入る項目
ページを持つプラグインは、magpie の行のメニューに項目を加えられます。プロバイダの行(右クリック)とエージェントの行(ハンドル、または右クリック)です。項目は magpie 自身の項目のあとに並び、ページのタイトルと同じくウィンドウの言語で表示されます。項目を選ぶとプラグインのページが開き、どの項目か、その行の id と名前だけが伝えられます。ページは magpie.onAction で受け取ります。ハンドラーを渡す前に選ばれた項目は、渡されるまで待ちます。
"page": { "title": "Balance", "file": "page/index.html" }, "actions": [ { "id": "check", "menu": "provider", "title": "Check balance", "titles": { "zh": "查余额" } } ]
magpie.onAction(({ id, provider }) => {
// id: "check"; provider: { id, name }
});
menu は provider か agent です。id は小文字、数字、ハイフンで、40 文字まで。項目はプラグインごとに最大 8 個で、title のない項目はページのタイトルを使います。ページのないプラグインは項目を加えません。「プラグイン」ページには、プラグインがどのメニューに項目を加えるかが表示されます。
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 を立ててください。