dhruvvarshney1/fanout-mcp
Run many independent subtasks in parallel on cheap, local or open-source models and return only the distilled result.
Changelog
[Unreleased]
Added
-
Core pydantic contracts:
SubTask,WorkerResult,RunSummary,FanOutResult. -
Project scaffolding: uv, ruff, mypy strict, pytest.
-
Provider layer:
Providerprotocol,OpenAICompatibleProvider,OllamaNativeProvider. -
providers.yamlloader with${ENV_VAR}interpolation and named profiles with fallback. -
fan_outengine: global + per-provider throttling, per-task timeout, wall-clock budget, jittered retry on 429/5xx/connection errors, and an optional cost ceiling. -
.mcp.jsonregistering the server for Claude Code,docs/local-setup.md, andscripts/dogfood/(three real fan-out tasks run against this repo). -
${VAR:-default}fallbacks inproviders.yamlinterpolation, soOLLAMA_BASE_URLis overridable without being required. -
FastMCP server (
dhruvvarshney.server,fanout-mcp) exposingfan_out,list_profilesandget_result, withfull/truncated/referencereturn modes, untrusted-output delimiters andfanout://runs/...resources. -
fan_outtakesper_task_timeout_sandwall_clock_budget_s; local models are far slower than the old fixed 60s ceiling allowed. -
thinkonProviderConfig, forwarded by the native Ollama provider, plusFielddescriptions on everySubTaskfield. -
list_profiles()reports each profile's effectiveconcurrency. -
JSONL run log at
$FANOUT_LOG: one line per task plus a run-summary line. -
fanout reportCLI (argparse, no new deps): runs, cost by provider/model, p50/p95 latency, failure rate, and savings against a configurable flagship rate. -
Optional OpenTelemetry spans, one per run and one per task, behind
FANOUT_OTEL=1and theotelextra. The import is guarded, so neither is required. -
Pluggable aggregation via
AGGREGATORS, selected per call:passthrough,concat,vote(majority with deterministic tie-breaking, counts and dissent),rank(a cheap model scores each output againstoriginal_intent, top-k wins) andmap_reduce(one model reduces them all; only the reduction is inline).rankandmap_reducecall the engine recursively and degrade topassthroughwith a note if their own model call fails. -
Claude Code plugin packaging:
.claude-plugin/plugin.json(withuserConfigfor the Ollama URL and model tag, optional Groq/OpenRouter keys, default profile and default return mode), a relocatable.mcp.jsonlaunching the server throughuvx --from ${CLAUDE_PLUGIN_ROOT},.claude-plugin/marketplace.json, thefanoutskill (when fan-out pays off, how to decompose, when not to bother) and the/fanout-reportcommand. -
FANOUT_PROFILEandFANOUT_RETURN_MODEset the defaults for afan_outcall that names neither; an empty or unrecognised return mode reads astruncated, since a plugin option arrives as an env var either way. -
docs/distribution.md: uvx vs. a PyInstaller binary vs. an HTTP container, and why uvx now and HTTP later. -
Multi-harness packaging generated from the canonical
.mcp.jsonand.claude-plugin/plugin.jsonbyscripts/gen_manifests.py: rootmcp.jsonandplugin.json(Agent Plugins 1.0, read by Copilot and Cursor),.agents/plugins/marketplace.json,.codex-plugin/{plugin,mcp}.json(with the Codexinterfaceblock) and.cursor-plugin/plugin.json. Generated, not symlinked: the harnesses disagree on JSON shape and on path-variable vocabulary.--checkfails on drift, from CI and frompytest. -
docs/install/: one page per harness — Claude Code, Codex, Copilot CLI, Cursor, Gemini CLI, Zed, OpenCode. -
GitHub Actions CI: ruff, mypy, the manifest drift check, pytest.
-
The default
providers.yamlships inside the package, souvx --from git+<repo> fanout-mcpruns with no config path and no environment — the only launch form harnesses without a plugin-root variable can express.FANOUT_CONFIG, then aproviders.yamlin the working directory, still win.
Removed
providers.example.yamlat the repo root; it is nowsrc/dhruvvarshney/providers.default.yamland ships in the wheel.- No OpenCode TypeScript plugin. OpenCode already speaks MCP via
opencode.json, so a wrapper would add an npm package, a release step and a second language to ship aSKILL.mdthatAGENTS.mdalready reads. Reasoning indocs/distribution.md.
Changed
ProviderConfigandPricingreject unknown keys, so a misspelt rate likeinput_per_1m_usdfails loudly instead of silently costing 0.return_modedefaults totruncated, notfull.- Result envelope states the untrusted-data warning, provider and model once per
run instead of once per task, and carries
chars=on every block. Cuts per-task overhead ~56% inline and ~65% inreferencemode. - Engine per-task timeout default 60s -> 120s.
aggregationwent from a one-value placeholder to a real registry.
Fixed
- A worker that returns empty text is an
errorcarryingfinish_reasonand the token counts, not anokwith nothing in it. - Native Ollama replies that put everything in
message.thinkingand stopped are no longer read as empty. Same forreasoning/reasoning_contenton the OpenAI-compatible path (NVIDIA NIM, vLLM).