mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-12 10:12:11 +03:00
* 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>
964 lines
34 KiB
TypeScript
964 lines
34 KiB
TypeScript
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;
|
|
}
|
|
});
|
|
});
|