mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-09-26 19:36:35 +08:00
273b1ea3cf
Reconcile migration branches without rewriting published revisions; retain additive Codex, monitoring, provider and platform fixes. Keep dynamic runner schemas and Host ownership, restore compatibility regressions, and preserve safe model-test error handling.
114 lines
4.4 KiB
Python
114 lines
4.4 KiB
Python
from types import SimpleNamespace
|
|
from unittest.mock import AsyncMock, Mock
|
|
|
|
import httpx
|
|
import pytest
|
|
from quart import Quart
|
|
from sqlalchemy.exc import SQLAlchemyError
|
|
|
|
from langbot.pkg.api.http.controller.groups.provider.models import (
|
|
EmbeddingModelsRouterGroup,
|
|
LLMModelsRouterGroup,
|
|
RerankModelsRouterGroup,
|
|
)
|
|
from langbot.pkg.api.http.authz import Permission
|
|
from langbot.pkg.provider.modelmgr.requesters.codex import CodexRequester
|
|
from tests.unit_tests.provider.test_codex import requester, MODEL, stream
|
|
|
|
|
|
CASES = [
|
|
(400, 400, 'codex_invalid_request'),
|
|
(401, 400, 'codex_reauthentication_required'),
|
|
(403, 403, 'codex_access_denied'),
|
|
(429, 429, 'codex_rate_limited'),
|
|
(500, 502, 'codex_upstream_failure'),
|
|
]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize('upstream,status,code', CASES)
|
|
async def test_requester_safe_error(monkeypatch, upstream, status, code):
|
|
obj = requester(monkeypatch, lambda request: httpx.Response(upstream, text='credential-secret'))
|
|
with pytest.raises(Exception) as caught:
|
|
await obj.invoke_llm(None, MODEL, [])
|
|
error = caught.value
|
|
assert getattr(error, 'status_code', None) == status
|
|
assert error.error_code == code
|
|
assert 'secret' not in str(error)
|
|
if upstream == 429:
|
|
assert 'rate limit' in str(error).lower()
|
|
assert 'usage limit reached' not in str(error).lower()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize('events', [[{'type': 'response.failed', 'error': 'credential-secret'}], []])
|
|
async def test_stream_safe_error(monkeypatch, events):
|
|
obj = requester(monkeypatch, lambda request: stream(events))
|
|
with pytest.raises(Exception) as caught:
|
|
await obj.invoke_llm(None, MODEL, [])
|
|
assert getattr(caught.value, 'status_code', None) == 502
|
|
assert caught.value.error_code == 'codex_upstream_failure'
|
|
assert 'secret' not in str(caught.value)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize(
|
|
'kind,code', [('usage_limit_reached', 'codex_usage_limit_reached'), ('unknown', 'codex_rate_limited')]
|
|
)
|
|
async def test_allowlisted_usage_error(monkeypatch, kind, code):
|
|
obj = requester(
|
|
monkeypatch,
|
|
lambda request: httpx.Response(
|
|
429, json={'error': {'type': kind, 'message': 'credential-secret', 'resets_at': 1789043289}}
|
|
),
|
|
)
|
|
with pytest.raises(Exception) as caught:
|
|
await obj.invoke_llm(None, MODEL, [])
|
|
assert caught.value.error_code == code
|
|
assert 'secret' not in str(caught.value)
|
|
|
|
|
|
async def client_for(error, model_kind='llm'):
|
|
app = Quart(__name__)
|
|
router_class, service_name, method_name = {
|
|
'llm': (LLMModelsRouterGroup, 'llm_model_service', 'test_llm_model'),
|
|
'embedding': (EmbeddingModelsRouterGroup, 'embedding_models_service', 'test_embedding_model'),
|
|
'rerank': (RerankModelsRouterGroup, 'rerank_models_service', 'test_rerank_model'),
|
|
}[model_kind]
|
|
service = SimpleNamespace(**{method_name: AsyncMock(side_effect=error)})
|
|
ap = SimpleNamespace(logger=Mock(), **{service_name: service})
|
|
router = router_class(ap, app)
|
|
router._authenticate_api_key = AsyncMock(
|
|
return_value=SimpleNamespace(
|
|
workspace_uuid='w',
|
|
workspace=SimpleNamespace(permissions=frozenset({Permission.PROVIDER_SECRET_MANAGE.value})),
|
|
)
|
|
)
|
|
await router.initialize()
|
|
return app.test_client(), ap
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize('upstream,status,code', CASES)
|
|
async def test_real_model_test_route_safe_error(upstream, status, code):
|
|
error = CodexRequester._http_error(upstream)
|
|
client, ap = await client_for(error)
|
|
response = await client.post('/api/v1/provider/models/llm/model/test', json={}, headers={'X-API-Key': 'synthetic'})
|
|
body = await response.get_json()
|
|
assert response.status_code == status
|
|
assert body['code'] == code
|
|
assert body['msg'] == str(error)
|
|
ap.logger.error.assert_not_called()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize('model_kind', ['llm', 'embedding', 'rerank'])
|
|
@pytest.mark.parametrize('error', [ValueError('private-value-secret'), SQLAlchemyError('private-sql-secret')])
|
|
async def test_real_model_test_route_unexpected_errors_hidden(error, model_kind):
|
|
client, _ = await client_for(error, model_kind)
|
|
response = await client.post(
|
|
f'/api/v1/provider/models/{model_kind}/model/test', json={}, headers={'X-API-Key': 'synthetic'}
|
|
)
|
|
assert response.status_code == 500
|
|
assert 'secret' not in await response.get_data(as_text=True)
|