diff --git a/Cargo.lock b/Cargo.lock index 1c74397aa..34ee38b8e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3811,7 +3811,7 @@ dependencies = [ [[package]] name = "openfang-api" -version = "0.4.8" +version = "0.4.9" dependencies = [ "async-trait", "axum", @@ -3851,7 +3851,7 @@ dependencies = [ [[package]] name = "openfang-channels" -version = "0.4.8" +version = "0.4.9" dependencies = [ "aes", "async-trait", @@ -3888,7 +3888,7 @@ dependencies = [ [[package]] name = "openfang-cli" -version = "0.4.8" +version = "0.4.9" dependencies = [ "clap", "clap_complete", @@ -3915,7 +3915,7 @@ dependencies = [ [[package]] name = "openfang-desktop" -version = "0.4.8" +version = "0.4.9" dependencies = [ "axum", "open", @@ -3941,7 +3941,7 @@ dependencies = [ [[package]] name = "openfang-extensions" -version = "0.4.8" +version = "0.4.9" dependencies = [ "aes-gcm", "argon2", @@ -3969,7 +3969,7 @@ dependencies = [ [[package]] name = "openfang-hands" -version = "0.4.8" +version = "0.4.9" dependencies = [ "chrono", "dashmap", @@ -3986,7 +3986,7 @@ dependencies = [ [[package]] name = "openfang-kernel" -version = "0.4.8" +version = "0.4.9" dependencies = [ "async-trait", "chrono", @@ -4024,7 +4024,7 @@ dependencies = [ [[package]] name = "openfang-memory" -version = "0.4.8" +version = "0.4.9" dependencies = [ "async-trait", "chrono", @@ -4043,7 +4043,7 @@ dependencies = [ [[package]] name = "openfang-migrate" -version = "0.4.8" +version = "0.4.9" dependencies = [ "chrono", "dirs 6.0.0", @@ -4062,7 +4062,7 @@ dependencies = [ [[package]] name = "openfang-runtime" -version = "0.4.8" +version = "0.4.9" dependencies = [ "anyhow", "async-trait", @@ -4096,7 +4096,7 @@ dependencies = [ [[package]] name = "openfang-skills" -version = "0.4.8" +version = "0.4.9" dependencies = [ "chrono", "hex", @@ -4119,7 +4119,7 @@ dependencies = [ [[package]] name = "openfang-types" -version = "0.4.8" +version = "0.4.9" dependencies = [ "async-trait", "chrono", @@ -4138,7 +4138,7 @@ dependencies = [ [[package]] name = "openfang-wire" -version = "0.4.8" +version = "0.4.9" dependencies = [ "async-trait", "chrono", @@ -8814,7 +8814,7 @@ checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" [[package]] name = "xtask" -version = "0.4.8" +version = "0.4.9" [[package]] name = "yoke" diff --git a/crates/openfang-api/src/routes.rs b/crates/openfang-api/src/routes.rs index 12d8b78c0..7d1c86377 100644 --- a/crates/openfang-api/src/routes.rs +++ b/crates/openfang-api/src/routes.rs @@ -363,7 +363,11 @@ pub async fn send_message( // (not as a separate session message which the LLM may not process). let content_blocks = if !req.attachments.is_empty() { let image_blocks = resolve_attachments(&req.attachments); - if image_blocks.is_empty() { None } else { Some(image_blocks) } + if image_blocks.is_empty() { + None + } else { + Some(image_blocks) + } } else { None }; diff --git a/crates/openfang-channels/src/whatsapp.rs b/crates/openfang-channels/src/whatsapp.rs index 06156ab5d..16f37b56d 100644 --- a/crates/openfang-channels/src/whatsapp.rs +++ b/crates/openfang-channels/src/whatsapp.rs @@ -258,7 +258,8 @@ impl ChannelAdapter for WhatsAppAdapter { "https://graph.facebook.com/v21.0/{}/messages", self.phone_number_id ); - let resp = self.client + let resp = self + .client .post(&api_url) .bearer_auth(&*self.access_token) .json(&body) @@ -284,7 +285,8 @@ impl ChannelAdapter for WhatsAppAdapter { "https://graph.facebook.com/v21.0/{}/messages", self.phone_number_id ); - let resp = self.client + let resp = self + .client .post(&api_url) .bearer_auth(&*self.access_token) .json(&body) @@ -310,7 +312,8 @@ impl ChannelAdapter for WhatsAppAdapter { "https://graph.facebook.com/v21.0/{}/messages", self.phone_number_id ); - let resp = self.client + let resp = self + .client .post(&api_url) .bearer_auth(&*self.access_token) .json(&body) diff --git a/crates/openfang-kernel/src/kernel.rs b/crates/openfang-kernel/src/kernel.rs index 230d52855..6cafe49a2 100644 --- a/crates/openfang-kernel/src/kernel.rs +++ b/crates/openfang-kernel/src/kernel.rs @@ -2003,19 +2003,24 @@ impl OpenFangKernel { // Persist usage to database (same as non-streaming path) let model = &manifest.model.model; let cost = MeteringEngine::estimate_cost_with_catalog( - &kernel_clone.model_catalog.read().unwrap_or_else(|e| e.into_inner()), + &kernel_clone + .model_catalog + .read() + .unwrap_or_else(|e| e.into_inner()), model, result.total_usage.input_tokens, result.total_usage.output_tokens, ); - let _ = kernel_clone.metering.record(&openfang_memory::usage::UsageRecord { - agent_id, - model: model.clone(), - input_tokens: result.total_usage.input_tokens, - output_tokens: result.total_usage.output_tokens, - cost_usd: cost, - tool_calls: result.iterations.saturating_sub(1), - }); + let _ = kernel_clone + .metering + .record(&openfang_memory::usage::UsageRecord { + agent_id, + model: model.clone(), + input_tokens: result.total_usage.input_tokens, + output_tokens: result.total_usage.output_tokens, + cost_usd: cost, + tool_calls: result.iterations.saturating_sub(1), + }); let _ = kernel_clone .registry diff --git a/crates/openfang-memory/src/knowledge.rs b/crates/openfang-memory/src/knowledge.rs index 8c87b0ac8..e4f5c7734 100644 --- a/crates/openfang-memory/src/knowledge.rs +++ b/crates/openfang-memory/src/knowledge.rs @@ -100,11 +100,7 @@ impl KnowledgeStore { let mut idx = 1; if let Some(ref source) = pattern.source { - sql.push_str(&format!( - " AND (s.id = ?{} OR s.name = ?{})", - idx, - idx + 1 - )); + sql.push_str(&format!(" AND (s.id = ?{} OR s.name = ?{})", idx, idx + 1)); params.push(Box::new(source.clone())); params.push(Box::new(source.clone())); idx += 2; @@ -117,11 +113,7 @@ impl KnowledgeStore { idx += 1; } if let Some(ref target) = pattern.target { - sql.push_str(&format!( - " AND (t.id = ?{} OR t.name = ?{})", - idx, - idx + 1 - )); + sql.push_str(&format!(" AND (t.id = ?{} OR t.name = ?{})", idx, idx + 1)); params.push(Box::new(target.clone())); params.push(Box::new(target.clone())); idx += 2; diff --git a/crates/openfang-runtime/src/agent_loop.rs b/crates/openfang-runtime/src/agent_loop.rs index c377584fd..8afc9224d 100644 --- a/crates/openfang-runtime/src/agent_loop.rs +++ b/crates/openfang-runtime/src/agent_loop.rs @@ -57,8 +57,15 @@ fn phantom_action_detected(text: &str) -> bool { let lower = text.to_lowercase(); let action_verbs = ["sent ", "posted ", "emailed ", "delivered ", "forwarded "]; let channel_refs = [ - "telegram", "whatsapp", "slack", "discord", "email", "channel", - "message sent", "successfully sent", "has been sent", + "telegram", + "whatsapp", + "slack", + "discord", + "email", + "channel", + "message sent", + "successfully sent", + "has been sent", ]; let has_action = action_verbs.iter().any(|v| lower.contains(v)); let has_channel = channel_refs.iter().any(|c| lower.contains(c)); @@ -272,7 +279,9 @@ pub async fn run_agent_loop( // The LLM already received them via llm_messages above. for msg in session.messages.iter_mut() { if let MessageContent::Blocks(blocks) = &mut msg.content { - let had_images = blocks.iter().any(|b| matches!(b, ContentBlock::Image { .. })); + let had_images = blocks + .iter() + .any(|b| matches!(b, ContentBlock::Image { .. })); if had_images { blocks.retain(|b| !matches!(b, ContentBlock::Image { .. })); if blocks.is_empty() { @@ -454,7 +463,10 @@ pub async fn run_agent_loop( // One-shot retry: if the LLM returns empty text with no tool use, // try once more before accepting the empty result. // Triggers on first call OR when input_tokens=0 (silently failed request). - if text.trim().is_empty() && response.tool_calls.is_empty() && !response.has_any_content() { + if text.trim().is_empty() + && response.tool_calls.is_empty() + && !response.has_any_content() + { let is_silent_failure = response.usage.input_tokens == 0 && response.usage.output_tokens == 0; if iteration == 0 || is_silent_failure { @@ -499,7 +511,10 @@ pub async fn run_agent_loop( // channel action (send, post, email, etc.) but never actually // called the corresponding tool, re-prompt once to force real // tool usage instead of hallucinated completion. - let text = if !any_tools_executed && iteration == 0 && phantom_action_detected(&text) { + let text = if !any_tools_executed + && iteration == 0 + && phantom_action_detected(&text) + { warn!(agent = %manifest.name, "Phantom action detected — re-prompting for real tool use"); messages.push(Message::assistant(text)); messages.push(Message::user( @@ -1275,7 +1290,9 @@ pub async fn run_agent_loop_streaming( // The LLM already received them via llm_messages above. for msg in session.messages.iter_mut() { if let MessageContent::Blocks(blocks) = &mut msg.content { - let had_images = blocks.iter().any(|b| matches!(b, ContentBlock::Image { .. })); + let had_images = blocks + .iter() + .any(|b| matches!(b, ContentBlock::Image { .. })); if had_images { blocks.retain(|b| !matches!(b, ContentBlock::Image { .. })); if blocks.is_empty() { @@ -1475,7 +1492,10 @@ pub async fn run_agent_loop_streaming( // One-shot retry: if the LLM returns empty text with no tool use, // try once more before accepting the empty result. // Triggers on first call OR when input_tokens=0 (silently failed request). - if text.trim().is_empty() && response.tool_calls.is_empty() && !response.has_any_content() { + if text.trim().is_empty() + && response.tool_calls.is_empty() + && !response.has_any_content() + { let is_silent_failure = response.usage.input_tokens == 0 && response.usage.output_tokens == 0; if iteration == 0 || is_silent_failure { diff --git a/crates/openfang-runtime/src/drivers/anthropic.rs b/crates/openfang-runtime/src/drivers/anthropic.rs index 344b4d463..1cad74055 100644 --- a/crates/openfang-runtime/src/drivers/anthropic.rs +++ b/crates/openfang-runtime/src/drivers/anthropic.rs @@ -471,9 +471,8 @@ impl LlmDriver for AnthropicDriver { input_json, }) = blocks.get(block_idx) { - let input: serde_json::Value = - serde_json::from_str(input_json) - .unwrap_or_else(|_| serde_json::json!({})); + let input: serde_json::Value = serde_json::from_str(input_json) + .unwrap_or_else(|_| serde_json::json!({})); let _ = tx .send(StreamEvent::ToolUseEnd { id: id.clone(), diff --git a/crates/openfang-runtime/src/drivers/mod.rs b/crates/openfang-runtime/src/drivers/mod.rs index b25c6aed5..17b2a5cc5 100644 --- a/crates/openfang-runtime/src/drivers/mod.rs +++ b/crates/openfang-runtime/src/drivers/mod.rs @@ -14,13 +14,13 @@ pub mod qwen_code; use crate::llm_driver::{DriverConfig, LlmDriver, LlmError}; use openfang_types::model_catalog::{ - AI21_BASE_URL, ANTHROPIC_BASE_URL, AZURE_OPENAI_BASE_URL, CEREBRAS_BASE_URL, CHUTES_BASE_URL, - COHERE_BASE_URL, DEEPSEEK_BASE_URL, FIREWORKS_BASE_URL, GEMINI_BASE_URL, GROQ_BASE_URL, - HUGGINGFACE_BASE_URL, KIMI_CODING_BASE_URL, LEMONADE_BASE_URL, LMSTUDIO_BASE_URL, - MINIMAX_BASE_URL, MISTRAL_BASE_URL, MOONSHOT_BASE_URL, NVIDIA_NIM_BASE_URL, OLLAMA_BASE_URL, - OPENAI_BASE_URL, OPENROUTER_BASE_URL, PERPLEXITY_BASE_URL, QIANFAN_BASE_URL, QWEN_BASE_URL, - REPLICATE_BASE_URL, SAMBANOVA_BASE_URL, TOGETHER_BASE_URL, VENICE_BASE_URL, VLLM_BASE_URL, - VOLCENGINE_BASE_URL, VOLCENGINE_CODING_BASE_URL, XAI_BASE_URL, ZAI_BASE_URL, + AI21_BASE_URL, ALIBABA_CODING_BASE_URL, ANTHROPIC_BASE_URL, AZURE_OPENAI_BASE_URL, + CEREBRAS_BASE_URL, CHUTES_BASE_URL, COHERE_BASE_URL, DEEPSEEK_BASE_URL, FIREWORKS_BASE_URL, + GEMINI_BASE_URL, GROQ_BASE_URL, HUGGINGFACE_BASE_URL, KIMI_CODING_BASE_URL, LEMONADE_BASE_URL, + LMSTUDIO_BASE_URL, MINIMAX_BASE_URL, MISTRAL_BASE_URL, MOONSHOT_BASE_URL, NVIDIA_NIM_BASE_URL, + OLLAMA_BASE_URL, OPENAI_BASE_URL, OPENROUTER_BASE_URL, PERPLEXITY_BASE_URL, QIANFAN_BASE_URL, + QWEN_BASE_URL, REPLICATE_BASE_URL, SAMBANOVA_BASE_URL, TOGETHER_BASE_URL, VENICE_BASE_URL, + VLLM_BASE_URL, VOLCENGINE_BASE_URL, VOLCENGINE_CODING_BASE_URL, XAI_BASE_URL, ZAI_BASE_URL, ZAI_CODING_BASE_URL, ZHIPU_BASE_URL, ZHIPU_CODING_BASE_URL, }; use std::sync::Arc; @@ -327,6 +327,33 @@ pub fn create_driver(config: &DriverConfig) -> Result, LlmErr ))); } + // DashScope Coding Plan International — requires User-Agent: OpenClaw/1.0 + // This endpoint provides multi-brand access (Qwen, Zhipu GLM, Moonshot Kimi, MiniMax) + // via a single ALIBABA_CODING_API_KEY. The User-Agent header is required to bypass + // the "Coding Agents only" restriction on the DashScope API. + if provider == "alibaba_coding" { + let api_key = config + .api_key + .clone() + .or_else(|| std::env::var("ALIBABA_CODING_API_KEY").ok()) + .ok_or_else(|| { + LlmError::MissingApiKey( + "Set ALIBABA_CODING_API_KEY environment variable for DashScope Coding Plan" + .to_string(), + ) + })?; + + let base_url = config + .base_url + .clone() + .unwrap_or_else(|| ALIBABA_CODING_BASE_URL.to_string()); + + return Ok(Arc::new( + openai::OpenAIDriver::new(api_key, base_url) + .with_extra_headers(vec![("User-Agent".to_string(), "OpenClaw/1.0".to_string())]), + )); + } + // GitHub Copilot — wraps OpenAI-compatible driver with automatic token exchange. // The CopilotDriver exchanges the GitHub PAT for a Copilot API token on demand, // caches it, and refreshes when expired. @@ -791,9 +818,7 @@ mod tests { let config = DriverConfig { provider: "azure".to_string(), api_key: Some("test-azure-key".to_string()), - base_url: Some( - "https://myresource.openai.azure.com/openai/deployments".to_string(), - ), + base_url: Some("https://myresource.openai.azure.com/openai/deployments".to_string()), skip_permissions: true, }; let driver = create_driver(&config); @@ -805,9 +830,7 @@ mod tests { let config = DriverConfig { provider: "azure".to_string(), api_key: None, - base_url: Some( - "https://myresource.openai.azure.com/openai/deployments".to_string(), - ), + base_url: Some("https://myresource.openai.azure.com/openai/deployments".to_string()), skip_permissions: true, }; let result = create_driver(&config); @@ -843,9 +866,7 @@ mod tests { let config = DriverConfig { provider: "azure-openai".to_string(), api_key: Some("test-azure-key".to_string()), - base_url: Some( - "https://myresource.openai.azure.com/openai/deployments".to_string(), - ), + base_url: Some("https://myresource.openai.azure.com/openai/deployments".to_string()), skip_permissions: true, }; let driver = create_driver(&config); diff --git a/crates/openfang-runtime/src/drivers/openai.rs b/crates/openfang-runtime/src/drivers/openai.rs index 7d54c432c..297a3412e 100644 --- a/crates/openfang-runtime/src/drivers/openai.rs +++ b/crates/openfang-runtime/src/drivers/openai.rs @@ -99,8 +99,7 @@ impl OpenAIDriver { if self.azure_mode { builder = builder.header("api-key", self.api_key.as_str()); } else { - builder = - builder.header("authorization", format!("Bearer {}", self.api_key.as_str())); + builder = builder.header("authorization", format!("Bearer {}", self.api_key.as_str())); } builder } diff --git a/crates/openfang-runtime/src/model_catalog.rs b/crates/openfang-runtime/src/model_catalog.rs index 7ffe402a4..e9d52ae3d 100644 --- a/crates/openfang-runtime/src/model_catalog.rs +++ b/crates/openfang-runtime/src/model_catalog.rs @@ -4,16 +4,16 @@ //! with alias resolution, auth status detection, and pricing lookups. use openfang_types::model_catalog::{ - AuthStatus, ModelCatalogEntry, ModelTier, ProviderInfo, AI21_BASE_URL, ANTHROPIC_BASE_URL, - AZURE_OPENAI_BASE_URL, BEDROCK_BASE_URL, CEREBRAS_BASE_URL, CHUTES_BASE_URL, - COHERE_BASE_URL, DEEPSEEK_BASE_URL, FIREWORKS_BASE_URL, GEMINI_BASE_URL, + AuthStatus, ModelCatalogEntry, ModelTier, ProviderInfo, AI21_BASE_URL, ALIBABA_CODING_BASE_URL, + ANTHROPIC_BASE_URL, AZURE_OPENAI_BASE_URL, BEDROCK_BASE_URL, CEREBRAS_BASE_URL, + CHUTES_BASE_URL, COHERE_BASE_URL, DEEPSEEK_BASE_URL, FIREWORKS_BASE_URL, GEMINI_BASE_URL, GITHUB_COPILOT_BASE_URL, GROQ_BASE_URL, HUGGINGFACE_BASE_URL, KIMI_CODING_BASE_URL, - LEMONADE_BASE_URL, LMSTUDIO_BASE_URL, MINIMAX_BASE_URL, MISTRAL_BASE_URL, - MOONSHOT_BASE_URL, NVIDIA_NIM_BASE_URL, OLLAMA_BASE_URL, OPENAI_BASE_URL, - OPENROUTER_BASE_URL, PERPLEXITY_BASE_URL, QIANFAN_BASE_URL, QWEN_BASE_URL, - REPLICATE_BASE_URL, SAMBANOVA_BASE_URL, TOGETHER_BASE_URL, VENICE_BASE_URL, VLLM_BASE_URL, - VOLCENGINE_BASE_URL, VOLCENGINE_CODING_BASE_URL, XAI_BASE_URL, ZAI_BASE_URL, - ZAI_CODING_BASE_URL, ZHIPU_BASE_URL, ZHIPU_CODING_BASE_URL, + LEMONADE_BASE_URL, LMSTUDIO_BASE_URL, MINIMAX_BASE_URL, MISTRAL_BASE_URL, MOONSHOT_BASE_URL, + NVIDIA_NIM_BASE_URL, OLLAMA_BASE_URL, OPENAI_BASE_URL, OPENROUTER_BASE_URL, + PERPLEXITY_BASE_URL, QIANFAN_BASE_URL, QWEN_BASE_URL, REPLICATE_BASE_URL, SAMBANOVA_BASE_URL, + TOGETHER_BASE_URL, VENICE_BASE_URL, VLLM_BASE_URL, VOLCENGINE_BASE_URL, + VOLCENGINE_CODING_BASE_URL, XAI_BASE_URL, ZAI_BASE_URL, ZAI_CODING_BASE_URL, ZHIPU_BASE_URL, + ZHIPU_CODING_BASE_URL, }; use std::collections::HashMap; @@ -642,7 +642,7 @@ fn builtin_providers() -> Vec { auth_status: AuthStatus::Missing, model_count: 0, }, - // ── Chinese providers (5) ──────────────────────────────────── + // ── Chinese providers (6) ──────────────────────────────────── ProviderInfo { id: "qwen".into(), display_name: "Qwen (Alibaba)".into(), @@ -652,6 +652,15 @@ fn builtin_providers() -> Vec { auth_status: AuthStatus::Missing, model_count: 0, }, + ProviderInfo { + id: "alibaba_coding".into(), + display_name: "DashScope Coding Plan (Intl)".into(), + api_key_env: "ALIBABA_CODING_API_KEY".into(), + base_url: ALIBABA_CODING_BASE_URL.into(), + key_required: true, + auth_status: AuthStatus::Missing, + model_count: 0, + }, ProviderInfo { id: "minimax".into(), display_name: "MiniMax".into(), @@ -3010,6 +3019,133 @@ fn builtin_models() -> Vec { aliases: vec![], }, // ══════════════════════════════════════════════════════════════ + // DashScope Coding Plan — International (8) + // All accessed via ALIBABA_CODING_API_KEY on alibaba_coding provider. + // Model IDs use "alibaba_coding/" format — the provider prefix is + // stripped automatically by strip_provider_prefix() before the API call, + // so the API receives the bare model name (e.g. "glm-5", "kimi-k2.5"). + // This mirrors the OpenRouter pattern and avoids collisions with native providers. + // ══════════════════════════════════════════════════════════════ + // ── Qwen (4) ──────────────────────────────────────────────── + ModelCatalogEntry { + id: "qwen3.5-plus".into(), + display_name: "Qwen 3.5 Plus".into(), + provider: "alibaba_coding".into(), + tier: ModelTier::Smart, + context_window: 1_000_000, + max_output_tokens: 65_536, + input_cost_per_m: 0.40, + output_cost_per_m: 2.40, + supports_tools: true, + supports_vision: true, + supports_streaming: true, + aliases: vec![], + }, + ModelCatalogEntry { + id: "qwen3-max-2026-01-23".into(), + display_name: "Qwen 3 Max (2026-01-23)".into(), + provider: "alibaba_coding".into(), + tier: ModelTier::Frontier, + context_window: 131_072, + max_output_tokens: 32_768, + input_cost_per_m: 4.00, + output_cost_per_m: 16.00, + supports_tools: true, + supports_vision: false, + supports_streaming: true, + aliases: vec![], + }, + ModelCatalogEntry { + id: "qwen3-coder-plus".into(), + display_name: "Qwen 3 Coder Plus".into(), + provider: "alibaba_coding".into(), + tier: ModelTier::Smart, + context_window: 131_072, + max_output_tokens: 32_768, + input_cost_per_m: 0.80, + output_cost_per_m: 3.20, + supports_tools: true, + supports_vision: false, + supports_streaming: true, + aliases: vec!["qwen3-coder".into()], + }, + ModelCatalogEntry { + id: "qwen3-coder-next".into(), + display_name: "Qwen 3 Coder Next".into(), + provider: "alibaba_coding".into(), + tier: ModelTier::Frontier, + context_window: 131_072, + max_output_tokens: 32_768, + input_cost_per_m: 4.00, + output_cost_per_m: 16.00, + supports_tools: true, + supports_vision: false, + supports_streaming: true, + aliases: vec![], + }, + // ── Zhipu / GLM via Coding Plan (2) ───────────────────────── + // API receives "glm-5" / "glm-4.7" after prefix stripping. + ModelCatalogEntry { + id: "alibaba_coding/glm-5".into(), + display_name: "GLM-5 (Coding Plan)".into(), + provider: "alibaba_coding".into(), + tier: ModelTier::Frontier, + context_window: 128_000, + max_output_tokens: 32_768, + input_cost_per_m: 4.00, + output_cost_per_m: 16.00, + supports_tools: true, + supports_vision: false, + supports_streaming: true, + aliases: vec![], + }, + ModelCatalogEntry { + id: "alibaba_coding/glm-4.7".into(), + display_name: "GLM-4.7 (Coding Plan)".into(), + provider: "alibaba_coding".into(), + tier: ModelTier::Smart, + context_window: 128_000, + max_output_tokens: 32_768, + input_cost_per_m: 0.80, + output_cost_per_m: 3.20, + supports_tools: true, + supports_vision: false, + supports_streaming: true, + aliases: vec![], + }, + // ── Moonshot / Kimi via Coding Plan (1) ───────────────────── + // API receives "kimi-k2.5" after prefix stripping. + ModelCatalogEntry { + id: "alibaba_coding/kimi-k2.5".into(), + display_name: "Kimi K2.5 (Coding Plan)".into(), + provider: "alibaba_coding".into(), + tier: ModelTier::Smart, + context_window: 128_000, + max_output_tokens: 32_768, + input_cost_per_m: 0.80, + output_cost_per_m: 3.20, + supports_tools: true, + supports_vision: true, + supports_streaming: true, + aliases: vec![], + }, + // ── MiniMax via Coding Plan (1) ────────────────────────────── + // API receives "MiniMax-M2.5" after prefix stripping. + ModelCatalogEntry { + id: "alibaba_coding/MiniMax-M2.5".into(), + display_name: "MiniMax M2.5 (Coding Plan)".into(), + provider: "alibaba_coding".into(), + tier: ModelTier::Smart, + context_window: 1_000_000, + max_output_tokens: 32_768, + input_cost_per_m: 0.80, + output_cost_per_m: 3.20, + supports_tools: true, + supports_vision: false, + supports_streaming: true, + aliases: vec![], + }, + // ══════════════════════════════════════════════════════════════ // MiniMax (6) // ══════════════════════════════════════════════════════════════ ModelCatalogEntry { @@ -3809,7 +3945,7 @@ mod tests { #[test] fn test_catalog_has_providers() { let catalog = ModelCatalog::new(); - assert_eq!(catalog.list_providers().len(), 41); + assert_eq!(catalog.list_providers().len(), 42); // 41 + alibaba_coding } #[test] diff --git a/crates/openfang-types/src/model_catalog.rs b/crates/openfang-types/src/model_catalog.rs index ea098f6f7..22b68cb87 100644 --- a/crates/openfang-types/src/model_catalog.rs +++ b/crates/openfang-types/src/model_catalog.rs @@ -49,6 +49,8 @@ pub const KIMI_CODING_BASE_URL: &str = "https://api.kimi.com/coding"; pub const QIANFAN_BASE_URL: &str = "https://qianfan.baidubce.com/v2"; pub const VOLCENGINE_BASE_URL: &str = "https://ark.cn-beijing.volces.com/api/v3"; pub const VOLCENGINE_CODING_BASE_URL: &str = "https://ark.cn-beijing.volces.com/api/coding/v3"; +/// DashScope Coding Plan International — accessed via ALIBABA_CODING_API_KEY. +pub const ALIBABA_CODING_BASE_URL: &str = "https://coding-intl.dashscope.aliyuncs.com/v1"; // ── Chutes.ai ──────────────────────────────────────────────────── pub const CHUTES_BASE_URL: &str = "https://llm.chutes.ai/v1";