Skip to content

Commit 05e5a55

Browse files
committed
Sync from internal 2026-07-07
1 parent 83d4f93 commit 05e5a55

2 files changed

Lines changed: 94 additions & 8 deletions

File tree

src/pkg/spec/openai/chat.go

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,38 @@
11
package openai
22

33
import (
4+
"strconv"
5+
"sync/atomic"
6+
"time"
7+
48
"nexus-mock-provider/pkg/core"
59
"nexus-mock-provider/pkg/sse"
610

711
"github.com/gin-gonic/gin"
812
)
913

1014
const (
11-
completionID = "chatcmpl-mock"
1215
created int64 = 1700000000
1316
objChat = "chat.completion"
1417
objChunk = "chat.completion.chunk"
1518
roleAssistant = "assistant"
1619
)
1720

21+
// Real providers (and the rig's audit test) key per-request records by the UPSTREAM response id, so a
22+
// constant id makes clients that upsert on it (e.g. LiteLLM's SpendLogs) collapse every request into
23+
// one row. Emit a UNIQUE id per completion. Hot path is lock-free: a per-process nonce computed once
24+
// at init (keeps ids unique across restarts) + a single atomic increment — no lock, one small alloc,
25+
// negligible vs marshaling the response. Keeps the "chatcmpl-mock" prefix so the rig's health/smoke
26+
// greps (substring match) still pass.
27+
var (
28+
completionSeq uint64
29+
completionIDBase = "chatcmpl-mock-" + strconv.FormatInt(time.Now().UnixNano(), 36) + "-"
30+
)
31+
32+
func nextCompletionID() string {
33+
return completionIDBase + strconv.FormatUint(atomic.AddUint64(&completionSeq, 1), 36)
34+
}
35+
1836
// ---- request ----
1937

2038
type chatRequest struct {
@@ -104,7 +122,7 @@ func handleChat(c *gin.Context) {
104122
func writeNonStream(c *gin.Context, r core.ChatResult) {
105123
reason := string(r.FinishReason)
106124
c.JSON(200, completion{
107-
ID: completionID,
125+
ID: nextCompletionID(),
108126
Object: objChat,
109127
Created: created,
110128
Model: r.Model,
@@ -128,7 +146,11 @@ func writeNonStream(c *gin.Context, r core.ChatResult) {
128146
func streamFrames(r core.ChatResult, includeUsage bool) []sse.Frame {
129147
frames := make([]sse.Frame, 0, len(r.Tokens)+4)
130148

131-
frames = append(frames, sse.DataFrame(jsonStr(chunk(r.Model, []choice{{
149+
// One id for the WHOLE completion — every chunk of a streamed response shares it (OpenAI
150+
// semantics), generated once so it's stable across this response's frames but unique per response.
151+
id := nextCompletionID()
152+
153+
frames = append(frames, sse.DataFrame(jsonStr(chunk(id, r.Model, []choice{{
132154
Index: 0, Delta: &delta{Role: roleAssistant},
133155
}}, nil))))
134156

@@ -137,18 +159,18 @@ func streamFrames(r core.ChatResult, includeUsage bool) []sse.Frame {
137159
if i > 0 {
138160
piece = " " + tok
139161
}
140-
frames = append(frames, sse.DataFrame(jsonStr(chunk(r.Model, []choice{{
162+
frames = append(frames, sse.DataFrame(jsonStr(chunk(id, r.Model, []choice{{
141163
Index: 0, Delta: &delta{Content: piece},
142164
}}, nil))))
143165
}
144166

145167
reason := string(r.FinishReason)
146-
frames = append(frames, sse.DataFrame(jsonStr(chunk(r.Model, []choice{{
168+
frames = append(frames, sse.DataFrame(jsonStr(chunk(id, r.Model, []choice{{
147169
Index: 0, Delta: &delta{}, FinishReason: &reason,
148170
}}, nil))))
149171

150172
if includeUsage {
151-
frames = append(frames, sse.DataFrame(jsonStr(chunk(r.Model, []choice{}, &usage{
173+
frames = append(frames, sse.DataFrame(jsonStr(chunk(id, r.Model, []choice{}, &usage{
152174
PromptTokens: r.PromptTokens,
153175
CompletionTokens: r.CompletionTokens,
154176
TotalTokens: r.PromptTokens + r.CompletionTokens,
@@ -159,9 +181,9 @@ func streamFrames(r core.ChatResult, includeUsage bool) []sse.Frame {
159181
return frames
160182
}
161183

162-
func chunk(model string, choices []choice, u *usage) completion {
184+
func chunk(id, model string, choices []choice, u *usage) completion {
163185
return completion{
164-
ID: completionID,
186+
ID: id,
165187
Object: objChunk,
166188
Created: created,
167189
Model: model,
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package openai
2+
3+
import (
4+
"strings"
5+
"sync"
6+
"testing"
7+
)
8+
9+
// Every id must be unique AND carry the "chatcmpl-mock" prefix the rig's health greps rely on.
10+
func TestNextCompletionID_UniqueAndPrefixed(t *testing.T) {
11+
const n = 100_000
12+
seen := make(map[string]struct{}, n)
13+
for i := 0; i < n; i++ {
14+
id := nextCompletionID()
15+
if !strings.HasPrefix(id, "chatcmpl-mock-") {
16+
t.Fatalf("id %q missing chatcmpl-mock prefix", id)
17+
}
18+
if _, dup := seen[id]; dup {
19+
t.Fatalf("duplicate id %q at %d", id, i)
20+
}
21+
seen[id] = struct{}{}
22+
}
23+
}
24+
25+
// Lock-free counter must stay unique under heavy concurrency (run with -race).
26+
func TestNextCompletionID_ConcurrentUnique(t *testing.T) {
27+
const goroutines, per = 64, 10_000
28+
var mu sync.Mutex
29+
seen := make(map[string]struct{}, goroutines*per)
30+
var wg sync.WaitGroup
31+
for g := 0; g < goroutines; g++ {
32+
wg.Add(1)
33+
go func() {
34+
defer wg.Done()
35+
local := make([]string, per)
36+
for i := range local {
37+
local[i] = nextCompletionID()
38+
}
39+
mu.Lock()
40+
for _, id := range local {
41+
if _, dup := seen[id]; dup {
42+
t.Errorf("duplicate id %q under concurrency", id)
43+
}
44+
seen[id] = struct{}{}
45+
}
46+
mu.Unlock()
47+
}()
48+
}
49+
wg.Wait()
50+
if len(seen) != goroutines*per {
51+
t.Fatalf("expected %d unique ids, got %d", goroutines*per, len(seen))
52+
}
53+
}
54+
55+
// Hot path is a single atomic increment + one format — must stay in the low-ns range so the mock's
56+
// throughput is unaffected. (Reported as ns/op; typically well under ~50 ns on the c6i target.)
57+
func BenchmarkNextCompletionID(b *testing.B) {
58+
b.ReportAllocs()
59+
b.RunParallel(func(pb *testing.PB) {
60+
for pb.Next() {
61+
_ = nextCompletionID()
62+
}
63+
})
64+
}

0 commit comments

Comments
 (0)