mirror of
https://github.com/RightNow-AI/openfang.git
synced 2026-07-30 15:01:15 +00:00
provider URLs
This commit is contained in:
@@ -4842,6 +4842,7 @@ pub async fn list_providers(State(state): State<Arc<AppState>>) -> impl IntoResp
|
||||
"model_count": p.model_count,
|
||||
"key_required": p.key_required,
|
||||
"api_key_env": p.api_key_env,
|
||||
"base_url": p.base_url,
|
||||
});
|
||||
|
||||
// For local providers, add reachability info via health probe
|
||||
@@ -5899,6 +5900,122 @@ pub async fn test_provider(
|
||||
}
|
||||
}
|
||||
|
||||
/// PUT /api/providers/{name}/url — Set a custom base URL for a provider.
|
||||
pub async fn set_provider_url(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(name): Path<String>,
|
||||
Json(body): Json<serde_json::Value>,
|
||||
) -> impl IntoResponse {
|
||||
// Validate provider exists
|
||||
let provider_exists = {
|
||||
let catalog = state
|
||||
.kernel
|
||||
.model_catalog
|
||||
.read()
|
||||
.unwrap_or_else(|e| e.into_inner());
|
||||
catalog.get_provider(&name).is_some()
|
||||
};
|
||||
if !provider_exists {
|
||||
return (
|
||||
StatusCode::NOT_FOUND,
|
||||
Json(serde_json::json!({"error": format!("Unknown provider '{}'", name)})),
|
||||
);
|
||||
}
|
||||
|
||||
let base_url = match body["base_url"].as_str() {
|
||||
Some(u) if !u.trim().is_empty() => u.trim().to_string(),
|
||||
_ => {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(serde_json::json!({"error": "Missing or empty 'base_url' field"})),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Validate URL scheme
|
||||
if !base_url.starts_with("http://") && !base_url.starts_with("https://") {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(serde_json::json!({"error": "base_url must start with http:// or https://"})),
|
||||
);
|
||||
}
|
||||
|
||||
// Update catalog in memory
|
||||
{
|
||||
let mut catalog = state
|
||||
.kernel
|
||||
.model_catalog
|
||||
.write()
|
||||
.unwrap_or_else(|e| e.into_inner());
|
||||
catalog.set_provider_url(&name, &base_url);
|
||||
}
|
||||
|
||||
// Persist to config.toml [provider_urls] section
|
||||
let config_path = state.kernel.config.home_dir.join("config.toml");
|
||||
if let Err(e) = upsert_provider_url(&config_path, &name, &base_url) {
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(serde_json::json!({"error": format!("Failed to save config: {e}")})),
|
||||
);
|
||||
}
|
||||
|
||||
// Probe reachability at the new URL
|
||||
let probe =
|
||||
openfang_runtime::provider_health::probe_provider(&name, &base_url).await;
|
||||
|
||||
(
|
||||
StatusCode::OK,
|
||||
Json(serde_json::json!({
|
||||
"status": "saved",
|
||||
"provider": name,
|
||||
"base_url": base_url,
|
||||
"reachable": probe.reachable,
|
||||
"latency_ms": probe.latency_ms,
|
||||
})),
|
||||
)
|
||||
}
|
||||
|
||||
/// Upsert a provider URL in the `[provider_urls]` section of config.toml.
|
||||
fn upsert_provider_url(
|
||||
config_path: &std::path::Path,
|
||||
provider: &str,
|
||||
url: &str,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let content = if config_path.exists() {
|
||||
std::fs::read_to_string(config_path)?
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
let mut doc: toml::Value = if content.trim().is_empty() {
|
||||
toml::Value::Table(toml::map::Map::new())
|
||||
} else {
|
||||
toml::from_str(&content)?
|
||||
};
|
||||
|
||||
let root = doc.as_table_mut().ok_or("Config is not a TOML table")?;
|
||||
|
||||
if !root.contains_key("provider_urls") {
|
||||
root.insert(
|
||||
"provider_urls".to_string(),
|
||||
toml::Value::Table(toml::map::Map::new()),
|
||||
);
|
||||
}
|
||||
let urls_table = root
|
||||
.get_mut("provider_urls")
|
||||
.and_then(|v| v.as_table_mut())
|
||||
.ok_or("provider_urls is not a table")?;
|
||||
|
||||
urls_table.insert(provider.to_string(), toml::Value::String(url.to_string()));
|
||||
|
||||
if let Some(parent) = config_path.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
|
||||
std::fs::write(config_path, toml::to_string_pretty(&doc)?)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// POST /api/skills/create — Create a local prompt-only skill.
|
||||
pub async fn create_skill(
|
||||
State(state): State<Arc<AppState>>,
|
||||
|
||||
@@ -460,6 +460,10 @@ pub async fn build_router(
|
||||
"/api/providers/{name}/test",
|
||||
axum::routing::post(routes::test_provider),
|
||||
)
|
||||
.route(
|
||||
"/api/providers/{name}/url",
|
||||
axum::routing::put(routes::set_provider_url),
|
||||
)
|
||||
.route(
|
||||
"/api/skills/create",
|
||||
axum::routing::post(routes::create_skill),
|
||||
|
||||
@@ -2886,6 +2886,19 @@ args = ["-y", "@modelcontextprotocol/server-filesystem", "/path"]</pre>
|
||||
<template x-if="!p.api_key_env || p.key_required === false">
|
||||
<div class="text-xs mt-2" style="color:var(--success)" x-show="p.auth_status !== 'configured' && p.auth_status !== 'not_set' && p.auth_status !== 'missing'">No API key needed — runs locally or is free</div>
|
||||
</template>
|
||||
<!-- Base URL editor for local providers -->
|
||||
<template x-if="p.is_local">
|
||||
<div class="mt-3" style="border-top:1px solid var(--border);padding-top:8px">
|
||||
<div class="text-xs text-dim mb-1">Base URL</div>
|
||||
<div class="key-input-group">
|
||||
<input type="text" :placeholder="'http://localhost:...'" x-model="providerUrlInputs[p.id]" style="font-size:12px">
|
||||
<button class="btn btn-primary btn-sm" @click="saveProviderUrl(p)" :disabled="providerUrlSaving[p.id]">
|
||||
<span x-show="!providerUrlSaving[p.id]">Save</span>
|
||||
<span x-show="providerUrlSaving[p.id]" class="spinner" style="width:10px;height:10px;border-width:2px"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
@@ -15,6 +15,8 @@ function settingsPage() {
|
||||
modelProviderFilter: '',
|
||||
modelTierFilter: '',
|
||||
providerKeyInputs: {},
|
||||
providerUrlInputs: {},
|
||||
providerUrlSaving: {},
|
||||
providerTesting: {},
|
||||
providerTestResults: {},
|
||||
loading: true,
|
||||
@@ -208,6 +210,12 @@ function settingsPage() {
|
||||
try {
|
||||
var data = await OpenFangAPI.get('/api/providers');
|
||||
this.providers = data.providers || [];
|
||||
for (var i = 0; i < this.providers.length; i++) {
|
||||
var p = this.providers[i];
|
||||
if (p.is_local && p.base_url && !this.providerUrlInputs[p.id]) {
|
||||
this.providerUrlInputs[p.id] = p.base_url;
|
||||
}
|
||||
}
|
||||
} catch(e) { this.providers = []; }
|
||||
},
|
||||
|
||||
@@ -378,6 +386,28 @@ function settingsPage() {
|
||||
this.providerTesting[provider.id] = false;
|
||||
},
|
||||
|
||||
async saveProviderUrl(provider) {
|
||||
var url = this.providerUrlInputs[provider.id];
|
||||
if (!url || !url.trim()) { OpenFangToast.error('Please enter a base URL'); return; }
|
||||
url = url.trim();
|
||||
if (url.indexOf('http://') !== 0 && url.indexOf('https://') !== 0) {
|
||||
OpenFangToast.error('URL must start with http:// or https://'); return;
|
||||
}
|
||||
this.providerUrlSaving[provider.id] = true;
|
||||
try {
|
||||
var result = await OpenFangAPI.put('/api/providers/' + encodeURIComponent(provider.id) + '/url', { base_url: url });
|
||||
if (result.reachable) {
|
||||
OpenFangToast.success(provider.display_name + ' URL saved — reachable (' + (result.latency_ms || '?') + 'ms)');
|
||||
} else {
|
||||
OpenFangToast.warning(provider.display_name + ' URL saved but not reachable');
|
||||
}
|
||||
await this.loadProviders();
|
||||
} catch(e) {
|
||||
OpenFangToast.error('Failed to save URL: ' + e.message);
|
||||
}
|
||||
this.providerUrlSaving[provider.id] = false;
|
||||
},
|
||||
|
||||
// -- Security methods --
|
||||
async loadSecurity() {
|
||||
this.secLoading = true;
|
||||
|
||||
@@ -41,6 +41,8 @@ pub enum HotAction {
|
||||
ReloadA2aConfig,
|
||||
/// Fallback provider chain changed.
|
||||
ReloadFallbackProviders,
|
||||
/// Provider base URL overrides changed.
|
||||
ReloadProviderUrls,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -235,6 +237,10 @@ pub fn build_reload_plan(old: &KernelConfig, new: &KernelConfig) -> ReloadPlan {
|
||||
plan.hot_actions.push(HotAction::ReloadFallbackProviders);
|
||||
}
|
||||
|
||||
if field_changed(&old.provider_urls, &new.provider_urls) {
|
||||
plan.hot_actions.push(HotAction::ReloadProviderUrls);
|
||||
}
|
||||
|
||||
// ----- No-op fields -----
|
||||
|
||||
if old.log_level != new.log_level {
|
||||
@@ -461,6 +467,17 @@ mod tests {
|
||||
assert!(plan.hot_actions.contains(&HotAction::ReloadExtensions));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_provider_urls_hot_reload() {
|
||||
let a = default_cfg();
|
||||
let mut b = default_cfg();
|
||||
b.provider_urls
|
||||
.insert("ollama".to_string(), "http://10.0.0.5:11434/v1".to_string());
|
||||
let plan = build_reload_plan(&a, &b);
|
||||
assert!(!plan.restart_required);
|
||||
assert!(plan.hot_actions.contains(&HotAction::ReloadProviderUrls));
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Mixed changes
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
@@ -590,9 +590,16 @@ impl OpenFangKernel {
|
||||
info!("RBAC enabled with {} users", auth.user_count());
|
||||
}
|
||||
|
||||
// Initialize model catalog and detect provider auth
|
||||
// Initialize model catalog, detect provider auth, and apply URL overrides
|
||||
let mut model_catalog = openfang_runtime::model_catalog::ModelCatalog::new();
|
||||
model_catalog.detect_auth();
|
||||
if !config.provider_urls.is_empty() {
|
||||
model_catalog.apply_url_overrides(&config.provider_urls);
|
||||
info!(
|
||||
"applied {} provider URL override(s)",
|
||||
config.provider_urls.len()
|
||||
);
|
||||
}
|
||||
let available_count = model_catalog.available_models().len();
|
||||
let total_count = model_catalog.list_models().len();
|
||||
let local_count = model_catalog
|
||||
@@ -2762,6 +2769,14 @@ impl OpenFangKernel {
|
||||
self.cron_scheduler
|
||||
.set_max_total_jobs(new_config.max_cron_jobs);
|
||||
}
|
||||
HotAction::ReloadProviderUrls => {
|
||||
info!("Hot-reload: applying provider URL overrides");
|
||||
let mut catalog = self
|
||||
.model_catalog
|
||||
.write()
|
||||
.unwrap_or_else(|e| e.into_inner());
|
||||
catalog.apply_url_overrides(&new_config.provider_urls);
|
||||
}
|
||||
_ => {
|
||||
// Other hot actions (channels, web, browser, extensions, etc.)
|
||||
// are logged but not applied here — they require subsystem-specific
|
||||
|
||||
@@ -148,20 +148,33 @@ impl MeteringEngine {
|
||||
/// | Model Family | Input $/M | Output $/M |
|
||||
/// |-----------------------|-----------|------------|
|
||||
/// | claude-haiku | 0.25 | 1.25 |
|
||||
/// | claude-sonnet | 3.00 | 15.00 |
|
||||
/// | claude-opus | 15.00 | 75.00 |
|
||||
/// | claude-sonnet-4-6 | 3.00 | 15.00 |
|
||||
/// | claude-opus-4-6 | 5.00 | 25.00 |
|
||||
/// | claude-opus (legacy) | 15.00 | 75.00 |
|
||||
/// | gpt-5.2(-pro) | 1.75 | 14.00 |
|
||||
/// | gpt-5(.1) | 1.25 | 10.00 |
|
||||
/// | gpt-5-mini | 0.25 | 2.00 |
|
||||
/// | gpt-5-nano | 0.05 | 0.40 |
|
||||
/// | gpt-4o | 2.50 | 10.00 |
|
||||
/// | gpt-4o-mini | 0.15 | 0.60 |
|
||||
/// | gpt-4.1 | 2.00 | 8.00 |
|
||||
/// | gpt-4.1-mini | 0.40 | 1.60 |
|
||||
/// | gpt-4.1-nano | 0.10 | 0.40 |
|
||||
/// | o3-mini | 1.10 | 4.40 |
|
||||
/// | gemini-2.0-flash | 0.10 | 0.40 |
|
||||
/// | gemini-3.1 | 2.50 | 15.00 |
|
||||
/// | gemini-3 | 0.50 | 3.00 |
|
||||
/// | gemini-2.5-flash-lite | 0.04 | 0.15 |
|
||||
/// | gemini-2.5-pro | 1.25 | 10.00 |
|
||||
/// | gemini-2.5-flash | 0.15 | 0.60 |
|
||||
/// | gemini-2.0-flash | 0.10 | 0.40 |
|
||||
/// | deepseek-chat/v3 | 0.27 | 1.10 |
|
||||
/// | deepseek-reasoner/r1 | 0.55 | 2.19 |
|
||||
/// | llama-4-maverick | 0.50 | 0.77 |
|
||||
/// | llama-4-scout | 0.11 | 0.34 |
|
||||
/// | llama/mixtral (groq) | 0.05 | 0.10 |
|
||||
/// | grok-4.1 | 0.20 | 0.50 |
|
||||
/// | grok-4 | 3.00 | 15.00 |
|
||||
/// | grok-3 | 3.00 | 15.00 |
|
||||
/// | qwen | 0.20 | 0.60 |
|
||||
/// | mistral-large | 2.00 | 6.00 |
|
||||
/// | mistral-small | 0.10 | 0.30 |
|
||||
@@ -222,14 +235,38 @@ fn estimate_cost_rates(model: &str) -> (f64, f64) {
|
||||
if model.contains("haiku") {
|
||||
return (0.25, 1.25);
|
||||
}
|
||||
if model.contains("opus-4-6") || model.contains("claude-opus-4-6") {
|
||||
return (5.0, 25.0);
|
||||
}
|
||||
if model.contains("opus") {
|
||||
return (15.0, 75.0);
|
||||
}
|
||||
if model.contains("sonnet-4-6") || model.contains("claude-sonnet-4-6") {
|
||||
return (3.0, 15.0);
|
||||
}
|
||||
if model.contains("sonnet") {
|
||||
return (3.0, 15.0);
|
||||
}
|
||||
|
||||
// ── OpenAI ─────────────────────────────────────────────────
|
||||
if model.contains("gpt-5.2-pro") {
|
||||
return (1.75, 14.0);
|
||||
}
|
||||
if model.contains("gpt-5.2") {
|
||||
return (1.75, 14.0);
|
||||
}
|
||||
if model.contains("gpt-5.1") {
|
||||
return (1.25, 10.0);
|
||||
}
|
||||
if model.contains("gpt-5-nano") {
|
||||
return (0.05, 0.40);
|
||||
}
|
||||
if model.contains("gpt-5-mini") {
|
||||
return (0.25, 2.0);
|
||||
}
|
||||
if model.contains("gpt-5") {
|
||||
return (1.25, 10.0);
|
||||
}
|
||||
if model.contains("gpt-4o-mini") {
|
||||
return (0.15, 0.60);
|
||||
}
|
||||
@@ -260,6 +297,15 @@ fn estimate_cost_rates(model: &str) -> (f64, f64) {
|
||||
}
|
||||
|
||||
// ── Google Gemini ──────────────────────────────────────────
|
||||
if model.contains("gemini-3.1") {
|
||||
return (2.50, 15.0);
|
||||
}
|
||||
if model.contains("gemini-3") {
|
||||
return (0.50, 3.0);
|
||||
}
|
||||
if model.contains("gemini-2.5-flash-lite") {
|
||||
return (0.04, 0.15);
|
||||
}
|
||||
if model.contains("gemini-2.5-pro") {
|
||||
return (1.25, 10.0);
|
||||
}
|
||||
@@ -298,6 +344,12 @@ fn estimate_cost_rates(model: &str) -> (f64, f64) {
|
||||
}
|
||||
|
||||
// ── Open-source (Groq, Together, etc.) ─────────────────────
|
||||
if model.contains("llama-4-maverick") {
|
||||
return (0.50, 0.77);
|
||||
}
|
||||
if model.contains("llama-4-scout") {
|
||||
return (0.11, 0.34);
|
||||
}
|
||||
if model.contains("llama") || model.contains("mixtral") {
|
||||
return (0.05, 0.10);
|
||||
}
|
||||
@@ -374,6 +426,12 @@ fn estimate_cost_rates(model: &str) -> (f64, f64) {
|
||||
}
|
||||
|
||||
// ── xAI / Grok ──────────────────────────────────────────────
|
||||
if model.contains("grok-4.1") {
|
||||
return (0.20, 0.50);
|
||||
}
|
||||
if model.contains("grok-4") {
|
||||
return (3.0, 15.0);
|
||||
}
|
||||
if model.contains("grok-3-mini") || model.contains("grok-2-mini") || model.contains("grok-mini")
|
||||
{
|
||||
return (0.30, 0.50);
|
||||
|
||||
@@ -128,6 +128,28 @@ impl ModelCatalog {
|
||||
&self.aliases
|
||||
}
|
||||
|
||||
/// Set a custom base URL for a provider, overriding the default.
|
||||
///
|
||||
/// Returns `true` if the provider was found and updated.
|
||||
pub fn set_provider_url(&mut self, provider: &str, url: &str) -> bool {
|
||||
if let Some(p) = self.providers.iter_mut().find(|p| p.id == provider) {
|
||||
p.base_url = url.to_string();
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply a batch of provider URL overrides from config.
|
||||
///
|
||||
/// Each entry maps a provider ID to a custom base URL.
|
||||
/// Unknown providers are silently skipped.
|
||||
pub fn apply_url_overrides(&mut self, overrides: &HashMap<String, String>) {
|
||||
for (provider, url) in overrides {
|
||||
self.set_provider_url(provider, url);
|
||||
}
|
||||
}
|
||||
|
||||
/// List models filtered by tier.
|
||||
pub fn models_by_tier(&self, tier: ModelTier) -> Vec<&ModelCatalogEntry> {
|
||||
self.models.iter().filter(|m| m.tier == tier).collect()
|
||||
@@ -446,18 +468,20 @@ fn builtin_providers() -> Vec<ProviderInfo> {
|
||||
|
||||
fn builtin_aliases() -> HashMap<String, String> {
|
||||
let pairs = [
|
||||
("sonnet", "claude-sonnet-4-20250514"),
|
||||
("claude-sonnet", "claude-sonnet-4-20250514"),
|
||||
("sonnet", "claude-sonnet-4-6"),
|
||||
("claude-sonnet", "claude-sonnet-4-6"),
|
||||
("haiku", "claude-haiku-4-5-20251001"),
|
||||
("claude-haiku", "claude-haiku-4-5-20251001"),
|
||||
("opus", "claude-opus-4-20250514"),
|
||||
("claude-opus", "claude-opus-4-20250514"),
|
||||
("opus", "claude-opus-4-6"),
|
||||
("claude-opus", "claude-opus-4-6"),
|
||||
("gpt4", "gpt-4o"),
|
||||
("gpt4o", "gpt-4o"),
|
||||
("gpt4-mini", "gpt-4o-mini"),
|
||||
("flash", "gemini-2.5-flash"),
|
||||
("gemini-flash", "gemini-2.5-flash"),
|
||||
("gemini-pro", "gemini-2.5-pro"),
|
||||
("gpt5", "gpt-5.2"),
|
||||
("gpt5-mini", "gpt-5-mini"),
|
||||
("flash", "gemini-3-flash"),
|
||||
("gemini-flash", "gemini-3-flash"),
|
||||
("gemini-pro", "gemini-3.1-pro"),
|
||||
("deepseek", "deepseek-chat"),
|
||||
("llama", "llama-3.3-70b-versatile"),
|
||||
("llama-70b", "llama-3.3-70b-versatile"),
|
||||
@@ -471,9 +495,10 @@ fn builtin_aliases() -> HashMap<String, String> {
|
||||
("mistral-nemo", "open-mistral-nemo"),
|
||||
("pixtral", "pixtral-large-latest"),
|
||||
// xAI aliases
|
||||
("grok", "grok-2"),
|
||||
("grok", "grok-4"),
|
||||
("grok-mini", "grok-2-mini"),
|
||||
("grok3", "grok-3"),
|
||||
("grok-fast", "grok-4.1-fast"),
|
||||
// Perplexity alias
|
||||
("sonar", "sonar-pro"),
|
||||
// AI21 aliases
|
||||
@@ -503,8 +528,36 @@ fn builtin_aliases() -> HashMap<String, String> {
|
||||
fn builtin_models() -> Vec<ModelCatalogEntry> {
|
||||
vec![
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// Anthropic (5)
|
||||
// Anthropic (7)
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
ModelCatalogEntry {
|
||||
id: "claude-opus-4-6".into(),
|
||||
display_name: "Claude Opus 4.6".into(),
|
||||
provider: "anthropic".into(),
|
||||
tier: ModelTier::Frontier,
|
||||
context_window: 200_000,
|
||||
max_output_tokens: 128_000,
|
||||
input_cost_per_m: 5.0,
|
||||
output_cost_per_m: 25.0,
|
||||
supports_tools: true,
|
||||
supports_vision: true,
|
||||
supports_streaming: true,
|
||||
aliases: vec!["opus".into(), "claude-opus".into()],
|
||||
},
|
||||
ModelCatalogEntry {
|
||||
id: "claude-sonnet-4-6".into(),
|
||||
display_name: "Claude Sonnet 4.6".into(),
|
||||
provider: "anthropic".into(),
|
||||
tier: ModelTier::Smart,
|
||||
context_window: 200_000,
|
||||
max_output_tokens: 64_000,
|
||||
input_cost_per_m: 3.0,
|
||||
output_cost_per_m: 15.0,
|
||||
supports_tools: true,
|
||||
supports_vision: true,
|
||||
supports_streaming: true,
|
||||
aliases: vec!["sonnet".into(), "claude-sonnet".into()],
|
||||
},
|
||||
ModelCatalogEntry {
|
||||
id: "claude-opus-4-20250514".into(),
|
||||
display_name: "Claude Opus 4".into(),
|
||||
@@ -517,7 +570,7 @@ fn builtin_models() -> Vec<ModelCatalogEntry> {
|
||||
supports_tools: true,
|
||||
supports_vision: true,
|
||||
supports_streaming: true,
|
||||
aliases: vec!["opus".into(), "claude-opus".into()],
|
||||
aliases: vec![],
|
||||
},
|
||||
ModelCatalogEntry {
|
||||
id: "claude-sonnet-4-20250514".into(),
|
||||
@@ -531,7 +584,7 @@ fn builtin_models() -> Vec<ModelCatalogEntry> {
|
||||
supports_tools: true,
|
||||
supports_vision: true,
|
||||
supports_streaming: true,
|
||||
aliases: vec!["sonnet".into(), "claude-sonnet".into()],
|
||||
aliases: vec![],
|
||||
},
|
||||
ModelCatalogEntry {
|
||||
id: "claude-haiku-4-5-20251001".into(),
|
||||
@@ -576,7 +629,7 @@ fn builtin_models() -> Vec<ModelCatalogEntry> {
|
||||
aliases: vec![],
|
||||
},
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// OpenAI (10)
|
||||
// OpenAI (16)
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
ModelCatalogEntry {
|
||||
id: "gpt-4o".into(),
|
||||
@@ -718,9 +771,149 @@ fn builtin_models() -> Vec<ModelCatalogEntry> {
|
||||
supports_streaming: true,
|
||||
aliases: vec![],
|
||||
},
|
||||
ModelCatalogEntry {
|
||||
id: "gpt-5".into(),
|
||||
display_name: "GPT-5".into(),
|
||||
provider: "openai".into(),
|
||||
tier: ModelTier::Frontier,
|
||||
context_window: 400_000,
|
||||
max_output_tokens: 128_000,
|
||||
input_cost_per_m: 1.25,
|
||||
output_cost_per_m: 10.0,
|
||||
supports_tools: true,
|
||||
supports_vision: true,
|
||||
supports_streaming: true,
|
||||
aliases: vec![],
|
||||
},
|
||||
ModelCatalogEntry {
|
||||
id: "gpt-5-mini".into(),
|
||||
display_name: "GPT-5 Mini".into(),
|
||||
provider: "openai".into(),
|
||||
tier: ModelTier::Balanced,
|
||||
context_window: 400_000,
|
||||
max_output_tokens: 128_000,
|
||||
input_cost_per_m: 0.25,
|
||||
output_cost_per_m: 2.0,
|
||||
supports_tools: true,
|
||||
supports_vision: true,
|
||||
supports_streaming: true,
|
||||
aliases: vec!["gpt5-mini".into()],
|
||||
},
|
||||
ModelCatalogEntry {
|
||||
id: "gpt-5-nano".into(),
|
||||
display_name: "GPT-5 Nano".into(),
|
||||
provider: "openai".into(),
|
||||
tier: ModelTier::Fast,
|
||||
context_window: 400_000,
|
||||
max_output_tokens: 128_000,
|
||||
input_cost_per_m: 0.05,
|
||||
output_cost_per_m: 0.40,
|
||||
supports_tools: true,
|
||||
supports_vision: false,
|
||||
supports_streaming: true,
|
||||
aliases: vec![],
|
||||
},
|
||||
ModelCatalogEntry {
|
||||
id: "gpt-5.1".into(),
|
||||
display_name: "GPT-5.1".into(),
|
||||
provider: "openai".into(),
|
||||
tier: ModelTier::Frontier,
|
||||
context_window: 400_000,
|
||||
max_output_tokens: 128_000,
|
||||
input_cost_per_m: 1.25,
|
||||
output_cost_per_m: 10.0,
|
||||
supports_tools: true,
|
||||
supports_vision: true,
|
||||
supports_streaming: true,
|
||||
aliases: vec![],
|
||||
},
|
||||
ModelCatalogEntry {
|
||||
id: "gpt-5.2".into(),
|
||||
display_name: "GPT-5.2".into(),
|
||||
provider: "openai".into(),
|
||||
tier: ModelTier::Frontier,
|
||||
context_window: 400_000,
|
||||
max_output_tokens: 128_000,
|
||||
input_cost_per_m: 1.75,
|
||||
output_cost_per_m: 14.0,
|
||||
supports_tools: true,
|
||||
supports_vision: true,
|
||||
supports_streaming: true,
|
||||
aliases: vec!["gpt5".into()],
|
||||
},
|
||||
ModelCatalogEntry {
|
||||
id: "gpt-5.2-pro".into(),
|
||||
display_name: "GPT-5.2 Pro".into(),
|
||||
provider: "openai".into(),
|
||||
tier: ModelTier::Frontier,
|
||||
context_window: 400_000,
|
||||
max_output_tokens: 128_000,
|
||||
input_cost_per_m: 1.75,
|
||||
output_cost_per_m: 14.0,
|
||||
supports_tools: true,
|
||||
supports_vision: true,
|
||||
supports_streaming: true,
|
||||
aliases: vec![],
|
||||
},
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// Google Gemini (6)
|
||||
// Google Gemini (10)
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
ModelCatalogEntry {
|
||||
id: "gemini-3.1-pro".into(),
|
||||
display_name: "Gemini 3.1 Pro".into(),
|
||||
provider: "gemini".into(),
|
||||
tier: ModelTier::Frontier,
|
||||
context_window: 1_048_576,
|
||||
max_output_tokens: 65_536,
|
||||
input_cost_per_m: 2.50,
|
||||
output_cost_per_m: 15.0,
|
||||
supports_tools: true,
|
||||
supports_vision: true,
|
||||
supports_streaming: true,
|
||||
aliases: vec!["gemini-pro".into()],
|
||||
},
|
||||
ModelCatalogEntry {
|
||||
id: "gemini-3-flash".into(),
|
||||
display_name: "Gemini 3 Flash".into(),
|
||||
provider: "gemini".into(),
|
||||
tier: ModelTier::Smart,
|
||||
context_window: 1_048_576,
|
||||
max_output_tokens: 65_536,
|
||||
input_cost_per_m: 0.50,
|
||||
output_cost_per_m: 3.0,
|
||||
supports_tools: true,
|
||||
supports_vision: true,
|
||||
supports_streaming: true,
|
||||
aliases: vec!["flash".into(), "gemini-flash".into()],
|
||||
},
|
||||
ModelCatalogEntry {
|
||||
id: "gemini-3-deep-think".into(),
|
||||
display_name: "Gemini 3 Deep Think".into(),
|
||||
provider: "gemini".into(),
|
||||
tier: ModelTier::Frontier,
|
||||
context_window: 1_048_576,
|
||||
max_output_tokens: 65_536,
|
||||
input_cost_per_m: 2.50,
|
||||
output_cost_per_m: 15.0,
|
||||
supports_tools: true,
|
||||
supports_vision: true,
|
||||
supports_streaming: true,
|
||||
aliases: vec![],
|
||||
},
|
||||
ModelCatalogEntry {
|
||||
id: "gemini-2.5-flash-lite".into(),
|
||||
display_name: "Gemini 2.5 Flash Lite".into(),
|
||||
provider: "gemini".into(),
|
||||
tier: ModelTier::Fast,
|
||||
context_window: 1_048_576,
|
||||
max_output_tokens: 8_192,
|
||||
input_cost_per_m: 0.04,
|
||||
output_cost_per_m: 0.15,
|
||||
supports_tools: true,
|
||||
supports_vision: true,
|
||||
supports_streaming: true,
|
||||
aliases: vec![],
|
||||
},
|
||||
ModelCatalogEntry {
|
||||
id: "gemini-2.5-pro".into(),
|
||||
display_name: "Gemini 2.5 Pro".into(),
|
||||
@@ -733,7 +926,7 @@ fn builtin_models() -> Vec<ModelCatalogEntry> {
|
||||
supports_tools: true,
|
||||
supports_vision: true,
|
||||
supports_streaming: true,
|
||||
aliases: vec!["gemini-pro".into()],
|
||||
aliases: vec![],
|
||||
},
|
||||
ModelCatalogEntry {
|
||||
id: "gemini-2.5-flash".into(),
|
||||
@@ -747,7 +940,7 @@ fn builtin_models() -> Vec<ModelCatalogEntry> {
|
||||
supports_tools: true,
|
||||
supports_vision: true,
|
||||
supports_streaming: true,
|
||||
aliases: vec!["flash".into(), "gemini-flash".into()],
|
||||
aliases: vec![],
|
||||
},
|
||||
ModelCatalogEntry {
|
||||
id: "gemini-2.0-flash".into(),
|
||||
@@ -865,7 +1058,7 @@ fn builtin_models() -> Vec<ModelCatalogEntry> {
|
||||
aliases: vec![],
|
||||
},
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// Groq (10)
|
||||
// Groq (11)
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
ModelCatalogEntry {
|
||||
id: "llama-3.3-70b-versatile".into(),
|
||||
@@ -1007,6 +1200,20 @@ fn builtin_models() -> Vec<ModelCatalogEntry> {
|
||||
supports_streaming: true,
|
||||
aliases: vec![],
|
||||
},
|
||||
ModelCatalogEntry {
|
||||
id: "meta-llama/llama-4-scout-17b-16e-instruct".into(),
|
||||
display_name: "Llama 4 Scout 17B".into(),
|
||||
provider: "groq".into(),
|
||||
tier: ModelTier::Balanced,
|
||||
context_window: 128_000,
|
||||
max_output_tokens: 8_192,
|
||||
input_cost_per_m: 0.11,
|
||||
output_cost_per_m: 0.34,
|
||||
supports_tools: true,
|
||||
supports_vision: true,
|
||||
supports_streaming: true,
|
||||
aliases: vec![],
|
||||
},
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// OpenRouter (5)
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
@@ -1744,8 +1951,36 @@ fn builtin_models() -> Vec<ModelCatalogEntry> {
|
||||
aliases: vec![],
|
||||
},
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// xAI (4)
|
||||
// xAI (6)
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
ModelCatalogEntry {
|
||||
id: "grok-4".into(),
|
||||
display_name: "Grok 4".into(),
|
||||
provider: "xai".into(),
|
||||
tier: ModelTier::Frontier,
|
||||
context_window: 256_000,
|
||||
max_output_tokens: 32_768,
|
||||
input_cost_per_m: 3.0,
|
||||
output_cost_per_m: 15.0,
|
||||
supports_tools: true,
|
||||
supports_vision: true,
|
||||
supports_streaming: true,
|
||||
aliases: vec!["grok".into()],
|
||||
},
|
||||
ModelCatalogEntry {
|
||||
id: "grok-4.1-fast".into(),
|
||||
display_name: "Grok 4.1 Fast".into(),
|
||||
provider: "xai".into(),
|
||||
tier: ModelTier::Fast,
|
||||
context_window: 2_000_000,
|
||||
max_output_tokens: 32_768,
|
||||
input_cost_per_m: 0.20,
|
||||
output_cost_per_m: 0.50,
|
||||
supports_tools: true,
|
||||
supports_vision: false,
|
||||
supports_streaming: true,
|
||||
aliases: vec!["grok-fast".into()],
|
||||
},
|
||||
ModelCatalogEntry {
|
||||
id: "grok-3".into(),
|
||||
display_name: "Grok 3".into(),
|
||||
@@ -1758,7 +1993,7 @@ fn builtin_models() -> Vec<ModelCatalogEntry> {
|
||||
supports_tools: true,
|
||||
supports_vision: true,
|
||||
supports_streaming: true,
|
||||
aliases: vec![],
|
||||
aliases: vec!["grok3".into()],
|
||||
},
|
||||
ModelCatalogEntry {
|
||||
id: "grok-3-mini".into(),
|
||||
@@ -1786,7 +2021,7 @@ fn builtin_models() -> Vec<ModelCatalogEntry> {
|
||||
supports_tools: true,
|
||||
supports_vision: true,
|
||||
supports_streaming: true,
|
||||
aliases: vec!["grok".into()],
|
||||
aliases: vec![],
|
||||
},
|
||||
ModelCatalogEntry {
|
||||
id: "grok-2-mini".into(),
|
||||
@@ -2205,8 +2440,36 @@ fn builtin_models() -> Vec<ModelCatalogEntry> {
|
||||
aliases: vec![],
|
||||
},
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// AWS Bedrock (6)
|
||||
// AWS Bedrock (8)
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
ModelCatalogEntry {
|
||||
id: "bedrock/anthropic.claude-opus-4-6".into(),
|
||||
display_name: "Claude Opus 4.6 (Bedrock)".into(),
|
||||
provider: "bedrock".into(),
|
||||
tier: ModelTier::Frontier,
|
||||
context_window: 200_000,
|
||||
max_output_tokens: 128_000,
|
||||
input_cost_per_m: 5.00,
|
||||
output_cost_per_m: 25.00,
|
||||
supports_tools: true,
|
||||
supports_vision: true,
|
||||
supports_streaming: true,
|
||||
aliases: vec![],
|
||||
},
|
||||
ModelCatalogEntry {
|
||||
id: "bedrock/anthropic.claude-sonnet-4-6".into(),
|
||||
display_name: "Claude Sonnet 4.6 (Bedrock)".into(),
|
||||
provider: "bedrock".into(),
|
||||
tier: ModelTier::Smart,
|
||||
context_window: 200_000,
|
||||
max_output_tokens: 64_000,
|
||||
input_cost_per_m: 3.00,
|
||||
output_cost_per_m: 15.00,
|
||||
supports_tools: true,
|
||||
supports_vision: true,
|
||||
supports_streaming: true,
|
||||
aliases: vec![],
|
||||
},
|
||||
ModelCatalogEntry {
|
||||
id: "bedrock/anthropic.claude-opus-4-20250514".into(),
|
||||
display_name: "Claude Opus 4 (Bedrock)".into(),
|
||||
@@ -2323,7 +2586,7 @@ mod tests {
|
||||
fn test_find_model_by_alias() {
|
||||
let catalog = ModelCatalog::new();
|
||||
let entry = catalog.find_model("sonnet").unwrap();
|
||||
assert_eq!(entry.id, "claude-sonnet-4-20250514");
|
||||
assert_eq!(entry.id, "claude-sonnet-4-6");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2344,7 +2607,7 @@ mod tests {
|
||||
let catalog = ModelCatalog::new();
|
||||
assert_eq!(
|
||||
catalog.resolve_alias("sonnet"),
|
||||
Some("claude-sonnet-4-20250514")
|
||||
Some("claude-sonnet-4-6")
|
||||
);
|
||||
assert_eq!(
|
||||
catalog.resolve_alias("haiku"),
|
||||
@@ -2357,7 +2620,7 @@ mod tests {
|
||||
fn test_models_by_provider() {
|
||||
let catalog = ModelCatalog::new();
|
||||
let anthropic = catalog.models_by_provider("anthropic");
|
||||
assert_eq!(anthropic.len(), 5);
|
||||
assert_eq!(anthropic.len(), 7);
|
||||
assert!(anthropic.iter().all(|m| m.provider == "anthropic"));
|
||||
}
|
||||
|
||||
@@ -2415,9 +2678,9 @@ mod tests {
|
||||
fn test_provider_model_counts() {
|
||||
let catalog = ModelCatalog::new();
|
||||
let anthropic = catalog.get_provider("anthropic").unwrap();
|
||||
assert_eq!(anthropic.model_count, 5);
|
||||
assert_eq!(anthropic.model_count, 7);
|
||||
let groq = catalog.get_provider("groq").unwrap();
|
||||
assert_eq!(groq.model_count, 10);
|
||||
assert_eq!(groq.model_count, 11);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2425,9 +2688,9 @@ mod tests {
|
||||
let catalog = ModelCatalog::new();
|
||||
let aliases = catalog.list_aliases();
|
||||
assert!(aliases.len() >= 20);
|
||||
assert_eq!(aliases.get("sonnet").unwrap(), "claude-sonnet-4-20250514");
|
||||
assert_eq!(aliases.get("sonnet").unwrap(), "claude-sonnet-4-6");
|
||||
// New aliases
|
||||
assert_eq!(aliases.get("grok").unwrap(), "grok-2");
|
||||
assert_eq!(aliases.get("grok").unwrap(), "grok-4");
|
||||
assert_eq!(aliases.get("jamba").unwrap(), "jamba-1.5-large");
|
||||
}
|
||||
|
||||
@@ -2435,7 +2698,7 @@ mod tests {
|
||||
fn test_find_grok_by_alias() {
|
||||
let catalog = ModelCatalog::new();
|
||||
let entry = catalog.find_model("grok").unwrap();
|
||||
assert_eq!(entry.id, "grok-2");
|
||||
assert_eq!(entry.id, "grok-4");
|
||||
assert_eq!(entry.provider, "xai");
|
||||
}
|
||||
|
||||
@@ -2456,11 +2719,13 @@ mod tests {
|
||||
fn test_xai_models() {
|
||||
let catalog = ModelCatalog::new();
|
||||
let xai = catalog.models_by_provider("xai");
|
||||
assert_eq!(xai.len(), 4);
|
||||
assert!(xai.iter().any(|m| m.id == "grok-2"));
|
||||
assert!(xai.iter().any(|m| m.id == "grok-2-mini"));
|
||||
assert_eq!(xai.len(), 6);
|
||||
assert!(xai.iter().any(|m| m.id == "grok-4"));
|
||||
assert!(xai.iter().any(|m| m.id == "grok-4.1-fast"));
|
||||
assert!(xai.iter().any(|m| m.id == "grok-3"));
|
||||
assert!(xai.iter().any(|m| m.id == "grok-3-mini"));
|
||||
assert!(xai.iter().any(|m| m.id == "grok-2"));
|
||||
assert!(xai.iter().any(|m| m.id == "grok-2-mini"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2543,6 +2808,52 @@ mod tests {
|
||||
fn test_bedrock_models() {
|
||||
let catalog = ModelCatalog::new();
|
||||
let bedrock = catalog.models_by_provider("bedrock");
|
||||
assert_eq!(bedrock.len(), 6);
|
||||
assert_eq!(bedrock.len(), 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_set_provider_url() {
|
||||
let mut catalog = ModelCatalog::new();
|
||||
let old_url = catalog.get_provider("ollama").unwrap().base_url.clone();
|
||||
assert_eq!(old_url, OLLAMA_BASE_URL);
|
||||
|
||||
let updated = catalog.set_provider_url("ollama", "http://192.168.1.100:11434/v1");
|
||||
assert!(updated);
|
||||
assert_eq!(
|
||||
catalog.get_provider("ollama").unwrap().base_url,
|
||||
"http://192.168.1.100:11434/v1"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_set_provider_url_unknown() {
|
||||
let mut catalog = ModelCatalog::new();
|
||||
let updated = catalog.set_provider_url("nonexistent", "http://localhost:9999");
|
||||
assert!(!updated);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_apply_url_overrides() {
|
||||
let mut catalog = ModelCatalog::new();
|
||||
let mut overrides = HashMap::new();
|
||||
overrides.insert("ollama".to_string(), "http://10.0.0.5:11434/v1".to_string());
|
||||
overrides.insert("vllm".to_string(), "http://10.0.0.6:8000/v1".to_string());
|
||||
overrides.insert("nonexistent".to_string(), "http://nowhere".to_string());
|
||||
|
||||
catalog.apply_url_overrides(&overrides);
|
||||
|
||||
assert_eq!(
|
||||
catalog.get_provider("ollama").unwrap().base_url,
|
||||
"http://10.0.0.5:11434/v1"
|
||||
);
|
||||
assert_eq!(
|
||||
catalog.get_provider("vllm").unwrap().base_url,
|
||||
"http://10.0.0.6:8000/v1"
|
||||
);
|
||||
// lmstudio should be unchanged
|
||||
assert_eq!(
|
||||
catalog.get_provider("lmstudio").unwrap().base_url,
|
||||
LMSTUDIO_BASE_URL
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,7 +149,7 @@ impl ModelRouter {
|
||||
|
||||
/// Resolve aliases in the routing config using the catalog.
|
||||
///
|
||||
/// For example, if "sonnet" is configured, resolves to "claude-sonnet-4-20250514".
|
||||
/// For example, if "sonnet" is configured, resolves to "claude-sonnet-4-6".
|
||||
pub fn resolve_aliases(&mut self, catalog: &crate::model_catalog::ModelCatalog) {
|
||||
if let Some(resolved) = catalog.resolve_alias(&self.config.simple_model) {
|
||||
self.config.simple_model = resolved.to_string();
|
||||
@@ -172,8 +172,8 @@ mod tests {
|
||||
fn default_config() -> ModelRoutingConfig {
|
||||
ModelRoutingConfig {
|
||||
simple_model: "llama-3.3-70b-versatile".to_string(),
|
||||
medium_model: "claude-sonnet-4-20250514".to_string(),
|
||||
complex_model: "claude-opus-4-20250514".to_string(),
|
||||
medium_model: "claude-sonnet-4-6".to_string(),
|
||||
complex_model: "claude-opus-4-6".to_string(),
|
||||
simple_threshold: 200,
|
||||
complex_threshold: 800,
|
||||
}
|
||||
@@ -274,11 +274,11 @@ mod tests {
|
||||
);
|
||||
assert_eq!(
|
||||
router.model_for_complexity(TaskComplexity::Medium),
|
||||
"claude-sonnet-4-20250514"
|
||||
"claude-sonnet-4-6"
|
||||
);
|
||||
assert_eq!(
|
||||
router.model_for_complexity(TaskComplexity::Complex),
|
||||
"claude-opus-4-20250514"
|
||||
"claude-opus-4-6"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -294,8 +294,8 @@ mod tests {
|
||||
let catalog = crate::model_catalog::ModelCatalog::new();
|
||||
let config = ModelRoutingConfig {
|
||||
simple_model: "llama-3.3-70b-versatile".to_string(),
|
||||
medium_model: "claude-sonnet-4-20250514".to_string(),
|
||||
complex_model: "claude-opus-4-20250514".to_string(),
|
||||
medium_model: "claude-sonnet-4-6".to_string(),
|
||||
complex_model: "claude-opus-4-6".to_string(),
|
||||
simple_threshold: 200,
|
||||
complex_threshold: 800,
|
||||
};
|
||||
@@ -309,8 +309,8 @@ mod tests {
|
||||
let catalog = crate::model_catalog::ModelCatalog::new();
|
||||
let config = ModelRoutingConfig {
|
||||
simple_model: "unknown-model".to_string(),
|
||||
medium_model: "claude-sonnet-4-20250514".to_string(),
|
||||
complex_model: "claude-opus-4-20250514".to_string(),
|
||||
medium_model: "claude-sonnet-4-6".to_string(),
|
||||
complex_model: "claude-opus-4-6".to_string(),
|
||||
simple_threshold: 200,
|
||||
complex_threshold: 800,
|
||||
};
|
||||
@@ -338,11 +338,11 @@ mod tests {
|
||||
);
|
||||
assert_eq!(
|
||||
router.model_for_complexity(TaskComplexity::Medium),
|
||||
"claude-sonnet-4-20250514"
|
||||
"claude-sonnet-4-6"
|
||||
);
|
||||
assert_eq!(
|
||||
router.model_for_complexity(TaskComplexity::Complex),
|
||||
"claude-opus-4-20250514"
|
||||
"claude-opus-4-6"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1039,6 +1039,10 @@ pub struct KernelConfig {
|
||||
/// Global spending budget configuration.
|
||||
#[serde(default)]
|
||||
pub budget: BudgetConfig,
|
||||
/// Provider base URL overrides (provider ID → custom base URL).
|
||||
/// e.g. `ollama = "http://192.168.1.100:11434/v1"`
|
||||
#[serde(default)]
|
||||
pub provider_urls: HashMap<String, String>,
|
||||
}
|
||||
|
||||
/// Global spending budget configuration.
|
||||
@@ -1183,6 +1187,7 @@ impl Default for KernelConfig {
|
||||
auth_profiles: HashMap::new(),
|
||||
thinking: None,
|
||||
budget: BudgetConfig::default(),
|
||||
provider_urls: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user