Skip to content

Commit ccba2ed

Browse files
feat(ollama): use official ollama library (RooCodeInc#1859)
Co-authored-by: Saoud Rizwan <[email protected]>
1 parent 0fc2f47 commit ccba2ed

File tree

7 files changed

+152
-17
lines changed

7 files changed

+152
-17
lines changed

package-lock.json

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,7 @@
290290
"isbinaryfile": "^5.0.2",
291291
"mammoth": "^1.8.0",
292292
"monaco-vscode-textmate-theme-converter": "^0.1.7",
293+
"ollama": "^0.5.13",
293294
"open-graph-scraper": "^6.9.0",
294295
"openai": "^4.83.0",
295296
"os-name": "^6.0.0",

src/api/providers/ollama.ts

Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,35 @@
11
import { Anthropic } from "@anthropic-ai/sdk"
2-
import OpenAI from "openai"
2+
import { Message, Ollama } from "ollama"
33
import { ApiHandler } from "../"
44
import { ApiHandlerOptions, ModelInfo, openAiModelInfoSaneDefaults } from "../../shared/api"
5-
import { convertToOpenAiMessages } from "../transform/openai-format"
5+
import { convertToOllamaMessages } from "../transform/ollama-format"
66
import { ApiStream } from "../transform/stream"
77

88
export class OllamaHandler implements ApiHandler {
99
private options: ApiHandlerOptions
10-
private client: OpenAI
10+
private client: Ollama
1111

1212
constructor(options: ApiHandlerOptions) {
1313
this.options = options
14-
this.client = new OpenAI({
15-
baseURL: (this.options.ollamaBaseUrl || "http://localhost:11434") + "/v1",
16-
apiKey: "ollama",
17-
})
14+
this.client = new Ollama({ host: this.options.ollamaBaseUrl || "http://localhost:11434" })
1815
}
1916

2017
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
21-
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
22-
{ role: "system", content: systemPrompt },
23-
...convertToOpenAiMessages(messages),
24-
]
18+
const ollamaMessages: Message[] = [{ role: "system", content: systemPrompt }, ...convertToOllamaMessages(messages)]
2519

26-
const stream = await this.client.chat.completions.create({
20+
const stream = await this.client.chat({
2721
model: this.getModel().id,
28-
messages: openAiMessages,
29-
temperature: 0,
22+
messages: ollamaMessages,
3023
stream: true,
24+
options: {
25+
num_ctx: Number(this.options.ollamaApiOptionsCtxNum) || 32768,
26+
},
3127
})
3228
for await (const chunk of stream) {
33-
const delta = chunk.choices[0]?.delta
34-
if (delta?.content) {
29+
if (typeof chunk.message.content === "string") {
3530
yield {
3631
type: "text",
37-
text: delta.content,
32+
text: chunk.message.content,
3833
}
3934
}
4035
}

src/api/transform/ollama-format.ts

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { Anthropic } from "@anthropic-ai/sdk"
2+
import { Message } from "ollama"
3+
4+
export function convertToOllamaMessages(anthropicMessages: Anthropic.Messages.MessageParam[]): Message[] {
5+
const ollamaMessages: Message[] = []
6+
7+
for (const anthropicMessage of anthropicMessages) {
8+
if (typeof anthropicMessage.content === "string") {
9+
ollamaMessages.push({
10+
role: anthropicMessage.role,
11+
content: anthropicMessage.content,
12+
})
13+
} else {
14+
if (anthropicMessage.role === "user") {
15+
const { nonToolMessages, toolMessages } = anthropicMessage.content.reduce<{
16+
nonToolMessages: (Anthropic.TextBlockParam | Anthropic.ImageBlockParam)[]
17+
toolMessages: Anthropic.ToolResultBlockParam[]
18+
}>(
19+
(acc, part) => {
20+
if (part.type === "tool_result") {
21+
acc.toolMessages.push(part)
22+
} else if (part.type === "text" || part.type === "image") {
23+
acc.nonToolMessages.push(part)
24+
}
25+
return acc
26+
},
27+
{ nonToolMessages: [], toolMessages: [] },
28+
)
29+
30+
// Process tool result messages FIRST since they must follow the tool use messages
31+
let toolResultImages: string[] = []
32+
toolMessages.forEach((toolMessage) => {
33+
// The Anthropic SDK allows tool results to be a string or an array of text and image blocks, enabling rich and structured content. In contrast, the Ollama SDK only supports tool results as a single string, so we map the Anthropic tool result parts into one concatenated string to maintain compatibility.
34+
let content: string
35+
36+
if (typeof toolMessage.content === "string") {
37+
content = toolMessage.content
38+
} else {
39+
content =
40+
toolMessage.content
41+
?.map((part) => {
42+
if (part.type === "image") {
43+
toolResultImages.push(`data:${part.source.media_type};base64,${part.source.data}`)
44+
return "(see following user message for image)"
45+
}
46+
return part.text
47+
})
48+
.join("\n") ?? ""
49+
}
50+
ollamaMessages.push({
51+
role: "user",
52+
images: toolResultImages.length > 0 ? toolResultImages : undefined,
53+
content: content,
54+
})
55+
})
56+
57+
// Process non-tool messages
58+
if (nonToolMessages.length > 0) {
59+
ollamaMessages.push({
60+
role: "user",
61+
content: nonToolMessages
62+
.map((part) => {
63+
if (part.type === "image") {
64+
return `data:${part.source.media_type};base64,${part.source.data}`
65+
}
66+
return part.text
67+
})
68+
.join("\n"),
69+
})
70+
}
71+
} else if (anthropicMessage.role === "assistant") {
72+
const { nonToolMessages, toolMessages } = anthropicMessage.content.reduce<{
73+
nonToolMessages: (Anthropic.TextBlockParam | Anthropic.ImageBlockParam)[]
74+
toolMessages: Anthropic.ToolUseBlockParam[]
75+
}>(
76+
(acc, part) => {
77+
if (part.type === "tool_use") {
78+
acc.toolMessages.push(part)
79+
} else if (part.type === "text" || part.type === "image") {
80+
acc.nonToolMessages.push(part)
81+
} // assistant cannot send tool_result messages
82+
return acc
83+
},
84+
{ nonToolMessages: [], toolMessages: [] },
85+
)
86+
87+
// Process non-tool messages
88+
let content: string = ""
89+
if (nonToolMessages.length > 0) {
90+
content = nonToolMessages
91+
.map((part) => {
92+
if (part.type === "image") {
93+
return "" // impossible as the assistant cannot send images
94+
}
95+
return part.text
96+
})
97+
.join("\n")
98+
}
99+
100+
ollamaMessages.push({
101+
role: "assistant",
102+
content,
103+
})
104+
}
105+
}
106+
}
107+
108+
return ollamaMessages
109+
}

src/core/webview/ClineProvider.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ type GlobalStateKey =
7979
| "openAiModelInfo"
8080
| "ollamaModelId"
8181
| "ollamaBaseUrl"
82+
| "ollamaApiOptionsCtxNum"
8283
| "lmStudioModelId"
8384
| "lmStudioBaseUrl"
8485
| "anthropicBaseUrl"
@@ -579,6 +580,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
579580
openAiModelInfo,
580581
ollamaModelId,
581582
ollamaBaseUrl,
583+
ollamaApiOptionsCtxNum,
582584
lmStudioModelId,
583585
lmStudioBaseUrl,
584586
anthropicBaseUrl,
@@ -624,6 +626,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
624626
await this.updateGlobalState("openAiModelInfo", openAiModelInfo)
625627
await this.updateGlobalState("ollamaModelId", ollamaModelId)
626628
await this.updateGlobalState("ollamaBaseUrl", ollamaBaseUrl)
629+
await this.updateGlobalState("ollamaApiOptionsCtxNum", ollamaApiOptionsCtxNum)
627630
await this.updateGlobalState("lmStudioModelId", lmStudioModelId)
628631
await this.updateGlobalState("lmStudioBaseUrl", lmStudioBaseUrl)
629632
await this.updateGlobalState("anthropicBaseUrl", anthropicBaseUrl)
@@ -1880,6 +1883,7 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
18801883
openAiModelInfo,
18811884
ollamaModelId,
18821885
ollamaBaseUrl,
1886+
ollamaApiOptionsCtxNum,
18831887
lmStudioModelId,
18841888
lmStudioBaseUrl,
18851889
anthropicBaseUrl,
@@ -1938,6 +1942,7 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
19381942
this.getGlobalState("openAiModelInfo") as Promise<ModelInfo | undefined>,
19391943
this.getGlobalState("ollamaModelId") as Promise<string | undefined>,
19401944
this.getGlobalState("ollamaBaseUrl") as Promise<string | undefined>,
1945+
this.getGlobalState("ollamaApiOptionsCtxNum") as Promise<string | undefined>,
19411946
this.getGlobalState("lmStudioModelId") as Promise<string | undefined>,
19421947
this.getGlobalState("lmStudioBaseUrl") as Promise<string | undefined>,
19431948
this.getGlobalState("anthropicBaseUrl") as Promise<string | undefined>,
@@ -2019,6 +2024,7 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
20192024
openAiModelInfo,
20202025
ollamaModelId,
20212026
ollamaBaseUrl,
2027+
ollamaApiOptionsCtxNum,
20222028
lmStudioModelId,
20232029
lmStudioBaseUrl,
20242030
anthropicBaseUrl,

src/shared/api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ export interface ApiHandlerOptions {
4444
openAiModelInfo?: ModelInfo
4545
ollamaModelId?: string
4646
ollamaBaseUrl?: string
47+
ollamaApiOptionsCtxNum?: string
4748
lmStudioModelId?: string
4849
lmStudioBaseUrl?: string
4950
geminiApiKey?: string

webview-ui/src/components/settings/ApiOptions.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1133,6 +1133,13 @@ const ApiOptions = ({ showModelOptions, apiErrorMessage, modelIdErrorMessage, is
11331133
placeholder={"e.g. llama3.1"}>
11341134
<span style={{ fontWeight: 500 }}>Model ID</span>
11351135
</VSCodeTextField>
1136+
<VSCodeTextField
1137+
value={apiConfiguration?.ollamaApiOptionsCtxNum || "32768"}
1138+
style={{ width: "100%" }}
1139+
onInput={handleInputChange("ollamaApiOptionsCtxNum")}
1140+
placeholder={"e.g. 32768"}>
1141+
<span style={{ fontWeight: 500 }}>Model Context Window</span>
1142+
</VSCodeTextField>
11361143
{ollamaModels.length > 0 && (
11371144
<VSCodeRadioGroup
11381145
value={

0 commit comments

Comments
 (0)