inject created timestamp for OpenAI spec compatibility
OpenWebUI was failing with 'NoneType' errors because it expects a 'created' field on all chat and embedding responses. OpenAI always includes this unix-timestamp field in both streaming chunks and non-streaming responses. - Inject 'created' into non-streaming responses if the upstream didn't provide one (chat + embeddings) - Inject 'created' into every streaming SSE chunk if missing - Add 'created' to Anthropic synthetic streaming chunks and non-streaming translated responses
This commit is contained in:
@@ -90,9 +90,10 @@ func (a *API) chatCompletions(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
a.Limiter.RecordTokens(entry.Address, resp.Usage.TotalTokens)
|
||||
respBody := injectCreated(resp.Body)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(resp.StatusCode)
|
||||
_, _ = w.Write(resp.Body)
|
||||
_, _ = w.Write(respBody)
|
||||
|
||||
completion := extractCompletion(resp.Body)
|
||||
a.logRequest(r, key, entry, address, resp.StatusCode, false, resp.Usage, "", start, nil, messages, completion)
|
||||
@@ -122,6 +123,7 @@ func (a *API) streamChat(w http.ResponseWriter, r *http.Request, cc providers.Ch
|
||||
|
||||
var ttft *int
|
||||
var sb strings.Builder
|
||||
created := time.Now().Unix()
|
||||
for {
|
||||
ev, err := stream.Recv()
|
||||
if errors.Is(err, io.EOF) {
|
||||
@@ -136,7 +138,7 @@ func (a *API) streamChat(w http.ResponseWriter, r *http.Request, cc providers.Ch
|
||||
}
|
||||
// Accumulate assistant content from delta chunks.
|
||||
sb.WriteString(extractDelta(ev.Data))
|
||||
_, _ = fmt.Fprintf(w, "data: %s\n\n", ev.Data)
|
||||
_, _ = fmt.Fprintf(w, "data: %s\n\n", injectCreatedChunk(ev.Data, created))
|
||||
if flusher != nil {
|
||||
flusher.Flush()
|
||||
}
|
||||
@@ -263,3 +265,34 @@ func clientIP(r *http.Request) string {
|
||||
}
|
||||
return host
|
||||
}
|
||||
|
||||
// injectCreated ensures a non-streaming response has a "created" timestamp.
|
||||
// If the upstream already provided one, it is preserved.
|
||||
func injectCreated(body []byte) []byte {
|
||||
var obj map[string]any
|
||||
if json.Unmarshal(body, &obj) == nil {
|
||||
if _, ok := obj["created"]; ok {
|
||||
return body // upstream already set it
|
||||
}
|
||||
obj["created"] = time.Now().Unix()
|
||||
if out, err := json.Marshal(obj); err == nil {
|
||||
return out
|
||||
}
|
||||
}
|
||||
return body // fall back to raw body on any error
|
||||
}
|
||||
|
||||
// injectCreatedChunk ensures a streaming SSE chunk has a "created" timestamp.
|
||||
func injectCreatedChunk(data []byte, created int64) []byte {
|
||||
var obj map[string]any
|
||||
if json.Unmarshal(data, &obj) == nil {
|
||||
if _, ok := obj["created"]; ok {
|
||||
return data // upstream already set it
|
||||
}
|
||||
obj["created"] = created
|
||||
if out, err := json.Marshal(obj); err == nil {
|
||||
return out
|
||||
}
|
||||
}
|
||||
return data // fall back to raw chunk on any error
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ func (a *API) embeddings(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(resp.StatusCode)
|
||||
_, _ = w.Write(resp.Body)
|
||||
_, _ = w.Write(injectCreated(resp.Body))
|
||||
|
||||
a.logRequest(r, key, entry, address, resp.StatusCode, false, resp.Usage, "", start, nil, nil, "")
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"git.govekar.net/LovroG05/localmodelroutex/internal/providers"
|
||||
)
|
||||
@@ -16,6 +17,7 @@ type stream struct {
|
||||
br *bufio.Reader
|
||||
model string
|
||||
id string
|
||||
created int64
|
||||
roleSent bool
|
||||
finished bool
|
||||
finishReason string
|
||||
@@ -24,10 +26,11 @@ type stream struct {
|
||||
|
||||
func newStream(rc io.ReadCloser, model string) *stream {
|
||||
return &stream{
|
||||
rc: rc,
|
||||
br: bufio.NewReaderSize(rc, 64*1024),
|
||||
model: model,
|
||||
id: "chatcmpl-stream",
|
||||
rc: rc,
|
||||
br: bufio.NewReaderSize(rc, 64*1024),
|
||||
model: model,
|
||||
id: "chatcmpl-stream",
|
||||
created: time.Now().Unix(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,6 +146,7 @@ func (s *stream) chunk(delta map[string]any, finish *string, usage map[string]an
|
||||
obj := map[string]any{
|
||||
"id": s.id,
|
||||
"object": "chat.completion.chunk",
|
||||
"created": s.created,
|
||||
"model": s.model,
|
||||
"choices": []any{choice},
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package anthropic
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.govekar.net/LovroG05/localmodelroutex/internal/providers"
|
||||
)
|
||||
@@ -246,9 +247,10 @@ func translateResponse(body []byte, model string) ([]byte, providers.Usage) {
|
||||
message["tool_calls"] = toolCalls
|
||||
}
|
||||
resp := map[string]any{
|
||||
"id": ar.ID,
|
||||
"object": "chat.completion",
|
||||
"model": model,
|
||||
"id": ar.ID,
|
||||
"object": "chat.completion",
|
||||
"created": time.Now().Unix(),
|
||||
"model": model,
|
||||
"choices": []any{map[string]any{
|
||||
"index": 0,
|
||||
"message": message,
|
||||
|
||||
Reference in New Issue
Block a user