Lyceum Serverless Inference
Use Context212 search as the retrieval layer for models hosted on Lyceum.
Lyceum serves open models through an OpenAI-compatible serverless endpoint. Pair them with Context212 search to build RAG pipelines where the retrieval stays on Context212's infrastructure and the generation runs on Lyceum.
The flow is:
- Search Context212 for the passages most relevant to the user's question.
- Pack those passages into the model's context window.
- Call the Lyceum model to generate an answer grounded in the retrieved content.
Prerequisites
- A
C212_API_KEY, available in the Console → API Keys section. - A Lyceum API key. Store it as
LYCEUM_API_KEY. - At least one workspace with indexed documents on Context212.
Installation
npm install openaiThe openai package is used here only for its client; Lyceum's endpoint is fully compatible with it.
Full example
import OpenAI from "openai";
const C212_API_KEY = process.env.C212_API_KEY!;
const LYCEUM_API_KEY = process.env.LYCEUM_API_KEY!;
const lyceum = new OpenAI({
baseURL: "https://api.lyceum.technology/api/v2/external/serverless",
apiKey: LYCEUM_API_KEY,
});
async function search(
query: string,
workspaceId?: number[],
maxResults = 5,
): Promise<Record<string, unknown>[]> {
const payload: Record<string, unknown> = { query, max_results: maxResults };
if (workspaceId) payload.workspace_id = workspaceId;
const response = await fetch("https://api.context212.com/api/v1/search", {
method: "POST",
headers: {
Authorization: `Bearer ${C212_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
if (!response.ok) throw new Error(`Search failed: ${response.status}`);
return (await response.json()).results;
}
async function answer(
question: string,
workspaceId?: number[],
model = "openbmb/MiniCPM-V-4_5",
): Promise<string> {
const results = await search(question, workspaceId);
const context = results
.filter((r: any) => r.content)
.map(
(r: any) =>
`[${r.source.filename}, p.${r.source.page_start}]\n${r.content}`,
)
.join("\n\n");
const completion = await lyceum.chat.completions.create({
model,
messages: [
{
role: "system",
content:
"You are a helpful assistant. Answer the user's question using only " +
"the provided context. If the context does not contain enough information, " +
"say so.\n\nContext:\n" +
context,
},
{ role: "user", content: question },
],
});
return completion.choices[0].message.content ?? "";
}
console.log(await answer("What is our data retention policy?"));Context212 search as a tool
Instead of always searching before calling the model, you can expose Context212 search as a tool and let the model decide when to call it. The model issues an context212_search tool call when it needs context; your code executes the search and feeds the results back; the model then produces a final answer.
import OpenAI from "openai";
const C212_API_KEY = process.env.C212_API_KEY!;
const LYCEUM_API_KEY = process.env.LYCEUM_API_KEY!;
const lyceum = new OpenAI({
baseURL: "https://api.lyceum.technology/api/v2/external/serverless",
apiKey: LYCEUM_API_KEY,
});
const SEARCH_TOOL = {
type: "function" as const,
function: {
name: "context212_search",
description:
"Search the company knowledge base for passages relevant to a query. " +
"Returns ranked excerpts with their source filename and page numbers.",
parameters: {
type: "object",
properties: {
query: {
type: "string",
description: "Natural-language search query.",
},
max_results: {
type: "integer",
description: "Number of passages to return (1–50, default 5).",
default: 5,
},
},
required: ["query"],
},
},
};
async function runSearch(query: string, maxResults = 5): Promise<string> {
const response = await fetch("https://api.context212.com/api/v1/search", {
method: "POST",
headers: {
Authorization: `Bearer ${C212_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ query, max_results: maxResults }),
});
if (!response.ok) throw new Error(`Search failed: ${response.status}`);
const results = (await response.json()).results;
const passages = results
.filter((r: any) => r.content)
.map(
(r: any) =>
`[${r.source.filename}, p.${r.source.page_start}]\n${r.content}`,
);
return passages.length ? passages.join("\n\n") : "No results found.";
}
async function answer(
question: string,
model = "openbmb/MiniCPM-V-4_5",
): Promise<string> {
const messages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{ role: "user", content: question },
];
while (true) {
const completion = await lyceum.chat.completions.create({
model,
tools: [SEARCH_TOOL],
messages,
});
const choice = completion.choices[0];
if (choice.finish_reason === "tool_calls") {
messages.push(choice.message);
for (const call of choice.message.tool_calls ?? []) {
const args = JSON.parse(call.function.arguments);
const result = await runSearch(args.query, args.max_results ?? 5);
messages.push({
role: "tool",
tool_call_id: call.id,
content: result,
});
}
} else {
return choice.message.content ?? "";
}
}
}
console.log(await answer("What is our data retention policy?"));The loop handles the case where the model issues multiple search calls in sequence before producing a final answer.
Multimodal input
Several models on Lyceum (such as openbmb/MiniCPM-V-4_5) are vision-language models that accept images alongside text. Pass an image_url content part to describe or reason over an image:
const response = await lyceum.chat.completions.create({
model: "openbmb/MiniCPM-V-4_5",
messages: [
{
role: "user",
content: [
{ type: "text", text: "What's in this image?" },
{ type: "image_url", image_url: { url: "<image-url>" } },
],
},
],
max_tokens: 256,
});
console.log(response.choices[0].message.content);You can combine this with Context212 search to ground answers about an image in your indexed documents.
Scoping retrieval to a workspace
Pass workspace_id to limit search to a specific workspace. This is useful in multi-tenant products where each customer's data lives in a dedicated workspace.
await answer("Summarize the onboarding checklist", [42]);Choosing a model
Lyceum's catalog includes a wide range of hosted models. Pass the model name to the model parameter:
| Model | Notes |
|---|---|
meta-llama/Llama-3.3-70B-Instruct | Strong general-purpose model, good default choice |
Qwen/Qwen3-235B-A22B-Instruct-2507 | Large MoE model for demanding reasoning |
Qwen/Qwen3-32B | Capable mid-size model, lower latency |
google/gemma-3-27b-it | Compact, efficient instruction-tuned model |
openai/gpt-oss-120b | Open-weight GPT model |
Qwen/Qwen2.5-VL-72B-Instruct | Vision-language model, accepts image input |
openbmb/MiniCPM-V-4_5 | Lightweight vision-language model |
You can list the models available to your key at any time:
console.log((await lyceum.models.list()).data.map((m) => m.id));Check the Lyceum documentation for the current model list and pricing.
Streaming responses
Lyceum's endpoint supports streaming. Enable it by passing stream: true and iterating over the response:
const stream = await lyceum.chat.completions.create({
model: "openbmb/MiniCPM-V-4_5",
messages: [/* ... */],
stream: true,
});
for await (const chunk of stream) {
const delta = chunk.choices[0].delta.content;
if (delta) process.stdout.write(delta);
}