AGPL-3.0-or-later · forever.

showcase · tier 2 · builders

Model bench

The same question fanned to three local models in parallel; a jq fan-in tabulates measured latency and length: no judge model, facts only.

Pick your default model on a measured table, not vibes: zero keys, zero spend. · conformance-gated in nika-spec ↗ · re-proven at every push

  • 5tasksthe plan falls out of the bindings
  • 3wavesparallel by construction
  • 2verbs exercisedinfer · invoke
  • T2the tierbuilders

The whole file

5 tasks · 3 waves

The plan falls out of the bindings, nothing is scheduled by hand.

model-bench.nika.yamlsource
nika: v1workflow:  id: model-bench  description: "One question → three local models → a measured comparison table"model: ollama/qwen3.5:4b # the house default · contenders below pick their own seatsconst:  question:    # A TYPED constant is `{ type, value }` — both keys, exactly those two.    # `{ type, default }` is not a declaration at all (01-envelope.md §const:    # an object missing either key is a bare literal object), so the prompt    # below would receive the whole `{type, default}` map instead of the    # question. Measured: the model gets a JSON blob and the bench is junk.    type: string    value: "Explain, in five lines, why a workflow should be audited before it runs."permits:  tools: ["nika:jq", "nika:write"]  fs: { write: ["out/model-bench.md"] }tasks:  ask_incumbent:    infer:      model: ollama/qwen2.5:14b # the quality bar · the seat to beat      prompt: "${{ const.question }}"      max_tokens: 600  ask_challenger:    infer:      model: ollama/llama3.2:3b # another family · same size class      prompt: "${{ const.question }}"      max_tokens: 600  ask_tiny:    infer:      model: ollama/qwen2.5:0.5b # the "is small enough?" probe      prompt: "${{ const.question }}"      max_tokens: 600  tabulate:    with:      ask_incumbent_duration_ms: ${{ tasks.ask_incumbent.duration_ms }}      ask_incumbent: ${{ tasks.ask_incumbent.output }}      ask_challenger_duration_ms: ${{ tasks.ask_challenger.duration_ms }}      ask_challenger: ${{ tasks.ask_challenger.output }}      ask_tiny_duration_ms: ${{ tasks.ask_tiny.duration_ms }}      ask_tiny: ${{ tasks.ask_tiny.output }}    invoke:      tool: "nika:jq"      args:        input:          - model: "ollama/qwen2.5:14b"            ms: "${{ with.ask_incumbent_duration_ms }}"            answer: "${{ with.ask_incumbent }}"          - model: "ollama/llama3.2:3b"            ms: "${{ with.ask_challenger_duration_ms }}"            answer: "${{ with.ask_challenger }}"          - model: "ollama/qwen2.5:0.5b"            ms: "${{ with.ask_tiny_duration_ms }}"            answer: "${{ with.ask_tiny }}"        expression: >-          "| model | latency | chars | answer (first 160) |\n|---|---|---|---|\n"          + (map("| " + .model + " | "               + (if .ms == null then "cached" else (.ms | tostring) + "ms" end) + " | "               + (.answer | length | tostring) + " | "               + (.answer | gsub("\n"; " ") | .[0:160]) + " |")             | join("\n"))  persist:    with:      tabulate: ${{ tasks.tabulate.output }}    invoke:      tool: "nika:write"      args:        path: out/model-bench.md        create_dirs: true        content: |          # Model bench — same question, measured          ${{ with.tabulate }}          Facts only: latency from the run's clock, length in characters.          The answers are side by side — the quality call is yours.outputs:  table:    value: ${{ tasks.tabulate.output }}    description: "The measured comparison table · latency + length per seat"

nika inspect · engine 0.106.1 · vendored graph, never re-derived

7 declared edges · 3 waves · the plan falls out of the bindings