mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-11-07 09:47:23 +00:00
Phase 2 收尾。证明「对外协议由适配器决定、内核只调度」不是纸面主张:
Anthropic 客户端打 /v1/messages,拿到的是 Anthropic 形状。
- internal/gateway/messages.go:入向把 Anthropic 请求解析进统一格式
(content blocks 原样保留——只有适配器知道自己的上游接受哪种块类型;
system 支持字符串与 [{type:text,text}] 两种写法)。
调度/配额/AUTO 链/冷却/审计/插件 hook 全部复用 /v1/chat/completions
的同一条路径(serveUnified),差别只有两端线格式。
- internal/lua/adapters/anthropic.lua:新增 serialize_response /
serialize_open_preamble / serialize_chunk / serialize_close,
把统一格式译回 Anthropic Messages 形状(含 content 分块、
stop_reason 反向映射、input/output_tokens 改名)。
- 流式是 message_start → content_block_delta → message_delta →
message_stop;DoneMarker 为空所以内核**不会**擅自加 [DONE]。
内核出向零 Anthropic 字段名:解析是表驱动的,序列化属于适配器。
测试:
- messages_test.go 5 个端到端用例(真实 core+gateway + 只懂 Anthropic 的
假上游):形状/stop_reason/块顺序/usage 命名、流式事件序列、入向校验、
鉴权、以及「未声明钩子时回落 OpenAI 兜底」。
- 修正两个 serializer 测试的前提断言(anthropic 现在确实声明了钩子,
这正是 Phase 2 的意图),并发测试改为按适配器各自校验形状。
golden 字节级未漂移;go test -tags luajit ./... 全绿。
110 lines
3.8 KiB
Go
110 lines
3.8 KiB
Go
package gateway
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// TestUI_ImageRequestSendsJSONBody pins the bug that made the image page
|
|
// unusable in production while every DOM assertion still passed.
|
|
//
|
|
// The page called api(path, {method, body: {...}}). api() is
|
|
// `(p, o) => fetch(p, o)` — o is the NATIVE fetch options object, so an object
|
|
// in `body` is coerced by fetch into the literal text "[object Object]". The
|
|
// gateway's json.Unmarshal rejected it, the response was a bare 400 with
|
|
// lat_ms 0, and the page showed an error toast with no image and no usable
|
|
// message.
|
|
//
|
|
// What made this expensive: the pane rendered, the model list had all six
|
|
// models, the button enabled itself, the console was clean. Only the WIRE
|
|
// format was wrong, and nothing in the DOM shows a wire format.
|
|
func TestUI_ImageRequestSendsJSONBody(t *testing.T) {
|
|
body := sendImageSource(t)
|
|
|
|
if !strings.Contains(body, "JSON.stringify") {
|
|
t.Error("sendImage() does not JSON.stringify its body — fetch coerces " +
|
|
"the object to the text \"[object Object]\" and the gateway answers " +
|
|
"400 with lat_ms 0")
|
|
}
|
|
if !strings.Contains(body, `"Content-Type": "application/json"`) {
|
|
t.Error("sendImage() does not set Content-Type: application/json; the " +
|
|
"server sees text/plain and may refuse to parse the body")
|
|
}
|
|
// The literal defect, asserted so a later refactor reintroducing it fails
|
|
// here rather than in a browser.
|
|
if strings.Contains(body, "body: size ? {") || strings.Contains(body, "body: { model") {
|
|
t.Error("sendImage() still passes an object literal as the fetch body")
|
|
}
|
|
}
|
|
|
|
// TestUI_PostCallsSendJSONString guards every POST in the file, not just the
|
|
// image one. The image page happened to be the only place that used api() with
|
|
// an object body, so a check scoped to it would pass while another page had the
|
|
// same defect.
|
|
func TestUI_PostCallsSendJSONString(t *testing.T) {
|
|
ui := uiSource(t)
|
|
lines := strings.Split(ui, "\n")
|
|
|
|
var bad []string
|
|
for i, l := range lines {
|
|
if !strings.Contains(l, `method: "POST"`) {
|
|
continue
|
|
}
|
|
end := i + 8
|
|
if end > len(lines) {
|
|
end = len(lines)
|
|
}
|
|
window := strings.Join(lines[i:end], "\n")
|
|
if !strings.Contains(window, "body:") {
|
|
continue // no body at all: legitimate (a bare trigger)
|
|
}
|
|
if strings.Contains(window, "JSON.stringify") || strings.Contains(window, "FormData") {
|
|
continue
|
|
}
|
|
bad = append(bad, strings.TrimSpace(l))
|
|
}
|
|
if len(bad) > 0 {
|
|
t.Errorf("%d POST call(s) send a non-string body:", len(bad))
|
|
for _, b := range bad {
|
|
if len(b) > 100 {
|
|
b = b[:100]
|
|
}
|
|
t.Errorf(" %s", b)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestUI_ImagePageReportsServerErrors pins that a failed generation surfaces
|
|
// something actionable. The production bug produced a bare "400 Bad Request"
|
|
// toast; the requirement is that the gateway's own message reaches the page.
|
|
func TestUI_ImagePageReportsServerErrors(t *testing.T) {
|
|
body := sendImageSource(t)
|
|
|
|
// api() throws Error(j.error.message) on !r.ok, so appending e.message is
|
|
// what puts the explanation on screen.
|
|
if !strings.Contains(body, "e && e.message") {
|
|
t.Error("sendImage() does not surface the server's error message; a " +
|
|
"failure would show a bare status with no cause")
|
|
}
|
|
// And a success carrying no image must not look like a success.
|
|
if !strings.Contains(body, "imgNoData") {
|
|
t.Error("sendImage() does not warn when the upstream returns no image")
|
|
}
|
|
}
|
|
|
|
// sendImageSource returns the text of sendImage() only, so an assertion cannot
|
|
// be satisfied by a string that happens to appear elsewhere in the file.
|
|
func sendImageSource(t *testing.T) string {
|
|
t.Helper()
|
|
ui := uiSource(t)
|
|
i := strings.Index(ui, "async function sendImage(")
|
|
if i < 0 {
|
|
t.Fatal("sendImage() not found")
|
|
}
|
|
body := ui[i:]
|
|
if end := strings.Index(body[1:], "\n function "); end > 0 {
|
|
body = body[:end+1]
|
|
}
|
|
return body
|
|
}
|