Claude commited on
Commit
e4c6475
Β·
unverified Β·
1 Parent(s): 9c9d382

fix(P0): Auto-detect OpenAI provider from BYOK api_key prefix

Browse files

Bug: When users entered an OpenAI key (sk-...) in Gradio without
a provider parameter, the client factory only checked settings.has_openai_key
(env var) and fell through to HuggingFace, causing V1 chat completion errors.

Fix: Add provider auto-detection from api_key prefix in get_chat_client():
- sk-ant-... β†’ Anthropic (NotImplementedError - not yet supported)
- sk-... β†’ OpenAI (now correctly routed)
- hf_... β†’ Falls through to HuggingFace (default)

Order matters: "sk-ant-" must be checked before "sk-" since both share prefix.

Tests added:
- test_byok_auto_detects_openai_from_key_prefix (the critical fix test)
- test_byok_auto_detects_anthropic_from_key_prefix
- test_byok_hf_token_falls_through_to_huggingface
- test_anthropic_provider_raises_not_implemented

All 310 unit tests pass.

src/clients/factory.py CHANGED
@@ -23,13 +23,14 @@ def get_chat_client(
23
 
24
  Auto-detection priority:
25
  1. Explicit provider parameter
26
- 2. OpenAI key (Best Function Calling)
27
- 3. Gemini key (Best Context/Cost)
28
- 4. HuggingFace (Free Fallback)
 
29
 
30
  Args:
31
  provider: Force specific provider ("openai", "gemini", "huggingface")
32
- api_key: Override API key for the provider
33
  model_id: Override default model ID
34
  **kwargs: Additional arguments for the client
35
 
@@ -38,13 +39,23 @@ def get_chat_client(
38
 
39
  Raises:
40
  ValueError: If an unsupported provider is explicitly requested
41
- NotImplementedError: If Gemini is explicitly requested (not yet implemented)
42
  """
43
  # Normalize provider to lowercase for case-insensitive matching
44
  normalized = provider.lower() if provider is not None else None
45
 
 
 
 
 
 
 
 
 
 
 
46
  # Validate explicit provider requests early
47
- valid_providers = (None, "openai", "gemini", "huggingface")
48
  if normalized not in valid_providers:
49
  raise ValueError(f"Unsupported provider: {provider!r}")
50
 
@@ -57,7 +68,15 @@ def get_chat_client(
57
  **kwargs,
58
  )
59
 
60
- # 2. Gemini (High Performance / Alternative)
 
 
 
 
 
 
 
 
61
  if normalized == "gemini":
62
  # Explicit request for Gemini - fail loudly
63
  raise NotImplementedError("Gemini client not yet implemented (Planned Phase 4)")
@@ -66,7 +85,7 @@ def get_chat_client(
66
  # Implicit (has key but not explicit) - log warning and fall through
67
  logger.warning("Gemini key detected but client not yet implemented; falling back")
68
 
69
- # 3. HuggingFace (Free Fallback)
70
  # This is the default if no other keys are present
71
  logger.info("Using HuggingFace Chat Client (Free Tier)")
72
  return HuggingFaceChatClient(
 
23
 
24
  Auto-detection priority:
25
  1. Explicit provider parameter
26
+ 2. API key prefix detection (sk- β†’ OpenAI, sk-ant- β†’ Anthropic)
27
+ 3. OpenAI key from env (Best Function Calling)
28
+ 4. Gemini key from env (Best Context/Cost)
29
+ 5. HuggingFace (Free Fallback)
30
 
31
  Args:
32
  provider: Force specific provider ("openai", "gemini", "huggingface")
33
+ api_key: Override API key for the provider (auto-detects provider from prefix)
34
  model_id: Override default model ID
35
  **kwargs: Additional arguments for the client
36
 
 
39
 
40
  Raises:
41
  ValueError: If an unsupported provider is explicitly requested
42
+ NotImplementedError: If Gemini or Anthropic is requested (not yet implemented)
43
  """
44
  # Normalize provider to lowercase for case-insensitive matching
45
  normalized = provider.lower() if provider is not None else None
46
 
47
+ # FIX: Auto-detect provider from API key prefix when not explicitly set
48
+ # This enables BYOK (Bring Your Own Key) from Gradio without explicit provider
49
+ # Order matters: "sk-ant-" must be checked before "sk-" (both start with "sk-")
50
+ if normalized is None and api_key:
51
+ if api_key.startswith("sk-ant-"):
52
+ normalized = "anthropic"
53
+ elif api_key.startswith("sk-"):
54
+ normalized = "openai"
55
+ # HF tokens start with "hf_" - no auto-detection needed (falls through to default)
56
+
57
  # Validate explicit provider requests early
58
+ valid_providers = (None, "openai", "anthropic", "gemini", "huggingface")
59
  if normalized not in valid_providers:
60
  raise ValueError(f"Unsupported provider: {provider!r}")
61
 
 
68
  **kwargs,
69
  )
70
 
71
+ # 2. Anthropic (Detected from sk-ant- prefix or explicit)
72
+ if normalized == "anthropic":
73
+ # Anthropic key was detected or explicitly requested - fail loudly
74
+ raise NotImplementedError(
75
+ "Anthropic client not yet implemented. "
76
+ "Use OpenAI key (sk-...) or leave empty for free HuggingFace tier."
77
+ )
78
+
79
+ # 3. Gemini (High Performance / Alternative)
80
  if normalized == "gemini":
81
  # Explicit request for Gemini - fail loudly
82
  raise NotImplementedError("Gemini client not yet implemented (Planned Phase 4)")
 
85
  # Implicit (has key but not explicit) - log warning and fall through
86
  logger.warning("Gemini key detected but client not yet implemented; falling back")
87
 
88
+ # 4. HuggingFace (Free Fallback)
89
  # This is the default if no other keys are present
90
  logger.info("Using HuggingFace Chat Client (Free Tier)")
91
  return HuggingFaceChatClient(
tests/unit/clients/test_chat_client_factory.py CHANGED
@@ -91,8 +91,73 @@ class TestChatClientFactory:
91
  from src.clients.factory import get_chat_client
92
 
93
  with pytest.raises(ValueError, match="Unsupported provider"):
 
 
 
 
 
 
 
 
 
 
 
94
  get_chat_client(provider="anthropic")
95
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  def test_provider_is_case_insensitive(self) -> None:
97
  """Provider matching should be case-insensitive."""
98
  with patch("src.clients.factory.settings") as mock_settings:
 
91
  from src.clients.factory import get_chat_client
92
 
93
  with pytest.raises(ValueError, match="Unsupported provider"):
94
+ get_chat_client(provider="invalid_provider")
95
+
96
+ def test_anthropic_provider_raises_not_implemented(self) -> None:
97
+ """Anthropic provider should raise NotImplementedError (not yet implemented)."""
98
+ with patch("src.clients.factory.settings") as mock_settings:
99
+ mock_settings.has_openai_key = False
100
+ mock_settings.has_gemini_key = False
101
+
102
+ from src.clients.factory import get_chat_client
103
+
104
+ with pytest.raises(NotImplementedError, match="Anthropic client not yet implemented"):
105
  get_chat_client(provider="anthropic")
106
 
107
+ def test_byok_auto_detects_openai_from_key_prefix(self) -> None:
108
+ """BYOK: api_key starting with 'sk-' should auto-select OpenAI without explicit provider.
109
+
110
+ This is the critical BYOK (Bring Your Own Key) test case:
111
+ - User enters 'sk-...' key in Gradio
112
+ - No explicit provider parameter
113
+ - No OPENAI_API_KEY in env (settings.has_openai_key = False)
114
+ - Should auto-detect OpenAI from the key prefix
115
+ """
116
+ with patch("src.clients.factory.settings") as mock_settings:
117
+ mock_settings.has_openai_key = False # No env key
118
+ mock_settings.has_gemini_key = False
119
+ mock_settings.openai_api_key = None
120
+ mock_settings.openai_model = "gpt-5"
121
+
122
+ from src.clients.factory import get_chat_client
123
+
124
+ # BYOK: Pass api_key without explicit provider
125
+ client = get_chat_client(api_key="sk-user-provided-key")
126
+
127
+ # Should auto-detect OpenAI from 'sk-' prefix
128
+ assert "OpenAI" in type(client).__name__
129
+
130
+ def test_byok_auto_detects_anthropic_from_key_prefix(self) -> None:
131
+ """BYOK: api_key starting with 'sk-ant-' should auto-detect Anthropic.
132
+
133
+ Anthropic keys start with 'sk-ant-' which is a superset of 'sk-'.
134
+ Detection must check 'sk-ant-' first to avoid misdetecting as OpenAI.
135
+ """
136
+ with patch("src.clients.factory.settings") as mock_settings:
137
+ mock_settings.has_openai_key = False
138
+ mock_settings.has_gemini_key = False
139
+
140
+ from src.clients.factory import get_chat_client
141
+
142
+ # BYOK: Anthropic key should raise NotImplementedError (not fall to HuggingFace!)
143
+ with pytest.raises(NotImplementedError, match="Anthropic client not yet implemented"):
144
+ get_chat_client(api_key="sk-ant-user-anthropic-key")
145
+
146
+ def test_byok_hf_token_falls_through_to_huggingface(self) -> None:
147
+ """BYOK: HuggingFace tokens (hf_...) should use HuggingFace client."""
148
+ with patch("src.clients.factory.settings") as mock_settings:
149
+ mock_settings.has_openai_key = False
150
+ mock_settings.has_gemini_key = False
151
+ mock_settings.huggingface_model = "Qwen/Qwen2.5-7B-Instruct"
152
+ mock_settings.hf_token = None
153
+
154
+ from src.clients.factory import get_chat_client
155
+
156
+ # HF tokens don't trigger auto-detection, falls through to HuggingFace
157
+ client = get_chat_client(api_key="hf_user_provided_token")
158
+
159
+ assert "HuggingFace" in type(client).__name__
160
+
161
  def test_provider_is_case_insensitive(self) -> None:
162
  """Provider matching should be case-insensitive."""
163
  with patch("src.clients.factory.settings") as mock_settings: