diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts index 1b1da702ff..9b8c988aa7 100644 --- a/packages/types/src/provider-settings.ts +++ b/packages/types/src/provider-settings.ts @@ -34,7 +34,15 @@ export const DEFAULT_CONSECUTIVE_MISTAKE_LIMIT = 3 * Dynamic provider requires external API calls in order to get the model list. */ -export const dynamicProviders = ["openrouter", "vercel-ai-gateway", "litellm", "requesty", "unbound", "poe"] as const +export const dynamicProviders = [ + "openrouter", + "vercel-ai-gateway", + "litellm", + "requesty", + "unbound", + "futurmix", + "poe", +] as const export type DynamicProvider = (typeof dynamicProviders)[number] @@ -336,6 +344,12 @@ const unboundSchema = baseProviderSettingsSchema.extend({ unboundModelId: z.string().optional(), }) +const futurmixSchema = baseProviderSettingsSchema.extend({ + futurmixApiKey: z.string().optional(), + futurmixBaseUrl: z.string().optional(), + futurmixModelId: z.string().optional(), +}) + const fakeAiSchema = baseProviderSettingsSchema.extend({ fakeAi: z.unknown().optional(), }) @@ -405,6 +419,7 @@ export const providerSettingsSchemaDiscriminated = z.discriminatedUnion("apiProv minimaxSchema.merge(z.object({ apiProvider: z.literal("minimax") })), requestySchema.merge(z.object({ apiProvider: z.literal("requesty") })), unboundSchema.merge(z.object({ apiProvider: z.literal("unbound") })), + futurmixSchema.merge(z.object({ apiProvider: z.literal("futurmix") })), fakeAiSchema.merge(z.object({ apiProvider: z.literal("fake-ai") })), xaiSchema.merge(z.object({ apiProvider: z.literal("xai") })), basetenSchema.merge(z.object({ apiProvider: z.literal("baseten") })), @@ -438,6 +453,7 @@ export const providerSettingsSchema = z.object({ ...minimaxSchema.shape, ...requestySchema.shape, ...unboundSchema.shape, + ...futurmixSchema.shape, ...fakeAiSchema.shape, ...xaiSchema.shape, ...basetenSchema.shape, @@ -475,6 +491,7 @@ export const modelIdKeys = [ "lmStudioDraftModelId", "requestyModelId", "unboundModelId", + "futurmixModelId", "litellmModelId", "vercelAiGatewayModelId", ] as const satisfies readonly (keyof ProviderSettings)[] @@ -514,6 +531,7 @@ export const modelIdKeysByProvider: Record = { "qwen-code": "apiModelId", requesty: "requestyModelId", unbound: "unboundModelId", + futurmix: "futurmixModelId", xai: "apiModelId", baseten: "apiModelId", litellm: "litellmModelId", @@ -631,6 +649,7 @@ export const MODELS_BY_PROVIDER: Record< openrouter: { id: "openrouter", label: "OpenRouter", models: [] }, requesty: { id: "requesty", label: "Requesty", models: [] }, unbound: { id: "unbound", label: "Unbound", models: [] }, + futurmix: { id: "futurmix", label: "FuturMix", models: [] }, "vercel-ai-gateway": { id: "vercel-ai-gateway", label: "Vercel AI Gateway", models: [] }, // Local providers; models discovered from localhost endpoints. diff --git a/packages/types/src/providers/futurmix.ts b/packages/types/src/providers/futurmix.ts new file mode 100644 index 0000000000..1ef152bde5 --- /dev/null +++ b/packages/types/src/providers/futurmix.ts @@ -0,0 +1,16 @@ +import type { ModelInfo } from "../model.js" + +// FuturMix +// https://futurmix.ai +export const futurmixDefaultModelId = "claude-sonnet-4-20250514" + +export const futurmixDefaultModelInfo: ModelInfo = { + maxTokens: 8192, + contextWindow: 200_000, + supportsImages: true, + supportsPromptCache: true, + inputPrice: 3.0, + outputPrice: 15.0, + cacheWritesPrice: 3.75, + cacheReadsPrice: 0.3, +} diff --git a/packages/types/src/providers/index.ts b/packages/types/src/providers/index.ts index 243ecd8d4e..5e5aa82eca 100644 --- a/packages/types/src/providers/index.ts +++ b/packages/types/src/providers/index.ts @@ -18,6 +18,7 @@ export * from "./qwen-code.js" export * from "./requesty.js" export * from "./sambanova.js" export * from "./unbound.js" +export * from "./futurmix.js" export * from "./vertex.js" export * from "./vscode-llm.js" export * from "./xai.js" @@ -41,6 +42,7 @@ import { qwenCodeDefaultModelId } from "./qwen-code.js" import { requestyDefaultModelId } from "./requesty.js" import { sambaNovaDefaultModelId } from "./sambanova.js" import { unboundDefaultModelId } from "./unbound.js" +import { futurmixDefaultModelId } from "./futurmix.js" import { vertexDefaultModelId } from "./vertex.js" import { vscodeLlmDefaultModelId } from "./vscode-llm.js" import { xaiDefaultModelId } from "./xai.js" @@ -109,6 +111,8 @@ export function getProviderDefaultModelId( return poeDefaultModelId case "unbound": return unboundDefaultModelId + case "futurmix": + return futurmixDefaultModelId case "vercel-ai-gateway": return vercelAiGatewayDefaultModelId case "anthropic": diff --git a/src/api/index.ts b/src/api/index.ts index 40ba31f39a..dd18d2cb45 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -23,6 +23,7 @@ import { VsCodeLmHandler, RequestyHandler, UnboundHandler, + FuturMixHandler, FakeAIHandler, XAIHandler, LiteLLMHandler, @@ -157,6 +158,8 @@ export function buildApiHandler(configuration: ProviderSettings): ApiHandler { return new RequestyHandler(options) case "unbound": return new UnboundHandler(options) + case "futurmix": + return new FuturMixHandler(options) case "fake-ai": return new FakeAIHandler(options) case "xai": diff --git a/src/api/providers/fetchers/futurmix.ts b/src/api/providers/fetchers/futurmix.ts new file mode 100644 index 0000000000..84eafad29c --- /dev/null +++ b/src/api/providers/fetchers/futurmix.ts @@ -0,0 +1,40 @@ +import axios from "axios" + +import type { ModelInfo } from "@roo-code/types" + +import { parseApiPrice } from "../../../shared/cost" + +export async function getFuturMixModels(apiKey?: string | null): Promise> { + const models: Record = {} + + try { + const headers: Record = {} + + if (apiKey) { + headers["Authorization"] = `Bearer ${apiKey}` + } + + const response = await axios.get("https://futurmix.ai/v1/models", { headers }) + const rawModels = response.data?.data ?? response.data + + for (const rawModel of rawModels) { + const modelInfo: ModelInfo = { + maxTokens: rawModel.max_output_tokens ?? 8192, + contextWindow: rawModel.context_window ?? 200_000, + supportsPromptCache: rawModel.supports_caching ?? false, + supportsImages: rawModel.supports_vision ?? false, + inputPrice: parseApiPrice(rawModel.input_price), + outputPrice: parseApiPrice(rawModel.output_price), + description: rawModel.description, + cacheWritesPrice: parseApiPrice(rawModel.caching_price), + cacheReadsPrice: parseApiPrice(rawModel.cached_price), + } + + models[rawModel.id] = modelInfo + } + } catch (error) { + console.error(`Error fetching FuturMix models: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`) + } + + return models +} diff --git a/src/api/providers/fetchers/modelCache.ts b/src/api/providers/fetchers/modelCache.ts index 2f0b23c6be..ddfac4e580 100644 --- a/src/api/providers/fetchers/modelCache.ts +++ b/src/api/providers/fetchers/modelCache.ts @@ -19,6 +19,7 @@ import { getOpenRouterModels } from "./openrouter" import { getVercelAiGatewayModels } from "./vercel-ai-gateway" import { getRequestyModels } from "./requesty" import { getUnboundModels } from "./unbound" +import { getFuturMixModels } from "./futurmix" import { getLiteLLMModels } from "./litellm" import { GetModelsOptions } from "../../../shared/api" import { getOllamaModels } from "./ollama" @@ -71,6 +72,9 @@ async function fetchModelsFromProvider(options: GetModelsOptions): Promise { + const { id: model, maxTokens: max_tokens, temperature } = await this.fetchModel() + + let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [{ role: "system", content: prompt }] + + const completionParams: OpenAI.Chat.ChatCompletionCreateParams = { + model, + max_tokens, + messages: openAiMessages, + temperature: temperature, + } + + let response: OpenAI.Chat.ChatCompletion + try { + response = await this.client.chat.completions.create(completionParams) + } catch (error) { + throw handleOpenAIError(error, this.providerName) + } + return response.choices[0]?.message.content || "" + } +} diff --git a/src/api/providers/index.ts b/src/api/providers/index.ts index b690a81234..56f6ca17c7 100644 --- a/src/api/providers/index.ts +++ b/src/api/providers/index.ts @@ -19,6 +19,7 @@ export { QwenCodeHandler } from "./qwen-code" export { RequestyHandler } from "./requesty" export { SambaNovaHandler } from "./sambanova" export { UnboundHandler } from "./unbound" +export { FuturMixHandler } from "./futurmix" export { VertexHandler } from "./vertex" export { VsCodeLmHandler } from "./vscode-lm" export { XAIHandler } from "./xai" diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index fac7ed10d5..945a780735 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -890,6 +890,7 @@ export const webviewMessageHandler = async (provider: ClineProvider, message: We litellm: {}, requesty: {}, unbound: {}, + futurmix: {}, ollama: {}, lmstudio: {}, poe: {}, @@ -926,6 +927,13 @@ export const webviewMessageHandler = async (provider: ClineProvider, message: We apiKey: apiConfiguration.unboundApiKey, }, }, + { + key: "futurmix", + options: { + provider: "futurmix", + apiKey: apiConfiguration.futurmixApiKey, + }, + }, { key: "vercel-ai-gateway", options: { provider: "vercel-ai-gateway" } }, ] diff --git a/src/shared/ProfileValidator.ts b/src/shared/ProfileValidator.ts index 7246a90177..5d0a7183bf 100644 --- a/src/shared/ProfileValidator.ts +++ b/src/shared/ProfileValidator.ts @@ -79,6 +79,8 @@ export class ProfileValidator { return profile.requestyModelId case "unbound": return profile.unboundModelId + case "futurmix": + return profile.futurmixModelId case "fake-ai": default: return undefined diff --git a/src/shared/api.ts b/src/shared/api.ts index 66ed4f25ad..51946d76c4 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -174,6 +174,7 @@ const dynamicProviderExtras = { litellm: {} as { apiKey: string; baseUrl: string }, requesty: {} as { apiKey?: string; baseUrl?: string }, unbound: {} as { apiKey?: string }, + futurmix: {} as { apiKey?: string }, ollama: {} as {}, // eslint-disable-line @typescript-eslint/no-empty-object-type lmstudio: {} as {}, // eslint-disable-line @typescript-eslint/no-empty-object-type poe: {} as { apiKey?: string; baseUrl?: string }, diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index d23bc6fa85..5fbe66d80b 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -32,6 +32,7 @@ import { vercelAiGatewayDefaultModelId, minimaxDefaultModelId, unboundDefaultModelId, + futurmixDefaultModelId, } from "@roo-code/types" import { @@ -85,6 +86,7 @@ import { Requesty, SambaNova, Unbound, + FuturMix, Vertex, VSCodeLM, XAI, @@ -334,6 +336,7 @@ const ApiOptions = ({ openrouter: { field: "openRouterModelId", default: openRouterDefaultModelId }, requesty: { field: "requestyModelId", default: requestyDefaultModelId }, unbound: { field: "unboundModelId", default: unboundDefaultModelId }, + futurmix: { field: "futurmixModelId", default: futurmixDefaultModelId }, litellm: { field: "litellmModelId", default: litellmDefaultModelId }, anthropic: { field: "apiModelId", default: anthropicDefaultModelId }, "openai-codex": { field: "apiModelId", default: openAiCodexDefaultModelId }, @@ -522,6 +525,18 @@ const ApiOptions = ({ /> )} + {selectedProvider === "futurmix" && ( + + )} + {selectedProvider === "anthropic" && ( a.label.localeCompare(b.label)) diff --git a/webview-ui/src/components/settings/providers/FuturMix.tsx b/webview-ui/src/components/settings/providers/FuturMix.tsx new file mode 100644 index 0000000000..af014f170b --- /dev/null +++ b/webview-ui/src/components/settings/providers/FuturMix.tsx @@ -0,0 +1,108 @@ +import { useCallback } from "react" +import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react" + +import { + type ProviderSettings, + type OrganizationAllowList, + type RouterModels, + futurmixDefaultModelId, +} from "@roo-code/types" + +import { vscode } from "@src/utils/vscode" +import { useAppTranslation } from "@src/i18n/TranslationContext" +import { Button } from "@src/components/ui" + +import { inputEventTransform } from "../transforms" +import { ModelPicker } from "../ModelPicker" + +type FuturMixProps = { + apiConfiguration: ProviderSettings + setApiConfigurationField: (field: keyof ProviderSettings, value: ProviderSettings[keyof ProviderSettings]) => void + routerModels?: RouterModels + refetchRouterModels: () => void + organizationAllowList: OrganizationAllowList + modelValidationError?: string + simplifySettings?: boolean +} + +export const FuturMix = ({ + apiConfiguration, + setApiConfigurationField, + routerModels, + organizationAllowList, + modelValidationError, + simplifySettings, +}: FuturMixProps) => { + const { t } = useAppTranslation() + + const handleInputChange = useCallback( + ( + field: K, + transform: (event: E) => ProviderSettings[K] = inputEventTransform, + ) => + (event: E | Event) => { + setApiConfigurationField(field, transform(event as E)) + }, + [setApiConfigurationField], + ) + + return ( + <> + +
+ +
+
+
+ {t("settings:providers.apiKeyStorageNotice")} +
+ + {t("settings:providers.getFuturMixApiKey")} + + + + + + + + ) +} diff --git a/webview-ui/src/components/settings/providers/index.ts b/webview-ui/src/components/settings/providers/index.ts index 7badb54311..c9fb159641 100644 --- a/webview-ui/src/components/settings/providers/index.ts +++ b/webview-ui/src/components/settings/providers/index.ts @@ -15,6 +15,7 @@ export { QwenCode } from "./QwenCode" export { Requesty } from "./Requesty" export { SambaNova } from "./SambaNova" export { Unbound } from "./Unbound" +export { FuturMix } from "./FuturMix" export { Vertex } from "./Vertex" export { VSCodeLM } from "./VSCodeLM" export { XAI } from "./XAI" diff --git a/webview-ui/src/components/ui/hooks/useSelectedModel.ts b/webview-ui/src/components/ui/hooks/useSelectedModel.ts index bf78236b82..68499d44cd 100644 --- a/webview-ui/src/components/ui/hooks/useSelectedModel.ts +++ b/webview-ui/src/components/ui/hooks/useSelectedModel.ts @@ -164,6 +164,11 @@ function getSelectedModel({ const routerInfo = routerModels.unbound?.[id] return { id, info: routerInfo } } + case "futurmix": { + const id = getValidatedModelId(apiConfiguration.futurmixModelId, routerModels.futurmix, defaultModelId) + const routerInfo = routerModels.futurmix?.[id] + return { id, info: routerInfo } + } case "litellm": { const id = getValidatedModelId(apiConfiguration.litellmModelId, routerModels.litellm, defaultModelId) const routerInfo = routerModels.litellm?.[id] diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index 01b6cd3e91..8d06cb9d46 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -412,6 +412,7 @@ "noCustomHeaders": "No custom headers defined. Click the + button to add one.", "unboundApiKey": "Unbound API Key", "getUnboundApiKey": "Get Unbound API Key", + "getFuturMixApiKey": "Get FuturMix API Key", "requestyApiKey": "Requesty API Key", "refreshModels": { "label": "Refresh Models", diff --git a/webview-ui/src/utils/__tests__/validate.spec.ts b/webview-ui/src/utils/__tests__/validate.spec.ts index e0b13fd49f..d57da225dc 100644 --- a/webview-ui/src/utils/__tests__/validate.spec.ts +++ b/webview-ui/src/utils/__tests__/validate.spec.ts @@ -40,6 +40,7 @@ describe("Model Validation Functions", () => { }, requesty: {}, unbound: {}, + futurmix: {}, litellm: {}, ollama: {}, lmstudio: {}, diff --git a/webview-ui/src/utils/validate.ts b/webview-ui/src/utils/validate.ts index f506171acc..3d76390309 100644 --- a/webview-ui/src/utils/validate.ts +++ b/webview-ui/src/utils/validate.ts @@ -53,6 +53,11 @@ function validateModelsAndKeysProvided(apiConfiguration: ProviderSettings): stri return i18next.t("settings:validation.apiKey") } break + case "futurmix": + if (!apiConfiguration.futurmixApiKey) { + return i18next.t("settings:validation.apiKey") + } + break case "litellm": if (!apiConfiguration.litellmApiKey) { return i18next.t("settings:validation.apiKey")