feat: add support for Router provider and enhance command handling

This commit is contained in:
asepharyana
2026-07-12 01:43:57 +07:00
parent a8fee71e06
commit 9f272d5973
5 changed files with 48 additions and 7 deletions
+2 -1
View File
@@ -1,10 +1,11 @@
use crate::model::settings::Settings; use crate::model::settings::Settings;
pub const PROVIDERS: &[&str] = &["Zen API", "OpenAI"]; pub const PROVIDERS: &[&str] = &["Zen API", "Router", "OpenAI"];
pub fn set_provider(settings: &mut Settings, provider: &str) { pub fn set_provider(settings: &mut Settings, provider: &str) {
settings.provider = match provider { settings.provider = match provider {
"Zen API" => "zen".to_string(), "Zen API" => "zen".to_string(),
"Router" => "router".to_string(),
"OpenAI" => "openai".to_string(), "OpenAI" => "openai".to_string(),
_ => "zen".to_string(), _ => "zen".to_string(),
}; };
+15 -2
View File
@@ -577,7 +577,7 @@ fn run_agent_turn(
}; };
let mut stream_started = false; let mut stream_started = false;
let (response, usage) = tc.client.chat_with_tools_streaming( let (response, usage) = match tc.client.chat_with_tools_streaming(
&wire_msgs, &wire_msgs,
Some(tc.tdefs.clone()), Some(tc.tdefs.clone()),
Some(tc.temperature), Some(tc.temperature),
@@ -602,8 +602,21 @@ fn run_agent_turn(
} }
_ => {} _ => {}
}, },
) {
Ok(result) => result,
Err(_stream_err) => {
// Provider doesn't support streaming — fall back to non-streaming
let (msg, usage_fb) = tc.client.chat_with_tools_non_streaming(
&wire_msgs, Some(tc.tdefs.clone()),
)?; )?;
let _ = usage; // already emitted as TurnEvent::Usage inside the on_event callback, if present if let Some((tok_in, tok_out)) = usage_fb {
if let Ok(mut q) = events_q.lock() {
q.push_back(TurnEvent::Usage { tokens_in: tok_in, tokens_out: tok_out });
}
}
(msg, usage_fb)
}
};
let has_tool_calls = response.tool_calls.is_some() let has_tool_calls = response.tool_calls.is_some()
&& response.tool_calls.as_ref().is_some_and(|tc| !tc.is_empty()); && response.tool_calls.as_ref().is_some_and(|tc| !tc.is_empty());
+10
View File
@@ -72,11 +72,21 @@ const COMMANDS: &[&str] = &[
"/lesson ls", "/lesson ls",
"/lesson export", "/lesson export",
"/lesson import", "/lesson import",
"/lesson accept",
"/lesson reject",
"/mode chat", "/mode chat",
"/mode bash", "/mode bash",
"/mode help", "/mode help",
"/mode settings", "/mode settings",
"/mode effort",
"/mode mcp",
"/mode rewind",
"/mode editor",
"/login", "/login",
"/login zen",
"/login openai",
"/edit",
"/mcp add",
]; ];
impl InputState { impl InputState {
+5
View File
@@ -32,6 +32,11 @@ impl Default for AppConfig {
api_key_env: Some("API_KEY".to_string()), api_key_env: Some("API_KEY".to_string()),
default_model: Some("deepseek-v4-flash-free".to_string()), default_model: Some("deepseek-v4-flash-free".to_string()),
}); });
providers.insert("router".to_string(), ProviderConfig {
api_base: "https://9router.asepharyana.my.id/v1".to_string(),
api_key_env: Some("ROUTER_API_KEY".to_string()),
default_model: Some("claude-opus-4-8".to_string()),
});
let mut model_roles = HashMap::new(); let mut model_roles = HashMap::new();
model_roles.insert("default".to_string(), ModelRole { model_roles.insert("default".to_string(), ModelRole {
provider: "zen".to_string(), provider: "zen".to_string(),
+15 -3
View File
@@ -48,6 +48,15 @@ impl LlmClient {
messages: &[ChatMessage], messages: &[ChatMessage],
tools: Option<Vec<ToolDef>>, tools: Option<Vec<ToolDef>>,
) -> Result<ChatMessage> { ) -> Result<ChatMessage> {
let (msg, _usage) = self.chat_with_tools_non_streaming(messages, tools)?;
Ok(msg)
}
pub fn chat_with_tools_non_streaming(
&self,
messages: &[ChatMessage],
tools: Option<Vec<ToolDef>>,
) -> Result<(ChatMessage, Option<(u64, u64)>)> {
let req = ChatRequest { let req = ChatRequest {
model: self.model.clone(), model: self.model.clone(),
messages: messages.to_vec(), messages: messages.to_vec(),
@@ -75,7 +84,7 @@ impl LlmClient {
http_req = http_req.header("Authorization", format!("Bearer {}", self.api_key)); http_req = http_req.header("Authorization", format!("Bearer {}", self.api_key));
} }
let result = (|| -> Result<ChatMessage> { let result = (|| -> Result<(ChatMessage, Option<(u64, u64)>)> {
let resp = http_req.json(&req).send().map_err(|e| { let resp = http_req.json(&req).send().map_err(|e| {
if e.is_timeout() { if e.is_timeout() {
anyhow::anyhow!("API request timed out after {:?}. Check your network or try again.", REQUEST_TIMEOUT) anyhow::anyhow!("API request timed out after {:?}. Check your network or try again.", REQUEST_TIMEOUT)
@@ -93,17 +102,20 @@ impl LlmClient {
} }
let data: crate::dto::provider::response::ChatResponse = resp.json()?; let data: crate::dto::provider::response::ChatResponse = resp.json()?;
let usage = data.usage.map(|u| {
(u.prompt_tokens.unwrap_or(0) as u64, u.completion_tokens.unwrap_or(0) as u64)
});
let message = data let message = data
.choices .choices
.into_iter() .into_iter()
.next() .next()
.map(|c| c.message) .map(|c| c.message)
.ok_or_else(|| anyhow::anyhow!("API response had no choices"))?; .ok_or_else(|| anyhow::anyhow!("API response had no choices"))?;
Ok(message) Ok((message, usage))
})(); })();
match result { match result {
Ok(msg) => return Ok(msg), Ok((msg, usage)) => return Ok((msg, usage)),
Err(e) => { Err(e) => {
if attempt >= max_retries { if attempt >= max_retries {
return Err(e); return Err(e);