diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts index 43135577e16..01a4024afa1 100644 --- a/packages/types/src/provider-settings.ts +++ b/packages/types/src/provider-settings.ts @@ -41,6 +41,7 @@ export const dynamicProviders = [ "requesty", "roo", "unbound", + "futurmix", "poe", ] as const @@ -344,6 +345,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(), }) @@ -418,6 +425,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") })), @@ -452,6 +460,7 @@ export const providerSettingsSchema = z.object({ ...minimaxSchema.shape, ...requestySchema.shape, ...unboundSchema.shape, + ...futurmixSchema.shape, ...fakeAiSchema.shape, ...xaiSchema.shape, ...basetenSchema.shape, @@ -490,6 +499,7 @@ export const modelIdKeys = [ "lmStudioDraftModelId", "requestyModelId", "unboundModelId", + "futurmixModelId", "litellmModelId", "vercelAiGatewayModelId", ] as const satisfies readonly (keyof ProviderSettings)[] @@ -529,6 +539,7 @@ export const modelIdKeysByProvider: Record = { "qwen-code": "apiModelId", requesty: "requestyModelId", unbound: "unboundModelId", + futurmix: "futurmixModelId", xai: "apiModelId", baseten: "apiModelId", litellm: "litellmModelId", @@ -653,6 +664,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 00000000000..1ef152bde50 --- /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 6c180d5dda4..440c7615d45 100644 --- a/packages/types/src/providers/index.ts +++ b/packages/types/src/providers/index.ts @@ -19,6 +19,7 @@ export * from "./requesty.js" export * from "./roo.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" @@ -43,6 +44,7 @@ import { requestyDefaultModelId } from "./requesty.js" import { rooDefaultModelId } from "./roo.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" @@ -113,6 +115,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 1891113c03b..31049b819b3 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -23,6 +23,7 @@ import { VsCodeLmHandler, RequestyHandler, UnboundHandler, + FuturMixHandler, FakeAIHandler, XAIHandler, LiteLLMHandler, @@ -155,6 +156,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 00000000000..84eafad29c3 --- /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 a2c98e49caf..c70f1141ac6 100644 --- a/src/api/providers/fetchers/modelCache.ts +++ b/src/api/providers/fetchers/modelCache.ts @@ -20,6 +20,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" @@ -73,6 +74,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 41aff953d43..8c7add6629d 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 e3b8c1bea88..ce5bbb51ca1 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -952,6 +952,7 @@ export const webviewMessageHandler = async ( litellm: {}, requesty: {}, unbound: {}, + futurmix: {}, ollama: {}, lmstudio: {}, roo: {}, @@ -989,6 +990,13 @@ export const webviewMessageHandler = async ( apiKey: apiConfiguration.unboundApiKey, }, }, + { + key: "futurmix", + options: { + provider: "futurmix", + apiKey: apiConfiguration.futurmixApiKey, + }, + }, { key: "vercel-ai-gateway", options: { provider: "vercel-ai-gateway" } }, { key: "roo", diff --git a/src/shared/ProfileValidator.ts b/src/shared/ProfileValidator.ts index 7246a90177a..5d0a7183bf0 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 a68abcc3adc..8c9861ae2ad 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 roo: {} as { apiKey?: string; baseUrl?: string }, diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index a6e4cc3f5f6..d956355d5e4 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -33,6 +33,7 @@ import { vercelAiGatewayDefaultModelId, minimaxDefaultModelId, unboundDefaultModelId, + futurmixDefaultModelId, } from "@roo-code/types" import { @@ -87,6 +88,7 @@ import { Roo, SambaNova, Unbound, + FuturMix, Vertex, VSCodeLM, XAI, @@ -337,6 +339,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 }, @@ -538,6 +541,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 00000000000..16ed1172b2e --- /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 4a64ce9586b..68c36dd63ec 100644 --- a/webview-ui/src/components/settings/providers/index.ts +++ b/webview-ui/src/components/settings/providers/index.ts @@ -16,6 +16,7 @@ export { Roo } from "./Roo" 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 7192d9d4ee4..3f0e0e1b571 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 8ec42367f14..4a015b36100 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -418,6 +418,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/validate.ts b/webview-ui/src/utils/validate.ts index f506171acce..3d76390309b 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")