+
+---
+
+## 🤔 Почему 9Router?
+
+**Перестаньте тратить деньги и упираться в лимиты:**
+
+- ❌ Квота подписки сгорает каждый месяц, не будучи израсходованной
+- ❌ Ограничение скорости (rate limit) прерывает вас прямо во время работы
+- ❌ Дорогие API ($20-50/мес за каждого провайдера)
+- ❌ Приходится вручную переключаться между провайдерами
+
+**9Router решает это:**
+
+- ✅ **Максимум из подписки** — Отслеживает квоту, использует каждый бит до сброса
+- ✅ **Автоматическое резервирование** — Подписка → Дёшево → Бесплатно, нулевой простой
+- ✅ **Несколько аккаунтов** — Round-robin по аккаунтам каждого провайдера
+- ✅ **Универсальность** — Работает с Claude Code, Codex, Gemini CLI, Cursor, Cline, любым CLI-инструментом
+
+---
+
+## 🔄 Как это работает
+
+```
+┌─────────────┐
+│ Your CLI │ (Claude Code, Codex, Gemini CLI, OpenClaw, Cursor, Cline...)
+│ Tool │
+└──────┬──────┘
+ │ http://localhost:20128/v1
+ ↓
+┌────────────────────────────────────────┐
+│ 9Router (Smart Router) │
+│ • Format translation (OpenAI ↔ Claude) │
+│ • Quota tracking │
+│ • Auto token refresh │
+└──────┬──────────────────────────────────┘
+ │
+ ├─→ [Tier 1: SUBSCRIPTION] Claude Code, Codex, Gemini CLI
+ │ ↓ quota exhausted
+ ├─→ [Tier 2: CHEAP] GLM ($0.6/1M), MiniMax ($0.2/1M)
+ │ budget limit
+ └─→ [Tier 3: FREE] iFlow, Qwen, Kiro (unlimited)
+
+Result: Never stop coding, minimal cost
+```
+
+---
+
+## ⚡ Быстрый старт
+
+**1. Глобальная установка:**
+
+```bash
+npm install -g 9router
+9router
+```
+
+🎉 Панель управления откроется на `http://localhost:20128`
+
+**2. Подключите БЕСПЛАТНОГО провайдера (без подписки):**
+
+Панель управления → Providers → Подключить **Claude Code** или **Antigravity** → Вход через OAuth → Готово!
+
+**3. Используйте в вашем CLI-инструменте:**
+
+```
+Настройки Claude Code/Codex/Gemini CLI/OpenClaw/Cursor/Cline:
+ Endpoint: http://localhost:20128/v1
+ API Key: [скопируйте из панели управления]
+ Model: if/kimi-k2-thinking
+```
+
+**Готово!** Начинайте кодить с БЕСПЛАТНЫМИ AI-моделями.
+
+**Альтернатива: запуск из исходников (этот репозиторий):**
+
+Пакет этого репозитория приватный (`9router-app`), поэтому запуск из исходников/Docker — это ожидаемый путь локальной разработки.
+
+```bash
+cp .env.example .env
+npm install
+PORT=20128 NEXT_PUBLIC_BASE_URL=http://localhost:20128 npm run dev
+```
+
+Режим Production:
+
+```bash
+npm run build
+PORT=20128 HOSTNAME=0.0.0.0 NEXT_PUBLIC_BASE_URL=http://localhost:20128 npm run start
+```
+
+URL по умолчанию:
+- Панель управления: `http://localhost:20128/dashboard`
+- OpenAI-совместимый API: `http://localhost:20128/v1`
+
+---
+
+## 🎥 Видео-руководство
+
+
+
+### 📺 Полное руководство по настройке - 9Router + Claude Code БЕСПЛАТНО
+
+[](https://www.youtube.com/watch?v=raEyZPg5xE0)
+
+**🎬 Полное пошаговое руководство:**
+- ✅ Установка и настройка 9Router
+- ✅ Настройка Claude Sonnet 4.5 БЕСПЛАТНО
+- ✅ Интеграция с Claude Code
+- ✅ Тестирование кода вживую
+
+**⏱️ Длительность:** 20 минут | **👥 Автор:** Сообщество разработчиков
+
+[▶️ Смотреть на YouTube](https://www.youtube.com/watch?v=o3qYCyjrFYg)
+
+
+
+---
+
+## 🛠️ Поддерживаемые CLI-инструменты
+
+9Router бесшовно работает со всеми основными AI-инструментами для кодинга:
+
+
...и более 20 других провайдеров, включая Nebius, Chutes, Hyperbolic и пользовательские OpenAI/Anthropic-совместимые эндпоинты
+
+
+---
+
+## 💡 Ключевые возможности
+
+| Возможность | Что делает | Почему это важно |
+|---------|--------------|----------------|
+| 🎯 **Smart 3-Tier Fallback** | Авто-маршрутизация: Подписка → Дёшево → Бесплатно | Никогда не прекращайте кодить, нулевой простой |
+| 📊 **Отслеживание квоты в реальном времени** | Живой подсчёт токенов + обратный отсчёт до сброса | Максимум ценности из подписки |
+| 🔄 **Трансляция форматов** | OpenAI ↔ Claude ↔ Gemini бесшовно | Работает с любым CLI-инструментом |
+| 👥 **Поддержка нескольких аккаунтов** | Несколько аккаунтов на каждого провайдера | Балансировка нагрузки + резервирование |
+| 🔄 **Авто-обновление токена** | OAuth-токены обновляются автоматически | Не нужно входить вручную заново |
+| 🎨 **Пользовательские комбо** | Создавайте безграничные комбинации моделей | Настройте резервирование под себя |
+| 📝 **Логирование запросов** | Режим отладки с полным логом запросов/ответов | Лёгкая диагностика проблем |
+| 💾 **Облачная синхронизация** | Синхронизация конфигурации между устройствами | Одинаковые настройки везде |
+| 📊 **Аналитика использования** | Отслеживание токенов, затрат, трендов во времени | Оптимизация расходов |
+| 🌐 **Развёртывание где угодно** | Localhost, VPS, Docker, Cloudflare Workers | Гибкие варианты развёртывания |
+
+
+📖 Подробности о возможностях
+
+### 🎯 Smart 3-Tier Fallback
+
+Создавайте комбо с автоматическим резервированием:
+
+```
+Combo: "my-coding-stack"
+ 1. cc/claude-opus-4-6 (ваша подписка)
+ 2. glm/glm-4.7 (дешёвый бэкап, $0.6/1M)
+ 3. if/kimi-k2-thinking (бесплатное резервирование)
+
+→ Автопереключение при исчерпании квоты или ошибке
+```
+
+### 📊 Отслеживание квоты в реальном времени
+
+- Потребление токенов по каждому провайдеру
+- Обратный отсчёт до сброса (5 часов, ежедневно, еженедельно)
+- Оценка затрат для платных уровней
+- Ежемесячный отчёт о расходах
+
+### 🔄 Трансляция форматов
+
+Бесшовная трансляция между форматами:
+- **OpenAI** ↔ **Claude** ↔ **Gemini** ↔ **OpenAI Responses**
+- Ваш CLI-инструмент отправляет формат OpenAI → 9Router транслирует → Провайдер получает родной формат
+- Работает с любым инструментом, поддерживающим пользовательский эндпоинт OpenAI
+
+### 👥 Поддержка нескольких аккаунтов
+
+- Добавляйте несколько аккаунтов на каждого провайдера
+- Round-robin или маршрутизация по приоритету автоматически
+- Резервирование на следующий аккаунт при достижении квоты
+
+### 🔄 Авто-обновление токена
+
+- OAuth-токены автоматически обновляются до истечения срока
+- Не нужна повторная ручная аутентификация
+- Бесшовный опыт со всеми провайдерами
+
+### 🎨 Пользовательские комбо
+
+- Создавайте безграничные комбинации моделей
+- Сочетайте уровни подписки, дешёвые и бесплатные
+- Называйте комбо для удобного доступа
+- Делитесь комбо между устройствами через облачную синхронизацию
+
+### 📝 Логирование запросов
+
+- Включите режим отладки для просмотра полного лога запросов/ответов
+- Отслеживайте вызовы API, заголовки и payload
+- Диагностируйте проблемы интеграции
+- Экспортируйте логи для анализа
+
+### 💾 Облачная синхронизация
+
+- Синхронизация провайдеров, комбо и настроек между устройствами
+- Автоматическая фоновая синхронизация
+- Безопасное зашифрованное хранилище
+- Доступ к настройкам откуда угодно
+
+#### Заметки о облачном рантайме
+
+- Приоритет серверным облачным переменным в production-окружении:
+ - `BASE_URL` (внутренний callback URL, используемый планировщиком синхронизации)
+ - `CLOUD_URL` (база эндпоинта облачной синхронизации)
+- `NEXT_PUBLIC_BASE_URL` и `NEXT_PUBLIC_CLOUD_URL` по-прежнему поддерживаются для совместимости/UI, но серверный рантайм теперь приоритезирует `BASE_URL`/`CLOUD_URL`.
+- Запросы облачной синхронизации теперь используют тайм-аут + fail-fast поведение, чтобы избежать зависания UI при недоступности DNS/облачной сети.
+
+### 📊 Аналитика использования
+
+- Отслеживание использования токенов по провайдеру и модели
+- Оценка затрат и тренды расходов
+- Ежемесячные отчёты и инсайты
+- Оптимизация ваших AI-расходов
+
+> **💡 ВАЖНО - Понимание «Затрат» на панели управления:**
+>
+> «Затраты», показанные в Аналитике использования, предназначены **только для отслеживания и сравнения**.
+> Сам 9Router **никогда ничего не взимает** с вас. Вы платите напрямую провайдерам (если используете платные сервисы).
+>
+> **Пример:** Если на панели показано «общие затраты $290» при использовании моделей iFlow, это представляет
+> сумму, которую вы заплатили бы при прямом использовании платного API. Ваши фактические затраты = **$0** (iFlow бесплатен без ограничений).
+>
+> Считайте это «трекером экономии», показывающим, сколько вы экономите, используя бесплатные модели или
+> маршрутизацию через 9Router!
+
+### 🌐 Развёртывание где угодно
+
+- 💻 **Localhost** — По умолчанию, работает офлайн
+- ☁️ **VPS/Cloud** — Общий доступ между устройствами
+- 🐳 **Docker** — Развёртывание одной командой
+- 🚀 **Cloudflare Workers** — Глобальная edge-сеть
+
+
+
+---
+
+## 💰 Обзор цен
+
+| Уровень | Провайдер | Стоимость | Сброс квоты | Лучше всего для |
+|------|----------|------|-------------|----------|
+| **💳 ПОДПИСКА** | Claude Code (Pro) | $20/мес | 5ч + еженедельно | Уже подписаны |
+| | Codex (Plus/Pro) | $20-200/мес | 5ч + еженедельно | Пользователи OpenAI |
+| | Gemini CLI | **БЕСПЛАТНО** | 180K/мес + 1K/день | Для всех! |
+| | GitHub Copilot | $10-19/мес | Ежемесячно | Пользователи GitHub |
+| **💰 ДЁШЕВО** | GLM-4.7 | $0.6/1M | 10:00 ежедневно | Бюджетный бэкап |
+| | MiniMax M2.1 | $0.2/1M | Скользящие 5 часов | Самый дешёвый вариант |
+| | Kimi K2 | $9/мес фикс. | 10M токенов/мес | Предсказуемая стоимость |
+| **🆓 БЕСПЛАТНО** | iFlow | $0 | Без ограничений | 8 бесплатных моделей |
+| | Qwen | $0 | Без ограничений | 3 бесплатные модели |
+| | Kiro | $0 | Без ограничений | Claude бесплатно |
+
+**💡 Профи-совет:** Начните с комбо Gemini CLI (180K бесплатно/мес) + iFlow (без ограничений бесплатно) = $0 затрат!
+
+---
+
+### 📊 Понимание затрат и оплаты в 9Router
+
+**Реальность оплаты 9Router:**
+
+✅ **Софт 9Router = БЕСПЛАТНО навсегда** (открытый код, никогда не взимает плату)
+✅ **«Затраты» на панели = Только для отображения/отслеживания** (не реальный счёт)
+✅ **Вы платите напрямую провайдерам** (подписка или плата за API)
+✅ **БЕСПЛАТНЫЕ провайдеры остаются БЕСПЛАТНЫМИ** (iFlow, Kiro, Qwen = $0 без ограничений)
+❌ **9Router никогда не выставляет счёт** и не списывает с вашей карты
+
+**Как работает отображение затрат:**
+
+Панель показывает **оценочные затраты**, как если бы вы напрямую использовали платный API. Это **не оплата** — это инструмент сравнения, показывающий вашу экономию.
+
+**Пример сценария:**
+```
+Показано на панели:
+• Всего запросов: 1,662
+• Всего токенов: 47M
+• Отображаемые затраты: $290
+
+Реальная проверка:
+• Провайдер: iFlow (БЕСПЛАТНО без ограничений)
+• Фактическая оплата: $0.00
+• Значение $290: Сумма, которую вы СЭКОНОМИЛИ, используя бесплатные модели!
+```
+
+**Правила оплаты:**
+- **Провайдеры подписки** (Claude Code, Codex): Платите им напрямую через их сайт
+- **Дешёвые провайдеры** (GLM, MiniMax): Платите им напрямую, 9Router только маршрутизирует
+- **БЕСПЛАТНЫЕ провайдеры** (iFlow, Kiro, Qwen): Действительно бесплатны навсегда, без скрытых платежей
+- **9Router**: Никогда ничего не взимает, никогда
+
+---
+
+## 🎯 Сценарии использования
+
+### Сценарий 1: «У меня подписка Claude Pro»
+
+**Проблема:** Квота сгорает неиспользованной, rate limit при интенсивной работе
+
+**Решение:**
+```
+Combo: "maximize-claude"
+ 1. cc/claude-opus-4-6 (полное использование подписки)
+ 2. glm/glm-4.7 (дешёвый бэкап при исчерпании квоты)
+ 3. if/kimi-k2-thinking (бесплатное аварийное резервирование)
+
+Месячная стоимость: $20 (подписка) + ~$5 (бэкап) = $25 итого
+против $20 + упирание в лимит = разочарование
+```
+
+### Сценарий 2: «Хочу нулевые затраты»
+
+**Проблема:** Не могу позволить подписку, нужен надёжный AI-кодинг
+
+**Решение:**
+```
+Combo: "free-forever"
+ 1. gc/gemini-3-flash (180K бесплатно/мес)
+ 2. if/kimi-k2-thinking (без ограничений бесплатно)
+ 3. qw/qwen3-coder-plus (без ограничений бесплатно)
+
+Месячная стоимость: $0
+Качество: Production-ready модели
+```
+
+### Сценарий 3: «Нужно кодить 24/7, без перерывов»
+
+**Проблема:** Дедлайны, нельзя допустить простоя
+
+**Решение:**
+```
+Combo: "always-on"
+ 1. cc/claude-opus-4-6 (лучшее качество)
+ 2. cx/gpt-5.2-codex (вторая подписка)
+ 3. glm/glm-4.7 (дёшево, ежедневный сброс)
+ 4. minimax/MiniMax-M2.1 (самый дешёвый, сброс 5ч)
+ 5. if/kimi-k2-thinking (бесплатно без ограничений)
+
+Результат: 5 слоёв резервирования = нулевой простой
+Месячная стоимость: $20-200 (подписки) + $10-20 (бэкап)
+```
+
+### Сценарий 4: «Хочу БЕСПЛАТНЫЙ AI в OpenClaw»
+
+**Проблема:** Нужен AI-ассистент в мессенджерах (WhatsApp, Telegram, Slack...), полностью бесплатно
+
+**Решение:**
+```
+Combo: "openclaw-free"
+ 1. if/glm-4.7 (без ограничений бесплатно)
+ 2. if/minimax-m2.1 (без ограничений бесплатно)
+ 3. if/kimi-k2-thinking (без ограничений бесплатно)
+
+Месячная стоимость: $0
+Доступ через: WhatsApp, Telegram, Slack, Discord, iMessage, Signal...
+```
+
+---
+
+## ❓ Часто задаваемые вопросы
+
+
+📊 Почему моя панель показывает высокие затраты?
+
+Панель отслеживает ваше использование токенов и показывает **оценочные затраты**, как если бы вы напрямую использовали платный API. Это **не реальная оплата** — это справка, показывающая, сколько вы экономите, используя бесплатные модели или существующие подписки через 9Router.
+
+**Пример:**
+- **Панель показывает:** «Общие затраты $290»
+- **Реальность:** Вы используете iFlow (БЕСПЛАТНО без ограничений)
+- **Ваши фактические затраты:** **$0.00**
+- **Значение $290:** Сумма, которую вы **экономите**, используя бесплатные модели вместо платного API!
+
+Отображение затрат — это «трекер экономии», помогающий понять паттерны использования и возможности оптимизации.
+
+
+
+
+💳 Взимает ли с меня плату 9Router?
+
+**Нет.** 9Router — это бесплатное ПО с открытым кодом, работающее на вашем собственном компьютере. Оно никогда ничего с вас не взимает.
+
+**Вы платите только:**
+- ✅ **Провайдерам подписки** (Claude Code $20/мес, Codex $20-200/мес) → Платите им напрямую на их сайте
+- ✅ **Дешёвым провайдерам** (GLM, MiniMax) → Платите им напрямую, 9Router только маршрутизирует ваши запросы
+- ❌ **Самому 9Router** → **Никогда ничего не взимает, никогда**
+
+9Router — это локальный прокси/роутер. У него нет вашей кредитной карты, он не может выставлять счета и не имеет платёжной системы. Это полностью бесплатное ПО.
+
+
+
+
+🆓 Действительно ли БЕСПЛАТНЫЕ провайдеры безлимитны?
+
+**Да!** Провайдеры, отмеченные как БЕСПЛАТНЫЕ (iFlow, Kiro, Qwen), действительно безлимитны и **без скрытых платежей**.
+
+Это бесплатные сервисы, предоставляемые соответствующими компаниями:
+- **iFlow**: Бесплатный безлимитный доступ к 8+ моделям через OAuth
+- **Kiro**: Бесплатные безлимитные модели Claude через AWS Builder ID
+- **Qwen**: Бесплатный безлимитный доступ к моделям Qwen через аутентификацию устройства
+
+9Router только маршрутизирует ваши запросы к ним — никаких «ловушек» или будущих платежей. Это действительно бесплатные сервисы, а 9Router облегчает их использование с поддержкой резервирования.
+
+**Примечание:** Некоторые провайдеры подписки (Antigravity, GitHub Copilot) могут иметь бесплатные пробные периоды, которые позже становятся платными, но об этом чётко уведомляют сами провайдеры, а не 9Router.
+
+
+
+
+💰 Как минимизировать мои реальные AI-затраты?
+
+**Стратегия «Бесплатное в приоритете»:**
+
+1. **Начните со 100% бесплатного комбо:**
+ ```
+ 1. gc/gemini-3-flash (180K/мес бесплатно от Google)
+ 2. if/kimi-k2-thinking (без ограничений бесплатно от iFlow)
+ 3. qw/qwen3-coder-plus (без ограничений бесплатно от Qwen)
+ ```
+ **Стоимость: $0/мес**
+
+2. **Добавьте дешёвый бэкап** только при необходимости:
+ ```
+ 4. glm/glm-4.7 ($0.6/1M токенов)
+ ```
+ **Доп. стоимость:** Платите только за то, что фактически используете
+
+3. **Используйте провайдеров подписки в последнюю очередь:**
+ - Только если они у вас уже есть
+ - 9Router помогает максимизировать их ценность через отслеживание квоты
+
+**Результат:** Большинство пользователей могут работать за $0/мес, используя только бесплатные уровни!
+
+
+
+
+📈 Что если моё использование внезапно вырастет?
+
+Умный механизм резервирования 9Router предотвращает неожиданные расходы:
+
+**Сценарий:** Вы в спринте кодинга и превышаете квоты
+
+**Без 9Router:**
+- ❌ Упёрлись в rate limit → Работа остановилась → Разочарование
+- ❌ Или: Случайно накопили огромный счёт за API
+
+**С 9Router:**
+- ✅ Подписка упёрлась в лимит → Авторезервирование на дешёвый уровень
+- ✅ Дешёвый уровень становится дорогим → Авторезервирование на бесплатный уровень
+- ✅ Никогда не прекращаете кодить → Предсказуемая стоимость
+
+**Вы контролируете:** Установите лимиты расходов на каждого провайдера в панели, и 9Router будет их соблюдать.
+
+
+
+---
+
+## 📖 Руководство по настройке
+
+
+🔐 Провайдеры подписки (Максимум ценности)
+
+### Claude Code (Pro/Max)
+
+```bash
+Панель управления → Providers → Подключить Claude Code
+→ Вход через OAuth → Авто-обновление токена
+→ Отслеживание квоты 5 часов + еженедельно
+
+Модели:
+ cc/claude-opus-4-6
+ cc/claude-sonnet-4-5-20250929
+ cc/claude-haiku-4-5-20251001
+```
+
+**Профи-совет:** Используйте Opus для сложных задач, Sonnet для скорости. 9Router отслеживает квоту для каждой модели!
+
+### OpenAI Codex (Plus/Pro)
+
+```bash
+Панель управления → Providers → Подключить Codex
+→ Вход через OAuth (порт 1455)
+→ Сброс 5 часов + еженедельно
+
+Модели:
+ cx/gpt-5.2-codex
+ cx/gpt-5.1-codex-max
+```
+
+### Gemini CLI (БЕСПЛАТНО 180K/мес!)
+
+```bash
+Панель управления → Providers → Подключить Gemini CLI
+→ Google OAuth
+→ 180K запросов/мес + 1K/день
+
+Модели:
+ gc/gemini-3-flash-preview
+ gc/gemini-2.5-pro
+```
+
+**Лучшая ценность:** Огромный бесплатный уровень! Используйте его перед платными уровнями.
+
+### GitHub Copilot
+
+```bash
+Панель управления → Providers → Подключить GitHub
+→ OAuth через GitHub
+→ Ежемесячный сброс (1-го числа месяца)
+
+Модели:
+ gh/gpt-5
+ gh/claude-4.5-sonnet
+ gh/gemini-3-pro
+```
+
+
+
+
+💰 Дешёвые провайдеры (Бэкап)
+
+### GLM-4.7 (Ежедневный сброс, $0.6/1M)
+
+1. Регистрация: [Zhipu AI](https://open.bigmodel.cn/)
+2. Получите API key из Coding Plan
+3. Панель управления → Добавить API Key:
+ - Провайдер: `glm`
+ - API Key: `your-key`
+
+**Использование:** `glm/glm-4.7`
+
+**Профи-совет:** Coding Plan даёт втрое больше квоты за 1/7 стоимости! Сброс ежедневно в 10:00.
+
+### MiniMax M2.1 (Сброс 5ч, $0.20/1M)
+
+1. Регистрация: [MiniMax](https://www.minimax.io/)
+2. Получите API key
+3. Панель управления → Добавить API Key
+
+**Использование:** `minimax/MiniMax-M2.1`
+
+**Профи-совет:** Самый дешёвый вариант для длинного контекста (1M)!
+
+### Kimi K2 ($9/мес фиксированно)
+
+1. Регистрация: [Moonshot AI](https://platform.moonshot.ai/)
+2. Получите API key
+3. Панель управления → Добавить API Key
+
+**Использование:** `kimi/kimi-latest`
+
+**Профи-совет:** Фиксированные $9/мес за 10M токенов = реальная стоимость $0.90/1M!
+
+
+
+
+🆓 БЕСПЛАТНЫЕ провайдеры (Аварийное резервирование)
+
+### iFlow (8 БЕСПЛАТНЫХ моделей)
+
+```bash
+Панель управления → Подключить iFlow
+→ Вход через OAuth iFlow
+→ Безлимитное использование
+
+Модели:
+ if/kimi-k2-thinking
+ if/qwen3-coder-plus
+ if/glm-4.7
+ if/minimax-m2
+ if/deepseek-r1
+```
+
+### Qwen (3 БЕСПЛАТНЫЕ модели)
+
+```bash
+Панель управления → Подключить Qwen
+→ Авторизация по коду устройства
+→ Безлимитное использование
+
+Модели:
+ qw/qwen3-coder-plus
+ qw/qwen3-coder-flash
+```
+
+### Kiro (БЕСПЛАТНЫЙ Claude)
+
+```bash
+Панель управления → Подключить Kiro
+→ AWS Builder ID или Google/GitHub
+→ Безлимитное использование
+
+Модели:
+ kr/claude-sonnet-4.5
+ kr/claude-haiku-4.5
+```
+
+
+
+
+🎨 Создание комбо
+
+### Пример 1: Максимум из подписки → Дешёвый бэкап
+
+```
+Панель управления → Combos → Создать новое
+
+Имя: premium-coding
+Модели:
+ 1. cc/claude-opus-4-6 (Основная подписка)
+ 2. glm/glm-4.7 (Дешёвый бэкап, $0.6/1M)
+ 3. minimax/MiniMax-M2.1 (Самое дешёвое резервирование, $0.20/1M)
+
+Использование в CLI: premium-coding
+
+Пример месячной стоимости (100M токенов):
+ 80M через Claude (подписка): $0 дополнительно
+ 15M через GLM: $9
+ 5M через MiniMax: $1
+ Итого: $10 + ваша подписка
+```
+
+### Пример 2: Только бесплатно (Нулевая стоимость)
+
+```
+Имя: free-combo
+Модели:
+ 1. gc/gemini-3-flash-preview (180K бесплатно/мес)
+ 2. if/kimi-k2-thinking (без ограничений)
+ 3. qw/qwen3-coder-plus (без ограничений)
+
+Стоимость: $0 навсегда!
+```
+
+
+
+
+🔧 Интеграция CLI
+
+### Cursor IDE
+
+```
+Settings → Models → Advanced:
+ OpenAI API Base URL: http://localhost:20128/v1
+ OpenAI API Key: [из панели управления 9router]
+ Model: cc/claude-opus-4-6
+```
+
+Или используйте комбо: `premium-coding`
+
+### Claude Code
+
+Отредактируйте `~/.claude/config.json`:
+
+```json
+{
+ "anthropic_api_base": "http://localhost:20128/v1",
+ "anthropic_api_key": "your-9router-api-key"
+}
+```
+
+### Codex CLI
+
+```bash
+export OPENAI_BASE_URL="http://localhost:20128"
+export OPENAI_API_KEY="your-9router-api-key"
+
+codex "ваш промпт"
+```
+
+### OpenClaw
+
+**Вариант 1 — Панель управления (рекомендуется):**
+
+```
+Панель управления → CLI Tools → OpenClaw → Выбрать модель → Применить
+```
+
+**Вариант 2 — Вручную:** Отредактируйте `~/.openclaw/openclaw.json`:
+
+```json
+{
+ "agents": {
+ "defaults": {
+ "model": {
+ "primary": "9router/if/glm-4.7"
+ }
+ }
+ },
+ "models": {
+ "providers": {
+ "9router": {
+ "baseUrl": "http://127.0.0.1:20128/v1",
+ "apiKey": "sk_9router",
+ "api": "openai-completions",
+ "models": [
+ {
+ "id": "if/glm-4.7",
+ "name": "glm-4.7"
+ }
+ ]
+ }
+ }
+ }
+}
+```
+
+> **Примечание:** OpenClaw работает только с локальным 9Router. Используйте `127.0.0.1` вместо `localhost`, чтобы избежать проблем с разрешением имён.
+
+### Cline / Continue / RooCode
+
+```
+Provider: OpenAI Compatible
+Base URL: http://localhost:20128/v1
+API Key: [из панели управления]
+Model: cc/claude-opus-4-6
+```
+
+
+
+
+🚀 Развёртывание
+
+### Развёртывание на VPS
+
+```bash
+# Clone and install
+git clone https://github.com/decolua/9router.git
+cd 9router
+npm install
+npm run build
+
+# Configure
+export JWT="your-secure-secret-change-this"
+export INITIAL_PASSWORD="your-password"
+export DATA_DIR="/var/lib/9router"
+export PORT="20128"
+export HOSTNAME="0.0.0.0"
+export NODE_ENV="production"
+export NEXT_PUBLIC_BASE_URL="http://localhost:20128"
+export NEXT_PUBLIC_CLOUD_URL="https://9router.com"
+export API_KEY_SECRET="endpoint-proxy-api-key-secret"
+export MACHINE_ID_SALT="endpoint-proxy-salt"
+
+# Start
+npm run start
+
+# Or use PM2
+npm install -g pm2
+pm2 start --name 9router -- start
+pm2 save
+pm2 startup
+```
+
+### Docker
+
+```bash
+# Build image (from repository root)
+docker build -t 9router .
+
+# Run container (command used in current setup)
+docker run -d \
+ --name 9router \
+ -p 20128:20128 \
+ --env-file /root/dev/9router/.env \
+ -v 9router-data:/app/data \
+ -v 9router-usage:/root/.9router \
+ 9router
+```
+
+Портативная команда (если вы уже в корне репозитория):
+
+```bash
+docker run -d \
+ --name 9router \
+ -p 20128:20128 \
+ --env-file ./.env \
+ -v 9router-data:/app/data \
+ -v 9router-usage:/root/.9router \
+ 9router
+```
+
+Значения по умолчанию контейнера:
+- `PORT=20128`
+- `HOSTNAME=0.0.0.0`
+
+Полезные команды:
+
+```bash
+docker logs -f 9router
+docker restart 9router
+docker stop 9router && docker rm 9router
+```
+
+### Переменные окружения
+
+| Переменная | По умолчанию | Описание |
+|----------|---------|-------------|
+| `JWT_SECRET` | Автогенерация (`~/.9router/jwt-secret`) | Секрет подписи JWT для cookie аутентификации панели (задайте для общего доступа между инстансами) |
+| `INITIAL_PASSWORD` | `123456` | Пароль первого входа при отсутствии сохранённого хеша |
+| `DATA_DIR` | `~/.9router` | Расположение основной БД приложения (`db.json`) |
+| `PORT` | framework default | Порт сервиса (`20128` в примерах) |
+| `HOSTNAME` | framework default | Bind host (Docker по умолчанию `0.0.0.0`) |
+| `NODE_ENV` | runtime default | Установите `production` для развёртывания |
+| `BASE_URL` | `http://localhost:20128` | Внутренний серверный базовый URL для задач облачной синхронизации |
+| `CLOUD_URL` | `https://9router.com` | Серверный базовый URL эндпоинта облачной синхронизации |
+| `NEXT_PUBLIC_BASE_URL` | `http://localhost:3000` | Обратно совместимый/публичный базовый URL (приоритет `BASE_URL` для серверного рантайма) |
+| `NEXT_PUBLIC_CLOUD_URL` | `https://9router.com` | Обратно совместимый/публичный облачный URL (приоритет `CLOUD_URL` для серверного рантайма) |
+| `API_KEY_SECRET` | `endpoint-proxy-api-key-secret` | HMAC-секрет для генерируемых API-ключей |
+| `MACHINE_ID_SALT` | `endpoint-proxy-salt` | Соль для стабильного хеширования ID машины |
+| `ENABLE_REQUEST_LOGS` | `false` | Включить лог запросов/ответов в `logs/` |
+| `AUTH_COOKIE_SECURE` | `false` | Принудительный `Secure` cookie аутентификации (задайте `true` за HTTPS reverse proxy) |
+| `REQUIRE_API_KEY` | `false` | Требовать Bearer API key на маршрутах `/v1/*` (рекомендуется для развёртываний с выходом в интернет) |
+| `HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY`, `NO_PROXY` | empty | Опциональный исходящий прокси для вызовов к провайдерам |
+
+Примечания:
+- Прокси-переменные в нижнем регистре также поддерживаются: `http_proxy`, `https_proxy`, `all_proxy`, `no_proxy`.
+- `.env` не запекается в Docker-образ (`.dockerignore`); подавайте runtime-конфигурацию через `--env-file` или `-e`.
+- В Windows для разрешения путей локального хранилища может использоваться `APPDATA`.
+- `INSTANCE_NAME` встречается в старых docs/env-шаблонах, но сейчас в рантайме не используется.
+
+### Runtime-файлы и хранилище
+
+- Основное состояние приложения: `${DATA_DIR}/db.json` (провайдеры, комбо, alias, ключи, настройки), управляется `src/lib/localDb.js`.
+- История использования и логи: `~/.9router/usage.json` и `~/.9router/log.txt`, управляется `src/lib/usageDb.js`.
+- Опциональные логи запросов/транслятора: `/logs/...` при `ENABLE_REQUEST_LOGS=true`.
+- Хранилище использования следует логике пути `~/.9router` и независимо от `DATA_DIR`.
+
+
+
+---
+
+## 📊 Доступные модели
+
+
+Показать все доступные модели
+
+**Claude Code (`cc/`)** - Pro/Max:
+- `cc/claude-opus-4-6`
+- `cc/claude-sonnet-4-5-20250929`
+- `cc/claude-haiku-4-5-20251001`
+
+**Codex (`cx/`)** - Plus/Pro:
+- `cx/gpt-5.2-codex`
+- `cx/gpt-5.1-codex-max`
+
+**Gemini CLI (`gc/`)** - БЕСПЛАТНО:
+- `gc/gemini-3-flash-preview`
+- `gc/gemini-2.5-pro`
+
+**GitHub Copilot (`gh/`)**:
+- `gh/gpt-5`
+- `gh/claude-4.5-sonnet`
+
+**GLM (`glm/`)** - $0.6/1M:
+- `glm/glm-4.7`
+
+**MiniMax (`minimax/`)** - $0.2/1M:
+- `minimax/MiniMax-M2.1`
+
+**iFlow (`if/`)** - БЕСПЛАТНО:
+- `if/kimi-k2-thinking`
+- `if/qwen3-coder-plus`
+- `if/deepseek-r1`
+
+**Qwen (`qw/`)** - БЕСПЛАТНО:
+- `qw/qwen3-coder-plus`
+- `qw/qwen3-coder-flash`
+
+**Kiro (`kr/`)** - БЕСПЛАТНО:
+- `kr/claude-sonnet-4.5`
+- `kr/claude-haiku-4.5`
+
+
+
+---
+
+## 🐛 Устранение неполадок
+
+**"Language model did not provide messages"**
+- Исчерпана квота провайдера → Проверьте трекер квоты на панели
+- Решение: Используйте резервирование комбо или переключитесь на более дешёвый уровень
+
+**Ограничение скорости (Rate limiting)**
+- Исчерпана квота подписки → Резервирование на GLM/MiniMax
+- Добавьте комбо: `cc/claude-opus-4-6 → glm/glm-4.7 → if/kimi-k2-thinking`
+
+**OAuth-токен истёк**
+- Автообновление 9Router
+- Если проблема сохраняется: Панель управления → Провайдеры → Переподключить
+
+**Высокие затраты**
+- Проверьте статистику использования в панели
+- Переключите основную модель на GLM/MiniMax
+- Используйте бесплатные уровни (Gemini CLI, iFlow) для некритичных задач
+
+**Панель открывается на неверном порту**
+- Установите `PORT=20128` и `NEXT_PUBLIC_BASE_URL=http://localhost:20128`
+
+**Ошибки облачной синхронизации**
+- Убедитесь, что `BASE_URL` указывает на ваш работающий инстанс (например, `http://localhost:20128`)
+- Убедитесь, что `CLOUD_URL` указывает на ожидаемый облачный эндпоинт (например, `https://9router.com`)
+- По возможности держите значения `NEXT_PUBLIC_*` согласованными с серверными значениями.
+
+**Облачный эндпоинт `stream=false` возвращает 500 (`Unexpected token 'd'...`)**
+- Симптом обычно появляется на публичном облачном эндпоинте (`https://9router.com/v1`) для непотоковых (non-streaming) вызовов.
+- Корневая причина: upstream возвращает SSE-payload (`data: ...`), тогда как клиент ожидает JSON.
+- Обходное решение: используйте `stream=true` для прямых вызовов в облако.
+- Локальный рантайм 9Router включает резервирование SSE→JSON для непотоковых вызовов, когда upstream возвращает `text/event-stream`.
+
+**Облако сообщает о подключении, но запрос всё равно падает с `Invalid API key`**
+- Создайте новый ключ в локальной панели (`/api/keys`) и запустите облачную синхронизацию (`Enable Cloud`, затем `Sync Now`).
+- Старые/несинхронизированные ключи могут возвращать `401` в облаке, даже если локальный эндпоинт работает.
+
+**Первый вход не работает**
+- Проверьте `INITIAL_PASSWORD` в `.env`
+- Если не задан, резервный пароль — `123456`
+
+**Нет логов запросов в `logs/`**
+- Установите `ENABLE_REQUEST_LOGS=true`
+
+---
+
+## 🛠️ Tech Stack
+
+- **Runtime**: Node.js 20+
+- **Framework**: Next.js 16
+- **UI**: React 19 + Tailwind 4
+- **Database**: LowDB (на основе JSON-файлов)
+- **Streaming**: Server-Sent Events (SSE)
+- **Auth**: OAuth 2.0 (PKCE) + JWT + API Keys
+
+---
+
+## 📝 Справочник по API
+
+### Chat Completions
+
+```bash
+POST http://localhost:20128/v1/chat/completions
+Authorization: Bearer your-api-key
+Content-Type: application/json
+
+{
+ "model": "cc/claude-opus-4-6",
+ "messages": [
+ {"role": "user", "content": "Напиши функцию для..."}
+ ],
+ "stream": true
+}
+```
+
+### Список моделей
+
+```bash
+GET http://localhost:20128/v1/models
+Authorization: Bearer your-api-key
+
+→ Возвращает все модели + комбо в формате OpenAI
+```
+
+### Совместимые эндпоинты
+
+- `POST /v1/chat/completions`
+- `POST /v1/messages`
+- `POST /v1/responses`
+- `GET /v1/models`
+- `POST /v1/messages/count_tokens`
+- `GET /v1beta/models`
+- `POST /v1beta/models/{...path}` (Gemini-style `generateContent`)
+- `POST /v1/api/chat` (путь конвертации в стиле Ollama)
+
+### Скрипты облачной аутентификации
+
+Добавлены тестовые скрипты в `tester/security/`:
+
+- `tester/security/test-docker-hardening.sh`
+ - Собирает Docker-образ и проверяет hardening-проверки (`/api/cloud/auth` auth guard, `REQUIRE_API_KEY`, безопасное поведение cookie аутентификации).
+- `tester/security/test-cloud-openai-compatible.sh`
+ - Отправляет OpenAI-совместимый запрос напрямую на облачный эндпоинт (`https://9router.com/v1/chat/completions`) с указанной моделью/ключом.
+- `tester/security/test-cloud-sync-and-call.sh`
+ - End-to-end процесс: создание локального ключа → включение/синхронизация облака → вызов облачного эндпоинта с повтором.
+ - Включает резервную проверку с `stream=true`, чтобы отличить ошибки аутентификации от проблем разбора потока.
+
+Заметки по безопасности для облачных тестовых скриптов:
+
+- Никогда не хардкодьте реальные API-ключи в скриптах/коммитах.
+- Передавайте ключи только через переменные окружения:
+ - `API_KEY`, `CLOUD_API_KEY` или `OPENAI_API_KEY` (поддерживается `test-cloud-openai-compatible.sh`)
+- Пример:
+
+```bash
+OPENAI_API_KEY="your-cloud-key" bash tester/security/test-cloud-openai-compatible.sh
+```
+
+Ожидаемое поведение по результатам недавней проверки:
+
+- Локально (`http://127.0.0.1:20128/v1/chat/completions`): работает с `stream=false` и `stream=true`.
+- Docker-рантайм (тот же API-путь, экспонируемый контейнером): hardening-проверки проходят, cloud auth guard работает, строгий режим API-ключа работает при включении.
+- Публичный облачный эндпоинт (`https://9router.com/v1/chat/completions`):
+ - `stream=true`: ожидается успех (возвращает SSE-чанки).
+ - `stream=false`: может падать с `500` + ошибкой разбора (`Unexpected token 'd'`), когда upstream возвращает SSE-контент для непотокового клиентского пути.
+
+### API управления и панели
+
+- Аутентификация/настройки: `/api/auth/login`, `/api/auth/logout`, `/api/settings`, `/api/settings/require-login`
+- Управление провайдерами: `/api/providers`, `/api/providers/[id]`, `/api/providers/[id]/test`, `/api/providers/[id]/models`, `/api/providers/validate`, `/api/provider-n*`
+- OAuth-потоки: `/api/oauth/[provider]/[action]` (+ специфичные для провайдеров импорты, такие как Cursor/Kiro)
+- Конфигурация маршрутизации: `/api/models/alias`, `/api/combos*`, `/api/keys*`, `/api/pricing`
+- Использование/логи: `/api/usage/history`, `/api/usage/logs`, `/api/usage/request-logs`, `/api/usage/[connectionId]`
+- Облачная синхронизация: `/api/sync/cloud`, `/api/sync/initialize`, `/api/cloud/*`
+- Помощники CLI: `/api/cli-tools/claude-settings`, `/api/cli-tools/codex-settings`, `/api/cli-tools/droid-settings`, `/api/cli-tools/openclaw-settings`
+
+### Поведение аутентификации
+
+- Маршруты панели (`/dashboard/*`) используют защиту cookie `auth_token`.
+- Вход использует сохранённый хеш пароля при наличии; иначе откатывается к `INITIAL_PASSWORD`.
+- `requireLogin` можно переключить через `/api/settings/require-login`.
+
+### Обработка запросов (высокоуровнево)
+
+1. Клиент отправляет запрос на `/v1/*`.
+2. Обработчик маршрута вызывает `handleChat` (`src/sse/handlers/chat.js`).
+3. Модель разрешается (прямой провайдер/модель или разрешение alias/combo).
+4. Учётные данные выбираются из локальной БД с фильтром доступности аккаунта.
+5. `handleChatCore` (`open-sse/handlers/chatCore.js`) определяет формат и транслирует запрос.
+6. Исполнитель провайдера отправляет upstream-запрос.
+7. Поток при необходимости транслируется обратно в клиентский формат.
+8. Использование/логи записываются (`src/lib/usageDb.js`).
+9. Резервирование применяется при ошибках провайдера/аккаунта/модели по правилам комбо.
+
+Полный справочник по архитектуре: [`docs/ARCHITECTURE.md`](../docs/ARCHITECTURE.md)
+
+---
+
+## 📧 Поддержка
+
+- **Сайт**: [9router.com](https://9router.com)
+- **GitHub**: [github.com/decolua/9router](https://github.com/decolua/9router)
+- **Issues**: [github.com/decolua/9router/issues](https://github.com/decolua/9router/issues)
+
+---
+
+## 👥 Контрибьюторы
+
+Спасибо всем, кто помогает делать 9Router лучше!
+
+[](https://github.com/decolua/9router/graphs/contributors)
+
+---
+
+## 📊 Star Chart
+
+[](https://starchart.cc/decolua/9router)
+
+### Как внести вклад
+
+1. Сделайте форк репозитория
+2. Создайте свою feature-ветку (`git checkout -b feature/amazing-feature`)
+3. Закоммитьте изменения (`git commit -m 'Add amazing feature'`)
+4. Запушьте в ветку (`git push origin feature/amazing-feature`)
+5. Откройте Pull Request
+
+См. [Pull Requests](https://github.com/decolua/9router/pulls) для подробных инструкций.
+
+---
+
+## 🔀 Форки
+
+**[OmniRoute](https://github.com/diegosouzapw/OmniRoute)** — Полнофункциональный TypeScript-форк 9Router. Добавляет 36+ провайдеров, авторезервирование на 4 уровнях, мультимодальный API (изображения, embedding, аудио, TTS), circuit breaker, семантическое кеширование, оценку LLM и доработанную панель. 368+ юнит-тестов. Доступен через npm.
+
+---
+
+## 🙏 Благодарности
+
+Особая благодарность **CLIProxyAPI** — оригинальной Go-реализации, вдохновившей этот JavaScript-порт.
+
+---
+
+## 📄 Лицензия
+
+Лицензия MIT — см. [LICENSE](../LICENSE) для деталей.
+
+---
+
+
+ Создано с ❤️ для разработчиков, которые кодят 24/7
+
diff --git a/next.config.mjs b/next.config.mjs
index 85c7a258f9a..3853b4b00bb 100644
--- a/next.config.mjs
+++ b/next.config.mjs
@@ -7,6 +7,7 @@ const projectRoot = dirname(fileURLToPath(import.meta.url));
const tracingRoot = process.env.NEXT_TRACING_ROOT_MODE === "workspace"
? join(projectRoot, "..")
: projectRoot;
+const proxyClientMaxBodySize = process.env.NINEROUTER_PROXY_CLIENT_MAX_BODY_SIZE || "128mb";
/** @type {import('next').NextConfig} */
const nextConfig = {
@@ -24,6 +25,10 @@ const nextConfig = {
unoptimized: true
},
env: {},
+ experimental: {
+ // #1529/#1572: LLM clients can send long context or base64 image payloads through /v1 rewrites.
+ proxyClientMaxBodySize,
+ },
webpack: (config, { isServer }) => {
// Ignore fs/path modules in browser bundle
if (!isServer) {
diff --git a/open-sse/config/providerModels.js b/open-sse/config/providerModels.js
index bc2e2a0e9da..8f51513224a 100644
--- a/open-sse/config/providerModels.js
+++ b/open-sse/config/providerModels.js
@@ -82,9 +82,9 @@ export const PROVIDER_MODELS = {
{ id: "iflow-rome-30ba3b", name: "iFlow ROME" },
],
ag: [ // Antigravity - special case: models call different backends
- { id: "gemini-3.5-flash-extra-low", name: "Gemini 3.5 Flash (Extra Low)" },
{ id: "gemini-3-flash-agent", name: "Gemini 3.5 Flash (High)" },
{ id: "gemini-3.5-flash-low", name: "Gemini 3.5 Flash (Medium)" },
+ { id: "gemini-3.5-flash-extra-low", name: "Gemini 3.5 Flash (Low)" },
{ id: "gemini-pro-agent", name: "Gemini 3.1 Pro (High)" },
{ id: "gemini-3.1-pro-low", name: "Gemini 3.1 Pro (Low)" },
{ id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6 (Thinking)" },
@@ -153,6 +153,7 @@ export const PROVIDER_MODELS = {
{ id: "lite", name: "Qoder Lite" },
// Frontier models — pin a specific backing model
{ id: "qmodel", name: "Qwen 3.6 Plus (Qoder)" },
+ { id: "qmodel_latest", name: "Qoder Qwen 3.7 Max" },
{ id: "dmodel", name: "DeepSeek V4 Pro (Qoder)" },
{ id: "dfmodel", name: "DeepSeek V4 Flash (Qoder)" },
{ id: "gm51model", name: "GLM 5.1 (Qoder)" },
@@ -337,6 +338,7 @@ export const PROVIDER_MODELS = {
{ id: "kimi-latest", name: "Kimi Latest" },
],
minimax: [
+ { id: "MiniMax-M3", name: "MiniMax M3", targetFormat: "claude" },
{ id: "MiniMax-M2.7", name: "MiniMax M2.7" },
{ id: "MiniMax-M2.5", name: "MiniMax M2.5" },
{ id: "MiniMax-M2.1", name: "MiniMax M2.1" },
@@ -363,6 +365,7 @@ export const PROVIDER_MODELS = {
{ id: "qwen3-vl-plus", name: "Qwen3 VL Plus" },
],
"minimax-cn": [
+ { id: "MiniMax-M3", name: "MiniMax M3", targetFormat: "claude" },
{ id: "MiniMax-M2.7", name: "MiniMax M2.7" },
{ id: "MiniMax-M2.5", name: "MiniMax M2.5" },
{ id: "MiniMax-M2.1", name: "MiniMax M2.1" },
@@ -547,6 +550,7 @@ export const PROVIDER_MODELS = {
],
"xiaomi-tokenplan": [
{ id: "mimo-v2.5-pro", name: "MiMo V2.5 Pro" },
+ { id: "mimo-v2.5-pro-claude", name: "MiMo V2.5 Pro (Claude Native)", targetFormat: "claude", upstreamModelId: "mimo-v2.5-pro" },
{ id: "mimo-v2.5", name: "MiMo V2.5" },
{ id: "mimo-v2-pro", name: "MiMo V2 Pro" },
{ id: "mimo-v2-omni", name: "MiMo V2 Omni" },
@@ -854,6 +858,13 @@ export function getModelTargetFormat(aliasOrId, modelId) {
return found?.targetFormat || null;
}
+export function getModelType(aliasOrId, modelId) {
+ const models = PROVIDER_MODELS[aliasOrId];
+ if (!models) return null;
+ const found = models.find(m => m.id === modelId);
+ return found?.type || null;
+}
+
export function getModelUpstreamId(aliasOrId, modelId) {
const models = PROVIDER_MODELS[aliasOrId];
const found = models?.find(m => m.id === modelId);
diff --git a/open-sse/config/providers.js b/open-sse/config/providers.js
index 8658aba9102..14e73f29d7a 100644
--- a/open-sse/config/providers.js
+++ b/open-sse/config/providers.js
@@ -71,8 +71,8 @@ export const PROVIDERS = {
baseUrl: "https://chatgpt.com/backend-api/codex/responses",
format: "openai-responses",
headers: {
- "originator": "codex-cli",
- "User-Agent": "codex-cli/1.0.18 (macOS; arm64)"
+ "originator": "codex_cli_rs",
+ "User-Agent": "codex_cli_rs/0.136.0"
},
clientId: "app_EMoamEEZ73f0CkXaXp7hrann",
tokenUrl: "https://auth.openai.com/oauth/token"
diff --git a/open-sse/config/runtimeConfig.js b/open-sse/config/runtimeConfig.js
index 1bdce2b1bc7..b89417a2ad4 100644
--- a/open-sse/config/runtimeConfig.js
+++ b/open-sse/config/runtimeConfig.js
@@ -31,11 +31,26 @@ export const MEMORY_CONFIG = {
proxyDispatchersMaxSize: 20,
};
-// Stream stall timeout: abort if no chunk received within this duration
-export const STREAM_STALL_TIMEOUT_MS = 30 * 1000;
+function envPositiveInt(name, fallback) {
+ const raw = process.env[name];
+ if (raw == null || raw === "") return fallback;
+ const parsed = Number.parseInt(raw, 10);
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
+}
+
+// Stream stall timeout: abort if no upstream bytes arrive within this duration.
+// Reasoning providers can legally stay silent for 60s+ while thinking, so keep
+// this well above common proxy read timeouts; downstream keepalives handle proxy
+// idleness separately.
+export const STREAM_STALL_TIMEOUT_MS = envPositiveInt("STREAM_STALL_TIMEOUT_MS", 5 * 60 * 1000);
+
+// Downstream keepalive cadence. SSE uses comment events; JSON uses leading
+// whitespace, which remains valid before the final JSON document.
+export const STREAM_HEARTBEAT_INTERVAL_MS = envPositiveInt("STREAM_HEARTBEAT_INTERVAL_MS", 10 * 1000);
+export const JSON_KEEPALIVE_INTERVAL_MS = envPositiveInt("JSON_KEEPALIVE_INTERVAL_MS", 10 * 1000);
// Fetch connect timeout: abort if upstream doesn't return response headers within this duration
-export const FETCH_CONNECT_TIMEOUT_MS = 20 * 1000;
+export const FETCH_CONNECT_TIMEOUT_MS = 60 * 1000;
// Default token limits
export const DEFAULT_MAX_TOKENS = 64000;
diff --git a/open-sse/executors/base.js b/open-sse/executors/base.js
index adb942b1007..aaf6ede88ed 100644
--- a/open-sse/executors/base.js
+++ b/open-sse/executors/base.js
@@ -1,4 +1,5 @@
import { HTTP_STATUS, RETRY_CONFIG, DEFAULT_RETRY_CONFIG, resolveRetryEntry, FETCH_CONNECT_TIMEOUT_MS } from "../config/runtimeConfig.js";
+import { shouldRefreshCredentials } from "../services/oauthCredentialManager.js";
import { proxyAwareFetch } from "../utils/proxyFetch.js";
import { dbg } from "../utils/debugLog.js";
@@ -87,9 +88,7 @@ export class BaseExecutor {
}
needsRefresh(credentials) {
- if (!credentials.expiresAt) return false;
- const expiresAtMs = new Date(credentials.expiresAt).getTime();
- return expiresAtMs - Date.now() < 5 * 60 * 1000;
+ return shouldRefreshCredentials(this.provider, credentials);
}
parseError(response, bodyText) {
@@ -122,15 +121,16 @@ export class BaseExecutor {
if (!retryAttemptsByUrl[urlIndex]) retryAttemptsByUrl[urlIndex] = 0;
- // Abort if upstream doesn't return response headers within FETCH_CONNECT_TIMEOUT_MS
+ // Abort if upstream doesn't return response headers within connection timeout
const connectCtrl = new AbortController();
- const connectTimer = setTimeout(() => connectCtrl.abort(new Error("fetch connect timeout")), FETCH_CONNECT_TIMEOUT_MS);
+ const timeoutMs = this.config?.timeoutMs || FETCH_CONNECT_TIMEOUT_MS;
+ const connectTimer = setTimeout(() => connectCtrl.abort(new Error("fetch connect timeout")), timeoutMs);
const mergedSignal = signal ? AbortSignal.any([signal, connectCtrl.signal]) : connectCtrl.signal;
try {
const bodyStr = JSON.stringify(transformedBody);
const fetchT0 = Date.now();
- dbg("FETCH", `${this.provider.toUpperCase()} → ${url} | body=${bodyStr.length}B | connectTimeout=${FETCH_CONNECT_TIMEOUT_MS}ms`);
+ dbg("FETCH", `${this.provider.toUpperCase()} → ${url} | body=${bodyStr.length}B | connectTimeout=${timeoutMs}ms`);
const response = await proxyAwareFetch(url, {
method: "POST",
headers,
diff --git a/open-sse/executors/codex.js b/open-sse/executors/codex.js
index db3b634f5ad..b2916987162 100644
--- a/open-sse/executors/codex.js
+++ b/open-sse/executors/codex.js
@@ -2,6 +2,10 @@ import { createHash } from "crypto";
import { BaseExecutor } from "./base.js";
import { CODEX_DEFAULT_INSTRUCTIONS } from "../config/codexInstructions.js";
import { PROVIDERS } from "../config/providers.js";
+import {
+ refreshProviderCredentials,
+ shouldRefreshCredentials,
+} from "../services/oauthCredentialManager.js";
import { normalizeResponsesInput } from "../translator/helpers/responsesApiHelper.js";
import { fetchImageAsBase64 } from "../translator/helpers/imageHelper.js";
import { getModelUpstreamId } from "../config/providerModels.js";
@@ -212,6 +216,15 @@ export class CodexExecutor extends BaseExecutor {
return this._isCompact ? `${base}/compact` : base;
}
+ async refreshCredentials(credentials, log) {
+ if (!credentials?.refreshToken) return null;
+ return refreshProviderCredentials("codex", credentials, log);
+ }
+
+ needsRefresh(credentials) {
+ return shouldRefreshCredentials("codex", credentials);
+ }
+
/**
* Prefetch remote image URLs and inline them as base64 data URIs.
* Runs before execute() because Codex backend cannot fetch remote images.
diff --git a/open-sse/executors/default.js b/open-sse/executors/default.js
index afb77efc8f4..885e3858302 100644
--- a/open-sse/executors/default.js
+++ b/open-sse/executors/default.js
@@ -1,5 +1,5 @@
import { BaseExecutor } from "./base.js";
-import { PROVIDERS, resolveXiaomiTokenplanBaseUrl } from "../config/providers.js";
+import { PROVIDERS } from "../config/providers.js";
import { OAUTH_ENDPOINTS, buildKimiHeaders } from "../config/appConstants.js";
import { buildClineHeaders } from "../../src/shared/utils/clineAuth.js";
import { getCachedClaudeHeaders } from "../utils/claudeHeaderCache.js";
@@ -67,9 +67,6 @@ export class DefaultExecutor extends BaseExecutor {
case "gemini":
return `${this.config.baseUrl}/${model}:${stream ? "streamGenerateContent?alt=sse" : "generateContent"}`;
default: {
- if (this.provider === "xiaomi-tokenplan") {
- return `${resolveXiaomiTokenplanBaseUrl(credentials)}/chat/completions`;
- }
const url = this.config.baseUrl;
if (url?.includes("{accountId}")) {
const accountId = credentials?.providerSpecificData?.accountId;
diff --git a/open-sse/executors/index.js b/open-sse/executors/index.js
index 78372cbf9ef..4a4439035de 100644
--- a/open-sse/executors/index.js
+++ b/open-sse/executors/index.js
@@ -15,6 +15,7 @@ import { GrokWebExecutor } from "./grok-web.js";
import { PerplexityWebExecutor } from "./perplexity-web.js";
import { OllamaLocalExecutor } from "./ollama-local.js";
import { CommandCodeExecutor } from "./commandcode.js";
+import { XiaomiTokenplanExecutor } from "./xiaomi-tokenplan.js";
import { DefaultExecutor } from "./default.js";
const executors = {
@@ -37,6 +38,7 @@ const executors = {
"perplexity-web": new PerplexityWebExecutor(),
"ollama-local": new OllamaLocalExecutor(),
commandcode: new CommandCodeExecutor(),
+ "xiaomi-tokenplan": new XiaomiTokenplanExecutor(),
};
const defaultCache = new Map();
@@ -70,3 +72,4 @@ export { GrokWebExecutor } from "./grok-web.js";
export { PerplexityWebExecutor } from "./perplexity-web.js";
export { OllamaLocalExecutor } from "./ollama-local.js";
export { CommandCodeExecutor } from "./commandcode.js";
+export { XiaomiTokenplanExecutor } from "./xiaomi-tokenplan.js";
diff --git a/open-sse/executors/qoder.js b/open-sse/executors/qoder.js
index 079f38be286..3be1f00f8a9 100644
--- a/open-sse/executors/qoder.js
+++ b/open-sse/executors/qoder.js
@@ -12,8 +12,8 @@
* - The request shape Qoder expects is non-trivial (chat_context with
* mirrored modelConfig, business block with stable IDs, system text
* hoisted out of the messages array). All ported from the reference.
- * - Model identifier is one of the canonical 11 keys (auto / ultimate /
- * performance / efficient / lite + 6 frontier "*model" ids); the
+ * - Model identifier is one of the canonical Qoder keys (auto / ultimate /
+ * performance / efficient / lite + frontier "*model" ids); the
* translator layer feeds us "qoder/" so we strip the prefix.
* - Per-model `model_config` is fetched live from /algo/api/v2/model/list
* and cached. Sending the wrong block silently downgrades to a
@@ -125,10 +125,9 @@ function truncate(s, n) {
*/
async function buildQoderRequestBody({ model, body, credentials, log, proxyOptions, signal }) {
const qoderKey = String(model || "").replace(/^qoder\//, "");
- if (!QODER_MODEL_MAP[qoderKey]) {
- throw new Error(`Unsupported qoder model: "${qoderKey}" (received "${model}")`);
- }
-
+
+ // Fetch model config from dynamic API instead of relying on static QODER_MODEL_MAP.
+ // This allows support for new Qoder models (e.g., qmodel_latest) without code changes.
let modelConfig = await getQoderModelConfig(credentials, qoderKey, { log, proxyOptions, signal });
if (!modelConfig) {
// Try a forced refresh once before giving up — the cache may simply
@@ -447,4 +446,5 @@ export default QoderExecutor;
export const __test__ = {
normalizeMessages,
wrapQoderSSE,
+ buildQoderRequestBody,
};
diff --git a/open-sse/executors/xiaomi-tokenplan.js b/open-sse/executors/xiaomi-tokenplan.js
new file mode 100644
index 00000000000..259da07f6c7
--- /dev/null
+++ b/open-sse/executors/xiaomi-tokenplan.js
@@ -0,0 +1,19 @@
+import { DefaultExecutor } from "./default.js";
+import { resolveXiaomiTokenplanBaseUrl } from "../config/providers.js";
+import { getModelTargetFormat } from "../config/providerModels.js";
+import { FORMATS } from "../translator/formats.js";
+
+export class XiaomiTokenplanExecutor extends DefaultExecutor {
+ constructor() {
+ super("xiaomi-tokenplan");
+ }
+
+ // Claude-native aliases route to the Anthropic-compatible messages endpoint
+ buildUrl(model, stream, urlIndex = 0, credentials = null) {
+ const baseUrl = resolveXiaomiTokenplanBaseUrl(credentials);
+ if (getModelTargetFormat(model, model) === FORMATS.CLAUDE) {
+ return `${baseUrl.replace(/\/v1\/?$/, "/anthropic/v1")}/messages`;
+ }
+ return `${baseUrl}/chat/completions`;
+ }
+}
diff --git a/open-sse/handlers/chatCore.js b/open-sse/handlers/chatCore.js
index 6120fec3cdd..9e0cafb6ca2 100644
--- a/open-sse/handlers/chatCore.js
+++ b/open-sse/handlers/chatCore.js
@@ -5,7 +5,7 @@ import { COLORS } from "../utils/stream.js";
import { createStreamController } from "../utils/streamHandler.js";
import { refreshWithRetry } from "../services/tokenRefresh.js";
import { createRequestLogger } from "../utils/requestLogger.js";
-import { getModelTargetFormat, getModelStrip, PROVIDER_ID_TO_ALIAS } from "../config/providerModels.js";
+import { getModelTargetFormat, getModelStrip, getModelUpstreamId, getModelType, PROVIDER_ID_TO_ALIAS } from "../config/providerModels.js";
import { createErrorResult, parseUpstreamError, formatProviderError } from "../utils/error.js";
import { HTTP_STATUS } from "../config/runtimeConfig.js";
import { handleBypassRequest } from "../utils/bypassHandler.js";
@@ -15,6 +15,7 @@ import { buildRequestDetail, extractRequestConfig } from "./chatCore/requestDeta
import { handleForcedSSEToJson } from "./chatCore/sseToJsonHandler.js";
import { handleNonStreamingResponse } from "./chatCore/nonStreamingHandler.js";
import { handleStreamingResponse, buildOnStreamComplete } from "./chatCore/streamingHandler.js";
+import { createJsonKeepaliveResponse } from "../utils/jsonKeepalive.js";
import { detectClientTool, isNativePassthrough } from "../utils/clientDetector.js";
import { dedupeTools } from "../utils/toolDeduper.js";
import { injectCaveman } from "../rtk/caveman.js";
@@ -45,6 +46,7 @@ export async function handleChatCore({ body, modelInfo, credentials, log, onCred
const modelTargetFormat = getModelTargetFormat(alias, model);
const targetFormat = modelTargetFormat || getTargetFormat(provider);
const stripList = getModelStrip(alias, model);
+ const upstreamModel = getModelUpstreamId(alias, model);
// Inject provider-level thinking config override (only if client hasn't set)
// on/off → extended type (body.thinking), none/low/medium/high → effort type (body.reasoning_effort)
@@ -93,16 +95,16 @@ export async function handleChatCore({ body, modelInfo, credentials, log, onCred
let toolNameMap;
if (passthrough) {
log?.debug?.("PASSTHROUGH", `${clientTool} → ${provider} | native lossless`);
- translatedBody = { ...body, model };
+ translatedBody = { ...body, model: upstreamModel };
} else {
- translatedBody = translateRequest(sourceFormat, targetFormat, model, body, stream, credentials, provider, reqLogger, stripList, connectionId, clientTool);
+ translatedBody = translateRequest(sourceFormat, targetFormat, upstreamModel, body, stream, credentials, provider, reqLogger, stripList, connectionId, clientTool);
if (!translatedBody) {
trackPendingRequest(model, provider, connectionId, false, true);
return createErrorResult(HTTP_STATUS.BAD_REQUEST, `Failed to translate request for ${sourceFormat} → ${targetFormat}`);
}
toolNameMap = translatedBody._toolNameMap;
delete translatedBody._toolNameMap;
- translatedBody.model = model;
+ translatedBody.model = upstreamModel;
}
// Dedupe duplicate built-in tools when equivalent MCP tools are present (Claude clients only).
@@ -124,6 +126,12 @@ export async function handleChatCore({ body, modelInfo, credentials, log, onCred
log?.debug?.("EFFORT", `stripped unsupported effort for ${model}`);
}
+ // TTS models don't support tool messages/function calling
+ if (getModelType(alias, model) === "tts" && translatedBody.messages) {
+ translatedBody.messages = translatedBody.messages.filter(msg => msg.role !== "tool");
+ delete translatedBody.tools;
+ }
+
// RTK: compress tool_result content
const rtkStats = compressMessages(translatedBody, rtkEnabled);
const rtkLine = formatRtkLog(rtkStats);
@@ -266,15 +274,23 @@ export async function handleChatCore({ body, modelInfo, credentials, log, onCred
// Provider forced streaming but client wants JSON
if (!clientRequestedStreaming && providerRequiresStreaming) {
+ const contentType = providerResponse.headers.get("content-type") || "";
+ const isForcedSSE = contentType.includes("text/event-stream") || (contentType === "" && provider === "codex");
+ if (isForcedSSE) {
+ const resultPromise = handleForcedSSEToJson({ ...sharedCtx, providerResponse, sourceFormat, trackDone, appendLog })
+ .finally(() => streamController.handleComplete());
+ return await createJsonKeepaliveResponse(resultPromise);
+ }
+
const result = await handleForcedSSEToJson({ ...sharedCtx, providerResponse, sourceFormat, trackDone, appendLog });
if (result) { streamController.handleComplete(); return result; }
}
// True non-streaming response
if (!stream) {
- const result = await handleNonStreamingResponse({ ...sharedCtx, providerResponse, sourceFormat, targetFormat, reqLogger, toolNameMap, trackDone, appendLog });
- streamController.handleComplete();
- return result;
+ const resultPromise = handleNonStreamingResponse({ ...sharedCtx, providerResponse, sourceFormat, targetFormat, reqLogger, toolNameMap, trackDone, appendLog })
+ .finally(() => streamController.handleComplete());
+ return await createJsonKeepaliveResponse(resultPromise);
}
// Streaming response — placeholder row and completion update must share one
diff --git a/open-sse/handlers/chatCore/nonStreamingHandler.js b/open-sse/handlers/chatCore/nonStreamingHandler.js
index b9a68a77c1e..b347218366f 100644
--- a/open-sse/handlers/chatCore/nonStreamingHandler.js
+++ b/open-sse/handlers/chatCore/nonStreamingHandler.js
@@ -72,12 +72,16 @@ export function translateNonStreamingResponse(responseBody, targetFormat, source
// Claude
if (targetFormat === FORMATS.CLAUDE) {
- if (!responseBody.content) return responseBody;
+ // Always translate a Claude-format body to OpenAI, even if `content` is
+ // missing/null (e.g. M3 with max_tokens:1 spends the budget on thinking
+ // and returns `content: null`). Returning the raw body would leave the
+ // OpenAI client without a `choices` array and surface as a UI test error.
+ if (responseBody.content && !Array.isArray(responseBody.content)) return responseBody;
let textContent = "", thinkingContent = "";
const toolCalls = [];
- for (const block of responseBody.content) {
+ for (const block of (responseBody.content || [])) {
if (block.type === "text") {
// Strip markdown code block markers (e.g. kimi wraps JSON in ```json...```)
const raw = block.text ?? "";
diff --git a/open-sse/handlers/chatCore/sseToJsonHandler.js b/open-sse/handlers/chatCore/sseToJsonHandler.js
index 98bc8de4789..4e4221a7e7c 100644
--- a/open-sse/handlers/chatCore/sseToJsonHandler.js
+++ b/open-sse/handlers/chatCore/sseToJsonHandler.js
@@ -167,7 +167,8 @@ export async function handleForcedSSEToJson({ providerResponse, sourceFormat, pr
} else {
const message = { role: "assistant", content: textContent || (hasToolCalls ? null : "") };
if (hasToolCalls) message.tool_calls = toolCalls;
- const finishReason = hasToolCalls ? "tool_calls" : (jsonResponse.status === "completed" ? "stop" : (jsonResponse.status || "stop"));
+ const responseDone = jsonResponse.status === "completed" || jsonResponse.status === "done";
+ const finishReason = hasToolCalls ? "tool_calls" : (responseDone ? "stop" : (jsonResponse.status || "stop"));
finalResp = {
id: jsonResponse.id || `chatcmpl-${Date.now()}`,
object: "chat.completion",
diff --git a/open-sse/handlers/chatCore/streamingHandler.js b/open-sse/handlers/chatCore/streamingHandler.js
index 2a7dfc18792..e86527c3b7a 100644
--- a/open-sse/handlers/chatCore/streamingHandler.js
+++ b/open-sse/handlers/chatCore/streamingHandler.js
@@ -2,6 +2,7 @@ import { FORMATS } from "../../translator/formats.js";
import { needsTranslation } from "../../translator/index.js";
import { createSSETransformStreamWithLogger, createPassthroughStreamWithLogger } from "../../utils/stream.js";
import { pipeWithDisconnect } from "../../utils/streamHandler.js";
+import { buildAbortedResponsesTerminalBytes } from "../../utils/responsesStreamHelpers.js";
import { buildRequestDetail, extractRequestConfig, saveUsageStats } from "./requestDetail.js";
import { saveRequestDetail } from "@/lib/usageDb.js";
@@ -43,7 +44,11 @@ export function handleStreamingResponse({ providerResponse, provider, model, sou
if (onRequestSuccess) onRequestSuccess();
const transformStream = buildTransformStream({ provider, sourceFormat, targetFormat, userAgent, reqLogger, toolNameMap, model, connectionId, body, onStreamComplete, apiKey });
- const transformedBody = pipeWithDisconnect(providerResponse, transformStream, streamController);
+
+ // Responses passthrough: synthesize response.failed + [DONE] if the stream aborts/stalls before a terminal event
+ const isResponsesPassthrough = sourceFormat === FORMATS.OPENAI_RESPONSES && targetFormat === FORMATS.OPENAI_RESPONSES;
+ const onAbortTerminal = isResponsesPassthrough ? buildAbortedResponsesTerminalBytes : null;
+ const transformedBody = pipeWithDisconnect(providerResponse, transformStream, streamController, onAbortTerminal);
saveRequestDetail(buildRequestDetail({
provider, model, connectionId, apiKey,
diff --git a/open-sse/handlers/imageProviders/codex.js b/open-sse/handlers/imageProviders/codex.js
index 2f8edc55189..591c7262134 100644
--- a/open-sse/handlers/imageProviders/codex.js
+++ b/open-sse/handlers/imageProviders/codex.js
@@ -3,8 +3,8 @@ import { randomUUID } from "node:crypto";
import { nowSec } from "./_base.js";
const CODEX_RESPONSES_URL = "https://chatgpt.com/backend-api/codex/responses";
-const CODEX_USER_AGENT = "codex-imagen/0.2.6";
-const CODEX_VERSION = "0.129.0";
+const CODEX_USER_AGENT = "codex_cli_rs/0.136.0";
+const CODEX_VERSION = "0.136.0";
const CODEX_ORIGINATOR = "codex_cli_rs";
const CODEX_MODEL_SUFFIX = "-image";
const CODEX_REF_DETAIL = "high";
diff --git a/open-sse/index.js b/open-sse/index.js
index dde009c5d31..4b2ac3a9ceb 100644
--- a/open-sse/index.js
+++ b/open-sse/index.js
@@ -59,6 +59,14 @@ export {
refreshTokenByProvider
} from "./services/tokenRefresh.js";
+export {
+ CODEX_MAX_REFRESH_AGE_MS,
+ shouldRefreshCredentials,
+ refreshProviderCredentials,
+ mergeRefreshedCredentials,
+ mergeProviderSpecificData,
+} from "./services/oauthCredentialManager.js";
+
// Handlers
export { handleChatCore, isTokenExpiringSoon } from "./handlers/chatCore.js";
export { createStreamController, pipeWithDisconnect, createDisconnectAwareStream } from "./utils/streamHandler.js";
diff --git a/open-sse/rtk/cavemanPrompts.js b/open-sse/rtk/cavemanPrompts.js
index c2de05fd8be..0b6f6f57d2f 100644
--- a/open-sse/rtk/cavemanPrompts.js
+++ b/open-sse/rtk/cavemanPrompts.js
@@ -5,31 +5,74 @@ export const CAVEMAN_LEVELS = {
LITE: "lite",
FULL: "full",
ULTRA: "ultra",
+ WENYAN_LITE: "wenyan-lite",
+ WENYAN: "wenyan",
+ WENYAN_ULTRA: "wenyan-ultra",
};
const SHARED_BOUNDARIES = "Code blocks, file paths, commands, errors, URLs: keep exact. Security warnings, irreversible action confirmations, multi-step ordered sequences: write normal. Resume terse style after.";
+const SHARED_EXAMPLES = "Not: \"Sure! I'd be happy to help you with that. The issue you're experiencing is likely caused by...\" Yes: \"Bug in auth middleware. Token expiry check use `<` not `<=`. Fix:\"";
+
+const SHARED_AUTO_CLARITY = "Auto-Clarity: drop caveman for security warnings, irreversible actions, multi-step sequences where fragment ambiguity risks misread, or when user repeats a question. Resume after the clear part.";
+
+const SHARED_PERSISTENCE = "ACTIVE EVERY RESPONSE. No revert after many turns. No filler drift. Still active if unsure.";
+
export const CAVEMAN_PROMPTS = {
[CAVEMAN_LEVELS.LITE]: [
"Respond tersely. Keep grammar and full sentences but drop filler, hedging and pleasantries (just/really/basically/sure/of course/I'd be happy to).",
"Pattern: state the thing, the action, the reason. Then next step.",
+ SHARED_EXAMPLES,
SHARED_BOUNDARIES,
- "Active every response until user asks for normal mode.",
+ SHARED_AUTO_CLARITY,
+ SHARED_PERSISTENCE,
].join(" "),
[CAVEMAN_LEVELS.FULL]: [
"Respond like terse caveman. All technical substance stay exact, only fluff die.",
"Drop: articles (a/an/the), filler (just/really/basically/actually/simply), pleasantries, hedging. Fragments OK. Short synonyms (big not extensive, fix not implement a solution for).",
"Pattern: [thing] [action] [reason]. [next step].",
+ SHARED_EXAMPLES,
SHARED_BOUNDARIES,
- "Active every response until user asks for normal mode.",
+ SHARED_AUTO_CLARITY,
+ SHARED_PERSISTENCE,
].join(" "),
[CAVEMAN_LEVELS.ULTRA]: [
"Respond ultra-terse. Maximum compression. Telegraphic.",
"Abbreviate (DB/auth/config/req/res/fn/impl), strip conjunctions, use arrows for causality (X → Y). One word when one word enough.",
"Pattern: [thing] → [result]. [fix].",
+ SHARED_EXAMPLES,
+ SHARED_BOUNDARIES,
+ SHARED_AUTO_CLARITY,
+ SHARED_PERSISTENCE,
+ ].join(" "),
+
+ [CAVEMAN_LEVELS.WENYAN_LITE]: [
+ "Respond semi-classical. Drop filler/hedging but keep grammar structure, classical register.",
+ "Use classical Chinese sentence patterns where natural. Keep English for technical terms.",
+ SHARED_EXAMPLES,
+ SHARED_BOUNDARIES,
+ SHARED_AUTO_CLARITY,
+ SHARED_PERSISTENCE,
+ ].join(" "),
+
+ [CAVEMAN_LEVELS.WENYAN]: [
+ "Respond classical Chinese (文言文). Maximum classical terseness. 80-90% character reduction.",
+ "Classical sentence patterns, verbs precede objects, subjects often omitted, classical particles (之/乃/為/其).",
+ "Keep English for code, commands, function names, API names, error strings.",
+ SHARED_EXAMPLES,
+ SHARED_BOUNDARIES,
+ SHARED_AUTO_CLARITY,
+ SHARED_PERSISTENCE,
+ ].join(" "),
+
+ [CAVEMAN_LEVELS.WENYAN_ULTRA]: [
+ "Respond extreme classical compression (文言文 ultra). Maximum compression, ultra terse.",
+ "Same classical rules as wenyan-full but even more compressed. One classical particle per clause.",
+ SHARED_EXAMPLES,
SHARED_BOUNDARIES,
- "Active every response until user asks for normal mode.",
+ SHARED_AUTO_CLARITY,
+ SHARED_PERSISTENCE,
].join(" "),
};
diff --git a/open-sse/services/model.js b/open-sse/services/model.js
index 2bd66e63511..d2dd3c8127c 100644
--- a/open-sse/services/model.js
+++ b/open-sse/services/model.js
@@ -29,6 +29,8 @@ const ALIAS_TO_PROVIDER_ID = {
kimi: "kimi",
minimax: "minimax",
"minimax-cn": "minimax-cn",
+ hf: "huggingface",
+ huggingface: "huggingface",
ds: "deepseek",
deepseek: "deepseek",
cmc: "commandcode",
diff --git a/open-sse/services/oauthCredentialManager.js b/open-sse/services/oauthCredentialManager.js
new file mode 100644
index 00000000000..466fcda95ea
--- /dev/null
+++ b/open-sse/services/oauthCredentialManager.js
@@ -0,0 +1,151 @@
+import {
+ getRefreshLeadMs,
+ isUnrecoverableRefreshError,
+ refreshTokenByProvider,
+} from "./tokenRefresh.js";
+
+export const CODEX_MAX_REFRESH_AGE_MS = 8 * 24 * 60 * 60 * 1000;
+
+const refreshLocks = new Map();
+
+function parseTimeMs(value) {
+ if (value === undefined || value === null || value === "") return null;
+ if (typeof value === "number") {
+ return value < 1e12 ? value * 1000 : value;
+ }
+
+ const parsed = new Date(value).getTime();
+ return Number.isFinite(parsed) ? parsed : null;
+}
+
+function toExpiresAt(expiresIn, nowMs = Date.now()) {
+ if (!expiresIn) return null;
+ return new Date(nowMs + expiresIn * 1000).toISOString();
+}
+
+export function getCredentialExpiryMs(credentials) {
+ return parseTimeMs(credentials?.expiresAt ?? credentials?.tokenExpiresAt);
+}
+
+export function getCredentialLastRefreshMs(credentials) {
+ return parseTimeMs(
+ credentials?.lastRefreshAt ??
+ credentials?.lastRefresh ??
+ credentials?.providerSpecificData?.lastRefreshAt
+ );
+}
+
+export function isCodexRefreshStale(credentials, nowMs = Date.now()) {
+ const lastRefreshMs = getCredentialLastRefreshMs(credentials);
+ return !lastRefreshMs || nowMs - lastRefreshMs >= CODEX_MAX_REFRESH_AGE_MS;
+}
+
+export function shouldRefreshCredentials(provider, credentials, nowMs = Date.now()) {
+ if (!credentials) return false;
+
+ const expiresAtMs = getCredentialExpiryMs(credentials);
+ if (expiresAtMs !== null && expiresAtMs - nowMs < getRefreshLeadMs(provider)) {
+ return true;
+ }
+
+ if (provider === "codex" && credentials.refreshToken && isCodexRefreshStale(credentials, nowMs)) {
+ return true;
+ }
+
+ return false;
+}
+
+export function mergeProviderSpecificData(existing, next) {
+ if (!next || typeof next !== "object") return existing;
+ return {
+ ...(existing || {}),
+ ...next,
+ };
+}
+
+export function mergeRefreshedCredentials(provider, currentCredentials, refreshedCredentials, nowMs = Date.now()) {
+ if (!refreshedCredentials) return null;
+ if (isUnrecoverableRefreshError(refreshedCredentials)) return refreshedCredentials;
+
+ const next = {};
+ const nowIso = new Date(nowMs).toISOString();
+
+ if (refreshedCredentials.accessToken) next.accessToken = refreshedCredentials.accessToken;
+ if (refreshedCredentials.apiKey) next.apiKey = refreshedCredentials.apiKey;
+ if (refreshedCredentials.token) next.token = refreshedCredentials.token;
+
+ const refreshToken = refreshedCredentials.refreshToken ?? currentCredentials?.refreshToken;
+ if (refreshToken) next.refreshToken = refreshToken;
+
+ const idToken = refreshedCredentials.idToken ?? currentCredentials?.idToken;
+ if (idToken) next.idToken = idToken;
+
+ if (refreshedCredentials.expiresIn) {
+ next.expiresIn = refreshedCredentials.expiresIn;
+ next.expiresAt = toExpiresAt(refreshedCredentials.expiresIn, nowMs);
+ } else if (refreshedCredentials.expiresAt) {
+ next.expiresAt = refreshedCredentials.expiresAt;
+ }
+
+ if (refreshedCredentials.projectId) next.projectId = refreshedCredentials.projectId;
+
+ if (refreshedCredentials.providerSpecificData) {
+ next.providerSpecificData = mergeProviderSpecificData(
+ currentCredentials?.providerSpecificData,
+ refreshedCredentials.providerSpecificData
+ );
+ }
+
+ if (refreshedCredentials.copilotToken) next.copilotToken = refreshedCredentials.copilotToken;
+ if (refreshedCredentials.copilotTokenExpiresAt) {
+ next.copilotTokenExpiresAt = refreshedCredentials.copilotTokenExpiresAt;
+ }
+
+ if (
+ provider === "codex" ||
+ next.accessToken ||
+ next.apiKey ||
+ next.token ||
+ next.refreshToken ||
+ next.copilotToken
+ ) {
+ next.lastRefreshAt = refreshedCredentials.lastRefreshAt || nowIso;
+ }
+
+ return next;
+}
+
+function getRefreshLockKey(provider, credentials) {
+ const stableId =
+ credentials?.connectionId ||
+ credentials?.id ||
+ credentials?.email ||
+ credentials?.name ||
+ credentials?.refreshToken?.slice?.(-16) ||
+ "default";
+ return `${provider}:${stableId}`;
+}
+
+export async function withCredentialRefreshLock(provider, credentials, refreshFn) {
+ const key = getRefreshLockKey(provider, credentials);
+ const existing = refreshLocks.get(key);
+ if (existing) return existing;
+
+ const pending = Promise.resolve()
+ .then(refreshFn)
+ .finally(() => {
+ refreshLocks.delete(key);
+ });
+
+ refreshLocks.set(key, pending);
+ return pending;
+}
+
+export async function refreshProviderCredentials(provider, credentials, log) {
+ if (!credentials) return null;
+
+ return withCredentialRefreshLock(provider, credentials, async () => {
+ const refreshed = await refreshTokenByProvider(provider, credentials, log);
+ return mergeRefreshedCredentials(provider, credentials, refreshed);
+ });
+}
diff --git a/open-sse/services/tokenRefresh.js b/open-sse/services/tokenRefresh.js
index 63b0fdf973e..0dce4121aa6 100644
--- a/open-sse/services/tokenRefresh.js
+++ b/open-sse/services/tokenRefresh.js
@@ -277,6 +277,27 @@ export async function refreshQwenToken(refreshToken, log) {
}, log);
}
+export function classifyOAuthRefreshError(errorText = "", status = 0) {
+ let parsed = null;
+ try {
+ parsed = errorText ? JSON.parse(errorText) : null;
+ } catch {
+ parsed = null;
+ }
+
+ const code = parsed?.error?.code || parsed?.error || parsed?.error_code || "";
+ const description = parsed?.error_description || parsed?.message || errorText || "";
+ const combined = `${code} ${description}`.toLowerCase();
+ const permanent = [
+ "refresh_token_expired",
+ "refresh_token_reused",
+ "refresh_token_invalidated",
+ "invalid_grant",
+ ].some((marker) => combined.includes(marker));
+
+ return { status, code, description, permanent };
+}
+
/**
* Specialized refresh for Codex (OpenAI) OAuth tokens.
* OpenAI uses rotating (one-time-use) refresh tokens.
@@ -286,68 +307,59 @@ export async function refreshQwenToken(refreshToken, log) {
export async function refreshCodexToken(refreshToken, log) {
if (!refreshToken) return null;
return dedupRefresh("codex", refreshToken, async () => {
- try {
- const response = await fetch(OAUTH_ENDPOINTS.openai.token, {
- method: "POST",
- headers: {
- "Content-Type": "application/x-www-form-urlencoded",
- Accept: "application/json",
- },
- body: new URLSearchParams({
- grant_type: "refresh_token",
- refresh_token: refreshToken,
- client_id: PROVIDERS.codex.clientId,
- scope: "openid profile email offline_access",
- }),
- });
-
- if (!response.ok) {
- const errorText = await response.text();
-
- // Detect unrecoverable errors (token reused/expired) — Auth0 revokes whole family on retry
- let errorCode = null;
try {
- const parsed = JSON.parse(errorText);
- errorCode = parsed?.error?.code || (typeof parsed?.error === "string" ? parsed.error : null);
- } catch {}
-
- if (
- errorCode === "refresh_token_reused" ||
- errorCode === "invalid_grant" ||
- errorCode === "token_expired" ||
- errorCode === "invalid_token"
- ) {
- log?.error?.("TOKEN_REFRESH", "Codex refresh token already used or invalid. Re-auth required.", {
- status: response.status,
- errorCode,
+ const response = await fetch(OAUTH_ENDPOINTS.openai.token, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ Accept: "application/json",
+ },
+ body: JSON.stringify({
+ client_id: PROVIDERS.codex.clientId,
+ grant_type: "refresh_token",
+ refresh_token: refreshToken,
+ }),
});
- return { error: "unrecoverable_refresh_error", code: errorCode };
- }
- log?.error?.("TOKEN_REFRESH", "Failed to refresh Codex token", {
- status: response.status,
- error: errorText,
- });
- return null;
- }
+ if (!response.ok) {
+ const errorText = await response.text();
+ const failure = classifyOAuthRefreshError(errorText, response.status);
+ if (failure.permanent) {
+ log?.error?.("TOKEN_REFRESH", "Codex refresh token already used or invalid. Re-auth required.", {
+ status: response.status,
+ code: failure.code,
+ });
+ return { error: "unrecoverable_refresh_error", code: failure.code };
+ }
- const tokens = await response.json();
+ log?.error?.("TOKEN_REFRESH", "Failed to refresh Codex token", {
+ status: response.status,
+ error: errorText,
+ code: failure.code,
+ permanent: failure.permanent,
+ });
+ return null;
+ }
- log?.info?.("TOKEN_REFRESH", "Successfully refreshed Codex token", {
- hasNewAccessToken: !!tokens.access_token,
- hasNewRefreshToken: !!tokens.refresh_token,
- expiresIn: tokens.expires_in,
- });
+ const tokens = await response.json();
- return {
- accessToken: tokens.access_token,
- refreshToken: tokens.refresh_token || refreshToken,
- expiresIn: tokens.expires_in,
- };
- } catch (error) {
- log?.error?.("TOKEN_REFRESH", `Network error refreshing Codex token: ${error.message}`);
- return null;
- }
+ log?.info?.("TOKEN_REFRESH", "Successfully refreshed Codex token", {
+ hasNewAccessToken: !!tokens.access_token,
+ hasNewRefreshToken: !!tokens.refresh_token,
+ hasIdToken: !!tokens.id_token,
+ expiresIn: tokens.expires_in,
+ });
+
+ return {
+ accessToken: tokens.access_token,
+ refreshToken: tokens.refresh_token || refreshToken,
+ idToken: tokens.id_token,
+ expiresIn: tokens.expires_in,
+ };
+ } catch (error) {
+ log?.error?.("TOKEN_REFRESH", `Network error refreshing Codex token: ${error.message}`);
+ return null;
+ }
}, log);
}
diff --git a/open-sse/services/usage.js b/open-sse/services/usage.js
index 277472b62c3..1441d2e3d5e 100644
--- a/open-sse/services/usage.js
+++ b/open-sse/services/usage.js
@@ -399,6 +399,7 @@ async function getAntigravityUsage(accessToken, providerSpecificData, proxyOptio
const importantModels = [
'gemini-3-flash-agent',
'gemini-3.5-flash-low',
+ 'gemini-3.5-flash-extra-low',
'gemini-pro-agent',
'gemini-3.1-pro-low',
'claude-sonnet-4-6',
@@ -995,6 +996,12 @@ function formatMiniMaxQuotaName(model) {
const rawName = getMiniMaxModelName(model);
if (!rawName) return "MiniMax";
+ // M3+ shared quota pool: MiniMax reports M-series as a single wildcard
+ // bucket ("MiniMax-M*"). Newer responses rename it to plain "general".
+ // Render both as a friendly series label rather than leaking the
+ // asterisk or the vague "general" word to the UI.
+ if (rawName === "MiniMax-M*" || rawName === "general") return "M-series";
+
return rawName
.replace(/[_-]+/g, " ")
.replace(/\s+/g, " ")
@@ -1005,6 +1012,15 @@ function formatMiniMaxQuotaName(model) {
.replace(/\bHd\b/g, "HD");
}
+function getMiniMaxProvidedPercent(model, snakeKey, camelKey) {
+ if (!model || typeof model !== "object") return null;
+ const raw = model[snakeKey] ?? model[camelKey];
+ if (raw === null || raw === undefined) return null;
+ const num = Number(raw);
+ if (!Number.isFinite(num)) return null;
+ return Math.max(0, Math.min(100, num));
+}
+
function getMiniMaxSessionTotal(model) {
return Math.max(0, Number(getMiniMaxField(model, "current_interval_total_count", "currentIntervalTotalCount")) || 0);
}
@@ -1014,7 +1030,12 @@ function getMiniMaxWeeklyTotal(model) {
}
function hasMiniMaxQuota(model) {
- return getMiniMaxSessionTotal(model) > 0 || getMiniMaxWeeklyTotal(model) > 0;
+ // Old format has real count totals; M3-era M-series buckets ship percent-only
+ // (count fields are 0) so accept those too.
+ if (getMiniMaxSessionTotal(model) > 0 || getMiniMaxWeeklyTotal(model) > 0) return true;
+ if (getMiniMaxProvidedPercent(model, "current_interval_remaining_percent", "currentIntervalRemainingPercent") !== null) return true;
+ if (getMiniMaxProvidedPercent(model, "current_weekly_remaining_percent", "currentWeeklyRemainingPercent") !== null) return true;
+ return false;
}
function getMiniMaxResetAt(model, capturedAtMs, remainsSnake, remainsCamel, endSnake, endCamel) {
@@ -1023,30 +1044,57 @@ function getMiniMaxResetAt(model, capturedAtMs, remainsSnake, remainsCamel, endS
return parseResetTime(getMiniMaxField(model, endSnake, endCamel));
}
-function buildMiniMaxQuota(total, count, resetAt, countMeansRemaining) {
+function buildMiniMaxQuota(total, count, resetAt, countMeansRemaining, providedPercent = null) {
const safeTotal = Math.max(0, total);
const used = countMeansRemaining ? Math.max(safeTotal - count, 0) : Math.min(Math.max(0, count), safeTotal);
const remaining = Math.max(safeTotal - used, 0);
+ // M-series buckets ship percent-only (count = 0). Prefer the upstream value
+ // when present, otherwise fall back to the computed percentage. When the
+ // quota is unbounded (no count) and no upstream percent is available, surface
+ // the percent anyway as long as it is defined.
+ const remainingPercentage = providedPercentage(providedPercent, remaining, safeTotal);
return {
used,
total: safeTotal,
remaining,
- remainingPercentage: safeTotal > 0 ? Math.max(0, Math.min(100, (remaining / safeTotal) * 100)) : 0,
+ remainingPercentage,
resetAt,
unlimited: false,
};
}
-function addMiniMaxQuota(quotas, key, model, getTotal, countSnake, countCamel, resetArgs, countMeansRemaining) {
+function providedPercentage(provided, remaining, total) {
+ if (provided !== null && provided !== undefined && Number.isFinite(provided)) {
+ return Math.max(0, Math.min(100, provided));
+ }
+ return total > 0 ? Math.max(0, Math.min(100, (remaining / total) * 100)) : 0;
+}
+
+function addMiniMaxQuota(quotas, key, model, getTotal, countSnake, countCamel, percentSnake, percentCamel, resetArgs, countMeansRemaining) {
const total = getTotal(model);
- if (total <= 0) return;
+ const providedPercent = getMiniMaxProvidedPercent(model, percentSnake, percentCamel);
+ if (total <= 0 && providedPercent === null) return;
const count = Math.max(0, Number(getMiniMaxField(model, countSnake, countCamel)) || 0);
+ let effectiveTotal = total;
+ let effectiveCount = count;
+ if (total <= 0) {
+ // M-series bucket: API only ships *_remaining_percent (count = 0). Normalize
+ // to total=100. The downstream buildMiniMaxQuota treats the count as
+ // "used" or "remaining" depending on countMeansRemaining, so the synthetic
+ // count has to match that semantic — otherwise the UI flips the percentage.
+ effectiveTotal = 100;
+ const pct = providedPercent;
+ effectiveCount = countMeansRemaining
+ ? Math.round(effectiveTotal * (pct / 100))
+ : Math.round(effectiveTotal * (1 - pct / 100));
+ }
quotas[key] = buildMiniMaxQuota(
- total,
- count,
+ effectiveTotal,
+ effectiveCount,
getMiniMaxResetAt(model, ...resetArgs),
- countMeansRemaining
+ countMeansRemaining,
+ providedPercent
);
}
@@ -1122,6 +1170,8 @@ async function getMiniMaxUsage(apiKey, provider, proxyOptions = null) {
getMiniMaxSessionTotal,
"current_interval_usage_count",
"currentIntervalUsageCount",
+ "current_interval_remaining_percent",
+ "currentIntervalRemainingPercent",
[capturedAtMs, "remains_time", "remainsTime", "end_time", "endTime"],
countMeansRemaining
);
@@ -1133,6 +1183,8 @@ async function getMiniMaxUsage(apiKey, provider, proxyOptions = null) {
getMiniMaxWeeklyTotal,
"current_weekly_usage_count",
"currentWeeklyUsageCount",
+ "current_weekly_remaining_percent",
+ "currentWeeklyRemainingPercent",
[capturedAtMs, "weekly_remains_time", "weeklyRemainsTime", "weekly_end_time", "weeklyEndTime"],
countMeansRemaining
);
diff --git a/open-sse/transformer/streamToJsonConverter.js b/open-sse/transformer/streamToJsonConverter.js
index 92cb723b87c..c08acb2553b 100644
--- a/open-sse/transformer/streamToJsonConverter.js
+++ b/open-sse/transformer/streamToJsonConverter.js
@@ -27,7 +27,7 @@ function processSSEMessage(msg, state) {
state.created = parsed.response?.created_at || state.created;
} else if (eventType === "response.output_item.done") {
state.items.set(parsed.output_index ?? 0, parsed.item);
- } else if (eventType === "response.completed") {
+ } else if (eventType === "response.completed" || eventType === "response.done") {
state.status = "completed";
if (parsed.response?.usage) {
state.usage.input_tokens = parsed.response.usage.input_tokens || 0;
diff --git a/open-sse/translator/request/openai-to-claude.js b/open-sse/translator/request/openai-to-claude.js
index f1a9baae655..9988c7691e5 100644
--- a/open-sse/translator/request/openai-to-claude.js
+++ b/open-sse/translator/request/openai-to-claude.js
@@ -291,15 +291,35 @@ function getContentBlocksFromMessage(msg, toolNameMap = new Map()) {
return blocks;
}
-// Convert OpenAI tool choice to Claude format
+// Convert OpenAI tool choice to Claude format.
+// Claude only accepts tool_choice.type of "auto" | "any" | "tool" | "none";
+// anything else (e.g. OpenAI's "function") triggers a 400, so we never pass an
+// unrecognized type through.
+const CLAUDE_TOOL_CHOICE_TYPES = new Set(["auto", "any", "tool", "none"]);
+
function convertOpenAIToolChoice(choice) {
if (!choice) return { type: "auto" };
- if (typeof choice === "object" && choice.type) return choice;
- if (choice === "auto" || choice === "none") return { type: "auto" };
- if (choice === "required") return { type: "any" };
- if (typeof choice === "object" && choice.function) {
- return { type: "tool", name: choice.function.name };
+
+ // OpenAI string forms: "auto" | "none" | "required"
+ if (typeof choice === "string") {
+ if (choice === "required") return { type: "any" };
+ return { type: "auto" }; // "auto", "none", or anything unexpected
}
+
+ if (typeof choice === "object") {
+ // OpenAI forced tool: { type: "function", function: { name } }.
+ // Checked before the native pass-through below, because the OpenAI shape
+ // also carries a `.type` ("function") that Claude rejects.
+ if (choice.function?.name) {
+ return { type: "tool", name: choice.function.name };
+ }
+ // Already Claude-native — only pass through types Claude actually accepts,
+ // so a malformed or unknown type can never leak into the upstream request.
+ if (CLAUDE_TOOL_CHOICE_TYPES.has(choice.type)) {
+ return choice;
+ }
+ }
+
return { type: "auto" };
}
diff --git a/open-sse/translator/request/openai-to-kiro.js b/open-sse/translator/request/openai-to-kiro.js
index 716ec861a54..d3a502f3e9c 100644
--- a/open-sse/translator/request/openai-to-kiro.js
+++ b/open-sse/translator/request/openai-to-kiro.js
@@ -12,22 +12,182 @@ import {
KIRO_AGENTIC_SYSTEM_PROMPT
} from "../../config/kiroConstants.js";
+/** Render a single tool call as a readable text line. */
+function toolCallToText(name, input) {
+ let argStr;
+ try {
+ argStr = typeof input === "string" ? input : JSON.stringify(input ?? {});
+ } catch {
+ argStr = "{}";
+ }
+ return `[Tool call: ${name || "unknown"}(${argStr})]`;
+}
+
+/** Render a tool result (string or content-block array) as a text line. */
+function toolResultToText(content) {
+ const text = Array.isArray(content)
+ ? content.map(c => (typeof c === "string" ? c : c.text || "")).join("\n")
+ : (typeof content === "string" ? content : "");
+ return `[Tool result: ${text}]`;
+}
+
+/**
+ * Flatten all tool calls/results in a conversation into plain text.
+ *
+ * Kiro's schema validator requires a non-empty
+ * currentMessage.userInputMessageContext.tools array whenever the history
+ * references any tool use; otherwise it returns "Improperly formed request"
+ * (HTTP 400). A client can hit this by omitting the `tools` array on a
+ * follow-up request — typically after client-side compaction (e.g. OpenCode).
+ *
+ * Rather than fabricate stub tool specs — which would advertise tool-calling
+ * capability the client never requested and may not handle, risking a phantom
+ * tool call on an otherwise plain turn — we collapse the tool interaction into
+ * text. The request stays honest, and since no structured tool content
+ * remains, the validator's "tools required" rule never fires.
+ *
+ * Only invoked when the client did NOT send tools; when tools are present the
+ * structured form is preserved.
+ */
+function flattenToolInteractions(messages) {
+ const out = [];
+
+ for (const msg of messages) {
+ // OpenAI tool-result message → user text line
+ if (msg.role === "tool") {
+ out.push({ role: "user", content: toolResultToText(msg.content) });
+ continue;
+ }
+
+ if (msg.role === "assistant") {
+ const parts = [];
+ if (Array.isArray(msg.content)) {
+ for (const c of msg.content) {
+ if (c.type === "tool_use") {
+ parts.push(toolCallToText(c.name, c.input));
+ } else if (c.type === "text" || c.text) {
+ parts.push(c.text || "");
+ }
+ }
+ } else if (typeof msg.content === "string") {
+ parts.push(msg.content);
+ }
+ for (const tc of msg.tool_calls || []) {
+ parts.push(toolCallToText(tc.function?.name, tc.function?.arguments));
+ }
+ out.push({ role: "assistant", content: parts.filter(Boolean).join("\n") });
+ continue;
+ }
+
+ // User messages: replace tool_result blocks with text, keep text + images.
+ if (msg.role === "user" && Array.isArray(msg.content)) {
+ const newContent = msg.content.map(c =>
+ c.type === "tool_result"
+ ? { type: "text", text: toolResultToText(c.content) }
+ : c
+ );
+ out.push({ ...msg, content: newContent });
+ continue;
+ }
+
+ out.push(msg);
+ }
+
+ return out;
+}
+
+/**
+ * Reconcile orphaned toolResults — those whose toolUseId has no matching
+ * toolUse in any assistant message. This happens when client-side compaction
+ * truncates the conversation and removes the assistant message containing the
+ * tool_use, but keeps the user message with the corresponding tool_result.
+ *
+ * A dangling structured reference makes Kiro return 400, so it must be removed.
+ * But the client deliberately kept the result content through compaction, so
+ * rather than discard it we fold it back into the user message as text — the
+ * same shape flattenToolInteractions() produces. The 400 trigger (the
+ * structured reference) is gone; the content survives.
+ *
+ * `messages` is every carrier that can hold toolResults — both history items
+ * and the popped-out currentMessage (orphans can land on either).
+ */
+function reconcileOrphanedToolResults(history, currentMessage) {
+ // Phase 1: collect all valid toolUseIds from assistant messages in history.
+ // (currentMessage is always a user turn, so it carries no toolUses.)
+ const validIds = new Set();
+ for (const h of history) {
+ const arm = h.assistantResponseMessage;
+ if (!arm) continue;
+ for (const tu of arm.toolUses || []) {
+ if (tu.toolUseId) validIds.add(tu.toolUseId);
+ }
+ }
+
+ // Phase 2: across history + currentMessage, keep results with a matching
+ // toolUse and salvage the rest as text.
+ const carriers = currentMessage ? [...history, currentMessage] : history;
+ for (const item of carriers) {
+ const uim = item.userInputMessage;
+ const ctx = uim?.userInputMessageContext;
+ if (!ctx?.toolResults?.length) continue;
+
+ const kept = [];
+ const salvaged = [];
+ for (const tr of ctx.toolResults) {
+ if (validIds.has(tr.toolUseId)) {
+ kept.push(tr);
+ } else {
+ salvaged.push(toolResultToText(tr.content));
+ }
+ }
+
+ if (salvaged.length === 0) continue; // no orphans — leave untouched
+
+ // Fold orphaned result content into the user text so it is not lost
+ const extra = salvaged.join("\n");
+ uim.content = uim.content ? `${uim.content}\n\n${extra}` : extra;
+
+ ctx.toolResults = kept;
+ if (kept.length === 0 && !ctx.tools?.length) {
+ delete uim.userInputMessageContext;
+ }
+ }
+}
+
+/**
+ * Safely parse JSON string, returning fallback on failure.
+ */
+function safeJSONParse(str, fallback) {
+ if (typeof str !== "string") return str ?? fallback;
+ try { return JSON.parse(str); } catch { return fallback; }
+}
+
/**
* Convert OpenAI messages to Kiro format
- * Rules: system/tool/user -> user role, merge consecutive same roles
+ * Rules: system/tool/user -> user role, merge consecutive same roles.
+ *
+ * Returns { history, currentMessage }.
*/
function convertMessages(messages, tools, model) {
let history = [];
let currentMessage = null;
-
+
+ const clientProvidedTools = tools && tools.length > 0;
+
+ // When the client did not send tools, flatten any tool calls/results in the
+ // history into plain text (see flattenToolInteractions). This keeps the
+ // request honest and sidesteps Kiro's "tools required" 400, since no
+ // structured tool content survives to trigger it.
+ if (!clientProvidedTools) {
+ messages = flattenToolInteractions(messages);
+ }
+
let pendingUserContent = [];
let pendingAssistantContent = [];
let pendingToolResults = [];
let pendingImages = [];
let currentRole = null;
-
- // Image support is pre-filtered by caps in translateRequest before reaching here
- const supportsImages = true;
+ let toolsInjectedToFirstUserMsg = false;
const flushPending = () => {
if (currentRole === "user") {
@@ -49,20 +209,23 @@ function convertMessages(messages, tools, model) {
toolResults: pendingToolResults
};
}
-
- // Add tools to first user message
- if (tools && tools.length > 0 && history.length === 0) {
+
+ // Add tools to the user message that has no preceding assistant messages,
+ // OR the first user message (whichever comes first after any opening
+ // assistant messages). We track whether any user message has already
+ // received tools via a flag on the history array.
+ if (clientProvidedTools && !toolsInjectedToFirstUserMsg) {
if (!userMsg.userInputMessage.userInputMessageContext) {
userMsg.userInputMessage.userInputMessageContext = {};
}
userMsg.userInputMessage.userInputMessageContext.tools = tools.map(t => {
const name = t.function?.name || t.name;
let description = t.function?.description || t.description || "";
-
+
if (!description.trim()) {
description = `Tool: ${name}`;
}
-
+
const schema = t.function?.parameters || t.parameters || t.input_schema || {};
// Normalize schema: Kiro requires required[] and proper type/properties
const normalizedSchema = Object.keys(schema).length === 0
@@ -77,8 +240,9 @@ function convertMessages(messages, tools, model) {
}
};
});
+ toolsInjectedToFirstUserMsg = true;
}
-
+
history.push(userMsg);
currentMessage = userMsg;
pendingUserContent = [];
@@ -99,18 +263,18 @@ function convertMessages(messages, tools, model) {
for (let i = 0; i < messages.length; i++) {
const msg = messages[i];
let role = msg.role;
-
+
// Normalize: system/tool -> user
if (role === "system" || role === "tool") {
role = "user";
}
-
+
// If role changes, flush pending
if (role !== currentRole && currentRole !== null) {
flushPending();
}
currentRole = role;
-
+
if (role === "user") {
// Extract content
let content = "";
@@ -121,7 +285,7 @@ function convertMessages(messages, tools, model) {
for (const c of msg.content) {
if (c.type === "text" || c.text) {
textParts.push(c.text || "");
- } else if (supportsImages && c.type === "image_url") {
+ } else if (c.type === "image_url") {
// OpenAI format: image_url.url with data URI
const url = c.image_url?.url || "";
const base64Match = url.match(/^data:([^;]+);base64,(.+)$/);
@@ -133,7 +297,7 @@ function convertMessages(messages, tools, model) {
// Kiro only supports base64 — fallback to URL text
textParts.push(`[Image: ${url}]`);
}
- } else if (supportsImages && c.type === "image") {
+ } else if (c.type === "image") {
// Claude format: source.type = "base64", source.media_type, source.data
if (c.source?.type === "base64" && c.source?.data) {
const mediaType = c.source.media_type || "image/png";
@@ -143,15 +307,15 @@ function convertMessages(messages, tools, model) {
}
}
content = textParts.join("\n");
-
+
// Check for tool_result blocks
const toolResultBlocks = msg.content.filter(c => c.type === "tool_result");
if (toolResultBlocks.length > 0) {
toolResultBlocks.forEach(block => {
- const text = Array.isArray(block.content)
+ const text = Array.isArray(block.content)
? block.content.map(c => c.text || "").join("\n")
: (typeof block.content === "string" ? block.content : "");
-
+
pendingToolResults.push({
toolUseId: block.tool_use_id,
status: "success",
@@ -160,7 +324,7 @@ function convertMessages(messages, tools, model) {
});
}
}
-
+
// Handle tool role (from normalized)
if (msg.role === "tool") {
const toolContent = typeof msg.content === "string" ? msg.content : "";
@@ -176,34 +340,30 @@ function convertMessages(messages, tools, model) {
// Extract text content and tool uses
let textContent = "";
let toolUses = [];
-
+
if (Array.isArray(msg.content)) {
const textBlocks = msg.content.filter(c => c.type === "text");
textContent = textBlocks.map(b => b.text).join("\n").trim();
-
+
const toolUseBlocks = msg.content.filter(c => c.type === "tool_use");
toolUses = toolUseBlocks;
} else if (typeof msg.content === "string") {
textContent = msg.content.trim();
}
-
+
if (msg.tool_calls && msg.tool_calls.length > 0) {
toolUses = msg.tool_calls;
}
-
+
if (textContent) {
pendingAssistantContent.push(textContent);
}
-
+
// Store tool uses in last assistant message
if (toolUses.length > 0) {
- if (pendingAssistantContent.length === 0) {
- // pendingAssistantContent.push("Call tools");
- }
-
// Flush to create assistant message with toolUses
flushPending();
-
+
const lastMsg = history[history.length - 1];
if (lastMsg?.assistantResponseMessage) {
lastMsg.assistantResponseMessage.toolUses = toolUses.map(tc => {
@@ -211,9 +371,7 @@ function convertMessages(messages, tools, model) {
return {
toolUseId: tc.id || uuidv4(),
name: tc.function.name,
- input: typeof tc.function.arguments === "string"
- ? JSON.parse(tc.function.arguments)
- : (tc.function.arguments || {})
+ input: safeJSONParse(tc.function.arguments, {})
};
} else {
return {
@@ -224,17 +382,17 @@ function convertMessages(messages, tools, model) {
}
});
}
-
+
currentRole = null;
}
}
}
-
+
// Flush remaining
if (currentRole !== null) {
flushPending();
}
-
+
// Pop last userInputMessage as currentMessage (search from end, skip trailing assistant messages)
for (let i = history.length - 1; i >= 0; i--) {
if (history[i].userInputMessage) {
@@ -261,6 +419,8 @@ function convertMessages(messages, tools, model) {
});
// Merge consecutive user messages (Kiro requires alternating user/assistant)
+ // When merging, also combine userInputMessageContext fields so toolResults
+ // and images from the second message are not silently dropped.
const mergedHistory = [];
for (let i = 0; i < history.length; i++) {
const current = history[i];
@@ -269,18 +429,63 @@ function convertMessages(messages, tools, model) {
mergedHistory[mergedHistory.length - 1].userInputMessage) {
const prev = mergedHistory[mergedHistory.length - 1];
prev.userInputMessage.content += "\n\n" + current.userInputMessage.content;
+ // Merge context: combine toolResults, images, etc.
+ const prevCtx = prev.userInputMessage.userInputMessageContext;
+ const curCtx = current.userInputMessage.userInputMessageContext;
+ if (curCtx) {
+ if (!prevCtx) {
+ prev.userInputMessage.userInputMessageContext = curCtx;
+ } else {
+ if (curCtx.toolResults?.length > 0) {
+ prevCtx.toolResults = [...(prevCtx.toolResults || []), ...curCtx.toolResults];
+ }
+ if (curCtx.tools?.length > 0) {
+ prevCtx.tools = [...(prevCtx.tools || []), ...curCtx.tools];
+ }
+ }
+ }
} else {
mergedHistory.push(current);
}
}
- // Inject tools into currentMessage AFTER cleanup
- if (firstHistoryTools && currentMessage?.userInputMessage &&
+ // When currentMessage is null (no user messages at all — edge case where
+ // input is only assistant messages), create a minimal currentMessage so
+ // tools and content can be injected.
+ if (!currentMessage) {
+ currentMessage = {
+ userInputMessage: {
+ content: "",
+ modelId: model,
+ }
+ };
+ }
+
+ // Reconcile orphaned toolResults across history AND currentMessage — when
+ // client-side compaction removes assistant messages containing tool_use but
+ // keeps the tool_result, the dangling reference triggers a Kiro 400. Fold the
+ // content back into the user text instead of discarding it. Run after
+ // currentMessage is finalized (an orphan can be merged into it) and before
+ // tool injection (which may re-add userInputMessageContext).
+ //
+ // Only needed on the tools-present path: when the client sent no tools,
+ // flattenToolInteractions already collapsed every toolResult to text, so
+ // there is nothing structured left to orphan.
+ if (clientProvidedTools) {
+ reconcileOrphanedToolResults(mergedHistory, currentMessage);
+ }
+
+ // Inject tools into currentMessage AFTER cleanup. Tools only exist here when
+ // the client explicitly sent them (otherwise flattenToolInteractions already
+ // collapsed all tool content to text upstream, so there is nothing to carry).
+ const resolvedTools = firstHistoryTools;
+
+ if (resolvedTools?.length > 0 &&
!currentMessage.userInputMessage.userInputMessageContext?.tools) {
if (!currentMessage.userInputMessage.userInputMessageContext) {
currentMessage.userInputMessage.userInputMessageContext = {};
}
- currentMessage.userInputMessage.userInputMessageContext.tools = firstHistoryTools;
+ currentMessage.userInputMessage.userInputMessageContext.tools = resolvedTools;
}
return { history: mergedHistory, currentMessage };
@@ -318,6 +523,7 @@ export function buildKiroPayload(model, body, stream, credentials) {
const profileArn = credentials?.providerSpecificData?.profileArn || "";
let finalContent = currentMessage?.userInputMessage?.content || "";
+
const timestamp = new Date().toISOString();
// Build the system-prompt prefix that goes ABOVE the user message body.
diff --git a/open-sse/translator/response/openai-responses.js b/open-sse/translator/response/openai-responses.js
index 0cf79cbd129..e500e4c7952 100644
--- a/open-sse/translator/response/openai-responses.js
+++ b/open-sse/translator/response/openai-responses.js
@@ -490,7 +490,7 @@ export function openaiResponsesToOpenAIResponse(chunk, state) {
}
// Response completed
- if (eventType === "response.completed") {
+ if (eventType === "response.completed" || eventType === "response.done") {
// Extract usage from response.completed event
const responseUsage = data.response?.usage;
if (responseUsage && typeof responseUsage === "object") {
diff --git a/open-sse/utils/claudeCloaking.js b/open-sse/utils/claudeCloaking.js
index 2c9d1f263fb..688573ab486 100644
--- a/open-sse/utils/claudeCloaking.js
+++ b/open-sse/utils/claudeCloaking.js
@@ -35,13 +35,16 @@ export function cloakClaudeTools(body) {
const tools = body.tools;
if (!tools || tools.length === 0) return { body, toolNameMap: null };
+ const suffix = (name) => `${name}${CLAUDE_TOOL_SUFFIX}`;
const toolNameMap = new Map();
+ const clientToolNames = new Set();
const clientDeclarations = [];
// All client tools get renamed with suffix
for (const tool of tools) {
- const suffixed = `${tool.name}${CLAUDE_TOOL_SUFFIX}`;
+ const suffixed = suffix(tool.name);
toolNameMap.set(suffixed, tool.name);
+ clientToolNames.add(tool.name);
clientDeclarations.push({ ...tool, name: suffixed });
}
@@ -51,17 +54,27 @@ export function cloakClaudeTools(body) {
// Rename tool_use in message history (all client tools get suffix)
const renamedMessages = body.messages?.map(msg => {
if (!Array.isArray(msg.content)) return msg;
- const renamedContent = msg.content.map(block => {
- if (block.type === "tool_use") {
- return { ...block, name: `${block.name}${CLAUDE_TOOL_SUFFIX}` };
- }
- return block;
- });
+ const renamedContent = msg.content.map(block =>
+ block.type === "tool_use" ? { ...block, name: suffix(block.name) } : block
+ );
return { ...msg, content: renamedContent };
});
+ const cloakedBody = { ...body, tools: allTools, messages: renamedMessages || body.messages };
+
+ // A forced tool_choice ({ type: "tool", name }) must point at the suffixed
+ // tool name, otherwise Claude rejects it: "Tool '' not found in provided tools".
+ // Only rewrite when the choice targets one of the client tools we actually
+ // renamed — never a decoy/built-in name (those are sent unsuffixed).
+ if (
+ body.tool_choice?.type === "tool" &&
+ clientToolNames.has(body.tool_choice.name)
+ ) {
+ cloakedBody.tool_choice = { ...body.tool_choice, name: suffix(body.tool_choice.name) };
+ }
+
return {
- body: { ...body, tools: allTools, messages: renamedMessages || body.messages },
+ body: cloakedBody,
toolNameMap: toolNameMap.size > 0 ? toolNameMap : null
};
}
diff --git a/open-sse/utils/jsonKeepalive.js b/open-sse/utils/jsonKeepalive.js
new file mode 100644
index 00000000000..58e76592f41
--- /dev/null
+++ b/open-sse/utils/jsonKeepalive.js
@@ -0,0 +1,99 @@
+import { JSON_KEEPALIVE_INTERVAL_MS } from "../config/runtimeConfig.js";
+
+const encoder = new TextEncoder();
+const KEEPALIVE_CHUNK = " \n";
+
+async function writeResponseBody(controller, response) {
+ if (!response?.body) {
+ controller.enqueue(encoder.encode("null"));
+ return;
+ }
+
+ const reader = response.body.getReader();
+ try {
+ while (true) {
+ const { value, done } = await reader.read();
+ if (done) break;
+ if (value) controller.enqueue(value);
+ }
+ } finally {
+ reader.releaseLock();
+ }
+}
+
+/**
+ * Return JSON response immediately and keep the downstream proxy alive while
+ * expensive non-stream conversion buffers upstream. Leading JSON whitespace is
+ * valid, so clients still parse the final document normally.
+ */
+export function createJsonKeepaliveResponse(resultPromise, { intervalMs = JSON_KEEPALIVE_INTERVAL_MS } = {}) {
+ if (intervalMs <= 0) return resultPromise;
+
+ const settled = resultPromise.then(
+ (result) => ({ settled: true, result }),
+ (error) => ({ settled: true, error })
+ );
+
+ return Promise.race([
+ settled,
+ new Promise((resolve) => setTimeout(() => resolve({ settled: false }), intervalMs))
+ ]).then((first) => {
+ if (first.settled) {
+ if (first.error) throw first.error;
+ return first.result;
+ }
+
+ return createStreamingJsonKeepaliveResponse(settled, intervalMs);
+ });
+}
+
+function createStreamingJsonKeepaliveResponse(settledPromise, intervalMs) {
+ let timer = null;
+ const clearTimer = () => {
+ if (timer) {
+ clearInterval(timer);
+ timer = null;
+ }
+ };
+
+ const stream = new ReadableStream({
+ start(controller) {
+ controller.enqueue(encoder.encode(KEEPALIVE_CHUNK));
+ timer = setInterval(() => {
+ try {
+ controller.enqueue(encoder.encode(KEEPALIVE_CHUNK));
+ } catch {
+ clearTimer();
+ }
+ }, intervalMs);
+
+ settledPromise.then(async ({ result, error }) => {
+ clearTimer();
+ if (error) throw error;
+ await writeResponseBody(controller, result?.response);
+ controller.close();
+ }).catch((err) => {
+ clearTimer();
+ const message = err?.message || "Failed to build JSON response";
+ controller.enqueue(encoder.encode(JSON.stringify({ error: { message, type: "server_error", code: "internal_server_error" } })));
+ controller.close();
+ });
+ },
+ cancel() {
+ clearTimer();
+ }
+ });
+
+ return {
+ success: true,
+ response: new Response(stream, {
+ status: 200,
+ headers: {
+ "Content-Type": "application/json",
+ "Cache-Control": "no-cache",
+ "Connection": "keep-alive",
+ "Access-Control-Allow-Origin": "*"
+ }
+ })
+ };
+}
diff --git a/open-sse/utils/reasoningContentInjector.js b/open-sse/utils/reasoningContentInjector.js
index 5d9f238f82c..a6829b724b7 100644
--- a/open-sse/utils/reasoningContentInjector.js
+++ b/open-sse/utils/reasoningContentInjector.js
@@ -1,4 +1,4 @@
-// Some thinking-mode providers (DeepSeek, Kimi, ...) require reasoning_content
+// Some thinking-mode providers (DeepSeek, Kimi, MiniMax, ...) require reasoning_content
// to be echoed back on assistant messages. Clients in OpenAI format don't send it,
// so we inject a non-empty placeholder to satisfy upstream validation.
@@ -6,7 +6,9 @@ const PLACEHOLDER = " ";
// Provider-level rules: keyed by executor.provider
const PROVIDER_RULES = {
- deepseek: { scope: "all" }
+ deepseek: { scope: "all" },
+ minimax: { scope: "all" },
+ "minimax-cn": { scope: "all" }
};
// Model-level rules: matched by predicate against model id
diff --git a/open-sse/utils/responsesStreamHelpers.js b/open-sse/utils/responsesStreamHelpers.js
new file mode 100644
index 00000000000..6f90a0c13fd
--- /dev/null
+++ b/open-sse/utils/responsesStreamHelpers.js
@@ -0,0 +1,49 @@
+// Helpers for OpenAI Responses API streaming termination + event framing
+import { FORMATS } from "../translator/formats.js";
+import { formatSSE } from "./streamHelpers.js";
+
+// Responses API events that signal the stream has reached a terminal state
+const OPENAI_RESPONSES_TERMINAL_EVENTS = new Set([
+ "response.completed",
+ "response.failed",
+ "error"
+]);
+
+export function getOpenAIResponsesEventName(eventName, chunk) {
+ if (eventName) return eventName;
+ if (chunk && typeof chunk.type === "string") return chunk.type;
+ return null;
+}
+
+export function isOpenAIResponsesTerminalEvent(eventName, chunk) {
+ const type = getOpenAIResponsesEventName(eventName, chunk);
+ if (OPENAI_RESPONSES_TERMINAL_EVENTS.has(type)) return true;
+ const status = chunk?.response?.status;
+ return status === "completed" || status === "failed";
+}
+
+const sharedEncoder = new TextEncoder();
+
+// Encoded response.failed + [DONE] payload for aborted/stalled Responses passthrough streams
+export function buildAbortedResponsesTerminalBytes() {
+ return sharedEncoder.encode(`${formatIncompleteOpenAIResponsesStreamFailure()}data: [DONE]\n\n`);
+}
+
+// Synthesize a response.failed event for streams that close without a terminal event
+export function formatIncompleteOpenAIResponsesStreamFailure() {
+ return formatSSE({
+ event: "response.failed",
+ data: {
+ type: "response.failed",
+ response: {
+ id: `resp_${Date.now()}`,
+ status: "failed",
+ error: {
+ type: "stream_error",
+ code: "stream_disconnected",
+ message: "stream closed before response.completed"
+ }
+ }
+ }
+ }, FORMATS.OPENAI_RESPONSES);
+}
diff --git a/open-sse/utils/stream.js b/open-sse/utils/stream.js
index 721ba7d8b2d..beaa62eaff9 100644
--- a/open-sse/utils/stream.js
+++ b/open-sse/utils/stream.js
@@ -3,7 +3,9 @@ import { FORMATS } from "../translator/formats.js";
import { trackPendingRequest, appendRequestLog } from "@/lib/usageDb.js";
import { extractUsage, hasValidUsage, estimateUsage, logUsage, addBufferToUsage, filterUsageForFormat, COLORS } from "./usageTracking.js";
import { parseSSELine, hasValuableContent, fixInvalidId, formatSSE } from "./streamHelpers.js";
+import { getOpenAIResponsesEventName, isOpenAIResponsesTerminalEvent, formatIncompleteOpenAIResponsesStreamFailure } from "./responsesStreamHelpers.js";
import { dbg, isDebugEnabled } from "./debugLog.js";
+import { STREAM_HEARTBEAT_INTERVAL_MS } from "../config/runtimeConfig.js";
export { COLORS, formatSSE };
@@ -17,7 +19,6 @@ const sharedEncoder = new TextEncoder();
// null, 0 tokens). Emitting an SSE comment line on a timer keeps the connection
// active. Comment lines (": ...") are ignored by every spec-compliant SSE
// client (Anthropic/OpenAI SDKs included), so this is invisible to callers.
-const DEFAULT_HEARTBEAT_INTERVAL_MS = 10_000;
const HEARTBEAT_COMMENT = ": 9router-keepalive\n\n";
/**
@@ -55,7 +56,7 @@ export function createSSEStream(options = {}) {
body = null,
onStreamComplete = null,
apiKey = null,
- heartbeatIntervalMs = DEFAULT_HEARTBEAT_INTERVAL_MS
+ heartbeatIntervalMs = STREAM_HEARTBEAT_INTERVAL_MS
} = options;
let buffer = "";
@@ -106,6 +107,10 @@ export function createSSEStream(options = {}) {
}
}, heartbeatIntervalMs);
};
+ const resetHeartbeat = (controller) => {
+ stopHeartbeat();
+ startHeartbeat(controller);
+ };
let finalized = false;
const finalizeOnce = (interruptedReason = null) => {
@@ -122,6 +127,11 @@ export function createSSEStream(options = {}) {
);
};
+ // Track Responses API event framing for same-format passthrough (codex)
+ let currentOpenAIResponsesEvent = null;
+ let openAIResponsesTerminalSeen = false;
+ let openAIResponsesDoneSent = false;
+
return new TransformStream({
start(controller) {
// Begin the idle keepalive immediately — the silent gap that kills the
@@ -131,10 +141,10 @@ export function createSSEStream(options = {}) {
transform(chunk, controller) {
if (!ttftAt) ttftAt = Date.now();
- // First real token arrived — the connection is no longer idle, so the
- // keepalive has done its job. Stop it so we never interleave comment
- // lines with genuine SSE data.
- stopHeartbeat();
+ // Reset the idle keepalive after every upstream chunk. SSE comments are
+ // legal between events and keep Cloudflare/nginx from seeing silence
+ // during later thinking gaps.
+ resetHeartbeat(controller);
const text = decoder.decode(chunk, { stream: true });
buffer += text;
reqLogger?.appendProviderChunk?.(text);
@@ -152,6 +162,11 @@ export function createSSEStream(options = {}) {
}
}
+ // Capture Responses API event name to preserve framing in same-format passthrough
+ if (mode === STREAM_MODE.TRANSLATE && targetFormat === FORMATS.OPENAI_RESPONSES && trimmed.startsWith("event:")) {
+ currentOpenAIResponsesEvent = trimmed.slice(6).trim();
+ }
+
// Passthrough mode: normalize and forward
if (mode === STREAM_MODE.PASSTHROUGH) {
let output;
@@ -243,12 +258,33 @@ export function createSSEStream(options = {}) {
const parsed = parseSSELine(trimmed, targetFormat);
if (!parsed) continue;
+ // Responses API same-format passthrough: preserve event framing + track terminal state
+ const isOpenAIResponsesStream = targetFormat === FORMATS.OPENAI_RESPONSES;
+ const keepsOpenAIResponsesFormat = isOpenAIResponsesStream && sourceFormat === FORMATS.OPENAI_RESPONSES;
+ const openAIResponsesEventName = isOpenAIResponsesStream
+ ? getOpenAIResponsesEventName(currentOpenAIResponsesEvent, parsed)
+ : null;
+
+ if (isOpenAIResponsesStream && isOpenAIResponsesTerminalEvent(openAIResponsesEventName, parsed)) {
+ openAIResponsesTerminalSeen = true;
+ }
+
// For Ollama: done=true is the final chunk with finish_reason/usage, must translate
// For other formats: done=true is the [DONE] sentinel, skip
if (parsed && parsed.done && targetFormat !== FORMATS.OLLAMA) {
+ // Synthesize response.failed if the Responses stream never sent a terminal event
+ if (keepsOpenAIResponsesFormat && !openAIResponsesTerminalSeen) {
+ const failedOutput = formatIncompleteOpenAIResponsesStreamFailure();
+ reqLogger?.appendConvertedChunk?.(failedOutput);
+ controller.enqueue(sharedEncoder.encode(failedOutput));
+ openAIResponsesTerminalSeen = true;
+ sseEmittedCount++;
+ }
+
const output = "data: [DONE]\n\n";
reqLogger?.appendConvertedChunk?.(output);
controller.enqueue(sharedEncoder.encode(output));
+ if (keepsOpenAIResponsesFormat) openAIResponsesDoneSent = true;
continue;
}
@@ -293,6 +329,18 @@ export function createSSEStream(options = {}) {
const extracted = extractUsage(parsed);
if (extracted) state.usage = extracted; // Keep original usage for logging
+ // Responses same-format passthrough: re-emit with original event framing
+ if (keepsOpenAIResponsesFormat && openAIResponsesEventName) {
+ const output = formatSSE({ event: openAIResponsesEventName, data: parsed }, sourceFormat);
+ reqLogger?.appendConvertedChunk?.(output);
+ controller.enqueue(sharedEncoder.encode(output));
+ currentOpenAIResponsesEvent = null;
+ sseEmittedCount++;
+ continue;
+ }
+
+ currentOpenAIResponsesEvent = null;
+
// Translate: targetFormat -> openai -> sourceFormat
const translated = translateResponse(targetFormat, sourceFormat, parsed, state);
@@ -306,6 +354,7 @@ export function createSSEStream(options = {}) {
if (translated?.length > 0) {
for (const item of translated) {
+ if (item === null || item === undefined) continue;
// Filter empty chunks
if (!hasValuableContent(item, sourceFormat)) {
continue; // Skip this empty chunk
@@ -386,6 +435,7 @@ export function createSSEStream(options = {}) {
if (translated?.length > 0) {
for (const item of translated) {
+ if (item === null || item === undefined) continue;
const output = formatSSE(item, sourceFormat);
reqLogger?.appendConvertedChunk?.(output);
controller.enqueue(sharedEncoder.encode(output));
@@ -405,15 +455,27 @@ export function createSSEStream(options = {}) {
if (flushed?.length > 0) {
for (const item of flushed) {
+ if (item === null || item === undefined) continue;
const output = formatSSE(item, sourceFormat);
reqLogger?.appendConvertedChunk?.(output);
controller.enqueue(sharedEncoder.encode(output));
}
}
- const doneOutput = "data: [DONE]\n\n";
- reqLogger?.appendConvertedChunk?.(doneOutput);
- controller.enqueue(sharedEncoder.encode(doneOutput));
+ // Synthesize response.failed if a Responses passthrough stream never reached a terminal event
+ const keepsOpenAIResponsesFormat = targetFormat === FORMATS.OPENAI_RESPONSES && sourceFormat === FORMATS.OPENAI_RESPONSES;
+ if (keepsOpenAIResponsesFormat && !openAIResponsesTerminalSeen) {
+ const failedOutput = formatIncompleteOpenAIResponsesStreamFailure();
+ reqLogger?.appendConvertedChunk?.(failedOutput);
+ controller.enqueue(sharedEncoder.encode(failedOutput));
+ openAIResponsesTerminalSeen = true;
+ }
+
+ if (!keepsOpenAIResponsesFormat || !openAIResponsesDoneSent) {
+ const doneOutput = "data: [DONE]\n\n";
+ reqLogger?.appendConvertedChunk?.(doneOutput);
+ controller.enqueue(sharedEncoder.encode(doneOutput));
+ }
if (!hasValidUsage(state?.usage) && totalContentLength > 0) {
state.usage = estimateUsage(body, totalContentLength, sourceFormat);
diff --git a/open-sse/utils/streamHandler.js b/open-sse/utils/streamHandler.js
index 567b59acf0e..35ef5e4ae26 100644
--- a/open-sse/utils/streamHandler.js
+++ b/open-sse/utils/streamHandler.js
@@ -94,13 +94,25 @@ export function createStreamController({ onDisconnect, onError, log, provider, m
* for long periods while raw bytes still flow (e.g. Kiro EventStream
* binary frames buffering, Claude reasoning streams).
*/
-export function createDisconnectAwareStream(transformStream, streamController) {
+export function createDisconnectAwareStream(transformStream, streamController, onAbortTerminal = null) {
const reader = transformStream.readable.getReader();
const writer = transformStream.writable.getWriter();
+ let terminalEmitted = false;
+
+ // Emit a synthesized terminal payload (e.g. Responses response.failed + [DONE]) once
+ const emitTerminal = (controller) => {
+ if (terminalEmitted || !onAbortTerminal) return;
+ terminalEmitted = true;
+ try {
+ const bytes = onAbortTerminal();
+ if (bytes) controller.enqueue(bytes);
+ } catch { /* best-effort terminal */ }
+ };
return new ReadableStream({
async pull(controller) {
if (!streamController.isConnected()) {
+ emitTerminal(controller);
controller.close();
return;
}
@@ -135,17 +147,16 @@ export function createDisconnectAwareStream(transformStream, streamController) {
code === "EPIPE" ||
code === "UND_ERR_SOCKET";
- if (!wasConnected || isNetworkClose) {
- try {
+ // Graceful close on network/abort, or when a structured terminal is available
+ // (Responses passthrough prefers response.failed + [DONE] over a raw transport error)
+ try {
+ if (!wasConnected || isNetworkClose || onAbortTerminal) {
+ emitTerminal(controller);
controller.close();
- } catch (e) {
- // Stream might already be closed or cancelled
- }
- } else {
- try {
+ } else {
controller.error(error);
- } catch (e) { /* already closed */ }
- }
+ }
+ } catch (e) { /* already closed or cancelled */ }
}
},
@@ -173,7 +184,7 @@ export function createDisconnectAwareStream(transformStream, streamController) {
* @param {TransformStream} transformStream - Transform stream for SSE
* @param {object} streamController - Stream controller from createStreamController
*/
-export function pipeWithDisconnect(providerResponse, transformStream, streamController) {
+export function pipeWithDisconnect(providerResponse, transformStream, streamController, onAbortTerminal = null) {
let stallTimer = null;
let chunkCount = 0;
let totalBytes = 0;
@@ -232,7 +243,8 @@ export function pipeWithDisconnect(providerResponse, transformStream, streamCont
return createDisconnectAwareStream(
{ readable: transformedBody, writable: { getWriter: () => ({ abort: () => Promise.resolve() }) } },
- wrappedController
+ wrappedController,
+ onAbortTerminal
);
}
diff --git a/package.json b/package.json
index 289565de72b..f17659aedf1 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "9router-app",
- "version": "0.4.66",
+ "version": "0.4.71",
"description": "9Router web dashboard",
"private": true,
"scripts": {
diff --git a/public/i18n/literals/ar.json b/public/i18n/literals/ar.json
index 7e8e955c351..32b72f8b9f7 100644
--- a/public/i18n/literals/ar.json
+++ b/public/i18n/literals/ar.json
@@ -190,5 +190,6 @@
"Sudo Password": "كلمة مرور Sudo",
"Click to add, click again to remove. Changes are saved automatically.": "انقر للإضافة، انقر مرة أخرى للإزالة. يتم حفظ التغييرات تلقائيًا.",
"⚠️ Risk Notice: This provider uses a subscription/OAuth session not officially licensed for proxy/router use. Account may be restricted or banned. Use at your own risk.": "⚠️ تنبيه مخاطر: يستخدم هذا الموفر اشتراكًا/جلسة OAuth غير مرخصة رسميًا للاستخدام عبر البروكسي/الراوتر. قد يتم تقييد الحساب أو حظره. الاستخدام على مسؤوليتك الخاصة.",
- "⚠️ MITM intercepts HTTPS traffic of IDE tools (Antigravity, GitHub Copilot, Kiro) via local CA to redirect requests to your providers. May violate ToS → account ban. Use at your own risk.": "⚠️ يعترض MITM حركة مرور HTTPS لأدوات IDE (Antigravity، GitHub Copilot، Kiro) عبر CA محلية لإعادة توجيه الطلبات إلى مزوديك. قد ينتهك شروط الخدمة → خطر حظر الحساب. الاستخدام على مسؤوليتك الخاصة."
+ "⚠️ MITM intercepts HTTPS traffic of IDE tools (Antigravity, GitHub Copilot, Kiro) via local CA to redirect requests to your providers. May violate ToS → account ban. Use at your own risk.": "⚠️ يعترض MITM حركة مرور HTTPS لأدوات IDE (Antigravity، GitHub Copilot، Kiro) عبر CA محلية لإعادة توجيه الطلبات إلى مزوديك. قد ينتهك شروط الخدمة → خطر حظر الحساب. الاستخدام على مسؤوليتك الخاصة.",
+ "Endpoint is exposed without an API key.": "نقطة النهاية مكشوفة بدون مفتاح API."
}
diff --git a/public/i18n/literals/bn.json b/public/i18n/literals/bn.json
index 1a67af92623..ef71ddf8d5a 100644
--- a/public/i18n/literals/bn.json
+++ b/public/i18n/literals/bn.json
@@ -190,5 +190,6 @@
"Sudo Password": "Sudo পাসওয়ার্ড",
"Click to add, click again to remove. Changes are saved automatically.": "যোগ করতে ক্লিক করুন, সরাতে আবার ক্লিক করুন। পরিবর্তনগুলি স্বয়ংক্রিয়ভাবে সংরক্ষিত হয়।",
"⚠️ Risk Notice: This provider uses a subscription/OAuth session not officially licensed for proxy/router use. Account may be restricted or banned. Use at your own risk.": "⚠️ ঝুঁকি বিজ্ঞপ্তি: এই প্রদানকারী একটি সাবস্ক্রিপশন/OAuth সেশন ব্যবহার করে যা প্রক্সি/রাউটার ব্যবহারের জন্য আনুষ্ঠানিকভাবে লাইসেন্সপ্রাপ্ত নয়। অ্যাকাউন্ট সীমাবদ্ধ বা নিষিদ্ধ হতে পারে। নিজের ঝুঁকিতে ব্যবহার করুন।",
- "⚠️ MITM intercepts HTTPS traffic of IDE tools (Antigravity, GitHub Copilot, Kiro) via local CA to redirect requests to your providers. May violate ToS → account ban. Use at your own risk.": "⚠️ MITM স্থানীয় CA এর মাধ্যমে IDE টুলগুলির (Antigravity, GitHub Copilot, Kiro) HTTPS ট্রাফিক ইন্টারসেপ্ট করে আপনার প্রদানকারীদের কাছে অনুরোধ পুনঃনির্দেশ করতে। ToS লঙ্ঘন করতে পারে → অ্যাকাউন্ট নিষিদ্ধ ঝুঁকি। নিজের ঝুঁকিতে ব্যবহার করুন।"
+ "⚠️ MITM intercepts HTTPS traffic of IDE tools (Antigravity, GitHub Copilot, Kiro) via local CA to redirect requests to your providers. May violate ToS → account ban. Use at your own risk.": "⚠️ MITM স্থানীয় CA এর মাধ্যমে IDE টুলগুলির (Antigravity, GitHub Copilot, Kiro) HTTPS ট্রাফিক ইন্টারসেপ্ট করে আপনার প্রদানকারীদের কাছে অনুরোধ পুনঃনির্দেশ করতে। ToS লঙ্ঘন করতে পারে → অ্যাকাউন্ট নিষিদ্ধ ঝুঁকি। নিজের ঝুঁকিতে ব্যবহার করুন।",
+ "Endpoint is exposed without an API key.": "এপিআই কী ছাড়াই এন্ডপয়েন্ট উন্মুক্ত।"
}
diff --git a/public/i18n/literals/cs.json b/public/i18n/literals/cs.json
index 2d9eccfe44f..ed0d991ac96 100644
--- a/public/i18n/literals/cs.json
+++ b/public/i18n/literals/cs.json
@@ -190,5 +190,6 @@
"Sudo Password": "Heslo sudo",
"Click to add, click again to remove. Changes are saved automatically.": "Kliknutím přidáte, dalším kliknutím odeberete. Změny se ukládají automaticky.",
"⚠️ Risk Notice: This provider uses a subscription/OAuth session not officially licensed for proxy/router use. Account may be restricted or banned. Use at your own risk.": "⚠️ Upozornění na riziko: Tento poskytovatel používá předplatné/OAuth relaci, která není oficiálně licencována pro použití přes proxy/router. Účet může být omezen nebo zablokován. Používejte na vlastní riziko.",
- "⚠️ MITM intercepts HTTPS traffic of IDE tools (Antigravity, GitHub Copilot, Kiro) via local CA to redirect requests to your providers. May violate ToS → account ban. Use at your own risk.": "⚠️ MITM zachytává HTTPS provoz IDE nástrojů (Antigravity, GitHub Copilot, Kiro) přes místní CA pro přesměrování požadavků na vaše poskytovatele. Může porušit ToS → riziko zákazu účtu. Používejte na vlastní riziko."
+ "⚠️ MITM intercepts HTTPS traffic of IDE tools (Antigravity, GitHub Copilot, Kiro) via local CA to redirect requests to your providers. May violate ToS → account ban. Use at your own risk.": "⚠️ MITM zachytává HTTPS provoz IDE nástrojů (Antigravity, GitHub Copilot, Kiro) přes místní CA pro přesměrování požadavků na vaše poskytovatele. Může porušit ToS → riziko zákazu účtu. Používejte na vlastní riziko.",
+ "Endpoint is exposed without an API key.": "Koncový bod je vystaven bez API klíče."
}
diff --git a/public/i18n/literals/da.json b/public/i18n/literals/da.json
index ef8d6a9596a..c81bbe79e20 100644
--- a/public/i18n/literals/da.json
+++ b/public/i18n/literals/da.json
@@ -190,5 +190,6 @@
"Sudo Password": "Sudo-adgangskode",
"Click to add, click again to remove. Changes are saved automatically.": "Klik for at tilføje, klik igen for at fjerne. Ændringer gemmes automatisk.",
"⚠️ Risk Notice: This provider uses a subscription/OAuth session not officially licensed for proxy/router use. Account may be restricted or banned. Use at your own risk.": "⚠️ Risikomeddelelse: Denne udbyder bruger et abonnement/OAuth-session, der ikke er officielt licenseret til proxy/router-brug. Kontoen kan blive begrænset eller forbudt. Brug på eget ansvar.",
- "⚠️ MITM intercepts HTTPS traffic of IDE tools (Antigravity, GitHub Copilot, Kiro) via local CA to redirect requests to your providers. May violate ToS → account ban. Use at your own risk.": "⚠️ MITM opfanger HTTPS-trafik fra IDE-værktøjer (Antigravity, GitHub Copilot, Kiro) via lokal CA for at omdirigere anmodninger til dine udbydere. Kan overtræde ToS → risiko for kontoforbud. Brug på eget ansvar."
+ "⚠️ MITM intercepts HTTPS traffic of IDE tools (Antigravity, GitHub Copilot, Kiro) via local CA to redirect requests to your providers. May violate ToS → account ban. Use at your own risk.": "⚠️ MITM opfanger HTTPS-trafik fra IDE-værktøjer (Antigravity, GitHub Copilot, Kiro) via lokal CA for at omdirigere anmodninger til dine udbydere. Kan overtræde ToS → risiko for kontoforbud. Brug på eget ansvar.",
+ "Endpoint is exposed without an API key.": "Endpointet er eksponeret uden en API-nøgle."
}
diff --git a/public/i18n/literals/de.json b/public/i18n/literals/de.json
index 6c37d13b2ac..57bf3ef39de 100644
--- a/public/i18n/literals/de.json
+++ b/public/i18n/literals/de.json
@@ -190,5 +190,6 @@
"Sudo Password": "Sudo-Passwort",
"Click to add, click again to remove. Changes are saved automatically.": "Klicken zum Hinzufügen, erneut klicken zum Entfernen. Änderungen werden automatisch gespeichert.",
"⚠️ Risk Notice: This provider uses a subscription/OAuth session not officially licensed for proxy/router use. Account may be restricted or banned. Use at your own risk.": "⚠️ Risikohinweis: Dieser Anbieter verwendet eine Abonnement-/OAuth-Sitzung, die nicht offiziell für die Proxy-/Router-Nutzung lizenziert ist. Das Konto kann eingeschränkt oder gesperrt werden. Nutzung auf eigene Gefahr.",
- "⚠️ MITM intercepts HTTPS traffic of IDE tools (Antigravity, GitHub Copilot, Kiro) via local CA to redirect requests to your providers. May violate ToS → account ban. Use at your own risk.": "⚠️ MITM fängt HTTPS-Verkehr von IDE-Tools (Antigravity, GitHub Copilot, Kiro) über lokale CA ab, um Anfragen an Ihre Anbieter umzuleiten. Kann gegen ToS verstoßen → Risiko der Kontosperrung. Nutzung auf eigene Gefahr."
+ "⚠️ MITM intercepts HTTPS traffic of IDE tools (Antigravity, GitHub Copilot, Kiro) via local CA to redirect requests to your providers. May violate ToS → account ban. Use at your own risk.": "⚠️ MITM fängt HTTPS-Verkehr von IDE-Tools (Antigravity, GitHub Copilot, Kiro) über lokale CA ab, um Anfragen an Ihre Anbieter umzuleiten. Kann gegen ToS verstoßen → Risiko der Kontosperrung. Nutzung auf eigene Gefahr.",
+ "Endpoint is exposed without an API key.": "Der Endpunkt ist ohne API-Schlüssel offengelegt."
}
diff --git a/public/i18n/literals/el.json b/public/i18n/literals/el.json
index 3ebde7364e6..bfbf8888651 100644
--- a/public/i18n/literals/el.json
+++ b/public/i18n/literals/el.json
@@ -190,5 +190,6 @@
"Sudo Password": "Κωδικός πρόσβασης Sudo",
"Click to add, click again to remove. Changes are saved automatically.": "Κάντε κλικ για προσθήκη, κάντε ξανά κλικ για αφαίρεση. Οι αλλαγές αποθηκεύονται αυτόματα.",
"⚠️ Risk Notice: This provider uses a subscription/OAuth session not officially licensed for proxy/router use. Account may be restricted or banned. Use at your own risk.": "⚠️ Ειδοποίηση κινδύνου: Αυτός ο πάροχος χρησιμοποιεί συνδρομή/συνεδρία OAuth που δεν έχει επίσημη άδεια για χρήση μέσω proxy/router. Ο λογαριασμός ενδέχεται να περιοριστεί ή να αποκλειστεί. Χρήση με δική σας ευθύνη.",
- "⚠️ MITM intercepts HTTPS traffic of IDE tools (Antigravity, GitHub Copilot, Kiro) via local CA to redirect requests to your providers. May violate ToS → account ban. Use at your own risk.": "⚠️ Το MITM υποκλέπτει την κίνηση HTTPS των εργαλείων IDE (Antigravity, GitHub Copilot, Kiro) μέσω τοπικού CA για ανακατεύθυνση αιτημάτων στους παρόχους σας. Μπορεί να παραβιάσει τους ToS → κίνδυνος αποκλεισμού λογαριασμού. Χρήση με δική σας ευθύνη."
+ "⚠️ MITM intercepts HTTPS traffic of IDE tools (Antigravity, GitHub Copilot, Kiro) via local CA to redirect requests to your providers. May violate ToS → account ban. Use at your own risk.": "⚠️ Το MITM υποκλέπτει την κίνηση HTTPS των εργαλείων IDE (Antigravity, GitHub Copilot, Kiro) μέσω τοπικού CA για ανακατεύθυνση αιτημάτων στους παρόχους σας. Μπορεί να παραβιάσει τους ToS → κίνδυνος αποκλεισμού λογαριασμού. Χρήση με δική σας ευθύνη.",
+ "Endpoint is exposed without an API key.": "Το τελικό σημείο είναι εκτεθειμένο χωρίς κλειδί API."
}
diff --git a/public/i18n/literals/es.json b/public/i18n/literals/es.json
index fa61ee6a63a..69d71e8fcfb 100644
--- a/public/i18n/literals/es.json
+++ b/public/i18n/literals/es.json
@@ -190,5 +190,6 @@
"Sudo Password": "Contraseña de sudo",
"Click to add, click again to remove. Changes are saved automatically.": "Haz clic para agregar, haz clic de nuevo para eliminar. Los cambios se guardan automáticamente.",
"⚠️ Risk Notice: This provider uses a subscription/OAuth session not officially licensed for proxy/router use. Account may be restricted or banned. Use at your own risk.": "⚠️ Aviso de Riesgo: Este proveedor usa una sesión de suscripción/OAuth no licenciada oficialmente para uso de proxy/router. La cuenta puede ser restringida o baneada. Use bajo su propio riesgo.",
- "⚠️ MITM intercepts HTTPS traffic of IDE tools (Antigravity, GitHub Copilot, Kiro) via local CA to redirect requests to your providers. May violate ToS → account ban. Use at your own risk.": "⚠️ MITM intercepta el tráfico HTTPS de herramientas IDE (Antigravity, GitHub Copilot, Kiro) mediante CA local para redirigir solicitudes a sus proveedores. Puede violar los ToS → riesgo de baneo de cuenta. Use bajo su propio riesgo."
+ "⚠️ MITM intercepts HTTPS traffic of IDE tools (Antigravity, GitHub Copilot, Kiro) via local CA to redirect requests to your providers. May violate ToS → account ban. Use at your own risk.": "⚠️ MITM intercepta el tráfico HTTPS de herramientas IDE (Antigravity, GitHub Copilot, Kiro) mediante CA local para redirigir solicitudes a sus proveedores. Puede violar los ToS → riesgo de baneo de cuenta. Use bajo su propio riesgo.",
+ "Endpoint is exposed without an API key.": "El endpoint está expuesto sin una clave de API."
}
diff --git a/public/i18n/literals/fi.json b/public/i18n/literals/fi.json
index 9eaaaf51553..dc8b116c317 100644
--- a/public/i18n/literals/fi.json
+++ b/public/i18n/literals/fi.json
@@ -190,5 +190,6 @@
"Sudo Password": "Sudo-salasana",
"Click to add, click again to remove. Changes are saved automatically.": "Napsauta lisätäksesi, napsauta uudelleen poistaaksesi. Muutokset tallennetaan automaattisesti.",
"⚠️ Risk Notice: This provider uses a subscription/OAuth session not officially licensed for proxy/router use. Account may be restricted or banned. Use at your own risk.": "⚠️ Riski-ilmoitus: Tämä palveluntarjoaja käyttää tilaus-/OAuth-istuntoa, jota ei ole virallisesti lisensoitu välityspalvelin-/reititinkäyttöön. Tili voidaan rajoittaa tai estää. Käyttö omalla vastuulla.",
- "⚠️ MITM intercepts HTTPS traffic of IDE tools (Antigravity, GitHub Copilot, Kiro) via local CA to redirect requests to your providers. May violate ToS → account ban. Use at your own risk.": "⚠️ MITM sieppaa IDE-työkalujen (Antigravity, GitHub Copilot, Kiro) HTTPS-liikennettä paikallisen CA:n kautta uudelleenohjatakseen pyyntöjä palveluntarjoajillesi. Voi rikkoa ToS:ää → tilin estoriski. Käyttö omalla vastuulla."
+ "⚠️ MITM intercepts HTTPS traffic of IDE tools (Antigravity, GitHub Copilot, Kiro) via local CA to redirect requests to your providers. May violate ToS → account ban. Use at your own risk.": "⚠️ MITM sieppaa IDE-työkalujen (Antigravity, GitHub Copilot, Kiro) HTTPS-liikennettä paikallisen CA:n kautta uudelleenohjatakseen pyyntöjä palveluntarjoajillesi. Voi rikkoa ToS:ää → tilin estoriski. Käyttö omalla vastuulla.",
+ "Endpoint is exposed without an API key.": "Päätepiste on alttiina ilman API-avainta."
}
diff --git a/public/i18n/literals/fr.json b/public/i18n/literals/fr.json
index d96b1066a92..bbf8854a494 100644
--- a/public/i18n/literals/fr.json
+++ b/public/i18n/literals/fr.json
@@ -190,5 +190,6 @@
"Sudo Password": "Mot de passe sudo",
"Click to add, click again to remove. Changes are saved automatically.": "Cliquez pour ajouter, cliquez à nouveau pour supprimer. Les modifications sont enregistrées automatiquement.",
"⚠️ Risk Notice: This provider uses a subscription/OAuth session not officially licensed for proxy/router use. Account may be restricted or banned. Use at your own risk.": "⚠️ Avis de risque : Ce fournisseur utilise une session d'abonnement/OAuth non officiellement autorisée pour une utilisation proxy/routeur. Le compte peut être restreint ou banni. Utilisez à vos propres risques.",
- "⚠️ MITM intercepts HTTPS traffic of IDE tools (Antigravity, GitHub Copilot, Kiro) via local CA to redirect requests to your providers. May violate ToS → account ban. Use at your own risk.": "⚠️ MITM intercepte le trafic HTTPS des outils IDE (Antigravity, GitHub Copilot, Kiro) via une CA locale pour rediriger les requêtes vers vos fournisseurs. Peut violer les CGU → risque de bannissement de compte. Utilisez à vos propres risques."
+ "⚠️ MITM intercepts HTTPS traffic of IDE tools (Antigravity, GitHub Copilot, Kiro) via local CA to redirect requests to your providers. May violate ToS → account ban. Use at your own risk.": "⚠️ MITM intercepte le trafic HTTPS des outils IDE (Antigravity, GitHub Copilot, Kiro) via une CA locale pour rediriger les requêtes vers vos fournisseurs. Peut violer les CGU → risque de bannissement de compte. Utilisez à vos propres risques.",
+ "Endpoint is exposed without an API key.": "Le point de terminaison est exposé sans clé API."
}
diff --git a/public/i18n/literals/he.json b/public/i18n/literals/he.json
index 192851884c2..c144d01b832 100644
--- a/public/i18n/literals/he.json
+++ b/public/i18n/literals/he.json
@@ -190,5 +190,6 @@
"Sudo Password": "סיסמת Sudo",
"Click to add, click again to remove. Changes are saved automatically.": "לחץ להוספה, לחץ שוב להסרה. השינויים נשמרים אוטומטית.",
"⚠️ Risk Notice: This provider uses a subscription/OAuth session not officially licensed for proxy/router use. Account may be restricted or banned. Use at your own risk.": "⚠️ הודעת סיכון: ספק זה משתמש במנוי/הפעלת OAuth שאינה מורשית רשמית לשימוש פרוקסי/ראוטר. החשבון עלול להיות מוגבל או חסום. השימוש על אחריותך בלבד.",
- "⚠️ MITM intercepts HTTPS traffic of IDE tools (Antigravity, GitHub Copilot, Kiro) via local CA to redirect requests to your providers. May violate ToS → account ban. Use at your own risk.": "⚠️ MITM יירוט תעבורת HTTPS של כלי IDE (Antigravity, GitHub Copilot, Kiro) באמצעות CA מקומי כדי להפנות בקשות לספקים שלך. עלול להפר את תנאי השירות → סיכון חסימת חשבון. השימוש על אחריותך."
+ "⚠️ MITM intercepts HTTPS traffic of IDE tools (Antigravity, GitHub Copilot, Kiro) via local CA to redirect requests to your providers. May violate ToS → account ban. Use at your own risk.": "⚠️ MITM יירוט תעבורת HTTPS של כלי IDE (Antigravity, GitHub Copilot, Kiro) באמצעות CA מקומי כדי להפנות בקשות לספקים שלך. עלול להפר את תנאי השירות → סיכון חסימת חשבון. השימוש על אחריותך.",
+ "Endpoint is exposed without an API key.": "נקודת הקצה חשופה ללא מפתח API."
}
diff --git a/public/i18n/literals/hi.json b/public/i18n/literals/hi.json
index 11a0bee0a27..2fc6b338b90 100644
--- a/public/i18n/literals/hi.json
+++ b/public/i18n/literals/hi.json
@@ -190,5 +190,6 @@
"Sudo Password": "Sudo पासवर्ड",
"Click to add, click again to remove. Changes are saved automatically.": "जोड़ने के लिए क्लिक करें, हटाने के लिए फिर से क्लिक करें। परिवर्तन स्वचालित रूप से सहेजे जाते हैं।",
"⚠️ Risk Notice: This provider uses a subscription/OAuth session not officially licensed for proxy/router use. Account may be restricted or banned. Use at your own risk.": "⚠️ जोखिम सूचना: यह प्रदाता एक सब्सक्रिप्शन/OAuth सत्र का उपयोग करता है जो प्रॉक्सी/राउटर उपयोग के लिए आधिकारिक रूप से लाइसेंस प्राप्त नहीं है। खाता प्रतिबंधित या बैन हो सकता है। अपने जोखिम पर उपयोग करें।",
- "⚠️ MITM intercepts HTTPS traffic of IDE tools (Antigravity, GitHub Copilot, Kiro) via local CA to redirect requests to your providers. May violate ToS → account ban. Use at your own risk.": "⚠️ MITM स्थानीय CA के माध्यम से IDE टूल्स (Antigravity, GitHub Copilot, Kiro) के HTTPS ट्रैफिक को इंटरसेप्ट करता है ताकि अनुरोधों को आपके प्रदाताओं पर पुनर्निर्देशित किया जा सके। ToS का उल्लंघन हो सकता है → खाता बैन का जोखिम। अपने जोखिम पर उपयोग करें।"
+ "⚠️ MITM intercepts HTTPS traffic of IDE tools (Antigravity, GitHub Copilot, Kiro) via local CA to redirect requests to your providers. May violate ToS → account ban. Use at your own risk.": "⚠️ MITM स्थानीय CA के माध्यम से IDE टूल्स (Antigravity, GitHub Copilot, Kiro) के HTTPS ट्रैफिक को इंटरसेप्ट करता है ताकि अनुरोधों को आपके प्रदाताओं पर पुनर्निर्देशित किया जा सके। ToS का उल्लंघन हो सकता है → खाता बैन का जोखिम। अपने जोखिम पर उपयोग करें।",
+ "Endpoint is exposed without an API key.": "एंडपॉइंट बिना API कुंजी के उजागर है।"
}
diff --git a/public/i18n/literals/hu.json b/public/i18n/literals/hu.json
index a6768bb864f..8e3392914f6 100644
--- a/public/i18n/literals/hu.json
+++ b/public/i18n/literals/hu.json
@@ -190,5 +190,6 @@
"Sudo Password": "Sudo jelszó",
"Click to add, click again to remove. Changes are saved automatically.": "Kattintson a hozzáadáshoz, kattintson újra az eltávolításhoz. A változtatások automatikusan mentésre kerülnek.",
"⚠️ Risk Notice: This provider uses a subscription/OAuth session not officially licensed for proxy/router use. Account may be restricted or banned. Use at your own risk.": "⚠️ Kockázati figyelmeztetés: Ez a szolgáltató olyan előfizetést/OAuth munkamenetet használ, amely hivatalosan nincs proxy/router használatra engedélyezve. A fiók korlátozható vagy letiltható. Saját felelősségre használja.",
- "⚠️ MITM intercepts HTTPS traffic of IDE tools (Antigravity, GitHub Copilot, Kiro) via local CA to redirect requests to your providers. May violate ToS → account ban. Use at your own risk.": "⚠️ A MITM elfogja az IDE eszközök (Antigravity, GitHub Copilot, Kiro) HTTPS forgalmát helyi CA-n keresztül, hogy átirányítsa a kéréseket a szolgáltatóidhoz. Megsértheti a ToS-t → fiók letiltási kockázat. Saját felelősségre használja."
+ "⚠️ MITM intercepts HTTPS traffic of IDE tools (Antigravity, GitHub Copilot, Kiro) via local CA to redirect requests to your providers. May violate ToS → account ban. Use at your own risk.": "⚠️ A MITM elfogja az IDE eszközök (Antigravity, GitHub Copilot, Kiro) HTTPS forgalmát helyi CA-n keresztül, hogy átirányítsa a kéréseket a szolgáltatóidhoz. Megsértheti a ToS-t → fiók letiltási kockázat. Saját felelősségre használja.",
+ "Endpoint is exposed without an API key.": "A végpont API-kulcs nélkül van kitéve."
}
diff --git a/public/i18n/literals/id.json b/public/i18n/literals/id.json
index 2ffbfa641cb..3e5097aa7d4 100644
--- a/public/i18n/literals/id.json
+++ b/public/i18n/literals/id.json
@@ -190,5 +190,6 @@
"Sudo Password": "Kata Sandi Sudo",
"Click to add, click again to remove. Changes are saved automatically.": "Klik untuk menambah, klik lagi untuk menghapus. Perubahan disimpan secara otomatis.",
"⚠️ Risk Notice: This provider uses a subscription/OAuth session not officially licensed for proxy/router use. Account may be restricted or banned. Use at your own risk.": "⚠️ Pemberitahuan Risiko: Penyedia ini menggunakan sesi langganan/OAuth yang tidak dilisensikan secara resmi untuk penggunaan proxy/router. Akun mungkin dibatasi atau diblokir. Gunakan dengan risiko Anda sendiri.",
- "⚠️ MITM intercepts HTTPS traffic of IDE tools (Antigravity, GitHub Copilot, Kiro) via local CA to redirect requests to your providers. May violate ToS → account ban. Use at your own risk.": "⚠️ MITM mencegat lalu lintas HTTPS alat IDE (Antigravity, GitHub Copilot, Kiro) melalui CA lokal untuk mengalihkan permintaan ke penyedia Anda. Mungkin melanggar ToS → risiko ban akun. Gunakan dengan risiko Anda sendiri."
+ "⚠️ MITM intercepts HTTPS traffic of IDE tools (Antigravity, GitHub Copilot, Kiro) via local CA to redirect requests to your providers. May violate ToS → account ban. Use at your own risk.": "⚠️ MITM mencegat lalu lintas HTTPS alat IDE (Antigravity, GitHub Copilot, Kiro) melalui CA lokal untuk mengalihkan permintaan ke penyedia Anda. Mungkin melanggar ToS → risiko ban akun. Gunakan dengan risiko Anda sendiri.",
+ "Endpoint is exposed without an API key.": "Endpoint terekspos tanpa kunci API."
}
diff --git a/public/i18n/literals/it.json b/public/i18n/literals/it.json
index 245f218732d..7f684e8344a 100644
--- a/public/i18n/literals/it.json
+++ b/public/i18n/literals/it.json
@@ -190,5 +190,6 @@
"Sudo Password": "Password Sudo",
"Click to add, click again to remove. Changes are saved automatically.": "Clicca per aggiungere, clicca di nuovo per rimuovere. Le modifiche vengono salvate automaticamente.",
"⚠️ Risk Notice: This provider uses a subscription/OAuth session not officially licensed for proxy/router use. Account may be restricted or banned. Use at your own risk.": "⚠️ Avviso di Rischio: Questo provider utilizza una sessione abbonamento/OAuth non ufficialmente autorizzata per l'uso proxy/router. L'account potrebbe essere limitato o bannato. Usa a tuo rischio.",
- "⚠️ MITM intercepts HTTPS traffic of IDE tools (Antigravity, GitHub Copilot, Kiro) via local CA to redirect requests to your providers. May violate ToS → account ban. Use at your own risk.": "⚠️ MITM intercetta il traffico HTTPS degli strumenti IDE (Antigravity, GitHub Copilot, Kiro) tramite CA locale per reindirizzare le richieste ai tuoi provider. Può violare i ToS → rischio ban account. Usa a tuo rischio."
+ "⚠️ MITM intercepts HTTPS traffic of IDE tools (Antigravity, GitHub Copilot, Kiro) via local CA to redirect requests to your providers. May violate ToS → account ban. Use at your own risk.": "⚠️ MITM intercetta il traffico HTTPS degli strumenti IDE (Antigravity, GitHub Copilot, Kiro) tramite CA locale per reindirizzare le richieste ai tuoi provider. Può violare i ToS → rischio ban account. Usa a tuo rischio.",
+ "Endpoint is exposed without an API key.": "L'endpoint è esposto senza una chiave API."
}
diff --git a/public/i18n/literals/ja.json b/public/i18n/literals/ja.json
index 4069c20e600..e448c685fba 100644
--- a/public/i18n/literals/ja.json
+++ b/public/i18n/literals/ja.json
@@ -190,5 +190,6 @@
"Sudo Password": "Sudoパスワード",
"Click to add, click again to remove. Changes are saved automatically.": "クリックで追加、もう一度クリックで削除。変更は自動的に保存されます。",
"⚠️ Risk Notice: This provider uses a subscription/OAuth session not officially licensed for proxy/router use. Account may be restricted or banned. Use at your own risk.": "⚠️ リスク通知: このプロバイダーは、プロキシ/ルーター使用について公式にライセンスされていないサブスクリプション/OAuthセッションを使用しています。アカウントが制限または禁止される可能性があります。自己責任でご使用ください。",
- "⚠️ MITM intercepts HTTPS traffic of IDE tools (Antigravity, GitHub Copilot, Kiro) via local CA to redirect requests to your providers. May violate ToS → account ban. Use at your own risk.": "⚠️ MITMはローカルCAを介してIDEツール (Antigravity, GitHub Copilot, Kiro) のHTTPSトラフィックを傍受し、リクエストをプロバイダーにリダイレクトします。ToS違反の可能性 → アカウントBANリスク。自己責任でご使用ください。"
+ "⚠️ MITM intercepts HTTPS traffic of IDE tools (Antigravity, GitHub Copilot, Kiro) via local CA to redirect requests to your providers. May violate ToS → account ban. Use at your own risk.": "⚠️ MITMはローカルCAを介してIDEツール (Antigravity, GitHub Copilot, Kiro) のHTTPSトラフィックを傍受し、リクエストをプロバイダーにリダイレクトします。ToS違反の可能性 → アカウントBANリスク。自己責任でご使用ください。",
+ "Endpoint is exposed without an API key.": "API キーなしでエンドポイントが公開されています。"
}
diff --git a/public/i18n/literals/ko.json b/public/i18n/literals/ko.json
index 784901884b3..1edb094ebfd 100644
--- a/public/i18n/literals/ko.json
+++ b/public/i18n/literals/ko.json
@@ -190,5 +190,6 @@
"Sudo Password": "Sudo 암호",
"Click to add, click again to remove. Changes are saved automatically.": "클릭하여 추가, 다시 클릭하여 제거. 변경 사항은 자동으로 저장됩니다.",
"⚠️ Risk Notice: This provider uses a subscription/OAuth session not officially licensed for proxy/router use. Account may be restricted or banned. Use at your own risk.": "⚠️ 위험 알림: 이 공급자는 프록시/라우터 사용에 대해 공식적으로 라이선스가 부여되지 않은 구독/OAuth 세션을 사용합니다. 계정이 제한되거나 차단될 수 있습니다. 사용에 대한 책임은 본인에게 있습니다.",
- "⚠️ MITM intercepts HTTPS traffic of IDE tools (Antigravity, GitHub Copilot, Kiro) via local CA to redirect requests to your providers. May violate ToS → account ban. Use at your own risk.": "⚠️ MITM은 로컬 CA를 통해 IDE 도구(Antigravity, GitHub Copilot, Kiro)의 HTTPS 트래픽을 가로채 요청을 공급자로 리다이렉트합니다. ToS 위반 가능성 → 계정 차단 위험. 사용에 대한 책임은 본인에게 있습니다."
+ "⚠️ MITM intercepts HTTPS traffic of IDE tools (Antigravity, GitHub Copilot, Kiro) via local CA to redirect requests to your providers. May violate ToS → account ban. Use at your own risk.": "⚠️ MITM은 로컬 CA를 통해 IDE 도구(Antigravity, GitHub Copilot, Kiro)의 HTTPS 트래픽을 가로채 요청을 공급자로 리다이렉트합니다. ToS 위반 가능성 → 계정 차단 위험. 사용에 대한 책임은 본인에게 있습니다.",
+ "Endpoint is exposed without an API key.": "API 키 없이 엔드포인트가 노출되어 있습니다."
}
diff --git a/public/i18n/literals/nl.json b/public/i18n/literals/nl.json
index 6db8aec0fe3..2eab85baf6d 100644
--- a/public/i18n/literals/nl.json
+++ b/public/i18n/literals/nl.json
@@ -190,5 +190,6 @@
"Sudo Password": "Sudo-wachtwoord",
"Click to add, click again to remove. Changes are saved automatically.": "Klik om toe te voegen, klik opnieuw om te verwijderen. Wijzigingen worden automatisch opgeslagen.",
"⚠️ Risk Notice: This provider uses a subscription/OAuth session not officially licensed for proxy/router use. Account may be restricted or banned. Use at your own risk.": "⚠️ Risicokennisgeving: Deze provider gebruikt een abonnement/OAuth-sessie die niet officieel is gelicentieerd voor proxy/router-gebruik. Account kan worden beperkt of verbannen. Gebruik op eigen risico.",
- "⚠️ MITM intercepts HTTPS traffic of IDE tools (Antigravity, GitHub Copilot, Kiro) via local CA to redirect requests to your providers. May violate ToS → account ban. Use at your own risk.": "⚠️ MITM onderschept HTTPS-verkeer van IDE-tools (Antigravity, GitHub Copilot, Kiro) via lokale CA om verzoeken om te leiden naar uw providers. Kan ToS schenden → risico op accountban. Gebruik op eigen risico."
+ "⚠️ MITM intercepts HTTPS traffic of IDE tools (Antigravity, GitHub Copilot, Kiro) via local CA to redirect requests to your providers. May violate ToS → account ban. Use at your own risk.": "⚠️ MITM onderschept HTTPS-verkeer van IDE-tools (Antigravity, GitHub Copilot, Kiro) via lokale CA om verzoeken om te leiden naar uw providers. Kan ToS schenden → risico op accountban. Gebruik op eigen risico.",
+ "Endpoint is exposed without an API key.": "Het eindpunt is blootgesteld zonder API-sleutel."
}
diff --git a/public/i18n/literals/no.json b/public/i18n/literals/no.json
index d359925099a..e3f521556eb 100644
--- a/public/i18n/literals/no.json
+++ b/public/i18n/literals/no.json
@@ -190,5 +190,6 @@
"Sudo Password": "Sudo-passord",
"Click to add, click again to remove. Changes are saved automatically.": "Klikk for å legge til, klikk igjen for å fjerne. Endringer lagres automatisk.",
"⚠️ Risk Notice: This provider uses a subscription/OAuth session not officially licensed for proxy/router use. Account may be restricted or banned. Use at your own risk.": "⚠️ Risikovarsel: Denne leverandøren bruker en abonnements-/OAuth-økt som ikke er offisielt lisensiert for proxy-/ruterbruk. Kontoen kan bli begrenset eller utestengt. Bruk på eget ansvar.",
- "⚠️ MITM intercepts HTTPS traffic of IDE tools (Antigravity, GitHub Copilot, Kiro) via local CA to redirect requests to your providers. May violate ToS → account ban. Use at your own risk.": "⚠️ MITM avskjærer HTTPS-trafikk fra IDE-verktøy (Antigravity, GitHub Copilot, Kiro) via lokal CA for å omdirigere forespørsler til dine leverandører. Kan bryte ToS → risiko for kontoutestengelse. Bruk på eget ansvar."
+ "⚠️ MITM intercepts HTTPS traffic of IDE tools (Antigravity, GitHub Copilot, Kiro) via local CA to redirect requests to your providers. May violate ToS → account ban. Use at your own risk.": "⚠️ MITM avskjærer HTTPS-trafikk fra IDE-verktøy (Antigravity, GitHub Copilot, Kiro) via lokal CA for å omdirigere forespørsler til dine leverandører. Kan bryte ToS → risiko for kontoutestengelse. Bruk på eget ansvar.",
+ "Endpoint is exposed without an API key.": "Endepunktet er eksponert uten en API-nøkkel."
}
diff --git a/public/i18n/literals/pl.json b/public/i18n/literals/pl.json
index 3b64484328e..f4b62a67fc9 100644
--- a/public/i18n/literals/pl.json
+++ b/public/i18n/literals/pl.json
@@ -190,5 +190,6 @@
"Sudo Password": "Hasło sudo",
"Click to add, click again to remove. Changes are saved automatically.": "Kliknij, aby dodać, kliknij ponownie, aby usunąć. Zmiany są zapisywane automatycznie.",
"⚠️ Risk Notice: This provider uses a subscription/OAuth session not officially licensed for proxy/router use. Account may be restricted or banned. Use at your own risk.": "⚠️ Ostrzeżenie o ryzyku: Ten dostawca używa sesji subskrypcji/OAuth, która nie jest oficjalnie licencjonowana do użytku proxy/routera. Konto może zostać ograniczone lub zbanowane. Używaj na własne ryzyko.",
- "⚠️ MITM intercepts HTTPS traffic of IDE tools (Antigravity, GitHub Copilot, Kiro) via local CA to redirect requests to your providers. May violate ToS → account ban. Use at your own risk.": "⚠️ MITM przechwytuje ruch HTTPS narzędzi IDE (Antigravity, GitHub Copilot, Kiro) przez lokalne CA, aby przekierować żądania do twoich dostawców. Może naruszyć ToS → ryzyko zbanowania konta. Używaj na własne ryzyko."
+ "⚠️ MITM intercepts HTTPS traffic of IDE tools (Antigravity, GitHub Copilot, Kiro) via local CA to redirect requests to your providers. May violate ToS → account ban. Use at your own risk.": "⚠️ MITM przechwytuje ruch HTTPS narzędzi IDE (Antigravity, GitHub Copilot, Kiro) przez lokalne CA, aby przekierować żądania do twoich dostawców. Może naruszyć ToS → ryzyko zbanowania konta. Używaj na własne ryzyko.",
+ "Endpoint is exposed without an API key.": "Punkt końcowy jest dostępny bez klucza API."
}
diff --git a/public/i18n/literals/pt-BR.json b/public/i18n/literals/pt-BR.json
index c603f1a2453..6edba2e7c32 100644
--- a/public/i18n/literals/pt-BR.json
+++ b/public/i18n/literals/pt-BR.json
@@ -190,5 +190,6 @@
"Sudo Password": "Senha sudo",
"Click to add, click again to remove. Changes are saved automatically.": "Clique para adicionar, clique novamente para remover. As alterações são salvas automaticamente.",
"⚠️ Risk Notice: This provider uses a subscription/OAuth session not officially licensed for proxy/router use. Account may be restricted or banned. Use at your own risk.": "⚠️ Aviso de Risco: Este provedor usa uma sessão de assinatura/OAuth não licenciada oficialmente para uso de proxy/roteador. A conta pode ser restrita ou banida. Use por sua conta e risco.",
- "⚠️ MITM intercepts HTTPS traffic of IDE tools (Antigravity, GitHub Copilot, Kiro) via local CA to redirect requests to your providers. May violate ToS → account ban. Use at your own risk.": "⚠️ MITM intercepta tráfego HTTPS de ferramentas IDE (Antigravity, GitHub Copilot, Kiro) via CA local para redirecionar solicitações aos seus provedores. Pode violar ToS → risco de banimento de conta. Use por sua conta e risco."
+ "⚠️ MITM intercepts HTTPS traffic of IDE tools (Antigravity, GitHub Copilot, Kiro) via local CA to redirect requests to your providers. May violate ToS → account ban. Use at your own risk.": "⚠️ MITM intercepta tráfego HTTPS de ferramentas IDE (Antigravity, GitHub Copilot, Kiro) via CA local para redirecionar solicitações aos seus provedores. Pode violar ToS → risco de banimento de conta. Use por sua conta e risco.",
+ "Endpoint is exposed without an API key.": "O endpoint está exposto sem uma chave de API."
}
diff --git a/public/i18n/literals/pt-PT.json b/public/i18n/literals/pt-PT.json
index b58f654d760..c17e932a294 100644
--- a/public/i18n/literals/pt-PT.json
+++ b/public/i18n/literals/pt-PT.json
@@ -190,5 +190,6 @@
"Sudo Password": "Palavra-passe sudo",
"Click to add, click again to remove. Changes are saved automatically.": "Clique para adicionar, clique novamente para remover. As alterações são guardadas automaticamente.",
"⚠️ Risk Notice: This provider uses a subscription/OAuth session not officially licensed for proxy/router use. Account may be restricted or banned. Use at your own risk.": "⚠️ Aviso de Risco: Este fornecedor utiliza uma sessão de subscrição/OAuth não licenciada oficialmente para uso de proxy/router. A conta pode ser restringida ou banida. Use por sua conta e risco.",
- "⚠️ MITM intercepts HTTPS traffic of IDE tools (Antigravity, GitHub Copilot, Kiro) via local CA to redirect requests to your providers. May violate ToS → account ban. Use at your own risk.": "⚠️ MITM interceta tráfego HTTPS de ferramentas IDE (Antigravity, GitHub Copilot, Kiro) via CA local para redirecionar pedidos para os seus fornecedores. Pode violar ToS → risco de banimento de conta. Use por sua conta e risco."
+ "⚠️ MITM intercepts HTTPS traffic of IDE tools (Antigravity, GitHub Copilot, Kiro) via local CA to redirect requests to your providers. May violate ToS → account ban. Use at your own risk.": "⚠️ MITM interceta tráfego HTTPS de ferramentas IDE (Antigravity, GitHub Copilot, Kiro) via CA local para redirecionar pedidos para os seus fornecedores. Pode violar ToS → risco de banimento de conta. Use por sua conta e risco.",
+ "Endpoint is exposed without an API key.": "O endpoint está exposto sem uma chave de API."
}
diff --git a/public/i18n/literals/ro.json b/public/i18n/literals/ro.json
index 7d21f06d86c..03384415b8e 100644
--- a/public/i18n/literals/ro.json
+++ b/public/i18n/literals/ro.json
@@ -190,5 +190,6 @@
"Sudo Password": "Parola Sudo",
"Click to add, click again to remove. Changes are saved automatically.": "Faceți clic pentru a adăuga, faceți clic din nou pentru a elimina. Modificările sunt salvate automat.",
"⚠️ Risk Notice: This provider uses a subscription/OAuth session not officially licensed for proxy/router use. Account may be restricted or banned. Use at your own risk.": "⚠️ Notificare de risc: Acest furnizor folosește un abonament/sesiune OAuth care nu este licențiat oficial pentru utilizare proxy/router. Contul poate fi restricționat sau interzis. Utilizați pe propriul risc.",
- "⚠️ MITM intercepts HTTPS traffic of IDE tools (Antigravity, GitHub Copilot, Kiro) via local CA to redirect requests to your providers. May violate ToS → account ban. Use at your own risk.": "⚠️ MITM interceptează traficul HTTPS al instrumentelor IDE (Antigravity, GitHub Copilot, Kiro) prin CA locală pentru a redirecționa cererile către furnizorii dvs. Poate încălca ToS → risc de interzicere a contului. Utilizați pe propriul risc."
+ "⚠️ MITM intercepts HTTPS traffic of IDE tools (Antigravity, GitHub Copilot, Kiro) via local CA to redirect requests to your providers. May violate ToS → account ban. Use at your own risk.": "⚠️ MITM interceptează traficul HTTPS al instrumentelor IDE (Antigravity, GitHub Copilot, Kiro) prin CA locală pentru a redirecționa cererile către furnizorii dvs. Poate încălca ToS → risc de interzicere a contului. Utilizați pe propriul risc.",
+ "Endpoint is exposed without an API key.": "Endpointul este expus fără o cheie API."
}
diff --git a/public/i18n/literals/ru.json b/public/i18n/literals/ru.json
index d235ec939f4..92b9f0e78de 100644
--- a/public/i18n/literals/ru.json
+++ b/public/i18n/literals/ru.json
@@ -190,5 +190,6 @@
"Sudo Password": "Пароль sudo",
"Click to add, click again to remove. Changes are saved automatically.": "Нажмите, чтобы добавить, нажмите ещё раз, чтобы удалить. Изменения сохраняются автоматически.",
"⚠️ Risk Notice: This provider uses a subscription/OAuth session not officially licensed for proxy/router use. Account may be restricted or banned. Use at your own risk.": "⚠️ Уведомление о риске: Этот провайдер использует сессию подписки/OAuth, не имеющую официальной лицензии для использования через прокси/маршрутизатор. Аккаунт может быть ограничен или заблокирован. Используйте на свой страх и риск.",
- "⚠️ MITM intercepts HTTPS traffic of IDE tools (Antigravity, GitHub Copilot, Kiro) via local CA to redirect requests to your providers. May violate ToS → account ban. Use at your own risk.": "⚠️ MITM перехватывает HTTPS-трафик IDE-инструментов (Antigravity, GitHub Copilot, Kiro) через локальный CA для перенаправления запросов вашим провайдерам. Может нарушить ToS → риск блокировки аккаунта. Используйте на свой страх и риск."
+ "⚠️ MITM intercepts HTTPS traffic of IDE tools (Antigravity, GitHub Copilot, Kiro) via local CA to redirect requests to your providers. May violate ToS → account ban. Use at your own risk.": "⚠️ MITM перехватывает HTTPS-трафик IDE-инструментов (Antigravity, GitHub Copilot, Kiro) через локальный CA для перенаправления запросов вашим провайдерам. Может нарушить ToS → риск блокировки аккаунта. Используйте на свой страх и риск.",
+ "Endpoint is exposed without an API key.": "Эндпоинт открыт без API-ключа."
}
diff --git a/public/i18n/literals/sv.json b/public/i18n/literals/sv.json
index 21ae9c3f226..0e4c3b46be7 100644
--- a/public/i18n/literals/sv.json
+++ b/public/i18n/literals/sv.json
@@ -190,5 +190,6 @@
"Sudo Password": "Sudo-lösenord",
"Click to add, click again to remove. Changes are saved automatically.": "Klicka för att lägga till, klicka igen för att ta bort. Ändringar sparas automatiskt.",
"⚠️ Risk Notice: This provider uses a subscription/OAuth session not officially licensed for proxy/router use. Account may be restricted or banned. Use at your own risk.": "⚠️ Riskmeddelande: Denna leverantör använder en prenumerations-/OAuth-session som inte är officiellt licensierad för proxy-/routeranvändning. Kontot kan begränsas eller bannlysas. Användning sker på egen risk.",
- "⚠️ MITM intercepts HTTPS traffic of IDE tools (Antigravity, GitHub Copilot, Kiro) via local CA to redirect requests to your providers. May violate ToS → account ban. Use at your own risk.": "⚠️ MITM avlyssnar HTTPS-trafik från IDE-verktyg (Antigravity, GitHub Copilot, Kiro) via lokal CA för att omdirigera förfrågningar till dina leverantörer. Kan bryta mot ToS → risk för kontoavstängning. Användning sker på egen risk."
+ "⚠️ MITM intercepts HTTPS traffic of IDE tools (Antigravity, GitHub Copilot, Kiro) via local CA to redirect requests to your providers. May violate ToS → account ban. Use at your own risk.": "⚠️ MITM avlyssnar HTTPS-trafik från IDE-verktyg (Antigravity, GitHub Copilot, Kiro) via lokal CA för att omdirigera förfrågningar till dina leverantörer. Kan bryta mot ToS → risk för kontoavstängning. Användning sker på egen risk.",
+ "Endpoint is exposed without an API key.": "Slutpunkten är exponerad utan en API-nyckel."
}
diff --git a/public/i18n/literals/th.json b/public/i18n/literals/th.json
index ad9e3a47e2d..6169829f723 100644
--- a/public/i18n/literals/th.json
+++ b/public/i18n/literals/th.json
@@ -190,5 +190,6 @@
"Sudo Password": "รหัสผ่าน Sudo",
"Click to add, click again to remove. Changes are saved automatically.": "คลิกเพื่อเพิ่ม คลิกอีกครั้งเพื่อลบ การเปลี่ยนแปลงจะถูกบันทึกโดยอัตโนมัติ",
"⚠️ Risk Notice: This provider uses a subscription/OAuth session not officially licensed for proxy/router use. Account may be restricted or banned. Use at your own risk.": "⚠️ ประกาศความเสี่ยง: ผู้ให้บริการนี้ใช้เซสชันสมัครสมาชิก/OAuth ที่ไม่ได้รับอนุญาตอย่างเป็นทางการสำหรับการใช้งานพร็อกซี/เราเตอร์ บัญชีอาจถูกจำกัดหรือถูกแบน ใช้งานด้วยความเสี่ยงของคุณเอง",
- "⚠️ MITM intercepts HTTPS traffic of IDE tools (Antigravity, GitHub Copilot, Kiro) via local CA to redirect requests to your providers. May violate ToS → account ban. Use at your own risk.": "⚠️ MITM ดักจับการรับส่งข้อมูล HTTPS ของเครื่องมือ IDE (Antigravity, GitHub Copilot, Kiro) ผ่าน CA ท้องถิ่นเพื่อเปลี่ยนเส้นทางคำขอไปยังผู้ให้บริการของคุณ อาจละเมิด ToS → เสี่ยงถูกแบนบัญชี ใช้งานด้วยความเสี่ยงของคุณเอง"
+ "⚠️ MITM intercepts HTTPS traffic of IDE tools (Antigravity, GitHub Copilot, Kiro) via local CA to redirect requests to your providers. May violate ToS → account ban. Use at your own risk.": "⚠️ MITM ดักจับการรับส่งข้อมูล HTTPS ของเครื่องมือ IDE (Antigravity, GitHub Copilot, Kiro) ผ่าน CA ท้องถิ่นเพื่อเปลี่ยนเส้นทางคำขอไปยังผู้ให้บริการของคุณ อาจละเมิด ToS → เสี่ยงถูกแบนบัญชี ใช้งานด้วยความเสี่ยงของคุณเอง",
+ "Endpoint is exposed without an API key.": "เอนด์พอยต์เปิดให้เข้าถึงโดยไม่มีคีย์ API"
}
diff --git a/public/i18n/literals/tl.json b/public/i18n/literals/tl.json
index bb7939d8a4c..51af4e24124 100644
--- a/public/i18n/literals/tl.json
+++ b/public/i18n/literals/tl.json
@@ -190,5 +190,6 @@
"Sudo Password": "Sudo Password",
"Click to add, click again to remove. Changes are saved automatically.": "I-click para idagdag, i-click muli para alisin. Ang mga pagbabago ay awtomatikong nase-save.",
"⚠️ Risk Notice: This provider uses a subscription/OAuth session not officially licensed for proxy/router use. Account may be restricted or banned. Use at your own risk.": "⚠️ Babala sa Panganib: Ang provider na ito ay gumagamit ng subscription/OAuth session na hindi opisyal na lisensyado para sa proxy/router na paggamit. Maaaring marestrikta o ma-ban ang account. Gamitin sa sarili mong panganib.",
- "⚠️ MITM intercepts HTTPS traffic of IDE tools (Antigravity, GitHub Copilot, Kiro) via local CA to redirect requests to your providers. May violate ToS → account ban. Use at your own risk.": "⚠️ Hinaharang ng MITM ang HTTPS traffic ng mga IDE tool (Antigravity, GitHub Copilot, Kiro) sa pamamagitan ng lokal na CA upang i-redirect ang mga kahilingan sa iyong mga provider. Maaaring lumabag sa ToS → panganib ng pag-ban sa account. Gamitin sa sarili mong panganib."
+ "⚠️ MITM intercepts HTTPS traffic of IDE tools (Antigravity, GitHub Copilot, Kiro) via local CA to redirect requests to your providers. May violate ToS → account ban. Use at your own risk.": "⚠️ Hinaharang ng MITM ang HTTPS traffic ng mga IDE tool (Antigravity, GitHub Copilot, Kiro) sa pamamagitan ng lokal na CA upang i-redirect ang mga kahilingan sa iyong mga provider. Maaaring lumabag sa ToS → panganib ng pag-ban sa account. Gamitin sa sarili mong panganib.",
+ "Endpoint is exposed without an API key.": "Nakalantad ang endpoint nang walang API key."
}
diff --git a/public/i18n/literals/tr.json b/public/i18n/literals/tr.json
index 0a8556e3766..ac4042aaf3c 100644
--- a/public/i18n/literals/tr.json
+++ b/public/i18n/literals/tr.json
@@ -190,5 +190,6 @@
"Sudo Password": "Sudo Parolası",
"Click to add, click again to remove. Changes are saved automatically.": "Eklemek için tıklayın, kaldırmak için tekrar tıklayın. Değişiklikler otomatik olarak kaydedilir.",
"⚠️ Risk Notice: This provider uses a subscription/OAuth session not officially licensed for proxy/router use. Account may be restricted or banned. Use at your own risk.": "⚠️ Risk Bildirimi: Bu sağlayıcı, proxy/yönlendirici kullanımı için resmi olarak lisanslı olmayan bir abonelik/OAuth oturumu kullanır. Hesap kısıtlanabilir veya yasaklanabilir. Kendi sorumluluğunuzda kullanın.",
- "⚠️ MITM intercepts HTTPS traffic of IDE tools (Antigravity, GitHub Copilot, Kiro) via local CA to redirect requests to your providers. May violate ToS → account ban. Use at your own risk.": "⚠️ MITM, isteklerinizi sağlayıcılarınıza yönlendirmek için yerel CA aracılığıyla IDE araçlarının (Antigravity, GitHub Copilot, Kiro) HTTPS trafiğini engeller. ToS'u ihlal edebilir → hesap yasaklama riski. Kendi sorumluluğunuzda kullanın."
+ "⚠️ MITM intercepts HTTPS traffic of IDE tools (Antigravity, GitHub Copilot, Kiro) via local CA to redirect requests to your providers. May violate ToS → account ban. Use at your own risk.": "⚠️ MITM, isteklerinizi sağlayıcılarınıza yönlendirmek için yerel CA aracılığıyla IDE araçlarının (Antigravity, GitHub Copilot, Kiro) HTTPS trafiğini engeller. ToS'u ihlal edebilir → hesap yasaklama riski. Kendi sorumluluğunuzda kullanın.",
+ "Endpoint is exposed without an API key.": "Uç nokta API anahtarı olmadan açıkta."
}
diff --git a/public/i18n/literals/uk.json b/public/i18n/literals/uk.json
index 51e26440cf5..e238ad2b8fd 100644
--- a/public/i18n/literals/uk.json
+++ b/public/i18n/literals/uk.json
@@ -190,5 +190,6 @@
"Sudo Password": "Пароль Sudo",
"Click to add, click again to remove. Changes are saved automatically.": "Натисніть, щоб додати, натисніть ще раз, щоб видалити. Зміни зберігаються автоматично.",
"⚠️ Risk Notice: This provider uses a subscription/OAuth session not officially licensed for proxy/router use. Account may be restricted or banned. Use at your own risk.": "⚠️ Сповіщення про ризик: Цей провайдер використовує сесію підписки/OAuth, яка офіційно не ліцензована для використання через проксі/маршрутизатор. Обліковий запис може бути обмежений або заблокований. Використовуйте на свій страх і ризик.",
- "⚠️ MITM intercepts HTTPS traffic of IDE tools (Antigravity, GitHub Copilot, Kiro) via local CA to redirect requests to your providers. May violate ToS → account ban. Use at your own risk.": "⚠️ MITM перехоплює HTTPS-трафік IDE-інструментів (Antigravity, GitHub Copilot, Kiro) через локальний CA для перенаправлення запитів до ваших провайдерів. Може порушити ToS → ризик блокування облікового запису. Використовуйте на свій страх і ризик."
+ "⚠️ MITM intercepts HTTPS traffic of IDE tools (Antigravity, GitHub Copilot, Kiro) via local CA to redirect requests to your providers. May violate ToS → account ban. Use at your own risk.": "⚠️ MITM перехоплює HTTPS-трафік IDE-інструментів (Antigravity, GitHub Copilot, Kiro) через локальний CA для перенаправлення запитів до ваших провайдерів. Може порушити ToS → ризик блокування облікового запису. Використовуйте на свій страх і ризик.",
+ "Endpoint is exposed without an API key.": "Кінцеву точку відкрито без API-ключа."
}
diff --git a/public/i18n/literals/ur.json b/public/i18n/literals/ur.json
index bb1c09bab86..e59217554b6 100644
--- a/public/i18n/literals/ur.json
+++ b/public/i18n/literals/ur.json
@@ -190,5 +190,6 @@
"Sudo Password": "Sudo پاس ورڈ",
"Click to add, click again to remove. Changes are saved automatically.": "شامل کرنے کے لیے کلک کریں، ہٹانے کے لیے دوبارہ کلک کریں۔ تبدیلیاں خودکار طور پر محفوظ ہو جاتی ہیں۔",
"⚠️ Risk Notice: This provider uses a subscription/OAuth session not officially licensed for proxy/router use. Account may be restricted or banned. Use at your own risk.": "⚠️ خطرے کا نوٹس: یہ فراہم کنندہ ایک سبسکرپشن/OAuth سیشن استعمال کرتا ہے جو پراکسی/راؤٹر استعمال کے لیے سرکاری طور پر لائسنس یافتہ نہیں ہے۔ اکاؤنٹ محدود یا پابند ہو سکتا ہے۔ اپنے خطرے پر استعمال کریں۔",
- "⚠️ MITM intercepts HTTPS traffic of IDE tools (Antigravity, GitHub Copilot, Kiro) via local CA to redirect requests to your providers. May violate ToS → account ban. Use at your own risk.": "⚠️ MITM آپ کے فراہم کنندگان کو درخواستوں کو ری ڈائریکٹ کرنے کے لیے مقامی CA کے ذریعے IDE ٹولز (Antigravity, GitHub Copilot, Kiro) کے HTTPS ٹریفک کو روکتا ہے۔ ToS کی خلاف ورزی کر سکتا ہے → اکاؤنٹ پابندی کا خطرہ۔ اپنے خطرے پر استعمال کریں۔"
+ "⚠️ MITM intercepts HTTPS traffic of IDE tools (Antigravity, GitHub Copilot, Kiro) via local CA to redirect requests to your providers. May violate ToS → account ban. Use at your own risk.": "⚠️ MITM آپ کے فراہم کنندگان کو درخواستوں کو ری ڈائریکٹ کرنے کے لیے مقامی CA کے ذریعے IDE ٹولز (Antigravity, GitHub Copilot, Kiro) کے HTTPS ٹریفک کو روکتا ہے۔ ToS کی خلاف ورزی کر سکتا ہے → اکاؤنٹ پابندی کا خطرہ۔ اپنے خطرے پر استعمال کریں۔",
+ "Endpoint is exposed without an API key.": "اینڈ پوائنٹ API کلید کے بغیر بے نقاب ہے۔"
}
diff --git a/public/i18n/literals/vi.json b/public/i18n/literals/vi.json
index 1b03b6e491e..5358d068176 100644
--- a/public/i18n/literals/vi.json
+++ b/public/i18n/literals/vi.json
@@ -190,5 +190,6 @@
"Sudo Password": "Mật khẩu Sudo",
"Click to add, click again to remove. Changes are saved automatically.": "Nhấp để thêm, nhấp lại để xóa. Thay đổi được lưu tự động.",
"⚠️ Risk Notice: This provider uses a subscription/OAuth session not officially licensed for proxy/router use. Account may be restricted or banned. Use at your own risk.": "⚠️ Cảnh báo rủi ro: Provider này sử dụng subscription/OAuth không được cấp phép chính thức cho mục đích proxy/router. Tài khoản có thể bị hạn chế hoặc cấm. Người dùng tự chịu trách nhiệm.",
- "⚠️ MITM intercepts HTTPS traffic of IDE tools (Antigravity, GitHub Copilot, Kiro) via local CA to redirect requests to your providers. May violate ToS → account ban. Use at your own risk.": "⚠️ MITM chặn lưu lượng HTTPS của các IDE tools (Antigravity, GitHub Copilot, Kiro) qua CA cục bộ để chuyển hướng request đến providers của bạn. Có thể vi phạm ToS → rủi ro bị ban tài khoản. Sử dụng với rủi ro của bạn."
+ "⚠️ MITM intercepts HTTPS traffic of IDE tools (Antigravity, GitHub Copilot, Kiro) via local CA to redirect requests to your providers. May violate ToS → account ban. Use at your own risk.": "⚠️ MITM chặn lưu lượng HTTPS của các IDE tools (Antigravity, GitHub Copilot, Kiro) qua CA cục bộ để chuyển hướng request đến providers của bạn. Có thể vi phạm ToS → rủi ro bị ban tài khoản. Sử dụng với rủi ro của bạn.",
+ "Endpoint is exposed without an API key.": "Endpoint đang mở mà không có API key."
}
diff --git a/public/i18n/literals/zh-CN.json b/public/i18n/literals/zh-CN.json
index a3282382028..7b3deb8170a 100644
--- a/public/i18n/literals/zh-CN.json
+++ b/public/i18n/literals/zh-CN.json
@@ -197,6 +197,8 @@
"Expose your local 9Router to the internet. No port forwarding, no static IP needed. Share endpoint URL with your team or use it in Cursor, Cline, and other AI tools from anywhere.": "将您本地的 9Router 暴露到互联网。无需端口转发,无需静态 IP。与您的团队共享端点 URL 或从任何地方在 Cursor、Cline 和其他 AI 工具中使用它。",
"Factory Droid - Manual Configuration": "Factory Droid - 手动配置",
"Factory Droid CLI not installed": "Factory Droid CLI 未安装",
+ "Fetch Qoder Models": "获取 Qoder 模型",
+ "Fetching...": "获取中...",
"Failed to load usage statistics.": "无法加载使用情况统计信息。",
"Features": "功能特性",
"Flush Interval (ms)": "刷新间隔(毫秒)",
@@ -326,6 +328,7 @@
"Paste refresh token from Kiro IDE.": "从 Kiro IDE 粘贴刷新令牌。",
"Paused": "已暂停",
"Please add and connect providers first to configure CLI tools.": "请先添加并连接提供商以配置 CLI 工具。",
+ "Please add an active Qoder connection first": "请先添加一个活跃的 Qoder 连接",
"Please copy the URL from the address bar and paste it in the application.": "请复制地址栏中的 URL 并将其粘贴到应用程序中。",
"Please enter a Proxy URL to test": "请输入代理 URL 进行测试",
"Please install Claude CLI to use this feature.": "请安装 Claude CLI 才能使用此功能。",
@@ -764,5 +767,6 @@
"Click to add, click again to remove. Changes are saved automatically.": "点击添加,再次点击删除。更改将自动保存。",
"Close": "关闭",
"⚠️ Risk Notice: This provider uses a subscription/OAuth session not officially licensed for proxy/router use. Account may be restricted or banned. Use at your own risk.": "⚠️ 风险提示:此提供商使用的订阅/OAuth 会话未获官方授权用于代理/路由器使用。账户可能被限制或封禁。使用风险自负。",
- "⚠️ MITM intercepts HTTPS traffic of IDE tools (Antigravity, GitHub Copilot, Kiro) via local CA to redirect requests to your providers. May violate ToS → account ban. Use at your own risk.": "⚠️ MITM 通过本地 CA 拦截 IDE 工具(Antigravity、GitHub Copilot、Kiro)的 HTTPS 流量,将请求重定向到您的提供商。可能违反 ToS → 账户封禁风险。使用风险自负。"
+ "⚠️ MITM intercepts HTTPS traffic of IDE tools (Antigravity, GitHub Copilot, Kiro) via local CA to redirect requests to your providers. May violate ToS → account ban. Use at your own risk.": "⚠️ MITM 通过本地 CA 拦截 IDE 工具(Antigravity、GitHub Copilot、Kiro)的 HTTPS 流量,将请求重定向到您的提供商。可能违反 ToS → 账户封禁风险。使用风险自负。",
+ "Endpoint is exposed without an API key.": "端点未设置 API 密钥即对外暴露。"
}
diff --git a/public/i18n/literals/zh-TW.json b/public/i18n/literals/zh-TW.json
index da44e1a20c3..ea9183ebdf4 100644
--- a/public/i18n/literals/zh-TW.json
+++ b/public/i18n/literals/zh-TW.json
@@ -190,5 +190,6 @@
"Sudo Password": "Sudo 密碼",
"Click to add, click again to remove. Changes are saved automatically.": "點擊新增,再次點擊移除。變更將自動儲存。",
"⚠️ Risk Notice: This provider uses a subscription/OAuth session not officially licensed for proxy/router use. Account may be restricted or banned. Use at your own risk.": "⚠️ 風險提示:此提供商使用的訂閱/OAuth 工作階段未獲官方授權用於代理/路由器使用。帳戶可能被限制或封禁。使用風險自負。",
- "⚠️ MITM intercepts HTTPS traffic of IDE tools (Antigravity, GitHub Copilot, Kiro) via local CA to redirect requests to your providers. May violate ToS → account ban. Use at your own risk.": "⚠️ MITM 透過本地 CA 攔截 IDE 工具(Antigravity、GitHub Copilot、Kiro)的 HTTPS 流量,將請求重新導向到您的提供商。可能違反 ToS → 帳戶封禁風險。使用風險自負。"
+ "⚠️ MITM intercepts HTTPS traffic of IDE tools (Antigravity, GitHub Copilot, Kiro) via local CA to redirect requests to your providers. May violate ToS → account ban. Use at your own risk.": "⚠️ MITM 透過本地 CA 攔截 IDE 工具(Antigravity、GitHub Copilot、Kiro)的 HTTPS 流量,將請求重新導向到您的提供商。可能違反 ToS → 帳戶封禁風險。使用風險自負。",
+ "Endpoint is exposed without an API key.": "端點未設定 API 金鑰即對外暴露。"
}
diff --git a/src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.js b/src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.js
index 2f55ca92bc1..1fe6c8e5f2e 100644
--- a/src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.js
+++ b/src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.js
@@ -4,6 +4,10 @@ import { useState, useEffect, useRef, useCallback } from "react";
import PropTypes from "prop-types";
import { Card, Button, Input, Modal, CardSkeleton, Toggle, ConfirmModal } from "@/shared/components";
import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard";
+import { getCurrentLocale, onLocaleChange } from "@/i18n/runtime";
+
+// Locales that unlock wenyan (classical Chinese) caveman levels
+const WENYAN_LOCALES = ["zh-CN", "zh-TW"];
const TUNNEL_BENEFITS = [
{ icon: "public", title: "Access Anywhere", desc: "Use your API from any network" },
@@ -53,6 +57,9 @@ const CAVEMAN_LEVELS = [
{ id: "lite", label: "Lite", desc: "Drop filler, keep grammar" },
{ id: "full", label: "Full", desc: "Drop articles, fragments OK" },
{ id: "ultra", label: "Ultra", desc: "Telegraphic, max compression" },
+ { id: "wenyan-lite", label: "文 Lite", desc: "Classical Chinese, light compression", wenyan: true },
+ { id: "wenyan", label: "文 Full", desc: "Maximum 文言文, 80-90% reduction", wenyan: true },
+ { id: "wenyan-ultra", label: "文 Ultra", desc: "Extreme classical compression", wenyan: true },
];
export default function APIPageClient({ machineId }) {
const [keys, setKeys] = useState([]);
@@ -74,6 +81,7 @@ export default function APIPageClient({ machineId }) {
const [rtkEnabled, setRtkEnabledState] = useState(true);
const [cavemanEnabled, setCavemanEnabled] = useState(false);
const [cavemanLevel, setCavemanLevel] = useState("full");
+ const [locale, setLocale] = useState("en");
// Email notification settings
const [emailSettings, setEmailSettings] = useState({
@@ -142,6 +150,33 @@ export default function APIPageClient({ machineId }) {
// API key visibility toggle state
const [visibleKeys, setVisibleKeys] = useState(new Set());
+ // Client-side local/remote detection (UI hint only, not a security gate)
+ const [isRemoteHost, setIsRemoteHost] = useState(false);
+ useEffect(() => {
+ if (typeof window !== "undefined")
+ setIsRemoteHost(!["localhost", "127.0.0.1", "::1"].includes(window.location.hostname));
+ }, []);
+
+ // Track app UI locale to gate wenyan caveman levels
+ useEffect(() => {
+ setLocale(getCurrentLocale());
+ return onLocaleChange(() => setLocale(getCurrentLocale()));
+ }, []);
+
+ const isWenyanLocale = WENYAN_LOCALES.includes(locale);
+ const visibleCavemanLevels = isWenyanLocale
+ ? CAVEMAN_LEVELS
+ : CAVEMAN_LEVELS.filter((lvl) => !lvl.wenyan);
+
+ // Reset wenyan level to "ultra" when leaving a Chinese locale
+ useEffect(() => {
+ const current = CAVEMAN_LEVELS.find((lvl) => lvl.id === cavemanLevel);
+ if (current?.wenyan && !isWenyanLocale) {
+ setCavemanLevel("ultra");
+ patchSetting({ cavemanLevel: "ultra" });
+ }
+ }, [isWenyanLocale, cavemanLevel]);
+
const { copied, copy } = useCopyToClipboard();
// Security gate: block remote exposure while dashboard uses default password or login is off.
@@ -1232,21 +1267,26 @@ export default function APIPageClient({ machineId }) {