Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ Or download our self-hosted PDF version of the paper [here](https://byterover.de
- 🖥️ Interactive TUI with REPL interface (React/Ink)
- 🧠 Context tree and knowledge storage management
- 🔀 Git-like version control for the context tree (branch, commit, merge, push/pull)
- 🤖 18 LLM providers (Anthropic, OpenAI, Google, Groq, Mistral, xAI, and more)
- 🤖 20 LLM providers (Anthropic, OpenAI, Google, Groq, Mistral, xAI, DeepSeek, and more)
- 🛠️ 24 built-in agent tools (code exec, file ops, knowledge search, memory management)
- 🔄 Cloud sync with push/pull
- 👀 Review workflow for curate operations (approve/reject pending changes)
Expand Down Expand Up @@ -220,7 +220,7 @@ Run `brv --help` for the full command reference.
<details>
<summary><h2>Supported LLM Providers</h2></summary>

ByteRover CLI supports 18 LLM providers out of the box. Connect and switch providers from the dashboard, or use `brv providers connect` / `brv providers switch`.
ByteRover CLI supports 20 LLM providers out of the box. Connect and switch providers from the dashboard, or use `brv providers connect` / `brv providers switch`.

| Provider | Description |
|----------|-------------|
Expand All @@ -233,13 +233,15 @@ ByteRover CLI supports 18 LLM providers out of the box. Connect and switch provi
| Cerebras | Fast inference |
| Cohere | Command models |
| DeepInfra | Open-source model hosting |
| DeepSeek | DeepSeek V3 and R1 reasoning models |
| OpenRouter | Multi-provider gateway |
| Perplexity | Search-augmented models |
| TogetherAI | Open-source model hosting |
| Vercel | AI SDK provider |
| Minimax | Minimax models |
| Moonshot | Kimi models |
| GLM | GLM models |
| GLM Coding Plan | GLM models on Z.AI Coding Plan subscription |
| OpenAI-Compatible | Any OpenAI-compatible API |
| ByteRover | ByteRover's hosted models |

Expand Down
10 changes: 6 additions & 4 deletions src/agent/infra/llm/model-capabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
* - Grok: `reasoning_content` or `reasoning_details` fields
* - Gemini via OpenRouter: `reasoning_details` array or `thoughts` field
* - GLM (Zhipu AI): `reasoning_content` field in API response
* - Claude/DeepSeek/MiniMax: `<think>...</think>` XML tags in content
* - DeepSeek (R1/Reasoner): `reasoning_content` field in API response (OpenAI-compatible)
* - Claude/MiniMax: `<think>...</think>` XML tags in content
*/

/**
Expand Down Expand Up @@ -54,7 +55,7 @@
* // { reasoning: true, reasoningFormat: 'think-tags' }
* ```
*/
export function getModelCapabilities(modelId: string): ModelCapabilities {

Check warning on line 58 in src/agent/infra/llm/model-capabilities.ts

View workflow job for this annotation

GitHub Actions / lint

Function 'getModelCapabilities' has a complexity of 28. Maximum allowed is 20
const id = modelId.toLowerCase()

// OpenAI reasoning models (o1, o3, gpt-5 series)
Expand Down Expand Up @@ -132,13 +133,14 @@
}
}

// DeepSeek models use think tags
// DeepSeek models — reasoning models stream `reasoning_content` natively
// (OpenAI-compatible field), not <think> tags.
if (id.includes('deepseek')) {
// DeepSeek-R1 and reasoning models
if (id.includes('r1') || id.includes('reasoner')) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: id.includes('r1') will also match distilled variants like deepseek-r1-distill-llama-70b and deepseek-r1-distill-qwen-32b. These models do emit reasoning_content when accessed directly, so the capability mapping is technically correct for direct DeepSeek API use. But users accessing them via OpenRouter may get a different response shape (OpenRouter normalises reasoning differently).

Is the intent that the model ID here always refers to the direct DeepSeek endpoint, so this is fine as-is? If OpenRouter-routed DeepSeek models reach this code path it could cause empty thinking blocks for distilled variants. A clarifying comment, or a test for deepseek-r1-distill-llama-70b, would make the intent explicit.

return {
reasoning: true,
reasoningFormat: 'think-tags',
reasoningField: 'reasoning_content',
reasoningFormat: 'native-field',
}
}

Expand Down
40 changes: 40 additions & 0 deletions src/agent/infra/llm/providers/deepseek.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* DeepSeek Provider Module
*
* Access to DeepSeek V3 (deepseek-chat) and R1 (deepseek-reasoner) via their
* OpenAI-compatible API. The reasoner model streams thinking through the
* native `reasoning_content` field rather than `<think>` tags — see
* model-capabilities.ts for the parser routing.
*/

import {createOpenAICompatible} from '@ai-sdk/openai-compatible'

import type {GeneratorFactoryConfig, ProviderModule} from './types.js'

import {AiSdkContentGenerator} from '../generators/ai-sdk-content-generator.js'

export const deepseekProvider: ProviderModule = {
apiKeyUrl: 'https://platform.deepseek.com/api_keys',
authType: 'api-key',
baseUrl: 'https://api.deepseek.com/v1',
category: 'other',
createGenerator(config: GeneratorFactoryConfig) {
const provider = createOpenAICompatible({
apiKey: config.apiKey!,
baseURL: 'https://api.deepseek.com/v1',
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: The base URL is hardcoded here a second time, duplicating line 19. If the URL ever changes, both spots must be updated in sync. Other providers (e.g. deepinfra, togetherai) use the same pattern, so this is consistent with the codebase — but it's worth extracting to a module-level constant so there's a single source of truth:

Suggested change
baseURL: 'https://api.deepseek.com/v1',
baseURL: 'https://api.deepseek.com/v1',

(No change needed to the suggestion text itself, but consider a shared constant like DEEPSEEK_BASE_URL at the top of the file.)

name: 'deepseek',
})

return new AiSdkContentGenerator({
model: provider.chatModel(config.model),
})
},
defaultModel: 'deepseek-chat',
description: 'DeepSeek V3 and R1 reasoning models',
envVars: ['DEEPSEEK_API_KEY'],
id: 'deepseek',
name: 'DeepSeek',
priority: 19,

providerType: 'openai',
}
39 changes: 39 additions & 0 deletions src/agent/infra/llm/providers/glm-coding-plan.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* GLM Coding Plan (Z.AI) Provider Module
*
* Same Z.AI account as the standard `glm` provider but routes through the
* coding-plan endpoint so subscription quota is consumed instead of
* pay-per-token billing.
*/

import {createOpenAICompatible} from '@ai-sdk/openai-compatible'

import type {GeneratorFactoryConfig, ProviderModule} from './types.js'

import {AiSdkContentGenerator} from '../generators/ai-sdk-content-generator.js'

export const glmCodingPlanProvider: ProviderModule = {
apiKeyUrl: 'https://docs.z.ai/devpack/overview',
authType: 'api-key',
baseUrl: 'https://api.z.ai/api/coding/paas/v4',
category: 'other',
createGenerator(config: GeneratorFactoryConfig) {
const provider = createOpenAICompatible({
apiKey: config.apiKey!,
baseURL: 'https://api.z.ai/api/coding/paas/v4',
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: Same URL duplication as in deepseek.tsbaseURL in createGenerator (here) repeats the value declared in the baseUrl module property (line 18). A module-level constant would prevent a silent drift if one is updated without the other.

name: 'glm-coding-plan',
})

return new AiSdkContentGenerator({
model: provider.chatModel(config.model),
})
},
defaultModel: 'glm-4.7',
description: 'GLM models on the Z.AI Coding Plan subscription',
envVars: ['ZHIPU_API_KEY'],
id: 'glm-coding-plan',
name: 'GLM Coding Plan (Z.AI)',
priority: 17.5,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: priority: 17.5 works because priority is typed as number, but every other provider uses an integer. Inserting a float between 17 and 18 makes the priority table harder to audit at a glance. If glm-coding-plan must appear after glm, consider shifting existing priorities (e.g. glm: 17, glm-coding-plan: 18, moonshot: 19, deepseek: 20) to keep the list integer-only.


providerType: 'openai',
}
4 changes: 4 additions & 0 deletions src/agent/infra/llm/providers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import {byteroverProvider} from './byterover.js'
import {cerebrasProvider} from './cerebras.js'
import {cohereProvider} from './cohere.js'
import {deepinfraProvider} from './deepinfra.js'
import {deepseekProvider} from './deepseek.js'
import {glmCodingPlanProvider} from './glm-coding-plan.js'
import {glmProvider} from './glm.js'
import {googleProvider} from './google.js'
import {groqProvider} from './groq.js'
Expand All @@ -38,7 +40,9 @@ const PROVIDER_MODULES: Readonly<Record<string, ProviderModule>> = {
cerebras: cerebrasProvider,
cohere: cohereProvider,
deepinfra: deepinfraProvider,
deepseek: deepseekProvider,
glm: glmProvider,
'glm-coding-plan': glmCodingPlanProvider,
google: googleProvider,
groq: groqProvider,
minimax: minimaxProvider,
Expand Down
26 changes: 26 additions & 0 deletions src/server/core/domain/entities/provider-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,19 @@ export const PROVIDER_REGISTRY: Readonly<Record<string, ProviderDefinition>> = {
name: 'DeepInfra',
priority: 10,
},
deepseek: {
apiKeyUrl: 'https://platform.deepseek.com/api_keys',
baseUrl: 'https://api.deepseek.com/v1',
category: 'other',
defaultModel: 'deepseek-chat',
description: 'DeepSeek V3 and R1 reasoning models',
envVars: ['DEEPSEEK_API_KEY'],
headers: {},
id: 'deepseek',
modelsEndpoint: '/models',
name: 'DeepSeek',
priority: 19,
},
glm: {
apiKeyUrl: 'https://open.z.ai',
baseUrl: 'https://api.z.ai/api/paas/v4',
Expand All @@ -157,6 +170,19 @@ export const PROVIDER_REGISTRY: Readonly<Record<string, ProviderDefinition>> = {
name: 'GLM (Z.AI)',
priority: 17,
},
'glm-coding-plan': {
apiKeyUrl: 'https://docs.z.ai/devpack/overview',
baseUrl: 'https://api.z.ai/api/coding/paas/v4',
category: 'other',
defaultModel: 'glm-4.7',
description: 'GLM models on the Z.AI Coding Plan subscription',
envVars: ['ZHIPU_API_KEY'],
headers: {},
id: 'glm-coding-plan',
modelsEndpoint: '',
name: 'GLM Coding Plan (Z.AI)',
priority: 17.5,
},
google: {
apiKeyUrl: 'https://aistudio.google.com/apikey',
baseUrl: '',
Expand Down
11 changes: 11 additions & 0 deletions src/server/infra/http/provider-model-fetcher-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export async function getModelFetcher(providerId: string): Promise<IProviderMode
case 'cerebras': // falls through
case 'cohere': // falls through
case 'deepinfra': // falls through
case 'deepseek': // falls through
case 'groq': // falls through
case 'mistral': // falls through
case 'togetherai': // falls through
Expand All @@ -85,6 +86,16 @@ export async function getModelFetcher(providerId: string): Promise<IProviderMode
break
}

case 'glm-coding-plan': {
fetcher = new ChatBasedModelFetcher(
'https://api.z.ai/api/coding/paas/v4',
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: For providers handled via OpenAICompatibleModelFetcher (e.g. deepseek at line 65), the base URL is sourced from PROVIDER_REGISTRY[providerId].baseUrl, giving a single source of truth. Here, the coding-plan URL is hardcoded again independently of PROVIDER_REGISTRY['glm-coding-plan'].baseUrl. The glm case above has the same pre-existing pattern, but it's worth noting so that a future URL change doesn't have to be applied in three places.

'GLM Coding Plan (Z.AI)',
['glm-4.7', 'glm-4.7-flash', 'glm-4.7-flashx', 'glm-5-turbo', 'glm-4.5', 'glm-4.5-flash'],
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: The PR description calls out glm-5-turbo as potentially returning 404, but there's no inline comment here to capture that. Since this is a static list that a subscriber would scroll through and select, a // unverified — may 404 on non-eligible subscriptions note would surface the known risk at the call site rather than requiring readers to check the PR description or IMPLEMENTATION.md:

Suggested change
['glm-4.7', 'glm-4.7-flash', 'glm-4.7-flashx', 'glm-5-turbo', 'glm-4.5', 'glm-4.5-flash'],
['glm-4.7', 'glm-4.7-flash', 'glm-4.7-flashx', 'glm-5-turbo' /* unverified, may 404 on non-eligible subscriptions */, 'glm-4.5', 'glm-4.5-flash'],

)

break
}

case 'google': {
fetcher = new GoogleModelFetcher()

Expand Down
3 changes: 3 additions & 0 deletions src/webui/assets/providers/deepseek-provider.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import byterover from '../../../../assets/providers/byterover-provider.svg'
import cerebras from '../../../../assets/providers/cerebras-provider.svg'
import cohere from '../../../../assets/providers/cohere-provider.svg'
import deepinfra from '../../../../assets/providers/deepinfra-provider.svg'
import deepseek from '../../../../assets/providers/deepseek-provider.svg'
import gemini from '../../../../assets/providers/gemini-provider.svg'
import groq from '../../../../assets/providers/groq-provider.svg'
import kimi from '../../../../assets/providers/kimi-provider.svg'
Expand All @@ -23,7 +24,9 @@ export const providerIcons: Record<string, string> = {
cerebras,
cohere,
deepinfra,
deepseek,
glm: zai,
'glm-coding-plan': zai,
google: gemini,
groq,
minimax,
Expand Down
25 changes: 25 additions & 0 deletions test/unit/agent/llm/model-capabilities.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import {expect} from 'chai'

import {getModelCapabilities} from '../../../../src/agent/infra/llm/model-capabilities.js'

describe('getModelCapabilities — DeepSeek', () => {
it('reports native reasoning_content for deepseek-reasoner', () => {
const caps = getModelCapabilities('deepseek-reasoner')
expect(caps.reasoning).to.equal(true)
expect(caps.reasoningField).to.equal('reasoning_content')
expect(caps.reasoningFormat).to.equal('native-field')
})

it('reports native reasoning_content for deepseek-r1', () => {
const caps = getModelCapabilities('deepseek-r1')
expect(caps.reasoning).to.equal(true)
expect(caps.reasoningField).to.equal('reasoning_content')
expect(caps.reasoningFormat).to.equal('native-field')
})

it('reports no reasoning for deepseek-chat', () => {
const caps = getModelCapabilities('deepseek-chat')
expect(caps.reasoning).to.equal(false)
expect(caps.reasoningFormat).to.equal('none')
})
})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Consider adding a test for a distilled-R1 model ID to make the matching intent explicit:

  it('reports native reasoning_content for deepseek-r1-distill variants', () => {
    const caps = getModelCapabilities('deepseek-r1-distill-llama-70b')
    expect(caps.reasoning).to.equal(true)
    expect(caps.reasoningField).to.equal('reasoning_content')
    expect(caps.reasoningFormat).to.equal('native-field')
  })

This pins the expected behaviour for distilled models and makes it obvious the r1 substring match is intentional for this whole family, not just deepseek-r1 proper.

31 changes: 31 additions & 0 deletions test/unit/agent/llm/providers/deepseek.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {expect} from 'chai'

import {getProviderModule} from '../../../../../src/agent/infra/llm/providers/index.js'

describe('deepseek provider module', () => {
const mod = getProviderModule('deepseek')

it('is registered', () => {
expect(mod).to.not.be.undefined
})

it('uses api-key auth', () => {
expect(mod?.authType).to.equal('api-key')
})

it('uses the openai provider type for formatter/tokenizer selection', () => {
expect(mod?.providerType).to.equal('openai')
})

it('defaults to deepseek-chat', () => {
expect(mod?.defaultModel).to.equal('deepseek-chat')
})

it('points at the official DeepSeek API base URL', () => {
expect(mod?.baseUrl).to.equal('https://api.deepseek.com/v1')
})

it('exposes DEEPSEEK_API_KEY for env detection', () => {
expect(mod?.envVars).to.include('DEEPSEEK_API_KEY')
})
})
37 changes: 37 additions & 0 deletions test/unit/agent/llm/providers/glm-coding-plan.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import {expect} from 'chai'

import {getProviderModule} from '../../../../../src/agent/infra/llm/providers/index.js'

describe('glm-coding-plan provider module', () => {
const mod = getProviderModule('glm-coding-plan')

it('is registered', () => {
expect(mod).to.not.be.undefined
})

it('uses api-key auth', () => {
expect(mod?.authType).to.equal('api-key')
})

it('uses the openai provider type for formatter/tokenizer selection', () => {
expect(mod?.providerType).to.equal('openai')
})

it('defaults to glm-4.7', () => {
expect(mod?.defaultModel).to.equal('glm-4.7')
})

it('points at the Z.AI Coding Plan endpoint', () => {
expect(mod?.baseUrl).to.equal('https://api.z.ai/api/coding/paas/v4')
})

it('exposes ZHIPU_API_KEY for env detection', () => {
expect(mod?.envVars).to.include('ZHIPU_API_KEY')
})

it('coexists with the standard glm provider', () => {
const standard = getProviderModule('glm')
expect(standard).to.not.be.undefined
expect(standard?.baseUrl).to.not.equal(mod?.baseUrl)
})
})
Loading
Loading