feat(agent): support custom API key and base URL configuration

Add apiKey and baseUrl options to AgentOptions, with support for:
- Explicit values via CLI args (--api-key, --base-url)
- Provider-specific environment variables (e.g., OPENAI_BASE_URL)
- Generic fallback format (PROVIDER_BASE_URL)

Priority: CLI args > env vars > library defaults.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
yushen
2026-01-30 13:12:24 +08:00
parent 6f8c9ef383
commit 9858576abe
3 changed files with 71 additions and 6 deletions

View File

@@ -5,6 +5,8 @@ type CliOptions = {
profile?: string | undefined;
provider?: string | undefined;
model?: string | undefined;
apiKey?: string | undefined;
baseUrl?: string | undefined;
system?: string | undefined;
thinking?: string | undefined;
cwd?: string | undefined;
@@ -21,6 +23,8 @@ function printUsage() {
console.log(" --profile ID Load agent profile (identity, soul, tools, memory)");
console.log(" --provider NAME LLM provider (e.g., openai, anthropic, kimi)");
console.log(" --model NAME Model name");
console.log(" --api-key KEY API key (overrides environment variable)");
console.log(" --base-url URL Custom base URL for the provider");
console.log(" --system TEXT System prompt (ignored if --profile is set)");
console.log(" --thinking LEVEL Thinking level");
console.log(" --cwd DIR Working directory for commands");
@@ -53,6 +57,14 @@ function parseArgs(argv: string[]) {
opts.model = args.shift();
continue;
}
if (arg === "--api-key") {
opts.apiKey = args.shift();
continue;
}
if (arg === "--base-url") {
opts.baseUrl = args.shift();
continue;
}
if (arg === "--system") {
opts.system = args.shift();
continue;
@@ -112,6 +124,8 @@ async function main() {
profileId: opts.profile,
provider: opts.provider,
model: opts.model,
apiKey: opts.apiKey,
baseUrl: opts.baseUrl,
systemPrompt: opts.system,
thinkingLevel: opts.thinking as any,
cwd: opts.cwd,

View File

@@ -13,9 +13,12 @@ import {
} from "./context-window/index.js";
/**
* Get API Key based on provider
* Get API Key based on provider.
* Priority: explicit key > provider-specific env var > generic env var format.
*/
function resolveApiKey(provider: string): string | undefined {
function resolveApiKey(provider: string, explicitKey?: string): string | undefined {
if (explicitKey) return explicitKey;
const providerEnvMap: Record<string, string> = {
openai: "OPENAI_API_KEY",
anthropic: "ANTHROPIC_API_KEY",
@@ -39,6 +42,36 @@ function resolveApiKey(provider: string): string | undefined {
return process.env[`${normalizedProvider}_API_KEY`];
}
/**
* Get Base URL based on provider.
* Priority: explicit URL > provider-specific env var > generic env var format.
*/
function resolveBaseUrl(provider: string, explicitUrl?: string): string | undefined {
if (explicitUrl) return explicitUrl;
const providerEnvMap: Record<string, string> = {
openai: "OPENAI_BASE_URL",
anthropic: "ANTHROPIC_BASE_URL",
google: "GOOGLE_BASE_URL",
"google-genai": "GOOGLE_BASE_URL",
kimi: "MOONSHOT_BASE_URL",
"kimi-coding": "MOONSHOT_BASE_URL",
deepseek: "DEEPSEEK_BASE_URL",
groq: "GROQ_BASE_URL",
mistral: "MISTRAL_BASE_URL",
together: "TOGETHER_BASE_URL",
};
const envVar = providerEnvMap[provider];
if (envVar) {
return process.env[envVar];
}
// Try generic format: PROVIDER_BASE_URL
const normalizedProvider = provider.toUpperCase().replace(/-/g, "_");
return process.env[`${normalizedProvider}_BASE_URL`];
}
export class Agent {
private readonly agent: PiAgentCore;
private readonly output;
@@ -57,7 +90,15 @@ export class Agent {
this.output = createAgentOutput({ stdout, stderr });
this.debug = options.debug ?? false;
this.agent = new PiAgentCore();
// Resolve provider for API key and base URL
const resolvedProvider = options.provider ?? "kimi-coding";
const apiKey = resolveApiKey(resolvedProvider, options.apiKey);
this.agent = new PiAgentCore(
apiKey
? { getApiKey: (_provider: string) => apiKey }
: {},
);
// Load Agent Profile (if profileId is specified)
let systemPrompt: string | undefined;
@@ -101,12 +142,18 @@ export class Agent {
return tempSession.getMeta();
})();
const model = options.provider && options.model ? resolveModel(options) : resolveModel({
let model = options.provider && options.model ? resolveModel(options) : resolveModel({
...options,
provider: storedMeta?.provider,
model: storedMeta?.model,
});
// Override base URL if provided via options or environment variable
const baseUrl = resolveBaseUrl(model.provider, options.baseUrl);
if (baseUrl) {
model = { ...model, baseUrl };
}
// === Context Window Guard ===
this.contextWindowGuard = checkContextWindow({
modelContextWindow: model.contextWindow,
@@ -133,7 +180,7 @@ export class Agent {
const compactionMode = options.compactionMode ?? "tokens"; // 默认使用 token 模式
// 获取 API Key用于 summary 模式)
const apiKey = compactionMode === "summary" ? resolveApiKey(model.provider) : undefined;
const summaryApiKey = compactionMode === "summary" ? resolveApiKey(model.provider, options.apiKey) : undefined;
// 创建 SessionManager带 context window 配置)
this.session = new SessionManager({
@@ -147,7 +194,7 @@ export class Agent {
minKeepMessages: options.minKeepMessages,
// Summary 模式参数
model: compactionMode === "summary" ? model : undefined,
apiKey,
apiKey: summaryApiKey,
customInstructions: options.summaryInstructions,
});

View File

@@ -17,6 +17,10 @@ export type AgentOptions = {
profileBaseDir?: string | undefined;
provider?: string | undefined;
model?: string | undefined;
/** Custom API key (overrides environment variable) */
apiKey?: string | undefined;
/** Custom base URL for the provider endpoint */
baseUrl?: string | undefined;
/** System prompt, if profileId is set will auto-construct from profile */
systemPrompt?: string | undefined;
thinkingLevel?: ThinkingLevel | undefined;