use crate::Api;
use crate::catalog::BuiltinProviderEntry;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AuthMethod {
Bearer,
XApiKey,
ApiKey,
None,
}
#[derive(Debug, Clone)]
pub struct BuiltinProvider {
pub name: &'static str,
pub display_name: &'static str,
pub aliases: &'static [&'static str],
pub api: Api,
pub env_key: &'static str,
pub extra_env_keys: &'static [&'static str],
pub base_url: &'static str,
pub default_enabled: bool,
pub auth_method: AuthMethod,
pub extra_headers: &'static [(&'static str, &'static str)],
pub category: &'static str,
pub description: &'static str,
}
fn parse_api(s: &str) -> Api {
Api::from_kebab_str(s).unwrap_or(Api::OpenAiCompletions)
}
impl From<&BuiltinProviderEntry> for BuiltinProvider {
fn from(entry: &BuiltinProviderEntry) -> Self {
let name: &'static str = Box::leak(entry.id.clone().into_boxed_str());
let display_name: &'static str = Box::leak(entry.display_name.clone().into_boxed_str());
let aliases: &'static [&'static str] = Box::leak(
entry
.aliases
.iter()
.map(|s| Box::leak(s.clone().into_boxed_str()) as &'static str)
.collect::<Vec<_>>()
.into_boxed_slice(),
);
let env_key: &'static str = Box::leak(entry.env_key.clone().into_boxed_str());
let extra_env_keys: &'static [&'static str] = Box::leak(
entry
.extra_env_keys
.iter()
.map(|s| Box::leak(s.clone().into_boxed_str()) as &'static str)
.collect::<Vec<_>>()
.into_boxed_slice(),
);
let base_url: &'static str = Box::leak(entry.base_url.clone().into_boxed_str());
let category: &'static str = Box::leak(entry.category.clone().into_boxed_str());
let description: &'static str = Box::leak(entry.description.clone().into_boxed_str());
let extra_headers: &'static [(&'static str, &'static str)] = Box::leak(
entry
.extra_headers
.iter()
.map(|(k, v)| {
(
Box::leak(k.clone().into_boxed_str()) as &'static str,
Box::leak(v.clone().into_boxed_str()) as &'static str,
)
})
.collect::<Vec<_>>()
.into_boxed_slice(),
);
BuiltinProvider {
name,
display_name,
aliases,
api: parse_api(&entry.api),
env_key,
extra_env_keys,
base_url,
default_enabled: entry.default_enabled,
auth_method: match entry.auth_method {
crate::catalog::AuthMethod::Bearer => AuthMethod::Bearer,
crate::catalog::AuthMethod::XApiKey => AuthMethod::XApiKey,
crate::catalog::AuthMethod::ApiKey => AuthMethod::ApiKey,
crate::catalog::AuthMethod::None => AuthMethod::None,
},
extra_headers,
category,
description,
}
}
}
static API_TO_PROVIDER: &[(&str, Api)] = &[
("anthropic-messages", Api::AnthropicMessages),
("openai-completions", Api::OpenAiCompletions),
("openai-responses", Api::OpenAiResponses),
("openai-codex-responses", Api::OpenAiCodexResponses),
("azure-openai-responses", Api::AzureOpenAiResponses),
("google-generative-ai", Api::GoogleGenerativeAi),
("google-gemini-cli", Api::GoogleGeminiCli),
("google-vertex", Api::GoogleVertex),
("bedrock-converse-stream", Api::BedrockConverseStream),
("cursor-agent", Api::CursorAgent),
("cursor", Api::CursorAgent),
("devin-agent", Api::DevinAgent),
("devin", Api::DevinAgent),
("gitlab-duo", Api::GitLabDuo),
("gitlab-duo-agent", Api::GitLabDuoAgent),
];
pub fn get_builtin_providers() -> &'static [BuiltinProvider] {
static CACHE: std::sync::OnceLock<Vec<BuiltinProvider>> = std::sync::OnceLock::new();
CACHE
.get_or_init(|| {
let mut builtins = crate::catalog::materialize::materialize_providers();
if let Some(overrides) = crate::catalog::load_overrides() {
crate::catalog::apply_provider_overrides(&mut builtins, &overrides.provider);
}
builtins.iter().map(BuiltinProvider::from).collect()
})
.as_slice()
}
pub fn get_builtin_provider(name: &str) -> Option<&'static BuiltinProvider> {
get_builtin_providers()
.iter()
.find(|p| p.name == name || p.aliases.contains(&name))
}
pub fn get_provider_env_key(name: &str) -> Option<&'static str> {
get_builtin_provider(name).map(|p| p.env_key)
}
pub fn get_provider_env_keys(name: &str) -> Vec<&'static str> {
if let Some(p) = get_builtin_provider(name) {
let mut keys = vec![p.env_key];
keys.extend_from_slice(p.extra_env_keys);
keys
} else {
vec![]
}
}
pub fn get_provider_api(name: &str) -> Option<Api> {
get_builtin_provider(name).map(|p| p.api)
}
pub fn get_provider_base_url(name: &str) -> Option<&'static str> {
get_builtin_provider(name).map(|p| p.base_url)
}
pub fn get_api_mappings() -> &'static [(&'static str, Api)] {
API_TO_PROVIDER
}
pub fn get_all_provider_names() -> Vec<&'static str> {
get_builtin_providers().iter().map(|p| p.name).collect()
}
pub fn get_all_provider_aliases() -> Vec<&'static str> {
let mut names: Vec<&'static str> = get_builtin_providers()
.iter()
.flat_map(|p| std::iter::once(p.name).chain(p.aliases.iter().copied()))
.collect();
names.sort();
names.dedup();
names
}
pub fn resolve_provider_name(name: &str) -> Option<&'static str> {
get_builtin_provider(name).map(|p| p.name)
}
pub fn is_builtin_provider(name: &str) -> bool {
get_builtin_provider(name).is_some()
}
pub fn create_builtin_provider(name: &str) -> Option<Box<dyn super::Provider>> {
let builtin = get_builtin_provider(name)?;
build_builtin_transport(builtin)
}
fn build_builtin_transport(builtin: &'static BuiltinProvider) -> Option<Box<dyn super::Provider>> {
match builtin.api {
Api::AnthropicMessages => {
let extra_headers: Vec<(String, String)> = builtin
.extra_headers
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect();
if !builtin.base_url.is_empty() && builtin.name != "anthropic" {
Some(Box::new(super::anthropic::AnthropicProvider::with_config(
builtin.base_url,
None,
extra_headers,
)))
} else {
Some(Box::new(super::anthropic::AnthropicProvider::new()))
}
}
Api::GoogleGenerativeAi => Some(Box::new(super::google::GoogleProvider::new())),
Api::GoogleGeminiCli => Some(Box::new(super::gemini_cli::GeminiCliProvider::new())),
Api::GoogleVertex => Some(Box::new(super::vertex::VertexProvider::new())),
Api::AzureOpenAiResponses => Some(Box::new(super::azure::AzureProvider::new())),
Api::BedrockConverseStream => Some(Box::new(super::bedrock::BedrockProvider::new())),
Api::OpenAiResponses => Some(Box::new(
super::openai_responses::OpenAiResponsesProvider::new(),
)),
Api::OpenAiCodexResponses => Some(Box::new(
super::openai_responses::OpenAiResponsesProvider::new(),
)),
Api::OpenAiCompletions => {
let extra_headers: Vec<(String, String)> = builtin
.extra_headers
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect();
if builtin.base_url.is_empty() {
if extra_headers.is_empty() {
Some(Box::new(super::openai::OpenAiProvider::new()))
} else {
Some(Box::new(super::openai::OpenAiProvider::with_config(
"https://api.openai.com/v1",
None,
extra_headers,
)))
}
} else if extra_headers.is_empty() {
Some(Box::new(super::openai::OpenAiProvider::with_base_url(
builtin.base_url,
)))
} else {
Some(Box::new(super::openai::OpenAiProvider::with_config(
builtin.base_url,
None,
extra_headers,
)))
}
}
Api::OllamaChat => {
let base = if builtin.base_url.is_empty() {
"http://localhost:11434"
} else {
builtin.base_url
};
Some(Box::new(super::ollama::OllamaProvider::with_base_url(base)))
}
Api::CursorAgent => Some(Box::new(super::cursor::CursorProvider::new())),
Api::DevinAgent => Some(Box::new(super::devin::DevinProvider::new())),
Api::GitLabDuo => Some(Box::new(super::gitlab_duo::GitLabDuoProvider::new())),
Api::GitLabDuoAgent => Some(Box::new(
super::gitlab_duo_agent::GitLabDuoAgentProvider::new(),
)),
_ => None,
}
}
pub fn create_builtin_provider_with_options(
name: &str,
api_key: Option<&str>,
base_url: Option<&str>,
) -> Option<Box<dyn super::Provider>> {
let builtin = get_builtin_provider(name)?;
build_builtin_transport_with_options(builtin, api_key, base_url)
}
fn build_builtin_transport_with_options(
builtin: &'static BuiltinProvider,
api_key: Option<&str>,
base_url: Option<&str>,
) -> Option<Box<dyn super::Provider>> {
let resolved_key = api_key.map(String::from).or_else(|| {
std::env::var(builtin.env_key).ok().or_else(|| {
builtin
.extra_env_keys
.iter()
.find_map(|k| std::env::var(k).ok())
})
});
let resolved_base_url = base_url.map(String::from).or_else(|| {
if builtin.base_url.is_empty() {
None
} else {
Some(builtin.base_url.to_string())
}
});
let extra_headers: Vec<(String, String)> = builtin
.extra_headers
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect();
match builtin.api {
Api::AnthropicMessages => {
if let Some(key) = resolved_key {
Some(Box::new(super::anthropic::AnthropicProvider::with_config(
resolved_base_url
.as_deref()
.unwrap_or("https://api.anthropic.com"),
Some(key),
extra_headers,
)))
} else if resolved_base_url.is_some() {
Some(Box::new(super::anthropic::AnthropicProvider::with_config(
resolved_base_url
.as_deref()
.unwrap_or("https://api.anthropic.com"),
None,
extra_headers,
)))
} else {
build_builtin_transport(builtin)
}
}
Api::GoogleGenerativeAi => build_builtin_transport(builtin),
Api::GoogleGeminiCli => build_builtin_transport(builtin),
Api::GoogleVertex => build_builtin_transport(builtin),
Api::AzureOpenAiResponses => build_builtin_transport(builtin),
Api::BedrockConverseStream => build_builtin_transport(builtin),
Api::OpenAiResponses => build_builtin_transport(builtin),
Api::OpenAiCodexResponses => build_builtin_transport(builtin),
Api::OpenAiCompletions => {
let url = resolved_base_url
.as_deref()
.unwrap_or(if builtin.base_url.is_empty() {
"https://api.openai.com/v1"
} else {
builtin.base_url
});
if let Some(key) = resolved_key {
if extra_headers.is_empty() {
Some(Box::new(
super::openai::OpenAiProvider::with_base_url_and_key(url, Some(key)),
))
} else {
Some(Box::new(super::openai::OpenAiProvider::with_config(
url,
Some(key),
extra_headers,
)))
}
} else if url != builtin.base_url || !extra_headers.is_empty() {
Some(Box::new(super::openai::OpenAiProvider::with_config(
url,
None,
extra_headers,
)))
} else {
build_builtin_transport(builtin)
}
}
Api::OllamaChat => {
let url = resolved_base_url
.as_deref()
.unwrap_or("http://localhost:11434");
Some(Box::new(super::ollama::OllamaProvider::with_config(
url,
resolved_key,
)))
}
Api::CursorAgent => build_builtin_transport(builtin),
Api::DevinAgent => build_builtin_transport(builtin),
Api::GitLabDuo => build_builtin_transport(builtin),
Api::GitLabDuoAgent => build_builtin_transport(builtin),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_create_builtin_provider_anthropic() {
assert!(create_builtin_provider("anthropic").is_some());
}
#[test]
fn test_create_builtin_provider_openai() {
assert!(create_builtin_provider("openai").is_some());
}
#[test]
fn test_create_builtin_provider_by_alias() {
assert!(create_builtin_provider("amazon-bedrock").is_some());
}
#[test]
fn test_create_builtin_provider_unknown() {
assert!(create_builtin_provider("unknown").is_none());
}
#[test]
fn layer2_override_adds_provider() {
use std::io::Write;
let dir = std::env::temp_dir().join("oxi-test-layer2");
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("overrides.toml");
let mut f = std::fs::File::create(&path).unwrap();
writeln!(
f,
r#"
[[provider]]
id = "test-injected-{}"
display_name = "Test Injected"
api = "openai-completions"
env_key = "TEST_INJECTED_KEY"
auth_method = "bearer"
category = "primary"
description = "Test provider from override"
"#,
std::process::id()
)
.unwrap();
drop(f);
unsafe {
std::env::set_var("OXI_CATALOG_OVERRIDE", &path);
}
let files = crate::catalog::find_override_files();
unsafe {
std::env::remove_var("OXI_CATALOG_OVERRIDE");
}
assert!(!files.is_empty(), "OXI_CATALOG_OVERRIDE should be detected");
let (found_path, _content) = &files[0];
assert_eq!(found_path, &path);
}
#[test]
fn test_create_builtin_provider_deepseek() {
assert!(create_builtin_provider("deepseek").is_some());
}
#[test]
fn test_create_builtin_provider_minimax() {
assert!(create_builtin_provider("minimax").is_some());
}
#[test]
fn test_create_builtin_provider_minimax_cn() {
assert!(create_builtin_provider("minimax-cn").is_some());
}
#[test]
fn test_create_builtin_provider_togetherai() {
assert!(create_builtin_provider("togetherai").is_some());
}
#[test]
fn test_create_builtin_provider_openrouter() {
assert!(create_builtin_provider("openrouter").is_some());
}
#[test]
fn test_create_builtin_provider_cerebras() {
assert!(create_builtin_provider("cerebras").is_some());
}
#[test]
fn test_get_builtin_provider_openai() {
let p = get_builtin_provider("openai").unwrap();
assert_eq!(p.name, "openai");
assert_eq!(p.display_name, "OpenAI");
assert_eq!(p.api, Api::OpenAiCompletions);
assert_eq!(p.auth_method, AuthMethod::Bearer);
}
#[test]
fn test_get_builtin_provider_anthropic() {
let p = get_builtin_provider("anthropic").unwrap();
assert_eq!(p.name, "anthropic");
assert_eq!(p.auth_method, AuthMethod::XApiKey);
}
#[test]
fn test_get_builtin_provider_azure() {
let p = get_builtin_provider("azure").unwrap();
assert_eq!(p.auth_method, AuthMethod::ApiKey);
}
#[test]
fn test_get_builtin_provider_by_alias() {
let p = get_builtin_provider("amazon-bedrock").unwrap();
assert_eq!(p.name, "amazon-bedrock");
}
#[test]
fn test_get_builtin_provider_unknown() {
assert!(get_builtin_provider("unknown-provider").is_none());
}
#[test]
fn test_get_provider_env_key() {
assert_eq!(get_provider_env_key("openai"), Some("OPENAI_API_KEY"));
assert_eq!(get_provider_env_key("anthropic"), Some("ANTHROPIC_API_KEY"));
}
#[test]
fn test_get_provider_env_keys_with_extras() {
let keys = get_provider_env_keys("google");
assert!(keys.contains(&"GOOGLE_API_KEY"));
assert!(keys.contains(&"GEMINI_API_KEY"));
}
#[test]
fn test_get_provider_api() {
assert_eq!(get_provider_api("anthropic"), Some(Api::AnthropicMessages));
assert_eq!(get_provider_api("google-vertex"), Some(Api::GoogleVertex));
assert_eq!(get_provider_api("google"), Some(Api::GoogleGenerativeAi));
}
#[test]
fn test_resolve_provider_name() {
assert_eq!(
resolve_provider_name("google-vertex"),
Some("google-vertex")
);
assert_eq!(
resolve_provider_name("amazon-bedrock"),
Some("amazon-bedrock")
);
assert_eq!(resolve_provider_name("openai"), Some("openai"));
}
#[test]
fn test_is_builtin_provider() {
assert!(is_builtin_provider("openai"));
assert!(is_builtin_provider("deepseek"));
assert!(is_builtin_provider("togetherai"));
assert!(!is_builtin_provider("fake-provider"));
}
#[test]
fn test_all_providers_have_env_key() {
for p in get_builtin_providers() {
assert!(!p.env_key.is_empty(), "Provider {} has no env key", p.name);
}
}
#[test]
fn test_all_providers_have_auth_method() {
for p in get_builtin_providers() {
match p.auth_method {
AuthMethod::Bearer
| AuthMethod::XApiKey
| AuthMethod::ApiKey
| AuthMethod::None => {}
}
}
}
#[test]
fn test_get_all_provider_names() {
let names = get_all_provider_names();
assert!(names.contains(&"openai"));
assert!(names.contains(&"anthropic"));
assert!(names.contains(&"amazon-bedrock"));
assert!(names.contains(&"togetherai"));
}
#[test]
fn test_openclaw_ported_providers_present() {
let names = get_all_provider_names();
for p in [
"chutes",
"venice",
"moonshotai",
"novita-ai",
"stepfun-ai",
"alibaba",
"google-vertex-anthropic",
"synthetic",
"amazon-bedrock",
"opencode",
] {
assert!(names.contains(&p), "Missing materialized provider: {p}");
}
}
#[test]
fn test_openclaw_provider_aliases() {
assert_eq!(resolve_provider_name("chutes"), Some("chutes"));
assert_eq!(resolve_provider_name("anthropic"), Some("anthropic"));
assert_eq!(resolve_provider_name("openai"), Some("openai"));
assert_eq!(resolve_provider_name("nonexistent-provider"), None);
}
#[test]
fn test_openclaw_provider_base_urls() {
assert_eq!(
get_provider_base_url("chutes"),
Some("https://llm.chutes.ai/v1")
);
assert_eq!(get_provider_base_url("venice"), Some(""));
assert_eq!(
get_provider_base_url("synthetic"),
Some("https://api.synthetic.new/openai/v1")
);
assert_eq!(
get_provider_base_url("openrouter"),
Some("https://openrouter.ai/api/v1")
);
}
#[test]
fn test_openclaw_local_providers_use_bearer() {
let names = get_all_provider_names();
let bearer_count = names
.iter()
.filter(|n| {
get_builtin_provider(n)
.map(|p| p.auth_method == AuthMethod::Bearer)
.unwrap_or(false)
})
.count();
assert!(
bearer_count > 50,
"expected >50 Bearer providers, got {bearer_count}"
);
}
#[test]
fn test_openclaw_anthropic_compat_providers() {
let bp = get_builtin_provider("google-vertex-anthropic").unwrap();
assert_eq!(
bp.api,
Api::GoogleVertex,
"google-vertex-anthropic maps to GoogleVertex"
);
let bp = get_builtin_provider("minimax").unwrap();
assert_eq!(
bp.api,
Api::AnthropicMessages,
"minimax uses AnthropicMessages"
);
}
#[test]
fn test_create_openclaw_providers() {
for p in [
"chutes",
"venice",
"moonshotai",
"novita-ai",
"gmicloud",
"deepinfra",
"fireworks-ai",
"togetherai",
"alibaba",
"amazon-bedrock",
"opencode",
"minimax",
] {
let bp = create_builtin_provider(p);
assert!(bp.is_some(), "create_builtin_provider({p}) failed");
}
}
#[test]
fn test_get_all_provider_aliases() {
let aliases = get_all_provider_aliases();
assert!(aliases.contains(&"amazon-bedrock"));
assert!(aliases.contains(&"togetherai"));
assert!(aliases.contains(&"anthropic"));
assert!(aliases.contains(&"openai"));
}
#[test]
fn test_get_provider_base_url() {
assert_eq!(get_provider_base_url("openai"), Some(""));
assert_eq!(get_provider_base_url("anthropic"), Some(""));
assert_eq!(
get_provider_base_url("openrouter"),
Some("https://openrouter.ai/api/v1")
);
}
#[test]
fn test_minimax_base_url() {
let p = get_builtin_provider("minimax").unwrap();
assert_eq!(p.base_url, "https://api.minimax.io/anthropic/v1");
assert_eq!(p.api, Api::AnthropicMessages);
}
#[test]
fn test_openrouter_extra_headers() {
let p = get_builtin_provider("openrouter").unwrap();
assert_eq!(
p.extra_headers,
&[("HTTP-Referer", "https://oxi.dev/"), ("X-Title", "oxi")]
);
}
#[test]
fn test_cerebras_extra_headers() {
let p = get_builtin_provider("cerebras").unwrap();
assert_eq!(
p.extra_headers,
&[("X-Cerebras-3rd-Party-Integration", "opencode")]
);
}
#[test]
fn test_create_builtin_provider_with_options_openai() {
let p = create_builtin_provider_with_options(
"openai",
Some("sk-test-key"),
Some("https://my-proxy.example.com/v1"),
);
assert!(p.is_some());
}
#[test]
fn test_create_builtin_provider_with_options_anthropic() {
let p = create_builtin_provider_with_options("anthropic", Some("sk-ant-test-key"), None);
assert!(p.is_some());
}
#[test]
fn test_create_builtin_provider_with_options_no_override() {
let p = create_builtin_provider_with_options("deepseek", None, None);
assert!(p.is_some());
}
#[test]
fn test_create_builtin_provider_with_options_unknown() {
let p = create_builtin_provider_with_options("nonexistent_provider", None, None);
assert!(p.is_none());
}
fn dispatch_with_api(api: &str) -> Option<Box<dyn crate::Provider>> {
let entry = crate::catalog::BuiltinProviderEntry {
id: "test-dispatch-target".to_string(),
display_name: "Test Dispatch Target".to_string(),
aliases: vec![],
api: api.to_string(),
env_key: "TEST_DISPATCH_KEY".to_string(),
extra_env_keys: vec![],
base_url: "http://127.0.0.1:1".to_string(),
auth_method: crate::catalog::AuthMethod::Bearer,
extra_headers: vec![],
category: "test".to_string(),
description: "Synthetic builtin for dispatch-arm coverage".to_string(),
default_enabled: true,
};
let builtin: &'static BuiltinProvider = Box::leak(Box::new(BuiltinProvider::from(&entry)));
build_builtin_transport(builtin)
}
fn dispatch_with_options_api(api: &str) -> Option<Box<dyn crate::Provider>> {
let entry = crate::catalog::BuiltinProviderEntry {
id: "test-dispatch-options-target".to_string(),
display_name: "Test Dispatch Options Target".to_string(),
aliases: vec![],
api: api.to_string(),
env_key: "TEST_DISPATCH_KEY".to_string(),
extra_env_keys: vec![],
base_url: "http://127.0.0.1:1".to_string(),
auth_method: crate::catalog::AuthMethod::Bearer,
extra_headers: vec![],
category: "test".to_string(),
description: "Synthetic builtin for dispatch-arm coverage".to_string(),
default_enabled: true,
};
let builtin: &'static BuiltinProvider = Box::leak(Box::new(BuiltinProvider::from(&entry)));
build_builtin_transport_with_options(builtin, None, None)
}
#[test]
fn dispatch_openai_codex_responses_returns_some() {
let provider = dispatch_with_api("openai-codex-responses")
.expect("Api::OpenAiCodexResponses must dispatch to a transport");
let _ = provider;
}
#[test]
fn dispatch_openai_codex_responses_with_options_returns_some() {
let provider = dispatch_with_options_api("openai-codex-responses")
.expect("Api::OpenAiCodexResponses must dispatch via _with_options path");
let _ = provider;
}
#[test]
fn dispatch_google_gemini_cli_returns_some() {
let provider = dispatch_with_api("google-gemini-cli")
.expect("Api::GoogleGeminiCli must dispatch to a transport");
let _ = provider;
}
#[test]
fn dispatch_google_gemini_cli_with_options_returns_some() {
let provider = dispatch_with_options_api("google-gemini-cli")
.expect("Api::GoogleGeminiCli must dispatch via _with_options path");
let _ = provider;
}
#[test]
fn gemini_cli_transport_emits_not_implemented_error() {
use crate::{Context, Model, ProviderError, StreamOptions};
let provider: Box<dyn crate::Provider> = dispatch_with_api("google-gemini-cli")
.expect("Api::GoogleGeminiCli must dispatch to a transport");
let model = Model::new(
"gemini-cli-test",
"Gemini CLI Test Model",
Api::GoogleGeminiCli,
"google-gemini-cli",
"http://127.0.0.1:1",
);
let context = Context::new();
let options = StreamOptions::default();
let result = futures::executor::block_on(provider.stream(&model, &context, Some(options)));
match result {
Err(ProviderError::NotImplemented(name)) => {
assert!(
name.to_lowercase().contains("gemini"),
"NotImplemented error name should mention gemini, got: {name}"
);
}
Ok(_) => panic!(
"Gemini CLI transport must NOT fake success — stream() must return NotImplemented"
),
Err(other) => panic!("Gemini CLI stream must return NotImplemented, got: {other:?}"),
}
}
}