1use super::{
16 Error, JsonSnafu, ModelListParams, Schema, SchemaAllowCreate, SchemaAllowEdit, SchemaType,
17 SchemaView, SqlxSnafu, Status, format_datetime, new_schema_options,
18};
19use serde::{Deserialize, Serialize};
20use snafu::ResultExt;
21use sqlx::FromRow;
22use sqlx::{Pool, Postgres, QueryBuilder};
23use std::collections::HashMap;
24use tibba_model::Model;
25use time::PrimitiveDateTime;
26
27type Result<T> = std::result::Result<T, Error>;
28
29pub const LLM_PROVIDER_OPENAI: &str = "openai";
31pub const LLM_PROVIDER_ANTHROPIC: &str = "anthropic";
32
33#[derive(FromRow)]
34struct TokenLlmSchema {
35 id: i64,
36 name: String,
37 url: String,
38 model: String,
39 api_key: String,
40 provider: String,
41 status: i16,
42 remark: String,
43 created: PrimitiveDateTime,
44 modified: PrimitiveDateTime,
45}
46
47#[derive(Debug, Clone, Deserialize, Serialize)]
48pub struct TokenLlm {
49 pub id: i64,
50 pub name: String,
51 pub url: String,
52 pub model: String,
53 pub api_key: String,
54 pub provider: String,
56 pub status: i16,
57 pub remark: String,
58 pub created: String,
59 pub modified: String,
60}
61
62impl From<TokenLlmSchema> for TokenLlm {
63 fn from(s: TokenLlmSchema) -> Self {
64 Self {
65 id: s.id,
66 name: s.name,
67 url: s.url,
68 model: s.model,
69 api_key: s.api_key,
70 provider: s.provider,
71 status: s.status,
72 remark: s.remark,
73 created: format_datetime(s.created),
74 modified: format_datetime(s.modified),
75 }
76 }
77}
78
79#[derive(Debug, Clone, Deserialize)]
80pub struct TokenLlmInsertParams {
81 pub name: String,
82 pub url: String,
83 pub model: String,
84 pub api_key: String,
85 pub provider: Option<String>,
86 pub status: Option<i16>,
87 pub remark: Option<String>,
88}
89
90#[derive(Debug, Clone, Deserialize, Default)]
91pub struct TokenLlmUpdateParams {
92 pub url: Option<String>,
93 pub model: Option<String>,
94 pub api_key: Option<String>,
95 pub provider: Option<String>,
96 pub status: Option<i16>,
97 pub remark: Option<String>,
98}
99
100#[derive(Default)]
101pub struct TokenLlmModel {}
102
103impl TokenLlmModel {
104 pub async fn get_by_name(&self, pool: &Pool<Postgres>, name: &str) -> Result<Option<TokenLlm>> {
106 let result = sqlx::query_as::<_, TokenLlmSchema>(
107 r#"SELECT * FROM token_llms
108 WHERE name = $1 AND status = 1 AND deleted_at IS NULL
109 LIMIT 1"#,
110 )
111 .bind(name)
112 .fetch_optional(pool)
113 .await
114 .context(SqlxSnafu)?;
115
116 if result.is_some() {
117 return Ok(result.map(Into::into));
118 }
119
120 if name != "default" {
122 let fallback = sqlx::query_as::<_, TokenLlmSchema>(
123 r#"SELECT * FROM token_llms
124 WHERE name = 'default' AND status = 1 AND deleted_at IS NULL
125 LIMIT 1"#,
126 )
127 .fetch_optional(pool)
128 .await
129 .context(SqlxSnafu)?;
130 return Ok(fallback.map(Into::into));
131 }
132
133 Ok(None)
134 }
135}
136
137impl Model for TokenLlmModel {
138 type Output = TokenLlm;
139 fn new() -> Self {
140 Self::default()
141 }
142
143 async fn schema_view(&self, _pool: &Pool<Postgres>) -> SchemaView {
144 SchemaView {
145 schemas: vec![
146 Schema::new_id(),
147 Schema {
148 name: "name".to_string(),
149 category: SchemaType::String,
150 required: true,
151 fixed: true,
152 filterable: true,
153 ..Default::default()
154 },
155 Schema {
156 name: "url".to_string(),
157 category: SchemaType::String,
158 required: true,
159 ..Default::default()
160 },
161 Schema {
162 name: "model".to_string(),
163 category: SchemaType::String,
164 required: true,
165 filterable: true,
166 options: Some(new_schema_options(&["mimo-v2.5-pro"])),
167 ..Default::default()
168 },
169 Schema {
170 name: "api_key".to_string(),
171 category: SchemaType::String,
172 required: true,
173 ..Default::default()
174 },
175 Schema {
176 name: "provider".to_string(),
177 category: SchemaType::String,
178 filterable: true,
179 options: Some(new_schema_options(&[
180 LLM_PROVIDER_OPENAI,
181 LLM_PROVIDER_ANTHROPIC,
182 ])),
183 default_value: Some(serde_json::json!(LLM_PROVIDER_OPENAI)),
184 ..Default::default()
185 },
186 Schema::new_status(),
187 Schema::new_remark(),
188 Schema::new_created(),
189 Schema::new_modified(),
190 ],
191 allow_edit: SchemaAllowEdit {
192 roles: vec!["su".to_string()],
193 ..Default::default()
194 },
195 allow_create: SchemaAllowCreate {
196 roles: vec!["su".to_string()],
197 ..Default::default()
198 },
199 }
200 }
201
202 async fn insert(&self, pool: &Pool<Postgres>, data: serde_json::Value) -> Result<u64> {
203 let p: TokenLlmInsertParams = serde_json::from_value(data).context(JsonSnafu)?;
204 let provider = p
205 .provider
206 .filter(|s| !s.is_empty())
207 .unwrap_or_else(|| LLM_PROVIDER_OPENAI.to_string());
208 let row: (i64,) = sqlx::query_as(
209 r#"INSERT INTO token_llms
210 (name, url, model, api_key, provider, status, remark)
211 VALUES ($1, $2, $3, $4, $5, $6, $7)
212 RETURNING id"#,
213 )
214 .bind(&p.name)
215 .bind(&p.url)
216 .bind(&p.model)
217 .bind(&p.api_key)
218 .bind(provider)
219 .bind(p.status.unwrap_or(Status::Enabled as i16))
220 .bind(p.remark.unwrap_or_default())
221 .fetch_one(pool)
222 .await
223 .context(SqlxSnafu)?;
224 Ok(row.0 as u64)
225 }
226
227 async fn get_by_id(&self, pool: &Pool<Postgres>, id: u64) -> Result<Option<Self::Output>> {
228 let result = sqlx::query_as::<_, TokenLlmSchema>(
229 r#"SELECT * FROM token_llms WHERE id = $1 AND deleted_at IS NULL"#,
230 )
231 .bind(id as i64)
232 .fetch_optional(pool)
233 .await
234 .context(SqlxSnafu)?;
235 Ok(result.map(Into::into))
236 }
237
238 async fn update_by_id(
239 &self,
240 pool: &Pool<Postgres>,
241 id: u64,
242 data: serde_json::Value,
243 ) -> Result<()> {
244 let p: TokenLlmUpdateParams = serde_json::from_value(data).context(JsonSnafu)?;
245 let mut qb: QueryBuilder<Postgres> =
246 QueryBuilder::new("UPDATE token_llms SET modified = NOW()");
247 if let Some(v) = p.url {
248 qb.push(", url = ").push_bind(v);
249 }
250 if let Some(v) = p.model {
251 qb.push(", model = ").push_bind(v);
252 }
253 if let Some(v) = p.api_key {
254 qb.push(", api_key = ").push_bind(v);
255 }
256 if let Some(v) = p.provider {
257 qb.push(", provider = ").push_bind(v);
258 }
259 if let Some(v) = p.status {
260 qb.push(", status = ").push_bind(v);
261 }
262 if let Some(v) = p.remark {
263 qb.push(", remark = ").push_bind(v);
264 }
265 qb.push(" WHERE id = ")
266 .push_bind(id as i64)
267 .push(" AND deleted_at IS NULL");
268 qb.build().execute(pool).await.context(SqlxSnafu)?;
269 Ok(())
270 }
271
272 async fn delete_by_id(&self, pool: &Pool<Postgres>, id: u64) -> Result<()> {
273 sqlx::query(
274 r#"UPDATE token_llms SET deleted_at = NOW(), modified = NOW() WHERE id = $1 AND deleted_at IS NULL"#,
275 )
276 .bind(id as i64)
277 .execute(pool)
278 .await
279 .context(SqlxSnafu)?;
280 Ok(())
281 }
282
283 async fn count(&self, pool: &Pool<Postgres>, params: &ModelListParams) -> Result<i64> {
284 let mut qb: QueryBuilder<Postgres> = QueryBuilder::new("SELECT COUNT(*) FROM token_llms");
285 self.push_conditions(&mut qb, params)?;
286 let row: (i64,) = qb
287 .build_query_as()
288 .fetch_one(pool)
289 .await
290 .context(SqlxSnafu)?;
291 Ok(row.0)
292 }
293
294 async fn list(
295 &self,
296 pool: &Pool<Postgres>,
297 params: &ModelListParams,
298 ) -> Result<Vec<Self::Output>> {
299 let mut qb: QueryBuilder<Postgres> = QueryBuilder::new("SELECT * FROM token_llms");
300 self.push_conditions(&mut qb, params)?;
301 params.push_pagination(&mut qb);
302 let rows = qb
303 .build_query_as::<TokenLlmSchema>()
304 .fetch_all(pool)
305 .await
306 .context(SqlxSnafu)?;
307 Ok(rows.into_iter().map(Into::into).collect())
308 }
309
310 fn push_filter_conditions<'args>(
311 &self,
312 qb: &mut QueryBuilder<'args, Postgres>,
313 filters: &HashMap<String, String>,
314 ) -> Result<()> {
315 if let Some(name) = filters.get("name") {
316 qb.push(" AND name = ").push_bind(name.clone());
317 }
318 if let Some(model) = filters.get("model") {
319 qb.push(" AND model = ").push_bind(model.clone());
320 }
321 if let Some(provider) = filters.get("provider") {
322 qb.push(" AND provider = ").push_bind(provider.clone());
323 }
324 if let Some(status) = filters.get("status") {
325 if let Ok(v) = status.parse::<i16>() {
326 qb.push(" AND status = ").push_bind(v);
327 }
328 }
329 Ok(())
330 }
331}