diff --git a/src/model_behavior_quirks.py b/src/model_behavior_quirks.py deleted file mode 100644 index 5eb5011ae..000000000 --- a/src/model_behavior_quirks.py +++ /dev/null @@ -1,297 +0,0 @@ -"""Canonical, exact-match model/provider behavior observations. - -The registry captures behavior that cannot safely be promoted to a provider- -wide capability. Selectors accept already-structured identity (provider, -model ID/family/version, API dialect, and canonical capabilities); they never -extract those facts from a display name with regexes or substring matching. - -This is shape/evidence data only. Runtime request builders can consume it in a -later integration pass after their endpoint has supplied structured identity. -""" - -from __future__ import annotations - -from collections.abc import Iterable, Mapping -from dataclasses import dataclass -from typing import Any - -from src import model_capabilities as mc -from src import provider_capability_schemas as pcs - - -def _identity(value: Any) -> str: - return str(value or "").strip().lower() - - -def _version(value: Any) -> tuple[int, ...]: - if not isinstance(value, (list, tuple)): - return () - out: list[int] = [] - for part in value: - try: - out.append(int(part)) - except (TypeError, ValueError): - return () - return tuple(out) - - -@dataclass(frozen=True) -class ModelBehaviorSelector: - providers: tuple[str, ...] = () - model_ids: tuple[str, ...] = () - model_families: tuple[str, ...] = () - minimum_model_version: tuple[int, ...] = () - minimum_provider_version: tuple[int, ...] = () - api_dialects: tuple[str, ...] = () - required_capabilities: tuple[str, ...] = () - - def matches( - self, - *, - provider: Any, - model_id: Any = "", - model_family: Any = "", - model_version: Any = (), - provider_version: Any = (), - api_dialect: Any = "", - capabilities: Any = (), - ) -> bool: - provider_id = pcs.normalize_provider_id(provider) - if self.providers and provider_id not in self.providers: - return False - - identity_constraints = bool(self.model_ids or self.model_families) - identity_match = ( - _identity(model_id) in self.model_ids - or _identity(model_family) in self.model_families - ) - if identity_constraints and not identity_match: - return False - - actual_model_version = _version(model_version) - if self.minimum_model_version and ( - not actual_model_version or actual_model_version < self.minimum_model_version - ): - return False - actual_provider_version = _version(provider_version) - if self.minimum_provider_version and ( - not actual_provider_version or actual_provider_version < self.minimum_provider_version - ): - return False - if self.api_dialects and str(api_dialect or "").strip() not in self.api_dialects: - return False - - if isinstance(capabilities, Mapping): - capability_values: Iterable[Any] = ( - key for key, enabled in capabilities.items() if enabled is True - ) - elif isinstance(capabilities, str): - capability_values = (capabilities,) - elif isinstance(capabilities, Iterable): - capability_values = capabilities - else: - capability_values = () - normalized_caps = { - normalized - for value in capability_values - if (normalized := mc.normalize_capability(value)) - } - return set(self.required_capabilities).issubset(normalized_caps) - - -@dataclass(frozen=True) -class ModelBehaviorQuirk: - quirk_id: str - selector: ModelBehaviorSelector - request_omit_paths: tuple[str, ...] = () - request_fixed_values: tuple[tuple[str, Any], ...] = () - required_history_paths: tuple[str, ...] = () - response_reasoning_paths: tuple[str, ...] = () - reasoning_controls: tuple[mc.ReasoningControl, ...] = () - status: str = mc.ASSERTION_CLAIMED - source: str = mc.SOURCE_PROVIDER_DOCS_REGISTRY - confidence: str = mc.CONFIDENCE_REGISTRY - evidence_refs: tuple[str, ...] = () - - def to_dict(self) -> dict[str, Any]: - return { - "quirk_id": self.quirk_id, - "selector": { - "providers": list(self.selector.providers), - "model_ids": list(self.selector.model_ids), - "model_families": list(self.selector.model_families), - "minimum_model_version": list(self.selector.minimum_model_version), - "minimum_provider_version": list(self.selector.minimum_provider_version), - "api_dialects": list(self.selector.api_dialects), - "required_capabilities": list(self.selector.required_capabilities), - }, - "request_omit_paths": list(self.request_omit_paths), - "request_fixed_values": dict(self.request_fixed_values), - "required_history_paths": list(self.required_history_paths), - "response_reasoning_paths": list(self.response_reasoning_paths), - "reasoning_controls": [control.to_dict() for control in self.reasoning_controls], - "status": self.status, - "source": self.source, - "confidence": self.confidence, - "evidence_refs": list(self.evidence_refs), - } - - -MODEL_BEHAVIOR_QUIRKS = ( - ModelBehaviorQuirk( - quirk_id="moonshot.kimi-k2.5-k2.6.provider-fixed-temperature", - selector=ModelBehaviorSelector( - providers=("moonshot",), - model_ids=("kimi-k2.5", "kimi-k2.6"), - model_families=("kimi-k2.5", "kimi-k2.6"), - api_dialects=(pcs.DIALECT_OPENAI_CHAT,), - ), - request_omit_paths=("temperature",), - evidence_refs=( - "github:odysseus-dev/odysseus#3960", - "commit:f5d3e509", - ), - ), - ModelBehaviorQuirk( - quirk_id="moonshot.kimi-k2.5-k2.6.tool-history-reasoning-content", - selector=ModelBehaviorSelector( - providers=("moonshot",), - model_ids=("kimi-k2.5", "kimi-k2.6"), - model_families=("kimi-k2.5", "kimi-k2.6"), - api_dialects=(pcs.DIALECT_OPENAI_CHAT,), - ), - required_history_paths=("messages[assistant+tool_calls].reasoning_content",), - response_reasoning_paths=( - "choices[].message.reasoning_content", - "choices[].delta.reasoning_content", - ), - evidence_refs=( - "github:odysseus-dev/odysseus#3118", - "commit:2e6fff22", - ), - ), - ModelBehaviorQuirk( - quirk_id="anthropic.claude-opus-4.7-plus.omit-sampling-controls", - selector=ModelBehaviorSelector( - providers=("anthropic",), - model_families=("claude-opus",), - minimum_model_version=(4, 7), - api_dialects=(pcs.DIALECT_ANTHROPIC_MESSAGES,), - ), - request_omit_paths=("temperature", "top_p", "top_k"), - evidence_refs=( - "github:odysseus-dev/odysseus#3117", - "commit:4f48cfa9", - ), - ), - ModelBehaviorQuirk( - quirk_id="mistral.reasoning.structured-content", - selector=ModelBehaviorSelector( - providers=("mistral",), - model_families=("magistral", "mistral-small", "mistral-medium"), - api_dialects=(pcs.DIALECT_OPENAI_CHAT,), - required_capabilities=(mc.CAP_REASONING,), - ), - response_reasoning_paths=( - "choices[].message.content[type=thinking].thinking[].text", - "choices[].delta.content[type=thinking].thinking[].text", - ), - reasoning_controls=( - mc.ReasoningControl.build( - mechanism=mc.REASONING_CONTROL_EFFORT, - values=(mc.REASONING_CONTROL_VALUE_ON, mc.REASONING_CONTROL_VALUE_OFF), - native_values=("high", "medium", "low", "none"), - request_path="reasoning_effort", - response_paths=("choices[].message.content[type=thinking]",), - status=mc.ASSERTION_CLAIMED, - source=mc.SOURCE_PROVIDER_DOCS_REGISTRY, - confidence=mc.CONFIDENCE_REGISTRY, - ), - ), - evidence_refs=( - "github:odysseus-dev/odysseus#4698", - "commit:bd9149f7", - "https://docs.mistral.ai/capabilities/reasoning/", - ), - ), - ModelBehaviorQuirk( - quirk_id="ollama.native.reasoning-control", - selector=ModelBehaviorSelector( - providers=("ollama",), - model_families=("qwen3", "deepseek-v3.1", "deepseek-r1"), - api_dialects=(pcs.DIALECT_OLLAMA_NATIVE,), - required_capabilities=(mc.CAP_REASONING,), - ), - response_reasoning_paths=("message.thinking", "thinking"), - reasoning_controls=( - mc.ReasoningControl.build( - mechanism=mc.REASONING_CONTROL_NATIVE_BOOL, - values=(mc.REASONING_CONTROL_VALUE_ON, mc.REASONING_CONTROL_VALUE_OFF), - native_values=(True, False), - request_path="think", - response_paths=("message.thinking", "thinking"), - status=mc.ASSERTION_CLAIMED, - source=mc.SOURCE_PROVIDER_DOCS_REGISTRY, - confidence=mc.CONFIDENCE_REGISTRY, - ), - ), - evidence_refs=( - "https://docs.ollama.com/capabilities/thinking", - "github:odysseus-dev/odysseus#3031", - ), - ), - ModelBehaviorQuirk( - quirk_id="ollama.native.gpt-oss-reasoning-level", - selector=ModelBehaviorSelector( - providers=("ollama",), - model_families=("gpt-oss", "gptoss"), - api_dialects=(pcs.DIALECT_OLLAMA_NATIVE,), - required_capabilities=(mc.CAP_REASONING,), - ), - response_reasoning_paths=("message.thinking", "thinking"), - reasoning_controls=( - mc.ReasoningControl.build( - mechanism=mc.REASONING_CONTROL_EFFORT, - values=(mc.REASONING_CONTROL_VALUE_ON,), - native_values=("low", "medium", "high"), - request_path="think", - response_paths=("message.thinking", "thinking"), - status=mc.ASSERTION_CLAIMED, - source=mc.SOURCE_PROVIDER_DOCS_REGISTRY, - confidence=mc.CONFIDENCE_REGISTRY, - ), - ), - evidence_refs=("https://docs.ollama.com/capabilities/thinking",), - ), - ModelBehaviorQuirk( - quirk_id="ollama.openai-compat.0.20.6-reasoning-disable", - selector=ModelBehaviorSelector( - providers=("ollama",), - model_families=("qwen3.5",), - minimum_provider_version=(0, 20, 6), - api_dialects=(pcs.DIALECT_OPENAI_CHAT,), - required_capabilities=(mc.CAP_REASONING,), - ), - request_fixed_values=(("reasoning_effort", "none"),), - status=mc.ASSERTION_CLAIMED, - source=mc.SOURCE_HEURISTIC, - confidence=mc.CONFIDENCE_HEURISTIC, - evidence_refs=("github:odysseus-dev/odysseus#5503",), - ), -) - - -def matching_quirks(**identity: Any) -> tuple[ModelBehaviorQuirk, ...]: - return tuple( - quirk - for quirk in MODEL_BEHAVIOR_QUIRKS - if quirk.selector.matches(**identity) - ) - - -__all__ = [ - "MODEL_BEHAVIOR_QUIRKS", - "ModelBehaviorQuirk", - "ModelBehaviorSelector", - "matching_quirks", -] diff --git a/src/model_capabilities.py b/src/model_capabilities.py index 2929d68ad..d5b3d8507 100644 --- a/src/model_capabilities.py +++ b/src/model_capabilities.py @@ -657,106 +657,6 @@ class DeterministicControl: } -@dataclass(frozen=True) -class ReasoningControl: - """A provider/model-supported request mechanism for reasoning. - - This is intentionally separate from :class:`DeterministicControl` and - from the user's on/off/auto preference. The mechanism records the native - request shape that a later resolver may choose after provider and model - evidence have been reconciled. - """ - - mechanism: str = "" - values: tuple[str, ...] = () - native_values: tuple[Any, ...] = () - request_path: str = "" - response_paths: tuple[str, ...] = () - status: str = ASSERTION_UNKNOWN - source: str = SOURCE_UNKNOWN - confidence: str = CONFIDENCE_UNKNOWN - evidence: tuple[tuple[str, Any], ...] = () - tested_at: str = "" - - @classmethod - def build( - cls, - *, - mechanism: Any, - values: Any = None, - native_values: Any = None, - request_path: Any = "", - response_paths: Any = None, - status: Any = ASSERTION_UNKNOWN, - source: Any = SOURCE_UNKNOWN, - confidence: Any = CONFIDENCE_UNKNOWN, - evidence: Mapping[str, Any] | None = None, - tested_at: Any = "", - ) -> "ReasoningControl": - normalized_mechanism = normalize_reasoning_control_mechanism(mechanism) - normalized_status = normalize_assertion_status(status) - if not normalized_mechanism: - normalized_status = ASSERTION_UNKNOWN - if isinstance(response_paths, str): - response_paths = (response_paths,) - elif isinstance(response_paths, Mapping) or not isinstance(response_paths, Iterable): - response_paths = () - if native_values is None: - native_values = () - elif ( - isinstance(native_values, (str, Mapping)) - or not isinstance(native_values, Iterable) - ): - native_values = (native_values,) - return cls( - mechanism=normalized_mechanism, - values=_normalize_tokens(values, normalize_reasoning_control_value), - native_values=tuple(native_values), - request_path=str(request_path or "").strip(), - response_paths=tuple( - str(path or "").strip() - for path in response_paths - if str(path or "").strip() - ), - status=normalized_status, - source=normalize_source(source), - confidence=normalize_confidence(confidence), - evidence=_normalize_limits(evidence), - tested_at=str(tested_at or "").strip(), - ) - - @classmethod - def from_dict(cls, value: Mapping[str, Any]) -> "ReasoningControl": - if not isinstance(value, Mapping): - return cls.build(mechanism="") - return cls.build( - mechanism=value.get("mechanism"), - values=value.get("values"), - native_values=value.get("native_values"), - request_path=value.get("request_path"), - response_paths=value.get("response_paths"), - status=value.get("status"), - source=value.get("source"), - confidence=value.get("confidence"), - evidence=value.get("evidence"), - tested_at=value.get("tested_at"), - ) - - def to_dict(self) -> dict[str, Any]: - return { - "mechanism": self.mechanism, - "values": list(self.values), - "native_values": list(self.native_values), - "request_path": self.request_path, - "response_paths": list(self.response_paths), - "status": self.status, - "source": self.source, - "confidence": self.confidence, - "evidence": dict(self.evidence), - "tested_at": self.tested_at, - } - - @dataclass(frozen=True) class CapabilityProbeResult: provider: str diff --git a/src/model_capability_readers/__init__.py b/src/model_capability_readers/__init__.py index a9425c524..56ff56e13 100644 --- a/src/model_capability_readers/__init__.py +++ b/src/model_capability_readers/__init__.py @@ -23,6 +23,7 @@ from src.model_capability_readers import ( sglang, ) from src.model_capability_readers.base import ( + CANONICAL_MODEL_SHAPE_VERSION, ModelCapabilityRecord, VENDOR_ANTHROPIC, VENDOR_CEREBRAS, @@ -104,7 +105,7 @@ def records_from_payload( vendor_id = detect_vendor(base_url, endpoint_kind) reader = reader_for_vendor(vendor_id) if reader is generic_openai: - record_vendor = vendor_id if vendor_id not in {VENDOR_UNKNOWN, ""} else VENDOR_GENERIC_OPENAI + record_vendor = vendor_id if vendor_id else VENDOR_UNKNOWN records = reader.records_from_payload( payload, vendor_id=record_vendor, @@ -113,13 +114,12 @@ def records_from_payload( ) else: records = reader.records_from_payload(payload, endpoint_id=endpoint_id, base_url=base_url) - shape_id = resolution.catalog_shape.shape_id if resolution.catalog_shape else "" return tuple( replace( record, - provider_schema_id=resolution.schema.provider_id, - catalog_shape_id=shape_id, - provider_resolution=resolution.stage, + provider_source=resolution.provider_source, + catalog_shape_id=resolution.shape_id, + fallback=resolution.fallback, ) for record in records ) @@ -127,6 +127,7 @@ def records_from_payload( __all__ = [ "ModelCapabilityRecord", + "CANONICAL_MODEL_SHAPE_VERSION", "PLACEHOLDER_VENDOR_IDS", "READER_MODULES", "VENDOR_ANTHROPIC", diff --git a/src/model_capability_readers/base.py b/src/model_capability_readers/base.py index 1aa2e6adc..c2651a823 100644 --- a/src/model_capability_readers/base.py +++ b/src/model_capability_readers/base.py @@ -44,6 +44,8 @@ VENDOR_XAI = "xai" VENDOR_ZAI = "zai" VENDOR_UNKNOWN = "unknown" +CANONICAL_MODEL_SHAPE_VERSION = 1 + @dataclass(frozen=True) class ModelCapabilityRecord: @@ -54,14 +56,9 @@ class ModelCapabilityRecord: stable_model_id: str = "" capability_assertions: tuple[mc.CapabilityAssertion, ...] = () deterministic_controls: tuple[mc.DeterministicControl, ...] = () - reasoning_controls: tuple[mc.ReasoningControl, ...] = () - model_family: str = "" - model_version: tuple[int, ...] = () - provider_version: tuple[int, ...] = () - api_dialect: str = "" - provider_schema_id: str = "" + provider_source: str = "unknown" catalog_shape_id: str = "" - provider_resolution: str = "" + fallback: bool = False raw: Mapping[str, Any] = field(default_factory=dict) def __post_init__(self) -> None: @@ -80,22 +77,31 @@ class ModelCapabilityRecord: ) def to_dict(self, *, include_raw: bool = False) -> dict[str, Any]: + controls = tuple( + dict.fromkeys( + control.control + for control in self.deterministic_controls + if control.control + ) + ) data = { - "vendor": self.vendor, - "model_id": self.model_id, - "stable_model_id": self.stable_model_id, - "display_name": self.display_name, - "capability": self.capability.to_dict(), - "capability_assertions": [assertion.to_dict() for assertion in self.capability_assertions], - "deterministic_controls": [control.to_dict() for control in self.deterministic_controls], - "reasoning_controls": [control.to_dict() for control in self.reasoning_controls], - "model_family": self.model_family, - "model_version": list(self.model_version), - "provider_version": list(self.provider_version), - "api_dialect": self.api_dialect, - "provider_schema_id": self.provider_schema_id, - "catalog_shape_id": self.catalog_shape_id, - "provider_resolution": self.provider_resolution, + "schema_version": CANONICAL_MODEL_SHAPE_VERSION, + "provider": self.vendor, + "model": self.model_id, + "stable_id": self.stable_model_id, + "family": self.capability.family, + "task": self.capability.primary_task, + "modalities": self.capability.modalities.to_dict(), + "features": list(self.capability.capabilities), + "limits": dict(self.capability.limits), + "controls": list(controls), + "evidence": { + "source": self.capability.source, + "confidence": self.capability.confidence, + "provider_source": self.provider_source, + "shape": self.catalog_shape_id, + "fallback": self.fallback, + }, } if include_raw: data["raw"] = dict(self.raw) @@ -314,5 +320,4 @@ def detect_vendor(base_url: Any = "", endpoint_kind: Any = "") -> str: ) if resolution.provider_id != pcs.PROVIDER_UNKNOWN: return resolution.provider_id - parsed = urlparse(compact_str(base_url)) - return VENDOR_GENERIC_OPENAI if parsed.hostname else VENDOR_UNKNOWN + return VENDOR_UNKNOWN diff --git a/src/model_capability_readers/chatgpt_subscription.py b/src/model_capability_readers/chatgpt_subscription.py index 81400f1b9..a1ddc4399 100644 --- a/src/model_capability_readers/chatgpt_subscription.py +++ b/src/model_capability_readers/chatgpt_subscription.py @@ -42,7 +42,6 @@ def record_from_model( source=mc.SOURCE_PROVIDER_READER, confidence=mc.CONFIDENCE_UNKNOWN, ), - model_family=compact_str(raw.get("family")), raw=raw, ) diff --git a/src/model_capability_readers/copilot.py b/src/model_capability_readers/copilot.py index 99920d0a0..ffc0c670e 100644 --- a/src/model_capability_readers/copilot.py +++ b/src/model_capability_readers/copilot.py @@ -94,7 +94,6 @@ def record_from_model( ), display_name=compact_str(raw.get("name")) or model_id, capability=capability, - model_family=compact_str(raw.get("family")), raw=raw, ) diff --git a/src/model_capability_readers/generic_openai.py b/src/model_capability_readers/generic_openai.py index 5759405b2..b101251ba 100644 --- a/src/model_capability_readers/generic_openai.py +++ b/src/model_capability_readers/generic_openai.py @@ -1,9 +1,8 @@ -"""General structural reader for OpenAI-compatible model-list payloads. +"""Inventory-only reader for unrecognized model-list envelopes. -Identity-only model cards remain unknown. Rich records are promoted only from -recognized explicit fields (modalities, task/type, capability booleans, -supported parameters, and numeric limits). Names and descriptions are never -parsed for capability hints. +Common field names are not a cross-provider capability contract. This reader +therefore recovers model identity and preserves the original record, but never +promotes tasks, modalities, parameters, limits, or capability booleans. """ from __future__ import annotations @@ -15,18 +14,9 @@ from src import model_capabilities as mc from src.model_capability_readers.base import ( ModelCapabilityRecord, VENDOR_GENERIC_OPENAI, - as_list, - as_mapping, - build_capability, compact_str, - deterministic_controls_from_supported_parameters, - family_from_modalities, - int_limit, - merge_unique, model_id_from, - modalities_from_value, openai_model_items, - split_modality_arrow, stable_model_id_for, ) @@ -34,191 +24,6 @@ from src.model_capability_readers.base import ( vendor = VENDOR_GENERIC_OPENAI -_TYPE_FAMILIES = { - "llm": mc.FAMILY_CHAT, - "chat": mc.FAMILY_CHAT, - "chat_completion": mc.FAMILY_CHAT, - "text_generation": mc.FAMILY_CHAT, - "causal_lm": mc.FAMILY_CHAT, - "image_text_to_text": mc.FAMILY_CHAT, - "image_question_answering": mc.FAMILY_CHAT, - "embedding": mc.FAMILY_EMBEDDING, - "embeddings": mc.FAMILY_EMBEDDING, - "text_embedding": mc.FAMILY_EMBEDDING, - "feature_extraction": mc.FAMILY_EMBEDDING, - "text_to_image": mc.FAMILY_IMAGE, - "image_to_image": mc.FAMILY_IMAGE, - "text_to_video": mc.FAMILY_VIDEO, - "automatic_speech_recognition": mc.FAMILY_AUDIO, - "text_to_speech": mc.FAMILY_AUDIO, - "rerank": mc.FAMILY_RERANK, - "reranking": mc.FAMILY_RERANK, - "classification": mc.FAMILY_CLASSIFICATION, - "text_classification": mc.FAMILY_CLASSIFICATION, - "moderation": mc.FAMILY_MODERATION, -} - -_PARAMETER_CAPABILITIES = { - "tools": mc.CAP_TOOL_CALL, - "tool_choice": mc.CAP_TOOL_CALL, - "parallel_tool_calls": mc.CAP_TOOL_CALL, - "function_calling": mc.CAP_TOOL_CALL, - "response_format": mc.CAP_JSON_MODE, - "structured_output": mc.CAP_STRUCTURED_OUTPUT, - "structured_outputs": mc.CAP_STRUCTURED_OUTPUT, - "json_schema": mc.CAP_STRUCTURED_OUTPUT, - "reasoning": mc.CAP_REASONING, - "reasoning_effort": mc.CAP_REASONING, - "include_reasoning": mc.CAP_REASONING, - "web_search": mc.CAP_WEB_SEARCH, - "web_search_options": mc.CAP_WEB_SEARCH, -} - - -def _shape_token(value: Any) -> str: - return compact_str(value).lower().replace("-", "_").replace(" ", "_") - - -def _family_from_explicit_fields(raw: Mapping[str, Any]) -> str: - for key in ("type", "model_type", "task", "pipeline_tag"): - family = _TYPE_FAMILIES.get(_shape_token(raw.get(key))) - if family: - return family - return mc.FAMILY_UNKNOWN - - -def _modalities(raw: Mapping[str, Any]) -> tuple[tuple[str, ...], tuple[str, ...]]: - architecture = as_mapping(raw.get("architecture")) - input_modalities = modalities_from_value( - raw.get("input_modalities") or architecture.get("input_modalities") - ) - output_modalities = modalities_from_value( - raw.get("output_modalities") or architecture.get("output_modalities") - ) - if not input_modalities or not output_modalities: - arrow_input, arrow_output = split_modality_arrow( - raw.get("modality") or architecture.get("modality") - ) - input_modalities = input_modalities or arrow_input - output_modalities = output_modalities or arrow_output - return input_modalities, output_modalities - - -def _capabilities_from_modalities( - input_modalities: tuple[str, ...], - output_modalities: tuple[str, ...], -) -> tuple[str, ...]: - input_set = set(input_modalities) - output_set = set(output_modalities) - out: list[str] = [] - if mc.MODALITY_IMAGE in input_set and mc.MODALITY_TEXT in output_set: - out.append(mc.CAP_VISION) - if mc.MODALITY_FILE in input_set: - out.append(mc.CAP_FILES) - if mc.MODALITY_PDF in input_set: - out.append(mc.CAP_PDF) - if mc.MODALITY_AUDIO in input_set: - out.append(mc.CAP_AUDIO_INPUT) - if mc.MODALITY_AUDIO in output_set: - out.append(mc.CAP_AUDIO_OUTPUT) - if mc.MODALITY_IMAGE in output_set: - out.append(mc.CAP_IMAGE_GENERATION) - if mc.MODALITY_IMAGE in input_set: - out.append(mc.CAP_IMAGE_EDITING) - if mc.MODALITY_VIDEO in output_set: - out.append(mc.CAP_VIDEO_GENERATION) - return tuple(out) - - -def _explicit_capabilities(raw: Mapping[str, Any]) -> tuple[str, ...]: - values: list[Any] = [] - payload = raw.get("capabilities") - if isinstance(payload, Mapping): - supports = payload.get("supports") - if isinstance(supports, Mapping): - values.extend(key for key, enabled in supports.items() if enabled is True) - values.extend(key for key, enabled in payload.items() if enabled is True) - elif isinstance(payload, (list, tuple)): - values.extend(payload) - - out: list[str] = [] - for value in values: - cap = mc.normalize_capability(value) - if cap and cap not in out: - out.append(cap) - for value in as_list(raw.get("supported_parameters")): - cap = _PARAMETER_CAPABILITIES.get(_shape_token(value)) - if cap and cap not in out: - out.append(cap) - task = next( - (_shape_token(raw.get(key)) for key in ("type", "model_type", "task", "pipeline_tag") if raw.get(key)), - "", - ) - task_capability = { - "automatic_speech_recognition": mc.CAP_TRANSCRIPTION, - "text_to_speech": mc.CAP_TTS, - "text_to_image": mc.CAP_IMAGE_GENERATION, - "image_to_image": mc.CAP_IMAGE_EDITING, - "text_to_video": mc.CAP_VIDEO_GENERATION, - "image_text_to_text": mc.CAP_VISION, - "image_question_answering": mc.CAP_VISION, - }.get(task) - if task_capability and task_capability not in out: - out.append(task_capability) - return tuple(out) - - -def _limits(raw: Mapping[str, Any]) -> dict[str, int]: - architecture = as_mapping(raw.get("architecture")) - top_provider = as_mapping(raw.get("top_provider")) - limits: dict[str, int] = {} - for keys, target in ( - (("context_length", "max_context_length", "max_model_len"), "context_tokens"), - (("input_token_limit", "inputTokenLimit"), "input_tokens"), - (("output_token_limit", "outputTokenLimit", "max_completion_tokens"), "output_tokens"), - ): - for key in keys: - value = int_limit(raw.get(key)) or int_limit(architecture.get(key)) or int_limit(top_provider.get(key)) - if value: - limits[target] = value - break - return limits - - -def _default_modalities( - family: str, - raw: Mapping[str, Any], -) -> tuple[tuple[str, ...], tuple[str, ...]]: - task = next( - (_shape_token(raw.get(key)) for key in ("type", "model_type", "task", "pipeline_tag") if raw.get(key)), - "", - ) - task_modalities = { - "automatic_speech_recognition": ((mc.MODALITY_AUDIO,), (mc.MODALITY_TEXT,)), - "text_to_speech": ((mc.MODALITY_TEXT,), (mc.MODALITY_AUDIO,)), - "text_to_image": ((mc.MODALITY_TEXT,), (mc.MODALITY_IMAGE,)), - "image_to_image": ((mc.MODALITY_IMAGE,), (mc.MODALITY_IMAGE,)), - "text_to_video": ((mc.MODALITY_TEXT,), (mc.MODALITY_VIDEO,)), - "image_text_to_text": ((mc.MODALITY_TEXT, mc.MODALITY_IMAGE), (mc.MODALITY_TEXT,)), - "image_question_answering": ((mc.MODALITY_TEXT, mc.MODALITY_IMAGE), (mc.MODALITY_TEXT,)), - }.get(task) - if task_modalities: - return task_modalities - if family == mc.FAMILY_CHAT: - return (mc.MODALITY_TEXT,), (mc.MODALITY_TEXT,) - if family == mc.FAMILY_EMBEDDING: - return (mc.MODALITY_TEXT,), (mc.MODALITY_EMBEDDING,) - if family == mc.FAMILY_IMAGE: - return (mc.MODALITY_TEXT,), (mc.MODALITY_IMAGE,) - if family == mc.FAMILY_VIDEO: - return (mc.MODALITY_TEXT,), (mc.MODALITY_VIDEO,) - if family == mc.FAMILY_AUDIO: - return (), () - if family in {mc.FAMILY_RERANK, mc.FAMILY_CLASSIFICATION, mc.FAMILY_MODERATION}: - return (mc.MODALITY_TEXT,), (mc.MODALITY_TEXT,) - return (), () - - def record_from_model( raw: Mapping[str, Any], *, @@ -226,44 +31,28 @@ def record_from_model( endpoint_id: Any = "", base_url: Any = "", ) -> ModelCapabilityRecord | None: - model_id = model_id_from(raw, "id", "name", "model") + model_id = model_id_from(raw, "id", "name", "model", "key", "slug") if not model_id: return None - - family = _family_from_explicit_fields(raw) - input_modalities, output_modalities = _modalities(raw) - if family == mc.FAMILY_UNKNOWN: - family = family_from_modalities(input_modalities, output_modalities) - if family != mc.FAMILY_UNKNOWN and not input_modalities and not output_modalities: - input_modalities, output_modalities = _default_modalities(family, raw) - capabilities = merge_unique( - _explicit_capabilities(raw), - _capabilities_from_modalities(input_modalities, output_modalities), - ) - limits = _limits(raw) - if family == mc.FAMILY_UNKNOWN and not capabilities and not limits: - capability = mc.unknown_capability( - source=mc.SOURCE_PROVIDER_READER, - confidence=mc.CONFIDENCE_UNKNOWN, - ) - else: - capability = build_capability( - family=family, - input_modalities=input_modalities, - output_modalities=output_modalities, - capabilities=capabilities, - limits=limits, - ) return ModelCapabilityRecord( vendor=vendor_id, model_id=model_id, - stable_model_id=stable_model_id_for(vendor_id, model_id, endpoint_id=endpoint_id, base_url=base_url), - display_name=compact_str(raw.get("display_name") or raw.get("name")), - capability=capability, - deterministic_controls=deterministic_controls_from_supported_parameters( - raw.get("supported_parameters") + stable_model_id=stable_model_id_for( + vendor_id, + model_id, + endpoint_id=endpoint_id, + base_url=base_url, + ), + display_name=compact_str( + raw.get("display_name") + or raw.get("name") + or raw.get("key") + or raw.get("slug") + ), + capability=mc.unknown_capability( + source=mc.SOURCE_PROVIDER_READER, + confidence=mc.CONFIDENCE_UNKNOWN, ), - model_family=compact_str(raw.get("root") or raw.get("model_family")), raw=raw, ) @@ -277,7 +66,12 @@ def records_from_payload( ) -> tuple[ModelCapabilityRecord, ...]: records: list[ModelCapabilityRecord] = [] for item in openai_model_items(payload): - record = record_from_model(item, vendor_id=vendor_id, endpoint_id=endpoint_id, base_url=base_url) + record = record_from_model( + item, + vendor_id=vendor_id, + endpoint_id=endpoint_id, + base_url=base_url, + ) if record: records.append(record) return tuple(records) diff --git a/src/model_capability_readers/google.py b/src/model_capability_readers/google.py index f6d0bfe28..9edb57bdb 100644 --- a/src/model_capability_readers/google.py +++ b/src/model_capability_readers/google.py @@ -42,7 +42,6 @@ def record_from_model( display_name=compact_str(raw.get("displayName")) or model_id, capability=ai_studio.capability_from_model(raw), deterministic_controls=ai_studio.deterministic_controls_from_model(raw), - model_family=compact_str(raw.get("baseModelId")), raw=raw, ) diff --git a/src/model_capability_readers/huggingface.py b/src/model_capability_readers/huggingface.py index edb34d471..c260513ea 100644 --- a/src/model_capability_readers/huggingface.py +++ b/src/model_capability_readers/huggingface.py @@ -6,11 +6,10 @@ from collections.abc import Mapping from typing import Any from src import model_capabilities as mc -from src.model_capability_readers import generic_openai from src.model_capability_readers.base import ( ModelCapabilityRecord, VENDOR_HUGGINGFACE, - as_mapping, + build_capability, compact_str, openai_model_items, stable_model_id_for, @@ -20,6 +19,86 @@ from src.model_capability_readers.base import ( vendor = VENDOR_HUGGINGFACE +# Hugging Face publishes ``pipeline_tag`` as a provider-owned task enum. Keep +# its interpretation here, rather than teaching the inventory fallback that a +# similarly named field has the same meaning for every provider. +_PIPELINE_SHAPES = { + "text-generation": (mc.FAMILY_CHAT, (mc.MODALITY_TEXT,), (mc.MODALITY_TEXT,), ()), + "image-text-to-text": ( + mc.FAMILY_CHAT, + (mc.MODALITY_TEXT, mc.MODALITY_IMAGE), + (mc.MODALITY_TEXT,), + (mc.CAP_VISION,), + ), + "image-question-answering": ( + mc.FAMILY_CHAT, + (mc.MODALITY_TEXT, mc.MODALITY_IMAGE), + (mc.MODALITY_TEXT,), + (mc.CAP_VISION,), + ), + "feature-extraction": ( + mc.FAMILY_EMBEDDING, + (mc.MODALITY_TEXT,), + (mc.MODALITY_EMBEDDING,), + (), + ), + "text-to-image": ( + mc.FAMILY_IMAGE, + (mc.MODALITY_TEXT,), + (mc.MODALITY_IMAGE,), + (mc.CAP_IMAGE_GENERATION,), + ), + "image-to-image": ( + mc.FAMILY_IMAGE, + (mc.MODALITY_IMAGE,), + (mc.MODALITY_IMAGE,), + (mc.CAP_IMAGE_GENERATION, mc.CAP_IMAGE_EDITING), + ), + "text-to-video": ( + mc.FAMILY_VIDEO, + (mc.MODALITY_TEXT,), + (mc.MODALITY_VIDEO,), + (mc.CAP_VIDEO_GENERATION,), + ), + "automatic-speech-recognition": ( + mc.FAMILY_AUDIO, + (mc.MODALITY_AUDIO,), + (mc.MODALITY_TEXT,), + (mc.CAP_TRANSCRIPTION,), + ), + "text-to-speech": ( + mc.FAMILY_AUDIO, + (mc.MODALITY_TEXT,), + (mc.MODALITY_AUDIO,), + (mc.CAP_TTS,), + ), + "text-classification": ( + mc.FAMILY_CLASSIFICATION, + (mc.MODALITY_TEXT,), + (mc.MODALITY_TEXT,), + (), + ), +} + + +def _capability_from_pipeline_tag(value: Any) -> mc.ModelCapability: + shape = _PIPELINE_SHAPES.get(compact_str(value).lower()) + if not shape: + return mc.unknown_capability( + source=mc.SOURCE_COOKBOOK_HF, + confidence=mc.CONFIDENCE_UNKNOWN, + ) + family, input_modalities, output_modalities, capabilities = shape + return build_capability( + family=family, + input_modalities=input_modalities, + output_modalities=output_modalities, + capabilities=capabilities, + source=mc.SOURCE_COOKBOOK_HF, + confidence=mc.CONFIDENCE_REGISTRY, + ) + + def record_from_model( raw: Mapping[str, Any], *, @@ -29,25 +108,6 @@ def record_from_model( model_id = compact_str(raw.get("modelId") or raw.get("id")) if not model_id: return None - structural = generic_openai.record_from_model( - {**raw, "id": model_id}, - vendor_id=VENDOR_HUGGINGFACE, - endpoint_id=endpoint_id, - base_url=base_url, - ) - if not structural: - return None - capability = mc.ModelCapability.build( - family=structural.capability.family, - primary_task=structural.capability.primary_task, - input_modalities=structural.capability.modalities.input, - output_modalities=structural.capability.modalities.output, - capabilities=structural.capability.capabilities, - limits=dict(structural.capability.limits), - source=mc.SOURCE_COOKBOOK_HF, - confidence=mc.CONFIDENCE_REGISTRY, - ) - config = as_mapping(raw.get("config")) return ModelCapabilityRecord( vendor=VENDOR_HUGGINGFACE, model_id=model_id, @@ -65,9 +125,7 @@ def record_from_model( ) or model_id ), - capability=capability, - deterministic_controls=structural.deterministic_controls, - model_family=compact_str(config.get("model_type")), + capability=_capability_from_pipeline_tag(raw.get("pipeline_tag")), raw=raw, ) diff --git a/src/model_capability_readers/lmstudio.py b/src/model_capability_readers/lmstudio.py index 919055936..960649959 100644 --- a/src/model_capability_readers/lmstudio.py +++ b/src/model_capability_readers/lmstudio.py @@ -160,7 +160,6 @@ def record_from_native_model( ), display_name=compact_str(raw.get("display_name") or raw.get("name")) or model_id, capability=capability, - model_family=compact_str(raw.get("architecture") or raw.get("arch")), raw=raw, ) diff --git a/src/model_capability_readers/mistral.py b/src/model_capability_readers/mistral.py index 6a316169a..7788984d6 100644 --- a/src/model_capability_readers/mistral.py +++ b/src/model_capability_readers/mistral.py @@ -93,7 +93,6 @@ def record_from_model( ), display_name=compact_str(raw.get("name")) or model_id, capability=capability, - model_family=compact_str(raw.get("root")), raw=raw, ) diff --git a/src/model_capability_readers/ollama.py b/src/model_capability_readers/ollama.py index cd21f6c0e..e9a1fdfeb 100644 --- a/src/model_capability_readers/ollama.py +++ b/src/model_capability_readers/ollama.py @@ -142,7 +142,6 @@ def record_from_show_payload( stable_model_id=stable_model_id_for(VENDOR_OLLAMA, model_id, endpoint_id=endpoint_id, base_url=base_url), display_name=model_id, capability=capability, - model_family=compact_str(as_mapping(payload.get("details")).get("family")), raw=payload, ) @@ -175,7 +174,6 @@ def records_from_tags_payload( source=mc.SOURCE_PROVIDER_READER, confidence=mc.CONFIDENCE_UNKNOWN, ), - model_family=compact_str(as_mapping(item.get("details")).get("family")), raw=item, ) ) diff --git a/src/model_capability_readers/sglang.py b/src/model_capability_readers/sglang.py index 5b6b96c63..3ac59b060 100644 --- a/src/model_capability_readers/sglang.py +++ b/src/model_capability_readers/sglang.py @@ -72,7 +72,6 @@ def record_from_model_info( display_name=model_id, capability=capability, deterministic_controls=deterministic_controls_from_supported_parameters(sampling.keys()), - model_family=compact_str(payload.get("model_type")), raw=payload, ) diff --git a/src/provider_capability_schemas.py b/src/provider_capability_schemas.py index 531cbd19b..55f4809da 100644 --- a/src/provider_capability_schemas.py +++ b/src/provider_capability_schemas.py @@ -1,20 +1,12 @@ -"""Canonical serving-provider and payload-shape metadata. +"""Provider identity and native model-catalog shape detection. -This module describes provider/API dialects and model-catalog JSON shapes. It -does not perform network I/O, parse model names, or claim that every model on a -provider supports every feature exposed by that provider. Model capability is -still read from each model record (or remains unknown). +The registry has one narrow job: identify a configured provider and recognize +tested provider-native catalog envelopes. Generic ``data``/``models``/list +envelopes are marked as fallback inventory only; they never promote model +capabilities. -Resolution is deliberately stepped and deterministic: - -1. an explicit provider/endpoint kind; -2. an exact known provider host; -3. a discriminating native payload shape; -4. a general structural model-list shape; -5. unknown. - -Unknown keys are left to the reader's ``raw`` evidence. They never become a -capability merely because a future provider happens to add them. +Request/response transport fields and model-specific behavior belong to their +runtime adapters, not this catalog detector. """ from __future__ import annotations @@ -28,24 +20,11 @@ from urllib.parse import urlparse PROVIDER_UNKNOWN = "unknown" PROVIDER_GENERIC_OPENAI = "generic_openai" -DIALECT_OPENAI_CHAT = "openai_chat_completions" -DIALECT_OPENAI_RESPONSES = "openai_responses" -DIALECT_ANTHROPIC_MESSAGES = "anthropic_messages" -DIALECT_COHERE_V2 = "cohere_v2" -DIALECT_GOOGLE_GENERATE_CONTENT = "google_generate_content" -DIALECT_OLLAMA_NATIVE = "ollama_native" -DIALECT_LMSTUDIO_NATIVE_V1 = "lmstudio_native_v1" -DIALECT_LLAMACPP_NATIVE = "llamacpp_native" -DIALECT_SGLANG_NATIVE = "sglang_native" -DIALECT_HUGGINGFACE_HUB = "huggingface_hub" -DIALECT_CHATGPT_SUBSCRIPTION = "chatgpt_subscription_responses" - -RESOLUTION_EXPLICIT = "explicit" -RESOLUTION_ENDPOINT_KIND = "endpoint_kind" -RESOLUTION_HOST = "host" -RESOLUTION_NATIVE_SHAPE = "native_shape" -RESOLUTION_GENERAL_SHAPE = "general_shape" -RESOLUTION_UNKNOWN = "unknown" +PROVIDER_SOURCE_EXPLICIT = "explicit" +PROVIDER_SOURCE_ENDPOINT_KIND = "endpoint_kind" +PROVIDER_SOURCE_HOST = "host" +PROVIDER_SOURCE_PAYLOAD = "payload" +PROVIDER_SOURCE_UNKNOWN = "unknown" ENVELOPE_DATA = "data" ENVELOPE_MODELS = "models" @@ -87,11 +66,10 @@ def _items_for_envelope(payload: Any, envelope: str) -> tuple[Mapping[str, Any], @dataclass(frozen=True) class ProviderCatalogShape: - """A declarative, versioned provider model-catalog shape.""" + """A tested provider-native shape or an explicit inventory fallback.""" shape_id: str provider_id: str - endpoint_path: str envelope: str identity_paths: tuple[str, ...] required_root_paths: tuple[str, ...] = () @@ -99,10 +77,8 @@ class ProviderCatalogShape: required_item_any_paths: tuple[str, ...] = () item_types: tuple[tuple[str, tuple[Any, ...]], ...] = () item_values: tuple[tuple[str, tuple[Any, ...]], ...] = () - capability_paths: tuple[str, ...] = () - api_version: str = "" - priority: int = 0 - latest: bool = True + detection_priority: int = 0 + fallback: bool = False def items(self, payload: Any) -> tuple[Mapping[str, Any], ...]: return _items_for_envelope(payload, self.envelope) @@ -114,19 +90,14 @@ class ProviderCatalogShape: if not all(_path_present(payload, path) for path in self.required_root_paths): return False - items = self.items(payload) - if not items: - return False - for item in items: - if self.identity_paths: - has_identity = False - for path in self.identity_paths: - value = _path_value(item, path) - if value is not _MISSING and value is not None and value != "": - has_identity = True - break - if not has_identity: - continue + for item in self.items(payload): + if self.identity_paths and not any( + (value := _path_value(item, path)) is not _MISSING + and value is not None + and value != "" + for path in self.identity_paths + ): + continue if not all(_path_present(item, path) for path in self.required_item_paths): continue if self.required_item_any_paths and not any( @@ -144,329 +115,148 @@ class ProviderCatalogShape: return False -@dataclass(frozen=True) -class ProviderApiShape: - """Stable request/response field paths for one API dialect. - - Paths are documentation and validation inputs, not late response parsers. - A model-specific exception can narrow these fields in the model quirk - registry without changing the provider's general transport contract. - """ - - dialect: str - request_path: str - stream_path: str = "" - model_field: str = "model" - message_field: str = "messages" - tool_request_paths: tuple[str, ...] = () - tool_response_paths: tuple[str, ...] = () - text_response_paths: tuple[str, ...] = () - reasoning_response_paths: tuple[str, ...] = () - request_control_paths: tuple[str, ...] = () - - @dataclass(frozen=True) class ProviderCapabilitySchema: provider_id: str - display_name: str aliases: tuple[str, ...] = () host_suffixes: tuple[str, ...] = () - api_shapes: tuple[ProviderApiShape, ...] = () catalog_shapes: tuple[ProviderCatalogShape, ...] = () - fallback_provider_id: str = PROVIDER_GENERIC_OPENAI - model_capabilities_are_per_model: bool = True @dataclass(frozen=True) class ProviderResolution: - provider_id: str - stage: str - schema: ProviderCapabilitySchema - catalog_shape: ProviderCatalogShape | None = None + provider_id: str = PROVIDER_UNKNOWN + provider_source: str = PROVIDER_SOURCE_UNKNOWN + shape_id: str = "" + fallback: bool = False def to_dict(self) -> dict[str, Any]: return { - "provider_id": self.provider_id, - "stage": self.stage, - "schema_id": self.schema.provider_id, - "catalog_shape_id": self.catalog_shape.shape_id if self.catalog_shape else "", + "provider": self.provider_id, + "provider_source": self.provider_source, + "shape": self.shape_id, + "fallback": self.fallback, } -OPENAI_CHAT_SHAPE = ProviderApiShape( - dialect=DIALECT_OPENAI_CHAT, - request_path="/v1/chat/completions", - stream_path="/v1/chat/completions", - tool_request_paths=("tools[].function", "tool_choice", "parallel_tool_calls"), - tool_response_paths=("choices[].message.tool_calls[].function", "choices[].delta.tool_calls[].function"), - text_response_paths=("choices[].message.content", "choices[].delta.content"), - reasoning_response_paths=( - "choices[].message.reasoning_content", - "choices[].delta.reasoning_content", - "choices[].delta.reasoning", - "choices[].delta.thinking", - ), - request_control_paths=( - "temperature", - "top_p", - "seed", - "response_format", - "reasoning_effort", - ), -) - -OPENAI_RESPONSES_SHAPE = ProviderApiShape( - dialect=DIALECT_OPENAI_RESPONSES, - request_path="/v1/responses", - stream_path="/v1/responses", - message_field="input", - tool_request_paths=("tools[]", "tool_choice", "parallel_tool_calls"), - tool_response_paths=( - "output[type=function_call].arguments", - "response.function_call_arguments.delta", - ), - text_response_paths=( - "output[type=message].content[type=output_text].text", - "response.output_text.delta", - ), - reasoning_response_paths=( - "output[type=reasoning].summary[].text", - "output[type=reasoning].encrypted_content", - "response.reasoning_summary_text.delta", - ), - request_control_paths=("temperature", "top_p", "reasoning", "text.format"), -) - -ANTHROPIC_MESSAGES_SHAPE = ProviderApiShape( - dialect=DIALECT_ANTHROPIC_MESSAGES, - request_path="/v1/messages", - stream_path="/v1/messages", - tool_request_paths=("tools[].input_schema", "tool_choice", "messages[].content[].tool_result"), - tool_response_paths=("content[].tool_use", "content_block_delta.delta.partial_json"), - text_response_paths=("content[].text", "content_block_delta.delta.text"), - reasoning_response_paths=("content[].thinking", "content[].signature"), - request_control_paths=("temperature", "top_p", "top_k", "thinking", "output_config"), -) - -COHERE_V2_SHAPE = ProviderApiShape( - dialect=DIALECT_COHERE_V2, - request_path="/v2/chat", - stream_path="/v2/chat", - tool_request_paths=("tools[].parameters", "messages[].tool_calls", "messages[].tool_results"), - tool_response_paths=("message.tool_calls", "tool-call-start", "tool-call-delta", "tool-call-end"), - text_response_paths=("message.content[].text", "content-delta.delta.message.content.text"), - reasoning_response_paths=("message.content[].thinking",), - request_control_paths=( - "temperature", - "p", - "k", - "seed", - "response_format", - "thinking", - ), -) - -GOOGLE_CONTENT_SHAPE = ProviderApiShape( - dialect=DIALECT_GOOGLE_GENERATE_CONTENT, - request_path="/v1beta/models/{model}:generateContent", - stream_path="/v1beta/models/{model}:streamGenerateContent?alt=sse", - message_field="contents", - tool_request_paths=("tools[].functionDeclarations", "toolConfig"), - tool_response_paths=("candidates[].content.parts[].functionCall", "candidates[].content.parts[].functionResponse"), - text_response_paths=("candidates[].content.parts[].text",), - reasoning_response_paths=("candidates[].content.parts[].thought", "candidates[].content.parts[].thoughtSignature"), - request_control_paths=( - "generationConfig.temperature", - "generationConfig.topP", - "generationConfig.topK", - "generationConfig.thinkingConfig", - "generationConfig.responseJsonSchema", - ), -) - -OLLAMA_NATIVE_SHAPE = ProviderApiShape( - dialect=DIALECT_OLLAMA_NATIVE, - request_path="/api/chat", - stream_path="/api/chat", - tool_request_paths=("tools[].function", "messages[].tool_calls[].function"), - tool_response_paths=("message.tool_calls[].function",), - text_response_paths=("message.content",), - reasoning_response_paths=("message.thinking",), - request_control_paths=("think", "format", "options.temperature", "options.top_p", "options.seed"), -) - -LMSTUDIO_NATIVE_SHAPE = ProviderApiShape( - dialect=DIALECT_LMSTUDIO_NATIVE_V1, - request_path="/api/v1/chat", - stream_path="/api/v1/chat", - message_field="input", - tool_request_paths=("integrations[].mcp",), - text_response_paths=("output[].content",), - reasoning_response_paths=("output[].reasoning",), - request_control_paths=("temperature", "top_p", "reasoning", "response_format"), -) - -SGLANG_NATIVE_SHAPE = ProviderApiShape( - dialect=DIALECT_SGLANG_NATIVE, - request_path="/generate", - stream_path="/generate", - message_field="text", - text_response_paths=("text",), - request_control_paths=("sampling_params.temperature", "sampling_params.top_p", "sampling_params.seed"), -) - -CHATGPT_SUBSCRIPTION_SHAPE = ProviderApiShape( - dialect=DIALECT_CHATGPT_SUBSCRIPTION, - request_path="/backend-api/codex/responses", - stream_path="/backend-api/codex/responses", - message_field="input", - tool_request_paths=("tools[]", "input[].function_call_output"), - tool_response_paths=("response.function_call_arguments.delta", "response.output_item.done"), - text_response_paths=("response.output_text.delta",), - reasoning_response_paths=("response.reasoning_summary_text.delta", "reasoning.encrypted_content"), - request_control_paths=("reasoning", "text", "parallel_tool_calls"), -) - - +# Generic envelopes are inventory fallbacks only. Their field names are not a +# portable capability contract, so readers may recover identity but nothing +# else from them. GENERAL_DATA_SHAPE = ProviderCatalogShape( - shape_id="openai-compatible.models.data.v1", - provider_id=PROVIDER_GENERIC_OPENAI, - endpoint_path="/v1/models", + shape_id="fallback.models.data.v1", + provider_id=PROVIDER_UNKNOWN, envelope=ENVELOPE_DATA, - identity_paths=("id", "name", "model"), + identity_paths=("id", "name", "model", "key", "slug"), + fallback=True, ) GENERAL_MODELS_SHAPE = ProviderCatalogShape( - shape_id="general.models-envelope.v1", - provider_id=PROVIDER_GENERIC_OPENAI, - endpoint_path="/models", + shape_id="fallback.models.envelope.v1", + provider_id=PROVIDER_UNKNOWN, envelope=ENVELOPE_MODELS, - identity_paths=("id", "key", "slug", "name", "model"), + identity_paths=("id", "name", "model", "key", "slug"), + fallback=True, ) GENERAL_BARE_SHAPE = ProviderCatalogShape( - shape_id="openai-compatible.models.bare-list.v1", - provider_id=PROVIDER_GENERIC_OPENAI, - endpoint_path="/models", + shape_id="fallback.models.list.v1", + provider_id=PROVIDER_UNKNOWN, envelope=ENVELOPE_BARE_LIST, - identity_paths=("id", "name", "model"), + identity_paths=("id", "name", "model", "key", "slug"), + fallback=True, ) +FALLBACK_CATALOG_SHAPES = ( + GENERAL_DATA_SHAPE, + GENERAL_MODELS_SHAPE, + GENERAL_BARE_SHAPE, +) + OPENAI_MODELS_SHAPE = ProviderCatalogShape( shape_id="openai.models.identity.v1", provider_id="openai", - endpoint_path="/v1/models", envelope=ENVELOPE_DATA, identity_paths=("id",), required_item_paths=("object", "created", "owned_by"), item_values=(("object", ("model",)),), - api_version="v1", ) OPENROUTER_MODELS_SHAPE = ProviderCatalogShape( shape_id="openrouter.models.rich.v1", provider_id="openrouter", - endpoint_path="/api/v1/models", envelope=ENVELOPE_DATA, identity_paths=("id",), - required_item_any_paths=("architecture", "supported_parameters", "top_provider", "canonical_slug"), - capability_paths=( - "architecture.input_modalities", - "architecture.output_modalities", + required_item_any_paths=( + "architecture", "supported_parameters", - "context_length", - "top_provider.max_completion_tokens", + "top_provider", + "canonical_slug", ), - api_version="v1", - priority=90, + detection_priority=90, ) GOOGLE_MODELS_SHAPE = ProviderCatalogShape( shape_id="google.generative-language.models.v1beta", provider_id="google", - endpoint_path="/v1beta/models", envelope=ENVELOPE_MODELS, identity_paths=("baseModelId", "name"), required_item_paths=("supportedGenerationMethods",), item_types=(("supportedGenerationMethods", (list, tuple)),), - capability_paths=( - "supportedGenerationMethods", - "inputTokenLimit", - "outputTokenLimit", - "thinking", - "temperature", - "topP", - "topK", - ), - api_version="v1beta", - priority=100, + detection_priority=100, ) OLLAMA_TAGS_SHAPE = ProviderCatalogShape( shape_id="ollama.tags.v1", provider_id="ollama", - endpoint_path="/api/tags", envelope=ENVELOPE_MODELS, identity_paths=("model", "name"), required_item_any_paths=("digest", "details.family", "details.families"), - capability_paths=("details.family", "details.families"), - priority=90, + detection_priority=90, ) OLLAMA_SHOW_SHAPE = ProviderCatalogShape( shape_id="ollama.show.v1", provider_id="ollama", - endpoint_path="/api/show", envelope=ENVELOPE_SINGLE, identity_paths=(), required_item_paths=("capabilities",), required_item_any_paths=("model_info", "details", "template", "parameters"), item_types=(("capabilities", (list, tuple)),), - capability_paths=("capabilities", "model_info.*.context_length"), - priority=100, + detection_priority=100, ) LMSTUDIO_MODELS_V1_SHAPE = ProviderCatalogShape( shape_id="lmstudio.models.native.v1", provider_id="lmstudio", - endpoint_path="/api/v1/models", envelope=ENVELOPE_MODELS, identity_paths=("key",), required_item_paths=("type",), - required_item_any_paths=("capabilities", "loaded_instances", "max_context_length", "architecture", "quantization"), + required_item_any_paths=( + "capabilities", + "loaded_instances", + "max_context_length", + "architecture", + "quantization", + ), item_types=(("type", (str,)),), - capability_paths=("type", "capabilities", "max_context_length", "loaded_instances[].config.context_length"), - api_version="1", - priority=100, + detection_priority=100, ) LMSTUDIO_MODELS_V0_SHAPE = ProviderCatalogShape( shape_id="lmstudio.models.native.v0", provider_id="lmstudio", - endpoint_path="/api/v0/models", envelope=ENVELOPE_DATA, identity_paths=("id",), required_item_paths=("type",), required_item_any_paths=("arch", "compatibility_type", "state", "max_context_length"), item_types=(("type", (str,)),), - capability_paths=("type", "max_context_length"), - api_version="0", - priority=80, - latest=False, + detection_priority=80, ) LLAMACPP_PROPS_SHAPE = ProviderCatalogShape( shape_id="llamacpp.props.v1", provider_id="llamacpp", - endpoint_path="/props", envelope=ENVELOPE_SINGLE, identity_paths=("model_alias", "model_path"), required_item_paths=("default_generation_settings",), required_item_any_paths=("chat_template_caps", "modalities", "total_slots"), item_types=(("default_generation_settings", (Mapping,)),), - capability_paths=( - "chat_template_caps", - "modalities", - "default_generation_settings.n_ctx", - "default_generation_settings.params", - ), - priority=100, + detection_priority=100, ) MISTRAL_MODELS_SHAPE = ProviderCatalogShape( shape_id="mistral.models.rich.v1", provider_id="mistral", - endpoint_path="/v1/models", envelope=ENVELOPE_DATA, identity_paths=("id",), required_item_paths=("capabilities",), @@ -478,14 +268,11 @@ MISTRAL_MODELS_SHAPE = ProviderCatalogShape( "capabilities.classification", ), item_types=(("capabilities", (Mapping,)),), - capability_paths=("capabilities", "max_context_length"), - api_version="v1", - priority=100, + detection_priority=100, ) COPILOT_MODELS_SHAPE = ProviderCatalogShape( shape_id="github-copilot.models.v1", provider_id="copilot", - endpoint_path="/models", envelope=ENVELOPE_DATA, identity_paths=("id",), required_item_paths=("model_picker_enabled", "capabilities.supports"), @@ -493,238 +280,184 @@ COPILOT_MODELS_SHAPE = ProviderCatalogShape( ("model_picker_enabled", (bool,)), ("capabilities.supports", (Mapping,)), ), - capability_paths=("capabilities.supports.tool_calls", "capabilities.supports.vision"), - priority=100, + detection_priority=100, ) ANTHROPIC_MODELS_SHAPE = ProviderCatalogShape( shape_id="anthropic.models.identity.v1", provider_id="anthropic", - endpoint_path="/v1/models", envelope=ENVELOPE_DATA, identity_paths=("id",), required_item_paths=("type", "display_name", "created_at"), item_values=(("type", ("model",)),), - api_version="2023-06-01", - priority=70, + detection_priority=70, ) CHATGPT_MODELS_SHAPE = ProviderCatalogShape( shape_id="chatgpt-subscription.codex-models.v1", provider_id="chatgpt_subscription", - endpoint_path="/backend-api/codex/models?client_version=1.0.0", envelope=ENVELOPE_MODELS, identity_paths=("slug",), required_item_any_paths=("visibility", "priority"), - priority=90, + detection_priority=90, ) SGLANG_MODEL_INFO_SHAPE = ProviderCatalogShape( shape_id="sglang.model-info.v2", provider_id="sglang", - endpoint_path="/model_info", envelope=ENVELOPE_SINGLE, identity_paths=("model_path",), required_item_paths=("is_generation",), - required_item_any_paths=("tokenizer_path", "has_image_understanding", "has_audio_understanding"), + required_item_any_paths=( + "tokenizer_path", + "has_image_understanding", + "has_audio_understanding", + ), item_types=(("is_generation", (bool,)),), - capability_paths=("is_generation", "has_image_understanding", "has_audio_understanding", "preferred_sampling_params"), - api_version="2", - priority=100, + detection_priority=100, ) SGLANG_MODELS_SHAPE = ProviderCatalogShape( shape_id="sglang.models.openai.v1", provider_id="sglang", - endpoint_path="/v1/models", envelope=ENVELOPE_DATA, identity_paths=("id",), required_item_paths=("root", "max_model_len"), item_values=(("owned_by", ("sglang",)),), - capability_paths=("max_model_len",), - priority=80, + detection_priority=80, ) VLLM_MODELS_SHAPE = ProviderCatalogShape( shape_id="vllm.models.openai.v1", provider_id="vllm", - endpoint_path="/v1/models", envelope=ENVELOPE_DATA, identity_paths=("id",), required_item_paths=("root", "max_model_len", "permission"), item_values=(("owned_by", ("vllm",)),), - capability_paths=("max_model_len",), - priority=80, + detection_priority=80, ) HUGGINGFACE_MODEL_SHAPE = ProviderCatalogShape( shape_id="huggingface.hub.model-info.v1", provider_id="huggingface", - endpoint_path="/api/models/{model}", envelope=ENVELOPE_SINGLE, identity_paths=("modelId", "id"), required_item_paths=("pipeline_tag",), item_types=(("pipeline_tag", (str,)),), - capability_paths=("pipeline_tag", "tags", "config"), - priority=80, + detection_priority=80, ) COHERE_MODELS_SHAPE = ProviderCatalogShape( shape_id="cohere.models.rich.v1", provider_id="cohere", - endpoint_path="/v1/models", envelope=ENVELOPE_MODELS, identity_paths=("name",), required_item_paths=("endpoints",), - required_item_any_paths=("context_length", "default_endpoints", "features", "sampling_defaults"), + required_item_any_paths=( + "context_length", + "default_endpoints", + "features", + "sampling_defaults", + ), item_types=(("endpoints", (list, tuple)),), - capability_paths=("endpoints", "context_length", "sampling_defaults"), - api_version="v1", - priority=100, + detection_priority=100, ) MINIMAX_MODELS_SHAPE = ProviderCatalogShape( shape_id="minimax.models.identity.v1", provider_id="minimax", - endpoint_path="/v1/models", envelope=ENVELOPE_DATA, identity_paths=("id",), required_item_paths=("object", "owned_by"), item_values=(("object", ("model",)), ("owned_by", ("minimax",))), - api_version="v1", - priority=90, + detection_priority=90, ) def _provider( provider_id: str, - display_name: str, *, aliases: tuple[str, ...] = (), hosts: tuple[str, ...] = (), - api_shapes: tuple[ProviderApiShape, ...] = (OPENAI_CHAT_SHAPE,), - catalog_shapes: tuple[ProviderCatalogShape, ...] = (), + shapes: tuple[ProviderCatalogShape, ...] = (), ) -> ProviderCapabilitySchema: return ProviderCapabilitySchema( provider_id=provider_id, - display_name=display_name, aliases=aliases, host_suffixes=hosts, - api_shapes=api_shapes, - catalog_shapes=catalog_shapes, + catalog_shapes=shapes, ) PROVIDER_SCHEMAS = { PROVIDER_GENERIC_OPENAI: _provider( PROVIDER_GENERIC_OPENAI, - "General OpenAI-compatible", - aliases=("openai_compatible", "proxy"), - api_shapes=(OPENAI_CHAT_SHAPE, OPENAI_RESPONSES_SHAPE), - catalog_shapes=(GENERAL_DATA_SHAPE, GENERAL_MODELS_SHAPE, GENERAL_BARE_SHAPE), - ), - "openai": _provider( - "openai", - "OpenAI", - hosts=("openai.com",), - api_shapes=(OPENAI_CHAT_SHAPE, OPENAI_RESPONSES_SHAPE), - catalog_shapes=(OPENAI_MODELS_SHAPE,), + aliases=("openai_compatible", "openai_compat"), ), + "openai": _provider("openai", hosts=("openai.com",), shapes=(OPENAI_MODELS_SHAPE,)), "openrouter": _provider( "openrouter", - "OpenRouter", hosts=("openrouter.ai",), - catalog_shapes=(OPENROUTER_MODELS_SHAPE,), + shapes=(OPENROUTER_MODELS_SHAPE,), ), "google": _provider( "google", - "Google Gemini", aliases=("gemini", "google_ai_studio"), hosts=("generativelanguage.googleapis.com",), - api_shapes=(GOOGLE_CONTENT_SHAPE, OPENAI_CHAT_SHAPE), - catalog_shapes=(GOOGLE_MODELS_SHAPE,), + shapes=(GOOGLE_MODELS_SHAPE,), ), "anthropic": _provider( "anthropic", - "Anthropic", hosts=("anthropic.com",), - api_shapes=(ANTHROPIC_MESSAGES_SHAPE,), - catalog_shapes=(ANTHROPIC_MODELS_SHAPE,), + shapes=(ANTHROPIC_MODELS_SHAPE,), ), "ollama": _provider( "ollama", - "Ollama", hosts=("ollama.com",), - api_shapes=(OLLAMA_NATIVE_SHAPE, OPENAI_CHAT_SHAPE), - catalog_shapes=(OLLAMA_SHOW_SHAPE, OLLAMA_TAGS_SHAPE), + shapes=(OLLAMA_SHOW_SHAPE, OLLAMA_TAGS_SHAPE), ), "lmstudio": _provider( "lmstudio", - "LM Studio", aliases=("lm_studio",), - api_shapes=(LMSTUDIO_NATIVE_SHAPE, OPENAI_CHAT_SHAPE, OPENAI_RESPONSES_SHAPE), - catalog_shapes=(LMSTUDIO_MODELS_V1_SHAPE, LMSTUDIO_MODELS_V0_SHAPE), + shapes=(LMSTUDIO_MODELS_V1_SHAPE, LMSTUDIO_MODELS_V0_SHAPE), ), "llamacpp": _provider( "llamacpp", - "llama.cpp", aliases=("llama.cpp", "llama_cpp", "llama_server"), - api_shapes=(OPENAI_CHAT_SHAPE, OPENAI_RESPONSES_SHAPE, ANTHROPIC_MESSAGES_SHAPE), - catalog_shapes=(LLAMACPP_PROPS_SHAPE,), + shapes=(LLAMACPP_PROPS_SHAPE,), ), "mistral": _provider( "mistral", - "Mistral", hosts=("mistral.ai",), - catalog_shapes=(MISTRAL_MODELS_SHAPE,), + shapes=(MISTRAL_MODELS_SHAPE,), ), "copilot": _provider( "copilot", - "GitHub Copilot", aliases=("github_copilot",), hosts=("api.githubcopilot.com",), - catalog_shapes=(COPILOT_MODELS_SHAPE,), + shapes=(COPILOT_MODELS_SHAPE,), ), "chatgpt_subscription": _provider( "chatgpt_subscription", - "ChatGPT Subscription", aliases=("chatgpt-subscription", "chatgpt", "codex_subscription"), hosts=("chatgpt.com",), - api_shapes=(CHATGPT_SUBSCRIPTION_SHAPE,), - catalog_shapes=(CHATGPT_MODELS_SHAPE,), + shapes=(CHATGPT_MODELS_SHAPE,), ), "sglang": _provider( "sglang", - "SGLang", - api_shapes=(OPENAI_CHAT_SHAPE, OPENAI_RESPONSES_SHAPE, SGLANG_NATIVE_SHAPE), - catalog_shapes=(SGLANG_MODEL_INFO_SHAPE, SGLANG_MODELS_SHAPE), - ), - "vllm": _provider( - "vllm", - "vLLM", - api_shapes=(OPENAI_CHAT_SHAPE, OPENAI_RESPONSES_SHAPE), - catalog_shapes=(VLLM_MODELS_SHAPE,), + shapes=(SGLANG_MODEL_INFO_SHAPE, SGLANG_MODELS_SHAPE), ), + "vllm": _provider("vllm", shapes=(VLLM_MODELS_SHAPE,)), "huggingface": _provider( "huggingface", - "Hugging Face", aliases=("hf", "hugging_face"), hosts=("huggingface.co",), - api_shapes=(OPENAI_CHAT_SHAPE,), - catalog_shapes=(HUGGINGFACE_MODEL_SHAPE,), + shapes=(HUGGINGFACE_MODEL_SHAPE,), ), "cohere": _provider( "cohere", - "Cohere", hosts=("cohere.ai", "cohere.com"), - api_shapes=(COHERE_V2_SHAPE, OPENAI_CHAT_SHAPE), - catalog_shapes=(COHERE_MODELS_SHAPE,), + shapes=(COHERE_MODELS_SHAPE,), ), "minimax": _provider( "minimax", - "MiniMax", hosts=("minimax.io", "minimaxi.com"), - api_shapes=(ANTHROPIC_MESSAGES_SHAPE, OPENAI_CHAT_SHAPE), - catalog_shapes=(MINIMAX_MODELS_SHAPE,), + shapes=(MINIMAX_MODELS_SHAPE,), ), } -# These providers are currently OpenAI-compatible in Odysseus and have no -# provider-reported capability catalog shape that is stronger than the general -# structural fallback. Keeping individual identities prevents transport -# quirks from being flattened into "OpenAI" while capability stays per-model. _GENERAL_PROVIDER_ALIASES = { "moonshot": ("moonshot_ai",), "nvidia": ("nvidia_nim", "nim"), @@ -740,44 +473,38 @@ _GENERAL_PROVIDER_ALIASES = { "mlx_lm": ("mlx",), "text_generation_inference": ("tgi", "huggingface_tgi", "hugging_face_tgi"), } -for _provider_id, _display, _hosts in ( - ("moonshot", "Moonshot AI", ("moonshot.ai", "moonshot.cn")), - ("groq", "Groq", ("groq.com",)), - ("nvidia", "NVIDIA NIM", ("nvidia.com",)), - ("cerebras", "Cerebras", ("cerebras.ai",)), - ("deepseek", "DeepSeek", ("deepseek.com",)), - ("together", "Together AI", ("together.xyz", "together.ai")), - ("fireworks", "Fireworks AI", ("fireworks.ai",)), - ("xai", "xAI", ("x.ai",)), - ("zai", "Z.AI", ("z.ai",)), - ("opencode", "OpenCode", ("opencode.ai",)), - ("perplexity", "Perplexity", ("perplexity.ai",)), - ("github_models", "GitHub Models", ("models.inference.ai.azure.com",)), - ("atlas_cloud", "Atlas Cloud", ("atlascloud.ai",)), - ("siliconflow", "SiliconFlow", ("siliconflow.cn", "siliconflow.com",)), - ("kimi_code", "Kimi Code", ("kimi.com",)), - ("venice", "Venice", ("venice.ai",)), - ("azure_openai", "Azure OpenAI", ("openai.azure.com",)), - ("bedrock", "AWS Bedrock", ()), - ("cloudflare_workers_ai", "Cloudflare Workers AI", ()), - ("mlx_lm", "MLX LM", ()), - ("text_generation_inference", "Hugging Face TGI", ()), - ("lmdeploy", "LMDeploy", ()), - ("litellm", "LiteLLM", ()), +for _provider_id, _hosts in ( + ("moonshot", ("moonshot.ai", "moonshot.cn")), + ("groq", ("groq.com",)), + ("nvidia", ("nvidia.com",)), + ("cerebras", ("cerebras.ai",)), + ("deepseek", ("deepseek.com",)), + ("together", ("together.xyz", "together.ai")), + ("fireworks", ("fireworks.ai",)), + ("xai", ("x.ai",)), + ("zai", ("z.ai",)), + ("opencode", ("opencode.ai",)), + ("perplexity", ("perplexity.ai",)), + ("github_models", ("models.inference.ai.azure.com",)), + ("atlas_cloud", ("atlascloud.ai",)), + ("siliconflow", ("siliconflow.cn", "siliconflow.com")), + ("kimi_code", ("kimi.com",)), + ("venice", ("venice.ai",)), + ("azure_openai", ("openai.azure.com",)), + ("bedrock", ()), + ("cloudflare_workers_ai", ()), + ("mlx_lm", ()), + ("text_generation_inference", ()), + ("lmdeploy", ()), + ("litellm", ()), ): PROVIDER_SCHEMAS[_provider_id] = _provider( _provider_id, - _display, aliases=_GENERAL_PROVIDER_ALIASES.get(_provider_id, ()), hosts=_hosts, ) -UNKNOWN_SCHEMA = ProviderCapabilitySchema( - provider_id=PROVIDER_UNKNOWN, - display_name="Unknown provider", - api_shapes=(), - catalog_shapes=(), -) +UNKNOWN_SCHEMA = ProviderCapabilitySchema(provider_id=PROVIDER_UNKNOWN) _ALIASES = { _token(alias): provider_id @@ -788,7 +515,12 @@ _ALIASES = { def normalize_provider_id(value: Any) -> str: token = _token(value) - return _ALIASES.get(token, token if token in PROVIDER_SCHEMAS else PROVIDER_UNKNOWN) + if not token or token == PROVIDER_UNKNOWN: + return PROVIDER_UNKNOWN + # An explicit, previously unseen provider id is still useful identity. It + # selects the inventory-only reader until a native schema is added; it does + # not acquire capabilities merely by being preserved here. + return _ALIASES.get(token, token) def schema_for_provider(value: Any) -> ProviderCapabilitySchema: @@ -808,46 +540,44 @@ def provider_from_host(base_url: Any) -> str: return PROVIDER_UNKNOWN if host.startswith("copilot-api.") and host.endswith(".ghe.com"): return "copilot" - matches = [ + matches = { schema.provider_id for schema in PROVIDER_SCHEMAS.values() if any(_host_matches(host, suffix) for suffix in schema.host_suffixes) - ] - return matches[0] if len(set(matches)) == 1 else PROVIDER_UNKNOWN + } + return next(iter(matches)) if len(matches) == 1 else PROVIDER_UNKNOWN -def catalog_shape_for_payload( +def native_shape_for_payload( payload: Any, *, provider_id: Any = None, - include_general: bool = True, ) -> ProviderCatalogShape | None: normalized = normalize_provider_id(provider_id) - provider_is_explicit = normalized not in {PROVIDER_UNKNOWN, PROVIDER_GENERIC_OPENAI} - if provider_is_explicit: - schemas = (PROVIDER_SCHEMAS[normalized],) + if normalized == PROVIDER_UNKNOWN: + shapes = tuple( + shape + for schema in PROVIDER_SCHEMAS.values() + for shape in schema.catalog_shapes + if shape.detection_priority > 0 + ) else: - schemas = tuple(PROVIDER_SCHEMAS.values()) + schema = PROVIDER_SCHEMAS.get(normalized) + shapes = schema.catalog_shapes if schema else () - candidates = [ - shape - for schema in schemas - for shape in schema.catalog_shapes - if (provider_is_explicit or shape.priority > 0) and shape.matches(payload) - ] - if candidates: - best_priority = max(shape.priority for shape in candidates) - best = [shape for shape in candidates if shape.priority == best_priority] - providers = {shape.provider_id for shape in best} - if len(providers) == 1: - return sorted(best, key=lambda shape: shape.shape_id)[0] - - if not include_general: + matches = [shape for shape in shapes if shape.matches(payload)] + if not matches: return None - for shape in PROVIDER_SCHEMAS[PROVIDER_GENERIC_OPENAI].catalog_shapes: - if shape.matches(payload): - return shape - return None + priority = max(shape.detection_priority for shape in matches) + best = [shape for shape in matches if shape.detection_priority == priority] + providers = {shape.provider_id for shape in best} + if len(providers) != 1: + return None + return sorted(best, key=lambda shape: shape.shape_id)[0] + + +def fallback_shape_for_payload(payload: Any) -> ProviderCatalogShape | None: + return next((shape for shape in FALLBACK_CATALOG_SHAPES if shape.matches(payload)), None) def resolve_provider( @@ -857,75 +587,68 @@ def resolve_provider( endpoint_kind: Any = None, base_url: Any = None, ) -> ProviderResolution: - explicit = normalize_provider_id(provider) - if explicit != PROVIDER_UNKNOWN: - shape = catalog_shape_for_payload(payload, provider_id=explicit) if payload is not None else None - return ProviderResolution(explicit, RESOLUTION_EXPLICIT, PROVIDER_SCHEMAS[explicit], shape) + provider_id = normalize_provider_id(provider) + provider_source = PROVIDER_SOURCE_EXPLICIT - kind = normalize_provider_id(endpoint_kind) - if kind != PROVIDER_UNKNOWN: - shape = catalog_shape_for_payload(payload, provider_id=kind) if payload is not None else None - return ProviderResolution(kind, RESOLUTION_ENDPOINT_KIND, PROVIDER_SCHEMAS[kind], shape) + if provider_id == PROVIDER_UNKNOWN: + provider_id = normalize_provider_id(endpoint_kind) + provider_source = PROVIDER_SOURCE_ENDPOINT_KIND + if provider_id == PROVIDER_UNKNOWN: + provider_id = provider_from_host(base_url) + provider_source = PROVIDER_SOURCE_HOST - host_provider = provider_from_host(base_url) - if host_provider != PROVIDER_UNKNOWN: - shape = catalog_shape_for_payload(payload, provider_id=host_provider) if payload is not None else None + if provider_id != PROVIDER_UNKNOWN: + native = ( + native_shape_for_payload(payload, provider_id=provider_id) + if payload is not None + else None + ) + if native: + return ProviderResolution(provider_id, provider_source, native.shape_id, False) + fallback = fallback_shape_for_payload(payload) if payload is not None else None return ProviderResolution( - host_provider, - RESOLUTION_HOST, - PROVIDER_SCHEMAS[host_provider], - shape, + provider_id, + provider_source, + fallback.shape_id if fallback else "", + bool(fallback), ) - shape = catalog_shape_for_payload(payload, include_general=False) if payload is not None else None - if shape: + native = native_shape_for_payload(payload) if payload is not None else None + if native: return ProviderResolution( - shape.provider_id, - RESOLUTION_NATIVE_SHAPE, - PROVIDER_SCHEMAS[shape.provider_id], - shape, + native.provider_id, + PROVIDER_SOURCE_PAYLOAD, + native.shape_id, + False, ) - shape = catalog_shape_for_payload(payload) if payload is not None else None - if shape: - return ProviderResolution( - PROVIDER_GENERIC_OPENAI, - RESOLUTION_GENERAL_SHAPE, - PROVIDER_SCHEMAS[PROVIDER_GENERIC_OPENAI], - shape, - ) - - return ProviderResolution(PROVIDER_UNKNOWN, RESOLUTION_UNKNOWN, UNKNOWN_SCHEMA, None) + fallback = fallback_shape_for_payload(payload) if payload is not None else None + return ProviderResolution( + PROVIDER_UNKNOWN, + PROVIDER_SOURCE_UNKNOWN, + fallback.shape_id if fallback else "", + bool(fallback), + ) __all__ = [ - "ANTHROPIC_MESSAGES_SHAPE", - "CHATGPT_SUBSCRIPTION_SHAPE", - "COHERE_V2_SHAPE", - "DIALECT_ANTHROPIC_MESSAGES", - "DIALECT_CHATGPT_SUBSCRIPTION", - "DIALECT_COHERE_V2", - "DIALECT_GOOGLE_GENERATE_CONTENT", - "DIALECT_HUGGINGFACE_HUB", - "DIALECT_LLAMACPP_NATIVE", - "DIALECT_LMSTUDIO_NATIVE_V1", - "DIALECT_OLLAMA_NATIVE", - "DIALECT_OPENAI_CHAT", - "DIALECT_OPENAI_RESPONSES", - "DIALECT_SGLANG_NATIVE", - "GOOGLE_CONTENT_SHAPE", - "LMSTUDIO_NATIVE_SHAPE", - "OLLAMA_NATIVE_SHAPE", - "OPENAI_CHAT_SHAPE", - "OPENAI_RESPONSES_SHAPE", + "FALLBACK_CATALOG_SHAPES", + "GENERAL_BARE_SHAPE", + "GENERAL_DATA_SHAPE", + "GENERAL_MODELS_SHAPE", "PROVIDER_GENERIC_OPENAI", "PROVIDER_SCHEMAS", + "PROVIDER_SOURCE_ENDPOINT_KIND", + "PROVIDER_SOURCE_EXPLICIT", + "PROVIDER_SOURCE_HOST", + "PROVIDER_SOURCE_PAYLOAD", + "PROVIDER_SOURCE_UNKNOWN", "PROVIDER_UNKNOWN", - "ProviderApiShape", "ProviderCapabilitySchema", "ProviderCatalogShape", "ProviderResolution", - "catalog_shape_for_payload", + "fallback_shape_for_payload", + "native_shape_for_payload", "normalize_provider_id", "provider_from_host", "resolve_provider", diff --git a/tests/test_model_capability_readers.py b/tests/test_model_capability_readers.py index 17734d448..ca547b15a 100644 --- a/tests/test_model_capability_readers.py +++ b/tests/test_model_capability_readers.py @@ -9,6 +9,7 @@ from src.model_capability_readers.base import ( VENDOR_OLLAMA, VENDOR_OPENAI, VENDOR_OPENROUTER, + VENDOR_UNKNOWN, detect_vendor, stable_model_id_for, ) @@ -24,10 +25,10 @@ def test_detect_vendor_uses_endpoint_kind_and_host_but_not_ambiguous_local_ports assert detect_vendor("https://openrouter.ai/api/v1") == VENDOR_OPENROUTER assert detect_vendor("https://api.openai.com/v1") == VENDOR_OPENAI assert detect_vendor("https://generativelanguage.googleapis.com/v1beta/openai") == VENDOR_GOOGLE - assert detect_vendor("http://127.0.0.1:11434") == VENDOR_GENERIC_OPENAI - assert detect_vendor("http://127.0.0.1:1234") == VENDOR_GENERIC_OPENAI - assert detect_vendor("http://127.0.0.1:8080") == VENDOR_GENERIC_OPENAI - assert detect_vendor("http://localhost:7000/v1") == VENDOR_GENERIC_OPENAI + assert detect_vendor("http://127.0.0.1:11434") == VENDOR_UNKNOWN + assert detect_vendor("http://127.0.0.1:1234") == VENDOR_UNKNOWN + assert detect_vendor("http://127.0.0.1:8080") == VENDOR_UNKNOWN + assert detect_vendor("http://localhost:7000/v1") == VENDOR_UNKNOWN def test_generic_openai_reader_keeps_basic_model_payload_unknown(): @@ -362,7 +363,6 @@ def test_ollama_reader_maps_show_capabilities_and_tags_are_unknown(): assert len(tags) == 1 assert tags[0].capability.family == mc.FAMILY_UNKNOWN - assert tags[0].model_family == "qwen3" assert surfaces(tags[0]) == set() diff --git a/tests/test_provider_capability_schemas.py b/tests/test_provider_capability_schemas.py index 855f85f5c..9d5349c85 100644 --- a/tests/test_provider_capability_schemas.py +++ b/tests/test_provider_capability_schemas.py @@ -1,7 +1,7 @@ -from src import model_behavior_quirks as quirks from src import model_capabilities as mc from src import provider_capability_schemas as pcs from src.model_capability_readers import ( + CANONICAL_MODEL_SHAPE_VERSION, anthropic, chatgpt_subscription, cohere, @@ -14,7 +14,7 @@ from src.model_capability_readers import ( ) -def test_provider_resolution_order_explicit_then_host_then_native_then_general(): +def test_provider_identity_and_catalog_shape_are_resolved_separately(): google_payload = { "models": [ { @@ -27,21 +27,39 @@ def test_provider_resolution_order_explicit_then_host_then_native_then_general() explicit = pcs.resolve_provider(google_payload, provider="openrouter") host = pcs.resolve_provider(google_payload, base_url="https://api.mistral.ai/v1") native = pcs.resolve_provider(google_payload) - general = pcs.resolve_provider([{"id": "future-model", "future": {"x": True}}]) + fallback = pcs.resolve_provider([{"id": "future-model", "future": {"x": True}}]) unknown = pcs.resolve_provider({"future": [{"not_an_identity": True}]}) - assert (explicit.provider_id, explicit.stage) == ("openrouter", pcs.RESOLUTION_EXPLICIT) - assert (host.provider_id, host.stage) == ("mistral", pcs.RESOLUTION_HOST) - assert (native.provider_id, native.stage) == ("google", pcs.RESOLUTION_NATIVE_SHAPE) - assert native.catalog_shape.shape_id == "google.generative-language.models.v1beta" - assert (general.provider_id, general.stage) == ( - pcs.PROVIDER_GENERIC_OPENAI, - pcs.RESOLUTION_GENERAL_SHAPE, - ) - assert (unknown.provider_id, unknown.stage) == ( - pcs.PROVIDER_UNKNOWN, - pcs.RESOLUTION_UNKNOWN, - ) + assert explicit.to_dict() == { + "provider": "openrouter", + "provider_source": pcs.PROVIDER_SOURCE_EXPLICIT, + "shape": "fallback.models.envelope.v1", + "fallback": True, + } + assert host.to_dict() == { + "provider": "mistral", + "provider_source": pcs.PROVIDER_SOURCE_HOST, + "shape": "fallback.models.envelope.v1", + "fallback": True, + } + assert native.to_dict() == { + "provider": "google", + "provider_source": pcs.PROVIDER_SOURCE_PAYLOAD, + "shape": "google.generative-language.models.v1beta", + "fallback": False, + } + assert fallback.to_dict() == { + "provider": pcs.PROVIDER_UNKNOWN, + "provider_source": pcs.PROVIDER_SOURCE_UNKNOWN, + "shape": "fallback.models.list.v1", + "fallback": True, + } + assert unknown.to_dict() == { + "provider": pcs.PROVIDER_UNKNOWN, + "provider_source": pcs.PROVIDER_SOURCE_UNKNOWN, + "shape": "", + "fallback": False, + } def test_provider_host_matching_rejects_lookalikes_and_does_not_use_ports(): @@ -53,31 +71,57 @@ def test_provider_host_matching_rejects_lookalikes_and_does_not_use_ports(): assert pcs.provider_from_host("http://127.0.0.1:30000") == pcs.PROVIDER_UNKNOWN -def test_provider_aliases_collapse_runtime_names_without_url_path_guessing(): +def test_provider_aliases_only_normalize_explicit_identity(): assert pcs.normalize_provider_id("opencode-go") == "opencode" assert pcs.normalize_provider_id("opencode-zen") == "opencode" assert pcs.normalize_provider_id("nvidia-nim") == "nvidia" assert pcs.normalize_provider_id("tgi") == "text_generation_inference" assert pcs.normalize_provider_id("llama.cpp") == "llamacpp" assert pcs.normalize_provider_id("Z.AI") == "zai" + assert pcs.normalize_provider_id("future-provider") == "future_provider" -def test_current_native_catalog_shapes_are_discriminating_and_versioned(): +def test_unregistered_explicit_provider_is_preserved_but_stays_on_fallback(): + resolution = pcs.resolve_provider( + {"data": [{"id": "future-model", "capabilities": {"tools": True}}]}, + provider="future-provider", + ) + records = records_from_payload( + {"data": [{"id": "future-model", "capabilities": {"tools": True}}]}, + vendor="future-provider", + ) + + assert resolution.to_dict() == { + "provider": "future_provider", + "provider_source": pcs.PROVIDER_SOURCE_EXPLICIT, + "shape": "fallback.models.data.v1", + "fallback": True, + } + assert records[0].vendor == "future_provider" + assert records[0].capability.family == mc.FAMILY_UNKNOWN + assert records[0].capability.capabilities == () + + +def test_current_native_catalog_shapes_are_discriminating(): cases = ( ( {"models": [{"key": "local/model", "type": "llm", "capabilities": {"vision": True}}]}, + "lmstudio", "lmstudio.models.native.v1", ), ( {"data": [{"id": "legacy", "type": "vlm", "arch": "gemma"}]}, + "lmstudio", "lmstudio.models.native.v0", ), ( {"models": [{"name": "local", "digest": "abc", "details": {"family": "qwen3"}}]}, + "ollama", "ollama.tags.v1", ), ( {"capabilities": ["completion", "vision"], "model_info": {"x.context_length": 4096}}, + "ollama", "ollama.show.v1", ), ( @@ -86,10 +130,12 @@ def test_current_native_catalog_shapes_are_discriminating_and_versioned(): "default_generation_settings": {"n_ctx": 4096}, "chat_template_caps": {"supports_tools": True}, }, + "llamacpp", "llamacpp.props.v1", ), ( {"data": [{"id": "mistral", "capabilities": {"completion_chat": True, "vision": False}}]}, + "mistral", "mistral.models.rich.v1", ), ( @@ -102,6 +148,7 @@ def test_current_native_catalog_shapes_are_discriminating_and_versioned(): } ] }, + "copilot", "github-copilot.models.v1", ), ( @@ -111,6 +158,7 @@ def test_current_native_catalog_shapes_are_discriminating_and_versioned(): "is_generation": True, "has_image_understanding": False, }, + "sglang", "sglang.model-info.v2", ), ( @@ -127,46 +175,38 @@ def test_current_native_catalog_shapes_are_discriminating_and_versioned(): } ], }, + "vllm", "vllm.models.openai.v1", ), ( {"models": [{"slug": "gpt-example", "visibility": "list", "priority": 1}]}, + "chatgpt_subscription", "chatgpt-subscription.codex-models.v1", ), ( - { - "models": [ - { - "name": "command-example", - "endpoints": ["chat"], - "context_length": 131072, - } - ] - }, + {"models": [{"name": "command-example", "endpoints": ["chat"], "context_length": 131072}]}, + "cohere", "cohere.models.rich.v1", ), ( { "object": "list", - "data": [ - { - "id": "MiniMax-M2-example", - "object": "model", - "owned_by": "minimax", - } - ], + "data": [{"id": "MiniMax-M2", "object": "model", "owned_by": "minimax"}], }, + "minimax", "minimax.models.identity.v1", ), ) - for payload, expected_shape in cases: + for payload, expected_provider, expected_shape in cases: resolution = pcs.resolve_provider(payload) - assert resolution.stage == pcs.RESOLUTION_NATIVE_SHAPE - assert resolution.catalog_shape.shape_id == expected_shape + assert resolution.provider_id == expected_provider + assert resolution.provider_source == pcs.PROVIDER_SOURCE_PAYLOAD + assert resolution.shape_id == expected_shape + assert resolution.fallback is False -def test_native_shape_detection_rejects_wrong_field_types_before_general_fallback(): +def test_wrong_native_field_types_degrade_to_explicit_fallback_inventory(): malformed_cohere = pcs.resolve_provider( {"models": [{"name": "future", "endpoints": "chat", "context_length": 4096}]} ) @@ -174,57 +214,69 @@ def test_native_shape_detection_rejects_wrong_field_types_before_general_fallbac {"data": [{"id": "future", "capabilities": ["completion_chat"]}]} ) - assert (malformed_cohere.provider_id, malformed_cohere.stage) == ( - pcs.PROVIDER_GENERIC_OPENAI, - pcs.RESOLUTION_GENERAL_SHAPE, - ) - assert (malformed_mistral.provider_id, malformed_mistral.stage) == ( - pcs.PROVIDER_GENERIC_OPENAI, - pcs.RESOLUTION_GENERAL_SHAPE, - ) + assert malformed_cohere.provider_id == pcs.PROVIDER_UNKNOWN + assert malformed_cohere.shape_id == "fallback.models.envelope.v1" + assert malformed_cohere.fallback is True + assert malformed_mistral.provider_id == pcs.PROVIDER_UNKNOWN + assert malformed_mistral.shape_id == "fallback.models.data.v1" + assert malformed_mistral.fallback is True -def test_general_reader_promotes_only_explicit_structural_fields_and_accepts_bare_lists(): - records = generic_openai.records_from_payload( - [ - { - "id": "future-rich-model", - "type": "chat", - "architecture": { - "input_modalities": ["text", "image"], - "output_modalities": ["text"], - }, - "supported_parameters": ["tools", "structured_outputs", "temperature"], - "max_model_len": 131072, - "future_capability": {"may_be_important_later": True}, +def test_fallback_reader_is_identity_only_even_for_dangerous_looking_fields(): + payload = [ + { + "id": "future-rich-model", + "type": "chat", + "architecture": { + "input_modalities": ["text", "image"], + "output_modalities": ["text"], }, - { - "id": "vision-reasoning-tools-in-the-name-only", - "description": "Claims every capability in prose", - "type": "image", - "future_capability": True, - }, - ] - ) + "capabilities": {"supports": {"tools": True, "reasoning": True}}, + "supported_parameters": ["tools", "structured_outputs", "temperature"], + "max_model_len": 131072, + }, + {"key": "key-only-model", "pipeline_tag": "text-to-image"}, + {"slug": "slug-only-model", "modality": "text_to_image"}, + ] - rich, identity_only = records - assert rich.capability.family == mc.FAMILY_CHAT - assert rich.capability.modalities.input == (mc.MODALITY_TEXT, mc.MODALITY_IMAGE) - assert rich.capability.capabilities == ( - mc.CAP_TOOL_CALL, - mc.CAP_STRUCTURED_OUTPUT, - mc.CAP_VISION, - ) - assert dict(rich.capability.limits) == {"context_tokens": 131072} - assert [control.control for control in rich.deterministic_controls] == [mc.CONTROL_TEMPERATURE] - assert rich.raw["future_capability"] == {"may_be_important_later": True} + direct = generic_openai.records_from_payload(payload) + wrapped = records_from_payload(payload, vendor="together") - assert identity_only.capability.family == mc.FAMILY_UNKNOWN - assert identity_only.capability.capabilities == () - assert identity_only.raw["future_capability"] is True + assert [record.model_id for record in direct] == [ + "future-rich-model", + "key-only-model", + "slug-only-model", + ] + for record in (*direct, *wrapped): + assert record.capability.family == mc.FAMILY_UNKNOWN + assert record.capability.capabilities == () + assert dict(record.capability.limits) == {} + assert record.deterministic_controls == () + + lean = wrapped[0].to_dict() + assert lean == { + "schema_version": CANONICAL_MODEL_SHAPE_VERSION, + "provider": "together", + "model": "future-rich-model", + "stable_id": "together|global|future-rich-model", + "family": "unknown", + "task": "unknown", + "modalities": {"input": [], "output": []}, + "features": [], + "limits": {}, + "controls": [], + "evidence": { + "source": "provider_reader", + "confidence": "unknown", + "provider_source": "explicit", + "shape": "fallback.models.list.v1", + "fallback": True, + }, + } + assert wrapped[0].to_dict(include_raw=True)["raw"] == payload[0] -def test_general_reader_fails_soft_for_null_and_malformed_envelopes(): +def test_fallback_reader_fails_soft_for_null_and_malformed_envelopes(): for payload in ( {"data": None}, {"models": None}, @@ -235,13 +287,12 @@ def test_general_reader_fails_soft_for_null_and_malformed_envelopes(): assert generic_openai.records_from_payload(payload) == () -def test_mistral_reader_maps_per_model_capabilities_without_provider_wide_inheritance(): +def test_mistral_reader_maps_per_model_capabilities_without_provider_inheritance(): records = mistral.records_from_payload( { "data": [ { "id": "vision-chat", - "root": "mistral-small", "capabilities": { "completion_chat": True, "function_calling": True, @@ -258,10 +309,7 @@ def test_mistral_reader_maps_per_model_capabilities_without_provider_wide_inheri "vision": False, }, }, - { - "id": "future-card", - "capabilities": {"future_only": True}, - }, + {"id": "future-card", "capabilities": {"future_only": True}}, ] } ) @@ -270,14 +318,12 @@ def test_mistral_reader_maps_per_model_capabilities_without_provider_wide_inheri assert records[0].capability.modalities.input == (mc.MODALITY_TEXT, mc.MODALITY_IMAGE) assert records[0].capability.capabilities == (mc.CAP_VISION, mc.CAP_TOOL_CALL) assert dict(records[0].capability.limits) == {"context_tokens": 32768} - assert records[0].model_family == "mistral-small" assert records[1].capability.family == mc.FAMILY_CLASSIFICATION assert records[2].capability.family == mc.FAMILY_UNKNOWN - assert records[2].capability.capabilities == () def test_copilot_reader_uses_picker_and_nested_supports_shape(): - records = copilot.records_from_payload( + record = copilot.records_from_payload( { "data": [ { @@ -285,23 +331,17 @@ def test_copilot_reader_uses_picker_and_nested_supports_shape(): "model_picker_enabled": True, "capabilities": {"supports": {"tool_calls": True, "vision": True}}, "limits": {"max_prompt_tokens": 64000, "max_output_tokens": 8192}, - }, - { - "id": "utility-model", - "model_picker_enabled": False, - "capabilities": {"supports": {}}, - }, + } ] } - ) + )[0] - assert records[0].capability.family == mc.FAMILY_CHAT - assert records[0].capability.capabilities == (mc.CAP_TOOL_CALL, mc.CAP_VISION) - assert dict(records[0].capability.limits) == {"input_tokens": 64000, "output_tokens": 8192} - assert records[1].capability.family == mc.FAMILY_UNKNOWN + assert record.capability.family == mc.FAMILY_CHAT + assert record.capability.capabilities == (mc.CAP_TOOL_CALL, mc.CAP_VISION) + assert dict(record.capability.limits) == {"input_tokens": 64000, "output_tokens": 8192} -def test_sglang_model_info_is_structural_and_non_generation_stays_unknown(): +def test_sglang_model_info_maps_native_generation_flags_only(): generation = sglang.records_from_payload( { "model_path": "org/vision-model", @@ -309,7 +349,6 @@ def test_sglang_model_info_is_structural_and_non_generation_stays_unknown(): "is_generation": True, "has_image_understanding": True, "has_audio_understanding": True, - "model_type": "future_arch", "preferred_sampling_params": {"temperature": 0.2, "top_p": 0.9}, } )[0] @@ -333,11 +372,10 @@ def test_sglang_model_info_is_structural_and_non_generation_stays_unknown(): mc.CONTROL_TEMPERATURE, mc.CONTROL_TOP_P, ] - assert generation.model_family == "future_arch" assert pooling.capability.family == mc.FAMILY_UNKNOWN -def test_identity_only_catalogs_do_not_claim_model_capability(): +def test_identity_only_native_catalogs_remain_unknown(): anthropic_record = anthropic.records_from_payload( { "data": [ @@ -356,13 +394,7 @@ def test_identity_only_catalogs_do_not_claim_model_capability(): minimax_record = records_from_payload( { "object": "list", - "data": [ - { - "id": "MiniMax-M2-example", - "object": "model", - "owned_by": "minimax", - } - ], + "data": [{"id": "MiniMax-M2", "object": "model", "owned_by": "minimax"}], } )[0] @@ -372,7 +404,7 @@ def test_identity_only_catalogs_do_not_claim_model_capability(): assert minimax_record.capability.family == mc.FAMILY_UNKNOWN -def test_huggingface_reader_maps_pipeline_tag_as_registry_evidence(): +def test_huggingface_reader_maps_provider_specific_pipeline_metadata(): record = huggingface.records_from_payload( { "modelId": "org/vision-model", @@ -387,7 +419,6 @@ def test_huggingface_reader_maps_pipeline_tag_as_registry_evidence(): assert record.capability.capabilities == (mc.CAP_VISION,) assert record.capability.source == mc.SOURCE_COOKBOOK_HF assert record.capability.confidence == mc.CONFIDENCE_REGISTRY - assert record.model_family == "future_vlm" def test_cohere_reader_maps_only_native_endpoint_and_limit_fields(): @@ -411,7 +442,6 @@ def test_cohere_reader_maps_only_native_endpoint_and_limit_fields(): ) assert chat.capability.family == mc.FAMILY_CHAT - assert chat.capability.modalities.input == (mc.MODALITY_TEXT,) assert dict(chat.capability.limits) == {"context_tokens": 131072} assert [control.control for control in chat.deterministic_controls] == [ mc.CONTROL_TEMPERATURE, @@ -422,8 +452,8 @@ def test_cohere_reader_maps_only_native_endpoint_and_limit_fields(): assert ambiguous.capability.family == mc.FAMILY_UNKNOWN -def test_registry_wrapper_records_resolution_and_preserves_compatible_provider_identity(): - mistral_records = records_from_payload( +def test_reader_wrapper_adds_one_lean_evidence_object(): + record = records_from_payload( { "data": [ { @@ -432,74 +462,20 @@ def test_registry_wrapper_records_resolution_and_preserves_compatible_provider_i } ] } - ) - together_records = records_from_payload( - [{"id": "served/model", "type": "chat", "supported_parameters": ["tools"]}], - vendor="together", - ) + )[0] + serialized = record.to_dict() - assert mistral_records[0].vendor == "mistral" - assert mistral_records[0].provider_schema_id == "mistral" - assert mistral_records[0].catalog_shape_id == "mistral.models.rich.v1" - assert mistral_records[0].provider_resolution == pcs.RESOLUTION_NATIVE_SHAPE - - assert together_records[0].vendor == "together" - assert together_records[0].capability.family == mc.FAMILY_CHAT - assert together_records[0].provider_schema_id == "together" - assert together_records[0].provider_resolution == pcs.RESOLUTION_EXPLICIT - - -def test_reasoning_control_preserves_canonical_and_native_values(): - control = mc.ReasoningControl.build( - mechanism="reasoning_effort", - values=("enabled", "disabled"), - native_values=("high", "medium", "low", "none"), - request_path="reasoning_effort", - response_paths=("choices[].delta.reasoning",), - status="claimed", - source="provider_docs_registry", - confidence="registry", - ) - - assert control.values == (mc.REASONING_CONTROL_VALUE_ON, mc.REASONING_CONTROL_VALUE_OFF) - assert control.native_values == ("high", "medium", "low", "none") - assert mc.ReasoningControl.from_dict(control.to_dict()) == control - - -def test_model_quirks_require_structured_exact_identity_not_name_parsing(): - matching = quirks.matching_quirks( - provider="moonshot", - model_id="kimi-k2.5", - model_family="", - api_dialect=pcs.DIALECT_OPENAI_CHAT, - capabilities=(mc.CAP_REASONING,), - ) - lookalike = quirks.matching_quirks( - provider="moonshot", - model_id="proxy/kimi-k2.5-lookalike", - model_family="", - api_dialect=pcs.DIALECT_OPENAI_CHAT, - capabilities=(mc.CAP_REASONING,), - ) - opus_without_version = quirks.matching_quirks( - provider="anthropic", - model_family="claude-opus", - model_id="claude-opus-4-8-in-name-only", - api_dialect=pcs.DIALECT_ANTHROPIC_MESSAGES, - ) - opus_structured = quirks.matching_quirks( - provider="anthropic", - model_family="claude-opus", - model_version=(4, 8), - api_dialect=pcs.DIALECT_ANTHROPIC_MESSAGES, - ) - - assert {quirk.quirk_id for quirk in matching} == { - "moonshot.kimi-k2.5-k2.6.provider-fixed-temperature", - "moonshot.kimi-k2.5-k2.6.tool-history-reasoning-content", + assert record.vendor == "mistral" + assert serialized["schema_version"] == 1 + assert serialized["provider"] == "mistral" + assert serialized["features"] == [mc.CAP_TOOL_CALL] + assert serialized["evidence"] == { + "source": mc.SOURCE_PROVIDER_READER, + "confidence": mc.CONFIDENCE_PROVIDER_REPORTED, + "provider_source": pcs.PROVIDER_SOURCE_PAYLOAD, + "shape": "mistral.models.rich.v1", + "fallback": False, } - assert lookalike == () - assert opus_without_version == () - assert [quirk.quirk_id for quirk in opus_structured] == [ - "anthropic.claude-opus-4.7-plus.omit-sampling-controls" - ] + assert "capability" not in serialized + assert "capability_assertions" not in serialized + assert "deterministic_controls" not in serialized