WebGraph

Providers

Bring your own key -- OpenAI-compatible endpoints (OpenAI, Groq, Together, OpenRouter, DeepSeek, Mistral, xAI, Ollama, LM Studio, vLLM), Anthropic and Gemini -- through three raw-httpx adapters; where the key travels and where it never goes.

WebGraph talks to models through three small adapters in packages/engine/src/webgraph/kg/providers.py, all plain httpx -- no vendor SDK, no LiteLLM. Every system the design studied ended up with "one module per vendor plus an OpenAI-compatible route with a base_url"; that is what this is, without adding anything to pip install webgraph.

providerendpointstructured output
openai-compatible{base_url}/chat/completionsresponse_format: json_schema (strict) → json_object with the schema in the prompt → plain prompt. A 400 on one rung steps down and remembers.
anthropic/v1/messagesoutput_config.format: json_schema; schema-in-prompt fallback
geminimodels/{model}:generateContentresponseJsonSchema; schema-in-prompt fallback
fakenonedeterministic; for tests and the CI benchmark

Responses are validated against the extraction schema with jsonschema regardless of mode, then every quote is verified against the section (see the provenance rule). Retries: 408, 409, 429 and 5xx with backoff, four attempts; any other 4xx is an error straight away.

Configuration shape

{
  "provider": "openai-compatible",
  "base_url": "http://localhost:11434/v1",
  "model": "qwen3:8b",
  "answer_model": null,
  "api_key": null,
  "api_key_env": null,
  "json_mode": "json_schema",
  "price_per_m_in": null,
  "price_per_m_out": null,
  "max_concurrency": 4
}

provider also accepts a preset name, which fills in the base_url and the conventional key variable: openai, groq, together, openrouter, deepseek, mistral, xai, ollama, lmstudio, vllm, anthropic, gemini. model is required (extraction is the volume; pick a small one); answer_model is optional and used for questions only. Prices are per million tokens and only turn the estimate and the max_usd cap into dollars -- nothing is guessed when they are unset.

From the environment

The server's or shell's defaults, read by ProviderConfig.from_env():

variablemeaning
WEBGRAPH_LLM_PROVIDERopenai-compatible, anthropic, gemini, or a preset name
WEBGRAPH_LLM_BASE_URLoverrides the preset's URL
WEBGRAPH_LLM_MODEL, WEBGRAPH_LLM_ANSWER_MODELthe models
WEBGRAPH_LLM_API_KEYa key, for a self-hosted deployment that owns one
WEBGRAPH_LLM_API_KEY_ENVthe name of another variable holding the key (e.g. OPENAI_API_KEY)
WEBGRAPH_LLM_JSON_MODEjson_schema (default), json_object, prompt
WEBGRAPH_LLM_CONCURRENCYsections in flight at once (default 4)
WEBGRAPH_LLM_PRICE_IN, WEBGRAPH_LLM_PRICE_OUTdollars per million tokens

Per request

POST /api/graph/build and POST /api/graph/query take a provider object of the same shape; whatever it carries overrides the environment default for that request.

Where the key goes, and where it does not

Keys stay local

A key arrives in the request body (the web UI) or from an environment variable (the CLI). It is held in a ProviderConfig field excluded from repr, sent as a header to the provider, and nowhere else.

  • Never persisted. Nothing writes the config to the SQLite store, a run trace or a log. ProviderConfig.redacted() -- provider, URL, model, has_key: true|false -- is the only form that appears in an event.
  • Never echoed. FastAPI's default 422 handler returns the offending request body; the API replaces it with one that redacts api_key and password on every route.
  • Never on a command line. webgraph kg takes --api-key-env NAME, not a key, and sync-neo4j reads the password from NEO4J_PASSWORD.
  • Provider error bodies are truncated to 300 characters before they enter an error message, since some providers echo the request.

Ollama and other local servers

ollama pull qwen3:8b
WEBGRAPH_KG=1 webgraph kg build https://example.edu/ --provider ollama --model qwen3:8b

Ollama serves /v1/chat/completions and accepts response_format with a JSON schema; it has no tool_choice, which is why the adapters never rely on forced tool calls. LM Studio (lmstudio, port 1234) and vLLM (vllm, port 8000) work the same way; for anything else that speaks the OpenAI shape, pass --base-url. Small local models break schemas more often -- the json_mode ladder and the one retry bound the damage, and the build's rejection_rate tells you when a model is not reading the block markers (above ~15% for a site, try a larger model or --json-mode json_object).

A custom endpoint

Any URL that serves the OpenAI chat-completions shape:

{"provider": "openai-compatible", "base_url": "https://llm.internal.example/v1", "model": "my-model", "api_key_env": "INTERNAL_LLM_KEY"}

api_key_env names a variable on the machine running the command. Over the API it may only name one of the conventional key variables (OPENAI_API_KEY, ANTHROPIC_API_KEY, GEMINI_API_KEY, GROQ_API_KEY, …, WEBGRAPH_LLM_API_KEY); anything else is refused with a 422. Otherwise a request could name any variable on the server and a base_url to post it to. For a custom variable, set WEBGRAPH_LLM_API_KEY_ENV on the server or send api_key in the request. A preset's key is sent only to the preset's host, decided by host and scheme, not by the URL's prefix.