归一化视图(request_body/response_body)会丢弃工具 schema、tool_use 块, 且看不到原始 SSE 帧,排查 provider 侧问题时信息不足。新增在 HTTP transport 层捕获真实 wire body 的能力: - llmrec: 新增 Capture,Recorder 挂到 ctx,quotaAwareTransport 填充。 请求取 GetBody 副本(不消费原 body),响应用 tee 保持流式;norma 内部重试 的多次尝试逐次保留(旧代码会读掉丢弃),单次时为字节级原文。 - db: llm_records 增加 raw_request/raw_response 两列,含旧库 ALTER 迁移。 - web: 录制页详情面板增加「原文」视图切换,及请求/响应内容复制按钮 (兼容非安全上下文的 execCommand 回退)。 - 测试: 单元(capture)、端到端(逐字节比对捕获=服务器实际收到)、 迁移(真实 PG 事务回滚)、全链路(Recorder→provider→PG 落库)。
63 lines
2.0 KiB
Go
63 lines
2.0 KiB
Go
package db
|
|
|
|
import (
|
|
"testing"
|
|
)
|
|
|
|
// The raw-body columns were added after release, so existing installs only get
|
|
// them through llmRecordsMigrate. This runs the real migration against the dev
|
|
// PG on a table stripped back to its pre-upgrade shape, inside a transaction
|
|
// that is always rolled back — PG does transactional DDL, so nothing persists.
|
|
func TestLLMRecordsMigrateAddsRawColumnsToOldTable(t *testing.T) {
|
|
d, err := Open(testDSN(t))
|
|
if err != nil {
|
|
t.Skipf("postgres unavailable (%v) — skipping", err)
|
|
}
|
|
defer d.Close()
|
|
if err := d.EnsureLLMRecordsTable(); err != nil {
|
|
t.Fatalf("ensure: %v", err)
|
|
}
|
|
|
|
tx, err := d.Begin()
|
|
if err != nil {
|
|
t.Fatalf("begin: %v", err)
|
|
}
|
|
defer tx.Rollback() //nolint:errcheck // the test never commits
|
|
|
|
// Roll the table back to how a pre-upgrade install looks.
|
|
if _, err := tx.Exec(`ALTER TABLE llm_records DROP COLUMN IF EXISTS raw_request, DROP COLUMN IF EXISTS raw_response`); err != nil {
|
|
t.Fatalf("simulate old table: %v", err)
|
|
}
|
|
if _, err := tx.Exec(llmRecordsMigrate); err != nil {
|
|
t.Fatalf("migrate: %v", err)
|
|
}
|
|
|
|
for _, col := range []string{"raw_request", "raw_response"} {
|
|
var n int
|
|
if err := tx.QueryRow(
|
|
`SELECT count(*) FROM information_schema.columns
|
|
WHERE table_name='llm_records' AND column_name=$1`, col).Scan(&n); err != nil {
|
|
t.Fatalf("inspect %s: %v", col, err)
|
|
}
|
|
if n != 1 {
|
|
t.Errorf("column %s missing after migrate", col)
|
|
}
|
|
}
|
|
|
|
// Re-running must stay a no-op (the migration runs on every startup).
|
|
if _, err := tx.Exec(llmRecordsMigrate); err != nil {
|
|
t.Fatalf("migrate is not idempotent: %v", err)
|
|
}
|
|
|
|
// An insert carrying raw bodies must round-trip through the migrated table.
|
|
var got string
|
|
if err := tx.QueryRow(
|
|
`INSERT INTO llm_records(model, raw_request, raw_response) VALUES ('m','{"a":1}','data: x')
|
|
RETURNING raw_response`).Scan(&got); err != nil {
|
|
t.Fatalf("insert into migrated table: %v", err)
|
|
}
|
|
if got != "data: x" {
|
|
t.Errorf("raw_response=%q want %q", got, "data: x")
|
|
}
|
|
}
|