From 06dd99a0478fb548d691cc5b61fc73b83c63e143 Mon Sep 17 00:00:00 2001 From: Oliver Kurz Date: Fri, 18 Sep 2026 10:10:30 +0200 Subject: [PATCH 1/2] feat: Add authentication token support for LLM investigation Motivation: Allow connecting to authenticated LLM APIs (e.g. OpenAI, Anthropic, Gemini proxies). Design Choices: Read LLM_API_TOKEN from the environment and inject it as a Bearer token in the Authorization header of the HTTP POST request. Benefits: Enables openqa-llm-investigate to be used with a wider range of hosted or remote LLM models. --- openqa-llm-investigate | 8 ++++++-- tests/test_openqa_llm_investigate.py | 13 +++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/openqa-llm-investigate b/openqa-llm-investigate index 8080d38a..5ed62ac8 100755 --- a/openqa-llm-investigate +++ b/openqa-llm-investigate @@ -191,6 +191,7 @@ def _perform_investigation( llm_url: str, llm_model: str, timeout: int, + llm_token: str | None, ) -> None: # Check for existing LLM investigation comments comments = fetch_json(client, f"{base_url}/api/v1/jobs/{job_id}/comments", default=[]) @@ -306,8 +307,10 @@ Provide exactly 3 sentences answering: "temperature": 0.3, } + headers = {"Authorization": f"Bearer {llm_token}"} if llm_token else {} + try: - res = client.post(llm_url, json=req, timeout=timeout) + res = client.post(llm_url, json=req, headers=headers, timeout=timeout) res.raise_for_status() res_json = res.json() llm_response = res_json["choices"][0]["message"]["content"] @@ -367,10 +370,11 @@ def investigate( llm_url = os.environ.get("LLM_API_URL", "http://localhost:8080/v1/chat/completions") llm_model = os.environ.get("LLM_MODEL", "gemma-4-26B-A4B-it") + llm_token = os.environ.get("LLM_API_TOKEN") transport = RetryTransport(retries=retries) with httpx.Client(transport=transport, headers={"User-Agent": USER_AGENT}) as client: - _perform_investigation(client, base_url, job_id, force, dry, llm_url, llm_model, timeout) + _perform_investigation(client, base_url, job_id, force, dry, llm_url, llm_model, timeout, llm_token) if __name__ == "__main__": diff --git a/tests/test_openqa_llm_investigate.py b/tests/test_openqa_llm_investigate.py index 71468a92..7372dc29 100644 --- a/tests/test_openqa_llm_investigate.py +++ b/tests/test_openqa_llm_investigate.py @@ -157,6 +157,19 @@ def test_investigate_cmd(mocker: MockerFixture) -> None: assert "BISECT: YES" in mock_post.call_args[0][2] +def test_investigate_cmd_with_token(mocker: MockerFixture) -> None: + mock_post = mocker.patch("llm_investigate.post_comment") + mocker.patch("builtins.print") + mocker.patch.dict("os.environ", {"LLM_API_TOKEN": "my-secret-token"}) + client = setup_mock_client(mocker) + + llm_investigate.investigate("123") + + client.post.assert_called_once() + assert client.post.call_args[1]["headers"] == {"Authorization": "Bearer my-secret-token"} + mock_post.assert_called_once() + + def test_investigate_cmd_passed_job(mocker: MockerFixture) -> None: mock_print = mocker.patch("builtins.print") setup_mock_client(mocker, overrides={"api/v1/jobs": {"job": {"id": 123, "result": "passed"}}}) From b44e394f05e65d7c7fd86c8193f18758f5059717 Mon Sep 17 00:00:00 2001 From: Oliver Kurz Date: Fri, 18 Sep 2026 10:13:11 +0200 Subject: [PATCH 2/2] feat: Add CLI options for LLM configuration Motivation: Allow users to specify LLM settings via CLI arguments. Design Choices: Updated typer options to include --llm-url, --llm-model, and --llm-token, mapping them to their respective envvars. Benefits: Better usability for configuring the LLM API endpoint. --- openqa-llm-investigate | 13 +++++++++---- tests/test_openqa_llm_investigate.py | 2 +- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/openqa-llm-investigate b/openqa-llm-investigate index 5ed62ac8..1bc193d4 100755 --- a/openqa-llm-investigate +++ b/openqa-llm-investigate @@ -360,6 +360,15 @@ def investigate( retries: Annotated[int, typer.Option("--retries", help="Number of retries on retry-worthy HTTP responses")] = int( os.environ.get("LLM_RETRIES", "7") ), + llm_url: Annotated[ + str, typer.Option("--llm-url", envvar="LLM_API_URL", help="URL for the LLM API endpoint") + ] = os.environ.get("LLM_API_URL", "http://localhost:8080/v1/chat/completions"), + llm_model: Annotated[ + str, typer.Option("--llm-model", envvar="LLM_MODEL", help="The LLM model to use") + ] = os.environ.get("LLM_MODEL", "gemma-4-26B-A4B-it"), + llm_token: Annotated[ + str | None, typer.Option("--llm-token", envvar="LLM_API_TOKEN", help="API token for authentication") + ] = os.environ.get("LLM_API_TOKEN"), ) -> None: setup_logging(verbose, quiet) base_url, job_id = parse_job_url(job_url) @@ -368,10 +377,6 @@ def investigate( log.error("Invalid job ID: %s", job_id) sys.exit(1) - llm_url = os.environ.get("LLM_API_URL", "http://localhost:8080/v1/chat/completions") - llm_model = os.environ.get("LLM_MODEL", "gemma-4-26B-A4B-it") - llm_token = os.environ.get("LLM_API_TOKEN") - transport = RetryTransport(retries=retries) with httpx.Client(transport=transport, headers={"User-Agent": USER_AGENT}) as client: _perform_investigation(client, base_url, job_id, force, dry, llm_url, llm_model, timeout, llm_token) diff --git a/tests/test_openqa_llm_investigate.py b/tests/test_openqa_llm_investigate.py index 7372dc29..5136e43b 100644 --- a/tests/test_openqa_llm_investigate.py +++ b/tests/test_openqa_llm_investigate.py @@ -163,7 +163,7 @@ def test_investigate_cmd_with_token(mocker: MockerFixture) -> None: mocker.patch.dict("os.environ", {"LLM_API_TOKEN": "my-secret-token"}) client = setup_mock_client(mocker) - llm_investigate.investigate("123") + llm_investigate.investigate("123", llm_token="my-secret-token") client.post.assert_called_once() assert client.post.call_args[1]["headers"] == {"Authorization": "Bearer my-secret-token"}