diff --git a/packages/types/package.json b/packages/types/package.json index d66d87ac72d..f3fce492fe5 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.18", "zod": "3.25.76" }, "devDependencies": { 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..c9b6211ddd1 --- /dev/null +++ b/packages/types/src/providers/poe.ts @@ -0,0 +1,2 @@ +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 d95c2f02346..8ada6ca93f7 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.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 @@ -818,6 +821,9 @@ importers: '@vscode/codicons': specifier: ^0.0.36 version: 0.0.36 + ai-sdk-provider-poe: + 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 @@ -907,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 @@ -1426,6 +1432,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'} @@ -1480,6 +1492,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'} @@ -1492,6 +1516,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'} @@ -1726,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'} @@ -1764,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'} @@ -1805,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'} @@ -2532,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] @@ -2552,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] @@ -4886,6 +4928,11 @@ packages: resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==} engines: {node: '>= 8.0.0'} + ai-sdk-provider-poe@2.0.18: + resolution: {integrity: sha512-uzFR5Zq+PD6LfrqpYAr4dt2KmfOfS9a0Wh8QJsOaGI/vOByhW02P/0mj0NtOnOWF1RIUfBkQJo647XjrgnFjjg==} + peerDependencies: + ai: '>=6.0.0' + ai@6.0.77: resolution: {integrity: sha512-tyyhrRpCRFVlivdNIFLK8cexSBB2jwTqO0z1qJQagk+UxZ+MW8h5V8xsvvb+xdKDY482Y8KAm0mr7TDnPKvvlw==} engines: {node: '>=18'} @@ -7712,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] @@ -8976,6 +9023,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: @@ -9565,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'} @@ -10898,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'} @@ -11058,6 +11123,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) @@ -11121,6 +11192,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 @@ -11136,6 +11219,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 @@ -11767,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': @@ -11827,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': @@ -11856,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 @@ -12575,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 @@ -12587,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': @@ -12610,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': @@ -14389,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 @@ -15129,6 +15229,17 @@ snapshots: dependencies: humanize-ms: 1.2.1 + 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) + '@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) @@ -17232,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: @@ -18251,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: @@ -19384,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: {} @@ -20538,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) @@ -22165,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/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/__tests__/poe.spec.ts b/src/api/providers/__tests__/poe.spec.ts new file mode 100644 index 00000000000..50f229a243b --- /dev/null +++ b/src/api/providers/__tests__/poe.spec.ts @@ -0,0 +1,307 @@ +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, + })), + getPoeDefaultModelInfo: vitest.fn(() => ({ + maxTokens: 8192, + contextWindow: 200_000, + supportsImages: true, + supportsPromptCache: true, + inputPrice: 3, + outputPrice: 15, + })), +})) + +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 { poeDefaultModelId } from "@roo-code/types" +import { PoeHandler } from "../poe" + +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.contextWindow).toBeGreaterThan(0) + expect(result.info.maxTokens).toBeGreaterThan(0) + }) + }) + + 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 })) + }) + }) + + 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("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/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..536d222acd8 --- /dev/null +++ b/src/api/providers/poe.ts @@ -0,0 +1,151 @@ +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, + getPoeDefaultModelInfo, + type ModelInfo, + 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: ModelInfo = cached?.[id] ?? getPoeDefaultModelInfo() + 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 + 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, + } + 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", + } + if (this.options.modelMaxTokens) { + maxOutputTokens = this.options.modelMaxTokens + } + } + + 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 { + for await (const part of result.fullStream) { + for (const chunk of processAiSdkStreamPart(part)) { + yield chunk + } + } + + 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/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/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) diff --git a/src/package.json b/src/package.json index 7c4889abd89..19096bb4865 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": "2.0.18", "async-mutex": "^0.5.0", "axios": "^1.12.0", "cheerio": "^1.0.0", 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 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..7b79ed85107 --- /dev/null +++ b/webview-ui/src/components/settings/providers/Poe.tsx @@ -0,0 +1,166 @@ +import { useCallback, useState, useEffect, useRef } from "react" +import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react" +import { useQueryClient } from "@tanstack/react-query" + +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 queryClient = useQueryClient() + 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") + // Invalidate the react-query router models cache so + // validation in ApiOptions picks up the refreshed list. + queryClient.invalidateQueries({ queryKey: ["routerModels"] }) + } + } + } + } + + window.addEventListener("message", handleMessage) + return () => { + window.removeEventListener("message", handleMessage) + } + }, [refreshStatus, queryClient]) + + 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/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]) } 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/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/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", 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 金鑰", 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 = {