Skip to content

152: More efficient http requests processing - #185

Open
natkam wants to merge 21 commits into
mainfrom
152-http-requests-processing
Open

natkam wants to merge 21 commits into
mainfrom
152-http-requests-processing

Conversation

@natkam

@natkam natkam commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Main goal

The gateway views no longer return Django StreamingHttpResponse/JsonResponse objects directly. Instead they return lightweight "raw" response objects, containing a Pydantic model or a dict as data, and a newly added HttpResponseMiddleware middleware converts those raw objects into real HTTP responses. (Exceptions: the two cases when there is no json in the response content: the speech endpoint, which streams bytes in a StreamingHttpResponse, and transcriptions with text format, which returns an HttpResponse with text content type.)

For streaming responses, in order to avoid repeatedly iterating over the content to apply any post-processing and then creating a new async generator, the post-processing transformations are gathered in a list (RawStreamingResponse.transforms) and then applied to individual chunks in the middleware. Example: normalize_reasoning_fields() decorator.

Note: MCP streaming responses have to yield a slightly different format of data then other streaming responses, and they do not have a request_log object attached, so the middleware handles them separately from the others.

The idea here is that any post-processing of the response can be now executed on a Python object, without repeatedly converting the response's content to JSON and back. The conversion happens only once, at the very end of the response handling - in the middleware.

Other unrelated changes

  • MCP tests can now run without internet connection, providing the server package is already installed (--prefer-offline flag).
  • Removed an unused helper method for tests (mock router).

natkam added 17 commits August 24, 2026 12:28
…sponses

The goal is to avoid parsing json data multiple times if we want to do
any post-processing on the data returned from the view.
Note: MCP views still return JsonResponse, because they won't work with
the raw one; the exceptions are error responses (just like in other views).
We have rewritten the tests and don't use it any more.
For now, chat completions, completions, and embeddings streaming works.

All the operations processing the streaming content are gathered
in the RawStreamingResponse's `transforms` attribute, and applied
to individual chunks in the middleware, at the end of the response
processing. This is necessary, because otherwise the streaming content
(an async iterator) gets exhausted after one transform is applied.
Only after applying the transformations are the chunks dumped into
SSE json format.
This also allows us to avoid repeated json-loading and json-dumping
of the response content.
Without the `--prefer-offline` flag, npx always tried to access internet,
and in case there was no internet connection, the server wouldn't start,
even if it was installed locally.
…il!)

The end-to-end test in test_http_response_middleware.py passes, but
the some of the tests in the test_mcp.py module fail inconsistently
with `httpx.ReadTimeout`.
@natkam natkam linked an issue Aug 26, 2026 that may be closed by this pull request
@natkam natkam changed the title 152: More effective http requests processing 152: More efficient http requests processing Aug 26, 2026
@natkam
natkam marked this pull request as ready for review August 27, 2026 14:36
@natkam
natkam marked this pull request as draft August 27, 2026 14:43
@natkam
natkam force-pushed the 152-http-requests-processing branch from 5f055b1 to a6980ce Compare August 27, 2026 16:24
natkam added 2 commits August 28, 2026 12:59
…onse

Pydantic models are now passed in the response as they are, and the
middleware takes care of dumping them to dict.
@natkam
natkam marked this pull request as ready for review September 3, 2026 16:35
@natkam

natkam commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

@JaeYeonLee0621 Since I'm away until 23.09, please feel free to do whatever you need with this PR, especially if it blocks you from working on other tasks! Otherwise, if you have any comments, I'll apply them when I'm back :)


data = resp.model_dump(exclude_unset=True)
request_log.token_usage = _get_token_usage(data)
# TODO # data = resp.model_dump(exclude_unset=True)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we still need this comment? It looks like it might be left over from before the change.

@@ -153,7 +153,7 @@ async def vector_stores(
# Return upstream response directly (ID already matches)
response_data = remote_vs.model_dump(mode="json")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we pass remote_vs here instead? If you agree, the logic below would need the same change.

if (
not self._registered
and isinstance(chunk, ResponseCreatedEvent)
and chunk.type == "response.created"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since ResponseCreatedEvent already narrows chunk.type to response.created, is this condition still needed?

type: Literal["response.created"]

response.content = response.content.model_dump(
exclude_none=True, exclude_unset=True, mode="json"
)
else:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we change model_dump() to model_dump(mode='json') here for consistency? Same for the logic below, if that works for you.

  • mode="python" (default) : keeps native Python types
  • mode="json" : converts to JSON-compatible types

@meffmadd
meffmadd added this pull request to stack #190 September 15, 2026 07:20

@meffmadd meffmadd left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you check the feasibility of the sub-classing approach? Maybe I missed something but subclassing seams simpler to me...

def _get_token_usage(content: bytes | dict[str, Any]) -> Usage:
"""Retrieves token usage information from the response content.

class RawJsonResponse:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RawJsonResponse is intended to replace JsonResponse so why not sub-class it?

I had a quick look at the implementation and it seems like Django probably uses the content property to determine what data to send. The only problem with the current JsonResponse is that it immediately json.dumps the content. We could just rewrite the class to dump the JSON content dynamically as far as I understand it. I assume Django just accepts a sub-class as response type.

This would make the middleware obsolete and would be cleaner I think.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

More efficient request post-processing

3 participants