diff --git a/docs/devel_doc/openapi.json b/docs/devel_doc/openapi.json index f66ea5cbd..252dc5926 100644 --- a/docs/devel_doc/openapi.json +++ b/docs/devel_doc/openapi.json @@ -12717,12 +12717,15 @@ { "type": "string" }, + { + "type": "integer" + }, { "type": "null" } ], "title": "PostgreSQL port", - "description": "PostgreSQL port for remote::pgvector. Defaults to ${env.POSTGRES_PORT} when rag_type is remote::pgvector." + "description": "PostgreSQL port for remote::pgvector. Defaults to ${env.POSTGRES_PORT} when rag_type is remote::pgvector. Accepts string placeholders and integer values." }, "db": { "anyOf": [ @@ -17519,12 +17522,15 @@ { "type": "string" }, + { + "type": "integer" + }, { "type": "null" } ], "title": "PostgreSQL port", - "description": "PostgreSQL port. Defaults to ${env.POSTGRES_PORT}." + "description": "PostgreSQL port. Defaults to ${env.POSTGRES_PORT}. Accepts string placeholders and integer values." }, "db": { "anyOf": [ diff --git a/src/models/config.py b/src/models/config.py index 0449a233d..8b27f9218 100644 --- a/src/models/config.py +++ b/src/models/config.py @@ -2085,11 +2085,12 @@ class ByokRag(ConfigurationBase): "Defaults to ${env.POSTGRES_HOST} when rag_type is remote::pgvector.", ) - port: Optional[str] = Field( + port: Optional[str | int] = Field( default=None, title="PostgreSQL port", description="PostgreSQL port for remote::pgvector. " - "Defaults to ${env.POSTGRES_PORT} when rag_type is remote::pgvector.", + "Defaults to ${env.POSTGRES_PORT} when rag_type is remote::pgvector. " + "Accepts string placeholders and integer values.", ) db: Optional[str] = Field( @@ -2153,10 +2154,11 @@ class PgvectorVectorStoreProviderConfig(ConfigurationBase): description="PostgreSQL host. Defaults to ${env.POSTGRES_HOST}.", ) - port: Optional[str] = Field( + port: Optional[str | int] = Field( default=None, title="PostgreSQL port", - description="PostgreSQL port. Defaults to ${env.POSTGRES_PORT}.", + description="PostgreSQL port. Defaults to ${env.POSTGRES_PORT}. " + "Accepts string placeholders and integer values.", ) db: Optional[str] = Field( diff --git a/tests/unit/models/config/test_byok_rag.py b/tests/unit/models/config/test_byok_rag.py index 1ac098299..6bddcb96d 100644 --- a/tests/unit/models/config/test_byok_rag.py +++ b/tests/unit/models/config/test_byok_rag.py @@ -247,6 +247,21 @@ def test_byok_rag_pgvector_custom_connection_fields() -> None: assert store.password.get_secret_value() == "secret" # pylint: disable=no-member +def test_byok_rag_pgvector_accepts_int_port() -> None: + """Int port (from replace_env_vars coercion) must validate for pgvector.""" + store = ByokRag( + rag_id="pg_store", + rag_type="remote::pgvector", + vector_db_id="vs_pg", + host="db.example.com", + port=5432, + db="my_knowledge", + user="admin", + password="secret", + ) + assert store.port == 5432 + + def test_byok_rag_pgvector_partial_overrides() -> None: """Test pgvector fills only missing connection fields with defaults.""" store = ByokRag( diff --git a/tests/unit/models/config/test_vector_store.py b/tests/unit/models/config/test_vector_store.py index ad3c13fba..a5d4917f9 100644 --- a/tests/unit/models/config/test_vector_store.py +++ b/tests/unit/models/config/test_vector_store.py @@ -5,6 +5,7 @@ import pytest import yaml +from llama_stack.core.stack import replace_env_vars from pydantic import SecretStr, TypeAdapter, ValidationError from models.config import Configuration, VectorStoreProvider @@ -93,6 +94,72 @@ def test_pgvector_applies_env_defaults() -> None: assert provider.config.password == SecretStr("${env.POSTGRES_PASSWORD}") +def test_pgvector_accepts_int_port_from_env_substitution( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Int port after replace_env_vars type coercion must validate. + + Llama Stack's replace_env_vars converts digit-only env values to int via + _convert_string_to_proper_type. LCORE loads config through that helper, so + port must accept int as well as str / ${env.*} placeholders. + + Parameters: + monkeypatch: Fixture that sets and restores environment variables. + """ + monkeypatch.setenv("PGVECTOR_PORT", "5432") + resolved = replace_env_vars({"port": "${env.PGVECTOR_PORT:=5432}"}) + assert resolved["port"] == 5432 + assert isinstance(resolved["port"], int) + + provider = _PROVIDER_ADAPTER.validate_python( + { + "id": "nb-pg", + "type": "pgvector", + "embedding_model": "/emb", + "embedding_dimension": 768, + "config": { + "host": "db.example.com", + "port": resolved["port"], + "db": "vectors", + "user": "pguser", + "password": "secret", + }, + } + ) + assert provider.config.port == 5432 + + +def test_pgvector_accepts_int_port_from_env_default( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Unset env with :=default still coerces the default to int and validates. + + Parameters: + monkeypatch: Fixture that removes and restores environment variables. + """ + monkeypatch.delenv("PGVECTOR_PORT", raising=False) + resolved = replace_env_vars({"port": "${env.PGVECTOR_PORT:=5432}"}) + assert resolved["port"] == 5432 + assert isinstance(resolved["port"], int) + + provider = _PROVIDER_ADAPTER.validate_python( + { + "id": "nb-pg", + "type": "pgvector", + "embedding_model": "/emb", + "embedding_dimension": 768, + "config": { + "host": "db.example.com", + "port": resolved["port"], + "db": "vectors", + "user": "pguser", + "password": "secret", + }, + } + ) + assert provider.config.port == 5432 + + def test_rejects_byok_prefix_id() -> None: """Provider id must not use the byok_ prefix reserved for BYOK RAG.""" with pytest.raises(ValidationError, match="byok_"): diff --git a/tests/unit/utils/test_models_dumper.py b/tests/unit/utils/test_models_dumper.py index 0a6cf445f..7798ee99a 100644 --- a/tests/unit/utils/test_models_dumper.py +++ b/tests/unit/utils/test_models_dumper.py @@ -541,10 +541,19 @@ def test_dump_models(tmpdir: Path) -> None: "title": "PostgreSQL host" }, "port": { - "type": "string", - "nullable": true, + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer" + }, + { + "type": "null" + } + ], "default": null, - "description": "PostgreSQL port for remote::pgvector. Defaults to ${env.POSTGRES_PORT} when rag_type is remote::pgvector.", + "description": "PostgreSQL port for remote::pgvector. Defaults to ${env.POSTGRES_PORT} when rag_type is remote::pgvector. Accepts string placeholders and integer values.", "title": "PostgreSQL port" }, "db": { @@ -9155,6 +9164,12 @@ def test_dump_models(tmpdir: Path) -> None: schemas = components["schemas"] assert schemas is not None + # ByokRag.port accepts str placeholders, int values, and null. + port_schema = schemas["ByokRag"]["properties"]["port"] + assert {"type": "string"} in port_schema["anyOf"] + assert {"type": "integer"} in port_schema["anyOf"] + assert {"type": "null"} in port_schema["anyOf"] + # list of schemas expected in a dump expected_schemas = ( "A2AStateConfiguration",