Files
OmniRoute/open-sse/executors/chatgpt-web/handoff.ts
backryun 67a0b99240 feat(providers): add GPT-5.6 model family (#6862)
* feat(providers): add GPT-5.6 model family

* fix(chatgpt-web): resume temporary chat handoffs

* fix(codex): auto-merge discovery, filter denylist, revalidate on lifecycle

Restore live/GitHub auto-merge for Codex catalogs, drop models via
explicit denylist (GPT-5.4 family), and run scrub+live re-sync once on
first-start, app upgrade, or setup completion. Success log:
kill deprecated models complete.

* fix(codex): preserve live catalog reconciliation

Expose remote-only Codex models without dropping user custom entries, and complete lifecycle revalidation only after a successful internal sync. Keep credentialed self-fetches pinned to the active dashboard listener.

---------

Co-authored-by: backryun <backryun@daonlab.local>
2026-07-12 03:30:47 -03:00

155 lines
4.2 KiB
TypeScript

import { tlsFetchChatGpt } from "../../services/chatgptTlsClient.ts";
const CONVERSATION_RESUME_URL = "https://chatgpt.com/backend-api/f/conversation/resume";
const RESUME_OFFSETS = [0, 1, 2] as const;
export interface FinalAssistantAnswer {
text: string;
messageId?: string;
metadata?: Record<string, unknown>;
finished: boolean;
}
interface HandoffContentChunk {
answer?: string;
messageId?: string;
metadata?: Record<string, unknown>;
error?: string;
}
type HandoffContentReader = (
eventStream: ReadableStream<Uint8Array>,
signal?: AbortSignal | null
) => AsyncIterable<HandoffContentChunk>;
interface ResumeHandoffOptions {
conversationId: string;
resumeToken: string;
headers: Record<string, string>;
timeoutMs: number;
signal?: AbortSignal | null;
log?: { warn?: (tag: string, message: string) => void } | null;
readContent: HandoffContentReader;
}
interface ResumeAttemptOptions extends Pick<
ResumeHandoffOptions,
"conversationId" | "timeoutMs" | "signal" | "log" | "readContent"
> {
offset: (typeof RESUME_OFFSETS)[number];
resumeHeaders: Record<string, string>;
}
interface ResumeAttemptResult {
answer: FinalAssistantAnswer | null;
shouldRetry: boolean;
}
function stringToStream(text: string): ReadableStream<Uint8Array> {
const bytes = new TextEncoder().encode(text);
return new ReadableStream<Uint8Array>({
start(controller) {
controller.enqueue(bytes);
controller.close();
},
});
}
async function readFinalAssistantAnswer(
eventStream: ReadableStream<Uint8Array>,
signal: AbortSignal | null | undefined,
readContent: HandoffContentReader
): Promise<FinalAssistantAnswer | null> {
let text = "";
let messageId: string | undefined;
let metadata: Record<string, unknown> | undefined;
for await (const chunk of readContent(eventStream, signal)) {
if (chunk.error) return null;
if (chunk.answer) text = chunk.answer;
if (chunk.messageId) messageId = chunk.messageId;
if (chunk.metadata) metadata = chunk.metadata;
}
if (!text.trim()) return null;
return { text, messageId, metadata, finished: true };
}
async function attemptResumeOffset({
conversationId,
offset,
resumeHeaders,
timeoutMs,
signal,
log,
readContent,
}: ResumeAttemptOptions): Promise<ResumeAttemptResult> {
try {
const response = await tlsFetchChatGpt(CONVERSATION_RESUME_URL, {
method: "POST",
headers: resumeHeaders,
body: JSON.stringify({ conversation_id: conversationId, offset }),
timeoutMs,
signal,
stream: true,
});
if (response.status === 404) return { answer: null, shouldRetry: true };
if (response.status >= 400) {
log?.warn?.(
"CGPT-WEB",
`conversation resume ${response.status}: ${(response.text || "").slice(0, 300)}`
);
return { answer: null, shouldRetry: false };
}
const eventStream = response.body ?? (response.text ? stringToStream(response.text) : null);
if (!eventStream) return { answer: null, shouldRetry: true };
const answer = await readFinalAssistantAnswer(eventStream, signal, readContent);
return { answer, shouldRetry: !answer };
} catch (error) {
log?.warn?.(
"CGPT-WEB",
`conversation resume failed: ${error instanceof Error ? error.message : String(error)}`
);
return { answer: null, shouldRetry: false };
}
}
export async function resumeChatGptHandoff({
conversationId,
resumeToken,
headers,
timeoutMs,
signal,
log,
readContent,
}: ResumeHandoffOptions): Promise<FinalAssistantAnswer | null> {
const resumeHeaders = {
...headers,
Accept: "text/event-stream",
"Content-Type": "application/json",
"x-conduit-token": resumeToken,
"X-OpenAI-Target-Path": "/backend-api/f/conversation/resume",
"X-OpenAI-Target-Route": "/backend-api/f/conversation/resume",
};
for (const offset of RESUME_OFFSETS) {
const attempt = await attemptResumeOffset({
conversationId,
resumeHeaders,
offset,
timeoutMs,
signal,
log,
readContent,
});
if (attempt.answer) return attempt.answer;
if (!attempt.shouldRetry) return null;
}
log?.warn?.("CGPT-WEB", `conversation resume returned no assistant text for ${conversationId}`);
return null;
}