Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ dependencies = [
"pydantic>=2.12.4",
"fastapi>=0.121.2",
"uvicorn>=0.38.0",
"openai>=2.21.0",
"httpx>=0.28",
]
# Python 3.9 is the minimum version available in RHEL 9, our lowest target.
requires-python = ">=3.9"
Expand Down Expand Up @@ -59,7 +61,6 @@ lint = [
]

test = [
"httpx>=0.28",
"pytest-asyncio>=0.25",
"pytest-cov>=7.0.0",
"pytest>=8.0",
Expand Down
45 changes: 17 additions & 28 deletions src/goose_proxy/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import json
import logging
import urllib.error
import typing as t

import openai

from fastapi import FastAPI
from fastapi import HTTPException
Expand Down Expand Up @@ -43,40 +44,28 @@ def _http_exception_handler(_: Request, exc: Exception) -> JSONResponse:
)


def _http_error_handler(_: Request, exc: Exception) -> JSONResponse:
assert isinstance(exc, urllib.error.HTTPError)
body = exc.read().decode()

logger.debug(
"Backend HTTP error\n\tURL: %s\n\tResponse status: %s %s\n\tResponse headers: %s\n\tResponse body: %s",
exc.url,
exc.code,
exc.reason,
dict(exc.headers) if exc.headers else {},
body,
)

try:
data = json.loads(body)
message = data.get("error", {}).get("message", str(exc))
except (json.JSONDecodeError, ValueError):
message = body or str(exc)
def _api_status_error_handler(_: Request, exc: Exception) -> JSONResponse:
assert isinstance(exc, openai.APIStatusError)
message = str(exc)
body = t.cast(dict[str, t.Any], exc.body) if isinstance(exc.body, dict) else None
if body:
error_info = body.get("error")
if isinstance(error_info, dict) and "message" in error_info:
message = error_info["message"]

return _openai_error_response(
status_code=exc.code,
status_code=exc.status_code,
message=message,
error_type="api_error",
)


def _url_error_handler(_: Request, exc: Exception) -> JSONResponse:
assert isinstance(exc, urllib.error.URLError)

logger.debug("Backend connection error: %s", exc.reason)
def _api_connection_error_handler(_: Request, exc: Exception) -> JSONResponse:
assert isinstance(exc, openai.APIConnectionError)

return _openai_error_response(
status_code=502,
message=str(exc.reason),
message=exc.message,
error_type="api_error",
)

Expand All @@ -98,6 +87,6 @@ def _cert_error_handler(_: Request, exc: Exception) -> JSONResponse:

def register_exception_handlers(app: FastAPI) -> None:
app.add_exception_handler(HTTPException, _http_exception_handler)
app.add_exception_handler(urllib.error.HTTPError, _http_error_handler)
app.add_exception_handler(urllib.error.URLError, _url_error_handler)
app.add_exception_handler(openai.APIStatusError, _api_status_error_handler)
app.add_exception_handler(openai.APIConnectionError, _api_connection_error_handler)
app.add_exception_handler(CertificateInitializationError, _cert_error_handler)
170 changes: 0 additions & 170 deletions src/goose_proxy/models/responses.py

This file was deleted.

12 changes: 6 additions & 6 deletions src/goose_proxy/translators/response.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,17 @@

import typing as t

from openai.types.responses import Response
from openai.types.responses import ResponseFunctionToolCall
from openai.types.responses import ResponseOutputMessage
from openai.types.responses import ResponseOutputText

from goose_proxy.models.chat import ChatCompletion
from goose_proxy.models.chat import ChatCompletionMessageToolCall
from goose_proxy.models.chat import ChatCompletionResponseMessage
from goose_proxy.models.chat import Choice
from goose_proxy.models.chat import CompletionUsage
from goose_proxy.models.chat import Function
from goose_proxy.models.responses import OutputItem
from goose_proxy.models.responses import Response
from goose_proxy.models.responses import ResponseFunctionToolCall
from goose_proxy.models.responses import ResponseOutputMessage
from goose_proxy.models.responses import ResponseOutputText


def _extract_text(output_message: ResponseOutputMessage) -> t.Optional[str]:
Expand All @@ -25,7 +25,7 @@ def _extract_text(output_message: ResponseOutputMessage) -> t.Optional[str]:


def _extract_tool_calls(
output: t.List[OutputItem],
output: list,
) -> t.Optional[t.List[ChatCompletionMessageToolCall]]:
"""Extract tool calls from the response output items."""
tool_calls = []
Expand Down
28 changes: 14 additions & 14 deletions src/goose_proxy/translators/streaming.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@
import time
import typing as t

from collections.abc import Iterator
from collections.abc import AsyncIterator

from goose_proxy.models.responses import ResponseCompletedEvent
from goose_proxy.models.responses import ResponseCreatedEvent
from goose_proxy.models.responses import ResponseFunctionCallArgumentsDeltaEvent
from goose_proxy.models.responses import ResponseFunctionToolCall
from goose_proxy.models.responses import ResponseOutputItemAddedEvent
from goose_proxy.models.responses import ResponseOutputMessage
from goose_proxy.models.responses import ResponseTextDeltaEvent
from goose_proxy.models.responses import StreamEvent
from openai.types.responses import ResponseCompletedEvent
from openai.types.responses import ResponseCreatedEvent
from openai.types.responses import ResponseFunctionCallArgumentsDeltaEvent
from openai.types.responses import ResponseFunctionToolCall
from openai.types.responses import ResponseOutputItemAddedEvent
from openai.types.responses import ResponseOutputMessage
from openai.types.responses import ResponseStreamEvent
from openai.types.responses import ResponseTextDeltaEvent


def _make_chunk(
Expand Down Expand Up @@ -95,14 +95,14 @@ def _translate_usage(event: ResponseCompletedEvent) -> t.Optional[dict[str, int]
}


def translate_stream(
stream: Iterator[StreamEvent],
async def translate_stream(
stream: AsyncIterator[ResponseStreamEvent],
model: t.Optional[str],
) -> Iterator[str]:
) -> AsyncIterator[str]:
"""Translate Responses API stream events into Chat Completions SSE lines.

Args:
stream: Iterator of streaming event objects from the backend.
stream: Async iterator of streaming event objects from the backend.
model: The original model name from the request, or None to use the response model.

Yields:
Expand All @@ -115,7 +115,7 @@ def translate_stream(
has_tool_calls = False
sent_role = False

for event in stream:
async for event in stream:
if isinstance(event, ResponseCreatedEvent):
request_id = event.response.id
created = int(event.response.created_at)
Expand Down
Loading
Loading