From 62d6fa9e26223f325dd13170e1559586f390e02f Mon Sep 17 00:00:00 2001 From: JackChen Date: Thu, 2 Apr 2026 19:33:10 +0800 Subject: [PATCH] feat: add baseURL and apiKey support for OpenAI-compatible APIs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enable connecting to any OpenAI-compatible API (Ollama, vLLM, LM Studio, etc.) by adding baseURL and apiKey fields to AgentConfig and OrchestratorConfig, threaded through to adapter constructors. - OpenAIAdapter and AnthropicAdapter accept optional baseURL - createAdapter() forwards baseURL to both adapters, warns if used with copilot - All execution paths (runAgent, runTeam coordinator, buildPool) merge defaults - Fully backward compatible — omitting new fields preserves existing behavior --- src/agent/agent.ts | 2 +- src/llm/adapter.ts | 9 +++++++-- src/llm/anthropic.ts | 3 ++- src/llm/openai.ts | 3 ++- src/orchestrator/orchestrator.ts | 18 +++++++++++++++--- src/types.ts | 10 ++++++++++ 6 files changed, 37 insertions(+), 8 deletions(-) diff --git a/src/agent/agent.ts b/src/agent/agent.ts index 1dc530d..4ef392e 100644 --- a/src/agent/agent.ts +++ b/src/agent/agent.ts @@ -109,7 +109,7 @@ export class Agent { } const provider = this.config.provider ?? 'anthropic' - const adapter = await createAdapter(provider) + const adapter = await createAdapter(provider, this.config.apiKey, this.config.baseURL) const runnerOptions: RunnerOptions = { model: this.config.model, diff --git a/src/llm/adapter.ts b/src/llm/adapter.ts index f641edd..cbe5b4f 100644 --- a/src/llm/adapter.ts +++ b/src/llm/adapter.ts @@ -54,24 +54,29 @@ export type SupportedProvider = 'anthropic' | 'copilot' | 'openai' * * @param provider - Which LLM provider to target. * @param apiKey - Optional API key override; falls back to env var. + * @param baseURL - Optional base URL for OpenAI-compatible APIs (Ollama, vLLM, etc.). * @throws {Error} When the provider string is not recognised. */ export async function createAdapter( provider: SupportedProvider, apiKey?: string, + baseURL?: string, ): Promise { switch (provider) { case 'anthropic': { const { AnthropicAdapter } = await import('./anthropic.js') - return new AnthropicAdapter(apiKey) + return new AnthropicAdapter(apiKey, baseURL) } case 'copilot': { + if (baseURL) { + console.warn('[open-multi-agent] baseURL is not supported for the copilot provider and will be ignored.') + } const { CopilotAdapter } = await import('./copilot.js') return new CopilotAdapter(apiKey) } case 'openai': { const { OpenAIAdapter } = await import('./openai.js') - return new OpenAIAdapter(apiKey) + return new OpenAIAdapter(apiKey, baseURL) } default: { // The `never` cast here makes TypeScript enforce exhaustiveness. diff --git a/src/llm/anthropic.ts b/src/llm/anthropic.ts index 6b91fd4..fd912d5 100644 --- a/src/llm/anthropic.ts +++ b/src/llm/anthropic.ts @@ -189,9 +189,10 @@ export class AnthropicAdapter implements LLMAdapter { readonly #client: Anthropic - constructor(apiKey?: string) { + constructor(apiKey?: string, baseURL?: string) { this.#client = new Anthropic({ apiKey: apiKey ?? process.env['ANTHROPIC_API_KEY'], + baseURL, }) } diff --git a/src/llm/openai.ts b/src/llm/openai.ts index a3c2ab9..568f94e 100644 --- a/src/llm/openai.ts +++ b/src/llm/openai.ts @@ -69,9 +69,10 @@ export class OpenAIAdapter implements LLMAdapter { readonly #client: OpenAI - constructor(apiKey?: string) { + constructor(apiKey?: string, baseURL?: string) { this.#client = new OpenAI({ apiKey: apiKey ?? process.env['OPENAI_API_KEY'], + baseURL, }) } diff --git a/src/orchestrator/orchestrator.ts b/src/orchestrator/orchestrator.ts index 0332969..1da8fb5 100644 --- a/src/orchestrator/orchestrator.ts +++ b/src/orchestrator/orchestrator.ts @@ -341,8 +341,8 @@ async function buildTaskPrompt(task: Task, team: Team): Promise { */ export class OpenMultiAgent { private readonly config: Required< - Omit - > & Pick + Omit + > & Pick private readonly teams: Map = new Map() private completedTaskCount = 0 @@ -360,6 +360,8 @@ export class OpenMultiAgent { maxConcurrency: config.maxConcurrency ?? DEFAULT_MAX_CONCURRENCY, defaultModel: config.defaultModel ?? DEFAULT_MODEL, defaultProvider: config.defaultProvider ?? 'anthropic', + defaultBaseURL: config.defaultBaseURL, + defaultApiKey: config.defaultApiKey, onProgress: config.onProgress, } } @@ -405,7 +407,13 @@ export class OpenMultiAgent { * @param prompt - The user prompt to send. */ async runAgent(config: AgentConfig, prompt: string): Promise { - const agent = buildAgent(config) + const effective: AgentConfig = { + ...config, + provider: config.provider ?? this.config.defaultProvider, + baseURL: config.baseURL ?? this.config.defaultBaseURL, + apiKey: config.apiKey ?? this.config.defaultApiKey, + } + const agent = buildAgent(effective) this.config.onProgress?.({ type: 'agent_start', agent: config.name, @@ -462,6 +470,8 @@ export class OpenMultiAgent { name: 'coordinator', model: this.config.defaultModel, provider: this.config.defaultProvider, + baseURL: this.config.defaultBaseURL, + apiKey: this.config.defaultApiKey, systemPrompt: this.buildCoordinatorSystemPrompt(agentConfigs), maxTurns: 3, } @@ -792,6 +802,8 @@ export class OpenMultiAgent { ...config, model: config.model, provider: config.provider ?? this.config.defaultProvider, + baseURL: config.baseURL ?? this.config.defaultBaseURL, + apiKey: config.apiKey ?? this.config.defaultApiKey, } pool.add(buildAgent(effective)) } diff --git a/src/types.ts b/src/types.ts index 146fb41..bd44065 100644 --- a/src/types.ts +++ b/src/types.ts @@ -187,6 +187,14 @@ export interface AgentConfig { readonly name: string readonly model: string readonly provider?: 'anthropic' | 'copilot' | 'openai' + /** + * Custom base URL for OpenAI-compatible APIs (Ollama, vLLM, LM Studio, etc.). + * Note: local servers that don't require auth still need `apiKey` set to a + * non-empty placeholder (e.g. `'ollama'`) because the OpenAI SDK validates it. + */ + readonly baseURL?: string + /** API key override; falls back to the provider's standard env var. */ + readonly apiKey?: string readonly systemPrompt?: string /** Names of tools (from the tool registry) available to this agent. */ readonly tools?: readonly string[] @@ -286,6 +294,8 @@ export interface OrchestratorConfig { readonly maxConcurrency?: number readonly defaultModel?: string readonly defaultProvider?: 'anthropic' | 'copilot' | 'openai' + readonly defaultBaseURL?: string + readonly defaultApiKey?: string onProgress?: (event: OrchestratorEvent) => void }