Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 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
1 change: 1 addition & 0 deletions packages/types/src/provider-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export const dynamicProviders = [
"roo",
"unbound",
"poe",
"deepseek",

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.

Thanks for the PR, can ask, do you see much of a difference to #6 ?
Is it mainly the dynamic model fetching?

] as const

export type DynamicProvider = (typeof dynamicProviders)[number]
Expand Down
36 changes: 33 additions & 3 deletions packages/types/src/providers/deepseek.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,35 @@ import type { ModelInfo } from "../model.js"
// continuation within the same turn. See: https://api-docs.deepseek.com/guides/thinking_mode
export type DeepSeekModelId = keyof typeof deepSeekModels

export const deepSeekDefaultModelId: DeepSeekModelId = "deepseek-chat"
export const deepSeekDefaultModelId: DeepSeekModelId = "deepseek-v4-pro"

export const deepSeekModels = {
"deepseek-v4-pro": {
maxTokens: 384_000, // 384K max output
contextWindow: 1_000_000, // 1M context
supportsImages: false,
supportsPromptCache: true,
preserveReasoning: true,
inputPrice: 1.74, // $1.74 per million tokens (cache miss)
outputPrice: 3.48, // $3.48 per million tokens
cacheWritesPrice: 1.74, // $1.74 per million tokens (cache miss)
cacheReadsPrice: 0.145, // $0.145 per million tokens (cache hit)
description:
"DeepSeek-V4-Pro is an open-source model with 1.6T total and 49B active parameters. It excels in agentic coding benchmarks, possesses rich world knowledge, and demonstrates world-class reasoning capabilities in Math/STEM/Coding. Supports both Thinking and Non-Thinking modes, tool calls, and 1M context window.",
},
"deepseek-v4-flash": {
maxTokens: 384_000, // 384K max output
contextWindow: 1_000_000, // 1M context
supportsImages: false,
supportsPromptCache: true,
preserveReasoning: true,
inputPrice: 0.14, // $0.14 per million tokens (cache miss)
outputPrice: 0.28, // $0.28 per million tokens
cacheWritesPrice: 0.14, // $0.14 per million tokens (cache miss)
cacheReadsPrice: 0.028, // $0.028 per million tokens (cache hit)
description:
"DeepSeek-V4-Flash is a fast, efficient, and economical model with 284B total and 13B active parameters. Its reasoning capabilities closely approach V4-Pro with smaller parameter size, faster response times, and cost-effective API pricing. Supports both Thinking and Non-Thinking modes, tool calls, and 1M context window.",
},
"deepseek-chat": {
maxTokens: 8192, // 8K max output
contextWindow: 128_000,
Expand All @@ -18,7 +44,9 @@ export const deepSeekModels = {
outputPrice: 0.42, // $0.42 per million tokens - Updated Dec 9, 2025
cacheWritesPrice: 0.28, // $0.28 per million tokens (cache miss) - Updated Dec 9, 2025
cacheReadsPrice: 0.028, // $0.028 per million tokens (cache hit) - Updated Dec 9, 2025
description: `DeepSeek-V3.2 (Non-thinking Mode) achieves a significant breakthrough in inference speed over previous models. It tops the leaderboard among open-source models and rivals the most advanced closed-source models globally. Supports JSON output, tool calls, chat prefix completion (beta), and FIM completion (beta).`,
deprecated: true,
description:
"DeepSeek-V3.2 (Non-thinking Mode). Deprecated: will be removed on 2026/07/24. Use deepseek-v4-pro or deepseek-v4-flash instead.",
},
"deepseek-reasoner": {
maxTokens: 8192, // 8K max output
Expand All @@ -30,7 +58,9 @@ export const deepSeekModels = {
outputPrice: 0.42, // $0.42 per million tokens - Updated Dec 9, 2025
cacheWritesPrice: 0.28, // $0.28 per million tokens (cache miss) - Updated Dec 9, 2025
cacheReadsPrice: 0.028, // $0.028 per million tokens (cache hit) - Updated Dec 9, 2025
description: `DeepSeek-V3.2 (Thinking Mode) achieves performance comparable to OpenAI-o1 across math, code, and reasoning tasks. Supports Chain of Thought reasoning with up to 8K output tokens. Supports JSON output, tool calls, and chat prefix completion (beta).`,
deprecated: true,
description:
"DeepSeek-V3.2 (Thinking Mode). Deprecated: will be removed on 2026/07/24. Use deepseek-v4-pro or deepseek-v4-flash instead.",
},
} as const satisfies Record<string, ModelInfo>

Expand Down
16 changes: 12 additions & 4 deletions src/api/providers/__tests__/deepseek.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,11 @@ describe("DeepSeekHandler", () => {

it("should NOT have preserveReasoning enabled for deepseek-chat", () => {
// deepseek-chat doesn't use thinking mode, so no need to preserve reasoning
const model = handler.getModel()
const chatHandler = new DeepSeekHandler({
...mockOptions,
apiModelId: "deepseek-chat",
})
const model = chatHandler.getModel()
// Cast to ModelInfo to access preserveReasoning which is an optional property
expect((model.info as ModelInfo).preserveReasoning).toBeUndefined()
})
Expand All @@ -255,10 +259,14 @@ describe("DeepSeekHandler", () => {
const model = handlerWithInvalidModel.getModel()
expect(model.id).toBe("invalid-model") // Returns provided ID
expect(model.info).toBeDefined()
// With the current implementation, it's the same object reference when using default model info
expect(model.info).toBe(handler.getModel().info)
// Should fall back to the default model (deepseek-v4-pro)
const defaultHandler = new DeepSeekHandler({
...mockOptions,
apiModelId: undefined,
})
expect(model.info).toBe(defaultHandler.getModel().info)
// Should have the same base properties
expect(model.info.contextWindow).toBe(handler.getModel().info.contextWindow)
expect(model.info.contextWindow).toBe(defaultHandler.getModel().info.contextWindow)
// And should have supportsPromptCache set to true
expect(model.info.supportsPromptCache).toBe(true)
})
Expand Down
16 changes: 11 additions & 5 deletions src/api/providers/deepseek.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ import { convertToR1Format } from "../transform/r1-format"
import { OpenAiHandler } from "./openai"
import type { ApiHandlerCreateMessageMetadata } from "../index"

// Custom interface for DeepSeek params to support thinking mode
// Custom interface for DeepSeek params to support thinking mode.
// The OpenAI Node.js SDK passes unknown fields through to the API,
// so we can add `thinking` directly in the body.
type DeepSeekChatCompletionParams = OpenAI.Chat.ChatCompletionCreateParamsStreaming & {
thinking?: { type: "enabled" | "disabled" }
}
Expand Down Expand Up @@ -55,12 +57,16 @@ export class DeepSeekHandler extends OpenAiHandler {
const modelId = this.options.apiModelId ?? deepSeekDefaultModelId
const { info: modelInfo } = this.getModel()

// Check if this is a thinking-enabled model (deepseek-reasoner)
const isThinkingModel = modelId.includes("deepseek-reasoner")
// Check if this is a thinking-enabled model by looking up the exact model
// in the static definitions. We can't rely on the fallback from getModel()
// because unknown models would incorrectly inherit the default's capabilities.
const exactModelInfo = deepSeekModels[modelId as keyof typeof deepSeekModels]
const isThinkingModel =
exactModelInfo && "preserveReasoning" in exactModelInfo && exactModelInfo.preserveReasoning === true

// Convert messages to R1 format (merges consecutive same-role messages)
// This is required for DeepSeek which does not support successive messages with the same role
// For thinking models (deepseek-reasoner), enable mergeToolResultText to preserve reasoning_content
// For thinking models, enable mergeToolResultText to preserve reasoning_content
// during tool call sequences. Without this, environment_details text after tool_results would
// create user messages that cause DeepSeek to drop all previous reasoning_content.
// See: https://api-docs.deepseek.com/guides/thinking_mode
Expand All @@ -74,7 +80,7 @@ export class DeepSeekHandler extends OpenAiHandler {
messages: convertedMessages,
stream: true as const,
stream_options: { include_usage: true },
// Enable thinking mode for deepseek-reasoner or when tools are used with thinking model
// Enable thinking mode for models that support it (deepseek-reasoner, v4-pro, v4-flash)
...(isThinkingModel && { thinking: { type: "enabled" } }),
tools: this.convertToolsForOpenAI(metadata?.tools),
tool_choice: metadata?.tool_choice,
Expand Down
102 changes: 102 additions & 0 deletions src/api/providers/fetchers/deepseek.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import type { ModelRecord } from "@roo-code/types"
import { deepSeekModels, DEEP_SEEK_DEFAULT_TEMPERATURE } from "@roo-code/types"

import { DEFAULT_HEADERS } from "../constants"

/**
* Fetches available models from the DeepSeek API and merges them with known specs.
*
* The DeepSeek /models endpoint only returns basic model IDs without pricing
* or context window info, so we merge the API response with the static
* `deepSeekModels` map for known models. Unknown models get sensible defaults.
*
* @param baseUrl - The base URL for the DeepSeek API (default: https://api.deepseek.com)
* @param apiKey - Optional API key for authentication
* @returns A promise that resolves to a record of model IDs to model info
*/
export async function getDeepSeekModels(baseUrl?: string, apiKey?: string): Promise<ModelRecord> {
const normalizedBase = (baseUrl || "https://api.deepseek.com").replace(/\/?v1\/?$/, "")
const url = `${normalizedBase}/models`

try {
const headers: Record<string, string> = {
"Content-Type": "application/json",
...DEFAULT_HEADERS,
}

if (apiKey) {
headers["Authorization"] = `Bearer ${apiKey}`
}

const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), 10000)

try {
const response = await fetch(url, {
headers,
signal: controller.signal,
})

if (!response.ok) {
let errorBody = ""
try {
errorBody = await response.text()
} catch {
errorBody = "(unable to read response body)"
}

console.error(`[getDeepSeekModels] HTTP error:`, {
status: response.status,
statusText: response.statusText,
url,
body: errorBody,
})

throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}

const data = await response.json()

if (!data?.data || !Array.isArray(data.data)) {
console.error("[getDeepSeekModels] Unexpected response format:", data)
throw new Error("Failed to fetch DeepSeek models: Unexpected response format.")
}

const models: ModelRecord = {}

for (const model of data.data) {
const modelId = model.id as string

if (!modelId) continue

// If we have known specs for this model, use them
const knownSpecs = deepSeekModels[modelId as keyof typeof deepSeekModels]

if (knownSpecs) {
models[modelId] = { ...knownSpecs }
} else {
// Unknown model - use sensible defaults based on DeepSeek's typical specs
models[modelId] = {
maxTokens: 8192,
contextWindow: 128_000,
supportsImages: false,
supportsPromptCache: true,
inputPrice: 0.28,
outputPrice: 0.42,
cacheWritesPrice: 0.28,
cacheReadsPrice: 0.028,
defaultTemperature: DEEP_SEEK_DEFAULT_TEMPERATURE,
description: `DeepSeek model: ${modelId}`,
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}
}

return models
} finally {
clearTimeout(timeoutId)
}
} catch (error) {
console.error(`[getDeepSeekModels] Failed to fetch models:`, error)
throw error
}
}
4 changes: 4 additions & 0 deletions src/api/providers/fetchers/modelCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { getOllamaModels } from "./ollama"
import { getLMStudioModels } from "./lmstudio"
import { getPoeModels } from "./poe"
import { getRooModels } from "./roo"
import { getDeepSeekModels } from "./deepseek"

const memoryCache = new NodeCache({ stdTTL: 5 * 60, checkperiod: 5 * 60 })

Expand Down Expand Up @@ -95,6 +96,9 @@ async function fetchModelsFromProvider(options: GetModelsOptions): Promise<Model
case "poe":
models = await getPoeModels(options.apiKey, options.baseUrl)
break
case "deepseek":
models = await getDeepSeekModels(options.baseUrl, options.apiKey)
break
default: {
// Ensures router is exhaustively checked if RouterName is a strict union.
const exhaustiveCheck: never = provider
Expand Down
16 changes: 16 additions & 0 deletions src/core/webview/webviewMessageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -928,6 +928,7 @@ export const webviewMessageHandler = async (
lmstudio: {},
roo: {},
poe: {},
deepseek: {},
}

const safeGetModels = async (options: GetModelsOptions): Promise<ModelRecord> => {
Expand Down Expand Up @@ -996,6 +997,21 @@ export const webviewMessageHandler = async (
})
}

// DeepSeek is conditional on apiKey
const deepSeekApiKey = apiConfiguration.deepSeekApiKey || message?.values?.deepSeekApiKey
const deepSeekBaseUrl = apiConfiguration.deepSeekBaseUrl || message?.values?.deepSeekBaseUrl

if (deepSeekApiKey) {
if (message?.values?.deepSeekApiKey || message?.values?.deepSeekBaseUrl) {
await flushModels({ provider: "deepseek", apiKey: deepSeekApiKey, baseUrl: deepSeekBaseUrl }, true)
}

candidates.push({
key: "deepseek",
options: { provider: "deepseek", apiKey: deepSeekApiKey, baseUrl: deepSeekBaseUrl },
})
}

// Apply single provider filter if specified
const modelFetchPromises = providerFilter
? candidates.filter(({ key }) => key === providerFilter)
Expand Down
1 change: 1 addition & 0 deletions src/shared/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ const dynamicProviderExtras = {
lmstudio: {} as {}, // eslint-disable-line @typescript-eslint/no-empty-object-type
roo: {} as { apiKey?: string; baseUrl?: string },
poe: {} as { apiKey?: string; baseUrl?: string },
deepseek: {} as { apiKey?: string; baseUrl?: string },
} as const satisfies Record<RouterName, object>

// Build the dynamic options union from the map, intersected with CommonFetchParams
Expand Down
3 changes: 3 additions & 0 deletions webview-ui/src/components/settings/ApiOptions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -613,7 +613,10 @@ const ApiOptions = ({
<DeepSeek
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
routerModels={routerModels}
simplifySettings={fromWelcomeView}
organizationAllowList={organizationAllowList}
modelValidationError={modelValidationError}
/>
)}

Expand Down
28 changes: 26 additions & 2 deletions webview-ui/src/components/settings/providers/DeepSeek.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,32 @@
import { useCallback } from "react"
import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"

import type { ProviderSettings } from "@roo-code/types"
import type { ProviderSettings, OrganizationAllowList, RouterModels } from "@roo-code/types"
import { deepSeekDefaultModelId } from "@roo-code/types"

import { useAppTranslation } from "@src/i18n/TranslationContext"
import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"

import { inputEventTransform } from "../transforms"
import { ModelPicker } from "../ModelPicker"

type DeepSeekProps = {
apiConfiguration: ProviderSettings
setApiConfigurationField: (field: keyof ProviderSettings, value: ProviderSettings[keyof ProviderSettings]) => void
routerModels?: RouterModels
simplifySettings?: boolean
organizationAllowList: OrganizationAllowList
modelValidationError?: string
}

export const DeepSeek = ({ apiConfiguration, setApiConfigurationField }: DeepSeekProps) => {
export const DeepSeek = ({
apiConfiguration,
setApiConfigurationField,
routerModels,
simplifySettings,
organizationAllowList,
modelValidationError,
}: DeepSeekProps) => {
const { t } = useAppTranslation()

const handleInputChange = useCallback(
Expand Down Expand Up @@ -46,6 +58,18 @@ export const DeepSeek = ({ apiConfiguration, setApiConfigurationField }: DeepSee
{t("settings:providers.getDeepSeekApiKey")}
</VSCodeButtonLink>
)}
<ModelPicker
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
defaultModelId={deepSeekDefaultModelId}
models={routerModels?.deepseek ?? {}}
modelIdKey="apiModelId"
serviceName="DeepSeek"
serviceUrl="https://platform.deepseek.com"
organizationAllowList={organizationAllowList}
errorMessage={modelValidationError}
simplifySettings={simplifySettings}
/>
</>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -182,14 +182,14 @@ describe("providerModelConfig", () => {
expect(shouldUseGenericModelPicker("anthropic")).toBe(true)
expect(shouldUseGenericModelPicker("bedrock")).toBe(true)
expect(shouldUseGenericModelPicker("gemini")).toBe(true)
expect(shouldUseGenericModelPicker("deepseek")).toBe(true)
})

it("returns false for providers with custom model UI", () => {
expect(shouldUseGenericModelPicker("openrouter")).toBe(false)
expect(shouldUseGenericModelPicker("ollama")).toBe(false)
expect(shouldUseGenericModelPicker("lmstudio")).toBe(false)
expect(shouldUseGenericModelPicker("vscode-lm")).toBe(false)
expect(shouldUseGenericModelPicker("deepseek")).toBe(false)
})

it("returns false for providers without static models", () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ export const PROVIDERS_WITH_CUSTOM_MODEL_UI: ProviderName[] = [
"ollama",
"lmstudio",
"vscode-lm",
"deepseek", // DeepSeek has its own component with ModelPicker using dynamic models
]

/**
Expand Down
Loading