-
Notifications
You must be signed in to change notification settings - Fork 449
feat: add DeepSeek + GLM Coding Plan providers, fix DeepSeek reasonin… #604
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
49a5144
5eb365e
f5a334d
c4e225a
d78ab72
3287f57
1aae446
b31fd36
b7752f5
9913ff5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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', | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Suggested change
(No change needed to the suggestion text itself, but consider a shared constant like |
||||||
| 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', | ||||||
| } | ||||||
| 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', | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nitpick: Same URL duplication as in |
||
| 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, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nitpick: |
||
|
|
||
| providerType: 'openai', | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -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 | ||||||
|
|
@@ -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', | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nitpick: For providers handled via |
||||||
| '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'], | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion: The PR description calls out
Suggested change
|
||||||
| ) | ||||||
|
|
||||||
| break | ||||||
| } | ||||||
|
|
||||||
| case 'google': { | ||||||
| fetcher = new GoogleModelFetcher() | ||||||
|
|
||||||
|
|
||||||
| 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') | ||
| }) | ||
| }) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| 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') | ||
| }) | ||
| }) |
| 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) | ||
| }) | ||
| }) |
There was a problem hiding this comment.
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 likedeepseek-r1-distill-llama-70banddeepseek-r1-distill-qwen-32b. These models do emitreasoning_contentwhen 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.