관리자 AI 제공자 연결 기능 추가 (claude·codex·agy·openrouter 토큰 붙여넣기 연결)
Some checks failed
API contract / OpenAPI type drift (push) Failing after 12m57s
Some checks failed
API contract / OpenAPI type drift (push) Failing after 12m57s
관리자 /admin/ai 화면에서 provider 토큰(OAuth 액세스 토큰 또는 API 키)을 발급받아 붙여넣으면 DB에 암호화 저장되고, shared-secret으로 보호되는 게이트웨이 내부 엔드포인트로 push되어 실행 중인 엔진 컨테이너에 즉시 적용된다. NAS처럼 게이트웨이가 컨테이너로 도는 환경에서 CLI·로컬 PC 의존 없이 claude_api·openai·openrouter를 연결할 수 있다. - 게이트웨이: openrouter 어댑터 신설(chat/completions), OAuth 토큰이면 Anthropic Bearer 헤더, /internal/provider-credentials GET/POST와 boot_id 기반 재동기화 - API: app.admin_provider_credential 테이블(idempotent DDL), stdlib HMAC-CTR+MAC 암호화 서비스(PROVIDER_CREDENTIAL_SECRET, 폴백 SESSION_SECRET), /admin/providers CRUD·검증 라우트 - 웹: 제공자 연결 패널(저장·검증·해제, 토큰 힌트만 표시), 엔진 선택에 OpenRouter 추가, api.gen.ts 재생성 - compose: api 서비스에 PROVIDER_CREDENTIAL_SECRET 전달(로컬·NAS)
This commit is contained in:
parent
6831a79ffb
commit
4b45d33142
22 changed files with 2088 additions and 13 deletions
90
apps/api/engine_gateway/test_provider_credentials_push.py
Normal file
90
apps/api/engine_gateway/test_provider_credentials_push.py
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
"""게이트웨이 내부 provider 자격증명 push 엔드포인트 테스트."""
|
||||
|
||||
import os
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from engine_gateway import gateway
|
||||
from engine_gateway import provider_registry
|
||||
|
||||
|
||||
class ProviderCredentialsPushTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.client = TestClient(gateway.app)
|
||||
|
||||
def tearDown(self):
|
||||
# 테스트가 흘려넣은 토큰을 프로세스 환경에서 회수한다.
|
||||
for name in (
|
||||
"ANTHROPIC_API_KEY",
|
||||
"ANTHROPIC_AUTH_TOKEN",
|
||||
"OPENAI_API_KEY",
|
||||
"AGY_API_KEY",
|
||||
"OPENROUTER_API_KEY",
|
||||
):
|
||||
os.environ.pop(name, None)
|
||||
provider_registry._PROVIDER_AUTH_KINDS.clear()
|
||||
|
||||
def test_status_reports_boot_id_and_loaded_providers(self):
|
||||
with patch.dict(os.environ, {"OPENROUTER_API_KEY": "sk-or-test-token"}, clear=False):
|
||||
response = self.client.get("/internal/provider-credentials")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
payload = response.json()
|
||||
self.assertTrue(payload["ok"])
|
||||
self.assertTrue(payload["boot_id"])
|
||||
self.assertIn("openrouter", payload["loaded_providers"])
|
||||
|
||||
def test_push_claude_oauth_token_routes_to_auth_token_env(self):
|
||||
response = self.client.post(
|
||||
"/internal/provider-credentials",
|
||||
json={
|
||||
"providers": {
|
||||
"claude": {"token": "oauth-token-abc", "auth_kind": "oauth_token"}
|
||||
}
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
payload = response.json()
|
||||
self.assertEqual(payload["applied"], ["claude"])
|
||||
self.assertEqual(os.environ.get("ANTHROPIC_AUTH_TOKEN"), "oauth-token-abc")
|
||||
self.assertIsNone(os.environ.get("ANTHROPIC_API_KEY"))
|
||||
self.assertEqual(provider_registry.get_provider_auth_kind("claude"), "oauth_token")
|
||||
|
||||
def test_push_claude_api_key_removes_auth_token_env(self):
|
||||
os.environ["ANTHROPIC_AUTH_TOKEN"] = "stale-oauth"
|
||||
response = self.client.post(
|
||||
"/internal/provider-credentials",
|
||||
json={
|
||||
"providers": {
|
||||
"claude": {"token": "sk-ant-api03-xyz", "auth_kind": "api_key"}
|
||||
}
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(os.environ.get("ANTHROPIC_API_KEY"), "sk-ant-api03-xyz")
|
||||
self.assertIsNone(os.environ.get("ANTHROPIC_AUTH_TOKEN"))
|
||||
|
||||
def test_push_openrouter_token(self):
|
||||
response = self.client.post(
|
||||
"/internal/provider-credentials",
|
||||
json={
|
||||
"providers": {
|
||||
"openrouter": {"token": "sk-or-v1-xyz", "auth_kind": "api_key"}
|
||||
}
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(os.environ.get("OPENROUTER_API_KEY"), "sk-or-v1-xyz")
|
||||
|
||||
def test_push_unknown_provider_is_ignored(self):
|
||||
response = self.client.post(
|
||||
"/internal/provider-credentials",
|
||||
json={"providers": {"nope": {"token": "x" * 16, "auth_kind": "api_key"}}},
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.json()["applied"], [])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue