( systemPrompt: string, messages: Anthropic.Messages.MessageParam[], metadata?: ApiHandlerCreateMessageMetadata, )
| 207 | } |
| 208 | |
| 209 | override async *createMessage( |
| 210 | systemPrompt: string, |
| 211 | messages: Anthropic.Messages.MessageParam[], |
| 212 | metadata?: ApiHandlerCreateMessageMetadata, |
| 213 | ): AsyncGenerator<ApiStreamChunk> { |
| 214 | const model = await this.fetchModel() |
| 215 | |
| 216 | let { id: modelId, maxTokens, temperature, topP, reasoning } = model |
| 217 | |
| 218 | // Reset reasoning_details accumulator for this request |
| 219 | this.currentReasoningDetails = [] |
| 220 | |
| 221 | // OpenRouter sends reasoning tokens by default for Gemini 2.5 Pro models |
| 222 | // even if you don't request them. This is not the default for |
| 223 | // other providers (including Gemini), so we need to explicitly disable |
| 224 | // them unless the user has explicitly configured reasoning. |
| 225 | // Note: Gemini 3 models use reasoning_details format with thought signatures, |
| 226 | // but we handle this via skip_thought_signature_validator injection below. |
| 227 | if ( |
| 228 | (modelId === "google/gemini-2.5-pro-preview" || modelId === "google/gemini-2.5-pro") && |
| 229 | typeof reasoning === "undefined" |
| 230 | ) { |
| 231 | reasoning = { exclude: true } |
| 232 | } |
| 233 | |
| 234 | // Convert Anthropic messages to OpenAI format. |
| 235 | // Pass normalization function for Mistral compatibility (requires 9-char alphanumeric IDs) |
| 236 | const isMistral = modelId.toLowerCase().includes("mistral") |
| 237 | let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [ |
| 238 | { role: "system", content: systemPrompt }, |
| 239 | ...convertToOpenAiMessages( |
| 240 | messages, |
| 241 | isMistral ? { normalizeToolCallId: normalizeMistralToolCallId } : undefined, |
| 242 | ), |
| 243 | ] |
| 244 | |
| 245 | // DeepSeek highly recommends using user instead of system role. |
| 246 | if (modelId.startsWith("deepseek/deepseek-r1") || modelId === "perplexity/sonar-reasoning") { |
| 247 | openAiMessages = convertToR1Format([{ role: "user", content: systemPrompt }, ...messages]) |
| 248 | } |
| 249 | |
| 250 | // Process reasoning_details when switching models to Gemini. |
| 251 | const isGemini = modelId.startsWith("google/gemini") |
| 252 | |
| 253 | // For Gemini models with native protocol: |
| 254 | // 1. Sanitize messages to handle thought signature validation issues. |
| 255 | // This must happen BEFORE fake encrypted block injection to avoid injecting for |
| 256 | // tool calls that will be dropped due to missing/mismatched reasoning_details. |
| 257 | // 2. Inject fake reasoning.encrypted block for tool calls without existing encrypted reasoning. |
| 258 | // This is required when switching from other models to Gemini to satisfy API validation. |
| 259 | // Per OpenRouter documentation (conversation with Toven, Nov 2025): |
| 260 | // - Create ONE reasoning_details entry per assistant message with tool calls |
| 261 | // - Set `id` to the FIRST tool call's ID from the tool_calls array |
| 262 | // - Set `data` to "skip_thought_signature_validator" to bypass signature validation |
| 263 | // - Set `index` to 0 |
| 264 | // See: https://github.com/cline/cline/issues/8214 |
| 265 | if (isGemini) { |
| 266 | // Step 1: Sanitize messages - filter out tool calls with missing/mismatched reasoning_details |
nothing calls this directly
no test coverage detected