Run Stanford STORM on a custom OpenAI-compatible endpoint.

Updated 2026-07-16

STORM builds every language model as a LitellmModel, and litellm accepts api_base. Put https://api.apisrouter.com/v1 in your shared openai_kwargs, prefix model ids with openai/, and all five LM slots of the article pipeline route through one endpoint and one key.

Quick answer: api_base in openai_kwargs, openai/ prefix on ids.

STORM's LitellmModel stores whatever kwargs you construct it with and merges them into every litellm.completion() call. litellm's api_base parameter is how you point the openai provider at a different host, so adding api_base to the openai_kwargs dict that STORM's own examples already use is the entire override. Prefix each model id with openai/ so litellm speaks the chat-completions protocol to that base, and the string after the slash is passed through to the gateway. Because the examples build one openai_kwargs dict and reuse it for every model, one added key reroutes the full pipeline. No STORM code changes, no fork; this is stock knowledge_storm behavior layered on litellm's documented routing.

openai_kwargs = {
    "api_key": os.getenv("APISROUTER_API_KEY"),
    "api_base": "https://api.apisrouter.com/v1",
    "temperature": 1.0,
    "top_p": 0.9,
}
fast = LitellmModel(model="openai/deepseek-v4-flash", max_tokens=500, **openai_kwargs)
strong = LitellmModel(model="openai/claude-sonnet-4-6", max_tokens=3000, **openai_kwargs)

How STORM splits an article across five LM slots.

STORM (stanford-oval on GitHub, roughly 30K stars) writes Wikipedia-style reports from scratch: it researches a topic through simulated multi-perspective conversations, builds an outline from what it learned, generates the full article section by section, and then polishes it. STORMWikiLMConfigs exposes that pipeline as five independently settable models: conv_simulator_lm and question_asker_lm drive the research conversations, outline_gen_lm structures the article, article_gen_lm writes it, and article_polish_lm does the final pass. The upstream README is explicit about the economics: the conversation simulator runs the highest call volume, so it recommends a faster model there and a more powerful model for article generation. That guidance assumed picking between OpenAI models; behind a multi-vendor endpoint it generalizes into something more useful. Each slot is its own LitellmModel with its own model string, so the research chatter can run on a fast DeepSeek id while outline and article generation run on Claude, and polish on whichever model you trust for tone, all authenticated by the same key against the same api_base. The retrieval side is separate machinery: STORM's runner takes an RM module (You.com, Bing, and several other search backends) with its own API key. Changing where the language models point does not touch how sources get fetched.

Full setup: five slots, one kwargs dict.

The working pattern mirrors the repo's own run scripts: build the shared kwargs once, construct one LitellmModel per role, and assign them through the STORMWikiLMConfigs setters. The api_key can be any name you like since you pass it explicitly; the example uses its own variable to make clear this is not an OpenAI account credential. litellm also honors provider-level environment variables, and the openai provider reads OPENAI_API_BASE, so an environment-only override is possible. The explicit kwargs path is still the one to prefer: it is visible in the code that produced a given article, it survives being run on a machine with different environment state, and it makes per-slot exceptions possible if you ever want one stage on a different endpoint.

import os
from knowledge_storm import STORMWikiRunnerArguments, STORMWikiRunner, STORMWikiLMConfigs
from knowledge_storm.lm import LitellmModel
from knowledge_storm.rm import YouRM

openai_kwargs = {
    "api_key": os.getenv("APISROUTER_API_KEY"),
    "api_base": "https://api.apisrouter.com/v1",
    "temperature": 1.0,
    "top_p": 0.9,
}
fast = LitellmModel(model="openai/deepseek-v4-flash", max_tokens=500, **openai_kwargs)
strong = LitellmModel(model="openai/claude-sonnet-4-6", max_tokens=3000, **openai_kwargs)

lm_configs = STORMWikiLMConfigs()
lm_configs.set_conv_simulator_lm(fast)
lm_configs.set_question_asker_lm(fast)
lm_configs.set_outline_gen_lm(strong)
lm_configs.set_article_gen_lm(strong)
lm_configs.set_article_polish_lm(strong)

engine_args = STORMWikiRunnerArguments(output_dir="./results")
rm = YouRM(ydc_api_key=os.getenv("YDC_API_KEY"), k=engine_args.search_top_k)
runner = STORMWikiRunner(engine_args, lm_configs, rm)
runner.run(topic="Small modular reactors")

Choosing models per pipeline stage.

Treat the five setters as a budget dial, not boilerplate. Upstream guidance already says to split fast and strong models across stages; a multi-vendor endpoint just widens the menu per stage. Change one slot at a time between runs on the same topic and diff the outputs, with the per-key usage log pricing each configuration.

  • conv_simulator_lm and question_asker_lm are the volume stages: multi-turn simulated interviews across several perspectives per topic. deepseek-v4-flash or another fast id keeps the research phase from dominating spend, and imperfect chatter is tolerable because it feeds notes, not prose.
  • article_gen_lm is the flagship slot. It writes long, structured, cited sections from accumulated research, which is sustained-generation work where claude-sonnet-4-6 or gpt-5.5 visibly outperforms smaller ids.
  • outline_gen_lm is few calls with outsized leverage, the same shape as a planning slot: a weak outline caps the article no matter how good the writer is. It is the natural place to test claude-opus-4-7.
  • article_polish_lm rewrites for flow and removes duplication across the assembled article, which benefits from a long-context id; gemini-3.1-pro-preview is worth benchmarking here.

Pay-as-you-go · transparent per-model pricing

Selected models are priced below official list prices. Exact input, output, cache, and per-request prices are shown for each model.

ModelOfficial PriceOur Price
DeepSeek V4 Flash$0.14 / $0.28 per M$0.13 / $0.25 per M
Claude Sonnet 4.6$3.00 / $15.00 per M$2.40 / $12.00 per M
Claude Opus 4.7$5.00 / $25.00 per M$4.00 / $20.00 per M
GPT-5.5$5.00 / $30.00 per M$4.00 / $24.00 per M
Gemini 3.1 Pro Preview$2.00 / $12.00 per M$1.60 / $9.60 per M

The failure modes specific to STORM.

A bare model id routes by inference, not by your api_base. litellm reads the prefix to pick a provider, and a prefixless Claude id gets inferred as an Anthropic-native call, which then wants ANTHROPIC_API_KEY and ignores your gateway entirely. Every id headed for the gateway must carry the openai/ prefix; the prefix names the protocol, not the vendor. One slot left behind. Each LitellmModel captures its kwargs at construction. If four slots share openai_kwargs and a fifth was built ad hoc without api_base, that slot silently posts to the vendor default and fails on auth, and the traceback names a pipeline stage rather than a config line. Build every slot from the same dict and this class of bug disappears. Retriever failures blamed on the endpoint. The research phase needs a working search backend; an invalid or exhausted retriever key (YDC_API_KEY, BING_SEARCH_API_KEY, or whichever RM you chose) fails runs during information gathering. That phase interleaves with LM calls, so read the traceback for which client raised before touching the LM config. The demo's secrets.toml is not your script's config. The Streamlit demo reads secrets.toml; programmatic runs read whatever your script passes. Editing one while running the other is a classic mismatch. max_tokens is per-slot too. STORM's examples set small limits on the fast slots (500) and larger on generation (3000). Pointing a slot at a long-form model without raising its max_tokens quietly truncates sections, which looks like a model quality problem but is a config number.

Who routes STORM through a gateway.

  • Teams generating knowledge reports at volume (briefs, wiki-style internal docs, topic primers), where the five-slot split makes per-stage cost tuning worth real money.
  • Researchers studying pipeline composition: which stage benefits from a stronger model is an empirical question, and one endpoint makes the grid of slot-model combinations trivial to enumerate.
  • Builders running Claude or Gemini in the writing slots of an OpenAI-shaped stack, without adding a vendor SDK per model family.
  • Anyone running batch topic lists, where research-phase volume multiplies across topics and the usage log becomes the per-topic cost ledger.
  • Developers without access to a given vendor's billing. Top-up based access with no card requirement removes the per-provider sign-up dependency.

Verify the endpoint and debug the first article.

List the gateway's models first: the string after openai/ in each slot must match a served id exactly. First-run failures follow the pipeline order. An auth error naming Anthropic or Google means a prefixless id routed to a native provider; add openai/. A 401 from the gateway means the api_key in your kwargs is not the gateway key. A model-not-found error names the slot whose id has a typo. Failures during the research phase that mention your search backend are retriever credentials, not LM routing. And truncated or oddly short article sections are usually a stingy max_tokens on the generation slot rather than anything upstream. A full STORM run is a large burst: simulated conversations across perspectives, then outline, generation, and polish. Once one completes, the APIsRouter console shows per-request model, token counts, and spend, which maps cleanly onto the five slots and tells you exactly which stage to retune before the next batch of topics.

curl -s https://api.apisrouter.com/v1/models \
  -H "Authorization: Bearer $APISROUTER_API_KEY" | head -50

FAQ

How does STORM support a custom OpenAI-compatible endpoint?

Through litellm. STORM builds every LM as a LitellmModel, which merges its constructor kwargs into each litellm.completion() call, and litellm accepts api_base for the openai provider. Add api_base to the openai_kwargs dict and every slot built from it routes to the gateway.

Why do model ids need the openai/ prefix?

litellm picks the provider from the prefix. openai/claude-sonnet-4-6 means "speak the OpenAI chat-completions protocol to my api_base with model claude-sonnet-4-6". Without the prefix, litellm infers the vendor from the name and routes natively, bypassing your endpoint.

Can different STORM stages use different vendors' models?

Yes. Each of the five slots is an independent LitellmModel, so the conversation simulator can run a DeepSeek id while article generation runs Claude and polish runs GPT, all through the same api_base and key. Upstream already recommends splitting fast and strong models across stages.

Does the search retriever change when I change api_base?

No. Retrieval runs through the RM module you pass to STORMWikiRunner (You.com, Bing, and other supported backends) with its own key. LM routing and source retrieval are independent systems that fail in different phases of a run.

Is there an environment-variable path instead of kwargs?

litellm honors provider-level variables, and the openai provider reads OPENAI_API_BASE. It works, but the explicit api_base kwarg is more reproducible: it travels with the script, survives machines with different environment state, and permits per-slot exceptions.

How many tokens does one STORM article consume?

The research phase dominates: multi-perspective simulated conversations multiply calls before a word of the article exists, then generation and polish add long-form output on top. Full runs commonly land in the hundreds of thousands of tokens, and the per-key usage view shows the exact per-stage split.