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 judge 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" }
}
} 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.
step.mode is the per-step routing intent: "discover" puts the step into discovery mode even when the request’s model names one (the pin becomes the search’s anchor and cost ceiling); "off" excludes it from discovery even when the agent has discovery enabled. Most
specific wins: this annotation > the model field’s sentinel
("discover") > the agent’s discovery setting.
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 pinned model |
actual_cost | What was spent, including exploration |
served_cost | The optimized path alone |
exploration_cost | Paid to learn whether a downshift was safe |
saving | baseline_cost − actual_cost |
Compare served_cost to baseline_cost for the honest gap. exploration_cost converges toward 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, … |