Skip to content

Templating

Every VidaiMock response is a Tera template (Jinja2-style). This is what makes the mock dynamic rather than a static fixture — a template can reflect request data, generate IDs, branch on fields, and synthesise realistic responses without any mocking code.

Request context

Templates receive the parsed request and request metadata:

Variable Contents
json The full parsed request body. json.model, json.messages, json.tools, …
headers Request headers (lowercased keys).
query URL query parameters.
path_segments Path split on / (e.g. ["v1", "chat", "completions"]).
status_code The resolved HTTP status (available in error templates).

Built-in helper functions

Helper Returns Use
uuid() random UUID string IDs — chatcmpl-{{ uuid() }}, msg_{{ uuid() }}
timestamp() unix seconds (int) created / created_at fields
iso_timestamp() ISO-8601 string human-readable timestamps
random_int(min, max) integer mock token counts, call IDs
random_float(min, max) float embedding values, scores
has_tool_result(messages, provider) bool agentic loop termination

has_tool_result(messages, provider)

Detects whether the conversation history already contains a tool result, so a template can switch from "emit a tool call" to "emit a plain-text answer" and agentic loops terminate. Implemented in Rust for robust deep-JSON inspection.

provider Detection
openai any message with role == "tool"
anthropic a user message whose content[] contains type == "tool_result"
gemini user content whose parts[] contains a functionResponse

Default is openai when provider is omitted. Malformed/missing input returns false, never raises — safe inside any {% if %} guard. Full explanation: Agentic workflow testing.

Branching example

The bundled OpenAI chat template, conceptually:

{% if json.tools and has_tool_result(messages=json.messages, provider="openai") %}
  {# tools declared but a tool result is already in history → answer in text #}
  { ... "finish_reason": "stop", "message": {"content": "Based on the tool results..."} }
{% elif json.tools %}
  {# tools declared, no result yet → emit a tool_call echoing the caller's name #}
  { ... "finish_reason": "tool_calls",
    "message": {"tool_calls": [{"function":
      {"name": "{{ json.tools.0.function.name }}", "arguments": "{}"}}]} }
{% elif json.response_format %}
  {# structured output → JSON-string content #}
{% elif json.model is starting_with("o") %}
  {# reasoning model → include reasoning_tokens in usage #}
{% else %}
  {# default → plain assistant text #}
{% endif %}

Reflecting request data

{
  "id": "chatcmpl-{{ uuid() }}",
  "created": {{ timestamp() }},
  "model": "{{ json.model }}",
  "choices": [{
    "message": {
      "role": "assistant",
      "content": "You said: {{ json.messages | last | get(key='content') }}"
    },
    "finish_reason": "stop"
  }]
}

Deep array access

Tera is unreliable at indexing into arrays of mixed-type objects. For history-spanning logic (like detecting a tool result) use the has_tool_result() helper rather than hand-rolling json.messages | … array walks. This is why that helper exists in Rust.

Streaming templates

Streaming providers use a lifecycle block (on_start, on_chunk, on_stop) plus optional frame_format: raw for full SSE control. The per-chunk content is available as chunk. Details: Streaming and Writing templates.

Where templates live

Bundled templates are embedded in the binary under config/templates/. Override any of them, or add your own, via --config-dir. See Overriding bundled defaults.