From 8dcfb436f5a196bef26892711052a44c40afc63b Mon Sep 17 00:00:00 2001 From: Pratham-Mishra04 Date: Tue, 16 Dec 2025 14:11:29 +0530 Subject: [PATCH 1/2] feat: http response parsing support added --- core/changelog.md | 1 + .../utils/html_response_handler_test.go | 302 ++++++++++++++++++ core/providers/utils/utils.go | 186 ++++++++++- core/providers/vertex/errors.go | 42 +++ core/schemas/provider.go | 2 + transports/changelog.md | 1 + .../views/modelProviderKeysTableView.tsx | 5 +- 7 files changed, 527 insertions(+), 12 deletions(-) create mode 100644 core/providers/utils/html_response_handler_test.go diff --git a/core/changelog.md b/core/changelog.md index 8b06ca4fa..d074acdac 100644 --- a/core/changelog.md +++ b/core/changelog.md @@ -1,3 +1,4 @@ +- feat: add handling for HTML and empty responses from providers - feat: adds new parameter for each provider key config `use_for_batch_apis`. This helps users to select which APIs or accounts to be used for Batch APIs. - feat: adds s3 bucket config support for Bedrock provider. - feat: prompt caching support for anthropic and bedrock(claude and nova models) diff --git a/core/providers/utils/html_response_handler_test.go b/core/providers/utils/html_response_handler_test.go new file mode 100644 index 000000000..a9b31d8a9 --- /dev/null +++ b/core/providers/utils/html_response_handler_test.go @@ -0,0 +1,302 @@ +package utils + +import ( + "strings" + "testing" + + "github.com/valyala/fasthttp" +) + +func TestIsHTMLResponse(t *testing.T) { + tests := []struct { + name string + contentType string + body []byte + expectedIsHTML bool + description string + }{ + { + name: "HTML with Content-Type header", + contentType: "text/html; charset=utf-8", + body: []byte("Error"), + expectedIsHTML: true, + description: "Should detect HTML from Content-Type header", + }, + { + name: "HTML without Content-Type", + contentType: "application/octet-stream", + body: []byte("Error 500"), + expectedIsHTML: true, + description: "Should detect HTML from DOCTYPE", + }, + { + name: "HTML with h1 tag", + contentType: "application/octet-stream", + body: []byte("

Service Unavailable

"), + expectedIsHTML: true, + description: "Should detect HTML from h1 tag", + }, + { + name: "JSON response", + contentType: "application/json", + body: []byte(`{"error": "invalid request"}`), + expectedIsHTML: false, + description: "Should not detect JSON as HTML", + }, + { + name: "Plain text response", + contentType: "text/plain", + body: []byte("Invalid request"), + expectedIsHTML: false, + description: "Should not detect plain text as HTML", + }, + { + name: "Empty body", + contentType: "text/html", + body: []byte(""), + expectedIsHTML: true, + description: "Should detect HTML from Content-Type even with empty body", + }, + { + name: "Very short body", + contentType: "application/json", + body: []byte("abc"), + expectedIsHTML: false, + description: "Should not detect very short body as HTML", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + resp := &fasthttp.Response{} + resp.Header.Set("Content-Type", tt.contentType) + + result := IsHTMLResponse(resp, tt.body) + if result != tt.expectedIsHTML { + t.Errorf("isHTMLResponse() = %v, want %v. %s", result, tt.expectedIsHTML, tt.description) + } + }) + } +} + +func TestExtractHTMLErrorMessage(t *testing.T) { + tests := []struct { + name string + htmlBody []byte + expectMsg string + description string + }{ + { + name: "Extract from title tag", + htmlBody: []byte(` + + + 404 Not Found +

The page was not found

+ + `), + expectMsg: "404 Not Found", + description: "Should extract title from title tag", + }, + { + name: "Extract from h1 tag", + htmlBody: []byte(` + + + +

Service Unavailable

+

The service is currently unavailable

+ + + `), + expectMsg: "Service Unavailable", + description: "Should extract from h1 tag when title is missing", + }, + { + name: "Extract from h2 tag", + htmlBody: []byte(` + + + +

Authentication Failed

+

Please check your credentials

+ + + `), + expectMsg: "Authentication Failed", + description: "Should extract from h2 tag with attributes", + }, + { + name: "Extract visible text when no headers", + htmlBody: []byte(` + + +
There was an error processing your request. Please try again later.
+ + + `), + expectMsg: "There was an error processing your request. Please try again later.", + description: "Should extract visible text from div when no headers found", + }, + { + name: "Ignore script and style tags", + htmlBody: []byte(` + + Error + + + +

Actual Error Message

+ + + `), + expectMsg: "Actual Error Message", + description: "Should ignore script and style content", + }, + { + name: "Extract from first valid h1", + htmlBody: []byte(` + + +

+

Second header with actual content

+ + + `), + expectMsg: "Second header with actual content", + description: "Should extract from first non-empty header", + }, + { + name: "Handle meta description", + htmlBody: []byte(` + + + + + + + `), + expectMsg: "Rate limit exceeded. Please wait 60 seconds.", + description: "Should extract from meta description", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := ExtractHTMLErrorMessage(tt.htmlBody) + if result != tt.expectMsg { + t.Errorf("extractHTMLErrorMessage() = %q, want %q. %s", result, tt.expectMsg, tt.description) + } + }) + } +} + +func TestHandleProviderAPIErrorWithHTML(t *testing.T) { + tests := []struct { + name string + statusCode int + contentType string + body []byte + description string + expectedInMessage string + }{ + { + name: "HTML 500 error - lazy detection", + statusCode: 500, + contentType: "text/html; charset=utf-8", + body: []byte(` + + + Internal Server Error +

Something went wrong

+ + `), + description: "Should detect and handle HTML only after JSON parse fails", + expectedInMessage: "Internal Server Error", + }, + { + name: "HTML 403 error - lazy detection", + statusCode: 403, + contentType: "text/html", + body: []byte(` + + +

Forbidden

+

Access denied

+ + + `), + description: "Should detect and extract message from HTML on parse failure", + expectedInMessage: "Forbidden", + }, + { + name: "Invalid JSON with HTML fallback", + statusCode: 400, + contentType: "application/json", + body: []byte(`not valid json`), + description: "Should fall back to raw string when not HTML", + expectedInMessage: "provider API error", + }, + { + name: "Valid JSON error response", + statusCode: 400, + contentType: "application/json", + body: []byte(`{"error": {"message": "Invalid request"}, "code": "invalid_request"}`), + description: "Should handle valid JSON without HTML detection", + expectedInMessage: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + resp := &fasthttp.Response{} + resp.SetStatusCode(tt.statusCode) + resp.Header.Set("Content-Type", tt.contentType) + resp.SetBody(tt.body) + + var errorResp map[string]interface{} + bifrostErr := HandleProviderAPIError(resp, &errorResp) + + if bifrostErr == nil { + t.Errorf("HandleProviderAPIError() returned nil error") + return + } + + if bifrostErr.StatusCode == nil || *bifrostErr.StatusCode != tt.statusCode { + t.Errorf("HandleProviderAPIError() status code = %v, want %v", bifrostErr.StatusCode, tt.statusCode) + } + + if bifrostErr.Error == nil { + t.Errorf("HandleProviderAPIError() error field is nil") + return + } + + // Check if expected message is in the response + if tt.expectedInMessage != "" && !strings.Contains(bifrostErr.Error.Message, tt.expectedInMessage) { + t.Errorf("Expected message to contain %q, got %q", tt.expectedInMessage, bifrostErr.Error.Message) + } + + t.Logf("Handled %s: status=%d, message=%q", tt.name, *bifrostErr.StatusCode, bifrostErr.Error.Message) + }) + } +} + +func BenchmarkIsHTMLResponse(b *testing.B) { + resp := &fasthttp.Response{} + resp.Header.Set("Content-Type", "text/html; charset=utf-8") + body := []byte(`Error

Test Error

`) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + IsHTMLResponse(resp, body) + } +} + +func BenchmarkExtractHTMLErrorMessage(b *testing.B) { + body := []byte(`Internal Server Error

Something went wrong

This is a detailed error message

`) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + ExtractHTMLErrorMessage(body) + } +} diff --git a/core/providers/utils/utils.go b/core/providers/utils/utils.go index c1a67647e..53a16ac55 100644 --- a/core/providers/utils/utils.go +++ b/core/providers/utils/utils.go @@ -11,6 +11,7 @@ import ( "net/http" "net/textproto" "net/url" + "regexp" "slices" "sort" "strings" @@ -314,12 +315,13 @@ func SetExtraHeadersHTTP(ctx context.Context, req *http.Request, extraHeaders ma // HandleProviderAPIError processes error responses from provider APIs. // It attempts to unmarshal the error response and returns a BifrostError // with the appropriate status code and error information. -// errorResp must be a pointer to the target struct for unmarshaling. +// HTML detection only runs if JSON parsing fails to avoid expensive regex operations +// on responses that are almost certainly valid JSON. errorResp must be a pointer to +// the target struct for unmarshaling. func HandleProviderAPIError(resp *fasthttp.Response, errorResp any) *schemas.BifrostError { statusCode := resp.StatusCode() - body := append([]byte(nil), resp.Body()...) - // decode body + // Decode body decodedBody, err := CheckAndDecodeBody(resp) if err != nil { return &schemas.BifrostError{ @@ -331,24 +333,48 @@ func HandleProviderAPIError(resp *fasthttp.Response, errorResp any) *schemas.Bif } } - body = decodedBody + // Check for empty response + trimmed := strings.TrimSpace(string(decodedBody)) + if len(trimmed) == 0 { + return &schemas.BifrostError{ + IsBifrostError: false, + StatusCode: &statusCode, + Error: &schemas.ErrorField{ + Message: schemas.ErrProviderResponseEmpty, + }, + } + } + + // Try JSON parsing first + if err := sonic.Unmarshal(decodedBody, errorResp); err == nil { + // JSON parsing succeeded, return success + return &schemas.BifrostError{ + IsBifrostError: false, + StatusCode: &statusCode, + Error: &schemas.ErrorField{}, + } + } - if err := sonic.Unmarshal(body, errorResp); err != nil { - rawResponse := body - message := fmt.Sprintf("provider API error: %s", string(rawResponse)) + // JSON parsing failed - now check if it's an HTML response (expensive operation) + if IsHTMLResponse(resp, decodedBody) { + errorMessage := ExtractHTMLErrorMessage(decodedBody) return &schemas.BifrostError{ IsBifrostError: false, StatusCode: &statusCode, Error: &schemas.ErrorField{ - Message: message, + Message: errorMessage, }, } } + // Not HTML either - return raw response as error message + message := fmt.Sprintf("provider API error: %s", string(decodedBody)) return &schemas.BifrostError{ IsBifrostError: false, StatusCode: &statusCode, - Error: &schemas.ErrorField{}, + Error: &schemas.ErrorField{ + Message: message, + }, } } @@ -356,7 +382,20 @@ func HandleProviderAPIError(resp *fasthttp.Response, errorResp any) *schemas.Bif // It attempts to parse the response body into the provided response type // and returns either the parsed response or a BifrostError if parsing fails. // If sendBackRawResponse is true, it returns the raw response interface, otherwise nil. +// HTML detection only runs if JSON parsing fails to avoid expensive regex operations +// on responses that are almost certainly valid JSON. func HandleProviderResponse[T any](responseBody []byte, response *T, requestBody []byte, sendBackRawRequest bool, sendBackRawResponse bool) (rawRequest interface{}, rawResponse interface{}, bifrostErr *schemas.BifrostError) { + // Check for empty response + trimmed := strings.TrimSpace(string(responseBody)) + if len(trimmed) == 0 { + return nil, nil, &schemas.BifrostError{ + IsBifrostError: true, + Error: &schemas.ErrorField{ + Message: schemas.ErrProviderResponseEmpty, + }, + } + } + var wg sync.WaitGroup var structuredErr, rawRequestErr, rawResponseErr error @@ -394,6 +433,18 @@ func HandleProviderResponse[T any](responseBody []byte, response *T, requestBody wg.Wait() if structuredErr != nil { + // JSON parsing failed - check if it's an HTML response (expensive operation) + if IsHTMLResponse(nil, responseBody) { + errorMessage := ExtractHTMLErrorMessage(responseBody) + return nil, nil, &schemas.BifrostError{ + IsBifrostError: false, + Error: &schemas.ErrorField{ + Message: schemas.ErrProviderResponseHTML, + Error: errors.New(errorMessage), + }, + } + } + return nil, nil, &schemas.BifrostError{ IsBifrostError: true, Error: &schemas.ErrorField{ @@ -495,6 +546,123 @@ func CheckAndDecodeBody(resp *fasthttp.Response) ([]byte, error) { } } +// IsHTMLResponse checks if the response is HTML by examining the Content-Type header +// and/or the response body for HTML indicators. +func IsHTMLResponse(resp *fasthttp.Response, body []byte) bool { + // Check Content-Type header first (most reliable indicator) + if resp != nil { + contentType := strings.ToLower(string(resp.Header.Peek("Content-Type"))) + if strings.Contains(contentType, "text/html") { + return true + } + } + + // If body is small, it's unlikely to be HTML + if len(body) < 20 { + return false + } + + // Check for HTML indicators in body + bodyLower := strings.ToLower(string(body)) + + // Look for common HTML tags or DOCTYPE + htmlIndicators := []string{ + "", + "

", + "

", + "

", + "

", + " maxBodySize { + body = body[:maxBodySize] + } + + bodyStr := string(body) + bodyLower := strings.ToLower(bodyStr) + + // Try to extract title first + if idx := strings.Index(bodyLower, ""); idx != -1 { + endIdx := strings.Index(bodyLower[idx:], "") + if endIdx != -1 { + title := strings.TrimSpace(bodyStr[idx+7 : idx+endIdx]) + if title != "" && title != "Error" { + return title + } + } + } + + // Try to extract from h1, h2, h3 tags (common for error pages) + for _, tag := range []string{"h1", "h2", "h3"} { + pattern := fmt.Sprintf("<%s[^>]*>([^<]+)", tag, tag) + re := regexp.MustCompile("(?i)" + pattern) + if matches := re.FindStringSubmatch(bodyStr); len(matches) > 1 { + msg := strings.TrimSpace(matches[1]) + if msg != "" { + return msg + } + } + } + + // Try to extract from meta description + pattern := ` 1 { + msg := strings.TrimSpace(matches[1]) + if msg != "" { + return msg + } + } + + // Extract visible text: remove script and style tags, then extract text + // Remove script and style tags and their content + re = regexp.MustCompile(`(?i)]*>.*?|]*>.*?`) + cleaned := re.ReplaceAllString(bodyStr, "") + + // Remove HTML tags + re = regexp.MustCompile(`<[^>]+>`) + cleaned = re.ReplaceAllString(cleaned, " ") + + // Clean up whitespace and get first meaningful sentence + sentences := strings.FieldsFunc(cleaned, func(r rune) bool { + return r == '\n' || r == '\r' + }) + + for _, sentence := range sentences { + trimmed := strings.TrimSpace(sentence) + if len(trimmed) > 10 && len(trimmed) < 500 { + // Limit to first 200 chars to avoid very long messages + if len(trimmed) > 200 { + trimmed = trimmed[:200] + "..." + } + return trimmed + } + } + + // If all else fails, return a generic message with status code context + return "HTML error response received from provider" +} + // JSONLParseResult holds parsed items and any line-level errors encountered during parsing. type JSONLParseResult struct { Errors []schemas.BatchError diff --git a/core/providers/vertex/errors.go b/core/providers/vertex/errors.go index af826e22a..dec9a93ed 100644 --- a/core/providers/vertex/errors.go +++ b/core/providers/vertex/errors.go @@ -1,6 +1,8 @@ package vertex import ( + "strings" + "github.com/bytedance/sonic" providerUtils "github.com/maximhq/bifrost/core/providers/utils" "github.com/maximhq/bifrost/core/schemas" @@ -29,6 +31,46 @@ func parseVertexError(resp *fasthttp.Response, meta *providerUtils.RequestMetada return bifrostErr } + // Check for empty response + trimmed := strings.TrimSpace(string(decodedBody)) + if len(trimmed) == 0 { + bifrostErr := &schemas.BifrostError{ + IsBifrostError: false, + StatusCode: schemas.Ptr(resp.StatusCode()), + Error: &schemas.ErrorField{ + Message: schemas.ErrProviderResponseEmpty, + }, + } + if meta != nil { + bifrostErr.ExtraFields = schemas.BifrostErrorExtraFields{ + Provider: meta.Provider, + ModelRequested: meta.Model, + RequestType: meta.RequestType, + } + } + return bifrostErr + } + + // Check for HTML error response before attempting JSON parsing + if providerUtils.IsHTMLResponse(resp, decodedBody) { + errorMsg := providerUtils.ExtractHTMLErrorMessage(decodedBody) + bifrostErr := &schemas.BifrostError{ + IsBifrostError: false, + StatusCode: schemas.Ptr(resp.StatusCode()), + Error: &schemas.ErrorField{ + Message: errorMsg, + }, + } + if meta != nil { + bifrostErr.ExtraFields = schemas.BifrostErrorExtraFields{ + Provider: meta.Provider, + ModelRequested: meta.Model, + RequestType: meta.RequestType, + } + } + return bifrostErr + } + createError := func(message string) *schemas.BifrostError { bifrostErr := providerUtils.NewProviderAPIError(message, nil, resp.StatusCode(), providerName, nil, nil) if meta != nil { diff --git a/core/schemas/provider.go b/core/schemas/provider.go index d7a0b7ea3..966a1914a 100644 --- a/core/schemas/provider.go +++ b/core/schemas/provider.go @@ -28,6 +28,8 @@ const ( ErrProviderDoRequest = "failed to execute HTTP request to provider API" ErrProviderResponseDecode = "failed to decode response body from provider API" ErrProviderResponseUnmarshal = "failed to unmarshal response from provider API" + ErrProviderResponseEmpty = "empty response received from provider" + ErrProviderResponseHTML = "HTML response received from provider" ErrProviderRawRequestUnmarshal = "failed to unmarshal raw request from provider API" ErrProviderRawResponseUnmarshal = "failed to unmarshal raw response from provider API" ErrProviderResponseDecompress = "failed to decompress provider's response" diff --git a/transports/changelog.md b/transports/changelog.md index ab10253af..fc8f44803 100644 --- a/transports/changelog.md +++ b/transports/changelog.md @@ -1,3 +1,4 @@ +- feat: add handling for HTML and empty responses from providers - feat: adds new parameter for each provider key config `use_for_batch_apis`. This helps users to select which APIs or accounts to be used for Batch APIs. - feat: adds recalculate missing costs for logs - [@hpbyte](https://github.com/hpbyte) - chore: increased provider-level timeout limit to 48 hours diff --git a/ui/app/workspace/providers/views/modelProviderKeysTableView.tsx b/ui/app/workspace/providers/views/modelProviderKeysTableView.tsx index b921a5119..accfd7fb4 100644 --- a/ui/app/workspace/providers/views/modelProviderKeysTableView.tsx +++ b/ui/app/workspace/providers/views/modelProviderKeysTableView.tsx @@ -134,13 +134,12 @@ export default function ModelProviderKeysTableView({ provider, className }: Prop { updateProvider({ ...provider, - keys: provider.keys.map((k, i) => - i === index ? { ...k, enabled: checked } : k - ), + keys: provider.keys.map((k, i) => (i === index ? { ...k, enabled: checked } : k)), }) .unwrap() .then(() => { From f10953276e5f209a270359c91588801db7abd4e5 Mon Sep 17 00:00:00 2001 From: Pratham-Mishra04 Date: Tue, 16 Dec 2025 17:54:18 +0530 Subject: [PATCH 2/2] chore: minor refactor and changelogs --- core/changelog.md | 1 + core/providers/elevenlabs/elevenlabs.go | 11 +++++++ core/providers/mistral/mistral.go | 40 ++++++++++++++++++++----- core/providers/openai/openai.go | 27 +++++++++++++++-- core/providers/utils/utils.go | 1 + transports/changelog.md | 2 ++ 6 files changed, 72 insertions(+), 10 deletions(-) diff --git a/core/changelog.md b/core/changelog.md index d074acdac..4c2e2ebe1 100644 --- a/core/changelog.md +++ b/core/changelog.md @@ -1,4 +1,5 @@ - feat: add handling for HTML and empty responses from providers +- feat: add audio token pricing support for models - feat: adds new parameter for each provider key config `use_for_batch_apis`. This helps users to select which APIs or accounts to be used for Batch APIs. - feat: adds s3 bucket config support for Bedrock provider. - feat: prompt caching support for anthropic and bedrock(claude and nova models) diff --git a/core/providers/elevenlabs/elevenlabs.go b/core/providers/elevenlabs/elevenlabs.go index 3443e5132..128b1f4c9 100644 --- a/core/providers/elevenlabs/elevenlabs.go +++ b/core/providers/elevenlabs/elevenlabs.go @@ -522,6 +522,17 @@ func (provider *ElevenlabsProvider) Transcription(ctx context.Context, key schem return nil, providerUtils.NewBifrostOperationError(schemas.ErrProviderResponseDecode, err, providerName) } + // Check for empty response + trimmed := strings.TrimSpace(string(responseBody)) + if len(trimmed) == 0 { + return nil, &schemas.BifrostError{ + IsBifrostError: true, + Error: &schemas.ErrorField{ + Message: schemas.ErrProviderResponseEmpty, + }, + } + } + chunks, err := parseTranscriptionResponse(responseBody) if err != nil { return nil, providerUtils.NewBifrostOperationError(err.Error(), nil, providerName) diff --git a/core/providers/mistral/mistral.go b/core/providers/mistral/mistral.go index eece5e224..e65445a92 100644 --- a/core/providers/mistral/mistral.go +++ b/core/providers/mistral/mistral.go @@ -299,12 +299,36 @@ func (provider *MistralProvider) Transcription(ctx context.Context, key schemas. return nil, openai.ParseOpenAIError(resp, schemas.TranscriptionRequest, providerName, request.Model) } - // Copy response body before releasing - responseBody := append([]byte(nil), resp.Body()...) + responseBody, err := providerUtils.CheckAndDecodeBody(resp) + if err != nil { + return nil, providerUtils.NewBifrostOperationError(schemas.ErrProviderResponseDecode, err, providerName) + } + + // Check for empty response + trimmed := strings.TrimSpace(string(responseBody)) + if len(trimmed) == 0 { + return nil, &schemas.BifrostError{ + IsBifrostError: true, + Error: &schemas.ErrorField{ + Message: schemas.ErrProviderResponseEmpty, + }, + } + } + + copiedResponseBody := append([]byte(nil), responseBody...) // Parse Mistral's transcription response var mistralResponse MistralTranscriptionResponse - if err := sonic.Unmarshal(responseBody, &mistralResponse); err != nil { + if err := sonic.Unmarshal(copiedResponseBody, &mistralResponse); err != nil { + if providerUtils.IsHTMLResponse(resp, copiedResponseBody) { + errorMessage := providerUtils.ExtractHTMLErrorMessage(copiedResponseBody) + return nil, &schemas.BifrostError{ + IsBifrostError: false, + Error: &schemas.ErrorField{ + Message: errorMessage, + }, + } + } return nil, providerUtils.NewBifrostOperationError(schemas.ErrProviderResponseUnmarshal, err, providerName) } @@ -323,7 +347,7 @@ func (provider *MistralProvider) Transcription(ctx context.Context, key schemas. // Set raw response if enabled if providerUtils.ShouldSendBackRawResponse(ctx, provider.sendBackRawResponse) { var rawResponse interface{} - if err := sonic.Unmarshal(responseBody, &rawResponse); err == nil { + if err := sonic.Unmarshal(copiedResponseBody, &rawResponse); err == nil { response.ExtraFields.RawResponse = rawResponse } } @@ -441,7 +465,7 @@ func (provider *MistralProvider) TranscriptionStream(ctx context.Context, postHo // Process accumulated event if we have both event and data if currentEvent != "" && currentData != "" { chunkIndex++ - provider.processStreamEvent(ctx, postHookRunner, currentEvent, currentData, request.Model, providerName, chunkIndex, startTime, &lastChunkTime, responseChan) + provider.processTranscriptionStreamEvent(ctx, postHookRunner, currentEvent, currentData, request.Model, providerName, chunkIndex, startTime, &lastChunkTime, responseChan) } // Reset for next event currentEvent = "" @@ -460,7 +484,7 @@ func (provider *MistralProvider) TranscriptionStream(ctx context.Context, postHo // Process any remaining event if currentEvent != "" && currentData != "" { chunkIndex++ - provider.processStreamEvent(ctx, postHookRunner, currentEvent, currentData, request.Model, providerName, chunkIndex, startTime, &lastChunkTime, responseChan) + provider.processTranscriptionStreamEvent(ctx, postHookRunner, currentEvent, currentData, request.Model, providerName, chunkIndex, startTime, &lastChunkTime, responseChan) } // Handle scanner errors @@ -473,8 +497,8 @@ func (provider *MistralProvider) TranscriptionStream(ctx context.Context, postHo return responseChan, nil } -// processStreamEvent processes a single SSE event and sends it to the response channel. -func (provider *MistralProvider) processStreamEvent( +// processTranscriptionStreamEvent processes a single SSE event and sends it to the response channel. +func (provider *MistralProvider) processTranscriptionStreamEvent( ctx context.Context, postHookRunner schemas.PostHookRunner, eventType string, diff --git a/core/providers/openai/openai.go b/core/providers/openai/openai.go index 4273d65bf..d5f10a85f 100644 --- a/core/providers/openai/openai.go +++ b/core/providers/openai/openai.go @@ -1890,17 +1890,40 @@ func (provider *OpenAIProvider) Transcription(ctx context.Context, key schemas.K return nil, providerUtils.NewBifrostOperationError(schemas.ErrProviderResponseDecode, err, providerName) } + // Check for empty response + trimmed := strings.TrimSpace(string(responseBody)) + if len(trimmed) == 0 { + return nil, &schemas.BifrostError{ + IsBifrostError: true, + Error: &schemas.ErrorField{ + Message: schemas.ErrProviderResponseEmpty, + }, + } + } + + copiedResponseBody := append([]byte(nil), responseBody...) + // Parse OpenAI's transcription response directly into BifrostTranscribe response := &schemas.BifrostTranscriptionResponse{} - if err := sonic.Unmarshal(responseBody, response); err != nil { + if err := sonic.Unmarshal(copiedResponseBody, response); err != nil { + // Check if it's an HTML response + if providerUtils.IsHTMLResponse(resp, copiedResponseBody) { + errorMessage := providerUtils.ExtractHTMLErrorMessage(copiedResponseBody) + return nil, &schemas.BifrostError{ + IsBifrostError: false, + Error: &schemas.ErrorField{ + Message: errorMessage, + }, + } + } return nil, providerUtils.NewBifrostOperationError(schemas.ErrProviderResponseUnmarshal, err, providerName) } // Parse raw response for RawResponse field var rawResponse interface{} if providerUtils.ShouldSendBackRawResponse(ctx, provider.sendBackRawResponse) { - if err := sonic.Unmarshal(responseBody, &rawResponse); err != nil { + if err := sonic.Unmarshal(copiedResponseBody, &rawResponse); err != nil { return nil, providerUtils.NewBifrostOperationError(schemas.ErrProviderRawResponseUnmarshal, err, providerName) } } diff --git a/core/providers/utils/utils.go b/core/providers/utils/utils.go index 53a16ac55..64e2bf86e 100644 --- a/core/providers/utils/utils.go +++ b/core/providers/utils/utils.go @@ -492,6 +492,7 @@ func HandleProviderResponse[T any](responseBody []byte, response *T, requestBody return nil, nil, nil } +// ParseAndSetRawRequest parses the raw request body and sets it in the extra fields. func ParseAndSetRawRequest(extraFields *schemas.BifrostResponseExtraFields, jsonBody []byte) { var rawRequest interface{} if err := sonic.Unmarshal(jsonBody, &rawRequest); err != nil { diff --git a/transports/changelog.md b/transports/changelog.md index fc8f44803..73744c67c 100644 --- a/transports/changelog.md +++ b/transports/changelog.md @@ -1,4 +1,6 @@ - feat: add handling for HTML and empty responses from providers +- feat: added transcription support for mistral + - feat: adds new parameter for each provider key config `use_for_batch_apis`. This helps users to select which APIs or accounts to be used for Batch APIs. - feat: adds recalculate missing costs for logs - [@hpbyte](https://github.com/hpbyte) - chore: increased provider-level timeout limit to 48 hours