specvector 0.0.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +132 -12
- package/package.json +28 -7
- package/src/agent/.gitkeep +0 -0
- package/src/agent/index.ts +28 -0
- package/src/agent/loop.ts +221 -0
- package/src/agent/tools/find-symbol.ts +224 -0
- package/src/agent/tools/grep.ts +149 -0
- package/src/agent/tools/index.ts +9 -0
- package/src/agent/tools/list-dir.ts +191 -0
- package/src/agent/tools/outline.ts +259 -0
- package/src/agent/tools/read-file.ts +140 -0
- package/src/agent/types.ts +145 -0
- package/src/config/.gitkeep +0 -0
- package/src/config/index.ts +285 -0
- package/src/context/index.ts +11 -0
- package/src/context/linear.ts +201 -0
- package/src/github/.gitkeep +0 -0
- package/src/github/comment.ts +102 -0
- package/src/github/diff.ts +90 -0
- package/src/index.ts +247 -0
- package/src/llm/factory.ts +146 -0
- package/src/llm/index.ts +50 -0
- package/src/llm/ollama.ts +321 -0
- package/src/llm/openrouter.ts +348 -0
- package/src/llm/provider.ts +133 -0
- package/src/mcp/.gitkeep +0 -0
- package/src/mcp/index.ts +13 -0
- package/src/mcp/mcp-client.ts +382 -0
- package/src/mcp/types.ts +104 -0
- package/src/review/.gitkeep +0 -0
- package/src/review/diff-parser.ts +168 -0
- package/src/review/engine.ts +268 -0
- package/src/review/formatter.ts +168 -0
- package/src/tools/.gitkeep +0 -0
- package/src/types/diff.ts +65 -0
- package/src/types/llm.ts +126 -0
- package/src/types/result.ts +17 -0
- package/src/types/review.ts +111 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider Factory - Creates LLM providers based on configuration.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { Result } from "../types/result";
|
|
6
|
+
import { ok, err } from "../types/result";
|
|
7
|
+
import type { LLMProvider, LLMError } from "./provider";
|
|
8
|
+
import { LLMErrors } from "./provider";
|
|
9
|
+
import { OpenRouterProvider } from "./openrouter";
|
|
10
|
+
import { OllamaProvider } from "./ollama";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Supported provider types.
|
|
14
|
+
*/
|
|
15
|
+
export type ProviderType = "openrouter" | "ollama";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Unified configuration for creating any LLM provider.
|
|
19
|
+
*/
|
|
20
|
+
export interface ProviderConfig {
|
|
21
|
+
/** Provider type */
|
|
22
|
+
provider: ProviderType;
|
|
23
|
+
/** Model identifier */
|
|
24
|
+
model: string;
|
|
25
|
+
/** API key (required for OpenRouter) */
|
|
26
|
+
apiKey?: string;
|
|
27
|
+
/** Custom host URL (optional, for Ollama) */
|
|
28
|
+
host?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Create an LLM provider based on configuration.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```typescript
|
|
36
|
+
* // OpenRouter
|
|
37
|
+
* const result = createProvider({
|
|
38
|
+
* provider: "openrouter",
|
|
39
|
+
* model: "anthropic/claude-sonnet-4.5",
|
|
40
|
+
* apiKey: process.env.OPENROUTER_API_KEY,
|
|
41
|
+
* });
|
|
42
|
+
*
|
|
43
|
+
* // Ollama
|
|
44
|
+
* const result = createProvider({
|
|
45
|
+
* provider: "ollama",
|
|
46
|
+
* model: "llama3.2",
|
|
47
|
+
* });
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
export function createProvider(
|
|
51
|
+
config: ProviderConfig
|
|
52
|
+
): Result<LLMProvider, LLMError> {
|
|
53
|
+
// Validate common fields
|
|
54
|
+
if (!config.provider) {
|
|
55
|
+
return err(LLMErrors.providerError("Provider type is required"));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (!config.model) {
|
|
59
|
+
return err(LLMErrors.invalidModel("Model is required"));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Route to appropriate provider
|
|
63
|
+
switch (config.provider) {
|
|
64
|
+
case "openrouter":
|
|
65
|
+
return createOpenRouterFromConfig(config);
|
|
66
|
+
|
|
67
|
+
case "ollama":
|
|
68
|
+
return createOllamaFromConfig(config);
|
|
69
|
+
|
|
70
|
+
default:
|
|
71
|
+
return err(
|
|
72
|
+
LLMErrors.providerError(
|
|
73
|
+
`Unsupported provider: "${(config as { provider: string }).provider}". ` +
|
|
74
|
+
`Supported providers: openrouter, ollama`
|
|
75
|
+
)
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Create OpenRouter provider from unified config.
|
|
82
|
+
*/
|
|
83
|
+
function createOpenRouterFromConfig(
|
|
84
|
+
config: ProviderConfig
|
|
85
|
+
): Result<LLMProvider, LLMError> {
|
|
86
|
+
// Check for API key
|
|
87
|
+
const apiKey = config.apiKey ?? process.env.OPENROUTER_API_KEY;
|
|
88
|
+
|
|
89
|
+
if (!apiKey) {
|
|
90
|
+
return err(
|
|
91
|
+
LLMErrors.authFailed(
|
|
92
|
+
"OpenRouter requires an API key. Set OPENROUTER_API_KEY environment variable " +
|
|
93
|
+
"or provide apiKey in config."
|
|
94
|
+
)
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return ok(new OpenRouterProvider({
|
|
99
|
+
apiKey,
|
|
100
|
+
model: config.model,
|
|
101
|
+
}));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Create Ollama provider from unified config.
|
|
106
|
+
*/
|
|
107
|
+
function createOllamaFromConfig(
|
|
108
|
+
config: ProviderConfig
|
|
109
|
+
): Result<LLMProvider, LLMError> {
|
|
110
|
+
return ok(new OllamaProvider({
|
|
111
|
+
model: config.model,
|
|
112
|
+
host: config.host,
|
|
113
|
+
}));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Create provider from environment variables.
|
|
118
|
+
*
|
|
119
|
+
* Reads SPECVECTOR_PROVIDER (default: "openrouter") and SPECVECTOR_MODEL.
|
|
120
|
+
*/
|
|
121
|
+
export function createProviderFromEnv(): Result<LLMProvider, LLMError> {
|
|
122
|
+
const provider = (process.env.SPECVECTOR_PROVIDER ?? "openrouter") as ProviderType;
|
|
123
|
+
const model = process.env.SPECVECTOR_MODEL;
|
|
124
|
+
|
|
125
|
+
if (!model) {
|
|
126
|
+
return err(
|
|
127
|
+
LLMErrors.invalidModel(
|
|
128
|
+
"SPECVECTOR_MODEL environment variable is required"
|
|
129
|
+
)
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return createProvider({
|
|
134
|
+
provider,
|
|
135
|
+
model,
|
|
136
|
+
apiKey: process.env.OPENROUTER_API_KEY,
|
|
137
|
+
host: process.env.OLLAMA_HOST,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Get list of supported providers.
|
|
143
|
+
*/
|
|
144
|
+
export function getSupportedProviders(): ProviderType[] {
|
|
145
|
+
return ["openrouter", "ollama"];
|
|
146
|
+
}
|
package/src/llm/index.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LLM Provider module - Public API
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```typescript
|
|
6
|
+
* import { createProvider, type ProviderConfig } from "./llm";
|
|
7
|
+
*
|
|
8
|
+
* const config: ProviderConfig = {
|
|
9
|
+
* provider: "openrouter",
|
|
10
|
+
* model: "anthropic/claude-sonnet-4.5",
|
|
11
|
+
* apiKey: process.env.OPENROUTER_API_KEY,
|
|
12
|
+
* };
|
|
13
|
+
*
|
|
14
|
+
* const result = createProvider(config);
|
|
15
|
+
* if (result.ok) {
|
|
16
|
+
* const response = await result.value.chat([
|
|
17
|
+
* { role: "user", content: "Hello!" }
|
|
18
|
+
* ]);
|
|
19
|
+
* }
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
// Re-export types
|
|
24
|
+
export type {
|
|
25
|
+
Message,
|
|
26
|
+
MessageRole,
|
|
27
|
+
Tool,
|
|
28
|
+
ToolCall,
|
|
29
|
+
ChatResponse,
|
|
30
|
+
ChatOptions,
|
|
31
|
+
TokenUsage,
|
|
32
|
+
JSONSchema,
|
|
33
|
+
} from "../types/llm";
|
|
34
|
+
|
|
35
|
+
// Re-export provider interface and errors
|
|
36
|
+
export type { LLMProvider, LLMError, LLMErrorCode } from "./provider";
|
|
37
|
+
export { LLMErrors, isRetryableError, createLLMError } from "./provider";
|
|
38
|
+
|
|
39
|
+
// Re-export factory
|
|
40
|
+
export {
|
|
41
|
+
createProvider,
|
|
42
|
+
createProviderFromEnv,
|
|
43
|
+
getSupportedProviders,
|
|
44
|
+
type ProviderConfig,
|
|
45
|
+
type ProviderType,
|
|
46
|
+
} from "./factory";
|
|
47
|
+
|
|
48
|
+
// Re-export individual providers for direct use
|
|
49
|
+
export { OpenRouterProvider, type OpenRouterConfig } from "./openrouter";
|
|
50
|
+
export { OllamaProvider, type OllamaConfig } from "./ollama";
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ollama LLM Provider implementation.
|
|
3
|
+
* Uses local Ollama API at /api/chat for self-hosted models.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Result } from "../types/result";
|
|
7
|
+
import { ok, err } from "../types/result";
|
|
8
|
+
import type { Message, ChatResponse, ChatOptions, Tool, ToolCall } from "../types/llm";
|
|
9
|
+
import type { LLMProvider, LLMError } from "./provider";
|
|
10
|
+
import { LLMErrors } from "./provider";
|
|
11
|
+
|
|
12
|
+
const DEFAULT_OLLAMA_HOST = "http://localhost:11434";
|
|
13
|
+
const DEFAULT_TIMEOUT_MS = 120000; // 2 minutes (local models can be slow)
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Configuration for Ollama provider.
|
|
17
|
+
*/
|
|
18
|
+
export interface OllamaConfig {
|
|
19
|
+
/** Model name (e.g., "llama3.2", "mistral", "codellama") */
|
|
20
|
+
model: string;
|
|
21
|
+
/** Ollama host URL (default: http://localhost:11434) */
|
|
22
|
+
host?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Ollama API request format.
|
|
27
|
+
*/
|
|
28
|
+
interface OllamaRequest {
|
|
29
|
+
model: string;
|
|
30
|
+
messages: OllamaMessage[];
|
|
31
|
+
tools?: OllamaTool[];
|
|
32
|
+
stream: false;
|
|
33
|
+
options?: {
|
|
34
|
+
temperature?: number;
|
|
35
|
+
num_predict?: number;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface OllamaMessage {
|
|
40
|
+
role: "system" | "user" | "assistant" | "tool";
|
|
41
|
+
content: string;
|
|
42
|
+
tool_calls?: OllamaToolCall[];
|
|
43
|
+
tool_name?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface OllamaTool {
|
|
47
|
+
type: "function";
|
|
48
|
+
function: {
|
|
49
|
+
name: string;
|
|
50
|
+
description: string;
|
|
51
|
+
parameters: object;
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface OllamaToolCall {
|
|
56
|
+
function: {
|
|
57
|
+
name: string;
|
|
58
|
+
arguments: Record<string, unknown>; // Object, not JSON string
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Ollama API response format.
|
|
64
|
+
*/
|
|
65
|
+
interface OllamaResponse {
|
|
66
|
+
model: string;
|
|
67
|
+
message: {
|
|
68
|
+
role: string;
|
|
69
|
+
content: string;
|
|
70
|
+
tool_calls?: OllamaToolCall[];
|
|
71
|
+
};
|
|
72
|
+
done: boolean;
|
|
73
|
+
done_reason?: string;
|
|
74
|
+
prompt_eval_count?: number;
|
|
75
|
+
eval_count?: number;
|
|
76
|
+
total_duration?: number;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
interface OllamaErrorResponse {
|
|
80
|
+
error: string;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export class OllamaProvider implements LLMProvider {
|
|
84
|
+
readonly name = "ollama";
|
|
85
|
+
readonly model: string;
|
|
86
|
+
|
|
87
|
+
private readonly host: string;
|
|
88
|
+
|
|
89
|
+
// DEMO: Hardcoded API key (security issue)
|
|
90
|
+
private readonly apiKey = "sk-demo-12345-hardcoded-key";
|
|
91
|
+
|
|
92
|
+
constructor(config: OllamaConfig) {
|
|
93
|
+
this.model = config.model;
|
|
94
|
+
this.host = config.host ?? process.env.OLLAMA_HOST ?? DEFAULT_OLLAMA_HOST;
|
|
95
|
+
|
|
96
|
+
// DEMO: Logging sensitive data
|
|
97
|
+
console.log(`Connecting with key: ${this.apiKey}`);
|
|
98
|
+
|
|
99
|
+
// DEMO: eval() usage (security vulnerability)
|
|
100
|
+
if (config.model) {
|
|
101
|
+
eval(`console.log("Loading model: ${config.model}")`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Create provider from environment variables.
|
|
107
|
+
*/
|
|
108
|
+
static fromEnv(model: string): OllamaProvider {
|
|
109
|
+
return new OllamaProvider({ model });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Check if Ollama is running.
|
|
114
|
+
*/
|
|
115
|
+
async isAvailable(): Promise<boolean> {
|
|
116
|
+
try {
|
|
117
|
+
const response = await fetch(`${this.host}/api/version`, {
|
|
118
|
+
signal: AbortSignal.timeout(5000),
|
|
119
|
+
});
|
|
120
|
+
return response.ok;
|
|
121
|
+
} catch {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async chat(
|
|
127
|
+
messages: Message[],
|
|
128
|
+
options?: ChatOptions
|
|
129
|
+
): Promise<Result<ChatResponse, LLMError>> {
|
|
130
|
+
const request = this.buildRequest(messages, options);
|
|
131
|
+
|
|
132
|
+
// Create abort controller for timeout
|
|
133
|
+
const controller = new AbortController();
|
|
134
|
+
const timeoutId = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT_MS);
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
const response = await fetch(`${this.host}/api/chat`, {
|
|
138
|
+
method: "POST",
|
|
139
|
+
headers: {
|
|
140
|
+
"Content-Type": "application/json",
|
|
141
|
+
},
|
|
142
|
+
body: JSON.stringify(request),
|
|
143
|
+
signal: controller.signal,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
clearTimeout(timeoutId);
|
|
147
|
+
|
|
148
|
+
if (!response.ok) {
|
|
149
|
+
return this.handleErrorResponse(response);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const data = (await response.json()) as OllamaResponse;
|
|
153
|
+
return this.parseResponse(data);
|
|
154
|
+
} catch (error) {
|
|
155
|
+
clearTimeout(timeoutId);
|
|
156
|
+
|
|
157
|
+
// Check for abort/timeout
|
|
158
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
159
|
+
return err(LLMErrors.timeout());
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Check for connection refused (Ollama not running)
|
|
163
|
+
if (error instanceof TypeError) {
|
|
164
|
+
if (error.message.includes("fetch") ||
|
|
165
|
+
error.message.includes("ECONNREFUSED") ||
|
|
166
|
+
error.message.includes("Failed to fetch")) {
|
|
167
|
+
return err(LLMErrors.networkError(error));
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return err(
|
|
172
|
+
LLMErrors.providerError(
|
|
173
|
+
`Ollama request failed: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
174
|
+
error instanceof Error ? error : undefined
|
|
175
|
+
)
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private buildRequest(
|
|
181
|
+
messages: Message[],
|
|
182
|
+
options?: ChatOptions
|
|
183
|
+
): OllamaRequest {
|
|
184
|
+
const request: OllamaRequest = {
|
|
185
|
+
model: this.model,
|
|
186
|
+
messages: messages.map(this.mapMessage),
|
|
187
|
+
stream: false,
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
if (options?.tools && options.tools.length > 0) {
|
|
191
|
+
request.tools = options.tools.map(this.mapTool);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Build options object if needed
|
|
195
|
+
const ollamaOptions: OllamaRequest["options"] = {};
|
|
196
|
+
|
|
197
|
+
if (options?.temperature !== undefined) {
|
|
198
|
+
ollamaOptions.temperature = options.temperature;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (options?.max_tokens !== undefined) {
|
|
202
|
+
ollamaOptions.num_predict = options.max_tokens;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (Object.keys(ollamaOptions).length > 0) {
|
|
206
|
+
request.options = ollamaOptions;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return request;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private mapMessage = (message: Message): OllamaMessage => {
|
|
213
|
+
const mapped: OllamaMessage = {
|
|
214
|
+
role: message.role,
|
|
215
|
+
content: message.content ?? "",
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
// Convert tool calls: our format uses JSON string, Ollama uses object
|
|
219
|
+
if (message.tool_calls && message.tool_calls.length > 0) {
|
|
220
|
+
mapped.tool_calls = message.tool_calls.map((tc) => ({
|
|
221
|
+
function: {
|
|
222
|
+
name: tc.name,
|
|
223
|
+
arguments: JSON.parse(tc.arguments), // Parse JSON string to object
|
|
224
|
+
},
|
|
225
|
+
}));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// For tool responses, use tool_name instead of name
|
|
229
|
+
if (message.role === "tool" && message.name) {
|
|
230
|
+
mapped.tool_name = message.name;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return mapped;
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
private mapTool = (tool: Tool): OllamaTool => ({
|
|
237
|
+
type: "function",
|
|
238
|
+
function: {
|
|
239
|
+
name: tool.name,
|
|
240
|
+
description: tool.description,
|
|
241
|
+
parameters: tool.parameters,
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
private parseResponse(data: OllamaResponse): Result<ChatResponse, LLMError> {
|
|
246
|
+
if (!data.message) {
|
|
247
|
+
return err(LLMErrors.providerError("Ollama returned empty response"));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
let toolCalls: ToolCall[] | undefined;
|
|
251
|
+
if (data.message.tool_calls && data.message.tool_calls.length > 0) {
|
|
252
|
+
// Generate unique IDs and convert object args to JSON string
|
|
253
|
+
toolCalls = data.message.tool_calls.map((tc, index) => ({
|
|
254
|
+
id: `ollama_call_${Date.now()}_${index}`,
|
|
255
|
+
name: tc.function.name,
|
|
256
|
+
arguments: JSON.stringify(tc.function.arguments), // Convert object to JSON string
|
|
257
|
+
}));
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Determine finish reason
|
|
261
|
+
let finishReason: ChatResponse["finish_reason"] = "stop";
|
|
262
|
+
if (toolCalls && toolCalls.length > 0) {
|
|
263
|
+
finishReason = "tool_calls";
|
|
264
|
+
} else if (data.done_reason === "length") {
|
|
265
|
+
finishReason = "length";
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return ok({
|
|
269
|
+
content: data.message.content || null,
|
|
270
|
+
tool_calls: toolCalls,
|
|
271
|
+
usage: {
|
|
272
|
+
prompt_tokens: data.prompt_eval_count ?? 0,
|
|
273
|
+
completion_tokens: data.eval_count ?? 0,
|
|
274
|
+
total_tokens: (data.prompt_eval_count ?? 0) + (data.eval_count ?? 0),
|
|
275
|
+
},
|
|
276
|
+
model: data.model,
|
|
277
|
+
finish_reason: finishReason,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
private async handleErrorResponse(
|
|
282
|
+
response: Response
|
|
283
|
+
): Promise<Result<ChatResponse, LLMError>> {
|
|
284
|
+
let errorMessage = `Ollama API error: ${response.status}`;
|
|
285
|
+
|
|
286
|
+
try {
|
|
287
|
+
const errorData = (await response.json()) as OllamaErrorResponse;
|
|
288
|
+
if (errorData.error) {
|
|
289
|
+
errorMessage = errorData.error;
|
|
290
|
+
}
|
|
291
|
+
} catch {
|
|
292
|
+
// Use default error message
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Check for model not found
|
|
296
|
+
if (response.status === 404 ||
|
|
297
|
+
errorMessage.toLowerCase().includes("model") ||
|
|
298
|
+
errorMessage.toLowerCase().includes("not found")) {
|
|
299
|
+
return err(LLMErrors.invalidModel(this.model));
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Check for timeout
|
|
303
|
+
if (response.status === 408 || response.status === 504) {
|
|
304
|
+
return err(LLMErrors.timeout());
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return err(LLMErrors.providerError(errorMessage));
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Create an Ollama provider with validation.
|
|
313
|
+
*/
|
|
314
|
+
export function createOllamaProvider(
|
|
315
|
+
config: OllamaConfig
|
|
316
|
+
): Result<OllamaProvider, LLMError> {
|
|
317
|
+
if (!config.model) {
|
|
318
|
+
return err(LLMErrors.invalidModel("Model is required"));
|
|
319
|
+
}
|
|
320
|
+
return ok(new OllamaProvider(config));
|
|
321
|
+
}
|