mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
feat: add FuturMix provider
Add FuturMix (https://futurmix.ai) as an OpenAI-compatible provider — unified gateway for 25+ frontier models (Claude, GPT, Gemini, DeepSeek, Kimi). - packages/types: provider schema, settings, dynamic-provider registration - src/api: FuturMix handler + model fetcher (OpenAI-compatible /v1) - webview-ui: settings panel, model selection, validation, i18n
This commit is contained in:
parent
b867ec9145
commit
a3e0173af6
20 changed files with 425 additions and 1 deletions
|
|
@ -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<TypicalProvider, ModelIdKey> = {
|
|||
"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.
|
||||
|
|
|
|||
16
packages/types/src/providers/futurmix.ts
Normal file
16
packages/types/src/providers/futurmix.ts
Normal file
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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":
|
||||
|
|
|
|||
|
|
@ -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":
|
||||
|
|
|
|||
40
src/api/providers/fetchers/futurmix.ts
Normal file
40
src/api/providers/fetchers/futurmix.ts
Normal file
|
|
@ -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<Record<string, ModelInfo>> {
|
||||
const models: Record<string, ModelInfo> = {}
|
||||
|
||||
try {
|
||||
const headers: Record<string, string> = {}
|
||||
|
||||
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
|
||||
}
|
||||
|
|
@ -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<Model
|
|||
case "unbound":
|
||||
models = await getUnboundModels(options.apiKey)
|
||||
break
|
||||
case "futurmix":
|
||||
models = await getFuturMixModels(options.apiKey)
|
||||
break
|
||||
case "litellm":
|
||||
// Type safety ensures apiKey and baseUrl are always provided for LiteLLM.
|
||||
models = await getLiteLLMModels(options.apiKey, options.baseUrl)
|
||||
|
|
|
|||
188
src/api/providers/futurmix.ts
Normal file
188
src/api/providers/futurmix.ts
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import OpenAI from "openai"
|
||||
|
||||
import { type ModelInfo, type ModelRecord, futurmixDefaultModelId, futurmixDefaultModelInfo } from "@roo-code/types"
|
||||
|
||||
import type { ApiHandlerOptions } from "../../shared/api"
|
||||
import { calculateApiCostOpenAI } from "../../shared/cost"
|
||||
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { ApiStream, ApiStreamUsageChunk } from "../transform/stream"
|
||||
import { getModelParams } from "../transform/model-params"
|
||||
|
||||
import { DEFAULT_HEADERS } from "./constants"
|
||||
import { getModels } from "./fetchers/modelCache"
|
||||
import { BaseProvider } from "./base-provider"
|
||||
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
|
||||
import { handleOpenAIError } from "./utils/openai-error-handler"
|
||||
import { applyRouterToolPreferences } from "./utils/router-tool-preferences"
|
||||
|
||||
// FuturMix usage includes extra fields for Anthropic cache tokens.
|
||||
interface FuturMixUsage extends OpenAI.CompletionUsage {
|
||||
cache_creation_input_tokens?: number
|
||||
cache_read_input_tokens?: number
|
||||
}
|
||||
|
||||
export class FuturMixHandler extends BaseProvider implements SingleCompletionHandler {
|
||||
protected options: ApiHandlerOptions
|
||||
protected models: ModelRecord = {}
|
||||
private client: OpenAI
|
||||
private readonly providerName = "FuturMix"
|
||||
|
||||
constructor(options: ApiHandlerOptions) {
|
||||
super()
|
||||
|
||||
this.options = options
|
||||
|
||||
const apiKey = this.options.futurmixApiKey ?? "not-provided"
|
||||
|
||||
this.client = new OpenAI({
|
||||
baseURL: this.options.futurmixBaseUrl || "https://futurmix.ai/v1",
|
||||
apiKey: apiKey,
|
||||
defaultHeaders: DEFAULT_HEADERS,
|
||||
})
|
||||
}
|
||||
|
||||
public async fetchModel() {
|
||||
this.models = await getModels({ provider: "futurmix", apiKey: this.options.futurmixApiKey })
|
||||
return this.getModel()
|
||||
}
|
||||
|
||||
override getModel() {
|
||||
const id = this.options.futurmixModelId ?? futurmixDefaultModelId
|
||||
const cachedInfo = this.models[id] ?? futurmixDefaultModelInfo
|
||||
let info: ModelInfo = cachedInfo
|
||||
|
||||
// Apply tool preferences for models accessed through routers (OpenAI, Gemini)
|
||||
info = applyRouterToolPreferences(id, info)
|
||||
|
||||
const params = getModelParams({
|
||||
format: "openai",
|
||||
modelId: id,
|
||||
model: info,
|
||||
settings: this.options,
|
||||
defaultTemperature: 0,
|
||||
})
|
||||
|
||||
return { id, info, ...params }
|
||||
}
|
||||
|
||||
protected processUsageMetrics(usage: any, modelInfo?: ModelInfo): ApiStreamUsageChunk {
|
||||
const futurmixUsage = usage as FuturMixUsage
|
||||
const inputTokens = futurmixUsage?.prompt_tokens || 0
|
||||
const outputTokens = futurmixUsage?.completion_tokens || 0
|
||||
const cacheWriteTokens = futurmixUsage?.cache_creation_input_tokens || 0
|
||||
const cacheReadTokens = futurmixUsage?.cache_read_input_tokens || 0
|
||||
const { totalCost } = modelInfo
|
||||
? calculateApiCostOpenAI(modelInfo, inputTokens, outputTokens, cacheWriteTokens, cacheReadTokens)
|
||||
: { totalCost: 0 }
|
||||
|
||||
return {
|
||||
type: "usage",
|
||||
inputTokens: inputTokens,
|
||||
outputTokens: outputTokens,
|
||||
cacheWriteTokens: cacheWriteTokens,
|
||||
cacheReadTokens: cacheReadTokens,
|
||||
totalCost: totalCost,
|
||||
}
|
||||
}
|
||||
|
||||
override async *createMessage(
|
||||
systemPrompt: string,
|
||||
messages: Anthropic.Messages.MessageParam[],
|
||||
metadata?: ApiHandlerCreateMessageMetadata,
|
||||
): ApiStream {
|
||||
const {
|
||||
id: model,
|
||||
info,
|
||||
maxTokens: max_tokens,
|
||||
temperature,
|
||||
reasoningEffort: reasoning_effort,
|
||||
reasoning: thinking,
|
||||
} = await this.fetchModel()
|
||||
|
||||
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
||||
{ role: "system", content: systemPrompt },
|
||||
...convertToOpenAiMessages(messages),
|
||||
]
|
||||
|
||||
// Map extended efforts to OpenAI Chat Completions-accepted values (omit unsupported)
|
||||
const allowedEffort = (["low", "medium", "high"] as const).includes(reasoning_effort as any)
|
||||
? (reasoning_effort as OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming["reasoning_effort"])
|
||||
: undefined
|
||||
|
||||
const completionParams: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = {
|
||||
messages: openAiMessages,
|
||||
model,
|
||||
max_tokens,
|
||||
temperature,
|
||||
...(allowedEffort && { reasoning_effort: allowedEffort }),
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
tools: this.convertToolsForOpenAI(metadata?.tools),
|
||||
tool_choice: metadata?.tool_choice,
|
||||
}
|
||||
|
||||
let stream
|
||||
try {
|
||||
stream = await this.client.chat.completions.create(completionParams)
|
||||
} catch (error) {
|
||||
throw handleOpenAIError(error, this.providerName)
|
||||
}
|
||||
let lastUsage: any = undefined
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
|
||||
if (delta?.content) {
|
||||
yield { type: "text", text: delta.content }
|
||||
}
|
||||
|
||||
if (delta && "reasoning_content" in delta && delta.reasoning_content) {
|
||||
yield { type: "reasoning", text: (delta.reasoning_content as string | undefined) || "" }
|
||||
}
|
||||
|
||||
// Handle native tool calls
|
||||
if (delta && "tool_calls" in delta && Array.isArray(delta.tool_calls)) {
|
||||
for (const toolCall of delta.tool_calls) {
|
||||
yield {
|
||||
type: "tool_call_partial",
|
||||
index: toolCall.index,
|
||||
id: toolCall.id,
|
||||
name: toolCall.function?.name,
|
||||
arguments: toolCall.function?.arguments,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (chunk.usage) {
|
||||
lastUsage = chunk.usage
|
||||
}
|
||||
}
|
||||
|
||||
if (lastUsage) {
|
||||
yield this.processUsageMetrics(lastUsage, info)
|
||||
}
|
||||
}
|
||||
|
||||
async completePrompt(prompt: string): Promise<string> {
|
||||
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 || ""
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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" } },
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
|
|
|
|||
|
|
@ -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" && (
|
||||
<FuturMix
|
||||
apiConfiguration={apiConfiguration}
|
||||
setApiConfigurationField={setApiConfigurationField}
|
||||
routerModels={routerModels}
|
||||
refetchRouterModels={refetchRouterModels}
|
||||
organizationAllowList={organizationAllowList}
|
||||
modelValidationError={modelValidationError}
|
||||
simplifySettings={fromWelcomeView}
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedProvider === "anthropic" && (
|
||||
<Anthropic
|
||||
apiConfiguration={apiConfiguration}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ type ModelIdKey = keyof Pick<
|
|||
| "requestyModelId"
|
||||
| "unboundModelId"
|
||||
| "openAiModelId"
|
||||
| "futurmixModelId"
|
||||
| "litellmModelId"
|
||||
| "vercelAiGatewayModelId"
|
||||
| "apiModelId"
|
||||
|
|
|
|||
|
|
@ -64,5 +64,6 @@ export const PROVIDERS = [
|
|||
{ value: "minimax", label: "MiniMax", proxy: false },
|
||||
{ value: "baseten", label: "Baseten", proxy: false },
|
||||
{ value: "unbound", label: "Unbound", proxy: false },
|
||||
{ value: "futurmix", label: "FuturMix", proxy: false },
|
||||
{ value: "poe", label: "Poe", proxy: false },
|
||||
].sort((a, b) => a.label.localeCompare(b.label))
|
||||
|
|
|
|||
108
webview-ui/src/components/settings/providers/FuturMix.tsx
Normal file
108
webview-ui/src/components/settings/providers/FuturMix.tsx
Normal file
|
|
@ -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(
|
||||
<K extends keyof ProviderSettings, E>(
|
||||
field: K,
|
||||
transform: (event: E) => ProviderSettings[K] = inputEventTransform,
|
||||
) =>
|
||||
(event: E | Event) => {
|
||||
setApiConfigurationField(field, transform(event as E))
|
||||
},
|
||||
[setApiConfigurationField],
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.futurmixApiKey || ""}
|
||||
type="password"
|
||||
onInput={handleInputChange("futurmixApiKey")}
|
||||
placeholder={t("settings:providers.apiKey")}
|
||||
className="w-full">
|
||||
<div className="flex justify-between items-center mb-1">
|
||||
<label className="block font-medium">{t("settings:providers.apiKey")}</label>
|
||||
</div>
|
||||
</VSCodeTextField>
|
||||
<div className="text-sm text-vscode-descriptionForeground -mt-2">
|
||||
{t("settings:providers.apiKeyStorageNotice")}
|
||||
</div>
|
||||
<a
|
||||
href="https://futurmix.ai?utm_source=github_roo"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center justify-center whitespace-nowrap text-sm font-medium focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground shadow hover:bg-primary/90 h-9 rounded-md px-3 w-full"
|
||||
style={{
|
||||
width: "100%",
|
||||
textDecoration: "none",
|
||||
color: "var(--vscode-button-foreground)",
|
||||
backgroundColor: "var(--vscode-button-background)",
|
||||
}}>
|
||||
{t("settings:providers.getFuturMixApiKey")}
|
||||
</a>
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.futurmixBaseUrl || ""}
|
||||
onInput={handleInputChange("futurmixBaseUrl")}
|
||||
placeholder="https://futurmix.ai/v1"
|
||||
className="w-full">
|
||||
<label className="block font-medium mb-1">{t("settings:providers.openAiBaseUrl")}</label>
|
||||
</VSCodeTextField>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
vscode.postMessage({ type: "requestRouterModels", values: { provider: "futurmix", refresh: true } })
|
||||
}}>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="codicon codicon-refresh" />
|
||||
{t("settings:providers.refreshModels.label")}
|
||||
</div>
|
||||
</Button>
|
||||
<ModelPicker
|
||||
apiConfiguration={apiConfiguration}
|
||||
setApiConfigurationField={setApiConfigurationField}
|
||||
defaultModelId={futurmixDefaultModelId}
|
||||
models={routerModels?.futurmix ?? {}}
|
||||
modelIdKey="futurmixModelId"
|
||||
serviceName="FuturMix"
|
||||
serviceUrl="https://futurmix.ai/v1/models"
|
||||
organizationAllowList={organizationAllowList}
|
||||
errorMessage={modelValidationError}
|
||||
simplifySettings={simplifySettings}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ describe("Model Validation Functions", () => {
|
|||
},
|
||||
requesty: {},
|
||||
unbound: {},
|
||||
futurmix: {},
|
||||
litellm: {},
|
||||
ollama: {},
|
||||
lmstudio: {},
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue