mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-09 03:18:44 +00:00
fix(cli): make CLI output ASCII-only so it doesn't crash legacy Windows consoles (#33465)
* fix(cli): force UTF-8 output so emoji don't crash the CLI on Windows Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(cli): drop dead flush calls flagged by review * fix(cli): replace non-ASCII CLI output with ASCII so legacy Windows consoles don't crash --------- Co-authored-by: ryan <ryan@berri.ai> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
edc30ea515
commit
260d1eae8e
9 changed files with 86 additions and 73 deletions
|
|
@ -72,7 +72,7 @@ def display_teams_table(teams: List[Dict[str, Any]]) -> None:
|
|||
console = Console()
|
||||
|
||||
if not teams:
|
||||
console.print("❌ No teams found for your user.")
|
||||
console.print("No teams found for your user.")
|
||||
return
|
||||
|
||||
table = Table(title="Available Teams")
|
||||
|
|
@ -162,7 +162,7 @@ def display_interactive_team_selection(teams: List[Dict[str, Any]], selected_ind
|
|||
# Clear the screen using Rich's method
|
||||
console.clear()
|
||||
|
||||
console.print("🎯 Select a Team (Use ↑↓ arrows, Enter to select, 'q' to skip):\n")
|
||||
console.print("Select a Team (Use up/down arrows, Enter to select, 'q' to skip):\n")
|
||||
|
||||
for i, team in enumerate(teams):
|
||||
team_alias = team.get("team_alias") or "N/A"
|
||||
|
|
@ -184,7 +184,7 @@ def display_interactive_team_selection(teams: List[Dict[str, Any]], selected_ind
|
|||
|
||||
# Highlight the selected item
|
||||
if i == selected_index:
|
||||
console.print(f"➤ [bold cyan]{team_alias}[/bold cyan] ({team_id})")
|
||||
console.print(f"> [bold cyan]{team_alias}[/bold cyan] ({team_id})")
|
||||
console.print(f" Models: [yellow]{models_str}[/yellow]")
|
||||
console.print(f" Budget: [blue]{budget_str}[/blue]\n")
|
||||
else:
|
||||
|
|
@ -220,15 +220,13 @@ def prompt_team_selection(teams: List[Dict[str, Any]]) -> Optional[Dict[str, Any
|
|||
# Clear screen and show selection
|
||||
console = Console()
|
||||
console.clear()
|
||||
click.echo(
|
||||
f"✅ Selected team: {selected_team.get('team_alias', 'N/A')} ({selected_team.get('team_id')})"
|
||||
)
|
||||
click.echo(f"Selected team: {selected_team.get('team_alias', 'N/A')} ({selected_team.get('team_id')})")
|
||||
return selected_team
|
||||
elif key == "quit" or key == "escape":
|
||||
# Clear screen
|
||||
console = Console()
|
||||
console.clear()
|
||||
click.echo("ℹ️ Team selection skipped.")
|
||||
click.echo("Team selection skipped.")
|
||||
return None
|
||||
elif key is None:
|
||||
# If we can't get key input, fall back to simple selection
|
||||
|
|
@ -237,7 +235,7 @@ def prompt_team_selection(teams: List[Dict[str, Any]]) -> Optional[Dict[str, Any
|
|||
except KeyboardInterrupt:
|
||||
console = Console()
|
||||
console.clear()
|
||||
click.echo("\n❌ Team selection cancelled.")
|
||||
click.echo("\nTeam selection cancelled.")
|
||||
return None
|
||||
except Exception:
|
||||
# If interactive mode fails, fall back to simple selection
|
||||
|
|
@ -265,15 +263,15 @@ def prompt_team_selection_fallback(
|
|||
if 0 <= index < len(teams):
|
||||
selected_team = teams[index]
|
||||
click.echo(
|
||||
f"\n✅ Selected team: {selected_team.get('team_alias', 'N/A')} ({selected_team.get('team_id')})"
|
||||
f"\nSelected team: {selected_team.get('team_alias', 'N/A')} ({selected_team.get('team_id')})"
|
||||
)
|
||||
return selected_team
|
||||
else:
|
||||
click.echo(f"❌ Invalid selection. Please enter a number between 1 and {len(teams)}")
|
||||
click.echo(f"Invalid selection. Please enter a number between 1 and {len(teams)}")
|
||||
except ValueError:
|
||||
click.echo("❌ Invalid input. Please enter a number or 'skip'")
|
||||
click.echo("Invalid input. Please enter a number or 'skip'")
|
||||
except KeyboardInterrupt:
|
||||
click.echo("\n❌ Team selection cancelled.")
|
||||
click.echo("\nTeam selection cancelled.")
|
||||
return None
|
||||
|
||||
|
||||
|
|
@ -437,7 +435,7 @@ def _poll_for_authentication(base_url: str, key_id: str, poll_secret: str) -> Op
|
|||
user_id = data.get("user_id")
|
||||
normalized_teams: List[Dict[str, Any]] = _normalize_teams(teams, team_details)
|
||||
if not normalized_teams:
|
||||
click.echo("⚠️ No teams available for selection.")
|
||||
click.echo("Warning: No teams available for selection.")
|
||||
return None
|
||||
|
||||
# User has multiple teams - let them select
|
||||
|
|
@ -457,7 +455,7 @@ def _poll_for_authentication(base_url: str, key_id: str, poll_secret: str) -> Op
|
|||
"team_id": None, # Set by server in JWT
|
||||
}
|
||||
|
||||
click.echo("❌ Team selection cancelled or JWT generation failed.")
|
||||
click.echo("Team selection cancelled or JWT generation failed.")
|
||||
return None
|
||||
|
||||
# JWT is ready (single team or team already selected)
|
||||
|
|
@ -468,7 +466,7 @@ def _poll_for_authentication(base_url: str, key_id: str, poll_secret: str) -> Op
|
|||
|
||||
# Show which team was assigned
|
||||
if team_id and len(teams) == 1:
|
||||
click.echo(f"\n✅ Automatically assigned to team: {team_id}")
|
||||
click.echo(f"\nAutomatically assigned to team: {team_id}")
|
||||
|
||||
if api_key:
|
||||
return {
|
||||
|
|
@ -494,19 +492,19 @@ def _handle_team_selection_during_polling(
|
|||
The JWT token with the selected team, or None if selection was skipped
|
||||
"""
|
||||
if not teams:
|
||||
click.echo("ℹ️ No teams found. You can create or join teams using the web interface.")
|
||||
click.echo("No teams found. You can create or join teams using the web interface.")
|
||||
return None
|
||||
|
||||
click.echo("\n" + "=" * 60)
|
||||
click.echo("📋 Select a team for your CLI session...")
|
||||
click.echo("Select a team for your CLI session...")
|
||||
|
||||
team_id = _render_and_prompt_for_team_selection(teams)
|
||||
|
||||
if not team_id:
|
||||
click.echo("ℹ️ No team selected.")
|
||||
click.echo("No team selected.")
|
||||
return None
|
||||
|
||||
click.echo(f"\n🔄 Generating JWT for team: {team_id}")
|
||||
click.echo(f"\nGenerating JWT for team: {team_id}")
|
||||
|
||||
poll_url = f"{base_url}/sso/cli/poll/{key_id}?team_id={team_id}"
|
||||
data = _poll_for_ready_data(
|
||||
|
|
@ -520,7 +518,7 @@ def _handle_team_selection_during_polling(
|
|||
return None
|
||||
jwt_token = data.get("key")
|
||||
if jwt_token:
|
||||
click.echo(f"✅ Successfully generated JWT for team: {team_id}")
|
||||
click.echo(f"Successfully generated JWT for team: {team_id}")
|
||||
return jwt_token
|
||||
|
||||
return None
|
||||
|
|
@ -568,14 +566,14 @@ def _render_and_prompt_for_team_selection(teams: List[Dict[str, Any]]) -> Option
|
|||
selected_team = teams[index]
|
||||
team_id = str(selected_team.get("team_id"))
|
||||
team_alias = selected_team.get("team_alias") or team_id
|
||||
click.echo(f"\n✅ Selected team: {team_alias} ({team_id})")
|
||||
click.echo(f"\nSelected team: {team_alias} ({team_id})")
|
||||
return team_id
|
||||
|
||||
click.echo(f"❌ Invalid selection. Please enter a number between 1 and {len(teams)}")
|
||||
click.echo(f"Invalid selection. Please enter a number between 1 and {len(teams)}")
|
||||
except ValueError:
|
||||
click.echo("❌ Invalid input. Please enter a number or 'skip'")
|
||||
click.echo("Invalid input. Please enter a number or 'skip'")
|
||||
except KeyboardInterrupt:
|
||||
click.echo("\n❌ Team selection cancelled.")
|
||||
click.echo("\nTeam selection cancelled.")
|
||||
return None
|
||||
|
||||
|
||||
|
|
@ -628,7 +626,7 @@ def login(ctx: click.Context):
|
|||
}
|
||||
)
|
||||
|
||||
click.echo("\n✅ Login successful!")
|
||||
click.echo("\nLogin successful!")
|
||||
click.echo(f"JWT Token: {api_key[:20]}...")
|
||||
click.echo("You can now use the CLI without specifying --api-key")
|
||||
|
||||
|
|
@ -637,7 +635,7 @@ def login(ctx: click.Context):
|
|||
show_commands()
|
||||
return
|
||||
else:
|
||||
click.echo("❌ Authentication timed out. Please try again.")
|
||||
click.echo("Authentication timed out. Please try again.")
|
||||
click.echo(
|
||||
"The proxy never reported the browser sign-in as finished. If you did complete it, "
|
||||
"check the proxy logs for /sso/callback errors and confirm SSO is configured on the proxy."
|
||||
|
|
@ -645,10 +643,10 @@ def login(ctx: click.Context):
|
|||
return
|
||||
|
||||
except KeyboardInterrupt:
|
||||
click.echo("\n❌ Authentication cancelled by user.")
|
||||
click.echo("\nAuthentication cancelled by user.")
|
||||
return
|
||||
except Exception as e:
|
||||
click.echo(f"❌ Authentication failed: {e}")
|
||||
click.echo(f"Authentication failed: {e}")
|
||||
return
|
||||
|
||||
|
||||
|
|
@ -656,7 +654,7 @@ def login(ctx: click.Context):
|
|||
def logout():
|
||||
"""Logout and clear stored authentication"""
|
||||
clear_token()
|
||||
click.echo("✅ Logged out successfully. Authentication token cleared.")
|
||||
click.echo("Logged out successfully. Authentication token cleared.")
|
||||
|
||||
|
||||
@click.command(name="print-token")
|
||||
|
|
@ -703,10 +701,10 @@ def whoami():
|
|||
token_data = load_token()
|
||||
|
||||
if not token_data:
|
||||
click.echo("❌ Not authenticated. Run 'lite login' to authenticate.")
|
||||
click.echo("Not authenticated. Run 'lite login' to authenticate.")
|
||||
return
|
||||
|
||||
click.echo("✅ Authenticated")
|
||||
click.echo("Authenticated")
|
||||
click.echo(f"User Email: {token_data.get('user_email', 'Unknown')}")
|
||||
click.echo(f"User ID: {token_data.get('user_id', 'Unknown')}")
|
||||
click.echo(f"User Role: {token_data.get('user_role', 'Unknown')}")
|
||||
|
|
@ -717,7 +715,7 @@ def whoami():
|
|||
click.echo(f"Token age: {age_hours:.1f} hours")
|
||||
|
||||
if age_hours > CLI_JWT_EXPIRATION_HOURS:
|
||||
click.echo(f"⚠️ Warning: Token is more than {CLI_JWT_EXPIRATION_HOURS} hours old and may have expired.")
|
||||
click.echo(f"Warning: Token is more than {CLI_JWT_EXPIRATION_HOURS} hours old and may have expired.")
|
||||
|
||||
|
||||
@click.group(name="auth")
|
||||
|
|
|
|||
|
|
@ -150,7 +150,7 @@ def chat(
|
|||
f"Max Tokens: [yellow]{max_tokens or 'unlimited'}[/yellow]\n\n"
|
||||
f"Type your messages and press Enter. Type '/quit' or '/exit' to end the session.\n"
|
||||
f"Type '/help' for more commands.",
|
||||
title="🤖 Chat Session",
|
||||
title="Chat Session",
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ def migrate(ctx: click.Context, check_only: bool, dry_run: bool):
|
|||
|
||||
Requires the proxy to be started with
|
||||
``general_settings.encryption_algorithm: aes-256-gcm``. Idempotent and
|
||||
resumable — safe to re-run after an interruption.
|
||||
resumable; safe to re-run after an interruption.
|
||||
|
||||
Examples:
|
||||
litellm-proxy encryption migrate --check # attestation scan, no writes
|
||||
|
|
|
|||
|
|
@ -309,12 +309,12 @@ def _import_keys_to_destination(
|
|||
imported_count += 1
|
||||
|
||||
key_alias = key.get("key_alias", "N/A")
|
||||
click.echo(f"✓ Imported key: {key_alias}")
|
||||
click.echo(f"Imported key: {key_alias}")
|
||||
|
||||
except Exception as e:
|
||||
failed_count += 1
|
||||
key_alias = key.get("key_alias", "N/A")
|
||||
click.echo(f"✗ Failed to import key {key_alias}: {str(e)}", err=True)
|
||||
click.echo(f"Failed to import key {key_alias}: {str(e)}", err=True)
|
||||
|
||||
return imported_count, failed_count
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ def display_teams_table(teams: List[Dict[str, Any]]) -> None:
|
|||
console = Console()
|
||||
|
||||
if not teams:
|
||||
console.print("❌ No teams found for your user.")
|
||||
console.print("No teams found for your user.")
|
||||
return
|
||||
|
||||
table = Table(title="Available Teams")
|
||||
|
|
@ -91,10 +91,10 @@ def available(ctx: click.Context):
|
|||
teams = client.teams.get_available()
|
||||
if teams:
|
||||
console = Console()
|
||||
console.print("\n🎯 Available Teams to Join:")
|
||||
console.print("\nAvailable Teams to Join:")
|
||||
display_teams_table(teams)
|
||||
else:
|
||||
click.echo("ℹ️ No available teams to join.")
|
||||
click.echo("No available teams to join.")
|
||||
except requests.exceptions.HTTPError as e:
|
||||
click.echo(f"Error: HTTP {e.response.status_code}", err=True)
|
||||
error_body = e.response.json()
|
||||
|
|
@ -113,7 +113,7 @@ def assign_key(ctx: click.Context, team_id: Optional[str]):
|
|||
api_key = ctx.obj["api_key"]
|
||||
|
||||
if not api_key:
|
||||
click.echo("❌ No API key found. Please login first using 'litellm login'")
|
||||
click.echo("No API key found. Please login first using 'litellm login'")
|
||||
raise click.Abort()
|
||||
|
||||
try:
|
||||
|
|
@ -122,7 +122,7 @@ def assign_key(ctx: click.Context, team_id: Optional[str]):
|
|||
teams = client.teams.list()
|
||||
|
||||
if not teams:
|
||||
click.echo("❌ No teams found for your user.")
|
||||
click.echo("No teams found for your user.")
|
||||
return
|
||||
|
||||
# Use interactive selection from auth module
|
||||
|
|
@ -133,14 +133,14 @@ def assign_key(ctx: click.Context, team_id: Optional[str]):
|
|||
if selected_team:
|
||||
team_id = selected_team.get("team_id")
|
||||
else:
|
||||
click.echo("❌ Operation cancelled.")
|
||||
click.echo("Operation cancelled.")
|
||||
return
|
||||
|
||||
# Update the key with the selected team
|
||||
if team_id:
|
||||
click.echo(f"\n🔄 Assigning your key to team: {team_id}")
|
||||
click.echo(f"\nAssigning your key to team: {team_id}")
|
||||
client.keys.update(key=api_key, team_id=team_id)
|
||||
click.echo(f"✅ Successfully assigned key to team: {team_id}")
|
||||
click.echo(f"Successfully assigned key to team: {team_id}")
|
||||
|
||||
# Show team details if available
|
||||
teams = client.teams.list()
|
||||
|
|
@ -148,9 +148,9 @@ def assign_key(ctx: click.Context, team_id: Optional[str]):
|
|||
if team.get("team_id") == team_id:
|
||||
models = team.get("models", [])
|
||||
if models:
|
||||
click.echo(f"🎯 You can now access models: {', '.join(models)}")
|
||||
click.echo(f"You can now access models: {', '.join(models)}")
|
||||
else:
|
||||
click.echo("🎯 You can now access all available models")
|
||||
click.echo("You can now access all available models")
|
||||
break
|
||||
|
||||
except requests.exceptions.HTTPError as e:
|
||||
|
|
|
|||
|
|
@ -27,13 +27,13 @@ def styled_prompt():
|
|||
verbose_logger.debug(f"Error getting terminal size: {e}")
|
||||
click.echo("\n" * 3)
|
||||
|
||||
# Unicode box drawing characters
|
||||
top_left = "┌"
|
||||
top_right = "┐"
|
||||
bottom_left = "└"
|
||||
bottom_right = "┘"
|
||||
horizontal = "─"
|
||||
vertical = "│"
|
||||
# ASCII box drawing characters
|
||||
top_left = "+"
|
||||
top_right = "+"
|
||||
bottom_left = "+"
|
||||
bottom_right = "+"
|
||||
horizontal = "-"
|
||||
vertical = "|"
|
||||
|
||||
# Create the box with increased width
|
||||
width = 80
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ class TestPollingErrorSurfacing:
|
|||
result = CliRunner().invoke(login, obj=mock_context.obj)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "❌ Authentication failed:" in result.output
|
||||
assert "Authentication failed:" in result.output
|
||||
assert "CLI login session not found or expired." in result.output
|
||||
assert "Authentication timed out" not in result.output
|
||||
|
||||
|
|
@ -414,7 +414,7 @@ class TestLoginCommand:
|
|||
result = self.runner.invoke(login, obj=mock_context.obj)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "✅ Login successful!" in result.output
|
||||
assert "Login successful!" in result.output
|
||||
assert "Automatically assigned to team: team-1" in result.output
|
||||
|
||||
# Verify browser was opened with correct URL
|
||||
|
|
@ -456,7 +456,7 @@ class TestLoginCommand:
|
|||
result = self.runner.invoke(login, obj=mock_context.obj)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "❌ Authentication timed out" in result.output
|
||||
assert "Authentication timed out" in result.output
|
||||
|
||||
def test_login_http_error(self):
|
||||
"""Test login with HTTP error"""
|
||||
|
|
@ -476,7 +476,7 @@ class TestLoginCommand:
|
|||
result = self.runner.invoke(login, obj=mock_context.obj)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "❌ Authentication timed out" in result.output
|
||||
assert "Authentication timed out" in result.output
|
||||
|
||||
def test_login_request_exception(self):
|
||||
"""Test login with request exception"""
|
||||
|
|
@ -497,7 +497,7 @@ class TestLoginCommand:
|
|||
result = self.runner.invoke(login, obj=mock_context.obj)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "❌ Authentication timed out" in result.output
|
||||
assert "Authentication timed out" in result.output
|
||||
|
||||
def test_login_keyboard_interrupt(self):
|
||||
"""Test login cancelled by user"""
|
||||
|
|
@ -512,7 +512,7 @@ class TestLoginCommand:
|
|||
result = self.runner.invoke(login, obj=mock_context.obj)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "❌ Authentication cancelled by user" in result.output
|
||||
assert "Authentication cancelled by user" in result.output
|
||||
|
||||
def test_login_no_api_key_in_response(self):
|
||||
"""Test login when response doesn't contain API key"""
|
||||
|
|
@ -536,7 +536,7 @@ class TestLoginCommand:
|
|||
result = self.runner.invoke(login, obj=mock_context.obj)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "❌ Authentication timed out" in result.output
|
||||
assert "Authentication timed out" in result.output
|
||||
|
||||
def test_login_general_exception(self):
|
||||
"""Test login with general exception (not requests exception)"""
|
||||
|
|
@ -551,7 +551,7 @@ class TestLoginCommand:
|
|||
result = self.runner.invoke(login, obj=mock_context.obj)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "❌ Authentication failed: Invalid value" in result.output
|
||||
assert "Authentication failed: Invalid value" in result.output
|
||||
|
||||
|
||||
class TestLogoutCommand:
|
||||
|
|
@ -567,7 +567,7 @@ class TestLogoutCommand:
|
|||
result = self.runner.invoke(logout)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "✅ Logged out successfully" in result.output
|
||||
assert "Logged out successfully" in result.output
|
||||
mock_clear.assert_called_once()
|
||||
|
||||
|
||||
|
|
@ -591,7 +591,7 @@ class TestWhoamiCommand:
|
|||
result = self.runner.invoke(whoami)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "✅ Authenticated" in result.output
|
||||
assert "Authenticated" in result.output
|
||||
assert "test@example.com" in result.output
|
||||
assert "test-user-123" in result.output
|
||||
assert "admin" in result.output
|
||||
|
|
@ -603,7 +603,7 @@ class TestWhoamiCommand:
|
|||
result = self.runner.invoke(whoami)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "❌ Not authenticated" in result.output
|
||||
assert "Not authenticated" in result.output
|
||||
assert "Run 'lite login'" in result.output
|
||||
|
||||
def test_whoami_old_token(self):
|
||||
|
|
@ -619,8 +619,8 @@ class TestWhoamiCommand:
|
|||
result = self.runner.invoke(whoami)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "✅ Authenticated" in result.output
|
||||
assert "⚠️ Warning: Token is more than 24 hours old" in result.output
|
||||
assert "Authenticated" in result.output
|
||||
assert "Warning: Token is more than 24 hours old" in result.output
|
||||
|
||||
def test_whoami_missing_fields(self):
|
||||
"""Test whoami with token missing some fields"""
|
||||
|
|
@ -633,7 +633,7 @@ class TestWhoamiCommand:
|
|||
result = self.runner.invoke(whoami)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "✅ Authenticated" in result.output
|
||||
assert "Authenticated" in result.output
|
||||
assert "Unknown" in result.output # Should show "Unknown" for missing fields
|
||||
|
||||
def test_whoami_no_timestamp(self):
|
||||
|
|
@ -655,7 +655,7 @@ class TestWhoamiCommand:
|
|||
result = self.runner.invoke(whoami)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "✅ Authenticated" in result.output
|
||||
assert "Authenticated" in result.output
|
||||
# Should calculate age based on timestamp=0
|
||||
assert "Token age:" in result.output
|
||||
|
||||
|
|
@ -714,7 +714,7 @@ class TestCLIKeyRegenerationFlow:
|
|||
result = self.runner.invoke(login, obj=mock_context.obj)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "✅ Login successful!" in result.output
|
||||
assert "Login successful!" in result.output
|
||||
assert "team-beta" in result.output
|
||||
# Ensure we surface the human-readable team alias to the user
|
||||
assert "Beta Team" in result.output
|
||||
|
|
@ -774,7 +774,7 @@ class TestCLIKeyRegenerationFlow:
|
|||
result = self.runner.invoke(login, obj=mock_context.obj)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "✅ Login successful!" in result.output
|
||||
assert "Login successful!" in result.output
|
||||
|
||||
# Verify browser was opened
|
||||
mock_browser.assert_called_once()
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
# stdlib imports
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
|
|
@ -11,6 +12,7 @@ sys.path.insert(
|
|||
) # Adds the parent directory to the system path
|
||||
|
||||
|
||||
import litellm.proxy.client.cli
|
||||
from litellm._version import version as litellm_version
|
||||
from litellm.proxy.client.cli import cli
|
||||
|
||||
|
|
@ -36,6 +38,19 @@ def test_cli_version_flag(cli_runner):
|
|||
assert "LiteLLM Proxy Server Version: 1.2.3" in result.output
|
||||
|
||||
|
||||
def test_cli_source_is_ascii_only():
|
||||
"""Non-ASCII output (emoji, box-drawing chars) raises UnicodeEncodeError on legacy Windows
|
||||
consoles (cp1252), so the whole CLI package must stay ASCII-only."""
|
||||
cli_root = Path(litellm.proxy.client.cli.__file__).parent
|
||||
offenders = [
|
||||
f"{path.relative_to(cli_root)}:{line_number}: {line.strip()}"
|
||||
for path in sorted(cli_root.rglob("*.py"))
|
||||
for line_number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1)
|
||||
if not line.isascii()
|
||||
]
|
||||
assert offenders == []
|
||||
|
||||
|
||||
def test_base_url_trailing_slash_normalized(cli_runner):
|
||||
"""A trailing slash on --base-url must not produce a double slash (e.g. '//sso/cli/start')."""
|
||||
with (
|
||||
|
|
|
|||
|
|
@ -262,7 +262,7 @@ def test_keys_import_actual_import_success(mock_keys_client, cli_runner):
|
|||
|
||||
assert result.exit_code == 0
|
||||
assert "Found 1 keys in source instance" in result.output
|
||||
assert "✓ Imported key: import-key-1" in result.output
|
||||
assert "Imported key: import-key-1" in result.output
|
||||
assert "Successfully imported: 1" in result.output
|
||||
assert "Failed to import: 0" in result.output
|
||||
|
||||
|
|
@ -481,8 +481,8 @@ def test_keys_import_partial_failure(mock_keys_client, cli_runner):
|
|||
)
|
||||
|
||||
assert result.exit_code == 0 # Command completes even with partial failures
|
||||
assert "✓ Imported key: success-key" in result.output
|
||||
assert "✗ Failed to import key fail-key" in result.output
|
||||
assert "Imported key: success-key" in result.output
|
||||
assert "Failed to import key fail-key" in result.output
|
||||
assert "Successfully imported: 1" in result.output
|
||||
assert "Failed to import: 1" in result.output
|
||||
assert "Total keys processed: 2" in result.output
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue