Types reference
Step kinds
An advisory prior on difficulty. The classifier’s fallback matches ten English keywords and rounds up when unsure, so every ambiguity is billed as overspend — and a non-English workload has almost no signal at all.
| Kind | Work |
|---|---|
classification | Pick a label from a known set |
extraction | Pull structured fields from supplied text |
summarization | Condense supplied text |
formatting | Reshape without adding judgment |
translation | Convert between languages or notations |
question_answering | Answer from supplied context |
generation | Open-ended prose a person will read |
planning | Decide the next action |
reasoning | Multi-hop inference where the chain is the work |
code | Write or modify code |
evaluation | Grade or verify another output |
other | Carries no prior |
An unrecognized value degrades to other rather than failing the request, so an
SDK newer than the edge does not break your calls.
Outcome signals
| Field | Type | Meaning |
|---|---|---|
schema_valid | bool | The response parsed and validated |
tool_executed | bool | The emitted tool call ran |
retried | bool | You re-ran this step |
run_completed | bool | The run reached its terminal state |
human_verdict | accepted | edited | rejected | What a person did |
score | 0..1 | Your own eval score |
All optional. Report only what you know — inferring from silence would manufacture evidence.
Precedence: a hard failure (schema invalid, tool failed, retried, human
rejected) scores 0 and outranks everything, including a supplied score. Then
your score, then a human verdict, then a clean structural pass at 0.95.
Nothing conclusive means inconclusive, and the quality gate decides as before.
Annotations
What the SDK puts on a request, under the wire field optimizer.
{
"optimizer": {
"run": { "id": "run-42", "name": "nightly", "seq": 0,
"parent": "…", "is_start": true },
"step": { "id": "classify", "name": "classify", "kind": "classification",
"quality": { "min_score": 0.8 },
"loop_index": 1, "is_retry": false, "mode": "discover",
"requires": { "input_modalities": ["image"], "tools": "full",
"min_context_tokens": 200000 } },
"source": { "file": "src/triage.ts", "line": 88,
"fn": "classifyIntent", "release": "git:abc1234" },
"intent": { "latency_budget_ms": 800, "objective": "cost" },
"phase": "dev",
"recording": "auto"
}
} Every field is optional, and every level is omitted when empty — the server distinguishes “said nothing” from “said false”, so an explicit null would read as a declaration.
phase is the process’s Phase — dev, tuning or production — and is always sent, even outside any scope. recording is sent
in the Dev Phase only: auto, record or strict, as in Recordings.
step.mode is the per-step search intent: "discover" gives the Step a Discovery search even when the request’s model names one (the pin becomes the search’s anchor and cost ceiling); "off" keeps
the Step out of Discovery. Most specific wins: this annotation > the model field’s "discover".
step.quality.min_score raises this Step’s quality gate:
the minimum probability, 0–1, that a cheaper model’s answer is equivalent to the
Baseline’s or better. It can only raise your account’s bar (0.70 by default), never
lower it.
step.requires declares the hard capabilities this step needs from whatever
model serves it. It is a filter, not a preference: the router never probes a
model that can’t meet a requirement, so a tool-calling step never wastes a probe
on a tool-less model and a vision step never lands on a text-only one.
| Field | Type | Meaning |
|---|---|---|
tools | "none" \| "partial" \| "full" | Minimum tool-calling support |
structured_outputs | bool | Needs JSON / structured output mode |
reasoning | bool | Needs a model with reasoning-effort control |
min_context_tokens | int | Smallest context window the step can use |
input_modalities | string[] | text \| image \| audio \| video \| file the step sends |
output_modalities | string[] | What the step needs produced (text today) |
Most requirements are inferred from the request — tools present, JSON mode
set, prompt size, and input modality. The edge accepts OpenAI’s polymorphic content (a plain string, or an array of text / image_url / input_audio parts), so a turn that actually carries an image is routed to a vision-capable
model automatically — no hint required. Declaring requires.input_modalities: ["image"] is only needed ahead of the image: when a later turn of this step
will attach one, but the request you’re making now is still text-only. A declared
requirement wins over an inferred one, per field — you know the step’s future
turns; the server sees only this one.
Requirements are priors, never verdicts: they decide which models are eligible to probe. The quality judge still decides which eligible model actually holds up.
Response accounting
| Field | Meaning |
|---|---|
cache_hit | Served from cache |
baseline_cost | What it would have cost on your Baseline |
actual_cost | What was spent, including the search’s exploration |
served_cost | The model that answered, alone |
exploration_cost | Paid to grade candidates against the Baseline |
saving | baseline_cost − actual_cost |
Every response through Shimmy carries this block, under optimizer; replays and
passthrough calls don’t. exploration_cost falls to zero as a Step settles —
faster when you report outcomes.
Source annotation
file, line, fn, release. Source metadata — never source text, argument
values, or anything a user typed. Turn it off with sourceContext: false.
TypeScript and Python walk a stack trace and filter their own frames. Rust uses #[track_caller], which resolves the location at compile time with no unwinding
at all — different mechanisms, same three fields on the wire.
Environment
| Variable | Falls back to |
|---|---|
SHIMMY_API_KEY | OPTIMIZER_API_KEY |
SHIMMY_BASE_URL | OPTIMIZER_BASE_URL |
SHIMMY_RELEASE | OPTIMIZER_RELEASE, then GITHUB_SHA, GIT_COMMIT, VERCEL_GIT_COMMIT_SHA, … |