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