From 4f9da854782904dfddcc727fa95f4358c80dec5a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 18 May 2026 09:50:24 +0000 Subject: [PATCH 1/3] Initial plan From ee1fb635230daea616b1d144abbfb79819352c54 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 18 May 2026 09:54:41 +0000 Subject: [PATCH 2/3] fix(ai): allow LM Studio test provider without API key Agent-Logs-Url: https://github.com/SweetingTech/TableTop_DM/sessions/3c220654-3035-4d4f-97fc-f4466095c5ca Co-authored-by: SweetingTech <4968985+SweetingTech@users.noreply.github.com> --- app.py | 5 +- .../contracts/test_control_plane_endpoints.py | 61 ++++++++++++++++++- 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/app.py b/app.py index 463f7e1..f7d28db 100644 --- a/app.py +++ b/app.py @@ -289,7 +289,10 @@ def _openai_client_for(provider: str, base_url: str | None): from openai import OpenAI final_base_url = resolve_provider_base_url(provider, base_url) - api_key = os.environ.get("OPENAI_API_KEY", "dev-local") + if (provider or "").lower() == "lmstudio": + api_key = os.environ.get("OPENAI_API_KEY") or "lmstudio-local" + else: + api_key = os.environ.get("OPENAI_API_KEY", "dev-local") return OpenAI(api_key=api_key, base_url=final_base_url) diff --git a/tests/contracts/test_control_plane_endpoints.py b/tests/contracts/test_control_plane_endpoints.py index d991b33..f267fbc 100644 --- a/tests/contracts/test_control_plane_endpoints.py +++ b/tests/contracts/test_control_plane_endpoints.py @@ -1,6 +1,7 @@ import pytest +from types import SimpleNamespace -from app import app +from app import app, _openai_client_for pytestmark = pytest.mark.contracts @@ -36,3 +37,61 @@ def test_ai_test_provider_mock(): r = client.post("/api/ai/test_provider", json={"provider": "mock", "model": "mock-model"}) assert r.status_code == 200 assert r.get_json()["ok"] is True + + +def test_ai_test_provider_lmstudio_allows_empty_env_api_key(monkeypatch): + captured = {} + + class _FakeModels: + @staticmethod + def list(): + return SimpleNamespace(data=[SimpleNamespace(id="local-model")]) + + class _FakeCompletions: + @staticmethod + def create(**_kwargs): + return SimpleNamespace( + choices=[SimpleNamespace(message=SimpleNamespace(content="ok"))] + ) + + class _FakeChat: + completions = _FakeCompletions() + + class _FakeOpenAI: + def __init__(self, api_key, base_url): + captured["api_key"] = api_key + captured["base_url"] = base_url + self.models = _FakeModels() + self.chat = _FakeChat() + + monkeypatch.setenv("OPENAI_API_KEY", "") + monkeypatch.setattr("openai.OpenAI", _FakeOpenAI) + + with app.test_client() as client: + r = client.post( + "/api/ai/test_provider", + json={ + "provider": "lmstudio", + "base_url": "http://localhost:1234/v1", + "model": "local-model", + }, + ) + assert r.status_code == 200 + assert r.get_json()["ok"] is True + assert captured["api_key"] == "lmstudio-local" + + +def test_openai_client_for_non_lmstudio_keeps_existing_env_behavior(monkeypatch): + captured = {} + + class _FakeOpenAI: + def __init__(self, api_key, base_url): + captured["api_key"] = api_key + captured["base_url"] = base_url + + monkeypatch.setenv("OPENAI_API_KEY", "") + monkeypatch.setattr("openai.OpenAI", _FakeOpenAI) + + _openai_client_for("openai", None) + + assert captured["api_key"] == "" From 7a998cbdf7e229dfd04d41834edcc8915eef6982 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 18 May 2026 09:56:11 +0000 Subject: [PATCH 3/3] test(ai): expand non-LMStudio key behavior regression coverage Agent-Logs-Url: https://github.com/SweetingTech/TableTop_DM/sessions/3c220654-3035-4d4f-97fc-f4466095c5ca Co-authored-by: SweetingTech <4968985+SweetingTech@users.noreply.github.com> --- app.py | 2 +- tests/contracts/test_control_plane_endpoints.py | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/app.py b/app.py index f7d28db..063bdd5 100644 --- a/app.py +++ b/app.py @@ -289,7 +289,7 @@ def _openai_client_for(provider: str, base_url: str | None): from openai import OpenAI final_base_url = resolve_provider_base_url(provider, base_url) - if (provider or "").lower() == "lmstudio": + if provider and provider.lower() == "lmstudio": api_key = os.environ.get("OPENAI_API_KEY") or "lmstudio-local" else: api_key = os.environ.get("OPENAI_API_KEY", "dev-local") diff --git a/tests/contracts/test_control_plane_endpoints.py b/tests/contracts/test_control_plane_endpoints.py index f267fbc..a83bfcc 100644 --- a/tests/contracts/test_control_plane_endpoints.py +++ b/tests/contracts/test_control_plane_endpoints.py @@ -81,7 +81,10 @@ def __init__(self, api_key, base_url): assert captured["api_key"] == "lmstudio-local" -def test_openai_client_for_non_lmstudio_keeps_existing_env_behavior(monkeypatch): +@pytest.mark.parametrize("provider", ["openai", "ollama", "openrouter"]) +def test_openai_client_for_non_lmstudio_keeps_existing_env_behavior( + monkeypatch, provider +): captured = {} class _FakeOpenAI: @@ -92,6 +95,6 @@ def __init__(self, api_key, base_url): monkeypatch.setenv("OPENAI_API_KEY", "") monkeypatch.setattr("openai.OpenAI", _FakeOpenAI) - _openai_client_for("openai", None) + _openai_client_for(provider, None) assert captured["api_key"] == ""