mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-07-19 10:21: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:
|
properties:
|
||||||
provider:
|
provider:
|
||||||
$ref: '#/components/schemas/GenAIProviderEnum'
|
$ref: '#/components/schemas/GenAIProviderEnum'
|
||||||
|
name:
|
||||||
|
anyOf:
|
||||||
|
- type: string
|
||||||
|
- type: 'null'
|
||||||
|
title: Name
|
||||||
api_key:
|
api_key:
|
||||||
anyOf:
|
anyOf:
|
||||||
- type: string
|
- type: string
|
||||||
|
|||||||
@ -196,7 +196,7 @@ def genai_models(request: Request):
|
|||||||
"before saving the configuration."
|
"before saving the configuration."
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
async def genai_probe(body: GenAIProbeBody):
|
async def genai_probe(request: Request, body: GenAIProbeBody):
|
||||||
load_providers()
|
load_providers()
|
||||||
|
|
||||||
provider_cls = PROVIDERS.get(body.provider)
|
provider_cls = PROVIDERS.get(body.provider)
|
||||||
@ -206,6 +206,13 @@ async def genai_probe(body: GenAIProbeBody):
|
|||||||
content={"success": False, "message": "Unknown provider"},
|
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
|
# The OpenAI-compatible SDKs accept "timeout" as a constructor kwarg via
|
||||||
# provider_options; other plugins use GenAIClient.timeout passed below.
|
# provider_options; other plugins use GenAIClient.timeout passed below.
|
||||||
# Don't inject timeout for Gemini — its HttpOptions interprets the value
|
# Don't inject timeout for Gemini — its HttpOptions interprets the value
|
||||||
@ -217,7 +224,7 @@ async def genai_probe(body: GenAIProbeBody):
|
|||||||
try:
|
try:
|
||||||
transient_cfg = GenAIConfig(
|
transient_cfg = GenAIConfig(
|
||||||
provider=body.provider,
|
provider=body.provider,
|
||||||
api_key=body.api_key,
|
api_key=api_key,
|
||||||
base_url=body.base_url,
|
base_url=body.base_url,
|
||||||
provider_options=probe_provider_options,
|
provider_options=probe_provider_options,
|
||||||
# model is required by the schema but irrelevant for listing.
|
# model is required by the schema but irrelevant for listing.
|
||||||
|
|||||||
@ -14,6 +14,7 @@ class AppConfigSetBody(BaseModel):
|
|||||||
|
|
||||||
class GenAIProbeBody(BaseModel):
|
class GenAIProbeBody(BaseModel):
|
||||||
provider: GenAIProviderEnum
|
provider: GenAIProviderEnum
|
||||||
|
name: str | None = None
|
||||||
api_key: str | None = None
|
api_key: str | None = None
|
||||||
base_url: str | None = None
|
base_url: str | None = None
|
||||||
provider_options: dict[str, Any] = Field(default_factory=dict)
|
provider_options: dict[str, Any] = Field(default_factory=dict)
|
||||||
|
|||||||
@ -132,6 +132,77 @@ class TestHttpApp(BaseTestHttp):
|
|||||||
"models": ["fake-model-a", "fake-model-b"],
|
"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):
|
def test_genai_probe_empty_list_is_treated_as_failure(self):
|
||||||
# The plugin's list_models() returns [] on connection failure rather
|
# The plugin's list_models() returns [] on connection failure rather
|
||||||
# than raising. The endpoint should surface that as success=false so
|
# than raising. The endpoint should surface that as success=false so
|
||||||
|
|||||||
@ -160,6 +160,7 @@ export function GenAIModelWidget(props: WidgetProps) {
|
|||||||
try {
|
try {
|
||||||
const res = await axios.post<ProbeResponse>("genai/probe", {
|
const res = await axios.post<ProbeResponse>("genai/probe", {
|
||||||
provider: formProvider,
|
provider: formProvider,
|
||||||
|
name: providerKey,
|
||||||
api_key:
|
api_key:
|
||||||
typeof formEntry.api_key === "string" ? formEntry.api_key : null,
|
typeof formEntry.api_key === "string" ? formEntry.api_key : null,
|
||||||
base_url:
|
base_url:
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user