Models and environments¶
Your code asks for a model by alias. The spec says which concrete model each alias means, and an environment overlay can change that without touching code. This chapter adds a second alias, prices and a production overlay.
Aliases¶
apiVersion: harnessweaver/v1
kind: Harness
metadata:
name: support-desk
owner: support-team
models:
provider: openai.chat
base_url: http://localhost:4000
api_key_env: LITELLM_API_KEY
default: fast
aliases: { fast: fast-chat, strong: strong-chat }
prices:
fast-chat: { input_per_mtok: 0.25, output_per_mtok: 2.00 }
strong-chat: { input_per_mtok: 3.00, output_per_mtok: 15.00 }
tasks:
triage:
runner: direct
model: fast
output: str
aliasesmaps each alias to a model the provider serves. Behind a gateway, that is a model name the gateway knows.defaultis the alias used when nothing else names one.- A task's
modelpicks the alias for that task. Heretriageusesfast. A heavier task could usestrong.
Code, prompts and tests only ever say fast or strong. Changing which model serves fast is a one-line spec change, reviewed like any other.
Tip
An org baseline usually sets models.allowed_aliases: [fast, strong] and locks it. A run that asks for another alias is then blocked with model_not_allowed. Profiles and the org baseline does this.
Cost¶
prices gives each model's price in US dollars per million tokens. Print what the run used:
import asyncio
import harnessweaver as hw
async def main() -> None:
harness = hw.load("harness.yaml")
result = await harness.run("triage", "I was charged twice for my Pro plan. Refund one?")
print(result.status.value)
print(result.output)
print(f"tokens: {result.usage.input_tokens} in, {result.usage.output_tokens} out")
print(f"cost: ${result.cost_usd:.6f}")
if __name__ == "__main__":
asyncio.run(main())
$ HARNESSWEAVER_ENV=offline uv run python main.py
completed
billing, priority 2: charged twice for the Pro plan, wants one refund.
tokens: 12 in, 18 out
cost: $0.000039
The scripted model estimates tokens from the text (one per four characters). A real provider reports them. The cost of a call is found in this order:
- The cost the provider reports, such as LiteLLM's
x-litellm-response-costheader, whichopenai.chatreads. - The spec's
models.prices, by the response's model, then the request's model, then the alias. - Otherwise the cost is unknown. The trace flags it (
cost_source: unknown),result.cost_knownisFalse, and nothing is guessed.
There is no built-in price table, because hard-coded prices go stale and mislead.
Warning
A max_usd budget can't count what it can't price. If you rely on one, run behind a gateway that reports cost, or give prices.
Environments¶
An overlay is a partial spec named harness.<env>.yaml, next to harness.yaml. HARNESSWEAVER_ENV=<env> (or hw.load(path, env=...)) merges it over the spec. You already have offline. Add one for production:
models:
base_url: https://llm-gateway.internal.example
aliases: { fast: fast-chat-prod }
With HARNESSWEAVER_ENV=prod, the app talks to the production gateway and fast means fast-chat-prod. Everything else in harness.yaml stays the same.
| File | Used when | Holds |
|---|---|---|
harness.yaml |
Always | What every environment shares |
harness.offline.yaml |
HARNESSWEAVER_ENV=offline |
A scripted model, for tests and demos |
harness.prod.yaml |
HARNESSWEAVER_ENV=prod |
The production gateway, model names, telemetry |
An overlay holds only sections: no metadata, no extends. A missing overlay file is not an error. Configuration lists every environment variable.
What happens on every model call¶
Every model call goes through the same steps: one made by a runner, by your own code in a session, by an instrumented client, or by an agent framework's adapter.
- The alias is resolved (the request's, else the task's
model, elsemodels.default) and checked againstmodels.allowed_aliases. - The budget is checked before the call: call count, tokens and cost so far, and whether the input fits (Budgets and deadlines).
model.beforemiddleware runs, and may change the request. A redaction made here is what the provider receives.- The provider is called. When streaming, each chunk fires
stream.chunk. - Usage and cost are recorded (the
harness.budgetnote you saw in the trace), andmodel.aftermiddleware runs. A request the model declines is recorded too, then ends the runblockedwith the codemodel_refusal, beforemodel.after. - Going over
max_tokensormax_usdafter the call blocks the run.
A provider that fails raises a ProviderError, code provider_error, which is retryable. Retries belong to the gateway, so openai.chat makes none of its own (RULES R5).
Providers¶
| Provider | From | For |
|---|---|---|
scripted |
The core | Tests and offline runs: answers from rules and a list of replies |
openai.chat |
harnessweaver-openai |
Any OpenAI-compatible endpoint: LiteLLM Proxy, OpenAI, vLLM, Ollama |
anthropic.messages |
harnessweaver-anthropic |
Claude, through the Messages API, with its thinking, effort and refusal fallbacks |
Each provider's own settings sit next to provider in the models section. Providers lists them all, and Choosing a model provider says what Claude loses through openai.chat.
Recap¶
- Code asks for aliases. The spec maps them to models, and overlays remap them per environment.
- Cost comes from the provider, else from
models.prices, else it is marked unknown. It is never guessed. harness.<env>.yamloverlays change a spec per environment, chosen byHARNESSWEAVER_ENV.- Every model call, however it is made, passes the same governed steps.