Skip to content

v0.25.10

Choose a tag to compare

@yuanhao yuanhao released this 19 Oct 21:44
· 25 commits to main since this release
655f773

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: a Budget never saw it, and neither did the run's stats. Recorded spend is priced by yoagent from the model's cost (never a dollar amount), counts against Budget at its next check, and lands in the new SessionStats::extensions bucket (ExtensionSpend { usage, cost_usd, requests }), which total_usage() and total_cost_usd() include — an unpriced call makes the total None, 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 metered llm host 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 / MessageEnd events) 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 on provider and id, no aliases; builds and prices only the match) and ModelConfig::generic(provider, id) -> Option<ModelConfig> (that provider's generic constructor with id as the name, never a preset, for the providers in PRICED_PROVIDERS; an OpenAI reasoning id gets the Responses API; None for gateways, local servers and custom endpoints). Write preset(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 where grok_4_7() has 500K and 64K, and ModelConfig::openai("gpt-6-sol", ..) talks Chat Completions where gpt_6_sol() uses the Responses API (found by yobuild). A unit test fails when a model priced in prices.json has a preset not listed (models priced without one — DeepSeek's, Meta's — are named in the test), and tests/preset_prices_test.rs when 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, zai and qwen state their default context window and output limit and point to their presets (and ModelConfig::preset); groq, minimax, deepseek and meta, which have none, state their limits. xai's doc no longer lists grok-4.7 as a model for the generic constructor.