1082 lines
41 KiB
Go
1082 lines
41 KiB
Go
package integrations
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/bytedance/sonic"
|
|
bifrost "github.com/maximhq/bifrost/core"
|
|
"github.com/maximhq/bifrost/core/providers/anthropic"
|
|
"github.com/maximhq/bifrost/core/schemas"
|
|
|
|
"github.com/maximhq/bifrost/transports/bifrost-http/lib"
|
|
"github.com/tidwall/gjson"
|
|
"github.com/valyala/fasthttp"
|
|
)
|
|
|
|
// AnthropicRouter handles Anthropic-compatible API endpoints
|
|
type AnthropicRouter struct {
|
|
*GenericRouter
|
|
}
|
|
|
|
// createAnthropicCompleteRouteConfig creates a route configuration for the `/v1/complete` endpoint.
|
|
func createAnthropicCompleteRouteConfig(pathPrefix string) RouteConfig {
|
|
return RouteConfig{
|
|
Type: RouteConfigTypeAnthropic,
|
|
Path: pathPrefix + "/v1/complete",
|
|
Method: "POST",
|
|
GetHTTPRequestType: func(ctx *fasthttp.RequestCtx) schemas.RequestType {
|
|
return schemas.TextCompletionRequest
|
|
},
|
|
GetRequestTypeInstance: func(ctx context.Context) interface{} {
|
|
return &anthropic.AnthropicTextRequest{}
|
|
},
|
|
RequestConverter: func(ctx *schemas.BifrostContext, req interface{}) (*schemas.BifrostRequest, error) {
|
|
if anthropicReq, ok := req.(*anthropic.AnthropicTextRequest); ok {
|
|
return &schemas.BifrostRequest{
|
|
TextCompletionRequest: anthropicReq.ToBifrostTextCompletionRequest(ctx),
|
|
}, nil
|
|
}
|
|
return nil, errors.New("invalid request type")
|
|
},
|
|
TextResponseConverter: func(ctx *schemas.BifrostContext, resp *schemas.BifrostTextCompletionResponse) (interface{}, error) {
|
|
if shouldUsePassthrough(ctx, resp.ExtraFields.Provider, resp.ExtraFields.OriginalModelRequested, resp.ExtraFields.ResolvedModelUsed) {
|
|
if resp.ExtraFields.RawResponse != nil {
|
|
return resp.ExtraFields.RawResponse, nil
|
|
}
|
|
}
|
|
return anthropic.ToAnthropicTextCompletionResponse(resp), nil
|
|
},
|
|
ErrorConverter: func(ctx *schemas.BifrostContext, err *schemas.BifrostError) interface{} {
|
|
return anthropic.ToAnthropicChatCompletionError(err)
|
|
},
|
|
PreCallback: checkAnthropicPassthrough,
|
|
}
|
|
}
|
|
|
|
// createAnthropicMessagesRouteConfig creates a route configuration for the `/v1/messages` endpoint.
|
|
func createAnthropicMessagesRouteConfig(pathPrefix string, logger schemas.Logger) []RouteConfig {
|
|
var routes []RouteConfig
|
|
for _, path := range []string{
|
|
"/v1/messages",
|
|
"/v1/messages/{path:*}",
|
|
} {
|
|
routes = append(routes, RouteConfig{
|
|
Type: RouteConfigTypeAnthropic,
|
|
Path: pathPrefix + path,
|
|
Method: "POST",
|
|
GetHTTPRequestType: func(ctx *fasthttp.RequestCtx) schemas.RequestType {
|
|
return schemas.ResponsesRequest
|
|
},
|
|
GetRequestTypeInstance: func(ctx context.Context) interface{} {
|
|
return &anthropic.AnthropicMessageRequest{}
|
|
},
|
|
RequestConverter: func(ctx *schemas.BifrostContext, req interface{}) (*schemas.BifrostRequest, error) {
|
|
if anthropicReq, ok := req.(*anthropic.AnthropicMessageRequest); ok {
|
|
bifrostReq := anthropicReq.ToBifrostResponsesRequest(ctx)
|
|
normalizeBifrostInputContentBlocks(bifrostReq)
|
|
return &schemas.BifrostRequest{
|
|
ResponsesRequest: bifrostReq,
|
|
}, nil
|
|
}
|
|
return nil, errors.New("invalid request type")
|
|
},
|
|
ResponsesResponseConverter: func(ctx *schemas.BifrostContext, resp *schemas.BifrostResponsesResponse) (interface{}, error) {
|
|
if isClaudeModel(resp.ExtraFields.OriginalModelRequested, resp.ExtraFields.ResolvedModelUsed, string(resp.ExtraFields.Provider)) {
|
|
if resp.ExtraFields.RawResponse != nil {
|
|
return resp.ExtraFields.RawResponse, nil
|
|
}
|
|
}
|
|
return anthropic.ToAnthropicResponsesResponse(ctx, resp), nil
|
|
},
|
|
AsyncResponsesResponseConverter: func(ctx *schemas.BifrostContext, resp *schemas.AsyncJobResponse, responsesResponseConverter ResponsesResponseConverter) (interface{}, map[string]string, error) {
|
|
if resp.Status == schemas.AsyncJobStatusCompleted {
|
|
responsesResp, ok := resp.Result.(*schemas.BifrostResponsesResponse)
|
|
if !ok {
|
|
return nil, nil, errors.New("invalid responses response type")
|
|
}
|
|
response, err := responsesResponseConverter(ctx, responsesResp)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return response, nil, nil
|
|
}
|
|
return &anthropic.AnthropicMessageResponse{
|
|
ID: resp.ID,
|
|
}, nil, nil
|
|
},
|
|
ErrorConverter: func(ctx *schemas.BifrostContext, err *schemas.BifrostError) interface{} {
|
|
return anthropic.ToAnthropicChatCompletionError(err)
|
|
},
|
|
StreamConfig: &StreamConfig{
|
|
ResponsesStreamResponseConverter: func(ctx *schemas.BifrostContext, resp *schemas.BifrostResponsesStreamResponse) (string, interface{}, error) {
|
|
if shouldUsePassthrough(ctx, resp.ExtraFields.Provider, resp.ExtraFields.OriginalModelRequested, resp.ExtraFields.ResolvedModelUsed) {
|
|
// Skip passthrough for ContentPartAdded: it's a synthetic bifrost event whose
|
|
// RawResponse carries the parent content_block_start already emitted by OutputItemAdded.
|
|
// Passing through here would produce a duplicate content_block_start that causes
|
|
// the Anthropic SDK to error and drop all subsequent content_block_delta events.
|
|
if resp.ExtraFields.RawResponse != nil && resp.Type != schemas.ResponsesStreamResponseTypeContentPartAdded {
|
|
raw, ok := resp.ExtraFields.RawResponse.(string)
|
|
if !ok {
|
|
return "", nil, fmt.Errorf("expected RawResponse string, got %T", resp.ExtraFields.RawResponse)
|
|
}
|
|
if t := gjson.Get(raw, "type"); t.Exists() {
|
|
return t.String(), raw, nil
|
|
}
|
|
}
|
|
// Fallback: if RawResponse is not available, use bifrost-to-anthropic conversion
|
|
// instead of silently dropping all events
|
|
}
|
|
anthropicResponse := anthropic.ToAnthropicResponsesStreamResponse(ctx, resp)
|
|
// Can happen for openai lifecycle events
|
|
if len(anthropicResponse) == 0 {
|
|
return "", nil, nil
|
|
}
|
|
if len(anthropicResponse) > 1 {
|
|
var combinedContent strings.Builder
|
|
for _, event := range anthropicResponse {
|
|
responseJSON, err := sonic.Marshal(event)
|
|
if err != nil {
|
|
logger.Error("failed to marshal anthropic streaming message: %v", err)
|
|
continue
|
|
}
|
|
fmt.Fprintf(&combinedContent, "event: %s\ndata: %s\n\n", event.Type, responseJSON)
|
|
}
|
|
return "", combinedContent.String(), nil
|
|
}
|
|
return string(anthropicResponse[0].Type), anthropicResponse[0], nil
|
|
},
|
|
ErrorConverter: func(ctx *schemas.BifrostContext, err *schemas.BifrostError) interface{} {
|
|
return anthropic.ToAnthropicResponsesStreamError(err)
|
|
},
|
|
},
|
|
PreCallback: checkAnthropicPassthrough,
|
|
})
|
|
}
|
|
return routes
|
|
}
|
|
|
|
// CreateAnthropicRouteConfigs creates route configurations for Anthropic endpoints.
|
|
func CreateAnthropicRouteConfigs(pathPrefix string, logger schemas.Logger) []RouteConfig {
|
|
return append([]RouteConfig{
|
|
createAnthropicCompleteRouteConfig(pathPrefix),
|
|
}, createAnthropicMessagesRouteConfig(pathPrefix, logger)...)
|
|
}
|
|
|
|
// passthroughSafeHeaders is a whitelist of headers that are safe to pass through
|
|
var passthroughSafeHeaders = map[string]bool{
|
|
"anthropic-beta": true,
|
|
"anthropic-dangerous-direct-browser-access": true,
|
|
"anthropic-version": true,
|
|
}
|
|
|
|
func hasPromptCachingScopeBetaHeader(headers map[string][]string) bool {
|
|
for k, v := range headers {
|
|
if strings.ToLower(k) == anthropic.AnthropicBetaHeader {
|
|
for _, headerValue := range v {
|
|
if strings.Contains(headerValue, anthropic.AnthropicPromptCachingScopeBetaHeader) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func hasFastModeBetaHeader(headers map[string][]string) bool {
|
|
for k, v := range headers {
|
|
if strings.ToLower(k) != anthropic.AnthropicBetaHeader {
|
|
continue
|
|
}
|
|
for _, headerValue := range v {
|
|
for beta := range strings.SplitSeq(headerValue, ",") {
|
|
if strings.HasPrefix(strings.TrimSpace(beta), anthropic.AnthropicFastModeBetaHeaderPrefix) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// filterVertexUnsupportedBetaHeaders removes beta headers that Vertex AI doesn't support.
|
|
// Vertex AI doesn't support: structured-outputs, advanced-tool-use, prompt-caching-scope, mcp-client.
|
|
func filterVertexUnsupportedBetaHeaders(headers map[string][]string) map[string][]string {
|
|
var betaHeaderKey string
|
|
var betaHeaders []string
|
|
var found bool
|
|
for k, v := range headers {
|
|
if strings.ToLower(k) == anthropic.AnthropicBetaHeader {
|
|
betaHeaderKey = k
|
|
betaHeaders = v
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if found {
|
|
var filteredBetas []string
|
|
for _, headerValue := range betaHeaders {
|
|
// Split comma-separated beta headers
|
|
for beta := range strings.SplitSeq(headerValue, ",") {
|
|
beta = strings.TrimSpace(beta)
|
|
if beta == "" {
|
|
continue
|
|
}
|
|
// Skip unsupported headers for Vertex.
|
|
// Use prefix matching so that future date bumps
|
|
// (e.g. structured-outputs-2025-12-15) are still caught.
|
|
if strings.HasPrefix(beta, anthropic.AnthropicAdvancedToolUseBetaHeaderPrefix) ||
|
|
strings.HasPrefix(beta, anthropic.AnthropicStructuredOutputsBetaHeaderPrefix) ||
|
|
strings.HasPrefix(beta, anthropic.AnthropicPromptCachingScopeBetaHeaderPrefix) ||
|
|
strings.HasPrefix(beta, anthropic.AnthropicMCPClientBetaHeaderPrefix) ||
|
|
strings.HasPrefix(beta, anthropic.AnthropicSkillsBetaHeaderPrefix) ||
|
|
strings.HasPrefix(beta, anthropic.AnthropicFastModeBetaHeaderPrefix) ||
|
|
strings.HasPrefix(beta, anthropic.AnthropicRedactThinkingBetaHeaderPrefix) {
|
|
continue
|
|
}
|
|
filteredBetas = append(filteredBetas, beta)
|
|
}
|
|
}
|
|
if len(filteredBetas) > 0 {
|
|
headers[betaHeaderKey] = []string{strings.Join(filteredBetas, ",")}
|
|
} else {
|
|
delete(headers, betaHeaderKey)
|
|
}
|
|
}
|
|
|
|
return headers
|
|
}
|
|
|
|
// extractPassthroughHeaders filters headers to only include those in the safe whitelist.
|
|
// Header matching is case-insensitive.
|
|
func extractPassthroughHeaders(allHeaders map[string][]string, provider schemas.ModelProvider) map[string][]string {
|
|
filtered := make(map[string][]string)
|
|
for k, v := range allHeaders {
|
|
if passthroughSafeHeaders[strings.ToLower(k)] {
|
|
filtered[strings.ToLower(k)] = v
|
|
}
|
|
}
|
|
|
|
return filtered
|
|
}
|
|
|
|
func CreateAnthropicListModelsRouteConfigs(pathPrefix string, handlerStore lib.HandlerStore) []RouteConfig {
|
|
return []RouteConfig{
|
|
{
|
|
Type: RouteConfigTypeAnthropic,
|
|
Path: pathPrefix + "/v1/models",
|
|
Method: "GET",
|
|
GetHTTPRequestType: func(ctx *fasthttp.RequestCtx) schemas.RequestType {
|
|
return schemas.ListModelsRequest
|
|
},
|
|
GetRequestTypeInstance: func(ctx context.Context) interface{} {
|
|
return &schemas.BifrostListModelsRequest{}
|
|
},
|
|
RequestConverter: func(ctx *schemas.BifrostContext, req interface{}) (*schemas.BifrostRequest, error) {
|
|
if listModelsReq, ok := req.(*schemas.BifrostListModelsRequest); ok {
|
|
return &schemas.BifrostRequest{
|
|
ListModelsRequest: listModelsReq,
|
|
}, nil
|
|
}
|
|
return nil, errors.New("invalid request type")
|
|
},
|
|
ListModelsResponseConverter: func(ctx *schemas.BifrostContext, resp *schemas.BifrostListModelsResponse) (interface{}, error) {
|
|
return anthropic.ToAnthropicListModelsResponse(resp), nil
|
|
},
|
|
ErrorConverter: func(ctx *schemas.BifrostContext, err *schemas.BifrostError) interface{} {
|
|
return anthropic.ToAnthropicChatCompletionError(err)
|
|
},
|
|
PreCallback: extractAnthropicListModelsParams,
|
|
},
|
|
}
|
|
}
|
|
|
|
func hydrateAnthropicRequestFromLargePayloadMetadata(bifrostCtx *schemas.BifrostContext, req interface{}) {
|
|
if bifrostCtx == nil {
|
|
return
|
|
}
|
|
isLargePayload, _ := bifrostCtx.Value(schemas.BifrostContextKeyLargePayloadMode).(bool)
|
|
if !isLargePayload {
|
|
return
|
|
}
|
|
metadata := resolveLargePayloadMetadata(bifrostCtx)
|
|
if metadata == nil {
|
|
return
|
|
}
|
|
|
|
switch r := req.(type) {
|
|
case *anthropic.AnthropicTextRequest:
|
|
if r.Model == "" {
|
|
r.Model = metadata.Model
|
|
}
|
|
if metadata.StreamRequested != nil && r.Stream == nil {
|
|
r.Stream = schemas.Ptr(*metadata.StreamRequested)
|
|
}
|
|
case *anthropic.AnthropicMessageRequest:
|
|
if r.Model == "" {
|
|
r.Model = metadata.Model
|
|
}
|
|
if metadata.StreamRequested != nil && r.Stream == nil {
|
|
r.Stream = schemas.Ptr(*metadata.StreamRequested)
|
|
}
|
|
}
|
|
}
|
|
|
|
// checkAnthropicPassthrough pre-callback checks if the request is for a claude model.
|
|
// If it is, it attaches the raw request body for direct use by the provider.
|
|
// It also checks for anthropic oauth headers and sets the bifrost context.
|
|
func checkAnthropicPassthrough(ctx *fasthttp.RequestCtx, bifrostCtx *schemas.BifrostContext, req interface{}) error {
|
|
hydrateAnthropicRequestFromLargePayloadMetadata(bifrostCtx, req)
|
|
|
|
var provider schemas.ModelProvider
|
|
var model string
|
|
|
|
switch r := req.(type) {
|
|
case *anthropic.AnthropicTextRequest:
|
|
provider, model = schemas.ParseModelString(r.Model, "")
|
|
// Check if model parameter explicitly has `anthropic/` prefix
|
|
if provider == schemas.Anthropic {
|
|
r.Model = model
|
|
}
|
|
|
|
case *anthropic.AnthropicMessageRequest:
|
|
provider, model = schemas.ParseModelString(r.Model, "")
|
|
// Check if model parameter explicitly has `anthropic/` prefix
|
|
if provider == schemas.Anthropic {
|
|
r.Model = model
|
|
}
|
|
}
|
|
|
|
headers := extractHeadersFromRequest(ctx)
|
|
schemas.ExtractAndSetUserAgentFromHeaders(headers, bifrostCtx)
|
|
|
|
// Check if anthropic oauth headers are present
|
|
if shouldUsePassthrough(bifrostCtx, provider, model, "") {
|
|
bifrostCtx.SetValue(schemas.BifrostContextKeyUseRawRequestBody, true)
|
|
bifrostCtx.SetValue(schemas.BifrostContextKeySendBackRawResponse, true)
|
|
if !isAnthropicAPIKeyAuth(ctx) && (provider == schemas.Anthropic || provider == "") {
|
|
url := extractExactPath(ctx)
|
|
if !strings.HasPrefix(url, "/") {
|
|
url = "/" + url
|
|
}
|
|
bifrostCtx.SetValue(schemas.BifrostContextKeyExtraHeaders, headers)
|
|
bifrostCtx.SetValue(schemas.BifrostContextKeyURLPath, url)
|
|
// This key is also used in IsClaudeCodeMaxMode
|
|
// So if you are changing the behaviour of this key, make sure to change IsClaudeCodeMaxMode as well
|
|
bifrostCtx.SetValue(schemas.BifrostContextKeySkipKeySelection, true)
|
|
} else {
|
|
// API key flow: pass only whitelisted safe headers (like anthropic-beta for feature detection)
|
|
passthroughHeaders := extractPassthroughHeaders(headers, provider)
|
|
if len(passthroughHeaders) > 0 {
|
|
bifrostCtx.SetValue(schemas.BifrostContextKeyExtraHeaders, passthroughHeaders)
|
|
}
|
|
}
|
|
if provider == schemas.Vertex && (hasPromptCachingScopeBetaHeader(headers) || hasFastModeBetaHeader(headers)) {
|
|
bifrostCtx.SetValue(schemas.BifrostContextKeyUseRawRequestBody, false)
|
|
return nil
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// shouldUsePassthrough checks if the request should be sent to the passthrough endpoint.
|
|
func shouldUsePassthrough(ctx *schemas.BifrostContext, provider schemas.ModelProvider, model string, alias string) bool {
|
|
return anthropic.IsClaudeCodeRequest(ctx) && isClaudeModel(model, alias, string(provider))
|
|
}
|
|
|
|
func isClaudeModel(model, alias, provider string) bool {
|
|
return (provider == string(schemas.Anthropic) ||
|
|
(provider == "" && (schemas.IsAnthropicModel(model) || schemas.IsAnthropicModel(alias)))) ||
|
|
(provider == string(schemas.Vertex) && (schemas.IsAnthropicModel(model) || schemas.IsAnthropicModel(alias))) ||
|
|
(provider == string(schemas.Azure) && (schemas.IsAnthropicModel(model) || schemas.IsAnthropicModel(alias)))
|
|
}
|
|
|
|
// extractAnthropicListModelsParams extracts query parameters for list models request
|
|
func extractAnthropicListModelsParams(ctx *fasthttp.RequestCtx, bifrostCtx *schemas.BifrostContext, req interface{}) error {
|
|
if listModelsReq, ok := req.(*schemas.BifrostListModelsRequest); ok {
|
|
|
|
// Extract limit from query parameters
|
|
if limitStr := string(ctx.QueryArgs().Peek("limit")); limitStr != "" {
|
|
if limit, err := strconv.Atoi(limitStr); err == nil {
|
|
listModelsReq.PageSize = limit
|
|
} else {
|
|
return fmt.Errorf("invalid limit parameter: %w", err)
|
|
}
|
|
}
|
|
|
|
if beforeID := string(ctx.QueryArgs().Peek("before_id")); beforeID != "" {
|
|
if listModelsReq.ExtraParams == nil {
|
|
listModelsReq.ExtraParams = make(map[string]interface{})
|
|
}
|
|
listModelsReq.ExtraParams["before_id"] = beforeID
|
|
}
|
|
|
|
if afterID := string(ctx.QueryArgs().Peek("after_id")); afterID != "" {
|
|
if listModelsReq.ExtraParams == nil {
|
|
listModelsReq.ExtraParams = make(map[string]interface{})
|
|
}
|
|
listModelsReq.ExtraParams["after_id"] = afterID
|
|
}
|
|
|
|
return nil
|
|
}
|
|
return errors.New("invalid request type for Anthropic list models")
|
|
}
|
|
|
|
// CreateAnthropicCountTokensRouteConfigs creates route configurations for Anthropic count tokens endpoint.
|
|
func CreateAnthropicCountTokensRouteConfigs(pathPrefix string, handlerStore lib.HandlerStore) []RouteConfig {
|
|
return []RouteConfig{
|
|
{
|
|
Type: RouteConfigTypeAnthropic,
|
|
Path: pathPrefix + "/v1/messages/count_tokens",
|
|
Method: "POST",
|
|
GetHTTPRequestType: func(ctx *fasthttp.RequestCtx) schemas.RequestType {
|
|
return schemas.CountTokensRequest
|
|
},
|
|
GetRequestTypeInstance: func(ctx context.Context) interface{} {
|
|
return &anthropic.AnthropicMessageRequest{}
|
|
},
|
|
RequestConverter: func(ctx *schemas.BifrostContext, req interface{}) (*schemas.BifrostRequest, error) {
|
|
if anthropicReq, ok := req.(*anthropic.AnthropicMessageRequest); ok {
|
|
bifrostReq := anthropicReq.ToBifrostResponsesRequest(ctx)
|
|
normalizeBifrostInputContentBlocks(bifrostReq)
|
|
return &schemas.BifrostRequest{
|
|
CountTokensRequest: bifrostReq,
|
|
}, nil
|
|
}
|
|
return nil, errors.New("invalid request type for Anthropic count tokens")
|
|
},
|
|
CountTokensResponseConverter: func(ctx *schemas.BifrostContext, resp *schemas.BifrostCountTokensResponse) (interface{}, error) {
|
|
return anthropic.ToAnthropicCountTokensResponse(resp), nil
|
|
},
|
|
ErrorConverter: func(ctx *schemas.BifrostContext, err *schemas.BifrostError) interface{} {
|
|
return anthropic.ToAnthropicChatCompletionError(err)
|
|
},
|
|
PreCallback: checkAnthropicPassthrough,
|
|
},
|
|
}
|
|
}
|
|
|
|
// CreateAnthropicBatchRouteConfigs creates route configurations for Anthropic Batch API endpoints.
|
|
func CreateAnthropicBatchRouteConfigs(pathPrefix string, handlerStore lib.HandlerStore) []RouteConfig {
|
|
var routes []RouteConfig
|
|
// Create batch endpoint - POST /v1/messages/batches
|
|
routes = append(routes, RouteConfig{
|
|
Type: RouteConfigTypeAnthropic,
|
|
Path: pathPrefix + "/v1/messages/batches",
|
|
Method: "POST",
|
|
GetHTTPRequestType: func(ctx *fasthttp.RequestCtx) schemas.RequestType {
|
|
return schemas.BatchCreateRequest
|
|
},
|
|
GetRequestTypeInstance: func(ctx context.Context) interface{} {
|
|
return &anthropic.AnthropicBatchCreateRequest{}
|
|
},
|
|
BatchRequestConverter: func(ctx *schemas.BifrostContext, req any) (*BatchRequest, error) {
|
|
if anthropicReq, ok := req.(*anthropic.AnthropicBatchCreateRequest); ok {
|
|
// Convert Anthropic batch request items to Bifrost format
|
|
isNonAnthropicProvider := false
|
|
var provider schemas.ModelProvider
|
|
var ok bool
|
|
if provider, ok = ctx.Value(bifrostContextKeyProvider).(schemas.ModelProvider); ok && provider != schemas.Anthropic {
|
|
isNonAnthropicProvider = true
|
|
}
|
|
var model *string
|
|
requests := make([]schemas.BatchRequestItem, len(anthropicReq.Requests))
|
|
for i, r := range anthropicReq.Requests {
|
|
if isNonAnthropicProvider {
|
|
requestModel, ok := r.Params["model"].(string)
|
|
if !ok {
|
|
return nil, errors.New("model is required")
|
|
}
|
|
if model == nil {
|
|
model = schemas.Ptr(requestModel)
|
|
} else if *model != requestModel {
|
|
return nil, errors.New("for non-Anthropic providers, model must be the same for all requests")
|
|
}
|
|
}
|
|
requests[i] = schemas.BatchRequestItem{
|
|
CustomID: r.CustomID,
|
|
Params: r.Params,
|
|
}
|
|
}
|
|
br := &BatchRequest{
|
|
Type: schemas.BatchCreateRequest,
|
|
CreateRequest: &schemas.BifrostBatchCreateRequest{
|
|
Model: model,
|
|
Provider: provider,
|
|
Requests: requests,
|
|
},
|
|
}
|
|
// If provider is openai, we need to generate endpoint too
|
|
if provider == schemas.OpenAI {
|
|
// Confirm if all requests have the same url
|
|
var url string
|
|
for _, request := range requests {
|
|
if urlParam, ok := request.Params["url"].(string); ok {
|
|
if url == "" {
|
|
url = urlParam
|
|
} else if url != urlParam {
|
|
return nil, errors.New("for OpenAI batch API, all requests must have the same url")
|
|
}
|
|
}
|
|
}
|
|
br.CreateRequest.Endpoint = schemas.BatchEndpoint(url)
|
|
}
|
|
return br, nil
|
|
}
|
|
return nil, errors.New("invalid batch create request type")
|
|
},
|
|
BatchCreateResponseConverter: func(ctx *schemas.BifrostContext, resp *schemas.BifrostBatchCreateResponse) (interface{}, error) {
|
|
if resp.ExtraFields.Provider == schemas.Gemini {
|
|
resp.ID = strings.Replace(resp.ID, "batches/", "batches-", 1)
|
|
}
|
|
return anthropic.ToAnthropicBatchCreateResponse(resp), nil
|
|
},
|
|
ErrorConverter: func(ctx *schemas.BifrostContext, err *schemas.BifrostError) interface{} {
|
|
return anthropic.ToAnthropicChatCompletionError(err)
|
|
},
|
|
PreCallback: extractAnthropicBatchCreateParams,
|
|
})
|
|
|
|
// List batches endpoint - GET /v1/messages/batches
|
|
routes = append(routes, RouteConfig{
|
|
Type: RouteConfigTypeAnthropic,
|
|
Path: pathPrefix + "/v1/messages/batches",
|
|
Method: "GET",
|
|
GetHTTPRequestType: func(ctx *fasthttp.RequestCtx) schemas.RequestType {
|
|
return schemas.BatchListRequest
|
|
},
|
|
GetRequestTypeInstance: func(ctx context.Context) interface{} {
|
|
return &anthropic.AnthropicBatchListRequest{}
|
|
},
|
|
BatchRequestConverter: func(ctx *schemas.BifrostContext, req interface{}) (*BatchRequest, error) {
|
|
if listReq, ok := req.(*anthropic.AnthropicBatchListRequest); ok {
|
|
provider, ok := ctx.Value(bifrostContextKeyProvider).(schemas.ModelProvider)
|
|
if !ok {
|
|
return nil, errors.New("provider not found in context")
|
|
}
|
|
return &BatchRequest{
|
|
Type: schemas.BatchListRequest,
|
|
ListRequest: &schemas.BifrostBatchListRequest{
|
|
Provider: provider,
|
|
PageSize: listReq.PageSize,
|
|
PageToken: listReq.PageToken,
|
|
},
|
|
}, nil
|
|
}
|
|
return nil, errors.New("invalid batch list request type")
|
|
},
|
|
BatchListResponseConverter: func(ctx *schemas.BifrostContext, resp *schemas.BifrostBatchListResponse) (interface{}, error) {
|
|
if resp.ExtraFields.RawResponse != nil && resp.ExtraFields.Provider == schemas.Anthropic {
|
|
return resp.ExtraFields.RawResponse, nil
|
|
}
|
|
if resp.ExtraFields.Provider == schemas.Gemini {
|
|
for i, batch := range resp.Data {
|
|
resp.Data[i].ID = strings.Replace(batch.ID, "batches/", "batches-", 1)
|
|
}
|
|
}
|
|
return anthropic.ToAnthropicBatchListResponse(resp), nil
|
|
},
|
|
ErrorConverter: func(ctx *schemas.BifrostContext, err *schemas.BifrostError) interface{} {
|
|
return anthropic.ToAnthropicChatCompletionError(err)
|
|
},
|
|
PreCallback: extractAnthropicBatchListQueryParams,
|
|
})
|
|
|
|
// Retrieve batch endpoint - GET /v1/messages/batches/{batch_id}
|
|
routes = append(routes, RouteConfig{
|
|
Type: RouteConfigTypeAnthropic,
|
|
Path: pathPrefix + "/v1/messages/batches/{batch_id}",
|
|
Method: "GET",
|
|
GetHTTPRequestType: func(ctx *fasthttp.RequestCtx) schemas.RequestType {
|
|
return schemas.BatchRetrieveRequest
|
|
},
|
|
GetRequestTypeInstance: func(ctx context.Context) interface{} {
|
|
return &anthropic.AnthropicBatchRetrieveRequest{}
|
|
},
|
|
BatchRequestConverter: func(ctx *schemas.BifrostContext, req interface{}) (*BatchRequest, error) {
|
|
if retrieveReq, ok := req.(*anthropic.AnthropicBatchRetrieveRequest); ok {
|
|
provider := ctx.Value(bifrostContextKeyProvider).(schemas.ModelProvider)
|
|
if provider == schemas.Gemini {
|
|
retrieveReq.BatchID = strings.Replace(retrieveReq.BatchID, "batches-", "batches/", 1)
|
|
}
|
|
return &BatchRequest{
|
|
Type: schemas.BatchRetrieveRequest,
|
|
RetrieveRequest: &schemas.BifrostBatchRetrieveRequest{
|
|
BatchID: retrieveReq.BatchID,
|
|
Provider: provider,
|
|
},
|
|
}, nil
|
|
}
|
|
return nil, errors.New("invalid batch retrieve request type")
|
|
},
|
|
BatchRetrieveResponseConverter: func(ctx *schemas.BifrostContext, resp *schemas.BifrostBatchRetrieveResponse) (interface{}, error) {
|
|
if resp.ExtraFields.Provider == schemas.Gemini {
|
|
resp.ID = strings.Replace(resp.ID, "batches/", "batches-", 1)
|
|
}
|
|
return anthropic.ToAnthropicBatchRetrieveResponse(resp), nil
|
|
},
|
|
ErrorConverter: func(ctx *schemas.BifrostContext, err *schemas.BifrostError) interface{} {
|
|
return anthropic.ToAnthropicChatCompletionError(err)
|
|
},
|
|
PreCallback: extractAnthropicBatchIDFromPath,
|
|
})
|
|
|
|
// Cancel batch endpoint - POST /v1/messages/batches/{batch_id}/cancel
|
|
routes = append(routes, RouteConfig{
|
|
Type: RouteConfigTypeAnthropic,
|
|
Path: pathPrefix + "/v1/messages/batches/{batch_id}/cancel",
|
|
Method: "POST",
|
|
GetHTTPRequestType: func(ctx *fasthttp.RequestCtx) schemas.RequestType {
|
|
return schemas.BatchCancelRequest
|
|
},
|
|
GetRequestTypeInstance: func(ctx context.Context) interface{} {
|
|
return &anthropic.AnthropicBatchCancelRequest{}
|
|
},
|
|
BatchRequestConverter: func(ctx *schemas.BifrostContext, req interface{}) (*BatchRequest, error) {
|
|
if cancelReq, ok := req.(*anthropic.AnthropicBatchCancelRequest); ok {
|
|
provider := ctx.Value(bifrostContextKeyProvider).(schemas.ModelProvider)
|
|
if provider == schemas.Gemini {
|
|
cancelReq.BatchID = strings.Replace(cancelReq.BatchID, "batches-", "batches/", 1)
|
|
}
|
|
return &BatchRequest{
|
|
Type: schemas.BatchCancelRequest,
|
|
CancelRequest: &schemas.BifrostBatchCancelRequest{
|
|
BatchID: cancelReq.BatchID,
|
|
Provider: provider,
|
|
},
|
|
}, nil
|
|
}
|
|
return nil, errors.New("invalid batch cancel request type")
|
|
},
|
|
BatchCancelResponseConverter: func(ctx *schemas.BifrostContext, resp *schemas.BifrostBatchCancelResponse) (interface{}, error) {
|
|
if resp.ExtraFields.RawResponse != nil {
|
|
return resp.ExtraFields.RawResponse, nil
|
|
}
|
|
return anthropic.ToAnthropicBatchCancelResponse(resp), nil
|
|
},
|
|
ErrorConverter: func(ctx *schemas.BifrostContext, err *schemas.BifrostError) interface{} {
|
|
return anthropic.ToAnthropicChatCompletionError(err)
|
|
},
|
|
PreCallback: extractAnthropicBatchIDFromPath,
|
|
})
|
|
|
|
// Get batch results endpoint - GET /v1/messages/batches/{batch_id}/results
|
|
routes = append(routes, RouteConfig{
|
|
Type: RouteConfigTypeAnthropic,
|
|
Path: pathPrefix + "/v1/messages/batches/{batch_id}/results",
|
|
Method: "GET",
|
|
GetHTTPRequestType: func(ctx *fasthttp.RequestCtx) schemas.RequestType {
|
|
return schemas.BatchResultsRequest
|
|
},
|
|
GetRequestTypeInstance: func(ctx context.Context) interface{} {
|
|
return &anthropic.AnthropicBatchResultsRequest{}
|
|
},
|
|
BatchRequestConverter: func(ctx *schemas.BifrostContext, req interface{}) (*BatchRequest, error) {
|
|
if resultsReq, ok := req.(*anthropic.AnthropicBatchResultsRequest); ok {
|
|
provider := ctx.Value(bifrostContextKeyProvider).(schemas.ModelProvider)
|
|
if provider == schemas.Gemini {
|
|
resultsReq.BatchID = strings.Replace(resultsReq.BatchID, "batches-", "batches/", 1)
|
|
}
|
|
return &BatchRequest{
|
|
Type: schemas.BatchResultsRequest,
|
|
ResultsRequest: &schemas.BifrostBatchResultsRequest{
|
|
BatchID: resultsReq.BatchID,
|
|
Provider: provider,
|
|
},
|
|
}, nil
|
|
}
|
|
return nil, errors.New("invalid batch results request type")
|
|
},
|
|
BatchResultsResponseConverter: func(ctx *schemas.BifrostContext, resp *schemas.BifrostBatchResultsResponse) (interface{}, error) {
|
|
if resp.ExtraFields.RawResponse != nil {
|
|
return resp.ExtraFields.RawResponse, nil
|
|
}
|
|
return resp, nil
|
|
},
|
|
ErrorConverter: func(ctx *schemas.BifrostContext, err *schemas.BifrostError) interface{} {
|
|
return anthropic.ToAnthropicChatCompletionError(err)
|
|
},
|
|
PreCallback: extractAnthropicBatchIDFromPath,
|
|
})
|
|
|
|
return routes
|
|
}
|
|
|
|
// extractAnthropicBatchCreateParams extracts provider from header for batch create requests
|
|
func extractAnthropicBatchCreateParams(ctx *fasthttp.RequestCtx, bifrostCtx *schemas.BifrostContext, req interface{}) error {
|
|
// Extract provider from header, default to Anthropic
|
|
provider := string(ctx.Request.Header.Peek("x-model-provider"))
|
|
if provider == "" {
|
|
provider = string(schemas.Anthropic)
|
|
}
|
|
// Store provider in context for batch create converter to use
|
|
bifrostCtx.SetValue(bifrostContextKeyProvider, schemas.ModelProvider(provider))
|
|
return nil
|
|
}
|
|
|
|
// extractAnthropicBatchListQueryParams extracts provider from header and query parameters for Anthropic batch list requests
|
|
func extractAnthropicBatchListQueryParams(ctx *fasthttp.RequestCtx, bifrostCtx *schemas.BifrostContext, req interface{}) error {
|
|
if listReq, ok := req.(*anthropic.AnthropicBatchListRequest); ok {
|
|
// Extract provider from header, default to Anthropic
|
|
provider := string(ctx.Request.Header.Peek("x-model-provider"))
|
|
if provider == "" {
|
|
provider = string(schemas.Anthropic)
|
|
}
|
|
bifrostCtx.SetValue(bifrostContextKeyProvider, schemas.ModelProvider(provider))
|
|
// Printing all query parameters
|
|
// Extract limit from query parameters
|
|
if limitStr := string(ctx.QueryArgs().Peek("page_size")); limitStr != "" {
|
|
if limit, err := strconv.Atoi(limitStr); err == nil {
|
|
listReq.PageSize = limit
|
|
} else {
|
|
listReq.PageSize = 30
|
|
}
|
|
}
|
|
// Extract before_id cursor
|
|
if pageToken := string(ctx.QueryArgs().Peek("page_token")); pageToken != "" {
|
|
listReq.PageToken = &pageToken
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// extractAnthropicBatchIDFromPath extracts provider from header and batch_id from path parameters
|
|
func extractAnthropicBatchIDFromPath(ctx *fasthttp.RequestCtx, bifrostCtx *schemas.BifrostContext, req interface{}) error {
|
|
// Extract provider from header, default to Anthropic
|
|
provider := string(ctx.Request.Header.Peek("x-model-provider"))
|
|
if provider == "" {
|
|
provider = string(schemas.Anthropic)
|
|
}
|
|
bifrostCtx.SetValue(bifrostContextKeyProvider, schemas.ModelProvider(provider))
|
|
batchID := ctx.UserValue("batch_id")
|
|
if batchID == nil {
|
|
return errors.New("batch_id is required")
|
|
}
|
|
batchIDStr, ok := batchID.(string)
|
|
if !ok || batchIDStr == "" {
|
|
return errors.New("batch_id must be a non-empty string")
|
|
}
|
|
switch r := req.(type) {
|
|
case *anthropic.AnthropicBatchRetrieveRequest:
|
|
r.BatchID = batchIDStr
|
|
case *anthropic.AnthropicBatchCancelRequest:
|
|
r.BatchID = batchIDStr
|
|
case *anthropic.AnthropicBatchResultsRequest:
|
|
r.BatchID = batchIDStr
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// extractAnthropicFileUploadParams extracts provider from header for file upload requests
|
|
func extractAnthropicFileUploadParams(ctx *fasthttp.RequestCtx, bifrostCtx *schemas.BifrostContext, req interface{}) error {
|
|
provider := string(ctx.Request.Header.Peek("x-model-provider"))
|
|
if provider == "" {
|
|
provider = string(schemas.Anthropic)
|
|
}
|
|
bifrostCtx.SetValue(bifrostContextKeyProvider, schemas.ModelProvider(provider))
|
|
return nil
|
|
}
|
|
|
|
// extractAnthropicFileListQueryParams extracts provider from header and query parameters for Anthropic file list requests
|
|
func extractAnthropicFileListQueryParams(ctx *fasthttp.RequestCtx, bifrostCtx *schemas.BifrostContext, req interface{}) error {
|
|
if listReq, ok := req.(*anthropic.AnthropicFileListRequest); ok {
|
|
// Extract provider from header, default to Anthropic
|
|
provider := string(ctx.Request.Header.Peek("x-model-provider"))
|
|
if provider == "" {
|
|
provider = string(schemas.Anthropic)
|
|
}
|
|
|
|
bifrostCtx.SetValue(bifrostContextKeyProvider, schemas.ModelProvider(provider))
|
|
|
|
// Extract limit from query parameters
|
|
if limitStr := string(ctx.QueryArgs().Peek("limit")); limitStr != "" {
|
|
if limit, err := strconv.Atoi(limitStr); err == nil {
|
|
listReq.Limit = limit
|
|
} else {
|
|
// We are keeping default as 30
|
|
listReq.Limit = 30
|
|
}
|
|
}
|
|
|
|
// Extract after_id cursor
|
|
if afterID := string(ctx.QueryArgs().Peek("after_id")); afterID != "" {
|
|
listReq.After = &afterID
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// extractAnthropicFileIDFromPath extracts provider from header and file_id from path parameters
|
|
func extractAnthropicFileIDFromPath(ctx *fasthttp.RequestCtx, bifrostCtx *schemas.BifrostContext, req interface{}) error {
|
|
// Extract provider from header, default to Anthropic
|
|
provider := string(ctx.Request.Header.Peek("x-model-provider"))
|
|
if provider == "" {
|
|
provider = string(schemas.Anthropic)
|
|
}
|
|
bifrostCtx.SetValue(bifrostContextKeyProvider, schemas.ModelProvider(provider))
|
|
fileID := ctx.UserValue("file_id")
|
|
if fileID == nil {
|
|
return errors.New("file_id is required")
|
|
}
|
|
|
|
fileIDStr, ok := fileID.(string)
|
|
if !ok || fileIDStr == "" {
|
|
return errors.New("file_id must be a non-empty string")
|
|
}
|
|
|
|
switch r := req.(type) {
|
|
case *anthropic.AnthropicFileRetrieveRequest:
|
|
r.FileID = fileIDStr
|
|
|
|
case *anthropic.AnthropicFileDeleteRequest:
|
|
r.FileID = fileIDStr
|
|
|
|
case *anthropic.AnthropicFileContentRequest:
|
|
r.FileID = fileIDStr
|
|
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// CreateAnthropicFilesRouteConfigs creates route configurations for Anthropic Files API endpoints.
|
|
func CreateAnthropicFilesRouteConfigs(pathPrefix string, handlerStore lib.HandlerStore) []RouteConfig {
|
|
var routes []RouteConfig
|
|
|
|
// Upload file endpoint - POST /v1/files
|
|
routes = append(routes, RouteConfig{
|
|
Type: RouteConfigTypeAnthropic,
|
|
Path: pathPrefix + "/v1/files",
|
|
Method: "POST",
|
|
GetHTTPRequestType: func(ctx *fasthttp.RequestCtx) schemas.RequestType {
|
|
return schemas.FileUploadRequest
|
|
},
|
|
GetRequestTypeInstance: func(ctx context.Context) interface{} {
|
|
return &anthropic.AnthropicFileUploadRequest{}
|
|
},
|
|
RequestParser: func(ctx *fasthttp.RequestCtx, req interface{}) error {
|
|
uploadReq, ok := req.(*anthropic.AnthropicFileUploadRequest)
|
|
if !ok {
|
|
return errors.New("invalid request type for file upload")
|
|
}
|
|
providerHeader := string(ctx.Request.Header.Peek("x-model-provider"))
|
|
if providerHeader == "" {
|
|
providerHeader = string(schemas.Anthropic)
|
|
}
|
|
provider := schemas.ModelProvider(providerHeader)
|
|
// Parse multipart form
|
|
form, err := ctx.MultipartForm()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Extract purpose (required)
|
|
purposeValues := form.Value["purpose"]
|
|
if len(purposeValues) > 0 && purposeValues[0] != "" {
|
|
uploadReq.Purpose = purposeValues[0]
|
|
} else if provider == schemas.OpenAI && uploadReq.Purpose == "" {
|
|
uploadReq.Purpose = "batch"
|
|
}
|
|
// Extract file (required)
|
|
fileHeaders := form.File["file"]
|
|
if len(fileHeaders) == 0 {
|
|
return errors.New("file field is required")
|
|
}
|
|
// Read file content
|
|
fileHeader := fileHeaders[0]
|
|
file, err := fileHeader.Open()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer file.Close()
|
|
// Read file data
|
|
fileData, err := io.ReadAll(file)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
uploadReq.File = fileData
|
|
uploadReq.Filename = fileHeader.Filename
|
|
return nil
|
|
},
|
|
FileRequestConverter: func(ctx *schemas.BifrostContext, req any) (*FileRequest, error) {
|
|
if uploadReq, ok := req.(*anthropic.AnthropicFileUploadRequest); ok {
|
|
// Here if provider is OpenAI and purpose is empty then we override it with "batch"
|
|
provider, ok := ctx.Value(bifrostContextKeyProvider).(schemas.ModelProvider)
|
|
if !ok {
|
|
return nil, errors.New("provider not found in context")
|
|
}
|
|
if provider == schemas.OpenAI && uploadReq.Purpose == "" {
|
|
uploadReq.Purpose = "batch"
|
|
}
|
|
return &FileRequest{
|
|
Type: schemas.FileUploadRequest,
|
|
UploadRequest: &schemas.BifrostFileUploadRequest{
|
|
File: uploadReq.File,
|
|
Filename: uploadReq.Filename,
|
|
Purpose: schemas.FilePurpose(uploadReq.Purpose),
|
|
Provider: provider,
|
|
},
|
|
}, nil
|
|
}
|
|
return nil, errors.New("invalid file upload request type")
|
|
},
|
|
FileUploadResponseConverter: func(ctx *schemas.BifrostContext, resp *schemas.BifrostFileUploadResponse) (interface{}, error) {
|
|
if resp.ExtraFields.RawResponse != nil {
|
|
return resp.ExtraFields.RawResponse, nil
|
|
}
|
|
if resp.ExtraFields.Provider == schemas.Gemini {
|
|
// Here we will convert fileId to replace files/ with files-
|
|
resp.ID = strings.Replace(resp.ID, "files/", "files-", 1)
|
|
}
|
|
return anthropic.ToAnthropicFileUploadResponse(resp), nil
|
|
},
|
|
ErrorConverter: func(ctx *schemas.BifrostContext, err *schemas.BifrostError) interface{} {
|
|
return anthropic.ToAnthropicChatCompletionError(err)
|
|
},
|
|
PreCallback: extractAnthropicFileUploadParams,
|
|
})
|
|
|
|
// List files endpoint - GET /v1/files
|
|
routes = append(routes, RouteConfig{
|
|
Type: RouteConfigTypeAnthropic,
|
|
Path: pathPrefix + "/v1/files",
|
|
Method: "GET",
|
|
GetHTTPRequestType: func(ctx *fasthttp.RequestCtx) schemas.RequestType {
|
|
return schemas.FileListRequest
|
|
},
|
|
GetRequestTypeInstance: func(ctx context.Context) interface{} {
|
|
return &anthropic.AnthropicFileListRequest{}
|
|
},
|
|
FileRequestConverter: func(ctx *schemas.BifrostContext, req interface{}) (*FileRequest, error) {
|
|
if listReq, ok := req.(*anthropic.AnthropicFileListRequest); ok {
|
|
provider := ctx.Value(bifrostContextKeyProvider).(schemas.ModelProvider)
|
|
return &FileRequest{
|
|
Type: schemas.FileListRequest,
|
|
ListRequest: &schemas.BifrostFileListRequest{
|
|
Limit: listReq.Limit,
|
|
After: listReq.After,
|
|
Order: listReq.Order,
|
|
Provider: provider,
|
|
},
|
|
}, nil
|
|
}
|
|
return nil, errors.New("invalid file list request type")
|
|
},
|
|
FileListResponseConverter: func(ctx *schemas.BifrostContext, resp *schemas.BifrostFileListResponse) (interface{}, error) {
|
|
if resp.ExtraFields.RawResponse != nil {
|
|
return resp.ExtraFields.RawResponse, nil
|
|
}
|
|
if resp.ExtraFields.Provider == schemas.Gemini {
|
|
// Here we will convert fileId to replace files/ with files-
|
|
for i, file := range resp.Data {
|
|
resp.Data[i].ID = strings.Replace(file.ID, "files/", "files-", 1)
|
|
}
|
|
}
|
|
return anthropic.ToAnthropicFileListResponse(resp), nil
|
|
},
|
|
ErrorConverter: func(ctx *schemas.BifrostContext, err *schemas.BifrostError) interface{} {
|
|
return anthropic.ToAnthropicChatCompletionError(err)
|
|
},
|
|
PreCallback: extractAnthropicFileListQueryParams,
|
|
})
|
|
|
|
// Retrieve file endpoint - GET /v1/files/{file_id}
|
|
routes = append(routes, RouteConfig{
|
|
Type: RouteConfigTypeAnthropic,
|
|
Path: pathPrefix + "/v1/files/{file_id}/content",
|
|
Method: "GET",
|
|
GetHTTPRequestType: func(ctx *fasthttp.RequestCtx) schemas.RequestType {
|
|
return schemas.FileContentRequest
|
|
},
|
|
GetRequestTypeInstance: func(ctx context.Context) interface{} {
|
|
return &anthropic.AnthropicFileRetrieveRequest{}
|
|
},
|
|
FileRequestConverter: func(ctx *schemas.BifrostContext, req interface{}) (*FileRequest, error) {
|
|
if retrieveReq, ok := req.(*anthropic.AnthropicFileRetrieveRequest); ok {
|
|
provider := ctx.Value(bifrostContextKeyProvider).(schemas.ModelProvider)
|
|
// Handle file id conversion for Gemini
|
|
if provider == schemas.Gemini {
|
|
retrieveReq.FileID = strings.Replace(retrieveReq.FileID, "files-", "files/", 1)
|
|
}
|
|
return &FileRequest{
|
|
Type: schemas.FileRetrieveRequest,
|
|
RetrieveRequest: &schemas.BifrostFileRetrieveRequest{
|
|
FileID: retrieveReq.FileID,
|
|
Provider: provider,
|
|
},
|
|
}, nil
|
|
}
|
|
return nil, errors.New("invalid file retrieve request type")
|
|
},
|
|
FileRetrieveResponseConverter: func(ctx *schemas.BifrostContext, resp *schemas.BifrostFileRetrieveResponse) (interface{}, error) {
|
|
if resp.ExtraFields.RawResponse != nil {
|
|
return resp.ExtraFields.RawResponse, nil
|
|
}
|
|
return anthropic.ToAnthropicFileRetrieveResponse(resp), nil
|
|
},
|
|
ErrorConverter: func(ctx *schemas.BifrostContext, err *schemas.BifrostError) interface{} {
|
|
return anthropic.ToAnthropicChatCompletionError(err)
|
|
},
|
|
PreCallback: extractAnthropicFileIDFromPath,
|
|
})
|
|
|
|
// Delete file endpoint - DELETE /v1/files/{file_id}
|
|
routes = append(routes, RouteConfig{
|
|
Type: RouteConfigTypeAnthropic,
|
|
Path: pathPrefix + "/v1/files/{file_id}",
|
|
Method: "DELETE",
|
|
GetHTTPRequestType: func(ctx *fasthttp.RequestCtx) schemas.RequestType {
|
|
return schemas.FileDeleteRequest
|
|
},
|
|
GetRequestTypeInstance: func(ctx context.Context) interface{} {
|
|
return &anthropic.AnthropicFileDeleteRequest{}
|
|
},
|
|
FileRequestConverter: func(ctx *schemas.BifrostContext, req interface{}) (*FileRequest, error) {
|
|
if deleteReq, ok := req.(*anthropic.AnthropicFileDeleteRequest); ok {
|
|
provider := ctx.Value(bifrostContextKeyProvider).(schemas.ModelProvider)
|
|
if provider == schemas.Gemini {
|
|
// Here we will convert fileId to replace files/ with files-
|
|
deleteReq.FileID = strings.Replace(deleteReq.FileID, "files-", "files/", 1)
|
|
}
|
|
return &FileRequest{
|
|
Type: schemas.FileDeleteRequest,
|
|
DeleteRequest: &schemas.BifrostFileDeleteRequest{
|
|
FileID: deleteReq.FileID,
|
|
Provider: provider,
|
|
},
|
|
}, nil
|
|
}
|
|
return nil, errors.New("invalid file delete request type")
|
|
},
|
|
FileDeleteResponseConverter: func(ctx *schemas.BifrostContext, resp *schemas.BifrostFileDeleteResponse) (interface{}, error) {
|
|
if resp.ExtraFields.RawResponse != nil {
|
|
return resp.ExtraFields.RawResponse, nil
|
|
}
|
|
return anthropic.ToAnthropicFileDeleteResponse(resp), nil
|
|
},
|
|
ErrorConverter: func(ctx *schemas.BifrostContext, err *schemas.BifrostError) interface{} {
|
|
return anthropic.ToAnthropicChatCompletionError(err)
|
|
},
|
|
PreCallback: extractAnthropicFileIDFromPath,
|
|
})
|
|
return routes
|
|
}
|
|
|
|
// NewAnthropicRouter creates a new AnthropicRouter with the given bifrost client.
|
|
func NewAnthropicRouter(client *bifrost.Bifrost, handlerStore lib.HandlerStore, logger schemas.Logger) *AnthropicRouter {
|
|
routes := CreateAnthropicRouteConfigs("/anthropic", logger)
|
|
routes = append(routes, CreateAnthropicListModelsRouteConfigs("/anthropic", handlerStore)...)
|
|
routes = append(routes, CreateAnthropicCountTokensRouteConfigs("/anthropic", handlerStore)...)
|
|
routes = append(routes, CreateAnthropicBatchRouteConfigs("/anthropic", handlerStore)...)
|
|
routes = append(routes, CreateAnthropicFilesRouteConfigs("/anthropic", handlerStore)...)
|
|
|
|
return &AnthropicRouter{
|
|
GenericRouter: NewGenericRouter(client, handlerStore, routes, nil, logger),
|
|
}
|
|
}
|