File size: 10,325 Bytes
175746c
 
 
 
 
 
 
 
 
e4a41fa
 
 
 
 
 
 
 
 
 
 
 
175746c
 
 
 
 
e4a41fa
175746c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e4a41fa
175746c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7a34030
 
175746c
 
 
 
7a34030
 
175746c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
"""Tests for universal provider support.

Tests OpenAICompatibleProvider, GoogleProvider, and LiteLLMProvider.
"""

from __future__ import annotations

import pytest

from headroom.providers import (
    GoogleProvider,
    ModelCapabilities,
    OpenAICompatibleProvider,
    create_groq_provider,
    create_lmstudio_provider,
    create_ollama_provider,
    create_together_provider,
    create_vllm_provider,
    is_litellm_available,
)


def _transformers_available() -> bool:
    """Check if transformers is available."""
    try:
        import transformers  # noqa: F401

        return True
    except ImportError:
        return False


class TestOpenAICompatibleProvider:
    """Tests for OpenAICompatibleProvider."""

    def test_init_default(self):
        """Test initialization with defaults."""
        provider = OpenAICompatibleProvider()
        assert provider.name == "openai_compatible"
        assert provider.base_url is None

    def test_init_with_config(self):
        """Test initialization with configuration."""
        provider = OpenAICompatibleProvider(
            name="custom",
            base_url="http://localhost:8080/v1",
            api_key="test-key",
        )
        assert provider.name == "custom"
        assert provider.base_url == "http://localhost:8080/v1"
        assert provider.api_key == "test-key"

    def test_supports_any_model(self):
        """Test that provider supports any model."""
        provider = OpenAICompatibleProvider()
        assert provider.supports_model("any-model") is True
        assert provider.supports_model("llama-3") is True
        assert provider.supports_model("custom-finetuned") is True

    @pytest.mark.skipif(
        not _transformers_available(),
        reason="transformers not installed - needed for HuggingFace tokenizer",
    )
    def test_get_token_counter(self):
        """Test getting token counter."""
        provider = OpenAICompatibleProvider()
        counter = provider.get_token_counter("llama-3-8b")
        assert counter is not None
        # Should be able to count tokens
        count = counter.count_text("Hello, world!")
        assert count > 0

    def test_get_context_limit_known_model(self):
        """Test context limit for known models."""
        provider = OpenAICompatibleProvider()
        # Llama 3.1 has 128K context
        limit = provider.get_context_limit("llama-3.1-8b")
        assert limit == 128000

    def test_get_context_limit_unknown_model(self):
        """Test context limit for unknown models (defaults to 128K)."""
        provider = OpenAICompatibleProvider()
        limit = provider.get_context_limit("unknown-model")
        assert limit == 128000

    def test_register_model(self):
        """Test registering a custom model."""
        provider = OpenAICompatibleProvider()
        provider.register_model(
            "my-model",
            context_window=64000,
            max_output_tokens=8192,
            input_cost_per_1m=1.0,
            output_cost_per_1m=2.0,
        )
        assert provider.get_context_limit("my-model") == 64000

    def test_estimate_cost_registered_model(self):
        """Test cost estimation for registered model."""
        provider = OpenAICompatibleProvider()
        provider.register_model(
            "priced-model",
            input_cost_per_1m=1.0,
            output_cost_per_1m=2.0,
        )
        cost = provider.estimate_cost(
            input_tokens=1000000,
            output_tokens=500000,
            model="priced-model",
        )
        assert cost == 2.0  # 1.0 + 1.0

    def test_estimate_cost_unknown_model(self):
        """Test cost estimation returns None for unknown model."""
        provider = OpenAICompatibleProvider()
        cost = provider.estimate_cost(
            input_tokens=1000,
            output_tokens=500,
            model="unknown-model",
        )
        assert cost is None


class TestModelCapabilities:
    """Tests for ModelCapabilities dataclass."""

    def test_default_values(self):
        """Test default capability values."""
        caps = ModelCapabilities(model="test-model")
        assert caps.context_window == 128000
        assert caps.max_output_tokens == 4096
        assert caps.supports_tools is True
        assert caps.supports_vision is False
        assert caps.supports_streaming is True

    def test_custom_values(self):
        """Test custom capability values."""
        caps = ModelCapabilities(
            model="custom-model",
            context_window=32000,
            max_output_tokens=16384,
            supports_tools=False,
            supports_vision=True,
            input_cost_per_1m=0.5,
            output_cost_per_1m=1.5,
        )
        assert caps.context_window == 32000
        assert caps.max_output_tokens == 16384
        assert caps.supports_tools is False
        assert caps.supports_vision is True
        assert caps.input_cost_per_1m == 0.5
        assert caps.output_cost_per_1m == 1.5


class TestGoogleProvider:
    """Tests for GoogleProvider."""

    @pytest.fixture
    def provider(self):
        """Create Google provider."""
        return GoogleProvider()

    def test_name(self, provider):
        """Test provider name."""
        assert provider.name == "google"

    def test_supports_gemini_models(self, provider):
        """Test support for Gemini models."""
        assert provider.supports_model("gemini-2.0-flash") is True
        assert provider.supports_model("gemini-1.5-pro") is True
        assert provider.supports_model("gemini-1.5-flash") is True

    def test_not_supports_other_models(self, provider):
        """Test non-support for other models."""
        assert provider.supports_model("gpt-4o") is False
        assert provider.supports_model("claude-3") is False

    def test_get_token_counter(self, provider):
        """Test getting token counter."""
        counter = provider.get_token_counter("gemini-2.0-flash")
        assert counter is not None
        count = counter.count_text("Hello, world!")
        assert count > 0

    def test_get_context_limit_gemini_2(self, provider):
        """Test context limit for Gemini 2.0."""
        limit = provider.get_context_limit("gemini-2.0-flash")
        # LiteLLM returns 1048576 (2^20), fallback returns 1000000
        assert limit in (1000000, 1048576)  # ~1M tokens

    def test_get_context_limit_gemini_1_5_pro(self, provider):
        """Test context limit for Gemini 1.5 Pro (2M!)."""
        limit = provider.get_context_limit("gemini-1.5-pro")
        # LiteLLM returns 2097152 (2^21), fallback returns 2000000
        assert limit in (2000000, 2097152)  # ~2M tokens!

    def test_estimate_cost(self, provider):
        """Test cost estimation."""
        cost = provider.estimate_cost(
            input_tokens=1000000,
            output_tokens=500000,
            model="gemini-2.0-flash",
        )
        assert cost is not None
        # 1M input * $0.10 + 0.5M output * $0.40 = $0.10 + $0.20 = $0.30
        assert abs(cost - 0.30) < 0.01

    def test_openai_compatible_url(self):
        """Test OpenAI-compatible URL."""
        url = GoogleProvider.get_openai_compatible_url("test-key")
        assert "generativelanguage.googleapis.com" in url


class TestProviderFactoryFunctions:
    """Tests for provider factory functions."""

    def test_create_ollama_provider(self):
        """Test creating Ollama provider."""
        provider = create_ollama_provider()
        assert provider.name == "ollama"
        assert provider.base_url == "http://localhost:11434/v1"

    def test_create_ollama_provider_custom_url(self):
        """Test creating Ollama provider with custom URL."""
        provider = create_ollama_provider("http://192.168.1.100:11434/v1")
        assert provider.base_url == "http://192.168.1.100:11434/v1"

    def test_create_together_provider(self):
        """Test creating Together provider."""
        provider = create_together_provider()
        assert provider.name == "together"
        assert "together.xyz" in provider.base_url

    def test_create_groq_provider(self):
        """Test creating Groq provider."""
        provider = create_groq_provider()
        assert provider.name == "groq"
        assert "groq.com" in provider.base_url

    def test_create_vllm_provider(self):
        """Test creating vLLM provider."""
        provider = create_vllm_provider("http://localhost:8000/v1")
        assert provider.name == "vllm"
        assert provider.base_url == "http://localhost:8000/v1"

    def test_create_lmstudio_provider(self):
        """Test creating LM Studio provider."""
        provider = create_lmstudio_provider()
        assert provider.name == "lmstudio"
        assert provider.base_url == "http://localhost:1234/v1"


class TestLiteLLMProvider:
    """Tests for LiteLLM provider."""

    def test_is_litellm_available(self):
        """Test checking LiteLLM availability."""
        result = is_litellm_available()
        assert isinstance(result, bool)

    @pytest.mark.skipif(
        not is_litellm_available(),
        reason="LiteLLM not installed",
    )
    def test_create_litellm_provider(self):
        """Test creating LiteLLM provider."""
        from headroom.providers import create_litellm_provider

        provider = create_litellm_provider()
        assert provider.name == "litellm"

    @pytest.mark.skipif(
        not is_litellm_available(),
        reason="LiteLLM not installed",
    )
    def test_litellm_supports_any_model(self):
        """Test LiteLLM supports any model."""
        from headroom.providers import create_litellm_provider

        provider = create_litellm_provider()
        assert provider.supports_model("gpt-4o") is True
        assert provider.supports_model("claude-3-sonnet") is True
        assert provider.supports_model("any-model") is True

    @pytest.mark.skipif(
        not is_litellm_available(),
        reason="LiteLLM not installed",
    )
    def test_litellm_list_providers(self):
        """Test listing LiteLLM providers."""
        from headroom.providers import LiteLLMProvider

        providers = LiteLLMProvider.list_supported_providers()
        assert "openai" in providers
        assert "anthropic" in providers
        assert "ollama" in providers