Backends¶
model.backend selects which client the engine talks through. The three are
not interchangeable by base_url alone — they send different request
bodies to different endpoints.
backend: |
Talks to | Key | Constrained decoding via |
|---|---|---|---|
ollama (default) |
Ollama's native /api/chat |
none | format: <schema> |
openai_compat |
a local llama.cpp server | none | llama.cpp's schema extension |
openai |
OpenAI, or any OpenAI-compatible endpoint | OPENAI_API_KEY |
standard json_schema, strict: true |
ollama¶
model:
backend: ollama
model: gemma4:27b
base_url: http://localhost:11434
think: false
Uses Ollama's native API rather than its OpenAI-compatible one, for two
capabilities the compat endpoint lacks: think: false, without which a
reasoning model puts its answer in a reasoning field and returns empty
content after burning the budget; and format: <json schema>, which is
what makes a vocab: field a decode-time guarantee.
openai_compat (llama.cpp)¶
model:
backend: openai_compat
model: ggml-org/Qwen3.8-27B-GGUF:Q4_K_M
base_url: http://127.0.0.1:8080/v1
think: false # sends chat_template_kwargs.enable_thinking=false
Speaks the OpenAI protocol, but relies on two llama.cpp extensions: a
schema key inside a json_object response format, and chat_template_kwargs
for think: false. Both are rejected by hosted APIs — which is why openai
is a separate backend rather than a different URL.
openai (hosted)¶
model:
backend: openai
model: <model-id> # required — no default, so nothing bills by accident
# base_url: # omit for OpenAI; set it for another provider
# strict: false # last resort if a provider chokes on the schema
params: # forwarded verbatim to the completions call
top_p: 0.9
The key comes from OPENAI_API_KEY, or model.api_key in the YAML. A
missing key or missing model id is a config error before the first request,
not a 401 per seed.
Anything exposing an OpenAI-compatible endpoint works here — Gemini, for
one, by pointing base_url at its compat endpoint and putting that key in
OPENAI_API_KEY.
Verify enums on a new provider
Structured-output support varies. Generate a handful of records first and confirm the enum fields actually came back constrained rather than assuming they did.
What changes when generation is metered¶
Enums still hold, via a schema rewrite. Hosted strict: true requires
additionalProperties: false and every property listed in required, with
optionality expressed as a null branch — the inverse of what the schema
builder emits. fields: is translated into that shape automatically, so a
vocab: field stays a hard decode-time constraint.
Cost is reported. Token usage accumulates per run and appears in the stats table beside the record counts.
Rate limits go to the SDK, whose backoff honours Retry-After on 429s.
The engine's own retry loop sleeps a flat second and is for bad records,
not throttling. Start at --workers 1 and raise it once you know your tier's
limit.
Why params: is a passthrough¶
reasoning_effort, max_completion_tokens and friends differ per provider
and per model generation. Pinning them as named options in the client would
date it, so whatever you put under params: goes to the completions call
verbatim. Nothing is sent by default beyond model, messages, temperature and
the response format.
Overriding at the command line¶
seedmill generate -t task.yaml --backend openai --model <id> --base-url https://…
--backend is an override, not a translator: it does not rewrite the YAML's
base_url, which is shaped for the backend it was written for. Override both
together.
Reasoning models¶
gemma4 and Qwen3 think by default. Both local backends disable it with
think: false — Ollama through its think flag, llama.cpp through
chat_template_kwargs.enable_thinking=false, which it forwards to the Jinja
template. On llama.cpp the reasoning text goes to reasoning_content and
never pollutes the parsed record, so leaving it on only costs latency (~30%
per batch, measured on Qwen3.8-27B).
The hosted openai backend has no think flag; where a provider exposes an
equivalent, pass it through params:.