diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index bf4a6035bd8..5b7003ace6c 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -809,6 +809,12 @@ export namespace Config { options: z .object({ apiKey: z.string().optional(), + apiKeyCommand: z + .array(z.string()) + .optional() + .describe( + "Command to execute to get the API key dynamically. The command should output the API key to stdout. Example: ['my-auth-cli', 'get-token']", + ), baseURL: z.string().optional(), enterpriseUrl: z.string().optional().describe("GitHub Enterprise URL for copilot authentication"), setCacheKey: z.boolean().optional().describe("Enable promptCacheKey for this provider (default false)"), diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 3b76b1e029d..42dec2d945c 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -64,6 +64,30 @@ export namespace Provider { "@ai-sdk/github-copilot": createGitHubCopilotOpenAICompatible, } + /** + * Executes an apiKeyCommand and returns the output as the API key. + * This allows dynamic API key retrieval from credential helpers or token services. + */ + async function resolveApiKeyCommand(command: string[]): Promise { + if (!command || command.length === 0) return undefined + try { + const proc = Bun.spawn(command, { + stdout: "pipe", + stderr: "pipe", + }) + const code = await proc.exited + if (code !== 0) { + log.warn("apiKeyCommand failed", { command, exitCode: code }) + return undefined + } + const stdout = await new Response(proc.stdout).text() + return stdout.trim() + } catch (e) { + log.warn("apiKeyCommand execution error", { command, error: e }) + return undefined + } + } + type CustomModelLoader = (sdk: any, modelID: string, options?: Record) => Promise type CustomLoader = (provider: Info) => Promise<{ autoload: boolean @@ -921,6 +945,12 @@ export namespace Provider { if (!options["baseURL"]) options["baseURL"] = model.api.url if (options["apiKey"] === undefined && provider.key) options["apiKey"] = provider.key + // If no apiKey yet, try apiKeyCommand + if (options["apiKey"] === undefined && options["apiKeyCommand"]) { + const key = await resolveApiKeyCommand(options["apiKeyCommand"]) + if (key) options["apiKey"] = key + delete options["apiKeyCommand"] // Don't pass command to SDK + } if (model.headers) options["headers"] = { ...options["headers"],