mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-09-06 17:47:14 +00:00
0f216a0d4d
* feat(provider): support Codex subscriptions with ChatGPT sign-in * style: format Codex live integration test * fix(provider): preserve Codex identity in temporary model tests * fix(web): portal provider selector without dialog overflow * fix(web): allow native scrolling in provider dropdown * fix(provider): surface safe Codex quota and upstream errors * fix(web): provide reliable Codex copy feedback in dialogs * feat(provider): confirm cascade deletion from edit dialog * fix(persistence): discard connections after failed commit * fix(web): polish provider loading and confirmation motion --------- Co-authored-by: dadachann <185672915+dadachann@users.noreply.github.com>
101 lines
3.9 KiB
Python
101 lines
3.9 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 LLMModelsRouterGroup
|
|
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):
|
|
app = Quart(__name__)
|
|
ap = SimpleNamespace(logger=Mock(), llm_model_service=SimpleNamespace(test_llm_model=AsyncMock(side_effect=error)))
|
|
router = LLMModelsRouterGroup(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('error', [ValueError('private-value-secret'), SQLAlchemyError('private-sql-secret')])
|
|
async def test_real_model_test_route_unexpected_errors_hidden(error):
|
|
client, _ = await client_for(error)
|
|
response = await client.post('/api/v1/provider/models/llm/model/test', json={}, headers={'X-API-Key': 'synthetic'})
|
|
assert response.status_code == 500
|
|
assert 'secret' not in await response.get_data(as_text=True)
|