mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 13:11:22 +00:00
feat(agents) : Hugging Face Inference provider first-class support and Together API fix and Direct Injection Refactor Auths [AI-assisted] (#13472)
* initial commit * removes assesment from docs * resolves automated review comments * resolves lint , type , tests , refactors , and submits * solves : why do we have to lint the tests xD * adds greptile fixes * solves a type error * solves a ci error * refactors auths * solves a failing test after i pulled from main lol * solves a failing test after i pulled from main lol * resolves token naming issue to comply with better practices when using hf / huggingface * fixes curly lints ! * fixes failing tests for google api from main * solve merge conflicts * solve failing tests with a defensive check 'undefined' openrouterapi key * fix: preserve Hugging Face auth-choice intent and token behavior (#13472) (thanks @Josephrp) * test: resolve auth-choice cherry-pick conflict cleanup (#13472) --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
@@ -1,35 +1,13 @@
|
||||
import type { AuthProfileStore } from "../agents/auth-profiles.js";
|
||||
import type { AuthChoice } from "./onboard-types.js";
|
||||
import type { AuthChoice, AuthChoiceGroupId } from "./onboard-types.js";
|
||||
|
||||
export type { AuthChoiceGroupId };
|
||||
|
||||
export type AuthChoiceOption = {
|
||||
value: AuthChoice;
|
||||
label: string;
|
||||
hint?: string;
|
||||
};
|
||||
|
||||
export type AuthChoiceGroupId =
|
||||
| "openai"
|
||||
| "anthropic"
|
||||
| "vllm"
|
||||
| "google"
|
||||
| "copilot"
|
||||
| "openrouter"
|
||||
| "litellm"
|
||||
| "ai-gateway"
|
||||
| "cloudflare-ai-gateway"
|
||||
| "moonshot"
|
||||
| "zai"
|
||||
| "xiaomi"
|
||||
| "opencode-zen"
|
||||
| "minimax"
|
||||
| "synthetic"
|
||||
| "venice"
|
||||
| "qwen"
|
||||
| "together"
|
||||
| "qianfan"
|
||||
| "xai"
|
||||
| "custom";
|
||||
|
||||
export type AuthChoiceGroup = {
|
||||
value: AuthChoiceGroupId;
|
||||
label: string;
|
||||
@@ -145,6 +123,12 @@ const AUTH_CHOICE_GROUP_DEFS: {
|
||||
hint: "API key",
|
||||
choices: ["together-api-key"],
|
||||
},
|
||||
{
|
||||
value: "huggingface",
|
||||
label: "Hugging Face",
|
||||
hint: "Inference API (HF token)",
|
||||
choices: ["huggingface-api-key"],
|
||||
},
|
||||
{
|
||||
value: "venice",
|
||||
label: "Venice AI",
|
||||
@@ -238,6 +222,11 @@ export function buildAuthChoiceOptions(params: {
|
||||
label: "Together AI API key",
|
||||
hint: "Access to Llama, DeepSeek, Qwen, and more open models",
|
||||
});
|
||||
options.push({
|
||||
value: "huggingface-api-key",
|
||||
label: "Hugging Face API key (HF token)",
|
||||
hint: "Inference Providers — OpenAI-compatible chat",
|
||||
});
|
||||
options.push({
|
||||
value: "github-copilot",
|
||||
label: "GitHub Copilot (GitHub device login)",
|
||||
|
||||
@@ -6,6 +6,8 @@ import {
|
||||
normalizeApiKeyInput,
|
||||
validateApiKeyInput,
|
||||
} from "./auth-choice.api-key.js";
|
||||
import { applyAuthChoiceHuggingface } from "./auth-choice.apply.huggingface.js";
|
||||
import { applyAuthChoiceOpenRouter } from "./auth-choice.apply.openrouter.js";
|
||||
import { applyDefaultModelChoice } from "./auth-choice.default-model.js";
|
||||
import {
|
||||
applyGoogleGeminiModelDefault,
|
||||
@@ -27,8 +29,6 @@ import {
|
||||
applyMoonshotProviderConfigCn,
|
||||
applyOpencodeZenConfig,
|
||||
applyOpencodeZenProviderConfig,
|
||||
applyOpenrouterConfig,
|
||||
applyOpenrouterProviderConfig,
|
||||
applySyntheticConfig,
|
||||
applySyntheticProviderConfig,
|
||||
applyTogetherConfig,
|
||||
@@ -46,7 +46,6 @@ import {
|
||||
QIANFAN_DEFAULT_MODEL_REF,
|
||||
KIMI_CODING_MODEL_REF,
|
||||
MOONSHOT_DEFAULT_MODEL_REF,
|
||||
OPENROUTER_DEFAULT_MODEL_REF,
|
||||
SYNTHETIC_DEFAULT_MODEL_REF,
|
||||
TOGETHER_DEFAULT_MODEL_REF,
|
||||
VENICE_DEFAULT_MODEL_REF,
|
||||
@@ -59,7 +58,6 @@ import {
|
||||
setKimiCodingApiKey,
|
||||
setMoonshotApiKey,
|
||||
setOpencodeZenApiKey,
|
||||
setOpenrouterApiKey,
|
||||
setSyntheticApiKey,
|
||||
setTogetherApiKey,
|
||||
setVeniceApiKey,
|
||||
@@ -120,6 +118,8 @@ export async function applyAuthChoiceApiProviders(
|
||||
authChoice = "venice-api-key";
|
||||
} else if (params.opts.tokenProvider === "together") {
|
||||
authChoice = "together-api-key";
|
||||
} else if (params.opts.tokenProvider === "huggingface") {
|
||||
authChoice = "huggingface-api-key";
|
||||
} else if (params.opts.tokenProvider === "opencode") {
|
||||
authChoice = "opencode-zen";
|
||||
} else if (params.opts.tokenProvider === "qianfan") {
|
||||
@@ -128,81 +128,7 @@ export async function applyAuthChoiceApiProviders(
|
||||
}
|
||||
|
||||
if (authChoice === "openrouter-api-key") {
|
||||
const store = ensureAuthProfileStore(params.agentDir, {
|
||||
allowKeychainPrompt: false,
|
||||
});
|
||||
const profileOrder = resolveAuthProfileOrder({
|
||||
cfg: nextConfig,
|
||||
store,
|
||||
provider: "openrouter",
|
||||
});
|
||||
const existingProfileId = profileOrder.find((profileId) => Boolean(store.profiles[profileId]));
|
||||
const existingCred = existingProfileId ? store.profiles[existingProfileId] : undefined;
|
||||
let profileId = "openrouter:default";
|
||||
let mode: "api_key" | "oauth" | "token" = "api_key";
|
||||
let hasCredential = false;
|
||||
|
||||
if (existingProfileId && existingCred?.type) {
|
||||
profileId = existingProfileId;
|
||||
mode =
|
||||
existingCred.type === "oauth"
|
||||
? "oauth"
|
||||
: existingCred.type === "token"
|
||||
? "token"
|
||||
: "api_key";
|
||||
hasCredential = true;
|
||||
}
|
||||
|
||||
if (!hasCredential && params.opts?.token && params.opts?.tokenProvider === "openrouter") {
|
||||
await setOpenrouterApiKey(normalizeApiKeyInput(params.opts.token), params.agentDir);
|
||||
hasCredential = true;
|
||||
}
|
||||
|
||||
if (!hasCredential) {
|
||||
const envKey = resolveEnvApiKey("openrouter");
|
||||
if (envKey) {
|
||||
const useExisting = await params.prompter.confirm({
|
||||
message: `Use existing OPENROUTER_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`,
|
||||
initialValue: true,
|
||||
});
|
||||
if (useExisting) {
|
||||
await setOpenrouterApiKey(envKey.apiKey, params.agentDir);
|
||||
hasCredential = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasCredential) {
|
||||
const key = await params.prompter.text({
|
||||
message: "Enter OpenRouter API key",
|
||||
validate: validateApiKeyInput,
|
||||
});
|
||||
await setOpenrouterApiKey(normalizeApiKeyInput(String(key ?? "")), params.agentDir);
|
||||
hasCredential = true;
|
||||
}
|
||||
|
||||
if (hasCredential) {
|
||||
nextConfig = applyAuthProfileConfig(nextConfig, {
|
||||
profileId,
|
||||
provider: "openrouter",
|
||||
mode,
|
||||
});
|
||||
}
|
||||
{
|
||||
const applied = await applyDefaultModelChoice({
|
||||
config: nextConfig,
|
||||
setDefaultModel: params.setDefaultModel,
|
||||
defaultModel: OPENROUTER_DEFAULT_MODEL_REF,
|
||||
applyDefaultConfig: applyOpenrouterConfig,
|
||||
applyProviderConfig: applyOpenrouterProviderConfig,
|
||||
noteDefault: OPENROUTER_DEFAULT_MODEL_REF,
|
||||
noteAgentModel,
|
||||
prompter: params.prompter,
|
||||
});
|
||||
nextConfig = applied.config;
|
||||
agentModelOverride = applied.agentModelOverride ?? agentModelOverride;
|
||||
}
|
||||
return { config: nextConfig, agentModelOverride };
|
||||
return applyAuthChoiceOpenRouter(params);
|
||||
}
|
||||
|
||||
if (authChoice === "litellm-api-key") {
|
||||
@@ -993,6 +919,10 @@ export async function applyAuthChoiceApiProviders(
|
||||
return { config: nextConfig, agentModelOverride };
|
||||
}
|
||||
|
||||
if (authChoice === "huggingface-api-key") {
|
||||
return applyAuthChoiceHuggingface({ ...params, authChoice });
|
||||
}
|
||||
|
||||
if (authChoice === "qianfan-api-key") {
|
||||
let hasCredential = false;
|
||||
if (!hasCredential && params.opts?.token && params.opts?.tokenProvider === "qianfan") {
|
||||
|
||||
163
src/commands/auth-choice.apply.huggingface.test.ts
Normal file
163
src/commands/auth-choice.apply.huggingface.test.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import type { WizardPrompter } from "../wizard/prompts.js";
|
||||
import { applyAuthChoiceHuggingface } from "./auth-choice.apply.huggingface.js";
|
||||
|
||||
const noopAsync = async () => {};
|
||||
const noop = () => {};
|
||||
const authProfilePathFor = (agentDir: string) => path.join(agentDir, "auth-profiles.json");
|
||||
|
||||
describe("applyAuthChoiceHuggingface", () => {
|
||||
const previousAgentDir = process.env.OPENCLAW_AGENT_DIR;
|
||||
const previousHfToken = process.env.HF_TOKEN;
|
||||
const previousHubToken = process.env.HUGGINGFACE_HUB_TOKEN;
|
||||
let tempStateDir: string | null = null;
|
||||
|
||||
afterEach(async () => {
|
||||
if (tempStateDir) {
|
||||
await fs.rm(tempStateDir, { recursive: true, force: true });
|
||||
tempStateDir = null;
|
||||
}
|
||||
if (previousAgentDir === undefined) {
|
||||
delete process.env.OPENCLAW_AGENT_DIR;
|
||||
} else {
|
||||
process.env.OPENCLAW_AGENT_DIR = previousAgentDir;
|
||||
}
|
||||
if (previousHfToken === undefined) {
|
||||
delete process.env.HF_TOKEN;
|
||||
} else {
|
||||
process.env.HF_TOKEN = previousHfToken;
|
||||
}
|
||||
if (previousHubToken === undefined) {
|
||||
delete process.env.HUGGINGFACE_HUB_TOKEN;
|
||||
} else {
|
||||
process.env.HUGGINGFACE_HUB_TOKEN = previousHubToken;
|
||||
}
|
||||
});
|
||||
|
||||
it("returns null when authChoice is not huggingface-api-key", async () => {
|
||||
const result = await applyAuthChoiceHuggingface({
|
||||
authChoice: "openrouter-api-key",
|
||||
config: {},
|
||||
prompter: {} as WizardPrompter,
|
||||
runtime: {} as RuntimeEnv,
|
||||
setDefaultModel: false,
|
||||
});
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("prompts for key and model, then writes config and auth profile", async () => {
|
||||
tempStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-hf-"));
|
||||
const agentDir = path.join(tempStateDir, "agent");
|
||||
process.env.OPENCLAW_AGENT_DIR = agentDir;
|
||||
await fs.mkdir(agentDir, { recursive: true });
|
||||
|
||||
const text = vi.fn().mockResolvedValue("hf-test-token");
|
||||
const select: WizardPrompter["select"] = vi.fn(
|
||||
async (params) => params.options?.[0]?.value as never,
|
||||
);
|
||||
const prompter: WizardPrompter = {
|
||||
intro: vi.fn(noopAsync),
|
||||
outro: vi.fn(noopAsync),
|
||||
note: vi.fn(noopAsync),
|
||||
select,
|
||||
multiselect: vi.fn(async () => []),
|
||||
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 applyAuthChoiceHuggingface({
|
||||
authChoice: "huggingface-api-key",
|
||||
config: {},
|
||||
prompter,
|
||||
runtime,
|
||||
setDefaultModel: true,
|
||||
});
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.config.auth?.profiles?.["huggingface:default"]).toMatchObject({
|
||||
provider: "huggingface",
|
||||
mode: "api_key",
|
||||
});
|
||||
expect(result?.config.agents?.defaults?.model?.primary).toMatch(/^huggingface\/.+/);
|
||||
expect(text).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ message: expect.stringContaining("Hugging Face") }),
|
||||
);
|
||||
expect(select).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ message: "Default Hugging Face model" }),
|
||||
);
|
||||
|
||||
const authProfilePath = authProfilePathFor(agentDir);
|
||||
const raw = await fs.readFile(authProfilePath, "utf8");
|
||||
const parsed = JSON.parse(raw) as {
|
||||
profiles?: Record<string, { key?: string }>;
|
||||
};
|
||||
expect(parsed.profiles?.["huggingface:default"]?.key).toBe("hf-test-token");
|
||||
});
|
||||
|
||||
it("does not prompt to reuse env token when opts.token already provided", async () => {
|
||||
tempStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-hf-"));
|
||||
const agentDir = path.join(tempStateDir, "agent");
|
||||
process.env.OPENCLAW_AGENT_DIR = agentDir;
|
||||
process.env.HF_TOKEN = "hf-env-token";
|
||||
delete process.env.HUGGINGFACE_HUB_TOKEN;
|
||||
await fs.mkdir(agentDir, { recursive: true });
|
||||
|
||||
const text = vi.fn().mockResolvedValue("hf-text-token");
|
||||
const select: WizardPrompter["select"] = vi.fn(
|
||||
async (params) => params.options?.[0]?.value as never,
|
||||
);
|
||||
const confirm = vi.fn(async () => true);
|
||||
const prompter: WizardPrompter = {
|
||||
intro: vi.fn(noopAsync),
|
||||
outro: vi.fn(noopAsync),
|
||||
note: vi.fn(noopAsync),
|
||||
select,
|
||||
multiselect: vi.fn(async () => []),
|
||||
text,
|
||||
confirm,
|
||||
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 applyAuthChoiceHuggingface({
|
||||
authChoice: "huggingface-api-key",
|
||||
config: {},
|
||||
prompter,
|
||||
runtime,
|
||||
setDefaultModel: true,
|
||||
opts: {
|
||||
tokenProvider: "huggingface",
|
||||
token: "hf-opts-token",
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(confirm).not.toHaveBeenCalled();
|
||||
expect(text).not.toHaveBeenCalled();
|
||||
|
||||
const authProfilePath = authProfilePathFor(agentDir);
|
||||
const raw = await fs.readFile(authProfilePath, "utf8");
|
||||
const parsed = JSON.parse(raw) as {
|
||||
profiles?: Record<string, { key?: string }>;
|
||||
};
|
||||
expect(parsed.profiles?.["huggingface:default"]?.key).toBe("hf-opts-token");
|
||||
});
|
||||
});
|
||||
165
src/commands/auth-choice.apply.huggingface.ts
Normal file
165
src/commands/auth-choice.apply.huggingface.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
import type { ApplyAuthChoiceParams, ApplyAuthChoiceResult } from "./auth-choice.apply.js";
|
||||
import {
|
||||
discoverHuggingfaceModels,
|
||||
isHuggingfacePolicyLocked,
|
||||
} from "../agents/huggingface-models.js";
|
||||
import { resolveEnvApiKey } from "../agents/model-auth.js";
|
||||
import {
|
||||
formatApiKeyPreview,
|
||||
normalizeApiKeyInput,
|
||||
validateApiKeyInput,
|
||||
} from "./auth-choice.api-key.js";
|
||||
import { applyDefaultModelChoice } from "./auth-choice.default-model.js";
|
||||
import { ensureModelAllowlistEntry } from "./model-allowlist.js";
|
||||
import {
|
||||
applyAuthProfileConfig,
|
||||
applyHuggingfaceProviderConfig,
|
||||
setHuggingfaceApiKey,
|
||||
HUGGINGFACE_DEFAULT_MODEL_REF,
|
||||
} from "./onboard-auth.js";
|
||||
|
||||
export async function applyAuthChoiceHuggingface(
|
||||
params: ApplyAuthChoiceParams,
|
||||
): Promise<ApplyAuthChoiceResult | null> {
|
||||
if (params.authChoice !== "huggingface-api-key") {
|
||||
return null;
|
||||
}
|
||||
|
||||
let nextConfig = params.config;
|
||||
let agentModelOverride: string | undefined;
|
||||
const noteAgentModel = async (model: string) => {
|
||||
if (!params.agentId) {
|
||||
return;
|
||||
}
|
||||
await params.prompter.note(
|
||||
`Default model set to ${model} for agent "${params.agentId}".`,
|
||||
"Model configured",
|
||||
);
|
||||
};
|
||||
|
||||
let hasCredential = false;
|
||||
let hfKey = "";
|
||||
|
||||
if (!hasCredential && params.opts?.token && params.opts.tokenProvider === "huggingface") {
|
||||
hfKey = normalizeApiKeyInput(params.opts.token);
|
||||
await setHuggingfaceApiKey(hfKey, params.agentDir);
|
||||
hasCredential = true;
|
||||
}
|
||||
|
||||
if (!hasCredential) {
|
||||
await params.prompter.note(
|
||||
[
|
||||
"Hugging Face Inference Providers offer OpenAI-compatible chat completions.",
|
||||
"Create a token at: https://huggingface.co/settings/tokens (fine-grained, 'Make calls to Inference Providers').",
|
||||
].join("\n"),
|
||||
"Hugging Face",
|
||||
);
|
||||
}
|
||||
|
||||
if (!hasCredential) {
|
||||
const envKey = resolveEnvApiKey("huggingface");
|
||||
if (envKey) {
|
||||
const useExisting = await params.prompter.confirm({
|
||||
message: `Use existing Hugging Face token (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`,
|
||||
initialValue: true,
|
||||
});
|
||||
if (useExisting) {
|
||||
hfKey = envKey.apiKey;
|
||||
await setHuggingfaceApiKey(hfKey, params.agentDir);
|
||||
hasCredential = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!hasCredential) {
|
||||
const key = await params.prompter.text({
|
||||
message: "Enter Hugging Face API key (HF token)",
|
||||
validate: validateApiKeyInput,
|
||||
});
|
||||
hfKey = normalizeApiKeyInput(String(key ?? ""));
|
||||
await setHuggingfaceApiKey(hfKey, params.agentDir);
|
||||
}
|
||||
nextConfig = applyAuthProfileConfig(nextConfig, {
|
||||
profileId: "huggingface:default",
|
||||
provider: "huggingface",
|
||||
mode: "api_key",
|
||||
});
|
||||
|
||||
const models = await discoverHuggingfaceModels(hfKey);
|
||||
const modelRefPrefix = "huggingface/";
|
||||
const options: { value: string; label: string }[] = [];
|
||||
for (const m of models) {
|
||||
const baseRef = `${modelRefPrefix}${m.id}`;
|
||||
const label = m.name ?? m.id;
|
||||
options.push({ value: baseRef, label });
|
||||
options.push({ value: `${baseRef}:cheapest`, label: `${label} (cheapest)` });
|
||||
options.push({ value: `${baseRef}:fastest`, label: `${label} (fastest)` });
|
||||
}
|
||||
const defaultRef = HUGGINGFACE_DEFAULT_MODEL_REF;
|
||||
options.sort((a, b) => {
|
||||
if (a.value === defaultRef) {
|
||||
return -1;
|
||||
}
|
||||
if (b.value === defaultRef) {
|
||||
return 1;
|
||||
}
|
||||
return a.label.localeCompare(b.label, undefined, { sensitivity: "base" });
|
||||
});
|
||||
const selectedModelRef =
|
||||
options.length === 0
|
||||
? defaultRef
|
||||
: options.length === 1
|
||||
? options[0].value
|
||||
: await params.prompter.select({
|
||||
message: "Default Hugging Face model",
|
||||
options,
|
||||
initialValue: options.some((o) => o.value === defaultRef)
|
||||
? defaultRef
|
||||
: options[0].value,
|
||||
});
|
||||
|
||||
if (isHuggingfacePolicyLocked(selectedModelRef)) {
|
||||
await params.prompter.note(
|
||||
"Provider locked — router will choose backend by cost or speed.",
|
||||
"Hugging Face",
|
||||
);
|
||||
}
|
||||
|
||||
const applied = await applyDefaultModelChoice({
|
||||
config: nextConfig,
|
||||
setDefaultModel: params.setDefaultModel,
|
||||
defaultModel: selectedModelRef,
|
||||
applyDefaultConfig: (config) => {
|
||||
const withProvider = applyHuggingfaceProviderConfig(config);
|
||||
const existingModel = withProvider.agents?.defaults?.model;
|
||||
const withPrimary = {
|
||||
...withProvider,
|
||||
agents: {
|
||||
...withProvider.agents,
|
||||
defaults: {
|
||||
...withProvider.agents?.defaults,
|
||||
model: {
|
||||
...(existingModel && typeof existingModel === "object" && "fallbacks" in existingModel
|
||||
? {
|
||||
fallbacks: (existingModel as { fallbacks?: string[] }).fallbacks,
|
||||
}
|
||||
: {}),
|
||||
primary: selectedModelRef,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
return ensureModelAllowlistEntry({
|
||||
cfg: withPrimary,
|
||||
modelRef: selectedModelRef,
|
||||
});
|
||||
},
|
||||
applyProviderConfig: applyHuggingfaceProviderConfig,
|
||||
noteDefault: selectedModelRef,
|
||||
noteAgentModel,
|
||||
prompter: params.prompter,
|
||||
});
|
||||
nextConfig = applied.config;
|
||||
agentModelOverride = applied.agentModelOverride ?? agentModelOverride;
|
||||
|
||||
return { config: nextConfig, agentModelOverride };
|
||||
}
|
||||
102
src/commands/auth-choice.apply.openrouter.ts
Normal file
102
src/commands/auth-choice.apply.openrouter.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import type { ApplyAuthChoiceParams, ApplyAuthChoiceResult } from "./auth-choice.apply.js";
|
||||
import { ensureAuthProfileStore, resolveAuthProfileOrder } from "../agents/auth-profiles.js";
|
||||
import { resolveEnvApiKey } from "../agents/model-auth.js";
|
||||
import {
|
||||
formatApiKeyPreview,
|
||||
normalizeApiKeyInput,
|
||||
validateApiKeyInput,
|
||||
} from "./auth-choice.api-key.js";
|
||||
import { applyDefaultModelChoice } from "./auth-choice.default-model.js";
|
||||
import {
|
||||
applyAuthProfileConfig,
|
||||
applyOpenrouterConfig,
|
||||
applyOpenrouterProviderConfig,
|
||||
setOpenrouterApiKey,
|
||||
OPENROUTER_DEFAULT_MODEL_REF,
|
||||
} from "./onboard-auth.js";
|
||||
|
||||
export async function applyAuthChoiceOpenRouter(
|
||||
params: ApplyAuthChoiceParams,
|
||||
): Promise<ApplyAuthChoiceResult> {
|
||||
let nextConfig = params.config;
|
||||
let agentModelOverride: string | undefined;
|
||||
const noteAgentModel = async (model: string) => {
|
||||
if (!params.agentId) {
|
||||
return;
|
||||
}
|
||||
await params.prompter.note(
|
||||
`Default model set to ${model} for agent "${params.agentId}".`,
|
||||
"Model configured",
|
||||
);
|
||||
};
|
||||
|
||||
const store = ensureAuthProfileStore(params.agentDir, { allowKeychainPrompt: false });
|
||||
const profileOrder = resolveAuthProfileOrder({
|
||||
cfg: nextConfig,
|
||||
store,
|
||||
provider: "openrouter",
|
||||
});
|
||||
const existingProfileId = profileOrder.find((profileId) => Boolean(store.profiles[profileId]));
|
||||
const existingCred = existingProfileId ? store.profiles[existingProfileId] : undefined;
|
||||
let profileId = "openrouter:default";
|
||||
let mode: "api_key" | "oauth" | "token" = "api_key";
|
||||
let hasCredential = false;
|
||||
|
||||
if (existingProfileId && existingCred?.type) {
|
||||
profileId = existingProfileId;
|
||||
mode =
|
||||
existingCred.type === "oauth" ? "oauth" : existingCred.type === "token" ? "token" : "api_key";
|
||||
hasCredential = true;
|
||||
}
|
||||
|
||||
if (!hasCredential && params.opts?.token && params.opts?.tokenProvider === "openrouter") {
|
||||
await setOpenrouterApiKey(normalizeApiKeyInput(params.opts.token), params.agentDir);
|
||||
hasCredential = true;
|
||||
}
|
||||
|
||||
if (!hasCredential) {
|
||||
const envKey = resolveEnvApiKey("openrouter");
|
||||
if (envKey) {
|
||||
const useExisting = await params.prompter.confirm({
|
||||
message: `Use existing OPENROUTER_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`,
|
||||
initialValue: true,
|
||||
});
|
||||
if (useExisting) {
|
||||
await setOpenrouterApiKey(envKey.apiKey, params.agentDir);
|
||||
hasCredential = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasCredential) {
|
||||
const key = await params.prompter.text({
|
||||
message: "Enter OpenRouter API key",
|
||||
validate: validateApiKeyInput,
|
||||
});
|
||||
await setOpenrouterApiKey(normalizeApiKeyInput(String(key ?? "")), params.agentDir);
|
||||
hasCredential = true;
|
||||
}
|
||||
|
||||
if (hasCredential) {
|
||||
nextConfig = applyAuthProfileConfig(nextConfig, {
|
||||
profileId,
|
||||
provider: "openrouter",
|
||||
mode,
|
||||
});
|
||||
}
|
||||
|
||||
const applied = await applyDefaultModelChoice({
|
||||
config: nextConfig,
|
||||
setDefaultModel: params.setDefaultModel,
|
||||
defaultModel: OPENROUTER_DEFAULT_MODEL_REF,
|
||||
applyDefaultConfig: applyOpenrouterConfig,
|
||||
applyProviderConfig: applyOpenrouterProviderConfig,
|
||||
noteDefault: OPENROUTER_DEFAULT_MODEL_REF,
|
||||
noteAgentModel,
|
||||
prompter: params.prompter,
|
||||
});
|
||||
nextConfig = applied.config;
|
||||
agentModelOverride = applied.agentModelOverride ?? agentModelOverride;
|
||||
|
||||
return { config: nextConfig, agentModelOverride };
|
||||
}
|
||||
@@ -34,6 +34,8 @@ describe("applyAuthChoice", () => {
|
||||
const previousPiAgentDir = process.env.PI_CODING_AGENT_DIR;
|
||||
const previousAnthropicKey = process.env.ANTHROPIC_API_KEY;
|
||||
const previousOpenrouterKey = process.env.OPENROUTER_API_KEY;
|
||||
const previousHfToken = process.env.HF_TOKEN;
|
||||
const previousHfHubToken = process.env.HUGGINGFACE_HUB_TOKEN;
|
||||
const previousLitellmKey = process.env.LITELLM_API_KEY;
|
||||
const previousAiGatewayKey = process.env.AI_GATEWAY_API_KEY;
|
||||
const previousCloudflareGatewayKey = process.env.CLOUDFLARE_AI_GATEWAY_API_KEY;
|
||||
@@ -73,6 +75,16 @@ describe("applyAuthChoice", () => {
|
||||
} else {
|
||||
process.env.OPENROUTER_API_KEY = previousOpenrouterKey;
|
||||
}
|
||||
if (previousHfToken === undefined) {
|
||||
delete process.env.HF_TOKEN;
|
||||
} else {
|
||||
process.env.HF_TOKEN = previousHfToken;
|
||||
}
|
||||
if (previousHfHubToken === undefined) {
|
||||
delete process.env.HUGGINGFACE_HUB_TOKEN;
|
||||
} else {
|
||||
process.env.HUGGINGFACE_HUB_TOKEN = previousHfHubToken;
|
||||
}
|
||||
if (previousLitellmKey === undefined) {
|
||||
delete process.env.LITELLM_API_KEY;
|
||||
} else {
|
||||
@@ -206,6 +218,60 @@ describe("applyAuthChoice", () => {
|
||||
expect(parsed.profiles?.["synthetic:default"]?.key).toBe("sk-synthetic-test");
|
||||
});
|
||||
|
||||
it("prompts and writes Hugging Face API key when selecting huggingface-api-key", async () => {
|
||||
tempStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-auth-"));
|
||||
process.env.OPENCLAW_STATE_DIR = tempStateDir;
|
||||
process.env.OPENCLAW_AGENT_DIR = path.join(tempStateDir, "agent");
|
||||
process.env.PI_CODING_AGENT_DIR = process.env.OPENCLAW_AGENT_DIR;
|
||||
|
||||
const text = vi.fn().mockResolvedValue("hf-test-token");
|
||||
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: "huggingface-api-key",
|
||||
config: {},
|
||||
prompter,
|
||||
runtime,
|
||||
setDefaultModel: true,
|
||||
});
|
||||
|
||||
expect(text).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ message: expect.stringContaining("Hugging Face") }),
|
||||
);
|
||||
expect(result.config.auth?.profiles?.["huggingface:default"]).toMatchObject({
|
||||
provider: "huggingface",
|
||||
mode: "api_key",
|
||||
});
|
||||
expect(result.config.agents?.defaults?.model?.primary).toMatch(/^huggingface\/.+/);
|
||||
|
||||
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?.["huggingface:default"]?.key).toBe("hf-test-token");
|
||||
});
|
||||
|
||||
it("prompts for Z.AI endpoint when selecting zai-api-key", async () => {
|
||||
tempStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-auth-"));
|
||||
process.env.OPENCLAW_STATE_DIR = tempStateDir;
|
||||
@@ -301,6 +367,64 @@ describe("applyAuthChoice", () => {
|
||||
expect(result.config.models?.providers?.zai?.baseUrl).toBe(ZAI_CODING_GLOBAL_BASE_URL);
|
||||
});
|
||||
|
||||
it("maps apiKey + tokenProvider=huggingface to huggingface-api-key flow", async () => {
|
||||
tempStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-auth-"));
|
||||
process.env.OPENCLAW_STATE_DIR = tempStateDir;
|
||||
process.env.OPENCLAW_AGENT_DIR = path.join(tempStateDir, "agent");
|
||||
process.env.PI_CODING_AGENT_DIR = process.env.OPENCLAW_AGENT_DIR;
|
||||
delete process.env.HF_TOKEN;
|
||||
delete process.env.HUGGINGFACE_HUB_TOKEN;
|
||||
|
||||
const text = vi.fn().mockResolvedValue("should-not-be-used");
|
||||
const select: WizardPrompter["select"] = vi.fn(
|
||||
async (params) => params.options[0]?.value as never,
|
||||
);
|
||||
const multiselect: WizardPrompter["multiselect"] = vi.fn(async () => []);
|
||||
const confirm = vi.fn(async () => false);
|
||||
const prompter: WizardPrompter = {
|
||||
intro: vi.fn(noopAsync),
|
||||
outro: vi.fn(noopAsync),
|
||||
note: vi.fn(noopAsync),
|
||||
select,
|
||||
multiselect,
|
||||
text,
|
||||
confirm,
|
||||
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: "apiKey",
|
||||
config: {},
|
||||
prompter,
|
||||
runtime,
|
||||
setDefaultModel: true,
|
||||
opts: {
|
||||
tokenProvider: "huggingface",
|
||||
token: "hf-token-provider-test",
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.config.auth?.profiles?.["huggingface:default"]).toMatchObject({
|
||||
provider: "huggingface",
|
||||
mode: "api_key",
|
||||
});
|
||||
expect(result.config.agents?.defaults?.model?.primary).toMatch(/^huggingface\/.+/);
|
||||
expect(text).not.toHaveBeenCalled();
|
||||
|
||||
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?.["huggingface:default"]?.key).toBe("hf-token-provider-test");
|
||||
});
|
||||
it("does not override the global default model when selecting xai-api-key without setDefaultModel", async () => {
|
||||
tempStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-auth-"));
|
||||
process.env.OPENCLAW_STATE_DIR = tempStateDir;
|
||||
|
||||
@@ -29,6 +29,7 @@ const PREFERRED_PROVIDER_BY_AUTH_CHOICE: Partial<Record<AuthChoice, string>> = {
|
||||
"synthetic-api-key": "synthetic",
|
||||
"venice-api-key": "venice",
|
||||
"together-api-key": "together",
|
||||
"huggingface-api-key": "huggingface",
|
||||
"github-copilot": "github-copilot",
|
||||
"copilot-proxy": "copilot-proxy",
|
||||
"minimax-cloud": "minimax",
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import type { ModelApi } from "../config/types.models.js";
|
||||
import {
|
||||
buildCloudflareAiGatewayModelDefinition,
|
||||
resolveCloudflareAiGatewayBaseUrl,
|
||||
} from "../agents/cloudflare-ai-gateway.js";
|
||||
buildHuggingfaceModelDefinition,
|
||||
HUGGINGFACE_BASE_URL,
|
||||
HUGGINGFACE_MODEL_CATALOG,
|
||||
} from "../agents/huggingface-models.js";
|
||||
import {
|
||||
buildQianfanProvider,
|
||||
buildXiaomiProvider,
|
||||
@@ -28,15 +29,25 @@ import {
|
||||
VENICE_MODEL_CATALOG,
|
||||
} from "../agents/venice-models.js";
|
||||
import {
|
||||
CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_REF,
|
||||
LITELLM_DEFAULT_MODEL_REF,
|
||||
HUGGINGFACE_DEFAULT_MODEL_REF,
|
||||
OPENROUTER_DEFAULT_MODEL_REF,
|
||||
TOGETHER_DEFAULT_MODEL_REF,
|
||||
VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF,
|
||||
XIAOMI_DEFAULT_MODEL_REF,
|
||||
ZAI_DEFAULT_MODEL_REF,
|
||||
XAI_DEFAULT_MODEL_REF,
|
||||
} from "./onboard-auth.credentials.js";
|
||||
export {
|
||||
applyCloudflareAiGatewayConfig,
|
||||
applyCloudflareAiGatewayProviderConfig,
|
||||
applyVercelAiGatewayConfig,
|
||||
applyVercelAiGatewayProviderConfig,
|
||||
} from "./onboard-auth.config-gateways.js";
|
||||
export {
|
||||
applyLitellmConfig,
|
||||
applyLitellmProviderConfig,
|
||||
LITELLM_BASE_URL,
|
||||
LITELLM_DEFAULT_MODEL_ID,
|
||||
} from "./onboard-auth.config-litellm.js";
|
||||
import {
|
||||
buildZaiModelDefinition,
|
||||
buildMoonshotModelDefinition,
|
||||
@@ -170,139 +181,6 @@ export function applyOpenrouterProviderConfig(cfg: OpenClawConfig): OpenClawConf
|
||||
};
|
||||
}
|
||||
|
||||
export function applyVercelAiGatewayProviderConfig(cfg: OpenClawConfig): OpenClawConfig {
|
||||
const models = { ...cfg.agents?.defaults?.models };
|
||||
models[VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF] = {
|
||||
...models[VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF],
|
||||
alias: models[VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF]?.alias ?? "Vercel AI Gateway",
|
||||
};
|
||||
|
||||
return {
|
||||
...cfg,
|
||||
agents: {
|
||||
...cfg.agents,
|
||||
defaults: {
|
||||
...cfg.agents?.defaults,
|
||||
models,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function applyCloudflareAiGatewayProviderConfig(
|
||||
cfg: OpenClawConfig,
|
||||
params?: { accountId?: string; gatewayId?: string },
|
||||
): OpenClawConfig {
|
||||
const models = { ...cfg.agents?.defaults?.models };
|
||||
models[CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_REF] = {
|
||||
...models[CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_REF],
|
||||
alias: models[CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_REF]?.alias ?? "Cloudflare AI Gateway",
|
||||
};
|
||||
|
||||
const providers = { ...cfg.models?.providers };
|
||||
const existingProvider = providers["cloudflare-ai-gateway"];
|
||||
const existingModels = Array.isArray(existingProvider?.models) ? existingProvider.models : [];
|
||||
const defaultModel = buildCloudflareAiGatewayModelDefinition();
|
||||
const hasDefaultModel = existingModels.some((model) => model.id === defaultModel.id);
|
||||
const mergedModels = hasDefaultModel ? existingModels : [...existingModels, defaultModel];
|
||||
const baseUrl =
|
||||
params?.accountId && params?.gatewayId
|
||||
? resolveCloudflareAiGatewayBaseUrl({
|
||||
accountId: params.accountId,
|
||||
gatewayId: params.gatewayId,
|
||||
})
|
||||
: existingProvider?.baseUrl;
|
||||
|
||||
if (!baseUrl) {
|
||||
return {
|
||||
...cfg,
|
||||
agents: {
|
||||
...cfg.agents,
|
||||
defaults: {
|
||||
...cfg.agents?.defaults,
|
||||
models,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const { apiKey: existingApiKey, ...existingProviderRest } = (existingProvider ?? {}) as Record<
|
||||
string,
|
||||
unknown
|
||||
> as { apiKey?: string };
|
||||
const resolvedApiKey = typeof existingApiKey === "string" ? existingApiKey : undefined;
|
||||
const normalizedApiKey = resolvedApiKey?.trim();
|
||||
providers["cloudflare-ai-gateway"] = {
|
||||
...existingProviderRest,
|
||||
baseUrl,
|
||||
api: "anthropic-messages",
|
||||
...(normalizedApiKey ? { apiKey: normalizedApiKey } : {}),
|
||||
models: mergedModels.length > 0 ? mergedModels : [defaultModel],
|
||||
};
|
||||
|
||||
return {
|
||||
...cfg,
|
||||
agents: {
|
||||
...cfg.agents,
|
||||
defaults: {
|
||||
...cfg.agents?.defaults,
|
||||
models,
|
||||
},
|
||||
},
|
||||
models: {
|
||||
mode: cfg.models?.mode ?? "merge",
|
||||
providers,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function applyVercelAiGatewayConfig(cfg: OpenClawConfig): OpenClawConfig {
|
||||
const next = applyVercelAiGatewayProviderConfig(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: VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function applyCloudflareAiGatewayConfig(
|
||||
cfg: OpenClawConfig,
|
||||
params?: { accountId?: string; gatewayId?: string },
|
||||
): OpenClawConfig {
|
||||
const next = applyCloudflareAiGatewayProviderConfig(cfg, params);
|
||||
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: CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_REF,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function applyOpenrouterConfig(cfg: OpenClawConfig): OpenClawConfig {
|
||||
const next = applyOpenrouterProviderConfig(cfg);
|
||||
const existingModel = next.agents?.defaults?.model;
|
||||
@@ -325,105 +203,6 @@ export function applyOpenrouterConfig(cfg: OpenClawConfig): OpenClawConfig {
|
||||
};
|
||||
}
|
||||
|
||||
export const LITELLM_BASE_URL = "http://localhost:4000";
|
||||
export const LITELLM_DEFAULT_MODEL_ID = "claude-opus-4-6";
|
||||
const LITELLM_DEFAULT_CONTEXT_WINDOW = 128_000;
|
||||
const LITELLM_DEFAULT_MAX_TOKENS = 8_192;
|
||||
const LITELLM_DEFAULT_COST = {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
};
|
||||
|
||||
function buildLitellmModelDefinition(): {
|
||||
id: string;
|
||||
name: string;
|
||||
reasoning: boolean;
|
||||
input: Array<"text" | "image">;
|
||||
cost: { input: number; output: number; cacheRead: number; cacheWrite: number };
|
||||
contextWindow: number;
|
||||
maxTokens: number;
|
||||
} {
|
||||
return {
|
||||
id: LITELLM_DEFAULT_MODEL_ID,
|
||||
name: "Claude Opus 4.6",
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
// LiteLLM routes to many upstreams; keep neutral placeholders.
|
||||
cost: LITELLM_DEFAULT_COST,
|
||||
contextWindow: LITELLM_DEFAULT_CONTEXT_WINDOW,
|
||||
maxTokens: LITELLM_DEFAULT_MAX_TOKENS,
|
||||
};
|
||||
}
|
||||
|
||||
export function applyLitellmProviderConfig(cfg: OpenClawConfig): OpenClawConfig {
|
||||
const models = { ...cfg.agents?.defaults?.models };
|
||||
models[LITELLM_DEFAULT_MODEL_REF] = {
|
||||
...models[LITELLM_DEFAULT_MODEL_REF],
|
||||
alias: models[LITELLM_DEFAULT_MODEL_REF]?.alias ?? "LiteLLM",
|
||||
};
|
||||
|
||||
const providers = { ...cfg.models?.providers };
|
||||
const existingProvider = providers.litellm;
|
||||
const existingModels = Array.isArray(existingProvider?.models) ? existingProvider.models : [];
|
||||
const defaultModel = buildLitellmModelDefinition();
|
||||
const hasDefaultModel = existingModels.some((model) => model.id === LITELLM_DEFAULT_MODEL_ID);
|
||||
const mergedModels = hasDefaultModel ? existingModels : [...existingModels, defaultModel];
|
||||
const { apiKey: existingApiKey, ...existingProviderRest } = (existingProvider ?? {}) as Record<
|
||||
string,
|
||||
unknown
|
||||
> as { apiKey?: string };
|
||||
const resolvedBaseUrl =
|
||||
typeof existingProvider?.baseUrl === "string" ? existingProvider.baseUrl.trim() : "";
|
||||
const resolvedApiKey = typeof existingApiKey === "string" ? existingApiKey : undefined;
|
||||
const normalizedApiKey = resolvedApiKey?.trim();
|
||||
providers.litellm = {
|
||||
...existingProviderRest,
|
||||
baseUrl: resolvedBaseUrl || LITELLM_BASE_URL,
|
||||
api: "openai-completions",
|
||||
...(normalizedApiKey ? { apiKey: normalizedApiKey } : {}),
|
||||
models: mergedModels.length > 0 ? mergedModels : [defaultModel],
|
||||
};
|
||||
|
||||
return {
|
||||
...cfg,
|
||||
agents: {
|
||||
...cfg.agents,
|
||||
defaults: {
|
||||
...cfg.agents?.defaults,
|
||||
models,
|
||||
},
|
||||
},
|
||||
models: {
|
||||
mode: cfg.models?.mode ?? "merge",
|
||||
providers,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function applyLitellmConfig(cfg: OpenClawConfig): OpenClawConfig {
|
||||
const next = applyLitellmProviderConfig(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: LITELLM_DEFAULT_MODEL_REF,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function applyMoonshotProviderConfig(cfg: OpenClawConfig): OpenClawConfig {
|
||||
return applyMoonshotProviderConfigWithBaseUrl(cfg, MOONSHOT_BASE_URL);
|
||||
}
|
||||
@@ -855,6 +634,79 @@ export function applyTogetherConfig(cfg: OpenClawConfig): OpenClawConfig {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply Hugging Face (Inference Providers) provider configuration without changing the default model.
|
||||
*/
|
||||
export function applyHuggingfaceProviderConfig(cfg: OpenClawConfig): OpenClawConfig {
|
||||
const models = { ...cfg.agents?.defaults?.models };
|
||||
models[HUGGINGFACE_DEFAULT_MODEL_REF] = {
|
||||
...models[HUGGINGFACE_DEFAULT_MODEL_REF],
|
||||
alias: models[HUGGINGFACE_DEFAULT_MODEL_REF]?.alias ?? "Hugging Face",
|
||||
};
|
||||
|
||||
const providers = { ...cfg.models?.providers };
|
||||
const existingProvider = providers.huggingface;
|
||||
const existingModels = Array.isArray(existingProvider?.models) ? existingProvider.models : [];
|
||||
const hfModels = HUGGINGFACE_MODEL_CATALOG.map(buildHuggingfaceModelDefinition);
|
||||
const mergedModels = [
|
||||
...existingModels,
|
||||
...hfModels.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.huggingface = {
|
||||
...existingProviderRest,
|
||||
baseUrl: HUGGINGFACE_BASE_URL,
|
||||
api: "openai-completions",
|
||||
...(normalizedApiKey ? { apiKey: normalizedApiKey } : {}),
|
||||
models: mergedModels.length > 0 ? mergedModels : hfModels,
|
||||
};
|
||||
|
||||
return {
|
||||
...cfg,
|
||||
agents: {
|
||||
...cfg.agents,
|
||||
defaults: {
|
||||
...cfg.agents?.defaults,
|
||||
models,
|
||||
},
|
||||
},
|
||||
models: {
|
||||
mode: cfg.models?.mode ?? "merge",
|
||||
providers,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply Hugging Face provider configuration AND set Hugging Face as the default model.
|
||||
*/
|
||||
export function applyHuggingfaceConfig(cfg: OpenClawConfig): OpenClawConfig {
|
||||
const next = applyHuggingfaceProviderConfig(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: HUGGINGFACE_DEFAULT_MODEL_REF,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function applyXaiProviderConfig(cfg: OpenClawConfig): OpenClawConfig {
|
||||
const models = { ...cfg.agents?.defaults?.models };
|
||||
models[XAI_DEFAULT_MODEL_REF] = {
|
||||
|
||||
142
src/commands/onboard-auth.config-gateways.ts
Normal file
142
src/commands/onboard-auth.config-gateways.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import {
|
||||
buildCloudflareAiGatewayModelDefinition,
|
||||
resolveCloudflareAiGatewayBaseUrl,
|
||||
} from "../agents/cloudflare-ai-gateway.js";
|
||||
import {
|
||||
CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_REF,
|
||||
VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF,
|
||||
} from "./onboard-auth.credentials.js";
|
||||
|
||||
export function applyVercelAiGatewayProviderConfig(cfg: OpenClawConfig): OpenClawConfig {
|
||||
const models = { ...cfg.agents?.defaults?.models };
|
||||
models[VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF] = {
|
||||
...models[VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF],
|
||||
alias: models[VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF]?.alias ?? "Vercel AI Gateway",
|
||||
};
|
||||
|
||||
return {
|
||||
...cfg,
|
||||
agents: {
|
||||
...cfg.agents,
|
||||
defaults: {
|
||||
...cfg.agents?.defaults,
|
||||
models,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function applyCloudflareAiGatewayProviderConfig(
|
||||
cfg: OpenClawConfig,
|
||||
params?: { accountId?: string; gatewayId?: string },
|
||||
): OpenClawConfig {
|
||||
const models = { ...cfg.agents?.defaults?.models };
|
||||
models[CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_REF] = {
|
||||
...models[CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_REF],
|
||||
alias: models[CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_REF]?.alias ?? "Cloudflare AI Gateway",
|
||||
};
|
||||
|
||||
const providers = { ...cfg.models?.providers };
|
||||
const existingProvider = providers["cloudflare-ai-gateway"];
|
||||
const existingModels = Array.isArray(existingProvider?.models) ? existingProvider.models : [];
|
||||
const defaultModel = buildCloudflareAiGatewayModelDefinition();
|
||||
const hasDefaultModel = existingModels.some((model) => model.id === defaultModel.id);
|
||||
const mergedModels = hasDefaultModel ? existingModels : [...existingModels, defaultModel];
|
||||
const baseUrl =
|
||||
params?.accountId && params?.gatewayId
|
||||
? resolveCloudflareAiGatewayBaseUrl({
|
||||
accountId: params.accountId,
|
||||
gatewayId: params.gatewayId,
|
||||
})
|
||||
: existingProvider?.baseUrl;
|
||||
|
||||
if (!baseUrl) {
|
||||
return {
|
||||
...cfg,
|
||||
agents: {
|
||||
...cfg.agents,
|
||||
defaults: {
|
||||
...cfg.agents?.defaults,
|
||||
models,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const { apiKey: existingApiKey, ...existingProviderRest } = (existingProvider ?? {}) as Record<
|
||||
string,
|
||||
unknown
|
||||
> as { apiKey?: string };
|
||||
const resolvedApiKey = typeof existingApiKey === "string" ? existingApiKey : undefined;
|
||||
const normalizedApiKey = resolvedApiKey?.trim();
|
||||
providers["cloudflare-ai-gateway"] = {
|
||||
...existingProviderRest,
|
||||
baseUrl,
|
||||
api: "anthropic-messages",
|
||||
...(normalizedApiKey ? { apiKey: normalizedApiKey } : {}),
|
||||
models: mergedModels.length > 0 ? mergedModels : [defaultModel],
|
||||
};
|
||||
|
||||
return {
|
||||
...cfg,
|
||||
agents: {
|
||||
...cfg.agents,
|
||||
defaults: {
|
||||
...cfg.agents?.defaults,
|
||||
models,
|
||||
},
|
||||
},
|
||||
models: {
|
||||
mode: cfg.models?.mode ?? "merge",
|
||||
providers,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function applyVercelAiGatewayConfig(cfg: OpenClawConfig): OpenClawConfig {
|
||||
const next = applyVercelAiGatewayProviderConfig(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: VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function applyCloudflareAiGatewayConfig(
|
||||
cfg: OpenClawConfig,
|
||||
params?: { accountId?: string; gatewayId?: string },
|
||||
): OpenClawConfig {
|
||||
const next = applyCloudflareAiGatewayProviderConfig(cfg, params);
|
||||
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: CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_REF,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
100
src/commands/onboard-auth.config-litellm.ts
Normal file
100
src/commands/onboard-auth.config-litellm.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { LITELLM_DEFAULT_MODEL_REF } from "./onboard-auth.credentials.js";
|
||||
|
||||
export const LITELLM_BASE_URL = "http://localhost:4000";
|
||||
export const LITELLM_DEFAULT_MODEL_ID = "claude-opus-4-6";
|
||||
const LITELLM_DEFAULT_CONTEXT_WINDOW = 128_000;
|
||||
const LITELLM_DEFAULT_MAX_TOKENS = 8_192;
|
||||
const LITELLM_DEFAULT_COST = {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
};
|
||||
|
||||
function buildLitellmModelDefinition(): {
|
||||
id: string;
|
||||
name: string;
|
||||
reasoning: boolean;
|
||||
input: Array<"text" | "image">;
|
||||
cost: { input: number; output: number; cacheRead: number; cacheWrite: number };
|
||||
contextWindow: number;
|
||||
maxTokens: number;
|
||||
} {
|
||||
return {
|
||||
id: LITELLM_DEFAULT_MODEL_ID,
|
||||
name: "Claude Opus 4.6",
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: LITELLM_DEFAULT_COST,
|
||||
contextWindow: LITELLM_DEFAULT_CONTEXT_WINDOW,
|
||||
maxTokens: LITELLM_DEFAULT_MAX_TOKENS,
|
||||
};
|
||||
}
|
||||
|
||||
export function applyLitellmProviderConfig(cfg: OpenClawConfig): OpenClawConfig {
|
||||
const models = { ...cfg.agents?.defaults?.models };
|
||||
models[LITELLM_DEFAULT_MODEL_REF] = {
|
||||
...models[LITELLM_DEFAULT_MODEL_REF],
|
||||
alias: models[LITELLM_DEFAULT_MODEL_REF]?.alias ?? "LiteLLM",
|
||||
};
|
||||
|
||||
const providers = { ...cfg.models?.providers };
|
||||
const existingProvider = providers.litellm;
|
||||
const existingModels = Array.isArray(existingProvider?.models) ? existingProvider.models : [];
|
||||
const defaultModel = buildLitellmModelDefinition();
|
||||
const hasDefaultModel = existingModels.some((model) => model.id === LITELLM_DEFAULT_MODEL_ID);
|
||||
const mergedModels = hasDefaultModel ? existingModels : [...existingModels, defaultModel];
|
||||
const { apiKey: existingApiKey, ...existingProviderRest } = (existingProvider ?? {}) as Record<
|
||||
string,
|
||||
unknown
|
||||
> as { apiKey?: string };
|
||||
const resolvedBaseUrl =
|
||||
typeof existingProvider?.baseUrl === "string" ? existingProvider.baseUrl.trim() : "";
|
||||
const resolvedApiKey = typeof existingApiKey === "string" ? existingApiKey : undefined;
|
||||
const normalizedApiKey = resolvedApiKey?.trim();
|
||||
providers.litellm = {
|
||||
...existingProviderRest,
|
||||
baseUrl: resolvedBaseUrl || LITELLM_BASE_URL,
|
||||
api: "openai-completions",
|
||||
...(normalizedApiKey ? { apiKey: normalizedApiKey } : {}),
|
||||
models: mergedModels.length > 0 ? mergedModels : [defaultModel],
|
||||
};
|
||||
|
||||
return {
|
||||
...cfg,
|
||||
agents: {
|
||||
...cfg.agents,
|
||||
defaults: {
|
||||
...cfg.agents?.defaults,
|
||||
models,
|
||||
},
|
||||
},
|
||||
models: {
|
||||
mode: cfg.models?.mode ?? "merge",
|
||||
providers,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function applyLitellmConfig(cfg: OpenClawConfig): OpenClawConfig {
|
||||
const next = applyLitellmProviderConfig(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: LITELLM_DEFAULT_MODEL_REF,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -118,6 +118,7 @@ export async function setVeniceApiKey(key: string, agentDir?: string) {
|
||||
export const ZAI_DEFAULT_MODEL_REF = "zai/glm-5";
|
||||
export const XIAOMI_DEFAULT_MODEL_REF = "xiaomi/mimo-v2-flash";
|
||||
export const OPENROUTER_DEFAULT_MODEL_REF = "openrouter/auto";
|
||||
export const HUGGINGFACE_DEFAULT_MODEL_REF = "huggingface/deepseek-ai/DeepSeek-R1";
|
||||
export const TOGETHER_DEFAULT_MODEL_REF = "together/moonshotai/Kimi-K2.5";
|
||||
export const LITELLM_DEFAULT_MODEL_REF = "litellm/claude-opus-4-6";
|
||||
export const VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF = "vercel-ai-gateway/anthropic/claude-opus-4.6";
|
||||
@@ -148,12 +149,14 @@ export async function setXiaomiApiKey(key: string, agentDir?: string) {
|
||||
}
|
||||
|
||||
export async function setOpenrouterApiKey(key: string, agentDir?: string) {
|
||||
// Never persist the literal "undefined" (e.g. when prompt returns undefined and caller used String(key)).
|
||||
const safeKey = key === "undefined" ? "" : key;
|
||||
upsertAuthProfile({
|
||||
profileId: "openrouter:default",
|
||||
credential: {
|
||||
type: "api_key",
|
||||
provider: "openrouter",
|
||||
key,
|
||||
key: safeKey,
|
||||
},
|
||||
agentDir: resolveAuthAgentDir(agentDir),
|
||||
});
|
||||
@@ -231,6 +234,18 @@ export async function setTogetherApiKey(key: string, agentDir?: string) {
|
||||
});
|
||||
}
|
||||
|
||||
export async function setHuggingfaceApiKey(key: string, agentDir?: string) {
|
||||
upsertAuthProfile({
|
||||
profileId: "huggingface:default",
|
||||
credential: {
|
||||
type: "api_key",
|
||||
provider: "huggingface",
|
||||
key,
|
||||
},
|
||||
agentDir: resolveAuthAgentDir(agentDir),
|
||||
});
|
||||
}
|
||||
|
||||
export function setQianfanApiKey(key: string, agentDir?: string) {
|
||||
upsertAuthProfile({
|
||||
profileId: "qianfan:default",
|
||||
|
||||
@@ -7,6 +7,8 @@ export {
|
||||
applyAuthProfileConfig,
|
||||
applyCloudflareAiGatewayConfig,
|
||||
applyCloudflareAiGatewayProviderConfig,
|
||||
applyHuggingfaceConfig,
|
||||
applyHuggingfaceProviderConfig,
|
||||
applyQianfanConfig,
|
||||
applyQianfanProviderConfig,
|
||||
applyKimiCodeConfig,
|
||||
@@ -63,12 +65,14 @@ export {
|
||||
setOpenrouterApiKey,
|
||||
setSyntheticApiKey,
|
||||
setTogetherApiKey,
|
||||
setHuggingfaceApiKey,
|
||||
setVeniceApiKey,
|
||||
setVercelAiGatewayApiKey,
|
||||
setXiaomiApiKey,
|
||||
setZaiApiKey,
|
||||
setXaiApiKey,
|
||||
writeOAuthCredentials,
|
||||
HUGGINGFACE_DEFAULT_MODEL_REF,
|
||||
VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF,
|
||||
XIAOMI_DEFAULT_MODEL_REF,
|
||||
ZAI_DEFAULT_MODEL_REF,
|
||||
|
||||
@@ -450,6 +450,36 @@ describe("onboard (non-interactive): provider auth", () => {
|
||||
});
|
||||
}, 60_000);
|
||||
|
||||
it("infers Together auth choice from --together-api-key and sets default model", async () => {
|
||||
await withOnboardEnv("openclaw-onboard-together-infer-", async ({ configPath, runtime }) => {
|
||||
await runNonInteractive(
|
||||
{
|
||||
nonInteractive: true,
|
||||
togetherApiKey: "together-test-key",
|
||||
skipHealth: true,
|
||||
skipChannels: true,
|
||||
skipSkills: true,
|
||||
json: true,
|
||||
},
|
||||
runtime,
|
||||
);
|
||||
|
||||
const cfg = await readJsonFile<{
|
||||
auth?: { profiles?: Record<string, { provider?: string; mode?: string }> };
|
||||
agents?: { defaults?: { model?: { primary?: string } } };
|
||||
}>(configPath);
|
||||
|
||||
expect(cfg.auth?.profiles?.["together:default"]?.provider).toBe("together");
|
||||
expect(cfg.auth?.profiles?.["together:default"]?.mode).toBe("api_key");
|
||||
expect(cfg.agents?.defaults?.model?.primary).toBe("together/moonshotai/Kimi-K2.5");
|
||||
await expectApiKeyProfile({
|
||||
profileId: "together:default",
|
||||
provider: "together",
|
||||
key: "together-test-key",
|
||||
});
|
||||
});
|
||||
}, 60_000);
|
||||
|
||||
it("configures a custom provider from non-interactive flags", async () => {
|
||||
await withOnboardEnv("openclaw-onboard-custom-provider-", async ({ configPath, runtime }) => {
|
||||
await runNonInteractive(
|
||||
|
||||
@@ -18,6 +18,8 @@ type AuthChoiceFlagOptions = Pick<
|
||||
| "kimiCodeApiKey"
|
||||
| "syntheticApiKey"
|
||||
| "veniceApiKey"
|
||||
| "togetherApiKey"
|
||||
| "huggingfaceApiKey"
|
||||
| "zaiApiKey"
|
||||
| "xiaomiApiKey"
|
||||
| "minimaxApiKey"
|
||||
@@ -44,11 +46,13 @@ const AUTH_CHOICE_FLAG_MAP = [
|
||||
{ flag: "kimiCodeApiKey", authChoice: "kimi-code-api-key", label: "--kimi-code-api-key" },
|
||||
{ flag: "syntheticApiKey", authChoice: "synthetic-api-key", label: "--synthetic-api-key" },
|
||||
{ flag: "veniceApiKey", authChoice: "venice-api-key", label: "--venice-api-key" },
|
||||
{ flag: "togetherApiKey", authChoice: "together-api-key", label: "--together-api-key" },
|
||||
{ flag: "zaiApiKey", authChoice: "zai-api-key", label: "--zai-api-key" },
|
||||
{ flag: "xiaomiApiKey", authChoice: "xiaomi-api-key", label: "--xiaomi-api-key" },
|
||||
{ flag: "xaiApiKey", authChoice: "xai-api-key", label: "--xai-api-key" },
|
||||
{ flag: "minimaxApiKey", authChoice: "minimax-api", label: "--minimax-api-key" },
|
||||
{ flag: "opencodeZenApiKey", authChoice: "opencode-zen", label: "--opencode-zen-api-key" },
|
||||
{ flag: "huggingfaceApiKey", authChoice: "huggingface-api-key", label: "--huggingface-api-key" },
|
||||
{ flag: "litellmApiKey", authChoice: "litellm-api-key", label: "--litellm-api-key" },
|
||||
] satisfies ReadonlyArray<AuthChoiceFlag>;
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
applySyntheticConfig,
|
||||
applyVeniceConfig,
|
||||
applyTogetherConfig,
|
||||
applyHuggingfaceConfig,
|
||||
applyVercelAiGatewayConfig,
|
||||
applyLitellmConfig,
|
||||
applyXaiConfig,
|
||||
@@ -42,6 +43,7 @@ import {
|
||||
setXaiApiKey,
|
||||
setVeniceApiKey,
|
||||
setTogetherApiKey,
|
||||
setHuggingfaceApiKey,
|
||||
setVercelAiGatewayApiKey,
|
||||
setXiaomiApiKey,
|
||||
setZaiApiKey,
|
||||
@@ -644,6 +646,29 @@ export async function applyNonInteractiveAuthChoice(params: {
|
||||
return applyTogetherConfig(nextConfig);
|
||||
}
|
||||
|
||||
if (authChoice === "huggingface-api-key") {
|
||||
const resolved = await resolveNonInteractiveApiKey({
|
||||
provider: "huggingface",
|
||||
cfg: baseConfig,
|
||||
flagValue: opts.huggingfaceApiKey,
|
||||
flagName: "--huggingface-api-key",
|
||||
envVar: "HF_TOKEN",
|
||||
runtime,
|
||||
});
|
||||
if (!resolved) {
|
||||
return null;
|
||||
}
|
||||
if (resolved.source !== "profile") {
|
||||
await setHuggingfaceApiKey(resolved.key);
|
||||
}
|
||||
nextConfig = applyAuthProfileConfig(nextConfig, {
|
||||
profileId: "huggingface:default",
|
||||
provider: "huggingface",
|
||||
mode: "api_key",
|
||||
});
|
||||
return applyHuggingfaceConfig(nextConfig);
|
||||
}
|
||||
|
||||
if (authChoice === "custom-api-key") {
|
||||
try {
|
||||
const customAuth = parseNonInteractiveCustomApiFlags({
|
||||
|
||||
@@ -22,6 +22,7 @@ export type AuthChoice =
|
||||
| "synthetic-api-key"
|
||||
| "venice-api-key"
|
||||
| "together-api-key"
|
||||
| "huggingface-api-key"
|
||||
| "codex-cli"
|
||||
| "apiKey"
|
||||
| "gemini-api-key"
|
||||
@@ -52,6 +53,7 @@ export type AuthChoiceGroupId =
|
||||
| "google"
|
||||
| "copilot"
|
||||
| "openrouter"
|
||||
| "litellm"
|
||||
| "ai-gateway"
|
||||
| "cloudflare-ai-gateway"
|
||||
| "moonshot"
|
||||
@@ -62,6 +64,8 @@ export type AuthChoiceGroupId =
|
||||
| "synthetic"
|
||||
| "venice"
|
||||
| "qwen"
|
||||
| "together"
|
||||
| "huggingface"
|
||||
| "qianfan"
|
||||
| "xai"
|
||||
| "custom";
|
||||
@@ -109,6 +113,7 @@ export type OnboardOptions = {
|
||||
syntheticApiKey?: string;
|
||||
veniceApiKey?: string;
|
||||
togetherApiKey?: string;
|
||||
huggingfaceApiKey?: string;
|
||||
opencodeZenApiKey?: string;
|
||||
xaiApiKey?: string;
|
||||
qianfanApiKey?: string;
|
||||
|
||||
Reference in New Issue
Block a user