Merge 0a269c6ed5 into 06289b36da
This commit is contained in:
commit
43179d1bd3
@ -124,6 +124,32 @@ Moltbot ships with the pi‑ai catalog. These providers require **no**
|
||||
Use `models.providers` (or `models.json`) to add **custom** providers or
|
||||
OpenAI/Anthropic‑compatible 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:
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
66
docs/providers/modelverse.md
Normal file
66
docs/providers/modelverse.md
Normal 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" }]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@ -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",
|
||||
|
||||
37
src/agents/models-config.providers.modelverse.test.ts
Normal file
37
src/agents/models-config.providers.modelverse.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
@ -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"] = {
|
||||
|
||||
132
src/agents/modelverse-models.ts
Normal file
132
src/agents/modelverse-models.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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();
|
||||
});
|
||||
|
||||
@ -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] = {
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user