Compare commits

..

2 commits

Author SHA1 Message Date
roomote-v0[bot]
3826eba06f
Merge ba42cbdf87 into c3cae397a1 2026-04-07 01:14:18 +00:00
Kamil Jopek
c3cae397a1
feat: add Poe as an AI provider (#12015)
Some checks failed
Code QA Roo Code / platform-unit-test (ubuntu-latest) (push) Has been cancelled
Code QA Roo Code / platform-unit-test (windows-latest) (push) Has been cancelled
Code QA Roo Code / knip (push) Has been cancelled
Code QA Roo Code / compile (push) Has been cancelled
Code QA Roo Code / check-translations (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Nightly Publish / publish-nightly (push) Has been cancelled
2026-04-05 23:37:29 -04:00
42 changed files with 1116 additions and 54 deletions

View file

@ -23,6 +23,7 @@
"clean": "rimraf dist .turbo"
},
"dependencies": {
"ai-sdk-provider-poe": "2.0.18",
"zod": "3.25.76"
},
"devDependencies": {

View file

@ -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<TypicalProvider, ModelIdKey> = {
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: [] },

View file

@ -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":

View file

@ -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"

213
pnpm-lock.yaml generated
View file

@ -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:

View file

@ -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)
}

View file

@ -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<typeof import("ai")>()
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",
}),
)
})
})
})

View file

@ -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"])
})
})

View file

@ -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<Model
models = await getRooModels(rooBaseUrl, options.apiKey)
break
}
case "poe":
models = await getPoeModels(options.apiKey, options.baseUrl)
break
default: {
// Ensures router is exhaustively checked if RouterName is a strict union.
const exhaustiveCheck: never = provider

View file

@ -0,0 +1,44 @@
import type { ModelInfo, ModelRecord } from "@roo-code/types"
import { fetchPoeModels, getModels } from "ai-sdk-provider-poe/code"
export async function getPoeModels(apiKey?: string, baseURL?: string): Promise<ModelRecord> {
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 {}
}
}

View file

@ -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"

151
src/api/providers/poe.ts Normal file
View file

@ -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<Parameters<typeof streamText>[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<string> {
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}`)
}
}
}

View file

@ -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,
})

View file

@ -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,
})

View file

@ -955,6 +955,7 @@ export const webviewMessageHandler = async (
ollama: {},
lmstudio: {},
roo: {},
poe: {},
}
const safeGetModels = async (options: GetModelsOptions): Promise<ModelRecord> => {
@ -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)

View file

@ -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",

View file

@ -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<RouterName, object>
// Build the dynamic options union from the map, intersected with CommonFetchParams

View file

@ -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" && (
<Poe
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
organizationAllowList={organizationAllowList}
modelValidationError={modelValidationError}
simplifySettings={fromWelcomeView}
/>
)}
{selectedProvider === "roo" && (
<Roo
apiConfiguration={apiConfiguration}
@ -800,6 +815,17 @@ const ApiOptions = ({
}
onChange={(value) => setApiConfigurationField("consecutiveMistakeLimit", value)}
/>
{selectedProvider === "poe" && (
<VSCodeTextField
value={apiConfiguration?.poeBaseUrl || ""}
onInput={handleInputChange("poeBaseUrl")}
placeholder="https://api.poe.com/v1"
className="w-full">
<label className="block font-medium mb-1">
{t("settings:providers.poeBaseUrl")}
</label>
</VSCodeTextField>
)}
{selectedProvider === "openrouter" &&
openRouterModelProviders &&
Object.keys(openRouterModelProviders).length > 0 && (

View file

@ -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))

View file

@ -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<string | undefined>()
const poeErrorJustReceived = useRef(false)
useEffect(() => {
const handleMessage = (event: MessageEvent<ExtensionMessage>) => {
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(
<K extends keyof ProviderSettings, E>(
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 (
<>
<VSCodeTextField
value={apiConfiguration?.poeApiKey || ""}
type="password"
onInput={handleInputChange("poeApiKey")}
placeholder={t("settings:placeholders.apiKey")}
className="w-full">
<label className="block font-medium mb-1">{t("settings:providers.poeApiKey")}</label>
</VSCodeTextField>
<div className="text-sm text-vscode-descriptionForeground -mt-2">
{t("settings:providers.apiKeyStorageNotice")}
</div>
{!apiConfiguration?.poeApiKey && (
<VSCodeButtonLink href="https://poe.com/api_key" appearance="secondary">
{t("settings:providers.getPoeApiKey")}
</VSCodeButtonLink>
)}
<Button
variant="outline"
onClick={handleRefreshModels}
disabled={refreshStatus === "loading" || !apiConfiguration.poeApiKey}>
<div className="flex items-center gap-2">
{refreshStatus === "loading" ? (
<span className="codicon codicon-loading codicon-modifier-spin" />
) : (
<span className="codicon codicon-refresh" />
)}
{t("settings:providers.refreshModels.label")}
</div>
</Button>
{refreshStatus === "loading" && (
<div className="text-sm text-vscode-descriptionForeground">
{t("settings:providers.refreshModels.loading")}
</div>
)}
{refreshStatus === "success" && (
<div className="text-sm text-vscode-foreground">{t("settings:providers.refreshModels.success")}</div>
)}
{refreshStatus === "error" && (
<div className="text-sm text-vscode-errorForeground">
{refreshError || t("settings:providers.refreshModels.error")}
</div>
)}
<ModelPicker
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
defaultModelId={poeDefaultModelId}
models={routerModels?.poe ?? {}}
modelIdKey="apiModelId"
serviceName="Poe"
serviceUrl="https://poe.com"
organizationAllowList={organizationAllowList}
errorMessage={modelValidationError}
simplifySettings={simplifySettings}
onModelChange={(modelId) =>
handleModelChangeSideEffects("poe" as ProviderName, modelId, setApiConfigurationField)
}
/>
</>
)
}

View file

@ -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"

View file

@ -150,8 +150,10 @@ export const handleModelChangeSideEffects = <K extends keyof ProviderSettings>(
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])
}

View file

@ -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]

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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 कुंजी",

View file

@ -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",

View file

@ -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",

View file

@ -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キー",

View file

@ -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 키",

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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-ключ",

View file

@ -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ı",

View file

@ -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",

View file

@ -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 密钥",

View file

@ -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 金鑰",

View file

@ -45,6 +45,7 @@ describe("Model Validation Functions", () => {
lmstudio: {},
"vercel-ai-gateway": {},
roo: {},
poe: {},
}
const allowAllOrganization: OrganizationAllowList = {