Skip to content

WIP: Add fallback support with new configuration, error handling, and metadata#20

Open
chonknick wants to merge 1 commit into
mainfrom
add-fallback-features
Open

WIP: Add fallback support with new configuration, error handling, and metadata#20
chonknick wants to merge 1 commit into
mainfrom
add-fallback-features

Conversation

@chonknick

Copy link
Copy Markdown
Contributor

This pull request adds robust support for fallback models and error handling in the catsu package. It introduces new exception types for configuration and fallback failures, extends the EmbedResponse model to include fallback metadata, and adds comprehensive tests for fallback normalization, credential validation, dimension compatibility, error classification, and the new error types.

Fallback and Error Handling Enhancements:

  • Added new exception classes ConfigurationError and FallbackExhaustedError to catsu.utils.errors, allowing the client to distinguish between configuration issues and cases where all fallback models have failed.
  • Updated src/catsu/__init__.py to export the new exceptions, making them available at the package level.

EmbedResponse Model Improvements:

  • Extended the EmbedResponse model with new fields: requested_model, fallback_used, and fallback_reason, providing detailed metadata when a fallback is triggered. The __repr__ method now includes fallback information when relevant. [1] [2] [3]

Testing and Validation of Fallback Logic:

  • Added comprehensive tests in tests/test_client.py covering:
    • Fallback normalization logic for different input types.
    • Credential validation for primary and fallback models, raising ConfigurationError when API keys are missing.
    • Dimension compatibility filtering for fallback models.
    • Error classification to determine which errors trigger fallbacks.
    • Client configuration for fallback behavior.
    • The new FallbackExhaustedError and ConfigurationError classes.
    • EmbedResponse fallback metadata and representation. [1] [2]

Minor Internal Improvements:

  • Updated type hints in catsu.utils.errors to include List, supporting the new exception classes.

Copilot AI review requested due to automatic review settings December 17, 2025 23:54
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
catsu-docs 22c4109 Commit Preview URL

Branch Preview URL
Dec 17 2025, 11:43 PM

@sentry

sentry Bot commented Dec 17, 2025

Copy link
Copy Markdown

Welcome to Codecov 🎉

Once you merge this PR into your default branch, you're all set! Codecov will compare coverage reports and display results in all future pull requests.

ℹ️ You can also turn on project coverage checks and project coverage reporting on Pull Request comment

Thanks for integrating Codecov - We've got you covered ☂️

Comment thread src/catsu/client.py
if model_dims is not None and not model_info.supports_dimensions:
# Skip this model if it doesn't support requested dimensions
if self.verbose:
print(f"[catsu] Skipping {model}: doesn't support custom dimensions")

Check failure

Code scanning / CodeQL

Clear-text logging of sensitive information High

This expression logs
sensitive data (password)
as clear text.
This expression logs
sensitive data (password)
as clear text.
This expression logs
sensitive data (password)
as clear text.
This expression logs
sensitive data (password)
as clear text.

Copilot Autofix

AI 7 months ago

The problem is the possible logging of sensitive information if, by mistake or malicious design, the model field in the chain dictionaries contains sensitive data (such as an API key). The best fix is to avoid logging this value directly. There are two main approaches:

  1. Sanitize logging: Only log safe, known, non-sensitive values; for example, you might only log model names after validating them.
  2. Remove or redact sensitive fields: Never log the actual value unless it is guaranteed (via whitelist or validation) not to be sensitive.

For this code, the least intrusive, most robust fix is to avoid logging the actual value of model, and instead indicate just the action (e.g., "Skipping model (name redacted): doesn't support custom dimensions"), or, if validation/whitelisting is possible, log only an explicit safe identifier.

Changes needed:

  • In src/catsu/client.py, in the log at line 510, replace print(f"[catsu] Skipping {model}: doesn't support custom dimensions") with a version that does not include the possibly-tainted variable. For example: print("[catsu] Skipping a model: doesn't support custom dimensions") or, preferably, log a sanitized model name (if you can be sure you have one; otherwise, just do not log it).
  • No imports or additional dependencies are needed.

Suggested changeset 1
src/catsu/client.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/catsu/client.py b/src/catsu/client.py
--- a/src/catsu/client.py
+++ b/src/catsu/client.py
@@ -507,7 +507,7 @@
                     if model_dims is not None and not model_info.supports_dimensions:
                         # Skip this model if it doesn't support requested dimensions
                         if self.verbose:
-                            print(f"[catsu] Skipping {model}: doesn't support custom dimensions")
+                            print("[catsu] Skipping a model due to unsupported custom dimensions")
                         continue
 
                     model_input_type = input_type
EOF
@@ -507,7 +507,7 @@
if model_dims is not None and not model_info.supports_dimensions:
# Skip this model if it doesn't support requested dimensions
if self.verbose:
print(f"[catsu] Skipping {model}: doesn't support custom dimensions")
print("[catsu] Skipping a model due to unsupported custom dimensions")
continue

model_input_type = input_type
Copilot is powered by AI and may make mistakes. Always verify output.
Comment thread src/catsu/client.py
)

if self.verbose and model != primary_model:
print(f"[catsu] Fallback succeeded with {model}")

Check failure

Code scanning / CodeQL

Clear-text logging of sensitive information High

This expression logs
sensitive data (password)
as clear text.
This expression logs
sensitive data (password)
as clear text.
This expression logs
sensitive data (password)
as clear text.
This expression logs
sensitive data (password)
as clear text.

Copilot Autofix

AI 7 months ago

The best way to fix this problem is to ensure that no potentially sensitive data is logged, even indirectly. In this case, the best fix is to avoid printing the full value of model. Instead, log only a safe representation of the model identifier (for example, by showing its index in the chain, a truncated, or obfuscated version, or just a generic success message without interpolating external input). This should be done by editing line 539 in the _embed_with_fallbacks method (and symmetrically in any similar logging in corresponding async methods or related functions).

Steps:

  • In the relevant block, replace print(f"[catsu] Fallback succeeded with {model}") with a variant that does not include the model value, or which only logs a known-safe representation.
  • No new imports are required for simple redaction or generic messages.
  • Since only the block shown is to be edited, avoid any changes to other files or regions.

Suggested changeset 1
src/catsu/client.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/catsu/client.py b/src/catsu/client.py
--- a/src/catsu/client.py
+++ b/src/catsu/client.py
@@ -536,7 +536,7 @@
                         )
 
                     if self.verbose and model != primary_model:
-                        print(f"[catsu] Fallback succeeded with {model}")
+                        print("[catsu] Fallback succeeded with secondary model.")
 
                     return response
 
EOF
@@ -536,7 +536,7 @@
)

if self.verbose and model != primary_model:
print(f"[catsu] Fallback succeeded with {model}")
print("[catsu] Fallback succeeded with secondary model.")

return response

Copilot is powered by AI and may make mistakes. Always verify output.
Comment thread src/catsu/client.py
except Exception as e:
errors[model] = e
if self.verbose:
print(f"[catsu] Cycle {cycle}: {model} failed: {e}")

Check failure

Code scanning / CodeQL

Clear-text logging of sensitive information High

This expression logs
sensitive data (password)
as clear text.
This expression logs
sensitive data (password)
as clear text.
This expression logs
sensitive data (password)
as clear text.
This expression logs
sensitive data (password)
as clear text.

Copilot Autofix

AI 7 months ago

To fix this problem, the code should ensure sensitive information (such as api_key and anything that could encode it) is never logged directly. This involves:

  • Avoid logging the entire config or model string if it may contain sensitive data.
  • When logging, only include non-sensitive identifiers (e.g., model names, cycle numbers), and explicitly avoid interpolating the entire config or exception message if they are not guaranteed to be free of secrets.
  • Modify the log statement at line 546 to output only safe, non-sensitive details. Instead of including model (which may be tainted) or e (the full exception, which may embed sensitive text), only log, for example, the position in the fallback list and the error type, or a sanitized error message.
  • The same principle applies to any similar surrounding log statements.

Required changes:

  • In src/catsu/client.py, refactor the print statement at line 546 to avoid directly logging user-tainted values. Only print the error type and, if useful, a safe identifier for the model, or redact as appropriate.
  • Add a redaction helper if needed, unless simply omitting the field is best.

Suggested changeset 1
src/catsu/client.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/catsu/client.py b/src/catsu/client.py
--- a/src/catsu/client.py
+++ b/src/catsu/client.py
@@ -543,7 +543,7 @@
                 except Exception as e:
                     errors[model] = e
                     if self.verbose:
-                        print(f"[catsu] Cycle {cycle}: {model} failed: {e}")
+                        print(f"[catsu] Cycle {cycle}: model fallback failed with error: {type(e).__name__}")
 
                     # Check if this error should trigger fallback
                     if not self._is_fallback_error(e):
EOF
@@ -543,7 +543,7 @@
except Exception as e:
errors[model] = e
if self.verbose:
print(f"[catsu] Cycle {cycle}: {model} failed: {e}")
print(f"[catsu] Cycle {cycle}: model fallback failed with error: {type(e).__name__}")

# Check if this error should trigger fallback
if not self._is_fallback_error(e):
Copilot is powered by AI and may make mistakes. Always verify output.
Comment thread src/catsu/client.py
model_dims = model_kwargs.get("dimensions")
if model_dims is not None and not model_info.supports_dimensions:
if self.verbose:
print(f"[catsu] Skipping {model}: doesn't support custom dimensions")

Check failure

Code scanning / CodeQL

Clear-text logging of sensitive information High

This expression logs
sensitive data (password)
as clear text.
This expression logs
sensitive data (password)
as clear text.
This expression logs
sensitive data (password)
as clear text.
This expression logs
sensitive data (password)
as clear text.

Copilot Autofix

AI 7 months ago

To fix this problem, we should ensure that no sensitive information or tainted data (such as api_key or model possibly containing sensitive info) is ever logged in clear text. The robust approach is to sanitize any values interpolated into logs and, where possible, log only safe, non-sensitive identifiers. For these logs, it is best to log only a whitelisted set of values or explicitly redact any value that could contain sensitive data.

  • Specifically, replace logging of the raw model variable with a redacted or validated safe identifier. One safe strategy is to log only the model's name after parsing, or, if not possible, use a constant string or placeholder such as "[REDACTED]" or "[MODEL_ID]" instead.
  • If there's a function for extracting just the safe model name (model_name from self._parse_model_string(model)), use that for logging.
  • All such logs in the function should be similarly sanitized.
  • Only changes within the shown snippet in src/catsu/client.py are permitted; external helpers or changing callsites are not.
Suggested changeset 1
src/catsu/client.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/catsu/client.py b/src/catsu/client.py
--- a/src/catsu/client.py
+++ b/src/catsu/client.py
@@ -626,7 +626,12 @@
                     model_dims = model_kwargs.get("dimensions")
                     if model_dims is not None and not model_info.supports_dimensions:
                         if self.verbose:
-                            print(f"[catsu] Skipping {model}: doesn't support custom dimensions")
+                            # Safely log only the sanitized model identifier, never user-supplied data
+                            try:
+                                provider_id, safe_model_id = self._parse_model_string(model)
+                                print(f"[catsu] Skipping model ({provider_id}/{safe_model_id}): doesn't support custom dimensions")
+                            except Exception:
+                                print("[catsu] Skipping an unsafe or invalid model identifier: doesn't support custom dimensions")
                         continue
 
                     model_input_type = input_type
@@ -654,14 +659,24 @@
                         )
 
                     if self.verbose and model != primary_model:
-                        print(f"[catsu] Fallback succeeded with {model}")
+                        # Safely log only the sanitized model identifier
+                        try:
+                            provider_id, safe_model_id = self._parse_model_string(model)
+                            print(f"[catsu] Fallback succeeded with model ({provider_id}/{safe_model_id})")
+                        except Exception:
+                            print("[catsu] Fallback succeeded with an unsafe or invalid model identifier")
 
                     return response
 
                 except Exception as e:
                     errors[model] = e
                     if self.verbose:
-                        print(f"[catsu] Cycle {cycle}: {model} failed: {e}")
+                        # Safely log only the sanitized model identifier
+                        try:
+                            provider_id, safe_model_id = self._parse_model_string(model)
+                            print(f"[catsu] Cycle {cycle}: model ({provider_id}/{safe_model_id}) failed: {e}")
+                        except Exception:
+                            print(f"[catsu] Cycle {cycle}: unsafe or invalid model identifier failed: {e}")
 
                     if not self._is_fallback_error(e):
                         raise
EOF
@@ -626,7 +626,12 @@
model_dims = model_kwargs.get("dimensions")
if model_dims is not None and not model_info.supports_dimensions:
if self.verbose:
print(f"[catsu] Skipping {model}: doesn't support custom dimensions")
# Safely log only the sanitized model identifier, never user-supplied data
try:
provider_id, safe_model_id = self._parse_model_string(model)
print(f"[catsu] Skipping model ({provider_id}/{safe_model_id}): doesn't support custom dimensions")
except Exception:
print("[catsu] Skipping an unsafe or invalid model identifier: doesn't support custom dimensions")
continue

model_input_type = input_type
@@ -654,14 +659,24 @@
)

if self.verbose and model != primary_model:
print(f"[catsu] Fallback succeeded with {model}")
# Safely log only the sanitized model identifier
try:
provider_id, safe_model_id = self._parse_model_string(model)
print(f"[catsu] Fallback succeeded with model ({provider_id}/{safe_model_id})")
except Exception:
print("[catsu] Fallback succeeded with an unsafe or invalid model identifier")

return response

except Exception as e:
errors[model] = e
if self.verbose:
print(f"[catsu] Cycle {cycle}: {model} failed: {e}")
# Safely log only the sanitized model identifier
try:
provider_id, safe_model_id = self._parse_model_string(model)
print(f"[catsu] Cycle {cycle}: model ({provider_id}/{safe_model_id}) failed: {e}")
except Exception:
print(f"[catsu] Cycle {cycle}: unsafe or invalid model identifier failed: {e}")

if not self._is_fallback_error(e):
raise
Copilot is powered by AI and may make mistakes. Always verify output.
Comment thread src/catsu/client.py
)

if self.verbose and model != primary_model:
print(f"[catsu] Fallback succeeded with {model}")

Check failure

Code scanning / CodeQL

Clear-text logging of sensitive information High

This expression logs
sensitive data (password)
as clear text.
This expression logs
sensitive data (password)
as clear text.
This expression logs
sensitive data (password)
as clear text.
This expression logs
sensitive data (password)
as clear text.

Copilot Autofix

AI 7 months ago

To fix the problem, ensure that the log message at line 657 does not reveal sensitive data. This includes not printing the raw model string if there is any chance it could contain secrets. Instead, sanitize or redact it before logging, or log only safe, non-sensitive identifiers/metadata. The safest fix is to avoid logging the raw model identifier entirely or to log only the provider and a redacted identifier if required for debugging (but never the API key or any secret value). The required code change is limited to the logging line(s) at or near line 657 in src/catsu/client.py. No extra imports are needed; we can refactor the log statement to show only generic metadata or an explicit [REDACTED] marker.


Suggested changeset 1
src/catsu/client.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/catsu/client.py b/src/catsu/client.py
--- a/src/catsu/client.py
+++ b/src/catsu/client.py
@@ -654,7 +654,8 @@
                         )
 
                     if self.verbose and model != primary_model:
-                        print(f"[catsu] Fallback succeeded with {model}")
+                        # Do not log potentially sensitive model identifiers or secrets.
+                        print("[catsu] Fallback succeeded with alternative model.")
 
                     return response
 
EOF
@@ -654,7 +654,8 @@
)

if self.verbose and model != primary_model:
print(f"[catsu] Fallback succeeded with {model}")
# Do not log potentially sensitive model identifiers or secrets.
print("[catsu] Fallback succeeded with alternative model.")

return response

Copilot is powered by AI and may make mistakes. Always verify output.
Comment thread src/catsu/client.py
except Exception as e:
errors[model] = e
if self.verbose:
print(f"[catsu] Cycle {cycle}: {model} failed: {e}")

Check failure

Code scanning / CodeQL

Clear-text logging of sensitive information High

This expression logs
sensitive data (password)
as clear text.
This expression logs
sensitive data (password)
as clear text.
This expression logs
sensitive data (password)
as clear text.
This expression logs
sensitive data (password)
as clear text.

Copilot Autofix

AI 7 months ago

To fix this problem, we should ensure that no sensitive information, such as API keys or passwords contained in the config dictionary, is ever printed to logs. Specifically, when logging model (which could be tainted), we should avoid logging full config details or any data that could come from sensitive input sources. The best way to do this is to only print the model name, after extracting it explicitly, and ensure that it is not the API key.

We must update the logging at line 664 and any similar log lines to ensure that only safe, non-sensitive details are logged. If additional info about the error is needed, the exception class name (type(e).__name__) can be logged, but not the potentially sensitive message (e).

  • Update any print/log lines logging potentially tainted values (e.g., config, model, e) so that only non-sensitive data is logged.
  • Optionally, we can explicitly redact or mask values when necessary.
  • No external library imports are needed, as only string construction and filtering are required.
  • Changes are limited to src/catsu/client.py, specifically the lines that contain print/log statements in the if self.verbose: and similar error blocks in _aembed_with_fallbacks (and any related areas that match the same pattern, if present).

Suggested changeset 1
src/catsu/client.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/catsu/client.py b/src/catsu/client.py
--- a/src/catsu/client.py
+++ b/src/catsu/client.py
@@ -661,7 +661,7 @@
                 except Exception as e:
                     errors[model] = e
                     if self.verbose:
-                        print(f"[catsu] Cycle {cycle}: {model} failed: {e}")
+                        print(f"[catsu] Cycle {cycle}: Model '{model}' failed with error type: {type(e).__name__}")
 
                     if not self._is_fallback_error(e):
                         raise
EOF
@@ -661,7 +661,7 @@
except Exception as e:
errors[model] = e
if self.verbose:
print(f"[catsu] Cycle {cycle}: {model} failed: {e}")
print(f"[catsu] Cycle {cycle}: Model '{model}' failed with error type: {type(e).__name__}")

if not self._is_fallback_error(e):
raise
Copilot is powered by AI and may make mistakes. Always verify output.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces comprehensive fallback support for embedding models, allowing automatic failover when primary models encounter transient errors. The implementation adds new exception types for configuration and exhaustion scenarios, extends the EmbedResponse model to track fallback metadata, and includes extensive test coverage for the fallback logic.

Key Changes:

  • New exception classes (ConfigurationError, FallbackExhaustedError) for distinguishing between configuration issues and exhausted fallback attempts
  • Enhanced EmbedResponse model with fallback metadata fields (requested_model, fallback_used, fallback_reason)
  • Client-level and request-level fallback configuration with dimension compatibility filtering and round-robin retry logic

Reviewed changes

Copilot reviewed 5 out of 6 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
tests/test_client.py Comprehensive test suite covering fallback normalization, credential validation, dimension filtering, error classification, client configuration, new exception types, and response metadata
src/catsu/utils/errors.py New ConfigurationError and FallbackExhaustedError exception classes with detailed attributes and messaging
src/catsu/models.py Extended EmbedResponse with fallback metadata fields and updated __repr__ to include fallback information
src/catsu/client.py Core fallback implementation including helper methods, orchestration logic, and updated public API with fallback parameters
src/catsu/__init__.py Export new exception classes at package level for public API access

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread tests/test_client.py
error = FallbackExhaustedError(
primary_model="voyage-3",
fallbacks=["model-a", "model-b"],
errors={"voyage-3": RateLimitError("Rate limited")},

Copilot AI Dec 17, 2025

Copy link

Choose a reason for hiding this comment

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

The RateLimitError constructor requires a provider parameter, but none is provided here. This will cause the test to fail with a TypeError.

Suggested change
errors={"voyage-3": RateLimitError("Rate limited")},
errors={"voyage-3": RateLimitError("Rate limited", provider="test")},

Copilot uses AI. Check for mistakes.
Comment thread src/catsu/utils/errors.py
def __init__(
self,
primary_model: str,
fallbacks: List[str],

Copilot AI Dec 17, 2025

Copy link

Choose a reason for hiding this comment

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

The parameter documentation describes 'fallbacks' as 'List of fallback model names that were tried', but the type hint is List[str]. Consider clarifying in the docstring that these are model name strings (e.g., 'voyage-3' or 'provider:model').

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants