diff --git a/packages/agent/src/transports/ProviderTransport.ts b/packages/agent/src/transports/ProviderTransport.ts index eacb53cd..ebe4d090 100644 --- a/packages/agent/src/transports/ProviderTransport.ts +++ b/packages/agent/src/transports/ProviderTransport.ts @@ -34,15 +34,7 @@ export class ProviderTransport implements AgentTransport { this.options = options; } - private async getModelAndKey(cfg: AgentRunConfig) { - let apiKey: string | undefined; - if (this.options.getApiKey) { - apiKey = await this.options.getApiKey(cfg.model.provider); - } - if (!apiKey) { - throw new Error(`No API key found for provider: ${cfg.model.provider}`); - } - + private getModel(cfg: AgentRunConfig) { let model = cfg.model; if (this.options.corsProxyUrl && cfg.model.baseUrl) { model = { @@ -50,8 +42,7 @@ export class ProviderTransport implements AgentTransport { baseUrl: `${this.options.corsProxyUrl}/?url=${encodeURIComponent(cfg.model.baseUrl)}`, }; } - - return { model, apiKey }; + return model; } private buildContext(messages: Message[], cfg: AgentRunConfig): AgentContext { @@ -62,19 +53,20 @@ export class ProviderTransport implements AgentTransport { }; } - private buildLoopConfig(model: typeof cfg.model, apiKey: string, cfg: AgentRunConfig): AgentLoopConfig { + private buildLoopConfig(model: AgentRunConfig["model"], cfg: AgentRunConfig): AgentLoopConfig { return { model, reasoning: cfg.reasoning, - apiKey, + // Resolve API key per assistant response (important for expiring OAuth tokens) + getApiKey: this.options.getApiKey ? (provider) => this.options.getApiKey?.(provider) : undefined, getQueuedMessages: cfg.getQueuedMessages, }; } async *run(messages: Message[], userMessage: Message, cfg: AgentRunConfig, signal?: AbortSignal) { - const { model, apiKey } = await this.getModelAndKey(cfg); + const model = this.getModel(cfg); const context = this.buildContext(messages, cfg); - const pc = this.buildLoopConfig(model, apiKey, cfg); + const pc = this.buildLoopConfig(model, cfg); for await (const ev of agentLoop(userMessage as unknown as UserMessage, context, pc, signal)) { yield ev; @@ -82,9 +74,9 @@ export class ProviderTransport implements AgentTransport { } async *continue(messages: Message[], cfg: AgentRunConfig, signal?: AbortSignal) { - const { model, apiKey } = await this.getModelAndKey(cfg); + const model = this.getModel(cfg); const context = this.buildContext(messages, cfg); - const pc = this.buildLoopConfig(model, apiKey, cfg); + const pc = this.buildLoopConfig(model, cfg); for await (const ev of agentLoopContinue(context, pc, signal)) { yield ev; diff --git a/packages/ai/src/agent/agent-loop.ts b/packages/ai/src/agent/agent-loop.ts index 17503229..2d844495 100644 --- a/packages/ai/src/agent/agent-loop.ts +++ b/packages/ai/src/agent/agent-loop.ts @@ -174,7 +174,12 @@ async function streamAssistantResponse( // Use custom stream function if provided, otherwise use default streamSimple const streamFunction = streamFn || streamSimple; - const response = await streamFunction(config.model, processedContext, { ...config, signal }); + + // Resolve API key for every assistant response (important for expiring tokens) + const resolvedApiKey = + (config.getApiKey ? await config.getApiKey(config.model.provider) : undefined) || config.apiKey; + + const response = await streamFunction(config.model, processedContext, { ...config, apiKey: resolvedApiKey, signal }); let partialMessage: AssistantMessage | null = null; let addedPartial = false; diff --git a/packages/ai/src/agent/types.ts b/packages/ai/src/agent/types.ts index a2efc011..c0cb2df6 100644 --- a/packages/ai/src/agent/types.ts +++ b/packages/ai/src/agent/types.ts @@ -85,6 +85,21 @@ export interface QueuedMessage { // Configuration for agent loop execution export interface AgentLoopConfig extends SimpleStreamOptions { model: Model; + + /** + * Optional hook to resolve an API key dynamically for each LLM call. + * + * This is useful for short-lived OAuth tokens (e.g. GitHub Copilot) that may + * expire during long-running tool execution phases. + * + * The agent loop will call this before each assistant response and pass the + * returned value as `apiKey` to `streamSimple()` (or a custom `streamFn`). + * + * If it returns `undefined`, the loop falls back to `config.apiKey`, and then + * to `streamSimple()`'s own provider key lookup (setApiKey/env vars). + */ + getApiKey?: (provider: string) => Promise | string | undefined; + preprocessor?: (messages: AgentContext["messages"], abortSignal?: AbortSignal) => Promise; getQueuedMessages?: () => Promise[]>; } diff --git a/packages/web-ui/src/agent/transports/ProviderTransport.ts b/packages/web-ui/src/agent/transports/ProviderTransport.ts index 68b498b9..b16991d1 100644 --- a/packages/web-ui/src/agent/transports/ProviderTransport.ts +++ b/packages/web-ui/src/agent/transports/ProviderTransport.ts @@ -15,7 +15,7 @@ import type { AgentRunConfig, AgentTransport } from "./types.js"; * Uses CORS proxy only for providers that require it (Anthropic OAuth, Z-AI). */ export class ProviderTransport implements AgentTransport { - private async getModelAndKey(cfg: AgentRunConfig) { + private async getModel(cfg: AgentRunConfig) { const apiKey = await getAppStorage().providerKeys.get(cfg.model.provider); if (!apiKey) { throw new Error("no-api-key"); @@ -25,7 +25,7 @@ export class ProviderTransport implements AgentTransport { const proxyUrl = await getAppStorage().settings.get("proxy.url"); const model = applyProxyIfNeeded(cfg.model, apiKey, proxyEnabled ? proxyUrl || undefined : undefined); - return { model, apiKey }; + return model; } private buildContext(messages: Message[], cfg: AgentRunConfig): AgentContext { @@ -36,19 +36,23 @@ export class ProviderTransport implements AgentTransport { }; } - private buildLoopConfig(model: typeof cfg.model, apiKey: string, cfg: AgentRunConfig): AgentLoopConfig { + private buildLoopConfig(model: AgentRunConfig["model"], cfg: AgentRunConfig): AgentLoopConfig { return { model, reasoning: cfg.reasoning, - apiKey, + // Resolve API key per assistant response (important for expiring OAuth tokens) + getApiKey: async (provider: string) => { + const key = await getAppStorage().providerKeys.get(provider); + return key ?? undefined; // Convert null to undefined for type compatibility + }, getQueuedMessages: cfg.getQueuedMessages, }; } async *run(messages: Message[], userMessage: Message, cfg: AgentRunConfig, signal?: AbortSignal) { - const { model, apiKey } = await this.getModelAndKey(cfg); + const model = await this.getModel(cfg); const context = this.buildContext(messages, cfg); - const pc = this.buildLoopConfig(model, apiKey, cfg); + const pc = this.buildLoopConfig(model, cfg); for await (const ev of agentLoop(userMessage as unknown as UserMessage, context, pc, signal)) { yield ev; @@ -56,9 +60,9 @@ export class ProviderTransport implements AgentTransport { } async *continue(messages: Message[], cfg: AgentRunConfig, signal?: AbortSignal) { - const { model, apiKey } = await this.getModelAndKey(cfg); + const model = await this.getModel(cfg); const context = this.buildContext(messages, cfg); - const pc = this.buildLoopConfig(model, apiKey, cfg); + const pc = this.buildLoopConfig(model, cfg); for await (const ev of agentLoopContinue(context, pc, signal)) { yield ev;