feat(models): add OPENAI_BASE_URL env override for OpenAI-compatible endpoints
This commit is contained in:
parent
9688454a30
commit
7cb2622688
@ -33,6 +33,37 @@ Moltbot ships with the pi‑ai catalog. These providers require **no**
|
||||
}
|
||||
```
|
||||
|
||||
#### OpenAI-compatible base URL override
|
||||
|
||||
To redirect `openai/*` requests to an OpenAI-compatible endpoint (LiteLLM, vLLM,
|
||||
LM Studio, etc.) without changing model refs:
|
||||
|
||||
```bash
|
||||
export OPENAI_BASE_URL="http://localhost:8000/v1"
|
||||
export OPENAI_API_KEY="your-key-or-dummy"
|
||||
```
|
||||
|
||||
This keeps the built-in OpenAI model catalog but routes requests to your custom
|
||||
endpoint. The env var takes effect on gateway startup (no config file needed).
|
||||
|
||||
Alternatively, configure via `models.providers.openai.baseUrl` in your config:
|
||||
|
||||
```json5
|
||||
{
|
||||
models: {
|
||||
providers: {
|
||||
openai: {
|
||||
baseUrl: "http://localhost:8000/v1",
|
||||
models: [] // empty = keep built-in catalog
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
For a custom provider id instead (e.g., `myproxy/<model>`), see [Providers via
|
||||
models.providers](#providers-via-modelsproviders-custombase-url) below.
|
||||
|
||||
### Anthropic
|
||||
|
||||
- Provider: `anthropic`
|
||||
@ -122,7 +153,15 @@ Moltbot ships with the pi‑ai catalog. These providers require **no**
|
||||
## Providers via `models.providers` (custom/base URL)
|
||||
|
||||
Use `models.providers` (or `models.json`) to add **custom** providers or
|
||||
OpenAI/Anthropic‑compatible proxies.
|
||||
OpenAI/Anthropic-compatible proxies.
|
||||
|
||||
Two patterns for OpenAI-compatible endpoints:
|
||||
|
||||
1. **Drop-in override** (keep `openai/*` refs): set `OPENAI_BASE_URL` or
|
||||
`models.providers.openai.baseUrl` with `models: []`. See [OpenAI section
|
||||
above](#openai).
|
||||
2. **Custom provider id** (e.g., `myproxy/<model>`): define a new provider entry
|
||||
with `baseUrl`, `apiKey`, and `api`. Examples below.
|
||||
|
||||
### Moonshot AI (Kimi)
|
||||
|
||||
@ -308,6 +347,20 @@ Notes:
|
||||
- `maxTokens: 8192`
|
||||
- Recommended: set explicit values that match your proxy/model limits.
|
||||
|
||||
## Other OpenAI-compatible surfaces
|
||||
|
||||
Beyond the main chat model, Moltbot uses OpenAI-compatible APIs for other
|
||||
features. Each has its own endpoint configuration:
|
||||
|
||||
- **Memory embeddings**: `agents.defaults.memorySearch.remote.baseUrl` or falls
|
||||
back to `models.providers.openai.baseUrl`. See [/concepts/memory](/concepts/memory).
|
||||
- **Media tools** (audio transcription, vision): per-model `baseUrl` in
|
||||
`tools.media.audio.models[].baseUrl` or `tools.media.image.models[].baseUrl`.
|
||||
See [/gateway/configuration](/gateway/configuration#tools-media).
|
||||
- **TTS (text-to-speech)**: `OPENAI_TTS_BASE_URL` env var (separate from
|
||||
`OPENAI_BASE_URL` because most OpenAI-compatible proxies do not implement
|
||||
`/audio/speech`).
|
||||
|
||||
## CLI examples
|
||||
|
||||
```bash
|
||||
|
||||
@ -2295,6 +2295,30 @@ Moltbot uses the **pi-coding-agent** model catalog. You can add custom providers
|
||||
Moltbot config under `models.providers`.
|
||||
Provider-by-provider overview + examples: [/concepts/model-providers](/concepts/model-providers).
|
||||
|
||||
**Quick: OpenAI-compatible base URL override**
|
||||
|
||||
To redirect all `openai/*` requests to an OpenAI-compatible endpoint without
|
||||
changing model refs, set the `OPENAI_BASE_URL` env var:
|
||||
|
||||
```bash
|
||||
export OPENAI_BASE_URL="http://localhost:8000/v1"
|
||||
export OPENAI_API_KEY="your-key-or-dummy"
|
||||
```
|
||||
|
||||
Or configure via config (keeps built-in model catalog):
|
||||
|
||||
```json5
|
||||
{
|
||||
models: {
|
||||
providers: {
|
||||
openai: { baseUrl: "http://localhost:8000/v1", models: [] }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Custom provider setup**
|
||||
|
||||
When `models.providers` is present, Moltbot writes/merges a `models.json` into
|
||||
`~/.clawdbot/agents/<agentId>/agent/` on startup:
|
||||
- default behavior: **merge** (keeps existing providers, overrides on name)
|
||||
|
||||
153
src/agents/models-config.openai-base-url-env-override.test.ts
Normal file
153
src/agents/models-config.openai-base-url-env-override.test.ts
Normal file
@ -0,0 +1,153 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { withTempHome as withTempHomeBase } from "../../test/helpers/temp-home.js";
|
||||
import type { MoltbotConfig } from "../config/config.js";
|
||||
import { resolveImplicitOpenAiProvider } from "./models-config.providers.js";
|
||||
|
||||
async function withTempHome<T>(fn: (home: string) => Promise<T>): Promise<T> {
|
||||
return withTempHomeBase(fn, { prefix: "moltbot-models-openai-" });
|
||||
}
|
||||
|
||||
describe("models-config OPENAI_BASE_URL env override", () => {
|
||||
let previousHome: string | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
previousHome = process.env.HOME;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.env.HOME = previousHome;
|
||||
});
|
||||
|
||||
it("injects openai provider with baseUrl when OPENAI_BASE_URL is set", async () => {
|
||||
await withTempHome(async () => {
|
||||
vi.resetModules();
|
||||
const prevBaseUrl = process.env.OPENAI_BASE_URL;
|
||||
process.env.OPENAI_BASE_URL = "http://localhost:8000/v1";
|
||||
try {
|
||||
const { ensureMoltbotModelsJson } = await import("./models-config.js");
|
||||
const { resolveMoltbotAgentDir } = await import("./agent-paths.js");
|
||||
|
||||
const cfg: MoltbotConfig = {};
|
||||
|
||||
await ensureMoltbotModelsJson(cfg);
|
||||
|
||||
const modelPath = path.join(resolveMoltbotAgentDir(), "models.json");
|
||||
const raw = await fs.readFile(modelPath, "utf8");
|
||||
const parsed = JSON.parse(raw) as {
|
||||
providers: Record<string, { baseUrl?: string; models?: unknown[] }>;
|
||||
};
|
||||
|
||||
expect(parsed.providers.openai).toBeDefined();
|
||||
expect(parsed.providers.openai?.baseUrl).toBe("http://localhost:8000/v1");
|
||||
// models should be empty to keep built-in catalog
|
||||
expect(parsed.providers.openai?.models).toEqual([]);
|
||||
} finally {
|
||||
if (prevBaseUrl === undefined) delete process.env.OPENAI_BASE_URL;
|
||||
else process.env.OPENAI_BASE_URL = prevBaseUrl;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("does not inject openai provider when OPENAI_BASE_URL is not set", async () => {
|
||||
await withTempHome(async () => {
|
||||
vi.resetModules();
|
||||
const prevBaseUrl = process.env.OPENAI_BASE_URL;
|
||||
delete process.env.OPENAI_BASE_URL;
|
||||
try {
|
||||
const { ensureMoltbotModelsJson } = await import("./models-config.js");
|
||||
const { resolveMoltbotAgentDir } = await import("./agent-paths.js");
|
||||
|
||||
const cfg: MoltbotConfig = {};
|
||||
|
||||
const result = await ensureMoltbotModelsJson(cfg);
|
||||
|
||||
// With no implicit providers configured, no models.json should be written
|
||||
// (unless other implicit providers like ollama are detected)
|
||||
if (result.wrote) {
|
||||
const modelPath = path.join(resolveMoltbotAgentDir(), "models.json");
|
||||
const raw = await fs.readFile(modelPath, "utf8");
|
||||
const parsed = JSON.parse(raw) as {
|
||||
providers: Record<string, { baseUrl?: string }>;
|
||||
};
|
||||
// openai should not be present from env override
|
||||
expect(parsed.providers.openai?.baseUrl).toBeUndefined();
|
||||
}
|
||||
} finally {
|
||||
if (prevBaseUrl !== undefined) process.env.OPENAI_BASE_URL = prevBaseUrl;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("explicit models.providers.openai.baseUrl takes precedence over env var", async () => {
|
||||
await withTempHome(async () => {
|
||||
vi.resetModules();
|
||||
const prevBaseUrl = process.env.OPENAI_BASE_URL;
|
||||
process.env.OPENAI_BASE_URL = "http://env-override:8000/v1";
|
||||
try {
|
||||
const { ensureMoltbotModelsJson } = await import("./models-config.js");
|
||||
const { resolveMoltbotAgentDir } = await import("./agent-paths.js");
|
||||
|
||||
const cfg: MoltbotConfig = {
|
||||
models: {
|
||||
providers: {
|
||||
openai: {
|
||||
baseUrl: "http://explicit-config:9000/v1",
|
||||
models: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
await ensureMoltbotModelsJson(cfg);
|
||||
|
||||
const modelPath = path.join(resolveMoltbotAgentDir(), "models.json");
|
||||
const raw = await fs.readFile(modelPath, "utf8");
|
||||
const parsed = JSON.parse(raw) as {
|
||||
providers: Record<string, { baseUrl?: string }>;
|
||||
};
|
||||
|
||||
// Explicit config should take precedence
|
||||
expect(parsed.providers.openai?.baseUrl).toBe("http://explicit-config:9000/v1");
|
||||
} finally {
|
||||
if (prevBaseUrl === undefined) delete process.env.OPENAI_BASE_URL;
|
||||
else process.env.OPENAI_BASE_URL = prevBaseUrl;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveImplicitOpenAiProvider", () => {
|
||||
it("returns null when OPENAI_BASE_URL is not set", () => {
|
||||
const result = resolveImplicitOpenAiProvider({ env: {} });
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("returns provider config with baseUrl when OPENAI_BASE_URL is set", () => {
|
||||
const result = resolveImplicitOpenAiProvider({
|
||||
env: { OPENAI_BASE_URL: "http://test:8000/v1" },
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
baseUrl: "http://test:8000/v1",
|
||||
models: [],
|
||||
});
|
||||
});
|
||||
|
||||
it("trims whitespace from OPENAI_BASE_URL", () => {
|
||||
const result = resolveImplicitOpenAiProvider({
|
||||
env: { OPENAI_BASE_URL: " http://test:8000/v1 " },
|
||||
});
|
||||
|
||||
expect(result?.baseUrl).toBe("http://test:8000/v1");
|
||||
});
|
||||
|
||||
it("returns null for empty OPENAI_BASE_URL", () => {
|
||||
const result = resolveImplicitOpenAiProvider({
|
||||
env: { OPENAI_BASE_URL: " " },
|
||||
});
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
@ -421,6 +421,28 @@ export async function resolveImplicitProviders(params: {
|
||||
return providers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve an implicit OpenAI provider override when OPENAI_BASE_URL is set.
|
||||
* This allows users to redirect all `openai/*` model requests to an OpenAI-compatible
|
||||
* endpoint (e.g., LiteLLM, vLLM, LM Studio) without changing model refs.
|
||||
*
|
||||
* Returns `{ baseUrl, models: [] }` so that the built-in OpenAI model catalog is preserved,
|
||||
* but requests go to the custom endpoint.
|
||||
*/
|
||||
export function resolveImplicitOpenAiProvider(params: {
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): ProviderConfig | null {
|
||||
const env = params.env ?? process.env;
|
||||
const baseUrl = env.OPENAI_BASE_URL?.trim();
|
||||
if (!baseUrl) return null;
|
||||
|
||||
// Only override baseUrl; keep the built-in model catalog by setting models: []
|
||||
return {
|
||||
baseUrl,
|
||||
models: [],
|
||||
} satisfies ProviderConfig;
|
||||
}
|
||||
|
||||
export async function resolveImplicitCopilotProvider(params: {
|
||||
agentDir: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
|
||||
@ -8,6 +8,7 @@ import {
|
||||
type ProviderConfig,
|
||||
resolveImplicitBedrockProvider,
|
||||
resolveImplicitCopilotProvider,
|
||||
resolveImplicitOpenAiProvider,
|
||||
resolveImplicitProviders,
|
||||
} from "./models-config.providers.js";
|
||||
|
||||
@ -81,8 +82,18 @@ export async function ensureMoltbotModelsJson(
|
||||
|
||||
const explicitProviders = (cfg.models?.providers ?? {}) as Record<string, ProviderConfig>;
|
||||
const implicitProviders = await resolveImplicitProviders({ agentDir });
|
||||
|
||||
// Check for OPENAI_BASE_URL env var to override the built-in openai provider endpoint.
|
||||
// This allows drop-in replacement with OpenAI-compatible APIs (LiteLLM, vLLM, etc.)
|
||||
// without changing model refs from openai/* to a custom provider.
|
||||
const implicitOpenAi = resolveImplicitOpenAiProvider({});
|
||||
const allImplicitProviders: Record<string, ProviderConfig> = {
|
||||
...implicitProviders,
|
||||
...(implicitOpenAi ? { openai: implicitOpenAi } : {}),
|
||||
};
|
||||
|
||||
const providers: Record<string, ProviderConfig> = mergeProviders({
|
||||
implicit: implicitProviders,
|
||||
implicit: allImplicitProviders,
|
||||
explicit: explicitProviders,
|
||||
});
|
||||
const implicitBedrock = await resolveImplicitBedrockProvider({ agentDir, config: cfg });
|
||||
|
||||
Loading…
Reference in New Issue
Block a user