Skip to content

Commit bd5dd9b

Browse files
committed
Extended inference providers configuration options
1 parent 055e1a5 commit bd5dd9b

9 files changed

Lines changed: 1368 additions & 242 deletions

File tree

‎docs/devel_doc/openapi.json‎

Lines changed: 864 additions & 83 deletions
Large diffs are not rendered by default.

‎src/models/config.py‎

Lines changed: 300 additions & 115 deletions
Large diffs are not rendered by default.

‎src/telemetry/configuration_snapshot.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,9 @@ class ListFieldSpec:
125125
FieldSpec("type", MaskingType.PASSTHROUGH),
126126
FieldSpec("id", MaskingType.PASSTHROUGH),
127127
FieldSpec("api_key_env", MaskingType.SENSITIVE),
128+
FieldSpec("skip_model_discovery", MaskingType.PASSTHROUGH),
128129
FieldSpec("allowed_models", MaskingType.PASSTHROUGH),
130+
FieldSpec("config", MaskingType.SENSITIVE),
129131
),
130132
),
131133
# Authentication & Authorization

‎tests/unit/models/config/test_inference_configuration.py‎

Lines changed: 5 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import pytest
44
from pydantic import ValidationError
55

6-
from models.config import InferenceConfiguration, UnifiedInferenceProvider
6+
from models.config import InferenceConfiguration, VllmInferenceProvider
77

88

99
def test_inference_constructor() -> None:
@@ -161,33 +161,17 @@ def test_max_tool_calls_accepts_none() -> None:
161161
assert config.max_tool_calls is None
162162

163163

164-
def test_unified_inference_provider_id_optional() -> None:
165-
"""Omitting id leaves it None (type-derived default at synthesis)."""
166-
provider = UnifiedInferenceProvider(type="vllm")
167-
assert provider.id is None
168-
169-
170-
@pytest.mark.parametrize(
171-
"provider_id",
172-
["vllm-prod", "vllm_staging", "openai", "a", "vllm-prod-2"],
173-
)
174-
def test_unified_inference_provider_id_accepts_valid(provider_id: str) -> None:
175-
"""Accept lowercase letters, digits, underscores, and hyphens."""
176-
provider = UnifiedInferenceProvider(type="vllm", id=provider_id)
177-
assert provider.id == provider_id
178-
179-
180-
def test_unified_inference_provider_id_strips_whitespace() -> None:
164+
def test_inference_provider_id_strips_whitespace() -> None:
181165
"""Leading and trailing whitespace are stripped from id."""
182-
provider = UnifiedInferenceProvider(type="vllm", id=" vllm-prod ")
166+
provider = VllmInferenceProvider(type="vllm", id=" vllm-prod ")
183167
assert provider.id == "vllm-prod"
184168

185169

186170
@pytest.mark.parametrize(
187171
"provider_id",
188172
["", " ", "VLLM", "vllm prod", "vllm.prod", "vllm/prod", "Vllm-prod"],
189173
)
190-
def test_unified_inference_provider_id_rejects_invalid(provider_id: str) -> None:
174+
def test_inference_provider_id_rejects_invalid(provider_id: str) -> None:
191175
"""Reject empty, whitespace-only, uppercase, spaces, and other punctuation."""
192176
with pytest.raises(ValidationError):
193-
UnifiedInferenceProvider(type="vllm", id=provider_id)
177+
VllmInferenceProvider(type="vllm", id=provider_id)
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
"""Unit tests for discriminated inference.providers[] config entries."""
2+
3+
import pytest
4+
from pydantic import TypeAdapter, ValidationError
5+
6+
from models.config import (
7+
AllowedModel,
8+
BedrockInferenceProvider,
9+
InferenceConfiguration,
10+
OpenAIInferenceProvider,
11+
SentenceTransformersInferenceProvider,
12+
UnifiedInferenceProvider,
13+
VllmInferenceProvider,
14+
)
15+
16+
PROVIDER_ADAPTER = TypeAdapter(UnifiedInferenceProvider)
17+
18+
19+
@pytest.mark.parametrize(
20+
"provider_type",
21+
[
22+
"openai",
23+
"azure",
24+
"vertexai",
25+
"bedrock",
26+
"watsonx",
27+
"vllm",
28+
"sentence_transformers",
29+
],
30+
)
31+
def test_all_seven_provider_types_parse(provider_type: str) -> None:
32+
"""Each supported provider type parses into the discriminated union."""
33+
payload: dict[str, object] = {"type": provider_type}
34+
if provider_type == "sentence_transformers":
35+
payload["allowed_models"] = ["all-MiniLM-L6-v2"]
36+
provider = PROVIDER_ADAPTER.validate_python(payload)
37+
assert provider.type == provider_type
38+
assert provider.id == provider_type
39+
40+
41+
def test_allowed_models_string_form_normalizes() -> None:
42+
"""Bare string allow-list entries set name and provider_model_id equal."""
43+
model = AllowedModel.model_validate("gpt-4o-mini")
44+
assert model.name == "gpt-4o-mini"
45+
assert model.provider_model_id == "gpt-4o-mini"
46+
assert model.model_type is None
47+
assert model.metadata == {}
48+
49+
50+
def test_allowed_models_structured_form() -> None:
51+
"""Structured allow-list entries keep local name, wire id, type, metadata."""
52+
model = AllowedModel.model_validate(
53+
{
54+
"name": "fast-chat",
55+
"provider_model_id": "gpt-4o-mini",
56+
"model_type": "llm",
57+
"metadata": {"context_window": 128000},
58+
}
59+
)
60+
assert model.name == "fast-chat"
61+
assert model.provider_model_id == "gpt-4o-mini"
62+
assert model.model_type == "llm"
63+
assert model.metadata["context_window"] == 128000
64+
65+
66+
def test_allowed_models_defaults_name_to_provider_model_id() -> None:
67+
"""Omitted name defaults to provider_model_id."""
68+
model = AllowedModel.model_validate(
69+
{"provider_model_id": "openai/gpt-4o-mini", "model_type": "llm"}
70+
)
71+
assert model.name == "openai/gpt-4o-mini"
72+
assert model.provider_model_id == "openai/gpt-4o-mini"
73+
74+
75+
def test_omitted_id_defaults_to_type() -> None:
76+
"""Provider id defaults to type when omitted."""
77+
provider = OpenAIInferenceProvider.model_validate({"type": "openai"})
78+
assert provider.id == "openai"
79+
80+
81+
def test_explicit_id_is_kept() -> None:
82+
"""Explicit provider id is preserved."""
83+
provider = VllmInferenceProvider.model_validate({"type": "vllm", "id": "vllm-prod"})
84+
assert provider.id == "vllm-prod"
85+
86+
87+
def test_duplicate_provider_ids_fail() -> None:
88+
"""Two providers resolving to the same id fail validation."""
89+
with pytest.raises(ValidationError, match="duplicate inference provider id"):
90+
InferenceConfiguration(
91+
providers=[
92+
{"type": "vllm"},
93+
{"type": "vllm"},
94+
]
95+
)
96+
97+
98+
def test_duplicate_local_model_names_within_provider_fail() -> None:
99+
"""Duplicate local names in one provider's allow-list fail validation."""
100+
with pytest.raises(ValidationError, match="allowed model names must be unique"):
101+
OpenAIInferenceProvider.model_validate(
102+
{
103+
"type": "openai",
104+
"allowed_models": [
105+
"same",
106+
{"name": "same", "provider_model_id": "other"},
107+
],
108+
}
109+
)
110+
111+
112+
def test_same_local_model_name_across_providers_allowed() -> None:
113+
"""The same local model name may appear on different providers."""
114+
config = InferenceConfiguration(
115+
providers=[
116+
{"type": "openai", "allowed_models": ["shared"]},
117+
{"type": "vllm", "id": "vllm-prod", "allowed_models": ["shared"]},
118+
]
119+
)
120+
assert config.providers[0].allowed_models is not None
121+
assert config.providers[1].allowed_models is not None
122+
assert config.providers[0].allowed_models[0].name == "shared"
123+
assert config.providers[1].allowed_models[0].name == "shared"
124+
125+
126+
def test_skip_model_discovery_requires_allowed_models() -> None:
127+
"""skip_model_discovery: true requires a non-empty allow-list."""
128+
with pytest.raises(ValidationError, match="skip_model_discovery"):
129+
OpenAIInferenceProvider.model_validate(
130+
{"type": "openai", "skip_model_discovery": True}
131+
)
132+
133+
134+
def test_skip_model_discovery_with_allowed_models() -> None:
135+
"""skip_model_discovery succeeds when allowed_models is non-empty."""
136+
provider = OpenAIInferenceProvider.model_validate(
137+
{
138+
"type": "openai",
139+
"skip_model_discovery": True,
140+
"allowed_models": ["gpt-4o-mini"],
141+
}
142+
)
143+
assert provider.skip_model_discovery is True
144+
145+
146+
def test_sentence_transformers_requires_allowed_models() -> None:
147+
"""Sentence-transformers rejects an empty/missing allow-list."""
148+
with pytest.raises(
149+
ValidationError,
150+
match="allowed_models must be non-empty for sentence_transformers",
151+
):
152+
SentenceTransformersInferenceProvider.model_validate(
153+
{"type": "sentence_transformers"}
154+
)
155+
156+
157+
def test_nested_config_parses() -> None:
158+
"""Typed nested config blocks parse for a remote provider."""
159+
provider = BedrockInferenceProvider.model_validate(
160+
{
161+
"type": "bedrock",
162+
"config": {"region": "${env.AWS_REGION}", "profile_name": "default"},
163+
}
164+
)
165+
assert provider.config.region == "${env.AWS_REGION}"
166+
assert provider.config.profile_name == "default"

‎tests/unit/telemetry/conftest.py‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@
3535
ModelContextProtocolServer,
3636
OgxConfiguration,
3737
OkpConfiguration,
38+
OpenAIInferenceProvider,
39+
OpenAIInferenceProviderConfig,
3840
PgvectorVectorStoreProvider,
3941
PgvectorVectorStoreProviderConfig,
4042
PostgreSQLDatabaseConfiguration,
@@ -58,7 +60,6 @@
5860
TLSConfiguration,
5961
TrustedProxyConfiguration,
6062
TrustedProxyServiceAccount,
61-
UnifiedInferenceProvider,
6263
UnifiedOgxConfig,
6364
UserDataCollection,
6465
VectorStoreConfiguration,
@@ -359,12 +360,17 @@ def build_fully_populated_config() -> Configuration:
359360
max_infer_iters=10,
360361
max_tool_calls=30,
361362
providers=[
362-
UnifiedInferenceProvider.model_construct(
363+
OpenAIInferenceProvider.model_construct(
363364
type="openai",
364365
id="openai-provider",
365366
api_key_env=PII_PROVIDER_API_KEY_ENV,
366367
allowed_models=["gpt-4o-mini", "gpt-4o"],
367368
extra={},
369+
skip_model_discovery=False,
370+
config=OpenAIInferenceProviderConfig.model_construct(
371+
api_key="sk-openai-secret-key",
372+
base_url="https://api.openai.com/v1",
373+
),
368374
),
369375
],
370376
),

‎tests/unit/telemetry/test_configuration_snapshot.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -622,6 +622,8 @@ def test_inference_providers_extraction(self) -> None:
622622
assert providers[0]["id"] == "openai-provider"
623623
assert providers[0]["api_key_env"] == CONFIGURED
624624
assert providers[0]["allowed_models"] == ["gpt-4o-mini", "gpt-4o"]
625+
assert providers[0]["skip_model_discovery"] is False
626+
assert providers[0]["config"] == CONFIGURED
625627

626628
def test_inference_providers_empty(self) -> None:
627629
"""Test inference providers when empty."""

‎tests/unit/test_ogx_synthesize.py‎

Lines changed: 6 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,16 @@
1313
import stat
1414
import sys
1515
from pathlib import Path
16-
from typing import Any, Optional, get_args
16+
from typing import Any, Optional
1717

1818
import pytest
1919
import yaml
2020
from ogx.core.stack import replace_env_vars
2121
from pydantic import ValidationError
2222

23-
from models.config import InferenceConfiguration, UnifiedInferenceProvider
23+
from models.config import InferenceConfiguration, VllmInferenceProvider
2424
from ogx_configuration import (
2525
CONDITIONAL_OPENAI_PROVIDER_ID,
26-
PROVIDER_TYPE_MAP,
2726
apply_high_level_inference,
2827
deep_merge_list_replace,
2928
ensure_mcp_tool_runtime,
@@ -531,12 +530,11 @@ def test_apply_high_level_inference_extra_cannot_override_api_key_env() -> None:
531530
assert entry["config"]["api_token"] == "${env.VLLM_API_KEY}"
532531

533532

534-
def test_unified_inference_provider_accepts_ollama_and_vllm() -> None:
535-
"""Pydantic model accepts the new ollama and vllm Literal values."""
536-
ollama = UnifiedInferenceProvider(type="ollama")
537-
assert ollama.type == "ollama"
538-
vllm = UnifiedInferenceProvider(type="vllm")
533+
def test_inference_provider_accepts_vllm() -> None:
534+
"""Pydantic model accepts the vllm provider type."""
535+
vllm = VllmInferenceProvider(type="vllm")
539536
assert vllm.type == "vllm"
537+
assert vllm.id == "vllm"
540538

541539

542540
def test_apply_high_level_inference_empty_is_noop() -> None:
@@ -713,18 +711,6 @@ def test_inference_config_allows_distinct_provider_ids() -> None:
713711
assert len(config.providers) == 2
714712

715713

716-
def test_provider_type_map_covers_every_literal_value() -> None:
717-
"""Every UnifiedInferenceProvider.type value has a PROVIDER_TYPE_MAP entry."""
718-
literal_values = set(
719-
get_args(
720-
UnifiedInferenceProvider.model_fields[ # pylint: disable=unsubscriptable-object
721-
"type"
722-
].annotation
723-
)
724-
)
725-
assert literal_values == set(PROVIDER_TYPE_MAP)
726-
727-
728714
# ---------------------------------------------------------------------------
729715
# synthesize_configuration
730716
# ---------------------------------------------------------------------------

‎tests/unit/utils/dumpers/test_models_dumper.py‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10259,7 +10259,21 @@ def test_dump_models(tmpdir: Path) -> None:
1025910259
"TurnCompleteStreamPayload",
1026010260
"TurnSummary",
1026110261
"UnauthorizedResponse",
10262-
"UnifiedInferenceProvider",
10262+
"AllowedModel",
10263+
"AzureInferenceProvider",
10264+
"AzureInferenceProviderConfig",
10265+
"BedrockInferenceProvider",
10266+
"BedrockInferenceProviderConfig",
10267+
"OpenAIInferenceProvider",
10268+
"OpenAIInferenceProviderConfig",
10269+
"SentenceTransformersInferenceProvider",
10270+
"SentenceTransformersInferenceProviderConfig",
10271+
"VertexAIInferenceProvider",
10272+
"VertexAIInferenceProviderConfig",
10273+
"VllmInferenceProvider",
10274+
"VllmInferenceProviderConfig",
10275+
"WatsonxInferenceProvider",
10276+
"WatsonxInferenceProviderConfig",
1026310277
"UnifiedOgxConfig",
1026410278
"UnprocessableEntityResponse",
1026510279
"UserDataCollection",

0 commit comments

Comments
 (0)