diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 965566a3193..c659eb80eb6 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -25,6 +25,7 @@ body: - AWS Bedrock - Chutes AI - DeepSeek + - Featherless AI - Fireworks AI - Glama - Google Gemini diff --git a/packages/types/npm/package.metadata.json b/packages/types/npm/package.metadata.json index b093d00a8f3..f34d00a0870 100644 --- a/packages/types/npm/package.metadata.json +++ b/packages/types/npm/package.metadata.json @@ -1,6 +1,6 @@ { "name": "@roo-code/types", - "version": "1.53.0", + "version": "1.55.0", "description": "TypeScript type definitions for Roo Code.", "publishConfig": { "access": "public", diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index 5d0854390da..dd72d72fe95 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -196,6 +196,7 @@ export const SECRET_STATE_KEYS = [ "sambaNovaApiKey", "zaiApiKey", "fireworksApiKey", + "featherlessApiKey", "ioIntelligenceApiKey", ] as const satisfies readonly (keyof ProviderSettings)[] export type SecretState = Pick diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts index c22683117ff..bacd74a1d6e 100644 --- a/packages/types/src/provider-settings.ts +++ b/packages/types/src/provider-settings.ts @@ -46,6 +46,7 @@ export const providerNames = [ "sambanova", "zai", "fireworks", + "featherless", "io-intelligence", "roo", ] as const @@ -284,6 +285,10 @@ const fireworksSchema = apiModelIdProviderModelSchema.extend({ fireworksApiKey: z.string().optional(), }) +const featherlessSchema = apiModelIdProviderModelSchema.extend({ + featherlessApiKey: z.string().optional(), +}) + const ioIntelligenceSchema = apiModelIdProviderModelSchema.extend({ ioIntelligenceModelId: z.string().optional(), ioIntelligenceApiKey: z.string().optional(), @@ -328,6 +333,7 @@ export const providerSettingsSchemaDiscriminated = z.discriminatedUnion("apiProv sambaNovaSchema.merge(z.object({ apiProvider: z.literal("sambanova") })), zaiSchema.merge(z.object({ apiProvider: z.literal("zai") })), fireworksSchema.merge(z.object({ apiProvider: z.literal("fireworks") })), + featherlessSchema.merge(z.object({ apiProvider: z.literal("featherless") })), ioIntelligenceSchema.merge(z.object({ apiProvider: z.literal("io-intelligence") })), rooSchema.merge(z.object({ apiProvider: z.literal("roo") })), defaultSchema, @@ -365,6 +371,7 @@ export const providerSettingsSchema = z.object({ ...sambaNovaSchema.shape, ...zaiSchema.shape, ...fireworksSchema.shape, + ...featherlessSchema.shape, ...ioIntelligenceSchema.shape, ...rooSchema.shape, ...codebaseIndexProviderSchema.shape, diff --git a/packages/types/src/providers/featherless.ts b/packages/types/src/providers/featherless.ts new file mode 100644 index 00000000000..d24f1fd8820 --- /dev/null +++ b/packages/types/src/providers/featherless.ts @@ -0,0 +1,58 @@ +import type { ModelInfo } from "../model.js" + +export type FeatherlessModelId = + | "deepseek-ai/DeepSeek-V3-0324" + | "deepseek-ai/DeepSeek-R1-0528" + | "moonshotai/Kimi-K2-Instruct" + | "openai/gpt-oss-120b" + | "Qwen/Qwen3-Coder-480B-A35B-Instruct" + +export const featherlessModels = { + "deepseek-ai/DeepSeek-V3-0324": { + maxTokens: 4096, + contextWindow: 32678, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0, + outputPrice: 0, + description: "DeepSeek V3 0324 model.", + }, + "deepseek-ai/DeepSeek-R1-0528": { + maxTokens: 4096, + contextWindow: 32678, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0, + outputPrice: 0, + description: "DeepSeek R1 0528 model.", + }, + "moonshotai/Kimi-K2-Instruct": { + maxTokens: 4096, + contextWindow: 32678, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0, + outputPrice: 0, + description: "Kimi K2 Instruct model.", + }, + "openai/gpt-oss-120b": { + maxTokens: 4096, + contextWindow: 32678, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0, + outputPrice: 0, + description: "GPT-OSS 120B model.", + }, + "Qwen/Qwen3-Coder-480B-A35B-Instruct": { + maxTokens: 4096, + contextWindow: 32678, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0, + outputPrice: 0, + description: "Qwen3 Coder 480B A35B Instruct model.", + }, +} as const satisfies Record + +export const featherlessDefaultModelId: FeatherlessModelId = "deepseek-ai/DeepSeek-R1-0528" diff --git a/packages/types/src/providers/index.ts b/packages/types/src/providers/index.ts index 6dff64a9795..2f60680bc68 100644 --- a/packages/types/src/providers/index.ts +++ b/packages/types/src/providers/index.ts @@ -26,3 +26,4 @@ export * from "./doubao.js" export * from "./zai.js" export * from "./fireworks.js" export * from "./roo.js" +export * from "./featherless.js" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c9fc6bb27f5..ff063ecb0d3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -584,8 +584,8 @@ importers: specifier: ^1.14.0 version: 1.14.0(typescript@5.8.3) '@roo-code/cloud': - specifier: ^0.18.0 - version: 0.18.0 + specifier: ^0.19.0 + version: 0.19.0 '@roo-code/ipc': specifier: workspace:^ version: link:../packages/ipc @@ -3106,11 +3106,11 @@ packages: cpu: [x64] os: [win32] - '@roo-code/cloud@0.18.0': - resolution: {integrity: sha512-Y2jbcUVB9RCQFAxHDPrfjWQU1o7yRvWaPAdA3eZjsUf+zfDL59Rwfghg6loqDfE/8HCkcJmHfLCKovNX5ju5qA==} + '@roo-code/cloud@0.19.0': + resolution: {integrity: sha512-alZ3X4+TPqRr0xSs9v/UDo3eTlcHaI8ZW8AbWPDtgqf86P8govnyM2hVUMhGXete3AlbYIPRE/9w3/7MrcIjsA==} - '@roo-code/types@1.54.0': - resolution: {integrity: sha512-Xj3Zn2FhXbG2bpwXuhrjKnkeuWypQCIPKljOLXnOCUqaMUhP1zkWwNZ+I3gIBUpDng/iWN3KHon1if0UaoXYQw==} + '@roo-code/types@1.55.0': + resolution: {integrity: sha512-+T5MP8IQcDp7htnGDnk3M4n7S5eYk6jNkw3VBSUBZRhS4EE2GuPDI+CcdmhnDiMb6NMV6yseL+CT4G4QV5ktUw==} '@sec-ant/readable-stream@0.4.1': resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} @@ -12314,9 +12314,9 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.40.2': optional: true - '@roo-code/cloud@0.18.0': + '@roo-code/cloud@0.19.0': dependencies: - '@roo-code/types': 1.54.0 + '@roo-code/types': 1.55.0 ioredis: 5.6.1 p-wait-for: 5.0.2 socket.io-client: 4.8.1 @@ -12326,7 +12326,7 @@ snapshots: - supports-color - utf-8-validate - '@roo-code/types@1.54.0': + '@roo-code/types@1.55.0': dependencies: zod: 3.25.76 diff --git a/src/api/index.ts b/src/api/index.ts index c80fd5bf721..48a0a89ec5c 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -36,6 +36,7 @@ import { ZAiHandler, FireworksHandler, RooHandler, + FeatherlessHandler, } from "./providers" import { NativeOllamaHandler } from "./providers/native-ollama" @@ -143,6 +144,8 @@ export function buildApiHandler(configuration: ProviderSettings): ApiHandler { return new IOIntelligenceHandler(options) case "roo": return new RooHandler(options) + case "featherless": + return new FeatherlessHandler(options) default: apiProvider satisfies "gemini-cli" | undefined return new AnthropicHandler(options) diff --git a/src/api/providers/__tests__/featherless.spec.ts b/src/api/providers/__tests__/featherless.spec.ts new file mode 100644 index 00000000000..b0b4c01b861 --- /dev/null +++ b/src/api/providers/__tests__/featherless.spec.ts @@ -0,0 +1,286 @@ +// npx vitest run api/providers/__tests__/featherless.spec.ts + +import { Anthropic } from "@anthropic-ai/sdk" +import OpenAI from "openai" + +import { + type FeatherlessModelId, + featherlessDefaultModelId, + featherlessModels, + DEEP_SEEK_DEFAULT_TEMPERATURE, +} from "@roo-code/types" + +import { FeatherlessHandler } from "../featherless" + +// Create mock functions +const mockCreate = vi.fn() + +// Mock OpenAI module +vi.mock("openai", () => ({ + default: vi.fn(() => ({ + chat: { + completions: { + create: mockCreate, + }, + }, + })), +})) + +describe("FeatherlessHandler", () => { + let handler: FeatherlessHandler + + beforeEach(() => { + vi.clearAllMocks() + // Set up default mock implementation + mockCreate.mockImplementation(async () => ({ + [Symbol.asyncIterator]: async function* () { + yield { + choices: [ + { + delta: { content: "Test response" }, + index: 0, + }, + ], + usage: null, + } + yield { + choices: [ + { + delta: {}, + index: 0, + }, + ], + usage: { + prompt_tokens: 10, + completion_tokens: 5, + total_tokens: 15, + }, + } + }, + })) + handler = new FeatherlessHandler({ featherlessApiKey: "test-key" }) + }) + + afterEach(() => { + vi.restoreAllMocks() + }) + + it("should use the correct Featherless base URL", () => { + new FeatherlessHandler({ featherlessApiKey: "test-featherless-api-key" }) + expect(OpenAI).toHaveBeenCalledWith(expect.objectContaining({ baseURL: "https://api.featherless.ai/v1" })) + }) + + it("should use the provided API key", () => { + const featherlessApiKey = "test-featherless-api-key" + new FeatherlessHandler({ featherlessApiKey }) + expect(OpenAI).toHaveBeenCalledWith(expect.objectContaining({ apiKey: featherlessApiKey })) + }) + + it("should handle DeepSeek R1 reasoning format", async () => { + // Override the mock for this specific test + mockCreate.mockImplementationOnce(async () => ({ + [Symbol.asyncIterator]: async function* () { + yield { + choices: [ + { + delta: { content: "Thinking..." }, + index: 0, + }, + ], + usage: null, + } + yield { + choices: [ + { + delta: { content: "Hello" }, + index: 0, + }, + ], + usage: null, + } + yield { + choices: [ + { + delta: {}, + index: 0, + }, + ], + usage: { prompt_tokens: 10, completion_tokens: 5 }, + } + }, + })) + + const systemPrompt = "You are a helpful assistant." + const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hi" }] + vi.spyOn(handler, "getModel").mockReturnValue({ + id: "deepseek-ai/DeepSeek-R1-0528", + info: { maxTokens: 1024, temperature: 0.7 }, + } as any) + + const stream = handler.createMessage(systemPrompt, messages) + const chunks = [] + for await (const chunk of stream) { + chunks.push(chunk) + } + + expect(chunks).toEqual([ + { type: "reasoning", text: "Thinking..." }, + { type: "text", text: "Hello" }, + { type: "usage", inputTokens: 10, outputTokens: 5 }, + ]) + }) + + it("should fall back to base provider for non-DeepSeek models", async () => { + // Use default mock implementation which returns text content + const systemPrompt = "You are a helpful assistant." + const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hi" }] + vi.spyOn(handler, "getModel").mockReturnValue({ + id: "some-other-model", + info: { maxTokens: 1024, temperature: 0.7 }, + } as any) + + const stream = handler.createMessage(systemPrompt, messages) + const chunks = [] + for await (const chunk of stream) { + chunks.push(chunk) + } + + expect(chunks).toEqual([ + { type: "text", text: "Test response" }, + { type: "usage", inputTokens: 10, outputTokens: 5 }, + ]) + }) + + it("should return default model when no model is specified", () => { + const model = handler.getModel() + expect(model.id).toBe(featherlessDefaultModelId) + expect(model.info).toEqual(expect.objectContaining(featherlessModels[featherlessDefaultModelId])) + }) + + it("should return specified model when valid model is provided", () => { + const testModelId: FeatherlessModelId = "deepseek-ai/DeepSeek-R1-0528" + const handlerWithModel = new FeatherlessHandler({ + apiModelId: testModelId, + featherlessApiKey: "test-featherless-api-key", + }) + const model = handlerWithModel.getModel() + expect(model.id).toBe(testModelId) + expect(model.info).toEqual(expect.objectContaining(featherlessModels[testModelId])) + }) + + it("completePrompt method should return text from Featherless API", async () => { + const expectedResponse = "This is a test response from Featherless" + mockCreate.mockResolvedValueOnce({ choices: [{ message: { content: expectedResponse } }] }) + const result = await handler.completePrompt("test prompt") + expect(result).toBe(expectedResponse) + }) + + it("should handle errors in completePrompt", async () => { + const errorMessage = "Featherless API error" + mockCreate.mockRejectedValueOnce(new Error(errorMessage)) + await expect(handler.completePrompt("test prompt")).rejects.toThrow( + `Featherless completion error: ${errorMessage}`, + ) + }) + + it("createMessage should yield text content from stream", async () => { + const testContent = "This is test content from Featherless stream" + + mockCreate.mockImplementationOnce(() => { + return { + [Symbol.asyncIterator]: () => ({ + next: vi + .fn() + .mockResolvedValueOnce({ + done: false, + value: { choices: [{ delta: { content: testContent } }] }, + }) + .mockResolvedValueOnce({ done: true }), + }), + } + }) + + const stream = handler.createMessage("system prompt", []) + const firstChunk = await stream.next() + + expect(firstChunk.done).toBe(false) + expect(firstChunk.value).toEqual({ type: "text", text: testContent }) + }) + + it("createMessage should yield usage data from stream", async () => { + mockCreate.mockImplementationOnce(() => { + return { + [Symbol.asyncIterator]: () => ({ + next: vi + .fn() + .mockResolvedValueOnce({ + done: false, + value: { choices: [{ delta: {} }], usage: { prompt_tokens: 10, completion_tokens: 20 } }, + }) + .mockResolvedValueOnce({ done: true }), + }), + } + }) + + const stream = handler.createMessage("system prompt", []) + const firstChunk = await stream.next() + + expect(firstChunk.done).toBe(false) + expect(firstChunk.value).toEqual({ type: "usage", inputTokens: 10, outputTokens: 20 }) + }) + + it("createMessage should pass correct parameters to Featherless client for DeepSeek R1", async () => { + const modelId: FeatherlessModelId = "deepseek-ai/DeepSeek-R1-0528" + + // Clear previous mocks and set up new implementation + mockCreate.mockClear() + mockCreate.mockImplementationOnce(async () => ({ + [Symbol.asyncIterator]: async function* () { + // Empty stream for this test + }, + })) + + const handlerWithModel = new FeatherlessHandler({ + apiModelId: modelId, + featherlessApiKey: "test-featherless-api-key", + }) + + const systemPrompt = "Test system prompt for Featherless" + const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Test message for Featherless" }] + + const messageGenerator = handlerWithModel.createMessage(systemPrompt, messages) + await messageGenerator.next() + + expect(mockCreate).toHaveBeenCalledWith( + expect.objectContaining({ + model: modelId, + messages: [ + { + role: "user", + content: `${systemPrompt}\n${messages[0].content}`, + }, + ], + }), + ) + }) + + it("should apply DeepSeek default temperature for R1 models", () => { + const testModelId: FeatherlessModelId = "deepseek-ai/DeepSeek-R1-0528" + const handlerWithModel = new FeatherlessHandler({ + apiModelId: testModelId, + featherlessApiKey: "test-featherless-api-key", + }) + const model = handlerWithModel.getModel() + expect(model.info.temperature).toBe(DEEP_SEEK_DEFAULT_TEMPERATURE) + }) + + it("should use default temperature for non-DeepSeek models", () => { + const testModelId: FeatherlessModelId = "moonshotai/Kimi-K2-Instruct" + const handlerWithModel = new FeatherlessHandler({ + apiModelId: testModelId, + featherlessApiKey: "test-featherless-api-key", + }) + const model = handlerWithModel.getModel() + expect(model.info.temperature).toBe(0.5) + }) +}) diff --git a/src/api/providers/featherless.ts b/src/api/providers/featherless.ts new file mode 100644 index 00000000000..56d7177de7c --- /dev/null +++ b/src/api/providers/featherless.ts @@ -0,0 +1,103 @@ +import { DEEP_SEEK_DEFAULT_TEMPERATURE, type FeatherlessModelId, featherlessDefaultModelId, featherlessModels } from "@roo-code/types" +import { Anthropic } from "@anthropic-ai/sdk" +import OpenAI from "openai" + +import type { ApiHandlerOptions } from "../../shared/api" +import { XmlMatcher } from "../../utils/xml-matcher" +import { convertToR1Format } from "../transform/r1-format" +import { convertToOpenAiMessages } from "../transform/openai-format" +import { ApiStream } from "../transform/stream" + +import { BaseOpenAiCompatibleProvider } from "./base-openai-compatible-provider" + +export class FeatherlessHandler extends BaseOpenAiCompatibleProvider { + constructor(options: ApiHandlerOptions) { + super({ + ...options, + providerName: "Featherless", + baseURL: "https://api.featherless.ai/v1", + apiKey: options.featherlessApiKey, + defaultProviderModelId: featherlessDefaultModelId, + providerModels: featherlessModels, + defaultTemperature: 0.5, + }) + } + + private getCompletionParams( + systemPrompt: string, + messages: Anthropic.Messages.MessageParam[], + ): OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming { + const { + id: model, + info: { maxTokens: max_tokens }, + } = this.getModel() + + const temperature = this.options.modelTemperature ?? this.getModel().info.temperature + + return { + model, + max_tokens, + temperature, + messages: [{ role: "system", content: systemPrompt }, ...convertToOpenAiMessages(messages)], + stream: true, + stream_options: { include_usage: true }, + } + } + + override async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream { + const model = this.getModel() + + if (model.id.includes("DeepSeek-R1")) { + const stream = await this.client.chat.completions.create({ + ...this.getCompletionParams(systemPrompt, messages), + messages: convertToR1Format([{ role: "user", content: systemPrompt }, ...messages]), + }) + + const matcher = new XmlMatcher( + "think", + (chunk) => + ({ + type: chunk.matched ? "reasoning" : "text", + text: chunk.data, + }) as const, + ) + + for await (const chunk of stream) { + const delta = chunk.choices[0]?.delta + + if (delta?.content) { + for (const processedChunk of matcher.update(delta.content)) { + yield processedChunk + } + } + + if (chunk.usage) { + yield { + type: "usage", + inputTokens: chunk.usage.prompt_tokens || 0, + outputTokens: chunk.usage.completion_tokens || 0, + } + } + } + + // Process any remaining content + for (const processedChunk of matcher.final()) { + yield processedChunk + } + } else { + yield* super.createMessage(systemPrompt, messages) + } + } + + override getModel() { + const model = super.getModel() + const isDeepSeekR1 = model.id.includes("DeepSeek-R1") + return { + ...model, + info: { + ...model.info, + temperature: isDeepSeekR1 ? DEEP_SEEK_DEFAULT_TEMPERATURE : this.defaultTemperature, + }, + } + } +} diff --git a/src/api/providers/index.ts b/src/api/providers/index.ts index 80ef0a28799..d256fbbe55e 100644 --- a/src/api/providers/index.ts +++ b/src/api/providers/index.ts @@ -30,3 +30,4 @@ export { XAIHandler } from "./xai" export { ZAiHandler } from "./zai" export { FireworksHandler } from "./fireworks" export { RooHandler } from "./roo" +export { FeatherlessHandler } from "./featherless" diff --git a/src/package.json b/src/package.json index d6c447db39f..3e77d3c3e17 100644 --- a/src/package.json +++ b/src/package.json @@ -427,7 +427,7 @@ "@mistralai/mistralai": "^1.3.6", "@modelcontextprotocol/sdk": "^1.9.0", "@qdrant/js-client-rest": "^1.14.0", - "@roo-code/cloud": "^0.18.0", + "@roo-code/cloud": "^0.19.0", "@roo-code/ipc": "workspace:^", "@roo-code/telemetry": "workspace:^", "@roo-code/types": "workspace:^", diff --git a/src/shared/ProfileValidator.ts b/src/shared/ProfileValidator.ts index afdf1b5232b..51eed227d3f 100644 --- a/src/shared/ProfileValidator.ts +++ b/src/shared/ProfileValidator.ts @@ -70,6 +70,7 @@ export class ProfileValidator { case "sambanova": case "chutes": case "fireworks": + case "featherless": return profile.apiModelId case "litellm": return profile.litellmModelId diff --git a/src/shared/__tests__/ProfileValidator.spec.ts b/src/shared/__tests__/ProfileValidator.spec.ts index 0129f8fc20d..4396e8268ab 100644 --- a/src/shared/__tests__/ProfileValidator.spec.ts +++ b/src/shared/__tests__/ProfileValidator.spec.ts @@ -195,6 +195,7 @@ describe("ProfileValidator", () => { "chutes", "sambanova", "fireworks", + "featherless", ] apiModelProviders.forEach((provider) => { diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index 6db3dab5298..787a95b1663 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -31,6 +31,7 @@ import { internationalZAiDefaultModelId, mainlandZAiDefaultModelId, fireworksDefaultModelId, + featherlessDefaultModelId, ioIntelligenceDefaultModelId, rooDefaultModelId, } from "@roo-code/types" @@ -87,6 +88,7 @@ import { XAI, ZAi, Fireworks, + Featherless, } from "./providers" import { MODELS_BY_PROVIDER, PROVIDERS } from "./constants" @@ -327,6 +329,7 @@ const ApiOptions = ({ : internationalZAiDefaultModelId, }, fireworks: { field: "apiModelId", default: fireworksDefaultModelId }, + featherless: { field: "apiModelId", default: featherlessDefaultModelId }, "io-intelligence": { field: "ioIntelligenceModelId", default: ioIntelligenceDefaultModelId }, roo: { field: "apiModelId", default: rooDefaultModelId }, openai: { field: "openAiModelId" }, @@ -600,6 +603,10 @@ const ApiOptions = ({ )} + {selectedProvider === "featherless" && ( + + )} + {selectedProviderModels.length > 0 && ( <>
diff --git a/webview-ui/src/components/settings/constants.ts b/webview-ui/src/components/settings/constants.ts index dc54d367eb6..cdeb71814d5 100644 --- a/webview-ui/src/components/settings/constants.ts +++ b/webview-ui/src/components/settings/constants.ts @@ -19,6 +19,7 @@ import { internationalZAiModels, fireworksModels, rooModels, + featherlessModels, } from "@roo-code/types" export const MODELS_BY_PROVIDER: Partial>> = { @@ -40,6 +41,7 @@ export const MODELS_BY_PROVIDER: Partial a.label.localeCompare(b.label)) diff --git a/webview-ui/src/components/settings/providers/Featherless.tsx b/webview-ui/src/components/settings/providers/Featherless.tsx new file mode 100644 index 00000000000..264e295dcca --- /dev/null +++ b/webview-ui/src/components/settings/providers/Featherless.tsx @@ -0,0 +1,50 @@ +import { useCallback } from "react" +import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react" + +import type { ProviderSettings } from "@roo-code/types" + +import { useAppTranslation } from "@src/i18n/TranslationContext" +import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink" + +import { inputEventTransform } from "../transforms" + +type FeatherlessProps = { + apiConfiguration: ProviderSettings + setApiConfigurationField: (field: keyof ProviderSettings, value: ProviderSettings[keyof ProviderSettings]) => void +} + +export const Featherless = ({ apiConfiguration, setApiConfigurationField }: FeatherlessProps) => { + 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")} +
+ {!apiConfiguration?.featherlessApiKey && ( + + {t("settings:providers.getFeatherlessApiKey")} + + )} + + ) +} diff --git a/webview-ui/src/components/settings/providers/index.ts b/webview-ui/src/components/settings/providers/index.ts index f054780b06e..eff33e12985 100644 --- a/webview-ui/src/components/settings/providers/index.ts +++ b/webview-ui/src/components/settings/providers/index.ts @@ -26,3 +26,4 @@ export { XAI } from "./XAI" export { ZAi } from "./ZAi" export { LiteLLM } from "./LiteLLM" export { Fireworks } from "./Fireworks" +export { Featherless } from "./Featherless" diff --git a/webview-ui/src/components/ui/hooks/useSelectedModel.ts b/webview-ui/src/components/ui/hooks/useSelectedModel.ts index a4a36857a09..75a4a968ad0 100644 --- a/webview-ui/src/components/ui/hooks/useSelectedModel.ts +++ b/webview-ui/src/components/ui/hooks/useSelectedModel.ts @@ -46,6 +46,8 @@ import { mainlandZAiModels, fireworksModels, fireworksDefaultModelId, + featherlessModels, + featherlessDefaultModelId, ioIntelligenceDefaultModelId, ioIntelligenceModels, rooDefaultModelId, @@ -292,6 +294,11 @@ function getSelectedModel({ const info = fireworksModels[id as keyof typeof fireworksModels] return { id, info } } + case "featherless": { + const id = apiConfiguration.apiModelId ?? featherlessDefaultModelId + const info = featherlessModels[id as keyof typeof featherlessModels] + return { id, info } + } case "io-intelligence": { const id = apiConfiguration.ioIntelligenceModelId ?? ioIntelligenceDefaultModelId const info = diff --git a/webview-ui/src/i18n/locales/ca/settings.json b/webview-ui/src/i18n/locales/ca/settings.json index 97c5e393521..d9c7ce7ee21 100644 --- a/webview-ui/src/i18n/locales/ca/settings.json +++ b/webview-ui/src/i18n/locales/ca/settings.json @@ -269,6 +269,8 @@ "getChutesApiKey": "Obtenir clau API de Chutes", "fireworksApiKey": "Clau API de Fireworks", "getFireworksApiKey": "Obtenir clau API de Fireworks", + "featherlessApiKey": "Clau API de Featherless", + "getFeatherlessApiKey": "Obtenir clau API de Featherless", "ioIntelligenceApiKey": "Clau API d'IO Intelligence", "ioIntelligenceApiKeyPlaceholder": "Introdueix la teva clau d'API de IO Intelligence", "getIoIntelligenceApiKey": "Obtenir clau API d'IO Intelligence", diff --git a/webview-ui/src/i18n/locales/de/settings.json b/webview-ui/src/i18n/locales/de/settings.json index 2d699a0e96b..7f09401e57a 100644 --- a/webview-ui/src/i18n/locales/de/settings.json +++ b/webview-ui/src/i18n/locales/de/settings.json @@ -271,6 +271,8 @@ "getChutesApiKey": "Chutes API-Schlüssel erhalten", "fireworksApiKey": "Fireworks API-Schlüssel", "getFireworksApiKey": "Fireworks API-Schlüssel erhalten", + "featherlessApiKey": "Featherless API-Schlüssel", + "getFeatherlessApiKey": "Featherless API-Schlüssel erhalten", "ioIntelligenceApiKey": "IO Intelligence API-Schlüssel", "ioIntelligenceApiKeyPlaceholder": "Gib deinen IO Intelligence API-Schlüssel ein", "getIoIntelligenceApiKey": "IO Intelligence API-Schlüssel erhalten", diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index 4f70e437c2c..d18a3bbd5e8 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -268,6 +268,8 @@ "getChutesApiKey": "Get Chutes API Key", "fireworksApiKey": "Fireworks API Key", "getFireworksApiKey": "Get Fireworks API Key", + "featherlessApiKey": "Featherless API Key", + "getFeatherlessApiKey": "Get Featherless API Key", "ioIntelligenceApiKey": "IO Intelligence API Key", "ioIntelligenceApiKeyPlaceholder": "Enter your IO Intelligence API key", "getIoIntelligenceApiKey": "Get IO Intelligence API Key", diff --git a/webview-ui/src/i18n/locales/es/settings.json b/webview-ui/src/i18n/locales/es/settings.json index 7f22887ee12..ec9795d8b07 100644 --- a/webview-ui/src/i18n/locales/es/settings.json +++ b/webview-ui/src/i18n/locales/es/settings.json @@ -269,6 +269,8 @@ "getChutesApiKey": "Obtener clave API de Chutes", "fireworksApiKey": "Clave API de Fireworks", "getFireworksApiKey": "Obtener clave API de Fireworks", + "featherlessApiKey": "Clave API de Featherless", + "getFeatherlessApiKey": "Obtener clave API de Featherless", "ioIntelligenceApiKey": "Clave API de IO Intelligence", "ioIntelligenceApiKeyPlaceholder": "Introduce tu clave de API de IO Intelligence", "getIoIntelligenceApiKey": "Obtener clave API de IO Intelligence", diff --git a/webview-ui/src/i18n/locales/fr/settings.json b/webview-ui/src/i18n/locales/fr/settings.json index c544673df64..68230b1a60d 100644 --- a/webview-ui/src/i18n/locales/fr/settings.json +++ b/webview-ui/src/i18n/locales/fr/settings.json @@ -269,6 +269,8 @@ "getChutesApiKey": "Obtenir la clé API Chutes", "fireworksApiKey": "Clé API Fireworks", "getFireworksApiKey": "Obtenir la clé API Fireworks", + "featherlessApiKey": "Clé API Featherless", + "getFeatherlessApiKey": "Obtenir la clé API Featherless", "ioIntelligenceApiKey": "Clé API IO Intelligence", "ioIntelligenceApiKeyPlaceholder": "Saisissez votre clé d'API IO Intelligence", "getIoIntelligenceApiKey": "Obtenir la clé API IO Intelligence", diff --git a/webview-ui/src/i18n/locales/hi/settings.json b/webview-ui/src/i18n/locales/hi/settings.json index 536d63d2a54..f98f7e65100 100644 --- a/webview-ui/src/i18n/locales/hi/settings.json +++ b/webview-ui/src/i18n/locales/hi/settings.json @@ -269,6 +269,8 @@ "getChutesApiKey": "Chutes API कुंजी प्राप्त करें", "fireworksApiKey": "Fireworks API कुंजी", "getFireworksApiKey": "Fireworks API कुंजी प्राप्त करें", + "featherlessApiKey": "Featherless API कुंजी", + "getFeatherlessApiKey": "Featherless API कुंजी प्राप्त करें", "ioIntelligenceApiKey": "IO Intelligence API कुंजी", "ioIntelligenceApiKeyPlaceholder": "अपना आईओ इंटेलिजेंस एपीआई कुंजी दर्ज करें", "getIoIntelligenceApiKey": "IO Intelligence API कुंजी प्राप्त करें", diff --git a/webview-ui/src/i18n/locales/id/settings.json b/webview-ui/src/i18n/locales/id/settings.json index 672adb9eda6..748bf198eb6 100644 --- a/webview-ui/src/i18n/locales/id/settings.json +++ b/webview-ui/src/i18n/locales/id/settings.json @@ -273,6 +273,8 @@ "getChutesApiKey": "Dapatkan Chutes API Key", "fireworksApiKey": "Fireworks API Key", "getFireworksApiKey": "Dapatkan Fireworks API Key", + "featherlessApiKey": "Featherless API Key", + "getFeatherlessApiKey": "Dapatkan Featherless API Key", "ioIntelligenceApiKey": "IO Intelligence API Key", "ioIntelligenceApiKeyPlaceholder": "Masukkan kunci API IO Intelligence Anda", "getIoIntelligenceApiKey": "Dapatkan IO Intelligence API Key", diff --git a/webview-ui/src/i18n/locales/it/settings.json b/webview-ui/src/i18n/locales/it/settings.json index a04258d3988..b97d96a61bc 100644 --- a/webview-ui/src/i18n/locales/it/settings.json +++ b/webview-ui/src/i18n/locales/it/settings.json @@ -269,6 +269,8 @@ "getChutesApiKey": "Ottieni chiave API Chutes", "fireworksApiKey": "Chiave API Fireworks", "getFireworksApiKey": "Ottieni chiave API Fireworks", + "featherlessApiKey": "Chiave API Featherless", + "getFeatherlessApiKey": "Ottieni chiave API Featherless", "ioIntelligenceApiKey": "Chiave API IO Intelligence", "ioIntelligenceApiKeyPlaceholder": "Inserisci la tua chiave API IO Intelligence", "getIoIntelligenceApiKey": "Ottieni chiave API IO Intelligence", diff --git a/webview-ui/src/i18n/locales/ja/settings.json b/webview-ui/src/i18n/locales/ja/settings.json index 6c1859c85dd..8061418d383 100644 --- a/webview-ui/src/i18n/locales/ja/settings.json +++ b/webview-ui/src/i18n/locales/ja/settings.json @@ -269,6 +269,8 @@ "getChutesApiKey": "Chutes APIキーを取得", "fireworksApiKey": "Fireworks APIキー", "getFireworksApiKey": "Fireworks APIキーを取得", + "featherlessApiKey": "Featherless APIキー", + "getFeatherlessApiKey": "Featherless APIキーを取得", "ioIntelligenceApiKey": "IO Intelligence APIキー", "ioIntelligenceApiKeyPlaceholder": "IO Intelligence APIキーを入力してください", "getIoIntelligenceApiKey": "IO Intelligence APIキーを取得", diff --git a/webview-ui/src/i18n/locales/ko/settings.json b/webview-ui/src/i18n/locales/ko/settings.json index e77a8069209..08f3a8c4e7c 100644 --- a/webview-ui/src/i18n/locales/ko/settings.json +++ b/webview-ui/src/i18n/locales/ko/settings.json @@ -269,6 +269,8 @@ "getChutesApiKey": "Chutes API 키 받기", "fireworksApiKey": "Fireworks API 키", "getFireworksApiKey": "Fireworks API 키 받기", + "featherlessApiKey": "Featherless API 키", + "getFeatherlessApiKey": "Featherless API 키 받기", "ioIntelligenceApiKey": "IO Intelligence API 키", "ioIntelligenceApiKeyPlaceholder": "IO Intelligence API 키를 입력하세요", "getIoIntelligenceApiKey": "IO Intelligence API 키 받기", diff --git a/webview-ui/src/i18n/locales/nl/settings.json b/webview-ui/src/i18n/locales/nl/settings.json index 42c1d97bdb9..cc4eb2c90f8 100644 --- a/webview-ui/src/i18n/locales/nl/settings.json +++ b/webview-ui/src/i18n/locales/nl/settings.json @@ -269,6 +269,8 @@ "getChutesApiKey": "Chutes API-sleutel ophalen", "fireworksApiKey": "Fireworks API-sleutel", "getFireworksApiKey": "Fireworks API-sleutel ophalen", + "featherlessApiKey": "Featherless API-sleutel", + "getFeatherlessApiKey": "Featherless API-sleutel ophalen", "ioIntelligenceApiKey": "IO Intelligence API-sleutel", "ioIntelligenceApiKeyPlaceholder": "Voer je IO Intelligence API-sleutel in", "getIoIntelligenceApiKey": "IO Intelligence API-sleutel ophalen", diff --git a/webview-ui/src/i18n/locales/pl/settings.json b/webview-ui/src/i18n/locales/pl/settings.json index f1abf9c79d6..107be09fdd6 100644 --- a/webview-ui/src/i18n/locales/pl/settings.json +++ b/webview-ui/src/i18n/locales/pl/settings.json @@ -269,6 +269,8 @@ "getChutesApiKey": "Uzyskaj klucz API Chutes", "fireworksApiKey": "Klucz API Fireworks", "getFireworksApiKey": "Uzyskaj klucz API Fireworks", + "featherlessApiKey": "Klucz API Featherless", + "getFeatherlessApiKey": "Uzyskaj klucz API Featherless", "ioIntelligenceApiKey": "Klucz API IO Intelligence", "ioIntelligenceApiKeyPlaceholder": "Wprowadź swój klucz API IO Intelligence", "getIoIntelligenceApiKey": "Uzyskaj klucz API IO Intelligence", diff --git a/webview-ui/src/i18n/locales/pt-BR/settings.json b/webview-ui/src/i18n/locales/pt-BR/settings.json index c566ee1e2d5..54343a2fc53 100644 --- a/webview-ui/src/i18n/locales/pt-BR/settings.json +++ b/webview-ui/src/i18n/locales/pt-BR/settings.json @@ -269,6 +269,8 @@ "getChutesApiKey": "Obter chave de API Chutes", "fireworksApiKey": "Chave de API Fireworks", "getFireworksApiKey": "Obter chave de API Fireworks", + "featherlessApiKey": "Chave de API Featherless", + "getFeatherlessApiKey": "Obter chave de API Featherless", "ioIntelligenceApiKey": "Chave de API IO Intelligence", "ioIntelligenceApiKeyPlaceholder": "Insira sua chave de API da IO Intelligence", "getIoIntelligenceApiKey": "Obter chave de API IO Intelligence", diff --git a/webview-ui/src/i18n/locales/ru/settings.json b/webview-ui/src/i18n/locales/ru/settings.json index 98df1f0138e..ac5fafdc176 100644 --- a/webview-ui/src/i18n/locales/ru/settings.json +++ b/webview-ui/src/i18n/locales/ru/settings.json @@ -269,6 +269,8 @@ "getChutesApiKey": "Получить Chutes API-ключ", "fireworksApiKey": "Fireworks API-ключ", "getFireworksApiKey": "Получить Fireworks API-ключ", + "featherlessApiKey": "Featherless API-ключ", + "getFeatherlessApiKey": "Получить Featherless API-ключ", "ioIntelligenceApiKey": "IO Intelligence API-ключ", "ioIntelligenceApiKeyPlaceholder": "Введите свой ключ API IO Intelligence", "getIoIntelligenceApiKey": "Получить IO Intelligence API-ключ", diff --git a/webview-ui/src/i18n/locales/tr/settings.json b/webview-ui/src/i18n/locales/tr/settings.json index 4fb043a8a0e..0c4138e783a 100644 --- a/webview-ui/src/i18n/locales/tr/settings.json +++ b/webview-ui/src/i18n/locales/tr/settings.json @@ -269,6 +269,8 @@ "getChutesApiKey": "Chutes API Anahtarı Al", "fireworksApiKey": "Fireworks API Anahtarı", "getFireworksApiKey": "Fireworks API Anahtarı Al", + "featherlessApiKey": "Featherless API Anahtarı", + "getFeatherlessApiKey": "Featherless API Anahtarı Al", "ioIntelligenceApiKey": "IO Intelligence API Anahtarı", "ioIntelligenceApiKeyPlaceholder": "IO Intelligence API anahtarınızı girin", "getIoIntelligenceApiKey": "IO Intelligence API Anahtarı Al", diff --git a/webview-ui/src/i18n/locales/vi/settings.json b/webview-ui/src/i18n/locales/vi/settings.json index c9e6b5afbbc..5b2a5a6ef81 100644 --- a/webview-ui/src/i18n/locales/vi/settings.json +++ b/webview-ui/src/i18n/locales/vi/settings.json @@ -269,6 +269,8 @@ "getChutesApiKey": "Lấy khóa API Chutes", "fireworksApiKey": "Khóa API Fireworks", "getFireworksApiKey": "Lấy khóa API Fireworks", + "featherlessApiKey": "Khóa API Featherless", + "getFeatherlessApiKey": "Lấy khóa API Featherless", "ioIntelligenceApiKey": "Khóa API IO Intelligence", "ioIntelligenceApiKeyPlaceholder": "Nhập khóa API IO Intelligence của bạn", "getIoIntelligenceApiKey": "Lấy khóa API IO Intelligence", diff --git a/webview-ui/src/i18n/locales/zh-CN/settings.json b/webview-ui/src/i18n/locales/zh-CN/settings.json index cc16cf349db..9e56ba74eea 100644 --- a/webview-ui/src/i18n/locales/zh-CN/settings.json +++ b/webview-ui/src/i18n/locales/zh-CN/settings.json @@ -269,6 +269,8 @@ "getChutesApiKey": "获取 Chutes API 密钥", "fireworksApiKey": "Fireworks API 密钥", "getFireworksApiKey": "获取 Fireworks API 密钥", + "featherlessApiKey": "Featherless API 密钥", + "getFeatherlessApiKey": "获取 Featherless API 密钥", "ioIntelligenceApiKey": "IO Intelligence API 密钥", "ioIntelligenceApiKeyPlaceholder": "输入您的 IO Intelligence API 密钥", "getIoIntelligenceApiKey": "获取 IO Intelligence API 密钥", diff --git a/webview-ui/src/i18n/locales/zh-TW/settings.json b/webview-ui/src/i18n/locales/zh-TW/settings.json index 791be9ac023..6bfab1435c6 100644 --- a/webview-ui/src/i18n/locales/zh-TW/settings.json +++ b/webview-ui/src/i18n/locales/zh-TW/settings.json @@ -269,6 +269,8 @@ "getChutesApiKey": "取得 Chutes API 金鑰", "fireworksApiKey": "Fireworks API 金鑰", "getFireworksApiKey": "取得 Fireworks API 金鑰", + "featherlessApiKey": "Featherless API 金鑰", + "getFeatherlessApiKey": "取得 Featherless API 金鑰", "ioIntelligenceApiKey": "IO Intelligence API 金鑰", "ioIntelligenceApiKeyPlaceholder": "輸入您的 IO Intelligence API 金鑰", "getIoIntelligenceApiKey": "取得 IO Intelligence API 金鑰", diff --git a/webview-ui/src/utils/validate.ts b/webview-ui/src/utils/validate.ts index e4c9ed483da..348a373059c 100644 --- a/webview-ui/src/utils/validate.ts +++ b/webview-ui/src/utils/validate.ts @@ -126,6 +126,11 @@ function validateModelsAndKeysProvided(apiConfiguration: ProviderSettings): stri return i18next.t("settings:validation.apiKey") } break + case "featherless": + if (!apiConfiguration.featherlessApiKey) { + return i18next.t("settings:validation.apiKey") + } + break } return undefined