merge: sync official main into PR branch
This commit is contained in:
commit
d803ae2b11
23 files changed
+1325
-68
No files matched your search
+21
-7
@@ -144,6 +144,13 @@ type LLMRecord struct {
|
||||
Error string `json:"error,omitempty"`
|
||||
RequestBody string `json:"request_body,omitempty"`
|
||||
ResponseBody string `json:"response_body,omitempty"`
|
||||
// RawRequest / RawResponse are the untouched HTTP bodies exchanged with the
|
||||
// provider — the request as buildBody() sent it (full tool schemas included)
|
||||
// and the raw SSE frames. RequestBody/ResponseBody above are the normalized
|
||||
// view, which drops tool schemas and tool_use blocks entirely. Empty for
|
||||
// records written before this was added, or when the call never reached HTTP.
|
||||
RawRequest string `json:"raw_request,omitempty"`
|
||||
RawResponse string `json:"raw_response,omitempty"`
|
||||
}
|
||||
|
||||
const llmRecordsSchema = `
|
||||
@@ -163,7 +170,9 @@ CREATE TABLE IF NOT EXISTS llm_records (
|
||||
status TEXT,
|
||||
error TEXT,
|
||||
request_body TEXT,
|
||||
response_body TEXT
|
||||
response_body TEXT,
|
||||
raw_request TEXT,
|
||||
raw_response TEXT
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_llm_records_ts ON llm_records(ts);
|
||||
CREATE INDEX IF NOT EXISTS idx_llm_records_session ON llm_records(session_id);
|
||||
@@ -174,6 +183,8 @@ const llmRecordsMigrate = `
|
||||
ALTER TABLE llm_records ADD COLUMN IF NOT EXISTS task_id TEXT;
|
||||
ALTER TABLE llm_records ADD COLUMN IF NOT EXISTS worker TEXT;
|
||||
ALTER TABLE llm_records ADD COLUMN IF NOT EXISTS profile_name TEXT;
|
||||
ALTER TABLE llm_records ADD COLUMN IF NOT EXISTS raw_request TEXT;
|
||||
ALTER TABLE llm_records ADD COLUMN IF NOT EXISTS raw_response TEXT;
|
||||
`
|
||||
|
||||
// EnsureLLMRecordsTable creates the llm_records table if it does not exist.
|
||||
@@ -188,11 +199,12 @@ func (d *DB) EnsureLLMRecordsTable() error {
|
||||
// InsertLLMRecord stores one LLM call record.
|
||||
func (d *DB) InsertLLMRecord(r *LLMRecord) error {
|
||||
_, err := d.Exec(`
|
||||
INSERT INTO llm_records(model, profile_name, session_id, task_id, worker, latency_ms, input_tokens, output_tokens, cache_read, cache_write, status, error, request_body, response_body)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)`,
|
||||
INSERT INTO llm_records(model, profile_name, session_id, task_id, worker, latency_ms, input_tokens, output_tokens, cache_read, cache_write, status, error, request_body, response_body, raw_request, raw_response)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16)`,
|
||||
r.Model, nullIfEmpty(r.ProfileName), r.SessionID, nullIfEmpty(r.TaskID), nullIfEmpty(r.Worker),
|
||||
r.LatencyMs, r.InputTokens, r.OutputTokens, r.CacheRead, r.CacheWrite,
|
||||
r.Status, nullIfEmpty(r.Error), nullIfEmpty(r.RequestBody), nullIfEmpty(r.ResponseBody))
|
||||
r.Status, nullIfEmpty(r.Error), nullIfEmpty(r.RequestBody), nullIfEmpty(r.ResponseBody),
|
||||
nullIfEmpty(r.RawRequest), nullIfEmpty(r.RawResponse))
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -305,19 +317,21 @@ func (d *DB) DeleteLLMRecords(task string) (int64, error) {
|
||||
// GetLLMRecord returns a single LLM record with full request/response bodies.
|
||||
func (d *DB) GetLLMRecord(id int64) (*LLMRecord, error) {
|
||||
var r LLMRecord
|
||||
var reqBody, respBody sql.NullString
|
||||
var reqBody, respBody, rawReq, rawResp sql.NullString
|
||||
err := d.QueryRow(`SELECT id, ts, COALESCE(model,''), COALESCE(profile_name,''), COALESCE(session_id,''), COALESCE(task_id,''), COALESCE(worker,''),
|
||||
COALESCE(latency_ms,0), COALESCE(input_tokens,0), COALESCE(output_tokens,0), COALESCE(cache_read,0), COALESCE(cache_write,0),
|
||||
COALESCE(status,''), COALESCE(error,''), request_body, response_body
|
||||
COALESCE(status,''), COALESCE(error,''), request_body, response_body, raw_request, raw_response
|
||||
FROM llm_records WHERE id=$1`, id).
|
||||
Scan(&r.ID, &r.Ts, &r.Model, &r.ProfileName, &r.SessionID, &r.TaskID, &r.Worker, &r.LatencyMs,
|
||||
&r.InputTokens, &r.OutputTokens, &r.CacheRead, &r.CacheWrite, &r.Status, &r.Error,
|
||||
&reqBody, &respBody)
|
||||
&reqBody, &respBody, &rawReq, &rawResp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.RequestBody = reqBody.String
|
||||
r.ResponseBody = respBody.String
|
||||
r.RawRequest = rawReq.String
|
||||
r.RawResponse = rawResp.String
|
||||
return &r, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -291,7 +291,7 @@ CREATE TABLE IF NOT EXISTS settings (
|
||||
CREATE TABLE IF NOT EXISTS llm_profiles (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
format TEXT NOT NULL CHECK (format IN ('openai','anthropic')),
|
||||
format TEXT NOT NULL CHECK (format IN ('openai','anthropic','openai-responses')),
|
||||
base_url TEXT,
|
||||
proxy TEXT,
|
||||
model TEXT NOT NULL,
|
||||
|
||||
Reference in new issue
Block a user