atlas-chat 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- atlas/__init__.py +40 -0
- atlas/application/__init__.py +7 -0
- atlas/application/chat/__init__.py +7 -0
- atlas/application/chat/agent/__init__.py +10 -0
- atlas/application/chat/agent/act_loop.py +179 -0
- atlas/application/chat/agent/factory.py +142 -0
- atlas/application/chat/agent/protocols.py +46 -0
- atlas/application/chat/agent/react_loop.py +338 -0
- atlas/application/chat/agent/think_act_loop.py +171 -0
- atlas/application/chat/approval_manager.py +151 -0
- atlas/application/chat/elicitation_manager.py +191 -0
- atlas/application/chat/events/__init__.py +1 -0
- atlas/application/chat/events/agent_event_relay.py +112 -0
- atlas/application/chat/modes/__init__.py +1 -0
- atlas/application/chat/modes/agent.py +125 -0
- atlas/application/chat/modes/plain.py +74 -0
- atlas/application/chat/modes/rag.py +81 -0
- atlas/application/chat/modes/tools.py +179 -0
- atlas/application/chat/orchestrator.py +213 -0
- atlas/application/chat/policies/__init__.py +1 -0
- atlas/application/chat/policies/tool_authorization.py +99 -0
- atlas/application/chat/preprocessors/__init__.py +1 -0
- atlas/application/chat/preprocessors/message_builder.py +92 -0
- atlas/application/chat/preprocessors/prompt_override_service.py +104 -0
- atlas/application/chat/service.py +454 -0
- atlas/application/chat/utilities/__init__.py +6 -0
- atlas/application/chat/utilities/error_handler.py +367 -0
- atlas/application/chat/utilities/event_notifier.py +546 -0
- atlas/application/chat/utilities/file_processor.py +613 -0
- atlas/application/chat/utilities/tool_executor.py +789 -0
- atlas/atlas_chat_cli.py +347 -0
- atlas/atlas_client.py +238 -0
- atlas/core/__init__.py +0 -0
- atlas/core/auth.py +205 -0
- atlas/core/authorization_manager.py +27 -0
- atlas/core/capabilities.py +123 -0
- atlas/core/compliance.py +215 -0
- atlas/core/domain_whitelist.py +147 -0
- atlas/core/domain_whitelist_middleware.py +82 -0
- atlas/core/http_client.py +28 -0
- atlas/core/log_sanitizer.py +102 -0
- atlas/core/metrics_logger.py +59 -0
- atlas/core/middleware.py +131 -0
- atlas/core/otel_config.py +242 -0
- atlas/core/prompt_risk.py +200 -0
- atlas/core/rate_limit.py +0 -0
- atlas/core/rate_limit_middleware.py +64 -0
- atlas/core/security_headers_middleware.py +51 -0
- atlas/domain/__init__.py +37 -0
- atlas/domain/chat/__init__.py +1 -0
- atlas/domain/chat/dtos.py +85 -0
- atlas/domain/errors.py +96 -0
- atlas/domain/messages/__init__.py +12 -0
- atlas/domain/messages/models.py +160 -0
- atlas/domain/rag_mcp_service.py +664 -0
- atlas/domain/sessions/__init__.py +7 -0
- atlas/domain/sessions/models.py +36 -0
- atlas/domain/unified_rag_service.py +371 -0
- atlas/infrastructure/__init__.py +10 -0
- atlas/infrastructure/app_factory.py +135 -0
- atlas/infrastructure/events/__init__.py +1 -0
- atlas/infrastructure/events/cli_event_publisher.py +140 -0
- atlas/infrastructure/events/websocket_publisher.py +140 -0
- atlas/infrastructure/sessions/in_memory_repository.py +56 -0
- atlas/infrastructure/transport/__init__.py +7 -0
- atlas/infrastructure/transport/websocket_connection_adapter.py +33 -0
- atlas/init_cli.py +226 -0
- atlas/interfaces/__init__.py +15 -0
- atlas/interfaces/events.py +134 -0
- atlas/interfaces/llm.py +54 -0
- atlas/interfaces/rag.py +40 -0
- atlas/interfaces/sessions.py +75 -0
- atlas/interfaces/tools.py +57 -0
- atlas/interfaces/transport.py +24 -0
- atlas/main.py +564 -0
- atlas/mcp/api_key_demo/README.md +76 -0
- atlas/mcp/api_key_demo/main.py +172 -0
- atlas/mcp/api_key_demo/run.sh +56 -0
- atlas/mcp/basictable/main.py +147 -0
- atlas/mcp/calculator/main.py +149 -0
- atlas/mcp/code-executor/execution_engine.py +98 -0
- atlas/mcp/code-executor/execution_environment.py +95 -0
- atlas/mcp/code-executor/main.py +528 -0
- atlas/mcp/code-executor/result_processing.py +276 -0
- atlas/mcp/code-executor/script_generation.py +195 -0
- atlas/mcp/code-executor/security_checker.py +140 -0
- atlas/mcp/corporate_cars/main.py +437 -0
- atlas/mcp/csv_reporter/main.py +545 -0
- atlas/mcp/duckduckgo/main.py +182 -0
- atlas/mcp/elicitation_demo/README.md +171 -0
- atlas/mcp/elicitation_demo/main.py +262 -0
- atlas/mcp/env-demo/README.md +158 -0
- atlas/mcp/env-demo/main.py +199 -0
- atlas/mcp/file_size_test/main.py +284 -0
- atlas/mcp/filesystem/main.py +348 -0
- atlas/mcp/image_demo/main.py +113 -0
- atlas/mcp/image_demo/requirements.txt +4 -0
- atlas/mcp/logging_demo/README.md +72 -0
- atlas/mcp/logging_demo/main.py +103 -0
- atlas/mcp/many_tools_demo/main.py +50 -0
- atlas/mcp/order_database/__init__.py +0 -0
- atlas/mcp/order_database/main.py +369 -0
- atlas/mcp/order_database/signal_data.csv +1001 -0
- atlas/mcp/pdfbasic/main.py +394 -0
- atlas/mcp/pptx_generator/main.py +760 -0
- atlas/mcp/pptx_generator/requirements.txt +13 -0
- atlas/mcp/pptx_generator/run_test.sh +1 -0
- atlas/mcp/pptx_generator/test_pptx_generator_security.py +169 -0
- atlas/mcp/progress_demo/main.py +167 -0
- atlas/mcp/progress_updates_demo/QUICKSTART.md +273 -0
- atlas/mcp/progress_updates_demo/README.md +120 -0
- atlas/mcp/progress_updates_demo/main.py +497 -0
- atlas/mcp/prompts/main.py +222 -0
- atlas/mcp/public_demo/main.py +189 -0
- atlas/mcp/sampling_demo/README.md +169 -0
- atlas/mcp/sampling_demo/main.py +234 -0
- atlas/mcp/thinking/main.py +77 -0
- atlas/mcp/tool_planner/main.py +240 -0
- atlas/mcp/ui-demo/badmesh.png +0 -0
- atlas/mcp/ui-demo/main.py +383 -0
- atlas/mcp/ui-demo/templates/button_demo.html +32 -0
- atlas/mcp/ui-demo/templates/data_visualization.html +32 -0
- atlas/mcp/ui-demo/templates/form_demo.html +28 -0
- atlas/mcp/username-override-demo/README.md +320 -0
- atlas/mcp/username-override-demo/main.py +308 -0
- atlas/modules/__init__.py +0 -0
- atlas/modules/config/__init__.py +34 -0
- atlas/modules/config/cli.py +231 -0
- atlas/modules/config/config_manager.py +1096 -0
- atlas/modules/file_storage/__init__.py +22 -0
- atlas/modules/file_storage/cli.py +330 -0
- atlas/modules/file_storage/content_extractor.py +290 -0
- atlas/modules/file_storage/manager.py +295 -0
- atlas/modules/file_storage/mock_s3_client.py +402 -0
- atlas/modules/file_storage/s3_client.py +417 -0
- atlas/modules/llm/__init__.py +19 -0
- atlas/modules/llm/caller.py +287 -0
- atlas/modules/llm/litellm_caller.py +675 -0
- atlas/modules/llm/models.py +19 -0
- atlas/modules/mcp_tools/__init__.py +17 -0
- atlas/modules/mcp_tools/client.py +2123 -0
- atlas/modules/mcp_tools/token_storage.py +556 -0
- atlas/modules/prompts/prompt_provider.py +130 -0
- atlas/modules/rag/__init__.py +24 -0
- atlas/modules/rag/atlas_rag_client.py +336 -0
- atlas/modules/rag/client.py +129 -0
- atlas/routes/admin_routes.py +865 -0
- atlas/routes/config_routes.py +484 -0
- atlas/routes/feedback_routes.py +361 -0
- atlas/routes/files_routes.py +274 -0
- atlas/routes/health_routes.py +40 -0
- atlas/routes/mcp_auth_routes.py +223 -0
- atlas/server_cli.py +164 -0
- atlas/tests/conftest.py +20 -0
- atlas/tests/integration/test_mcp_auth_integration.py +152 -0
- atlas/tests/manual_test_sampling.py +87 -0
- atlas/tests/modules/mcp_tools/test_client_auth.py +226 -0
- atlas/tests/modules/mcp_tools/test_client_env.py +191 -0
- atlas/tests/test_admin_mcp_server_management_routes.py +141 -0
- atlas/tests/test_agent_roa.py +135 -0
- atlas/tests/test_app_factory_smoke.py +47 -0
- atlas/tests/test_approval_manager.py +439 -0
- atlas/tests/test_atlas_client.py +188 -0
- atlas/tests/test_atlas_rag_client.py +447 -0
- atlas/tests/test_atlas_rag_integration.py +224 -0
- atlas/tests/test_attach_file_flow.py +287 -0
- atlas/tests/test_auth_utils.py +165 -0
- atlas/tests/test_backend_public_url.py +185 -0
- atlas/tests/test_banner_logging.py +287 -0
- atlas/tests/test_capability_tokens_and_injection.py +203 -0
- atlas/tests/test_compliance_level.py +54 -0
- atlas/tests/test_compliance_manager.py +253 -0
- atlas/tests/test_config_manager.py +617 -0
- atlas/tests/test_config_manager_paths.py +12 -0
- atlas/tests/test_core_auth.py +18 -0
- atlas/tests/test_core_utils.py +190 -0
- atlas/tests/test_docker_env_sync.py +202 -0
- atlas/tests/test_domain_errors.py +329 -0
- atlas/tests/test_domain_whitelist.py +359 -0
- atlas/tests/test_elicitation_manager.py +408 -0
- atlas/tests/test_elicitation_routing.py +296 -0
- atlas/tests/test_env_demo_server.py +88 -0
- atlas/tests/test_error_classification.py +113 -0
- atlas/tests/test_error_flow_integration.py +116 -0
- atlas/tests/test_feedback_routes.py +333 -0
- atlas/tests/test_file_content_extraction.py +1134 -0
- atlas/tests/test_file_extraction_routes.py +158 -0
- atlas/tests/test_file_library.py +107 -0
- atlas/tests/test_file_manager_unit.py +18 -0
- atlas/tests/test_health_route.py +49 -0
- atlas/tests/test_http_client_stub.py +8 -0
- atlas/tests/test_imports_smoke.py +30 -0
- atlas/tests/test_interfaces_llm_response.py +9 -0
- atlas/tests/test_issue_access_denied_fix.py +136 -0
- atlas/tests/test_llm_env_expansion.py +836 -0
- atlas/tests/test_log_level_sensitive_data.py +285 -0
- atlas/tests/test_mcp_auth_routes.py +341 -0
- atlas/tests/test_mcp_client_auth.py +331 -0
- atlas/tests/test_mcp_data_injection.py +270 -0
- atlas/tests/test_mcp_get_authorized_servers.py +95 -0
- atlas/tests/test_mcp_hot_reload.py +512 -0
- atlas/tests/test_mcp_image_content.py +424 -0
- atlas/tests/test_mcp_logging.py +172 -0
- atlas/tests/test_mcp_progress_updates.py +313 -0
- atlas/tests/test_mcp_prompt_override_system_prompt.py +102 -0
- atlas/tests/test_mcp_prompts_server.py +39 -0
- atlas/tests/test_mcp_tool_result_parsing.py +296 -0
- atlas/tests/test_metrics_logger.py +56 -0
- atlas/tests/test_middleware_auth.py +379 -0
- atlas/tests/test_prompt_risk_and_acl.py +141 -0
- atlas/tests/test_rag_mcp_aggregator.py +204 -0
- atlas/tests/test_rag_mcp_service.py +224 -0
- atlas/tests/test_rate_limit_middleware.py +45 -0
- atlas/tests/test_routes_config_smoke.py +60 -0
- atlas/tests/test_routes_files_download_token.py +41 -0
- atlas/tests/test_routes_files_health.py +18 -0
- atlas/tests/test_runtime_imports.py +53 -0
- atlas/tests/test_sampling_integration.py +482 -0
- atlas/tests/test_security_admin_routes.py +61 -0
- atlas/tests/test_security_capability_tokens.py +65 -0
- atlas/tests/test_security_file_stats_scope.py +21 -0
- atlas/tests/test_security_header_injection.py +191 -0
- atlas/tests/test_security_headers_and_filename.py +63 -0
- atlas/tests/test_shared_session_repository.py +101 -0
- atlas/tests/test_system_prompt_loading.py +181 -0
- atlas/tests/test_token_storage.py +505 -0
- atlas/tests/test_tool_approval_config.py +93 -0
- atlas/tests/test_tool_approval_utils.py +356 -0
- atlas/tests/test_tool_authorization_group_filtering.py +223 -0
- atlas/tests/test_tool_details_in_config.py +108 -0
- atlas/tests/test_tool_planner.py +300 -0
- atlas/tests/test_unified_rag_service.py +398 -0
- atlas/tests/test_username_override_in_approval.py +258 -0
- atlas/tests/test_websocket_auth_header.py +168 -0
- atlas/version.py +6 -0
- atlas_chat-0.1.0.data/data/.env.example +253 -0
- atlas_chat-0.1.0.data/data/config/defaults/compliance-levels.json +44 -0
- atlas_chat-0.1.0.data/data/config/defaults/domain-whitelist.json +123 -0
- atlas_chat-0.1.0.data/data/config/defaults/file-extractors.json +74 -0
- atlas_chat-0.1.0.data/data/config/defaults/help-config.json +198 -0
- atlas_chat-0.1.0.data/data/config/defaults/llmconfig-buggy.yml +11 -0
- atlas_chat-0.1.0.data/data/config/defaults/llmconfig.yml +19 -0
- atlas_chat-0.1.0.data/data/config/defaults/mcp.json +138 -0
- atlas_chat-0.1.0.data/data/config/defaults/rag-sources.json +17 -0
- atlas_chat-0.1.0.data/data/config/defaults/splash-config.json +16 -0
- atlas_chat-0.1.0.dist-info/METADATA +236 -0
- atlas_chat-0.1.0.dist-info/RECORD +250 -0
- atlas_chat-0.1.0.dist-info/WHEEL +5 -0
- atlas_chat-0.1.0.dist-info/entry_points.txt +4 -0
- atlas_chat-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,836 @@
|
|
|
1
|
+
"""Integration tests for LLM environment variable expansion."""
|
|
2
|
+
|
|
3
|
+
import importlib
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from atlas.modules.config.config_manager import LLMConfig, ModelConfig
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _get_real_litellm_caller():
|
|
12
|
+
"""
|
|
13
|
+
Get the real LiteLLMCaller class, even if another test has patched sys.modules.
|
|
14
|
+
|
|
15
|
+
Some test files (e.g., test_capability_tokens_and_injection.py) patch the
|
|
16
|
+
litellm_caller module at import time. This function forces a reimport of
|
|
17
|
+
the real module to get the real LiteLLMCaller class.
|
|
18
|
+
"""
|
|
19
|
+
module_name = "atlas.modules.llm.litellm_caller"
|
|
20
|
+
|
|
21
|
+
# Remove the potentially fake module from sys.modules
|
|
22
|
+
if module_name in sys.modules:
|
|
23
|
+
old_module = sys.modules.pop(module_name)
|
|
24
|
+
# Check if it's a fake by seeing if LiteLLMCaller has expected attributes
|
|
25
|
+
if hasattr(old_module, "LiteLLMCaller"):
|
|
26
|
+
caller_class = old_module.LiteLLMCaller
|
|
27
|
+
if not hasattr(caller_class, "_get_model_kwargs"):
|
|
28
|
+
# It's a fake, reimport the real one
|
|
29
|
+
real_module = importlib.import_module(module_name)
|
|
30
|
+
return real_module.LiteLLMCaller
|
|
31
|
+
else:
|
|
32
|
+
# It's real, restore it
|
|
33
|
+
sys.modules[module_name] = old_module
|
|
34
|
+
return caller_class
|
|
35
|
+
|
|
36
|
+
# Not in sys.modules, import fresh
|
|
37
|
+
real_module = importlib.import_module(module_name)
|
|
38
|
+
return real_module.LiteLLMCaller
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# Get the real LiteLLMCaller at module load time
|
|
42
|
+
LiteLLMCaller = _get_real_litellm_caller()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class TestLLMEnvExpansionIntegration:
|
|
46
|
+
"""Integration tests for LLM caller with environment variable expansion."""
|
|
47
|
+
|
|
48
|
+
def test_litellm_caller_resolves_api_key_env_var(self, monkeypatch):
|
|
49
|
+
"""LiteLLMCaller should resolve environment variables in api_key."""
|
|
50
|
+
monkeypatch.setenv("TEST_OPENAI_KEY", "sk-test-12345")
|
|
51
|
+
|
|
52
|
+
# Create LLM config with env var in api_key
|
|
53
|
+
llm_config = LLMConfig(
|
|
54
|
+
models={
|
|
55
|
+
"test-model": ModelConfig(
|
|
56
|
+
model_name="gpt-4",
|
|
57
|
+
model_url="https://api.openai.com/v1",
|
|
58
|
+
api_key="${TEST_OPENAI_KEY}"
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# Create LiteLLMCaller
|
|
64
|
+
caller = LiteLLMCaller(llm_config, debug_mode=True)
|
|
65
|
+
|
|
66
|
+
# Get model kwargs - this should resolve the env var
|
|
67
|
+
_ = caller._get_model_kwargs("test-model")
|
|
68
|
+
|
|
69
|
+
# Verify that the environment variable was set (LiteLLMCaller sets env vars for provider detection)
|
|
70
|
+
import os
|
|
71
|
+
assert os.environ.get("OPENAI_API_KEY") == "sk-test-12345"
|
|
72
|
+
|
|
73
|
+
# Cleanup to avoid leaking into other tests
|
|
74
|
+
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
|
|
75
|
+
|
|
76
|
+
def test_litellm_caller_raises_on_missing_api_key_env_var(self):
|
|
77
|
+
"""LiteLLMCaller should raise ValueError when api_key env var is missing."""
|
|
78
|
+
# Create LLM config with missing env var in api_key
|
|
79
|
+
llm_config = LLMConfig(
|
|
80
|
+
models={
|
|
81
|
+
"test-model": ModelConfig(
|
|
82
|
+
model_name="gpt-4",
|
|
83
|
+
model_url="https://api.openai.com/v1",
|
|
84
|
+
api_key="${MISSING_OPENAI_KEY}"
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# Create LiteLLMCaller
|
|
90
|
+
caller = LiteLLMCaller(llm_config, debug_mode=True)
|
|
91
|
+
|
|
92
|
+
# Get model kwargs - this should raise ValueError
|
|
93
|
+
with pytest.raises(ValueError, match="Environment variable 'MISSING_OPENAI_KEY' is not set"):
|
|
94
|
+
caller._get_model_kwargs("test-model")
|
|
95
|
+
|
|
96
|
+
def test_litellm_caller_handles_literal_api_key(self, monkeypatch):
|
|
97
|
+
"""LiteLLMCaller should handle literal api_key values."""
|
|
98
|
+
# Create LLM config with literal api_key
|
|
99
|
+
llm_config = LLMConfig(
|
|
100
|
+
models={
|
|
101
|
+
"test-model": ModelConfig(
|
|
102
|
+
model_name="gpt-4",
|
|
103
|
+
model_url="https://api.openai.com/v1",
|
|
104
|
+
api_key="sk-literal-key-12345"
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# Create LiteLLMCaller
|
|
110
|
+
caller = LiteLLMCaller(llm_config, debug_mode=True)
|
|
111
|
+
|
|
112
|
+
# Get model kwargs - this should work without errors
|
|
113
|
+
_ = caller._get_model_kwargs("test-model")
|
|
114
|
+
|
|
115
|
+
# Verify that the environment variable was set
|
|
116
|
+
import os
|
|
117
|
+
assert os.environ.get("OPENAI_API_KEY") == "sk-literal-key-12345"
|
|
118
|
+
|
|
119
|
+
# Cleanup to avoid leaking into other tests
|
|
120
|
+
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
|
|
121
|
+
|
|
122
|
+
def test_litellm_caller_resolves_extra_headers_env_vars(self, monkeypatch):
|
|
123
|
+
"""LiteLLMCaller should resolve environment variables in extra_headers."""
|
|
124
|
+
monkeypatch.setenv("TEST_REFERER", "https://myapp.com")
|
|
125
|
+
monkeypatch.setenv("TEST_APP_NAME", "MyTestApp")
|
|
126
|
+
|
|
127
|
+
# Create LLM config with env vars in extra_headers
|
|
128
|
+
llm_config = LLMConfig(
|
|
129
|
+
models={
|
|
130
|
+
"test-model": ModelConfig(
|
|
131
|
+
model_name="llama-3-70b",
|
|
132
|
+
model_url="https://openrouter.ai/api/v1",
|
|
133
|
+
api_key="sk-test",
|
|
134
|
+
extra_headers={
|
|
135
|
+
"HTTP-Referer": "${TEST_REFERER}",
|
|
136
|
+
"X-Title": "${TEST_APP_NAME}"
|
|
137
|
+
}
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
# Create LiteLLMCaller
|
|
143
|
+
caller = LiteLLMCaller(llm_config, debug_mode=True)
|
|
144
|
+
|
|
145
|
+
# Get model kwargs - this should resolve the env vars
|
|
146
|
+
model_kwargs = caller._get_model_kwargs("test-model")
|
|
147
|
+
|
|
148
|
+
# Verify that extra_headers were resolved
|
|
149
|
+
assert "extra_headers" in model_kwargs
|
|
150
|
+
assert model_kwargs["extra_headers"]["HTTP-Referer"] == "https://myapp.com"
|
|
151
|
+
assert model_kwargs["extra_headers"]["X-Title"] == "MyTestApp"
|
|
152
|
+
|
|
153
|
+
def test_litellm_caller_raises_on_missing_extra_headers_env_var(self):
|
|
154
|
+
"""LiteLLMCaller should raise ValueError when extra_headers env var is missing."""
|
|
155
|
+
# Create LLM config with missing env var in extra_headers
|
|
156
|
+
llm_config = LLMConfig(
|
|
157
|
+
models={
|
|
158
|
+
"test-model": ModelConfig(
|
|
159
|
+
model_name="llama-3-70b",
|
|
160
|
+
model_url="https://openrouter.ai/api/v1",
|
|
161
|
+
api_key="sk-test",
|
|
162
|
+
extra_headers={
|
|
163
|
+
"HTTP-Referer": "${MISSING_REFERER}"
|
|
164
|
+
}
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
# Create LiteLLMCaller
|
|
170
|
+
caller = LiteLLMCaller(llm_config, debug_mode=True)
|
|
171
|
+
|
|
172
|
+
# Get model kwargs - this should raise ValueError
|
|
173
|
+
with pytest.raises(ValueError, match="Environment variable 'MISSING_REFERER' is not set"):
|
|
174
|
+
caller._get_model_kwargs("test-model")
|
|
175
|
+
|
|
176
|
+
def test_litellm_caller_handles_literal_extra_headers(self):
|
|
177
|
+
"""LiteLLMCaller should handle literal extra_headers values."""
|
|
178
|
+
# Create LLM config with literal extra_headers
|
|
179
|
+
llm_config = LLMConfig(
|
|
180
|
+
models={
|
|
181
|
+
"test-model": ModelConfig(
|
|
182
|
+
model_name="llama-3-70b",
|
|
183
|
+
model_url="https://openrouter.ai/api/v1",
|
|
184
|
+
api_key="sk-test",
|
|
185
|
+
extra_headers={
|
|
186
|
+
"HTTP-Referer": "https://literal-app.com",
|
|
187
|
+
"X-Title": "LiteralApp"
|
|
188
|
+
}
|
|
189
|
+
)
|
|
190
|
+
}
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
# Create LiteLLMCaller
|
|
194
|
+
caller = LiteLLMCaller(llm_config, debug_mode=True)
|
|
195
|
+
|
|
196
|
+
# Get model kwargs - this should work without errors
|
|
197
|
+
model_kwargs = caller._get_model_kwargs("test-model")
|
|
198
|
+
|
|
199
|
+
# Verify that extra_headers were passed through
|
|
200
|
+
assert "extra_headers" in model_kwargs
|
|
201
|
+
assert model_kwargs["extra_headers"]["HTTP-Referer"] == "https://literal-app.com"
|
|
202
|
+
assert model_kwargs["extra_headers"]["X-Title"] == "LiteralApp"
|
|
203
|
+
|
|
204
|
+
def test_custom_endpoint_with_env_var_api_key(self, monkeypatch):
|
|
205
|
+
"""Custom endpoint should pass api_key in kwargs when using env var."""
|
|
206
|
+
monkeypatch.setenv("CUSTOM_LLM_KEY", "sk-custom-12345")
|
|
207
|
+
|
|
208
|
+
# Create LLM config for custom endpoint with env var in api_key
|
|
209
|
+
llm_config = LLMConfig(
|
|
210
|
+
models={
|
|
211
|
+
"custom-model": ModelConfig(
|
|
212
|
+
model_name="custom-model-name",
|
|
213
|
+
model_url="https://custom-llm.example.com/v1",
|
|
214
|
+
api_key="${CUSTOM_LLM_KEY}"
|
|
215
|
+
)
|
|
216
|
+
}
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
# Create LiteLLMCaller
|
|
220
|
+
caller = LiteLLMCaller(llm_config, debug_mode=True)
|
|
221
|
+
|
|
222
|
+
# Get model kwargs
|
|
223
|
+
model_kwargs = caller._get_model_kwargs("custom-model")
|
|
224
|
+
|
|
225
|
+
# Verify that api_key is in kwargs (critical for custom endpoints)
|
|
226
|
+
assert "api_key" in model_kwargs
|
|
227
|
+
assert model_kwargs["api_key"] == "sk-custom-12345"
|
|
228
|
+
|
|
229
|
+
# Verify that api_base is set for custom endpoint
|
|
230
|
+
assert "api_base" in model_kwargs
|
|
231
|
+
assert model_kwargs["api_base"] == "https://custom-llm.example.com/v1"
|
|
232
|
+
|
|
233
|
+
# Verify fallback env var is set for OpenAI-compatible endpoints
|
|
234
|
+
import os
|
|
235
|
+
assert os.environ.get("OPENAI_API_KEY") == "sk-custom-12345"
|
|
236
|
+
|
|
237
|
+
# Cleanup to avoid leaking into other tests
|
|
238
|
+
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
|
|
239
|
+
|
|
240
|
+
def test_custom_endpoint_with_literal_api_key(self):
|
|
241
|
+
"""Custom endpoint should pass api_key in kwargs when using literal value."""
|
|
242
|
+
# Create LLM config for custom endpoint with literal api_key
|
|
243
|
+
llm_config = LLMConfig(
|
|
244
|
+
models={
|
|
245
|
+
"custom-model": ModelConfig(
|
|
246
|
+
model_name="custom-model-name",
|
|
247
|
+
model_url="https://custom-llm.example.com/v1",
|
|
248
|
+
api_key="sk-literal-custom-key"
|
|
249
|
+
)
|
|
250
|
+
}
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
# Create LiteLLMCaller
|
|
254
|
+
caller = LiteLLMCaller(llm_config, debug_mode=True)
|
|
255
|
+
|
|
256
|
+
# Get model kwargs
|
|
257
|
+
model_kwargs = caller._get_model_kwargs("custom-model")
|
|
258
|
+
|
|
259
|
+
# Verify that api_key is in kwargs (critical for custom endpoints)
|
|
260
|
+
assert "api_key" in model_kwargs
|
|
261
|
+
assert model_kwargs["api_key"] == "sk-literal-custom-key"
|
|
262
|
+
|
|
263
|
+
# Verify that api_base is set for custom endpoint
|
|
264
|
+
assert "api_base" in model_kwargs
|
|
265
|
+
assert model_kwargs["api_base"] == "https://custom-llm.example.com/v1"
|
|
266
|
+
|
|
267
|
+
def test_openai_env_not_overwritten_if_same_value(self, monkeypatch):
|
|
268
|
+
"""OPENAI_API_KEY is left as-is when value matches."""
|
|
269
|
+
# Pre-set env to a specific value
|
|
270
|
+
monkeypatch.setenv("OPENAI_API_KEY", "sk-openai-same")
|
|
271
|
+
|
|
272
|
+
llm_config = LLMConfig(
|
|
273
|
+
models={
|
|
274
|
+
"openai-model": ModelConfig(
|
|
275
|
+
model_name="gpt-4",
|
|
276
|
+
model_url="https://api.openai.com/v1",
|
|
277
|
+
api_key="sk-openai-same",
|
|
278
|
+
)
|
|
279
|
+
}
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
caller = LiteLLMCaller(llm_config, debug_mode=True)
|
|
283
|
+
model_kwargs = caller._get_model_kwargs("openai-model")
|
|
284
|
+
|
|
285
|
+
import os
|
|
286
|
+
# Still should have correct key in kwargs
|
|
287
|
+
assert model_kwargs["api_key"] == "sk-openai-same"
|
|
288
|
+
# Env var should remain the same
|
|
289
|
+
assert os.environ.get("OPENAI_API_KEY") == "sk-openai-same"
|
|
290
|
+
|
|
291
|
+
def test_openai_env_overwritten_with_warning(self, monkeypatch, caplog):
|
|
292
|
+
"""OPENAI_API_KEY overwrite should occur with a warning when value differs."""
|
|
293
|
+
monkeypatch.setenv("OPENAI_API_KEY", "sk-openai-original")
|
|
294
|
+
|
|
295
|
+
llm_config = LLMConfig(
|
|
296
|
+
models={
|
|
297
|
+
"openai-model": ModelConfig(
|
|
298
|
+
model_name="gpt-4",
|
|
299
|
+
model_url="https://api.openai.com/v1",
|
|
300
|
+
api_key="sk-openai-new",
|
|
301
|
+
)
|
|
302
|
+
}
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
caller = LiteLLMCaller(llm_config, debug_mode=True)
|
|
306
|
+
|
|
307
|
+
with caplog.at_level("WARNING"):
|
|
308
|
+
model_kwargs = caller._get_model_kwargs("openai-model")
|
|
309
|
+
|
|
310
|
+
import os
|
|
311
|
+
# kwargs should use the new key
|
|
312
|
+
assert model_kwargs["api_key"] == "sk-openai-new"
|
|
313
|
+
# Env var should be overwritten to the new value
|
|
314
|
+
assert os.environ.get("OPENAI_API_KEY") == "sk-openai-new"
|
|
315
|
+
# A warning about overwriting should be logged
|
|
316
|
+
assert any("Overwriting existing environment variable OPENAI_API_KEY" in rec.getMessage() for rec in caplog.records)
|
|
317
|
+
|
|
318
|
+
def test_openai_and_custom_models_resolved_in_succession(self, monkeypatch):
|
|
319
|
+
"""Sequence of OpenAI then custom endpoint should keep last key in env while kwargs stay correct."""
|
|
320
|
+
monkeypatch.setenv("OPENAI_API_KEY", "sk-preexisting")
|
|
321
|
+
|
|
322
|
+
llm_config = LLMConfig(
|
|
323
|
+
models={
|
|
324
|
+
"openai-model": ModelConfig(
|
|
325
|
+
model_name="gpt-4",
|
|
326
|
+
model_url="https://api.openai.com/v1",
|
|
327
|
+
api_key="sk-openai-1",
|
|
328
|
+
),
|
|
329
|
+
"custom-model": ModelConfig(
|
|
330
|
+
model_name="custom-model-name",
|
|
331
|
+
model_url="https://custom-llm.example.com/v1",
|
|
332
|
+
api_key="sk-custom-2",
|
|
333
|
+
),
|
|
334
|
+
}
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
caller = LiteLLMCaller(llm_config, debug_mode=True)
|
|
338
|
+
|
|
339
|
+
# First resolve OpenAI model
|
|
340
|
+
openai_kwargs = caller._get_model_kwargs("openai-model")
|
|
341
|
+
# Then resolve custom model
|
|
342
|
+
custom_kwargs = caller._get_model_kwargs("custom-model")
|
|
343
|
+
|
|
344
|
+
import os
|
|
345
|
+
# kwargs should always reflect model-specific keys
|
|
346
|
+
assert openai_kwargs["api_key"] == "sk-openai-1"
|
|
347
|
+
assert custom_kwargs["api_key"] == "sk-custom-2"
|
|
348
|
+
# Env var ends up with the last key used (custom model)
|
|
349
|
+
assert os.environ.get("OPENAI_API_KEY") == "sk-custom-2"
|
|
350
|
+
|
|
351
|
+
def test_custom_endpoint_with_extra_headers(self, monkeypatch):
|
|
352
|
+
"""Custom endpoint should handle extra_headers correctly."""
|
|
353
|
+
monkeypatch.setenv("CUSTOM_API_KEY", "sk-custom-auth")
|
|
354
|
+
monkeypatch.setenv("CUSTOM_TENANT", "tenant-123")
|
|
355
|
+
|
|
356
|
+
# Create LLM config for custom endpoint with extra headers
|
|
357
|
+
llm_config = LLMConfig(
|
|
358
|
+
models={
|
|
359
|
+
"custom-model": ModelConfig(
|
|
360
|
+
model_name="custom-model-name",
|
|
361
|
+
model_url="https://custom-llm.example.com/v1",
|
|
362
|
+
api_key="${CUSTOM_API_KEY}",
|
|
363
|
+
extra_headers={
|
|
364
|
+
"X-Tenant-ID": "${CUSTOM_TENANT}",
|
|
365
|
+
"X-Custom-Header": "custom-value"
|
|
366
|
+
}
|
|
367
|
+
)
|
|
368
|
+
}
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
# Create LiteLLMCaller
|
|
372
|
+
caller = LiteLLMCaller(llm_config, debug_mode=True)
|
|
373
|
+
|
|
374
|
+
# Get model kwargs
|
|
375
|
+
model_kwargs = caller._get_model_kwargs("custom-model")
|
|
376
|
+
|
|
377
|
+
# Verify api_key is passed
|
|
378
|
+
assert "api_key" in model_kwargs
|
|
379
|
+
assert model_kwargs["api_key"] == "sk-custom-auth"
|
|
380
|
+
|
|
381
|
+
# Verify extra_headers are resolved and passed
|
|
382
|
+
assert "extra_headers" in model_kwargs
|
|
383
|
+
assert model_kwargs["extra_headers"]["X-Tenant-ID"] == "tenant-123"
|
|
384
|
+
assert model_kwargs["extra_headers"]["X-Custom-Header"] == "custom-value"
|
|
385
|
+
|
|
386
|
+
# Verify api_base is set
|
|
387
|
+
assert "api_base" in model_kwargs
|
|
388
|
+
|
|
389
|
+
def test_known_providers_still_get_api_key_in_kwargs(self, monkeypatch):
|
|
390
|
+
"""Verify that known providers also get api_key in kwargs (backward compatibility)."""
|
|
391
|
+
# Test OpenAI
|
|
392
|
+
llm_config = LLMConfig(
|
|
393
|
+
models={
|
|
394
|
+
"openai-model": ModelConfig(
|
|
395
|
+
model_name="gpt-4",
|
|
396
|
+
model_url="https://api.openai.com/v1",
|
|
397
|
+
api_key="sk-openai-test"
|
|
398
|
+
)
|
|
399
|
+
}
|
|
400
|
+
)
|
|
401
|
+
caller = LiteLLMCaller(llm_config, debug_mode=True)
|
|
402
|
+
model_kwargs = caller._get_model_kwargs("openai-model")
|
|
403
|
+
|
|
404
|
+
# OpenAI should get api_key in kwargs
|
|
405
|
+
assert "api_key" in model_kwargs
|
|
406
|
+
assert model_kwargs["api_key"] == "sk-openai-test"
|
|
407
|
+
|
|
408
|
+
# cleanup any env var potentially set by implementation
|
|
409
|
+
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
|
|
410
|
+
|
|
411
|
+
# Test OpenRouter
|
|
412
|
+
llm_config = LLMConfig(
|
|
413
|
+
models={
|
|
414
|
+
"openrouter-model": ModelConfig(
|
|
415
|
+
model_name="meta-llama/llama-3-70b",
|
|
416
|
+
model_url="https://openrouter.ai/api/v1",
|
|
417
|
+
api_key="sk-or-test"
|
|
418
|
+
)
|
|
419
|
+
}
|
|
420
|
+
)
|
|
421
|
+
caller = LiteLLMCaller(llm_config, debug_mode=True)
|
|
422
|
+
model_kwargs = caller._get_model_kwargs("openrouter-model")
|
|
423
|
+
|
|
424
|
+
# OpenRouter should get api_key in kwargs
|
|
425
|
+
assert "api_key" in model_kwargs
|
|
426
|
+
assert model_kwargs["api_key"] == "sk-or-test"
|
|
427
|
+
|
|
428
|
+
# cleanup any env var potentially set by implementation
|
|
429
|
+
monkeypatch.delenv("OPENROUTER_API_KEY", raising=False)
|
|
430
|
+
|
|
431
|
+
def test_openai_key_from_dotenv_file(self, monkeypatch):
|
|
432
|
+
"""Test that OPENAI_API_KEY set in .env file (loaded into os.environ) works correctly."""
|
|
433
|
+
# Simulate .env file loading by setting the env var
|
|
434
|
+
# In real scenarios, python-dotenv loads .env into os.environ
|
|
435
|
+
monkeypatch.setenv("OPENAI_API_KEY", "sk-from-dotenv-file")
|
|
436
|
+
|
|
437
|
+
# Create LLM config that doesn't specify an api_key (relies on env)
|
|
438
|
+
llm_config = LLMConfig(
|
|
439
|
+
models={
|
|
440
|
+
"openai-model": ModelConfig(
|
|
441
|
+
model_name="gpt-4",
|
|
442
|
+
model_url="https://api.openai.com/v1",
|
|
443
|
+
api_key="${OPENAI_API_KEY}"
|
|
444
|
+
)
|
|
445
|
+
}
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
caller = LiteLLMCaller(llm_config, debug_mode=True)
|
|
449
|
+
model_kwargs = caller._get_model_kwargs("openai-model")
|
|
450
|
+
|
|
451
|
+
# Should use the key from .env
|
|
452
|
+
assert model_kwargs["api_key"] == "sk-from-dotenv-file"
|
|
453
|
+
|
|
454
|
+
import os
|
|
455
|
+
assert os.environ.get("OPENAI_API_KEY") == "sk-from-dotenv-file"
|
|
456
|
+
|
|
457
|
+
def test_multiple_custom_openai_compatible_endpoints_with_different_keys(self, monkeypatch):
|
|
458
|
+
"""Test multiple OpenAI-compatible custom endpoints each with their own API key."""
|
|
459
|
+
monkeypatch.setenv("CUSTOM_LLM_A_KEY", "sk-custom-a-12345")
|
|
460
|
+
monkeypatch.setenv("CUSTOM_LLM_B_KEY", "sk-custom-b-67890")
|
|
461
|
+
monkeypatch.setenv("CUSTOM_LLM_C_KEY", "sk-custom-c-abcde")
|
|
462
|
+
|
|
463
|
+
llm_config = LLMConfig(
|
|
464
|
+
models={
|
|
465
|
+
"custom-llm-a": ModelConfig(
|
|
466
|
+
model_name="custom-model-a",
|
|
467
|
+
model_url="https://llm-a.example.com/v1",
|
|
468
|
+
api_key="${CUSTOM_LLM_A_KEY}"
|
|
469
|
+
),
|
|
470
|
+
"custom-llm-b": ModelConfig(
|
|
471
|
+
model_name="custom-model-b",
|
|
472
|
+
model_url="https://llm-b.example.com/v1",
|
|
473
|
+
api_key="${CUSTOM_LLM_B_KEY}"
|
|
474
|
+
),
|
|
475
|
+
"custom-llm-c": ModelConfig(
|
|
476
|
+
model_name="custom-model-c",
|
|
477
|
+
model_url="https://llm-c.example.com/v1",
|
|
478
|
+
api_key="${CUSTOM_LLM_C_KEY}"
|
|
479
|
+
),
|
|
480
|
+
}
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
caller = LiteLLMCaller(llm_config, debug_mode=True)
|
|
484
|
+
|
|
485
|
+
# Get kwargs for each model
|
|
486
|
+
kwargs_a = caller._get_model_kwargs("custom-llm-a")
|
|
487
|
+
kwargs_b = caller._get_model_kwargs("custom-llm-b")
|
|
488
|
+
kwargs_c = caller._get_model_kwargs("custom-llm-c")
|
|
489
|
+
|
|
490
|
+
# Each should have its own correct API key in kwargs
|
|
491
|
+
assert kwargs_a["api_key"] == "sk-custom-a-12345"
|
|
492
|
+
assert kwargs_b["api_key"] == "sk-custom-b-67890"
|
|
493
|
+
assert kwargs_c["api_key"] == "sk-custom-c-abcde"
|
|
494
|
+
|
|
495
|
+
# Each should have its own api_base
|
|
496
|
+
assert kwargs_a["api_base"] == "https://llm-a.example.com/v1"
|
|
497
|
+
assert kwargs_b["api_base"] == "https://llm-b.example.com/v1"
|
|
498
|
+
assert kwargs_c["api_base"] == "https://llm-c.example.com/v1"
|
|
499
|
+
|
|
500
|
+
# OPENAI_API_KEY env var will be set to the last one resolved
|
|
501
|
+
import os
|
|
502
|
+
assert os.environ.get("OPENAI_API_KEY") == "sk-custom-c-abcde"
|
|
503
|
+
|
|
504
|
+
# Cleanup
|
|
505
|
+
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
|
|
506
|
+
|
|
507
|
+
def test_custom_and_real_openai_endpoints_use_correct_keys(self, monkeypatch):
|
|
508
|
+
"""Test that custom endpoints and real OpenAI endpoints each use their correct API keys."""
|
|
509
|
+
monkeypatch.setenv("REAL_OPENAI_KEY", "sk-real-openai-xyz")
|
|
510
|
+
monkeypatch.setenv("CUSTOM_PROVIDER_KEY", "sk-custom-provider-abc")
|
|
511
|
+
|
|
512
|
+
llm_config = LLMConfig(
|
|
513
|
+
models={
|
|
514
|
+
"openai-gpt4": ModelConfig(
|
|
515
|
+
model_name="gpt-4",
|
|
516
|
+
model_url="https://api.openai.com/v1",
|
|
517
|
+
api_key="${REAL_OPENAI_KEY}"
|
|
518
|
+
),
|
|
519
|
+
"custom-provider": ModelConfig(
|
|
520
|
+
model_name="custom-llm-7b",
|
|
521
|
+
model_url="https://custom-provider.example.com/v1",
|
|
522
|
+
api_key="${CUSTOM_PROVIDER_KEY}"
|
|
523
|
+
),
|
|
524
|
+
}
|
|
525
|
+
)
|
|
526
|
+
|
|
527
|
+
caller = LiteLLMCaller(llm_config, debug_mode=True)
|
|
528
|
+
|
|
529
|
+
# Test OpenAI endpoint
|
|
530
|
+
openai_kwargs = caller._get_model_kwargs("openai-gpt4")
|
|
531
|
+
assert openai_kwargs["api_key"] == "sk-real-openai-xyz"
|
|
532
|
+
# Standard OpenAI endpoint doesn't set api_base (uses default)
|
|
533
|
+
assert "api_base" not in openai_kwargs
|
|
534
|
+
|
|
535
|
+
# Test custom endpoint
|
|
536
|
+
custom_kwargs = caller._get_model_kwargs("custom-provider")
|
|
537
|
+
assert custom_kwargs["api_key"] == "sk-custom-provider-abc"
|
|
538
|
+
assert custom_kwargs["api_base"] == "https://custom-provider.example.com/v1"
|
|
539
|
+
|
|
540
|
+
# Both should be callable with correct keys
|
|
541
|
+
import os
|
|
542
|
+
# Last one will be in OPENAI_API_KEY env var
|
|
543
|
+
assert os.environ.get("OPENAI_API_KEY") in ["sk-real-openai-xyz", "sk-custom-provider-abc"]
|
|
544
|
+
|
|
545
|
+
# Cleanup
|
|
546
|
+
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
|
|
547
|
+
|
|
548
|
+
def test_custom_endpoint_missing_api_key_env_var_raises_error(self):
|
|
549
|
+
"""Test that missing custom API key env var raises appropriate error."""
|
|
550
|
+
# Create config with undefined env var
|
|
551
|
+
llm_config = LLMConfig(
|
|
552
|
+
models={
|
|
553
|
+
"custom-model": ModelConfig(
|
|
554
|
+
model_name="custom-model",
|
|
555
|
+
model_url="https://custom.example.com/v1",
|
|
556
|
+
api_key="${UNDEFINED_CUSTOM_KEY}"
|
|
557
|
+
)
|
|
558
|
+
}
|
|
559
|
+
)
|
|
560
|
+
|
|
561
|
+
caller = LiteLLMCaller(llm_config, debug_mode=True)
|
|
562
|
+
|
|
563
|
+
# Should raise ValueError about missing env var
|
|
564
|
+
with pytest.raises(ValueError, match="Environment variable 'UNDEFINED_CUSTOM_KEY' is not set"):
|
|
565
|
+
caller._get_model_kwargs("custom-model")
|
|
566
|
+
|
|
567
|
+
def test_multiple_endpoints_with_mixed_key_sources(self, monkeypatch):
|
|
568
|
+
"""Test mixture of literal keys, env vars, and .env-loaded keys across multiple endpoints."""
|
|
569
|
+
# Simulate some keys from .env file
|
|
570
|
+
monkeypatch.setenv("OPENAI_API_KEY", "sk-from-dotenv")
|
|
571
|
+
# Some from explicit env vars
|
|
572
|
+
monkeypatch.setenv("CUSTOM_A_KEY", "sk-custom-a-env")
|
|
573
|
+
|
|
574
|
+
llm_config = LLMConfig(
|
|
575
|
+
models={
|
|
576
|
+
"openai-from-dotenv": ModelConfig(
|
|
577
|
+
model_name="gpt-4",
|
|
578
|
+
model_url="https://api.openai.com/v1",
|
|
579
|
+
api_key="${OPENAI_API_KEY}" # Uses .env value
|
|
580
|
+
),
|
|
581
|
+
"custom-from-env": ModelConfig(
|
|
582
|
+
model_name="custom-a",
|
|
583
|
+
model_url="https://custom-a.example.com/v1",
|
|
584
|
+
api_key="${CUSTOM_A_KEY}" # Uses explicit env var
|
|
585
|
+
),
|
|
586
|
+
"custom-literal": ModelConfig(
|
|
587
|
+
model_name="custom-b",
|
|
588
|
+
model_url="https://custom-b.example.com/v1",
|
|
589
|
+
api_key="sk-literal-hardcoded" # Literal value
|
|
590
|
+
),
|
|
591
|
+
}
|
|
592
|
+
)
|
|
593
|
+
|
|
594
|
+
caller = LiteLLMCaller(llm_config, debug_mode=True)
|
|
595
|
+
|
|
596
|
+
# Each should resolve correctly
|
|
597
|
+
kwargs_openai = caller._get_model_kwargs("openai-from-dotenv")
|
|
598
|
+
assert kwargs_openai["api_key"] == "sk-from-dotenv"
|
|
599
|
+
|
|
600
|
+
kwargs_custom_a = caller._get_model_kwargs("custom-from-env")
|
|
601
|
+
assert kwargs_custom_a["api_key"] == "sk-custom-a-env"
|
|
602
|
+
|
|
603
|
+
kwargs_custom_b = caller._get_model_kwargs("custom-literal")
|
|
604
|
+
assert kwargs_custom_b["api_key"] == "sk-literal-hardcoded"
|
|
605
|
+
|
|
606
|
+
# Cleanup
|
|
607
|
+
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
|
|
608
|
+
|
|
609
|
+
def test_empty_api_key_raises_appropriate_error(self):
|
|
610
|
+
"""Test that empty API key (after env var expansion) raises error."""
|
|
611
|
+
# Note: Current implementation may not explicitly check for empty strings
|
|
612
|
+
# This test documents expected behavior
|
|
613
|
+
llm_config = LLMConfig(
|
|
614
|
+
models={
|
|
615
|
+
"model-with-empty-key": ModelConfig(
|
|
616
|
+
model_name="test-model",
|
|
617
|
+
model_url="https://api.example.com/v1",
|
|
618
|
+
api_key="" # Empty string
|
|
619
|
+
)
|
|
620
|
+
}
|
|
621
|
+
)
|
|
622
|
+
|
|
623
|
+
caller = LiteLLMCaller(llm_config, debug_mode=True)
|
|
624
|
+
|
|
625
|
+
# Get kwargs - empty api_key is treated as None/missing
|
|
626
|
+
# The implementation only sets api_key if it's truthy
|
|
627
|
+
kwargs = caller._get_model_kwargs("model-with-empty-key")
|
|
628
|
+
|
|
629
|
+
# Empty string is not passed through (falsy value)
|
|
630
|
+
assert "api_key" not in kwargs
|
|
631
|
+
|
|
632
|
+
def test_switching_between_models_updates_env_correctly(self, monkeypatch):
|
|
633
|
+
"""Test that switching between different model types updates environment correctly."""
|
|
634
|
+
monkeypatch.setenv("ANTHROPIC_API_KEY", "sk-ant-initial")
|
|
635
|
+
|
|
636
|
+
llm_config = LLMConfig(
|
|
637
|
+
models={
|
|
638
|
+
"anthropic-model": ModelConfig(
|
|
639
|
+
model_name="claude-3",
|
|
640
|
+
model_url="https://api.anthropic.com/v1",
|
|
641
|
+
api_key="sk-ant-new"
|
|
642
|
+
),
|
|
643
|
+
"openai-model": ModelConfig(
|
|
644
|
+
model_name="gpt-4",
|
|
645
|
+
model_url="https://api.openai.com/v1",
|
|
646
|
+
api_key="sk-openai-new"
|
|
647
|
+
),
|
|
648
|
+
"custom-model": ModelConfig(
|
|
649
|
+
model_name="custom",
|
|
650
|
+
model_url="https://custom.example.com/v1",
|
|
651
|
+
api_key="sk-custom-new"
|
|
652
|
+
),
|
|
653
|
+
}
|
|
654
|
+
)
|
|
655
|
+
|
|
656
|
+
caller = LiteLLMCaller(llm_config, debug_mode=True)
|
|
657
|
+
|
|
658
|
+
import os
|
|
659
|
+
|
|
660
|
+
# Call Anthropic
|
|
661
|
+
anthropic_kwargs = caller._get_model_kwargs("anthropic-model")
|
|
662
|
+
assert anthropic_kwargs["api_key"] == "sk-ant-new"
|
|
663
|
+
assert os.environ.get("ANTHROPIC_API_KEY") == "sk-ant-new"
|
|
664
|
+
|
|
665
|
+
# Call OpenAI
|
|
666
|
+
openai_kwargs = caller._get_model_kwargs("openai-model")
|
|
667
|
+
assert openai_kwargs["api_key"] == "sk-openai-new"
|
|
668
|
+
assert os.environ.get("OPENAI_API_KEY") == "sk-openai-new"
|
|
669
|
+
|
|
670
|
+
# Call custom (should also set OPENAI_API_KEY as fallback)
|
|
671
|
+
custom_kwargs = caller._get_model_kwargs("custom-model")
|
|
672
|
+
assert custom_kwargs["api_key"] == "sk-custom-new"
|
|
673
|
+
assert os.environ.get("OPENAI_API_KEY") == "sk-custom-new"
|
|
674
|
+
|
|
675
|
+
# Anthropic key should still be set
|
|
676
|
+
assert os.environ.get("ANTHROPIC_API_KEY") == "sk-ant-new"
|
|
677
|
+
|
|
678
|
+
# Cleanup
|
|
679
|
+
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
|
|
680
|
+
monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False)
|
|
681
|
+
|
|
682
|
+
def test_custom_endpoints_with_openai_prefix_in_model_name(self, monkeypatch):
|
|
683
|
+
"""Test custom endpoints that use 'openai/' prefix in model name use correct API keys.
|
|
684
|
+
|
|
685
|
+
LiteLLM uses model name prefixes (e.g., 'openai/', 'anthropic/') to detect providers.
|
|
686
|
+
This test ensures that when we have custom endpoints with model names like
|
|
687
|
+
'openai/custom-model1', each endpoint still gets its own correct API key.
|
|
688
|
+
"""
|
|
689
|
+
monkeypatch.setenv("CUSTOM_ENDPOINT_A_KEY", "sk-custom-a-12345")
|
|
690
|
+
monkeypatch.setenv("CUSTOM_ENDPOINT_B_KEY", "sk-custom-b-67890")
|
|
691
|
+
|
|
692
|
+
llm_config = LLMConfig(
|
|
693
|
+
models={
|
|
694
|
+
"custom-a": ModelConfig(
|
|
695
|
+
model_name="openai/custom-model1", # Has openai/ prefix but custom endpoint
|
|
696
|
+
model_url="https://custom-a.example.com/v1",
|
|
697
|
+
api_key="${CUSTOM_ENDPOINT_A_KEY}"
|
|
698
|
+
),
|
|
699
|
+
"custom-b": ModelConfig(
|
|
700
|
+
model_name="openai/custom-model2", # Has openai/ prefix but custom endpoint
|
|
701
|
+
model_url="https://custom-b.example.com/v1",
|
|
702
|
+
api_key="${CUSTOM_ENDPOINT_B_KEY}"
|
|
703
|
+
),
|
|
704
|
+
}
|
|
705
|
+
)
|
|
706
|
+
|
|
707
|
+
caller = LiteLLMCaller(llm_config, debug_mode=True)
|
|
708
|
+
|
|
709
|
+
# Get kwargs for each custom endpoint
|
|
710
|
+
kwargs_a = caller._get_model_kwargs("custom-a")
|
|
711
|
+
kwargs_b = caller._get_model_kwargs("custom-b")
|
|
712
|
+
|
|
713
|
+
# Each should have its own correct API key in kwargs
|
|
714
|
+
assert kwargs_a["api_key"] == "sk-custom-a-12345"
|
|
715
|
+
assert kwargs_b["api_key"] == "sk-custom-b-67890"
|
|
716
|
+
|
|
717
|
+
# Each should have its own api_base set (custom endpoints)
|
|
718
|
+
assert kwargs_a["api_base"] == "https://custom-a.example.com/v1"
|
|
719
|
+
assert kwargs_b["api_base"] == "https://custom-b.example.com/v1"
|
|
720
|
+
|
|
721
|
+
# Verify the LiteLLM model names don't have prefixes for custom endpoints
|
|
722
|
+
litellm_name_a = caller._get_litellm_model_name("custom-a")
|
|
723
|
+
litellm_name_b = caller._get_litellm_model_name("custom-b")
|
|
724
|
+
|
|
725
|
+
# Custom endpoints should use model_id directly (not add prefix)
|
|
726
|
+
assert litellm_name_a == "openai/custom-model1"
|
|
727
|
+
assert litellm_name_b == "openai/custom-model2"
|
|
728
|
+
|
|
729
|
+
# Cleanup
|
|
730
|
+
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
|
|
731
|
+
|
|
732
|
+
def test_mixed_real_and_custom_openai_endpoints_with_same_prefix(self, monkeypatch):
|
|
733
|
+
"""Test that real OpenAI and custom OpenAI-compatible endpoints are handled correctly.
|
|
734
|
+
|
|
735
|
+
When you have both:
|
|
736
|
+
- A real OpenAI endpoint (api.openai.com)
|
|
737
|
+
- Custom OpenAI-compatible endpoints
|
|
738
|
+
|
|
739
|
+
Each should use its own API key even though they might have similar model name patterns.
|
|
740
|
+
|
|
741
|
+
NOTE: If a custom endpoint URL contains 'openai' in the hostname (e.g.,
|
|
742
|
+
'custom-openai.example.com'), it will be detected as an OpenAI endpoint and get
|
|
743
|
+
the 'openai/' prefix. To avoid this, use URLs without 'openai' in them.
|
|
744
|
+
"""
|
|
745
|
+
monkeypatch.setenv("REAL_OPENAI_KEY", "sk-real-openai-xyz")
|
|
746
|
+
monkeypatch.setenv("CUSTOM_COMPAT_KEY", "sk-custom-compat-abc")
|
|
747
|
+
|
|
748
|
+
llm_config = LLMConfig(
|
|
749
|
+
models={
|
|
750
|
+
"real-openai": ModelConfig(
|
|
751
|
+
model_name="gpt-4o",
|
|
752
|
+
model_url="https://api.openai.com/v1",
|
|
753
|
+
api_key="${REAL_OPENAI_KEY}"
|
|
754
|
+
),
|
|
755
|
+
"custom-compat": ModelConfig(
|
|
756
|
+
model_name="custom-gpt-4",
|
|
757
|
+
# Use URL without 'openai' in it to avoid provider detection
|
|
758
|
+
model_url="https://llm-provider.example.com/v1",
|
|
759
|
+
api_key="${CUSTOM_COMPAT_KEY}"
|
|
760
|
+
),
|
|
761
|
+
}
|
|
762
|
+
)
|
|
763
|
+
|
|
764
|
+
caller = LiteLLMCaller(llm_config, debug_mode=True)
|
|
765
|
+
|
|
766
|
+
# Get kwargs for real OpenAI
|
|
767
|
+
real_kwargs = caller._get_model_kwargs("real-openai")
|
|
768
|
+
assert real_kwargs["api_key"] == "sk-real-openai-xyz"
|
|
769
|
+
# Real OpenAI doesn't set custom api_base
|
|
770
|
+
assert "api_base" not in real_kwargs
|
|
771
|
+
|
|
772
|
+
# Get kwargs for custom endpoint
|
|
773
|
+
custom_kwargs = caller._get_model_kwargs("custom-compat")
|
|
774
|
+
assert custom_kwargs["api_key"] == "sk-custom-compat-abc"
|
|
775
|
+
# Custom endpoint sets api_base
|
|
776
|
+
assert custom_kwargs["api_base"] == "https://llm-provider.example.com/v1"
|
|
777
|
+
|
|
778
|
+
# Verify LiteLLM model names
|
|
779
|
+
assert caller._get_litellm_model_name("real-openai") == "openai/gpt-4o"
|
|
780
|
+
# Custom endpoint without provider keywords in URL gets no prefix
|
|
781
|
+
assert caller._get_litellm_model_name("custom-compat") == "custom-gpt-4"
|
|
782
|
+
|
|
783
|
+
# Cleanup
|
|
784
|
+
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
|
|
785
|
+
|
|
786
|
+
def test_multiple_custom_endpoints_sequential_calls_preserve_keys(self, monkeypatch):
|
|
787
|
+
"""Test that calling multiple custom endpoints in sequence preserves correct API keys.
|
|
788
|
+
|
|
789
|
+
This is critical because the implementation sets OPENAI_API_KEY as a fallback
|
|
790
|
+
for custom endpoints. We need to ensure that when switching between custom
|
|
791
|
+
endpoints, each call still gets the correct API key in kwargs even though
|
|
792
|
+
the env var might have been overwritten.
|
|
793
|
+
"""
|
|
794
|
+
monkeypatch.setenv("CUSTOM_1_KEY", "sk-custom-1")
|
|
795
|
+
monkeypatch.setenv("CUSTOM_2_KEY", "sk-custom-2")
|
|
796
|
+
monkeypatch.setenv("CUSTOM_3_KEY", "sk-custom-3")
|
|
797
|
+
|
|
798
|
+
llm_config = LLMConfig(
|
|
799
|
+
models={
|
|
800
|
+
"custom-1": ModelConfig(
|
|
801
|
+
model_name="model-1",
|
|
802
|
+
model_url="https://custom1.example.com/v1",
|
|
803
|
+
api_key="${CUSTOM_1_KEY}"
|
|
804
|
+
),
|
|
805
|
+
"custom-2": ModelConfig(
|
|
806
|
+
model_name="model-2",
|
|
807
|
+
model_url="https://custom2.example.com/v1",
|
|
808
|
+
api_key="${CUSTOM_2_KEY}"
|
|
809
|
+
),
|
|
810
|
+
"custom-3": ModelConfig(
|
|
811
|
+
model_name="model-3",
|
|
812
|
+
model_url="https://custom3.example.com/v1",
|
|
813
|
+
api_key="${CUSTOM_3_KEY}"
|
|
814
|
+
),
|
|
815
|
+
}
|
|
816
|
+
)
|
|
817
|
+
|
|
818
|
+
caller = LiteLLMCaller(llm_config, debug_mode=True)
|
|
819
|
+
|
|
820
|
+
# Call them in sequence multiple times
|
|
821
|
+
for _ in range(2):
|
|
822
|
+
kwargs_1 = caller._get_model_kwargs("custom-1")
|
|
823
|
+
assert kwargs_1["api_key"] == "sk-custom-1", "Custom-1 should always get its own key"
|
|
824
|
+
|
|
825
|
+
kwargs_2 = caller._get_model_kwargs("custom-2")
|
|
826
|
+
assert kwargs_2["api_key"] == "sk-custom-2", "Custom-2 should always get its own key"
|
|
827
|
+
|
|
828
|
+
kwargs_3 = caller._get_model_kwargs("custom-3")
|
|
829
|
+
assert kwargs_3["api_key"] == "sk-custom-3", "Custom-3 should always get its own key"
|
|
830
|
+
|
|
831
|
+
# Going back to custom-1 should still work
|
|
832
|
+
kwargs_1_again = caller._get_model_kwargs("custom-1")
|
|
833
|
+
assert kwargs_1_again["api_key"] == "sk-custom-1", "Custom-1 should still get its own key"
|
|
834
|
+
|
|
835
|
+
# Cleanup
|
|
836
|
+
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
|