Skip to content

Commit 6fbac34

Browse files
committed
🤖 feat: add support for Ollama local models
Integrates ollama-ai-provider-v2 to enable running AI models locally through Ollama without requiring API keys. Changes: - Add ollama-ai-provider-v2 dependency - Implement Ollama provider in aiService.ts with lazy loading - Add OllamaProviderOptions type for future extensibility - Support Ollama model display formatting (e.g., llama3.2:7b -> Llama 3.2 (7B)) - Update providers.jsonc template with Ollama configuration example - Add comprehensive Ollama documentation to models.md - Add unit tests for Ollama model name formatting Ollama is a local service that doesn't require API keys. Users can run any model from the Ollama Library (https://ollama.com/library) locally. Example configuration in ~/.cmux/providers.jsonc: { "ollama": { "baseUrl": "http://localhost:11434" } } Example model usage: ollama:llama3.2:7b _Generated with `cmux`_
1 parent 7ca32f4 commit 6fbac34

File tree

8 files changed

+187
-10
lines changed

8 files changed

+187
-10
lines changed

bun.lock

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"lru-cache": "^11.2.2",
2929
"markdown-it": "^14.1.0",
3030
"minimist": "^1.2.8",
31+
"ollama-ai-provider-v2": "^1.5.3",
3132
"rehype-harden": "^1.1.5",
3233
"shescape": "^2.1.6",
3334
"source-map-support": "^0.5.21",
@@ -2238,6 +2239,8 @@
22382239

22392240
"object.values": ["object.values@1.2.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA=="],
22402241

2242+
"ollama-ai-provider-v2": ["ollama-ai-provider-v2@1.5.3", "", { "dependencies": { "@ai-sdk/provider": "^2.0.0", "@ai-sdk/provider-utils": "^3.0.7" }, "peerDependencies": { "zod": "^4.0.16" } }, "sha512-LnpvKuxNJyE+cB03cfUjFJnaiBJoUqz3X97GFc71gz09gOdrxNh1AsVBxrpw3uX5aiMxRIWPOZ8god0dHSChsg=="],
2243+
22412244
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
22422245

22432246
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],

docs/models.md

Lines changed: 69 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,81 @@ See also:
44

55
- [System Prompt](./system-prompt.md)
66

7-
Currently we support the Sonnet 4 models and GPT-5 family of models:
7+
cmux supports multiple AI providers through its flexible provider architecture.
8+
9+
### Supported Providers
10+
11+
#### Anthropic (Cloud)
12+
13+
Best supported provider with full feature support:
814

915
- `anthropic:claude-sonnet-4-5`
1016
- `anthropic:claude-opus-4-1`
17+
18+
#### OpenAI (Cloud)
19+
20+
GPT-5 family of models:
21+
1122
- `openai:gpt-5`
1223
- `openai:gpt-5-pro`
1324
- `openai:gpt-5-codex`
1425

15-
And we intend to always support the models used by 90% of the community.
16-
17-
Anthropic models are better supported than GPT-5 class models due to an outstanding issue in the
18-
Vercel AI SDK.
26+
**Note:** Anthropic models are better supported than GPT-5 class models due to an outstanding issue in the Vercel AI SDK.
1927

2028
TODO: add issue link here.
29+
30+
#### Ollama (Local)
31+
32+
Run models locally with Ollama. No API key required:
33+
34+
- `ollama:llama3.2:7b`
35+
- `ollama:llama3.2:13b`
36+
- `ollama:codellama:7b`
37+
- `ollama:qwen2.5:7b`
38+
- Any model from the [Ollama Library](https://ollama.com/library)
39+
40+
**Setup:**
41+
42+
1. Install Ollama from [ollama.com](https://ollama.com)
43+
2. Pull a model: `ollama pull llama3.2:7b`
44+
3. Configure in `~/.cmux/providers.jsonc`:
45+
46+
```jsonc
47+
{
48+
"ollama": {
49+
// Default configuration - Ollama runs on localhost:11434
50+
"baseUrl": "http://localhost:11434"
51+
}
52+
}
53+
```
54+
55+
For remote Ollama instances, update `baseUrl` to point to your server.
56+
57+
### Provider Configuration
58+
59+
All providers are configured in `~/.cmux/providers.jsonc`. See example configurations:
60+
61+
```jsonc
62+
{
63+
"anthropic": {
64+
"apiKey": "sk-ant-..."
65+
},
66+
"openai": {
67+
"apiKey": "sk-..."
68+
},
69+
"ollama": {
70+
"baseUrl": "http://localhost:11434" // Default - only needed if different
71+
}
72+
}
73+
```
74+
75+
### Model Selection
76+
77+
Use the Command Palette (`Cmd+Shift+P`) to switch models:
78+
79+
1. Open Command Palette
80+
2. Type "model"
81+
3. Select "Change Model"
82+
4. Choose from available models
83+
84+
Models are specified in the format: `provider:model-name`

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
"lru-cache": "^11.2.2",
7070
"markdown-it": "^14.1.0",
7171
"minimist": "^1.2.8",
72+
"ollama-ai-provider-v2": "^1.5.3",
7273
"rehype-harden": "^1.1.5",
7374
"shescape": "^2.1.6",
7475
"source-map-support": "^0.5.21",

src/config.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -426,8 +426,13 @@ export class Config {
426426
// Example:
427427
// {
428428
// "anthropic": {
429-
// "apiKey": "sk-...",
430-
// "baseUrl": "https://api.anthropic.com"
429+
// "apiKey": "sk-ant-..."
430+
// },
431+
// "openai": {
432+
// "apiKey": "sk-..."
433+
// },
434+
// "ollama": {
435+
// "baseUrl": "http://localhost:11434"
431436
// }
432437
// }
433438
${jsonString}`;

src/services/aiService.ts

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,15 +93,19 @@ if (typeof globalFetchWithExtras.certificate === "function") {
9393

9494
/**
9595
* Preload AI SDK provider modules to avoid race conditions in concurrent test environments.
96-
* This function loads @ai-sdk/anthropic and @ai-sdk/openai eagerly so that subsequent
97-
* dynamic imports in createModel() hit the module cache instead of racing.
96+
* This function loads @ai-sdk/anthropic, @ai-sdk/openai, and ollama-ai-provider-v2 eagerly
97+
* so that subsequent dynamic imports in createModel() hit the module cache instead of racing.
9898
*
9999
* In production, providers are lazy-loaded on first use to optimize startup time.
100100
* In tests, we preload them once during setup to ensure reliable concurrent execution.
101101
*/
102102
export async function preloadAISDKProviders(): Promise<void> {
103103
// Preload providers to ensure they're in the module cache before concurrent tests run
104-
await Promise.all([import("@ai-sdk/anthropic"), import("@ai-sdk/openai")]);
104+
await Promise.all([
105+
import("@ai-sdk/anthropic"),
106+
import("@ai-sdk/openai"),
107+
import("ollama-ai-provider-v2"),
108+
]);
105109
}
106110

107111
export class AIService extends EventEmitter {
@@ -372,6 +376,25 @@ export class AIService extends EventEmitter {
372376
return Ok(model);
373377
}
374378

379+
// Handle Ollama provider
380+
if (providerName === "ollama") {
381+
// Ollama doesn't require API key - it's a local service
382+
// Use custom fetch if provided, otherwise default with unlimited timeout
383+
const baseFetch =
384+
typeof providerConfig.fetch === "function"
385+
? (providerConfig.fetch as typeof fetch)
386+
: defaultFetchWithUnlimitedTimeout;
387+
388+
// Lazy-load Ollama provider to reduce startup time
389+
const { createOllama } = await import("ollama-ai-provider-v2");
390+
const provider = createOllama({
391+
...providerConfig,
392+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment
393+
fetch: baseFetch as any,
394+
});
395+
return Ok(provider(modelId));
396+
}
397+
375398
return Err({
376399
type: "provider_not_supported",
377400
provider: providerName,

src/types/providerOptions.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,20 @@ export interface OpenAIProviderOptions {
2929
simulateToolPolicyNoop?: boolean;
3030
}
3131

32+
/**
33+
* Ollama-specific options
34+
* Currently empty - Ollama is a local service and doesn't require special options.
35+
* This interface is provided for future extensibility.
36+
*/
37+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
38+
export interface OllamaProviderOptions {}
39+
3240
/**
3341
* Cmux provider options - used by both frontend and backend
3442
*/
3543
export interface CmuxProviderOptions {
3644
/** Provider-specific options */
3745
anthropic?: AnthropicProviderOptions;
3846
openai?: OpenAIProviderOptions;
47+
ollama?: OllamaProviderOptions;
3948
}

src/utils/ai/modelDisplay.test.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { describe, expect, test } from "bun:test";
2+
import { formatModelDisplayName } from "./modelDisplay";
3+
4+
describe("formatModelDisplayName", () => {
5+
describe("Claude models", () => {
6+
test("formats Sonnet models", () => {
7+
expect(formatModelDisplayName("claude-sonnet-4-5")).toBe("Sonnet 4.5");
8+
expect(formatModelDisplayName("claude-sonnet-4")).toBe("Sonnet 4");
9+
});
10+
11+
test("formats Opus models", () => {
12+
expect(formatModelDisplayName("claude-opus-4-1")).toBe("Opus 4.1");
13+
});
14+
});
15+
16+
describe("GPT models", () => {
17+
test("formats GPT models", () => {
18+
expect(formatModelDisplayName("gpt-5-pro")).toBe("GPT-5 Pro");
19+
expect(formatModelDisplayName("gpt-4o")).toBe("GPT-4o");
20+
expect(formatModelDisplayName("gpt-4o-mini")).toBe("GPT-4o Mini");
21+
});
22+
});
23+
24+
describe("Gemini models", () => {
25+
test("formats Gemini models", () => {
26+
expect(formatModelDisplayName("gemini-2-0-flash-exp")).toBe("Gemini 2.0 Flash Exp");
27+
});
28+
});
29+
30+
describe("Ollama models", () => {
31+
test("formats Llama models with size", () => {
32+
expect(formatModelDisplayName("llama3.2:7b")).toBe("Llama 3.2 (7B)");
33+
expect(formatModelDisplayName("llama3.2:13b")).toBe("Llama 3.2 (13B)");
34+
});
35+
36+
test("formats Codellama models with size", () => {
37+
expect(formatModelDisplayName("codellama:7b")).toBe("Codellama (7B)");
38+
expect(formatModelDisplayName("codellama:13b")).toBe("Codellama (13B)");
39+
});
40+
41+
test("formats Qwen models with size", () => {
42+
expect(formatModelDisplayName("qwen2.5:7b")).toBe("Qwen 2.5 (7B)");
43+
});
44+
45+
test("handles models without size suffix", () => {
46+
expect(formatModelDisplayName("llama3")).toBe("Llama3");
47+
});
48+
});
49+
50+
describe("fallback formatting", () => {
51+
test("capitalizes dash-separated parts", () => {
52+
expect(formatModelDisplayName("custom-model-name")).toBe("Custom Model Name");
53+
});
54+
});
55+
});

src/utils/ai/modelDisplay.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,23 @@ export function formatModelDisplayName(modelName: string): string {
8585
}
8686
}
8787

88+
// Ollama models - handle format like "llama3.2:7b" or "codellama:13b"
89+
// Split by colon to handle quantization/size suffix
90+
const [baseName, size] = modelName.split(":");
91+
if (size) {
92+
// "llama3.2:7b" -> "Llama 3.2 (7B)"
93+
// "codellama:13b" -> "Codellama (13B)"
94+
const formatted = baseName
95+
.split(/(\d+\.?\d*)/)
96+
.map((part, idx) => {
97+
if (idx === 0) return capitalize(part);
98+
if (/^\d+\.?\d*$/.test(part)) return ` ${part}`;
99+
return part;
100+
})
101+
.join("");
102+
return `${formatted.trim()} (${size.toUpperCase()})`;
103+
}
104+
88105
// Fallback: capitalize first letter of each dash-separated part
89106
return modelName.split("-").map(capitalize).join(" ");
90107
}

0 commit comments

Comments
 (0)