Просмотр исходного кода

feat: Featherless provider (#7235)

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
Co-authored-by: cte <[email protected]>
DarinVerheijke 7 месяцев назад
Родитель
Сommit
57ea6257dd
38 измененных файлов с 583 добавлено и 11 удалено
  1. 1 0
      .github/ISSUE_TEMPLATE/bug_report.yml
  2. 1 1
      packages/types/npm/package.metadata.json
  3. 1 0
      packages/types/src/global-settings.ts
  4. 7 0
      packages/types/src/provider-settings.ts
  5. 58 0
      packages/types/src/providers/featherless.ts
  6. 1 0
      packages/types/src/providers/index.ts
  7. 9 9
      pnpm-lock.yaml
  8. 3 0
      src/api/index.ts
  9. 286 0
      src/api/providers/__tests__/featherless.spec.ts
  10. 103 0
      src/api/providers/featherless.ts
  11. 1 0
      src/api/providers/index.ts
  12. 1 1
      src/package.json
  13. 1 0
      src/shared/ProfileValidator.ts
  14. 1 0
      src/shared/__tests__/ProfileValidator.spec.ts
  15. 7 0
      webview-ui/src/components/settings/ApiOptions.tsx
  16. 3 0
      webview-ui/src/components/settings/constants.ts
  17. 50 0
      webview-ui/src/components/settings/providers/Featherless.tsx
  18. 1 0
      webview-ui/src/components/settings/providers/index.ts
  19. 7 0
      webview-ui/src/components/ui/hooks/useSelectedModel.ts
  20. 2 0
      webview-ui/src/i18n/locales/ca/settings.json
  21. 2 0
      webview-ui/src/i18n/locales/de/settings.json
  22. 2 0
      webview-ui/src/i18n/locales/en/settings.json
  23. 2 0
      webview-ui/src/i18n/locales/es/settings.json
  24. 2 0
      webview-ui/src/i18n/locales/fr/settings.json
  25. 2 0
      webview-ui/src/i18n/locales/hi/settings.json
  26. 2 0
      webview-ui/src/i18n/locales/id/settings.json
  27. 2 0
      webview-ui/src/i18n/locales/it/settings.json
  28. 2 0
      webview-ui/src/i18n/locales/ja/settings.json
  29. 2 0
      webview-ui/src/i18n/locales/ko/settings.json
  30. 2 0
      webview-ui/src/i18n/locales/nl/settings.json
  31. 2 0
      webview-ui/src/i18n/locales/pl/settings.json
  32. 2 0
      webview-ui/src/i18n/locales/pt-BR/settings.json
  33. 2 0
      webview-ui/src/i18n/locales/ru/settings.json
  34. 2 0
      webview-ui/src/i18n/locales/tr/settings.json
  35. 2 0
      webview-ui/src/i18n/locales/vi/settings.json
  36. 2 0
      webview-ui/src/i18n/locales/zh-CN/settings.json
  37. 2 0
      webview-ui/src/i18n/locales/zh-TW/settings.json
  38. 5 0
      webview-ui/src/utils/validate.ts

+ 1 - 0
.github/ISSUE_TEMPLATE/bug_report.yml

@@ -25,6 +25,7 @@ body:
         - AWS Bedrock
         - Chutes AI
         - DeepSeek
+        - Featherless AI
         - Fireworks AI
         - Glama
         - Google Gemini

+ 1 - 1
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",

+ 1 - 0
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<ProviderSettings, (typeof SECRET_STATE_KEYS)[number]>

+ 7 - 0
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,

+ 58 - 0
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<string, ModelInfo>
+
+export const featherlessDefaultModelId: FeatherlessModelId = "deepseek-ai/DeepSeek-R1-0528"

+ 1 - 0
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"

+ 9 - 9
pnpm-lock.yaml

@@ -584,8 +584,8 @@ importers:
         specifier: ^1.14.0
         version: 1.14.0([email protected])
       '@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/[email protected]8.0':
-    resolution: {integrity: sha512-Y2jbcUVB9RCQFAxHDPrfjWQU1o7yRvWaPAdA3eZjsUf+zfDL59Rwfghg6loqDfE/8HCkcJmHfLCKovNX5ju5qA==}
+  '@roo-code/[email protected]9.0':
+    resolution: {integrity: sha512-alZ3X4+TPqRr0xSs9v/UDo3eTlcHaI8ZW8AbWPDtgqf86P8govnyM2hVUMhGXete3AlbYIPRE/9w3/7MrcIjsA==}
 
-  '@roo-code/[email protected]4.0':
-    resolution: {integrity: sha512-Xj3Zn2FhXbG2bpwXuhrjKnkeuWypQCIPKljOLXnOCUqaMUhP1zkWwNZ+I3gIBUpDng/iWN3KHon1if0UaoXYQw==}
+  '@roo-code/[email protected]5.0':
+    resolution: {integrity: sha512-+T5MP8IQcDp7htnGDnk3M4n7S5eYk6jNkw3VBSUBZRhS4EE2GuPDI+CcdmhnDiMb6NMV6yseL+CT4G4QV5ktUw==}
 
   '@sec-ant/[email protected]':
     resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==}
@@ -12314,9 +12314,9 @@ snapshots:
   '@rollup/[email protected]':
     optional: true
 
-  '@roo-code/[email protected]8.0':
+  '@roo-code/[email protected]9.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/[email protected]4.0':
+  '@roo-code/[email protected]5.0':
     dependencies:
       zod: 3.25.76
 

+ 3 - 0
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)

+ 286 - 0
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: "<think>Thinking..." },
+							index: 0,
+						},
+					],
+					usage: null,
+				}
+				yield {
+					choices: [
+						{
+							delta: { content: "</think>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)
+	})
+})

+ 103 - 0
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<FeatherlessModelId> {
+	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,
+			},
+		}
+	}
+}

+ 1 - 0
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"

+ 1 - 1
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:^",

+ 1 - 0
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

+ 1 - 0
src/shared/__tests__/ProfileValidator.spec.ts

@@ -195,6 +195,7 @@ describe("ProfileValidator", () => {
 			"chutes",
 			"sambanova",
 			"fireworks",
+			"featherless",
 		]
 
 		apiModelProviders.forEach((provider) => {

+ 7 - 0
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 = ({
 				</div>
 			)}
 
+			{selectedProvider === "featherless" && (
+				<Featherless apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} />
+			)}
+
 			{selectedProviderModels.length > 0 && (
 				<>
 					<div>

+ 3 - 0
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<Record<ProviderName, Record<string, ModelInfo>>> = {
@@ -40,6 +41,7 @@ export const MODELS_BY_PROVIDER: Partial<Record<ProviderName, Record<string, Mod
 	zai: internationalZAiModels,
 	fireworks: fireworksModels,
 	roo: rooModels,
+	featherless: featherlessModels,
 }
 
 export const PROVIDERS = [
@@ -71,6 +73,7 @@ export const PROVIDERS = [
 	{ value: "sambanova", label: "SambaNova" },
 	{ value: "zai", label: "Z AI" },
 	{ value: "fireworks", label: "Fireworks AI" },
+	{ value: "featherless", label: "Featherless AI" },
 	{ value: "io-intelligence", label: "IO Intelligence" },
 	{ value: "roo", label: "Roo Code Cloud" },
 ].sort((a, b) => a.label.localeCompare(b.label))

+ 50 - 0
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(
+		<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?.featherlessApiKey || ""}
+				type="password"
+				onInput={handleInputChange("featherlessApiKey")}
+				placeholder={t("settings:placeholders.apiKey")}
+				className="w-full">
+				<label className="block font-medium mb-1">{t("settings:providers.featherlessApiKey")}</label>
+			</VSCodeTextField>
+			<div className="text-sm text-vscode-descriptionForeground -mt-2">
+				{t("settings:providers.apiKeyStorageNotice")}
+			</div>
+			{!apiConfiguration?.featherlessApiKey && (
+				<VSCodeButtonLink href="https://featherless.ai/account/api-keys" appearance="secondary">
+					{t("settings:providers.getFeatherlessApiKey")}
+				</VSCodeButtonLink>
+			)}
+		</>
+	)
+}

+ 1 - 0
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"

+ 7 - 0
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 =

+ 2 - 0
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",

+ 2 - 0
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",

+ 2 - 0
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",

+ 2 - 0
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",

+ 2 - 0
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",

+ 2 - 0
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 कुंजी प्राप्त करें",

+ 2 - 0
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",

+ 2 - 0
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",

+ 2 - 0
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キーを取得",

+ 2 - 0
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 키 받기",

+ 2 - 0
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",

+ 2 - 0
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",

+ 2 - 0
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",

+ 2 - 0
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-ключ",

+ 2 - 0
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",

+ 2 - 0
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",

+ 2 - 0
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 密钥",

+ 2 - 0
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 金鑰",

+ 5 - 0
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