Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions core/bifrost.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ func Init(config schemas.BifrostConfig) (*Bifrost, error) {
// If the primary provider fails, it will try each fallback provider in order until one succeeds.
func (bifrost *Bifrost) TextCompletionRequest(ctx context.Context, req *schemas.BifrostRequest) (*schemas.BifrostResponse, *schemas.BifrostError) {
if err := validateRequest(req); err != nil {
err.Provider = req.Provider
return nil, err
}

Expand All @@ -202,12 +203,14 @@ func (bifrost *Bifrost) TextCompletionRequest(ctx context.Context, req *schemas.
}

if primaryErr.Error.Type != nil && *primaryErr.Error.Type == schemas.RequestCancelled {
primaryErr.Provider = req.Provider
return nil, primaryErr
}

// Check if this is a short-circuit error that doesn't allow fallbacks
// Note: AllowFallbacks = nil is treated as true (allow fallbacks by default)
if primaryErr.AllowFallbacks != nil && !*primaryErr.AllowFallbacks {
primaryErr.Provider = req.Provider
return nil, primaryErr
}

Expand All @@ -234,13 +237,16 @@ func (bifrost *Bifrost) TextCompletionRequest(ctx context.Context, req *schemas.
return result, nil
}
if fallbackErr.Error.Type != nil && *fallbackErr.Error.Type == schemas.RequestCancelled {
fallbackErr.Provider = fallback.Provider
return nil, fallbackErr
}

bifrost.logger.Warn(fmt.Sprintf("Fallback provider %s failed: %s", fallback.Provider, fallbackErr.Error.Message))
}
}

primaryErr.Provider = req.Provider

// All providers failed, return the original error
return nil, primaryErr
}
Expand All @@ -250,6 +256,7 @@ func (bifrost *Bifrost) TextCompletionRequest(ctx context.Context, req *schemas.
// If the primary provider fails, it will try each fallback provider in order until one succeeds.
func (bifrost *Bifrost) ChatCompletionRequest(ctx context.Context, req *schemas.BifrostRequest) (*schemas.BifrostResponse, *schemas.BifrostError) {
if err := validateRequest(req); err != nil {
err.Provider = req.Provider
return nil, err
}

Expand All @@ -259,9 +266,15 @@ func (bifrost *Bifrost) ChatCompletionRequest(ctx context.Context, req *schemas.
return primaryResult, nil
}

if primaryErr.Error.Type != nil && *primaryErr.Error.Type == schemas.RequestCancelled {
primaryErr.Provider = req.Provider
return nil, primaryErr
}

// Check if this is a short-circuit error that doesn't allow fallbacks
// Note: AllowFallbacks = nil is treated as true (allow fallbacks by default)
if primaryErr.AllowFallbacks != nil && !*primaryErr.AllowFallbacks {
primaryErr.Provider = req.Provider
return nil, primaryErr
}

Expand All @@ -288,13 +301,16 @@ func (bifrost *Bifrost) ChatCompletionRequest(ctx context.Context, req *schemas.
return result, nil
}
if fallbackErr.Error.Type != nil && *fallbackErr.Error.Type == schemas.RequestCancelled {
fallbackErr.Provider = fallback.Provider
return nil, fallbackErr
}

bifrost.logger.Warn(fmt.Sprintf("Fallback provider %s failed: %s", fallback.Provider, fallbackErr.Error.Message))
}
}

primaryErr.Provider = req.Provider

// All providers failed, return the original error
return nil, primaryErr
}
Expand All @@ -304,6 +320,7 @@ func (bifrost *Bifrost) ChatCompletionRequest(ctx context.Context, req *schemas.
// If the primary provider fails, it will try each fallback provider in order until one succeeds.
func (bifrost *Bifrost) EmbeddingRequest(ctx context.Context, req *schemas.BifrostRequest) (*schemas.BifrostResponse, *schemas.BifrostError) {
if err := validateRequest(req); err != nil {
err.Provider = req.Provider
return nil, err
}

Expand All @@ -317,9 +334,15 @@ func (bifrost *Bifrost) EmbeddingRequest(ctx context.Context, req *schemas.Bifro
return primaryResult, nil
}

if primaryErr.Error.Type != nil && *primaryErr.Error.Type == schemas.RequestCancelled {
primaryErr.Provider = req.Provider
return nil, primaryErr
}

// Check if this is a short-circuit error that doesn't allow fallbacks
// Note: AllowFallbacks = nil is treated as true (allow fallbacks by default)
if primaryErr.AllowFallbacks != nil && !*primaryErr.AllowFallbacks {
primaryErr.Provider = req.Provider
return nil, primaryErr
}

Expand All @@ -345,13 +368,16 @@ func (bifrost *Bifrost) EmbeddingRequest(ctx context.Context, req *schemas.Bifro
return result, nil
}
if fallbackErr.Error.Type != nil && *fallbackErr.Error.Type == schemas.RequestCancelled {
fallbackErr.Provider = fallback.Provider
return nil, fallbackErr
}

bifrost.logger.Warn(fmt.Sprintf("Fallback provider %s failed: %s", fallback.Provider, fallbackErr.Error.Message))
}
}

primaryErr.Provider = req.Provider

// All providers failed, return the original error
return nil, primaryErr
}
Expand Down
13 changes: 7 additions & 6 deletions core/schemas/bifrost.go
Original file line number Diff line number Diff line change
Expand Up @@ -431,12 +431,13 @@ const (
// - AllowFallbacks = &false: Bifrost will return this error immediately, no fallbacks
// - AllowFallbacks = nil: Treated as true by default (fallbacks allowed for resilience)
type BifrostError struct {
EventID *string `json:"event_id,omitempty"`
Type *string `json:"type,omitempty"`
IsBifrostError bool `json:"is_bifrost_error"`
StatusCode *int `json:"status_code,omitempty"`
Error ErrorField `json:"error"`
AllowFallbacks *bool `json:"-"` // Optional: Controls fallback behavior (nil = true by default)
Provider ModelProvider `json:"-"`
EventID *string `json:"event_id,omitempty"`
Type *string `json:"type,omitempty"`
IsBifrostError bool `json:"is_bifrost_error"`
StatusCode *int `json:"status_code,omitempty"`
Error ErrorField `json:"error"`
AllowFallbacks *bool `json:"-"` // Optional: Controls fallback behavior (nil = true by default)
}

// ErrorField represents detailed error information.
Expand Down
7 changes: 6 additions & 1 deletion tests/transports-integrations/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ The Bifrost integration tests use a centralized configuration system that routes

## 📋 Test Categories

Our test suite covers 11 comprehensive scenarios for each integration:
Our test suite covers 12 comprehensive scenarios for each integration:

1. **Simple Chat** - Basic single-message conversations
2. **Multi-turn Conversation** - Conversation history and context retention
Expand All @@ -47,6 +47,7 @@ Our test suite covers 11 comprehensive scenarios for each integration:
9. **Multiple Images** - Multi-image analysis and comparison
10. **Complex End-to-End** - Comprehensive multimodal workflows
11. **Integration-Specific Features** - Integration-unique capabilities
12. **Error Handling** - Invalid request error processing and propagation

## 📁 Directory Structure

Expand Down Expand Up @@ -179,6 +180,10 @@ python run_integration_tests.py litellm

# Option 3: Using pytest directly
pytest tests/integrations/test_openai.py -v

# Run specific test categories
pytest tests/integrations/ -k "error_handling" -v # Run only error handling tests
pytest tests/integrations/ -k "test_12" -v # Run all 12th test cases (error handling)
```

#### Makefile Commands
Expand Down
19 changes: 19 additions & 0 deletions tests/transports-integrations/tests/integrations/test_anthropic.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,16 @@
MULTIPLE_TOOL_CALL_MESSAGES,
IMAGE_URL,
BASE64_IMAGE,
INVALID_ROLE_MESSAGES,
WEATHER_TOOL,
CALCULATOR_TOOL,
ALL_TOOLS,
Comment on lines +38 to +41
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Remove unused import.

The ALL_TOOLS import is not used in this test file.

-    ALL_TOOLS,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
INVALID_ROLE_MESSAGES,
WEATHER_TOOL,
CALCULATOR_TOOL,
ALL_TOOLS,
INVALID_ROLE_MESSAGES,
WEATHER_TOOL,
CALCULATOR_TOOL,
🧰 Tools
🪛 Ruff (0.11.9)

41-41: ..utils.common.ALL_TOOLS imported but unused

Remove unused import: ..utils.common.ALL_TOOLS

(F401)

🤖 Prompt for AI Agents
In tests/transports-integrations/tests/integrations/test_anthropic.py around
lines 38 to 41, the import ALL_TOOLS is not used anywhere in the file. Remove
the ALL_TOOLS import from the import statement to clean up unused code.

mock_tool_response,
assert_valid_chat_response,
assert_has_tool_calls,
assert_valid_image_response,
assert_valid_error_response,
assert_error_propagation,
extract_tool_calls,
get_api_key,
skip_if_no_api_key,
Expand Down Expand Up @@ -544,6 +548,21 @@ def test_11_integration_specific_features(self, anthropic_client, test_config):
# Should prefer calculator for math question
assert tool_calls[0]["name"] == "calculate"

@skip_if_no_api_key("anthropic")
def test_12_error_handling_invalid_roles(self, anthropic_client, test_config):
"""Test Case 12: Error handling for invalid roles"""
with pytest.raises(Exception) as exc_info:
anthropic_client.messages.create(
model=get_model("anthropic", "chat"),
messages=INVALID_ROLE_MESSAGES,
max_tokens=100,
)

# Verify the error is properly caught and contains role-related information
error = exc_info.value
assert_valid_error_response(error, "tester")
assert_error_propagation(error, "anthropic")

Comment on lines +551 to +565
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Improve error handling test specificity.

The test implementation follows the correct pattern but could be improved:

  1. The exception type is too broad - consider using a more specific exception type
  2. Add proper type annotations for better code quality
-    def test_12_error_handling_invalid_roles(self, anthropic_client, test_config):
+    def test_12_error_handling_invalid_roles(self, anthropic_client, test_config) -> None:
         """Test Case 12: Error handling for invalid roles"""
-        with pytest.raises(Exception) as exc_info:
+        with pytest.raises(Exception, match=r".*(role|invalid|tester).*") as exc_info:

The test correctly validates error propagation and follows the established pattern across other integration tests.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@skip_if_no_api_key("anthropic")
def test_12_error_handling_invalid_roles(self, anthropic_client, test_config):
"""Test Case 12: Error handling for invalid roles"""
with pytest.raises(Exception) as exc_info:
anthropic_client.messages.create(
model=get_model("anthropic", "chat"),
messages=INVALID_ROLE_MESSAGES,
max_tokens=100,
)
# Verify the error is properly caught and contains role-related information
error = exc_info.value
assert_valid_error_response(error, "tester")
assert_error_propagation(error, "anthropic")
@skip_if_no_api_key("anthropic")
def test_12_error_handling_invalid_roles(self, anthropic_client, test_config) -> None:
"""Test Case 12: Error handling for invalid roles"""
with pytest.raises(Exception, match=r".*(role|invalid|tester).*") as exc_info:
anthropic_client.messages.create(
model=get_model("anthropic", "chat"),
messages=INVALID_ROLE_MESSAGES,
max_tokens=100,
)
# Verify the error is properly caught and contains role-related information
error = exc_info.value
assert_valid_error_response(error, "tester")
assert_error_propagation(error, "anthropic")
🧰 Tools
🪛 Ruff (0.11.9)

552-552: Missing return type annotation for public function test_12_error_handling_invalid_roles

Add return type annotation: None

(ANN201)


552-552: Missing type annotation for function argument anthropic_client

(ANN001)


552-552: Missing type annotation for function argument test_config

(ANN001)


552-552: Unused method argument: test_config

(ARG002)


554-554: pytest.raises(Exception) is too broad, set the match parameter or use a more specific exception

(PT011)

🤖 Prompt for AI Agents
In tests/transports-integrations/tests/integrations/test_anthropic.py around
lines 551 to 565, the test_12_error_handling_invalid_roles function uses a broad
Exception type in pytest.raises and lacks type annotations. Update the test to
catch a more specific exception type relevant to invalid roles, such as a custom
error or a more precise built-in exception. Additionally, add appropriate type
annotations to the test method parameters and return type to improve code
clarity and quality.


# Additional helper functions specific to Anthropic
def extract_anthropic_tool_calls(response: Any) -> List[Dict[str, Any]]:
Expand Down
17 changes: 17 additions & 0 deletions tests/transports-integrations/tests/integrations/test_google.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,19 @@
MULTIPLE_TOOL_CALL_MESSAGES,
IMAGE_URL,
BASE64_IMAGE,
INVALID_ROLE_MESSAGES,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Remove unused import.

The INVALID_ROLE_MESSAGES import is not used in this test file since Google GenAI uses GENAI_INVALID_ROLE_CONTENT instead.

-    INVALID_ROLE_MESSAGES,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
INVALID_ROLE_MESSAGES,
from ..utils.common import (
GENAI_INVALID_ROLE_CONTENT,
)
🧰 Tools
🪛 Ruff (0.11.9)

34-34: ..utils.common.INVALID_ROLE_MESSAGES imported but unused

Remove unused import: ..utils.common.INVALID_ROLE_MESSAGES

(F401)

🤖 Prompt for AI Agents
In tests/transports-integrations/tests/integrations/test_google.py at line 34,
remove the import of INVALID_ROLE_MESSAGES since it is not used in this file;
the tests use GENAI_INVALID_ROLE_CONTENT instead. Simply delete the line
importing INVALID_ROLE_MESSAGES to clean up unused imports.

WEATHER_TOOL,
CALCULATOR_TOOL,
assert_valid_chat_response,
assert_valid_image_response,
assert_valid_error_response,
assert_error_propagation,
get_api_key,
skip_if_no_api_key,
COMPARISON_KEYWORDS,
WEATHER_KEYWORDS,
LOCATION_KEYWORDS,
GENAI_INVALID_ROLE_CONTENT,
)
from ..utils.config_loader import get_model

Expand Down Expand Up @@ -422,6 +426,19 @@ def test_11_integration_specific_features(self, google_client, test_config):

assert_valid_chat_response(response3)

@skip_if_no_api_key("google")
def test_12_error_handling_invalid_roles(self, google_client, test_config):
"""Test Case 12: Error handling for invalid roles"""
with pytest.raises(Exception) as exc_info:
google_client.models.generate_content(
model=get_model("google", "chat"), contents=GENAI_INVALID_ROLE_CONTENT
)

# Verify the error is properly caught and contains role-related information
error = exc_info.value
assert_valid_error_response(error, "tester")
assert_error_propagation(error, "google")


# Additional helper functions specific to Google GenAI
def extract_google_function_calls(response: Any) -> List[Dict[str, Any]]:
Expand Down
19 changes: 19 additions & 0 deletions tests/transports-integrations/tests/integrations/test_litellm.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,18 @@
IMAGE_BASE64_MESSAGES,
MULTIPLE_IMAGES_MESSAGES,
COMPLEX_E2E_MESSAGES,
INVALID_ROLE_MESSAGES,
WEATHER_TOOL,
CALCULATOR_TOOL,
mock_tool_response,
assert_valid_chat_response,
assert_has_tool_calls,
assert_valid_image_response,
assert_valid_error_response,
assert_error_propagation,
extract_tool_calls,
get_api_key,
skip_if_no_api_key,
Comment on lines +49 to +50
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Remove unused imports.

The get_api_key and skip_if_no_api_key imports are not used in this test file. Based on the established pattern for LiteLLM tests, these should be removed.

-    get_api_key,
-    skip_if_no_api_key,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
get_api_key,
skip_if_no_api_key,
- get_api_key,
- skip_if_no_api_key,
🧰 Tools
🪛 Ruff (0.11.9)

49-49: ..utils.common.get_api_key imported but unused

Remove unused import

(F401)


50-50: ..utils.common.skip_if_no_api_key imported but unused

Remove unused import

(F401)

🤖 Prompt for AI Agents
In tests/transports-integrations/tests/integrations/test_litellm.py around lines
49 to 50, the imports get_api_key and skip_if_no_api_key are not used anywhere
in the file. Remove these two imports to clean up the code and follow the
established pattern for LiteLLM tests.

COMPARISON_KEYWORDS,
WEATHER_KEYWORDS,
LOCATION_KEYWORDS,
Expand Down Expand Up @@ -339,6 +344,20 @@ def test_11_integration_specific_features(self, test_config):

assert_valid_chat_response(response3)

def test_12_error_handling_invalid_roles(self, test_config):
"""Test Case 12: Error handling for invalid roles"""
with pytest.raises(Exception) as exc_info:
litellm.completion(
model=get_model("litellm", "chat"),
messages=INVALID_ROLE_MESSAGES,
max_tokens=100,
)

# Verify the error is properly caught and contains role-related information
error = exc_info.value
assert_valid_error_response(error, "tester")
assert_error_propagation(error, "litellm")


# Additional helper functions specific to LiteLLM
def extract_litellm_tool_calls(response: Any) -> List[Dict[str, Any]]:
Expand Down
18 changes: 18 additions & 0 deletions tests/transports-integrations/tests/integrations/test_openai.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,15 @@
IMAGE_BASE64_MESSAGES,
MULTIPLE_IMAGES_MESSAGES,
COMPLEX_E2E_MESSAGES,
INVALID_ROLE_MESSAGES,
WEATHER_TOOL,
CALCULATOR_TOOL,
mock_tool_response,
assert_valid_chat_response,
assert_has_tool_calls,
assert_valid_image_response,
assert_valid_error_response,
assert_error_propagation,
extract_tool_calls,
get_api_key,
skip_if_no_api_key,
Expand Down Expand Up @@ -389,3 +392,18 @@ def test_11_integration_specific_features(self, openai_client, test_config):
)

assert_valid_chat_response(response3)

@skip_if_no_api_key("openai")
def test_12_error_handling_invalid_roles(self, openai_client, test_config):
"""Test Case 12: Error handling for invalid roles"""
with pytest.raises(Exception) as exc_info:
openai_client.chat.completions.create(
model=get_model("openai", "chat"),
messages=INVALID_ROLE_MESSAGES,
max_tokens=100,
)

# Verify the error is properly caught and contains role-related information
error = exc_info.value
assert_valid_error_response(error, "tester")
assert_error_propagation(error, "openai")
Loading