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/2] 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/2] 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.