mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-09-03 06:35:00 +08:00
test(claude): expand OpenAI conversion tests for reasoning_content and stream/non-stream parity
Closes: #5148
This commit is contained in:
@@ -210,22 +210,173 @@ func TestConvertClaudeResponseToOpenAINonStream_RefusalStopReason(t *testing.T)
|
||||
func TestConvertClaudeResponseToOpenAINonStream_ReasoningContent(t *testing.T) {
|
||||
rawJSON := []byte("data: {\"type\":\"message_start\",\"message\":{\"id\":\"msg_123\",\"model\":\"claude-opus-4-6\"}}\n" +
|
||||
"data: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"thinking\",\"thinking\":\"\"}}\n" +
|
||||
"data: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"Let me think... \"}}\n" +
|
||||
"data: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"Done thinking.\"}}\n" +
|
||||
"data: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"Let me analyze the problem.\"}}\n" +
|
||||
"data: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" Step 2 is clear.\"}}\n" +
|
||||
"data: {\"type\":\"content_block_stop\",\"index\":0}\n" +
|
||||
"data: {\"type\":\"content_block_start\",\"index\":1,\"content_block\":{\"type\":\"text\",\"text\":\"\"}}\n" +
|
||||
"data: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"text_delta\",\"text\":\"Hello world\"}}\n" +
|
||||
"data: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"text_delta\",\"text\":\"Here is the solution.\"}}\n" +
|
||||
"data: {\"type\":\"content_block_stop\",\"index\":1}\n" +
|
||||
"data: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"end_turn\"},\"usage\":{\"input_tokens\":10,\"output_tokens\":20}}\n")
|
||||
|
||||
out := ConvertClaudeResponseToOpenAINonStream(context.Background(), "", nil, nil, rawJSON, nil)
|
||||
|
||||
gotReasoningContent := gjson.GetBytes(out, "choices.0.message.reasoning_content").String()
|
||||
wantReasoningContent := "Let me think... Done thinking."
|
||||
if gotReasoningContent != wantReasoningContent {
|
||||
t.Fatalf("expected reasoning_content %q, got %q, payload=%s", wantReasoningContent, gotReasoningContent, string(out))
|
||||
gotRC := gjson.GetBytes(out, "choices.0.message.reasoning_content")
|
||||
if !gotRC.Exists() {
|
||||
t.Fatalf("expected choices.0.message.reasoning_content to exist, payload=%s", string(out))
|
||||
}
|
||||
if gotLegacyReasoning := gjson.GetBytes(out, "choices.0.message.reasoning"); gotLegacyReasoning.Exists() {
|
||||
t.Fatalf("expected reasoning to not exist, got %q, payload=%s", gotLegacyReasoning.String(), string(out))
|
||||
wantRC := "Let me analyze the problem. Step 2 is clear."
|
||||
if gotRC.String() != wantRC {
|
||||
t.Fatalf("reasoning_content = %q, want %q", gotRC.String(), wantRC)
|
||||
}
|
||||
|
||||
if gotOldReasoning := gjson.GetBytes(out, "choices.0.message.reasoning"); gotOldReasoning.Exists() {
|
||||
t.Fatalf("choices.0.message.reasoning should not exist, got %q", gotOldReasoning.String())
|
||||
}
|
||||
|
||||
gotContent := gjson.GetBytes(out, "choices.0.message.content").String()
|
||||
wantContent := "Here is the solution."
|
||||
if gotContent != wantContent {
|
||||
t.Fatalf("content = %q, want %q", gotContent, wantContent)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertClaudeResponseToOpenAINonStream_OmitsReasoningContentWhenAbsent(t *testing.T) {
|
||||
rawJSON := []byte("data: {\"type\":\"message_start\",\"message\":{\"id\":\"msg_123\",\"model\":\"claude-opus-4-6\"}}\n" +
|
||||
"data: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"}}\n" +
|
||||
"data: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"Just plain text.\"}}\n" +
|
||||
"data: {\"type\":\"content_block_stop\",\"index\":0}\n" +
|
||||
"data: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"end_turn\"},\"usage\":{\"input_tokens\":10,\"output_tokens\":20}}\n")
|
||||
|
||||
out := ConvertClaudeResponseToOpenAINonStream(context.Background(), "", nil, nil, rawJSON, nil)
|
||||
|
||||
if gotRC := gjson.GetBytes(out, "choices.0.message.reasoning_content"); gotRC.Exists() {
|
||||
t.Fatalf("choices.0.message.reasoning_content should be omitted when absent, got %q", gotRC.String())
|
||||
}
|
||||
if gotReasoning := gjson.GetBytes(out, "choices.0.message.reasoning"); gotReasoning.Exists() {
|
||||
t.Fatalf("choices.0.message.reasoning should not exist, got %q", gotReasoning.String())
|
||||
}
|
||||
if gotContent := gjson.GetBytes(out, "choices.0.message.content").String(); gotContent != "Just plain text." {
|
||||
t.Fatalf("content = %q, want %q", gotContent, "Just plain text.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertClaudeResponseToOpenAI_StreamAndNonStreamParity(t *testing.T) {
|
||||
events := [][]byte{
|
||||
[]byte(`data: {"type":"message_start","message":{"id":"msg_123","model":"claude-opus-4-6","usage":{"input_tokens":15,"output_tokens":1}}}`),
|
||||
[]byte(`data: {"type":"content_block_start","index":0,"content_block":{"type":"thinking","thinking":""}}`),
|
||||
[]byte(`data: {"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":"First thought. "}}`),
|
||||
[]byte(`data: {"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":"Second thought."}}`),
|
||||
[]byte(`data: {"type":"content_block_stop","index":0}`),
|
||||
[]byte(`data: {"type":"content_block_start","index":1,"content_block":{"type":"text","text":""}}`),
|
||||
[]byte(`data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":"Final "}}`),
|
||||
[]byte(`data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":"answer."}}`),
|
||||
[]byte(`data: {"type":"content_block_stop","index":1}`),
|
||||
[]byte(`data: {"type":"message_delta","delta":{"stop_reason":"end_turn"},"usage":{"output_tokens":25}}`),
|
||||
[]byte(`data: {"type":"message_stop"}`),
|
||||
}
|
||||
|
||||
// 1. Process via streaming
|
||||
ctx := context.Background()
|
||||
var param any
|
||||
var streamReasoning string
|
||||
var streamContent string
|
||||
var streamFinishReason string
|
||||
|
||||
for _, ev := range events {
|
||||
chunks := ConvertClaudeResponseToOpenAI(ctx, "claude-opus-4-6", nil, nil, ev, ¶m)
|
||||
for _, chunk := range chunks {
|
||||
if rc := gjson.GetBytes(chunk, "choices.0.delta.reasoning_content"); rc.Exists() {
|
||||
streamReasoning += rc.String()
|
||||
}
|
||||
if c := gjson.GetBytes(chunk, "choices.0.delta.content"); c.Exists() {
|
||||
streamContent += c.String()
|
||||
}
|
||||
if fr := gjson.GetBytes(chunk, "choices.0.finish_reason"); fr.Exists() && fr.String() != "" {
|
||||
streamFinishReason = fr.String()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Process via non-stream
|
||||
var rawBuffer []byte
|
||||
for _, ev := range events {
|
||||
rawBuffer = append(rawBuffer, ev...)
|
||||
rawBuffer = append(rawBuffer, '\n')
|
||||
}
|
||||
|
||||
nonStreamOut := ConvertClaudeResponseToOpenAINonStream(ctx, "", nil, nil, rawBuffer, nil)
|
||||
nonStreamRC := gjson.GetBytes(nonStreamOut, "choices.0.message.reasoning_content").String()
|
||||
nonStreamContent := gjson.GetBytes(nonStreamOut, "choices.0.message.content").String()
|
||||
nonStreamFinishReason := gjson.GetBytes(nonStreamOut, "choices.0.finish_reason").String()
|
||||
|
||||
if streamReasoning != "First thought. Second thought." {
|
||||
t.Fatalf("streamReasoning = %q, want %q", streamReasoning, "First thought. Second thought.")
|
||||
}
|
||||
if nonStreamRC != streamReasoning {
|
||||
t.Fatalf("parity mismatch for reasoning_content: nonStream=%q, stream=%q", nonStreamRC, streamReasoning)
|
||||
}
|
||||
if streamContent != "Final answer." {
|
||||
t.Fatalf("streamContent = %q, want %q", streamContent, "Final answer.")
|
||||
}
|
||||
if nonStreamContent != streamContent {
|
||||
t.Fatalf("parity mismatch for content: nonStream=%q, stream=%q", nonStreamContent, streamContent)
|
||||
}
|
||||
if streamFinishReason != "stop" {
|
||||
t.Fatalf("streamFinishReason = %q, want %q", streamFinishReason, "stop")
|
||||
}
|
||||
if nonStreamFinishReason != streamFinishReason {
|
||||
t.Fatalf("parity mismatch for finish_reason: nonStream=%q, stream=%q", nonStreamFinishReason, streamFinishReason)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertClaudeResponseToOpenAI_RedactedThinkingIgnored(t *testing.T) {
|
||||
events := [][]byte{
|
||||
[]byte(`data: {"type":"message_start","message":{"id":"msg_123","model":"claude-opus-4-6"}}`),
|
||||
[]byte(`data: {"type":"content_block_start","index":0,"content_block":{"type":"redacted_thinking","data":"encrypted_blob"}}`),
|
||||
[]byte(`data: {"type":"content_block_stop","index":0}`),
|
||||
[]byte(`data: {"type":"content_block_start","index":1,"content_block":{"type":"text","text":""}}`),
|
||||
[]byte(`data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":"Visible reply."}}`),
|
||||
[]byte(`data: {"type":"content_block_stop","index":1}`),
|
||||
[]byte(`data: {"type":"message_delta","delta":{"stop_reason":"end_turn"},"usage":{"input_tokens":10,"output_tokens":20}}`),
|
||||
}
|
||||
|
||||
// Non-stream check
|
||||
var rawJSON []byte
|
||||
for _, ev := range events {
|
||||
rawJSON = append(rawJSON, ev...)
|
||||
rawJSON = append(rawJSON, '\n')
|
||||
}
|
||||
|
||||
outNonStream := ConvertClaudeResponseToOpenAINonStream(context.Background(), "", nil, nil, rawJSON, nil)
|
||||
if gotRC := gjson.GetBytes(outNonStream, "choices.0.message.reasoning_content"); gotRC.Exists() {
|
||||
t.Fatalf("redacted_thinking must never map to reasoning_content in non-stream, got %q", gotRC.String())
|
||||
}
|
||||
if gotReasoning := gjson.GetBytes(outNonStream, "choices.0.message.reasoning"); gotReasoning.Exists() {
|
||||
t.Fatalf("redacted_thinking must not produce reasoning field in non-stream, got %q", gotReasoning.String())
|
||||
}
|
||||
if gotContent := gjson.GetBytes(outNonStream, "choices.0.message.content").String(); gotContent != "Visible reply." {
|
||||
t.Fatalf("content = %q, want %q", gotContent, "Visible reply.")
|
||||
}
|
||||
|
||||
// Stream check
|
||||
ctx := context.Background()
|
||||
var param any
|
||||
var streamContent string
|
||||
for _, line := range events {
|
||||
chunks := ConvertClaudeResponseToOpenAI(ctx, "claude-opus-4-6", nil, nil, line, ¶m)
|
||||
for _, chunk := range chunks {
|
||||
if gotRC := gjson.GetBytes(chunk, "choices.0.delta.reasoning_content"); gotRC.Exists() {
|
||||
t.Fatalf("redacted_thinking must never map to reasoning_content in stream, got %q", gotRC.String())
|
||||
}
|
||||
if gotReasoning := gjson.GetBytes(chunk, "choices.0.delta.reasoning"); gotReasoning.Exists() {
|
||||
t.Fatalf("redacted_thinking must not produce delta.reasoning field in stream, got %q", gotReasoning.String())
|
||||
}
|
||||
if c := gjson.GetBytes(chunk, "choices.0.delta.content"); c.Exists() {
|
||||
streamContent += c.String()
|
||||
}
|
||||
}
|
||||
}
|
||||
if streamContent != "Visible reply." {
|
||||
t.Fatalf("stream content = %q, want %q", streamContent, "Visible reply.")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user