Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

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

Claude Code Best supports Ollama through the native Ollama API, not the
OpenAI-compatible endpoint. This lets Cloud and local Ollama use the same
request shape for chat, tool calling, thinking, model discovery, and web
utilities.

## Configure

Use `/login` and choose `Ollama`, or configure these environment variables:

```bash
CLAUDE_CODE_USE_OLLAMA=1
OLLAMA_API_KEY=ollama_api_key
OLLAMA_BASE_URL=https://ollama.com/api
OLLAMA_DEFAULT_HAIKU_MODEL=qwen3:cloud
OLLAMA_DEFAULT_SONNET_MODEL=qwen3-coder
OLLAMA_DEFAULT_OPUS_MODEL=glm-4.7:cloud
```

`OLLAMA_API_KEY` is required for direct Ollama Cloud API access. It is not
required for local Ollama. For local Ollama, set:

```bash
OLLAMA_BASE_URL=http://localhost:11434/api
```

If `OLLAMA_BASE_URL` is omitted, Claude Code Best uses
`https://ollama.com/api`.

## Model Mapping

Ollama model routing uses the same three Claude model families shown by
`/model`:

- `OLLAMA_DEFAULT_HAIKU_MODEL`
- `OLLAMA_DEFAULT_SONNET_MODEL`
- `OLLAMA_DEFAULT_OPUS_MODEL`

There is no global `OLLAMA_MODEL` override. This keeps Ollama behavior aligned
with other third-party providers, where Haiku/Sonnet/Opus can map to different
backend models.

When a direct Ollama model name is selected from `/model` or `--model`, it is
sent to Ollama unchanged. When a Claude family model is selected, Claude Code
Best maps it through the matching `OLLAMA_DEFAULT_*_MODEL` variable. If no
family mapping is configured, the fallback is `qwen3-coder`.

## Supported Features

- Native `POST /api/chat` streaming
- Ollama tool calling through `tools`
- Ollama thinking through `think`
- Native `POST /api/web_search`
- Native `POST /api/web_fetch`
- Dynamic context length discovery through `POST /api/show`
- Local Ollama and Ollama Cloud through the same provider

The provider reads model context length from `model_info.*.context_length` or
the `num_ctx` parameter returned by `/api/show`, then uses that value to choose
the request output limit.

## Known Limits

Ollama does not expose an official Cloud quota or remaining-balance API in the
documented native API. Claude Code Best therefore does not show Ollama Cloud
remaining quota.

Anthropic-only server tools are not sent directly to Ollama. Web search and web
fetch are handled client-side through Ollama's native web APIs when the Ollama
provider is active.
11 changes: 11 additions & 0 deletions packages/@ant/model-provider/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,17 @@ export * from './types/index.js'
export { resolveOpenAIModel } from './providers/openai/modelMapping.js'
export { resolveGrokModel } from './providers/grok/modelMapping.js'
export { resolveGeminiModel } from './providers/gemini/modelMapping.js'
export { resolveOllamaModel } from './providers/ollama/modelMapping.js'
export { anthropicMessagesToOllama } from './providers/ollama/convertMessages.js'
export { anthropicToolsToOllama } from './providers/ollama/convertTools.js'
export { adaptOllamaStreamToAnthropic } from './providers/ollama/streamAdapter.js'
export type {
OllamaChatChunk,
OllamaChatRequest,
OllamaMessage,
OllamaTool,
OllamaToolCall,
} from './providers/ollama/types.js'

// Gemini provider utilities
export { anthropicMessagesToGemini } from './providers/gemini/convertMessages.js'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { describe, expect, test } from 'bun:test'
import { anthropicMessagesToOllama } from '../convertMessages.js'
import type { SystemPrompt } from '../../../types/systemPrompt.js'

describe('anthropicMessagesToOllama', () => {
test('converts system, text, tool use, and tool result messages', () => {
const result = anthropicMessagesToOllama(
[
{
type: 'user',
message: {
role: 'user',
content: [{ type: 'text', text: 'weather?' }],
},
} as any,
{
type: 'assistant',
message: {
role: 'assistant',
content: [
{
type: 'tool_use',
id: 'toolu_1',
name: 'get_weather',
input: { city: 'Paris' },
},
],
},
} as any,
{
type: 'user',
message: {
role: 'user',
content: [
{
type: 'tool_result',
tool_use_id: 'toolu_1',
content: 'sunny',
},
],
},
} as any,
],
['You are concise.'] as unknown as SystemPrompt,
)

expect(result).toEqual([
{ role: 'system', content: 'You are concise.' },
{ role: 'user', content: 'weather?' },
{
role: 'assistant',
content: '',
tool_calls: [
{
type: 'function',
function: {
index: 0,
name: 'get_weather',
arguments: { city: 'Paris' },
},
},
],
},
{ role: 'tool', tool_name: 'get_weather', content: 'sunny' },
])
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { describe, expect, test } from 'bun:test'
import { anthropicToolsToOllama } from '../convertTools.js'

describe('anthropicToolsToOllama', () => {
test('converts basic tools to Ollama function tools', () => {
const tools = [
{
type: 'custom',
name: 'bash',
description: 'Run a bash command',
input_schema: {
type: 'object',
properties: { command: { type: 'string' } },
required: ['command'],
},
},
]

expect(anthropicToolsToOllama(tools as any)).toEqual([
{
type: 'function',
function: {
name: 'bash',
description: 'Run a bash command',
parameters: {
type: 'object',
properties: { command: { type: 'string' } },
required: ['command'],
},
},
},
])
})

test('keeps WebFetch parameters in Ollama-compatible schema subset', () => {
const tools = [
{
type: 'custom',
name: 'WebFetch',
description: 'Fetch a URL',
input_schema: {
$schema: 'https://json-schema.org/draft/2020-12/schema',
type: 'object',
properties: {
url: {
type: 'string',
format: 'uri',
description: 'The URL to fetch content from',
},
prompt: {
type: 'string',
description: 'The prompt to run on the fetched content',
},
},
required: ['url', 'prompt'],
additionalProperties: false,
},
},
]

expect(
anthropicToolsToOllama(tools as any)[0]?.function.parameters,
).toEqual({
type: 'object',
properties: {
url: {
type: 'string',
description: 'The URL to fetch content from',
},
prompt: {
type: 'string',
description: 'The prompt to run on the fetched content',
},
},
required: ['url', 'prompt'],
})
})

test('converts const and strips unsupported schema keywords recursively', () => {
const tools = [
{
type: 'custom',
name: 'complex',
description: 'Complex schema',
input_schema: {
type: 'object',
patternProperties: {
'^x-': { type: 'string' },
},
properties: {
mode: { const: 'strict' },
metadata: {
type: 'object',
additionalProperties: { type: 'string' },
propertyNames: { pattern: '^[a-z]+$' },
},
},
required: ['mode'],
},
},
]

expect(
anthropicToolsToOllama(tools as any)[0]?.function.parameters,
).toEqual({
type: 'object',
properties: {
mode: {
type: 'string',
enum: ['strict'],
},
metadata: {
type: 'object',
},
},
required: ['mode'],
})
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { afterAll, beforeEach, describe, expect, test } from 'bun:test'
import { resolveOllamaModel } from '../modelMapping.js'

const envKeys = [
'OLLAMA_MODEL',
'OLLAMA_DEFAULT_HAIKU_MODEL',
'OLLAMA_DEFAULT_SONNET_MODEL',
'OLLAMA_DEFAULT_OPUS_MODEL',
'ANTHROPIC_DEFAULT_SONNET_MODEL',
] as const

const savedEnv: Record<string, string | undefined> = {}

for (const key of envKeys) {
savedEnv[key] = process.env[key]
}

beforeEach(() => {
for (const key of envKeys) {
delete process.env[key]
}
})

afterAll(() => {
for (const key of envKeys) {
if (savedEnv[key] === undefined) {
delete process.env[key]
} else {
process.env[key] = savedEnv[key]
}
}
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.

describe('resolveOllamaModel', () => {
test('keeps direct Ollama model names selected from /model', () => {
expect(resolveOllamaModel('qwen3-coder')).toBe('qwen3-coder')
expect(resolveOllamaModel('glm-4.7:cloud')).toBe('glm-4.7:cloud')
})

test('maps Claude family model ids to Ollama defaults', () => {
process.env.OLLAMA_DEFAULT_SONNET_MODEL = 'qwen3-coder'

expect(resolveOllamaModel('claude-sonnet-4-6')).toBe('qwen3-coder')
})

test('does not fall back to Anthropic model env vars for Ollama', () => {
process.env.ANTHROPIC_DEFAULT_SONNET_MODEL = 'claude-sonnet-custom'

expect(resolveOllamaModel('claude-sonnet-4-6')).toBe('qwen3-coder')
})

test('ignores legacy OLLAMA_MODEL global override', () => {
process.env.OLLAMA_MODEL = 'legacy-global-model'

expect(resolveOllamaModel('claude-sonnet-4-6')).toBe('qwen3-coder')
})
})
Loading