Skip to content
Open
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
19 changes: 16 additions & 3 deletions src/renderer/src/aiCore/chunk/AiSdkToChunkAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@ export class AiSdkToChunkAdapter {
text: '',
reasoningContent: '',
webSearchResults: [],
reasoningId: ''
reasoningId: '',
reasoningDetails: [] as any[]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reasoningdetails有类型我没记错的话

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reasoningdetails有类型我没记错的话

确实是有的(https://openrouter.ai/docs/guides/best-practices/reasoning-tokens#reasoning_details-array-structure),不过在通用字段外,3种子类型有各自特殊字段,未来其他模型供应商也可能会带来新的类型。我觉得如果不会对客户端造成什么安全性风险,用Any或许没什么问题?因为使用reasoning_details时,我们已经相信OpenRouter会对数据的合法性、格式对接负责了。

}
this.resetTimingState()
this.responseStartTimestamp = Date.now()
Expand Down Expand Up @@ -141,7 +142,13 @@ export class AiSdkToChunkAdapter {
*/
private convertAndEmitChunk(
chunk: TextStreamPart<any>,
final: { text: string; reasoningContent: string; webSearchResults: AISDKWebSearchResult[]; reasoningId: string }
final: {
text: string
reasoningContent: string
webSearchResults: AISDKWebSearchResult[]
reasoningId: string
reasoningDetails: any[]
}
) {
logger.silly(`AI SDK chunk type: ${chunk.type}`, chunk)
switch (chunk.type) {
Expand Down Expand Up @@ -281,6 +288,11 @@ export class AiSdkToChunkAdapter {

case 'finish-step': {
const { providerMetadata, finishReason } = chunk
// Capture reasoning_details from OpenRouter providerMetadata
const openrouterMeta = providerMetadata?.openrouter as Record<string, any> | undefined
if (openrouterMeta?.reasoning_details && Array.isArray(openrouterMeta.reasoning_details)) {
final.reasoningDetails = openrouterMeta.reasoning_details
}
// googel web search
if (providerMetadata?.google?.groundingMetadata) {
this.onChunk({
Expand Down Expand Up @@ -351,7 +363,8 @@ export class AiSdkToChunkAdapter {
const metrics = this.buildMetrics(chunk.totalUsage)
const baseResponse = {
text: final.text || '',
reasoning_content: final.reasoningContent || ''
reasoning_content: final.reasoningContent || '',
reasoning_details: final.reasoningDetails.length > 0 ? final.reasoningDetails : undefined
}

this.onChunk({
Expand Down
22 changes: 18 additions & 4 deletions src/renderer/src/aiCore/prepareParams/messageConverter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* 将 Cherry Studio 消息格式转换为 AI SDK 消息格式
*/

import type { ReasoningPart } from '@ai-sdk/provider-utils'
import type { ProviderOptions, ReasoningPart } from '@ai-sdk/provider-utils'
import { loggerService } from '@logger'
import { isImageEnhancementModel, isVisionModel } from '@renderer/config/models'
import type { Message, Model } from '@renderer/types'
Expand Down Expand Up @@ -45,7 +45,7 @@ export async function convertMessageToSdkParam(
if (message.role === 'user' || message.role === 'system') {
return convertMessageToUserModelMessage(content, fileBlocks, imageBlocks, isVisionModel, model)
} else {
return convertMessageToAssistantModelMessage(content, fileBlocks, imageBlocks, reasoningBlocks, model)
return convertMessageToAssistantModelMessage(content, fileBlocks, imageBlocks, reasoningBlocks, model, message)
}
}

Expand Down Expand Up @@ -163,10 +163,14 @@ async function convertMessageToAssistantModelMessage(
fileBlocks: FileMessageBlock[],
imageBlocks: ImageMessageBlock[],
thinkingBlocks: ThinkingMessageBlock[],
model?: Model
model?: Model,
message?: Message
): Promise<AssistantModelMessage> {
const parts: Array<TextPart | ReasoningPart | FilePart> = []

// Extract reasoning_details directly from the message (stored at the same level as content)
const reasoningDetails = message?.reasoning_details

// Add reasoning blocks first (required by AWS Bedrock for Claude extended thinking)
for (const thinkingBlock of thinkingBlocks) {
parts.push({ type: 'reasoning', text: thinkingBlock.content })
Expand Down Expand Up @@ -201,9 +205,19 @@ async function convertMessageToAssistantModelMessage(
parts.push({ type: 'text', text: '[Image]' })
}

// If reasoning_details exists from OpenRouter, pass it via providerOptions.
// The OpenRouter SDK reads from providerOptions.openrouter.reasoning_details
// and flattens it to reasoning_details at the same level as content in the API request,
// enabling models like Claude/Gemini to resume encrypted reasoning.
// Note: JSON round-trip strips undefined values that would fail AI SDK's Zod validation.
const providerOptions: ProviderOptions | undefined = reasoningDetails?.length
? { openrouter: { reasoning_details: JSON.parse(JSON.stringify(reasoningDetails)) } }
: undefined

return {
role: 'assistant',
content: parts
content: parts,
...(providerOptions ? { providerOptions } : {})
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,14 @@ export const createBaseCallbacks = (deps: BaseCallbacksDependencies) => {
}
}

const messageUpdates = { status, metrics: response?.metrics, usage: response?.usage }
const messageUpdates = {
status,
metrics: response?.metrics,
usage: response?.usage,
// Store reasoning_details directly on the message for subsequent requests.
// When present, this allows models like Claude/Gemini to resume encrypted reasoning.
...(response?.reasoning_details ? { reasoning_details: response.reasoning_details } : {})
}
dispatch(
newMessagesActions.updateMessage({
topicId,
Expand Down
15 changes: 15 additions & 0 deletions src/renderer/src/types/newMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,14 @@ export type Message = {
usage?: Usage
metrics?: Metrics

/**
* Raw reasoning details from OpenRouter.
* When present, this should be saved and sent back in subsequent requests
* to allow models like Claude/Gemini to correctly resume encrypted reasoning.
* Has higher priority than other reasoning fields.
*/
reasoning_details?: any[]

Comment on lines +208 to +215
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

provider specific的数据不应该额外添加字段,应该放到providerMetadata字段中

// UI相关
multiModelMessageStyle?: 'horizontal' | 'vertical' | 'fold' | 'grid'
foldSelected?: boolean
Expand All @@ -226,6 +234,13 @@ export type Message = {
export interface Response {
text?: string
reasoning_content?: string
/**
* Raw reasoning details from OpenRouter.
* When present, this should be saved and sent back in subsequent requests
* to allow models like Claude/Gemini to correctly resume encrypted reasoning.
* Has higher priority than other reasoning fields.
*/
reasoning_details?: any[]
Comment on lines +237 to +243
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里同样,不要额外添加字段。我们目前所有provider specific的数据都依赖上游providerMetadata处理,你可以看看openrouter aisdk provider的类型定义是否已更新

usage?: Usage
metrics?: Metrics
webSearch?: WebSearchResponse
Expand Down