From d93cbf8c3242618c95aad00243ee63255515eccf Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Wed, 24 Dec 2025 23:34:23 +0100 Subject: [PATCH] WIP: remove setApiKey, resolveApiKey --- packages/ai/CHANGELOG.md | 6 ++- packages/ai/src/providers/anthropic.ts | 4 +- packages/ai/src/providers/google.ts | 13 ++--- .../ai/src/providers/openai-completions.ts | 4 +- packages/ai/src/providers/openai-responses.ts | 5 +- packages/ai/src/stream.ts | 50 ++----------------- 6 files changed, 21 insertions(+), 61 deletions(-) diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index d82872e7..7d892a02 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +## Breaking Changes +- **setApiKey, resolveApiKey**: removed. You need to create your own api key storage/resolution +- **getApiKey**: renamed to `getApiKeyFromEnv`. Given a provider, checks for the known env variable holding the API key. + ## [0.27.7] - 2025-12-24 ### Fixed @@ -18,7 +22,7 @@ - **Gemini multimodal tool results**: Fixed images in tool results causing flaky/broken responses with Gemini models. For Gemini 3, images are now nested inside `functionResponse.parts` per the [docs](https://ai.google.dev/gemini-api/docs/function-calling#multimodal). For older models (which don't support multimodal function responses), images are sent in a separate user message. -- **Queued message steering**: When `getQueuedMessages` is provided, the agent loop now checks for queued user messages after each tool call and skips remaining tool calls in the current assistant message when a queued message arrives (emitting error tool results). +- **Queued message steering**: When `getQueuedMessages` is provided, the agent loop now checks for queued user messages after each tool call and skips remaining tool calls in the current assistant message when a queued message arrives (emitting error tool results). - **Double API version path in Google provider URL**: Fixed Gemini API calls returning 404 after baseUrl support was added. The SDK was appending its default apiVersion to baseUrl which already included the version path. ([#251](https://github.com/badlogic/pi-mono/pull/251) by [@shellfyred](https://github.com/shellfyred)) diff --git a/packages/ai/src/providers/anthropic.ts b/packages/ai/src/providers/anthropic.ts index f031fe09..f7a52a39 100644 --- a/packages/ai/src/providers/anthropic.ts +++ b/packages/ai/src/providers/anthropic.ts @@ -5,7 +5,7 @@ import type { MessageParam, } from "@anthropic-ai/sdk/resources/messages.js"; import { calculateCost } from "../models.js"; -import { getApiKey } from "../stream.js"; +import { getApiKeyFromEnv } from "../stream.js"; import type { Api, AssistantMessage, @@ -114,7 +114,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = ( }; try { - const apiKey = options?.apiKey ?? getApiKey(model.provider) ?? ""; + const apiKey = options?.apiKey ?? getApiKeyFromEnv(model.provider) ?? ""; const { client, isOAuthToken } = createClient(model, apiKey, options?.interleavedThinking ?? true); const params = buildParams(model, context, isOAuthToken, options); const anthropicStream = client.messages.stream({ ...params, stream: true }, { signal: options?.signal }); diff --git a/packages/ai/src/providers/google.ts b/packages/ai/src/providers/google.ts index 25ff0974..ebd017bb 100644 --- a/packages/ai/src/providers/google.ts +++ b/packages/ai/src/providers/google.ts @@ -6,6 +6,7 @@ import { type ThinkingLevel, } from "@google/genai"; import { calculateCost } from "../models.js"; +import { getApiKeyFromEnv } from "../stream.js"; import type { Api, AssistantMessage, @@ -60,7 +61,8 @@ export const streamGoogle: StreamFunction<"google-generative-ai"> = ( }; try { - const client = createClient(model, options?.apiKey); + const apiKey = options?.apiKey || getApiKeyFromEnv(model.provider) || ""; + const client = createClient(model, apiKey); const params = buildParams(model, context, options); const googleStream = await client.models.generateContentStream(params); @@ -248,15 +250,6 @@ export const streamGoogle: StreamFunction<"google-generative-ai"> = ( }; function createClient(model: Model<"google-generative-ai">, apiKey?: string): GoogleGenAI { - if (!apiKey) { - if (!process.env.GEMINI_API_KEY) { - throw new Error( - "Gemini API key is required. Set GEMINI_API_KEY environment variable or pass it as an argument.", - ); - } - apiKey = process.env.GEMINI_API_KEY; - } - const httpOptions: { baseUrl?: string; apiVersion?: string; headers?: Record } = {}; if (model.baseUrl) { httpOptions.baseUrl = model.baseUrl; diff --git a/packages/ai/src/providers/openai-completions.ts b/packages/ai/src/providers/openai-completions.ts index 8a43b7b7..928b21f9 100644 --- a/packages/ai/src/providers/openai-completions.ts +++ b/packages/ai/src/providers/openai-completions.ts @@ -9,6 +9,7 @@ import type { ChatCompletionToolMessageParam, } from "openai/resources/chat/completions.js"; import { calculateCost } from "../models.js"; +import { getApiKeyFromEnv } from "../stream.js"; import type { AssistantMessage, Context, @@ -98,7 +99,8 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = ( }; try { - const client = createClient(model, context, options?.apiKey); + const apiKey = options?.apiKey || getApiKeyFromEnv(model.provider) || ""; + const client = createClient(model, context, apiKey); const params = buildParams(model, context, options); const openaiStream = await client.chat.completions.create(params, { signal: options?.signal }); stream.push({ type: "start", partial: output }); diff --git a/packages/ai/src/providers/openai-responses.ts b/packages/ai/src/providers/openai-responses.ts index 0fd7e87b..3d7bd410 100644 --- a/packages/ai/src/providers/openai-responses.ts +++ b/packages/ai/src/providers/openai-responses.ts @@ -11,6 +11,7 @@ import type { ResponseReasoningItem, } from "openai/resources/responses/responses.js"; import { calculateCost } from "../models.js"; +import { getApiKeyFromEnv } from "../stream.js"; import type { Api, AssistantMessage, @@ -27,7 +28,6 @@ import type { import { AssistantMessageEventStream } from "../utils/event-stream.js"; import { parseStreamingJson } from "../utils/json-parse.js"; import { sanitizeSurrogates } from "../utils/sanitize-unicode.js"; - import { transformMessages } from "./transorm-messages.js"; /** Fast deterministic hash to shorten long strings */ @@ -82,7 +82,8 @@ export const streamOpenAIResponses: StreamFunction<"openai-responses"> = ( try { // Create OpenAI client - const client = createClient(model, context, options?.apiKey); + const apiKey = options?.apiKey || getApiKeyFromEnv(model.provider) || ""; + const client = createClient(model, context, apiKey); const params = buildParams(model, context, options); const openaiStream = await client.responses.create(params, { signal: options?.signal }); stream.push({ type: "start", partial: output }); diff --git a/packages/ai/src/stream.ts b/packages/ai/src/stream.ts index f2c15050..233a4d45 100644 --- a/packages/ai/src/stream.ts +++ b/packages/ai/src/stream.ts @@ -16,27 +16,14 @@ import type { ReasoningEffort, SimpleStreamOptions, } from "./types.js"; -import { getOAuthApiKey, getOAuthProviderForModelProvider } from "./utils/oauth/index.js"; - -const apiKeys: Map = new Map(); - -export function setApiKey(provider: KnownProvider, key: string): void; -export function setApiKey(provider: string, key: string): void; -export function setApiKey(provider: any, key: string): void { - apiKeys.set(provider, key); -} /** * Get API key from environment variables (sync). * Does NOT check OAuth credentials - use getApiKeyAsync for that. */ -export function getApiKey(provider: KnownProvider): string | undefined; -export function getApiKey(provider: string): string | undefined; -export function getApiKey(provider: any): string | undefined { - // Check explicit keys first - const key = apiKeys.get(provider); - if (key) return key; - +export function getApiKeyFromEnv(provider: KnownProvider): string | undefined; +export function getApiKeyFromEnv(provider: string): string | undefined; +export function getApiKeyFromEnv(provider: any): string | undefined { // Fall back to environment variables if (provider === "github-copilot") { return process.env.COPILOT_GITHUB_TOKEN || process.env.GH_TOKEN || process.env.GITHUB_TOKEN; @@ -58,39 +45,12 @@ export function getApiKey(provider: any): string | undefined { return envVar ? process.env[envVar] : undefined; } -/** - * Resolve API key from OAuth credentials or environment (async). - * Automatically refreshes expired OAuth tokens. - * - * Priority: - * 1. Explicitly set keys (via setApiKey) - * 2. OAuth credentials from ~/.pi/agent/oauth.json - * 3. Environment variables - */ -export async function resolveApiKey(provider: KnownProvider): Promise; -export async function resolveApiKey(provider: string): Promise; -export async function resolveApiKey(provider: any): Promise { - // Check explicit keys first - const key = apiKeys.get(provider); - if (key) return key; - - // Check OAuth credentials (auto-refresh if expired) - const oauthProvider = getOAuthProviderForModelProvider(provider); - if (oauthProvider) { - const oauthKey = await getOAuthApiKey(oauthProvider); - if (oauthKey) return oauthKey; - } - - // Fall back to sync getApiKey for env vars - return getApiKey(provider); -} - export function stream( model: Model, context: Context, options?: OptionsForApi, ): AssistantMessageEventStream { - const apiKey = options?.apiKey || getApiKey(model.provider); + const apiKey = options?.apiKey || getApiKeyFromEnv(model.provider); if (!apiKey) { throw new Error(`No API key for provider: ${model.provider}`); } @@ -139,7 +99,7 @@ export function streamSimple( context: Context, options?: SimpleStreamOptions, ): AssistantMessageEventStream { - const apiKey = options?.apiKey || getApiKey(model.provider); + const apiKey = options?.apiKey || getApiKeyFromEnv(model.provider); if (!apiKey) { throw new Error(`No API key for provider: ${model.provider}`); }