Skip to main content

Fuse multiple AI models and judge the best answer for coding

Project description

Fuse Fable

PyPI Python CI License: MIT

๐ŸŒ Languages: English ยท เน„เธ—เธข (Thai)

Fan out a coding prompt to many AI models in parallel, then let a judge pick the single best answer โ€” total latency โ‰ˆ x2 (slowest model + one judge pass), not x7โ€“x15.

Works three ways: as a CLI, as an MCP server (connect Cursor / VS Code / Claude), and as a subagent / pipe (callable by other tools and scripts).

Install

pip install fusefable            # core
pip install "fusefable[mcp]"     # if you want the MCP server

From source:

git clone https://github.com/proultrax9/fusefable.git
cd fusefable
pip install -e ".[mcp]"

Setup (first run)

fusefable config
  • Choose AI Gateway โ†’ one key for everything, then it asks "how many models?" and prompts for each one.
    • Known gateways/providers (base URL auto-filled): openrouter, groq, together, fireworks, deepinfra, novita, hyperbolic, aimlapi, portkey, deepseek, openai, minimax, mimo โ€” any other works too, just type its base URL.
  • Or Single providers โ†’ it asks how many, then the API kind of each:
    • openai_compat โ€” any OpenAI-compatible endpoint (you provide the base URL)
    • anthropic โ€” Anthropic native (/v1/messages, base URL auto-filled)
    • google โ€” Google Gemini native (generateContent, base URL auto-filled)

Set your API key as an environment variable named as the wizard asks:

export OPENROUTER_API_KEY=sk-...      # macOS/Linux
setx OPENROUTER_API_KEY "sk-..."      # Windows (open a new terminal afterwards)

Config is stored at ~/.fusefable/config.yaml.

1) Use as a CLI

fusefable ask "Write a quicksort function in Python"
fusefable ask --show-all "..."                   # show every answer + judge reason
fusefable ask --models gpt-5,qwen3-coder "..."   # restrict to specific models
fusefable ask --cheap "..."                      # use cheap_models from config
ff ask "..."                                      # short alias

2) Use as a subagent / in a pipe

fusefable ask --quiet "..."                # print only the answer (no headers)
echo "Explain this code" | fusefable ask --quiet   # read the prompt from stdin
cat bug.py | fusefable ask -q "Find the bug in this code"
fusefable ask --json "..."                 # JSON output: answer, chosen_model, reason, cost, candidates

--json is ideal for scripts/agents that parse the result; --quiet is ideal for piping.

3) Use as an MCP server (Cursor / VS Code / Claude / other agents)

Run as an MCP server over stdio:

fusefable mcp

Exposes a tool fuse_ask(question, models?, cheap?) for any MCP client.

Cursor

~/.cursor/mcp.json (or Settings โ†’ MCP):

{
  "mcpServers": {
    "fusefable": {
      "command": "fusefable",
      "args": ["mcp"],
      "env": { "OPENROUTER_API_KEY": "sk-..." }
    }
  }
}

VS Code (Copilot / MCP-compatible extension)

.vscode/mcp.json in your project:

{
  "servers": {
    "fusefable": {
      "command": "fusefable",
      "args": ["mcp"],
      "env": { "OPENROUTER_API_KEY": "sk-..." }
    }
  }
}

Claude Desktop

claude_desktop_config.json:

{
  "mcpServers": {
    "fusefable": {
      "command": "fusefable",
      "args": ["mcp"],
      "env": { "OPENROUTER_API_KEY": "sk-..." }
    }
  }
}

Hermes Agent (Nous Research)

Hermes is an MCP client (usable over Telegram, Discord, etc.). Add Fuse Fable as a stdio MCP server in your Hermes config (mcp_servers):

{
  "mcp_servers": {
    "fusefable": {
      "transport": "stdio",
      "command": "fusefable",
      "args": ["mcp"],
      "env": { "OPENROUTER_API_KEY": "sk-..." }
    }
  }
}

Then run hermes mcp install (or restart the gateway) to pick it up. See the Hermes MCP docs. The fuse_ask tool becomes available to your Hermes agent โ€” handy when driving it remotely (e.g. via Telegram) against external providers like MiniMax or MiMo.

Requires pip install "fusefable[mcp]" and a completed fusefable config. If fusefable isn't on the app's PATH, use a full path such as python -m fusefable.cli.

Ensemble, cache & budget

fusefable ask --ensemble "..."     # merge all answers into one (vs picking one)
fusefable ask --cache "..."        # reuse the answer for an identical question
fusefable ask --no-cache "..."     # force a fresh run
  • Ensemble mode (--ensemble, config fusion_mode: ensemble): instead of the judge picking one answer, a model synthesizes a single answer combining the strengths of all candidates (anonymized). Falls back to the first answer if synthesis fails.
  • Cache (--cache, config cache: true, cache_ttl_seconds): identical question + same models/mode/compression returns the stored answer instantly with no API calls (cached, $0). Stored in ~/.fusefable/cache/. cache_ttl_seconds: 0 = never expires.
  • Budget cap (config budget_cap_usd, budget_action: warn|stop): before firing, the run estimates cost. If it exceeds the cap โ€” warn prints a warning and continues, stop aborts before spending anything.

Prompt compression (save tokens)

Reduce token usage while keeping answer quality โ€” useful when you pay per-provider directly. Two tiers, opt-in via --compress:

fusefable ask --compress "<long prompt or pasted code>"
# [compressed: 5200โ†’1800 chars, ~65% saved via llm]
  • Tier 1 (lossless): trims trailing whitespace, collapses blank lines, strips zero-width chars โ€” keeps indentation and inner spacing intact (safe for code).
  • Tier 2 (LLM): for prompts above compress_min_chars (default 2000), a cheap model compresses semantically โ€” once, then the compressed prompt is sent to all models, so you save tokens ร— number-of-models.
  • Quality guards: prompts under the threshold skip the LLM; if the compressed result is empty, longer, or under 30% of the original, it falls back to the lossless text. The judge always sees the original question.

Config (~/.fusefable/config.yaml): compress, compress_min_chars, compress_model (empty = reuse the judge model).

Architecture

              ENTRYPOINTS (one shared core)
   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
   โ”‚  CLI        โ”‚  pipe / subagent โ”‚  MCP server         โ”‚
   โ”‚ fusefable   โ”‚ stdin ยท --quiet  โ”‚ fusefable mcp       โ”‚
   โ”‚   ask "..." โ”‚      ยท --json    โ”‚ tool: fuse_ask()    โ”‚
   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
          โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                          โ”‚
                          โ–ผ
                โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”      ~/.fusefable/config.yaml
                โ”‚   core.fuse()     โ”‚โ—€โ”€โ”€โ”€โ”€ (gateway | single providers,
                โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜        models, judge, timeout)
                          โ”‚
                          โ–ผ
                โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                โ”‚  routing          โ”‚  build (provider, model) routes
                โ”‚  + provider       โ”‚  via factory โ†’ pick adapter by kind
                โ”‚    factory        โ”‚
                โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                          โ–ผ
        โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€  FAN-OUT (asyncio.gather)  โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
        โ”‚   เธ—เธธเธเธ•เธฑเธงเธขเธดเธ‡เธžเธฃเน‰เธญเธกเธเธฑเธ™ ยท per-model timeout ยท เธ•เธฑเธงเธžเธฑเธ‡ = เธ•เธฑเธ”เธ—เธดเน‰เธ‡       โ”‚
        โ”‚                                                         โ”‚
        โ–ผ            โ–ผ              โ–ผ                โ–ผ            โ–ผ
   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
   โ”‚ openai_ โ”‚ โ”‚ anthropicโ”‚  โ”‚  google    โ”‚   โ”‚ openai_  โ”‚  โ”‚  ...   โ”‚
   โ”‚ compat  โ”‚ โ”‚ native   โ”‚  โ”‚  native    โ”‚   โ”‚ compat   โ”‚  โ”‚        โ”‚
   โ”‚(gateway)โ”‚ โ”‚/v1/msgs  โ”‚  โ”‚generateContโ”‚   โ”‚          โ”‚  โ”‚        โ”‚
   โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”˜
        โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                          โ”‚  Completion[] (เธชเธณเน€เธฃเน‡เธˆเน€เธ—เนˆเธฒเธ™เธฑเน‰เธ™)
                          โ–ผ
                โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                โ”‚  judge            โ”‚  เธ›เธเธ›เธดเธ”เธŠเธทเนˆเธญ โ†’ Answer A/B/C...
                โ”‚  (anonymized)     โ”‚  เนƒเธซเน‰ judge model เน€เธฅเธทเธญเธเธ•เธฑเธงเธ”เธตเธชเธธเธ”
                โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  (เธžเธฑเธ‡ โ†’ fallback เธ•เธฑเธงเนเธฃเธ)
                          โ–ผ
                โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                โ”‚  FinalAnswer      โ”‚  text ยท chosen_model ยท reason
                โ”‚  (+ cost estimate)โ”‚  ยท cost_usd ยท candidates
                โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Request lifecycle

  1. Entrypoint โ€” CLI, a pipe/subagent call, or the MCP tool fuse_ask() โ€” all funnel into one core function core.fuse().
  2. Routing โ€” config (gateway or single providers) is turned into (provider, model) routes; a provider factory picks the right adapter per kind (openai_compat / anthropic / google).
  3. Fan-out โ€” every model is called concurrently via asyncio.gather (total time = slowest model). Each call has its own timeout; any model that times out or errors is dropped and never slows the run.
  4. Judge โ€” model names are anonymized (Answer A/B/C...) so the judge picks on quality alone, not by brand; if the judge fails it falls back to the first answer.
  5. Result โ€” returns the best answer with the chosen model, the judge's reason, an estimated cost, and all candidates.

Components (fusefable/)

File Responsibility
cli.py Typer CLI (ask / config / mcp), output modes
mcp_server.py MCP server exposing the fuse_ask tool
core.py shared fuse() entrypoint + model selection
config.py load/save config.yaml
wizard.py interactive setup (gateway vs single, API kind)
routing.py config โ†’ (provider, model) routes
providers/factory.py pick adapter by kind
providers/openai_compat.py ยท anthropic.py ยท google.py provider adapters
fanout.py parallel fan-out (drops failures)
judge.py anonymized judging
fusion.py orchestrator: fan-out โ†’ judge โ†’ FinalAnswer
cost.py token-based cost estimate
models.py dataclasses: Completion, FinalAnswer, ProviderConfig

Development

pip install -e ".[dev,mcp]"
pytest -q

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

fusefable-0.3.1.tar.gz (31.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

fusefable-0.3.1-py3-none-any.whl (28.3 kB view details)

Uploaded Python 3

File details

Details for the file fusefable-0.3.1.tar.gz.

File metadata

  • Download URL: fusefable-0.3.1.tar.gz
  • Upload date:
  • Size: 31.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for fusefable-0.3.1.tar.gz
Algorithm Hash digest
SHA256 abebfab17f7215b41ef2b67e4316c50377f4680e2d1e44974c9e60231c42f1a8
MD5 227666c691c96e227325ea9905aa7f43
BLAKE2b-256 952069599ba4f843b2a1b0196037d5242ee4bf2327ff6ef109b8185e4b85a959

See more details on using hashes here.

File details

Details for the file fusefable-0.3.1-py3-none-any.whl.

File metadata

  • Download URL: fusefable-0.3.1-py3-none-any.whl
  • Upload date:
  • Size: 28.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for fusefable-0.3.1-py3-none-any.whl
Algorithm Hash digest
SHA256 8e001ab297763b4faf97ceb60ceaf02de1b99f23a5b1642e93a7f7f9021d7df8
MD5 8a9894f81d1df251b47a0bbf4b1bbe63
BLAKE2b-256 4bc0036597aaf601df00bf4336bb31a742d857c7f1366b4883570af48bdcb0bb

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page