agy تنها ارائه‌دهندهٔ مدل؛ حذف بخش ارائه‌دهنده‌ها و افزودن زنجیرهٔ انتخاب مدل

- حذف کامل providerهای mock و openai_compatible: دامنه، آداپوترها، رجیستری، کانفیگ، seed و رابطٔ مدیریت
- بخش «ارائه‌دهنده‌ها» از کنسول حذف شد؛ ورود اکانت‌ها فقط از صفحهٔ اکانت‌های agy
- صفحهٔ مدل‌ها بازطراحی شد: ارائه‌دهنده → اکانت → مدل (کاتالوگ لحظه‌ای و زندهٔ همان اکانت از Google) → افورت استدلال؛ بدون هیچ ورود دستی نام مدل
- مدل‌ها ستون reasoning_effort گرفتند؛ آداپتور آن را به thinkingConfig (بودجهٔ ۱۰۲۴ تا ۳۲۷۶۸) ترجمه می‌کند و فقط به مدل‌های استدلالی می‌فرستد
- مهاجرت c2d3e4f5b6a7: افزودن reasoning_effort و حذف ردیف‌های providerهای قدیمی
- تست‌ها: ۲۴۶ تست بک‌اند و ۵۱ تست فرانت سبز؛ ruff و mypy و tsc و eslint تمیز
This commit is contained in:
mjalmoudy94
2026-10-13 10:01:11 +03:30
parent a2284db4f4
commit da5a2a5d1a
29 changed files with 906 additions and 995 deletions
+110 -13
View File
@@ -1,7 +1,10 @@
"""Shared test fixtures.
Tests run against an in-memory SQLite database and the ``mock`` adapters, so the
suite needs no Postgres, no Redis and no credentials.
Tests run against an in-memory SQLite database with the real agy-shaped adapter
wiring and a fake model provider, so the suite needs no Postgres, no Redis, no
Google and no accounts. agy is the only model provider the platform talks to
(see ``app/infrastructure/adapters/registry.py``); the fake keeps completions
deterministic and offline.
"""
from __future__ import annotations
@@ -96,6 +99,76 @@ async def client(engine, session_factory):
app.dependency_overrides.clear()
# ---------------------------------------------------------------------------
# Fake model provider — the only sanctioned stand-in for agy in tests
# ---------------------------------------------------------------------------
class _FakeModelProvider:
"""Offline stand-in for the agy adapter.
The canned text must read like model output, not like a debug tag: the
provider/model identity is platform-internal and must never surface in
anything a customer can read (see the e2e smoke test).
"""
async def complete(
self,
*,
model,
messages,
system="",
temperature=0.2,
max_tokens=2048,
credential=None,
credentials=None,
):
import asyncio
import time
from app.application.ports import ModelResponse
started = time.perf_counter()
last_user = next((m.content for m in reversed(messages) if m.role == "user"), "")
prompt_tokens = sum(len(m.content.split()) for m in messages)
# A tiny sleep so latency bookkeeping runs a real code path.
await asyncio.sleep(0)
content = (
"این پاسخ آزمایشی است. "
f"درخواست دریافتی: {last_user[:280] or '—'}"
)
return ModelResponse(
content=content,
model_internal_name=model.internal_name,
input_tokens=prompt_tokens + len(system.split()),
output_tokens=len(content.split()),
latency_ms=int((time.perf_counter() - started) * 1000),
raw={},
)
async def health(self):
from app.application.ports import ModelHealth
return ModelHealth(ok=True, detail="fake agy pool")
@pytest.fixture(autouse=True)
def _fake_model_provider(monkeypatch):
"""Swap the real model adapter out of every request path.
``container`` and the health router bind ``get_model_provider`` at import
time, so both import sites are patched; without this a completion test
would open the developer's real database and reach the real Google API.
"""
from app.application import container as di_container
from app.interfaces.http.routers import health as health_router
provider = _FakeModelProvider()
monkeypatch.setattr(di_container, "get_model_provider", lambda: provider)
monkeypatch.setattr(health_router, "get_model_provider", lambda: provider)
return provider
async def signup(
client: AsyncClient, email: str, password: str = "password123", name: str = ""
) -> dict:
@@ -149,10 +222,31 @@ SYSTEM_ROUTING_POLICIES: list[tuple[str, str, bool, str]] = [
("استدلال عمیق", "reasoning", False, "برای تحلیل پیچیده."),
]
#: The mock models ``app/seed.py`` installs: (internal_name, display, capabilities).
SYSTEM_MODELS: list[tuple[str, str, list[str]]] = [
("mock-fast", "Mock Fast", ["text"]),
("mock-reasoning", "Mock Reasoning", ["text", "reasoning", "code"]),
#: The agy models ``app/seed.py`` installs: (internal_name, display,
#: capabilities, reasoning_effort). agy is the only provider, so this is the
#: whole catalogue.
SYSTEM_MODELS: list[tuple[str, str, list[str], str]] = [
("gemini-3-flash", "Gemini 3 Flash", ["text", "code", "long_context"], ""),
(
"gemini-3-pro-high",
"Gemini 3 Pro",
["text", "reasoning", "code", "vision", "long_context"],
"high",
),
("claude-sonnet-4-5", "Claude Sonnet 4.5", ["text", "code", "tools"], ""),
(
"claude-sonnet-4-5-thinking",
"Claude Sonnet 4.5 (Thinking)",
["text", "code", "reasoning"],
"high",
),
(
"claude-opus-4-5-thinking",
"Claude Opus 4.5 (Thinking)",
["text", "reasoning", "code", "long_context"],
"high",
),
("gpt-oss-120b-medium", "GPT-OSS 120B Medium", ["text", "code"], ""),
]
#: The rate cards ``app/seed.py`` installs: (unit, customer_price, internal_cost).
@@ -174,11 +268,11 @@ SYSTEM_SANDBOX_TEMPLATES: list[tuple[str, str, str]] = [
async def seeded(session_factory):
"""Install the platform-level seed rows a fresh install would have.
Agent templates, routing policies, the mock provider/models, the rate cards
and the sandbox templates are all *platform* data (no organization owns
them), so without this an employee cannot be created, a run cannot resolve a
model to route to, usage cannot be priced and no sandbox can be started —
none of which the real seeded install lacks.
Agent templates, routing policies, the agy provider with its model catalog,
the rate cards and the sandbox templates are all *platform* data (no
organization owns them), so without this an employee cannot be created, a
run cannot resolve a model to route to, usage cannot be priced and no
sandbox can be started — none of which the real seeded install lacks.
"""
from app.infrastructure.db.models import (
AgentTemplateRow,
@@ -214,9 +308,11 @@ async def seeded(session_factory):
# surfaces as a ``ProviderUnavailableError`` and a *failed* run.
provider_id = new_id()
session.add(
AiProviderRow(id=provider_id, name="mock", type="mock", base_url="", is_active=True)
AiProviderRow(
id=provider_id, name="agy", type="agy", base_url="", is_active=True
)
)
for internal, display, capabilities in SYSTEM_MODELS:
for internal, display, capabilities, effort in SYSTEM_MODELS:
session.add(
AiModelRow(
provider_id=provider_id,
@@ -225,6 +321,7 @@ async def seeded(session_factory):
capabilities=capabilities,
input_price_per_1m=0.3,
output_price_per_1m=1.2,
reasoning_effort=effort,
is_active=True,
)
)