Writing Templates¶
A response template is a Tera file under
config/templates/. See Templating for the request
context and helper catalogue; this page is the practical how-to for authoring
your own.
A minimal non-streaming template¶
my-config/templates/acme/response.j2:
{
"id": "resp-{{ uuid() }}",
"created": {{ timestamp() }},
"model": "{{ json.model | default(value='acme-1') }}",
"output": "Echo: {{ json.input | default(value='(none)') }}"
}
Wire it up with a provider:
# my-config/providers/acme.yaml
name: "acme"
matcher: "^/acme/generate$"
response_template: "acme/response.j2"
./vidaimock --config-dir ./my-config &
curl http://localhost:8100/acme/generate -H 'Content-Type: application/json' \
-d '{"model":"acme-1","input":"hello"}'
Branching¶
Use Tera control flow to vary the response by request fields. Always guard
against missing fields with default or the has_tool_result helper:
{% if json.tools and has_tool_result(messages=json.messages, provider="openai") %}
{ ... plain-text answer, finish_reason: "stop" ... }
{% elif json.tools %}
{ ... tool_calls echoing {{ json.tools.0.function.name }} ... }
{% else %}
{ ... default text ... }
{% endif %}
Streaming templates¶
A streaming provider declares a lifecycle. Each stage is a template:
stream:
enabled: true
frame_format: raw
lifecycle:
on_start: { template_path: "acme/stream_start.j2" }
on_chunk: { template_path: "acme/stream_delta.j2" }
on_stop: { template_path: "acme/stream_stop.j2" }
on_startrenders once at the beginning.on_chunkrenders once per content chunk; the chunk is available aschunk(a string for text, or a structured value for a tool call).on_stoprenders once at the end.
frame_format¶
| Value | Behaviour |
|---|---|
| (unset / default) | Engine wraps each rendered chunk as data: <chunk>\n\n. |
raw |
Template emits its own framing verbatim. Blank lines in the template output are preserved as SSE event separators, so one template can emit a multi-event sequence. |
Use raw for typed-event providers (Responses API, Anthropic). Example
on_stop that emits a finish chunk, an optional usage chunk, then [DONE],
each as its own SSE event:
data: {"choices":[{"delta":{},"finish_reason":"stop"}]}
{% if json.stream_options.include_usage %}
data: {"choices":[],"usage":{"total_tokens":42}}
{% endif %}
data: [DONE]
Handling structured chunks (tool calls)¶
When the response is a tool call, chunk is a structured value, not a
string. Branch on its type so you emit a single tool frame rather than
word-chunking JSON as fake text:
Gotchas¶
- Deep array indexing is unreliable in Tera. For history-spanning logic
use
has_tool_result(); for first-element access preferjson.tools.0.function.namewith a| default(...)fallback. - Keep JSON valid after interpolation. Quote string values and use
| json_encode()when embedding structured data, otherwise a stray quote inchunkbreaks the JSON. error_templateruns on ≥ 400. If your provider can return errors, give it anerror_templateso the failure body is provider-shaped. Thestatus_codevariable is in scope there.- Validate with
--strict-equivalent thinking. Run the endpoint and pipe the body throughjq/od -cfor streaming to confirm framing.
Where bundled templates live (read them for examples)¶
config/templates/ in the repo. Good references:
openai/chat.json.j2— full multi-branch chat (tools, reasoning, structured output, loop termination).anthropic/stream/*.json.j2— typed 7-event streaming with tool-mode branching.gemini/stream_chunk.json.j2/stream_final.json.j2— Gemini's delta-then-terminal pattern.*/error.json.j2— provider-shaped error envelopes.