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:
2026-07-07 11:18:17 +02:00
parent e02dfd4317
commit 8d3dee46ba
4 changed files with 49 additions and 10 deletions

View File

@@ -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
}

View File

@@ -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, "")
}

View File

@@ -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},
}

View File

@@ -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,