From 387926744ecb82ef0034409212c271d56d8bd0bf Mon Sep 17 00:00:00 2001 From: Mahmoud Mabrouk Date: Sun, 23 Aug 2026 00:44:58 +0200 Subject: [PATCH] feat(api): probe provider connections with stored secrets --- api/entrypoints/routers.py | 1 + api/oss/src/apis/fastapi/providers/models.py | 35 +- api/oss/src/apis/fastapi/providers/router.py | 114 ++++- .../unit/providers/test_provider_probe.py | 445 +++++++++++++++++- .../provider-discovery.md | 2 +- 5 files changed, 587 insertions(+), 10 deletions(-) diff --git a/api/entrypoints/routers.py b/api/entrypoints/routers.py index aeaa5355e4b..b01db2d512d 100644 --- a/api/entrypoints/routers.py +++ b/api/entrypoints/routers.py @@ -955,6 +955,7 @@ async def _dispatch_detached_run(*, project_id, user_id, request) -> str: providers = ProvidersRouter( provider_probe_service=provider_probe_service, + vault_service=vault_service, ) webhooks = WebhooksRouter( diff --git a/api/oss/src/apis/fastapi/providers/models.py b/api/oss/src/apis/fastapi/providers/models.py index d9f7ca59642..44de278ee32 100644 --- a/api/oss/src/apis/fastapi/providers/models.py +++ b/api/oss/src/apis/fastapi/providers/models.py @@ -1,6 +1,8 @@ from datetime import datetime +from typing import Optional +from uuid import UUID -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, model_validator from oss.src.core.providers.dtos import ( CredentialResult, @@ -15,10 +17,37 @@ class ProbeProviderRequest(BaseModel): `kind` is a StandardProviderKind or CustomProviderKind value; `provider` carries the same field vocabulary the vault stores, so a card can probe what it is about to save without reshaping it. + + `secret_id` names a connection already stored in the caller's project, and is how a + write-only connection is testable at all: its value never comes back to the browser, + so there is nothing for the card to send. The stored kind and credentials are the + base; anything typed in this request replaces the stored value for that field, which + is what lets a card test an edit — a new base URL, say — before saving it. """ - kind: str = Field(description="Provider kind, e.g. 'openai', 'azure', 'custom'.") - provider: ProviderCredentials + kind: Optional[str] = Field( + default=None, + description=( + "Provider kind, e.g. 'openai', 'azure', 'custom'. Optional when `secret_id` " + "is given: the stored kind is used unless this overrides it." + ), + ) + provider: ProviderCredentials = Field(default_factory=ProviderCredentials) + secret_id: Optional[UUID] = Field( + default=None, + description=( + "Test the credential stored under this secret, in the caller's project. " + "Fields sent in `provider` override the stored ones." + ), + ) + + @model_validator(mode="after") + def require_something_to_probe(self): + """A probe needs a subject: a kind to test against, or a stored secret to load.""" + if self.kind is None and self.secret_id is None: + raise ValueError("provide `kind`, `secret_id`, or both") + + return self class ProbeProviderResponse(BaseModel): diff --git a/api/oss/src/apis/fastapi/providers/router.py b/api/oss/src/apis/fastapi/providers/router.py index 0cf8ca099ca..532b8bdad87 100644 --- a/api/oss/src/apis/fastapi/providers/router.py +++ b/api/oss/src/apis/fastapi/providers/router.py @@ -1,5 +1,9 @@ +from typing import Optional, Tuple +from uuid import UUID + from fastapi import APIRouter, HTTPException, Request, status from fastapi.responses import JSONResponse +from pydantic import SecretStr from oss.src.apis.fastapi.providers.models import ( ProbeProviderRequest, @@ -8,8 +12,11 @@ from oss.src.apis.fastapi.vault.router import SecretSafeRoute from oss.src.core.access.permissions.service import check_action_access from oss.src.core.access.permissions.types import Permission +from oss.src.core.providers.dtos import ProviderCredentials from oss.src.core.providers.exceptions import ProviderProbeError from oss.src.core.providers.service import ProviderProbeService +from oss.src.core.secrets.redaction import PRIMARY_CREDENTIAL_FIELDS +from oss.src.core.secrets.services import VaultService from oss.src.utils.exceptions import intercept_exceptions from oss.src.utils.logging import get_module_logger @@ -17,6 +24,50 @@ log = get_module_logger(__name__) +def _typed_or_stored(typed, stored): + if typed is None: + return stored + + value = typed.get_secret_value() if isinstance(typed, SecretStr) else typed + if value in ("", {}, []): + return stored + + return typed + + +def _stored_credential(secret, settings, extras): + container_name, field = PRIMARY_CREDENTIAL_FIELDS.get( + str(getattr(secret.kind, "value", secret.kind)), (None, None) + ) + + if container_name is not None: + container = getattr(secret.data, container_name, None) + primary = getattr(container, field, None) if container is not None else None + if primary: + return primary + + return (extras or {}).get("api_key") or None + + +def _merged_extras(typed, stored): + if not typed: + return stored + + merged = dict(stored or {}) + for name, value in typed.items(): + if value in (None, ""): + continue + merged[name] = value + + return merged or None + + +def _stored_kind(secret) -> str: + kind = getattr(secret.data, "kind", None) + + return str(getattr(kind, "value", kind)) + + class ProvidersRouter: """Credential test and model discovery for a provider connection. @@ -27,8 +78,10 @@ class ProvidersRouter: def __init__( self, provider_probe_service: ProviderProbeService, + vault_service: VaultService, ): self.service = provider_probe_service + self.vault_service = vault_service self.router = APIRouter(route_class=SecretSafeRoute) @@ -40,6 +93,54 @@ def __init__( response_model=ProbeProviderResponse, ) + async def _merge_stored_secret( + self, + *, + project_id: UUID, + secret_id: UUID, + kind: Optional[str], + typed: ProviderCredentials, + ) -> Tuple[str, ProviderCredentials]: + secret = await self.vault_service.get_secret_by_id( + secret_id=secret_id, + project_id=project_id, + ) + + if secret is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Secret not found", + ) + + if secret.management is not None: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail="Managed secrets cannot be probed.", + ) + + stored_kind = _stored_kind(secret) + settings = getattr(secret.data, "provider", None) + stored_extras = getattr(settings, "extras", None) if settings else None + stored_key = _stored_credential(secret, settings, stored_extras) + + if kind is not None and kind != stored_kind and typed.key is None: + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, + detail=( + "Testing this connection as a different provider requires its " + "credential; the stored one belongs to the saved provider." + ), + ) + + merged = ProviderCredentials( + key=_typed_or_stored(typed.key, stored_key), + url=_typed_or_stored(typed.url, getattr(settings, "url", None)), + version=_typed_or_stored(typed.version, getattr(settings, "version", None)), + extras=_merged_extras(typed.extras, stored_extras), + ) + + return kind or stored_kind, merged + @intercept_exceptions() async def probe_provider(self, request: Request, body: ProbeProviderRequest): # EDIT_SECRET, not VIEW_SECRET: a probe spends a caller-supplied credential on an @@ -57,10 +158,19 @@ async def probe_provider(self, request: Request, body: ProbeProviderRequest): status_code=403, ) + kind, credentials = body.kind, body.provider + if body.secret_id is not None: + kind, credentials = await self._merge_stored_secret( + project_id=UUID(request.state.project_id), + secret_id=body.secret_id, + kind=kind, + typed=credentials, + ) + try: return await self.service.probe( - kind=body.kind, - credentials=body.provider, + kind=kind, + credentials=credentials, ) except ProviderProbeError as e: raise HTTPException( diff --git a/api/oss/tests/pytest/unit/providers/test_provider_probe.py b/api/oss/tests/pytest/unit/providers/test_provider_probe.py index eea17e962a9..5031ed0b675 100644 --- a/api/oss/tests/pytest/unit/providers/test_provider_probe.py +++ b/api/oss/tests/pytest/unit/providers/test_provider_probe.py @@ -20,12 +20,14 @@ from oss.src.apis.fastapi.providers.models import ProbeProviderRequest from oss.src.apis.fastapi.providers.router import ProvidersRouter from oss.src.core.providers.dtos import ProviderCredentials +from oss.src.core.secrets.dtos import SecretResponseDTO from oss.src.core.providers.exceptions import ( ProviderEndpointNotAllowed, ProviderEndpointRequired, UnsupportedProviderKind, ) from oss.src.core.providers.service import ProviderProbeService +from oss.src.core.secrets.managed import SecretManagementDTO, SecretManager CANARY = "sk-CANARY-DO-NOT-LEAK-abc123" @@ -648,24 +650,76 @@ def capture(*args, **kwargs): # --- route wiring ----------------------------------------------------------- # -def build_client(monkeypatch, responder, *, permitted: bool = True) -> TestClient: +PROJECT_ID = uuid4() +STORED_KEY = "sk-STORED-DO-NOT-LEAK-abc123" + + +class _StubVault: + """Holds one secret per (project, id), like the scoped vault read does.""" + + def __init__(self): + self.records: dict = {} + + def store(self, secret_id, project_id, secret): + self.records[(str(project_id), str(secret_id))] = secret + + async def get_secret_by_id( + self, *, secret_id, project_id=None, organization_id=None + ): + return self.records.get((str(project_id), str(secret_id))) + + +def _stored_provider_key(key: str = STORED_KEY, kind: str = "openai"): + """A stored provider_key row as the vault hands it to an in-process reader.""" + return SecretResponseDTO( + id=uuid4(), + slug=f"{kind}-stored", + kind="provider_key", + data={"kind": kind, "provider": {"key": key}}, + header={"name": "Stored"}, + write_only=True, + ) + + +def _stored_custom_provider(url: str, key: str = STORED_KEY): + return SecretResponseDTO( + id=uuid4(), + slug="gateway-stored", + kind="custom_provider", + data={ + "kind": "custom", + "provider": {"key": key, "url": url}, + "models": [{"slug": "gpt-5.6-luna"}], + }, + header={"name": "Gateway"}, + write_only=True, + ) + + +def build_client( + monkeypatch, responder, *, permitted: bool = True, vault=None +) -> TestClient: async def _check_action_access(**_kwargs) -> bool: return permitted monkeypatch.setattr(router_module, "check_action_access", _check_action_access) service, _ = probe_service(responder) + vault = vault if vault is not None else _StubVault() app = FastAPI() @app.middleware("http") async def _scope(request, call_next): request.state.user_id = str(uuid4()) - request.state.project_id = str(uuid4()) + request.state.project_id = str(PROJECT_ID) return await call_next(request) - app.include_router(ProvidersRouter(provider_probe_service=service).router) app.include_router( - ProvidersRouter(provider_probe_service=service).router, prefix="/vault/v1" + ProvidersRouter(provider_probe_service=service, vault_service=vault).router + ) + app.include_router( + ProvidersRouter(provider_probe_service=service, vault_service=vault).router, + prefix="/vault/v1", ) return TestClient(app) @@ -742,3 +796,386 @@ def test_a_malformed_probe_body_does_not_echo_the_credential(monkeypatch): assert response.status_code == 422 assert CANARY not in response.text + + +# --- probing a stored connection --------------------------------------------- # + + +def test_a_stored_key_probes_without_the_caller_sending_one(monkeypatch): + # The case this exists for: once a connection is write-only its value never comes + # back to the browser, so "Test" had nothing to send and stayed disabled. + vault = _StubVault() + secret_id = uuid4() + vault.store(secret_id, PROJECT_ID, _stored_provider_key()) + client = build_client( + monkeypatch, json_response({"data": [{"id": "gpt-5.6-luna"}]}), vault=vault + ) + + response = client.post( + "/providers/probe", json={"secret_id": str(secret_id), "provider": {}} + ) + + assert response.status_code == 200 + body = response.json() + assert body["credential"]["status"] == "valid" + assert body["discovery"]["models"] == ["gpt-5.6-luna"] + assert STORED_KEY not in response.text + + +def test_the_stored_key_is_what_reaches_the_provider(monkeypatch): + vault = _StubVault() + secret_id = uuid4() + vault.store(secret_id, PROJECT_ID, _stored_provider_key()) + recorder = Recorder(json_response({"data": []})) + client = build_client(monkeypatch, recorder, vault=vault) + + client.post("/providers/probe", json={"secret_id": str(secret_id)}) + + (sent,) = recorder.requests + assert sent.headers["authorization"] == f"Bearer {STORED_KEY}" + + +def test_a_typed_base_url_overrides_the_stored_one(monkeypatch, public_dns): + # Testing an edit before saving it: the card changed the URL, the key is still the + # stored one because it was never readable. + vault = _StubVault() + secret_id = uuid4() + vault.store( + secret_id, PROJECT_ID, _stored_custom_provider(url="https://old.example.com/v1") + ) + recorder = Recorder(json_response({"data": [{"id": "gpt-5.6-luna"}]})) + client = build_client(monkeypatch, recorder, vault=vault) + + response = client.post( + "/providers/probe", + json={ + "secret_id": str(secret_id), + "provider": {"url": "https://new.example.com/v1"}, + }, + ) + + assert response.status_code == 200 + (sent,) = recorder.requests + # The egress guard pins the connection to the resolved address and carries the + # hostname in the Host header, so that is where the typed URL shows up. + assert sent.headers["host"] == "new.example.com" + assert sent.headers["authorization"] == f"Bearer {STORED_KEY}" + + +def test_a_typed_key_overrides_the_stored_one(monkeypatch): + vault = _StubVault() + secret_id = uuid4() + vault.store(secret_id, PROJECT_ID, _stored_provider_key()) + recorder = Recorder(json_response({"data": []})) + client = build_client(monkeypatch, recorder, vault=vault) + + client.post( + "/providers/probe", + json={"secret_id": str(secret_id), "provider": {"key": CANARY}}, + ) + + (sent,) = recorder.requests + assert sent.headers["authorization"] == f"Bearer {CANARY}" + + +def test_a_secret_from_another_project_is_not_found(monkeypatch): + vault = _StubVault() + secret_id = uuid4() + vault.store(secret_id, uuid4(), _stored_provider_key()) # someone else's project + client = build_client(monkeypatch, json_response({"data": []}), vault=vault) + + response = client.post("/providers/probe", json={"secret_id": str(secret_id)}) + + assert response.status_code == 404 + assert STORED_KEY not in response.text + + +def test_an_unknown_secret_is_not_found(monkeypatch): + client = build_client(monkeypatch, json_response({"data": []})) + + response = client.post("/providers/probe", json={"secret_id": str(uuid4())}) + + assert response.status_code == 404 + + +def test_the_stored_key_is_not_lent_to_another_provider(monkeypatch): + # A stored credential belongs to the provider it was saved for. Changing the kind + # while using it would send one provider's key to another's endpoint. + vault = _StubVault() + secret_id = uuid4() + vault.store(secret_id, PROJECT_ID, _stored_provider_key(kind="openai")) + recorder = Recorder(json_response({"data": []})) + client = build_client(monkeypatch, recorder, vault=vault) + + response = client.post( + "/providers/probe", json={"secret_id": str(secret_id), "kind": "anthropic"} + ) + + assert response.status_code == 422 + assert recorder.requests == [] + assert STORED_KEY not in response.text + + +def test_a_kind_change_is_allowed_when_the_caller_brings_the_credential(monkeypatch): + vault = _StubVault() + secret_id = uuid4() + vault.store(secret_id, PROJECT_ID, _stored_provider_key(kind="openai")) + client = build_client(monkeypatch, json_response({"data": []}), vault=vault) + + response = client.post( + "/providers/probe", + json={ + "secret_id": str(secret_id), + "kind": "anthropic", + "provider": {"key": CANARY}, + }, + ) + + assert response.status_code == 200 + + +def test_a_probe_must_name_a_kind_or_a_secret(monkeypatch): + client = build_client(monkeypatch, json_response({"data": []})) + + response = client.post("/providers/probe", json={"provider": {"key": CANARY}}) + + assert response.status_code == 422 + assert CANARY not in response.text + + +def test_blank_typed_fields_fall_back_to_the_stored_ones(monkeypatch, public_dns): + # The card drops blanks, but a form that submits "" must not be read as "probe with + # no URL and no key" — that fails a connection which is actually fine. + vault = _StubVault() + secret_id = uuid4() + vault.store( + secret_id, PROJECT_ID, _stored_custom_provider(url="https://old.example.com/v1") + ) + recorder = Recorder(json_response({"data": [{"id": "gpt-5.6-luna"}]})) + client = build_client(monkeypatch, recorder, vault=vault) + + response = client.post( + "/providers/probe", + json={ + "secret_id": str(secret_id), + "provider": {"url": "", "key": "", "version": "", "extras": {}}, + }, + ) + + assert response.status_code == 200 + (sent,) = recorder.requests + assert sent.headers["host"] == "old.example.com" + assert sent.headers["authorization"] == f"Bearer {STORED_KEY}" + + +def test_an_omitted_provider_object_probes_exactly_what_is_stored( + monkeypatch, public_dns +): + vault = _StubVault() + secret_id = uuid4() + vault.store( + secret_id, PROJECT_ID, _stored_custom_provider(url="https://old.example.com/v1") + ) + recorder = Recorder(json_response({"data": []})) + client = build_client(monkeypatch, recorder, vault=vault) + + response = client.post("/providers/probe", json={"secret_id": str(secret_id)}) + + assert response.status_code == 200 + (sent,) = recorder.requests + assert sent.headers["host"] == "old.example.com" + assert sent.headers["authorization"] == f"Bearer {STORED_KEY}" + + +def _stored_bedrock(token: str = STORED_KEY, region: str = "us-east-1"): + return SecretResponseDTO( + id=uuid4(), + slug="bedrock-stored", + kind="custom_provider", + data={ + "kind": "bedrock", + "provider": { + "extras": { + "aws_bearer_token_bedrock": token, + "aws_region_name": region, + } + }, + "models": [{"slug": "claude-opus-5"}], + }, + header={"name": "Bedrock"}, + write_only=True, + ) + + +def test_typing_one_extra_keeps_the_stored_credential(monkeypatch, public_dns): + # The card sends only what the user touched. If extras replaced the stored dict + # wholesale, typing a region would drop the credential beside it and the probe would + # report a working connection as broken. + vault = _StubVault() + secret_id = uuid4() + vault.store(secret_id, PROJECT_ID, _stored_bedrock()) + recorder = Recorder(json_response({"modelSummaries": []})) + client = build_client(monkeypatch, recorder, vault=vault) + + response = client.post( + "/providers/probe", + json={ + "secret_id": str(secret_id), + "provider": {"extras": {"aws_region_name": "eu-central-1"}}, + }, + ) + + assert response.status_code == 200 + assert response.json()["credential"]["status"] == "valid" + (sent,) = recorder.requests + # The typed region routed the request; the stored token still authenticated it. + assert "eu-central-1" in str(sent.url) + assert sent.headers["authorization"] == f"Bearer {STORED_KEY}" + + +def test_a_typed_extra_overrides_the_stored_one_of_the_same_name( + monkeypatch, public_dns +): + vault = _StubVault() + secret_id = uuid4() + vault.store(secret_id, PROJECT_ID, _stored_bedrock()) + recorder = Recorder(json_response({"modelSummaries": []})) + client = build_client(monkeypatch, recorder, vault=vault) + + client.post( + "/providers/probe", + json={ + "secret_id": str(secret_id), + "provider": {"extras": {"aws_bearer_token_bedrock": CANARY}}, + }, + ) + + (sent,) = recorder.requests + assert sent.headers["authorization"] == f"Bearer {CANARY}" + # The stored region survived: only the named key was replaced. + assert "us-east-1" in str(sent.url) + + +def _stored_custom_with_key_in_extras(url: str, key: str = STORED_KEY): + """How the connection form actually saves a custom provider: key inside extras.""" + return SecretResponseDTO( + id=uuid4(), + slug="gateway-extras", + kind="custom_provider", + data={ + "kind": "custom", + "provider": {"url": url, "extras": {"api_key": key}}, + "models": [{"slug": "gpt-5.6-luna"}], + }, + header={"name": "Gateway"}, + write_only=True, + ) + + +def test_a_custom_connection_authenticates_with_the_key_stored_in_extras( + monkeypatch, public_dns +): + # The connection form writes a custom provider's key to provider.extras.api_key, not + # to provider.key. Reading only the latter sent the probe out with no Authorization + # header, and against a provider whose catalog is public that reads as a pass. + vault = _StubVault() + secret_id = uuid4() + vault.store( + secret_id, + PROJECT_ID, + _stored_custom_with_key_in_extras(url="https://gateway.example.com/v1"), + ) + recorder = Recorder(json_response({"data": [{"id": "gpt-5.6-luna"}]})) + client = build_client(monkeypatch, recorder, vault=vault) + + response = client.post("/providers/probe", json={"secret_id": str(secret_id)}) + + assert response.status_code == 200 + (sent,) = recorder.requests + assert sent.headers["authorization"] == f"Bearer {STORED_KEY}" + # A key that reaches the provider is a key that was tested. + assert response.json()["credential"]["status"] == "valid" + assert STORED_KEY not in response.text + + +def test_provider_key_still_wins_over_an_extras_key(monkeypatch, public_dns): + vault = _StubVault() + secret_id = uuid4() + secret = _stored_custom_with_key_in_extras(url="https://gateway.example.com/v1") + secret.data.provider.key = "sk-STORED-PRIMARY-abc123" + vault.store(secret_id, PROJECT_ID, secret) + recorder = Recorder(json_response({"data": []})) + client = build_client(monkeypatch, recorder, vault=vault) + + client.post("/providers/probe", json={"secret_id": str(secret_id)}) + + (sent,) = recorder.requests + assert sent.headers["authorization"] == "Bearer sk-STORED-PRIMARY-abc123" + + +def test_a_bedrock_connection_probes_with_its_stored_extras_credential( + monkeypatch, public_dns +): + # Bedrock keeps its credential in extras and the adapter reads it from there, so + # nothing is typed at all: the whole credential has to survive the merge. + vault = _StubVault() + secret_id = uuid4() + vault.store(secret_id, PROJECT_ID, _stored_bedrock()) + recorder = Recorder(json_response({"modelSummaries": []})) + client = build_client(monkeypatch, recorder, vault=vault) + + response = client.post("/providers/probe", json={"secret_id": str(secret_id)}) + + assert response.status_code == 200 + assert response.json()["credential"]["status"] == "valid" + (sent,) = recorder.requests + assert sent.headers["authorization"] == f"Bearer {STORED_KEY}" + assert "us-east-1" in str(sent.url) + assert STORED_KEY not in response.text + + +def test_every_secret_kind_the_classifier_knows_has_a_credential_location(): + # The probe asks the classifier where a kind keeps its credential. If a kind is ever + # added to the vault without an entry there, this probe would silently send nothing. + from oss.src.core.secrets.redaction import PRIMARY_CREDENTIAL_FIELDS + + assert PRIMARY_CREDENTIAL_FIELDS["provider_key"] == ("provider", "key") + assert PRIMARY_CREDENTIAL_FIELDS["custom_provider"] == ("provider", "key") + + +@pytest.mark.parametrize("path", ["/providers/probe", "/vault/v1/providers/probe"]) +def test_a_managed_secret_cannot_be_probed_even_with_caller_overrides( + monkeypatch, path +): + vault = _StubVault() + secret_id = uuid4() + vault.store( + secret_id, + PROJECT_ID, + _stored_provider_key(), + ) + secret = vault.records[(str(PROJECT_ID), str(secret_id))] + secret.management = SecretManagementDTO( + manager=SecretManager.STARTER_CREDITS_BRIDGE, + ) + recorder = Recorder(json_response({"data": []})) + client = build_client(monkeypatch, recorder, vault=vault) + + response = client.post( + path, + json={ + "secret_id": str(secret_id), + "kind": "custom", + "provider": { + "key": CANARY, + "url": "https://override.example.com/v1", + "extras": {"api_key": CANARY, "region": "override"}, + }, + }, + ) + + assert response.status_code == 409 + assert response.json() == {"detail": "Managed secrets cannot be probed."} + assert recorder.requests == [] + assert STORED_KEY not in response.text + assert CANARY not in response.text diff --git a/docs/design/provider-connections-models/provider-discovery.md b/docs/design/provider-connections-models/provider-discovery.md index 3d017fe3618..ba5ea1d0e76 100644 --- a/docs/design/provider-connections-models/provider-discovery.md +++ b/docs/design/provider-connections-models/provider-discovery.md @@ -51,7 +51,7 @@ The current custom-provider record type already stores those fields today; see | Perplexity | API key | No, not through its public model endpoint | Catalog only through `GET /v1/models` | Refresh models. Do not report that the key is valid. | | MiniMax | API key | Not confirmed in its public API reference | No confirmed list endpoint | Keep Agenta's catalog and manual IDs. Do not use paid generation as a test. | | Anyscale | API key | No current hosted-model endpoint confirmed | No current hosted-model endpoint confirmed | Treat a user endpoint as custom. Keep manual IDs. | -| OpenAI-compatible endpoint | API key plus base URL | Not guaranteed by the compatibility label | Try `GET {base_url}/models` | A 404 or 405 means discovery is unsupported, not that the key is invalid. | +| OpenAI-compatible endpoint | Base URL; API key optional | Not guaranteed by the compatibility label, and nothing is proven when no key is sent | Try `GET {base_url}/models` | A 404 or 405 means discovery is unsupported, not that the key is invalid. A keyless endpoint can still list its models; that reaching it succeeded says nothing about a credential, so the probe reports the credential as unknown rather than valid. | Official references: