From db991d8f878f10067d5ffb3a26fbfb70403e7e83 Mon Sep 17 00:00:00 2001 From: Yuanhao Li Date: Sat, 11 Jul 2026 02:22:33 +0200 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20Meta=20Model=20API=20preset=20?= =?UTF-8?q?=E2=80=94=20Muse=20Spark,=20day=20one?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Meta opened its first paid API this week (public preview): the Meta Model API serving Muse Spark 1.1, OpenAI-compatible. yoagent supports it via the compat provider with a one-preset addition. - ModelConfig::meta(id, name): base https://api.meta.ai/v1, protocol OpenAiCompletions, 1,048,576 context / 131,072 max output, launch pricing ($1.25/M in, $4.25/M out) pre-configured for session_cost_usd and the telemetry cost_usd span field. - OpenAiCompat::meta(): max_completion_tokens; conservative flags (reasoning_effort/streamed-usage off) until Meta documents them on the chat endpoint. - Key resolution: META_API_KEY preferred, Meta's documented MODEL_API_KEY accepted (registry table + hint updated). - CLI example: --provider meta (default model muse-spark-1.1). Tests: preset values pinned to the launch specs (URL, window, pricing, conservative flags); env resolution incl. preference order. Docs: model-presets table row, README provider list, CHANGELOG. Co-Authored-By: Claude Fable 5 --- CHANGELOG.md | 6 ++++ README.md | 2 +- docs/providers/model-presets.md | 1 + examples/cli.rs | 4 ++- src/provider/model.rs | 60 +++++++++++++++++++++++++++++++++ src/provider/registry.rs | 17 ++++++++++ 6 files changed, 88 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c894027..e05f1ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,12 @@ 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 (public preview): + 1M context, 128K output, launch pricing pre-configured. 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..ec85b18 100644 --- a/README.md +++ b/README.md @@ -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..0500d25 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) | `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..8f69c5a 100644 --- a/examples/cli.rs +++ b/examples/cli.rs @@ -100,6 +100,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 +343,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..31b9058 100644 --- a/src/provider/model.rs +++ b/src/provider/model.rs @@ -149,6 +149,18 @@ impl OpenAiCompat { } } + /// Compat flags for the Meta Model API (Muse Spark). + /// + /// OpenAI-compatible chat completions. Conservative flags until Meta + /// documents reasoning-effort and streamed-usage support on the chat + /// endpoint. + pub fn meta() -> Self { + Self { + max_tokens_field: MaxTokensField::MaxCompletionTokens, + ..Default::default() + } + } + /// Compat flags for xAI (Grok). pub fn xai() -> Self { Self { @@ -683,6 +695,34 @@ impl ModelConfig { } } + /// Create a new Meta Model API config (Muse Spark). + /// + /// Models: `muse-spark-1.1` (1M context, 128K max output). Public + /// preview; OpenAI-compatible endpoint at `https://api.meta.ai/v1`. + /// Key resolves from `META_API_KEY`, then Meta's documented + /// `MODEL_API_KEY`. + 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: false, + 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.0, + 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 +869,26 @@ 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); + let compat = mc.compat.expect("compat flags set"); + assert!(matches!( + compat.max_tokens_field, + MaxTokensField::MaxCompletionTokens + )); + // Conservative until Meta documents these on chat completions: + assert!(!compat.supports_reasoning_effort); + } + #[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..5064a0e 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,18 @@ mod resolve_key_tests { assert_eq!(resolve_api_key("vertex"), None); } + #[test] + fn test_meta_env_resolution() { + // Own vars, no other test touches them. + 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 From eedd82899be7564c8b161df23c59451ec51b66cf Mon Sep 17 00:00:00 2001 From: Yuanhao Li Date: Sat, 11 Jul 2026 05:44:12 +0200 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20address=20PR=20#69=20review=20findin?= =?UTF-8?q?gs=20=E2=80=94=20Meta=20docs=20say=20more=20than=20we=20assumed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fact-check against Meta's official chat-completions schemas and pricing page corrected two false assumptions and confirmed the rest: - reasoning_effort IS documented (none..xhigh, server default medium) — supports_reasoning_effort now true, so ThinkingLevel tunes it. Preset docs note the gotcha: Off omits the field, which means Meta's medium default applies, not "no reasoning". reasoning: true accordingly; README caveat updated (openai/deepseek/meta opt in). - Cached-input pricing added: $0.15/M per the official pricing page (was 0.0 while the CHANGELOG claimed "launch pricing pre-configured") — cache-heavy workloads no longer undercount spend. Test pins it. - Streamed usage is documented (stream_options.include_usage) — flag and comment now say so honestly. - 128K max output softened to "per Meta's integration examples (no official model card yet)"; US-only public preview noted with a date anchor in the preset docs + presets table. - Registry env test hardened (clears a developer-shell META_API_KEY first); comment ages better. - CLI example: qwen-style special case so --provider meta actually reads META_API_KEY / MODEL_API_KEY instead of demanding ANTHROPIC_API_KEY. Verified clean by the fact-checker: base URL, model id casing, 1,048,576 context, $1.25/$4.25 pricing, MODEL_API_KEY attribution wording, and MaxCompletionTokens (Meta deprecates max_tokens — the code reviewer's concern resolved in our favor by the official schemas). Co-Authored-By: Claude Fable 5 --- CHANGELOG.md | 10 ++++++---- README.md | 2 +- docs/providers/model-presets.md | 2 +- examples/cli.rs | 4 ++++ src/provider/model.rs | 35 ++++++++++++++++++++++----------- src/provider/registry.rs | 4 +++- 6 files changed, 39 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e05f1ba..e812fbf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,10 +9,12 @@ 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 (public preview): - 1M context, 128K output, launch pricing pre-configured. Key resolves from - `META_API_KEY`, then Meta's documented `MODEL_API_KEY`. Also available in - the CLI example via `--provider meta`. + ...)` 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 diff --git a/README.md b/README.md index ec85b18..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 diff --git a/docs/providers/model-presets.md b/docs/providers/model-presets.md index 0500d25..0a8400f 100644 --- a/docs/providers/model-presets.md +++ b/docs/providers/model-presets.md @@ -23,7 +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) | `OpenAiCompletions` | `https://api.meta.ai/v1` | 1M | 131,072 | +| `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 8f69c5a..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")) diff --git a/src/provider/model.rs b/src/provider/model.rs index 31b9058..b4045b9 100644 --- a/src/provider/model.rs +++ b/src/provider/model.rs @@ -151,11 +151,14 @@ impl OpenAiCompat { /// Compat flags for the Meta Model API (Muse Spark). /// - /// OpenAI-compatible chat completions. Conservative flags until Meta - /// documents reasoning-effort and streamed-usage support on the chat - /// endpoint. + /// 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() } @@ -697,10 +700,16 @@ impl ModelConfig { /// Create a new Meta Model API config (Muse Spark). /// - /// Models: `muse-spark-1.1` (1M context, 128K max output). Public - /// preview; OpenAI-compatible endpoint at `https://api.meta.ai/v1`. - /// Key resolves from `META_API_KEY`, then Meta's documented - /// `MODEL_API_KEY`. + /// 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(), @@ -708,13 +717,13 @@ impl ModelConfig { api: ApiProtocol::OpenAiCompletions, provider: "meta".into(), base_url: "https://api.meta.ai/v1".into(), - reasoning: false, + 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.0, + cache_read_per_million: 0.15, cache_write_per_million: 0.0, }, headers: HashMap::new(), @@ -880,13 +889,17 @@ mod tests { 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 )); - // Conservative until Meta documents these on chat completions: - assert!(!compat.supports_reasoning_effort); + // Documented in Meta's chat-completions schemas. + assert!(compat.supports_reasoning_effort); + assert!(compat.supports_usage_in_streaming); } #[test] diff --git a/src/provider/registry.rs b/src/provider/registry.rs index 5064a0e..5219584 100644 --- a/src/provider/registry.rs +++ b/src/provider/registry.rs @@ -251,7 +251,9 @@ mod resolve_key_tests { #[test] fn test_meta_env_resolution() { - // Own vars, no other test touches them. + // 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.