fix(providers): retire common ChatGPT Web provider

This commit is contained in:
diegosouzapw
2026-08-27 03:13:19 -03:00
committed by Markus Hartung
parent 825f8fe425
commit 9c5dd6760c
172 changed files with 3193 additions and 11318 deletions

View File

@@ -0,0 +1,195 @@
-- Retire the common ChatGPT Web integration because its inherited provenance cannot be cleared.
--
-- Match the complete ECMAScript trim whitespace set so database tombstones and
-- the TypeScript runtime agree even for restored provider ids wrapped in Unicode
-- spaces (NBSP, OGHAM, U+2000..U+200A, line/paragraph separators and BOM).
--
-- Keep connection rows and historical records for auditability. Disabling the
-- connections is deliberately fail-closed: API-key allowed_connections entries
-- continue to reference the same connection ids instead of becoming an empty
-- allowlist, which would mean unrestricted access in the policy layer.
UPDATE exclusive_connection_leases
SET state = 'INVALIDATED',
ended_at = datetime('now'),
end_reason = 'CONNECTION_INELIGIBLE'
WHERE state = 'ACTIVE'
AND (
lower(trim(provider, char(9,10,11,12,13,32,160,5760,8192,8193,8194,8195,8196,8197,8198,8199,8200,8201,8202,8232,8233,8239,8287,12288,65279)))
IN ('chatgpt-web', 'cgpt-web')
OR connection_id IN (
SELECT id
FROM provider_connections
WHERE lower(trim(provider, char(9,10,11,12,13,32,160,5760,8192,8193,8194,8195,8196,8197,8198,8199,8200,8201,8202,8232,8233,8239,8287,12288,65279)))
IN ('chatgpt-web', 'cgpt-web')
)
);
UPDATE provider_connections
SET is_active = 0,
test_status = 'unavailable',
error_code = 'PROVIDER_REMOVED',
last_error = 'Provider integration retired from OmniRoute v3.8.50',
last_error_type = 'provider_removed',
last_error_source = 'migration:retire-chatgpt-web',
last_error_at = datetime('now'),
updated_at = datetime('now')
WHERE lower(trim(provider, char(9,10,11,12,13,32,160,5760,8192,8193,8194,8195,8196,8197,8198,8199,8200,8201,8202,8232,8233,8239,8287,12288,65279)))
IN ('chatgpt-web', 'cgpt-web')
AND (
is_active IS NOT 0
OR test_status IS NOT 'unavailable'
OR error_code IS NOT 'PROVIDER_REMOVED'
OR last_error IS NOT 'Provider integration retired from OmniRoute v3.8.50'
OR last_error_type IS NOT 'provider_removed'
OR last_error_source IS NOT 'migration:retire-chatgpt-web'
OR last_error_at IS NULL
);
-- Migrations run before settings imports. Keep the tombstone durable when an
-- old db.json snapshot or an admin PATCH later attempts to reactivate either
-- retired id. The WHEN predicates are null-safe and prevent timestamp churn
-- when an already-normalized row is written again.
CREATE TRIGGER IF NOT EXISTS provider_connections_retire_chatgpt_web_insert
AFTER INSERT ON provider_connections
WHEN lower(trim(NEW.provider, char(9,10,11,12,13,32,160,5760,8192,8193,8194,8195,8196,8197,8198,8199,8200,8201,8202,8232,8233,8239,8287,12288,65279)))
IN ('chatgpt-web', 'cgpt-web')
BEGIN
UPDATE provider_connections
SET is_active = 0,
test_status = 'unavailable',
error_code = 'PROVIDER_REMOVED',
last_error = 'Provider integration retired from OmniRoute v3.8.50',
last_error_type = 'provider_removed',
last_error_source = 'migration:retire-chatgpt-web',
last_error_at = datetime('now'),
updated_at = datetime('now')
WHERE id = NEW.id
AND (
is_active IS NOT 0
OR test_status IS NOT 'unavailable'
OR error_code IS NOT 'PROVIDER_REMOVED'
OR last_error IS NOT 'Provider integration retired from OmniRoute v3.8.50'
OR last_error_type IS NOT 'provider_removed'
OR last_error_source IS NOT 'migration:retire-chatgpt-web'
OR last_error_at IS NULL
);
UPDATE exclusive_connection_leases
SET state = 'INVALIDATED',
ended_at = datetime('now'),
end_reason = 'CONNECTION_INELIGIBLE'
WHERE state = 'ACTIVE'
AND connection_id = NEW.id;
END;
CREATE TRIGGER IF NOT EXISTS provider_connections_retire_chatgpt_web_update
AFTER UPDATE OF provider, is_active, test_status, error_code, last_error,
last_error_type, last_error_source, last_error_at ON provider_connections
WHEN lower(trim(NEW.provider, char(9,10,11,12,13,32,160,5760,8192,8193,8194,8195,8196,8197,8198,8199,8200,8201,8202,8232,8233,8239,8287,12288,65279)))
IN ('chatgpt-web', 'cgpt-web')
BEGIN
UPDATE provider_connections
SET is_active = 0,
test_status = 'unavailable',
error_code = 'PROVIDER_REMOVED',
last_error = 'Provider integration retired from OmniRoute v3.8.50',
last_error_type = 'provider_removed',
last_error_source = 'migration:retire-chatgpt-web',
last_error_at = datetime('now'),
updated_at = datetime('now')
WHERE id = NEW.id
AND (
is_active IS NOT 0
OR test_status IS NOT 'unavailable'
OR error_code IS NOT 'PROVIDER_REMOVED'
OR last_error IS NOT 'Provider integration retired from OmniRoute v3.8.50'
OR last_error_type IS NOT 'provider_removed'
OR last_error_source IS NOT 'migration:retire-chatgpt-web'
OR last_error_at IS NULL
);
UPDATE exclusive_connection_leases
SET state = 'INVALIDATED',
ended_at = datetime('now'),
end_reason = 'CONNECTION_INELIGIBLE'
WHERE state = 'ACTIVE'
AND connection_id = NEW.id;
END;
-- Once a connection id belongs to a retired provider, imports and internal
-- writers must not repurpose that same audited identity as another provider.
-- Retired-to-retired normalization remains allowed and is re-tombstoned by the
-- AFTER UPDATE trigger above.
CREATE TRIGGER IF NOT EXISTS provider_connections_preserve_chatgpt_web_identity_insert
BEFORE INSERT ON provider_connections
WHEN EXISTS (
SELECT 1
FROM provider_connections
WHERE id = NEW.id
AND lower(trim(provider, char(9,10,11,12,13,32,160,5760,8192,8193,8194,8195,8196,8197,8198,8199,8200,8201,8202,8232,8233,8239,8287,12288,65279)))
IN ('chatgpt-web', 'cgpt-web')
)
AND lower(trim(NEW.provider, char(9,10,11,12,13,32,160,5760,8192,8193,8194,8195,8196,8197,8198,8199,8200,8201,8202,8232,8233,8239,8287,12288,65279)))
NOT IN ('chatgpt-web', 'cgpt-web')
BEGIN
SELECT RAISE(ABORT, 'Retired provider connection identity cannot be changed');
END;
CREATE TRIGGER IF NOT EXISTS provider_connections_preserve_chatgpt_web_identity_update
BEFORE UPDATE OF provider ON provider_connections
WHEN lower(trim(OLD.provider, char(9,10,11,12,13,32,160,5760,8192,8193,8194,8195,8196,8197,8198,8199,8200,8201,8202,8232,8233,8239,8287,12288,65279)))
IN ('chatgpt-web', 'cgpt-web')
AND lower(trim(NEW.provider, char(9,10,11,12,13,32,160,5760,8192,8193,8194,8195,8196,8197,8198,8199,8200,8201,8202,8232,8233,8239,8287,12288,65279)))
NOT IN ('chatgpt-web', 'cgpt-web')
BEGIN
SELECT RAISE(ABORT, 'Retired provider connection identity cannot be changed');
END;
-- A restore can also insert lease rows after migrations have completed. Keep
-- lease state fail-closed independently of request-time auth selection.
CREATE TRIGGER IF NOT EXISTS exclusive_connection_leases_retire_chatgpt_web_insert
AFTER INSERT ON exclusive_connection_leases
WHEN NEW.state = 'ACTIVE'
AND (
lower(trim(NEW.provider, char(9,10,11,12,13,32,160,5760,8192,8193,8194,8195,8196,8197,8198,8199,8200,8201,8202,8232,8233,8239,8287,12288,65279)))
IN ('chatgpt-web', 'cgpt-web')
OR EXISTS (
SELECT 1
FROM provider_connections
WHERE id = NEW.connection_id
AND lower(trim(provider, char(9,10,11,12,13,32,160,5760,8192,8193,8194,8195,8196,8197,8198,8199,8200,8201,8202,8232,8233,8239,8287,12288,65279)))
IN ('chatgpt-web', 'cgpt-web')
)
)
BEGIN
UPDATE exclusive_connection_leases
SET state = 'INVALIDATED',
ended_at = datetime('now'),
end_reason = 'CONNECTION_INELIGIBLE'
WHERE id = NEW.id
AND state = 'ACTIVE';
END;
CREATE TRIGGER IF NOT EXISTS exclusive_connection_leases_retire_chatgpt_web_update
AFTER UPDATE OF provider, connection_id, state ON exclusive_connection_leases
WHEN NEW.state = 'ACTIVE'
AND (
lower(trim(NEW.provider, char(9,10,11,12,13,32,160,5760,8192,8193,8194,8195,8196,8197,8198,8199,8200,8201,8202,8232,8233,8239,8287,12288,65279)))
IN ('chatgpt-web', 'cgpt-web')
OR EXISTS (
SELECT 1
FROM provider_connections
WHERE id = NEW.connection_id
AND lower(trim(provider, char(9,10,11,12,13,32,160,5760,8192,8193,8194,8195,8196,8197,8198,8199,8200,8201,8202,8232,8233,8239,8287,12288,65279)))
IN ('chatgpt-web', 'cgpt-web')
)
)
BEGIN
UPDATE exclusive_connection_leases
SET state = 'INVALIDATED',
ended_at = datetime('now'),
end_reason = 'CONNECTION_INELIGIBLE'
WHERE id = NEW.id
AND state = 'ACTIVE';
END;

View File

@@ -3,6 +3,8 @@
*/
import { v4 as uuidv4 } from "uuid";
import { isCommonChatGptWebRetiredProviderId } from "@/shared/constants/chatgptWebRetirement";
import { getDbInstance, rowToCamel, cleanNulls } from "./core";
import { backupDbFile } from "./backup";
import {
@@ -595,13 +597,19 @@ export async function createProviderConnection(data: JsonRecord) {
_updateConnectionRow(db, existingId, encryptConnectionFields(persistence));
})();
backupDbFile("pre-write");
return withNullableRateLimitOverrides(
const returnedConnection = withNullableRateLimitOverrides(
withNullableQuotaWindowThresholds(
withNullableMaxConcurrent(cleanNulls(merged), merged),
merged
),
merged
);
if (isCommonChatGptWebRetiredProviderId(merged.provider)) {
return (await getProviderConnectionById(existingId)) ?? returnedConnection;
}
return returnedConnection;
}
// Generate name: prefer explicit name, then email, then a stable short-ID label.
@@ -722,13 +730,19 @@ export async function createProviderConnection(data: JsonRecord) {
backupDbFile("pre-write");
invalidateDbCache("connections"); // Bust connections read cache
return withNullableRateLimitOverrides(
const returnedConnection = withNullableRateLimitOverrides(
withNullableQuotaWindowThresholds(
withNullableMaxConcurrent(cleanNulls(connection), connection),
connection
),
connection
);
if (isCommonChatGptWebRetiredProviderId(providerId)) {
return (await getProviderConnectionById(String(connection.id))) ?? returnedConnection;
}
return returnedConnection;
}
function _insertConnectionRow(db: DbLike, conn: JsonRecord) {
@@ -967,13 +981,19 @@ export async function updateProviderConnection(id: string, data: JsonRecord) {
reorderConnections(db, providerId);
}
return withNullableRateLimitOverrides(
const returnedConnection = withNullableRateLimitOverrides(
withNullableQuotaWindowThresholds(
withNullableMaxConcurrent(cleanNulls(merged), merged),
merged
),
merged
);
if (isCommonChatGptWebRetiredProviderId(merged.provider)) {
return (await getProviderConnectionById(id)) ?? returnedConnection;
}
return returnedConnection;
}
export {

View File

@@ -7,7 +7,7 @@
* 1. Bare combo / alias name with no slash (`image`) — resolved to the combo's single
* image target, then that target is itself prefix-resolved. Bare combos intentionally
* override built-in image aliases with the same name.
* 2. Built-in image model id / alias (`cgpt-web/...`, `gpt-image-1`, ) — untouched.
* 2. Built-in image model id / alias (`openai/gpt-image-2`, `gpt-image-1`, etc.) — untouched.
* 3. Custom provider *prefix* form (`myImg/gpt-image-2`) — rewritten to the internal
* `<nodeId>/<model>` id (#3205 did this inline in the generations route only).
*
@@ -19,6 +19,7 @@ import { resolveComboTargets } from "@omniroute/open-sse/services/combo.ts";
import { getComboByName, getCombos } from "@/lib/db/combos";
import { getCachedProviderNodes } from "@/lib/localDb";
import { assertCommonChatGptWebModelAvailable } from "@/shared/constants/chatgptWebRetirement";
/**
* Rewrite a `prefix/model` custom image model to its internal `<nodeId>/<model>` form.
@@ -28,6 +29,7 @@ import { getCachedProviderNodes } from "@/lib/localDb";
*/
export async function resolveImageModelPrefix(modelStr: string): Promise<string> {
if (typeof modelStr !== "string") return modelStr;
assertCommonChatGptWebModelAvailable(modelStr);
const slash = modelStr.indexOf("/");
if (slash <= 0) return modelStr;
@@ -75,6 +77,7 @@ export async function resolveSingleImageComboTarget(name: string): Promise<strin
*/
export async function resolveImageRouteModel(modelStr: string): Promise<string> {
if (typeof modelStr !== "string" || !modelStr.trim()) return modelStr;
assertCommonChatGptWebModelAvailable(modelStr);
const parsedModel = parseImageModel(modelStr);
const hasSlash = modelStr.includes("/");

View File

@@ -17,7 +17,7 @@ const SENSITIVE_KEYS = new Set([
"secret",
"token",
// secret-leak hardening: session cookies + browser-storage credentials that
// some web-impersonation providers (Meta AI ecto_1_sess, chatgpt-web
// some web-impersonation providers (Meta AI ecto_1_sess, Perplexity Web
// storageState / runtimeKey) can surface into a request/response BODY field
// rather than a header. Header-borne values are already masked by
// maskSensitiveHeaders; this covers the body path into the on-disk call-log

View File

@@ -35,8 +35,11 @@
* Built-in/no-compatible catalog entries are always eligible.
*/
import { REGISTRY } from "@omniroute/open-sse/config/providerRegistry.ts";
import { getProviderNodes } from "@/lib/db/providers/nodes";
import {
getReservedProviderPrefixes,
isReservedProviderPrefix,
} from "@/shared/constants/reservedProviderPrefixes";
export type ProviderPrefixStatus = "unique" | "ambiguous" | "reserved";
@@ -66,12 +69,7 @@ export interface ProviderPrefixIndex {
* prefixes can never shadow a built-in provider.
*/
export function buildReservedPrefixes(): Set<string> {
const reserved = new Set<string>();
for (const entry of Object.values(REGISTRY)) {
if (entry?.id) reserved.add(entry.id);
if (entry?.alias) reserved.add(entry.alias);
}
return reserved;
return new Set(getReservedProviderPrefixes());
}
export interface CompatibleNodeLike {
@@ -97,7 +95,6 @@ export function selectCompatibleNodeForPrefix(
}
export async function getProviderPrefixIndex(): Promise<ProviderPrefixIndex> {
const reserved = buildReservedPrefixes();
const nodes = (await getProviderNodes()) as CompatibleNodeLike[];
const compatible = nodes.filter(
(n) => n.type === "openai-compatible" || n.type === "anthropic-compatible"
@@ -123,8 +120,9 @@ export async function getProviderPrefixIndex(): Promise<ProviderPrefixIndex> {
const eligibleNodeIds = new Set<string>();
for (const [prefix, prefixNodes] of byPrefix) {
if (reserved.has(prefix)) {
// Built-in registry id/alias — never a compatible public target.
if (isReservedProviderPrefix(prefix)) {
// Built-in registry id/alias or case-insensitive retired id — never a
// compatible public target.
entries.set(prefix, { prefix, status: "reserved" });
continue;
}

View File

@@ -0,0 +1,30 @@
import { errorResponse } from "@omniroute/open-sse/utils/error.ts";
import {
assertCommonChatGptWebProviderAvailable,
CHATGPT_WEB_RETIRED_ERROR_CODE,
CHATGPT_WEB_RETIRED_MESSAGE,
isCommonChatGptWebRetiredProviderId,
isCommonChatGptWebRetirementError,
} from "@/shared/constants/chatgptWebRetirement";
export function commonChatGptWebRetirementResponse(): Response {
return errorResponse(410, CHATGPT_WEB_RETIRED_MESSAGE, {
type: "provider_error",
code: CHATGPT_WEB_RETIRED_ERROR_CODE,
});
}
export function rejectRetiredCommonChatGptWebProvider(providerId: unknown): Response | null {
return isCommonChatGptWebRetiredProviderId(providerId)
? commonChatGptWebRetirementResponse()
: null;
}
export function assertProviderAvailable(providerId: unknown): void {
assertCommonChatGptWebProviderAvailable(providerId);
}
export function responseForError(error: unknown): Response | null {
return isCommonChatGptWebRetirementError(error) ? commonChatGptWebRetirementResponse() : null;
}

View File

@@ -16,7 +16,7 @@ const TOOL_ONLY_SERVICE_KINDS = new Set<string>(["webSearch", "webFetch"]);
* are intentionally NOT curated: their model list is discovered live from the
* console API (see volcenginePlanModelDiscovery.ts) and merged into the synced
* catalog, so the static registry only acts as a capability-seed fallback. */
const CURATED_MODEL_ONLY_PROVIDERS = new Set<string>(["chatgpt-web", "kimi-web", "zai-web"]);
const CURATED_MODEL_ONLY_PROVIDERS = new Set<string>(["kimi-web", "zai-web"]);
export function providerUsesCuratedModelsOnly(providerId: string): boolean {
return CURATED_MODEL_ONLY_PROVIDERS.has(providerId.trim().toLowerCase());

View File

@@ -31,7 +31,6 @@ import {
validateDeepSeekWebProvider,
validateQwenWebProvider,
validateGrokWebProvider,
validateChatGptWebProvider,
validatePerplexityWebProvider,
validateBlackboxWebProvider,
validateKimiWebProvider,
@@ -166,7 +165,11 @@ export async function validateFreebuffProvider({ apiKey }: { apiKey: string }) {
return { valid: false, error: "Invalid or expired Freebuff Auth Token", unsupported: false };
}
const errText = await res.text().catch(() => "");
return { valid: false, error: `Freebuff validation returned ${res.status}: ${errText.slice(0, 100)}`, unsupported: false };
return {
valid: false,
error: `Freebuff validation returned ${res.status}: ${errText.slice(0, 100)}`,
unsupported: false,
};
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : String(err);
return { valid: false, error: `Freebuff validation network error: ${msg}`, unsupported: false };
@@ -307,7 +310,6 @@ export async function validateProviderApiKey({ provider, apiKey, providerSpecifi
"grok-web": validateGrokWebProvider,
"qwen-web": validateQwenWebProvider,
"kimi-web": validateKimiWebProvider,
"chatgpt-web": validateChatGptWebProvider,
"chatgpt-web-codex": validateChatGptWebCodexProvider,
"perplexity-web": validatePerplexityWebProvider,
"blackbox-web": validateBlackboxWebProvider,
@@ -376,7 +378,7 @@ export async function validateProviderApiKey({ provider, apiKey, providerSpecifi
// Web-cookie providers WITHOUT a dedicated specialty validator above fall back to the generic
// session-ping check (AUTH_007 SESSION_EXPIRED on 401/403). Providers that DO have a rich
// per-provider validator (grok-web, chatgpt-web, claude-web, ) are handled by
// per-provider validator (grok-web, perplexity-web, claude-web, etc.) are handled by
// SPECIALTY_VALIDATORS first and must not be shadowed by this generic probe (issue: the
// #4023 dispatch was placed too early and intercepted every web-cookie provider).
const canonicalProvider = resolveProviderId(provider);

View File

@@ -120,7 +120,6 @@ const WEB_COOKIE_PROVIDERS_WITH_UNRELIABLE_MODELS_PROBE = new Set(["lmarena"]);
// WEB_COOKIE_PROVIDERS_WITH_UNRELIABLE_MODELS_PROBE/REDIRECT_BLOCKED path above (#7542).
export const WEB_COOKIE_PROVIDERS_WITHOUT_MODELS_API = new Set([
"huggingchat",
"chatgpt-web",
"grok-web",
"notion-web",
"t3-web",

View File

@@ -1,4 +1,4 @@
// Web-cookie provider key validators (part A): deepseek-web, qwen-web, grok-web, chatgpt-web,
// Web-cookie provider key validators (part A): deepseek-web, qwen-web, grok-web,
// perplexity-web, blackbox-web. Extracted from validation.ts (god-file decomposition) — top-level
// functions with no dispatcher-state captures; behavior is byte-identical to the original inline defs.
import { addModelsSuffix } from "./urlHelpers";
@@ -488,114 +488,6 @@ export async function validateGrokWebProvider({ apiKey, providerSpecificData = {
}
}
export async function validateChatGptWebProvider({ apiKey, providerSpecificData = {} }: any) {
try {
// Accept bare value, unchunked cookie, chunked (.0/.1) cookies, or full
// "Cookie: ..." DevTools line. Pass through verbatim once recognised.
let cookieHeader = String(apiKey || "").trim();
if (/^cookie\s*:\s*/i.test(cookieHeader)) {
cookieHeader = cookieHeader.replace(/^cookie\s*:\s*/i, "");
}
if (!/__Secure-next-auth\.session-token(?:\.\d+)?\s*=/.test(cookieHeader)) {
cookieHeader = `__Secure-next-auth.session-token=${cookieHeader}`;
}
// Use the TLS-impersonating client — Cloudflare on chatgpt.com pins
// cf_clearance to JA3/JA4 + HTTP/2 SETTINGS, so plain Node fetch always
// gets cf-mitigated: challenge regardless of cookies.
const { tlsFetchChatGpt, TlsClientUnavailableError } =
await import("@omniroute/open-sse/services/chatgptTlsClient.ts");
let response;
try {
response = await tlsFetchChatGpt("https://chatgpt.com/api/auth/session", {
method: "GET",
headers: applyCustomUserAgent(
{
Accept: "application/json",
"Accept-Language": "en-US,en;q=0.9",
"Cache-Control": "no-cache",
Cookie: cookieHeader,
Origin: "https://chatgpt.com",
Pragma: "no-cache",
Referer: "https://chatgpt.com/",
"Sec-Fetch-Dest": "empty",
"Sec-Fetch-Mode": "cors",
"Sec-Fetch-Site": "same-origin",
"User-Agent":
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:152.0) Gecko/20100101 Firefox/152.0",
},
providerSpecificData
),
timeoutMs: 30_000,
});
} catch (err: any) {
if (err instanceof TlsClientUnavailableError) {
return {
valid: false,
error: `${err.message} (chatgpt-web requires this — without it, Cloudflare blocks every request)`,
};
}
throw err;
}
const contentType = response.headers.get("content-type") || "";
const cfRay = response.headers.get("cf-ray");
const cfMitigated = response.headers.get("cf-mitigated");
if (response.status === 401 || response.status === 403) {
const bodyText = response.text || "";
if (cfMitigated || /just a moment|cloudflare|cf-chl|attention required/i.test(bodyText)) {
return {
valid: false,
error:
"Cloudflare blocked the validator — open chatgpt.com in your browser, then copy the FULL Cookie line from DevTools (Network → request → Cookie) including cf_clearance, __cf_bm, _cfuvid, and the session-token chunks.",
};
}
return {
valid: false,
error:
"Invalid ChatGPT session cookie — re-paste __Secure-next-auth.session-token from chatgpt.com DevTools → Cookies",
};
}
if (response.status >= 500) {
return { valid: false, error: `ChatGPT unavailable (${response.status})` };
}
if (response.status >= 400) {
return { valid: false, error: `Validation failed: ${response.status}` };
}
if (!contentType.includes("json")) {
return {
valid: false,
error: `ChatGPT returned non-JSON (${contentType || "no content-type"}${cfRay ? `, cf-ray=${cfRay}` : ""}) — paste the FULL Cookie line including cf_clearance, __cf_bm, _cfuvid alongside the session-token chunks.`,
};
}
let data: any = {};
try {
data = JSON.parse(response.text || "{}");
} catch {
return {
valid: false,
error:
"ChatGPT session response was not JSON — paste the FULL Cookie line including cf_clearance and __cf_bm.",
};
}
if (!data?.accessToken) {
return {
valid: false,
error: "ChatGPT session expired — log into chatgpt.com and copy a fresh cookie",
};
}
return { valid: true, error: null };
} catch (error: any) {
return toValidationErrorResult(error);
}
}
export async function validatePerplexityWebProvider({ apiKey, providerSpecificData = {} }: any) {
try {
let sessionToken = apiKey;

View File

@@ -2,7 +2,7 @@
* Flat-rate (subscription / cookie-web) provider classification — issue #5552.
*
* Some providers are billed at a flat rate (a subscription or a coding plan),
* not per token: cookie/web sessions (ChatGPT Web, grok-web, …) are backed by a
* not per token: cookie/web sessions (ChatGPT Web (Codex), grok-web, …) are backed by a
* consumer subscription, and several "Coding Plan" providers (Codex, MiniMax
* Coding, Kimi Coding, GLM Coding, …) bill a fixed monthly fee. These providers
* still carry per-token pricing rows (used for pre-flight estimates), so cost