Roo-Code/src/api/providers/fireworks.ts
Hannes Rudolph 1b75d59a68
fix(ai-sdk): preserve reasoning parts in message conversion (#11217)
* fix(ai-sdk): preserve reasoning parts in message conversion

* fix(ai-sdk): convert message-level reasoning_content to reasoning part

* fix(task): remove invalid openai-compatible from reasoning allowlist

* feat: add isAiSdkProvider() method for dynamic AI SDK provider detection

- Add isAiSdkProvider() method to ApiHandler interface
- Default implementation in BaseProvider returns false
- Override to return true in 11 AI SDK providers:
  deepseek, fireworks, mistral, groq, xai, cerebras,
  sambanova, huggingface, gemini, vertex, openai-compatible
- Update Task.ts to use dynamic detection instead of hardcoded Set
- Add method to FakeAIHandler and update test mocks

* fix: handle reasoning parts in flattenAiSdkMessagesToStringContent

- Strip reasoning parts when flattening messages for string-only models
- Allow flattening when message contains only text and reasoning parts
- Add tests for reasoning part handling in string-only model contexts

This addresses the review feedback about ensuring flattenAiSdkMessagesToStringContent
works correctly when reasoning parts are present (e.g., SambaNova DeepSeek).

---------

Co-authored-by: daniel-lxs <ricciodaniel98@gmail.com>
2026-02-05 10:48:21 -07:00

179 lines
5.3 KiB
TypeScript

import { Anthropic } from "@anthropic-ai/sdk"
import { createFireworks } from "@ai-sdk/fireworks"
import { streamText, generateText, ToolSet } from "ai"
import { fireworksModels, fireworksDefaultModelId, type ModelInfo } from "@roo-code/types"
import type { ApiHandlerOptions } from "../../shared/api"
import {
convertToAiSdkMessages,
convertToolsForAiSdk,
processAiSdkStreamPart,
mapToolChoice,
handleAiSdkError,
} from "../transform/ai-sdk"
import { ApiStream, ApiStreamUsageChunk } from "../transform/stream"
import { getModelParams } from "../transform/model-params"
import { DEFAULT_HEADERS } from "./constants"
import { BaseProvider } from "./base-provider"
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
const FIREWORKS_DEFAULT_TEMPERATURE = 0.5
/**
* Fireworks provider using the dedicated @ai-sdk/fireworks package.
* Provides native support for various models including reasoning models.
*/
export class FireworksHandler extends BaseProvider implements SingleCompletionHandler {
protected options: ApiHandlerOptions
protected provider: ReturnType<typeof createFireworks>
constructor(options: ApiHandlerOptions) {
super()
this.options = options
// Create the Fireworks provider using AI SDK
this.provider = createFireworks({
baseURL: "https://api.fireworks.ai/inference/v1",
apiKey: options.fireworksApiKey ?? "not-provided",
headers: DEFAULT_HEADERS,
})
}
override getModel(): { id: string; info: ModelInfo; maxTokens?: number; temperature?: number } {
const id = this.options.apiModelId ?? fireworksDefaultModelId
const info = fireworksModels[id as keyof typeof fireworksModels] || fireworksModels[fireworksDefaultModelId]
const params = getModelParams({
format: "openai",
modelId: id,
model: info,
settings: this.options,
defaultTemperature: FIREWORKS_DEFAULT_TEMPERATURE,
})
return { id, info, ...params }
}
/**
* Get the language model for the configured model ID.
*/
protected getLanguageModel() {
const { id } = this.getModel()
return this.provider(id)
}
/**
* Process usage metrics from the AI SDK response.
*/
protected processUsageMetrics(
usage: {
inputTokens?: number
outputTokens?: number
details?: {
cachedInputTokens?: number
reasoningTokens?: number
}
},
providerMetadata?: {
fireworks?: {
promptCacheHitTokens?: number
promptCacheMissTokens?: number
}
},
): ApiStreamUsageChunk {
// Extract cache metrics from Fireworks' providerMetadata if available
const cacheReadTokens = providerMetadata?.fireworks?.promptCacheHitTokens ?? usage.details?.cachedInputTokens
const cacheWriteTokens = providerMetadata?.fireworks?.promptCacheMissTokens
return {
type: "usage",
inputTokens: usage.inputTokens || 0,
outputTokens: usage.outputTokens || 0,
cacheReadTokens,
cacheWriteTokens,
reasoningTokens: usage.details?.reasoningTokens,
}
}
/**
* Get the max tokens parameter to include in the request.
*/
protected getMaxOutputTokens(): number | undefined {
const { info } = this.getModel()
return this.options.modelMaxTokens || info.maxTokens || undefined
}
/**
* Create a message stream using the AI SDK.
*/
override async *createMessage(
systemPrompt: string,
messages: Anthropic.Messages.MessageParam[],
metadata?: ApiHandlerCreateMessageMetadata,
): ApiStream {
const { temperature } = this.getModel()
const languageModel = this.getLanguageModel()
// Convert messages to AI SDK format
const aiSdkMessages = convertToAiSdkMessages(messages)
// Convert tools to OpenAI format first, then to AI SDK format
const openAiTools = this.convertToolsForOpenAI(metadata?.tools)
const aiSdkTools = convertToolsForAiSdk(openAiTools) as ToolSet | undefined
// Build the request options
const requestOptions: Parameters<typeof streamText>[0] = {
model: languageModel,
system: systemPrompt,
messages: aiSdkMessages,
temperature: this.options.modelTemperature ?? temperature ?? FIREWORKS_DEFAULT_TEMPERATURE,
maxOutputTokens: this.getMaxOutputTokens(),
tools: aiSdkTools,
toolChoice: mapToolChoice(metadata?.tool_choice),
}
// Use streamText for streaming responses
const result = streamText(requestOptions)
try {
// Process the full stream to get all events including reasoning
for await (const part of result.fullStream) {
for (const chunk of processAiSdkStreamPart(part)) {
yield chunk
}
}
// Yield usage metrics at the end, including cache metrics from providerMetadata
const usage = await result.usage
const providerMetadata = await result.providerMetadata
if (usage) {
yield this.processUsageMetrics(usage, providerMetadata as any)
}
} catch (error) {
// Handle AI SDK errors (AI_RetryError, AI_APICallError, etc.)
throw handleAiSdkError(error, "Fireworks")
}
}
/**
* Complete a prompt using the AI SDK generateText.
*/
async completePrompt(prompt: string): Promise<string> {
const { temperature } = this.getModel()
const languageModel = this.getLanguageModel()
const { text } = await generateText({
model: languageModel,
prompt,
maxOutputTokens: this.getMaxOutputTokens(),
temperature: this.options.modelTemperature ?? temperature ?? FIREWORKS_DEFAULT_TEMPERATURE,
})
return text
}
override isAiSdkProvider(): boolean {
return true
}
}