mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-04-06 21:08:30 +08:00
Split the 1446-line services/provider.rs into 5 focused modules: - gemini_auth.rs (250 lines): Gemini authentication type detection - PackyCode, Google OAuth, and generic provider detection - Security flag management for different auth types - live.rs (300 lines): Live configuration operations - LiveSnapshot backup/restore - Reading and writing live config files - Sync current provider to live config - usage.rs (150 lines): Usage script execution - Query and test usage scripts - Format usage results - Validate usage script configuration - endpoints.rs (80 lines): Custom endpoints management - CRUD operations for provider custom endpoints - Last-used timestamp tracking - mod.rs (650 lines): Core provider CRUD and service facade - ProviderService struct with all public methods - Provider add/update/delete/switch operations - Claude model key normalization - Credential extraction and validation All 107 tests pass. This improves maintainability by: - Separating concerns into cohesive modules - Making Gemini-specific logic easier to find and modify - Reducing cognitive load when working on specific features
648 lines
22 KiB
Rust
648 lines
22 KiB
Rust
//! Provider service module
|
|
//!
|
|
//! Handles provider CRUD operations, switching, and configuration management.
|
|
|
|
mod endpoints;
|
|
mod gemini_auth;
|
|
mod live;
|
|
mod usage;
|
|
|
|
use indexmap::IndexMap;
|
|
use regex::Regex;
|
|
use serde::Deserialize;
|
|
use serde_json::Value;
|
|
|
|
use crate::app_config::AppType;
|
|
use crate::codex_config::write_codex_live_atomic;
|
|
use crate::config::{get_claude_settings_path, write_json_file};
|
|
use crate::error::AppError;
|
|
use crate::provider::{Provider, UsageResult};
|
|
use crate::services::mcp::McpService;
|
|
use crate::settings::CustomEndpoint;
|
|
use crate::store::AppState;
|
|
|
|
// Re-export sub-module functions for external access
|
|
pub use live::{import_default_config, read_live_settings, sync_current_from_db};
|
|
|
|
// Internal re-exports (pub(crate))
|
|
pub(crate) use live::write_live_snapshot;
|
|
|
|
// Internal re-exports
|
|
use gemini_auth::{
|
|
detect_gemini_auth_type, ensure_google_oauth_security_flag, ensure_packycode_security_flag,
|
|
GeminiAuthType,
|
|
};
|
|
use live::write_gemini_live;
|
|
use usage::validate_usage_script;
|
|
|
|
/// Provider business logic service
|
|
pub struct ProviderService;
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use serde_json::json;
|
|
|
|
#[test]
|
|
fn validate_provider_settings_rejects_missing_auth() {
|
|
let provider = Provider::with_id(
|
|
"codex".into(),
|
|
"Codex".into(),
|
|
json!({ "config": "base_url = \"https://example.com\"" }),
|
|
None,
|
|
);
|
|
let err = ProviderService::validate_provider_settings(&AppType::Codex, &provider)
|
|
.expect_err("missing auth should be rejected");
|
|
assert!(
|
|
err.to_string().contains("auth"),
|
|
"expected auth error, got {err:?}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn extract_credentials_returns_expected_values() {
|
|
let provider = Provider::with_id(
|
|
"claude".into(),
|
|
"Claude".into(),
|
|
json!({
|
|
"env": {
|
|
"ANTHROPIC_AUTH_TOKEN": "token",
|
|
"ANTHROPIC_BASE_URL": "https://claude.example"
|
|
}
|
|
}),
|
|
None,
|
|
);
|
|
let (api_key, base_url) =
|
|
ProviderService::extract_credentials(&provider, &AppType::Claude).unwrap();
|
|
assert_eq!(api_key, "token");
|
|
assert_eq!(base_url, "https://claude.example");
|
|
}
|
|
}
|
|
|
|
impl ProviderService {
|
|
fn normalize_provider_if_claude(app_type: &AppType, provider: &mut Provider) {
|
|
if matches!(app_type, AppType::Claude) {
|
|
let mut v = provider.settings_config.clone();
|
|
if normalize_claude_models_in_value(&mut v) {
|
|
provider.settings_config = v;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// List all providers for an app type
|
|
pub fn list(
|
|
state: &AppState,
|
|
app_type: AppType,
|
|
) -> Result<IndexMap<String, Provider>, AppError> {
|
|
state.db.get_all_providers(app_type.as_str())
|
|
}
|
|
|
|
/// Get current provider ID
|
|
pub fn current(state: &AppState, app_type: AppType) -> Result<String, AppError> {
|
|
state
|
|
.db
|
|
.get_current_provider(app_type.as_str())
|
|
.map(|opt| opt.unwrap_or_default())
|
|
}
|
|
|
|
/// Add a new provider
|
|
pub fn add(state: &AppState, app_type: AppType, provider: Provider) -> Result<bool, AppError> {
|
|
let mut provider = provider;
|
|
// Normalize Claude model keys
|
|
Self::normalize_provider_if_claude(&app_type, &mut provider);
|
|
Self::validate_provider_settings(&app_type, &provider)?;
|
|
|
|
// Save to database
|
|
state.db.save_provider(app_type.as_str(), &provider)?;
|
|
|
|
// Check if sync is needed (if this is current provider, or no current provider)
|
|
let current = state.db.get_current_provider(app_type.as_str())?;
|
|
if current.is_none() {
|
|
// No current provider, set as current and sync
|
|
state
|
|
.db
|
|
.set_current_provider(app_type.as_str(), &provider.id)?;
|
|
write_live_snapshot(&app_type, &provider)?;
|
|
}
|
|
|
|
Ok(true)
|
|
}
|
|
|
|
/// Update a provider
|
|
pub fn update(
|
|
state: &AppState,
|
|
app_type: AppType,
|
|
provider: Provider,
|
|
) -> Result<bool, AppError> {
|
|
let mut provider = provider;
|
|
// Normalize Claude model keys
|
|
Self::normalize_provider_if_claude(&app_type, &mut provider);
|
|
Self::validate_provider_settings(&app_type, &provider)?;
|
|
|
|
// Check if this is current provider
|
|
let current_id = state.db.get_current_provider(app_type.as_str())?;
|
|
let is_current = current_id.as_deref() == Some(provider.id.as_str());
|
|
|
|
// Save to database
|
|
state.db.save_provider(app_type.as_str(), &provider)?;
|
|
|
|
if is_current {
|
|
write_live_snapshot(&app_type, &provider)?;
|
|
// Sync MCP
|
|
McpService::sync_all_enabled(state)?;
|
|
}
|
|
|
|
Ok(true)
|
|
}
|
|
|
|
/// Delete a provider
|
|
pub fn delete(state: &AppState, app_type: AppType, id: &str) -> Result<(), AppError> {
|
|
let current = state.db.get_current_provider(app_type.as_str())?;
|
|
if current.as_deref() == Some(id) {
|
|
return Err(AppError::Message(
|
|
"无法删除当前正在使用的供应商".to_string(),
|
|
));
|
|
}
|
|
state.db.delete_provider(app_type.as_str(), id)
|
|
}
|
|
|
|
/// Switch to a provider
|
|
///
|
|
/// Switch flow:
|
|
/// 1. Validate target provider exists
|
|
/// 2. **Backfill mechanism**: Backfill current live config to current provider, protect user manual modifications
|
|
/// 3. Set new current provider
|
|
/// 4. Write target provider config to live files
|
|
/// 5. Gemini additional security flag handling
|
|
/// 6. Sync MCP configuration
|
|
pub fn switch(state: &AppState, app_type: AppType, id: &str) -> Result<(), AppError> {
|
|
// Check if provider exists
|
|
let providers = state.db.get_all_providers(app_type.as_str())?;
|
|
let provider = providers
|
|
.get(id)
|
|
.ok_or_else(|| AppError::Message(format!("供应商 {id} 不存在")))?;
|
|
|
|
// Backfill: Backfill current live config to current provider
|
|
if let Some(current_id) = state.db.get_current_provider(app_type.as_str())? {
|
|
if current_id != id {
|
|
// Only backfill when switching to a different provider
|
|
if let Ok(live_config) = read_live_settings(app_type.clone()) {
|
|
if let Some(mut current_provider) = providers.get(¤t_id).cloned() {
|
|
current_provider.settings_config = live_config;
|
|
// Ignore backfill failure, don't affect switch flow
|
|
let _ = state.db.save_provider(app_type.as_str(), ¤t_provider);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Set current
|
|
state.db.set_current_provider(app_type.as_str(), id)?;
|
|
|
|
// Sync to live
|
|
write_live_snapshot(&app_type, provider)?;
|
|
|
|
// Gemini needs additional security flag handling (PackyCode or Google OAuth)
|
|
if matches!(app_type, AppType::Gemini) {
|
|
let auth_type = detect_gemini_auth_type(provider);
|
|
match auth_type {
|
|
GeminiAuthType::GoogleOfficial => ensure_google_oauth_security_flag(provider)?,
|
|
GeminiAuthType::Packycode => ensure_packycode_security_flag(provider)?,
|
|
GeminiAuthType::Generic => {}
|
|
}
|
|
}
|
|
|
|
// Sync MCP
|
|
McpService::sync_all_enabled(state)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Sync current provider from database to live configuration (re-export)
|
|
pub fn sync_current_from_db(state: &AppState) -> Result<(), AppError> {
|
|
sync_current_from_db(state)
|
|
}
|
|
|
|
/// Import default configuration from live files (re-export)
|
|
pub fn import_default_config(state: &AppState, app_type: AppType) -> Result<(), AppError> {
|
|
import_default_config(state, app_type)
|
|
}
|
|
|
|
/// Read current live settings (re-export)
|
|
pub fn read_live_settings(app_type: AppType) -> Result<Value, AppError> {
|
|
read_live_settings(app_type)
|
|
}
|
|
|
|
/// Get custom endpoints list (re-export)
|
|
pub fn get_custom_endpoints(
|
|
state: &AppState,
|
|
app_type: AppType,
|
|
provider_id: &str,
|
|
) -> Result<Vec<CustomEndpoint>, AppError> {
|
|
endpoints::get_custom_endpoints(state, app_type, provider_id)
|
|
}
|
|
|
|
/// Add custom endpoint (re-export)
|
|
pub fn add_custom_endpoint(
|
|
state: &AppState,
|
|
app_type: AppType,
|
|
provider_id: &str,
|
|
url: String,
|
|
) -> Result<(), AppError> {
|
|
endpoints::add_custom_endpoint(state, app_type, provider_id, url)
|
|
}
|
|
|
|
/// Remove custom endpoint (re-export)
|
|
pub fn remove_custom_endpoint(
|
|
state: &AppState,
|
|
app_type: AppType,
|
|
provider_id: &str,
|
|
url: String,
|
|
) -> Result<(), AppError> {
|
|
endpoints::remove_custom_endpoint(state, app_type, provider_id, url)
|
|
}
|
|
|
|
/// Update endpoint last used timestamp (re-export)
|
|
pub fn update_endpoint_last_used(
|
|
state: &AppState,
|
|
app_type: AppType,
|
|
provider_id: &str,
|
|
url: String,
|
|
) -> Result<(), AppError> {
|
|
endpoints::update_endpoint_last_used(state, app_type, provider_id, url)
|
|
}
|
|
|
|
/// Update provider sort order
|
|
pub fn update_sort_order(
|
|
state: &AppState,
|
|
app_type: AppType,
|
|
updates: Vec<ProviderSortUpdate>,
|
|
) -> Result<bool, AppError> {
|
|
let mut providers = state.db.get_all_providers(app_type.as_str())?;
|
|
|
|
for update in updates {
|
|
if let Some(provider) = providers.get_mut(&update.id) {
|
|
provider.sort_index = Some(update.sort_index);
|
|
state.db.save_provider(app_type.as_str(), provider)?;
|
|
}
|
|
}
|
|
|
|
Ok(true)
|
|
}
|
|
|
|
/// Query provider usage (re-export)
|
|
pub async fn query_usage(
|
|
state: &AppState,
|
|
app_type: AppType,
|
|
provider_id: &str,
|
|
) -> Result<UsageResult, AppError> {
|
|
usage::query_usage(state, app_type, provider_id).await
|
|
}
|
|
|
|
/// Test usage script (re-export)
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub async fn test_usage_script(
|
|
state: &AppState,
|
|
app_type: AppType,
|
|
provider_id: &str,
|
|
script_code: &str,
|
|
timeout: u64,
|
|
api_key: Option<&str>,
|
|
base_url: Option<&str>,
|
|
access_token: Option<&str>,
|
|
user_id: Option<&str>,
|
|
) -> Result<UsageResult, AppError> {
|
|
usage::test_usage_script(
|
|
state,
|
|
app_type,
|
|
provider_id,
|
|
script_code,
|
|
timeout,
|
|
api_key,
|
|
base_url,
|
|
access_token,
|
|
user_id,
|
|
)
|
|
.await
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
fn write_codex_live(provider: &Provider) -> Result<(), AppError> {
|
|
let settings = provider
|
|
.settings_config
|
|
.as_object()
|
|
.ok_or_else(|| AppError::Config("Codex 配置必须是 JSON 对象".into()))?;
|
|
let auth = settings
|
|
.get("auth")
|
|
.ok_or_else(|| AppError::Config(format!("供应商 {} 缺少 auth 配置", provider.id)))?;
|
|
if !auth.is_object() {
|
|
return Err(AppError::Config(format!(
|
|
"供应商 {} 的 auth 必须是对象",
|
|
provider.id
|
|
)));
|
|
}
|
|
let cfg_text = settings.get("config").and_then(Value::as_str);
|
|
|
|
write_codex_live_atomic(auth, cfg_text)?;
|
|
Ok(())
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
fn write_claude_live(provider: &Provider) -> Result<(), AppError> {
|
|
let settings_path = get_claude_settings_path();
|
|
let mut content = provider.settings_config.clone();
|
|
let _ = normalize_claude_models_in_value(&mut content);
|
|
write_json_file(&settings_path, &content)?;
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn write_gemini_live(provider: &Provider) -> Result<(), AppError> {
|
|
write_gemini_live(provider)
|
|
}
|
|
|
|
fn validate_provider_settings(app_type: &AppType, provider: &Provider) -> Result<(), AppError> {
|
|
match app_type {
|
|
AppType::Claude => {
|
|
if !provider.settings_config.is_object() {
|
|
return Err(AppError::localized(
|
|
"provider.claude.settings.not_object",
|
|
"Claude 配置必须是 JSON 对象",
|
|
"Claude configuration must be a JSON object",
|
|
));
|
|
}
|
|
}
|
|
AppType::Codex => {
|
|
let settings = provider.settings_config.as_object().ok_or_else(|| {
|
|
AppError::localized(
|
|
"provider.codex.settings.not_object",
|
|
"Codex 配置必须是 JSON 对象",
|
|
"Codex configuration must be a JSON object",
|
|
)
|
|
})?;
|
|
|
|
let auth = settings.get("auth").ok_or_else(|| {
|
|
AppError::localized(
|
|
"provider.codex.auth.missing",
|
|
format!("供应商 {} 缺少 auth 配置", provider.id),
|
|
format!("Provider {} is missing auth configuration", provider.id),
|
|
)
|
|
})?;
|
|
if !auth.is_object() {
|
|
return Err(AppError::localized(
|
|
"provider.codex.auth.not_object",
|
|
format!("供应商 {} 的 auth 配置必须是 JSON 对象", provider.id),
|
|
format!(
|
|
"Provider {} auth configuration must be a JSON object",
|
|
provider.id
|
|
),
|
|
));
|
|
}
|
|
|
|
if let Some(config_value) = settings.get("config") {
|
|
if !(config_value.is_string() || config_value.is_null()) {
|
|
return Err(AppError::localized(
|
|
"provider.codex.config.invalid_type",
|
|
"Codex config 字段必须是字符串",
|
|
"Codex config field must be a string",
|
|
));
|
|
}
|
|
if let Some(cfg_text) = config_value.as_str() {
|
|
crate::codex_config::validate_config_toml(cfg_text)?;
|
|
}
|
|
}
|
|
}
|
|
AppType::Gemini => {
|
|
use crate::gemini_config::validate_gemini_settings;
|
|
validate_gemini_settings(&provider.settings_config)?
|
|
}
|
|
}
|
|
|
|
// Validate and clean UsageScript configuration (common for all app types)
|
|
if let Some(meta) = &provider.meta {
|
|
if let Some(usage_script) = &meta.usage_script {
|
|
validate_usage_script(usage_script)?;
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
fn extract_credentials(
|
|
provider: &Provider,
|
|
app_type: &AppType,
|
|
) -> Result<(String, String), AppError> {
|
|
match app_type {
|
|
AppType::Claude => {
|
|
let env = provider
|
|
.settings_config
|
|
.get("env")
|
|
.and_then(|v| v.as_object())
|
|
.ok_or_else(|| {
|
|
AppError::localized(
|
|
"provider.claude.env.missing",
|
|
"配置格式错误: 缺少 env",
|
|
"Invalid configuration: missing env section",
|
|
)
|
|
})?;
|
|
|
|
let api_key = env
|
|
.get("ANTHROPIC_AUTH_TOKEN")
|
|
.or_else(|| env.get("ANTHROPIC_API_KEY"))
|
|
.and_then(|v| v.as_str())
|
|
.ok_or_else(|| {
|
|
AppError::localized(
|
|
"provider.claude.api_key.missing",
|
|
"缺少 API Key",
|
|
"API key is missing",
|
|
)
|
|
})?
|
|
.to_string();
|
|
|
|
let base_url = env
|
|
.get("ANTHROPIC_BASE_URL")
|
|
.and_then(|v| v.as_str())
|
|
.ok_or_else(|| {
|
|
AppError::localized(
|
|
"provider.claude.base_url.missing",
|
|
"缺少 ANTHROPIC_BASE_URL 配置",
|
|
"Missing ANTHROPIC_BASE_URL configuration",
|
|
)
|
|
})?
|
|
.to_string();
|
|
|
|
Ok((api_key, base_url))
|
|
}
|
|
AppType::Codex => {
|
|
let auth = provider
|
|
.settings_config
|
|
.get("auth")
|
|
.and_then(|v| v.as_object())
|
|
.ok_or_else(|| {
|
|
AppError::localized(
|
|
"provider.codex.auth.missing",
|
|
"配置格式错误: 缺少 auth",
|
|
"Invalid configuration: missing auth section",
|
|
)
|
|
})?;
|
|
|
|
let api_key = auth
|
|
.get("OPENAI_API_KEY")
|
|
.and_then(|v| v.as_str())
|
|
.ok_or_else(|| {
|
|
AppError::localized(
|
|
"provider.codex.api_key.missing",
|
|
"缺少 API Key",
|
|
"API key is missing",
|
|
)
|
|
})?
|
|
.to_string();
|
|
|
|
let config_toml = provider
|
|
.settings_config
|
|
.get("config")
|
|
.and_then(|v| v.as_str())
|
|
.unwrap_or("");
|
|
|
|
let base_url = if config_toml.contains("base_url") {
|
|
let re = Regex::new(r#"base_url\s*=\s*["']([^"']+)["']"#).map_err(|e| {
|
|
AppError::localized(
|
|
"provider.regex_init_failed",
|
|
format!("正则初始化失败: {e}"),
|
|
format!("Failed to initialize regex: {e}"),
|
|
)
|
|
})?;
|
|
re.captures(config_toml)
|
|
.and_then(|caps| caps.get(1))
|
|
.map(|m| m.as_str().to_string())
|
|
.ok_or_else(|| {
|
|
AppError::localized(
|
|
"provider.codex.base_url.invalid",
|
|
"config.toml 中 base_url 格式错误",
|
|
"base_url in config.toml has invalid format",
|
|
)
|
|
})?
|
|
} else {
|
|
return Err(AppError::localized(
|
|
"provider.codex.base_url.missing",
|
|
"config.toml 中缺少 base_url 配置",
|
|
"base_url is missing from config.toml",
|
|
));
|
|
};
|
|
|
|
Ok((api_key, base_url))
|
|
}
|
|
AppType::Gemini => {
|
|
use crate::gemini_config::json_to_env;
|
|
|
|
let env_map = json_to_env(&provider.settings_config)?;
|
|
|
|
let api_key = env_map.get("GEMINI_API_KEY").cloned().ok_or_else(|| {
|
|
AppError::localized(
|
|
"gemini.missing_api_key",
|
|
"缺少 GEMINI_API_KEY",
|
|
"Missing GEMINI_API_KEY",
|
|
)
|
|
})?;
|
|
|
|
let base_url = env_map
|
|
.get("GOOGLE_GEMINI_BASE_URL")
|
|
.cloned()
|
|
.unwrap_or_else(|| "https://generativelanguage.googleapis.com".to_string());
|
|
|
|
Ok((api_key, base_url))
|
|
}
|
|
}
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
fn app_not_found(app_type: &AppType) -> AppError {
|
|
AppError::localized(
|
|
"provider.app_not_found",
|
|
format!("应用类型不存在: {app_type:?}"),
|
|
format!("App type not found: {app_type:?}"),
|
|
)
|
|
}
|
|
}
|
|
|
|
/// Normalize Claude model keys in a JSON value
|
|
///
|
|
/// Reads old key (ANTHROPIC_SMALL_FAST_MODEL), writes new keys (DEFAULT_*), and deletes old key.
|
|
pub(crate) fn normalize_claude_models_in_value(settings: &mut Value) -> bool {
|
|
let mut changed = false;
|
|
let env = match settings.get_mut("env").and_then(|v| v.as_object_mut()) {
|
|
Some(obj) => obj,
|
|
None => return changed,
|
|
};
|
|
|
|
let model = env
|
|
.get("ANTHROPIC_MODEL")
|
|
.and_then(|v| v.as_str())
|
|
.map(|s| s.to_string());
|
|
let small_fast = env
|
|
.get("ANTHROPIC_SMALL_FAST_MODEL")
|
|
.and_then(|v| v.as_str())
|
|
.map(|s| s.to_string());
|
|
|
|
let current_haiku = env
|
|
.get("ANTHROPIC_DEFAULT_HAIKU_MODEL")
|
|
.and_then(|v| v.as_str())
|
|
.map(|s| s.to_string());
|
|
let current_sonnet = env
|
|
.get("ANTHROPIC_DEFAULT_SONNET_MODEL")
|
|
.and_then(|v| v.as_str())
|
|
.map(|s| s.to_string());
|
|
let current_opus = env
|
|
.get("ANTHROPIC_DEFAULT_OPUS_MODEL")
|
|
.and_then(|v| v.as_str())
|
|
.map(|s| s.to_string());
|
|
|
|
let target_haiku = current_haiku
|
|
.or_else(|| small_fast.clone())
|
|
.or_else(|| model.clone());
|
|
let target_sonnet = current_sonnet
|
|
.or_else(|| model.clone())
|
|
.or_else(|| small_fast.clone());
|
|
let target_opus = current_opus
|
|
.or_else(|| model.clone())
|
|
.or_else(|| small_fast.clone());
|
|
|
|
if env.get("ANTHROPIC_DEFAULT_HAIKU_MODEL").is_none() {
|
|
if let Some(v) = target_haiku {
|
|
env.insert(
|
|
"ANTHROPIC_DEFAULT_HAIKU_MODEL".to_string(),
|
|
Value::String(v),
|
|
);
|
|
changed = true;
|
|
}
|
|
}
|
|
if env.get("ANTHROPIC_DEFAULT_SONNET_MODEL").is_none() {
|
|
if let Some(v) = target_sonnet {
|
|
env.insert(
|
|
"ANTHROPIC_DEFAULT_SONNET_MODEL".to_string(),
|
|
Value::String(v),
|
|
);
|
|
changed = true;
|
|
}
|
|
}
|
|
if env.get("ANTHROPIC_DEFAULT_OPUS_MODEL").is_none() {
|
|
if let Some(v) = target_opus {
|
|
env.insert("ANTHROPIC_DEFAULT_OPUS_MODEL".to_string(), Value::String(v));
|
|
changed = true;
|
|
}
|
|
}
|
|
|
|
if env.remove("ANTHROPIC_SMALL_FAST_MODEL").is_some() {
|
|
changed = true;
|
|
}
|
|
|
|
changed
|
|
}
|
|
|
|
#[derive(Debug, Clone, Deserialize)]
|
|
pub struct ProviderSortUpdate {
|
|
pub id: String,
|
|
#[serde(rename = "sortIndex")]
|
|
pub sort_index: usize,
|
|
}
|