Release / update-version (push) Has been cancelled
Release / build-frontend (push) Has been cancelled
Release / release (push) Has been cancelled
Release / sync-version-file (push) Has been cancelled
CI / shell (push) Canceled after 0s
CI / test (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
Security Scan / backend-security (push) Canceled after 0s
Security Scan / frontend-security (push) Canceled after 0s
188 lines
7.3 KiB
Go
188 lines
7.3 KiB
Go
package apicompat
|
|
|
|
import "testing"
|
|
|
|
// TestAnthropicEventToResponses_TextEmitsContentPart pins that a message text
|
|
// stream emits response.content_part.added, and that it precedes the first
|
|
// output_text.delta for that part.
|
|
//
|
|
// Why: the OpenAI SDK's accumulating stream helper (client.responses.stream)
|
|
// only appends a content part to the message item when it sees
|
|
// content_part.added. The item is added with content: [], so a missing event
|
|
// makes the following output_text.delta index output.content[content_index] and
|
|
// raise IndexError. Raw event iteration does not accumulate, so a regression
|
|
// here is easy to miss.
|
|
func TestAnthropicEventToResponses_TextEmitsContentPart(t *testing.T) {
|
|
state := NewAnthropicEventToResponsesState()
|
|
state.Model = "claude-sonnet-4-5"
|
|
|
|
var types []string
|
|
feed := func(evt *AnthropicStreamEvent) {
|
|
for _, out := range AnthropicEventToResponsesEvents(evt, state) {
|
|
types = append(types, out.Type)
|
|
}
|
|
}
|
|
|
|
idx := 0
|
|
feed(&AnthropicStreamEvent{Type: "message_start", Message: &AnthropicResponse{ID: "msg_1", Model: "claude-sonnet-4-5"}})
|
|
feed(&AnthropicStreamEvent{Type: "content_block_start", Index: &idx, ContentBlock: &AnthropicContentBlock{Type: "text"}})
|
|
feed(&AnthropicStreamEvent{Type: "content_block_delta", Index: &idx, Delta: &AnthropicDelta{Type: "text_delta", Text: "Hel"}})
|
|
feed(&AnthropicStreamEvent{Type: "content_block_delta", Index: &idx, Delta: &AnthropicDelta{Type: "text_delta", Text: "lo"}})
|
|
feed(&AnthropicStreamEvent{Type: "content_block_stop", Index: &idx})
|
|
feed(&AnthropicStreamEvent{Type: "message_stop"})
|
|
|
|
posOf := func(target string) int {
|
|
for i, ty := range types {
|
|
if ty == target {
|
|
return i
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
partAdded := posOf("response.content_part.added")
|
|
firstDelta := posOf("response.output_text.delta")
|
|
|
|
if partAdded < 0 {
|
|
t.Fatalf("response.content_part.added was not emitted; got %v", types)
|
|
}
|
|
if firstDelta < 0 {
|
|
t.Fatalf("response.output_text.delta was not emitted; got %v", types)
|
|
}
|
|
if partAdded > firstDelta {
|
|
t.Errorf("content_part.added must precede the first output_text.delta; got %v", types)
|
|
}
|
|
if posOf("response.content_part.done") < 0 {
|
|
t.Errorf("response.content_part.done was not emitted; got %v", types)
|
|
}
|
|
}
|
|
|
|
// TestAnthropicEventToResponses_DoneEventsCarryFullText pins that done events
|
|
// carry the part's full text (deltas carry increments only).
|
|
func TestAnthropicEventToResponses_DoneEventsCarryFullText(t *testing.T) {
|
|
state := NewAnthropicEventToResponsesState()
|
|
state.Model = "claude-sonnet-4-5"
|
|
|
|
var events []ResponsesStreamEvent
|
|
feed := func(evt *AnthropicStreamEvent) {
|
|
events = append(events, AnthropicEventToResponsesEvents(evt, state)...)
|
|
}
|
|
|
|
idx := 0
|
|
feed(&AnthropicStreamEvent{Type: "message_start", Message: &AnthropicResponse{ID: "msg_1"}})
|
|
feed(&AnthropicStreamEvent{Type: "content_block_start", Index: &idx, ContentBlock: &AnthropicContentBlock{Type: "text"}})
|
|
feed(&AnthropicStreamEvent{Type: "content_block_delta", Index: &idx, Delta: &AnthropicDelta{Type: "text_delta", Text: "Hello "}})
|
|
feed(&AnthropicStreamEvent{Type: "content_block_delta", Index: &idx, Delta: &AnthropicDelta{Type: "text_delta", Text: "world"}})
|
|
feed(&AnthropicStreamEvent{Type: "content_block_stop", Index: &idx})
|
|
|
|
const want = "Hello world"
|
|
var sawTextDone, sawPartDone bool
|
|
for _, e := range events {
|
|
switch e.Type {
|
|
case "response.output_text.done":
|
|
sawTextDone = true
|
|
if e.Text != want {
|
|
t.Errorf("output_text.done text = %q, want %q", e.Text, want)
|
|
}
|
|
case "response.content_part.done":
|
|
sawPartDone = true
|
|
if e.Part == nil || e.Part.Text != want {
|
|
t.Errorf("content_part.done part = %+v, want text %q", e.Part, want)
|
|
}
|
|
}
|
|
}
|
|
if !sawTextDone || !sawPartDone {
|
|
t.Errorf("missing done events: output_text.done=%v content_part.done=%v", sawTextDone, sawPartDone)
|
|
}
|
|
}
|
|
|
|
// TestAnthropicEventToResponses_CompletedCarriesOutput pins that
|
|
// response.completed carries the full output list. The SDK's
|
|
// get_final_response() and tracing integrations parse the terminal event's
|
|
// response directly; an empty output leaves them with nothing (the text still
|
|
// renders from deltas, which is why this is invisible when only watching the
|
|
// stream).
|
|
func TestAnthropicEventToResponses_CompletedCarriesOutput(t *testing.T) {
|
|
state := NewAnthropicEventToResponsesState()
|
|
state.Model = "claude-sonnet-4-5"
|
|
|
|
var events []ResponsesStreamEvent
|
|
feed := func(evt *AnthropicStreamEvent) {
|
|
events = append(events, AnthropicEventToResponsesEvents(evt, state)...)
|
|
}
|
|
|
|
idx := 0
|
|
feed(&AnthropicStreamEvent{Type: "message_start", Message: &AnthropicResponse{ID: "msg_1"}})
|
|
feed(&AnthropicStreamEvent{Type: "content_block_start", Index: &idx, ContentBlock: &AnthropicContentBlock{Type: "text"}})
|
|
feed(&AnthropicStreamEvent{Type: "content_block_delta", Index: &idx, Delta: &AnthropicDelta{Type: "text_delta", Text: "4826"}})
|
|
feed(&AnthropicStreamEvent{Type: "content_block_stop", Index: &idx})
|
|
feed(&AnthropicStreamEvent{Type: "message_stop"})
|
|
|
|
var completed *ResponsesStreamEvent
|
|
for i := range events {
|
|
if events[i].Type == "response.completed" {
|
|
completed = &events[i]
|
|
}
|
|
}
|
|
if completed == nil || completed.Response == nil {
|
|
t.Fatalf("response.completed was not emitted")
|
|
}
|
|
if len(completed.Response.Output) == 0 {
|
|
t.Fatalf("response.completed carries an empty output; clients would see no result")
|
|
}
|
|
msg := completed.Response.Output[0]
|
|
if msg.Type != "message" || len(msg.Content) == 0 {
|
|
t.Fatalf("output[0] = %+v, want a message with content", msg)
|
|
}
|
|
if msg.Content[0].Text != "4826" {
|
|
t.Errorf("output[0].content[0].text = %q, want %q", msg.Content[0].Text, "4826")
|
|
}
|
|
}
|
|
|
|
// TestAnthropicEventToResponses_ToolCallCompletedCarriesArguments pins that a
|
|
// function call's accumulated arguments survive into output_item.done and
|
|
// response.completed.
|
|
func TestAnthropicEventToResponses_ToolCallCompletedCarriesArguments(t *testing.T) {
|
|
state := NewAnthropicEventToResponsesState()
|
|
state.Model = "claude-sonnet-4-5"
|
|
|
|
var events []ResponsesStreamEvent
|
|
feed := func(evt *AnthropicStreamEvent) {
|
|
events = append(events, AnthropicEventToResponsesEvents(evt, state)...)
|
|
}
|
|
|
|
idx := 0
|
|
feed(&AnthropicStreamEvent{Type: "message_start", Message: &AnthropicResponse{ID: "msg_1"}})
|
|
feed(&AnthropicStreamEvent{Type: "content_block_start", Index: &idx, ContentBlock: &AnthropicContentBlock{
|
|
Type: "tool_use", ID: "toolu_1", Name: "get_weather",
|
|
}})
|
|
feed(&AnthropicStreamEvent{Type: "content_block_delta", Index: &idx, Delta: &AnthropicDelta{
|
|
Type: "input_json_delta", PartialJSON: `{"city":`,
|
|
}})
|
|
feed(&AnthropicStreamEvent{Type: "content_block_delta", Index: &idx, Delta: &AnthropicDelta{
|
|
Type: "input_json_delta", PartialJSON: `"SH"}`,
|
|
}})
|
|
feed(&AnthropicStreamEvent{Type: "content_block_stop", Index: &idx})
|
|
feed(&AnthropicStreamEvent{Type: "message_stop"})
|
|
|
|
var completed *ResponsesStreamEvent
|
|
for i := range events {
|
|
if events[i].Type == "response.completed" {
|
|
completed = &events[i]
|
|
}
|
|
}
|
|
if completed == nil || completed.Response == nil || len(completed.Response.Output) == 0 {
|
|
t.Fatalf("response.completed carries no output")
|
|
}
|
|
fc := completed.Response.Output[0]
|
|
if fc.Type != "function_call" {
|
|
t.Fatalf("output[0].type = %q, want function_call", fc.Type)
|
|
}
|
|
if fc.Arguments != `{"city":"SH"}` {
|
|
t.Errorf("arguments = %q, want %q", fc.Arguments, `{"city":"SH"}`)
|
|
}
|
|
if fc.Name != "get_weather" {
|
|
t.Errorf("name = %q, want get_weather", fc.Name)
|
|
}
|
|
}
|