feat(providers): add Conol (conol.ai) web session provider (#8974)

* feat(providers): add Conol web support

* fix(conol): preserve sessions and image turns

* fix(conol): pin session model and effort via /model endpoint

Conol ignores agentModel/agentEffort on POST /api/sessions, so every
session silently ran on the downgraded account default (the create
response reports modelDowngraded: true / effectiveModel).

Sessions are now created empty and configured out-of-band against
POST /api/sessions/{id}/model before the first turn is submitted, in the
order the web client uses: modelPreset, then agentModel, then agentEffort.
The ordering is load-bearing because the model call resets agentEffort to
null server-side.

Effort now defaults to xhigh when the caller does not pin one via the
-<effort> model suffix, and is clamped onto the ladder each model actually
advertises, so xhigh degrades to high on claude-sonnet-5 and is skipped
entirely for models without an effort ladder such as openrouter/fusion.

Model and effort are also dropped from the session binding key so switching
models re-pins the existing session instead of stranding it and losing the
conversation history. Re-pinning only happens on an actual change, so
steady-state follow-ups cost no extra round trips.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
NOXX - Commiter
2026-08-11 15:08:23 +03:00
committed by GitHub
parent 55c2b35eb7
commit f5ce51a9ff
21 changed files with 2870 additions and 1 deletions

View File

@@ -101,6 +101,7 @@ import { sensenovaProvider } from "./registry/sensenova/index.ts";
import { hyperbolicProvider } from "./registry/hyperbolic/index.ts";
import { lambda_aiProvider } from "./registry/lambda-ai/index.ts";
import { t3_webProvider } from "./registry/t3-web/index.ts";
import { conol_webProvider } from "./registry/conol-web/index.ts";
import { iflytekProvider } from "./registry/iflytek/index.ts";
import { crofProvider } from "./registry/crof/index.ts";
import { moonshotProvider } from "./registry/moonshot/index.ts";
@@ -331,6 +332,7 @@ export const REGISTRY: Record<string, RegistryEntry> = {
hyperbolic: hyperbolicProvider,
"lambda-ai": lambda_aiProvider,
"t3-web": t3_webProvider,
"conol-web": conol_webProvider,
iflytek: iflytekProvider,
crof: crofProvider,
moonshot: moonshotProvider,

View File

@@ -0,0 +1,14 @@
import type { RegistryEntry } from "../../shared.ts";
import { CONOL_FALLBACK_MODELS } from "../../../services/conolModels.ts";
export const conol_webProvider: RegistryEntry = {
id: "conol-web",
alias: "cnl",
format: "openai",
executor: "conol-web",
baseUrl: "https://conol.ai/api/sessions",
authType: "apikey",
authHeader: "cookie",
passthroughModels: true,
models: CONOL_FALLBACK_MODELS,
};

View File

@@ -0,0 +1,893 @@
/**
* ConolExecutor — conol.ai browser-session chat (Unofficial/Experimental).
*
* Protocol verified against the web client on 2026-07-30:
* - POST /api/assets for raw image uploads
* - POST /api/sessions to create a session
* - POST /api/sessions/{id}/model to pin preset, then model, then effort
* - POST /api/sessions/{id}/messages to submit a turn
* - GET /api/sessions/{id}/messages?logDeltas=1 for cumulative NDJSON updates
* - Cookie authentication via __Secure-better-auth.session_token
*
* Session creation ignores agentModel/agentEffort and answers with
* `modelDowngraded: true` on the account default, so the session is always
* created empty and configured via /model before the first turn is submitted.
*/
import { createHash } from "node:crypto";
import { BaseExecutor, mergeAbortSignals, type ExecuteInput } from "./base.ts";
import { makeExecutorErrorResult as makeErrorResult } from "../utils/error.ts";
import { CursorImageError, extractImageUrls, resolveCursorImages } from "../utils/cursorImages.ts";
import { normalizeConolCookie, resolveConolCredentials } from "../services/conolAuth.ts";
import { resolveConolModelSelection, type ConolEffort } from "../services/conolModels.ts";
import {
applyConolSessionModel,
buildConolSessionModelPlan,
} from "../services/conolSessionModel.ts";
export { normalizeConolCookie, resolveConolCredentials };
const CONOL_ORIGIN = "https://conol.ai";
const CONOL_SESSION_URL = `${CONOL_ORIGIN}/api/sessions`;
const CONOL_REQUEST_TIMEOUT_MS = 300_000;
const CONOL_MAX_STREAM_BYTES = 16 * 1024 * 1024;
const CONOL_SESSION_TTL_MS = 6 * 60 * 60 * 1000;
const CONOL_MAX_SESSION_BINDINGS = 500;
const USER_AGENT =
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36";
interface ChatMessage {
role: string;
content: unknown;
}
interface ConolRequestBody {
messages?: ChatMessage[];
model?: string;
timezone?: string;
metadata?: unknown;
conversation_id?: unknown;
conversationId?: unknown;
session_id?: unknown;
sessionId?: unknown;
prompt_cache_key?: unknown;
promptCacheKey?: unknown;
}
interface ConolMessagePart {
type: "text" | "image";
content: string;
mediaType?: string;
}
interface ConolUserTurn {
text: string;
imageUrls: string[];
}
interface ConolSessionBinding {
upstreamSessionId: string;
lastUsedAt: number;
/** Model preset already primed on this session — sent once, not per turn. */
presetApplied: boolean;
/** Model/effort currently pinned upstream, so we only re-pin on an actual switch. */
appliedModel: string;
appliedEffort: ConolEffort | null;
/** Conol wants `hasImageHistory` sticky once the session has seen an image. */
hasImageHistory: boolean;
}
export interface ParsedConolStream {
text: string;
usedTokens: number | null;
contextWindow: number | null;
modelId: string;
done: boolean;
}
const conolSessionBindings = new Map<string, ConolSessionBinding>();
const conolSessionLocks = new Map<string, Promise<void>>();
function readString(value: unknown): string {
return typeof value === "string" ? value.trim() : "";
}
function extractText(value: unknown): string {
if (typeof value === "string") return value;
if (value == null) return "";
if (Array.isArray(value)) {
return value
.map((item) => extractText(item))
.filter(Boolean)
.join("\n");
}
if (typeof value !== "object") return "";
const record = value as Record<string, unknown>;
const type = readString(record.type).toLowerCase();
if (type === "image_url" || type === "input_image" || type === "image") return "";
return (
readString(record.text) ||
(typeof record.content === "string" ? record.content : extractText(record.content)) ||
extractText(record.output) ||
extractText(record.result)
);
}
function extractUserText(value: unknown): string {
if (typeof value === "string") return value;
if (value == null) return "";
if (Array.isArray(value)) {
return value
.map((item) => extractUserText(item))
.filter(Boolean)
.join("\n");
}
if (typeof value !== "object") return "";
const record = value as Record<string, unknown>;
const type = readString(record.type).toLowerCase();
if (type === "text" || type === "input_text" || type === "output_text") {
return readString(record.text) || readString(record.content);
}
if (type) {
// Conol owns the agent loop. Do not flatten tool calls/results, images, or
// other agentic protocol blocks into the user's text prompt.
return "";
}
return readString(record.text) || extractUserText(record.content);
}
function stripGeneratedImageMarkers(value: string): string {
return value
.replace(/^\s*\[Image\s+\d+\]:\s*\(unavailable\)\s*$/gim, "")
.replace(/^\s*\[Image:\s*source:\s*[^\]\r\n]+\]\s*$/gim, "")
.replace(/\n{3,}/g, "\n\n")
.trim();
}
export function buildConolUserTurn(messages: ChatMessage[]): ConolUserTurn {
const latestUserMessage = [...messages]
.reverse()
.find((message) => readString(message.role).toLowerCase() === "user");
if (!latestUserMessage) return { text: "", imageUrls: [] };
return {
text: stripGeneratedImageMarkers(extractUserText(latestUserMessage.content)),
imageUrls: extractImageUrls(latestUserMessage.content),
};
}
export function buildConolPromptText(messages: ChatMessage[]): string {
return buildConolUserTurn(messages).text;
}
function readHeader(headers: Record<string, string> | null | undefined, name: string): string {
if (!headers) return "";
const direct = readString(headers[name]);
if (direct) return direct;
const normalizedName = name.toLowerCase();
for (const [key, value] of Object.entries(headers)) {
if (key.toLowerCase() === normalizedName) return readString(value);
}
return "";
}
function readMetadataSessionId(metadata: unknown): string {
if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) return "";
const record = metadata as Record<string, unknown>;
const direct = readString(record.session_id) || readString(record.sessionId);
if (direct) return direct;
const userId = record.user_id;
if (userId && typeof userId === "object" && !Array.isArray(userId)) {
return readString((userId as Record<string, unknown>).session_id);
}
if (typeof userId !== "string" || userId.length > 4096) return "";
try {
const parsed = JSON.parse(userId) as unknown;
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
? readString((parsed as Record<string, unknown>).session_id)
: "";
} catch {
return "";
}
}
function hashKey(value: string): string {
return createHash("sha256").update(value).digest("hex");
}
export function resolveConolClientSessionKey(
body: ConolRequestBody,
clientHeaders?: Record<string, string> | null
): string | null {
const candidates = [
readHeader(clientHeaders, "x-claude-code-session-id"),
readHeader(clientHeaders, "x-codex-session-id"),
readHeader(clientHeaders, "x-session-id"),
readHeader(clientHeaders, "x_session_id"),
readHeader(clientHeaders, "session-id"),
readHeader(clientHeaders, "session_id"),
readHeader(clientHeaders, "x-omniroute-session-id"),
readHeader(clientHeaders, "x-omniroute-session"),
readMetadataSessionId(body.metadata),
readString(body.conversation_id),
readString(body.conversationId),
readString(body.session_id),
readString(body.sessionId),
readString(body.prompt_cache_key),
readString(body.promptCacheKey),
];
const candidate = candidates.find((value) => value.length > 0 && value.length <= 4096);
return candidate ? hashKey(candidate) : null;
}
function sweepConolSessionBindings(now = Date.now()): void {
for (const [key, binding] of conolSessionBindings) {
if (now - binding.lastUsedAt > CONOL_SESSION_TTL_MS) {
conolSessionBindings.delete(key);
}
}
while (conolSessionBindings.size > CONOL_MAX_SESSION_BINDINGS) {
let oldestKey = "";
let oldestTime = Number.POSITIVE_INFINITY;
for (const [key, binding] of conolSessionBindings) {
if (binding.lastUsedAt < oldestTime) {
oldestKey = key;
oldestTime = binding.lastUsedAt;
}
}
if (!oldestKey) break;
conolSessionBindings.delete(oldestKey);
}
}
function getConolSessionBinding(key: string): ConolSessionBinding | null {
sweepConolSessionBindings();
const binding = conolSessionBindings.get(key);
if (!binding) return null;
binding.lastUsedAt = Date.now();
return binding;
}
function setConolSessionBinding(
key: string,
binding: Omit<ConolSessionBinding, "lastUsedAt">
): void {
conolSessionBindings.set(key, { ...binding, lastUsedAt: Date.now() });
sweepConolSessionBindings();
}
/**
* Model/effort are deliberately excluded: switching models must re-pin the
* existing Conol session (POST /model) rather than stranding it and losing the
* conversation history.
*/
function buildConolSessionBindingKey(
input: ExecuteInput,
cookie: string,
clientSessionKey: string
): string {
const accountKey = input.credentials.connectionId
? `connection:${hashKey(input.credentials.connectionId)}`
: `cookie:${hashKey(cookie)}`;
return hashKey(`${accountKey}:${clientSessionKey}`);
}
async function withConolSessionLock<T>(
key: string | null,
operation: () => Promise<T>
): Promise<T> {
if (!key) return operation();
const previous = conolSessionLocks.get(key) ?? Promise.resolve();
let releaseCurrent!: () => void;
const currentGate = new Promise<void>((resolve) => {
releaseCurrent = resolve;
});
const current = previous.catch(() => undefined).then(() => currentGate);
conolSessionLocks.set(key, current);
await previous.catch(() => undefined);
try {
return await operation();
} finally {
releaseCurrent();
if (conolSessionLocks.get(key) === current) {
conolSessionLocks.delete(key);
}
}
}
export function clearConolSessionBindingsForTests(): void {
conolSessionBindings.clear();
conolSessionLocks.clear();
}
/** True when this turn continues a session we created on an earlier request. */
function reusedSessionCandidate(
cachedBinding: ConolSessionBinding | null,
sessionId: string
): boolean {
return !!cachedBinding && cachedBinding.upstreamSessionId === sessionId;
}
function messageText(value: unknown): string {
if (!value || typeof value !== "object" || Array.isArray(value)) return "";
const message = value as Record<string, unknown>;
if (readString(message.role).toLowerCase() !== "assistant") return "";
return extractText(message.content).trim();
}
function stageAssistantText(stages: unknown, field: "logs" | "preview"): string {
if (!Array.isArray(stages)) return "";
let result = "";
for (const stage of stages) {
if (!stage || typeof stage !== "object" || Array.isArray(stage)) continue;
const entries = (stage as Record<string, unknown>)[field];
if (!Array.isArray(entries)) continue;
for (const entry of entries) {
const text = messageText(entry);
if (text) result = text;
}
}
return result;
}
function parseEventLine(originalLine: string): unknown | null {
let line = originalLine.trim();
if (!line || line.startsWith(":") || line.startsWith("event:")) return null;
if (line.startsWith("data:")) line = line.slice(5).trim();
if (line.startsWith("message\t")) line = line.slice("message\t".length);
if (!line) return null;
if (line === "[DONE]") return { type: "done" };
try {
return JSON.parse(line);
} catch {
// Ignore non-JSON keepalive and timestamp lines.
return null;
}
}
function isDoneEvent(value: unknown): boolean {
return (
!!value &&
typeof value === "object" &&
!Array.isArray(value) &&
readString((value as Record<string, unknown>).type) === "done"
);
}
function parseEventLines(raw: string): unknown[] {
const events: unknown[] = [];
for (const line of raw.replace(/\r\n/g, "\n").split("\n")) {
const event = parseEventLine(line);
if (event) events.push(event);
}
return events;
}
/**
* Conol emits a terminal `done` event but keeps the HTTP stream open. Reading
* `response.text()` therefore waits until the request timeout even though the
* assistant answer is already complete. Consume complete lines and cancel the
* reader as soon as `done` arrives.
*/
export async function collectConolMessageStream(response: Response): Promise<string> {
if (!response.body) return response.text();
const reader = response.body.getReader();
const decoder = new TextDecoder();
const lines: string[] = [];
let pending = "";
let totalBytes = 0;
let doneEventReceived = false;
try {
while (!doneEventReceived) {
const chunk = await reader.read();
if (chunk.done) {
pending += decoder.decode();
break;
}
totalBytes += chunk.value.byteLength;
if (totalBytes > CONOL_MAX_STREAM_BYTES) {
throw new Error("Conol message stream exceeded the safety limit");
}
pending += decoder.decode(chunk.value, { stream: true });
const completeLines = pending.split(/\r?\n/);
pending = completeLines.pop() ?? "";
for (const line of completeLines) {
lines.push(line);
if (isDoneEvent(parseEventLine(line))) {
doneEventReceived = true;
break;
}
}
}
if (!doneEventReceived && pending) lines.push(pending);
} finally {
if (doneEventReceived) {
try {
await reader.cancel();
} catch {
// The upstream may close at the same instant as its done event.
}
} else {
reader.releaseLock();
}
}
return lines.join("\n");
}
export function parseConolMessageStream(raw: string): ParsedConolStream {
let finalizedText = "";
let previewText = "";
let streamedText = "";
let usedTokens: number | null = null;
let contextWindow: number | null = null;
let modelId = "";
let done = false;
for (const value of parseEventLines(raw)) {
if (!value || typeof value !== "object" || Array.isArray(value)) continue;
const event = value as Record<string, unknown>;
const type = readString(event.type);
if (type === "done") {
done = true;
continue;
}
const finalCandidate = stageAssistantText(event.stages, "logs");
const previewCandidate = stageAssistantText(event.stages, "preview");
if (finalCandidate) finalizedText = finalCandidate;
if (previewCandidate) previewText = previewCandidate;
if (type === "assistant") {
const direct = extractText(event.content ?? event.message ?? event.text).trim();
if (direct) finalizedText = direct;
} else if (type === "stream_event") {
const delta = extractText(event.delta ?? event.content ?? event.text);
if (delta) streamedText += delta;
}
const context =
event.contextUsage &&
typeof event.contextUsage === "object" &&
!Array.isArray(event.contextUsage)
? (event.contextUsage as Record<string, unknown>)
: null;
if (context) {
const used = Number(context.usedTokens);
const window = Number(context.contextWindow);
if (Number.isFinite(used)) usedTokens = used;
if (Number.isFinite(window)) contextWindow = window;
modelId = readString(context.modelId) || modelId;
}
}
return {
text: finalizedText || previewText || streamedText,
usedTokens,
contextWindow,
modelId,
done,
};
}
function conolHeaders(
cookie: string,
extra?: Record<string, string>,
sessionId?: string
): Record<string, string> {
return {
accept: "application/json",
"accept-language": "en-US,en;q=0.9",
cookie,
origin: CONOL_ORIGIN,
referer: sessionId
? `${CONOL_ORIGIN}/home?chat_session=${encodeURIComponent(sessionId)}`
: `${CONOL_ORIGIN}/home`,
"user-agent": USER_AGENT,
...extra,
};
}
function safeTimezone(value: unknown): string {
const explicit = readString(value);
if (/^[A-Za-z_+-]+(?:\/[A-Za-z0-9_+-]+)*$/.test(explicit)) return explicit;
try {
return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
} catch {
return "UTC";
}
}
async function uploadConolImages(
cookie: string,
imageUrls: string[],
signal?: AbortSignal | null,
sessionId?: string
): Promise<ConolMessagePart[]> {
const images = await resolveCursorImages(imageUrls);
const parts: ConolMessagePart[] = [];
for (const image of images) {
const response = await fetch(`${CONOL_ORIGIN}/api/assets`, {
method: "POST",
headers: conolHeaders(
cookie,
{
accept: "application/json",
"content-type": image.mimeType,
},
sessionId
),
body: image.data,
signal: signal ?? undefined,
});
if (!response.ok) {
throw new Error(`Conol image upload failed (HTTP ${response.status})`);
}
const payload = (await response.json()) as Record<string, unknown>;
const id = readString(payload.id);
if (!/^[A-Za-z0-9_-]+$/.test(id)) {
throw new Error("Conol image upload returned an invalid asset ID");
}
parts.push({
type: "image",
content: `/api/assets/${id}`,
mediaType: readString(payload.mediaType) || image.mimeType,
});
}
return parts;
}
function estimateTokens(text: string): number {
return Math.max(0, Math.ceil(text.length / 4));
}
function completionResponse(
text: string,
model: string,
sessionId: string,
prompt: string
): Response {
const promptTokens = estimateTokens(prompt);
const completionTokens = estimateTokens(text);
return new Response(
JSON.stringify({
id: `chatcmpl-conol-${sessionId}`,
object: "chat.completion",
created: Math.floor(Date.now() / 1000),
model,
choices: [
{
index: 0,
message: { role: "assistant", content: text },
finish_reason: "stop",
},
],
usage: {
prompt_tokens: promptTokens,
completion_tokens: completionTokens,
total_tokens: promptTokens + completionTokens,
},
}),
{ status: 200, headers: { "content-type": "application/json" } }
);
}
function streamResponse(text: string, model: string, sessionId: string): Response {
const encoder = new TextEncoder();
const id = `chatcmpl-conol-${sessionId}`;
const created = Math.floor(Date.now() / 1000);
const readable = new ReadableStream({
start(controller) {
const chunks = [
{
id,
object: "chat.completion.chunk",
created,
model,
choices: [{ index: 0, delta: { role: "assistant", content: text }, finish_reason: null }],
},
{
id,
object: "chat.completion.chunk",
created,
model,
choices: [{ index: 0, delta: {}, finish_reason: "stop" }],
},
];
for (const chunk of chunks) {
controller.enqueue(encoder.encode(`data: ${JSON.stringify(chunk)}\n\n`));
}
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
controller.close();
},
});
return new Response(readable, {
status: 200,
headers: {
"content-type": "text/event-stream",
"cache-control": "no-cache",
connection: "keep-alive",
},
});
}
export class ConolWebExecutor extends BaseExecutor {
constructor() {
super("conol-web", { id: "conol-web", baseUrl: CONOL_SESSION_URL });
}
async execute(input: ExecuteInput) {
const requestBody = (input.body || {}) as ConolRequestBody;
const messages = Array.isArray(requestBody.messages) ? requestBody.messages : [];
const userTurn = buildConolUserTurn(messages);
const prompt = userTurn.text;
const imageUrls = userTurn.imageUrls;
if (!prompt && imageUrls.length === 0) {
return makeErrorResult(
400,
"No user message found",
{ model: input.model },
CONOL_SESSION_URL
);
}
const { cookie } = resolveConolCredentials(input.credentials);
if (!cookie) {
return makeErrorResult(
401,
"Missing Conol session cookie — sign in with the browser or paste the Cookie header",
{ model: input.model },
CONOL_SESSION_URL
);
}
const { model, effort, effortExplicit } = resolveConolModelSelection(
input.model || requestBody.model
);
const clientSessionKey = resolveConolClientSessionKey(requestBody, input.clientHeaders);
const sessionBindingKey = clientSessionKey
? buildConolSessionBindingKey(input, cookie, clientSessionKey)
: null;
const timeoutSignal = AbortSignal.timeout(CONOL_REQUEST_TIMEOUT_MS);
const upstreamSignal = input.signal
? mergeAbortSignals(input.signal, timeoutSignal)
: timeoutSignal;
try {
return await withConolSessionLock(sessionBindingKey, async () => {
if (upstreamSignal.aborted) {
throw upstreamSignal.reason ?? new DOMException("Aborted", "AbortError");
}
const cachedBinding = sessionBindingKey ? getConolSessionBinding(sessionBindingKey) : null;
let sessionId = cachedBinding?.upstreamSessionId || "";
let reusedSession = false;
let presetApplied = cachedBinding?.presetApplied ?? false;
let appliedModel = cachedBinding?.appliedModel ?? "";
let appliedEffort: ConolEffort | null = cachedBinding?.appliedEffort ?? null;
const imageParts = await uploadConolImages(
cookie,
imageUrls,
upstreamSignal,
sessionId || undefined
);
const parts: ConolMessagePart[] = [...imageParts];
if (prompt) parts.push({ type: "text", content: prompt });
const timezone = safeTimezone(requestBody.timezone);
// Sticky: once a session has carried an image, Conol keeps treating it as
// multimodal, which drives preset text/multimodal model resolution.
const hasImageHistory =
(cachedBinding?.hasImageHistory ?? false) || imageParts.length > 0;
// Conol ignores agentModel/agentEffort on session creation, so create the
// session empty and configure it before any turn is submitted. Otherwise the
// very first turn silently runs on the downgraded account default.
if (!sessionId) {
const createResponse = await fetch(CONOL_SESSION_URL, {
method: "POST",
headers: conolHeaders(cookie, { "content-type": "application/json" }),
body: JSON.stringify({ source: { type: "home" }, messages: [], timezone }),
signal: upstreamSignal,
});
if (createResponse.status === 401 || createResponse.status === 403) {
return makeErrorResult(
createResponse.status,
"Conol session expired or is invalid — sign in again",
{ model },
CONOL_SESSION_URL
);
}
if (!createResponse.ok) {
return makeErrorResult(
createResponse.status,
`Conol session creation failed (HTTP ${createResponse.status})`,
{ model },
CONOL_SESSION_URL
);
}
const created = (await createResponse.json()) as Record<string, unknown>;
sessionId = readString(created.sessionId);
if (!/^[A-Za-z0-9_-]+$/.test(sessionId)) {
return makeErrorResult(
502,
"Conol returned an invalid session identifier",
{ model },
CONOL_SESSION_URL
);
}
presetApplied = false;
appliedModel = "";
appliedEffort = null;
}
const plan = buildConolSessionModelPlan({ model, effort, hasImageHistory });
const desiredEffort = plan.effort?.agentEffort ?? null;
// Re-pin only on a real change: a new session, a model switch, or an
// effort switch. Steady-state follow-ups cost no extra round trips.
const needsModelUpdate =
!presetApplied || appliedModel !== model || appliedEffort !== desiredEffort;
if (needsModelUpdate) {
const configured = await applyConolSessionModel({
sessionId,
plan,
skipPreset: presetApplied,
buildHeaders: (id) => conolHeaders(cookie, undefined, id),
signal: upstreamSignal,
onWarning: (message) => input.log?.warn?.("conol-web", message),
});
presetApplied = presetApplied || configured.presetApplied;
if (configured.modelApplied) {
appliedModel = model;
appliedEffort = configured.effortApplied;
}
}
if (reusedSessionCandidate(cachedBinding, sessionId)) {
const followUpUrl = `${CONOL_SESSION_URL}/${sessionId}/messages`;
const followUpResponse = await fetch(followUpUrl, {
method: "POST",
headers: conolHeaders(cookie, { "content-type": "application/json" }, sessionId),
body: JSON.stringify({ messages: parts, timezone }),
signal: upstreamSignal,
});
if (followUpResponse.status === 401 || followUpResponse.status === 403) {
return makeErrorResult(
followUpResponse.status,
"Conol session expired or is invalid — sign in again",
{ model },
followUpUrl
);
}
if (followUpResponse.status === 404 || followUpResponse.status === 410) {
if (sessionBindingKey) conolSessionBindings.delete(sessionBindingKey);
return makeErrorResult(
followUpResponse.status,
"Conol session no longer exists — retry to start a new session",
{ model, sessionId },
followUpUrl
);
}
if (!followUpResponse.ok) {
return makeErrorResult(
followUpResponse.status,
`Conol follow-up submission failed (HTTP ${followUpResponse.status})`,
{ model, sessionId },
followUpUrl
);
}
reusedSession = true;
await followUpResponse.body?.cancel().catch(() => undefined);
} else {
const firstTurnUrl = `${CONOL_SESSION_URL}/${sessionId}/messages`;
const firstTurnResponse = await fetch(firstTurnUrl, {
method: "POST",
headers: conolHeaders(cookie, { "content-type": "application/json" }, sessionId),
body: JSON.stringify({ messages: parts, timezone }),
signal: upstreamSignal,
});
if (firstTurnResponse.status === 401 || firstTurnResponse.status === 403) {
return makeErrorResult(
firstTurnResponse.status,
"Conol session expired or is invalid — sign in again",
{ model },
firstTurnUrl
);
}
if (!firstTurnResponse.ok) {
return makeErrorResult(
firstTurnResponse.status,
`Conol message submission failed (HTTP ${firstTurnResponse.status})`,
{ model, sessionId },
firstTurnUrl
);
}
await firstTurnResponse.body?.cancel().catch(() => undefined);
}
if (sessionBindingKey) {
setConolSessionBinding(sessionBindingKey, {
upstreamSessionId: sessionId,
presetApplied,
appliedModel,
appliedEffort,
hasImageHistory,
});
}
const messagesUrl = `${CONOL_SESSION_URL}/${sessionId}/messages?logDeltas=1`;
const messageResponse = await fetch(messagesUrl, {
method: "GET",
headers: conolHeaders(
cookie,
{ accept: "text/event-stream, application/x-ndjson" },
sessionId
),
signal: upstreamSignal,
});
if (!messageResponse.ok) {
if (
sessionBindingKey &&
(messageResponse.status === 404 || messageResponse.status === 410)
) {
conolSessionBindings.delete(sessionBindingKey);
}
return makeErrorResult(
messageResponse.status,
`Conol message stream failed (HTTP ${messageResponse.status})`,
{ model, sessionId },
messagesUrl
);
}
const parsed = parseConolMessageStream(await collectConolMessageStream(messageResponse));
if (!parsed.text) {
return makeErrorResult(
502,
"Conol returned no assistant response",
{ model, sessionId },
messagesUrl
);
}
const response = input.stream
? streamResponse(parsed.text, model, sessionId)
: completionResponse(parsed.text, model, sessionId, prompt);
return {
response,
url: messagesUrl,
headers: { cookie: "***" },
transformedBody: {
model,
...(appliedEffort ? { effort: appliedEffort } : {}),
effortRequested: effort,
effortExplicit,
sessionId,
reusedSession,
clientSessionBound: sessionBindingKey !== null,
imageCount: imageParts.length,
},
};
});
} catch (error) {
const isTimeout = error instanceof Error && error.name === "TimeoutError";
const status = error instanceof CursorImageError ? error.status : isTimeout ? 504 : 502;
const message =
error instanceof CursorImageError
? error.message
: isTimeout
? "Conol request timed out"
: error instanceof Error && error.name === "AbortError"
? "Conol request was cancelled"
: "Conol request failed";
return makeErrorResult(status, message, { model }, CONOL_SESSION_URL);
}
}
}

View File

@@ -73,6 +73,7 @@ import { TinyCmsExecutor } from "./tinycms.ts";
import { HyperAgentExecutor } from "./hyperagent.ts";
import { XaiExecutor } from "./xai.ts";
import { PromptQlExecutor } from "./promptql.ts";
import { ConolWebExecutor } from "./conol-web.ts";
const executors = {
antigravity: new AntigravityExecutor(),
@@ -209,6 +210,9 @@ const executors = {
xai: new XaiExecutor(),
"xai-oauth": new XaiExecutor("xai-oauth"),
xao: new XaiExecutor("xai-oauth"),
qw: new QwenWebExecutor(), // Alias
"conol-web": new ConolWebExecutor(),
cnl: new ConolWebExecutor(), // Alias
};
const defaultCache = new Map();
@@ -306,3 +310,4 @@ export { XaiExecutor } from "./xai.ts";
export { MoonshotExecutor } from "./moonshot.ts";
export { CheaperInferenceExecutor } from "./cheaperinference.ts";
export { PromptQlExecutor } from "./promptql.ts";
export { ConolWebExecutor } from "./conol-web.ts";

View File

@@ -0,0 +1,55 @@
export const CONOL_SESSION_COOKIE_NAME = "__Secure-better-auth.session_token";
export interface ConolCredentialInput {
apiKey?: unknown;
accessToken?: unknown;
cookie?: unknown;
providerSpecificData?: unknown;
}
function readString(value: unknown): string {
return typeof value === "string" ? value.trim() : "";
}
function readStoredValue(value: unknown): string {
const raw = readString(value);
if (!raw || !raw.startsWith("{")) return raw;
try {
const parsed = JSON.parse(raw) as Record<string, unknown>;
return (
readString(parsed.cookie) ||
readString(parsed[CONOL_SESSION_COOKIE_NAME]) ||
readString(parsed.sessionToken)
);
} catch {
return raw;
}
}
export function normalizeConolCookie(rawValue: string): string {
const raw = readStoredValue(rawValue).replace(/^Cookie:\s*/i, "").trim();
if (!raw) return "";
if (raw.includes("=")) return raw;
return `${CONOL_SESSION_COOKIE_NAME}=${raw}`;
}
export function resolveConolCredentials(credentials?: ConolCredentialInput): {
cookie: string;
} {
const providerData =
credentials?.providerSpecificData &&
typeof credentials.providerSpecificData === "object" &&
!Array.isArray(credentials.providerSpecificData)
? (credentials.providerSpecificData as Record<string, unknown>)
: {};
const raw =
readStoredValue(providerData.cookie) ||
readStoredValue(providerData[CONOL_SESSION_COOKIE_NAME]) ||
readStoredValue(providerData.sessionToken) ||
readStoredValue(credentials?.cookie) ||
readStoredValue(credentials?.apiKey) ||
readStoredValue(credentials?.accessToken);
return { cookie: normalizeConolCookie(raw) };
}

View File

@@ -0,0 +1,120 @@
import { CONOL_SESSION_COOKIE_NAME } from "./conolAuth.ts";
import { sanitizeErrorMessage } from "../utils/error.ts";
const CONOL_HOME_URL = "https://conol.ai/home";
const DEFAULT_LOGIN_TIMEOUT_MS = 300_000;
const MIN_LOGIN_TIMEOUT_MS = 15_000;
const MAX_LOGIN_TIMEOUT_MS = 600_000;
const POLL_INTERVAL_MS = 1_000;
interface BrowserCookieLike {
name: string;
value: string;
domain?: string;
}
export interface ConolBrowserLoginResult {
success: boolean;
credentials?: { cookie: string };
error?: string;
}
type BrowserLauncher = Pick<typeof import("playwright"), "chromium">;
function clampTimeout(value: unknown): number {
if (typeof value !== "number" || !Number.isFinite(value)) return DEFAULT_LOGIN_TIMEOUT_MS;
return Math.max(MIN_LOGIN_TIMEOUT_MS, Math.min(MAX_LOGIN_TIMEOUT_MS, Math.trunc(value)));
}
export function extractConolBrowserCredentials(
cookies: BrowserCookieLike[]
): { cookie: string } | null {
const session = cookies.find(
(candidate) =>
candidate.name === CONOL_SESSION_COOKIE_NAME &&
(!candidate.domain || candidate.domain === "conol.ai" || candidate.domain.endsWith(".conol.ai"))
);
const value = session?.value?.trim() || "";
if (!value || /[\r\n;]/.test(value)) return null;
return { cookie: `${CONOL_SESSION_COOKIE_NAME}=${value}` };
}
export async function launchConolLoginBrowser(
playwright: BrowserLauncher
): Promise<import("playwright").Browser> {
const configuredPath = process.env.OMNIROUTE_LOGIN_BROWSER_PATH?.trim();
const attempts: Array<Record<string, unknown>> = [
...(configuredPath ? [{ headless: false, executablePath: configuredPath }] : []),
{ headless: false, channel: "chrome" },
{ headless: false, channel: "msedge" },
{ headless: false },
];
let lastError: unknown;
for (const options of attempts) {
try {
return await playwright.chromium.launch(options);
} catch (error) {
lastError = error;
}
}
throw lastError instanceof Error
? lastError
: new Error("No compatible browser is available for sign-in");
}
export async function startConolBrowserLogin(
requestedTimeout?: unknown
): Promise<ConolBrowserLoginResult> {
const timeout = clampTimeout(requestedTimeout);
let playwright: typeof import("playwright");
try {
playwright = await import("playwright");
} catch {
return {
success: false,
error: "Browser sign-in is unavailable. Paste the Conol Cookie header instead.",
};
}
let browser: import("playwright").Browser | null = null;
try {
browser = await launchConolLoginBrowser(playwright);
const context = await browser.newContext({
viewport: { width: 1280, height: 800 },
locale: "en-US",
});
const page = await context.newPage();
await page.goto(CONOL_HOME_URL, {
waitUntil: "domcontentloaded",
timeout: Math.min(timeout, 60_000),
});
const deadline = Date.now() + timeout;
while (Date.now() < deadline) {
const credentials = extractConolBrowserCredentials(
await context.cookies(["https://conol.ai"])
);
if (credentials) return { success: true, credentials };
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
}
return {
success: false,
error: "Conol sign-in timed out. Complete login in the opened browser and try again.",
};
} catch (error) {
return {
success: false,
error: sanitizeErrorMessage(error instanceof Error ? error.message : error),
};
} finally {
if (browser) {
try {
await browser.close();
} catch {
// The user may close the login window before extraction completes.
}
}
}
}

View File

@@ -0,0 +1,310 @@
import { CONOL_SESSION_COOKIE_NAME, normalizeConolCookie } from "./conolAuth.ts";
export type ConolEffort = "minimal" | "low" | "medium" | "high" | "xhigh";
/** Ordered weakest → strongest. Used to clamp a requested effort onto a model. */
export const CONOL_EFFORT_ORDER: readonly ConolEffort[] = [
"minimal",
"low",
"medium",
"high",
"xhigh",
];
export interface ConolModel {
id: string;
name: string;
supportsVision?: boolean;
/** Efforts the upstream advertises for this model. Empty means "not tunable". */
efforts?: ConolEffort[];
}
export interface ConolModelDiscovery {
agentServerId: string;
defaultModel: string;
models: ConolModel[];
modelPresets: ConolModelPreset[];
}
export interface ConolModelPreset {
id: string;
text?: string;
multimodal?: string;
}
/** Effort ladders observed on https://conol.ai/api/agent-servers (2026-07-30). */
const EFFORTS_XHIGH: ConolEffort[] = ["low", "medium", "high", "xhigh"];
const EFFORTS_STANDARD: ConolEffort[] = ["minimal", "low", "medium", "high"];
const EFFORTS_NO_XHIGH: ConolEffort[] = ["low", "medium", "high"];
const EFFORTS_HIGH_ONLY: ConolEffort[] = ["high", "xhigh"];
const EFFORTS_PRO: ConolEffort[] = ["medium", "high", "xhigh"];
interface FallbackModelSeed {
id: string;
vision: boolean;
efforts: ConolEffort[];
}
const FALLBACK_MODEL_SEEDS: FallbackModelSeed[] = [
{ id: "claude-opus-5", vision: true, efforts: EFFORTS_XHIGH },
{ id: "claude-opus-4-8", vision: true, efforts: EFFORTS_XHIGH },
{ id: "claude-fable-5", vision: true, efforts: EFFORTS_XHIGH },
{ id: "claude-opus-4-7", vision: true, efforts: EFFORTS_XHIGH },
{ id: "claude-sonnet-5", vision: true, efforts: EFFORTS_NO_XHIGH },
{ id: "claude-sonnet-4-6", vision: true, efforts: EFFORTS_NO_XHIGH },
{ id: "claude-haiku-4-5", vision: true, efforts: EFFORTS_STANDARD },
{ id: "gpt-5.5", vision: true, efforts: EFFORTS_XHIGH },
{ id: "gpt-5.5-pro", vision: true, efforts: EFFORTS_PRO },
{ id: "gpt-5.6-sol", vision: true, efforts: EFFORTS_XHIGH },
{ id: "gpt-5.6-terra", vision: true, efforts: EFFORTS_XHIGH },
{ id: "gpt-5.6-luna", vision: true, efforts: EFFORTS_XHIGH },
{ id: "deepseek/deepseek-v4-pro", vision: false, efforts: EFFORTS_HIGH_ONLY },
{ id: "openrouter/fusion", vision: false, efforts: [] },
{ id: "z-ai/glm-5.2", vision: false, efforts: EFFORTS_STANDARD },
{ id: "z-ai/glm-5.1", vision: false, efforts: EFFORTS_STANDARD },
{ id: "tencent/hy3", vision: false, efforts: EFFORTS_STANDARD },
{ id: "moonshotai/kimi-k3", vision: true, efforts: EFFORTS_STANDARD },
{ id: "moonshotai/kimi-k2.7-code", vision: true, efforts: EFFORTS_STANDARD },
{ id: "qwen/qwen3.7-plus", vision: true, efforts: EFFORTS_STANDARD },
{ id: "qwen/qwen3.7-max", vision: false, efforts: EFFORTS_STANDARD },
{ id: "minimax/minimax-m3", vision: true, efforts: EFFORTS_STANDARD },
{ id: "stepfun/step-3.7-flash", vision: true, efforts: EFFORTS_STANDARD },
{ id: "google/gemini-3.5-flash", vision: true, efforts: EFFORTS_STANDARD },
{ id: "google/gemini-3.1-pro-preview", vision: true, efforts: EFFORTS_STANDARD },
{ id: "google/gemini-3.1-flash-lite", vision: true, efforts: EFFORTS_STANDARD },
{ id: "x-ai/grok-4.3", vision: true, efforts: EFFORTS_STANDARD },
{ id: "deepseek/deepseek-v4-flash", vision: false, efforts: EFFORTS_HIGH_ONLY },
{ id: "xiaomi/mimo-v2.5", vision: true, efforts: EFFORTS_STANDARD },
{ id: "xiaomi/mimo-v2.5-pro", vision: false, efforts: EFFORTS_STANDARD },
];
/** Presets exposed by the web client's model picker (id → text/multimodal model). */
export const CONOL_FALLBACK_MODEL_PRESETS: ConolModelPreset[] = [
{ id: "flash", text: "deepseek/deepseek-v4-flash", multimodal: "google/gemini-3.5-flash" },
{ id: "moderate", text: "deepseek/deepseek-v4-pro", multimodal: "claude-sonnet-5" },
{ id: "pro", text: "z-ai/glm-5.2", multimodal: "moonshotai/kimi-k3" },
{ id: "ultra", text: "claude-fable-5", multimodal: "claude-fable-5" },
];
function modelName(id: string): string {
return id
.split("/")
.pop()!
.split("-")
.map((part) => {
const lower = part.toLowerCase();
if (["gpt", "ai", "glm"].includes(lower)) return lower.toUpperCase();
return part.length ? part[0]!.toUpperCase() + part.slice(1) : part;
})
.join(" ");
}
export const CONOL_FALLBACK_MODELS: ConolModel[] = FALLBACK_MODEL_SEEDS.map((seed) => ({
id: seed.id,
name: modelName(seed.id),
supportsVision: seed.vision,
efforts: [...seed.efforts],
}));
const CONOL_FALLBACK_EFFORTS = new Map<string, ConolEffort[]>(
FALLBACK_MODEL_SEEDS.map((seed) => [seed.id, seed.efforts])
);
function readString(value: unknown): string {
return typeof value === "string" ? value.trim() : "";
}
function toEfforts(value: unknown): ConolEffort[] | null {
if (!Array.isArray(value)) return null;
const efforts = value
.map((entry) => readString(entry).toLowerCase())
.filter((entry): entry is ConolEffort =>
(CONOL_EFFORT_ORDER as readonly string[]).includes(entry)
);
// Normalize to the canonical weakest→strongest order and de-duplicate.
return CONOL_EFFORT_ORDER.filter((effort) => efforts.includes(effort));
}
function toModel(value: unknown): ConolModel | null {
if (typeof value === "string") {
const id = value.trim();
return id ? { id, name: modelName(id) } : null;
}
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
const item = value as Record<string, unknown>;
const id =
readString(item.id) ||
readString(item.modelId) ||
readString(item.value) ||
readString(item.name);
if (!id) return null;
const inputModalities = Array.isArray(item.inputModalities)
? item.inputModalities.filter((modality): modality is string => typeof modality === "string")
: null;
const efforts = toEfforts(item.efforts);
return {
id,
name: readString(item.displayName) || readString(item.name) || modelName(id),
...(inputModalities
? { supportsVision: inputModalities.some((modality) => modality.toLowerCase() === "image") }
: {}),
...(efforts ? { efforts } : {}),
};
}
function toModelPreset(value: unknown): ConolModelPreset | null {
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
const item = value as Record<string, unknown>;
const id = readString(item.id);
if (!id) return null;
const text = readString(item.text);
const multimodal = readString(item.multimodal);
return { id, ...(text ? { text } : {}), ...(multimodal ? { multimodal } : {}) };
}
/**
* Clamp a requested effort onto the ladder a model actually advertises.
* Returns `null` when the model exposes no effort control at all.
*/
export function clampConolEffort(
requested: ConolEffort,
supported: readonly ConolEffort[] | undefined
): ConolEffort | null {
const ladder =
supported && supported.length
? CONOL_EFFORT_ORDER.filter((effort) => supported.includes(effort))
: [];
if (!ladder.length) return null;
if (ladder.includes(requested)) return requested;
const requestedRank = CONOL_EFFORT_ORDER.indexOf(requested);
// Prefer the strongest supported effort at or below the request; otherwise the weakest above.
let below: ConolEffort | null = null;
for (const effort of ladder) {
if (CONOL_EFFORT_ORDER.indexOf(effort) <= requestedRank) below = effort;
}
return below ?? ladder[0]!;
}
/** Effort ladder for a model id, using discovery data when available. */
export function conolEffortsForModel(
modelId: string,
discovered?: readonly ConolModel[]
): ConolEffort[] {
const fromDiscovery = discovered?.find((model) => model.id === modelId)?.efforts;
if (fromDiscovery) return [...fromDiscovery];
return [...(CONOL_FALLBACK_EFFORTS.get(modelId) ?? [])];
}
export function parseConolAgentServers(payload: unknown): ConolModelDiscovery {
const root = Array.isArray(payload)
? payload
: payload && typeof payload === "object"
? ((payload as Record<string, unknown>).agentServers ??
(payload as Record<string, unknown>).servers ??
[])
: [];
const servers = Array.isArray(root) ? root : [];
const server = servers.find(
(value) => value && typeof value === "object" && !Array.isArray(value)
) as Record<string, unknown> | undefined;
const capabilities =
server?.capabilities &&
typeof server.capabilities === "object" &&
!Array.isArray(server.capabilities)
? (server.capabilities as Record<string, unknown>)
: null;
const agents = Array.isArray(capabilities?.agents) ? capabilities.agents : [];
const defaultAgent = readString(capabilities?.defaultAgent);
const agent = (agents.find((value) => {
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
return readString((value as Record<string, unknown>).name) === defaultAgent;
}) ?? agents[0]) as Record<string, unknown> | undefined;
const seen = new Set<string>();
const rawModels = Array.isArray(agent?.models)
? agent.models
: Array.isArray(server?.models)
? server.models
: [];
const models = rawModels.map(toModel).filter((model): model is ConolModel => {
if (!model || seen.has(model.id)) return false;
seen.add(model.id);
return true;
});
const rawPresets = Array.isArray(agent?.modelPresets) ? agent.modelPresets : [];
const seenPresets = new Set<string>();
const modelPresets = rawPresets
.map(toModelPreset)
.filter((preset): preset is ConolModelPreset => {
if (!preset || seenPresets.has(preset.id)) return false;
seenPresets.add(preset.id);
return true;
});
return {
agentServerId: readString(server?.id),
defaultModel: readString(agent?.defaultModel) || readString(server?.defaultModel),
models,
modelPresets,
};
}
/**
* Effort applied when the caller does not pin one via the `-<effort>` model suffix.
* Clamped per-model, so models without an `xhigh` rung fall back to their strongest rung.
*/
export const CONOL_DEFAULT_EFFORT: ConolEffort = "xhigh";
export function resolveConolModelSelection(value: unknown): {
model: string;
effort: ConolEffort;
/** True when the effort came from an explicit `-<effort>` suffix rather than the default. */
effortExplicit: boolean;
} {
let model = readString(value);
if (model.startsWith("conol-web/")) model = model.slice("conol-web/".length);
else if (model.startsWith("conol/")) model = model.slice("conol/".length);
else if (model.startsWith("cnl/")) model = model.slice("cnl/".length);
model ||= "claude-sonnet-5";
const effortMatch = model.match(/-(xhigh|high|medium|low|minimal)$/);
if (!effortMatch) return { model, effort: CONOL_DEFAULT_EFFORT, effortExplicit: false };
return {
model: model.slice(0, -effortMatch[0].length),
effort: effortMatch[1] as ConolEffort,
effortExplicit: true,
};
}
export function resolveConolModelId(value: unknown): string {
return resolveConolModelSelection(value).model;
}
export async function discoverConolModels(options: {
cookie: string;
fetchImpl?: typeof fetch;
signal?: AbortSignal;
}): Promise<ConolModelDiscovery> {
const cookie = normalizeConolCookie(options.cookie);
if (!cookie) throw new Error(`Missing ${CONOL_SESSION_COOKIE_NAME} cookie`);
const response = await (options.fetchImpl ?? fetch)("https://conol.ai/api/agent-servers", {
method: "GET",
headers: {
accept: "application/json",
cookie,
referer: "https://conol.ai/home",
},
signal: options.signal,
});
if (!response.ok) {
throw new Error(`Conol model discovery returned HTTP ${response.status}`);
}
const discovered = parseConolAgentServers(await response.json());
if (!discovered.models.length) {
throw new Error("Conol model discovery returned an empty catalog");
}
return discovered;
}

View File

@@ -0,0 +1,148 @@
/**
* Conol session model/effort configuration.
*
* `POST /api/sessions` ignores `agentModel`/`agentEffort` in its body — a freshly
* created session always starts on the account default and Conol reports the
* downgrade via `modelDowngraded` / `effectiveModel`. The web client therefore
* configures the session out-of-band against `POST /api/sessions/{id}/model`,
* which accepts three distinct payload shapes (verified 2026-07-30):
*
* 1. `{"modelPreset":"pro","hasImageHistory":false}` — picker preset
* 2. `{"agentModel":"claude-fable-5","agentEffort":null}` — pin an explicit model
* 3. `{"agentEffort":"xhigh"}` — pin the effort
*
* Shape 2 resets `agentEffort` to `null`, so the effort call must always follow
* the model call. All three return `{"ok":true}`.
*/
import {
clampConolEffort,
conolEffortsForModel,
type ConolEffort,
type ConolModel,
} from "./conolModels.ts";
export const CONOL_ORIGIN = "https://conol.ai";
/** Preset the web client sends on every new session before pinning a model. */
export const CONOL_DEFAULT_MODEL_PRESET = "pro";
export type ConolModelPresetId = "flash" | "moderate" | "pro" | "ultra";
const KNOWN_PRESETS = new Set<ConolModelPresetId>(["flash", "moderate", "pro", "ultra"]);
export function isConolModelPreset(value: string): value is ConolModelPresetId {
return KNOWN_PRESETS.has(value as ConolModelPresetId);
}
export interface ConolSessionModelPlan {
/** Preset priming call, sent once per session. */
preset: { modelPreset: string; hasImageHistory: boolean };
/** Explicit model pin. Always clears effort so the effort call can apply cleanly. */
model: { agentModel: string; agentEffort: null };
/** Effort pin, omitted when the model exposes no effort ladder. */
effort: { agentEffort: ConolEffort } | null;
}
export interface BuildConolSessionModelPlanOptions {
model: string;
effort: ConolEffort;
hasImageHistory: boolean;
/** Discovery catalog, when available, so effort ladders stay accurate. */
catalog?: readonly ConolModel[];
/** Overrides the default `pro` priming preset. */
modelPreset?: string;
}
/**
* Build the ordered preset → model → effort payloads for a session.
* Effort is clamped onto the ladder the target model actually advertises, so a
* default of `xhigh` degrades to `high` on models such as `claude-sonnet-5`.
*/
export function buildConolSessionModelPlan(
options: BuildConolSessionModelPlanOptions
): ConolSessionModelPlan {
const supported = conolEffortsForModel(options.model, options.catalog);
const effort = clampConolEffort(options.effort, supported);
return {
preset: {
modelPreset: options.modelPreset || CONOL_DEFAULT_MODEL_PRESET,
hasImageHistory: options.hasImageHistory,
},
model: { agentModel: options.model, agentEffort: null },
effort: effort ? { agentEffort: effort } : null,
};
}
export function conolSessionModelUrl(sessionId: string): string {
return `${CONOL_ORIGIN}/api/sessions/${encodeURIComponent(sessionId)}/model`;
}
export interface ApplyConolSessionModelOptions {
sessionId: string;
plan: ConolSessionModelPlan;
/** Skip the preset priming call when the session was already primed. */
skipPreset?: boolean;
buildHeaders: (sessionId: string) => Record<string, string>;
fetchImpl?: typeof fetch;
signal?: AbortSignal | null;
onWarning?: (message: string) => void;
}
export interface AppliedConolSessionModel {
presetApplied: boolean;
modelApplied: boolean;
effortApplied: ConolEffort | null;
}
async function postSessionModel(
url: string,
body: unknown,
options: ApplyConolSessionModelOptions
): Promise<boolean> {
const response = await (options.fetchImpl ?? fetch)(url, {
method: "POST",
headers: { ...options.buildHeaders(options.sessionId), "content-type": "application/json" },
body: JSON.stringify(body),
signal: options.signal ?? undefined,
});
// Drain so the socket can be reused; the payload is only `{"ok":true}`.
await response.body?.cancel().catch(() => undefined);
if (!response.ok) {
options.onWarning?.(
`Conol session model update failed (HTTP ${response.status}) for ${JSON.stringify(body)}`
);
return false;
}
return true;
}
/**
* Apply preset → model → effort in order. Ordering is load-bearing: the model
* call nulls the effort, so applying effort first would silently drop it.
* Failures are reported but non-fatal — the turn still runs on Conol's default.
*/
export async function applyConolSessionModel(
options: ApplyConolSessionModelOptions
): Promise<AppliedConolSessionModel> {
const url = conolSessionModelUrl(options.sessionId);
const applied: AppliedConolSessionModel = {
presetApplied: false,
modelApplied: false,
effortApplied: null,
};
if (!options.skipPreset) {
applied.presetApplied = await postSessionModel(url, options.plan.preset, options);
}
applied.modelApplied = await postSessionModel(url, options.plan.model, options);
// Only pin effort if the model pin landed; otherwise the session is on an
// unknown model whose effort ladder we cannot reason about.
if (applied.modelApplied && options.plan.effort) {
const ok = await postSessionModel(url, options.plan.effort, options);
if (ok) applied.effortApplied = options.plan.effort.agentEffort;
}
return applied;
}

View File

@@ -0,0 +1,111 @@
import { normalizeConolCookie } from "./conolAuth.ts";
interface UsageQuota {
used: number;
total: number;
remaining: number;
remainingPercentage: number;
resetAt: null;
unlimited: boolean;
}
interface ConolBalance {
dailyCredits?: unknown;
subscriptionCredits?: unknown;
subscriptionAmount?: unknown;
extraCredits?: unknown;
total?: unknown;
}
interface ConolUsageResult {
plan: string;
quotas: Record<"credits" | "daily" | "subscription" | "extra", UsageQuota>;
message: string | null;
}
function numberValue(value: unknown): number {
const parsed = typeof value === "number" ? value : Number(value);
return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
}
function remainingQuota(remaining: number, total = remaining): UsageQuota {
const boundedTotal = Math.max(total, remaining);
const used = Math.max(0, boundedTotal - remaining);
return {
used,
total: boundedTotal,
remaining,
remainingPercentage:
boundedTotal > 0 ? Math.round((remaining / boundedTotal) * 1000) / 10 : 0,
resetAt: null,
unlimited: false,
};
}
export function buildConolUsageResult(balance: ConolBalance): ConolUsageResult {
const daily = numberValue(balance.dailyCredits);
const subscription = numberValue(balance.subscriptionCredits);
const subscriptionAmount = numberValue(balance.subscriptionAmount);
const extra = numberValue(balance.extraCredits);
const aggregate = numberValue(balance.total) || daily + subscription + extra;
return {
plan: subscriptionAmount > 0 ? "Subscription" : "Free",
quotas: {
credits: remainingQuota(aggregate),
daily: remainingQuota(daily),
subscription: remainingQuota(subscription, subscriptionAmount || subscription),
extra: remainingQuota(extra),
},
message: null,
};
}
function readString(value: unknown): string {
return typeof value === "string" ? value.trim() : "";
}
function readProviderValue(data: unknown, keys: readonly string[]): string {
if (!data || typeof data !== "object" || Array.isArray(data)) return "";
const record = data as Record<string, unknown>;
for (const key of keys) {
const value = readString(record[key]);
if (value) return value;
}
return "";
}
export async function getConolUsage(
apiKey: unknown,
providerSpecificData?: unknown
): Promise<ConolUsageResult | { message: string }> {
const raw =
readProviderValue(providerSpecificData, [
"cookie",
"__Secure-better-auth.session_token",
"sessionToken",
]) || readString(apiKey);
const cookie = normalizeConolCookie(raw);
if (!cookie) return { message: "Missing Conol session cookie" };
try {
const response = await fetch("https://conol.ai/api/billing/balance", {
method: "GET",
headers: {
accept: "application/json",
cookie,
referer: "https://conol.ai/home",
},
signal: AbortSignal.timeout(15_000),
});
if (response.status === 401 || response.status === 403) {
return { message: "Conol session expired or is invalid" };
}
if (!response.ok) {
return { message: `Conol balance request failed (HTTP ${response.status})` };
}
return buildConolUsageResult((await response.json()) as ConolBalance);
} catch {
return { message: "Conol balance request failed" };
}
}

View File

@@ -69,6 +69,7 @@ import { getXaiOauthUsage } from "./usage/xaiOauth.ts";
import { getGrokCliUsage } from "./usage/grokCli.ts";
import { getFirecrawlUsage } from "./usage/firecrawl.ts";
import { getCommandCodeUsage } from "./usage/command-code.ts";
import { getConolUsage } from "./conolUsage.ts";
type JsonRecord = Record<string, unknown>;
type UsageProviderConnection = JsonRecord & {
@@ -133,6 +134,8 @@ export const USAGE_FETCHER_PROVIDERS = [
"firecrawl",
// Command Code credits + 5h/weekly windows (GET /alpha/billing/credits)
"command-code",
"conol-web",
"cnl",
] as const;
export type UsageFetcherProvider = (typeof USAGE_FETCHER_PROVIDERS)[number];
@@ -234,6 +237,9 @@ export async function getUsageForProvider(
return await getFirecrawlUsage(id || "", apiKey, connection);
case "command-code":
return await getCommandCodeUsage(apiKey || accessToken || "");
case "conol-web":
case "cnl":
return await getConolUsage(apiKey || accessToken, providerSpecificData);
default:
return { message: `Usage API not implemented for ${provider}` };
}

View File

@@ -189,6 +189,46 @@ export async function POST(
}
}
// Conol: unofficial browser-session chat with cookie auth
// (__Secure-better-auth.session_token). Dedicated browser login + credential
// persistence (same shape as the other web-cookie providers).
if (providerSlug === "conol-web" || providerSlug === "cnl") {
try {
const { startConolBrowserLogin } = await import(
"@omniroute/open-sse/services/conolBrowserLogin.ts"
);
const result = await startConolBrowserLogin(
typeof body.timeout === "number" ? body.timeout : undefined
);
if (!result.success || !result.credentials) {
return NextResponse.json(result, { status: 400 });
}
try {
await updateProviderConnection(id, {
apiKey: JSON.stringify(result.credentials),
providerSpecificData: result.credentials,
});
} catch (err) {
const msg = sanitizeErrorMessage(err instanceof Error ? err.message : err);
return NextResponse.json(
{ success: false, error: `Extracted but failed to persist: ${msg}` },
{ status: 500 }
);
}
return NextResponse.json({
success: true,
credentials: result.credentials,
persisted: true,
});
} catch (err) {
const msg = sanitizeErrorMessage(err instanceof Error ? err.message : err);
return NextResponse.json(
{ success: false, error: `Login endpoint error: ${msg}` },
{ status: 500 }
);
}
}
try {
// Generic web-cookie path: pass the provider SLUG (not the DB id) so
// TOKEN_EXTRACTION_CONFIGS can find the extraction config.

View File

@@ -0,0 +1,97 @@
import { SAFE_OUTBOUND_FETCH_PRESETS, safeOutboundFetch } from "@/shared/network/safeOutboundFetch";
import { getProviderOutboundGuard } from "@/shared/network/outboundUrlGuard";
import { resolveConolCredentials } from "@omniroute/open-sse/services/conolAuth.ts";
import {
CONOL_FALLBACK_MODELS,
discoverConolModels,
type ConolModel,
} from "@omniroute/open-sse/services/conolModels.ts";
import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error";
interface DiscoveryWarnings {
cacheWarning?: string;
localWarning?: string;
}
interface ConolDiscoveryRouteOptions {
provider: string;
connectionId: string;
apiKey: unknown;
accessToken: unknown;
providerSpecificData: unknown;
proxy: unknown;
maybeReturnCachedDiscovery: () => Response | null;
maybeReturnAutoFetchDisabled: () => Response | null;
buildDiscoveryFallbackResponse: (warnings: DiscoveryWarnings) => Response | null;
buildResponse: (payload: Record<string, unknown>) => Response;
buildApiDiscoveryResponse: (models: ConolModel[]) => Promise<Response>;
}
export async function maybeHandleConolModelDiscovery(
options: ConolDiscoveryRouteOptions
): Promise<Response | null> {
if (options.provider !== "conol-web" && options.provider !== "cnl") return null;
const cachedResponse = options.maybeReturnCachedDiscovery();
if (cachedResponse) return cachedResponse;
const autoFetchDisabledResponse = options.maybeReturnAutoFetchDisabled();
if (autoFetchDisabledResponse) return autoFetchDisabledResponse;
const { cookie } = resolveConolCredentials({
apiKey: options.apiKey,
accessToken: options.accessToken,
providerSpecificData: options.providerSpecificData,
});
const seedModels = CONOL_FALLBACK_MODELS.map((model) => ({
id: model.id,
name: model.name,
supportsVision: model.supportsVision,
}));
if (!cookie) {
const fallback = options.buildDiscoveryFallbackResponse({
cacheWarning: "No Conol cookie configured — using cached catalog",
localWarning: "No Conol cookie configured — using local catalog",
});
if (fallback) return fallback;
return options.buildResponse({
provider: options.provider,
connectionId: options.connectionId,
models: seedModels,
source: "local_catalog",
intentional: true,
warning: "No Conol session cookie — using seed model list",
});
}
try {
const discovery = await discoverConolModels({
cookie,
fetchImpl: (url, init) =>
safeOutboundFetch(url, {
...SAFE_OUTBOUND_FETCH_PRESETS.modelsDiscovery,
guard: getProviderOutboundGuard(),
proxyConfig: options.proxy,
...init,
}),
});
return options.buildApiDiscoveryResponse(discovery.models);
} catch (error) {
console.log("Error fetching models from conol-web", {
error: sanitizeErrorMessage(error instanceof Error ? error.message : error),
});
const fallback = options.buildDiscoveryFallbackResponse({
cacheWarning: "Conol model discovery failed — using cached catalog",
localWarning: "Conol model discovery failed — using seed catalog",
});
if (fallback) return fallback;
return options.buildResponse({
provider: options.provider,
connectionId: options.connectionId,
models: seedModels,
source: "local_catalog",
intentional: true,
warning: "API unavailable — using seed Conol model list",
});
}
}

View File

@@ -127,6 +127,7 @@ import {
fetchCodexDiscoveryModels,
fetchCodexGithubCatalogModels,
} from "./discovery/codex";
import { maybeHandleConolModelDiscovery } from "./conolDiscovery";
function toLiveModel(item: Record<string, unknown>): { id: string; name: string } | null {
const itemId = typeof item.id === "string" ? item.id.trim() : "";
@@ -667,6 +668,20 @@ export async function GET(
});
}
}
const conolResponse = await maybeHandleConolModelDiscovery({
provider,
connectionId,
apiKey,
accessToken,
providerSpecificData: connection.providerSpecificData,
proxy,
maybeReturnCachedDiscovery,
maybeReturnAutoFetchDisabled,
buildDiscoveryFallbackResponse,
buildResponse,
buildApiDiscoveryResponse,
});
if (conolResponse) return conolResponse;
if (provider === "bedrock") {
const cachedResponse = maybeReturnCachedDiscovery();

View File

@@ -165,7 +165,9 @@ function getRegistryModel(providerIdOrAlias: string | null, modelId: string | nu
const providerAlias = PROVIDER_ID_TO_ALIAS[providerIdOrAlias] || providerIdOrAlias;
const models = PROVIDER_MODELS[providerAlias];
if (!Array.isArray(models)) return null;
return models.find((model) => model?.id === modelId) || null;
const normalizedModelId =
providerAlias === "cnl" ? modelId.replace(/-(?:xhigh|high|medium|low)$/i, "") : modelId;
return models.find((model) => model?.id === normalizedModelId) || null;
}
function resolveCapabilityInput(input: CapabilityInput) {

View File

@@ -95,6 +95,8 @@ const PROVIDER_LIMITS_APIKEY_PROVIDERS = new Set([
"firecrawl",
// Command Code API key → /alpha/billing/credits + windowLimits
"command-code",
"conol-web",
"cnl",
]);
const DEFAULT_PROVIDER_LIMITS_SYNC_INTERVAL_MINUTES = 70;
const PROVIDER_LIMITS_AUTO_SYNC_SETTING_KEY = "provider_limits_auto_sync_last_run";

View File

@@ -54,6 +54,7 @@ export function supportsDualAuthProvider(providerId: unknown): boolean {
// Web / Cookie Providers
// API Key Providers
// Sub-categories within APIKEY_PROVIDERS (used by dashboard and catalog views).
@@ -470,6 +471,8 @@ export const USAGE_SUPPORTED_PROVIDERS = [
"firecrawl",
// Command Code credits + 5h/weekly rolling windows
"command-code",
"conol-web",
"cnl",
];
// ── Zod validation at module load (Phase 7.2) ──

View File

@@ -466,6 +466,19 @@ export const WEB_COOKIE_PROVIDERS = {
authHint:
"Paste the full Cookie header from hyperagent.com (DevTools → Network → any request → Request Headers → Cookie). Session cookies power chat + billing usage.",
},
"conol-web": {
id: "conol-web",
alias: "cnl",
name: "Conol (Unofficial/Experimental)",
icon: "auto_awesome",
color: "#F6C945",
textIcon: "CO",
website: "https://conol.ai",
subscriptionRisk: true,
riskNoticeVariant: "webCookie",
authHint:
"Use browser sign-in, or paste the full Cookie header from conol.ai. The __Secure-better-auth.session_token cookie is required.",
},
};
/** Resolved public site for a web-session provider (href + display host). */

View File

@@ -325,6 +325,14 @@ export const WEB_SESSION_CREDENTIAL_REQUIREMENTS = {
acceptsFullCookieHeader: true,
storageKeys: ["cookie", "token", "access_token", "accessToken"],
},
"conol-web": {
kind: "cookie",
credentialName: "__Secure-better-auth.session_token",
placeholder:
"__Secure-better-auth.session_token=... or full Cookie header from conol.ai",
acceptsFullCookieHeader: true,
storageKeys: ["cookie", "__Secure-better-auth.session_token"],
},
} satisfies Record<string, WebSessionCredentialRequirement> &
Record<keyof typeof WEB_COOKIE_PROVIDERS, WebSessionCredentialRequirement>;

View File

@@ -0,0 +1,963 @@
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import {
buildConolUserTurn,
buildConolPromptText,
clearConolSessionBindingsForTests,
collectConolMessageStream,
ConolWebExecutor,
normalizeConolCookie,
parseConolMessageStream,
resolveConolClientSessionKey,
resolveConolCredentials,
} from "../../open-sse/executors/conol-web.ts";
import {
CONOL_FALLBACK_MODELS,
clampConolEffort,
parseConolAgentServers,
resolveConolModelSelection,
} from "../../open-sse/services/conolModels.ts";
import { buildConolSessionModelPlan } from "../../open-sse/services/conolSessionModel.ts";
import { buildConolUsageResult } from "../../open-sse/services/conolUsage.ts";
import { extractConolBrowserCredentials } from "../../open-sse/services/conolBrowserLogin.ts";
import { claudeToOpenAIRequest } from "../../open-sse/translator/request/claude-to-openai.ts";
import { getResolvedModelCapabilities } from "../../src/lib/modelCapabilities.ts";
const SESSION_COOKIE_NAME = "__Secure-better-auth.session_token";
describe("Conol web provider", () => {
it("normalizes raw, full-header, JSON, and provider-data credentials", () => {
assert.equal(normalizeConolCookie("token-value"), `${SESSION_COOKIE_NAME}=token-value`);
assert.equal(
normalizeConolCookie(`Cookie: preference=compact; ${SESSION_COOKIE_NAME}=token-value`),
`preference=compact; ${SESSION_COOKIE_NAME}=token-value`
);
assert.deepEqual(
resolveConolCredentials({
apiKey: JSON.stringify({ cookie: `${SESSION_COOKIE_NAME}=json-token` }),
}),
{ cookie: `${SESSION_COOKIE_NAME}=json-token` }
);
assert.deepEqual(
resolveConolCredentials({
providerSpecificData: { [SESSION_COOKIE_NAME]: "provider-token" },
}),
{ cookie: `${SESSION_COOKIE_NAME}=provider-token` }
);
});
it("sends only the latest user turn and strips generated image metadata", () => {
const messages = [
{ role: "system", content: "Be concise." },
{ role: "user", content: "Earlier user turn" },
{ role: "assistant", content: "Ready." },
{ role: "tool", content: "secret tool output" },
{
role: "user",
content: [
{
type: "text",
text:
"[Image 1]: (unavailable)\n" +
"[Image: source: C:\\Users\\someone\\.claude\\image-cache\\id\\2.png]\n" +
"Inspect this",
},
{ type: "image_url", image_url: { url: "data:image/png;base64,YQ==" } },
],
},
];
const turn = buildConolUserTurn(messages);
const prompt = buildConolPromptText(messages);
assert.equal(prompt, "Inspect this");
assert.equal(turn.text, "Inspect this");
assert.deepEqual(turn.imageUrls, ["data:image/png;base64,YQ=="]);
assert.doesNotMatch(prompt, /Be concise|Earlier user turn|Ready|secret tool output/);
assert.doesNotMatch(prompt, /image-cache|unavailable/);
assert.doesNotMatch(prompt, /base64/);
});
it("derives stable client session keys without exposing the raw identifier", () => {
const fromHeader = resolveConolClientSessionKey(
{},
{ "x-claude-code-session-id": "client-session-123" }
);
const repeated = resolveConolClientSessionKey(
{},
{ "X-Claude-Code-Session-Id": "client-session-123" }
);
const movedToBody = resolveConolClientSessionKey({
conversation_id: "client-session-123",
});
const fromMetadata = resolveConolClientSessionKey({
metadata: { user_id: JSON.stringify({ session_id: "metadata-session-456" }) },
});
assert.equal(fromHeader, repeated);
assert.equal(fromHeader, movedToBody);
assert.match(fromHeader || "", /^[a-f0-9]{64}$/);
assert.doesNotMatch(fromHeader || "", /client-session-123/);
assert.match(fromMetadata || "", /^[a-f0-9]{64}$/);
assert.equal(resolveConolClientSessionKey({}), null);
});
it("keeps Claude system/tool data out while preserving its translated image", () => {
const translated = claudeToOpenAIRequest(
"conol-web/claude-fable-5",
{
system: [{ type: "text", text: "Large Claude Code system instructions." }],
messages: [
{
role: "assistant",
content: [
{
type: "tool_use",
id: "tool-1",
name: "Read",
input: { path: "private.txt" },
},
],
},
{
role: "user",
content: [
{
type: "tool_result",
tool_use_id: "tool-1",
content: "Private tool result",
},
{
type: "text",
text: "Describe this image",
},
{
type: "image",
source: {
type: "base64",
media_type: "image/png",
data: "aW1hZ2U=",
},
},
],
},
],
},
false
);
const turn = buildConolUserTurn(
translated.messages as Array<{
role: string;
content: unknown;
}>
);
assert.equal(turn.text, "Describe this image");
assert.deepEqual(turn.imageUrls, ["data:image/png;base64,aW1hZ2U="]);
assert.doesNotMatch(turn.text, /system instructions|Private tool result|private\.txt/);
});
it("uses the latest cumulative history snapshot", () => {
const raw = [
JSON.stringify({
type: "history_delta",
stages: [
{
preview: [{ role: "assistant", content: [{ type: "text", text: "Hel" }] }],
},
],
}),
JSON.stringify({
type: "history_delta",
stages: [
{
logs: [{ role: "assistant", content: [{ type: "text", text: "Hello world!" }] }],
},
],
contextUsage: {
usedTokens: 42,
contextWindow: 200000,
modelId: "claude-fable-5",
},
}),
JSON.stringify({ type: "done" }),
].join("\n");
assert.deepEqual(parseConolMessageStream(raw), {
text: "Hello world!",
usedTokens: 42,
contextWindow: 200000,
modelId: "claude-fable-5",
done: true,
});
});
it("stops reading when done arrives even if the upstream never closes", async () => {
const encoder = new TextEncoder();
let cancelled = false;
const body = new ReadableStream<Uint8Array>({
start(controller) {
controller.enqueue(
encoder.encode(
[
`message\t${JSON.stringify({
type: "history_delta",
stages: [
{
logs: [{ role: "assistant", content: [{ type: "text", text: "Finished" }] }],
},
],
})}`,
`message\t${JSON.stringify({ type: "done" })}`,
"",
].join("\n")
)
);
},
cancel() {
cancelled = true;
},
});
const raw = await Promise.race([
collectConolMessageStream(new Response(body)),
new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error("collector did not stop at done")), 1_000)
),
]);
assert.equal(parseConolMessageStream(raw).text, "Finished");
assert.equal(cancelled, true);
});
it("parses the live nested agent-server model schema and strips server secrets", () => {
const discovery = parseConolAgentServers([
{
id: "server-1",
apiKey: "must-not-leak",
capabilities: {
defaultAgent: "conol",
agents: [
{
name: "conol",
defaultModel: "claude-fable-5",
models: [
{
name: "claude-fable-5",
displayName: "Claude Fable 5",
efforts: ["low", "xhigh"],
inputModalities: ["text", "image"],
},
{
name: "deepseek/deepseek-v4-pro",
displayName: "DeepSeek V4 Pro",
inputModalities: ["text"],
},
],
},
],
},
},
]);
assert.deepEqual(discovery, {
agentServerId: "server-1",
defaultModel: "claude-fable-5",
models: [
{
id: "claude-fable-5",
name: "Claude Fable 5",
supportsVision: true,
efforts: ["low", "xhigh"],
},
{
id: "deepseek/deepseek-v4-pro",
name: "DeepSeek V4 Pro",
supportsVision: false,
},
],
modelPresets: [],
});
assert.equal(JSON.stringify(discovery).includes("must-not-leak"), false);
assert.ok(CONOL_FALLBACK_MODELS.length > 0);
});
it("parses model presets from the agent-server payload", () => {
const discovery = parseConolAgentServers([
{
id: "server-1",
capabilities: {
defaultAgent: "default",
agents: [
{
name: "default",
models: [{ name: "z-ai/glm-5.2" }],
modelPresets: [
{ id: "pro", text: "z-ai/glm-5.2", multimodal: "moonshotai/kimi-k3" },
{ id: "ultra", text: "claude-fable-5", multimodal: "claude-fable-5" },
{ text: "ignored-without-id" },
],
},
],
},
},
]);
assert.deepEqual(discovery.modelPresets, [
{ id: "pro", text: "z-ai/glm-5.2", multimodal: "moonshotai/kimi-k3" },
{ id: "ultra", text: "claude-fable-5", multimodal: "claude-fable-5" },
]);
});
it("clamps effort onto the ladder each model actually advertises", () => {
// claude-sonnet-5 has no xhigh rung, so the xhigh default degrades to high.
assert.equal(clampConolEffort("xhigh", ["low", "medium", "high"]), "high");
// Exact matches pass through untouched.
assert.equal(clampConolEffort("xhigh", ["low", "medium", "high", "xhigh"]), "xhigh");
// deepseek only exposes high/xhigh, so a weak request climbs to the weakest rung.
assert.equal(clampConolEffort("minimal", ["high", "xhigh"]), "high");
// Models without an effort ladder (openrouter/fusion) must not receive one.
assert.equal(clampConolEffort("xhigh", []), null);
assert.equal(clampConolEffort("xhigh", undefined), null);
});
it("orders the session model plan preset -> model -> effort", () => {
const plan = buildConolSessionModelPlan({
model: "claude-fable-5",
effort: "xhigh",
hasImageHistory: false,
});
assert.deepEqual(plan.preset, { modelPreset: "pro", hasImageHistory: false });
// The model call must null the effort, since Conol resets it server-side.
assert.deepEqual(plan.model, { agentModel: "claude-fable-5", agentEffort: null });
assert.deepEqual(plan.effort, { agentEffort: "xhigh" });
// A model without an xhigh rung gets the clamped effort.
assert.deepEqual(
buildConolSessionModelPlan({
model: "claude-sonnet-5",
effort: "xhigh",
hasImageHistory: true,
}).effort,
{ agentEffort: "high" }
);
// A model with no effort ladder at all skips the effort call.
assert.equal(
buildConolSessionModelPlan({
model: "openrouter/fusion",
effort: "xhigh",
hasImageHistory: false,
}).effort,
null
);
});
it("reports native vision support for effort variants and preserves text-only models", () => {
assert.equal(
getResolvedModelCapabilities("conol-web/claude-fable-5-xhigh").supportsVision,
true
);
assert.equal(
getResolvedModelCapabilities("cnl/deepseek/deepseek-v4-pro").supportsVision,
false
);
});
it("separates effort suffixes and defaults to xhigh when none is pinned", () => {
assert.deepEqual(resolveConolModelSelection("conol-web/claude-fable-5-xhigh"), {
model: "claude-fable-5",
effort: "xhigh",
effortExplicit: true,
});
assert.deepEqual(resolveConolModelSelection("conol-web/claude-haiku-4-5-minimal"), {
model: "claude-haiku-4-5",
effort: "minimal",
effortExplicit: true,
});
// No suffix -> xhigh by default, per the provider contract.
assert.deepEqual(resolveConolModelSelection("cnl/gpt-5.6-sol"), {
model: "gpt-5.6-sol",
effort: "xhigh",
effortExplicit: false,
});
});
it("extracts only a valid Conol secure browser cookie", () => {
assert.deepEqual(
extractConolBrowserCredentials([
{ name: "other", value: "ignored", domain: ".conol.ai" },
{ name: SESSION_COOKIE_NAME, value: "browser-token", domain: ".conol.ai" },
]),
{ cookie: `${SESSION_COOKIE_NAME}=browser-token` }
);
assert.equal(
extractConolBrowserCredentials([
{ name: SESSION_COOKIE_NAME, value: "unsafe;cookie", domain: ".conol.ai" },
]),
null
);
assert.equal(
extractConolBrowserCredentials([
{ name: SESSION_COOKIE_NAME, value: "wrong-domain", domain: ".example.com" },
]),
null
);
});
it("maps remaining balances without inventing consumed history", () => {
const usage = buildConolUsageResult({
dailyCredits: 12.5,
subscriptionCredits: 7,
subscriptionAmount: 20,
extraCredits: 3,
total: 22.5,
});
assert.equal(usage.plan, "Subscription");
assert.equal(usage.quotas.credits.remaining, 22.5);
assert.equal(usage.quotas.credits.used, 0);
assert.equal(usage.quotas.subscription.total, 20);
assert.equal(usage.quotas.subscription.used, 13);
});
it("pins preset, model, then effort on a new session before sending the turn", async () => {
clearConolSessionBindingsForTests();
const originalFetch = globalThis.fetch;
const calls: Array<{ url: string; init?: RequestInit }> = [];
globalThis.fetch = (async (input: string | URL | Request, init?: RequestInit) => {
const url = input instanceof Request ? input.url : String(input);
calls.push({ url, init });
if (url.endsWith("/api/sessions")) {
return new Response(JSON.stringify({ sessionId: "session_123" }), {
status: 201,
headers: { "content-type": "application/json" },
});
}
if (url.endsWith("/api/sessions/session_123/model")) {
return new Response(JSON.stringify({ ok: true }), { status: 200 });
}
if (url.includes("/api/sessions/session_123/messages?logDeltas=1")) {
return new Response(
[
JSON.stringify({
type: "history_delta",
stages: [
{
logs: [
{
role: "assistant",
content: [{ type: "text", text: "OK" }],
},
],
},
],
}),
JSON.stringify({ type: "done" }),
].join("\n"),
{ status: 200, headers: { "content-type": "application/x-ndjson" } }
);
}
if (url.endsWith("/api/sessions/session_123/messages")) {
return new Response(null, { status: 202 });
}
throw new Error(`Unexpected test URL: ${url}`);
}) as typeof fetch;
try {
const executor = new ConolWebExecutor();
const result = await executor.execute({
model: "conol-web/claude-fable-5-xhigh",
stream: false,
body: {
messages: [{ role: "user", content: "Reply OK" }],
timezone: "Europe/Chisinau",
},
credentials: {
providerSpecificData: { cookie: `${SESSION_COOKIE_NAME}=synthetic-token` },
},
});
const capture = result as {
response: Response;
headers: Record<string, string>;
transformedBody: Record<string, unknown>;
};
assert.deepEqual(capture.headers, { cookie: "***" });
assert.equal(JSON.stringify(capture).includes("synthetic-token"), false);
assert.deepEqual(capture.transformedBody, {
model: "claude-fable-5",
effort: "xhigh",
effortRequested: "xhigh",
effortExplicit: true,
sessionId: "session_123",
reusedSession: false,
clientSessionBound: false,
imageCount: 0,
});
// The session is created empty — model/effort there would be ignored upstream.
const createBody = JSON.parse(String(calls[0]?.init?.body));
assert.equal(calls[0]?.url.endsWith("/api/sessions"), true);
assert.deepEqual(createBody.messages, []);
assert.equal("agentModel" in createBody, false);
assert.equal("agentEffort" in createBody, false);
assert.equal(calls[0]?.init?.headers instanceof Headers, false);
// Then exactly three /model calls, in preset -> model -> effort order.
const modelCalls = calls.filter((call) => call.url.endsWith("/model"));
assert.equal(modelCalls.length, 3);
assert.deepEqual(JSON.parse(String(modelCalls[0]?.init?.body)), {
modelPreset: "pro",
hasImageHistory: false,
});
assert.deepEqual(JSON.parse(String(modelCalls[1]?.init?.body)), {
agentModel: "claude-fable-5",
agentEffort: null,
});
assert.deepEqual(JSON.parse(String(modelCalls[2]?.init?.body)), { agentEffort: "xhigh" });
// Configuration must complete before the turn is submitted.
const turnIndex = calls.findIndex(
(call) => call.url.endsWith("/api/sessions/session_123/messages") && call.init?.method === "POST"
);
const lastModelIndex = calls.map((call) => call.url.endsWith("/model")).lastIndexOf(true);
assert.ok(lastModelIndex < turnIndex, "model config must precede the message turn");
const responseBody = await capture.response.json();
assert.equal(responseBody.choices[0].message.content, "OK");
assert.equal(responseBody.model, "claude-fable-5");
} finally {
globalThis.fetch = originalFetch;
clearConolSessionBindingsForTests();
}
});
it("defaults to xhigh, clamps it per model, and skips effort when unsupported", async () => {
const runWithModel = async (model: string) => {
clearConolSessionBindingsForTests();
const originalFetch = globalThis.fetch;
const modelBodies: unknown[] = [];
globalThis.fetch = (async (input: string | URL | Request, init?: RequestInit) => {
const url = input instanceof Request ? input.url : String(input);
if (url.endsWith("/api/sessions")) {
return new Response(JSON.stringify({ sessionId: "s1" }), { status: 201 });
}
if (url.endsWith("/api/sessions/s1/model")) {
modelBodies.push(JSON.parse(String(init?.body)));
return new Response(JSON.stringify({ ok: true }), { status: 200 });
}
if (url.includes("?logDeltas=1")) {
return new Response(
`${JSON.stringify({
type: "history_delta",
stages: [{ logs: [{ role: "assistant", content: [{ type: "text", text: "hi" }] }] }],
})}\n${JSON.stringify({ type: "done" })}`,
{ status: 200 }
);
}
return new Response(null, { status: 202 });
}) as typeof fetch;
try {
// No effort suffix -> the xhigh default applies.
await new ConolWebExecutor().execute({
model: `conol-web/${model}`,
stream: false,
body: { messages: [{ role: "user", content: "hi" }] },
credentials: { apiKey: `${SESSION_COOKIE_NAME}=synthetic-token` },
});
return modelBodies;
} finally {
globalThis.fetch = originalFetch;
clearConolSessionBindingsForTests();
}
};
// Supports xhigh -> applied verbatim.
assert.deepEqual((await runWithModel("claude-fable-5")).at(-1), { agentEffort: "xhigh" });
// No xhigh rung -> clamped down to high.
assert.deepEqual((await runWithModel("claude-sonnet-5")).at(-1), { agentEffort: "high" });
// No effort ladder -> only preset + model calls, no effort call.
const fusionBodies = await runWithModel("openrouter/fusion");
assert.equal(fusionBodies.length, 2);
assert.deepEqual(fusionBodies.at(-1), {
agentModel: "openrouter/fusion",
agentEffort: null,
});
});
it("reuses one Conol session for follow-ups and forwards only the newest user turn", async () => {
clearConolSessionBindingsForTests();
const originalFetch = globalThis.fetch;
const calls: Array<{ url: string; init?: RequestInit }> = [];
let streamCount = 0;
globalThis.fetch = (async (input: string | URL | Request, init?: RequestInit) => {
const url = input instanceof Request ? input.url : String(input);
calls.push({ url, init });
if (url.endsWith("/api/sessions") && init?.method === "POST") {
return new Response(JSON.stringify({ sessionId: "sticky_session" }), { status: 201 });
}
if (url.endsWith("/api/sessions/sticky_session/model")) {
return new Response(JSON.stringify({ ok: true }), { status: 200 });
}
if (url.endsWith("/api/sessions/sticky_session/messages") && init?.method === "POST") {
return new Response(null, { status: 202 });
}
if (url.includes("/api/sessions/sticky_session/messages?logDeltas=1")) {
streamCount += 1;
return new Response(
[
JSON.stringify({
type: "history_delta",
stages: [
{
logs: [
{
role: "assistant",
content: [{ type: "text", text: streamCount === 1 ? "First" : "Second" }],
},
],
},
],
}),
JSON.stringify({ type: "done" }),
].join("\n"),
{ status: 200 }
);
}
throw new Error(`Unexpected test URL: ${url}`);
}) as typeof fetch;
const executor = new ConolWebExecutor();
const sharedInput = {
model: "conol-web/claude-fable-5",
stream: false,
credentials: {
connectionId: "connection-1",
apiKey: `${SESSION_COOKIE_NAME}=synthetic-token`,
},
clientHeaders: { "x-claude-code-session-id": "logical-chat-1" },
};
try {
const first = await executor.execute({
...sharedInput,
body: {
messages: [
{ role: "system", content: "Never forward this system prompt." },
{ role: "user", content: "First user turn" },
],
},
});
const second = await executor.execute({
...sharedInput,
body: {
messages: [
{ role: "system", content: "Never forward this system prompt." },
{ role: "user", content: "First user turn" },
{ role: "assistant", content: "First" },
{ role: "tool", content: "Never forward this tool output." },
{ role: "user", content: "Second user turn" },
],
},
});
assert.equal((first as { response: Response }).response.status, 200);
assert.equal((second as { response: Response }).response.status, 200);
const createCalls = calls.filter(
(call) => call.url.endsWith("/api/sessions") && call.init?.method === "POST"
);
const turnCalls = calls.filter(
(call) =>
call.url.endsWith("/api/sessions/sticky_session/messages") && call.init?.method === "POST"
);
const modelCalls = calls.filter((call) => call.url.endsWith("/model"));
// One session, two turns posted into it.
assert.equal(createCalls.length, 1);
assert.equal(turnCalls.length, 2);
// Preset + model + effort once; the unchanged second turn re-pins nothing.
assert.equal(modelCalls.length, 3);
const createBody = JSON.parse(String(createCalls[0]?.init?.body));
const firstTurnBody = JSON.parse(String(turnCalls[0]?.init?.body));
const followUpBody = JSON.parse(String(turnCalls[1]?.init?.body));
assert.deepEqual(createBody.messages, []);
assert.deepEqual(firstTurnBody.messages, [{ type: "text", content: "First user turn" }]);
assert.deepEqual(followUpBody.messages, [{ type: "text", content: "Second user turn" }]);
assert.equal("source" in followUpBody, false);
assert.equal("agentModel" in followUpBody, false);
assert.doesNotMatch(
JSON.stringify([createBody, firstTurnBody, followUpBody]),
/system prompt|tool output|"role"/
);
assert.equal(
(first as { transformedBody: Record<string, unknown> }).transformedBody.reusedSession,
false
);
assert.equal(
(second as { transformedBody: Record<string, unknown> }).transformedBody.reusedSession,
true
);
} finally {
globalThis.fetch = originalFetch;
clearConolSessionBindingsForTests();
}
});
it("re-pins the same session on a model switch instead of stranding it", async () => {
clearConolSessionBindingsForTests();
const originalFetch = globalThis.fetch;
const calls: Array<{ url: string; body: unknown }> = [];
let created = 0;
globalThis.fetch = (async (input: string | URL | Request, init?: RequestInit) => {
const url = input instanceof Request ? input.url : String(input);
if (url.endsWith("/api/sessions") && init?.method === "POST") {
created += 1;
return new Response(JSON.stringify({ sessionId: "switch_session" }), { status: 201 });
}
if (url.endsWith("/model")) {
calls.push({ url, body: JSON.parse(String(init?.body)) });
return new Response(JSON.stringify({ ok: true }), { status: 200 });
}
if (url.includes("?logDeltas=1")) {
return new Response(
`${JSON.stringify({
type: "history_delta",
stages: [{ logs: [{ role: "assistant", content: [{ type: "text", text: "ok" }] }] }],
})}\n${JSON.stringify({ type: "done" })}`,
{ status: 200 }
);
}
return new Response(null, { status: 202 });
}) as typeof fetch;
const shared = {
stream: false,
credentials: {
connectionId: "connection-1",
apiKey: `${SESSION_COOKIE_NAME}=synthetic-token`,
},
clientHeaders: { "x-session-id": "same-chat" },
body: { messages: [{ role: "user", content: "hi" }] },
};
try {
const executor = new ConolWebExecutor();
await executor.execute({ ...shared, model: "conol-web/claude-fable-5-high" });
calls.length = 0;
// Same logical chat, different model -> must reuse the session and re-pin.
const switched = await executor.execute({ ...shared, model: "conol-web/gpt-5.6-sol-low" });
assert.equal(created, 1, "a model switch must not create a second session");
assert.equal(
(switched as { transformedBody: Record<string, unknown> }).transformedBody.reusedSession,
true
);
// Preset is already primed, so only model + effort are re-sent.
assert.deepEqual(
calls.map((call) => call.body),
[{ agentModel: "gpt-5.6-sol", agentEffort: null }, { agentEffort: "low" }]
);
// A third turn with no change must not re-pin anything.
calls.length = 0;
await executor.execute({ ...shared, model: "conol-web/gpt-5.6-sol-low" });
assert.deepEqual(calls, []);
} finally {
globalThis.fetch = originalFetch;
clearConolSessionBindingsForTests();
}
});
it("keeps different client session IDs in different Conol sessions", async () => {
clearConolSessionBindingsForTests();
const originalFetch = globalThis.fetch;
let createdCount = 0;
globalThis.fetch = (async (input: string | URL | Request, init?: RequestInit) => {
const url = input instanceof Request ? input.url : String(input);
if (url.endsWith("/api/sessions") && init?.method === "POST") {
createdCount += 1;
return new Response(JSON.stringify({ sessionId: `session_${createdCount}` }), {
status: 201,
});
}
if (url.endsWith("/model")) {
return new Response(JSON.stringify({ ok: true }), { status: 200 });
}
if (url.includes("/messages?logDeltas=1")) {
return new Response(
`${JSON.stringify({
type: "history_delta",
stages: [
{
logs: [{ role: "assistant", content: [{ type: "text", text: "Isolated" }] }],
},
],
})}\n${JSON.stringify({ type: "done" })}\n`,
{ status: 200 }
);
}
if (url.includes("/messages") && init?.method === "POST") {
return new Response(null, { status: 202 });
}
throw new Error(`Unexpected test URL: ${url}`);
}) as typeof fetch;
try {
const executor = new ConolWebExecutor();
for (const sessionId of ["client-a", "client-b"]) {
await executor.execute({
model: "conol-web/claude-fable-5",
stream: false,
body: { messages: [{ role: "user", content: "Same text" }] },
credentials: {
connectionId: "connection-1",
apiKey: `${SESSION_COOKIE_NAME}=synthetic-token`,
},
clientHeaders: { "x-session-id": sessionId },
});
}
assert.equal(createdCount, 2);
} finally {
globalThis.fetch = originalFetch;
clearConolSessionBindingsForTests();
}
});
it("uploads the structured image and references it before clean user text", async () => {
clearConolSessionBindingsForTests();
const originalFetch = globalThis.fetch;
const calls: Array<{ url: string; init?: RequestInit }> = [];
const png = Buffer.from(
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9Wl2nGQAAAAASUVORK5CYII=",
"base64"
);
globalThis.fetch = (async (input: string | URL | Request, init?: RequestInit) => {
const url = input instanceof Request ? input.url : String(input);
calls.push({ url, init });
if (url.endsWith("/api/assets")) {
assert.deepEqual(Buffer.from(init?.body as Uint8Array), png);
return new Response(
JSON.stringify({
id: "asset_1",
url: "/api/assets/asset_1",
mediaType: "image/png",
}),
{ status: 201 }
);
}
if (url.endsWith("/api/sessions")) {
return new Response(JSON.stringify({ sessionId: "image_session" }), { status: 201 });
}
if (url.endsWith("/api/sessions/image_session/model")) {
return new Response(JSON.stringify({ ok: true }), { status: 200 });
}
if (url.includes("/api/sessions/image_session/messages?logDeltas=1")) {
return new Response(
`${JSON.stringify({
type: "history_delta",
stages: [
{
logs: [{ role: "assistant", content: [{ type: "text", text: "Image received" }] }],
},
],
})}\n${JSON.stringify({ type: "done" })}\n`,
{ status: 200 }
);
}
if (url.endsWith("/api/sessions/image_session/messages")) {
return new Response(null, { status: 202 });
}
throw new Error(`Unexpected test URL: ${url}`);
}) as typeof fetch;
try {
const result = await new ConolWebExecutor().execute({
model: "conol-web/claude-fable-5",
stream: false,
body: {
messages: [
{ role: "system", content: "System data must stay local." },
{
role: "user",
content: [
{
type: "text",
text:
"[Image 1]: (unavailable)\n" +
"[Image: source: C:\\Users\\someone\\.claude\\image-cache\\id\\2.png]\n" +
"What is on the image?",
},
{
type: "image_url",
image_url: { url: `data:image/png;base64,${png.toString("base64")}` },
},
],
},
],
},
credentials: { apiKey: `${SESSION_COOKIE_NAME}=synthetic-token` },
});
assert.equal((result as { response: Response }).response.status, 200);
const turnCall = calls.find(
(call) =>
call.url.endsWith("/api/sessions/image_session/messages") && call.init?.method === "POST"
);
const turnBody = JSON.parse(String(turnCall?.init?.body));
assert.deepEqual(turnBody.messages, [
{
type: "image",
content: "/api/assets/asset_1",
mediaType: "image/png",
},
{ type: "text", content: "What is on the image?" },
]);
assert.doesNotMatch(JSON.stringify(turnBody), /unavailable|image-cache|System data/);
// An image turn must prime the preset as multimodal.
const presetBody = JSON.parse(
String(calls.find((call) => call.url.endsWith("/model"))?.init?.body)
);
assert.deepEqual(presetBody, { modelPreset: "pro", hasImageHistory: true });
} finally {
globalThis.fetch = originalFetch;
clearConolSessionBindingsForTests();
}
});
it("emits OpenAI SSE data and a terminal DONE marker", async () => {
clearConolSessionBindingsForTests();
const originalFetch = globalThis.fetch;
globalThis.fetch = (async (input: string | URL | Request) => {
const url = input instanceof Request ? input.url : String(input);
if (url.endsWith("/api/sessions")) {
return new Response(JSON.stringify({ sessionId: "session_stream" }), { status: 201 });
}
return new Response(
`${JSON.stringify({
type: "history_delta",
stages: [
{
logs: [{ role: "assistant", content: [{ type: "text", text: "Streamed" }] }],
},
],
})}\n${JSON.stringify({ type: "done" })}\n`,
{ status: 200 }
);
}) as typeof fetch;
try {
const result = await new ConolWebExecutor().execute({
model: "conol-web/claude-haiku-4-5",
stream: true,
body: { messages: [{ role: "user", content: "Test" }] },
credentials: { apiKey: `${SESSION_COOKIE_NAME}=synthetic-token` },
});
const text = await (result as { response: Response }).response.text();
assert.match(text, /"content":"Streamed"/);
assert.match(text, /data: \[DONE\]/);
} finally {
globalThis.fetch = originalFetch;
}
});
});

View File

@@ -215,6 +215,62 @@ test("VB-S02b: respects native vision support for GPT-family models", async () =
}
});
test("VB-S02c: Conol multimodal models bypass the vision bridge", async () => {
const guardrail = createGuardrail();
const model = "conol-web/claude-fable-5-xhigh";
const payload = createPayload({
model,
messages: [
{
role: "user",
content: [
{ type: "text", text: "What is this?" },
{
type: "image_url",
image_url: { url: "data:image/png;base64,aW1hZ2U=" },
},
],
},
],
});
visionCallCount = 0;
const result = await guardrail.preCall(payload, createContext({ model }));
assert.equal(getResolvedModelCapabilities(model).supportsVision, true);
assert.strictEqual(result.block, false);
assert.strictEqual(result.modifiedPayload, undefined);
assert.strictEqual(visionCallCount, 0);
});
test("VB-S02d: Conol text-only models remain eligible for the vision bridge", async () => {
const guardrail = createGuardrail();
const model = "conol-web/deepseek/deepseek-v4-pro";
const payload = createPayload({
model,
messages: [
{
role: "user",
content: [
{ type: "text", text: "What is this?" },
{
type: "image_url",
image_url: { url: "data:image/png;base64,aW1hZ2U=" },
},
],
},
],
});
visionCallCount = 0;
const result = await guardrail.preCall(payload, createContext({ model }));
assert.equal(getResolvedModelCapabilities(model).supportsVision, false);
assert.strictEqual(result.block, false);
assert.notStrictEqual(result.modifiedPayload, undefined);
assert.strictEqual(visionCallCount, 1);
});
test("VB-S02: model capabilities returns supportsVision for known models", () => {
const gpt4oCaps = getResolvedModelCapabilities("openai/gpt-4o");
// supportsVision may be true (if sync data exists) or null (if not synced)

View File

@@ -82,6 +82,12 @@ test("web session credential metadata identifies cookie, token, and no-auth prov
// #5465 — t3.chat ships a step-by-step DevTools copy hint (localStorage + Cookie header).
hintKey: "t3ChatWebCookieHint",
});
assert.deepEqual(webSessionCredentials.getWebSessionCredentialRequirement("conol-web"), {
kind: "cookie",
credentialName: "__Secure-better-auth.session_token",
placeholder: "__Secure-better-auth.session_token=... or full Cookie header from conol.ai",
acceptsFullCookieHeader: true,
});
});
test("web session credential validator requires provider-specific non-empty values", () => {