Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 |
Expand Down
1 change: 1 addition & 0 deletions docs/providers/model-presets.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
8 changes: 7 additions & 1 deletion examples/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
Expand All @@ -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",
Expand Down Expand Up @@ -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);
}
};
Expand Down
73 changes: 73 additions & 0 deletions src/provider/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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<String>, name: impl Into<String>) -> 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.
Expand Down Expand Up @@ -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");
Expand Down
19 changes: 19 additions & 0 deletions src/provider/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ mod tests {
/// | `google` | `GEMINI_API_KEY`, `GOOGLE_API_KEY` |
/// | `xai` / `groq` / `deepseek` / `mistral` / `zai` / `minimax` / `openrouter` / `cerebras` | `<PROVIDER>_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]` |
Expand All @@ -166,6 +167,9 @@ pub fn resolve_api_key(provider: &str) -> Option<String> {
"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"]),
Expand Down Expand Up @@ -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(...)",
Expand Down Expand Up @@ -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
Expand Down
Loading