mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-10 21:14:31 +00:00
feat(secrets): expand onboarding secret-ref flows and custom-provider parity
This commit is contained in:
committed by
Peter Steinberger
parent
e8637c79b3
commit
5e3a86fd2f
@@ -2,12 +2,18 @@ import { DEFAULT_PROVIDER } from "../agents/defaults.js";
|
||||
import { buildModelAliasIndex, modelKey } from "../agents/model-selection.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import type { ModelProviderConfig } from "../config/types.models.js";
|
||||
import { isSecretRef, type SecretInput } from "../config/types.secrets.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import { fetchWithTimeout } from "../utils/fetch-timeout.js";
|
||||
import { normalizeOptionalSecretInput } from "../utils/normalize-secret-input.js";
|
||||
import {
|
||||
normalizeSecretInput,
|
||||
normalizeOptionalSecretInput,
|
||||
} from "../utils/normalize-secret-input.js";
|
||||
import type { WizardPrompter } from "../wizard/prompts.js";
|
||||
import { ensureApiKeyFromEnvOrPrompt } from "./auth-choice.apply-helpers.js";
|
||||
import { applyPrimaryModel } from "./model-picker.js";
|
||||
import { normalizeAlias } from "./models/shared.js";
|
||||
import type { SecretInputMode } from "./onboard-types.js";
|
||||
|
||||
const DEFAULT_OLLAMA_BASE_URL = "http://127.0.0.1:11434/v1";
|
||||
const DEFAULT_CONTEXT_WINDOW = 4096;
|
||||
@@ -63,7 +69,7 @@ export type ApplyCustomApiConfigParams = {
|
||||
baseUrl: string;
|
||||
modelId: string;
|
||||
compatibility: CustomApiCompatibility;
|
||||
apiKey?: string;
|
||||
apiKey?: SecretInput;
|
||||
providerId?: string;
|
||||
alias?: string;
|
||||
};
|
||||
@@ -246,6 +252,13 @@ type VerificationResult = {
|
||||
error?: unknown;
|
||||
};
|
||||
|
||||
function normalizeOptionalProviderApiKey(value: unknown): SecretInput | undefined {
|
||||
if (isSecretRef(value)) {
|
||||
return value;
|
||||
}
|
||||
return normalizeOptionalSecretInput(value);
|
||||
}
|
||||
|
||||
function resolveVerificationEndpoint(params: {
|
||||
baseUrl: string;
|
||||
modelId: string;
|
||||
@@ -338,8 +351,10 @@ async function requestAnthropicVerification(params: {
|
||||
|
||||
async function promptBaseUrlAndKey(params: {
|
||||
prompter: WizardPrompter;
|
||||
config: OpenClawConfig;
|
||||
secretInputMode?: SecretInputMode;
|
||||
initialBaseUrl?: string;
|
||||
}): Promise<{ baseUrl: string; apiKey: string }> {
|
||||
}): Promise<{ baseUrl: string; apiKey?: SecretInput; resolvedApiKey: string }> {
|
||||
const baseUrlInput = await params.prompter.text({
|
||||
message: "API Base URL",
|
||||
initialValue: params.initialBaseUrl ?? DEFAULT_OLLAMA_BASE_URL,
|
||||
@@ -353,12 +368,27 @@ async function promptBaseUrlAndKey(params: {
|
||||
}
|
||||
},
|
||||
});
|
||||
const apiKeyInput = await params.prompter.text({
|
||||
message: "API Key (leave blank if not required)",
|
||||
placeholder: "sk-...",
|
||||
initialValue: "",
|
||||
const baseUrl = baseUrlInput.trim();
|
||||
const providerHint = buildEndpointIdFromUrl(baseUrl) || "custom";
|
||||
let apiKeyInput: SecretInput | undefined;
|
||||
const resolvedApiKey = await ensureApiKeyFromEnvOrPrompt({
|
||||
config: params.config,
|
||||
provider: providerHint,
|
||||
envLabel: "CUSTOM_API_KEY",
|
||||
promptMessage: "API Key (leave blank if not required)",
|
||||
normalize: normalizeSecretInput,
|
||||
validate: () => undefined,
|
||||
prompter: params.prompter,
|
||||
secretInputMode: params.secretInputMode,
|
||||
setCredential: async (apiKey) => {
|
||||
apiKeyInput = apiKey;
|
||||
},
|
||||
});
|
||||
return { baseUrl: baseUrlInput.trim(), apiKey: apiKeyInput.trim() };
|
||||
return {
|
||||
baseUrl,
|
||||
apiKey: normalizeOptionalProviderApiKey(apiKeyInput),
|
||||
resolvedApiKey: normalizeSecretInput(resolvedApiKey),
|
||||
};
|
||||
}
|
||||
|
||||
type CustomApiRetryChoice = "baseUrl" | "model" | "both";
|
||||
@@ -386,22 +416,27 @@ async function promptCustomApiModelId(prompter: WizardPrompter): Promise<string>
|
||||
|
||||
async function applyCustomApiRetryChoice(params: {
|
||||
prompter: WizardPrompter;
|
||||
config: OpenClawConfig;
|
||||
secretInputMode?: SecretInputMode;
|
||||
retryChoice: CustomApiRetryChoice;
|
||||
current: { baseUrl: string; apiKey: string; modelId: string };
|
||||
}): Promise<{ baseUrl: string; apiKey: string; modelId: string }> {
|
||||
let { baseUrl, apiKey, modelId } = params.current;
|
||||
current: { baseUrl: string; apiKey?: SecretInput; resolvedApiKey: string; modelId: string };
|
||||
}): Promise<{ baseUrl: string; apiKey?: SecretInput; resolvedApiKey: string; modelId: string }> {
|
||||
let { baseUrl, apiKey, resolvedApiKey, modelId } = params.current;
|
||||
if (params.retryChoice === "baseUrl" || params.retryChoice === "both") {
|
||||
const retryInput = await promptBaseUrlAndKey({
|
||||
prompter: params.prompter,
|
||||
config: params.config,
|
||||
secretInputMode: params.secretInputMode,
|
||||
initialBaseUrl: baseUrl,
|
||||
});
|
||||
baseUrl = retryInput.baseUrl;
|
||||
apiKey = retryInput.apiKey;
|
||||
resolvedApiKey = retryInput.resolvedApiKey;
|
||||
}
|
||||
if (params.retryChoice === "model" || params.retryChoice === "both") {
|
||||
modelId = await promptCustomApiModelId(params.prompter);
|
||||
}
|
||||
return { baseUrl, apiKey, modelId };
|
||||
return { baseUrl, apiKey, resolvedApiKey, modelId };
|
||||
}
|
||||
|
||||
function resolveProviderApi(
|
||||
@@ -542,7 +577,8 @@ export function applyCustomApiConfig(params: ApplyCustomApiConfigParams): Custom
|
||||
const mergedModels = hasModel ? existingModels : [...existingModels, nextModel];
|
||||
const { apiKey: existingApiKey, ...existingProviderRest } = existingProvider ?? {};
|
||||
const normalizedApiKey =
|
||||
normalizeOptionalSecretInput(params.apiKey) ?? normalizeOptionalSecretInput(existingApiKey);
|
||||
normalizeOptionalProviderApiKey(params.apiKey) ??
|
||||
normalizeOptionalProviderApiKey(existingApiKey);
|
||||
|
||||
let config: OpenClawConfig = {
|
||||
...params.config,
|
||||
@@ -596,12 +632,18 @@ export async function promptCustomApiConfig(params: {
|
||||
prompter: WizardPrompter;
|
||||
runtime: RuntimeEnv;
|
||||
config: OpenClawConfig;
|
||||
secretInputMode?: SecretInputMode;
|
||||
}): Promise<CustomApiResult> {
|
||||
const { prompter, runtime, config } = params;
|
||||
|
||||
const baseInput = await promptBaseUrlAndKey({ prompter });
|
||||
const baseInput = await promptBaseUrlAndKey({
|
||||
prompter,
|
||||
config,
|
||||
secretInputMode: params.secretInputMode,
|
||||
});
|
||||
let baseUrl = baseInput.baseUrl;
|
||||
let apiKey = baseInput.apiKey;
|
||||
let resolvedApiKey = baseInput.resolvedApiKey;
|
||||
|
||||
const compatibilityChoice = await prompter.select({
|
||||
message: "Endpoint compatibility",
|
||||
@@ -621,13 +663,21 @@ export async function promptCustomApiConfig(params: {
|
||||
let verifiedFromProbe = false;
|
||||
if (!compatibility) {
|
||||
const probeSpinner = prompter.progress("Detecting endpoint type...");
|
||||
const openaiProbe = await requestOpenAiVerification({ baseUrl, apiKey, modelId });
|
||||
const openaiProbe = await requestOpenAiVerification({
|
||||
baseUrl,
|
||||
apiKey: resolvedApiKey,
|
||||
modelId,
|
||||
});
|
||||
if (openaiProbe.ok) {
|
||||
probeSpinner.stop("Detected OpenAI-compatible endpoint.");
|
||||
compatibility = "openai";
|
||||
verifiedFromProbe = true;
|
||||
} else {
|
||||
const anthropicProbe = await requestAnthropicVerification({ baseUrl, apiKey, modelId });
|
||||
const anthropicProbe = await requestAnthropicVerification({
|
||||
baseUrl,
|
||||
apiKey: resolvedApiKey,
|
||||
modelId,
|
||||
});
|
||||
if (anthropicProbe.ok) {
|
||||
probeSpinner.stop("Detected Anthropic-compatible endpoint.");
|
||||
compatibility = "anthropic";
|
||||
@@ -639,10 +689,12 @@ export async function promptCustomApiConfig(params: {
|
||||
"Endpoint detection",
|
||||
);
|
||||
const retryChoice = await promptCustomApiRetryChoice(prompter);
|
||||
({ baseUrl, apiKey, modelId } = await applyCustomApiRetryChoice({
|
||||
({ baseUrl, apiKey, resolvedApiKey, modelId } = await applyCustomApiRetryChoice({
|
||||
prompter,
|
||||
config,
|
||||
secretInputMode: params.secretInputMode,
|
||||
retryChoice,
|
||||
current: { baseUrl, apiKey, modelId },
|
||||
current: { baseUrl, apiKey, resolvedApiKey, modelId },
|
||||
}));
|
||||
continue;
|
||||
}
|
||||
@@ -656,8 +708,8 @@ export async function promptCustomApiConfig(params: {
|
||||
const verifySpinner = prompter.progress("Verifying...");
|
||||
const result =
|
||||
compatibility === "anthropic"
|
||||
? await requestAnthropicVerification({ baseUrl, apiKey, modelId })
|
||||
: await requestOpenAiVerification({ baseUrl, apiKey, modelId });
|
||||
? await requestAnthropicVerification({ baseUrl, apiKey: resolvedApiKey, modelId })
|
||||
: await requestOpenAiVerification({ baseUrl, apiKey: resolvedApiKey, modelId });
|
||||
if (result.ok) {
|
||||
verifySpinner.stop("Verification successful.");
|
||||
break;
|
||||
@@ -668,10 +720,12 @@ export async function promptCustomApiConfig(params: {
|
||||
verifySpinner.stop(`Verification failed: ${formatVerificationError(result.error)}`);
|
||||
}
|
||||
const retryChoice = await promptCustomApiRetryChoice(prompter);
|
||||
({ baseUrl, apiKey, modelId } = await applyCustomApiRetryChoice({
|
||||
({ baseUrl, apiKey, resolvedApiKey, modelId } = await applyCustomApiRetryChoice({
|
||||
prompter,
|
||||
config,
|
||||
secretInputMode: params.secretInputMode,
|
||||
retryChoice,
|
||||
current: { baseUrl, apiKey, modelId },
|
||||
current: { baseUrl, apiKey, resolvedApiKey, modelId },
|
||||
}));
|
||||
if (compatibilityChoice === "unknown") {
|
||||
compatibility = null;
|
||||
|
||||
Reference in New Issue
Block a user