Skip to content

feat: add streaming endpoint for inference requests - #157

Merged
doringeman merged 7 commits into
docker:mainfrom
doringeman:requests-sse
Sep 16, 2025
Merged

doringeman merged 7 commits into
docker:mainfrom
doringeman:requests-sse

Conversation

@doringeman

@doringeman doringeman commented Sep 12, 2025 •

Copy link
Copy Markdown
Contributor

Add a streaming endpoint for the requests+responses recorded by the OpenAIRecorder.

MODEL_RUNNER_PORT=8080 make run
$ curl -N http://localhost:8080/engines/requests/stream\?include_existing\=true
event: existing_request
data: {"id":"sha256:354bf30d0aa3af413d2aa5ae4f23c66d78980072d1e07a5b0d776e9606a2f0b9_1757684038418214000","model":"ai/smollm2","method":"POST","url":"/engines/v1/chat/completions","request":"{\"model\":\"ai/smollm2\",\"messages\":[{\"role\":\"user\",\"content\":\"hi\"}],\"stream\":true}","response":"{\"choices\":[{\"finish_reason\":\"stop\",\"index\":0,\"message\":{\"content\":\"Hello, I am a helpful assistant. How can I assist you today?\",\"role\":\"assistant\"}}],\"created\":1757684038,\"id\":\"chatcmpl-o6igq8UbWNbQG21cJloEbdd1TVaBqIOU\",\"model\":\"ai/smollm2\",\"object\":\"chat.completion\",\"system_fingerprint\":\"b1-c610b6c\",\"timings\":{\"predicted_ms\":84.945,\"predicted_n\":16,\"predicted_per_second\":188.35717228795104,\"predicted_per_token_ms\":5.3090625,\"prompt_ms\":35.378,\"prompt_n\":31,\"prompt_per_second\":876.250777319238,\"prompt_per_token_ms\":1.141225806451613},\"usage\":{\"completion_tokens\":16,\"prompt_tokens\":31,\"total_tokens\":47}}","timestamp":1757684038,"status_code":200,"user_agent":"docker-model-cli/dev"}

event: connected
data: {"status": "connected"}

event: new_request
data: {"id":"sha256:354bf30d0aa3af413d2aa5ae4f23c66d78980072d1e07a5b0d776e9606a2f0b9_1757684048095779000","model":"ai/smollm2","method":"POST","url":"/engines/v1/chat/completions","request":"{\"model\":\"ai/smollm2\",\"messages\":[{\"role\":\"user\",\"content\":\"hi\"}],\"stream\":true}","response":"{\"choices\":[{\"finish_reason\":\"stop\",\"index\":0,\"message\":{\"content\":\"Hello, hello! Hi there!\",\"role\":\"assistant\"}}],\"created\":1757684048,\"id\":\"chatcmpl-UV3PfQhZRAvhfsefXuaHsAMqM3sm0EZM\",\"model\":\"ai/smollm2\",\"object\":\"chat.completion\",\"system_fingerprint\":\"b1-c610b6c\",\"timings\":{\"predicted_ms\":40.469,\"predicted_n\":8,\"predicted_per_second\":197.68217648076305,\"predicted_per_token_ms\":5.058625,\"prompt_ms\":43.48,\"prompt_n\":1,\"prompt_per_second\":22.999080036798528,\"prompt_per_token_ms\":43.48},\"usage\":{\"completion_tokens\":8,\"prompt_tokens\":31,\"total_tokens\":39}}","timestamp":1757684048,"status_code":200,"user_agent":"docker-model-cli/dev"}

You can also interact with it using docker-archive-public/docker.model-cli#151.

Signed-off-by: Dorin Geman <dorin.geman@docker.com>
Comment thread pkg/inference/scheduling/scheduler.go Outdated
m["POST "+inference.InferencePrefix+"/{backend}/_configure"] = s.Configure
m["POST "+inference.InferencePrefix+"/_configure"] = s.Configure
m["GET "+inference.InferencePrefix+"/requests"] = s.openAIRecorder.GetRecordsHandler()
m["GET "+inference.InferencePrefix+"/requests/stream"] = s.openAIRecorder.StreamRequestsHandler()

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.

have you considered using same /requests endpoint?and return streams in case of "Content-Type": "text/event-stream",?

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.

Right, using an Accept: text/event-stream in the GET request might provide an easy differentiator (with no header defaulting to one-shot application/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.

Ah, it looks like you're already sending that in the CLI, so yeah, that might keep the API a bit cleaner.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good idea! Implemented in 6ad7d25. Thanks!

@ericcurtin
ericcurtin requested a review from Copilot September 15, 2025 14:12

Copilot AI left a comment

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.

Pull Request Overview

This PR adds a streaming endpoint for inference requests using Server-Sent Events (SSE) to the OpenAI recorder functionality. The change enables real-time monitoring of requests and responses recorded by the OpenAIRecorder.

Key changes:

  • Added SSE streaming support to the existing /engines/requests endpoint
  • Implemented subscriber management for broadcasting new request/response pairs
  • Added support for including existing records when subscribing to the stream

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

Comment thread pkg/metrics/openai_recorder.go Outdated
Comment thread pkg/metrics/openai_recorder.go Outdated
Comment thread pkg/metrics/openai_recorder.go Outdated

// Create subscriber channel.
subscriberID := fmt.Sprintf("sub_%d", time.Now().UnixNano())
ch := make(chan *RequestResponsePair, 100)

Copilot AI Sep 15, 2025

Copy link

Choose a reason for hiding this comment

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

The buffer size of 100 is a magic number. Consider defining this as a named constant (e.g., subscriberChannelBufferSize) to make the code more maintainable and allow for easier configuration.

Copilot uses AI. Check for mistakes.
Differentiate regular and streaming based on the Accept Header.

Signed-off-by: Dorin Geman <dorin.geman@docker.com>
Signed-off-by: Dorin Geman <dorin.geman@docker.com>
@doringeman
doringeman requested a review from Copilot September 16, 2025 12:19

Copilot AI left a comment

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.

Pull Request Overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 3 comments.


Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

Comment thread pkg/metrics/openai_recorder.go
Comment thread pkg/metrics/openai_recorder.go
Comment thread pkg/metrics/openai_recorder.go Outdated
…tion

Explains that modelRecords is expected to have size 1 due to how broadcastToSubscribers is called, avoiding the need for a second model config query.

Signed-off-by: Dorin Geman <dorin.geman@docker.com>
Signed-off-by: Dorin Geman <dorin.geman@docker.com>
@ericcurtin

Copy link
Copy Markdown
Contributor

@doringeman do you think some of the review comments are worth addressing? If not please resolve.

Signed-off-by: Dorin Geman <dorin.geman@docker.com>
@doringeman

Copy link
Copy Markdown
Contributor Author

@ericcurtin addressed them in the last 3 commits. Resolved.

@ilopezluna ilopezluna left a comment

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.

I added a question, not to change the code, but just to understand the convention.

Comment on lines +317 to +322
allRecords := r.getAllRecords()
if allRecords == nil {
// No records found.
http.Error(w, "No records found", http.StatusNotFound)
return
}

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.

There’s no need to change it, this is actually the current behavior. I’m just curious whether this is considered idiomatic in Go.
In Java, for example, a getAllRecords() method would return an empty array if no records are found, and the request would still return a 200 status code, which makes sense since the request itself succeeded.
In our case, though, the HTTP status code is being used to reflect business logic: a 404 means "no records found."
This morning, I got confused while debugging because I interpreted the 404 Not Found as if the endpoint itself didn’t exist, rather than simply meaning there were no records.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Actually you're right, it's more REST idiomatic to return an empty list and 200.
The 404 doesn't really make sense here as the endpoint and the resource exist (although it's empty).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 296301f. Thank you! 🙌


// Optional: Send existing records first.
model := req.URL.Query().Get("model")
if includeExisting := req.URL.Query().Get("include_existing"); includeExisting == "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.

nice!

Signed-off-by: Dorin Geman <dorin.geman@docker.com>
Copilot AI review requested due to automatic review settings September 16, 2025 13:14

Copilot AI left a comment

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.

Pull Request Overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 3 comments.


Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

Comment thread pkg/metrics/openai_recorder.go
Comment thread pkg/metrics/openai_recorder.go
Comment thread pkg/metrics/openai_recorder.go
@doringeman
doringeman merged commit 14e9c6c into docker:main Sep 16, 2025
1 check passed
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.

5 participants