docs: Added documentation for LLMProviders and named instances

This commit is contained in:
2026-07-09 19:35:18 +05:30
parent 6626adf38d
commit cc9d0006e5
10 changed files with 195 additions and 13 deletions

View File

@@ -11,7 +11,7 @@ import {
setProviderMapping,
updateProviderInstance,
} from "@/app/play/actions";
import type { LLMProviderInstance } from "@/lib/provider-manager";
import type { LLMProviderInstance } from "@omnia/llm";
interface ConfigStatus {
apiKeySet: boolean;

View File

@@ -4,8 +4,7 @@ import path from "path";
import fs from "fs";
import { simulationManager } from "@/lib/simulation";
import type { SimSnapshot } from "@/lib/simulation";
import { ProviderManager } from "@/lib/provider-manager";
import type { LLMProviderInstance } from "@/lib/provider-manager";
import { ProviderManager, LLMProviderInstance } from "@omnia/llm";
function resolveScenarioPath(relative: string): string {
const cwd = process.cwd();

View File

@@ -13,7 +13,7 @@ import {
listProviderInstances,
} from "@/app/play/actions";
import type { SimSnapshot } from "@/lib/simulation-types";
import type { LLMProviderInstance } from "@/lib/provider-manager";
import type { LLMProviderInstance } from "@omnia/llm";
function IntentTag({
intent,

View File

@@ -25,9 +25,8 @@ import {
IActorProseGenerator,
buildBufferEntryForIntent,
} from "@omnia/actor";
import { GeminiProvider, ILLMProvider, MockLLMProvider } from "@omnia/llm";
import { GeminiProvider, ILLMProvider, MockLLMProvider, ProviderManager } from "@omnia/llm";
import { ScenarioLoader } from "@omnia/scenario";
import { ProviderManager } from "./provider-manager";
import type {
IntentInfo,

View File

@@ -7,6 +7,10 @@
".": "./dist/index.js"
},
"dependencies": {
"@types/node": "^26.1.0"
"@types/node": "^26.1.0",
"better-sqlite3": "^12.11.1"
},
"devDependencies": {
"@types/better-sqlite3": "^7.6.13"
}
}

View File

@@ -2,3 +2,4 @@ export * from "./llm.js";
export * from "./config.js";
export * from "./providers/google-genai.js";
export * from "./providers/mock.js";
export * from "./provider-manager.js";

View File

@@ -1,12 +1,27 @@
import Database from "better-sqlite3";
import path from "path";
import fs from "fs";
import type { LLMProviderInstance } from "@omnia/llm";
import type { LLMProviderInstance } from "./llm.js";
export type { LLMProviderInstance };
function getWorkspaceRoot() {
let current = process.cwd();
while (current !== "/" && current !== path.parse(current).root) {
if (
fs.existsSync(path.join(current, "pnpm-workspace.yaml")) ||
fs.existsSync(path.join(current, "package.json"))
) {
if (fs.existsSync(path.join(current, "pnpm-workspace.yaml"))) {
return current;
}
}
current = path.dirname(current);
}
return process.cwd();
}
function getSettingsDb() {
const dbDir = path.resolve(process.cwd(), "data");
const wsRoot = getWorkspaceRoot();
const dbDir = path.resolve(wsRoot, "data");
if (!fs.existsSync(dbDir)) {
fs.mkdirSync(dbDir, { recursive: true });
}
@@ -127,6 +142,7 @@ export class ProviderManager {
static getActive(): LLMProviderInstance | null {
const db = getSettingsDb();
try {
// Query the DB
const row = db.prepare(`SELECT * FROM provider_instances WHERE isActive = 1`).get() as {
id: string;
name: string;
@@ -135,7 +151,34 @@ export class ProviderManager {
isActive: number;
modelName?: string;
} | undefined;
if (!row) return null;
if (!row) {
// Check if there are any rows at all
const totalCount = db.prepare(`SELECT COUNT(*) as count FROM provider_instances`).get() as { count: number };
if (totalCount.count === 0) {
// Database is completely empty! Check if GOOGLE_API_KEY env is set.
const envKey = process.env.GOOGLE_API_KEY;
if (envKey && envKey.trim()) {
// Auto-bootstrap default active instance from env
const id = "provider-default-env";
db.prepare(`
INSERT INTO provider_instances (id, name, providerName, apiKey, isActive, modelName)
VALUES (?, ?, ?, ?, ?, ?)
`).run(id, "Default (Env)", "google-genai", envKey, 1, "gemini-2.5-flash");
return {
id,
name: "Default (Env)",
providerName: "google-genai",
apiKey: envKey,
isActive: true,
modelName: "gemini-2.5-flash",
};
}
}
return null;
}
return {
id: row.id,
name: row.name,
@@ -144,6 +187,20 @@ export class ProviderManager {
isActive: true,
modelName: row.modelName || undefined,
};
} catch {
// Lock or write issue fallback: return an in-memory active key if env key exists
const envKey = process.env.GOOGLE_API_KEY;
if (envKey) {
return {
id: "provider-default-env-fallback",
name: "Default (Env Fallback)",
providerName: "google-genai",
apiKey: envKey,
isActive: true,
modelName: "gemini-2.5-flash",
};
}
return null;
} finally {
db.close();
}

View File

@@ -2,6 +2,7 @@ import { z } from "zod";
import { ChatGoogleGenerativeAI } from "@langchain/google-genai";
import { ILLMProvider, LLMRequest, LLMResponse, LLMCallRecord } from "../llm.js";
import { llmConfig } from "../config.js";
import { ProviderManager } from "../provider-manager.js";
export class GeminiProvider implements ILLMProvider {
providerName = "Gemini";
@@ -9,13 +10,30 @@ export class GeminiProvider implements ILLMProvider {
lastCalls: LLMCallRecord[] = [];
constructor(apiKey?: string, modelName?: string) {
const key = apiKey || llmConfig.GOOGLE_API_KEY;
let key = apiKey;
let model = modelName;
if (!key) {
const active = ProviderManager.getActive();
if (active) {
key = active.apiKey;
if (!model) {
model = active.modelName;
}
}
}
if (!key) {
key = llmConfig.GOOGLE_API_KEY;
}
if (!key) {
throw new Error("GOOGLE_API_KEY is required to initialize GeminiProvider");
}
this.model = new ChatGoogleGenerativeAI({
apiKey: key,
model: modelName || "gemini-2.5-flash",
model: model || "gemini-2.5-flash",
});
}

7
pnpm-lock.yaml generated
View File

@@ -365,6 +365,13 @@ importers:
'@types/node':
specifier: ^26.1.0
version: 26.1.0
better-sqlite3:
specifier: ^12.11.1
version: 12.11.1
devDependencies:
'@types/better-sqlite3':
specifier: ^7.6.13
version: 7.6.13
packages/memory:
dependencies:

View File

@@ -0,0 +1,97 @@
---
title: LLM Providers & Configuration
description: Details of the LLM provider instances, task routing, and self-bootstrapping setup in Omnia.
sidebar:
order: 5
---
In Omnia, all non-player character behaviors, action validation, intent decoding, and time step logic are simulated using Large Language Models (LLMs). The LLM subsystem is built around **polymorphism, key instance management, and task provider routing**.
## Core Interfaces
All LLM providers implement the common `ILLMProvider` interface defined in `packages/llm/src/llm.ts`:
```typescript
export interface ILLMProvider {
providerName: string;
generateStructuredResponse<T extends z.ZodTypeAny>(
request: LLMRequest<T>,
): Promise<LLMResponse<z.infer<T>>>;
lastCalls?: LLMCallRecord[];
}
```
The codebase provides two primary implementations:
1. **`GeminiProvider`:** The production provider utilizing Google's Gemini Models via the `@langchain/google-genai` SDK.
2. **`MockLLMProvider`:** A stateless, pre-programmed mock provider used for fast, deterministic unit testing and local integration tests.
---
## LLM Provider Instances
To support multiple different API keys, key rotation, and model variation, Omnia utilizes a **Provider Instance model** rather than relying on static configuration:
```typescript
export interface LLMProviderInstance {
id: string;
name: string;
providerName: string;
apiKey: string;
isActive: boolean;
modelName?: string;
}
```
Users can register multiple provider instances in the **Configuration Page** under the GUI. Each instance is given:
* A friendly, human-readable name (e.g., `"Gemini Production Key"`, `"Experimental Gemini Pro"`).
* A provider type (e.g., `google-genai`, `mock`).
* An API key credential.
* A custom target model name (e.g., `gemini-2.5-flash` or `gemini-2.5-pro`).
* An **Active** status flag (one key is marked as globally active).
Configurations are stored globally in `data/settings.db` (separated from specific simulation run databases like `data/sim-*.db` to keep key storage and audit logs isolated).
---
## Task Provider Routing
During a simulation run, the engine executes four distinct LLM operations. To optimize costs, latency, or model accuracy, you can route each of these tasks to different LLM provider instances:
| Task Name | Key ID | Description | Default Model |
| :--- | :--- | :--- | :--- |
| **Actor Prose Generation** | `actor-prose` | Generates roleplay and narrative behavioral prose for Non-Player Characters. | `gemini-2.5-flash` |
| **LLM Validator** | `llm-validator` | Arbitrates and validates proposed actions against the world state rules and constraints. | `gemini-2.5-flash` |
| **Intent Decoder** | `intent-decoder` | Parses and splits free-text actions/prose into structured intent sequences. | `gemini-2.5-flash` |
| **TimeDelta Generator** | `timedelta` | Calculates the duration of character actions to advance the game clock. | `gemini-2.5-flash` |
If no specific provider instance is mapped to a task, the task automatically routes to the globally marked **Active** provider instance.
---
## Automatic Bootstrapping (Environment Key Fallback)
To maintain backwards-compatibility and support headless runs, live evaluation suites, and automated unit tests without requiring database pre-configuration, the config manager supports **self-bootstrapping**:
1. When the provider manager queries the active key instance, if `data/settings.db` contains **0 registered keys**, it checks the process environment for `GOOGLE_API_KEY`.
2. If `process.env.GOOGLE_API_KEY` is present, it automatically creates, saves, and activates a default provider instance (`Default (Env)`) in `settings.db`.
3. If database write locks occur (e.g., during high-concurrency Vitest test suites), the system seamlessly returns a temporary in-memory `LLMProviderInstance` to keep execution fluent and error-free.
---
## Developer Guide: Managing Mappings
Configuration settings are managed through `ProviderManager` static methods:
```typescript
// Query the active provider configuration
const activeConfig = ProviderManager.getActive();
// List all registered provider instances
const allConfigs = ProviderManager.list();
// Retrieve task-specific mappings
const mappings = ProviderManager.getMappings(); // e.g., { "actor-prose": "provider-123" }
// Map a task to a provider instance
ProviderManager.setMapping("actor-prose", "provider-123");
```