feat: model dir auto-scan, remote API models, model plaza with HF/ModelScope downloads

This commit is contained in:
Xianren Studio
2026-08-13 15:37:28 +08:00
parent 0cf86c5c39
commit 451cc75cd9
17 changed files with 1132 additions and 239 deletions
+2 -1
View File
@@ -1,8 +1,9 @@
pub mod error;
pub mod manager;
pub mod remote;
pub mod types;
pub use error::{EngineError, Result};
pub use manager::EngineManager;
pub use remote::{stream_chat_remote, RemoteConfig};
pub use types::{ChatMessage, ChatRequest, EngineConfig, EngineStatus};
+1 -1
View File
@@ -203,7 +203,7 @@ fn free_port() -> Result<u16> {
}
/// 把 reqwest 的字节流解析为 SSE 行级文本流(增量 token)。
fn sse_text_stream(
pub(crate) fn sse_text_stream(
bytes: impl Stream<Item = std::result::Result<bytes::Bytes, reqwest::Error>>
+ Unpin
+ Send
+44
View File
@@ -0,0 +1,44 @@
use crate::error::{EngineError, Result};
use crate::manager::sse_text_stream;
use crate::types::ChatRequest;
/// OpenAI 兼容的远程模型端点配置。
#[derive(Debug, Clone)]
pub struct RemoteConfig {
pub base_url: String,
pub api_key: Option<String>,
pub model: String,
}
/// 调用远程 OpenAI 兼容 API 并返回流式增量文本。
pub async fn stream_chat_remote(
cfg: &RemoteConfig,
req: ChatRequest,
) -> Result<futures::stream::BoxStream<'static, Result<String>>> {
let base = normalize_base(&cfg.base_url);
let url = format!("{base}/chat/completions");
let client = reqwest::Client::new();
let mut builder = client.post(&url).json(&req);
if let Some(key) = &cfg.api_key {
if !key.trim().is_empty() {
builder = builder.bearer_auth(key);
}
}
let response = builder.send().await?;
let status = response.status();
if !status.is_success() {
let body = response.text().await.unwrap_or_default();
return Err(EngineError::EngineHttp(status, body));
}
Ok(sse_text_stream(response.bytes_stream()))
}
/// 把用户填写的 base url 归一化为 `<scheme>://<host>/v1` 形式。
fn normalize_base(base: &str) -> String {
let mut s = base.trim().trim_end_matches('/').to_string();
if !s.ends_with("/v1") {
s.push_str("/v1");
}
s
}