- حذف کامل providerهای mock و openai_compatible: دامنه، آداپوترها، رجیستری، کانفیگ، seed و رابطٔ مدیریت - بخش «ارائهدهندهها» از کنسول حذف شد؛ ورود اکانتها فقط از صفحهٔ اکانتهای agy - صفحهٔ مدلها بازطراحی شد: ارائهدهنده → اکانت → مدل (کاتالوگ لحظهای و زندهٔ همان اکانت از Google) → افورت استدلال؛ بدون هیچ ورود دستی نام مدل - مدلها ستون reasoning_effort گرفتند؛ آداپتور آن را به thinkingConfig (بودجهٔ ۱۰۲۴ تا ۳۲۷۶۸) ترجمه میکند و فقط به مدلهای استدلالی میفرستد - مهاجرت c2d3e4f5b6a7: افزودن reasoning_effort و حذف ردیفهای providerهای قدیمی - تستها: ۲۴۶ تست بکاند و ۵۱ تست فرانت سبز؛ ruff و mypy و tsc و eslint تمیز
176 lines
6.4 KiB
Python
176 lines
6.4 KiB
Python
"""AI Control Plane: access control and the no-leak guarantee.
|
|
|
|
Two properties matter more than any feature here:
|
|
|
|
1. A normal customer can never reach these endpoints.
|
|
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
|
|
|
|
from httpx import AsyncClient
|
|
|
|
from tests.conftest import auth_header, signup
|
|
|
|
|
|
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/models", headers=auth_header(token))
|
|
assert response.status_code == 403
|
|
assert response.json()["error"]["code"] == "permission_denied"
|
|
|
|
|
|
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/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/models")
|
|
assert response.status_code == 401
|
|
|
|
|
|
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/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
|
|
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/models", headers=auth_header(platform_owner_token))
|
|
assert listing.status_code == 200
|
|
assert [m["internal_name"] for m in listing.json()] == ["gemini-3-pro-high"]
|
|
|
|
|
|
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/models", json=payload, headers=auth_header(platform_owner_token)
|
|
)
|
|
assert first.status_code == 201
|
|
second = await client.post(
|
|
"/api/admin/models", json=payload, headers=auth_header(platform_owner_token)
|
|
)
|
|
assert second.status_code == 409
|
|
|
|
|
|
async def test_unknown_reasoning_effort_is_rejected(
|
|
client: AsyncClient, platform_owner_token: str
|
|
):
|
|
response = await client.post(
|
|
"/api/admin/models",
|
|
json={"internal_name": "gemini-3-flash", "reasoning_effort": "ultra"},
|
|
headers=auth_header(platform_owner_token),
|
|
)
|
|
assert response.status_code == 422
|
|
|
|
|
|
async def test_agy_account_and_model_endpoints_are_owner_only(
|
|
client: AsyncClient, platform_owner_token: str
|
|
):
|
|
"""The account catalog is owner-only and never carries token material."""
|
|
token = (await signup(client, "peon3@example.com"))["access_token"]
|
|
response = await client.get(
|
|
"/api/admin/agy/accounts/whatever/models", headers=auth_header(token)
|
|
)
|
|
assert response.status_code == 403
|
|
|
|
unauthenticated = await client.get("/api/admin/agy/accounts/whatever/models")
|
|
assert unauthenticated.status_code == 401
|
|
|
|
|
|
async def test_routing_policy_creation_and_default_swap(
|
|
client: AsyncClient, platform_owner_token: str
|
|
):
|
|
first = await client.post(
|
|
"/api/admin/routing-policies",
|
|
json={"name": "cheap", "mode": "cheap", "is_default": True},
|
|
headers=auth_header(platform_owner_token),
|
|
)
|
|
assert first.status_code == 201
|
|
|
|
second = await client.post(
|
|
"/api/admin/routing-policies",
|
|
json={"name": "coding", "mode": "coding", "is_default": True},
|
|
headers=auth_header(platform_owner_token),
|
|
)
|
|
assert second.status_code == 201
|
|
|
|
listing = (
|
|
await client.get("/api/admin/routing-policies", headers=auth_header(platform_owner_token))
|
|
).json()
|
|
defaults = [p for p in listing if p["is_default"]]
|
|
assert len(defaults) == 1
|
|
assert defaults[0]["name"] == "coding"
|
|
|
|
|
|
async def test_rate_card_upsert_updates_in_place(client: AsyncClient, platform_owner_token: str):
|
|
payload = {
|
|
"unit": "model_input_tokens",
|
|
"name": "input",
|
|
"customer_price": 0.0000005,
|
|
"internal_cost_estimate": 0.0000002,
|
|
}
|
|
first = await client.put(
|
|
"/api/admin/rate-cards", json=payload, headers=auth_header(platform_owner_token)
|
|
)
|
|
assert first.status_code == 200, first.text
|
|
|
|
payload["customer_price"] = 0.0000009
|
|
second = await client.put(
|
|
"/api/admin/rate-cards", json=payload, headers=auth_header(platform_owner_token)
|
|
)
|
|
assert second.json()["customer_price"] == 0.0000009
|
|
|
|
listing = (
|
|
await client.get("/api/admin/rate-cards", headers=auth_header(platform_owner_token))
|
|
).json()
|
|
assert len([c for c in listing if c["unit"] == "model_input_tokens"]) == 1
|
|
|
|
|
|
async def test_platform_owner_sees_all_organizations(
|
|
client: AsyncClient, platform_owner_token: str
|
|
):
|
|
customer = (await signup(client, "tenant-x@example.com"))["access_token"]
|
|
await client.post(
|
|
"/api/organizations", json={"name": "شرکت مشتری"}, headers=auth_header(customer)
|
|
)
|
|
|
|
response = await client.get(
|
|
"/api/admin/organizations", headers=auth_header(platform_owner_token)
|
|
)
|
|
assert response.status_code == 200
|
|
assert [o["name"] for o in response.json()] == ["شرکت مشتری"]
|
|
assert response.json()[0]["member_count"] == 1
|
|
|
|
|
|
async def test_customer_cannot_see_other_organizations_through_admin(
|
|
client: AsyncClient,
|
|
):
|
|
customer = (await signup(client, "tenant-y@example.com"))["access_token"]
|
|
response = await client.get("/api/admin/organizations", headers=auth_header(customer))
|
|
assert response.status_code == 403
|