From 3d3b7992cccdf9bff0be867cc373799e3df786e9 Mon Sep 17 00:00:00 2001 From: Kamil Jopek Date: Thu, 26 Mar 2026 22:25:34 -0500 Subject: [PATCH 01/15] fix: clear all reasoning settings when switching models Previously only reasoningEffort was reset on model switch. Now also clears modelMaxTokens and modelMaxThinkingTokens so the new model's defaults take effect. Different models within the same provider can have different reasoning defaults/options. --- .../src/components/settings/utils/providerModelConfig.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/webview-ui/src/components/settings/utils/providerModelConfig.ts b/webview-ui/src/components/settings/utils/providerModelConfig.ts index fa718143905..59f76862b45 100644 --- a/webview-ui/src/components/settings/utils/providerModelConfig.ts +++ b/webview-ui/src/components/settings/utils/providerModelConfig.ts @@ -150,8 +150,10 @@ export const handleModelChangeSideEffects = ( setApiConfigurationField("awsCustomArn" as K, "" as ProviderSettings[K]) } - // All providers: Clear reasoning effort when switching models to allow - // the new model's default to take effect. Different models within the - // same provider can have different reasoning effort defaults/options. + // All providers: Clear reasoning settings when switching models to allow + // the new model's defaults to take effect. Different models within the + // same provider can have different reasoning defaults/options. setApiConfigurationField("reasoningEffort" as K, undefined as ProviderSettings[K]) + setApiConfigurationField("modelMaxTokens" as K, undefined as ProviderSettings[K]) + setApiConfigurationField("modelMaxThinkingTokens" as K, undefined as ProviderSettings[K]) } From a0765700a3aef087ed735d362ed1bab55a215b0d Mon Sep 17 00:00:00 2001 From: Kamil Jopek Date: Thu, 26 Mar 2026 22:26:17 -0500 Subject: [PATCH 02/15] feat(types): add Poe provider types and schema Add Poe as a dynamic provider with type definitions including: - Default model ID (claude-sonnet-4) and model info - Provider settings schema (poeApiKey, poeBaseUrl) - Integration into discriminated union, modelIdKeysByProvider, and MODELS_BY_PROVIDER registry --- packages/types/src/provider-settings.ts | 19 ++++++++++++++++++- packages/types/src/providers/index.ts | 4 ++++ packages/types/src/providers/poe.ts | 15 +++++++++++++++ 3 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 packages/types/src/providers/poe.ts diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts index 859792d7c36..43135577e16 100644 --- a/packages/types/src/provider-settings.ts +++ b/packages/types/src/provider-settings.ts @@ -34,7 +34,15 @@ export const DEFAULT_CONSECUTIVE_MISTAKE_LIMIT = 3 * Dynamic provider requires external API calls in order to get the model list. */ -export const dynamicProviders = ["openrouter", "vercel-ai-gateway", "litellm", "requesty", "roo", "unbound"] as const +export const dynamicProviders = [ + "openrouter", + "vercel-ai-gateway", + "litellm", + "requesty", + "roo", + "unbound", + "poe", +] as const export type DynamicProvider = (typeof dynamicProviders)[number] @@ -306,6 +314,11 @@ const deepSeekSchema = apiModelIdProviderModelSchema.extend({ deepSeekApiKey: z.string().optional(), }) +const poeSchema = apiModelIdProviderModelSchema.extend({ + poeApiKey: z.string().optional(), + poeBaseUrl: z.string().optional(), +}) + const moonshotSchema = apiModelIdProviderModelSchema.extend({ moonshotBaseUrl: z .union([z.literal("https://api.moonshot.ai/v1"), z.literal("https://api.moonshot.cn/v1")]) @@ -400,6 +413,7 @@ export const providerSettingsSchemaDiscriminated = z.discriminatedUnion("apiProv openAiNativeSchema.merge(z.object({ apiProvider: z.literal("openai-native") })), mistralSchema.merge(z.object({ apiProvider: z.literal("mistral") })), deepSeekSchema.merge(z.object({ apiProvider: z.literal("deepseek") })), + poeSchema.merge(z.object({ apiProvider: z.literal("poe") })), moonshotSchema.merge(z.object({ apiProvider: z.literal("moonshot") })), minimaxSchema.merge(z.object({ apiProvider: z.literal("minimax") })), requestySchema.merge(z.object({ apiProvider: z.literal("requesty") })), @@ -433,6 +447,7 @@ export const providerSettingsSchema = z.object({ ...openAiNativeSchema.shape, ...mistralSchema.shape, ...deepSeekSchema.shape, + ...poeSchema.shape, ...moonshotSchema.shape, ...minimaxSchema.shape, ...requestySchema.shape, @@ -510,6 +525,7 @@ export const modelIdKeysByProvider: Record = { moonshot: "apiModelId", minimax: "apiModelId", deepseek: "apiModelId", + poe: "apiModelId", "qwen-code": "apiModelId", requesty: "requestyModelId", unbound: "unboundModelId", @@ -632,6 +648,7 @@ export const MODELS_BY_PROVIDER: Record< baseten: { id: "baseten", label: "Baseten", models: Object.keys(basetenModels) }, // Dynamic providers; models pulled from remote APIs. + poe: { id: "poe", label: "Poe", models: [] }, litellm: { id: "litellm", label: "LiteLLM", models: [] }, openrouter: { id: "openrouter", label: "OpenRouter", models: [] }, requesty: { id: "requesty", label: "Requesty", models: [] }, diff --git a/packages/types/src/providers/index.ts b/packages/types/src/providers/index.ts index 6bb959c7056..6c180d5dda4 100644 --- a/packages/types/src/providers/index.ts +++ b/packages/types/src/providers/index.ts @@ -13,6 +13,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 "./qwen-code.js" export * from "./requesty.js" export * from "./roo.js" @@ -36,6 +37,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" @@ -107,6 +109,8 @@ export function getProviderDefaultModelId( return rooDefaultModelId case "qwen-code": return qwenCodeDefaultModelId + case "poe": + return poeDefaultModelId case "unbound": return unboundDefaultModelId case "vercel-ai-gateway": diff --git a/packages/types/src/providers/poe.ts b/packages/types/src/providers/poe.ts new file mode 100644 index 00000000000..1056d0c6031 --- /dev/null +++ b/packages/types/src/providers/poe.ts @@ -0,0 +1,15 @@ +import type { ModelInfo } from "../model.js" + +export const POE_DEFAULT_BASE_URL = "https://api.poe.com/v1" + +export const poeDefaultModelId = "claude-sonnet-4" + +export const poeDefaultModelInfo: ModelInfo = { + maxTokens: 8192, + contextWindow: 200_000, + supportsImages: true, + supportsPromptCache: true, + inputPrice: 3, + outputPrice: 15, + description: "Claude Sonnet 4 via Poe API", +} From 33f87f69a5612a4c95b2359a7a97b8ea0a582a24 Mon Sep 17 00:00:00 2001 From: Kamil Jopek Date: Thu, 26 Mar 2026 22:26:44 -0500 Subject: [PATCH 03/15] build: add ai-sdk-provider-poe dependency Add local link to ai-sdk-provider-poe SDK which provides the Poe AI provider integration for the Vercel AI SDK. --- pnpm-lock.yaml | 6 +++++- src/package.json | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d95c2f02346..ee0ca3d8633 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -818,6 +818,9 @@ importers: '@vscode/codicons': specifier: ^0.0.36 version: 0.0.36 + ai-sdk-provider-poe: + specifier: link:/Users/kjopek/Workspace/roo-code-extension/ai-sdk-provider-poe + version: link:../../ai-sdk-provider-poe async-mutex: specifier: ^0.5.0 version: 0.5.0 @@ -8976,6 +8979,7 @@ packages: prebuild-install@7.1.3: resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} engines: {node: '>=10'} + deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available. hasBin: true prelude-ls@1.2.1: @@ -14974,7 +14978,7 @@ snapshots: sirv: 3.0.1 tinyglobby: 0.2.14 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.2.1)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@20.17.50)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0) '@vitest/utils@3.2.4': dependencies: diff --git a/src/package.json b/src/package.json index 7c4889abd89..845c5a284c1 100644 --- a/src/package.json +++ b/src/package.json @@ -471,6 +471,7 @@ "@roo-code/telemetry": "workspace:^", "@roo-code/types": "workspace:^", "@vscode/codicons": "^0.0.36", + "ai-sdk-provider-poe": "link:/Users/kjopek/Workspace/roo-code-extension/ai-sdk-provider-poe", "async-mutex": "^0.5.0", "axios": "^1.12.0", "cheerio": "^1.0.0", From d2a74186d3720641754ffb02d9bf3532d5cbb3bd Mon Sep 17 00:00:00 2001 From: Kamil Jopek Date: Thu, 26 Mar 2026 22:27:02 -0500 Subject: [PATCH 04/15] feat: add Poe provider handler and model fetcher Implement PoeHandler using the ai-sdk-provider-poe SDK with support for: - Streaming text generation via Vercel AI SDK - Reasoning budget (Anthropic-style thinking) and reasoning effort (OpenAI-style) based on model capabilities - Dynamic model fetching from the Poe API with caching - Tool calling with OpenAI-compatible schema conversion - Error handling with telemetry capture Wire the handler into the API factory and model cache router. --- src/api/index.ts | 3 + src/api/providers/fetchers/modelCache.ts | 4 + src/api/providers/fetchers/poe.ts | 44 +++++++ src/api/providers/index.ts | 1 + src/api/providers/poe.ts | 152 +++++++++++++++++++++++ src/shared/api.ts | 1 + 6 files changed, 205 insertions(+) create mode 100644 src/api/providers/fetchers/poe.ts create mode 100644 src/api/providers/poe.ts diff --git a/src/api/index.ts b/src/api/index.ts index ebc2682a1a8..1891113c03b 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -9,6 +9,7 @@ import { AnthropicHandler, AwsBedrockHandler, OpenRouterHandler, + PoeHandler, VertexHandler, AnthropicVertexHandler, OpenAiHandler, @@ -176,6 +177,8 @@ export function buildApiHandler(configuration: ProviderSettings): ApiHandler { return new MiniMaxHandler(options) case "baseten": return new BasetenHandler(options) + case "poe": + return new PoeHandler(options) default: return new AnthropicHandler(options) } diff --git a/src/api/providers/fetchers/modelCache.ts b/src/api/providers/fetchers/modelCache.ts index a574a660bc5..a2c98e49caf 100644 --- a/src/api/providers/fetchers/modelCache.ts +++ b/src/api/providers/fetchers/modelCache.ts @@ -24,6 +24,7 @@ import { getLiteLLMModels } from "./litellm" import { GetModelsOptions } from "../../../shared/api" import { getOllamaModels } from "./ollama" import { getLMStudioModels } from "./lmstudio" +import { getPoeModels } from "./poe" import { getRooModels } from "./roo" const memoryCache = new NodeCache({ stdTTL: 5 * 60, checkperiod: 5 * 60 }) @@ -91,6 +92,9 @@ async function fetchModelsFromProvider(options: GetModelsOptions): Promise { + try { + // fetchPoeModels populates the internal model store, then getModels() + // returns only code-capable models with camelCase fields. + await fetchPoeModels({ apiKey, baseURL }) + const poeModels = getModels() + const models: ModelRecord = {} + + for (const m of poeModels) { + // The library's applyReasoningFallbacks workaround sets + // supportsReasoningEffort to boolean `true` for any model that + // supports /v1/responses, even when the model has no actual + // reasoning capability (e.g. Haiku 3/3.5). Only trust the value + // when it is an explicit array of effort levels. + const effort = Array.isArray(m.supportsReasoningEffort) ? m.supportsReasoningEffort : undefined + const info: ModelInfo = { + contextWindow: m.contextWindow, + maxTokens: m.maxOutputTokens, + supportsImages: m.supportsImages, + supportsPromptCache: m.supportsPromptCache, + ...(m.supportsReasoningBudget && { supportsReasoningBudget: m.supportsReasoningBudget }), + ...(effort && { + supportsReasoningEffort: effort as ModelInfo["supportsReasoningEffort"], + }), + ...(m.pricing?.inputPerMillion != null && { inputPrice: m.pricing.inputPerMillion }), + ...(m.pricing?.outputPerMillion != null && { outputPrice: m.pricing.outputPerMillion }), + ...(m.pricing?.cacheReadPerMillion != null && { cacheReadsPrice: m.pricing.cacheReadPerMillion }), + ...(m.pricing?.cacheWritePerMillion != null && { cacheWritesPrice: m.pricing.cacheWritePerMillion }), + } + + models[m.id] = info + } + + return models + } catch (error) { + console.error( + `[Poe] Error fetching models: ${JSON.stringify(error, Object.getOwnPropertyNames(error as object), 2)}`, + ) + return {} + } +} diff --git a/src/api/providers/index.ts b/src/api/providers/index.ts index b6de7952104..41aff953d43 100644 --- a/src/api/providers/index.ts +++ b/src/api/providers/index.ts @@ -14,6 +14,7 @@ export { OpenAiHandler } from "./openai" export { OpenAICompatibleHandler } from "./openai-compatible" export type { OpenAICompatibleConfig } from "./openai-compatible" export { OpenRouterHandler } from "./openrouter" +export { PoeHandler } from "./poe" 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..46a912fb003 --- /dev/null +++ b/src/api/providers/poe.ts @@ -0,0 +1,152 @@ +import { Anthropic } from "@anthropic-ai/sdk" +import { createPoe, type PoeProvider, type PoeScopedProviderOptions } from "ai-sdk-provider-poe" +import { extractUsageMetrics, mapToolChoice } from "ai-sdk-provider-poe/code" +import { streamText, generateText, type ToolSet } from "ai" + +import { poeDefaultModelId, poeDefaultModelInfo, type ReasoningEffortExtended, ApiProviderError } from "@roo-code/types" +import { TelemetryService } from "@roo-code/telemetry" + +import { shouldUseReasoningBudget, shouldUseReasoningEffort, type ApiHandlerOptions } from "../../shared/api" + +import { convertToAiSdkMessages, convertToolsForAiSdk, processAiSdkStreamPart } from "../transform/ai-sdk" +import { ApiStream } from "../transform/stream" + +import { BaseProvider } from "./base-provider" +import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" +import { getModelsFromCache } from "./fetchers/modelCache" + +const DEFAULT_THINKING_BUDGET = 8192 + +export class PoeHandler extends BaseProvider implements SingleCompletionHandler { + protected options: ApiHandlerOptions + private poe: PoeProvider + + constructor(options: ApiHandlerOptions) { + super() + this.options = options + this.poe = createPoe({ + apiKey: options.poeApiKey ?? "not-provided", + baseURL: options.poeBaseUrl || undefined, + }) + } + + override getModel() { + const id = this.options.apiModelId ?? poeDefaultModelId + const cached = getModelsFromCache("poe") + const info = cached?.[id] ?? poeDefaultModelInfo + return { id, info } + } + + override async *createMessage( + systemPrompt: string, + messages: Anthropic.Messages.MessageParam[], + metadata?: ApiHandlerCreateMessageMetadata, + ): ApiStream { + const { id, info } = this.getModel() + const languageModel = this.poe(id) + + const aiSdkMessages = convertToAiSdkMessages(messages) + const openAiTools = this.convertToolsForOpenAI(metadata?.tools) + const aiSdkTools = convertToolsForAiSdk(openAiTools) as ToolSet | undefined + + const useBudget = shouldUseReasoningBudget({ model: info, settings: this.options }) + const useEffort = !useBudget && shouldUseReasoningEffort({ model: info, settings: this.options }) + + // Only pass temperature when the user explicitly configured it. + let temperature: number | undefined = this.options.modelTemperature ?? undefined + // Only pass maxOutputTokens when reasoning is active and the user configured it via the UI toggles. + const maxOutputTokens: number | undefined = + (useBudget || useEffort) && this.options.modelMaxTokens ? this.options.modelMaxTokens : undefined + const providerOptions: NonNullable[0]["providerOptions"]> & { + poe?: PoeScopedProviderOptions + } = {} + + if (useBudget) { + const requestedBudget = this.options.modelMaxThinkingTokens ?? DEFAULT_THINKING_BUDGET + providerOptions.poe = { + reasoningBudgetTokens: requestedBudget, + } + temperature = 1.0 + } else if (useEffort) { + let effort = (this.options.reasoningEffort ?? info.reasoningEffort ?? "medium") as ReasoningEffortExtended + // Validate that the effort level is actually supported by the current model + const supportedEfforts = info.supportsReasoningEffort + if (Array.isArray(supportedEfforts) && !supportedEfforts.includes(effort as any)) { + effort = (info.reasoningEffort as ReasoningEffortExtended) ?? "medium" + } + providerOptions.poe = { + reasoningEffort: effort, + reasoningSummary: "auto", + } + } + + let result + try { + result = streamText({ + model: languageModel, + system: systemPrompt, + messages: aiSdkMessages, + temperature, + maxOutputTokens, + tools: aiSdkTools, + toolChoice: mapToolChoice(metadata?.tool_choice as any), + ...(Object.keys(providerOptions).length > 0 && { providerOptions }), + }) + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + TelemetryService.instance.captureException(new ApiProviderError(errorMessage, "poe", id, "createMessage")) + throw new Error(`Poe completion error: ${errorMessage}`) + } + + try { + let sawReasoning = false + for await (const part of result.fullStream) { + for (const chunk of processAiSdkStreamPart(part)) { + if (chunk.type === "reasoning" && chunk.text.trim().length > 0) { + sawReasoning = true + } + yield chunk + } + } + + if (!sawReasoning) { + const reasoningText = await result.reasoningText + if (reasoningText?.trim()) { + yield { type: "reasoning", text: reasoningText } + } + } + + const usage = await result.usage + if (usage) { + const metrics = extractUsageMetrics(usage as any) + yield { + type: "usage" as const, + inputTokens: metrics.inputTokens, + outputTokens: metrics.outputTokens, + cacheReadTokens: metrics.cacheReadTokens, + cacheWriteTokens: metrics.cacheWriteTokens, + reasoningTokens: metrics.reasoningTokens, + } + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + TelemetryService.instance.captureException(new ApiProviderError(errorMessage, "poe", id, "createMessage")) + throw new Error(`Poe streaming error: ${errorMessage}`) + } + } + + async completePrompt(prompt: string): Promise { + const { id } = this.getModel() + try { + const { text } = await generateText({ + model: this.poe(id), + prompt, + }) + return text + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + TelemetryService.instance.captureException(new ApiProviderError(errorMessage, "poe", id, "completePrompt")) + throw new Error(`Poe completion error: ${errorMessage}`) + } + } +} diff --git a/src/shared/api.ts b/src/shared/api.ts index 52af6b20727..a68abcc3adc 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -177,6 +177,7 @@ const dynamicProviderExtras = { ollama: {} as {}, // eslint-disable-line @typescript-eslint/no-empty-object-type lmstudio: {} as {}, // eslint-disable-line @typescript-eslint/no-empty-object-type roo: {} as { apiKey?: string; baseUrl?: string }, + poe: {} as { apiKey?: string; baseUrl?: string }, } as const satisfies Record // Build the dynamic options union from the map, intersected with CommonFetchParams From 88cc81b6c9077fab538f385127c050c1fa4a49d8 Mon Sep 17 00:00:00 2001 From: Kamil Jopek Date: Thu, 26 Mar 2026 22:27:29 -0500 Subject: [PATCH 05/15] feat: wire Poe model fetching into webview message handler Add Poe to the router model fetching pipeline so models are fetched when a Poe API key is configured. Supports both automatic fetch on provider selection and manual refresh with key/baseUrl overrides. --- src/api/providers/poe.ts | 10 +++++++--- src/core/webview/webviewMessageHandler.ts | 16 ++++++++++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/api/providers/poe.ts b/src/api/providers/poe.ts index 46a912fb003..f0d5b2f1851 100644 --- a/src/api/providers/poe.ts +++ b/src/api/providers/poe.ts @@ -54,15 +54,16 @@ export class PoeHandler extends BaseProvider implements SingleCompletionHandler // Only pass temperature when the user explicitly configured it. let temperature: number | undefined = this.options.modelTemperature ?? undefined - // Only pass maxOutputTokens when reasoning is active and the user configured it via the UI toggles. - const maxOutputTokens: number | undefined = - (useBudget || useEffort) && this.options.modelMaxTokens ? this.options.modelMaxTokens : undefined + let maxOutputTokens: number | undefined const providerOptions: NonNullable[0]["providerOptions"]> & { poe?: PoeScopedProviderOptions } = {} if (useBudget) { const requestedBudget = this.options.modelMaxThinkingTokens ?? DEFAULT_THINKING_BUDGET + // maxOutputTokens is the text-only budget; reasoningBudgetTokens is + // separate, so total output = maxOutputTokens + reasoningBudgetTokens. + maxOutputTokens = this.options.modelMaxTokens ?? Math.max(0, (info.maxTokens ?? 0) - requestedBudget) providerOptions.poe = { reasoningBudgetTokens: requestedBudget, } @@ -78,6 +79,9 @@ export class PoeHandler extends BaseProvider implements SingleCompletionHandler reasoningEffort: effort, reasoningSummary: "auto", } + if (this.options.modelMaxTokens) { + maxOutputTokens = this.options.modelMaxTokens + } } let result diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index d27fd6bec09..e3b8c1bea88 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -955,6 +955,7 @@ export const webviewMessageHandler = async ( ollama: {}, lmstudio: {}, roo: {}, + poe: {}, } const safeGetModels = async (options: GetModelsOptions): Promise => { @@ -1018,6 +1019,21 @@ export const webviewMessageHandler = async ( }) } + // Poe is conditional on apiKey + const poeApiKey = apiConfiguration.poeApiKey || message?.values?.poeApiKey + const poeBaseUrl = apiConfiguration.poeBaseUrl || message?.values?.poeBaseUrl + + if (poeApiKey) { + if (message?.values?.poeApiKey || message?.values?.poeBaseUrl) { + await flushModels({ provider: "poe", apiKey: poeApiKey, baseUrl: poeBaseUrl }, true) + } + + candidates.push({ + key: "poe", + options: { provider: "poe", apiKey: poeApiKey, baseUrl: poeBaseUrl }, + }) + } + // Apply single provider filter if specified const modelFetchPromises = providerFilter ? candidates.filter(({ key }) => key === providerFilter) From d92ec7a82e42967c1d8239437d55410fe79fa692 Mon Sep 17 00:00:00 2001 From: Kamil Jopek Date: Thu, 26 Mar 2026 22:30:39 -0500 Subject: [PATCH 06/15] feat(ui): add Poe provider settings UI Add Poe settings component with API key input, base URL configuration, model picker with dynamic model list from the Poe API, and manual model refresh. Integrate into ApiOptions provider selection and the useSelectedModel hook. --- .../src/components/settings/ApiOptions.tsx | 30 +++- .../src/components/settings/constants.ts | 1 + .../src/components/settings/providers/Poe.tsx | 161 ++++++++++++++++++ .../components/settings/providers/index.ts | 1 + .../components/ui/hooks/useSelectedModel.ts | 5 + webview-ui/src/i18n/locales/en/settings.json | 3 + 6 files changed, 199 insertions(+), 2 deletions(-) create mode 100644 webview-ui/src/components/settings/providers/Poe.tsx diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index 4d914a4833a..a6e4cc3f5f6 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -1,7 +1,7 @@ import React, { memo, useCallback, useEffect, useMemo, useState } from "react" import { convertHeadersToObject } from "./utils/headers" import { useDebounce } from "react-use" -import { VSCodeLink } from "@vscode/webview-ui-toolkit/react" +import { VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react" import { ExternalLinkIcon } from "@radix-ui/react-icons" import { @@ -10,6 +10,7 @@ import { isRetiredProvider, DEFAULT_CONSECUTIVE_MISTAKE_LIMIT, openRouterDefaultModelId, + poeDefaultModelId, requestyDefaultModelId, litellmDefaultModelId, openAiNativeDefaultModelId, @@ -80,6 +81,7 @@ import { OpenAICompatible, OpenAICodex, OpenRouter, + Poe, QwenCode, Requesty, Roo, @@ -238,7 +240,7 @@ const ApiOptions = ({ vscode.postMessage({ type: "requestLmStudioModels" }) } else if (selectedProvider === "vscode-lm") { vscode.postMessage({ type: "requestVsCodeLmModels" }) - } else if (selectedProvider === "litellm" || selectedProvider === "roo") { + } else if (selectedProvider === "litellm" || selectedProvider === "roo" || selectedProvider === "poe") { vscode.postMessage({ type: "requestRouterModels" }) } }, @@ -252,6 +254,8 @@ const ApiOptions = ({ apiConfiguration?.lmStudioBaseUrl, apiConfiguration?.litellmBaseUrl, apiConfiguration?.litellmApiKey, + apiConfiguration?.poeApiKey, + apiConfiguration?.poeBaseUrl, customHeaders, ], ) @@ -356,6 +360,7 @@ const ApiOptions = ({ : internationalZAiDefaultModelId, }, fireworks: { field: "apiModelId", default: fireworksDefaultModelId }, + poe: { field: "apiModelId", default: poeDefaultModelId }, roo: { field: "apiModelId", default: rooDefaultModelId }, "vercel-ai-gateway": { field: "vercelAiGatewayModelId", default: vercelAiGatewayDefaultModelId }, openai: { field: "openAiModelId" }, @@ -703,6 +708,16 @@ const ApiOptions = ({ /> )} + {selectedProvider === "poe" && ( + + )} + {selectedProvider === "roo" && ( setApiConfigurationField("consecutiveMistakeLimit", value)} /> + {selectedProvider === "poe" && ( + + + + )} {selectedProvider === "openrouter" && openRouterModelProviders && Object.keys(openRouterModelProviders).length > 0 && ( diff --git a/webview-ui/src/components/settings/constants.ts b/webview-ui/src/components/settings/constants.ts index 46789cb67a6..14f04cb5b22 100644 --- a/webview-ui/src/components/settings/constants.ts +++ b/webview-ui/src/components/settings/constants.ts @@ -65,4 +65,5 @@ export const PROVIDERS = [ { value: "minimax", label: "MiniMax", proxy: false }, { value: "baseten", label: "Baseten", proxy: false }, { value: "unbound", label: "Unbound", proxy: false }, + { value: "poe", label: "Poe", proxy: false }, ].sort((a, b) => 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..741d8971c37 --- /dev/null +++ b/webview-ui/src/components/settings/providers/Poe.tsx @@ -0,0 +1,161 @@ +import { useCallback, useState, useEffect, useRef } from "react" +import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react" + +import { + type ProviderSettings, + type OrganizationAllowList, + type ExtensionMessage, + poeDefaultModelId, + type ProviderName, +} from "@roo-code/types" + +import { RouterName } from "@roo/api" + +import { useAppTranslation } from "@src/i18n/TranslationContext" +import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink" +import { useExtensionState } from "@src/context/ExtensionStateContext" +import { vscode } from "@src/utils/vscode" +import { Button } from "@src/components/ui" + +import { inputEventTransform } from "../transforms" +import { ModelPicker } from "../ModelPicker" +import { handleModelChangeSideEffects } from "../utils/providerModelConfig" + +type PoeProps = { + apiConfiguration: ProviderSettings + setApiConfigurationField: (field: keyof ProviderSettings, value: ProviderSettings[keyof ProviderSettings]) => void + organizationAllowList: OrganizationAllowList + modelValidationError?: string + simplifySettings?: boolean +} + +export const Poe = ({ + apiConfiguration, + setApiConfigurationField, + organizationAllowList, + modelValidationError, + simplifySettings, +}: PoeProps) => { + const { t } = useAppTranslation() + const { routerModels } = useExtensionState() + const [refreshStatus, setRefreshStatus] = useState<"idle" | "loading" | "success" | "error">("idle") + const [refreshError, setRefreshError] = useState() + const poeErrorJustReceived = useRef(false) + + useEffect(() => { + const handleMessage = (event: MessageEvent) => { + const message = event.data + if (message.type === "singleRouterModelFetchResponse" && !message.success) { + const providerName = message.values?.provider as RouterName + if (providerName === "poe") { + poeErrorJustReceived.current = true + setRefreshStatus("error") + setRefreshError(message.error) + } + } else if (message.type === "routerModels") { + if (refreshStatus === "loading") { + if (!poeErrorJustReceived.current) { + setRefreshStatus("success") + } + } + } + } + + window.addEventListener("message", handleMessage) + return () => { + window.removeEventListener("message", handleMessage) + } + }, [refreshStatus]) + + const handleInputChange = useCallback( + ( + field: K, + transform: (event: E) => ProviderSettings[K] = inputEventTransform, + ) => + (event: E | Event) => { + setApiConfigurationField(field, transform(event as E)) + }, + [setApiConfigurationField], + ) + + const handleRefreshModels = useCallback(() => { + poeErrorJustReceived.current = false + setRefreshStatus("loading") + setRefreshError(undefined) + + const key = apiConfiguration.poeApiKey + + if (!key) { + setRefreshStatus("error") + setRefreshError(t("settings:providers.refreshModels.missingConfig")) + return + } + + vscode.postMessage({ + type: "requestRouterModels", + values: { poeApiKey: key, poeBaseUrl: apiConfiguration.poeBaseUrl }, + }) + }, [apiConfiguration, t]) + + return ( + <> + + + +
+ {t("settings:providers.apiKeyStorageNotice")} +
+ {!apiConfiguration?.poeApiKey && ( + + {t("settings:providers.getPoeApiKey")} + + )} + + {refreshStatus === "loading" && ( +
+ {t("settings:providers.refreshModels.loading")} +
+ )} + {refreshStatus === "success" && ( +
{t("settings:providers.refreshModels.success")}
+ )} + {refreshStatus === "error" && ( +
+ {refreshError || t("settings:providers.refreshModels.error")} +
+ )} + + handleModelChangeSideEffects("poe" as ProviderName, modelId, setApiConfigurationField) + } + /> + + ) +} diff --git a/webview-ui/src/components/settings/providers/index.ts b/webview-ui/src/components/settings/providers/index.ts index 597caffd1d7..4a64ce9586b 100644 --- a/webview-ui/src/components/settings/providers/index.ts +++ b/webview-ui/src/components/settings/providers/index.ts @@ -10,6 +10,7 @@ export { OpenAI } from "./OpenAI" export { OpenAICodex } from "./OpenAICodex" export { OpenAICompatible } from "./OpenAICompatible" export { OpenRouter } from "./OpenRouter" +export { Poe } from "./Poe" export { QwenCode } from "./QwenCode" export { Roo } from "./Roo" export { Requesty } from "./Requesty" diff --git a/webview-ui/src/components/ui/hooks/useSelectedModel.ts b/webview-ui/src/components/ui/hooks/useSelectedModel.ts index c32a08990c8..7192d9d4ee4 100644 --- a/webview-ui/src/components/ui/hooks/useSelectedModel.ts +++ b/webview-ui/src/components/ui/hooks/useSelectedModel.ts @@ -317,6 +317,11 @@ function getSelectedModel({ const info = routerModels.roo?.[id] return { id, info } } + case "poe": { + const id = apiConfiguration.apiModelId ?? defaultModelId + const info = routerModels.poe?.[id] + return { id, info } + } case "qwen-code": { const id = apiConfiguration.apiModelId ?? defaultModelId const info = qwenCodeModels[id as keyof typeof qwenCodeModels] diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index 3b2497aaee7..183cd663e31 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -443,6 +443,9 @@ "vertex1MContextBetaDescription": "Extends context window to 1 million tokens for Claude Sonnet 4.x / Claude Opus 4.6", "basetenApiKey": "Baseten API Key", "getBasetenApiKey": "Get Baseten API Key", + "poeApiKey": "Poe API Key", + "getPoeApiKey": "Get Poe API Key", + "poeBaseUrl": "Poe Base URL", "fireworksApiKey": "Fireworks API Key", "getFireworksApiKey": "Get Fireworks API Key", "deepSeekApiKey": "DeepSeek API Key", From 35ca23191dcc5da53c8c2e82cdeef203be716187 Mon Sep 17 00:00:00 2001 From: Kamil Jopek Date: Thu, 26 Mar 2026 22:30:56 -0500 Subject: [PATCH 07/15] test: add tests for Poe provider Add unit tests for PoeHandler (constructor, getModel, createMessage, reasoning budget/effort, completePrompt) and getPoeModels fetcher (model conversion, optional fields, reasoning effort mapping). Update existing test fixtures to include the poe router entry. --- src/api/providers/__tests__/poe.spec.ts | 415 ++++++++++++++++++ .../providers/fetchers/__tests__/poe.spec.ts | 131 ++++++ .../webview/__tests__/ClineProvider.spec.ts | 3 + .../__tests__/webviewMessageHandler.spec.ts | 3 + .../src/utils/__tests__/validate.spec.ts | 1 + 5 files changed, 553 insertions(+) create mode 100644 src/api/providers/__tests__/poe.spec.ts create mode 100644 src/api/providers/fetchers/__tests__/poe.spec.ts diff --git a/src/api/providers/__tests__/poe.spec.ts b/src/api/providers/__tests__/poe.spec.ts new file mode 100644 index 00000000000..418b0eadb08 --- /dev/null +++ b/src/api/providers/__tests__/poe.spec.ts @@ -0,0 +1,415 @@ +const mockStreamText = vitest.fn() +const mockGenerateText = vitest.fn() +const mockCreatePoe = vitest.fn() + +vitest.mock("ai-sdk-provider-poe", () => ({ + createPoe: (...args: unknown[]) => mockCreatePoe(...args), +})) + +vitest.mock("ai-sdk-provider-poe/code", () => ({ + mapToolChoice: vitest.fn((value: unknown) => value), + extractUsageMetrics: vitest.fn((usage: any) => ({ + inputTokens: usage?.inputTokens || 0, + outputTokens: usage?.outputTokens || 0, + cacheReadTokens: usage?.cacheReadTokens, + cacheWriteTokens: usage?.cacheWriteTokens, + reasoningTokens: usage?.reasoningTokens, + })), +})) + +vitest.mock("ai", async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + streamText: (...args: unknown[]) => mockStreamText(...args), + generateText: (...args: unknown[]) => mockGenerateText(...args), + } +}) + +vitest.mock("../fetchers/modelCache", () => ({ + getModelsFromCache: vitest.fn().mockReturnValue({ + "anthropic/claude-sonnet-4": { + maxTokens: 10_000, + contextWindow: 200_000, + supportsImages: true, + supportsPromptCache: true, + supportsReasoningBudget: true, + inputPrice: 3, + outputPrice: 15, + }, + "openai/gpt-4o": { + maxTokens: 16_384, + contextWindow: 128_000, + supportsImages: true, + supportsPromptCache: false, + inputPrice: 2.5, + outputPrice: 10, + }, + "openai/o3": { + maxTokens: 100_000, + contextWindow: 200_000, + supportsImages: true, + supportsPromptCache: false, + supportsReasoningEffort: ["low", "medium", "high"], + inputPrice: 10, + outputPrice: 40, + }, + }), +})) + +import type { Anthropic } from "@anthropic-ai/sdk" +import { poeDefaultModelId, poeDefaultModelInfo } from "@roo-code/types" +import { PoeHandler } from "../poe" +import type { ApiHandlerOptions } from "../../../shared/api" + +describe("PoeHandler", () => { + const mockLanguageModel = { modelId: "test-model" } + const mockPoeProvider = vitest.fn().mockReturnValue(mockLanguageModel) + + beforeEach(() => { + vitest.clearAllMocks() + mockCreatePoe.mockReturnValue(mockPoeProvider) + }) + + describe("constructor", () => { + it("creates poe provider with api key and default base URL", () => { + new PoeHandler({ poeApiKey: "test-key" }) + + expect(mockCreatePoe).toHaveBeenCalledWith({ + apiKey: "test-key", + baseURL: undefined, + }) + }) + + it("creates poe provider with custom base URL", () => { + new PoeHandler({ poeApiKey: "key", poeBaseUrl: "https://custom.poe.com/v1" }) + + expect(mockCreatePoe).toHaveBeenCalledWith({ + apiKey: "key", + baseURL: "https://custom.poe.com/v1", + }) + }) + + it("uses fallback api key when not provided", () => { + new PoeHandler({}) + + expect(mockCreatePoe).toHaveBeenCalledWith({ + apiKey: "not-provided", + baseURL: undefined, + }) + }) + }) + + describe("getModel", () => { + it("returns model info from cache", () => { + const handler = new PoeHandler({ poeApiKey: "key", apiModelId: "anthropic/claude-sonnet-4" }) + const result = handler.getModel() + + expect(result.id).toBe("anthropic/claude-sonnet-4") + expect(result.info.contextWindow).toBe(200_000) + expect(result.info.maxTokens).toBe(10_000) + }) + + it("returns default model when no model ID specified", () => { + const handler = new PoeHandler({ poeApiKey: "key" }) + const result = handler.getModel() + + expect(result.id).toBe(poeDefaultModelId) + }) + + it("falls back to default model info when model not in cache", () => { + const handler = new PoeHandler({ poeApiKey: "key", apiModelId: "unknown/model" }) + const result = handler.getModel() + + expect(result.id).toBe("unknown/model") + expect(result.info).toEqual(poeDefaultModelInfo) + }) + }) + + describe("createMessage", () => { + it("streams text chunks", async () => { + const handler = new PoeHandler({ poeApiKey: "key", apiModelId: "anthropic/claude-sonnet-4" }) + + const fullStream = (async function* () { + yield { type: "text-delta", text: "Hello " } + yield { type: "text-delta", text: "world!" } + })() + + mockStreamText.mockReturnValue({ + fullStream, + usage: Promise.resolve({ inputTokens: 10, outputTokens: 5 }), + }) + + const chunks = [] + for await (const chunk of handler.createMessage("system prompt", [ + { role: "user" as const, content: "Hi" }, + ])) { + chunks.push(chunk) + } + + expect(chunks).toContainEqual({ type: "text", text: "Hello " }) + expect(chunks).toContainEqual({ type: "text", text: "world!" }) + expect(chunks).toContainEqual(expect.objectContaining({ type: "usage", inputTokens: 10, outputTokens: 5 })) + }) + + it("passes tools and tool_choice to streamText", async () => { + const handler = new PoeHandler({ poeApiKey: "key", apiModelId: "openai/gpt-4o" }) + + const fullStream = (async function* () { + yield { type: "text-delta", text: "ok" } + })() + + mockStreamText.mockReturnValue({ + fullStream, + usage: Promise.resolve({ inputTokens: 1, outputTokens: 1 }), + }) + + const tools = [ + { + type: "function" as const, + function: { + name: "read_file", + description: "Read a file", + parameters: { type: "object", properties: { path: { type: "string" } }, required: ["path"] }, + }, + }, + ] + + const iterator = handler.createMessage("system", [{ role: "user" as const, content: "read file" }], { + taskId: "test", + tools, + tool_choice: "auto" as any, + }) + + // Consume stream + for await (const _ of iterator) { + /* drain */ + } + + expect(mockStreamText).toHaveBeenCalledWith( + expect.objectContaining({ + model: mockLanguageModel, + system: "system", + tools: expect.any(Object), + }), + ) + }) + + it("calls poe provider with correct model ID", async () => { + const handler = new PoeHandler({ poeApiKey: "key", apiModelId: "openai/gpt-4o" }) + + const fullStream = (async function* () {})() + mockStreamText.mockReturnValue({ + fullStream, + usage: Promise.resolve({ inputTokens: 0, outputTokens: 0 }), + }) + + for await (const _ of handler.createMessage("sys", [{ role: "user" as const, content: "hi" }])) { + /* drain */ + } + + expect(mockPoeProvider).toHaveBeenCalledWith("openai/gpt-4o") + }) + }) + + describe("reasoning", () => { + it("passes anthropic thinking config for budget models", async () => { + const handler = new PoeHandler({ + poeApiKey: "key", + apiModelId: "anthropic/claude-sonnet-4", + enableReasoningEffort: true, + modelMaxThinkingTokens: 4096, + }) + + const fullStream = (async function* () { + yield { type: "reasoning", text: "Let me think..." } + yield { type: "text-delta", text: "Answer" } + })() + + mockStreamText.mockReturnValue({ + fullStream, + usage: Promise.resolve({ inputTokens: 10, outputTokens: 5 }), + }) + + const chunks = [] + const modelMaxTokens = handler.getModel().info.maxTokens ?? 0 + for await (const chunk of handler.createMessage("system", [ + { role: "user" as const, content: "think about this" }, + ])) { + chunks.push(chunk) + } + + const callArgs = mockStreamText.mock.calls[0][0] + expect(callArgs.temperature).toBe(1.0) + expect(callArgs.providerOptions).toEqual({ + poe: { + reasoningBudgetTokens: 4096, + }, + }) + expect(callArgs.maxOutputTokens).toBe(modelMaxTokens - 4096) + + expect(chunks).toContainEqual({ type: "reasoning", text: "Let me think..." }) + expect(chunks).toContainEqual({ type: "text", text: "Answer" }) + }) + + it("passes openai reasoning effort for effort models", async () => { + const handler = new PoeHandler({ + poeApiKey: "key", + apiModelId: "openai/o3", + enableReasoningEffort: true, + reasoningEffort: "high", + }) + + const fullStream = (async function* () { + yield { type: "text-delta", text: "Answer" } + })() + + mockStreamText.mockReturnValue({ + fullStream, + usage: Promise.resolve({ inputTokens: 10, outputTokens: 5 }), + }) + + for await (const _ of handler.createMessage("system", [{ role: "user" as const, content: "reason" }])) { + /* drain */ + } + + expect(mockStreamText).toHaveBeenCalledWith( + expect.objectContaining({ + providerOptions: { + poe: { + reasoningEffort: "high", + reasoningSummary: "auto", + }, + }, + }), + ) + }) + + it("emits final reasoning text when the stream has no reasoning chunks", async () => { + const handler = new PoeHandler({ + poeApiKey: "key", + apiModelId: "openai/o3", + enableReasoningEffort: true, + reasoningEffort: "high", + }) + + const fullStream = (async function* () { + yield { type: "text-delta", text: "Answer" } + })() + + mockStreamText.mockReturnValue({ + fullStream, + reasoningText: Promise.resolve("Condensed reasoning"), + usage: Promise.resolve({ inputTokens: 10, outputTokens: 5 }), + }) + + const chunks = [] + for await (const chunk of handler.createMessage("system", [{ role: "user" as const, content: "reason" }])) { + chunks.push(chunk) + } + + expect(chunks).toContainEqual({ type: "text", text: "Answer" }) + expect(chunks).toContainEqual({ type: "reasoning", text: "Condensed reasoning" }) + }) + + it("does not duplicate reasoning when the stream already contains reasoning chunks", async () => { + const handler = new PoeHandler({ + poeApiKey: "key", + apiModelId: "openai/o3", + enableReasoningEffort: true, + reasoningEffort: "high", + }) + + const fullStream = (async function* () { + yield { type: "reasoning-delta", text: "Live reasoning" } + yield { type: "text-delta", text: "Answer" } + })() + + mockStreamText.mockReturnValue({ + fullStream, + reasoningText: Promise.resolve("Condensed reasoning"), + usage: Promise.resolve({ inputTokens: 10, outputTokens: 5 }), + }) + + const chunks = [] + for await (const chunk of handler.createMessage("system", [{ role: "user" as const, content: "reason" }])) { + chunks.push(chunk) + } + + expect(chunks.filter((chunk) => chunk.type === "reasoning")).toEqual([ + { type: "reasoning", text: "Live reasoning" }, + ]) + }) + + it("does not pass providerOptions when reasoning is disabled", async () => { + const handler = new PoeHandler({ + poeApiKey: "key", + apiModelId: "anthropic/claude-sonnet-4", + enableReasoningEffort: false, + }) + + const fullStream = (async function* () { + yield { type: "text-delta", text: "Answer" } + })() + + mockStreamText.mockReturnValue({ + fullStream, + usage: Promise.resolve({ inputTokens: 1, outputTokens: 1 }), + }) + + for await (const _ of handler.createMessage("system", [{ role: "user" as const, content: "hi" }])) { + /* drain */ + } + + const callArgs = mockStreamText.mock.calls[0][0] + expect(callArgs.providerOptions).toBeUndefined() + expect(callArgs.temperature).toBeUndefined() + }) + + it("uses default thinking budget when not specified", async () => { + const handler = new PoeHandler({ + poeApiKey: "key", + apiModelId: "anthropic/claude-sonnet-4", + enableReasoningEffort: true, + }) + + const fullStream = (async function* () {})() + mockStreamText.mockReturnValue({ + fullStream, + usage: Promise.resolve({ inputTokens: 0, outputTokens: 0 }), + }) + + for await (const _ of handler.createMessage("system", [{ role: "user" as const, content: "hi" }])) { + /* drain */ + } + + const callArgs = mockStreamText.mock.calls[0][0] + expect(callArgs.providerOptions).toEqual({ + poe: { + reasoningBudgetTokens: expect.any(Number), + }, + }) + expect(callArgs.providerOptions.poe.reasoningBudgetTokens + callArgs.maxOutputTokens).toBe( + handler.getModel().info.maxTokens, + ) + }) + }) + + describe("completePrompt", () => { + it("returns generated text", async () => { + const handler = new PoeHandler({ poeApiKey: "key", apiModelId: "openai/gpt-4o" }) + + mockGenerateText.mockResolvedValue({ text: "generated response" }) + + const result = await handler.completePrompt("complete this") + + expect(result).toBe("generated response") + expect(mockGenerateText).toHaveBeenCalledWith( + expect.objectContaining({ + model: mockLanguageModel, + prompt: "complete this", + }), + ) + }) + }) +}) 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..070caf0cc6c --- /dev/null +++ b/src/api/providers/fetchers/__tests__/poe.spec.ts @@ -0,0 +1,131 @@ +import { getPoeModels } from "../poe" + +const mockFetchPoeModels = vitest.fn() +const mockGetModels = vitest.fn() + +vitest.mock("ai-sdk-provider-poe/code", () => ({ + fetchPoeModels: (...args: unknown[]) => mockFetchPoeModels(...args), + getModels: () => mockGetModels(), +})) + +describe("getPoeModels", () => { + beforeEach(() => vitest.clearAllMocks()) + + it("converts PoeModelInfo to ModelRecord", async () => { + mockFetchPoeModels.mockResolvedValue([]) + mockGetModels.mockReturnValue([ + { + id: "anthropic/claude-sonnet-4", + rawId: "claude-sonnet-4", + contextWindow: 200_000, + maxOutputTokens: 8192, + supportsImages: true, + supportsPromptCache: true, + supportsReasoningBudget: true, + pricing: { + inputPerMillion: 3, + outputPerMillion: 15, + cacheReadPerMillion: 0.3, + cacheWritePerMillion: 3.75, + }, + }, + { + id: "openai/gpt-4o", + rawId: "gpt-4o", + contextWindow: 128_000, + maxOutputTokens: 16_384, + supportsImages: true, + supportsPromptCache: false, + pricing: { + inputPerMillion: 2.5, + outputPerMillion: 10, + }, + }, + ]) + + const models = await getPoeModels("test-key") + + expect(mockFetchPoeModels).toHaveBeenCalledWith({ apiKey: "test-key", baseURL: undefined }) + + expect(models["anthropic/claude-sonnet-4"]).toEqual({ + contextWindow: 200_000, + maxTokens: 8192, + supportsImages: true, + supportsPromptCache: true, + supportsReasoningBudget: true, + inputPrice: 3, + outputPrice: 15, + cacheReadsPrice: 0.3, + cacheWritesPrice: 3.75, + }) + + expect(models["openai/gpt-4o"]).toEqual({ + contextWindow: 128_000, + maxTokens: 16_384, + supportsImages: true, + supportsPromptCache: false, + inputPrice: 2.5, + outputPrice: 10, + }) + }) + + it("passes baseURL when provided", async () => { + mockFetchPoeModels.mockResolvedValue([]) + mockGetModels.mockReturnValue([]) + + await getPoeModels("key", "https://custom.api.com/v1") + + expect(mockFetchPoeModels).toHaveBeenCalledWith({ apiKey: "key", baseURL: "https://custom.api.com/v1" }) + }) + + it("returns empty record on empty response", async () => { + mockFetchPoeModels.mockResolvedValue([]) + mockGetModels.mockReturnValue([]) + + const models = await getPoeModels("key") + + expect(models).toEqual({}) + }) + + it("handles models with missing optional fields", async () => { + mockFetchPoeModels.mockResolvedValue([]) + mockGetModels.mockReturnValue([ + { + id: "some-model", + rawId: "some-model", + contextWindow: 4096, + maxOutputTokens: 1024, + supportsImages: false, + supportsPromptCache: false, + }, + ]) + + const models = await getPoeModels("key") + + expect(models["some-model"]).toEqual({ + contextWindow: 4096, + maxTokens: 1024, + supportsImages: false, + supportsPromptCache: false, + }) + }) + + it("maps supportsReasoningEffort when present", async () => { + mockFetchPoeModels.mockResolvedValue([]) + mockGetModels.mockReturnValue([ + { + id: "openai/o3", + rawId: "o3", + contextWindow: 200_000, + maxOutputTokens: 100_000, + supportsImages: true, + supportsPromptCache: false, + supportsReasoningEffort: ["low", "medium", "high"], + }, + ]) + + const models = await getPoeModels("key") + + expect(models["openai/o3"].supportsReasoningEffort).toEqual(["low", "medium", "high"]) + }) +}) diff --git a/src/core/webview/__tests__/ClineProvider.spec.ts b/src/core/webview/__tests__/ClineProvider.spec.ts index cfa4b0317f8..da0fb2003fb 100644 --- a/src/core/webview/__tests__/ClineProvider.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.spec.ts @@ -2494,6 +2494,7 @@ describe("ClineProvider - Router Models", () => { ollama: {}, lmstudio: {}, "vercel-ai-gateway": mockModels, + poe: {}, }, values: undefined, }) @@ -2540,6 +2541,7 @@ describe("ClineProvider - Router Models", () => { lmstudio: {}, litellm: {}, "vercel-ai-gateway": mockModels, + poe: {}, }, values: undefined, }) @@ -2634,6 +2636,7 @@ describe("ClineProvider - Router Models", () => { ollama: {}, lmstudio: {}, "vercel-ai-gateway": mockModels, + poe: {}, }, values: undefined, }) diff --git a/src/core/webview/__tests__/webviewMessageHandler.spec.ts b/src/core/webview/__tests__/webviewMessageHandler.spec.ts index 2dcaaf3db51..cb9327c6015 100644 --- a/src/core/webview/__tests__/webviewMessageHandler.spec.ts +++ b/src/core/webview/__tests__/webviewMessageHandler.spec.ts @@ -346,6 +346,7 @@ describe("webviewMessageHandler - requestRouterModels", () => { ollama: {}, lmstudio: {}, "vercel-ai-gateway": mockModels, + poe: {}, }, values: undefined, }) @@ -431,6 +432,7 @@ describe("webviewMessageHandler - requestRouterModels", () => { ollama: {}, lmstudio: {}, "vercel-ai-gateway": mockModels, + poe: {}, }, values: undefined, }) @@ -486,6 +488,7 @@ describe("webviewMessageHandler - requestRouterModels", () => { ollama: {}, lmstudio: {}, "vercel-ai-gateway": mockModels, + poe: {}, }, values: undefined, }) diff --git a/webview-ui/src/utils/__tests__/validate.spec.ts b/webview-ui/src/utils/__tests__/validate.spec.ts index 9b0b7a66e0d..40cd7fade87 100644 --- a/webview-ui/src/utils/__tests__/validate.spec.ts +++ b/webview-ui/src/utils/__tests__/validate.spec.ts @@ -45,6 +45,7 @@ describe("Model Validation Functions", () => { lmstudio: {}, "vercel-ai-gateway": {}, roo: {}, + poe: {}, } const allowAllOrganization: OrganizationAllowList = { From 44a1503bfa40b357bba7045178a71ea36c1a13db Mon Sep 17 00:00:00 2001 From: Kamil Jopek Date: Fri, 27 Mar 2026 07:43:04 -0500 Subject: [PATCH 08/15] build: use published ai-sdk-provider-poe ^2.0.14 Replace local link dependency with the published npm package. --- pnpm-lock.yaml | 69 ++++++++++++++++++++++++++++++++++++++++++++++-- src/package.json | 2 +- 2 files changed, 68 insertions(+), 3 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ee0ca3d8633..f8b2bc69cd2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -819,8 +819,8 @@ importers: specifier: ^0.0.36 version: 0.0.36 ai-sdk-provider-poe: - specifier: link:/Users/kjopek/Workspace/roo-code-extension/ai-sdk-provider-poe - version: link:../../ai-sdk-provider-poe + specifier: ^2.0.14 + version: 2.0.14(ai@6.0.77(zod@3.25.76))(zod@3.25.76) async-mutex: specifier: ^0.5.0 version: 0.5.0 @@ -1429,6 +1429,12 @@ packages: peerDependencies: zod: 3.25.76 + '@ai-sdk/anthropic@3.0.42': + resolution: {integrity: sha512-snoLXB9DmvAmmngbPN/Io8IGzZ9zWpC208EgIIztYf1e1JhwuMkgKCYkL30vGhSen4PrBafu2+sO4G/17wu45A==} + engines: {node: '>=18'} + peerDependencies: + zod: 3.25.76 + '@ai-sdk/baseten@1.0.31': resolution: {integrity: sha512-tGbV96WBb5nnfyUYFrPyBxrhw53YlKSJbMC+rH3HhQlUaIs8+m/Bm4M0isrek9owIIf4MmmSDZ5VZL08zz7eFQ==} engines: {node: '>=18'} @@ -1483,6 +1489,18 @@ packages: peerDependencies: zod: 3.25.76 + '@ai-sdk/openai-compatible@2.0.37': + resolution: {integrity: sha512-+POSFVcgiu47BK64dhsI6OpcDC0/VAE2ZSaXdXGNNhpC/ava++uSRJYks0k2bpfY0wwCTgpAWZsXn/dG2Yppiw==} + engines: {node: '>=18'} + peerDependencies: + zod: 3.25.76 + + '@ai-sdk/openai@3.0.27': + resolution: {integrity: sha512-pLMxWOypwroXiK9dxNpn60/HGhWWWDEOJ3lo9vZLoxvpJNtKnLKojwVIvlW3yEjlD7ll1+jUO2uzsABNTaP5Yg==} + engines: {node: '>=18'} + peerDependencies: + zod: 3.25.76 + '@ai-sdk/provider-utils@3.0.5': resolution: {integrity: sha512-HliwB/yzufw3iwczbFVE2Fiwf1XqROB/I6ng8EKUsPM5+2wnIa8f4VbljZcDx+grhFrPV+PnRZH7zBqi8WZM7Q==} engines: {node: '>=18'} @@ -1495,6 +1513,12 @@ packages: peerDependencies: zod: 3.25.76 + '@ai-sdk/provider-utils@4.0.21': + resolution: {integrity: sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw==} + engines: {node: '>=18'} + peerDependencies: + zod: 3.25.76 + '@ai-sdk/provider@2.0.0': resolution: {integrity: sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA==} engines: {node: '>=18'} @@ -4889,6 +4913,11 @@ packages: resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==} engines: {node: '>= 8.0.0'} + ai-sdk-provider-poe@2.0.14: + resolution: {integrity: sha512-ESSGBnMPOU76IJWkKEWLckD0hUt44zCFSVKWKV+F7p+/3FtavMNoBC4XJESPgMwF5HOpKR26aRi+MhBI0zdX6g==} + peerDependencies: + ai: '>=6.0.0' + ai@6.0.77: resolution: {integrity: sha512-tyyhrRpCRFVlivdNIFLK8cexSBB2jwTqO0z1qJQagk+UxZ+MW8h5V8xsvvb+xdKDY482Y8KAm0mr7TDnPKvvlw==} engines: {node: '>=18'} @@ -11062,6 +11091,12 @@ snapshots: '@ai-sdk/provider-utils': 4.0.14(zod@3.25.76) zod: 3.25.76 + '@ai-sdk/anthropic@3.0.42(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 3.0.8 + '@ai-sdk/provider-utils': 4.0.14(zod@3.25.76) + zod: 3.25.76 + '@ai-sdk/baseten@1.0.31(zod@3.25.76)': dependencies: '@ai-sdk/openai-compatible': 2.0.28(zod@3.25.76) @@ -11125,6 +11160,18 @@ snapshots: '@ai-sdk/provider-utils': 4.0.14(zod@3.25.76) zod: 3.25.76 + '@ai-sdk/openai-compatible@2.0.37(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 3.0.8 + '@ai-sdk/provider-utils': 4.0.21(zod@3.25.76) + zod: 3.25.76 + + '@ai-sdk/openai@3.0.27(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 3.0.8 + '@ai-sdk/provider-utils': 4.0.14(zod@3.25.76) + zod: 3.25.76 + '@ai-sdk/provider-utils@3.0.5(zod@3.25.76)': dependencies: '@ai-sdk/provider': 2.0.0 @@ -11140,6 +11187,13 @@ snapshots: eventsource-parser: 3.0.6 zod: 3.25.76 + '@ai-sdk/provider-utils@4.0.21(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 3.0.8 + '@standard-schema/spec': 1.1.0 + eventsource-parser: 3.0.6 + zod: 3.25.76 + '@ai-sdk/provider@2.0.0': dependencies: json-schema: 0.4.0 @@ -15133,6 +15187,17 @@ snapshots: dependencies: humanize-ms: 1.2.1 + ai-sdk-provider-poe@2.0.14(ai@6.0.77(zod@3.25.76))(zod@3.25.76): + dependencies: + '@ai-sdk/anthropic': 3.0.42(zod@3.25.76) + '@ai-sdk/openai': 3.0.27(zod@3.25.76) + '@ai-sdk/openai-compatible': 2.0.37(zod@3.25.76) + '@ai-sdk/provider': 3.0.8 + '@ai-sdk/provider-utils': 4.0.14(zod@3.25.76) + ai: 6.0.77(zod@3.25.76) + transitivePeerDependencies: + - zod + ai@6.0.77(zod@3.25.76): dependencies: '@ai-sdk/gateway': 3.0.39(zod@3.25.76) diff --git a/src/package.json b/src/package.json index 845c5a284c1..cd033b33e8a 100644 --- a/src/package.json +++ b/src/package.json @@ -471,7 +471,7 @@ "@roo-code/telemetry": "workspace:^", "@roo-code/types": "workspace:^", "@vscode/codicons": "^0.0.36", - "ai-sdk-provider-poe": "link:/Users/kjopek/Workspace/roo-code-extension/ai-sdk-provider-poe", + "ai-sdk-provider-poe": "^2.0.14", "async-mutex": "^0.5.0", "axios": "^1.12.0", "cheerio": "^1.0.0", From 8562ab73274833637f2d40e406a4eb5f1397cc87 Mon Sep 17 00:00:00 2001 From: Kamil Jopek Date: Fri, 27 Mar 2026 10:52:40 -0500 Subject: [PATCH 09/15] refactor: use ai-sdk-provider-poe exports for defaults Re-export poeDefaultModelId, POE_DEFAULT_BASE_URL, and getPoeDefaultModelInfo from the SDK instead of hardcoding them. Bump ai-sdk-provider-poe to ^2.0.15. --- packages/types/package.json | 1 + packages/types/src/providers/poe.ts | 17 +-- pnpm-lock.yaml | 156 ++++++++++++++++-------- src/api/providers/__tests__/poe.spec.ts | 13 +- src/api/providers/poe.ts | 10 +- src/package.json | 2 +- 6 files changed, 125 insertions(+), 74 deletions(-) diff --git a/packages/types/package.json b/packages/types/package.json index d66d87ac72d..9d4d85470cd 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -23,6 +23,7 @@ "clean": "rimraf dist .turbo" }, "dependencies": { + "ai-sdk-provider-poe": "^2.0.15", "zod": "3.25.76" }, "devDependencies": { diff --git a/packages/types/src/providers/poe.ts b/packages/types/src/providers/poe.ts index 1056d0c6031..c9b6211ddd1 100644 --- a/packages/types/src/providers/poe.ts +++ b/packages/types/src/providers/poe.ts @@ -1,15 +1,2 @@ -import type { ModelInfo } from "../model.js" - -export const POE_DEFAULT_BASE_URL = "https://api.poe.com/v1" - -export const poeDefaultModelId = "claude-sonnet-4" - -export const poeDefaultModelInfo: ModelInfo = { - maxTokens: 8192, - contextWindow: 200_000, - supportsImages: true, - supportsPromptCache: true, - inputPrice: 3, - outputPrice: 15, - description: "Claude Sonnet 4 via Poe API", -} +export { poeDefaultModelId, POE_DEFAULT_BASE_URL, getPoeDefaultModelInfo } from "ai-sdk-provider-poe/code" +export type { PoeDefaultModelInfo } from "ai-sdk-provider-poe/code" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f8b2bc69cd2..a208df23cc4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -569,7 +569,7 @@ importers: version: 7.0.5 openai: specifier: ^5.12.2 - version: 5.12.2(ws@8.18.3)(zod@3.25.76) + version: 5.12.2(ws@8.20.0)(zod@3.25.76) zod: specifier: 3.25.76 version: 3.25.76 @@ -709,6 +709,9 @@ importers: packages/types: dependencies: + ai-sdk-provider-poe: + specifier: ^2.0.15 + version: 2.0.15(ai@6.0.77(zod@3.25.76))(zod@3.25.76) zod: specifier: 3.25.76 version: 3.25.76 @@ -819,8 +822,8 @@ importers: specifier: ^0.0.36 version: 0.0.36 ai-sdk-provider-poe: - specifier: ^2.0.14 - version: 2.0.14(ai@6.0.77(zod@3.25.76))(zod@3.25.76) + specifier: ^2.0.15 + version: 2.0.15(ai@6.0.77(zod@3.25.76))(zod@3.25.76) async-mutex: specifier: ^0.5.0 version: 0.5.0 @@ -910,7 +913,7 @@ importers: version: 0.5.17 openai: specifier: ^5.12.2 - version: 5.12.2(ws@8.18.3)(zod@3.25.76) + version: 5.12.2(ws@8.20.0)(zod@3.25.76) os-name: specifier: ^6.0.0 version: 6.1.0 @@ -1753,6 +1756,10 @@ packages: resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} + '@babel/compat-data@7.27.2': resolution: {integrity: sha512-TUtMJYRPyUb/9aU8f3K0mjmjf6M9N5Woshn2CS6nqJSeJtTtQcpLUXjGt9vbF8ZGff0El99sWkLgzwW3VXnxZQ==} engines: {node: '>=6.9.0'} @@ -1791,6 +1798,10 @@ packages: resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + '@babel/helper-validator-option@7.27.1': resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} engines: {node: '>=6.9.0'} @@ -1832,6 +1843,10 @@ packages: resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==} engines: {node: '>=6.9.0'} + '@babel/runtime@7.29.2': + resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} + engines: {node: '>=6.9.0'} + '@babel/template@7.27.2': resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} engines: {node: '>=6.9.0'} @@ -2559,13 +2574,13 @@ packages: '@libsql/core@0.15.15': resolution: {integrity: sha512-C88Z6UKl+OyuKKPwz224riz02ih/zHYI3Ho/LAcVOgjsunIRZoBw7fjRfaH9oPMmSNeQfhGklSG2il1URoOIsA==} - '@libsql/darwin-arm64@0.5.22': - resolution: {integrity: sha512-4B8ZlX3nIDPndfct7GNe0nI3Yw6ibocEicWdC4fvQbSs/jdq/RC2oCsoJxJ4NzXkvktX70C1J4FcmmoBy069UA==} + '@libsql/darwin-arm64@0.5.29': + resolution: {integrity: sha512-K+2RIB1OGFPYQbfay48GakLhqf3ArcbHqPFu7EZiaUcRgFcdw8RoltsMyvbj5ix2fY0HV3Q3Ioa/ByvQdaSM0A==} cpu: [arm64] os: [darwin] - '@libsql/darwin-x64@0.5.22': - resolution: {integrity: sha512-ny2HYWt6lFSIdNFzUFIJ04uiW6finXfMNJ7wypkAD8Pqdm6nAByO+Fdqu8t7sD0sqJGeUCiOg480icjyQ2/8VA==} + '@libsql/darwin-x64@0.5.29': + resolution: {integrity: sha512-OtT+KFHsKFy1R5FVadr8FJ2Bb1mghtXTyJkxv0trocq7NuHntSki1eUbxpO5ezJesDvBlqFjnWaYYY516QNLhQ==} cpu: [x64] os: [darwin] @@ -2579,38 +2594,38 @@ packages: '@libsql/isomorphic-ws@0.1.5': resolution: {integrity: sha512-DtLWIH29onUYR00i0GlQ3UdcTRC6EP4u9w/h9LxpUZJWRMARk6dQwZ6Jkd+QdwVpuAOrdxt18v0K2uIYR3fwFg==} - '@libsql/linux-arm-gnueabihf@0.5.22': - resolution: {integrity: sha512-3Uo3SoDPJe/zBnyZKosziRGtszXaEtv57raWrZIahtQDsjxBVjuzYQinCm9LRCJCUT5t2r5Z5nLDPJi2CwZVoA==} + '@libsql/linux-arm-gnueabihf@0.5.29': + resolution: {integrity: sha512-CD4n4zj7SJTHso4nf5cuMoWoMSS7asn5hHygsDuhRl8jjjCTT3yE+xdUvI4J7zsyb53VO5ISh4cwwOtf6k2UhQ==} cpu: [arm] os: [linux] - '@libsql/linux-arm-musleabihf@0.5.22': - resolution: {integrity: sha512-LCsXh07jvSojTNJptT9CowOzwITznD+YFGGW+1XxUr7fS+7/ydUrpDfsMX7UqTqjm7xG17eq86VkWJgHJfvpNg==} + '@libsql/linux-arm-musleabihf@0.5.29': + resolution: {integrity: sha512-2Z9qBVpEJV7OeflzIR3+l5yAd4uTOLxklScYTwpZnkm2vDSGlC1PRlueLaufc4EFITkLKXK2MWBpexuNJfMVcg==} cpu: [arm] os: [linux] - '@libsql/linux-arm64-gnu@0.5.22': - resolution: {integrity: sha512-KSdnOMy88c9mpOFKUEzPskSaF3VLflfSUCBwas/pn1/sV3pEhtMF6H8VUCd2rsedwoukeeCSEONqX7LLnQwRMA==} + '@libsql/linux-arm64-gnu@0.5.29': + resolution: {integrity: sha512-gURBqaiXIGGwFNEaUj8Ldk7Hps4STtG+31aEidCk5evMMdtsdfL3HPCpvys+ZF/tkOs2MWlRWoSq7SOuCE9k3w==} cpu: [arm64] os: [linux] - '@libsql/linux-arm64-musl@0.5.22': - resolution: {integrity: sha512-mCHSMAsDTLK5YH//lcV3eFEgiR23Ym0U9oEvgZA0667gqRZg/2px+7LshDvErEKv2XZ8ixzw3p1IrBzLQHGSsw==} + '@libsql/linux-arm64-musl@0.5.29': + resolution: {integrity: sha512-fwgYZ0H8mUkyVqXZHF3mT/92iIh1N94Owi/f66cPVNsk9BdGKq5gVpoKO+7UxaNzuEH1roJp2QEwsCZMvBLpqg==} cpu: [arm64] os: [linux] - '@libsql/linux-x64-gnu@0.5.22': - resolution: {integrity: sha512-kNBHaIkSg78Y4BqAdgjcR2mBilZXs4HYkAmi58J+4GRwDQZh5fIUWbnQvB9f95DkWUIGVeenqLRFY2pcTmlsew==} + '@libsql/linux-x64-gnu@0.5.29': + resolution: {integrity: sha512-y14V0vY0nmMC6G0pHeJcEarcnGU2H6cm21ZceRkacWHvQAEhAG0latQkCtoS2njFOXiYIg+JYPfAoWKbi82rkg==} cpu: [x64] os: [linux] - '@libsql/linux-x64-musl@0.5.22': - resolution: {integrity: sha512-UZ4Xdxm4pu3pQXjvfJiyCzZop/9j/eA2JjmhMaAhe3EVLH2g11Fy4fwyUp9sT1QJYR1kpc2JLuybPM0kuXv/Tg==} + '@libsql/linux-x64-musl@0.5.29': + resolution: {integrity: sha512-gquqwA/39tH4pFl+J9n3SOMSymjX+6kZ3kWgY3b94nXFTwac9bnFNMffIomgvlFaC4ArVqMnOZD3nuJ3H3VO1w==} cpu: [x64] os: [linux] - '@libsql/win32-x64-msvc@0.5.22': - resolution: {integrity: sha512-Fj0j8RnBpo43tVZUVoNK6BV/9AtDUM5S7DF3LB4qTYg1LMSZqi3yeCneUTLJD6XomQJlZzbI4mst89yspVSAnA==} + '@libsql/win32-x64-msvc@0.5.29': + resolution: {integrity: sha512-4/0CvEdhi6+KjMxMaVbFM2n2Z44escBRoEYpR+gZg64DdetzGnYm8mcNLcoySaDJZNaBd6wz5DNdgRmcI4hXcg==} cpu: [x64] os: [win32] @@ -4913,8 +4928,8 @@ packages: resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==} engines: {node: '>= 8.0.0'} - ai-sdk-provider-poe@2.0.14: - resolution: {integrity: sha512-ESSGBnMPOU76IJWkKEWLckD0hUt44zCFSVKWKV+F7p+/3FtavMNoBC4XJESPgMwF5HOpKR26aRi+MhBI0zdX6g==} + ai-sdk-provider-poe@2.0.15: + resolution: {integrity: sha512-QoczOeUPja4GaluifZCFCFM6BlF1F64FppzEdJNVelNfdzO2DhliSCVqaUorpPT+PdAmaxA+/XW7/ZEk6raaJA==} peerDependencies: ai: '>=6.0.0' @@ -7744,8 +7759,8 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} - libsql@0.5.22: - resolution: {integrity: sha512-NscWthMQt7fpU8lqd7LXMvT9pi+KhhmTHAJWUB/Lj6MWa0MKFv0F2V4C6WKKpjCVZl0VwcDz4nOI3CyaT1DDiA==} + libsql@0.5.29: + resolution: {integrity: sha512-8lMP8iMgiBzzoNbAPQ59qdVcj6UaE/Vnm+fiwX4doX4Narook0a4GPKWBEv+CR8a1OwbfkgL18uBfBjWdF0Fzg==} cpu: [x64, arm64, wasm32, arm] os: [darwin, linux, win32] @@ -9598,6 +9613,11 @@ packages: engines: {node: '>=10'} hasBin: true + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + send@1.2.0: resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==} engines: {node: '>= 18'} @@ -10931,6 +10951,18 @@ packages: utf-8-validate: optional: true + ws@8.20.0: + resolution: {integrity: sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + xml-name-validator@5.0.0: resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} engines: {node: '>=18'} @@ -11825,6 +11857,12 @@ snapshots: js-tokens: 4.0.0 picocolors: 1.1.1 + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + '@babel/compat-data@7.27.2': {} '@babel/core@7.27.1': @@ -11885,6 +11923,8 @@ snapshots: '@babel/helper-validator-identifier@7.27.1': {} + '@babel/helper-validator-identifier@7.28.5': {} + '@babel/helper-validator-option@7.27.1': {} '@babel/helpers@7.27.1': @@ -11914,6 +11954,8 @@ snapshots: '@babel/runtime@7.28.4': {} + '@babel/runtime@7.29.2': {} + '@babel/template@7.27.2': dependencies: '@babel/code-frame': 7.27.1 @@ -12633,7 +12675,7 @@ snapshots: '@libsql/core': 0.15.15 '@libsql/hrana-client': 0.7.0 js-base64: 3.7.8 - libsql: 0.5.22 + libsql: 0.5.29 promise-limit: 2.7.0 transitivePeerDependencies: - bufferutil @@ -12645,10 +12687,10 @@ snapshots: js-base64: 3.7.8 optional: true - '@libsql/darwin-arm64@0.5.22': + '@libsql/darwin-arm64@0.5.29': optional: true - '@libsql/darwin-x64@0.5.22': + '@libsql/darwin-x64@0.5.29': optional: true '@libsql/hrana-client@0.7.0': @@ -12668,31 +12710,31 @@ snapshots: '@libsql/isomorphic-ws@0.1.5': dependencies: '@types/ws': 8.18.1 - ws: 8.18.3 + ws: 8.20.0 transitivePeerDependencies: - bufferutil - utf-8-validate optional: true - '@libsql/linux-arm-gnueabihf@0.5.22': + '@libsql/linux-arm-gnueabihf@0.5.29': optional: true - '@libsql/linux-arm-musleabihf@0.5.22': + '@libsql/linux-arm-musleabihf@0.5.29': optional: true - '@libsql/linux-arm64-gnu@0.5.22': + '@libsql/linux-arm64-gnu@0.5.29': optional: true - '@libsql/linux-arm64-musl@0.5.22': + '@libsql/linux-arm64-musl@0.5.29': optional: true - '@libsql/linux-x64-gnu@0.5.22': + '@libsql/linux-x64-gnu@0.5.29': optional: true - '@libsql/linux-x64-musl@0.5.22': + '@libsql/linux-x64-musl@0.5.29': optional: true - '@libsql/win32-x64-msvc@0.5.22': + '@libsql/win32-x64-msvc@0.5.29': optional: true '@lmstudio/lms-isomorphic@0.4.5': @@ -14447,8 +14489,8 @@ snapshots: '@testing-library/dom@10.4.0': dependencies: - '@babel/code-frame': 7.27.1 - '@babel/runtime': 7.28.4 + '@babel/code-frame': 7.29.0 + '@babel/runtime': 7.29.2 '@types/aria-query': 5.0.4 aria-query: 5.3.0 chalk: 4.1.2 @@ -15032,7 +15074,7 @@ snapshots: sirv: 3.0.1 tinyglobby: 0.2.14 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@20.17.50)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.2.1)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0) '@vitest/utils@3.2.4': dependencies: @@ -15187,7 +15229,7 @@ snapshots: dependencies: humanize-ms: 1.2.1 - ai-sdk-provider-poe@2.0.14(ai@6.0.77(zod@3.25.76))(zod@3.25.76): + ai-sdk-provider-poe@2.0.15(ai@6.0.77(zod@3.25.76))(zod@3.25.76): dependencies: '@ai-sdk/anthropic': 3.0.42(zod@3.25.76) '@ai-sdk/openai': 3.0.27(zod@3.25.76) @@ -17301,7 +17343,7 @@ snapshots: '@petamoriken/float16': 3.9.3 debug: 4.4.3 env-paths: 3.0.0 - semver: 7.7.3 + semver: 7.7.4 shell-quote: 1.8.3 which: 4.0.0 transitivePeerDependencies: @@ -18320,20 +18362,20 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 - libsql@0.5.22: + libsql@0.5.29: dependencies: '@neon-rs/load': 0.0.4 detect-libc: 2.0.2 optionalDependencies: - '@libsql/darwin-arm64': 0.5.22 - '@libsql/darwin-x64': 0.5.22 - '@libsql/linux-arm-gnueabihf': 0.5.22 - '@libsql/linux-arm-musleabihf': 0.5.22 - '@libsql/linux-arm64-gnu': 0.5.22 - '@libsql/linux-arm64-musl': 0.5.22 - '@libsql/linux-x64-gnu': 0.5.22 - '@libsql/linux-x64-musl': 0.5.22 - '@libsql/win32-x64-msvc': 0.5.22 + '@libsql/darwin-arm64': 0.5.29 + '@libsql/darwin-x64': 0.5.29 + '@libsql/linux-arm-gnueabihf': 0.5.29 + '@libsql/linux-arm-musleabihf': 0.5.29 + '@libsql/linux-arm64-gnu': 0.5.29 + '@libsql/linux-arm64-musl': 0.5.29 + '@libsql/linux-x64-gnu': 0.5.29 + '@libsql/linux-x64-musl': 0.5.29 + '@libsql/win32-x64-msvc': 0.5.29 optional: true lie@3.3.0: @@ -19453,9 +19495,9 @@ snapshots: is-inside-container: 1.0.0 is-wsl: 3.1.0 - openai@5.12.2(ws@8.18.3)(zod@3.25.76): + openai@5.12.2(ws@8.20.0)(zod@3.25.76): optionalDependencies: - ws: 8.18.3 + ws: 8.20.0 zod: 3.25.76 option@0.2.4: {} @@ -20607,6 +20649,9 @@ snapshots: semver@7.7.3: {} + semver@7.7.4: + optional: true + send@1.2.0: dependencies: debug: 4.4.1(supports-color@8.1.1) @@ -22234,6 +22279,9 @@ snapshots: ws@8.18.3: {} + ws@8.20.0: + optional: true + xml-name-validator@5.0.0: {} xml2js@0.5.0: diff --git a/src/api/providers/__tests__/poe.spec.ts b/src/api/providers/__tests__/poe.spec.ts index 418b0eadb08..3f7d95f610b 100644 --- a/src/api/providers/__tests__/poe.spec.ts +++ b/src/api/providers/__tests__/poe.spec.ts @@ -15,6 +15,14 @@ vitest.mock("ai-sdk-provider-poe/code", () => ({ cacheWriteTokens: usage?.cacheWriteTokens, reasoningTokens: usage?.reasoningTokens, })), + getPoeDefaultModelInfo: vitest.fn(() => ({ + maxTokens: 8192, + contextWindow: 200_000, + supportsImages: true, + supportsPromptCache: true, + inputPrice: 3, + outputPrice: 15, + })), })) vitest.mock("ai", async (importOriginal) => { @@ -58,7 +66,7 @@ vitest.mock("../fetchers/modelCache", () => ({ })) import type { Anthropic } from "@anthropic-ai/sdk" -import { poeDefaultModelId, poeDefaultModelInfo } from "@roo-code/types" +import { poeDefaultModelId } from "@roo-code/types" import { PoeHandler } from "../poe" import type { ApiHandlerOptions } from "../../../shared/api" @@ -122,7 +130,8 @@ describe("PoeHandler", () => { const result = handler.getModel() expect(result.id).toBe("unknown/model") - expect(result.info).toEqual(poeDefaultModelInfo) + expect(result.info.contextWindow).toBeGreaterThan(0) + expect(result.info.maxTokens).toBeGreaterThan(0) }) }) diff --git a/src/api/providers/poe.ts b/src/api/providers/poe.ts index f0d5b2f1851..d666cc6a77a 100644 --- a/src/api/providers/poe.ts +++ b/src/api/providers/poe.ts @@ -3,7 +3,13 @@ import { createPoe, type PoeProvider, type PoeScopedProviderOptions } from "ai-s import { extractUsageMetrics, mapToolChoice } from "ai-sdk-provider-poe/code" import { streamText, generateText, type ToolSet } from "ai" -import { poeDefaultModelId, poeDefaultModelInfo, type ReasoningEffortExtended, ApiProviderError } from "@roo-code/types" +import { + poeDefaultModelId, + getPoeDefaultModelInfo, + type ModelInfo, + type ReasoningEffortExtended, + ApiProviderError, +} from "@roo-code/types" import { TelemetryService } from "@roo-code/telemetry" import { shouldUseReasoningBudget, shouldUseReasoningEffort, type ApiHandlerOptions } from "../../shared/api" @@ -33,7 +39,7 @@ export class PoeHandler extends BaseProvider implements SingleCompletionHandler override getModel() { const id = this.options.apiModelId ?? poeDefaultModelId const cached = getModelsFromCache("poe") - const info = cached?.[id] ?? poeDefaultModelInfo + const info: ModelInfo = cached?.[id] ?? getPoeDefaultModelInfo() return { id, info } } diff --git a/src/package.json b/src/package.json index cd033b33e8a..b0661b88aa4 100644 --- a/src/package.json +++ b/src/package.json @@ -471,7 +471,7 @@ "@roo-code/telemetry": "workspace:^", "@roo-code/types": "workspace:^", "@vscode/codicons": "^0.0.36", - "ai-sdk-provider-poe": "^2.0.14", + "ai-sdk-provider-poe": "^2.0.15", "async-mutex": "^0.5.0", "axios": "^1.12.0", "cheerio": "^1.0.0", From 978f8e38fdd4e7bd8f9863505a1aa1f65d4fe1ff Mon Sep 17 00:00:00 2001 From: Kamil Jopek Date: Fri, 27 Mar 2026 11:16:52 -0500 Subject: [PATCH 10/15] refactor: remove unnecessary sawReasoning deduplication in Poe provider MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The AI SDK's reasoningText promise is always derived from the same stream parts emitted via fullStream — it can never contain content that wasn't already yielded. Remove the redundant fallback to match the openai-compatible provider pattern. --- src/api/providers/__tests__/poe.spec.ts | 56 ------------------------- src/api/providers/poe.ts | 11 ----- 2 files changed, 67 deletions(-) diff --git a/src/api/providers/__tests__/poe.spec.ts b/src/api/providers/__tests__/poe.spec.ts index 3f7d95f610b..f701273b71d 100644 --- a/src/api/providers/__tests__/poe.spec.ts +++ b/src/api/providers/__tests__/poe.spec.ts @@ -294,62 +294,6 @@ describe("PoeHandler", () => { ) }) - it("emits final reasoning text when the stream has no reasoning chunks", async () => { - const handler = new PoeHandler({ - poeApiKey: "key", - apiModelId: "openai/o3", - enableReasoningEffort: true, - reasoningEffort: "high", - }) - - const fullStream = (async function* () { - yield { type: "text-delta", text: "Answer" } - })() - - mockStreamText.mockReturnValue({ - fullStream, - reasoningText: Promise.resolve("Condensed reasoning"), - usage: Promise.resolve({ inputTokens: 10, outputTokens: 5 }), - }) - - const chunks = [] - for await (const chunk of handler.createMessage("system", [{ role: "user" as const, content: "reason" }])) { - chunks.push(chunk) - } - - expect(chunks).toContainEqual({ type: "text", text: "Answer" }) - expect(chunks).toContainEqual({ type: "reasoning", text: "Condensed reasoning" }) - }) - - it("does not duplicate reasoning when the stream already contains reasoning chunks", async () => { - const handler = new PoeHandler({ - poeApiKey: "key", - apiModelId: "openai/o3", - enableReasoningEffort: true, - reasoningEffort: "high", - }) - - const fullStream = (async function* () { - yield { type: "reasoning-delta", text: "Live reasoning" } - yield { type: "text-delta", text: "Answer" } - })() - - mockStreamText.mockReturnValue({ - fullStream, - reasoningText: Promise.resolve("Condensed reasoning"), - usage: Promise.resolve({ inputTokens: 10, outputTokens: 5 }), - }) - - const chunks = [] - for await (const chunk of handler.createMessage("system", [{ role: "user" as const, content: "reason" }])) { - chunks.push(chunk) - } - - expect(chunks.filter((chunk) => chunk.type === "reasoning")).toEqual([ - { type: "reasoning", text: "Live reasoning" }, - ]) - }) - it("does not pass providerOptions when reasoning is disabled", async () => { const handler = new PoeHandler({ poeApiKey: "key", diff --git a/src/api/providers/poe.ts b/src/api/providers/poe.ts index d666cc6a77a..536d222acd8 100644 --- a/src/api/providers/poe.ts +++ b/src/api/providers/poe.ts @@ -109,23 +109,12 @@ export class PoeHandler extends BaseProvider implements SingleCompletionHandler } try { - let sawReasoning = false for await (const part of result.fullStream) { for (const chunk of processAiSdkStreamPart(part)) { - if (chunk.type === "reasoning" && chunk.text.trim().length > 0) { - sawReasoning = true - } yield chunk } } - if (!sawReasoning) { - const reasoningText = await result.reasoningText - if (reasoningText?.trim()) { - yield { type: "reasoning", text: reasoningText } - } - } - const usage = await result.usage if (usage) { const metrics = extractUsageMetrics(usage as any) From 0a0c18382be865794bc350b2c70bd6b4cbef9c00 Mon Sep 17 00:00:00 2001 From: Kamil Jopek Date: Fri, 27 Mar 2026 11:21:28 -0500 Subject: [PATCH 11/15] test: remove redundant Poe provider tests Remove tests for tool passthrough and model ID plumbing that are already covered by the ai-sdk transform tests and implicitly by other createMessage tests. Clean up unused imports. --- src/api/providers/__tests__/poe.spec.ts | 61 ------------------------- 1 file changed, 61 deletions(-) diff --git a/src/api/providers/__tests__/poe.spec.ts b/src/api/providers/__tests__/poe.spec.ts index f701273b71d..50f229a243b 100644 --- a/src/api/providers/__tests__/poe.spec.ts +++ b/src/api/providers/__tests__/poe.spec.ts @@ -65,10 +65,8 @@ vitest.mock("../fetchers/modelCache", () => ({ }), })) -import type { Anthropic } from "@anthropic-ai/sdk" import { poeDefaultModelId } from "@roo-code/types" import { PoeHandler } from "../poe" -import type { ApiHandlerOptions } from "../../../shared/api" describe("PoeHandler", () => { const mockLanguageModel = { modelId: "test-model" } @@ -160,65 +158,6 @@ describe("PoeHandler", () => { expect(chunks).toContainEqual({ type: "text", text: "world!" }) expect(chunks).toContainEqual(expect.objectContaining({ type: "usage", inputTokens: 10, outputTokens: 5 })) }) - - it("passes tools and tool_choice to streamText", async () => { - const handler = new PoeHandler({ poeApiKey: "key", apiModelId: "openai/gpt-4o" }) - - const fullStream = (async function* () { - yield { type: "text-delta", text: "ok" } - })() - - mockStreamText.mockReturnValue({ - fullStream, - usage: Promise.resolve({ inputTokens: 1, outputTokens: 1 }), - }) - - const tools = [ - { - type: "function" as const, - function: { - name: "read_file", - description: "Read a file", - parameters: { type: "object", properties: { path: { type: "string" } }, required: ["path"] }, - }, - }, - ] - - const iterator = handler.createMessage("system", [{ role: "user" as const, content: "read file" }], { - taskId: "test", - tools, - tool_choice: "auto" as any, - }) - - // Consume stream - for await (const _ of iterator) { - /* drain */ - } - - expect(mockStreamText).toHaveBeenCalledWith( - expect.objectContaining({ - model: mockLanguageModel, - system: "system", - tools: expect.any(Object), - }), - ) - }) - - it("calls poe provider with correct model ID", async () => { - const handler = new PoeHandler({ poeApiKey: "key", apiModelId: "openai/gpt-4o" }) - - const fullStream = (async function* () {})() - mockStreamText.mockReturnValue({ - fullStream, - usage: Promise.resolve({ inputTokens: 0, outputTokens: 0 }), - }) - - for await (const _ of handler.createMessage("sys", [{ role: "user" as const, content: "hi" }])) { - /* drain */ - } - - expect(mockPoeProvider).toHaveBeenCalledWith("openai/gpt-4o") - }) }) describe("reasoning", () => { From 089cfa42b8729fe98f27f5192a41f4c7d2333e40 Mon Sep 17 00:00:00 2001 From: Kamil Jopek Date: Fri, 27 Mar 2026 11:35:16 -0500 Subject: [PATCH 12/15] build: bump ai-sdk-provider-poe to ^2.0.16 --- packages/types/package.json | 2 +- pnpm-lock.yaml | 14 +++++++------- src/package.json | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/types/package.json b/packages/types/package.json index 9d4d85470cd..97d7e228ebb 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -23,7 +23,7 @@ "clean": "rimraf dist .turbo" }, "dependencies": { - "ai-sdk-provider-poe": "^2.0.15", + "ai-sdk-provider-poe": "^2.0.16", "zod": "3.25.76" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a208df23cc4..88c258f5a8f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -710,8 +710,8 @@ importers: packages/types: dependencies: ai-sdk-provider-poe: - specifier: ^2.0.15 - version: 2.0.15(ai@6.0.77(zod@3.25.76))(zod@3.25.76) + specifier: ^2.0.16 + version: 2.0.16(ai@6.0.77(zod@3.25.76))(zod@3.25.76) zod: specifier: 3.25.76 version: 3.25.76 @@ -822,8 +822,8 @@ importers: specifier: ^0.0.36 version: 0.0.36 ai-sdk-provider-poe: - specifier: ^2.0.15 - version: 2.0.15(ai@6.0.77(zod@3.25.76))(zod@3.25.76) + specifier: ^2.0.16 + version: 2.0.16(ai@6.0.77(zod@3.25.76))(zod@3.25.76) async-mutex: specifier: ^0.5.0 version: 0.5.0 @@ -4928,8 +4928,8 @@ packages: resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==} engines: {node: '>= 8.0.0'} - ai-sdk-provider-poe@2.0.15: - resolution: {integrity: sha512-QoczOeUPja4GaluifZCFCFM6BlF1F64FppzEdJNVelNfdzO2DhliSCVqaUorpPT+PdAmaxA+/XW7/ZEk6raaJA==} + ai-sdk-provider-poe@2.0.16: + resolution: {integrity: sha512-tE6qJRp7pzYN+KRFSYOMi+UWQTLaOxKzbITNQnAd+EJ8c9GV8L/jfY1D/EZp/mO93SKa3NNwVqTStaJTUBmOsw==} peerDependencies: ai: '>=6.0.0' @@ -15229,7 +15229,7 @@ snapshots: dependencies: humanize-ms: 1.2.1 - ai-sdk-provider-poe@2.0.15(ai@6.0.77(zod@3.25.76))(zod@3.25.76): + ai-sdk-provider-poe@2.0.16(ai@6.0.77(zod@3.25.76))(zod@3.25.76): dependencies: '@ai-sdk/anthropic': 3.0.42(zod@3.25.76) '@ai-sdk/openai': 3.0.27(zod@3.25.76) diff --git a/src/package.json b/src/package.json index b0661b88aa4..22779dea039 100644 --- a/src/package.json +++ b/src/package.json @@ -471,7 +471,7 @@ "@roo-code/telemetry": "workspace:^", "@roo-code/types": "workspace:^", "@vscode/codicons": "^0.0.36", - "ai-sdk-provider-poe": "^2.0.15", + "ai-sdk-provider-poe": "^2.0.16", "async-mutex": "^0.5.0", "axios": "^1.12.0", "cheerio": "^1.0.0", From 59b6c556a7402fb144cec09df2ac0f3393ffcf9a Mon Sep 17 00:00:00 2001 From: Kamil Jopek Date: Fri, 27 Mar 2026 12:24:57 -0500 Subject: [PATCH 13/15] fix: invalidate router models cache after Poe refresh After clicking Refresh Models, the ModelPicker dropdown (powered by ExtensionStateContext) updated correctly, but the model validation in ApiOptions still used a stale react-query cache. This caused newly fetched models like gpt-5.4 to show "not available" even though they appeared in the picker. Invalidate the react-query ["routerModels"] cache on successful Poe refresh so validation sees the same model list as the dropdown. Also bumps ai-sdk-provider-poe to ^2.0.17. --- packages/types/package.json | 2 +- pnpm-lock.yaml | 14 +++++++------- src/package.json | 2 +- .../src/components/settings/providers/Poe.tsx | 7 ++++++- 4 files changed, 15 insertions(+), 10 deletions(-) diff --git a/packages/types/package.json b/packages/types/package.json index 97d7e228ebb..9aa35a0dde6 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -23,7 +23,7 @@ "clean": "rimraf dist .turbo" }, "dependencies": { - "ai-sdk-provider-poe": "^2.0.16", + "ai-sdk-provider-poe": "^2.0.17", "zod": "3.25.76" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 88c258f5a8f..5c9ed706044 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -710,8 +710,8 @@ importers: packages/types: dependencies: ai-sdk-provider-poe: - specifier: ^2.0.16 - version: 2.0.16(ai@6.0.77(zod@3.25.76))(zod@3.25.76) + specifier: ^2.0.17 + version: 2.0.17(ai@6.0.77(zod@3.25.76))(zod@3.25.76) zod: specifier: 3.25.76 version: 3.25.76 @@ -822,8 +822,8 @@ importers: specifier: ^0.0.36 version: 0.0.36 ai-sdk-provider-poe: - specifier: ^2.0.16 - version: 2.0.16(ai@6.0.77(zod@3.25.76))(zod@3.25.76) + specifier: ^2.0.17 + version: 2.0.17(ai@6.0.77(zod@3.25.76))(zod@3.25.76) async-mutex: specifier: ^0.5.0 version: 0.5.0 @@ -4928,8 +4928,8 @@ packages: resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==} engines: {node: '>= 8.0.0'} - ai-sdk-provider-poe@2.0.16: - resolution: {integrity: sha512-tE6qJRp7pzYN+KRFSYOMi+UWQTLaOxKzbITNQnAd+EJ8c9GV8L/jfY1D/EZp/mO93SKa3NNwVqTStaJTUBmOsw==} + ai-sdk-provider-poe@2.0.17: + resolution: {integrity: sha512-a+glxLPxgBT7mTBvdP0ML8lv5bpyJD1K8x8v+d0YS2iZ1tEkBUC9mY1uMZx62N88/pPVsD+WcMddiE2hfZuxLg==} peerDependencies: ai: '>=6.0.0' @@ -15229,7 +15229,7 @@ snapshots: dependencies: humanize-ms: 1.2.1 - ai-sdk-provider-poe@2.0.16(ai@6.0.77(zod@3.25.76))(zod@3.25.76): + ai-sdk-provider-poe@2.0.17(ai@6.0.77(zod@3.25.76))(zod@3.25.76): dependencies: '@ai-sdk/anthropic': 3.0.42(zod@3.25.76) '@ai-sdk/openai': 3.0.27(zod@3.25.76) diff --git a/src/package.json b/src/package.json index 22779dea039..88637187ef0 100644 --- a/src/package.json +++ b/src/package.json @@ -471,7 +471,7 @@ "@roo-code/telemetry": "workspace:^", "@roo-code/types": "workspace:^", "@vscode/codicons": "^0.0.36", - "ai-sdk-provider-poe": "^2.0.16", + "ai-sdk-provider-poe": "^2.0.17", "async-mutex": "^0.5.0", "axios": "^1.12.0", "cheerio": "^1.0.0", diff --git a/webview-ui/src/components/settings/providers/Poe.tsx b/webview-ui/src/components/settings/providers/Poe.tsx index 741d8971c37..7b79ed85107 100644 --- a/webview-ui/src/components/settings/providers/Poe.tsx +++ b/webview-ui/src/components/settings/providers/Poe.tsx @@ -1,5 +1,6 @@ import { useCallback, useState, useEffect, useRef } from "react" import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react" +import { useQueryClient } from "@tanstack/react-query" import { type ProviderSettings, @@ -37,6 +38,7 @@ export const Poe = ({ simplifySettings, }: PoeProps) => { const { t } = useAppTranslation() + const queryClient = useQueryClient() const { routerModels } = useExtensionState() const [refreshStatus, setRefreshStatus] = useState<"idle" | "loading" | "success" | "error">("idle") const [refreshError, setRefreshError] = useState() @@ -56,6 +58,9 @@ export const Poe = ({ if (refreshStatus === "loading") { if (!poeErrorJustReceived.current) { setRefreshStatus("success") + // Invalidate the react-query router models cache so + // validation in ApiOptions picks up the refreshed list. + queryClient.invalidateQueries({ queryKey: ["routerModels"] }) } } } @@ -65,7 +70,7 @@ export const Poe = ({ return () => { window.removeEventListener("message", handleMessage) } - }, [refreshStatus]) + }, [refreshStatus, queryClient]) const handleInputChange = useCallback( ( From 3437d603a032506d0546130161367f4a5346a26a Mon Sep 17 00:00:00 2001 From: Kamil Jopek Date: Fri, 27 Mar 2026 13:45:18 -0500 Subject: [PATCH 14/15] build: pin ai-sdk-provider-poe to 2.0.18 Replace local file: references with the pinned registry version and remove debug canary from extension.ts. --- packages/types/package.json | 2 +- pnpm-lock.yaml | 14 +++++++------- src/package.json | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/types/package.json b/packages/types/package.json index 9aa35a0dde6..f3fce492fe5 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -23,7 +23,7 @@ "clean": "rimraf dist .turbo" }, "dependencies": { - "ai-sdk-provider-poe": "^2.0.17", + "ai-sdk-provider-poe": "2.0.18", "zod": "3.25.76" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5c9ed706044..8ada6ca93f7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -710,8 +710,8 @@ importers: packages/types: dependencies: ai-sdk-provider-poe: - specifier: ^2.0.17 - version: 2.0.17(ai@6.0.77(zod@3.25.76))(zod@3.25.76) + specifier: 2.0.18 + version: 2.0.18(ai@6.0.77(zod@3.25.76))(zod@3.25.76) zod: specifier: 3.25.76 version: 3.25.76 @@ -822,8 +822,8 @@ importers: specifier: ^0.0.36 version: 0.0.36 ai-sdk-provider-poe: - specifier: ^2.0.17 - version: 2.0.17(ai@6.0.77(zod@3.25.76))(zod@3.25.76) + specifier: 2.0.18 + version: 2.0.18(ai@6.0.77(zod@3.25.76))(zod@3.25.76) async-mutex: specifier: ^0.5.0 version: 0.5.0 @@ -4928,8 +4928,8 @@ packages: resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==} engines: {node: '>= 8.0.0'} - ai-sdk-provider-poe@2.0.17: - resolution: {integrity: sha512-a+glxLPxgBT7mTBvdP0ML8lv5bpyJD1K8x8v+d0YS2iZ1tEkBUC9mY1uMZx62N88/pPVsD+WcMddiE2hfZuxLg==} + ai-sdk-provider-poe@2.0.18: + resolution: {integrity: sha512-uzFR5Zq+PD6LfrqpYAr4dt2KmfOfS9a0Wh8QJsOaGI/vOByhW02P/0mj0NtOnOWF1RIUfBkQJo647XjrgnFjjg==} peerDependencies: ai: '>=6.0.0' @@ -15229,7 +15229,7 @@ snapshots: dependencies: humanize-ms: 1.2.1 - ai-sdk-provider-poe@2.0.17(ai@6.0.77(zod@3.25.76))(zod@3.25.76): + ai-sdk-provider-poe@2.0.18(ai@6.0.77(zod@3.25.76))(zod@3.25.76): dependencies: '@ai-sdk/anthropic': 3.0.42(zod@3.25.76) '@ai-sdk/openai': 3.0.27(zod@3.25.76) diff --git a/src/package.json b/src/package.json index 88637187ef0..19096bb4865 100644 --- a/src/package.json +++ b/src/package.json @@ -471,7 +471,7 @@ "@roo-code/telemetry": "workspace:^", "@roo-code/types": "workspace:^", "@vscode/codicons": "^0.0.36", - "ai-sdk-provider-poe": "^2.0.17", + "ai-sdk-provider-poe": "2.0.18", "async-mutex": "^0.5.0", "axios": "^1.12.0", "cheerio": "^1.0.0", From 5e409ecf3cd1d159ddf531242fa82362eaab0988 Mon Sep 17 00:00:00 2001 From: Kamil Jopek Date: Fri, 27 Mar 2026 14:14:50 -0500 Subject: [PATCH 15/15] i18n: add Poe provider translations for all locales Add poeApiKey, getPoeApiKey, and poeBaseUrl translation keys to all 17 non-English locale files. --- webview-ui/src/i18n/locales/ca/settings.json | 3 +++ webview-ui/src/i18n/locales/de/settings.json | 3 +++ webview-ui/src/i18n/locales/es/settings.json | 3 +++ webview-ui/src/i18n/locales/fr/settings.json | 3 +++ webview-ui/src/i18n/locales/hi/settings.json | 3 +++ webview-ui/src/i18n/locales/id/settings.json | 3 +++ webview-ui/src/i18n/locales/it/settings.json | 3 +++ webview-ui/src/i18n/locales/ja/settings.json | 3 +++ webview-ui/src/i18n/locales/ko/settings.json | 3 +++ webview-ui/src/i18n/locales/nl/settings.json | 3 +++ webview-ui/src/i18n/locales/pl/settings.json | 3 +++ webview-ui/src/i18n/locales/pt-BR/settings.json | 3 +++ webview-ui/src/i18n/locales/ru/settings.json | 3 +++ webview-ui/src/i18n/locales/tr/settings.json | 3 +++ webview-ui/src/i18n/locales/vi/settings.json | 3 +++ webview-ui/src/i18n/locales/zh-CN/settings.json | 3 +++ webview-ui/src/i18n/locales/zh-TW/settings.json | 3 +++ 17 files changed, 51 insertions(+) diff --git a/webview-ui/src/i18n/locales/ca/settings.json b/webview-ui/src/i18n/locales/ca/settings.json index 2c83cabbbcb..4bd1b8ef33d 100644 --- a/webview-ui/src/i18n/locales/ca/settings.json +++ b/webview-ui/src/i18n/locales/ca/settings.json @@ -380,6 +380,9 @@ "vertex1MContextBetaDescription": "Amplia la finestra de context a 1 milió de tokens per a Claude Sonnet 4.x / Claude Opus 4.6", "basetenApiKey": "Clau API de Baseten", "getBasetenApiKey": "Obtenir clau API de Baseten", + "poeApiKey": "Clau API de Poe", + "getPoeApiKey": "Obtenir clau API de Poe", + "poeBaseUrl": "URL base de Poe", "fireworksApiKey": "Clau API de Fireworks", "getFireworksApiKey": "Obtenir clau API de Fireworks", "deepSeekApiKey": "Clau API de DeepSeek", diff --git a/webview-ui/src/i18n/locales/de/settings.json b/webview-ui/src/i18n/locales/de/settings.json index c31d29147d4..c524ffb4627 100644 --- a/webview-ui/src/i18n/locales/de/settings.json +++ b/webview-ui/src/i18n/locales/de/settings.json @@ -380,6 +380,9 @@ "vertex1MContextBetaDescription": "Erweitert das Kontextfenster für Claude Sonnet 4.x / Claude Opus 4.6 auf 1 Million Token", "basetenApiKey": "Baseten API-Schlüssel", "getBasetenApiKey": "Baseten API-Schlüssel erhalten", + "poeApiKey": "Poe API-Schlüssel", + "getPoeApiKey": "Poe API-Schlüssel erhalten", + "poeBaseUrl": "Poe Basis-URL", "fireworksApiKey": "Fireworks API-Schlüssel", "getFireworksApiKey": "Fireworks API-Schlüssel erhalten", "deepSeekApiKey": "DeepSeek API-Schlüssel", diff --git a/webview-ui/src/i18n/locales/es/settings.json b/webview-ui/src/i18n/locales/es/settings.json index 6595c4f9079..dc2634b7186 100644 --- a/webview-ui/src/i18n/locales/es/settings.json +++ b/webview-ui/src/i18n/locales/es/settings.json @@ -380,6 +380,9 @@ "vertex1MContextBetaDescription": "Amplía la ventana de contexto a 1 millón de tokens para Claude Sonnet 4.x / Claude Opus 4.6", "basetenApiKey": "Clave API de Baseten", "getBasetenApiKey": "Obtener clave API de Baseten", + "poeApiKey": "Clave API de Poe", + "getPoeApiKey": "Obtener clave API de Poe", + "poeBaseUrl": "URL base de Poe", "fireworksApiKey": "Clave API de Fireworks", "getFireworksApiKey": "Obtener clave API de Fireworks", "deepSeekApiKey": "Clave API de DeepSeek", diff --git a/webview-ui/src/i18n/locales/fr/settings.json b/webview-ui/src/i18n/locales/fr/settings.json index 56337bda14c..ceed03755ce 100644 --- a/webview-ui/src/i18n/locales/fr/settings.json +++ b/webview-ui/src/i18n/locales/fr/settings.json @@ -380,6 +380,9 @@ "vertex1MContextBetaDescription": "Étend la fenêtre de contexte à 1 million de tokens pour Claude Sonnet 4.x / Claude Opus 4.6", "basetenApiKey": "Clé API Baseten", "getBasetenApiKey": "Obtenir la clé API Baseten", + "poeApiKey": "Clé API Poe", + "getPoeApiKey": "Obtenir la clé API Poe", + "poeBaseUrl": "URL de base Poe", "fireworksApiKey": "Clé API Fireworks", "getFireworksApiKey": "Obtenir la clé API Fireworks", "deepSeekApiKey": "Clé API DeepSeek", diff --git a/webview-ui/src/i18n/locales/hi/settings.json b/webview-ui/src/i18n/locales/hi/settings.json index abd334bec09..a9e778e4a7d 100644 --- a/webview-ui/src/i18n/locales/hi/settings.json +++ b/webview-ui/src/i18n/locales/hi/settings.json @@ -380,6 +380,9 @@ "vertex1MContextBetaDescription": "Claude Sonnet 4.x / Claude Opus 4.6 के लिए संदर्भ विंडो को 1 मिलियन टोकन तक बढ़ाता है", "basetenApiKey": "Baseten API कुंजी", "getBasetenApiKey": "Baseten API कुंजी प्राप्त करें", + "poeApiKey": "Poe API कुंजी", + "getPoeApiKey": "Poe API कुंजी प्राप्त करें", + "poeBaseUrl": "Poe बेस URL", "fireworksApiKey": "Fireworks API कुंजी", "getFireworksApiKey": "Fireworks API कुंजी प्राप्त करें", "deepSeekApiKey": "DeepSeek API कुंजी", diff --git a/webview-ui/src/i18n/locales/id/settings.json b/webview-ui/src/i18n/locales/id/settings.json index 1ebcf2073b6..36c0f6799f4 100644 --- a/webview-ui/src/i18n/locales/id/settings.json +++ b/webview-ui/src/i18n/locales/id/settings.json @@ -380,6 +380,9 @@ "vertex1MContextBetaDescription": "Memperluas jendela konteks menjadi 1 juta token untuk Claude Sonnet 4.x / Claude Opus 4.6", "basetenApiKey": "Baseten API Key", "getBasetenApiKey": "Dapatkan Baseten API Key", + "poeApiKey": "Poe API Key", + "getPoeApiKey": "Dapatkan Poe API Key", + "poeBaseUrl": "Poe Base URL", "fireworksApiKey": "Fireworks API Key", "getFireworksApiKey": "Dapatkan Fireworks API Key", "deepSeekApiKey": "DeepSeek API Key", diff --git a/webview-ui/src/i18n/locales/it/settings.json b/webview-ui/src/i18n/locales/it/settings.json index 4a0c7161654..970752b16b4 100644 --- a/webview-ui/src/i18n/locales/it/settings.json +++ b/webview-ui/src/i18n/locales/it/settings.json @@ -380,6 +380,9 @@ "vertex1MContextBetaDescription": "Estende la finestra di contesto a 1 milione di token per Claude Sonnet 4.x / Claude Opus 4.6", "basetenApiKey": "Chiave API Baseten", "getBasetenApiKey": "Ottieni chiave API Baseten", + "poeApiKey": "Chiave API Poe", + "getPoeApiKey": "Ottieni chiave API Poe", + "poeBaseUrl": "URL base Poe", "fireworksApiKey": "Chiave API Fireworks", "getFireworksApiKey": "Ottieni chiave API Fireworks", "deepSeekApiKey": "Chiave API DeepSeek", diff --git a/webview-ui/src/i18n/locales/ja/settings.json b/webview-ui/src/i18n/locales/ja/settings.json index b0d921571af..1b2a125600e 100644 --- a/webview-ui/src/i18n/locales/ja/settings.json +++ b/webview-ui/src/i18n/locales/ja/settings.json @@ -380,6 +380,9 @@ "vertex1MContextBetaDescription": "Claude Sonnet 4.x / Claude Opus 4.6のコンテキストウィンドウを100万トークンに拡張します", "basetenApiKey": "Baseten APIキー", "getBasetenApiKey": "Baseten APIキーを取得", + "poeApiKey": "Poe APIキー", + "getPoeApiKey": "Poe APIキーを取得", + "poeBaseUrl": "Poe ベースURL", "fireworksApiKey": "Fireworks APIキー", "getFireworksApiKey": "Fireworks APIキーを取得", "deepSeekApiKey": "DeepSeek APIキー", diff --git a/webview-ui/src/i18n/locales/ko/settings.json b/webview-ui/src/i18n/locales/ko/settings.json index 88fc8e6d79e..e35ab51e3b9 100644 --- a/webview-ui/src/i18n/locales/ko/settings.json +++ b/webview-ui/src/i18n/locales/ko/settings.json @@ -380,6 +380,9 @@ "vertex1MContextBetaDescription": "Claude Sonnet 4.x / Claude Opus 4.6의 컨텍스트 창을 100만 토큰으로 확장", "basetenApiKey": "Baseten API 키", "getBasetenApiKey": "Baseten API 키 가져오기", + "poeApiKey": "Poe API 키", + "getPoeApiKey": "Poe API 키 가져오기", + "poeBaseUrl": "Poe 기본 URL", "fireworksApiKey": "Fireworks API 키", "getFireworksApiKey": "Fireworks API 키 받기", "deepSeekApiKey": "DeepSeek API 키", diff --git a/webview-ui/src/i18n/locales/nl/settings.json b/webview-ui/src/i18n/locales/nl/settings.json index fcfad37d376..9ec6a91df90 100644 --- a/webview-ui/src/i18n/locales/nl/settings.json +++ b/webview-ui/src/i18n/locales/nl/settings.json @@ -380,6 +380,9 @@ "vertex1MContextBetaDescription": "Breidt het contextvenster uit tot 1 miljoen tokens voor Claude Sonnet 4.x / Claude Opus 4.6", "basetenApiKey": "Baseten API-sleutel", "getBasetenApiKey": "Baseten API-sleutel verkrijgen", + "poeApiKey": "Poe API-sleutel", + "getPoeApiKey": "Poe API-sleutel verkrijgen", + "poeBaseUrl": "Poe Basis-URL", "fireworksApiKey": "Fireworks API-sleutel", "getFireworksApiKey": "Fireworks API-sleutel ophalen", "deepSeekApiKey": "DeepSeek API-sleutel", diff --git a/webview-ui/src/i18n/locales/pl/settings.json b/webview-ui/src/i18n/locales/pl/settings.json index fa48bc6b212..45570384f1c 100644 --- a/webview-ui/src/i18n/locales/pl/settings.json +++ b/webview-ui/src/i18n/locales/pl/settings.json @@ -380,6 +380,9 @@ "vertex1MContextBetaDescription": "Rozszerza okno kontekstowe do 1 miliona tokenów dla Claude Sonnet 4.x / Claude Opus 4.6", "basetenApiKey": "Klucz API Baseten", "getBasetenApiKey": "Uzyskaj klucz API Baseten", + "poeApiKey": "Klucz API Poe", + "getPoeApiKey": "Uzyskaj klucz API Poe", + "poeBaseUrl": "Bazowy URL Poe", "fireworksApiKey": "Klucz API Fireworks", "getFireworksApiKey": "Uzyskaj klucz API Fireworks", "deepSeekApiKey": "Klucz API DeepSeek", diff --git a/webview-ui/src/i18n/locales/pt-BR/settings.json b/webview-ui/src/i18n/locales/pt-BR/settings.json index a8387e05121..cebf1c5cd23 100644 --- a/webview-ui/src/i18n/locales/pt-BR/settings.json +++ b/webview-ui/src/i18n/locales/pt-BR/settings.json @@ -380,6 +380,9 @@ "vertex1MContextBetaDescription": "Estende a janela de contexto para 1 milhão de tokens para o Claude Sonnet 4.x / Claude Opus 4.6", "basetenApiKey": "Chave de API Baseten", "getBasetenApiKey": "Obter chave de API Baseten", + "poeApiKey": "Chave de API Poe", + "getPoeApiKey": "Obter chave de API Poe", + "poeBaseUrl": "URL base do Poe", "fireworksApiKey": "Chave de API Fireworks", "getFireworksApiKey": "Obter chave de API Fireworks", "deepSeekApiKey": "Chave de API DeepSeek", diff --git a/webview-ui/src/i18n/locales/ru/settings.json b/webview-ui/src/i18n/locales/ru/settings.json index fe24ebee299..698ae4c4038 100644 --- a/webview-ui/src/i18n/locales/ru/settings.json +++ b/webview-ui/src/i18n/locales/ru/settings.json @@ -380,6 +380,9 @@ "vertex1MContextBetaDescription": "Расширяет контекстное окно до 1 миллиона токенов для Claude Sonnet 4.x / Claude Opus 4.6", "basetenApiKey": "Baseten API-ключ", "getBasetenApiKey": "Получить Baseten API-ключ", + "poeApiKey": "API-ключ Poe", + "getPoeApiKey": "Получить API-ключ Poe", + "poeBaseUrl": "Базовый URL Poe", "fireworksApiKey": "Fireworks API-ключ", "getFireworksApiKey": "Получить Fireworks API-ключ", "deepSeekApiKey": "DeepSeek API-ключ", diff --git a/webview-ui/src/i18n/locales/tr/settings.json b/webview-ui/src/i18n/locales/tr/settings.json index 7171718f1c5..55ba2ece655 100644 --- a/webview-ui/src/i18n/locales/tr/settings.json +++ b/webview-ui/src/i18n/locales/tr/settings.json @@ -380,6 +380,9 @@ "vertex1MContextBetaDescription": "Claude Sonnet 4.x / Claude Opus 4.6 için bağlam penceresini 1 milyon token'a genişletir", "basetenApiKey": "Baseten API Anahtarı", "getBasetenApiKey": "Baseten API Anahtarı Al", + "poeApiKey": "Poe API Anahtarı", + "getPoeApiKey": "Poe API Anahtarı Al", + "poeBaseUrl": "Poe Temel URL", "fireworksApiKey": "Fireworks API Anahtarı", "getFireworksApiKey": "Fireworks API Anahtarı Al", "deepSeekApiKey": "DeepSeek API Anahtarı", diff --git a/webview-ui/src/i18n/locales/vi/settings.json b/webview-ui/src/i18n/locales/vi/settings.json index 95b4f2d6863..9d77fbc5705 100644 --- a/webview-ui/src/i18n/locales/vi/settings.json +++ b/webview-ui/src/i18n/locales/vi/settings.json @@ -380,6 +380,9 @@ "vertex1MContextBetaDescription": "Mở rộng cửa sổ ngữ cảnh lên 1 triệu token cho Claude Sonnet 4.x / Claude Opus 4.6", "basetenApiKey": "Khóa API Baseten", "getBasetenApiKey": "Lấy khóa API Baseten", + "poeApiKey": "Khóa API Poe", + "getPoeApiKey": "Lấy khóa API Poe", + "poeBaseUrl": "URL cơ sở Poe", "fireworksApiKey": "Khóa API Fireworks", "getFireworksApiKey": "Lấy khóa API Fireworks", "deepSeekApiKey": "Khóa API DeepSeek", diff --git a/webview-ui/src/i18n/locales/zh-CN/settings.json b/webview-ui/src/i18n/locales/zh-CN/settings.json index eeba6bb079d..fea5dcc684c 100644 --- a/webview-ui/src/i18n/locales/zh-CN/settings.json +++ b/webview-ui/src/i18n/locales/zh-CN/settings.json @@ -380,6 +380,9 @@ "vertex1MContextBetaDescription": "为 Claude Sonnet 4.x / Claude Opus 4.6 将上下文窗口扩展至 100 万个 token", "basetenApiKey": "Baseten API 密钥", "getBasetenApiKey": "获取 Baseten API 密钥", + "poeApiKey": "Poe API 密钥", + "getPoeApiKey": "获取 Poe API 密钥", + "poeBaseUrl": "Poe 基础 URL", "fireworksApiKey": "Fireworks API 密钥", "getFireworksApiKey": "获取 Fireworks API 密钥", "deepSeekApiKey": "DeepSeek API 密钥", diff --git a/webview-ui/src/i18n/locales/zh-TW/settings.json b/webview-ui/src/i18n/locales/zh-TW/settings.json index 9f4241c3dd9..1a306043a36 100644 --- a/webview-ui/src/i18n/locales/zh-TW/settings.json +++ b/webview-ui/src/i18n/locales/zh-TW/settings.json @@ -390,6 +390,9 @@ "vertex1MContextBetaDescription": "為 Claude Sonnet 4.x / Claude Opus 4.6 將上下文視窗擴展至 100 萬個 token", "basetenApiKey": "Baseten API 金鑰", "getBasetenApiKey": "取得 Baseten API 金鑰", + "poeApiKey": "Poe API 金鑰", + "getPoeApiKey": "取得 Poe API 金鑰", + "poeBaseUrl": "Poe 基礎 URL", "fireworksApiKey": "Fireworks API 金鑰", "getFireworksApiKey": "取得 Fireworks API 金鑰", "deepSeekApiKey": "DeepSeek API 金鑰",