============================= test session starts ==============================
platform darwin -- Python 3.13.8, pytest-9.0.2, pluggy-1.6.0
rootdir: /Users/franvozzi/Documents/Workspace/tknmtr
configfile: pyproject.toml
plugins: anyio-4.12.1, asyncio-1.3.0, cov-7.0.0
asyncio: mode=Mode.AUTO, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function
collected 1 item

tests/unit/test_gateway.py F                                             [100%]

=================================== FAILURES ===================================
____________ TestGatewayChatCompletions.test_returns_200_on_success ____________

self = <tests.unit.test_gateway.TestGatewayChatCompletions object at 0x1138d8cd0>
mock_supabase = <MagicMock name='get_supabase' id='4617943568'>
mock_litellm = <MagicMock name='litellm' id='4617944240'>
mock_log = <MagicMock name='_log_gateway_usage' id='4617944576'>
client = <starlette.testclient.TestClient object at 0x113403620>

    @patch("tknmtr.gateway.router._log_gateway_usage")
    @patch("tknmtr.gateway.router.litellm")
    @patch("tknmtr.api.deps.get_supabase")
    def test_returns_200_on_success(self, mock_supabase, mock_litellm, mock_log, client):
        """Valid request with mocked auth returns 200."""
        mock_db = MagicMock()
        mock_supabase.return_value = mock_db
    
        # Mock API key lookup
        mock_db.table.return_value.select.return_value.eq.return_value.execute.return_value.data = [_mock_key_data()]
    
        # Mock LiteLLM response
        mock_litellm.acompletion = AsyncMock(return_value=_mock_litellm_response())
    
>       response = client.post(
            "/v1/chat/completions",
            json=_valid_request(),
            headers=_auth_header(),
        )

tests/unit/test_gateway.py:153: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.venv/lib/python3.13/site-packages/starlette/testclient.py:546: in post
    return super().post(
.venv/lib/python3.13/site-packages/httpx/_client.py:1144: in post
    return self.request(
.venv/lib/python3.13/site-packages/starlette/testclient.py:445: in request
    return super().request(
.venv/lib/python3.13/site-packages/httpx/_client.py:825: in request
    return self.send(request, auth=auth, follow_redirects=follow_redirects)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.13/site-packages/sentry_sdk/utils.py:1841: in runner
    return sentry_patched_function(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.13/site-packages/sentry_sdk/integrations/httpx.py:87: in send
    rv = real_send(self, request, **kwargs)
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.13/site-packages/httpx/_client.py:914: in send
    response = self._send_handling_auth(
.venv/lib/python3.13/site-packages/httpx/_client.py:942: in _send_handling_auth
    response = self._send_handling_redirects(
.venv/lib/python3.13/site-packages/httpx/_client.py:979: in _send_handling_redirects
    response = self._send_single_request(request)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.13/site-packages/httpx/_client.py:1014: in _send_single_request
    response = transport.handle_request(request)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.13/site-packages/starlette/testclient.py:348: in handle_request
    raise exc
.venv/lib/python3.13/site-packages/starlette/testclient.py:345: in handle_request
    portal.call(self.app, scope, receive, send)
.venv/lib/python3.13/site-packages/anyio/from_thread.py:334: in call
    return cast(T_Retval, self.start_task_soon(func, *args).result())
                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/homebrew/Cellar/python@3.13/3.13.8/Frameworks/Python.framework/Versions/3.13/lib/python3.13/concurrent/futures/_base.py:456: in result
    return self.__get_result()
           ^^^^^^^^^^^^^^^^^^^
/opt/homebrew/Cellar/python@3.13/3.13.8/Frameworks/Python.framework/Versions/3.13/lib/python3.13/concurrent/futures/_base.py:401: in __get_result
    raise self._exception
.venv/lib/python3.13/site-packages/anyio/from_thread.py:259: in _call_func
    retval = await retval_or_awaitable
             ^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.13/site-packages/fastapi/applications.py:1135: in __call__
    await super().__call__(scope, receive, send)
.venv/lib/python3.13/site-packages/sentry_sdk/integrations/starlette.py:416: in _sentry_patched_asgi_app
    return await middleware(scope, receive, send)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.13/site-packages/sentry_sdk/integrations/asgi.py:170: in _run_asgi3
    return await self._run_app(scope, receive, send, asgi_version=3)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.13/site-packages/sentry_sdk/integrations/asgi.py:262: in _run_app
    raise exc from None
.venv/lib/python3.13/site-packages/sentry_sdk/integrations/asgi.py:257: in _run_app
    return await self.app(
.venv/lib/python3.13/site-packages/starlette/applications.py:107: in __call__
    await self.middleware_stack(scope, receive, send)
.venv/lib/python3.13/site-packages/sentry_sdk/integrations/starlette.py:164: in _create_span_call
    return await old_call(app, scope, receive, send, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.13/site-packages/starlette/middleware/errors.py:186: in __call__
    raise exc
.venv/lib/python3.13/site-packages/starlette/middleware/errors.py:164: in __call__
    await self.app(scope, receive, _send)
.venv/lib/python3.13/site-packages/sentry_sdk/integrations/starlette.py:164: in _create_span_call
    return await old_call(app, scope, receive, send, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.13/site-packages/starlette/middleware/cors.py:85: in __call__
    await self.app(scope, receive, send)
.venv/lib/python3.13/site-packages/sentry_sdk/integrations/starlette.py:303: in _sentry_exceptionmiddleware_call
    await old_call(self, scope, receive, send)
.venv/lib/python3.13/site-packages/sentry_sdk/integrations/starlette.py:164: in _create_span_call
    return await old_call(app, scope, receive, send, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.13/site-packages/starlette/middleware/exceptions.py:63: in __call__
    await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)
.venv/lib/python3.13/site-packages/starlette/_exception_handler.py:53: in wrapped_app
    raise exc
.venv/lib/python3.13/site-packages/starlette/_exception_handler.py:42: in wrapped_app
    await app(scope, receive, sender)
.venv/lib/python3.13/site-packages/sentry_sdk/integrations/starlette.py:164: in _create_span_call
    return await old_call(app, scope, receive, send, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.13/site-packages/fastapi/middleware/asyncexitstack.py:18: in __call__
    await self.app(scope, receive, send)
.venv/lib/python3.13/site-packages/starlette/routing.py:716: in __call__
    await self.middleware_stack(scope, receive, send)
.venv/lib/python3.13/site-packages/starlette/routing.py:736: in app
    await route.handle(scope, receive, send)
.venv/lib/python3.13/site-packages/starlette/routing.py:290: in handle
    await self.app(scope, receive, send)
.venv/lib/python3.13/site-packages/fastapi/routing.py:115: in app
    await wrap_app_handling_exceptions(app, request)(scope, receive, send)
.venv/lib/python3.13/site-packages/starlette/_exception_handler.py:53: in wrapped_app
    raise exc
.venv/lib/python3.13/site-packages/starlette/_exception_handler.py:42: in wrapped_app
    await app(scope, receive, sender)
.venv/lib/python3.13/site-packages/fastapi/routing.py:101: in app
    response = await f(request)
               ^^^^^^^^^^^^^^^^
.venv/lib/python3.13/site-packages/sentry_sdk/integrations/fastapi.py:132: in _sentry_app
    return await old_app(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.13/site-packages/fastapi/routing.py:355: in app
    raw_response = await run_endpoint_function(
.venv/lib/python3.13/site-packages/fastapi/routing.py:243: in run_endpoint_function
    return await dependant.call(**values)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

request = ChatCompletionRequest(model='gpt-4o', messages=[ChatMessage(role='user', content='Hello', name=None)], temperature=None, top_p=None, n=None, stream=False, max_tokens=None, presence_penalty=None, frequency_penalty=None, stop=None, user=None)
key_data = {'id': 'key-123', 'is_active': True, 'secret_hash': 'abc123', 'user_id': 'user-456'}
db = <MagicMock name='get_supabase()' id='4623335504'>
background_tasks = <fastapi.background.BackgroundTasks object at 0x11392b380>

    @router.post("/v1/chat/completions", response_model=None)
    async def chat_completions(
        request: ChatCompletionRequest,
        key_data: Annotated[dict[str, Any], Depends(verify_bearer_token)],
        db: Annotated[Client, Depends(get_db)],
        background_tasks: BackgroundTasks,
    ) -> ChatCompletionResponse | StreamingResponse:
        """
        OpenAI-compatible Chat Completions endpoint.
    
        Drop-in replacement — change your `base_url` to `https://api.tknmtr.com`
        and use your TKNMTR API key as the Bearer token.
        """
        try:
            # Credit Check
            user_id = key_data["user_id"]
            # Use sync execute since we are in async def but supabase-py might be sync?
            # Actually supabase-py 'execute()' is sync usually unless using async client.
            # But 'db' is likely a sync client or we need to await if it's async.
            # Let's assume standard supabase-py usage which is sync blocking (in threadpool via FastAPI default)
            # or we might need to be careful.
            # Given 'db: Client', it is likely the sync client.
    
            profile_resp = (
                db.table("profiles").select("billing_credits").eq("id", user_id).single().execute()
            )
            credits = 0.0
            if profile_resp.data:
                profile_data = cast(dict[str, Any], profile_resp.data)
                credits = float(profile_data.get("billing_credits", 0.0) or 0.0)
    
            if credits <= 0.0:
                raise HTTPException(
                    status_code=402,
                    detail="Insufficient credits. Please top up your account to use the TKNMTR Gateway.",
                )
    
            # Semantic Routing
            if request.model == "tknmtr-auto":
                # Convert Pydantic messages to dict list for router
                msgs = [{"role": str(m.role), "content": str(m.content or "")} for m in request.messages]
                target_model = await semantic_router.route(msgs)
                logger.info(f"Routed request to {target_model}")
                request.model = target_model
    
            # Prompt Compression
            tokens_original = 0
            tokens_optimized = 0
            last_user_msg_idx = -1
    
            for i in range(len(request.messages) - 1, -1, -1):
                if request.messages[i].role == "user" and request.messages[i].content:
                    last_user_msg_idx = i
                    break
    
            if last_user_msg_idx != -1:
                msg_content = request.messages[last_user_msg_idx].content or ""
                tokens_original = litellm.token_counter(model=request.model, text=msg_content)
    
                compressor = DeterministicCompressor()
                compressed_content = compressor.compress(msg_content)
    
                tokens_optimized = litellm.token_counter(model=request.model, text=compressed_content)
                request.messages[last_user_msg_idx].content = compressed_content
    
            tknmtr_meta = TKNMTRMeta(
                tokens_original=tokens_original,
                tokens_optimized=tokens_optimized,
                tokens_saved=max(0, tokens_original - tokens_optimized),
                savings_pct=round(max(0, tokens_original - tokens_optimized) / tokens_original * 100, 1) if tokens_original > 0 else 0.0,
                model_routed=request.model,
            )
    
            # Build kwargs for LiteLLM
            completion_kwargs = _build_litellm_kwargs(request)
    
            if request.stream:
                return await _handle_streaming(
                    completion_kwargs, request.model, key_data, db, background_tasks, tknmtr_meta
                )
            else:
                return await _handle_non_streaming(
                    completion_kwargs, request.model, key_data, db, background_tasks, tknmtr_meta
                )
    
        except HTTPException:
            raise
>       except litellm.exceptions.AuthenticationError as e:
E       TypeError: catching classes that do not inherit from BaseException is not allowed

src/tknmtr/gateway/router.py:136: TypeError
=============================== warnings summary ===============================
.venv/lib/python3.13/site-packages/pyiceberg/expressions/parser.py:72
  /Users/franvozzi/Documents/Workspace/tknmtr/.venv/lib/python3.13/site-packages/pyiceberg/expressions/parser.py:72: PyparsingDeprecationWarning: 'enablePackrat' deprecated - use 'enable_packrat'
    ParserElement.enablePackrat()

.venv/lib/python3.13/site-packages/pyiceberg/expressions/parser.py:85
  /Users/franvozzi/Documents/Workspace/tknmtr/.venv/lib/python3.13/site-packages/pyiceberg/expressions/parser.py:85: PyparsingDeprecationWarning: 'escChar' argument is deprecated, use 'esc_char'
    quoted_identifier = QuotedString('"', escChar="\\", unquoteResults=True)

.venv/lib/python3.13/site-packages/pyiceberg/expressions/parser.py:85
  /Users/franvozzi/Documents/Workspace/tknmtr/.venv/lib/python3.13/site-packages/pyiceberg/expressions/parser.py:85: PyparsingDeprecationWarning: 'unquoteResults' argument is deprecated, use 'unquote_results'
    quoted_identifier = QuotedString('"', escChar="\\", unquoteResults=True)

.venv/lib/python3.13/site-packages/pyiceberg/table/metadata.py:365
  /Users/franvozzi/Documents/Workspace/tknmtr/.venv/lib/python3.13/site-packages/pyiceberg/table/metadata.py:365: PydanticDeprecatedSince212: Using `@model_validator` with mode='after' on a classmethod is deprecated. Instead, use an instance method. See the documentation at https://docs.pydantic.dev/2.12/concepts/validators/#model-after-validator. Deprecated in Pydantic V2.12 to be removed in V3.0.
    @model_validator(mode="after")

.venv/lib/python3.13/site-packages/pyiceberg/table/metadata.py:494
  /Users/franvozzi/Documents/Workspace/tknmtr/.venv/lib/python3.13/site-packages/pyiceberg/table/metadata.py:494: PydanticDeprecatedSince212: Using `@model_validator` with mode='after' on a classmethod is deprecated. Instead, use an instance method. See the documentation at https://docs.pydantic.dev/2.12/concepts/validators/#model-after-validator. Deprecated in Pydantic V2.12 to be removed in V3.0.
    @model_validator(mode="after")

.venv/lib/python3.13/site-packages/pyiceberg/table/metadata.py:498
  /Users/franvozzi/Documents/Workspace/tknmtr/.venv/lib/python3.13/site-packages/pyiceberg/table/metadata.py:498: PydanticDeprecatedSince212: Using `@model_validator` with mode='after' on a classmethod is deprecated. Instead, use an instance method. See the documentation at https://docs.pydantic.dev/2.12/concepts/validators/#model-after-validator. Deprecated in Pydantic V2.12 to be removed in V3.0.
    @model_validator(mode="after")

.venv/lib/python3.13/site-packages/pyiceberg/table/metadata.py:502
  /Users/franvozzi/Documents/Workspace/tknmtr/.venv/lib/python3.13/site-packages/pyiceberg/table/metadata.py:502: PydanticDeprecatedSince212: Using `@model_validator` with mode='after' on a classmethod is deprecated. Instead, use an instance method. See the documentation at https://docs.pydantic.dev/2.12/concepts/validators/#model-after-validator. Deprecated in Pydantic V2.12 to be removed in V3.0.
    @model_validator(mode="after")

.venv/lib/python3.13/site-packages/pyiceberg/table/metadata.py:506
  /Users/franvozzi/Documents/Workspace/tknmtr/.venv/lib/python3.13/site-packages/pyiceberg/table/metadata.py:506: PydanticDeprecatedSince212: Using `@model_validator` with mode='after' on a classmethod is deprecated. Instead, use an instance method. See the documentation at https://docs.pydantic.dev/2.12/concepts/validators/#model-after-validator. Deprecated in Pydantic V2.12 to be removed in V3.0.
    @model_validator(mode="after")

.venv/lib/python3.13/site-packages/pyiceberg/table/metadata.py:538
  /Users/franvozzi/Documents/Workspace/tknmtr/.venv/lib/python3.13/site-packages/pyiceberg/table/metadata.py:538: PydanticDeprecatedSince212: Using `@model_validator` with mode='after' on a classmethod is deprecated. Instead, use an instance method. See the documentation at https://docs.pydantic.dev/2.12/concepts/validators/#model-after-validator. Deprecated in Pydantic V2.12 to be removed in V3.0.
    @model_validator(mode="after")

.venv/lib/python3.13/site-packages/pyiceberg/table/metadata.py:542
  /Users/franvozzi/Documents/Workspace/tknmtr/.venv/lib/python3.13/site-packages/pyiceberg/table/metadata.py:542: PydanticDeprecatedSince212: Using `@model_validator` with mode='after' on a classmethod is deprecated. Instead, use an instance method. See the documentation at https://docs.pydantic.dev/2.12/concepts/validators/#model-after-validator. Deprecated in Pydantic V2.12 to be removed in V3.0.
    @model_validator(mode="after")

.venv/lib/python3.13/site-packages/pyiceberg/table/metadata.py:546
  /Users/franvozzi/Documents/Workspace/tknmtr/.venv/lib/python3.13/site-packages/pyiceberg/table/metadata.py:546: PydanticDeprecatedSince212: Using `@model_validator` with mode='after' on a classmethod is deprecated. Instead, use an instance method. See the documentation at https://docs.pydantic.dev/2.12/concepts/validators/#model-after-validator. Deprecated in Pydantic V2.12 to be removed in V3.0.
    @model_validator(mode="after")

.venv/lib/python3.13/site-packages/pyiceberg/table/metadata.py:550
  /Users/franvozzi/Documents/Workspace/tknmtr/.venv/lib/python3.13/site-packages/pyiceberg/table/metadata.py:550: PydanticDeprecatedSince212: Using `@model_validator` with mode='after' on a classmethod is deprecated. Instead, use an instance method. See the documentation at https://docs.pydantic.dev/2.12/concepts/validators/#model-after-validator. Deprecated in Pydantic V2.12 to be removed in V3.0.
    @model_validator(mode="after")

-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/unit/test_gateway.py::TestGatewayChatCompletions::test_returns_200_on_success
======================== 1 failed, 12 warnings in 7.00s ========================
