diff --git a/.changeset/add-openai-gpt-5-4-mini-nano.md b/.changeset/add-openai-gpt-5-4-mini-nano.md deleted file mode 100644 index 4c47f6c202..0000000000 --- a/.changeset/add-openai-gpt-5-4-mini-nano.md +++ /dev/null @@ -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. diff --git a/CHANGELOG.md b/CHANGELOG.md index f37ddcc424..c251c88d03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index e972366df7..7a21f92e8a 100644 --- a/README.md +++ b/README.md @@ -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.
🌐 Available languages @@ -40,7 +41,7 @@ - [简体中文](locales/zh-CN/README.md) - [繁體中文](locales/zh-TW/README.md) - ... -
+ --- diff --git a/apps/web-evals/CHANGELOG.md b/apps/web-evals/CHANGELOG.md new file mode 100644 index 0000000000..b3531905ac --- /dev/null +++ b/apps/web-evals/CHANGELOG.md @@ -0,0 +1,3 @@ +# @roo-code/web-evals + +## 0.0.1 diff --git a/apps/web-evals/package.json b/apps/web-evals/package.json index 83d69edd59..1723f57583 100644 --- a/apps/web-evals/package.json +++ b/apps/web-evals/package.json @@ -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", diff --git a/apps/web-roo-code/CHANGELOG.md b/apps/web-roo-code/CHANGELOG.md new file mode 100644 index 0000000000..0f77020fa1 --- /dev/null +++ b/apps/web-roo-code/CHANGELOG.md @@ -0,0 +1,3 @@ +# @roo-code/web-roo-code + +## 0.0.1 diff --git a/apps/web-roo-code/package.json b/apps/web-roo-code/package.json index 8ad1a95051..4cb37dc611 100644 --- a/apps/web-roo-code/package.json +++ b/apps/web-roo-code/package.json @@ -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", diff --git a/locales/ca/README.md b/locales/ca/README.md index cf60961e98..5303b2db05 100644 --- a/locales/ca/README.md +++ b/locales/ca/README.md @@ -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.
🌐 Idiomes disponibles @@ -40,7 +41,7 @@ - [简体中文](../zh-CN/README.md) - [繁體中文](../zh-TW/README.md) - ... -
+ --- diff --git a/locales/de/README.md b/locales/de/README.md index b9342fb121..7a7beb7638 100644 --- a/locales/de/README.md +++ b/locales/de/README.md @@ -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.
🌐 Verfügbare Sprachen @@ -40,7 +41,7 @@ - [简体中文](../zh-CN/README.md) - [繁體中文](../zh-TW/README.md) - ... -
+ --- diff --git a/locales/es/README.md b/locales/es/README.md index 30cf9d5b30..2388af2a30 100644 --- a/locales/es/README.md +++ b/locales/es/README.md @@ -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.
🌐 Idiomas disponibles @@ -40,7 +41,7 @@ - [简体中文](../zh-CN/README.md) - [繁體中文](../zh-TW/README.md) - ... -
+ --- diff --git a/locales/fr/README.md b/locales/fr/README.md index c56d5423a0..bbd40d899c 100644 --- a/locales/fr/README.md +++ b/locales/fr/README.md @@ -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 d’OpenAI 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 d’IA 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.
🌐 Langues disponibles @@ -40,7 +41,7 @@ - [简体中文](../zh-CN/README.md) - [繁體中文](../zh-TW/README.md) - ... -
+ --- diff --git a/locales/hi/README.md b/locales/hi/README.md index 11c96f7c5d..e1d1dd211c 100644 --- a/locales/hi/README.md +++ b/locales/hi/README.md @@ -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 को ठीक किया गया है।
🌐 उपलब्ध भाषाएँ @@ -40,7 +41,7 @@ - [简体中文](../zh-CN/README.md) - [繁體中文](../zh-TW/README.md) - ... -
+ --- diff --git a/locales/id/README.md b/locales/id/README.md index 782fb27e0d..e779455325 100644 --- a/locales/id/README.md +++ b/locales/id/README.md @@ -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.
🌐 Bahasa yang tersedia @@ -40,7 +41,7 @@ - [简体中文](../zh-CN/README.md) - [繁體中文](../zh-TW/README.md) - ... -
+ --- diff --git a/locales/it/README.md b/locales/it/README.md index 213f373481..63152835c4 100644 --- a/locales/it/README.md +++ b/locales/it/README.md @@ -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.
🌐 Lingue disponibili @@ -40,7 +41,7 @@ - [简体中文](../zh-CN/README.md) - [繁體中文](../zh-TW/README.md) - ... -
+ --- diff --git a/locales/ja/README.md b/locales/ja/README.md index 9a992038c2..e487fb5ca4 100644 --- a/locales/ja/README.md +++ b/locales/ja/README.md @@ -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 のモデル一覧とコンテキストウィンドウの扱いを修正しました。
🌐 利用可能な言語 @@ -40,7 +41,7 @@ - [简体中文](../zh-CN/README.md) - [繁體中文](../zh-TW/README.md) - ... -
+ --- diff --git a/locales/ko/README.md b/locales/ko/README.md index 174f43bc5d..7e6e8a7696 100644 --- a/locales/ko/README.md +++ b/locales/ko/README.md @@ -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 모델 목록과 컨텍스트 윈도우 처리를 수정했어요.
🌐 사용 가능한 언어 @@ -40,7 +41,7 @@ - [简体中文](../zh-CN/README.md) - [繁體中文](../zh-TW/README.md) - ... -
+ --- diff --git a/locales/nl/README.md b/locales/nl/README.md index 9ef7e2cde2..ea1a13aa3c 100644 --- a/locales/nl/README.md +++ b/locales/nl/README.md @@ -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.
🌐 Beschikbare talen @@ -40,7 +41,7 @@ - [简体中文](../zh-CN/README.md) - [繁體中文](../zh-TW/README.md) - ... -
+ --- diff --git a/locales/pl/README.md b/locales/pl/README.md index cd9d3f28dc..b14fc7567c 100644 --- a/locales/pl/README.md +++ b/locales/pl/README.md @@ -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.
🌐 Dostępne języki @@ -40,7 +41,7 @@ - [简体中文](../zh-CN/README.md) - [繁體中文](../zh-TW/README.md) - ... -
+ --- diff --git a/locales/pt-BR/README.md b/locales/pt-BR/README.md index cdc868addd..e1da800128 100644 --- a/locales/pt-BR/README.md +++ b/locales/pt-BR/README.md @@ -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.
🌐 Idiomas disponíveis @@ -40,7 +41,7 @@ - [简体中文](../zh-CN/README.md) - [繁體中文](../zh-TW/README.md) - ... -
+ --- diff --git a/locales/ru/README.md b/locales/ru/README.md index 9d0e73a2c2..8e6f555b85 100644 --- a/locales/ru/README.md +++ b/locales/ru/README.md @@ -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 и обработка окна контекста для более надёжной настройки.
🌐 Доступные языки @@ -40,7 +41,7 @@ - [简体中文](../zh-CN/README.md) - [繁體中文](../zh-TW/README.md) - ... -
+ --- diff --git a/locales/tr/README.md b/locales/tr/README.md index 8b130dfde0..9bee46f9b7 100644 --- a/locales/tr/README.md +++ b/locales/tr/README.md @@ -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.
🌐 Mevcut diller @@ -40,7 +41,7 @@ - [简体中文](../zh-CN/README.md) - [繁體中文](../zh-TW/README.md) - ... -
+ --- diff --git a/locales/vi/README.md b/locales/vi/README.md index 9e567456fa..00b0d2e09b 100644 --- a/locales/vi/README.md +++ b/locales/vi/README.md @@ -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.
🌐 Các ngôn ngữ có sẵn @@ -40,7 +41,7 @@ - [简体中文](../zh-CN/README.md) - [繁體中文](../zh-TW/README.md) - ... -
+ --- diff --git a/locales/zh-CN/README.md b/locales/zh-CN/README.md index 73310b47b7..a2f4055845 100644 --- a/locales/zh-CN/README.md +++ b/locales/zh-CN/README.md @@ -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 模型列表和上下文窗口处理方式,让配置更加可靠。
🌐 可用语言 @@ -40,7 +41,7 @@ - [简体中文](../zh-CN/README.md) - [繁體中文](../zh-TW/README.md) - ... -
+ --- diff --git a/locales/zh-TW/README.md b/locales/zh-TW/README.md index 3298eb0666..d66d469819 100644 --- a/locales/zh-TW/README.md +++ b/locales/zh-TW/README.md @@ -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 模型清單和上下文視窗處理方式,讓設定更加可靠。
🌐 支援語言 @@ -40,7 +41,7 @@ - [简体中文](../zh-CN/README.md) - [繁體中文](../zh-TW/README.md) - ... -
+ --- diff --git a/packages/cloud/CHANGELOG.md b/packages/cloud/CHANGELOG.md new file mode 100644 index 0000000000..059ec66fd7 --- /dev/null +++ b/packages/cloud/CHANGELOG.md @@ -0,0 +1,3 @@ +# @roo-code/cloud + +## 0.0.1 diff --git a/packages/cloud/package.json b/packages/cloud/package.json index 92ecaa1764..7b9a03d6f8 100644 --- a/packages/cloud/package.json +++ b/packages/cloud/package.json @@ -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": { diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md new file mode 100644 index 0000000000..6b8401868e --- /dev/null +++ b/packages/core/CHANGELOG.md @@ -0,0 +1,3 @@ +# @roo-code/core + +## 0.0.1 diff --git a/packages/core/package.json b/packages/core/package.json index 25e6224e8c..00ae5752c6 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -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", diff --git a/packages/evals/CHANGELOG.md b/packages/evals/CHANGELOG.md new file mode 100644 index 0000000000..1757e8a938 --- /dev/null +++ b/packages/evals/CHANGELOG.md @@ -0,0 +1,3 @@ +# @roo-code/evals + +## 0.0.1 diff --git a/packages/evals/package.json b/packages/evals/package.json index 611ee6e79c..b9c48237f2 100644 --- a/packages/evals/package.json +++ b/packages/evals/package.json @@ -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": { diff --git a/packages/ipc/CHANGELOG.md b/packages/ipc/CHANGELOG.md new file mode 100644 index 0000000000..1ee764e1b5 --- /dev/null +++ b/packages/ipc/CHANGELOG.md @@ -0,0 +1,3 @@ +# @roo-code/ipc + +## 0.0.1 diff --git a/packages/ipc/package.json b/packages/ipc/package.json index 03cb3beeca..d371faa315 100644 --- a/packages/ipc/package.json +++ b/packages/ipc/package.json @@ -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": { diff --git a/packages/telemetry/CHANGELOG.md b/packages/telemetry/CHANGELOG.md new file mode 100644 index 0000000000..251acfd822 --- /dev/null +++ b/packages/telemetry/CHANGELOG.md @@ -0,0 +1,3 @@ +# @roo-code/telemetry + +## 0.0.1 diff --git a/packages/telemetry/package.json b/packages/telemetry/package.json index 1d434ad290..dd31289e16 100644 --- a/packages/telemetry/package.json +++ b/packages/telemetry/package.json @@ -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": { diff --git a/packages/types/CHANGELOG.md b/packages/types/CHANGELOG.md new file mode 100644 index 0000000000..855f8ebf32 --- /dev/null +++ b/packages/types/CHANGELOG.md @@ -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. diff --git a/packages/types/package.json b/packages/types/package.json index d66d87ac72..cdb734cca0 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -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" } } diff --git a/packages/types/scripts/generate-roomodes-schema.ts b/packages/types/scripts/generate-roomodes-schema.ts new file mode 100644 index 0000000000..a7d6c0b978 --- /dev/null +++ b/packages/types/scripts/generate-roomodes-schema.ts @@ -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)}`) diff --git a/packages/types/src/__tests__/roomodes-schema-sync.spec.ts b/packages/types/src/__tests__/roomodes-schema-sync.spec.ts new file mode 100644 index 0000000000..fb053c2fc2 --- /dev/null +++ b/packages/types/src/__tests__/roomodes-schema-sync.spec.ts @@ -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) + }) +}) diff --git a/packages/types/src/__tests__/roomodes-schema.spec.ts b/packages/types/src/__tests__/roomodes-schema.spec.ts new file mode 100644 index 0000000000..f3530a225d --- /dev/null +++ b/packages/types/src/__tests__/roomodes-schema.spec.ts @@ -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 + 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) + }) +}) diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts index 859792d7c3..43135577e1 100644 --- a/packages/types/src/provider-settings.ts +++ b/packages/types/src/provider-settings.ts @@ -34,7 +34,15 @@ export const DEFAULT_CONSECUTIVE_MISTAKE_LIMIT = 3 * Dynamic provider requires external API calls in order to get the model list. */ -export const dynamicProviders = ["openrouter", "vercel-ai-gateway", "litellm", "requesty", "roo", "unbound"] as const +export const dynamicProviders = [ + "openrouter", + "vercel-ai-gateway", + "litellm", + "requesty", + "roo", + "unbound", + "poe", +] as const export type DynamicProvider = (typeof dynamicProviders)[number] @@ -306,6 +314,11 @@ const deepSeekSchema = apiModelIdProviderModelSchema.extend({ deepSeekApiKey: z.string().optional(), }) +const poeSchema = apiModelIdProviderModelSchema.extend({ + poeApiKey: z.string().optional(), + poeBaseUrl: z.string().optional(), +}) + const moonshotSchema = apiModelIdProviderModelSchema.extend({ moonshotBaseUrl: z .union([z.literal("https://api.moonshot.ai/v1"), z.literal("https://api.moonshot.cn/v1")]) @@ -400,6 +413,7 @@ export const providerSettingsSchemaDiscriminated = z.discriminatedUnion("apiProv openAiNativeSchema.merge(z.object({ apiProvider: z.literal("openai-native") })), mistralSchema.merge(z.object({ apiProvider: z.literal("mistral") })), deepSeekSchema.merge(z.object({ apiProvider: z.literal("deepseek") })), + poeSchema.merge(z.object({ apiProvider: z.literal("poe") })), moonshotSchema.merge(z.object({ apiProvider: z.literal("moonshot") })), minimaxSchema.merge(z.object({ apiProvider: z.literal("minimax") })), requestySchema.merge(z.object({ apiProvider: z.literal("requesty") })), @@ -433,6 +447,7 @@ export const providerSettingsSchema = z.object({ ...openAiNativeSchema.shape, ...mistralSchema.shape, ...deepSeekSchema.shape, + ...poeSchema.shape, ...moonshotSchema.shape, ...minimaxSchema.shape, ...requestySchema.shape, @@ -510,6 +525,7 @@ export const modelIdKeysByProvider: Record = { moonshot: "apiModelId", minimax: "apiModelId", deepseek: "apiModelId", + poe: "apiModelId", "qwen-code": "apiModelId", requesty: "requestyModelId", unbound: "unboundModelId", @@ -632,6 +648,7 @@ export const MODELS_BY_PROVIDER: Record< baseten: { id: "baseten", label: "Baseten", models: Object.keys(basetenModels) }, // Dynamic providers; models pulled from remote APIs. + poe: { id: "poe", label: "Poe", models: [] }, litellm: { id: "litellm", label: "LiteLLM", models: [] }, openrouter: { id: "openrouter", label: "OpenRouter", models: [] }, requesty: { id: "requesty", label: "Requesty", models: [] }, diff --git a/packages/types/src/providers/index.ts b/packages/types/src/providers/index.ts index 6bb959c705..6c180d5dda 100644 --- a/packages/types/src/providers/index.ts +++ b/packages/types/src/providers/index.ts @@ -13,6 +13,7 @@ export * from "./openai.js" export * from "./openai-codex.js" export * from "./openai-codex-rate-limits.js" export * from "./openrouter.js" +export * from "./poe.js" export * from "./qwen-code.js" export * from "./requesty.js" export * from "./roo.js" @@ -36,6 +37,7 @@ import { mistralDefaultModelId } from "./mistral.js" import { moonshotDefaultModelId } from "./moonshot.js" import { openAiCodexDefaultModelId } from "./openai-codex.js" import { openRouterDefaultModelId } from "./openrouter.js" +import { poeDefaultModelId } from "./poe.js" import { qwenCodeDefaultModelId } from "./qwen-code.js" import { requestyDefaultModelId } from "./requesty.js" import { rooDefaultModelId } from "./roo.js" @@ -107,6 +109,8 @@ export function getProviderDefaultModelId( return rooDefaultModelId case "qwen-code": return qwenCodeDefaultModelId + case "poe": + return poeDefaultModelId case "unbound": return unboundDefaultModelId case "vercel-ai-gateway": diff --git a/packages/types/src/providers/minimax.ts b/packages/types/src/providers/minimax.ts index a47907168d..2ac02cf9af 100644 --- a/packages/types/src/providers/minimax.ts +++ b/packages/types/src/providers/minimax.ts @@ -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 diff --git a/packages/types/src/providers/poe.ts b/packages/types/src/providers/poe.ts new file mode 100644 index 0000000000..c9b6211ddd --- /dev/null +++ b/packages/types/src/providers/poe.ts @@ -0,0 +1,2 @@ +export { poeDefaultModelId, POE_DEFAULT_BASE_URL, getPoeDefaultModelInfo } from "ai-sdk-provider-poe/code" +export type { PoeDefaultModelInfo } from "ai-sdk-provider-poe/code" diff --git a/packages/types/src/providers/xai.ts b/packages/types/src/providers/xai.ts index 755d692f40..a80e0d7357 100644 --- a/packages/types/src/providers/xai.ts +++ b/packages/types/src/providers/xai.ts @@ -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"], }, diff --git a/packages/types/src/roomodes-schema.ts b/packages/types/src/roomodes-schema.ts new file mode 100644 index 0000000000..6283395686 --- /dev/null +++ b/packages/types/src/roomodes-schema.ts @@ -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 { + const jsonSchema = zodToJsonSchema(roomodesZodSchema, { + $refStrategy: "none", + target: "jsonSchema7", + }) as Record + + 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 +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d95c2f0234..3b9421f6bc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 diff --git a/schemas/roomodes.json b/schemas/roomodes.json new file mode 100644 index 0000000000..2b4349afcb --- /dev/null +++ b/schemas/roomodes.json @@ -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." +} diff --git a/src/api/index.ts b/src/api/index.ts index ebc2682a1a..1891113c03 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -9,6 +9,7 @@ import { AnthropicHandler, AwsBedrockHandler, OpenRouterHandler, + PoeHandler, VertexHandler, AnthropicVertexHandler, OpenAiHandler, @@ -176,6 +177,8 @@ export function buildApiHandler(configuration: ProviderSettings): ApiHandler { return new MiniMaxHandler(options) case "baseten": return new BasetenHandler(options) + case "poe": + return new PoeHandler(options) default: return new AnthropicHandler(options) } diff --git a/src/api/providers/__tests__/minimax.spec.ts b/src/api/providers/__tests__/minimax.spec.ts index e5df368f50..37f6f12798 100644 --- a/src/api/providers/__tests__/minimax.spec.ts +++ b/src/api/providers/__tests__/minimax.spec.ts @@ -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) + }) }) }) diff --git a/src/api/providers/__tests__/poe.spec.ts b/src/api/providers/__tests__/poe.spec.ts new file mode 100644 index 0000000000..50f229a243 --- /dev/null +++ b/src/api/providers/__tests__/poe.spec.ts @@ -0,0 +1,307 @@ +const mockStreamText = vitest.fn() +const mockGenerateText = vitest.fn() +const mockCreatePoe = vitest.fn() + +vitest.mock("ai-sdk-provider-poe", () => ({ + createPoe: (...args: unknown[]) => mockCreatePoe(...args), +})) + +vitest.mock("ai-sdk-provider-poe/code", () => ({ + mapToolChoice: vitest.fn((value: unknown) => value), + extractUsageMetrics: vitest.fn((usage: any) => ({ + inputTokens: usage?.inputTokens || 0, + outputTokens: usage?.outputTokens || 0, + cacheReadTokens: usage?.cacheReadTokens, + cacheWriteTokens: usage?.cacheWriteTokens, + reasoningTokens: usage?.reasoningTokens, + })), + getPoeDefaultModelInfo: vitest.fn(() => ({ + maxTokens: 8192, + contextWindow: 200_000, + supportsImages: true, + supportsPromptCache: true, + inputPrice: 3, + outputPrice: 15, + })), +})) + +vitest.mock("ai", async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + streamText: (...args: unknown[]) => mockStreamText(...args), + generateText: (...args: unknown[]) => mockGenerateText(...args), + } +}) + +vitest.mock("../fetchers/modelCache", () => ({ + getModelsFromCache: vitest.fn().mockReturnValue({ + "anthropic/claude-sonnet-4": { + maxTokens: 10_000, + contextWindow: 200_000, + supportsImages: true, + supportsPromptCache: true, + supportsReasoningBudget: true, + inputPrice: 3, + outputPrice: 15, + }, + "openai/gpt-4o": { + maxTokens: 16_384, + contextWindow: 128_000, + supportsImages: true, + supportsPromptCache: false, + inputPrice: 2.5, + outputPrice: 10, + }, + "openai/o3": { + maxTokens: 100_000, + contextWindow: 200_000, + supportsImages: true, + supportsPromptCache: false, + supportsReasoningEffort: ["low", "medium", "high"], + inputPrice: 10, + outputPrice: 40, + }, + }), +})) + +import { poeDefaultModelId } from "@roo-code/types" +import { PoeHandler } from "../poe" + +describe("PoeHandler", () => { + const mockLanguageModel = { modelId: "test-model" } + const mockPoeProvider = vitest.fn().mockReturnValue(mockLanguageModel) + + beforeEach(() => { + vitest.clearAllMocks() + mockCreatePoe.mockReturnValue(mockPoeProvider) + }) + + describe("constructor", () => { + it("creates poe provider with api key and default base URL", () => { + new PoeHandler({ poeApiKey: "test-key" }) + + expect(mockCreatePoe).toHaveBeenCalledWith({ + apiKey: "test-key", + baseURL: undefined, + }) + }) + + it("creates poe provider with custom base URL", () => { + new PoeHandler({ poeApiKey: "key", poeBaseUrl: "https://custom.poe.com/v1" }) + + expect(mockCreatePoe).toHaveBeenCalledWith({ + apiKey: "key", + baseURL: "https://custom.poe.com/v1", + }) + }) + + it("uses fallback api key when not provided", () => { + new PoeHandler({}) + + expect(mockCreatePoe).toHaveBeenCalledWith({ + apiKey: "not-provided", + baseURL: undefined, + }) + }) + }) + + describe("getModel", () => { + it("returns model info from cache", () => { + const handler = new PoeHandler({ poeApiKey: "key", apiModelId: "anthropic/claude-sonnet-4" }) + const result = handler.getModel() + + expect(result.id).toBe("anthropic/claude-sonnet-4") + expect(result.info.contextWindow).toBe(200_000) + expect(result.info.maxTokens).toBe(10_000) + }) + + it("returns default model when no model ID specified", () => { + const handler = new PoeHandler({ poeApiKey: "key" }) + const result = handler.getModel() + + expect(result.id).toBe(poeDefaultModelId) + }) + + it("falls back to default model info when model not in cache", () => { + const handler = new PoeHandler({ poeApiKey: "key", apiModelId: "unknown/model" }) + const result = handler.getModel() + + expect(result.id).toBe("unknown/model") + expect(result.info.contextWindow).toBeGreaterThan(0) + expect(result.info.maxTokens).toBeGreaterThan(0) + }) + }) + + describe("createMessage", () => { + it("streams text chunks", async () => { + const handler = new PoeHandler({ poeApiKey: "key", apiModelId: "anthropic/claude-sonnet-4" }) + + const fullStream = (async function* () { + yield { type: "text-delta", text: "Hello " } + yield { type: "text-delta", text: "world!" } + })() + + mockStreamText.mockReturnValue({ + fullStream, + usage: Promise.resolve({ inputTokens: 10, outputTokens: 5 }), + }) + + const chunks = [] + for await (const chunk of handler.createMessage("system prompt", [ + { role: "user" as const, content: "Hi" }, + ])) { + chunks.push(chunk) + } + + expect(chunks).toContainEqual({ type: "text", text: "Hello " }) + expect(chunks).toContainEqual({ type: "text", text: "world!" }) + expect(chunks).toContainEqual(expect.objectContaining({ type: "usage", inputTokens: 10, outputTokens: 5 })) + }) + }) + + describe("reasoning", () => { + it("passes anthropic thinking config for budget models", async () => { + const handler = new PoeHandler({ + poeApiKey: "key", + apiModelId: "anthropic/claude-sonnet-4", + enableReasoningEffort: true, + modelMaxThinkingTokens: 4096, + }) + + const fullStream = (async function* () { + yield { type: "reasoning", text: "Let me think..." } + yield { type: "text-delta", text: "Answer" } + })() + + mockStreamText.mockReturnValue({ + fullStream, + usage: Promise.resolve({ inputTokens: 10, outputTokens: 5 }), + }) + + const chunks = [] + const modelMaxTokens = handler.getModel().info.maxTokens ?? 0 + for await (const chunk of handler.createMessage("system", [ + { role: "user" as const, content: "think about this" }, + ])) { + chunks.push(chunk) + } + + const callArgs = mockStreamText.mock.calls[0][0] + expect(callArgs.temperature).toBe(1.0) + expect(callArgs.providerOptions).toEqual({ + poe: { + reasoningBudgetTokens: 4096, + }, + }) + expect(callArgs.maxOutputTokens).toBe(modelMaxTokens - 4096) + + expect(chunks).toContainEqual({ type: "reasoning", text: "Let me think..." }) + expect(chunks).toContainEqual({ type: "text", text: "Answer" }) + }) + + it("passes openai reasoning effort for effort models", async () => { + const handler = new PoeHandler({ + poeApiKey: "key", + apiModelId: "openai/o3", + enableReasoningEffort: true, + reasoningEffort: "high", + }) + + const fullStream = (async function* () { + yield { type: "text-delta", text: "Answer" } + })() + + mockStreamText.mockReturnValue({ + fullStream, + usage: Promise.resolve({ inputTokens: 10, outputTokens: 5 }), + }) + + for await (const _ of handler.createMessage("system", [{ role: "user" as const, content: "reason" }])) { + /* drain */ + } + + expect(mockStreamText).toHaveBeenCalledWith( + expect.objectContaining({ + providerOptions: { + poe: { + reasoningEffort: "high", + reasoningSummary: "auto", + }, + }, + }), + ) + }) + + it("does not pass providerOptions when reasoning is disabled", async () => { + const handler = new PoeHandler({ + poeApiKey: "key", + apiModelId: "anthropic/claude-sonnet-4", + enableReasoningEffort: false, + }) + + const fullStream = (async function* () { + yield { type: "text-delta", text: "Answer" } + })() + + mockStreamText.mockReturnValue({ + fullStream, + usage: Promise.resolve({ inputTokens: 1, outputTokens: 1 }), + }) + + for await (const _ of handler.createMessage("system", [{ role: "user" as const, content: "hi" }])) { + /* drain */ + } + + const callArgs = mockStreamText.mock.calls[0][0] + expect(callArgs.providerOptions).toBeUndefined() + expect(callArgs.temperature).toBeUndefined() + }) + + it("uses default thinking budget when not specified", async () => { + const handler = new PoeHandler({ + poeApiKey: "key", + apiModelId: "anthropic/claude-sonnet-4", + enableReasoningEffort: true, + }) + + const fullStream = (async function* () {})() + mockStreamText.mockReturnValue({ + fullStream, + usage: Promise.resolve({ inputTokens: 0, outputTokens: 0 }), + }) + + for await (const _ of handler.createMessage("system", [{ role: "user" as const, content: "hi" }])) { + /* drain */ + } + + const callArgs = mockStreamText.mock.calls[0][0] + expect(callArgs.providerOptions).toEqual({ + poe: { + reasoningBudgetTokens: expect.any(Number), + }, + }) + expect(callArgs.providerOptions.poe.reasoningBudgetTokens + callArgs.maxOutputTokens).toBe( + handler.getModel().info.maxTokens, + ) + }) + }) + + describe("completePrompt", () => { + it("returns generated text", async () => { + const handler = new PoeHandler({ poeApiKey: "key", apiModelId: "openai/gpt-4o" }) + + mockGenerateText.mockResolvedValue({ text: "generated response" }) + + const result = await handler.completePrompt("complete this") + + expect(result).toBe("generated response") + expect(mockGenerateText).toHaveBeenCalledWith( + expect.objectContaining({ + model: mockLanguageModel, + prompt: "complete this", + }), + ) + }) + }) +}) diff --git a/src/api/providers/__tests__/xai.spec.ts b/src/api/providers/__tests__/xai.spec.ts index c622c9d4fc..763d10d027 100644 --- a/src/api/providers/__tests__/xai.spec.ts +++ b/src/api/providers/__tests__/xai.spec.ts @@ -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}`) }) }) diff --git a/src/api/providers/fetchers/__tests__/poe.spec.ts b/src/api/providers/fetchers/__tests__/poe.spec.ts new file mode 100644 index 0000000000..070caf0cc6 --- /dev/null +++ b/src/api/providers/fetchers/__tests__/poe.spec.ts @@ -0,0 +1,131 @@ +import { getPoeModels } from "../poe" + +const mockFetchPoeModels = vitest.fn() +const mockGetModels = vitest.fn() + +vitest.mock("ai-sdk-provider-poe/code", () => ({ + fetchPoeModels: (...args: unknown[]) => mockFetchPoeModels(...args), + getModels: () => mockGetModels(), +})) + +describe("getPoeModels", () => { + beforeEach(() => vitest.clearAllMocks()) + + it("converts PoeModelInfo to ModelRecord", async () => { + mockFetchPoeModels.mockResolvedValue([]) + mockGetModels.mockReturnValue([ + { + id: "anthropic/claude-sonnet-4", + rawId: "claude-sonnet-4", + contextWindow: 200_000, + maxOutputTokens: 8192, + supportsImages: true, + supportsPromptCache: true, + supportsReasoningBudget: true, + pricing: { + inputPerMillion: 3, + outputPerMillion: 15, + cacheReadPerMillion: 0.3, + cacheWritePerMillion: 3.75, + }, + }, + { + id: "openai/gpt-4o", + rawId: "gpt-4o", + contextWindow: 128_000, + maxOutputTokens: 16_384, + supportsImages: true, + supportsPromptCache: false, + pricing: { + inputPerMillion: 2.5, + outputPerMillion: 10, + }, + }, + ]) + + const models = await getPoeModels("test-key") + + expect(mockFetchPoeModels).toHaveBeenCalledWith({ apiKey: "test-key", baseURL: undefined }) + + expect(models["anthropic/claude-sonnet-4"]).toEqual({ + contextWindow: 200_000, + maxTokens: 8192, + supportsImages: true, + supportsPromptCache: true, + supportsReasoningBudget: true, + inputPrice: 3, + outputPrice: 15, + cacheReadsPrice: 0.3, + cacheWritesPrice: 3.75, + }) + + expect(models["openai/gpt-4o"]).toEqual({ + contextWindow: 128_000, + maxTokens: 16_384, + supportsImages: true, + supportsPromptCache: false, + inputPrice: 2.5, + outputPrice: 10, + }) + }) + + it("passes baseURL when provided", async () => { + mockFetchPoeModels.mockResolvedValue([]) + mockGetModels.mockReturnValue([]) + + await getPoeModels("key", "https://custom.api.com/v1") + + expect(mockFetchPoeModels).toHaveBeenCalledWith({ apiKey: "key", baseURL: "https://custom.api.com/v1" }) + }) + + it("returns empty record on empty response", async () => { + mockFetchPoeModels.mockResolvedValue([]) + mockGetModels.mockReturnValue([]) + + const models = await getPoeModels("key") + + expect(models).toEqual({}) + }) + + it("handles models with missing optional fields", async () => { + mockFetchPoeModels.mockResolvedValue([]) + mockGetModels.mockReturnValue([ + { + id: "some-model", + rawId: "some-model", + contextWindow: 4096, + maxOutputTokens: 1024, + supportsImages: false, + supportsPromptCache: false, + }, + ]) + + const models = await getPoeModels("key") + + expect(models["some-model"]).toEqual({ + contextWindow: 4096, + maxTokens: 1024, + supportsImages: false, + supportsPromptCache: false, + }) + }) + + it("maps supportsReasoningEffort when present", async () => { + mockFetchPoeModels.mockResolvedValue([]) + mockGetModels.mockReturnValue([ + { + id: "openai/o3", + rawId: "o3", + contextWindow: 200_000, + maxOutputTokens: 100_000, + supportsImages: true, + supportsPromptCache: false, + supportsReasoningEffort: ["low", "medium", "high"], + }, + ]) + + const models = await getPoeModels("key") + + expect(models["openai/o3"].supportsReasoningEffort).toEqual(["low", "medium", "high"]) + }) +}) diff --git a/src/api/providers/fetchers/modelCache.ts b/src/api/providers/fetchers/modelCache.ts index a574a660bc..a2c98e49ca 100644 --- a/src/api/providers/fetchers/modelCache.ts +++ b/src/api/providers/fetchers/modelCache.ts @@ -24,6 +24,7 @@ import { getLiteLLMModels } from "./litellm" import { GetModelsOptions } from "../../../shared/api" import { getOllamaModels } from "./ollama" import { getLMStudioModels } from "./lmstudio" +import { getPoeModels } from "./poe" import { getRooModels } from "./roo" const memoryCache = new NodeCache({ stdTTL: 5 * 60, checkperiod: 5 * 60 }) @@ -91,6 +92,9 @@ async function fetchModelsFromProvider(options: GetModelsOptions): Promise { + try { + // fetchPoeModels populates the internal model store, then getModels() + // returns only code-capable models with camelCase fields. + await fetchPoeModels({ apiKey, baseURL }) + const poeModels = getModels() + const models: ModelRecord = {} + + for (const m of poeModels) { + // The library's applyReasoningFallbacks workaround sets + // supportsReasoningEffort to boolean `true` for any model that + // supports /v1/responses, even when the model has no actual + // reasoning capability (e.g. Haiku 3/3.5). Only trust the value + // when it is an explicit array of effort levels. + const effort = Array.isArray(m.supportsReasoningEffort) ? m.supportsReasoningEffort : undefined + const info: ModelInfo = { + contextWindow: m.contextWindow, + maxTokens: m.maxOutputTokens, + supportsImages: m.supportsImages, + supportsPromptCache: m.supportsPromptCache, + ...(m.supportsReasoningBudget && { supportsReasoningBudget: m.supportsReasoningBudget }), + ...(effort && { + supportsReasoningEffort: effort as ModelInfo["supportsReasoningEffort"], + }), + ...(m.pricing?.inputPerMillion != null && { inputPrice: m.pricing.inputPerMillion }), + ...(m.pricing?.outputPerMillion != null && { outputPrice: m.pricing.outputPerMillion }), + ...(m.pricing?.cacheReadPerMillion != null && { cacheReadsPrice: m.pricing.cacheReadPerMillion }), + ...(m.pricing?.cacheWritePerMillion != null && { cacheWritesPrice: m.pricing.cacheWritePerMillion }), + } + + models[m.id] = info + } + + return models + } catch (error) { + console.error( + `[Poe] Error fetching models: ${JSON.stringify(error, Object.getOwnPropertyNames(error as object), 2)}`, + ) + return {} + } +} diff --git a/src/api/providers/index.ts b/src/api/providers/index.ts index b6de795210..41aff953d4 100644 --- a/src/api/providers/index.ts +++ b/src/api/providers/index.ts @@ -14,6 +14,7 @@ export { OpenAiHandler } from "./openai" export { OpenAICompatibleHandler } from "./openai-compatible" export type { OpenAICompatibleConfig } from "./openai-compatible" export { OpenRouterHandler } from "./openrouter" +export { PoeHandler } from "./poe" export { QwenCodeHandler } from "./qwen-code" export { RequestyHandler } from "./requesty" export { SambaNovaHandler } from "./sambanova" diff --git a/src/api/providers/poe.ts b/src/api/providers/poe.ts new file mode 100644 index 0000000000..536d222acd --- /dev/null +++ b/src/api/providers/poe.ts @@ -0,0 +1,151 @@ +import { Anthropic } from "@anthropic-ai/sdk" +import { createPoe, type PoeProvider, type PoeScopedProviderOptions } from "ai-sdk-provider-poe" +import { extractUsageMetrics, mapToolChoice } from "ai-sdk-provider-poe/code" +import { streamText, generateText, type ToolSet } from "ai" + +import { + poeDefaultModelId, + getPoeDefaultModelInfo, + type ModelInfo, + type ReasoningEffortExtended, + ApiProviderError, +} from "@roo-code/types" +import { TelemetryService } from "@roo-code/telemetry" + +import { shouldUseReasoningBudget, shouldUseReasoningEffort, type ApiHandlerOptions } from "../../shared/api" + +import { convertToAiSdkMessages, convertToolsForAiSdk, processAiSdkStreamPart } from "../transform/ai-sdk" +import { ApiStream } from "../transform/stream" + +import { BaseProvider } from "./base-provider" +import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index" +import { getModelsFromCache } from "./fetchers/modelCache" + +const DEFAULT_THINKING_BUDGET = 8192 + +export class PoeHandler extends BaseProvider implements SingleCompletionHandler { + protected options: ApiHandlerOptions + private poe: PoeProvider + + constructor(options: ApiHandlerOptions) { + super() + this.options = options + this.poe = createPoe({ + apiKey: options.poeApiKey ?? "not-provided", + baseURL: options.poeBaseUrl || undefined, + }) + } + + override getModel() { + const id = this.options.apiModelId ?? poeDefaultModelId + const cached = getModelsFromCache("poe") + const info: ModelInfo = cached?.[id] ?? getPoeDefaultModelInfo() + return { id, info } + } + + override async *createMessage( + systemPrompt: string, + messages: Anthropic.Messages.MessageParam[], + metadata?: ApiHandlerCreateMessageMetadata, + ): ApiStream { + const { id, info } = this.getModel() + const languageModel = this.poe(id) + + const aiSdkMessages = convertToAiSdkMessages(messages) + const openAiTools = this.convertToolsForOpenAI(metadata?.tools) + const aiSdkTools = convertToolsForAiSdk(openAiTools) as ToolSet | undefined + + const useBudget = shouldUseReasoningBudget({ model: info, settings: this.options }) + const useEffort = !useBudget && shouldUseReasoningEffort({ model: info, settings: this.options }) + + // Only pass temperature when the user explicitly configured it. + let temperature: number | undefined = this.options.modelTemperature ?? undefined + let maxOutputTokens: number | undefined + const providerOptions: NonNullable[0]["providerOptions"]> & { + poe?: PoeScopedProviderOptions + } = {} + + if (useBudget) { + const requestedBudget = this.options.modelMaxThinkingTokens ?? DEFAULT_THINKING_BUDGET + // maxOutputTokens is the text-only budget; reasoningBudgetTokens is + // separate, so total output = maxOutputTokens + reasoningBudgetTokens. + maxOutputTokens = this.options.modelMaxTokens ?? Math.max(0, (info.maxTokens ?? 0) - requestedBudget) + providerOptions.poe = { + reasoningBudgetTokens: requestedBudget, + } + temperature = 1.0 + } else if (useEffort) { + let effort = (this.options.reasoningEffort ?? info.reasoningEffort ?? "medium") as ReasoningEffortExtended + // Validate that the effort level is actually supported by the current model + const supportedEfforts = info.supportsReasoningEffort + if (Array.isArray(supportedEfforts) && !supportedEfforts.includes(effort as any)) { + effort = (info.reasoningEffort as ReasoningEffortExtended) ?? "medium" + } + providerOptions.poe = { + reasoningEffort: effort, + reasoningSummary: "auto", + } + if (this.options.modelMaxTokens) { + maxOutputTokens = this.options.modelMaxTokens + } + } + + let result + try { + result = streamText({ + model: languageModel, + system: systemPrompt, + messages: aiSdkMessages, + temperature, + maxOutputTokens, + tools: aiSdkTools, + toolChoice: mapToolChoice(metadata?.tool_choice as any), + ...(Object.keys(providerOptions).length > 0 && { providerOptions }), + }) + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + TelemetryService.instance.captureException(new ApiProviderError(errorMessage, "poe", id, "createMessage")) + throw new Error(`Poe completion error: ${errorMessage}`) + } + + try { + for await (const part of result.fullStream) { + for (const chunk of processAiSdkStreamPart(part)) { + yield chunk + } + } + + const usage = await result.usage + if (usage) { + const metrics = extractUsageMetrics(usage as any) + yield { + type: "usage" as const, + inputTokens: metrics.inputTokens, + outputTokens: metrics.outputTokens, + cacheReadTokens: metrics.cacheReadTokens, + cacheWriteTokens: metrics.cacheWriteTokens, + reasoningTokens: metrics.reasoningTokens, + } + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + TelemetryService.instance.captureException(new ApiProviderError(errorMessage, "poe", id, "createMessage")) + throw new Error(`Poe streaming error: ${errorMessage}`) + } + } + + async completePrompt(prompt: string): Promise { + const { id } = this.getModel() + try { + const { text } = await generateText({ + model: this.poe(id), + prompt, + }) + return text + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + TelemetryService.instance.captureException(new ApiProviderError(errorMessage, "poe", id, "completePrompt")) + throw new Error(`Poe completion error: ${errorMessage}`) + } + } +} diff --git a/src/api/providers/qwen-code.ts b/src/api/providers/qwen-code.ts index 18d09a59f3..f2a207051e 100644 --- a/src/api/providers/qwen-code.ts +++ b/src/api/providers/qwen-code.ts @@ -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 diff --git a/src/api/providers/xai.ts b/src/api/providers/xai.ts index 8b973d41c4..0cd9cb0273 100644 --- a/src/api/providers/xai.ts +++ b/src/api/providers/xai.ts @@ -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 = { + 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 try { - stream = await this.client.chat.completions.create(requestOptions) + stream = (await this.client.responses.create({ + ...requestBody, + stream: true, + } as any)) as unknown as AsyncIterable } 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 { - 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) } diff --git a/src/api/transform/__tests__/responses-api-input.spec.ts b/src/api/transform/__tests__/responses-api-input.spec.ts new file mode 100644 index 0000000000..c57b61d895 --- /dev/null +++ b/src/api/transform/__tests__/responses-api-input.spec.ts @@ -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", + }), + ) + }) + }) +}) diff --git a/src/api/transform/__tests__/responses-api-stream.spec.ts b/src/api/transform/__tests__/responses-api-stream.spec.ts new file mode 100644 index 0000000000..6abdbddb65 --- /dev/null +++ b/src/api/transform/__tests__/responses-api-stream.spec.ts @@ -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) { + 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, + }), + ) + }) +}) diff --git a/src/api/transform/responses-api-input.ts b/src/api/transform/responses-api-input.ts new file mode 100644 index 0000000000..a766dfef6e --- /dev/null +++ b/src/api/transform/responses-api-input.ts @@ -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 +} diff --git a/src/api/transform/responses-api-stream.ts b/src/api/transform/responses-api-stream.ts new file mode 100644 index 0000000000..884a755136 --- /dev/null +++ b/src/api/transform/responses-api-stream.ts @@ -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, + 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 } : {}), + } + } +} diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 7bd969e52d..2ffe421c09 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -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 diff --git a/src/core/webview/__tests__/ClineProvider.spec.ts b/src/core/webview/__tests__/ClineProvider.spec.ts index cfa4b0317f..da0fb2003f 100644 --- a/src/core/webview/__tests__/ClineProvider.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.spec.ts @@ -2494,6 +2494,7 @@ describe("ClineProvider - Router Models", () => { ollama: {}, lmstudio: {}, "vercel-ai-gateway": mockModels, + poe: {}, }, values: undefined, }) @@ -2540,6 +2541,7 @@ describe("ClineProvider - Router Models", () => { lmstudio: {}, litellm: {}, "vercel-ai-gateway": mockModels, + poe: {}, }, values: undefined, }) @@ -2634,6 +2636,7 @@ describe("ClineProvider - Router Models", () => { ollama: {}, lmstudio: {}, "vercel-ai-gateway": mockModels, + poe: {}, }, values: undefined, }) diff --git a/src/core/webview/__tests__/webviewMessageHandler.spec.ts b/src/core/webview/__tests__/webviewMessageHandler.spec.ts index 2dcaaf3db5..cb9327c601 100644 --- a/src/core/webview/__tests__/webviewMessageHandler.spec.ts +++ b/src/core/webview/__tests__/webviewMessageHandler.spec.ts @@ -346,6 +346,7 @@ describe("webviewMessageHandler - requestRouterModels", () => { ollama: {}, lmstudio: {}, "vercel-ai-gateway": mockModels, + poe: {}, }, values: undefined, }) @@ -431,6 +432,7 @@ describe("webviewMessageHandler - requestRouterModels", () => { ollama: {}, lmstudio: {}, "vercel-ai-gateway": mockModels, + poe: {}, }, values: undefined, }) @@ -486,6 +488,7 @@ describe("webviewMessageHandler - requestRouterModels", () => { ollama: {}, lmstudio: {}, "vercel-ai-gateway": mockModels, + poe: {}, }, values: undefined, }) diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index d27fd6bec0..e3b8c1bea8 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -955,6 +955,7 @@ export const webviewMessageHandler = async ( ollama: {}, lmstudio: {}, roo: {}, + poe: {}, } const safeGetModels = async (options: GetModelsOptions): Promise => { @@ -1018,6 +1019,21 @@ export const webviewMessageHandler = async ( }) } + // Poe is conditional on apiKey + const poeApiKey = apiConfiguration.poeApiKey || message?.values?.poeApiKey + const poeBaseUrl = apiConfiguration.poeBaseUrl || message?.values?.poeBaseUrl + + if (poeApiKey) { + if (message?.values?.poeApiKey || message?.values?.poeBaseUrl) { + await flushModels({ provider: "poe", apiKey: poeApiKey, baseUrl: poeBaseUrl }, true) + } + + candidates.push({ + key: "poe", + options: { provider: "poe", apiKey: poeApiKey, baseUrl: poeBaseUrl }, + }) + } + // Apply single provider filter if specified const modelFetchPromises = providerFilter ? candidates.filter(({ key }) => key === providerFilter) diff --git a/src/package.json b/src/package.json index 7c4889abd8..ea424f475a 100644 --- a/src/package.json +++ b/src/package.json @@ -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", diff --git a/src/shared/api.ts b/src/shared/api.ts index 52af6b2072..a68abcc3ad 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -177,6 +177,7 @@ const dynamicProviderExtras = { ollama: {} as {}, // eslint-disable-line @typescript-eslint/no-empty-object-type lmstudio: {} as {}, // eslint-disable-line @typescript-eslint/no-empty-object-type roo: {} as { apiKey?: string; baseUrl?: string }, + poe: {} as { apiKey?: string; baseUrl?: string }, } as const satisfies Record // Build the dynamic options union from the map, intersected with CommonFetchParams diff --git a/webview-ui/src/components/chat/__tests__/Announcement.spec.tsx b/webview-ui/src/components/chat/__tests__/Announcement.spec.tsx index 8a06322f7b..44d147ecbc 100644 --- a/webview-ui/src/components/chat/__tests__/Announcement.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/Announcement.spec.tsx @@ -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 = { "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() - 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() }) diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index 4d914a4833..a6e4cc3f5f 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -1,7 +1,7 @@ import React, { memo, useCallback, useEffect, useMemo, useState } from "react" import { convertHeadersToObject } from "./utils/headers" import { useDebounce } from "react-use" -import { VSCodeLink } from "@vscode/webview-ui-toolkit/react" +import { VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react" import { ExternalLinkIcon } from "@radix-ui/react-icons" import { @@ -10,6 +10,7 @@ import { isRetiredProvider, DEFAULT_CONSECUTIVE_MISTAKE_LIMIT, openRouterDefaultModelId, + poeDefaultModelId, requestyDefaultModelId, litellmDefaultModelId, openAiNativeDefaultModelId, @@ -80,6 +81,7 @@ import { OpenAICompatible, OpenAICodex, OpenRouter, + Poe, QwenCode, Requesty, Roo, @@ -238,7 +240,7 @@ const ApiOptions = ({ vscode.postMessage({ type: "requestLmStudioModels" }) } else if (selectedProvider === "vscode-lm") { vscode.postMessage({ type: "requestVsCodeLmModels" }) - } else if (selectedProvider === "litellm" || selectedProvider === "roo") { + } else if (selectedProvider === "litellm" || selectedProvider === "roo" || selectedProvider === "poe") { vscode.postMessage({ type: "requestRouterModels" }) } }, @@ -252,6 +254,8 @@ const ApiOptions = ({ apiConfiguration?.lmStudioBaseUrl, apiConfiguration?.litellmBaseUrl, apiConfiguration?.litellmApiKey, + apiConfiguration?.poeApiKey, + apiConfiguration?.poeBaseUrl, customHeaders, ], ) @@ -356,6 +360,7 @@ const ApiOptions = ({ : internationalZAiDefaultModelId, }, fireworks: { field: "apiModelId", default: fireworksDefaultModelId }, + poe: { field: "apiModelId", default: poeDefaultModelId }, roo: { field: "apiModelId", default: rooDefaultModelId }, "vercel-ai-gateway": { field: "vercelAiGatewayModelId", default: vercelAiGatewayDefaultModelId }, openai: { field: "openAiModelId" }, @@ -703,6 +708,16 @@ const ApiOptions = ({ /> )} + {selectedProvider === "poe" && ( + + )} + {selectedProvider === "roo" && ( setApiConfigurationField("consecutiveMistakeLimit", value)} /> + {selectedProvider === "poe" && ( + + + + )} {selectedProvider === "openrouter" && openRouterModelProviders && Object.keys(openRouterModelProviders).length > 0 && ( diff --git a/webview-ui/src/components/settings/constants.ts b/webview-ui/src/components/settings/constants.ts index 46789cb67a..14f04cb5b2 100644 --- a/webview-ui/src/components/settings/constants.ts +++ b/webview-ui/src/components/settings/constants.ts @@ -65,4 +65,5 @@ export const PROVIDERS = [ { value: "minimax", label: "MiniMax", proxy: false }, { value: "baseten", label: "Baseten", proxy: false }, { value: "unbound", label: "Unbound", proxy: false }, + { value: "poe", label: "Poe", proxy: false }, ].sort((a, b) => a.label.localeCompare(b.label)) diff --git a/webview-ui/src/components/settings/providers/Poe.tsx b/webview-ui/src/components/settings/providers/Poe.tsx new file mode 100644 index 0000000000..7b79ed8510 --- /dev/null +++ b/webview-ui/src/components/settings/providers/Poe.tsx @@ -0,0 +1,166 @@ +import { useCallback, useState, useEffect, useRef } from "react" +import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react" +import { useQueryClient } from "@tanstack/react-query" + +import { + type ProviderSettings, + type OrganizationAllowList, + type ExtensionMessage, + poeDefaultModelId, + type ProviderName, +} from "@roo-code/types" + +import { RouterName } from "@roo/api" + +import { useAppTranslation } from "@src/i18n/TranslationContext" +import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink" +import { useExtensionState } from "@src/context/ExtensionStateContext" +import { vscode } from "@src/utils/vscode" +import { Button } from "@src/components/ui" + +import { inputEventTransform } from "../transforms" +import { ModelPicker } from "../ModelPicker" +import { handleModelChangeSideEffects } from "../utils/providerModelConfig" + +type PoeProps = { + apiConfiguration: ProviderSettings + setApiConfigurationField: (field: keyof ProviderSettings, value: ProviderSettings[keyof ProviderSettings]) => void + organizationAllowList: OrganizationAllowList + modelValidationError?: string + simplifySettings?: boolean +} + +export const Poe = ({ + apiConfiguration, + setApiConfigurationField, + organizationAllowList, + modelValidationError, + simplifySettings, +}: PoeProps) => { + const { t } = useAppTranslation() + const queryClient = useQueryClient() + const { routerModels } = useExtensionState() + const [refreshStatus, setRefreshStatus] = useState<"idle" | "loading" | "success" | "error">("idle") + const [refreshError, setRefreshError] = useState() + const poeErrorJustReceived = useRef(false) + + useEffect(() => { + const handleMessage = (event: MessageEvent) => { + const message = event.data + if (message.type === "singleRouterModelFetchResponse" && !message.success) { + const providerName = message.values?.provider as RouterName + if (providerName === "poe") { + poeErrorJustReceived.current = true + setRefreshStatus("error") + setRefreshError(message.error) + } + } else if (message.type === "routerModels") { + if (refreshStatus === "loading") { + if (!poeErrorJustReceived.current) { + setRefreshStatus("success") + // Invalidate the react-query router models cache so + // validation in ApiOptions picks up the refreshed list. + queryClient.invalidateQueries({ queryKey: ["routerModels"] }) + } + } + } + } + + window.addEventListener("message", handleMessage) + return () => { + window.removeEventListener("message", handleMessage) + } + }, [refreshStatus, queryClient]) + + const handleInputChange = useCallback( + ( + field: K, + transform: (event: E) => ProviderSettings[K] = inputEventTransform, + ) => + (event: E | Event) => { + setApiConfigurationField(field, transform(event as E)) + }, + [setApiConfigurationField], + ) + + const handleRefreshModels = useCallback(() => { + poeErrorJustReceived.current = false + setRefreshStatus("loading") + setRefreshError(undefined) + + const key = apiConfiguration.poeApiKey + + if (!key) { + setRefreshStatus("error") + setRefreshError(t("settings:providers.refreshModels.missingConfig")) + return + } + + vscode.postMessage({ + type: "requestRouterModels", + values: { poeApiKey: key, poeBaseUrl: apiConfiguration.poeBaseUrl }, + }) + }, [apiConfiguration, t]) + + return ( + <> + + + +
+ {t("settings:providers.apiKeyStorageNotice")} +
+ {!apiConfiguration?.poeApiKey && ( + + {t("settings:providers.getPoeApiKey")} + + )} + + {refreshStatus === "loading" && ( +
+ {t("settings:providers.refreshModels.loading")} +
+ )} + {refreshStatus === "success" && ( +
{t("settings:providers.refreshModels.success")}
+ )} + {refreshStatus === "error" && ( +
+ {refreshError || t("settings:providers.refreshModels.error")} +
+ )} + + handleModelChangeSideEffects("poe" as ProviderName, modelId, setApiConfigurationField) + } + /> + + ) +} diff --git a/webview-ui/src/components/settings/providers/index.ts b/webview-ui/src/components/settings/providers/index.ts index 597caffd1d..4a64ce9586 100644 --- a/webview-ui/src/components/settings/providers/index.ts +++ b/webview-ui/src/components/settings/providers/index.ts @@ -10,6 +10,7 @@ export { OpenAI } from "./OpenAI" export { OpenAICodex } from "./OpenAICodex" export { OpenAICompatible } from "./OpenAICompatible" export { OpenRouter } from "./OpenRouter" +export { Poe } from "./Poe" export { QwenCode } from "./QwenCode" export { Roo } from "./Roo" export { Requesty } from "./Requesty" diff --git a/webview-ui/src/components/settings/utils/providerModelConfig.ts b/webview-ui/src/components/settings/utils/providerModelConfig.ts index fa71814390..59f76862b4 100644 --- a/webview-ui/src/components/settings/utils/providerModelConfig.ts +++ b/webview-ui/src/components/settings/utils/providerModelConfig.ts @@ -150,8 +150,10 @@ export const handleModelChangeSideEffects = ( setApiConfigurationField("awsCustomArn" as K, "" as ProviderSettings[K]) } - // All providers: Clear reasoning effort when switching models to allow - // the new model's default to take effect. Different models within the - // same provider can have different reasoning effort defaults/options. + // All providers: Clear reasoning settings when switching models to allow + // the new model's defaults to take effect. Different models within the + // same provider can have different reasoning defaults/options. setApiConfigurationField("reasoningEffort" as K, undefined as ProviderSettings[K]) + setApiConfigurationField("modelMaxTokens" as K, undefined as ProviderSettings[K]) + setApiConfigurationField("modelMaxThinkingTokens" as K, undefined as ProviderSettings[K]) } diff --git a/webview-ui/src/components/ui/hooks/__tests__/useSelectedModel.spec.ts b/webview-ui/src/components/ui/hooks/__tests__/useSelectedModel.spec.ts index 2c24e4b565..ed408d4e59 100644 --- a/webview-ui/src/components/ui/hooks/__tests__/useSelectedModel.spec.ts +++ b/webview-ui/src/components/ui/hooks/__tests__/useSelectedModel.spec.ts @@ -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"]) + }) + }) }) diff --git a/webview-ui/src/components/ui/hooks/useSelectedModel.ts b/webview-ui/src/components/ui/hooks/useSelectedModel.ts index c32a08990c..7192d9d4ee 100644 --- a/webview-ui/src/components/ui/hooks/useSelectedModel.ts +++ b/webview-ui/src/components/ui/hooks/useSelectedModel.ts @@ -317,6 +317,11 @@ function getSelectedModel({ const info = routerModels.roo?.[id] return { id, info } } + case "poe": { + const id = apiConfiguration.apiModelId ?? defaultModelId + const info = routerModels.poe?.[id] + return { id, info } + } case "qwen-code": { const id = apiConfiguration.apiModelId ?? defaultModelId const info = qwenCodeModels[id as keyof typeof qwenCodeModels] diff --git a/webview-ui/src/i18n/locales/ca/chat.json b/webview-ui/src/i18n/locales/ca/chat.json index c33634067c..57f0acecd0 100644 --- a/webview-ui/src/i18n/locales/ca/chat.json +++ b/webview-ui/src/i18n/locales/ca/chat.json @@ -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 d’OpenAI 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 d’IA 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." diff --git a/webview-ui/src/i18n/locales/ca/settings.json b/webview-ui/src/i18n/locales/ca/settings.json index 2c83cabbbc..4bd1b8ef33 100644 --- a/webview-ui/src/i18n/locales/ca/settings.json +++ b/webview-ui/src/i18n/locales/ca/settings.json @@ -380,6 +380,9 @@ "vertex1MContextBetaDescription": "Amplia la finestra de context a 1 milió de tokens per a Claude Sonnet 4.x / Claude Opus 4.6", "basetenApiKey": "Clau API de Baseten", "getBasetenApiKey": "Obtenir clau API de Baseten", + "poeApiKey": "Clau API de Poe", + "getPoeApiKey": "Obtenir clau API de Poe", + "poeBaseUrl": "URL base de Poe", "fireworksApiKey": "Clau API de Fireworks", "getFireworksApiKey": "Obtenir clau API de Fireworks", "deepSeekApiKey": "Clau API de DeepSeek", diff --git a/webview-ui/src/i18n/locales/de/chat.json b/webview-ui/src/i18n/locales/de/chat.json index 0512dd92ef..727ec44c2d 100644 --- a/webview-ui/src/i18n/locales/de/chat.json +++ b/webview-ui/src/i18n/locales/de/chat.json @@ -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." diff --git a/webview-ui/src/i18n/locales/de/settings.json b/webview-ui/src/i18n/locales/de/settings.json index c31d29147d..c524ffb462 100644 --- a/webview-ui/src/i18n/locales/de/settings.json +++ b/webview-ui/src/i18n/locales/de/settings.json @@ -380,6 +380,9 @@ "vertex1MContextBetaDescription": "Erweitert das Kontextfenster für Claude Sonnet 4.x / Claude Opus 4.6 auf 1 Million Token", "basetenApiKey": "Baseten API-Schlüssel", "getBasetenApiKey": "Baseten API-Schlüssel erhalten", + "poeApiKey": "Poe API-Schlüssel", + "getPoeApiKey": "Poe API-Schlüssel erhalten", + "poeBaseUrl": "Poe Basis-URL", "fireworksApiKey": "Fireworks API-Schlüssel", "getFireworksApiKey": "Fireworks API-Schlüssel erhalten", "deepSeekApiKey": "DeepSeek API-Schlüssel", diff --git a/webview-ui/src/i18n/locales/en/chat.json b/webview-ui/src/i18n/locales/en/chat.json index 0e790e5c9a..81338ad786 100644 --- a/webview-ui/src/i18n/locales/en/chat.json +++ b/webview-ui/src/i18n/locales/en/chat.json @@ -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:", diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index 3b2497aaee..183cd663e3 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -443,6 +443,9 @@ "vertex1MContextBetaDescription": "Extends context window to 1 million tokens for Claude Sonnet 4.x / Claude Opus 4.6", "basetenApiKey": "Baseten API Key", "getBasetenApiKey": "Get Baseten API Key", + "poeApiKey": "Poe API Key", + "getPoeApiKey": "Get Poe API Key", + "poeBaseUrl": "Poe Base URL", "fireworksApiKey": "Fireworks API Key", "getFireworksApiKey": "Get Fireworks API Key", "deepSeekApiKey": "DeepSeek API Key", diff --git a/webview-ui/src/i18n/locales/es/chat.json b/webview-ui/src/i18n/locales/es/chat.json index c984150dc3..1aa49f74f0 100644 --- a/webview-ui/src/i18n/locales/es/chat.json +++ b/webview-ui/src/i18n/locales/es/chat.json @@ -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." diff --git a/webview-ui/src/i18n/locales/es/settings.json b/webview-ui/src/i18n/locales/es/settings.json index 6595c4f907..dc2634b718 100644 --- a/webview-ui/src/i18n/locales/es/settings.json +++ b/webview-ui/src/i18n/locales/es/settings.json @@ -380,6 +380,9 @@ "vertex1MContextBetaDescription": "Amplía la ventana de contexto a 1 millón de tokens para Claude Sonnet 4.x / Claude Opus 4.6", "basetenApiKey": "Clave API de Baseten", "getBasetenApiKey": "Obtener clave API de Baseten", + "poeApiKey": "Clave API de Poe", + "getPoeApiKey": "Obtener clave API de Poe", + "poeBaseUrl": "URL base de Poe", "fireworksApiKey": "Clave API de Fireworks", "getFireworksApiKey": "Obtener clave API de Fireworks", "deepSeekApiKey": "Clave API de DeepSeek", diff --git a/webview-ui/src/i18n/locales/fr/chat.json b/webview-ui/src/i18n/locales/fr/chat.json index 6c44efc21c..316a846199 100644 --- a/webview-ui/src/i18n/locales/fr/chat.json +++ b/webview-ui/src/i18n/locales/fr/chat.json @@ -348,8 +348,8 @@ }, "release": { "heading": "Quoi de neuf :", - "gpt54": "Prise en charge d’OpenAI 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 d’IA 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." diff --git a/webview-ui/src/i18n/locales/fr/settings.json b/webview-ui/src/i18n/locales/fr/settings.json index 56337bda14..ceed03755c 100644 --- a/webview-ui/src/i18n/locales/fr/settings.json +++ b/webview-ui/src/i18n/locales/fr/settings.json @@ -380,6 +380,9 @@ "vertex1MContextBetaDescription": "Étend la fenêtre de contexte à 1 million de tokens pour Claude Sonnet 4.x / Claude Opus 4.6", "basetenApiKey": "Clé API Baseten", "getBasetenApiKey": "Obtenir la clé API Baseten", + "poeApiKey": "Clé API Poe", + "getPoeApiKey": "Obtenir la clé API Poe", + "poeBaseUrl": "URL de base Poe", "fireworksApiKey": "Clé API Fireworks", "getFireworksApiKey": "Obtenir la clé API Fireworks", "deepSeekApiKey": "Clé API DeepSeek", diff --git a/webview-ui/src/i18n/locales/hi/chat.json b/webview-ui/src/i18n/locales/hi/chat.json index e6feea9d1c..410cd829ad 100644 --- a/webview-ui/src/i18n/locales/hi/chat.json +++ b/webview-ui/src/i18n/locales/hi/chat.json @@ -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 एक स्थिर कमांड इंटरफ़ेस के साथ अपनी पहली आधिकारिक रिलीज़ तक पहुँचा।" diff --git a/webview-ui/src/i18n/locales/hi/settings.json b/webview-ui/src/i18n/locales/hi/settings.json index abd334bec0..a9e778e4a7 100644 --- a/webview-ui/src/i18n/locales/hi/settings.json +++ b/webview-ui/src/i18n/locales/hi/settings.json @@ -380,6 +380,9 @@ "vertex1MContextBetaDescription": "Claude Sonnet 4.x / Claude Opus 4.6 के लिए संदर्भ विंडो को 1 मिलियन टोकन तक बढ़ाता है", "basetenApiKey": "Baseten API कुंजी", "getBasetenApiKey": "Baseten API कुंजी प्राप्त करें", + "poeApiKey": "Poe API कुंजी", + "getPoeApiKey": "Poe API कुंजी प्राप्त करें", + "poeBaseUrl": "Poe बेस URL", "fireworksApiKey": "Fireworks API कुंजी", "getFireworksApiKey": "Fireworks API कुंजी प्राप्त करें", "deepSeekApiKey": "DeepSeek API कुंजी", diff --git a/webview-ui/src/i18n/locales/id/chat.json b/webview-ui/src/i18n/locales/id/chat.json index 7a32c8943c..c499024e4c 100644 --- a/webview-ui/src/i18n/locales/id/chat.json +++ b/webview-ui/src/i18n/locales/id/chat.json @@ -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." diff --git a/webview-ui/src/i18n/locales/id/settings.json b/webview-ui/src/i18n/locales/id/settings.json index 1ebcf2073b..36c0f6799f 100644 --- a/webview-ui/src/i18n/locales/id/settings.json +++ b/webview-ui/src/i18n/locales/id/settings.json @@ -380,6 +380,9 @@ "vertex1MContextBetaDescription": "Memperluas jendela konteks menjadi 1 juta token untuk Claude Sonnet 4.x / Claude Opus 4.6", "basetenApiKey": "Baseten API Key", "getBasetenApiKey": "Dapatkan Baseten API Key", + "poeApiKey": "Poe API Key", + "getPoeApiKey": "Dapatkan Poe API Key", + "poeBaseUrl": "Poe Base URL", "fireworksApiKey": "Fireworks API Key", "getFireworksApiKey": "Dapatkan Fireworks API Key", "deepSeekApiKey": "DeepSeek API Key", diff --git a/webview-ui/src/i18n/locales/it/chat.json b/webview-ui/src/i18n/locales/it/chat.json index c4c06de522..006ee09a89 100644 --- a/webview-ui/src/i18n/locales/it/chat.json +++ b/webview-ui/src/i18n/locales/it/chat.json @@ -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." diff --git a/webview-ui/src/i18n/locales/it/settings.json b/webview-ui/src/i18n/locales/it/settings.json index 4a0c716165..970752b16b 100644 --- a/webview-ui/src/i18n/locales/it/settings.json +++ b/webview-ui/src/i18n/locales/it/settings.json @@ -380,6 +380,9 @@ "vertex1MContextBetaDescription": "Estende la finestra di contesto a 1 milione di token per Claude Sonnet 4.x / Claude Opus 4.6", "basetenApiKey": "Chiave API Baseten", "getBasetenApiKey": "Ottieni chiave API Baseten", + "poeApiKey": "Chiave API Poe", + "getPoeApiKey": "Ottieni chiave API Poe", + "poeBaseUrl": "URL base Poe", "fireworksApiKey": "Chiave API Fireworks", "getFireworksApiKey": "Ottieni chiave API Fireworks", "deepSeekApiKey": "Chiave API DeepSeek", diff --git a/webview-ui/src/i18n/locales/ja/chat.json b/webview-ui/src/i18n/locales/ja/chat.json index 0d322ae373..d8bea631f3 100644 --- a/webview-ui/src/i18n/locales/ja/chat.json +++ b/webview-ui/src/i18n/locales/ja/chat.json @@ -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 が安定したコマンドインターフェースで初の正式リリースに到達しました。" diff --git a/webview-ui/src/i18n/locales/ja/settings.json b/webview-ui/src/i18n/locales/ja/settings.json index b0d921571a..1b2a125600 100644 --- a/webview-ui/src/i18n/locales/ja/settings.json +++ b/webview-ui/src/i18n/locales/ja/settings.json @@ -380,6 +380,9 @@ "vertex1MContextBetaDescription": "Claude Sonnet 4.x / Claude Opus 4.6のコンテキストウィンドウを100万トークンに拡張します", "basetenApiKey": "Baseten APIキー", "getBasetenApiKey": "Baseten APIキーを取得", + "poeApiKey": "Poe APIキー", + "getPoeApiKey": "Poe APIキーを取得", + "poeBaseUrl": "Poe ベースURL", "fireworksApiKey": "Fireworks APIキー", "getFireworksApiKey": "Fireworks APIキーを取得", "deepSeekApiKey": "DeepSeek APIキー", diff --git a/webview-ui/src/i18n/locales/ko/chat.json b/webview-ui/src/i18n/locales/ko/chat.json index cfa1a99e97..c16ac36402 100644 --- a/webview-ui/src/i18n/locales/ko/chat.json +++ b/webview-ui/src/i18n/locales/ko/chat.json @@ -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가 안정적인 명령어 인터페이스로 첫 공식 릴리스에 도달했습니다." diff --git a/webview-ui/src/i18n/locales/ko/settings.json b/webview-ui/src/i18n/locales/ko/settings.json index 88fc8e6d79..e35ab51e3b 100644 --- a/webview-ui/src/i18n/locales/ko/settings.json +++ b/webview-ui/src/i18n/locales/ko/settings.json @@ -380,6 +380,9 @@ "vertex1MContextBetaDescription": "Claude Sonnet 4.x / Claude Opus 4.6의 컨텍스트 창을 100만 토큰으로 확장", "basetenApiKey": "Baseten API 키", "getBasetenApiKey": "Baseten API 키 가져오기", + "poeApiKey": "Poe API 키", + "getPoeApiKey": "Poe API 키 가져오기", + "poeBaseUrl": "Poe 기본 URL", "fireworksApiKey": "Fireworks API 키", "getFireworksApiKey": "Fireworks API 키 받기", "deepSeekApiKey": "DeepSeek API 키", diff --git a/webview-ui/src/i18n/locales/nl/chat.json b/webview-ui/src/i18n/locales/nl/chat.json index 642cbee7ab..19a128f29d 100644 --- a/webview-ui/src/i18n/locales/nl/chat.json +++ b/webview-ui/src/i18n/locales/nl/chat.json @@ -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." diff --git a/webview-ui/src/i18n/locales/nl/settings.json b/webview-ui/src/i18n/locales/nl/settings.json index fcfad37d37..9ec6a91df9 100644 --- a/webview-ui/src/i18n/locales/nl/settings.json +++ b/webview-ui/src/i18n/locales/nl/settings.json @@ -380,6 +380,9 @@ "vertex1MContextBetaDescription": "Breidt het contextvenster uit tot 1 miljoen tokens voor Claude Sonnet 4.x / Claude Opus 4.6", "basetenApiKey": "Baseten API-sleutel", "getBasetenApiKey": "Baseten API-sleutel verkrijgen", + "poeApiKey": "Poe API-sleutel", + "getPoeApiKey": "Poe API-sleutel verkrijgen", + "poeBaseUrl": "Poe Basis-URL", "fireworksApiKey": "Fireworks API-sleutel", "getFireworksApiKey": "Fireworks API-sleutel ophalen", "deepSeekApiKey": "DeepSeek API-sleutel", diff --git a/webview-ui/src/i18n/locales/pl/chat.json b/webview-ui/src/i18n/locales/pl/chat.json index 25e3bb1e9a..efbb47dad6 100644 --- a/webview-ui/src/i18n/locales/pl/chat.json +++ b/webview-ui/src/i18n/locales/pl/chat.json @@ -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ń." diff --git a/webview-ui/src/i18n/locales/pl/settings.json b/webview-ui/src/i18n/locales/pl/settings.json index fa48bc6b21..45570384f1 100644 --- a/webview-ui/src/i18n/locales/pl/settings.json +++ b/webview-ui/src/i18n/locales/pl/settings.json @@ -380,6 +380,9 @@ "vertex1MContextBetaDescription": "Rozszerza okno kontekstowe do 1 miliona tokenów dla Claude Sonnet 4.x / Claude Opus 4.6", "basetenApiKey": "Klucz API Baseten", "getBasetenApiKey": "Uzyskaj klucz API Baseten", + "poeApiKey": "Klucz API Poe", + "getPoeApiKey": "Uzyskaj klucz API Poe", + "poeBaseUrl": "Bazowy URL Poe", "fireworksApiKey": "Klucz API Fireworks", "getFireworksApiKey": "Uzyskaj klucz API Fireworks", "deepSeekApiKey": "Klucz API DeepSeek", diff --git a/webview-ui/src/i18n/locales/pt-BR/chat.json b/webview-ui/src/i18n/locales/pt-BR/chat.json index d7b4e5a6e8..6db70b0342 100644 --- a/webview-ui/src/i18n/locales/pt-BR/chat.json +++ b/webview-ui/src/i18n/locales/pt-BR/chat.json @@ -348,8 +348,8 @@ }, "release": { "heading": "Novidades:", - "gpt54": "Suporte ao OpenAI GPT-5.4: adicionamos OpenAI GPT-5.4 e GPT-5.3 Chat Latest para você usar os modelos de chat mais novos da OpenAI no Roo Code.", - "slashSkills": "Skills como comandos slash: agora as skills podem ser expostas como comandos slash com execução de fallback para acionar workflows reutilizáveis mais rápido.", + "gpt54": "Provedor Poe: adicionamos o Poe como provedor de IA para você acessar os modelos do Poe diretamente no Roo Code.", + "slashSkills": "Melhorias em xAI e MiniMax: migramos o provedor xAI para a Responses API, adicionamos utilitários de transformação reutilizáveis, atualizamos os padrões do Grok-4.20 e corrigimos as listagens de modelos do MiniMax e o tratamento da janela de contexto para uma configuração mais confiável.", "geminiPro": "Suporte ao Gemini 3.1 Pro: Adicionado suporte ao modelo Gemini 3.1 Pro e definido como modelo Gemini padrão para melhor desempenho.", "cliNdjson": "Protocolo NDJSON do CLI: Novo protocolo NDJSON via stdin, subcomandos de listagem e comando de execução modularizado para fluxos de trabalho CLI mais flexíveis.", "cliRelease": "CLI v0.1.0: O CLI do Roo Code alcança sua primeira versão oficial com uma interface de comandos estável." diff --git a/webview-ui/src/i18n/locales/pt-BR/settings.json b/webview-ui/src/i18n/locales/pt-BR/settings.json index a8387e0512..cebf1c5cd2 100644 --- a/webview-ui/src/i18n/locales/pt-BR/settings.json +++ b/webview-ui/src/i18n/locales/pt-BR/settings.json @@ -380,6 +380,9 @@ "vertex1MContextBetaDescription": "Estende a janela de contexto para 1 milhão de tokens para o Claude Sonnet 4.x / Claude Opus 4.6", "basetenApiKey": "Chave de API Baseten", "getBasetenApiKey": "Obter chave de API Baseten", + "poeApiKey": "Chave de API Poe", + "getPoeApiKey": "Obter chave de API Poe", + "poeBaseUrl": "URL base do Poe", "fireworksApiKey": "Chave de API Fireworks", "getFireworksApiKey": "Obter chave de API Fireworks", "deepSeekApiKey": "Chave de API DeepSeek", diff --git a/webview-ui/src/i18n/locales/ru/chat.json b/webview-ui/src/i18n/locales/ru/chat.json index 90ca7be7e9..50864bff80 100644 --- a/webview-ui/src/i18n/locales/ru/chat.json +++ b/webview-ui/src/i18n/locales/ru/chat.json @@ -322,8 +322,8 @@ }, "release": { "heading": "Что нового:", - "gpt54": "Поддержка OpenAI GPT-5.4: мы добавили OpenAI GPT-5.4 и GPT-5.3 Chat Latest, чтобы ты мог использовать новейшие чат-модели OpenAI в Roo Code.", - "slashSkills": "Skills как slash-команды: теперь skills можно открывать как slash-команды с резервным выполнением, чтобы быстрее запускать рабочие процессы.", + "gpt54": "Провайдер Poe: мы добавили Poe как AI-провайдера, чтобы ты мог напрямую получать доступ к моделям Poe в Roo Code.", + "slashSkills": "Улучшения xAI и MiniMax: провайдер xAI мигрирован на Responses API, добавлены переиспользуемые утилиты трансформации, обновлены настройки Grok-4.20 по умолчанию, а также исправлены списки моделей MiniMax и обработка окна контекста для более надёжной настройки.", "geminiPro": "Поддержка Gemini 3.1 Pro: Добавлена поддержка модели Gemini 3.1 Pro и установлена как модель Gemini по умолчанию для лучшей производительности.", "cliNdjson": "Протокол NDJSON в CLI: Новый протокол NDJSON через stdin, подкоманды list и модульная команда run для более гибких CLI-процессов.", "cliRelease": "CLI v0.1.0: CLI Roo Code достиг своего первого официального релиза со стабильным интерфейсом командной строки." diff --git a/webview-ui/src/i18n/locales/ru/settings.json b/webview-ui/src/i18n/locales/ru/settings.json index fe24ebee29..698ae4c403 100644 --- a/webview-ui/src/i18n/locales/ru/settings.json +++ b/webview-ui/src/i18n/locales/ru/settings.json @@ -380,6 +380,9 @@ "vertex1MContextBetaDescription": "Расширяет контекстное окно до 1 миллиона токенов для Claude Sonnet 4.x / Claude Opus 4.6", "basetenApiKey": "Baseten API-ключ", "getBasetenApiKey": "Получить Baseten API-ключ", + "poeApiKey": "API-ключ Poe", + "getPoeApiKey": "Получить API-ключ Poe", + "poeBaseUrl": "Базовый URL Poe", "fireworksApiKey": "Fireworks API-ключ", "getFireworksApiKey": "Получить Fireworks API-ключ", "deepSeekApiKey": "DeepSeek API-ключ", diff --git a/webview-ui/src/i18n/locales/tr/chat.json b/webview-ui/src/i18n/locales/tr/chat.json index 10bf82ee45..545e61d50e 100644 --- a/webview-ui/src/i18n/locales/tr/chat.json +++ b/webview-ui/src/i18n/locales/tr/chat.json @@ -349,8 +349,8 @@ }, "release": { "heading": "Yenilikler:", - "gpt54": "OpenAI GPT-5.4 Desteği: Roo Code içinde en yeni OpenAI sohbet modellerini kullanabilmen için OpenAI GPT-5.4 ve GPT-5.3 Chat Latest eklendi.", - "slashSkills": "Slash komutu olarak skills: Skills artık daha hızlı iş akışları için fallback yürütmeyle slash komutları olarak kullanılabiliyor.", + "gpt54": "Poe Sağlayıcısı: Poe, Roo Code içinde Poe modellerine doğrudan erişebilmen için bir AI sağlayıcısı olarak eklendi.", + "slashSkills": "xAI ve MiniMax İyileştirmeleri: xAI sağlayıcısı Responses API'ye taşındı, yeniden kullanılabilir dönüştürme yardımcıları eklendi, Grok-4.20 varsayılanları güncellendi ve daha güvenilir bir kurulum için MiniMax model listeleriyle context window işleme düzeltildi.", "geminiPro": "Gemini 3.1 Pro Desteği: Gemini 3.1 Pro model desteği eklendi ve daha iyi performans için varsayılan Gemini modeli olarak ayarlandı.", "cliNdjson": "CLI NDJSON Protokolü: Yeni NDJSON stdin protokolü, list alt komutları ve daha esnek CLI iş akışları için modüler run komutu.", "cliRelease": "CLI v0.1.0: Roo Code CLI, kararlı bir komut arayüzüyle ilk resmi sürümüne ulaştı." diff --git a/webview-ui/src/i18n/locales/tr/settings.json b/webview-ui/src/i18n/locales/tr/settings.json index 7171718f1c..55ba2ece65 100644 --- a/webview-ui/src/i18n/locales/tr/settings.json +++ b/webview-ui/src/i18n/locales/tr/settings.json @@ -380,6 +380,9 @@ "vertex1MContextBetaDescription": "Claude Sonnet 4.x / Claude Opus 4.6 için bağlam penceresini 1 milyon token'a genişletir", "basetenApiKey": "Baseten API Anahtarı", "getBasetenApiKey": "Baseten API Anahtarı Al", + "poeApiKey": "Poe API Anahtarı", + "getPoeApiKey": "Poe API Anahtarı Al", + "poeBaseUrl": "Poe Temel URL", "fireworksApiKey": "Fireworks API Anahtarı", "getFireworksApiKey": "Fireworks API Anahtarı Al", "deepSeekApiKey": "DeepSeek API Anahtarı", diff --git a/webview-ui/src/i18n/locales/vi/chat.json b/webview-ui/src/i18n/locales/vi/chat.json index 7d9383888f..e36c58b54a 100644 --- a/webview-ui/src/i18n/locales/vi/chat.json +++ b/webview-ui/src/i18n/locales/vi/chat.json @@ -349,8 +349,8 @@ }, "release": { "heading": "Tính năng mới:", - "gpt54": "Hỗ trợ OpenAI GPT-5.4: Đã thêm 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.", - "slashSkills": "Skills dưới dạng lệnh slash: Giờ đây skills 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 quy trình nhanh hơn.", + "gpt54": "Nhà cung cấp Poe: Đã 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.", + "slashSkills": "Cải tiến xAI và MiniMax: Đã chuyển nhà cung cấp xAI sang Responses API, thêm các tiện ích chuyển đổi có thể tái sử dụng, cập nhật các giá trị mặc định của Grok-4.20 và sửa danh sách model MiniMax cùng cách xử lý context window để thiết lập đáng tin cậy hơn.", "geminiPro": "Hỗ trợ Gemini 3.1 Pro: Đã thêm hỗ trợ model Gemini 3.1 Pro và đặt làm model Gemini mặc định để cải thiện hiệu suất.", "cliNdjson": "Giao thức NDJSON CLI: Giao thức NDJSON stdin mới, lệnh con list, và lệnh run được module hóa cho quy trình CLI linh hoạt hơn.", "cliRelease": "CLI v0.1.0: CLI Roo Code đạt bản phát hành chính thức đầu tiên với giao diện lệnh ổn định." diff --git a/webview-ui/src/i18n/locales/vi/settings.json b/webview-ui/src/i18n/locales/vi/settings.json index 95b4f2d686..9d77fbc570 100644 --- a/webview-ui/src/i18n/locales/vi/settings.json +++ b/webview-ui/src/i18n/locales/vi/settings.json @@ -380,6 +380,9 @@ "vertex1MContextBetaDescription": "Mở rộng cửa sổ ngữ cảnh lên 1 triệu token cho Claude Sonnet 4.x / Claude Opus 4.6", "basetenApiKey": "Khóa API Baseten", "getBasetenApiKey": "Lấy khóa API Baseten", + "poeApiKey": "Khóa API Poe", + "getPoeApiKey": "Lấy khóa API Poe", + "poeBaseUrl": "URL cơ sở Poe", "fireworksApiKey": "Khóa API Fireworks", "getFireworksApiKey": "Lấy khóa API Fireworks", "deepSeekApiKey": "Khóa API DeepSeek", diff --git a/webview-ui/src/i18n/locales/zh-CN/chat.json b/webview-ui/src/i18n/locales/zh-CN/chat.json index 259c98a795..3e6f223e71 100644 --- a/webview-ui/src/i18n/locales/zh-CN/chat.json +++ b/webview-ui/src/i18n/locales/zh-CN/chat.json @@ -349,8 +349,8 @@ }, "release": { "heading": "新增功能:", - "gpt54": "OpenAI GPT-5.4 支持:已添加 OpenAI GPT-5.4 和 GPT-5.3 Chat Latest,让你可以在 Roo Code 中使用 OpenAI 最新的聊天模型。", - "slashSkills": "Slash 命令技能:现在 skills 可以作为 slash 命令公开,并支持 fallback execution,让工作流触发更快。", + "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 子命令,以及模块化的 run 命令,提供更灵活的 CLI 工作流。", "cliRelease": "CLI v0.1.0:Roo Code CLI 以稳定的命令接口达成首个正式版本。" diff --git a/webview-ui/src/i18n/locales/zh-CN/settings.json b/webview-ui/src/i18n/locales/zh-CN/settings.json index eeba6bb079..fea5dcc684 100644 --- a/webview-ui/src/i18n/locales/zh-CN/settings.json +++ b/webview-ui/src/i18n/locales/zh-CN/settings.json @@ -380,6 +380,9 @@ "vertex1MContextBetaDescription": "为 Claude Sonnet 4.x / Claude Opus 4.6 将上下文窗口扩展至 100 万个 token", "basetenApiKey": "Baseten API 密钥", "getBasetenApiKey": "获取 Baseten API 密钥", + "poeApiKey": "Poe API 密钥", + "getPoeApiKey": "获取 Poe API 密钥", + "poeBaseUrl": "Poe 基础 URL", "fireworksApiKey": "Fireworks API 密钥", "getFireworksApiKey": "获取 Fireworks API 密钥", "deepSeekApiKey": "DeepSeek API 密钥", diff --git a/webview-ui/src/i18n/locales/zh-TW/chat.json b/webview-ui/src/i18n/locales/zh-TW/chat.json index 19de5a5fdd..4fe9c57843 100644 --- a/webview-ui/src/i18n/locales/zh-TW/chat.json +++ b/webview-ui/src/i18n/locales/zh-TW/chat.json @@ -370,8 +370,8 @@ }, "release": { "heading": "新增功能:", - "gpt54": "OpenAI GPT-5.4 支援:已新增 OpenAI GPT-5.4 和 GPT-5.3 Chat Latest,讓你可以在 Roo Code 中使用 OpenAI 最新的聊天模型。", - "slashSkills": "Slash 指令技能:現在 skills 可以作為 slash 指令公開,並支援 fallback execution,讓工作流程觸發更快。", + "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 子指令,以及模組化的 run 指令,提供更彈性的 CLI 工作流程。", "cliRelease": "CLI v0.1.0:Roo Code CLI 以穩定的指令介面達成首個正式版本。" diff --git a/webview-ui/src/i18n/locales/zh-TW/settings.json b/webview-ui/src/i18n/locales/zh-TW/settings.json index 9f4241c3dd..1a306043a3 100644 --- a/webview-ui/src/i18n/locales/zh-TW/settings.json +++ b/webview-ui/src/i18n/locales/zh-TW/settings.json @@ -390,6 +390,9 @@ "vertex1MContextBetaDescription": "為 Claude Sonnet 4.x / Claude Opus 4.6 將上下文視窗擴展至 100 萬個 token", "basetenApiKey": "Baseten API 金鑰", "getBasetenApiKey": "取得 Baseten API 金鑰", + "poeApiKey": "Poe API 金鑰", + "getPoeApiKey": "取得 Poe API 金鑰", + "poeBaseUrl": "Poe 基礎 URL", "fireworksApiKey": "Fireworks API 金鑰", "getFireworksApiKey": "取得 Fireworks API 金鑰", "deepSeekApiKey": "DeepSeek API 金鑰", diff --git a/webview-ui/src/utils/__tests__/validate.spec.ts b/webview-ui/src/utils/__tests__/validate.spec.ts index 9b0b7a66e0..40cd7fade8 100644 --- a/webview-ui/src/utils/__tests__/validate.spec.ts +++ b/webview-ui/src/utils/__tests__/validate.spec.ts @@ -45,6 +45,7 @@ describe("Model Validation Functions", () => { lmstudio: {}, "vercel-ai-gateway": {}, roo: {}, + poe: {}, } const allowAllOrganization: OrganizationAllowList = {