af4ae1aef5
Add the Task 3 parameter contract layer (design doc 6.1-6.4):
- adapters.py: versioned built-in contracts for the five phase-1
adapters plus the openai-compatible/glm-5.2 model-specific contract
(verbatim section 6.2 values); exact > longest glob > generic
matching with spec_revision pinning; resolve_parameters implementing
the section 6.1 inherit/omit semantics, contract validation with
stable error codes, and named normalizers (identity,
clamp_to_model_limit, omit_when_none, omit_when_auto,
reject_non_auto); Adapter.build_request as the single entry point
mapping ResolvedModelConfig to {client_options, request_options};
compute_effective_capabilities (protocol AND declared AND verified).
- factory.py: build_chat_model(resolved_config, http_client, *,
credential=None) with no **kwargs and no setdefault merging; injects
the safe HTTP client into ChatOpenAI/ChatAnthropic/ChatOllama, never
reads provider API-key environment variables, and strips the
OLLAMA_API_KEY authorization header for mode=none adapters.
- tests: per-adapter request-capturing fakes plus an httpx.MockTransport
outbound capture proving registry resolution matches the wire request.
653 lines
25 KiB
Python
653 lines
25 KiB
Python
"""Tests for the adapter parameter contracts (design doc sections 6.1-6.3).
|
|
|
|
Covers the built-in contract registry (five phase-1 adapters plus the
|
|
``openai-compatible``/``glm-5.2`` model-specific contract), the contract
|
|
matching order, save-time parameter resolution with the section 6.1
|
|
inheritance semantics, and ``Adapter.build_request`` mapping.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from EvoScientist.model_registry.adapters import (
|
|
Adapter,
|
|
adapter_specs,
|
|
compute_effective_capabilities,
|
|
find_adapter_spec,
|
|
get_adapter,
|
|
resolve_parameters,
|
|
)
|
|
from EvoScientist.model_registry.errors import (
|
|
ADAPTER_NOT_SUPPORTED,
|
|
AUTH_MODE_UNSUPPORTED,
|
|
CAPABILITY_UNSUPPORTED_BY_ADAPTER,
|
|
CREDENTIAL_NOT_CONFIGURED,
|
|
UNSUPPORTED_RUNTIME_PARAMETER,
|
|
ModelRegistryError,
|
|
)
|
|
from EvoScientist.model_registry.schemas import (
|
|
AdapterParameterSpec,
|
|
Capabilities,
|
|
ModelConfig,
|
|
ModelRuntimeConfig,
|
|
ProviderConfig,
|
|
ResolvedModelConfig,
|
|
)
|
|
|
|
PHASE_ONE_ADAPTERS = [
|
|
"openai",
|
|
"anthropic",
|
|
"openai-compatible",
|
|
"anthropic-compatible",
|
|
"ollama",
|
|
]
|
|
|
|
UNOPENED_ADAPTERS = ["google-genai", "grok", "openrouter", "nvidia", "antigravity"]
|
|
|
|
|
|
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 ProviderConfig.model_validate(payload)
|
|
|
|
|
|
def _model(provider: ProviderConfig, **runtime_overrides) -> ModelConfig:
|
|
model = provider.models[0]
|
|
if not runtime_overrides:
|
|
return model
|
|
runtime = model.runtime.model_dump()
|
|
runtime.update(runtime_overrides)
|
|
return model.model_copy(
|
|
update={"runtime": ModelRuntimeConfig.model_validate(runtime)}
|
|
)
|
|
|
|
|
|
def _resolved_config(**overrides):
|
|
payload = {
|
|
"model_ref": {"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": {
|
|
"mode": "api_key",
|
|
"credential_id": "zhipu-primary",
|
|
"credential_revision": 1,
|
|
},
|
|
"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": 1015808,
|
|
"fixed_reserves": {
|
|
"fixed_system_reserve_tokens": 4096,
|
|
"fixed_tools_reserve_tokens": 8192,
|
|
"fixed_attachments_reserve_tokens": 4096,
|
|
},
|
|
"message_budget": 1000000,
|
|
},
|
|
"effective_capabilities": {
|
|
"tools": True,
|
|
"vision": False,
|
|
"structured_output": True,
|
|
},
|
|
}
|
|
payload.update(overrides)
|
|
return ResolvedModelConfig.model_validate(payload)
|
|
|
|
|
|
class TestContractRegistry:
|
|
@pytest.mark.parametrize("adapter_id", PHASE_ONE_ADAPTERS)
|
|
def test_generic_contract_exists_for_each_phase_one_adapter(self, adapter_id):
|
|
spec = find_adapter_spec(adapter_id, "any-model")
|
|
assert spec is not None
|
|
assert spec.adapter_id == adapter_id
|
|
assert spec.model_selector == "*"
|
|
assert spec.spec_revision == 1
|
|
assert spec.connection is not None
|
|
assert spec.parameters
|
|
|
|
def test_glm_contract_matches_design_doc_verbatim(self):
|
|
spec = find_adapter_spec("openai-compatible", "glm-5.2")
|
|
assert spec is not None
|
|
assert spec.model_selector == "glm-5.2"
|
|
assert spec.spec_revision == 1
|
|
assert spec.connection is not None
|
|
assert spec.connection.chat_model == "ChatOpenAI"
|
|
assert spec.connection.model_field == "model"
|
|
assert spec.connection.base_url_field == "base_url"
|
|
auth = spec.auth_specs["api_key"]
|
|
assert auth.credential_required is True
|
|
assert auth.credential_kind == "api_key"
|
|
assert auth.target == "client_option"
|
|
assert auth.target_name == "api_key"
|
|
|
|
parameters = spec.parameters
|
|
timeout = parameters["timeout_seconds"]
|
|
assert (timeout.supported, timeout.value_type) == (True, "integer")
|
|
assert (timeout.minimum, timeout.maximum) == (10, 600)
|
|
assert timeout.nullable == "forbidden"
|
|
assert (timeout.target, timeout.target_name) == ("client_option", "timeout")
|
|
assert timeout.normalizer == "identity"
|
|
|
|
retries = parameters["max_retries"]
|
|
assert (retries.minimum, retries.maximum) == (0, 5)
|
|
assert (retries.target, retries.target_name) == (
|
|
"client_option",
|
|
"max_retries",
|
|
)
|
|
|
|
max_output = parameters["max_output_tokens"]
|
|
assert (max_output.minimum, max_output.maximum) == (1, 32768)
|
|
assert (max_output.target, max_output.target_name) == (
|
|
"request_option",
|
|
"max_tokens",
|
|
)
|
|
assert max_output.normalizer == "clamp_to_model_limit"
|
|
|
|
temperature = parameters["temperature"]
|
|
assert (temperature.minimum, temperature.maximum) == (0, 1)
|
|
assert temperature.nullable == "omit"
|
|
assert temperature.normalizer == "omit_when_none"
|
|
|
|
top_p = parameters["top_p"]
|
|
assert (top_p.minimum, top_p.maximum) == (0.000001, 1)
|
|
|
|
reasoning = parameters["reasoning_effort"]
|
|
assert reasoning.supported is False
|
|
assert reasoning.nullable == "omit"
|
|
assert reasoning.target_name == ""
|
|
assert reasoning.normalizer == "reject_non_auto"
|
|
|
|
assert spec.protocol_capabilities == Capabilities(
|
|
tools=True, vision=False, structured_output=True
|
|
)
|
|
|
|
def test_exact_match_beats_generic_contract(self):
|
|
spec = find_adapter_spec("openai-compatible", "glm-5.2")
|
|
assert spec is not None
|
|
assert spec.model_selector == "glm-5.2"
|
|
|
|
def test_other_models_fall_back_to_generic_contract(self):
|
|
spec = find_adapter_spec("openai-compatible", "some-other-model")
|
|
assert spec is not None
|
|
assert spec.model_selector == "*"
|
|
|
|
def test_longest_glob_beats_shorter_glob(self):
|
|
extra = [
|
|
spec.model_copy(update={"model_selector": "glm-*"})
|
|
for spec in adapter_specs()
|
|
if spec.adapter_id == "openai-compatible"
|
|
and spec.model_selector == "glm-5.2"
|
|
]
|
|
spec = find_adapter_spec(
|
|
"openai-compatible", "glm-5.2-flash", specs=[*extra, *adapter_specs()]
|
|
)
|
|
assert spec is not None
|
|
assert spec.model_selector == "glm-*"
|
|
|
|
@pytest.mark.parametrize("adapter_id", UNOPENED_ADAPTERS)
|
|
def test_unopened_adapter_ids_rejected(self, adapter_id):
|
|
with pytest.raises(ModelRegistryError) as excinfo:
|
|
find_adapter_spec(adapter_id, "any-model")
|
|
assert excinfo.value.code == ADAPTER_NOT_SUPPORTED
|
|
|
|
def test_unknown_adapter_id_rejected(self):
|
|
with pytest.raises(ModelRegistryError) as excinfo:
|
|
find_adapter_spec("made-up-adapter", "any-model")
|
|
assert excinfo.value.code == ADAPTER_NOT_SUPPORTED
|
|
|
|
def test_pinned_spec_revision_must_exist(self):
|
|
assert (
|
|
find_adapter_spec("openai-compatible", "glm-5.2", spec_revision=1)
|
|
is not None
|
|
)
|
|
assert (
|
|
find_adapter_spec("openai-compatible", "glm-5.2", spec_revision=99) is None
|
|
)
|
|
with pytest.raises(ModelRegistryError) as excinfo:
|
|
get_adapter("openai-compatible", "glm-5.2", spec_revision=99)
|
|
assert excinfo.value.code == ADAPTER_NOT_SUPPORTED
|
|
|
|
|
|
class TestGlm52Resolution:
|
|
def test_design_doc_mapping(self):
|
|
provider = _provider()
|
|
spec = find_adapter_spec("openai-compatible", "glm-5.2")
|
|
assert spec is not None
|
|
resolved = resolve_parameters(provider, provider.models[0], spec)
|
|
assert resolved.timeout_seconds == 120
|
|
assert resolved.max_retries == 2
|
|
assert resolved.max_output_tokens == 32768
|
|
assert resolved.temperature == 0.7
|
|
assert resolved.top_p == 0.95
|
|
assert resolved.reasoning_effort == "auto"
|
|
|
|
adapter = Adapter(spec)
|
|
request = adapter.build_request(_resolved_config(), credential="test-secret")
|
|
assert request.client_options == {
|
|
"model": "glm-5.2",
|
|
"base_url": "https://open.bigmodel.cn/api/paas/v4",
|
|
"timeout": 120,
|
|
"max_retries": 2,
|
|
"api_key": "test-secret",
|
|
}
|
|
assert request.request_options == {
|
|
"max_tokens": 32768,
|
|
"temperature": 0.7,
|
|
"top_p": 0.95,
|
|
}
|
|
|
|
def test_model_value_overrides_provider_default(self):
|
|
provider = _provider()
|
|
model = _model(provider, temperature=0.4)
|
|
spec = find_adapter_spec("openai-compatible", "glm-5.2")
|
|
assert spec is not None
|
|
resolved = resolve_parameters(provider, model, spec)
|
|
assert resolved.temperature == 0.4
|
|
|
|
def test_provider_null_omits_field_and_never_sends_zero(self):
|
|
provider = _provider()
|
|
provider = provider.model_copy(
|
|
update={
|
|
"runtime": provider.runtime.model_copy(
|
|
update={"default_temperature": None, "default_top_p": None}
|
|
)
|
|
}
|
|
)
|
|
spec = find_adapter_spec("openai-compatible", "glm-5.2")
|
|
assert spec is not None
|
|
resolved = resolve_parameters(provider, provider.models[0], spec)
|
|
assert resolved.temperature is None
|
|
assert resolved.top_p is None
|
|
|
|
adapter = Adapter(spec)
|
|
request = adapter.build_request(
|
|
_resolved_config(
|
|
request_options={
|
|
"max_output_tokens": 32768,
|
|
"temperature": None,
|
|
"top_p": None,
|
|
"reasoning_effort": "auto",
|
|
}
|
|
),
|
|
credential="test-secret",
|
|
)
|
|
assert "temperature" not in request.request_options
|
|
assert "top_p" not in request.request_options
|
|
assert request.request_options.get("temperature") != 0
|
|
|
|
def test_reasoning_effort_auto_is_omitted(self):
|
|
adapter = get_adapter("openai-compatible", "glm-5.2")
|
|
request = adapter.build_request(_resolved_config(), credential="test-secret")
|
|
assert all("reasoning" not in key for key in request.request_options)
|
|
|
|
def test_reasoning_effort_non_auto_rejected_at_save(self):
|
|
provider = _provider()
|
|
model = _model(provider, reasoning_effort="high")
|
|
spec = find_adapter_spec("openai-compatible", "glm-5.2")
|
|
assert spec is not None
|
|
with pytest.raises(ModelRegistryError) as excinfo:
|
|
resolve_parameters(provider, model, spec)
|
|
assert excinfo.value.code == UNSUPPORTED_RUNTIME_PARAMETER
|
|
|
|
def test_inherited_reasoning_effort_non_auto_rejected(self):
|
|
provider = _provider()
|
|
provider = provider.model_copy(
|
|
update={
|
|
"runtime": provider.runtime.model_copy(
|
|
update={"default_reasoning_effort": "high"}
|
|
)
|
|
}
|
|
)
|
|
spec = find_adapter_spec("openai-compatible", "glm-5.2")
|
|
assert spec is not None
|
|
with pytest.raises(ModelRegistryError) as excinfo:
|
|
resolve_parameters(provider, provider.models[0], spec)
|
|
assert excinfo.value.code == UNSUPPORTED_RUNTIME_PARAMETER
|
|
|
|
def test_reasoning_effort_non_auto_rejected_at_run(self):
|
|
adapter = get_adapter("openai-compatible", "glm-5.2")
|
|
resolved = _resolved_config(
|
|
request_options={
|
|
"max_output_tokens": 32768,
|
|
"temperature": 0.7,
|
|
"top_p": 0.95,
|
|
"reasoning_effort": "high",
|
|
}
|
|
)
|
|
with pytest.raises(ModelRegistryError) as excinfo:
|
|
adapter.build_request(resolved, credential="test-secret")
|
|
assert excinfo.value.code == UNSUPPORTED_RUNTIME_PARAMETER
|
|
|
|
def test_temperature_above_contract_maximum_rejected(self):
|
|
provider = _provider()
|
|
model = _model(provider, temperature=1.5)
|
|
spec = find_adapter_spec("openai-compatible", "glm-5.2")
|
|
assert spec is not None
|
|
with pytest.raises(ModelRegistryError) as excinfo:
|
|
resolve_parameters(provider, model, spec)
|
|
assert excinfo.value.code == UNSUPPORTED_RUNTIME_PARAMETER
|
|
|
|
def test_top_p_below_contract_minimum_rejected(self):
|
|
provider = _provider()
|
|
model = _model(provider, top_p=0.0000001)
|
|
spec = find_adapter_spec("openai-compatible", "glm-5.2")
|
|
assert spec is not None
|
|
with pytest.raises(ModelRegistryError) as excinfo:
|
|
resolve_parameters(provider, model, spec)
|
|
assert excinfo.value.code == UNSUPPORTED_RUNTIME_PARAMETER
|
|
|
|
def test_max_output_tokens_clamped_to_model_limit(self):
|
|
provider = _provider()
|
|
model = _model(provider, max_output_tokens=40000)
|
|
spec = find_adapter_spec("openai-compatible", "glm-5.2")
|
|
assert spec is not None
|
|
resolved = resolve_parameters(provider, model, spec)
|
|
assert resolved.max_output_tokens == 32768
|
|
|
|
def test_declared_vision_capability_rejected(self):
|
|
provider = _provider()
|
|
model = _model(
|
|
provider,
|
|
declared_capabilities={
|
|
"tools": True,
|
|
"vision": True,
|
|
"structured_output": True,
|
|
},
|
|
)
|
|
spec = find_adapter_spec("openai-compatible", "glm-5.2")
|
|
assert spec is not None
|
|
with pytest.raises(ModelRegistryError) as excinfo:
|
|
resolve_parameters(provider, model, spec)
|
|
assert excinfo.value.code == CAPABILITY_UNSUPPORTED_BY_ADAPTER
|
|
|
|
def test_auth_mode_outside_contract_rejected(self):
|
|
provider = _provider(auth={"mode": "bearer", "credential_id": "zhipu-primary"})
|
|
spec = find_adapter_spec("openai-compatible", "glm-5.2")
|
|
assert spec is not None
|
|
with pytest.raises(ModelRegistryError) as excinfo:
|
|
resolve_parameters(provider, provider.models[0], spec)
|
|
assert excinfo.value.code == AUTH_MODE_UNSUPPORTED
|
|
|
|
def test_missing_credential_reference_rejected(self):
|
|
provider = _provider(auth={"mode": "api_key", "credential_id": None})
|
|
spec = find_adapter_spec("openai-compatible", "glm-5.2")
|
|
assert spec is not None
|
|
with pytest.raises(ModelRegistryError) as excinfo:
|
|
resolve_parameters(provider, provider.models[0], spec)
|
|
assert excinfo.value.code == CREDENTIAL_NOT_CONFIGURED
|
|
|
|
def test_build_request_requires_credential(self):
|
|
adapter = get_adapter("openai-compatible", "glm-5.2")
|
|
with pytest.raises(ModelRegistryError) as excinfo:
|
|
adapter.build_request(_resolved_config())
|
|
assert excinfo.value.code == CREDENTIAL_NOT_CONFIGURED
|
|
|
|
def test_build_request_rejects_unsupported_auth_mode(self):
|
|
adapter = get_adapter("openai-compatible", "glm-5.2")
|
|
resolved = _resolved_config(
|
|
auth_ref={
|
|
"mode": "bearer",
|
|
"credential_id": "zhipu-primary",
|
|
"credential_revision": 1,
|
|
}
|
|
)
|
|
with pytest.raises(ModelRegistryError) as excinfo:
|
|
adapter.build_request(resolved, credential="test-secret")
|
|
assert excinfo.value.code == AUTH_MODE_UNSUPPORTED
|
|
|
|
|
|
def _anthropic_models():
|
|
"""Anthropic protocol declares no structured_output in phase 1."""
|
|
return [
|
|
{
|
|
"key": "glm-5.2",
|
|
"name": "GLM-5.2",
|
|
"upstream_model_id": "glm-5.2",
|
|
"enabled": True,
|
|
"runtime": _model_runtime(
|
|
declared_capabilities={
|
|
"tools": True,
|
|
"vision": False,
|
|
"structured_output": False,
|
|
}
|
|
),
|
|
}
|
|
]
|
|
|
|
|
|
class TestGenericContractMappings:
|
|
def _resolve_and_build(self, provider: ProviderConfig, credential=None):
|
|
model = provider.models[0]
|
|
spec = find_adapter_spec(provider.adapter, model.upstream_model_id)
|
|
assert spec is not None
|
|
resolved = resolve_parameters(provider, model, spec)
|
|
config = _resolved_config(
|
|
model_ref={"provider_id": provider.id, "model_key": model.key},
|
|
adapter_id=provider.adapter,
|
|
adapter_spec_revision=spec.spec_revision,
|
|
upstream_model_id=model.upstream_model_id,
|
|
base_url=provider.base_url,
|
|
auth_ref={
|
|
"mode": provider.auth.mode,
|
|
"credential_id": provider.auth.credential_id,
|
|
"credential_revision": (
|
|
1 if provider.auth.credential_id is not None else None
|
|
),
|
|
},
|
|
client_options={
|
|
"timeout_seconds": resolved.timeout_seconds,
|
|
"max_retries": resolved.max_retries,
|
|
},
|
|
request_options={
|
|
"max_output_tokens": resolved.max_output_tokens,
|
|
"temperature": resolved.temperature,
|
|
"top_p": resolved.top_p,
|
|
"reasoning_effort": resolved.reasoning_effort,
|
|
},
|
|
)
|
|
return spec, Adapter(spec).build_request(config, credential=credential)
|
|
|
|
def test_openai_generic_contract(self):
|
|
provider = _provider(
|
|
id="openai-main",
|
|
adapter="openai",
|
|
base_url="https://api.openai.com/v1",
|
|
)
|
|
spec, request = self._resolve_and_build(provider, credential="sk-test")
|
|
assert spec.connection is not None
|
|
assert spec.connection.chat_model == "ChatOpenAI"
|
|
assert request.client_options["model"] == "glm-5.2"
|
|
assert request.client_options["timeout"] == 120
|
|
assert request.client_options["max_retries"] == 2
|
|
assert request.client_options["api_key"] == "sk-test"
|
|
assert request.request_options["max_tokens"] == 32768
|
|
assert request.request_options["temperature"] == 0.7
|
|
|
|
def test_openai_generic_contract_supports_reasoning_effort(self):
|
|
provider = _provider(
|
|
id="openai-main",
|
|
adapter="openai",
|
|
base_url="https://api.openai.com/v1",
|
|
)
|
|
provider.models[0].runtime.reasoning_effort = "low"
|
|
spec = find_adapter_spec("openai", "glm-5.2")
|
|
assert spec is not None
|
|
resolved = resolve_parameters(provider, provider.models[0], spec)
|
|
assert resolved.reasoning_effort == "low"
|
|
_, request = self._resolve_and_build(provider, credential="sk-test")
|
|
assert request.request_options["reasoning_effort"] == "low"
|
|
|
|
def test_anthropic_generic_contract(self):
|
|
provider = _provider(
|
|
id="anthropic-main",
|
|
adapter="anthropic",
|
|
base_url="https://api.anthropic.com",
|
|
auth={"mode": "api_key", "credential_id": "anthropic-primary"},
|
|
models=_anthropic_models(),
|
|
)
|
|
spec, request = self._resolve_and_build(provider, credential="sk-ant")
|
|
assert spec.connection is not None
|
|
assert spec.connection.chat_model == "ChatAnthropic"
|
|
assert request.client_options["max_retries"] == 2
|
|
assert request.client_options["api_key"] == "sk-ant"
|
|
assert request.request_options["max_tokens"] == 32768
|
|
assert "reasoning_effort" not in request.request_options
|
|
|
|
def test_anthropic_rejects_non_auto_reasoning_effort(self):
|
|
provider = _provider(
|
|
id="anthropic-main",
|
|
adapter="anthropic",
|
|
base_url="https://api.anthropic.com",
|
|
models=_anthropic_models(),
|
|
)
|
|
provider.models[0].runtime.reasoning_effort = "medium"
|
|
spec = find_adapter_spec("anthropic", "glm-5.2")
|
|
assert spec is not None
|
|
with pytest.raises(ModelRegistryError) as excinfo:
|
|
resolve_parameters(provider, provider.models[0], spec)
|
|
assert excinfo.value.code == UNSUPPORTED_RUNTIME_PARAMETER
|
|
|
|
def test_openai_compatible_bearer_auth_maps_to_header(self):
|
|
provider = _provider(
|
|
id="compat",
|
|
adapter="openai-compatible",
|
|
base_url="https://gateway.example.com/v1",
|
|
auth={"mode": "bearer", "credential_id": "compat-token"},
|
|
models=[
|
|
{
|
|
"key": "other",
|
|
"name": "Other",
|
|
"upstream_model_id": "other-model",
|
|
"enabled": True,
|
|
"runtime": _model_runtime(),
|
|
}
|
|
],
|
|
)
|
|
spec, request = self._resolve_and_build(provider, credential="tok-123")
|
|
assert spec.model_selector == "*"
|
|
assert request.client_options["default_headers"] == {
|
|
"Authorization": "Bearer tok-123"
|
|
}
|
|
assert "api_key" not in request.client_options
|
|
|
|
def test_anthropic_compatible_generic_contract(self):
|
|
provider = _provider(
|
|
id="ant-compat",
|
|
adapter="anthropic-compatible",
|
|
base_url="https://anthropic-gateway.example.com",
|
|
models=_anthropic_models(),
|
|
)
|
|
spec, request = self._resolve_and_build(provider, credential="sk-ant")
|
|
assert spec.connection is not None
|
|
assert spec.connection.chat_model == "ChatAnthropic"
|
|
assert request.client_options["base_url"] == (
|
|
"https://anthropic-gateway.example.com"
|
|
)
|
|
assert request.request_options["max_tokens"] == 32768
|
|
|
|
def test_ollama_generic_contract_needs_no_credential(self):
|
|
provider = _provider(
|
|
id="local-ollama",
|
|
adapter="ollama",
|
|
base_url="http://localhost:11434",
|
|
auth={"mode": "none", "credential_id": None},
|
|
)
|
|
spec, request = self._resolve_and_build(provider)
|
|
assert spec.connection is not None
|
|
assert spec.connection.chat_model == "ChatOllama"
|
|
assert request.client_options["model"] == "glm-5.2"
|
|
assert request.client_options["base_url"] == "http://localhost:11434"
|
|
assert request.client_options["client_kwargs"] == {"timeout": 120}
|
|
assert request.request_options["num_predict"] == 32768
|
|
assert not any(
|
|
"key" in option or "authorization" in option.lower()
|
|
for option in request.client_options
|
|
)
|
|
|
|
def test_ollama_mode_none_discards_credential(self):
|
|
provider = _provider(
|
|
id="local-ollama",
|
|
adapter="ollama",
|
|
base_url="http://localhost:11434",
|
|
auth={"mode": "none", "credential_id": None},
|
|
)
|
|
_spec, request = self._resolve_and_build(provider, credential="ignored")
|
|
assert "api_key" not in request.client_options
|
|
assert "default_headers" not in request.client_options
|
|
|
|
|
|
class TestEffectiveCapabilities:
|
|
def test_intersection_rule(self):
|
|
protocol = Capabilities(tools=True, vision=False, structured_output=True)
|
|
declared = Capabilities(tools=True, vision=True, structured_output=True)
|
|
verified = Capabilities(tools=True, vision=True, structured_output=False)
|
|
effective = compute_effective_capabilities(protocol, declared, verified)
|
|
assert effective.tools is True
|
|
assert effective.vision is False
|
|
assert effective.structured_output is False
|
|
|
|
def test_all_true_when_every_layer_agrees(self):
|
|
all_true = Capabilities(tools=True, vision=True, structured_output=True)
|
|
effective = compute_effective_capabilities(all_true, all_true, all_true)
|
|
assert effective == all_true
|
|
|
|
def test_adapter_spec_type_is_schema_model(self):
|
|
for spec in adapter_specs():
|
|
assert isinstance(spec, AdapterParameterSpec)
|