Skip to content

Commit 5dff360

Browse files
committed
refactor(cli): move Provider enum from core to cli crate
1 parent 49742ca commit 5dff360

9 files changed

Lines changed: 86 additions & 201 deletions

File tree

crates/rullm-cli/src/aliases.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1+
use super::provider::Provider;
12
use rullm_core::error::LlmError;
2-
use rullm_core::providers::Provider;
33
use serde::{Deserialize, Serialize};
44
use std::collections::HashMap;
55
use std::path::Path;

crates/rullm-cli/src/api_keys.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
use rullm_core::{Provider, error::LlmError};
1+
use crate::provider::Provider;
2+
use rullm_core::error::LlmError;
23
use serde::{Deserialize, Serialize};
34
use std::path::Path;
45

crates/rullm-cli/src/client.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1+
use super::provider::Provider;
12
use crate::api_keys::ApiKeys;
23
use crate::args::{Cli, CliConfig};
34
use crate::constants;
45
use anyhow::{Context, Result};
5-
use rullm_core::Provider;
66

77
use rullm_core::simple::{SimpleLlmBuilder, SimpleLlmClient, SimpleLlmConfig};
88
use rullm_core::{AnthropicConfig, GoogleAiConfig, LlmError, OpenAIConfig};

crates/rullm-cli/src/commands/keys.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@ use anyhow::Result;
22
use clap::{Args, Subcommand};
33
use strum::IntoEnumIterator;
44

5+
use crate::provider::Provider;
56
use crate::{
67
api_keys::ApiKeys,
78
args::{Cli, CliConfig},
89
output::OutputLevel,
910
};
10-
use rullm_core::Provider;
1111

1212
#[derive(Args)]
1313
pub struct KeysArgs {

crates/rullm-cli/src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ mod commands;
99
mod config;
1010
mod constants;
1111
mod output;
12+
mod provider;
1213

1314
use anyhow::Result;
1415
use args::{Cli, CliConfig};

crates/rullm-cli/src/provider.rs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
use clap::{ValueEnum, builder::PossibleValue};
2+
use strum::IntoEnumIterator;
3+
use strum_macros::EnumIter;
4+
5+
#[derive(Clone, Debug, PartialEq, Eq, EnumIter)]
6+
pub enum Provider {
7+
OpenAI,
8+
Anthropic,
9+
Google,
10+
}
11+
12+
impl std::fmt::Display for Provider {
13+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
14+
let name = match self {
15+
Provider::OpenAI => "openai",
16+
Provider::Anthropic => "anthropic",
17+
Provider::Google => "google",
18+
};
19+
write!(f, "{name}")
20+
}
21+
}
22+
23+
impl ValueEnum for Provider {
24+
fn value_variants<'a>() -> &'a [Self] {
25+
&[Self::OpenAI, Self::Anthropic, Self::Google]
26+
}
27+
28+
fn to_possible_value(&self) -> Option<PossibleValue> {
29+
let value = match self {
30+
Self::OpenAI => PossibleValue::new("openai"),
31+
Self::Anthropic => PossibleValue::new("anthropic"),
32+
Self::Google => PossibleValue::new("google"),
33+
};
34+
Some(value)
35+
}
36+
}
37+
38+
impl Provider {
39+
pub fn aliases(&self) -> &'static [&'static str] {
40+
match self {
41+
Provider::OpenAI => &["openai", "gpt"],
42+
Provider::Anthropic => &["anthropic", "claude"],
43+
Provider::Google => &["google", "gemini"],
44+
}
45+
}
46+
47+
pub fn from_alias(alias: &str) -> Option<Provider> {
48+
let candidate = alias.to_ascii_lowercase();
49+
Provider::iter().find(|p| p.aliases().contains(&candidate.as_str()))
50+
}
51+
52+
#[allow(dead_code)]
53+
pub fn from_model(input: &str) -> Result<(Provider, String), anyhow::Error> {
54+
if let Some((provider_str, model)) = input.split_once('/') {
55+
let provider = Provider::from_alias(provider_str)
56+
.ok_or_else(|| anyhow::anyhow!(format!("Unsupported provider: {provider_str}")))?;
57+
58+
if model.is_empty() {
59+
return Err(anyhow::anyhow!("Model name cannot be empty"));
60+
}
61+
62+
Ok((provider, model.to_string()))
63+
} else {
64+
Err(anyhow::anyhow!(format!(
65+
"Invalid model format '{input}'. Expected 'provider/model'"
66+
)))
67+
}
68+
}
69+
70+
pub fn env_key(&self) -> &'static str {
71+
match self {
72+
Provider::OpenAI => "OPENAI_API_KEY",
73+
Provider::Anthropic => "ANTHROPIC_API_KEY",
74+
Provider::Google => "GOOGLE_AI_API_KEY",
75+
}
76+
}
77+
}

crates/rullm-core/src/lib.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,6 @@ mod tests;
156156
pub use config::{AnthropicConfig, ConfigBuilder, GoogleAiConfig, OpenAIConfig, ProviderConfig};
157157
pub use error::LlmError;
158158
pub use middleware::{LlmServiceBuilder, MiddlewareConfig, MiddlewareStack, RateLimit};
159-
pub use providers::Provider;
160159
pub use providers::{AnthropicProvider, GoogleProvider, OpenAIProvider};
161160
pub use simple::{DefaultModels, SimpleLlm, SimpleLlmBuilder, SimpleLlmClient, SimpleLlmConfig};
162161
pub use types::{
Lines changed: 3 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -1,89 +1,7 @@
1-
// Re-export all providers
2-
pub mod anthropic;
3-
pub mod google;
4-
pub mod openai;
1+
mod anthropic;
2+
mod google;
3+
mod openai;
54

65
pub use anthropic::AnthropicProvider;
76
pub use google::GoogleProvider;
87
pub use openai::OpenAIProvider;
9-
10-
use clap::{ValueEnum, builder::PossibleValue};
11-
use strum::IntoEnumIterator;
12-
use strum_macros::EnumIter;
13-
14-
/// Enum of supported providers usable throughout the library and by downstream
15-
/// crates (e.g. `rullm-cli`). It is placed in the core crate so both the
16-
/// library and any front-end binaries share a single source of truth.
17-
#[derive(Clone, Debug, PartialEq, Eq, EnumIter)]
18-
pub enum Provider {
19-
OpenAI,
20-
Anthropic,
21-
Google,
22-
}
23-
24-
impl std::fmt::Display for Provider {
25-
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26-
let name = match self {
27-
Provider::OpenAI => "openai",
28-
Provider::Anthropic => "anthropic",
29-
Provider::Google => "google",
30-
};
31-
write!(f, "{name}")
32-
}
33-
}
34-
35-
impl ValueEnum for Provider {
36-
fn value_variants<'a>() -> &'a [Self] {
37-
&[Self::OpenAI, Self::Anthropic, Self::Google]
38-
}
39-
40-
fn to_possible_value(&self) -> Option<PossibleValue> {
41-
let value = match self {
42-
Self::OpenAI => PossibleValue::new("openai"),
43-
Self::Anthropic => PossibleValue::new("anthropic"),
44-
Self::Google => PossibleValue::new("google"),
45-
};
46-
Some(value)
47-
}
48-
}
49-
50-
impl Provider {
51-
// this part should be moved to cli
52-
pub fn aliases(&self) -> &'static [&'static str] {
53-
match self {
54-
Provider::OpenAI => &["openai", "gpt"],
55-
Provider::Anthropic => &["anthropic", "claude"],
56-
Provider::Google => &["google", "gemini"],
57-
}
58-
}
59-
60-
// this part should be moved to cli
61-
/// Attempt to resolve a provider from a string identifier (case-insensitive).
62-
pub fn from_alias(alias: &str) -> Option<Provider> {
63-
let candidate = alias.to_ascii_lowercase();
64-
Provider::iter().find(|p| p.aliases().contains(&candidate.as_str()))
65-
}
66-
67-
// this part should be moved to cli
68-
/// Parse a model identifier in explicit provider/model format and return the provider
69-
/// along with the model name (e.g. "openai/gpt-4" → (OpenAI, "gpt-4")).
70-
pub fn from_model(input: &str) -> Result<(Provider, String), crate::error::LlmError> {
71-
if let Some((provider_str, model)) = input.split_once('/') {
72-
let provider = Provider::from_alias(provider_str).ok_or_else(|| {
73-
crate::error::LlmError::validation(format!("Unsupported provider: {provider_str}"))
74-
})?;
75-
76-
if model.is_empty() {
77-
return Err(crate::error::LlmError::validation(
78-
"Model name cannot be empty".to_string(),
79-
));
80-
}
81-
82-
Ok((provider, model.to_string()))
83-
} else {
84-
Err(crate::error::LlmError::validation(format!(
85-
"Invalid model format '{input}'. Expected 'provider/model'"
86-
)))
87-
}
88-
}
89-
}

crates/rullm-core/src/tests.rs

Lines changed: 0 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -1386,114 +1386,3 @@ async fn test_middleware_performance_timing() {
13861386
// The call should complete relatively quickly for a mock provider
13871387
assert!(duration < Duration::from_secs(1));
13881388
}
1389-
1390-
#[cfg(test)]
1391-
mod provider_from_model_tests {
1392-
use crate::providers::Provider;
1393-
1394-
#[test]
1395-
fn test_explicit_provider_prefixes() {
1396-
// Valid explicit prefixes
1397-
assert_eq!(
1398-
Provider::from_model("openai/gpt-4").unwrap(),
1399-
(Provider::OpenAI, "gpt-4".to_string())
1400-
);
1401-
assert_eq!(
1402-
Provider::from_model("anthropic/claude-3-sonnet").unwrap(),
1403-
(Provider::Anthropic, "claude-3-sonnet".to_string())
1404-
);
1405-
assert_eq!(
1406-
Provider::from_model("google/gemini-1.5-pro").unwrap(),
1407-
(Provider::Google, "gemini-1.5-pro".to_string())
1408-
);
1409-
1410-
// Test aliases as prefixes
1411-
assert_eq!(
1412-
Provider::from_model("gpt/gpt-4o-mini").unwrap(),
1413-
(Provider::OpenAI, "gpt-4o-mini".to_string())
1414-
);
1415-
assert_eq!(
1416-
Provider::from_model("claude/claude-3-haiku").unwrap(),
1417-
(Provider::Anthropic, "claude-3-haiku".to_string())
1418-
);
1419-
assert_eq!(
1420-
Provider::from_model("gemini/gemini-1.5-flash").unwrap(),
1421-
(Provider::Google, "gemini-1.5-flash".to_string())
1422-
);
1423-
}
1424-
1425-
#[test]
1426-
fn test_model_prefix_inference() {
1427-
// Core library now only supports explicit provider/model format
1428-
// Patterns without provider prefix should fail
1429-
assert!(Provider::from_model("gpt-4o-mini").is_err());
1430-
assert!(Provider::from_model("gpt-3.5-turbo").is_err());
1431-
assert!(Provider::from_model("gpt-4").is_err());
1432-
assert!(Provider::from_model("claude-3-5-sonnet-20241022").is_err());
1433-
assert!(Provider::from_model("claude-3-sonnet").is_err());
1434-
assert!(Provider::from_model("gemini-1.5-pro").is_err());
1435-
assert!(Provider::from_model("gemini-1.5-flash").is_err());
1436-
}
1437-
1438-
#[test]
1439-
fn test_unrecognized_model_patterns() {
1440-
// These should fail since they don't match our alias patterns or built-in aliases
1441-
// Note: o1-preview now succeeds due to built-in alias, so we test other patterns
1442-
assert!(Provider::from_model("text-davinci-003").is_err());
1443-
assert!(Provider::from_model("models/gemini-pro").is_err());
1444-
assert!(Provider::from_model("random-model-name").is_err());
1445-
assert!(Provider::from_model("unknown-prefix-model").is_err());
1446-
}
1447-
1448-
#[test]
1449-
fn test_built_in_aliases() {
1450-
// Core library no longer supports built-in aliases - they are CLI-specific
1451-
// All single-word inputs should fail as they need explicit provider/model format
1452-
assert!(Provider::from_model("gpt4").is_err());
1453-
assert!(Provider::from_model("chatgpt").is_err());
1454-
assert!(Provider::from_model("claude").is_err());
1455-
assert!(Provider::from_model("sonnet").is_err());
1456-
assert!(Provider::from_model("gemini").is_err());
1457-
assert!(Provider::from_model("o1-preview").is_err());
1458-
}
1459-
1460-
#[test]
1461-
fn test_case_insensitive_aliases() {
1462-
// Case insensitive provider prefix matching still works
1463-
assert_eq!(
1464-
Provider::from_model("OpenAI/gpt-4").unwrap(),
1465-
(Provider::OpenAI, "gpt-4".to_string())
1466-
);
1467-
assert_eq!(
1468-
Provider::from_model("ANTHROPIC/claude-3-sonnet").unwrap(),
1469-
(Provider::Anthropic, "claude-3-sonnet".to_string())
1470-
);
1471-
assert_eq!(
1472-
Provider::from_model("Google/gemini-1.5-pro").unwrap(),
1473-
(Provider::Google, "gemini-1.5-pro".to_string())
1474-
);
1475-
1476-
// Single-word aliases are no longer supported in core
1477-
assert!(Provider::from_model("GPT4").is_err());
1478-
assert!(Provider::from_model("CLAUDE").is_err());
1479-
}
1480-
1481-
#[test]
1482-
fn test_error_cases() {
1483-
// Unknown provider prefix
1484-
assert!(Provider::from_model("unknown/some-model").is_err());
1485-
assert!(Provider::from_model("invalid-provider/model").is_err());
1486-
assert!(Provider::from_model("").is_err());
1487-
}
1488-
1489-
#[test]
1490-
fn test_provider_names_should_error() {
1491-
// All provider names should error in core since they don't use provider/model format
1492-
assert!(Provider::from_model("openai").is_err());
1493-
assert!(Provider::from_model("gpt").is_err());
1494-
assert!(Provider::from_model("anthropic").is_err());
1495-
assert!(Provider::from_model("google").is_err());
1496-
assert!(Provider::from_model("claude").is_err());
1497-
assert!(Provider::from_model("gemini").is_err());
1498-
}
1499-
}

0 commit comments

Comments
 (0)