mirror of
https://github.com/ollama/ollama.git
synced 2026-09-21 21:48:02 -05:00
209 lines
5.8 KiB
Go
209 lines
5.8 KiB
Go
package proxy
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
)
|
|
|
|
const (
|
|
codexSubscriptionMessage = "This model requires a subscription or extra usage credits. Please upgrade at https://ollama.com/upgrade or add extra usage at https://ollama.com/settings to use this model."
|
|
codexSignInMessage = "This model requires an Ollama account. Please sign in to Ollama to use this model."
|
|
)
|
|
|
|
// rewriteAccessErrors changes only Ollama access-error messages. Successful
|
|
// events and unrelated errors retain their original bytes and status codes.
|
|
func (h *CodexDesktop) rewriteAccessErrors(resp *http.Response) error {
|
|
if encoding := resp.Header.Get("Content-Encoding"); encoding != "" && encoding != "identity" {
|
|
return nil
|
|
}
|
|
if strings.HasPrefix(strings.ToLower(resp.Header.Get("Content-Type")), "text/event-stream") {
|
|
resp.Body = &codexAccessErrorStream{
|
|
ReadCloser: resp.Body,
|
|
reader: bufio.NewReader(resp.Body),
|
|
rewrite: func(body []byte) ([]byte, bool) { return h.rewriteAccessErrorJSON(body, resp.StatusCode) },
|
|
limit: h.maxBodyBytes,
|
|
}
|
|
resp.ContentLength = -1
|
|
resp.Header.Del("Content-Length")
|
|
return nil
|
|
}
|
|
if resp.StatusCode < http.StatusBadRequest {
|
|
return nil
|
|
}
|
|
body, err := io.ReadAll(io.LimitReader(resp.Body, h.maxBodyBytes+1))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if int64(len(body)) > h.maxBodyBytes {
|
|
resp.Body = struct {
|
|
io.Reader
|
|
io.Closer
|
|
}{io.MultiReader(bytes.NewReader(body), resp.Body), resp.Body}
|
|
return nil
|
|
}
|
|
resp.Body.Close()
|
|
rewritten, changed := h.rewriteAccessErrorJSON(body, resp.StatusCode)
|
|
resp.Body = io.NopCloser(bytes.NewReader(rewritten))
|
|
if changed {
|
|
resp.ContentLength = int64(len(rewritten))
|
|
resp.Header.Del("Content-Length")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (h *CodexDesktop) rewriteAccessErrorJSON(body []byte, status int) ([]byte, bool) {
|
|
var payload map[string]json.RawMessage
|
|
if json.Unmarshal(body, &payload) != nil || payload == nil {
|
|
return body, false
|
|
}
|
|
var kind string
|
|
_ = json.Unmarshal(payload["type"], &kind)
|
|
if kind == "response.failed" {
|
|
rewritten, changed := h.rewriteAccessErrorJSON(payload["response"], status)
|
|
if !changed {
|
|
return body, false
|
|
}
|
|
payload["response"] = rewritten
|
|
} else {
|
|
var message string
|
|
var fields map[string]json.RawMessage
|
|
stringError := json.Unmarshal(payload["error"], &message) == nil
|
|
flatError := false
|
|
if !stringError {
|
|
if _, nested := payload["error"]; !nested && kind == "error" {
|
|
fields = payload
|
|
flatError = true
|
|
} else if json.Unmarshal(payload["error"], &fields) != nil || fields == nil {
|
|
return body, false
|
|
}
|
|
if json.Unmarshal(fields["message"], &message) != nil {
|
|
return body, false
|
|
}
|
|
}
|
|
var code, errorType string
|
|
_ = json.Unmarshal(fields["code"], &code)
|
|
_ = json.Unmarshal(fields["type"], &errorType)
|
|
var rewritten, reason string
|
|
switch {
|
|
case status == http.StatusUnauthorized || code == "authentication_error" || code == "unauthorized" || errorType == "authentication_error" || strings.EqualFold(strings.TrimSpace(message), "unauthorized"):
|
|
rewritten = codexSignInMessage
|
|
reason = "sign_in"
|
|
// Codex displays these errors as plain text, including raw HTTP error
|
|
// bodies. Keep device sign-in URLs out of the user-facing response.
|
|
delete(payload, "signin_url")
|
|
delete(fields, "signin_url")
|
|
case strings.Contains(strings.ToLower(message), "this model requires a subscription or extra usage"):
|
|
rewritten = codexSubscriptionMessage
|
|
reason = "subscription"
|
|
default:
|
|
return body, false
|
|
}
|
|
h.logger.Debug("Codex Ollama access error", "status", status, "reason", reason)
|
|
encoded, _ := json.Marshal(rewritten)
|
|
if stringError {
|
|
payload["error"] = encoded
|
|
} else {
|
|
fields["message"] = encoded
|
|
if !flatError {
|
|
payload["error"], _ = json.Marshal(fields)
|
|
}
|
|
}
|
|
}
|
|
rewritten, err := json.Marshal(payload)
|
|
if err != nil {
|
|
return body, false
|
|
}
|
|
return rewritten, true
|
|
}
|
|
|
|
// Buffer one SSE frame, not the whole response. Oversized frames fall back to
|
|
// passthrough so cosmetic error changes cannot interrupt a valid model stream.
|
|
type codexAccessErrorStream struct {
|
|
io.ReadCloser
|
|
reader *bufio.Reader
|
|
rewrite func([]byte) ([]byte, bool)
|
|
limit int64
|
|
pending []byte
|
|
err error
|
|
passthrough bool
|
|
}
|
|
|
|
func (s *codexAccessErrorStream) Read(p []byte) (int, error) {
|
|
if len(p) == 0 {
|
|
return 0, nil
|
|
}
|
|
if len(s.pending) == 0 {
|
|
if s.err != nil {
|
|
return 0, s.err
|
|
}
|
|
if s.passthrough {
|
|
return s.reader.Read(p)
|
|
}
|
|
}
|
|
if len(s.pending) == 0 && s.err == nil {
|
|
var frame []byte
|
|
partialLine := false
|
|
for {
|
|
line, err := s.reader.ReadSlice('\n')
|
|
frame = append(frame, line...)
|
|
if err != nil && err != bufio.ErrBufferFull {
|
|
s.err = err
|
|
}
|
|
if int64(len(frame)) > s.limit {
|
|
s.passthrough = true
|
|
break
|
|
}
|
|
if s.err != nil {
|
|
break
|
|
}
|
|
if !partialLine && (bytes.Equal(line, []byte("\n")) || bytes.Equal(line, []byte("\r\n"))) {
|
|
break
|
|
}
|
|
partialLine = err == bufio.ErrBufferFull
|
|
}
|
|
s.pending = frame
|
|
if !s.passthrough {
|
|
s.pending = rewriteCodexErrorFrame(frame, s.rewrite)
|
|
}
|
|
}
|
|
if len(s.pending) > 0 {
|
|
n := copy(p, s.pending)
|
|
s.pending = s.pending[n:]
|
|
return n, nil
|
|
}
|
|
return 0, s.err
|
|
}
|
|
|
|
func rewriteCodexErrorFrame(frame []byte, rewrite func([]byte) ([]byte, bool)) []byte {
|
|
lines := bytes.SplitAfter(frame, []byte("\n"))
|
|
var data [][]byte
|
|
for _, line := range lines {
|
|
if value, ok := bytes.CutPrefix(line, []byte("data:")); ok {
|
|
data = append(data, bytes.TrimSpace(value))
|
|
}
|
|
}
|
|
rewritten, changed := rewrite(bytes.Join(data, []byte("\n")))
|
|
if !changed {
|
|
return frame
|
|
}
|
|
var result bytes.Buffer
|
|
written := false
|
|
for _, line := range lines {
|
|
if bytes.HasPrefix(line, []byte("data:")) {
|
|
if !written {
|
|
result.WriteString("data: ")
|
|
result.Write(rewritten)
|
|
result.WriteByte('\n')
|
|
written = true
|
|
}
|
|
} else {
|
|
result.Write(line)
|
|
}
|
|
}
|
|
return result.Bytes()
|
|
}
|