Merge branch 'RooCodeInc:main' into feat/fix-bedrock

This commit is contained in:
nangsontay 2026-04-09 20:42:26 +07:00 committed by GitHub
commit f89044e295
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
113 changed files with 3416 additions and 769 deletions

View file

@ -1,6 +0,0 @@
---
"roo-cline": patch
"@roo-code/types": patch
---
Add support for OpenAI `gpt-5.4-mini` and `gpt-5.4-nano` models.

View file

@ -1,5 +1,20 @@
# Roo Code Changelog
## 3.52.0
### Minor Changes
- Add Poe as an AI provider so users can access Poe models directly in Roo Code (PR #12015 by @kamilio)
- Improve the xAI provider by migrating it to the Responses API with reusable transform utilities (#11961 by @carlesso, PR #11962 by @carlesso)
- Fix MiniMax model listings and context window handling for more reliable configuration (#11999 by @Rexarrior, PR #12069 by @Rexarrior)
- Add xAI Grok-4.20 models and update the default xAI model selection (#11955 by @carlesso, PR #11956 by @carlesso)
- Add OpenAI GPT-5.4 mini and nano models to expand the available OpenAI model lineup (PR #11946 by @PeterDaveHello)
- Chore: include the automated version bump PR from the previous release cycle for complete release accounting (PR #11892 by @app/github-actions)
### Patch Changes
- Add support for OpenAI `gpt-5.4-mini` and `gpt-5.4-nano` models.
## 3.51.1
### Patch Changes

View file

@ -13,10 +13,11 @@
> Your AI-Powered Dev Team, Right in Your Editor
## What's New in v3.51.0
## What's New in v3.52.0
- Add support for OpenAI GPT-5.4 and GPT-5.3 Chat Latest so you can use the newest OpenAI chat models in Roo Code.
- Expose skills as slash commands with fallback execution to make reusable workflows faster to trigger.
- Add Poe as an AI provider so you can access Poe models directly in Roo Code.
- Improve the xAI provider with a Responses API migration, reusable transform utilities, and updated Grok-4.20 defaults.
- Fix MiniMax model listings and context window handling for more reliable setup.
<details>
<summary>🌐 Available languages</summary>
@ -40,7 +41,7 @@
- [简体中文](locales/zh-CN/README.md)
- [繁體中文](locales/zh-TW/README.md)
- ...
</details>
</details>
---

View file

@ -0,0 +1,3 @@
# @roo-code/web-evals
## 0.0.1

View file

@ -1,6 +1,6 @@
{
"name": "@roo-code/web-evals",
"version": "0.0.0",
"version": "0.0.1",
"type": "module",
"scripts": {
"lint": "eslint src --ext=ts,tsx --max-warnings=0",

View file

@ -0,0 +1,3 @@
# @roo-code/web-roo-code
## 0.0.1

View file

@ -1,6 +1,6 @@
{
"name": "@roo-code/web-roo-code",
"version": "0.0.0",
"version": "0.0.1",
"type": "module",
"scripts": {
"lint": "eslint src --ext=ts,tsx --max-warnings=0",
@ -18,7 +18,7 @@
"@radix-ui/react-navigation-menu": "^1.2.14",
"@radix-ui/react-slot": "^1.2.4",
"@roo-code/evals": "workspace:^",
"@roo-code/types": "^1.108.0",
"@roo-code/types": "workspace:^",
"@tanstack/react-query": "^5.90.20",
"@vercel/og": "^0.8.6",
"class-variance-authority": "^0.7.1",

9
locales/ca/README.md generated
View file

@ -13,10 +13,11 @@
> El teu equip de desenvolupament impulsat per IA, directament al teu editor
## Novetats de la v3.51.0
## Novetats de la v3.52.0
- S'ha afegit compatibilitat amb OpenAI GPT-5.4 i GPT-5.3 Chat Latest perquè puguis utilitzar els models de xat més nous d'OpenAI a Roo Code.
- Les skills es poden exposar com a ordres slash amb execució de reserva perquè els fluxos de treball reutilitzables s'activin més de pressa.
- S'ha afegit Poe com a proveïdor d'IA perquè puguis accedir directament als models de Poe a Roo Code.
- S'ha millorat el proveïdor xAI amb una migració a la Responses API, utilitats de transformació reutilitzables i els valors per defecte actualitzats de Grok-4.20.
- S'han corregit els llistats de models de MiniMax i la gestió de la finestra de context per oferir una configuració més fiable.
<details>
<summary>🌐 Idiomes disponibles</summary>
@ -40,7 +41,7 @@
- [简体中文](../zh-CN/README.md)
- [繁體中文](../zh-TW/README.md)
- ...
</details>
</details>
---

9
locales/de/README.md generated
View file

@ -13,10 +13,11 @@
> Dein KI-gestütztes Dev-Team direkt in deinem Editor
## Neu in v3.51.0
## Neu in v3.52.0
- Unterstützung für OpenAI GPT-5.4 und GPT-5.3 Chat Latest hinzugefügt, damit du die neuesten OpenAI-Chatmodelle in Roo Code nutzen kannst.
- Skills als Slash-Commands mit Fallback-Ausführung verfügbar gemacht, damit wiederverwendbare Workflows schneller ausgelöst werden können.
- Poe als KI-Anbieter hinzugefügt, damit du direkt in Roo Code auf Poe-Modelle zugreifen kannst.
- Den xAI-Anbieter mit einer Migration auf die Responses API, wiederverwendbaren Transformations-Utilities und aktualisierten Grok-4.20-Standards verbessert.
- MiniMax-Modelllisten und die Handhabung des Kontextfensters korrigiert, damit die Einrichtung zuverlässiger funktioniert.
<details>
<summary>🌐 Verfügbare Sprachen</summary>
@ -40,7 +41,7 @@
- [简体中文](../zh-CN/README.md)
- [繁體中文](../zh-TW/README.md)
- ...
</details>
</details>
---

9
locales/es/README.md generated
View file

@ -13,10 +13,11 @@
> Tu equipo de desarrollo con IA, directamente en tu editor
## Novedades de la v3.51.0
## Novedades de la v3.52.0
- Se añadió compatibilidad con OpenAI GPT-5.4 y GPT-5.3 Chat Latest para que puedas usar los modelos de chat más recientes de OpenAI en Roo Code.
- Las skills ahora pueden exponerse como comandos slash con ejecución de respaldo para activar más rápido los flujos de trabajo reutilizables.
- Se añadió Poe como proveedor de IA para que puedas acceder directamente a los modelos de Poe en Roo Code.
- Se mejoró el proveedor xAI con una migración a la Responses API, utilidades de transformación reutilizables y valores predeterminados actualizados de Grok-4.20.
- Se corrigieron los listados de modelos de MiniMax y el manejo de la ventana de contexto para una configuración más fiable.
<details>
<summary>🌐 Idiomas disponibles</summary>
@ -40,7 +41,7 @@
- [简体中文](../zh-CN/README.md)
- [繁體中文](../zh-TW/README.md)
- ...
</details>
</details>
---

9
locales/fr/README.md generated
View file

@ -13,10 +13,11 @@
> Ton équipe de dev propulsée par l'IA, directement dans ton éditeur
## Nouveautés de la v3.51.0
## Nouveautés de la v3.52.0
- Ajout de la prise en charge dOpenAI GPT-5.4 et de GPT-5.3 Chat Latest pour que tu puisses utiliser les modèles de chat OpenAI les plus récents dans Roo Code.
- Les skills peuvent désormais être exposées comme slash commands avec une exécution de secours pour déclencher plus vite des workflows réutilisables.
- Ajout de Poe comme fournisseur dIA pour que tu puisses accéder directement aux modèles Poe dans Roo Code.
- Amélioration du fournisseur xAI avec une migration vers la Responses API, des utilitaires de transformation réutilisables et des valeurs par défaut Grok-4.20 mises à jour.
- Correction des listes de modèles MiniMax et de la gestion de la fenêtre de contexte pour une configuration plus fiable.
<details>
<summary>🌐 Langues disponibles</summary>
@ -40,7 +41,7 @@
- [简体中文](../zh-CN/README.md)
- [繁體中文](../zh-TW/README.md)
- ...
</details>
</details>
---

9
locales/hi/README.md generated
View file

@ -13,10 +13,11 @@
> तुम्हारी AI-संचालित डेवलपमेंट टीम, सीधे तुम्हारे एडिटर में
## v3.51.0 में नया क्या है
## v3.52.0 में नया क्या है
- OpenAI GPT-5.4 और GPT-5.3 Chat Latest के लिए सपोर्ट जोड़ा गया है, ताकि आप Roo Code में OpenAI के सबसे नए chat models इस्तेमाल कर सकें।
- अब skills को slash commands के रूप में fallback execution के साथ expose किया जा सकता है, जिससे reusable workflows को तेज़ी से trigger किया जा सके।
- Poe को AI provider के रूप में जोड़ा गया है, ताकि आप Roo Code में सीधे Poe models तक पहुँच सकें।
- xAI provider को Responses API migration, reusable transform utilities, और updated Grok-4.20 defaults के साथ बेहतर बनाया गया है।
- ज़्यादा भरोसेमंद setup के लिए MiniMax model listings और context window handling को ठीक किया गया है।
<details>
<summary>🌐 उपलब्ध भाषाएँ</summary>
@ -40,7 +41,7 @@
- [简体中文](../zh-CN/README.md)
- [繁體中文](../zh-TW/README.md)
- ...
</details>
</details>
---

9
locales/id/README.md generated
View file

@ -13,10 +13,11 @@
> Tim dev bertenaga AI-mu, langsung di editor kamu
## Yang Baru di v3.51.0
## Yang Baru di v3.52.0
- Menambahkan dukungan untuk OpenAI GPT-5.4 dan GPT-5.3 Chat Latest agar kamu bisa memakai model chat OpenAI terbaru di Roo Code.
- Skill kini bisa diekspos sebagai slash command dengan eksekusi fallback supaya workflow yang bisa dipakai ulang lebih cepat dijalankan.
- Menambahkan Poe sebagai provider AI agar kamu bisa langsung mengakses model Poe di Roo Code.
- Meningkatkan provider xAI dengan migrasi ke Responses API, utilitas transformasi yang bisa dipakai ulang, dan default Grok-4.20 yang diperbarui.
- Memperbaiki daftar model MiniMax dan penanganan context window agar penyiapannya lebih andal.
<details>
<summary>🌐 Bahasa yang tersedia</summary>
@ -40,7 +41,7 @@
- [简体中文](../zh-CN/README.md)
- [繁體中文](../zh-TW/README.md)
- ...
</details>
</details>
---

9
locales/it/README.md generated
View file

@ -13,10 +13,11 @@
> Il tuo team di sviluppo con IA, direttamente nel tuo editor
## Novità in v3.51.0
## Novità in v3.52.0
- Aggiunto il supporto per OpenAI GPT-5.4 e GPT-5.3 Chat Latest così puoi usare i modelli di chat OpenAI più recenti in Roo Code.
- Le skill ora possono essere esposte come slash command con esecuzione di fallback per attivare più rapidamente i workflow riutilizzabili.
- Aggiunto Poe come provider AI così puoi accedere direttamente ai modelli Poe in Roo Code.
- Migliorato il provider xAI con una migrazione alla Responses API, utility di trasformazione riutilizzabili e valori predefiniti aggiornati per Grok-4.20.
- Corretti gli elenchi dei modelli MiniMax e la gestione della finestra di contesto per una configurazione più affidabile.
<details>
<summary>🌐 Lingue disponibili</summary>
@ -40,7 +41,7 @@
- [简体中文](../zh-CN/README.md)
- [繁體中文](../zh-TW/README.md)
- ...
</details>
</details>
---

9
locales/ja/README.md generated
View file

@ -13,10 +13,11 @@
> あなたのエディタの中に、AIで強化された開発チームを
## v3.51.0 の新機能
## v3.52.0 の新機能
- OpenAI GPT-5.4 と GPT-5.3 Chat Latest のサポートを追加し、Roo Code で OpenAI の最新チャットモデルを使えるようにしました。
- スキルをスラッシュコマンドとして公開し、フォールバック実行にも対応したことで、再利用可能なワークフローをより素早く呼び出せます。
- Poe を AI プロバイダーとして追加し、Roo Code から Poe のモデルに直接アクセスできるようにしました。
- xAI プロバイダーを Responses API への移行、再利用可能な変換ユーティリティ、更新された Grok-4.20 のデフォルト設定で改善しました。
- より信頼性の高いセットアップのために、MiniMax のモデル一覧とコンテキストウィンドウの扱いを修正しました。
<details>
<summary>🌐 利用可能な言語</summary>
@ -40,7 +41,7 @@
- [简体中文](../zh-CN/README.md)
- [繁體中文](../zh-TW/README.md)
- ...
</details>
</details>
---

9
locales/ko/README.md generated
View file

@ -13,10 +13,11 @@
> AI로 강화된 너의 개발 팀, 네 에디터 안에
## v3.51.0의 새로운 기능
## v3.52.0의 새로운 기능
- OpenAI GPT-5.4와 GPT-5.3 Chat Latest 지원을 추가해 Roo Code에서 최신 OpenAI 채팅 모델을 사용할 수 있어요.
- 이제 스킬을 슬래시 명령어로 노출하고 fallback 실행도 지원해 재사용 가능한 워크플로를 더 빠르게 트리거할 수 있어요.
- Poe를 AI 제공업체로 추가해 Roo Code에서 Poe 모델에 직접 접근할 수 있게 했어요.
- Responses API 마이그레이션, 재사용 가능한 변환 유틸리티, 업데이트된 Grok-4.20 기본값으로 xAI 제공업체를 개선했어요.
- 더 안정적인 설정을 위해 MiniMax 모델 목록과 컨텍스트 윈도우 처리를 수정했어요.
<details>
<summary>🌐 사용 가능한 언어</summary>
@ -40,7 +41,7 @@
- [简体中文](../zh-CN/README.md)
- [繁體中文](../zh-TW/README.md)
- ...
</details>
</details>
---

9
locales/nl/README.md generated
View file

@ -13,10 +13,11 @@
> Jouw AI-aangedreven dev-team, rechtstreeks in je editor
## Nieuw in v3.51.0
## Nieuw in v3.52.0
- Ondersteuning toegevoegd voor OpenAI GPT-5.4 en GPT-5.3 Chat Latest, zodat je de nieuwste OpenAI-chatmodellen in Roo Code kunt gebruiken.
- Skills beschikbaar gemaakt als slash-commands met fallback-uitvoering, zodat herbruikbare workflows sneller te starten zijn.
- Poe toegevoegd als AI-provider, zodat je in Roo Code direct toegang hebt tot Poe-modellen.
- De xAI-provider verbeterd met een migratie naar de Responses API, herbruikbare transformatiehulpmiddelen en bijgewerkte Grok-4.20-standaardinstellingen.
- MiniMax-modellijsten en de afhandeling van het contextvenster gerepareerd voor een betrouwbaardere configuratie.
<details>
<summary>🌐 Beschikbare talen</summary>
@ -40,7 +41,7 @@
- [简体中文](../zh-CN/README.md)
- [繁體中文](../zh-TW/README.md)
- ...
</details>
</details>
---

9
locales/pl/README.md generated
View file

@ -13,10 +13,11 @@
> Twój zespół deweloperski zasilany AI — prosto w edytorze
## Nowości w v3.51.0
## Nowości w v3.52.0
- Dodano obsługę OpenAI GPT-5.4 i GPT-5.3 Chat Latest, żebyś mógł używać najnowszych modeli czatu OpenAI w Roo Code.
- Skills można teraz udostępniać jako slash commandy z wykonaniem awaryjnym, żeby szybciej uruchamiać wielokrotnego użytku workflowy.
- Dodano Poe jako dostawcę AI, żebyś mógł bezpośrednio uzyskać dostęp do modeli Poe w Roo Code.
- Ulepszono dostawcę xAI dzięki migracji do Responses API, narzędziom transformacji wielokrotnego użytku i zaktualizowanym domyślnym ustawieniom Grok-4.20.
- Naprawiono listy modeli MiniMax i obsługę okna kontekstu, żeby konfiguracja była bardziej niezawodna.
<details>
<summary>🌐 Dostępne języki</summary>
@ -40,7 +41,7 @@
- [简体中文](../zh-CN/README.md)
- [繁體中文](../zh-TW/README.md)
- ...
</details>
</details>
---

View file

@ -13,10 +13,11 @@
> Seu time de desenvolvimento com IA, direto no seu editor
## Novidades na v3.51.0
## Novidades na v3.52.0
- Adicionamos suporte ao OpenAI GPT-5.4 e ao GPT-5.3 Chat Latest para você usar os modelos de chat mais novos da OpenAI no Roo Code.
- Agora as skills podem ser expostas como comandos slash com execução de fallback para acionar workflows reutilizáveis mais rápido.
- Adicionamos o Poe como provedor de IA para você acessar os modelos do Poe diretamente no Roo Code.
- Melhoramos o provedor xAI com uma migração para a Responses API, utilitários de transformação reutilizáveis e padrões atualizados do Grok-4.20.
- Corrigimos as listagens de modelos do MiniMax e o tratamento da janela de contexto para uma configuração mais confiável.
<details>
<summary>🌐 Idiomas disponíveis</summary>
@ -40,7 +41,7 @@
- [简体中文](../zh-CN/README.md)
- [繁體中文](../zh-TW/README.md)
- ...
</details>
</details>
---

9
locales/ru/README.md generated
View file

@ -13,10 +13,11 @@
> Твоя команда разработчиков на ИИ — прямо в редакторе
## Что нового в v3.51.0
## Что нового в v3.52.0
- Добавлена поддержка OpenAI GPT-5.4 и GPT-5.3 Chat Latest, чтобы ты мог использовать новейшие чат-модели OpenAI в Roo Code.
- Skills теперь можно открывать как slash-команды с резервным выполнением, чтобы быстрее запускать переиспользуемые рабочие процессы.
- Добавлен Poe как AI-провайдер, чтобы ты мог напрямую получать доступ к моделям Poe в Roo Code.
- Улучшен провайдер xAI: выполнена миграция на Responses API, добавлены переиспользуемые утилиты трансформации и обновлены настройки Grok-4.20 по умолчанию.
- Исправлены списки моделей MiniMax и обработка окна контекста для более надёжной настройки.
<details>
<summary>🌐 Доступные языки</summary>
@ -40,7 +41,7 @@
- [简体中文](../zh-CN/README.md)
- [繁體中文](../zh-TW/README.md)
- ...
</details>
</details>
---

9
locales/tr/README.md generated
View file

@ -13,10 +13,11 @@
> AI destekli dev ekibin, doğrudan editörünün içinde
## v3.51.0'daki Yenilikler
## v3.52.0'daki Yenilikler
- Roo Code içinde en yeni OpenAI sohbet modellerini kullanabilmen için OpenAI GPT-5.4 ve GPT-5.3 Chat Latest desteği eklendi.
- Skills artık fallback yürütmeyle slash komutları olarak sunulabiliyor, böylece yeniden kullanılabilir iş akışları daha hızlı tetiklenebiliyor.
- Poe, Roo Code içinde Poe modellerine doğrudan erişebilmen için bir AI sağlayıcısı olarak eklendi.
- xAI sağlayıcısı, Responses API geçişi, yeniden kullanılabilir dönüştürme yardımcıları ve güncellenmiş Grok-4.20 varsayılanlarıyla iyileştirildi.
- Daha güvenilir bir kurulum için MiniMax model listeleri ve context window işleme düzeltildi.
<details>
<summary>🌐 Mevcut diller</summary>
@ -40,7 +41,7 @@
- [简体中文](../zh-CN/README.md)
- [繁體中文](../zh-TW/README.md)
- ...
</details>
</details>
---

9
locales/vi/README.md generated
View file

@ -13,10 +13,11 @@
> Đội ngũ dev dùng AI của bạn, ngay trong trình chỉnh sửa
## Điểm mới trong v3.51.0
## Điểm mới trong v3.52.0
- Đã thêm hỗ trợ cho OpenAI GPT-5.4 và GPT-5.3 Chat Latest để bạn có thể dùng các mô hình chat OpenAI mới nhất trong Roo Code.
- Skills giờ có thể được cung cấp dưới dạng lệnh slash kèm thực thi dự phòng để kích hoạt các quy trình tái sử dụng nhanh hơn.
- Đã thêm Poe làm nhà cung cấp AI để bạn có thể truy cập trực tiếp các model Poe trong Roo Code.
- Đã cải thiện nhà cung cấp xAI với quá trình chuyển sang Responses API, các tiện ích chuyển đổi có thể tái sử dụng và các giá trị mặc định Grok-4.20 đã được cập nhật.
- Đã sửa danh sách model MiniMax và cách xử lý context window để thiết lập đáng tin cậy hơn.
<details>
<summary>🌐 Các ngôn ngữ có sẵn</summary>
@ -40,7 +41,7 @@
- [简体中文](../zh-CN/README.md)
- [繁體中文](../zh-TW/README.md)
- ...
</details>
</details>
---

View file

@ -13,10 +13,11 @@
> 你的 AI 驱动开发团队,就在你的编辑器里
## v3.51.0 新增内容
## v3.52.0 新增内容
- 已添加对 OpenAI GPT-5.4 和 GPT-5.3 Chat Latest 的支持,让你可以在 Roo Code 中使用 OpenAI 最新的聊天模型。
- 现在可以将 skills 公开为 slash 命令,并支持 fallback execution让可复用工作流触发得更快。
- 已将 Poe 添加为 AI 提供商,让你可以在 Roo Code 中直接访问 Poe 模型。
- 已改进 xAI 提供商,完成了向 Responses API 的迁移,加入了可复用的转换工具,并更新了 Grok-4.20 默认设置。
- 已修复 MiniMax 模型列表和上下文窗口处理方式,让配置更加可靠。
<details>
<summary>🌐 可用语言</summary>
@ -40,7 +41,7 @@
- [简体中文](../zh-CN/README.md)
- [繁體中文](../zh-TW/README.md)
- ...
</details>
</details>
---

View file

@ -13,10 +13,11 @@
> 您的 AI 驅動開發團隊,就在您的編輯器中
## v3.51.0 新功能
## v3.52.0 新功能
- 已新增對 OpenAI GPT-5.4 和 GPT-5.3 Chat Latest 的支援,讓你可以在 Roo Code 中使用 OpenAI 最新的聊天模型。
- 現在可以將 skills 公開為 slash 指令,並支援 fallback execution讓可重用工作流程觸發得更快。
- 已將 Poe 新增為 AI 供應商,讓你可以在 Roo Code 中直接存取 Poe 模型。
- 已改善 xAI 供應商,完成 Responses API 遷移、加入可重複使用的轉換工具,並更新了 Grok-4.20 預設值。
- 已修正 MiniMax 模型清單和上下文視窗處理方式,讓設定更加可靠。
<details>
<summary>🌐 支援語言</summary>
@ -40,7 +41,7 @@
- [简体中文](../zh-CN/README.md)
- [繁體中文](../zh-TW/README.md)
- ...
</details>
</details>
---

View file

@ -0,0 +1,3 @@
# @roo-code/cloud
## 0.0.1

View file

@ -1,7 +1,7 @@
{
"name": "@roo-code/cloud",
"description": "Roo Code Cloud services.",
"version": "0.0.0",
"version": "0.0.1",
"type": "module",
"exports": "./src/index.ts",
"scripts": {

View file

@ -0,0 +1,3 @@
# @roo-code/core
## 0.0.1

View file

@ -1,7 +1,7 @@
{
"name": "@roo-code/core",
"description": "Platform agnostic core functionality for Roo Code.",
"version": "0.0.0",
"version": "0.0.1",
"type": "module",
"exports": {
".": "./src/index.ts",

View file

@ -0,0 +1,3 @@
# @roo-code/evals
## 0.0.1

View file

@ -1,7 +1,7 @@
{
"name": "@roo-code/evals",
"description": "Roo Code evals.",
"version": "0.0.0",
"version": "0.0.1",
"type": "module",
"exports": "./src/index.ts",
"scripts": {

View file

@ -0,0 +1,3 @@
# @roo-code/ipc
## 0.0.1

View file

@ -1,7 +1,7 @@
{
"name": "@roo-code/ipc",
"description": "IPC server and client for remote Roo Code access.",
"version": "0.0.0",
"version": "0.0.1",
"type": "module",
"exports": "./src/index.ts",
"scripts": {

View file

@ -0,0 +1,3 @@
# @roo-code/telemetry
## 0.0.1

View file

@ -1,7 +1,7 @@
{
"name": "@roo-code/telemetry",
"description": "Roo Code telemetry service and clients.",
"version": "0.0.0",
"version": "0.0.1",
"type": "module",
"exports": "./src/index.ts",
"scripts": {

View file

@ -0,0 +1,7 @@
# @roo-code/types
## 0.0.1
### Patch Changes
- Add support for OpenAI `gpt-5.4-mini` and `gpt-5.4-nano` models.

View file

@ -1,6 +1,6 @@
{
"name": "@roo-code/types",
"version": "0.0.0",
"version": "0.0.1",
"type": "module",
"main": "./dist/index.cjs",
"exports": {
@ -20,9 +20,11 @@
"build": "tsup",
"build:watch": "tsup --watch --outDir npm/dist --onSuccess 'echo ✅ Types rebuilt to npm/dist'",
"npm:publish": "node scripts/publish-npm.cjs",
"clean": "rimraf dist .turbo"
"clean": "rimraf dist .turbo",
"generate:schema": "tsx scripts/generate-roomodes-schema.ts"
},
"dependencies": {
"ai-sdk-provider-poe": "2.0.18",
"zod": "3.25.76"
},
"devDependencies": {
@ -31,6 +33,8 @@
"@types/node": "^24.1.0",
"globals": "^16.3.0",
"tsup": "^8.4.0",
"vitest": "^3.2.3"
"ajv": "^8.18.0",
"vitest": "^3.2.3",
"zod-to-json-schema": "^3.25.1"
}
}

View file

@ -0,0 +1,25 @@
/**
* Generates the JSON Schema for .roomodes configuration files from the Zod
* schemas defined in packages/types/src/mode.ts.
*
* This ensures the schema stays in sync with the TypeScript types. Run via:
* pnpm --filter @roo-code/types generate:schema
*
* The output is written to schemas/roomodes.json at the repository root.
*/
import * as fs from "fs"
import * as path from "path"
import { fileURLToPath } from "url"
import { generateRoomodesJsonSchema } from "../src/roomodes-schema.js"
const jsonSchema = generateRoomodesJsonSchema()
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const repoRoot = path.resolve(__dirname, "../../..")
const outPath = path.join(repoRoot, "schemas", "roomodes.json")
fs.mkdirSync(path.dirname(outPath), { recursive: true })
fs.writeFileSync(outPath, JSON.stringify(jsonSchema, null, "\t") + "\n", "utf-8")
console.log(`Generated ${path.relative(repoRoot, outPath)}`)

View file

@ -0,0 +1,26 @@
import { describe, it, expect } from "vitest"
import * as fs from "fs"
import * as path from "path"
import { fileURLToPath } from "url"
import { generateRoomodesJsonSchema } from "../roomodes-schema.js"
/**
* This test verifies that the checked-in schemas/roomodes.json matches what
* would be generated from the current Zod schemas. If this test fails, run:
*
* pnpm --filter @roo-code/types generate:schema
*
* to regenerate the schema file.
*/
describe("roomodes schema sync", () => {
it("should match the dynamically generated schema from Zod types", () => {
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const schemaPath = path.resolve(__dirname, "../../../../schemas/roomodes.json")
const checkedIn = JSON.parse(fs.readFileSync(schemaPath, "utf-8"))
const generated = generateRoomodesJsonSchema()
expect(checkedIn).toEqual(generated)
})
})

View file

@ -0,0 +1,438 @@
/**
* Validates the generated schemas/roomodes.json against sample configurations
* using AJV. The schema itself is dynamically generated from the Zod types in
* packages/types/src/mode.ts -- see packages/types/scripts/generate-roomodes-schema.ts.
*
* A separate drift-detection test (roomodes-schema-sync.spec.ts) ensures the
* checked-in schema stays in sync with the Zod source of truth.
*/
import { describe, it, expect, beforeAll } from "vitest"
import Ajv, { type ValidateFunction } from "ajv"
import * as fs from "fs"
import * as path from "path"
import { fileURLToPath } from "url"
describe("roomodes JSON schema", () => {
let schema: Record<string, unknown>
let validate: ValidateFunction
beforeAll(() => {
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const schemaPath = path.resolve(__dirname, "../../../../schemas/roomodes.json")
schema = JSON.parse(fs.readFileSync(schemaPath, "utf-8"))
const ajv = new Ajv.default({ strict: false })
validate = ajv.compile(schema)
})
it("should be a valid JSON Schema", () => {
expect(validate).toBeDefined()
})
it("should accept a minimal valid .roomodes config", () => {
const config = {
customModes: [
{
slug: "my-mode",
name: "My Mode",
roleDefinition: "You are a helpful assistant.",
groups: ["read"],
},
],
}
const valid = validate(config)
expect(validate.errors).toBeNull()
expect(valid).toBe(true)
})
it("should accept a mode with all optional properties", () => {
const config = {
customModes: [
{
slug: "full-mode",
name: "Full Mode",
roleDefinition: "A complete mode definition.",
whenToUse: "Use when you need everything.",
description: "A mode with all properties.",
customInstructions: "Follow these additional rules.",
groups: ["read", "edit", "command", "mcp"],
source: "project",
},
],
}
const valid = validate(config)
expect(validate.errors).toBeNull()
expect(valid).toBe(true)
})
it("should accept the built-in architect mode with tuple-style edit group", () => {
const config = {
customModes: [
{
slug: "architect",
name: "Architect",
roleDefinition: "You are an experienced technical leader.",
whenToUse: "Use this mode when you need to plan.",
description: "Plan and design before implementation",
groups: ["read", ["edit", { fileRegex: "\\.md$", description: "Markdown files only" }], "mcp"],
source: "project",
},
],
}
const valid = validate(config)
expect(validate.errors).toBeNull()
expect(valid).toBe(true)
})
it("should accept a tuple group entry with only fileRegex", () => {
const config = {
customModes: [
{
slug: "restricted",
name: "Restricted",
roleDefinition: "Limited editor.",
groups: [["edit", { fileRegex: "\\.ts$" }]],
},
],
}
const valid = validate(config)
expect(validate.errors).toBeNull()
expect(valid).toBe(true)
})
it("should accept a tuple group entry with empty options", () => {
const config = {
customModes: [
{
slug: "empty-opts",
name: "Empty Options",
roleDefinition: "Mode with empty group options.",
groups: [["edit", {}]],
},
],
}
const valid = validate(config)
expect(validate.errors).toBeNull()
expect(valid).toBe(true)
})
it("should accept the modes tool group", () => {
const config = {
customModes: [
{
slug: "orchestrator",
name: "Orchestrator",
roleDefinition: "You orchestrate other modes.",
groups: ["read", "modes"],
},
],
}
const valid = validate(config)
expect(validate.errors).toBeNull()
expect(valid).toBe(true)
})
it("should reject a config missing customModes", () => {
const config = {}
const valid = validate(config)
expect(valid).toBe(false)
})
it("should reject a mode missing required slug", () => {
const config = {
customModes: [
{
name: "No Slug",
roleDefinition: "Missing slug.",
groups: ["read"],
},
],
}
const valid = validate(config)
expect(valid).toBe(false)
})
it("should reject a mode missing required groups", () => {
const config = {
customModes: [
{
slug: "no-groups",
name: "No Groups",
roleDefinition: "Missing groups.",
},
],
}
const valid = validate(config)
expect(valid).toBe(false)
})
it("should reject a slug with invalid characters", () => {
const config = {
customModes: [
{
slug: "invalid slug!",
name: "Bad Slug",
roleDefinition: "Invalid slug characters.",
groups: ["read"],
},
],
}
const valid = validate(config)
expect(valid).toBe(false)
})
it("should reject an invalid tool group name", () => {
const config = {
customModes: [
{
slug: "bad-group",
name: "Bad Group",
roleDefinition: "Invalid group name.",
groups: ["nonexistent"],
},
],
}
const valid = validate(config)
expect(valid).toBe(false)
})
it("should reject additional properties on CustomMode", () => {
const config = {
customModes: [
{
slug: "extra-props",
name: "Extra Props",
roleDefinition: "Has extra properties.",
groups: ["read"],
unknownField: true,
},
],
}
const valid = validate(config)
expect(valid).toBe(false)
})
it("should reject additional properties on GroupOptions", () => {
const config = {
customModes: [
{
slug: "bad-opts",
name: "Bad Options",
roleDefinition: "Extra options properties.",
groups: [["edit", { fileRegex: "\\.md$", unknownOpt: true }]],
},
],
}
const valid = validate(config)
expect(valid).toBe(false)
})
it("should reject a tuple with more than 2 elements", () => {
const config = {
customModes: [
{
slug: "big-tuple",
name: "Big Tuple",
roleDefinition: "Too many tuple elements.",
groups: [["edit", { fileRegex: "\\.md$" }, "extra"]],
},
],
}
const valid = validate(config)
expect(valid).toBe(false)
})
it("should reject an invalid source value", () => {
const config = {
customModes: [
{
slug: "bad-source",
name: "Bad Source",
roleDefinition: "Invalid source.",
groups: ["read"],
source: "unknown",
},
],
}
const valid = validate(config)
expect(valid).toBe(false)
})
it("should accept an empty customModes array", () => {
const config = {
customModes: [],
}
const valid = validate(config)
expect(validate.errors).toBeNull()
expect(valid).toBe(true)
})
it("should accept the browser tool group (deprecated but valid)", () => {
const config = {
customModes: [
{
slug: "browser-mode",
name: "Browser Mode",
roleDefinition: "A mode that uses the browser tool group.",
groups: ["read", "browser", "command"],
},
],
}
const valid = validate(config)
expect(validate.errors).toBeNull()
expect(valid).toBe(true)
})
it("should accept a browser tuple group entry", () => {
const config = {
customModes: [
{
slug: "browser-tuple",
name: "Browser Tuple",
roleDefinition: "A mode with browser tuple.",
groups: [["browser", { fileRegex: "\\.html$", description: "HTML files only" }]],
},
],
}
const valid = validate(config)
expect(validate.errors).toBeNull()
expect(valid).toBe(true)
})
it("should accept a mode with rulesFiles", () => {
const config = {
customModes: [
{
slug: "rules-mode",
name: "Rules Mode",
roleDefinition: "A mode with rules files.",
groups: ["read"],
rulesFiles: [
{
relativePath: "rule1.md",
content: "# Rule 1\nFollow this rule.",
},
{
relativePath: "subfolder/rule2.md",
content: "# Rule 2\nFollow this other rule.",
},
],
},
],
}
const valid = validate(config)
expect(validate.errors).toBeNull()
expect(valid).toBe(true)
})
it("should accept a mode with empty rulesFiles array", () => {
const config = {
customModes: [
{
slug: "empty-rules",
name: "Empty Rules",
roleDefinition: "A mode with empty rules files.",
groups: ["read"],
rulesFiles: [],
},
],
}
const valid = validate(config)
expect(validate.errors).toBeNull()
expect(valid).toBe(true)
})
it("should accept rulesFiles entries with only relativePath (content is optional)", () => {
const config = {
customModes: [
{
slug: "path-only-rules",
name: "Path Only Rules",
roleDefinition: "A mode with rules files that only have relativePath.",
groups: ["read"],
rulesFiles: [{ relativePath: "rule1.md" }],
},
],
}
const valid = validate(config)
expect(validate.errors).toBeNull()
expect(valid).toBe(true)
})
it("should reject rulesFiles entries missing required relativePath", () => {
const config = {
customModes: [
{
slug: "bad-rules",
name: "Bad Rules",
roleDefinition: "A mode with invalid rules files.",
groups: ["read"],
rulesFiles: [{ content: "some content" }],
},
],
}
const valid = validate(config)
expect(valid).toBe(false)
})
it("should reject rulesFiles entries with extra properties", () => {
const config = {
customModes: [
{
slug: "extra-rules",
name: "Extra Rules",
roleDefinition: "A mode with extra rule properties.",
groups: ["read"],
rulesFiles: [{ relativePath: "rule1.md", content: "content", extra: true }],
},
],
}
const valid = validate(config)
expect(valid).toBe(false)
})
it("should accept multiple modes", () => {
const config = {
customModes: [
{
slug: "mode-a",
name: "Mode A",
roleDefinition: "First mode.",
groups: ["read"],
},
{
slug: "mode-b",
name: "Mode B",
roleDefinition: "Second mode.",
groups: ["read", "edit"],
},
],
}
const valid = validate(config)
expect(validate.errors).toBeNull()
expect(valid).toBe(true)
})
})

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

@ -1,11 +1,11 @@
import type { ModelInfo } from "../model.js"
// Minimax
// https://platform.minimax.io/docs/guides/pricing
// https://platform.minimax.io/docs/api-reference/text-openai-api
// https://platform.minimax.io/docs/api-reference/text-anthropic-api
// https://platform.minimax.io/docs/guides/models-intro
// https://platform.minimax.io/docs/guides/pricing-paygo
// https://platform.minimax.io/docs/guides/pricing-tokenplan
export type MinimaxModelId = keyof typeof minimaxModels
export const minimaxDefaultModelId: MinimaxModelId = "MiniMax-M2.5"
export const minimaxDefaultModelId: MinimaxModelId = "MiniMax-M2.7"
export const minimaxModels = {
"MiniMax-M2.5": {
@ -21,11 +21,56 @@ export const minimaxModels = {
cacheWritesPrice: 0.375,
cacheReadsPrice: 0.03,
description:
"MiniMax M2.5, the latest MiniMax model with enhanced coding and agentic capabilities, building on the strengths of the M2 series.",
"MiniMax M2.5, the latest MiniMax model with enhanced coding and agentic capabilities, building on the strengths of the M2 series. See pricing at https://platform.minimax.io/docs/guides/pricing-paygo. Note: When using TokenPlan, usage is billed per request, not per token.",
},
"MiniMax-M2.5-highspeed": {
maxTokens: 16_384,
contextWindow: 204_800,
supportsImages: false,
supportsPromptCache: true,
includedTools: ["search_and_replace"],
excludedTools: ["apply_diff"],
preserveReasoning: true,
inputPrice: 0.6,
outputPrice: 2.4,
cacheWritesPrice: 0.375,
cacheReadsPrice: 0.03,
description:
"MiniMax M2.5 highspeed: same performance as M2.5 but with faster response (approximately 100 tps vs 60 tps). See pricing at https://platform.minimax.io/docs/guides/pricing-paygo. Requires TokenPlan High-Speed subscription for use with TokenPlan keys. Note: When using TokenPlan, usage is billed per request, not per token.",
},
"MiniMax-M2.7": {
maxTokens: 16_384,
contextWindow: 204_800,
supportsImages: false,
supportsPromptCache: true,
includedTools: ["search_and_replace"],
excludedTools: ["apply_diff"],
preserveReasoning: true,
inputPrice: 0.3,
outputPrice: 1.2,
cacheWritesPrice: 0.375,
cacheReadsPrice: 0.06,
description:
"MiniMax M2.7, the latest MiniMax model with recursive self-improvement capabilities. See pricing at https://platform.minimax.io/docs/guides/pricing-paygo. Note: When using TokenPlan, usage is billed per request, not per token.",
},
"MiniMax-M2.7-highspeed": {
maxTokens: 16_384,
contextWindow: 204_800,
supportsImages: false,
supportsPromptCache: true,
includedTools: ["search_and_replace"],
excludedTools: ["apply_diff"],
preserveReasoning: true,
inputPrice: 0.6,
outputPrice: 2.4,
cacheWritesPrice: 0.375,
cacheReadsPrice: 0.06,
description:
"MiniMax M2.7 highspeed: same performance as M2.7 but with faster response (approximately 100 tps vs 60 tps). See pricing at https://platform.minimax.io/docs/guides/pricing-paygo. Requires TokenPlan High-Speed subscription for use with TokenPlan keys. Note: When using TokenPlan, usage is billed per request, not per token.",
},
"MiniMax-M2": {
maxTokens: 16_384,
contextWindow: 192_000,
contextWindow: 204_800,
supportsImages: false,
supportsPromptCache: true,
includedTools: ["search_and_replace"],
@ -36,11 +81,11 @@ export const minimaxModels = {
cacheWritesPrice: 0.375,
cacheReadsPrice: 0.03,
description:
"MiniMax M2, a model born for Agents and code, featuring Top-tier Coding Capabilities, Powerful Agentic Performance, and Ultimate Cost-Effectiveness & Speed.",
"MiniMax M2, a model born for Agents and code, featuring Top-tier Coding Capabilities, Powerful Agentic Performance, and Ultimate Cost-Effectiveness & Speed. See pricing at https://platform.minimax.io/docs/guides/pricing-paygo. Note: When using TokenPlan, usage is billed per request, not per token.",
},
"MiniMax-M2-Stable": {
maxTokens: 16_384,
contextWindow: 192_000,
contextWindow: 204_800,
supportsImages: false,
supportsPromptCache: true,
includedTools: ["search_and_replace"],
@ -51,11 +96,11 @@ export const minimaxModels = {
cacheWritesPrice: 0.375,
cacheReadsPrice: 0.03,
description:
"MiniMax M2 Stable (High Concurrency, Commercial Use), a model born for Agents and code, featuring Top-tier Coding Capabilities, Powerful Agentic Performance, and Ultimate Cost-Effectiveness & Speed.",
"MiniMax M2 Stable (High Concurrency, Commercial Use), a model born for Agents and code, featuring Top-tier Coding Capabilities, Powerful Agentic Performance, and Ultimate Cost-Effectiveness & Speed. See pricing at https://platform.minimax.io/docs/guides/pricing-paygo. Note: When using TokenPlan, usage is billed per request, not per token.",
},
"MiniMax-M2.1": {
maxTokens: 16_384,
contextWindow: 192_000,
contextWindow: 204_800,
supportsImages: false,
supportsPromptCache: true,
includedTools: ["search_and_replace"],
@ -66,7 +111,22 @@ export const minimaxModels = {
cacheWritesPrice: 0.375,
cacheReadsPrice: 0.03,
description:
"MiniMax M2.1 builds on M2 with improved overall performance for agentic coding tasks and significantly faster response times.",
"MiniMax M2.1 builds on M2 with improved overall performance for agentic coding tasks and significantly faster response times. See pricing at https://platform.minimax.io/docs/guides/pricing-paygo. Note: When using TokenPlan, usage is billed per request, not per token.",
},
"MiniMax-M2.1-highspeed": {
maxTokens: 16_384,
contextWindow: 204_800,
supportsImages: false,
supportsPromptCache: true,
includedTools: ["search_and_replace"],
excludedTools: ["apply_diff"],
preserveReasoning: true,
inputPrice: 0.6,
outputPrice: 2.4,
cacheWritesPrice: 0.375,
cacheReadsPrice: 0.03,
description:
"MiniMax M2.1 highspeed: same performance as M2.1 but with faster response (approximately 100 tps vs 60 tps). See pricing at https://platform.minimax.io/docs/guides/pricing-paygo. Requires TokenPlan High-Speed subscription for use with TokenPlan keys. Note: When using TokenPlan, usage is billed per request, not per token.",
},
} as const satisfies Record<string, ModelInfo>

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"

View file

@ -3,10 +3,10 @@ import type { ModelInfo } from "../model.js"
// https://docs.x.ai/docs/api-reference
export type XAIModelId = keyof typeof xaiModels
export const xaiDefaultModelId: XAIModelId = "grok-4.20-beta-0309-reasoning"
export const xaiDefaultModelId: XAIModelId = "grok-4.20"
export const xaiModels = {
"grok-4.20-beta-0309-reasoning": {
"grok-4.20": {
maxTokens: 65_536,
contextWindow: 2_000_000,
supportsImages: true,
@ -15,21 +15,7 @@ export const xaiModels = {
outputPrice: 6.0,
cacheWritesPrice: 0.5,
cacheReadsPrice: 0.5,
description:
"xAI's Grok 4.20 reasoning model with 2M context. Reasoning is internal (not exposed via Chat Completions API).",
includedTools: ["search_replace"],
excludedTools: ["apply_diff"],
},
"grok-4.20-beta-0309-non-reasoning": {
maxTokens: 65_536,
contextWindow: 2_000_000,
supportsImages: true,
supportsPromptCache: true,
inputPrice: 2.0,
outputPrice: 6.0,
cacheWritesPrice: 0.5,
cacheReadsPrice: 0.5,
description: "xAI's Grok 4.20 non-reasoning model - faster inference with 2M context.",
description: "xAI's flagship Grok 4.20 model with 2M context and reasoning support via Responses API.",
includedTools: ["search_replace"],
excludedTools: ["apply_diff"],
},

View file

@ -0,0 +1,58 @@
/**
* Builds the Zod schema for .roomodes configuration files and converts it
* to JSON Schema (draft-07). This module is the single source of truth for
* both the generator script (scripts/generate-roomodes-schema.ts) and the
* drift-detection test.
*/
import { z } from "zod"
import { zodToJsonSchema } from "zod-to-json-schema"
import { toolGroups, deprecatedToolGroups } from "./tool.js"
import { groupOptionsSchema, modeConfigSchema } from "./mode.js"
// Build a ToolGroup enum that includes deprecated groups so existing configs
// still validate.
const allToolGroups = [...toolGroups, ...deprecatedToolGroups] as [string, ...string[]]
const allToolGroupsSchema = z.enum(allToolGroups)
// Build a GroupEntry schema that uses the extended tool group list.
const groupEntrySchema = z.union([allToolGroupsSchema, z.tuple([allToolGroupsSchema, groupOptionsSchema])])
// Build the RuleFile schema (used during import/export but not part of the
// core Zod types).
const ruleFileSchema = z.object({
relativePath: z.string(),
content: z.string().optional(),
})
// Build an extended ModeConfig schema that includes rulesFiles and uses the
// extended groups (with deprecated entries).
const exportedModeConfigSchema = modeConfigSchema.omit({ groups: true }).extend({
groups: z.array(groupEntrySchema),
rulesFiles: z.array(ruleFileSchema).optional(),
})
// Build the top-level .roomodes schema.
const roomodesZodSchema = z
.object({
customModes: z.array(exportedModeConfigSchema),
})
.strict()
/**
* Generates the JSON Schema object for .roomodes configuration files.
* Includes metadata fields ($id, title, description).
*/
export function generateRoomodesJsonSchema(): Record<string, unknown> {
const jsonSchema = zodToJsonSchema(roomodesZodSchema, {
$refStrategy: "none",
target: "jsonSchema7",
}) as Record<string, unknown>
jsonSchema["$id"] = "https://github.com/RooCodeInc/Roo-Code/blob/main/schemas/roomodes.json"
jsonSchema["title"] = "Roo Code Custom Modes"
jsonSchema["description"] = "Schema for .roomodes configuration files used by Roo Code to define custom modes."
return jsonSchema
}

266
pnpm-lock.yaml generated
View file

@ -350,8 +350,8 @@ importers:
specifier: workspace:^
version: link:../../packages/evals
'@roo-code/types':
specifier: ^1.108.0
version: 1.108.0
specifier: workspace:^
version: link:../../packages/types
'@tanstack/react-query':
specifier: ^5.90.20
version: 5.90.20(react@18.3.1)
@ -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
@ -722,6 +725,9 @@ importers:
'@types/node':
specifier: ^24.1.0
version: 24.2.1
ajv:
specifier: ^8.18.0
version: 8.18.0
globals:
specifier: ^16.3.0
version: 16.3.0
@ -731,6 +737,9 @@ importers:
vitest:
specifier: ^3.2.3
version: 3.2.4(@types/debug@4.1.12)(@types/node@24.2.1)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0)
zod-to-json-schema:
specifier: ^3.25.1
version: 3.25.1(zod@3.25.76)
packages/vscode-shim:
devDependencies:
@ -818,6 +827,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 +919,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 +1438,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 +1498,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 +1522,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 +1762,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 +1804,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 +1849,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 +2580,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 +2600,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]
@ -3850,9 +3898,6 @@ packages:
cpu: [x64]
os: [win32]
'@roo-code/types@1.108.0':
resolution: {integrity: sha512-0Of0DOuU125i1VTI2OTdL0j47xA+JrQr6KyYinYS+CPwUsszUJt2PeWyy/AZYI1w23FYrcCvh8FqycDuwXocSA==}
'@sec-ant/readable-stream@0.4.1':
resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==}
@ -4886,6 +4931,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'}
@ -4895,6 +4945,9 @@ packages:
ajv@6.12.6:
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
ajv@8.18.0:
resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==}
ansi-colors@4.1.3:
resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==}
engines: {node: '>=6'}
@ -5130,6 +5183,7 @@ packages:
basic-ftp@5.0.5:
resolution: {integrity: sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==}
engines: {node: '>=10.0.0'}
deprecated: Security vulnerability fixed in 5.2.0, please upgrade
better-path-resolve@1.0.0:
resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==}
@ -6577,6 +6631,9 @@ packages:
fast-shallow-equal@1.0.0:
resolution: {integrity: sha512-HPtaa38cPgWvaCFmRNhlc6NG7pv6NUHqjPgVAkWGoB9mQMwYB27/K0CvOM5Czy+qpT3e8XJ6Q4aPAnzpNpzNaw==}
fast-uri@3.1.0:
resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==}
fast-xml-parser@5.2.3:
resolution: {integrity: sha512-OdCYfRqfpuLUFonTNjvd30rCBZUneHpSQkCqfaeWQ9qrKcl6XlWeDBNVwGb+INAIxRshuN2jF+BE0L6gbBO2mw==}
hasBin: true
@ -7605,6 +7662,9 @@ packages:
json-schema-traverse@0.4.1:
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
json-schema-traverse@1.0.0:
resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
json-schema@0.4.0:
resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==}
@ -7712,8 +7772,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 +9036,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:
@ -9386,6 +9447,10 @@ packages:
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
engines: {node: '>=0.10.0'}
require-from-string@2.0.2:
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
engines: {node: '>=0.10.0'}
resize-observer-polyfill@1.5.1:
resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==}
@ -9565,6 +9630,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 +10968,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'}
@ -11002,6 +11084,11 @@ packages:
peerDependencies:
zod: 3.25.76
zod-to-json-schema@3.25.1:
resolution: {integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==}
peerDependencies:
zod: 3.25.76
zod-to-ts@1.2.0:
resolution: {integrity: sha512-x30XE43V+InwGpvTySRNz9kB7qFU8DlyEy7BsSTCHPH1R0QasMmHWZDCzYm6bVXtj/9NNJAZF3jW8rzFvH5OFA==}
peerDependencies:
@ -11058,6 +11145,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 +11214,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 +11241,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 +11879,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 +11945,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 +11976,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 +12697,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 +12709,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 +12732,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':
@ -13863,10 +13985,6 @@ snapshots:
'@rollup/rollup-win32-x64-msvc@4.40.2':
optional: true
'@roo-code/types@1.108.0':
dependencies:
zod: 3.25.76
'@sec-ant/readable-stream@0.4.1': {}
'@sevinf/maybe@0.5.0': {}
@ -14389,8 +14507,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 +15247,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)
@ -15144,6 +15273,13 @@ snapshots:
json-schema-traverse: 0.4.1
uri-js: 4.4.1
ajv@8.18.0:
dependencies:
fast-deep-equal: 3.1.3
fast-uri: 3.1.0
json-schema-traverse: 1.0.0
require-from-string: 2.0.2
ansi-colors@4.1.3: {}
ansi-escapes@7.0.0:
@ -16986,6 +17122,8 @@ snapshots:
fast-shallow-equal@1.0.0: {}
fast-uri@3.1.0: {}
fast-xml-parser@5.2.3:
dependencies:
strnum: 2.1.1
@ -17232,7 +17370,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:
@ -18115,6 +18253,8 @@ snapshots:
json-schema-traverse@0.4.1: {}
json-schema-traverse@1.0.0: {}
json-schema@0.4.0: {}
json-stable-stringify-without-jsonify@1.0.1: {}
@ -18251,20 +18391,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 +19524,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: {}
@ -20328,6 +20468,8 @@ snapshots:
require-directory@2.1.1: {}
require-from-string@2.0.2: {}
resize-observer-polyfill@1.5.1: {}
resolve-from@4.0.0: {}
@ -20538,6 +20680,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 +22310,9 @@ snapshots:
ws@8.18.3: {}
ws@8.20.0:
optional: true
xml-name-validator@5.0.0: {}
xml2js@0.5.0:
@ -22267,6 +22415,10 @@ snapshots:
dependencies:
zod: 3.25.76
zod-to-json-schema@3.25.1(zod@3.25.76):
dependencies:
zod: 3.25.76
zod-to-ts@1.2.0(typescript@5.8.3)(zod@3.25.76):
dependencies:
typescript: 5.8.3

96
schemas/roomodes.json Normal file
View file

@ -0,0 +1,96 @@
{
"type": "object",
"properties": {
"customModes": {
"type": "array",
"items": {
"type": "object",
"properties": {
"slug": {
"type": "string",
"pattern": "^[a-zA-Z0-9-]+$"
},
"name": {
"type": "string",
"minLength": 1
},
"roleDefinition": {
"type": "string",
"minLength": 1
},
"whenToUse": {
"type": "string"
},
"description": {
"type": "string"
},
"customInstructions": {
"type": "string"
},
"source": {
"type": "string",
"enum": ["global", "project"]
},
"groups": {
"type": "array",
"items": {
"anyOf": [
{
"type": "string",
"enum": ["read", "edit", "command", "mcp", "modes", "browser"]
},
{
"type": "array",
"minItems": 2,
"maxItems": 2,
"items": [
{
"type": "string",
"enum": ["read", "edit", "command", "mcp", "modes", "browser"]
},
{
"type": "object",
"properties": {
"fileRegex": {
"type": "string"
},
"description": {
"type": "string"
}
},
"additionalProperties": false
}
]
}
]
}
},
"rulesFiles": {
"type": "array",
"items": {
"type": "object",
"properties": {
"relativePath": {
"type": "string"
},
"content": {
"type": "string"
}
},
"required": ["relativePath"],
"additionalProperties": false
}
}
},
"required": ["slug", "name", "roleDefinition", "groups"],
"additionalProperties": false
}
}
},
"required": ["customModes"],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://github.com/RooCodeInc/Roo-Code/blob/main/schemas/roomodes.json",
"title": "Roo Code Custom Modes",
"description": "Schema for .roomodes configuration files used by Roo Code to define custom modes."
}

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

@ -112,7 +112,7 @@ describe("MiniMaxHandler", () => {
const model = handlerWithModel.getModel()
expect(model.id).toBe(testModelId)
expect(model.info).toEqual(minimaxModels[testModelId])
expect(model.info.contextWindow).toBe(192_000)
expect(model.info.contextWindow).toBe(204_800)
expect(model.info.maxTokens).toBe(16_384)
expect(model.info.supportsPromptCache).toBe(true)
expect(model.info.cacheWritesPrice).toBe(0.375)
@ -128,7 +128,7 @@ describe("MiniMaxHandler", () => {
const model = handlerWithModel.getModel()
expect(model.id).toBe(testModelId)
expect(model.info).toEqual(minimaxModels[testModelId])
expect(model.info.contextWindow).toBe(192_000)
expect(model.info.contextWindow).toBe(204_800)
expect(model.info.maxTokens).toBe(16_384)
expect(model.info.supportsPromptCache).toBe(true)
expect(model.info.cacheWritesPrice).toBe(0.375)
@ -191,10 +191,10 @@ describe("MiniMaxHandler", () => {
expect(model.info).toEqual(minimaxModels[minimaxDefaultModelId])
})
it("should default to MiniMax-M2.5 model", () => {
it("should default to MiniMax-M2.7 model", () => {
const handlerDefault = new MiniMaxHandler({ minimaxApiKey: "test-minimax-api-key" })
const model = handlerDefault.getModel()
expect(model.id).toBe("MiniMax-M2.5")
expect(model.id).toBe("MiniMax-M2.7")
})
})
@ -399,7 +399,7 @@ describe("MiniMaxHandler", () => {
it("should correctly configure MiniMax-M2 model properties", () => {
const model = minimaxModels["MiniMax-M2"]
expect(model.maxTokens).toBe(16_384)
expect(model.contextWindow).toBe(192_000)
expect(model.contextWindow).toBe(204_800)
expect(model.supportsImages).toBe(false)
expect(model.supportsPromptCache).toBe(true)
expect(model.inputPrice).toBe(0.3)
@ -411,7 +411,7 @@ describe("MiniMaxHandler", () => {
it("should correctly configure MiniMax-M2-Stable model properties", () => {
const model = minimaxModels["MiniMax-M2-Stable"]
expect(model.maxTokens).toBe(16_384)
expect(model.contextWindow).toBe(192_000)
expect(model.contextWindow).toBe(204_800)
expect(model.supportsImages).toBe(false)
expect(model.supportsPromptCache).toBe(true)
expect(model.inputPrice).toBe(0.3)
@ -419,5 +419,63 @@ describe("MiniMaxHandler", () => {
expect(model.cacheWritesPrice).toBe(0.375)
expect(model.cacheReadsPrice).toBe(0.03)
})
it("should correctly configure MiniMax-M2.7 model properties", () => {
const model = minimaxModels["MiniMax-M2.7"]
expect(model.maxTokens).toBe(16_384)
expect(model.contextWindow).toBe(204_800)
expect(model.supportsImages).toBe(false)
expect(model.supportsPromptCache).toBe(true)
expect(model.inputPrice).toBe(0.3)
expect(model.outputPrice).toBe(1.2)
expect(model.cacheWritesPrice).toBe(0.375)
expect(model.cacheReadsPrice).toBe(0.06)
})
it("should correctly configure MiniMax-M2.7-highspeed model properties", () => {
const model = minimaxModels["MiniMax-M2.7-highspeed"]
expect(model.maxTokens).toBe(16_384)
expect(model.contextWindow).toBe(204_800)
expect(model.supportsImages).toBe(false)
expect(model.supportsPromptCache).toBe(true)
expect(model.inputPrice).toBe(0.6)
expect(model.outputPrice).toBe(2.4)
expect(model.cacheWritesPrice).toBe(0.375)
expect(model.cacheReadsPrice).toBe(0.06)
})
it("should correctly configure MiniMax-M2.5-highspeed model properties", () => {
const model = minimaxModels["MiniMax-M2.5-highspeed"]
expect(model.maxTokens).toBe(16_384)
expect(model.contextWindow).toBe(204_800)
expect(model.supportsImages).toBe(false)
expect(model.supportsPromptCache).toBe(true)
expect(model.inputPrice).toBe(0.6)
expect(model.outputPrice).toBe(2.4)
expect(model.cacheWritesPrice).toBe(0.375)
expect(model.cacheReadsPrice).toBe(0.03)
})
it("should correctly configure MiniMax-M2.1-highspeed model properties", () => {
const model = minimaxModels["MiniMax-M2.1-highspeed"]
expect(model.maxTokens).toBe(16_384)
expect(model.contextWindow).toBe(204_800)
expect(model.supportsImages).toBe(false)
expect(model.supportsPromptCache).toBe(true)
expect(model.inputPrice).toBe(0.6)
expect(model.outputPrice).toBe(2.4)
expect(model.cacheWritesPrice).toBe(0.375)
expect(model.cacheReadsPrice).toBe(0.03)
})
it("should correctly configure MiniMax-M2.1 model properties with updated context window", () => {
const model = minimaxModels["MiniMax-M2.1"]
expect(model.contextWindow).toBe(204_800)
})
it("should correctly configure MiniMax-M2 model properties with updated context window", () => {
const model = minimaxModels["MiniMax-M2"]
expect(model.contextWindow).toBe(204_800)
})
})
})

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

@ -10,14 +10,16 @@ vitest.mock("@roo-code/telemetry", () => ({
},
}))
const mockCreate = vitest.fn()
const mockResponsesCreate = vitest.fn()
vitest.mock("openai", () => {
const mockConstructor = vitest.fn()
return {
__esModule: true,
default: mockConstructor.mockImplementation(() => ({ chat: { completions: { create: mockCreate } } })),
default: mockConstructor.mockImplementation(() => ({
responses: { create: mockResponsesCreate },
})),
}
})
@ -28,16 +30,30 @@ import { xaiDefaultModelId, xaiModels } from "@roo-code/types"
import { XAIHandler } from "../xai"
// Helper to create an async iterable from events
function mockStream(events: any[]) {
return {
[Symbol.asyncIterator]: () => {
let index = 0
return {
async next() {
if (index < events.length) {
return { done: false, value: events[index++] }
}
return { done: true, value: undefined }
},
}
},
}
}
describe("XAIHandler", () => {
let handler: XAIHandler
beforeEach(() => {
// Reset all mocks
vi.clearAllMocks()
mockCreate.mockClear()
mockResponsesCreate.mockClear()
mockCaptureException.mockClear()
// Create handler with mock
handler = new XAIHandler({})
})
@ -50,14 +66,9 @@ describe("XAIHandler", () => {
})
it("should use the provided API key", () => {
// Clear mocks before this specific test
vi.clearAllMocks()
// Create a handler with our API key
const xaiApiKey = "test-api-key"
new XAIHandler({ xaiApiKey })
// Verify the OpenAI constructor was called with our API key
expect(OpenAI).toHaveBeenCalledWith(
expect.objectContaining({
apiKey: xaiApiKey,
@ -71,111 +82,41 @@ describe("XAIHandler", () => {
expect(model.info).toEqual(xaiModels[xaiDefaultModelId])
})
test("should return specified model when valid model is provided", () => {
it("should return specified model when valid model is provided", () => {
const testModelId = "grok-3"
const handlerWithModel = new XAIHandler({ apiModelId: testModelId })
const model = handlerWithModel.getModel()
expect(model.id).toBe(testModelId)
expect(model.info).toEqual(xaiModels[testModelId])
})
it("should include reasoning_effort parameter for mini models", async () => {
const miniModelHandler = new XAIHandler({
apiModelId: "grok-3-mini",
reasoningEffort: "high",
})
it("should use Responses API (client.responses.create)", async () => {
mockResponsesCreate.mockResolvedValueOnce(mockStream([]))
// Setup mock for streaming response
mockCreate.mockImplementationOnce(() => {
return {
[Symbol.asyncIterator]: () => ({
async next() {
return { done: true }
},
}),
}
})
const stream = handler.createMessage("test prompt", [])
await stream.next()
// Start generating a message
const messageGenerator = miniModelHandler.createMessage("test prompt", [])
await messageGenerator.next() // Start the generator
// Check that reasoning_effort was included
expect(mockCreate).toHaveBeenCalledWith(
expect(mockResponsesCreate).toHaveBeenCalledWith(
expect.objectContaining({
reasoning_effort: "high",
model: xaiDefaultModelId,
instructions: "test prompt",
stream: true,
store: false,
include: ["reasoning.encrypted_content"],
}),
)
})
it("should not include reasoning_effort parameter for non-mini models", async () => {
const regularModelHandler = new XAIHandler({
apiModelId: "grok-3",
reasoningEffort: "high",
})
// Setup mock for streaming response
mockCreate.mockImplementationOnce(() => {
return {
[Symbol.asyncIterator]: () => ({
async next() {
return { done: true }
},
}),
}
})
// Start generating a message
const messageGenerator = regularModelHandler.createMessage("test prompt", [])
await messageGenerator.next() // Start the generator
// Check call args for reasoning_effort
const calls = mockCreate.mock.calls
const lastCall = calls[calls.length - 1][0]
expect(lastCall).not.toHaveProperty("reasoning_effort")
})
it("completePrompt method should return text from OpenAI API", async () => {
const expectedResponse = "This is a test response"
mockCreate.mockResolvedValueOnce({ choices: [{ message: { content: expectedResponse } }] })
const result = await handler.completePrompt("test prompt")
expect(result).toBe(expectedResponse)
})
it("should handle errors in completePrompt", async () => {
const errorMessage = "API error"
mockCreate.mockRejectedValueOnce(new Error(errorMessage))
await expect(handler.completePrompt("test prompt")).rejects.toThrow(`xAI completion error: ${errorMessage}`)
})
it("createMessage should yield text content from stream", async () => {
const testContent = "This is test content"
// Setup mock for streaming response
mockCreate.mockImplementationOnce(() => {
return {
[Symbol.asyncIterator]: () => ({
next: vi
.fn()
.mockResolvedValueOnce({
done: false,
value: {
choices: [{ delta: { content: testContent } }],
},
})
.mockResolvedValueOnce({ done: true }),
}),
}
})
mockResponsesCreate.mockResolvedValueOnce(
mockStream([{ type: "response.output_text.delta", delta: testContent }]),
)
// Create and consume the stream
const stream = handler.createMessage("system prompt", [])
const firstChunk = await stream.next()
// Verify the content
expect(firstChunk.done).toBe(false)
expect(firstChunk.value).toEqual({
type: "text",
@ -186,28 +127,13 @@ describe("XAIHandler", () => {
it("createMessage should yield reasoning content from stream", async () => {
const testReasoning = "Test reasoning content"
// Setup mock for streaming response
mockCreate.mockImplementationOnce(() => {
return {
[Symbol.asyncIterator]: () => ({
next: vi
.fn()
.mockResolvedValueOnce({
done: false,
value: {
choices: [{ delta: { reasoning_content: testReasoning } }],
},
})
.mockResolvedValueOnce({ done: true }),
}),
}
})
mockResponsesCreate.mockResolvedValueOnce(
mockStream([{ type: "response.reasoning_text.delta", delta: testReasoning }]),
)
// Create and consume the stream
const stream = handler.createMessage("system prompt", [])
const firstChunk = await stream.next()
// Verify the reasoning content
expect(firstChunk.done).toBe(false)
expect(firstChunk.value).toEqual({
type: "reasoning",
@ -215,373 +141,158 @@ describe("XAIHandler", () => {
})
})
it("createMessage should yield usage data from stream", async () => {
// Setup mock for streaming response that includes usage data
mockCreate.mockImplementationOnce(() => {
return {
[Symbol.asyncIterator]: () => ({
next: vi
.fn()
.mockResolvedValueOnce({
done: false,
value: {
choices: [{ delta: {} }], // Needs to have choices array to avoid error
usage: {
prompt_tokens: 10,
completion_tokens: 20,
cache_read_input_tokens: 5,
cache_creation_input_tokens: 15,
},
},
})
.mockResolvedValueOnce({ done: true }),
}),
}
})
it("createMessage should yield usage data from response.completed", async () => {
mockResponsesCreate.mockResolvedValueOnce(
mockStream([
{
type: "response.completed",
response: {
usage: {
input_tokens: 10,
output_tokens: 20,
input_tokens_details: { cached_tokens: 5 },
output_tokens_details: { reasoning_tokens: 8 },
},
},
},
]),
)
// Create and consume the stream
const stream = handler.createMessage("system prompt", [])
const firstChunk = await stream.next()
// Verify the usage data
expect(firstChunk.done).toBe(false)
expect(firstChunk.value).toEqual({
type: "usage",
inputTokens: 10,
outputTokens: 20,
cacheReadTokens: 5,
cacheWriteTokens: 15,
})
})
it("createMessage should pass correct parameters to OpenAI client", async () => {
// Setup a handler with specific model
const modelId = "grok-3"
const modelInfo = xaiModels[modelId]
const handlerWithModel = new XAIHandler({ apiModelId: modelId })
// Setup mock for streaming response
mockCreate.mockImplementationOnce(() => {
return {
[Symbol.asyncIterator]: () => ({
async next() {
return { done: true }
},
}),
}
})
// System prompt and messages
const systemPrompt = "Test system prompt"
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Test message" }]
// Start generating a message
const messageGenerator = handlerWithModel.createMessage(systemPrompt, messages)
await messageGenerator.next() // Start the generator
// Check that all parameters were passed correctly
expect(mockCreate).toHaveBeenCalledWith(
expect(firstChunk.value).toEqual(
expect.objectContaining({
model: modelId,
max_tokens: modelInfo.maxTokens,
temperature: 0,
messages: expect.arrayContaining([{ role: "system", content: systemPrompt }]),
stream: true,
stream_options: { include_usage: true },
type: "usage",
inputTokens: 10,
outputTokens: 20,
cacheReadTokens: 5,
reasoningTokens: 8,
}),
)
})
describe("Native Tool Calling", () => {
it("createMessage should yield tool_call from output_item.done", async () => {
mockResponsesCreate.mockResolvedValueOnce(
mockStream([
{
type: "response.output_item.done",
item: {
type: "function_call",
call_id: "call_123",
name: "test_tool",
arguments: '{"arg1":"value"}',
},
},
]),
)
const stream = handler.createMessage("system prompt", [])
const firstChunk = await stream.next()
expect(firstChunk.done).toBe(false)
expect(firstChunk.value).toEqual({
type: "tool_call",
id: "call_123",
name: "test_tool",
arguments: '{"arg1":"value"}',
})
})
it("should include tools in Responses API format", async () => {
const testTools = [
{
type: "function" as const,
function: {
name: "test_tool",
description: "A test tool",
parameters: {
type: "object",
properties: {
arg1: { type: "string", description: "First argument" },
},
required: ["arg1"],
},
parameters: { type: "object", properties: { arg1: { type: "string" } }, required: ["arg1"] },
},
},
]
it("should include tools in request when model supports native tools and tools are provided (native is default)", async () => {
const handlerWithTools = new XAIHandler({ apiModelId: "grok-3" })
mockResponsesCreate.mockResolvedValueOnce(mockStream([]))
mockCreate.mockImplementationOnce(() => {
return {
[Symbol.asyncIterator]: () => ({
async next() {
return { done: true }
},
}),
}
})
const messageGenerator = handlerWithTools.createMessage("test prompt", [], {
taskId: "test-task-id",
tools: testTools,
})
await messageGenerator.next()
expect(mockCreate).toHaveBeenCalledWith(
expect.objectContaining({
tools: expect.arrayContaining([
expect.objectContaining({
type: "function",
function: expect.objectContaining({
name: "test_tool",
}),
}),
]),
parallel_tool_calls: true,
}),
)
const stream = handler.createMessage("test prompt", [], {
taskId: "test-task-id",
tools: testTools,
})
await stream.next()
it("should include tool_choice when provided", async () => {
const handlerWithTools = new XAIHandler({ apiModelId: "grok-3" })
mockCreate.mockImplementationOnce(() => {
return {
[Symbol.asyncIterator]: () => ({
async next() {
return { done: true }
},
expect(mockResponsesCreate).toHaveBeenCalledWith(
expect.objectContaining({
tools: [
expect.objectContaining({
type: "function",
name: "test_tool",
description: "A test tool",
strict: true,
}),
}
})
const messageGenerator = handlerWithTools.createMessage("test prompt", [], {
taskId: "test-task-id",
tools: testTools,
],
tool_choice: "auto",
})
await messageGenerator.next()
parallel_tool_calls: true,
}),
)
})
expect(mockCreate).toHaveBeenCalledWith(
expect.objectContaining({
tool_choice: "auto",
it("completePrompt should return text from Responses API", async () => {
const expectedResponse = "This is a test response"
mockResponsesCreate.mockResolvedValueOnce({
output_text: expectedResponse,
})
const result = await handler.completePrompt("test prompt")
expect(result).toBe(expectedResponse)
})
it("should handle errors in completePrompt", async () => {
const errorMessage = "API error"
mockResponsesCreate.mockRejectedValueOnce(new Error(errorMessage))
await expect(handler.completePrompt("test prompt")).rejects.toThrow(`xAI completion error: ${errorMessage}`)
})
it("should include reasoning_effort for mini models", async () => {
const miniModelHandler = new XAIHandler({
apiModelId: "grok-3-mini",
reasoningEffort: "high",
})
mockResponsesCreate.mockResolvedValueOnce(mockStream([]))
const stream = miniModelHandler.createMessage("test prompt", [])
await stream.next()
expect(mockResponsesCreate).toHaveBeenCalledWith(
expect.objectContaining({
reasoning: expect.objectContaining({
reasoning_effort: "high",
}),
)
}),
)
})
it("should not include reasoning for non-mini models", async () => {
const regularHandler = new XAIHandler({
apiModelId: "grok-3",
reasoningEffort: "high",
})
it("should always include tools and tool_choice (tools are guaranteed to be present after ALWAYS_AVAILABLE_TOOLS)", async () => {
const handlerWithTools = new XAIHandler({ apiModelId: "grok-3" })
mockResponsesCreate.mockResolvedValueOnce(mockStream([]))
mockCreate.mockImplementationOnce(() => {
return {
[Symbol.asyncIterator]: () => ({
async next() {
return { done: true }
},
}),
}
})
const stream = regularHandler.createMessage("test prompt", [])
await stream.next()
const messageGenerator = handlerWithTools.createMessage("test prompt", [], {
taskId: "test-task-id",
})
await messageGenerator.next()
const callArgs = mockResponsesCreate.mock.calls[mockResponsesCreate.mock.calls.length - 1][0]
expect(callArgs).not.toHaveProperty("reasoning")
})
// Tools are now always present (minimum 6 from ALWAYS_AVAILABLE_TOOLS)
const callArgs = mockCreate.mock.calls[mockCreate.mock.calls.length - 1][0]
expect(callArgs).toHaveProperty("tools")
expect(callArgs).toHaveProperty("tool_choice")
expect(callArgs).toHaveProperty("parallel_tool_calls", true)
})
it("should handle errors in createMessage", async () => {
const errorMessage = "Stream error"
mockResponsesCreate.mockRejectedValueOnce(new Error(errorMessage))
it("should yield tool_call_partial chunks during streaming", async () => {
const handlerWithTools = new XAIHandler({ apiModelId: "grok-3" })
mockCreate.mockImplementationOnce(() => {
return {
[Symbol.asyncIterator]: () => ({
next: vi
.fn()
.mockResolvedValueOnce({
done: false,
value: {
choices: [
{
delta: {
tool_calls: [
{
index: 0,
id: "call_123",
function: {
name: "test_tool",
arguments: '{"arg1":',
},
},
],
},
},
],
},
})
.mockResolvedValueOnce({
done: false,
value: {
choices: [
{
delta: {
tool_calls: [
{
index: 0,
function: {
arguments: '"value"}',
},
},
],
},
},
],
},
})
.mockResolvedValueOnce({ done: true }),
}),
}
})
const stream = handlerWithTools.createMessage("test prompt", [], {
taskId: "test-task-id",
tools: testTools,
})
const chunks = []
for await (const chunk of stream) {
chunks.push(chunk)
}
expect(chunks).toContainEqual({
type: "tool_call_partial",
index: 0,
id: "call_123",
name: "test_tool",
arguments: '{"arg1":',
})
expect(chunks).toContainEqual({
type: "tool_call_partial",
index: 0,
id: undefined,
name: undefined,
arguments: '"value"}',
})
})
it("should set parallel_tool_calls based on metadata", async () => {
const handlerWithTools = new XAIHandler({ apiModelId: "grok-3" })
mockCreate.mockImplementationOnce(() => {
return {
[Symbol.asyncIterator]: () => ({
async next() {
return { done: true }
},
}),
}
})
const messageGenerator = handlerWithTools.createMessage("test prompt", [], {
taskId: "test-task-id",
tools: testTools,
parallelToolCalls: true,
})
await messageGenerator.next()
expect(mockCreate).toHaveBeenCalledWith(
expect.objectContaining({
parallel_tool_calls: true,
}),
)
})
it("should yield tool_call_end events when finish_reason is tool_calls", async () => {
// Import NativeToolCallParser to set up state
const { NativeToolCallParser } = await import("../../../core/assistant-message/NativeToolCallParser")
// Clear any previous state
NativeToolCallParser.clearRawChunkState()
const handlerWithTools = new XAIHandler({ apiModelId: "grok-3" })
mockCreate.mockImplementationOnce(() => {
return {
[Symbol.asyncIterator]: () => ({
next: vi
.fn()
.mockResolvedValueOnce({
done: false,
value: {
choices: [
{
delta: {
tool_calls: [
{
index: 0,
id: "call_xai_test",
function: {
name: "test_tool",
arguments: '{"arg1":"value"}',
},
},
],
},
},
],
},
})
.mockResolvedValueOnce({
done: false,
value: {
choices: [
{
delta: {},
finish_reason: "tool_calls",
},
],
usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },
},
})
.mockResolvedValueOnce({ done: true }),
}),
}
})
const stream = handlerWithTools.createMessage("test prompt", [], {
taskId: "test-task-id",
tools: testTools,
})
const chunks = []
for await (const chunk of stream) {
// Simulate what Task.ts does: when we receive tool_call_partial,
// process it through NativeToolCallParser to populate rawChunkTracker
if (chunk.type === "tool_call_partial") {
NativeToolCallParser.processRawChunk({
index: chunk.index,
id: chunk.id,
name: chunk.name,
arguments: chunk.arguments,
})
}
chunks.push(chunk)
}
// Should have tool_call_partial and tool_call_end
const partialChunks = chunks.filter((chunk) => chunk.type === "tool_call_partial")
const endChunks = chunks.filter((chunk) => chunk.type === "tool_call_end")
expect(partialChunks).toHaveLength(1)
expect(endChunks).toHaveLength(1)
expect(endChunks[0].id).toBe("call_xai_test")
})
const stream = handler.createMessage("test prompt", [])
await expect(stream.next()).rejects.toThrow(`xAI completion error: ${errorMessage}`)
})
})

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

@ -69,6 +69,12 @@ export class QwenCodeHandler extends BaseProvider implements SingleCompletionHan
this.client = new OpenAI({
apiKey: "dummy-key-will-be-replaced",
baseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1",
defaultHeaders: {
"User-Agent": `QwenCode/1.0.0 (${os.platform()}; ${os.arch()})`,
"X-DashScope-CacheControl": "enable",
"X-DashScope-UserAgent": `QwenCode/1.0.0 (${os.platform()}; ${os.arch()})`,
"X-DashScope-AuthType": "qwen-oauth",
},
})
}
return this.client

View file

@ -4,17 +4,18 @@ import OpenAI from "openai"
import { type XAIModelId, xaiDefaultModelId, xaiModels, ApiProviderError } from "@roo-code/types"
import { TelemetryService } from "@roo-code/telemetry"
import { NativeToolCallParser } from "../../core/assistant-message/NativeToolCallParser"
import type { ApiHandlerOptions } from "../../shared/api"
import { ApiStream } from "../transform/stream"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { convertToResponsesApiInput } from "../transform/responses-api-input"
import { processResponsesApiStream, createUsageNormalizer } from "../transform/responses-api-stream"
import { getModelParams } from "../transform/model-params"
import { DEFAULT_HEADERS } from "./constants"
import { BaseProvider } from "./base-provider"
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
import { handleOpenAIError } from "./utils/openai-error-handler"
import { isMcpTool } from "../../utils/mcp-name"
const XAI_DEFAULT_TEMPERATURE = 0
@ -53,118 +54,108 @@ export class XAIHandler extends BaseProvider implements SingleCompletionHandler
return { id, info, ...params }
}
/**
* Convert tools from OpenAI Chat Completions format to Responses API format.
* Chat Completions: { type: "function", function: { name, description, parameters } }
* Responses API: { type: "function", name, description, parameters }
*
* Uses base provider's convertToolSchemaForOpenAI() for schema hardening
* (additionalProperties: false, ensureAllRequired) and handles MCP tools.
*/
private mapResponseTools(tools?: any[]): any[] | undefined {
const converted = this.convertToolsForOpenAI(tools)
if (!converted?.length) {
return undefined
}
return converted
.filter((tool) => tool?.type === "function")
.map((tool) => {
const isMcp = isMcpTool(tool.function.name)
return {
type: "function",
name: tool.function.name,
description: tool.function.description,
parameters: isMcp
? tool.function.parameters
: this.convertToolSchemaForOpenAI(tool.function.parameters),
strict: !isMcp,
}
})
}
override async *createMessage(
systemPrompt: string,
messages: Anthropic.Messages.MessageParam[],
metadata?: ApiHandlerCreateMessageMetadata,
): ApiStream {
const { id: modelId, info: modelInfo, reasoning } = this.getModel()
const model = this.getModel()
// Use the OpenAI-compatible API.
const requestOptions = {
model: modelId,
max_tokens: modelInfo.maxTokens,
temperature: this.options.modelTemperature ?? XAI_DEFAULT_TEMPERATURE,
messages: [
{ role: "system", content: systemPrompt },
...convertToOpenAiMessages(messages),
] as OpenAI.Chat.ChatCompletionMessageParam[],
stream: true as const,
stream_options: { include_usage: true },
...(reasoning && reasoning),
tools: this.convertToolsForOpenAI(metadata?.tools),
tool_choice: metadata?.tool_choice,
parallel_tool_calls: metadata?.parallelToolCalls ?? true,
// Convert directly from Anthropic format to Responses API input format
const input = convertToResponsesApiInput(messages)
const responseTools = this.mapResponseTools(metadata?.tools)
// Build request options
const requestBody: Record<string, any> = {
model: model.id,
instructions: systemPrompt,
input: input,
stream: true,
store: false, // Don't store responses server-side for privacy
include: ["reasoning.encrypted_content"],
}
let stream
if (model.maxTokens) {
requestBody.max_output_tokens = model.maxTokens
}
if (model.temperature !== undefined) {
requestBody.temperature = model.temperature
}
if (responseTools) {
requestBody.tools = responseTools
// Cast tool_choice since metadata uses Chat Completions types but Responses API has its own type
requestBody.tool_choice = (metadata?.tool_choice ?? "auto") as any
requestBody.parallel_tool_calls = metadata?.parallelToolCalls ?? true
}
// Pass reasoning effort for models that support it (e.g., mini models)
if (model.reasoning) {
requestBody.reasoning = model.reasoning
}
let stream: AsyncIterable<any>
try {
stream = await this.client.chat.completions.create(requestOptions)
stream = (await this.client.responses.create({
...requestBody,
stream: true,
} as any)) as unknown as AsyncIterable<any>
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
const apiError = new ApiProviderError(errorMessage, this.providerName, modelId, "createMessage")
const apiError = new ApiProviderError(errorMessage, this.providerName, model.id, "createMessage")
TelemetryService.instance.captureException(apiError)
throw handleOpenAIError(error, this.providerName)
}
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
const finishReason = chunk.choices[0]?.finish_reason
if (delta?.content) {
yield {
type: "text",
text: delta.content,
}
}
if (delta && "reasoning_content" in delta && delta.reasoning_content) {
yield {
type: "reasoning",
text: delta.reasoning_content as string,
}
}
// Handle tool calls in stream - emit partial chunks for NativeToolCallParser
if (delta?.tool_calls) {
for (const toolCall of delta.tool_calls) {
yield {
type: "tool_call_partial",
index: toolCall.index,
id: toolCall.id,
name: toolCall.function?.name,
arguments: toolCall.function?.arguments,
}
}
}
// Process finish_reason to emit tool_call_end events
// This ensures tool calls are finalized even if the stream doesn't properly close
if (finishReason) {
const endEvents = NativeToolCallParser.processFinishReason(finishReason)
for (const event of endEvents) {
yield event
}
}
if (chunk.usage) {
// Extract detailed token information if available
// First check for prompt_tokens_details structure (real API response)
const promptDetails = "prompt_tokens_details" in chunk.usage ? chunk.usage.prompt_tokens_details : null
const cachedTokens = promptDetails && "cached_tokens" in promptDetails ? promptDetails.cached_tokens : 0
// Fall back to direct fields in usage (used in test mocks)
const readTokens =
cachedTokens ||
("cache_read_input_tokens" in chunk.usage ? (chunk.usage as any).cache_read_input_tokens : 0)
const writeTokens =
"cache_creation_input_tokens" in chunk.usage ? (chunk.usage as any).cache_creation_input_tokens : 0
yield {
type: "usage",
inputTokens: chunk.usage.prompt_tokens || 0,
outputTokens: chunk.usage.completion_tokens || 0,
cacheReadTokens: readTokens,
cacheWriteTokens: writeTokens,
}
}
}
const normalizeUsage = createUsageNormalizer()
yield* processResponsesApiStream(stream, normalizeUsage)
}
async completePrompt(prompt: string): Promise<string> {
const { id: modelId, reasoning } = this.getModel()
const model = this.getModel()
try {
const response = await this.client.chat.completions.create({
model: modelId,
messages: [{ role: "user", content: prompt }],
...(reasoning && reasoning),
const response = await this.client.responses.create({
model: model.id,
input: [{ role: "user", content: [{ type: "input_text", text: prompt }] }],
store: false,
})
return response.choices[0]?.message.content || ""
// output_text is a convenience field on the Responses API response
return response.output_text || ""
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
const apiError = new ApiProviderError(errorMessage, this.providerName, modelId, "completePrompt")
const apiError = new ApiProviderError(errorMessage, this.providerName, model.id, "completePrompt")
TelemetryService.instance.captureException(apiError)
throw handleOpenAIError(error, this.providerName)
}

View file

@ -0,0 +1,338 @@
import type { Anthropic } from "@anthropic-ai/sdk"
import { convertToResponsesApiInput } from "../responses-api-input"
describe("convertToResponsesApiInput", () => {
it("should return empty array for empty messages", () => {
expect(convertToResponsesApiInput([])).toEqual([])
})
describe("string content messages", () => {
it("should convert string content to input_text", () => {
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hello" }]
const result = convertToResponsesApiInput(messages)
expect(result).toEqual([{ role: "user", content: [{ type: "input_text", text: "Hello" }] }])
})
it("should convert assistant string content to output_text message format", () => {
const messages: Anthropic.Messages.MessageParam[] = [{ role: "assistant", content: "Hi there" }]
const result = convertToResponsesApiInput(messages)
expect(result).toEqual([
{
type: "message",
role: "assistant",
content: [{ type: "output_text", text: "Hi there" }],
},
])
})
})
describe("user messages with content blocks", () => {
it("should convert text blocks to input_text", () => {
const messages: Anthropic.Messages.MessageParam[] = [
{
role: "user",
content: [{ type: "text", text: "What is this?" }],
},
]
const result = convertToResponsesApiInput(messages)
expect(result).toEqual([{ role: "user", content: [{ type: "input_text", text: "What is this?" }] }])
})
it("should convert image blocks to input_image", () => {
const messages: Anthropic.Messages.MessageParam[] = [
{
role: "user",
content: [
{
type: "image",
source: { type: "base64", media_type: "image/png", data: "abc123" },
},
],
},
]
const result = convertToResponsesApiInput(messages)
expect(result).toEqual([
{
role: "user",
content: [
{
type: "input_image",
detail: "auto",
image_url: "data:image/png;base64,abc123",
},
],
},
])
})
it("should convert tool_result to function_call_output", () => {
const messages: Anthropic.Messages.MessageParam[] = [
{
role: "user",
content: [
{
type: "tool_result",
tool_use_id: "tool_123",
content: "Result text",
},
],
},
]
const result = convertToResponsesApiInput(messages)
expect(result).toEqual([
{
type: "function_call_output",
call_id: "tool_123",
output: "Result text",
},
])
})
it("should use (empty) for empty tool_result content", () => {
const messages: Anthropic.Messages.MessageParam[] = [
{
role: "user",
content: [
{
type: "tool_result",
tool_use_id: "tool_123",
content: "",
},
],
},
]
const result = convertToResponsesApiInput(messages)
expect(result).toEqual([
{
type: "function_call_output",
call_id: "tool_123",
output: "(empty)",
},
])
})
it("should extract text from array tool_result content", () => {
const messages: Anthropic.Messages.MessageParam[] = [
{
role: "user",
content: [
{
type: "tool_result",
tool_use_id: "tool_123",
content: [
{ type: "text", text: "Line 1" },
{ type: "text", text: "Line 2" },
],
},
],
},
]
const result = convertToResponsesApiInput(messages)
expect(result).toEqual([
{
type: "function_call_output",
call_id: "tool_123",
output: "Line 1\nLine 2",
},
])
})
it("should flush pending user content before tool_result", () => {
const messages: Anthropic.Messages.MessageParam[] = [
{
role: "user",
content: [
{ type: "text", text: "Here is context" },
{
type: "tool_result",
tool_use_id: "tool_123",
content: "Done",
},
],
},
]
const result = convertToResponsesApiInput(messages)
expect(result).toEqual([
{ role: "user", content: [{ type: "input_text", text: "Here is context" }] },
{ type: "function_call_output", call_id: "tool_123", output: "Done" },
])
})
})
describe("assistant messages with content blocks", () => {
it("should convert text blocks to output_text messages", () => {
const messages: Anthropic.Messages.MessageParam[] = [
{
role: "assistant",
content: [{ type: "text", text: "Here is my response" }],
},
]
const result = convertToResponsesApiInput(messages)
expect(result).toEqual([
{
type: "message",
role: "assistant",
content: [{ type: "output_text", text: "Here is my response" }],
},
])
})
it("should convert tool_use to function_call", () => {
const messages: Anthropic.Messages.MessageParam[] = [
{
role: "assistant",
content: [
{
type: "tool_use",
id: "call_abc",
name: "read_file",
input: { path: "/tmp/test.txt" },
},
],
},
]
const result = convertToResponsesApiInput(messages)
expect(result).toEqual([
{
type: "function_call",
call_id: "call_abc",
name: "read_file",
arguments: '{"path":"/tmp/test.txt"}',
},
])
})
it("should handle tool_use with string input", () => {
const messages: Anthropic.Messages.MessageParam[] = [
{
role: "assistant",
content: [
{
type: "tool_use",
id: "call_abc",
name: "run_command",
input: '{"cmd":"ls"}' as any,
},
],
},
]
const result = convertToResponsesApiInput(messages)
expect(result[0]).toEqual(
expect.objectContaining({
type: "function_call",
arguments: '{"cmd":"ls"}',
}),
)
})
it("should handle mixed text and tool_use in assistant message", () => {
const messages: Anthropic.Messages.MessageParam[] = [
{
role: "assistant",
content: [
{ type: "text", text: "Let me read that file" },
{
type: "tool_use",
id: "call_abc",
name: "read_file",
input: { path: "/tmp/test.txt" },
},
],
},
]
const result = convertToResponsesApiInput(messages)
expect(result).toHaveLength(2)
expect(result[0]).toEqual(
expect.objectContaining({
type: "message",
role: "assistant",
content: [{ type: "output_text", text: "Let me read that file" }],
}),
)
expect(result[1]).toEqual(
expect.objectContaining({
type: "function_call",
name: "read_file",
}),
)
})
})
describe("multi-turn conversations", () => {
it("should handle a complete tool use cycle", () => {
const messages: Anthropic.Messages.MessageParam[] = [
{ role: "user", content: "Read /tmp/test.txt" },
{
role: "assistant",
content: [
{
type: "tool_use",
id: "call_1",
name: "read_file",
input: { path: "/tmp/test.txt" },
},
],
},
{
role: "user",
content: [
{
type: "tool_result",
tool_use_id: "call_1",
content: "file contents here",
},
],
},
{
role: "assistant",
content: [{ type: "text", text: "The file contains: file contents here" }],
},
]
const result = convertToResponsesApiInput(messages)
expect(result).toHaveLength(4)
expect(result[0]).toEqual({ role: "user", content: [{ type: "input_text", text: "Read /tmp/test.txt" }] })
expect(result[1]).toEqual(
expect.objectContaining({ type: "function_call", call_id: "call_1", name: "read_file" }),
)
expect(result[2]).toEqual(
expect.objectContaining({
type: "function_call_output",
call_id: "call_1",
output: "file contents here",
}),
)
expect(result[3]).toEqual(
expect.objectContaining({
type: "message",
role: "assistant",
}),
)
})
})
})

View file

@ -0,0 +1,392 @@
import { processResponsesApiStream, createUsageNormalizer } from "../responses-api-stream"
// Helper to create an async iterable from events
async function* mockStream(events: any[]) {
for (const event of events) {
yield event
}
}
// Helper to collect all chunks from a stream
async function collectChunks(stream: AsyncGenerator<any>) {
const chunks = []
for await (const chunk of stream) {
chunks.push(chunk)
}
return chunks
}
const noopUsage = () => undefined
describe("processResponsesApiStream", () => {
describe("text deltas", () => {
it("should yield text chunk for response.output_text.delta", async () => {
const stream = mockStream([{ type: "response.output_text.delta", delta: "Hello world" }])
const chunks = await collectChunks(processResponsesApiStream(stream, noopUsage))
expect(chunks).toEqual([{ type: "text", text: "Hello world" }])
})
it("should yield text chunk for response.text.delta", async () => {
const stream = mockStream([{ type: "response.text.delta", delta: "Hello" }])
const chunks = await collectChunks(processResponsesApiStream(stream, noopUsage))
expect(chunks).toEqual([{ type: "text", text: "Hello" }])
})
it("should skip text delta with empty delta", async () => {
const stream = mockStream([{ type: "response.output_text.delta", delta: "" }])
const chunks = await collectChunks(processResponsesApiStream(stream, noopUsage))
expect(chunks).toEqual([])
})
})
describe("reasoning deltas", () => {
it("should yield reasoning chunk for response.reasoning_text.delta", async () => {
const stream = mockStream([{ type: "response.reasoning_text.delta", delta: "Let me think..." }])
const chunks = await collectChunks(processResponsesApiStream(stream, noopUsage))
expect(chunks).toEqual([{ type: "reasoning", text: "Let me think..." }])
})
it("should yield reasoning chunk for response.reasoning.delta", async () => {
const stream = mockStream([{ type: "response.reasoning.delta", delta: "Step 1" }])
const chunks = await collectChunks(processResponsesApiStream(stream, noopUsage))
expect(chunks).toEqual([{ type: "reasoning", text: "Step 1" }])
})
it("should yield reasoning chunk for response.reasoning_summary_text.delta", async () => {
const stream = mockStream([{ type: "response.reasoning_summary_text.delta", delta: "Summary" }])
const chunks = await collectChunks(processResponsesApiStream(stream, noopUsage))
expect(chunks).toEqual([{ type: "reasoning", text: "Summary" }])
})
it("should yield reasoning chunk for response.reasoning_summary.delta", async () => {
const stream = mockStream([{ type: "response.reasoning_summary.delta", delta: "Summary" }])
const chunks = await collectChunks(processResponsesApiStream(stream, noopUsage))
expect(chunks).toEqual([{ type: "reasoning", text: "Summary" }])
})
})
describe("tool calls", () => {
it("should yield tool_call for function_call in output_item.done", async () => {
const stream = mockStream([
{
type: "response.output_item.done",
item: {
type: "function_call",
call_id: "call_123",
name: "read_file",
arguments: '{"path":"/tmp/test.txt"}',
},
},
])
const chunks = await collectChunks(processResponsesApiStream(stream, noopUsage))
expect(chunks).toEqual([
{
type: "tool_call",
id: "call_123",
name: "read_file",
arguments: '{"path":"/tmp/test.txt"}',
},
])
})
it("should yield tool_call for tool_call type in output_item.done", async () => {
const stream = mockStream([
{
type: "response.output_item.done",
item: {
type: "tool_call",
tool_call_id: "call_456",
name: "write_file",
arguments: '{"path":"/tmp/out.txt"}',
},
},
])
const chunks = await collectChunks(processResponsesApiStream(stream, noopUsage))
expect(chunks).toEqual([
{
type: "tool_call",
id: "call_456",
name: "write_file",
arguments: '{"path":"/tmp/out.txt"}',
},
])
})
it("should handle object arguments by JSON.stringifying", async () => {
const stream = mockStream([
{
type: "response.output_item.done",
item: {
type: "function_call",
call_id: "call_789",
name: "test",
input: { key: "value" },
},
},
])
const chunks = await collectChunks(processResponsesApiStream(stream, noopUsage))
expect(chunks[0].arguments).toBe('{"key":"value"}')
})
it("should skip tool_call with missing call_id or name", async () => {
const stream = mockStream([
{
type: "response.output_item.done",
item: { type: "function_call", call_id: "", name: "test", arguments: "{}" },
},
{
type: "response.output_item.done",
item: { type: "function_call", call_id: "call_1", name: "", arguments: "{}" },
},
])
const chunks = await collectChunks(processResponsesApiStream(stream, noopUsage))
expect(chunks).toEqual([])
})
it("should yield tool_call_partial for function_call_arguments.delta", async () => {
const stream = mockStream([
{
type: "response.function_call_arguments.delta",
call_id: "call_123",
name: "read_file",
delta: '{"path":',
index: 0,
},
])
const chunks = await collectChunks(processResponsesApiStream(stream, noopUsage))
expect(chunks).toEqual([
{
type: "tool_call_partial",
index: 0,
id: "call_123",
name: "read_file",
arguments: '{"path":',
},
])
})
})
describe("completion and usage", () => {
it("should yield usage from response.completed", async () => {
const mockNormalize = (usage: any) => ({
type: "usage" as const,
inputTokens: usage.input_tokens,
outputTokens: usage.output_tokens,
})
const stream = mockStream([
{
type: "response.completed",
response: { usage: { input_tokens: 100, output_tokens: 50 } },
},
])
const chunks = await collectChunks(processResponsesApiStream(stream, mockNormalize))
expect(chunks).toEqual([{ type: "usage", inputTokens: 100, outputTokens: 50 }])
})
it("should yield usage from response.done", async () => {
const mockNormalize = (usage: any) => ({
type: "usage" as const,
inputTokens: usage.input_tokens,
outputTokens: usage.output_tokens,
})
const stream = mockStream([
{
type: "response.done",
response: { usage: { input_tokens: 200, output_tokens: 100 } },
},
])
const chunks = await collectChunks(processResponsesApiStream(stream, mockNormalize))
expect(chunks).toEqual([{ type: "usage", inputTokens: 200, outputTokens: 100 }])
})
it("should not yield usage when normalizer returns undefined", async () => {
const stream = mockStream([
{
type: "response.completed",
response: { usage: null },
},
])
const chunks = await collectChunks(processResponsesApiStream(stream, noopUsage))
expect(chunks).toEqual([])
})
})
describe("unknown events", () => {
it("should silently ignore unknown event types", async () => {
const stream = mockStream([
{ type: "response.created" },
{ type: "response.in_progress" },
{ type: "response.output_item.added", item: { type: "message" } },
{ type: "response.content_part.added" },
])
const chunks = await collectChunks(processResponsesApiStream(stream, noopUsage))
expect(chunks).toEqual([])
})
})
describe("full conversation stream", () => {
it("should handle a complete stream with reasoning, text, and usage", async () => {
const mockNormalize = (usage: any) => ({
type: "usage" as const,
inputTokens: usage.input_tokens,
outputTokens: usage.output_tokens,
})
const stream = mockStream([
{ type: "response.reasoning_text.delta", delta: "Thinking..." },
{ type: "response.reasoning_text.delta", delta: " done." },
{ type: "response.output_text.delta", delta: "The answer is " },
{ type: "response.output_text.delta", delta: "42." },
{
type: "response.completed",
response: { usage: { input_tokens: 50, output_tokens: 30 } },
},
])
const chunks = await collectChunks(processResponsesApiStream(stream, mockNormalize))
expect(chunks).toEqual([
{ type: "reasoning", text: "Thinking..." },
{ type: "reasoning", text: " done." },
{ type: "text", text: "The answer is " },
{ type: "text", text: "42." },
{ type: "usage", inputTokens: 50, outputTokens: 30 },
])
})
})
})
describe("createUsageNormalizer", () => {
it("should return undefined for null/undefined usage", () => {
const normalize = createUsageNormalizer()
expect(normalize(null)).toBeUndefined()
expect(normalize(undefined)).toBeUndefined()
})
it("should extract input and output tokens", () => {
const normalize = createUsageNormalizer()
const result = normalize({ input_tokens: 100, output_tokens: 50 })
expect(result).toEqual(
expect.objectContaining({
type: "usage",
inputTokens: 100,
outputTokens: 50,
}),
)
})
it("should extract cached tokens from input_tokens_details", () => {
const normalize = createUsageNormalizer()
const result = normalize({
input_tokens: 100,
output_tokens: 50,
input_tokens_details: { cached_tokens: 30 },
})
expect(result?.cacheReadTokens).toBe(30)
})
it("should extract cache write tokens", () => {
const normalize = createUsageNormalizer()
const result = normalize({
input_tokens: 100,
output_tokens: 50,
cache_creation_input_tokens: 15,
})
expect(result?.cacheWriteTokens).toBe(15)
})
it("should extract reasoning tokens from output_tokens_details", () => {
const normalize = createUsageNormalizer()
const result = normalize({
input_tokens: 100,
output_tokens: 50,
output_tokens_details: { reasoning_tokens: 20 },
})
expect(result?.reasoningTokens).toBe(20)
})
it("should not include reasoningTokens when not present", () => {
const normalize = createUsageNormalizer()
const result = normalize({ input_tokens: 100, output_tokens: 50 })
expect(result).not.toHaveProperty("reasoningTokens")
})
it("should compute totalCost when calculateCost is provided", () => {
const calculateCost = (input: number, output: number, cached: number) => 0.42
const normalize = createUsageNormalizer(calculateCost)
const result = normalize({ input_tokens: 100, output_tokens: 50 })
expect(result?.totalCost).toBe(0.42)
})
it("should not include totalCost when calculateCost is not provided", () => {
const normalize = createUsageNormalizer()
const result = normalize({ input_tokens: 100, output_tokens: 50 })
expect(result).not.toHaveProperty("totalCost")
})
it("should handle Chat Completions style field names as fallback", () => {
const normalize = createUsageNormalizer()
const result = normalize({
prompt_tokens: 100,
completion_tokens: 50,
prompt_tokens_details: { cached_tokens: 10 },
})
expect(result).toEqual(
expect.objectContaining({
inputTokens: 100,
outputTokens: 50,
cacheReadTokens: 10,
}),
)
})
})

View file

@ -0,0 +1,118 @@
import { Anthropic } from "@anthropic-ai/sdk"
/**
* Converts Anthropic-format messages to the OpenAI Responses API input format.
*
* Key differences from Chat Completions format:
* - Content parts use { type: "input_text" } instead of { type: "text" }
* - Images use { type: "input_image" } instead of { type: "image_url" }
* - Tool results use { type: "function_call_output", call_id } instead of { role: "tool", tool_call_id }
* - Tool uses become { type: "function_call", call_id, name, arguments } items
* - System prompt goes via the `instructions` parameter, not as a message
*
* @param messages - Array of Anthropic MessageParam objects
* @returns Array of Responses API input items
*/
export function convertToResponsesApiInput(messages: Anthropic.Messages.MessageParam[]): any[] {
const input: any[] = []
for (const message of messages) {
if (typeof message.content === "string") {
if (message.role === "assistant") {
// Assistant messages use output_text in the Responses API format
input.push({
type: "message",
role: "assistant",
content: [{ type: "output_text", text: message.content }],
})
} else {
input.push({
role: message.role,
content: [{ type: "input_text", text: message.content }],
})
}
continue
}
if (message.role === "assistant") {
for (const part of message.content) {
switch (part.type) {
case "text":
input.push({
type: "message",
role: "assistant",
content: [{ type: "output_text", text: part.text }],
})
break
case "tool_use":
input.push({
type: "function_call",
call_id: part.id,
name: part.name,
arguments: typeof part.input === "string" ? part.input : JSON.stringify(part.input ?? {}),
})
break
case "thinking":
// Include reasoning if it has content
if ((part as any).thinking && (part as any).thinking.trim().length > 0) {
input.push({
type: "message",
role: "assistant",
content: [{ type: "output_text", text: `[Thinking] ${(part as any).thinking}` }],
})
}
break
}
}
} else {
// User messages
const contentParts: any[] = []
for (const part of message.content) {
switch (part.type) {
case "text":
contentParts.push({ type: "input_text", text: part.text })
break
case "image":
contentParts.push({
type: "input_image",
detail: "auto",
image_url: `data:${part.source.media_type};base64,${part.source.data}`,
})
break
case "tool_result": {
// Flush any pending user content before the tool result
if (contentParts.length > 0) {
input.push({ role: "user", content: [...contentParts] })
contentParts.length = 0
}
// Convert tool result content
let output: string
if (typeof part.content === "string") {
output = part.content || "(empty)"
} else if (Array.isArray(part.content)) {
output =
part.content
.filter((c): c is Anthropic.TextBlockParam => c.type === "text")
.map((c) => c.text)
.join("\n") || "(empty)"
} else {
output = "(empty)"
}
input.push({
type: "function_call_output",
call_id: part.tool_use_id,
output,
})
break
}
}
}
// Flush remaining user content
if (contentParts.length > 0) {
input.push({ role: "user", content: contentParts })
}
}
}
return input
}

View file

@ -0,0 +1,140 @@
import type { ApiStream, ApiStreamUsageChunk } from "./stream"
/**
* Processes Responses API stream events and yields ApiStreamChunks.
*
* This is a shared utility for providers that use OpenAI's Responses API
* (POST /v1/responses with stream: true). It handles the core event types:
*
* - Text deltas (response.output_text.delta)
* - Reasoning deltas (response.reasoning_text.delta, response.reasoning_summary_text.delta)
* - Tool/function calls (response.output_item.done with function_call type)
* - Usage data (response.completed)
*
* Provider-specific concerns (WebSocket mode, SSE fallback, duplicate detection,
* pending tool tracking) are intentionally left to individual providers.
*
* @param stream - AsyncIterable of Responses API stream events
* @param normalizeUsage - Provider-specific function to normalize usage data into ApiStreamUsageChunk
*/
export async function* processResponsesApiStream(
stream: AsyncIterable<any>,
normalizeUsage: (usage: any) => ApiStreamUsageChunk | undefined,
): ApiStream {
for await (const event of stream) {
// Text content deltas
if (event?.type === "response.output_text.delta" || event?.type === "response.text.delta") {
if (event?.delta) {
yield { type: "text", text: event.delta }
}
continue
}
// Reasoning deltas
if (
event?.type === "response.reasoning_text.delta" ||
event?.type === "response.reasoning.delta" ||
event?.type === "response.reasoning_summary_text.delta" ||
event?.type === "response.reasoning_summary.delta"
) {
if (event?.delta) {
yield { type: "reasoning", text: event.delta }
}
continue
}
// Output item events — handle completed function calls and fallback text
if (event?.type === "response.output_item.done") {
const item = event?.item
if (item?.type === "function_call" || item?.type === "tool_call") {
const callId = item.call_id || item.tool_call_id || item.id
const name = item.name || item.function?.name
const argsRaw = item.arguments || item.function?.arguments || item.input
const args =
typeof argsRaw === "string"
? argsRaw
: argsRaw && typeof argsRaw === "object"
? JSON.stringify(argsRaw)
: ""
if (typeof callId === "string" && callId.length > 0 && typeof name === "string" && name.length > 0) {
yield {
type: "tool_call",
id: callId,
name,
arguments: args,
}
}
}
continue
}
// Function call argument deltas (for streaming tool calls)
if (
event?.type === "response.function_call_arguments.delta" ||
event?.type === "response.tool_call_arguments.delta"
) {
const callId = event.call_id || event.tool_call_id || event.id || event.item_id
const name = event.name || event.function_name
if (typeof callId === "string" && callId.length > 0) {
yield {
type: "tool_call_partial",
index: event.index ?? 0,
id: callId,
name,
arguments: typeof event.delta === "string" ? event.delta : "",
}
}
continue
}
// Completion events — extract usage
if (event?.type === "response.completed" || event?.type === "response.done") {
const usage = event?.response?.usage || event?.usage
const usageData = normalizeUsage(usage)
if (usageData) {
yield usageData
}
continue
}
}
}
/**
* Creates a standard usage normalizer for providers with per-token pricing.
* Extracts input/output tokens, cache tokens, reasoning tokens, and computes cost.
*
* @param calculateCost - Optional function to compute total cost from token counts
*/
export function createUsageNormalizer(
calculateCost?: (inputTokens: number, outputTokens: number, cacheReadTokens: number) => number,
): (usage: any) => ApiStreamUsageChunk | undefined {
return (usage: any): ApiStreamUsageChunk | undefined => {
if (!usage) return undefined
const inputDetails = usage.input_tokens_details ?? usage.prompt_tokens_details
const cachedTokens = inputDetails?.cached_tokens ?? 0
const inputTokens = usage.input_tokens ?? usage.prompt_tokens ?? 0
const outputTokens = usage.output_tokens ?? usage.completion_tokens ?? 0
const cacheReadTokens = usage.cache_read_input_tokens ?? cachedTokens ?? 0
const cacheWriteTokens = usage.cache_creation_input_tokens ?? usage.cache_write_tokens ?? 0
const reasoningTokens =
typeof usage.output_tokens_details?.reasoning_tokens === "number"
? usage.output_tokens_details.reasoning_tokens
: undefined
const totalCost = calculateCost ? calculateCost(inputTokens, outputTokens, cacheReadTokens) : undefined
return {
type: "usage",
inputTokens,
outputTokens,
cacheWriteTokens,
cacheReadTokens,
...(typeof reasoningTokens === "number" ? { reasoningTokens } : {}),
...(typeof totalCost === "number" ? { totalCost } : {}),
}
}
}

View file

@ -169,7 +169,7 @@ export class ClineProvider
public isViewLaunched = false
public settingsImportedAt?: number
public readonly latestAnnouncementId = "mar-2026-v3.51.0-gpt-54-slash-skills" // v3.51.0 OpenAI GPT-5.4 support and slash command skills
public readonly latestAnnouncementId = "apr-2026-v3.52.0-poe-xai-minimax" // v3.52.0 Poe provider, xAI improvements, and MiniMax fixes
public readonly providerSettingsManager: ProviderSettingsManager
public readonly customModesManager: CustomModesManager

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

@ -3,7 +3,7 @@
"displayName": "%extension.displayName%",
"description": "%extension.description%",
"publisher": "RooVeterinaryInc",
"version": "3.51.1",
"version": "3.52.0",
"icon": "assets/icons/icon.png",
"galleryBanner": {
"color": "#617A91",
@ -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

@ -12,7 +12,7 @@ vi.mock("@src/utils/vscode", () => ({
vi.mock("@roo/package", () => ({
Package: {
version: "3.51.0",
version: "3.52.0",
},
}))
@ -34,9 +34,9 @@ vi.mock("@src/i18n/TranslationContext", () => ({
const translations: Record<string, string> = {
"chat:announcement.release.heading": "What's New:",
"chat:announcement.release.gpt54":
"OpenAI GPT-5.4 Support: Added OpenAI GPT-5.4 and GPT-5.3 Chat Latest so you can use the newest OpenAI chat models in Roo Code.",
"Poe Provider: Added Poe as an AI provider so you can access Poe models directly in Roo Code.",
"chat:announcement.release.slashSkills":
"Slash Command Skills: Skills can now be exposed as slash commands with fallback execution for faster workflows.",
"xAI and MiniMax Improvements: Migrated the xAI provider to the Responses API, added Grok-4.20 defaults, and fixed MiniMax model listings and context window handling for a more reliable setup.",
}
if (key === "chat:announcement.title") {
@ -49,18 +49,18 @@ vi.mock("@src/i18n/TranslationContext", () => ({
}))
describe("Announcement", () => {
it("renders the v3.51.0 announcement title and highlights", () => {
it("renders the v3.52.0 announcement title and highlights", () => {
render(<Announcement hideAnnouncement={vi.fn()} />)
expect(screen.getByText("Roo Code 3.51.0 Released")).toBeInTheDocument()
expect(screen.getByText("Roo Code 3.52.0 Released")).toBeInTheDocument()
expect(
screen.getByText(
"OpenAI GPT-5.4 Support: Added OpenAI GPT-5.4 and GPT-5.3 Chat Latest so you can use the newest OpenAI chat models in Roo Code.",
"Poe Provider: Added Poe as an AI provider so you can access Poe models directly in Roo Code.",
),
).toBeInTheDocument()
expect(
screen.getByText(
"Slash Command Skills: Skills can now be exposed as slash commands with fallback execution for faster workflows.",
"xAI and MiniMax Improvements: Migrated the xAI provider to the Responses API, added Grok-4.20 defaults, and fixed MiniMax model listings and context window handling for a more reliable setup.",
),
).toBeInTheDocument()
})

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

@ -11,6 +11,8 @@ import {
BEDROCK_1M_CONTEXT_MODEL_IDS,
litellmDefaultModelInfo,
openAiModelInfoSaneDefaults,
minimaxDefaultModelId,
minimaxModels,
} from "@roo-code/types"
import { useSelectedModel } from "../useSelectedModel"
@ -725,4 +727,51 @@ describe("useSelectedModel", () => {
expect(result.current.info).toEqual(customModelInfo)
})
})
describe("minimax provider", () => {
beforeEach(() => {
mockUseRouterModels.mockReturnValue({
data: {
openrouter: {},
requesty: {},
litellm: {},
},
isLoading: false,
isError: false,
} as any)
mockUseOpenRouterModelProviders.mockReturnValue({
data: {},
isLoading: false,
isError: false,
} as any)
})
it("should return default minimax model when no custom model is specified", () => {
const apiConfiguration: ProviderSettings = {
apiProvider: "minimax",
}
const wrapper = createWrapper()
const { result } = renderHook(() => useSelectedModel(apiConfiguration), { wrapper })
expect(result.current.provider).toBe("minimax")
expect(result.current.id).toBe(minimaxDefaultModelId)
expect(result.current.info).toEqual(minimaxModels[minimaxDefaultModelId])
})
it("should use custom model ID and info when model exists in minimaxModels", () => {
const apiConfiguration: ProviderSettings = {
apiProvider: "minimax",
apiModelId: "MiniMax-M2.7",
}
const wrapper = createWrapper()
const { result } = renderHook(() => useSelectedModel(apiConfiguration), { wrapper })
expect(result.current.provider).toBe("minimax")
expect(result.current.id).toBe("MiniMax-M2.7")
expect(result.current.info).toEqual(minimaxModels["MiniMax-M2.7"])
})
})
})

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

@ -348,8 +348,8 @@
},
"release": {
"heading": "Què hi ha de nou:",
"gpt54": "Compatibilitat amb OpenAI GPT-5.4: Hem afegit OpenAI GPT-5.4 i GPT-5.3 Chat Latest perquè puguis fer servir els models de xat més nous dOpenAI a Roo Code.",
"slashSkills": "Skills com a ordres slash: Ara les skills es poden exposar com a ordres slash amb execució de reserva per accelerar els fluxos de treball.",
"gpt54": "Proveïdor Poe: S'ha afegit Poe com a proveïdor dIA perquè puguis accedir directament als models de Poe a Roo Code.",
"slashSkills": "Millores a xAI i MiniMax: S'ha migrat el proveïdor xAI a la Responses API, s'hi han afegit utilitats de transformació reutilitzables, s'han actualitzat els valors per defecte de Grok-4.20 i s'han corregit els llistats de models de MiniMax i la gestió de la finestra de context per oferir una configuració més fiable.",
"geminiPro": "Suport per Gemini 3.1 Pro: S'ha afegit el suport per al model Gemini 3.1 Pro i s'ha establert com a model Gemini per defecte per a un millor rendiment.",
"cliNdjson": "Protocol NDJSON del CLI: Nou protocol NDJSON via stdin, subcomandes de llistat i comanda d'execució modularitzada per a fluxos de treball CLI més flexibles.",
"cliRelease": "CLI v0.1.0: El CLI de Roo Code arriba a la seva primera versió oficial amb una interfície de comandes estable."

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

@ -348,8 +348,8 @@
},
"release": {
"heading": "Was ist neu:",
"gpt54": "OpenAI GPT-5.4-Unterstützung: OpenAI GPT-5.4 und GPT-5.3 Chat Latest wurden hinzugefügt, damit du die neuesten OpenAI-Chatmodelle in Roo Code nutzen kannst.",
"slashSkills": "Slash-Command-Skills: Skills können jetzt als Slash-Commands mit Fallback-Ausführung verfügbar gemacht werden, damit Workflows schneller ablaufen.",
"gpt54": "Poe-Anbieter: Poe als KI-Anbieter hinzugefügt, damit du direkt in Roo Code auf Poe-Modelle zugreifen kannst.",
"slashSkills": "xAI- und MiniMax-Verbesserungen: Den xAI-Anbieter auf die Responses API migriert, wiederverwendbare Transformations-Utilities hinzugefügt, Grok-4.20-Standards aktualisiert und MiniMax-Modelllisten sowie die Handhabung des Kontextfensters für eine zuverlässigere Einrichtung korrigiert.",
"geminiPro": "Gemini 3.1 Pro Unterstützung: Gemini 3.1 Pro wurde als Modell hinzugefügt und als Standard-Gemini-Modell für bessere Leistung festgelegt.",
"cliNdjson": "CLI NDJSON-Protokoll: Neues NDJSON-stdin-Protokoll, List-Unterbefehle und modularisierter Run-Befehl für flexiblere CLI-Workflows.",
"cliRelease": "CLI v0.1.0: Das Roo Code CLI erreicht seine erste offizielle Veröffentlichung mit einer stabilen Befehlsoberfläche."

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

@ -367,8 +367,8 @@
},
"release": {
"heading": "What's New:",
"gpt54": "OpenAI GPT-5.4 Support: Added OpenAI GPT-5.4 and GPT-5.3 Chat Latest so you can use the newest OpenAI chat models in Roo Code.",
"slashSkills": "Slash Command Skills: Skills can now be exposed as slash commands with fallback execution for faster workflows."
"gpt54": "Poe Provider: Added Poe as an AI provider so you can access Poe models directly in Roo Code.",
"slashSkills": "xAI and MiniMax Improvements: Migrated the xAI provider to the Responses API, added Grok-4.20 defaults, and fixed MiniMax model listings and context window handling for a more reliable setup."
},
"cloudAgents": {
"heading": "New in the Cloud:",

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

@ -348,8 +348,8 @@
},
"release": {
"heading": "Qué hay de nuevo:",
"gpt54": "Compatibilidad con OpenAI GPT-5.4: Se añadieron OpenAI GPT-5.4 y GPT-5.3 Chat Latest para que puedas usar los modelos de chat más recientes de OpenAI en Roo Code.",
"slashSkills": "Skills como comandos slash: Ahora las skills pueden exponerse como comandos slash con ejecución de respaldo para activar flujos de trabajo más rápido.",
"gpt54": "Proveedor Poe: Se añadió Poe como proveedor de IA para que puedas acceder directamente a los modelos de Poe en Roo Code.",
"slashSkills": "Mejoras en xAI y MiniMax: Se migró el proveedor xAI a la Responses API, se añadieron utilidades de transformación reutilizables, se actualizaron los valores predeterminados de Grok-4.20 y se corrigieron los listados de modelos de MiniMax y el manejo de la ventana de contexto para una configuración más fiable.",
"geminiPro": "Soporte para Gemini 3.1 Pro: Se añadió soporte para el modelo Gemini 3.1 Pro y se estableció como modelo Gemini predeterminado para un mejor rendimiento.",
"cliNdjson": "Protocolo NDJSON del CLI: Nuevo protocolo NDJSON por stdin, subcomandos de listado y comando de ejecución modularizado para flujos de trabajo CLI más flexibles.",
"cliRelease": "CLI v0.1.0: El CLI de Roo Code alcanza su primera versión oficial con una interfaz de comandos estable."

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

@ -348,8 +348,8 @@
},
"release": {
"heading": "Quoi de neuf :",
"gpt54": "Prise en charge dOpenAI GPT-5.4 : OpenAI GPT-5.4 et GPT-5.3 Chat Latest ont été ajoutés pour que tu puisses utiliser les modèles de chat OpenAI les plus récents dans Roo Code.",
"slashSkills": "Skills en slash commands : Les skills peuvent maintenant être exposées comme slash commands avec une exécution de secours pour accélérer les workflows.",
"gpt54": "Fournisseur Poe : Poe a été ajouté comme fournisseur dIA pour que tu puisses accéder directement aux modèles Poe dans Roo Code.",
"slashSkills": "Améliorations xAI et MiniMax : Le fournisseur xAI a été migré vers la Responses API, des utilitaires de transformation réutilisables ont été ajoutés, les valeurs par défaut de Grok-4.20 ont été mises à jour et les listes de modèles MiniMax ainsi que la gestion de la fenêtre de contexte ont été corrigées pour une configuration plus fiable.",
"geminiPro": "Support de Gemini 3.1 Pro : Le modèle Gemini 3.1 Pro a été ajouté et défini comme modèle Gemini par défaut pour de meilleures performances.",
"cliNdjson": "Protocole NDJSON du CLI : Nouveau protocole NDJSON via stdin, sous-commandes de liste et commande d'exécution modularisée pour des workflows CLI plus flexibles.",
"cliRelease": "CLI v0.1.0 : Le CLI de Roo Code atteint sa première version officielle avec une interface de commandes stable."

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

@ -348,8 +348,8 @@
},
"release": {
"heading": "नया क्या है:",
"gpt54": "OpenAI GPT-5.4 सपोर्ट: OpenAI GPT-5.4 और GPT-5.3 Chat Latest जोड़े गए हैं, ताकि आप Roo Code में OpenAI के सबसे नए chat models इस्तेमाल कर सकें।",
"slashSkills": "Slash command skills: अब skills को slash commands के रूप में fallback execution के साथ expose किया जा सकता है, जिससे workflows तेज़ हो जाते हैं।",
"gpt54": "Poe provider: Poe को AI provider के रूप में जोड़ा गया है, ताकि आप Roo Code में सीधे Poe models तक पहुँच सकें।",
"slashSkills": "xAI और MiniMax में सुधार: xAI provider को Responses API पर migrate किया गया है, reusable transform utilities जोड़ी गई हैं, Grok-4.20 defaults अपडेट किए गए हैं, और ज़्यादा भरोसेमंद setup के लिए MiniMax model listings व context window handling को ठीक किया गया है।",
"geminiPro": "Gemini 3.1 Pro सपोर्ट: Gemini 3.1 Pro मॉडल का सपोर्ट जोड़ा गया और बेहतर प्रदर्शन के लिए इसे डिफ़ॉल्ट Gemini मॉडल के रूप में सेट किया गया।",
"cliNdjson": "CLI NDJSON प्रोटोकॉल: नया NDJSON stdin प्रोटोकॉल, list सबकमांड, और अधिक लचीले CLI वर्कफ़्लो के लिए मॉड्यूलर run कमांड।",
"cliRelease": "CLI v0.1.0: Roo Code CLI एक स्थिर कमांड इंटरफ़ेस के साथ अपनी पहली आधिकारिक रिलीज़ तक पहुँचा।"

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

@ -377,8 +377,8 @@
},
"release": {
"heading": "Yang Baru:",
"gpt54": "Dukungan OpenAI GPT-5.4: OpenAI GPT-5.4 dan GPT-5.3 Chat Latest kini ditambahkan agar kamu bisa memakai model chat OpenAI terbaru di Roo Code.",
"slashSkills": "Skill sebagai slash command: Skill sekarang bisa diekspos sebagai slash command dengan eksekusi fallback supaya workflow bisa berjalan lebih cepat.",
"gpt54": "Provider Poe: Poe ditambahkan sebagai provider AI agar kamu bisa langsung mengakses model Poe di Roo Code.",
"slashSkills": "Peningkatan xAI dan MiniMax: Provider xAI dimigrasikan ke Responses API, utilitas transformasi yang bisa dipakai ulang ditambahkan, default Grok-4.20 diperbarui, dan daftar model MiniMax serta penanganan context window diperbaiki agar setup lebih andal.",
"geminiPro": "Dukungan Gemini 3.1 Pro: Dukungan model Gemini 3.1 Pro telah ditambahkan dan ditetapkan sebagai model Gemini default untuk performa yang lebih baik.",
"cliNdjson": "Protokol NDJSON CLI: Protokol NDJSON stdin baru, subperintah list, dan perintah run yang dimodularisasi untuk alur kerja CLI yang lebih fleksibel.",
"cliRelease": "CLI v0.1.0: CLI Roo Code mencapai rilis resmi pertamanya dengan antarmuka perintah yang stabil."

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

@ -348,8 +348,8 @@
},
"release": {
"heading": "Novità:",
"gpt54": "Supporto per OpenAI GPT-5.4: abbiamo aggiunto OpenAI GPT-5.4 e GPT-5.3 Chat Latest così puoi usare i modelli di chat OpenAI più recenti in Roo Code.",
"slashSkills": "Skill come slash command: ora le skill possono essere esposte come slash command con esecuzione di fallback per attivare i workflow più rapidamente.",
"gpt54": "Provider Poe: abbiamo aggiunto Poe come provider AI così puoi accedere direttamente ai modelli Poe in Roo Code.",
"slashSkills": "Miglioramenti per xAI e MiniMax: il provider xAI è stato migrato alla Responses API, sono state aggiunte utility di trasformazione riutilizzabili, sono stati aggiornati i valori predefiniti di Grok-4.20 e sono stati corretti gli elenchi dei modelli MiniMax e la gestione della finestra di contesto per una configurazione più affidabile.",
"geminiPro": "Supporto Gemini 3.1 Pro: Aggiunto il supporto per il modello Gemini 3.1 Pro e impostato come modello Gemini predefinito per prestazioni migliori.",
"cliNdjson": "Protocollo NDJSON del CLI: Nuovo protocollo NDJSON via stdin, sottocomandi di elenco e comando di esecuzione modularizzato per flussi di lavoro CLI più flessibili.",
"cliRelease": "CLI v0.1.0: Il CLI di Roo Code raggiunge la sua prima release ufficiale con un'interfaccia di comandi stabile."

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

@ -348,8 +348,8 @@
},
"release": {
"heading": "新機能:",
"gpt54": "OpenAI GPT-5.4 サポート: OpenAI GPT-5.4 と GPT-5.3 Chat Latest を追加し、Roo Code で OpenAI の最新チャットモデルを使えるようにしました。",
"slashSkills": "スラッシュコマンド対応スキル: スキルをスラッシュコマンドとして公開し、フォールバック実行にも対応したことで、ワークフローをより素早く実行できます。",
"gpt54": "Poe プロバイダー: Poe を AI プロバイダーとして追加し、Roo Code から Poe のモデルに直接アクセスできるようにしました。",
"slashSkills": "xAI と MiniMax の改善: xAI プロバイダーを Responses API へ移行し、再利用可能な変換ユーティリティを追加し、Grok-4.20 のデフォルト設定を更新し、MiniMax のモデル一覧とコンテキストウィンドウの扱いを修正して、より信頼性の高いセットアップにしました。",
"geminiPro": "Gemini 3.1 Pro サポート: Gemini 3.1 Pro モデルのサポートを追加し、パフォーマンス向上のためデフォルトの Gemini モデルに設定しました。",
"cliNdjson": "CLI NDJSON プロトコル: 新しい NDJSON stdin プロトコル、list サブコマンド、より柔軟な CLI ワークフローのためのモジュール化された run コマンド。",
"cliRelease": "CLI v0.1.0: Roo Code CLI が安定したコマンドインターフェースで初の正式リリースに到達しました。"

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

@ -348,8 +348,8 @@
},
"release": {
"heading": "새로운 기능:",
"gpt54": "OpenAI GPT-5.4 지원: OpenAI GPT-5.4와 GPT-5.3 Chat Latest를 추가해 Roo Code에서 최신 OpenAI 채팅 모델을 사용할 수 있게 했어요.",
"slashSkills": "슬래시 명령어 스킬: 이제 스킬을 슬래시 명령어로 노출하고 fallback 실행도 지원해 워크플로를 더 빠르게 실행할 수 있어요.",
"gpt54": "Poe 제공업체: Poe를 AI 제공업체로 추가해 Roo Code에서 Poe 모델에 직접 접근할 수 있게 했어요.",
"slashSkills": "xAI 및 MiniMax 개선: xAI 제공업체를 Responses API로 마이그레이션하고, 재사용 가능한 변환 유틸리티를 추가하고, Grok-4.20 기본값을 업데이트하고, MiniMax 모델 목록과 컨텍스트 윈도우 처리를 수정해 더 안정적인 설정을 제공해요.",
"geminiPro": "Gemini 3.1 Pro 지원: Gemini 3.1 Pro 모델 지원이 추가되었으며, 향상된 성능을 위해 기본 Gemini 모델로 설정되었습니다.",
"cliNdjson": "CLI NDJSON 프로토콜: 새로운 NDJSON stdin 프로토콜, list 하위 명령어, 더 유연한 CLI 워크플로를 위한 모듈화된 run 명령어.",
"cliRelease": "CLI v0.1.0: Roo Code CLI가 안정적인 명령어 인터페이스로 첫 공식 릴리스에 도달했습니다."

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

@ -321,8 +321,8 @@
},
"release": {
"heading": "Wat is er nieuw:",
"gpt54": "Ondersteuning voor OpenAI GPT-5.4: OpenAI GPT-5.4 en GPT-5.3 Chat Latest zijn toegevoegd, zodat je de nieuwste OpenAI-chatmodellen in Roo Code kunt gebruiken.",
"slashSkills": "Skills als slash-commands: Skills kunnen nu als slash-commands worden aangeboden met fallback-uitvoering voor snellere workflows.",
"gpt54": "Poe-provider: Poe toegevoegd als AI-provider, zodat je in Roo Code direct toegang hebt tot Poe-modellen.",
"slashSkills": "xAI- en MiniMax-verbeteringen: De xAI-provider gemigreerd naar de Responses API, herbruikbare transformatiehulpmiddelen toegevoegd, Grok-4.20-standaardinstellingen bijgewerkt en MiniMax-modellijsten plus de afhandeling van het contextvenster gerepareerd voor een betrouwbaardere configuratie.",
"geminiPro": "Gemini 3.1 Pro ondersteuning: Ondersteuning voor het Gemini 3.1 Pro model is toegevoegd en ingesteld als standaard Gemini-model voor betere prestaties.",
"cliNdjson": "CLI NDJSON-protocol: Nieuw NDJSON stdin-protocol, list-subopdrachten en gemodulariseerd run-commando voor flexibelere CLI-workflows.",
"cliRelease": "CLI v0.1.0: De Roo Code CLI bereikt zijn eerste officiële release met een stabiele opdrachtinterface."

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

@ -348,8 +348,8 @@
},
"release": {
"heading": "Co nowego:",
"gpt54": "Obsługa OpenAI GPT-5.4: Dodaliśmy OpenAI GPT-5.4 i GPT-5.3 Chat Latest, żebyś mógł używać najnowszych modeli czatu OpenAI w Roo Code.",
"slashSkills": "Umiejętności jako slash commandy: Skills można teraz udostępniać jako slash commandy z wykonaniem awaryjnym, żeby szybciej uruchamiać workflowy.",
"gpt54": "Dostawca Poe: Dodano Poe jako dostawcę AI, żebyś mógł bezpośrednio uzyskać dostęp do modeli Poe w Roo Code.",
"slashSkills": "Usprawnienia xAI i MiniMax: Przeniesiono dostawcę xAI do Responses API, dodano narzędzia transformacji wielokrotnego użytku, zaktualizowano domyślne ustawienia Grok-4.20 oraz naprawiono listy modeli MiniMax i obsługę okna kontekstu, żeby konfiguracja była bardziej niezawodna.",
"geminiPro": "Wsparcie dla Gemini 3.1 Pro: Dodano wsparcie dla modelu Gemini 3.1 Pro i ustawiono go jako domyślny model Gemini dla lepszej wydajności.",
"cliNdjson": "Protokół NDJSON w CLI: Nowy protokół NDJSON przez stdin, podkomendy list oraz zmodularyzowana komenda run dla bardziej elastycznych przepływów pracy CLI.",
"cliRelease": "CLI v0.1.0: CLI Roo Code osiąga swoją pierwszą oficjalną wersję ze stabilnym interfejsem poleceń."

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

Some files were not shown because too many files have changed in this diff Show more