Fix/frontend review findings #4
@@ -443,6 +443,7 @@ class ProviderPreset(Contract):
|
|||||||
name: str
|
name: str
|
||||||
provider_type: ProviderType
|
provider_type: ProviderType
|
||||||
base_url: str
|
base_url: str
|
||||||
|
default_credential_id: str | None = None
|
||||||
requires_credential: bool = True
|
requires_credential: bool = True
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -10,8 +10,17 @@ class CredentialResolver(Protocol):
|
|||||||
class EnvironmentCredentialResolver:
|
class EnvironmentCredentialResolver:
|
||||||
"""解析由桌面 Host 注入 Sidecar 进程的临时凭证上下文。"""
|
"""解析由桌面 Host 注入 Sidecar 进程的临时凭证上下文。"""
|
||||||
|
|
||||||
|
_development_aliases = {
|
||||||
|
"openai": "OPENAI_API_KEY",
|
||||||
|
"deepseek": "DEEPSEEK_API_KEY",
|
||||||
|
}
|
||||||
|
|
||||||
def resolve(self, credential_id: str | None) -> str | None:
|
def resolve(self, credential_id: str | None) -> str | None:
|
||||||
if not credential_id:
|
if not credential_id:
|
||||||
return None
|
return None
|
||||||
normalized = re.sub(r"[^A-Za-z0-9]", "_", credential_id).upper()
|
normalized = re.sub(r"[^A-Za-z0-9]", "_", credential_id).upper()
|
||||||
return os.getenv(f"AINOTE_CREDENTIAL_{normalized}")
|
injected = os.getenv(f"AINOTE_CREDENTIAL_{normalized}")
|
||||||
|
if injected:
|
||||||
|
return injected
|
||||||
|
alias = self._development_aliases.get(credential_id.lower())
|
||||||
|
return os.getenv(alias) if alias else None
|
||||||
|
|||||||
@@ -35,12 +35,14 @@ class ProviderFactory:
|
|||||||
name="OpenAI",
|
name="OpenAI",
|
||||||
provider_type=ProviderType.openai_chat,
|
provider_type=ProviderType.openai_chat,
|
||||||
base_url="https://api.openai.com/v1",
|
base_url="https://api.openai.com/v1",
|
||||||
|
default_credential_id="openai",
|
||||||
),
|
),
|
||||||
ProviderPreset(
|
ProviderPreset(
|
||||||
preset_id="deepseek",
|
preset_id="deepseek",
|
||||||
name="DeepSeek",
|
name="DeepSeek",
|
||||||
provider_type=ProviderType.openai_compatible,
|
provider_type=ProviderType.openai_compatible,
|
||||||
base_url="https://api.deepseek.com",
|
base_url="https://api.deepseek.com",
|
||||||
|
default_credential_id="deepseek",
|
||||||
),
|
),
|
||||||
ProviderPreset(
|
ProviderPreset(
|
||||||
preset_id="ollama",
|
preset_id="ollama",
|
||||||
|
|||||||
@@ -264,6 +264,11 @@ class OpenAICompatibleProvider(TurnStreamingMixin):
|
|||||||
def _headers(self) -> dict[str, str]:
|
def _headers(self) -> dict[str, str]:
|
||||||
headers = {"Content-Type": "application/json"}
|
headers = {"Content-Type": "application/json"}
|
||||||
api_key = self.credentials.resolve(self.credential_id)
|
api_key = self.credentials.resolve(self.credential_id)
|
||||||
|
if self.credential_id and not api_key:
|
||||||
|
raise ProviderError(
|
||||||
|
"PROVIDER_CREDENTIAL_MISSING",
|
||||||
|
f'Credential "{self.credential_id}" is not available in the AI Core process.',
|
||||||
|
)
|
||||||
if api_key:
|
if api_key:
|
||||||
headers["Authorization"] = f"Bearer {api_key}"
|
headers["Authorization"] = f"Bearer {api_key}"
|
||||||
return headers
|
return headers
|
||||||
|
|||||||
@@ -517,6 +517,7 @@ async def list_provider_models(provider_id: str) -> ProviderModelsResponse:
|
|||||||
models = await container.providers.list_models(provider_id)
|
models = await container.providers.list_models(provider_id)
|
||||||
except ProviderError as exc:
|
except ProviderError as exc:
|
||||||
status_code = {
|
status_code = {
|
||||||
|
"PROVIDER_CREDENTIAL_MISSING": 422,
|
||||||
"PROVIDER_AUTH_FAILED": 401,
|
"PROVIDER_AUTH_FAILED": 401,
|
||||||
"MODEL_NOT_FOUND": 404,
|
"MODEL_NOT_FOUND": 404,
|
||||||
"PROVIDER_RATE_LIMITED": 429,
|
"PROVIDER_RATE_LIMITED": 429,
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ def test_provider_presets_include_openai_and_deepseek() -> None:
|
|||||||
assert by_id["openai"].base_url == "https://api.openai.com/v1"
|
assert by_id["openai"].base_url == "https://api.openai.com/v1"
|
||||||
assert by_id["deepseek"].base_url == "https://api.deepseek.com"
|
assert by_id["deepseek"].base_url == "https://api.deepseek.com"
|
||||||
assert by_id["deepseek"].provider_type == ProviderType.openai_compatible
|
assert by_id["deepseek"].provider_type == ProviderType.openai_compatible
|
||||||
|
assert by_id["deepseek"].default_credential_id == "deepseek"
|
||||||
|
|
||||||
|
|
||||||
def test_provider_presets_static_route_precedes_provider_id_route() -> None:
|
def test_provider_presets_static_route_precedes_provider_id_route() -> None:
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ from app.contracts import (
|
|||||||
ToolDefinition,
|
ToolDefinition,
|
||||||
)
|
)
|
||||||
from app.providers.ollama import OllamaProvider
|
from app.providers.ollama import OllamaProvider
|
||||||
|
from app.providers.base import ProviderError
|
||||||
|
from app.providers.credentials import EnvironmentCredentialResolver
|
||||||
from app.providers.openai_compatible import OpenAICompatibleProvider
|
from app.providers.openai_compatible import OpenAICompatibleProvider
|
||||||
|
|
||||||
|
|
||||||
@@ -156,6 +158,31 @@ def test_openai_compatible_fetches_and_maps_model_list() -> None:
|
|||||||
assert [item.model for item in models] == ["model-b", "model-a"]
|
assert [item.model for item in models] == ["model-b", "model-a"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_environment_credentials_support_deepseek_development_alias(monkeypatch) -> None:
|
||||||
|
monkeypatch.setenv("DEEPSEEK_API_KEY", "secret-test-key")
|
||||||
|
|
||||||
|
assert EnvironmentCredentialResolver().resolve("deepseek") == "secret-test-key"
|
||||||
|
|
||||||
|
|
||||||
|
def test_openai_compatible_rejects_missing_named_credential_before_request() -> None:
|
||||||
|
class EmptyCredentials:
|
||||||
|
def resolve(self, credential_id: str | None) -> str | None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
provider = OpenAICompatibleProvider(
|
||||||
|
base_url="https://provider.test/v1",
|
||||||
|
credential_id="deepseek",
|
||||||
|
credentials=EmptyCredentials(),
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
run(provider.list_models())
|
||||||
|
except ProviderError as error:
|
||||||
|
assert error.code == "PROVIDER_CREDENTIAL_MISSING"
|
||||||
|
else:
|
||||||
|
raise AssertionError("Missing credential should fail before the provider request")
|
||||||
|
|
||||||
|
|
||||||
def test_ollama_maps_models_and_completion() -> None:
|
def test_ollama_maps_models_and_completion() -> None:
|
||||||
def handler(request: httpx.Request) -> httpx.Response:
|
def handler(request: httpx.Request) -> httpx.Response:
|
||||||
if request.url.path == "/api/tags":
|
if request.url.path == "/api/tags":
|
||||||
|
|||||||
Reference in New Issue
Block a user