feat: add DeepSeek + GLM Coding Plan providers, fix DeepSeek reasonin…#604
feat: add DeepSeek + GLM Coding Plan providers, fix DeepSeek reasonin…#604ngduyanhece wants to merge 1 commit intomainfrom
Conversation
…g detection Adds two LLM providers to bring byterover-cli to opencode parity for the four user-requested integrations (DeepSeek, MiniMax, Moonshot/Kimi, Z.AI): - DeepSeek (deepseek-chat, deepseek-reasoner) via @ai-sdk/openai-compatible against https://api.deepseek.com/v1, env DEEPSEEK_API_KEY. - GLM Coding Plan as a separate provider ID (glm-coding-plan) pointing at the Z.AI Coding Plan subscription endpoint (api.z.ai/api/coding/paas/v4), reusing ZHIPU_API_KEY. Coexists with the existing 'glm' provider so standard pay-per-token users aren't silently switched onto a subscription. Bug fix: deepseek-r1/deepseek-reasoner were mapped to reasoningFormat 'think-tags' in model-capabilities.ts, but the DeepSeek OpenAI-compatible API streams reasoning through the native 'reasoning_content' field — the old path never matched, so reasoning was effectively broken. Now correctly mapped to native-field with reasoningField: 'reasoning_content'. MiniMax, Moonshot/Kimi, and standard GLM were already wired and work unchanged. The Kimi Coding Plan subscription tier uses the same Moonshot endpoint, so no new provider entry is needed for it (matches opencode's behavior — opencode also has no kimi-coding-plan provider). Plan: plan/llm-providers/{DESIGN,IMPLEMENTATION}.md TDD-authored; 21 new test assertions across registry, capability, and provider-module shape. Full suite: 7316 passing, 16 pending. 0 lint errors. Icon asset for DeepSeek sourced from Wikimedia Commons (MIT-licensed), whale-only path with cropped viewBox. GLM Coding Plan reuses the existing zai icon. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
|
Claude finished @ngduyanhece's task in 3m 16s —— View job PR Review: feat: DeepSeek + GLM Coding Plan providers, fix DeepSeek reasoning
SummaryOverall score: 4 / 5 This is a well-executed, well-scoped PR. The TDD sequence is evidenced clearly, the bug fix rationale is sound, and the new providers follow the established module pattern precisely. No security issues found. The issues below are all minor maintainability and documentation concerns. What's Good
Issues (inline comments posted)
None of these block merging — the most actionable before merge are the |
| createGenerator(config: GeneratorFactoryConfig) { | ||
| const provider = createOpenAICompatible({ | ||
| apiKey: config.apiKey!, | ||
| baseURL: 'https://api.deepseek.com/v1', |
There was a problem hiding this comment.
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:
| 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.)
| createGenerator(config: GeneratorFactoryConfig) { | ||
| const provider = createOpenAICompatible({ | ||
| apiKey: config.apiKey!, | ||
| baseURL: 'https://api.z.ai/api/coding/paas/v4', |
There was a problem hiding this comment.
nitpick: Same URL duplication as in deepseek.ts — baseURL 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.
| // (OpenAI-compatible field), not <think> tags. | ||
| if (id.includes('deepseek')) { | ||
| // DeepSeek-R1 and reasoning models | ||
| if (id.includes('r1') || id.includes('reasoner')) { |
There was a problem hiding this comment.
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.
| fetcher = new ChatBasedModelFetcher( | ||
| 'https://api.z.ai/api/coding/paas/v4', | ||
| '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'], |
There was a problem hiding this comment.
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:
| ['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'], |
|
|
||
| case 'glm-coding-plan': { | ||
| fetcher = new ChatBasedModelFetcher( | ||
| 'https://api.z.ai/api/coding/paas/v4', |
There was a problem hiding this comment.
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.
| envVars: ['ZHIPU_API_KEY'], | ||
| id: 'glm-coding-plan', | ||
| name: 'GLM Coding Plan (Z.AI)', | ||
| priority: 17.5, |
There was a problem hiding this comment.
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.
| expect(caps.reasoning).to.equal(false) | ||
| expect(caps.reasoningFormat).to.equal('none') | ||
| }) | ||
| }) |
There was a problem hiding this comment.
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.

Summary
think-tagsparser that never matched the API's actualreasoning_contentfield — reasoning output was silently dropped.deepseekprovider via@ai-sdk/openai-compatibleagainsthttps://api.deepseek.com/v1(envDEEPSEEK_API_KEY).glm-coding-planprovider as a separate ID alongside the existingglm, pointing athttps://api.z.ai/api/coding/paas/v4, reusingZHIPU_API_KEY.deepseek-r1/deepseek-reasonernow correctly map toreasoningFormat: 'native-field'withreasoningField: 'reasoning_content'.zaiicon.glmwere already wired and untouched. Nokimi-coding-planprovider, and Moonshot's coding-plan tier uses the sameapi.moonshot.ai/v1endpoint as the standard provider.zhipuai-coding-plan(mainland-CN),minimax-coding-plan(Anthropic-compatible), and full models.dev catalog adoption are deferred perplan/llm-providers/DESIGN.mdD3/D4.Type of change
Scope (select all touched areas)
Linked issues
Root cause (bug fixes only)
getModelCapabilities()insrc/agent/infra/llm/model-capabilities.tsmappeddeepseek-r1/deepseek-reasonertoreasoningFormat: 'think-tags'. DeepSeek's OpenAI-compatible API does not emit<think>...</think>markers — it streams reasoning in a nativereasoning_contentfield. The think-tag scanner never matched, so reasoning content was effectively dropped on the floor.brv providers connectand never exercised end-to-end. The capability rule sat in a code path that no integration test could trigger. The fix is forced by adding the provider; this PR closes the loop in one go.Test plan
test/unit/agent/llm/model-capabilities.test.ts(new) — 3 assertions for the capability fixtest/unit/agent/llm/providers/deepseek.test.ts(new) — 6 assertions for provider-module shapetest/unit/agent/llm/providers/glm-coding-plan.test.ts(new) — 7 assertions for provider-module shapetest/unit/core/domain/entities/provider-registry.test.ts(extended) — 13 new assertions (6 DeepSeek + 7 GLM Coding Plan)deepseek-reasoneranddeepseek-r1resolve tonative-field/reasoning_content;deepseek-chatreports no reasoningglm-coding-plancoexists withglm(regression check thatgetProviderById('glm')still resolves and the two have distinct base URLs)providerRequiresApiKey('deepseek')andproviderRequiresApiKey('glm-coding-plan')both returntrueUser-visible changes
brv providers connectpicker: DeepSeek and GLM Coding Plan (Z.AI).deepseek-reasoner,deepseek-r1) now stream reasoning into the dedicated thinking block instead of silently dropping it.Evidence
TDD sequence (full output in commit history):
Targeted run after Phase 2 (DeepSeek + GLM Coding Plan):
Checklist
npm test— 7316 passing)npm run lint— 0 errors)npm run typecheck— root + webui clean)npm run build)feat:prefix)mainRisks and mitigations
glm-coding-planincludesglm-5-turbo.provider-model-fetcher-registry.ts. Smoke test with a subscription holder is documented as a follow-up inplan/llm-providers/IMPLEMENTATION.md§2.3.deepseek-reasoneraccessible via OpenRouter (or any other route that flows throughgetModelCapabilities).think-tagsmapping never matched a real DeepSeek response, so users were not seeing reasoning anyway — this is a fix, not a regression. OpenRouter has its own capability detection path that is unaffected.glmusers on the Z.AI Coding Plan subscription might not realize they need to switch to the newglm-coding-planprovider to consume their subscription quota.GLM (Z.AI)vsGLM Coding Plan (Z.AI)) and the description string call out the subscription. README table makes both explicit.