Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions internal/dialect/pipeline.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"net/http"
"strings"

"gpt-load/internal/platform/httpheader"
"gpt-load/internal/state"
)

Expand All @@ -26,6 +27,9 @@ func ApplyCredential(
headers.Set(name, strings.ReplaceAll(value, "${API_KEY}", apiKey))
}
for _, name := range rules.Remove {
if httpheader.IsForbiddenRequestRuleSetName(name) {
continue
}
headers.Del(name)
}
}
36 changes: 12 additions & 24 deletions internal/dialect/pipeline_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,7 @@ import (

func TestApplyCredentialUsesDialectDefaultWithEmptyRules(t *testing.T) {
headers := make(http.Header)

ApplyCredential(
NewOpenAI(http.DefaultClient),
headers,
"sk-default",
state.HeaderRules{},
)

ApplyCredential(NewOpenAI(http.DefaultClient), headers, "sk-default", state.HeaderRules{})
if got := headers.Get("Authorization"); got != "Bearer sk-default" {
t.Fatalf("Authorization = %q, want default Bearer credential", got)
}
Expand All @@ -31,14 +24,7 @@ func TestApplyCredentialExpandsSetRulesAfterDefault(t *testing.T) {
},
Remove: []string{"X-Remove-Me"},
}

ApplyCredential(
NewOpenAI(http.DefaultClient),
headers,
"sk-custom",
rules,
)

ApplyCredential(NewOpenAI(http.DefaultClient), headers, "sk-custom", rules)
if got := headers.Get("Authorization"); got != "Token sk-custom" {
t.Fatalf("Authorization = %q, want custom override", got)
}
Expand All @@ -50,20 +36,22 @@ func TestApplyCredentialExpandsSetRulesAfterDefault(t *testing.T) {
}
}

func TestApplyCredentialCannotRemoveSystemOwnedContentCodingHeaders(t *testing.T) {
headers := http.Header{"Accept-Encoding": {"identity"}}
rules := state.HeaderRules{Remove: []string{"Accept-Encoding", "Content-Encoding", "Content-Length"}}
ApplyCredential(NewOpenAI(http.DefaultClient), headers, "sk-system", rules)
if got := headers.Get("Accept-Encoding"); got != "identity" {
t.Fatalf("Accept-Encoding = %q, want identity", got)
}
}

func TestApplyCredentialRemoveWinsOverSet(t *testing.T) {
headers := make(http.Header)
rules := state.HeaderRules{
Set: map[string]string{"Authorization": "Token ${API_KEY}"},
Remove: []string{"Authorization"},
}

ApplyCredential(
NewOpenAI(http.DefaultClient),
headers,
"sk-removed",
rules,
)

ApplyCredential(NewOpenAI(http.DefaultClient), headers, "sk-removed", rules)
if got := headers.Get("Authorization"); got != "" {
t.Fatalf("Authorization = %q, want final remove to win", got)
}
Expand Down
53 changes: 53 additions & 0 deletions internal/gateway/content_coding.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package gateway

import (
"fmt"
"net/http"
"strconv"

"gpt-load/internal/platform/contentcoding"
)

func readDecodedRequestBody(
request *http.Request,
encodedLimit int64,
decodedLimit int64,
) ([]byte, http.Header, error) {
if request == nil || request.Body == nil {
return nil, nil, fmt.Errorf("%w: request body is required", contentcoding.ErrInvalidEncoding)
}
if request.ContentLength > encodedLimit && encodedLimit >= 0 {
return nil, nil, contentcoding.ErrEncodedTooLarge
}
body, err := contentcoding.ReadDecodedBody(
request.Body,
request.Header.Values("Content-Encoding"),
encodedLimit,
decodedLimit,
)
if err != nil {
return nil, nil, err
}
headers := cloneEndToEndHeaders(request.Header)
stripRepresentationMetadata(headers)
headers.Set("Accept-Encoding", "identity")
return body, headers, nil
}

func stripRepresentationMetadata(headers http.Header) {
if headers == nil {
return
}
for _, name := range representationMetadataHeaderNames {
deleteHeaderField(headers, name)
}
}

func rebuildPlainBufferedResponseHeaders(headers http.Header, bodyLength int) {
stripRepresentationMetadata(headers)
headers.Set("Content-Length", strconv.Itoa(bodyLength))
}

func normalizePlainStreamingResponseHeaders(headers http.Header) {
stripRepresentationMetadata(headers)
}
27 changes: 27 additions & 0 deletions internal/gateway/content_coding_reason_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package gateway

import (
"net/http"
"testing"
)

// These public reason codes are part of the data-plane compatibility contract.
func TestContentCodingReasonsRemainStable(t *testing.T) {
tests := []struct {
name string
reason reason
status int
code string
}{
{name: "invalid encoding", reason: reasonInvalidContentEncoding, status: http.StatusBadRequest, code: "invalid_content_encoding"},
{name: "unsupported encoding", reason: reasonUnsupportedContentEncoding, status: http.StatusUnsupportedMediaType, code: "unsupported_content_encoding"},
{name: "identity rejected", reason: reasonNotAcceptable, status: http.StatusNotAcceptable, code: "not_acceptable"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if test.reason.Status != test.status || test.reason.Code != test.code {
t.Fatalf("reason = %#v, want status/code %d/%q", test.reason, test.status, test.code)
}
})
}
}
174 changes: 174 additions & 0 deletions internal/gateway/content_coding_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
package gateway

import (
"bytes"
"compress/gzip"
"compress/zlib"
"errors"
"io"
"net/http"
"net/http/httptest"
"strconv"
"testing"

"github.com/andybalholm/brotli"
"github.com/gin-gonic/gin"
"github.com/klauspost/compress/zstd"

"gpt-load/internal/platform/contentcoding"
)

func TestReadDecodedRequestBodyNormalizesSupportedEncodings(t *testing.T) {
plain := []byte(`{"model":"gpt-test","messages":[]}`)
for _, encoding := range []string{"", "identity", "gzip", "br", "deflate", "zstd"} {
t.Run(encoding, func(t *testing.T) {
wire := encodeGatewayContentCodingFixture(t, encoding, plain)
request := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", bytes.NewReader(wire))
if encoding != "" {
request.Header.Set("Content-Encoding", encoding)
}
request.Header.Set("Content-Type", "application/json")
request.Header.Set("Accept-Encoding", "gzip")
request.Header.Set("Digest", "sha-256=stale")

body, headers, err := readDecodedRequestBody(request, 1<<20, 1<<20)
if err != nil || !bytes.Equal(body, plain) {
t.Fatalf("readDecodedRequestBody(%q) = %q, %v", encoding, body, err)
}
for _, name := range []string{"Content-Encoding", "Content-Length", "Digest"} {
if values := headers.Values(name); values != nil {
t.Errorf("normalized header %s survived: %#v", name, values)
}
}
if headers.Get("Accept-Encoding") != "identity" || headers.Get("Content-Type") != "application/json" {
t.Fatalf("normalized headers = %#v", headers)
}
})
}
}

func TestReadDecodedRequestBodyReturnsStableContentCodingErrors(t *testing.T) {
request := httptest.NewRequest(http.MethodPost, "/v1/chat/completions", bytes.NewReader([]byte("body")))
request.Header.Set("Content-Encoding", "gzip, br")
if _, _, err := readDecodedRequestBody(request, 1<<20, 1<<20); !errors.Is(err, contentcoding.ErrUnsupportedEncoding) {
t.Fatalf("stacked encoding error = %v", err)
}

request = httptest.NewRequest(http.MethodPost, "/v1/chat/completions", bytes.NewReader([]byte("not-gzip")))
request.Header.Set("Content-Encoding", "gzip")
if _, _, err := readDecodedRequestBody(request, 1<<20, 1<<20); !errors.Is(err, contentcoding.ErrInvalidEncoding) {
t.Fatalf("malformed encoding error = %v", err)
}

plain := bytes.Repeat([]byte("x"), 1025)
wire := encodeGatewayContentCodingFixture(t, "gzip", plain)
request = httptest.NewRequest(http.MethodPost, "/v1/chat/completions", bytes.NewReader(wire))
request.Header.Set("Content-Encoding", "gzip")
if _, _, err := readDecodedRequestBody(request, 1<<20, 1024); !errors.Is(err, contentcoding.ErrDecodedTooLarge) {
t.Fatalf("decoded overflow error = %v", err)
}
}

func TestPlaintextResponseWriterDecodesCompressedBufferedResponse(t *testing.T) {
gin.SetMode(gin.TestMode)
plain := []byte(`{"id":"response","model":"gpt-test"}`)
for _, encoding := range []string{"gzip", "br", "deflate", "zstd"} {
t.Run(encoding, func(t *testing.T) {
wire := encodeGatewayContentCodingFixture(t, encoding, plain)
recorder := httptest.NewRecorder()
engine := gin.New()
engine.GET("/", (&Handler{}).normalizeDownstreamContentCoding(), func(context *gin.Context) {
context.Header("Content-Type", "application/json")
context.Header("Content-Encoding", encoding)
context.Header("Content-Length", strconv.Itoa(len(wire)))
context.Header("ETag", "stale-compressed-etag")
context.Status(http.StatusOK)
_, _ = context.Writer.Write(wire)
context.Writer.Flush()
})
engine.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/", nil))
if recorder.Code != http.StatusOK || !bytes.Equal(recorder.Body.Bytes(), plain) {
t.Fatalf("response = %d %q, want %q", recorder.Code, recorder.Body.Bytes(), plain)
}
if recorder.Header().Get("Content-Encoding") != "" || recorder.Header().Get("ETag") != "" ||
recorder.Header().Get("Content-Length") != strconv.Itoa(len(plain)) {
t.Fatalf("plaintext headers = %#v", recorder.Header())
}
})
}
}

func TestPlaintextResponseWriterPreservesValidIdentityMetadata(t *testing.T) {
gin.SetMode(gin.TestMode)
body := []byte(`{"ok":true}`)
recorder := httptest.NewRecorder()
engine := gin.New()
engine.GET("/", (&Handler{}).normalizeDownstreamContentCoding(), func(context *gin.Context) {
context.Header("Content-Encoding", "identity")
context.Header("ETag", "valid-identity-etag")
context.Status(http.StatusOK)
_, _ = context.Writer.Write(body)
context.Writer.Flush()
})
engine.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/", nil))
if !bytes.Equal(recorder.Body.Bytes(), body) || recorder.Header().Get("Content-Encoding") != "" ||
recorder.Header().Get("ETag") != "valid-identity-etag" {
t.Fatalf("identity response = %q headers=%#v", recorder.Body.Bytes(), recorder.Header())
}
}

func TestPlaintextResponseWriterStreamsIdentitySSE(t *testing.T) {
gin.SetMode(gin.TestMode)
recorder := httptest.NewRecorder()
engine := gin.New()
engine.GET("/", (&Handler{}).normalizeDownstreamContentCoding(), func(context *gin.Context) {
context.Header("Content-Type", "text/event-stream")
context.Header("Content-Encoding", "identity")
context.Header("ETag", "stale-stream-etag")
context.Status(http.StatusOK)
_, _ = context.Writer.WriteString("data: one\n\n")
context.Writer.Flush()
_, _ = context.Writer.WriteString("data: two\n\n")
context.Writer.Flush()
})
engine.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/", nil))
if recorder.Body.String() != "data: one\n\ndata: two\n\n" {
t.Fatalf("stream body = %q", recorder.Body.String())
}
if recorder.Header().Get("Content-Encoding") != "" || recorder.Header().Get("Content-Length") != "" ||
recorder.Header().Get("ETag") != "" {
t.Fatalf("stream headers = %#v", recorder.Header())
}
}

func encodeGatewayContentCodingFixture(t *testing.T, encoding string, plain []byte) []byte {
t.Helper()
if encoding == "" || encoding == "identity" {
return bytes.Clone(plain)
}
var buffer bytes.Buffer
var writer io.WriteCloser
switch encoding {
case "gzip":
writer = gzip.NewWriter(&buffer)
case "br":
writer = brotli.NewWriter(&buffer)
case "deflate":
writer = zlib.NewWriter(&buffer)
case "zstd":
encoder, err := zstd.NewWriter(&buffer, zstd.WithEncoderConcurrency(1))
if err != nil {
t.Fatal(err)
}
writer = encoder
default:
t.Fatalf("unsupported fixture encoding %q", encoding)
}
if _, err := writer.Write(plain); err != nil {
t.Fatal(err)
}
if err := writer.Close(); err != nil {
t.Fatal(err)
}
return buffer.Bytes()
}
Loading
Loading