Q87: provider errors keep status and provider code; text never decides recovery

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-08 14:52:31 -04:00
1 parent 7ecffe84e3
commit 4668396f25
28 files changed
+285 -108

No files matched your search

@@ -8,6 +8,7 @@
import { startOAuthCallbackServer } from "./oauth-callback-server";
import { generatePkce } from "./pkce";
import type { OAuthCredentials, OAuthFlow, OAuthLoginCallbacks } from "./oauth-types";
import { httpFailure } from "../provider-http";
const CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
const AUTHORIZE_URL = "https://claude.ai/oauth/authorize";
@@ -61,7 +62,7 @@ export async function refreshClaudePro(refreshToken: string): Promise<OAuthCrede
body: JSON.stringify({ grant_type: "refresh_token", client_id: CLIENT_ID, refresh_token: refreshToken }),
});
const text = await response.text();
if (!response.ok) throw new Error(`Claude token refresh failed: ${response.status} ${text}`);
if (!response.ok) throw httpFailure(`Claude token refresh failed: ${response.status} ${text}`, response, text);
const json = JSON.parse(text) as { access_token?: unknown; refresh_token?: unknown; expires_in?: unknown };
if (typeof json.access_token !== "string" || typeof json.refresh_token !== "string" || typeof json.expires_in !== "number") {
throw new Error("Claude token refresh response was missing required fields.");
@@ -6,6 +6,7 @@
*/
import { listProviderModels } from "../../../shared/provider-models";
import type { OAuthCredentials, OAuthFlow, OAuthLoginCallbacks } from "./oauth-types";
import { httpFailure } from "../provider-http";
// Decoded from the published GitHub Copilot client id used by the official CLI.
const CLIENT_ID = "Iv1.b507a08c87ecfe98";
@@ -47,7 +48,7 @@ async function fetchJson(url: string, init: RequestInit): Promise<Record<string,
const response = await fetch(url, init);
if (!response.ok) {
const text = await response.text().catch(() => "");
throw new Error(`${response.status} ${response.statusText}: ${text}`);
throw httpFailure(`${response.status} ${response.statusText}: ${text}`, response, text);
}
return (await response.json()) as Record<string, unknown>;
}
@@ -7,6 +7,7 @@
import { startOAuthCallbackServer } from "./oauth-callback-server";
import { generatePkce } from "./pkce";
import type { OAuthCredentials, OAuthFlow, OAuthLoginCallbacks } from "./oauth-types";
import { httpFailure } from "../provider-http";
const CLIENT_ID = "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com";
const CALLBACK_PORT = 51121;
@@ -115,7 +116,7 @@ export async function refreshAntigravity(refreshToken: string, projectId: string
}),
});
const text = await response.text();
if (!response.ok) throw new Error(`Antigravity token refresh failed: ${response.status} ${text}`);
if (!response.ok) throw httpFailure(`Antigravity token refresh failed: ${response.status} ${text}`, response, text);
const json = JSON.parse(text) as { access_token?: unknown; refresh_token?: unknown; expires_in?: unknown };
if (typeof json.access_token !== "string" || typeof json.expires_in !== "number") {
throw new Error("Antigravity token refresh response was missing required fields.");
+7 -6
View File
@@ -8,6 +8,7 @@
import { createHash } from "node:crypto";
import { hostname, platform as osPlatform, release as osRelease, userInfo } from "node:os";
import type { OAuthCredentials, OAuthFlow, OAuthLoginCallbacks } from "./oauth-types";
import { httpFailure } from "../provider-http";
// Published public client id of the official Kimi CLI; reused by third-party
// coding agents per Kimi's device-flow integration guidance.
@@ -47,7 +48,7 @@ function deviceHeaders(): Record<string, string> {
};
}
async function postForm(path: string, form: Record<string, string>): Promise<{ status: number; data: Record<string, unknown> }> {
async function postForm(path: string, form: Record<string, string>): Promise<{ status: number; data: Record<string, unknown>; response: Response; text: string }> {
const response = await fetch(`${OAUTH_HOST}${path}`, {
method: "POST",
headers: {
@@ -62,12 +63,12 @@ async function postForm(path: string, form: Record<string, string>): Promise<{ s
try {
data = JSON.parse(text);
} catch {
throw new Error(`Kimi OAuth endpoint returned a non-JSON response: ${response.status} ${text.slice(0, 200)}`);
throw httpFailure(`Kimi OAuth endpoint returned a non-JSON response: ${response.status} ${text.slice(0, 200)}`, response, text);
}
if (typeof data !== "object" || data === null || Array.isArray(data)) {
throw new Error(`Kimi OAuth endpoint returned an unexpected payload: ${response.status}`);
throw httpFailure(`Kimi OAuth endpoint returned an unexpected payload: ${response.status}`, response, text);
}
return { status: response.status, data: data as Record<string, unknown> };
return { status: response.status, data: data as Record<string, unknown>, response, text };
}
interface DeviceAuthorization {
@@ -167,12 +168,12 @@ async function pollForToken(device: DeviceAuthorization, signal?: AbortSignal):
/** Refresh an expired Kimi Code access token (they live ~15 minutes). */
export async function refreshKimiCode(refreshToken: string): Promise<OAuthCredentials> {
const { status, data } = await postForm("/api/oauth/token", {
const { status, data, response, text } = await postForm("/api/oauth/token", {
client_id: CLIENT_ID,
grant_type: "refresh_token",
refresh_token: refreshToken,
});
if (status !== 200) throw new Error(`Kimi token refresh failed: ${status} ${JSON.stringify(data)}`);
if (status !== 200) throw httpFailure(`Kimi token refresh failed: ${status} ${JSON.stringify(data)}`, response, text);
return credentialsFromToken(data);
}
+2 -1
View File
@@ -7,6 +7,7 @@
import { startOAuthCallbackServer } from "./oauth-callback-server";
import { generatePkce } from "./pkce";
import type { OAuthCredentials, OAuthFlow, OAuthLoginCallbacks } from "./oauth-types";
import { httpFailure } from "../provider-http";
const CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
const AUTHORIZE_URL = "https://auth.openai.com/oauth/authorize";
@@ -84,7 +85,7 @@ export async function refreshCodex(refreshToken: string): Promise<OAuthCredentia
body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: refreshToken, client_id: CLIENT_ID }),
});
const text = await response.text();
if (!response.ok) throw new Error(`Codex token refresh failed: ${response.status} ${text}`);
if (!response.ok) throw httpFailure(`Codex token refresh failed: ${response.status} ${text}`, response, text);
const json = JSON.parse(text) as { access_token?: unknown; refresh_token?: unknown; expires_in?: unknown };
if (typeof json.access_token !== "string" || typeof json.refresh_token !== "string" || typeof json.expires_in !== "number") {
throw new Error("Codex token refresh response was missing required fields.");