"""Static model capability metadata for AI Automation Suggester. The catalog is intentionally conservative. Providers still allow custom model names, but known metadata lets the integration choose safer request parameters and warn users about stale defaults, preview models, and deprecated IDs. """ from __future__ import annotations from dataclasses import dataclass, field STATUS_STABLE = "stable" STATUS_PREVIEW = "preview" STATUS_DEPRECATED = "deprecated" STATUS_CUSTOM = "custom" @dataclass(frozen=True) class ModelCapabilities: """Capabilities for a specific model or model family.""" model: str label: str = "" endpoint_family: str = "chat" context_window: int | None = None max_output_tokens: int | None = None token_parameter: str = "max_tokens" supports_structured_output: bool = False supports_json_schema: bool = False supports_reasoning: bool = False omit_temperature: bool = False status: str = STATUS_STABLE notes: tuple[str, ...] = field(default_factory=tuple) @dataclass(frozen=True) class ProviderCatalog: """Default model and known capabilities for a provider.""" provider: str default_model: str models: tuple[ModelCapabilities, ...] supports_model_listing: bool = False model_listing_url: str | None = None SUGGESTION_RESPONSE_SCHEMA: dict = { "type": "object", "properties": { "suggestions": { "type": "array", "items": { "type": "object", "properties": { "title": {"type": "string"}, "description": {"type": "string"}, "yaml": {"type": "string"}, "entities_used": {"type": "array", "items": {"type": "string"}}, "automation_ids_used": {"type": "array", "items": {"type": "string"}}, "confidence": {"type": "number"}, "warnings": {"type": "array", "items": {"type": "string"}}, }, "required": ["title", "description", "yaml"], "additionalProperties": True, }, } }, "required": ["suggestions"], "additionalProperties": True, } OPENAI_MODELS = ( ModelCapabilities( "gpt-5.5", "GPT-5.5", endpoint_family="responses", token_parameter="max_output_tokens", supports_structured_output=True, supports_json_schema=True, supports_reasoning=True, omit_temperature=True, notes=("Use the OpenAI Responses API.",), ), ModelCapabilities( "gpt-5.5-pro", "GPT-5.5 Pro", endpoint_family="responses", token_parameter="max_output_tokens", supports_structured_output=True, supports_json_schema=True, supports_reasoning=True, omit_temperature=True, notes=("Highest-intelligence OpenAI option; expect higher latency and cost.",), ), ModelCapabilities( "gpt-5.4-mini", "GPT-5.4 Mini", endpoint_family="responses", token_parameter="max_output_tokens", supports_structured_output=True, supports_json_schema=True, supports_reasoning=True, omit_temperature=True, ), ModelCapabilities( "gpt-5.4", "GPT-5.4", endpoint_family="responses", token_parameter="max_output_tokens", supports_structured_output=True, supports_json_schema=True, supports_reasoning=True, omit_temperature=True, ), ModelCapabilities( "gpt-4o-mini", "GPT-4o Mini", token_parameter="max_completion_tokens", supports_structured_output=True, supports_json_schema=True, ), ModelCapabilities( "gpt-4.1-mini", "GPT-4.1 Mini", token_parameter="max_completion_tokens", supports_structured_output=True, supports_json_schema=True, ), ModelCapabilities( "o3", "o3", token_parameter="max_completion_tokens", supports_reasoning=True, omit_temperature=True, ), ModelCapabilities( "o4-mini", "o4 Mini", token_parameter="max_completion_tokens", supports_reasoning=True, omit_temperature=True, ), ) PROVIDER_CATALOGS: dict[str, ProviderCatalog] = { "OpenAI": ProviderCatalog( "OpenAI", "gpt-5.4-mini", OPENAI_MODELS, True, "https://api.openai.com/v1/models", ), "OpenAI Azure": ProviderCatalog( "OpenAI Azure", "gpt-5.4-mini", OPENAI_MODELS, ), "Anthropic": ProviderCatalog( "Anthropic", "claude-sonnet-4-6", ( ModelCapabilities( "claude-opus-4-7", "Claude Opus 4.7", token_parameter="max_tokens", supports_reasoning=True, notes=("Use adaptive thinking; manual budget_tokens is not accepted.",), ), ModelCapabilities( "claude-sonnet-4-6", "Claude Sonnet 4.6", token_parameter="max_tokens", supports_reasoning=True, notes=("Adaptive thinking is recommended for reasoning-heavy requests.",), ), ModelCapabilities("claude-haiku-4-5", "Claude Haiku 4.5"), ModelCapabilities( "claude-3-7-sonnet-latest", "Claude 3.7 Sonnet Latest", status=STATUS_DEPRECATED, notes=("Prefer Claude Sonnet 4.6 for new configurations.",), ), ), True, "https://api.anthropic.com/v1/models", ), "Google": ProviderCatalog( "Google", "gemini-2.5-flash", ( ModelCapabilities( "gemini-2.5-flash", "Gemini 2.5 Flash", supports_structured_output=True, supports_json_schema=True, ), ModelCapabilities( "gemini-2.5-pro", "Gemini 2.5 Pro", supports_structured_output=True, supports_json_schema=True, ), ModelCapabilities( "gemini-3-flash-preview", "Gemini 3 Flash Preview", supports_structured_output=True, supports_json_schema=True, status=STATUS_PREVIEW, ), ModelCapabilities( "gemini-3.1-pro-preview", "Gemini 3.1 Pro Preview", supports_structured_output=True, supports_json_schema=True, status=STATUS_PREVIEW, ), ModelCapabilities( "gemini-2.0-flash", "Gemini 2.0 Flash", supports_structured_output=True, supports_json_schema=True, status=STATUS_DEPRECATED, notes=("Gemini 2.0 Flash is deprecated in current Gemini docs.",), ), ), True, "https://generativelanguage.googleapis.com/v1beta/models", ), "Groq": ProviderCatalog( "Groq", "llama-3.3-70b-versatile", ( ModelCapabilities("llama-3.3-70b-versatile", "Llama 3.3 70B Versatile"), ModelCapabilities("llama-3.1-8b-instant", "Llama 3.1 8B Instant"), ModelCapabilities("openai/gpt-oss-120b", "GPT OSS 120B"), ModelCapabilities("openai/gpt-oss-20b", "GPT OSS 20B"), ModelCapabilities( "llama3-8b-8192", "Llama 3 8B 8192", status=STATUS_DEPRECATED, notes=("Use llama-3.1-8b-instant or llama-3.3-70b-versatile.",), ), ), True, "https://api.groq.com/openai/v1/models", ), "LocalAI": ProviderCatalog("LocalAI", "llama3", (), True), "Ollama": ProviderCatalog("Ollama", "llama3.1", (), True), "Custom OpenAI": ProviderCatalog("Custom OpenAI", "gpt-4o-mini", OPENAI_MODELS, True), "Generic OpenAI": ProviderCatalog("Generic OpenAI", "gpt-4o-mini", OPENAI_MODELS, True), "Mistral AI": ProviderCatalog( "Mistral AI", "mistral-small-latest", ( ModelCapabilities( "mistral-small-latest", "Mistral Small Latest", supports_structured_output=True, supports_json_schema=True, ), ModelCapabilities( "mistral-medium-latest", "Mistral Medium Latest", supports_structured_output=True, supports_json_schema=True, ), ModelCapabilities( "mistral-large-latest", "Mistral Large Latest", supports_structured_output=True, supports_json_schema=True, ), ModelCapabilities( "mistral-medium", "Mistral Medium", status=STATUS_DEPRECATED, notes=("Prefer current -latest aliases or published dated IDs.",), ), ), True, "https://api.mistral.ai/v1/models", ), "Perplexity AI": ProviderCatalog( "Perplexity AI", "sonar", ( ModelCapabilities("sonar", "Sonar", supports_structured_output=True, supports_json_schema=True), ModelCapabilities( "sonar-pro", "Sonar Pro", supports_structured_output=True, supports_json_schema=True, ), ModelCapabilities( "sonar-reasoning-pro", "Sonar Reasoning Pro", supports_structured_output=True, supports_json_schema=True, supports_reasoning=True, ), ModelCapabilities( "sonar-deep-research", "Sonar Deep Research", supports_structured_output=True, supports_json_schema=True, supports_reasoning=True, notes=("Expect higher latency for research workflows.",), ), ), ), "OpenRouter": ProviderCatalog( "OpenRouter", "openai/gpt-5.4-mini", ( ModelCapabilities( "openai/gpt-5.5", "OpenAI GPT-5.5 via OpenRouter", token_parameter="max_tokens", supports_structured_output=True, supports_json_schema=True, supports_reasoning=True, omit_temperature=True, ), ModelCapabilities( "openai/gpt-5.4-mini", "OpenAI GPT-5.4 Mini via OpenRouter", supports_structured_output=True, supports_json_schema=True, supports_reasoning=True, omit_temperature=True, ), ModelCapabilities( "anthropic/claude-opus-4.7", "Claude Opus 4.7 via OpenRouter", supports_structured_output=True, supports_json_schema=True, supports_reasoning=True, ), ModelCapabilities( "anthropic/claude-sonnet-4.6", "Claude Sonnet 4.6 via OpenRouter", supports_structured_output=True, supports_json_schema=True, supports_reasoning=True, ), ), True, "https://openrouter.ai/api/v1/models", ), } def get_provider_catalog(provider: str) -> ProviderCatalog | None: """Return the static catalog for a provider.""" return PROVIDER_CATALOGS.get(provider) def get_default_model(provider: str) -> str: """Return the recommended default model for a provider.""" catalog = get_provider_catalog(provider) return catalog.default_model if catalog else "" def get_model_capabilities(provider: str, model: str | None) -> ModelCapabilities: """Return known capabilities for a selected model. Unknown user-supplied model names remain valid and are treated as custom chat models with conservative OpenAI-compatible defaults. """ selected = model or get_default_model(provider) catalog = get_provider_catalog(provider) if catalog: for item in catalog.models: if item.model == selected: return item for item in catalog.models: if selected.startswith(item.model.rstrip("*")): return item if provider in {"OpenAI", "OpenAI Azure", "Custom OpenAI", "Generic OpenAI"}: if selected.startswith(("gpt-5", "o3", "o4")): return ModelCapabilities( selected, endpoint_family="responses" if provider == "OpenAI" else "chat", token_parameter="max_output_tokens" if provider == "OpenAI" else "max_completion_tokens", supports_structured_output=provider == "OpenAI", supports_json_schema=provider == "OpenAI", supports_reasoning=True, omit_temperature=True, ) if selected.startswith(("gpt-4o", "gpt-4.1")): return ModelCapabilities( selected, token_parameter="max_completion_tokens", supports_structured_output=True, supports_json_schema=True, ) return ModelCapabilities(selected, status=STATUS_CUSTOM) def model_uses_responses_api(provider: str, model: str | None) -> bool: """Return True when the model should use OpenAI's Responses API.""" return provider == "OpenAI" and get_model_capabilities(provider, model).endpoint_family == "responses" def chat_token_parameter(provider: str, model: str | None) -> str: """Return the correct token limit parameter for chat-compatible APIs.""" capabilities = get_model_capabilities(provider, model) if capabilities.token_parameter == "max_output_tokens": return "max_completion_tokens" return capabilities.token_parameter def should_send_temperature(provider: str, model: str | None) -> bool: """Return False for models known to reject custom temperature settings.""" return not get_model_capabilities(provider, model).omit_temperature def supports_json_schema(provider: str, model: str | None) -> bool: """Return True if this provider/model should receive JSON schema requests.""" return get_model_capabilities(provider, model).supports_json_schema def json_schema_response_format() -> dict: """Return an OpenAI-compatible JSON schema response_format block.""" return { "type": "json_schema", "json_schema": { "name": "automation_suggestions", "strict": False, "schema": SUGGESTION_RESPONSE_SCHEMA, }, } def _strip_schema_keys(value, unsupported_keys: set[str]): """Return a copy of a schema value without unsupported keys.""" if isinstance(value, dict): return { key: _strip_schema_keys(schema_value, unsupported_keys) for key, schema_value in value.items() if key not in unsupported_keys } if isinstance(value, list): return [_strip_schema_keys(item, unsupported_keys) for item in value] return value def google_json_schema_response_format() -> dict: """Return a Google Gemini-compatible JSON schema response format.""" response_format = json_schema_response_format() return { "type": response_format["type"], "json_schema": { "name": response_format["json_schema"]["name"], "strict": response_format["json_schema"]["strict"], "schema": _strip_schema_keys( response_format["json_schema"]["schema"], {"additionalProperties"}, ), }, } def compatibility_warnings(provider: str, model: str | None) -> list[str]: """Return user-facing warnings for the selected provider/model.""" capabilities = get_model_capabilities(provider, model) warnings: list[str] = [] if capabilities.status == STATUS_PREVIEW: warnings.append(f"{capabilities.model} is a preview model and may change without notice.") if capabilities.status == STATUS_DEPRECATED: warnings.append(f"{capabilities.model} is deprecated; choose a current model when possible.") warnings.extend(capabilities.notes) return warnings