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
60 changes: 60 additions & 0 deletions packages/session/src/__tests__/openai-compatible.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
18 changes: 16 additions & 2 deletions packages/session/src/adapters/openai-compatible.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => ({
Expand Down Expand Up @@ -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<string, unknown> | 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 [{
Expand Down Expand Up @@ -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<string, unknown> | 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 }
}
}
},
Expand Down
12 changes: 11 additions & 1 deletion packages/session/src/create-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
}
Expand All @@ -254,6 +255,7 @@ function buildSession(

return {
content: result.content,
reasoningContent: result.reasoningContent,
toolCalls: result.toolCalls,
usage: result.usage,
}
Expand Down Expand Up @@ -314,11 +316,13 @@ function buildSession(
let result: SendResult
if (options.llm.stream) {
let accumulated = ''
let accumulatedReasoning = ''
const toolCallsByIndex = new Map<number, { id?: string; name?: string; input: string }>()
// 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: '' }
Expand All @@ -333,7 +337,11 @@ function buildSession(
name: call.name ?? 'unknown_tool',
input: call.input ? JSON.parse(call.input) as Record<string, unknown> : {},
}))
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) {
Expand All @@ -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(),
}
Expand All @@ -359,6 +368,7 @@ function buildSession(

return {
content: result.content,
reasoningContent: result.reasoningContent,
toolCalls: result.toolCalls,
usage: result.usage,
}
Expand Down
2 changes: 2 additions & 0 deletions packages/session/src/types/functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ export interface LoadSessionOptions {
export interface SendResult {
/** LLM 文本响应 */
content: string | null
/** 推理模型的思考内容,多轮对话时需回传给 API */
reasoningContent?: string | null
/** LLM 返回的工具调用(由上层决定是否执行) */
toolCalls?: ToolCall[]
/** token 用量统计 */
Expand Down
6 changes: 6 additions & 0 deletions packages/session/src/types/llm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 时有效 */
Expand Down Expand Up @@ -35,6 +37,8 @@ export interface LLMCompleteOptions {
/** LLM 完成后的返回结果 */
export interface LLMResult {
content: string | null
/** 推理模型的思考内容,多轮对话时需回传给 API */
reasoningContent?: string | null
toolCalls?: ToolCall[]
usage?: {
promptTokens: number
Expand All @@ -46,6 +50,8 @@ export interface LLMResult {
export interface LLMChunk {
/** 文本增量片段 */
delta: string
/** 推理内容增量片段(stepFun/DeepSeek 等推理模型) */
reasoningDelta?: string
/** 工具调用增量片段(用于流式拼接 tool call) */
toolCallDeltas?: Array<{
index: number
Expand Down
Loading