Skip to content
Merged
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
7 changes: 7 additions & 0 deletions changelog/unreleased/qoder-unique-completion-id.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
### English

- Qoder streaming responses now carry a unique completion `id` per request, shared by every chunk of that request, so clients no longer merge replies from different requests.

### 中文

- Qoder 流式响应现在每个请求使用唯一的 completion `id`,同一请求的所有分片保持一致,客户端不再把不同请求的回复合并在一起。
62 changes: 62 additions & 0 deletions internal/providers/qoder/completion_id_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package qoder

import (
"bytes"
"encoding/json"
"strings"
"testing"
)

func completionIDs(t *testing.T, stream string) []string {
t.Helper()
var output bytes.Buffer
if err := writeOpenAIChunks(&output, strings.NewReader(stream), "synthetic-model"); err != nil {
t.Fatal(err)
}
var ids []string
for _, line := range strings.Split(output.String(), "\n") {
data, ok := strings.CutPrefix(line, "data: ")
if !ok || data == "[DONE]" {
continue
}
var chunk struct {
ID string `json:"id"`
}
if err := json.Unmarshal([]byte(data), &chunk); err != nil {
t.Fatalf("decode %q: %v", data, err)
}
ids = append(ids, chunk.ID)
}
return ids
}

// Regression for #292: every request gets its own completion id, shared by
// all of its chunks including the synthesized terminal and usage chunks.
func TestWriteOpenAIChunksUsesOneUniqueIDPerRequest(t *testing.T) {
cases := map[string]string{
// No upstream finish: a terminal chunk is synthesized.
"synthesized_finish": "data: " + `{"choices":[{"delta":{"content":"hi"}}]}` + "\n\n",
// Finish without usage, then usage: a trailing usage chunk is synthesized.
"synthesized_usage": "data: " + `{"choices":[{"delta":{"content":"hi"}}],"usage":{"prompt_tokens":1,"completion_tokens":1,"total_tokens":2}}` + "\n\n" +
"data: " + `{"choices":[{"delta":{"content":"!"},"finish_reason":"stop"}]}` + "\n\n",
}
for name, stream := range cases {
t.Run(name, func(t *testing.T) {
first := completionIDs(t, stream)
second := completionIDs(t, stream)
if len(first) < 2 {
t.Fatalf("expected synthesized chunk, got ids %v", first)
}
for _, ids := range [][]string{first, second} {
for _, id := range ids {
if id != ids[0] || !strings.HasPrefix(id, "chatcmpl-") || id == "chatcmpl-qoder" {
t.Fatalf("inconsistent or fixed ids %v", ids)
}
}
}
if first[0] == second[0] {
t.Fatalf("two requests share completion id %q", first[0])
}
})
}
}
17 changes: 14 additions & 3 deletions internal/providers/qoder/direct.go
Original file line number Diff line number Diff line change
Expand Up @@ -819,7 +819,18 @@ func (b *convertedBody) Close() error {
return b.closeUpstream()
}

// newCompletionID returns one OpenAI completion id per request. Clients that
// merge assistant chunks by id must not see two requests share one (#292).
func newCompletionID() string {
id, err := randomUUID()
if err != nil {
return fmt.Sprintf("chatcmpl-qoder-%d", time.Now().UnixNano())
}
return "chatcmpl-" + strings.ReplaceAll(id, "-", "")
}

func writeOpenAIChunks(w io.Writer, src io.Reader, model string) error {
completionID := newCompletionID()
var sawFinish, sawTools, sawPayload, sawError bool
var lastUsage any
var lastChunkEmittedHadUsage bool
Expand All @@ -841,7 +852,7 @@ func writeOpenAIChunks(w io.Writer, src io.Reader, model string) error {
return err
}
chunk := map[string]any{
"id": "chatcmpl-qoder", "object": "chat.completion.chunk", "model": model,
"id": completionID, "object": "chat.completion.chunk", "model": model,
"choices": []any{map[string]any{"index": 0, "delta": deltaOf(frame), "finish_reason": emptyOrNil(frame.finish)}},
}
if len(frame.usage) > 0 {
Expand Down Expand Up @@ -881,7 +892,7 @@ func writeOpenAIChunks(w io.Writer, src io.Reader, model string) error {
finish = "tool_calls"
}
chunk := map[string]any{
"id": "chatcmpl-qoder", "object": "chat.completion.chunk", "model": model,
"id": completionID, "object": "chat.completion.chunk", "model": model,
"choices": []any{map[string]any{"index": 0, "delta": map[string]any{}, "finish_reason": finish}},
}
if lastUsage != nil {
Expand All @@ -906,7 +917,7 @@ func writeOpenAIChunks(w io.Writer, src io.Reader, model string) error {
}
} else if !lastChunkEmittedHadUsage && lastUsage != nil {
chunk := map[string]any{
"id": "chatcmpl-qoder", "object": "chat.completion.chunk", "model": model,
"id": completionID, "object": "chat.completion.chunk", "model": model,
"choices": []any{},
"usage": lastUsage,
}
Expand Down
Loading