* feat(images): 接入 OpenAI 图片生成与编辑 * fix(images): 补齐响应容量与边界处理 * fix(images): 修正协议观测与响应上限覆盖 * fix(stream): 保证取消优先于流关闭 * fix(images): 修正空 SSE 错误对象判定 * fix(images): 避免媒体数据触发凭据误判 * fix(images): 阻止流式响应泄漏凭据 * fix(images): 校验完整流事件后再转发 * fix(images): 修正响应模型观测
138 lines
5.1 KiB
Go
138 lines
5.1 KiB
Go
package bifrost
|
|
|
|
import (
|
|
"bytes"
|
|
"strings"
|
|
"testing"
|
|
|
|
"gpt-load/internal/execution"
|
|
"gpt-load/internal/protocol"
|
|
)
|
|
|
|
func TestNativeFirstSSEEventGateAcceptsTwoMiBEventAcrossChunks(t *testing.T) {
|
|
gate := &nativeFirstSSEEventGate{}
|
|
wire := []byte("data: " + strings.Repeat("x", 2<<20) + "\n\n")
|
|
split := len(wire) / 3
|
|
for _, chunk := range [][]byte{wire[:split], wire[split : 2*split], wire[2*split:]} {
|
|
output, err := gate.push(chunk)
|
|
if err != nil {
|
|
t.Fatalf("push() error = %v", err)
|
|
}
|
|
if gate.ready && len(output) != len(wire) {
|
|
t.Fatalf("ready output length = %d, want %d", len(output), len(wire))
|
|
}
|
|
}
|
|
if !gate.ready {
|
|
t.Fatal("two MiB first SSE event did not become ready")
|
|
}
|
|
}
|
|
|
|
func TestNativeSSEBoundariesUseImagesEventLimit(t *testing.T) {
|
|
payload := `{"type":"image_generation.partial_image","model":"provider-model","b64_json":"` +
|
|
strings.Repeat("A", maxNativeFirstSSEEventBytes+1) + `"}`
|
|
wire := []byte("event: image_generation.partial_image\ndata: " + payload + "\n\n")
|
|
spec := execution.AttemptSpec{
|
|
ClientProtocol: protocol.OpenAIImages,
|
|
ClientModel: "public-model",
|
|
UpstreamModel: "provider-model",
|
|
}
|
|
|
|
gate := newNativeFirstSSEEventGate(spec)
|
|
output, err := gate.push(wire)
|
|
if err != nil || !bytes.Equal(output, wire) {
|
|
t.Fatalf("first-event gate output/error = %d bytes / %v", len(output), err)
|
|
}
|
|
|
|
rewriter := newNativeAliasSSERewriter(spec)
|
|
rewritten, err := rewriter.push(wire)
|
|
if err != nil {
|
|
t.Fatalf("alias rewriter error = %v", err)
|
|
}
|
|
if !bytes.Contains(rewritten, []byte(`"model":"public-model"`)) {
|
|
t.Fatal("alias rewriter did not preserve the oversized Images event")
|
|
}
|
|
}
|
|
|
|
func TestRewriteNativeResponseModelCoversNativeProtocolShapes(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
for _, test := range []struct {
|
|
name string
|
|
clientProtocol protocol.Protocol
|
|
body string
|
|
}{
|
|
{name: "OpenAI chat", clientProtocol: protocol.OpenAICompletions, body: `{"model":"provider-model","choices":[]}`},
|
|
{name: "OpenAI Responses object", clientProtocol: protocol.OpenAIResponses, body: `{"type":"response","model":"provider-model","output":[]}`},
|
|
{name: "OpenAI Responses event", clientProtocol: protocol.OpenAIResponses, body: `{"type":"response.completed","response":{"model":"provider-model"}}`},
|
|
{name: "Anthropic message", clientProtocol: protocol.Anthropic, body: `{"type":"message","model":"provider-model","content":[]}`},
|
|
{name: "Anthropic message_start", clientProtocol: protocol.Anthropic, body: `{"type":"message_start","message":{"model":"provider-model"}}`},
|
|
{name: "Gemini response", clientProtocol: protocol.Gemini, body: `{"modelVersion":"provider-model","candidates":[]}`},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
rewritten, err := rewriteClientResponseModel(test.clientProtocol, []byte(test.body), "public-model")
|
|
if err != nil {
|
|
t.Fatalf("rewriteClientResponseModel() error = %v", err)
|
|
}
|
|
if !bytes.Contains(rewritten, []byte(`"public-model"`)) || bytes.Contains(rewritten, []byte(`"provider-model"`)) {
|
|
t.Fatalf("rewritten response = %s", rewritten)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNativeAliasSSERewriterCoversNativeProtocolShapes(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
for _, test := range []struct {
|
|
name string
|
|
clientProtocol protocol.Protocol
|
|
payload string
|
|
}{
|
|
{name: "OpenAI chat", clientProtocol: protocol.OpenAICompletions, payload: `{"model":"provider-model","choices":[]}`},
|
|
{name: "OpenAI Responses", clientProtocol: protocol.OpenAIResponses, payload: `{"type":"response.completed","response":{"model":"provider-model"}}`},
|
|
{name: "Anthropic", clientProtocol: protocol.Anthropic, payload: `{"type":"message_start","message":{"model":"provider-model"}}`},
|
|
{name: "Gemini", clientProtocol: protocol.Gemini, payload: `{"modelVersion":"provider-model","candidates":[]}`},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
rewriter := newNativeAliasSSERewriter(execution.AttemptSpec{
|
|
ClientProtocol: test.clientProtocol,
|
|
ClientModel: "public-model",
|
|
UpstreamModel: "provider-model",
|
|
})
|
|
raw := []byte("event: update\r\ndata: " + test.payload + "\r\n\r\ndata: [DONE]\r\n\r\n")
|
|
split := len(raw) / 2
|
|
first, err := rewriter.push(raw[:split])
|
|
if err != nil {
|
|
t.Fatalf("first push error = %v", err)
|
|
}
|
|
second, err := rewriter.push(raw[split:])
|
|
if err != nil {
|
|
t.Fatalf("second push error = %v", err)
|
|
}
|
|
tail, err := rewriter.finish()
|
|
if err != nil {
|
|
t.Fatalf("finish error = %v", err)
|
|
}
|
|
output := bytes.Join([][]byte{first, second, tail}, nil)
|
|
if !bytes.Contains(output, []byte(`"public-model"`)) || bytes.Contains(output, []byte(`"provider-model"`)) {
|
|
t.Fatalf("rewritten SSE = %s", output)
|
|
}
|
|
if !bytes.Contains(output, []byte("data: [DONE]\r\n\r\n")) {
|
|
t.Fatalf("terminal SSE marker changed: %q", output)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNativeAliasSSERewriterIsDisabledForEqualAliases(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
if rewriter := newNativeAliasSSERewriter(execution.AttemptSpec{
|
|
ClientProtocol: protocol.OpenAICompletions,
|
|
ClientModel: "same-model",
|
|
UpstreamModel: "same-model",
|
|
}); rewriter != nil {
|
|
t.Fatal("equal aliases enabled native SSE rewriting")
|
|
}
|
|
}
|