Skip to content

Connect your LLM provider

Core LLM Wiki never calls a model directly. You pass an llmProvider object, and the engine calls it with ready-made prompts.

interface LLMProvider {
generateText(p: { systemPrompt: string; userPrompt: string }): Promise<string>; // required
embed?(text: string): Promise<number[]>; // semantic search
maxOutputTokens?: number; // batching hint
generateTextWithUsage?(p): Promise<{ text: string; usage?: { inputTokens: number; outputTokens: number } }>;
classify?(req: ClassifyRequest): Promise<ClassifyResponse>; // ontology backfill
}

The engine calls each method with your provider object as this, so class-based providers that keep an SDK client on the instance work fine.

Return the model’s raw text. The engine expects JSON and tolerates a markdown code fence around it.

import OpenAI from 'openai';
const openai = new OpenAI();
const llmProvider = {
generateText: async ({ systemPrompt, userPrompt }) => {
const r = await openai.chat.completions.create({
model: 'gpt-4o-mini',
response_format: { type: 'json_object' },
messages: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: userPrompt },
],
});
return r.choices[0].message.content ?? '{}';
},
};

Return a fixed-length vector for any input. The engine calls it once per fact when the fact is created or updated, and once per read() query.

embed: async (text) => {
const r = await openai.embeddings.create({ model: 'text-embedding-3-small', input: text });
return r.data[0].embedding;
},
  • Without embed, or when it throws, read() falls back to keyword search.
  • Pass onRetrievalFallback (beside llmProvider) to find out when an embedding attempt failed.
  • Changed embedding models? Run runReembed(). Until it finishes, a dimension mismatch makes reads fall back to keyword search instead of returning bad matches.
  • The engine embeds at most maxEmbedChars characters of title, body, and tags (6,000 by default).

This is your model’s output-token ceiling. Heal and ontology backfill use it to size their first batch correctly, instead of discovering the limit from a truncated response and retrying with a smaller batch. It’s a hint, not a guarantee.

generateTextWithUsage (optional, for metering)

Section titled “generateTextWithUsage (optional, for metering)”

If you implement this, the engine calls it instead of generateText and uses the reported token counts for tokenBudget accounting and llm_usage diagnostics. Without it, usage is estimated at characters ÷ 4.

generateTextWithUsage: async ({ systemPrompt, userPrompt }) => {
const r = await openai.chat.completions.create({ /* … */ });
return {
text: r.choices[0].message.content ?? '{}',
usage: { inputTokens: r.usage!.prompt_tokens, outputTokens: r.usage!.completion_tokens },
};
},

classify (optional, non-generative typing)

Section titled “classify (optional, non-generative typing)”

A System-One classifier can assign ontology types to facts during backfill without generating text. It’s off until you opt in with config.ontology.backfillClassifier: 'auto'. See Ontologies → classifier mode.

These go on the options object, not inside config:

createWiki(db, {
llmProvider,
config: { /* … */ },
onRetrievalFallback: (err) => showOfflineBadge(),
onDiagnostic: (d) => telemetry.record(d),
vectorRanker, // see "Pluggable vector search"
vectorRankerFallback: 'js-cosine',
});