From 84ab615365737f1db98ac9297bbffa8829c04963 Mon Sep 17 00:00:00 2001 From: Marcie Peters Date: Thu, 29 Jan 2026 20:56:12 +0000 Subject: [PATCH 1/4] feat: add Poe provider --- .changeset/add-poe-provider.md | 5 + apps/kilocode-docs/docs/providers/poe.md | 29 + cli/src/config/mapper.ts | 2 + cli/src/constants/providers/labels.ts | 1 + cli/src/constants/providers/models.ts | 6 + cli/src/constants/providers/settings.ts | 16 + cli/src/constants/providers/validation.ts | 1 + packages/core-schemas/src/config/provider.ts | 9 + packages/types/src/provider-settings.ts | 13 + packages/types/src/providers/index.ts | 4 + packages/types/src/providers/poe.ts | 17 + src/api/index.ts | 5 + src/api/providers/__tests__/poe.spec.ts | 538 ++++++++++++++++++ .../providers/fetchers/__tests__/poe.spec.ts | 427 ++++++++++++++ src/api/providers/fetchers/modelCache.ts | 6 + src/api/providers/fetchers/poe.ts | 71 +++ src/api/providers/index.ts | 1 + src/api/providers/poe.ts | 243 ++++++++ .../webview/__tests__/ClineProvider.spec.ts | 4 + .../__tests__/webviewMessageHandler.spec.ts | 18 + src/core/webview/webviewMessageHandler.ts | 5 + src/shared/api.ts | 1 + .../__tests__/getModelsByProvider.spec.ts | 1 + .../kilocode/hooks/useProviderModels.ts | 7 + .../src/components/settings/ApiOptions.tsx | 15 + .../src/components/settings/ModelPicker.tsx | 1 + .../src/components/settings/constants.ts | 1 + .../src/components/settings/providers/Poe.tsx | 99 ++++ .../components/settings/providers/index.ts | 1 + .../components/ui/hooks/useSelectedModel.ts | 7 + webview-ui/src/i18n/locales/en/settings.json | 2 + .../src/utils/__tests__/validate.spec.ts | 1 + 32 files changed, 1557 insertions(+) create mode 100644 .changeset/add-poe-provider.md create mode 100644 apps/kilocode-docs/docs/providers/poe.md create mode 100644 packages/types/src/providers/poe.ts create mode 100644 src/api/providers/__tests__/poe.spec.ts create mode 100644 src/api/providers/fetchers/__tests__/poe.spec.ts create mode 100644 src/api/providers/fetchers/poe.ts create mode 100644 src/api/providers/poe.ts create mode 100644 webview-ui/src/components/settings/providers/Poe.tsx diff --git a/.changeset/add-poe-provider.md b/.changeset/add-poe-provider.md new file mode 100644 index 00000000000..1de70d16bec --- /dev/null +++ b/.changeset/add-poe-provider.md @@ -0,0 +1,5 @@ +--- +"kilo-code": minor +--- + +Add Poe as a supported API provider diff --git a/apps/kilocode-docs/docs/providers/poe.md b/apps/kilocode-docs/docs/providers/poe.md new file mode 100644 index 00000000000..0a04ecb5bf1 --- /dev/null +++ b/apps/kilocode-docs/docs/providers/poe.md @@ -0,0 +1,29 @@ +--- +sidebar_label: Poe +--- + +# Using Poe With Kilo Code + +Kilo Code supports accessing models through the [Poe](https://www.poe.com/) platform. Poe provides an easy and optimized API for interacting with 200+ large language models (LLMs). + +**Website:** [https://www.poe.com/](https://www.poe.com/) + +## Getting an API Key + +1. **Sign Up/Sign In:** Go to the [Poe website](https://www.poe.com/) and create an account or sign in. +2. **Get API Key:** You can get an API key from the [API Management](https://poe.com/api_key) section of your Poe dashboard. + +## Supported Models + +Poe provides access to a wide range of models. Kilo Code will automatically fetch the latest list of available models. You can see the full list of available models on the [Model List](https://poe.com/explore?category=Official) page. + +## Configuration in Kilo Code + +1. **Open Kilo Code Settings:** Click the gear icon () in the Kilo Code panel. +2. **Select Provider:** Choose "Poe" from the "API Provider" dropdown. +3. **Enter API Key:** Paste your Poe API key into the "Poe API Key" field. +4. **Select Model:** Choose your desired model from the "Model" dropdown. + +## Relevant resources + +- [Poe Discord](https://discord.com/invite/joinpoe) diff --git a/cli/src/config/mapper.ts b/cli/src/config/mapper.ts index 89fba1b4cf1..511285ae254 100644 --- a/cli/src/config/mapper.ts +++ b/cli/src/config/mapper.ts @@ -127,6 +127,8 @@ export function getModelIdForProvider(provider: ProviderConfig): string { return provider.ioIntelligenceModelId || "" case "ovhcloud": return provider.ovhCloudAiEndpointsModelId || "" + case "poe": + return provider.poeModelId || "" case "inception": return provider.inceptionLabsModelId || "" case "bedrock": diff --git a/cli/src/constants/providers/labels.ts b/cli/src/constants/providers/labels.ts index ed69536f288..231a7b09e36 100644 --- a/cli/src/constants/providers/labels.ts +++ b/cli/src/constants/providers/labels.ts @@ -47,6 +47,7 @@ export const PROVIDER_LABELS: Record = { "human-relay": "Human Relay", "fake-ai": "Fake AI", ovhcloud: "OVHcloud AI Endpoints", + poe: "Poe", inception: "Inception", synthetic: "Synthetic", "sap-ai-core": "SAP AI Core", diff --git a/cli/src/constants/providers/models.ts b/cli/src/constants/providers/models.ts index fd97bbb13d0..c09e9190f6d 100644 --- a/cli/src/constants/providers/models.ts +++ b/cli/src/constants/providers/models.ts @@ -67,6 +67,7 @@ export type RouterName = | "vercel-ai-gateway" | "ovhcloud" | "nano-gpt" + | "poe" /** * ModelInfo interface - mirrors the one from packages/types/src/model.ts @@ -133,6 +134,7 @@ export const PROVIDER_TO_ROUTER_NAME: Record = "io-intelligence": "io-intelligence", "vercel-ai-gateway": "vercel-ai-gateway", ovhcloud: "ovhcloud", + poe: "poe", // Providers without dynamic model support anthropic: null, bedrock: null, @@ -186,6 +188,7 @@ export const PROVIDER_MODEL_FIELD: Record = { "io-intelligence": "ioIntelligenceModelId", "vercel-ai-gateway": "vercelAiGatewayModelId", ovhcloud: "ovhCloudAiEndpointsModelId", + poe: "poeModelId", // Providers without dynamic model support anthropic: null, bedrock: null, @@ -286,6 +289,7 @@ export const DEFAULT_MODEL_IDS: Partial> = { roo: rooDefaultModelId, "gemini-cli": geminiCliDefaultModelId, ovhcloud: ovhCloudAiEndpointsDefaultModelId, + poe: "gpt-4o", } /** @@ -466,6 +470,8 @@ export function getModelIdKey(provider: ProviderName): string { return "ovhCloudAiEndpointsModelId" case "nano-gpt": return "nanoGptModelId" + case "poe": + return "poeModelId" default: return "apiModelId" } diff --git a/cli/src/constants/providers/settings.ts b/cli/src/constants/providers/settings.ts index 8029adce74e..dc75486bc5f 100644 --- a/cli/src/constants/providers/settings.ts +++ b/cli/src/constants/providers/settings.ts @@ -666,6 +666,18 @@ export const FIELD_REGISTRY: Record = { type: "text", placeholder: "Enter profiles configuration...", }, + + // Poe fields + poeApiKey: { + label: "API Key", + type: "password", + placeholder: "Enter Poe API key...", + }, + poeModelId: { + label: "Model ID", + type: "text", + placeholder: "Enter model ID...", + }, } /** @@ -1046,6 +1058,9 @@ export const getProviderSettings = (provider: ProviderName, config: ProviderSett createFieldConfig("sapAiCoreModelId", config), ] + case "poe": + return [createFieldConfig("poeApiKey", config), createFieldConfig("poeModelId", config, "gpt-4o")] + default: return [] } @@ -1097,6 +1112,7 @@ export const PROVIDER_DEFAULT_MODELS: Record = { "fake-ai": "fake-model", "human-relay": "human-relay-model", ovhcloud: "gpt-oss-120b", + poe: "gpt-4o", inception: "gpt-4o", synthetic: "synthetic-model", "sap-ai-core": "gpt-4o", diff --git a/cli/src/constants/providers/validation.ts b/cli/src/constants/providers/validation.ts index b210acada70..2505c12993b 100644 --- a/cli/src/constants/providers/validation.ts +++ b/cli/src/constants/providers/validation.ts @@ -43,6 +43,7 @@ export const PROVIDER_REQUIRED_FIELDS: Record = { "fake-ai": ["apiModelId"], "human-relay": ["apiModelId"], ovhcloud: ["ovhCloudAiEndpointsApiKey", "ovhCloudAiEndpointsModelId"], + poe: ["poeApiKey", "poeModelId"], inception: ["inceptionLabsApiKey", "inceptionLabsModelId"], synthetic: ["syntheticApiKey", "apiModelId"], "sap-ai-core": ["sapAiCoreServiceKey", "sapAiCoreResourceGroup", "sapAiCoreDeploymentId", "sapAiCoreModelId"], diff --git a/packages/core-schemas/src/config/provider.ts b/packages/core-schemas/src/config/provider.ts index 588ef85044f..eecf377828f 100644 --- a/packages/core-schemas/src/config/provider.ts +++ b/packages/core-schemas/src/config/provider.ts @@ -106,6 +106,13 @@ export const deepInfraProviderSchema = baseProviderSchema.extend({ deepInfraApiKey: z.string().optional(), }) +// Poe provider +export const poeProviderSchema = baseProviderSchema.extend({ + provider: z.literal("poe"), + poeModelId: z.string().optional(), + poeApiKey: z.string().optional(), +}) + // Unbound provider export const unboundProviderSchema = baseProviderSchema.extend({ provider: z.literal("unbound"), @@ -394,6 +401,7 @@ export const providerConfigSchema = z.discriminatedUnion("provider", [ glamaProviderSchema, liteLLMProviderSchema, deepInfraProviderSchema, + poeProviderSchema, unboundProviderSchema, requestyProviderSchema, vercelAiGatewayProviderSchema, @@ -439,6 +447,7 @@ export type LMStudioProviderConfig = z.infer export type GlamaProviderConfig = z.infer export type LiteLLMProviderConfig = z.infer export type DeepInfraProviderConfig = z.infer +export type PoeProviderConfig = z.infer export type UnboundProviderConfig = z.infer export type RequestyProviderConfig = z.infer export type VercelAiGatewayProviderConfig = z.infer diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts index 72065e35942..79f8cd723bd 100644 --- a/packages/types/src/provider-settings.ts +++ b/packages/types/src/provider-settings.ts @@ -65,6 +65,7 @@ export const dynamicProviders = [ "roo", "chutes", "nano-gpt", //kilocode_change + "poe", // kilocode_change ] as const export type DynamicProvider = (typeof dynamicProviders)[number] @@ -371,6 +372,13 @@ const deepInfraSchema = apiModelIdProviderModelSchema.extend({ deepInfraModelId: z.string().optional(), }) +// kilocode_change start +const poeSchema = baseProviderSettingsSchema.extend({ + poeApiKey: z.string().optional(), + poeModelId: z.string().optional(), +}) +// kilocode_change end + const doubaoSchema = apiModelIdProviderModelSchema.extend({ doubaoBaseUrl: z.string().optional(), doubaoApiKey: z.string().optional(), @@ -563,6 +571,7 @@ export const providerSettingsSchemaDiscriminated = z.discriminatedUnion("apiProv mistralSchema.merge(z.object({ apiProvider: z.literal("mistral") })), deepSeekSchema.merge(z.object({ apiProvider: z.literal("deepseek") })), deepInfraSchema.merge(z.object({ apiProvider: z.literal("deepinfra") })), + poeSchema.merge(z.object({ apiProvider: z.literal("poe") })), // kilocode_change doubaoSchema.merge(z.object({ apiProvider: z.literal("doubao") })), moonshotSchema.merge(z.object({ apiProvider: z.literal("moonshot") })), minimaxSchema.merge(z.object({ apiProvider: z.literal("minimax") })), @@ -623,6 +632,7 @@ export const providerSettingsSchema = z.object({ ...mistralSchema.shape, ...deepSeekSchema.shape, ...deepInfraSchema.shape, + ...poeSchema.shape, // kilocode_change ...doubaoSchema.shape, ...moonshotSchema.shape, ...minimaxSchema.shape, @@ -682,6 +692,7 @@ export const modelIdKeys = [ "ioIntelligenceModelId", "vercelAiGatewayModelId", "deepInfraModelId", + "poeModelId", // kilocode_change "kilocodeModel", "ovhCloudAiEndpointsModelId", // kilocode_change "inceptionLabsModelId", // kilocode_change @@ -724,6 +735,7 @@ export const modelIdKeysByProvider: Record = { minimax: "apiModelId", deepseek: "apiModelId", deepinfra: "deepInfraModelId", + poe: "poeModelId", // kilocode_change doubao: "apiModelId", "qwen-code": "apiModelId", unbound: "unboundModelId", @@ -901,6 +913,7 @@ export const MODELS_BY_PROVIDER: Record< "virtual-quota-fallback": { id: "virtual-quota-fallback", label: "Virtual Quota Fallback", models: [] }, // kilocode_change end deepinfra: { id: "deepinfra", label: "DeepInfra", models: [] }, + poe: { id: "poe", label: "Poe", models: [] }, // kilocode_change "vercel-ai-gateway": { id: "vercel-ai-gateway", label: "Vercel AI Gateway", models: [] }, chutes: { id: "chutes", label: "Chutes AI", models: [] }, diff --git a/packages/types/src/providers/index.ts b/packages/types/src/providers/index.ts index 24f2aa9ccf9..18718784725 100644 --- a/packages/types/src/providers/index.ts +++ b/packages/types/src/providers/index.ts @@ -29,6 +29,7 @@ export * from "./ollama.js" export * from "./openai.js" export * from "./openai-codex.js" export * from "./openrouter.js" +export * from "./poe.js" export * from "./qwen-code.js" export * from "./requesty.js" export * from "./roo.js" @@ -61,6 +62,7 @@ import { mistralDefaultModelId } from "./mistral.js" import { moonshotDefaultModelId } from "./moonshot.js" import { openAiCodexDefaultModelId } from "./openai-codex.js" import { openRouterDefaultModelId } from "./openrouter.js" +import { poeDefaultModelId } from "./poe.js" import { qwenCodeDefaultModelId } from "./qwen-code.js" import { requestyDefaultModelId } from "./requesty.js" import { rooDefaultModelId } from "./roo.js" @@ -89,6 +91,8 @@ export function getProviderDefaultModelId( switch (provider) { case "openrouter": return openRouterDefaultModelId + case "poe": + return poeDefaultModelId case "requesty": return requestyDefaultModelId // kilocode_change start diff --git a/packages/types/src/providers/poe.ts b/packages/types/src/providers/poe.ts new file mode 100644 index 00000000000..6d6a439d2e8 --- /dev/null +++ b/packages/types/src/providers/poe.ts @@ -0,0 +1,17 @@ +import type { ModelInfo } from "../model.js" + +export const POE_BASE_URL = "https://api.poe.com/v1/" + +// Default fallback values for Poe when model metadata is not yet loaded. +export const poeDefaultModelId = "gpt-4o" + +export const poeDefaultModelInfo: ModelInfo = { + maxTokens: 8192, + contextWindow: 128000, + supportsImages: true, + supportsPromptCache: false, + supportsNativeTools: true, + inputPrice: 2.25, + outputPrice: 9.0, + description: "GPT-4o via Poe API", +} diff --git a/src/api/index.ts b/src/api/index.ts index 118f0bab4ab..9f2c1d1664e 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -11,6 +11,7 @@ import { AwsBedrockHandler, CerebrasHandler, OpenRouterHandler, + PoeHandler, // kilocode_change VertexHandler, AnthropicVertexHandler, OpenAiHandler, @@ -173,6 +174,10 @@ export function buildApiHandler(configuration: ProviderSettings): ApiHandler { // kilocode_change end case "openrouter": return new OpenRouterHandler(options) + // kilocode_change start + case "poe": + return new PoeHandler(options) + // kilocode_change end case "bedrock": return new AwsBedrockHandler(options) case "vertex": diff --git a/src/api/providers/__tests__/poe.spec.ts b/src/api/providers/__tests__/poe.spec.ts new file mode 100644 index 00000000000..cd021761261 --- /dev/null +++ b/src/api/providers/__tests__/poe.spec.ts @@ -0,0 +1,538 @@ +// npx vitest run src/api/providers/__tests__/poe.spec.ts + +import { Anthropic } from "@anthropic-ai/sdk" +import OpenAI from "openai" + +import { NATIVE_TOOL_DEFAULTS, POE_BASE_URL, poeDefaultModelId, poeDefaultModelInfo } from "@roo-code/types" + +import { PoeHandler } from "../poe" +import { ApiHandlerOptions } from "../../../shared/api" +import { Package } from "../../../shared/package" +import { ApiHandlerCreateMessageMetadata } from "../../index" + +const mockCreate = vitest.fn() + +vitest.mock("openai", () => { + return { + default: vitest.fn().mockImplementation(() => ({ + chat: { + completions: { + create: mockCreate, + }, + }, + apiKey: "test-key", + })), + } +}) + +// Mock model cache - returns models WITHOUT supportsNativeTools to test NATIVE_TOOL_DEFAULTS merge +vitest.mock("../fetchers/modelCache", () => ({ + getModels: vitest.fn().mockImplementation(() => { + return Promise.resolve({ + "gpt-4o": { + maxTokens: 16384, + contextWindow: 128000, + supportsImages: true, + supportsPromptCache: false, + supportsReasoningEffort: true, + inputPrice: 2.5, + outputPrice: 10, + description: "GPT-4o via Poe", + // Note: supportsNativeTools is intentionally NOT included + }, + "claude-sonnet-4": { + maxTokens: 8192, + contextWindow: 200000, + supportsImages: true, + supportsPromptCache: true, + supportsReasoningBudget: true, + inputPrice: 3, + outputPrice: 15, + description: "Claude Sonnet 4 via Poe", + }, + }) + }), +})) + +describe("PoeHandler", () => { + const mockOptions: ApiHandlerOptions = { + poeApiKey: "test-poe-key", + poeModelId: "gpt-4o", + } + + beforeEach(() => vitest.clearAllMocks()) + + describe("constructor", () => { + it("initializes with correct options", () => { + const handler = new PoeHandler(mockOptions) + expect(handler).toBeInstanceOf(PoeHandler) + + expect(OpenAI).toHaveBeenCalledWith({ + baseURL: POE_BASE_URL, + apiKey: mockOptions.poeApiKey, + defaultHeaders: { + "HTTP-Referer": "https://kilocode.ai", + "X-Title": "Kilo Code", + "X-KiloCode-Version": Package.version, + "User-Agent": `Kilo-Code/${Package.version}`, + }, + }) + }) + + it("uses default API key when not provided", () => { + const handler = new PoeHandler({}) + expect(handler).toBeInstanceOf(PoeHandler) + + expect(OpenAI).toHaveBeenCalledWith( + expect.objectContaining({ + apiKey: "not-provided", + }), + ) + }) + }) + + describe("getModel", () => { + it("merges NATIVE_TOOL_DEFAULTS into cached model info", async () => { + const handler = new PoeHandler(mockOptions) + const result = await handler.fetchModel() + + // Verify supportsNativeTools is true even though it's not in the cached model + expect(result.info.supportsNativeTools).toBe(true) + expect(result.info.defaultToolProtocol).toBe(NATIVE_TOOL_DEFAULTS.defaultToolProtocol) + + // Verify other cached properties are preserved + expect(result.id).toBe("gpt-4o") + expect(result.info.maxTokens).toBe(16384) + expect(result.info.contextWindow).toBe(128000) + expect(result.info.supportsImages).toBe(true) + }) + + it("returns default model when modelId not specified", async () => { + const handler = new PoeHandler({ poeApiKey: "test-key" }) + const result = await handler.fetchModel() + + expect(result.id).toBe(poeDefaultModelId) + }) + + it("falls back to default model info when model not in cache", async () => { + const handler = new PoeHandler({ poeApiKey: "test-key", poeModelId: "unknown-model" }) + const result = await handler.fetchModel() + + // Should fall back to default model info with NATIVE_TOOL_DEFAULTS merged + expect(result.info.supportsNativeTools).toBe(true) + }) + }) + + describe("createMessage", () => { + it("generates correct stream chunks for text content", async () => { + const handler = new PoeHandler(mockOptions) + + const mockStream = { + async *[Symbol.asyncIterator]() { + yield { + id: "test-id", + choices: [{ delta: { content: "Hello " } }], + } + yield { + id: "test-id", + choices: [{ delta: { content: "world!" } }], + } + yield { + id: "test-id", + choices: [{ delta: {} }], + usage: { + prompt_tokens: 10, + completion_tokens: 20, + }, + } + }, + } + + mockCreate.mockResolvedValue(mockStream) + + const systemPrompt = "You are a helpful assistant" + const messages: Anthropic.Messages.MessageParam[] = [{ role: "user" as const, content: "Hello" }] + + const chunks = [] + for await (const chunk of handler.createMessage(systemPrompt, messages)) { + chunks.push(chunk) + } + + expect(chunks).toHaveLength(3) + expect(chunks[0]).toEqual({ type: "text", text: "Hello " }) + expect(chunks[1]).toEqual({ type: "text", text: "world!" }) + expect(chunks[2]).toMatchObject({ + type: "usage", + inputTokens: 10, + outputTokens: 20, + }) + }) + + it("handles reasoning_content in stream", async () => { + const handler = new PoeHandler(mockOptions) + + const mockStream = { + async *[Symbol.asyncIterator]() { + yield { + id: "test-id", + choices: [{ delta: { reasoning_content: "Let me think..." } }], + } + yield { + id: "test-id", + choices: [{ delta: { content: "The answer is 42" } }], + } + yield { + id: "test-id", + choices: [{ delta: {} }], + usage: { prompt_tokens: 10, completion_tokens: 20 }, + } + }, + } + + mockCreate.mockResolvedValue(mockStream) + + const chunks = [] + for await (const chunk of handler.createMessage("system", [{ role: "user", content: "question" }])) { + chunks.push(chunk) + } + + expect(chunks[0]).toEqual({ type: "reasoning", text: "Let me think..." }) + expect(chunks[1]).toEqual({ type: "text", text: "The answer is 42" }) + }) + + it("includes tools in request when provided", async () => { + const mockStream = { + async *[Symbol.asyncIterator]() { + yield { id: "test-id", choices: [{ delta: { content: "test" } }] } + }, + } + mockCreate.mockResolvedValue(mockStream) + + const mockTools: OpenAI.Chat.ChatCompletionTool[] = [ + { + type: "function", + function: { + name: "read_file", + description: "Read a file", + parameters: { + type: "object", + properties: { path: { type: "string" } }, + required: ["path"], + }, + }, + }, + ] + + const metadata: ApiHandlerCreateMessageMetadata = { + taskId: "test-task", + tools: mockTools, + tool_choice: "auto", + } + + const handler = new PoeHandler(mockOptions) + const iterator = handler.createMessage("system", [{ role: "user", content: "read file" }], metadata) + await iterator.next() + + expect(mockCreate).toHaveBeenCalledWith( + expect.objectContaining({ + tools: expect.arrayContaining([ + expect.objectContaining({ + type: "function", + function: expect.objectContaining({ + name: "read_file", + }), + }), + ]), + tool_choice: "auto", + }), + ) + }) + + it("emits tool_call_partial chunks for streaming tool calls", async () => { + const mockStreamWithToolCalls = { + async *[Symbol.asyncIterator]() { + yield { + id: "test-id", + choices: [ + { + delta: { + tool_calls: [ + { + index: 0, + id: "call_abc123", + function: { + name: "read_file", + arguments: '{"path":', + }, + }, + ], + }, + }, + ], + } + yield { + id: "test-id", + choices: [ + { + delta: { + tool_calls: [ + { + index: 0, + function: { + arguments: '"/test.txt"}', + }, + }, + ], + }, + }, + ], + } + yield { + id: "test-id", + choices: [{ delta: {}, finish_reason: "tool_calls" }], + usage: { prompt_tokens: 10, completion_tokens: 20 }, + } + }, + } + mockCreate.mockResolvedValue(mockStreamWithToolCalls) + + const handler = new PoeHandler(mockOptions) + const chunks = [] + for await (const chunk of handler.createMessage("system", [{ role: "user", content: "read" }])) { + chunks.push(chunk) + } + + // Expect: 2 tool_call_partial, 1 tool_call_end, 1 usage + expect(chunks).toHaveLength(4) + + expect(chunks[0]).toEqual({ + type: "tool_call_partial", + index: 0, + id: "call_abc123", + name: "read_file", + arguments: '{"path":', + }) + + expect(chunks[1]).toEqual({ + type: "tool_call_partial", + index: 0, + id: undefined, + name: undefined, + arguments: '"/test.txt"}', + }) + + // Verify tool_call_end is emitted when finish_reason is "tool_calls" + expect(chunks[2]).toEqual({ + type: "tool_call_end", + id: "call_abc123", + }) + + expect(chunks[3]).toMatchObject({ + type: "usage", + inputTokens: 10, + outputTokens: 20, + }) + }) + + it("handles multiple concurrent tool calls", async () => { + const mockStreamWithMultipleTools = { + async *[Symbol.asyncIterator]() { + yield { + id: "test-id", + choices: [ + { + delta: { + tool_calls: [ + { index: 0, id: "call_1", function: { name: "tool_a", arguments: "{}" } }, + { index: 1, id: "call_2", function: { name: "tool_b", arguments: "{}" } }, + ], + }, + }, + ], + } + yield { + id: "test-id", + choices: [{ delta: {}, finish_reason: "tool_calls" }], + usage: { prompt_tokens: 10, completion_tokens: 20 }, + } + }, + } + mockCreate.mockResolvedValue(mockStreamWithMultipleTools) + + const handler = new PoeHandler(mockOptions) + const chunks = [] + for await (const chunk of handler.createMessage("system", [{ role: "user", content: "test" }])) { + chunks.push(chunk) + } + + // 2 tool_call_partial + 2 tool_call_end + 1 usage + expect(chunks).toHaveLength(5) + + const endChunks = chunks.filter((c) => c.type === "tool_call_end") + expect(endChunks).toHaveLength(2) + expect(endChunks.map((c) => c.id).sort()).toEqual(["call_1", "call_2"]) + }) + + it("handles API errors", async () => { + const handler = new PoeHandler(mockOptions) + const mockError = new Error("API Error") + mockCreate.mockRejectedValue(mockError) + + const generator = handler.createMessage("test", []) + await expect(generator.next()).rejects.toThrow() + }) + + it("processes usage metrics with cache tokens", async () => { + const mockStream = { + async *[Symbol.asyncIterator]() { + yield { + id: "test-id", + choices: [{ delta: { content: "test" } }], + } + yield { + id: "test-id", + choices: [{ delta: {} }], + usage: { + prompt_tokens: 100, + completion_tokens: 50, + prompt_tokens_details: { + caching_tokens: 20, + cached_tokens: 30, + }, + }, + } + }, + } + mockCreate.mockResolvedValue(mockStream) + + const handler = new PoeHandler(mockOptions) + const chunks = [] + for await (const chunk of handler.createMessage("system", [{ role: "user", content: "test" }])) { + chunks.push(chunk) + } + + const usageChunk = chunks.find((c) => c.type === "usage") + expect(usageChunk).toMatchObject({ + type: "usage", + inputTokens: 100, + outputTokens: 50, + cacheWriteTokens: 20, + cacheReadTokens: 30, + }) + }) + }) + + describe("getReasoningParams", () => { + it("uses thinking_budget for Anthropic models", async () => { + const handler = new PoeHandler({ + poeApiKey: "test-key", + poeModelId: "claude-sonnet-4", + modelMaxThinkingTokens: 10000, + enableReasoningEffort: true, + }) + + const mockStream = { + async *[Symbol.asyncIterator]() { + yield { id: "test-id", choices: [{ delta: { content: "test" } }] } + }, + } + mockCreate.mockResolvedValue(mockStream) + + const iterator = handler.createMessage("system", [{ role: "user", content: "test" }]) + await iterator.next() + + expect(mockCreate).toHaveBeenCalledWith( + expect.objectContaining({ + thinking_budget: 10000, + }), + ) + }) + + it("uses reasoning_effort for OpenAI models", async () => { + const handler = new PoeHandler({ + poeApiKey: "test-key", + poeModelId: "gpt-4o", + reasoningEffort: "high", + }) + + const mockStream = { + async *[Symbol.asyncIterator]() { + yield { id: "test-id", choices: [{ delta: { content: "test" } }] } + }, + } + mockCreate.mockResolvedValue(mockStream) + + const iterator = handler.createMessage("system", [{ role: "user", content: "test" }]) + await iterator.next() + + expect(mockCreate).toHaveBeenCalledWith( + expect.objectContaining({ + reasoning_effort: "high", + }), + ) + }) + + it("filters out unsupported reasoning_effort values for OpenAI", async () => { + const handler = new PoeHandler({ + poeApiKey: "test-key", + poeModelId: "gpt-4o", + reasoningEffort: "xhigh" as any, // Unsupported value + }) + + const mockStream = { + async *[Symbol.asyncIterator]() { + yield { id: "test-id", choices: [{ delta: { content: "test" } }] } + }, + } + mockCreate.mockResolvedValue(mockStream) + + const iterator = handler.createMessage("system", [{ role: "user", content: "test" }]) + await iterator.next() + + expect(mockCreate).toHaveBeenCalledWith( + expect.not.objectContaining({ + reasoning_effort: expect.anything(), + }), + ) + }) + }) + + describe("completePrompt", () => { + it("returns correct response", async () => { + const handler = new PoeHandler(mockOptions) + const mockResponse = { choices: [{ message: { content: "test completion" } }] } + + mockCreate.mockResolvedValue(mockResponse) + + const result = await handler.completePrompt("test prompt") + + expect(result).toBe("test completion") + + expect(mockCreate).toHaveBeenCalledWith( + expect.objectContaining({ + model: mockOptions.poeModelId, + messages: [{ role: "user", content: "test prompt" }], + }), + ) + }) + + it("returns empty string when no content", async () => { + const handler = new PoeHandler(mockOptions) + const mockResponse = { choices: [{ message: {} }] } + + mockCreate.mockResolvedValue(mockResponse) + + const result = await handler.completePrompt("test prompt") + + expect(result).toBe("") + }) + + it("handles API errors", async () => { + const handler = new PoeHandler(mockOptions) + const mockError = new Error("API Error") + mockCreate.mockRejectedValue(mockError) + + await expect(handler.completePrompt("test prompt")).rejects.toThrow() + }) + }) +}) diff --git a/src/api/providers/fetchers/__tests__/poe.spec.ts b/src/api/providers/fetchers/__tests__/poe.spec.ts new file mode 100644 index 00000000000..6db43d79789 --- /dev/null +++ b/src/api/providers/fetchers/__tests__/poe.spec.ts @@ -0,0 +1,427 @@ +// npx vitest run src/api/providers/fetchers/__tests__/poe.spec.ts + +vi.mock("axios") + +import type { Mock } from "vitest" +import axios from "axios" +import { getPoeModels } from "../poe" +import { POE_BASE_URL } from "@roo-code/types" + +const mockedAxios = axios as typeof axios & { + get: Mock +} + +describe("getPoeModels", () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it("should fetch and parse models successfully", async () => { + const mockResponse = { + data: { + data: [ + { + id: "gpt-4o", + object: "model", + owned_by: "OpenAI", + description: "GPT-4o model", + architecture: { + input_modalities: ["text", "image"], + output_modalities: ["text"], + }, + context_window: { + context_length: 128000, + max_output_tokens: 16384, + }, + pricing: { + prompt: "0.0000025", + completion: "0.00001", + }, + }, + ], + }, + } + + mockedAxios.get.mockResolvedValue(mockResponse) + + const models = await getPoeModels("test-api-key") + + expect(mockedAxios.get).toHaveBeenCalledWith(`${POE_BASE_URL}models`, { + headers: { + Authorization: "Bearer test-api-key", + }, + }) + + expect(models["gpt-4o"]).toEqual({ + maxTokens: 16384, + contextWindow: 128000, + supportsImages: true, + supportsPromptCache: false, + supportsComputerUse: undefined, + supportsReasoningBudget: false, + supportsReasoningEffort: false, + requiredReasoningBudget: undefined, + inputPrice: 2.5, + outputPrice: 10, + description: "GPT-4o model", + cacheWritesPrice: undefined, + cacheReadsPrice: undefined, + }) + }) + + it("should work without API key", async () => { + const mockResponse = { + data: { + data: [ + { + id: "test-model", + architecture: { + input_modalities: ["text"], + output_modalities: ["text"], + }, + context_window: { + context_length: 8000, + max_output_tokens: 4000, + }, + }, + ], + }, + } + + mockedAxios.get.mockResolvedValue(mockResponse) + + const models = await getPoeModels() + + expect(mockedAxios.get).toHaveBeenCalledWith(`${POE_BASE_URL}models`, { + headers: {}, + }) + + expect(models["test-model"]).toBeDefined() + }) + + it("should detect image support from input_modalities", async () => { + const mockResponse = { + data: { + data: [ + { + id: "vision-model", + architecture: { + input_modalities: ["text", "image"], + output_modalities: ["text"], + }, + context_window: { + context_length: 128000, + max_output_tokens: 8192, + }, + }, + { + id: "text-only-model", + architecture: { + input_modalities: ["text"], + output_modalities: ["text"], + }, + context_window: { + context_length: 64000, + max_output_tokens: 4096, + }, + }, + ], + }, + } + + mockedAxios.get.mockResolvedValue(mockResponse) + + const models = await getPoeModels("test-key") + + expect(models["vision-model"].supportsImages).toBe(true) + expect(models["text-only-model"].supportsImages).toBe(false) + }) + + it("should parse reasoning capabilities", async () => { + const mockResponse = { + data: { + data: [ + { + id: "claude-opus-4.5", + architecture: { + input_modalities: ["text", "image"], + output_modalities: ["text"], + }, + context_window: { + context_length: 200000, + max_output_tokens: 64000, + }, + reasoning: { + budget: { + max_tokens: 63999, + min_tokens: 0, + }, + required: false, + supports_reasoning_effort: false, + }, + }, + { + id: "gpt-5.2-pro", + architecture: { + input_modalities: ["text"], + output_modalities: ["text"], + }, + context_window: { + context_length: 400000, + max_output_tokens: 128000, + }, + reasoning: { + supports_reasoning_effort: true, + }, + }, + ], + }, + } + + mockedAxios.get.mockResolvedValue(mockResponse) + + const models = await getPoeModels("test-key") + + expect(models["claude-opus-4.5"].supportsReasoningBudget).toBe(true) + expect(models["claude-opus-4.5"].supportsReasoningEffort).toBe(false) + expect(models["claude-opus-4.5"].requiredReasoningBudget).toBe(undefined) + + expect(models["gpt-5.2-pro"].supportsReasoningBudget).toBe(false) + expect(models["gpt-5.2-pro"].supportsReasoningEffort).toBe(true) + }) + + it("should handle required reasoning budget", async () => { + const mockResponse = { + data: { + data: [ + { + id: "reasoning-model", + architecture: { + input_modalities: ["text"], + output_modalities: ["text"], + }, + context_window: { + context_length: 100000, + max_output_tokens: 32000, + }, + reasoning: { + budget: { + max_tokens: 50000, + min_tokens: 1000, + }, + required: true, + }, + }, + ], + }, + } + + mockedAxios.get.mockResolvedValue(mockResponse) + + const models = await getPoeModels("test-key") + + expect(models["reasoning-model"].requiredReasoningBudget).toBe(true) + }) + + it("should detect prompt cache support from pricing", async () => { + const mockResponse = { + data: { + data: [ + { + id: "cache-model", + architecture: { + input_modalities: ["text"], + output_modalities: ["text"], + }, + context_window: { + context_length: 200000, + max_output_tokens: 8192, + }, + pricing: { + prompt: "0.000003", + completion: "0.000015", + input_cache_read: "0.0000003", + input_cache_write: "0.00000375", + }, + }, + { + id: "no-cache-model", + architecture: { + input_modalities: ["text"], + output_modalities: ["text"], + }, + context_window: { + context_length: 128000, + max_output_tokens: 4096, + }, + pricing: { + prompt: "0.000002", + completion: "0.00001", + }, + }, + ], + }, + } + + mockedAxios.get.mockResolvedValue(mockResponse) + + const models = await getPoeModels("test-key") + + expect(models["cache-model"].supportsPromptCache).toBe(true) + expect(models["cache-model"].cacheReadsPrice).toBe(0.3) + expect(models["cache-model"].cacheWritesPrice).toBe(3.75) + + expect(models["no-cache-model"].supportsPromptCache).toBe(false) + }) + + it("should skip models without text output modality", async () => { + const mockResponse = { + data: { + data: [ + { + id: "image-gen-model", + architecture: { + input_modalities: ["text"], + output_modalities: ["image"], // Not text + }, + context_window: { + context_length: 4096, + max_output_tokens: 1, + }, + }, + { + id: "text-model", + architecture: { + input_modalities: ["text"], + output_modalities: ["text"], + }, + context_window: { + context_length: 128000, + max_output_tokens: 8192, + }, + }, + ], + }, + } + + mockedAxios.get.mockResolvedValue(mockResponse) + + const models = await getPoeModels("test-key") + + expect(models["image-gen-model"]).toBeUndefined() + expect(models["text-model"]).toBeDefined() + }) + + it("should return empty object on API error", async () => { + mockedAxios.get.mockRejectedValue(new Error("Network error")) + + const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}) + + const models = await getPoeModels("test-key") + + expect(models).toEqual({}) + expect(consoleErrorSpy).toHaveBeenCalled() + + consoleErrorSpy.mockRestore() + }) + + it("should return empty object when API returns empty data", async () => { + const mockResponse = { + data: { + data: [], + }, + } + + mockedAxios.get.mockResolvedValue(mockResponse) + + const models = await getPoeModels("test-key") + + expect(models).toEqual({}) + }) + + it("should handle missing context_window gracefully", async () => { + const mockResponse = { + data: { + data: [ + { + id: "minimal-model", + architecture: { + input_modalities: ["text"], + output_modalities: ["text"], + }, + // No context_window field + }, + ], + }, + } + + mockedAxios.get.mockResolvedValue(mockResponse) + + const models = await getPoeModels("test-key") + + expect(models["minimal-model"]).toBeDefined() + expect(models["minimal-model"].contextWindow).toBe(0) + expect(models["minimal-model"].maxTokens).toBe(0) + }) + + it("should parse computer use support", async () => { + const mockResponse = { + data: { + data: [ + { + id: "computer-use-model", + architecture: { + input_modalities: ["text", "image"], + output_modalities: ["text"], + }, + context_window: { + context_length: 200000, + max_output_tokens: 8192, + }, + supports_computer_use: true, + }, + ], + }, + } + + mockedAxios.get.mockResolvedValue(mockResponse) + + const models = await getPoeModels("test-key") + + expect(models["computer-use-model"].supportsComputerUse).toBe(true) + }) + + it("should handle alternative cache pricing field names", async () => { + const mockResponse = { + data: { + data: [ + { + id: "alt-cache-model", + architecture: { + input_modalities: ["text"], + output_modalities: ["text"], + }, + context_window: { + context_length: 200000, + max_output_tokens: 8192, + }, + pricing: { + prompt: "0.000003", + completion: "0.000015", + cache_read: "0.0000003", // Alternative field name + cache_creation: "0.00000375", // Alternative field name + }, + }, + ], + }, + } + + mockedAxios.get.mockResolvedValue(mockResponse) + + const models = await getPoeModels("test-key") + + expect(models["alt-cache-model"].supportsPromptCache).toBe(true) + expect(models["alt-cache-model"].cacheReadsPrice).toBe(0.3) + expect(models["alt-cache-model"].cacheWritesPrice).toBe(3.75) + }) +}) diff --git a/src/api/providers/fetchers/modelCache.ts b/src/api/providers/fetchers/modelCache.ts index 0837fca3ce5..75770b300f5 100644 --- a/src/api/providers/fetchers/modelCache.ts +++ b/src/api/providers/fetchers/modelCache.ts @@ -40,6 +40,7 @@ import { getHuggingFaceModels } from "./huggingface" import { getRooModels } from "./roo" import { getChutesModels } from "./chutes" import { getNanoGptModels } from "./nano-gpt" //kilocode_change +import { getPoeModels } from "./poe" // kilocode_change const memoryCache = new NodeCache({ stdTTL: 5 * 60, checkperiod: 5 * 60 }) @@ -174,6 +175,11 @@ async function fetchModelsFromProvider(options: GetModelsOptions): Promise> { + const models: Record = {} + + try { + const headers: Record = {} + + if (apiKey) { + headers["Authorization"] = `Bearer ${apiKey}` + } + const modelsUrl = new URL("models", POE_BASE_URL) + + const response = await axios.get(modelsUrl.toString(), { headers }) + const rawModels = response.data.data + + for (const rawModel of rawModels) { + const { architecture, reasoning, context_window } = rawModel + + const supportText = architecture?.output_modalities?.includes("text") + + if (!supportText) { + continue + } + + const supportsImages = architecture?.input_modalities?.includes("image") ?? false + + // Read reasoning capabilities from the API's reasoning object + const supportsReasoningBudget = reasoning?.budget ? true : false + const supportsReasoningEffort = reasoning?.supports_reasoning_effort ?? false + const requiredReasoningBudget = reasoning?.required ?? false + + // Handle context window structure + const contextLength = context_window?.context_length ?? 0 + const maxOutputTokens = context_window?.max_output_tokens ?? 0 + + // Determine cache support from pricing fields + const hasCacheReads = rawModel.pricing?.input_cache_read || rawModel.pricing?.cache_read + const supportsPromptCache = !!hasCacheReads + + const modelInfo: ModelInfo = { + maxTokens: maxOutputTokens, + contextWindow: contextLength, + supportsPromptCache, + supportsImages, + supportsComputerUse: rawModel.supports_computer_use, + supportsReasoningBudget, + supportsReasoningEffort, + requiredReasoningBudget: requiredReasoningBudget || undefined, + inputPrice: parseApiPrice(rawModel.pricing?.prompt), + outputPrice: parseApiPrice(rawModel.pricing?.completion), + description: rawModel.description, + cacheWritesPrice: parseApiPrice( + rawModel.pricing?.input_cache_write || rawModel.pricing?.cache_creation, + ), + cacheReadsPrice: parseApiPrice(rawModel.pricing?.input_cache_read || rawModel.pricing?.cache_read), + } + + models[rawModel.id] = modelInfo + } + } catch (error) { + console.error(`Error fetching Poe models: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`) + } + + return models +} diff --git a/src/api/providers/index.ts b/src/api/providers/index.ts index 4143ff709aa..1db4cf6097c 100644 --- a/src/api/providers/index.ts +++ b/src/api/providers/index.ts @@ -22,6 +22,7 @@ export { OpenAiCodexHandler } from "./openai-codex" export { OpenAiNativeHandler } from "./openai-native" export { OpenAiHandler } from "./openai" export { OpenRouterHandler } from "./openrouter" +export { PoeHandler } from "./poe" // kilocode_change export { QwenCodeHandler } from "./qwen-code" export { RequestyHandler } from "./requesty" export { SambaNovaHandler } from "./sambanova" diff --git a/src/api/providers/poe.ts b/src/api/providers/poe.ts new file mode 100644 index 00000000000..9bc703bf4b7 --- /dev/null +++ b/src/api/providers/poe.ts @@ -0,0 +1,243 @@ +// kilocode_change - provider added +import { Anthropic } from "@anthropic-ai/sdk" +import OpenAI from "openai" + +import { + type ModelInfo, + type ReasoningEffortExtended, + poeDefaultModelId, + poeDefaultModelInfo, + POE_BASE_URL, + NATIVE_TOOL_DEFAULTS, +} from "@roo-code/types" + +import type { ApiHandlerOptions } from "../../shared/api" +import { calculateApiCostOpenAI } from "../../shared/cost" + +import { ApiStream, ApiStreamUsageChunk } from "../transform/stream" +import { convertToOpenAiMessages } from "../transform/openai-format" +import { getModelParams } from "../transform/model-params" + +import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" +import { RouterProvider } from "./router-provider" +import { getModels } from "./fetchers/modelCache" +import { verifyFinishReason } from "./kilocode/verifyFinishReason" +import { handleOpenAIError } from "./utils/openai-error-handler" + +// Extended params type for Poe-specific fields +type PoeExtensions = { + thinking_budget?: number +} + +type PoeChatCompletionParamsStreaming = OpenAI.Chat.ChatCompletionCreateParamsStreaming & PoeExtensions +type PoeChatCompletionParamsNonStreaming = OpenAI.Chat.ChatCompletionCreateParamsNonStreaming & PoeExtensions + +export class PoeHandler extends RouterProvider implements SingleCompletionHandler { + private readonly providerName = "Poe" + + constructor(options: ApiHandlerOptions) { + super({ + options, + name: "poe", + baseURL: POE_BASE_URL, + apiKey: options.poeApiKey || "not-provided", + modelId: options.poeModelId, + defaultModelId: poeDefaultModelId, + defaultModelInfo: poeDefaultModelInfo, + }) + } + + public override async fetchModel() { + this.models = await getModels({ provider: this.name, apiKey: this.client.apiKey }) + return this.getModel() + } + + override getModel() { + const id = this.options.poeModelId ?? poeDefaultModelId + const cachedInfo = this.models[id] ?? poeDefaultModelInfo + + // Merge native tool defaults for cached models that may lack these fields + const info: ModelInfo = { ...NATIVE_TOOL_DEFAULTS, ...cachedInfo } + + const params = getModelParams({ + format: "openai", + modelId: id, + model: info, + settings: this.options, + }) + + return { id, info, ...params } + } + + /** + * Determines reasoning parameters based on model origin. + * Anthropic models use thinking_budget, OpenAI models use reasoning_effort. + * Other providers (Gemini, etc.) may have different requirements in the future. + */ + private getReasoningParams( + modelId: string, + reasoningBudget: number | undefined, + reasoningEffort: ReasoningEffortExtended | undefined, + ): { thinking_budget?: number; reasoning_effort?: OpenAI.Chat.ChatCompletionCreateParams["reasoning_effort"] } { + const isAnthropicModel = modelId.startsWith("claude-") + const isOpenAiModel = modelId.startsWith("gpt-") + + if (isAnthropicModel && reasoningBudget) { + return { thinking_budget: reasoningBudget } + } + + // OpenAI only supports "low" | "medium" | "high" - filter out unsupported values + if (isOpenAiModel && reasoningEffort) { + if (["low", "medium", "high"].includes(reasoningEffort)) { + return { + reasoning_effort: reasoningEffort as OpenAI.Chat.ChatCompletionCreateParams["reasoning_effort"], + } + } + } + + // Other providers (Gemini, etc.) - no reasoning params for now + return {} + } + + override async *createMessage( + systemPrompt: string, + messages: Anthropic.Messages.MessageParam[], + metadata?: ApiHandlerCreateMessageMetadata, + ): ApiStream { + const { + id: modelId, + info, + maxTokens: max_tokens, + temperature, + reasoningBudget, + reasoningEffort, + } = await this.fetchModel() + + const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [ + { role: "system", content: systemPrompt }, + ...convertToOpenAiMessages(messages), + ] + + const reasoningParams = this.getReasoningParams(modelId, reasoningBudget, reasoningEffort) + + const completionParams: PoeChatCompletionParamsStreaming = { + model: modelId, + messages: openAiMessages, + max_tokens, + temperature, + ...reasoningParams, + stream: true, + stream_options: { include_usage: true }, + ...(metadata?.tools && { tools: this.convertToolsForOpenAI(metadata.tools) }), + ...(metadata?.tool_choice && { tool_choice: metadata.tool_choice }), + } + + let stream: Awaited> & + AsyncIterable + try { + stream = await this.client.chat.completions.create(completionParams) + } catch (error) { + throw handleOpenAIError(error, this.providerName) + } + + let lastUsage: any = undefined + const activeToolCallIds = new Set() + + for await (const chunk of stream) { + verifyFinishReason(chunk.choices[0]) + const delta = chunk.choices[0]?.delta + const finishReason = chunk.choices[0]?.finish_reason + + 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 tool calls in stream - emit partial chunks for NativeToolCallParser + if (delta?.tool_calls) { + for (const toolCall of delta.tool_calls) { + if (toolCall.id) { + activeToolCallIds.add(toolCall.id) + } + yield { + type: "tool_call_partial", + index: toolCall.index, + id: toolCall.id, + name: toolCall.function?.name, + arguments: toolCall.function?.arguments, + } + } + } + + // Emit tool_call_end events when finish_reason is "tool_calls" + if (finishReason === "tool_calls" && activeToolCallIds.size > 0) { + for (const id of activeToolCallIds) { + yield { type: "tool_call_end", id } + } + activeToolCallIds.clear() + } + + if (chunk.usage) { + lastUsage = chunk.usage + } + } + + if (lastUsage) { + yield this.processUsageMetrics(lastUsage, info) + } + } + + async completePrompt(prompt: string): Promise { + const { + id: modelId, + maxTokens: max_tokens, + temperature, + reasoningBudget, + reasoningEffort, + } = await this.fetchModel() + + const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [{ role: "user", content: prompt }] + + const reasoningParams = this.getReasoningParams(modelId, reasoningBudget, reasoningEffort) + + const completionParams: PoeChatCompletionParamsNonStreaming = { + model: modelId, + messages: openAiMessages, + max_tokens, + temperature, + ...reasoningParams, + } + + 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 || "" + } + + protected processUsageMetrics(usage: any, modelInfo?: ModelInfo): ApiStreamUsageChunk { + const inputTokens = usage?.prompt_tokens || 0 + const outputTokens = usage?.completion_tokens || 0 + const cacheWriteTokens = usage?.prompt_tokens_details?.caching_tokens || 0 + const cacheReadTokens = usage?.prompt_tokens_details?.cached_tokens || 0 + + const costResult = modelInfo + ? calculateApiCostOpenAI(modelInfo, inputTokens, outputTokens, cacheWriteTokens, cacheReadTokens) + : { totalCost: 0 } + + return { + type: "usage", + inputTokens, + outputTokens, + cacheWriteTokens: cacheWriteTokens || undefined, + cacheReadTokens: cacheReadTokens || undefined, + totalCost: costResult.totalCost, + } + } +} diff --git a/src/core/webview/__tests__/ClineProvider.spec.ts b/src/core/webview/__tests__/ClineProvider.spec.ts index 16d516675b8..e95fb6aca41 100644 --- a/src/core/webview/__tests__/ClineProvider.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.spec.ts @@ -2806,6 +2806,7 @@ describe("ClineProvider - Router Models", () => { "sap-ai-core": {}, // kilocode_change huggingface: {}, "io-intelligence": {}, + poe: mockModels, // kilocode_change }, values: undefined, }) @@ -2858,6 +2859,7 @@ describe("ClineProvider - Router Models", () => { .mockResolvedValueOnce(mockModels) // kilocode_change: synthetic success .mockResolvedValueOnce(mockModels) // roo success .mockRejectedValueOnce(new Error("Chutes API error")) // chutes fail + .mockResolvedValueOnce(mockModels) // kilocode_change: poe success .mockRejectedValueOnce(new Error("LiteLLM connection failed")) // litellm fail await messageHandler({ type: "requestRouterModels" }) @@ -2886,6 +2888,7 @@ describe("ClineProvider - Router Models", () => { "sap-ai-core": {}, // kilocode_change huggingface: {}, "io-intelligence": {}, + poe: mockModels, // kilocode_change }, values: undefined, }) @@ -3042,6 +3045,7 @@ describe("ClineProvider - Router Models", () => { "sap-ai-core": {}, // kilocode_change huggingface: {}, "io-intelligence": {}, + poe: mockModels, // kilocode_change }, values: undefined, }) diff --git a/src/core/webview/__tests__/webviewMessageHandler.spec.ts b/src/core/webview/__tests__/webviewMessageHandler.spec.ts index 007f39d7d98..33e66371b07 100644 --- a/src/core/webview/__tests__/webviewMessageHandler.spec.ts +++ b/src/core/webview/__tests__/webviewMessageHandler.spec.ts @@ -221,6 +221,7 @@ describe("webviewMessageHandler - requestRouterModels", () => { ovhCloudAiEndpointsApiKey: "ovhcloud-key", inceptionLabsApiKey: "inception-key", inceptionLabsBaseUrl: "https://api.inceptionlabs.ai/v1/", + poeApiKey: "poe-key", // kilocode_change end }, }) @@ -270,6 +271,10 @@ describe("webviewMessageHandler - requestRouterModels", () => { apiKey: "nano-gpt-key", nanoGptModelList: undefined, }) + expect(mockGetModels).toHaveBeenCalledWith({ + provider: "poe", + apiKey: "poe-key", + }) // kilocode_change end expect(mockGetModels).toHaveBeenCalledWith({ provider: "vercel-ai-gateway" }) expect(mockGetModels).toHaveBeenCalledWith({ provider: "deepinfra" }) @@ -303,6 +308,7 @@ describe("webviewMessageHandler - requestRouterModels", () => { "nano-gpt": mockModels, // kilocode_change roo: mockModels, chutes: mockModels, + poe: mockModels, // kilocode_change ollama: mockModels, // kilocode_change lmstudio: {}, "vercel-ai-gateway": mockModels, @@ -365,6 +371,7 @@ describe("webviewMessageHandler - requestRouterModels", () => { ovhCloudAiEndpointsApiKey: "ovhcloud-key", chutesApiKey: "chutes-key", nanoGptApiKey: "nano-gpt-key", + poeApiKey: "poe-key", // kilocode_change end // Missing litellm config }, @@ -406,6 +413,7 @@ describe("webviewMessageHandler - requestRouterModels", () => { unbound: mockModels, roo: mockModels, chutes: mockModels, + poe: mockModels, // kilocode_change litellm: {}, kilocode: mockModels, "nano-gpt": mockModels, // kilocode_change @@ -449,6 +457,7 @@ describe("webviewMessageHandler - requestRouterModels", () => { .mockRejectedValueOnce(new Error("Synthetic API error")) // kilocode_change .mockResolvedValueOnce(mockModels) // roo .mockRejectedValueOnce(new Error("Chutes API error")) // chutes + .mockResolvedValueOnce(mockModels) // poe // kilocode_change .mockRejectedValueOnce(new Error("LiteLLM connection failed")) // litellm await webviewMessageHandler(mockClineProvider, { @@ -511,6 +520,7 @@ describe("webviewMessageHandler - requestRouterModels", () => { unbound: {}, roo: mockModels, chutes: {}, + poe: mockModels, // kilocode_change litellm: {}, ollama: {}, lmstudio: {}, @@ -549,6 +559,7 @@ describe("webviewMessageHandler - requestRouterModels", () => { .mockRejectedValueOnce(new Error("Synthetic API error")) // kilocode_change synthetic .mockRejectedValueOnce(new Error("Roo API error")) // roo .mockRejectedValueOnce(new Error("Chutes API error")) // chutes + .mockRejectedValueOnce(new Error("Poe API error")) // poe // kilocode_change .mockRejectedValueOnce(new Error("LiteLLM connection failed")) // litellm await webviewMessageHandler(mockClineProvider, { @@ -655,6 +666,13 @@ describe("webviewMessageHandler - requestRouterModels", () => { values: { provider: "chutes" }, }) + expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ + type: "singleRouterModelFetchResponse", + success: false, + error: "Poe API error", + values: { provider: "poe" }, + }) + expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ type: "singleRouterModelFetchResponse", success: false, diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 47d61ce2cf2..25ccc216809 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -876,6 +876,7 @@ export const webviewMessageHandler = async ( "sap-ai-core": {}, // kilocode_change chutes: {}, "nano-gpt": {}, // kilocode_change + poe: {}, // kilocode_change } const safeGetModels = async (options: GetModelsOptions): Promise => { @@ -978,6 +979,10 @@ export const webviewMessageHandler = async ( key: "chutes", options: { provider: "chutes", apiKey: apiConfiguration.chutesApiKey }, }, + { + key: "poe", + options: { provider: "poe", apiKey: apiConfiguration.poeApiKey }, + }, ] // kilocode_change end diff --git a/src/shared/api.ts b/src/shared/api.ts index 1a5a034e314..7ceb04da5bf 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -198,6 +198,7 @@ const dynamicProviderExtras = { synthetic: {} as { apiKey?: string }, // kilocode_change roo: {} as { apiKey?: string; baseUrl?: string }, chutes: {} as { apiKey?: string }, + poe: {} as { apiKey?: string }, // kilocode_change // kilocode_change start "sap-ai-core": {} as { sapAiCoreServiceKey?: string diff --git a/webview-ui/src/components/kilocode/hooks/__tests__/getModelsByProvider.spec.ts b/webview-ui/src/components/kilocode/hooks/__tests__/getModelsByProvider.spec.ts index d1fe30c8723..50e39271034 100644 --- a/webview-ui/src/components/kilocode/hooks/__tests__/getModelsByProvider.spec.ts +++ b/webview-ui/src/components/kilocode/hooks/__tests__/getModelsByProvider.spec.ts @@ -42,6 +42,7 @@ describe("getModelsByProvider", () => { synthetic: { "test-model": testModel }, inception: { "test-model": testModel }, roo: { "test-model": testModel }, + poe: { "test-model": testModel }, } it("returns models for all providers", () => { diff --git a/webview-ui/src/components/kilocode/hooks/useProviderModels.ts b/webview-ui/src/components/kilocode/hooks/useProviderModels.ts index 9a8c095cfca..c4f513c0b4a 100644 --- a/webview-ui/src/components/kilocode/hooks/useProviderModels.ts +++ b/webview-ui/src/components/kilocode/hooks/useProviderModels.ts @@ -52,6 +52,7 @@ import { cerebrasModels, cerebrasDefaultModelId, nanoGptDefaultModelId, //kilocode_change + poeDefaultModelId, //kilocode_change ovhCloudAiEndpointsDefaultModelId, inceptionDefaultModelId, minimaxModels, @@ -251,6 +252,12 @@ export const getModelsByProvider = ({ } } // kilocode_change start + case "poe": { + return { + models: routerModels.poe, + defaultModel: poeDefaultModelId, + } + } case "synthetic": { return { models: routerModels.synthetic, diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index 65386e07323..5ca3a67b308 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -48,6 +48,7 @@ import { deepInfraDefaultModelId, minimaxDefaultModelId, nanoGptDefaultModelId, //kilocode_change + poeDefaultModelId, } from "@roo-code/types" import { vscode } from "@src/utils/vscode" @@ -121,6 +122,7 @@ import { VercelAiGateway, DeepInfra, MiniMax, + Poe, } from "./providers" import { MODELS_BY_PROVIDER, PROVIDERS } from "./constants" @@ -291,6 +293,7 @@ const ApiOptions = ({ selectedProvider === "deepinfra" || selectedProvider === "chutes" || // kilocode_change selectedProvider === "synthetic" || // kilocode_change + selectedProvider === "poe" || selectedProvider === "roo" ) { vscode.postMessage({ type: "requestRouterModels" }) @@ -456,6 +459,7 @@ const ApiOptions = ({ synthetic: { field: "apiModelId", default: syntheticDefaultModelId }, ovhcloud: { field: "ovhCloudAiEndpointsModelId", default: ovhCloudAiEndpointsDefaultModelId }, inception: { field: "inceptionLabsModelId", default: inceptionDefaultModelId }, + poe: { field: "poeModelId", default: poeDefaultModelId }, // kilocode_change end } @@ -671,6 +675,17 @@ const ApiOptions = ({ )} {/* kilocode_change end */} + {selectedProvider === "poe" && ( + + )} + {selectedProvider === "anthropic" && ( a.label.localeCompare(b.label)) PROVIDERS.unshift({ value: "kilocode", label: "Kilo Gateway", proxy: false }) // kilocode_change diff --git a/webview-ui/src/components/settings/providers/Poe.tsx b/webview-ui/src/components/settings/providers/Poe.tsx new file mode 100644 index 00000000000..f7270a46bc3 --- /dev/null +++ b/webview-ui/src/components/settings/providers/Poe.tsx @@ -0,0 +1,99 @@ +// kilocode_change - file added +import { useCallback, useState } from "react" +import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react" + +import { OrganizationAllowList, type ProviderSettings, poeDefaultModelId } from "@roo-code/types" + +import type { RouterModels } from "@roo/api" + +import { vscode } from "@src/utils/vscode" +import { useAppTranslation } from "@src/i18n/TranslationContext" +import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink" +import { Button } from "@src/components/ui" + +import { inputEventTransform } from "../transforms" +import { ModelPicker } from "../ModelPicker" + +type PoeProps = { + apiConfiguration: ProviderSettings + setApiConfigurationField: (field: keyof ProviderSettings, value: ProviderSettings[keyof ProviderSettings]) => void + routerModels?: RouterModels + refetchRouterModels: () => void + organizationAllowList: OrganizationAllowList + modelValidationError?: string +} + +export const Poe = ({ + apiConfiguration, + setApiConfigurationField, + routerModels, + refetchRouterModels, + organizationAllowList, + modelValidationError, +}: PoeProps) => { + const { t } = useAppTranslation() + + const [didRefetch, setDidRefetch] = useState() + + 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?.poeApiKey && ( + + {t("settings:providers.getPoeApiKey")} + + )} + + + {didRefetch && ( +
+ {t("settings:providers.refreshModels.hint")} +
+ )} + + + + ) +} diff --git a/webview-ui/src/components/settings/providers/index.ts b/webview-ui/src/components/settings/providers/index.ts index e7407ff5ed2..b070726067a 100644 --- a/webview-ui/src/components/settings/providers/index.ts +++ b/webview-ui/src/components/settings/providers/index.ts @@ -43,3 +43,4 @@ export { VercelAiGateway } from "./VercelAiGateway" export { DeepInfra } from "./DeepInfra" export { MiniMax } from "./MiniMax" export { Baseten } from "./Baseten" +export { Poe } from "./Poe" diff --git a/webview-ui/src/components/ui/hooks/useSelectedModel.ts b/webview-ui/src/components/ui/hooks/useSelectedModel.ts index ed3f08b7b0c..cca9de3dcf5 100644 --- a/webview-ui/src/components/ui/hooks/useSelectedModel.ts +++ b/webview-ui/src/components/ui/hooks/useSelectedModel.ts @@ -394,6 +394,13 @@ function getSelectedModel({ const info = routerModels.deepinfra?.[id] return { id, info } } + // kilocode_change start + case "poe": { + const id = getValidatedModelId(apiConfiguration.poeModelId, routerModels.poe, defaultModelId) + const info = routerModels.poe?.[id] + return { id, info } + } + // kilocode_change end case "vscode-lm": { const id = apiConfiguration?.vsCodeLmModelSelector ? `${apiConfiguration.vsCodeLmModelSelector.vendor}/${apiConfiguration.vsCodeLmModelSelector.family}` diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index 9e487e9b312..f8761b8f441 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -394,6 +394,8 @@ }, "getSambaNovaApiKey": "Get SambaNova API Key", "sambaNovaApiKey": "SambaNova API Key", + "poeApiKey": "Poe API Key", + "getPoeApiKey": "Get Poe API Key", "getHuggingFaceApiKey": "Get Hugging Face API Key", "huggingFaceApiKey": "Hugging Face API Key", "getOvhCloudAiEndpointsApiKey": "Get OVHcloud AI Endpoints API Key", diff --git a/webview-ui/src/utils/__tests__/validate.spec.ts b/webview-ui/src/utils/__tests__/validate.spec.ts index 05a2aa371fe..4f3a7fe1924 100644 --- a/webview-ui/src/utils/__tests__/validate.spec.ts +++ b/webview-ui/src/utils/__tests__/validate.spec.ts @@ -89,6 +89,7 @@ describe("Model Validation Functions", () => { // kilocode_change end roo: {}, chutes: {}, + poe: {}, } const allowAllOrganization: OrganizationAllowList = { From 1473f203b155556d629df611808a1515fbaa41fd Mon Sep 17 00:00:00 2001 From: Kevin van Dijk Date: Sat, 21 Feb 2026 19:17:41 +0100 Subject: [PATCH 2/4] Add missing translations --- webview-ui/src/i18n/locales/ar/settings.json | 2 ++ webview-ui/src/i18n/locales/ca/settings.json | 2 ++ webview-ui/src/i18n/locales/cs/settings.json | 2 ++ webview-ui/src/i18n/locales/de/settings.json | 2 ++ webview-ui/src/i18n/locales/es/settings.json | 2 ++ webview-ui/src/i18n/locales/fr/settings.json | 2 ++ webview-ui/src/i18n/locales/hi/settings.json | 2 ++ webview-ui/src/i18n/locales/id/settings.json | 2 ++ webview-ui/src/i18n/locales/it/settings.json | 2 ++ webview-ui/src/i18n/locales/ja/settings.json | 2 ++ webview-ui/src/i18n/locales/ko/settings.json | 2 ++ webview-ui/src/i18n/locales/nl/settings.json | 2 ++ webview-ui/src/i18n/locales/pl/settings.json | 2 ++ webview-ui/src/i18n/locales/pt-BR/settings.json | 2 ++ webview-ui/src/i18n/locales/ru/settings.json | 2 ++ webview-ui/src/i18n/locales/sk/settings.json | 2 ++ webview-ui/src/i18n/locales/th/settings.json | 2 ++ webview-ui/src/i18n/locales/tr/settings.json | 2 ++ webview-ui/src/i18n/locales/uk/settings.json | 2 ++ webview-ui/src/i18n/locales/vi/settings.json | 2 ++ webview-ui/src/i18n/locales/zh-CN/settings.json | 2 ++ webview-ui/src/i18n/locales/zh-TW/settings.json | 2 ++ 22 files changed, 44 insertions(+) diff --git a/webview-ui/src/i18n/locales/ar/settings.json b/webview-ui/src/i18n/locales/ar/settings.json index dab1fefb824..7922201cffb 100644 --- a/webview-ui/src/i18n/locales/ar/settings.json +++ b/webview-ui/src/i18n/locales/ar/settings.json @@ -465,6 +465,8 @@ "groqApiKey": "مفتاح Groq", "getSambaNovaApiKey": "احصل على مفتاح SambaNova", "sambaNovaApiKey": "مفتاح SambaNova", + "poeApiKey": "مفتاح Poe", + "getPoeApiKey": "احصل على مفتاح Poe", "getGeminiApiKey": "احصل على مفتاح Gemini", "openAiApiKey": "مفتاح OpenAI", "getHuggingFaceApiKey": "احصل على مفتاح Hugging Face API", diff --git a/webview-ui/src/i18n/locales/ca/settings.json b/webview-ui/src/i18n/locales/ca/settings.json index 9e79b85d902..be782e5c02d 100644 --- a/webview-ui/src/i18n/locales/ca/settings.json +++ b/webview-ui/src/i18n/locales/ca/settings.json @@ -427,6 +427,8 @@ }, "getSambaNovaApiKey": "Obtenir clau API de SambaNova", "sambaNovaApiKey": "Clau API de SambaNova", + "poeApiKey": "Clau API de Poe", + "getPoeApiKey": "Obtenir clau API de Poe", "getHuggingFaceApiKey": "Obtenir clau API de Hugging Face", "huggingFaceApiKey": "Clau API de Hugging Face", "getOvhCloudAiEndpointsApiKey": "Obtenir clau API de OVHcloud AI Endpoints", diff --git a/webview-ui/src/i18n/locales/cs/settings.json b/webview-ui/src/i18n/locales/cs/settings.json index 44251f2161a..1919c02f736 100644 --- a/webview-ui/src/i18n/locales/cs/settings.json +++ b/webview-ui/src/i18n/locales/cs/settings.json @@ -450,6 +450,8 @@ "groqApiKey": "Klíč API Groq", "getSambaNovaApiKey": "Získat klíč API SambaNova", "sambaNovaApiKey": "Klíč API SambaNova", + "poeApiKey": "Klíč API Poe", + "getPoeApiKey": "Získat klíč API Poe", "getGeminiApiKey": "Získat klíč API Gemini", "openAiApiKey": "Klíč API OpenAI", "getHuggingFaceApiKey": "Získat klíč API Hugging Face", diff --git a/webview-ui/src/i18n/locales/de/settings.json b/webview-ui/src/i18n/locales/de/settings.json index 3b38a5f97e3..8be9844c0db 100644 --- a/webview-ui/src/i18n/locales/de/settings.json +++ b/webview-ui/src/i18n/locales/de/settings.json @@ -428,6 +428,8 @@ "groqApiKey": "Groq API-Schlüssel", "getSambaNovaApiKey": "SambaNova API-Schlüssel erhalten", "sambaNovaApiKey": "SambaNova API-Schlüssel", + "poeApiKey": "Poe API-Schlüssel", + "getPoeApiKey": "Poe API-Schlüssel erhalten", "getHuggingFaceApiKey": "Hugging Face API-Schlüssel erhalten", "huggingFaceApiKey": "Hugging Face API-Schlüssel", "getOvhCloudAiEndpointsApiKey": "OVHcloud AI Endpoints API-Schlüssel erhalten", diff --git a/webview-ui/src/i18n/locales/es/settings.json b/webview-ui/src/i18n/locales/es/settings.json index 077f44f6c96..999d6a24187 100644 --- a/webview-ui/src/i18n/locales/es/settings.json +++ b/webview-ui/src/i18n/locales/es/settings.json @@ -428,6 +428,8 @@ "groqApiKey": "Clave API de Groq", "getSambaNovaApiKey": "Obtener clave API de SambaNova", "sambaNovaApiKey": "Clave API de SambaNova", + "poeApiKey": "Clave API de Poe", + "getPoeApiKey": "Obtener clave API de Poe", "getHuggingFaceApiKey": "Obtener clave API de Hugging Face", "huggingFaceApiKey": "Clave API de Hugging Face", "getOvhCloudAiEndpointsApiKey": "Obtener clave API de OVHcloud AI Endpoints", diff --git a/webview-ui/src/i18n/locales/fr/settings.json b/webview-ui/src/i18n/locales/fr/settings.json index efad3998197..4e9eda4a27b 100644 --- a/webview-ui/src/i18n/locales/fr/settings.json +++ b/webview-ui/src/i18n/locales/fr/settings.json @@ -428,6 +428,8 @@ "groqApiKey": "Clé API Groq", "getSambaNovaApiKey": "Obtenir la clé API SambaNova", "sambaNovaApiKey": "Clé API SambaNova", + "poeApiKey": "Clé API Poe", + "getPoeApiKey": "Obtenir la clé API Poe", "getHuggingFaceApiKey": "Obtenir la clé API Hugging Face", "huggingFaceApiKey": "Clé API Hugging Face", "getOvhCloudAiEndpointsApiKey": "Obtenir la clé API d'OVHcloud AI Endpoints", diff --git a/webview-ui/src/i18n/locales/hi/settings.json b/webview-ui/src/i18n/locales/hi/settings.json index 0596ef8e8e3..7f98dd26c97 100644 --- a/webview-ui/src/i18n/locales/hi/settings.json +++ b/webview-ui/src/i18n/locales/hi/settings.json @@ -427,6 +427,8 @@ "groqApiKey": "Groq API कुंजी", "getSambaNovaApiKey": "SambaNova API कुंजी प्राप्त करें", "sambaNovaApiKey": "SambaNova API कुंजी", + "poeApiKey": "Poe API कुंजी", + "getPoeApiKey": "Poe API कुंजी प्राप्त करें", "getHuggingFaceApiKey": "Hugging Face API कुंजी प्राप्त करें", "huggingFaceApiKey": "Hugging Face API कुंजी", "getOvhCloudAiEndpointsApiKey": "OVHcloud AI Endpoints API कुंजी प्राप्त करें", diff --git a/webview-ui/src/i18n/locales/id/settings.json b/webview-ui/src/i18n/locales/id/settings.json index d337cb60a21..a50dec196c1 100644 --- a/webview-ui/src/i18n/locales/id/settings.json +++ b/webview-ui/src/i18n/locales/id/settings.json @@ -427,6 +427,8 @@ "groqApiKey": "Groq API Key", "getSambaNovaApiKey": "Dapatkan SambaNova API Key", "sambaNovaApiKey": "SambaNova API Key", + "poeApiKey": "Poe API Key", + "getPoeApiKey": "Dapatkan Poe API Key", "getHuggingFaceApiKey": "Dapatkan Kunci API Hugging Face", "huggingFaceApiKey": "Kunci API Hugging Face", "getOvhCloudAiEndpointsApiKey": "Dapatkan Kunci API OVHcloud AI Endpoints", diff --git a/webview-ui/src/i18n/locales/it/settings.json b/webview-ui/src/i18n/locales/it/settings.json index 623533692d1..132e713c199 100644 --- a/webview-ui/src/i18n/locales/it/settings.json +++ b/webview-ui/src/i18n/locales/it/settings.json @@ -441,6 +441,8 @@ }, "getSambaNovaApiKey": "Ottieni chiave API SambaNova", "sambaNovaApiKey": "Chiave API SambaNova", + "poeApiKey": "Chiave API Poe", + "getPoeApiKey": "Ottieni chiave API Poe", "getHuggingFaceApiKey": "Ottieni chiave API Hugging Face", "huggingFaceApiKey": "Chiave API Hugging Face", "getOvhCloudAiEndpointsApiKey": "Ottieni chiave API OVHcloud AI Endpoints", diff --git a/webview-ui/src/i18n/locales/ja/settings.json b/webview-ui/src/i18n/locales/ja/settings.json index bf394b314e3..cc3cf495a99 100644 --- a/webview-ui/src/i18n/locales/ja/settings.json +++ b/webview-ui/src/i18n/locales/ja/settings.json @@ -424,6 +424,8 @@ "groqApiKey": "Groq APIキー", "getSambaNovaApiKey": "SambaNova APIキーを取得", "sambaNovaApiKey": "SambaNova APIキー", + "poeApiKey": "Poe APIキー", + "getPoeApiKey": "Poe APIキーを取得", "getHuggingFaceApiKey": "Hugging Face APIキーを取得", "huggingFaceApiKey": "Hugging Face APIキー", "getOvhCloudAiEndpointsApiKey": "OVHcloud AI Endpoints APIキーを取得", diff --git a/webview-ui/src/i18n/locales/ko/settings.json b/webview-ui/src/i18n/locales/ko/settings.json index 078476e7c55..01bb03a9c34 100644 --- a/webview-ui/src/i18n/locales/ko/settings.json +++ b/webview-ui/src/i18n/locales/ko/settings.json @@ -427,6 +427,8 @@ }, "getSambaNovaApiKey": "SambaNova API 키 받기", "sambaNovaApiKey": "SambaNova API 키", + "poeApiKey": "Poe API 키", + "getPoeApiKey": "Poe API 키 받기", "getGeminiApiKey": "Gemini API 키 받기", "getHuggingFaceApiKey": "Hugging Face API 키 받기", "huggingFaceApiKey": "Hugging Face API 키", diff --git a/webview-ui/src/i18n/locales/nl/settings.json b/webview-ui/src/i18n/locales/nl/settings.json index 9fdb02e78fe..8f8bc20b537 100644 --- a/webview-ui/src/i18n/locales/nl/settings.json +++ b/webview-ui/src/i18n/locales/nl/settings.json @@ -427,6 +427,8 @@ "groqApiKey": "Groq API-sleutel", "getSambaNovaApiKey": "SambaNova API-sleutel ophalen", "sambaNovaApiKey": "SambaNova API-sleutel", + "poeApiKey": "Poe API-sleutel", + "getPoeApiKey": "Poe API-sleutel ophalen", "getGeminiApiKey": "Gemini API-sleutel ophalen", "getHuggingFaceApiKey": "Hugging Face API-sleutel ophalen", "huggingFaceApiKey": "Hugging Face API-sleutel", diff --git a/webview-ui/src/i18n/locales/pl/settings.json b/webview-ui/src/i18n/locales/pl/settings.json index 5e3308dda3c..0ae16b5be57 100644 --- a/webview-ui/src/i18n/locales/pl/settings.json +++ b/webview-ui/src/i18n/locales/pl/settings.json @@ -419,6 +419,8 @@ "groqApiKey": "Klucz API Groq", "getSambaNovaApiKey": "Uzyskaj klucz API SambaNova", "sambaNovaApiKey": "Klucz API SambaNova", + "poeApiKey": "Klucz API Poe", + "getPoeApiKey": "Uzyskaj klucz API Poe", "getGeminiApiKey": "Uzyskaj klucz API Gemini", "getHuggingFaceApiKey": "Uzyskaj klucz API Hugging Face", "huggingFaceApiKey": "Klucz API Hugging Face", diff --git a/webview-ui/src/i18n/locales/pt-BR/settings.json b/webview-ui/src/i18n/locales/pt-BR/settings.json index 2fc2d8fdc84..190c94e166e 100644 --- a/webview-ui/src/i18n/locales/pt-BR/settings.json +++ b/webview-ui/src/i18n/locales/pt-BR/settings.json @@ -427,6 +427,8 @@ "groqApiKey": "Chave de API Groq", "getSambaNovaApiKey": "Obter chave de API SambaNova", "sambaNovaApiKey": "Chave de API SambaNova", + "poeApiKey": "Chave de API Poe", + "getPoeApiKey": "Obter chave de API Poe", "getGeminiApiKey": "Obter chave de API Gemini", "getHuggingFaceApiKey": "Obter chave de API Hugging Face", "huggingFaceApiKey": "Chave de API Hugging Face", diff --git a/webview-ui/src/i18n/locales/ru/settings.json b/webview-ui/src/i18n/locales/ru/settings.json index 0b374569ded..dc7bff9b9c8 100644 --- a/webview-ui/src/i18n/locales/ru/settings.json +++ b/webview-ui/src/i18n/locales/ru/settings.json @@ -419,6 +419,8 @@ "groqApiKey": "Groq API-ключ", "getSambaNovaApiKey": "Получить SambaNova API-ключ", "sambaNovaApiKey": "SambaNova API-ключ", + "poeApiKey": "Poe API-ключ", + "getPoeApiKey": "Получить Poe API-ключ", "getGeminiApiKey": "Получить Gemini API-ключ", "getHuggingFaceApiKey": "Получить Hugging Face API-ключ", "huggingFaceApiKey": "Hugging Face API-ключ", diff --git a/webview-ui/src/i18n/locales/sk/settings.json b/webview-ui/src/i18n/locales/sk/settings.json index d1469ae7586..90d780a2559 100644 --- a/webview-ui/src/i18n/locales/sk/settings.json +++ b/webview-ui/src/i18n/locales/sk/settings.json @@ -459,6 +459,8 @@ "groqApiKey": "Kľúč API Groq", "getSambaNovaApiKey": "Získať kľúč API SambaNova", "sambaNovaApiKey": "Kľúč API SambaNova", + "poeApiKey": "Kľúč API Poe", + "getPoeApiKey": "Získať kľúč API Poe", "getGeminiApiKey": "Získať kľúč API Gemini", "openAiApiKey": "Kľúč API OpenAI", "getHuggingFaceApiKey": "Získať kľúč API Hugging Face", diff --git a/webview-ui/src/i18n/locales/th/settings.json b/webview-ui/src/i18n/locales/th/settings.json index 73a2c0d5e57..6a514921458 100644 --- a/webview-ui/src/i18n/locales/th/settings.json +++ b/webview-ui/src/i18n/locales/th/settings.json @@ -578,6 +578,8 @@ "getZaiApiKey": "รับคีย์ API ของ Z.AI", "sambaNovaApiKey": "คีย์ API ของ SambaNova", "getSambaNovaApiKey": "รับคีย์ API ของ SambaNova", + "poeApiKey": "คีย์ API ของ Poe", + "getPoeApiKey": "รับคีย์ API ของ Poe", "customModel": { "capabilities": "กำหนดค่าความสามารถและราคาสำหรับโมเดลที่เข้ากันได้กับ OpenAI ที่กำหนดเองของคุณ ระมัดระวังในการระบุความสามารถของโมเดล เนื่องจากอาจส่งผลต่อประสิทธิภาพของ Kilo Code", "maxTokens": { diff --git a/webview-ui/src/i18n/locales/tr/settings.json b/webview-ui/src/i18n/locales/tr/settings.json index 79f1271b053..c7f695eadf3 100644 --- a/webview-ui/src/i18n/locales/tr/settings.json +++ b/webview-ui/src/i18n/locales/tr/settings.json @@ -419,6 +419,8 @@ "groqApiKey": "Groq API Anahtarı", "getSambaNovaApiKey": "SambaNova API Anahtarı Al", "sambaNovaApiKey": "SambaNova API Anahtarı", + "poeApiKey": "Poe API Anahtarı", + "getPoeApiKey": "Poe API Anahtarı Al", "getHuggingFaceApiKey": "Hugging Face API Anahtarı Al", "huggingFaceApiKey": "Hugging Face API Anahtarı", "getOvhCloudAiEndpointsApiKey": "OVHcloud AI Endpoints API Anahtarı Al", diff --git a/webview-ui/src/i18n/locales/uk/settings.json b/webview-ui/src/i18n/locales/uk/settings.json index 66a2150b85b..24602abe3f9 100644 --- a/webview-ui/src/i18n/locales/uk/settings.json +++ b/webview-ui/src/i18n/locales/uk/settings.json @@ -608,6 +608,8 @@ "getZaiApiKey": "Отримати ключ API Z.AI", "sambaNovaApiKey": "Ключ API SambaNova", "getSambaNovaApiKey": "Отримати ключ API SambaNova", + "poeApiKey": "Ключ API Poe", + "getPoeApiKey": "Отримати ключ API Poe", "nanoGptApiKey": "Ключ API Nano-GPT", "getNanoGptApiKey": "Отримати ключ API Nano-GPT", "nanoGptModelList": "Список моделей", diff --git a/webview-ui/src/i18n/locales/vi/settings.json b/webview-ui/src/i18n/locales/vi/settings.json index 2500e617320..b5820f76477 100644 --- a/webview-ui/src/i18n/locales/vi/settings.json +++ b/webview-ui/src/i18n/locales/vi/settings.json @@ -427,6 +427,8 @@ "groqApiKey": "Khóa API Groq", "getSambaNovaApiKey": "Lấy khóa API SambaNova", "sambaNovaApiKey": "Khóa API SambaNova", + "poeApiKey": "Khóa API Poe", + "getPoeApiKey": "Lấy khóa API Poe", "getHuggingFaceApiKey": "Lấy Khóa API Hugging Face", "huggingFaceApiKey": "Khóa API Hugging Face", "getOvhCloudAiEndpointsApiKey": "Lấy Khóa API OVHcloud AI Endpoints", diff --git a/webview-ui/src/i18n/locales/zh-CN/settings.json b/webview-ui/src/i18n/locales/zh-CN/settings.json index 2a32cd52764..d4cdf374340 100644 --- a/webview-ui/src/i18n/locales/zh-CN/settings.json +++ b/webview-ui/src/i18n/locales/zh-CN/settings.json @@ -427,6 +427,8 @@ "groqApiKey": "Groq API 密钥", "getSambaNovaApiKey": "获取 SambaNova API 密钥", "sambaNovaApiKey": "SambaNova API 密钥", + "poeApiKey": "Poe API 密钥", + "getPoeApiKey": "获取 Poe API 密钥", "getHuggingFaceApiKey": "获取 Hugging Face API 密钥", "huggingFaceApiKey": "Hugging Face API 密钥", "getOvhCloudAiEndpointsApiKey": "获取 OVHcloud AI Endpoints API 密钥", diff --git a/webview-ui/src/i18n/locales/zh-TW/settings.json b/webview-ui/src/i18n/locales/zh-TW/settings.json index 1c85bac4438..f0e78aaf064 100644 --- a/webview-ui/src/i18n/locales/zh-TW/settings.json +++ b/webview-ui/src/i18n/locales/zh-TW/settings.json @@ -429,6 +429,8 @@ "groqApiKey": "Groq API 金鑰", "getSambaNovaApiKey": "取得 SambaNova API 金鑰", "sambaNovaApiKey": "SambaNova API 金鑰", + "poeApiKey": "Poe API 金鑰", + "getPoeApiKey": "取得 Poe API 金鑰", "getHuggingFaceApiKey": "取得 Hugging Face API 金鑰", "huggingFaceApiKey": "Hugging Face API 金鑰", "getOvhCloudAiEndpointsApiKey": "取得 OVHcloud AI Endpoints API 金鑰", From be8b382d730b5359641543449468bddd419563a9 Mon Sep 17 00:00:00 2001 From: Kevin van Dijk Date: Sat, 21 Feb 2026 19:29:13 +0100 Subject: [PATCH 3/4] Remove files that are not in main anymore --- cli/src/config/mapper.ts | 202 ---- cli/src/constants/providers/labels.ts | 72 -- cli/src/constants/providers/models.ts | 641 ------------ cli/src/constants/providers/settings.ts | 1129 --------------------- cli/src/constants/providers/validation.ts | 56 - 5 files changed, 2100 deletions(-) delete mode 100644 cli/src/config/mapper.ts delete mode 100644 cli/src/constants/providers/labels.ts delete mode 100644 cli/src/constants/providers/models.ts delete mode 100644 cli/src/constants/providers/settings.ts delete mode 100644 cli/src/constants/providers/validation.ts diff --git a/cli/src/config/mapper.ts b/cli/src/config/mapper.ts deleted file mode 100644 index 511285ae254..00000000000 --- a/cli/src/config/mapper.ts +++ /dev/null @@ -1,202 +0,0 @@ -import type { CLIConfig, ProviderConfig } from "./types.js" -import type { ExtensionState, ProviderSettings, ProviderSettingsEntry } from "../types/messages.js" -import { logs } from "../services/logs.js" -import { DEFAULT_MAX_CONCURRENT_FILE_READS } from "@kilocode/core-schemas" - -export function mapConfigToExtensionState( - config: CLIConfig, - currentState?: Partial, -): Partial { - try { - // Find selected provider - let provider = config.providers.find((p) => p.id === config.provider) - - if (!provider) { - logs.warn("Selected provider not found, using first provider", "ConfigMapper") - provider = config.providers[0] - if (!provider) { - throw new Error("No providers configured") - } - } - - // Map provider config to API configuration - const apiConfiguration = mapProviderToApiConfig(provider) - - // Create list of provider metadata - const listApiConfigMeta: ProviderSettingsEntry[] = config.providers.map((p) => ({ - id: p.id, - name: p.id, - apiProvider: p.provider, - modelId: getModelIdForProvider(p), - })) - - // Map auto-approval settings from CLI config to extension state - // These settings control whether the extension auto-approves operations - // or asks the CLI for approval (which then prompts the user) - const autoApproval = config.autoApproval - const autoApprovalEnabled = autoApproval?.enabled ?? false - - return { - ...currentState, - apiConfiguration, - currentApiConfigName: provider.id, - listApiConfigMeta, - telemetrySetting: config.telemetry ? "enabled" : "disabled", - mode: config.mode, - // Auto-approval settings - these control whether the extension auto-approves - // or defers to the CLI's approval flow - autoApprovalEnabled, - alwaysAllowReadOnly: autoApprovalEnabled && (autoApproval?.read?.enabled ?? false), - alwaysAllowReadOnlyOutsideWorkspace: - autoApprovalEnabled && (autoApproval?.read?.enabled ?? false) && (autoApproval?.read?.outside ?? false), - alwaysAllowWrite: autoApprovalEnabled && (autoApproval?.write?.enabled ?? false), - alwaysAllowWriteOutsideWorkspace: - autoApprovalEnabled && - (autoApproval?.write?.enabled ?? false) && - (autoApproval?.write?.outside ?? false), - alwaysAllowWriteProtected: - autoApprovalEnabled && - (autoApproval?.write?.enabled ?? false) && - (autoApproval?.write?.protected ?? false), - alwaysAllowBrowser: autoApprovalEnabled && (autoApproval?.browser?.enabled ?? false), - alwaysApproveResubmit: autoApprovalEnabled && (autoApproval?.retry?.enabled ?? false), - requestDelaySeconds: autoApproval?.retry?.delay ?? 10, - alwaysAllowMcp: autoApprovalEnabled && (autoApproval?.mcp?.enabled ?? false), - alwaysAllowModeSwitch: autoApprovalEnabled && (autoApproval?.mode?.enabled ?? false), - alwaysAllowSubtasks: autoApprovalEnabled && (autoApproval?.subtasks?.enabled ?? false), - alwaysAllowExecute: autoApprovalEnabled && (autoApproval?.execute?.enabled ?? false), - allowedCommands: autoApproval?.execute?.allowed ?? [], - deniedCommands: autoApproval?.execute?.denied ?? [], - alwaysAllowFollowupQuestions: autoApprovalEnabled && (autoApproval?.question?.enabled ?? false), - followupAutoApproveTimeoutMs: (autoApproval?.question?.timeout ?? 60) * 1000, - alwaysAllowUpdateTodoList: autoApprovalEnabled && (autoApproval?.todo?.enabled ?? false), - // Context management settings - maxConcurrentFileReads: config.maxConcurrentFileReads ?? DEFAULT_MAX_CONCURRENT_FILE_READS, - } - } catch (error) { - logs.error("Failed to map config to extension state", "ConfigMapper", { error }) - throw error - } -} - -export function mapProviderToApiConfig(provider: ProviderConfig): ProviderSettings { - const config: ProviderSettings = { - apiProvider: provider.provider, - } - - // Copy all provider-specific fields - Object.keys(provider).forEach((key) => { - if (key !== "id" && key !== "provider") { - // Type assertion needed because we're dynamically accessing keys - ;(config as Record)[key] = (provider as Record)[key] - } - }) - - return config -} - -export function getModelIdForProvider(provider: ProviderConfig): string { - switch (provider.provider) { - case "kilocode": - return provider.kilocodeModel || "" - case "anthropic": - return provider.apiModelId || "" - case "openai-native": - return provider.apiModelId || "" - case "openrouter": - return provider.openRouterModelId || "" - case "ollama": - return provider.ollamaModelId || "" - case "lmstudio": - return provider.lmStudioModelId || "" - case "openai": - return provider.openAiModelId || "" - case "glama": - return provider.glamaModelId || "" - case "litellm": - return provider.litellmModelId || "" - case "deepinfra": - return provider.deepInfraModelId || "" - case "unbound": - return provider.unboundModelId || "" - case "requesty": - return provider.requestyModelId || "" - case "vercel-ai-gateway": - return provider.vercelAiGatewayModelId || "" - case "io-intelligence": - return provider.ioIntelligenceModelId || "" - case "ovhcloud": - return provider.ovhCloudAiEndpointsModelId || "" - case "poe": - return provider.poeModelId || "" - case "inception": - return provider.inceptionLabsModelId || "" - case "bedrock": - case "vertex": - case "gemini": - case "gemini-cli": - case "mistral": - case "moonshot": - case "minimax": - case "deepseek": - case "doubao": - case "qwen-code": - case "xai": - case "groq": - case "chutes": - case "cerebras": - case "sambanova": - case "zai": - case "fireworks": - case "featherless": - case "roo": - case "claude-code": - case "synthetic": - return provider.apiModelId || "" - case "virtual-quota-fallback": - return provider.profiles && provider.profiles.length > 0 ? `${provider.profiles.length} profile(s)` : "" - case "vscode-lm": - if (provider.vsCodeLmModelSelector) { - return `${provider.vsCodeLmModelSelector.vendor}/${provider.vsCodeLmModelSelector.family}` - } - return "" - case "huggingface": - return provider.huggingFaceModelId || "" - case "fake-ai": - case "human-relay": - return "" - } -} - -export function mapExtensionStateToConfig(state: ExtensionState, currentConfig?: CLIConfig): CLIConfig { - // This is for future bi-directional sync if needed - const config: CLIConfig = currentConfig || { - version: "1.0.0", - mode: state.mode || "code", - telemetry: state.telemetrySetting === "enabled", - provider: state.currentApiConfigName || "default", - providers: [], - } - - // Map current API configuration to provider - if (state.apiConfiguration) { - const providerId = state.currentApiConfigName || "current" - const existingProvider = config.providers.find((p) => p.id === providerId) - - if (!existingProvider) { - const newProvider = { - id: providerId, - provider: state.apiConfiguration.apiProvider || "kilocode", - ...state.apiConfiguration, - } as ProviderConfig - config.providers.push(newProvider) - } else { - // Update existing provider - Object.assign(existingProvider, state.apiConfiguration) - } - - config.provider = providerId - } - - return config -} diff --git a/cli/src/constants/providers/labels.ts b/cli/src/constants/providers/labels.ts deleted file mode 100644 index 231a7b09e36..00000000000 --- a/cli/src/constants/providers/labels.ts +++ /dev/null @@ -1,72 +0,0 @@ -import type { ProviderName } from "../../types/messages.js" - -/** - * Provider display labels mapping - * Maps provider internal names to user-friendly display names - */ -export const PROVIDER_LABELS: Record = { - kilocode: "Kilo Code", - anthropic: "Anthropic", - "openai-native": "OpenAI", - "openai-codex": "OpenAI - ChatGPT Plus/Pro", - openrouter: "OpenRouter", - bedrock: "Amazon Bedrock", - gemini: "Google Gemini", - vertex: "GCP Vertex AI", - "claude-code": "Claude Code", - mistral: "Mistral", - groq: "Groq", - deepseek: "DeepSeek", - xai: "xAI (Grok)", - cerebras: "Cerebras", - ollama: "Ollama", - lmstudio: "LM Studio", - "vscode-lm": "VS Code LM API", - openai: "OpenAI Compatible", - glama: "Glama", - "nano-gpt": "Nano-GPT", - huggingface: "Hugging Face", - litellm: "LiteLLM", - moonshot: "Moonshot", - doubao: "Doubao", - chutes: "Chutes AI", - sambanova: "SambaNova", - fireworks: "Fireworks", - featherless: "Featherless", - deepinfra: "DeepInfra", - "io-intelligence": "IO Intelligence", - "qwen-code": "Qwen Code", - "gemini-cli": "Gemini CLI", - zai: "Zai", - minimax: "MiniMax", - unbound: "Unbound", - requesty: "Requesty", - roo: "Roo", - "vercel-ai-gateway": "Vercel AI Gateway", - "virtual-quota-fallback": "Virtual Quota Fallback", - "human-relay": "Human Relay", - "fake-ai": "Fake AI", - ovhcloud: "OVHcloud AI Endpoints", - poe: "Poe", - inception: "Inception", - synthetic: "Synthetic", - "sap-ai-core": "SAP AI Core", - baseten: "BaseTen", -} - -/** - * Provider list with value and label pairs - * Used for selection components and dropdowns - */ -export const PROVIDER_OPTIONS: Array<{ value: ProviderName; label: string }> = Object.entries(PROVIDER_LABELS).map( - ([value, label]) => ({ value: value as ProviderName, label }), -) - -/** - * Get provider display label by provider name - * @param provider - Provider name or undefined - * @returns User-friendly display name - */ -export const getProviderLabel = (provider: ProviderName | undefined): string => { - return provider ? PROVIDER_LABELS[provider] || provider : "No provider selected" -} diff --git a/cli/src/constants/providers/models.ts b/cli/src/constants/providers/models.ts deleted file mode 100644 index c09e9190f6d..00000000000 --- a/cli/src/constants/providers/models.ts +++ /dev/null @@ -1,641 +0,0 @@ -import type { ProviderName, ProviderSettings } from "../../types/messages.js" -import type { ProviderConfig } from "../../config/types.js" - -// Import model definitions from @roo-code/types -import { - anthropicModels, - anthropicDefaultModelId, - bedrockModels, - bedrockDefaultModelId, - vertexModels, - vertexDefaultModelId, - openAiNativeModels, - openAiNativeDefaultModelId, - geminiModels, - geminiDefaultModelId, - mistralModels, - mistralDefaultModelId, - moonshotModels, - moonshotDefaultModelId, - deepSeekModels, - deepSeekDefaultModelId, - doubaoModels, - doubaoDefaultModelId, - qwenCodeModels, - qwenCodeDefaultModelId, - xaiModels, - xaiDefaultModelId, - groqModels, - groqDefaultModelId, - chutesModels, - chutesDefaultModelId, - cerebrasModels, - cerebrasDefaultModelId, - sambaNovaModels, - sambaNovaDefaultModelId, - internationalZAiModels, - internationalZAiDefaultModelId, - fireworksModels, - fireworksDefaultModelId, - featherlessModels, - featherlessDefaultModelId, - rooModels, - rooDefaultModelId, - claudeCodeModels, - claudeCodeDefaultModelId, - geminiCliModels, - geminiCliDefaultModelId, - minimaxModels, - minimaxDefaultModelId, - ovhCloudAiEndpointsDefaultModelId, -} from "@roo-code/types" - -/** - * RouterName type - mirrors the one from src/shared/api.ts - */ -export type RouterName = - | "openrouter" - | "requesty" - | "glama" - | "unbound" - | "litellm" - | "kilocode" - | "ollama" - | "lmstudio" - | "io-intelligence" - | "deepinfra" - | "vercel-ai-gateway" - | "ovhcloud" - | "nano-gpt" - | "poe" - -/** - * ModelInfo interface - mirrors the one from packages/types/src/model.ts - */ -export interface ModelInfo { - maxTokens?: number | null - maxThinkingTokens?: number | null - contextWindow: number - supportsImages?: boolean - supportsComputerUse?: boolean - supportsPromptCache: boolean - promptCacheRetention?: "in_memory" | "24h" - supportsVerbosity?: boolean - supportsReasoningBudget?: boolean - supportsReasoningBinary?: boolean - supportsTemperature?: boolean - defaultTemperature?: number - requiredReasoningBudget?: boolean - supportsReasoningEffort?: boolean | ("disable" | "none" | "minimal" | "low" | "medium" | "high")[] - requiredReasoningEffort?: boolean - preserveReasoning?: boolean - supportedParameters?: ("max_tokens" | "temperature" | "reasoning" | "include_reasoning")[] - inputPrice?: number - outputPrice?: number - cacheWritesPrice?: number - cacheReadsPrice?: number - description?: string - reasoningEffort?: "none" | "minimal" | "low" | "medium" | "high" | "xhigh" - minTokensPerCachePoint?: number - maxCachePoints?: number - cachableFields?: string[] - displayName?: string | null - preferredIndex?: number | null - deprecated?: boolean - isFree?: boolean - supportsNativeTools?: boolean - tiers?: Array<{ - name?: "default" | "flex" | "priority" - contextWindow: number - inputPrice?: number - outputPrice?: number - cacheWritesPrice?: number - cacheReadsPrice?: number - }> -} - -export type ModelRecord = Record -export type RouterModels = Record - -/** - * Mapping from ProviderName to RouterName for model fetching - */ -export const PROVIDER_TO_ROUTER_NAME: Record = { - kilocode: "kilocode", - openrouter: "openrouter", - ollama: "ollama", - lmstudio: "lmstudio", - litellm: "litellm", - glama: "glama", - "nano-gpt": "nano-gpt", - unbound: "unbound", - requesty: "requesty", - deepinfra: "deepinfra", - "io-intelligence": "io-intelligence", - "vercel-ai-gateway": "vercel-ai-gateway", - ovhcloud: "ovhcloud", - poe: "poe", - // Providers without dynamic model support - anthropic: null, - bedrock: null, - vertex: null, - openai: null, - "vscode-lm": null, - gemini: null, - "openai-native": null, - "openai-codex": null, - mistral: null, - moonshot: null, - deepseek: null, - doubao: null, - minimax: null, - "qwen-code": null, - "fake-ai": null, - "human-relay": null, - xai: null, - groq: null, - chutes: null, - cerebras: null, - sambanova: null, - zai: null, - fireworks: null, - featherless: null, - roo: null, - "claude-code": null, - "gemini-cli": null, - "virtual-quota-fallback": null, - huggingface: null, - inception: null, - synthetic: null, - "sap-ai-core": null, - baseten: null, -} - -/** - * Mapping from ProviderName to the field name that stores the model ID - */ -export const PROVIDER_MODEL_FIELD: Record = { - kilocode: "kilocodeModel", - openrouter: "openRouterModelId", - ollama: "ollamaModelId", - lmstudio: "lmStudioModelId", - litellm: "litellmModelId", - glama: "glamaModelId", - "nano-gpt": "nanoGptModelId", - unbound: "unboundModelId", - requesty: "requestyModelId", - deepinfra: "deepInfraModelId", - "io-intelligence": "ioIntelligenceModelId", - "vercel-ai-gateway": "vercelAiGatewayModelId", - ovhcloud: "ovhCloudAiEndpointsModelId", - poe: "poeModelId", - // Providers without dynamic model support - anthropic: null, - bedrock: null, - vertex: null, - openai: null, - "vscode-lm": "vsCodeLmModelSelector", - gemini: null, - "openai-native": null, - "openai-codex": null, - mistral: null, - moonshot: null, - deepseek: null, - doubao: null, - minimax: null, - "qwen-code": null, - "fake-ai": null, - "human-relay": null, - xai: null, - groq: null, - chutes: null, - cerebras: null, - sambanova: null, - zai: null, - fireworks: null, - featherless: null, - roo: null, - "claude-code": null, - "gemini-cli": null, - "virtual-quota-fallback": null, - huggingface: null, - inception: "inceptionLabsModelId", - synthetic: null, - "sap-ai-core": "sapAiCoreModelId", - baseten: null, -} - -/** - * Check if a provider supports dynamic model lists - */ -export const providerSupportsModelList = (provider: ProviderName): boolean => { - return PROVIDER_TO_ROUTER_NAME[provider] !== null -} - -/** - * Check if a field is a model selection field - */ -export const isModelField = (field: string): boolean => { - return Object.values(PROVIDER_MODEL_FIELD).includes(field) -} - -/** - * Get the RouterName for a provider - */ -export const getRouterNameForProvider = (provider: ProviderName): RouterName | null => { - return PROVIDER_TO_ROUTER_NAME[provider] -} - -/** - * Get the model field name for a provider - */ -export const getModelFieldForProvider = (provider: ProviderName): string | null => { - return PROVIDER_MODEL_FIELD[provider] -} - -/** - * Default model IDs for each provider - * For providers without router support, these are fallback defaults - */ -export const DEFAULT_MODEL_IDS: Partial> = { - anthropic: anthropicDefaultModelId, - bedrock: bedrockDefaultModelId, - vertex: vertexDefaultModelId, - gemini: geminiDefaultModelId, - deepseek: deepSeekDefaultModelId, - "openai-native": openAiNativeDefaultModelId, - mistral: mistralDefaultModelId, - xai: xaiDefaultModelId, - groq: groqDefaultModelId, - chutes: chutesDefaultModelId, - cerebras: cerebrasDefaultModelId, - "vscode-lm": "gpt-3.5-turbo", - openrouter: "anthropic/claude-sonnet-4.5", - requesty: "anthropic/claude-sonnet-4.5", - glama: "anthropic/claude-sonnet-4.5", - unbound: "anthropic/claude-sonnet-4.5", - litellm: "gpt-4", - "qwen-code": qwenCodeDefaultModelId, - "claude-code": claudeCodeDefaultModelId, - doubao: doubaoDefaultModelId, - fireworks: fireworksDefaultModelId, - "io-intelligence": "deepseek-ai/DeepSeek-R1-0528", - moonshot: moonshotDefaultModelId, - sambanova: sambaNovaDefaultModelId, - featherless: featherlessDefaultModelId, - deepinfra: "deepseek-ai/DeepSeek-R1-0528", - minimax: "MiniMax-M2", - zai: internationalZAiDefaultModelId, - roo: rooDefaultModelId, - "gemini-cli": geminiCliDefaultModelId, - ovhcloud: ovhCloudAiEndpointsDefaultModelId, - poe: "gpt-4o", -} - -/** - * Get models for a specific provider - * Mirrors the logic from webview-ui/src/components/kilocode/hooks/useProviderModels.ts - */ -export function getModelsByProvider(params: { - provider: ProviderName - routerModels: RouterModels | null - kilocodeDefaultModel: string -}): { models: ModelRecord; defaultModel: string } { - const { provider, routerModels, kilocodeDefaultModel } = params - - // Handle router-based providers - const routerName = PROVIDER_TO_ROUTER_NAME[provider] - if (routerName && routerModels && routerModels[routerName]) { - const defaultModelId = DEFAULT_MODEL_IDS[provider] || "" - return { - models: routerModels[routerName], - defaultModel: provider === "kilocode" ? kilocodeDefaultModel : defaultModelId, - } - } - - // Handle non-router providers with static model definitions - switch (provider) { - case "anthropic": - return { - models: anthropicModels as ModelRecord, - defaultModel: anthropicDefaultModelId, - } - case "bedrock": - return { - models: bedrockModels as ModelRecord, - defaultModel: bedrockDefaultModelId, - } - case "vertex": - return { - models: vertexModels as ModelRecord, - defaultModel: vertexDefaultModelId, - } - case "openai-native": - return { - models: openAiNativeModels as ModelRecord, - defaultModel: openAiNativeDefaultModelId, - } - case "gemini": - return { - models: geminiModels as ModelRecord, - defaultModel: geminiDefaultModelId, - } - case "mistral": - return { - models: mistralModels as ModelRecord, - defaultModel: mistralDefaultModelId, - } - case "moonshot": - return { - models: moonshotModels as ModelRecord, - defaultModel: moonshotDefaultModelId, - } - case "minimax": - return { - models: minimaxModels as ModelRecord, - defaultModel: minimaxDefaultModelId, - } - case "deepseek": - return { - models: deepSeekModels as ModelRecord, - defaultModel: deepSeekDefaultModelId, - } - case "doubao": - return { - models: doubaoModels as ModelRecord, - defaultModel: doubaoDefaultModelId, - } - case "qwen-code": - return { - models: qwenCodeModels as ModelRecord, - defaultModel: qwenCodeDefaultModelId, - } - case "xai": - return { - models: xaiModels as ModelRecord, - defaultModel: xaiDefaultModelId, - } - case "groq": - return { - models: groqModels as ModelRecord, - defaultModel: groqDefaultModelId, - } - case "chutes": - return { - models: chutesModels as ModelRecord, - defaultModel: chutesDefaultModelId, - } - case "cerebras": - return { - models: cerebrasModels as ModelRecord, - defaultModel: cerebrasDefaultModelId, - } - case "sambanova": - return { - models: sambaNovaModels as ModelRecord, - defaultModel: sambaNovaDefaultModelId, - } - case "zai": - return { - models: internationalZAiModels as ModelRecord, - defaultModel: internationalZAiDefaultModelId, - } - case "fireworks": - return { - models: fireworksModels as ModelRecord, - defaultModel: fireworksDefaultModelId, - } - case "featherless": - return { - models: featherlessModels as ModelRecord, - defaultModel: featherlessDefaultModelId, - } - case "roo": - return { - models: rooModels as ModelRecord, - defaultModel: rooDefaultModelId, - } - case "claude-code": - return { - models: claudeCodeModels as ModelRecord, - defaultModel: claudeCodeDefaultModelId, - } - case "gemini-cli": - return { - models: geminiCliModels as ModelRecord, - defaultModel: geminiCliDefaultModelId, - } - default: - // For providers without static models (e.g., vscode-lm, fake-ai, virtual-quota-fallback) - return { - models: {}, - defaultModel: DEFAULT_MODEL_IDS[provider] || "", - } - } -} - -/** - * Get the model ID key for a provider - * Mirrors the logic from webview-ui/src/components/kilocode/hooks/useSelectedModel.ts - */ -export function getModelIdKey(provider: ProviderName): string { - switch (provider) { - case "openrouter": - return "openRouterModelId" - case "requesty": - return "requestyModelId" - case "glama": - return "glamaModelId" - case "unbound": - return "unboundModelId" - case "litellm": - return "litellmModelId" - case "openai": - return "openAiModelId" - case "ollama": - return "ollamaModelId" - case "lmstudio": - return "lmStudioModelId" - case "vscode-lm": - return "vsCodeLmModelSelector" - case "kilocode": - return "kilocodeModel" - case "deepinfra": - return "deepInfraModelId" - case "io-intelligence": - return "ioIntelligenceModelId" - case "vercel-ai-gateway": - return "vercelAiGatewayModelId" - case "ovhcloud": - return "ovhCloudAiEndpointsModelId" - case "nano-gpt": - return "nanoGptModelId" - case "poe": - return "poeModelId" - default: - return "apiModelId" - } -} - -/** - * Get the current model ID from provider config - */ -export function getCurrentModelId(params: { - providerConfig: ProviderConfig - routerModels: RouterModels | null - kilocodeDefaultModel: string -}): string { - const { providerConfig, routerModels, kilocodeDefaultModel } = params - const provider = providerConfig.provider - const modelIdKey = getModelIdKey(provider) - - // Special handling for vscode-lm - if (provider === "vscode-lm" && providerConfig.vsCodeLmModelSelector) { - const selector = providerConfig.vsCodeLmModelSelector as ProviderSettings["vsCodeLmModelSelector"] - return `${selector?.vendor}/${selector?.family}` - } - - // Get model ID from config - const modelId = providerConfig[modelIdKey] as string | undefined - - // If model ID exists, return it - if (modelId) { - return modelId - } - - // Otherwise, get default model - const { defaultModel } = getModelsByProvider({ - provider, - routerModels, - kilocodeDefaultModel, - }) - - return defaultModel -} - -/** - * Sort models by preferred index - * Mirrors the logic from webview-ui/src/components/ui/hooks/kilocode/usePreferredModels.ts - */ -export function sortModelsByPreference(models: ModelRecord): string[] { - const preferredModelIds: string[] = [] - const restModelIds: string[] = [] - - // First add the preferred models - for (const [key, model] of Object.entries(models)) { - if (Number.isInteger(model.preferredIndex)) { - preferredModelIds.push(key) - } - } - - // Sort preferred by index - preferredModelIds.sort((a, b) => { - const modelA = models[a] - const modelB = models[b] - if (!modelA || !modelB) return 0 - return (modelA.preferredIndex ?? 0) - (modelB.preferredIndex ?? 0) - }) - - // Then add the rest - for (const [key] of Object.entries(models)) { - if (!preferredModelIds.includes(key)) { - restModelIds.push(key) - } - } - - // Sort rest alphabetically - restModelIds.sort((a, b) => a.localeCompare(b)) - - return [...preferredModelIds, ...restModelIds] -} - -/** - * Format price for display - */ -export function formatPrice(price?: number): string { - if (price === undefined || price === null) { - return "N/A" - } - return `$${price.toFixed(2)}` -} - -/** - * Format model info for display - */ -export function formatModelInfo(modelId: string, model: ModelInfo): string { - const parts: string[] = [] - - // Context window - if (model.contextWindow) { - const contextK = Math.floor(model.contextWindow / 1000) - parts.push(`${contextK}K context`) - } - - // Pricing - if (model.inputPrice !== undefined && model.outputPrice !== undefined) { - parts.push(`${formatPrice(model.inputPrice)}/${formatPrice(model.outputPrice)} per 1M`) - } - - // Capabilities - const capabilities: string[] = [] - if (model.supportsImages) capabilities.push("Images") - if (model.supportsComputerUse) capabilities.push("Computer Use") - if (model.supportsPromptCache) capabilities.push("Cache") - if (model.supportsVerbosity) capabilities.push("Verbosity") - if (model.supportsReasoningEffort) capabilities.push("Reasoning") - - if (capabilities.length > 0) { - parts.push(capabilities.join(", ")) - } - - return parts.join(" | ") -} - -/** - * Fuzzy filter models by name - * Simple fuzzy matching: checks if all characters in filter appear in order in the model ID - */ -export function fuzzyFilterModels(models: ModelRecord, filter: string): string[] { - if (!filter) { - return Object.keys(models) - } - - const lowerFilter = filter.toLowerCase() - const filtered: string[] = [] - - for (const modelId of Object.keys(models)) { - const lowerModelId = modelId.toLowerCase() - const model = models[modelId] - const displayName = model?.displayName?.toLowerCase() || "" - - // Check if filter matches model ID or display name - if (lowerModelId.includes(lowerFilter) || displayName.includes(lowerFilter)) { - filtered.push(modelId) - } - } - - return filtered -} - -/** - * Get a pretty name for a model - */ -export function prettyModelName(modelId: string): string { - // Remove common prefixes - let name = modelId - .replace(/^anthropic\./, "") - .replace(/^accounts\/fireworks\/models\//, "") - .replace(/^deepseek-ai\//, "") - .replace(/^meta-llama\//, "") - - // Convert dashes and underscores to spaces - name = name.replace(/[-_]/g, " ") - - // Capitalize words - name = name - .split(" ") - .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) - .join(" ") - - return name -} diff --git a/cli/src/constants/providers/settings.ts b/cli/src/constants/providers/settings.ts deleted file mode 100644 index dc75486bc5f..00000000000 --- a/cli/src/constants/providers/settings.ts +++ /dev/null @@ -1,1129 +0,0 @@ -import type { ProviderName, ProviderSettings } from "../../types/messages.js" - -/** - * Option for select fields - */ -export interface SelectOption { - value: string - label: string - description?: string -} - -/** - * Provider setting configuration interface - */ -export interface ProviderSettingConfig { - field: string - label: string - value: string - actualValue: string - type: "text" | "password" | "boolean" | "select" - options?: SelectOption[] -} - -/** - * Field metadata interface for centralized field registry - */ -export interface FieldMetadata { - label: string - type: "text" | "password" | "boolean" | "select" - placeholder?: string - isOptional?: boolean - options?: SelectOption[] - defaultValue?: string -} - -/** - * Centralized field metadata registry - * Contains labels, types, placeholders, and optional flags for all provider fields - */ -export const FIELD_REGISTRY: Record = { - // Kilocode fields - kilocodeToken: { - label: "Kilo Code Token", - type: "password", - placeholder: "Enter your Kilo Code token...", - }, - kilocodeOrganizationId: { - label: "Organization ID", - type: "text", - placeholder: "Enter organization ID (or leave empty for personal)...", - isOptional: true, - }, - kilocodeModel: { - label: "Model", - type: "text", - placeholder: "Enter model name...", - }, - - // Anthropic fields - apiKey: { - label: "API Key", - type: "password", - placeholder: "Enter API key...", - }, - apiModelId: { - label: "Model", - type: "text", - placeholder: "Enter model name...", - }, - anthropicBaseUrl: { - label: "Base URL", - type: "text", - placeholder: "Enter base URL (or leave empty for default)...", - isOptional: true, - }, - - // OpenRouter fields - openRouterApiKey: { - label: "API Key", - type: "password", - placeholder: "Enter OpenRouter API key...", - }, - openRouterModelId: { - label: "Model", - type: "text", - placeholder: "Enter model name...", - }, - openRouterBaseUrl: { - label: "Base URL", - type: "text", - placeholder: "Enter base URL (or leave empty for default)...", - isOptional: true, - }, - openRouterProviderDataCollection: { - label: "Provider Data Collection", - type: "select", - isOptional: true, - options: [ - { - value: "allow", - label: "Allow", - description: "Allow data collection by the provider", - }, - { - value: "deny", - label: "Deny", - description: "Deny data collection by the provider", - }, - ], - }, - openRouterProviderSort: { - label: "Provider Sort Preference", - type: "select", - isOptional: true, - options: [ - { - value: "price", - label: "Price", - description: "Sort by price (lowest first)", - }, - { - value: "throughput", - label: "Throughput", - description: "Sort by throughput (highest first)", - }, - { - value: "latency", - label: "Latency", - description: "Sort by latency (lowest first)", - }, - ], - }, - openRouterSpecificProvider: { - label: "Specific Provider", - type: "text", - placeholder: "Enter specific provider (optional)...", - isOptional: true, - }, - openRouterUseMiddleOutTransform: { - label: "Use Middle-Out Transform", - type: "boolean", - }, - openRouterZdr: { - label: "Zero Data Retention", - type: "boolean", - }, - - // OpenAI Native fields - openAiNativeApiKey: { - label: "API Key", - type: "password", - placeholder: "Enter OpenAI API key...", - }, - openAiNativeBaseUrl: { - label: "Base URL", - type: "text", - placeholder: "Enter base URL (or leave empty for default)...", - isOptional: true, - }, - openAiNativeServiceTier: { - label: "Service Tier", - type: "select", - defaultValue: "auto", - isOptional: true, - options: [ - { - value: "auto", - label: "Auto", - description: "Automatically select the best tier", - }, - { - value: "default", - label: "Default", - description: "Standard processing with balanced performance", - }, - { - value: "flex", - label: "Flex", - description: "Cost-optimized with variable latency", - }, - { - value: "priority", - label: "Priority", - description: "Fastest processing with higher priority", - }, - ], - }, - - // AWS Bedrock fields - awsAccessKey: { - label: "AWS Access Key", - type: "password", - placeholder: "Enter AWS access key...", - }, - awsSecretKey: { - label: "AWS Secret Key", - type: "password", - placeholder: "Enter AWS secret key...", - }, - awsSessionToken: { - label: "AWS Session Token", - type: "password", - placeholder: "Enter AWS session token...", - isOptional: true, - }, - awsRegion: { - label: "AWS Region", - type: "text", - placeholder: "Enter AWS region...", - }, - awsUseCrossRegionInference: { - label: "Use Cross-Region Inference", - type: "boolean", - }, - - // Gemini fields - geminiApiKey: { - label: "API Key", - type: "password", - placeholder: "Enter Gemini API key...", - }, - googleGeminiBaseUrl: { - label: "Base URL", - type: "text", - placeholder: "Enter base URL (or leave empty for default)...", - isOptional: true, - }, - - // Vertex fields - vertexJsonCredentials: { - label: "JSON Credentials", - type: "password", - placeholder: "Enter JSON credentials...", - }, - vertexKeyFile: { - label: "Key File Path", - type: "text", - placeholder: "Enter key file path...", - }, - vertexProjectId: { - label: "Project ID", - type: "text", - placeholder: "Enter project ID...", - }, - vertexRegion: { - label: "Region", - type: "text", - placeholder: "Enter region...", - }, - - // Claude Code fields - claudeCodePath: { - label: "Claude Code Path", - type: "text", - placeholder: "Enter Claude Code path...", - }, - claudeCodeMaxOutputTokens: { - label: "Max Output Tokens", - type: "text", - placeholder: "Enter max output tokens...", - }, - - // Mistral fields - mistralApiKey: { - label: "API Key", - type: "password", - placeholder: "Enter Mistral API key...", - }, - mistralCodestralUrl: { - label: "Codestral Base URL", - type: "text", - placeholder: "Enter Codestral base URL (or leave empty for default)...", - isOptional: true, - }, - - // Groq fields - groqApiKey: { - label: "API Key", - type: "password", - placeholder: "Enter Groq API key...", - }, - - // DeepSeek fields - deepSeekApiKey: { - label: "API Key", - type: "password", - placeholder: "Enter DeepSeek API key...", - }, - - // xAI fields - xaiApiKey: { - label: "API Key", - type: "password", - placeholder: "Enter xAI API key...", - }, - - // Cerebras fields - cerebrasApiKey: { - label: "API Key", - type: "password", - placeholder: "Enter Cerebras API key...", - }, - - // Ollama fields - ollamaBaseUrl: { - label: "Base URL", - type: "text", - placeholder: "Enter Ollama base URL...", - }, - ollamaModelId: { - label: "Model ID", - type: "text", - placeholder: "Enter model ID...", - }, - ollamaApiKey: { - label: "API Key (Optional)", - type: "password", - placeholder: "Enter API key (optional)...", - isOptional: true, - }, - - // LM Studio fields - lmStudioBaseUrl: { - label: "Base URL", - type: "text", - placeholder: "Enter LM Studio base URL...", - }, - lmStudioModelId: { - label: "Model ID", - type: "text", - placeholder: "Enter model ID...", - }, - lmStudioSpeculativeDecodingEnabled: { - label: "Speculative Decoding", - type: "boolean", - }, - - // VSCode LM fields - vsCodeLmModelSelector: { - label: "Model Selector", - type: "text", - placeholder: "Enter model selector...", - }, - - // OpenAI fields - openAiApiKey: { - label: "API Key", - type: "password", - placeholder: "Enter OpenAI API key...", - }, - openAiBaseUrl: { - label: "Base URL", - type: "text", - placeholder: "Enter base URL (or leave empty for default)...", - isOptional: true, - }, - - // Glama fields - glamaApiKey: { - label: "API Key", - type: "password", - placeholder: "Enter Glama API key...", - }, - glamaModelId: { - label: "Model ID", - type: "text", - placeholder: "Enter model ID...", - }, - - // Nano-GPT fields - nanoGptApiKey: { - label: "API Key", - type: "password", - placeholder: "Enter Nano-GPT API key...", - }, - nanoGptModelId: { - label: "Model ID", - type: "text", - placeholder: "Enter model ID...", - }, - - // HuggingFace fields - huggingFaceApiKey: { - label: "API Key", - type: "password", - placeholder: "Enter HuggingFace API key...", - }, - huggingFaceModelId: { - label: "Model ID", - type: "text", - placeholder: "Enter model ID...", - }, - huggingFaceInferenceProvider: { - label: "Inference Provider", - type: "text", - placeholder: "Enter inference provider...", - }, - - // LiteLLM fields - litellmBaseUrl: { - label: "Base URL", - type: "text", - placeholder: "Enter LiteLLM base URL...", - }, - litellmApiKey: { - label: "API Key", - type: "password", - placeholder: "Enter API key...", - }, - litellmModelId: { - label: "Model ID", - type: "text", - placeholder: "Enter model ID...", - }, - - // Moonshot fields - moonshotBaseUrl: { - label: "Base URL", - type: "text", - placeholder: "Enter Moonshot base URL...", - }, - moonshotApiKey: { - label: "API Key", - type: "password", - placeholder: "Enter Moonshot API key...", - }, - - // Doubao fields - doubaoApiKey: { - label: "API Key", - type: "password", - placeholder: "Enter Doubao API key...", - }, - - // Chutes fields - chutesApiKey: { - label: "API Key", - type: "password", - placeholder: "Enter Chutes API key...", - }, - - // SambaNova fields - sambaNovaApiKey: { - label: "API Key", - type: "password", - placeholder: "Enter SambaNova API key...", - }, - - // Fireworks fields - fireworksApiKey: { - label: "API Key", - type: "password", - placeholder: "Enter Fireworks API key...", - }, - - // Featherless fields - featherlessApiKey: { - label: "API Key", - type: "password", - placeholder: "Enter Featherless API key...", - }, - - // DeepInfra fields - deepInfraApiKey: { - label: "API Key", - type: "password", - placeholder: "Enter DeepInfra API key...", - }, - deepInfraModelId: { - label: "Model ID", - type: "text", - placeholder: "Enter model ID...", - }, - - // IO Intelligence fields - ioIntelligenceApiKey: { - label: "API Key", - type: "password", - placeholder: "Enter IO Intelligence API key...", - }, - ioIntelligenceModelId: { - label: "Model ID", - type: "text", - placeholder: "Enter model ID...", - }, - - // Qwen Code fields - qwenCodeOauthPath: { - label: "OAuth Credentials Path", - type: "text", - placeholder: "Enter OAuth credentials path...", - }, - - // Gemini CLI fields - geminiCliOAuthPath: { - label: "OAuth Credentials Path", - type: "text", - placeholder: "Enter OAuth credentials path...", - }, - geminiCliProjectId: { - label: "Project ID", - type: "text", - placeholder: "Enter project ID...", - }, - - // ZAI fields - zaiApiKey: { - label: "API Key", - type: "password", - placeholder: "Enter ZAI API key...", - }, - zaiApiLine: { - label: "API Line", - type: "select", - defaultValue: "international_coding", - options: [ - { - value: "international_coding", - label: "International Coding Plan", - description: "Optimized for coding tasks (International)", - }, - { - value: "international", - label: "International Standard", - description: "General-purpose API (International)", - }, - { - value: "china_coding", - label: "China Coding Plan", - description: "Optimized for coding tasks (China)", - }, - { - value: "china", - label: "China Standard", - description: "General-purpose API (China)", - }, - ], - }, - - // Minimax fields - minimaxBaseUrl: { - label: "Base URL", - type: "text", - placeholder: "Enter MiniMax base URL...", - }, - minimaxApiKey: { - label: "API Key", - type: "password", - placeholder: "Enter MiniMax API key...", - }, - - // Unbound fields - unboundApiKey: { - label: "API Key", - type: "password", - placeholder: "Enter Unbound API key...", - }, - unboundModelId: { - label: "Model ID", - type: "text", - placeholder: "Enter model ID...", - }, - - // Requesty fields - requestyApiKey: { - label: "API Key", - type: "password", - placeholder: "Enter Requesty API key...", - }, - requestyBaseUrl: { - label: "Base URL", - type: "text", - placeholder: "Enter base URL (or leave empty for default)...", - isOptional: true, - }, - requestyModelId: { - label: "Model ID", - type: "text", - placeholder: "Enter model ID...", - }, - - // Vercel AI Gateway fields - vercelAiGatewayApiKey: { - label: "API Key", - type: "password", - placeholder: "Enter Vercel AI Gateway API key...", - }, - vercelAiGatewayModelId: { - label: "Model ID", - type: "text", - placeholder: "Enter model ID...", - }, - - // OVHcloud AI Endpoints fields - ovhCloudAiEndpointsApiKey: { - label: "API Key", - type: "password", - placeholder: "Enter OVHcloud AI Endpoints API key...", - }, - ovhCloudAiEndpointsModelId: { - label: "Model ID", - type: "text", - placeholder: "Enter model ID...", - }, - ovhCloudAiEndpointsBaseUrl: { - label: "Base URL", - type: "text", - placeholder: "Enter base URL (or leave empty for default)...", - isOptional: true, - }, - - // Inception Labs fields - inceptionLabsApiKey: { - label: "API Key", - type: "password", - placeholder: "Enter Inception Labs API key...", - }, - inceptionLabsBaseUrl: { - label: "Base URL", - type: "text", - placeholder: "Enter base URL (or leave empty for default)...", - isOptional: true, - }, - inceptionLabsModelId: { - label: "Model ID", - type: "text", - placeholder: "Enter model ID...", - }, - - // Synthetic fields - syntheticApiKey: { - label: "API Key", - type: "password", - placeholder: "Enter Synthetic API key...", - }, - - // SAP AI Core fields - sapAiCoreServiceKey: { - label: "Service Key", - type: "password", - placeholder: "Enter SAP AI Core service key...", - }, - sapAiCoreResourceGroup: { - label: "Resource Group", - type: "text", - placeholder: "Enter resource group...", - }, - sapAiCoreUseOrchestration: { - label: "Use Orchestration", - type: "boolean", - }, - sapAiCoreModelId: { - label: "Model ID", - type: "text", - placeholder: "Enter model ID...", - }, - sapAiCoreDeploymentId: { - label: "Deployment ID", - type: "text", - placeholder: "Enter deployment ID...", - }, - - // Virtual Quota Fallback fields - profiles: { - label: "Profiles Configuration", - type: "text", - placeholder: "Enter profiles configuration...", - }, - - // Poe fields - poeApiKey: { - label: "API Key", - type: "password", - placeholder: "Enter Poe API key...", - }, - poeModelId: { - label: "Model ID", - type: "text", - placeholder: "Enter model ID...", - }, -} - -/** - * Get field display information - * @param field - Field name - * @returns Object with label, placeholder, type, options, and defaultValue - */ -export const getFieldInfo = (field: string) => { - const metadata = FIELD_REGISTRY[field] - if (metadata) { - return { - label: metadata.label, - placeholder: metadata.placeholder || `Enter ${field}...`, - type: metadata.type, - options: metadata.options, - defaultValue: metadata.defaultValue, - } - } - - return { - label: field, - placeholder: `Enter ${field}...`, - type: "text" as const, - options: undefined, - defaultValue: undefined, - } -} - -/** - * Check if a field is a sensitive field (password/token) - * @param field - Field name - * @returns True if field contains sensitive data - */ -export const isSensitiveField = (field: string): boolean => { - const metadata = FIELD_REGISTRY[field] - if (metadata) { - return metadata.type === "password" - } - - // Fallback logic for fields not in registry - return ( - field.toLowerCase().includes("key") || - field.toLowerCase().includes("token") || - field.toLowerCase().includes("secret") || - field.toLowerCase().includes("credentials") - ) -} - -/** - * Check if a field is optional (can be empty) - * @param field - Field name - * @returns True if field is optional - */ -export const isOptionalField = (field: string): boolean => { - const metadata = FIELD_REGISTRY[field] - if (metadata) { - return metadata.isOptional === true - } - - // Fallback logic for fields not in registry - return field.includes("BaseUrl") || field === "kilocodeOrganizationId" -} - -/** - * Helper function to create a field configuration using centralized metadata - * @param field - Field name - * @param config - Provider configuration object - * @param defaultValue - Default value to display when field is empty - * @returns ProviderSettingConfig object - */ -const createFieldConfig = (field: string, config: ProviderSettings, defaultValue?: string): ProviderSettingConfig => { - const fieldInfo = getFieldInfo(field) - const rawValue = config[field as keyof ProviderSettings] - const actualValue = rawValue ?? "" - - let displayValue: string - if (fieldInfo.type === "password") { - displayValue = actualValue ? "••••••••" : "Not set" - } else if (fieldInfo.type === "boolean") { - displayValue = actualValue ? "Enabled" : "Disabled" - } else if (fieldInfo.type === "select") { - // For select fields, show the label of the selected option - if (actualValue && fieldInfo.options) { - const selectedOption = fieldInfo.options.find((opt) => opt.value === actualValue) - displayValue = selectedOption ? selectedOption.label : String(actualValue) - } else { - displayValue = defaultValue || "Not set" - } - } else { - displayValue = (typeof actualValue === "string" ? actualValue : "") || defaultValue || "Not set" - } - - const result: ProviderSettingConfig = { - field, - label: fieldInfo.label, - value: displayValue, - actualValue: fieldInfo.type === "boolean" ? (actualValue ? "true" : "false") : String(actualValue), - type: fieldInfo.type, - } - - // Only add options if they exist - if (fieldInfo.options) { - result.options = fieldInfo.options - } - - return result -} - -/** - * Get provider-specific settings configuration - * @param provider - Provider name - * @param config - Provider configuration object - * @returns Array of setting configurations - */ -export const getProviderSettings = (provider: ProviderName, config: ProviderSettings): ProviderSettingConfig[] => { - switch (provider) { - case "kilocode": - return [ - createFieldConfig("kilocodeToken", config), - createFieldConfig("kilocodeOrganizationId", config, "personal"), - createFieldConfig("kilocodeModel", config, "anthropic/claude-sonnet-4"), - ] - - case "anthropic": - return [ - createFieldConfig("apiKey", config), - createFieldConfig("apiModelId", config, "claude-3-5-sonnet-20241022"), - createFieldConfig("anthropicBaseUrl", config, "Default"), - ] - - case "openrouter": - return [ - createFieldConfig("openRouterApiKey", config), - createFieldConfig("openRouterModelId", config, "anthropic/claude-3-5-sonnet"), - createFieldConfig("openRouterBaseUrl", config, "Default"), - ] - - case "openai-native": - return [ - createFieldConfig("openAiNativeApiKey", config), - createFieldConfig("apiModelId", config, "gpt-4o"), - createFieldConfig("openAiNativeBaseUrl", config, "Default"), - ] - - case "openai-codex": - return [createFieldConfig("apiModelId", config, "gpt-4o")] - - case "bedrock": - return [ - createFieldConfig("awsAccessKey", config), - createFieldConfig("awsSecretKey", config), - createFieldConfig("awsSessionToken", config), - createFieldConfig("awsRegion", config, "us-east-1"), - createFieldConfig("awsUseCrossRegionInference", config), - ] - - case "gemini": - return [ - createFieldConfig("geminiApiKey", config), - createFieldConfig("googleGeminiBaseUrl", config, "Default"), - ] - - case "vertex": - return [ - createFieldConfig("vertexJsonCredentials", config), - createFieldConfig("vertexKeyFile", config), - createFieldConfig("vertexProjectId", config), - createFieldConfig("vertexRegion", config, "us-central1"), - ] - - case "claude-code": - return [ - createFieldConfig("claudeCodePath", config), - createFieldConfig("claudeCodeMaxOutputTokens", config, "8000"), - ] - - case "mistral": - return [ - createFieldConfig("mistralApiKey", config), - createFieldConfig("mistralCodestralUrl", config, "Default"), - ] - - case "groq": - return [createFieldConfig("groqApiKey", config)] - - case "deepseek": - return [createFieldConfig("deepSeekApiKey", config)] - - case "xai": - return [createFieldConfig("xaiApiKey", config)] - - case "cerebras": - return [createFieldConfig("cerebrasApiKey", config)] - - case "ollama": - return [ - createFieldConfig("ollamaBaseUrl", config, "http://localhost:11434"), - createFieldConfig("ollamaModelId", config, "llama3.2"), - createFieldConfig("ollamaApiKey", config), - ] - - case "lmstudio": - return [ - createFieldConfig("lmStudioBaseUrl", config, "http://localhost:1234/v1"), - createFieldConfig("lmStudioModelId", config, "local-model"), - createFieldConfig("lmStudioSpeculativeDecodingEnabled", config), - ] - - case "vscode-lm": - return [ - { - field: "vsCodeLmModelSelector", - label: "Model Selector", - value: config.vsCodeLmModelSelector - ? `${config.vsCodeLmModelSelector.vendor}/${config.vsCodeLmModelSelector.family}` - : "Not set", - actualValue: config.vsCodeLmModelSelector ? JSON.stringify(config.vsCodeLmModelSelector) : "", - type: "text", - }, - ] - - case "openai": - return [createFieldConfig("openAiApiKey", config), createFieldConfig("openAiBaseUrl", config, "Default")] - - case "glama": - return [ - createFieldConfig("glamaApiKey", config), - createFieldConfig("glamaModelId", config, "llama-3.1-70b-versatile"), - ] - - case "nano-gpt": - return [createFieldConfig("nanoGptApiKey", config), createFieldConfig("nanoGptModelId", config, "gpt-4o")] - - case "huggingface": - return [ - createFieldConfig("huggingFaceApiKey", config), - createFieldConfig("huggingFaceModelId", config, "meta-llama/Llama-2-70b-chat-hf"), - createFieldConfig("huggingFaceInferenceProvider", config, "auto"), - ] - - case "litellm": - return [ - createFieldConfig("litellmBaseUrl", config), - createFieldConfig("litellmApiKey", config), - createFieldConfig("litellmModelId", config, "gpt-4o"), - ] - - case "moonshot": - return [ - createFieldConfig("moonshotBaseUrl", config, "https://api.moonshot.ai/v1"), - createFieldConfig("moonshotApiKey", config), - ] - - case "doubao": - return [createFieldConfig("doubaoApiKey", config)] - - case "chutes": - return [createFieldConfig("chutesApiKey", config)] - - case "sambanova": - return [createFieldConfig("sambaNovaApiKey", config)] - - case "fireworks": - return [createFieldConfig("fireworksApiKey", config)] - - case "featherless": - return [createFieldConfig("featherlessApiKey", config)] - - case "deepinfra": - return [ - createFieldConfig("deepInfraApiKey", config), - createFieldConfig("deepInfraModelId", config, "meta-llama/Meta-Llama-3.1-70B-Instruct"), - ] - - case "io-intelligence": - return [ - createFieldConfig("ioIntelligenceApiKey", config), - createFieldConfig("ioIntelligenceModelId", config, "gpt-4o"), - ] - - case "qwen-code": - return [createFieldConfig("qwenCodeOauthPath", config, "~/.qwen/oauth_creds.json")] - - case "gemini-cli": - return [ - createFieldConfig("geminiCliOAuthPath", config, "~/.gemini/oauth_creds.json"), - createFieldConfig("geminiCliProjectId", config), - ] - - case "zai": - return [ - createFieldConfig("zaiApiKey", config), - createFieldConfig("zaiApiLine", config, "international_coding"), - ] - - case "unbound": - return [createFieldConfig("unboundApiKey", config), createFieldConfig("unboundModelId", config, "gpt-4o")] - - case "requesty": - return [ - createFieldConfig("requestyApiKey", config), - createFieldConfig("requestyBaseUrl", config, "Default"), - createFieldConfig("requestyModelId", config, "gpt-4o"), - ] - - case "roo": - return [createFieldConfig("apiModelId", config, "gpt-4o")] - - case "vercel-ai-gateway": - return [ - createFieldConfig("vercelAiGatewayApiKey", config), - createFieldConfig("vercelAiGatewayModelId", config, "gpt-4o"), - ] - - case "virtual-quota-fallback": - return [ - { - field: "profiles", - label: "Profiles Configuration", - value: config.profiles ? `${config.profiles.length} profile(s)` : "Not configured", - actualValue: config.profiles ? JSON.stringify(config.profiles) : "", - type: "text", - }, - ] - - case "minimax": - return [ - createFieldConfig("minimaxBaseUrl", config, "https://api.minimax.io/anthropic"), - createFieldConfig("minimaxApiKey", config), - ] - case "fake-ai": - return [ - { - field: "apiModelId", - label: "Model", - value: "fake-model", - actualValue: "fake-model", - type: "text", - }, - ] - - case "human-relay": - return [ - { - field: "apiModelId", - label: "Model", - value: "human-relay-model", - actualValue: "human-relay-model", - type: "text", - }, - ] - - case "ovhcloud": - return [ - createFieldConfig("ovhCloudAiEndpointsApiKey", config), - createFieldConfig("ovhCloudAiEndpointsModelId", config, "gpt-oss-120b"), - createFieldConfig("ovhCloudAiEndpointsBaseUrl", config, "Default"), - ] - - case "inception": - return [ - createFieldConfig("inceptionLabsApiKey", config), - createFieldConfig("inceptionLabsBaseUrl", config, "Default"), - createFieldConfig("inceptionLabsModelId", config, "gpt-4o"), - ] - - case "synthetic": - return [ - createFieldConfig("syntheticApiKey", config), - createFieldConfig("apiModelId", config, "synthetic-model"), - ] - - case "sap-ai-core": - return [ - createFieldConfig("sapAiCoreServiceKey", config), - createFieldConfig("sapAiCoreResourceGroup", config), - createFieldConfig("sapAiCoreDeploymentId", config), - createFieldConfig("sapAiCoreModelId", config), - ] - - case "poe": - return [createFieldConfig("poeApiKey", config), createFieldConfig("poeModelId", config, "gpt-4o")] - - default: - return [] - } -} - -/** - * Provider-specific default models - */ -export const PROVIDER_DEFAULT_MODELS: Record = { - kilocode: "anthropic/claude-sonnet-4", - anthropic: "claude-3-5-sonnet-20241022", - "openai-native": "gpt-4o", - "openai-codex": "gpt-4o", - openrouter: "anthropic/claude-3-5-sonnet", - bedrock: "anthropic.claude-3-5-sonnet-20241022-v2:0", - gemini: "gemini-1.5-pro-latest", - vertex: "claude-3-5-sonnet@20241022", - "claude-code": "claude-3-5-sonnet-20241022", - mistral: "mistral-large-latest", - groq: "llama-3.1-70b-versatile", - deepseek: "deepseek-chat", - xai: "grok-beta", - cerebras: "llama3.1-8b", - ollama: "llama3.2", - lmstudio: "local-model", - "vscode-lm": "copilot-gpt-4o", - openai: "gpt-4o", - glama: "llama-3.1-70b-versatile", - "nano-gpt": "gpt-4o", - huggingface: "meta-llama/Llama-2-70b-chat-hf", - litellm: "gpt-4o", - moonshot: "moonshot-v1-8k", - doubao: "ep-20241022-******", - chutes: "gpt-4o", - sambanova: "Meta-Llama-3.1-70B-Instruct", - fireworks: "accounts/fireworks/models/llama-v3p1-70b-instruct", - featherless: "meta-llama/Llama-3.1-70B-Instruct", - deepinfra: "meta-llama/Meta-Llama-3.1-70B-Instruct", - "io-intelligence": "gpt-4o", - "qwen-code": "qwen-coder-plus-latest", - "gemini-cli": "gemini-1.5-pro-latest", - zai: "gpt-4o", - unbound: "gpt-4o", - requesty: "gpt-4o", - roo: "gpt-4o", - "vercel-ai-gateway": "gpt-4o", - "virtual-quota-fallback": "gpt-4o", - minimax: "MiniMax-M2", - "fake-ai": "fake-model", - "human-relay": "human-relay-model", - ovhcloud: "gpt-oss-120b", - poe: "gpt-4o", - inception: "gpt-4o", - synthetic: "synthetic-model", - "sap-ai-core": "gpt-4o", - baseten: "zai-org/GLM-4.6", -} - -/** - * Get default model for a provider - * @param provider - Provider name - * @returns Default model string - */ -export const getProviderDefaultModel = (provider: ProviderName): string => { - return PROVIDER_DEFAULT_MODELS[provider] || "default-model" -} diff --git a/cli/src/constants/providers/validation.ts b/cli/src/constants/providers/validation.ts deleted file mode 100644 index 2505c12993b..00000000000 --- a/cli/src/constants/providers/validation.ts +++ /dev/null @@ -1,56 +0,0 @@ -import type { ProviderName } from "../../types/messages.js" - -/** - * Configuration map for provider validation requirements. - * Maps each provider to its required fields that must be non-empty when selected. - */ -export const PROVIDER_REQUIRED_FIELDS: Record = { - kilocode: ["kilocodeToken", "kilocodeModel"], - anthropic: ["apiKey", "apiModelId"], - "openai-native": ["openAiNativeApiKey", "apiModelId"], - "openai-codex": ["apiModelId"], - openrouter: ["openRouterApiKey", "openRouterModelId"], - ollama: ["ollamaBaseUrl", "ollamaModelId"], - lmstudio: ["lmStudioBaseUrl", "lmStudioModelId"], - bedrock: ["awsRegion", "apiModelId"], // Auth fields handled in handleSpecialValidations (supports API key, profile, or direct credentials) - gemini: ["geminiApiKey", "apiModelId"], - "claude-code": ["claudeCodePath", "apiModelId"], - mistral: ["mistralApiKey", "apiModelId"], - groq: ["groqApiKey", "apiModelId"], - deepseek: ["deepSeekApiKey", "apiModelId"], - xai: ["xaiApiKey", "apiModelId"], - openai: ["openAiApiKey"], - cerebras: ["cerebrasApiKey", "apiModelId"], - glama: ["glamaApiKey", "glamaModelId"], - "nano-gpt": ["nanoGptApiKey", "nanoGptModelId"], - huggingface: ["huggingFaceApiKey", "huggingFaceModelId", "huggingFaceInferenceProvider"], - litellm: ["litellmBaseUrl", "litellmApiKey", "litellmModelId"], - moonshot: ["moonshotBaseUrl", "moonshotApiKey", "apiModelId"], - doubao: ["doubaoApiKey", "apiModelId"], - chutes: ["chutesApiKey", "apiModelId"], - sambanova: ["sambaNovaApiKey", "apiModelId"], - fireworks: ["fireworksApiKey", "apiModelId"], - featherless: ["featherlessApiKey", "apiModelId"], - deepinfra: ["deepInfraApiKey", "deepInfraModelId"], - "io-intelligence": ["ioIntelligenceApiKey", "ioIntelligenceModelId"], - "qwen-code": ["qwenCodeOauthPath", "apiModelId"], - "gemini-cli": ["geminiCliOAuthPath", "geminiCliProjectId", "apiModelId"], - zai: ["zaiApiKey", "zaiApiLine", "apiModelId"], - unbound: ["unboundApiKey", "unboundModelId"], - requesty: ["requestyApiKey", "requestyModelId"], - roo: ["apiModelId"], - "vercel-ai-gateway": ["vercelAiGatewayApiKey", "vercelAiGatewayModelId"], - "fake-ai": ["apiModelId"], - "human-relay": ["apiModelId"], - ovhcloud: ["ovhCloudAiEndpointsApiKey", "ovhCloudAiEndpointsModelId"], - poe: ["poeApiKey", "poeModelId"], - inception: ["inceptionLabsApiKey", "inceptionLabsModelId"], - synthetic: ["syntheticApiKey", "apiModelId"], - "sap-ai-core": ["sapAiCoreServiceKey", "sapAiCoreResourceGroup", "sapAiCoreDeploymentId", "sapAiCoreModelId"], - // Special cases handled separately in handleSpecialValidations - vertex: [], // Has special validation logic (either/or fields) - "vscode-lm": [], // Has nested object validation - "virtual-quota-fallback": [], // Has array validation - minimax: ["minimaxBaseUrl", "minimaxApiKey", "apiModelId"], - baseten: ["basetenApiKey", "apiModelId"], -} From ae8aa3f1d5a490aa67eb5b13be103517c57395af Mon Sep 17 00:00:00 2001 From: Kevin van Dijk Date: Sat, 21 Feb 2026 19:59:12 +0100 Subject: [PATCH 4/4] Add markers --- packages/core-schemas/src/config/provider.ts | 6 ++++-- packages/types/src/providers/index.ts | 8 ++++---- packages/types/src/providers/poe.ts | 1 + src/api/providers/__tests__/poe.spec.ts | 1 + src/api/providers/fetchers/__tests__/poe.spec.ts | 1 + src/core/webview/__tests__/webviewMessageHandler.spec.ts | 2 ++ src/core/webview/webviewMessageHandler.ts | 2 ++ webview-ui/src/components/settings/ApiOptions.tsx | 8 +++++--- webview-ui/src/components/settings/constants.ts | 2 +- webview-ui/src/components/settings/providers/index.ts | 2 +- webview-ui/src/utils/__tests__/validate.spec.ts | 2 +- 11 files changed, 23 insertions(+), 12 deletions(-) diff --git a/packages/core-schemas/src/config/provider.ts b/packages/core-schemas/src/config/provider.ts index 2990b052137..9ad7a73bc61 100644 --- a/packages/core-schemas/src/config/provider.ts +++ b/packages/core-schemas/src/config/provider.ts @@ -145,12 +145,14 @@ export const deepInfraProviderSchema = baseProviderSchema.extend({ deepInfraApiKey: z.string().optional(), }) +// kilocode_change start // Poe provider export const poeProviderSchema = baseProviderSchema.extend({ provider: z.literal("poe"), poeModelId: z.string().optional(), poeApiKey: z.string().optional(), }) +// kilocode_change end // Unbound provider export const unboundProviderSchema = baseProviderSchema.extend({ @@ -435,7 +437,7 @@ export const providerConfigSchema = z.discriminatedUnion("provider", [ glamaProviderSchema, liteLLMProviderSchema, deepInfraProviderSchema, - poeProviderSchema, + poeProviderSchema, // kilocode_change unboundProviderSchema, requestyProviderSchema, vercelAiGatewayProviderSchema, @@ -483,7 +485,7 @@ export type LMStudioProviderConfig = z.infer export type GlamaProviderConfig = z.infer export type LiteLLMProviderConfig = z.infer export type DeepInfraProviderConfig = z.infer -export type PoeProviderConfig = z.infer +export type PoeProviderConfig = z.infer // kilocode_change export type UnboundProviderConfig = z.infer export type RequestyProviderConfig = z.infer export type VercelAiGatewayProviderConfig = z.infer diff --git a/packages/types/src/providers/index.ts b/packages/types/src/providers/index.ts index bcf4062281f..239f0c4b6dd 100644 --- a/packages/types/src/providers/index.ts +++ b/packages/types/src/providers/index.ts @@ -33,7 +33,7 @@ export * from "./openai.js" export * from "./openai-codex.js" export * from "./openai-codex-rate-limits.js" export * from "./openrouter.js" -export * from "./poe.js" +export * from "./poe.js" // kilocode_change export * from "./qwen-code.js" export * from "./requesty.js" export * from "./roo.js" @@ -70,7 +70,7 @@ import { mistralDefaultModelId } from "./mistral.js" import { moonshotDefaultModelId } from "./moonshot.js" import { openAiCodexDefaultModelId } from "./openai-codex.js" import { openRouterDefaultModelId } from "./openrouter.js" -import { poeDefaultModelId } from "./poe.js" +import { poeDefaultModelId } from "./poe.js" // kilocode_change import { qwenCodeDefaultModelId } from "./qwen-code.js" import { requestyDefaultModelId } from "./requesty.js" import { rooDefaultModelId } from "./roo.js" @@ -99,8 +99,8 @@ export function getProviderDefaultModelId( switch (provider) { case "openrouter": return openRouterDefaultModelId - case "poe": - return poeDefaultModelId + case "poe": // kilocode_change + return poeDefaultModelId // kilocode_change case "zenmux": // kilocode_change return zenmuxDefaultModelId // kilocode_change case "requesty": diff --git a/packages/types/src/providers/poe.ts b/packages/types/src/providers/poe.ts index 6d6a439d2e8..ae12e1ae096 100644 --- a/packages/types/src/providers/poe.ts +++ b/packages/types/src/providers/poe.ts @@ -1,3 +1,4 @@ +// kilocode_change - new file import type { ModelInfo } from "../model.js" export const POE_BASE_URL = "https://api.poe.com/v1/" diff --git a/src/api/providers/__tests__/poe.spec.ts b/src/api/providers/__tests__/poe.spec.ts index cd021761261..8106e8f184f 100644 --- a/src/api/providers/__tests__/poe.spec.ts +++ b/src/api/providers/__tests__/poe.spec.ts @@ -1,3 +1,4 @@ +// kilocode_change - new file // npx vitest run src/api/providers/__tests__/poe.spec.ts import { Anthropic } from "@anthropic-ai/sdk" diff --git a/src/api/providers/fetchers/__tests__/poe.spec.ts b/src/api/providers/fetchers/__tests__/poe.spec.ts index 6db43d79789..13e2f0e9d80 100644 --- a/src/api/providers/fetchers/__tests__/poe.spec.ts +++ b/src/api/providers/fetchers/__tests__/poe.spec.ts @@ -1,3 +1,4 @@ +// kilocode_change - new file // npx vitest run src/api/providers/fetchers/__tests__/poe.spec.ts vi.mock("axios") diff --git a/src/core/webview/__tests__/webviewMessageHandler.spec.ts b/src/core/webview/__tests__/webviewMessageHandler.spec.ts index 2865df6c9a1..bccfda6e255 100644 --- a/src/core/webview/__tests__/webviewMessageHandler.spec.ts +++ b/src/core/webview/__tests__/webviewMessageHandler.spec.ts @@ -741,12 +741,14 @@ describe("webviewMessageHandler - requestRouterModels", () => { values: { provider: "chutes" }, }) + // kilocode_change start expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ type: "singleRouterModelFetchResponse", success: false, error: "Poe API error", values: { provider: "poe" }, }) + // kilocode_change end expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ type: "singleRouterModelFetchResponse", diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 5b73560ee10..53dece050f2 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -1037,10 +1037,12 @@ export const webviewMessageHandler = async ( key: "chutes", options: { provider: "chutes", apiKey: apiConfiguration.chutesApiKey }, }, + // kilocode_change start { key: "poe", options: { provider: "poe", apiKey: apiConfiguration.poeApiKey }, }, + // kilocode_change end { key: "zenmux", options: { diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index b7b224d586c..c9118bc6610 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -50,7 +50,7 @@ import { deepInfraDefaultModelId, minimaxDefaultModelId, nanoGptDefaultModelId, //kilocode_change - poeDefaultModelId, + poeDefaultModelId, // kilocode_change } from "@roo-code/types" import { vscode } from "@src/utils/vscode" @@ -127,7 +127,7 @@ import { VercelAiGateway, DeepInfra, MiniMax, - Poe, + Poe, // kilocode_change } from "./providers" import { MODELS_BY_PROVIDER, PROVIDERS } from "./constants" @@ -303,7 +303,7 @@ const ApiOptions = ({ selectedProvider === "deepinfra" || selectedProvider === "chutes" || // kilocode_change selectedProvider === "synthetic" || // kilocode_change - selectedProvider === "poe" || + selectedProvider === "poe" || // kilocode_change selectedProvider === "roo" ) { vscode.postMessage({ type: "requestRouterModels" }) @@ -722,6 +722,7 @@ const ApiOptions = ({ )} {/* kilocode_change end */} + {/* kilocode_change start */} {selectedProvider === "poe" && ( )} + {/* kilocode_change end */} {selectedProvider === "anthropic" && ( a.label.localeCompare(b.label)) diff --git a/webview-ui/src/components/settings/providers/index.ts b/webview-ui/src/components/settings/providers/index.ts index 4024e858eea..11ce77e0d31 100644 --- a/webview-ui/src/components/settings/providers/index.ts +++ b/webview-ui/src/components/settings/providers/index.ts @@ -45,5 +45,5 @@ export { VercelAiGateway } from "./VercelAiGateway" export { DeepInfra } from "./DeepInfra" export { MiniMax } from "./MiniMax" export { Baseten } from "./Baseten" -export { Poe } from "./Poe" +export { Poe } from "./Poe" // kilocode_change export { Corethink } from "./Corethink" diff --git a/webview-ui/src/utils/__tests__/validate.spec.ts b/webview-ui/src/utils/__tests__/validate.spec.ts index 015b2f9b3b0..05e77096421 100644 --- a/webview-ui/src/utils/__tests__/validate.spec.ts +++ b/webview-ui/src/utils/__tests__/validate.spec.ts @@ -89,7 +89,7 @@ describe("Model Validation Functions", () => { // kilocode_change end roo: {}, chutes: {}, - poe: {}, + poe: {}, // kilocode_change zenmux: {}, }