diff --git a/CHANGELOG.md b/CHANGELOG.md index c894027..e812fbf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,14 @@ adheres to [Semantic Versioning](https://semver.org/). ### Added +- **Meta Model API (Muse Spark)** — `ModelConfig::meta("muse-spark-1.1", + ...)` preset for Meta's OpenAI-compatible endpoint (US-only public preview + at launch): 1M context, 128K output, launch pricing pre-configured + ($1.25/$4.25 per M, $0.15/M cached input). `reasoning_effort` is wired + (`ThinkingLevel` tunes it; note Meta's server default is `medium`). Key + resolves from `META_API_KEY`, then Meta's documented `MODEL_API_KEY`. Also + available in the CLI example via `--provider meta`. + - **GASP bridge** (feature `gasp`) — `gasp::GaspRecorder` records agent runs into a [GASP](https://github.com/yologdev/gasp) agent repo via `yoagent-state`: append-only `state/events.jsonl` (goal/run/model/tool diff --git a/README.md b/README.md index e3fbda7..4e9d084 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ Everything is observable via events. Supports 7 API protocols covering 20+ LLM p - 7 API protocols, 20+ providers behind one `StreamProvider` trait - One OpenAI-compatible implementation covers OpenAI, xAI, Groq, Cerebras, OpenRouter, Mistral, and more - Per-provider quirk flags (`OpenAiCompat`, `AnthropicCompat`) handle auth, reasoning format, and tool handling differences -- Capability notes: thinking/reasoning controls are wired for **all 7 protocols** — Anthropic (adaptive/budget), OpenAI-compatible where the `ModelConfig` opts in (the `openai()`/`deepseek()` presets do; other compat presets silently drop `thinking_level`), OpenAI Responses & Azure (reasoning effort), Gemini & Vertex (`thinkingConfig` with thought summaries streamed back), Bedrock (Anthropic-style budgets, reasoning deltas streamed back). Client-side prompt-cache breakpoints are Anthropic-specific; most other providers cache server-side automatically, but Bedrock has no automatic caching +- Capability notes: thinking/reasoning controls are wired for **all 7 protocols** — Anthropic (adaptive/budget), OpenAI-compatible where the `ModelConfig` opts in (the `openai()`/`deepseek()`/`meta()` presets do; other compat presets silently drop `thinking_level`), OpenAI Responses & Azure (reasoning effort), Gemini & Vertex (`thinkingConfig` with thought summaries streamed back), Bedrock (Anthropic-style budgets, reasoning deltas streamed back). Client-side prompt-cache breakpoints are Anthropic-specific; most other providers cache server-side automatically, but Bedrock has no automatic caching **Built-in Tools** - `bash` — Shell execution with timeout, output truncation, command deny patterns @@ -206,7 +206,7 @@ let mut agent = Agent::from_config(ModelConfig::google("gemini-2.5-pro", "Gemini | Protocol | Providers | |----------|-----------| | Anthropic Messages | Anthropic (Claude) | -| OpenAI Completions | OpenAI, xAI, Groq, Mistral, DeepSeek, MiniMax, Z.ai, Qwen, Ollama, local servers, and custom compatible APIs | +| OpenAI Completions | OpenAI, xAI, Groq, Mistral, DeepSeek, MiniMax, Z.ai, Qwen, Meta (Muse Spark), Ollama, local servers, and custom compatible APIs | | OpenAI Responses | OpenAI (newer API) | | Azure OpenAI | Azure OpenAI | | Google Generative AI | Google Gemini | diff --git a/docs/providers/model-presets.md b/docs/providers/model-presets.md index 526a2d6..0a8400f 100644 --- a/docs/providers/model-presets.md +++ b/docs/providers/model-presets.md @@ -23,6 +23,7 @@ Use a preset when the provider is listed here. Use a custom `ModelConfig` when y | `ModelConfig::deepseek(id, name)` | DeepSeek | `OpenAiCompletions` | `https://api.deepseek.com` | 1M | 384K | | `ModelConfig::mistral(id, name)` | Mistral | `OpenAiCompletions` | `https://api.mistral.ai/v1` | 128K | 4,096 | | `ModelConfig::minimax(id, name)` | MiniMax | `OpenAiCompletions` | `https://api.minimaxi.chat/v1` | 1M | 4,096 | +| `ModelConfig::meta(id, name)` | Meta (Muse Spark) — US-only preview as of 2026-07 | `OpenAiCompletions` | `https://api.meta.ai/v1` | 1M | 131,072 | | `ModelConfig::zai(id, name)` | Z.ai | `OpenAiCompletions` | `https://api.z.ai/api/paas/v4` | 128K | 4,096 | | `ModelConfig::qwen(id, name)` | Qwen / DashScope | `OpenAiCompletions` | `https://dashscope-intl.aliyuncs.com/compatible-mode/v1` | 128K | 4,096 | | `ModelConfig::ollama(base_url, model_id)` | Ollama | `OpenAiCompletions` | caller provided | 128K | 4,096 | diff --git a/examples/cli.rs b/examples/cli.rs index c629a35..1aa2831 100644 --- a/examples/cli.rs +++ b/examples/cli.rs @@ -81,6 +81,10 @@ async fn main() { std::env::var("DASHSCOPE_API_KEY") .or_else(|_| std::env::var("API_KEY")) .expect("Set DASHSCOPE_API_KEY or API_KEY") + } else if provider_name.as_deref() == Some("meta") { + std::env::var("META_API_KEY") + .or_else(|_| std::env::var("MODEL_API_KEY")) + .expect("Set META_API_KEY or MODEL_API_KEY") } else if api_key_optional { std::env::var("ANTHROPIC_API_KEY") .or_else(|_| std::env::var("API_KEY")) @@ -100,6 +104,7 @@ async fn main() { Some("deepseek") => "deepseek-v4-flash", Some("mistral") => "mistral-large-latest", Some("minimax") => "MiniMax-Text-01", + Some("meta") => "muse-spark-1.1", Some("ollama") => "llama3.1:8b", Some("google") => "gemini-2.5-pro", _ => "claude-sonnet-5", @@ -342,10 +347,11 @@ fn make_provider_agent(provider: &str, model: &str) -> Agent { "deepseek" => ModelConfig::deepseek(model, model), "mistral" => ModelConfig::mistral(model, model), "minimax" => ModelConfig::minimax(model, model), + "meta" => ModelConfig::meta(model, model), "ollama" => ModelConfig::ollama("http://localhost:11434/v1", model), "google" => ModelConfig::google(model, model), other => { - eprintln!("Unknown provider: {other}. Supported: zai, qwen, openai, xai, groq, deepseek, mistral, minimax, ollama, google."); + eprintln!("Unknown provider: {other}. Supported: zai, qwen, openai, xai, groq, deepseek, mistral, minimax, meta, ollama, google."); std::process::exit(1); } }; diff --git a/src/provider/model.rs b/src/provider/model.rs index a14be0f..b4045b9 100644 --- a/src/provider/model.rs +++ b/src/provider/model.rs @@ -149,6 +149,21 @@ impl OpenAiCompat { } } + /// Compat flags for the Meta Model API (Muse Spark). + /// + /// OpenAI-compatible chat completions. Meta documents `reasoning_effort` + /// (default `medium` server-side) and streamed usage via + /// `stream_options.include_usage`; `max_tokens` is deprecated in favor of + /// `max_completion_tokens`. + pub fn meta() -> Self { + Self { + supports_reasoning_effort: true, + supports_usage_in_streaming: true, + max_tokens_field: MaxTokensField::MaxCompletionTokens, + ..Default::default() + } + } + /// Compat flags for xAI (Grok). pub fn xai() -> Self { Self { @@ -683,6 +698,40 @@ impl ModelConfig { } } + /// Create a new Meta Model API config (Muse Spark). + /// + /// Models: `muse-spark-1.1` — 1,048,576-token context; 128K max output + /// per Meta's integration examples (no official model card yet). + /// US-only public preview as of July 2026. OpenAI-compatible endpoint at + /// `https://api.meta.ai/v1`. Key resolves from `META_API_KEY`, then + /// Meta's documented `MODEL_API_KEY`. + /// + /// Reasoning: Meta's endpoint defaults to `reasoning_effort: medium` + /// server-side. Set a [`ThinkingLevel`](crate::types::ThinkingLevel) to + /// tune it; `Off` omits the field, which means Meta's default (medium) + /// applies — not "no reasoning". + pub fn meta(id: impl Into, name: impl Into) -> Self { + Self { + id: id.into(), + name: name.into(), + api: ApiProtocol::OpenAiCompletions, + provider: "meta".into(), + base_url: "https://api.meta.ai/v1".into(), + reasoning: true, + context_window: 1_048_576, + max_tokens: 131_072, + cost: CostConfig { + input_per_million: 1.25, + output_per_million: 4.25, + cache_read_per_million: 0.15, + cache_write_per_million: 0.0, + }, + headers: HashMap::new(), + anthropic: None, + compat: Some(OpenAiCompat::meta()), + } + } + /// Create a new MiniMax model config. /// /// Models: `MiniMax-Text-01`, `MiniMax-M1`, etc. @@ -829,6 +878,30 @@ impl ModelConfig { mod tests { use super::*; + #[test] + fn meta_preset_matches_launch_specs() { + let mc = ModelConfig::meta("muse-spark-1.1", "Muse Spark 1.1"); + assert_eq!(mc.provider, "meta"); + assert_eq!(mc.api, ApiProtocol::OpenAiCompletions); + assert_eq!(mc.base_url, "https://api.meta.ai/v1"); + assert_eq!(mc.context_window, 1_048_576); + assert_eq!(mc.max_tokens, 131_072); + assert!(mc.cost.is_configured()); + assert_eq!(mc.cost.input_per_million, 1.25); + assert_eq!(mc.cost.output_per_million, 4.25); + // Meta documents a cached-input rate; cache writes are not charged. + assert_eq!(mc.cost.cache_read_per_million, 0.15); + assert_eq!(mc.cost.cache_write_per_million, 0.0); + let compat = mc.compat.expect("compat flags set"); + assert!(matches!( + compat.max_tokens_field, + MaxTokensField::MaxCompletionTokens + )); + // Documented in Meta's chat-completions schemas. + assert!(compat.supports_reasoning_effort); + assert!(compat.supports_usage_in_streaming); + } + #[test] fn test_model_config_anthropic() { let config = ModelConfig::anthropic("claude-sonnet-5", "Claude Sonnet 5"); diff --git a/src/provider/registry.rs b/src/provider/registry.rs index 1cb48cb..5219584 100644 --- a/src/provider/registry.rs +++ b/src/provider/registry.rs @@ -141,6 +141,7 @@ mod tests { /// | `google` | `GEMINI_API_KEY`, `GOOGLE_API_KEY` | /// | `xai` / `groq` / `deepseek` / `mistral` / `zai` / `minimax` / `openrouter` / `cerebras` | `_API_KEY` | /// | `qwen` | `DASHSCOPE_API_KEY` | +/// | `meta` | `META_API_KEY`, then `MODEL_API_KEY` | /// | `opencode-zen` / `opencode-go` | `OPENCODE_API_KEY` | /// | `azure` | `AZURE_OPENAI_API_KEY` | /// | `bedrock` | `AWS_ACCESS_KEY_ID` + `AWS_SECRET_ACCESS_KEY` (+ `AWS_SESSION_TOKEN`), composed as `access:secret[:token]` | @@ -166,6 +167,9 @@ pub fn resolve_api_key(provider: &str) -> Option { "mistral" => first(&["MISTRAL_API_KEY"]), "zai" => first(&["ZAI_API_KEY"]), "minimax" => first(&["MINIMAX_API_KEY"]), + // Meta's docs name the generic MODEL_API_KEY; prefer the unambiguous + // META_API_KEY, accept the official one. + "meta" => first(&["META_API_KEY", "MODEL_API_KEY"]), "openrouter" => first(&["OPENROUTER_API_KEY"]), "cerebras" => first(&["CEREBRAS_API_KEY"]), "qwen" => first(&["DASHSCOPE_API_KEY"]), @@ -217,6 +221,7 @@ fn api_key_env_hint(provider: &str) -> &'static str { "mistral" => "set MISTRAL_API_KEY or call .with_api_key(...)", "zai" => "set ZAI_API_KEY or call .with_api_key(...)", "minimax" => "set MINIMAX_API_KEY or call .with_api_key(...)", + "meta" => "set META_API_KEY (or MODEL_API_KEY) or call .with_api_key(...)", "openrouter" => "set OPENROUTER_API_KEY or call .with_api_key(...)", "cerebras" => "set CEREBRAS_API_KEY or call .with_api_key(...)", "qwen" => "set DASHSCOPE_API_KEY or call .with_api_key(...)", @@ -244,6 +249,20 @@ mod resolve_key_tests { assert_eq!(resolve_api_key("vertex"), None); } + #[test] + fn test_meta_env_resolution() { + // Own vars as of this writing — grep before reusing MODEL_API_KEY in + // another test. Clear first: a developer shell may export these. + std::env::remove_var("META_API_KEY"); + std::env::set_var("MODEL_API_KEY", "official-var"); + assert_eq!(resolve_api_key("meta").as_deref(), Some("official-var")); + // The unambiguous var wins when both are set. + std::env::set_var("META_API_KEY", "preferred-var"); + assert_eq!(resolve_api_key("meta").as_deref(), Some("preferred-var")); + std::env::remove_var("META_API_KEY"); + std::env::remove_var("MODEL_API_KEY"); + } + #[test] fn test_env_resolution() { // Use a provider name unique to this test to avoid env races with