mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-21 14:22:14 +03:00
fix(providers): retire common ChatGPT Web provider
This commit is contained in:
committed by
Markus Hartung
parent
825f8fe425
commit
9c5dd6760c
195
src/lib/db/migrations/163_retire_chatgpt_web.sql
Normal file
195
src/lib/db/migrations/163_retire_chatgpt_web.sql
Normal 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;
|
||||
@@ -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 {
|
||||
|
||||
@@ -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("/");
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
30
src/lib/providers/chatgptWebRetirementResponse.ts
Normal file
30
src/lib/providers/chatgptWebRetirementResponse.ts
Normal 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;
|
||||
}
|
||||
@@ -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());
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user