first commit
This commit is contained in:
627
plugins/logging/operations_test.go
Normal file
627
plugins/logging/operations_test.go
Normal file
@@ -0,0 +1,627 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/maximhq/bifrost/core/schemas"
|
||||
"github.com/maximhq/bifrost/framework/logstore"
|
||||
)
|
||||
|
||||
type testLogger struct{}
|
||||
|
||||
func (testLogger) Debug(string, ...any) {}
|
||||
func (testLogger) Info(string, ...any) {}
|
||||
func (testLogger) Warn(string, ...any) {}
|
||||
func (testLogger) Error(string, ...any) {}
|
||||
func (testLogger) Fatal(string, ...any) {}
|
||||
func (testLogger) SetLevel(schemas.LogLevel) {}
|
||||
func (testLogger) SetOutputType(schemas.LoggerOutputType) {}
|
||||
func (testLogger) LogHTTPRequest(schemas.LogLevel, string) schemas.LogEventBuilder {
|
||||
return schemas.NoopLogEvent
|
||||
}
|
||||
|
||||
func newTestStore(t *testing.T) logstore.LogStore {
|
||||
t.Helper()
|
||||
|
||||
store, err := logstore.NewLogStore(context.Background(), &logstore.Config{
|
||||
Enabled: true,
|
||||
Type: logstore.LogStoreTypeSQLite,
|
||||
Config: &logstore.SQLiteConfig{
|
||||
Path: filepath.Join(t.TempDir(), "logging.db"),
|
||||
},
|
||||
}, testLogger{})
|
||||
if err != nil {
|
||||
t.Fatalf("NewLogStore() error = %v", err)
|
||||
}
|
||||
return store
|
||||
}
|
||||
|
||||
func TestUpdateLogEntryPreservesResponsesInputContentSummary(t *testing.T) {
|
||||
store := newTestStore(t)
|
||||
plugin := &LoggerPlugin{
|
||||
store: store,
|
||||
logger: testLogger{},
|
||||
}
|
||||
|
||||
requestID := "req-1"
|
||||
now := time.Now().UTC()
|
||||
inputText := "request-side text"
|
||||
initial := &InitialLogData{
|
||||
Object: "responses",
|
||||
Provider: "openai",
|
||||
Model: "gpt-4o-mini",
|
||||
ResponsesInputHistory: []schemas.ResponsesMessage{{
|
||||
Content: &schemas.ResponsesMessageContent{
|
||||
ContentStr: &inputText,
|
||||
},
|
||||
}},
|
||||
}
|
||||
|
||||
if err := plugin.insertInitialLogEntry(context.Background(), requestID, "", now, 0, nil, initial); err != nil {
|
||||
t.Fatalf("insertInitialLogEntry() error = %v", err)
|
||||
}
|
||||
|
||||
responsesText := "responses output"
|
||||
update := &UpdateLogData{
|
||||
Status: "success",
|
||||
ResponsesOutput: []schemas.ResponsesMessage{{
|
||||
Content: &schemas.ResponsesMessageContent{
|
||||
ContentStr: &responsesText,
|
||||
},
|
||||
}},
|
||||
}
|
||||
|
||||
if err := plugin.updateLogEntry(context.Background(), requestID, "", "", 10, "", "", "", "", 0, nil, "", update); err != nil {
|
||||
t.Fatalf("updateLogEntry() error = %v", err)
|
||||
}
|
||||
|
||||
logEntry, err := store.FindByID(context.Background(), requestID)
|
||||
if err != nil {
|
||||
t.Fatalf("FindByID() error = %v", err)
|
||||
}
|
||||
if !strings.Contains(logEntry.ContentSummary, inputText) {
|
||||
t.Fatalf("expected content summary to preserve responses input, got %q", logEntry.ContentSummary)
|
||||
}
|
||||
if strings.Contains(logEntry.ContentSummary, responsesText) {
|
||||
t.Fatalf("expected content summary to avoid overwriting with responses output-only data, got %q", logEntry.ContentSummary)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateLogEntryUpdatesContentSummaryForChatOutput(t *testing.T) {
|
||||
store := newTestStore(t)
|
||||
plugin := &LoggerPlugin{
|
||||
store: store,
|
||||
logger: testLogger{},
|
||||
}
|
||||
|
||||
requestID := "req-chat"
|
||||
now := time.Now().UTC()
|
||||
initial := &InitialLogData{
|
||||
Object: "chat_completion",
|
||||
Provider: "openai",
|
||||
Model: "gpt-4o-mini",
|
||||
}
|
||||
|
||||
if err := plugin.insertInitialLogEntry(context.Background(), requestID, "", now, 0, nil, initial); err != nil {
|
||||
t.Fatalf("insertInitialLogEntry() error = %v", err)
|
||||
}
|
||||
|
||||
chatText := "assistant output"
|
||||
update := &UpdateLogData{
|
||||
Status: "success",
|
||||
ChatOutput: &schemas.ChatMessage{
|
||||
Role: schemas.ChatMessageRoleAssistant,
|
||||
Content: &schemas.ChatMessageContent{
|
||||
ContentStr: &chatText,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if err := plugin.updateLogEntry(context.Background(), requestID, "", "", 10, "", "", "", "", 0, nil, "", update); err != nil {
|
||||
t.Fatalf("updateLogEntry() error = %v", err)
|
||||
}
|
||||
|
||||
logEntry, err := store.FindByID(context.Background(), requestID)
|
||||
if err != nil {
|
||||
t.Fatalf("FindByID() error = %v", err)
|
||||
}
|
||||
if !strings.Contains(logEntry.ContentSummary, chatText) {
|
||||
t.Fatalf("expected content summary to include chat output, got %q", logEntry.ContentSummary)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateLogEntrySuppressesChatOutputWhenContentLoggingDisabled(t *testing.T) {
|
||||
store := newTestStore(t)
|
||||
disableContentLogging := true
|
||||
plugin := &LoggerPlugin{
|
||||
store: store,
|
||||
logger: testLogger{},
|
||||
disableContentLogging: &disableContentLogging,
|
||||
}
|
||||
|
||||
requestID := "req-chat-disabled"
|
||||
now := time.Now().UTC()
|
||||
initial := &InitialLogData{
|
||||
Object: "chat_completion",
|
||||
Provider: "openai",
|
||||
Model: "gpt-4o-mini",
|
||||
}
|
||||
|
||||
if err := plugin.insertInitialLogEntry(context.Background(), requestID, "", now, 0, nil, initial); err != nil {
|
||||
t.Fatalf("insertInitialLogEntry() error = %v", err)
|
||||
}
|
||||
|
||||
chatText := "assistant output should not be logged"
|
||||
update := &UpdateLogData{
|
||||
Status: "success",
|
||||
ChatOutput: &schemas.ChatMessage{
|
||||
Role: schemas.ChatMessageRoleAssistant,
|
||||
Content: &schemas.ChatMessageContent{
|
||||
ContentStr: &chatText,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if err := plugin.updateLogEntry(context.Background(), requestID, "", "", 10, "", "", "", "", 0, nil, "", update); err != nil {
|
||||
t.Fatalf("updateLogEntry() error = %v", err)
|
||||
}
|
||||
|
||||
logEntry, err := store.FindByID(context.Background(), requestID)
|
||||
if err != nil {
|
||||
t.Fatalf("FindByID() error = %v", err)
|
||||
}
|
||||
if logEntry.OutputMessage != "" {
|
||||
t.Fatalf("expected output_message to be suppressed, got %q", logEntry.OutputMessage)
|
||||
}
|
||||
if strings.Contains(logEntry.ContentSummary, chatText) {
|
||||
t.Fatalf("expected content summary to suppress chat output, got %q", logEntry.ContentSummary)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStoreOrEnqueueRetryPreservesAllEntries(t *testing.T) {
|
||||
// Simulate fallback/retry scenario where multiple PostLLMHook calls
|
||||
// store entries under the same traceID. All entries must be preserved.
|
||||
plugin := &LoggerPlugin{
|
||||
logger: testLogger{},
|
||||
writeQueue: make(chan *writeQueueEntry, 10),
|
||||
}
|
||||
|
||||
traceID := "trace-retry-test"
|
||||
ctx := schemas.NewBifrostContext(context.Background(), schemas.NoDeadline)
|
||||
ctx.SetValue(schemas.BifrostContextKeyTraceID, traceID)
|
||||
|
||||
// Simulate 3 retry attempts storing entries under the same traceID
|
||||
entry1 := &logstore.Log{ID: "req-attempt-1", Model: "gpt-4o"}
|
||||
entry2 := &logstore.Log{ID: "req-attempt-2", Model: "gpt-4o"}
|
||||
entry3 := &logstore.Log{ID: "req-attempt-3", Model: "claude-3-5-sonnet"}
|
||||
|
||||
plugin.storeOrEnqueueEntry(ctx, entry1, nil)
|
||||
plugin.storeOrEnqueueEntry(ctx, entry2, nil)
|
||||
plugin.storeOrEnqueueEntry(ctx, entry3, nil)
|
||||
|
||||
// Verify all 3 entries are stored
|
||||
val, ok := plugin.pendingLogsToInject.Load(traceID)
|
||||
if !ok {
|
||||
t.Fatal("expected pending entries for traceID, got none")
|
||||
}
|
||||
pending, ok := val.(*pendingInjectEntries)
|
||||
if !ok {
|
||||
t.Fatal("expected *pendingInjectEntries type")
|
||||
}
|
||||
if len(pending.entries) != 3 {
|
||||
t.Fatalf("expected 3 entries, got %d", len(pending.entries))
|
||||
}
|
||||
if pending.entries[0].ID != "req-attempt-1" || pending.entries[1].ID != "req-attempt-2" || pending.entries[2].ID != "req-attempt-3" {
|
||||
t.Fatalf("entries not in expected order: %v, %v, %v", pending.entries[0].ID, pending.entries[1].ID, pending.entries[2].ID)
|
||||
}
|
||||
|
||||
// Now test Inject flushes all entries with plugin logs attached
|
||||
trace := &schemas.Trace{
|
||||
TraceID: traceID,
|
||||
PluginLogs: []schemas.PluginLogEntry{
|
||||
{PluginName: "hello-world", Level: schemas.LogLevelInfo, Message: "test log"},
|
||||
},
|
||||
}
|
||||
|
||||
if err := plugin.Inject(context.Background(), trace); err != nil {
|
||||
t.Fatalf("Inject() error = %v", err)
|
||||
}
|
||||
|
||||
// Verify all 3 entries were enqueued to writeQueue
|
||||
if len(plugin.writeQueue) != 3 {
|
||||
t.Fatalf("expected 3 entries in writeQueue, got %d", len(plugin.writeQueue))
|
||||
}
|
||||
|
||||
// Verify plugin logs were attached to each entry
|
||||
for i := 0; i < 3; i++ {
|
||||
qe := <-plugin.writeQueue
|
||||
if qe.log.PluginLogs == "" {
|
||||
t.Fatalf("entry %d: expected PluginLogs to be set", i)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify pendingLogsToInject was cleaned up
|
||||
if _, ok := plugin.pendingLogsToInject.Load(traceID); ok {
|
||||
t.Fatal("expected pendingLogsToInject to be cleaned up after Inject")
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyRealtimeOutputToEntryBackfillsUserTranscriptFromRawRequest(t *testing.T) {
|
||||
plugin := &LoggerPlugin{}
|
||||
entry := &logstore.Log{}
|
||||
|
||||
assistantText := "Hello!"
|
||||
messageType := schemas.ResponsesMessageTypeMessage
|
||||
assistantRole := schemas.ResponsesInputMessageRoleAssistant
|
||||
result := &schemas.BifrostResponse{
|
||||
ResponsesResponse: &schemas.BifrostResponsesResponse{
|
||||
Output: []schemas.ResponsesMessage{{
|
||||
Type: &messageType,
|
||||
Role: &assistantRole,
|
||||
Content: &schemas.ResponsesMessageContent{
|
||||
ContentStr: &assistantText,
|
||||
},
|
||||
}},
|
||||
ExtraFields: schemas.BifrostResponseExtraFields{
|
||||
RequestType: schemas.RealtimeRequest,
|
||||
RawRequest: `{"type":"conversation.item.input_audio_transcription.completed","transcript":"Hello."}`,
|
||||
RawResponse: `{"type":"response.done"}`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
plugin.applyRealtimeOutputToEntry(entry, result, true)
|
||||
if err := entry.SerializeFields(); err != nil {
|
||||
t.Fatalf("SerializeFields() error = %v", err)
|
||||
}
|
||||
|
||||
if len(entry.InputHistoryParsed) != 1 {
|
||||
t.Fatalf("len(InputHistoryParsed) = %d, want 1", len(entry.InputHistoryParsed))
|
||||
}
|
||||
if entry.InputHistoryParsed[0].Role != schemas.ChatMessageRoleUser {
|
||||
t.Fatalf("InputHistoryParsed[0].Role = %q, want user", entry.InputHistoryParsed[0].Role)
|
||||
}
|
||||
if entry.InputHistoryParsed[0].Content == nil || entry.InputHistoryParsed[0].Content.ContentStr == nil || *entry.InputHistoryParsed[0].Content.ContentStr != "Hello." {
|
||||
t.Fatalf("InputHistoryParsed[0] = %+v, want transcript", entry.InputHistoryParsed[0])
|
||||
}
|
||||
if entry.OutputMessageParsed == nil || entry.OutputMessageParsed.Content == nil || entry.OutputMessageParsed.Content.ContentStr == nil || *entry.OutputMessageParsed.Content.ContentStr != assistantText {
|
||||
t.Fatalf("OutputMessageParsed = %+v, want assistant text", entry.OutputMessageParsed)
|
||||
}
|
||||
if !strings.Contains(entry.ContentSummary, "Hello.") {
|
||||
t.Fatalf("ContentSummary = %q, want user transcript", entry.ContentSummary)
|
||||
}
|
||||
if !strings.Contains(entry.ContentSummary, "Hello!") {
|
||||
t.Fatalf("ContentSummary = %q, want assistant text", entry.ContentSummary)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyRealtimeOutputToEntryBackfillsMissingTranscriptPlaceholder(t *testing.T) {
|
||||
plugin := &LoggerPlugin{}
|
||||
entry := &logstore.Log{}
|
||||
|
||||
assistantText := "Hi there!"
|
||||
messageType := schemas.ResponsesMessageTypeMessage
|
||||
assistantRole := schemas.ResponsesInputMessageRoleAssistant
|
||||
result := &schemas.BifrostResponse{
|
||||
ResponsesResponse: &schemas.BifrostResponsesResponse{
|
||||
Output: []schemas.ResponsesMessage{{
|
||||
Type: &messageType,
|
||||
Role: &assistantRole,
|
||||
Content: &schemas.ResponsesMessageContent{
|
||||
ContentStr: &assistantText,
|
||||
},
|
||||
}},
|
||||
ExtraFields: schemas.BifrostResponseExtraFields{
|
||||
RequestType: schemas.RealtimeRequest,
|
||||
RawRequest: `{"type":"conversation.item.input_audio_transcription.completed","transcript":""}`,
|
||||
RawResponse: `{"type":"response.done"}`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
plugin.applyRealtimeOutputToEntry(entry, result, true)
|
||||
if err := entry.SerializeFields(); err != nil {
|
||||
t.Fatalf("SerializeFields() error = %v", err)
|
||||
}
|
||||
|
||||
if len(entry.InputHistoryParsed) != 1 {
|
||||
t.Fatalf("len(InputHistoryParsed) = %d, want 1", len(entry.InputHistoryParsed))
|
||||
}
|
||||
if entry.InputHistoryParsed[0].Content == nil || entry.InputHistoryParsed[0].Content.ContentStr == nil || *entry.InputHistoryParsed[0].Content.ContentStr != realtimeMissingTranscriptText {
|
||||
t.Fatalf("InputHistoryParsed[0] = %+v, want missing transcript placeholder", entry.InputHistoryParsed[0])
|
||||
}
|
||||
if !strings.Contains(entry.ContentSummary, realtimeMissingTranscriptText) {
|
||||
t.Fatalf("ContentSummary = %q, want missing transcript placeholder", entry.ContentSummary)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyRealtimeOutputToEntryBackfillsDoneMissingTranscriptPlaceholder(t *testing.T) {
|
||||
plugin := &LoggerPlugin{}
|
||||
entry := &logstore.Log{}
|
||||
|
||||
assistantText := "Hi there!"
|
||||
messageType := schemas.ResponsesMessageTypeMessage
|
||||
assistantRole := schemas.ResponsesInputMessageRoleAssistant
|
||||
result := &schemas.BifrostResponse{
|
||||
ResponsesResponse: &schemas.BifrostResponsesResponse{
|
||||
Output: []schemas.ResponsesMessage{{
|
||||
Type: &messageType,
|
||||
Role: &assistantRole,
|
||||
Content: &schemas.ResponsesMessageContent{
|
||||
ContentStr: &assistantText,
|
||||
},
|
||||
}},
|
||||
ExtraFields: schemas.BifrostResponseExtraFields{
|
||||
RequestType: schemas.RealtimeRequest,
|
||||
RawRequest: `{"type":"conversation.item.done","item":{"id":"item_user","type":"message","role":"user","status":"completed","content":[{"type":"input_audio","transcript":null}]}}`,
|
||||
RawResponse: `{"type":"response.done"}`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
plugin.applyRealtimeOutputToEntry(entry, result, true)
|
||||
if err := entry.SerializeFields(); err != nil {
|
||||
t.Fatalf("SerializeFields() error = %v", err)
|
||||
}
|
||||
|
||||
if len(entry.InputHistoryParsed) != 1 {
|
||||
t.Fatalf("len(InputHistoryParsed) = %d, want 1", len(entry.InputHistoryParsed))
|
||||
}
|
||||
if entry.InputHistoryParsed[0].Content == nil || entry.InputHistoryParsed[0].Content.ContentStr == nil || *entry.InputHistoryParsed[0].Content.ContentStr != realtimeMissingTranscriptText {
|
||||
t.Fatalf("InputHistoryParsed[0] = %+v, want missing transcript placeholder", entry.InputHistoryParsed[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyRealtimeOutputToEntryBackfillsRetrievedUserAndToolHistory(t *testing.T) {
|
||||
plugin := &LoggerPlugin{}
|
||||
entry := &logstore.Log{}
|
||||
|
||||
assistantText := "I checked that for you."
|
||||
messageType := schemas.ResponsesMessageTypeMessage
|
||||
assistantRole := schemas.ResponsesInputMessageRoleAssistant
|
||||
result := &schemas.BifrostResponse{
|
||||
ResponsesResponse: &schemas.BifrostResponsesResponse{
|
||||
Output: []schemas.ResponsesMessage{{
|
||||
Type: &messageType,
|
||||
Role: &assistantRole,
|
||||
Content: &schemas.ResponsesMessageContent{
|
||||
ContentStr: &assistantText,
|
||||
},
|
||||
}},
|
||||
ExtraFields: schemas.BifrostResponseExtraFields{
|
||||
RequestType: schemas.RealtimeRequest,
|
||||
RawRequest: strings.Join([]string{
|
||||
`{"type":"conversation.item.retrieved","item":{"id":"item_user","type":"message","role":"user","status":"completed","content":[{"type":"input_text","text":"Where is my order?"}]}}`,
|
||||
`{"type":"conversation.item.retrieved","item":{"id":"item_tool","type":"function_call_output","call_id":"call_123","status":"completed","output":"{\"status\":\"delivered\"}"}}`,
|
||||
}, "\n\n"),
|
||||
RawResponse: `{"type":"response.done"}`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
plugin.applyRealtimeOutputToEntry(entry, result, true)
|
||||
if err := entry.SerializeFields(); err != nil {
|
||||
t.Fatalf("SerializeFields() error = %v", err)
|
||||
}
|
||||
|
||||
if len(entry.InputHistoryParsed) != 2 {
|
||||
t.Fatalf("len(InputHistoryParsed) = %d, want 2", len(entry.InputHistoryParsed))
|
||||
}
|
||||
if entry.InputHistoryParsed[0].Role != schemas.ChatMessageRoleUser {
|
||||
t.Fatalf("InputHistoryParsed[0].Role = %q, want user", entry.InputHistoryParsed[0].Role)
|
||||
}
|
||||
if entry.InputHistoryParsed[0].Content == nil || entry.InputHistoryParsed[0].Content.ContentStr == nil || *entry.InputHistoryParsed[0].Content.ContentStr != "Where is my order?" {
|
||||
t.Fatalf("InputHistoryParsed[0] = %+v, want user content", entry.InputHistoryParsed[0])
|
||||
}
|
||||
if entry.InputHistoryParsed[1].Role != schemas.ChatMessageRoleTool {
|
||||
t.Fatalf("InputHistoryParsed[1].Role = %q, want tool", entry.InputHistoryParsed[1].Role)
|
||||
}
|
||||
if entry.InputHistoryParsed[1].Content == nil || entry.InputHistoryParsed[1].Content.ContentStr == nil || *entry.InputHistoryParsed[1].Content.ContentStr != `{"status":"delivered"}` {
|
||||
t.Fatalf("InputHistoryParsed[1] = %+v, want tool content", entry.InputHistoryParsed[1])
|
||||
}
|
||||
if entry.InputHistoryParsed[1].ChatToolMessage == nil || entry.InputHistoryParsed[1].ChatToolMessage.ToolCallID == nil || *entry.InputHistoryParsed[1].ChatToolMessage.ToolCallID != "call_123" {
|
||||
t.Fatalf("InputHistoryParsed[1].ChatToolMessage = %+v, want tool call id", entry.InputHistoryParsed[1].ChatToolMessage)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyRealtimeOutputToEntryBackfillsCreatedUserAndToolHistory(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
plugin := &LoggerPlugin{}
|
||||
entry := &logstore.Log{}
|
||||
result := &schemas.BifrostResponse{
|
||||
ResponsesResponse: &schemas.BifrostResponsesResponse{
|
||||
ExtraFields: schemas.BifrostResponseExtraFields{
|
||||
RawRequest: strings.Join([]string{
|
||||
`{"type":"conversation.item.created","item":{"id":"item_user","type":"message","role":"user","status":"completed","content":[{"type":"input_text","text":"I need help"}]}}`,
|
||||
`{"type":"conversation.item.created","item":{"id":"item_tool","type":"function_call_output","call_id":"call_456","status":"completed","output":"{\"status\":\"ok\"}"}}`,
|
||||
}, "\n\n"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
plugin.applyRealtimeOutputToEntry(entry, result, true)
|
||||
|
||||
if len(entry.InputHistoryParsed) != 2 {
|
||||
t.Fatalf("len(InputHistoryParsed) = %d, want 2", len(entry.InputHistoryParsed))
|
||||
}
|
||||
if entry.InputHistoryParsed[0].Role != schemas.ChatMessageRoleUser {
|
||||
t.Fatalf("InputHistoryParsed[0].Role = %q, want user", entry.InputHistoryParsed[0].Role)
|
||||
}
|
||||
if entry.InputHistoryParsed[0].Content == nil || entry.InputHistoryParsed[0].Content.ContentStr == nil || *entry.InputHistoryParsed[0].Content.ContentStr != "I need help" {
|
||||
t.Fatalf("InputHistoryParsed[0] = %+v, want user content", entry.InputHistoryParsed[0])
|
||||
}
|
||||
if entry.InputHistoryParsed[1].Role != schemas.ChatMessageRoleTool {
|
||||
t.Fatalf("InputHistoryParsed[1].Role = %q, want tool", entry.InputHistoryParsed[1].Role)
|
||||
}
|
||||
if entry.InputHistoryParsed[1].Content == nil || entry.InputHistoryParsed[1].Content.ContentStr == nil || *entry.InputHistoryParsed[1].Content.ContentStr != `{"status":"ok"}` {
|
||||
t.Fatalf("InputHistoryParsed[1] = %+v, want tool content", entry.InputHistoryParsed[1])
|
||||
}
|
||||
if entry.InputHistoryParsed[1].ChatToolMessage == nil || entry.InputHistoryParsed[1].ChatToolMessage.ToolCallID == nil || *entry.InputHistoryParsed[1].ChatToolMessage.ToolCallID != "call_456" {
|
||||
t.Fatalf("InputHistoryParsed[1].ChatToolMessage = %+v, want tool call id", entry.InputHistoryParsed[1].ChatToolMessage)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyRealtimeOutputToEntryBackfillsAddedUserAndToolHistory(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
plugin := &LoggerPlugin{}
|
||||
entry := &logstore.Log{}
|
||||
|
||||
assistantText := "Done."
|
||||
messageType := schemas.ResponsesMessageTypeMessage
|
||||
assistantRole := schemas.ResponsesInputMessageRoleAssistant
|
||||
result := &schemas.BifrostResponse{
|
||||
ResponsesResponse: &schemas.BifrostResponsesResponse{
|
||||
Output: []schemas.ResponsesMessage{{
|
||||
Type: &messageType,
|
||||
Role: &assistantRole,
|
||||
Content: &schemas.ResponsesMessageContent{
|
||||
ContentStr: &assistantText,
|
||||
},
|
||||
}},
|
||||
ExtraFields: schemas.BifrostResponseExtraFields{
|
||||
RequestType: schemas.RealtimeRequest,
|
||||
RawRequest: strings.Join([]string{
|
||||
`{"type":"conversation.item.added","item":{"id":"item_user","type":"message","role":"user","status":"completed","content":[{"type":"input_text","text":"hello from added item"}]}}`,
|
||||
`{"type":"conversation.item.added","item":{"id":"item_tool","type":"function_call_output","call_id":"call_added","status":"completed","output":"{\"status\":\"ok\"}"}}`,
|
||||
}, "\n\n"),
|
||||
RawResponse: `{"type":"response.done"}`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
plugin.applyRealtimeOutputToEntry(entry, result, true)
|
||||
if err := entry.SerializeFields(); err != nil {
|
||||
t.Fatalf("SerializeFields() error = %v", err)
|
||||
}
|
||||
|
||||
if len(entry.InputHistoryParsed) != 2 {
|
||||
t.Fatalf("len(InputHistoryParsed) = %d, want 2", len(entry.InputHistoryParsed))
|
||||
}
|
||||
if entry.InputHistoryParsed[0].Content == nil || entry.InputHistoryParsed[0].Content.ContentStr == nil || *entry.InputHistoryParsed[0].Content.ContentStr != "hello from added item" {
|
||||
t.Fatalf("InputHistoryParsed[0] = %+v, want added user content", entry.InputHistoryParsed[0])
|
||||
}
|
||||
if entry.InputHistoryParsed[1].ChatToolMessage == nil || entry.InputHistoryParsed[1].ChatToolMessage.ToolCallID == nil || *entry.InputHistoryParsed[1].ChatToolMessage.ToolCallID != "call_added" {
|
||||
t.Fatalf("InputHistoryParsed[1].ChatToolMessage = %+v, want added tool call id", entry.InputHistoryParsed[1].ChatToolMessage)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyRealtimeOutputToEntryMergesRawTranscriptIntoStructuredRealtimeHistory(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
plugin := &LoggerPlugin{}
|
||||
entry := &logstore.Log{
|
||||
InputHistoryParsed: []schemas.ChatMessage{
|
||||
{
|
||||
Role: schemas.ChatMessageRoleUser,
|
||||
Content: &schemas.ChatMessageContent{
|
||||
ContentStr: schemas.Ptr("Can you help with my ticket?"),
|
||||
},
|
||||
},
|
||||
{
|
||||
Role: schemas.ChatMessageRoleTool,
|
||||
Content: &schemas.ChatMessageContent{
|
||||
ContentStr: schemas.Ptr(`{"status":"open"}`),
|
||||
},
|
||||
ChatToolMessage: &schemas.ChatToolMessage{
|
||||
ToolCallID: schemas.Ptr("call_789"),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
assistantText := "Let me check."
|
||||
messageType := schemas.ResponsesMessageTypeMessage
|
||||
assistantRole := schemas.ResponsesInputMessageRoleAssistant
|
||||
result := &schemas.BifrostResponse{
|
||||
ResponsesResponse: &schemas.BifrostResponsesResponse{
|
||||
Output: []schemas.ResponsesMessage{{
|
||||
Type: &messageType,
|
||||
Role: &assistantRole,
|
||||
Content: &schemas.ResponsesMessageContent{
|
||||
ContentStr: &assistantText,
|
||||
},
|
||||
}},
|
||||
ExtraFields: schemas.BifrostResponseExtraFields{
|
||||
RequestType: schemas.RealtimeRequest,
|
||||
RawRequest: strings.Join([]string{
|
||||
`{"type":"conversation.item.input_audio_transcription.completed","transcript":"Hello."}`,
|
||||
`{"type":"conversation.item.retrieved","item":{"id":"item_tool","type":"function_call_output","call_id":"call_789","status":"completed","output":"{\"status\":\"open\"}"}}`,
|
||||
}, "\n\n"),
|
||||
RawResponse: `{"type":"response.done"}`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
plugin.applyRealtimeOutputToEntry(entry, result, true)
|
||||
if err := entry.SerializeFields(); err != nil {
|
||||
t.Fatalf("SerializeFields() error = %v", err)
|
||||
}
|
||||
|
||||
if len(entry.InputHistoryParsed) != 3 {
|
||||
t.Fatalf("len(InputHistoryParsed) = %d, want 3", len(entry.InputHistoryParsed))
|
||||
}
|
||||
if entry.InputHistoryParsed[0].Content == nil || entry.InputHistoryParsed[0].Content.ContentStr == nil || *entry.InputHistoryParsed[0].Content.ContentStr != "Can you help with my ticket?" {
|
||||
t.Fatalf("InputHistoryParsed[0] = %+v, want structured user content", entry.InputHistoryParsed[0])
|
||||
}
|
||||
if entry.InputHistoryParsed[1].Role != schemas.ChatMessageRoleUser {
|
||||
t.Fatalf("InputHistoryParsed[1].Role = %q, want user", entry.InputHistoryParsed[1].Role)
|
||||
}
|
||||
if entry.InputHistoryParsed[1].Content == nil || entry.InputHistoryParsed[1].Content.ContentStr == nil || *entry.InputHistoryParsed[1].Content.ContentStr != "Hello." {
|
||||
t.Fatalf("InputHistoryParsed[1] = %+v, want raw transcript merge", entry.InputHistoryParsed[1])
|
||||
}
|
||||
if entry.InputHistoryParsed[2].Role != schemas.ChatMessageRoleTool {
|
||||
t.Fatalf("InputHistoryParsed[2].Role = %q, want tool", entry.InputHistoryParsed[2].Role)
|
||||
}
|
||||
if entry.InputHistoryParsed[2].ChatToolMessage == nil || entry.InputHistoryParsed[2].ChatToolMessage.ToolCallID == nil || *entry.InputHistoryParsed[2].ChatToolMessage.ToolCallID != "call_789" {
|
||||
t.Fatalf("InputHistoryParsed[2].ChatToolMessage = %+v, want original tool call id", entry.InputHistoryParsed[2].ChatToolMessage)
|
||||
}
|
||||
if strings.Count(entry.ContentSummary, "Hello.") != 1 {
|
||||
t.Fatalf("ContentSummary = %q, want one merged transcript", entry.ContentSummary)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyRealtimeOutputToEntryDoesNotPersistRawWhenShouldStoreRawFalse(t *testing.T) {
|
||||
plugin := &LoggerPlugin{}
|
||||
entry := &logstore.Log{}
|
||||
|
||||
assistantText := "Hello!"
|
||||
messageType := schemas.ResponsesMessageTypeMessage
|
||||
assistantRole := schemas.ResponsesInputMessageRoleAssistant
|
||||
result := &schemas.BifrostResponse{
|
||||
ResponsesResponse: &schemas.BifrostResponsesResponse{
|
||||
Output: []schemas.ResponsesMessage{{
|
||||
Type: &messageType,
|
||||
Role: &assistantRole,
|
||||
Content: &schemas.ResponsesMessageContent{
|
||||
ContentStr: &assistantText,
|
||||
},
|
||||
}},
|
||||
ExtraFields: schemas.BifrostResponseExtraFields{
|
||||
RequestType: schemas.RealtimeRequest,
|
||||
RawRequest: `{"type":"conversation.item.input_audio_transcription.completed","transcript":"Hello."}`,
|
||||
RawResponse: `{"type":"response.done"}`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
plugin.applyRealtimeOutputToEntry(entry, result, false)
|
||||
|
||||
if entry.RawRequest != "" {
|
||||
t.Fatalf("expected RawRequest to remain empty when shouldStoreRaw=false, got %q", entry.RawRequest)
|
||||
}
|
||||
if entry.RawResponse != "" {
|
||||
t.Fatalf("expected RawResponse to remain empty when shouldStoreRaw=false, got %q", entry.RawResponse)
|
||||
}
|
||||
if len(entry.InputHistoryParsed) == 0 {
|
||||
t.Fatal("expected InputHistoryParsed to still be backfilled when shouldStoreRaw=false")
|
||||
}
|
||||
if entry.InputHistoryParsed[0].Role != schemas.ChatMessageRoleUser {
|
||||
t.Fatalf("InputHistoryParsed[0].Role = %q, want user", entry.InputHistoryParsed[0].Role)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user