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/packages/core-schemas/src/config/provider.ts b/packages/core-schemas/src/config/provider.ts index e6e4639db84..9ad7a73bc61 100644 --- a/packages/core-schemas/src/config/provider.ts +++ b/packages/core-schemas/src/config/provider.ts @@ -145,6 +145,15 @@ 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({ provider: z.literal("unbound"), @@ -428,6 +437,7 @@ export const providerConfigSchema = z.discriminatedUnion("provider", [ glamaProviderSchema, liteLLMProviderSchema, deepInfraProviderSchema, + poeProviderSchema, // kilocode_change unboundProviderSchema, requestyProviderSchema, vercelAiGatewayProviderSchema, @@ -475,6 +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 // kilocode_change 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 49a3da4f250..bc266b8a3b9 100644 --- a/packages/types/src/provider-settings.ts +++ b/packages/types/src/provider-settings.ts @@ -69,6 +69,7 @@ export const dynamicProviders = [ "roo", "chutes", "nano-gpt", //kilocode_change + "poe", // kilocode_change ] as const export type DynamicProvider = (typeof dynamicProviders)[number] @@ -414,6 +415,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(), @@ -624,6 +632,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") })), @@ -689,6 +698,7 @@ export const providerSettingsSchema = z.object({ ...mistralSchema.shape, ...deepSeekSchema.shape, ...deepInfraSchema.shape, + ...poeSchema.shape, // kilocode_change ...doubaoSchema.shape, ...moonshotSchema.shape, ...minimaxSchema.shape, @@ -750,6 +760,7 @@ export const modelIdKeys = [ "ioIntelligenceModelId", "vercelAiGatewayModelId", "deepInfraModelId", + "poeModelId", // kilocode_change "kilocodeModel", "ovhCloudAiEndpointsModelId", // kilocode_change "inceptionLabsModelId", // kilocode_change @@ -793,6 +804,7 @@ export const modelIdKeysByProvider: Record = { minimax: "apiModelId", deepseek: "apiModelId", deepinfra: "deepInfraModelId", + poe: "poeModelId", // kilocode_change doubao: "apiModelId", "qwen-code": "apiModelId", unbound: "unboundModelId", @@ -985,6 +997,7 @@ export const MODELS_BY_PROVIDER: Record< zenmux: { id: "zenmux", label: "ZenMux", models: [] }, // kilocode_change // 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 48e3c3020ed..239f0c4b6dd 100644 --- a/packages/types/src/providers/index.ts +++ b/packages/types/src/providers/index.ts @@ -33,6 +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" // kilocode_change export * from "./qwen-code.js" export * from "./requesty.js" export * from "./roo.js" @@ -69,6 +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" // kilocode_change import { qwenCodeDefaultModelId } from "./qwen-code.js" import { requestyDefaultModelId } from "./requesty.js" import { rooDefaultModelId } from "./roo.js" @@ -97,6 +99,8 @@ export function getProviderDefaultModelId( switch (provider) { case "openrouter": return openRouterDefaultModelId + 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 new file mode 100644 index 00000000000..ae12e1ae096 --- /dev/null +++ b/packages/types/src/providers/poe.ts @@ -0,0 +1,18 @@ +// kilocode_change - new file +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 bc890b2eee7..d110a2d170a 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -11,6 +11,7 @@ import { AwsBedrockHandler, CerebrasHandler, OpenRouterHandler, + PoeHandler, // kilocode_change ZenMuxHandler, // kilocode_change VertexHandler, AnthropicVertexHandler, @@ -191,6 +192,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 "zenmux": // kilocode_change return new ZenMuxHandler(options) // kilocode_change case "bedrock": diff --git a/src/api/providers/__tests__/poe.spec.ts b/src/api/providers/__tests__/poe.spec.ts new file mode 100644 index 00000000000..8106e8f184f --- /dev/null +++ b/src/api/providers/__tests__/poe.spec.ts @@ -0,0 +1,539 @@ +// kilocode_change - new file +// 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..13e2f0e9d80 --- /dev/null +++ b/src/api/providers/fetchers/__tests__/poe.spec.ts @@ -0,0 +1,428 @@ +// kilocode_change - new file +// 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 e4e2a07687e..a62f95f94cf 100644 --- a/src/api/providers/fetchers/modelCache.ts +++ b/src/api/providers/fetchers/modelCache.ts @@ -42,6 +42,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 import { getZenmuxModels } from "./zenmux" const memoryCache = new NodeCache({ stdTTL: 5 * 60, checkperiod: 5 * 60 }) @@ -194,6 +195,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 23b1951ba73..d9b28864497 100644 --- a/src/api/providers/index.ts +++ b/src/api/providers/index.ts @@ -25,6 +25,7 @@ export { OpenAiCompatibleResponsesHandler } from "./openai-responses" // kilocod export { OpenAICompatibleHandler } from "./openai-compatible" export type { OpenAICompatibleConfig } from "./openai-compatible" export { OpenRouterHandler } from "./openrouter" +export { PoeHandler } from "./poe" // kilocode_change export { ZenMuxHandler } from "./zenmux" // kilocode_change export { QwenCodeHandler } from "./qwen-code" export { RequestyHandler } from "./requesty" 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 03ff82b9229..e188a1aa00d 100644 --- a/src/core/webview/__tests__/ClineProvider.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.spec.ts @@ -2852,6 +2852,7 @@ describe("ClineProvider - Router Models", () => { "sap-ai-core": {}, // kilocode_change huggingface: {}, "io-intelligence": {}, + poe: mockModels, // kilocode_change zenmux: mockModels, }, values: undefined, @@ -2906,6 +2907,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 .mockResolvedValueOnce(mockModels) // zenmux success .mockRejectedValueOnce(new Error("LiteLLM connection failed")) // litellm fail @@ -2937,6 +2939,7 @@ describe("ClineProvider - Router Models", () => { "sap-ai-core": {}, // kilocode_change huggingface: {}, "io-intelligence": {}, + poe: mockModels, // kilocode_change zenmux: mockModels, }, values: undefined, @@ -3096,6 +3099,7 @@ describe("ClineProvider - Router Models", () => { "sap-ai-core": {}, // kilocode_change huggingface: {}, "io-intelligence": {}, + poe: mockModels, // kilocode_change zenmux: mockModels, }, values: undefined, diff --git a/src/core/webview/__tests__/webviewMessageHandler.spec.ts b/src/core/webview/__tests__/webviewMessageHandler.spec.ts index 466a6293602..bccfda6e255 100644 --- a/src/core/webview/__tests__/webviewMessageHandler.spec.ts +++ b/src/core/webview/__tests__/webviewMessageHandler.spec.ts @@ -278,6 +278,7 @@ describe("webviewMessageHandler - requestRouterModels", () => { ovhCloudAiEndpointsApiKey: "ovhcloud-key", inceptionLabsApiKey: "inception-key", inceptionLabsBaseUrl: "https://api.inceptionlabs.ai/v1/", + poeApiKey: "poe-key", // kilocode_change end }, }) @@ -332,6 +333,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" }) @@ -367,6 +372,7 @@ describe("webviewMessageHandler - requestRouterModels", () => { aihubmix: mockModels, // kilocode_change roo: mockModels, chutes: mockModels, + poe: mockModels, // kilocode_change zenmux: mockModels, ollama: mockModels, // kilocode_change lmstudio: {}, @@ -430,6 +436,7 @@ describe("webviewMessageHandler - requestRouterModels", () => { ovhCloudAiEndpointsApiKey: "ovhcloud-key", chutesApiKey: "chutes-key", nanoGptApiKey: "nano-gpt-key", + poeApiKey: "poe-key", // kilocode_change end // Missing litellm config }, @@ -472,6 +479,7 @@ describe("webviewMessageHandler - requestRouterModels", () => { unbound: mockModels, roo: mockModels, chutes: mockModels, + poe: mockModels, // kilocode_change zenmux: mockModels, litellm: {}, kilocode: mockModels, @@ -518,6 +526,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 .mockResolvedValueOnce(mockModels) // zenmux .mockRejectedValueOnce(new Error("LiteLLM connection failed")) // litellm @@ -582,6 +591,7 @@ describe("webviewMessageHandler - requestRouterModels", () => { unbound: {}, roo: mockModels, chutes: {}, + poe: mockModels, // kilocode_change zenmux: mockModels, litellm: {}, ollama: {}, @@ -623,6 +633,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 .mockResolvedValueOnce({}) // zenmux .mockRejectedValueOnce(new Error("LiteLLM connection failed")) // litellm @@ -730,6 +741,15 @@ 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", success: false, diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 3d2ba899500..53dece050f2 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -925,6 +925,7 @@ export const webviewMessageHandler = async ( "sap-ai-core": {}, // kilocode_change chutes: {}, "nano-gpt": {}, // kilocode_change + poe: {}, // kilocode_change aihubmix: {}, // kilocode_change zenmux: {}, } @@ -1036,6 +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/src/shared/api.ts b/src/shared/api.ts index 8c33ff305c8..e938290b3e0 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -202,6 +202,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 4e288619eed..b28d147094e 100644 --- a/webview-ui/src/components/kilocode/hooks/__tests__/getModelsByProvider.spec.ts +++ b/webview-ui/src/components/kilocode/hooks/__tests__/getModelsByProvider.spec.ts @@ -43,6 +43,7 @@ describe("getModelsByProvider", () => { synthetic: { "test-model": testModel }, inception: { "test-model": testModel }, roo: { "test-model": testModel }, + poe: { "test-model": testModel }, aihubmix: { "test-model": testModel }, // kilocode_change zenmux: { "test-model": testModel }, } diff --git a/webview-ui/src/components/kilocode/hooks/useProviderModels.ts b/webview-ui/src/components/kilocode/hooks/useProviderModels.ts index 80f24f46a69..6d5c74d8ea3 100644 --- a/webview-ui/src/components/kilocode/hooks/useProviderModels.ts +++ b/webview-ui/src/components/kilocode/hooks/useProviderModels.ts @@ -53,6 +53,7 @@ import { cerebrasModels, cerebrasDefaultModelId, nanoGptDefaultModelId, //kilocode_change + poeDefaultModelId, //kilocode_change apertisDefaultModelId, // kilocode_change aihubmixDefaultModelId, // kilocode_change ovhCloudAiEndpointsDefaultModelId, @@ -255,6 +256,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 7005d797dd2..c9118bc6610 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -50,6 +50,7 @@ import { deepInfraDefaultModelId, minimaxDefaultModelId, nanoGptDefaultModelId, //kilocode_change + poeDefaultModelId, // kilocode_change } from "@roo-code/types" import { vscode } from "@src/utils/vscode" @@ -126,6 +127,7 @@ import { VercelAiGateway, DeepInfra, MiniMax, + Poe, // kilocode_change } from "./providers" import { MODELS_BY_PROVIDER, PROVIDERS } from "./constants" @@ -301,6 +303,7 @@ const ApiOptions = ({ selectedProvider === "deepinfra" || selectedProvider === "chutes" || // kilocode_change selectedProvider === "synthetic" || // kilocode_change + selectedProvider === "poe" || // kilocode_change selectedProvider === "roo" ) { vscode.postMessage({ type: "requestRouterModels" }) @@ -481,6 +484,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 } @@ -718,6 +722,19 @@ 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/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 2d73713fde8..11ce77e0d31 100644 --- a/webview-ui/src/components/settings/providers/index.ts +++ b/webview-ui/src/components/settings/providers/index.ts @@ -45,4 +45,5 @@ export { VercelAiGateway } from "./VercelAiGateway" export { DeepInfra } from "./DeepInfra" export { MiniMax } from "./MiniMax" export { Baseten } from "./Baseten" +export { Poe } from "./Poe" // kilocode_change export { Corethink } from "./Corethink" diff --git a/webview-ui/src/components/ui/hooks/useSelectedModel.ts b/webview-ui/src/components/ui/hooks/useSelectedModel.ts index fde71448a21..73705522778 100644 --- a/webview-ui/src/components/ui/hooks/useSelectedModel.ts +++ b/webview-ui/src/components/ui/hooks/useSelectedModel.ts @@ -424,6 +424,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/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/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index 79e4356acba..b791d497f76 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -448,6 +448,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/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 金鑰", diff --git a/webview-ui/src/utils/__tests__/validate.spec.ts b/webview-ui/src/utils/__tests__/validate.spec.ts index 935770ddc01..05e77096421 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: {}, // kilocode_change zenmux: {}, }