Files
OmniRoute/open-sse/services/conolModels.ts
NOXX - Commiter f5ce51a9ff 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>
2026-08-11 09:08:23 -03:00

311 lines
12 KiB
TypeScript

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;
}