From 9f272d5973333605b54f226c42536b62a3c6428b Mon Sep 17 00:00:00 2001 From: asepharyana Date: Sun, 12 Jul 2026 01:43:57 +0700 Subject: [PATCH] feat: add support for Router provider and enhance command handling --- src/app/mode/onboard_provider.rs | 3 ++- src/app/runtime/actions/mod.rs | 19 ++++++++++++++++--- src/app/state/misc.rs | 10 ++++++++++ src/model/app_config.rs | 5 +++++ src/service/provider.rs | 18 +++++++++++++++--- 5 files changed, 48 insertions(+), 7 deletions(-) diff --git a/src/app/mode/onboard_provider.rs b/src/app/mode/onboard_provider.rs index 49224ab..dfd0995 100644 --- a/src/app/mode/onboard_provider.rs +++ b/src/app/mode/onboard_provider.rs @@ -1,10 +1,11 @@ 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) { settings.provider = match provider { "Zen API" => "zen".to_string(), + "Router" => "router".to_string(), "OpenAI" => "openai".to_string(), _ => "zen".to_string(), }; diff --git a/src/app/runtime/actions/mod.rs b/src/app/runtime/actions/mod.rs index c029787..85419a6 100644 --- a/src/app/runtime/actions/mod.rs +++ b/src/app/runtime/actions/mod.rs @@ -577,7 +577,7 @@ fn run_agent_turn( }; 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, Some(tc.tdefs.clone()), Some(tc.temperature), @@ -602,8 +602,21 @@ fn run_agent_turn( } _ => {} }, - )?; - let _ = usage; // already emitted as TurnEvent::Usage inside the on_event callback, if present + ) { + 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()), + )?; + 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() && response.tool_calls.as_ref().is_some_and(|tc| !tc.is_empty()); diff --git a/src/app/state/misc.rs b/src/app/state/misc.rs index 24d1974..16120be 100644 --- a/src/app/state/misc.rs +++ b/src/app/state/misc.rs @@ -72,11 +72,21 @@ const COMMANDS: &[&str] = &[ "/lesson ls", "/lesson export", "/lesson import", + "/lesson accept", + "/lesson reject", "/mode chat", "/mode bash", "/mode help", "/mode settings", + "/mode effort", + "/mode mcp", + "/mode rewind", + "/mode editor", "/login", + "/login zen", + "/login openai", + "/edit", + "/mcp add", ]; impl InputState { diff --git a/src/model/app_config.rs b/src/model/app_config.rs index bda12de..459cae2 100644 --- a/src/model/app_config.rs +++ b/src/model/app_config.rs @@ -32,6 +32,11 @@ impl Default for AppConfig { api_key_env: Some("API_KEY".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(); model_roles.insert("default".to_string(), ModelRole { provider: "zen".to_string(), diff --git a/src/service/provider.rs b/src/service/provider.rs index bc6bd9a..916f770 100644 --- a/src/service/provider.rs +++ b/src/service/provider.rs @@ -48,6 +48,15 @@ impl LlmClient { messages: &[ChatMessage], tools: Option>, ) -> Result { + 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>, + ) -> Result<(ChatMessage, Option<(u64, u64)>)> { let req = ChatRequest { model: self.model.clone(), messages: messages.to_vec(), @@ -75,7 +84,7 @@ impl LlmClient { http_req = http_req.header("Authorization", format!("Bearer {}", self.api_key)); } - let result = (|| -> Result { + let result = (|| -> Result<(ChatMessage, Option<(u64, u64)>)> { let resp = http_req.json(&req).send().map_err(|e| { if e.is_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 usage = data.usage.map(|u| { + (u.prompt_tokens.unwrap_or(0) as u64, u.completion_tokens.unwrap_or(0) as u64) + }); let message = data .choices .into_iter() .next() .map(|c| c.message) .ok_or_else(|| anyhow::anyhow!("API response had no choices"))?; - Ok(message) + Ok((message, usage)) })(); match result { - Ok(msg) => return Ok(msg), + Ok((msg, usage)) => return Ok((msg, usage)), Err(e) => { if attempt >= max_retries { return Err(e);