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:
ENCHIGO
2026-02-12 03:48:45 +08:00
committed by GitHub
parent c8d9733e41
commit 029b77c85b
13 changed files with 791 additions and 90 deletions

View File

@@ -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}).`);

View File

@@ -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,

View File

@@ -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" ||