weft_core/api/
openai_compat.rs1use crate::config::AppConfig;
2use crate::pipeline::Pipeline;
3use crate::process::ProcessManager;
4use crate::types::{ApiError, ApiErrorBody, ChatRequest};
5use crate::vkeys::VirtualKeyStore;
6use axum::extract::State;
7use axum::http::StatusCode;
8use axum::response::sse::{Event, KeepAlive, Sse};
9use axum::response::{IntoResponse, Response};
10use axum::Json;
11use futures_util::StreamExt;
12use std::convert::Infallible;
13use std::sync::{Arc, Mutex as StdMutex};
14use tokio::sync::RwLock;
15
16pub type SharedConfig = Arc<RwLock<AppConfig>>;
17pub type SharedPipeline = Arc<Pipeline>;
18
19#[derive(Clone)]
20pub struct AppState {
21 pub config: SharedConfig,
22 pub config_path: std::path::PathBuf,
23 pub pipeline: SharedPipeline,
24 pub process_manager: Arc<ProcessManager>,
25 pub vkey_store: Arc<VirtualKeyStore>,
26 pub package_manager: Arc<RwLock<crate::package::PackageManager>>,
27 pub wasm_handle: Arc<RwLock<Option<crate::package::bridge::WasmHandle>>>,
28 pub native_handle: Arc<RwLock<Option<crate::package::NativeHandle>>>,
29 pub resolved_apps: Arc<RwLock<crate::app::ResolvedAppMap>>,
30 pub capability_registry: Arc<RwLock<crate::app::CapabilityRegistry>>,
31 pub active_profile: Arc<RwLock<crate::app::AppProfile>>,
32 pub core_policy: Arc<crate::app::CorePolicy>,
33 pub generation_store: Arc<RwLock<crate::app::GenerationStoreMap>>,
34 pub package_index: Arc<crate::app::PackageIndex>,
35 pub repo_root: std::path::PathBuf,
36 pub data_dir: std::path::PathBuf,
37 pub runtime_token: Option<String>,
38 pub runtime_token_path: Option<std::path::PathBuf>,
39 pub chat_providers: Arc<RwLock<Vec<ChatProviderInfo>>>,
40 pub shutdown_tx: Arc<StdMutex<Option<tokio::sync::oneshot::Sender<()>>>>,
41 pub stream_buffer: Arc<StdMutex<std::collections::HashMap<String, Vec<String>>>>,
44}
45
46#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
47pub struct ChatProviderInfo {
48 pub name: String,
49 pub endpoint: String,
50 pub description: String,
51}
52
53pub async fn chat_completions(
54 State(state): State<AppState>,
55 Json(request): Json<ChatRequest>,
56) -> Response {
57 if request.stream {
58 return chat_completions_stream(state, request).await;
59 }
60
61 let config = state.config.read().await;
62 match state.pipeline.execute(&request, &config).await {
63 Ok(resp) => Json(resp).into_response(),
64 Err(e) => {
65 let body = ApiError {
66 error: ApiErrorBody {
67 message: e.to_string(),
68 error_type: "proxy_error".into(),
69 code: Some("bad_gateway".into()),
70 },
71 };
72 (StatusCode::BAD_GATEWAY, Json(body)).into_response()
73 }
74 }
75}
76
77async fn chat_completions_stream(state: AppState, request: ChatRequest) -> Response {
78 let config = state.config.read().await.clone();
79
80 let (provider_name, resp) = match state.pipeline.execute_stream(&request, &config).await {
81 Ok(r) => r,
82 Err(e) => {
83 let body = ApiError {
84 error: ApiErrorBody {
85 message: e.to_string(),
86 error_type: "proxy_error".into(),
87 code: Some("bad_gateway".into()),
88 },
89 };
90 return (StatusCode::BAD_GATEWAY, Json(body)).into_response();
91 }
92 };
93
94 let provider = config
95 .providers
96 .iter()
97 .find(|p| p.name == provider_name)
98 .cloned();
99
100 let transforms = state.pipeline.transforms.clone();
101
102 let stream = async_stream::stream! {
103 let mut byte_stream = resp.bytes_stream();
104 let mut buffer: Vec<u8> = Vec::new();
108
109 while let Some(chunk_result) = byte_stream.next().await {
110 let chunk = match chunk_result {
111 Ok(c) => c,
112 Err(e) => {
113 tracing::error!("Stream read error: {}", e);
114 break;
115 }
116 };
117
118 buffer.extend_from_slice(&chunk);
119
120 while let Some(newline_pos) = buffer.iter().position(|&b| b == b'\n') {
121 let line_bytes: Vec<u8> = buffer.drain(..=newline_pos).collect();
122 let line = String::from_utf8_lossy(&line_bytes).to_string();
124
125 let line = line.trim().to_string();
126 if line.is_empty() {
127 continue;
128 }
129
130 if let Some(ref prov) = provider {
131 match transforms.for_format(&prov.format).transform_stream_chunk(&line, prov).await {
132 Ok(Some(chunk)) => {
133 let data = serde_json::to_string(&chunk).unwrap_or_default();
134 yield Ok::<_, Infallible>(Event::default().data(data));
135 }
136 Ok(None) => {
137 if line.contains("[DONE]") {
138 yield Ok(Event::default().data("[DONE]"));
139 }
140 }
141 Err(e) => {
142 tracing::warn!("Stream chunk transform error: {}", e);
143 }
144 }
145 }
146 }
147 }
148 };
149
150 Sse::new(stream)
151 .keep_alive(KeepAlive::default())
152 .into_response()
153}
154
155pub async fn list_models(State(state): State<AppState>) -> Json<serde_json::Value> {
156 let config = state.config.read().await;
157 let models: Vec<serde_json::Value> = config
158 .providers
159 .iter()
160 .flat_map(|p| {
161 p.models.iter().map(move |m| {
162 serde_json::json!({
163 "id": m,
164 "object": "model",
165 "owned_by": p.name,
166 })
167 })
168 })
169 .collect();
170
171 Json(serde_json::json!({
172 "object": "list",
173 "data": models,
174 }))
175}