Skip to content

Commit 2fa75aa

Browse files
committed
feat: 添加 Ollama 原生供应商适配
1 parent 2545dca commit 2fa75aa

37 files changed

Lines changed: 2918 additions & 104 deletions

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
| **Web Search** | 内置网页搜索工具, 支持 bing 和 brave 搜索 | [文档](https://ccb.agent-aura.top/docs/features/web-browser-tool) |
2727
| **Poor Mode** | 穷鬼模式,关闭记忆提取和键入建议,大幅度减少并发请求 | /poor 可以开关 |
2828
| **Channels 频道通知** | MCP 服务器推送外部消息到会话(飞书/Slack/Discord/微信等),`--channels plugin:name@marketplace` 启用 | [文档](https://ccb.agent-aura.top/docs/features/channels) |
29-
| **自定义模型供应商** | OpenAI/Anthropic/Gemini/Grok 兼容 (`/login`) | [文档](https://ccb.agent-aura.top/docs/features/all-features-guide) |
29+
| **自定义模型供应商** | OpenAI/Anthropic/Gemini/Grok/Ollama 兼容 (`/login`) | [文档](https://ccb.agent-aura.top/docs/features/all-features-guide) / [Ollama](docs/features/ollama-provider.md) |
3030
| Voice Mode | 语音输入,支持豆包语言输入(`/voice doubao`| [文档](https://ccb.agent-aura.top/docs/features/voice-mode) |
3131
| Computer Use | 屏幕截图、键鼠控制 | [文档](https://ccb.agent-aura.top/docs/features/computer-use) |
3232
| Chrome Use | 浏览器自动化、表单填写、数据抓取 | [自托管](https://ccb.agent-aura.top/docs/features/chrome-use-mcp) [原生版](https://ccb.agent-aura.top/docs/features/claude-in-chrome-mcp) |

docs/features/ollama-provider.md

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# Ollama Native Provider
2+
3+
Claude Code Best supports Ollama through the native Ollama API, not the
4+
OpenAI-compatible endpoint. This lets Cloud and local Ollama use the same
5+
request shape for chat, tool calling, thinking, model discovery, and web
6+
utilities.
7+
8+
## Configure
9+
10+
Use `/login` and choose `Ollama`, or configure these environment variables:
11+
12+
```bash
13+
CLAUDE_CODE_USE_OLLAMA=1
14+
OLLAMA_API_KEY=ollama_api_key
15+
OLLAMA_BASE_URL=https://ollama.com/api
16+
OLLAMA_DEFAULT_HAIKU_MODEL=qwen3:cloud
17+
OLLAMA_DEFAULT_SONNET_MODEL=qwen3-coder
18+
OLLAMA_DEFAULT_OPUS_MODEL=glm-4.7:cloud
19+
```
20+
21+
`OLLAMA_API_KEY` is required for direct Ollama Cloud API access. It is not
22+
required for local Ollama. For local Ollama, set:
23+
24+
```bash
25+
OLLAMA_BASE_URL=http://localhost:11434/api
26+
```
27+
28+
If `OLLAMA_BASE_URL` is omitted, Claude Code Best uses
29+
`https://ollama.com/api`.
30+
31+
## Model Mapping
32+
33+
Ollama model routing uses the same three Claude model families shown by
34+
`/model`:
35+
36+
- `OLLAMA_DEFAULT_HAIKU_MODEL`
37+
- `OLLAMA_DEFAULT_SONNET_MODEL`
38+
- `OLLAMA_DEFAULT_OPUS_MODEL`
39+
40+
There is no global `OLLAMA_MODEL` override. This keeps Ollama behavior aligned
41+
with other third-party providers, where Haiku/Sonnet/Opus can map to different
42+
backend models.
43+
44+
When a direct Ollama model name is selected from `/model` or `--model`, it is
45+
sent to Ollama unchanged. When a Claude family model is selected, Claude Code
46+
Best maps it through the matching `OLLAMA_DEFAULT_*_MODEL` variable. If no
47+
family mapping is configured, the fallback is `qwen3-coder`.
48+
49+
## Supported Features
50+
51+
- Native `POST /api/chat` streaming
52+
- Ollama tool calling through `tools`
53+
- Ollama thinking through `think`
54+
- Native `POST /api/web_search`
55+
- Native `POST /api/web_fetch`
56+
- Dynamic context length discovery through `POST /api/show`
57+
- Local Ollama and Ollama Cloud through the same provider
58+
59+
The provider reads model context length from `model_info.*.context_length` or
60+
the `num_ctx` parameter returned by `/api/show`, then uses that value to choose
61+
the request output limit.
62+
63+
## Known Limits
64+
65+
Ollama does not expose an official Cloud quota or remaining-balance API in the
66+
documented native API. Claude Code Best therefore does not show Ollama Cloud
67+
remaining quota.
68+
69+
Anthropic-only server tools are not sent directly to Ollama. Web search and web
70+
fetch are handled client-side through Ollama's native web APIs when the Ollama
71+
provider is active.

packages/@ant/model-provider/src/index.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,17 @@ export * from './types/index.js'
2626
export { resolveOpenAIModel } from './providers/openai/modelMapping.js'
2727
export { resolveGrokModel } from './providers/grok/modelMapping.js'
2828
export { resolveGeminiModel } from './providers/gemini/modelMapping.js'
29+
export { resolveOllamaModel } from './providers/ollama/modelMapping.js'
30+
export { anthropicMessagesToOllama } from './providers/ollama/convertMessages.js'
31+
export { anthropicToolsToOllama } from './providers/ollama/convertTools.js'
32+
export { adaptOllamaStreamToAnthropic } from './providers/ollama/streamAdapter.js'
33+
export type {
34+
OllamaChatChunk,
35+
OllamaChatRequest,
36+
OllamaMessage,
37+
OllamaTool,
38+
OllamaToolCall,
39+
} from './providers/ollama/types.js'
2940

3041
// Gemini provider utilities
3142
export { anthropicMessagesToGemini } from './providers/gemini/convertMessages.js'
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { describe, expect, test } from 'bun:test'
2+
import { anthropicMessagesToOllama } from '../convertMessages.js'
3+
import type { SystemPrompt } from '../../../types/systemPrompt.js'
4+
5+
describe('anthropicMessagesToOllama', () => {
6+
test('converts system, text, tool use, and tool result messages', () => {
7+
const result = anthropicMessagesToOllama(
8+
[
9+
{
10+
type: 'user',
11+
message: {
12+
role: 'user',
13+
content: [{ type: 'text', text: 'weather?' }],
14+
},
15+
} as any,
16+
{
17+
type: 'assistant',
18+
message: {
19+
role: 'assistant',
20+
content: [
21+
{
22+
type: 'tool_use',
23+
id: 'toolu_1',
24+
name: 'get_weather',
25+
input: { city: 'Paris' },
26+
},
27+
],
28+
},
29+
} as any,
30+
{
31+
type: 'user',
32+
message: {
33+
role: 'user',
34+
content: [
35+
{
36+
type: 'tool_result',
37+
tool_use_id: 'toolu_1',
38+
content: 'sunny',
39+
},
40+
],
41+
},
42+
} as any,
43+
],
44+
['You are concise.'] as unknown as SystemPrompt,
45+
)
46+
47+
expect(result).toEqual([
48+
{ role: 'system', content: 'You are concise.' },
49+
{ role: 'user', content: 'weather?' },
50+
{
51+
role: 'assistant',
52+
content: '',
53+
tool_calls: [
54+
{
55+
type: 'function',
56+
function: {
57+
index: 0,
58+
name: 'get_weather',
59+
arguments: { city: 'Paris' },
60+
},
61+
},
62+
],
63+
},
64+
{ role: 'tool', tool_name: 'get_weather', content: 'sunny' },
65+
])
66+
})
67+
})
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import { describe, expect, test } from 'bun:test'
2+
import { anthropicToolsToOllama } from '../convertTools.js'
3+
4+
describe('anthropicToolsToOllama', () => {
5+
test('converts basic tools to Ollama function tools', () => {
6+
const tools = [
7+
{
8+
type: 'custom',
9+
name: 'bash',
10+
description: 'Run a bash command',
11+
input_schema: {
12+
type: 'object',
13+
properties: { command: { type: 'string' } },
14+
required: ['command'],
15+
},
16+
},
17+
]
18+
19+
expect(anthropicToolsToOllama(tools as any)).toEqual([
20+
{
21+
type: 'function',
22+
function: {
23+
name: 'bash',
24+
description: 'Run a bash command',
25+
parameters: {
26+
type: 'object',
27+
properties: { command: { type: 'string' } },
28+
required: ['command'],
29+
},
30+
},
31+
},
32+
])
33+
})
34+
35+
test('keeps WebFetch parameters in Ollama-compatible schema subset', () => {
36+
const tools = [
37+
{
38+
type: 'custom',
39+
name: 'WebFetch',
40+
description: 'Fetch a URL',
41+
input_schema: {
42+
$schema: 'https://json-schema.org/draft/2020-12/schema',
43+
type: 'object',
44+
properties: {
45+
url: {
46+
type: 'string',
47+
format: 'uri',
48+
description: 'The URL to fetch content from',
49+
},
50+
prompt: {
51+
type: 'string',
52+
description: 'The prompt to run on the fetched content',
53+
},
54+
},
55+
required: ['url', 'prompt'],
56+
additionalProperties: false,
57+
},
58+
},
59+
]
60+
61+
expect(
62+
anthropicToolsToOllama(tools as any)[0]?.function.parameters,
63+
).toEqual({
64+
type: 'object',
65+
properties: {
66+
url: {
67+
type: 'string',
68+
description: 'The URL to fetch content from',
69+
},
70+
prompt: {
71+
type: 'string',
72+
description: 'The prompt to run on the fetched content',
73+
},
74+
},
75+
required: ['url', 'prompt'],
76+
})
77+
})
78+
79+
test('converts const and strips unsupported schema keywords recursively', () => {
80+
const tools = [
81+
{
82+
type: 'custom',
83+
name: 'complex',
84+
description: 'Complex schema',
85+
input_schema: {
86+
type: 'object',
87+
patternProperties: {
88+
'^x-': { type: 'string' },
89+
},
90+
properties: {
91+
mode: { const: 'strict' },
92+
metadata: {
93+
type: 'object',
94+
additionalProperties: { type: 'string' },
95+
propertyNames: { pattern: '^[a-z]+$' },
96+
},
97+
},
98+
required: ['mode'],
99+
},
100+
},
101+
]
102+
103+
expect(
104+
anthropicToolsToOllama(tools as any)[0]?.function.parameters,
105+
).toEqual({
106+
type: 'object',
107+
properties: {
108+
mode: {
109+
type: 'string',
110+
enum: ['strict'],
111+
},
112+
metadata: {
113+
type: 'object',
114+
},
115+
},
116+
required: ['mode'],
117+
})
118+
})
119+
})
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { afterEach, describe, expect, test } from 'bun:test'
2+
import { resolveOllamaModel } from '../modelMapping.js'
3+
4+
const envKeys = [
5+
'OLLAMA_MODEL',
6+
'OLLAMA_DEFAULT_HAIKU_MODEL',
7+
'OLLAMA_DEFAULT_SONNET_MODEL',
8+
'OLLAMA_DEFAULT_OPUS_MODEL',
9+
'ANTHROPIC_DEFAULT_SONNET_MODEL',
10+
] as const
11+
12+
const savedEnv: Record<string, string | undefined> = {}
13+
14+
for (const key of envKeys) {
15+
savedEnv[key] = process.env[key]
16+
delete process.env[key]
17+
}
18+
19+
afterEach(() => {
20+
for (const key of envKeys) {
21+
if (savedEnv[key] === undefined) {
22+
delete process.env[key]
23+
} else {
24+
process.env[key] = savedEnv[key]
25+
}
26+
}
27+
})
28+
29+
describe('resolveOllamaModel', () => {
30+
test('keeps direct Ollama model names selected from /model', () => {
31+
expect(resolveOllamaModel('qwen3-coder')).toBe('qwen3-coder')
32+
expect(resolveOllamaModel('glm-4.7:cloud')).toBe('glm-4.7:cloud')
33+
})
34+
35+
test('maps Claude family model ids to Ollama defaults', () => {
36+
process.env.OLLAMA_DEFAULT_SONNET_MODEL = 'qwen3-coder'
37+
38+
expect(resolveOllamaModel('claude-sonnet-4-6')).toBe('qwen3-coder')
39+
})
40+
41+
test('does not fall back to Anthropic model env vars for Ollama', () => {
42+
process.env.ANTHROPIC_DEFAULT_SONNET_MODEL = 'claude-sonnet-custom'
43+
44+
expect(resolveOllamaModel('claude-sonnet-4-6')).toBe('qwen3-coder')
45+
})
46+
47+
test('ignores legacy OLLAMA_MODEL global override', () => {
48+
process.env.OLLAMA_MODEL = 'legacy-global-model'
49+
process.env.OLLAMA_DEFAULT_SONNET_MODEL = 'qwen3-coder'
50+
51+
expect(resolveOllamaModel('claude-sonnet-4-6')).toBe('qwen3-coder')
52+
})
53+
})

0 commit comments

Comments
 (0)