From 2af938fb7ae500918226be6d4acbd384f5ab18b8 Mon Sep 17 00:00:00 2001
From: Samyabrata Maji <116789799+sammaji@users.noreply.github.com>
Date: Thu, 16 Jul 2026 19:54:45 +0530
Subject: [PATCH 1/3] fix: adds sgl anthropic compatible apis via key level
setting
---
core/providers/anthropic/requestbuilder.go | 3 +-
core/providers/anthropic/utils.go | 8 +
core/providers/sgl/sgl.go | 144 +++++++++++++++++-
core/schemas/account.go | 5 +-
framework/configstore/clientconfig.go | 17 ++-
framework/configstore/migrations.go | 32 +++-
framework/configstore/rdb.go | 8 +-
framework/configstore/tables/key.go | 12 ++
transports/bifrost-http/lib/config.go | 4 +-
transports/config.schema.json | 127 ++++++++++++++-
.../fragments/apiKeysFormFragment.tsx | 23 +++
.../providers/fragments/deploymentsTable.tsx | 28 ++++
ui/lib/types/config.ts | 2 +
ui/lib/types/schemas.ts | 2 +
14 files changed, 399 insertions(+), 16 deletions(-)
diff --git a/core/providers/anthropic/requestbuilder.go b/core/providers/anthropic/requestbuilder.go
index 20950d7cc2c..301e4a9706b 100644
--- a/core/providers/anthropic/requestbuilder.go
+++ b/core/providers/anthropic/requestbuilder.go
@@ -124,6 +124,7 @@ var AnthropicProviderRequestDefaultsMap = map[schemas.ModelProvider]AnthropicPro
RemapToolVersions: true,
InjectBetaHeadersIntoBody: true,
},
+ schemas.SGL: {},
}
// BuildAnthropicResponsesRequestBody is the single implementation of the
@@ -617,4 +618,4 @@ func BuildAnthropicChatRequestBody(ctx *schemas.BifrostContext, request *schemas
}
return jsonBody, nil
-}
+}
\ No newline at end of file
diff --git a/core/providers/anthropic/utils.go b/core/providers/anthropic/utils.go
index 2b9a2fc9658..ce6ff0e0305 100644
--- a/core/providers/anthropic/utils.go
+++ b/core/providers/anthropic/utils.go
@@ -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
+}
\ No newline at end of file
diff --git a/core/providers/sgl/sgl.go b/core/providers/sgl/sgl.go
index 558c51fd23a..a89e750de77 100644
--- a/core/providers/sgl/sgl.go
+++ b/core/providers/sgl/sgl.go
@@ -7,12 +7,17 @@ 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"
"github.com/valyala/fasthttp"
)
+// sglAnthropicVersion is the Anthropic API version sent as the "anthropic-version" header on
+// SGLang's Anthropic-compatible endpoint.
+const sglAnthropicVersion = "2023-06-01"
+
// SGLProvider implements the Provider interface for SGL's API.
type SGLProvider struct {
logger schemas.Logger // Logger for provider operations
@@ -80,6 +85,13 @@ func (provider *SGLProvider) getBaseURL(key schemas.Key) string {
return ""
}
+// anthropicHeaders builds the auth and version headers for SGL's Anthropic-compatible endpoint.
+func anthropicHeaders(key schemas.Key) map[string]string {
+ headers := openai.BearerAuthHeader(key)
+ headers["anthropic-version"] = sglAnthropicVersion
+ return headers
+}
+
// baseURLOrError returns the resolved base URL or a BifrostError when none is configured.
func (provider *SGLProvider) baseURLOrError(key schemas.Key) (string, *schemas.BifrostError) {
u := provider.getBaseURL(key)
@@ -124,7 +136,10 @@ func (provider *SGLProvider) ListModels(ctx *schemas.BifrostContext, keys []sche
)
}
-// TextCompletion performs a text completion request to the SGL API.
+// TextCompletion performs a text completion request to the SGL API. UseAnthropicEndpoints has
+// no effect here: Anthropic's legacy text-completions surface has no reusable shared handler
+// and SGLang's Anthropic-compatible layer doesn't expose one either, so this always uses the
+// OpenAI-compatible /v1/completions endpoint.
func (provider *SGLProvider) TextCompletion(ctx *schemas.BifrostContext, key schemas.Key, request *schemas.BifrostTextCompletionRequest) (*schemas.BifrostTextCompletionResponse, *schemas.BifrostError) {
ctx.SetValue(schemas.BifrostContextKeyPassthroughExtraParams, true)
baseURL, bifrostErr := provider.baseURLOrError(key)
@@ -176,13 +191,35 @@ func (provider *SGLProvider) TextCompletionStream(ctx *schemas.BifrostContext, p
)
}
-// ChatCompletion performs a chat completion request to the SGL API.
+// ChatCompletion performs a chat completion request to the SGL API. When the key (or the
+// resolved alias) has UseAnthropicEndpoints set, the request is routed through SGLang's
+// Anthropic-compatible Messages endpoint instead of its OpenAI-compatible one.
func (provider *SGLProvider) ChatCompletion(ctx *schemas.BifrostContext, key schemas.Key, request *schemas.BifrostChatRequest) (*schemas.BifrostChatResponse, *schemas.BifrostError) {
ctx.SetValue(schemas.BifrostContextKeyPassthroughExtraParams, true)
baseURL, bifrostErr := provider.baseURLOrError(key)
if bifrostErr != nil {
return nil, bifrostErr
}
+
+ if anthropic.ResolveUseAnthropicEndpoints(ctx, key) {
+ return anthropic.HandleAnthropicChatCompletionRequest(
+ ctx,
+ provider.client,
+ baseURL+providerUtils.GetPathFromContext(ctx, "/v1/messages"),
+ request,
+ anthropic.AnthropicRequestBuildConfig{
+ Provider: provider.GetProviderKey(),
+ BetaHeaderOverrides: provider.networkConfig.BetaHeaderOverrides,
+ ShouldSendBackRawRequest: provider.sendBackRawRequest,
+ ShouldSendBackRawResponse: provider.sendBackRawResponse,
+ },
+ anthropicHeaders(key),
+ provider.networkConfig.ExtraHeaders,
+ nil,
+ provider.logger,
+ )
+ }
+
return openai.HandleOpenAIChatCompletionRequest(
ctx,
provider.client,
@@ -210,6 +247,39 @@ func (provider *SGLProvider) ChatCompletionStream(ctx *schemas.BifrostContext, p
if bifrostErr != nil {
return nil, bifrostErr
}
+
+ if anthropic.ResolveUseAnthropicEndpoints(ctx, key) {
+ url := baseURL + providerUtils.GetPathFromContext(ctx, "/v1/messages")
+ jsonData, bifrostErr := anthropic.BuildAnthropicChatRequestBody(ctx, request, anthropic.AnthropicRequestBuildConfig{
+ Provider: provider.GetProviderKey(),
+ IsStreaming: true,
+ BetaHeaderOverrides: provider.networkConfig.BetaHeaderOverrides,
+ ShouldSendBackRawRequest: provider.sendBackRawRequest,
+ ShouldSendBackRawResponse: provider.sendBackRawResponse,
+ })
+ if bifrostErr != nil {
+ return nil, bifrostErr
+ }
+ return anthropic.HandleAnthropicChatCompletionStreaming(
+ ctx,
+ provider.streamingClient,
+ url,
+ jsonData,
+ 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,
+ )
+ }
+
return openai.HandleOpenAIChatCompletionStreaming(
ctx,
provider.streamingClient,
@@ -233,8 +303,36 @@ func (provider *SGLProvider) ChatCompletionStream(ctx *schemas.BifrostContext, p
)
}
-// Responses performs a responses request to the SGL API.
+// Responses performs a responses request to the SGL API. When the key (or the resolved alias)
+// has UseAnthropicEndpoints set, the request is routed through SGLang's Anthropic-compatible
+// Messages endpoint directly (rather than falling back through ChatCompletion), since the
+// Anthropic responses handler operates on the Responses-shaped request natively.
func (provider *SGLProvider) Responses(ctx *schemas.BifrostContext, key schemas.Key, request *schemas.BifrostResponsesRequest) (*schemas.BifrostResponsesResponse, *schemas.BifrostError) {
+ if anthropic.ResolveUseAnthropicEndpoints(ctx, key) {
+ ctx.SetValue(schemas.BifrostContextKeyPassthroughExtraParams, true)
+ baseURL, bifrostErr := provider.baseURLOrError(key)
+ if bifrostErr != nil {
+ return nil, bifrostErr
+ }
+ return anthropic.HandleAnthropicResponsesRequest(
+ ctx,
+ provider.client,
+ baseURL+providerUtils.GetPathFromContext(ctx, "/v1/messages"),
+ request,
+ anthropic.AnthropicRequestBuildConfig{
+ Provider: provider.GetProviderKey(),
+ ValidateTools: true,
+ BetaHeaderOverrides: provider.networkConfig.BetaHeaderOverrides,
+ ShouldSendBackRawRequest: provider.sendBackRawRequest,
+ ShouldSendBackRawResponse: provider.sendBackRawResponse,
+ },
+ anthropicHeaders(key),
+ provider.networkConfig.ExtraHeaders,
+ nil,
+ provider.logger,
+ )
+ }
+
chatResponse, err := provider.ChatCompletion(ctx, key, request.ToChatRequest())
if err != nil {
return nil, err
@@ -247,6 +345,44 @@ func (provider *SGLProvider) Responses(ctx *schemas.BifrostContext, key schemas.
// ResponsesStream performs a streaming responses request to the SGL API.
func (provider *SGLProvider) 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) {
+ ctx.SetValue(schemas.BifrostContextKeyPassthroughExtraParams, true)
+ baseURL, bifrostErr := provider.baseURLOrError(key)
+ if bifrostErr != nil {
+ return nil, bifrostErr
+ }
+ url := baseURL + providerUtils.GetPathFromContext(ctx, "/v1/messages")
+ jsonData, bifrostErr := anthropic.BuildAnthropicResponsesRequestBody(ctx, request, anthropic.AnthropicRequestBuildConfig{
+ Provider: provider.GetProviderKey(),
+ IsStreaming: true,
+ ValidateTools: true,
+ BetaHeaderOverrides: provider.networkConfig.BetaHeaderOverrides,
+ ShouldSendBackRawRequest: provider.sendBackRawRequest,
+ ShouldSendBackRawResponse: provider.sendBackRawResponse,
+ })
+ if bifrostErr != nil {
+ return nil, bifrostErr
+ }
+ return anthropic.HandleAnthropicResponsesStream(
+ ctx,
+ provider.streamingClient,
+ url,
+ jsonData,
+ 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,
@@ -480,4 +616,4 @@ func (provider *SGLProvider) Passthrough(_ *schemas.BifrostContext, _ schemas.Ke
func (provider *SGLProvider) PassthroughStream(_ *schemas.BifrostContext, _ schemas.PostHookRunner, _ func(context.Context), _ schemas.Key, _ *schemas.BifrostPassthroughRequest) (chan *schemas.BifrostStreamChunk, *schemas.BifrostError) {
return nil, providerUtils.NewUnsupportedOperationError(schemas.PassthroughStreamRequest, provider.GetProviderKey())
-}
+}
\ No newline at end of file
diff --git a/core/schemas/account.go b/core/schemas/account.go
index d6e34077bcf..436a2734c5d 100644
--- a/core/schemas/account.go
+++ b/core/schemas/account.go
@@ -141,6 +141,7 @@ type Key struct {
SGLKeyConfig *SGLKeyConfig `json:"sgl_key_config,omitempty"` // SGLang-specific key configuration
Enabled *bool `json:"enabled,omitempty"` // Whether the key is active (default:true)
UseForBatchAPI *bool `json:"use_for_batch_api,omitempty"` // Whether this key can be used for batch API operations (default:false for new keys, migrated keys default to true)
+ UseAnthropicEndpoints *bool `json:"use_anthropic_endpoints,omitempty"` // Whether to use anthropic endpoints for this key
ConfigHash string `json:"config_hash,omitempty"` // Hash of config.json version, used for change detection
Status KeyStatusType `json:"status,omitempty"` // Status of key
Description string `json:"description,omitempty"` // Description of key
@@ -229,7 +230,8 @@ type AliasConfig struct {
// top-level (rather than inside each provider sub-config) so the flat "project_id"
// JSON key does not collide between embedded sub-configs — Go/sonic silently drop
// a field name shared by multiple same-depth anonymous structs.
- ProjectID *SecretVar `json:"project_id,omitempty"`
+ ProjectID *SecretVar `json:"project_id,omitempty"`
+ UseAnthropicEndpoints *bool `json:"use_anthropic_endpoints,omitempty"` // Whether to use anthropic endpoints for this alias
*AzureAliasCfg
*VertexAliasCfg
@@ -247,6 +249,7 @@ func (ac AliasConfig) isLegacyShape() bool {
ac.Description == "" &&
ac.Region == nil &&
ac.ProjectID == nil &&
+ ac.UseAnthropicEndpoints == nil &&
ac.AzureAliasCfg == nil &&
ac.VertexAliasCfg == nil &&
ac.BedrockAliasCfg == nil &&
diff --git a/framework/configstore/clientconfig.go b/framework/configstore/clientconfig.go
index 9c2e08bd07f..dd818f760af 100644
--- a/framework/configstore/clientconfig.go
+++ b/framework/configstore/clientconfig.go
@@ -13,7 +13,6 @@ import (
"time"
"github.com/bytedance/sonic"
- bifrost "github.com/maximhq/bifrost/core"
"github.com/maximhq/bifrost/core/schemas"
"github.com/maximhq/bifrost/framework/configstore/tables"
)
@@ -523,7 +522,13 @@ func (p *ProviderConfig) Redacted() *ProviderConfig {
if key.UseForBatchAPI != nil {
redactedConfig.Keys[i].UseForBatchAPI = key.UseForBatchAPI
} else {
- redactedConfig.Keys[i].UseForBatchAPI = bifrost.Ptr(false)
+ redactedConfig.Keys[i].UseForBatchAPI = new(false)
+ }
+ // Add back use anthropic endpoints
+ if key.UseAnthropicEndpoints != nil {
+ redactedConfig.Keys[i].UseAnthropicEndpoints = key.UseAnthropicEndpoints
+ } else {
+ redactedConfig.Keys[i].UseAnthropicEndpoints = new(false)
}
// Add model discovery status and error
@@ -853,6 +858,14 @@ func GenerateKeyHash(key schemas.Key) (string, error) {
if useForBatchAPI {
hash.Write([]byte("useForBatchAPI:true"))
}
+ // Hash UseAnthropicEndpoints (nil = default false for new keys)
+ useAnthropicEndpoints := false
+ if key.UseAnthropicEndpoints != nil {
+ useAnthropicEndpoints = *key.UseAnthropicEndpoints
+ }
+ if useAnthropicEndpoints {
+ hash.Write([]byte("useAnthropicEndpoints:true"))
+ }
return hex.EncodeToString(hash.Sum(nil)), nil
}
diff --git a/framework/configstore/migrations.go b/framework/configstore/migrations.go
index 20f12d03a88..6e22c990ff1 100644
--- a/framework/configstore/migrations.go
+++ b/framework/configstore/migrations.go
@@ -451,6 +451,7 @@ var configstoreMigrationSteps = []migrationStep{
{IDs: []string{"add_webhook_jobs_table"}, run: migrationAddWebhookJobsTable},
{IDs: []string{"add_webhook_config_client_column"}, run: migrationAddWebhookConfigClientColumn},
{IDs: []string{"add_oauth_config_resource_column"}, run: migrationAddOauthConfigResourceColumn},
+ {IDs: []string{"add_use_anthropic_endpoints_column"}, run: migrationAddUseAnthropicEndpointsColumn},
}
// quoteSQLiteIdentifier quotes a SQLite identifier, escaping any double quotes.
@@ -3144,6 +3145,34 @@ func migrationAddUseForBatchAPIColumnAndS3BucketsConfig(ctx context.Context, db
return nil
}
+// migrationAddUseAnthropicEndpointsColumn adds the use_anthropic_endpoints column to the config_keys table.
+func migrationAddUseAnthropicEndpointsColumn(ctx context.Context, db *gorm.DB, logger schemas.Logger) error {
+ migrationName := "add_use_anthropic_endpoints_column"
+ logger.Info("[configstore] starting migration %s", migrationName)
+ defer logger.Info("[configstore] finished migration %s", migrationName)
+ m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
+ ID: migrationName,
+ Migrate: func(tx *gorm.DB) error {
+ tx = tx.WithContext(ctx)
+ if err := addColumnIfNotExists(tx, logger, &tables.TableKey{}, "use_anthropic_endpoints"); err != nil {
+ return fmt.Errorf("failed to add use_anthropic_endpoints column: %w", err)
+ }
+ return nil
+ },
+ Rollback: func(tx *gorm.DB) error {
+ tx = tx.WithContext(ctx)
+ if err := dropColumnIfExists(tx, logger, &tables.TableKey{}, "use_anthropic_endpoints"); err != nil {
+ return fmt.Errorf("failed to drop use_anthropic_endpoints column: %w", err)
+ }
+ return nil
+ },
+ }})
+ if err := m.Migrate(); err != nil {
+ return fmt.Errorf("error running add_use_anthropic_endpoints_column migration: %s", err.Error())
+ }
+ return nil
+}
+
// migrationAddHeaderFilterConfigJSONColumn adds the header_filter_config_json column to the config_client table
func migrationAddHeaderFilterConfigJSONColumn(ctx context.Context, db *gorm.DB, logger schemas.Logger) error {
migrationName := "add_header_filter_config_json_column"
@@ -7771,7 +7800,6 @@ func migrationAddMCPClientDiscoveredToolsColumns(ctx context.Context, db *gorm.D
}})
if err := m.Migrate(); err != nil {
return fmt.Errorf("error running add_mcp_client_discovered_tools_columns migration: %s", err.Error())
-
}
return nil
}
@@ -7875,7 +7903,6 @@ func migrationAddFlexTierPricingColumns(ctx context.Context, db *gorm.DB, logger
}})
if err := m.Migrate(); err != nil {
return fmt.Errorf("error while running flex tier pricing columns migration: %s", err.Error())
-
}
return nil
}
@@ -8304,7 +8331,6 @@ func migrationNormalizeOtelTraceType(ctx context.Context, db *gorm.DB, logger sc
}})
if err := m.Migrate(); err != nil {
return fmt.Errorf("error running normalize_otel_trace_type migration: %s", err.Error())
-
}
return nil
}
diff --git a/framework/configstore/rdb.go b/framework/configstore/rdb.go
index 39dd6530de0..6dbc084b265 100644
--- a/framework/configstore/rdb.go
+++ b/framework/configstore/rdb.go
@@ -152,6 +152,7 @@ func schemaKeyFromTableKey(dbKey tables.TableKey) schemas.Key {
Weight: getWeight(dbKey.Weight),
Enabled: dbKey.Enabled,
UseForBatchAPI: dbKey.UseForBatchAPI,
+ UseAnthropicEndpoints: dbKey.UseAnthropicEndpoints,
AzureKeyConfig: dbKey.AzureKeyConfig,
VertexKeyConfig: dbKey.VertexKeyConfig,
BedrockKeyConfig: dbKey.BedrockKeyConfig,
@@ -180,6 +181,7 @@ func tableKeyFromSchemaKey(provider tables.TableProvider, key schemas.Key) (tabl
Weight: &key.Weight,
Enabled: key.Enabled,
UseForBatchAPI: key.UseForBatchAPI,
+ UseAnthropicEndpoints: key.UseAnthropicEndpoints,
AzureKeyConfig: key.AzureKeyConfig,
VertexKeyConfig: key.VertexKeyConfig,
BedrockKeyConfig: key.BedrockKeyConfig,
@@ -717,6 +719,7 @@ func (s *RDBConfigStore) UpdateProvidersConfig(ctx context.Context, providers ma
Weight: &key.Weight,
Enabled: key.Enabled,
UseForBatchAPI: key.UseForBatchAPI,
+ UseAnthropicEndpoints: key.UseAnthropicEndpoints,
AzureKeyConfig: key.AzureKeyConfig,
VertexKeyConfig: key.VertexKeyConfig,
BedrockKeyConfig: key.BedrockKeyConfig,
@@ -947,6 +950,7 @@ func (s *RDBConfigStore) UpdateProvider(ctx context.Context, provider schemas.Mo
Weight: &key.Weight,
Enabled: key.Enabled,
UseForBatchAPI: key.UseForBatchAPI,
+ UseAnthropicEndpoints: key.UseAnthropicEndpoints,
AzureKeyConfig: key.AzureKeyConfig,
VertexKeyConfig: key.VertexKeyConfig,
BedrockKeyConfig: key.BedrockKeyConfig,
@@ -1088,6 +1092,7 @@ func (s *RDBConfigStore) AddProvider(ctx context.Context, provider schemas.Model
Weight: &key.Weight,
Enabled: key.Enabled,
UseForBatchAPI: key.UseForBatchAPI,
+ UseAnthropicEndpoints: key.UseAnthropicEndpoints,
AzureKeyConfig: key.AzureKeyConfig,
VertexKeyConfig: key.VertexKeyConfig,
BedrockKeyConfig: key.BedrockKeyConfig,
@@ -1976,6 +1981,7 @@ func (s *RDBConfigStore) GetProtectedMCPLibrarySlugs(ctx context.Context) ([]str
}
return slugs, nil
}
+
func (s *RDBConfigStore) GetMCPClientByID(ctx context.Context, id string) (*tables.TableMCPClient, error) {
var mcpClient tables.TableMCPClient
if err := s.DB().WithContext(ctx).Where("client_id = ?", id).First(&mcpClient).Error; err != nil {
@@ -7781,4 +7787,4 @@ func (s *RDBConfigStore) DeleteWebhookJob(ctx context.Context, id, runnerID stri
return fmt.Errorf("webhook job not found or no longer owned by caller")
}
return nil
-}
+}
\ No newline at end of file
diff --git a/framework/configstore/tables/key.go b/framework/configstore/tables/key.go
index e8986b46d3b..98bda98fbd7 100644
--- a/framework/configstore/tables/key.go
+++ b/framework/configstore/tables/key.go
@@ -84,6 +84,10 @@ type TableKey struct {
// Batch API configuration
UseForBatchAPI *bool `gorm:"default:false" json:"use_for_batch_api,omitempty"` // Whether this key can be used for batch API operations
+ // UseAnthropicEndpoints routes inference through the provider's Anthropic-compatible
+ // endpoints instead of its OpenAI-compatible ones.
+ UseAnthropicEndpoints *bool `gorm:"default:false" json:"use_anthropic_endpoints,omitempty"`
+
Status string `gorm:"type:varchar(50);default:'unknown'" json:"status"`
Description string `gorm:"type:text" json:"description,omitempty"`
@@ -136,6 +140,10 @@ func (k *TableKey) BeforeSave(tx *gorm.DB) error {
useForBatchAPI := false // DB default
k.UseForBatchAPI = &useForBatchAPI
}
+ if k.UseAnthropicEndpoints == nil {
+ useAnthropicEndpoints := false // DB default
+ k.UseAnthropicEndpoints = &useAnthropicEndpoints
+ }
// IMPORTANT: All *SecretVar fields assigned from provider config structs (AzureKeyConfig,
// VertexKeyConfig, BedrockKeyConfig) MUST be value-copied before assignment. The caller
// may retain the config struct pointer; if BeforeSave (or future encryption) mutates a
@@ -656,6 +664,10 @@ func (k *TableKey) AfterFind(tx *gorm.DB) error {
useForBatchAPI := false // DB default
k.UseForBatchAPI = &useForBatchAPI
}
+ if k.UseAnthropicEndpoints == nil {
+ useAnthropicEndpoints := false // DB default
+ k.UseAnthropicEndpoints = &useAnthropicEndpoints
+ }
// Reconstruct Azure config if fields are present
if k.AzureEndpoint != nil || k.AzureClientID != nil || k.AzureClientSecret != nil || k.AzureTenantID != nil || (k.AzureScopesJSON != nil && *k.AzureScopesJSON != "") {
var scopes []string
diff --git a/transports/bifrost-http/lib/config.go b/transports/bifrost-http/lib/config.go
index daf6e2b001e..2af7779d1c6 100644
--- a/transports/bifrost-http/lib/config.go
+++ b/transports/bifrost-http/lib/config.go
@@ -1556,6 +1556,7 @@ func mergeProviderKeys(provider schemas.ModelProvider, fileKeys, dbKeys []schema
SGLKeyConfig: dbKey.SGLKeyConfig,
Enabled: dbKey.Enabled,
UseForBatchAPI: dbKey.UseForBatchAPI,
+ UseAnthropicEndpoints: dbKey.UseAnthropicEndpoints,
})
if err != nil {
logger.Warn("failed to generate key hash for db key %s (%s): %v, falling back to name comparison", dbKey.Name, provider, err)
@@ -1638,6 +1639,7 @@ func reconcileProviderKeys(provider schemas.ModelProvider, fileKeys, dbKeys []sc
SGLKeyConfig: dbKey.SGLKeyConfig,
Enabled: dbKey.Enabled,
UseForBatchAPI: dbKey.UseForBatchAPI,
+ UseAnthropicEndpoints: dbKey.UseAnthropicEndpoints,
})
if err != nil {
logger.Warn("failed to generate key hash for db key %s (%s): %v", dbKey.Name, provider, err)
@@ -6645,4 +6647,4 @@ func DeepCopy[T any](in T) (T, error) {
}
err = sonic.Unmarshal(b, &out)
return out, err
-}
+}
\ No newline at end of file
diff --git a/transports/config.schema.json b/transports/config.schema.json
index b2748521476..9cf7ca279c3 100644
--- a/transports/config.schema.json
+++ b/transports/config.schema.json
@@ -420,7 +420,7 @@
"$ref": "#/$defs/provider"
},
"deepseek": {
- "$ref": "#/$defs/provider"
+ "$ref": "#/$defs/provider_with_deepseek_config"
},
"vllm": {
"$ref": "#/$defs/provider_with_vllm_config"
@@ -429,7 +429,7 @@
"$ref": "#/$defs/provider"
},
"fireworks": {
- "$ref": "#/$defs/provider"
+ "$ref": "#/$defs/provider_with_fireworks_config"
},
"nebius": {
"$ref": "#/$defs/provider"
@@ -3813,6 +3813,10 @@
"use_deployments_endpoint": {
"type": "boolean",
"description": "Replicate: use the deployments endpoint instead of the predictions endpoint for this alias."
+ },
+ "use_anthropic_endpoints": {
+ "type": "boolean",
+ "description": "Routes chat completions and responses requests through Anthropic-compatible endpoints."
}
},
"required": ["model_id"],
@@ -4067,12 +4071,51 @@
},
"required": ["url"],
"additionalProperties": false
+ },
+ "use_anthropic_endpoints": {
+ "type": "boolean",
+ "description": "Routes chat completions and responses requests through Anthropic-compatible endpoints.",
+ "default": false
}
},
"required": ["sgl_key_config"]
}
]
},
+ "deepseek_key": {
+ "allOf": [
+ {
+ "$ref": "#/$defs/base_key"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "use_anthropic_endpoints": {
+ "type": "boolean",
+ "description": "Routes chat completions and responses requests through Anthropic-compatible endpoints.",
+ "default": false
+ }
+ }
+ }
+ ]
+ },
+ "fireworks_key": {
+ "allOf": [
+ {
+ "$ref": "#/$defs/base_key"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "use_anthropic_endpoints": {
+ "type": "boolean",
+ "description": "Routes chat completions and responses requests through Anthropic-compatible endpoints.",
+ "default": false
+ }
+ }
+ }
+ ]
+ },
"azure_key": {
"allOf": [
{
@@ -4513,6 +4556,84 @@
},
"additionalProperties": false
},
+ "provider_with_deepseek_config": {
+ "type": "object",
+ "properties": {
+ "keys": {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/deepseek_key"
+ },
+ "minItems": 1,
+ "description": "API keys for this provider"
+ },
+ "network_config": {
+ "$ref": "#/$defs/network_config"
+ },
+ "concurrency_and_buffer_size": {
+ "$ref": "#/$defs/concurrency_and_buffer_size"
+ },
+ "proxy_config": {
+ "$ref": "#/$defs/proxy_config"
+ },
+ "send_back_raw_request": {
+ "type": "boolean",
+ "description": "Include raw request in BifrostResponse (default: false)"
+ },
+ "send_back_raw_response": {
+ "type": "boolean",
+ "description": "Include raw response in BifrostResponse (default: false)"
+ },
+ "store_raw_request_response": {
+ "type": "boolean",
+ "description": "Capture raw request/response for internal logging only; strip from API responses returned to clients (default: false)"
+ },
+ "custom_provider_config": {
+ "$ref": "#/$defs/custom_provider_config"
+ }
+ },
+ "required": ["keys"],
+ "additionalProperties": false
+ },
+ "provider_with_fireworks_config": {
+ "type": "object",
+ "properties": {
+ "keys": {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/fireworks_key"
+ },
+ "minItems": 1,
+ "description": "API keys for this provider"
+ },
+ "network_config": {
+ "$ref": "#/$defs/network_config"
+ },
+ "concurrency_and_buffer_size": {
+ "$ref": "#/$defs/concurrency_and_buffer_size"
+ },
+ "proxy_config": {
+ "$ref": "#/$defs/proxy_config"
+ },
+ "send_back_raw_request": {
+ "type": "boolean",
+ "description": "Include raw request in BifrostResponse (default: false)"
+ },
+ "send_back_raw_response": {
+ "type": "boolean",
+ "description": "Include raw response in BifrostResponse (default: false)"
+ },
+ "store_raw_request_response": {
+ "type": "boolean",
+ "description": "Capture raw request/response for internal logging only; strip from API responses returned to clients (default: false)"
+ },
+ "custom_provider_config": {
+ "$ref": "#/$defs/custom_provider_config"
+ }
+ },
+ "required": ["keys"],
+ "additionalProperties": false
+ },
"mcp_client_config": {
"type": "object",
"properties": {
@@ -7035,4 +7156,4 @@
}
}
}
-}
+}
\ No newline at end of file
diff --git a/ui/app/workspace/providers/fragments/apiKeysFormFragment.tsx b/ui/app/workspace/providers/fragments/apiKeysFormFragment.tsx
index 36e8ac9508c..91fdcc47a2f 100644
--- a/ui/app/workspace/providers/fragments/apiKeysFormFragment.tsx
+++ b/ui/app/workspace/providers/fragments/apiKeysFormFragment.tsx
@@ -60,6 +60,8 @@ export function ApiKeyFormFragment({ control, providerName, baseProviderType, fo
const isVLLM = effectiveProvider === "vllm";
const isOllama = effectiveProvider === "ollama";
const isSGL = effectiveProvider === "sgl";
+ const isDeepseek = effectiveProvider === "deepseek";
+ const isFireworks = effectiveProvider === "fireworks";
const isKeylessProvider = isOllama || isSGL;
const supportsBatchAPI = BATCH_SUPPORTED_PROVIDERS.includes(effectiveProvider);
@@ -766,6 +768,27 @@ export function ApiKeyFormFragment({ control, providerName, baseProviderType, fo
/>
)}
+ {(isSGL || isDeepseek || isFireworks) && (
+
+
(
+
+
+ Use Anthropic Endpoints
+
+ Routes chat completions and responses requests through Anthropic-compatible endpoints.
+
+
+
+
+
+
+ )}
+ />
+
+ )}
{isBedrock && (
diff --git a/ui/app/workspace/providers/fragments/deploymentsTable.tsx b/ui/app/workspace/providers/fragments/deploymentsTable.tsx
index d87f6881cb6..1121bb56beb 100644
--- a/ui/app/workspace/providers/fragments/deploymentsTable.tsx
+++ b/ui/app/workspace/providers/fragments/deploymentsTable.tsx
@@ -292,6 +292,28 @@ function ReplicateSection({ config, onChange, disabled }: ProviderSectionProps)
);
}
+function UseAnthropicEndpointsToggleSection({ config, onChange, disabled, providerName }: ProviderSectionProps & { providerName: string }) {
+ return (
+
+
+
+
+
+
+ Route chat completions and responses requests through Anthropic-compatible endpoints.
+
+
+
onChange({ use_anthropic_endpoints: checked ? true : undefined })}
+ disabled={disabled}
+ />
+
+
+ );
+}
+
function ProviderSection({ providerName, ...props }: ProviderSectionProps & { providerName: string }) {
switch (providerName) {
case "azure":
@@ -304,6 +326,12 @@ function ProviderSection({ providerName, ...props }: ProviderSectionProps & { pr
return
;
case "replicate":
return
;
+ case "sgl":
+ return
;
+ case "deepseek":
+ return
;
+ case "fireworks":
+ return
;
default:
return null;
}
diff --git a/ui/lib/types/config.ts b/ui/lib/types/config.ts
index cf368bc8f95..ce180e40525 100644
--- a/ui/lib/types/config.ts
+++ b/ui/lib/types/config.ts
@@ -73,6 +73,7 @@ export interface AliasConfig {
inference_profile_arn?: SecretVar;
// Replicate overrides
use_deployments_endpoint?: boolean;
+ use_anthropic_endpoints?: boolean;
}
// AzureKeyConfig matching Go's schemas.AzureKeyConfig
@@ -217,6 +218,7 @@ export interface ModelProviderKey {
weight: number;
enabled?: boolean;
use_for_batch_api?: boolean;
+ use_anthropic_endpoints?: boolean;
aliases?: Record
;
azure_key_config?: AzureKeyConfig;
vertex_key_config?: VertexKeyConfig;
diff --git a/ui/lib/types/schemas.ts b/ui/lib/types/schemas.ts
index 88523d61067..acd0e10d252 100644
--- a/ui/lib/types/schemas.ts
+++ b/ui/lib/types/schemas.ts
@@ -302,6 +302,7 @@ const aliasConfigObjectSchema = z.object({
inference_profile_arn: secretVarSchema.optional(),
// Replicate overrides
use_deployments_endpoint: z.boolean().optional(),
+ use_anthropic_endpoints: z.boolean().optional(),
});
// The Go server emits the legacy string wire shape (`{"my-alias": "model-id"}`)
@@ -348,6 +349,7 @@ export const modelProviderKeySchema = z
ollama_key_config: ollamaKeyConfigSchema.optional(),
sgl_key_config: sglKeyConfigSchema.optional(),
use_for_batch_api: z.boolean().optional(),
+ use_anthropic_endpoints: z.boolean().optional(),
enabled: z.boolean().optional(),
})
.refine(
From e6b494bcbde7597ef5042dd69ff76c6728408622 Mon Sep 17 00:00:00 2001
From: Samyabrata Maji <116789799+sammaji@users.noreply.github.com>
Date: Thu, 9 Jul 2026 16:26:50 +0530
Subject: [PATCH 2/3] fix: use anthropic endpoints in chat completion and
responses in deepseek
---
core/providers/anthropic/chat.go | 7 +
core/providers/anthropic/requestbuilder.go | 1 +
core/providers/anthropic/responses.go | 7 +
core/providers/anthropic/types.go | 17 +-
core/providers/deepseek/deepseek.go | 172 +++++++-
.../deepseek/deepseek_anthropic_test.go | 380 ++++++++++++++++++
core/providers/deepseek/deepseek_test.go | 37 +-
plugins/compat/conversion.go | 38 --
8 files changed, 598 insertions(+), 61 deletions(-)
create mode 100644 core/providers/deepseek/deepseek_anthropic_test.go
diff --git a/core/providers/anthropic/chat.go b/core/providers/anthropic/chat.go
index 4b433ab9345..d67a953d4f2 100644
--- a/core/providers/anthropic/chat.go
+++ b/core/providers/anthropic/chat.go
@@ -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)
diff --git a/core/providers/anthropic/requestbuilder.go b/core/providers/anthropic/requestbuilder.go
index 301e4a9706b..ecdabc16bc5 100644
--- a/core/providers/anthropic/requestbuilder.go
+++ b/core/providers/anthropic/requestbuilder.go
@@ -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.
diff --git a/core/providers/anthropic/responses.go b/core/providers/anthropic/responses.go
index b7e8b63940e..e717776df20 100644
--- a/core/providers/anthropic/responses.go
+++ b/core/providers/anthropic/responses.go
@@ -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 {
diff --git a/core/providers/anthropic/types.go b/core/providers/anthropic/types.go
index 91b8ea897be..0dde2c7f8dd 100644
--- a/core/providers/anthropic/types.go
+++ b/core/providers/anthropic/types.go
@@ -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 ====================
@@ -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"`
-}
\ No newline at end of file
+}
diff --git a/core/providers/deepseek/deepseek.go b/core/providers/deepseek/deepseek.go
index bba9d74e71d..663748b2d59 100644
--- a/core/providers/deepseek/deepseek.go
+++ b/core/providers/deepseek/deepseek.go
@@ -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"
@@ -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
@@ -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,
@@ -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,
@@ -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
@@ -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,
diff --git a/core/providers/deepseek/deepseek_anthropic_test.go b/core/providers/deepseek/deepseek_anthropic_test.go
new file mode 100644
index 00000000000..e256850960c
--- /dev/null
+++ b/core/providers/deepseek/deepseek_anthropic_test.go
@@ -0,0 +1,380 @@
+package deepseek_test
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "net/http/httptest"
+ "strings"
+ "testing"
+
+ "github.com/maximhq/bifrost/core/internal/llmtests"
+ deepseek "github.com/maximhq/bifrost/core/providers/deepseek"
+ schemas "github.com/maximhq/bifrost/core/schemas"
+)
+
+type testLogger struct{}
+
+func (l testLogger) Debug(string, ...any) {}
+func (l testLogger) Info(string, ...any) {}
+func (l testLogger) Warn(string, ...any) {}
+func (l testLogger) Error(string, ...any) {}
+func (l testLogger) Fatal(string, ...any) {}
+func (l testLogger) SetLevel(schemas.LogLevel) {}
+func (l testLogger) SetOutputType(schemas.LoggerOutputType) {}
+func (l testLogger) LogHTTPRequest(schemas.LogLevel, string) schemas.LogEventBuilder {
+ return schemas.NoopLogEvent
+}
+
+func newTestDeepSeekProvider(baseURL string) (*deepseek.DeepSeekProvider, error) {
+ return deepseek.NewDeepSeekProvider(&schemas.ProviderConfig{
+ NetworkConfig: schemas.NetworkConfig{
+ BaseURL: baseURL,
+ DefaultRequestTimeoutInSeconds: 5,
+ StreamIdleTimeoutInSeconds: 5,
+ MaxConnsPerHost: 1,
+ },
+ ConcurrencyAndBufferSize: schemas.ConcurrencyAndBufferSize{
+ Concurrency: 1,
+ BufferSize: 1,
+ },
+ }, testLogger{})
+}
+
+func newAnthropicResponse() string {
+ return `{"id":"msg_1","type":"message","role":"assistant","model":"deepseek-chat","content":[{"type":"text","text":"hello"}],"stop_reason":"end_turn","usage":{"input_tokens":1,"output_tokens":1}}`
+}
+
+func TestChatCompletion_UsesAnthropicEndpoint(t *testing.T) {
+ t.Parallel()
+
+ var captured map[string]any
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.URL.Path != "/anthropic/v1/messages" {
+ t.Fatalf("path = %q, want /anthropic/v1/messages", r.URL.Path)
+ }
+ if got := r.Header.Get("x-api-key"); got != "test-api-key" {
+ t.Fatalf("x-api-key = %q, want test-api-key", got)
+ }
+ body, err := io.ReadAll(r.Body)
+ if err != nil {
+ t.Fatalf("read body: %v", err)
+ }
+ if err := json.Unmarshal(body, &captured); err != nil {
+ t.Fatalf("decode body: %v", err)
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ fmt.Fprint(w, newAnthropicResponse())
+ }))
+ defer server.Close()
+
+ provider, err := newTestDeepSeekProvider(server.URL)
+ if err != nil {
+ t.Fatalf("NewDeepSeekProvider: %v", err)
+ }
+
+ ctx := schemas.NewBifrostContext(context.Background(), schemas.NoDeadline)
+ msg := "hello"
+ resp, bifrostErr := provider.ChatCompletion(ctx, schemas.Key{Value: schemas.SecretVar{Val: "test-api-key"}, UseAnthropicEndpoints: new(true)}, &schemas.BifrostChatRequest{
+ Provider: schemas.DeepSeek,
+ Model: "deepseek-v4-flash",
+ Input: []schemas.ChatMessage{{
+ Role: schemas.ChatMessageRoleUser,
+ Content: &schemas.ChatMessageContent{ContentStr: &msg},
+ }},
+ })
+ if bifrostErr != nil {
+ t.Fatalf("ChatCompletion: %v", bifrostErr.Error.Message)
+ }
+ if resp == nil || len(resp.Choices) == 0 {
+ t.Fatalf("expected chat response, got %#v", resp)
+ }
+ if _, ok := captured["messages"]; !ok {
+ t.Fatalf("outbound body missing messages: %#v", captured)
+ }
+}
+
+func TestResponses_UsesAnthropicEndpointAndKeepsWebSearch(t *testing.T) {
+ t.Parallel()
+
+ var captured map[string]any
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.URL.Path != "/anthropic/v1/messages" {
+ t.Fatalf("path = %q, want /anthropic/v1/messages", r.URL.Path)
+ }
+ if got := r.Header.Get("x-api-key"); got != "test-api-key" {
+ t.Fatalf("x-api-key = %q, want test-api-key", got)
+ }
+
+ body, err := io.ReadAll(r.Body)
+ if err != nil {
+ t.Fatalf("read body: %v", err)
+ }
+ if err := json.Unmarshal(body, &captured); err != nil {
+ t.Fatalf("decode body: %v", err)
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ fmt.Fprint(w, newAnthropicResponse())
+ }))
+ defer server.Close()
+
+ provider, err := newTestDeepSeekProvider(server.URL)
+ if err != nil {
+ t.Fatalf("NewDeepSeekProvider: %v", err)
+ }
+
+ ctx := schemas.NewBifrostContext(context.Background(), schemas.NoDeadline)
+ resp, bifrostErr := provider.Responses(ctx, schemas.Key{Value: schemas.SecretVar{Val: "test-api-key"}, UseAnthropicEndpoints: new(true)}, &schemas.BifrostResponsesRequest{
+ Provider: schemas.DeepSeek,
+ Model: "deepseek-v4-pro",
+ Input: []schemas.ResponsesMessage{{
+ Type: new(schemas.ResponsesMessageTypeMessage),
+ Role: new(schemas.ResponsesInputMessageRoleUser),
+ Content: &schemas.ResponsesMessageContent{
+ ContentStr: new("hello"),
+ },
+ }},
+ Params: &schemas.ResponsesParameters{
+ Tools: []schemas.ResponsesTool{{Type: schemas.ResponsesToolTypeWebSearch}},
+ },
+ })
+ if bifrostErr != nil {
+ t.Fatalf("Responses: %v", bifrostErr.Error.Message)
+ }
+ if resp == nil || len(resp.Output) == 0 {
+ t.Fatalf("expected responses payload, got %#v", resp)
+ }
+ tools, ok := captured["tools"].([]any)
+ if !ok || len(tools) == 0 {
+ t.Fatalf("outbound body missing tools: %#v", captured)
+ }
+ toolJSON, _ := json.Marshal(tools[0])
+ if !json.Valid(toolJSON) || !strings.Contains(string(toolJSON), "web_search") {
+ t.Fatalf("outbound tool body did not preserve web search: %s", toolJSON)
+ }
+}
+
+func TestChatCompletion_DisablesThinkingForForcedToolChoice(t *testing.T) {
+ t.Parallel()
+
+ var captured map[string]any
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ body, err := io.ReadAll(r.Body)
+ if err != nil {
+ t.Fatalf("read body: %v", err)
+ }
+ if err := json.Unmarshal(body, &captured); err != nil {
+ t.Fatalf("decode body: %v", err)
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ fmt.Fprint(w, newAnthropicResponse())
+ }))
+ defer server.Close()
+
+ provider, err := newTestDeepSeekProvider(server.URL)
+ if err != nil {
+ t.Fatalf("NewDeepSeekProvider: %v", err)
+ }
+
+ chatTool := llmtests.GetSampleChatTool(llmtests.SampleToolTypeTime)
+ if chatTool == nil {
+ t.Fatal("GetSampleChatTool returned nil")
+ }
+
+ ctx := schemas.NewBifrostContext(context.Background(), schemas.NoDeadline)
+ msg := "get the current time in UTC"
+ _, bifrostErr := provider.ChatCompletion(ctx, schemas.Key{Value: schemas.SecretVar{Val: "test-api-key"}, UseAnthropicEndpoints: new(true)}, &schemas.BifrostChatRequest{
+ Provider: schemas.DeepSeek,
+ Model: "deepseek-v4-flash",
+ Input: []schemas.ChatMessage{{
+ Role: schemas.ChatMessageRoleUser,
+ Content: &schemas.ChatMessageContent{ContentStr: &msg},
+ }},
+ Params: &schemas.ChatParameters{
+ Tools: []schemas.ChatTool{*chatTool},
+ ToolChoice: &schemas.ChatToolChoice{
+ ChatToolChoiceStruct: &schemas.ChatToolChoiceStruct{
+ Type: schemas.ChatToolChoiceTypeFunction,
+ Function: &schemas.ChatToolChoiceFunction{
+ Name: string(llmtests.SampleToolTypeTime),
+ },
+ },
+ },
+ },
+ })
+ if bifrostErr != nil {
+ t.Fatalf("ChatCompletion: %v", bifrostErr.Error.Message)
+ }
+
+ thinking, ok := captured["thinking"].(map[string]any)
+ if !ok {
+ t.Fatalf("expected thinking block in outbound body, got %#v", captured)
+ }
+ if got := thinking["type"]; got != "disabled" {
+ t.Fatalf("thinking.type = %v, want disabled", got)
+ }
+}
+
+func TestResponses_DisablesThinkingForForcedToolChoice(t *testing.T) {
+ t.Parallel()
+
+ var captured map[string]any
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ body, err := io.ReadAll(r.Body)
+ if err != nil {
+ t.Fatalf("read body: %v", err)
+ }
+ if err := json.Unmarshal(body, &captured); err != nil {
+ t.Fatalf("decode body: %v", err)
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ fmt.Fprint(w, newAnthropicResponse())
+ }))
+ defer server.Close()
+
+ provider, err := newTestDeepSeekProvider(server.URL)
+ if err != nil {
+ t.Fatalf("NewDeepSeekProvider: %v", err)
+ }
+
+ responsesTool := llmtests.GetSampleResponsesTool(llmtests.SampleToolTypeTime)
+ if responsesTool == nil {
+ t.Fatal("GetSampleResponsesTool returned nil")
+ }
+
+ ctx := schemas.NewBifrostContext(context.Background(), schemas.NoDeadline)
+ _, bifrostErr := provider.Responses(ctx, schemas.Key{Value: schemas.SecretVar{Val: "test-api-key"}, UseAnthropicEndpoints: new(true)}, &schemas.BifrostResponsesRequest{
+ Provider: schemas.DeepSeek,
+ Model: "deepseek-v4-pro",
+ Input: []schemas.ResponsesMessage{{
+ Type: new(schemas.ResponsesMessageTypeMessage),
+ Role: new(schemas.ResponsesInputMessageRoleUser),
+ Content: &schemas.ResponsesMessageContent{
+ ContentStr: new("get the current time in UTC"),
+ },
+ }},
+ Params: &schemas.ResponsesParameters{
+ Tools: []schemas.ResponsesTool{*responsesTool},
+ ToolChoice: &schemas.ResponsesToolChoice{
+ ResponsesToolChoiceStruct: &schemas.ResponsesToolChoiceStruct{
+ Type: schemas.ResponsesToolChoiceTypeFunction,
+ Name: new(string(llmtests.SampleToolTypeTime)),
+ },
+ },
+ },
+ })
+ if bifrostErr != nil {
+ t.Fatalf("Responses: %v", bifrostErr.Error.Message)
+ }
+
+ thinking, ok := captured["thinking"].(map[string]any)
+ if !ok {
+ t.Fatalf("expected thinking block in outbound body, got %#v", captured)
+ }
+ if got := thinking["type"]; got != "disabled" {
+ t.Fatalf("thinking.type = %v, want disabled", got)
+ }
+}
+
+func TestChatCompletion_DefaultsToOpenAIEndpoint(t *testing.T) {
+ t.Parallel()
+
+ var capturedPath string
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ capturedPath = r.URL.Path
+ if got := r.Header.Get("Authorization"); got != "Bearer test-api-key" {
+ t.Fatalf("Authorization = %q, want Bearer test-api-key", got)
+ }
+ w.Header().Set("Content-Type", "application/json")
+ fmt.Fprint(w, `{"id":"chatcmpl_1","object":"chat.completion","model":"deepseek-v4-flash","choices":[{"index":0,"message":{"role":"assistant","content":"hello"},"finish_reason":"stop"}],"usage":{"prompt_tokens":1,"completion_tokens":1,"total_tokens":2}}`)
+ }))
+ defer server.Close()
+
+ provider, err := newTestDeepSeekProvider(server.URL)
+ if err != nil {
+ t.Fatalf("NewDeepSeekProvider: %v", err)
+ }
+
+ ctx := schemas.NewBifrostContext(context.Background(), schemas.NoDeadline)
+ msg := "hello"
+ resp, bifrostErr := provider.ChatCompletion(ctx, schemas.Key{Value: schemas.SecretVar{Val: "test-api-key"}}, &schemas.BifrostChatRequest{
+ Provider: schemas.DeepSeek,
+ Model: "deepseek-v4-flash",
+ Input: []schemas.ChatMessage{{
+ Role: schemas.ChatMessageRoleUser,
+ Content: &schemas.ChatMessageContent{ContentStr: &msg},
+ }},
+ })
+ if bifrostErr != nil {
+ t.Fatalf("ChatCompletion: %v", bifrostErr.Error.Message)
+ }
+ if resp == nil || len(resp.Choices) == 0 {
+ t.Fatalf("expected chat response, got %#v", resp)
+ }
+ if capturedPath != "/chat/completions" {
+ t.Fatalf("path = %q, want /chat/completions", capturedPath)
+ }
+}
+
+func TestChatCompletion_OpenAIEndpointDisablesThinkingForRequiredToolChoice(t *testing.T) {
+ t.Parallel()
+
+ var captured map[string]any
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ body, err := io.ReadAll(r.Body)
+ if err != nil {
+ t.Fatalf("read body: %v", err)
+ }
+ if err := json.Unmarshal(body, &captured); err != nil {
+ t.Fatalf("decode body: %v", err)
+ }
+ w.Header().Set("Content-Type", "application/json")
+ fmt.Fprint(w, `{"id":"chatcmpl_1","object":"chat.completion","model":"deepseek-v4-flash","choices":[{"index":0,"message":{"role":"assistant","content":"hello"},"finish_reason":"stop"}],"usage":{"prompt_tokens":1,"completion_tokens":1,"total_tokens":2}}`)
+ }))
+ defer server.Close()
+
+ provider, err := newTestDeepSeekProvider(server.URL)
+ if err != nil {
+ t.Fatalf("NewDeepSeekProvider: %v", err)
+ }
+
+ chatTool := llmtests.GetSampleChatTool(llmtests.SampleToolTypeTime)
+ if chatTool == nil {
+ t.Fatal("GetSampleChatTool returned nil")
+ }
+
+ ctx := schemas.NewBifrostContext(context.Background(), schemas.NoDeadline)
+ msg := "get the current time in UTC"
+ requiredChoice := string(schemas.ChatToolChoiceTypeRequired)
+ _, bifrostErr := provider.ChatCompletion(ctx, schemas.Key{Value: schemas.SecretVar{Val: "test-api-key"}}, &schemas.BifrostChatRequest{
+ Provider: schemas.DeepSeek,
+ Model: "deepseek-v4-flash",
+ Input: []schemas.ChatMessage{{
+ Role: schemas.ChatMessageRoleUser,
+ Content: &schemas.ChatMessageContent{ContentStr: &msg},
+ }},
+ Params: &schemas.ChatParameters{
+ Tools: []schemas.ChatTool{*chatTool},
+ ToolChoice: &schemas.ChatToolChoice{
+ ChatToolChoiceStr: &requiredChoice,
+ },
+ },
+ })
+ if bifrostErr != nil {
+ t.Fatalf("ChatCompletion: %v", bifrostErr.Error.Message)
+ }
+
+ thinking, ok := captured["thinking"].(map[string]any)
+ if !ok {
+ t.Fatalf("expected thinking block in outbound body, got %#v", captured)
+ }
+ if got := thinking["type"]; got != "disabled" {
+ t.Fatalf("thinking.type = %v, want disabled", got)
+ }
+}
diff --git a/core/providers/deepseek/deepseek_test.go b/core/providers/deepseek/deepseek_test.go
index 76d173cbf23..38f07628b4f 100644
--- a/core/providers/deepseek/deepseek_test.go
+++ b/core/providers/deepseek/deepseek_test.go
@@ -34,27 +34,28 @@ func TestDeepseek(t *testing.T) {
EmbeddingModel: "", // DeepSeek doesn't support embedding
ReasoningModel: "deepseek-v4-pro",
Scenarios: llmtests.TestScenarios{
- TextCompletion: true,
- TextCompletionStream: true,
- SimpleChat: true,
- CompletionStream: true,
- MultiTurnConversation: true,
- ToolCalls: true,
- ToolCallsStreaming: true,
- MultipleToolCalls: false,
- End2EndToolCalling: true,
- AutomaticFunctionCall: true,
- ImageURL: false,
- ImageBase64: false,
- MultipleImages: false,
- CompleteEnd2End: true,
- Embedding: false,
- ListModels: true,
- Reasoning: true,
+ TextCompletion: true,
+ TextCompletionStream: true,
+ SimpleChat: true,
+ CompletionStream: true,
+ MultiTurnConversation: true,
+ ToolCalls: true,
+ ToolCallsStreaming: true,
+ MultipleToolCalls: true,
+ MultipleToolCallsStreaming: true,
+ End2EndToolCalling: true,
+ AutomaticFunctionCall: true,
+ ImageURL: false,
+ ImageBase64: false,
+ MultipleImages: false,
+ CompleteEnd2End: true,
+ Embedding: false,
+ ListModels: true,
+ Reasoning: true,
},
}
t.Run("DeepSeekTests", func(t *testing.T) {
llmtests.RunAllComprehensiveTests(t, client, ctx, testConfig)
})
-}
+}
\ No newline at end of file
diff --git a/plugins/compat/conversion.go b/plugins/compat/conversion.go
index 80932405870..7ea01ae03a1 100644
--- a/plugins/compat/conversion.go
+++ b/plugins/compat/conversion.go
@@ -11,45 +11,7 @@ func applyParameterConversion(req *schemas.BifrostRequest) {
}
if req.ResponsesRequest != nil {
flattenNamespaceTools(req.ResponsesRequest)
- disableThinkingWithToolChoiceForResponses(req.ResponsesRequest)
}
- if req.ChatRequest != nil {
- disableThinkingWithToolChoice(req.ChatRequest)
- }
-}
-
-// disableThinkingWithToolChoice disables thinking when tool_choice forces a tool call.
-func disableThinkingWithToolChoice(req *schemas.BifrostChatRequest) {
- if req.Provider != schemas.DeepSeek || req.Params == nil || req.Params.ToolChoice == nil {
- return
- }
- tc := req.Params.ToolChoice
- if tc.ChatToolChoiceStr != nil && *tc.ChatToolChoiceStr == string(schemas.ChatToolChoiceTypeRequired) {
- req.Params.ExtraParams = disableThinking(req.Params.ExtraParams)
- }
-}
-
-// disableThinkingWithToolChoiceForResponses disables thinking when tool_choice forces a tool call.
-func disableThinkingWithToolChoiceForResponses(req *schemas.BifrostResponsesRequest) {
- if req.Provider != schemas.DeepSeek || req.Params == nil || req.Params.ToolChoice == nil {
- return
- }
- tc := req.Params.ToolChoice
- if tc.ResponsesToolChoiceStr != nil && *tc.ResponsesToolChoiceStr == string(schemas.ResponsesToolChoiceTypeRequired) {
- req.Params.ExtraParams = disableThinking(req.Params.ExtraParams)
- }
-}
-
-// disableThinking sets thinking {"type": "disabled"} in extraParams, overwriting
-// any caller-provided value. DeepSeek models run with thinking enabled by default,
-// and thinking mode rejects forced tool_choice — so a forced tool call requires
-// thinking off.
-func disableThinking(extraParams map[string]any) map[string]any {
- if extraParams == nil {
- extraParams = make(map[string]any, 1)
- }
- extraParams["thinking"] = map[string]any{"type": "disabled"}
- return extraParams
}
// flattenNamespaceTools expands namespace scoped tools into a flat list of tools.
From 0717f57729452b3eb2ca6b0cf373f1bbdbbac902 Mon Sep 17 00:00:00 2001
From: Samyabrata Maji <116789799+sammaji@users.noreply.github.com>
Date: Tue, 14 Jul 2026 13:37:50 +0530
Subject: [PATCH 3/3] chore: update deepseek provider docs
---
.../supported-providers/deepseek.mdx | 193 +++++++++++++-----
1 file changed, 142 insertions(+), 51 deletions(-)
diff --git a/docs/providers/supported-providers/deepseek.mdx b/docs/providers/supported-providers/deepseek.mdx
index b37729590a9..0deae174308 100644
--- a/docs/providers/supported-providers/deepseek.mdx
+++ b/docs/providers/supported-providers/deepseek.mdx
@@ -1,34 +1,34 @@
---
title: "DeepSeek"
-description: "DeepSeek API conversion guide - OpenAI-compatible format, chat, streaming, tool calling, reasoning, and beta text completions"
+description: "DeepSeek API conversion guide - OpenAI-compatible chat and responses by default, optional per-key/per-alias Anthropic-compatible endpoints, OpenAI-compatible beta text completions, streaming, tool calling, and reasoning"
icon: "d"
---
## Overview
-DeepSeek is an **OpenAI-compatible provider** with a dedicated Bifrost provider implementation for DeepSeek's endpoint layout. Bifrost uses the shared OpenAI-compatible request and response converters, while preserving DeepSeek-specific extra parameters and routing text completions to the beta FIM endpoint. Key characteristics:
+DeepSeek is a provider with a dedicated Bifrost provider implementation. By default, Chat Completions, the Responses API, and Text Completions all use DeepSeek's **OpenAI-compatible** endpoints. Each key (or an individual alias) can opt into routing Chat Completions and the Responses API through DeepSeek's **Anthropic-compatible** endpoint instead, using the `use_anthropic_endpoints` toggle. Key characteristics:
-- **OpenAI-compatible chat** - Chat Completions use `/chat/completions`
-- **Streaming support** - Server-Sent Events for chat and text completions
-- **Tool calling** - Function tools are passed through using the OpenAI-compatible schema
-- **Reasoning support** - Reasoning parameters and DeepSeek extra parameters are forwarded
-- **Responses API** - Supported by converting Responses requests to Chat Completions internally
-- **Beta text completions** - Text/FIM completions use DeepSeek's `/beta/completions` endpoint
+- **OpenAI-compatible by default** - Chat Completions use `/chat/completions`, authenticated with a bearer token
+- **Optional Anthropic-compatible mode** - Set `use_anthropic_endpoints` on a key (or override it per-alias) to route Chat Completions and the Responses API through `/anthropic/v1/messages`, authenticated with `x-api-key`, using the shared Anthropic request/response converters
+- **Streaming support** - Server-Sent Events for chat, responses, and text completions, in both endpoint modes
+- **Tool calling** - Function tools are supported on both the OpenAI-compatible and Anthropic-compatible paths
+- **Reasoning support** - Reasoning parameters are mapped through the OpenAI converters by default, or the Anthropic converters when Anthropic-compatible mode is enabled
+- **Beta text completions** - Text/FIM completions always use DeepSeek's OpenAI-compatible `/beta/completions` endpoint, regardless of `use_anthropic_endpoints`
### Supported Operations
-| Operation | Non-Streaming | Streaming | Endpoint |
-|-----------|---------------|-----------|----------|
-| Chat Completions | ✅ | ✅ | `/chat/completions` |
-| Responses API | ✅ | ✅ | `/chat/completions` |
-| Text Completions | ✅ | ✅ | `/beta/completions` |
-| List Models | ✅ | - | `/models` |
-| Embeddings | ❌ | ❌ | - |
-| Image Generation | ❌ | ❌ | - |
-| Speech (TTS) | ❌ | ❌ | - |
-| Transcriptions (STT) | ❌ | ❌ | - |
-| Files | ❌ | ❌ | - |
-| Batch | ❌ | ❌ | - |
+| Operation | Non-Streaming | Streaming | Endpoint (default) | Endpoint (`use_anthropic_endpoints: true`) |
+|-----------|---------------|-----------|---------------------|---------------------------------------------|
+| Chat Completions | ✅ | ✅ | `/chat/completions` | `/anthropic/v1/messages` |
+| Responses API | ✅ | ✅ | `/chat/completions` (via Chat Completions fallback) | `/anthropic/v1/messages` |
+| Text Completions | ✅ | ✅ | `/beta/completions` | `/beta/completions` (unaffected) |
+| List Models | ✅ | - | `/models` | `/models` (unaffected) |
+| Embeddings | ❌ | ❌ | - | - |
+| Image Generation | ❌ | ❌ | - | - |
+| Speech (TTS) | ❌ | ❌ | - | - |
+| Transcriptions (STT) | ❌ | ❌ | - | - |
+| Files | ❌ | ❌ | - | - |
+| Batch | ❌ | ❌ | - | - |
**Unsupported Operations** (❌): Embeddings, Image Generation, Speech, Transcriptions, Files, Batch, cached content, containers, token counting, compaction, OCR, rerank, video, and passthrough are not supported by the upstream DeepSeek API through this provider. These return `UnsupportedOperationError`.
@@ -48,7 +48,8 @@ Configure DeepSeek as a provider.
3. Set a name for your key.
4. Paste your API key directly or use an environment variable (for example, `env.DEEPSEEK_API_KEY`).
5. Set **Allowed Models** to **All Models** (default) or the specific model allowlist you want this key to serve.
-6. Save the provider configuration.
+6. Leave **Use Anthropic Endpoints** off to use DeepSeek's OpenAI-compatible endpoints (the default), or turn it on to route Chat Completions and the Responses API through DeepSeek's Anthropic-compatible endpoint instead. See [Anthropic-Compatible Endpoints](#anthropic-compatible-endpoints-optional) below.
+7. Save the provider configuration.
@@ -93,63 +94,132 @@ case schemas.DeepSeek:
---
+## Anthropic-Compatible Endpoints (optional)
+
+DeepSeek exposes an Anthropic-compatible Messages endpoint (`/anthropic/v1/messages`) alongside its default OpenAI-compatible Chat Completions API. Setting `use_anthropic_endpoints` routes Chat Completions and the Responses API through that endpoint instead — Text Completions are unaffected and always use `/beta/completions`.
+
+The setting can be configured per key, and overridden per model alias:
+
+- **Key-level** - Sets the default endpoint mode for every request made with that key.
+- **Alias-level** - Overrides the key-level default for a single alias, so one key can serve some aliases through the OpenAI-compatible endpoints and others through the Anthropic-compatible endpoint.
+
+If neither is set, requests fall back to DeepSeek's OpenAI-compatible endpoints.
+
+
+
+
+On the key form, toggle **Use Anthropic Endpoints** (off by default). To override this for a specific alias, open that alias's expanded row in the deployments table and toggle **Use Anthropic endpoints** under **Deepseek overrides** — this takes priority over the key-level setting for that alias only.
+
+
+
+The `use_anthropic_endpoints` boolean is part of the same key payload used by [Provider Keys Management](https://docs.getbifrost.ai/api-reference/providers/create-a-key-for-a-provider), and the alias payload for that key's `models` entries.
+
+
+
+```json
+{
+ "providers": {
+ "deepseek": {
+ "keys": [
+ {
+ "name": "deepseek-key-1",
+ "value": "env.DEEPSEEK_API_KEY",
+ "models": [
+ "*"
+ ],
+ "weight": 1.0,
+ "use_anthropic_endpoints": true
+ }
+ ]
+ }
+ }
+}
+```
+
+To override this per-alias (for example, on a virtual key's model config), set `use_anthropic_endpoints` alongside the alias's `model_id`:
+
+```json
+{
+ "model_id": "deepseek-v4-flash",
+ "use_anthropic_endpoints": false
+}
+```
+
+| Field | Type | Required | Description |
+|-------|------|----------|--------------|
+| `use_anthropic_endpoints` | boolean | No | Routes chat completions and responses requests through Anthropic-compatible endpoints. Default: `false`. |
+
+
+
+
+---
+
# 1. Chat Completions
## Request Parameters
-DeepSeek supports OpenAI-compatible chat completion parameters. For the full parameter reference and message conversion behavior, see [OpenAI Chat Completions](/providers/supported-providers/openai#1-chat-completions).
+By default, DeepSeek Chat Completions use DeepSeek's OpenAI-compatible `/chat/completions` endpoint, authenticated with `Authorization: Bearer `. For the full parameter reference and message conversion behavior, see [OpenAI Chat Completions](/providers/supported-providers/openai#1-chat-completions).
+
+When `use_anthropic_endpoints` is enabled, requests are sent instead to DeepSeek's Anthropic-compatible endpoint (`/anthropic/v1/messages`), authenticated with `x-api-key: `, and built using the shared Anthropic converters. For that parameter reference and message conversion behavior, see [Anthropic Chat Completions](/providers/supported-providers/anthropic#1-chat-completions).
-### Filtered Parameters
+### Authentication
-Removed for DeepSeek compatibility:
-- `prediction` - OpenAI-specific predicted output
-- `prompt_cache_key` - OpenAI-specific prompt cache key
-- `prompt_cache_retention` - OpenAI-specific prompt cache retention
-- `verbosity` - Anthropic-specific
-- `store` - OpenAI-specific response storage
-- `web_search_options` - OpenAI-specific web search options
+| Mode | Header |
+|------|--------|
+| Default (OpenAI-compatible) | `Authorization: Bearer ` |
+| `use_anthropic_endpoints: true` | `x-api-key: ` |
+
+Bifrost sets the correct header automatically based on the resolved endpoint mode for the request.
### Reasoning Parameter
-DeepSeek delegates through `ToOpenAIChatRequest` with provider-specific compatibility handling. Reasoning effort is normalized using the OpenAI-compatible provider convention, and DeepSeek V4 models preserve `reasoning.effort: "max"` when requested.
+- **Default (OpenAI-compatible):** Reasoning parameters follow the same conventions as the [OpenAI provider](/providers/supported-providers/openai#1-chat-completions) (for example, `reasoning.effort`).
+- **`use_anthropic_endpoints: true`:** Reasoning/thinking parameters are mapped through the Anthropic converters (`reasoning` → `thinking`), the same as the [Anthropic provider](/providers/supported-providers/anthropic#1-chat-completions). Reasoning effort is sent as `output_config.effort` (Anthropic's own field placement), not nested under `thinking.reasoning_effort` as DeepSeek's native API documents it.
-Assistant-message `reasoning` details are stripped before sending follow-up messages because DeepSeek rejects `reasoning_details` in assistant messages.
+### Forced Tool Choice
-### Extra Parameters
+DeepSeek models run with thinking enabled by default, even when no `reasoning` parameter is set, and reject certain forced `tool_choice` combinations while thinking is on. Bifrost automatically disables thinking (`thinking: {"type": "disabled"}`) to avoid this, but which combination triggers the fix depends on the endpoint mode:
-DeepSeek enables passthrough extra parameters for chat and text completion requests. Provider-specific options such as DeepSeek thinking controls can be sent through `extra_params` without being dropped by Bifrost.
+- **Default (OpenAI-compatible):** Thinking is disabled when `tool_choice` is the generic `"required"` string (forcing some tool call, without pinning a specific one).
+- **`use_anthropic_endpoints: true`:** Thinking is disabled when `tool_choice` pins a specific named function, for both Chat Completions and the Responses API. `tool_choice: "required"`/`"any"` is left untouched in this mode, since DeepSeek's Anthropic-compatible endpoint accepts that combination with thinking on.
-DeepSeek supports standard OpenAI message types, tools, responses, and streaming formats. For details on message handling, tool conversion, responses, and streaming, refer to [OpenAI Chat Completions](/providers/supported-providers/openai#1-chat-completions).
+### Extra Parameters
+
+DeepSeek enables passthrough extra parameters for Chat Completions and Text Completions when using the default OpenAI-compatible endpoints. Extra parameters are **not** passed through by default when `use_anthropic_endpoints` is enabled.
---
# 2. Responses API
-Bifrost converts Responses API format to Chat Completions internally, then converts the response back:
+- **Default (OpenAI-compatible):** Responses requests fall back to Chat Completions, the same conversion pattern used by other OpenAI-compatible-only providers:
-```
-BifrostResponsesRequest
- → ToChatRequest()
- → ChatCompletion
- → ToBifrostResponsesResponse()
-```
+ ```
+ ResponsesRequest → ChatRequest → Response conversion
+ ```
-Same parameter support as Chat Completions with response format differences (output items instead of message content). Streaming Responses requests are also routed through Chat Completions streaming.
+- **`use_anthropic_endpoints: true`:** Responses requests are sent natively to DeepSeek's Anthropic-compatible endpoint at `/anthropic/v1/messages` — there is no internal conversion to Chat Completions. Both non-streaming and streaming Responses requests build an Anthropic-format request body directly from the `BifrostResponsesRequest` and convert the response back to Bifrost's Responses format.
+
+Same parameter support as Chat Completions in either mode, with response format differences (output items instead of message content).
---
# 3. Text Completions
-DeepSeek supports beta text/FIM completions through `/beta/completions`:
+DeepSeek supports beta text/FIM (Fill-In-Middle) completions through `/beta/completions`, regardless of `use_anthropic_endpoints`:
| Parameter | Mapping |
|-----------|---------|
| `prompt` | Sent as-is |
+| `suffix` | Enables FIM mode — text that should follow the completion; sent as-is |
| `max_tokens` | max_tokens |
| `temperature` | temperature |
| `top_p` | top_p |
| `stop` | stop sequences |
-| `extra_params` | Passed through to DeepSeek |
+| `echo` | echo |
+| `logprobs` | logprobs |
+| `extra_params` | Passed through to DeepSeek (e.g. `thinking` control) |
+
+Setting `suffix` alongside `prompt` puts the request in FIM mode: DeepSeek generates the text that belongs between `prompt` and `suffix` rather than a plain continuation of `prompt`.
Response returns `choices[].text` with completion text.
@@ -200,16 +270,37 @@ Lists available models from DeepSeek through `/models`.
**Code**: `TextCompletion` and `TextCompletionStream` use `/beta/completions`
-
+
**Severity**: Low
**Behavior**: User field > 64 characters is silently dropped
**Impact**: Longer user identifiers are lost
-**Code**: `SanitizeUserField` enforces 64-char max in the shared OpenAI converter
+**Code**: `SanitizeUserField` enforces 64-char max in the shared OpenAI converter, still used for Text Completions
+
+
+
+**Severity**: Low
+**Behavior**: An alias-level `use_anthropic_endpoints` override always wins over the key-level setting for that alias; if neither is set, requests default to the OpenAI-compatible endpoints
+**Impact**: A single key can serve some aliases through OpenAI-compatible endpoints and others through the Anthropic-compatible endpoint
+**Code**: `anthropic.ResolveUseAnthropicEndpoints` in `core/providers/anthropic/utils.go`, used by `ChatCompletion`, `ChatCompletionStream`, `Responses`, and `ResponsesStream` in `core/providers/deepseek/deepseek.go`
+
+
+
+**Severity**: Medium
+**Behavior**: When thinking is on (the default), Bifrost forces `thinking: {"type": "disabled"}` in the outbound request — for the generic `tool_choice: "required"` on the default OpenAI-compatible endpoint, or for a `tool_choice` pinning a specific named function on the Anthropic-compatible endpoint
+**Impact**: Prevents DeepSeek's `"Thinking mode does not support this tool_choice"` error for the combination each endpoint mode actually rejects; the other combination is left untouched on each path
+**Code**: `disableThinkingForForcedToolChoice` in `core/providers/deepseek/deepseek.go` (OpenAI-compatible path); `core/providers/anthropic/chat.go` and `core/providers/anthropic/responses.go`, gated on `Provider == DeepSeek` (Anthropic-compatible path)
+
+
+
+**Severity**: Low
+**Behavior**: Extra parameters are merged into the outbound request body by default on the OpenAI-compatible path (Chat and Text Completions), but not on the Anthropic-compatible path
+**Impact**: Provider-specific `extra_params` set on a request may be silently dropped when `use_anthropic_endpoints` is enabled
+**Code**: `BifrostContextKeyPassthroughExtraParams` is set in the OpenAI-compatible branches of `ChatCompletion`/`ChatCompletionStream` (and in `TextCompletion`/`TextCompletionStream`), but not in the Anthropic-compatible branches, in `core/providers/deepseek/deepseek.go`
-
+
**Severity**: Medium
-**Behavior**: Assistant-message `reasoning` details are removed before forwarding follow-up chat messages
-**Impact**: Prevents DeepSeek request failures when previous assistant turns contain reasoning metadata
-**Code**: `stripReasoningDetails` applies to DeepSeek in `ToOpenAIChatRequest`
+**Behavior**: Text Completions always use DeepSeek's OpenAI-compatible beta endpoint (`/beta/completions`, bearer token auth) regardless of `use_anthropic_endpoints`. Chat Completions and the Responses API use DeepSeek's OpenAI-compatible endpoints by default, or its Anthropic-compatible endpoint (`/anthropic/v1/messages`, `x-api-key` auth) when `use_anthropic_endpoints` is enabled
+**Impact**: When Anthropic-compatible mode is enabled, parameters and behavior documented for the [OpenAI provider](/providers/supported-providers/openai) no longer apply to DeepSeek Chat Completions or Responses — refer to the [Anthropic provider](/providers/supported-providers/anthropic) docs for those instead
+**Code**: `ChatCompletion`, `ChatCompletionStream`, `Responses`, and `ResponsesStream` in `core/providers/deepseek/deepseek.go` branch on `anthropic.ResolveUseAnthropicEndpoints`; `TextCompletion` and `TextCompletionStream` always delegate to the `openai` package