Skip to content

Commit 67e038c

Browse files
mattflesebclaude
authored
feat(letsgo): add Gemini and Azure support (#5706)
Add remote::gemini and remote::azure to the provider candidates probed by `ogx letsgo`. Both require API credentials that are checked before any network probe is attempted. Extend _probe_endpoint with an optional api_key_header parameter to support providers that use a custom auth header instead of Authorization: Bearer. Use it for Anthropic (x-api-key) and Azure (api-key), and drop the redundant x-api-key header from the default Bearer path. Signed-off-by: Sébastien Han <seb@redhat.com> Co-authored-by: Sébastien Han <seb@redhat.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent ef8cf45 commit 67e038c

2 files changed

Lines changed: 72 additions & 14 deletions

File tree

src/ogx/cli/stack/lets_go.py

Lines changed: 52 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -206,11 +206,20 @@ def _autodetect_providers() -> str:
206206
when the key environment variable is not set.
207207
"""
208208
candidates = [
209-
# provider_type, env_for_base_url, default_base_url, probe_path, requires_api_key, api_key_env, extra_headers
210-
("remote::ollama", "OLLAMA_URL", "http://localhost:11434/v1", "models", False, None, {}),
211-
("remote::vllm", "VLLM_URL", "http://localhost:8000/v1", "health", False, None, {}),
212-
("remote::llama-cpp-server", "LLAMA_CPP_SERVER_URL", "http://localhost:8080/v1", "models", False, None, {}),
213-
("remote::openai", "OPENAI_BASE_URL", "https://api.openai.com/v1", "models", True, "OPENAI_API_KEY", {}),
209+
# provider_type, env_for_base_url, default_base_url, probe_path, requires_api_key, api_key_env, extra_headers, api_key_header
210+
("remote::ollama", "OLLAMA_URL", "http://localhost:11434/v1", "models", False, None, {}, None),
211+
("remote::vllm", "VLLM_URL", "http://localhost:8000/v1", "health", False, None, {}, None),
212+
(
213+
"remote::llama-cpp-server",
214+
"LLAMA_CPP_SERVER_URL",
215+
"http://localhost:8080/v1",
216+
"models",
217+
False,
218+
None,
219+
{},
220+
None,
221+
),
222+
("remote::openai", "OPENAI_BASE_URL", "https://api.openai.com/v1", "models", True, "OPENAI_API_KEY", {}, None),
214223
(
215224
"remote::llama-openai-compat",
216225
"LLAMA_API_BASE_URL",
@@ -219,6 +228,7 @@ def _autodetect_providers() -> str:
219228
True,
220229
"LLAMA_API_KEY",
221230
{},
231+
None,
222232
),
223233
(
224234
"remote::anthropic",
@@ -228,12 +238,42 @@ def _autodetect_providers() -> str:
228238
True,
229239
"ANTHROPIC_API_KEY",
230240
{"anthropic-version": "2023-06-01"},
241+
"x-api-key",
242+
),
243+
(
244+
"remote::gemini",
245+
None,
246+
"https://generativelanguage.googleapis.com/v1beta/openai",
247+
"models",
248+
True,
249+
"GEMINI_API_KEY",
250+
{},
251+
None,
252+
),
253+
(
254+
"remote::azure",
255+
"AZURE_API_BASE",
256+
"",
257+
"openai/models?api-version=2024-12-01-preview",
258+
True,
259+
"AZURE_API_KEY",
260+
{},
261+
"api-key",
231262
),
232263
]
233264

234265
passed: list[str] = []
235266
cprint("Scanning for available providers...", color="cyan")
236-
for provider_type, base_env, default_base, probe_path, requires_key, key_env, extra_headers in candidates:
267+
for (
268+
provider_type,
269+
base_env,
270+
default_base,
271+
probe_path,
272+
requires_key,
273+
key_env,
274+
extra_headers,
275+
api_key_header,
276+
) in candidates:
237277
env_val: str | None = os.getenv(base_env) if base_env else None
238278
if env_val:
239279
base = env_val
@@ -242,7 +282,7 @@ def _autodetect_providers() -> str:
242282
base = default_base
243283
base_source = "default"
244284

245-
status = _probe_endpoint(base, probe_path, requires_key, key_env, extra_headers)
285+
status = _probe_endpoint(base, probe_path, requires_key, key_env, extra_headers, api_key_header)
246286

247287
# Build annotation parts
248288
parts = [f"{base}, {base_source}"]
@@ -290,6 +330,7 @@ def _probe_endpoint(
290330
requires_key: bool,
291331
key_env: str | None,
292332
extra_headers: dict[str, str] | None = None,
333+
api_key_header: str | None = None,
293334
) -> _ProbeStatus:
294335
"""Perform a lightweight HTTP probe for a provider."""
295336
if not base_url:
@@ -302,8 +343,10 @@ def _probe_endpoint(
302343
if not key_env or not os.getenv(key_env):
303344
return _ProbeStatus.NO_KEY
304345
key: str = os.getenv(key_env, "")
305-
headers["Authorization"] = f"Bearer {key}"
306-
headers["x-api-key"] = key
346+
if api_key_header:
347+
headers[api_key_header] = key
348+
else:
349+
headers["Authorization"] = f"Bearer {key}"
307350

308351
try:
309352
resp = cast(httpx.Response, httpx.get(url, headers=headers, timeout=2.0))

tests/unit/cli/test_stack_lets_go.py

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ def test_auth_headers_set_when_key_present(self, monkeypatch: pytest.MonkeyPatch
196196
_probe_endpoint("https://api.openai.com/v1", "models", True, "OPENAI_API_KEY")
197197
headers = mock_get.call_args.kwargs["headers"]
198198
assert headers["Authorization"] == "Bearer sk-secret"
199-
assert headers["x-api-key"] == "sk-secret"
199+
assert "x-api-key" not in headers
200200

201201

202202
class TestAutodetect:
@@ -230,14 +230,19 @@ def test_autodetect_all_ok(self, mock_probe: MagicMock):
230230
assert "inference=remote::anthropic" in parts
231231
assert "files=inline::localfs" in parts
232232
assert "responses=inline::builtin" in parts
233-
assert len(parts) == 12 # 6 probed + 6 inline
233+
assert len(parts) == 14 # 8 probed + 6 inline
234234

235235
@patch("ogx.cli.stack.lets_go._probe_endpoint")
236236
def test_autodetect_only_ollama(self, mock_probe: MagicMock):
237237
from ogx.cli.stack.lets_go import _autodetect_providers
238238

239239
def side_effect(
240-
base_url: str, probe_path: str, requires_key: bool, key_env: object, extra_headers: object = None
240+
base_url: str,
241+
probe_path: str,
242+
requires_key: bool,
243+
key_env: object,
244+
extra_headers: object = None,
245+
api_key_header: object = None,
241246
) -> _ProbeStatus:
242247
if "11434" in base_url:
243248
return _ProbeStatus.OK
@@ -258,7 +263,12 @@ def test_autodetect_uses_env_var_base_url(self, mock_probe: MagicMock, monkeypat
258263
captured: list[str] = []
259264

260265
def side_effect(
261-
base_url: str, probe_path: str, requires_key: bool, key_env: object, extra_headers: object = None
266+
base_url: str,
267+
probe_path: str,
268+
requires_key: bool,
269+
key_env: object,
270+
extra_headers: object = None,
271+
api_key_header: object = None,
262272
) -> _ProbeStatus:
263273
captured.append(base_url)
264274
return _ProbeStatus.UNREACHABLE
@@ -276,7 +286,12 @@ def test_autodetect_result_order_matches_candidate_order(
276286
monkeypatch.setenv("OPENAI_API_KEY", "sk-test")
277287

278288
def side_effect(
279-
base_url: str, probe_path: str, requires_key: bool, key_env: object, extra_headers: object = None
289+
base_url: str,
290+
probe_path: str,
291+
requires_key: bool,
292+
key_env: object,
293+
extra_headers: object = None,
294+
api_key_header: object = None,
280295
) -> _ProbeStatus:
281296
if "11434" in base_url or "openai.com" in base_url:
282297
return _ProbeStatus.OK

0 commit comments

Comments
 (0)