mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-07 15:08:02 +08:00
fix(apicompat): accept chat reasoning alias
This commit is contained in:
@@ -423,19 +423,20 @@ func ChatCompletionsResponseToAnthropic(resp *ChatCompletionsResponse, model str
|
||||
// + the reasoning→thinking mapping in ResponsesToAnthropic.
|
||||
func chatMessageToAnthropicBlocks(message ChatMessage) []AnthropicContentBlock {
|
||||
var blocks []AnthropicContentBlock
|
||||
reasoning := message.reasoningText()
|
||||
|
||||
if message.ReasoningContent != "" {
|
||||
if reasoning != "" {
|
||||
blocks = append(blocks, AnthropicContentBlock{
|
||||
Type: "thinking",
|
||||
Thinking: message.ReasoningContent,
|
||||
Thinking: reasoning,
|
||||
})
|
||||
}
|
||||
|
||||
text := chatMessageContentText(message.Content)
|
||||
// DeepSeek reasoning-only fallback: when there is no text and no tool calls,
|
||||
// surface the reasoning content as visible text so the turn isn't empty.
|
||||
if text == "" && strings.TrimSpace(message.ReasoningContent) != "" && len(message.ToolCalls) == 0 {
|
||||
text = message.ReasoningContent
|
||||
if text == "" && strings.TrimSpace(reasoning) != "" && len(message.ToolCalls) == 0 {
|
||||
text = reasoning
|
||||
}
|
||||
if text != "" || len(message.ToolCalls) == 0 {
|
||||
blocks = append(blocks, AnthropicContentBlock{Type: "text", Text: text})
|
||||
@@ -608,11 +609,12 @@ func ChatCompletionsChunkToAnthropicEvents(
|
||||
|
||||
for _, choice := range chunk.Choices {
|
||||
// Reasoning content → thinking block.
|
||||
if choice.Delta.ReasoningContent != nil && *choice.Delta.ReasoningContent != "" {
|
||||
reasoning := choice.Delta.reasoningText()
|
||||
if reasoning != nil && *reasoning != "" {
|
||||
events = append(events, ensureCCAnthropicThinkingBlock(state)...)
|
||||
events = append(events, ccAnthropicDelta(state, &AnthropicDelta{
|
||||
Type: "thinking_delta",
|
||||
Thinking: *choice.Delta.ReasoningContent,
|
||||
Thinking: *reasoning,
|
||||
})...)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
package apicompat
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func readIssue5302Fixture(t *testing.T, name string, dst any) {
|
||||
t.Helper()
|
||||
payload, err := os.ReadFile(filepath.Join("testdata", "issue5302", name))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, json.Unmarshal(payload, dst))
|
||||
}
|
||||
|
||||
func TestChatReasoningAlias_AnthropicNonStreaming(t *testing.T) {
|
||||
var response ChatCompletionsResponse
|
||||
readIssue5302Fixture(t, "nonstream_reasoning.json", &response)
|
||||
|
||||
out := ChatCompletionsResponseToAnthropic(&response, "claude-sonnet-4-20250514")
|
||||
require.Len(t, out.Content, 2)
|
||||
require.Equal(t, "thinking", out.Content[0].Type)
|
||||
require.Equal(t, "fallback reasoning", out.Content[0].Thinking)
|
||||
require.Equal(t, "final answer", out.Content[1].Text)
|
||||
}
|
||||
|
||||
func TestChatReasoningAlias_AnthropicStreaming(t *testing.T) {
|
||||
var chunk ChatCompletionsChunk
|
||||
readIssue5302Fixture(t, "stream_reasoning.json", &chunk)
|
||||
|
||||
events := ChatCompletionsChunkToAnthropicEvents(&chunk, NewChatCompletionsToAnthropicStreamState("reasoning-model"))
|
||||
var thinking string
|
||||
for _, event := range events {
|
||||
if event.Delta != nil && event.Delta.Type == "thinking_delta" {
|
||||
thinking += event.Delta.Thinking
|
||||
}
|
||||
}
|
||||
require.Equal(t, "streamed fallback", thinking)
|
||||
}
|
||||
|
||||
func TestChatReasoningAlias_ResponsesSharedPaths(t *testing.T) {
|
||||
var response ChatCompletionsResponse
|
||||
readIssue5302Fixture(t, "nonstream_reasoning.json", &response)
|
||||
nonStream := ChatCompletionsResponseToResponses(&response, "reasoning-model", nil, false, nil)
|
||||
require.Len(t, nonStream.Output, 2)
|
||||
require.Equal(t, "reasoning", nonStream.Output[0].Type)
|
||||
require.Equal(t, "fallback reasoning", nonStream.Output[0].Summary[0].Text)
|
||||
|
||||
var chunk ChatCompletionsChunk
|
||||
readIssue5302Fixture(t, "stream_reasoning.json", &chunk)
|
||||
events := ChatCompletionsChunkToResponsesEvents(&chunk, NewChatCompletionsToResponsesStreamState("reasoning-model"))
|
||||
var deltas []string
|
||||
for _, event := range events {
|
||||
if event.Type == "response.reasoning_summary_text.delta" {
|
||||
deltas = append(deltas, event.Delta)
|
||||
}
|
||||
}
|
||||
require.Equal(t, []string{"streamed fallback"}, deltas)
|
||||
}
|
||||
|
||||
func TestChatReasoningAlias_ReasoningContentTakesPrecedence(t *testing.T) {
|
||||
var chunk ChatCompletionsChunk
|
||||
readIssue5302Fixture(t, "reasoning_content_precedence.json", &chunk)
|
||||
|
||||
events := ChatCompletionsChunkToResponsesEvents(&chunk, NewChatCompletionsToResponsesStreamState("reasoning-model"))
|
||||
var deltas []string
|
||||
for _, event := range events {
|
||||
if event.Type == "response.reasoning_summary_text.delta" {
|
||||
deltas = append(deltas, event.Delta)
|
||||
}
|
||||
}
|
||||
require.Equal(t, []string{"preferred reasoning"}, deltas)
|
||||
}
|
||||
@@ -1053,20 +1053,21 @@ func ChatCompletionsResponseToResponses(resp *ChatCompletionsResponse, model str
|
||||
|
||||
func chatMessageToResponsesOutput(message ChatMessage, customTools map[string]bool, toolSearch bool, namespaceTools map[string]NamespacedToolName) []ResponsesOutput {
|
||||
var outputs []ResponsesOutput
|
||||
if message.ReasoningContent != "" {
|
||||
reasoning := message.reasoningText()
|
||||
if reasoning != "" {
|
||||
outputs = append(outputs, ResponsesOutput{
|
||||
Type: "reasoning",
|
||||
ID: generateItemID(),
|
||||
Summary: []ResponsesSummary{{
|
||||
Type: "summary_text",
|
||||
Text: message.ReasoningContent,
|
||||
Text: reasoning,
|
||||
}},
|
||||
})
|
||||
}
|
||||
|
||||
text := chatMessageContentText(message.Content)
|
||||
if text == "" && strings.TrimSpace(message.ReasoningContent) != "" && len(message.ToolCalls) == 0 {
|
||||
text = message.ReasoningContent
|
||||
if text == "" && strings.TrimSpace(reasoning) != "" && len(message.ToolCalls) == 0 {
|
||||
text = reasoning
|
||||
}
|
||||
if text != "" || len(message.ToolCalls) == 0 {
|
||||
outputs = append(outputs, ResponsesOutput{
|
||||
@@ -1328,13 +1329,14 @@ func ChatCompletionsChunkToResponsesEvents(
|
||||
// (output_item.added + reasoning_summary_part.added) before the first
|
||||
// delta, otherwise a strict client discards the delta. The leading
|
||||
// empty-string reasoning delta upstreams send is filtered out.
|
||||
if choice.Delta.ReasoningContent != nil && *choice.Delta.ReasoningContent != "" {
|
||||
reasoning := choice.Delta.reasoningText()
|
||||
if reasoning != nil && *reasoning != "" {
|
||||
events = append(events, ensureChatReasoningItem(state)...)
|
||||
_, _ = state.Reasoning.WriteString(*choice.Delta.ReasoningContent)
|
||||
_, _ = state.Reasoning.WriteString(*reasoning)
|
||||
events = append(events, chatToResponsesEvent(state, "response.reasoning_summary_text.delta", &ResponsesStreamEvent{
|
||||
OutputIndex: state.ReasoningIndex,
|
||||
SummaryIndex: 0,
|
||||
Delta: *choice.Delta.ReasoningContent,
|
||||
Delta: *reasoning,
|
||||
ItemID: state.ReasoningItemID,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"id": "chatcmpl-sanitized",
|
||||
"object": "chat.completion",
|
||||
"model": "reasoning-model",
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "final answer",
|
||||
"reasoning": "fallback reasoning",
|
||||
"reasoning_details": [
|
||||
{"type": "reasoning.text", "text": "fallback reasoning"}
|
||||
]
|
||||
},
|
||||
"finish_reason": "stop"
|
||||
}
|
||||
]
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"id": "chatcmpl-sanitized",
|
||||
"object": "chat.completion.chunk",
|
||||
"model": "reasoning-model",
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"delta": {
|
||||
"reasoning_content": "preferred reasoning",
|
||||
"reasoning": "fallback reasoning"
|
||||
},
|
||||
"finish_reason": null
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"id": "chatcmpl-sanitized",
|
||||
"object": "chat.completion.chunk",
|
||||
"model": "reasoning-model",
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"delta": {
|
||||
"reasoning": "streamed fallback",
|
||||
"reasoning_details": [
|
||||
{"type": "reasoning.text", "text": "streamed fallback"}
|
||||
]
|
||||
},
|
||||
"finish_reason": null
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -651,6 +651,7 @@ type ChatMessage struct {
|
||||
Role string `json:"role"` // "system" | "user" | "assistant" | "tool" | "function"
|
||||
Content json.RawMessage `json:"content,omitempty"`
|
||||
ReasoningContent string `json:"reasoning_content,omitempty"`
|
||||
Reasoning string `json:"reasoning,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
ToolCalls []ChatToolCall `json:"tool_calls,omitempty"`
|
||||
ToolCallID string `json:"tool_call_id,omitempty"`
|
||||
@@ -771,9 +772,24 @@ type ChatDelta struct {
|
||||
Role string `json:"role,omitempty"`
|
||||
Content *string `json:"content,omitempty"` // pointer: omit when not present, null vs "" matters
|
||||
ReasoningContent *string `json:"reasoning_content,omitempty"`
|
||||
Reasoning *string `json:"reasoning,omitempty"`
|
||||
ToolCalls []ChatToolCall `json:"tool_calls,omitempty"`
|
||||
}
|
||||
|
||||
func (m ChatMessage) reasoningText() string {
|
||||
if m.ReasoningContent != "" {
|
||||
return m.ReasoningContent
|
||||
}
|
||||
return m.Reasoning
|
||||
}
|
||||
|
||||
func (d ChatDelta) reasoningText() *string {
|
||||
if d.ReasoningContent != nil {
|
||||
return d.ReasoningContent
|
||||
}
|
||||
return d.Reasoning
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Shared constants
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user