From 8af638e7e20c645b22d96da5e30665e8e9cbf6ad Mon Sep 17 00:00:00 2001 From: Marius Date: Tue, 12 May 2026 09:27:32 +0200 Subject: [PATCH] Fix expired Codex auth recovery (#10136) * fix: recover expired Codex auth * fix: minimize Codex auth shared changes * fix: format Codex auth prompt --- .changeset/codex-chatgpt-reauth.md | 6 + .../kilo-vscode/tests/unit/errorUtils.test.ts | 21 +++ .../src/components/chat/ErrorDisplay.tsx | 53 ++++++++ .../kilo-vscode/webview-ui/src/i18n/ar.ts | 5 + .../kilo-vscode/webview-ui/src/i18n/br.ts | 5 + .../kilo-vscode/webview-ui/src/i18n/bs.ts | 5 + .../kilo-vscode/webview-ui/src/i18n/da.ts | 5 + .../kilo-vscode/webview-ui/src/i18n/de.ts | 5 + .../kilo-vscode/webview-ui/src/i18n/en.ts | 5 + .../kilo-vscode/webview-ui/src/i18n/es.ts | 5 + .../kilo-vscode/webview-ui/src/i18n/fr.ts | 5 + .../kilo-vscode/webview-ui/src/i18n/ja.ts | 5 + .../kilo-vscode/webview-ui/src/i18n/ko.ts | 5 + .../kilo-vscode/webview-ui/src/i18n/nl.ts | 5 + .../kilo-vscode/webview-ui/src/i18n/no.ts | 5 + .../kilo-vscode/webview-ui/src/i18n/pl.ts | 5 + .../kilo-vscode/webview-ui/src/i18n/ru.ts | 5 + .../kilo-vscode/webview-ui/src/i18n/th.ts | 5 + .../kilo-vscode/webview-ui/src/i18n/tr.ts | 5 + .../kilo-vscode/webview-ui/src/i18n/uk.ts | 5 + .../kilo-vscode/webview-ui/src/i18n/zh.ts | 4 + .../kilo-vscode/webview-ui/src/i18n/zht.ts | 4 + .../webview-ui/src/utils/errorUtils.ts | 19 +++ .../src/kilocode/provider/codex-refresh.ts | 102 +++++++++++++++ packages/opencode/src/plugin/codex.ts | 19 +-- packages/opencode/src/session/message-v2.ts | 9 ++ .../test/kilocode/codex-auth-refresh.test.ts | 123 ++++++++++++++++++ 27 files changed, 430 insertions(+), 15 deletions(-) create mode 100644 .changeset/codex-chatgpt-reauth.md create mode 100644 packages/opencode/src/kilocode/provider/codex-refresh.ts create mode 100644 packages/opencode/test/kilocode/codex-auth-refresh.test.ts diff --git a/.changeset/codex-chatgpt-reauth.md b/.changeset/codex-chatgpt-reauth.md new file mode 100644 index 00000000000..50ba4d3fd1c --- /dev/null +++ b/.changeset/codex-chatgpt-reauth.md @@ -0,0 +1,6 @@ +--- +"kilo-code": patch +"@kilocode/cli": patch +--- + +Show ChatGPT sign-in again when Codex authentication expires. diff --git a/packages/kilo-vscode/tests/unit/errorUtils.test.ts b/packages/kilo-vscode/tests/unit/errorUtils.test.ts index 4667a1ba3db..aef7ae2dcb5 100644 --- a/packages/kilo-vscode/tests/unit/errorUtils.test.ts +++ b/packages/kilo-vscode/tests/unit/errorUtils.test.ts @@ -3,6 +3,7 @@ import type { AssistantMessage } from "@kilocode/sdk/v2" import { unwrapError, parseAssistantError, + parseProviderAuthError, isUnauthorizedPaidModelError, isUnauthorizedPromotionLimitError, } from "../../webview-ui/src/utils/errorUtils" @@ -97,6 +98,26 @@ describe("parseAssistantError", () => { }) }) +describe("parseProviderAuthError", () => { + it("extracts provider auth errors", () => { + const error: AssistantError = { + name: "ProviderAuthError", + data: { providerID: "openai", message: "Sign in again" }, + } + + expect(parseProviderAuthError(error)).toEqual({ providerID: "openai", message: "Sign in again" }) + }) + + it("returns null for non-provider-auth errors", () => { + const error: AssistantError = { + name: "APIError", + data: { statusCode: 401, message: "Unauthorized", isRetryable: false }, + } + + expect(parseProviderAuthError(error)).toBeNull() + }) +}) + describe("isUnauthorizedPaidModelError", () => { it("returns true for 401 + PAID_MODEL_AUTH_REQUIRED", () => { expect(isUnauthorizedPaidModelError({ statusCode: 401, code: "PAID_MODEL_AUTH_REQUIRED" })).toBe(true) diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/ErrorDisplay.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/ErrorDisplay.tsx index e84d4c10f02..ef600bccfc3 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/ErrorDisplay.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/ErrorDisplay.tsx @@ -1,16 +1,20 @@ import { Component, createMemo, Switch, Match } from "solid-js" import { Card } from "@kilocode/kilo-ui/card" import { Collapsible } from "@kilocode/kilo-ui/collapsible" +import { useDialog } from "@kilocode/kilo-ui/context/dialog" import { ErrorDetails } from "@kilocode/kilo-ui/error-details" import { Button } from "@kilocode/kilo-ui/button" import type { AssistantMessage } from "@kilocode/sdk/v2" import { useLanguage } from "../../context/language" +import { useProvider } from "../../context/provider" import { unwrapError, parseAssistantError, + parseProviderAuthError, isUnauthorizedPaidModelError, isUnauthorizedPromotionLimitError, } from "../../utils/errorUtils" +import ProviderConnectDialog from "../settings/ProviderConnectDialog" export interface ErrorDisplayProps { error: NonNullable @@ -19,7 +23,28 @@ export interface ErrorDisplayProps { export const ErrorDisplay: Component = (props) => { const { t } = useLanguage() + const dialog = useDialog() + const provider = useProvider() const parsed = createMemo(() => parseAssistantError(props.error)) + const auth = createMemo(() => parseProviderAuthError(props.error)) + const authProvider = createMemo(() => { + const err = auth() + if (!err) return + return provider.providers()[err.providerID] + }) + const canAuth = createMemo(() => { + const err = auth() + if (!err || !authProvider()) return false + return (provider.authMethods()[err.providerID] ?? []).length > 0 + }) + const oauth = createMemo(() => { + const err = auth() + if (!err) return false + return ( + err.providerID === "openai" && + (provider.authMethods()[err.providerID] ?? []).some((method) => method.type === "oauth") + ) + }) const errorText = createMemo(() => { const msg = props.error.data?.message @@ -28,6 +53,12 @@ export const ErrorDisplay: Component = (props) => { return unwrapError(String(msg)) }) + function connectProvider() { + const err = auth() + if (!err) return + dialog.show(() => ) + } + return ( = (props) => { + +
+
+ + + {oauth() + ? t("error.providerAuth.chatgpt.title") + : t("error.providerAuth.title", { provider: authProvider()?.name ?? auth()?.providerID ?? "provider" })} + +
+

+ {oauth() + ? t("error.providerAuth.chatgpt.description") + : t("error.providerAuth.description", { + provider: authProvider()?.name ?? auth()?.providerID ?? "provider", + })} +

+ +
+
) } diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts index 5e9e559b4be..cb292f16288 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts @@ -462,6 +462,11 @@ export const dict = { "error.promotionLimit.description": "سجّل مجانًا للمتابعة واستكشاف أكثر من 500 نموذج. يستغرق دقيقتين، بدون بطاقة ائتمان. أو عُد لاحقًا.", "error.promotionLimit.action": "التسجيل", + "error.providerAuth.title": "تم تسجيل خروجك من {{provider}}", + "error.providerAuth.description": "أعد الاتصال بـ {{provider}}، ثم أرسل رسالتك مرة أخرى.", + "error.providerAuth.chatgpt.title": "تسجيل الدخول باستخدام ChatGPT مرة أخرى", + "error.providerAuth.chatgpt.description": + "سجل الدخول باستخدام ChatGPT مرة أخرى، ثم أرسل رسالتك مرة أخرى لمواصلة استخدام نماذج Codex.", "error.chain.unknown": "خطأ غير معروف", "error.chain.causedBy": "بسبب:", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/br.ts b/packages/kilo-vscode/webview-ui/src/i18n/br.ts index afb0af7903d..c7dd24dc01c 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/br.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/br.ts @@ -465,6 +465,11 @@ export const dict = { "error.promotionLimit.description": "Cadastre-se gratuitamente para continuar e explorar mais de 500 modelos. Leva 2 minutos, sem cartão de crédito. Ou volte mais tarde.", "error.promotionLimit.action": "Cadastrar-se", + "error.providerAuth.title": "{{provider}} desconectou você", + "error.providerAuth.description": "Reconecte o {{provider}} e envie sua mensagem novamente.", + "error.providerAuth.chatgpt.title": "OpenAI desconectou você", + "error.providerAuth.chatgpt.description": + "Faça login no ChatGPT novamente e envie sua mensagem de novo para continuar usando os modelos Codex.", "error.chain.unknown": "Erro desconhecido", "error.chain.causedBy": "Causado por:", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts index e80ad6075ae..461584d1240 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts @@ -469,6 +469,11 @@ export const dict = { "error.promotionLimit.description": "Registrujte se besplatno da nastavite i istražite preko 500 modela. Traje 2 minute, bez kreditne kartice. Ili se vratite kasnije.", "error.promotionLimit.action": "Registracija", + "error.providerAuth.title": "{{provider}} vas je odjavio", + "error.providerAuth.description": "Ponovo se povežite sa {{provider}}, a zatim ponovo pošaljite poruku.", + "error.providerAuth.chatgpt.title": "OpenAI vas je odjavio", + "error.providerAuth.chatgpt.description": + "Ponovo se prijavite na ChatGPT, a zatim ponovo pošaljite poruku da nastavite koristiti Codex modele.", "error.chain.unknown": "Nepoznata greška", "error.chain.causedBy": "Uzrok:", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/da.ts b/packages/kilo-vscode/webview-ui/src/i18n/da.ts index 419fa7e6784..77996e22001 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/da.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/da.ts @@ -466,6 +466,11 @@ export const dict = { "error.promotionLimit.description": "Tilmeld dig gratis for at fortsætte og udforske over 500 modeller. Tager 2 minutter, intet kreditkort nødvendigt. Eller kom tilbage senere.", "error.promotionLimit.action": "Tilmeld dig", + "error.providerAuth.title": "{{provider}} har logget dig af", + "error.providerAuth.description": "Forbind til {{provider}} igen, og send din besked på ny.", + "error.providerAuth.chatgpt.title": "OpenAI har logget dig af", + "error.providerAuth.chatgpt.description": + "Log ind med ChatGPT igen, og send din besked på ny for at fortsætte med at bruge Codex-modeller.", "error.chain.unknown": "Ukendt fejl", "error.chain.causedBy": "Forårsaget af:", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/de.ts b/packages/kilo-vscode/webview-ui/src/i18n/de.ts index ddebf63ce2f..231bab9d25a 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/de.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/de.ts @@ -471,6 +471,11 @@ export const dict = { "error.promotionLimit.description": "Registriere dich kostenlos, um weiterzumachen und über 500 Modelle zu entdecken. Dauert 2 Minuten, keine Kreditkarte nötig. Oder komm später wieder.", "error.promotionLimit.action": "Registrieren", + "error.providerAuth.title": "{{provider}} hat Sie abgemeldet", + "error.providerAuth.description": "Verbinden Sie {{provider}} erneut und senden Sie Ihre Nachricht noch einmal.", + "error.providerAuth.chatgpt.title": "OpenAI hat Sie abgemeldet", + "error.providerAuth.chatgpt.description": + "Melden Sie sich erneut bei ChatGPT an und senden Sie Ihre Nachricht noch einmal, um weiterhin Codex-Modelle zu verwenden.", "error.chain.unknown": "Unbekannter Fehler", "error.chain.causedBy": "Verursacht durch:", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/en.ts b/packages/kilo-vscode/webview-ui/src/i18n/en.ts index 412d8a6ed65..9770f4753e4 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/en.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/en.ts @@ -467,6 +467,11 @@ export const dict = { "error.promotionLimit.description": "Sign up for free to continue and explore 500 other models. Takes 2 minutes, no credit card required. Or come back later.", "error.promotionLimit.action": "Sign Up", + "error.providerAuth.title": "{{provider}} signed you out", + "error.providerAuth.description": "Reconnect {{provider}}, then send your message again.", + "error.providerAuth.chatgpt.title": "OpenAI signed you out", + "error.providerAuth.chatgpt.description": + "Sign in with ChatGPT again, then send your message again to keep using Codex models.", "error.chain.unknown": "Unknown error", "error.chain.causedBy": "Caused by:", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/es.ts b/packages/kilo-vscode/webview-ui/src/i18n/es.ts index 7e36eaef358..626627dadfa 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/es.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/es.ts @@ -469,6 +469,11 @@ export const dict = { "error.promotionLimit.description": "Regístrate gratis para continuar y explorar más de 500 modelos. Solo 2 minutos, sin tarjeta de crédito. O vuelve más tarde.", "error.promotionLimit.action": "Registrarse", + "error.providerAuth.title": "{{provider}} cerró tu sesión", + "error.providerAuth.description": "Vuelve a conectar {{provider}} y envía tu mensaje de nuevo.", + "error.providerAuth.chatgpt.title": "OpenAI cerró tu sesión", + "error.providerAuth.chatgpt.description": + "Vuelve a iniciar sesión con ChatGPT y envía tu mensaje de nuevo para seguir usando los modelos Codex.", "error.chain.unknown": "Error desconocido", "error.chain.causedBy": "Causado por:", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts index 81eb548966f..159e32340c2 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts @@ -473,6 +473,11 @@ export const dict = { "error.promotionLimit.description": "Inscrivez-vous gratuitement pour continuer et explorer plus de 500 modèles. 2 minutes, sans carte bancaire. Ou revenez plus tard.", "error.promotionLimit.action": "S'inscrire", + "error.providerAuth.title": "{{provider}} vous a déconnecté", + "error.providerAuth.description": "Reconnectez {{provider}}, puis renvoyez votre message.", + "error.providerAuth.chatgpt.title": "OpenAI vous a déconnecté", + "error.providerAuth.chatgpt.description": + "Reconnectez-vous à ChatGPT, puis renvoyez votre message pour continuer à utiliser les modèles Codex.", "error.chain.unknown": "Erreur inconnue", "error.chain.causedBy": "Causé par :", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts index b46b36598c9..a6a13784a65 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts @@ -463,6 +463,11 @@ export const dict = { "error.promotionLimit.description": "無料でサインアップして、500以上のモデルを探索しましょう。2分で完了、クレジットカード不要。または後でお戻りください。", "error.promotionLimit.action": "サインアップ", + "error.providerAuth.title": "{{provider}} からログアウトしました", + "error.providerAuth.description": "{{provider}} に再接続してから、メッセージを再送信してください。", + "error.providerAuth.chatgpt.title": "OpenAI からログアウトしました", + "error.providerAuth.chatgpt.description": + "Codex モデルを引き続き使用するには、ChatGPT に再度ログインしてから、メッセージを再送信してください。", "error.chain.unknown": "不明なエラー", "error.chain.causedBy": "原因:", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts index 41171ac67da..2367e50cc87 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts @@ -465,6 +465,11 @@ export const dict = { "error.promotionLimit.description": "무료로 가입하여 500개 이상의 모델을 탐색하세요. 2분이면 완료, 신용카드 불필요. 또는 나중에 다시 오세요.", "error.promotionLimit.action": "가입하기", + "error.providerAuth.title": "{{provider}}에서 로그아웃되었습니다", + "error.providerAuth.description": "{{provider}}에 다시 연결한 후 메시지를 다시 보내주세요.", + "error.providerAuth.chatgpt.title": "OpenAI에서 로그아웃되었습니다", + "error.providerAuth.chatgpt.description": + "ChatGPT에 다시 로그인한 후 메시지를 다시 보내 Codex 모델을 계속 사용하세요.", "error.chain.unknown": "알 수 없는 오류", "error.chain.causedBy": "원인:", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/nl.ts b/packages/kilo-vscode/webview-ui/src/i18n/nl.ts index 55244598e57..6c93dc790a1 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/nl.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/nl.ts @@ -469,6 +469,11 @@ export const dict = { "error.promotionLimit.description": "Registreer je gratis om door te gaan en 500 andere modellen te ontdekken. Duurt 2 minuten, geen creditcard vereist. Of kom later terug.", "error.promotionLimit.action": "Registreren", + "error.providerAuth.title": "{{provider}} heeft je afgemeld", + "error.providerAuth.description": "Verbind opnieuw met {{provider}} en stuur je bericht nog een keer.", + "error.providerAuth.chatgpt.title": "OpenAI heeft je afgemeld", + "error.providerAuth.chatgpt.description": + "Meld je opnieuw aan bij ChatGPT en stuur je bericht nog een keer om Codex-modellen te blijven gebruiken.", "error.chain.unknown": "Onbekende fout", "error.chain.causedBy": "Veroorzaakt door:", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/no.ts b/packages/kilo-vscode/webview-ui/src/i18n/no.ts index 9c7c00a004c..a7b08921e48 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/no.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/no.ts @@ -470,6 +470,11 @@ export const dict = { "error.promotionLimit.description": "Registrer deg gratis for å fortsette og utforske over 500 modeller. Tar 2 minutter, ingen kredittkort nødvendig. Eller kom tilbake senere.", "error.promotionLimit.action": "Registrer deg", + "error.providerAuth.title": "{{provider}} logget deg ut", + "error.providerAuth.description": "Koble til {{provider}} på nytt, og send meldingen din igjen.", + "error.providerAuth.chatgpt.title": "OpenAI logget deg ut", + "error.providerAuth.chatgpt.description": + "Logg på ChatGPT igjen, og send meldingen din på nytt for å fortsette å bruke Codex-modeller.", "error.chain.unknown": "Ukjent feil", "error.chain.causedBy": "Forårsaket av:", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts index 7ac1250f64e..84550227de4 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts @@ -467,6 +467,11 @@ export const dict = { "error.promotionLimit.description": "Zarejestruj się za darmo, aby kontynuować i odkryć ponad 500 modeli. Zajmie to 2 minuty, bez karty kredytowej. Lub wróć później.", "error.promotionLimit.action": "Zarejestruj się", + "error.providerAuth.title": "{{provider}} wylogował Cię", + "error.providerAuth.description": "Połącz się ponownie z {{provider}}, a następnie wyślij wiadomość jeszcze raz.", + "error.providerAuth.chatgpt.title": "OpenAI wylogowało Cię", + "error.providerAuth.chatgpt.description": + "Zaloguj się ponownie do ChatGPT, a następnie wyślij wiadomość jeszcze raz, aby kontynuować korzystanie z modeli Codex.", "error.chain.unknown": "Nieznany błąd", "error.chain.causedBy": "Spowodowany przez:", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts index dab90d88b53..baf9174e0d4 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts @@ -466,6 +466,11 @@ export const dict = { "error.promotionLimit.description": "Зарегистрируйтесь бесплатно, чтобы продолжить и исследовать более 500 моделей. Займёт 2 минуты, кредитная карта не нужна. Или вернитесь позже.", "error.promotionLimit.action": "Зарегистрироваться", + "error.providerAuth.title": "Сеанс {{provider}} завершен", + "error.providerAuth.description": "Подключитесь к {{provider}} снова, затем отправьте сообщение еще раз.", + "error.providerAuth.chatgpt.title": "Сеанс OpenAI завершен", + "error.providerAuth.chatgpt.description": + "Войдите в ChatGPT снова, затем отправьте сообщение еще раз, чтобы продолжить использование моделей Codex.", "error.chain.unknown": "Неизвестная ошибка", "error.chain.causedBy": "Причина:", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/th.ts b/packages/kilo-vscode/webview-ui/src/i18n/th.ts index d2065e63a4d..5438042d7ee 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/th.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/th.ts @@ -464,6 +464,11 @@ export const dict = { "error.promotionLimit.description": "สมัครฟรีเพื่อดำเนินการต่อและสำรวจโมเดลกว่า 500 รายการ ใช้เวลา 2 นาที ไม่ต้องใช้บัตรเครดิต หรือกลับมาทีหลัง", "error.promotionLimit.action": "สมัครสมาชิก", + "error.providerAuth.title": "{{provider}} ออกจากระบบของคุณแล้ว", + "error.providerAuth.description": "เชื่อมต่อ {{provider}} อีกครั้ง จากนั้นส่งข้อความของคุณใหม่", + "error.providerAuth.chatgpt.title": "OpenAI ออกจากระบบของคุณแล้ว", + "error.providerAuth.chatgpt.description": + "เข้าสู่ระบบ ChatGPT อีกครั้ง จากนั้นส่งข้อความของคุณใหม่เพื่อใช้งานโมเดล Codex ต่อไป", "error.chain.unknown": "ข้อผิดพลาดที่ไม่รู้จัก", "error.chain.causedBy": "สาเหตุ:", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/tr.ts b/packages/kilo-vscode/webview-ui/src/i18n/tr.ts index 6b306c13c9d..8a7d0c756ae 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/tr.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/tr.ts @@ -466,6 +466,11 @@ export const dict = { "error.promotionLimit.description": "Devam etmek ve 500'den fazla modeli keşfetmek için ücretsiz kayıt olun. 2 dakika sürer, kredi kartı gerekmez. Ya da daha sonra gelin.", "error.promotionLimit.action": "Kayıt Ol", + "error.providerAuth.title": "{{provider}} oturumunuzu kapattı", + "error.providerAuth.description": "{{provider}} bağlantısını yeniden kurun ve mesajınızı tekrar gönderin.", + "error.providerAuth.chatgpt.title": "OpenAI oturumunuzu kapattı", + "error.providerAuth.chatgpt.description": + "Codex modellerini kullanmaya devam etmek için ChatGPT ile tekrar giriş yapın ve mesajınızı yeniden gönderin.", "error.chain.unknown": "Bilinmeyen hata", "error.chain.causedBy": "Nedeni:", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/uk.ts b/packages/kilo-vscode/webview-ui/src/i18n/uk.ts index 99a692dd7b3..e2f36ebc4a3 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/uk.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/uk.ts @@ -470,6 +470,11 @@ export const dict = { "error.promotionLimit.description": "Зареєструйтеся безкоштовно для продовження та доступу до 500+ моделей. Займе 2 хвилини, кредитна картка не потрібна. Або поверніться пізніше.", "error.promotionLimit.action": "Зареєструватися", + "error.providerAuth.title": "Сеанс {{provider}} завершено", + "error.providerAuth.description": "Підключіться до {{provider}} знову, а потім надішліть повідомлення ще раз.", + "error.providerAuth.chatgpt.title": "Сеанс OpenAI завершено", + "error.providerAuth.chatgpt.description": + "Увійдіть у ChatGPT знову, а потім надішліть повідомлення ще раз, щоб продовжити використання моделей Codex.", "error.chain.unknown": "Невідома помилка", "error.chain.causedBy": "Спричинено:", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts index 1f3aa2431ad..9d55d18cfa9 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts @@ -459,6 +459,10 @@ export const dict = { "error.promotionLimit.title": "您需要注册才能继续", "error.promotionLimit.description": "免费注册以继续探索500多个模型。只需2分钟,无需信用卡。或稍后再来。", "error.promotionLimit.action": "注册", + "error.providerAuth.title": "{{provider}} 已将您登出", + "error.providerAuth.description": "请重新连接 {{provider}},然后再次发送消息。", + "error.providerAuth.chatgpt.title": "OpenAI 已将您登出", + "error.providerAuth.chatgpt.description": "请再次登录 ChatGPT,然后重新发送消息以继续使用 Codex 模型。", "error.chain.unknown": "未知错误", "error.chain.causedBy": "原因:", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts index 8add4703792..4cddda95852 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts @@ -459,6 +459,10 @@ export const dict = { "error.promotionLimit.title": "您需要註冊才能繼續", "error.promotionLimit.description": "免費註冊以繼續探索500多個模型。只需2分鐘,無需信用卡。或稍後再來。", "error.promotionLimit.action": "註冊", + "error.providerAuth.title": "{{provider}} 已將您登出", + "error.providerAuth.description": "請重新連接 {{provider}},然後再次發送訊息。", + "error.providerAuth.chatgpt.title": "OpenAI 已將您登出", + "error.providerAuth.chatgpt.description": "請再次登入 ChatGPT,然後重新發送訊息以繼續使用 Codex 模型。", "error.chain.unknown": "未知錯誤", "error.chain.causedBy": "原因:", diff --git a/packages/kilo-vscode/webview-ui/src/utils/errorUtils.ts b/packages/kilo-vscode/webview-ui/src/utils/errorUtils.ts index ba2d965cad7..a498668d66e 100644 --- a/packages/kilo-vscode/webview-ui/src/utils/errorUtils.ts +++ b/packages/kilo-vscode/webview-ui/src/utils/errorUtils.ts @@ -53,6 +53,11 @@ export interface ParsedError { message?: string } +export interface ParsedProviderAuthError { + providerID: string + message: string +} + export function parseAssistantError(error: AssistantMessage["error"] | null | undefined): ParsedError | null { if (!error) return null if (error.name !== "APIError") return null @@ -81,6 +86,20 @@ export function parseAssistantError(error: AssistantMessage["error"] | null | un return { statusCode, code, message } } +export function parseProviderAuthError( + error: AssistantMessage["error"] | null | undefined, +): ParsedProviderAuthError | null { + if (!error) return null + if (error.name !== "ProviderAuthError") return null + + const data = error.data + if (!data) return null + const providerID = typeof data.providerID === "string" ? data.providerID : undefined + const message = typeof data.message === "string" ? data.message : undefined + if (!providerID || !message) return null + return { providerID, message } +} + export function isUnauthorizedPaidModelError(parsed: ParsedError | null): boolean { if (!parsed) return false return parsed.statusCode === 401 && parsed.code === errorCodes.PAID_MODEL_AUTH_REQUIRED diff --git a/packages/opencode/src/kilocode/provider/codex-refresh.ts b/packages/opencode/src/kilocode/provider/codex-refresh.ts new file mode 100644 index 00000000000..81a160a3411 --- /dev/null +++ b/packages/opencode/src/kilocode/provider/codex-refresh.ts @@ -0,0 +1,102 @@ +import type { PluginInput } from "@kilocode/plugin" + +export class CodexAuthExpiredError extends Error { + constructor( + message = "Your ChatGPT sign-in expired or was revoked. Sign in with ChatGPT again to continue using Codex models.", + ) { + super(message) + this.name = "CodexAuthExpiredError" + } +} + +type Auth = { + type: "oauth" + refresh: string + access: string + expires: number + accountId?: string +} + +type Tokens = { + id_token: string + access_token: string + refresh_token: string + expires_in?: number +} + +type Input = { + input: PluginInput + getAuth: () => Promise + auth: Auth + refresh: (refresh: string) => Promise + account: (tokens: Tokens) => string | undefined +} + +const pending = new Map>() + +function valid(auth: Auth) { + return auth.access && auth.expires > Date.now() +} + +function usable(auth: Auth, refresh: string) { + return auth.refresh !== refresh || valid(auth) +} + +function oauth(auth: unknown): Auth | undefined { + if (!auth || typeof auth !== "object" || !("type" in auth) || auth.type !== "oauth") return + return auth as Auth +} + +function assign(auth: Auth, next: Auth) { + auth.access = next.access + auth.refresh = next.refresh + auth.expires = next.expires + auth.accountId = next.accountId +} + +function recoverable(err: unknown) { + return err instanceof Error && /^Token refresh failed: 401\b/.test(err.message) +} + +export async function refreshCodexAuth(input: Input) { + const inflight = pending.get(input.auth.refresh) + if (inflight) { + const next = await inflight + assign(input.auth, next) + return next + } + + const promise = (async () => { + const fresh = await input.getAuth() + const current = oauth(fresh) + if (current && valid(current)) return current + + try { + const base = current && current.refresh !== input.auth.refresh ? current : input.auth + const tokens = await input.refresh(base.refresh) + const id = input.account(tokens) || base.accountId + const next = { + type: "oauth" as const, + refresh: tokens.refresh_token, + access: tokens.access_token, + expires: Date.now() + (tokens.expires_in ?? 3600) * 1000, + ...(id && { accountId: id }), + } + await input.input.client.auth.set({ path: { id: "openai" }, body: next }) + return next + } catch (err) { + if (!recoverable(err)) throw err + + const latest = await input.getAuth() + const next = oauth(latest) + if (next && usable(next, input.auth.refresh)) return next + + throw new CodexAuthExpiredError() + } + })().finally(() => pending.delete(input.auth.refresh)) + + pending.set(input.auth.refresh, promise) + const next = await promise + assign(input.auth, next) + return next +} diff --git a/packages/opencode/src/plugin/codex.ts b/packages/opencode/src/plugin/codex.ts index d24a0fa959b..fc3701618ac 100644 --- a/packages/opencode/src/plugin/codex.ts +++ b/packages/opencode/src/plugin/codex.ts @@ -1,11 +1,11 @@ import type { Hooks, PluginInput } from "@kilocode/plugin" import * as Log from "@opencode-ai/core/util/log" -import { Installation } from "../installation" import { InstallationVersion } from "@opencode-ai/core/installation/version" import { OAUTH_DUMMY_KEY } from "../auth" import os from "os" import { setTimeout as sleep } from "node:timers/promises" import { createServer } from "http" +import { refreshCodexAuth } from "@/kilocode/provider/codex-refresh" // kilocode_change const log = Log.create({ service: "plugin.codex" }) @@ -439,20 +439,9 @@ export async function CodexAuthPlugin(input: PluginInput): Promise { // Check if token needs refresh if (!currentAuth.access || currentAuth.expires < Date.now()) { log.info("refreshing codex access token") - const tokens = await refreshAccessToken(currentAuth.refresh) - const newAccountId = extractAccountId(tokens) || authWithAccount.accountId - await input.client.auth.set({ - path: { id: "openai" }, - body: { - type: "oauth", - refresh: tokens.refresh_token, - access: tokens.access_token, - expires: Date.now() + (tokens.expires_in ?? 3600) * 1000, - ...(newAccountId && { accountId: newAccountId }), - }, - }) - currentAuth.access = tokens.access_token - authWithAccount.accountId = newAccountId + // kilocode_change start + await refreshCodexAuth({ input, getAuth, auth: currentAuth, refresh: refreshAccessToken, account: extractAccountId }) + // kilocode_change end } // Build headers diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts index 83287e63c0e..53b5dc23abc 100644 --- a/packages/opencode/src/session/message-v2.ts +++ b/packages/opencode/src/session/message-v2.ts @@ -23,6 +23,7 @@ import type { SystemError } from "bun" import type { Provider } from "@/provider/provider" import { ModelID, ProviderID } from "@/provider/schema" import { SessionNetwork } from "./network" // kilocode_change +import { CodexAuthExpiredError } from "@/kilocode/provider/codex-refresh" // kilocode_change import { Effect, Schema, Types } from "effect" import { zod, ZodOverride } from "@/util/effect-zod" import { NonNegativeInt, withStatics } from "@/util/schema" @@ -1240,6 +1241,14 @@ export function fromError( }, { cause: e }, ).toObject() + case e instanceof CodexAuthExpiredError: // kilocode_change start + return new AuthError( + { + providerID: "openai", + message: e.message, + }, + { cause: e }, + ).toObject() // kilocode_change end case SessionNetwork.disconnected(e): // kilocode_change start return new APIError( { diff --git a/packages/opencode/test/kilocode/codex-auth-refresh.test.ts b/packages/opencode/test/kilocode/codex-auth-refresh.test.ts new file mode 100644 index 00000000000..33d4ed7b3c3 --- /dev/null +++ b/packages/opencode/test/kilocode/codex-auth-refresh.test.ts @@ -0,0 +1,123 @@ +import { describe, expect, test } from "bun:test" +import { CodexAuthExpiredError, refreshCodexAuth } from "../../src/kilocode/provider/codex-refresh" +import type { PluginInput } from "@kilocode/plugin" +import { MessageV2 } from "../../src/session/message-v2" +import { ProviderID } from "../../src/provider/schema" + +type Auth = { + type: "oauth" + refresh: string + access: string + expires: number + accountId?: string +} + +const expired = (): Auth => ({ + type: "oauth", + access: "old-access", + refresh: "old-refresh", + expires: 0, +}) + +function plugin(persist: (auth: Auth) => void): PluginInput { + const set = async (req: { body: Auth }) => { + persist(req.body) + } + return { + client: { + auth: { set }, + }, + } as unknown as PluginInput +} + +describe("Codex auth refresh", () => { + test("serializes expired Codex auth as ProviderAuthError", () => { + const result = MessageV2.fromError(new CodexAuthExpiredError(), { providerID: ProviderID.make("openai") }) + + expect(result).toStrictEqual({ + name: "ProviderAuthError", + data: { + providerID: "openai", + message: "Your ChatGPT sign-in expired or was revoked. Sign in with ChatGPT again to continue using Codex models.", + }, + }) + }) + + test("coalesces concurrent refreshes and persists rotated tokens", async () => { + const calls: string[] = [] + const writes: Auth[] = [] + const first = expired() + const second = expired() + const refresh = async (token: string) => { + calls.push(token) + await new Promise((resolve) => setTimeout(resolve, 1)) + return { id_token: "", access_token: "next-access", refresh_token: "next-refresh", expires_in: 60 } + } + + const [a, b] = await Promise.all([ + refreshCodexAuth({ + input: plugin((auth) => writes.push(auth)), + getAuth: async () => first, + auth: first, + refresh, + account: () => undefined, + }), + refreshCodexAuth({ + input: plugin((auth) => writes.push(auth)), + getAuth: async () => second, + auth: second, + refresh, + account: () => undefined, + }), + ]) + + expect(calls).toEqual(["old-refresh"]) + expect(writes).toHaveLength(1) + expect(a.access).toBe("next-access") + expect(b.refresh).toBe("next-refresh") + expect(first.access).toBe("next-access") + expect(second.access).toBe("next-access") + }) + + test("uses a newer stored token after refresh 401", async () => { + const fresh = { + type: "oauth" as const, + access: "fresh-access", + refresh: "fresh-refresh", + expires: Date.now() + 60_000, + } + const auth = expired() + let count = 0 + const getAuth = async () => { + count++ + return count === 1 ? auth : fresh + } + + const result = await refreshCodexAuth({ + input: plugin(() => {}), + getAuth, + auth, + refresh: async () => { + throw new Error("Token refresh failed: 401") + }, + account: () => undefined, + }) + + expect(result).toBe(fresh) + }) + + test("throws reauth error when refresh 401 has no newer stored token", async () => { + const auth = expired() + await expect( + refreshCodexAuth({ + input: plugin(() => {}), + getAuth: async () => auth, + auth, + refresh: async () => { + throw new Error("Token refresh failed: 401") + }, + account: () => undefined, + }), + ).rejects.toBeInstanceOf(CodexAuthExpiredError) + }) +})