Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions contributors.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
206 changes: 206 additions & 0 deletions src/services/api/openai/__tests__/convertMessages.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,3 +249,209 @@ describe('anthropicMessagesToOpenAI', () => {
)
})
})

describe('DeepSeek thinking mode (enableThinking)', () => {
test('preserves thinking block as reasoning_content when enabled', () => {
const result = anthropicMessagesToOpenAI(
[makeUserMsg('question'), makeAssistantMsg([
{ type: 'thinking' as const, thinking: 'Let me reason about this...' },
{ type: 'text', text: 'The answer is 42.' },
])],
[] as any,
{ enableThinking: true },
)
// Should have: user, assistant with reasoning_content
expect(result).toHaveLength(2)
expect(result[0].role).toBe('user')
const assistant = result[1] as any
expect(assistant.role).toBe('assistant')
expect(assistant.content).toBe('The answer is 42.')
expect(assistant.reasoning_content).toBe('Let me reason about this...')
})

test('drops thinking block when enableThinking is false (default)', () => {
const result = anthropicMessagesToOpenAI(
[makeAssistantMsg([
{ type: 'thinking' as const, thinking: 'internal thoughts...' },
{ type: 'text', text: 'visible response' },
])],
[] as any,
)
const assistant = result[0] as any
expect(assistant.content).toBe('visible response')
expect(assistant.reasoning_content).toBeUndefined()
})

test('preserves reasoning_content with tool_calls in same turn', () => {
const result = anthropicMessagesToOpenAI(
[
makeUserMsg('what is the weather?'),
makeAssistantMsg([
{ type: 'thinking' as const, thinking: 'I need to call the weather tool.' },
{ type: 'text', text: '' },
{
type: 'tool_use' as const,
id: 'toolu_001',
name: 'get_weather',
input: { location: 'Hangzhou' },
},
]),
makeUserMsg([
{
type: 'tool_result' as const,
tool_use_id: 'toolu_001',
content: 'Cloudy 7~13°C',
},
]),
],
[] as any,
{ enableThinking: true },
)

// Find the assistant message
const assistants = result.filter(m => m.role === 'assistant')
expect(assistants.length).toBe(1)
const assistant = assistants[0] as any
expect(assistant.reasoning_content).toBe('I need to call the weather tool.')
expect(assistant.tool_calls).toBeDefined()
expect(assistant.tool_calls[0].function.name).toBe('get_weather')
})

test('strips reasoning_content from previous turns', () => {
const result = anthropicMessagesToOpenAI(
[
// Turn 1: user → assistant (with thinking)
makeUserMsg('question 1'),
makeAssistantMsg([
{ type: 'thinking' as const, thinking: 'Turn 1 reasoning...' },
{ type: 'text', text: 'Turn 1 answer' },
]),
// Turn 2: new user message → previous reasoning should be stripped
makeUserMsg('question 2'),
makeAssistantMsg([
{ type: 'thinking' as const, thinking: 'Turn 2 reasoning...' },
{ type: 'text', text: 'Turn 2 answer' },
]),
],
[] as any,
{ enableThinking: true },
)

const assistants = result.filter(m => m.role === 'assistant')
// Turn 1 assistant: reasoning should be stripped (previous turn)
expect((assistants[0] as any).reasoning_content).toBeUndefined()
expect((assistants[0] as any).content).toBe('Turn 1 answer')
// Turn 2 assistant: reasoning should be preserved (current turn)
expect((assistants[1] as any).reasoning_content).toBe('Turn 2 reasoning...')
expect((assistants[1] as any).content).toBe('Turn 2 answer')
})

test('preserves reasoning_content in multi-iteration tool call within same turn', () => {
// Simulates a full DeepSeek tool call iteration:
// user → assistant(thinking+tool_call) → tool_result → assistant(thinking+tool_call) → tool_result → assistant(thinking+text)
const result = anthropicMessagesToOpenAI(
[
makeUserMsg("tomorrow's weather in Hangzhou"),
// Iteration 1: thinking + tool call
makeAssistantMsg([
{ type: 'thinking' as const, thinking: 'I need the date first.' },
{
type: 'tool_use' as const,
id: 'toolu_001',
name: 'get_date',
input: {},
},
]),
makeUserMsg([
{
type: 'tool_result' as const,
tool_use_id: 'toolu_001',
content: '2026-04-08',
},
]),
// Iteration 2: thinking + tool call
makeAssistantMsg([
{ type: 'thinking' as const, thinking: 'Now I can get the weather.' },
{
type: 'tool_use' as const,
id: 'toolu_002',
name: 'get_weather',
input: { location: 'Hangzhou', date: '2026-04-08' },
},
]),
makeUserMsg([
{
type: 'tool_result' as const,
tool_use_id: 'toolu_002',
content: 'Cloudy 7~13°C',
},
]),
// Iteration 3: thinking + final answer
makeAssistantMsg([
{ type: 'thinking' as const, thinking: 'I have the info now.' },
{ type: 'text', text: 'Tomorrow will be cloudy, 7-13°C.' },
]),
],
[] as any,
{ enableThinking: true },
)

// All 3 assistant messages are in the current turn (after last user msg is the last tool_result,
// but the "last user message" boundary logic finds the last user-typed message).
// Actually, tool_result messages are also UserMessage type, so the last user message
// is the one with tool_result for toolu_002. All assistant messages after that should have reasoning.
const assistants = result.filter(m => m.role === 'assistant')
expect(assistants.length).toBe(3)
// All iterations within the same turn preserve reasoning
expect((assistants[0] as any).reasoning_content).toBe('I need the date first.')
expect((assistants[1] as any).reasoning_content).toBe('Now I can get the weather.')
expect((assistants[2] as any).reasoning_content).toBe('I have the info now.')
})

test('handles multiple thinking blocks in single assistant message', () => {
const result = anthropicMessagesToOpenAI(
[makeUserMsg('question'), makeAssistantMsg([
{ type: 'thinking' as const, thinking: 'First thought.' },
{ type: 'thinking' as const, thinking: 'Second thought.' },
{ type: 'text', text: 'Final answer.' },
])],
[] as any,
{ enableThinking: true },
)
const assistant = result.filter(m => m.role === 'assistant')[0] as any
expect(assistant.reasoning_content).toBe('First thought.\nSecond thought.')
})

test('skips empty thinking blocks', () => {
const result = anthropicMessagesToOpenAI(
[makeUserMsg('question'), makeAssistantMsg([
{ type: 'thinking' as const, thinking: '' },
{ type: 'text', text: 'Answer.' },
])],
[] as any,
{ enableThinking: true },
)
const assistant = result.filter(m => m.role === 'assistant')[0] as any
expect(assistant.reasoning_content).toBeUndefined()
})

test('sets content to null when only thinking and tool_calls present', () => {
const result = anthropicMessagesToOpenAI(
[makeUserMsg('question'), makeAssistantMsg([
{ type: 'thinking' as const, thinking: 'Reasoning only.' },
{
type: 'tool_use' as const,
id: 'toolu_001',
name: 'bash',
input: { command: 'ls' },
},
])],
[] as any,
{ enableThinking: true },
)
const assistant = result.filter(m => m.role === 'assistant')[0] as any
expect(assistant.content).toBeNull()
expect(assistant.reasoning_content).toBe('Reasoning only.')
expect(assistant.tool_calls).toHaveLength(1)
})
})
Loading