agy تنها ارائهدهندهٔ مدل؛ حذف بخش ارائهدهندهها و افزودن زنجیرهٔ انتخاب مدل
- حذف کامل providerهای mock و openai_compatible: دامنه، آداپوترها، رجیستری، کانفیگ، seed و رابطٔ مدیریت - بخش «ارائهدهندهها» از کنسول حذف شد؛ ورود اکانتها فقط از صفحهٔ اکانتهای agy - صفحهٔ مدلها بازطراحی شد: ارائهدهنده → اکانت → مدل (کاتالوگ لحظهای و زندهٔ همان اکانت از Google) → افورت استدلال؛ بدون هیچ ورود دستی نام مدل - مدلها ستون reasoning_effort گرفتند؛ آداپتور آن را به thinkingConfig (بودجهٔ ۱۰۲۴ تا ۳۲۷۶۸) ترجمه میکند و فقط به مدلهای استدلالی میفرستد - مهاجرت c2d3e4f5b6a7: افزودن reasoning_effort و حذف ردیفهای providerهای قدیمی - تستها: ۲۴۶ تست بکاند و ۵۱ تست فرانت سبز؛ ruff و mypy و tsc و eslint تمیز
This commit is contained in:
@@ -3,7 +3,9 @@
|
||||
Two properties matter more than any feature here:
|
||||
|
||||
1. A normal customer can never reach these endpoints.
|
||||
2. A provider API key can never come back out of the API.
|
||||
2. Model identity and account material stay inside the console — a customer
|
||||
never sees a raw model, and nothing that could act as a credential (a Google
|
||||
refresh token, an API key) ever comes back out.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -12,146 +14,92 @@ from httpx import AsyncClient
|
||||
|
||||
from tests.conftest import auth_header, signup
|
||||
|
||||
SECRET_KEY = "sk-super-secret-value-do-not-leak-1234567890"
|
||||
|
||||
|
||||
async def test_normal_user_cannot_list_providers(client: AsyncClient):
|
||||
async def test_normal_user_cannot_list_models(client: AsyncClient):
|
||||
token = (await signup(client, "peon@example.com"))["access_token"]
|
||||
response = await client.get("/api/admin/providers", headers=auth_header(token))
|
||||
response = await client.get("/api/admin/models", headers=auth_header(token))
|
||||
assert response.status_code == 403
|
||||
assert response.json()["error"]["code"] == "permission_denied"
|
||||
|
||||
|
||||
async def test_normal_user_cannot_create_provider(client: AsyncClient):
|
||||
async def test_normal_user_cannot_create_model(client: AsyncClient):
|
||||
token = (await signup(client, "peon2@example.com"))["access_token"]
|
||||
response = await client.post(
|
||||
"/api/admin/providers",
|
||||
json={"name": "evil", "type": "mock", "base_url": ""},
|
||||
"/api/admin/models",
|
||||
json={"internal_name": "evil-model"},
|
||||
headers=auth_header(token),
|
||||
)
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
async def test_admin_endpoints_require_authentication(client: AsyncClient):
|
||||
response = await client.get("/api/admin/providers")
|
||||
response = await client.get("/api/admin/models")
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
async def test_platform_owner_can_create_and_list_provider(
|
||||
async def test_model_creation_resolves_the_agy_provider_itself(
|
||||
client: AsyncClient, platform_owner_token: str
|
||||
):
|
||||
"""The provider is not a client choice: agy is the only one."""
|
||||
created = await client.post(
|
||||
"/api/admin/providers",
|
||||
json={"name": "openrouter", "type": "openrouter", "base_url": "https://openrouter.ai/api/v1"},
|
||||
"/api/admin/models",
|
||||
json={
|
||||
"internal_name": "gemini-3-pro-high",
|
||||
"capabilities": ["text", "reasoning"],
|
||||
"input_price_per_1m": 0.3,
|
||||
"output_price_per_1m": 1.2,
|
||||
"reasoning_effort": "high",
|
||||
},
|
||||
headers=auth_header(platform_owner_token),
|
||||
)
|
||||
assert created.status_code == 201, created.text
|
||||
assert created.json()["name"] == "openrouter"
|
||||
body = created.json()
|
||||
assert body["internal_name"] == "gemini-3-pro-high"
|
||||
# The display name falls back to the model id; nothing is typed twice.
|
||||
assert body["display_name_for_admin"] == "gemini-3-pro-high"
|
||||
assert body["reasoning_effort"] == "high"
|
||||
assert body["provider_id"], "the agy provider must be resolved server-side"
|
||||
|
||||
listing = await client.get("/api/admin/providers", headers=auth_header(platform_owner_token))
|
||||
listing = await client.get("/api/admin/models", headers=auth_header(platform_owner_token))
|
||||
assert listing.status_code == 200
|
||||
assert [p["name"] for p in listing.json()] == ["openrouter"]
|
||||
assert [m["internal_name"] for m in listing.json()] == ["gemini-3-pro-high"]
|
||||
|
||||
|
||||
async def test_duplicate_provider_name_is_rejected(client: AsyncClient, platform_owner_token: str):
|
||||
payload = {"name": "dupe", "type": "mock", "base_url": ""}
|
||||
async def test_duplicate_model_name_is_rejected(client: AsyncClient, platform_owner_token: str):
|
||||
payload = {"internal_name": "claude-sonnet-4-5-thinking", "reasoning_effort": "high"}
|
||||
first = await client.post(
|
||||
"/api/admin/providers", json=payload, headers=auth_header(platform_owner_token)
|
||||
"/api/admin/models", json=payload, headers=auth_header(platform_owner_token)
|
||||
)
|
||||
assert first.status_code == 201
|
||||
second = await client.post(
|
||||
"/api/admin/providers", json=payload, headers=auth_header(platform_owner_token)
|
||||
"/api/admin/models", json=payload, headers=auth_header(platform_owner_token)
|
||||
)
|
||||
assert second.status_code == 409
|
||||
|
||||
|
||||
async def test_credential_creation_returns_only_a_masked_key(
|
||||
async def test_unknown_reasoning_effort_is_rejected(
|
||||
client: AsyncClient, platform_owner_token: str
|
||||
):
|
||||
provider = (
|
||||
await client.post(
|
||||
"/api/admin/providers",
|
||||
json={"name": "p1", "type": "openai_compatible", "base_url": "https://x.test/v1"},
|
||||
headers=auth_header(platform_owner_token),
|
||||
)
|
||||
).json()
|
||||
|
||||
response = await client.post(
|
||||
f"/api/admin/providers/{provider['id']}/credentials",
|
||||
json={"name": "primary", "api_key": SECRET_KEY},
|
||||
"/api/admin/models",
|
||||
json={"internal_name": "gemini-3-flash", "reasoning_effort": "ultra"},
|
||||
headers=auth_header(platform_owner_token),
|
||||
)
|
||||
assert response.status_code == 201, response.text
|
||||
body = response.json()
|
||||
assert SECRET_KEY not in str(body)
|
||||
assert body["masked_key"].startswith("•")
|
||||
assert body["masked_key"].endswith(SECRET_KEY[-4:])
|
||||
assert response.status_code == 422
|
||||
|
||||
|
||||
async def test_listing_credentials_never_exposes_the_key(
|
||||
async def test_agy_account_and_model_endpoints_are_owner_only(
|
||||
client: AsyncClient, platform_owner_token: str
|
||||
):
|
||||
provider = (
|
||||
await client.post(
|
||||
"/api/admin/providers",
|
||||
json={"name": "p2", "type": "openai_compatible", "base_url": "https://y.test/v1"},
|
||||
headers=auth_header(platform_owner_token),
|
||||
)
|
||||
).json()
|
||||
await client.post(
|
||||
f"/api/admin/providers/{provider['id']}/credentials",
|
||||
json={"name": "primary", "api_key": SECRET_KEY},
|
||||
headers=auth_header(platform_owner_token),
|
||||
)
|
||||
|
||||
"""The account catalog is owner-only and never carries token material."""
|
||||
token = (await signup(client, "peon3@example.com"))["access_token"]
|
||||
response = await client.get(
|
||||
f"/api/admin/providers/{provider['id']}/credentials",
|
||||
headers=auth_header(platform_owner_token),
|
||||
"/api/admin/agy/accounts/whatever/models", headers=auth_header(token)
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert SECRET_KEY not in response.text
|
||||
assert response.json()[0]["masked_key"] == "••••••"
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
async def test_model_creation_and_listing(client: AsyncClient, platform_owner_token: str):
|
||||
provider = (
|
||||
await client.post(
|
||||
"/api/admin/providers",
|
||||
json={"name": "p3", "type": "mock", "base_url": ""},
|
||||
headers=auth_header(platform_owner_token),
|
||||
)
|
||||
).json()
|
||||
|
||||
created = await client.post(
|
||||
"/api/admin/models",
|
||||
json={
|
||||
"provider_id": provider["id"],
|
||||
"internal_name": "internal-coder-v1",
|
||||
"display_name_for_admin": "Coder V1",
|
||||
"capabilities": ["text", "code"],
|
||||
"input_price_per_1m": 0.3,
|
||||
"output_price_per_1m": 1.2,
|
||||
},
|
||||
headers=auth_header(platform_owner_token),
|
||||
)
|
||||
assert created.status_code == 201, created.text
|
||||
|
||||
listing = await client.get("/api/admin/models", headers=auth_header(platform_owner_token))
|
||||
assert listing.status_code == 200
|
||||
assert listing.json()[0]["internal_name"] == "internal-coder-v1"
|
||||
|
||||
|
||||
async def test_model_requires_existing_provider(client: AsyncClient, platform_owner_token: str):
|
||||
response = await client.post(
|
||||
"/api/admin/models",
|
||||
json={
|
||||
"provider_id": "does-not-exist",
|
||||
"internal_name": "x",
|
||||
"display_name_for_admin": "X",
|
||||
},
|
||||
headers=auth_header(platform_owner_token),
|
||||
)
|
||||
assert response.status_code == 404
|
||||
unauthenticated = await client.get("/api/admin/agy/accounts/whatever/models")
|
||||
assert unauthenticated.status_code == 401
|
||||
|
||||
|
||||
async def test_routing_policy_creation_and_default_swap(
|
||||
|
||||
Reference in New Issue
Block a user