Skip to content

Commit 56447e1

Browse files
authored
Parametric LLM model tests (#617)
* Make LLM tests fully parametric in model name via EFFECTFUL_LLM_MODEL env var Replace provider-specific environment variable checks (OPENAI_API_KEY, ANTHROPIC_API_KEY) and skip markers (requires_openai, requires_anthropic) with a single EFFECTFUL_LLM_MODEL environment variable that controls which model is used for all LLM integration tests. - Remove hardcoded model names from all test files in favor of LLM_MODEL read from EFFECTFUL_LLM_MODEL env var - Replace requires_openai/requires_anthropic markers with requires_llm - Remove model parametrization that was cross-provider; tests now use whichever model the env var specifies - Use litellm.supports_vision() to conditionally skip vision tests - Remove default model from LiteLLMProvider (make model required) - Update CI workflow to pass EFFECTFUL_LLM_MODEL as a matrix parameter, making it easy to add parallel CI stages for different providers - Rename/remove fixture files to match updated test names Closes #589 * Deduplicate LLM_MODEL/requires_llm into conftest, fix import order, rename test - Move LLM_MODEL and requires_llm definitions to tests/conftest.py; all four LLM test files now import from there - Fix import ordering in test_handlers_llm_encoding.py (stdlib before local) - Rename test_agent_tool_names_are_openai_compatible_integration to test_agent_tool_names_are_valid_integration since it's no longer OpenAI-specific * Address review feedback: fix breaking change, import hygiene, redundancy - Restore LiteLLMProvider default model via env var fallback so existing callers (template.py, test_handlers_llm_template.py, notebook) are not broken: model=os.environ.get("EFFECTFUL_LLM_MODEL", "gpt-4o") - Move LLM_MODEL/requires_llm from conftest.py to tests/_llm_helpers.py since conftest.py should not be imported directly - Fix import placement in test_handlers_llm_provider.py (was between module-level constants instead of grouped with imports) - Remove redundant @requires_llm on vision tests where the skipif condition already covers the not-LLM_MODEL case * Revert LiteLLMProvider default to plain literal "gpt-4o" The env var belongs in test infrastructure, not the library API. LiteLLMProvider should have a clean, explicit default. * Minor fix * minor fixes * minor * Make LLM test suite parametric in model name (#589) Remove provider-specific environment variable checks and skip markers from LLM tests in favor of a single EFFECTFUL_LLM_MODEL env var. - Add LLM_MODEL and requires_llm to tests/conftest.py; LLM_MODEL defaults to "gpt-4o-mini" and is overridable via EFFECTFUL_LLM_MODEL - Live tests (tool calling, encoding, agent tool names) use LLM_MODEL and are gated by requires_llm which checks for any provider API key - Integration tests use make_provider() which returns a live LiteLLMProvider when API keys are available, else falls back to ReplayLiteLLMProvider for offline replay from fixtures - Replay-only tests (simple_prompt_multiple_models, cross_endpoint, caching) keep hardcoded model names and always run since they never call the API - Update CI workflow to pass EFFECTFUL_LLM_MODEL as a matrix parameter for easy parallel stages with different providers Closes #589 * Minor fix * Minor fix * Fix tuple type schemas for OpenAI structured output compatibility Three fixes in encoding.py: 1. TupleEncodable.encode() now returns a TupleItems model instance (not a raw tuple), and deserialize() returns the model directly. This fixes pydantic validation in litellm integration tests for NamedTuple and fixed-tuple types. 2. Add _TupleSafeJsonSchema that overrides pydantic's tuple_schema() to produce object schemas (item_0, item_1 properties) instead of prefixItems arrays. Applied via _BoxEncoding.model_json_schema() so dataclasses containing tuple fields produce OpenAI-compatible schemas. 3. SequenceEncodable.encode() returns a list (not tuple) to preserve encode idempotency — nested_type on a list dispatches to the sequence encoder, avoiding a mismatch with TupleEncodable. Also adds test_handlers_llm_encoding.py back to CI workflow. * Clean up * Split out tuple encoding fixes to separate branch Revert encoding.py to master and remove encoding tests from CI workflow. Tuple schema fixes will be in a dedicated PR. * Removing hard-coded test value and renaminng const * Use litellm to check requires vision * Fix missing fixture and lint formatting - Add renamed fixture for test_simple_prompt (was test_simple_prompt_multiple_models[gpt-4o-mini]) - Reformat long lines to pass ruff lint
1 parent ec7dcb8 commit 56447e1

10 files changed

Lines changed: 74 additions & 259 deletions

.github/workflows/test_llm.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ jobs:
1414
strategy:
1515
matrix:
1616
python-version: ["'3.13'"]
17+
model: ["gpt-4o-mini"]
1718
steps:
1819
- uses: actions/checkout@v4
1920

@@ -34,7 +35,8 @@ jobs:
3435
3536
- name: Run LLM integration tests
3637
env:
38+
EFFECTFUL_LLM_MODEL: ${{ matrix.model }}
3739
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
3840
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
3941
run: |
40-
uv run pytest tests/test_handlers_llm_provider.py -v --tb=short
42+
uv run pytest tests/test_handlers_llm_provider.py tests/test_handlers_llm_tool_calling_poem.py tests/test_handlers_llm_tool_calling_book.py -v --tb=short

tests/conftest.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,24 @@
1+
import os
2+
3+
import litellm
14
import pytest
25

6+
EFFECTFUL_LLM_MODEL = os.environ.get("EFFECTFUL_LLM_MODEL", "gpt-4o-mini")
7+
8+
_HAS_LLM_API_KEY = litellm.validate_environment(model=EFFECTFUL_LLM_MODEL)[
9+
"keys_in_environment"
10+
]
11+
12+
requires_llm = pytest.mark.skipif(
13+
not _HAS_LLM_API_KEY,
14+
reason=f"No API key configured for model {EFFECTFUL_LLM_MODEL}",
15+
)
16+
17+
requires_vision = pytest.mark.skipif(
18+
not litellm.supports_vision(model=EFFECTFUL_LLM_MODEL),
19+
reason=f"Model {EFFECTFUL_LLM_MODEL} does not support vision",
20+
)
21+
322
UNIMPLEMENTED_SUBSTRINGS = [
423
"infer.JitTrace_ELBO",
524
"the event_dim arg",

tests/fixtures/tests_test_handlers_llm_provider.py__TestLiteLLMProvider__test_simple_prompt_multiple_models[gpt-4o-mini].json renamed to tests/fixtures/tests_test_handlers_llm_provider.py__TestLiteLLMProvider__test_simple_prompt.json

File renamed without changes.

tests/fixtures/tests_test_handlers_llm_provider.py__TestLiteLLMProvider__test_simple_prompt_cross_endpoint[claude-haiku-4-5].json

Lines changed: 0 additions & 39 deletions
This file was deleted.

tests/fixtures/tests_test_handlers_llm_provider.py__TestLiteLLMProvider__test_simple_prompt_cross_endpoint[gpt-4o-mini].json

Lines changed: 0 additions & 44 deletions
This file was deleted.

tests/fixtures/tests_test_handlers_llm_provider.py__TestLiteLLMProvider__test_simple_prompt_multiple_models[gpt-5-nano].json

Lines changed: 0 additions & 44 deletions
This file was deleted.

tests/test_handlers_llm_encoding.py

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,7 @@
2828
from effectful.internals.unification import nested_type
2929
from effectful.ops.semantics import handler
3030
from effectful.ops.types import Operation, Term
31-
from tests.test_handlers_llm_tool_calling_book import requires_openai
32-
33-
CHEAP_MODEL = "gpt-4o-mini"
31+
from tests.conftest import EFFECTFUL_LLM_MODEL, requires_llm
3432

3533
# ---------------------------------------------------------------------------
3634
# Module-level type definitions
@@ -744,14 +742,14 @@ def _encode_tool_spec(tool: Tool[..., Any]) -> dict[str, Any]:
744742
raise TypeError(f"Unexpected encoded tool spec type: {type(tool_spec_obj)}")
745743

746744

747-
@requires_openai
745+
@requires_llm
748746
@pytest.mark.parametrize("ty,_value,ctx", PROVIDER_CASES)
749747
def test_litellm_completion_accepts_encodable_response_model_for_supported_types(
750748
ty: Any, _value: Any, ctx: Mapping[str, Any] | None
751749
) -> None:
752750
enc = Encodable.define(ty, ctx)
753751
kwargs: dict[str, Any] = {
754-
"model": CHEAP_MODEL,
752+
"model": EFFECTFUL_LLM_MODEL,
755753
"messages": [
756754
{
757755
"role": "user",
@@ -777,7 +775,7 @@ def test_litellm_completion_accepts_encodable_response_model_for_supported_types
777775
pydantic.TypeAdapter(enc.base).validate_python(decoded)
778776

779777

780-
@requires_openai
778+
@requires_llm
781779
@pytest.mark.parametrize("ty,_value,ctx", PROVIDER_CASES)
782780
def test_litellm_completion_accepts_tool_with_type_as_param(
783781
ty: Any, _value: Any, ctx: Mapping[str, Any] | None
@@ -793,7 +791,7 @@ def _fn(value):
793791

794792
tool: Tool[..., Any] = Tool.define(_fn)
795793
response = litellm.completion(
796-
model=CHEAP_MODEL,
794+
model=EFFECTFUL_LLM_MODEL,
797795
messages=[{"role": "user", "content": "Return hello, do NOT call any tools."}],
798796
tools=[_encode_tool_spec(tool)],
799797
tool_choice="none",
@@ -802,7 +800,7 @@ def _fn(value):
802800
assert response is not None
803801

804802

805-
@requires_openai
803+
@requires_llm
806804
@pytest.mark.parametrize("ty,_value,ctx", PROVIDER_CASES)
807805
def test_litellm_completion_accepts_tool_with_type_as_return(
808806
ty: Any, _value: Any, ctx: Mapping[str, Any] | None
@@ -818,7 +816,7 @@ def _fn():
818816

819817
tool: Tool[..., Any] = Tool.define(_fn)
820818
response = litellm.completion(
821-
model=CHEAP_MODEL,
819+
model=EFFECTFUL_LLM_MODEL,
822820
messages=[{"role": "user", "content": "Return hello, do NOT call any tools."}],
823821
tools=[_encode_tool_spec(tool)],
824822
tool_choice="none",

0 commit comments

Comments
 (0)