Allow configuring local AI endpoint and key

swadhinbiswas committed 8 months ago
37142e2
1 parent 7dbf5b1
commit 37142e2
5 files changed
+58 -4
▼src/lib/ai-config.ts
+16-2
8
8
openrouter?: string;
9
9
together?: string;
10
10
google?: string;
11
+ local?: string;
11
12
externalAgent?: string;
12
13
};
13
14
15
16
provider: string;
16
17
model: string;
17
18
apiKeys: AIKeys;
19
+ localBaseUrl?: string;
18
20
externalAgentWebhookUrl?: string;
19
21
}
20
22
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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