AI Media Router

io.github.jutsuhq由 GitHub 帳號 jutsuhq 發布v0.1.0更新於 Nov 1, 2026

Image and video generation across 40+ providers with your own keys, plus a local asset library.

發布方自己的服務:370.ai — AI Gateway: 100+ Chat Models, Image & Speech Generation

概覽

AI 產生的概覽

本機 MCP 伺服器,用你自己的 API 金鑰透過 40 多家供應商生成圖片與影片,支援比價與可搜尋的本機素材庫。

可以做什麼
  • 生成前比較各供應商的目錄價格並估算成本
  • 生成圖片與影片、批次生成、查詢任務狀態與取消任務,並以美元預算在送出前擋下超支任務
  • 在本機素材庫中搜尋、審核、評分與匯出生成結果,並用本機 ffmpeg 做裁切、拼接、疊加與混音等編輯
  • 儲存具名參考與佔位符,讓角色或產品在多個畫面間保持一致
  • 檢視任務歷史、血緣關係與各模型統計,儲存配方或預設集以重跑單次生成或整條流程
  • 讀取並修改供應商規格,修復介面變動的供應商
適用情境
適合希望助理依提示詞生成圖片或影片、且你已持有一家或多家生成供應商金鑰的情境,尤其是需要比價、限制花費,並把結果保存在本機可搜尋素材庫而非會過期的供應商連結時。
執行需求
以本機程序透過 stdio 執行(預先編譯的執行檔、Rust 建置或已發布的容器映像)。至少需要一個供應商金鑰,透過命令列金鑰指令或環境變數設定,例如 FAL_KEY、KIE_API_KEY、REPLICATE_API_TOKEN、RUNWARE_API_KEY、WAVESPEED_API_KEY 等。可選自架 ComfyUI 位址與權杖,以及用於預覽與影片中繼資料的 ffmpeg/ffprobe。需要能連線到所選供應商的網路。
安裝前請注意
生成會在供應商端產生實際費用,由你自己的帳戶扣款;預算僅在能估算出成本時才會擋下任務。供應商金鑰屬於機密,除環境變數外會存入系統鑰匙圈或設定檔。任務會向第三方供應商送出請求並上傳引用的檔案。素材庫會在本機寫入檔案與中繼資料,規格編輯工具可即時改變供應商行為。

安裝

在 SourceWeft 中

  1. 開啟 儀表板中的 AI Media Router,將其新增到工作區。
  2. 為需要使用其工具的對話啟用該服務。

Desktop only,透過 STDIO。 STDIO 服務會啟動本機處理程序,因此需要 SourceWeft 桌面主機。

其他 MCP 客戶端

參照 儲存庫 中的啟動說明。

README

[AI Media Router]

One MCP server for AI image and video generation across 40+ providers, with your own keys and a local library that remembers everything.

[CI] [Latest release] [License: MIT] [MCP server]

AI Media Router is an open-source MCP server that lets Claude, Cursor, Codex or any MCP client generate images and videos with Seedance, Kling, Veo, Wan, FLUX, GPT Image, Nano Banana, Seedream and more, through 43 providers: fal, Replicate, kie, Runware, WaveSpeed, OpenRouter, BytePlus and 35 others, or your own ComfyUI server. The agent compares prices across providers before it spends, pays each one with your own key, and gets every result back as a local file it can search, review, reuse as a reference and chain into the next shot. Self-hosted, a single Rust binary, MIT.

For example, on November 1, 2026 the price catalog listed Seedance 2.5 text-to-video at 720p with audio from $0.216 a second (302.AI) to $0.462 (fal) among the providers the router can call: the same 10-second clip for $2.16 or $4.62. search_prices shows the agent that spread before it picks.

[How it works: 1. your AI agent asks for an image or video with one tool call; 2. the router checks price and budget, fills in references and submits to one of 43 providers with your key; 3. the result comes back; 4. it is saved in your local library and the agent gets a local file path. Your keys, no middleman, budgets before spending, nothing gets lost.]

Contents:

Why

Without itWith AI Media Router
Every provider has its own API, auth, polling and output formatOne generate → get_job loop for all 43; providers are TOML files
Result URLs expire after hours or daysEvery output is downloaded at once, content-hashed, indexed, previewed
The agent forgets what it made last weekFull-text and similarity search, history, lineage graph, per-model stats
A character or product drifts between shotsNamed references fed into every generation with {{ref:name}}
"$5 budget" is a promise in a promptBudgets refuse a job before it is submitted, from real price estimates
An API change breaks the integration until a releaseThe agent reads the raw exchange, fixes the spec, saves it live

Principles: bring your own keys (no middleman account), the agent chooses provider and price (no hidden routing), model and input pass through unchanged (no lagging unified schema). Details are in docs/concept.md.

Supported providers

43 built-in provider specs, each a TOML file in providers/ with recorded fixtures. ai-media-router providers lists them with the env vars each one still needs.

KindProviders (key)
Inference platforms and API aggregatorsfal.ai (fal), Replicate (replicate), Runware (runware), WaveSpeedAI (wavespeed), kie.ai (kie), OpenRouter (openrouter), Vercel AI Gateway (vercel), Together AI (together), DeepInfra (deepinfra), Novita AI (novita), SiliconFlow (siliconflow), Segmind (segmind), Atlas Cloud (atlascloud), each::labs (eachlabs), AI/ML API (aimlapi), 302.AI (ai302), APIMart (apimart), CometAPI (cometapi), EvoLink (evolink), PiAPI (piapi), ModelsLab (modelslab), Makernice (makernice), Pollo AI (pollo), Sume (sume), Unifically (unifically), Yir (yir), Higgsfield (higgsfield), Comfy Router partner models (comfy)
Model makers' own APIsBytePlus ModelArk (byteplus) and Volcengine Ark (volcengine) for Seedance and Seedream, Kling (kling), Gemini API (gemini) for Veo and Nano Banana, Google Vertex AI (vertex) for Veo, Imagen and Nano Banana, Alibaba Cloud Model Studio (alibaba, alibaba-cn) for Wan, MiniMax Hailuo (minimax), Runway (runway), Luma (luma), LTX (ltx), PixVerse (pixverse), Vidu (vidu), xAI Grok Imagine (xai)
Self-hostedComfyUI (comfyui): your own server or a rented GPU, any workflow in API format, 0 USD per job

Models are not a fixed list: model goes to the provider as written, so a model a provider adds today works today. get_provider shows the model id format and an example call. Specs not yet checked against a live response say so in their notes; live verification of every spec is on the roadmap.

Compared with other ways to generate from an agent

One provider's own MCP server (for example fal's or Replicate's)A local ComfyUIAI Media Router
ProvidersThat oneModels you run yourself43 providers, plus your ComfyUI as one of them
Price comparisonIts own pricesFree per job, your GPUCatalog prices across providers, before the agent picks
Spending limitWhatever that provider offersNot neededNamed USD budgets that refuse a job before it is sent
Where results end upThat provider's storage or URLsYour ComfyUI output folderA local library: search, ratings, references, lineage
AccountThat provider'sNoneNone of ours: your keys go straight to each provider

AI Media Router is not a hosted gateway like OpenRouter: there is no account, no balance and no markup, and OpenRouter itself is one of the providers it calls. It does not route to the cheapest provider by itself either; it shows prices and the agent or you choose.

Quick start

1. Install. Prebuilt binaries for Linux (x86_64, arm64), macOS (Apple silicon, Intel) and Windows (x86_64) are on the releases page. You can also build from source with Rust 1.89 or later:

bash
cargo install --git https://github.com/jutsuhq/ai-media-router

Or run the container (stdio MCP; /data holds the library):

bash
docker run -i --rm -v ai-media-router:/data -e KIE_API_KEY ghcr.io/jutsuhq/ai-media-router

2. Add keys in a terminal. The value is read from a hidden prompt, or from stdin, and never passes through an agent or a tool call:

bash
ai-media-router keys set KIE_API_KEYpbpaste | ai-media-router keys set FAL_KEY

On Windows there is no hidden prompt: pipe the value in (Get-Clipboard | ai-media-router keys set FAL_KEY).

--store auto (the default) keeps the secret in the OS keychain (macOS keychain, Linux Secret Service via secret-tool) when one is available, else in <home>/config.toml under [env], written atomically with mode 0600; --store keychain or --store file forces one. In config.toml a keychain-held key appears as FAL_KEY = "keychain:ai-media-router/<id>", with no secret in the file. The process environment still overrides both, and keys apply without a restart.

ai-media-router keys list shows every key variable, which providers use it, whether it is set, its source (environment, keychain or config file), its last four characters and the exact keys set command. ai-media-router keys test kie makes a free auth check where its spec defines auth_check: accepted, refused, inconclusive, not_supported or not_configured. ai-media-router keys remove FAL_KEY deletes a key.

<home> is set by --home or AI_MEDIA_ROUTER_HOME. By default it is the OS data dir: ~/Library/Application Support/jutsu.ai-media-router on macOS, %APPDATA%\jutsu\ai-media-router\data on Windows, ~/.local/share/ai-media-router on Linux. ai-media-router providers lists every provider and the env vars it still needs.

3. Connect an agent. For Claude Code:

bash
claude mcp add ai-media-router -- ai-media-router mcp

For Claude Desktop (claude_desktop_config.json), Cursor (~/.cursor/mcp.json) and other clients that take an mcpServers block, use the binary's full path, since these apps may not see your shell's PATH:

json
{  "mcpServers": {    "ai-media-router": {      "command": "/usr/local/bin/ai-media-router",      "args": ["mcp"]    }  }}

Then ask the agent something like "find the cheapest Seedance 2.5 offer at 720p among my providers and make a 5-second clip of a paper boat on a rainy street".

ModeCommand
MCP over stdio (one process per agent session)ai-media-router mcp
HTTP: REST /v1/* and MCP /mcpai-media-router serve --bind 127.0.0.1:8787
CLIai-media-router prices seedance --res 1080p, generate, jobs, assets, … (--help)

serve refuses a non-loopback address unless AI_MEDIA_ROUTER_TOKEN is set. Then every request needs Authorization: Bearer <token>, and save_spec / reset_spec stay disabled unless --allow-spec-writes is given.

4. Save a preset. A preset is a saved single generation; --input merges over its input field by field:

bash
ai-media-router recipes save video-draft --provider fal --model <model> --input '{"prompt": "…"}' --budget adai-media-router generate --recipe video-draft --input '{"prompt": "a bottle on wet stone"}'

The preset supplies provider, model, input, save_to, offer and budget; fields given in the call win. ai-media-router recipes list shows the presets.

Optional extras:

Install / setUnlocks
COMFYUI_URL (e.g. http://127.0.0.1:8188), optional COMFYUI_TOKENYour own ComfyUI server as provider comfyui: input is the API-format workflow, {{asset:…}} in a LoadImage node uploads the file, jobs cost 0 USD
ffmpeg and ffprobe on PATH, or FFMPEG / FFPROBE in [env]Previews; width, height, duration_secs, has_audio on assets; media operations; {{job:…:last_frame}}
EMBEDDINGS_API_KEY (OpenAI), or EMBEDDINGS_URL + EMBEDDINGS_MODEL for any OpenAI-compatible endpoint such as Ollamasimilar_assets

How a generation works

mermaid
sequenceDiagram    autonumber    participant A as Agent    participant R as AI Media Router    participant P as Provider    participant L as Library    A->>R: list_recipes    R-->>A: presets: provider, model, input, offer, budget    alt a preset fits        A->>R: generate {recipe, input}    else choosing a provider or model        A->>R: search_prices "seedance 1080p"        R-->>A: offers, cheapest first, with provider_key and offer_id        A->>R: generate {provider, model, input, offer_id, save_to}    end    R->>L: resolve placeholders ({{ref:hero}} → files)    R->>P: upload inputs (cached), submit    R-->>A: job {id, status: running}    loop worker, by lease, survives restarts        R->>P: poll    end    R->>P: fetch result    R->>L: download, hash, preview, probe, file into collection / reference    A->>R: get_job {id, wait_secs: 50}    R-->>A: done, assets[].path, hint for the next step

A job moves through these states:

mermaid
stateDiagram-v2    direction LR    [*] --> waiting: input names a parent job that is still running    [*] --> running: submitted    waiting --> running: every parent done    waiting --> failed: parent failed or cancel_job    running --> done: outputs downloaded    running --> failed: provider error, timeout or cancel_job    done --> [*]    failed --> [*]

The five pillars

PillarThe agent's questionMain tools
Generation gateway"Make this with that model, at that price."search_prices, estimate_cost, preflight, generate, get_job, generate_batch, cancel_job, budgets
Asset library"Where is everything I have made?"search_assets, get_asset, review_asset, collections, export_collection, media operations
Reference manager"Keep this character, product or style consistent."save_reference, {{ref:name}}, add_to_reference
Agent memory"What did I make before, from what, and what worked?"list_jobs, get_lineage, similar_assets, get_stats
Prompt blocks and recipes"Do that again, with this changed."save_snippet, save_recipe, run_recipe, MCP prompts

Provenance in files

Every generated file carries where it came from, so a file shared, moved or restored from a backup still knows its history without the database. Saved outputs get one JSON record: generator (router version), job_id, provider, model, input (snippets expanded, {{asset:…}} / {{ref:…}} placeholders kept), prompt and parents (the asset ids the job read).

FormatWhereWriter
PNGiTXt chunk ai-media-routerbuilt in
JPEGCOM segmentbuilt in
MP4, MOV, WebMcomment tag, stream copy (no re-encode)ffmpeg; skipped without it
WebP, GIF, audio, othersnot written

API keys are never written: values of the provider's key variables are replaced by <redacted>, URLs lose their query string and credentials (or become {{asset:<id>}} when the library knows them), and data URIs are left out. The asset id and size are those of the file with its metadata.

import_asset reads the record back: it fills prompt, adds provider and model to tags, links the asset to its job (recreating it as a finished job in another library, so save_recipe {from_job} works) and to its parents. The result has provenance, missing_parents (import those files to link them) and a hint. Importing the same file again is harmless and links parents imported since.

Set EMBED_METADATA = "false" in [env] to store provider files unchanged. Embedding makes every job's file unique, so two jobs returning identical bytes no longer share one asset.

Production pipelines

Single generations are rarely the deliverable. The router supports the whole production loop: lock what must not change, fan out variants, keep only the good takes, chain stills into video, and finish locally.

mermaid
flowchart LR    ref["Lock<br/>save_reference<br/>character · product · style"]    fan["Fan out<br/>generate_batch<br/>items × matrix, budget-checked"]    gate{"Review<br/>review_asset"}    chain["Chain<br/>{{job:id:last_frame}}<br/>still → video → next shot"]    edit["Finish locally<br/>overlay packshot, reframe,<br/>trim, concat, mux_audio"]    ship["Ship<br/>export_collection<br/>only_approved"]    ref --> fan --> gate    gate -->|approved| chain --> edit --> ship    gate -->|rejected| fan    gate -.->|ratings, failures| stats[("get_stats<br/>what worked")]    stats -.-> fan

Example: a three-shot product ad.

jsonc
// 1. lock the real product photosave_reference {"name": "bottle", "kind": "product", "description": "amber glass bottle, gold cap", "asset_ids": ["a1f3…"]}
// 2. four hero stills: two aspect ratios × two seeds, the whole batch checked against a $3 capset_budget     {"name": "ad", "limit_usd": 3}generate_batch {"defaults": {"provider": "fal", "model": "<image model from get_provider>", "budget": "ad", "estimated_cost_usd": 0.05,                             "input": {"prompt": "{{ref:bottle.description}} on wet black stone, {{snippet:brand-light}}",                                       "image_url": "{{ref:bottle}}"},                             "save_to": {"collection": "ad"}},                "matrix": {"aspect_ratio": ["16:9", "9:16"], "seed": [1, 2]}}
// 3. keep the best take, animate it (priced from the catalog offer), continue from its last framereview_asset   {"asset_ids": ["b7…"], "status": "approved", "rating": 5}generate       {"provider": "kie", "model": "<video model>", "budget": "ad", "offer_id": "<from search_prices>",                "input": {"image_url": "{{asset:b7…}}", "duration": 5}}generate       {"provider": "kie", "model": "<video model>", "budget": "ad", "offer_id": "<from search_prices>",                "input": {"image_url": "{{job:<clip 1 job id>:last_frame}}", "duration": 5}}
// 4. join, add the real packshot and the voice-over, export what was approvedconcat / overlay / mux_audio → export_collection {"collection": "ad", "only_approved": true}

Every step is recorded, so get_lineage on the final video shows the whole graph back to the product photo:

mermaid
flowchart LR    photo["bottle.jpg<br/>imported"] -->|"{{ref:bottle}}"| still["hero still<br/>fal job"]    still -->|"{{asset:…}}"| v1["clip 1<br/>kie job"]    v1 -->|"last_frame<br/>local extract_frame job"| frame["frame.png"]    frame --> v2["clip 2<br/>kie job"]    v1 --> cut["concat<br/>local job"]    v2 --> cut    cut --> final["final.mp4<br/>overlay + mux_audio"]

Placeholders

model and input go to the provider unchanged, except for these placeholders. The job stores the input with its placeholders, so it can be rerun or saved as a recipe.

PlaceholderWrite it asThe provider receives
{{asset:<id>}}a whole string valuethe file uploaded through the provider (URL, file id or data URI), cached per asset and provider
{{ref:<name>}}a whole string valueinside an array: all assets of the reference; as a single value: its first one. Rejected assets are skipped
{{job:<id>}}a whole string valuethat job's outputs (all of them in an array, else the first). The new job is waiting until the parent is done
{{job:<id>:<n>}}a whole string valueoutput n of that job, counting from 0
{{job:<id>:last_frame}}, first_framea whole string valuethat frame of the job's first video, extracted locally and recorded in lineage
{{snippet:<name>}}anywhere inside a stringthe snippet's text
{{ref:<name>.description}}, .tagsanywhere inside a stringthe reference's words
{{var:<name>}}in a recipe's inputthe variable's value. As a whole string it keeps its JSON type: 5 stays a number
{{step:<name>}}, {{step:<name>:<n>}}, {{step:<name>:last_frame}}a whole string value, in a recipe stepthe outputs of an earlier step of the same run, like {{job:…}}

An unknown name fails with the list of known ones. A parent job that fails also fails its waiting children, with the error parent job X failed: … (step `name` (job X) failed: … inside a recipe run).

Recipes

A recipe saves what worked so it can run again with new values. It holds either one generation (provider, model, input) or a whole chain of steps. Each step is a generation or one of the ffmpeg edits (extract_frame, reframe, trim, concat, overlay, mux_audio), and later steps take earlier outputs through {{step:<name>}}. A single-generation recipe is a preset: save it with save_recipe {name, provider, model, input, save_to, offer_id, budget, description} and run it directly with generate {recipe, input}, which merges the call's input over the preset's field by field; vars fills {{var:…}}.

jsonc
save_recipe {"name": "teaser", "save_to": {"collection": "teasers"}, "steps": [  {"name": "still", "provider": "fal", "model": "<image model>", "estimated_cost_usd": 0.05,   "input": {"prompt": "{{ref:bottle.description}}, {{var:scene}}", "image_url": "{{ref:bottle}}"}},  {"name": "clip",  "provider": "kie", "model": "<video model>", "offer_id": "<from search_prices>",   "input": {"image_url": "{{step:still}}", "duration": 5}},  {"name": "final", "op": "mux_audio", "input": {"video_asset_id": "{{step:clip}}", "audio_asset_id": "{{asset:<voice-over>}}"}}]}run_recipe {"name": "teaser", "vars": {"scene": "on wet black stone"}, "budget": "ad"}
mermaid
flowchart LR    run["run_recipe"] --> b["one batch"]    b --> still["still<br/>fal job"]    still -->|"{{step:still}}"| clip["clip<br/>kie job, waiting"]    clip -->|"{{step:clip}}"| final["final<br/>local mux_audio job, waiting"]    final --> out["get_batch outputs"]
  • One call. run_recipe creates every step's job at once, in one batch. A step that uses another waits for it, like a {{job:…}} chain; edit steps are local jobs that the worker runs with ffmpeg when their inputs are done. It returns the last step's job, and get_batch {batch_id} follows the whole run. The batch's outputs are the final steps' files (those no other step uses), and jobs[].step names each step.
  • Failures. A failed step fails every step after it, with an error that names the failed step.
  • Money. Each generation step carries its own estimated_cost_usd or offer_id. With budget, the sum of all steps is admitted before anything is submitted, as for a batch. preflight {recipe, vars} checks every step (checks named <step>/<check>) and estimate_cost {recipe, vars} lists each step's price and the sum.
  • From lineage. save_recipe {name, from_job: <final job>, chain: true} (or from_asset) walks back through the jobs that made its inputs, down to imported files and references, and saves them as steps; their job and asset ids become {{step:…}}. Without chain it copies that one job, as before.
  • Variables and prompts. {{var:<name>}} works in any step. Every recipe is the MCP prompt recipe:<name>, which lists its steps and variables, and the resource ai-media-router://recipes/<name>.

Costs and budgets

mermaid
flowchart LR    subgraph estimate["1 · estimate"]        direction TB        e1["a · estimated_cost_usd"]        e2["b · offer_id price<br/>× seconds or images"]        e3["c · median of<br/>past jobs"]        e1 ~~~ e2 ~~~ e3    end    estimate --> b{"2 · budget?"}    b -->|"none"| ok(["submit"])    b -->|"spent + estimate ≤ limit"| ok    b -->|"over the limit,<br/>or no estimate at all"| no["refused before anything<br/>is sent, with the reason"]

The estimate is the first of a, b, c that exists. Without a budget, the estimate is only stored on the job.

  • Offers. search_prices returns catalog offers, cheapest first, each with a stable offer_id (provider, model, variant, mode, resolution, audio, quality; the price is not part of it). It lists only the user's configured providers by default (once at least one key is set); configured_only: false (CLI prices --all) lists every provider. Video and image prices come from ai-media-api-prices; kind: "image" lists image offers. estimate_cost shows the number and the formula used to get it. A saved preset already carries its offer_id, so running it needs no price search.
  • Seconds are read from duration, duration_seconds, duration_secs, seconds, video_length or length. "5s" counts as 5.
  • Output count is read from num_images, n, num_outputs, image_count, number_of_images, sample_count, batch_size, num_videos or count. The top level is checked first, then nested objects.
  • Spending. A job counts its reported USD cost once known, otherwise its estimate. Failed jobs count nothing.
  • Batches are admitted against the budget as one sum, so a batch either fits or is refused whole.

Not paying twice

Every job stores a fingerprint: a hash of provider, model and the input after placeholder expansion. Library files count by asset id, which is a content hash, so the same files give the same fingerprint whether they come through {{asset:…}}, {{ref:…}} or {{job:…}}, and whatever upload URL they got.

  • reuse: true on generate, run_recipe or generate_batch defaults (CLI --reuse): when a job with the same fingerprint is done, its outputs are all still in the library and none is rejected, the call returns at once with a new job. That job is done, costs 0 USD, names the original in reused_from, and links the same assets as outputs and inputs. It is filed into its own save_to, belongs to its own batch, and chains like any job. get_stats and the price-history median skip reused jobs, so they count only real generations. Reuse is off by default, because models are not deterministic and a repeat is usually meant as a new variant.
  • Identical requests in flight. A request identical to one started in the last 5 minutes that is still waiting or running is not submitted again, even without reuse: agents call again when their client times out. With the same save_to the call returns that job, with a note saying so. Otherwise, or in a batch, it gets a new job that waits for the original and takes its outputs at no cost. Identical items of one batch are variants and are not merged. reuse: false (CLI --no-reuse) always submits.
  • Before running. preflight and estimate_cost (both take reuse) return reuse: {state, job_id} and an estimate of 0 when the run would be reused.

Checks before spending

preflight takes the arguments of generate and runs every check generate would run, without sending or spending anything:

  • provider and keys;
  • snippets and placeholders: assets, references, parent jobs;
  • upload support and save_to;
  • cost and budget.

It also returns the input as the provider would receive it and the exact HTTP requests. ready says whether generate would start. On the CLI it is generate --dry-run, which exits 1 when a check fails.

doctor checks the installation:

  • the data directory and the database;
  • ffmpeg and ffprobe;
  • configured providers, broken overrides and fixtures that no longer match;
  • jobs that no worker is polling;
  • the last day's failure kinds.

doctor --online also pings each configured provider. Run it first when something does not work.

When a job fails

Every failed job carries error_kind, retryable and a hint with what to do next. list_jobs {status: "failed", error_kind} finds failed jobs.

error_kindRetryableWhat the agent should do
moderationnoRewrite the prompt or replace the input image
authnoSet the key with ai-media-router keys set <NAME> in a terminal (get_provider shows what is missing)
quotanoTop up with the provider, or switch provider
invalid_inputnoCompare with get_provider's example and docs
not_foundnoCheck the model id against model_hint
specnoThe API changed: fix the spec
cancellednoGenerate again if it is still needed
rate_limityesWait, or switch provider
provider_erroryesRetry later, or switch provider
networkyesCheck the network and the spec's base_url
timeoutyesRetry, or switch provider

The kind is classified in this order:

  1. The provider's error text, using built-in patterns plus the spec's [errors.kinds].
  2. The HTTP status.
  3. The engine's own messages.

get_stats sums the kinds up per provider and model.

Automatic retries. Pass retry: {attempts: 1-5, delay_secs} to generate, run_recipe or generate_batch defaults, or --retry N --retry-delay S on the CLI. After a retryable failure the same job is submitted again, and the n-th retry waits n × delay_secs (default 30). progress says which attempt failed and why, and attempts counts submissions. Retries are off by default, because some providers bill a failed attempt.

Interface

The same operations exist as MCP tools, REST endpoints and CLI commands. They are defined once, in src/api/. Keys are the exception: keys set and keys remove are CLI-only, so a secret never passes through a tool call; the MCP tools list_keys and test_key only report status. The MCP server also sends short instructions, so an agent knows the order of the tools without reading this file.

Generate and follow jobs
MCP toolRESTPurpose
list_providersGET /v1/providersproviders, missing env vars, built-in or override, broken override files
get_providerGET /v1/providers/{key}model id format, example call, docs, upload support, quirks
list_keysGET /v1/keysevery key variable: set or not, source, last 4 characters, the keys set command; never a value
test_keyPOST /v1/providers/{key}/test_keyfree auth check where the spec has [auth_check]: accepted, refused, inconclusive, not_supported, not_configured
search_pricesGET /v1/prices?query=&provider=&res=&mode=&kind=&quality=&configured_only=&limit=video (per second) and image (per image) offers, cheapest first, with kind, provider_key and offer_id; configured providers only unless configured_only=false
estimate_costPOST /v1/estimate {provider, model, input, offer_id, reuse} or {recipe, vars, reuse}USD estimate, the formula, the median of past jobs; per step for a recipe
preflightPOST /v1/jobs/preflight (same body as generate, or {recipe, vars, budget})dry run: every check plus the rendered requests; sends and spends nothing
generatePOST /v1/jobs {provider, model, input, save_to, budget, estimated_cost_usd, offer_id, retry, reuse} or {recipe, input, vars}start a job, run a preset, or reuse an identical one
get_jobGET /v1/jobs/{id}?wait_secs=&debug=status, progress, inputs, files; debug adds the raw exchanges
list_jobsGET /v1/jobs?limit=&reference=&collection=&status=&error_kind=&batch=generation history
cancel_jobPOST /v1/jobs/{id}/cancelfail a waiting or running job and its waiting children; the provider is not told
generate_batchPOST /v1/batches {defaults, items, matrix, label, recipe}up to 100 jobs, items × matrix; with recipe the matrix fills its variables
get_batchGET /v1/batches/{id}?wait_secs=counts per status, jobs, every output file
doctorGET /v1/doctor?online=installation check: home, database, ffmpeg, keys, specs, fixtures, stalled jobs, recent failures
Library, review and memory
MCP toolRESTPurpose
search_assetsGET /v1/assets?query=&collection=&status=&min_rating=&limit=full-text search over prompts, tags, descriptions, review notes
similar_assetsGET /v1/similar?query=&asset_id=&collection=&limit=assets closest in meaning to a text or another asset
get_assetGET /v1/assets/{id}, /file, /previewmetadata (fills a missing preview and metadata), bytes, preview
import_assetPOST /v1/assets {path, tags, prompt}add a local file; restores provenance a file made here carries
update_assetPATCH /v1/assets/{id} {tags, description}edit tags and description
delete_assetDELETE /v1/assets/{id}remove the file and its entry
upload_assetPOST /v1/assets/{id}/upload {provider}upload by hand; returns the reference and where it goes in input
review_assetPOST /v1/review {asset_ids, status, rating, notes}approve, reject, rate 1–5
get_lineageGET /v1/assets/{id}/lineageancestors and descendants with the job of each step
get_statsGET /v1/stats?provider=&model=&collection=per provider+model: done/failed share, failure kinds, rating, approvals, cost, seconds
References and collections
MCP toolRESTPurpose
save_referencePOST /v1/references {name, kind, description, tags, asset_ids}create or update a reference
get_referenceGET /v1/references/{ref}one reference with its assets
list_referencesGET /v1/references?kind=&query=&limit=references, recently changed first
add_to_referencePOST /v1/references/{ref}/assets {asset_ids, role}append assets
remove_from_referenceDELETE /v1/references/{ref}/assets {asset_ids}drop assets from it
delete_referenceDELETE /v1/references/{ref}delete it; the assets stay
list_collectionsGET /v1/collectionscollections with asset counts
add_to_collectionPOST /v1/collections/{name}/assets {asset_ids, description}add assets, creating the collection
remove_from_collectionDELETE /v1/collections/{name}/assets {asset_ids}drop assets; an empty collection disappears
export_collectionGET /v1/collections/{name}/export?only_approved=ZIP of files, previews and collection.json
Budgets
MCP toolRESTPurpose
set_budgetPUT /v1/budgets {name, limit_usd}create a USD cap or change its limit
list_budgetsGET /v1/budgetslimit, spent, remaining
delete_budgetDELETE /v1/budgets/{name}remove a cap; its jobs stay
Media operations (ffmpeg, local, free)

Each result is a new asset made by a finished job with provider local, so lineage, history and save_to treat it like a generation. Without ffmpeg these tools fail with install instructions. CLI: ai-media-router media frame|reframe|trim|concat|overlay|mux.

MCP toolRESTPurpose
extract_framePOST /v1/media/extract_frame {asset_id, at, save_to}a video frame (first, last or seconds) as PNG
reframePOST /v1/media/reframe {asset_id, aspect, mode, save_to}new aspect ratio, centered crop or black pad
trimPOST /v1/media/trim {asset_id, start, end, save_to}cut a video or audio file
concatPOST /v1/media/concat {asset_ids, save_to}join videos in order (first clip's size, 30 fps)
overlayPOST /v1/media/overlay {base_asset_id, overlay_asset_id, x, y, width, save_to}a real packshot or logo over an image or video
mux_audioPOST /v1/media/mux_audio {video_asset_id, audio_asset_id, save_to}replace a video's sound
Snippets, recipes and MCP prompts
MCP toolRESTPurpose
save_snippetPUT /v1/snippets {name, text, description}a prompt block used as {{snippet:name}}
list_snippetsGET /v1/snippetssnippets with their text
delete_snippetDELETE /v1/snippets/{name}remove a snippet
save_recipePUT /v1/recipes {name, from_job | from_asset [chain] | provider+model+input [+offer_id, budget] | steps, description, save_to, variables}a generation template with {{var:name}}, a preset, or a chain of steps
list_recipesGET /v1/recipesrecipes with input or steps, and variables
get_recipeGET /v1/recipes/{name}one recipe
run_recipePOST /v1/recipes/{name}/run {vars, save_to, budget, estimated_cost_usd, offer_id, retry, reuse}one job from a recipe, or every step of a chain in one batch; for many single runs, generate_batch {recipe, matrix}
delete_recipeDELETE /v1/recipes/{name}remove a recipe; its jobs stay
MCP prompts/list, prompts/getGET /v1/prompts, GET /v1/prompts/{name}?<arg>=workflows product_shoot, ugc_ad, character_consistency, localization_fanout, fix_provider, and every recipe as recipe:<name>
Provider specs
MCP toolRESTPurpose
get_specGET /v1/providers/{key}/specactive spec TOML, its source, fixture names
check_specPOST /v1/specs/check {provider | spec, model, input, vars}validate, replay fixtures, render requests without sending
save_specPUT /v1/specs {spec}save an override, active immediately
reset_specDELETE /v1/providers/{key}/specback to the built-in spec

For agents. skills/ai-media-router/SKILL.md is a ready agent skill: setup, the generation loop, rules on money and failures, placeholders, batches and recipes. Copy it into your agent's skills folder. The MCP server exposes it as the resource ai-media-router://guide, along with every provider spec (ai-media-router://providers/<key>) and recipe (ai-media-router://recipes/<name>). AGENTS.md is for agents working on this repository.

Fixing a provider whose API changed

No rebuild and no restart. The same loop works from an agent over MCP and from a shell:

mermaid
flowchart LR    fail["job failed<br/>error_kind: spec"] --> trace["get_job {debug: true}<br/>raw request + response"]    trace --> read["get_spec<br/>active TOML"]    read --> edit["edit the TOML"]    edit --> check["check_spec<br/>validate, replay fixtures,<br/>render requests"]    check -->|errors| edit    check -->|ok| save["save_spec<br/>live in every process"]    save --> retry["generate again"]    save -.->|"undo"| reset["reset_spec"]
StepMCPCLI
See the raw exchangeget_job {job_id, debug: true}ai-media-router jobs <id> --debug
Read the specget_spec {provider}ai-media-router spec show kie
Check an editcheck_spec {spec, model, input}ai-media-router spec check kie --file kie.toml --model … --input '{…}'
Activatesave_spec {spec}ai-media-router spec save kie.toml
Undoreset_spec {provider}ai-media-router spec reset kie

Error messages already point at the broken field. For example: kie submit (builtin providers/kie.toml): capture id pointer /data/taskId matched nothing in the response: {…}.

To make a fix permanent:

  1. Copy the override into providers/kie.toml.
  2. Add a fixture under providers/fixtures/kie/. A debug trace already has the fixture shape.
  3. Run cargo test.

The format is described in docs/provider-spec.md.

Adding a provider is the same loop with a new file:

  1. Write providers/<key>.toml following the spec format. Put the provider's name from the price catalog into catalog.
  2. Add fixtures from the docs' example responses.
  3. Run cargo test.

Parts of a spec that have not been verified go into its notes, which get_provider shows to agents.

Architecture

mermaid
flowchart TB    subgraph interfaces["interfaces/ (thin wrappers)"]        mcp["mcp.rs<br/>tools · prompts · resources"]        rest["rest.rs<br/>/v1"]        cli["cli.rs<br/>commands, servers"]    end    api["api/: the one contract"]    mcp --> api    rest --> api    cli --> api    subgraph core["Core"]        jobs["jobs/<br/>worker, leases, chains, retries,<br/>batches, placeholders, preflight, doctor"]        pricing["pricing/<br/>catalog, estimates, budgets"]        templates["templates/<br/>snippets, recipes, prompts"]    end    subgraph library["library/"]        assets["assets · previews · media (ffmpeg) · provenance"]        refs["references · collections · lineage · embeddings"]        db[("db/schema.sql<br/>SQLite: WAL, FTS5, STRICT")]    end    subgraph engine["providers/ (engine)"]        registry["mod.rs<br/>built-ins + overrides, hot reload"]        run["engine.rs<br/>prepare → submit → poll → result"]        specs[/"providers/*.toml<br/>43 specs + fixtures"/]    end    api --> core    api --> library    core --> library    jobs --> engine    registry --> specs
  • Providers are data, not code. Each providers/<key>.toml describes:

    • auth;
    • the HTTP steps;
    • JSON pointers to ids, status and outputs;
    • uploads.

    build.rs embeds every spec and fixture. Files in <home>/providers/ override built-ins and are reloaded when they change.

  • One request type end to end. The request a spec renders is the same one that is sent, shown by check_spec, stored in a job's trace and compared with fixtures, so these cannot drift apart. Credentials never leave the auth layer: traces show <ENV_NAME> instead.

  • Auth schemes live in the engine, so specs only name env vars. Supported: bearer, header, query, headers, basic, jwt_hs256 (Kling) and command (gcloud tokens).

  • Jobs survive restarts and parallel processes. State lives in SQLite. Every process runs a worker, and each job is polled by whichever process wins the UPDATE … RETURNING lease. A retry or a job whose parents finished goes back through the same queue.

  • One schema, no migrations (pre-release). src/db/schema.sql creates the database. A library from another schema version is refused with a message instead of being migrated.

  • Results are always local. URLs, data URIs, base64 and authenticated downloads are all streamed into <home>/assets/, named by content hash.

<home>/  ai-media-router.db   jobs, assets, references, collections, lineage, batches, budgets,                       embeddings, snippets, recipes, upload cache (schema: src/db/schema.sql)  assets/              files, named by content hash  assets/previews/     JPEG previews  exports/             collection ZIPs  providers/           spec overrides (hot-reloaded)  config.toml          [env]: settings; secrets kept in the file (mode 0600) or a                       `keychain:<service>` marker  prices.json          video price catalog, cached for a day  image-prices.json    image price catalog, cached for a day

Testing

bash
cargo test

cargo test:

  • validates every spec: required steps, auth fields, pointer and template lints, unique keys;
  • replays every fixture;
  • runs the engine and the API end to end against a mock provider.
bash
python3 scripts/emulate.py --bin "$PWD/target/debug/ai-media-router"

scripts/emulate.py runs the real binary as an MCP server over stdio against a local emulator of every provider. The emulator answers the way each spec says that API answers. It checks:

  • every route of every provider: submit, poll, result, download and uploads;
  • every MCP tool, MCP prompt and MCP resource;
  • batches, chains and last_frame;
  • error kinds;
  • every recorded fixture, replayed over HTTP.

--only kie,fal narrows the run. CI runs both commands on Linux, and cargo test on macOS and Windows too.

FAQ

What is an MCP server for image and video generation? A program an AI agent such as Claude, Cursor or Codex calls through the Model Context Protocol to make images and videos. AI Media Router is one: the agent calls generate and get_job, and the router talks to the provider's API and saves the result.

Is AI Media Router free? Yes, MIT-licensed. You pay each provider directly at its own prices with your own key. The router adds no markup and has no account; ComfyUI jobs on your own server cost 0 USD.

Does it pick the cheapest provider automatically? No. search_prices lists offers cheapest first, and the agent or you choose. There is no silent routing or fallback, so cost and quality stay explicit decisions.

Which models can it use? Any model a supported provider serves, for example Seedance, Kling, Veo, Wan, Hailuo, Runway, Luma, LTX, FLUX, GPT Image, Nano Banana, Imagen and Seedream. model and input pass through unchanged.

Is it a fal MCP or Replicate MCP alternative? It covers fal and Replicate as two of its 43 providers, behind one set of tools, with price comparison, budgets and a local library on top. Use a provider's own MCP server if you only ever use that provider.

Is it like OpenRouter for images and video? In the sense of one interface over many providers, yes. Unlike OpenRouter it runs on your machine and has no account or balance: each provider bills you directly. OpenRouter is one of the providers it can call.

Does it work with ComfyUI? Yes. Set COMFYUI_URL and your server becomes provider comfyui: the input is an API-format workflow, and files referenced as {{asset:…}} in a LoadImage node are uploaded for you. Comfy's hosted partner models are the separate provider comfy.

Where are generated files stored? On your machine, in <home>/assets/, named by content hash and indexed in SQLite, with the prompt, provider, model and parents written into the file itself. Provider URLs that expire do not matter.

Can an agent overspend? Not with a budget set. set_budget creates a USD cap, and a job is refused before anything is sent when what was spent plus its estimate would exceed it. The check works from estimates, so a provider that bills more than its listed price can still go over by that difference.

License

MIT, see LICENSE.

來源:README.md,提交 f4b9472

工具

0
SourceWeft 曾在隔離沙箱中以無憑證方式啟動該軟體包一次,以列出其工具,但未能完成:該軟體包類型無法在沙箱中執行。

版本歷史

1
  1. v0.1.0最新Nov 1, 2026

評分

已滿足適用檢查項的 50%

該評分衡量此收錄條目的完整程度,依據註冊中心條目、端點與公開來源計算得出。它不是對伺服器本身的評價,也不表示伺服器安全或目前可用:可連通性是帶日期的檢查結果,評分也不包含安裝量或使用者評價。

  • 近 30 天內可連通25 分

    請保持端點能夠回應 MCP 握手;SourceWeft 每 30 天探測一次。

  • 已宣告認證方式10 分

    請在註冊中心條目中宣告憑證請求標頭,或保持端點能夠回應探測,以便判定其認證方式。

  • 已列出工具15 分

    請讓伺服器在無憑證時回應 tools/list,或發布可在沙箱中無憑證啟動的軟體包。

  • 描述必需15 分
  • 圖示10 分
  • 文件15 分
  • 授權條款10 分

其他 Router 整合