Skip to content
Merged
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
4 changes: 3 additions & 1 deletion packages/types/src/providers/deepseek.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,14 @@ export const deepSeekModels = {
supportsReasoningEffort: ["disable", "low", "high", "max"], // Updated 2026-08-13
preserveReasoning: true,
reasoningEffort: "high",
supportsTemperature: true,
defaultTemperature: 1.0,
inputPrice: 0, // the inputs are priced as cache read/write, so `inputPrice` should be 0
// Static estimates use peak rates; off-peak rates are 50% lower.
outputPrice: 1.32,
cacheWritesPrice: 0.44,
cacheReadsPrice: 0.014,
description: `DeepSeek-V4-Flash-Vision-Exp is DeepSeek's experimental multimodal V4 Flash model with image understanding. It supports thinking and non-thinking modes, JSON output, tool calls, chat prefix completion (beta), and image inputs.`,
description: `DeepSeek-V4-Flash-Vision-Exp is DeepSeek's experimental multimodal V4 Flash model with image understanding. It supports thinking and non-thinking modes, JSON output, tool calls, chat prefix completion (beta), and image input through Chat Completions, Responses, and Anthropic-compatible APIs.`,
},
} as const satisfies Record<string, ModelInfo>

Expand Down
14 changes: 14 additions & 0 deletions packages/types/src/providers/fireworks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export type FireworksModelId =
| "accounts/fireworks/models/deepseek-v3p2"
| "accounts/fireworks/models/deepseek-v4-pro"
| "accounts/fireworks/models/deepseek-v4-pro-0813"
| "accounts/fireworks/models/deepseek-v4-flash-vision-exp"
| "accounts/fireworks/models/glm-4p5"
| "accounts/fireworks/models/glm-4p5-air"
| "accounts/fireworks/models/glm-4p6"
Expand Down Expand Up @@ -280,6 +281,19 @@ export const fireworksModels = {
description:
"DeepSeek V4 Pro 0813 is DeepSeek's production checkpoint for advanced reasoning, coding, and long-context agentic workloads.",
},
"accounts/fireworks/models/deepseek-v4-flash-vision-exp": {
displayName: "DeepSeek V4 Flash Vision Exp",
maxTokens: 384_000,
contextWindow: 1_048_576,
supportsImages: true,
supportsPromptCache: true,
supportsMaxTokens: true,
inputPrice: 0.22,
outputPrice: 0.66,
cacheReadsPrice: 0.007,
description:
"DeepSeek V4 Flash Vision Exp is an experimental multimodal model with text and image input, function calling, and long-context support.",
},
"accounts/fireworks/models/kimi-k2p7-code": {
maxTokens: 16384,
contextWindow: 262144,
Expand Down
2 changes: 1 addition & 1 deletion src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ export function buildApiHandler(configuration: ProviderSettings): ApiHandler {
case providerIdentifiers.poe:
return new PoeHandler(options)
case providerIdentifiers.geminiCli:
// Intentionally falls through to the Anthropic handler pending a dedicated Gemini CLI handler implementation.
// Intentionally falls through to the Anthropic handler pending a dedicated Gemini CLI handler implementation.
default:
return new AnthropicHandler(options)
}
Expand Down
92 changes: 50 additions & 42 deletions src/api/providers/__tests__/deepseek.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,16 +249,23 @@ describe("DeepSeekHandler", () => {
expect((model.info as ModelInfo).reasoningEffort).toBe("high")
})

it("should return vision model info for deepseek-v4-flash-vision-exp", () => {
const handlerWithVision = new DeepSeekHandler({
it("should return vision metadata for deepseek-v4-flash-vision-exp", () => {
const visionHandler = new DeepSeekHandler({
...mockOptions,
apiModelId: "deepseek-v4-flash-vision-exp",
})
const model = handlerWithVision.getModel()
const model = visionHandler.getModel()

expect(model.id).toBe("deepseek-v4-flash-vision-exp")
expect(model.info.supportsImages).toBe(true)
expect(model.info.supportsPromptCache).toBe(true)
expect((model.info as ModelInfo).preserveReasoning).toBe(true)
expect(model.info).toMatchObject({
maxTokens: 384_000,
contextWindow: 1_000_000,
supportsImages: true,
supportsPromptCache: true,
preserveReasoning: true,
reasoningEffort: "high",
defaultTemperature: 1.0,
})
})

it("should return provided model ID with default model info if model does not exist", () => {
Expand Down Expand Up @@ -336,6 +343,43 @@ describe("DeepSeekHandler", () => {
expect(textChunks[0].text).toBe("Test response")
})

it("should send images and V4 thinking controls to deepseek-v4-flash-vision-exp", async () => {
const visionHandler = new DeepSeekHandler({
...mockOptions,
apiModelId: "deepseek-v4-flash-vision-exp",
})
const visionMessages: Anthropic.Messages.MessageParam[] = [
{
role: "user",
content: [
{ type: "text", text: "Describe this image." },
{
type: "image",
source: { type: "base64", media_type: "image/png", data: "image-data" },
},
],
},
]

await collectStream(visionHandler.createMessage(systemPrompt, visionMessages))

const callArgs = mockCreate.mock.calls[0][0]
expect(callArgs).toMatchObject({
model: "deepseek-v4-flash-vision-exp",
thinking: { type: "enabled" },
reasoning_effort: "high",
max_completion_tokens: 200_000,
})
expect(callArgs.temperature).toBeUndefined()
expect(callArgs.messages).toContainEqual({
role: "user",
content: expect.arrayContaining([
{ type: "text", text: expect.stringContaining("Describe this image.") },
{ type: "image_url", image_url: { url: "data:image/png;base64,image-data" } },
]),
})
})

it("should include usage information", async () => {
const chunks: any[] = await collectStream(handler.createMessage(systemPrompt, messages))

Expand Down Expand Up @@ -424,42 +468,6 @@ describe("DeepSeekHandler", () => {
const reasoningChunks = chunks.filter((chunk) => chunk.type === "reasoning")
expect(reasoningChunks).toEqual([{ type: "reasoning", text: "primary thought" }])
})

it("enables thinking and forwards image_url for the vision model", async () => {
const visionHandler = new DeepSeekHandler({
...mockOptions,
apiModelId: "deepseek-v4-flash-vision-exp",
})
const withImage: Anthropic.Messages.MessageParam[] = [
{
role: "user",
content: [
{ type: "text", text: "Describe:" },
{ type: "image", source: { type: "base64", media_type: "image/png", data: "abc" } },
],
},
]

await collectStream(visionHandler.createMessage(systemPrompt, withImage))

const callArgs = mockCreate.mock.calls[0][0]
expect(callArgs.thinking).toEqual({ type: "enabled" })
expect(callArgs.messages).toEqual(
expect.arrayContaining([
expect.objectContaining({
role: "user",
content: expect.arrayContaining([
expect.objectContaining({
type: "image_url",
image_url: expect.objectContaining({
url: "data:image/png;base64,abc",
}),
}),
]),
}),
]),
)
})
})

describe("processUsageMetrics", () => {
Expand Down
17 changes: 17 additions & 0 deletions src/api/providers/__tests__/fireworks.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,13 @@ describe("FireworksHandler", () => {
outputPrice: 3.96,
cacheReadsPrice: 0.044,
},
{
modelId: "accounts/fireworks/models/deepseek-v4-flash-vision-exp" as const,
contextWindow: 1_048_576,
inputPrice: 0.22,
outputPrice: 0.66,
cacheReadsPrice: 0.007,
},
])(
"should expose newly added model $modelId",
({ modelId, contextWindow, inputPrice, outputPrice, cacheReadsPrice }) => {
Expand All @@ -147,6 +154,16 @@ describe("FireworksHandler", () => {
},
)

it("should expose vision support for DeepSeek V4 Flash Vision Exp", () => {
const model = fireworksModels["accounts/fireworks/models/deepseek-v4-flash-vision-exp"]

expect(model).toMatchObject({
supportsImages: true,
supportsPromptCache: true,
supportsMaxTokens: true,
})
})

it("should return Kimi K2 Instruct model with correct configuration", () => {
const testModelId: FireworksModelId = "accounts/fireworks/models/kimi-k2-instruct"
const handlerWithModel = new FireworksHandler({
Expand Down
1 change: 1 addition & 0 deletions src/api/providers/fetchers/__tests__/deepseek.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ describe("getDeepSeekModels", () => {
expect(globalThis.fetch).toHaveBeenCalledWith("http://127.0.0.1:43123/models", expect.any(Object))
expect(models["deepseek-v4-flash"]).toEqual(deepSeekModels["deepseek-v4-flash"])
expect(models["deepseek-v4-pro"]).toEqual(deepSeekModels["deepseek-v4-pro"])
expect(models["deepseek-v4-flash-vision-exp"]).toEqual(deepSeekModels["deepseek-v4-flash-vision-exp"])
})

it("throws for 404 responses when fallback flag is not enabled", async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,21 @@ describe("useSelectedModel", () => {
},
)

it("selects static vision metadata when the DeepSeek catalog is unavailable", () => {
const modelId = "deepseek-v4-flash-vision-exp"
mockUseRouterModels.mockReturnValue(createRouterModelsResult({ [providerIdentifiers.deepseek]: null }))
mockUseOpenRouterModelProviders.mockReturnValue(createOpenRouterModelProvidersResult({}))

const { result } = renderHook(
() => useSelectedModel({ apiProvider: providerIdentifiers.deepseek, apiModelId: modelId }),
{ wrapper: createWrapper() },
)

expect(result.current.id).toBe(modelId)
expect(result.current.info).toEqual(deepSeekModels[modelId])
expect(result.current.info?.supportsImages).toBe(true)
})

it.each([providerIdentifiers.deepseek, providerIdentifiers.moonshot])(
"falls back to static data when the %s router catalog is null",
(provider) => {
Expand Down
Loading