1use serde::{Deserialize, Serialize};
9use sha2::{Digest, Sha256};
10
11use crate::app::{AppCtx, AppError};
12
13const KEY_LIMIT: f64 = 5.0;
15const KEY_LIMIT_RESET: &str = "daily";
17
18pub const OPENROUTER_API_URL: &str = "https://openrouter.ai/api/v1";
20
21#[derive(Serialize)]
23struct CreateKeyRequest {
24 name: String,
25 limit: f64,
26 limit_reset: String,
27}
28
29#[derive(Deserialize)]
31struct CreateKeyData {
32 hash: String,
33}
34
35#[derive(Deserialize)]
37struct CreateKeyResponse {
38 key: String,
39 data: CreateKeyData,
40}
41
42pub async fn get_inference_key(ctx: &AppCtx) -> Result<String, AppError> {
44 let Some(user) = &ctx.user else {
45 return Err(AppError::Unauthorized);
46 };
47
48 let provisioning_key = ctx
49 .state
50 .openrouter_provisioning_key
51 .as_ref()
52 .ok_or(AppError::InferenceUnavailable)?;
53
54 get_or_create_key(
55 &ctx.state.db,
56 &ctx.state.http_client,
57 &ctx.state.openrouter_base_url,
58 provisioning_key,
59 &user.user_id,
60 )
61 .await
62}
63
64pub async fn invalidate_inference_key(
66 db: &sqlx::PgPool,
67 http_client: &reqwest::Client,
68 base_url: &str,
69 provisioning_key: &str,
70 identifier: &str,
71) -> Result<(), AppError> {
72 let row = sqlx::query!(
73 "SELECT id, inference_hash FROM users WHERE id = $1 OR username = $1",
74 identifier,
75 )
76 .fetch_optional(db)
77 .await?;
78
79 let Some(row) = row else {
80 return Ok(());
81 };
82
83 let Some(hash) = row.inference_hash else {
84 return Ok(());
85 };
86
87 openrouter_delete_key(http_client, provisioning_key, base_url, &hash).await?;
88
89 sqlx::query!(
90 "UPDATE users SET inference_key = NULL, inference_hash = NULL WHERE id = $1",
91 row.id,
92 )
93 .execute(db)
94 .await?;
95
96 Ok(())
97}
98
99async fn get_or_create_key(
101 db: &sqlx::PgPool,
102 http_client: &reqwest::Client,
103 base_url: &str,
104 provisioning_key: &str,
105 user_id: &str,
106) -> Result<String, AppError> {
107 let existing_key: Option<String> =
108 sqlx::query_scalar!("SELECT inference_key FROM users WHERE id = $1", user_id,)
109 .fetch_one(db)
110 .await?;
111
112 if let Some(key) = existing_key {
113 return Ok(key);
114 }
115
116 let response = openrouter_create_key(
117 http_client,
118 provisioning_key,
119 base_url,
120 &key_name(user_id, provisioning_key),
121 )
122 .await?;
123
124 sqlx::query!(
125 "UPDATE users SET inference_key = $2, inference_hash = $3 WHERE id = $1",
126 user_id,
127 response.key,
128 response.data.hash,
129 )
130 .execute(db)
131 .await?;
132
133 Ok(response.key)
134}
135
136async fn openrouter_create_key(
138 client: &reqwest::Client,
139 provisioning_key: &str,
140 base_url: &str,
141 name: &str,
142) -> Result<CreateKeyResponse, AppError> {
143 let request = CreateKeyRequest {
144 name: name.to_string(),
145 limit: KEY_LIMIT,
146 limit_reset: KEY_LIMIT_RESET.to_string(),
147 };
148
149 let body = serde_json::to_vec(&request)
150 .map_err(|e| AppError::OpenRouter(format!("failed to serialize request: {e}")))?;
151
152 let response = client
153 .post(format!("{base_url}/keys"))
154 .bearer_auth(provisioning_key)
155 .header("Content-Type", "application/json")
156 .body(body)
157 .send()
158 .await
159 .map_err(|e| AppError::OpenRouter(e.to_string()))?;
160
161 if !response.status().is_success() {
162 let status = response.status();
163 let body = response.text().await.unwrap_or_default();
164 return Err(AppError::OpenRouter(format!("OpenRouter returned {status}: {body}")));
165 }
166
167 let bytes = response.bytes().await.map_err(|e| AppError::OpenRouter(e.to_string()))?;
168
169 serde_json::from_slice::<CreateKeyResponse>(&bytes)
170 .map_err(|e| AppError::OpenRouter(format!("failed to parse key response: {e}")))
171}
172
173async fn openrouter_delete_key(
179 client: &reqwest::Client,
180 provisioning_key: &str,
181 base_url: &str,
182 hash: &str,
183) -> Result<(), AppError> {
184 let response = client
185 .delete(format!("{base_url}/keys/{hash}"))
186 .bearer_auth(provisioning_key)
187 .send()
188 .await
189 .map_err(|e| AppError::OpenRouter(e.to_string()))?;
190
191 let status = response.status();
192
193 if status.is_success() || status.as_u16() == 404 {
194 return Ok(());
195 }
196
197 let body = response.text().await.unwrap_or_default();
198 Err(AppError::OpenRouter(format!("OpenRouter returned {status}: {body}")))
199}
200
201fn key_name(user_id: &str, provisioning_key: &str) -> String {
204 let hash = Sha256::new()
205 .chain_update(user_id.as_bytes())
206 .chain_update(provisioning_key.as_bytes())
207 .finalize();
208 let hex: String = hash[..8].iter().map(|b| format!("{b:02x}")).collect();
209 format!("catcolab:{hex}")
210}