Repository navigation
v0.25.10
Additive: new API and docs, and two fixes.
- Extensions can record model calls they make into the run (
RunContext::spend_handle() -> Option<SpendHandle>;SpendHandle::add(&usage, &model)/add_priced(&usage, Option<&CostConfig>)). A host service that makes a model call for a plugin had no way to charge it to the run: aBudgetnever saw it, and neither did the run's stats. Recorded spend is priced by yoagent from the model'scost(never a dollar amount), counts againstBudgetat its next check, and lands in the newSessionStats::extensionsbucket (ExtensionSpend { usage, cost_usd, requests }), whichtotal_usage()andtotal_cost_usd()include — an unpriced call makes the totalNone, as an unpriced turn does — and which sub-agents' runs fold into.Agent::extension_spend()sums it across runs. The handle is cheap to clone and works from any task while the run is live; spend recorded after the run ended is not counted, with a warning. Asked for by yobuild (a meteredllmhost service). - Steering and follow-ups are no longer lost when a run is cancelled. The loop takes steering messages from their queue at the start, between tools and after each turn (and follow-ups when the agent would stop), and holds them until the next turn injects them. A cancel landing in between ended the run without injecting them, so they were in neither the history nor their queue: a user's steer sent as the run was being stopped vanished. They are now appended to the history (with their
MessageStart/MessageEndevents) before the cancel marker, so the next run sees them. Reported by yobuild, which had been re-sending what it steered. ToolExecutionStrategy::Batched { size: 0 }no longer answers a call twice when steered. Size 0 runs as size 1, but the count of calls already run used the raw 0, so a steer after the first batch gave every call — including the ones that ran — an extra "skipped" result, which providers reject as a duplicate tool result.- Model presets can be listed and looked up by provider and id:
ModelConfig::presets() -> impl Iterator<Item = ModelConfig>(all 22 named presets; order unspecified),ModelConfig::preset(provider, id) -> Option<ModelConfig>(exact, case-sensitive match onproviderandid, no aliases; builds and prices only the match) andModelConfig::generic(provider, id) -> Option<ModelConfig>(that provider's generic constructor withidas the name, never a preset, for the providers inPRICED_PROVIDERS; an OpenAI reasoning id gets the Responses API;Nonefor gateways, local servers and custom endpoints). Writepreset(p, id).or_else(|| generic(p, id)), and tell the user when the fallback ran: its limits are the provider's defaults, not the model's. A host that takes a model as text (xai/grok-4.7) had to keep its own list of presets, which drifts silently, and a generic constructor gives a known model wrong limits without a word:ModelConfig::xai("grok-4.7", ..)budgets for a 131K window and stops replies at 4,096 tokens wheregrok_4_7()has 500K and 64K, andModelConfig::openai("gpt-6-sol", ..)talks Chat Completions wheregpt_6_sol()uses the Responses API (found by yobuild). A unit test fails when a model priced inprices.jsonhas a preset not listed (models priced without one — DeepSeek's, Meta's — are named in the test), andtests/preset_prices_test.rswhen the list and the pinned presets differ. - Docs: each generic constructor says its limits and names the presets for its provider.
openai,openai_responses,google,xai,mistral,zaiandqwenstate their default context window and output limit and point to their presets (andModelConfig::preset);groq,minimax,deepseekandmeta, which have none, state their limits.xai's doc no longer listsgrok-4.7as a model for the generic constructor.