11package openai
22
33import (
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
1014const (
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
2038type chatRequest struct {
@@ -104,7 +122,7 @@ func handleChat(c *gin.Context) {
104122func 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) {
128146func 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 ,
0 commit comments