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
7 changes: 7 additions & 0 deletions core/providers/anthropic/chat.go
Original file line number Diff line number Diff line change
Expand Up @@ -582,6 +582,13 @@ func ToAnthropicChatRequest(ctx *schemas.BifrostContext, bifrostReq *schemas.Bif
}
}

// DeepSeek rejects a forced tool_choice while thinking is enabled (which is
// the default). Force thinking off when tool_choice pins a specific tool.
if bifrostReq.Provider == schemas.DeepSeek && anthropicReq.ToolChoice != nil &&
anthropicReq.ToolChoice.Type == "tool" {
anthropicReq.Thinking = &AnthropicThinking{Type: "disabled"}
}

// Convert service tier
if bifrostReq.Params.ServiceTier != nil {
mapped := MapBifrostServiceTierToAnthropicRequest(*bifrostReq.Params.ServiceTier)
Expand Down
4 changes: 3 additions & 1 deletion core/providers/anthropic/requestbuilder.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ var AnthropicProviderRequestDefaultsMap = map[schemas.ModelProvider]AnthropicPro
schemas.BedrockMantle: {
RemapToolVersions: true,
},
schemas.DeepSeek: {},
// Vertex publisher endpoint: model + region in URL, anthropic_version
// required, beta headers in body (not HTTP), cache_control.scope stripped
// at marshal time, tool versions remapped.
Expand All @@ -124,6 +125,7 @@ var AnthropicProviderRequestDefaultsMap = map[schemas.ModelProvider]AnthropicPro
RemapToolVersions: true,
InjectBetaHeadersIntoBody: true,
},
schemas.SGL: {},
}

// BuildAnthropicResponsesRequestBody is the single implementation of the
Expand Down Expand Up @@ -617,4 +619,4 @@ func BuildAnthropicChatRequestBody(ctx *schemas.BifrostContext, request *schemas
}

return jsonBody, nil
}
}
7 changes: 7 additions & 0 deletions core/providers/anthropic/responses.go
Original file line number Diff line number Diff line change
Expand Up @@ -3736,6 +3736,13 @@ func ToAnthropicResponsesRequest(ctx *schemas.BifrostContext, bifrostReq *schema
anthropicReq.ToolChoice = anthropicToolChoice
}
}

// DeepSeek rejects a forced tool_choice while thinking is on. Force thinking
// off when tool_choice pins a specific tool.
if bifrostReq.Provider == schemas.DeepSeek && anthropicReq.ToolChoice != nil &&
anthropicReq.ToolChoice.Type == "tool" {
anthropicReq.Thinking = &AnthropicThinking{Type: "disabled"}
}
}

if bifrostReq.Input != nil {
Expand Down
17 changes: 16 additions & 1 deletion core/providers/anthropic/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,21 @@ var ProviderFeatures = map[schemas.ModelProvider]ProviderFeatureSupport{
// FastMode, InferenceGeo, AdvisorTool, TaskBudgets — not documented on Az-platform; leave off.
ServiceTier: true,
},
schemas.DeepSeek: {
WebSearch: true,
WebSearchDynamic: true,
ContainerBasic: true,
ContextManagementField: true,
Compaction: true,
ContextEditing: true,
PromptCachingScope: true,
AdvancedToolUse: true,
InputExamples: true,
EagerInputStreaming: true,
StructuredOutputs: true,
InterleavedThinking: true,
ServiceTier: true,
},
}

// ==================== REQUEST TYPES ====================
Expand Down Expand Up @@ -1861,4 +1876,4 @@ func parseAnthropicFileTimestamp(timestamp string) int64 {
// AnthropicCountTokensResponse models the payload returned by Anthropic's count tokens endpoint.
type AnthropicCountTokensResponse struct {
InputTokens int `json:"input_tokens"`
}
}
8 changes: 8 additions & 0 deletions core/providers/anthropic/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -3353,3 +3353,11 @@ func IsClaudeCodeRequest(ctx *schemas.BifrostContext) bool {
}
return false
}

// ResolveUseAnthropicEndpoints reports whether the request should be routed through Anthropic-compatible endpoints
func ResolveUseAnthropicEndpoints(ctx *schemas.BifrostContext, key schemas.Key) bool {
if ra := schemas.GetResolvedAlias(ctx); ra != nil && ra.Config != nil && ra.Config.UseAnthropicEndpoints != nil {
return *ra.Config.UseAnthropicEndpoints
}
return key.UseAnthropicEndpoints != nil && *key.UseAnthropicEndpoints
}
172 changes: 168 additions & 4 deletions core/providers/deepseek/deepseek.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"strings"
"time"

"github.com/maximhq/bifrost/core/providers/anthropic"
"github.com/maximhq/bifrost/core/providers/openai"
providerUtils "github.com/maximhq/bifrost/core/providers/utils"
schemas "github.com/maximhq/bifrost/core/schemas"
Expand Down Expand Up @@ -60,6 +61,69 @@ func NewDeepSeekProvider(config *schemas.ProviderConfig, logger schemas.Logger)
}, nil
}

func (provider *DeepSeekProvider) anthropicHeaders(key schemas.Key) map[string]string {
headers := map[string]string{}
if key.Value.GetValue() != "" {
headers["x-api-key"] = key.Value.GetValue()
}
return headers
}

// disableThinkingForForcedToolChoice disables thinking when it would otherwise be
// rejected by DeepSeek's OpenAI-compatible endpoint. This covers two distinct cases:
//
// 1. A forced tool_choice ("required"/"any", or the struct form pinning a specific
// function/custom/allowed_tools call) — DeepSeek rejects a forced tool_choice while
// thinking is enabled (the default).
// 2. A conversation that already contains an assistant turn without reasoning_content
// (e.g. synthetic/injected history, or a turn produced while thinking was off) —
// DeepSeek requires prior reasoning_content to be replayed once thinking is on, so if
// any assistant turn is missing it, thinking must stay off for the whole request.
func disableThinkingForForcedToolChoice(request *schemas.BifrostChatRequest) {
if request.Params == nil {
return
}

disable := false

if tc := request.Params.ToolChoice; tc != nil {
switch {
case tc.ChatToolChoiceStr != nil:
switch schemas.ChatToolChoiceType(*tc.ChatToolChoiceStr) {
case schemas.ChatToolChoiceTypeRequired, schemas.ChatToolChoiceTypeAny:
disable = true
}
case tc.ChatToolChoiceStruct != nil:
switch tc.ChatToolChoiceStruct.Type {
case schemas.ChatToolChoiceTypeRequired, schemas.ChatToolChoiceTypeAny,
schemas.ChatToolChoiceTypeFunction, schemas.ChatToolChoiceTypeCustom,
schemas.ChatToolChoiceTypeAllowedTools:
disable = true
}
}
}

if !disable {
for _, msg := range request.Input {
if msg.Role != schemas.ChatMessageRoleAssistant {
continue
}
if msg.ChatAssistantMessage == nil || msg.ChatAssistantMessage.Reasoning == nil {
disable = true
break
}
}
}

if !disable {
return
}
if request.Params.ExtraParams == nil {
request.Params.ExtraParams = make(map[string]any, 1)
}
request.Params.ExtraParams["thinking"] = map[string]any{"type": "disabled"}
}

// GetProviderKey returns the provider identifier for DeepSeek.
func (provider *DeepSeekProvider) GetProviderKey() schemas.ModelProvider {
return schemas.DeepSeek
Expand Down Expand Up @@ -126,9 +190,28 @@ func (provider *DeepSeekProvider) TextCompletionStream(ctx *schemas.BifrostConte
)
}

// ChatCompletion performs a chat completion request to the DeepSeek API.
// ChatCompletion performs a chat completion request to DeepSeek's Anthropic-compatible API.
func (provider *DeepSeekProvider) ChatCompletion(ctx *schemas.BifrostContext, key schemas.Key, request *schemas.BifrostChatRequest) (*schemas.BifrostChatResponse, *schemas.BifrostError) {
if anthropic.ResolveUseAnthropicEndpoints(ctx, key) {
return anthropic.HandleAnthropicChatCompletionRequest(
ctx,
provider.client,
provider.networkConfig.BaseURL+providerUtils.GetPathFromContext(ctx, "/anthropic/v1/messages"),
request,
anthropic.AnthropicRequestBuildConfig{
Provider: schemas.DeepSeek,
ShouldSendBackRawRequest: provider.sendBackRawRequest,
ShouldSendBackRawResponse: provider.sendBackRawResponse,
},
provider.anthropicHeaders(key),
provider.networkConfig.ExtraHeaders,
nil,
provider.logger,
)
}

ctx.SetValue(schemas.BifrostContextKeyPassthroughExtraParams, true)
disableThinkingForForcedToolChoice(request)
return openai.HandleOpenAIChatCompletionRequest(
ctx,
provider.client,
Expand All @@ -146,12 +229,43 @@ func (provider *DeepSeekProvider) ChatCompletion(ctx *schemas.BifrostContext, ke
)
}

// ChatCompletionStream performs a streaming chat completion request to the DeepSeek API.
// ChatCompletionStream performs a streaming chat completion request to DeepSeek's Anthropic-compatible API.
// It supports real-time streaming of responses using Server-Sent Events (SSE).
// Uses DeepSeek's OpenAI-compatible streaming format.
// Returns a channel containing BifrostStreamChunk objects representing the stream or an error if the request fails.
func (provider *DeepSeekProvider) ChatCompletionStream(ctx *schemas.BifrostContext, postHookRunner schemas.PostHookRunner, postHookSpanFinalizer func(context.Context), key schemas.Key, request *schemas.BifrostChatRequest) (chan *schemas.BifrostStreamChunk, *schemas.BifrostError) {
if anthropic.ResolveUseAnthropicEndpoints(ctx, key) {
jsonData, bifrostErr := anthropic.BuildAnthropicChatRequestBody(ctx, request, anthropic.AnthropicRequestBuildConfig{
Provider: schemas.DeepSeek,
IsStreaming: true,
ShouldSendBackRawRequest: provider.sendBackRawRequest,
ShouldSendBackRawResponse: provider.sendBackRawResponse,
})
if bifrostErr != nil {
return nil, bifrostErr
}

return anthropic.HandleAnthropicChatCompletionStreaming(
ctx,
provider.streamingClient,
provider.networkConfig.BaseURL+providerUtils.GetPathFromContext(ctx, "/anthropic/v1/messages"),
jsonData,
provider.anthropicHeaders(key),
provider.networkConfig.ExtraHeaders,
provider.networkConfig.StreamIdleTimeoutInSeconds,
provider.networkConfig.BetaHeaderOverrides,
providerUtils.ShouldSendBackRawRequest(ctx, provider.sendBackRawRequest),
providerUtils.ShouldSendBackRawResponse(ctx, provider.sendBackRawResponse),
schemas.DeepSeek,
postHookRunner,
nil,
nil,
provider.logger,
postHookSpanFinalizer,
)
}

ctx.SetValue(schemas.BifrostContextKeyPassthroughExtraParams, true)
disableThinkingForForcedToolChoice(request)
return openai.HandleOpenAIChatCompletionStreaming(
ctx,
provider.streamingClient,
Expand All @@ -175,7 +289,26 @@ func (provider *DeepSeekProvider) ChatCompletionStream(ctx *schemas.BifrostConte
)
}

// Responses performs a Responses API request against DeepSeek's Anthropic-compatible endpoint.
func (provider *DeepSeekProvider) Responses(ctx *schemas.BifrostContext, key schemas.Key, request *schemas.BifrostResponsesRequest) (*schemas.BifrostResponsesResponse, *schemas.BifrostError) {
if anthropic.ResolveUseAnthropicEndpoints(ctx, key) {
return anthropic.HandleAnthropicResponsesRequest(
ctx,
provider.client,
provider.networkConfig.BaseURL+providerUtils.GetPathFromContext(ctx, "/anthropic/v1/messages"),
request,
anthropic.AnthropicRequestBuildConfig{
Provider: schemas.DeepSeek,
ShouldSendBackRawRequest: provider.sendBackRawRequest,
ShouldSendBackRawResponse: provider.sendBackRawResponse,
},
provider.anthropicHeaders(key),
provider.networkConfig.ExtraHeaders,
nil,
provider.logger,
)
}

chatResponse, err := provider.ChatCompletion(ctx, key, request.ToChatRequest())
if err != nil {
return nil, err
Expand All @@ -186,8 +319,39 @@ func (provider *DeepSeekProvider) Responses(ctx *schemas.BifrostContext, key sch
return response, nil
}

// ResponsesStream performs a streaming responses request to the DeepSeek API.
// ResponsesStream performs a streaming Responses API request to DeepSeek's Anthropic-compatible endpoint.
func (provider *DeepSeekProvider) ResponsesStream(ctx *schemas.BifrostContext, postHookRunner schemas.PostHookRunner, postHookSpanFinalizer func(context.Context), key schemas.Key, request *schemas.BifrostResponsesRequest) (chan *schemas.BifrostStreamChunk, *schemas.BifrostError) {
if anthropic.ResolveUseAnthropicEndpoints(ctx, key) {
jsonData, bifrostErr := anthropic.BuildAnthropicResponsesRequestBody(ctx, request, anthropic.AnthropicRequestBuildConfig{
Provider: schemas.DeepSeek,
IsStreaming: true,
ShouldSendBackRawRequest: provider.sendBackRawRequest,
ShouldSendBackRawResponse: provider.sendBackRawResponse,
})
if bifrostErr != nil {
return nil, bifrostErr
}

return anthropic.HandleAnthropicResponsesStream(
ctx,
provider.streamingClient,
provider.networkConfig.BaseURL+providerUtils.GetPathFromContext(ctx, "/anthropic/v1/messages"),
jsonData,
provider.anthropicHeaders(key),
provider.networkConfig.ExtraHeaders,
provider.networkConfig.StreamIdleTimeoutInSeconds,
provider.networkConfig.BetaHeaderOverrides,
providerUtils.ShouldSendBackRawRequest(ctx, provider.sendBackRawRequest),
providerUtils.ShouldSendBackRawResponse(ctx, provider.sendBackRawResponse),
provider.GetProviderKey(),
postHookRunner,
nil,
nil,
provider.logger,
postHookSpanFinalizer,
)
}

ctx.SetValue(schemas.BifrostContextKeyIsResponsesToChatCompletionFallback, true)
return provider.ChatCompletionStream(
ctx,
Expand Down
Loading
Loading