mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-19 01:51:21 +08:00
Fix expired Codex auth recovery (#10136)
* fix: recover expired Codex auth * fix: minimize Codex auth shared changes * fix: format Codex auth prompt
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
"@kilocode/cli": patch
|
||||
---
|
||||
|
||||
Show ChatGPT sign-in again when Codex authentication expires.
|
||||
@@ -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)
|
||||
|
||||
@@ -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<AssistantMessage["error"]>
|
||||
@@ -19,7 +23,28 @@ export interface ErrorDisplayProps {
|
||||
|
||||
export const ErrorDisplay: Component<ErrorDisplayProps> = (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<ErrorDisplayProps> = (props) => {
|
||||
return unwrapError(String(msg))
|
||||
})
|
||||
|
||||
function connectProvider() {
|
||||
const err = auth()
|
||||
if (!err) return
|
||||
dialog.show(() => <ProviderConnectDialog providerID={err.providerID} oauthOnly={oauth()} />)
|
||||
}
|
||||
|
||||
return (
|
||||
<Switch
|
||||
fallback={
|
||||
@@ -69,6 +100,28 @@ export const ErrorDisplay: Component<ErrorDisplayProps> = (props) => {
|
||||
</Button>
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={canAuth()}>
|
||||
<div data-component="auth-prompt">
|
||||
<div data-slot="auth-prompt-header">
|
||||
<span data-slot="auth-prompt-icon">↻</span>
|
||||
<span data-slot="auth-prompt-title">
|
||||
{oauth()
|
||||
? t("error.providerAuth.chatgpt.title")
|
||||
: t("error.providerAuth.title", { provider: authProvider()?.name ?? auth()?.providerID ?? "provider" })}
|
||||
</span>
|
||||
</div>
|
||||
<p data-slot="auth-prompt-description">
|
||||
{oauth()
|
||||
? t("error.providerAuth.chatgpt.description")
|
||||
: t("error.providerAuth.description", {
|
||||
provider: authProvider()?.name ?? auth()?.providerID ?? "provider",
|
||||
})}
|
||||
</p>
|
||||
<Button variant="primary" onClick={connectProvider}>
|
||||
{oauth() ? t("settings.providers.action.signInChatGPT") : t("common.connect")}
|
||||
</Button>
|
||||
</div>
|
||||
</Match>
|
||||
</Switch>
|
||||
)
|
||||
}
|
||||
|
||||
+5
@@ -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": "بسبب:",
|
||||
|
||||
+5
@@ -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:",
|
||||
|
||||
+5
@@ -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:",
|
||||
|
||||
+5
@@ -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:",
|
||||
|
||||
+5
@@ -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:",
|
||||
|
||||
@@ -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:",
|
||||
|
||||
+5
@@ -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:",
|
||||
|
||||
+5
@@ -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 :",
|
||||
|
||||
+5
@@ -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": "原因:",
|
||||
|
||||
+5
@@ -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": "원인:",
|
||||
|
||||
+5
@@ -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:",
|
||||
|
||||
+5
@@ -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:",
|
||||
|
||||
+5
@@ -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:",
|
||||
|
||||
+5
@@ -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": "Причина:",
|
||||
|
||||
+5
@@ -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": "สาเหตุ:",
|
||||
|
||||
+5
@@ -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:",
|
||||
|
||||
+5
@@ -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": "Спричинено:",
|
||||
|
||||
+4
@@ -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": "原因:",
|
||||
|
||||
+4
@@ -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": "原因:",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<unknown>
|
||||
auth: Auth
|
||||
refresh: (refresh: string) => Promise<Tokens>
|
||||
account: (tokens: Tokens) => string | undefined
|
||||
}
|
||||
|
||||
const pending = new Map<string, Promise<Auth>>()
|
||||
|
||||
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
|
||||
}
|
||||
@@ -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<Hooks> {
|
||||
// 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
|
||||
|
||||
@@ -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(
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user