agy تنها ارائهدهندهٔ مدل؛ حذف بخش ارائهدهندهها و افزودن زنجیرهٔ انتخاب مدل
- حذف کامل providerهای mock و openai_compatible: دامنه، آداپوترها، رجیستری، کانفیگ، seed و رابطٔ مدیریت - بخش «ارائهدهندهها» از کنسول حذف شد؛ ورود اکانتها فقط از صفحهٔ اکانتهای agy - صفحهٔ مدلها بازطراحی شد: ارائهدهنده → اکانت → مدل (کاتالوگ لحظهای و زندهٔ همان اکانت از Google) → افورت استدلال؛ بدون هیچ ورود دستی نام مدل - مدلها ستون reasoning_effort گرفتند؛ آداپتور آن را به thinkingConfig (بودجهٔ ۱۰۲۴ تا ۳۲۷۶۸) ترجمه میکند و فقط به مدلهای استدلالی میفرستد - مهاجرت c2d3e4f5b6a7: افزودن reasoning_effort و حذف ردیفهای providerهای قدیمی - تستها: ۲۴۶ تست بکاند و ۵۱ تست فرانت سبز؛ ruff و mypy و tsc و eslint تمیز
This commit is contained in:
+110
-13
@@ -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,
|
||||
)
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user