diff --git a/packages/session/src/__tests__/openai-compatible.test.ts b/packages/session/src/__tests__/openai-compatible.test.ts index dfb29c5..4355a38 100644 --- a/packages/session/src/__tests__/openai-compatible.test.ts +++ b/packages/session/src/__tests__/openai-compatible.test.ts @@ -106,6 +106,66 @@ describe('createOpenAICompatibleAdapter', () => { ) }) + it('complete() 提取响应中的 reasoning_content', async () => { + createCompletion.mockResolvedValueOnce({ + choices: [{ message: { content: 'answer', reasoning_content: 'thinking...' } }], + usage: { prompt_tokens: 10, completion_tokens: 5 }, + }) + + const adapter = createOpenAICompatibleAdapter({ + apiKey: 'test-key', + baseURL: 'https://api.example.com/v1', + model: 'test-model', + maxContextTokens: 128_000, + }) + + const result = await adapter.complete([{ role: 'user', content: 'hello' }]) + expect(result.reasoningContent).toBe('thinking...') + expect(result.content).toBe('answer') + }) + + it('assistant 消息的 reasoningContent 回传为 reasoning_content', async () => { + const adapter = createOpenAICompatibleAdapter({ + apiKey: 'test-key', + baseURL: 'https://api.example.com/v1', + model: 'test-model', + maxContextTokens: 128_000, + }) + + const messages: Message[] = [ + { role: 'user', content: 'hello' }, + { + role: 'assistant', + content: 'let me think', + reasoningContent: 'step 1: ...', + toolCalls: [{ id: 'tc_1', name: 'foo', input: {} }], + }, + { role: 'tool', content: 'result', toolCallId: 'tc_1' }, + ] + + await adapter.complete(messages) + + const sentMessages = createCompletion.mock.calls[0]![0].messages + expect(sentMessages[1]).toMatchObject({ + role: 'assistant', + content: 'let me think', + reasoning_content: 'step 1: ...', + tool_calls: [{ id: 'tc_1', type: 'function', function: { name: 'foo', arguments: '{}' } }], + }) + }) + + it('非推理模型响应不含 reasoningContent', async () => { + const adapter = createOpenAICompatibleAdapter({ + apiKey: 'test-key', + baseURL: 'https://api.example.com/v1', + model: 'test-model', + maxContextTokens: 128_000, + }) + + const result = await adapter.complete([{ role: 'user', content: 'hello' }]) + expect(result.reasoningContent).toBeUndefined() + }) + it('signal 透传到 SDK request options', async () => { const adapter = createOpenAICompatibleAdapter({ apiKey: 'test-key', diff --git a/packages/session/src/adapters/openai-compatible.ts b/packages/session/src/adapters/openai-compatible.ts index 42d4f27..22ad1ee 100644 --- a/packages/session/src/adapters/openai-compatible.ts +++ b/packages/session/src/adapters/openai-compatible.ts @@ -71,6 +71,9 @@ export function createOpenAICompatibleAdapter(options: OpenAICompatibleOptions): role: m.role as 'system' | 'user' | 'assistant' | 'tool', content: m.content, ...(m.role === 'tool' && m.toolCallId ? { tool_call_id: m.toolCallId } : {}), + ...(m.role === 'assistant' && m.reasoningContent + ? { reasoning_content: m.reasoningContent } + : {}), ...(m.role === 'assistant' && m.toolCalls && m.toolCalls.length > 0 ? { tool_calls: m.toolCalls.map((toolCall) => ({ @@ -100,9 +103,15 @@ export function createOpenAICompatibleAdapter(options: OpenAICompatibleOptions): ) as ChatCompletion const choice = response.choices[0] + // 提取推理模型的思考内容(stepFun/DeepSeek 等使用 reasoning_content 字段) + const rawMessage = choice?.message as Record | undefined + const reasoningContent = typeof rawMessage?.reasoning_content === 'string' + ? rawMessage.reasoning_content + : null return { content: choice?.message?.content ?? null, + ...(reasoningContent ? { reasoningContent } : {}), toolCalls: (choice?.message?.tool_calls ?? []).flatMap((call) => { if (!('function' in call) || !call.function) return [] return [{ @@ -131,14 +140,19 @@ export function createOpenAICompatibleAdapter(options: OpenAICompatibleOptions): for await (const chunk of stream) { const delta = chunk.choices[0]?.delta?.content ?? '' + // 提取推理模型的思考内容增量(stepFun/DeepSeek 等使用 reasoning_content 字段) + const rawDelta = chunk.choices[0]?.delta as Record | undefined + const reasoningDelta = typeof rawDelta?.reasoning_content === 'string' + ? rawDelta.reasoning_content + : undefined const toolCallDeltas = (chunk.choices[0]?.delta?.tool_calls ?? []).map((call: ChatToolCallDelta) => ({ index: call.index ?? 0, id: call.id, name: call.function?.name, input: call.function?.arguments, })) - if (delta || toolCallDeltas.length > 0) { - yield { delta, toolCallDeltas } + if (delta || reasoningDelta || toolCallDeltas.length > 0) { + yield { delta, ...(reasoningDelta ? { reasoningDelta } : {}), toolCallDeltas } } } }, diff --git a/packages/session/src/create-session.ts b/packages/session/src/create-session.ts index 66e8396..4f705d2 100644 --- a/packages/session/src/create-session.ts +++ b/packages/session/src/create-session.ts @@ -244,6 +244,7 @@ function buildSession( const assistantRecord: Message = { role: 'assistant', content: result.content ?? '', + ...(result.reasoningContent ? { reasoningContent: result.reasoningContent } : {}), ...(result.toolCalls && result.toolCalls.length > 0 ? { toolCalls: result.toolCalls } : {}), timestamp: new Date().toISOString(), } @@ -254,6 +255,7 @@ function buildSession( return { content: result.content, + reasoningContent: result.reasoningContent, toolCalls: result.toolCalls, usage: result.usage, } @@ -314,11 +316,13 @@ function buildSession( let result: SendResult if (options.llm.stream) { let accumulated = '' + let accumulatedReasoning = '' const toolCallsByIndex = new Map() // adapter 在 abort 时抛 AbortError,这里直接向上传播给 result promise; // 下方 L3 写入分支不会执行(policy: drop entirely),与非流式 send() 对称。 for await (const chunk of options.llm.stream(promptMessages, { tools, signal: sendOptions?.signal })) { accumulated += chunk.delta + if (chunk.reasoningDelta) accumulatedReasoning += chunk.reasoningDelta push(chunk.delta) for (const delta of chunk.toolCallDeltas ?? []) { const current = toolCallsByIndex.get(delta.index) ?? { input: '' } @@ -333,7 +337,11 @@ function buildSession( name: call.name ?? 'unknown_tool', input: call.input ? JSON.parse(call.input) as Record : {}, })) - result = { content: accumulated, toolCalls } + result = { + content: accumulated, + ...(accumulatedReasoning ? { reasoningContent: accumulatedReasoning } : {}), + toolCalls, + } } else { result = await options.llm.complete(promptMessages, { tools, signal: sendOptions?.signal }) if (result.content) { @@ -344,6 +352,7 @@ function buildSession( const assistantRecord: Message = { role: 'assistant', content: result.content ?? '', + ...(result.reasoningContent ? { reasoningContent: result.reasoningContent } : {}), ...(result.toolCalls && result.toolCalls.length > 0 ? { toolCalls: result.toolCalls } : {}), timestamp: new Date().toISOString(), } @@ -359,6 +368,7 @@ function buildSession( return { content: result.content, + reasoningContent: result.reasoningContent, toolCalls: result.toolCalls, usage: result.usage, } diff --git a/packages/session/src/types/functions.ts b/packages/session/src/types/functions.ts index 3d538c2..f53d94f 100644 --- a/packages/session/src/types/functions.ts +++ b/packages/session/src/types/functions.ts @@ -49,6 +49,8 @@ export interface LoadSessionOptions { export interface SendResult { /** LLM 文本响应 */ content: string | null + /** 推理模型的思考内容,多轮对话时需回传给 API */ + reasoningContent?: string | null /** LLM 返回的工具调用(由上层决定是否执行) */ toolCalls?: ToolCall[] /** token 用量统计 */ diff --git a/packages/session/src/types/llm.ts b/packages/session/src/types/llm.ts index a2d9f87..0449182 100644 --- a/packages/session/src/types/llm.ts +++ b/packages/session/src/types/llm.ts @@ -2,6 +2,8 @@ export interface Message { role: 'system' | 'user' | 'assistant' | 'tool' content: string + /** 推理模型的思考内容(stepFun/DeepSeek 等),仅 role=assistant 时有效 */ + reasoningContent?: string /** assistant 发起的工具调用列表,仅 role=assistant 时有效 */ toolCalls?: ToolCall[] /** 关联的工具调用 ID,仅 role=tool 时有效 */ @@ -35,6 +37,8 @@ export interface LLMCompleteOptions { /** LLM 完成后的返回结果 */ export interface LLMResult { content: string | null + /** 推理模型的思考内容,多轮对话时需回传给 API */ + reasoningContent?: string | null toolCalls?: ToolCall[] usage?: { promptTokens: number @@ -46,6 +50,8 @@ export interface LLMResult { export interface LLMChunk { /** 文本增量片段 */ delta: string + /** 推理内容增量片段(stepFun/DeepSeek 等推理模型) */ + reasoningDelta?: string /** 工具调用增量片段(用于流式拼接 tool call) */ toolCallDeltas?: Array<{ index: number