Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- **Atlas Cloud provider** — `LLMWIKI_PROVIDER=atlascloud` (aliases `atlas-cloud`, `atlas`) routes chat and tool calls through the [Atlas Cloud](https://www.atlascloud.ai) gateway, which exposes an OpenAI-compatible API across models from several publishers. Authenticate with `ATLASCLOUD_API_KEY` or `ATLAS_CLOUD_API_KEY`; `ATLASCLOUD_BASE_URL` overrides the endpoint.

Model ids are namespaced by publisher, and `compile` extracts concepts through a tool call, so the default is a model Atlas Cloud lists as supporting tools. Embeddings are not wired up: the provider fails closed rather than inheriting OpenAI's semantics, so route them elsewhere with `LLMWIKI_EMBEDDING_PROVIDER` for semantic search.

Contributed by **@binyangzhu000-sudo** (#167).

- **Separate embedding provider** — `LLMWIKI_EMBEDDING_PROVIDER` selects the backend that serves embeddings, independently of `LLMWIKI_PROVIDER`. This makes split setups possible, such as Claude Agent SDK for generation with a local vLLM instance serving embeddings over its OpenAI-compatible endpoint. Valid values are `anthropic`, `claude-agent`, `openai`, and `ollama`. `minimax` and `copilot` expose no embeddings API, and naming one now fails with a clear error listing the valid values instead of an opaque failure from the provider's `embed()`. When the variable is set, the provider's own credential is required — `VOYAGE_API_KEY` for `anthropic` and `claude-agent`, `OPENAI_API_KEY` for `openai` — unless `OPENAI_EMBEDDINGS_BASE_URL` points at a self-hosted endpoint, which needs no key. Behaviour is unchanged when the variable is unset.

Thanks to **@knew-inventai** for the request (#154).
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ Do not use llmwiki as a general static-site generator, a heavy ontology database
- **SDK.** `createWiki({ root })` drives ingest, compile, query, context, status, export, eval, and OKF import/export from TypeScript without shelling out.
- **Open Knowledge Format exchange.** Export and import OKF bundles for portable, markdown-native knowledge exchange. External OKF imports are staged through the review queue by default; trusted bundles can be written live explicitly.
- **Other portable exports.** Export JSON, JSON-LD, GraphML, Marp slides, and `llms.txt` for downstream systems.
- **Provider portable.** Anthropic, Claude Agent SDK local login, OpenAI-compatible servers, Ollama, GitHub Copilot, and local OpenAI-compatible runtimes.
- **Provider portable.** Anthropic, Claude Agent SDK local login, OpenAI-compatible servers, Ollama, GitHub Copilot, Atlas Cloud, and local OpenAI-compatible runtimes.

## Configurable Lifecycle Profiles (CLP)

Expand Down Expand Up @@ -276,6 +276,7 @@ Provider selection is environment-driven:
| OpenAI-compatible | `LLMWIKI_PROVIDER=openai`, `OPENAI_API_KEY`, optional `OPENAI_BASE_URL` |
| Ollama | `LLMWIKI_PROVIDER=ollama`, `OLLAMA_HOST` |
| GitHub Copilot | `LLMWIKI_PROVIDER=copilot`, `GITHUB_TOKEN=$(gh auth token)` |
| Atlas Cloud | `LLMWIKI_PROVIDER=atlascloud`, `ATLASCLOUD_API_KEY` |

See [`docs/configuration/providers.mdx`](docs/configuration/providers.mdx) and [`docs/configuration/environment-variables.mdx`](docs/configuration/environment-variables.mdx).

Expand Down
13 changes: 12 additions & 1 deletion docs/configuration/environment-variables.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ You don't need to set everything - only the variables relevant to your chosen pr

| Variable | Default | Description |
|---|---|---|
| `LLMWIKI_PROVIDER` | `anthropic` | Provider to use. One of: `anthropic`, `openai`, `ollama`, `minimax`, `copilot`, `claude-agent` |
| `LLMWIKI_PROVIDER` | `anthropic` | Provider to use. One of: `anthropic`, `openai`, `ollama`, `minimax`, `copilot`, `claude-agent`, `atlascloud` (aliases: `atlas-cloud`, `atlas`) |
| `LLMWIKI_MODEL` | Provider default | Model name to use. Overrides the provider's built-in default model |
| `LLMWIKI_EMBEDDING_PROVIDER` | Active chat provider | Backend serving embeddings, independent of `LLMWIKI_PROVIDER`. One of `anthropic`, `claude-agent`, `openai`, `ollama` |
| `LLMWIKI_EMBEDDING_MODEL` | Provider default | Embedding model to use. Applies only when the effective embedding provider is `openai` or `ollama` - ignored for `anthropic` and `claude-agent`, even when `LLMWIKI_EMBEDDING_PROVIDER` names one of them explicitly |
Expand Down Expand Up @@ -79,6 +79,17 @@ Either `ANTHROPIC_API_KEY` or `ANTHROPIC_AUTH_TOKEN` satisfies authentication -

---

## Atlas Cloud

| Variable | Required | Description |
|---|---|---|
| `ATLASCLOUD_API_KEY` | Yes (for `atlascloud` provider) | API key for the [Atlas Cloud](https://www.atlascloud.ai) gateway. `ATLAS_CLOUD_API_KEY` is accepted as an alternative; whichever is set first in that order wins |
| `ATLASCLOUD_BASE_URL` | No | Override the API base URL. `ATLAS_CLOUD_BASE_URL` is accepted as an alternative |

Model names are namespaced by publisher (e.g. `qwen/qwen3.5-35b-a3b`, the default). An override set through `LLMWIKI_MODEL` must be a model Atlas Cloud lists as supporting tools, because `compile` extracts concepts through a tool call.

---

## Voyage (embeddings)

| Variable | Required | Description |
Expand Down
40 changes: 39 additions & 1 deletion docs/configuration/providers.mdx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: "LLM Provider Setup and Configuration Guide for llmwiki"
sidebarTitle: "Providers"
description: "Configure llmwiki to use Anthropic, OpenAI-compatible endpoints, Ollama, GitHub Copilot, or the Claude Agent SDK provider for local login."
description: "Configure llmwiki to use Anthropic, OpenAI-compatible endpoints, Ollama, GitHub Copilot, Atlas Cloud, or the Claude Agent SDK provider for local login."
---

llmwiki is provider-portable. Whether you have an Anthropic API key, a GitHub Copilot subscription, a locally-running Ollama server, or just a Claude Code login, you can point llmwiki at the right backend with a handful of environment variables - no config files required for most setups. Choose the provider that matches your existing credentials and infrastructure.
Expand Down Expand Up @@ -220,6 +220,44 @@ export OPENAI_EMBEDDINGS_API_KEY=<your-key>
`GITHUB_TOKEN` must be a GitHub OAuth token obtained via `gh auth token` after running `gh auth refresh --scopes copilot`. Classic personal access tokens (PATs) are rejected by the Copilot API and will not work.
</Warning>

</Tab>
<Tab title="Atlas Cloud">

The `atlascloud` provider routes calls through [Atlas Cloud](https://www.atlascloud.ai), a hosted gateway exposing an OpenAI-compatible API across models from several vendors. Model names are namespaced by publisher (for example `qwen/qwen3.5-35b-a3b`).

`LLMWIKI_PROVIDER` also accepts `atlas-cloud` and `atlas` as aliases.

**Variables**

| Variable | Required | Description |
|---|---|---|
| `LLMWIKI_PROVIDER` | Yes | Set to `atlascloud` (or `atlas-cloud`, `atlas`) |
| `ATLASCLOUD_API_KEY` | Yes | Your Atlas Cloud API key. `ATLAS_CLOUD_API_KEY` is accepted as an alternative |
| `LLMWIKI_MODEL` | No | Model override. Must be a namespaced id such as `deepseek-ai/deepseek-v3.2` |
| `ATLASCLOUD_BASE_URL` | No | Override the API base URL. `ATLAS_CLOUD_BASE_URL` is accepted as an alternative |

**Setup**

```bash
export LLMWIKI_PROVIDER=atlascloud
export ATLASCLOUD_API_KEY=<your-key>
llmwiki compile
```

**Choosing a model**

`llmwiki compile` extracts concepts through a tool call and requires the model to return tool arguments, so an override must be a model Atlas Cloud lists as supporting tools. The default, `qwen/qwen3.5-35b-a3b`, does. A model without tool support fails on the first extraction request rather than degrading.

**Embeddings**

Atlas Cloud embeddings are not wired up in llmwiki, so the provider fails closed rather than inheriting OpenAI's embedding semantics. To keep Atlas Cloud for chat while using embeddings, route them to another backend with `LLMWIKI_EMBEDDING_PROVIDER` - see [Environment Variables](/configuration/environment-variables):

```bash
export LLMWIKI_PROVIDER=atlascloud
export LLMWIKI_EMBEDDING_PROVIDER=openai
export OPENAI_EMBEDDINGS_API_KEY=<your-key>
```

</Tab>
</Tabs>

Expand Down
2 changes: 1 addition & 1 deletion src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,7 @@ program
.option("--no-open", "Skip the viewer handoff after a successful compile")
.option(
"--provider <name>",
"Override LLMWIKI_PROVIDER for this run only (e.g. anthropic, openai, ollama)",
"Override LLMWIKI_PROVIDER for this run only (e.g. anthropic, openai, ollama, atlascloud)",
)
.option(
"--lang <code>",
Expand Down
9 changes: 7 additions & 2 deletions src/eval/citation-support.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,12 @@ import path from "path";
import { collectAllPages } from "../linter/rules.js";
import { parseFrontmatter, extractClaimCitations, splitProseParagraphs } from "../utils/markdown.js";
import { callClaude } from "../utils/llm.js";
import { SOURCES_DIR, DEFAULT_PROVIDER, PROVIDER_MODELS } from "../utils/constants.js";
import {
SOURCES_DIR,
DEFAULT_PROVIDER,
PROVIDER_MODELS,
normalizeProviderName,
} from "../utils/constants.js";
import { resolveSourceFile } from "./source-path.js";
import type { LLMTool } from "../utils/provider.js";
import type { CitationJudgement, CitationSupportResult } from "./types.js";
Expand Down Expand Up @@ -212,7 +217,7 @@ async function appendCachedJudgement(root: string, judgement: CitationJudgement)

/** Resolve the current model identifier for recording in judgements. */
function resolveModel(): string {
const provider = process.env.LLMWIKI_PROVIDER ?? DEFAULT_PROVIDER;
const provider = normalizeProviderName(process.env.LLMWIKI_PROVIDER ?? DEFAULT_PROVIDER);
return process.env.LLMWIKI_MODEL ?? PROVIDER_MODELS[provider] ?? provider;
}

Expand Down
54 changes: 54 additions & 0 deletions src/providers/atlascloud.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/**
* Atlas Cloud LLM provider implementation.
*
* Atlas Cloud exposes an OpenAI-compatible chat completions API. Embeddings are
* not wired until a compatible embedding model is verified for llmwiki.
*/

import { OpenAIProvider } from "./openai.js";
import {
ATLASCLOUD_API_KEY_ENV_VARS,
ATLASCLOUD_BASE_URL,
ATLASCLOUD_BASE_URL_ENV_VARS,
} from "../utils/constants.js";

function readFirstEnv(names: readonly string[]): string | undefined {
for (const name of names) {
const value = process.env[name]?.trim();
if (value) return value;
}
return undefined;
}

/** Resolve Atlas Cloud API key from the supported env-var aliases. */
export function resolveAtlasCloudApiKeyFromEnv(): string | undefined {
return readFirstEnv(ATLASCLOUD_API_KEY_ENV_VARS);
}

/** Resolve Atlas Cloud OpenAI-compatible base URL from env, or use the default. */
export function resolveAtlasCloudBaseURLFromEnv(): string {
return readFirstEnv(ATLASCLOUD_BASE_URL_ENV_VARS) ?? ATLASCLOUD_BASE_URL;
}

/** Atlas Cloud-backed LLM provider using the OpenAI-compatible endpoint. */
export class AtlasCloudProvider extends OpenAIProvider {
constructor(model: string, apiKey: string, baseURL = ATLASCLOUD_BASE_URL) {
super(model, { baseURL, apiKey });
}

/** Atlas Cloud embedding support is unverified; fail closed instead of inheriting OpenAI semantics. */
override async embed(_text: string): Promise<number[]> {
throw new Error(
"Atlas Cloud provider does not support embeddings in llmwiki yet.\n" +
" For semantic search, route embeddings to another backend and keep this\n" +
" provider for chat: export LLMWIKI_EMBEDDING_PROVIDER=openai (or anthropic,\n" +
" claude-agent, ollama).",
);
}

/** Atlas Cloud batch embeddings are unsupported for the same reason as single embeddings. */
override async embedBatch(_texts: string[]): Promise<number[][]> {
await this.embed("");
return [];
}
}
7 changes: 4 additions & 3 deletions src/providers/copilot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,10 @@ export class CopilotProvider extends OpenAIProvider {
override async embed(_text: string): Promise<number[]> {
throw new Error(
"GitHub Copilot does not support embeddings.\n" +
" For semantic search (llmwiki query), switch to the OpenAI provider:\n" +
" export LLMWIKI_PROVIDER=openai\n" +
" export OPENAI_API_KEY=sk-...",
" For semantic search (llmwiki query), route embeddings to another backend\n" +
" and keep Copilot for chat:\n" +
" export LLMWIKI_EMBEDDING_PROVIDER=openai\n" +
" export OPENAI_EMBEDDINGS_API_KEY=sk-...",
);
}

Expand Down
4 changes: 3 additions & 1 deletion src/providers/minimax.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ export class MiniMaxProvider extends OpenAIProvider {
override async embed(_text: string): Promise<number[]> {
throw new Error(
"MiniMax provider does not support embeddings in llmwiki yet.\n" +
" For semantic search, use LLMWIKI_PROVIDER=openai, anthropic, claude-agent, or ollama.",
" For semantic search, route embeddings to another backend and keep this\n" +
" provider for chat: export LLMWIKI_EMBEDDING_PROVIDER=openai (or anthropic,\n" +
" claude-agent, ollama).",
);
}

Expand Down
39 changes: 39 additions & 0 deletions src/utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,27 @@ export const RETRY_MULTIPLIER = 4;
/** Default provider when LLMWIKI_PROVIDER is not set. */
export const DEFAULT_PROVIDER = "anthropic";

/** Provider names accepted by LLMWIKI_PROVIDER, including aliases. */
export const SUPPORTED_PROVIDER_INPUTS = [
"anthropic",
"claude-agent",
"openai",
"ollama",
"minimax",
"copilot",
"atlascloud",
"atlas-cloud",
"atlas",
] as const;

/** Normalize accepted LLMWIKI_PROVIDER aliases to provider implementation names. */
export function normalizeProviderName(providerName: string): string {
if (providerName === "atlas-cloud" || providerName === "atlas") {
return "atlascloud";
}
return providerName;
}

/** Default model per provider. */
export const PROVIDER_MODELS: Record<string, string> = {
anthropic: "claude-sonnet-4-6",
Expand All @@ -74,6 +95,15 @@ export const PROVIDER_MODELS: Record<string, string> = {
ollama: "llama3.1",
minimax: "MiniMax-M2.7",
copilot: "gpt-4o",
// Atlas Cloud's catalogue marks tool support per model in `supported_features`,
// and `qwen/qwen3.5-flash` carries no such entry while 35 of its 136 models
// advertise `["json_mode","structured_outputs","tools"]`. Compile's extraction
// pass goes through `toolCall` with `tool_choice: "required"`, so a default
// that cannot return tool calls fails on the first request rather than
// degrading — the provider would look installed and not work. This is the
// smallest catalogue model that does advertise tools, keeping the cheap-and-fast
// intent of the original default.
atlascloud: "qwen/qwen3.5-35b-a3b",
};

/** Default Ollama API base URL. */
Expand All @@ -82,6 +112,15 @@ export const OLLAMA_DEFAULT_HOST = "http://localhost:11434/v1";
/** GitHub Copilot API base URL (OpenAI-compatible, requires OAuth token). */
export const COPILOT_BASE_URL = "https://api.githubcopilot.com";

/** Atlas Cloud OpenAI-compatible API base URL. */
export const ATLASCLOUD_BASE_URL = "https://api.atlascloud.ai/v1";

/** Atlas Cloud API key env vars, checked in order. */
export const ATLASCLOUD_API_KEY_ENV_VARS = ["ATLASCLOUD_API_KEY", "ATLAS_CLOUD_API_KEY"] as const;

/** Atlas Cloud base URL env vars, checked in order. */
export const ATLASCLOUD_BASE_URL_ENV_VARS = ["ATLASCLOUD_BASE_URL", "ATLAS_CLOUD_BASE_URL"] as const;

/**
* Default request timeout for cloud OpenAI-compatible providers (10 minutes).
* Matches the OpenAI SDK's own default; called out here so it's explicit.
Expand Down
44 changes: 31 additions & 13 deletions src/utils/provider-guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,12 @@
* surface fired the guard.
*/

import { DEFAULT_PROVIDER } from "./constants.js";
import {
ATLASCLOUD_API_KEY_ENV_VARS,
DEFAULT_PROVIDER,
SUPPORTED_PROVIDER_INPUTS,
normalizeProviderName,
} from "./constants.js";
import { resolveAnthropicAuthFromEnv } from "./claude-settings.js";
import { findEmbeddingProviderProblem } from "./embedding-provider.js";

Expand All @@ -39,16 +44,27 @@ export class UnknownProviderError extends Error {
}
}

/** Map of provider name to the env var that satisfies it. Null = no key needed. */
const PROVIDER_KEY_VARS: Record<string, string | null> = {
/**
* Map of provider name to the env var(s) that satisfy it. Null = no key needed.
*
* A LIST means any one of them satisfies the check, in the order a message
* should name them.
*/
const PROVIDER_KEY_VARS: Record<string, string | readonly string[] | null> = {
anthropic: "ANTHROPIC_API_KEY",
"claude-agent": null,
openai: "OPENAI_API_KEY",
ollama: null,
minimax: "MINIMAX_API_KEY",
copilot: "GITHUB_TOKEN",
atlascloud: ATLASCLOUD_API_KEY_ENV_VARS,
};

/** One-or-many credential names as a list, so the check has a single shape. */
function normalizeKeyVars(keyVars: string | readonly string[]): string[] {
return typeof keyVars === "string" ? [keyVars] : [...keyVars];
}

/**
* Throw if LLMWIKI_EMBEDDING_PROVIDER names a backend that cannot serve
* embeddings, or one whose credential is missing (issue #154).
Expand Down Expand Up @@ -80,7 +96,7 @@ function ensureEmbeddingProviderAvailable(): void {
*/
export function ensureProviderAvailable(): void {
ensureEmbeddingProviderAvailable();
const provider = process.env.LLMWIKI_PROVIDER ?? DEFAULT_PROVIDER;
const provider = normalizeProviderName(process.env.LLMWIKI_PROVIDER ?? DEFAULT_PROVIDER);

if (provider === "anthropic") {
const auth = resolveAnthropicAuthFromEnv();
Expand All @@ -97,20 +113,22 @@ export function ensureProviderAvailable(): void {

const keyVar = PROVIDER_KEY_VARS[provider];
if (keyVar === undefined) {
const supported = Object.keys(PROVIDER_KEY_VARS);
const supported = [...SUPPORTED_PROVIDER_INPUTS];
throw new UnknownProviderError(
provider,
supported,
`Unknown provider "${provider}".\n` + ` Supported: ${supported.join(", ")}`,
);
}

if (keyVar && !process.env[keyVar]) {
throw new ProviderUnavailableError(
provider,
[keyVar],
`${keyVar} environment variable is required for the "${provider}" provider.\n` +
` Set it with: export ${keyVar}=<your-key>`,
);
}
if (!keyVar) return;

const keyVars = normalizeKeyVars(keyVar);
if (keyVars.some((name) => Boolean(process.env[name]?.trim()))) return;
throw new ProviderUnavailableError(
provider,
keyVars,
`${keyVars.join(" or ")} environment variable is required for the "${provider}" provider.\n` +
` Set one with: export ${keyVars[0]}=<your-key>`,
);
}
Loading
Loading