This commit is contained in:
jason 2026-01-29 11:33:56 -05:00 committed by GitHub
commit 43179d1bd3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 551 additions and 1 deletions

View File

@ -124,6 +124,32 @@ Moltbot ships with the piai catalog. These providers require **no**
Use `models.providers` (or `models.json`) to add **custom** providers or
OpenAI/Anthropiccompatible proxies.
### Modelverse
Modelverse (UCloud) provides an OpenAI-compatible API for multiple model families.
- Provider: `modelverse`
- Auth: `MODELVERSE_API_KEY`
- Base URL: `https://api.modelverse.cn/v1`
- Example model: `modelverse/gpt-5.2`
- CLI: `clawdbot onboard --auth-choice modelverse-api-key`
Model refs:
- `modelverse/gpt-5.2`
- `modelverse/claude-opus-4-5-20251101`
- `modelverse/claude-sonnet-4-5-20250929`
- `modelverse/deepseek-ai/DeepSeek-V3.2`
- `modelverse/deepseek-ai/DeepSeek-R1`
- `modelverse/deepseek-ai/DeepSeek-V3-0324`
- `modelverse/openai/gpt-4o`
- `modelverse/zai-org/glm-4.7`
- `modelverse/gemini-3-flash-preview`
- `modelverse/gemini-3-pro-preview`
- `modelverse/gemini-2.5-flash`
- `modelverse/gemini-2.5-pro`
See [/providers/modelverse](/providers/modelverse).
### Moonshot AI (Kimi)
Moonshot uses OpenAI-compatible endpoints, so configure it as a custom provider:

View File

@ -44,6 +44,7 @@ See [Venice AI](/providers/venice).
- [Z.AI](/providers/zai)
- [GLM models](/providers/glm)
- [MiniMax](/providers/minimax)
- [Modelverse (OpenAI-compatible)](/providers/modelverse)
- [Venius (Venice AI, privacy-focused)](/providers/venice)
- [Ollama (local models)](/providers/ollama)

View File

@ -41,6 +41,7 @@ See [Venice AI](/providers/venice).
- [Z.AI](/providers/zai)
- [GLM models](/providers/glm)
- [MiniMax](/providers/minimax)
- [Modelverse (OpenAI-compatible)](/providers/modelverse)
- [Venius (Venice AI)](/providers/venice)
- [Amazon Bedrock](/bedrock)

View File

@ -0,0 +1,66 @@
---
summary: "Use Modelverse (UCloud) as an OpenAI-compatible model provider in Clawdbot"
read_when:
- You want to use Modelverse models in Clawdbot
- You need the base URL, API key setup, and model refs for Modelverse
---
# Modelverse
Modelverse (UCloud) exposes an **OpenAI-compatible** API that can route to multiple model families.
## Authenticate
Get an API key:
https://console.ucloud-global.com/modelverse/experience/api-keys
Then run:
```bash
clawdbot onboard --auth-choice modelverse-api-key
```
This stores the key in Clawdbot's auth profiles and writes a `models.providers.modelverse` entry.
## Model refs
Use `modelverse/<modelId>`:
- `modelverse/gpt-5.2`
- `modelverse/claude-opus-4-5-20251101`
- `modelverse/claude-sonnet-4-5-20250929`
- `modelverse/deepseek-ai/DeepSeek-V3.2`
- `modelverse/deepseek-ai/DeepSeek-R1`
- `modelverse/deepseek-ai/DeepSeek-V3-0324`
- `modelverse/openai/gpt-4o`
- `modelverse/zai-org/glm-4.7`
- `modelverse/gemini-3-flash-preview`
- `modelverse/gemini-3-pro-preview`
- `modelverse/gemini-2.5-flash`
- `modelverse/gemini-2.5-pro`
Switch models with:
```bash
clawdbot models set modelverse/gpt-5.2
```
## Config snippet (manual)
```json5
{
env: { MODELVERSE_API_KEY: "sk-..." },
agents: { defaults: { model: { primary: "modelverse/gpt-5.2" } } },
models: {
mode: "merge",
providers: {
modelverse: {
baseUrl: "https://api.modelverse.cn/v1",
apiKey: "${MODELVERSE_API_KEY}",
api: "openai-completions",
models: [{ id: "gpt-5.2", name: "GPT-5.2" }]
}
}
}
}
```

View File

@ -271,6 +271,7 @@ export function resolveEnvApiKey(provider: string): EnvApiKeyResult | null {
const envMap: Record<string, string> = {
openai: "OPENAI_API_KEY",
modelverse: "MODELVERSE_API_KEY",
google: "GEMINI_API_KEY",
groq: "GROQ_API_KEY",
deepgram: "DEEPGRAM_API_KEY",

View File

@ -0,0 +1,37 @@
import { afterEach, describe, expect, it } from "vitest";
import { resolveImplicitProviders } from "./models-config.providers.js";
import { mkdtempSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
describe("Modelverse provider", () => {
const previousKey = process.env.MODELVERSE_API_KEY;
afterEach(() => {
if (previousKey === undefined) {
delete process.env.MODELVERSE_API_KEY;
} else {
process.env.MODELVERSE_API_KEY = previousKey;
}
});
it("should not include modelverse when no API key is configured", async () => {
const agentDir = mkdtempSync(join(tmpdir(), "clawd-test-"));
const providers = await resolveImplicitProviders({ agentDir });
expect(providers?.modelverse).toBeUndefined();
});
it("should include modelverse when MODELVERSE_API_KEY is configured", async () => {
process.env.MODELVERSE_API_KEY = "sk-modelverse-test";
const agentDir = mkdtempSync(join(tmpdir(), "clawd-test-"));
const providers = await resolveImplicitProviders({ agentDir });
expect(providers?.modelverse).toBeDefined();
expect(providers.modelverse).toMatchObject({
baseUrl: "https://api.modelverse.cn/v1",
api: "openai-completions",
});
expect(providers.modelverse.models.some((model) => model.id === "gpt-5.2")).toBe(true);
});
});

View File

@ -13,6 +13,11 @@ import {
SYNTHETIC_MODEL_CATALOG,
} from "./synthetic-models.js";
import { discoverVeniceModels, VENICE_BASE_URL } from "./venice-models.js";
import {
buildModelverseModelDefinition,
MODELVERSE_BASE_URL,
MODELVERSE_MODEL_CATALOG,
} from "./modelverse-models.js";
type ModelsConfig = NonNullable<MoltbotConfig["models"]>;
export type ProviderConfig = NonNullable<ModelsConfig["providers"]>[string];
@ -359,6 +364,14 @@ async function buildOllamaProvider(): Promise<ProviderConfig> {
};
}
function buildModelverseProvider(): ProviderConfig {
return {
baseUrl: MODELVERSE_BASE_URL,
api: "openai-completions",
models: MODELVERSE_MODEL_CATALOG.map(buildModelverseModelDefinition),
};
}
export async function resolveImplicitProviders(params: {
agentDir: string;
}): Promise<ModelsConfig["providers"]> {
@ -402,6 +415,13 @@ export async function resolveImplicitProviders(params: {
providers.venice = { ...(await buildVeniceProvider()), apiKey: veniceKey };
}
const modelverseKey =
resolveEnvApiKeyVarName("modelverse") ??
resolveApiKeyFromProfiles({ provider: "modelverse", store: authStore });
if (modelverseKey) {
providers.modelverse = { ...buildModelverseProvider(), apiKey: modelverseKey };
}
const qwenProfiles = listProfilesForProvider(authStore, "qwen-portal");
if (qwenProfiles.length > 0) {
providers["qwen-portal"] = {

View File

@ -0,0 +1,132 @@
import type { ModelDefinitionConfig } from "../config/types.js";
export const MODELVERSE_BASE_URL = "https://api.modelverse.cn/v1";
export const MODELVERSE_DEFAULT_MODEL_ID = "gpt-5.2";
export const MODELVERSE_DEFAULT_MODEL_REF = `modelverse/${MODELVERSE_DEFAULT_MODEL_ID}`;
export const MODELVERSE_DEFAULT_COST = {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
};
// Modelverse is an OpenAI-compatible proxy; metadata is best-effort and can be overridden via
// models.providers.modelverse.models in config.
const DEFAULT_CONTEXT_WINDOW = 200_000;
const DEFAULT_MAX_TOKENS = 8192;
export const MODELVERSE_MODEL_CATALOG = [
{
id: MODELVERSE_DEFAULT_MODEL_ID,
name: "GPT-5.2",
reasoning: false,
input: ["text"],
contextWindow: DEFAULT_CONTEXT_WINDOW,
maxTokens: DEFAULT_MAX_TOKENS,
},
{
id: "claude-opus-4-5-20251101",
name: "Claude Opus 4.5 (20251101)",
reasoning: false,
input: ["text"],
contextWindow: DEFAULT_CONTEXT_WINDOW,
maxTokens: DEFAULT_MAX_TOKENS,
},
{
id: "claude-sonnet-4-5-20250929",
name: "Claude Sonnet 4.5 (20250929)",
reasoning: false,
input: ["text"],
contextWindow: DEFAULT_CONTEXT_WINDOW,
maxTokens: DEFAULT_MAX_TOKENS,
},
{
id: "deepseek-ai/DeepSeek-V3.2",
name: "DeepSeek V3.2",
reasoning: false,
input: ["text"],
contextWindow: DEFAULT_CONTEXT_WINDOW,
maxTokens: DEFAULT_MAX_TOKENS,
},
{
id: "deepseek-ai/DeepSeek-R1",
name: "DeepSeek R1",
reasoning: false,
input: ["text"],
contextWindow: DEFAULT_CONTEXT_WINDOW,
maxTokens: DEFAULT_MAX_TOKENS,
},
{
id: "deepseek-ai/DeepSeek-V3-0324",
name: "DeepSeek V3 0324",
reasoning: false,
input: ["text"],
contextWindow: DEFAULT_CONTEXT_WINDOW,
maxTokens: DEFAULT_MAX_TOKENS,
},
{
id: "openai/gpt-4o",
name: "GPT-4o",
reasoning: false,
input: ["text"],
contextWindow: DEFAULT_CONTEXT_WINDOW,
maxTokens: DEFAULT_MAX_TOKENS,
},
{
id: "zai-org/glm-4.7",
name: "GLM-4.7",
reasoning: false,
input: ["text"],
contextWindow: DEFAULT_CONTEXT_WINDOW,
maxTokens: DEFAULT_MAX_TOKENS,
},
{
id: "gemini-3-flash-preview",
name: "Gemini 3 Flash Preview",
reasoning: false,
input: ["text"],
contextWindow: DEFAULT_CONTEXT_WINDOW,
maxTokens: DEFAULT_MAX_TOKENS,
},
{
id: "gemini-3-pro-preview",
name: "Gemini 3 Pro Preview",
reasoning: false,
input: ["text"],
contextWindow: DEFAULT_CONTEXT_WINDOW,
maxTokens: DEFAULT_MAX_TOKENS,
},
{
id: "gemini-2.5-flash",
name: "Gemini 2.5 Flash",
reasoning: false,
input: ["text"],
contextWindow: DEFAULT_CONTEXT_WINDOW,
maxTokens: DEFAULT_MAX_TOKENS,
},
{
id: "gemini-2.5-pro",
name: "Gemini 2.5 Pro",
reasoning: false,
input: ["text"],
contextWindow: DEFAULT_CONTEXT_WINDOW,
maxTokens: DEFAULT_MAX_TOKENS,
},
] as const;
export type ModelverseCatalogEntry = (typeof MODELVERSE_MODEL_CATALOG)[number];
export function buildModelverseModelDefinition(
entry: ModelverseCatalogEntry,
): ModelDefinitionConfig {
return {
id: entry.id,
name: entry.name,
reasoning: entry.reasoning,
input: [...entry.input],
cost: MODELVERSE_DEFAULT_COST,
contextWindow: entry.contextWindow,
maxTokens: entry.maxTokens,
};
}

View File

@ -52,7 +52,7 @@ export function registerOnboardCommand(program: Command) {
.option("--mode <mode>", "Wizard mode: local|remote")
.option(
"--auth-choice <choice>",
"Auth: setup-token|token|chutes|openai-codex|openai-api-key|openrouter-api-key|ai-gateway-api-key|moonshot-api-key|kimi-code-api-key|synthetic-api-key|venice-api-key|gemini-api-key|zai-api-key|apiKey|minimax-api|minimax-api-lightning|opencode-zen|skip",
"Auth: setup-token|token|chutes|openai-codex|openai-api-key|modelverse-api-key|openrouter-api-key|ai-gateway-api-key|moonshot-api-key|kimi-code-api-key|synthetic-api-key|venice-api-key|gemini-api-key|zai-api-key|apiKey|minimax-api|minimax-api-lightning|opencode-zen|skip",
)
.option(
"--token-provider <id>",
@ -66,6 +66,7 @@ export function registerOnboardCommand(program: Command) {
.option("--token-expires-in <duration>", "Optional token expiry duration (e.g. 365d, 12h)")
.option("--anthropic-api-key <key>", "Anthropic API key")
.option("--openai-api-key <key>", "OpenAI API key")
.option("--modelverse-api-key <key>", "Modelverse API key")
.option("--openrouter-api-key <key>", "OpenRouter API key")
.option("--ai-gateway-api-key <key>", "Vercel AI Gateway API key")
.option("--moonshot-api-key <key>", "Moonshot API key")
@ -116,6 +117,7 @@ export function registerOnboardCommand(program: Command) {
tokenExpiresIn: opts.tokenExpiresIn as string | undefined,
anthropicApiKey: opts.anthropicApiKey as string | undefined,
openaiApiKey: opts.openaiApiKey as string | undefined,
modelverseApiKey: opts.modelverseApiKey as string | undefined,
openrouterApiKey: opts.openrouterApiKey as string | undefined,
aiGatewayApiKey: opts.aiGatewayApiKey as string | undefined,
moonshotApiKey: opts.moonshotApiKey as string | undefined,

View File

@ -9,6 +9,7 @@ export type AuthChoiceOption = {
export type AuthChoiceGroupId =
| "openai"
| "modelverse"
| "anthropic"
| "google"
| "copilot"
@ -41,6 +42,12 @@ const AUTH_CHOICE_GROUP_DEFS: {
hint: "Codex OAuth + API key",
choices: ["openai-codex", "openai-api-key"],
},
{
value: "modelverse",
label: "Modelverse",
hint: "MODELVERSE-API-KEY",
choices: ["modelverse-api-key"],
},
{
value: "anthropic",
label: "Anthropic",
@ -134,6 +141,11 @@ export function buildAuthChoiceOptions(params: {
});
options.push({ value: "chutes", label: "Chutes (OAuth)" });
options.push({ value: "openai-api-key", label: "OpenAI API key" });
options.push({
value: "modelverse-api-key",
label: "Modelverse API key",
hint: "OpenAI-compatible proxy (api.modelverse.cn)",
});
options.push({ value: "openrouter-api-key", label: "OpenRouter API key" });
options.push({
value: "ai-gateway-api-key",

View File

@ -15,6 +15,8 @@ import {
applyAuthProfileConfig,
applyKimiCodeConfig,
applyKimiCodeProviderConfig,
applyModelverseConfig,
applyModelverseProviderConfig,
applyMoonshotConfig,
applyMoonshotProviderConfig,
applyOpencodeZenConfig,
@ -29,6 +31,7 @@ import {
applyVercelAiGatewayProviderConfig,
applyZaiConfig,
KIMI_CODE_MODEL_REF,
MODELVERSE_DEFAULT_MODEL_REF,
MOONSHOT_DEFAULT_MODEL_REF,
OPENROUTER_DEFAULT_MODEL_REF,
SYNTHETIC_DEFAULT_MODEL_REF,
@ -36,6 +39,7 @@ import {
VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF,
setGeminiApiKey,
setKimiCodeApiKey,
setModelverseApiKey,
setMoonshotApiKey,
setOpencodeZenApiKey,
setOpenrouterApiKey,
@ -69,6 +73,8 @@ export async function applyAuthChoiceApiProviders(
) {
if (params.opts.tokenProvider === "openrouter") {
authChoice = "openrouter-api-key";
} else if (params.opts.tokenProvider === "modelverse") {
authChoice = "modelverse-api-key";
} else if (params.opts.tokenProvider === "vercel-ai-gateway") {
authChoice = "ai-gateway-api-key";
} else if (params.opts.tokenProvider === "moonshot") {
@ -166,6 +172,65 @@ export async function applyAuthChoiceApiProviders(
return { config: nextConfig, agentModelOverride };
}
if (authChoice === "modelverse-api-key") {
let hasCredential = false;
if (!hasCredential && params.opts?.token && params.opts?.tokenProvider === "modelverse") {
await setModelverseApiKey(normalizeApiKeyInput(params.opts.token), params.agentDir);
hasCredential = true;
}
if (!hasCredential) {
await params.prompter.note(
[
"Modelverse provides an OpenAI-compatible API for multiple model families.",
"Get your API key at: https://console.ucloud-global.com/modelverse/experience/api-keys",
].join("\n"),
"Modelverse",
);
}
const envKey = resolveEnvApiKey("modelverse");
if (envKey) {
const useExisting = await params.prompter.confirm({
message: `Use existing MODELVERSE_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`,
initialValue: true,
});
if (useExisting) {
await setModelverseApiKey(envKey.apiKey, params.agentDir);
hasCredential = true;
}
}
if (!hasCredential) {
const key = await params.prompter.text({
message: "Enter Modelverse API key",
validate: validateApiKeyInput,
});
await setModelverseApiKey(normalizeApiKeyInput(String(key)), params.agentDir);
}
nextConfig = applyAuthProfileConfig(nextConfig, {
profileId: "modelverse:default",
provider: "modelverse",
mode: "api_key",
});
{
const applied = await applyDefaultModelChoice({
config: nextConfig,
setDefaultModel: params.setDefaultModel,
defaultModel: MODELVERSE_DEFAULT_MODEL_REF,
applyDefaultConfig: applyModelverseConfig,
applyProviderConfig: applyModelverseProviderConfig,
noteDefault: MODELVERSE_DEFAULT_MODEL_REF,
noteAgentModel,
prompter: params.prompter,
});
nextConfig = applied.config;
agentModelOverride = applied.agentModelOverride ?? agentModelOverride;
}
return { config: nextConfig, agentModelOverride };
}
if (authChoice === "ai-gateway-api-key") {
let hasCredential = false;

View File

@ -10,6 +10,7 @@ const PREFERRED_PROVIDER_BY_AUTH_CHOICE: Partial<Record<AuthChoice, string>> = {
"codex-cli": "openai-codex",
chutes: "chutes",
"openai-api-key": "openai",
"modelverse-api-key": "modelverse",
"openrouter-api-key": "openrouter",
"ai-gateway-api-key": "vercel-ai-gateway",
"moonshot-api-key": "moonshot",

View File

@ -31,6 +31,7 @@ describe("applyAuthChoice", () => {
const previousStateDir = process.env.CLAWDBOT_STATE_DIR;
const previousAgentDir = process.env.CLAWDBOT_AGENT_DIR;
const previousPiAgentDir = process.env.PI_CODING_AGENT_DIR;
const previousModelverseKey = process.env.MODELVERSE_API_KEY;
const previousOpenrouterKey = process.env.OPENROUTER_API_KEY;
const previousAiGatewayKey = process.env.AI_GATEWAY_API_KEY;
const previousSshTty = process.env.SSH_TTY;
@ -59,6 +60,11 @@ describe("applyAuthChoice", () => {
} else {
process.env.PI_CODING_AGENT_DIR = previousPiAgentDir;
}
if (previousModelverseKey === undefined) {
delete process.env.MODELVERSE_API_KEY;
} else {
process.env.MODELVERSE_API_KEY = previousModelverseKey;
}
if (previousOpenrouterKey === undefined) {
delete process.env.OPENROUTER_API_KEY;
} else {
@ -187,6 +193,64 @@ describe("applyAuthChoice", () => {
expect(parsed.profiles?.["synthetic:default"]?.key).toBe("sk-synthetic-test");
});
it("prompts and writes Modelverse API key when selecting modelverse-api-key", async () => {
tempStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-auth-"));
process.env.CLAWDBOT_STATE_DIR = tempStateDir;
process.env.CLAWDBOT_AGENT_DIR = path.join(tempStateDir, "agent");
process.env.PI_CODING_AGENT_DIR = process.env.CLAWDBOT_AGENT_DIR;
const text = vi.fn().mockResolvedValue("sk-modelverse-test");
const select: WizardPrompter["select"] = vi.fn(
async (params) => params.options[0]?.value as never,
);
const multiselect: WizardPrompter["multiselect"] = vi.fn(async () => []);
const prompter: WizardPrompter = {
intro: vi.fn(noopAsync),
outro: vi.fn(noopAsync),
note: vi.fn(noopAsync),
select,
multiselect,
text,
confirm: vi.fn(async () => false),
progress: vi.fn(() => ({ update: noop, stop: noop })),
};
const runtime: RuntimeEnv = {
log: vi.fn(),
error: vi.fn(),
exit: vi.fn((code: number) => {
throw new Error(`exit:${code}`);
}),
};
const result = await applyAuthChoice({
authChoice: "modelverse-api-key",
config: {},
prompter,
runtime,
setDefaultModel: true,
});
expect(text).toHaveBeenCalledWith(
expect.objectContaining({ message: "Enter Modelverse API key" }),
);
expect(result.config.auth?.profiles?.["modelverse:default"]).toMatchObject({
provider: "modelverse",
mode: "api_key",
});
expect(result.config.agents?.defaults?.model?.primary).toBe("modelverse/gpt-5.2");
expect(result.config.models?.providers?.modelverse).toMatchObject({
baseUrl: "https://api.modelverse.cn/v1",
api: "openai-completions",
});
const authProfilePath = authProfilePathFor(requireAgentDir());
const raw = await fs.readFile(authProfilePath, "utf8");
const parsed = JSON.parse(raw) as {
profiles?: Record<string, { key?: string }>;
};
expect(parsed.profiles?.["modelverse:default"]?.key).toBe("sk-modelverse-test");
});
it("sets default model when selecting github-copilot", async () => {
tempStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-auth-"));
process.env.CLAWDBOT_STATE_DIR = tempStateDir;
@ -597,6 +661,10 @@ describe("resolvePreferredProviderForAuthChoice", () => {
expect(resolvePreferredProviderForAuthChoice("qwen-portal")).toBe("qwen-portal");
});
it("maps modelverse-api-key to the provider", () => {
expect(resolvePreferredProviderForAuthChoice("modelverse-api-key")).toBe("modelverse");
});
it("returns undefined for unknown choices", () => {
expect(resolvePreferredProviderForAuthChoice("unknown" as AuthChoice)).toBeUndefined();
});

View File

@ -4,6 +4,12 @@ import {
SYNTHETIC_DEFAULT_MODEL_REF,
SYNTHETIC_MODEL_CATALOG,
} from "../agents/synthetic-models.js";
import {
buildModelverseModelDefinition,
MODELVERSE_BASE_URL,
MODELVERSE_DEFAULT_MODEL_REF,
MODELVERSE_MODEL_CATALOG,
} from "../agents/modelverse-models.js";
import {
buildVeniceModelDefinition,
VENICE_BASE_URL,
@ -202,6 +208,75 @@ export function applyMoonshotConfig(cfg: MoltbotConfig): MoltbotConfig {
};
}
export function applyModelverseProviderConfig(cfg: MoltbotConfig): MoltbotConfig {
const models = { ...cfg.agents?.defaults?.models };
models[MODELVERSE_DEFAULT_MODEL_REF] = {
...models[MODELVERSE_DEFAULT_MODEL_REF],
alias: models[MODELVERSE_DEFAULT_MODEL_REF]?.alias ?? "Modelverse",
};
const providers = { ...cfg.models?.providers };
const existingProvider = providers.modelverse;
const existingModels = Array.isArray(existingProvider?.models) ? existingProvider.models : [];
const modelverseModels = MODELVERSE_MODEL_CATALOG.map(buildModelverseModelDefinition);
const mergedModels = [
...existingModels,
...modelverseModels.filter(
(model) => !existingModels.some((existing) => existing.id === model.id),
),
];
const { apiKey: existingApiKey, ...existingProviderRest } = (existingProvider ?? {}) as Record<
string,
unknown
> as { apiKey?: string };
const resolvedApiKey = typeof existingApiKey === "string" ? existingApiKey : undefined;
const normalizedApiKey = resolvedApiKey?.trim();
providers.modelverse = {
...existingProviderRest,
baseUrl: MODELVERSE_BASE_URL,
api: "openai-completions",
...(normalizedApiKey ? { apiKey: normalizedApiKey } : {}),
models: mergedModels.length > 0 ? mergedModels : modelverseModels,
};
return {
...cfg,
agents: {
...cfg.agents,
defaults: {
...cfg.agents?.defaults,
models,
},
},
models: {
mode: cfg.models?.mode ?? "merge",
providers,
},
};
}
export function applyModelverseConfig(cfg: MoltbotConfig): MoltbotConfig {
const next = applyModelverseProviderConfig(cfg);
const existingModel = next.agents?.defaults?.model;
return {
...next,
agents: {
...next.agents,
defaults: {
...next.agents?.defaults,
model: {
...(existingModel && "fallbacks" in (existingModel as Record<string, unknown>)
? {
fallbacks: (existingModel as { fallbacks?: string[] }).fallbacks,
}
: undefined),
primary: MODELVERSE_DEFAULT_MODEL_REF,
},
},
},
};
}
export function applyKimiCodeProviderConfig(cfg: MoltbotConfig): MoltbotConfig {
const models = { ...cfg.agents?.defaults?.models };
models[KIMI_CODE_MODEL_REF] = {

View File

@ -73,6 +73,19 @@ export async function setMoonshotApiKey(key: string, agentDir?: string) {
});
}
export async function setModelverseApiKey(key: string, agentDir?: string) {
// Write to resolved agent dir so gateway finds credentials on startup.
upsertAuthProfile({
profileId: "modelverse:default",
credential: {
type: "api_key",
provider: "modelverse",
key,
},
agentDir: resolveAuthAgentDir(agentDir),
});
}
export async function setKimiCodeApiKey(key: string, agentDir?: string) {
// Write to resolved agent dir so gateway finds credentials on startup.
upsertAuthProfile({

View File

@ -3,10 +3,16 @@ export {
SYNTHETIC_DEFAULT_MODEL_REF,
} from "../agents/synthetic-models.js";
export { VENICE_DEFAULT_MODEL_ID, VENICE_DEFAULT_MODEL_REF } from "../agents/venice-models.js";
export {
MODELVERSE_DEFAULT_MODEL_ID,
MODELVERSE_DEFAULT_MODEL_REF,
} from "../agents/modelverse-models.js";
export {
applyAuthProfileConfig,
applyKimiCodeConfig,
applyKimiCodeProviderConfig,
applyModelverseConfig,
applyModelverseProviderConfig,
applyMoonshotConfig,
applyMoonshotProviderConfig,
applyOpenrouterConfig,
@ -37,6 +43,7 @@ export {
setAnthropicApiKey,
setGeminiApiKey,
setKimiCodeApiKey,
setModelverseApiKey,
setMinimaxApiKey,
setMoonshotApiKey,
setOpencodeZenApiKey,

View File

@ -11,6 +11,7 @@ import {
applyKimiCodeConfig,
applyMinimaxApiConfig,
applyMinimaxConfig,
applyModelverseConfig,
applyMoonshotConfig,
applyOpencodeZenConfig,
applyOpenrouterConfig,
@ -21,6 +22,7 @@ import {
setAnthropicApiKey,
setGeminiApiKey,
setKimiCodeApiKey,
setModelverseApiKey,
setMinimaxApiKey,
setMoonshotApiKey,
setOpencodeZenApiKey,
@ -195,6 +197,25 @@ export async function applyNonInteractiveAuthChoice(params: {
return nextConfig;
}
if (authChoice === "modelverse-api-key") {
const resolved = await resolveNonInteractiveApiKey({
provider: "modelverse",
cfg: baseConfig,
flagValue: opts.modelverseApiKey,
flagName: "--modelverse-api-key",
envVar: "MODELVERSE_API_KEY",
runtime,
});
if (!resolved) return null;
if (resolved.source !== "profile") await setModelverseApiKey(resolved.key);
nextConfig = applyAuthProfileConfig(nextConfig, {
profileId: "modelverse:default",
provider: "modelverse",
mode: "api_key",
});
return applyModelverseConfig(nextConfig);
}
if (authChoice === "openrouter-api-key") {
const resolved = await resolveNonInteractiveApiKey({
provider: "openrouter",

View File

@ -11,6 +11,7 @@ export type AuthChoice =
| "chutes"
| "openai-codex"
| "openai-api-key"
| "modelverse-api-key"
| "openrouter-api-key"
| "ai-gateway-api-key"
| "moonshot-api-key"
@ -61,6 +62,7 @@ export type OnboardOptions = {
tokenExpiresIn?: string;
anthropicApiKey?: string;
openaiApiKey?: string;
modelverseApiKey?: string;
openrouterApiKey?: string;
aiGatewayApiKey?: string;
moonshotApiKey?: string;