Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
3d8605f
Document model pricing and the agent's per-session spend caps
DavidCockerill Oct 6, 2026
7df48c5
Rename the session token cap to maxSessionTokens, and say which calls…
DavidCockerill Oct 6, 2026
d435808
Say how to leave a session whose cap cannot be measured, and that a r…
DavidCockerill Oct 6, 2026
30cb0c7
Date model pricing and the agent spend caps to 5.4.0, and add them to…
DavidCockerill Oct 6, 2026
22705b6
Say an unmeasured call keeps the session total unmeasured, not that i…
DavidCockerill Oct 6, 2026
093e0c9
Keep the http_fetch section badge at v5.3.2, and make the 5.4 notes e…
DavidCockerill Oct 6, 2026
5f602d6
Say that cancel_agent_run answers every pending approval
DavidCockerill Oct 6, 2026
0122412
Contrast the two cost caps, say clearing a cap by set_agent_config la…
DavidCockerill Oct 6, 2026
089f328
Say that an approved action already running when a cancel arrives fin…
DavidCockerill Oct 6, 2026
b1ff812
Merge remote-tracking branch 'origin/main' into david/3040-agent-usag…
DavidCockerill Oct 12, 2026
4a72a5e
Say maxTurns is validated, and that a cancelled session stays aborted
DavidCockerill Oct 12, 2026
ee7415c
Add agent cancellation and maxTurns validation to the 5.4 release notes
DavidCockerill Oct 12, 2026
e65e021
Say a cancelled run makes no further model call, and that maxTurns is…
DavidCockerill Oct 12, 2026
c00c685
Say agent_prompt answers 409 while a cancelled run finishes, and that…
DavidCockerill Oct 12, 2026
ca925cf
Say how long the 409 after a cancel can last
DavidCockerill Oct 12, 2026
5c38284
Say a prompt that races a cancel is recorded but not run
DavidCockerill Oct 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 23 additions & 3 deletions reference/configuration/options.md
Original file line number Diff line number Diff line change
Expand Up @@ -404,16 +404,36 @@ agent:
- `enabled` — Enable the agent component; _Default_: `false`
- `provider` — Recorded on the session but **not yet used to route the model call** — only `model` reaches the provider. Set the provider through the [`models`](../models/overview.md#configuration) configuration instead
- `model` — Model id override, passed through to the model call; _Default_: the [`models`](../models/overview.md#configuration) generative default
- `maxTurns` — Maximum tool-call iterations in a single run; _Default_: `50`
- `maxCostUsd` — Intended per-run cost ceiling. **Not enforced** — it is a stored setting only, and nothing checks spend against it; _Default_: `5.00`
- `maxTurns` — Maximum tool-call iterations in a single run; a positive integer, validated from v5.4.0. A bare `maxTurns:` keeps the default; _Default_: `50`
- `maxSessionTokens` <VersionBadge version="v5.4.0" /> — Per-session cap on prompt plus completion tokens, summed over every model call in the session that returns; a positive integer. See [Spend caps](#agent-spend-caps); _Default_: none
- `maxCostUsd` <VersionBadge type="changed" version="v5.4.0" /> — Per-session cap in USD, priced from the [`pricing`](../models/overview.md#pricing) on the `models.generative` entry that served each call; a number of `0` or more, and `0` stops every session before its first call. See [Spend caps](#agent-spend-caps). Before v5.4.0 this setting was stored but never enforced, and defaulted to `5.00`; _Default_: none
- `autoApprove` — Run without per-action approval gates; _Default_: `false`
- `allowDestructive` — Include the tools marked destructive in the agent's toolset: `write_file`, the inspector's code-evaluation tools, and any operations tool carrying MCP's [`destructiveHint`](../mcp/tool-metadata.md) (`drop_table`, `delete`, `restart`, `set_configuration`, ...). When `false` they are removed entirely rather than gated. That hint comes from a curated set in core which does not cover every damaging operation, so this is not a complete safety boundary on its own — see [Agent operations](../operations-api/operations.md#agent); _Default_: `false`
- `user` — Harper user the agent's **operations** tools run as; the filesystem, HTTP, schedule, and inspector tools always run at process privilege regardless. If it cannot be resolved and it is not the default, the agent fails closed and runs with no operations tools; _Default_: `hdb_agent`, which falls back to a `super_user` bootstrap identity
- `componentsScope` — Filesystem write scope for component edits, relative to `rootPath`; _Default_: the full `componentsRoot`
- `httpFetch` <VersionBadge version="v5.3.2" /> — Whether the agent has its `http_fetch` tool, and which hosts it may reach: `true`, `false`, or `{ allow: [...] }`. Read at startup only. See [Restricting `http_fetch`](#restricting-http_fetch); _Default_: `true`
- `systemPromptAppend` — Operator text appended to the agent's system prompt

`enabled`, `provider`, `model`, `maxTurns`, `maxCostUsd`, `autoApprove`, `allowDestructive`, and `systemPromptAppend` can also be changed at runtime with [`set_agent_config`](../operations-api/operations.md#set_agent_config), which applies in memory only. `enabled` is the exception worth knowing: it cannot switch the agent on, because with the agent disabled at startup no agent operation is registered at all.
`enabled`, `provider`, `model`, `maxTurns`, `maxSessionTokens`, `maxCostUsd`, `autoApprove`, `allowDestructive`, and `systemPromptAppend` can also be changed at runtime with [`set_agent_config`](../operations-api/operations.md#set_agent_config), which applies in memory only. `enabled` is the exception worth knowing: it cannot switch the agent on, because with the agent disabled at startup no agent operation is registered at all.

### Agent spend caps

<VersionBadge version="v5.4.0" />

`maxSessionTokens` and `maxCostUsd` bound a whole session, across every run in it. Approving a paused action or sending a follow-up prompt starts a new run, but does not reset a cap the way it resets `maxTurns`. Each assistant message records the usage of the model call that produced it, and [`get_agent_session`](../operations-api/operations.md#get_agent_session) reports the session's totals as `usage`.

A cap is checked when a run starts, before it executes an approved action, and after every model call:

- **At or over a cap**, the session makes no further model call and ends `error`, with `lastError` naming the cap, for example `agent.maxSessionTokens=200000 reached: this session used 201450 tokens`. Tool calls the model had just requested are answered with a `budget_exceeded` result rather than run, so the transcript stays valid if you continue the session. A final answer that crosses the cap is still delivered: the run ends `completed`, with `lastError` naming the cap.
- **A cap that cannot be measured stops the session too.** `maxCostUsd` needs every call priced, and `maxSessionTokens` needs every call's token counts. A model with no `pricing`, or a provider that reports no usage, stops the session after its first call rather than letting it run uncapped: it ends `error`, or `completed` if that call was the final answer, with `lastError` saying why either way. The unmeasured call stays in the transcript, so the session's total stays unmeasured and later runs stop before making any call, and raising the cap or adding `pricing` does not clear it: start a new session, or clear the cap. `set_agent_config` with `null` clears it until the next restart; to clear it for good, remove the key from the configuration file. A session created before v5.4.0, whose earlier calls recorded no usage, is in the same state.
- **Pending approvals are closed too.** A run stopped before it executes an approved action answers that action, and any approval still awaiting a decision, with `budget_exceeded`, so nothing is left pending. The action does not run later, even once the cap is raised; the model sees the refusal and can ask again.
- **A capped session stays capped.** Raise the cap with `set_agent_config` or start a new session.

A cap is a threshold checked after each call, not a guarantee: the call that reaches it can overshoot it. Only calls that return are counted. A call that fails, for example on a timeout or a provider error and whether or not another model then serves it, is not, even if the provider billed it.

A cap, or a `maxTurns`, that is not a usable number fails the agent's start: Harper logs the error and registers no agent operation. Through `set_agent_config` the same value is rejected with a 400.

If you upgrade with `maxCostUsd` already set in your configuration, it starts being enforced, and needs `pricing` on the generative entry the agent uses.

### Restricting `http_fetch`

Expand Down
17 changes: 9 additions & 8 deletions reference/models/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,21 +68,22 @@ Additional options apply only when `toolMode: 'auto'`; they are documented in [T

### GenerateResult

| Field | Type | Description |
| -------------- | -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `content` | `string` | The generated text |
| `finishReason` | `'stop'` \| `'length'` \| `'tool_calls'` \| `'content_filter'` | Why generation stopped, normalized across backends |
| `toolCalls` | `ToolCall[]` | Tool calls the model requested, when `finishReason` is `'tool_calls'` (each `{ id, name, arguments }`, with `arguments` parsed to an object) |
| `usage` | `TokenUsage` | Token usage reported by the backend (`promptTokens`, `completionTokens`, …), when available |
| `trace` | `ToolTraceEntry[]` | Per-tool-invocation trace; only populated by the `toolMode: 'auto'` loop — see [Tool Calling](./tool-calling) |
| Field | Type | Description |
| -------------- | -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `content` | `string` | The generated text |
| `finishReason` | `'stop'` \| `'length'` \| `'tool_calls'` \| `'content_filter'` | Why generation stopped, normalized across backends |
| `toolCalls` | `ToolCall[]` | Tool calls the model requested, when `finishReason` is `'tool_calls'` (each `{ id, name, arguments }`, with `arguments` parsed to an object) |
| `usage` | `TokenUsage` | Token usage reported by the backend (`promptTokens`, `completionTokens`, …), when available |
| `costUsd` | `number` | From v5.4.0, the call's cost from the serving entry's [pricing](./overview#pricing); absent when that entry is unpriced or usage is incomplete. With `toolMode: 'auto'`, the final round's only, like `usage` |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

When documenting behavior changes to an existing API surface (such as adding the new costUsd field to the GenerateResult response), we should use the <VersionBadge type="changed" version="v5.4.0" /> component to clearly highlight the change.

Suggested change
| `costUsd` | `number` | From v5.4.0, the call's cost from the serving entry's [pricing](./overview#pricing); absent when that entry is unpriced or usage is incomplete. With `toolMode: 'auto'`, the final round's only, like `usage` |
| `costUsd` | `number` | <VersionBadge type="changed" version="v5.4.0" /> The call's cost from the serving entry's [pricing](./overview#pricing); absent when that entry is unpriced or usage is incomplete. With `toolMode: 'auto'`, the final round's only, like `usage` |
References
  1. When documenting behavior changes to an existing API surface (e.g., adding new fields to a response), use .

| `trace` | `ToolTraceEntry[]` | Per-tool-invocation trace; only populated by the `toolMode: 'auto'` loop — see [Tool Calling](./tool-calling) |

## generateStream()

```typescript
models.generateStream(input: GenerateInput, options?: GenerateOpts): AsyncIterable<GenerateChunk>
```

Identical to `generate()` but yields the completion incrementally:
Identical to `generate()` but yields the completion incrementally. Streamed calls are not [priced](./overview#pricing), so no chunk carries `costUsd`:

```javascript
let text = '';
Expand Down
1 change: 1 addition & 0 deletions reference/models/backends.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ All backends support these common fields:
| `model` | Provider-side model identifier (e.g. `gpt-4o`) used when a call does not pass its own `model` option |
| `requestTimeoutMs` | Per-request timeout in milliseconds; composed with any caller-provided `AbortSignal` |
| `revision` | From v5.3.1, a non-secret label for the deployment behind the entry, for [calibration](./calibration#when-the-deployment-changes-but-the-configuration-does-not); change it when the same settings reach a different model |
| `pricing` | From v5.4.0, `generative` entries only: `{ inputPerMtok, outputPerMtok }`, what you pay in USD per million prompt and completion tokens. See [Pricing](./overview#pricing) |

## Ollama

Expand Down
24 changes: 24 additions & 0 deletions reference/models/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,30 @@ The logical name `default` is used when application code does not pass an explic

See [Backends](./backends) for the full set of configuration fields supported by each backend.

### Pricing

<VersionBadge version="v5.4.0" />

A `generative` entry can declare what you pay for its model, so Harper can put a dollar figure on each call. Harper ships no prices of its own: the rates are yours to keep current.

```yaml
models:
generative:
default:
backend: openai
apiKey: ${OPENAI_API_KEY}
model: gpt-4o
pricing:
inputPerMtok: 2.5 # USD per million prompt tokens
outputPerMtok: 10 # USD per million completion tokens
```

Each `generate()` call is priced at the rates of the entry that served it, and the result carries `costUsd`. With `toolMode: 'auto'`, the result's `costUsd` covers the final round only, like its `usage`. A call that fell back to another entry in a [fallback group](./routing#fallback-groups) is priced at that entry's rates, not the requested one's. A call has no `costUsd` when the entry that served it declares no `pricing`, or when the provider did not report both a prompt and a completion token count.

The two rates are the only cost inputs Harper uses. Discounts for cached prompt tokens, long-context surcharges and provisioned capacity are not, so set rates that match what an average call costs you, or rates that overestimate it if the figure backs a cap. Streaming calls (`generateStream()`) are not priced.

The price is read by [`maxCostUsd`](./tool-calling#options) on `toolMode: 'auto'` calls and by the [built-in agent's `maxCostUsd`](../configuration/options#agent-spend-caps). `pricing` is accepted only on `generative` entries; on an `embedding` or `decision` entry it fails configuration validation. A price change applies on config reload, except on an entry whose `backend` is a module rather than a built-in, which takes it on restart like any other change to that entry. It does not affect [calibration](./calibration).

### Credentials

String values in model entries support environment-variable indirection with `${VAR_NAME}` syntax, resolved at startup. Use this for API keys rather than placing the literal key in the configuration file — Harper logs a warning at startup when a credential field contains a literal value. If the referenced environment variable is unset, the placeholder is left as-is; for credential fields the backend rejects the unresolved placeholder at startup, while other fields (such as `host` or `model`) carry the literal placeholder into requests — surfacing as per-request failures rather than a startup error. Indirection applies to string-typed fields only; numeric fields such as `requestTimeoutMs` must be literal values.
Expand Down
Loading
Loading