Implemented provider-neutral OpenAI-compatible LLM endpoint support
This commit is contained in:
174
tests/test_framework_llm.py
Normal file
174
tests/test_framework_llm.py
Normal file
@@ -0,0 +1,174 @@
|
||||
import sys
|
||||
import types
|
||||
|
||||
import pytest
|
||||
|
||||
from audita.core.config import AuditaConfig
|
||||
from audita.core.errors import AuditaLLMError
|
||||
from audita.framework.llm import OpenAICompatibleStructuredLLMClient
|
||||
|
||||
|
||||
class DummyResponseModel:
|
||||
pass
|
||||
|
||||
|
||||
def _config(**overrides):
|
||||
base = AuditaConfig.from_sources(env={})
|
||||
data = {
|
||||
"api_key": "test-key",
|
||||
"module_keys": base.module_keys,
|
||||
"model": base.model,
|
||||
"base_url": base.base_url,
|
||||
"max_retries": base.max_retries,
|
||||
"max_section_tokens": base.max_section_tokens,
|
||||
"glossary_confidence_threshold": base.glossary_confidence_threshold,
|
||||
"grammar_confidence_threshold": base.grammar_confidence_threshold,
|
||||
"homophones_confidence_threshold": base.homophones_confidence_threshold,
|
||||
"spoken_word_confidence_threshold": base.spoken_word_confidence_threshold,
|
||||
"normalize_max_segment_gap": base.normalize_max_segment_gap,
|
||||
"normalize_ellipsis_gap": base.normalize_ellipsis_gap,
|
||||
"normalize_max_segment_duration": base.normalize_max_segment_duration,
|
||||
"normalize_max_segment_tokens": base.normalize_max_segment_tokens,
|
||||
"work_dir": base.work_dir,
|
||||
"work_dir_retention": base.work_dir_retention,
|
||||
}
|
||||
data.update(overrides)
|
||||
return AuditaConfig(**data)
|
||||
|
||||
|
||||
def _install_fake_llm_modules(monkeypatch):
|
||||
create_calls = []
|
||||
openai_inits = []
|
||||
|
||||
class FakePatchedClient:
|
||||
def __init__(self):
|
||||
self.chat = types.SimpleNamespace(completions=types.SimpleNamespace(create=self._create))
|
||||
|
||||
def _create(self, **kwargs):
|
||||
create_calls.append(kwargs)
|
||||
return {"ok": True}
|
||||
|
||||
class FakeOpenAI:
|
||||
def __init__(self, *, api_key, base_url):
|
||||
openai_inits.append({"api_key": api_key, "base_url": base_url})
|
||||
|
||||
fake_instructor = types.SimpleNamespace(
|
||||
Mode=types.SimpleNamespace(TOOLS="TOOLS"),
|
||||
patch=lambda client, mode: FakePatchedClient(),
|
||||
)
|
||||
fake_openai = types.SimpleNamespace(OpenAI=FakeOpenAI)
|
||||
monkeypatch.setitem(sys.modules, "instructor", fake_instructor)
|
||||
monkeypatch.setitem(sys.modules, "openai", fake_openai)
|
||||
return create_calls, openai_inits
|
||||
|
||||
|
||||
def test_openrouter_requests_strip_prefix_and_include_extra_body(monkeypatch):
|
||||
create_calls, _ = _install_fake_llm_modules(monkeypatch)
|
||||
client = OpenAICompatibleStructuredLLMClient()
|
||||
|
||||
client.run_structured(
|
||||
stage_name="test-stage",
|
||||
messages=[{"role": "user", "content": "Hello"}],
|
||||
response_model=DummyResponseModel,
|
||||
config=_config(model="openrouter/google/gemma-4-31b-it"),
|
||||
)
|
||||
|
||||
assert create_calls == [
|
||||
{
|
||||
"model": "google/gemma-4-31b-it",
|
||||
"messages": [{"role": "user", "content": "Hello"}],
|
||||
"response_model": DummyResponseModel,
|
||||
"max_retries": 3,
|
||||
"extra_body": {"provider": {"require_parameters": True}},
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_openrouter_default_base_url_uses_openrouter_request_shape(monkeypatch):
|
||||
create_calls, _ = _install_fake_llm_modules(monkeypatch)
|
||||
client = OpenAICompatibleStructuredLLMClient()
|
||||
|
||||
client.run_structured(
|
||||
stage_name="test-stage",
|
||||
messages=[{"role": "user", "content": "Hello"}],
|
||||
response_model=DummyResponseModel,
|
||||
config=_config(model="google/gemma-4-31b-it"),
|
||||
)
|
||||
|
||||
assert create_calls == [
|
||||
{
|
||||
"model": "google/gemma-4-31b-it",
|
||||
"messages": [{"role": "user", "content": "Hello"}],
|
||||
"response_model": DummyResponseModel,
|
||||
"max_retries": 3,
|
||||
"extra_body": {"provider": {"require_parameters": True}},
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_generic_endpoint_requests_keep_model_and_omit_extra_body(monkeypatch):
|
||||
create_calls, _ = _install_fake_llm_modules(monkeypatch)
|
||||
client = OpenAICompatibleStructuredLLMClient()
|
||||
|
||||
client.run_structured(
|
||||
stage_name="test-stage",
|
||||
messages=[{"role": "user", "content": "Hello"}],
|
||||
response_model=DummyResponseModel,
|
||||
config=_config(
|
||||
model="meta-llama/Llama-3.1-8B-Instruct",
|
||||
base_url="http://localhost:8000/v1",
|
||||
),
|
||||
)
|
||||
|
||||
assert create_calls == [
|
||||
{
|
||||
"model": "meta-llama/Llama-3.1-8B-Instruct",
|
||||
"messages": [{"role": "user", "content": "Hello"}],
|
||||
"response_model": DummyResponseModel,
|
||||
"max_retries": 3,
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_client_cache_identity_uses_api_key_and_base_url(monkeypatch):
|
||||
_, openai_inits = _install_fake_llm_modules(monkeypatch)
|
||||
client = OpenAICompatibleStructuredLLMClient()
|
||||
first = _config(api_key="key-1", base_url="http://localhost:8000/v1")
|
||||
second = _config(api_key="key-1", base_url="http://localhost:8000/v1")
|
||||
third = _config(api_key="key-1", base_url="https://api.openai.com/v1")
|
||||
|
||||
client.run_structured(
|
||||
stage_name="one",
|
||||
messages=[{"role": "user", "content": "Hello"}],
|
||||
response_model=DummyResponseModel,
|
||||
config=first,
|
||||
)
|
||||
client.run_structured(
|
||||
stage_name="two",
|
||||
messages=[{"role": "user", "content": "Hello"}],
|
||||
response_model=DummyResponseModel,
|
||||
config=second,
|
||||
)
|
||||
client.run_structured(
|
||||
stage_name="three",
|
||||
messages=[{"role": "user", "content": "Hello"}],
|
||||
response_model=DummyResponseModel,
|
||||
config=third,
|
||||
)
|
||||
|
||||
assert openai_inits == [
|
||||
{"api_key": "key-1", "base_url": "http://localhost:8000/v1"},
|
||||
{"api_key": "key-1", "base_url": "https://api.openai.com/v1"},
|
||||
]
|
||||
|
||||
|
||||
def test_missing_api_key_error_is_provider_neutral():
|
||||
client = OpenAICompatibleStructuredLLMClient()
|
||||
|
||||
with pytest.raises(AuditaLLMError, match="AUDITA_LLM_API_KEY"):
|
||||
client.run_structured(
|
||||
stage_name="test-stage",
|
||||
messages=[{"role": "user", "content": "Hello"}],
|
||||
response_model=DummyResponseModel,
|
||||
config=_config(api_key=None),
|
||||
)
|
||||
@@ -20,6 +20,7 @@ def test_process_help_exposes_framework_flags(capsys):
|
||||
assert exc.value.code == 0
|
||||
output = capsys.readouterr().out
|
||||
assert "--report-json" in output
|
||||
assert "--llm-api-key" in output
|
||||
assert "--modules" in output
|
||||
assert "--model" in output
|
||||
assert "--base-url" in output
|
||||
@@ -139,3 +140,57 @@ def test_cli_process_passes_modules_override_to_config(monkeypatch, tmp_path):
|
||||
|
||||
assert exit_code == 0
|
||||
assert captured["module_keys"] == "grammar"
|
||||
|
||||
|
||||
def test_cli_process_passes_llm_api_key_override_to_config(monkeypatch, tmp_path):
|
||||
captured = {}
|
||||
transcript = parse_transcript_json(
|
||||
"""
|
||||
[
|
||||
{"id": 1, "speaker": "Eric", "start": 0.0, "end": 1.0, "text": "Fixed."}
|
||||
]
|
||||
"""
|
||||
)
|
||||
report = RunReport(
|
||||
status="success",
|
||||
config={"model": "m", "base_url": "b"},
|
||||
normalization={"source_segment_count": 1, "normalized_segment_count": 1, "merge_count": 0},
|
||||
pipeline=["grammar"],
|
||||
modules=[],
|
||||
applied_changes=[],
|
||||
skipped_corrections=[],
|
||||
totals={"output_segment_count": 1, "applied_change_count": 0, "skipped_correction_count": 0},
|
||||
work_dir_retention="auto",
|
||||
work_dir_retained=False,
|
||||
work_dir=None,
|
||||
error=None,
|
||||
)
|
||||
result = ProcessResult(
|
||||
transcript=transcript,
|
||||
report=report,
|
||||
run_dir=tmp_path / "run",
|
||||
work_dir_retained=False,
|
||||
)
|
||||
|
||||
def _fake_from_sources(*, overrides=None):
|
||||
captured["llm_api_key"] = overrides.llm_api_key
|
||||
return object()
|
||||
|
||||
monkeypatch.setattr("audita.cli.AuditaConfig.from_sources", _fake_from_sources)
|
||||
monkeypatch.setattr("audita.cli.load_transcript", lambda path: [])
|
||||
monkeypatch.setattr("audita.cli.load_glossary", lambda path: object())
|
||||
monkeypatch.setattr("audita.cli.process_transcript_result", lambda *args, **kwargs: result)
|
||||
|
||||
exit_code = main(
|
||||
[
|
||||
"process",
|
||||
"transcript.json",
|
||||
"--glossary",
|
||||
"glossary.yaml",
|
||||
"--llm-api-key",
|
||||
"cli-key",
|
||||
]
|
||||
)
|
||||
|
||||
assert exit_code == 0
|
||||
assert captured["llm_api_key"] == "cli-key"
|
||||
|
||||
@@ -36,6 +36,44 @@ def test_cli_overrides_take_precedence():
|
||||
assert config.max_section_tokens == 2000
|
||||
|
||||
|
||||
def test_generic_llm_api_key_env_is_read():
|
||||
config = AuditaConfig.from_sources(env={"AUDITA_LLM_API_KEY": "generic-key"})
|
||||
|
||||
assert config.api_key == "generic-key"
|
||||
|
||||
|
||||
def test_generic_llm_api_key_takes_precedence_over_openrouter_env():
|
||||
config = AuditaConfig.from_sources(
|
||||
env={
|
||||
"AUDITA_LLM_API_KEY": "generic-key",
|
||||
"OPENROUTER_API_KEY": "legacy-key",
|
||||
}
|
||||
)
|
||||
|
||||
assert config.api_key == "generic-key"
|
||||
|
||||
|
||||
def test_llm_api_key_cli_override_takes_precedence_over_env():
|
||||
config = AuditaConfig.from_sources(
|
||||
env={
|
||||
"AUDITA_LLM_API_KEY": "generic-key",
|
||||
"OPENROUTER_API_KEY": "legacy-key",
|
||||
},
|
||||
overrides=ConfigOverrides(llm_api_key="cli-key"),
|
||||
)
|
||||
|
||||
assert config.api_key == "cli-key"
|
||||
|
||||
|
||||
def test_blank_llm_api_key_override_resolves_to_none():
|
||||
config = AuditaConfig.from_sources(
|
||||
env={"OPENROUTER_API_KEY": "legacy-key"},
|
||||
overrides=ConfigOverrides(llm_api_key=" "),
|
||||
)
|
||||
|
||||
assert config.api_key is None
|
||||
|
||||
|
||||
def test_module_key_overrides_take_precedence():
|
||||
config = AuditaConfig.from_sources(
|
||||
env={"AUDITA_MODULES": "grammar"},
|
||||
|
||||
@@ -267,7 +267,7 @@ def test_process_transcript_result_missing_api_key_writes_failed_report(tmp_path
|
||||
work_dir_retention="never",
|
||||
)
|
||||
|
||||
with pytest.raises(AuditaLLMError, match="OPENROUTER_API_KEY"):
|
||||
with pytest.raises(AuditaLLMError, match="AUDITA_LLM_API_KEY"):
|
||||
process_transcript_result(_transcript(), _glossary(), config)
|
||||
|
||||
run_dir = next((tmp_path / "work").iterdir())
|
||||
@@ -287,6 +287,7 @@ def test_process_transcript_result_missing_api_key_writes_failed_report(tmp_path
|
||||
assert report["skipped_corrections"] == []
|
||||
assert report["work_dir_retained"] is True
|
||||
assert report["work_dir"] == str(run_dir)
|
||||
assert "AUDITA_LLM_API_KEY" in report["error"]
|
||||
assert "OPENROUTER_API_KEY" in report["error"]
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user