feat(chat,seahorse): persist and display model_name across history (#2897)

* feat(chat,seahorse): persist and display model_name across history

* test(seahorse): fix lint regressions in repair coverage

* fix(pico): preserve model_name in live updates

* fix(pico): preserve model_name through live stream wrappers
This commit is contained in:
LC
2026-05-20 13:42:21 +08:00
committed by GitHub
parent 548dc15acd
commit b7db059544
41 changed files with 1266 additions and 139 deletions
+11
View File
@@ -20,6 +20,17 @@ type MessageEditor interface {
EditMessage(ctx context.Context, chatID string, messageID string, content string) error
}
// MessageEditorWithPayload extends MessageEditor for channels that can update
// structured message metadata in addition to plain text content.
type MessageEditorWithPayload interface {
EditMessageWithPayload(
ctx context.Context,
chatID string,
messageID string,
payload map[string]any,
) error
}
// MessageDeleter — channels that can delete a message by ID.
type MessageDeleter interface {
DeleteMessage(ctx context.Context, chatID string, messageID string) error
+63 -2
View File
@@ -191,6 +191,19 @@ func outboundMessageBypassesPlaceholderEdit(msg bus.OutboundMessage) bool {
return strings.EqualFold(kind, "thought") || strings.EqualFold(kind, "tool_calls")
}
func outboundMessageEditPayload(msg bus.OutboundMessage, content string) map[string]any {
payload := map[string]any{
"content": content,
}
if len(msg.Context.Raw) == 0 {
return payload
}
if modelName := strings.TrimSpace(msg.Context.Raw["model_name"]); modelName != "" {
payload["model_name"] = modelName
}
return payload
}
func outboundMediaChannel(msg bus.OutboundMediaMessage) string {
return msg.Context.Channel
}
@@ -394,7 +407,16 @@ func (m *Manager) preSend(ctx context.Context, name string, msg bus.OutboundMess
if deleter, ok := ch.(MessageDeleter); ok {
deleter.DeleteMessage(ctx, chatID, entry.id) // best effort
} else if editor, ok := ch.(MessageEditor); ok {
editor.EditMessage(ctx, chatID, entry.id, msg.Content) // fallback
if payloadEditor, ok := ch.(MessageEditorWithPayload); ok {
_ = payloadEditor.EditMessageWithPayload(
ctx,
chatID,
entry.id,
outboundMessageEditPayload(msg, msg.Content),
)
} else {
editor.EditMessage(ctx, chatID, entry.id, msg.Content) // fallback
}
}
}
}
@@ -446,7 +468,18 @@ func (m *Manager) preSend(ctx context.Context, name string, msg bus.OutboundMess
trackedContent = prepareToolFeedbackMessageContent(ch, msg.Content)
content = InitialAnimatedToolFeedbackContent(trackedContent)
}
if err := editor.EditMessage(ctx, chatID, entry.id, content); err == nil {
err := func() error {
if payloadEditor, ok := ch.(MessageEditorWithPayload); ok {
return payloadEditor.EditMessageWithPayload(
ctx,
chatID,
entry.id,
outboundMessageEditPayload(msg, content),
)
}
return editor.EditMessage(ctx, chatID, entry.id, content)
}()
if err == nil {
trackedChatID := trackedToolFeedbackMessageChatID(ch, chatID, &msg.Context)
if tracker, ok := ch.(toolFeedbackMessageTracker); ok && isToolFeedback {
tracker.RecordToolFeedbackMessage(trackedChatID, entry.id, trackedContent)
@@ -643,6 +676,18 @@ func reasoningStreamerFrom(streamer bus.Streamer) bus.ReasoningStreamer {
return nil
}
type modelNameStreamer interface {
SetModelName(modelName string)
}
func setStreamerModelName(streamer any, modelName string) {
setter, ok := streamer.(modelNameStreamer)
if !ok {
return
}
setter.SetModelName(modelName)
}
// splitMarkerStreamer turns accumulated streaming text containing
// MessageSplitMarker into separate channel stream messages.
type splitMarkerStreamer struct {
@@ -654,6 +699,7 @@ type splitMarkerStreamer struct {
finalized bool
onFinalize func(context.Context, string)
clearMarker func()
modelName string
}
func (s *splitMarkerStreamer) Update(ctx context.Context, content string) error {
@@ -682,6 +728,7 @@ func (s *splitMarkerStreamer) UpdateReasoning(ctx context.Context, content strin
if s.reasoning == nil {
return nil
}
setStreamerModelName(s.reasoning, s.modelName)
return s.reasoning.UpdateReasoning(ctx, content)
}
@@ -691,9 +738,18 @@ func (s *splitMarkerStreamer) FinalizeReasoning(ctx context.Context, content str
if s.reasoning == nil {
return nil
}
setStreamerModelName(s.reasoning, s.modelName)
return s.reasoning.FinalizeReasoning(ctx, content)
}
func (s *splitMarkerStreamer) SetModelName(modelName string) {
s.mu.Lock()
defer s.mu.Unlock()
s.modelName = strings.TrimSpace(modelName)
setStreamerModelName(s.current, s.modelName)
setStreamerModelName(s.reasoning, s.modelName)
}
func (s *splitMarkerStreamer) Cancel(ctx context.Context) {
s.mu.Lock()
defer s.mu.Unlock()
@@ -772,6 +828,7 @@ func (s *splitMarkerStreamer) ensureCurrentLocked(ctx context.Context) error {
return err
}
s.current = streamer
setStreamerModelName(s.current, s.modelName)
return nil
}
@@ -856,6 +913,10 @@ func (s *finalizeHookStreamer) FinalizeReasoning(ctx context.Context, content st
return nil
}
func (s *finalizeHookStreamer) SetModelName(modelName string) {
setStreamerModelName(s.Streamer, strings.TrimSpace(modelName))
}
func (s *finalizeHookStreamer) runFinalizeHook(ctx context.Context, content string) {
if s.onFinalize != nil {
s.onFinalize(ctx, content)
+102
View File
@@ -142,11 +142,21 @@ func (m *mockReasoningStreamer) FinalizeReasoning(_ context.Context, content str
return nil
}
type modelTrackingReasoningStreamer struct {
mockReasoningStreamer
modelNames []string
}
func (m *modelTrackingReasoningStreamer) SetModelName(modelName string) {
m.modelNames = append(m.modelNames, strings.TrimSpace(modelName))
}
type recordingStreamSegment struct {
updates []string
finals []string
finalUsage *bus.ContextUsage
canceledCount int
modelNames []string
}
func (s *recordingStreamSegment) Update(_ context.Context, content string) error {
@@ -168,6 +178,10 @@ func (s *recordingStreamSegment) Cancel(context.Context) {
s.canceledCount++
}
func (s *recordingStreamSegment) SetModelName(modelName string) {
s.modelNames = append(s.modelNames, strings.TrimSpace(modelName))
}
type mockStreamingChannel struct {
mockMessageEditor
streamer Streamer
@@ -2068,6 +2082,42 @@ func TestGetStreamer_PreservesReasoningStreamer(t *testing.T) {
}
}
func TestGetStreamer_PreservesModelNameSetter(t *testing.T) {
m := newTestManager()
inner := &modelTrackingReasoningStreamer{}
ch := &mockStreamingChannel{
streamer: inner,
}
m.channels["test"] = ch
streamer, ok := m.GetStreamer(context.Background(), "test", "123", "")
if !ok {
t.Fatal("expected streamer to be available")
}
setter, ok := streamer.(interface{ SetModelName(modelName string) })
if !ok {
t.Fatal("manager-wrapped streamer should preserve SetModelName")
}
setter.SetModelName("gpt-5.4")
if err := streamer.Update(context.Background(), "hello"); err != nil {
t.Fatalf("Update() error = %v", err)
}
reasoningStreamer, ok := streamer.(bus.ReasoningStreamer)
if !ok {
t.Fatal("manager-wrapped streamer should preserve ReasoningStreamer")
}
setter.SetModelName("gpt-5.4")
if err := reasoningStreamer.UpdateReasoning(context.Background(), "thinking"); err != nil {
t.Fatalf("UpdateReasoning() error = %v", err)
}
if len(inner.modelNames) != 2 {
t.Fatalf("model name calls = %v, want 2 forwarded calls", inner.modelNames)
}
if inner.modelNames[0] != "gpt-5.4" || inner.modelNames[1] != "gpt-5.4" {
t.Fatalf("model name calls = %v, want both forwarded as gpt-5.4", inner.modelNames)
}
}
func TestGetStreamer_SplitOnMarkerStreamsSeparateSegments(t *testing.T) {
m := newTestManager()
m.config = &config.Config{
@@ -2188,6 +2238,58 @@ func TestGetStreamer_SplitOnMarkerKeepsReasoningOnInitialStreamer(t *testing.T)
}
}
func TestGetStreamer_SplitOnMarkerPreservesModelNameSetter(t *testing.T) {
m := newTestManager()
m.config = &config.Config{
Agents: config.AgentsConfig{
Defaults: config.AgentDefaults{
SplitOnMarker: true,
},
},
}
initial := &modelTrackingReasoningStreamer{}
next := &recordingStreamSegment{}
callCount := 0
ch := &mockStreamingChannel{
beginStreamFn: func(context.Context, string) (Streamer, error) {
callCount++
if callCount == 1 {
return initial, nil
}
return next, nil
},
}
m.channels["test"] = ch
streamer, ok := m.GetStreamer(context.Background(), "test", "123", "")
if !ok {
t.Fatal("expected streamer to be available")
}
setter, ok := streamer.(interface{ SetModelName(modelName string) })
if !ok {
t.Fatal("split streamer should preserve SetModelName")
}
setter.SetModelName("gpt-5.4-mini")
if err := streamer.Update(context.Background(), "hello<|[SPLIT]|>world"); err != nil {
t.Fatalf("Update() error = %v", err)
}
reasoningStreamer, ok := streamer.(bus.ReasoningStreamer)
if !ok {
t.Fatal("split streamer should preserve ReasoningStreamer")
}
if err := reasoningStreamer.UpdateReasoning(context.Background(), "thinking"); err != nil {
t.Fatalf("UpdateReasoning() error = %v", err)
}
if len(initial.modelNames) == 0 || initial.modelNames[0] != "gpt-5.4-mini" {
t.Fatalf("initial model names = %v, want forwarded gpt-5.4-mini", initial.modelNames)
}
if len(next.modelNames) == 0 || next.modelNames[0] != "gpt-5.4-mini" {
t.Fatalf("next model names = %v, want forwarded gpt-5.4-mini", next.modelNames)
}
}
func TestGetStreamer_FinalizeSeparateMessagesClearsTrackedToolFeedback(t *testing.T) {
m := newTestManager()
m.config = &config.Config{
+91 -9
View File
@@ -325,6 +325,9 @@ func (c *PicoChannel) Send(ctx context.Context, msg bus.OutboundMessage) ([]stri
PayloadKeyContent: content,
"message_id": msgID,
}
if modelName := strings.TrimSpace(msg.Context.Raw[PayloadKeyModelName]); modelName != "" {
payload[PayloadKeyModelName] = modelName
}
switch {
case isThought:
payload[PayloadKeyKind] = MessageKindThought
@@ -359,6 +362,15 @@ func (c *PicoChannel) EditMessage(ctx context.Context, chatID string, messageID
return c.editMessage(ctx, chatID, messageID, content, nil)
}
func (c *PicoChannel) EditMessageWithPayload(
ctx context.Context,
chatID string,
messageID string,
payload map[string]any,
) error {
return c.editMessagePayload(ctx, chatID, messageID, payload, nil)
}
// DeleteMessage implements channels.MessageDeleter.
func (c *PicoChannel) DeleteMessage(ctx context.Context, chatID string, messageID string) error {
outMsg := newMessage(TypeMessageDelete, map[string]any{
@@ -419,14 +431,23 @@ func (c *PicoChannel) finalizeTrackedToolFeedbackMessage(
ctx context.Context,
chatID string,
content string,
editFn func(context.Context, string, string, string, *bus.ContextUsage) error,
editFn func(context.Context, string, string, map[string]any, *bus.ContextUsage) error,
payload map[string]any,
contextUsage *bus.ContextUsage,
) ([]string, bool) {
msgID, baseContent, ok := c.takeToolFeedbackMessage(chatID)
if !ok || editFn == nil {
return nil, false
}
if err := editFn(ctx, chatID, msgID, content, contextUsage); err != nil {
if payload == nil {
payload = map[string]any{
PayloadKeyContent: content,
}
}
if _, ok := payload[PayloadKeyContent]; !ok {
payload[PayloadKeyContent] = content
}
if err := editFn(ctx, chatID, msgID, payload, contextUsage); err != nil {
c.RecordToolFeedbackMessage(chatID, msgID, baseContent)
return nil, false
}
@@ -437,7 +458,20 @@ func (c *PicoChannel) FinalizeToolFeedbackMessage(ctx context.Context, msg bus.O
if !outboundMessageFinalizesTrackedToolFeedback(msg) {
return nil, false
}
return c.finalizeTrackedToolFeedbackMessage(ctx, msg.ChatID, msg.Content, c.editMessage, msg.ContextUsage)
payload := map[string]any{
PayloadKeyContent: msg.Content,
}
if modelName := strings.TrimSpace(msg.Context.Raw[PayloadKeyModelName]); modelName != "" {
payload[PayloadKeyModelName] = modelName
}
return c.finalizeTrackedToolFeedbackMessage(
ctx,
msg.ChatID,
msg.Content,
c.editMessagePayload,
payload,
msg.ContextUsage,
)
}
// StartTyping implements channels.TypingCapable.
@@ -496,6 +530,7 @@ func (c *PicoChannel) BeginStream(ctx context.Context, chatID string) (channels.
type picoStreamer struct {
channel *PicoChannel
chatID string
modelName string
messageID string
reasoningID string
throttleInterval time.Duration
@@ -509,6 +544,15 @@ type picoStreamer struct {
mu sync.Mutex
}
func (s *picoStreamer) SetModelName(modelName string) {
if s == nil {
return
}
s.mu.Lock()
defer s.mu.Unlock()
s.modelName = strings.TrimSpace(modelName)
}
func (s *picoStreamer) Update(ctx context.Context, content string) error {
s.mu.Lock()
defer s.mu.Unlock()
@@ -613,13 +657,23 @@ func (s *picoStreamer) sendLocked(ctx context.Context, content string, contextUs
PayloadKeyContent: content,
"message_id": s.messageID,
}
if s.modelName != "" {
payload[PayloadKeyModelName] = s.modelName
}
setContextUsagePayload(payload, contextUsage)
outMsg := newMessage(TypeMessageCreate, payload)
if err := s.channel.broadcastToSession(s.chatID, outMsg); err != nil {
return err
}
} else if content != s.lastContent || contextUsage != nil {
if err := s.channel.editMessage(ctx, s.chatID, s.messageID, content, contextUsage); err != nil {
payload := map[string]any{
PayloadKeyContent: content,
"message_id": s.messageID,
}
if s.modelName != "" {
payload[PayloadKeyModelName] = s.modelName
}
if err := s.channel.editMessagePayload(ctx, s.chatID, s.messageID, payload, contextUsage); err != nil {
return err
}
}
@@ -642,6 +696,9 @@ func (s *picoStreamer) sendReasoningLocked(ctx context.Context, content string)
PayloadKeyKind: MessageKindThought,
PayloadKeyThought: true,
}
if s.modelName != "" {
payload[PayloadKeyModelName] = s.modelName
}
outMsg := newMessage(TypeMessageCreate, payload)
if err := s.channel.broadcastToSession(s.chatID, outMsg); err != nil {
return err
@@ -653,6 +710,9 @@ func (s *picoStreamer) sendReasoningLocked(ctx context.Context, content string)
PayloadKeyKind: MessageKindThought,
PayloadKeyThought: true,
}
if s.modelName != "" {
payload[PayloadKeyModelName] = s.modelName
}
outMsg := newMessage(TypeMessageUpdate, payload)
if err := s.channel.broadcastToSession(s.chatID, outMsg); err != nil {
return err
@@ -744,6 +804,9 @@ func (c *PicoChannel) SendMedia(ctx context.Context, msg bus.OutboundMediaMessag
"attachments": attachments,
"message_id": msgID,
})
if modelName := strings.TrimSpace(msg.Context.Raw[PayloadKeyModelName]); modelName != "" {
outMsg.Payload[PayloadKeyModelName] = modelName
}
if err := c.broadcastToSession(msg.ChatID, outMsg); err != nil {
return nil, err
@@ -1358,11 +1421,30 @@ func (c *PicoChannel) editMessage(
content string,
contextUsage *bus.ContextUsage,
) error {
payload := map[string]any{
"message_id": messageID,
"content": content,
return c.editMessagePayload(ctx, chatID, messageID, map[string]any{
PayloadKeyContent: content,
}, contextUsage)
}
func (c *PicoChannel) editMessagePayload(
ctx context.Context,
chatID string,
messageID string,
payload map[string]any,
contextUsage *bus.ContextUsage,
) error {
if payload == nil {
payload = map[string]any{}
}
setContextUsagePayload(payload, contextUsage)
outMsg := newMessage(TypeMessageUpdate, payload)
normalized := make(map[string]any, len(payload)+1)
for key, value := range payload {
normalized[key] = value
}
if _, ok := normalized[PayloadKeyContent]; !ok {
normalized[PayloadKeyContent] = ""
}
normalized["message_id"] = messageID
setContextUsagePayload(normalized, contextUsage)
outMsg := newMessage(TypeMessageUpdate, normalized)
return c.broadcastToSession(chatID, outMsg)
}
+90 -4
View File
@@ -46,12 +46,15 @@ func TestFinalizeTrackedToolFeedbackMessage_StopsTrackingBeforeEdit(t *testing.T
context.Background(),
"pico:chat-1",
"final reply",
func(_ context.Context, chatID, messageID, content string, contextUsage *bus.ContextUsage) error {
func(_ context.Context, chatID, messageID string, payload map[string]any, contextUsage *bus.ContextUsage) error {
if _, ok := ch.currentToolFeedbackMessage(chatID); ok {
t.Fatal("expected tracked tool feedback to be stopped before edit")
}
if chatID != "pico:chat-1" || messageID != "msg-1" || content != "final reply" {
t.Fatalf("unexpected edit args: %s %s %s", chatID, messageID, content)
if chatID != "pico:chat-1" || messageID != "msg-1" {
t.Fatalf("unexpected edit args: %s %s", chatID, messageID)
}
if got := payload[PayloadKeyContent]; got != "final reply" {
t.Fatalf("unexpected content payload: %#v", got)
}
if contextUsage != nil {
t.Fatalf("unexpected context usage: %+v", contextUsage)
@@ -59,6 +62,7 @@ func TestFinalizeTrackedToolFeedbackMessage_StopsTrackingBeforeEdit(t *testing.T
return nil
},
nil,
nil,
)
if !handled {
t.Fatal("expected finalizeTrackedToolFeedbackMessage to handle tracked message")
@@ -115,7 +119,8 @@ func TestSend_ThoughtMessageDoesNotFinalizeTrackedToolFeedback(t *testing.T) {
Channel: "pico",
ChatID: "pico:sess-1",
Raw: map[string]string{
"message_kind": MessageKindThought,
"message_kind": MessageKindThought,
PayloadKeyModelName: "gpt-5.4-mini",
},
},
}); err != nil {
@@ -134,6 +139,9 @@ func TestSend_ThoughtMessageDoesNotFinalizeTrackedToolFeedback(t *testing.T) {
if got := payload[PayloadKeyKind]; got != MessageKindThought {
t.Fatalf("thought kind = %#v, want %q", got, MessageKindThought)
}
if got := payload[PayloadKeyModelName]; got != "gpt-5.4-mini" {
t.Fatalf("thought model_name = %#v, want %q", got, "gpt-5.4-mini")
}
if got := payload["message_id"]; got == "msg-progress" || got == nil || got == "" {
t.Fatalf("thought message_id = %#v, want new non-progress id", got)
}
@@ -151,6 +159,9 @@ func TestSend_ThoughtMessageDoesNotFinalizeTrackedToolFeedback(t *testing.T) {
Context: bus.InboundContext{
Channel: "pico",
ChatID: "pico:sess-1",
Raw: map[string]string{
PayloadKeyModelName: "gpt-5.4",
},
},
ContextUsage: &bus.ContextUsage{
UsedTokens: 321,
@@ -174,6 +185,9 @@ func TestSend_ThoughtMessageDoesNotFinalizeTrackedToolFeedback(t *testing.T) {
if got := payload[PayloadKeyContent]; got != "final reply" {
t.Fatalf("final content = %#v, want %q", got, "final reply")
}
if got := payload[PayloadKeyModelName]; got != "gpt-5.4" {
t.Fatalf("final model_name = %#v, want %q", got, "gpt-5.4")
}
rawUsage, ok := payload["context_usage"].(map[string]any)
if !ok {
t.Fatalf("final context_usage = %#v, want map payload", payload["context_usage"])
@@ -193,6 +207,54 @@ func TestSend_ThoughtMessageDoesNotFinalizeTrackedToolFeedback(t *testing.T) {
}
}
func TestSend_ToolCallsMessageIncludesModelName(t *testing.T) {
ch := newTestPicoChannel(t)
if err := ch.Start(context.Background()); err != nil {
t.Fatalf("Start() error = %v", err)
}
defer ch.Stop(context.Background())
clientConn, received, cleanup := newTestPicoWebSocket(t)
defer cleanup()
ch.addConnForTest(&picoConn{id: "conn-1", conn: clientConn, sessionID: "sess-1"})
if _, err := ch.Send(context.Background(), bus.OutboundMessage{
ChatID: "pico:sess-1",
Content: "",
Context: bus.InboundContext{
Channel: "pico",
ChatID: "pico:sess-1",
Raw: map[string]string{
"message_kind": MessageKindToolCalls,
PayloadKeyModelName: "gpt-5.4",
PayloadKeyToolCalls: `[{"id":"call_1","type":"function","function":{"name":"read_file","arguments":"{\"path\":\"README.md\"}"}}]`,
},
},
}); err != nil {
t.Fatalf("Send(tool_calls) error = %v", err)
}
select {
case msg := <-received:
if msg.Type != TypeMessageCreate {
t.Fatalf("tool_calls message type = %q, want %q", msg.Type, TypeMessageCreate)
}
payload := msg.Payload
if got := payload[PayloadKeyKind]; got != MessageKindToolCalls {
t.Fatalf("tool_calls kind = %#v, want %q", got, MessageKindToolCalls)
}
if got := payload[PayloadKeyModelName]; got != "gpt-5.4" {
t.Fatalf("tool_calls model_name = %#v, want %q", got, "gpt-5.4")
}
if _, ok := payload[PayloadKeyToolCalls].([]any); !ok {
t.Fatalf("tool_calls payload = %#v, want parsed array", payload[PayloadKeyToolCalls])
}
case <-time.After(time.Second):
t.Fatal("expected tool_calls message to be delivered")
}
}
func TestSendPlaceholder_EmitsNormalMessageWithoutKind(t *testing.T) {
ch := newTestPicoChannel(t)
ch.bc.Placeholder.Enabled = true
@@ -257,6 +319,9 @@ func TestBeginStream_CreatesAndUpdatesSameMessage(t *testing.T) {
if err != nil {
t.Fatalf("BeginStream() error = %v", err)
}
if setter, ok := streamer.(interface{ SetModelName(modelName string) }); ok {
setter.SetModelName("gpt-5.4")
}
if err := streamer.Update(context.Background(), "hello"); err != nil {
t.Fatalf("Update(first) error = %v", err)
}
@@ -271,6 +336,9 @@ func TestBeginStream_CreatesAndUpdatesSameMessage(t *testing.T) {
if got := first.Payload[PayloadKeyContent]; got != "hello" {
t.Fatalf("first content = %#v, want hello", got)
}
if got := first.Payload[PayloadKeyModelName]; got != "gpt-5.4" {
t.Fatalf("first model_name = %#v, want %q", got, "gpt-5.4")
}
rawStreamer := streamer.(*picoStreamer)
rawStreamer.mu.Lock()
@@ -290,6 +358,9 @@ func TestBeginStream_CreatesAndUpdatesSameMessage(t *testing.T) {
if got := second.Payload[PayloadKeyContent]; got != secondContent {
t.Fatalf("second content = %#v, want %q", got, secondContent)
}
if got := second.Payload[PayloadKeyModelName]; got != "gpt-5.4" {
t.Fatalf("second model_name = %#v, want %q", got, "gpt-5.4")
}
}
func TestBeginStream_DefaultStreamingShowsSmallIncrements(t *testing.T) {
@@ -355,6 +426,9 @@ func TestBeginStream_StreamsReasoningAsThoughtUpdates(t *testing.T) {
if !ok {
t.Fatal("pico stream should support reasoning updates")
}
if setter, ok := streamer.(interface{ SetModelName(modelName string) }); ok {
setter.SetModelName("gpt-5.4-mini")
}
if err := reasoningStreamer.UpdateReasoning(context.Background(), "thinking"); err != nil {
t.Fatalf("UpdateReasoning(first) error = %v", err)
}
@@ -372,6 +446,9 @@ func TestBeginStream_StreamsReasoningAsThoughtUpdates(t *testing.T) {
if got := first.Payload[PayloadKeyContent]; got != "thinking" {
t.Fatalf("first content = %#v, want thinking", got)
}
if got := first.Payload[PayloadKeyModelName]; got != "gpt-5.4-mini" {
t.Fatalf("first model_name = %#v, want %q", got, "gpt-5.4-mini")
}
if err := reasoningStreamer.UpdateReasoning(context.Background(), "thinking more"); err != nil {
t.Fatalf("UpdateReasoning(second) error = %v", err)
@@ -389,6 +466,9 @@ func TestBeginStream_StreamsReasoningAsThoughtUpdates(t *testing.T) {
if got := second.Payload[PayloadKeyContent]; got != "thinking more" {
t.Fatalf("second content = %#v, want thinking more", got)
}
if got := second.Payload[PayloadKeyModelName]; got != "gpt-5.4-mini" {
t.Fatalf("second model_name = %#v, want %q", got, "gpt-5.4-mini")
}
}
func TestBeginStream_ThrottlesIntermediateUpdatesAndFinalFlushes(t *testing.T) {
@@ -473,6 +553,9 @@ func TestBeginStream_FinalizeIncludesContextUsage(t *testing.T) {
if err != nil {
t.Fatalf("BeginStream() error = %v", err)
}
if setter, ok := streamer.(interface{ SetModelName(modelName string) }); ok {
setter.SetModelName("gpt-5.4")
}
if err := streamer.Update(context.Background(), "partial"); err != nil {
t.Fatalf("Update() error = %v", err)
}
@@ -501,6 +584,9 @@ func TestBeginStream_FinalizeIncludesContextUsage(t *testing.T) {
if got := final.Payload["message_id"]; got != msgID {
t.Fatalf("final message_id = %#v, want %q", got, msgID)
}
if got := final.Payload[PayloadKeyModelName]; got != "gpt-5.4" {
t.Fatalf("final model_name = %#v, want %q", got, "gpt-5.4")
}
rawUsage, ok := final.Payload["context_usage"].(map[string]any)
if !ok {
t.Fatalf("final context_usage = %#v, want map", final.Payload["context_usage"])
+1
View File
@@ -27,6 +27,7 @@ const (
PayloadKeyKind = "kind"
PayloadKeyPlaceholder = "placeholder"
PayloadKeyToolCalls = "tool_calls"
PayloadKeyModelName = "model_name"
MessageKindThought = "thought"
MessageKindToolCalls = "tool_calls"