diff --git a/.github/template-version.txt b/.github/template-version.txt index fb798e28..9daf9c50 100644 --- a/.github/template-version.txt +++ b/.github/template-version.txt @@ -1 +1 @@ -reloaded-templates-rust:1.0.1 +reloaded-templates-rust:1.1.1 diff --git a/.github/workflows/models-dev-update.yml b/.github/workflows/models-dev-update.yml new file mode 100644 index 00000000..49557ed5 --- /dev/null +++ b/.github/workflows/models-dev-update.yml @@ -0,0 +1,31 @@ +name: Update models.dev catalog + +on: + schedule: + - cron: "0 9 * * 1" + workflow_dispatch: + +jobs: + update-catalog: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + steps: + - uses: actions/checkout@v4 + - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: stable + cache: true + - name: Run models.dev updater + working-directory: src + run: cargo run -p llm-coding-tools-models-dev --bin models-dev-update + - name: Create pull request + uses: peter-evans/create-pull-request@v6 + with: + title: "chore: update models.dev catalog" + body: "Automated update of models.dev catalog snapshot." + commit-message: "chore: update models.dev catalog" + branch: "automation/models-dev-catalog" + add-paths: | + src/llm-coding-tools-models-dev/data/models.dev.min.json diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..8e6bbf86 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1 @@ +@src/AGENTS.md \ No newline at end of file diff --git a/README.MD b/README.MD index 2d728aac..fbee1663 100644 --- a/README.MD +++ b/README.MD @@ -2,10 +2,9 @@ [![Crates.io - llm-coding-tools-core](https://img.shields.io/crates/v/llm-coding-tools-core.svg)](https://crates.io/crates/llm-coding-tools-core) [![Crates.io - llm-coding-tools-serdesai](https://img.shields.io/crates/v/llm-coding-tools-serdesai.svg)](https://crates.io/crates/llm-coding-tools-serdesai) -[![Docs.rs](https://docs.rs/llm-coding-tools-serdesai/badge.svg)](https://docs.rs/llm-coding-tools-serdesai) -[![CI](https://github.com/Sewer56/llm-coding-tools/actions/workflows/rust.yml/badge.svg)](https://github.com/Sewer56/llm-coding-tools/actions) +[![CI](https://github.com/Sewer56/llm-coding-tools/actions/workflows/rust.yml/badge.svg)](https://github.com/Sewer56/llm-coding-tools/actions/workflows/rust.yml) -Lightweight, high-performance coding tool implementations for LLM-powered development agents. Plug and play into your favourite frameworks. +Lightweight, high-performance coding tool implementations for LLM-powered development agents. ## About This Workspace @@ -13,6 +12,7 @@ This workspace contains multiple Rust crates for integrating coding tools with L - **[llm-coding-tools-core](./src/llm-coding-tools-core/)**: Framework-agnostic core operations and utilities - **[llm-coding-tools-serdesai](./src/llm-coding-tools-serdesai/)**: serdesAI framework-specific Tool implementations +- **[llm-coding-tools-agents](./src/llm-coding-tools-agents/)**: Agent configuration loading for registry-driven Task tools ## Features @@ -30,50 +30,26 @@ This workspace contains multiple Rust crates for integrating coding tools with L ## Quick Start -Add to your `Cargo.toml`: - -```toml -[dependencies] -llm-coding-tools-serdesai = "0.1" -``` - -```rust,no_run -use llm_coding_tools_serdesai::{AgentBuilder, BashTool, TodoTools}; -use llm_coding_tools_serdesai::absolute::{ReadTool, WriteTool, EditTool, GlobTool, GrepTool}; - -let mut builder = AgentBuilder::new(); -let todos = TodoTools::new(); - -builder - .track(ReadTool::::new()) - .track(WriteTool::new()) - .track(EditTool::::new()) - .track(GlobTool::new()) - .track(GrepTool::::new()) - .track(BashTool::new()) - .track(&todos.read) - .track(&todos.write); - -let mut agent = builder.build(); - -// Use the agent -// let response = agent.invoke("List all files").await?; -``` +See [llm-coding-tools-serdesai](./src/llm-coding-tools-serdesai/README.md) for serdesAI framework support. ## Examples ```bash # serdesAI framework - Basic agent setup -cargo run --example serdesai-agents -p llm-coding-tools-serdesai +cargo run --example serdesai-basic -p llm-coding-tools-serdesai # serdesAI framework - Sandboxed file access cargo run --example serdesai-sandboxed -p llm-coding-tools-serdesai + +# serdesAI framework - Registry-driven agent invocation +cargo run --example serdesai-agents -p llm-coding-tools-serdesai ``` ## Documentation - [llm-coding-tools-core README](./src/llm-coding-tools-core/README.md) - [llm-coding-tools-serdesai README](./src/llm-coding-tools-serdesai/README.md) +- [llm-coding-tools-agents README](./src/llm-coding-tools-agents/README.md) - [Developer Guidelines](./src/AGENTS.md) ## Contributing diff --git a/src/AGENTS.md b/src/AGENTS.md index 9dc22ede..ba050262 100644 --- a/src/AGENTS.md +++ b/src/AGENTS.md @@ -1,4 +1,6 @@ -Basic coding oriented tools for LLM agents +Basic coding oriented tools for LLM agents. + +This is a headless library, there is no TUI interaction model here, so interactive `ask` approval flows and autocomplete-style agent UX are out of scope. # Feature Flags (llm-coding-tools-core) @@ -16,15 +18,14 @@ The `async` and `blocking` features are mutually exclusive - enabling both cause - `src/error.rs` - Unified error types - `src/output.rs` - Tool output formatting - `src/util.rs` - Shared utilities +- `llm-coding-tools-agents/` - Agent config loading and permission model +- `llm-coding-tools-models-dev/` - models.dev catalog integration and snapshot tooling - `llm-coding-tools-serdesai/` - serdesAI framework Tool implementations - - `src/absolute/` - Unrestricted file system tools - - `src/allowed/` - Sandboxed file system tools - - `src/schema.rs` - Schema building utilities - - `src/convert.rs` - Type conversions between core and serdesAI # Code & Performance Guidelines -This is a high-performance library. Optimize aggressively. +This is a high-performance library. Optimize aggressively. Use arrays instead of maps if size is known ahead of time. +Optimize for memory. Preallocate or trim if possible. Minimize memory use. Use smaller integers/types where appropriate. Use any other tricks that improve CPU or memory efficiency. ## Memory & Allocation @@ -66,6 +67,6 @@ This is a high-performance library. Optimize aggressively. - Focus comments on "why" not "what" - Use [`TypeName`] rustdoc links, not backticks. -# Post-Change Verification +# Verification -After you make a change to source code, always run `.cargo/verify.sh` (`.cargo/verify.ps1` on Windows) before returning to the user. +After code changes or for checks (testing/linting/building/docs/formatting), run `.cargo/verify.sh` (`.cargo/verify.ps1` on Windows). It echoes each command and runs the full suite, including core tests and any extra checks. Do this before returning to the user. diff --git a/src/Cargo.lock b/src/Cargo.lock index a389a306..ea9218ee 100644 --- a/src/Cargo.lock +++ b/src/Cargo.lock @@ -1292,19 +1292,37 @@ dependencies = [ "wiremock", ] +[[package]] +name = "llm-coding-tools-models-dev" +version = "0.1.0" +dependencies = [ + "reqwest 0.13.1", + "serde", + "serde_json", + "tempfile", + "thiserror 2.0.18", + "tokio", + "wiremock", + "zstd", +] + [[package]] name = "llm-coding-tools-serdesai" version = "0.2.0" dependencies = [ + "ahash", "async-trait", "futures", + "indexmap", + "indoc", + "llm-coding-tools-agents", "llm-coding-tools-core", + "llm-coding-tools-models-dev", "reqwest 0.13.1", "serde", "serde_json", "serdes-ai", "serdes-ai-models", - "serdes-ai-streaming", "tempfile", "tokio", "wiremock", @@ -1557,6 +1575,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + [[package]] name = "plotters" version = "0.3.7" @@ -3579,3 +3603,31 @@ name = "zmij" version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfcd145825aace48cff44a8844de64bf75feec3080e0aa5cdbde72961ae51a65" + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/src/Cargo.toml b/src/Cargo.toml index 0dbd669e..7429dbb9 100644 --- a/src/Cargo.toml +++ b/src/Cargo.toml @@ -1,7 +1,7 @@ [workspace] resolver = "2" -members = ["llm-coding-tools-core", "llm-coding-tools-serdesai", "llm-coding-tools-agents"] +members = ["llm-coding-tools-core", "llm-coding-tools-serdesai", "llm-coding-tools-agents", "llm-coding-tools-models-dev"] # Profile Build [profile.profile] diff --git a/src/llm-coding-tools-models-dev/Cargo.toml b/src/llm-coding-tools-models-dev/Cargo.toml new file mode 100644 index 00000000..8425903f --- /dev/null +++ b/src/llm-coding-tools-models-dev/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "llm-coding-tools-models-dev" +version = "0.1.0" +edition = "2021" +description = "Bundled models.dev catalog snapshot and lookup API" +repository = "https://github.com/Sewer56/llm-coding-tools" +license = "Apache-2.0" +readme = "README.md" +include = ["src/**/*", "data/**/*", "build.rs", "README.md"] + +[dependencies] +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +thiserror = "2.0" +reqwest = { version = "0.13", default-features = false, features = [ + "rustls", + "rustls-native-certs", +] } +tokio = { version = "1.48", features = ["fs", "io-util", "rt", "macros", "sync"] } +zstd = "0.13" + +[build-dependencies] +zstd = "0.13" + +[dev-dependencies] +tokio = { version = "1.48", features = ["rt", "macros", "sync"] } +tempfile = "3.24" +wiremock = "0.6" + +[[bin]] +name = "models-dev-update" +path = "src/bin/models-dev-update.rs" diff --git a/src/llm-coding-tools-models-dev/README.md b/src/llm-coding-tools-models-dev/README.md new file mode 100644 index 00000000..9b9a34c6 --- /dev/null +++ b/src/llm-coding-tools-models-dev/README.md @@ -0,0 +1,61 @@ +# llm-coding-tools-models-dev + +Bundled models.dev catalog snapshot and lookup API. + +This crate provides a standalone catalog for models.dev provider and model data, with embedded snapshot support and filtering capabilities. + +## Features + +- Bundled zstd-compressed snapshot (level 22) +- Load from bundled, cached, or downloaded sources +- Model → provider index with optional filtering +- Provider metadata lookup +- Deterministic JSON output for vendored snapshots + +## Usage + +```rust +# fn main() -> Result<(), Box> { +use llm_coding_tools_models_dev::{CatalogSource, ModelLimits, ModelsDevCatalog}; +use std::collections::HashSet; + +// Load bundled snapshot +let (catalog, source) = ModelsDevCatalog::from_bundled()?; +assert!(matches!(source, CatalogSource::Bundled)); + +// Resolve providers for a model +let providers = catalog.resolve_provider_for_model("gpt-4o"); +if let Some(provider_ids) = providers { + for provider_id in provider_ids { + if let Some(metadata) = catalog.get_provider(provider_id) { + println!("Provider: {} - env: {:?}", metadata.id, metadata.env); + } + } +} + +// Optional model-limit lookups +if let Some(ModelLimits { context, output }) = catalog.get_model_limits("gpt-4o") { + println!("gpt-4o context={} output={:?}", context, output); +} + +// Load with model filtering +let mut filter = HashSet::new(); +filter.insert("gpt-4o".to_string()); +let (catalog, _) = ModelsDevCatalog::from_bundled_filtered(&filter)?; +# Ok(()) +# } +``` + +## Update Binary + +Regenerate the vendored snapshot from models.dev: + +```bash +cargo run -p llm-coding-tools-models-dev --bin models-dev-update +``` + +This fetches the latest data from and writes a minimal snapshot to `data/models.dev.min.json`. + +## License + +Apache-2.0 diff --git a/src/llm-coding-tools-models-dev/build.rs b/src/llm-coding-tools-models-dev/build.rs new file mode 100644 index 00000000..f762f6f0 --- /dev/null +++ b/src/llm-coding-tools-models-dev/build.rs @@ -0,0 +1,15 @@ +use std::{env, fs, path::PathBuf}; +use zstd::bulk::compress; + +fn main() { + let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR")); + let input = manifest_dir.join("data/models.dev.min.json"); + let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR")); + let output = out_dir.join("models.dev.min.json.zst"); + + let json = fs::read(&input).expect("read models.dev.min.json"); + let compressed = compress(&json, 22).expect("zstd compress"); + fs::write(&output, compressed).expect("write models.dev.min.json.zst"); + + println!("cargo:rerun-if-changed={}", input.display()); +} diff --git a/src/llm-coding-tools-models-dev/data/models.dev.min.json b/src/llm-coding-tools-models-dev/data/models.dev.min.json new file mode 100644 index 00000000..f0120ee7 --- /dev/null +++ b/src/llm-coding-tools-models-dev/data/models.dev.min.json @@ -0,0 +1 @@ +{"providers":{"302ai":{"id":"302ai","npm":"@ai-sdk/openai-compatible","api":"https://api.302.ai/v1","env":["302AI_API_KEY"],"models":{"MiniMax-M1":{"limit":{"context":1000000,"output":128000}},"MiniMax-M2":{"limit":{"context":1000000,"output":128000}},"MiniMax-M2.1":{"limit":{"context":1000000,"output":131072}},"chatgpt-4o-latest":{"limit":{"context":128000,"output":16384}},"claude-haiku-4-5-20251001":{"limit":{"context":200000,"output":64000}},"claude-opus-4-1-20250805":{"limit":{"context":200000,"output":32000}},"claude-opus-4-1-20250805-thinking":{"limit":{"context":200000,"output":32000}},"claude-opus-4-5-20251101":{"limit":{"context":200000,"output":64000}},"claude-opus-4-5-20251101-thinking":{"limit":{"context":200000,"output":64000}},"claude-sonnet-4-5-20250929":{"limit":{"context":200000,"output":64000}},"claude-sonnet-4-5-20250929-thinking":{"limit":{"context":200000,"output":64000}},"deepseek-chat":{"limit":{"context":128000,"output":8192}},"deepseek-reasoner":{"limit":{"context":128000,"output":128000}},"deepseek-v3.2":{"limit":{"context":128000,"output":8192}},"deepseek-v3.2-thinking":{"limit":{"context":128000,"output":128000}},"doubao-seed-1-6-thinking-250715":{"limit":{"context":256000,"output":16000}},"doubao-seed-1-6-vision-250815":{"limit":{"context":256000,"output":32000}},"doubao-seed-1-8-251215":{"limit":{"context":224000,"output":64000}},"doubao-seed-code-preview-251028":{"limit":{"context":256000,"output":32000}},"gemini-2.0-flash-lite":{"limit":{"context":2000000,"output":8192}},"gemini-2.5-flash":{"limit":{"context":1000000,"output":65536}},"gemini-2.5-flash-image":{"limit":{"context":32768,"output":32768}},"gemini-2.5-flash-lite-preview-09-2025":{"limit":{"context":1000000,"output":65536}},"gemini-2.5-flash-nothink":{"limit":{"context":1000000,"output":65536}},"gemini-2.5-flash-preview-09-2025":{"limit":{"context":1000000,"output":65536}},"gemini-2.5-pro":{"limit":{"context":1000000,"output":65536}},"gemini-3-flash-preview":{"limit":{"context":1000000,"output":65536}},"gemini-3-pro-image-preview":{"limit":{"context":32768,"output":64000}},"gemini-3-pro-preview":{"limit":{"context":1000000,"output":64000}},"glm-4.5":{"limit":{"context":128000,"output":98304}},"glm-4.5v":{"limit":{"context":64000,"output":16384}},"glm-4.6":{"limit":{"context":200000,"output":131072}},"glm-4.6v":{"limit":{"context":128000,"output":32768}},"glm-4.7":{"limit":{"context":200000,"output":131072}},"gpt-4.1":{"limit":{"context":1000000,"output":32768}},"gpt-4.1-mini":{"limit":{"context":1000000,"output":32768}},"gpt-4.1-nano":{"limit":{"context":1000000,"output":32768}},"gpt-4o":{"limit":{"context":128000,"output":16384}},"gpt-5":{"limit":{"context":400000,"output":128000}},"gpt-5-mini":{"limit":{"context":400000,"output":128000}},"gpt-5-pro":{"limit":{"context":400000,"output":272000}},"gpt-5-thinking":{"limit":{"context":400000,"output":128000}},"gpt-5.1":{"limit":{"context":400000,"output":128000}},"gpt-5.1-chat-latest":{"limit":{"context":128000,"output":16384}},"gpt-5.2":{"limit":{"context":400000,"output":128000}},"gpt-5.2-chat-latest":{"limit":{"context":128000,"output":16384}},"grok-4-1-fast-non-reasoning":{"limit":{"context":2000000,"output":30000}},"grok-4-1-fast-reasoning":{"limit":{"context":2000000,"output":30000}},"grok-4-fast-non-reasoning":{"limit":{"context":2000000,"output":30000}},"grok-4-fast-reasoning":{"limit":{"context":2000000,"output":30000}},"grok-4.1":{"limit":{"context":200000,"output":64000}},"kimi-k2-0905-preview":{"limit":{"context":262144,"output":262144}},"kimi-k2-thinking":{"limit":{"context":262144,"output":262144}},"kimi-k2-thinking-turbo":{"limit":{"context":262144,"output":262144}},"ministral-14b-2512":{"limit":{"context":128000,"output":128000}},"mistral-large-2512":{"limit":{"context":128000,"output":262144}},"qwen-flash":{"limit":{"context":1000000,"output":32768}},"qwen-max-latest":{"limit":{"context":131072,"output":8192}},"qwen-plus":{"limit":{"context":1000000,"output":32768}},"qwen3-235b-a22b":{"limit":{"context":128000,"output":16384}},"qwen3-235b-a22b-instruct-2507":{"limit":{"context":128000,"output":65536}},"qwen3-30b-a3b":{"limit":{"context":128000,"output":8192}},"qwen3-coder-480b-a35b-instruct":{"limit":{"context":262144,"output":65536}},"qwen3-max-2025-09-23":{"limit":{"context":258048,"output":65536}}}},"abacus":{"id":"abacus","npm":"@ai-sdk/openai-compatible","api":"https://routellm.abacus.ai/v1","env":["ABACUS_API_KEY"],"models":{"Qwen/QwQ-32B":{"limit":{"context":32768,"output":32768}},"Qwen/Qwen2.5-72B-Instruct":{"limit":{"context":128000,"output":8192}},"Qwen/Qwen3-235B-A22B-Instruct-2507":{"limit":{"context":262144,"output":8192}},"Qwen/Qwen3-32B":{"limit":{"context":128000,"output":8192}},"Qwen/qwen3-coder-480b-a35b-instruct":{"limit":{"context":262144,"output":65536}},"claude-3-7-sonnet-20250219":{"limit":{"context":200000,"output":64000}},"claude-haiku-4-5-20251001":{"limit":{"context":200000,"output":64000}},"claude-opus-4-1-20250805":{"limit":{"context":200000,"output":32000}},"claude-opus-4-20250514":{"limit":{"context":200000,"output":32000}},"claude-opus-4-5-20251101":{"limit":{"context":200000,"output":64000}},"claude-sonnet-4-20250514":{"limit":{"context":200000,"output":64000}},"claude-sonnet-4-5-20250929":{"limit":{"context":200000,"output":64000}},"deepseek-ai/DeepSeek-R1":{"limit":{"context":128000,"output":8192}},"deepseek-ai/DeepSeek-V3.1-Terminus":{"limit":{"context":128000,"output":8192}},"deepseek-ai/DeepSeek-V3.2":{"limit":{"context":128000,"output":8192}},"deepseek/deepseek-v3.1":{"limit":{"context":128000,"output":8192}},"gemini-2.0-flash-001":{"limit":{"context":1000000,"output":8192}},"gemini-2.0-pro-exp-02-05":{"limit":{"context":2000000,"output":8192}},"gemini-2.5-flash":{"limit":{"context":1048576,"output":65536}},"gemini-2.5-pro":{"limit":{"context":1048576,"output":65536}},"gemini-3-flash-preview":{"limit":{"context":1048576,"output":65536}},"gemini-3-pro-preview":{"limit":{"context":1000000,"output":65000}},"gpt-4.1":{"limit":{"context":1047576,"output":32768}},"gpt-4.1-mini":{"limit":{"context":1047576,"output":32768}},"gpt-4.1-nano":{"limit":{"context":1047576,"output":32768}},"gpt-4o-2024-11-20":{"limit":{"context":128000,"output":16384}},"gpt-4o-mini":{"limit":{"context":128000,"output":16384}},"gpt-5":{"limit":{"context":400000,"output":128000}},"gpt-5-mini":{"limit":{"context":400000,"output":128000}},"gpt-5-nano":{"limit":{"context":400000,"output":128000}},"gpt-5.1":{"limit":{"context":400000,"output":128000}},"gpt-5.1-chat-latest":{"limit":{"context":400000,"output":128000}},"gpt-5.2":{"limit":{"context":400000,"output":128000}},"gpt-5.2-chat-latest":{"limit":{"context":400000,"output":128000}},"grok-4-0709":{"limit":{"context":256000,"output":16384}},"grok-4-1-fast-non-reasoning":{"limit":{"context":2000000,"output":16384}},"grok-4-fast-non-reasoning":{"limit":{"context":2000000,"output":16384}},"grok-code-fast-1":{"limit":{"context":256000,"output":16384}},"kimi-k2-turbo-preview":{"limit":{"context":256000,"output":8192}},"llama-3.3-70b-versatile":{"limit":{"context":128000,"output":32768}},"meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8":{"limit":{"context":1000000,"output":32768}},"meta-llama/Meta-Llama-3.1-405B-Instruct-Turbo":{"limit":{"context":128000,"output":4096}},"meta-llama/Meta-Llama-3.1-70B-Instruct":{"limit":{"context":128000,"output":4096}},"meta-llama/Meta-Llama-3.1-8B-Instruct":{"limit":{"context":128000,"output":4096}},"o3":{"limit":{"context":200000,"output":100000}},"o3-mini":{"limit":{"context":200000,"output":100000}},"o3-pro":{"limit":{"context":200000,"output":100000}},"o4-mini":{"limit":{"context":200000,"output":100000}},"openai/gpt-oss-120b":{"limit":{"context":128000,"output":32768}},"qwen-2.5-coder-32b":{"limit":{"context":128000,"output":8192}},"qwen3-max":{"limit":{"context":131072,"output":16384}},"route-llm":{"limit":{"context":128000,"output":16384}},"zai-org/glm-4.5":{"limit":{"context":128000,"output":8192}},"zai-org/glm-4.6":{"limit":{"context":128000,"output":8192}},"zai-org/glm-4.7":{"limit":{"context":128000,"output":8192}}}},"aihubmix":{"id":"aihubmix","npm":"@ai-sdk/openai-compatible","api":"https://aihubmix.com/v1","env":["AIHUBMIX_API_KEY"],"models":{"Kimi-K2-0905":{"limit":{"context":262144,"output":262144}},"claude-haiku-4-5":{"limit":{"context":200000,"output":64000}},"claude-opus-4-1":{"limit":{"context":200000,"output":32000}},"claude-opus-4-5":{"limit":{"context":200000,"output":32000}},"claude-sonnet-4-5":{"limit":{"context":200000,"output":64000}},"coding-glm-4.7":{"limit":{"context":204800,"output":131072}},"coding-glm-4.7-free":{"limit":{"context":204800,"output":131072}},"coding-minimax-m2.1-free":{"limit":{"context":204800,"output":131072}},"deepseek-v3.2":{"limit":{"context":131000,"output":64000}},"deepseek-v3.2-fast":{"limit":{"context":128000,"output":128000}},"deepseek-v3.2-think":{"limit":{"context":131000,"output":64000}},"gemini-2.5-flash":{"limit":{"context":1000000,"output":65000}},"gemini-2.5-pro":{"limit":{"context":2000000,"output":65000}},"gemini-3-pro-preview":{"limit":{"context":1000000,"output":65000}},"glm-4.6v":{"limit":{"context":128000,"output":32768}},"glm-4.7":{"limit":{"context":204800,"output":131072}},"gpt-4.1":{"limit":{"context":1047576,"output":32768}},"gpt-4.1-mini":{"limit":{"context":1047576,"output":32768}},"gpt-4.1-nano":{"limit":{"context":1047576,"output":32768}},"gpt-4o":{"limit":{"context":128000,"output":16384}},"gpt-5":{"limit":{"context":400000,"output":128000}},"gpt-5-codex":{"limit":{"context":400000,"output":128000}},"gpt-5-mini":{"limit":{"context":200000,"output":64000}},"gpt-5-nano":{"limit":{"context":128000,"output":16384}},"gpt-5-pro":{"limit":{"context":400000,"output":128000}},"gpt-5.1":{"limit":{"context":400000,"output":128000}},"gpt-5.1-codex":{"limit":{"context":400000,"output":128000}},"gpt-5.1-codex-max":{"limit":{"context":400000,"output":128000}},"gpt-5.1-codex-mini":{"limit":{"context":400000,"output":128000}},"gpt-5.2":{"limit":{"context":400000,"output":128000}},"kimi-k2.5":{"limit":{"context":262144,"output":262144}},"minimax-m2.1":{"limit":{"context":204800,"output":131072}},"o4-mini":{"limit":{"context":200000,"output":65536}},"qwen3-235b-a22b-instruct-2507":{"limit":{"context":262144,"output":262144}},"qwen3-235b-a22b-thinking-2507":{"limit":{"context":262144,"output":262144}},"qwen3-coder-480b-a35b-instruct":{"limit":{"context":262144,"output":131000}},"qwen3-max-2026-01-23":{"limit":{"context":262144,"output":65536}}}},"alibaba":{"id":"alibaba","npm":"@ai-sdk/openai-compatible","api":"https://dashscope-intl.aliyuncs.com/compatible-mode/v1","env":["DASHSCOPE_API_KEY"],"models":{"qvq-max":{"limit":{"context":131072,"output":8192}},"qwen-flash":{"limit":{"context":1000000,"output":32768}},"qwen-max":{"limit":{"context":32768,"output":8192}},"qwen-mt-plus":{"limit":{"context":16384,"output":8192}},"qwen-mt-turbo":{"limit":{"context":16384,"output":8192}},"qwen-omni-turbo":{"limit":{"context":32768,"output":2048}},"qwen-omni-turbo-realtime":{"limit":{"context":32768,"output":2048}},"qwen-plus":{"limit":{"context":1000000,"output":32768}},"qwen-plus-character-ja":{"limit":{"context":8192,"output":512}},"qwen-turbo":{"limit":{"context":1000000,"output":16384}},"qwen-vl-max":{"limit":{"context":131072,"output":8192}},"qwen-vl-ocr":{"limit":{"context":34096,"output":4096}},"qwen-vl-plus":{"limit":{"context":131072,"output":8192}},"qwen2-5-14b-instruct":{"limit":{"context":131072,"output":8192}},"qwen2-5-32b-instruct":{"limit":{"context":131072,"output":8192}},"qwen2-5-72b-instruct":{"limit":{"context":131072,"output":8192}},"qwen2-5-7b-instruct":{"limit":{"context":131072,"output":8192}},"qwen2-5-omni-7b":{"limit":{"context":32768,"output":2048}},"qwen2-5-vl-72b-instruct":{"limit":{"context":131072,"output":8192}},"qwen2-5-vl-7b-instruct":{"limit":{"context":131072,"output":8192}},"qwen3-14b":{"limit":{"context":131072,"output":8192}},"qwen3-235b-a22b":{"limit":{"context":131072,"output":16384}},"qwen3-32b":{"limit":{"context":131072,"output":16384}},"qwen3-8b":{"limit":{"context":131072,"output":8192}},"qwen3-asr-flash":{"limit":{"context":53248,"output":4096}},"qwen3-coder-30b-a3b-instruct":{"limit":{"context":262144,"output":65536}},"qwen3-coder-480b-a35b-instruct":{"limit":{"context":262144,"output":65536}},"qwen3-coder-flash":{"limit":{"context":1000000,"output":65536}},"qwen3-coder-plus":{"limit":{"context":1048576,"output":65536}},"qwen3-livetranslate-flash-realtime":{"limit":{"context":53248,"output":4096}},"qwen3-max":{"limit":{"context":262144,"output":65536}},"qwen3-next-80b-a3b-instruct":{"limit":{"context":131072,"output":32768}},"qwen3-next-80b-a3b-thinking":{"limit":{"context":131072,"output":32768}},"qwen3-omni-flash":{"limit":{"context":65536,"output":16384}},"qwen3-omni-flash-realtime":{"limit":{"context":65536,"output":16384}},"qwen3-vl-235b-a22b":{"limit":{"context":131072,"output":32768}},"qwen3-vl-30b-a3b":{"limit":{"context":131072,"output":32768}},"qwen3-vl-plus":{"limit":{"context":262144,"output":32768}},"qwq-plus":{"limit":{"context":131072,"output":8192}}}},"alibaba-cn":{"id":"alibaba-cn","npm":"@ai-sdk/openai-compatible","api":"https://dashscope.aliyuncs.com/compatible-mode/v1","env":["DASHSCOPE_API_KEY"],"models":{"deepseek-r1":{"limit":{"context":131072,"output":16384}},"deepseek-r1-0528":{"limit":{"context":131072,"output":16384}},"deepseek-r1-distill-llama-70b":{"limit":{"context":32768,"output":16384}},"deepseek-r1-distill-llama-8b":{"limit":{"context":32768,"output":16384}},"deepseek-r1-distill-qwen-1-5b":{"limit":{"context":32768,"output":16384}},"deepseek-r1-distill-qwen-14b":{"limit":{"context":32768,"output":16384}},"deepseek-r1-distill-qwen-32b":{"limit":{"context":32768,"output":16384}},"deepseek-r1-distill-qwen-7b":{"limit":{"context":32768,"output":16384}},"deepseek-v3":{"limit":{"context":65536,"output":8192}},"deepseek-v3-1":{"limit":{"context":131072,"output":65536}},"deepseek-v3-2-exp":{"limit":{"context":131072,"output":65536}},"kimi-k2-thinking":{"limit":{"context":262144,"output":16384}},"kimi-k2.5":{"limit":{"context":262144,"output":32768}},"moonshot-kimi-k2-instruct":{"limit":{"context":131072,"output":8192}},"qvq-max":{"limit":{"context":131072,"output":8192}},"qwen-deep-research":{"limit":{"context":1000000,"output":32768}},"qwen-doc-turbo":{"limit":{"context":131072,"output":8192}},"qwen-flash":{"limit":{"context":1000000,"output":32768}},"qwen-long":{"limit":{"context":10000000,"output":8192}},"qwen-math-plus":{"limit":{"context":4096,"output":3072}},"qwen-math-turbo":{"limit":{"context":4096,"output":3072}},"qwen-max":{"limit":{"context":131072,"output":8192}},"qwen-mt-plus":{"limit":{"context":16384,"output":8192}},"qwen-mt-turbo":{"limit":{"context":16384,"output":8192}},"qwen-omni-turbo":{"limit":{"context":32768,"output":2048}},"qwen-omni-turbo-realtime":{"limit":{"context":32768,"output":2048}},"qwen-plus":{"limit":{"context":1000000,"output":32768}},"qwen-plus-character":{"limit":{"context":32768,"output":4096}},"qwen-turbo":{"limit":{"context":1000000,"output":16384}},"qwen-vl-max":{"limit":{"context":131072,"output":8192}},"qwen-vl-ocr":{"limit":{"context":34096,"output":4096}},"qwen-vl-plus":{"limit":{"context":131072,"output":8192}},"qwen2-5-14b-instruct":{"limit":{"context":131072,"output":8192}},"qwen2-5-32b-instruct":{"limit":{"context":131072,"output":8192}},"qwen2-5-72b-instruct":{"limit":{"context":131072,"output":8192}},"qwen2-5-7b-instruct":{"limit":{"context":131072,"output":8192}},"qwen2-5-coder-32b-instruct":{"limit":{"context":131072,"output":8192}},"qwen2-5-coder-7b-instruct":{"limit":{"context":131072,"output":8192}},"qwen2-5-math-72b-instruct":{"limit":{"context":4096,"output":3072}},"qwen2-5-math-7b-instruct":{"limit":{"context":4096,"output":3072}},"qwen2-5-omni-7b":{"limit":{"context":32768,"output":2048}},"qwen2-5-vl-72b-instruct":{"limit":{"context":131072,"output":8192}},"qwen2-5-vl-7b-instruct":{"limit":{"context":131072,"output":8192}},"qwen3-14b":{"limit":{"context":131072,"output":8192}},"qwen3-235b-a22b":{"limit":{"context":131072,"output":16384}},"qwen3-32b":{"limit":{"context":131072,"output":16384}},"qwen3-8b":{"limit":{"context":131072,"output":8192}},"qwen3-asr-flash":{"limit":{"context":53248,"output":4096}},"qwen3-coder-30b-a3b-instruct":{"limit":{"context":262144,"output":65536}},"qwen3-coder-480b-a35b-instruct":{"limit":{"context":262144,"output":65536}},"qwen3-coder-flash":{"limit":{"context":1000000,"output":65536}},"qwen3-coder-plus":{"limit":{"context":1048576,"output":65536}},"qwen3-max":{"limit":{"context":262144,"output":65536}},"qwen3-next-80b-a3b-instruct":{"limit":{"context":131072,"output":32768}},"qwen3-next-80b-a3b-thinking":{"limit":{"context":131072,"output":32768}},"qwen3-omni-flash":{"limit":{"context":65536,"output":16384}},"qwen3-omni-flash-realtime":{"limit":{"context":65536,"output":16384}},"qwen3-vl-235b-a22b":{"limit":{"context":131072,"output":32768}},"qwen3-vl-30b-a3b":{"limit":{"context":131072,"output":32768}},"qwen3-vl-plus":{"limit":{"context":262144,"output":32768}},"qwq-32b":{"limit":{"context":131072,"output":8192}},"qwq-plus":{"limit":{"context":131072,"output":8192}},"tongyi-intent-detect-v3":{"limit":{"context":8192,"output":1024}}}},"amazon-bedrock":{"id":"amazon-bedrock","npm":"@ai-sdk/amazon-bedrock","api":null,"env":["AWS_ACCESS_KEY_ID","AWS_SECRET_ACCESS_KEY","AWS_REGION"],"models":{"ai21.jamba-1-5-large-v1:0":{"limit":{"context":256000,"output":4096}},"ai21.jamba-1-5-mini-v1:0":{"limit":{"context":256000,"output":4096}},"amazon.nova-2-lite-v1:0":{"limit":{"context":128000,"output":4096}},"amazon.nova-lite-v1:0":{"limit":{"context":300000,"output":8192}},"amazon.nova-micro-v1:0":{"limit":{"context":128000,"output":8192}},"amazon.nova-premier-v1:0":{"limit":{"context":1000000,"output":16384}},"amazon.nova-pro-v1:0":{"limit":{"context":300000,"output":8192}},"amazon.titan-text-express-v1":{"limit":{"context":128000,"output":4096}},"amazon.titan-text-express-v1:0:8k":{"limit":{"context":128000,"output":4096}},"anthropic.claude-3-5-haiku-20241022-v1:0":{"limit":{"context":200000,"output":8192}},"anthropic.claude-3-5-sonnet-20240620-v1:0":{"limit":{"context":200000,"output":8192}},"anthropic.claude-3-5-sonnet-20241022-v2:0":{"limit":{"context":200000,"output":8192}},"anthropic.claude-3-7-sonnet-20250219-v1:0":{"limit":{"context":200000,"output":8192}},"anthropic.claude-3-haiku-20240307-v1:0":{"limit":{"context":200000,"output":4096}},"anthropic.claude-3-opus-20240229-v1:0":{"limit":{"context":200000,"output":4096}},"anthropic.claude-3-sonnet-20240229-v1:0":{"limit":{"context":200000,"output":4096}},"anthropic.claude-haiku-4-5-20251001-v1:0":{"limit":{"context":200000,"output":64000}},"anthropic.claude-instant-v1":{"limit":{"context":100000,"output":4096}},"anthropic.claude-opus-4-1-20250805-v1:0":{"limit":{"context":200000,"output":32000}},"anthropic.claude-opus-4-20250514-v1:0":{"limit":{"context":200000,"output":32000}},"anthropic.claude-opus-4-5-20251101-v1:0":{"limit":{"context":200000,"output":64000}},"anthropic.claude-opus-4-6-v1":{"limit":{"context":1000000,"output":128000}},"anthropic.claude-sonnet-4-20250514-v1:0":{"limit":{"context":200000,"output":64000}},"anthropic.claude-sonnet-4-5-20250929-v1:0":{"limit":{"context":200000,"output":64000}},"anthropic.claude-v2":{"limit":{"context":100000,"output":4096}},"anthropic.claude-v2:1":{"limit":{"context":200000,"output":4096}},"cohere.command-light-text-v14":{"limit":{"context":4096,"output":4096}},"cohere.command-r-plus-v1:0":{"limit":{"context":128000,"output":4096}},"cohere.command-r-v1:0":{"limit":{"context":128000,"output":4096}},"cohere.command-text-v14":{"limit":{"context":4096,"output":4096}},"deepseek.r1-v1:0":{"limit":{"context":128000,"output":32768}},"deepseek.v3-v1:0":{"limit":{"context":163840,"output":81920}},"eu.anthropic.claude-haiku-4-5-20251001-v1:0":{"limit":{"context":200000,"output":64000}},"eu.anthropic.claude-opus-4-5-20251101-v1:0":{"limit":{"context":200000,"output":64000}},"eu.anthropic.claude-opus-4-6-v1":{"limit":{"context":1000000,"output":128000}},"eu.anthropic.claude-sonnet-4-20250514-v1:0":{"limit":{"context":200000,"output":64000}},"eu.anthropic.claude-sonnet-4-5-20250929-v1:0":{"limit":{"context":200000,"output":64000}},"global.anthropic.claude-haiku-4-5-20251001-v1:0":{"limit":{"context":200000,"output":64000}},"global.anthropic.claude-opus-4-5-20251101-v1:0":{"limit":{"context":200000,"output":64000}},"global.anthropic.claude-opus-4-6-v1":{"limit":{"context":1000000,"output":128000}},"global.anthropic.claude-sonnet-4-20250514-v1:0":{"limit":{"context":200000,"output":64000}},"global.anthropic.claude-sonnet-4-5-20250929-v1:0":{"limit":{"context":200000,"output":64000}},"google.gemma-3-12b-it":{"limit":{"context":131072,"output":8192}},"google.gemma-3-27b-it":{"limit":{"context":202752,"output":8192}},"google.gemma-3-4b-it":{"limit":{"context":128000,"output":4096}},"meta.llama3-1-70b-instruct-v1:0":{"limit":{"context":128000,"output":4096}},"meta.llama3-1-8b-instruct-v1:0":{"limit":{"context":128000,"output":4096}},"meta.llama3-2-11b-instruct-v1:0":{"limit":{"context":128000,"output":4096}},"meta.llama3-2-1b-instruct-v1:0":{"limit":{"context":131000,"output":4096}},"meta.llama3-2-3b-instruct-v1:0":{"limit":{"context":131000,"output":4096}},"meta.llama3-2-90b-instruct-v1:0":{"limit":{"context":128000,"output":4096}},"meta.llama3-3-70b-instruct-v1:0":{"limit":{"context":128000,"output":4096}},"meta.llama3-70b-instruct-v1:0":{"limit":{"context":8192,"output":2048}},"meta.llama3-8b-instruct-v1:0":{"limit":{"context":8192,"output":2048}},"meta.llama4-maverick-17b-instruct-v1:0":{"limit":{"context":1000000,"output":16384}},"meta.llama4-scout-17b-instruct-v1:0":{"limit":{"context":3500000,"output":16384}},"minimax.minimax-m2":{"limit":{"context":204608,"output":128000}},"minimax.minimax-m2.1":{"limit":{"context":204800,"output":131072}},"mistral.ministral-3-14b-instruct":{"limit":{"context":128000,"output":4096}},"mistral.ministral-3-8b-instruct":{"limit":{"context":128000,"output":4096}},"mistral.mistral-7b-instruct-v0:2":{"limit":{"context":127000,"output":127000}},"mistral.mistral-large-2402-v1:0":{"limit":{"context":128000,"output":4096}},"mistral.mixtral-8x7b-instruct-v0:1":{"limit":{"context":32000,"output":32000}},"mistral.voxtral-mini-3b-2507":{"limit":{"context":128000,"output":4096}},"mistral.voxtral-small-24b-2507":{"limit":{"context":32000,"output":8192}},"moonshot.kimi-k2-thinking":{"limit":{"context":256000,"output":256000}},"moonshotai.kimi-k2.5":{"limit":{"context":256000,"output":256000}},"nvidia.nemotron-nano-12b-v2":{"limit":{"context":128000,"output":4096}},"nvidia.nemotron-nano-9b-v2":{"limit":{"context":128000,"output":4096}},"openai.gpt-oss-120b-1:0":{"limit":{"context":128000,"output":4096}},"openai.gpt-oss-20b-1:0":{"limit":{"context":128000,"output":4096}},"openai.gpt-oss-safeguard-120b":{"limit":{"context":128000,"output":4096}},"openai.gpt-oss-safeguard-20b":{"limit":{"context":128000,"output":4096}},"qwen.qwen3-235b-a22b-2507-v1:0":{"limit":{"context":262144,"output":131072}},"qwen.qwen3-32b-v1:0":{"limit":{"context":16384,"output":16384}},"qwen.qwen3-coder-30b-a3b-v1:0":{"limit":{"context":262144,"output":131072}},"qwen.qwen3-coder-480b-a35b-v1:0":{"limit":{"context":131072,"output":65536}},"qwen.qwen3-next-80b-a3b":{"limit":{"context":262000,"output":262000}},"qwen.qwen3-vl-235b-a22b":{"limit":{"context":262000,"output":262000}},"us.anthropic.claude-haiku-4-5-20251001-v1:0":{"limit":{"context":200000,"output":64000}},"us.anthropic.claude-opus-4-1-20250805-v1:0":{"limit":{"context":200000,"output":32000}},"us.anthropic.claude-opus-4-20250514-v1:0":{"limit":{"context":200000,"output":32000}},"us.anthropic.claude-opus-4-5-20251101-v1:0":{"limit":{"context":200000,"output":64000}},"us.anthropic.claude-opus-4-6-v1":{"limit":{"context":1000000,"output":128000}},"us.anthropic.claude-sonnet-4-20250514-v1:0":{"limit":{"context":200000,"output":64000}},"us.anthropic.claude-sonnet-4-5-20250929-v1:0":{"limit":{"context":200000,"output":64000}},"writer.palmyra-x4-v1:0":{"limit":{"context":122880,"output":8192}},"writer.palmyra-x5-v1:0":{"limit":{"context":1040000,"output":8192}},"zai.glm-4.7":{"limit":{"context":204800,"output":131072}},"zai.glm-4.7-flash":{"limit":{"context":200000,"output":131072}}}},"anthropic":{"id":"anthropic","npm":"@ai-sdk/anthropic","api":null,"env":["ANTHROPIC_API_KEY"],"models":{"claude-3-5-haiku-20241022":{"limit":{"context":200000,"output":8192}},"claude-3-5-haiku-latest":{"limit":{"context":200000,"output":8192}},"claude-3-5-sonnet-20240620":{"limit":{"context":200000,"output":8192}},"claude-3-5-sonnet-20241022":{"limit":{"context":200000,"output":8192}},"claude-3-7-sonnet-20250219":{"limit":{"context":200000,"output":64000}},"claude-3-7-sonnet-latest":{"limit":{"context":200000,"output":64000}},"claude-3-haiku-20240307":{"limit":{"context":200000,"output":4096}},"claude-3-opus-20240229":{"limit":{"context":200000,"output":4096}},"claude-3-sonnet-20240229":{"limit":{"context":200000,"output":4096}},"claude-haiku-4-5":{"limit":{"context":200000,"output":64000}},"claude-haiku-4-5-20251001":{"limit":{"context":200000,"output":64000}},"claude-opus-4-0":{"limit":{"context":200000,"output":32000}},"claude-opus-4-1":{"limit":{"context":200000,"output":32000}},"claude-opus-4-1-20250805":{"limit":{"context":200000,"output":32000}},"claude-opus-4-20250514":{"limit":{"context":200000,"output":32000}},"claude-opus-4-5":{"limit":{"context":200000,"output":64000}},"claude-opus-4-5-20251101":{"limit":{"context":200000,"output":64000}},"claude-opus-4-6":{"limit":{"context":200000,"output":128000}},"claude-sonnet-4-0":{"limit":{"context":200000,"output":64000}},"claude-sonnet-4-20250514":{"limit":{"context":200000,"output":64000}},"claude-sonnet-4-5":{"limit":{"context":200000,"output":64000}},"claude-sonnet-4-5-20250929":{"limit":{"context":200000,"output":64000}}}},"azure":{"id":"azure","npm":"@ai-sdk/azure","api":null,"env":["AZURE_RESOURCE_NAME","AZURE_API_KEY"],"models":{"claude-haiku-4-5":{"limit":{"context":200000,"output":64000}},"claude-opus-4-1":{"limit":{"context":200000,"output":32000}},"claude-opus-4-5":{"limit":{"context":200000,"output":64000}},"claude-opus-4-6":{"limit":{"context":200000,"output":128000}},"claude-sonnet-4-5":{"limit":{"context":200000,"output":64000}},"codestral-2501":{"limit":{"context":256000,"output":256000}},"codex-mini":{"limit":{"context":200000,"output":100000}},"cohere-command-a":{"limit":{"context":256000,"output":8000}},"cohere-command-r-08-2024":{"limit":{"context":128000,"output":4000}},"cohere-command-r-plus-08-2024":{"limit":{"context":128000,"output":4000}},"cohere-embed-v-4-0":{"limit":{"context":128000,"output":1536}},"cohere-embed-v3-english":{"limit":{"context":512,"output":1024}},"cohere-embed-v3-multilingual":{"limit":{"context":512,"output":1024}},"deepseek-r1":{"limit":{"context":163840,"output":163840}},"deepseek-r1-0528":{"limit":{"context":163840,"output":163840}},"deepseek-v3-0324":{"limit":{"context":131072,"output":131072}},"deepseek-v3.1":{"limit":{"context":131072,"output":131072}},"deepseek-v3.2":{"limit":{"context":128000,"output":128000}},"deepseek-v3.2-speciale":{"limit":{"context":128000,"output":128000}},"gpt-3.5-turbo-0125":{"limit":{"context":16384,"output":16384}},"gpt-3.5-turbo-0301":{"limit":{"context":4096,"output":4096}},"gpt-3.5-turbo-0613":{"limit":{"context":16384,"output":16384}},"gpt-3.5-turbo-1106":{"limit":{"context":16384,"output":16384}},"gpt-3.5-turbo-instruct":{"limit":{"context":4096,"output":4096}},"gpt-4":{"limit":{"context":8192,"output":8192}},"gpt-4-32k":{"limit":{"context":32768,"output":32768}},"gpt-4-turbo":{"limit":{"context":128000,"output":4096}},"gpt-4-turbo-vision":{"limit":{"context":128000,"output":4096}},"gpt-4.1":{"limit":{"context":1047576,"output":32768}},"gpt-4.1-mini":{"limit":{"context":1047576,"output":32768}},"gpt-4.1-nano":{"limit":{"context":1047576,"output":32768}},"gpt-4o":{"limit":{"context":128000,"output":16384}},"gpt-4o-mini":{"limit":{"context":128000,"output":16384}},"gpt-5":{"limit":{"context":272000,"output":128000}},"gpt-5-chat":{"limit":{"context":128000,"output":16384}},"gpt-5-codex":{"limit":{"context":400000,"output":128000}},"gpt-5-mini":{"limit":{"context":272000,"output":128000}},"gpt-5-nano":{"limit":{"context":272000,"output":128000}},"gpt-5-pro":{"limit":{"context":400000,"output":272000}},"gpt-5.1":{"limit":{"context":272000,"output":128000}},"gpt-5.1-chat":{"limit":{"context":128000,"output":16384}},"gpt-5.1-codex":{"limit":{"context":400000,"output":128000}},"gpt-5.1-codex-max":{"limit":{"context":400000,"output":128000}},"gpt-5.1-codex-mini":{"limit":{"context":400000,"output":128000}},"gpt-5.2":{"limit":{"context":400000,"output":128000}},"gpt-5.2-chat":{"limit":{"context":128000,"output":16384}},"gpt-5.2-codex":{"limit":{"context":400000,"output":128000}},"grok-3":{"limit":{"context":131072,"output":8192}},"grok-3-mini":{"limit":{"context":131072,"output":8192}},"grok-4":{"limit":{"context":256000,"output":64000}},"grok-4-fast-non-reasoning":{"limit":{"context":2000000,"output":30000}},"grok-4-fast-reasoning":{"limit":{"context":2000000,"output":30000}},"grok-code-fast-1":{"limit":{"context":256000,"output":10000}},"kimi-k2-thinking":{"limit":{"context":262144,"output":262144}},"kimi-k2.5":{"limit":{"context":262144,"output":262144}},"llama-3.2-11b-vision-instruct":{"limit":{"context":128000,"output":8192}},"llama-3.2-90b-vision-instruct":{"limit":{"context":128000,"output":8192}},"llama-3.3-70b-instruct":{"limit":{"context":128000,"output":32768}},"llama-4-maverick-17b-128e-instruct-fp8":{"limit":{"context":128000,"output":8192}},"llama-4-scout-17b-16e-instruct":{"limit":{"context":128000,"output":8192}},"mai-ds-r1":{"limit":{"context":128000,"output":8192}},"meta-llama-3-70b-instruct":{"limit":{"context":8192,"output":2048}},"meta-llama-3-8b-instruct":{"limit":{"context":8192,"output":2048}},"meta-llama-3.1-405b-instruct":{"limit":{"context":128000,"output":32768}},"meta-llama-3.1-70b-instruct":{"limit":{"context":128000,"output":32768}},"meta-llama-3.1-8b-instruct":{"limit":{"context":128000,"output":32768}},"ministral-3b":{"limit":{"context":128000,"output":8192}},"mistral-large-2411":{"limit":{"context":128000,"output":32768}},"mistral-medium-2505":{"limit":{"context":128000,"output":128000}},"mistral-nemo":{"limit":{"context":128000,"output":128000}},"mistral-small-2503":{"limit":{"context":128000,"output":32768}},"model-router":{"limit":{"context":128000,"output":16384}},"o1":{"limit":{"context":200000,"output":100000}},"o1-mini":{"limit":{"context":128000,"output":65536}},"o1-preview":{"limit":{"context":128000,"output":32768}},"o3":{"limit":{"context":200000,"output":100000}},"o3-mini":{"limit":{"context":200000,"output":100000}},"o4-mini":{"limit":{"context":200000,"output":100000}},"phi-3-medium-128k-instruct":{"limit":{"context":128000,"output":4096}},"phi-3-medium-4k-instruct":{"limit":{"context":4096,"output":1024}},"phi-3-mini-128k-instruct":{"limit":{"context":128000,"output":4096}},"phi-3-mini-4k-instruct":{"limit":{"context":4096,"output":1024}},"phi-3-small-128k-instruct":{"limit":{"context":128000,"output":4096}},"phi-3-small-8k-instruct":{"limit":{"context":8192,"output":2048}},"phi-3.5-mini-instruct":{"limit":{"context":128000,"output":4096}},"phi-3.5-moe-instruct":{"limit":{"context":128000,"output":4096}},"phi-4":{"limit":{"context":128000,"output":4096}},"phi-4-mini":{"limit":{"context":128000,"output":4096}},"phi-4-mini-reasoning":{"limit":{"context":128000,"output":4096}},"phi-4-multimodal":{"limit":{"context":128000,"output":4096}},"phi-4-reasoning":{"limit":{"context":32000,"output":4096}},"phi-4-reasoning-plus":{"limit":{"context":32000,"output":4096}},"text-embedding-3-large":{"limit":{"context":8191,"output":3072}},"text-embedding-3-small":{"limit":{"context":8191,"output":1536}},"text-embedding-ada-002":{"limit":{"context":8192,"output":1536}}}},"azure-cognitive-services":{"id":"azure-cognitive-services","npm":"@ai-sdk/azure","api":null,"env":["AZURE_COGNITIVE_SERVICES_RESOURCE_NAME","AZURE_COGNITIVE_SERVICES_API_KEY"],"models":{"claude-haiku-4-5":{"limit":{"context":200000,"output":64000}},"claude-opus-4-1":{"limit":{"context":200000,"output":32000}},"claude-opus-4-5":{"limit":{"context":200000,"output":64000}},"claude-opus-4-6":{"limit":{"context":200000,"output":128000}},"claude-sonnet-4-5":{"limit":{"context":200000,"output":64000}},"codestral-2501":{"limit":{"context":256000,"output":256000}},"codex-mini":{"limit":{"context":200000,"output":100000}},"cohere-command-a":{"limit":{"context":256000,"output":8000}},"cohere-command-r-08-2024":{"limit":{"context":128000,"output":4000}},"cohere-command-r-plus-08-2024":{"limit":{"context":128000,"output":4000}},"cohere-embed-v-4-0":{"limit":{"context":128000,"output":1536}},"cohere-embed-v3-english":{"limit":{"context":512,"output":1024}},"cohere-embed-v3-multilingual":{"limit":{"context":512,"output":1024}},"deepseek-r1":{"limit":{"context":163840,"output":163840}},"deepseek-r1-0528":{"limit":{"context":163840,"output":163840}},"deepseek-v3-0324":{"limit":{"context":131072,"output":131072}},"deepseek-v3.1":{"limit":{"context":131072,"output":131072}},"deepseek-v3.2":{"limit":{"context":128000,"output":128000}},"deepseek-v3.2-speciale":{"limit":{"context":128000,"output":128000}},"gpt-3.5-turbo-0125":{"limit":{"context":16384,"output":16384}},"gpt-3.5-turbo-0301":{"limit":{"context":4096,"output":4096}},"gpt-3.5-turbo-0613":{"limit":{"context":16384,"output":16384}},"gpt-3.5-turbo-1106":{"limit":{"context":16384,"output":16384}},"gpt-3.5-turbo-instruct":{"limit":{"context":4096,"output":4096}},"gpt-4":{"limit":{"context":8192,"output":8192}},"gpt-4-32k":{"limit":{"context":32768,"output":32768}},"gpt-4-turbo":{"limit":{"context":128000,"output":4096}},"gpt-4-turbo-vision":{"limit":{"context":128000,"output":4096}},"gpt-4.1":{"limit":{"context":1047576,"output":32768}},"gpt-4.1-mini":{"limit":{"context":1047576,"output":32768}},"gpt-4.1-nano":{"limit":{"context":1047576,"output":32768}},"gpt-4o":{"limit":{"context":128000,"output":16384}},"gpt-4o-mini":{"limit":{"context":128000,"output":16384}},"gpt-5":{"limit":{"context":272000,"output":128000}},"gpt-5-chat":{"limit":{"context":128000,"output":16384}},"gpt-5-codex":{"limit":{"context":400000,"output":128000}},"gpt-5-mini":{"limit":{"context":272000,"output":128000}},"gpt-5-nano":{"limit":{"context":272000,"output":128000}},"gpt-5-pro":{"limit":{"context":400000,"output":272000}},"gpt-5.1":{"limit":{"context":272000,"output":128000}},"gpt-5.1-chat":{"limit":{"context":128000,"output":16384}},"gpt-5.1-codex":{"limit":{"context":400000,"output":128000}},"gpt-5.1-codex-mini":{"limit":{"context":400000,"output":128000}},"gpt-5.2-chat":{"limit":{"context":128000,"output":16384}},"gpt-5.2-codex":{"limit":{"context":400000,"output":128000}},"grok-3":{"limit":{"context":131072,"output":8192}},"grok-3-mini":{"limit":{"context":131072,"output":8192}},"grok-4":{"limit":{"context":256000,"output":64000}},"grok-4-fast-non-reasoning":{"limit":{"context":2000000,"output":30000}},"grok-4-fast-reasoning":{"limit":{"context":2000000,"output":30000}},"grok-code-fast-1":{"limit":{"context":256000,"output":10000}},"kimi-k2-thinking":{"limit":{"context":262144,"output":262144}},"kimi-k2.5":{"limit":{"context":262144,"output":262144}},"llama-3.2-11b-vision-instruct":{"limit":{"context":128000,"output":8192}},"llama-3.2-90b-vision-instruct":{"limit":{"context":128000,"output":8192}},"llama-3.3-70b-instruct":{"limit":{"context":128000,"output":32768}},"llama-4-maverick-17b-128e-instruct-fp8":{"limit":{"context":128000,"output":8192}},"llama-4-scout-17b-16e-instruct":{"limit":{"context":128000,"output":8192}},"mai-ds-r1":{"limit":{"context":128000,"output":8192}},"meta-llama-3-70b-instruct":{"limit":{"context":8192,"output":2048}},"meta-llama-3-8b-instruct":{"limit":{"context":8192,"output":2048}},"meta-llama-3.1-405b-instruct":{"limit":{"context":128000,"output":32768}},"meta-llama-3.1-70b-instruct":{"limit":{"context":128000,"output":32768}},"meta-llama-3.1-8b-instruct":{"limit":{"context":128000,"output":32768}},"ministral-3b":{"limit":{"context":128000,"output":8192}},"mistral-large-2411":{"limit":{"context":128000,"output":32768}},"mistral-medium-2505":{"limit":{"context":128000,"output":128000}},"mistral-nemo":{"limit":{"context":128000,"output":128000}},"mistral-small-2503":{"limit":{"context":128000,"output":32768}},"model-router":{"limit":{"context":128000,"output":16384}},"o1":{"limit":{"context":200000,"output":100000}},"o1-mini":{"limit":{"context":128000,"output":65536}},"o1-preview":{"limit":{"context":128000,"output":32768}},"o3":{"limit":{"context":200000,"output":100000}},"o3-mini":{"limit":{"context":200000,"output":100000}},"o4-mini":{"limit":{"context":200000,"output":100000}},"phi-3-medium-128k-instruct":{"limit":{"context":128000,"output":4096}},"phi-3-medium-4k-instruct":{"limit":{"context":4096,"output":1024}},"phi-3-mini-128k-instruct":{"limit":{"context":128000,"output":4096}},"phi-3-mini-4k-instruct":{"limit":{"context":4096,"output":1024}},"phi-3-small-128k-instruct":{"limit":{"context":128000,"output":4096}},"phi-3-small-8k-instruct":{"limit":{"context":8192,"output":2048}},"phi-3.5-mini-instruct":{"limit":{"context":128000,"output":4096}},"phi-3.5-moe-instruct":{"limit":{"context":128000,"output":4096}},"phi-4":{"limit":{"context":128000,"output":4096}},"phi-4-mini":{"limit":{"context":128000,"output":4096}},"phi-4-mini-reasoning":{"limit":{"context":128000,"output":4096}},"phi-4-multimodal":{"limit":{"context":128000,"output":4096}},"phi-4-reasoning":{"limit":{"context":32000,"output":4096}},"phi-4-reasoning-plus":{"limit":{"context":32000,"output":4096}},"text-embedding-3-large":{"limit":{"context":8191,"output":3072}},"text-embedding-3-small":{"limit":{"context":8191,"output":1536}},"text-embedding-ada-002":{"limit":{"context":8192,"output":1536}}}},"bailing":{"id":"bailing","npm":"@ai-sdk/openai-compatible","api":"https://api.tbox.cn/api/llm/v1/chat/completions","env":["BAILING_API_TOKEN"],"models":{"Ling-1T":{"limit":{"context":128000,"output":32000}},"Ring-1T":{"limit":{"context":128000,"output":32000}}}},"baseten":{"id":"baseten","npm":"@ai-sdk/openai-compatible","api":"https://inference.baseten.co/v1","env":["BASETEN_API_KEY"],"models":{"Qwen/Qwen3-Coder-480B-A35B-Instruct":{"limit":{"context":262144,"output":66536}},"deepseek-ai/DeepSeek-V3.2":{"limit":{"context":163800,"output":131100}},"moonshotai/Kimi-K2-Instruct-0905":{"limit":{"context":262144,"output":262144}},"moonshotai/Kimi-K2-Thinking":{"limit":{"context":262144,"output":262144}},"moonshotai/Kimi-K2.5":{"limit":{"context":262144,"output":8192}},"zai-org/GLM-4.6":{"limit":{"context":200000,"output":200000}},"zai-org/GLM-4.7":{"limit":{"context":204800,"output":131072}}}},"berget":{"id":"berget","npm":"@ai-sdk/openai-compatible","api":"https://api.berget.ai/v1","env":["BERGET_API_KEY"],"models":{"BAAI/bge-reranker-v2-m3":{"limit":{"context":512,"output":512}},"KBLab/kb-whisper-large":{"limit":{"context":480000,"output":4800}},"intfloat/multilingual-e5-large":{"limit":{"context":512,"output":1024}},"intfloat/multilingual-e5-large-instruct":{"limit":{"context":512,"output":1024}},"meta-llama/Llama-3.3-70B-Instruct":{"limit":{"context":128000,"output":8192}},"mistralai/Mistral-Small-3.2-24B-Instruct-2506":{"limit":{"context":32000,"output":8192}},"openai/gpt-oss-120b":{"limit":{"context":128000,"output":8192}},"zai-org/GLM-4.7":{"limit":{"context":128000,"output":8192}}}},"cerebras":{"id":"cerebras","npm":"@ai-sdk/cerebras","api":null,"env":["CEREBRAS_API_KEY"],"models":{"gpt-oss-120b":{"limit":{"context":131072,"output":32768}},"llama3.1-8b":{"limit":{"context":32000,"output":8000}},"qwen-3-235b-a22b-instruct-2507":{"limit":{"context":131000,"output":32000}},"zai-glm-4.7":{"limit":{"context":131072,"output":40000}}}},"chutes":{"id":"chutes","npm":"@ai-sdk/openai-compatible","api":"https://llm.chutes.ai/v1","env":["CHUTES_API_KEY"],"models":{"MiniMaxAI/MiniMax-M2.1-TEE":{"limit":{"context":196608,"output":65536}},"NousResearch/DeepHermes-3-Mistral-24B-Preview":{"limit":{"context":32768,"output":32768}},"NousResearch/Hermes-4-14B":{"limit":{"context":40960,"output":40960}},"NousResearch/Hermes-4-405B-FP8-TEE":{"limit":{"context":131072,"output":65536}},"NousResearch/Hermes-4-70B":{"limit":{"context":131072,"output":131072}},"NousResearch/Hermes-4.3-36B":{"limit":{"context":32768,"output":8192}},"OpenGVLab/InternVL3-78B-TEE":{"limit":{"context":32768,"output":32768}},"Qwen/Qwen2.5-72B-Instruct":{"limit":{"context":32768,"output":32768}},"Qwen/Qwen2.5-Coder-32B-Instruct":{"limit":{"context":32768,"output":32768}},"Qwen/Qwen2.5-VL-32B-Instruct":{"limit":{"context":16384,"output":16384}},"Qwen/Qwen2.5-VL-72B-Instruct-TEE":{"limit":{"context":32768,"output":32768}},"Qwen/Qwen3-14B":{"limit":{"context":40960,"output":40960}},"Qwen/Qwen3-235B-A22B":{"limit":{"context":40960,"output":40960}},"Qwen/Qwen3-235B-A22B-Instruct-2507-TEE":{"limit":{"context":262144,"output":65536}},"Qwen/Qwen3-235B-A22B-Thinking-2507":{"limit":{"context":262144,"output":262144}},"Qwen/Qwen3-30B-A3B":{"limit":{"context":40960,"output":40960}},"Qwen/Qwen3-30B-A3B-Instruct-2507":{"limit":{"context":262144,"output":262144}},"Qwen/Qwen3-32B":{"limit":{"context":40960,"output":40960}},"Qwen/Qwen3-Coder-480B-A35B-Instruct-FP8-TEE":{"limit":{"context":262144,"output":262144}},"Qwen/Qwen3-Coder-Next":{"limit":{"context":262144,"output":65536}},"Qwen/Qwen3-Next-80B-A3B-Instruct":{"limit":{"context":262144,"output":262144}},"Qwen/Qwen3-VL-235B-A22B-Instruct":{"limit":{"context":262144,"output":262144}},"Qwen/Qwen3Guard-Gen-0.6B":{"limit":{"context":32768,"output":8192}},"XiaomiMiMo/MiMo-V2-Flash":{"limit":{"context":32768,"output":8192}},"chutesai/Mistral-Small-3.1-24B-Instruct-2503":{"limit":{"context":131072,"output":131072}},"chutesai/Mistral-Small-3.2-24B-Instruct-2506":{"limit":{"context":131072,"output":131072}},"deepseek-ai/DeepSeek-R1-0528-TEE":{"limit":{"context":163840,"output":65536}},"deepseek-ai/DeepSeek-R1-Distill-Llama-70B":{"limit":{"context":131072,"output":131072}},"deepseek-ai/DeepSeek-R1-TEE":{"limit":{"context":163840,"output":163840}},"deepseek-ai/DeepSeek-V3":{"limit":{"context":163840,"output":163840}},"deepseek-ai/DeepSeek-V3-0324-TEE":{"limit":{"context":163840,"output":65536}},"deepseek-ai/DeepSeek-V3.1-TEE":{"limit":{"context":163840,"output":65536}},"deepseek-ai/DeepSeek-V3.1-Terminus-TEE":{"limit":{"context":163840,"output":65536}},"deepseek-ai/DeepSeek-V3.2-Speciale-TEE":{"limit":{"context":163840,"output":65536}},"deepseek-ai/DeepSeek-V3.2-TEE":{"limit":{"context":163840,"output":65536}},"miromind-ai/MiroThinker-v1.5-235B":{"limit":{"context":262144,"output":8192}},"mistralai/Devstral-2-123B-Instruct-2512-TEE":{"limit":{"context":262144,"output":65536}},"moonshotai/Kimi-K2-Instruct-0905":{"limit":{"context":262144,"output":262144}},"moonshotai/Kimi-K2-Thinking-TEE":{"limit":{"context":262144,"output":65535}},"moonshotai/Kimi-K2.5-TEE":{"limit":{"context":262144,"output":65535}},"nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-BF16":{"limit":{"context":262144,"output":262144}},"openai/gpt-oss-120b-TEE":{"limit":{"context":131072,"output":65536}},"openai/gpt-oss-20b":{"limit":{"context":131072,"output":131072}},"rednote-hilab/dots.ocr":{"limit":{"context":131072,"output":131072}},"tngtech/DeepSeek-R1T-Chimera":{"limit":{"context":163840,"output":163840}},"tngtech/DeepSeek-TNG-R1T2-Chimera":{"limit":{"context":163840,"output":163840}},"tngtech/TNG-R1T-Chimera-TEE":{"limit":{"context":163840,"output":65536}},"tngtech/TNG-R1T-Chimera-Turbo":{"limit":{"context":163840,"output":65536}},"unsloth/Llama-3.2-1B-Instruct":{"limit":{"context":32768,"output":8192}},"unsloth/Llama-3.2-3B-Instruct":{"limit":{"context":16384,"output":16384}},"unsloth/Mistral-Nemo-Instruct-2407":{"limit":{"context":131072,"output":131072}},"unsloth/Mistral-Small-24B-Instruct-2501":{"limit":{"context":32768,"output":32768}},"unsloth/gemma-3-12b-it":{"limit":{"context":131072,"output":131072}},"unsloth/gemma-3-27b-it":{"limit":{"context":128000,"output":65536}},"unsloth/gemma-3-4b-it":{"limit":{"context":96000,"output":96000}},"zai-org/GLM-4.5-Air":{"limit":{"context":131072,"output":131072}},"zai-org/GLM-4.5-FP8":{"limit":{"context":131072,"output":65536}},"zai-org/GLM-4.5-TEE":{"limit":{"context":131072,"output":65536}},"zai-org/GLM-4.6-FP8":{"limit":{"context":202752,"output":65535}},"zai-org/GLM-4.6-TEE":{"limit":{"context":202752,"output":65536}},"zai-org/GLM-4.6V":{"limit":{"context":131072,"output":65536}},"zai-org/GLM-4.7-FP8":{"limit":{"context":202752,"output":65535}},"zai-org/GLM-4.7-Flash":{"limit":{"context":202752,"output":65535}},"zai-org/GLM-4.7-TEE":{"limit":{"context":202752,"output":65535}}}},"cloudflare-ai-gateway":{"id":"cloudflare-ai-gateway","npm":"ai-gateway-provider","api":null,"env":["CLOUDFLARE_API_TOKEN","CLOUDFLARE_ACCOUNT_ID","CLOUDFLARE_GATEWAY_ID"],"models":{"anthropic/claude-3-5-haiku":{"limit":{"context":200000,"output":8192}},"anthropic/claude-3-haiku":{"limit":{"context":200000,"output":4096}},"anthropic/claude-3-opus":{"limit":{"context":200000,"output":4096}},"anthropic/claude-3-sonnet":{"limit":{"context":200000,"output":4096}},"anthropic/claude-3.5-haiku":{"limit":{"context":200000,"output":8192}},"anthropic/claude-3.5-sonnet":{"limit":{"context":200000,"output":8192}},"anthropic/claude-haiku-4-5":{"limit":{"context":200000,"output":64000}},"anthropic/claude-opus-4":{"limit":{"context":200000,"output":32000}},"anthropic/claude-opus-4-1":{"limit":{"context":200000,"output":32000}},"anthropic/claude-opus-4-5":{"limit":{"context":200000,"output":64000}},"anthropic/claude-opus-4-6":{"limit":{"context":1000000,"output":128000}},"anthropic/claude-sonnet-4":{"limit":{"context":200000,"output":64000}},"anthropic/claude-sonnet-4-5":{"limit":{"context":200000,"output":64000}},"openai/gpt-3.5-turbo":{"limit":{"context":16385,"output":4096}},"openai/gpt-4":{"limit":{"context":8192,"output":8192}},"openai/gpt-4-turbo":{"limit":{"context":128000,"output":4096}},"openai/gpt-4o":{"limit":{"context":128000,"output":16384}},"openai/gpt-4o-mini":{"limit":{"context":128000,"output":16384}},"openai/gpt-5.1":{"limit":{"context":400000,"output":128000}},"openai/gpt-5.1-codex":{"limit":{"context":400000,"output":128000}},"openai/gpt-5.2":{"limit":{"context":400000,"output":128000}},"openai/o1":{"limit":{"context":200000,"output":100000}},"openai/o3":{"limit":{"context":200000,"output":100000}},"openai/o3-mini":{"limit":{"context":200000,"output":100000}},"openai/o3-pro":{"limit":{"context":200000,"output":100000}},"openai/o4-mini":{"limit":{"context":200000,"output":100000}},"workers-ai/@cf/ai4bharat/indictrans2-en-indic-1B":{"limit":{"context":128000,"output":16384}},"workers-ai/@cf/aisingapore/gemma-sea-lion-v4-27b-it":{"limit":{"context":128000,"output":16384}},"workers-ai/@cf/baai/bge-base-en-v1.5":{"limit":{"context":128000,"output":16384}},"workers-ai/@cf/baai/bge-large-en-v1.5":{"limit":{"context":128000,"output":16384}},"workers-ai/@cf/baai/bge-m3":{"limit":{"context":128000,"output":16384}},"workers-ai/@cf/baai/bge-reranker-base":{"limit":{"context":128000,"output":16384}},"workers-ai/@cf/baai/bge-small-en-v1.5":{"limit":{"context":128000,"output":16384}},"workers-ai/@cf/deepgram/aura-2-en":{"limit":{"context":128000,"output":16384}},"workers-ai/@cf/deepgram/aura-2-es":{"limit":{"context":128000,"output":16384}},"workers-ai/@cf/deepgram/nova-3":{"limit":{"context":128000,"output":16384}},"workers-ai/@cf/deepseek-ai/deepseek-r1-distill-qwen-32b":{"limit":{"context":128000,"output":16384}},"workers-ai/@cf/facebook/bart-large-cnn":{"limit":{"context":128000,"output":16384}},"workers-ai/@cf/google/gemma-3-12b-it":{"limit":{"context":128000,"output":16384}},"workers-ai/@cf/huggingface/distilbert-sst-2-int8":{"limit":{"context":128000,"output":16384}},"workers-ai/@cf/ibm-granite/granite-4.0-h-micro":{"limit":{"context":128000,"output":16384}},"workers-ai/@cf/meta/llama-2-7b-chat-fp16":{"limit":{"context":128000,"output":16384}},"workers-ai/@cf/meta/llama-3-8b-instruct":{"limit":{"context":128000,"output":16384}},"workers-ai/@cf/meta/llama-3-8b-instruct-awq":{"limit":{"context":128000,"output":16384}},"workers-ai/@cf/meta/llama-3.1-8b-instruct":{"limit":{"context":128000,"output":16384}},"workers-ai/@cf/meta/llama-3.1-8b-instruct-awq":{"limit":{"context":128000,"output":16384}},"workers-ai/@cf/meta/llama-3.1-8b-instruct-fp8":{"limit":{"context":128000,"output":16384}},"workers-ai/@cf/meta/llama-3.2-11b-vision-instruct":{"limit":{"context":128000,"output":16384}},"workers-ai/@cf/meta/llama-3.2-1b-instruct":{"limit":{"context":128000,"output":16384}},"workers-ai/@cf/meta/llama-3.2-3b-instruct":{"limit":{"context":128000,"output":16384}},"workers-ai/@cf/meta/llama-3.3-70b-instruct-fp8-fast":{"limit":{"context":128000,"output":16384}},"workers-ai/@cf/meta/llama-4-scout-17b-16e-instruct":{"limit":{"context":128000,"output":16384}},"workers-ai/@cf/meta/llama-guard-3-8b":{"limit":{"context":128000,"output":16384}},"workers-ai/@cf/meta/m2m100-1.2b":{"limit":{"context":128000,"output":16384}},"workers-ai/@cf/mistral/mistral-7b-instruct-v0.1":{"limit":{"context":128000,"output":16384}},"workers-ai/@cf/mistralai/mistral-small-3.1-24b-instruct":{"limit":{"context":128000,"output":16384}},"workers-ai/@cf/myshell-ai/melotts":{"limit":{"context":128000,"output":16384}},"workers-ai/@cf/openai/gpt-oss-120b":{"limit":{"context":128000,"output":16384}},"workers-ai/@cf/openai/gpt-oss-20b":{"limit":{"context":128000,"output":16384}},"workers-ai/@cf/pfnet/plamo-embedding-1b":{"limit":{"context":128000,"output":16384}},"workers-ai/@cf/pipecat-ai/smart-turn-v2":{"limit":{"context":128000,"output":16384}},"workers-ai/@cf/qwen/qwen2.5-coder-32b-instruct":{"limit":{"context":128000,"output":16384}},"workers-ai/@cf/qwen/qwen3-30b-a3b-fp8":{"limit":{"context":128000,"output":16384}},"workers-ai/@cf/qwen/qwen3-embedding-0.6b":{"limit":{"context":128000,"output":16384}},"workers-ai/@cf/qwen/qwq-32b":{"limit":{"context":128000,"output":16384}}}},"cloudflare-workers-ai":{"id":"cloudflare-workers-ai","npm":"@ai-sdk/openai-compatible","api":"https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/ai/v1","env":["CLOUDFLARE_ACCOUNT_ID","CLOUDFLARE_API_KEY"],"models":{"@cf/ai4bharat/indictrans2-en-indic-1B":{"limit":{"context":128000,"output":16384}},"@cf/aisingapore/gemma-sea-lion-v4-27b-it":{"limit":{"context":128000,"output":16384}},"@cf/baai/bge-base-en-v1.5":{"limit":{"context":128000,"output":16384}},"@cf/baai/bge-large-en-v1.5":{"limit":{"context":128000,"output":16384}},"@cf/baai/bge-m3":{"limit":{"context":128000,"output":16384}},"@cf/baai/bge-reranker-base":{"limit":{"context":128000,"output":16384}},"@cf/baai/bge-small-en-v1.5":{"limit":{"context":128000,"output":16384}},"@cf/deepgram/aura-2-en":{"limit":{"context":128000,"output":16384}},"@cf/deepgram/aura-2-es":{"limit":{"context":128000,"output":16384}},"@cf/deepgram/nova-3":{"limit":{"context":128000,"output":16384}},"@cf/deepseek-ai/deepseek-r1-distill-qwen-32b":{"limit":{"context":128000,"output":16384}},"@cf/facebook/bart-large-cnn":{"limit":{"context":128000,"output":16384}},"@cf/google/gemma-3-12b-it":{"limit":{"context":128000,"output":16384}},"@cf/huggingface/distilbert-sst-2-int8":{"limit":{"context":128000,"output":16384}},"@cf/ibm-granite/granite-4.0-h-micro":{"limit":{"context":128000,"output":16384}},"@cf/meta/llama-2-7b-chat-fp16":{"limit":{"context":128000,"output":16384}},"@cf/meta/llama-3-8b-instruct":{"limit":{"context":128000,"output":16384}},"@cf/meta/llama-3-8b-instruct-awq":{"limit":{"context":128000,"output":16384}},"@cf/meta/llama-3.1-8b-instruct":{"limit":{"context":128000,"output":16384}},"@cf/meta/llama-3.1-8b-instruct-awq":{"limit":{"context":128000,"output":16384}},"@cf/meta/llama-3.1-8b-instruct-fp8":{"limit":{"context":128000,"output":16384}},"@cf/meta/llama-3.2-11b-vision-instruct":{"limit":{"context":128000,"output":16384}},"@cf/meta/llama-3.2-1b-instruct":{"limit":{"context":128000,"output":16384}},"@cf/meta/llama-3.2-3b-instruct":{"limit":{"context":128000,"output":16384}},"@cf/meta/llama-3.3-70b-instruct-fp8-fast":{"limit":{"context":128000,"output":16384}},"@cf/meta/llama-4-scout-17b-16e-instruct":{"limit":{"context":128000,"output":16384}},"@cf/meta/llama-guard-3-8b":{"limit":{"context":128000,"output":16384}},"@cf/meta/m2m100-1.2b":{"limit":{"context":128000,"output":16384}},"@cf/mistral/mistral-7b-instruct-v0.1":{"limit":{"context":128000,"output":16384}},"@cf/mistralai/mistral-small-3.1-24b-instruct":{"limit":{"context":128000,"output":16384}},"@cf/myshell-ai/melotts":{"limit":{"context":128000,"output":16384}},"@cf/openai/gpt-oss-120b":{"limit":{"context":128000,"output":16384}},"@cf/openai/gpt-oss-20b":{"limit":{"context":128000,"output":16384}},"@cf/pfnet/plamo-embedding-1b":{"limit":{"context":128000,"output":16384}},"@cf/pipecat-ai/smart-turn-v2":{"limit":{"context":128000,"output":16384}},"@cf/qwen/qwen2.5-coder-32b-instruct":{"limit":{"context":128000,"output":16384}},"@cf/qwen/qwen3-30b-a3b-fp8":{"limit":{"context":128000,"output":16384}},"@cf/qwen/qwen3-embedding-0.6b":{"limit":{"context":128000,"output":16384}},"@cf/qwen/qwq-32b":{"limit":{"context":128000,"output":16384}}}},"cohere":{"id":"cohere","npm":"@ai-sdk/cohere","api":null,"env":["COHERE_API_KEY"],"models":{"c4ai-aya-expanse-32b":{"limit":{"context":128000,"output":4000}},"c4ai-aya-expanse-8b":{"limit":{"context":8000,"output":4000}},"c4ai-aya-vision-32b":{"limit":{"context":16000,"output":4000}},"c4ai-aya-vision-8b":{"limit":{"context":16000,"output":4000}},"command-a-03-2025":{"limit":{"context":256000,"output":8000}},"command-a-reasoning-08-2025":{"limit":{"context":256000,"output":32000}},"command-a-translate-08-2025":{"limit":{"context":8000,"output":8000}},"command-a-vision-07-2025":{"limit":{"context":128000,"output":8000}},"command-r-08-2024":{"limit":{"context":128000,"output":4000}},"command-r-plus-08-2024":{"limit":{"context":128000,"output":4000}},"command-r7b-12-2024":{"limit":{"context":128000,"output":4000}},"command-r7b-arabic-02-2025":{"limit":{"context":128000,"output":4000}}}},"cortecs":{"id":"cortecs","npm":"@ai-sdk/openai-compatible","api":"https://api.cortecs.ai/v1","env":["CORTECS_API_KEY"],"models":{"claude-4-5-sonnet":{"limit":{"context":200000,"output":200000}},"claude-sonnet-4":{"limit":{"context":200000,"output":64000}},"deepseek-v3-0324":{"limit":{"context":128000,"output":128000}},"devstral-2512":{"limit":{"context":262000,"output":262000}},"devstral-small-2512":{"limit":{"context":262000,"output":262000}},"gemini-2.5-pro":{"limit":{"context":1048576,"output":65535}},"glm-4p5":{"limit":{"context":131072,"output":131072}},"glm-4p5-air":{"limit":{"context":131072,"output":131072}},"glm-4p7":{"limit":{"context":198000,"output":198000}},"gpt-4.1":{"limit":{"context":1047576,"output":32768}},"gpt-oss-120b":{"limit":{"context":128000,"output":128000}},"intellect-3":{"limit":{"context":128000,"output":128000}},"kimi-k2-instruct":{"limit":{"context":131000,"output":131000}},"kimi-k2-thinking":{"limit":{"context":262000,"output":262000}},"llama-3.1-405b-instruct":{"limit":{"context":128000,"output":128000}},"minimax-m2":{"limit":{"context":400000,"output":400000}},"minimax-m2p1":{"limit":{"context":196000,"output":196000}},"nova-pro-v1":{"limit":{"context":300000,"output":5000}},"qwen3-32b":{"limit":{"context":16384,"output":16384}},"qwen3-coder-480b-a35b-instruct":{"limit":{"context":262000,"output":262000}},"qwen3-next-80b-a3b-thinking":{"limit":{"context":128000,"output":128000}}}},"deepinfra":{"id":"deepinfra","npm":"@ai-sdk/deepinfra","api":null,"env":["DEEPINFRA_API_KEY"],"models":{"MiniMaxAI/MiniMax-M2":{"limit":{"context":262144,"output":32768}},"MiniMaxAI/MiniMax-M2.1":{"limit":{"context":196608,"output":196608}},"Qwen/Qwen3-Coder-480B-A35B-Instruct":{"limit":{"context":262144,"output":66536}},"Qwen/Qwen3-Coder-480B-A35B-Instruct-Turbo":{"limit":{"context":262144,"output":66536}},"anthropic/claude-3-7-sonnet-latest":{"limit":{"context":200000,"output":64000}},"anthropic/claude-4-opus":{"limit":{"context":200000,"output":32000}},"deepseek-ai/DeepSeek-R1-0528":{"limit":{"context":163840,"output":64000}},"deepseek-ai/DeepSeek-V3.2":{"limit":{"context":163840,"output":64000}},"moonshotai/Kimi-K2-Instruct":{"limit":{"context":131072,"output":32768}},"moonshotai/Kimi-K2-Thinking":{"limit":{"context":131072,"output":32768}},"moonshotai/Kimi-K2.5":{"limit":{"context":262144,"output":32768}},"openai/gpt-oss-120b":{"limit":{"context":131072,"output":16384}},"openai/gpt-oss-20b":{"limit":{"context":131072,"output":16384}},"zai-org/GLM-4.5":{"limit":{"context":131072,"output":98304}},"zai-org/GLM-4.7":{"limit":{"context":202752,"output":16384}},"zai-org/GLM-4.7-Flash":{"limit":{"context":202752,"output":16384}}}},"deepseek":{"id":"deepseek","npm":"@ai-sdk/openai-compatible","api":"https://api.deepseek.com","env":["DEEPSEEK_API_KEY"],"models":{"deepseek-chat":{"limit":{"context":128000,"output":8192}},"deepseek-reasoner":{"limit":{"context":128000,"output":128000}}}},"fastrouter":{"id":"fastrouter","npm":"@ai-sdk/openai-compatible","api":"https://go.fastrouter.ai/api/v1","env":["FASTROUTER_API_KEY"],"models":{"anthropic/claude-opus-4.1":{"limit":{"context":200000,"output":32000}},"anthropic/claude-sonnet-4":{"limit":{"context":200000,"output":64000}},"deepseek-ai/deepseek-r1-distill-llama-70b":{"limit":{"context":131072,"output":131072}},"google/gemini-2.5-flash":{"limit":{"context":1048576,"output":65536}},"google/gemini-2.5-pro":{"limit":{"context":1048576,"output":65536}},"moonshotai/kimi-k2":{"limit":{"context":131072,"output":32768}},"openai/gpt-4.1":{"limit":{"context":1047576,"output":32768}},"openai/gpt-5":{"limit":{"context":400000,"output":128000}},"openai/gpt-5-mini":{"limit":{"context":400000,"output":128000}},"openai/gpt-5-nano":{"limit":{"context":400000,"output":128000}},"openai/gpt-oss-120b":{"limit":{"context":131072,"output":32768}},"openai/gpt-oss-20b":{"limit":{"context":131072,"output":65536}},"qwen/qwen3-coder":{"limit":{"context":262144,"output":66536}},"x-ai/grok-4":{"limit":{"context":256000,"output":64000}}}},"fireworks-ai":{"id":"fireworks-ai","npm":"@ai-sdk/openai-compatible","api":"https://api.fireworks.ai/inference/v1/","env":["FIREWORKS_API_KEY"],"models":{"accounts/fireworks/models/deepseek-r1-0528":{"limit":{"context":160000,"output":16384}},"accounts/fireworks/models/deepseek-v3-0324":{"limit":{"context":160000,"output":16384}},"accounts/fireworks/models/deepseek-v3p1":{"limit":{"context":163840,"output":163840}},"accounts/fireworks/models/deepseek-v3p2":{"limit":{"context":160000,"output":160000}},"accounts/fireworks/models/glm-4p5":{"limit":{"context":131072,"output":131072}},"accounts/fireworks/models/glm-4p5-air":{"limit":{"context":131072,"output":131072}},"accounts/fireworks/models/glm-4p6":{"limit":{"context":198000,"output":198000}},"accounts/fireworks/models/glm-4p7":{"limit":{"context":198000,"output":198000}},"accounts/fireworks/models/glm-5":{"limit":{"context":202752,"output":131072}},"accounts/fireworks/models/gpt-oss-120b":{"limit":{"context":131072,"output":32768}},"accounts/fireworks/models/gpt-oss-20b":{"limit":{"context":131072,"output":32768}},"accounts/fireworks/models/kimi-k2-instruct":{"limit":{"context":128000,"output":16384}},"accounts/fireworks/models/kimi-k2-thinking":{"limit":{"context":256000,"output":256000}},"accounts/fireworks/models/kimi-k2p5":{"limit":{"context":256000,"output":256000}},"accounts/fireworks/models/minimax-m2":{"limit":{"context":192000,"output":192000}},"accounts/fireworks/models/minimax-m2p1":{"limit":{"context":200000,"output":200000}},"accounts/fireworks/models/minimax-m2p5":{"limit":{"context":196608,"output":196608}},"accounts/fireworks/models/qwen3-235b-a22b":{"limit":{"context":128000,"output":16384}},"accounts/fireworks/models/qwen3-coder-480b-a35b-instruct":{"limit":{"context":256000,"output":32768}}}},"firmware":{"id":"firmware","npm":"@ai-sdk/openai-compatible","api":"https://app.firmware.ai/api/v1","env":["FIRMWARE_API_KEY"],"models":{"claude-haiku-4-5":{"limit":{"context":200000,"output":64000}},"claude-opus-4-5":{"limit":{"context":200000,"output":64000}},"claude-opus-4-6":{"limit":{"context":200000,"output":128000}},"claude-sonnet-4-5":{"limit":{"context":200000,"output":64000}},"deepseek-r1":{"limit":{"context":128000,"output":65536}},"gemini-2.5-flash":{"limit":{"context":1048576,"output":65536}},"gemini-2.5-pro":{"limit":{"context":1048576,"output":65536}},"gemini-3-flash-preview":{"limit":{"context":1048576,"output":65536}},"gemini-3-pro-preview":{"limit":{"context":1000000,"output":64000}},"gpt-4o":{"limit":{"context":128000,"output":16384}},"gpt-5":{"limit":{"context":400000,"output":128000}},"gpt-5-mini":{"limit":{"context":400000,"output":128000}},"gpt-5-nano":{"limit":{"context":400000,"output":128000}},"gpt-5.2":{"limit":{"context":400000,"output":128000}},"gpt-oss-120b":{"limit":{"context":131072,"output":32768}},"gpt-oss-20b":{"limit":{"context":131072,"output":32768}},"kimi-k2-thinking":{"limit":{"context":262000,"output":128000}},"kimi-k2.5":{"limit":{"context":256000,"output":128000}},"zai-glm-4.7":{"limit":{"context":131072,"output":40000}},"zai-glm-4.7-flash":{"limit":{"context":131072,"output":40000}}}},"friendli":{"id":"friendli","npm":"@ai-sdk/openai-compatible","api":"https://api.friendli.ai/serverless/v1","env":["FRIENDLI_TOKEN"],"models":{"LGAI-EXAONE/EXAONE-4.0.1-32B":{"limit":{"context":131072,"output":131072}},"LGAI-EXAONE/K-EXAONE-236B-A23B":{"limit":{"context":262144,"output":262144}},"MiniMaxAI/MiniMax-M2.1":{"limit":{"context":196608,"output":196608}},"Qwen/Qwen3-235B-A22B-Instruct-2507":{"limit":{"context":262144,"output":262144}},"meta-llama/Llama-3.1-8B-Instruct":{"limit":{"context":131072,"output":8000}},"meta-llama/Llama-3.3-70B-Instruct":{"limit":{"context":131072,"output":131072}},"zai-org/GLM-4.7":{"limit":{"context":202752,"output":202752}},"zai-org/GLM-5":{"limit":{"context":202752,"output":202752}}}},"github-copilot":{"id":"github-copilot","npm":"@ai-sdk/openai-compatible","api":"https://api.githubcopilot.com","env":["GITHUB_TOKEN"],"models":{"claude-haiku-4.5":{"limit":{"context":128000,"output":32000}},"claude-opus-4.5":{"limit":{"context":128000,"output":32000}},"claude-opus-4.6":{"limit":{"context":128000,"output":64000}},"claude-opus-41":{"limit":{"context":80000,"output":16000}},"claude-sonnet-4":{"limit":{"context":128000,"output":16000}},"claude-sonnet-4.5":{"limit":{"context":128000,"output":32000}},"gemini-2.5-pro":{"limit":{"context":128000,"output":64000}},"gemini-3-flash-preview":{"limit":{"context":128000,"output":64000}},"gemini-3-pro-preview":{"limit":{"context":128000,"output":64000}},"gpt-4.1":{"limit":{"context":64000,"output":16384}},"gpt-4o":{"limit":{"context":64000,"output":16384}},"gpt-5":{"limit":{"context":128000,"output":128000}},"gpt-5-mini":{"limit":{"context":128000,"output":64000}},"gpt-5.1":{"limit":{"context":128000,"output":64000}},"gpt-5.1-codex":{"limit":{"context":128000,"output":128000}},"gpt-5.1-codex-max":{"limit":{"context":128000,"output":128000}},"gpt-5.1-codex-mini":{"limit":{"context":128000,"output":128000}},"gpt-5.2":{"limit":{"context":128000,"output":64000}},"gpt-5.2-codex":{"limit":{"context":272000,"output":128000}},"gpt-5.3-codex":{"limit":{"context":400000,"output":128000}},"grok-code-fast-1":{"limit":{"context":128000,"output":64000}}}},"github-models":{"id":"github-models","npm":"@ai-sdk/openai-compatible","api":"https://models.github.ai/inference","env":["GITHUB_TOKEN"],"models":{"ai21-labs/ai21-jamba-1.5-large":{"limit":{"context":256000,"output":4096}},"ai21-labs/ai21-jamba-1.5-mini":{"limit":{"context":256000,"output":4096}},"cohere/cohere-command-a":{"limit":{"context":128000,"output":4096}},"cohere/cohere-command-r":{"limit":{"context":128000,"output":4096}},"cohere/cohere-command-r-08-2024":{"limit":{"context":128000,"output":4096}},"cohere/cohere-command-r-plus":{"limit":{"context":128000,"output":4096}},"cohere/cohere-command-r-plus-08-2024":{"limit":{"context":128000,"output":4096}},"core42/jais-30b-chat":{"limit":{"context":8192,"output":2048}},"deepseek/deepseek-r1":{"limit":{"context":65536,"output":8192}},"deepseek/deepseek-r1-0528":{"limit":{"context":65536,"output":8192}},"deepseek/deepseek-v3-0324":{"limit":{"context":128000,"output":8192}},"meta/llama-3.2-11b-vision-instruct":{"limit":{"context":128000,"output":8192}},"meta/llama-3.2-90b-vision-instruct":{"limit":{"context":128000,"output":8192}},"meta/llama-3.3-70b-instruct":{"limit":{"context":128000,"output":32768}},"meta/llama-4-maverick-17b-128e-instruct-fp8":{"limit":{"context":128000,"output":8192}},"meta/llama-4-scout-17b-16e-instruct":{"limit":{"context":128000,"output":8192}},"meta/meta-llama-3-70b-instruct":{"limit":{"context":8192,"output":2048}},"meta/meta-llama-3-8b-instruct":{"limit":{"context":8192,"output":2048}},"meta/meta-llama-3.1-405b-instruct":{"limit":{"context":128000,"output":32768}},"meta/meta-llama-3.1-70b-instruct":{"limit":{"context":128000,"output":32768}},"meta/meta-llama-3.1-8b-instruct":{"limit":{"context":128000,"output":32768}},"microsoft/mai-ds-r1":{"limit":{"context":65536,"output":8192}},"microsoft/phi-3-medium-128k-instruct":{"limit":{"context":128000,"output":4096}},"microsoft/phi-3-medium-4k-instruct":{"limit":{"context":4096,"output":1024}},"microsoft/phi-3-mini-128k-instruct":{"limit":{"context":128000,"output":4096}},"microsoft/phi-3-mini-4k-instruct":{"limit":{"context":4096,"output":1024}},"microsoft/phi-3-small-128k-instruct":{"limit":{"context":128000,"output":4096}},"microsoft/phi-3-small-8k-instruct":{"limit":{"context":8192,"output":2048}},"microsoft/phi-3.5-mini-instruct":{"limit":{"context":128000,"output":4096}},"microsoft/phi-3.5-moe-instruct":{"limit":{"context":128000,"output":4096}},"microsoft/phi-3.5-vision-instruct":{"limit":{"context":128000,"output":4096}},"microsoft/phi-4":{"limit":{"context":16000,"output":4096}},"microsoft/phi-4-mini-instruct":{"limit":{"context":128000,"output":4096}},"microsoft/phi-4-mini-reasoning":{"limit":{"context":128000,"output":4096}},"microsoft/phi-4-multimodal-instruct":{"limit":{"context":128000,"output":4096}},"microsoft/phi-4-reasoning":{"limit":{"context":128000,"output":4096}},"mistral-ai/codestral-2501":{"limit":{"context":32000,"output":8192}},"mistral-ai/ministral-3b":{"limit":{"context":128000,"output":8192}},"mistral-ai/mistral-large-2411":{"limit":{"context":128000,"output":32768}},"mistral-ai/mistral-medium-2505":{"limit":{"context":128000,"output":32768}},"mistral-ai/mistral-nemo":{"limit":{"context":128000,"output":8192}},"mistral-ai/mistral-small-2503":{"limit":{"context":128000,"output":32768}},"openai/gpt-4.1":{"limit":{"context":128000,"output":16384}},"openai/gpt-4.1-mini":{"limit":{"context":128000,"output":16384}},"openai/gpt-4.1-nano":{"limit":{"context":128000,"output":16384}},"openai/gpt-4o":{"limit":{"context":128000,"output":16384}},"openai/gpt-4o-mini":{"limit":{"context":128000,"output":16384}},"openai/o1":{"limit":{"context":200000,"output":100000}},"openai/o1-mini":{"limit":{"context":128000,"output":65536}},"openai/o1-preview":{"limit":{"context":128000,"output":32768}},"openai/o3":{"limit":{"context":200000,"output":100000}},"openai/o3-mini":{"limit":{"context":200000,"output":100000}},"openai/o4-mini":{"limit":{"context":200000,"output":100000}},"xai/grok-3":{"limit":{"context":128000,"output":8192}},"xai/grok-3-mini":{"limit":{"context":128000,"output":8192}}}},"gitlab":{"id":"gitlab","npm":"@gitlab/gitlab-ai-provider","api":null,"env":["GITLAB_TOKEN"],"models":{"duo-chat-gpt-5-1":{"limit":{"context":400000,"output":128000}},"duo-chat-gpt-5-2":{"limit":{"context":400000,"output":128000}},"duo-chat-gpt-5-2-codex":{"limit":{"context":400000,"output":128000}},"duo-chat-gpt-5-codex":{"limit":{"context":400000,"output":128000}},"duo-chat-gpt-5-mini":{"limit":{"context":400000,"output":128000}},"duo-chat-haiku-4-5":{"limit":{"context":200000,"output":64000}},"duo-chat-opus-4-5":{"limit":{"context":200000,"output":64000}},"duo-chat-opus-4-6":{"limit":{"context":200000,"output":64000}},"duo-chat-sonnet-4-5":{"limit":{"context":200000,"output":64000}}}},"google":{"id":"google","npm":"@ai-sdk/google","api":null,"env":["GOOGLE_GENERATIVE_AI_API_KEY","GEMINI_API_KEY"],"models":{"gemini-1.5-flash":{"limit":{"context":1000000,"output":8192}},"gemini-1.5-flash-8b":{"limit":{"context":1000000,"output":8192}},"gemini-1.5-pro":{"limit":{"context":1000000,"output":8192}},"gemini-2.0-flash":{"limit":{"context":1048576,"output":8192}},"gemini-2.0-flash-lite":{"limit":{"context":1048576,"output":8192}},"gemini-2.5-flash":{"limit":{"context":1048576,"output":65536}},"gemini-2.5-flash-image":{"limit":{"context":32768,"output":32768}},"gemini-2.5-flash-image-preview":{"limit":{"context":32768,"output":32768}},"gemini-2.5-flash-lite":{"limit":{"context":1048576,"output":65536}},"gemini-2.5-flash-lite-preview-06-17":{"limit":{"context":1048576,"output":65536}},"gemini-2.5-flash-lite-preview-09-2025":{"limit":{"context":1048576,"output":65536}},"gemini-2.5-flash-preview-04-17":{"limit":{"context":1048576,"output":65536}},"gemini-2.5-flash-preview-05-20":{"limit":{"context":1048576,"output":65536}},"gemini-2.5-flash-preview-09-2025":{"limit":{"context":1048576,"output":65536}},"gemini-2.5-flash-preview-tts":{"limit":{"context":8000,"output":16000}},"gemini-2.5-pro":{"limit":{"context":1048576,"output":65536}},"gemini-2.5-pro-preview-05-06":{"limit":{"context":1048576,"output":65536}},"gemini-2.5-pro-preview-06-05":{"limit":{"context":1048576,"output":65536}},"gemini-2.5-pro-preview-tts":{"limit":{"context":8000,"output":16000}},"gemini-3-flash-preview":{"limit":{"context":1048576,"output":65536}},"gemini-3-pro-preview":{"limit":{"context":1000000,"output":64000}},"gemini-embedding-001":{"limit":{"context":2048,"output":3072}},"gemini-flash-latest":{"limit":{"context":1048576,"output":65536}},"gemini-flash-lite-latest":{"limit":{"context":1048576,"output":65536}},"gemini-live-2.5-flash":{"limit":{"context":128000,"output":8000}},"gemini-live-2.5-flash-preview-native-audio":{"limit":{"context":131072,"output":65536}}}},"google-vertex":{"id":"google-vertex","npm":"@ai-sdk/google-vertex","api":null,"env":["GOOGLE_VERTEX_PROJECT","GOOGLE_VERTEX_LOCATION","GOOGLE_APPLICATION_CREDENTIALS"],"models":{"gemini-2.0-flash":{"limit":{"context":1048576,"output":8192}},"gemini-2.0-flash-lite":{"limit":{"context":1048576,"output":8192}},"gemini-2.5-flash":{"limit":{"context":1048576,"output":65536}},"gemini-2.5-flash-lite":{"limit":{"context":1048576,"output":65536}},"gemini-2.5-flash-lite-preview-06-17":{"limit":{"context":65536,"output":65536}},"gemini-2.5-flash-lite-preview-09-2025":{"limit":{"context":1048576,"output":65536}},"gemini-2.5-flash-preview-04-17":{"limit":{"context":1048576,"output":65536}},"gemini-2.5-flash-preview-05-20":{"limit":{"context":1048576,"output":65536}},"gemini-2.5-flash-preview-09-2025":{"limit":{"context":1048576,"output":65536}},"gemini-2.5-pro":{"limit":{"context":1048576,"output":65536}},"gemini-2.5-pro-preview-05-06":{"limit":{"context":1048576,"output":65536}},"gemini-2.5-pro-preview-06-05":{"limit":{"context":1048576,"output":65536}},"gemini-3-flash-preview":{"limit":{"context":1048576,"output":65536}},"gemini-3-pro-preview":{"limit":{"context":1048576,"output":65536}},"gemini-embedding-001":{"limit":{"context":2048,"output":3072}},"gemini-flash-latest":{"limit":{"context":1048576,"output":65536}},"gemini-flash-lite-latest":{"limit":{"context":1048576,"output":65536}},"openai/gpt-oss-120b-maas":{"limit":{"context":131072,"output":32768}},"openai/gpt-oss-20b-maas":{"limit":{"context":131072,"output":32768}},"zai-org/glm-4.7-maas":{"limit":{"context":204800,"output":131072}}}},"google-vertex-anthropic":{"id":"google-vertex-anthropic","npm":"@ai-sdk/google-vertex/anthropic","api":null,"env":["GOOGLE_VERTEX_PROJECT","GOOGLE_VERTEX_LOCATION","GOOGLE_APPLICATION_CREDENTIALS"],"models":{"claude-3-5-haiku@20241022":{"limit":{"context":200000,"output":8192}},"claude-3-5-sonnet@20241022":{"limit":{"context":200000,"output":8192}},"claude-3-7-sonnet@20250219":{"limit":{"context":200000,"output":64000}},"claude-haiku-4-5@20251001":{"limit":{"context":200000,"output":64000}},"claude-opus-4-1@20250805":{"limit":{"context":200000,"output":32000}},"claude-opus-4-5@20251101":{"limit":{"context":200000,"output":64000}},"claude-opus-4-6@default":{"limit":{"context":1000000,"output":128000}},"claude-opus-4@20250514":{"limit":{"context":200000,"output":32000}},"claude-sonnet-4-5@20250929":{"limit":{"context":200000,"output":64000}},"claude-sonnet-4@20250514":{"limit":{"context":200000,"output":64000}}}},"groq":{"id":"groq","npm":"@ai-sdk/groq","api":null,"env":["GROQ_API_KEY"],"models":{"deepseek-r1-distill-llama-70b":{"limit":{"context":131072,"output":8192}},"gemma2-9b-it":{"limit":{"context":8192,"output":8192}},"llama-3.1-8b-instant":{"limit":{"context":131072,"output":131072}},"llama-3.3-70b-versatile":{"limit":{"context":131072,"output":32768}},"llama-guard-3-8b":{"limit":{"context":8192,"output":8192}},"llama3-70b-8192":{"limit":{"context":8192,"output":8192}},"llama3-8b-8192":{"limit":{"context":8192,"output":8192}},"meta-llama/llama-4-maverick-17b-128e-instruct":{"limit":{"context":131072,"output":8192}},"meta-llama/llama-4-scout-17b-16e-instruct":{"limit":{"context":131072,"output":8192}},"meta-llama/llama-guard-4-12b":{"limit":{"context":131072,"output":1024}},"mistral-saba-24b":{"limit":{"context":32768,"output":32768}},"moonshotai/kimi-k2-instruct":{"limit":{"context":131072,"output":16384}},"moonshotai/kimi-k2-instruct-0905":{"limit":{"context":262144,"output":16384}},"openai/gpt-oss-120b":{"limit":{"context":131072,"output":65536}},"openai/gpt-oss-20b":{"limit":{"context":131072,"output":65536}},"qwen-qwq-32b":{"limit":{"context":131072,"output":16384}},"qwen/qwen3-32b":{"limit":{"context":131072,"output":16384}}}},"helicone":{"id":"helicone","npm":"@ai-sdk/openai-compatible","api":"https://ai-gateway.helicone.ai/v1","env":["HELICONE_API_KEY"],"models":{"chatgpt-4o-latest":{"limit":{"context":128000,"output":16384}},"claude-3-haiku-20240307":{"limit":{"context":200000,"output":4096}},"claude-3.5-haiku":{"limit":{"context":200000,"output":8192}},"claude-3.5-sonnet-v2":{"limit":{"context":200000,"output":8192}},"claude-3.7-sonnet":{"limit":{"context":200000,"output":64000}},"claude-4.5-haiku":{"limit":{"context":200000,"output":8192}},"claude-4.5-opus":{"limit":{"context":200000,"output":64000}},"claude-4.5-sonnet":{"limit":{"context":200000,"output":64000}},"claude-haiku-4-5-20251001":{"limit":{"context":200000,"output":8192}},"claude-opus-4":{"limit":{"context":200000,"output":32000}},"claude-opus-4-1":{"limit":{"context":200000,"output":32000}},"claude-opus-4-1-20250805":{"limit":{"context":200000,"output":32000}},"claude-sonnet-4":{"limit":{"context":200000,"output":64000}},"claude-sonnet-4-5-20250929":{"limit":{"context":200000,"output":64000}},"codex-mini-latest":{"limit":{"context":200000,"output":100000}},"deepseek-r1-distill-llama-70b":{"limit":{"context":128000,"output":4096}},"deepseek-reasoner":{"limit":{"context":128000,"output":64000}},"deepseek-tng-r1t2-chimera":{"limit":{"context":130000,"output":163840}},"deepseek-v3":{"limit":{"context":128000,"output":8192}},"deepseek-v3.1-terminus":{"limit":{"context":128000,"output":16384}},"deepseek-v3.2":{"limit":{"context":163840,"output":65536}},"ernie-4.5-21b-a3b-thinking":{"limit":{"context":128000,"output":8000}},"gemini-2.5-flash":{"limit":{"context":1048576,"output":65535}},"gemini-2.5-flash-lite":{"limit":{"context":1048576,"output":65535}},"gemini-2.5-pro":{"limit":{"context":1048576,"output":65536}},"gemini-3-pro-preview":{"limit":{"context":1048576,"output":65536}},"gemma-3-12b-it":{"limit":{"context":131072,"output":8192}},"gemma2-9b-it":{"limit":{"context":8192,"output":8192}},"glm-4.6":{"limit":{"context":204800,"output":131072}},"gpt-4.1":{"limit":{"context":1047576,"output":32768}},"gpt-4.1-mini":{"limit":{"context":1047576,"output":32768}},"gpt-4.1-mini-2025-04-14":{"limit":{"context":1047576,"output":32768}},"gpt-4.1-nano":{"limit":{"context":1047576,"output":32768}},"gpt-4o":{"limit":{"context":128000,"output":16384}},"gpt-4o-mini":{"limit":{"context":128000,"output":16384}},"gpt-5":{"limit":{"context":400000,"output":128000}},"gpt-5-chat-latest":{"limit":{"context":128000,"output":16384}},"gpt-5-codex":{"limit":{"context":400000,"output":128000}},"gpt-5-mini":{"limit":{"context":400000,"output":128000}},"gpt-5-nano":{"limit":{"context":400000,"output":128000}},"gpt-5-pro":{"limit":{"context":128000,"output":32768}},"gpt-5.1":{"limit":{"context":400000,"output":128000}},"gpt-5.1-chat-latest":{"limit":{"context":128000,"output":16384}},"gpt-5.1-codex":{"limit":{"context":400000,"output":128000}},"gpt-5.1-codex-mini":{"limit":{"context":400000,"output":128000}},"gpt-oss-120b":{"limit":{"context":131072,"output":131072}},"gpt-oss-20b":{"limit":{"context":131072,"output":131072}},"grok-3":{"limit":{"context":131072,"output":131072}},"grok-3-mini":{"limit":{"context":131072,"output":131072}},"grok-4":{"limit":{"context":256000,"output":256000}},"grok-4-1-fast-non-reasoning":{"limit":{"context":2000000,"output":30000}},"grok-4-1-fast-reasoning":{"limit":{"context":2000000,"output":2000000}},"grok-4-fast-non-reasoning":{"limit":{"context":2000000,"output":2000000}},"grok-4-fast-reasoning":{"limit":{"context":2000000,"output":2000000}},"grok-code-fast-1":{"limit":{"context":256000,"output":10000}},"hermes-2-pro-llama-3-8b":{"limit":{"context":131072,"output":131072}},"kimi-k2-0711":{"limit":{"context":131072,"output":16384}},"kimi-k2-0905":{"limit":{"context":262144,"output":16384}},"kimi-k2-thinking":{"limit":{"context":256000,"output":262144}},"llama-3.1-8b-instant":{"limit":{"context":131072,"output":32678}},"llama-3.1-8b-instruct":{"limit":{"context":16384,"output":16384}},"llama-3.1-8b-instruct-turbo":{"limit":{"context":128000,"output":128000}},"llama-3.3-70b-instruct":{"limit":{"context":128000,"output":16400}},"llama-3.3-70b-versatile":{"limit":{"context":131072,"output":32678}},"llama-4-maverick":{"limit":{"context":131072,"output":8192}},"llama-4-scout":{"limit":{"context":131072,"output":8192}},"llama-guard-4":{"limit":{"context":131072,"output":1024}},"llama-prompt-guard-2-22m":{"limit":{"context":512,"output":2}},"llama-prompt-guard-2-86m":{"limit":{"context":512,"output":2}},"mistral-large-2411":{"limit":{"context":128000,"output":32768}},"mistral-nemo":{"limit":{"context":128000,"output":16400}},"mistral-small":{"limit":{"context":128000,"output":128000}},"o1":{"limit":{"context":200000,"output":100000}},"o1-mini":{"limit":{"context":128000,"output":65536}},"o3":{"limit":{"context":200000,"output":100000}},"o3-mini":{"limit":{"context":200000,"output":100000}},"o3-pro":{"limit":{"context":200000,"output":100000}},"o4-mini":{"limit":{"context":200000,"output":100000}},"qwen2.5-coder-7b-fast":{"limit":{"context":32000,"output":8192}},"qwen3-235b-a22b-thinking":{"limit":{"context":262144,"output":81920}},"qwen3-30b-a3b":{"limit":{"context":41000,"output":41000}},"qwen3-32b":{"limit":{"context":131072,"output":40960}},"qwen3-coder":{"limit":{"context":262144,"output":16384}},"qwen3-coder-30b-a3b-instruct":{"limit":{"context":262144,"output":262144}},"qwen3-next-80b-a3b-instruct":{"limit":{"context":262000,"output":16384}},"qwen3-vl-235b-a22b-instruct":{"limit":{"context":256000,"output":16384}},"sonar":{"limit":{"context":127000,"output":4096}},"sonar-deep-research":{"limit":{"context":127000,"output":4096}},"sonar-pro":{"limit":{"context":200000,"output":4096}},"sonar-reasoning":{"limit":{"context":127000,"output":4096}},"sonar-reasoning-pro":{"limit":{"context":127000,"output":4096}}}},"huggingface":{"id":"huggingface","npm":"@ai-sdk/openai-compatible","api":"https://router.huggingface.co/v1","env":["HF_TOKEN"],"models":{"MiniMaxAI/MiniMax-M2.1":{"limit":{"context":204800,"output":131072}},"Qwen/Qwen3-235B-A22B-Thinking-2507":{"limit":{"context":262144,"output":131072}},"Qwen/Qwen3-Coder-480B-A35B-Instruct":{"limit":{"context":262144,"output":66536}},"Qwen/Qwen3-Embedding-4B":{"limit":{"context":32000,"output":2048}},"Qwen/Qwen3-Embedding-8B":{"limit":{"context":32000,"output":4096}},"Qwen/Qwen3-Next-80B-A3B-Instruct":{"limit":{"context":262144,"output":66536}},"Qwen/Qwen3-Next-80B-A3B-Thinking":{"limit":{"context":262144,"output":131072}},"XiaomiMiMo/MiMo-V2-Flash":{"limit":{"context":262144,"output":4096}},"deepseek-ai/DeepSeek-R1-0528":{"limit":{"context":163840,"output":163840}},"deepseek-ai/DeepSeek-V3.2":{"limit":{"context":163840,"output":65536}},"moonshotai/Kimi-K2-Instruct":{"limit":{"context":131072,"output":16384}},"moonshotai/Kimi-K2-Instruct-0905":{"limit":{"context":262144,"output":16384}},"moonshotai/Kimi-K2-Thinking":{"limit":{"context":262144,"output":262144}},"moonshotai/Kimi-K2.5":{"limit":{"context":262144,"output":262144}},"zai-org/GLM-4.7":{"limit":{"context":204800,"output":131072}},"zai-org/GLM-4.7-Flash":{"limit":{"context":200000,"output":128000}},"zai-org/GLM-5":{"limit":{"context":202752,"output":131072}}}},"iflowcn":{"id":"iflowcn","npm":"@ai-sdk/openai-compatible","api":"https://apis.iflow.cn/v1","env":["IFLOW_API_KEY"],"models":{"deepseek-r1":{"limit":{"context":128000,"output":32000}},"deepseek-v3":{"limit":{"context":128000,"output":32000}},"deepseek-v3.2":{"limit":{"context":128000,"output":64000}},"glm-4.6":{"limit":{"context":200000,"output":128000}},"kimi-k2":{"limit":{"context":128000,"output":64000}},"kimi-k2-0905":{"limit":{"context":256000,"output":64000}},"qwen3-235b":{"limit":{"context":128000,"output":32000}},"qwen3-235b-a22b-instruct":{"limit":{"context":256000,"output":64000}},"qwen3-235b-a22b-thinking-2507":{"limit":{"context":256000,"output":64000}},"qwen3-32b":{"limit":{"context":128000,"output":32000}},"qwen3-coder-plus":{"limit":{"context":256000,"output":64000}},"qwen3-max":{"limit":{"context":256000,"output":32000}},"qwen3-max-preview":{"limit":{"context":256000,"output":32000}},"qwen3-vl-plus":{"limit":{"context":256000,"output":32000}}}},"inception":{"id":"inception","npm":"@ai-sdk/openai-compatible","api":"https://api.inceptionlabs.ai/v1/","env":["INCEPTION_API_KEY"],"models":{"mercury":{"limit":{"context":128000,"output":16384}},"mercury-coder":{"limit":{"context":128000,"output":16384}}}},"inference":{"id":"inference","npm":"@ai-sdk/openai-compatible","api":"https://inference.net/v1","env":["INFERENCE_API_KEY"],"models":{"google/gemma-3":{"limit":{"context":125000,"output":4096}},"meta/llama-3.1-8b-instruct":{"limit":{"context":16000,"output":4096}},"meta/llama-3.2-11b-vision-instruct":{"limit":{"context":16000,"output":4096}},"meta/llama-3.2-1b-instruct":{"limit":{"context":16000,"output":4096}},"meta/llama-3.2-3b-instruct":{"limit":{"context":16000,"output":4096}},"mistral/mistral-nemo-12b-instruct":{"limit":{"context":16000,"output":4096}},"osmosis/osmosis-structure-0.6b":{"limit":{"context":4000,"output":2048}},"qwen/qwen-2.5-7b-vision-instruct":{"limit":{"context":125000,"output":4096}},"qwen/qwen3-embedding-4b":{"limit":{"context":32000,"output":2048}}}},"io-net":{"id":"io-net","npm":"@ai-sdk/openai-compatible","api":"https://api.intelligence.io.solutions/api/v1","env":["IOINTELLIGENCE_API_KEY"],"models":{"Intel/Qwen3-Coder-480B-A35B-Instruct-int4-mixed-ar":{"limit":{"context":106000,"output":4096}},"Qwen/Qwen2.5-VL-32B-Instruct":{"limit":{"context":32000,"output":4096}},"Qwen/Qwen3-235B-A22B-Thinking-2507":{"limit":{"context":262144,"output":4096}},"Qwen/Qwen3-Next-80B-A3B-Instruct":{"limit":{"context":262144,"output":4096}},"deepseek-ai/DeepSeek-R1-0528":{"limit":{"context":128000,"output":4096}},"meta-llama/Llama-3.2-90B-Vision-Instruct":{"limit":{"context":16000,"output":4096}},"meta-llama/Llama-3.3-70B-Instruct":{"limit":{"context":128000,"output":4096}},"meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8":{"limit":{"context":430000,"output":4096}},"mistralai/Devstral-Small-2505":{"limit":{"context":128000,"output":4096}},"mistralai/Magistral-Small-2506":{"limit":{"context":128000,"output":4096}},"mistralai/Mistral-Large-Instruct-2411":{"limit":{"context":128000,"output":4096}},"mistralai/Mistral-Nemo-Instruct-2407":{"limit":{"context":128000,"output":4096}},"moonshotai/Kimi-K2-Instruct-0905":{"limit":{"context":32768,"output":4096}},"moonshotai/Kimi-K2-Thinking":{"limit":{"context":32768,"output":4096}},"openai/gpt-oss-120b":{"limit":{"context":131072,"output":4096}},"openai/gpt-oss-20b":{"limit":{"context":64000,"output":4096}},"zai-org/GLM-4.6":{"limit":{"context":200000,"output":4096}}}},"jiekou":{"id":"jiekou","npm":"@ai-sdk/openai-compatible","api":"https://api.jiekou.ai/openai","env":["JIEKOU_API_KEY"],"models":{"baidu/ernie-4.5-300b-a47b-paddle":{"limit":{"context":123000,"output":12000}},"baidu/ernie-4.5-vl-424b-a47b":{"limit":{"context":123000,"output":16000}},"claude-haiku-4-5-20251001":{"limit":{"context":20000,"output":64000}},"claude-opus-4-1-20250805":{"limit":{"context":200000,"output":32000}},"claude-opus-4-20250514":{"limit":{"context":200000,"output":32000}},"claude-opus-4-5-20251101":{"limit":{"context":200000,"output":65536}},"claude-opus-4-6":{"limit":{"context":1000000,"output":128000}},"claude-sonnet-4-20250514":{"limit":{"context":200000,"output":64000}},"claude-sonnet-4-5-20250929":{"limit":{"context":200000,"output":64000}},"deepseek/deepseek-r1-0528":{"limit":{"context":163840,"output":32768}},"deepseek/deepseek-v3-0324":{"limit":{"context":163840,"output":163840}},"deepseek/deepseek-v3.1":{"limit":{"context":163840,"output":32768}},"gemini-2.5-flash":{"limit":{"context":1048576,"output":65535}},"gemini-2.5-flash-lite":{"limit":{"context":1048576,"output":65535}},"gemini-2.5-flash-lite-preview-06-17":{"limit":{"context":1048576,"output":65535}},"gemini-2.5-flash-lite-preview-09-2025":{"limit":{"context":1048576,"output":65536}},"gemini-2.5-flash-preview-05-20":{"limit":{"context":1048576,"output":200000}},"gemini-2.5-pro":{"limit":{"context":1048576,"output":65535}},"gemini-2.5-pro-preview-06-05":{"limit":{"context":1048576,"output":200000}},"gemini-3-flash-preview":{"limit":{"context":1048576,"output":65536}},"gemini-3-pro-preview":{"limit":{"context":1048576,"output":65536}},"gpt-5-chat-latest":{"limit":{"context":400000,"output":128000}},"gpt-5-codex":{"limit":{"context":400000,"output":128000}},"gpt-5-mini":{"limit":{"context":400000,"output":128000}},"gpt-5-nano":{"limit":{"context":400000,"output":128000}},"gpt-5-pro":{"limit":{"context":400000,"output":272000}},"gpt-5.1":{"limit":{"context":400000,"output":128000}},"gpt-5.1-codex":{"limit":{"context":400000,"output":128000}},"gpt-5.1-codex-max":{"limit":{"context":400000,"output":128000}},"gpt-5.1-codex-mini":{"limit":{"context":400000,"output":128000}},"gpt-5.2":{"limit":{"context":400000,"output":128000}},"gpt-5.2-codex":{"limit":{"context":400000,"output":128000}},"gpt-5.2-pro":{"limit":{"context":400000,"output":128000}},"grok-4-0709":{"limit":{"context":256000,"output":8192}},"grok-4-1-fast-non-reasoning":{"limit":{"context":2000000,"output":2000000}},"grok-4-1-fast-reasoning":{"limit":{"context":2000000,"output":2000000}},"grok-4-fast-non-reasoning":{"limit":{"context":2000000,"output":2000000}},"grok-4-fast-reasoning":{"limit":{"context":2000000,"output":2000000}},"grok-code-fast-1":{"limit":{"context":256000,"output":256000}},"minimax/minimax-m2.1":{"limit":{"context":204800,"output":131072}},"minimaxai/minimax-m1-80k":{"limit":{"context":1000000,"output":40000}},"moonshotai/kimi-k2-0905":{"limit":{"context":262144,"output":262144}},"moonshotai/kimi-k2-instruct":{"limit":{"context":131072,"output":131072}},"moonshotai/kimi-k2.5":{"limit":{"context":262144,"output":262144}},"o3":{"limit":{"context":131072,"output":131072}},"o3-mini":{"limit":{"context":131072,"output":131072}},"o4-mini":{"limit":{"context":200000,"output":100000}},"qwen/qwen3-235b-a22b-fp8":{"limit":{"context":40960,"output":20000}},"qwen/qwen3-235b-a22b-instruct-2507":{"limit":{"context":131072,"output":16384}},"qwen/qwen3-235b-a22b-thinking-2507":{"limit":{"context":131072,"output":131072}},"qwen/qwen3-30b-a3b-fp8":{"limit":{"context":40960,"output":20000}},"qwen/qwen3-32b-fp8":{"limit":{"context":40960,"output":20000}},"qwen/qwen3-coder-480b-a35b-instruct":{"limit":{"context":262144,"output":65536}},"qwen/qwen3-coder-next":{"limit":{"context":262144,"output":65536}},"qwen/qwen3-next-80b-a3b-instruct":{"limit":{"context":65536,"output":65536}},"qwen/qwen3-next-80b-a3b-thinking":{"limit":{"context":65536,"output":65536}},"xiaomimimo/mimo-v2-flash":{"limit":{"context":262144,"output":131072}},"zai-org/glm-4.5":{"limit":{"context":131072,"output":98304}},"zai-org/glm-4.5v":{"limit":{"context":65536,"output":16384}},"zai-org/glm-4.7":{"limit":{"context":204800,"output":131072}},"zai-org/glm-4.7-flash":{"limit":{"context":200000,"output":128000}}}},"kimi-for-coding":{"id":"kimi-for-coding","npm":"@ai-sdk/anthropic","api":"https://api.kimi.com/coding/v1","env":["KIMI_API_KEY"],"models":{"k2p5":{"limit":{"context":262144,"output":32768}},"kimi-k2-thinking":{"limit":{"context":262144,"output":32768}}}},"kuae-cloud-coding-plan":{"id":"kuae-cloud-coding-plan","npm":"@ai-sdk/openai-compatible","api":"https://coding-plan-endpoint.kuaecloud.net/v1","env":["KUAE_API_KEY"],"models":{"GLM-4.7":{"limit":{"context":204800,"output":131072}}}},"llama":{"id":"llama","npm":"@ai-sdk/openai-compatible","api":"https://api.llama.com/compat/v1/","env":["LLAMA_API_KEY"],"models":{"cerebras-llama-4-maverick-17b-128e-instruct":{"limit":{"context":128000,"output":4096}},"cerebras-llama-4-scout-17b-16e-instruct":{"limit":{"context":128000,"output":4096}},"groq-llama-4-maverick-17b-128e-instruct":{"limit":{"context":128000,"output":4096}},"llama-3.3-70b-instruct":{"limit":{"context":128000,"output":4096}},"llama-3.3-8b-instruct":{"limit":{"context":128000,"output":4096}},"llama-4-maverick-17b-128e-instruct-fp8":{"limit":{"context":128000,"output":4096}},"llama-4-scout-17b-16e-instruct-fp8":{"limit":{"context":128000,"output":4096}}}},"lmstudio":{"id":"lmstudio","npm":"@ai-sdk/openai-compatible","api":"http://127.0.0.1:1234/v1","env":["LMSTUDIO_API_KEY"],"models":{"openai/gpt-oss-20b":{"limit":{"context":131072,"output":32768}},"qwen/qwen3-30b-a3b-2507":{"limit":{"context":262144,"output":16384}},"qwen/qwen3-coder-30b":{"limit":{"context":262144,"output":65536}}}},"lucidquery":{"id":"lucidquery","npm":"@ai-sdk/openai-compatible","api":"https://lucidquery.com/api/v1","env":["LUCIDQUERY_API_KEY"],"models":{"lucidnova-rf1-100b":{"limit":{"context":120000,"output":8000}},"lucidquery-nexus-coder":{"limit":{"context":250000,"output":60000}}}},"minimax":{"id":"minimax","npm":"@ai-sdk/anthropic","api":"https://api.minimax.io/anthropic/v1","env":["MINIMAX_API_KEY"],"models":{"MiniMax-M2":{"limit":{"context":196608,"output":128000}},"MiniMax-M2.1":{"limit":{"context":204800,"output":131072}},"MiniMax-M2.5":{"limit":{"context":204800,"output":131072}}}},"minimax-cn":{"id":"minimax-cn","npm":"@ai-sdk/anthropic","api":"https://api.minimaxi.com/anthropic/v1","env":["MINIMAX_API_KEY"],"models":{"MiniMax-M2":{"limit":{"context":196608,"output":128000}},"MiniMax-M2.1":{"limit":{"context":204800,"output":131072}},"MiniMax-M2.5":{"limit":{"context":204800,"output":131072}}}},"minimax-cn-coding-plan":{"id":"minimax-cn-coding-plan","npm":"@ai-sdk/anthropic","api":"https://api.minimaxi.com/anthropic/v1","env":["MINIMAX_API_KEY"],"models":{"MiniMax-M2":{"limit":{"context":196608,"output":128000}},"MiniMax-M2.1":{"limit":{"context":204800,"output":131072}},"MiniMax-M2.5":{"limit":{"context":204800,"output":131072}}}},"minimax-coding-plan":{"id":"minimax-coding-plan","npm":"@ai-sdk/anthropic","api":"https://api.minimax.io/anthropic/v1","env":["MINIMAX_API_KEY"],"models":{"MiniMax-M2":{"limit":{"context":196608,"output":128000}},"MiniMax-M2.1":{"limit":{"context":204800,"output":131072}},"MiniMax-M2.5":{"limit":{"context":204800,"output":131072}}}},"mistral":{"id":"mistral","npm":"@ai-sdk/mistral","api":null,"env":["MISTRAL_API_KEY"],"models":{"codestral-latest":{"limit":{"context":256000,"output":4096}},"devstral-2512":{"limit":{"context":262144,"output":262144}},"devstral-medium-2507":{"limit":{"context":128000,"output":128000}},"devstral-medium-latest":{"limit":{"context":262144,"output":262144}},"devstral-small-2505":{"limit":{"context":128000,"output":128000}},"devstral-small-2507":{"limit":{"context":128000,"output":128000}},"labs-devstral-small-2512":{"limit":{"context":256000,"output":256000}},"magistral-medium-latest":{"limit":{"context":128000,"output":16384}},"magistral-small":{"limit":{"context":128000,"output":128000}},"ministral-3b-latest":{"limit":{"context":128000,"output":128000}},"ministral-8b-latest":{"limit":{"context":128000,"output":128000}},"mistral-embed":{"limit":{"context":8000,"output":3072}},"mistral-large-2411":{"limit":{"context":131072,"output":16384}},"mistral-large-2512":{"limit":{"context":262144,"output":262144}},"mistral-large-latest":{"limit":{"context":262144,"output":262144}},"mistral-medium-2505":{"limit":{"context":131072,"output":131072}},"mistral-medium-2508":{"limit":{"context":262144,"output":262144}},"mistral-medium-latest":{"limit":{"context":128000,"output":16384}},"mistral-nemo":{"limit":{"context":128000,"output":128000}},"mistral-small-2506":{"limit":{"context":128000,"output":16384}},"mistral-small-latest":{"limit":{"context":128000,"output":16384}},"open-mistral-7b":{"limit":{"context":8000,"output":8000}},"open-mixtral-8x22b":{"limit":{"context":64000,"output":64000}},"open-mixtral-8x7b":{"limit":{"context":32000,"output":32000}},"pixtral-12b":{"limit":{"context":128000,"output":128000}},"pixtral-large-latest":{"limit":{"context":128000,"output":128000}}}},"moark":{"id":"moark","npm":"@ai-sdk/openai-compatible","api":"https://moark.com/v1","env":["MOARK_API_KEY"],"models":{"GLM-4.7":{"limit":{"context":204800,"output":131072}},"MiniMax-M2.1":{"limit":{"context":204800,"output":131072}}}},"modelscope":{"id":"modelscope","npm":"@ai-sdk/openai-compatible","api":"https://api-inference.modelscope.cn/v1","env":["MODELSCOPE_API_KEY"],"models":{"Qwen/Qwen3-235B-A22B-Instruct-2507":{"limit":{"context":262144,"output":131072}},"Qwen/Qwen3-235B-A22B-Thinking-2507":{"limit":{"context":262144,"output":131072}},"Qwen/Qwen3-30B-A3B-Instruct-2507":{"limit":{"context":262144,"output":16384}},"Qwen/Qwen3-30B-A3B-Thinking-2507":{"limit":{"context":262144,"output":32768}},"Qwen/Qwen3-Coder-30B-A3B-Instruct":{"limit":{"context":262144,"output":65536}},"ZhipuAI/GLM-4.5":{"limit":{"context":131072,"output":98304}},"ZhipuAI/GLM-4.6":{"limit":{"context":202752,"output":98304}}}},"moonshotai":{"id":"moonshotai","npm":"@ai-sdk/openai-compatible","api":"https://api.moonshot.ai/v1","env":["MOONSHOT_API_KEY"],"models":{"kimi-k2-0711-preview":{"limit":{"context":131072,"output":16384}},"kimi-k2-0905-preview":{"limit":{"context":262144,"output":262144}},"kimi-k2-thinking":{"limit":{"context":262144,"output":262144}},"kimi-k2-thinking-turbo":{"limit":{"context":262144,"output":262144}},"kimi-k2-turbo-preview":{"limit":{"context":262144,"output":262144}},"kimi-k2.5":{"limit":{"context":262144,"output":262144}}}},"moonshotai-cn":{"id":"moonshotai-cn","npm":"@ai-sdk/openai-compatible","api":"https://api.moonshot.cn/v1","env":["MOONSHOT_API_KEY"],"models":{"kimi-k2-0711-preview":{"limit":{"context":131072,"output":16384}},"kimi-k2-0905-preview":{"limit":{"context":262144,"output":262144}},"kimi-k2-thinking":{"limit":{"context":262144,"output":262144}},"kimi-k2-thinking-turbo":{"limit":{"context":262144,"output":262144}},"kimi-k2-turbo-preview":{"limit":{"context":262144,"output":262144}},"kimi-k2.5":{"limit":{"context":262144,"output":262144}}}},"morph":{"id":"morph","npm":"@ai-sdk/openai-compatible","api":"https://api.morphllm.com/v1","env":["MORPH_API_KEY"],"models":{"auto":{"limit":{"context":32000,"output":32000}},"morph-v3-fast":{"limit":{"context":16000,"output":16000}},"morph-v3-large":{"limit":{"context":32000,"output":32000}}}},"nano-gpt":{"id":"nano-gpt","npm":"@ai-sdk/openai-compatible","api":"https://nano-gpt.com/api/v1","env":["NANO_GPT_API_KEY"],"models":{"deepseek/deepseek-r1":{"limit":{"context":128000,"output":8192}},"deepseek/deepseek-v3.2:thinking":{"limit":{"context":128000,"output":8192}},"meta-llama/llama-3.3-70b-instruct":{"limit":{"context":128000,"output":8192}},"meta-llama/llama-4-maverick":{"limit":{"context":128000,"output":8192}},"minimax/minimax-m2.1":{"limit":{"context":128000,"output":8192}},"minimax/minimax-m2.5":{"limit":{"context":204800,"output":131072}},"minimax/minimax-m2.5-official":{"limit":{"context":204800,"output":131072}},"mistralai/devstral-2-123b-instruct-2512":{"limit":{"context":131072,"output":8192}},"mistralai/ministral-14b-instruct-2512":{"limit":{"context":131072,"output":8192}},"mistralai/mistral-large-3-675b-instruct-2512":{"limit":{"context":131072,"output":8192}},"moonshotai/kimi-k2-instruct":{"limit":{"context":131072,"output":8192}},"moonshotai/kimi-k2-thinking":{"limit":{"context":32768,"output":8192}},"moonshotai/kimi-k2.5":{"limit":{"context":256000,"output":65536}},"moonshotai/kimi-k2.5-thinking":{"limit":{"context":256000,"output":65536}},"nousresearch/hermes-4-405b:thinking":{"limit":{"context":128000,"output":8192}},"nvidia/llama-3_3-nemotron-super-49b-v1_5":{"limit":{"context":128000,"output":8192}},"openai/gpt-oss-120b":{"limit":{"context":128000,"output":8192}},"qwen/qwen3-235b-a22b-thinking-2507":{"limit":{"context":262144,"output":8192}},"qwen/qwen3-coder":{"limit":{"context":106000,"output":8192}},"zai-org/glm-4.5-air":{"limit":{"context":128000,"output":8192}},"zai-org/glm-4.5-air:thinking":{"limit":{"context":128000,"output":8192}},"zai-org/glm-4.6":{"limit":{"context":200000,"output":8192}},"zai-org/glm-4.6:thinking":{"limit":{"context":128000,"output":8192}},"zai-org/glm-4.7":{"limit":{"context":204800,"output":8192}},"zai-org/glm-4.7:thinking":{"limit":{"context":128000,"output":8192}},"zai-org/glm-5":{"limit":{"context":200000,"output":128000}},"zai-org/glm-5-original":{"limit":{"context":200000,"output":128000}},"zai-org/glm-5-original:thinking":{"limit":{"context":200000,"output":128000}},"zai-org/glm-5:thinking":{"limit":{"context":200000,"output":128000}}}},"nebius":{"id":"nebius","npm":"@ai-sdk/openai-compatible","api":"https://api.tokenfactory.nebius.com/v1","env":["NEBIUS_API_KEY"],"models":{"BAAI/bge-en-icl":{"limit":{"context":32768,"output":0}},"BAAI/bge-multilingual-gemma2":{"limit":{"context":8192,"output":0}},"MiniMaxAI/minimax-m2.1":{"limit":{"context":128000,"output":8192}},"NousResearch/hermes-4-405b":{"limit":{"context":128000,"output":8192}},"NousResearch/hermes-4-70b":{"limit":{"context":128000,"output":8192}},"PrimeIntellect/intellect-3":{"limit":{"context":128000,"output":8192}},"black-forest-labs/flux-dev":{"limit":{"context":77,"output":0}},"black-forest-labs/flux-schnell":{"limit":{"context":77,"output":0}},"deepseek-ai/deepseek-r1-0528":{"limit":{"context":128000,"output":32768}},"deepseek-ai/deepseek-r1-0528-fast":{"limit":{"context":131072,"output":8192}},"deepseek-ai/deepseek-v3-0324":{"limit":{"context":128000,"output":8192}},"deepseek-ai/deepseek-v3-0324-fast":{"limit":{"context":128000,"output":8192}},"deepseek-ai/deepseek-v3.2":{"limit":{"context":128000,"output":8192}},"google/gemma-2-2b-it":{"limit":{"context":8192,"output":4096}},"google/gemma-2-9b-it-fast":{"limit":{"context":8192,"output":4096}},"google/gemma-3-27b-it":{"limit":{"context":128000,"output":8192}},"google/gemma-3-27b-it-fast":{"limit":{"context":128000,"output":8192}},"intfloat/e5-mistral-7b-instruct":{"limit":{"context":32768,"output":0}},"meta-llama/Llama-3.3-70B-Instruct":{"limit":{"context":128000,"output":8192}},"meta-llama/llama-3.3-70b-instruct-fast":{"limit":{"context":128000,"output":8192}},"meta-llama/llama-guard-3-8b":{"limit":{"context":8192,"output":1024}},"meta-llama/meta-llama-3.1-8b-instruct":{"limit":{"context":128000,"output":4096}},"meta-llama/meta-llama-3.1-8b-instruct-fast":{"limit":{"context":128000,"output":4096}},"moonshotai/Kimi-K2.5":{"limit":{"context":262144,"output":8192}},"moonshotai/kimi-k2-instruct":{"limit":{"context":200000,"output":8192}},"moonshotai/kimi-k2-thinking":{"limit":{"context":128000,"output":16384}},"nvidia/llama-3_1-nemotron-ultra-253b-v1":{"limit":{"context":128000,"output":4096}},"nvidia/nemotron-nano-v2-12b":{"limit":{"context":32000,"output":4096}},"nvidia/nvidia-nemotron-3-nano-30b-a3b":{"limit":{"context":32000,"output":4096}},"openai/gpt-oss-120b":{"limit":{"context":128000,"output":8192}},"openai/gpt-oss-20b":{"limit":{"context":128000,"output":4096}},"qwen/qwen2.5-coder-7b-fast":{"limit":{"context":128000,"output":8192}},"qwen/qwen2.5-vl-72b-instruct":{"limit":{"context":128000,"output":8192}},"qwen/qwen3-235b-a22b-instruct-2507":{"limit":{"context":262144,"output":8192}},"qwen/qwen3-235b-a22b-thinking-2507":{"limit":{"context":262144,"output":8192}},"qwen/qwen3-30b-a3b-instruct-2507":{"limit":{"context":128000,"output":8192}},"qwen/qwen3-30b-a3b-thinking-2507":{"limit":{"context":128000,"output":16384}},"qwen/qwen3-32b":{"limit":{"context":128000,"output":8192}},"qwen/qwen3-32b-fast":{"limit":{"context":128000,"output":8192}},"qwen/qwen3-coder-30b-a3b-instruct":{"limit":{"context":128000,"output":8192}},"qwen/qwen3-coder-480b-a35b-instruct":{"limit":{"context":262144,"output":66536}},"qwen/qwen3-embedding-8b":{"limit":{"context":32768,"output":0}},"qwen/qwen3-next-80b-a3b-thinking":{"limit":{"context":128000,"output":16384}},"zai-org/glm-4.5":{"limit":{"context":128000,"output":4096}},"zai-org/glm-4.5-air":{"limit":{"context":128000,"output":4096}},"zai-org/glm-4.7-fp8":{"limit":{"context":128000,"output":4096}}}},"nova":{"id":"nova","npm":"@ai-sdk/openai-compatible","api":"https://api.nova.amazon.com/v1","env":["NOVA_API_KEY"],"models":{"nova-2-lite-v1":{"limit":{"context":1000000,"output":64000}},"nova-2-pro-v1":{"limit":{"context":1000000,"output":64000}}}},"novita-ai":{"id":"novita-ai","npm":"@ai-sdk/openai-compatible","api":"https://api.novita.ai/openai","env":["NOVITA_API_KEY"],"models":{"baichuan/baichuan-m2-32b":{"limit":{"context":131072,"output":131072}},"baidu/ernie-4.5-21B-a3b":{"limit":{"context":120000,"output":8000}},"baidu/ernie-4.5-21B-a3b-thinking":{"limit":{"context":131072,"output":65536}},"baidu/ernie-4.5-300b-a47b-paddle":{"limit":{"context":123000,"output":12000}},"baidu/ernie-4.5-vl-28b-a3b":{"limit":{"context":30000,"output":8000}},"baidu/ernie-4.5-vl-28b-a3b-thinking":{"limit":{"context":131072,"output":65536}},"baidu/ernie-4.5-vl-424b-a47b":{"limit":{"context":123000,"output":16000}},"deepseek/deepseek-ocr":{"limit":{"context":8192,"output":8192}},"deepseek/deepseek-ocr-2":{"limit":{"context":8192,"output":8192}},"deepseek/deepseek-prover-v2-671b":{"limit":{"context":160000,"output":160000}},"deepseek/deepseek-r1-0528":{"limit":{"context":163840,"output":32768}},"deepseek/deepseek-r1-0528-qwen3-8b":{"limit":{"context":128000,"output":32000}},"deepseek/deepseek-r1-distill-llama-70b":{"limit":{"context":8192,"output":8192}},"deepseek/deepseek-r1-turbo":{"limit":{"context":64000,"output":16000}},"deepseek/deepseek-v3-0324":{"limit":{"context":163840,"output":163840}},"deepseek/deepseek-v3-turbo":{"limit":{"context":64000,"output":16000}},"deepseek/deepseek-v3.1":{"limit":{"context":131072,"output":32768}},"deepseek/deepseek-v3.1-terminus":{"limit":{"context":131072,"output":32768}},"deepseek/deepseek-v3.2":{"limit":{"context":163840,"output":65536}},"deepseek/deepseek-v3.2-exp":{"limit":{"context":163840,"output":65536}},"google/gemma-3-27b-it":{"limit":{"context":98304,"output":16384}},"gryphe/mythomax-l2-13b":{"limit":{"context":4096,"output":3200}},"kwaipilot/kat-coder":{"limit":{"context":256000,"output":32000}},"kwaipilot/kat-coder-pro":{"limit":{"context":256000,"output":128000}},"meta-llama/llama-3-70b-instruct":{"limit":{"context":8192,"output":8000}},"meta-llama/llama-3-8b-instruct":{"limit":{"context":8192,"output":8192}},"meta-llama/llama-3.1-8b-instruct":{"limit":{"context":16384,"output":16384}},"meta-llama/llama-3.3-70b-instruct":{"limit":{"context":131072,"output":120000}},"meta-llama/llama-4-maverick-17b-128e-instruct-fp8":{"limit":{"context":1048576,"output":8192}},"meta-llama/llama-4-scout-17b-16e-instruct":{"limit":{"context":131072,"output":131072}},"microsoft/wizardlm-2-8x22b":{"limit":{"context":65535,"output":8000}},"minimax/minimax-m2":{"limit":{"context":204800,"output":131072}},"minimax/minimax-m2.1":{"limit":{"context":204800,"output":131072}},"minimax/minimax-m2.5":{"limit":{"context":204800,"output":131100}},"minimaxai/minimax-m1-80k":{"limit":{"context":1000000,"output":40000}},"mistralai/mistral-nemo":{"limit":{"context":60288,"output":16000}},"moonshotai/kimi-k2-0905":{"limit":{"context":262144,"output":262144}},"moonshotai/kimi-k2-instruct":{"limit":{"context":131072,"output":131072}},"moonshotai/kimi-k2-thinking":{"limit":{"context":262144,"output":262144}},"moonshotai/kimi-k2.5":{"limit":{"context":262144,"output":262144}},"nousresearch/hermes-2-pro-llama-3-8b":{"limit":{"context":8192,"output":8192}},"openai/gpt-oss-120b":{"limit":{"context":131072,"output":32768}},"openai/gpt-oss-20b":{"limit":{"context":131072,"output":32768}},"paddlepaddle/paddleocr-vl":{"limit":{"context":16384,"output":16384}},"qwen/qwen-2.5-72b-instruct":{"limit":{"context":32000,"output":8192}},"qwen/qwen-mt-plus":{"limit":{"context":16384,"output":8192}},"qwen/qwen2.5-7b-instruct":{"limit":{"context":32000,"output":32000}},"qwen/qwen2.5-vl-72b-instruct":{"limit":{"context":32768,"output":32768}},"qwen/qwen3-235b-a22b-fp8":{"limit":{"context":40960,"output":20000}},"qwen/qwen3-235b-a22b-instruct-2507":{"limit":{"context":131072,"output":16384}},"qwen/qwen3-235b-a22b-thinking-2507":{"limit":{"context":131072,"output":32768}},"qwen/qwen3-30b-a3b-fp8":{"limit":{"context":40960,"output":20000}},"qwen/qwen3-32b-fp8":{"limit":{"context":40960,"output":20000}},"qwen/qwen3-4b-fp8":{"limit":{"context":128000,"output":20000}},"qwen/qwen3-8b-fp8":{"limit":{"context":128000,"output":20000}},"qwen/qwen3-coder-30b-a3b-instruct":{"limit":{"context":160000,"output":32768}},"qwen/qwen3-coder-480b-a35b-instruct":{"limit":{"context":262144,"output":65536}},"qwen/qwen3-coder-next":{"limit":{"context":262144,"output":65536}},"qwen/qwen3-max":{"limit":{"context":262144,"output":65536}},"qwen/qwen3-next-80b-a3b-instruct":{"limit":{"context":131072,"output":32768}},"qwen/qwen3-next-80b-a3b-thinking":{"limit":{"context":131072,"output":32768}},"qwen/qwen3-omni-30b-a3b-instruct":{"limit":{"context":65536,"output":16384}},"qwen/qwen3-omni-30b-a3b-thinking":{"limit":{"context":65536,"output":16384}},"qwen/qwen3-vl-235b-a22b-instruct":{"limit":{"context":131072,"output":32768}},"qwen/qwen3-vl-235b-a22b-thinking":{"limit":{"context":131072,"output":32768}},"qwen/qwen3-vl-30b-a3b-instruct":{"limit":{"context":131072,"output":32768}},"qwen/qwen3-vl-30b-a3b-thinking":{"limit":{"context":131072,"output":32768}},"qwen/qwen3-vl-8b-instruct":{"limit":{"context":131072,"output":32768}},"sao10k/L3-8B-Stheno-v3.2":{"limit":{"context":8192,"output":32000}},"sao10k/l3-70b-euryale-v2.1":{"limit":{"context":8192,"output":8192}},"sao10k/l3-8b-lunaris":{"limit":{"context":8192,"output":8192}},"sao10k/l31-70b-euryale-v2.2":{"limit":{"context":8192,"output":8192}},"skywork/r1v4-lite":{"limit":{"context":262144,"output":65536}},"xiaomimimo/mimo-v2-flash":{"limit":{"context":262144,"output":32000}},"zai-org/autoglm-phone-9b-multilingual":{"limit":{"context":65536,"output":65536}},"zai-org/glm-4.5":{"limit":{"context":131072,"output":98304}},"zai-org/glm-4.5-air":{"limit":{"context":131072,"output":98304}},"zai-org/glm-4.5v":{"limit":{"context":65536,"output":16384}},"zai-org/glm-4.6":{"limit":{"context":204800,"output":131072}},"zai-org/glm-4.6v":{"limit":{"context":131072,"output":32768}},"zai-org/glm-4.7":{"limit":{"context":204800,"output":131072}},"zai-org/glm-4.7-flash":{"limit":{"context":200000,"output":128000}},"zai-org/glm-5":{"limit":{"context":202800,"output":131072}}}},"nvidia":{"id":"nvidia","npm":"@ai-sdk/openai-compatible","api":"https://integrate.api.nvidia.com/v1","env":["NVIDIA_API_KEY"],"models":{"black-forest-labs/flux.1-dev":{"limit":{"context":4096,"output":0}},"deepseek-ai/deepseek-coder-6.7b-instruct":{"limit":{"context":128000,"output":4096}},"deepseek-ai/deepseek-r1":{"limit":{"context":128000,"output":4096}},"deepseek-ai/deepseek-r1-0528":{"limit":{"context":128000,"output":4096}},"deepseek-ai/deepseek-v3.1":{"limit":{"context":128000,"output":8192}},"deepseek-ai/deepseek-v3.1-terminus":{"limit":{"context":128000,"output":8192}},"deepseek-ai/deepseek-v3.2":{"limit":{"context":163840,"output":65536}},"google/codegemma-1.1-7b":{"limit":{"context":128000,"output":4096}},"google/codegemma-7b":{"limit":{"context":128000,"output":4096}},"google/gemma-2-27b-it":{"limit":{"context":128000,"output":4096}},"google/gemma-2-2b-it":{"limit":{"context":128000,"output":4096}},"google/gemma-3-12b-it":{"limit":{"context":128000,"output":4096}},"google/gemma-3-1b-it":{"limit":{"context":128000,"output":4096}},"google/gemma-3-27b-it":{"limit":{"context":131072,"output":8192}},"google/gemma-3n-e2b-it":{"limit":{"context":128000,"output":4096}},"google/gemma-3n-e4b-it":{"limit":{"context":128000,"output":4096}},"meta/codellama-70b":{"limit":{"context":128000,"output":4096}},"meta/llama-3.1-405b-instruct":{"limit":{"context":128000,"output":4096}},"meta/llama-3.1-70b-instruct":{"limit":{"context":128000,"output":4096}},"meta/llama-3.2-11b-vision-instruct":{"limit":{"context":128000,"output":4096}},"meta/llama-3.2-1b-instruct":{"limit":{"context":128000,"output":4096}},"meta/llama-3.3-70b-instruct":{"limit":{"context":128000,"output":4096}},"meta/llama-4-maverick-17b-128e-instruct":{"limit":{"context":128000,"output":4096}},"meta/llama-4-scout-17b-16e-instruct":{"limit":{"context":128000,"output":4096}},"meta/llama3-70b-instruct":{"limit":{"context":128000,"output":4096}},"meta/llama3-8b-instruct":{"limit":{"context":128000,"output":4096}},"microsoft/phi-3-medium-128k-instruct":{"limit":{"context":128000,"output":4096}},"microsoft/phi-3-medium-4k-instruct":{"limit":{"context":4000,"output":4096}},"microsoft/phi-3-small-128k-instruct":{"limit":{"context":128000,"output":4096}},"microsoft/phi-3-small-8k-instruct":{"limit":{"context":8000,"output":4096}},"microsoft/phi-3-vision-128k-instruct":{"limit":{"context":128000,"output":4096}},"microsoft/phi-3.5-moe-instruct":{"limit":{"context":128000,"output":4096}},"microsoft/phi-3.5-vision-instruct":{"limit":{"context":128000,"output":4096}},"microsoft/phi-4-mini-instruct":{"limit":{"context":131072,"output":8192}},"minimaxai/minimax-m2":{"limit":{"context":128000,"output":16384}},"minimaxai/minimax-m2.1":{"limit":{"context":204800,"output":131072}},"mistralai/codestral-22b-instruct-v0.1":{"limit":{"context":128000,"output":4096}},"mistralai/devstral-2-123b-instruct-2512":{"limit":{"context":262144,"output":262144}},"mistralai/mamba-codestral-7b-v0.1":{"limit":{"context":128000,"output":4096}},"mistralai/ministral-14b-instruct-2512":{"limit":{"context":262144,"output":262144}},"mistralai/mistral-large-2-instruct":{"limit":{"context":128000,"output":4096}},"mistralai/mistral-large-3-675b-instruct-2512":{"limit":{"context":262144,"output":262144}},"mistralai/mistral-small-3.1-24b-instruct-2503":{"limit":{"context":128000,"output":4096}},"moonshotai/kimi-k2-instruct":{"limit":{"context":128000,"output":8192}},"moonshotai/kimi-k2-instruct-0905":{"limit":{"context":262144,"output":262144}},"moonshotai/kimi-k2-thinking":{"limit":{"context":262144,"output":262144}},"moonshotai/kimi-k2.5":{"limit":{"context":262144,"output":262144}},"nvidia/cosmos-nemotron-34b":{"limit":{"context":131072,"output":8192}},"nvidia/llama-3.1-nemotron-51b-instruct":{"limit":{"context":128000,"output":4096}},"nvidia/llama-3.1-nemotron-70b-instruct":{"limit":{"context":128000,"output":4096}},"nvidia/llama-3.1-nemotron-ultra-253b-v1":{"limit":{"context":131072,"output":8192}},"nvidia/llama-3.3-nemotron-super-49b-v1":{"limit":{"context":128000,"output":4096}},"nvidia/llama-3.3-nemotron-super-49b-v1.5":{"limit":{"context":128000,"output":4096}},"nvidia/llama-embed-nemotron-8b":{"limit":{"context":32768,"output":2048}},"nvidia/llama3-chatqa-1.5-70b":{"limit":{"context":128000,"output":4096}},"nvidia/nemoretriever-ocr-v1":{"limit":{"context":0,"output":4096}},"nvidia/nemotron-3-nano-30b-a3b":{"limit":{"context":131072,"output":131072}},"nvidia/nemotron-4-340b-instruct":{"limit":{"context":128000,"output":4096}},"nvidia/nvidia-nemotron-nano-9b-v2":{"limit":{"context":131072,"output":131072}},"nvidia/parakeet-tdt-0.6b-v2":{"limit":{"context":0,"output":4096}},"openai/gpt-oss-120b":{"limit":{"context":128000,"output":8192}},"openai/whisper-large-v3":{"limit":{"context":0,"output":4096}},"qwen/qwen2.5-coder-32b-instruct":{"limit":{"context":128000,"output":4096}},"qwen/qwen2.5-coder-7b-instruct":{"limit":{"context":128000,"output":4096}},"qwen/qwen3-235b-a22b":{"limit":{"context":131072,"output":8192}},"qwen/qwen3-coder-480b-a35b-instruct":{"limit":{"context":262144,"output":66536}},"qwen/qwen3-next-80b-a3b-instruct":{"limit":{"context":262144,"output":16384}},"qwen/qwen3-next-80b-a3b-thinking":{"limit":{"context":262144,"output":16384}},"qwen/qwq-32b":{"limit":{"context":128000,"output":4096}},"z-ai/glm4.7":{"limit":{"context":204800,"output":131072}},"z-ai/glm5":{"limit":{"context":202752,"output":131000}}}},"ollama-cloud":{"id":"ollama-cloud","npm":"@ai-sdk/openai-compatible","api":"https://ollama.com/v1","env":["OLLAMA_API_KEY"],"models":{"cogito-2.1:671b":{"limit":{"context":163840,"output":32000}},"deepseek-v3.1:671b":{"limit":{"context":163840,"output":163840}},"deepseek-v3.2":{"limit":{"context":163840,"output":65536}},"devstral-2:123b":{"limit":{"context":262144,"output":262144}},"devstral-small-2:24b":{"limit":{"context":262144,"output":262144}},"gemini-3-flash-preview":{"limit":{"context":1048576,"output":65536}},"gemini-3-pro-preview":{"limit":{"context":1048576,"output":64000}},"gemma3:12b":{"limit":{"context":131072,"output":131072}},"gemma3:27b":{"limit":{"context":131072,"output":131072}},"gemma3:4b":{"limit":{"context":131072,"output":131072}},"glm-4.6":{"limit":{"context":202752,"output":131072}},"glm-4.7":{"limit":{"context":202752,"output":131072}},"glm-5":{"limit":{"context":202752,"output":131072}},"gpt-oss:120b":{"limit":{"context":131072,"output":32768}},"gpt-oss:20b":{"limit":{"context":131072,"output":32768}},"kimi-k2-thinking":{"limit":{"context":262144,"output":262144}},"kimi-k2.5":{"limit":{"context":262144,"output":262144}},"kimi-k2:1t":{"limit":{"context":262144,"output":262144}},"minimax-m2":{"limit":{"context":204800,"output":128000}},"minimax-m2.1":{"limit":{"context":204800,"output":131072}},"minimax-m2.5":{"limit":{"context":204800,"output":131072}},"ministral-3:14b":{"limit":{"context":262144,"output":128000}},"ministral-3:3b":{"limit":{"context":262144,"output":128000}},"ministral-3:8b":{"limit":{"context":262144,"output":128000}},"mistral-large-3:675b":{"limit":{"context":262144,"output":262144}},"nemotron-3-nano:30b":{"limit":{"context":1048576,"output":131072}},"qwen3-coder-next":{"limit":{"context":262144,"output":65536}},"qwen3-coder:480b":{"limit":{"context":262144,"output":65536}},"qwen3-next:80b":{"limit":{"context":262144,"output":32768}},"qwen3-vl:235b":{"limit":{"context":262144,"output":32768}},"qwen3-vl:235b-instruct":{"limit":{"context":262144,"output":131072}},"rnj-1:8b":{"limit":{"context":32768,"output":4096}}}},"openai":{"id":"openai","npm":"@ai-sdk/openai","api":null,"env":["OPENAI_API_KEY"],"models":{"codex-mini-latest":{"limit":{"context":200000,"output":100000}},"gpt-3.5-turbo":{"limit":{"context":16385,"output":4096}},"gpt-4":{"limit":{"context":8192,"output":8192}},"gpt-4-turbo":{"limit":{"context":128000,"output":4096}},"gpt-4.1":{"limit":{"context":1047576,"output":32768}},"gpt-4.1-mini":{"limit":{"context":1047576,"output":32768}},"gpt-4.1-nano":{"limit":{"context":1047576,"output":32768}},"gpt-4o":{"limit":{"context":128000,"output":16384}},"gpt-4o-2024-05-13":{"limit":{"context":128000,"output":4096}},"gpt-4o-2024-08-06":{"limit":{"context":128000,"output":16384}},"gpt-4o-2024-11-20":{"limit":{"context":128000,"output":16384}},"gpt-4o-mini":{"limit":{"context":128000,"output":16384}},"gpt-5":{"limit":{"context":400000,"output":128000}},"gpt-5-chat-latest":{"limit":{"context":400000,"output":128000}},"gpt-5-codex":{"limit":{"context":400000,"output":128000}},"gpt-5-mini":{"limit":{"context":400000,"output":128000}},"gpt-5-nano":{"limit":{"context":400000,"output":128000}},"gpt-5-pro":{"limit":{"context":400000,"output":272000}},"gpt-5.1":{"limit":{"context":400000,"output":128000}},"gpt-5.1-chat-latest":{"limit":{"context":128000,"output":16384}},"gpt-5.1-codex":{"limit":{"context":400000,"output":128000}},"gpt-5.1-codex-max":{"limit":{"context":400000,"output":128000}},"gpt-5.1-codex-mini":{"limit":{"context":400000,"output":128000}},"gpt-5.2":{"limit":{"context":400000,"output":128000}},"gpt-5.2-chat-latest":{"limit":{"context":128000,"output":16384}},"gpt-5.2-codex":{"limit":{"context":400000,"output":128000}},"gpt-5.2-pro":{"limit":{"context":400000,"output":128000}},"gpt-5.3-codex":{"limit":{"context":400000,"output":128000}},"gpt-5.3-codex-spark":{"limit":{"context":128000,"output":32000}},"o1":{"limit":{"context":200000,"output":100000}},"o1-mini":{"limit":{"context":128000,"output":65536}},"o1-preview":{"limit":{"context":128000,"output":32768}},"o1-pro":{"limit":{"context":200000,"output":100000}},"o3":{"limit":{"context":200000,"output":100000}},"o3-deep-research":{"limit":{"context":200000,"output":100000}},"o3-mini":{"limit":{"context":200000,"output":100000}},"o3-pro":{"limit":{"context":200000,"output":100000}},"o4-mini":{"limit":{"context":200000,"output":100000}},"o4-mini-deep-research":{"limit":{"context":200000,"output":100000}},"text-embedding-3-large":{"limit":{"context":8191,"output":3072}},"text-embedding-3-small":{"limit":{"context":8191,"output":1536}},"text-embedding-ada-002":{"limit":{"context":8192,"output":1536}}}},"opencode":{"id":"opencode","npm":"@ai-sdk/openai-compatible","api":"https://opencode.ai/zen/v1","env":["OPENCODE_API_KEY"],"models":{"big-pickle":{"limit":{"context":200000,"output":128000}},"claude-3-5-haiku":{"limit":{"context":200000,"output":8192}},"claude-haiku-4-5":{"limit":{"context":200000,"output":64000}},"claude-opus-4-1":{"limit":{"context":200000,"output":32000}},"claude-opus-4-5":{"limit":{"context":200000,"output":64000}},"claude-opus-4-6":{"limit":{"context":1000000,"output":128000}},"claude-sonnet-4":{"limit":{"context":1000000,"output":64000}},"claude-sonnet-4-5":{"limit":{"context":1000000,"output":64000}},"gemini-3-flash":{"limit":{"context":1048576,"output":65536}},"gemini-3-pro":{"limit":{"context":1048576,"output":65536}},"glm-4.6":{"limit":{"context":204800,"output":131072}},"glm-4.7":{"limit":{"context":204800,"output":131072}},"glm-4.7-free":{"limit":{"context":204800,"output":131072}},"glm-5":{"limit":{"context":204800,"output":131072}},"gpt-5":{"limit":{"context":400000,"output":128000}},"gpt-5-codex":{"limit":{"context":400000,"output":128000}},"gpt-5-nano":{"limit":{"context":400000,"output":128000}},"gpt-5.1":{"limit":{"context":400000,"output":128000}},"gpt-5.1-codex":{"limit":{"context":400000,"output":128000}},"gpt-5.1-codex-max":{"limit":{"context":400000,"output":128000}},"gpt-5.1-codex-mini":{"limit":{"context":400000,"output":128000}},"gpt-5.2":{"limit":{"context":400000,"output":128000}},"gpt-5.2-codex":{"limit":{"context":400000,"output":128000}},"grok-code":{"limit":{"context":256000,"output":256000}},"kimi-k2":{"limit":{"context":262144,"output":262144}},"kimi-k2-thinking":{"limit":{"context":262144,"output":262144}},"kimi-k2.5":{"limit":{"context":262144,"output":262144}},"kimi-k2.5-free":{"limit":{"context":262144,"output":262144}},"minimax-m2.1":{"limit":{"context":204800,"output":131072}},"minimax-m2.1-free":{"limit":{"context":204800,"output":131072}},"minimax-m2.5":{"limit":{"context":204800,"output":131072}},"minimax-m2.5-free":{"limit":{"context":204800,"output":131072}},"qwen3-coder":{"limit":{"context":262144,"output":65536}},"trinity-large-preview-free":{"limit":{"context":131072,"output":131072}}}},"openrouter":{"id":"openrouter","npm":"@openrouter/ai-sdk-provider","api":"https://openrouter.ai/api/v1","env":["OPENROUTER_API_KEY"],"models":{"allenai/molmo-2-8b:free":{"limit":{"context":36864,"output":36864}},"anthropic/claude-3.5-haiku":{"limit":{"context":200000,"output":8192}},"anthropic/claude-3.7-sonnet":{"limit":{"context":200000,"output":128000}},"anthropic/claude-haiku-4.5":{"limit":{"context":200000,"output":64000}},"anthropic/claude-opus-4":{"limit":{"context":200000,"output":32000}},"anthropic/claude-opus-4.1":{"limit":{"context":200000,"output":32000}},"anthropic/claude-opus-4.5":{"limit":{"context":200000,"output":32000}},"anthropic/claude-opus-4.6":{"limit":{"context":1000000,"output":128000}},"anthropic/claude-sonnet-4":{"limit":{"context":200000,"output":64000}},"anthropic/claude-sonnet-4.5":{"limit":{"context":1000000,"output":64000}},"arcee-ai/trinity-large-preview:free":{"limit":{"context":131072,"output":131072}},"arcee-ai/trinity-mini:free":{"limit":{"context":131072,"output":131072}},"black-forest-labs/flux.2-flex":{"limit":{"context":67344,"output":67344}},"black-forest-labs/flux.2-klein-4b":{"limit":{"context":40960,"output":40960}},"black-forest-labs/flux.2-max":{"limit":{"context":46864,"output":46864}},"black-forest-labs/flux.2-pro":{"limit":{"context":46864,"output":46864}},"bytedance-seed/seedream-4.5":{"limit":{"context":4096,"output":4096}},"cognitivecomputations/dolphin-mistral-24b-venice-edition:free":{"limit":{"context":32768,"output":32768}},"cognitivecomputations/dolphin3.0-mistral-24b":{"limit":{"context":32768,"output":8192}},"cognitivecomputations/dolphin3.0-r1-mistral-24b":{"limit":{"context":32768,"output":8192}},"deepseek/deepseek-chat-v3-0324":{"limit":{"context":16384,"output":8192}},"deepseek/deepseek-chat-v3.1":{"limit":{"context":163840,"output":163840}},"deepseek/deepseek-r1-0528-qwen3-8b:free":{"limit":{"context":131072,"output":131072}},"deepseek/deepseek-r1-0528:free":{"limit":{"context":163840,"output":163840}},"deepseek/deepseek-r1-distill-llama-70b":{"limit":{"context":8192,"output":8192}},"deepseek/deepseek-r1-distill-qwen-14b":{"limit":{"context":64000,"output":8192}},"deepseek/deepseek-r1:free":{"limit":{"context":163840,"output":163840}},"deepseek/deepseek-v3-base:free":{"limit":{"context":163840,"output":163840}},"deepseek/deepseek-v3.1-terminus":{"limit":{"context":131072,"output":65536}},"deepseek/deepseek-v3.1-terminus:exacto":{"limit":{"context":131072,"output":65536}},"deepseek/deepseek-v3.2":{"limit":{"context":163840,"output":65536}},"deepseek/deepseek-v3.2-speciale":{"limit":{"context":163840,"output":65536}},"featherless/qwerky-72b":{"limit":{"context":32768,"output":8192}},"google/gemini-2.0-flash-001":{"limit":{"context":1048576,"output":8192}},"google/gemini-2.0-flash-exp:free":{"limit":{"context":1048576,"output":1048576}},"google/gemini-2.5-flash":{"limit":{"context":1048576,"output":65536}},"google/gemini-2.5-flash-lite":{"limit":{"context":1048576,"output":65536}},"google/gemini-2.5-flash-lite-preview-09-2025":{"limit":{"context":1048576,"output":65536}},"google/gemini-2.5-flash-preview-09-2025":{"limit":{"context":1048576,"output":65536}},"google/gemini-2.5-pro":{"limit":{"context":1048576,"output":65536}},"google/gemini-2.5-pro-preview-05-06":{"limit":{"context":1048576,"output":65536}},"google/gemini-2.5-pro-preview-06-05":{"limit":{"context":1048576,"output":65536}},"google/gemini-3-flash-preview":{"limit":{"context":1048576,"output":65536}},"google/gemini-3-pro-preview":{"limit":{"context":1050000,"output":66000}},"google/gemma-2-9b-it":{"limit":{"context":8192,"output":8192}},"google/gemma-3-12b-it":{"limit":{"context":131072,"output":131072}},"google/gemma-3-12b-it:free":{"limit":{"context":32768,"output":8192}},"google/gemma-3-27b-it":{"limit":{"context":96000,"output":96000}},"google/gemma-3-27b-it:free":{"limit":{"context":131072,"output":8192}},"google/gemma-3-4b-it":{"limit":{"context":96000,"output":96000}},"google/gemma-3-4b-it:free":{"limit":{"context":32768,"output":8192}},"google/gemma-3n-e2b-it:free":{"limit":{"context":8192,"output":2000}},"google/gemma-3n-e4b-it":{"limit":{"context":32768,"output":32768}},"google/gemma-3n-e4b-it:free":{"limit":{"context":8192,"output":2000}},"kwaipilot/kat-coder-pro:free":{"limit":{"context":256000,"output":65536}},"liquid/lfm-2.5-1.2b-instruct:free":{"limit":{"context":131072,"output":32768}},"liquid/lfm-2.5-1.2b-thinking:free":{"limit":{"context":131072,"output":32768}},"meta-llama/llama-3.1-405b-instruct:free":{"limit":{"context":131072,"output":131072}},"meta-llama/llama-3.2-11b-vision-instruct":{"limit":{"context":131072,"output":8192}},"meta-llama/llama-3.2-3b-instruct:free":{"limit":{"context":131072,"output":131072}},"meta-llama/llama-3.3-70b-instruct:free":{"limit":{"context":131072,"output":131072}},"meta-llama/llama-4-scout:free":{"limit":{"context":64000,"output":64000}},"microsoft/mai-ds-r1:free":{"limit":{"context":163840,"output":163840}},"minimax/minimax-01":{"limit":{"context":1000000,"output":1000000}},"minimax/minimax-m1":{"limit":{"context":1000000,"output":40000}},"minimax/minimax-m2":{"limit":{"context":196600,"output":118000}},"minimax/minimax-m2.1":{"limit":{"context":204800,"output":131072}},"minimax/minimax-m2.5":{"limit":{"context":204800,"output":131072}},"mistralai/codestral-2508":{"limit":{"context":256000,"output":256000}},"mistralai/devstral-2512":{"limit":{"context":262144,"output":262144}},"mistralai/devstral-2512:free":{"limit":{"context":262144,"output":262144}},"mistralai/devstral-medium-2507":{"limit":{"context":131072,"output":131072}},"mistralai/devstral-small-2505":{"limit":{"context":128000,"output":128000}},"mistralai/devstral-small-2505:free":{"limit":{"context":32768,"output":32768}},"mistralai/devstral-small-2507":{"limit":{"context":131072,"output":131072}},"mistralai/mistral-7b-instruct:free":{"limit":{"context":32768,"output":32768}},"mistralai/mistral-medium-3":{"limit":{"context":131072,"output":131072}},"mistralai/mistral-medium-3.1":{"limit":{"context":262144,"output":262144}},"mistralai/mistral-nemo:free":{"limit":{"context":131072,"output":131072}},"mistralai/mistral-small-3.1-24b-instruct":{"limit":{"context":128000,"output":8192}},"mistralai/mistral-small-3.2-24b-instruct":{"limit":{"context":96000,"output":8192}},"mistralai/mistral-small-3.2-24b-instruct:free":{"limit":{"context":96000,"output":96000}},"moonshotai/kimi-dev-72b:free":{"limit":{"context":131072,"output":131072}},"moonshotai/kimi-k2":{"limit":{"context":131072,"output":32768}},"moonshotai/kimi-k2-0905":{"limit":{"context":262144,"output":16384}},"moonshotai/kimi-k2-0905:exacto":{"limit":{"context":262144,"output":16384}},"moonshotai/kimi-k2-thinking":{"limit":{"context":262144,"output":262144}},"moonshotai/kimi-k2.5":{"limit":{"context":262144,"output":262144}},"moonshotai/kimi-k2:free":{"limit":{"context":32800,"output":32800}},"nousresearch/deephermes-3-llama-3-8b-preview":{"limit":{"context":131072,"output":8192}},"nousresearch/hermes-3-llama-3.1-405b:free":{"limit":{"context":131072,"output":131072}},"nousresearch/hermes-4-405b":{"limit":{"context":131072,"output":131072}},"nousresearch/hermes-4-70b":{"limit":{"context":131072,"output":131072}},"nvidia/nemotron-3-nano-30b-a3b:free":{"limit":{"context":256000,"output":256000}},"nvidia/nemotron-nano-12b-v2-vl:free":{"limit":{"context":128000,"output":128000}},"nvidia/nemotron-nano-9b-v2":{"limit":{"context":131072,"output":131072}},"nvidia/nemotron-nano-9b-v2:free":{"limit":{"context":128000,"output":128000}},"openai/gpt-4.1":{"limit":{"context":1047576,"output":32768}},"openai/gpt-4.1-mini":{"limit":{"context":1047576,"output":32768}},"openai/gpt-4o-mini":{"limit":{"context":128000,"output":16384}},"openai/gpt-5":{"limit":{"context":400000,"output":128000}},"openai/gpt-5-chat":{"limit":{"context":400000,"output":128000}},"openai/gpt-5-codex":{"limit":{"context":400000,"output":128000}},"openai/gpt-5-image":{"limit":{"context":400000,"output":128000}},"openai/gpt-5-mini":{"limit":{"context":400000,"output":128000}},"openai/gpt-5-nano":{"limit":{"context":400000,"output":128000}},"openai/gpt-5-pro":{"limit":{"context":400000,"output":272000}},"openai/gpt-5.1":{"limit":{"context":400000,"output":128000}},"openai/gpt-5.1-chat":{"limit":{"context":128000,"output":16384}},"openai/gpt-5.1-codex":{"limit":{"context":400000,"output":128000}},"openai/gpt-5.1-codex-max":{"limit":{"context":400000,"output":128000}},"openai/gpt-5.1-codex-mini":{"limit":{"context":400000,"output":100000}},"openai/gpt-5.2":{"limit":{"context":400000,"output":128000}},"openai/gpt-5.2-chat":{"limit":{"context":128000,"output":16384}},"openai/gpt-5.2-codex":{"limit":{"context":400000,"output":128000}},"openai/gpt-5.2-pro":{"limit":{"context":400000,"output":128000}},"openai/gpt-oss-120b":{"limit":{"context":131072,"output":32768}},"openai/gpt-oss-120b:exacto":{"limit":{"context":131072,"output":32768}},"openai/gpt-oss-120b:free":{"limit":{"context":131072,"output":32768}},"openai/gpt-oss-20b":{"limit":{"context":131072,"output":32768}},"openai/gpt-oss-20b:free":{"limit":{"context":131072,"output":32768}},"openai/gpt-oss-safeguard-20b":{"limit":{"context":131072,"output":65536}},"openai/o4-mini":{"limit":{"context":200000,"output":100000}},"openrouter/sherlock-dash-alpha":{"limit":{"context":1840000,"output":0}},"openrouter/sherlock-think-alpha":{"limit":{"context":1840000,"output":0}},"qwen/qwen-2.5-coder-32b-instruct":{"limit":{"context":32768,"output":8192}},"qwen/qwen-2.5-vl-7b-instruct:free":{"limit":{"context":32768,"output":32768}},"qwen/qwen2.5-vl-32b-instruct:free":{"limit":{"context":8192,"output":8192}},"qwen/qwen2.5-vl-72b-instruct":{"limit":{"context":32768,"output":8192}},"qwen/qwen2.5-vl-72b-instruct:free":{"limit":{"context":32768,"output":32768}},"qwen/qwen3-14b:free":{"limit":{"context":40960,"output":40960}},"qwen/qwen3-235b-a22b-07-25":{"limit":{"context":262144,"output":131072}},"qwen/qwen3-235b-a22b-07-25:free":{"limit":{"context":262144,"output":131072}},"qwen/qwen3-235b-a22b-thinking-2507":{"limit":{"context":262144,"output":81920}},"qwen/qwen3-235b-a22b:free":{"limit":{"context":131072,"output":131072}},"qwen/qwen3-30b-a3b-instruct-2507":{"limit":{"context":262000,"output":262000}},"qwen/qwen3-30b-a3b-thinking-2507":{"limit":{"context":262000,"output":262000}},"qwen/qwen3-30b-a3b:free":{"limit":{"context":40960,"output":40960}},"qwen/qwen3-32b:free":{"limit":{"context":40960,"output":40960}},"qwen/qwen3-4b:free":{"limit":{"context":40960,"output":40960}},"qwen/qwen3-8b:free":{"limit":{"context":40960,"output":40960}},"qwen/qwen3-coder":{"limit":{"context":262144,"output":66536}},"qwen/qwen3-coder-30b-a3b-instruct":{"limit":{"context":160000,"output":65536}},"qwen/qwen3-coder-flash":{"limit":{"context":128000,"output":66536}},"qwen/qwen3-coder:exacto":{"limit":{"context":131072,"output":32768}},"qwen/qwen3-coder:free":{"limit":{"context":262144,"output":66536}},"qwen/qwen3-max":{"limit":{"context":262144,"output":32768}},"qwen/qwen3-next-80b-a3b-instruct":{"limit":{"context":262144,"output":262144}},"qwen/qwen3-next-80b-a3b-instruct:free":{"limit":{"context":262144,"output":262144}},"qwen/qwen3-next-80b-a3b-thinking":{"limit":{"context":262144,"output":262144}},"qwen/qwq-32b:free":{"limit":{"context":32768,"output":32768}},"rekaai/reka-flash-3":{"limit":{"context":32768,"output":8192}},"sarvamai/sarvam-m:free":{"limit":{"context":32768,"output":32768}},"sourceful/riverflow-v2-fast-preview":{"limit":{"context":8192,"output":8192}},"sourceful/riverflow-v2-max-preview":{"limit":{"context":8192,"output":8192}},"sourceful/riverflow-v2-standard-preview":{"limit":{"context":8192,"output":8192}},"stepfun/step-3.5-flash":{"limit":{"context":256000,"output":256000}},"stepfun/step-3.5-flash:free":{"limit":{"context":256000,"output":256000}},"thudm/glm-z1-32b:free":{"limit":{"context":32768,"output":32768}},"tngtech/deepseek-r1t2-chimera:free":{"limit":{"context":163840,"output":163840}},"tngtech/tng-r1t-chimera:free":{"limit":{"context":163840,"output":163840}},"x-ai/grok-3":{"limit":{"context":131072,"output":8192}},"x-ai/grok-3-beta":{"limit":{"context":131072,"output":8192}},"x-ai/grok-3-mini":{"limit":{"context":131072,"output":8192}},"x-ai/grok-3-mini-beta":{"limit":{"context":131072,"output":8192}},"x-ai/grok-4":{"limit":{"context":256000,"output":64000}},"x-ai/grok-4-fast":{"limit":{"context":2000000,"output":30000}},"x-ai/grok-4.1-fast":{"limit":{"context":2000000,"output":30000}},"x-ai/grok-code-fast-1":{"limit":{"context":256000,"output":10000}},"xiaomi/mimo-v2-flash":{"limit":{"context":262144,"output":65536}},"z-ai/glm-4.5":{"limit":{"context":128000,"output":96000}},"z-ai/glm-4.5-air":{"limit":{"context":128000,"output":96000}},"z-ai/glm-4.5-air:free":{"limit":{"context":128000,"output":96000}},"z-ai/glm-4.5v":{"limit":{"context":64000,"output":16384}},"z-ai/glm-4.6":{"limit":{"context":200000,"output":128000}},"z-ai/glm-4.6:exacto":{"limit":{"context":200000,"output":128000}},"z-ai/glm-4.7":{"limit":{"context":204800,"output":131072}},"z-ai/glm-4.7-flash":{"limit":{"context":200000,"output":65535}},"z-ai/glm-5":{"limit":{"context":202752,"output":131000}}}},"ovhcloud":{"id":"ovhcloud","npm":"@ai-sdk/openai-compatible","api":"https://oai.endpoints.kepler.ai.cloud.ovh.net/v1","env":["OVHCLOUD_API_KEY"],"models":{"deepseek-r1-distill-llama-70b":{"limit":{"context":131072,"output":131072}},"gpt-oss-120b":{"limit":{"context":131072,"output":131072}},"gpt-oss-20b":{"limit":{"context":131072,"output":131072}},"llama-3.1-8b-instruct":{"limit":{"context":131072,"output":131072}},"meta-llama-3_3-70b-instruct":{"limit":{"context":131072,"output":131072}},"mistral-7b-instruct-v0.3":{"limit":{"context":65536,"output":65536}},"mistral-nemo-instruct-2407":{"limit":{"context":65536,"output":65536}},"mistral-small-3.2-24b-instruct-2506":{"limit":{"context":131072,"output":131072}},"mixtral-8x7b-instruct-v0.1":{"limit":{"context":32768,"output":32768}},"qwen2.5-coder-32b-instruct":{"limit":{"context":32768,"output":32768}},"qwen2.5-vl-72b-instruct":{"limit":{"context":32768,"output":32768}},"qwen3-32b":{"limit":{"context":32768,"output":32768}},"qwen3-coder-30b-a3b-instruct":{"limit":{"context":262144,"output":262144}}}},"perplexity":{"id":"perplexity","npm":"@ai-sdk/perplexity","api":null,"env":["PERPLEXITY_API_KEY"],"models":{"sonar":{"limit":{"context":128000,"output":4096}},"sonar-pro":{"limit":{"context":200000,"output":8192}},"sonar-reasoning-pro":{"limit":{"context":128000,"output":4096}}}},"poe":{"id":"poe","npm":"@ai-sdk/openai-compatible","api":"https://api.poe.com/v1","env":["POE_API_KEY"],"models":{"anthropic/claude-haiku-3":{"limit":{"context":189096,"output":8192}},"anthropic/claude-haiku-3.5":{"limit":{"context":189096,"output":8192}},"anthropic/claude-haiku-4.5":{"limit":{"context":192000,"output":64000}},"anthropic/claude-opus-4":{"limit":{"context":192512,"output":28672}},"anthropic/claude-opus-4.1":{"limit":{"context":196608,"output":32000}},"anthropic/claude-opus-4.5":{"limit":{"context":196608,"output":64000}},"anthropic/claude-opus-4.6":{"limit":{"context":983040,"output":128000}},"anthropic/claude-sonnet-3.5":{"limit":{"context":189096,"output":8192}},"anthropic/claude-sonnet-3.5-june":{"limit":{"context":189096,"output":8192}},"anthropic/claude-sonnet-3.7":{"limit":{"context":196608,"output":128000}},"anthropic/claude-sonnet-4":{"limit":{"context":983040,"output":64000}},"anthropic/claude-sonnet-4.5":{"limit":{"context":983040,"output":32768}},"cerebras/gpt-oss-120b-cs":{"limit":{"context":0,"output":0}},"cerebras/llama-3.1-8b-cs":{"limit":{"context":0,"output":0}},"cerebras/llama-3.3-70b-cs":{"limit":{"context":0,"output":0}},"cerebras/qwen3-235b-2507-cs":{"limit":{"context":0,"output":0}},"cerebras/qwen3-32b-cs":{"limit":{"context":0,"output":0}},"elevenlabs/elevenlabs-music":{"limit":{"context":2000,"output":0}},"elevenlabs/elevenlabs-v2.5-turbo":{"limit":{"context":128000,"output":0}},"elevenlabs/elevenlabs-v3":{"limit":{"context":128000,"output":0}},"google/gemini-2.0-flash":{"limit":{"context":990000,"output":8192}},"google/gemini-2.0-flash-lite":{"limit":{"context":990000,"output":8192}},"google/gemini-2.5-flash":{"limit":{"context":1065535,"output":65535}},"google/gemini-2.5-flash-lite":{"limit":{"context":1024000,"output":64000}},"google/gemini-2.5-pro":{"limit":{"context":1065535,"output":65535}},"google/gemini-3-flash":{"limit":{"context":1048576,"output":65536}},"google/gemini-3-pro":{"limit":{"context":1048576,"output":65536}},"google/gemini-deep-research":{"limit":{"context":1048576,"output":0}},"google/imagen-3":{"limit":{"context":480,"output":0}},"google/imagen-3-fast":{"limit":{"context":480,"output":0}},"google/imagen-4":{"limit":{"context":480,"output":0}},"google/imagen-4-fast":{"limit":{"context":480,"output":0}},"google/imagen-4-ultra":{"limit":{"context":480,"output":0}},"google/lyria":{"limit":{"context":0,"output":0}},"google/nano-banana":{"limit":{"context":65536,"output":0}},"google/nano-banana-pro":{"limit":{"context":65536,"output":0}},"google/veo-2":{"limit":{"context":480,"output":0}},"google/veo-3":{"limit":{"context":480,"output":0}},"google/veo-3-fast":{"limit":{"context":480,"output":0}},"google/veo-3.1":{"limit":{"context":480,"output":0}},"google/veo-3.1-fast":{"limit":{"context":480,"output":0}},"ideogramai/ideogram":{"limit":{"context":150,"output":0}},"ideogramai/ideogram-v2":{"limit":{"context":150,"output":0}},"ideogramai/ideogram-v2a":{"limit":{"context":150,"output":0}},"ideogramai/ideogram-v2a-turbo":{"limit":{"context":150,"output":0}},"lumalabs/ray2":{"limit":{"context":5000,"output":0}},"novita/glm-4.6":{"limit":{"context":0,"output":0}},"novita/glm-4.6v":{"limit":{"context":131000,"output":32768}},"novita/glm-4.7":{"limit":{"context":205000,"output":131072}},"novita/glm-4.7-flash":{"limit":{"context":200000,"output":65500}},"novita/glm-4.7-n":{"limit":{"context":205000,"output":131072}},"novita/kimi-k2-thinking":{"limit":{"context":256000,"output":0}},"novita/kimi-k2.5":{"limit":{"context":256000,"output":262144}},"novita/minimax-m2.1":{"limit":{"context":205000,"output":131072}},"openai/chatgpt-4o-latest":{"limit":{"context":128000,"output":8192}},"openai/dall-e-3":{"limit":{"context":800,"output":0}},"openai/gpt-3.5-turbo":{"limit":{"context":16384,"output":2048}},"openai/gpt-3.5-turbo-instruct":{"limit":{"context":3500,"output":1024}},"openai/gpt-3.5-turbo-raw":{"limit":{"context":4524,"output":2048}},"openai/gpt-4-classic":{"limit":{"context":8192,"output":4096}},"openai/gpt-4-classic-0314":{"limit":{"context":8192,"output":4096}},"openai/gpt-4-turbo":{"limit":{"context":128000,"output":4096}},"openai/gpt-4.1":{"limit":{"context":1047576,"output":32768}},"openai/gpt-4.1-mini":{"limit":{"context":1047576,"output":32768}},"openai/gpt-4.1-nano":{"limit":{"context":1047576,"output":32768}},"openai/gpt-4o":{"limit":{"context":128000,"output":8192}},"openai/gpt-4o-aug":{"limit":{"context":128000,"output":8192}},"openai/gpt-4o-mini":{"limit":{"context":128000,"output":4096}},"openai/gpt-4o-mini-search":{"limit":{"context":128000,"output":8192}},"openai/gpt-4o-search":{"limit":{"context":128000,"output":8192}},"openai/gpt-5":{"limit":{"context":400000,"output":128000}},"openai/gpt-5-chat":{"limit":{"context":128000,"output":16384}},"openai/gpt-5-codex":{"limit":{"context":400000,"output":128000}},"openai/gpt-5-mini":{"limit":{"context":400000,"output":128000}},"openai/gpt-5-nano":{"limit":{"context":400000,"output":128000}},"openai/gpt-5-pro":{"limit":{"context":400000,"output":128000}},"openai/gpt-5.1":{"limit":{"context":400000,"output":128000}},"openai/gpt-5.1-codex":{"limit":{"context":400000,"output":128000}},"openai/gpt-5.1-codex-max":{"limit":{"context":400000,"output":128000}},"openai/gpt-5.1-codex-mini":{"limit":{"context":400000,"output":128000}},"openai/gpt-5.1-instant":{"limit":{"context":128000,"output":16384}},"openai/gpt-5.2":{"limit":{"context":400000,"output":128000}},"openai/gpt-5.2-codex":{"limit":{"context":400000,"output":128000}},"openai/gpt-5.2-instant":{"limit":{"context":128000,"output":16384}},"openai/gpt-5.2-pro":{"limit":{"context":400000,"output":128000}},"openai/gpt-image-1":{"limit":{"context":128000,"output":0}},"openai/gpt-image-1-mini":{"limit":{"context":0,"output":0}},"openai/gpt-image-1.5":{"limit":{"context":128000,"output":0}},"openai/o1":{"limit":{"context":200000,"output":100000}},"openai/o1-pro":{"limit":{"context":200000,"output":100000}},"openai/o3":{"limit":{"context":200000,"output":100000}},"openai/o3-deep-research":{"limit":{"context":200000,"output":100000}},"openai/o3-mini":{"limit":{"context":200000,"output":100000}},"openai/o3-mini-high":{"limit":{"context":200000,"output":100000}},"openai/o3-pro":{"limit":{"context":200000,"output":100000}},"openai/o4-mini":{"limit":{"context":200000,"output":100000}},"openai/o4-mini-deep-research":{"limit":{"context":200000,"output":100000}},"openai/sora-2":{"limit":{"context":0,"output":0}},"openai/sora-2-pro":{"limit":{"context":0,"output":0}},"poetools/claude-code":{"limit":{"context":0,"output":0}},"runwayml/runway":{"limit":{"context":256,"output":0}},"runwayml/runway-gen-4-turbo":{"limit":{"context":256,"output":0}},"stabilityai/stablediffusionxl":{"limit":{"context":200,"output":0}},"topazlabs-co/topazlabs":{"limit":{"context":204,"output":0}},"trytako/tako":{"limit":{"context":2048,"output":0}},"xai/grok-3":{"limit":{"context":131072,"output":8192}},"xai/grok-3-mini":{"limit":{"context":131072,"output":8192}},"xai/grok-4":{"limit":{"context":256000,"output":128000}},"xai/grok-4-fast-non-reasoning":{"limit":{"context":2000000,"output":128000}},"xai/grok-4-fast-reasoning":{"limit":{"context":2000000,"output":128000}},"xai/grok-4.1-fast-non-reasoning":{"limit":{"context":2000000,"output":30000}},"xai/grok-4.1-fast-reasoning":{"limit":{"context":2000000,"output":30000}},"xai/grok-code-fast-1":{"limit":{"context":256000,"output":128000}}}},"privatemode-ai":{"id":"privatemode-ai","npm":"@ai-sdk/openai-compatible","api":"http://localhost:8080/v1","env":["PRIVATEMODE_API_KEY","PRIVATEMODE_ENDPOINT"],"models":{"gemma-3-27b":{"limit":{"context":128000,"output":8192}},"gpt-oss-120b":{"limit":{"context":128000,"output":128000}},"qwen3-coder-30b-a3b":{"limit":{"context":128000,"output":32768}},"qwen3-embedding-4b":{"limit":{"context":32000,"output":2560}},"whisper-large-v3":{"limit":{"context":0,"output":4096}}}},"requesty":{"id":"requesty","npm":"@ai-sdk/openai-compatible","api":"https://router.requesty.ai/v1","env":["REQUESTY_API_KEY"],"models":{"anthropic/claude-3-7-sonnet":{"limit":{"context":200000,"output":64000}},"anthropic/claude-haiku-4-5":{"limit":{"context":200000,"output":62000}},"anthropic/claude-opus-4":{"limit":{"context":200000,"output":32000}},"anthropic/claude-opus-4-1":{"limit":{"context":200000,"output":32000}},"anthropic/claude-opus-4-5":{"limit":{"context":200000,"output":64000}},"anthropic/claude-sonnet-4":{"limit":{"context":200000,"output":64000}},"anthropic/claude-sonnet-4-5":{"limit":{"context":1000000,"output":64000}},"google/gemini-2.5-flash":{"limit":{"context":1048576,"output":65536}},"google/gemini-2.5-pro":{"limit":{"context":1048576,"output":65536}},"google/gemini-3-flash-preview":{"limit":{"context":1048576,"output":65536}},"google/gemini-3-pro-preview":{"limit":{"context":1048576,"output":65536}},"openai/gpt-4.1":{"limit":{"context":1047576,"output":32768}},"openai/gpt-4.1-mini":{"limit":{"context":1047576,"output":32768}},"openai/gpt-4o-mini":{"limit":{"context":128000,"output":16384}},"openai/gpt-5":{"limit":{"context":400000,"output":128000}},"openai/gpt-5-mini":{"limit":{"context":128000,"output":32000}},"openai/gpt-5-nano":{"limit":{"context":16000,"output":4000}},"openai/o4-mini":{"limit":{"context":200000,"output":100000}},"xai/grok-4":{"limit":{"context":256000,"output":64000}},"xai/grok-4-fast":{"limit":{"context":2000000,"output":64000}}}},"sap-ai-core":{"id":"sap-ai-core","npm":"@jerome-benoit/sap-ai-provider-v2","api":null,"env":["AICORE_SERVICE_KEY"],"models":{"anthropic--claude-3-haiku":{"limit":{"context":200000,"output":4096}},"anthropic--claude-3-opus":{"limit":{"context":200000,"output":4096}},"anthropic--claude-3-sonnet":{"limit":{"context":200000,"output":4096}},"anthropic--claude-3.5-sonnet":{"limit":{"context":200000,"output":8192}},"anthropic--claude-3.7-sonnet":{"limit":{"context":200000,"output":64000}},"anthropic--claude-4-opus":{"limit":{"context":200000,"output":32000}},"anthropic--claude-4-sonnet":{"limit":{"context":200000,"output":64000}},"anthropic--claude-4.5-haiku":{"limit":{"context":200000,"output":64000}},"anthropic--claude-4.5-opus":{"limit":{"context":200000,"output":64000}},"anthropic--claude-4.5-sonnet":{"limit":{"context":200000,"output":64000}},"gemini-2.5-flash":{"limit":{"context":1048576,"output":65536}},"gemini-2.5-pro":{"limit":{"context":1048576,"output":65536}},"gpt-5":{"limit":{"context":400000,"output":128000}},"gpt-5-mini":{"limit":{"context":400000,"output":128000}},"gpt-5-nano":{"limit":{"context":400000,"output":128000}}}},"scaleway":{"id":"scaleway","npm":"@ai-sdk/openai-compatible","api":"https://api.scaleway.ai/v1","env":["SCALEWAY_API_KEY"],"models":{"bge-multilingual-gemma2":{"limit":{"context":8191,"output":3072}},"deepseek-r1-distill-llama-70b":{"limit":{"context":32000,"output":4096}},"devstral-2-123b-instruct-2512":{"limit":{"context":256000,"output":8192}},"gemma-3-27b-it":{"limit":{"context":40000,"output":8192}},"gpt-oss-120b":{"limit":{"context":128000,"output":8192}},"llama-3.1-8b-instruct":{"limit":{"context":128000,"output":16384}},"llama-3.3-70b-instruct":{"limit":{"context":100000,"output":4096}},"mistral-nemo-instruct-2407":{"limit":{"context":128000,"output":8192}},"mistral-small-3.2-24b-instruct-2506":{"limit":{"context":128000,"output":8192}},"pixtral-12b-2409":{"limit":{"context":128000,"output":4096}},"qwen3-235b-a22b-instruct-2507":{"limit":{"context":260000,"output":8192}},"qwen3-coder-30b-a3b-instruct":{"limit":{"context":128000,"output":8192}},"voxtral-small-24b-2507":{"limit":{"context":32000,"output":8192}},"whisper-large-v3":{"limit":{"context":0,"output":4096}}}},"siliconflow":{"id":"siliconflow","npm":"@ai-sdk/openai-compatible","api":"https://api.siliconflow.com/v1","env":["SILICONFLOW_API_KEY"],"models":{"ByteDance-Seed/Seed-OSS-36B-Instruct":{"limit":{"context":262000,"output":262000}},"MiniMaxAI/MiniMax-M2.1":{"limit":{"context":197000,"output":131000}},"Qwen/QwQ-32B":{"limit":{"context":131000,"output":131000}},"Qwen/Qwen2.5-14B-Instruct":{"limit":{"context":33000,"output":4000}},"Qwen/Qwen2.5-32B-Instruct":{"limit":{"context":33000,"output":4000}},"Qwen/Qwen2.5-72B-Instruct":{"limit":{"context":33000,"output":4000}},"Qwen/Qwen2.5-72B-Instruct-128K":{"limit":{"context":131000,"output":4000}},"Qwen/Qwen2.5-7B-Instruct":{"limit":{"context":33000,"output":4000}},"Qwen/Qwen2.5-Coder-32B-Instruct":{"limit":{"context":33000,"output":4000}},"Qwen/Qwen2.5-VL-32B-Instruct":{"limit":{"context":131000,"output":131000}},"Qwen/Qwen2.5-VL-72B-Instruct":{"limit":{"context":131000,"output":4000}},"Qwen/Qwen2.5-VL-7B-Instruct":{"limit":{"context":33000,"output":4000}},"Qwen/Qwen3-14B":{"limit":{"context":131000,"output":131000}},"Qwen/Qwen3-235B-A22B":{"limit":{"context":131000,"output":131000}},"Qwen/Qwen3-235B-A22B-Instruct-2507":{"limit":{"context":262000,"output":262000}},"Qwen/Qwen3-235B-A22B-Thinking-2507":{"limit":{"context":262000,"output":262000}},"Qwen/Qwen3-30B-A3B-Instruct-2507":{"limit":{"context":262000,"output":262000}},"Qwen/Qwen3-30B-A3B-Thinking-2507":{"limit":{"context":262000,"output":131000}},"Qwen/Qwen3-32B":{"limit":{"context":131000,"output":131000}},"Qwen/Qwen3-8B":{"limit":{"context":131000,"output":131000}},"Qwen/Qwen3-Coder-30B-A3B-Instruct":{"limit":{"context":262000,"output":262000}},"Qwen/Qwen3-Coder-480B-A35B-Instruct":{"limit":{"context":262000,"output":262000}},"Qwen/Qwen3-Next-80B-A3B-Instruct":{"limit":{"context":262000,"output":262000}},"Qwen/Qwen3-Next-80B-A3B-Thinking":{"limit":{"context":262000,"output":262000}},"Qwen/Qwen3-Omni-30B-A3B-Captioner":{"limit":{"context":66000,"output":66000}},"Qwen/Qwen3-Omni-30B-A3B-Instruct":{"limit":{"context":66000,"output":66000}},"Qwen/Qwen3-Omni-30B-A3B-Thinking":{"limit":{"context":66000,"output":66000}},"Qwen/Qwen3-VL-235B-A22B-Instruct":{"limit":{"context":262000,"output":262000}},"Qwen/Qwen3-VL-235B-A22B-Thinking":{"limit":{"context":262000,"output":262000}},"Qwen/Qwen3-VL-30B-A3B-Instruct":{"limit":{"context":262000,"output":262000}},"Qwen/Qwen3-VL-30B-A3B-Thinking":{"limit":{"context":262000,"output":262000}},"Qwen/Qwen3-VL-32B-Instruct":{"limit":{"context":262000,"output":262000}},"Qwen/Qwen3-VL-32B-Thinking":{"limit":{"context":262000,"output":262000}},"Qwen/Qwen3-VL-8B-Instruct":{"limit":{"context":262000,"output":262000}},"Qwen/Qwen3-VL-8B-Thinking":{"limit":{"context":262000,"output":262000}},"THUDM/GLM-4-32B-0414":{"limit":{"context":33000,"output":33000}},"THUDM/GLM-4-9B-0414":{"limit":{"context":33000,"output":33000}},"THUDM/GLM-Z1-32B-0414":{"limit":{"context":131000,"output":131000}},"THUDM/GLM-Z1-9B-0414":{"limit":{"context":131000,"output":131000}},"baidu/ERNIE-4.5-300B-A47B":{"limit":{"context":131000,"output":131000}},"deepseek-ai/DeepSeek-R1":{"limit":{"context":164000,"output":164000}},"deepseek-ai/DeepSeek-R1-Distill-Qwen-14B":{"limit":{"context":131000,"output":131000}},"deepseek-ai/DeepSeek-R1-Distill-Qwen-32B":{"limit":{"context":131000,"output":131000}},"deepseek-ai/DeepSeek-V3":{"limit":{"context":164000,"output":164000}},"deepseek-ai/DeepSeek-V3.1":{"limit":{"context":164000,"output":164000}},"deepseek-ai/DeepSeek-V3.1-Terminus":{"limit":{"context":164000,"output":164000}},"deepseek-ai/DeepSeek-V3.2":{"limit":{"context":164000,"output":164000}},"deepseek-ai/DeepSeek-V3.2-Exp":{"limit":{"context":164000,"output":164000}},"deepseek-ai/deepseek-vl2":{"limit":{"context":4000,"output":4000}},"inclusionAI/Ling-flash-2.0":{"limit":{"context":131000,"output":131000}},"inclusionAI/Ling-mini-2.0":{"limit":{"context":131000,"output":131000}},"inclusionAI/Ring-flash-2.0":{"limit":{"context":131000,"output":131000}},"meta-llama/Meta-Llama-3.1-8B-Instruct":{"limit":{"context":33000,"output":4000}},"moonshotai/Kimi-K2-Instruct":{"limit":{"context":131000,"output":131000}},"moonshotai/Kimi-K2-Instruct-0905":{"limit":{"context":262000,"output":262000}},"moonshotai/Kimi-K2-Thinking":{"limit":{"context":262000,"output":262000}},"moonshotai/Kimi-K2.5":{"limit":{"context":262000,"output":262000}},"nex-agi/DeepSeek-V3.1-Nex-N1":{"limit":{"context":131000,"output":131000}},"openai/gpt-oss-120b":{"limit":{"context":131000,"output":8000}},"openai/gpt-oss-20b":{"limit":{"context":131000,"output":8000}},"stepfun-ai/Step-3.5-Flash":{"limit":{"context":262000,"output":262000}},"tencent/Hunyuan-A13B-Instruct":{"limit":{"context":131000,"output":131000}},"tencent/Hunyuan-MT-7B":{"limit":{"context":33000,"output":33000}},"zai-org/GLM-4.5":{"limit":{"context":131000,"output":131000}},"zai-org/GLM-4.5-Air":{"limit":{"context":131000,"output":131000}},"zai-org/GLM-4.5V":{"limit":{"context":66000,"output":66000}},"zai-org/GLM-4.6":{"limit":{"context":205000,"output":205000}},"zai-org/GLM-4.6V":{"limit":{"context":131000,"output":131000}},"zai-org/GLM-4.7":{"limit":{"context":205000,"output":205000}},"zai-org/GLM-5":{"limit":{"context":205000,"output":205000}}}},"siliconflow-cn":{"id":"siliconflow-cn","npm":"@ai-sdk/openai-compatible","api":"https://api.siliconflow.cn/v1","env":["SILICONFLOW_CN_API_KEY"],"models":{"ByteDance-Seed/Seed-OSS-36B-Instruct":{"limit":{"context":262000,"output":262000}},"Kwaipilot/KAT-Dev":{"limit":{"context":128000,"output":128000}},"Pro/MiniMaxAI/MiniMax-M2.1":{"limit":{"context":197000,"output":131000}},"Pro/deepseek-ai/DeepSeek-R1":{"limit":{"context":164000,"output":164000}},"Pro/deepseek-ai/DeepSeek-V3":{"limit":{"context":164000,"output":164000}},"Pro/deepseek-ai/DeepSeek-V3.1-Terminus":{"limit":{"context":164000,"output":164000}},"Pro/deepseek-ai/DeepSeek-V3.2":{"limit":{"context":164000,"output":164000}},"Pro/moonshotai/Kimi-K2-Instruct-0905":{"limit":{"context":262000,"output":262000}},"Pro/moonshotai/Kimi-K2-Thinking":{"limit":{"context":262000,"output":262000}},"Pro/moonshotai/Kimi-K2.5":{"limit":{"context":262000,"output":262000}},"Pro/zai-org/GLM-4.7":{"limit":{"context":205000,"output":205000}},"Pro/zai-org/GLM-5":{"limit":{"context":205000,"output":205000}},"Qwen/QwQ-32B":{"limit":{"context":131000,"output":131000}},"Qwen/Qwen2.5-14B-Instruct":{"limit":{"context":33000,"output":4000}},"Qwen/Qwen2.5-32B-Instruct":{"limit":{"context":33000,"output":4000}},"Qwen/Qwen2.5-72B-Instruct":{"limit":{"context":33000,"output":4000}},"Qwen/Qwen2.5-72B-Instruct-128K":{"limit":{"context":131000,"output":4000}},"Qwen/Qwen2.5-7B-Instruct":{"limit":{"context":33000,"output":4000}},"Qwen/Qwen2.5-Coder-32B-Instruct":{"limit":{"context":33000,"output":4000}},"Qwen/Qwen2.5-VL-32B-Instruct":{"limit":{"context":131000,"output":131000}},"Qwen/Qwen2.5-VL-72B-Instruct":{"limit":{"context":131000,"output":4000}},"Qwen/Qwen3-14B":{"limit":{"context":131000,"output":131000}},"Qwen/Qwen3-235B-A22B-Instruct-2507":{"limit":{"context":262000,"output":262000}},"Qwen/Qwen3-235B-A22B-Thinking-2507":{"limit":{"context":262000,"output":262000}},"Qwen/Qwen3-30B-A3B-Instruct-2507":{"limit":{"context":262000,"output":262000}},"Qwen/Qwen3-30B-A3B-Thinking-2507":{"limit":{"context":262000,"output":131000}},"Qwen/Qwen3-32B":{"limit":{"context":131000,"output":131000}},"Qwen/Qwen3-8B":{"limit":{"context":131000,"output":131000}},"Qwen/Qwen3-Coder-30B-A3B-Instruct":{"limit":{"context":262000,"output":262000}},"Qwen/Qwen3-Coder-480B-A35B-Instruct":{"limit":{"context":262000,"output":262000}},"Qwen/Qwen3-Next-80B-A3B-Instruct":{"limit":{"context":262000,"output":262000}},"Qwen/Qwen3-Next-80B-A3B-Thinking":{"limit":{"context":262000,"output":262000}},"Qwen/Qwen3-Omni-30B-A3B-Captioner":{"limit":{"context":66000,"output":66000}},"Qwen/Qwen3-Omni-30B-A3B-Instruct":{"limit":{"context":66000,"output":66000}},"Qwen/Qwen3-Omni-30B-A3B-Thinking":{"limit":{"context":66000,"output":66000}},"Qwen/Qwen3-VL-235B-A22B-Instruct":{"limit":{"context":262000,"output":262000}},"Qwen/Qwen3-VL-235B-A22B-Thinking":{"limit":{"context":262000,"output":262000}},"Qwen/Qwen3-VL-30B-A3B-Instruct":{"limit":{"context":262000,"output":262000}},"Qwen/Qwen3-VL-30B-A3B-Thinking":{"limit":{"context":262000,"output":262000}},"Qwen/Qwen3-VL-32B-Instruct":{"limit":{"context":262000,"output":262000}},"Qwen/Qwen3-VL-32B-Thinking":{"limit":{"context":262000,"output":262000}},"Qwen/Qwen3-VL-8B-Instruct":{"limit":{"context":262000,"output":262000}},"Qwen/Qwen3-VL-8B-Thinking":{"limit":{"context":262000,"output":262000}},"THUDM/GLM-4-32B-0414":{"limit":{"context":33000,"output":33000}},"THUDM/GLM-4-9B-0414":{"limit":{"context":33000,"output":33000}},"THUDM/GLM-Z1-32B-0414":{"limit":{"context":131000,"output":131000}},"THUDM/GLM-Z1-9B-0414":{"limit":{"context":131000,"output":131000}},"ascend-tribe/pangu-pro-moe":{"limit":{"context":128000,"output":128000}},"baidu/ERNIE-4.5-300B-A47B":{"limit":{"context":131000,"output":131000}},"deepseek-ai/DeepSeek-R1":{"limit":{"context":164000,"output":164000}},"deepseek-ai/DeepSeek-R1-Distill-Qwen-14B":{"limit":{"context":131000,"output":131000}},"deepseek-ai/DeepSeek-R1-Distill-Qwen-32B":{"limit":{"context":131000,"output":131000}},"deepseek-ai/DeepSeek-V3":{"limit":{"context":164000,"output":164000}},"deepseek-ai/DeepSeek-V3.1-Terminus":{"limit":{"context":164000,"output":164000}},"deepseek-ai/DeepSeek-V3.2":{"limit":{"context":164000,"output":164000}},"deepseek-ai/deepseek-vl2":{"limit":{"context":4000,"output":4000}},"inclusionAI/Ling-flash-2.0":{"limit":{"context":131000,"output":131000}},"inclusionAI/Ling-mini-2.0":{"limit":{"context":131000,"output":131000}},"inclusionAI/Ring-flash-2.0":{"limit":{"context":131000,"output":131000}},"moonshotai/Kimi-K2-Instruct-0905":{"limit":{"context":262000,"output":262000}},"moonshotai/Kimi-K2-Thinking":{"limit":{"context":262000,"output":262000}},"stepfun-ai/Step-3.5-Flash":{"limit":{"context":262000,"output":262000}},"tencent/Hunyuan-A13B-Instruct":{"limit":{"context":131000,"output":131000}},"tencent/Hunyuan-MT-7B":{"limit":{"context":33000,"output":33000}},"zai-org/GLM-4.5-Air":{"limit":{"context":131000,"output":131000}},"zai-org/GLM-4.5V":{"limit":{"context":66000,"output":66000}},"zai-org/GLM-4.6":{"limit":{"context":205000,"output":205000}},"zai-org/GLM-4.6V":{"limit":{"context":131000,"output":131000}}}},"stackit":{"id":"stackit","npm":"@ai-sdk/openai-compatible","api":"https://api.openai-compat.model-serving.eu01.onstackit.cloud/v1","env":["STACKIT_API_KEY"],"models":{"Qwen/Qwen3-VL-235B-A22B-Instruct-FP8":{"limit":{"context":218000,"output":8192}},"Qwen/Qwen3-VL-Embedding-8B":{"limit":{"context":32000,"output":4096}},"cortecs/Llama-3.3-70B-Instruct-FP8-Dynamic":{"limit":{"context":128000,"output":8192}},"google/gemma-3-27b-it":{"limit":{"context":37000,"output":8192}},"intfloat/e5-mistral-7b-instruct":{"limit":{"context":4096,"output":4096}},"neuralmagic/Meta-Llama-3.1-8B-Instruct-FP8":{"limit":{"context":128000,"output":8192}},"neuralmagic/Mistral-Nemo-Instruct-2407-FP8":{"limit":{"context":128000,"output":8192}},"openai/gpt-oss-120b":{"limit":{"context":131000,"output":8192}}}},"submodel":{"id":"submodel","npm":"@ai-sdk/openai-compatible","api":"https://llm.submodel.ai/v1","env":["SUBMODEL_INSTAGEN_ACCESS_KEY"],"models":{"Qwen/Qwen3-235B-A22B-Instruct-2507":{"limit":{"context":262144,"output":131072}},"Qwen/Qwen3-235B-A22B-Thinking-2507":{"limit":{"context":262144,"output":131072}},"Qwen/Qwen3-Coder-480B-A35B-Instruct-FP8":{"limit":{"context":262144,"output":262144}},"deepseek-ai/DeepSeek-R1-0528":{"limit":{"context":75000,"output":163840}},"deepseek-ai/DeepSeek-V3-0324":{"limit":{"context":75000,"output":163840}},"deepseek-ai/DeepSeek-V3.1":{"limit":{"context":75000,"output":163840}},"openai/gpt-oss-120b":{"limit":{"context":131072,"output":32768}},"zai-org/GLM-4.5-Air":{"limit":{"context":131072,"output":131072}},"zai-org/GLM-4.5-FP8":{"limit":{"context":131072,"output":131072}}}},"synthetic":{"id":"synthetic","npm":"@ai-sdk/openai-compatible","api":"https://api.synthetic.new/v1","env":["SYNTHETIC_API_KEY"],"models":{"hf:MiniMaxAI/MiniMax-M2":{"limit":{"context":196608,"output":131000}},"hf:MiniMaxAI/MiniMax-M2.1":{"limit":{"context":204800,"output":131072}},"hf:Qwen/Qwen2.5-Coder-32B-Instruct":{"limit":{"context":32768,"output":32768}},"hf:Qwen/Qwen3-235B-A22B-Instruct-2507":{"limit":{"context":256000,"output":32000}},"hf:Qwen/Qwen3-235B-A22B-Thinking-2507":{"limit":{"context":256000,"output":32000}},"hf:Qwen/Qwen3-Coder-480B-A35B-Instruct":{"limit":{"context":256000,"output":32000}},"hf:deepseek-ai/DeepSeek-R1":{"limit":{"context":128000,"output":128000}},"hf:deepseek-ai/DeepSeek-R1-0528":{"limit":{"context":128000,"output":128000}},"hf:deepseek-ai/DeepSeek-V3":{"limit":{"context":128000,"output":128000}},"hf:deepseek-ai/DeepSeek-V3-0324":{"limit":{"context":128000,"output":128000}},"hf:deepseek-ai/DeepSeek-V3.1":{"limit":{"context":128000,"output":128000}},"hf:deepseek-ai/DeepSeek-V3.1-Terminus":{"limit":{"context":128000,"output":128000}},"hf:deepseek-ai/DeepSeek-V3.2":{"limit":{"context":162816,"output":8000}},"hf:meta-llama/Llama-3.1-405B-Instruct":{"limit":{"context":128000,"output":32768}},"hf:meta-llama/Llama-3.1-70B-Instruct":{"limit":{"context":128000,"output":32768}},"hf:meta-llama/Llama-3.1-8B-Instruct":{"limit":{"context":128000,"output":32768}},"hf:meta-llama/Llama-3.3-70B-Instruct":{"limit":{"context":128000,"output":32768}},"hf:meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8":{"limit":{"context":524000,"output":4096}},"hf:meta-llama/Llama-4-Scout-17B-16E-Instruct":{"limit":{"context":328000,"output":4096}},"hf:moonshotai/Kimi-K2-Instruct-0905":{"limit":{"context":262144,"output":32768}},"hf:moonshotai/Kimi-K2-Thinking":{"limit":{"context":262144,"output":262144}},"hf:moonshotai/Kimi-K2.5":{"limit":{"context":262144,"output":65536}},"hf:nvidia/Kimi-K2.5-NVFP4":{"limit":{"context":262144,"output":65536}},"hf:openai/gpt-oss-120b":{"limit":{"context":128000,"output":32768}},"hf:zai-org/GLM-4.6":{"limit":{"context":200000,"output":64000}},"hf:zai-org/GLM-4.7":{"limit":{"context":200000,"output":64000}}}},"togetherai":{"id":"togetherai","npm":"@ai-sdk/togetherai","api":null,"env":["TOGETHER_API_KEY"],"models":{"Qwen/Qwen3-235B-A22B-Instruct-2507-tput":{"limit":{"context":262144,"output":262144}},"Qwen/Qwen3-Coder-480B-A35B-Instruct-FP8":{"limit":{"context":262144,"output":262144}},"Qwen/Qwen3-Coder-Next-FP8":{"limit":{"context":262144,"output":262144}},"Qwen/Qwen3-Next-80B-A3B-Instruct":{"limit":{"context":262144,"output":262144}},"deepseek-ai/DeepSeek-R1":{"limit":{"context":163839,"output":163839}},"deepseek-ai/DeepSeek-V3":{"limit":{"context":131072,"output":131072}},"deepseek-ai/DeepSeek-V3-1":{"limit":{"context":131072,"output":131072}},"essentialai/Rnj-1-Instruct":{"limit":{"context":32768,"output":32768}},"meta-llama/Llama-3.3-70B-Instruct-Turbo":{"limit":{"context":131072,"output":131072}},"moonshotai/Kimi-K2-Instruct":{"limit":{"context":131072,"output":131072}},"moonshotai/Kimi-K2-Instruct-0905":{"limit":{"context":262144,"output":262144}},"moonshotai/Kimi-K2-Thinking":{"limit":{"context":262144,"output":262144}},"moonshotai/Kimi-K2.5":{"limit":{"context":262144,"output":262144}},"openai/gpt-oss-120b":{"limit":{"context":131072,"output":131072}},"zai-org/GLM-4.6":{"limit":{"context":200000,"output":200000}},"zai-org/GLM-4.7":{"limit":{"context":200000,"output":200000}},"zai-org/GLM-5":{"limit":{"context":202752,"output":131072}}}},"upstage":{"id":"upstage","npm":"@ai-sdk/openai-compatible","api":"https://api.upstage.ai/v1/solar","env":["UPSTAGE_API_KEY"],"models":{"solar-mini":{"limit":{"context":32768,"output":4096}},"solar-pro2":{"limit":{"context":65536,"output":8192}},"solar-pro3":{"limit":{"context":131072,"output":8192}}}},"v0":{"id":"v0","npm":"@ai-sdk/vercel","api":null,"env":["V0_API_KEY"],"models":{"v0-1.0-md":{"limit":{"context":128000,"output":32000}},"v0-1.5-lg":{"limit":{"context":512000,"output":32000}},"v0-1.5-md":{"limit":{"context":128000,"output":32000}}}},"venice":{"id":"venice","npm":"venice-ai-sdk-provider","api":null,"env":["VENICE_API_KEY"],"models":{"claude-opus-4-6":{"limit":{"context":1000000,"output":128000}},"claude-opus-45":{"limit":{"context":198000,"output":49500}},"claude-sonnet-45":{"limit":{"context":198000,"output":49500}},"deepseek-v3.2":{"limit":{"context":160000,"output":40000}},"gemini-3-flash-preview":{"limit":{"context":256000,"output":64000}},"gemini-3-pro-preview":{"limit":{"context":198000,"output":49500}},"google-gemma-3-27b-it":{"limit":{"context":198000,"output":49500}},"grok-41-fast":{"limit":{"context":256000,"output":64000}},"grok-code-fast-1":{"limit":{"context":256000,"output":64000}},"hermes-3-llama-3.1-405b":{"limit":{"context":128000,"output":32000}},"kimi-k2-5":{"limit":{"context":256000,"output":64000}},"kimi-k2-thinking":{"limit":{"context":256000,"output":64000}},"llama-3.2-3b":{"limit":{"context":128000,"output":32000}},"llama-3.3-70b":{"limit":{"context":128000,"output":32000}},"minimax-m21":{"limit":{"context":198000,"output":49500}},"minimax-m25":{"limit":{"context":198000,"output":32000}},"mistral-31-24b":{"limit":{"context":128000,"output":32000}},"openai-gpt-52":{"limit":{"context":256000,"output":64000}},"openai-gpt-52-codex":{"limit":{"context":256000,"output":64000}},"openai-gpt-oss-120b":{"limit":{"context":128000,"output":32000}},"qwen3-235b-a22b-instruct-2507":{"limit":{"context":128000,"output":32000}},"qwen3-235b-a22b-thinking-2507":{"limit":{"context":128000,"output":32000}},"qwen3-4b":{"limit":{"context":32000,"output":8000}},"qwen3-coder-480b-a35b-instruct":{"limit":{"context":256000,"output":64000}},"qwen3-next-80b":{"limit":{"context":256000,"output":64000}},"qwen3-vl-235b-a22b":{"limit":{"context":256000,"output":64000}},"venice-uncensored":{"limit":{"context":32000,"output":8000}},"zai-org-glm-4.7":{"limit":{"context":198000,"output":49500}},"zai-org-glm-4.7-flash":{"limit":{"context":128000,"output":32000}},"zai-org-glm-5":{"limit":{"context":198000,"output":49500}}}},"vercel":{"id":"vercel","npm":"@ai-sdk/gateway","api":null,"env":["AI_GATEWAY_API_KEY"],"models":{"alibaba/qwen-3-14b":{"limit":{"context":40960,"output":16384}},"alibaba/qwen-3-235b":{"limit":{"context":40960,"output":16384}},"alibaba/qwen-3-30b":{"limit":{"context":40960,"output":16384}},"alibaba/qwen-3-32b":{"limit":{"context":40960,"output":16384}},"alibaba/qwen3-235b-a22b-thinking":{"limit":{"context":262114,"output":262114}},"alibaba/qwen3-coder":{"limit":{"context":262144,"output":66536}},"alibaba/qwen3-coder-30b-a3b":{"limit":{"context":160000,"output":32768}},"alibaba/qwen3-coder-plus":{"limit":{"context":1000000,"output":1000000}},"alibaba/qwen3-embedding-0.6b":{"limit":{"context":32768,"output":32768}},"alibaba/qwen3-embedding-4b":{"limit":{"context":32768,"output":32768}},"alibaba/qwen3-embedding-8b":{"limit":{"context":32768,"output":32768}},"alibaba/qwen3-max":{"limit":{"context":262144,"output":32768}},"alibaba/qwen3-max-preview":{"limit":{"context":262144,"output":32768}},"alibaba/qwen3-max-thinking":{"limit":{"context":256000,"output":65536}},"alibaba/qwen3-next-80b-a3b-instruct":{"limit":{"context":262144,"output":32768}},"alibaba/qwen3-next-80b-a3b-thinking":{"limit":{"context":131072,"output":65536}},"alibaba/qwen3-vl-instruct":{"limit":{"context":131072,"output":129024}},"alibaba/qwen3-vl-thinking":{"limit":{"context":131072,"output":129024}},"amazon/nova-2-lite":{"limit":{"context":1000000,"output":1000000}},"amazon/nova-lite":{"limit":{"context":300000,"output":8192}},"amazon/nova-micro":{"limit":{"context":128000,"output":8192}},"amazon/nova-pro":{"limit":{"context":300000,"output":8192}},"amazon/titan-embed-text-v2":{"limit":{"context":8192,"output":1536}},"anthropic/claude-3-haiku":{"limit":{"context":200000,"output":4096}},"anthropic/claude-3-opus":{"limit":{"context":200000,"output":4096}},"anthropic/claude-3.5-haiku":{"limit":{"context":200000,"output":8192}},"anthropic/claude-3.5-sonnet":{"limit":{"context":200000,"output":8192}},"anthropic/claude-3.5-sonnet-20240620":{"limit":{"context":200000,"output":8192}},"anthropic/claude-3.7-sonnet":{"limit":{"context":200000,"output":64000}},"anthropic/claude-haiku-4.5":{"limit":{"context":200000,"output":64000}},"anthropic/claude-opus-4":{"limit":{"context":200000,"output":32000}},"anthropic/claude-opus-4.1":{"limit":{"context":200000,"output":32000}},"anthropic/claude-opus-4.5":{"limit":{"context":200000,"output":64000}},"anthropic/claude-opus-4.6":{"limit":{"context":1000000,"output":128000}},"anthropic/claude-sonnet-4":{"limit":{"context":200000,"output":64000}},"anthropic/claude-sonnet-4.5":{"limit":{"context":200000,"output":64000}},"arcee-ai/trinity-large-preview":{"limit":{"context":131000,"output":131000}},"arcee-ai/trinity-mini":{"limit":{"context":131072,"output":131072}},"bfl/flux-kontext-max":{"limit":{"context":512,"output":0}},"bfl/flux-kontext-pro":{"limit":{"context":512,"output":0}},"bfl/flux-pro-1.0-fill":{"limit":{"context":512,"output":0}},"bfl/flux-pro-1.1":{"limit":{"context":512,"output":0}},"bfl/flux-pro-1.1-ultra":{"limit":{"context":512,"output":0}},"bytedance/seed-1.6":{"limit":{"context":256000,"output":32000}},"bytedance/seed-1.8":{"limit":{"context":256000,"output":64000}},"cohere/command-a":{"limit":{"context":256000,"output":8000}},"cohere/embed-v4.0":{"limit":{"context":8192,"output":1536}},"deepseek/deepseek-r1":{"limit":{"context":128000,"output":32768}},"deepseek/deepseek-v3":{"limit":{"context":163840,"output":16384}},"deepseek/deepseek-v3.1":{"limit":{"context":163840,"output":128000}},"deepseek/deepseek-v3.1-terminus":{"limit":{"context":131072,"output":65536}},"deepseek/deepseek-v3.2":{"limit":{"context":163842,"output":8000}},"deepseek/deepseek-v3.2-exp":{"limit":{"context":163840,"output":163840}},"deepseek/deepseek-v3.2-thinking":{"limit":{"context":128000,"output":64000}},"google/gemini-2.0-flash":{"limit":{"context":1048576,"output":8192}},"google/gemini-2.0-flash-lite":{"limit":{"context":1048576,"output":8192}},"google/gemini-2.5-flash":{"limit":{"context":1048576,"output":65536}},"google/gemini-2.5-flash-image":{"limit":{"context":32768,"output":32768}},"google/gemini-2.5-flash-image-preview":{"limit":{"context":32768,"output":32768}},"google/gemini-2.5-flash-lite":{"limit":{"context":1048576,"output":65536}},"google/gemini-2.5-flash-lite-preview-09-2025":{"limit":{"context":1048576,"output":65536}},"google/gemini-2.5-flash-preview-09-2025":{"limit":{"context":1048576,"output":65536}},"google/gemini-2.5-pro":{"limit":{"context":1048576,"output":65536}},"google/gemini-3-flash":{"limit":{"context":1000000,"output":64000}},"google/gemini-3-pro-image":{"limit":{"context":65536,"output":32768}},"google/gemini-3-pro-preview":{"limit":{"context":1000000,"output":64000}},"google/gemini-embedding-001":{"limit":{"context":8192,"output":1536}},"google/imagen-4.0-fast-generate-001":{"limit":{"context":480,"output":0}},"google/imagen-4.0-generate-001":{"limit":{"context":480,"output":0}},"google/imagen-4.0-ultra-generate-001":{"limit":{"context":480,"output":0}},"google/text-embedding-005":{"limit":{"context":8192,"output":1536}},"google/text-multilingual-embedding-002":{"limit":{"context":8192,"output":1536}},"inception/mercury-coder-small":{"limit":{"context":32000,"output":16384}},"kwaipilot/kat-coder-pro-v1":{"limit":{"context":256000,"output":32000}},"meituan/longcat-flash-chat":{"limit":{"context":128000,"output":8192}},"meituan/longcat-flash-thinking":{"limit":{"context":128000,"output":8192}},"meta/llama-3.1-70b":{"limit":{"context":131072,"output":16384}},"meta/llama-3.1-8b":{"limit":{"context":131072,"output":16384}},"meta/llama-3.2-11b":{"limit":{"context":128000,"output":8192}},"meta/llama-3.2-1b":{"limit":{"context":128000,"output":8192}},"meta/llama-3.2-3b":{"limit":{"context":128000,"output":8192}},"meta/llama-3.2-90b":{"limit":{"context":128000,"output":8192}},"meta/llama-3.3-70b":{"limit":{"context":128000,"output":4096}},"meta/llama-4-maverick":{"limit":{"context":128000,"output":4096}},"meta/llama-4-scout":{"limit":{"context":128000,"output":4096}},"minimax/minimax-m2":{"limit":{"context":262114,"output":262114}},"minimax/minimax-m2.1":{"limit":{"context":204800,"output":131072}},"minimax/minimax-m2.1-lightning":{"limit":{"context":204800,"output":131072}},"mistral/codestral":{"limit":{"context":256000,"output":4096}},"mistral/codestral-embed":{"limit":{"context":8192,"output":1536}},"mistral/devstral-2":{"limit":{"context":256000,"output":256000}},"mistral/devstral-small":{"limit":{"context":128000,"output":64000}},"mistral/devstral-small-2":{"limit":{"context":256000,"output":256000}},"mistral/magistral-medium":{"limit":{"context":128000,"output":16384}},"mistral/magistral-small":{"limit":{"context":128000,"output":128000}},"mistral/ministral-14b":{"limit":{"context":256000,"output":256000}},"mistral/ministral-3b":{"limit":{"context":128000,"output":128000}},"mistral/ministral-8b":{"limit":{"context":128000,"output":128000}},"mistral/mistral-embed":{"limit":{"context":8192,"output":1536}},"mistral/mistral-large-3":{"limit":{"context":256000,"output":256000}},"mistral/mistral-medium":{"limit":{"context":128000,"output":64000}},"mistral/mistral-nemo":{"limit":{"context":60288,"output":16000}},"mistral/mistral-small":{"limit":{"context":128000,"output":16384}},"mistral/mixtral-8x22b-instruct":{"limit":{"context":64000,"output":64000}},"mistral/pixtral-12b":{"limit":{"context":128000,"output":128000}},"mistral/pixtral-large":{"limit":{"context":128000,"output":128000}},"moonshotai/kimi-k2":{"limit":{"context":131072,"output":16384}},"moonshotai/kimi-k2-0905":{"limit":{"context":131072,"output":16384}},"moonshotai/kimi-k2-thinking":{"limit":{"context":216144,"output":216144}},"moonshotai/kimi-k2-thinking-turbo":{"limit":{"context":262114,"output":262114}},"moonshotai/kimi-k2-turbo":{"limit":{"context":256000,"output":16384}},"moonshotai/kimi-k2.5":{"limit":{"context":262144,"output":262144}},"morph/morph-v3-fast":{"limit":{"context":16000,"output":16000}},"morph/morph-v3-large":{"limit":{"context":32000,"output":32000}},"nvidia/nemotron-3-nano-30b-a3b":{"limit":{"context":262144,"output":262144}},"nvidia/nemotron-nano-12b-v2-vl":{"limit":{"context":131072,"output":131072}},"nvidia/nemotron-nano-9b-v2":{"limit":{"context":131072,"output":131072}},"openai/codex-mini":{"limit":{"context":200000,"output":100000}},"openai/gpt-3.5-turbo":{"limit":{"context":16385,"output":4096}},"openai/gpt-3.5-turbo-instruct":{"limit":{"context":8192,"output":4096}},"openai/gpt-4-turbo":{"limit":{"context":128000,"output":4096}},"openai/gpt-4.1":{"limit":{"context":1047576,"output":32768}},"openai/gpt-4.1-mini":{"limit":{"context":1047576,"output":32768}},"openai/gpt-4.1-nano":{"limit":{"context":1047576,"output":32768}},"openai/gpt-4o":{"limit":{"context":128000,"output":16384}},"openai/gpt-4o-mini":{"limit":{"context":128000,"output":16384}},"openai/gpt-4o-mini-search-preview":{"limit":{"context":128000,"output":16384}},"openai/gpt-5":{"limit":{"context":400000,"output":128000}},"openai/gpt-5-chat":{"limit":{"context":128000,"output":16384}},"openai/gpt-5-codex":{"limit":{"context":400000,"output":128000}},"openai/gpt-5-mini":{"limit":{"context":400000,"output":128000}},"openai/gpt-5-nano":{"limit":{"context":400000,"output":128000}},"openai/gpt-5-pro":{"limit":{"context":400000,"output":272000}},"openai/gpt-5.1-codex":{"limit":{"context":400000,"output":128000}},"openai/gpt-5.1-codex-max":{"limit":{"context":400000,"output":128000}},"openai/gpt-5.1-codex-mini":{"limit":{"context":400000,"output":128000}},"openai/gpt-5.1-instant":{"limit":{"context":128000,"output":16384}},"openai/gpt-5.1-thinking":{"limit":{"context":400000,"output":128000}},"openai/gpt-5.2":{"limit":{"context":400000,"output":128000}},"openai/gpt-5.2-chat":{"limit":{"context":128000,"output":16384}},"openai/gpt-5.2-codex":{"limit":{"context":400000,"output":128000}},"openai/gpt-5.2-pro":{"limit":{"context":400000,"output":128000}},"openai/gpt-oss-120b":{"limit":{"context":131072,"output":131072}},"openai/gpt-oss-20b":{"limit":{"context":131072,"output":32768}},"openai/gpt-oss-safeguard-20b":{"limit":{"context":131072,"output":65536}},"openai/o1":{"limit":{"context":200000,"output":100000}},"openai/o3":{"limit":{"context":200000,"output":100000}},"openai/o3-deep-research":{"limit":{"context":200000,"output":100000}},"openai/o3-mini":{"limit":{"context":200000,"output":100000}},"openai/o3-pro":{"limit":{"context":200000,"output":100000}},"openai/o4-mini":{"limit":{"context":200000,"output":100000}},"openai/text-embedding-3-large":{"limit":{"context":8192,"output":1536}},"openai/text-embedding-3-small":{"limit":{"context":8192,"output":1536}},"openai/text-embedding-ada-002":{"limit":{"context":8192,"output":1536}},"perplexity/sonar":{"limit":{"context":127000,"output":8000}},"perplexity/sonar-pro":{"limit":{"context":200000,"output":8000}},"perplexity/sonar-reasoning":{"limit":{"context":127000,"output":8000}},"perplexity/sonar-reasoning-pro":{"limit":{"context":127000,"output":8000}},"prime-intellect/intellect-3":{"limit":{"context":131072,"output":131072}},"recraft/recraft-v2":{"limit":{"context":512,"output":0}},"recraft/recraft-v3":{"limit":{"context":512,"output":0}},"vercel/v0-1.0-md":{"limit":{"context":128000,"output":32000}},"vercel/v0-1.5-md":{"limit":{"context":128000,"output":32000}},"voyage/voyage-3-large":{"limit":{"context":8192,"output":1536}},"voyage/voyage-3.5":{"limit":{"context":8192,"output":1536}},"voyage/voyage-3.5-lite":{"limit":{"context":8192,"output":1536}},"voyage/voyage-code-2":{"limit":{"context":8192,"output":1536}},"voyage/voyage-code-3":{"limit":{"context":8192,"output":1536}},"voyage/voyage-finance-2":{"limit":{"context":8192,"output":1536}},"voyage/voyage-law-2":{"limit":{"context":8192,"output":1536}},"xai/grok-2-vision":{"limit":{"context":8192,"output":4096}},"xai/grok-3":{"limit":{"context":131072,"output":8192}},"xai/grok-3-fast":{"limit":{"context":131072,"output":8192}},"xai/grok-3-mini":{"limit":{"context":131072,"output":8192}},"xai/grok-3-mini-fast":{"limit":{"context":131072,"output":8192}},"xai/grok-4":{"limit":{"context":256000,"output":64000}},"xai/grok-4-fast-non-reasoning":{"limit":{"context":2000000,"output":30000}},"xai/grok-4-fast-reasoning":{"limit":{"context":2000000,"output":256000}},"xai/grok-4.1-fast-non-reasoning":{"limit":{"context":2000000,"output":30000}},"xai/grok-4.1-fast-reasoning":{"limit":{"context":2000000,"output":30000}},"xai/grok-code-fast-1":{"limit":{"context":256000,"output":10000}},"xiaomi/mimo-v2-flash":{"limit":{"context":262144,"output":32000}},"zai/glm-4.5":{"limit":{"context":131072,"output":131072}},"zai/glm-4.5-air":{"limit":{"context":128000,"output":96000}},"zai/glm-4.5v":{"limit":{"context":66000,"output":66000}},"zai/glm-4.6":{"limit":{"context":200000,"output":96000}},"zai/glm-4.6v":{"limit":{"context":128000,"output":24000}},"zai/glm-4.6v-flash":{"limit":{"context":128000,"output":24000}},"zai/glm-4.7":{"limit":{"context":202752,"output":120000}},"zai/glm-4.7-flashx":{"limit":{"context":200000,"output":128000}}}},"vivgrid":{"id":"vivgrid","npm":"@ai-sdk/openai","api":"https://api.vivgrid.com/v1","env":["VIVGRID_API_KEY"],"models":{"gemini-3-flash-preview":{"limit":{"context":1048576,"output":65536}},"gemini-3-pro-preview":{"limit":{"context":1048576,"output":65536}},"gpt-5.1-codex":{"limit":{"context":400000,"output":128000}},"gpt-5.1-codex-max":{"limit":{"context":400000,"output":128000}},"gpt-5.2-codex":{"limit":{"context":400000,"output":128000}}}},"vultr":{"id":"vultr","npm":"@ai-sdk/openai-compatible","api":"https://api.vultrinference.com/v1","env":["VULTR_API_KEY"],"models":{"deepseek-r1-distill-llama-70b":{"limit":{"context":121808,"output":8192}},"deepseek-r1-distill-qwen-32b":{"limit":{"context":121808,"output":8192}},"gpt-oss-120b":{"limit":{"context":121808,"output":8192}},"kimi-k2-instruct":{"limit":{"context":58904,"output":4096}},"qwen2.5-coder-32b-instruct":{"limit":{"context":12952,"output":2048}}}},"wandb":{"id":"wandb","npm":"@ai-sdk/openai-compatible","api":"https://api.inference.wandb.ai/v1","env":["WANDB_API_KEY"],"models":{"Qwen/Qwen3-235B-A22B-Instruct-2507":{"limit":{"context":262144,"output":131072}},"Qwen/Qwen3-235B-A22B-Thinking-2507":{"limit":{"context":262144,"output":131072}},"Qwen/Qwen3-Coder-480B-A35B-Instruct":{"limit":{"context":262144,"output":66536}},"deepseek-ai/DeepSeek-R1-0528":{"limit":{"context":161000,"output":163840}},"deepseek-ai/DeepSeek-V3-0324":{"limit":{"context":161000,"output":8192}},"meta-llama/Llama-3.1-8B-Instruct":{"limit":{"context":128000,"output":32768}},"meta-llama/Llama-3.3-70B-Instruct":{"limit":{"context":128000,"output":32768}},"meta-llama/Llama-4-Scout-17B-16E-Instruct":{"limit":{"context":64000,"output":8192}},"microsoft/Phi-4-mini-instruct":{"limit":{"context":128000,"output":4096}},"moonshotai/Kimi-K2-Instruct":{"limit":{"context":128000,"output":16384}}}},"xai":{"id":"xai","npm":"@ai-sdk/xai","api":null,"env":["XAI_API_KEY"],"models":{"grok-2":{"limit":{"context":131072,"output":8192}},"grok-2-1212":{"limit":{"context":131072,"output":8192}},"grok-2-latest":{"limit":{"context":131072,"output":8192}},"grok-2-vision":{"limit":{"context":8192,"output":4096}},"grok-2-vision-1212":{"limit":{"context":8192,"output":4096}},"grok-2-vision-latest":{"limit":{"context":8192,"output":4096}},"grok-3":{"limit":{"context":131072,"output":8192}},"grok-3-fast":{"limit":{"context":131072,"output":8192}},"grok-3-fast-latest":{"limit":{"context":131072,"output":8192}},"grok-3-latest":{"limit":{"context":131072,"output":8192}},"grok-3-mini":{"limit":{"context":131072,"output":8192}},"grok-3-mini-fast":{"limit":{"context":131072,"output":8192}},"grok-3-mini-fast-latest":{"limit":{"context":131072,"output":8192}},"grok-3-mini-latest":{"limit":{"context":131072,"output":8192}},"grok-4":{"limit":{"context":256000,"output":64000}},"grok-4-1-fast":{"limit":{"context":2000000,"output":30000}},"grok-4-1-fast-non-reasoning":{"limit":{"context":2000000,"output":30000}},"grok-4-fast":{"limit":{"context":2000000,"output":30000}},"grok-4-fast-non-reasoning":{"limit":{"context":2000000,"output":30000}},"grok-beta":{"limit":{"context":131072,"output":4096}},"grok-code-fast-1":{"limit":{"context":256000,"output":10000}},"grok-vision-beta":{"limit":{"context":8192,"output":4096}}}},"xiaomi":{"id":"xiaomi","npm":"@ai-sdk/openai-compatible","api":"https://api.xiaomimimo.com/v1","env":["XIAOMI_API_KEY"],"models":{"mimo-v2-flash":{"limit":{"context":256000,"output":32000}}}},"zai":{"id":"zai","npm":"@ai-sdk/openai-compatible","api":"https://api.z.ai/api/paas/v4","env":["ZHIPU_API_KEY"],"models":{"glm-4.5":{"limit":{"context":131072,"output":98304}},"glm-4.5-air":{"limit":{"context":131072,"output":98304}},"glm-4.5-flash":{"limit":{"context":131072,"output":98304}},"glm-4.5v":{"limit":{"context":64000,"output":16384}},"glm-4.6":{"limit":{"context":204800,"output":131072}},"glm-4.6v":{"limit":{"context":128000,"output":32768}},"glm-4.7":{"limit":{"context":204800,"output":131072}},"glm-4.7-flash":{"limit":{"context":200000,"output":131072}},"glm-5":{"limit":{"context":204800,"output":131072}}}},"zai-coding-plan":{"id":"zai-coding-plan","npm":"@ai-sdk/openai-compatible","api":"https://api.z.ai/api/coding/paas/v4","env":["ZHIPU_API_KEY"],"models":{"glm-4.5":{"limit":{"context":131072,"output":98304}},"glm-4.5-air":{"limit":{"context":131072,"output":98304}},"glm-4.5-flash":{"limit":{"context":131072,"output":98304}},"glm-4.5v":{"limit":{"context":64000,"output":16384}},"glm-4.6":{"limit":{"context":204800,"output":131072}},"glm-4.6v":{"limit":{"context":128000,"output":32768}},"glm-4.7":{"limit":{"context":204800,"output":131072}},"glm-4.7-flash":{"limit":{"context":200000,"output":131072}},"glm-5":{"limit":{"context":204800,"output":131072}}}},"zenmux":{"id":"zenmux","npm":"@ai-sdk/anthropic","api":"https://zenmux.ai/api/anthropic/v1","env":["ZENMUX_API_KEY"],"models":{"anthropic/claude-3.5-haiku":{"limit":{"context":200000,"output":64000}},"anthropic/claude-3.5-sonnet":{"limit":{"context":200000,"output":64000}},"anthropic/claude-3.7-sonnet":{"limit":{"context":200000,"output":64000}},"anthropic/claude-haiku-4.5":{"limit":{"context":200000,"output":64000}},"anthropic/claude-opus-4":{"limit":{"context":200000,"output":64000}},"anthropic/claude-opus-4.1":{"limit":{"context":200000,"output":64000}},"anthropic/claude-opus-4.5":{"limit":{"context":200000,"output":64000}},"anthropic/claude-opus-4.6":{"limit":{"context":1000000,"output":128000}},"anthropic/claude-sonnet-4":{"limit":{"context":1000000,"output":64000}},"anthropic/claude-sonnet-4.5":{"limit":{"context":1000000,"output":64000}},"baidu/ernie-5.0-thinking-preview":{"limit":{"context":128000,"output":64000}},"deepseek/deepseek-chat":{"limit":{"context":128000,"output":64000}},"deepseek/deepseek-v3.2":{"limit":{"context":128000,"output":64000}},"deepseek/deepseek-v3.2-exp":{"limit":{"context":163000,"output":64000}},"google/gemini-2.5-flash":{"limit":{"context":1048000,"output":64000}},"google/gemini-2.5-flash-lite":{"limit":{"context":1048000,"output":64000}},"google/gemini-2.5-pro":{"limit":{"context":1048000,"output":64000}},"google/gemini-3-flash-preview":{"limit":{"context":1048000,"output":64000}},"google/gemini-3-pro-preview":{"limit":{"context":1048000,"output":64000}},"inclusionai/ling-1t":{"limit":{"context":128000,"output":64000}},"inclusionai/ring-1t":{"limit":{"context":128000,"output":64000}},"kuaishou/kat-coder-pro-v1":{"limit":{"context":256000,"output":64000}},"kuaishou/kat-coder-pro-v1-free":{"limit":{"context":256000,"output":64000}},"minimax/minimax-m2":{"limit":{"context":204000,"output":64000}},"minimax/minimax-m2.1":{"limit":{"context":204000,"output":64000}},"minimax/minimax-m2.5":{"limit":{"context":204800,"output":131072}},"minimax/minimax-m2.5-lightning":{"limit":{"context":204800,"output":131072}},"moonshotai/kimi-k2-0905":{"limit":{"context":262000,"output":64000}},"moonshotai/kimi-k2-thinking":{"limit":{"context":262000,"output":64000}},"moonshotai/kimi-k2-thinking-turbo":{"limit":{"context":262000,"output":64000}},"moonshotai/kimi-k2.5":{"limit":{"context":262000,"output":64000}},"openai/gpt-5":{"limit":{"context":400000,"output":64000}},"openai/gpt-5-codex":{"limit":{"context":400000,"output":64000}},"openai/gpt-5.1":{"limit":{"context":400000,"output":64000}},"openai/gpt-5.1-chat":{"limit":{"context":128000,"output":64000}},"openai/gpt-5.1-codex":{"limit":{"context":400000,"output":64000}},"openai/gpt-5.1-codex-mini":{"limit":{"context":400000,"output":64000}},"openai/gpt-5.2":{"limit":{"context":400000,"output":64000}},"openai/gpt-5.2-codex":{"limit":{"context":400000,"output":64000}},"qwen/qwen3-coder-plus":{"limit":{"context":1000000,"output":64000}},"qwen/qwen3-max":{"limit":{"context":256000,"output":64000}},"stepfun/step-3":{"limit":{"context":65536,"output":64000}},"stepfun/step-3.5-flash":{"limit":{"context":256000,"output":64000}},"stepfun/step-3.5-flash-free":{"limit":{"context":256000,"output":64000}},"volcengine/doubao-seed-1.8":{"limit":{"context":256000,"output":64000}},"volcengine/doubao-seed-code":{"limit":{"context":256000,"output":64000}},"x-ai/grok-4":{"limit":{"context":256000,"output":64000}},"x-ai/grok-4-fast":{"limit":{"context":2000000,"output":64000}},"x-ai/grok-4.1-fast":{"limit":{"context":2000000,"output":64000}},"x-ai/grok-4.1-fast-non-reasoning":{"limit":{"context":2000000,"output":64000}},"x-ai/grok-code-fast-1":{"limit":{"context":256000,"output":64000}},"xiaomi/mimo-v2-flash":{"limit":{"context":262000,"output":64000}},"xiaomi/mimo-v2-flash-free":{"limit":{"context":262000,"output":64000}},"z-ai/glm-4.5":{"limit":{"context":128000,"output":64000}},"z-ai/glm-4.5-air":{"limit":{"context":128000,"output":64000}},"z-ai/glm-4.6":{"limit":{"context":200000,"output":64000}},"z-ai/glm-4.6v":{"limit":{"context":200000,"output":64000}},"z-ai/glm-4.6v-flash":{"limit":{"context":200000,"output":64000}},"z-ai/glm-4.6v-flash-free":{"limit":{"context":200000,"output":64000}},"z-ai/glm-4.7":{"limit":{"context":200000,"output":64000}},"z-ai/glm-4.7-flash-free":{"limit":{"context":200000,"output":64000}},"z-ai/glm-4.7-flashx":{"limit":{"context":200000,"output":64000}},"z-ai/glm-5":{"limit":{"context":200000,"output":128000}}}},"zhipuai":{"id":"zhipuai","npm":"@ai-sdk/openai-compatible","api":"https://open.bigmodel.cn/api/paas/v4","env":["ZHIPU_API_KEY"],"models":{"glm-4.5":{"limit":{"context":131072,"output":98304}},"glm-4.5-air":{"limit":{"context":131072,"output":98304}},"glm-4.5-flash":{"limit":{"context":131072,"output":98304}},"glm-4.5v":{"limit":{"context":64000,"output":16384}},"glm-4.6":{"limit":{"context":204800,"output":131072}},"glm-4.6v":{"limit":{"context":128000,"output":32768}},"glm-4.7":{"limit":{"context":204800,"output":131072}},"glm-4.7-flash":{"limit":{"context":200000,"output":131072}},"glm-5":{"limit":{"context":204800,"output":131072}}}},"zhipuai-coding-plan":{"id":"zhipuai-coding-plan","npm":"@ai-sdk/openai-compatible","api":"https://open.bigmodel.cn/api/coding/paas/v4","env":["ZHIPU_API_KEY"],"models":{"glm-4.5":{"limit":{"context":131072,"output":98304}},"glm-4.5-air":{"limit":{"context":131072,"output":98304}},"glm-4.5-flash":{"limit":{"context":131072,"output":98304}},"glm-4.5v":{"limit":{"context":64000,"output":16384}},"glm-4.6":{"limit":{"context":204800,"output":131072}},"glm-4.6v":{"limit":{"context":128000,"output":32768}},"glm-4.6v-flash":{"limit":{"context":128000,"output":32768}},"glm-4.7":{"limit":{"context":204800,"output":131072}},"glm-5":{"limit":{"context":204800,"output":131072}}}}}} \ No newline at end of file diff --git a/src/llm-coding-tools-models-dev/src/bin/models-dev-update.rs b/src/llm-coding-tools-models-dev/src/bin/models-dev-update.rs new file mode 100644 index 00000000..70c95b07 --- /dev/null +++ b/src/llm-coding-tools-models-dev/src/bin/models-dev-update.rs @@ -0,0 +1,126 @@ +use reqwest::Client; +use serde::Deserialize; +use serde_json::to_vec; +use std::{collections::BTreeMap, env, path::PathBuf, time::Duration}; +use tokio::fs; + +#[derive(Debug, Deserialize)] +#[serde(untagged)] +enum FullSnapshot { + Nested { + providers: std::collections::HashMap, + }, + Flat(std::collections::HashMap), +} + +impl FullSnapshot { + fn into_providers(self) -> std::collections::HashMap { + match self { + FullSnapshot::Nested { providers } => providers, + FullSnapshot::Flat(providers) => providers, + } + } +} + +#[derive(Debug, Deserialize)] +struct FullProvider { + id: String, + #[serde(default)] + npm: Option, + #[serde(default)] + api: Option, + #[serde(default)] + env: Vec, + #[serde(default)] + models: std::collections::HashMap, +} + +#[derive(Debug, Deserialize)] +struct FullModel { + #[serde(default)] + limit: Option, +} + +#[derive(Debug, Deserialize)] +struct FullModelLimit { + context: u32, + #[serde(default)] + output: Option, +} + +#[derive(Debug, serde::Serialize)] +struct Snapshot { + providers: BTreeMap, +} + +#[derive(Debug, serde::Serialize)] +struct ProviderSnapshot { + id: String, + npm: Option, + api: Option, + env: Vec, + models: BTreeMap, +} + +#[derive(Debug, serde::Serialize)] +struct ModelSnapshot { + #[serde(skip_serializing_if = "Option::is_none")] + limit: Option, +} + +#[derive(Debug, serde::Serialize)] +struct ModelLimitSnapshot { + context: u32, + #[serde(skip_serializing_if = "Option::is_none")] + output: Option, +} + +#[tokio::main(flavor = "current_thread")] +async fn main() -> Result<(), Box> { + let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let output = manifest_dir.join("data/models.dev.min.json"); + if let Some(parent) = output.parent() { + fs::create_dir_all(parent).await?; + } + + let client = Client::builder().timeout(Duration::from_secs(30)).build()?; + let response = client + .get("https://models.dev/api.json") + .send() + .await? + .error_for_status()?; + let bytes = response.bytes().await?; + + let full: FullSnapshot = serde_json::from_slice(&bytes)?; + let full_providers = full.into_providers(); + let mut providers = BTreeMap::new(); + for (provider_id, provider) in full_providers { + let mut models = BTreeMap::new(); + for (model_id, model) in provider.models { + models.insert( + model_id, + ModelSnapshot { + limit: model.limit.map(|limit| ModelLimitSnapshot { + context: limit.context, + output: limit.output, + }), + }, + ); + } + providers.insert( + provider_id, + ProviderSnapshot { + id: provider.id, + npm: provider.npm, + api: provider.api, + env: provider.env, + models, + }, + ); + } + + let snapshot = Snapshot { providers }; + let json = to_vec(&snapshot)?; + fs::write(output, json).await?; + Ok(()) +} diff --git a/src/llm-coding-tools-models-dev/src/lib.rs b/src/llm-coding-tools-models-dev/src/lib.rs new file mode 100644 index 00000000..352b4f37 --- /dev/null +++ b/src/llm-coding-tools-models-dev/src/lib.rs @@ -0,0 +1,1074 @@ +#![doc = include_str!(concat!("../", env!("CARGO_PKG_README")))] + +use reqwest::Client; +use serde::{Deserialize, Serialize}; +use std::collections::{HashMap, HashSet}; +use std::env; +use std::io::{self, BufReader, BufWriter, Cursor, Read}; +use std::path::{Path, PathBuf}; +use std::time::Duration; +use thiserror::Error; +use tokio::fs; +use tokio::io::AsyncWriteExt; +use tokio::task; +use zstd::stream::decode_all; +use zstd::stream::write::Encoder; + +const MODELS_DEV_API_URL: &str = "https://models.dev/api.json"; +const MODELS_DEV_API_URL_ENV: &str = "MODELS_DEV_API_URL"; +const CACHE_PATH_ENV: &str = "OPENCODE_MODELS_DEV_CACHE_PATH"; +static BUNDLED_ZST: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/models.dev.min.json.zst")); + +mod model_limits { + use serde::{Deserialize, Serialize}; + + /// Compact model token limits extracted from models.dev. + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] + pub struct ModelLimits { + pub context: u32, + #[serde(default)] + pub output: Option, + } +} + +pub use model_limits::ModelLimits; + +/// Metadata for a models.dev provider. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ProviderMetadata { + pub id: String, + pub npm: Option, + pub api: Option, + pub env: Vec, +} + +/// Indicates where a catalog was loaded from. +#[derive(Debug, Clone)] +pub enum CatalogSource { + Bundled, + Cache(PathBuf), + Downloaded(PathBuf), +} + +/// Errors returned by the models.dev catalog. +#[derive(Debug, Error)] +pub enum CatalogError { + #[error("I/O error: {0}")] + Io(#[from] std::io::Error), + #[error("JSON error: {0}")] + Json(#[from] serde_json::Error), + #[error("zstd error: {0}")] + Zstd(std::io::Error), + #[error("missing bundled snapshot")] + MissingBundledSnapshot, + #[error("task join error: {0}")] + JoinError(#[from] tokio::task::JoinError), +} + +/// Outcome of loading from cache with bundled fallback. +pub struct CacheLoadResult { + pub catalog: ModelsDevCatalog, + pub source: CatalogSource, + pub cache_error: Option, +} + +/// In-memory catalog with model→provider index. +#[derive(Debug, Clone)] +pub struct ModelsDevCatalog { + providers: HashMap, + models_to_providers: HashMap>, + model_limits_by_provider: HashMap>, + model_limits_by_model: HashMap, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct Snapshot { + providers: HashMap, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct ProviderSnapshot { + id: String, + #[serde(default)] + npm: Option, + #[serde(default)] + api: Option, + #[serde(default)] + env: Vec, + #[serde(default)] + models: ProviderModelsSnapshot, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +enum ProviderModelsSnapshot { + Legacy(Vec), + Detailed(HashMap), +} + +impl Default for ProviderModelsSnapshot { + fn default() -> Self { + Self::Legacy(Vec::new()) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +struct SnapshotModel { + #[serde(default)] + limit: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct SnapshotModelLimit { + context: u32, + #[serde(default)] + output: Option, +} + +#[derive(Debug, Deserialize)] +#[serde(untagged)] +enum FullSnapshot { + Nested { + providers: HashMap, + }, + Flat(HashMap), +} + +impl FullSnapshot { + fn into_providers(self) -> HashMap { + match self { + FullSnapshot::Nested { providers } => providers, + FullSnapshot::Flat(providers) => providers, + } + } +} + +#[derive(Debug, Deserialize)] +struct FullProvider { + id: String, + #[serde(default)] + npm: Option, + #[serde(default)] + api: Option, + #[serde(default)] + env: Vec, + #[serde(default)] + models: HashMap, +} + +#[derive(Debug, Deserialize)] +struct FullModel { + #[serde(default)] + limit: Option, +} + +#[derive(Debug, Deserialize)] +struct FullModelLimit { + context: u32, + #[serde(default)] + output: Option, +} + +/// Resolve the shared cache path for models.dev snapshots. +/// +/// Returns: `Some(PathBuf)` when a cache directory can be determined, or `None`. +pub fn shared_cache_path() -> Option { + if let Some(path) = env::var_os(CACHE_PATH_ENV) { + if !path.is_empty() { + return Some(PathBuf::from(path)); + } + } + + let base = if cfg!(target_os = "windows") { + env::var_os("LOCALAPPDATA").map(PathBuf::from) + } else if cfg!(target_os = "macos") { + env::var_os("HOME").map(|home| PathBuf::from(home).join("Library").join("Caches")) + } else { + env::var_os("XDG_CACHE_HOME") + .map(PathBuf::from) + .or_else(|| env::var_os("HOME").map(|home| PathBuf::from(home).join(".cache"))) + }?; + + Some( + base.join("opencode") + .join("models.dev") + .join("models.dev.min.json.zst"), + ) +} + +fn models_dev_api_url() -> String { + env::var(MODELS_DEV_API_URL_ENV) + .ok() + .filter(|value| !value.is_empty()) + .unwrap_or_else(|| MODELS_DEV_API_URL.to_string()) +} + +impl ModelsDevCatalog { + /// Load the bundled snapshot embedded in the crate. + /// + /// Returns: the catalog and [`CatalogSource::Bundled`]. + pub fn from_bundled() -> Result<(Self, CatalogSource), CatalogError> { + let catalog = Self::from_bundled_bytes(BUNDLED_ZST, None)?; + Ok((catalog, CatalogSource::Bundled)) + } + + /// Load a cached snapshot from disk. + /// + /// Parameters: + /// - `path`: zstd-compressed snapshot path. + /// + /// Returns: the catalog and [`CatalogSource::Cache`]. + pub fn from_cache(path: &Path) -> Result<(Self, CatalogSource), CatalogError> { + let catalog = Self::from_compressed_path(path, None)?; + Ok((catalog, CatalogSource::Cache(path.to_path_buf()))) + } + + /// Load a cached snapshot while keeping only selected model IDs. + /// + /// Parameters: + /// - `path`: zstd-compressed snapshot path. + /// - `model_ids`: model ID set to index. + /// + /// Returns: the filtered catalog and [`CatalogSource::Cache`]. + pub fn from_cache_filtered( + path: &Path, + model_ids: &HashSet, + ) -> Result<(Self, CatalogSource), CatalogError> { + let catalog = Self::from_compressed_path(path, Some(model_ids))?; + Ok((catalog, CatalogSource::Cache(path.to_path_buf()))) + } + + /// Load a downloaded snapshot from disk. + /// + /// Parameters: + /// - `path`: zstd-compressed snapshot path. + /// + /// Returns: the catalog and [`CatalogSource::Downloaded`]. + pub fn from_downloaded(path: &Path) -> Result<(Self, CatalogSource), CatalogError> { + let catalog = Self::from_compressed_path(path, None)?; + Ok((catalog, CatalogSource::Downloaded(path.to_path_buf()))) + } + + /// Load a downloaded snapshot while keeping only selected model IDs. + /// + /// Parameters: + /// - `path`: zstd-compressed snapshot path. + /// - `model_ids`: model ID set to index. + /// + /// Returns: the filtered catalog and [`CatalogSource::Downloaded`]. + pub fn from_downloaded_filtered( + path: &Path, + model_ids: &HashSet, + ) -> Result<(Self, CatalogSource), CatalogError> { + let catalog = Self::from_compressed_path(path, Some(model_ids))?; + Ok((catalog, CatalogSource::Downloaded(path.to_path_buf()))) + } + + /// Load the bundled snapshot while keeping only selected model IDs. + /// + /// Parameters: + /// - `model_ids`: model ID set to index. + /// + /// Returns: the filtered catalog and [`CatalogSource::Bundled`]. + pub fn from_bundled_filtered( + model_ids: &HashSet, + ) -> Result<(Self, CatalogSource), CatalogError> { + let catalog = Self::from_bundled_bytes(BUNDLED_ZST, Some(model_ids))?; + Ok((catalog, CatalogSource::Bundled)) + } + + /// Load a catalog from a local models.dev `api.json` file. + /// + /// Parameters: + /// - `path`: path to the full `api.json` file. + /// + /// Returns: a `ModelsDevCatalog` built from the minified schema. + pub fn from_local_api_json(path: &Path) -> Result { + let file = std::fs::File::open(path)?; + let reader = BufReader::new(file); + let snapshot = snapshot_from_full_reader(reader)?; + Ok(Self::from_snapshot(snapshot, None)) + } + + /// Load from a cache path if available; fall back to bundled snapshot on missing/corrupt cache. + /// + /// Parameters: + /// - `path`: zstd-compressed cache path to attempt. + /// + /// Returns: `CacheLoadResult` with `cache_error` set when fallback is due to corruption. + pub fn load_cache_or_bundled(path: &Path) -> Result { + match Self::from_cache(path) { + Ok((catalog, source)) => Ok(CacheLoadResult { + catalog, + source, + cache_error: None, + }), + Err(err) => { + let cache_error = match &err { + CatalogError::Io(io_err) if io_err.kind() == io::ErrorKind::NotFound => None, + _ => Some(err), + }; + let (catalog, source) = Self::from_bundled()?; + Ok(CacheLoadResult { + catalog, + source, + cache_error, + }) + } + } + } + + /// Primary entrypoint: load from shared cache if available; fall back to bundled snapshot. + pub fn load_shared_cache_or_bundled() -> Result { + if let Some(path) = shared_cache_path() { + Self::load_cache_or_bundled(&path) + } else { + let (catalog, source) = Self::from_bundled()?; + Ok(CacheLoadResult { + catalog, + source, + cache_error: None, + }) + } + } + + /// Download the latest models.dev snapshot and write a compressed cache file. + /// + /// Parameters: + /// - `path`: destination path for the zstd-compressed snapshot. + /// + /// Returns: `Ok(())` when the cache file is written. + /// + /// Honors the `MODELS_DEV_API_URL` environment variable when set and non-empty. + /// + /// This uses a two-pass I/O flow (download to disk, then read/strip/compress) + /// to avoid buffering the full raw API response in memory (≈500KB–1MB). + pub async fn download_to(path: &Path) -> Result<(), CatalogError> { + let url = models_dev_api_url(); + Self::download_to_url(path, &url).await + } + + /// Refresh a cache path by downloading and rewriting the snapshot. + /// + /// Parameters: + /// - `path`: destination path for the zstd-compressed snapshot. + /// + /// Returns: `Ok(())` when the cache file is written. + /// + /// Honors the `MODELS_DEV_API_URL` environment variable when set and non-empty. + /// + /// This uses a two-pass I/O flow (download to disk, then read/strip/compress) + /// to avoid buffering the full raw API response in memory (≈500KB–1MB). + pub async fn refresh_cache(path: &Path) -> Result<(), CatalogError> { + let url = models_dev_api_url(); + Self::download_to_url(path, &url).await + } + + async fn download_to_url(path: &Path, url: &str) -> Result<(), CatalogError> { + let tmp_download = path.with_extension("json.tmp"); + let tmp_cache = path.with_extension("json.zst.tmp"); + let result = Self::download_and_compress(url, &tmp_download, &tmp_cache).await; + let result = match result { + Ok(()) => match fs::rename(&tmp_cache, path).await { + Ok(()) => Ok(()), + Err(rename_err) => { + if cfg!(windows) && rename_err.kind() == io::ErrorKind::AlreadyExists { + let _ = fs::remove_file(path).await; + match fs::rename(&tmp_cache, path).await { + Ok(()) => Ok(()), + Err(_) => Err(rename_err.into()), + } + } else { + Err(rename_err.into()) + } + } + }, + Err(err) => Err(err), + }; + + let _ = fs::remove_file(&tmp_download).await; + if result.is_err() { + let _ = fs::remove_file(&tmp_cache).await; + } + + result + } + + async fn download_and_compress( + url: &str, + tmp_download: &Path, + tmp_cache: &Path, + ) -> Result<(), CatalogError> { + if let Some(parent) = tmp_download.parent() { + fs::create_dir_all(parent).await?; + } + + let client = Client::builder() + .timeout(Duration::from_secs(30)) + .build() + .map_err(io::Error::other)?; + let mut response = client + .get(url) + .send() + .await + .map_err(io::Error::other)? + .error_for_status() + .map_err(io::Error::other)?; + + let mut tmp_file = fs::File::create(tmp_download).await?; + while let Some(chunk) = response.chunk().await.map_err(io::Error::other)? { + tmp_file.write_all(&chunk).await?; + } + tmp_file.flush().await?; + drop(tmp_file); + + let tmp_download = tmp_download.to_path_buf(); + let tmp_cache = tmp_cache.to_path_buf(); + task::spawn_blocking(move || { + let raw = std::fs::File::open(&tmp_download)?; + let reader = BufReader::new(raw); + let snapshot = snapshot_from_full_reader(reader)?; + + if let Some(parent) = tmp_cache.parent() { + std::fs::create_dir_all(parent)?; + } + + let file = std::fs::File::create(&tmp_cache)?; + let writer = BufWriter::new(file); + let mut encoder = Encoder::new(writer, 22).map_err(CatalogError::Zstd)?; + serde_json::to_writer(&mut encoder, &snapshot)?; + encoder.finish().map_err(CatalogError::Zstd)?; + Ok::<(), CatalogError>(()) + }) + .await + .map_err(CatalogError::JoinError)??; + + Ok(()) + } + + /// Resolve provider IDs for a model ID. + /// + /// Parameters: + /// - `model_id`: models.dev model ID. + /// + /// Returns: `Some(&[String])` of provider IDs, or `None` if the model is unknown. + #[inline] + pub fn resolve_provider_for_model(&self, model_id: &str) -> Option<&[String]> { + self.models_to_providers.get(model_id).map(Vec::as_slice) + } + + /// Look up provider metadata by provider ID. + /// + /// Parameters: + /// - `provider_id`: models.dev provider ID. + /// + /// Returns: provider metadata if present. + #[inline] + pub fn get_provider(&self, provider_id: &str) -> Option<&ProviderMetadata> { + self.providers.get(provider_id) + } + + /// Look up model limits by model ID when limits are unambiguous across providers. + #[inline] + pub fn get_model_limits(&self, model_id: &str) -> Option<&ModelLimits> { + self.model_limits_by_model.get(model_id) + } + + /// Look up model limits for a model constrained to a provider. + #[inline] + pub fn get_model_limits_for_provider( + &self, + provider_id: &str, + model_id: &str, + ) -> Option<&ModelLimits> { + self.model_limits_by_provider + .get(provider_id)? + .get(model_id) + } + + fn from_snapshot_bytes( + json: &[u8], + model_filter: Option<&HashSet>, + ) -> Result { + let snapshot: Snapshot = serde_json::from_slice(json)?; + Ok(Self::from_snapshot(snapshot, model_filter)) + } + + fn from_compressed_path( + path: &Path, + model_filter: Option<&HashSet>, + ) -> Result { + let compressed = std::fs::read(path)?; + Self::from_compressed_bytes(&compressed, model_filter) + } + + fn from_compressed_bytes( + compressed: &[u8], + model_filter: Option<&HashSet>, + ) -> Result { + let json = decode_all(Cursor::new(compressed)).map_err(CatalogError::Zstd)?; + Self::from_snapshot_bytes(&json, model_filter) + } + + fn from_bundled_bytes( + compressed: &[u8], + model_filter: Option<&HashSet>, + ) -> Result { + if compressed.is_empty() { + return Err(CatalogError::MissingBundledSnapshot); + } + Self::from_compressed_bytes(compressed, model_filter) + } + + #[allow(clippy::unnecessary_map_or, clippy::unwrap_or_default)] + fn from_snapshot(snapshot: Snapshot, model_filter: Option<&HashSet>) -> Self { + let mut providers = HashMap::with_capacity(snapshot.providers.len()); + let mut models_to_providers = HashMap::new(); + let mut model_limits_by_provider: HashMap> = + HashMap::new(); + let mut model_limits_by_model: HashMap = HashMap::new(); + let mut conflicting_model_limits: HashSet = HashSet::new(); + + for (provider_id, provider) in snapshot.providers { + let metadata = ProviderMetadata { + id: provider.id.clone(), + npm: provider.npm, + api: provider.api, + env: provider.env, + }; + providers.insert(provider_id.clone(), metadata); + + match provider.models { + ProviderModelsSnapshot::Legacy(model_ids) => { + for model_id in model_ids { + if model_filter.map_or(false, |filter| !filter.contains(&model_id)) { + continue; + } + models_to_providers + .entry(model_id) + .or_insert_with(Vec::new) + .push(provider_id.clone()); + } + } + ProviderModelsSnapshot::Detailed(models) => { + for (model_id, model) in models { + if model_filter.map_or(false, |filter| !filter.contains(&model_id)) { + continue; + } + models_to_providers + .entry(model_id.clone()) + .or_insert_with(Vec::new) + .push(provider_id.clone()); + + if let Some(limit) = model.limit { + let limits = ModelLimits { + context: limit.context, + output: limit.output, + }; + + model_limits_by_provider + .entry(provider_id.clone()) + .or_default() + .insert(model_id.clone(), limits.clone()); + + if !conflicting_model_limits.contains(&model_id) { + match model_limits_by_model.get(&model_id) { + None => { + model_limits_by_model.insert(model_id.clone(), limits); + } + Some(existing) if existing == &limits => {} + Some(_) => { + model_limits_by_model.remove(&model_id); + conflicting_model_limits.insert(model_id.clone()); + } + } + } + } + } + } + } + } + + Self { + providers, + models_to_providers, + model_limits_by_provider, + model_limits_by_model, + } + } +} + +fn snapshot_from_full_reader(reader: R) -> Result { + let full: FullSnapshot = serde_json::from_reader(reader)?; + let full_providers = full.into_providers(); + let mut providers = HashMap::with_capacity(full_providers.len()); + for (provider_id, provider) in full_providers { + let mut models = HashMap::with_capacity(provider.models.len()); + for (model_id, model) in provider.models { + models.insert( + model_id, + SnapshotModel { + limit: model.limit.map(|limit| SnapshotModelLimit { + context: limit.context, + output: limit.output, + }), + }, + ); + } + providers.insert( + provider_id, + ProviderSnapshot { + id: provider.id, + npm: provider.npm, + api: provider.api, + env: provider.env, + models: ProviderModelsSnapshot::Detailed(models), + }, + ); + } + Ok(Snapshot { providers }) +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::TempDir; + use tokio::sync::Mutex; + use wiremock::matchers::{method, path}; + use wiremock::{Mock, MockServer, ResponseTemplate}; + use zstd::bulk::compress; + + static ENV_LOCK: Mutex<()> = Mutex::const_new(()); + + fn snapshot_from_full_bytes(bytes: &[u8]) -> Result { + snapshot_from_full_reader(std::io::Cursor::new(bytes)) + } + + fn assert_cache_fallback(path: &Path, expect_error: bool) { + let result = ModelsDevCatalog::load_cache_or_bundled(path).expect("fallback"); + assert!(matches!(result.source, CatalogSource::Bundled)); + if expect_error { + assert!(result.cache_error.is_some()); + } else { + assert!(result.cache_error.is_none()); + } + } + + #[test] + fn bundled_snapshot_loads() { + let (catalog, source) = ModelsDevCatalog::from_bundled().expect("bundled snapshot loads"); + assert!(matches!(source, CatalogSource::Bundled)); + assert!(!catalog.providers.is_empty()); + } + + #[test] + fn lookup_works_for_fixture_snapshot() { + let json = br#"{"providers":{"alpha":{"id":"alpha","npm":null,"api":null,"env":["ALPHA_KEY"],"models":["m1","m2"]}}}"#; + let catalog = ModelsDevCatalog::from_snapshot_bytes(json, None).expect("parse fixture"); + let providers = catalog.resolve_provider_for_model("m1").expect("providers"); + assert!(providers.iter().any(|id| id == "alpha")); + let provider = catalog.get_provider("alpha").expect("provider exists"); + assert_eq!(provider.env, vec!["ALPHA_KEY".to_string()]); + } + + #[test] + fn from_cache_loads_fixture() { + let json = br#"{"providers":{"alpha":{"id":"alpha","npm":null,"api":null,"env":[],"models":["m1"]}}}"#; + let compressed = compress(json, 22).expect("compress fixture"); + let temp = TempDir::new().expect("tempdir"); + let path = temp.path().join("snapshot.zst"); + std::fs::write(&path, compressed).expect("write cache"); + + let (catalog, source) = ModelsDevCatalog::from_cache(&path).expect("cache loads"); + assert!(matches!(source, CatalogSource::Cache(_))); + assert!(catalog.get_provider("alpha").is_some()); + } + + #[test] + fn from_cache_filtered_keeps_selected_model() { + let json = br#"{"providers":{"alpha":{"id":"alpha","npm":null,"api":null,"env":[],"models":["m1","m2"]}}}"#; + let compressed = compress(json, 22).expect("compress fixture"); + let temp = TempDir::new().expect("tempdir"); + let path = temp.path().join("snapshot.zst"); + std::fs::write(&path, compressed).expect("write cache"); + + let mut filter = HashSet::new(); + filter.insert("m2".to_string()); + + let (catalog, source) = + ModelsDevCatalog::from_cache_filtered(&path, &filter).expect("cache filtered"); + assert!(matches!(source, CatalogSource::Cache(_))); + assert!(catalog.resolve_provider_for_model("m1").is_none()); + let providers = catalog.resolve_provider_for_model("m2").expect("providers"); + assert!(providers.iter().any(|id| id == "alpha")); + } + + #[test] + fn from_bundled_filtered_keeps_selected_model() { + let json = decode_all(Cursor::new(BUNDLED_ZST)).expect("decode bundled"); + let snapshot: Snapshot = serde_json::from_slice(&json).expect("parse bundled"); + let (model_id, provider_id) = snapshot + .providers + .values() + .find_map(|provider| match &provider.models { + ProviderModelsSnapshot::Detailed(models) => models + .keys() + .next() + .map(|id| (id.clone(), provider.id.clone())), + ProviderModelsSnapshot::Legacy(models) => { + models.first().map(|id| (id.clone(), provider.id.clone())) + } + }) + .expect("bundled has model"); + let mut filter = HashSet::new(); + filter.insert(model_id.clone()); + + let (catalog, source) = + ModelsDevCatalog::from_bundled_filtered(&filter).expect("filtered load"); + assert!(matches!(source, CatalogSource::Bundled)); + let providers = catalog + .resolve_provider_for_model(&model_id) + .expect("providers"); + assert!(providers.iter().any(|id| id == &provider_id)); + } + + #[test] + fn missing_bundled_snapshot_errors() { + let err = ModelsDevCatalog::from_bundled_bytes(&[], None).expect_err("missing bundled"); + assert!(matches!(err, CatalogError::MissingBundledSnapshot)); + } + + #[test] + fn corrupt_zstd_errors() { + let err = + ModelsDevCatalog::from_compressed_bytes(b"not-zstd", None).expect_err("corrupt zstd"); + assert!(matches!(err, CatalogError::Zstd(_))); + } + + #[test] + fn json_parse_errors() { + let err = ModelsDevCatalog::from_snapshot_bytes(b"{not json}", None).expect_err("bad json"); + assert!(matches!(err, CatalogError::Json(_))); + } + + #[test] + fn lookup_cases_parameterized() { + let json = br#"{"providers":{ + "alpha":{"id":"alpha","npm":null,"api":null,"env":[],"models":["m1","m2"]}, + "beta":{"id":"beta","npm":null,"api":null,"env":[],"models":["m2"]} + }}"#; + let catalog = ModelsDevCatalog::from_snapshot_bytes(json, None).expect("parse fixture"); + + let cases = [ + ("m2", &["alpha", "beta"][..]), + ("m1", &["alpha"][..]), + ("missing", &[][..]), + ]; + for (model_id, expected) in cases { + let providers = catalog.resolve_provider_for_model(model_id).unwrap_or(&[]); + let mut providers = providers.iter().map(String::as_str).collect::>(); + providers.sort_unstable(); + let mut expected = expected.to_vec(); + expected.sort_unstable(); + assert_eq!(providers, expected); + } + + assert!(catalog.get_provider("missing").is_none()); + } + + #[test] + fn snapshot_from_full_bytes_accepts_flat_map() { + let json = br#"{"alpha":{"id":"alpha","npm":null,"api":null,"env":[],"models":{"m1":{}}}}"#; + let snapshot = snapshot_from_full_bytes(json).expect("parse flat full snapshot"); + let provider = snapshot.providers.get("alpha").expect("alpha provider"); + match &provider.models { + ProviderModelsSnapshot::Detailed(models) => { + assert!(models.contains_key("m1")); + } + _ => panic!("expected Detailed variant"), + } + } + + #[test] + fn snapshot_from_full_bytes_accepts_nested_map() { + let json = br#"{"providers":{"alpha":{"id":"alpha","npm":null,"api":null,"env":[],"models":{"m1":{"limit":{"context":128000}}}}}}"#; + let snapshot = snapshot_from_full_bytes(json).expect("parse nested full snapshot"); + let snapshot_json = serde_json::to_vec(&snapshot).expect("serialize snapshot"); + let catalog = ModelsDevCatalog::from_snapshot_bytes(&snapshot_json, None) + .expect("parse snapshot bytes"); + assert_eq!( + catalog + .get_model_limits_for_provider("alpha", "m1") + .expect("provider scoped limit") + .context, + 128000 + ); + } + + #[tokio::test] + async fn download_to_writes_snapshot() { + let server = MockServer::start().await; + let body = br#"{"providers":{"alpha":{"id":"alpha","npm":null,"api":null,"env":[],"models":{"m1":{}}}}}"#; + Mock::given(method("GET")) + .and(path("/api.json")) + .respond_with(ResponseTemplate::new(200).set_body_bytes(body)) + .mount(&server) + .await; + + let temp = TempDir::new().expect("tempdir"); + let path = temp.path().join("download.zst"); + ModelsDevCatalog::download_to_url(&path, &format!("{}/api.json", server.uri())) + .await + .expect("download"); + let (catalog, source) = ModelsDevCatalog::from_downloaded(&path).expect("load downloaded"); + assert!(matches!(source, CatalogSource::Downloaded(_))); + assert!(catalog.get_provider("alpha").is_some()); + + let mut filter = HashSet::new(); + filter.insert("m1".to_string()); + let (filtered, source) = ModelsDevCatalog::from_downloaded_filtered(&path, &filter) + .expect("load downloaded filtered"); + assert!(matches!(source, CatalogSource::Downloaded(_))); + assert!(filtered.resolve_provider_for_model("m1").is_some()); + assert!(filtered.resolve_provider_for_model("missing").is_none()); + } + + #[tokio::test] + async fn download_to_creates_parent_directories() { + let server = MockServer::start().await; + let body = br#"{"providers":{"alpha":{"id":"alpha","npm":null,"api":null,"env":[],"models":{"m1":{}}}}}"#; + Mock::given(method("GET")) + .and(path("/api.json")) + .respond_with(ResponseTemplate::new(200).set_body_bytes(body)) + .mount(&server) + .await; + + let temp = TempDir::new().expect("tempdir"); + let path = temp.path().join("nested/dir/download.zst"); + ModelsDevCatalog::download_to_url(&path, &format!("{}/api.json", server.uri())) + .await + .expect("download"); + assert!(path.exists()); + } + + #[tokio::test] + async fn download_to_errors_on_bad_status() { + let server = MockServer::start().await; + Mock::given(method("GET")) + .and(path("/api.json")) + .respond_with(ResponseTemplate::new(500)) + .mount(&server) + .await; + + let temp = TempDir::new().expect("tempdir"); + let path = temp.path().join("download.zst"); + let err = ModelsDevCatalog::download_to_url(&path, &format!("{}/api.json", server.uri())) + .await + .expect_err("bad status"); + assert!(matches!(err, CatalogError::Io(_))); + } + + #[tokio::test] + async fn refresh_cache_writes_valid_snapshot_and_cleans_temp() { + let server = MockServer::start().await; + let body = br#"{"providers":{"alpha":{"id":"alpha","npm":null,"api":null,"env":[],"models":{"m1":{}}}}}"#; + Mock::given(method("GET")) + .and(path("/api.json")) + .respond_with(ResponseTemplate::new(200).set_body_bytes(body)) + .mount(&server) + .await; + + let temp = TempDir::new().expect("tempdir"); + let path = temp.path().join("cache/models.dev.min.json.zst"); + ModelsDevCatalog::download_to_url(&path, &format!("{}/api.json", server.uri())) + .await + .expect("refresh"); + + let (catalog, source) = ModelsDevCatalog::from_cache(&path).expect("load cache"); + assert!(matches!(source, CatalogSource::Cache(_))); + assert!(catalog.get_provider("alpha").is_some()); + + let tmp_download = path.with_extension("json.tmp"); + let tmp_cache = path.with_extension("json.zst.tmp"); + assert!(!tmp_download.exists()); + assert!(!tmp_cache.exists()); + } + + #[test] + fn from_local_api_json_loads_minified_schema() { + let temp = TempDir::new().expect("tempdir"); + let path = temp.path().join("api.json"); + let json = br#"{"providers":{"alpha":{"id":"alpha","npm":null,"api":null,"env":["ALPHA_KEY"],"models":{"m1":{}}}}}"#; + std::fs::write(&path, json).expect("write api.json"); + let catalog = ModelsDevCatalog::from_local_api_json(&path).expect("load local"); + assert!(catalog.get_provider("alpha").is_some()); + assert!(catalog.resolve_provider_for_model("m1").is_some()); + } + + #[test] + fn cache_error_is_none_for_missing_cache() { + let temp = TempDir::new().expect("tempdir"); + let path = temp.path().join("missing.zst"); + assert_cache_fallback(&path, false); + } + + #[test] + fn cache_error_is_some_for_corrupt_cache() { + let temp = TempDir::new().expect("tempdir"); + let path = temp.path().join("bad.zst"); + std::fs::write(&path, b"not-zstd").expect("write bad zstd"); + assert_cache_fallback(&path, true); + } + + #[test] + fn snapshot_strips_model_fields_except_limits() { + let json = br#"{"providers":{"alpha":{"id":"alpha","npm":null,"api":null,"env":[],"models":{"m1":{"description":"desc","limit":{"context":4096}}}}}}"#; + let snapshot = snapshot_from_full_bytes(json).expect("snapshot"); + let provider = snapshot.providers.get("alpha").expect("provider"); + match &provider.models { + ProviderModelsSnapshot::Detailed(models) => { + let model = models.get("m1").expect("m1 model"); + assert!(model.limit.is_some()); + assert_eq!(model.limit.as_ref().unwrap().context, 4096); + } + _ => panic!("expected Detailed variant"), + } + } + + #[tokio::test] + async fn download_uses_env_override_url() { + let _guard = ENV_LOCK.lock().await; + let server = MockServer::start().await; + let body = br#"{"providers":{"alpha":{"id":"alpha","npm":null,"api":null,"env":[],"models":{"m1":{}}}}}"#; + Mock::given(method("GET")) + .and(path("/api.json")) + .respond_with(ResponseTemplate::new(200).set_body_bytes(body)) + .mount(&server) + .await; + + std::env::set_var(MODELS_DEV_API_URL_ENV, format!("{}/api.json", server.uri())); + let temp = TempDir::new().expect("tempdir"); + let path = temp.path().join("download.zst"); + ModelsDevCatalog::download_to(&path) + .await + .expect("download"); + std::env::remove_var(MODELS_DEV_API_URL_ENV); + } + + async fn assert_shared_cache_fallback(path: &Path, corrupt_payload: Option<&[u8]>) { + let _guard = ENV_LOCK.lock().await; + if let Some(payload) = corrupt_payload { + std::fs::write(path, payload).expect("write cache"); + } + std::env::set_var(CACHE_PATH_ENV, path); + let result = ModelsDevCatalog::load_shared_cache_or_bundled().expect("fallback"); + std::env::remove_var(CACHE_PATH_ENV); + assert!(matches!(result.source, CatalogSource::Bundled)); + assert_eq!(result.cache_error.is_some(), corrupt_payload.is_some()); + } + + #[cfg(windows)] + #[tokio::test] + async fn refresh_cache_windows_already_exists_fallback() { + let server = MockServer::start().await; + let body = br#"{"providers":{"alpha":{"id":"alpha","npm":null,"api":null,"env":[],"models":{"m1":{}}}}}"#; + Mock::given(method("GET")) + .and(path("/api.json")) + .respond_with(ResponseTemplate::new(200).set_body_bytes(body)) + .mount(&server) + .await; + + let temp = TempDir::new().expect("tempdir"); + let path = temp.path().join("cache/models.dev.min.json.zst"); + if let Some(parent) = path.parent() { + fs::create_dir_all(parent).await.expect("mkdir"); + } + fs::write(&path, b"existing").await.expect("write existing"); + + ModelsDevCatalog::download_to_url(&path, &format!("{}/api.json", server.uri())) + .await + .expect("refresh with fallback"); + + let (catalog, source) = ModelsDevCatalog::from_cache(&path).expect("load cache"); + assert!(matches!(source, CatalogSource::Cache(_))); + assert!(catalog.get_provider("alpha").is_some()); + } + + #[tokio::test] + async fn load_shared_cache_fallback_variants() { + let temp = TempDir::new().expect("tempdir"); + let missing = temp.path().join("missing.zst"); + let corrupt = temp.path().join("bad.zst"); + assert_shared_cache_fallback(&missing, None).await; + assert_shared_cache_fallback(&corrupt, Some(b"not-zstd")).await; + } + + #[test] + fn model_limits_lookup_from_detailed_snapshot() { + let json = br#"{"providers":{"alpha":{"id":"alpha","npm":null,"api":null,"env":[],"models":{"m1":{"limit":{"context":8192,"output":1024}}}}}}"#; + let catalog = + ModelsDevCatalog::from_snapshot_bytes(json, None).expect("parse detailed snapshot"); + + let limits = catalog.get_model_limits("m1").expect("model limits"); + assert_eq!(limits.context, 8192); + assert_eq!(limits.output, Some(1024)); + assert_eq!( + catalog + .get_model_limits_for_provider("alpha", "m1") + .expect("provider limits") + .context, + 8192 + ); + } + + #[test] + fn provider_scoped_lookup_returns_none_when_provider_missing_model() { + let json = br#"{"providers":{"alpha":{"id":"alpha","npm":null,"api":null,"env":[],"models":{"m1":{"limit":{"context":4096,"output":512}}}},"beta":{"id":"beta","npm":null,"api":null,"env":[],"models":{"m2":{"limit":{"context":4096,"output":256}}}}}}"#; + let catalog = + ModelsDevCatalog::from_snapshot_bytes(json, None).expect("parse detailed snapshot"); + + assert!(catalog + .get_model_limits_for_provider("beta", "m1") + .is_none()); + assert!(catalog + .get_model_limits_for_provider("missing", "m1") + .is_none()); + } + + #[test] + fn conflicting_limits_for_same_model_stay_provider_scoped() { + let json = br#"{"providers":{"alpha":{"id":"alpha","npm":null,"api":null,"env":[],"models":{"m-shared":{"limit":{"context":8192,"output":1024}}}},"beta":{"id":"beta","npm":null,"api":null,"env":[],"models":{"m-shared":{"limit":{"context":16384}}}}}}"#; + let catalog = ModelsDevCatalog::from_snapshot_bytes(json, None) + .expect("parse conflicting detailed snapshot"); + + assert_eq!( + catalog + .get_model_limits_for_provider("alpha", "m-shared") + .expect("alpha scoped") + .output, + Some(1024) + ); + assert_eq!( + catalog + .get_model_limits_for_provider("beta", "m-shared") + .expect("beta scoped") + .output, + None + ); + assert!(catalog.get_model_limits("m-shared").is_none()); + } + + #[test] + fn legacy_snapshot_without_limits_still_works() { + let json = br#"{"providers":{"alpha":{"id":"alpha","npm":null,"api":null,"env":[],"models":["m1","m2"]}}}"#; + let catalog = + ModelsDevCatalog::from_snapshot_bytes(json, None).expect("parse legacy snapshot"); + + let providers = catalog.resolve_provider_for_model("m1").expect("providers"); + assert!(providers.iter().any(|id| id == "alpha")); + assert!(catalog.get_model_limits("m1").is_none()); + assert!(catalog + .get_model_limits_for_provider("alpha", "m1") + .is_none()); + } +} diff --git a/src/llm-coding-tools-models-dev/tests/public_api.rs b/src/llm-coding-tools-models-dev/tests/public_api.rs new file mode 100644 index 00000000..aff6ed4c --- /dev/null +++ b/src/llm-coding-tools-models-dev/tests/public_api.rs @@ -0,0 +1,18 @@ +use llm_coding_tools_models_dev::{ModelLimits, ModelsDevCatalog}; + +#[test] +fn model_limits_type_is_publicly_importable() { + let _limits = ModelLimits { + context: 1024, + output: Some(256), + }; + + // Verify the method exists and is callable with the expected signature + fn check_signature<'a>( + catalog: &'a ModelsDevCatalog, + model_id: &'a str, + ) -> Option<&'a ModelLimits> { + catalog.get_model_limits(model_id) + } + let _ = check_signature; +} diff --git a/src/llm-coding-tools-serdesai/Cargo.toml b/src/llm-coding-tools-serdesai/Cargo.toml index 70f6bcf0..12eb3a20 100644 --- a/src/llm-coding-tools-serdesai/Cargo.toml +++ b/src/llm-coding-tools-serdesai/Cargo.toml @@ -14,11 +14,24 @@ llm-coding-tools-core = { version = "0.2.0", path = "../llm-coding-tools-core", "tokio", ] } +# Agent registry and task tool core +llm-coding-tools-agents = { version = "0.1.0", path = "../llm-coding-tools-agents" } + +# models.dev catalog for provider metadata +llm-coding-tools-models-dev = { version = "0.1.0", path = "../llm-coding-tools-models-dev" } + # serdes-ai provides Tool trait, ToolDefinition, RunContext serdes-ai = "0.1" -serdes-ai-models = { version = "0.1", features = ["openrouter"] } -serdes-ai-streaming = "0.1" -futures = "0.3" +serdes-ai-models = { version = "0.1", features = [ + "openai", + "anthropic", + "groq", + "mistral", + "google", + "cohere", + "openrouter", + "huggingface", +] } # Tool trait is async - async-trait is NOT re-exported from serdes-ai async-trait = "0.1" @@ -33,7 +46,15 @@ reqwest = { version = "0.13", default-features = false, features = [ "rustls-native-certs", ] } +# IndexMap is needed for permission config +indexmap = "2.7" + +# AHashMap for performance (matches llm-coding-tools-agents) +ahash = "0.8" + [dev-dependencies] tokio = { version = "1", features = ["macros", "rt-multi-thread"] } tempfile = "3" wiremock = "0.6" +indoc = "2" +futures = "0.3" diff --git a/src/llm-coding-tools-serdesai/README.md b/src/llm-coding-tools-serdesai/README.md index 4cc752d6..e2834462 100644 --- a/src/llm-coding-tools-serdesai/README.md +++ b/src/llm-coding-tools-serdesai/README.md @@ -13,6 +13,7 @@ Lightweight, high-performance serdesAI framework Tool implementations for coding - **Shell execution** - Cross-platform command execution with timeout - **Web fetching** - URL content retrieval with format conversion - **Todo management** - Shared-state todo list tracking +- **Task tool** - Registry-driven agent invocation with permission checks - **Context strings** - LLM guidance text for tool usage (re-exported from core) - **Schema builders** - Composable helpers for custom tool definitions @@ -27,33 +28,59 @@ llm-coding-tools-serdesai = "0.1" ## Quick Start -Minimal runnable agent (requires `OPENAI_API_KEY`): +Minimal runnable registry-backed agent (requires `SYNTHETIC_API_KEY`): ```rust,no_run -use llm_coding_tools_serdesai::absolute::{GlobTool, GrepTool, ReadTool}; -use llm_coding_tools_serdesai::agent_ext::AgentBuilderExt; -use llm_coding_tools_serdesai::{BashTool, SystemPromptBuilder, create_todo_tools}; +use llm_coding_tools_agents::{AgentCatalog, AgentLoader}; +use llm_coding_tools_serdesai::{ + AgentDefaults, AgentRegistryBuilder, AllowedPathResolver, ProviderOverrides, TodoState, + default_tools, +}; use serdes_ai::prelude::*; +use std::sync::Arc; + +const MODEL_SPEC: &str = "synthetic/hf:zai-org/GLM-4.7"; +const FILE_READER: &str = r#"--- +name: file-reader +mode: subagent +description: Example subagent +permission: + read: allow + glob: allow +--- +Respond concisely. +"#; #[tokio::main] async fn main() -> std::result::Result<(), Box> { - let (todo_read, todo_write, _state) = create_todo_tools(); - let mut pb = SystemPromptBuilder::new(); - - // Build agent with tools - call .system_prompt() last - let agent = AgentBuilder::<(), String>::from_model("openai:gpt-4o")? - .tool(pb.track(ReadTool::::new())) - .tool(pb.track(GlobTool::new())) - .tool(pb.track(GrepTool::::new())) - .tool(pb.track(BashTool::new())) - .tool(pb.track(todo_read)) - .tool(pb.track(todo_write)) - .system_prompt(pb.build()) // Last, after tracking all tools - .build(); - - // Run agent with tools - let response = agent - .run("Search for TODO comments in src/", ()) + let mut catalog = AgentCatalog::new(); + AgentLoader::new().add_from_str(&mut catalog, FILE_READER, "file-reader")?; + + let allowed_paths = + AllowedPathResolver::new([std::env::current_dir()?, std::env::temp_dir()])?; + let tools = default_tools(true, Some(allowed_paths), TodoState::new()); + + let defaults = AgentDefaults { + model: MODEL_SPEC.to_string(), + model_resolver: None, + provider_overrides: ProviderOverrides::new(), + api_key: None, + base_url: None, + temperature: None, + top_p: None, + options: Default::default(), + }; + + let deps = Arc::new(()); + let registry = AgentRegistryBuilder::<()>::new(defaults, tools) + .build_with_recursive_task(&catalog, Arc::clone(&deps))?; + let primary = registry + .get("file-reader") + .ok_or_else(|| std::io::Error::other("missing file-reader agent"))?; + + let response = primary + .agent + .run("List Rust files in the current directory.", deps) .await?; println!("{}", response.output()); @@ -61,7 +88,7 @@ async fn main() -> std::result::Result<(), Box> { } ``` -See the [serdesai-basic example](examples/serdesai-basic.rs) for a complete working setup. +See the [serdesai-agents example](examples/serdesai-agents.rs) for a complete working setup. ## Usage @@ -83,14 +110,156 @@ let sandboxed_read: AllowedReadTool = AllowedReadTool::new(resolver.clone( let sandboxed_write = AllowedWriteTool::new(resolver); ``` -Other tools: `BashTool`, `WebFetchTool`, `TaskTool`, `TodoReadTool`, `TodoWriteTool`. -Use `SystemPromptBuilder` to track tools and pass `pb.build()` to `.system_prompt()`. Set `working_directory()` so the environment section is populated. -Use `AgentBuilderExt::tool()` to add tools that implement `Tool` to the agent. -Context strings are re-exported in `llm_coding_tools_serdesai::context` (e.g., `BASH`, `READ_ABSOLUTE`). +### Task Tool (Registry-Driven) + +The Task tool allows agents to invoke other agents via a registry-based lookup. + +**Note**: For a complete runnable example, see `examples/serdesai-agents.rs`. + +Setup requires three steps: + +1. **Load agent configs** into `AgentCatalog` +2. **Build tool catalog** with `default_tools` +3. **Build recursive registry** with `AgentRegistryBuilder::build_with_recursive_task` + +```rust,no_run +use llm_coding_tools_agents::{AgentCatalog, AgentLoader}; +use llm_coding_tools_serdesai::{ + AgentDefaults, AgentRegistryBuilder, ProviderOverrides, TodoState, default_tools, +}; +use std::sync::Arc; + +fn main() -> Result<(), Box> { + let loader = AgentLoader::new(); + let mut catalog = AgentCatalog::new(); + loader.add_file(&mut catalog, "agents/example.md")?; + + let defaults = AgentDefaults { + model: "synthetic/hf:zai-org/GLM-4.7".to_string(), + model_resolver: None, + provider_overrides: ProviderOverrides::new(), + api_key: None, + base_url: None, + temperature: None, + top_p: None, + options: Default::default(), + }; + + let tools = default_tools(true, None, TodoState::new()); + let deps = Arc::new(()); + let _registry = AgentRegistryBuilder::<()>::new(defaults, tools) + .build_with_recursive_task(&catalog, deps)?; + + Ok(()) +} +``` + +`default_tools` returns cloneable `ToolCatalogEntry` items. `AgentRegistryBuilder` applies permission filtering per agent and wires `Task` automatically when `permission.task` allows delegation. + +### Other Tools + +The following tools are available for use with agents: + +- `BashTool` - Execute shell commands +- `WebFetchTool` - Fetch content from URLs +- `TodoReadTool` / `TodoWriteTool` - Manage todo items + +Use `SystemPromptBuilder` to track tools and populate the environment section: + +```rust,ignore +use llm_coding_tools_serdesai::SystemPromptBuilder; + +let mut pb = SystemPromptBuilder::new() + .working_directory(std::env::current_dir()?); +// ... track tools with pb.track() ... +// Finally set the system prompt: +let agent = AgentBuilder::from_model("synthetic/hf:zai-org/GLM-4.7")? + .system_prompt(pb.build()) + .build()?; +``` + +Add tools to agents using `AgentBuilderExt::tool()`: + +```rust,ignore +let agent = AgentBuilder::from_model("synthetic/hf:zai-org/GLM-4.7")? + .tool(MyTool::new()) + .build()?; +``` + +Context strings (e.g., `BASH`, `READ_ABSOLUTE`) are re-exported in `llm_coding_tools_serdesai::context`. + +### Model Resolver + +Registry builds always resolve models through `AgentDefaults.model_resolver`. + +Recommended default (`model_resolver: None`): + +```rust,no_run +# use llm_coding_tools_serdesai::{AgentDefaults, ProviderOverrides}; +let defaults = AgentDefaults { + model: "synthetic/hf:zai-org/GLM-4.7".into(), + model_resolver: None, + provider_overrides: ProviderOverrides::new(), + api_key: None, + base_url: None, + temperature: None, + top_p: None, + options: Default::default(), +}; +``` + +This uses the default resolver abstraction, which is models.dev-backed today and can be replaced by injecting your own `Arc`. + +Manual openai-compatible endpoint override fallback: + +```rust,no_run +# use llm_coding_tools_serdesai::{AgentDefaults, ProviderOverride, ProviderOverrides}; +let overrides = ProviderOverrides::new().insert_override( + "synthetic", + ProviderOverride { + api_key: None, + base_url: Some("https://your-openai-compatible-endpoint/v1".into()), + endpoint_env: None, + }, +); + +let defaults = AgentDefaults { + model: "synthetic/hf:zai-org/GLM-4.7".into(), + model_resolver: None, + provider_overrides: overrides, + api_key: None, + base_url: None, + temperature: None, + top_p: None, + options: Default::default(), +}; +``` + +**OpenCode model specs**: use `/` in agent/frontmatter configuration (for example `synthetic/hf:zai-org/GLM-4.7`). Resolver preserves the original spec and infers runtime provider family from provider metadata. + +**OpenAI-compatible providers**: keep provider identity in the user spec (for example `synthetic/hf:zai-org/GLM-4.7`); resolver derives openai-compatible runtime behavior and resolves provider-specific endpoint settings. + +**Resolver fallback behavior**: when no resolver is injected, registry load uses shared-cache or bundled catalog data. If catalog loading fails, resolution falls back to explicit raw spec behavior. + + +### Migration from Legacy Task APIs + +The previous task setup using `TaskToolCore` and `SubagentRegistry` has been replaced with the registry-driven flow. Key changes: + +| Legacy | New | +|--------|-----| +| `SubagentRegistry` from agents crate | `AgentCatalog` + serdesAI `AgentRegistry` | +| `TaskToolCore` | `TaskTool` (registry-based implementation) | +| Manually building agents | `AgentRegistryBuilder` builds all at once | + +For a detailed migration example, see `examples/serdesai-agents.rs`. ## Examples ```bash +# Registry + resolver flow (recommended) +SYNTHETIC_API_KEY=... cargo run --example serdesai-agents -p llm-coding-tools-serdesai + # Basic agent setup with AgentBuilderExt cargo run --example serdesai-basic -p llm-coding-tools-serdesai diff --git a/src/llm-coding-tools-serdesai/examples/agents/file-reader.md b/src/llm-coding-tools-serdesai/examples/agents/file-reader.md new file mode 100644 index 00000000..e1333d66 --- /dev/null +++ b/src/llm-coding-tools-serdesai/examples/agents/file-reader.md @@ -0,0 +1,10 @@ +--- +name: file-reader +mode: subagent +description: Example subagent +permission: + read: allow + glob: allow + task: allow +--- +You are a helpful subagent. Respond concisely. diff --git a/src/llm-coding-tools-serdesai/examples/serdesai-agents.rs b/src/llm-coding-tools-serdesai/examples/serdesai-agents.rs new file mode 100644 index 00000000..984546de --- /dev/null +++ b/src/llm-coding-tools-serdesai/examples/serdesai-agents.rs @@ -0,0 +1,159 @@ +//! Agent-driven Task tool example (serdesAI). +//! +//! Demonstrates: +//! - Loading a subagent config from an embedded file using include_str! +//! - Using default_tools to build the tool catalog +//! - Building an AgentRegistry from AgentCatalog and tools +//! - Creating a TaskTool for subagent invocation +//! - Setting up a primary agent with only the Task tool (forces delegation) +//! - Running a task that requires the primary agent to invoke a subagent +//! - Streaming output with XML-style logging +//! +//! Run: SYNTHETIC_API_KEY=... cargo run --example serdesai-agents -p llm-coding-tools-serdesai +//! Or set SYNTHETIC_API_KEY in the const below. + +use futures::StreamExt; +use llm_coding_tools_agents::{AgentCatalog, AgentLoader}; +use llm_coding_tools_serdesai::{ + AgentDefaults, AgentRegistryBuilder, AllowedPathResolver, ProviderOverride, ProviderOverrides, + TodoState, default_tools, +}; +use serdes_ai::prelude::*; +use std::fmt::Write; +use std::sync::Arc; + +// Model and provider are inherited from MODEL_SPEC (parsed from models.dev format). +const MODEL_SPEC: &str = "synthetic/hf:zai-org/GLM-4.7"; + +// Set your Synthetic API key here or via SYNTHETIC_API_KEY environment variable. +/// Fallback API key if env var is not set. Leave empty to require env var. +const SYNTHETIC_API_KEY: &str = ""; + +fn get_synthetic_api_key() -> String { + std::env::var("SYNTHETIC_API_KEY").unwrap_or_else(|_| { + if SYNTHETIC_API_KEY.is_empty() { + panic!("SYNTHETIC_API_KEY environment variable must be set"); + } + SYNTHETIC_API_KEY.to_string() + }) +} + +fn provider_overrides_from_env() -> ProviderOverrides { + let endpoint_override = std::env::var("SYNTHETIC_BASE_URL") + .ok() + .filter(|value| !value.trim().is_empty()); + + let api_key = get_synthetic_api_key(); + + ProviderOverrides::new().insert_override( + "synthetic", + ProviderOverride { + api_key: Some(api_key), + base_url: endpoint_override, + endpoint_env: None, + }, + ) +} + +// Embedded subagent config (loaded via include_str!) +const SUBAGENT_CONFIG: &str = include_str!("agents/file-reader.md"); + +#[tokio::main] +async fn main() -> std::result::Result<(), Box> { + // === Load agent config === + // + // Load a single embedded agent config using include_str!. + let loader = AgentLoader::new(); + let mut catalog = AgentCatalog::new(); + loader.add_from_str(&mut catalog, SUBAGENT_CONFIG, "file-reader")?; + + // === Setup allowed paths for sandboxed tools === + // + // Tools are sandboxed to the current directory and temp directory. + let allowed_path_resolver = + AllowedPathResolver::new([std::env::current_dir()?, std::env::temp_dir()])?; + + // === Build tool catalog === + // + // Use default_tools to create a catalog of cloneable tools. + // Tools are sandboxed to allowed directories. + let tools = default_tools(true, Some(allowed_path_resolver), TodoState::new()); + + let provider_overrides = provider_overrides_from_env(); + + // === Build registry === + // + // AgentDefaults specifies model resolution + sampling parameters. + // model_resolver: None uses the default resolver abstraction (models.dev-backed). + // api_key is set via ProviderOverrides above (required for non-OpenAI providers). + let defaults = AgentDefaults { + model: MODEL_SPEC.to_string(), + model_resolver: None, + provider_overrides, + api_key: None, + base_url: None, + temperature: None, + top_p: None, + options: Default::default(), + }; + + // Build the registry with recursive Task wiring enabled. + // + // The registry prebuilds all agents with their allowed tools from the catalog. + // Recursive Task availability is controlled by each agent's permission.task rules. + // Agents with allow rules for task can delegate to other agents. + let deps = Arc::new(()); + let registry = AgentRegistryBuilder::<()>::new(defaults, tools) + .build_with_recursive_task(&catalog, Arc::clone(&deps))?; + + // Primary agent comes from the same catalog and already carries Task + // wiring according to its own permission.task rules. + // For this example, we use the file-reader agent as the entry point. + let primary = registry + .get("file-reader") + .ok_or_else(|| "missing file-reader agent".to_string())?; + + // === Print tool info === + println!( + "=== Agent Ready ({} tools) ===", + primary.agent.tools().len() + ); + + // === Invoke a subagent via Task === + // + // Prompt the primary agent to use the Task tool to invoke a subagent. + // The primary agent must delegate because it only has the Task tool. + let prompt = "Use the Task tool with subagent_type 'file-reader' to read Cargo.toml and summarize dependencies."; + println!("\n=== Running Agent ==="); + + let mut stream = primary.agent.run_stream(prompt, Arc::clone(&deps)).await?; + + fn log_xml(request_id: u32, tag: &str, content: &str) { + let mut line = String::with_capacity(content.len() + tag.len() * 2 + 18); + let _ = write!(line, "<{request_id}:{tag}>{content}"); + println!("{line}"); + } + + let mut request_id = 0u32; + log_xml(request_id, "user", prompt); + request_id = request_id.saturating_add(1); + let mut assistant_message = String::with_capacity(256); + + while let Some(event) = stream.next().await { + match event? { + AgentStreamEvent::TextDelta { text, .. } => assistant_message.push_str(&text), + AgentStreamEvent::RequestStart { .. } => assistant_message.clear(), + AgentStreamEvent::ToolCallStart { tool_name, .. } => { + log_xml(request_id, "tool", &tool_name); + request_id = request_id.saturating_add(1); + } + AgentStreamEvent::ResponseComplete { .. } => { + log_xml(request_id, "assistant", &assistant_message); + request_id = request_id.saturating_add(1); + } + _ => {} + } + } + + Ok(()) +} diff --git a/src/llm-coding-tools-serdesai/examples/serdesai-basic.rs b/src/llm-coding-tools-serdesai/examples/serdesai-basic.rs index 4bca2182..0b83892d 100644 --- a/src/llm-coding-tools-serdesai/examples/serdesai-basic.rs +++ b/src/llm-coding-tools-serdesai/examples/serdesai-basic.rs @@ -12,14 +12,24 @@ use futures::StreamExt; use llm_coding_tools_serdesai::absolute::{GlobTool, GrepTool, ReadTool}; use llm_coding_tools_serdesai::agent_ext::AgentBuilderExt; use llm_coding_tools_serdesai::{BashTool, SystemPromptBuilder, WebFetchTool, create_todo_tools}; -use serdes_ai::models::openrouter::OpenRouterModel; +use serdes_ai::models::openai::OpenAIChatModel; use serdes_ai::prelude::*; use std::fmt::Write; -// API key below has a zero spend limit; it cannot incur charges. -// If this no longer works, find a free model to use on OpenRouter for testing. -const OPENROUTER_API_KEY: &str = ""; -const OPENROUTER_MODEL: &str = "z-ai/glm-4.5-air:free"; +// Set your OpenAI API key here or via OPENAI_API_KEY environment variable. +/// Fallback API key if env var is not set. Leave empty to require env var. +const OPENAI_API_KEY: &str = ""; +const OPENAI_MODEL: &str = "hf:zai-org/GLM-4.7"; +const OPENAI_BASE_URL: &str = "https://api.synthetic.new/openai/v1"; + +fn get_openai_api_key() -> String { + std::env::var("OPENAI_API_KEY").unwrap_or_else(|_| { + if OPENAI_API_KEY.is_empty() { + panic!("OPENAI_API_KEY environment variable must be set"); + } + OPENAI_API_KEY.to_string() + }) +} #[tokio::main] async fn main() -> std::result::Result<(), Box> { @@ -31,7 +41,8 @@ async fn main() -> std::result::Result<(), Box> { let (todo_read, todo_write, _state) = create_todo_tools(); // === Build agent with tools - call .system_prompt() last === - let model = OpenRouterModel::new(OPENROUTER_MODEL, OPENROUTER_API_KEY); + let model = + OpenAIChatModel::new(OPENAI_MODEL, get_openai_api_key()).with_base_url(OPENAI_BASE_URL); let agent = AgentBuilder::<(), String>::new(model) .instructions("Use tools to answer; call at least one tool before responding.") // File operations diff --git a/src/llm-coding-tools-serdesai/examples/serdesai-sandboxed.rs b/src/llm-coding-tools-serdesai/examples/serdesai-sandboxed.rs index 71c66218..458cb615 100644 --- a/src/llm-coding-tools-serdesai/examples/serdesai-sandboxed.rs +++ b/src/llm-coding-tools-serdesai/examples/serdesai-sandboxed.rs @@ -14,14 +14,18 @@ use llm_coding_tools_serdesai::AllowedPathResolver; use llm_coding_tools_serdesai::SystemPromptBuilder; use llm_coding_tools_serdesai::agent_ext::AgentBuilderExt; use llm_coding_tools_serdesai::allowed::{EditTool, GlobTool, GrepTool, ReadTool, WriteTool}; -use serdes_ai::models::openrouter::OpenRouterModel; +use serdes_ai::models::openai::OpenAIChatModel; use serdes_ai::prelude::*; use std::fmt::Write; -// API key below has a zero spend limit; it cannot incur charges. -// If this no longer works, find a free model to use on OpenRouter for testing. -const OPENROUTER_API_KEY: &str = ""; -const OPENROUTER_MODEL: &str = "z-ai/glm-4.5-air:free"; +// Set your OpenAI API key here or via OPENAI_API_KEY environment variable. +const OPENAI_API_KEY: &str = ""; +const OPENAI_MODEL: &str = "hf:zai-org/GLM-4.7"; +const OPENAI_BASE_URL: &str = "https://api.synthetic.new/openai/v1"; + +fn get_openai_api_key() -> String { + std::env::var("OPENAI_API_KEY").unwrap_or_else(|_| OPENAI_API_KEY.to_string()) +} #[tokio::main] async fn main() -> std::result::Result<(), Box> { @@ -55,7 +59,8 @@ async fn main() -> std::result::Result<(), Box> { .working_directory(std::env::current_dir()?.display().to_string()) .allowed_paths(&resolver); - let model = OpenRouterModel::new(OPENROUTER_MODEL, OPENROUTER_API_KEY); + let model = + OpenAIChatModel::new(OPENAI_MODEL, get_openai_api_key()).with_base_url(OPENAI_BASE_URL); let agent = AgentBuilder::<(), String>::new(model) .instructions("Use tools to answer; call at least one tool before responding.") .tool(pb.track(read)) diff --git a/src/llm-coding-tools-serdesai/src/lib.rs b/src/llm-coding-tools-serdesai/src/lib.rs index 9e9e3e9e..15d55b3f 100644 --- a/src/llm-coding-tools-serdesai/src/lib.rs +++ b/src/llm-coding-tools-serdesai/src/lib.rs @@ -1,5 +1,4 @@ #![doc = include_str!(concat!("../", env!("CARGO_PKG_README")))] -#![warn(missing_docs)] pub mod absolute; pub mod agent_ext; @@ -7,7 +6,12 @@ pub mod allowed; pub mod bash; mod common; pub mod convert; +/// Model resolver for resolving model specs into provider-specific settings. +pub mod model_resolver; +pub mod registry; +pub mod task; pub mod todo; +pub mod tool_catalog; pub mod webfetch; /// Re-export core types for convenience. @@ -46,5 +50,15 @@ pub use llm_coding_tools_core::{ // Re-export standalone tools pub use bash::BashTool; +pub use model_resolver::{ + ModelResolveError, ModelResolver, ModelsDevResolver, ProviderOverride, ProviderOverrides, + ResolutionSource, ResolvedModel, SharedModelResolver, +}; +pub use registry::{ + AgentDefaults, AgentRegistry, AgentRegistryBuildError, AgentRegistryBuilder, + AgentRegistryEntry, RegistryAgent, RegistryAgentError, +}; +pub use task::{TaskDefinitionSnapshot, TaskRegistryHandle, TaskTargetSummary, TaskTool}; pub use todo::{TodoReadTool, TodoWriteTool, create_todo_tools}; +pub use tool_catalog::{ToolCatalogEntry, default_tools}; pub use webfetch::WebFetchTool; diff --git a/src/llm-coding-tools-serdesai/src/model_resolver.rs b/src/llm-coding-tools-serdesai/src/model_resolver.rs new file mode 100644 index 00000000..aa0ec12a --- /dev/null +++ b/src/llm-coding-tools-serdesai/src/model_resolver.rs @@ -0,0 +1,1131 @@ +//! Model resolver for resolving model specs into provider-specific settings using models.dev catalog. + +use llm_coding_tools_models_dev::{ModelsDevCatalog, ProviderMetadata}; +use std::collections::HashMap; +use std::env; +use std::fmt; +use std::sync::Arc; +use std::time::Duration; + +/// Resolved model settings computed by a [`ModelResolver`]. +#[derive(Clone)] +pub struct ResolvedModel { + /// Original model spec as requested by agent/frontmatter. + pub spec: String, + /// Runtime provider family used by registry model construction. + pub runtime_provider: String, + /// Runtime model identifier consumed by provider-specific builders. + pub runtime_model_id: String, + /// Runtime canonical `provider:model` spec used by `ModelConfig`. + pub runtime_spec: String, + /// Resolved API key, if required by the provider. + pub api_key: Option, + /// Resolved base URL, when supported and required. + pub base_url: Option, + /// Optional per-model timeout override. + pub timeout: Option, + /// Source of the resolution result. + pub source: ResolutionSource, + /// Original provider ID from models.dev metadata. + pub provider_id: String, +} + +impl fmt::Debug for ResolvedModel { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("ResolvedModel") + .field("spec", &self.spec) + .field("runtime_provider", &self.runtime_provider) + .field("runtime_model_id", &self.runtime_model_id) + .field("runtime_spec", &self.runtime_spec) + .field("api_key", &self.api_key.as_ref().map(|_| "[REDACTED]")) + .field("base_url", &self.base_url) + .field("timeout", &self.timeout) + .field("source", &self.source) + .field("provider_id", &self.provider_id) + .finish() + } +} + +/// Tracks how a resolved model was determined. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ResolutionSource { + /// Explicit override provided (e.g., API key/base URL). + ExplicitOverride, + /// Resolved via models.dev catalog. + ModelsDev, + /// Fallback behavior when catalog is unavailable. + Fallback, +} + +/// Per-provider overrides for API key/base URL resolution. +#[derive(Clone, Default)] +pub struct ProviderOverride { + /// Explicit API key override for this provider. + pub api_key: Option, + /// Explicit base URL override for this provider. + pub base_url: Option, + /// Explicit endpoint env var name for base URL lookup. + pub endpoint_env: Option, +} + +impl fmt::Debug for ProviderOverride { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("ProviderOverride") + .field("api_key", &self.api_key.as_ref().map(|_| "[REDACTED]")) + .field("base_url", &self.base_url) + .field("endpoint_env", &self.endpoint_env) + .finish() + } +} + +/// Overrides keyed by provider ID with optional default provider preference. +#[derive(Clone, Default)] +pub struct ProviderOverrides { + /// Preferred provider ID for ambiguous model IDs. + default_provider: Option, + /// Per-provider override entries. + providers: HashMap, +} + +impl fmt::Debug for ProviderOverrides { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("ProviderOverrides") + .field("default_provider", &self.default_provider) + .field("providers", &self.providers) + .finish() + } +} + +impl ProviderOverrides { + /// Create an empty override set. + /// + /// Returns: an empty [`ProviderOverrides`]. + pub fn new() -> Self { + Self::default() + } + + /// Set the default provider preference. + /// + /// Parameters: + /// - `provider`: provider ID to prefer for ambiguous model IDs. + /// + /// Returns: updated [`ProviderOverrides`]. + pub fn with_default_provider(mut self, provider: impl Into) -> Self { + self.default_provider = Some(provider.into()); + self + } + + /// Insert a provider override. + /// + /// Parameters: + /// - `provider`: provider ID to override. + /// - `value`: override settings for that provider. + /// + /// Returns: updated [`ProviderOverrides`]. + pub fn insert_override(mut self, provider: impl Into, value: ProviderOverride) -> Self { + self.providers.insert(provider.into(), value); + self + } + + /// Returns an override entry for a provider, if configured. + /// + /// Parameters: + /// - `provider`: provider ID to look up. + /// + /// Returns: `Some(&ProviderOverride)` when present. + pub fn get_override(&self, provider: &str) -> Option<&ProviderOverride> { + self.providers.get(provider) + } +} + +/// Errors produced by model resolution. +#[derive(Debug, Clone)] +pub enum ModelResolveError { + /// Model ID not found in catalog. + NotFound(String), + /// Provider prefix is unknown. + UnknownProvider(String), + /// Provider is not supported by ModelConfig/build_model_with_config. + UnsupportedProvider { + /// Provider ID. + provider: String, + /// NPM package name, if available. + npm: Option, + }, + /// Provider exists but does not support the requested model. + ModelNotSupported { + /// Provider ID. + provider: String, + /// Model ID that is not supported. + model_id: String, + }, + /// Model ID maps to multiple providers and cannot be disambiguated. + Ambiguous { + /// Model ID that is ambiguous. + model_id: String, + /// List of provider IDs that support this model. + providers: Vec, + /// The default provider preference, if set. + default_provider: Option, + }, + /// No API key env var candidate found. + MissingApiKeyEnv { + /// Provider ID. + provider: String, + }, + /// API key env var missing or empty. + MissingApiKeyValue { + /// Provider ID. + provider: String, + /// Environment variable name. + env: String, + }, + /// Base URL is required but missing. + MissingBaseUrl { + /// Provider ID. + provider: String, + }, + /// Base URL was provided for a provider that does not support it. + BaseUrlUnsupported { + /// Provider ID. + provider: String, + }, +} + +impl fmt::Display for ModelResolveError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::NotFound(model) => write!(f, "model '{model}' not found"), + Self::UnknownProvider(provider) => write!(f, "unknown provider '{provider}'"), + Self::UnsupportedProvider { provider, npm } => write!( + f, + "unsupported provider '{provider}' (npm: {:?})", + npm.as_deref() + ), + Self::ModelNotSupported { provider, model_id } => write!( + f, + "model '{model_id}' is not supported by provider '{provider}'" + ), + Self::Ambiguous { + model_id, + providers, + default_provider, + } => write!( + f, + "model '{model_id}' is ambiguous across providers: {:?} (default: {:?})", + providers, default_provider + ), + Self::MissingApiKeyEnv { provider } => { + write!(f, "no API key env var candidate for provider '{provider}'") + } + Self::MissingApiKeyValue { provider, env } => write!( + f, + "API key env var '{env}' missing for provider '{provider}'" + ), + Self::MissingBaseUrl { provider } => write!( + f, + "missing base URL for provider '{provider}' (can be supplied via override or env var)" + ), + Self::BaseUrlUnsupported { provider } => write!( + f, + "base URL overrides are unsupported for provider '{provider}'" + ), + } + } +} + +impl std::error::Error for ModelResolveError {} + +/// Resolves model specs into per-provider settings. +pub trait ModelResolver: Send + Sync { + /// Resolves the provided model spec. + /// + /// Parameters: + /// - `model_spec`: input spec in `provider:model` or `model` format. + /// + /// Returns: resolved model settings or a [`ModelResolveError`]. + fn resolve(&self, model_spec: &str) -> Result; +} + +/// Shared, cloneable resolver handle for registry defaults and builder wiring. +pub type SharedModelResolver = Arc; + +/// models.dev-backed resolver implementation. +#[derive(Debug, Clone)] +pub struct ModelsDevResolver { + /// Optional catalog for models.dev lookups. + catalog: Option, + /// Override values for resolution behavior. + overrides: ProviderOverrides, +} + +impl ModelsDevResolver { + /// Create a resolver with optional catalog and overrides. + /// + /// Parameters: + /// - `catalog`: optional models.dev catalog. + /// - `overrides`: provider override configuration. + /// + /// Returns: a new [`ModelsDevResolver`]. + pub fn new(catalog: Option, overrides: ProviderOverrides) -> Self { + Self { catalog, overrides } + } +} + +struct ParsedModelSpec<'a> { + requested_spec: &'a str, + explicit_provider: Option<&'a str>, + model_id: &'a str, +} + +fn parse_model_spec<'a>(model_spec: &'a str) -> ParsedModelSpec<'a> { + let colon_pos = model_spec.find(':'); + let slash_pos = model_spec.find('/'); + + let use_colon = match (colon_pos, slash_pos) { + (Some(c), Some(s)) => c < s, + (Some(_), None) => true, + (None, Some(_)) => false, + (None, None) => false, + }; + + if use_colon { + if let Some((provider, model_id)) = model_spec.split_once(':') + && !provider.is_empty() + && !model_id.is_empty() + { + return ParsedModelSpec { + requested_spec: model_spec, + explicit_provider: Some(provider), + model_id, + }; + } + } else if let Some((provider, model_id)) = model_spec.split_once('/') + && !provider.is_empty() + && !model_id.is_empty() + { + return ParsedModelSpec { + requested_spec: model_spec, + explicit_provider: Some(provider), + model_id, + }; + } + + ParsedModelSpec { + requested_spec: model_spec, + explicit_provider: None, + model_id: model_spec, + } +} + +fn infer_runtime_from_raw_spec(model_spec: &str) -> (String, String, String) { + let parsed = parse_model_spec(model_spec); + let provider = parsed.explicit_provider.unwrap_or("openai"); + let model_id = parsed.model_id; + + let runtime_provider = match provider { + "anthropic" => "anthropic", + "google" => "google", + "groq" => "groq", + "mistral" => "mistral", + "cohere" => "cohere", + "ollama" => "ollama", + "openrouter" => "openrouter", + "huggingface" => "huggingface", + _ => "openai", + }; + + let runtime_spec = format!("{}:{}", runtime_provider, model_id); + ( + runtime_provider.to_string(), + model_id.to_string(), + runtime_spec, + ) +} + +impl ModelResolver for ModelsDevResolver { + fn resolve(&self, model_spec: &str) -> Result { + let Some(catalog) = &self.catalog else { + let (runtime_provider, runtime_model_id, runtime_spec) = + infer_runtime_from_raw_spec(model_spec); + return Ok(ResolvedModel { + spec: model_spec.to_string(), + runtime_provider, + runtime_model_id, + runtime_spec, + api_key: None, + base_url: None, + timeout: None, + source: ResolutionSource::Fallback, + provider_id: String::new(), + }); + }; + + let parsed = parse_model_spec(model_spec); + let provider_prefix = parsed.explicit_provider; + let model_id = parsed.model_id; + + if let Some(provider_id) = provider_prefix { + let provider = catalog + .get_provider(provider_id) + .ok_or_else(|| ModelResolveError::UnknownProvider(provider_id.to_string()))?; + let providers = catalog.resolve_provider_for_model(model_id).unwrap_or(&[]); + if !providers.iter().any(|id| id == provider_id) { + return Err(ModelResolveError::ModelNotSupported { + provider: provider_id.to_string(), + model_id: model_id.to_string(), + }); + } + + return resolve_provider_model( + provider, + parsed.requested_spec, + model_id, + true, + &self.overrides, + ); + } + + let providers = catalog.resolve_provider_for_model(model_id).unwrap_or(&[]); + if providers.is_empty() { + return Err(ModelResolveError::NotFound(model_id.to_string())); + } + + let provider_id = if providers.len() == 1 { + // Single provider is not ambiguous - select it directly + providers.first().map(String::as_str).unwrap() + } else if let Some(default_provider) = self.overrides.default_provider.as_deref() { + if let Some(found) = providers.iter().find(|id| id.as_str() == default_provider) { + found.as_str() + } else { + return Err(ModelResolveError::Ambiguous { + model_id: model_id.to_string(), + providers: providers.to_vec(), + default_provider: Some(default_provider.to_string()), + }); + } + } else if let Some(found) = providers.iter().find(|id| id.as_str() == "openai") { + found.as_str() + } else { + return Err(ModelResolveError::Ambiguous { + model_id: model_id.to_string(), + providers: providers.to_vec(), + default_provider: None, + }); + }; + + let provider = catalog + .get_provider(provider_id) + .ok_or_else(|| ModelResolveError::UnknownProvider(provider_id.to_string()))?; + resolve_provider_model( + provider, + parsed.requested_spec, + model_id, + false, + &self.overrides, + ) + } +} + +fn resolve_provider_model( + provider: &ProviderMetadata, + requested_spec: &str, + model_id: &str, + explicit_provider: bool, + overrides: &ProviderOverrides, +) -> Result { + let (serdes_provider, supports_base_url, requires_api_key, requires_base_url) = + match provider.npm.as_deref() { + Some("@ai-sdk/openai") => ("openai", true, true, false), + Some("@ai-sdk/openai-compatible") => ("openai", true, true, true), + Some("@ai-sdk/anthropic") => ("anthropic", true, true, false), + Some("@ai-sdk/groq") => ("groq", false, true, false), + Some("@ai-sdk/mistral") => ("mistral", true, true, false), + Some("@ai-sdk/google") => ("google", true, true, false), + Some("@ai-sdk/cohere") => ("cohere", true, true, false), + Some("@ai-sdk/ollama") => ("ollama", true, false, false), + Some("@ai-sdk/openrouter") => ("openrouter", false, true, false), + Some("@ai-sdk/huggingface") => ("huggingface", true, false, false), + Some("@ai-sdk/azure") + | Some("@ai-sdk/google-vertex") + | Some("@ai-sdk/google-vertex/anthropic") => { + return Err(ModelResolveError::UnsupportedProvider { + provider: provider.id.clone(), + npm: provider.npm.clone(), + }); + } + Some(_) | None => { + return Err(ModelResolveError::UnsupportedProvider { + provider: provider.id.clone(), + npm: provider.npm.clone(), + }); + } + }; + + let override_entry = overrides.providers.get(&provider.id); + + let (api_key, used_api_override) = if let Some(api_key) = override_entry + .and_then(|cfg| cfg.api_key.as_deref()) + .filter(|value| !value.trim().is_empty()) + { + (Some(api_key.to_string()), true) + } else if !requires_api_key { + (None, false) + } else { + let env_name = provider + .env + .iter() + .map(|value| value.as_str()) + .find(|name| { + let upper = name.to_ascii_uppercase(); + let is_key_like = + upper.contains("API_KEY") || upper.contains("TOKEN") || upper.ends_with("_KEY"); + let is_endpoint = upper.contains("ENDPOINT") + || upper.contains("BASE_URL") + || upper.contains("BASEURL") + || upper.contains("API_URL"); + let is_misc = upper.contains("PROJECT") + || upper.contains("LOCATION") + || upper.contains("CREDENTIALS") + || upper.contains("RESOURCE"); + is_key_like && !is_endpoint && !is_misc + }) + .ok_or_else(|| ModelResolveError::MissingApiKeyEnv { + provider: provider.id.clone(), + })?; + + let value = env::var(env_name).map_err(|_| ModelResolveError::MissingApiKeyValue { + provider: provider.id.clone(), + env: env_name.to_string(), + })?; + if value.trim().is_empty() { + return Err(ModelResolveError::MissingApiKeyValue { + provider: provider.id.clone(), + env: env_name.to_string(), + }); + } + (Some(value), false) + }; + + let (base_url, used_base_override) = if let Some(base_url) = override_entry + .and_then(|cfg| cfg.base_url.as_deref()) + .filter(|value| !value.trim().is_empty()) + { + if !supports_base_url { + return Err(ModelResolveError::BaseUrlUnsupported { + provider: provider.id.clone(), + }); + } + (Some(base_url.to_string()), true) + } else if !supports_base_url { + (None, false) + } else { + let endpoint_env = override_entry + .and_then(|cfg| cfg.endpoint_env.as_deref()) + .or_else(|| { + provider + .env + .iter() + .map(|value| value.as_str()) + .find(|name| { + let upper = name.to_ascii_uppercase(); + let is_endpoint = upper.contains("ENDPOINT") + || upper.contains("BASE_URL") + || upper.contains("BASEURL") + || upper.contains("API_URL"); + let is_key_like = upper.contains("API_KEY") + || upper.contains("TOKEN") + || upper.ends_with("_KEY"); + is_endpoint && !is_key_like + }) + }); + + let env_value = endpoint_env + .and_then(|env_name| env::var(env_name).ok()) + .filter(|value| !value.trim().is_empty()); + + if let Some(value) = env_value { + (Some(value), false) + } else if requires_base_url { + let api = provider + .api + .clone() + .ok_or_else(|| ModelResolveError::MissingBaseUrl { + provider: provider.id.clone(), + })?; + (Some(api), false) + } else { + (None, false) + } + }; + + let runtime_spec = format!("{}:{}", serdes_provider, model_id); + let source = if explicit_provider || used_api_override || used_base_override { + ResolutionSource::ExplicitOverride + } else { + ResolutionSource::ModelsDev + }; + + Ok(ResolvedModel { + spec: requested_spec.to_string(), + runtime_provider: serdes_provider.to_string(), + runtime_model_id: model_id.to_string(), + runtime_spec, + api_key, + base_url, + timeout: None, + source, + provider_id: provider.id.clone(), + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::Mutex; + + static ENV_LOCK: Mutex<()> = Mutex::new(()); + + fn catalog_from_json(json: &str) -> ModelsDevCatalog { + let temp = tempfile::TempDir::new().expect("tempdir"); + let path = temp.path().join("api.json"); + std::fs::write(&path, json).expect("write api.json"); + ModelsDevCatalog::from_local_api_json(&path).expect("catalog") + } + + #[test] + fn prefixed_resolution_success() { + let _guard = ENV_LOCK.lock().unwrap(); + unsafe { std::env::set_var("ALPHA_API_KEY", "key") }; + let json = r#"{"providers":{"alpha":{"id":"alpha","npm":"@ai-sdk/openai","api":null,"env":["ALPHA_API_KEY"],"models":{"m1":{}}}}}"#; + let resolver = + ModelsDevResolver::new(Some(catalog_from_json(json)), ProviderOverrides::new()); + let resolved = resolver.resolve("alpha:m1").expect("resolve"); + assert_eq!(resolved.spec, "alpha:m1"); + assert_eq!(resolved.runtime_spec, "openai:m1"); + assert_eq!(resolved.runtime_provider, "openai"); + assert_eq!(resolved.runtime_model_id, "m1"); + unsafe { std::env::remove_var("ALPHA_API_KEY") }; + } + + #[test] + fn prefixed_unknown_provider_errors() { + let resolver = ModelsDevResolver::new( + Some(catalog_from_json(r#"{"providers":{}}"#)), + ProviderOverrides::new(), + ); + let err = resolver + .resolve("unknown:m1") + .expect_err("unknown provider"); + assert!(matches!(err, ModelResolveError::UnknownProvider(_))); + } + + #[test] + fn unique_provider_resolution_success() { + let _guard = ENV_LOCK.lock().unwrap(); + unsafe { std::env::set_var("ALPHA_API_KEY", "key") }; + let json = r#"{"providers":{"alpha":{"id":"alpha","npm":"@ai-sdk/openai","api":null,"env":["ALPHA_API_KEY"],"models":{"m1":{}}}}}"#; + let resolver = + ModelsDevResolver::new(Some(catalog_from_json(json)), ProviderOverrides::new()); + let resolved = resolver.resolve("m1").expect("resolve"); + assert_eq!(resolved.spec, "m1"); + assert_eq!(resolved.runtime_spec, "openai:m1"); + assert_eq!(resolved.runtime_provider, "openai"); + assert_eq!(resolved.runtime_model_id, "m1"); + unsafe { std::env::remove_var("ALPHA_API_KEY") }; + } + + #[test] + fn ambiguous_model_without_openai_errors() { + let json = r#"{"providers":{"alpha":{"id":"alpha","npm":"@ai-sdk/anthropic","api":null,"env":["ALPHA_API_KEY"],"models":{"m1":{}}},"beta":{"id":"beta","npm":"@ai-sdk/mistral","api":null,"env":["BETA_API_KEY"],"models":{"m1":{}}}}}"#; + let resolver = + ModelsDevResolver::new(Some(catalog_from_json(json)), ProviderOverrides::new()); + let err = resolver.resolve("m1").expect_err("ambiguous"); + assert!(matches!(err, ModelResolveError::Ambiguous { .. })); + } + + #[test] + fn fallback_when_catalog_missing() { + let resolver = ModelsDevResolver::new(None, ProviderOverrides::new()); + let resolved = resolver.resolve("openai:gpt-4o").expect("fallback"); + assert_eq!(resolved.spec, "openai:gpt-4o"); + assert_eq!(resolved.runtime_provider, "openai"); + assert_eq!(resolved.runtime_model_id, "gpt-4o"); + assert_eq!(resolved.runtime_spec, "openai:gpt-4o"); + assert!(matches!(resolved.source, ResolutionSource::Fallback)); + } + + #[test] + fn fallback_preserves_provider_id() { + let resolver = ModelsDevResolver::new(None, ProviderOverrides::new()); + let resolved = resolver.resolve("any:model").expect("fallback"); + assert_eq!(resolved.provider_id, ""); + assert_eq!(resolved.runtime_provider, "openai"); + assert_eq!(resolved.runtime_model_id, "model"); + } + + #[test] + fn api_key_resolution_success() { + let _guard = ENV_LOCK.lock().unwrap(); + unsafe { std::env::set_var("ALPHA_API_KEY", "key") }; + let json = r#"{"providers":{"alpha":{"id":"alpha","npm":"@ai-sdk/openai","api":null,"env":["ALPHA_API_KEY"],"models":{"m1":{}}}}}"#; + let resolver = + ModelsDevResolver::new(Some(catalog_from_json(json)), ProviderOverrides::new()); + let resolved = resolver.resolve("alpha:m1").expect("resolve"); + assert_eq!(resolved.api_key.as_deref(), Some("key")); + unsafe { std::env::remove_var("ALPHA_API_KEY") }; + } + + #[test] + fn api_key_from_override() { + let _guard = ENV_LOCK.lock().unwrap(); + let json = r#"{"providers":{"alpha":{"id":"alpha","npm":"@ai-sdk/openai","api":null,"env":["ALPHA_API_KEY"],"models":{"m1":{}}}}}"#; + let overrides = ProviderOverrides::new().insert_override( + "alpha", + ProviderOverride { + api_key: Some("override_key".to_string()), + base_url: None, + endpoint_env: None, + }, + ); + let resolver = ModelsDevResolver::new(Some(catalog_from_json(json)), overrides); + let resolved = resolver.resolve("alpha:m1").expect("resolve"); + assert_eq!(resolved.api_key.as_deref(), Some("override_key")); + } + + #[test] + fn missing_api_key_env_errors() { + let _guard = ENV_LOCK.lock().unwrap(); + unsafe { std::env::remove_var("ALPHA_API_KEY") }; + let json = r#"{"providers":{"alpha":{"id":"alpha","npm":"@ai-sdk/openai","api":null,"env":["ALPHA_API_KEY"],"models":{"m1":{}}}}}"#; + let resolver = + ModelsDevResolver::new(Some(catalog_from_json(json)), ProviderOverrides::new()); + let err = resolver + .resolve("alpha:m1") + .expect_err("missing api key env"); + assert!(matches!(err, ModelResolveError::MissingApiKeyValue { .. })); + } + + #[test] + fn ollama_no_api_key_required() { + let json = r#"{"providers":{"ollama":{"id":"ollama","npm":"@ai-sdk/ollama","api":null,"env":[],"models":{"m1":{}}}}}"#; + let resolver = + ModelsDevResolver::new(Some(catalog_from_json(json)), ProviderOverrides::new()); + let resolved = resolver.resolve("ollama:m1").expect("resolve"); + assert_eq!(resolved.api_key, None); + assert_eq!(resolved.base_url, None); + } + + #[test] + fn provider_with_unsupported_env_rejected() { + let json = r#"{"providers":{"azure":{"id":"azure","npm":"@ai-sdk/azure","api":null,"env":["AZURE_API_KEY","AZURE_PROJECT"],"models":{"m1":{}}}}}"#; + let resolver = + ModelsDevResolver::new(Some(catalog_from_json(json)), ProviderOverrides::new()); + let err = resolver + .resolve("azure:m1") + .expect_err("unsupported provider"); + assert!(matches!(err, ModelResolveError::UnsupportedProvider { .. })); + } + + #[test] + fn provider_with_extra_non_key_env_resolves_successfully() { + let _guard = ENV_LOCK.lock().unwrap(); + unsafe { std::env::set_var("OPENAI_API_KEY", "key") }; + // Provider has extra env vars (PROJECT, REGION) but should still resolve via API key + let json = r#"{"providers":{"openai":{"id":"openai","npm":"@ai-sdk/openai","api":null,"env":["OPENAI_API_KEY","OPENAI_PROJECT","OPENAI_REGION"],"models":{"gpt-4":{}}}}}"#; + let resolver = + ModelsDevResolver::new(Some(catalog_from_json(json)), ProviderOverrides::new()); + let resolved = resolver.resolve("openai:gpt-4").expect("resolve"); + assert_eq!(resolved.spec, "openai:gpt-4"); + assert_eq!(resolved.runtime_spec, "openai:gpt-4"); + assert_eq!(resolved.api_key.as_deref(), Some("key")); + unsafe { std::env::remove_var("OPENAI_API_KEY") }; + } + + #[test] + fn provider_without_npm_rejected() { + let json = r#"{"providers":{"custom":{"id":"custom","npm":null,"api":null,"env":["CUSTOM_KEY"],"models":{"m1":{}}}}}"#; + let resolver = + ModelsDevResolver::new(Some(catalog_from_json(json)), ProviderOverrides::new()); + let err = resolver + .resolve("custom:m1") + .expect_err("unsupported provider"); + assert!(matches!(err, ModelResolveError::UnsupportedProvider { .. })); + } + + #[test] + fn base_url_resolution_success_from_api() { + let _guard = ENV_LOCK.lock().unwrap(); + unsafe { std::env::set_var("ROUTER_API_KEY", "key") }; + let json = r#"{"providers":{"router":{"id":"router","npm":"@ai-sdk/openai-compatible","api":"https://example.com","env":["ROUTER_API_KEY"],"models":{"m1":{}}}}}"#; + let resolver = + ModelsDevResolver::new(Some(catalog_from_json(json)), ProviderOverrides::new()); + let resolved = resolver.resolve("router:m1").expect("resolve"); + assert_eq!(resolved.base_url.as_deref(), Some("https://example.com")); + unsafe { std::env::remove_var("ROUTER_API_KEY") }; + } + + #[test] + fn base_url_precedence_override_then_env_then_api() { + let _guard = ENV_LOCK.lock().unwrap(); + unsafe { std::env::set_var("ROUTER_API_KEY", "key") }; + unsafe { std::env::set_var("ROUTER_ENDPOINT", "https://env.example.com") }; + let json = r#"{"providers":{"router":{"id":"router","npm":"@ai-sdk/openai-compatible","api":"https://api.example.com","env":["ROUTER_API_KEY","ROUTER_ENDPOINT"],"models":{"m1":{}}}}}"#; + let overrides = ProviderOverrides::new().insert_override( + "router", + ProviderOverride { + api_key: None, + base_url: Some("https://override.example.com".to_string()), + endpoint_env: None, + }, + ); + let resolver = ModelsDevResolver::new(Some(catalog_from_json(json)), overrides); + let resolved = resolver.resolve("router:m1").expect("resolve"); + assert_eq!( + resolved.base_url.as_deref(), + Some("https://override.example.com") + ); + unsafe { std::env::remove_var("ROUTER_API_KEY") }; + unsafe { std::env::remove_var("ROUTER_ENDPOINT") }; + } + + #[test] + fn base_url_precedence_env_over_api() { + let _guard = ENV_LOCK.lock().unwrap(); + unsafe { std::env::set_var("ROUTER_API_KEY", "key") }; + unsafe { std::env::set_var("ROUTER_ENDPOINT", "https://env.example.com") }; + let json = r#"{"providers":{"router":{"id":"router","npm":"@ai-sdk/openai-compatible","api":"https://api.example.com","env":["ROUTER_API_KEY","ROUTER_ENDPOINT"],"models":{"m1":{}}}}}"#; + let resolver = + ModelsDevResolver::new(Some(catalog_from_json(json)), ProviderOverrides::new()); + let resolved = resolver.resolve("router:m1").expect("resolve"); + assert_eq!( + resolved.base_url.as_deref(), + Some("https://env.example.com") + ); + unsafe { std::env::remove_var("ROUTER_API_KEY") }; + unsafe { std::env::remove_var("ROUTER_ENDPOINT") }; + } + + #[test] + fn base_url_endpoint_env_override() { + let _guard = ENV_LOCK.lock().unwrap(); + unsafe { std::env::set_var("ROUTER_API_KEY", "key") }; + unsafe { std::env::set_var("CUSTOM_ENDPOINT", "https://custom.example.com") }; + let json = r#"{"providers":{"router":{"id":"router","npm":"@ai-sdk/openai-compatible","api":"https://api.example.com","env":["ROUTER_API_KEY","ROUTER_ENDPOINT"],"models":{"m1":{}}}}}"#; + let overrides = ProviderOverrides::new().insert_override( + "router", + ProviderOverride { + api_key: None, + base_url: None, + endpoint_env: Some("CUSTOM_ENDPOINT".to_string()), + }, + ); + let resolver = ModelsDevResolver::new(Some(catalog_from_json(json)), overrides); + let resolved = resolver.resolve("router:m1").expect("resolve"); + assert_eq!( + resolved.base_url.as_deref(), + Some("https://custom.example.com") + ); + unsafe { std::env::remove_var("ROUTER_API_KEY") }; + unsafe { std::env::remove_var("CUSTOM_ENDPOINT") }; + } + + #[test] + fn missing_base_url_errors_when_required() { + let _guard = ENV_LOCK.lock().unwrap(); + unsafe { + std::env::remove_var("ROUTER_ENDPOINT"); + std::env::remove_var("ROUTER_BASE_URL"); + std::env::remove_var("ROUTER_API_URL"); + std::env::set_var("ROUTER_API_KEY", "key") + }; + let json = r#"{"providers":{"router":{"id":"router","npm":"@ai-sdk/openai-compatible","api":null,"env":["ROUTER_API_KEY"],"models":{"m1":{}}}}}"#; + let resolver = + ModelsDevResolver::new(Some(catalog_from_json(json)), ProviderOverrides::new()); + let err = resolver.resolve("router:m1").expect_err("missing base url"); + assert!(matches!(err, ModelResolveError::MissingBaseUrl { .. })); + unsafe { std::env::remove_var("ROUTER_API_KEY") }; + } + + #[test] + fn base_url_unsupported_for_groq_override() { + let _guard = ENV_LOCK.lock().unwrap(); + unsafe { std::env::set_var("GROQ_API_KEY", "key") }; + let json = r#"{"providers":{"groq":{"id":"groq","npm":"@ai-sdk/groq","api":null,"env":["GROQ_API_KEY"],"models":{"m1":{}}}}}"#; + let overrides = ProviderOverrides::new().insert_override( + "groq", + ProviderOverride { + api_key: None, + base_url: Some("https://example.com".to_string()), + endpoint_env: None, + }, + ); + let resolver = ModelsDevResolver::new(Some(catalog_from_json(json)), overrides); + let err = resolver + .resolve("groq:m1") + .expect_err("base_url unsupported"); + assert!(matches!(err, ModelResolveError::BaseUrlUnsupported { .. })); + unsafe { std::env::remove_var("GROQ_API_KEY") }; + } + + #[test] + fn model_not_supported_by_provider_errors() { + let _guard = ENV_LOCK.lock().unwrap(); + unsafe { std::env::set_var("ALPHA_API_KEY", "key") }; + let json = r#"{"providers":{"alpha":{"id":"alpha","npm":"@ai-sdk/openai","api":null,"env":["ALPHA_API_KEY"],"models":{"m1":{}}}}}"#; + let resolver = + ModelsDevResolver::new(Some(catalog_from_json(json)), ProviderOverrides::new()); + let err = resolver + .resolve("alpha:m2") + .expect_err("model not supported"); + assert!(matches!(err, ModelResolveError::ModelNotSupported { .. })); + unsafe { std::env::remove_var("ALPHA_API_KEY") }; + } + + #[test] + fn model_not_found_errors() { + let json = r#"{"providers":{"alpha":{"id":"alpha","npm":"@ai-sdk/openai","api":null,"env":["ALPHA_API_KEY"],"models":{"m1":{}}}}}"#; + let resolver = + ModelsDevResolver::new(Some(catalog_from_json(json)), ProviderOverrides::new()); + let err = resolver.resolve("unknown_model").expect_err("not found"); + assert!(matches!(err, ModelResolveError::NotFound(_))); + } + + #[test] + fn explicit_override_sets_source() { + let _guard = ENV_LOCK.lock().unwrap(); + unsafe { std::env::set_var("ALPHA_API_KEY", "key") }; + let json = r#"{"providers":{"alpha":{"id":"alpha","npm":"@ai-sdk/openai","api":null,"env":["ALPHA_API_KEY"],"models":{"m1":{}}}}}"#; + let resolver = + ModelsDevResolver::new(Some(catalog_from_json(json)), ProviderOverrides::new()); + let resolved = resolver.resolve("alpha:m1").expect("resolve"); + assert_eq!(resolved.source, ResolutionSource::ExplicitOverride); + unsafe { std::env::remove_var("ALPHA_API_KEY") }; + } + + #[test] + fn models_dev_source_set_for_implicit_resolution() { + let _guard = ENV_LOCK.lock().unwrap(); + unsafe { std::env::set_var("OPENAI_API_KEY", "key") }; + let json = r#"{"providers":{"openai":{"id":"openai","npm":"@ai-sdk/openai","api":null,"env":["OPENAI_API_KEY"],"models":{"gpt-4":{}}}}}"#; + let resolver = + ModelsDevResolver::new(Some(catalog_from_json(json)), ProviderOverrides::new()); + let resolved = resolver.resolve("gpt-4").expect("resolve"); + assert_eq!(resolved.source, ResolutionSource::ModelsDev); + unsafe { std::env::remove_var("OPENAI_API_KEY") }; + } + + #[test] + fn provider_id_preserved_for_openai_compatible() { + let _guard = ENV_LOCK.lock().unwrap(); + unsafe { std::env::set_var("ROUTER_API_KEY", "key") }; + let json = r#"{"providers":{"router":{"id":"router","npm":"@ai-sdk/openai-compatible","api":"https://example.com","env":["ROUTER_API_KEY"],"models":{"m1":{}}}}}"#; + let resolver = + ModelsDevResolver::new(Some(catalog_from_json(json)), ProviderOverrides::new()); + let resolved = resolver.resolve("router:m1").expect("resolve"); + assert_eq!(resolved.provider_id, "router"); + assert_eq!(resolved.spec, "router:m1"); + assert_eq!(resolved.runtime_spec, "openai:m1"); + assert_eq!(resolved.runtime_provider, "openai"); + unsafe { std::env::remove_var("ROUTER_API_KEY") }; + } + + #[test] + fn default_provider_disambiguates() { + let _guard = ENV_LOCK.lock().unwrap(); + unsafe { std::env::set_var("ALPHA_API_KEY", "key") }; + unsafe { std::env::set_var("BETA_API_KEY", "key") }; + let json = r#"{"providers":{"alpha":{"id":"alpha","npm":"@ai-sdk/anthropic","api":null,"env":["ALPHA_API_KEY"],"models":{"m1":{}}},"beta":{"id":"beta","npm":"@ai-sdk/mistral","api":null,"env":["BETA_API_KEY"],"models":{"m1":{}}}}}"#; + let overrides = ProviderOverrides::new().with_default_provider("beta"); + let resolver = ModelsDevResolver::new(Some(catalog_from_json(json)), overrides); + let resolved = resolver.resolve("m1").expect("resolve"); + assert_eq!(resolved.spec, "m1"); + assert_eq!(resolved.runtime_spec, "mistral:m1"); + assert_eq!(resolved.runtime_provider, "mistral"); + unsafe { std::env::remove_var("ALPHA_API_KEY") }; + unsafe { std::env::remove_var("BETA_API_KEY") }; + } + + #[test] + fn provider_overrides_empty_by_default() { + let overrides = ProviderOverrides::new(); + assert!(overrides.providers.is_empty()); + assert!(overrides.default_provider.is_none()); + } + + #[test] + fn token_based_api_key_recognized() { + let _guard = ENV_LOCK.lock().unwrap(); + unsafe { std::env::set_var("ANTHROPIC_TOKEN", "token") }; + let json = r#"{"providers":{"anthropic":{"id":"anthropic","npm":"@ai-sdk/anthropic","api":null,"env":["ANTHROPIC_TOKEN"],"models":{"claude-3":{}}}}}"#; + let resolver = + ModelsDevResolver::new(Some(catalog_from_json(json)), ProviderOverrides::new()); + let resolved = resolver.resolve("anthropic:claude-3").expect("resolve"); + assert_eq!(resolved.api_key.as_deref(), Some("token")); + unsafe { std::env::remove_var("ANTHROPIC_TOKEN") }; + } + + #[test] + fn suffix_key_based_api_key_recognized() { + let _guard = ENV_LOCK.lock().unwrap(); + unsafe { std::env::set_var("MISTRAL_KEY", "key") }; + let json = r#"{"providers":{"mistral":{"id":"mistral","npm":"@ai-sdk/mistral","api":null,"env":["MISTRAL_KEY"],"models":{"mistral-large":{}}}}}"#; + let resolver = + ModelsDevResolver::new(Some(catalog_from_json(json)), ProviderOverrides::new()); + let resolved = resolver.resolve("mistral:mistral-large").expect("resolve"); + assert_eq!(resolved.api_key.as_deref(), Some("key")); + unsafe { std::env::remove_var("MISTRAL_KEY") }; + } + + #[test] + fn anthropic_base_url_from_env() { + let _guard = ENV_LOCK.lock().unwrap(); + unsafe { std::env::set_var("ANTHROPIC_API_KEY", "key") }; + unsafe { std::env::set_var("ANTHROPIC_BASE_URL", "https://custom.anthropic.com") }; + let json = r#"{"providers":{"anthropic":{"id":"anthropic","npm":"@ai-sdk/anthropic","api":"https://api.anthropic.com","env":["ANTHROPIC_API_KEY","ANTHROPIC_BASE_URL"],"models":{"claude-3":{}}}}}"#; + let resolver = + ModelsDevResolver::new(Some(catalog_from_json(json)), ProviderOverrides::new()); + let resolved = resolver.resolve("anthropic:claude-3").expect("resolve"); + assert_eq!( + resolved.base_url.as_deref(), + Some("https://custom.anthropic.com") + ); + unsafe { std::env::remove_var("ANTHROPIC_API_KEY") }; + unsafe { std::env::remove_var("ANTHROPIC_BASE_URL") }; + } + + #[test] + fn google_provider_supported() { + let _guard = ENV_LOCK.lock().unwrap(); + unsafe { std::env::set_var("GOOGLE_API_KEY", "key") }; + let json = r#"{"providers":{"google":{"id":"google","npm":"@ai-sdk/google","api":null,"env":["GOOGLE_API_KEY"],"models":{"gemini-pro":{}}}}}"#; + let resolver = + ModelsDevResolver::new(Some(catalog_from_json(json)), ProviderOverrides::new()); + let resolved = resolver.resolve("google:gemini-pro").expect("resolve"); + assert_eq!(resolved.spec, "google:gemini-pro"); + assert_eq!(resolved.runtime_spec, "google:gemini-pro"); + assert_eq!(resolved.runtime_provider, "google"); + assert_eq!(resolved.api_key.as_deref(), Some("key")); + unsafe { std::env::remove_var("GOOGLE_API_KEY") }; + } + + #[test] + fn google_vertex_provider_rejected() { + let json = r#"{"providers":{"vertex":{"id":"vertex","npm":"@ai-sdk/google-vertex","api":null,"env":["VERTEX_PROJECT"],"models":{"gemini-pro":{}}}}}"#; + let resolver = + ModelsDevResolver::new(Some(catalog_from_json(json)), ProviderOverrides::new()); + let err = resolver + .resolve("vertex:gemini-pro") + .expect_err("unsupported provider"); + assert!(matches!(err, ModelResolveError::UnsupportedProvider { .. })); + } + + #[test] + fn empty_api_key_env_treated_as_missing() { + let _guard = ENV_LOCK.lock().unwrap(); + unsafe { std::env::set_var("ALPHA_API_KEY", "") }; + let json = r#"{"providers":{"alpha":{"id":"alpha","npm":"@ai-sdk/openai","api":null,"env":["ALPHA_API_KEY"],"models":{"m1":{}}}}}"#; + let resolver = + ModelsDevResolver::new(Some(catalog_from_json(json)), ProviderOverrides::new()); + let err = resolver.resolve("alpha:m1").expect_err("empty api key"); + assert!(matches!(err, ModelResolveError::MissingApiKeyValue { .. })); + unsafe { std::env::remove_var("ALPHA_API_KEY") }; + } + + #[test] + fn spec_with_colon_in_model_id_handles_correctly() { + let _guard = ENV_LOCK.lock().unwrap(); + unsafe { std::env::set_var("ALPHA_API_KEY", "key") }; + let json = r#"{"providers":{"alpha":{"id":"alpha","npm":"@ai-sdk/openai","api":null,"env":["ALPHA_API_KEY"],"models":{"model:v1":{}}}}}"#; + let resolver = + ModelsDevResolver::new(Some(catalog_from_json(json)), ProviderOverrides::new()); + let resolved = resolver.resolve("alpha:model:v1").expect("resolve"); + assert_eq!(resolved.spec, "alpha:model:v1"); + assert_eq!(resolved.runtime_provider, "openai"); + assert_eq!(resolved.runtime_model_id, "model:v1"); + assert_eq!(resolved.runtime_spec, "openai:model:v1"); + unsafe { std::env::remove_var("ALPHA_API_KEY") }; + } + + #[test] + fn fallback_populates_runtime_fields() { + let resolver = ModelsDevResolver::new(None, ProviderOverrides::new()); + let resolved = resolver + .resolve("synthetic/hf:zai-org/GLM-4.7") + .expect("fallback"); + assert_eq!(resolved.spec, "synthetic/hf:zai-org/GLM-4.7"); + assert_eq!(resolved.runtime_provider, "openai"); + assert_eq!(resolved.runtime_model_id, "hf:zai-org/GLM-4.7"); + assert_eq!(resolved.runtime_spec, "openai:hf:zai-org/GLM-4.7"); + assert!(matches!(resolved.source, ResolutionSource::Fallback)); + } + + #[test] + fn fallback_colon_form_with_slash_model_id_populates_runtime_fields() { + let resolver = ModelsDevResolver::new(None, ProviderOverrides::new()); + let resolved = resolver + .resolve("huggingface:tiiuae/falcon-7b") + .expect("fallback"); + assert_eq!(resolved.spec, "huggingface:tiiuae/falcon-7b"); + assert_eq!(resolved.runtime_provider, "huggingface"); + assert_eq!(resolved.runtime_model_id, "tiiuae/falcon-7b"); + assert_eq!(resolved.runtime_spec, "huggingface:tiiuae/falcon-7b"); + assert!(matches!(resolved.source, ResolutionSource::Fallback)); + } + + #[test] + fn slash_form_unknown_provider_errors() { + let _guard = ENV_LOCK.lock().unwrap(); + unsafe { std::env::set_var("ALPHA_API_KEY", "key") }; + let resolver = ModelsDevResolver::new( + Some(catalog_from_json( + r#"{"providers":{"alpha":{"id":"alpha","npm":"@ai-sdk/openai","api":null,"env":["ALPHA_API_KEY"],"models":{"m1":{}}}}}"#, + )), + ProviderOverrides::new(), + ); + let err = resolver + .resolve("unknown/m1") + .expect_err("unknown slash provider"); + assert!( + matches!(err, ModelResolveError::UnknownProvider(provider) if provider == "unknown") + ); + unsafe { std::env::remove_var("ALPHA_API_KEY") }; + } + + #[test] + fn slash_form_with_colon_model_id_resolves() { + let _guard = ENV_LOCK.lock().unwrap(); + unsafe { std::env::set_var("SYNTH_API_KEY", "key") }; + let resolver = ModelsDevResolver::new( + Some(catalog_from_json( + r#"{"providers":{"synthetic":{"id":"synthetic","npm":"@ai-sdk/openai-compatible","api":"https://api.synthetic/v1","env":["SYNTH_API_KEY"],"models":{"hf:zai-org/GLM-4.7":{}}}}}"#, + )), + ProviderOverrides::new(), + ); + let resolved = resolver + .resolve("synthetic/hf:zai-org/GLM-4.7") + .expect("resolve"); + assert_eq!(resolved.spec, "synthetic/hf:zai-org/GLM-4.7"); + assert_eq!(resolved.runtime_provider, "openai"); + assert_eq!(resolved.runtime_model_id, "hf:zai-org/GLM-4.7"); + unsafe { std::env::remove_var("SYNTH_API_KEY") }; + } + + #[test] + fn colon_form_with_slash_model_id_still_resolves() { + let _guard = ENV_LOCK.lock().unwrap(); + unsafe { std::env::set_var("HF_API_KEY", "key") }; + let resolver = ModelsDevResolver::new( + Some(catalog_from_json( + r#"{"providers":{"huggingface":{"id":"huggingface","npm":"@ai-sdk/openai-compatible","api":"https://api.hf/v1","env":["HF_API_KEY"],"models":{"tiiuae/falcon-7b":{}}}}}"#, + )), + ProviderOverrides::new(), + ); + let resolved = resolver + .resolve("huggingface:tiiuae/falcon-7b") + .expect("resolve"); + assert_eq!(resolved.spec, "huggingface:tiiuae/falcon-7b"); + assert_eq!(resolved.runtime_model_id, "tiiuae/falcon-7b"); + unsafe { std::env::remove_var("HF_API_KEY") }; + } +} diff --git a/src/llm-coding-tools-serdesai/src/registry.rs b/src/llm-coding-tools-serdesai/src/registry.rs new file mode 100644 index 00000000..369cc863 --- /dev/null +++ b/src/llm-coding-tools-serdesai/src/registry.rs @@ -0,0 +1,783 @@ +//! SerdesAI agent registry with precomputed tool context and system prompts. + +use crate::agent_ext::AgentBuilderExt; +use crate::model_resolver::{ + ModelResolver, ModelsDevResolver, ProviderOverrides, SharedModelResolver, +}; +use crate::task::{TaskDefinitionSnapshot, TaskRegistryHandle, TaskTargetSummary, TaskTool}; +use crate::tool_catalog::ToolCatalogEntry; +use async_trait::async_trait; +use indexmap::IndexMap; +use llm_coding_tools_agents::{AgentCatalog, AgentConfig, AgentMode, PermissionRule, RulesetExt}; +use llm_coding_tools_core::SystemPromptBuilder; +use llm_coding_tools_core::permissions::{PermissionAction, Ruleset}; +use llm_coding_tools_core::tool_names; +use llm_coding_tools_models_dev::ModelsDevCatalog; +use serde_json::{Map, Value}; +use serdes_ai::agent::ModelConfig; +use serdes_ai::{Agent, AgentBuilder, ModelSettings}; +use serdes_ai_models::huggingface::HuggingFaceModel; +use serdes_ai_models::openrouter::OpenRouterModel; +use ahash::AHashMap; +use std::sync::Arc; + +/// Default model + sampling settings for serdesAI agents. +#[derive(Clone)] +pub struct AgentDefaults { + /// Default model ID (e.g., "provider:model-id"). + pub model: String, + /// Optional injected resolver used to resolve per-agent model specs. + pub model_resolver: Option, + /// Per-provider overrides applied by the resolver. + pub provider_overrides: ProviderOverrides, + /// Legacy OpenAI API key override (applied only when provider is `openai`). + pub api_key: Option, + /// Legacy OpenAI base URL override (applied only when provider is `openai`). + pub base_url: Option, + /// Default temperature override (if any). + pub temperature: Option, + /// Default top-p override (if any). + pub top_p: Option, + /// Default additional model params merged into per-agent options. + pub options: AHashMap, +} + +impl std::fmt::Debug for AgentDefaults { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("AgentDefaults") + .field("model", &self.model) + .field( + "model_resolver", + &self.model_resolver.as_ref().map(|_| "[INJECTED]"), + ) + .field("provider_overrides", &self.provider_overrides) + .field("api_key", &self.api_key.as_ref().map(|_| "[REDACTED]")) + .field("base_url", &self.base_url) + .field("temperature", &self.temperature) + .field("top_p", &self.top_p) + .field("options", &self.options) + .finish() + } +} + +/// Errors returned when building a serdesAI agent registry. +#[derive(Debug)] +pub enum AgentRegistryBuildError { + /// No model was provided by defaults or agent config. + MissingModel { + /// The name of the agent missing a model. + agent: String, + }, + /// Failed to build the serdesAI agent instance. + BuildFailed { + /// The name of the agent that failed to build. + agent: String, + /// The error message describing the failure. + message: String, + }, +} + +impl std::fmt::Display for AgentRegistryBuildError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::MissingModel { agent } => write!(f, "missing model for agent '{agent}'"), + Self::BuildFailed { agent, message } => { + write!(f, "failed to build agent '{agent}': {message}") + } + } + } +} + +impl std::error::Error for AgentRegistryBuildError {} + +/// Error returned by registry agent invocations. +#[derive(Debug, Clone)] +pub struct RegistryAgentError { + /// Human-readable error message. + message: String, +} + +impl RegistryAgentError { + /// Creates a new error from any displayable message. + /// + /// Parameters: + /// - `message`: error message to store. + /// + /// Returns: a new [`RegistryAgentError`]. + pub fn new(message: impl Into) -> Self { + Self { + message: message.into(), + } + } +} + +impl std::fmt::Display for RegistryAgentError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.message) + } +} + +impl std::error::Error for RegistryAgentError {} + +/// Minimal prompt interface used by the registry + Task tool. +#[async_trait] +pub trait RegistryAgent: Send + Sync { + /// Executes a user prompt and returns the agent response. + /// + /// Parameters: + /// - `message`: fully constructed user message (includes task context + prompt). + /// - `deps`: dependencies passed to the agent run. + /// + /// Returns: agent output text on success. + async fn prompt(&self, message: String, deps: Arc) -> Result; +} + +#[async_trait] +impl RegistryAgent for Agent, String> +where + Deps: Send + Sync + 'static, +{ + async fn prompt(&self, message: String, deps: Arc) -> Result { + let result = self + .run(message, deps) + .await + .map_err(|err| RegistryAgentError::new(err.to_string()))?; + Ok(result.into_output()) + } +} + +/// Precomputed serdesAI registry entry for a single agent. +pub struct AgentRegistryEntry { + /// Source configuration used to build the agent. + pub config: AgentConfig, + /// Runtime-evaluated ruleset derived from config.permission. + pub ruleset: Ruleset, + /// Allowed tool names after permission filtering. + pub tool_names: Vec, + /// Prebuilt system prompt (tool context + agent prompt). + pub system_prompt: String, + /// Built serdesAI agent implementation. + pub agent: A, +} + +impl AgentRegistryEntry { + /// Returns true if the agent can be invoked via Task (Subagent or All). + /// + /// Returns: `true` when [`AgentMode::Subagent`] or [`AgentMode::All`]. + #[inline] + pub fn is_invocable(&self) -> bool { + matches!(self.config.mode, AgentMode::Subagent | AgentMode::All) + } +} + +/// SerdesAI registry mapping agent name to prebuilt entries. +pub struct AgentRegistry { + entries: AHashMap>, +} + +impl AgentRegistry { + /// Creates a registry from prebuilt entries. + /// + /// Parameters: + /// - `entries`: iterator of `(name, entry)` pairs. + /// + /// Returns: a populated [`AgentRegistry`]. + pub fn from_entries( + entries: impl IntoIterator)>, + ) -> Self { + Self { + entries: entries.into_iter().collect(), + } + } + + /// Returns the entry for a given agent name. + /// + /// Parameters: + /// - `name`: agent name to lookup. + /// + /// Returns: `Some(&AgentRegistryEntry)` if found; otherwise `None`. + #[inline] + pub fn get(&self, name: &str) -> Option<&AgentRegistryEntry> { + self.entries.get(name) + } + + /// Returns an iterator over all entries. + /// + /// Returns: iterator over `(name, entry)` pairs. + #[inline] + pub fn iter(&self) -> impl Iterator)> { + self.entries.iter() + } +} + +fn permission_rule_has_allow(rule: &PermissionRule) -> bool { + match rule { + PermissionRule::Action(action) => *action == PermissionAction::Allow, + PermissionRule::Pattern(patterns) => patterns + .values() + .any(|action| *action == PermissionAction::Allow), + } +} + +fn task_has_any_allow(permission: &IndexMap) -> bool { + permission.iter().any(|(key, rule)| { + key.eq_ignore_ascii_case(tool_names::TASK) && permission_rule_has_allow(rule) + }) +} + +/// Builder for constructing a serdesAI registry from configs + tools. +pub struct AgentRegistryBuilder { + defaults: AgentDefaults, + tools: Vec, + _deps: std::marker::PhantomData, +} + +struct SharedBuildSetup { + builder: AgentBuilder, String>, + prompt_builder: SystemPromptBuilder, + ruleset: Ruleset, + allowed_tools: Vec, + tool_names: Vec, + temperature: Option, + top_p: Option, +} + +impl AgentRegistryBuilder +where + Deps: Send + Sync + 'static, +{ + /// Creates a new registry builder. + /// + /// Parameters: + /// - `defaults`: default model + sampling settings. + /// - `tools`: cloneable tool catalog used for filtering and agent construction. + /// + /// Returns: a new [`AgentRegistryBuilder`]. + pub fn new(defaults: AgentDefaults, tools: Vec) -> Self { + Self { + defaults, + tools, + _deps: std::marker::PhantomData, + } + } + + fn default_models_dev_resolver(&self) -> SharedModelResolver { + let catalog = ModelsDevCatalog::load_shared_cache_or_bundled() + .map(|result| result.catalog) + .ok(); + Arc::new(ModelsDevResolver::new( + catalog, + self.defaults.provider_overrides.clone(), + )) + } + + fn create_resolver_from_defaults(&self) -> SharedModelResolver { + self.defaults + .model_resolver + .clone() + .unwrap_or_else(|| self.default_models_dev_resolver()) + } + + fn resolve_model_and_builder( + &self, + config: &AgentConfig, + resolver: &dyn ModelResolver, + ) -> Result, AgentRegistryBuildError> { + let model = config + .model + .clone() + .filter(|m| !m.is_empty()) + .or_else(|| Some(self.defaults.model.clone())) + .filter(|m| !m.is_empty()) + .ok_or_else(|| AgentRegistryBuildError::MissingModel { + agent: config.name.clone(), + })?; + let temperature = config.temperature.or(self.defaults.temperature); + let top_p = config.top_p.or(self.defaults.top_p); + + let mut resolved = + resolver + .resolve(&model) + .map_err(|err| AgentRegistryBuildError::BuildFailed { + agent: config.name.clone(), + message: err.to_string(), + })?; + + let resolved_provider = resolved.runtime_provider.as_str(); + let resolved_model_id = resolved.runtime_model_id.as_str(); + + debug_assert!(!resolved.runtime_provider.is_empty()); + debug_assert!(!resolved.runtime_model_id.is_empty()); + debug_assert!(!resolved.runtime_spec.is_empty()); + + if (resolved.provider_id.is_empty() || resolved.provider_id == "openai") + && resolved_provider == "openai" + { + if resolved.api_key.is_none() { + resolved.api_key = self.defaults.api_key.clone(); + } + if resolved.base_url.is_none() { + resolved.base_url = self.defaults.base_url.clone(); + } + } + + let ruleset = Ruleset::from_permission_config(&config.permission); + let mut allowed_tools = Vec::with_capacity(self.tools.len()); + let mut tool_names = Vec::with_capacity(self.tools.len()); + for tool in &self.tools { + if ruleset.is_allowed(tool.name(), "*") { + allowed_tools.push(tool.clone()); + tool_names.push(tool.name().to_string()); + } + } + + let mut prompt_builder = SystemPromptBuilder::new(); + if !config.prompt.is_empty() { + prompt_builder = prompt_builder.system_prompt(config.prompt.clone()); + } + + let builder = match resolved_provider { + "openrouter" => { + let model = if let Some(api_key) = resolved.api_key.as_deref() { + OpenRouterModel::new(resolved_model_id, api_key) + } else { + OpenRouterModel::from_env(resolved_model_id).map_err(|err| { + AgentRegistryBuildError::BuildFailed { + agent: config.name.clone(), + message: err.to_string(), + } + })? + }; + AgentBuilder::, String>::new(model) + } + "huggingface" => { + let mut model = if let Some(api_key) = resolved.api_key.as_deref() { + HuggingFaceModel::new(resolved_model_id, api_key) + } else { + HuggingFaceModel::from_env(resolved_model_id).map_err(|err| { + AgentRegistryBuildError::BuildFailed { + agent: config.name.clone(), + message: err.to_string(), + } + })? + }; + if let Some(endpoint) = resolved.base_url.as_deref() { + model = model.with_endpoint(endpoint); + } + AgentBuilder::, String>::new(model) + } + _ => { + let mut model_config = ModelConfig::new(&resolved.runtime_spec); + if let Some(api_key) = resolved.api_key { + model_config = model_config.with_api_key(api_key); + } + if let Some(base_url) = resolved.base_url { + model_config = model_config.with_base_url(base_url); + } + AgentBuilder::, String>::from_config(model_config).map_err(|err| { + AgentRegistryBuildError::BuildFailed { + agent: config.name.clone(), + message: err.to_string(), + } + })? + } + }; + + Ok(SharedBuildSetup { + builder, + prompt_builder, + ruleset, + allowed_tools, + tool_names, + temperature, + top_p, + }) + } + + fn apply_settings_and_params( + &self, + mut builder: AgentBuilder, String>, + temperature: Option, + top_p: Option, + options: &AHashMap, + ) -> AgentBuilder, String> { + let mut settings = ModelSettings::new(); + if let Some(value) = temperature { + settings = settings.temperature(value); + } + if let Some(value) = top_p { + settings = settings.top_p(value); + } + + let mut params = Map::with_capacity(self.defaults.options.len() + options.len()); + for (key, value) in &self.defaults.options { + params.insert(key.clone(), value.clone()); + } + for (key, value) in options { + params.insert(key.clone(), value.clone()); + } + if !params.is_empty() { + settings = settings.extra(Value::Object(params)); + } + if !settings.is_empty() { + builder = builder.model_settings(settings); + } + builder + } + + fn register_allowed_tools( + &self, + mut builder: AgentBuilder, String>, + prompt_builder: &mut SystemPromptBuilder, + allowed_tools: Vec, + ) -> AgentBuilder, String> { + for tool in allowed_tools { + builder = tool.register_with_prompt(builder, prompt_builder); + } + builder + } + + /// Builds a serdesAI registry from the provided agent catalog. + /// + /// Parameters: + /// - `catalog`: config-only agent catalog. + /// + /// Returns: a populated [`AgentRegistry`] or [`AgentRegistryBuildError`]. + pub fn build( + &self, + catalog: &AgentCatalog, + ) -> Result, String>>, AgentRegistryBuildError> { + let resolver = self.create_resolver_from_defaults(); + + let mut entries = AHashMap::with_capacity(catalog.iter().count()); + + for config in catalog.iter() { + let SharedBuildSetup { + builder, + mut prompt_builder, + ruleset, + allowed_tools, + tool_names, + temperature, + top_p, + } = self.resolve_model_and_builder(config, resolver.as_ref())?; + + let mut builder = + self.register_allowed_tools(builder, &mut prompt_builder, allowed_tools); + builder = self.apply_settings_and_params(builder, temperature, top_p, &config.options); + + let system_prompt = prompt_builder.build(); + let agent = builder.system_prompt(system_prompt.clone()).build(); + + entries.insert( + config.name.clone(), + AgentRegistryEntry { + config: config.clone(), // AgentConfig is small and cheap to clone for error cases + ruleset, + tool_names, + system_prompt, + agent, + }, + ); + } + + Ok(AgentRegistry { entries }) + } + + fn contains_task_tool(names: &[String]) -> bool { + names + .iter() + .any(|name| name.eq_ignore_ascii_case(tool_names::TASK)) + } + + /// Builds a registry with recursive Task tool wiring enabled. + /// + /// This two-phase construction allows agents to delegate to each other + /// via Task tool, with permissions evaluated at each hop based on the + /// caller agent's own permission rules. + /// + /// Parameters: + /// - `catalog`: agent configurations defining available agents. + /// - `task_deps`: dependencies passed to Task tool executions. + /// + /// Returns: a populated [`AgentRegistry`] wrapped in [`Arc`]. + #[allow(clippy::type_complexity)] + pub fn build_with_recursive_task( + &self, + catalog: &AgentCatalog, + task_deps: Arc, + ) -> Result, String>>>, AgentRegistryBuildError> { + let resolver = self.create_resolver_from_defaults(); + + let registry_handle: Arc, String>>> = + Arc::new(TaskRegistryHandle::new()); + + let mut planned_targets = Vec::with_capacity(catalog.iter().count()); + let mut shared_setups = Vec::with_capacity(catalog.iter().count()); + for config in catalog.iter() { + let setup = self.resolve_model_and_builder(config, resolver.as_ref())?; + let mut tool_names = Vec::with_capacity(setup.tool_names.len() + 1); + tool_names.extend(setup.tool_names.iter().cloned()); + if task_has_any_allow(&config.permission) + && !Self::contains_task_tool(&setup.tool_names) + { + tool_names.push(tool_names::TASK.to_string()); + } + planned_targets.push(TaskTargetSummary { + name: config.name.clone(), + mode: config.mode, + tool_names, + }); + shared_setups.push((config.clone(), setup)); + } + + let snapshot = TaskDefinitionSnapshot { + targets: planned_targets, + }; + + let mut entries = AHashMap::with_capacity(shared_setups.len()); + for (config, setup) in shared_setups { + let SharedBuildSetup { + builder, + mut prompt_builder, + ruleset, + allowed_tools, + mut tool_names, + temperature, + top_p, + } = setup; + + let mut builder = + self.register_allowed_tools(builder, &mut prompt_builder, allowed_tools); + + if task_has_any_allow(&config.permission) && !Self::contains_task_tool(&tool_names) { + let task_tool = TaskTool::for_registry_caller( + Arc::clone(®istry_handle), + config.name.clone(), + ruleset.clone(), + snapshot.clone(), + Arc::clone(&task_deps), + ); + builder = builder.tool(prompt_builder.track(task_tool)); + tool_names.push(tool_names::TASK.to_string()); + } + + builder = self.apply_settings_and_params(builder, temperature, top_p, &config.options); + + let system_prompt = prompt_builder.build(); + let agent = builder.system_prompt(system_prompt.clone()).build(); + + entries.insert( + config.name.clone(), + AgentRegistryEntry { + config: config.clone(), + ruleset, + tool_names, + system_prompt, + agent, + }, + ); + } + + let registry = Arc::new(AgentRegistry { entries }); + registry_handle.set(Arc::clone(®istry)).map_err(|_| { + AgentRegistryBuildError::BuildFailed { + agent: "*".to_string(), + message: "recursive task registry handle already initialized".to_string(), + } + })?; + + Ok(registry) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use indexmap::IndexMap; + use std::sync::Arc; + + #[test] + fn agent_defaults_with_all_fields() { + use crate::model_resolver::{ModelsDevResolver, ProviderOverrides}; + + let mut options = AHashMap::new(); + options.insert("key1".to_string(), Value::Bool(true)); + + let defaults = AgentDefaults { + model: "test-model".to_string(), + model_resolver: Some(Arc::new(ModelsDevResolver::new( + None, + ProviderOverrides::new(), + ))), + provider_overrides: ProviderOverrides::new(), + api_key: Some("test-key".to_string()), + base_url: Some("https://example.com".to_string()), + temperature: Some(0.7), + top_p: Some(0.9), + options, + }; + + assert_eq!(defaults.model, "test-model"); + assert_eq!(defaults.api_key.as_deref(), Some("test-key")); + assert_eq!(defaults.base_url.as_deref(), Some("https://example.com")); + assert_eq!(defaults.temperature, Some(0.7)); + assert_eq!(defaults.top_p, Some(0.9)); + assert_eq!(defaults.options.len(), 1); + assert!(defaults.model_resolver.is_some()); + } + + #[test] + fn agent_registry_builder_is_send() { + fn assert_send() {} + assert_send::>(); + } + + #[test] + fn agent_registry_entry_is_invocable() { + let config = AgentConfig { + name: "test".to_string(), + mode: AgentMode::Subagent, + description: String::new(), + model: None, + hidden: false, + temperature: None, + top_p: None, + permission: IndexMap::new(), + options: AHashMap::new(), + prompt: String::new(), + }; + + let entry = AgentRegistryEntry { + config, + ruleset: Ruleset::new(), + tool_names: vec!["Read".to_string()], + system_prompt: String::new(), + agent: Arc::new(()), + }; + + assert!(entry.is_invocable()); + } + + #[test] + fn agent_registry_entry_not_invocable_for_primary() { + let config = AgentConfig { + name: "test".to_string(), + mode: AgentMode::Primary, + description: String::new(), + model: None, + hidden: false, + temperature: None, + top_p: None, + permission: IndexMap::new(), + options: AHashMap::new(), + prompt: String::new(), + }; + + let entry = AgentRegistryEntry { + config, + ruleset: Ruleset::new(), + tool_names: vec!["Read".to_string()], + system_prompt: String::new(), + agent: Arc::new(()), + }; + + assert!(!entry.is_invocable()); + } + + #[test] + fn agent_registry_entry_is_invocable_for_all_mode() { + let config = AgentConfig { + name: "test".to_string(), + mode: AgentMode::All, + description: String::new(), + model: None, + hidden: false, + temperature: None, + top_p: None, + permission: IndexMap::new(), + options: AHashMap::new(), + prompt: String::new(), + }; + let entry = AgentRegistryEntry { + config, + ruleset: Ruleset::new(), + tool_names: vec![], + system_prompt: String::new(), + agent: Arc::new(()), + }; + assert!(entry.is_invocable()); + } + + #[test] + fn agent_registry_from_entries_and_get() { + let config1 = AgentConfig { + name: "agent1".to_string(), + mode: AgentMode::Subagent, + description: String::new(), + model: None, + hidden: false, + temperature: None, + top_p: None, + permission: IndexMap::new(), + options: AHashMap::new(), + prompt: String::new(), + }; + + let entry1 = AgentRegistryEntry { + config: config1, + ruleset: Ruleset::new(), + tool_names: vec!["Read".to_string()], + system_prompt: String::new(), + agent: Arc::new(()), + }; + + let registry = AgentRegistry::from_entries([("agent1".to_string(), entry1)]); + let retrieved = registry.get("agent1"); + assert!(retrieved.is_some()); + assert_eq!(retrieved.unwrap().tool_names, vec!["Read".to_string()]); + } + + #[tokio::test] + async fn registry_agent_error_display() { + let error = RegistryAgentError::new("test error message"); + assert_eq!(format!("{}", error), "test error message"); + } + + #[test] + fn agent_registry_build_error_missing_model() { + let config = AgentConfig { + name: "no-model".to_string(), + mode: AgentMode::Subagent, + description: String::new(), + model: None, + hidden: false, + temperature: None, + top_p: None, + permission: IndexMap::new(), + options: AHashMap::new(), + prompt: String::new(), + }; + + let defaults = AgentDefaults { + model: "".to_string(), // Empty model + model_resolver: None, + provider_overrides: crate::model_resolver::ProviderOverrides::new(), + api_key: None, + base_url: None, + temperature: None, + top_p: None, + options: AHashMap::new(), + }; + + let catalog = AgentCatalog::from_entries(vec![config]); + let builder = AgentRegistryBuilder::<()>::new(defaults, vec![]); + + let result = builder.build(&catalog); + assert!(matches!( + result, + Err(AgentRegistryBuildError::MissingModel { agent }) + if agent == "no-model" + )); + } +} diff --git a/src/llm-coding-tools-serdesai/src/task/mod.rs b/src/llm-coding-tools-serdesai/src/task/mod.rs new file mode 100644 index 00000000..0b8b464e --- /dev/null +++ b/src/llm-coding-tools-serdesai/src/task/mod.rs @@ -0,0 +1,386 @@ +//! Task tool for invoking subagents (serdesAI adapter). +//! +//! Thin tool that validates access and dispatches to prebuilt agents in a registry. + +use crate::convert::to_serdes_result; +use crate::registry::{AgentRegistry, RegistryAgent}; +use async_trait::async_trait; +use llm_coding_tools_agents::AgentMode; +use llm_coding_tools_core::context::ToolContext; +use llm_coding_tools_core::permissions::{PermissionAction, Ruleset}; +use llm_coding_tools_core::tool_names; +use llm_coding_tools_core::tools::TaskInput; +use serde::Deserialize; +use serdes_ai::tools::{RunContext, SchemaBuilder, Tool, ToolDefinition, ToolError, ToolResult}; +use std::borrow::Cow; +use std::sync::{Arc, OnceLock}; + +/// Arguments for the Task tool (internal deserialization). +#[derive(Debug, Clone, Deserialize)] +struct TaskArgs { + description: String, + prompt: String, + subagent_type: String, + #[serde(default)] + session_id: Option, + #[serde(default)] + command: Option, +} + +impl From for TaskInput { + fn from(args: TaskArgs) -> Self { + Self { + description: args.description, + prompt: args.prompt, + subagent_type: args.subagent_type, + session_id: args.session_id, + command: args.command, + } + } +} + +/// Summary of a Task target agent for definition-time snapshot capture. +#[derive(Debug, Clone)] +pub struct TaskTargetSummary { + /// Agent name. + pub name: String, + /// Agent mode (controls invocability). + pub mode: AgentMode, + /// Tool names available to the agent. + pub tool_names: Vec, +} + +impl TaskTargetSummary { + #[inline] + fn is_invocable(&self) -> bool { + matches!(self.mode, AgentMode::Subagent | AgentMode::All) + } +} + +/// Snapshot of Task definition metadata captured at build time. +/// +/// This avoids runtime registry dependency during tool definition generation. +#[derive(Debug, Clone, Default)] +pub struct TaskDefinitionSnapshot { + /// Available Task targets at build time. + pub targets: Vec, +} + +/// Handle for lazy registry initialization in recursive Task wiring. +/// +/// Enables two-phase construction where Task tools are created before +/// the registry is fully assembled, then wired together after. +pub struct TaskRegistryHandle { + registry: OnceLock>>, +} + +impl TaskRegistryHandle { + /// Creates a new uninitialized handle. + pub fn new() -> Self { + Self { + registry: OnceLock::new(), + } + } + + /// Creates a handle pre-initialized with a registry. + pub fn from_registry(registry: Arc>) -> Self { + let handle = Self::new(); + let _ = handle.registry.set(registry); + handle + } + + /// Sets the registry. Returns Err if already set. + pub fn set(&self, registry: Arc>) -> Result<(), Arc>> { + self.registry.set(registry) + } + + /// Returns the registry if initialized. + pub fn get(&self) -> Option<&Arc>> { + self.registry.get() + } +} + +impl Default for TaskRegistryHandle { + fn default() -> Self { + Self::new() + } +} + +/// Authority source for Task permission evaluation. +enum TaskCallerAuthority { + /// Static ruleset from legacy construction. + StaticRules(Ruleset), + /// Registry-caller with name for runtime lookup and build-time rules fallback. + RegistryCaller { + caller_name: String, + build_rules: Ruleset, + }, +} + +/// Task tool for serdesAI framework. +/// +/// Validates access, builds the request message, and dispatches to stored agents. +pub struct TaskTool +where + A: RegistryAgent, +{ + registry: Arc>, + authority: TaskCallerAuthority, + definition_snapshot: TaskDefinitionSnapshot, + deps: Arc, +} + +impl TaskTool +where + A: RegistryAgent, +{ + /// Creates a new Task tool with the given registry, caller permissions, and deps. + /// + /// Parameters: + /// - `registry`: serdesAI agent registry. + /// - `caller_rules`: permission rules for the calling agent. + /// - `deps`: dependencies passed to registry agents. + /// + /// Returns: a new [`TaskTool`]. + pub fn new(registry: Arc>, caller_rules: Ruleset, deps: Arc) -> Self { + let definition_snapshot = TaskDefinitionSnapshot { + targets: registry + .iter() + .map(|(name, entry)| TaskTargetSummary { + name: name.clone(), + mode: entry.config.mode, + tool_names: entry.tool_names.clone(), + }) + .collect(), + }; + Self { + registry: Arc::new(TaskRegistryHandle::from_registry(registry)), + authority: TaskCallerAuthority::StaticRules(caller_rules), + definition_snapshot, + deps, + } + } + + /// Creates a Task tool for a registry-caller with snapshot-based definition. + /// + /// Parameters: + /// - `registry`: registry handle for runtime lookup. + /// - `caller_name`: name of the calling agent for runtime rules lookup. + /// - `caller_rules`: build-time ruleset for definition generation. + /// - `definition_snapshot`: precomputed target metadata. + /// - `deps`: dependencies passed to registry agents. + /// + /// Returns: a new [`TaskTool`] configured for recursive delegation. + pub fn for_registry_caller( + registry: Arc>, + caller_name: impl Into, + caller_rules: Ruleset, + definition_snapshot: TaskDefinitionSnapshot, + deps: Arc, + ) -> Self { + Self { + registry, + authority: TaskCallerAuthority::RegistryCaller { + caller_name: caller_name.into(), + build_rules: caller_rules, + }, + definition_snapshot, + deps, + } + } + + fn definition_rules(&self) -> &Ruleset { + match &self.authority { + TaskCallerAuthority::StaticRules(ruleset) => ruleset, + TaskCallerAuthority::RegistryCaller { build_rules, .. } => build_rules, + } + } + + fn resolve_registry(&self) -> Result<&AgentRegistry, ToolError> { + self.registry + .get() + .map(|registry| registry.as_ref()) + .ok_or_else(|| { + ToolError::execution_failed("Task registry is not initialized".to_string()) + }) + } + + fn resolve_runtime_rules<'a>(&'a self, registry: &'a AgentRegistry) -> Cow<'a, Ruleset> { + match &self.authority { + TaskCallerAuthority::StaticRules(ruleset) => Cow::Borrowed(ruleset), + TaskCallerAuthority::RegistryCaller { caller_name, .. } => registry + .get(caller_name) + .map(|entry| Cow::Borrowed(&entry.ruleset)) + .unwrap_or_else(|| Cow::Owned(Ruleset::new())), + } + } +} + +#[async_trait] +impl Tool for TaskTool +where + A: RegistryAgent + 'static, + Deps: Send + Sync + 'static, + RuntimeDeps: Send + Sync, +{ + fn definition(&self) -> ToolDefinition { + // Build the Task tool description from invocable + permitted agents + let mut names: Vec<_> = self + .definition_snapshot + .targets + .iter() + .map(|target| target.name.as_str()) + .collect(); + names.sort_unstable(); + + let mut lines = Vec::with_capacity(names.len()); + for name in names { + let target = match self + .definition_snapshot + .targets + .iter() + .find(|target| target.name == name) + { + Some(target) => target, + None => continue, + }; + if !target.is_invocable() { + continue; + } + if self.definition_rules().evaluate(tool_names::TASK, name) != PermissionAction::Allow { + continue; + } + lines.push(format!("- {}: {}", name, target.tool_names.join(", "))); + } + + let description = if lines.is_empty() { + "Task tool is not available - no accessible agents.".to_string() + } else { + const TEMPLATE: &str = r#"Launch a new agent to handle complex, multistep tasks autonomously. + +Available agent types and the tools they have access to: +{agents} + +When using the Task tool, you must specify a subagent_type parameter to select which agent type to use."#; + TEMPLATE.replace("{agents}", &lines.join("\n")) + }; + + ToolDefinition::new(tool_names::TASK, description).with_parameters( + SchemaBuilder::new() + .string_constrained( + "description", + "A short (3-5 words) description of the task", + true, + Some(1), + Some(100), + None, + ) + .string_constrained( + "prompt", + "The task for the agent to perform", + true, + Some(1), + None, + None, + ) + .string_constrained( + "subagent_type", + "The type of specialized agent to use for this task", + true, + Some(1), + None, + None, + ) + .string("session_id", "Existing Task session to continue", false) + .string("command", "The command that triggered this task", false) + .build() + .expect("schema serialization should never fail"), + ) + } + + async fn call(&self, _ctx: &RunContext, args: serde_json::Value) -> ToolResult { + let args: TaskArgs = serde_json::from_value(args) + .map_err(|e| ToolError::validation_error(tool_names::TASK, None, e.to_string()))?; + + let input: TaskInput = args.into(); + let registry = self.resolve_registry()?; + let entry = match registry.get(&input.subagent_type) { + Some(entry) => entry, + None => { + return Err(ToolError::validation_error( + tool_names::TASK, + Some("subagent_type".to_string()), + format!("Unknown agent type: {}", input.subagent_type), + )); + } + }; + + if !entry.is_invocable() { + return Err(ToolError::validation_error( + tool_names::TASK, + Some("subagent_type".to_string()), + format!( + "Agent '{}' is not available for task invocation", + input.subagent_type + ), + )); + } + + let caller_rules = self.resolve_runtime_rules(registry); + if caller_rules.evaluate(tool_names::TASK, &input.subagent_type) != PermissionAction::Allow + { + return Err(ToolError::validation_error( + tool_names::TASK, + Some("subagent_type".to_string()), + format!( + "Access denied: cannot invoke agent '{}'", + input.subagent_type + ), + )); + } + + // Build the required task context user message + let mut message = String::with_capacity(input.prompt.len() + 128); + message.push_str("\n"); + message.push_str("description: "); + message.push_str(&input.description); + message.push('\n'); + message.push_str("command: "); + if let Some(command) = &input.command { + message.push_str(command); + } + message.push('\n'); + message.push_str("session_id: "); + if let Some(session_id) = &input.session_id { + message.push_str(session_id); + } + message.push('\n'); + message.push_str("\n\n\n"); + message.push_str(&input.prompt); + message.push_str(""); + + let result = entry + .agent + .prompt(message, Arc::clone(&self.deps)) + .await + .map_err(|err| { + ToolError::execution_failed(format!("Task execution failed: {}", err)) + })?; + + to_serdes_result( + tool_names::TASK, + Ok(llm_coding_tools_core::ToolOutput::new(result)), + ) + } +} + +impl, Deps> ToolContext for TaskTool { + const NAME: &'static str = tool_names::TASK; + + fn context(&self) -> &'static str { + "Use the Task tool to delegate complex, multi-step tasks to specialized subagents." + } +} + +#[cfg(test)] +mod tests; diff --git a/src/llm-coding-tools-serdesai/src/task/tests.rs b/src/llm-coding-tools-serdesai/src/task/tests.rs new file mode 100644 index 00000000..722e7e49 --- /dev/null +++ b/src/llm-coding-tools-serdesai/src/task/tests.rs @@ -0,0 +1,403 @@ +use super::*; +use async_trait::async_trait; +use llm_coding_tools_agents::{AgentConfig, AgentMode}; +use llm_coding_tools_core::permissions::{PermissionAction, Rule, Ruleset}; +use serdes_ai::tools::RunContext; +use ahash::AHashMap; +use std::sync::{Arc, Mutex}; + +use crate::registry::{AgentRegistry, AgentRegistryEntry, RegistryAgent, RegistryAgentError}; + +/// Asserts that a ToolError is a ValidationFailed error with the expected tool name and message fragment. +fn assert_validation_message(err: serdes_ai::tools::ToolError, expected_fragment: &str) { + match err { + serdes_ai::tools::ToolError::ValidationFailed { tool_name, errors } => { + assert_eq!(tool_name, tool_names::TASK); + assert!(!errors.is_empty()); + assert!( + errors[0].message.contains(expected_fragment), + "Expected error message to contain '{}', but got: '{}'", + expected_fragment, + errors[0].message + ); + } + other => panic!("Expected ValidationFailed error, got: {other:?}"), + } +} + +struct MockAgent { + last_prompt: Arc>>, +} + +#[async_trait] +impl RegistryAgent<()> for MockAgent { + async fn prompt(&self, message: String, _deps: Arc<()>) -> Result { + *self.last_prompt.lock().unwrap() = Some(message); + Ok("mock response".to_string()) + } +} + +fn make_entry(name: &str, mode: AgentMode, hidden: bool) -> AgentRegistryEntry { + AgentRegistryEntry { + config: AgentConfig { + name: name.to_string(), + mode, + description: String::new(), + model: None, + hidden, + temperature: None, + top_p: None, + permission: indexmap::IndexMap::new(), + options: AHashMap::new(), + prompt: String::new(), + }, + ruleset: Ruleset::new(), + tool_names: vec!["Read".to_string(), "Bash".to_string()], + system_prompt: String::new(), + agent: MockAgent { + last_prompt: Arc::new(Mutex::new(None)), + }, + } +} + +#[tokio::test] +async fn task_tool_hidden_flag_is_noop_for_description() { + let registry = AgentRegistry::from_entries([ + ( + "visible".to_string(), + make_entry("visible", AgentMode::Subagent, false), + ), + ( + "hidden".to_string(), + make_entry("hidden", AgentMode::Subagent, true), + ), + ]); + let mut rules = Ruleset::new(); + rules.push(Rule::new("task", "*", PermissionAction::Allow)); + let tool = TaskTool::new(Arc::new(registry), rules, Arc::new(())); + + // Need type annotation: RuntimeDeps = () + let defn = as serdes_ai::tools::Tool<()>>::definition(&tool); + let description = defn.description(); + assert!(description.contains("visible")); + assert!(description.contains("hidden")); +} + +#[tokio::test] +async fn task_tool_denies_unpermitted_agent() { + let registry = AgentRegistry::from_entries([( + "agent-a".to_string(), + make_entry("agent-a", AgentMode::Subagent, false), + )]); + let rules = Ruleset::new(); + let tool = TaskTool::new(Arc::new(registry), rules, Arc::new(())); + + let args = serde_json::json!({ + "description": "Test", + "prompt": "Do something", + "subagent_type": "agent-a" + }); + + let result = tool.call(&RunContext::minimal("test-model"), args).await; + assert!(result.is_err()); + match result.unwrap_err() { + serdes_ai::tools::ToolError::ValidationFailed { tool_name, errors } => { + assert_eq!(tool_name, tool_names::TASK); + assert!(errors[0].message.contains("Access denied")); + } + _ => panic!("Expected ValidationFailed error"), + } +} + +#[tokio::test] +async fn task_tool_rejects_non_invocable_agent() { + let registry = AgentRegistry::from_entries([( + "primary-only".to_string(), + make_entry("primary-only", AgentMode::Primary, false), + )]); + let mut rules = Ruleset::new(); + rules.push(Rule::new("task", "*", PermissionAction::Allow)); + let tool = TaskTool::new(Arc::new(registry), rules, Arc::new(())); + + let args = serde_json::json!({ + "description": "Test", + "prompt": "Do something", + "subagent_type": "primary-only" + }); + + let result = tool.call(&RunContext::minimal("test-model"), args).await; + assert!(result.is_err()); + match result.unwrap_err() { + serdes_ai::tools::ToolError::ValidationFailed { errors, .. } => { + assert!(errors[0].message.contains("not available")); + } + _ => panic!("Expected ValidationFailed error"), + } +} + +#[tokio::test] +async fn task_tool_validates_and_builds_task_message() { + let entry = make_entry("agent-a", AgentMode::Subagent, false); + let last_prompt = entry.agent.last_prompt.clone(); + let registry = AgentRegistry::from_entries([("agent-a".to_string(), entry)]); + let mut rules = Ruleset::new(); + rules.push(Rule::new("task", "*", PermissionAction::Allow)); + let tool = TaskTool::new(Arc::new(registry), rules, Arc::new(())); + + let args = serde_json::json!({ + "description": "Test task", + "prompt": "Do something", + "subagent_type": "agent-a", + "session_id": "sess-1", + "command": "/cmd" + }); + + let _ = tool + .call(&RunContext::minimal("test-model"), args) + .await + .unwrap(); + let message = last_prompt.lock().unwrap().clone().unwrap(); + assert!(message.contains("")); + assert!(message.contains("description: Test task")); + assert!(message.contains("command: /cmd")); + assert!(message.contains("session_id: sess-1")); + assert!(message.contains("")); + assert!(message.contains("Do something")); +} + +#[tokio::test] +async fn task_tool_description_filters_by_permissions() { + let registry = AgentRegistry::from_entries([ + ( + "allowed".to_string(), + make_entry("allowed", AgentMode::Subagent, false), + ), + ( + "denied".to_string(), + make_entry("denied", AgentMode::Subagent, false), + ), + ]); + let mut rules = Ruleset::new(); + rules.push(Rule::new("task", "allowed", PermissionAction::Allow)); + let tool = TaskTool::new(Arc::new(registry), rules, Arc::new(())); + + // Need type annotation: RuntimeDeps = () + let defn = as serdes_ai::tools::Tool<()>>::definition(&tool); + let description = defn.description(); + assert!(description.contains("allowed")); + assert!(!description.contains("denied")); +} + +#[tokio::test] +async fn task_tool_hidden_agent_remains_invocable() { + let entry = make_entry("hidden", AgentMode::Subagent, true); + let last_prompt = entry.agent.last_prompt.clone(); + let registry = AgentRegistry::from_entries([("hidden".to_string(), entry)]); + let mut rules = Ruleset::new(); + rules.push(Rule::new("task", "hidden", PermissionAction::Allow)); + let tool = TaskTool::new(Arc::new(registry), rules, Arc::new(())); + + let args = serde_json::json!({ + "description": "Hidden run", + "prompt": "Do hidden", + "subagent_type": "hidden" + }); + + let _ = tool + .call(&RunContext::minimal("test-model"), args) + .await + .unwrap(); + assert!(last_prompt.lock().unwrap().is_some()); +} + +#[tokio::test] +async fn task_tool_returns_unknown_for_nonexistent_agent() { + let registry = AgentRegistry::from_entries([( + "agent-a".to_string(), + make_entry("agent-a", AgentMode::Subagent, false), + )]); + let mut rules = Ruleset::new(); + rules.push(Rule::new("task", "*", PermissionAction::Allow)); + let tool = TaskTool::new(Arc::new(registry), rules, Arc::new(())); + + let args = serde_json::json!({ + "description": "Test", + "prompt": "Do something", + "subagent_type": "nonexistent" + }); + + let result = tool.call(&RunContext::minimal("test-model"), args).await; + assert!(result.is_err()); + match result.unwrap_err() { + serdes_ai::tools::ToolError::ValidationFailed { tool_name, errors } => { + assert_eq!(tool_name, tool_names::TASK); + assert!(errors[0].message.contains("Unknown agent type")); + } + _ => panic!("Expected ValidationFailed error"), + } +} + +#[test] +fn task_tool_schema_has_required_fields() { + let registry = AgentRegistry::from_entries([( + "agent".to_string(), + make_entry("agent", AgentMode::Subagent, false), + )]); + let rules = Ruleset::new(); + let tool = TaskTool::new(Arc::new(registry), rules, Arc::new(())); + + let binding = as serdes_ai::tools::Tool<()>>::definition(&tool); + + assert_eq!(binding.name(), tool_names::TASK); + + let params = binding.parameters(); + assert_eq!(params["type"], "object"); + + let required = params["required"].as_array().unwrap(); + assert!(required.contains(&serde_json::json!("description"))); + assert!(required.contains(&serde_json::json!("prompt"))); + assert!(required.contains(&serde_json::json!("subagent_type"))); +} + +#[tokio::test] +async fn task_tool_description_filters_by_mode_and_permission() { + let registry = AgentRegistry::from_entries([ + ( + "subagent-allowed".to_string(), + make_entry("subagent-allowed", AgentMode::Subagent, false), + ), + ( + "all-allowed".to_string(), + make_entry("all-allowed", AgentMode::All, false), + ), + ( + "primary-allowed".to_string(), + make_entry("primary-allowed", AgentMode::Primary, false), + ), + ( + "subagent-denied".to_string(), + make_entry("subagent-denied", AgentMode::Subagent, false), + ), + ]); + let mut rules = Ruleset::new(); + // Allow wildcard first, then deny specific - tests that mode filtering takes precedence over permission + rules.push(Rule::new("task", "*", PermissionAction::Allow)); + rules.push(Rule::new("task", "subagent-denied", PermissionAction::Deny)); + let tool = TaskTool::new(Arc::new(registry), rules, Arc::new(())); + + let defn = as serdes_ai::tools::Tool<()>>::definition(&tool); + let description = defn.description(); + + // Mode-invocable + permission-allowed targets appear + assert!(description.contains("subagent-allowed")); + assert!(description.contains("all-allowed")); + // Primary agents are excluded even if permission-allowed (mode filtering takes precedence) + assert!(!description.contains("primary-allowed")); + // Permission-denied agents are excluded even if mode-invocable + assert!(!description.contains("subagent-denied")); +} + +#[tokio::test] +async fn task_tool_invocation_respects_wildcard_last_match_wins() { + // Create entries with captured prompts for verification + let approved = make_entry("ops-approved", AgentMode::Subagent, false); + let approved_prompt = approved.agent.last_prompt.clone(); + let blocked = make_entry("ops-blocked", AgentMode::Subagent, false); + // Wildcard-only target with no exact rule match - proves wildcard matching is exercised + let wildcard_only = make_entry("ops-generic", AgentMode::Subagent, false); + let wildcard_prompt = wildcard_only.agent.last_prompt.clone(); + + let registry = AgentRegistry::from_entries([ + ("ops-approved".to_string(), approved), + ("ops-blocked".to_string(), blocked), + ("ops-generic".to_string(), wildcard_only), + ]); + + let mut rules = Ruleset::new(); + // Default deny all via wildcard (must come first to allow later overrides) + rules.push(Rule::new("task", "*", PermissionAction::Deny)); + // Allow all ops-* via wildcard (overrides the default deny for ops agents) + rules.push(Rule::new("task", "ops-*", PermissionAction::Allow)); + // Deny specific target (last-match-wins over earlier wildcard allow) + rules.push(Rule::new("task", "ops-blocked", PermissionAction::Deny)); + // Re-allow specific target (last-match-wins over earlier specific deny) + rules.push(Rule::new("task", "ops-approved", PermissionAction::Allow)); + + let tool = TaskTool::new(Arc::new(registry), rules, Arc::new(())); + + // Test: ops-blocked should be denied (last match is explicit deny after wildcard allow) + let denied = serde_json::json!({"description":"d","prompt":"p","subagent_type":"ops-blocked"}); + let denied_result = tool.call(&RunContext::minimal("test-model"), denied).await; + assert_validation_message(denied_result.unwrap_err(), "Access denied"); + + // Test: ops-generic should be allowed (matches wildcard "ops-*", no later matching rule) + let wildcard = + serde_json::json!({"description":"d","prompt":"p","subagent_type":"ops-generic"}); + let wildcard_result = tool + .call(&RunContext::minimal("test-model"), wildcard) + .await; + assert!(wildcard_result.is_ok()); + assert!(wildcard_prompt.lock().unwrap().is_some()); + + // Test: ops-approved should be allowed (last match is explicit allow after specific deny) + let allowed = + serde_json::json!({"description":"d","prompt":"p","subagent_type":"ops-approved"}); + let allowed_result = tool.call(&RunContext::minimal("test-model"), allowed).await; + assert!(allowed_result.is_ok()); + assert!(approved_prompt.lock().unwrap().is_some()); +} + +#[tokio::test] +async fn task_tool_invocation_outcome_matrix() { + // Test matrix covering all four required outcomes: unknown, primary, denied, allowed + let allowed = make_entry("allowed-agent", AgentMode::Subagent, false); + let allowed_prompt = allowed.agent.last_prompt.clone(); + let registry = AgentRegistry::from_entries([ + ( + "primary-agent".to_string(), + make_entry("primary-agent", AgentMode::Primary, false), + ), + ( + "denied-agent".to_string(), + make_entry("denied-agent", AgentMode::Subagent, false), + ), + ("allowed-agent".to_string(), allowed), + ]); + + let mut rules = Ruleset::new(); + // Default deny via wildcard + rules.push(Rule::new("task", "*", PermissionAction::Deny)); + // Explicit allow for one specific agent + rules.push(Rule::new("task", "allowed-agent", PermissionAction::Allow)); + + let tool = TaskTool::new(Arc::new(registry), rules, Arc::new(())); + + // Outcome 1: Unknown target -> validation error (REQ-010) + let unknown = + serde_json::json!({"description":"d","prompt":"p","subagent_type":"missing-agent"}); + let unknown_result = tool.call(&RunContext::minimal("test-model"), unknown).await; + assert_validation_message(unknown_result.unwrap_err(), "Unknown agent type"); + + // Outcome 2: Primary target -> validation error for non-invocable (REQ-011) + let primary = + serde_json::json!({"description":"d","prompt":"p","subagent_type":"primary-agent"}); + let primary_result = tool.call(&RunContext::minimal("test-model"), primary).await; + assert_validation_message( + primary_result.unwrap_err(), + "not available for task invocation", + ); + + // Outcome 3: Denied target -> permission-failure outcome (REQ-012) + let denied = serde_json::json!({"description":"d","prompt":"p","subagent_type":"denied-agent"}); + let denied_result = tool.call(&RunContext::minimal("test-model"), denied).await; + assert_validation_message(denied_result.unwrap_err(), "Access denied"); + + // Outcome 4: Allowed target -> successful dispatch (REQ-013) + let permitted = + serde_json::json!({"description":"d","prompt":"p","subagent_type":"allowed-agent"}); + let permitted_result = tool + .call(&RunContext::minimal("test-model"), permitted) + .await; + assert!(permitted_result.is_ok()); + assert!(allowed_prompt.lock().unwrap().is_some()); +} diff --git a/src/llm-coding-tools-serdesai/src/tool_catalog.rs b/src/llm-coding-tools-serdesai/src/tool_catalog.rs new file mode 100644 index 00000000..b766e256 --- /dev/null +++ b/src/llm-coding-tools-serdesai/src/tool_catalog.rs @@ -0,0 +1,334 @@ +//! Cloneable tool catalog for serdesAI framework. +//! +//! Provides [`ToolCatalogEntry`] enum that wraps all tool types with +//! [`Clone`] support for registry-based agent construction. +//! +//! # Example +//! +//! ```no_run +//! use llm_coding_tools_serdesai::{default_tools, TodoState, ToolCatalogEntry}; +//! use llm_coding_tools_core::path::AllowedPathResolver; +//! +//! // Get default tools with line numbers and absolute paths +//! let tools = default_tools(true, None, TodoState::new()); +//! +//! // Get default tools with allowed path sandboxing +//! let resolver = AllowedPathResolver::new(["/allowed/path"]).unwrap(); +//! let tools = default_tools(true, Some(resolver), TodoState::new()); +//! ``` + +use crate::absolute; +use crate::agent_ext::AgentBuilderExt; +use crate::allowed; +use crate::{BashTool, TodoReadTool, TodoState, TodoWriteTool, WebFetchTool}; +use llm_coding_tools_core::path::AllowedPathResolver; +use llm_coding_tools_core::tool_names; +use llm_coding_tools_core::{SystemPromptBuilder, ToolContext}; +use serdes_ai::AgentBuilder; + +/// Cloneable catalog entry for serdesAI tool instances. +/// +/// Exposes tool metadata for registries and enables builder registration. +#[derive(Debug, Clone)] +pub enum ToolCatalogEntry { + /// Read tool with line numbers enabled (absolute path variant). + ReadLines(absolute::ReadTool), + /// Read tool without line numbers (absolute path variant). + ReadRaw(absolute::ReadTool), + /// Read tool with line numbers enabled (allowed path variant). + ReadAllowedLines(allowed::ReadTool), + /// Read tool without line numbers (allowed path variant). + ReadAllowedRaw(allowed::ReadTool), + /// Write tool (absolute path variant). + Write(absolute::WriteTool), + /// Write tool (allowed path variant). + WriteAllowed(allowed::WriteTool), + /// Edit tool (absolute path variant). + Edit(absolute::EditTool), + /// Edit tool (allowed path variant). + EditAllowed(allowed::EditTool), + /// Glob tool (absolute path variant). + Glob(absolute::GlobTool), + /// Glob tool (allowed path variant). + GlobAllowed(allowed::GlobTool), + /// Grep tool with line numbers enabled (absolute path variant). + GrepLines(absolute::GrepTool), + /// Grep tool without line numbers (absolute path variant). + GrepRaw(absolute::GrepTool), + /// Grep tool with line numbers enabled (allowed path variant). + GrepAllowedLines(allowed::GrepTool), + /// Grep tool without line numbers (allowed path variant). + GrepAllowedRaw(allowed::GrepTool), + /// Bash shell command execution tool. + Bash(BashTool), + /// Web content fetching tool. + WebFetch(WebFetchTool), + /// Todo list read tool. + TodoRead(TodoReadTool), + /// Todo list write tool. + TodoWrite(TodoWriteTool), +} + +impl ToolCatalogEntry { + /// Returns the canonical tool name. + /// + /// Returns: one of the [`tool_names`] constants (e.g., [`tool_names::READ`]). + #[inline] + pub fn name(&self) -> &'static str { + match self { + Self::ReadLines(_) + | Self::ReadRaw(_) + | Self::ReadAllowedLines(_) + | Self::ReadAllowedRaw(_) => tool_names::READ, + Self::Write(_) | Self::WriteAllowed(_) => tool_names::WRITE, + Self::Edit(_) | Self::EditAllowed(_) => tool_names::EDIT, + Self::Glob(_) | Self::GlobAllowed(_) => tool_names::GLOB, + Self::GrepLines(_) + | Self::GrepRaw(_) + | Self::GrepAllowedLines(_) + | Self::GrepAllowedRaw(_) => tool_names::GREP, + Self::Bash(_) => tool_names::BASH, + Self::WebFetch(_) => tool_names::WEBFETCH, + Self::TodoRead(_) => tool_names::TODO_READ, + Self::TodoWrite(_) => tool_names::TODO_WRITE, + } + } + + /// Returns the tool's system prompt context string. + /// + /// Returns: the context string for this tool. + #[inline] + pub fn context(&self) -> &'static str { + match self { + Self::ReadLines(tool) => tool.context(), + Self::ReadRaw(tool) => tool.context(), + Self::ReadAllowedLines(tool) => tool.context(), + Self::ReadAllowedRaw(tool) => tool.context(), + Self::Write(tool) => tool.context(), + Self::WriteAllowed(tool) => tool.context(), + Self::Edit(tool) => tool.context(), + Self::EditAllowed(tool) => tool.context(), + Self::Glob(tool) => tool.context(), + Self::GlobAllowed(tool) => tool.context(), + Self::GrepLines(tool) => tool.context(), + Self::GrepRaw(tool) => tool.context(), + Self::GrepAllowedLines(tool) => tool.context(), + Self::GrepAllowedRaw(tool) => tool.context(), + Self::Bash(tool) => tool.context(), + Self::WebFetch(tool) => tool.context(), + Self::TodoRead(tool) => tool.context(), + Self::TodoWrite(tool) => tool.context(), + } + } + + /// Registers this tool on a serdesAI agent builder. + /// + /// Parameters: + /// - `builder`: the serdesAI agent builder to register on. + /// + /// Returns: the builder after registering this tool. + pub fn register( + self, + builder: AgentBuilder, + ) -> AgentBuilder + where + Deps: Send + Sync + 'static, + Output: Send + Sync + 'static, + { + match self { + Self::ReadLines(tool) => builder.tool(tool), + Self::ReadRaw(tool) => builder.tool(tool), + Self::ReadAllowedLines(tool) => builder.tool(tool), + Self::ReadAllowedRaw(tool) => builder.tool(tool), + Self::Write(tool) => builder.tool(tool), + Self::WriteAllowed(tool) => builder.tool(tool), + Self::Edit(tool) => builder.tool(tool), + Self::EditAllowed(tool) => builder.tool(tool), + Self::Glob(tool) => builder.tool(tool), + Self::GlobAllowed(tool) => builder.tool(tool), + Self::GrepLines(tool) => builder.tool(tool), + Self::GrepRaw(tool) => builder.tool(tool), + Self::GrepAllowedLines(tool) => builder.tool(tool), + Self::GrepAllowedRaw(tool) => builder.tool(tool), + Self::Bash(tool) => builder.tool(tool), + Self::WebFetch(tool) => builder.tool(tool), + Self::TodoRead(tool) => builder.tool(tool), + Self::TodoWrite(tool) => builder.tool(tool), + } + } + + /// Registers this tool on a serdesAI agent builder while tracking prompt context. + /// + /// Parameters: + /// - `builder`: the serdesAI agent builder to register on. + /// - `pb`: system prompt builder used to track tool context. + /// + /// Returns: the builder after registering this tool. + pub fn register_with_prompt( + self, + builder: AgentBuilder, + pb: &mut SystemPromptBuilder, + ) -> AgentBuilder + where + Deps: Send + Sync + 'static, + Output: Send + Sync + 'static, + { + match self { + Self::ReadLines(tool) => builder.tool(pb.track(tool)), + Self::ReadRaw(tool) => builder.tool(pb.track(tool)), + Self::ReadAllowedLines(tool) => builder.tool(pb.track(tool)), + Self::ReadAllowedRaw(tool) => builder.tool(pb.track(tool)), + Self::Write(tool) => builder.tool(pb.track(tool)), + Self::WriteAllowed(tool) => builder.tool(pb.track(tool)), + Self::Edit(tool) => builder.tool(pb.track(tool)), + Self::EditAllowed(tool) => builder.tool(pb.track(tool)), + Self::Glob(tool) => builder.tool(pb.track(tool)), + Self::GlobAllowed(tool) => builder.tool(pb.track(tool)), + Self::GrepLines(tool) => builder.tool(pb.track(tool)), + Self::GrepRaw(tool) => builder.tool(pb.track(tool)), + Self::GrepAllowedLines(tool) => builder.tool(pb.track(tool)), + Self::GrepAllowedRaw(tool) => builder.tool(pb.track(tool)), + Self::Bash(tool) => builder.tool(pb.track(tool)), + Self::WebFetch(tool) => builder.tool(pb.track(tool)), + Self::TodoRead(tool) => builder.tool(pb.track(tool)), + Self::TodoWrite(tool) => builder.tool(pb.track(tool)), + } + } +} + +/// Builds the default tool catalog for serdesAI. +/// +/// Parameters: +/// - `line_numbers`: whether read/grep tools include line numbers in output. +/// - `resolver`: `None` for absolute tools, or `Some(resolver)` for allowed tools. +/// - `todo_state`: shared [`TodoState`] used by todo read/write tools. +/// +/// Returns: a list of non-Task tool catalog entries in canonical order. +pub fn default_tools( + line_numbers: bool, + resolver: Option, + todo_state: TodoState, +) -> Vec { + let mut tools = Vec::with_capacity(9); + + let allowed_resolvers = resolver.map(|resolver| { + let [ + read_resolver, + write_resolver, + edit_resolver, + glob_resolver, + grep_resolver, + ] = [(); 5].map(|_| resolver.clone()); + ( + read_resolver, + write_resolver, + edit_resolver, + glob_resolver, + grep_resolver, + ) + }); + + match allowed_resolvers { + None => { + let read = if line_numbers { + ToolCatalogEntry::ReadLines(absolute::ReadTool::::new()) + } else { + ToolCatalogEntry::ReadRaw(absolute::ReadTool::::new()) + }; + let grep = if line_numbers { + ToolCatalogEntry::GrepLines(absolute::GrepTool::::new()) + } else { + ToolCatalogEntry::GrepRaw(absolute::GrepTool::::new()) + }; + + tools.extend([ + read, + ToolCatalogEntry::Write(absolute::WriteTool::new()), + ToolCatalogEntry::Edit(absolute::EditTool::new()), + ToolCatalogEntry::Glob(absolute::GlobTool::new()), + grep, + ]); + } + Some((read_resolver, write_resolver, edit_resolver, glob_resolver, grep_resolver)) => { + let read = if line_numbers { + ToolCatalogEntry::ReadAllowedLines(allowed::ReadTool::::new(read_resolver)) + } else { + ToolCatalogEntry::ReadAllowedRaw(allowed::ReadTool::::new(read_resolver)) + }; + let grep = if line_numbers { + ToolCatalogEntry::GrepAllowedLines(allowed::GrepTool::::new(grep_resolver)) + } else { + ToolCatalogEntry::GrepAllowedRaw(allowed::GrepTool::::new(grep_resolver)) + }; + + tools.extend([ + read, + ToolCatalogEntry::WriteAllowed(allowed::WriteTool::new(write_resolver)), + ToolCatalogEntry::EditAllowed(allowed::EditTool::new(edit_resolver)), + ToolCatalogEntry::GlobAllowed(allowed::GlobTool::new(glob_resolver)), + grep, + ]); + } + } + + let todo_read = ToolCatalogEntry::TodoRead(TodoReadTool::new(todo_state.clone())); + let todo_write = ToolCatalogEntry::TodoWrite(TodoWriteTool::new(todo_state)); + tools.extend([ + ToolCatalogEntry::Bash(BashTool::new()), + ToolCatalogEntry::WebFetch(WebFetchTool::new()), + todo_read, + todo_write, + ]); + + tools +} + +#[cfg(test)] +mod tests { + use super::*; + use llm_coding_tools_core::tool_names; + use tempfile::TempDir; + + const EXPECTED_NAMES: [&str; 9] = [ + tool_names::READ, + tool_names::WRITE, + tool_names::EDIT, + tool_names::GLOB, + tool_names::GREP, + tool_names::BASH, + tool_names::WEBFETCH, + tool_names::TODO_READ, + tool_names::TODO_WRITE, + ]; + + fn assert_default_tools( + line_numbers: bool, + resolver: Option, + todo_state: TodoState, + ) { + let tools = default_tools(line_numbers, resolver, todo_state); + let mut names: Vec<_> = tools.iter().map(|tool| tool.name()).collect(); + let mut expected: Vec<_> = EXPECTED_NAMES.into(); + names.sort_unstable(); + expected.sort_unstable(); + assert_eq!(names, expected); + let mut dedup = names.clone(); + dedup.dedup(); + assert_eq!(dedup.len(), names.len()); + assert!(!names.contains(&tool_names::TASK)); + } + + #[test] + fn default_tools_absolute_has_unique_names() { + assert_default_tools(true, None, TodoState::new()); + assert_default_tools(false, None, TodoState::new()); + } + + #[test] + fn default_tools_allowed_has_unique_names() { + let dir = TempDir::new().unwrap(); + let resolver = AllowedPathResolver::new([dir.path()]).unwrap(); + assert_default_tools(true, Some(resolver.clone()), TodoState::new()); + assert_default_tools(false, Some(resolver), TodoState::new()); + } +} diff --git a/src/llm-coding-tools-serdesai/tests/recursive_task_delegation_integration.rs b/src/llm-coding-tools-serdesai/tests/recursive_task_delegation_integration.rs new file mode 100644 index 00000000..312001a3 --- /dev/null +++ b/src/llm-coding-tools-serdesai/tests/recursive_task_delegation_integration.rs @@ -0,0 +1,410 @@ +use async_trait::async_trait; +use indexmap::IndexMap; +use llm_coding_tools_agents::{AgentConfig, AgentMode}; +use llm_coding_tools_core::permissions::{PermissionAction, Rule, Ruleset}; +use llm_coding_tools_serdesai::{ + AgentRegistry, AgentRegistryEntry, RegistryAgent, RegistryAgentError, TaskDefinitionSnapshot, + TaskRegistryHandle, TaskTargetSummary, TaskTool, +}; +use serdes_ai::tools::{RunContext, Tool}; +use ahash::AHashMap; +use std::sync::{Arc, Mutex}; + +/// Creates a TaskDefinitionSnapshot from a registry. +fn snapshot_from_registry(registry: &AgentRegistry) -> TaskDefinitionSnapshot { + TaskDefinitionSnapshot { + targets: registry + .iter() + .map(|(name, entry)| TaskTargetSummary { + name: name.clone(), + mode: entry.config.mode, + tool_names: entry.tool_names.clone(), + }) + .collect(), + } +} + +/// Mock agent that records prompts for verification. +struct ScriptedAgent { + /// The response to return when prompted. + response: String, + /// Last prompt received (captured for verification). + last_prompt: Arc>>, +} + +impl ScriptedAgent { + fn new(response: impl Into) -> Self { + Self { + response: response.into(), + last_prompt: Arc::new(Mutex::new(None)), + } + } +} + +#[async_trait] +impl RegistryAgent<()> for ScriptedAgent { + async fn prompt(&self, message: String, _deps: Arc<()>) -> Result { + *self.last_prompt.lock().unwrap() = Some(message); + Ok(self.response.clone()) + } +} + +/// Creates a registry entry with the given name, mode, and permission rules. +fn make_entry(name: &str, mode: AgentMode, ruleset: Ruleset) -> AgentRegistryEntry { + AgentRegistryEntry { + config: AgentConfig { + name: name.to_string(), + mode, + description: String::new(), + model: None, + hidden: false, + temperature: None, + top_p: None, + permission: IndexMap::new(), + options: AHashMap::new(), + prompt: String::new(), + }, + ruleset, + tool_names: vec![], + system_prompt: String::new(), + agent: ScriptedAgent::new(format!("response-from-{}", name)), + } +} + +/// Creates a ruleset that allows specific targets. +fn rules_allow(targets: &[&str]) -> Ruleset { + let mut ruleset = Ruleset::new(); + // Default deny + ruleset.push(Rule::new("task", "*", PermissionAction::Deny)); + // Allow specific targets + for target in targets { + ruleset.push(Rule::new("task", *target, PermissionAction::Allow)); + } + ruleset +} + +/// Creates a ruleset that denies a specific target (with wildcard allow before it). +fn rules_deny(target: &str) -> Ruleset { + let mut ruleset = Ruleset::new(); + // Allow all via wildcard + ruleset.push(Rule::new("task", "*", PermissionAction::Allow)); + // Deny specific target + ruleset.push(Rule::new("task", target, PermissionAction::Deny)); + ruleset +} + +#[tokio::test] +async fn depth_2_allow_chain_succeeds() { + // Agent A -> Agent B (A allows B, B has no allow entries - default-deny) + let agent_a = make_entry("agent-a", AgentMode::Subagent, rules_allow(&["agent-b"])); + let agent_b = make_entry("agent-b", AgentMode::Subagent, rules_allow(&[])); + + let registry = AgentRegistry::from_entries([ + ("agent-a".to_string(), agent_a), + ("agent-b".to_string(), agent_b), + ]); + + let handle = Arc::new(TaskRegistryHandle::from_registry(Arc::new(registry))); + let snapshot = snapshot_from_registry(handle.get().unwrap()); + + // Create TaskTool for agent-a (can delegate to agent-b) + let task_tool = TaskTool::for_registry_caller( + Arc::clone(&handle), + "agent-a", + rules_allow(&["agent-b"]), + snapshot, + Arc::new(()), + ); + + let args = serde_json::json!({ + "description": "Delegate to B", + "prompt": "Do something", + "subagent_type": "agent-b" + }); + + let result = task_tool + .call(&RunContext::minimal("test-model"), args) + .await; + + assert!( + result.is_ok(), + "Expected successful delegation, got: {:?}", + result + ); +} + +#[tokio::test] +async fn depth_2_b_denies_but_a_to_b_succeeds() { + // Agent A -> Agent B (A allows B, but B denies everyone) + let agent_a = make_entry("agent-a", AgentMode::Subagent, rules_allow(&["agent-b"])); + let agent_b = make_entry("agent-b", AgentMode::Subagent, rules_deny("*")); + + let registry = AgentRegistry::from_entries([ + ("agent-a".to_string(), agent_a), + ("agent-b".to_string(), agent_b), + ]); + + let handle = Arc::new(TaskRegistryHandle::from_registry(Arc::new(registry))); + let snapshot = snapshot_from_registry(handle.get().unwrap()); + + // Create TaskTool for agent-a (can delegate to agent-b) + let task_tool = TaskTool::for_registry_caller( + Arc::clone(&handle), + "agent-a", + rules_allow(&["agent-b"]), + snapshot, + Arc::new(()), + ); + + let args = serde_json::json!({ + "description": "Delegate to B", + "prompt": "Do something", + "subagent_type": "agent-b" + }); + + let result = task_tool + .call(&RunContext::minimal("test-model"), args) + .await; + + // The call should succeed (agent-a can call agent-b), but if agent-b + // were to try calling someone else, it would fail. + assert!(result.is_ok(), "Expected successful call to allowed agent"); +} + +#[tokio::test] +async fn depth_2_fails_when_caller_denies_target() { + // Agent A denies B, so A cannot delegate to B + let agent_a = make_entry("agent-a", AgentMode::Subagent, rules_deny("agent-b")); + let agent_b = make_entry("agent-b", AgentMode::Subagent, Ruleset::new()); + + let registry = AgentRegistry::from_entries([ + ("agent-a".to_string(), agent_a), + ("agent-b".to_string(), agent_b), + ]); + + let handle = Arc::new(TaskRegistryHandle::from_registry(Arc::new(registry))); + let snapshot = snapshot_from_registry(handle.get().unwrap()); + + // Create TaskTool for agent-a (denies agent-b) + let task_tool = TaskTool::for_registry_caller( + Arc::clone(&handle), + "agent-a", + rules_deny("agent-b"), + snapshot, + Arc::new(()), + ); + + let args = serde_json::json!({ + "description": "Delegate to B", + "prompt": "Do something", + "subagent_type": "agent-b" + }); + + let result = task_tool + .call(&RunContext::minimal("test-model"), args) + .await; + + assert!( + result.is_err(), + "Expected access denied when caller denies target" + ); + let err = result.unwrap_err(); + let err_str = format!("{:?}", err); + assert!( + err_str.contains("Access denied") || err_str.contains("validation"), + "Expected access denied error, got: {}", + err_str + ); +} + +#[tokio::test] +async fn depth_3_chain_with_runtime_permission_lookup() { + // Agent A -> Agent B -> Agent C + // A allows B, B allows C + let agent_a = make_entry("agent-a", AgentMode::Subagent, rules_allow(&["agent-b"])); + let agent_b = make_entry("agent-b", AgentMode::Subagent, rules_allow(&["agent-c"])); + let agent_c = make_entry("agent-c", AgentMode::Subagent, rules_allow(&[])); + + let registry = AgentRegistry::from_entries([ + ("agent-a".to_string(), agent_a), + ("agent-b".to_string(), agent_b), + ("agent-c".to_string(), agent_c), + ]); + + let handle = Arc::new(TaskRegistryHandle::from_registry(Arc::new(registry))); + let snapshot = snapshot_from_registry(handle.get().unwrap()); + + // Test: agent-a can call agent-b + let task_a = TaskTool::for_registry_caller( + Arc::clone(&handle), + "agent-a", + rules_allow(&["agent-b"]), + snapshot.clone(), + Arc::new(()), + ); + + let result = task_a + .call( + &RunContext::minimal("test-model"), + serde_json::json!({ + "description": "Delegate to B", + "prompt": "Do something", + "subagent_type": "agent-b" + }), + ) + .await; + assert!(result.is_ok(), "agent-a should be able to call agent-b"); + + // Test: agent-b can call agent-c + let task_b = TaskTool::for_registry_caller( + Arc::clone(&handle), + "agent-b", + rules_allow(&["agent-c"]), + snapshot.clone(), + Arc::new(()), + ); + + let result = task_b + .call( + &RunContext::minimal("test-model"), + serde_json::json!({ + "description": "Delegate to C", + "prompt": "Do something else", + "subagent_type": "agent-c" + }), + ) + .await; + assert!(result.is_ok(), "agent-b should be able to call agent-c"); + + // Test: agent-a cannot call agent-c directly (not in its allow list) + let task_a_limited = TaskTool::for_registry_caller( + Arc::clone(&handle), + "agent-a", + rules_allow(&["agent-b"]), // Only allows agent-b, not agent-c + snapshot, + Arc::new(()), + ); + + let result = task_a_limited + .call( + &RunContext::minimal("test-model"), + serde_json::json!({ + "description": "Try to delegate to C", + "prompt": "Do something", + "subagent_type": "agent-c" + }), + ) + .await; + assert!( + result.is_err(), + "agent-a should not be able to call agent-c directly" + ); +} + +#[test] +fn task_registry_handle_set_once_behavior() { + let handle: TaskRegistryHandle = TaskRegistryHandle::new(); + + // First set should succeed + let registry = Arc::new(AgentRegistry::from_entries([])); + assert!(handle.set(Arc::clone(®istry)).is_ok()); + + // Second set should fail + let registry2 = Arc::new(AgentRegistry::from_entries([])); + assert!(handle.set(registry2).is_err()); +} + +#[test] +fn task_definition_snapshot_default_is_empty() { + let snapshot = TaskDefinitionSnapshot::default(); + assert!(snapshot.targets.is_empty()); +} + +#[tokio::test] +async fn task_tool_registry_caller_uses_per_agent_rules() { + // Verify that runtime rules lookup uses per-agent ruleset from registry entry + let mut agent_a_rules = Ruleset::new(); + agent_a_rules.push(Rule::new("task", "agent-b", PermissionAction::Allow)); + + let mut agent_b_rules = Ruleset::new(); + agent_b_rules.push(Rule::new("task", "agent-c", PermissionAction::Allow)); + + let agent_a = make_entry("agent-a", AgentMode::Subagent, agent_a_rules.clone()); + let agent_b = make_entry("agent-b", AgentMode::Subagent, agent_b_rules.clone()); + + let registry = AgentRegistry::from_entries([ + ("agent-a".to_string(), agent_a), + ("agent-b".to_string(), agent_b), + ]); + + let handle = Arc::new(TaskRegistryHandle::from_registry(Arc::new(registry))); + let snapshot = snapshot_from_registry(handle.get().unwrap()); + + // Use empty build_rules - runtime lookup should use per-agent rules from registry + let task_a = TaskTool::for_registry_caller( + Arc::clone(&handle), + "agent-a", + Ruleset::new(), // Empty at build time + snapshot, + Arc::new(()), + ); + + // This should work because runtime lookup uses agent-a's rules from registry + let result = task_a + .call( + &RunContext::minimal("test-model"), + serde_json::json!({ + "description": "Delegate to B", + "prompt": "Do something", + "subagent_type": "agent-b" + }), + ) + .await; + + assert!( + result.is_ok(), + "Should use per-agent rules from registry: {:?}", + result + ); +} + +#[tokio::test] +async fn task_tool_registry_caller_missing_entry_defaults_to_deny() { + // When caller name is not in registry, should default to deny-all + let agent_b = make_entry("agent-b", AgentMode::Subagent, Ruleset::new()); + + let registry = AgentRegistry::from_entries([("agent-b".to_string(), agent_b)]); + + let handle = Arc::new(TaskRegistryHandle::from_registry(Arc::new(registry))); + let snapshot = snapshot_from_registry(handle.get().unwrap()); + + // Use a caller name that doesn't exist in registry + let task_unknown = TaskTool::for_registry_caller( + Arc::clone(&handle), + "unknown-caller", + Ruleset::new(), + snapshot, + Arc::new(()), + ); + + let result = task_unknown + .call( + &RunContext::minimal("test-model"), + serde_json::json!({ + "description": "Try to delegate", + "prompt": "Do something", + "subagent_type": "agent-b" + }), + ) + .await; + + // Should be denied because unknown-caller has no rules in registry + assert!(result.is_err(), "Unknown caller should default to deny-all"); +} + +// Note: End-to-end tests with self-delegating agents would require a more complex +// setup to handle the circular dependency between agents and the registry. +// The existing tests above verify the core recursive delegation functionality: +// - depth_2_allow_chain_succeeds: Basic 1-hop delegation +// - depth_3_chain_with_runtime_permission_lookup: Multiple hops with separate TaskTool instances +// - task_tool_registry_caller_uses_per_agent_rules: Runtime permission lookup from registry diff --git a/src/llm-coding-tools-serdesai/tests/registry_integration.rs b/src/llm-coding-tools-serdesai/tests/registry_integration.rs new file mode 100644 index 00000000..c9a16d14 --- /dev/null +++ b/src/llm-coding-tools-serdesai/tests/registry_integration.rs @@ -0,0 +1,440 @@ +use ahash::AHashMap; +use indexmap::IndexMap; +use llm_coding_tools_agents::{AgentCatalog, AgentConfig, AgentMode, PermissionRule}; +use llm_coding_tools_core::permissions::PermissionAction; +use llm_coding_tools_core::tool_names; +use llm_coding_tools_models_dev::ModelsDevCatalog; +use llm_coding_tools_serdesai::{ + default_tools, AgentDefaults, AgentRegistryBuildError, AgentRegistryBuilder, ModelResolveError, + ModelResolver, ModelsDevResolver, ProviderOverride, ProviderOverrides, ResolutionSource, + ResolvedModel, TodoState, +}; +use std::sync::{Arc, Mutex}; + +static ENV_LOCK: Mutex<()> = Mutex::new(()); + +fn catalog_from_json(json: &str) -> ModelsDevCatalog { + let temp = tempfile::TempDir::new().expect("tempdir"); + let path = temp.path().join("api.json"); + std::fs::write(&path, json).expect("write api.json"); + ModelsDevCatalog::from_local_api_json(&path).expect("catalog") +} + +fn base_defaults(resolver: Arc) -> AgentDefaults { + AgentDefaults { + model: "openai:gpt-4o".to_string(), + model_resolver: Some(resolver), + provider_overrides: ProviderOverrides::new(), + api_key: None, + base_url: None, + temperature: None, + top_p: None, + options: AHashMap::new(), + } +} + +#[test] +fn registry_builds_mixed_openai_and_openai_compatible() { + let _guard = ENV_LOCK.lock().unwrap(); + unsafe { + std::env::set_var("OPENAI_API_KEY", "key"); + std::env::set_var("ROUTER_API_KEY", "key"); + } + let json = r#"{"providers":{"openai":{"id":"openai","npm":"@ai-sdk/openai","api":null,"env":["OPENAI_API_KEY"],"models":{"gpt-4o":{}}},"router":{"id":"router","npm":"@ai-sdk/openai-compatible","api":"https://router.example/v1","env":["ROUTER_API_KEY"],"models":{"m1":{}}}}}"#; + let resolver = ModelsDevResolver::new(Some(catalog_from_json(json)), ProviderOverrides::new()); + let defaults = base_defaults(Arc::new(resolver)); + + let config_openai = AgentConfig { + name: "primary".to_string(), + mode: AgentMode::Primary, + description: "primary agent".to_string(), + model: None, + hidden: false, + temperature: None, + top_p: None, + permission: IndexMap::new(), + options: AHashMap::new(), + prompt: String::new(), + }; + let config_router = AgentConfig { + name: "router".to_string(), + mode: AgentMode::Subagent, + description: "router agent".to_string(), + model: Some("router/m1".to_string()), + hidden: false, + temperature: None, + top_p: None, + permission: IndexMap::new(), + options: AHashMap::new(), + prompt: String::new(), + }; + let catalog = AgentCatalog::from_entries(vec![config_openai, config_router]); + + let registry = AgentRegistryBuilder::<()>::new(defaults, vec![]) + .build(&catalog) + .unwrap(); + assert_eq!(registry.iter().count(), 2); + + unsafe { + std::env::remove_var("OPENAI_API_KEY"); + std::env::remove_var("ROUTER_API_KEY"); + } +} + +#[test] +fn subagents_do_not_inherit_openai_defaults() { + let _guard = ENV_LOCK.lock().unwrap(); + unsafe { std::env::set_var("OPENAI_API_KEY", "key") }; + + // Ensure ANTHROPIC_API_KEY is not set + unsafe { std::env::remove_var("ANTHROPIC_API_KEY") }; + + let json = r#"{"providers":{"openai":{"id":"openai","npm":"@ai-sdk/openai","api":null,"env":["OPENAI_API_KEY"],"models":{"gpt-4o":{}}},"anthropic":{"id":"anthropic","npm":"@ai-sdk/anthropic","api":null,"env":["ANTHROPIC_API_KEY"],"models":{"claude-3":{}}}}}"#; + let overrides = ProviderOverrides::new().insert_override( + "openai", + ProviderOverride { + api_key: Some("key".into()), + base_url: None, + endpoint_env: None, + }, + ); + let resolver = ModelsDevResolver::new(Some(catalog_from_json(json)), overrides.clone()); + let defaults = AgentDefaults { + provider_overrides: overrides, + ..base_defaults(Arc::new(resolver)) + }; + + let config_subagent = AgentConfig { + name: "anthropic-agent".to_string(), + mode: AgentMode::Subagent, + description: "anthropic subagent".to_string(), + model: Some("anthropic:claude-3".to_string()), + hidden: false, + temperature: None, + top_p: None, + permission: IndexMap::new(), + options: AHashMap::new(), + prompt: String::new(), + }; + let catalog = AgentCatalog::from_entries(vec![config_subagent]); + let result = AgentRegistryBuilder::<()>::new(defaults, vec![]).build(&catalog); + + assert!(matches!( + result, + Err(AgentRegistryBuildError::BuildFailed { .. }) + )); + + unsafe { + std::env::remove_var("OPENAI_API_KEY"); + } +} + +#[test] +fn unsupported_providers_error() { + let _guard = ENV_LOCK.lock().unwrap(); + let json = r#"{"providers":{"azure":{"id":"azure","npm":"@ai-sdk/azure","api":null,"env":["AZURE_API_KEY"],"models":{"m1":{}}}}}"#; + let resolver = ModelsDevResolver::new(Some(catalog_from_json(json)), ProviderOverrides::new()); + let defaults = base_defaults(Arc::new(resolver)); + + let config = AgentConfig { + name: "azure-agent".to_string(), + mode: AgentMode::Subagent, + description: "azure agent".to_string(), + model: Some("azure:m1".to_string()), + hidden: false, + temperature: None, + top_p: None, + permission: IndexMap::new(), + options: AHashMap::new(), + prompt: String::new(), + }; + let catalog = AgentCatalog::from_entries(vec![config]); + let result = AgentRegistryBuilder::<()>::new(defaults, vec![]).build(&catalog); + assert!(matches!( + result, + Err(AgentRegistryBuildError::BuildFailed { .. }) + )); +} + +#[test] +fn registry_builds_huggingface_directly() { + let _guard = ENV_LOCK.lock().unwrap(); + unsafe { std::env::set_var("HF_TOKEN", "key") }; + let json = r#"{"providers":{"huggingface":{"id":"huggingface","npm":"@ai-sdk/huggingface","api":null,"env":["HF_TOKEN"],"models":{"tiiuae/falcon-7b":{}}}}}"#; + let resolver = ModelsDevResolver::new(Some(catalog_from_json(json)), ProviderOverrides::new()); + let defaults = base_defaults(Arc::new(resolver)); + + let config = AgentConfig { + name: "hf-agent".to_string(), + mode: AgentMode::Subagent, + description: "hf agent".to_string(), + model: Some("huggingface:tiiuae/falcon-7b".to_string()), + hidden: false, + temperature: None, + top_p: None, + permission: IndexMap::new(), + options: AHashMap::new(), + prompt: String::new(), + }; + let catalog = AgentCatalog::from_entries(vec![config]); + let result = AgentRegistryBuilder::<()>::new(defaults, vec![]).build(&catalog); + assert!(result.is_ok()); + + unsafe { + std::env::remove_var("HF_TOKEN"); + } +} + +#[test] +fn registry_builds_openrouter_directly() { + let _guard = ENV_LOCK.lock().unwrap(); + unsafe { std::env::set_var("OPENROUTER_API_KEY", "key") }; + let json = r#"{"providers":{"openrouter":{"id":"openrouter","npm":"@ai-sdk/openrouter","api":null,"env":["OPENROUTER_API_KEY"],"models":{"anthropic/claude-3-opus":{}}}}}"#; + let resolver = ModelsDevResolver::new(Some(catalog_from_json(json)), ProviderOverrides::new()); + let defaults = base_defaults(Arc::new(resolver)); + + let config = AgentConfig { + name: "openrouter-agent".to_string(), + mode: AgentMode::Subagent, + description: "openrouter agent".to_string(), + model: Some("openrouter:anthropic/claude-3-opus".to_string()), + hidden: false, + temperature: None, + top_p: None, + permission: IndexMap::new(), + options: AHashMap::new(), + prompt: String::new(), + }; + let catalog = AgentCatalog::from_entries(vec![config]); + let result = AgentRegistryBuilder::<()>::new(defaults, vec![]).build(&catalog); + assert!(result.is_ok()); + + unsafe { + std::env::remove_var("OPENROUTER_API_KEY"); + } +} + +#[test] +fn registry_builds_slash_spec_with_colon_model_id() { + let _guard = ENV_LOCK.lock().unwrap(); + unsafe { + std::env::set_var("SYNTH_API_KEY", "key"); + } + let json = r#"{"providers":{"synthetic":{"id":"synthetic","npm":"@ai-sdk/openai-compatible","api":"https://api.synthetic/v1","env":["SYNTH_API_KEY"],"models":{"hf:zai-org/GLM-4.7":{}}}}}"#; + let resolver = ModelsDevResolver::new(Some(catalog_from_json(json)), ProviderOverrides::new()); + let defaults = AgentDefaults { + model: "synthetic/hf:zai-org/GLM-4.7".to_string(), + ..base_defaults(Arc::new(resolver)) + }; + + let config = AgentConfig { + name: "synthetic-agent".to_string(), + mode: AgentMode::Primary, + description: "synthetic provider".to_string(), + model: None, + hidden: false, + temperature: None, + top_p: None, + permission: IndexMap::new(), + options: AHashMap::new(), + prompt: String::new(), + }; + + let catalog = AgentCatalog::from_entries(vec![config]); + let result = AgentRegistryBuilder::<()>::new(defaults, vec![]).build(&catalog); + assert!(result.is_ok()); + + unsafe { + std::env::remove_var("SYNTH_API_KEY"); + } +} + +#[test] +fn recursive_builder_injects_task_only_for_allow_configs_and_dedups() { + let _guard = ENV_LOCK.lock().unwrap(); + unsafe { std::env::set_var("OPENAI_API_KEY", "key") }; + + let json = r#"{"providers":{"openai":{"id":"openai","npm":"@ai-sdk/openai","api":null,"env":["OPENAI_API_KEY"],"models":{"gpt-4o":{}}}}}"#; + let resolver = ModelsDevResolver::new(Some(catalog_from_json(json)), ProviderOverrides::new()); + let defaults = base_defaults(Arc::new(resolver)); + let tools = default_tools(true, None, TodoState::new()); + + let mut allow_patterns = IndexMap::new(); + allow_patterns.insert("agent-b".to_string(), PermissionAction::Allow); + let mut deny_patterns = IndexMap::new(); + deny_patterns.insert("agent-c".to_string(), PermissionAction::Deny); + + let config_a = AgentConfig { + name: "agent-a".to_string(), + mode: AgentMode::Subagent, + description: "a".to_string(), + model: None, + hidden: false, + temperature: None, + top_p: None, + permission: IndexMap::from([( + tool_names::TASK.to_string(), + PermissionRule::Pattern(allow_patterns), + )]), + options: AHashMap::new(), + prompt: String::new(), + }; + + let config_b = AgentConfig { + name: "agent-b".to_string(), + mode: AgentMode::Subagent, + description: "b".to_string(), + model: None, + hidden: false, + temperature: None, + top_p: None, + permission: IndexMap::from([( + tool_names::TASK.to_string(), + PermissionRule::Action(PermissionAction::Allow), + )]), + options: AHashMap::new(), + prompt: String::new(), + }; + + let config_c = AgentConfig { + name: "agent-c".to_string(), + mode: AgentMode::Subagent, + description: "c".to_string(), + model: None, + hidden: false, + temperature: None, + top_p: None, + permission: IndexMap::from([( + tool_names::TASK.to_string(), + PermissionRule::Pattern(deny_patterns), + )]), + options: AHashMap::new(), + prompt: String::new(), + }; + + let catalog = AgentCatalog::from_entries(vec![config_a, config_b, config_c]); + let registry = AgentRegistryBuilder::<()>::new(defaults, tools) + .build_with_recursive_task(&catalog, Arc::new(())) + .unwrap(); + + let a = registry.get("agent-a").unwrap(); + let b = registry.get("agent-b").unwrap(); + let c = registry.get("agent-c").unwrap(); + + assert_eq!( + a.tool_names + .iter() + .filter(|n| *n == tool_names::TASK) + .count(), + 1 + ); + assert_eq!( + b.tool_names + .iter() + .filter(|n| *n == tool_names::TASK) + .count(), + 1 + ); + assert_eq!( + c.tool_names + .iter() + .filter(|n| *n == tool_names::TASK) + .count(), + 0 + ); + + unsafe { std::env::remove_var("OPENAI_API_KEY") }; +} + +#[test] +fn registry_builds_with_default_models_dev_resolver_when_none_injected() { + let provider_overrides = ProviderOverrides::new().insert_override( + "openai", + ProviderOverride { + api_key: Some("test-openai-key".to_string()), + base_url: None, + endpoint_env: None, + }, + ); + + let defaults = AgentDefaults { + model: "openai:gpt-4o".to_string(), + model_resolver: None, + provider_overrides, + api_key: None, + base_url: None, + temperature: None, + top_p: None, + options: AHashMap::new(), + }; + + let config = AgentConfig { + name: "default-resolver-agent".to_string(), + mode: AgentMode::Primary, + description: "default resolver path".to_string(), + model: None, + hidden: false, + temperature: None, + top_p: None, + permission: IndexMap::new(), + options: AHashMap::new(), + prompt: String::new(), + }; + + let catalog = AgentCatalog::from_entries(vec![config]); + let result = AgentRegistryBuilder::<()>::new(defaults, vec![]).build(&catalog); + assert!(result.is_ok()); +} + +// A simple custom resolver for testing injection +#[derive(Debug, Clone)] +struct TestCustomResolver; + +impl ModelResolver for TestCustomResolver { + fn resolve(&self, model_spec: &str) -> Result { + Ok(ResolvedModel { + spec: model_spec.to_string(), + runtime_provider: "openai".to_string(), + runtime_model_id: model_spec.to_string(), + runtime_spec: format!("openai:{}", model_spec), + api_key: Some("test-key".to_string()), + base_url: None, + timeout: None, + source: ResolutionSource::ExplicitOverride, + provider_id: "openai".to_string(), + }) + } +} + +#[test] +fn registry_builds_with_injected_custom_resolver() { + let custom_resolver = Arc::new(TestCustomResolver); + + let defaults = AgentDefaults { + model: "custom-model".to_string(), + model_resolver: Some(custom_resolver), + provider_overrides: ProviderOverrides::new(), + api_key: None, + base_url: None, + temperature: None, + top_p: None, + options: AHashMap::new(), + }; + + let config = AgentConfig { + name: "custom-resolver-agent".to_string(), + mode: AgentMode::Primary, + description: "custom resolver test".to_string(), + model: None, + hidden: false, + temperature: None, + top_p: None, + permission: IndexMap::new(), + options: AHashMap::new(), + prompt: String::new(), + }; + + let catalog = AgentCatalog::from_entries(vec![config]); + let result = AgentRegistryBuilder::<()>::new(defaults, vec![]).build(&catalog); + assert!(result.is_ok()); +}