mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-07-18 18:01:14 +03:00
resolve saved credential sentinel to the stored api_key in the GenAI probe
This commit is contained in:
parent
d02a1156b7
commit
0e61db3922
5
docs/static/frigate-api.yaml
vendored
5
docs/static/frigate-api.yaml
vendored
@ -8244,6 +8244,11 @@ components:
|
||||
properties:
|
||||
provider:
|
||||
$ref: '#/components/schemas/GenAIProviderEnum'
|
||||
name:
|
||||
anyOf:
|
||||
- type: string
|
||||
- type: 'null'
|
||||
title: Name
|
||||
api_key:
|
||||
anyOf:
|
||||
- type: string
|
||||
|
||||
@ -196,7 +196,7 @@ def genai_models(request: Request):
|
||||
"before saving the configuration."
|
||||
),
|
||||
)
|
||||
async def genai_probe(body: GenAIProbeBody):
|
||||
async def genai_probe(request: Request, body: GenAIProbeBody):
|
||||
load_providers()
|
||||
|
||||
provider_cls = PROVIDERS.get(body.provider)
|
||||
@ -206,6 +206,13 @@ async def genai_probe(body: GenAIProbeBody):
|
||||
content={"success": False, "message": "Unknown provider"},
|
||||
)
|
||||
|
||||
api_key = body.api_key
|
||||
if api_key == REDACTED_CREDENTIAL_SENTINEL:
|
||||
saved_cfg = (
|
||||
request.app.frigate_config.genai.get(body.name) if body.name else None
|
||||
)
|
||||
api_key = saved_cfg.api_key if saved_cfg else None
|
||||
|
||||
# The OpenAI-compatible SDKs accept "timeout" as a constructor kwarg via
|
||||
# provider_options; other plugins use GenAIClient.timeout passed below.
|
||||
# Don't inject timeout for Gemini — its HttpOptions interprets the value
|
||||
@ -217,7 +224,7 @@ async def genai_probe(body: GenAIProbeBody):
|
||||
try:
|
||||
transient_cfg = GenAIConfig(
|
||||
provider=body.provider,
|
||||
api_key=body.api_key,
|
||||
api_key=api_key,
|
||||
base_url=body.base_url,
|
||||
provider_options=probe_provider_options,
|
||||
# model is required by the schema but irrelevant for listing.
|
||||
|
||||
@ -14,6 +14,7 @@ class AppConfigSetBody(BaseModel):
|
||||
|
||||
class GenAIProbeBody(BaseModel):
|
||||
provider: GenAIProviderEnum
|
||||
name: str | None = None
|
||||
api_key: str | None = None
|
||||
base_url: str | None = None
|
||||
provider_options: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
@ -132,6 +132,77 @@ class TestHttpApp(BaseTestHttp):
|
||||
"models": ["fake-model-a", "fake-model-b"],
|
||||
}
|
||||
|
||||
def test_genai_probe_resolves_sentinel_to_saved_api_key(self):
|
||||
# After a save the UI's api_key field holds the redaction sentinel;
|
||||
# the probe must substitute the saved key for the named entry instead
|
||||
# of sending the literal sentinel to the provider (GH discussion 23754).
|
||||
probed_keys: list[str | None] = []
|
||||
|
||||
class CapturingClient(GenAIClient):
|
||||
def list_models(self):
|
||||
probed_keys.append(self.genai_config.api_key)
|
||||
return ["fake-model"]
|
||||
|
||||
self.minimal_config["genai"] = {
|
||||
"llm": {
|
||||
"provider": "openai",
|
||||
"api_key": "sk-saved",
|
||||
"base_url": "https://example.invalid",
|
||||
"model": "fake-model",
|
||||
}
|
||||
}
|
||||
app = super().create_app()
|
||||
|
||||
with (
|
||||
AuthTestClient(app) as client,
|
||||
patch.dict(
|
||||
frigate.genai.PROVIDERS,
|
||||
{GenAIProviderEnum.openai: CapturingClient},
|
||||
),
|
||||
):
|
||||
response = client.post(
|
||||
"/genai/probe",
|
||||
json={
|
||||
"provider": "openai",
|
||||
"name": "llm",
|
||||
"api_key": REDACTED_CREDENTIAL_SENTINEL,
|
||||
"base_url": "https://example.invalid",
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["success"] is True
|
||||
assert probed_keys == ["sk-saved"]
|
||||
|
||||
def test_genai_probe_sentinel_without_saved_entry_sends_no_key(self):
|
||||
# If the sentinel arrives for an entry that has no saved config, the
|
||||
# probe must drop the key entirely rather than leak the sentinel.
|
||||
probed_keys: list[str | None] = []
|
||||
|
||||
class CapturingClient(GenAIClient):
|
||||
def list_models(self):
|
||||
probed_keys.append(self.genai_config.api_key)
|
||||
return ["fake-model"]
|
||||
|
||||
app = super().create_app()
|
||||
|
||||
with (
|
||||
AuthTestClient(app) as client,
|
||||
patch.dict(
|
||||
frigate.genai.PROVIDERS,
|
||||
{GenAIProviderEnum.openai: CapturingClient},
|
||||
),
|
||||
):
|
||||
response = client.post(
|
||||
"/genai/probe",
|
||||
json={
|
||||
"provider": "openai",
|
||||
"name": "llm",
|
||||
"api_key": REDACTED_CREDENTIAL_SENTINEL,
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert probed_keys == [None]
|
||||
|
||||
def test_genai_probe_empty_list_is_treated_as_failure(self):
|
||||
# The plugin's list_models() returns [] on connection failure rather
|
||||
# than raising. The endpoint should surface that as success=false so
|
||||
|
||||
@ -160,6 +160,7 @@ export function GenAIModelWidget(props: WidgetProps) {
|
||||
try {
|
||||
const res = await axios.post<ProbeResponse>("genai/probe", {
|
||||
provider: formProvider,
|
||||
name: providerKey,
|
||||
api_key:
|
||||
typeof formEntry.api_key === "string" ? formEntry.api_key : null,
|
||||
base_url:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user