Allow configuring local AI endpoint and key
swadhinbiswas committed 8 months ago
37142e2
1 parent 7dbf5b1
commit 37142e2
5 files changed
+58 -4
Showing 5 changed files+58-4
▼src/lib/ai-config.ts
+16-2
| @@ -8,6 +8,7 @@ type AIKeys = { | ||
8 | 8 | openrouter?: string; |
9 | 9 | together?: string; |
10 | 10 | google?: string; |
11 | + local?: string; | |
11 | 12 | externalAgent?: string; |
12 | 13 | }; |
13 | 14 | |
| @@ -15,6 +16,7 @@ export interface AIConfig { | ||
15 | 16 | provider: string; |
16 | 17 | model: string; |
17 | 18 | apiKeys: AIKeys; |
19 | + localBaseUrl?: string; | |
18 | 20 | externalAgentWebhookUrl?: string; |
19 | 21 | } |
20 | 22 | |
| @@ -29,8 +31,10 @@ export interface PublicAIConfig { | ||
29 | 31 | openrouter: { isSet: boolean; masked?: string }; |
30 | 32 | together: { isSet: boolean; masked?: string }; |
31 | 33 | google: { isSet: boolean; masked?: string }; |
34 | + local: { isSet: boolean; masked?: string }; | |
32 | 35 | externalAgent: { isSet: boolean; masked?: string }; |
33 | 36 | }; |
37 | + localBaseUrl?: string; | |
34 | 38 | externalAgentWebhookUrl?: string; |
35 | 39 | } |
36 | 40 | |
| @@ -113,7 +117,7 @@ export function parseAIConfigFromStorage(raw: string | null | undefined): AIConf | ||
113 | 117 | }; |
114 | 118 | |
115 | 119 | const decryptedApiKeys: AIKeys = {}; |
116 | - for (const provider of ["openai", "anthropic", "groq", "bytez", "openrouter", "together", "google", "externalAgent"] as const) { | |
120 | + for (const provider of ["openai", "anthropic", "groq", "bytez", "openrouter", "together", "google", "local", "externalAgent"] as const) { | |
117 | 121 | const value = parsed.apiKeys?.[provider]; |
118 | 122 | if (value) { |
119 | 123 | decryptedApiKeys[provider] = decryptValue(value); |
| @@ -124,6 +128,7 @@ export function parseAIConfigFromStorage(raw: string | null | undefined): AIConf | ||
124 | 128 | provider: parsed.provider || DEFAULT_AI_CONFIG.provider, |
125 | 129 | model: parsed.model || DEFAULT_AI_CONFIG.model, |
126 | 130 | apiKeys: decryptedApiKeys, |
131 | + localBaseUrl: typeof parsed.localBaseUrl === "string" ? parsed.localBaseUrl : undefined, | |
127 | 132 | externalAgentWebhookUrl: |
128 | 133 | typeof parsed.externalAgentWebhookUrl === "string" ? parsed.externalAgentWebhookUrl : undefined, |
129 | 134 | }; |
| @@ -134,7 +139,7 @@ export function parseAIConfigFromStorage(raw: string | null | undefined): AIConf | ||
134 | 139 | |
135 | 140 | export function buildStoredAIConfig(config: AIConfig): string { |
136 | 141 | const encryptedApiKeys: AIKeys = {}; |
137 | - for (const provider of ["openai", "anthropic", "groq", "bytez", "openrouter", "together", "google", "externalAgent"] as const) { | |
142 | + for (const provider of ["openai", "anthropic", "groq", "bytez", "openrouter", "together", "google", "local", "externalAgent"] as const) { | |
138 | 143 | const value = config.apiKeys?.[provider]; |
139 | 144 | if (value) { |
140 | 145 | encryptedApiKeys[provider] = encryptValue(value); |
| @@ -145,6 +150,7 @@ export function buildStoredAIConfig(config: AIConfig): string { | ||
145 | 150 | provider: config.provider, |
146 | 151 | model: config.model, |
147 | 152 | apiKeys: encryptedApiKeys, |
153 | + localBaseUrl: config.localBaseUrl, | |
148 | 154 | externalAgentWebhookUrl: config.externalAgentWebhookUrl, |
149 | 155 | }); |
150 | 156 | } |
| @@ -188,11 +194,16 @@ export function sanitizeAIConfigForClient(config: AIConfig): PublicAIConfig { | ||
188 | 194 | isSet: Boolean(config.apiKeys.google), |
189 | 195 | masked: mask(config.apiKeys.google), |
190 | 196 | }, |
197 | + local: { | |
198 | + isSet: Boolean(config.apiKeys.local), | |
199 | + masked: mask(config.apiKeys.local), | |
200 | + }, | |
191 | 201 | externalAgent: { |
192 | 202 | isSet: Boolean(config.apiKeys.externalAgent), |
193 | 203 | masked: mask(config.apiKeys.externalAgent), |
194 | 204 | }, |
195 | 205 | }, |
206 | + localBaseUrl: config.localBaseUrl, | |
196 | 207 | externalAgentWebhookUrl: config.externalAgentWebhookUrl, |
197 | 208 | }; |
198 | 209 | } |
| @@ -203,6 +214,7 @@ export function mergeAIConfig( | ||
203 | 214 | provider?: string; |
204 | 215 | model?: string; |
205 | 216 | apiKeys?: Partial<AIKeys>; |
217 | + localBaseUrl?: string; | |
206 | 218 | externalAgentWebhookUrl?: string; |
207 | 219 | } |
208 | 220 | ): AIConfig { |
| @@ -217,8 +229,10 @@ export function mergeAIConfig( | ||
217 | 229 | openrouter: updates.apiKeys?.openrouter ?? existing.apiKeys.openrouter, |
218 | 230 | together: updates.apiKeys?.together ?? existing.apiKeys.together, |
219 | 231 | google: updates.apiKeys?.google ?? existing.apiKeys.google, |
232 | + local: updates.apiKeys?.local ?? existing.apiKeys.local, | |
220 | 233 | externalAgent: updates.apiKeys?.externalAgent ?? existing.apiKeys.externalAgent, |
221 | 234 | }, |
235 | + localBaseUrl: updates.localBaseUrl ?? existing.localBaseUrl, | |
222 | 236 | externalAgentWebhookUrl: |
223 | 237 | updates.externalAgentWebhookUrl ?? existing.externalAgentWebhookUrl, |
224 | 238 | }; |
225 | 239 | |
▼src/lib/openapi.ts
+2-0
| @@ -1021,6 +1021,7 @@ export const openApiSpec = { | ||
1021 | 1021 | enum: ["openai", "anthropic", "groq", "bytez", "openrouter", "together", "google", "external_agent", "local"], |
1022 | 1022 | }, |
1023 | 1023 | model: { type: "string" }, |
1024 | + localBaseUrl: { type: "string", format: "uri" }, | |
1024 | 1025 | externalAgentWebhookUrl: { type: "string", format: "uri" }, |
1025 | 1026 | apiKeys: { |
1026 | 1027 | type: "object", |
| @@ -1032,6 +1033,7 @@ export const openApiSpec = { | ||
1032 | 1033 | openrouter: { type: "string" }, |
1033 | 1034 | together: { type: "string" }, |
1034 | 1035 | google: { type: "string" }, |
1036 | + local: { type: "string" }, | |
1035 | 1037 | externalAgent: { type: "string" }, |
1036 | 1038 | }, |
1037 | 1039 | }, |
1038 | 1040 | |
▼src/pages/api/repos/[owner]/[repo]/pulls/[number]/ai-review.ts
+5-2
| @@ -141,11 +141,12 @@ export const POST: APIRoute = withErrorHandler(async ({ request, params }) => { | ||
141 | 141 | provider === "bytez" || |
142 | 142 | provider === "openrouter" || |
143 | 143 | provider === "together" || |
144 | - provider === "google" | |
144 | + provider === "google" || | |
145 | + provider === "local" | |
145 | 146 | ? provider |
146 | 147 | : provider === "external_agent" |
147 | 148 | ? "externalAgent" |
148 | - : undefined; | |
149 | + : undefined; | |
149 | 150 | aiConfig = { |
150 | 151 | provider, |
151 | 152 | model: userConfig.model || aiConfig.model, |
| @@ -156,6 +157,8 @@ export const POST: APIRoute = withErrorHandler(async ({ request, params }) => { | ||
156 | 157 | baseUrl: |
157 | 158 | provider === "external_agent" |
158 | 159 | ? (userConfig.externalAgentWebhookUrl || aiConfig.baseUrl) |
160 | + : provider === "local" | |
161 | + ? userConfig.localBaseUrl | |
159 | 162 | : undefined, |
160 | 163 | }; |
161 | 164 | } catch (e) { |
162 | 165 | |
▼src/pages/api/user/ai-config.ts
+5-0
| @@ -43,8 +43,13 @@ export const POST: APIRoute = async ({ request, locals }) => { | ||
43 | 43 | openrouter: typeof apiKeys?.openrouter === "string" && apiKeys.openrouter.trim() !== "" ? apiKeys.openrouter.trim() : undefined, |
44 | 44 | together: typeof apiKeys?.together === "string" && apiKeys.together.trim() !== "" ? apiKeys.together.trim() : undefined, |
45 | 45 | google: typeof apiKeys?.google === "string" && apiKeys.google.trim() !== "" ? apiKeys.google.trim() : undefined, |
46 | + local: typeof apiKeys?.local === "string" && apiKeys.local.trim() !== "" ? apiKeys.local.trim() : undefined, | |
46 | 47 | externalAgent: typeof apiKeys?.externalAgent === "string" && apiKeys.externalAgent.trim() !== "" ? apiKeys.externalAgent.trim() : undefined, |
47 | 48 | }, |
49 | + localBaseUrl: | |
50 | + typeof data?.localBaseUrl === "string" && data.localBaseUrl.trim() !== "" | |
51 | + ? data.localBaseUrl.trim() | |
52 | + : undefined, | |
48 | 53 | externalAgentWebhookUrl: |
49 | 54 | typeof data?.externalAgentWebhookUrl === "string" && data.externalAgentWebhookUrl.trim() !== "" |
50 | 55 | ? data.externalAgentWebhookUrl.trim() |
51 | 56 | |
▼src/pages/settings/ai.astro
+30-0
| @@ -31,8 +31,10 @@ let aiConfig: PublicAIConfig = { | ||
31 | 31 | openrouter: { isSet: false }, |
32 | 32 | together: { isSet: false }, |
33 | 33 | google: { isSet: false }, |
34 | + local: { isSet: false }, | |
34 | 35 | externalAgent: { isSet: false }, |
35 | 36 | }, |
37 | + localBaseUrl: "", | |
36 | 38 | externalAgentWebhookUrl: "", |
37 | 39 | }; |
38 | 40 | |
| @@ -303,6 +305,32 @@ const sideNav = [ | ||
303 | 305 | /> |
304 | 306 | </div> |
305 | 307 | |
308 | + <div class="space-y-2"> | |
309 | + <Label htmlFor="local-base-url" | |
310 | + >Local AI Base URL</Label | |
311 | + > | |
312 | + <Input | |
313 | + id="local-base-url" | |
314 | + name="localBaseUrl" | |
315 | + type="url" | |
316 | + placeholder={aiConfig.localBaseUrl || "http://127.0.0.1:11434/v1"} | |
317 | + value="" | |
318 | + /> | |
319 | + </div> | |
320 | + | |
321 | + <div class="space-y-2"> | |
322 | + <Label htmlFor="local-key" | |
323 | + >Local AI API Key (optional)</Label | |
324 | + > | |
325 | + <Input | |
326 | + id="local-key" | |
327 | + name="localKey" | |
328 | + type="password" | |
329 | + placeholder={aiConfig.apiKeys.local.masked || "ollama"} | |
330 | + value="" | |
331 | + /> | |
332 | + </div> | |
333 | + | |
306 | 334 | <div class="space-y-2"> |
307 | 335 | <Label htmlFor="external-agent-webhook" |
308 | 336 | >External Agent Webhook URL</Label |
| @@ -361,8 +389,10 @@ const sideNav = [ | ||
361 | 389 | openrouter: formData.get("openrouterKey"), |
362 | 390 | together: formData.get("togetherKey"), |
363 | 391 | google: formData.get("googleKey"), |
392 | + local: formData.get("localKey"), | |
364 | 393 | externalAgent: formData.get("externalAgentKey"), |
365 | 394 | }, |
395 | + localBaseUrl: formData.get("localBaseUrl"), | |
366 | 396 | externalAgentWebhookUrl: formData.get("externalAgentWebhookUrl"), |
367 | 397 | }; |
368 | 398 | |
369 | 399 | |