mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-17 04:12:17 +03:00
Adds uncensored.com as two OpenAI-compatible providers mirroring UC's own surfaces: uc, the persona/subscription side over WebSocket with a durable Clerk credential minting a short-lived per-connect token (no API key, un-metered), as a full multimodal port — chat, tools, vision, doc-RAG, image, video, TTS; and uc-direct, the metered Developer API over REST with X-api-key. Same underlying models, two billing surfaces. Reconciled on merge. 57 files conflicted; only seven carried UC content, the rest was drift from the older release line and took the tip's side. - executors/index.ts: the tip has since refactored the executor map to lazy dynamic imports, so uc is registered in that shape. uc-direct needs no entry — it routes through the default OpenAI-compatible executor. - imageGeneration.ts: the branch still carried the retired designerWeb import alongside ucImage; kept only the UC one. - config/providers/index.ts, webSessionCredentials.ts and web-cookie.ts resolved additively against the MaxAI entries #11461 put on the tip an hour earlier. - web-cookie.ts: the uc entry declared no serviceKinds, required since #11392, so provider validation would have thrown at load. Declared ["llm"]. uc-direct already declared it at the end of its own entry — an earlier pass of mine added a second one after id and TypeScript caught the duplicate (TS1117); the author's placement is what shipped. Every count was measured against the merged tree rather than taken from the branch, and all three would have been wrong: reserved prefixes are 406, not 399; APIKEY_PROVIDERS is 237, not 234; providers are 355. PROVIDER_REFERENCE.md regenerated, the count updated across README/AGENTS.md/llm.txt and its 42 mirrors, package.json and 6 SVGs — every changed line in the protected surfaces is a digit substitution and nothing else, verified by masking digits and comparing the removed and added sets (90 lines each, identical). The executor-map golden snapshot went 134 -> 135. The branch's file-size-baseline.json predates #12411's ratchet re-tightening and was discarded rather than merged; imageGeneration.ts (+12 for the uc-image format branch) was entered against the current baseline under a _rebaseline annotation, and no other cap moves. Verified: typecheck:core clean, check:provider-consistency OK (271 REGISTRY entries, 355 canonical providers), check:docs-counts exit 0, check-file-size OK, check:cycles OK, 119/119 across the PR's test files, and 2/2 executor-map-golden. Thanks @arminanton — two providers for two real billing surfaces, rather than one entry pretending to be both, is the right modelling.
180 lines
6.1 KiB
TypeScript
180 lines
6.1 KiB
TypeScript
/**
|
|
* UC (uncensored.com) PERSONA WebSocket driver.
|
|
*
|
|
* Opens one socket per turn (connect → send the persona frame → stream frames →
|
|
* close), mirroring the reference client and the muse-spark-web WS executor. Auth
|
|
* is 100% the `?token=` query param (a 60s Clerk JWT); the ONLY required
|
|
* handshake header is `Origin: https://uncensored.com` (the backend checks it —
|
|
* NO Cookie, NO Authorization on the upgrade).
|
|
*
|
|
* The driver is transport-only: it classifies frames via UcFrameParser and hands
|
|
* each event to an `onEvent` callback, so the executor can drive both a live
|
|
* OpenAI SSE stream and a buffered non-streaming response from the same path. The
|
|
* module-level constructor + `__setUcWebSocketForTesting` hook let tests inject a
|
|
* fake socket (same pattern as muse-spark-web).
|
|
*/
|
|
import WebSocket from "ws";
|
|
|
|
import { UC_ORIGIN, UC_WS_HOST, UC_WS_TIMEOUT_MS } from "./constants.ts";
|
|
import { buildPersonaFrame, type UcHistoryEntry } from "./protocol.ts";
|
|
import { UcFrameParser, type UcEvent } from "./stream.ts";
|
|
|
|
let WebSocketCtor: typeof WebSocket = WebSocket;
|
|
|
|
/** Inject a fake WebSocket constructor for tests. Returns a restore fn. */
|
|
export function __setUcWebSocketForTesting(ctor: typeof WebSocket): () => void {
|
|
const previous = WebSocketCtor;
|
|
WebSocketCtor = ctor;
|
|
return () => {
|
|
WebSocketCtor = previous;
|
|
};
|
|
}
|
|
|
|
/** Build the persona WS URL: wss://.../ws/{uid}?token={jwt}&_t={epochms}. */
|
|
export function buildUcWsUrl(uid: string, jwt: string): string {
|
|
return `${UC_WS_HOST}/${encodeURIComponent(uid)}?token=${encodeURIComponent(jwt)}&_t=${Date.now()}`;
|
|
}
|
|
|
|
export interface UcTurnInput {
|
|
jwt: string;
|
|
uid: string;
|
|
model: string;
|
|
text: string;
|
|
history: UcHistoryEntry[];
|
|
/** Uploaded input-media blobs (images/docs) for the current turn. */
|
|
media?: Array<{ blobName: string; contentType: string }>;
|
|
timeoutMs?: number;
|
|
signal?: AbortSignal | null;
|
|
/** Called for each classified event (delta/reasoning/status/done/error). */
|
|
onEvent?: (evt: UcEvent) => void;
|
|
}
|
|
|
|
export interface UcTurnResult {
|
|
/** The final answer text (raw_text authoritative, else concatenated deltas). */
|
|
content: string;
|
|
/** Reasoning text accumulated from intermediary_message frames. */
|
|
reasoning: string;
|
|
/** Set when the turn failed (error frame, transport failure, or timeout). */
|
|
error?: string;
|
|
}
|
|
|
|
/**
|
|
* Drive one persona turn to completion. Never rejects — a transport/timeout/error
|
|
* failure resolves with `{ error }` set (and any partial content). The caller
|
|
* decides whether a partial is usable or should surface the error.
|
|
*/
|
|
export function runUcTurn(input: UcTurnInput): Promise<UcTurnResult> {
|
|
const timeoutMs = input.timeoutMs ?? UC_WS_TIMEOUT_MS;
|
|
const url = buildUcWsUrl(input.uid, input.jwt);
|
|
const parser = new UcFrameParser();
|
|
const reasoningParts: string[] = [];
|
|
|
|
return new Promise<UcTurnResult>((resolve) => {
|
|
let ws: WebSocket;
|
|
try {
|
|
ws = new WebSocketCtor(url, {
|
|
headers: { Origin: UC_ORIGIN },
|
|
// The persona frame + long answers can exceed the default 100MB cap only
|
|
// in pathological cases; leave the library default. permessage-deflate is
|
|
// negotiated by the server and handled by `ws` transparently.
|
|
});
|
|
} catch (err) {
|
|
resolve({
|
|
content: "",
|
|
reasoning: "",
|
|
error: `ws connect failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
});
|
|
return;
|
|
}
|
|
|
|
let settled = false;
|
|
let errorText: string | undefined;
|
|
let timeout: ReturnType<typeof setTimeout> | null = null;
|
|
let abortHandler: (() => void) | null = null;
|
|
|
|
const finish = (result: UcTurnResult) => {
|
|
if (settled) return;
|
|
settled = true;
|
|
if (timeout) clearTimeout(timeout);
|
|
if (input.signal && abortHandler) input.signal.removeEventListener("abort", abortHandler);
|
|
try {
|
|
ws.close();
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
resolve(result);
|
|
};
|
|
|
|
const fail = (error: string) =>
|
|
finish({ content: parser.accumulated.trim(), reasoning: reasoningParts.join(""), error });
|
|
|
|
timeout = setTimeout(
|
|
() => fail(`UC persona WS timed out (readyState=${ws.readyState})`),
|
|
timeoutMs
|
|
);
|
|
abortHandler = () => fail("Request aborted");
|
|
input.signal?.addEventListener("abort", abortHandler, { once: true });
|
|
|
|
ws.onopen = () => {
|
|
try {
|
|
const frame = buildPersonaFrame({
|
|
model: input.model,
|
|
text: input.text,
|
|
history: input.history,
|
|
uid: input.uid,
|
|
media: input.media,
|
|
});
|
|
ws.send(JSON.stringify(frame));
|
|
} catch (err) {
|
|
fail(`ws send failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
}
|
|
};
|
|
|
|
ws.onmessage = (event: WebSocket.MessageEvent) => {
|
|
let raw = "";
|
|
const data = event.data as unknown;
|
|
if (typeof data === "string") {
|
|
raw = data;
|
|
} else if (Buffer.isBuffer(data)) {
|
|
raw = data.toString("utf-8");
|
|
} else if (data instanceof ArrayBuffer) {
|
|
raw = new TextDecoder().decode(data);
|
|
} else if (ArrayBuffer.isView(data as ArrayBufferView)) {
|
|
raw = new TextDecoder().decode(data as ArrayBufferView);
|
|
}
|
|
if (!raw) return;
|
|
|
|
for (const evt of parser.feed(raw)) {
|
|
input.onEvent?.(evt);
|
|
if (evt.kind === "reasoning") {
|
|
reasoningParts.push(evt.text);
|
|
} else if (evt.kind === "error") {
|
|
errorText = evt.text;
|
|
} else if (evt.kind === "done") {
|
|
finish({ content: evt.text, reasoning: reasoningParts.join("") });
|
|
return;
|
|
}
|
|
}
|
|
if (parser.done) {
|
|
// Terminal error frame consumed by the parser.
|
|
finish({
|
|
content: parser.accumulated.trim(),
|
|
reasoning: reasoningParts.join(""),
|
|
error: errorText,
|
|
});
|
|
}
|
|
};
|
|
|
|
ws.onerror = () => fail("UC persona WebSocket connection error");
|
|
ws.onclose = () => {
|
|
if (settled) return;
|
|
// Closed without an explicit end_of_stream: use whatever we accumulated.
|
|
finish({
|
|
content: parser.finalText(),
|
|
reasoning: reasoningParts.join(""),
|
|
error: errorText,
|
|
});
|
|
};
|
|
});
|
|
}
|