fix(llm): support opt-in OpenAI streaming#359
Conversation
|
@lizhengfeng101 Could you please review this PR when you have time? It fixes #358 by adding an opt-in SDK streaming path for OpenAI-compatible providers while preserving the existing non-streaming default. |
|
🔍 OpenCodeReview found 2 issue(s) in this PR.
|
| if chunkUsage := resolveUsage([]byte(chunk.RawJSON())); chunkUsage != nil { | ||
| usage = chunkUsage | ||
| } |
There was a problem hiding this comment.
Performance concern: resolveUsage performs a full JSON unmarshal (json.Unmarshal into map[string]any) on every streaming chunk. In typical OpenAI streaming responses, usage information only appears in the final chunk (when stream_options.include_usage is set). Parsing every chunk's raw JSON is wasteful, especially for high-volume streams with many tokens.
Consider only attempting to resolve usage from the last chunk, or at minimum checking for the presence of a "usage" key in the raw JSON string before doing the full unmarshal (e.g., a simple strings.Contains(chunk.RawJSON(), "\"usage\"") guard).
There was a problem hiding this comment.
Fixed in d5df669. Usage resolution is now gated by the SDK field metadata chunk.JSON.Usage.Valid(), so omitted or null usage chunks skip resolveUsage JSON unmarshalling while the final usage object still preserves cached-token fields. The usage fixture now exercises usage:null token chunks followed by the final usage object. Verified with the streaming tests and race detector.
| if !accumulator.AddChunk(chunk) { | ||
| return nil, fmt.Errorf("OpenAI streaming response contained inconsistent chunks") | ||
| } |
There was a problem hiding this comment.
Potential error masking: When accumulator.AddChunk(chunk) returns false, the function returns a generic "inconsistent chunks" error without first checking stream.Err(). While the SDK documents that AddChunk returns false for inconsistent data, there may be cases where an underlying stream error coincides with or causes accumulation failure. Checking stream.Err() before returning would provide better diagnostics:
if !accumulator.AddChunk(chunk) {
if err := stream.Err(); err != nil {
return nil, fmt.Errorf("OpenAI streaming error during accumulation: %w", err)
}
return nil, fmt.Errorf("OpenAI streaming response contained inconsistent chunks")
}There was a problem hiding this comment.
Kept the existing ordering after checking openai-go v3.41.0. Entering the loop means stream.Next() returned true, which only happens after the current event has decoded successfully; stream.Err() is therefore nil at this point. Decode, SSE, transport, and cancellation errors make Next() return false and are returned by the existing post-loop Err() check. AddChunk false specifically reports accumulator inconsistency, so checking Err() there would add an unreachable branch.
3f795fa to
0227157
Compare
0227157 to
d5df669
Compare
|
Addressed the two review threads in d5df669:
Local verification:
Reviewer manual gateway regression
Prerequisite: use an OpenAI-compatible gateway that rejects non-streaming Chat Completions requests but accepts the JSON boolean Set up an isolated config so the check does not read or modify the reviewer's normal OCR configuration: go build -o /tmp/ocr-pr359 ./cmd/opencodereview
tmp_home=$(mktemp -d)
export STREAM_GATEWAY_URL="https://gateway.example.com/v1"
export STREAM_GATEWAY_MODEL="<model>"
read -rsp "Gateway API key: " STREAM_GATEWAY_API_KEY; echo
HOME="$tmp_home" /tmp/ocr-pr359 config set custom_providers.stream-only.url "$STREAM_GATEWAY_URL"
HOME="$tmp_home" /tmp/ocr-pr359 config set custom_providers.stream-only.protocol openai
HOME="$tmp_home" /tmp/ocr-pr359 config set custom_providers.stream-only.model "$STREAM_GATEWAY_MODEL"
HOME="$tmp_home" /tmp/ocr-pr359 config set custom_providers.stream-only.api_key "$STREAM_GATEWAY_API_KEY"
HOME="$tmp_home" /tmp/ocr-pr359 config set provider stream-onlyNegative control — confirm the existing non-streaming path still reproduces the gateway failure: HOME="$tmp_home" /tmp/ocr-pr359 config set custom_providers.stream-only.extra_body '{"stream":false}'
HOME="$tmp_home" /tmp/ocr-pr359 llm testExpected on a stream-only gateway: the request fails with Opt-in path — enable real SSE handling: HOME="$tmp_home" /tmp/ocr-pr359 config set custom_providers.stream-only.extra_body '{"stream":true}'
HOME="$tmp_home" /tmp/ocr-pr359 llm testExpected: exit code End-to-end review on a small real commit: cd /path/to/a/test-repository
HOME="$tmp_home" /tmp/ocr-pr359 review --commit HEAD --format jsonExpected: the review completes and emits review output without the gateway Clean up the temporary config and binary: unset STREAM_GATEWAY_API_KEY
rm -rf "$tmp_home" /tmp/ocr-pr359If results are posted back to the PR, redact gateway URLs and credentials. |
Summary
This PR adds an opt-in streamed Chat Completions path for OpenAI-compatible providers whose gateways require SSE responses.
Fixes #358.
Changes
extra_body.streamis the JSON booleantruereasoning_contentinto the existingChatResponsestream_optionshttptest.Servercoverage for the streaming request and response contractsConfiguration
Streaming remains opt-in through the existing provider configuration:
The value must be a JSON boolean. A missing value,
false, or the string"true"keeps the existing non-streaming path.Non-goals
Validation
go test ./internal/llm -run 'TestOpenAIClient_(StreamOnlyGateway|Streaming.*)$' -count=1go test ./internal/llm -count=1go test ./cmd/opencodereview ./internal/llm -count=1go vet ./...go build -o /tmp/ocr-issue-358 ./cmd/opencodereviewgit diff --check origin/main...HEADgo test ./... -count=1andmake testalso pass all changed and dependent packages, includinginternal/llm. On this machine they still report the clean-origin/maininternal/toolfailures because Apple Git 2.39.3 does not supportgit grep --end-of-options(fatal: unable to resolve revision: --end-of-options).