Add local Ollama-compatible AI provider support
swadhinbiswas committed 8 months ago
7dbf5b1
1 parent 7ceb46b
commit 7dbf5b1
5 files changed
+60 -3
Showing 5 changed files+60-3
▼doc/feature_audit.md
+1-1
| @@ -71,7 +71,7 @@ | ||
71 | 71 | | Required approval policies | ✅ | Via branch protection rules | |
72 | 72 | | Partial file approvals | ✅ | Implemented via API + PR UI (`partial-file-approvals`, `file-approvals.ts`) | |
73 | 73 | | Multi-reviewer rules | ⚠️ | Merge gates now enforce explicit required-reviewer approvals; broader state/rule UX still maturing | |
74 | -| AI code review | ✅ | Multi-provider adapters (OpenAI/Anthropic/Groq/Bytez/OpenRouter/Together/Google) + external-agent async callback flow | | |
74 | +| AI code review | ✅ | Multi-provider adapters (OpenAI/Anthropic/Groq/Bytez/OpenRouter/Together/Google/Local-Ollama) + external-agent async callback flow | | |
75 | 75 | |
76 | 76 | ## 4. Issues & Planning |
77 | 77 | | Feature | Status | Implementation Notes | |
78 | 78 | |
▼src/lib/ai/index.ts
+3-0
| @@ -6,6 +6,7 @@ import { OpenRouterAdapter } from "./openrouter"; | ||
6 | 6 | import { TogetherAdapter } from "./together"; |
7 | 7 | import { GoogleAdapter } from "./google"; |
8 | 8 | import { ExternalAgentAdapter } from "./external-agent"; |
9 | +import { LocalAdapter } from "./local"; | |
9 | 10 | import type { AIAdapter } from "./types"; |
10 | 11 | |
11 | 12 | export function getAIAdapter(provider: string): AIAdapter { |
| @@ -24,6 +25,8 @@ export function getAIAdapter(provider: string): AIAdapter { | ||
24 | 25 | return new GoogleAdapter(); |
25 | 26 | case "external_agent": |
26 | 27 | return new ExternalAgentAdapter(); |
28 | + case "local": | |
29 | + return new LocalAdapter(); | |
27 | 30 | case "anthropic": |
28 | 31 | return new AnthropicAdapter(); |
29 | 32 | default: |
30 | 33 | |
▼src/lib/ai/local.ts
+39-0
| @@ -0,0 +1,39 @@ | ||
1 | +import OpenAI from "openai"; | |
2 | +import type { AIAdapter, AICompletionResult, AIConfig, AIReviewPrompt } from "./types"; | |
3 | +import { logger } from "@/lib/logger"; | |
4 | + | |
5 | +export class LocalAdapter implements AIAdapter { | |
6 | + async complete(prompt: AIReviewPrompt, config: AIConfig): Promise<AICompletionResult> { | |
7 | + const baseURL = config.baseUrl || process.env.LOCAL_AI_BASE_URL || "http://127.0.0.1:11434/v1"; | |
8 | + const apiKey = config.apiKey || process.env.LOCAL_AI_API_KEY || "ollama"; | |
9 | + | |
10 | + const client = new OpenAI({ | |
11 | + apiKey, | |
12 | + baseURL, | |
13 | + }); | |
14 | + | |
15 | + const model = config.model || "llama3.1"; | |
16 | + logger.info({ model, provider: "local", baseURL }, "Calling local OpenAI-compatible model"); | |
17 | + | |
18 | + const response = await client.chat.completions.create({ | |
19 | + model, | |
20 | + messages: [ | |
21 | + { role: "system", content: prompt.system }, | |
22 | + { role: "user", content: prompt.user }, | |
23 | + ], | |
24 | + temperature: config.temperature ?? 0.2, | |
25 | + }); | |
26 | + | |
27 | + const content = response.choices[0]?.message?.content || "{}"; | |
28 | + const usage = response.usage || { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }; | |
29 | + | |
30 | + return { | |
31 | + content, | |
32 | + usage: { | |
33 | + inputTokens: usage.prompt_tokens, | |
34 | + outputTokens: usage.completion_tokens, | |
35 | + totalTokens: usage.total_tokens, | |
36 | + }, | |
37 | + }; | |
38 | + } | |
39 | +} | |
0 | 40 | |
▼src/pages/settings/ai.astro
+3-2
| @@ -163,7 +163,7 @@ const sideNav = [ | ||
163 | 163 | value="local" |
164 | 164 | selected={aiConfig.provider === |
165 | 165 | "local"} |
166 | - >Local LLM (Coming Soon)</option | |
166 | + >Local LLM (Ollama/OpenAI-compatible)</option | |
167 | 167 | > |
168 | 168 | </select> |
169 | 169 | </div> |
| @@ -180,7 +180,8 @@ const sideNav = [ | ||
180 | 180 | Leave blank to use provider default. |
181 | 181 | Recommended: `gpt-4-turbo` (OpenAI), |
182 | 182 | `llama3-70b-8192` (Groq), |
183 | - `meta-llama/llama-3.1-8b-instruct:free` (OpenRouter). | |
183 | + `meta-llama/llama-3.1-8b-instruct:free` (OpenRouter), | |
184 | + `llama3.1` (Local). | |
184 | 185 | </p> |
185 | 186 | </div> |
186 | 187 | </CardContent> |
187 | 188 | |
▼tests/unit/ai-adapters.test.ts
+14-0
| @@ -0,0 +1,14 @@ | ||
1 | +import { describe, expect, it } from "vitest"; | |
2 | +import { getAIAdapter } from "@/lib/ai"; | |
3 | +import { LocalAdapter } from "@/lib/ai/local"; | |
4 | + | |
5 | +describe("ai adapter registry", () => { | |
6 | + it("returns local adapter for local provider", () => { | |
7 | + const adapter = getAIAdapter("local"); | |
8 | + expect(adapter).toBeInstanceOf(LocalAdapter); | |
9 | + }); | |
10 | + | |
11 | + it("throws for unknown providers", () => { | |
12 | + expect(() => getAIAdapter("unknown-provider")).toThrow(/Unknown AI provider/i); | |
13 | + }); | |
14 | +}); | |
0 | 15 | |