diff --git a/docs/devel_doc/openapi.json b/docs/devel_doc/openapi.json index 6cb66b486..ac167792c 100644 --- a/docs/devel_doc/openapi.json +++ b/docs/devel_doc/openapi.json @@ -12356,6 +12356,51 @@ "title": "AgentSkill", "description": "Represents a distinct capability or function that an agent can perform." }, + "AllowedModel": { + "properties": { + "name": { + "type": "string", + "minLength": 1, + "title": "Local model name" + }, + "provider_model_id": { + "type": "string", + "minLength": 1, + "title": "Backend model id" + }, + "model_type": { + "anyOf": [ + { + "type": "string", + "enum": [ + "llm", + "embedding", + "rerank" + ] + }, + { + "type": "null" + } + ], + "title": "Model type", + "description": "Optional capability override (llm, embedding, or rerank)." + }, + "metadata": { + "additionalProperties": true, + "type": "object", + "title": "Model metadata", + "description": "Optional catalog metadata" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "name", + "provider_model_id" + ], + "title": "AllowedModel", + "description": "Configuration for an allowed inference model.\n\nAccepts string IDs or dictionaries. String IDs populate both `name` and\n`provider_model_id`; dictionary inputs default `name` to `provider_model_id`." + }, "AllowedToolsFilter": { "properties": { "tool_names": { @@ -12720,19 +12765,19 @@ "tenant_id": { "type": "string", "format": "password", - "title": "Tenant Id", + "title": "Tenant ID", "writeOnly": true }, "client_id": { "type": "string", "format": "password", - "title": "Client Id", + "title": "Client ID", "writeOnly": true }, "client_secret": { "type": "string", "format": "password", - "title": "Client Secret", + "title": "Client secret", "writeOnly": true }, "scope": { @@ -12752,6 +12797,137 @@ "title": "AzureEntraIdConfiguration", "description": "Microsoft Entra ID authentication attributes for Azure." }, + "AzureInferenceProviderConfig": { + "properties": { + "base_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Azure OpenAI endpoint" + }, + "api_key": { + "anyOf": [ + { + "type": "string", + "format": "password", + "writeOnly": true + }, + { + "type": "null" + } + ], + "title": "API key" + }, + "api_version": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "API version" + }, + "entra_id": { + "anyOf": [ + { + "$ref": "#/components/schemas/AzureEntraIdConfiguration" + }, + { + "type": "null" + } + ], + "title": "Entra ID", + "description": "Preferred Entra ID auth for this provider. Takes precedence over the legacy top-level Azure Entra ID settings." + } + }, + "additionalProperties": false, + "type": "object", + "title": "AzureInferenceProviderConfig", + "description": "Connection settings for an Azure OpenAI inference provider." + }, + "AzureInferenceProviderEntry": { + "properties": { + "id": { + "type": "string", + "title": "Provider ID" + }, + "skip_model_discovery": { + "type": "boolean", + "title": "Skip model discovery", + "default": false + }, + "allowed_models": { + "items": { + "$ref": "#/components/schemas/AllowedModel" + }, + "type": "array", + "title": "Allowed models" + }, + "model_profiles_path": { + "anyOf": [ + { + "type": "string", + "format": "file-path" + }, + { + "type": "null" + } + ], + "title": "Model profiles path" + }, + "api_key_env": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "API key environment variable", + "description": "Deprecated: prefer config.api_key with ${env.VAR}." + }, + "extra": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Extra provider config", + "description": "Deprecated escape hatch for unmodeled provider knobs." + }, + "type": { + "type": "string", + "const": "azure", + "title": "Provider type", + "description": "Azure OpenAI remote inference provider.", + "default": "azure" + }, + "config": { + "$ref": "#/components/schemas/AzureInferenceProviderConfig", + "title": "Connection settings", + "description": "Endpoint, API key or Entra ID, and API version." + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "id" + ], + "title": "AzureInferenceProviderEntry", + "description": "Azure OpenAI inference provider." + }, "BadRequestResponse": { "properties": { "status_code": { @@ -12795,6 +12971,126 @@ } ] }, + "BedrockInferenceProviderConfig": { + "properties": { + "region": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "AWS region" + }, + "profile_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "AWS profile" + }, + "api_key": { + "anyOf": [ + { + "type": "string", + "format": "password", + "writeOnly": true + }, + { + "type": "null" + } + ], + "title": "Bearer token", + "description": "Optional Bedrock bearer token (alternative to SigV4)." + } + }, + "additionalProperties": false, + "type": "object", + "title": "BedrockInferenceProviderConfig", + "description": "Connection settings for an AWS Bedrock inference provider." + }, + "BedrockInferenceProviderEntry": { + "properties": { + "id": { + "type": "string", + "title": "Provider ID" + }, + "skip_model_discovery": { + "type": "boolean", + "title": "Skip model discovery", + "default": false + }, + "allowed_models": { + "items": { + "$ref": "#/components/schemas/AllowedModel" + }, + "type": "array", + "title": "Allowed models" + }, + "model_profiles_path": { + "anyOf": [ + { + "type": "string", + "format": "file-path" + }, + { + "type": "null" + } + ], + "title": "Model profiles path" + }, + "api_key_env": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "API key environment variable", + "description": "Deprecated: prefer config.api_key with ${env.VAR}." + }, + "extra": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Extra provider config", + "description": "Deprecated escape hatch for unmodeled provider knobs." + }, + "type": { + "type": "string", + "const": "bedrock", + "title": "Provider type", + "description": "AWS Bedrock remote inference provider.", + "default": "bedrock" + }, + "config": { + "$ref": "#/components/schemas/BedrockInferenceProviderConfig", + "title": "Connection settings", + "description": "AWS region, optional profile, and optional bearer token." + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "id" + ], + "title": "BedrockInferenceProviderEntry", + "description": "AWS Bedrock inference provider." + }, "Body_create_file_v1_files_post": { "properties": { "file": { @@ -14974,11 +15270,45 @@ }, "providers": { "items": { - "$ref": "#/components/schemas/UnifiedInferenceProvider" + "oneOf": [ + { + "$ref": "#/components/schemas/OpenAIInferenceProviderEntry" + }, + { + "$ref": "#/components/schemas/AzureInferenceProviderEntry" + }, + { + "$ref": "#/components/schemas/VertexAIInferenceProviderEntry" + }, + { + "$ref": "#/components/schemas/BedrockInferenceProviderEntry" + }, + { + "$ref": "#/components/schemas/VllmInferenceProviderEntry" + }, + { + "$ref": "#/components/schemas/WatsonxInferenceProviderEntry" + }, + { + "$ref": "#/components/schemas/SentenceTransformersInferenceProviderEntry" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "azure": "#/components/schemas/AzureInferenceProviderEntry", + "bedrock": "#/components/schemas/BedrockInferenceProviderEntry", + "openai": "#/components/schemas/OpenAIInferenceProviderEntry", + "sentence_transformers": "#/components/schemas/SentenceTransformersInferenceProviderEntry", + "vertexai": "#/components/schemas/VertexAIInferenceProviderEntry", + "vllm": "#/components/schemas/VllmInferenceProviderEntry", + "watsonx": "#/components/schemas/WatsonxInferenceProviderEntry" + } + } }, "type": "array", - "title": "High-level inference providers", - "description": "Unified-mode synthesis input (Decision S5): a high-level, backend-agnostic list of inference providers the synthesizer expands into OGX provider entries. Lives at the configuration root so it survives a future backend change. A non-empty list signals unified mode. Empty (the default) leaves legacy/remote modes unaffected. The sibling default_model / default_provider keep their query-time routing meaning and are independent of this list." + "title": "Inference providers", + "description": "Discriminated list of inference providers. Empty by default. The sibling default_model / default_provider keep their query-time routing meaning and are independent of this list." }, "max_infer_iters": { "anyOf": [ @@ -16309,14 +16639,122 @@ "title": "OkpConfiguration", "description": "OKP (Offline Knowledge Portal) provider configuration.\n\nControls provider-specific behaviour for the OKP vector store.\nOnly relevant when ``\"okp\"`` is listed in ``rag.retrieval.inline.sources``\nor ``rag.retrieval.tool.sources``." }, - "OpenAIResponseAnnotationCitation": { + "OpenAIInferenceProviderConfig": { "properties": { - "type": { - "type": "string", - "const": "url_citation", - "title": "Type", - "default": "url_citation" - }, + "api_key": { + "anyOf": [ + { + "type": "string", + "format": "password", + "writeOnly": true + }, + { + "type": "null" + } + ], + "title": "API key" + }, + "base_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Base URL" + } + }, + "additionalProperties": false, + "type": "object", + "title": "OpenAIInferenceProviderConfig", + "description": "Connection settings for an OpenAI inference provider." + }, + "OpenAIInferenceProviderEntry": { + "properties": { + "id": { + "type": "string", + "title": "Provider ID" + }, + "skip_model_discovery": { + "type": "boolean", + "title": "Skip model discovery", + "default": false + }, + "allowed_models": { + "items": { + "$ref": "#/components/schemas/AllowedModel" + }, + "type": "array", + "title": "Allowed models" + }, + "model_profiles_path": { + "anyOf": [ + { + "type": "string", + "format": "file-path" + }, + { + "type": "null" + } + ], + "title": "Model profiles path" + }, + "api_key_env": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "API key environment variable", + "description": "Deprecated: prefer config.api_key with ${env.VAR}." + }, + "extra": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Extra provider config", + "description": "Deprecated escape hatch for unmodeled provider knobs." + }, + "type": { + "type": "string", + "const": "openai", + "title": "Provider type", + "description": "OpenAI remote inference provider.", + "default": "openai" + }, + "config": { + "$ref": "#/components/schemas/OpenAIInferenceProviderConfig", + "title": "Connection settings", + "description": "API key and optional base URL." + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "id" + ], + "title": "OpenAIInferenceProviderEntry", + "description": "OpenAI inference provider." + }, + "OpenAIResponseAnnotationCitation": { + "properties": { + "type": { + "type": "string", + "const": "url_citation", + "title": "Type", + "default": "url_citation" + }, "end_index": { "type": "integer", "title": "End Index" @@ -21781,6 +22219,112 @@ ], "title": "SecurityScheme" }, + "SentenceTransformersInferenceProviderConfig": { + "properties": { + "device": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Device" + }, + "cache_dir": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Hugging Face cache directory" + } + }, + "additionalProperties": false, + "type": "object", + "title": "SentenceTransformersInferenceProviderConfig", + "description": "Settings for the local sentence-transformers inference provider." + }, + "SentenceTransformersInferenceProviderEntry": { + "properties": { + "id": { + "type": "string", + "title": "Provider ID" + }, + "skip_model_discovery": { + "type": "boolean", + "title": "Skip model discovery", + "default": false + }, + "allowed_models": { + "items": { + "$ref": "#/components/schemas/AllowedModel" + }, + "type": "array", + "title": "Allowed models" + }, + "model_profiles_path": { + "anyOf": [ + { + "type": "string", + "format": "file-path" + }, + { + "type": "null" + } + ], + "title": "Model profiles path" + }, + "api_key_env": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "API key environment variable", + "description": "Deprecated: prefer config.api_key with ${env.VAR}." + }, + "extra": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Extra provider config", + "description": "Deprecated escape hatch for unmodeled provider knobs." + }, + "type": { + "type": "string", + "const": "sentence_transformers", + "title": "Provider type", + "description": "Local sentence-transformers embedding and rerank provider.", + "default": "sentence_transformers" + }, + "config": { + "$ref": "#/components/schemas/SentenceTransformersInferenceProviderConfig", + "title": "Connection settings", + "description": "Optional device and Hugging Face cache directory." + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "id" + ], + "title": "SentenceTransformersInferenceProviderEntry", + "description": "Local sentence-transformers inference provider." + }, "ServiceConfiguration": { "properties": { "host": { @@ -22615,78 +23159,6 @@ } ] }, - "UnifiedInferenceProvider": { - "properties": { - "type": { - "type": "string", - "enum": [ - "openai", - "ollama", - "vllm", - "sentence_transformers", - "azure", - "vertexai", - "watsonx", - "vllm_rhaiis", - "vllm_rhel_ai" - ], - "title": "Provider type", - "description": "Canonical, backend-agnostic provider identifier mapped to an OGX provider_type by the synthesizer." - }, - "id": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Provider ID", - "description": "Optional identifier emitted as the OGX provider_id. When omitted, synthesized as type with underscores hyphenated. If set, must be non-empty after stripping whitespace and may contain only lowercase letters, digits, underscores, and hyphens." - }, - "api_key_env": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "API key environment variable", - "description": "Name of the environment variable holding the provider API key. Emitted as a ${env.} reference so the secret is never written to disk in resolved form." - }, - "allowed_models": { - "anyOf": [ - { - "items": { - "type": "string" - }, - "type": "array" - }, - { - "type": "null" - } - ], - "title": "Allowed models", - "description": "Optional allow-list of model identifiers for this provider." - }, - "extra": { - "additionalProperties": true, - "type": "object", - "title": "Extra provider config", - "description": "Additional provider-config keys merged verbatim into the synthesized provider's config block." - } - }, - "additionalProperties": false, - "type": "object", - "required": [ - "type" - ], - "title": "UnifiedInferenceProvider", - "description": "A high-level inference provider entry for unified-mode synthesis.\n\nOperators describe inference providers at this high level (backend-agnostic\nvocabulary) instead of authoring raw OGX provider blocks. The\nsynthesizer (`apply_high_level_inference`) expands each entry into an OGX\n`providers.inference` entry, mapping `type` to a `provider_type` and\nemitting `${env.}` references for secrets (never literal values).\n\nAttributes:\n type: Canonical provider identifier. Vendor-neutral so it survives a\n future backend change; each backend-specific synthesizer maps it to\n its own provider vocabulary.\n id: Optional identifier emitted as the OGX provider_id. When\n omitted, synthesized as type with underscores hyphenated. If set,\n must be non-empty after stripping whitespace and may contain only\n lowercase letters, digits, underscores, and hyphens.\n api_key_env: Name of the environment variable holding the provider API\n key. Emitted verbatim as `${env.}` so the secret never lands\n on disk resolved.\n allowed_models: Optional allow-list of model identifiers passed through\n to the synthesized provider config.\n extra: Additional provider-config keys merged verbatim into the\n synthesized provider's `config` block \u2014 an escape hatch for\n provider-specific knobs not modeled here." - }, "UnifiedOgxConfig": { "properties": { "baseline": { @@ -23548,6 +24020,339 @@ } ] }, + "VertexAIInferenceProviderConfig": { + "properties": { + "project": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "GCP project" + }, + "location": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "GCP location" + } + }, + "additionalProperties": false, + "type": "object", + "title": "VertexAIInferenceProviderConfig", + "description": "Connection settings for a Vertex AI inference provider.\n\nAuthentication uses Google Application Default Credentials. Set\nGOOGLE_APPLICATION_CREDENTIALS in the environment when a credentials file\nis required; there is no credentials path field in config." + }, + "VertexAIInferenceProviderEntry": { + "properties": { + "id": { + "type": "string", + "title": "Provider ID" + }, + "skip_model_discovery": { + "type": "boolean", + "title": "Skip model discovery", + "default": false + }, + "allowed_models": { + "items": { + "$ref": "#/components/schemas/AllowedModel" + }, + "type": "array", + "title": "Allowed models" + }, + "model_profiles_path": { + "anyOf": [ + { + "type": "string", + "format": "file-path" + }, + { + "type": "null" + } + ], + "title": "Model profiles path" + }, + "api_key_env": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "API key environment variable", + "description": "Deprecated: prefer config.api_key with ${env.VAR}." + }, + "extra": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Extra provider config", + "description": "Deprecated escape hatch for unmodeled provider knobs." + }, + "type": { + "type": "string", + "const": "vertexai", + "title": "Provider type", + "description": "Google Vertex AI remote inference provider.", + "default": "vertexai" + }, + "config": { + "$ref": "#/components/schemas/VertexAIInferenceProviderConfig", + "title": "Connection settings", + "description": "GCP project and location." + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "id" + ], + "title": "VertexAIInferenceProviderEntry", + "description": "Vertex AI inference provider." + }, + "VllmInferenceProviderConfig": { + "properties": { + "base_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Base URL" + }, + "api_key": { + "anyOf": [ + { + "type": "string", + "format": "password", + "writeOnly": true + }, + { + "type": "null" + } + ], + "title": "API key" + } + }, + "additionalProperties": false, + "type": "object", + "title": "VllmInferenceProviderConfig", + "description": "Connection settings for a vLLM inference provider." + }, + "VllmInferenceProviderEntry": { + "properties": { + "id": { + "type": "string", + "title": "Provider ID" + }, + "skip_model_discovery": { + "type": "boolean", + "title": "Skip model discovery", + "default": false + }, + "allowed_models": { + "items": { + "$ref": "#/components/schemas/AllowedModel" + }, + "type": "array", + "title": "Allowed models" + }, + "model_profiles_path": { + "anyOf": [ + { + "type": "string", + "format": "file-path" + }, + { + "type": "null" + } + ], + "title": "Model profiles path" + }, + "api_key_env": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "API key environment variable", + "description": "Deprecated: prefer config.api_key with ${env.VAR}." + }, + "extra": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Extra provider config", + "description": "Deprecated escape hatch for unmodeled provider knobs." + }, + "type": { + "type": "string", + "const": "vllm", + "title": "Provider type", + "description": "vLLM OpenAI-compatible remote inference provider.", + "default": "vllm" + }, + "config": { + "$ref": "#/components/schemas/VllmInferenceProviderConfig", + "title": "Connection settings", + "description": "Base URL and optional API key." + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "id" + ], + "title": "VllmInferenceProviderEntry", + "description": "vLLM inference provider." + }, + "WatsonxInferenceProviderConfig": { + "properties": { + "api_key": { + "anyOf": [ + { + "type": "string", + "format": "password", + "writeOnly": true + }, + { + "type": "null" + } + ], + "title": "API key" + }, + "project_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Project ID" + }, + "base_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Base URL" + } + }, + "additionalProperties": false, + "type": "object", + "title": "WatsonxInferenceProviderConfig", + "description": "Connection settings for a WatsonX inference provider." + }, + "WatsonxInferenceProviderEntry": { + "properties": { + "id": { + "type": "string", + "title": "Provider ID" + }, + "skip_model_discovery": { + "type": "boolean", + "title": "Skip model discovery", + "default": false + }, + "allowed_models": { + "items": { + "$ref": "#/components/schemas/AllowedModel" + }, + "type": "array", + "title": "Allowed models" + }, + "model_profiles_path": { + "anyOf": [ + { + "type": "string", + "format": "file-path" + }, + { + "type": "null" + } + ], + "title": "Model profiles path" + }, + "api_key_env": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "API key environment variable", + "description": "Deprecated: prefer config.api_key with ${env.VAR}." + }, + "extra": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Extra provider config", + "description": "Deprecated escape hatch for unmodeled provider knobs." + }, + "type": { + "type": "string", + "const": "watsonx", + "title": "Provider type", + "description": "IBM WatsonX remote inference provider.", + "default": "watsonx" + }, + "config": { + "$ref": "#/components/schemas/WatsonxInferenceProviderConfig", + "title": "Connection settings", + "description": "API key, project ID, and base URL." + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "id" + ], + "title": "WatsonxInferenceProviderEntry", + "description": "WatsonX inference provider." + }, "WebSearchActionFind": { "properties": { "type": { diff --git a/docs/models/successful_responses.json b/docs/models/successful_responses.json index 749a3cbfe..ce0881660 100644 --- a/docs/models/successful_responses.json +++ b/docs/models/successful_responses.json @@ -1953,7 +1953,7 @@ "providers": { "description": "Unified-mode synthesis input (Decision S5): a high-level, backend-agnostic list of inference providers the synthesizer expands into OGX provider entries. Lives at the configuration root so it survives a future backend change. A non-empty list signals unified mode. Empty (the default) leaves legacy/remote modes unaffected. The sibling default_model / default_provider keep their query-time routing meaning and are independent of this list.", "items": { - "$ref": "`#/components/schemas/`UnifiedInferenceProvider" + "$ref": "`#/components/schemas/`UnifiedInferenceProviderEntry" }, "title": "High-level inference providers", "type": "array" @@ -6748,7 +6748,7 @@ "title": "TrustedProxyServiceAccount", "type": "object" }, - "UnifiedInferenceProvider": { + "UnifiedInferenceProviderEntry": { "additionalProperties": false, "description": "A high-level inference provider entry for unified-mode synthesis.\n\nOperators describe inference providers at this high level (backend-agnostic\nvocabulary) instead of authoring raw OGX provider blocks. The\nsynthesizer (`apply_high_level_inference`) expands each entry into an OGX\n`providers.inference` entry, mapping `type` to a `provider_type` and\nemitting `${env.}` references for secrets (never literal values).\n\nAttributes:\n type: Canonical provider identifier. Vendor-neutral so it survives a\n future backend change; each backend-specific synthesizer maps it to\n its own provider vocabulary.\n id: Optional identifier emitted as the OGX provider_id. When\n omitted, synthesized as type with underscores hyphenated. If set,\n must be non-empty after stripping whitespace and may contain only\n lowercase letters, digits, underscores, and hyphens.\n api_key_env: Name of the environment variable holding the provider API\n key. Emitted verbatim as `${env.}` so the secret never lands\n on disk resolved.\n allowed_models: Optional allow-list of model identifiers passed through\n to the synthesized provider config.\n extra: Additional provider-config keys merged verbatim into the\n synthesized provider's `config` block \u2014 an escape hatch for\n provider-specific knobs not modeled here.", "properties": { @@ -6799,7 +6799,7 @@ "required": [ "type" ], - "title": "UnifiedInferenceProvider", + "title": "UnifiedInferenceProviderEntry", "type": "object" }, "UnifiedOgxConfig": { diff --git a/docs/models/successful_responses.md b/docs/models/successful_responses.md index f205dfd1c..4870226ea 100644 --- a/docs/models/successful_responses.md +++ b/docs/models/successful_responses.md @@ -2904,7 +2904,7 @@ A Kubernetes ServiceAccount identity for trusted-proxy allowlist. | name | string | Name of the Kubernetes ServiceAccount. | -## UnifiedInferenceProvider +## UnifiedInferenceProviderEntry A high-level inference provider entry for unified-mode synthesis. diff --git a/docs/user_doc/config.json b/docs/user_doc/config.json index 819219774..147a97338 100644 --- a/docs/user_doc/config.json +++ b/docs/user_doc/config.json @@ -1025,7 +1025,7 @@ "providers": { "description": "Unified-mode synthesis input (Decision S5): a high-level, backend-agnostic list of inference providers the synthesizer expands into OGX provider entries. Lives at the configuration root so it survives a future backend change. A non-empty list signals unified mode. Empty (the default) leaves legacy/remote modes unaffected. The sibling default_model / default_provider keep their query-time routing meaning and are independent of this list.", "items": { - "$ref": "`#/components/schemas/`UnifiedInferenceProvider" + "$ref": "`#/components/schemas/`UnifiedInferenceProviderEntry" }, "title": "High-level inference providers", "type": "array" @@ -2391,7 +2391,7 @@ "title": "TrustedProxyServiceAccount", "type": "object" }, - "UnifiedInferenceProvider": { + "UnifiedInferenceProviderEntry": { "additionalProperties": false, "description": "A high-level inference provider entry for unified-mode synthesis.\n\nOperators describe inference providers at this high level (backend-agnostic\nvocabulary) instead of authoring raw OGX provider blocks. The\nsynthesizer (`apply_high_level_inference`) expands each entry into an OGX\n`providers.inference` entry, mapping `type` to a `provider_type` and\nemitting `${env.}` references for secrets (never literal values).\n\nAttributes:\n type: Canonical provider identifier. Vendor-neutral so it survives a\n future backend change; each backend-specific synthesizer maps it to\n its own provider vocabulary.\n id: Optional identifier emitted as the OGX provider_id. When\n omitted, synthesized as type with underscores hyphenated. If set,\n must be non-empty after stripping whitespace and may contain only\n lowercase letters, digits, underscores, and hyphens.\n api_key_env: Name of the environment variable holding the provider API\n key. Emitted verbatim as `${env.}` so the secret never lands\n on disk resolved.\n allowed_models: Optional allow-list of model identifiers passed through\n to the synthesized provider config.\n extra: Additional provider-config keys merged verbatim into the\n synthesized provider's `config` block — an escape hatch for\n provider-specific knobs not modeled here.", "properties": { @@ -2442,7 +2442,7 @@ "required": [ "type" ], - "title": "UnifiedInferenceProvider", + "title": "UnifiedInferenceProviderEntry", "type": "object" }, "UnifiedOgxConfig": { diff --git a/docs/user_doc/config.md b/docs/user_doc/config.md index 236828422..560144d0a 100644 --- a/docs/user_doc/config.md +++ b/docs/user_doc/config.md @@ -1134,7 +1134,7 @@ A Kubernetes ServiceAccount identity for trusted-proxy allowlist. | name | string | Name of the Kubernetes ServiceAccount. | -## UnifiedInferenceProvider +## UnifiedInferenceProviderEntry A high-level inference provider entry for unified-mode synthesis. diff --git a/src/models/config.py b/src/models/config.py index d59924edb..044f85408 100644 --- a/src/models/config.py +++ b/src/models/config.py @@ -702,112 +702,326 @@ def resolve_auth_headers(self) -> Self: return self -class UnifiedInferenceProvider(ConfigurationBase): - """A high-level inference provider entry for unified-mode synthesis. +class AllowedModel(ConfigurationBase): + """Configuration for an allowed inference model. - Operators describe inference providers at this high level (backend-agnostic - vocabulary) instead of authoring raw OGX provider blocks. The - synthesizer (`apply_high_level_inference`) expands each entry into an OGX - `providers.inference` entry, mapping `type` to a `provider_type` and - emitting `${env.}` references for secrets (never literal values). + Accepts string IDs or dictionaries. String IDs populate both `name` and + `provider_model_id`; dictionary inputs default `name` to `provider_model_id`. + """ - Attributes: - type: Canonical provider identifier. Vendor-neutral so it survives a - future backend change; each backend-specific synthesizer maps it to - its own provider vocabulary. - id: Optional identifier emitted as the OGX provider_id. When - omitted, synthesized as type with underscores hyphenated. If set, - must be non-empty after stripping whitespace and may contain only - lowercase letters, digits, underscores, and hyphens. - api_key_env: Name of the environment variable holding the provider API - key. Emitted verbatim as `${env.}` so the secret never lands - on disk resolved. - allowed_models: Optional allow-list of model identifiers passed through - to the synthesized provider config. - extra: Additional provider-config keys merged verbatim into the - synthesized provider's `config` block — an escape hatch for - provider-specific knobs not modeled here. + name: str = Field(..., min_length=1, title="Local model name") + provider_model_id: str = Field(..., min_length=1, title="Backend model id") + model_type: Optional[Literal["llm", "embedding", "rerank"]] = Field( + None, + title="Model type", + description="Optional capability override (llm, embedding, or rerank).", + ) + metadata: dict[str, Any] = Field( + default_factory=dict, + title="Model metadata", + description="Optional catalog metadata", + ) - """ + @model_validator(mode="before") + @classmethod + def normalize_input(cls, data: Any) -> Any: + """Normalize string and dictionary inputs.""" + if isinstance(data, str): + return {"name": data, "provider_model_id": data} + + if isinstance(data, dict): + normalized = dict(data) + provider_model_id = normalized.get("provider_model_id") + if not provider_model_id: + raise ValueError("provider_model_id must be provided and non-empty") + normalized.setdefault("name", provider_model_id) + return normalized - type: Literal[ - "openai", - "ollama", - "vllm", - "sentence_transformers", - "azure", - "vertexai", - "watsonx", - "vllm_rhaiis", - "vllm_rhel_ai", - ] = Field( - ..., - title="Provider type", - description="Canonical, backend-agnostic provider identifier mapped to " - "an OGX provider_type by the synthesizer.", + return data + + @field_validator("name", "provider_model_id") + @classmethod + def strip_nonempty(cls, value: str) -> str: + """Strip whitespace and reject empty identifiers.""" + stripped = value.strip() + if not stripped: + raise ValueError("must be non-empty after stripping whitespace") + return stripped + + @field_validator("metadata") + @classmethod + def validate_metadata(cls, value: dict[str, Any]) -> dict[str, Any]: + """Validate metadata values.""" + context_window = value.get("context_window") + if context_window is not None and ( + not isinstance(context_window, int) + or isinstance(context_window, bool) + or context_window <= 0 + ): + raise ValueError("metadata.context_window must be a positive int") + return value + + +class AzureEntraIdConfiguration(ConfigurationBase): + """Microsoft Entra ID authentication attributes for Azure.""" + + tenant_id: SecretStr = Field(..., title="Tenant ID") + client_id: SecretStr = Field(..., title="Client ID") + client_secret: SecretStr = Field(..., title="Client secret") + scope: str = Field( + "https://cognitiveservices.azure.com/.default", + title="Token scope", + description="Azure Cognitive Services scope for token requests. " + "Override only if using a different Azure service.", ) - id: Optional[str] = Field( - None, - title="Provider ID", - description="Optional identifier emitted as the OGX provider_id. When omitted, " - "synthesized as type with underscores hyphenated. If set, must " - "be non-empty after stripping whitespace and may contain only " - "lowercase letters, digits, underscores, and hyphens.", + +class OpenAIInferenceProviderConfig(ConfigurationBase): + """Connection settings for an OpenAI inference provider.""" + + api_key: Optional[SecretStr] = Field(default=None, title="API key") + base_url: Optional[str] = Field(default=None, title="Base URL") + + +class AzureInferenceProviderConfig(ConfigurationBase): + """Connection settings for an Azure OpenAI inference provider.""" + + base_url: Optional[str] = Field(default=None, title="Azure OpenAI endpoint") + api_key: Optional[SecretStr] = Field(default=None, title="API key") + api_version: Optional[str] = Field(default=None, title="API version") + entra_id: Optional[AzureEntraIdConfiguration] = Field( + default=None, + title="Entra ID", + description="Preferred Entra ID auth for this provider. Takes " + "precedence over the legacy top-level Azure Entra ID settings.", + ) + + +class VertexAIInferenceProviderConfig(ConfigurationBase): + """Connection settings for a Vertex AI inference provider. + + Authentication uses Google Application Default Credentials. Set + GOOGLE_APPLICATION_CREDENTIALS in the environment when a credentials file + is required; there is no credentials path field in config. + """ + + project: Optional[str] = Field(default=None, title="GCP project") + location: Optional[str] = Field(default=None, title="GCP location") + + +class BedrockInferenceProviderConfig(ConfigurationBase): + """Connection settings for an AWS Bedrock inference provider.""" + + region: Optional[str] = Field(default=None, title="AWS region") + profile_name: Optional[str] = Field(default=None, title="AWS profile") + api_key: Optional[SecretStr] = Field( + default=None, + title="Bearer token", + description="Optional Bedrock bearer token (alternative to SigV4).", ) + +class VllmInferenceProviderConfig(ConfigurationBase): + """Connection settings for a vLLM inference provider.""" + + base_url: Optional[str] = Field(default=None, title="Base URL") + api_key: Optional[SecretStr] = Field(default=None, title="API key") + + +class WatsonxInferenceProviderConfig(ConfigurationBase): + """Connection settings for a WatsonX inference provider.""" + + api_key: Optional[SecretStr] = Field(default=None, title="API key") + project_id: Optional[str] = Field(default=None, title="Project ID") + base_url: Optional[str] = Field(default=None, title="Base URL") + + +class SentenceTransformersInferenceProviderConfig(ConfigurationBase): + """Settings for the local sentence-transformers inference provider.""" + + device: Optional[str] = Field(default=None, title="Device") + cache_dir: Optional[str] = Field(default=None, title="Hugging Face cache directory") + + +class InferenceProviderEntryBase(ConfigurationBase): + """Shared fields for inference provider entries.""" + + id: str = Field(..., title="Provider ID") + skip_model_discovery: bool = Field(False, title="Skip model discovery") + allowed_models: list[AllowedModel] = Field( + default_factory=list, title="Allowed models" + ) + model_profiles_path: Optional[FilePath] = Field(None, title="Model profiles path") api_key_env: Optional[str] = Field( None, title="API key environment variable", - description="Name of the environment variable holding the provider API " - "key. Emitted as a ${env.} reference so the secret is never " - "written to disk in resolved form.", + description="Deprecated: prefer config.api_key with ${env.VAR}.", ) - - allowed_models: Optional[list[str]] = Field( + extra: Optional[dict[str, object]] = Field( None, - title="Allowed models", - description="Optional allow-list of model identifiers for this provider.", - ) - - extra: dict[str, object] = Field( - default_factory=dict, title="Extra provider config", - description="Additional provider-config keys merged verbatim into the " - "synthesized provider's config block.", + description="Deprecated escape hatch for unmodeled provider knobs.", ) + @model_validator(mode="before") + @classmethod + def default_id_from_type(cls, data: Any) -> Any: + """Set provider ID from type when omitted.""" + if isinstance(data, dict) and data.get("id") is None: + normalized = dict(data) + normalized["id"] = normalized.get("type", cls.model_fields["type"].default) + return normalized + return data + @field_validator("id") @classmethod - def validate_id(cls, value: Optional[str]) -> Optional[str]: - """Strip and validate an optional high-level provider id. + def validate_id(cls, value: str) -> str: + """Validate the provider ID.""" + value = value.strip() + if not value or not re.fullmatch(r"[a-z0-9_-]+", value): + raise ValueError( + "id must contain only lowercase letters, digits, " + "underscores, and hyphens" + ) + return value - Parameters: - value: The configured id, or None when omitted. + @model_validator(mode="after") + def validate_allowed_models(self) -> Self: + """Require unique names; nonempty allow-list when skipping discovery.""" + names = [model.name for model in self.allowed_models] + if len(names) != len(set(names)): + raise ValueError("allowed model names must be unique") + if self.skip_model_discovery and not self.allowed_models: + raise ValueError( + "allowed_models must be non-empty when skip_model_discovery is true" + ) + return self - Returns: - The stripped id, or None when the field is omitted. - Raises: - ValueError: If the value is empty after stripping or contains - characters other than lowercase letters, digits, underscores, - and hyphens. +class OpenAIInferenceProviderEntry(InferenceProviderEntryBase): + """OpenAI inference provider.""" - """ - if value is None: - return None - stripped = value.strip() - if not stripped: - raise ValueError( - "id must be non-empty after stripping whitespace; " - "omit the field to use the type-derived default" - ) - if not re.fullmatch(r"[a-z0-9_-]+", stripped): + type: Literal["openai"] = Field( + "openai", + title="Provider type", + description="OpenAI remote inference provider.", + ) + config: OpenAIInferenceProviderConfig = Field( + default_factory=OpenAIInferenceProviderConfig, + title="Connection settings", + description="API key and optional base URL.", + ) + + +class AzureInferenceProviderEntry(InferenceProviderEntryBase): + """Azure OpenAI inference provider.""" + + type: Literal["azure"] = Field( + "azure", + title="Provider type", + description="Azure OpenAI remote inference provider.", + ) + config: AzureInferenceProviderConfig = Field( + default_factory=AzureInferenceProviderConfig, + title="Connection settings", + description="Endpoint, API key or Entra ID, and API version.", + ) + + +class VertexAIInferenceProviderEntry(InferenceProviderEntryBase): + """Vertex AI inference provider.""" + + type: Literal["vertexai"] = Field( + "vertexai", + title="Provider type", + description="Google Vertex AI remote inference provider.", + ) + config: VertexAIInferenceProviderConfig = Field( + default_factory=VertexAIInferenceProviderConfig, + title="Connection settings", + description="GCP project and location.", + ) + + +class BedrockInferenceProviderEntry(InferenceProviderEntryBase): + """AWS Bedrock inference provider.""" + + type: Literal["bedrock"] = Field( + "bedrock", + title="Provider type", + description="AWS Bedrock remote inference provider.", + ) + config: BedrockInferenceProviderConfig = Field( + default_factory=BedrockInferenceProviderConfig, + title="Connection settings", + description="AWS region, optional profile, and optional bearer token.", + ) + + +class VllmInferenceProviderEntry(InferenceProviderEntryBase): + """vLLM inference provider.""" + + type: Literal["vllm"] = Field( + "vllm", + title="Provider type", + description="vLLM OpenAI-compatible remote inference provider.", + ) + config: VllmInferenceProviderConfig = Field( + default_factory=VllmInferenceProviderConfig, + title="Connection settings", + description="Base URL and optional API key.", + ) + + +class WatsonxInferenceProviderEntry(InferenceProviderEntryBase): + """WatsonX inference provider.""" + + type: Literal["watsonx"] = Field( + "watsonx", + title="Provider type", + description="IBM WatsonX remote inference provider.", + ) + config: WatsonxInferenceProviderConfig = Field( + default_factory=WatsonxInferenceProviderConfig, + title="Connection settings", + description="API key, project ID, and base URL.", + ) + + +class SentenceTransformersInferenceProviderEntry(InferenceProviderEntryBase): + """Local sentence-transformers inference provider.""" + + type: Literal["sentence_transformers"] = Field( + "sentence_transformers", + title="Provider type", + description="Local sentence-transformers embedding and rerank provider.", + ) + config: SentenceTransformersInferenceProviderConfig = Field( + default_factory=SentenceTransformersInferenceProviderConfig, + title="Connection settings", + description="Optional device and Hugging Face cache directory.", + ) + + @model_validator(mode="after") + def require_allowed_models(self) -> Self: + """Require explicitly configured models.""" + if not self.allowed_models: raise ValueError( - "id may contain only lowercase letters, digits, " - "underscores, and hyphens" + "allowed_models must be non-empty for sentence_transformers" ) - return stripped + return self + + +UnifiedInferenceProviderEntry = Annotated[ + OpenAIInferenceProviderEntry + | AzureInferenceProviderEntry + | VertexAIInferenceProviderEntry + | BedrockInferenceProviderEntry + | VllmInferenceProviderEntry + | WatsonxInferenceProviderEntry + | SentenceTransformersInferenceProviderEntry, + Field(discriminator="type"), +] class UnifiedOgxConfig(ConfigurationBase): @@ -1846,18 +2060,13 @@ class InferenceConfiguration(ConfigurationBase): "back to their own default or skip the token-based trigger.", ) - providers: list[UnifiedInferenceProvider] = Field( + providers: list[UnifiedInferenceProviderEntry] = Field( default_factory=list, - title="High-level inference providers", + title="Inference providers", description=( - "Unified-mode synthesis input (Decision S5): a high-level, backend-agnostic " - "list of inference providers the synthesizer expands into " - "OGX provider entries. Lives at the configuration root " - "so it survives a future backend change. A " - "non-empty list signals unified mode. Empty (the default) " - "leaves legacy/remote modes unaffected. The sibling default_model / " - "default_provider keep their query-time routing meaning and are " - "independent of this list." + "Discriminated list of inference providers. Empty by default. " + "The sibling default_model / default_provider keep their " + "query-time routing meaning and are independent of this list." ), ) @@ -1903,35 +2112,22 @@ def check_default_model_and_provider(self) -> Self: ) return self - @model_validator(mode="after") - def check_unique_provider_ids(self) -> Self: - """Reject two high-level providers that resolve to the same provider_id. - - The synthesizer emits each provider under its explicit ``id`` when set, - otherwise the ``type`` with underscores hyphenated. Two entries - resolving to the same emitted id would collide in the synthesized - ``providers.inference`` list (the later one silently overwriting the - earlier), so reject the ambiguity here rather than resolving it as - last-wins at synthesis time. - - Raises: - ValueError: If two providers resolve to the same emitted id. - - Returns: - self (Self): The validated configuration instance. - - """ + @field_validator("providers") + @classmethod + def validate_unique_provider_ids( + cls, providers: list[UnifiedInferenceProviderEntry] + ) -> list[UnifiedInferenceProviderEntry]: + """Reject two providers that share the same local provider id.""" seen: set[str] = set() - for provider in self.providers: - emitted_id = provider.id or provider.type.replace("_", "-") - if emitted_id in seen: + for provider in providers: + if provider.id in seen: raise ValueError( - f"duplicate inference provider id {emitted_id!r}: two " + f"duplicate inference provider id {provider.id!r}: two " "inference.providers entries resolve to the same " "provider_id; set a distinct 'id' on one of them" ) - seen.add(emitted_id) - return self + seen.add(provider.id) + return providers class CompactionConfiguration(ConfigurationBase): @@ -2891,20 +3087,6 @@ def validate_reranker_auto_enable(self) -> Self: return self -class AzureEntraIdConfiguration(ConfigurationBase): - """Microsoft Entra ID authentication attributes for Azure.""" - - tenant_id: SecretStr - client_id: SecretStr - client_secret: SecretStr - scope: str = Field( - "https://cognitiveservices.azure.com/.default", - title="Token scope", - description="Azure Cognitive Services scope for token requests. " - "Override only if using a different Azure service.", - ) - - class SkillsConfiguration(ConfigurationBase): """Agent skills configuration. diff --git a/src/telemetry/configuration_snapshot.py b/src/telemetry/configuration_snapshot.py index 1940e57bf..2e0e9075b 100644 --- a/src/telemetry/configuration_snapshot.py +++ b/src/telemetry/configuration_snapshot.py @@ -125,7 +125,9 @@ class ListFieldSpec: FieldSpec("type", MaskingType.PASSTHROUGH), FieldSpec("id", MaskingType.PASSTHROUGH), FieldSpec("api_key_env", MaskingType.SENSITIVE), + FieldSpec("skip_model_discovery", MaskingType.PASSTHROUGH), FieldSpec("allowed_models", MaskingType.PASSTHROUGH), + FieldSpec("config", MaskingType.SENSITIVE), ), ), # Authentication & Authorization diff --git a/tests/unit/models/config/test_inference_configuration.py b/tests/unit/models/config/test_inference_configuration.py index 0fcabed96..f6cb6cfca 100644 --- a/tests/unit/models/config/test_inference_configuration.py +++ b/tests/unit/models/config/test_inference_configuration.py @@ -3,7 +3,7 @@ import pytest from pydantic import ValidationError -from models.config import InferenceConfiguration, UnifiedInferenceProvider +from models.config import InferenceConfiguration, VllmInferenceProviderEntry def test_inference_constructor() -> None: @@ -161,25 +161,9 @@ def test_max_tool_calls_accepts_none() -> None: assert config.max_tool_calls is None -def test_unified_inference_provider_id_optional() -> None: - """Omitting id leaves it None (type-derived default at synthesis).""" - provider = UnifiedInferenceProvider(type="vllm") - assert provider.id is None - - -@pytest.mark.parametrize( - "provider_id", - ["vllm-prod", "vllm_staging", "openai", "a", "vllm-prod-2"], -) -def test_unified_inference_provider_id_accepts_valid(provider_id: str) -> None: - """Accept lowercase letters, digits, underscores, and hyphens.""" - provider = UnifiedInferenceProvider(type="vllm", id=provider_id) - assert provider.id == provider_id - - -def test_unified_inference_provider_id_strips_whitespace() -> None: +def test_inference_provider_id_strips_whitespace() -> None: """Leading and trailing whitespace are stripped from id.""" - provider = UnifiedInferenceProvider(type="vllm", id=" vllm-prod ") + provider = VllmInferenceProviderEntry(type="vllm", id=" vllm-prod ") assert provider.id == "vllm-prod" @@ -187,7 +171,7 @@ def test_unified_inference_provider_id_strips_whitespace() -> None: "provider_id", ["", " ", "VLLM", "vllm prod", "vllm.prod", "vllm/prod", "Vllm-prod"], ) -def test_unified_inference_provider_id_rejects_invalid(provider_id: str) -> None: +def test_inference_provider_id_rejects_invalid(provider_id: str) -> None: """Reject empty, whitespace-only, uppercase, spaces, and other punctuation.""" with pytest.raises(ValidationError): - UnifiedInferenceProvider(type="vllm", id=provider_id) + VllmInferenceProviderEntry(type="vllm", id=provider_id) diff --git a/tests/unit/models/config/test_inference_provider_entries.py b/tests/unit/models/config/test_inference_provider_entries.py new file mode 100644 index 000000000..9dd256061 --- /dev/null +++ b/tests/unit/models/config/test_inference_provider_entries.py @@ -0,0 +1,168 @@ +"""Unit tests for discriminated inference.providers[] config entries.""" + +import pytest +from pydantic import TypeAdapter, ValidationError + +from models.config import ( + AllowedModel, + BedrockInferenceProviderEntry, + InferenceConfiguration, + OpenAIInferenceProviderEntry, + SentenceTransformersInferenceProviderEntry, + UnifiedInferenceProviderEntry, + VllmInferenceProviderEntry, +) + +PROVIDER_ADAPTER = TypeAdapter(UnifiedInferenceProviderEntry) + + +@pytest.mark.parametrize( + "provider_type", + [ + "openai", + "azure", + "vertexai", + "bedrock", + "watsonx", + "vllm", + "sentence_transformers", + ], +) +def test_all_seven_provider_types_parse(provider_type: str) -> None: + """Each supported provider type parses into the discriminated union.""" + payload: dict[str, object] = {"type": provider_type} + if provider_type == "sentence_transformers": + payload["allowed_models"] = ["all-MiniLM-L6-v2"] + provider = PROVIDER_ADAPTER.validate_python(payload) + assert provider.type == provider_type + assert provider.id == provider_type + + +def test_allowed_models_string_form_normalizes() -> None: + """Bare string allow-list entries set name and provider_model_id equal.""" + model = AllowedModel.model_validate("gpt-4o-mini") + assert model.name == "gpt-4o-mini" + assert model.provider_model_id == "gpt-4o-mini" + assert model.model_type is None + assert model.metadata == {} + + +def test_allowed_models_structured_form() -> None: + """Structured allow-list entries keep local name, wire id, type, metadata.""" + model = AllowedModel.model_validate( + { + "name": "fast-chat", + "provider_model_id": "gpt-4o-mini", + "model_type": "llm", + "metadata": {"context_window": 128000}, + } + ) + assert model.name == "fast-chat" + assert model.provider_model_id == "gpt-4o-mini" + assert model.model_type == "llm" + assert model.metadata["context_window"] == 128000 + + +def test_allowed_models_defaults_name_to_provider_model_id() -> None: + """Omitted name defaults to provider_model_id.""" + model = AllowedModel.model_validate( + {"provider_model_id": "openai/gpt-4o-mini", "model_type": "llm"} + ) + assert model.name == "openai/gpt-4o-mini" + assert model.provider_model_id == "openai/gpt-4o-mini" + + +def test_omitted_id_defaults_to_type() -> None: + """Provider id defaults to type when omitted.""" + provider = OpenAIInferenceProviderEntry.model_validate({"type": "openai"}) + assert provider.id == "openai" + + +def test_explicit_id_is_kept() -> None: + """Explicit provider id is preserved.""" + provider = VllmInferenceProviderEntry.model_validate( + {"type": "vllm", "id": "vllm-prod"} + ) + assert provider.id == "vllm-prod" + + +def test_duplicate_provider_ids_fail() -> None: + """Two providers resolving to the same id fail validation.""" + with pytest.raises(ValidationError, match="duplicate inference provider id"): + InferenceConfiguration( + providers=[ + {"type": "vllm"}, + {"type": "vllm"}, + ] + ) + + +def test_duplicate_local_model_names_within_provider_fail() -> None: + """Duplicate local names in one provider's allow-list fail validation.""" + with pytest.raises(ValidationError, match="allowed model names must be unique"): + OpenAIInferenceProviderEntry.model_validate( + { + "type": "openai", + "allowed_models": [ + "same", + {"name": "same", "provider_model_id": "other"}, + ], + } + ) + + +def test_same_local_model_name_across_providers_allowed() -> None: + """The same local model name may appear on different providers.""" + config = InferenceConfiguration( + providers=[ + {"type": "openai", "allowed_models": ["shared"]}, + {"type": "vllm", "id": "vllm-prod", "allowed_models": ["shared"]}, + ] + ) + assert config.providers[0].allowed_models is not None + assert config.providers[1].allowed_models is not None + assert config.providers[0].allowed_models[0].name == "shared" + assert config.providers[1].allowed_models[0].name == "shared" + + +def test_skip_model_discovery_requires_allowed_models() -> None: + """skip_model_discovery: true requires a non-empty allow-list.""" + with pytest.raises(ValidationError, match="skip_model_discovery"): + OpenAIInferenceProviderEntry.model_validate( + {"type": "openai", "skip_model_discovery": True} + ) + + +def test_skip_model_discovery_with_allowed_models() -> None: + """skip_model_discovery succeeds when allowed_models is non-empty.""" + provider = OpenAIInferenceProviderEntry.model_validate( + { + "type": "openai", + "skip_model_discovery": True, + "allowed_models": ["gpt-4o-mini"], + } + ) + assert provider.skip_model_discovery is True + + +def test_sentence_transformers_requires_allowed_models() -> None: + """Sentence-transformers rejects an empty/missing allow-list.""" + with pytest.raises( + ValidationError, + match="allowed_models must be non-empty for sentence_transformers", + ): + SentenceTransformersInferenceProviderEntry.model_validate( + {"type": "sentence_transformers"} + ) + + +def test_nested_config_parses() -> None: + """Typed nested config blocks parse for a remote provider.""" + provider = BedrockInferenceProviderEntry.model_validate( + { + "type": "bedrock", + "config": {"region": "${env.AWS_REGION}", "profile_name": "default"}, + } + ) + assert provider.config.region == "${env.AWS_REGION}" + assert provider.config.profile_name == "default" diff --git a/tests/unit/telemetry/conftest.py b/tests/unit/telemetry/conftest.py index 5d9244fde..144664468 100644 --- a/tests/unit/telemetry/conftest.py +++ b/tests/unit/telemetry/conftest.py @@ -35,6 +35,8 @@ ModelContextProtocolServer, OgxConfiguration, OkpConfiguration, + OpenAIInferenceProviderConfig, + OpenAIInferenceProviderEntry, PgvectorVectorStoreProvider, PgvectorVectorStoreProviderConfig, PostgreSQLDatabaseConfiguration, @@ -58,7 +60,6 @@ TLSConfiguration, TrustedProxyConfiguration, TrustedProxyServiceAccount, - UnifiedInferenceProvider, UnifiedOgxConfig, UserDataCollection, VectorStoreConfiguration, @@ -359,12 +360,17 @@ def build_fully_populated_config() -> Configuration: max_infer_iters=10, max_tool_calls=30, providers=[ - UnifiedInferenceProvider.model_construct( + OpenAIInferenceProviderEntry.model_construct( type="openai", id="openai-provider", api_key_env=PII_PROVIDER_API_KEY_ENV, allowed_models=["gpt-4o-mini", "gpt-4o"], extra={}, + skip_model_discovery=False, + config=OpenAIInferenceProviderConfig.model_construct( + api_key=SecretStr("sk-openai-secret-key"), + base_url="https://api.openai.com/v1", + ), ), ], ), diff --git a/tests/unit/telemetry/test_configuration_snapshot.py b/tests/unit/telemetry/test_configuration_snapshot.py index 0912a34c3..881708c19 100644 --- a/tests/unit/telemetry/test_configuration_snapshot.py +++ b/tests/unit/telemetry/test_configuration_snapshot.py @@ -622,6 +622,8 @@ def test_inference_providers_extraction(self) -> None: assert providers[0]["id"] == "openai-provider" assert providers[0]["api_key_env"] == CONFIGURED assert providers[0]["allowed_models"] == ["gpt-4o-mini", "gpt-4o"] + assert providers[0]["skip_model_discovery"] is False + assert providers[0]["config"] == CONFIGURED def test_inference_providers_empty(self) -> None: """Test inference providers when empty.""" diff --git a/tests/unit/test_ogx_synthesize.py b/tests/unit/test_ogx_synthesize.py index 712188e0e..884b06c15 100644 --- a/tests/unit/test_ogx_synthesize.py +++ b/tests/unit/test_ogx_synthesize.py @@ -13,17 +13,16 @@ import stat import sys from pathlib import Path -from typing import Any, Optional, get_args +from typing import Any, Optional import pytest import yaml from ogx.core.stack import replace_env_vars from pydantic import ValidationError -from models.config import InferenceConfiguration, UnifiedInferenceProvider +from models.config import InferenceConfiguration, VllmInferenceProviderEntry from ogx_configuration import ( CONDITIONAL_OPENAI_PROVIDER_ID, - PROVIDER_TYPE_MAP, apply_high_level_inference, deep_merge_list_replace, ensure_mcp_tool_runtime, @@ -531,12 +530,11 @@ def test_apply_high_level_inference_extra_cannot_override_api_key_env() -> None: assert entry["config"]["api_token"] == "${env.VLLM_API_KEY}" -def test_unified_inference_provider_accepts_ollama_and_vllm() -> None: - """Pydantic model accepts the new ollama and vllm Literal values.""" - ollama = UnifiedInferenceProvider(type="ollama") - assert ollama.type == "ollama" - vllm = UnifiedInferenceProvider(type="vllm") +def test_inference_provider_accepts_vllm() -> None: + """Pydantic model accepts the vllm provider type.""" + vllm = VllmInferenceProviderEntry(type="vllm") assert vllm.type == "vllm" + assert vllm.id == "vllm" def test_apply_high_level_inference_empty_is_noop() -> None: @@ -713,18 +711,6 @@ def test_inference_config_allows_distinct_provider_ids() -> None: assert len(config.providers) == 2 -def test_provider_type_map_covers_every_literal_value() -> None: - """Every UnifiedInferenceProvider.type value has a PROVIDER_TYPE_MAP entry.""" - literal_values = set( - get_args( - UnifiedInferenceProvider.model_fields[ # pylint: disable=unsubscriptable-object - "type" - ].annotation - ) - ) - assert literal_values == set(PROVIDER_TYPE_MAP) - - # --------------------------------------------------------------------------- # synthesize_configuration # --------------------------------------------------------------------------- diff --git a/tests/unit/utils/dumpers/test_models_dumper.py b/tests/unit/utils/dumpers/test_models_dumper.py index 8cf74b772..8389ad0ac 100644 --- a/tests/unit/utils/dumpers/test_models_dumper.py +++ b/tests/unit/utils/dumpers/test_models_dumper.py @@ -2480,7 +2480,7 @@ def test_dump_models(tmpdir: Path) -> None: "providers": { "description": "Unified-mode synthesis input (Decision S5): a high-level, backend-agnostic list of inference providers the synthesizer expands into OGX provider entries. Lives at the configuration root so it survives a future backend change. A non-empty list signals unified mode. Empty (the default) leaves legacy/remote modes unaffected. The sibling default_model / default_provider keep their query-time routing meaning and are independent of this list.", "items": { - "$ref": "`#/components/schemas/`UnifiedInferenceProvider" + "$ref": "`#/components/schemas/`UnifiedInferenceProviderEntry" }, "title": "High-level inference providers", "type": "array" @@ -9228,7 +9228,7 @@ def test_dump_models(tmpdir: Path) -> None: "title": "UnauthorizedResponse", "type": "object" }, - "UnifiedInferenceProvider": { + "UnifiedInferenceProviderEntry": { "additionalProperties": false, "description": "A high-level inference provider entry for unified-mode synthesis.\n\nOperators describe inference providers at this high level (backend-agnostic\nvocabulary) instead of authoring raw OGX provider blocks. The\nsynthesizer (`apply_high_level_inference`) expands each entry into a Llama\nStack `providers.inference` entry, mapping `type` to a `provider_type` and\nemitting `${env.}` references for secrets (never literal values).\n\nAttributes:\n type: Canonical provider identifier. Vendor-neutral so it survives a\n future backend change; each backend-specific synthesizer maps it to\n its own provider vocabulary.\n id: Optional identifier emitted as the OGX provider_id. When\n omitted, synthesized as type with underscores hyphenated. If set,\n must be non-empty after stripping whitespace and may contain only\n lowercase letters, digits, underscores, and hyphens.\n api_key_env: Name of the environment variable holding the provider API\n key. Emitted verbatim as `${env.}` so the secret never lands\n on disk resolved.\n allowed_models: Optional allow-list of model identifiers passed through\n to the synthesized provider config.\n extra: Additional provider-config keys merged verbatim into the\n synthesized provider's `config` block \u2014 an escape hatch for\n provider-specific knobs not modeled here.", "properties": { @@ -9279,7 +9279,7 @@ def test_dump_models(tmpdir: Path) -> None: "required": [ "type" ], - "title": "UnifiedInferenceProvider", + "title": "UnifiedInferenceProviderEntry", "type": "object" }, "UnifiedOgxConfig": { @@ -10259,7 +10259,21 @@ def test_dump_models(tmpdir: Path) -> None: "TurnCompleteStreamPayload", "TurnSummary", "UnauthorizedResponse", - "UnifiedInferenceProvider", + "AllowedModel", + "AzureInferenceProviderEntry", + "AzureInferenceProviderConfig", + "BedrockInferenceProviderEntry", + "BedrockInferenceProviderConfig", + "OpenAIInferenceProviderEntry", + "OpenAIInferenceProviderConfig", + "SentenceTransformersInferenceProviderEntry", + "SentenceTransformersInferenceProviderConfig", + "VertexAIInferenceProviderEntry", + "VertexAIInferenceProviderConfig", + "VllmInferenceProviderEntry", + "VllmInferenceProviderConfig", + "WatsonxInferenceProviderEntry", + "WatsonxInferenceProviderConfig", "UnifiedOgxConfig", "UnprocessableEntityResponse", "UserDataCollection",