631 lines
22 KiB
Python
631 lines
22 KiB
Python
"""Tests for the RegistryV4 schema, adapter contracts, and error taxonomy.
|
|
|
|
These cover design doc sections 4.3 (configuration objects), 6.2 (adapter
|
|
parameter contracts), 6.4 (resolved config), 9.1 (read models), and 9.5
|
|
(unified error structure).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
from pydantic import ValidationError
|
|
|
|
from EvoScientist.model_registry.errors import (
|
|
CREDENTIAL_NOT_CONFIGURED,
|
|
DELEGATION_REPLAYED,
|
|
ERROR_HTTP_STATUS,
|
|
MODEL_NOT_FOUND,
|
|
PROVIDER_UNREACHABLE,
|
|
REGISTRY_REVISION_CONFLICT,
|
|
ErrorDetail,
|
|
ErrorPayload,
|
|
ModelRegistryError,
|
|
)
|
|
from EvoScientist.model_registry.hashing import configuration_hash
|
|
from EvoScientist.model_registry.schemas import (
|
|
AdapterParameterSpec,
|
|
AuthConfig,
|
|
AuthRef,
|
|
AuthSpec,
|
|
CredentialStatus,
|
|
CredentialWrite,
|
|
ModelAvailability,
|
|
ModelConfig,
|
|
ModelRef,
|
|
ModelRuntimeConfig,
|
|
ParameterRule,
|
|
ProviderConfig,
|
|
ProviderRuntimeConfig,
|
|
RegistryV4,
|
|
ResolvedModelConfig,
|
|
VerificationInfo,
|
|
)
|
|
|
|
ALL_ERROR_CODES = {
|
|
"INVALID_REQUEST": 400,
|
|
"REGISTRY_REVISION_CONFLICT": 409,
|
|
"THREAD_MODEL_SELECTION_CONFLICT": 409,
|
|
"RUN_REQUEST_CONFLICT": 409,
|
|
"SNAPSHOT_ALREADY_BOUND": 409,
|
|
"SNAPSHOT_EXPIRED": 409,
|
|
"MODEL_CONFIGURATION_CHANGED": 409,
|
|
"DELEGATION_REPLAYED": 401,
|
|
"UNAUTHENTICATED": 401,
|
|
"FORBIDDEN": 403,
|
|
"MODEL_NOT_FOUND": 404,
|
|
"SNAPSHOT_NOT_FOUND": 404,
|
|
"MODEL_REGISTRY_NOT_READY": 422,
|
|
"MODEL_DISABLED": 422,
|
|
"MODEL_NOT_AVAILABLE": 422,
|
|
"MODEL_CONFIG_OUTSIDE_SNAPSHOT": 422,
|
|
"CREDENTIAL_NOT_CONFIGURED": 422,
|
|
"CREDENTIAL_REJECTED": 422,
|
|
"RUN_CREDENTIAL_REVISION_UNAVAILABLE": 422,
|
|
"AUTH_MODE_UNSUPPORTED": 422,
|
|
"ADAPTER_NOT_SUPPORTED": 422,
|
|
"ENDPOINT_NOT_ALLOWED": 422,
|
|
"CAPABILITY_UNSUPPORTED_BY_ADAPTER": 422,
|
|
"MODEL_CAPABILITY_UNAVAILABLE": 422,
|
|
"UNSUPPORTED_RUNTIME_PARAMETER": 422,
|
|
"MODEL_LIMITS_UNCONFIRMED": 422,
|
|
"IMAGE_MODEL_NOT_CHAT_MODEL": 422,
|
|
"CONTEXT_BUDGET_UNSATISFIABLE": 422,
|
|
"PROVIDER_UNREACHABLE": 422,
|
|
"VALIDATION_FAILED": 422,
|
|
"PLATFORM_CONFIG_MISSING": 500,
|
|
}
|
|
|
|
|
|
def _model_runtime(**overrides):
|
|
payload = {
|
|
"limit_mode": "combined",
|
|
"context_window_tokens": 1048576,
|
|
"max_input_tokens": None,
|
|
"max_output_tokens": 32768,
|
|
"min_effective_input_tokens": 8192,
|
|
"fixed_system_reserve_tokens": 4096,
|
|
"fixed_tools_reserve_tokens": 8192,
|
|
"fixed_attachments_reserve_tokens": 4096,
|
|
"limits_status": "confirmed",
|
|
"limits_source": "provider",
|
|
"temperature": None,
|
|
"top_p": None,
|
|
"reasoning_effort": "auto",
|
|
"declared_capabilities": {
|
|
"tools": True,
|
|
"vision": False,
|
|
"structured_output": True,
|
|
},
|
|
}
|
|
payload.update(overrides)
|
|
return payload
|
|
|
|
|
|
def _provider(**overrides):
|
|
payload = {
|
|
"id": "zhipu-glm",
|
|
"name": "Zhipu GLM",
|
|
"adapter": "openai-compatible",
|
|
"base_url": "https://open.bigmodel.cn/api/paas/v4",
|
|
"auth": {"mode": "api_key", "credential_id": "zhipu-primary"},
|
|
"enabled": True,
|
|
"runtime": {
|
|
"timeout_seconds": 120,
|
|
"max_retries": 2,
|
|
"default_temperature": 0.7,
|
|
"default_top_p": 0.95,
|
|
"default_reasoning_effort": "auto",
|
|
},
|
|
"models": [
|
|
{
|
|
"key": "glm-5.2",
|
|
"name": "GLM-5.2",
|
|
"upstream_model_id": "glm-5.2",
|
|
"enabled": True,
|
|
"runtime": _model_runtime(),
|
|
}
|
|
],
|
|
}
|
|
payload.update(overrides)
|
|
return payload
|
|
|
|
|
|
def _registry(**overrides):
|
|
payload = {
|
|
"version": 4,
|
|
"revision": 1,
|
|
"state": "bootstrap",
|
|
"defaults": {
|
|
"primary": {"provider_id": "zhipu-glm", "model_key": "glm-5.2"},
|
|
},
|
|
"providers": [_provider()],
|
|
}
|
|
payload.update(overrides)
|
|
return payload
|
|
|
|
|
|
class TestRegistryV4:
|
|
def test_design_doc_example_validates(self):
|
|
registry = RegistryV4.model_validate(_registry(state="active", revision=17))
|
|
assert registry.version == 4
|
|
assert registry.revision == 17
|
|
assert registry.state == "active"
|
|
assert registry.defaults.primary == ModelRef(
|
|
provider_id="zhipu-glm", model_key="glm-5.2"
|
|
)
|
|
provider = registry.providers[0]
|
|
assert provider.runtime.timeout_seconds == 120
|
|
model = provider.models[0]
|
|
assert model.runtime.min_effective_input_tokens == 8192
|
|
assert model.runtime.declared_capabilities.tools is True
|
|
|
|
def test_version_must_be_four(self):
|
|
with pytest.raises(ValidationError):
|
|
RegistryV4.model_validate(_registry(version=3))
|
|
|
|
def test_revision_must_be_positive(self):
|
|
with pytest.raises(ValidationError):
|
|
RegistryV4.model_validate(_registry(revision=0))
|
|
|
|
def test_bootstrap_allows_empty_providers_and_null_primary(self):
|
|
registry = RegistryV4.model_validate(
|
|
_registry(
|
|
state="bootstrap",
|
|
defaults={"primary": None},
|
|
providers=[],
|
|
)
|
|
)
|
|
assert registry.state == "bootstrap"
|
|
assert registry.providers == []
|
|
|
|
def test_active_requires_primary(self):
|
|
with pytest.raises(ValidationError):
|
|
RegistryV4.model_validate(
|
|
_registry(
|
|
state="active",
|
|
defaults={"primary": None},
|
|
)
|
|
)
|
|
|
|
def test_active_requires_primary_referencing_enabled_model(self):
|
|
provider = _provider()
|
|
provider["models"][0]["enabled"] = False
|
|
with pytest.raises(ValidationError):
|
|
RegistryV4.model_validate(_registry(state="active", providers=[provider]))
|
|
|
|
def test_active_rejects_primary_referencing_disabled_provider(self):
|
|
provider = _provider(enabled=False)
|
|
with pytest.raises(ValidationError):
|
|
RegistryV4.model_validate(_registry(state="active", providers=[provider]))
|
|
|
|
def test_defaults_must_reference_existing_models(self):
|
|
with pytest.raises(ValidationError):
|
|
RegistryV4.model_validate(
|
|
_registry(
|
|
defaults={
|
|
"primary": {"provider_id": "zhipu-glm", "model_key": "missing"},
|
|
}
|
|
)
|
|
)
|
|
|
|
def test_provider_ids_must_be_unique(self):
|
|
with pytest.raises(ValidationError):
|
|
RegistryV4.model_validate(
|
|
_registry(providers=[_provider(), _provider(name="Duplicate")])
|
|
)
|
|
|
|
def test_model_keys_must_be_unique_per_provider(self):
|
|
provider = _provider()
|
|
provider["models"].append(dict(provider["models"][0]))
|
|
with pytest.raises(ValidationError):
|
|
ProviderConfig.model_validate(provider)
|
|
|
|
|
|
class TestIdPatterns:
|
|
@pytest.mark.parametrize(
|
|
"value",
|
|
["Zhipu", "-abc", ".abc", "_abc", "", "a" * 65, "abc$", "abc def", "ABC"],
|
|
)
|
|
def test_provider_id_rejects_invalid_patterns(self, value):
|
|
with pytest.raises(ValidationError):
|
|
ModelRef(provider_id=value, model_key="glm-5.2")
|
|
|
|
@pytest.mark.parametrize(
|
|
"value", ["zhipu-glm", "a", "a.b_c-d", "glm-5.2", "0abc", "x" * 64]
|
|
)
|
|
def test_provider_id_accepts_valid_patterns(self, value):
|
|
ref = ModelRef(provider_id=value, model_key="glm-5.2")
|
|
assert ref.provider_id == value
|
|
|
|
def test_credential_id_uses_same_pattern(self):
|
|
with pytest.raises(ValidationError):
|
|
AuthConfig(mode="api_key", credential_id="Invalid Id")
|
|
|
|
def test_upstream_model_id_preserves_case_and_bounds_length(self):
|
|
model = ModelConfig.model_validate(
|
|
{
|
|
"key": "glm-5.2",
|
|
"name": "GLM-5.2",
|
|
"upstream_model_id": "GLM-5.2-Air.X",
|
|
"enabled": True,
|
|
"runtime": _model_runtime(),
|
|
}
|
|
)
|
|
assert model.upstream_model_id == "GLM-5.2-Air.X"
|
|
with pytest.raises(ValidationError):
|
|
ModelConfig.model_validate(
|
|
{
|
|
"key": "glm-5.2",
|
|
"name": "GLM-5.2",
|
|
"upstream_model_id": "m" * 301,
|
|
"enabled": True,
|
|
"runtime": _model_runtime(),
|
|
}
|
|
)
|
|
with pytest.raises(ValidationError):
|
|
ModelConfig.model_validate(
|
|
{
|
|
"key": "glm-5.2",
|
|
"name": "GLM-5.2",
|
|
"upstream_model_id": "",
|
|
"enabled": True,
|
|
"runtime": _model_runtime(),
|
|
}
|
|
)
|
|
|
|
|
|
class TestRuntimeRanges:
|
|
@pytest.mark.parametrize("value", [9, 601, 0, -10])
|
|
def test_timeout_seconds_range(self, value):
|
|
with pytest.raises(ValidationError):
|
|
ProviderRuntimeConfig(timeout_seconds=value)
|
|
|
|
@pytest.mark.parametrize("value", [10, 120, 600])
|
|
def test_timeout_seconds_accepts_bounds(self, value):
|
|
assert ProviderRuntimeConfig(timeout_seconds=value).timeout_seconds == value
|
|
|
|
@pytest.mark.parametrize("value", [-1, 6])
|
|
def test_max_retries_range(self, value):
|
|
with pytest.raises(ValidationError):
|
|
ProviderRuntimeConfig(max_retries=value)
|
|
|
|
def test_temperature_upper_bound_is_two(self):
|
|
assert ProviderRuntimeConfig(default_temperature=2).default_temperature == 2
|
|
with pytest.raises(ValidationError):
|
|
ProviderRuntimeConfig(default_temperature=2.5)
|
|
with pytest.raises(ValidationError):
|
|
ModelRuntimeConfig.model_validate(_model_runtime(temperature=2.5))
|
|
with pytest.raises(ValidationError):
|
|
ModelRuntimeConfig.model_validate(_model_runtime(temperature=-0.1))
|
|
|
|
def test_top_p_is_open_closed_interval(self):
|
|
with pytest.raises(ValidationError):
|
|
ProviderRuntimeConfig(default_top_p=0)
|
|
with pytest.raises(ValidationError):
|
|
ProviderRuntimeConfig(default_top_p=1.5)
|
|
assert ProviderRuntimeConfig(default_top_p=1).default_top_p == 1
|
|
|
|
def test_reasoning_effort_defaults_to_auto(self):
|
|
assert ProviderRuntimeConfig().default_reasoning_effort == "auto"
|
|
runtime = ModelRuntimeConfig.model_validate(_model_runtime())
|
|
assert runtime.reasoning_effort == "auto"
|
|
with pytest.raises(ValidationError):
|
|
ModelRuntimeConfig.model_validate(_model_runtime(reasoning_effort="max"))
|
|
|
|
def test_min_effective_input_tokens_default_and_floor(self):
|
|
runtime = ModelRuntimeConfig.model_validate(
|
|
_model_runtime(min_effective_input_tokens=4096)
|
|
)
|
|
assert runtime.min_effective_input_tokens == 4096
|
|
with pytest.raises(ValidationError):
|
|
ModelRuntimeConfig.model_validate(
|
|
_model_runtime(min_effective_input_tokens=1023)
|
|
)
|
|
|
|
def test_fixed_reserves_are_non_negative(self):
|
|
with pytest.raises(ValidationError):
|
|
ModelRuntimeConfig.model_validate(
|
|
_model_runtime(fixed_system_reserve_tokens=-1)
|
|
)
|
|
|
|
|
|
class TestLimitModes:
|
|
def test_combined_requires_context_window(self):
|
|
with pytest.raises(ValidationError):
|
|
ModelRuntimeConfig.model_validate(
|
|
_model_runtime(limit_mode="combined", context_window_tokens=None)
|
|
)
|
|
|
|
def test_input_only_requires_max_input_tokens(self):
|
|
with pytest.raises(ValidationError):
|
|
ModelRuntimeConfig.model_validate(
|
|
_model_runtime(limit_mode="input_only", max_input_tokens=None)
|
|
)
|
|
|
|
def test_input_only_valid(self):
|
|
runtime = ModelRuntimeConfig.model_validate(
|
|
_model_runtime(
|
|
limit_mode="input_only",
|
|
context_window_tokens=None,
|
|
max_input_tokens=131072,
|
|
)
|
|
)
|
|
assert runtime.max_input_tokens == 131072
|
|
|
|
|
|
class TestAuthConfig:
|
|
def test_mode_none_forbids_credential_id(self):
|
|
with pytest.raises(ValidationError):
|
|
AuthConfig(mode="none", credential_id="zhipu-primary")
|
|
|
|
def test_mode_none_allows_null_credential(self):
|
|
auth = AuthConfig(mode="none", credential_id=None)
|
|
assert auth.credential_id is None
|
|
|
|
def test_api_key_allows_credential_reference(self):
|
|
auth = AuthConfig(mode="api_key", credential_id="zhipu-primary")
|
|
assert auth.mode == "api_key"
|
|
|
|
def test_unknown_mode_rejected(self):
|
|
with pytest.raises(ValidationError):
|
|
AuthConfig(mode="oauth")
|
|
|
|
|
|
class TestAdapterContracts:
|
|
def _glm_spec_payload(self):
|
|
return {
|
|
"adapter_id": "openai-compatible",
|
|
"spec_revision": 1,
|
|
"model_selector": "glm-5.2",
|
|
"connection": {
|
|
"chat_model": "ChatOpenAI",
|
|
"model_field": "model",
|
|
"base_url_field": "base_url",
|
|
},
|
|
"auth_specs": {
|
|
"api_key": {
|
|
"credential_required": True,
|
|
"credential_kind": "api_key",
|
|
"target": "client_option",
|
|
"target_name": "api_key",
|
|
}
|
|
},
|
|
"parameters": {
|
|
"timeout_seconds": {
|
|
"supported": True,
|
|
"value_type": "integer",
|
|
"minimum": 10,
|
|
"maximum": 600,
|
|
"nullable": "forbidden",
|
|
"target": "client_option",
|
|
"target_name": "timeout",
|
|
"normalizer": "identity",
|
|
},
|
|
"temperature": {
|
|
"supported": True,
|
|
"value_type": "number",
|
|
"minimum": 0,
|
|
"maximum": 1,
|
|
"nullable": "omit",
|
|
"target": "request_option",
|
|
"target_name": "temperature",
|
|
"normalizer": "omit_when_none",
|
|
},
|
|
"reasoning_effort": {
|
|
"supported": False,
|
|
"value_type": "enum",
|
|
"nullable": "omit",
|
|
"target": "request_option",
|
|
"target_name": "",
|
|
"normalizer": "reject_non_auto",
|
|
},
|
|
},
|
|
"protocol_capabilities": {
|
|
"tools": True,
|
|
"vision": False,
|
|
"structured_output": True,
|
|
},
|
|
}
|
|
|
|
def test_adapter_parameter_spec_from_design_doc(self):
|
|
spec = AdapterParameterSpec.model_validate(self._glm_spec_payload())
|
|
assert spec.adapter_id == "openai-compatible"
|
|
assert spec.spec_revision == 1
|
|
assert spec.model_selector == "glm-5.2"
|
|
assert spec.auth_specs["api_key"].credential_kind == "api_key"
|
|
assert spec.parameters["temperature"].maximum == 1
|
|
assert spec.parameters["reasoning_effort"].supported is False
|
|
assert spec.protocol_capabilities.structured_output is True
|
|
|
|
def test_auth_spec_rejects_unknown_credential_kind(self):
|
|
with pytest.raises(ValidationError):
|
|
AuthSpec(
|
|
credential_required=True,
|
|
credential_kind="oauth_token",
|
|
target="client_option",
|
|
target_name="api_key",
|
|
)
|
|
|
|
def test_parameter_rule_conflicts_default_empty(self):
|
|
rule = ParameterRule(
|
|
supported=True,
|
|
value_type="integer",
|
|
nullable="forbidden",
|
|
target="client_option",
|
|
target_name="max_retries",
|
|
normalizer="identity",
|
|
)
|
|
assert rule.conflicts_with == []
|
|
|
|
|
|
class TestAvailabilityAndResolvedConfig:
|
|
def test_model_availability_shape(self):
|
|
availability = ModelAvailability(
|
|
model_ref=ModelRef(provider_id="zhipu-glm", model_key="glm-5.2"),
|
|
state="enabled",
|
|
selectable=True,
|
|
reason_code=None,
|
|
verification=VerificationInfo(status="passed"),
|
|
effective_capabilities={
|
|
"tools": True,
|
|
"vision": False,
|
|
"structured_output": True,
|
|
},
|
|
)
|
|
assert availability.selectable is True
|
|
assert availability.verification.status == "passed"
|
|
|
|
def test_resolved_model_config_has_no_secret_value(self):
|
|
resolved = ResolvedModelConfig(
|
|
model_ref=ModelRef(provider_id="zhipu-glm", model_key="glm-5.2"),
|
|
role="primary",
|
|
adapter_id="openai-compatible",
|
|
adapter_spec_revision=1,
|
|
upstream_model_id="glm-5.2",
|
|
base_url="https://open.bigmodel.cn/api/paas/v4",
|
|
auth_ref=AuthRef(
|
|
mode="api_key", credential_id="zhipu-primary", credential_revision=3
|
|
),
|
|
client_options={"timeout_seconds": 120, "max_retries": 2},
|
|
request_options={
|
|
"max_output_tokens": 32768,
|
|
"temperature": 0.7,
|
|
"top_p": 0.95,
|
|
"reasoning_effort": "auto",
|
|
},
|
|
budget={
|
|
"resolved_input_limit": 1048576 - 32768,
|
|
"fixed_reserves": {
|
|
"fixed_system_reserve_tokens": 4096,
|
|
"fixed_tools_reserve_tokens": 8192,
|
|
"fixed_attachments_reserve_tokens": 4096,
|
|
},
|
|
"message_budget": 1048576 - 32768 - 16384,
|
|
},
|
|
effective_capabilities={
|
|
"tools": True,
|
|
"vision": False,
|
|
"structured_output": True,
|
|
},
|
|
)
|
|
assert "secret" not in ResolvedModelConfig.model_fields
|
|
assert "secret_value" not in AuthRef.model_fields
|
|
assert resolved.auth_ref.credential_revision == 3
|
|
|
|
def test_auth_ref_mode_none_forbids_credential(self):
|
|
with pytest.raises(ValidationError):
|
|
AuthRef(mode="none", credential_id="zhipu-primary")
|
|
|
|
|
|
class TestCredentialModels:
|
|
def test_credential_status_shape(self):
|
|
status = CredentialStatus(
|
|
credential_id="zhipu-primary",
|
|
configured=True,
|
|
hint="...abcd",
|
|
updated_at="2026-07-20T00:00:00Z",
|
|
)
|
|
assert status.hint == "...abcd"
|
|
|
|
def test_credential_write_requires_replace_operation(self):
|
|
write = CredentialWrite(
|
|
credential_id="zhipu-primary", secret_value="sk-test-1234"
|
|
)
|
|
assert write.operation == "replace"
|
|
with pytest.raises(ValidationError):
|
|
CredentialWrite(
|
|
credential_id="zhipu-primary",
|
|
operation="append",
|
|
secret_value="sk-test-1234",
|
|
)
|
|
|
|
|
|
class TestErrorTaxonomy:
|
|
def test_error_table_covers_all_documented_codes(self):
|
|
assert ERROR_HTTP_STATUS == ALL_ERROR_CODES
|
|
|
|
def test_spot_check_http_mappings(self):
|
|
assert ERROR_HTTP_STATUS[REGISTRY_REVISION_CONFLICT] == 409
|
|
assert ERROR_HTTP_STATUS[DELEGATION_REPLAYED] == 401
|
|
assert ERROR_HTTP_STATUS[MODEL_NOT_FOUND] == 404
|
|
assert ERROR_HTTP_STATUS[PROVIDER_UNREACHABLE] == 422
|
|
|
|
def test_error_payload_matches_section_9_5(self):
|
|
payload = ErrorPayload(
|
|
code="STABLE_ERROR_CODE",
|
|
message="safe human-readable message",
|
|
details=[
|
|
ErrorDetail(
|
|
path="providers[0].runtime.temperature",
|
|
code="UNSUPPORTED_RUNTIME_PARAMETER",
|
|
)
|
|
],
|
|
request_id="req-1",
|
|
)
|
|
assert payload.model_dump() == {
|
|
"code": "STABLE_ERROR_CODE",
|
|
"message": "safe human-readable message",
|
|
"details": [
|
|
{
|
|
"path": "providers[0].runtime.temperature",
|
|
"code": "UNSUPPORTED_RUNTIME_PARAMETER",
|
|
}
|
|
],
|
|
"request_id": "req-1",
|
|
}
|
|
|
|
def test_registry_error_carries_code_status_and_payload(self):
|
|
error = ModelRegistryError(
|
|
CREDENTIAL_NOT_CONFIGURED,
|
|
"credential is not configured",
|
|
details=[{"path": "providers[0].auth", "code": CREDENTIAL_NOT_CONFIGURED}],
|
|
)
|
|
assert error.code == CREDENTIAL_NOT_CONFIGURED
|
|
assert error.http_status == 422
|
|
payload = error.payload(request_id="req-9")
|
|
assert payload["code"] == CREDENTIAL_NOT_CONFIGURED
|
|
assert payload["request_id"] == "req-9"
|
|
assert payload["details"] == [
|
|
{"path": "providers[0].auth", "code": CREDENTIAL_NOT_CONFIGURED}
|
|
]
|
|
|
|
def test_unknown_error_code_rejected(self):
|
|
with pytest.raises(ValueError, match="Unknown model registry error code"):
|
|
ModelRegistryError("NOT_A_REAL_CODE", "bad")
|
|
|
|
|
|
class TestConfigurationHash:
|
|
def test_hash_is_deterministic(self):
|
|
provider = ProviderConfig.model_validate(_provider())
|
|
first = configuration_hash(provider, provider.models[0])
|
|
second = configuration_hash(provider, provider.models[0])
|
|
assert first == second
|
|
assert len(first) == 64
|
|
|
|
def test_hash_changes_with_runtime_parameters(self):
|
|
provider = ProviderConfig.model_validate(_provider())
|
|
changed = ProviderConfig.model_validate(
|
|
_provider(runtime={"timeout_seconds": 300})
|
|
)
|
|
assert configuration_hash(provider, provider.models[0]) != configuration_hash(
|
|
changed, changed.models[0]
|
|
)
|
|
|
|
def test_hash_changes_with_base_url_and_upstream_model(self):
|
|
provider = ProviderConfig.model_validate(_provider())
|
|
other_url = ProviderConfig.model_validate(
|
|
_provider(base_url="https://example.com/v1")
|
|
)
|
|
assert configuration_hash(provider, provider.models[0]) != configuration_hash(
|
|
other_url, other_url.models[0]
|
|
)
|
|
|
|
def test_hash_changes_with_limits(self):
|
|
provider = ProviderConfig.model_validate(_provider())
|
|
changed_limits = _provider()
|
|
changed_limits["models"][0]["runtime"] = _model_runtime(max_output_tokens=16384)
|
|
other = ProviderConfig.model_validate(changed_limits)
|
|
assert configuration_hash(provider, provider.models[0]) != configuration_hash(
|
|
other, other.models[0]
|
|
)
|