ドキュメント · プラグイン

プラグインを書く

プラグインはサービスにサインインし、そのサービスへのリクエストを送ります。magpie はそれを、すべてのエージェントが使えるプロバイダに変えます。複数アカウント、フェイルオーバー、使用量表示なども備わります。magpie は OpenCode のプロバイダプラグインを動かすので、OpenCode 向けに書いたプラグインはそのままここで動きます。プロバイダを登録する pi のパッケージも動きます(pi パッケージを参照)。このページでは、プラグイン作者に必要なこと、つまり magpie が呼び出すフック、その戻り値を magpie がどう扱うか、そしてプラグインのテスト・公開・掲載の方法を説明します。

ひとことで言うと。プラグインは npm パッケージ、git リポジトリ、またはフォルダです。そのモジュールは OpenCode v1 プラグイン形式の async 関数をエクスポートします。magpie はすべてのプラグインをひとつの Bun プロセス、つまりプラグインホストに読み込みます。関数はフックを返します:
  • config はプロバイダとそのデフォルトのモデルを宣言します。
  • auth はサインイン方法と、各リクエストを送る fetch を持つ loader を提供します。
  • provider.models はアカウントごとのモデルを列挙します。
  • auth.usage はプランをどれだけ使ったかを返します。magpie 独自のフックで、OpenCode は無視します。
  • auth.refresh はアカウントのトークンを期限切れ前に、一度にひとつずつ更新します。これも magpie 独自のフックです。
エージェントはどの API で送ってきても構いません。ゲートウェイはリクエストをモデルが話す API(chat completions、Responses、Anthropic Messages、Gemini)に変換し、プラグインの fetch に渡して、応答をストリームで返します。

5 分でプラグインを作る

この例は架空のコーディングプラン Acme にサインインします。Acme は API キーを受け取り、OpenAI 互換のエンドポイントを持ち、アカウントごとのモデルと使用量を返します。実際のプラグインとの違いは主に、サインインの方法とリクエストへの署名の仕方です。この例はすべて magpie v0.1.676 と、Acme の API のローカルな代用品で実際に動かしています。

package.json
{
  "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 }
}
index.mjs
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 としてそのモデルを使えます。アプリでは プラグイン を開き、プラグインを追加 をクリックしてフォルダを指定します。その後、プロバイダの行からサインインします。

サンドボックスで試しましょう。開発中のプラグインが普段使いの magpie に触れることがあってはいけません。テストにあるように、専用の HOME で magpie を動かしてください。

magpie がプラグインを動かす仕組み

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 と同じ場所から探し、プラグインをエクスポートしている最初のファイルを読み込みます:

  1. package.json の exports["./server"]、次に main、次に exports["."](文字列なら exports)。
  2. それがなければ index.ts、index.tsx、index.js、index.mjs、index.cjs。

単一ファイルへのパスなら、そのファイルを読み込みます。

モジュールがエクスポートするもの

ですから、エントリファイルからヘルパー関数をエクスポートしないでください。テストに必要なものはオブジェクトにまとめましょう。コミュニティのプラグインは 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 フック
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 のゲートウェイがすでに変換済みです:

npmAPIベース以降のパス
@ai-sdk/anthropic, @ai-sdk/google-vertex/anthropicAnthropic Messages/messages
@ai-sdk/openai, @ai-sdk/azureOpenAI Responses/responses
@ai-sdk/googleGemini/models/<id>:streamGenerateContent?alt=sse
@ai-sdk/github-copilotGPT-5 以降(gpt-5-mini を除く)は Responses、それ以外は chat completions/responses または /chat/completions
それ以外すべて(@ai-sdk/openai-compatible, xai, groq, deepseek, …)Chat completions/chat/completions

ベースは次の順で決まります:

  1. loader の baseURL;
  2. モデルの provider.api;
  3. プロバイダの api;
  4. AI SDK パッケージ自身のデフォルト(https://api.anthropic.com/v1、https://api.openai.com/v1、…)。

本文の model はモデルの API id です。id があればそれ、なければキーです。

アカウントごとのモデル

モデルがアカウントによって変わる場合は、provider フックを追加します:

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 は一覧を取得できなかった組み込みサブスクリプションと同じく、すでに持っている一覧を保持します:

失敗の原因がサインインなら、signIn: "expired" を付けたエラーを投げてください。するとアカウントは再サインインが必要と記録されます(サインイン状態を参照)。

サインイン

auth フックは { provider, methods, loader, usage?, refresh?, refreshLead?, icon?, maxConcurrency? } です。各メソッドはユーザーが選べるサインイン方法です。アプリではメソッドがボタンになり、ターミナルでは magpie plugin login <id> [<method>] がどれを使うか尋ねます。

API キー

api メソッド
{ type: "api", label: "Acme API key", placeholder: "acme-…" }

ブラウザやデバイスでのサインイン

oauth メソッド
{
  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 }
      },
    }
  },
}

サインイン前の質問

メソッドの prompts は authorize の前に順番に尋ねられます。それぞれ条件を満たすときだけです:

prompts
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-") },
]

アカウント

サインインの更新

magpie 0.1.684 以降、プラグインは magpie 独自のフック auth.refresh でトークンの更新を magpie に任せられます(OpenCode はこれを無視します):

auth フック
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
}

リクエスト

loader(getAuth, provider) はアカウントごとに 1 回実行され、サインインが変わると再実行されます。OpenCode が AI SDK に渡すものを返します:

フィールドmagpie での扱い
baseURLリクエストの送り先。モデルやプロバイダの api より優先されます。
apiKeyAI SDK パッケージと同じ方法で送られます:Anthropic なら x-api-key、Google なら x-goog-api-key、それ以外は Authorization: Bearer。API キーでのサインインでは、保存されたキーがデフォルトです。
headersすべてのリクエストに追加されます。
fetchリクエストを送ります。なければ magpie のホストは標準の fetch を使います。

あなたの fetch(input, init) が受け取るリクエスト:

Response を返してください。そのステータス、ヘッダー、本文はそのまま、チャンクごとにストリームで返されます。ストリーミングの本文を書き換える場合は、ヘッダーから content-length と content-encoding を取り除いてください。

失敗とフェイルオーバー

chat.headers

OpenCode の chat.headers フックはリクエストごとに実行されます。その input.sessionID は magpie の会話 id で、会話のターンをまたいで同じままです。ベンダーのセッションヘッダーやキャッシュヘッダーに使ってください:

chat.headers
"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" を付けたエラーを投げても同じ効果があります:

index.mjs
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 に表示します。フィールドはすべて省略可能です:

usage の戻り値
{
  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.logmagpie のログに plugin [level]: [service] message として書き込みます。
client.tui.showToastmagpie のログに書き込まれます。
client.config.getすべてのプラグインの config フックを経た後の設定。
そのほかの client.*{ data: undefined } を返します。
directory, worktreemagpie の設定フォルダ。ファイルの保存場所に使えます。
project{ id: "magpie", worktree }。
$Bun のシェル。
serverUrlプレースホルダーです。背後に OpenCode サーバーはありません。

magpie が呼び出すフックは config、auth(そのメソッド、loader、usage)、provider.models、chat.headers です。プラグインのほかのフック(tool、event、chat.params、…)は呼び出されません。ツールはエージェント自身が実行します。

プロバイダ 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

公開と掲載

pi パッケージ

magpie は pi のパッケージも動かします。pi.registerProvider でプロバイダを登録する pi 拡張は、何も変えずに pi と同じように magpie で動き、ほかのプラグインと同じ方法で追加できます(magpie plugin add pi-antigravity)。

ホストプロトコル

プラグインを書くのにこの節は不要です。magpie 自体の開発や、ホストのデバッグをする人向けです。magpie はホストの stdin に {id, method, params} を 1 行にひとつずつ書き込みます。ホストは {id, result} または {id, error: {message}} で応答します。

メソッド動作
initプラグインを読み込み、その config フックを実行します。ほかのすべての呼び出しはこれを待ちます。
providers各プロバイダとそのメソッド、アカウント、アカウントごとのモデル。
prompt, validateメソッドの次の質問、回答のチェック。
authorize, callback, apiKeyサインイン。
loadアカウントに対する loader のオプション。
fetch1 件のリクエスト。{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 を立ててください。