-
-
Notifications
You must be signed in to change notification settings - Fork 3.1k
fix(api): SSE streaming format to comply with specification #7182
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
✅ Deploy Preview for localai ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
- Replace json.Encoder with json.Marshal for explicit formatting - Use explicit \n\n for all SSE messages (instead of relying on implicit newlines) - Change %v to %s format specifier for proper string formatting - Fix error message streaming to include proper SSE format - Ensure consistency between chat.go and completion.go endpoints Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
- Handle json.Marshal errors explicitly in error response paths - Add fallback simple error message if marshal fails - Prevents sending 'data: <nil>' on marshal failures - Addresses code review feedback Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
|
@copilot try to address additionally also these points:
|
- Change FinishReason from string to *string in Choice schema - Streaming chunks now omit finish_reason (null) instead of empty string - Final chunks properly set finish_reason to "stop", "tool_calls", etc. - Remove empty content from initial streaming chunks (only send role) - Final streaming chunk sends empty delta with finish_reason - Addresses OpenAI API compliance issues causing client failures Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
All three issues have been addressed in commit deefcb0:
The streaming format now matches OpenAI's specification:
|
- Use consistent pattern: declare variable then take address - Remove inline anonymous function for better readability - Addresses code review feedback Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
core/http/endpoints/openai/chat.go
Outdated
| } | ||
| log.Error().Msgf("Stream ended with error: %v", err) | ||
|
|
||
| stopReason := "stop" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@copilot move common finish reasons to constants
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done in commit 9c0c1ae. Created constants.go with:
FinishReasonStop = "stop"FinishReasonToolCalls = "tool_calls"FinishReasonFunctionCall = "function_call"
All string literals replaced with these constants across chat.go, completion.go, and realtime.go.
- Create constants.go with FinishReasonStop, FinishReasonToolCalls, FinishReasonFunctionCall - Replace all string literals with constants in chat.go, completion.go, realtime.go - Improves code maintainability and prevents typos Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
| if marshalErr != nil { | ||
| log.Error().Msgf("Failed to marshal error response: %v", marshalErr) | ||
| // Send a simple error message as fallback | ||
| w.WriteString("data: {\"error\":\"Internal error\"}\n\n") |
Check warning
Code scanning / gosec
Errors unhandled Warning
| // Send a simple error message as fallback | ||
| w.WriteString("data: {\"error\":\"Internal error\"}\n\n") | ||
| } else { | ||
| w.WriteString(fmt.Sprintf("data: %s\n\n", respData)) |
Check warning
Code scanning / gosec
Errors unhandled Warning
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
|
@copilot we shouldn't omit finish_reason, it should be either having a value string (e.g. "stop") or null, see this example: |
- Remove omitempty from FinishReason field in Choice struct - Explicitly set FinishReason to nil for all streaming chunks - Ensures finish_reason appears as null in JSON for streaming chunks - Final chunks still properly set finish_reason to "stop", "tool_calls", etc. - Complies with OpenAI API specification example Co-authored-by: mudler <2420543+mudler@users.noreply.github.com>
... Fixed in commit 1bdc171. Removed // Streaming chunks now have:
"finish_reason": null
// Final chunk has:
"finish_reason": "stop" |
There was a problem hiding this comment.
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 refactors the FinishReason field in OpenAI API responses to use a pointer type and introduces constants for finish reason values. The changes improve API consistency and error handling in streaming responses.
Key changes:
- Changed
FinishReasonfromstringto*stringin theChoicestruct for proper OpenAI API compliance - Introduced constants (
FinishReasonStop,FinishReasonToolCalls,FinishReasonFunctionCall) to replace string literals - Improved streaming error handling with structured JSON responses
- Removed unused imports (
bytes) and cleaned up Go module dependencies
Reviewed Changes
Copilot reviewed 7 out of 8 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| core/schema/openai.go | Changed FinishReason field from string to *string for nullable support |
| core/http/endpoints/openai/constants.go | Added new file with finish reason constants |
| core/http/endpoints/openai/chat.go | Updated to use pointer type and constants; improved streaming error handling |
| core/http/endpoints/openai/completion.go | Updated to use pointer type and constants; improved streaming error handling |
| core/http/endpoints/openai/realtime.go | Updated to use pointer type and constants |
| docs/go.mod | Removed unused Hugo bootstrap dependency |
| docs/go.sum | Cleared checksums for removed dependencies |
| .github/gallery-agent/go.mod | Removed direct yaml.v3 dependency (remains as indirect) |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| FinishReason: &finishReason, | ||
| Index: 0, | ||
| Delta: &schema.Message{Content: &textContentToReturn}, | ||
| Delta: &schema.Message{}, |
Copilot
AI
Nov 9, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The final streaming chunk sends an empty Delta message, but should include the content to maintain consistency. According to OpenAI's streaming specification, the final delta with finish_reason can include an empty Delta object, but earlier in the code (line 93, 147) an initial message with role 'assistant' is sent. Consider whether the final chunk should maintain consistency with the initial message format or truly be empty. If this is intentional API behavior change, it may break client expectations.
| Delta: &schema.Message{}, | |
| Delta: &schema.Message{Role: "assistant"}, |
Fix SSE streaming format to comply with specification
Complete Solution
This PR fixes all streaming response issues causing OpenAI API client failures:
SSE Format Fixes ✅
data: {json}\n\nwith proper%sspecifierOpenAI Compliance Fixes ✅
finish_reason always present: Changed to
*stringWITHOUTomitempty"finish_reason": null(explicitly present)"finish_reason": "stop"(with value)Empty initial chunks: Initial message now only sends role
{"delta": {"role": "assistant", "content": ""}}{"delta": {"role": "assistant"}}Proper stream termination: Final chunk has empty delta with finish_reason
{"delta": {"content": ""}, "finish_reason": "stop"}{"delta": {}, "finish_reason": "stop"}+data: [DONE]Code Quality Improvements ✅
FinishReasonStop = "stop"FinishReasonToolCalls = "tool_calls"FinishReasonFunctionCall = "function_call"Streaming Flow (OpenAI Compliant)
This matches the OpenAI API specification example:
Files Modified
core/schema/openai.go: Removedomitemptyfrom FinishReason fieldcore/http/endpoints/openai/constants.go: New file with finish reason constantscore/http/endpoints/openai/chat.go: Streaming fixes + explicit nil for finish_reason + use constantscore/http/endpoints/openai/completion.go: Streaming fixes + explicit nil for finish_reason + use constantscore/http/endpoints/openai/realtime.go: Consistency updates + use constantsValidation
✅ Passes
go vetandgofmt✅ No security vulnerabilities (CodeQL)
✅ OpenAI API specification compliant
✅ Backward compatible for non-streaming responses
✅
finish_reasonnow always present (null or string value)Fixes #7180
💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.