mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 07:47:39 +00:00
onboard: support custom provider in non-interactive flow (#14223)
Merged via /review-pr -> /prepare-pr -> /merge-pr.
Prepared head SHA: 5b98d6514e
Co-authored-by: ENCHIGO <38551565+ENCHIGO@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
This commit is contained in:
@@ -45,9 +45,11 @@ export async function resolveNonInteractiveApiKey(params: {
|
||||
flagValue?: string;
|
||||
flagName: string;
|
||||
envVar: string;
|
||||
envVarName?: string;
|
||||
runtime: RuntimeEnv;
|
||||
agentDir?: string;
|
||||
allowProfile?: boolean;
|
||||
required?: boolean;
|
||||
}): Promise<{ key: string; source: NonInteractiveApiKeySource } | null> {
|
||||
const flagKey = normalizeOptionalSecretInput(params.flagValue);
|
||||
if (flagKey) {
|
||||
@@ -59,6 +61,14 @@ export async function resolveNonInteractiveApiKey(params: {
|
||||
return { key: envResolved.apiKey, source: "env" };
|
||||
}
|
||||
|
||||
const explicitEnvVar = params.envVarName?.trim();
|
||||
if (explicitEnvVar) {
|
||||
const explicitEnvKey = normalizeOptionalSecretInput(process.env[explicitEnvVar]);
|
||||
if (explicitEnvKey) {
|
||||
return { key: explicitEnvKey, source: "env" };
|
||||
}
|
||||
}
|
||||
|
||||
if (params.allowProfile ?? true) {
|
||||
const profileKey = await resolveApiKeyFromProfiles({
|
||||
provider: params.provider,
|
||||
@@ -70,6 +80,10 @@ export async function resolveNonInteractiveApiKey(params: {
|
||||
}
|
||||
}
|
||||
|
||||
if (params.required === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const profileHint =
|
||||
params.allowProfile === false ? "" : `, or existing ${params.provider} API-key profile`;
|
||||
params.runtime.error(`Missing ${params.flagName} (or ${params.envVar} in env${profileHint}).`);
|
||||
|
||||
@@ -24,6 +24,9 @@ type AuthChoiceFlagOptions = Pick<
|
||||
| "opencodeZenApiKey"
|
||||
| "xaiApiKey"
|
||||
| "litellmApiKey"
|
||||
| "customBaseUrl"
|
||||
| "customModelId"
|
||||
| "customApiKey"
|
||||
>;
|
||||
|
||||
const AUTH_CHOICE_FLAG_MAP = [
|
||||
@@ -54,15 +57,27 @@ export type AuthChoiceInference = {
|
||||
matches: AuthChoiceFlag[];
|
||||
};
|
||||
|
||||
function hasStringValue(value: unknown): boolean {
|
||||
return typeof value === "string" ? value.trim().length > 0 : Boolean(value);
|
||||
}
|
||||
|
||||
// Infer auth choice from explicit provider API key flags.
|
||||
export function inferAuthChoiceFromFlags(opts: OnboardOptions): AuthChoiceInference {
|
||||
const matches = AUTH_CHOICE_FLAG_MAP.filter(({ flag }) => {
|
||||
const value = opts[flag];
|
||||
if (typeof value === "string") {
|
||||
return value.trim().length > 0;
|
||||
}
|
||||
return Boolean(value);
|
||||
});
|
||||
const matches: AuthChoiceFlag[] = AUTH_CHOICE_FLAG_MAP.filter(({ flag }) =>
|
||||
hasStringValue(opts[flag]),
|
||||
);
|
||||
|
||||
if (
|
||||
hasStringValue(opts.customBaseUrl) ||
|
||||
hasStringValue(opts.customModelId) ||
|
||||
hasStringValue(opts.customApiKey)
|
||||
) {
|
||||
matches.push({
|
||||
flag: "customBaseUrl",
|
||||
authChoice: "custom-api-key",
|
||||
label: "--custom-base-url/--custom-model-id/--custom-api-key",
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
choice: matches[0]?.authChoice,
|
||||
|
||||
@@ -46,6 +46,12 @@ import {
|
||||
setXiaomiApiKey,
|
||||
setZaiApiKey,
|
||||
} from "../../onboard-auth.js";
|
||||
import {
|
||||
applyCustomApiConfig,
|
||||
CustomApiError,
|
||||
parseNonInteractiveCustomApiFlags,
|
||||
resolveCustomProviderId,
|
||||
} from "../../onboard-custom.js";
|
||||
import { applyOpenAIConfig } from "../../openai-model-default.js";
|
||||
import { resolveNonInteractiveApiKey } from "../api-keys.js";
|
||||
|
||||
@@ -594,6 +600,65 @@ export async function applyNonInteractiveAuthChoice(params: {
|
||||
return applyTogetherConfig(nextConfig);
|
||||
}
|
||||
|
||||
if (authChoice === "custom-api-key") {
|
||||
try {
|
||||
const customAuth = parseNonInteractiveCustomApiFlags({
|
||||
baseUrl: opts.customBaseUrl,
|
||||
modelId: opts.customModelId,
|
||||
compatibility: opts.customCompatibility,
|
||||
apiKey: opts.customApiKey,
|
||||
providerId: opts.customProviderId,
|
||||
});
|
||||
const resolvedProviderId = resolveCustomProviderId({
|
||||
config: nextConfig,
|
||||
baseUrl: customAuth.baseUrl,
|
||||
providerId: customAuth.providerId,
|
||||
});
|
||||
const resolvedCustomApiKey = await resolveNonInteractiveApiKey({
|
||||
provider: resolvedProviderId.providerId,
|
||||
cfg: baseConfig,
|
||||
flagValue: customAuth.apiKey,
|
||||
flagName: "--custom-api-key",
|
||||
envVar: "CUSTOM_API_KEY",
|
||||
envVarName: "CUSTOM_API_KEY",
|
||||
runtime,
|
||||
required: false,
|
||||
});
|
||||
const result = applyCustomApiConfig({
|
||||
config: nextConfig,
|
||||
baseUrl: customAuth.baseUrl,
|
||||
modelId: customAuth.modelId,
|
||||
compatibility: customAuth.compatibility,
|
||||
apiKey: resolvedCustomApiKey?.key,
|
||||
providerId: customAuth.providerId,
|
||||
});
|
||||
if (result.providerIdRenamedFrom && result.providerId) {
|
||||
runtime.log(
|
||||
`Custom provider ID "${result.providerIdRenamedFrom}" already exists for a different base URL. Using "${result.providerId}".`,
|
||||
);
|
||||
}
|
||||
return result.config;
|
||||
} catch (err) {
|
||||
if (err instanceof CustomApiError) {
|
||||
switch (err.code) {
|
||||
case "missing_required":
|
||||
case "invalid_compatibility":
|
||||
runtime.error(err.message);
|
||||
break;
|
||||
default:
|
||||
runtime.error(`Invalid custom provider config: ${err.message}`);
|
||||
break;
|
||||
}
|
||||
runtime.exit(1);
|
||||
return null;
|
||||
}
|
||||
const reason = err instanceof Error ? err.message : String(err);
|
||||
runtime.error(`Invalid custom provider config: ${reason}`);
|
||||
runtime.exit(1);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
authChoice === "oauth" ||
|
||||
authChoice === "chutes" ||
|
||||
|
||||
Reference in New Issue
Block a user