diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 6b7b6542..bda449eb 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Breaking Changes + +- Changed direct provider stream functions to require explicit `options.apiKey`; top-level `stream*`/`complete*` helpers still resolve built-in environment auth. + ### Fixed - Fixed OpenCode Go Kimi K2.6 thinking requests to send `thinking` objects instead of invalid string values, and fixed OpenCode Zen Grok Build thinking requests to omit unsupported `reasoning_effort` ([#5169](https://github.com/earendil-works/pi/issues/5169)). diff --git a/packages/ai/src/providers/anthropic.ts b/packages/ai/src/providers/anthropic.ts index 2de6198f..74d4be38 100644 --- a/packages/ai/src/providers/anthropic.ts +++ b/packages/ai/src/providers/anthropic.ts @@ -6,7 +6,6 @@ import type { MessageParam, RawMessageStreamEvent, } from "@anthropic-ai/sdk/resources/messages.js"; -import { getEnvApiKey } from "../env-api-keys.ts"; import { calculateCost } from "../models.ts"; import type { AnthropicMessagesCompat, @@ -479,7 +478,10 @@ export const streamAnthropic: StreamFunction<"anthropic-messages", AnthropicOpti client = options.client; isOAuth = false; } else { - const apiKey = options?.apiKey ?? getEnvApiKey(model.provider) ?? ""; + const apiKey = options?.apiKey; + if (!apiKey) { + throw new Error(`No API key for provider: ${model.provider}`); + } let copilotDynamicHeaders: Record | undefined; if (model.provider === "github-copilot") { @@ -735,7 +737,7 @@ export const streamSimpleAnthropic: StreamFunction<"anthropic-messages", SimpleS context: Context, options?: SimpleStreamOptions, ): AssistantMessageEventStream => { - const apiKey = options?.apiKey || getEnvApiKey(model.provider); + const apiKey = options?.apiKey; if (!apiKey) { throw new Error(`No API key for provider: ${model.provider}`); } diff --git a/packages/ai/src/providers/azure-openai-responses.ts b/packages/ai/src/providers/azure-openai-responses.ts index 770d5930..921a482e 100644 --- a/packages/ai/src/providers/azure-openai-responses.ts +++ b/packages/ai/src/providers/azure-openai-responses.ts @@ -1,6 +1,5 @@ import { AzureOpenAI } from "openai"; import type { ResponseCreateParamsStreaming } from "openai/resources/responses/responses.js"; -import { getEnvApiKey } from "../env-api-keys.ts"; import { clampThinkingLevel } from "../models.ts"; import type { Api, @@ -101,7 +100,10 @@ export const streamAzureOpenAIResponses: StreamFunction<"azure-openai-responses" try { // Create Azure OpenAI client - const apiKey = options?.apiKey || getEnvApiKey(model.provider) || ""; + const apiKey = options?.apiKey; + if (!apiKey) { + throw new Error(`No API key for provider: ${model.provider}`); + } const client = createClient(model, apiKey, options); let params = buildParams(model, context, options, deploymentName); const nextParams = await options?.onPayload?.(params, model); @@ -150,7 +152,7 @@ export const streamSimpleAzureOpenAIResponses: StreamFunction<"azure-openai-resp context: Context, options?: SimpleStreamOptions, ): AssistantMessageEventStream => { - const apiKey = options?.apiKey || getEnvApiKey(model.provider); + const apiKey = options?.apiKey; if (!apiKey) { throw new Error(`No API key for provider: ${model.provider}`); } @@ -224,15 +226,6 @@ function resolveAzureConfig( } function createClient(model: Model<"azure-openai-responses">, apiKey: string, options?: AzureOpenAIResponsesOptions) { - if (!apiKey) { - if (!process.env.AZURE_OPENAI_API_KEY) { - throw new Error( - "Azure OpenAI API key is required. Set AZURE_OPENAI_API_KEY environment variable or pass it as an argument.", - ); - } - apiKey = process.env.AZURE_OPENAI_API_KEY; - } - const headers = { ...model.headers }; if (options?.headers) { diff --git a/packages/ai/src/providers/google-vertex.ts b/packages/ai/src/providers/google-vertex.ts index e5f18dab..6feeabb6 100644 --- a/packages/ai/src/providers/google-vertex.ts +++ b/packages/ai/src/providers/google-vertex.ts @@ -395,7 +395,7 @@ function baseUrlIncludesApiVersion(baseUrl: string): boolean { } function resolveApiKey(options?: GoogleVertexOptions): string | undefined { - const apiKey = options?.apiKey?.trim() || process.env.GOOGLE_CLOUD_API_KEY?.trim(); + const apiKey = options?.apiKey?.trim(); if (!apiKey || apiKey === GCP_VERTEX_CREDENTIALS_MARKER || isPlaceholderApiKey(apiKey)) { return undefined; } diff --git a/packages/ai/src/providers/google.ts b/packages/ai/src/providers/google.ts index d0ddf673..d3f6f623 100644 --- a/packages/ai/src/providers/google.ts +++ b/packages/ai/src/providers/google.ts @@ -4,7 +4,6 @@ import { GoogleGenAI, type ThinkingConfig, } from "@google/genai"; -import { getEnvApiKey } from "../env-api-keys.ts"; import { calculateCost, clampThinkingLevel } from "../models.ts"; import type { Api, @@ -72,7 +71,10 @@ export const streamGoogle: StreamFunction<"google-generative-ai", GoogleOptions> }; try { - const apiKey = options?.apiKey || getEnvApiKey(model.provider) || ""; + const apiKey = options?.apiKey; + if (!apiKey) { + throw new Error(`No API key for provider: ${model.provider}`); + } const client = createClient(model, apiKey, options?.headers); let params = buildParams(model, context, options); const nextParams = await options?.onPayload?.(params, model); @@ -280,7 +282,7 @@ export const streamSimpleGoogle: StreamFunction<"google-generative-ai", SimpleSt context: Context, options?: SimpleStreamOptions, ): AssistantMessageEventStream => { - const apiKey = options?.apiKey || getEnvApiKey(model.provider); + const apiKey = options?.apiKey; if (!apiKey) { throw new Error(`No API key for provider: ${model.provider}`); } diff --git a/packages/ai/src/providers/images/openrouter.ts b/packages/ai/src/providers/images/openrouter.ts index 13ba4f34..54caeaf0 100644 --- a/packages/ai/src/providers/images/openrouter.ts +++ b/packages/ai/src/providers/images/openrouter.ts @@ -6,7 +6,6 @@ import type { ChatCompletionContentPartText, ChatCompletionCreateParamsNonStreaming, } from "openai/resources/chat/completions.js"; -import { getEnvApiKey } from "../../env-api-keys.ts"; import type { AssistantImages, ImageContent, @@ -50,9 +49,9 @@ export const generateImagesOpenRouter: ImagesFunction<"openrouter-images", Image }; try { - const apiKey = options?.apiKey || getEnvApiKey(model.provider); + const apiKey = options?.apiKey; if (!apiKey) { - throw new Error(`No API key available for provider: ${model.provider}`); + throw new Error(`No API key for provider: ${model.provider}`); } const client = createClient(model, apiKey, options?.headers); let params = buildParams(model, context); diff --git a/packages/ai/src/providers/mistral.ts b/packages/ai/src/providers/mistral.ts index a15f141a..1bc7d4ce 100644 --- a/packages/ai/src/providers/mistral.ts +++ b/packages/ai/src/providers/mistral.ts @@ -6,7 +6,6 @@ import type { ContentChunk, FunctionTool, } from "@mistralai/mistralai/models/components"; -import { getEnvApiKey } from "../env-api-keys.ts"; import { calculateCost, clampThinkingLevel } from "../models.ts"; import type { AssistantMessage, @@ -57,7 +56,7 @@ export const streamMistral: StreamFunction<"mistral-conversations", MistralOptio const output = createOutput(model); try { - const apiKey = options?.apiKey || getEnvApiKey(model.provider); + const apiKey = options?.apiKey; if (!apiKey) { throw new Error(`No API key for provider: ${model.provider}`); } @@ -113,7 +112,7 @@ export const streamSimpleMistral: StreamFunction<"mistral-conversations", Simple context: Context, options?: SimpleStreamOptions, ): AssistantMessageEventStream => { - const apiKey = options?.apiKey || getEnvApiKey(model.provider); + const apiKey = options?.apiKey; if (!apiKey) { throw new Error(`No API key for provider: ${model.provider}`); } diff --git a/packages/ai/src/providers/openai-codex-responses.ts b/packages/ai/src/providers/openai-codex-responses.ts index f2c60af1..73991e12 100644 --- a/packages/ai/src/providers/openai-codex-responses.ts +++ b/packages/ai/src/providers/openai-codex-responses.ts @@ -20,7 +20,6 @@ if (typeof process !== "undefined" && (process.versions?.node || process.version }); } -import { getEnvApiKey } from "../env-api-keys.ts"; import { clampThinkingLevel } from "../models.ts"; import { registerSessionResourceCleanup } from "../session-resources.ts"; import type { @@ -219,7 +218,7 @@ export const streamOpenAICodexResponses: StreamFunction<"openai-codex-responses" }; try { - const apiKey = options?.apiKey || getEnvApiKey(model.provider) || ""; + const apiKey = options?.apiKey; if (!apiKey) { throw new Error(`No API key for provider: ${model.provider}`); } @@ -410,7 +409,7 @@ export const streamSimpleOpenAICodexResponses: StreamFunction<"openai-codex-resp context: Context, options?: SimpleStreamOptions, ): AssistantMessageEventStream => { - const apiKey = options?.apiKey || getEnvApiKey(model.provider); + const apiKey = options?.apiKey; if (!apiKey) { throw new Error(`No API key for provider: ${model.provider}`); } diff --git a/packages/ai/src/providers/openai-completions.ts b/packages/ai/src/providers/openai-completions.ts index 6834d1e7..245e0bc8 100644 --- a/packages/ai/src/providers/openai-completions.ts +++ b/packages/ai/src/providers/openai-completions.ts @@ -10,7 +10,6 @@ import type { ChatCompletionSystemMessageParam, ChatCompletionToolMessageParam, } from "openai/resources/chat/completions.js"; -import { getEnvApiKey } from "../env-api-keys.ts"; import { calculateCost, clampThinkingLevel } from "../models.ts"; import type { AssistantMessage, @@ -136,7 +135,10 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions", OpenA }; try { - const apiKey = options?.apiKey || getEnvApiKey(model.provider) || ""; + const apiKey = options?.apiKey; + if (!apiKey) { + throw new Error(`No API key for provider: ${model.provider}`); + } const compat = getCompat(model); const cacheRetention = resolveCacheRetention(options?.cacheRetention); const cacheSessionId = cacheRetention === "none" ? undefined : options?.sessionId; @@ -428,7 +430,7 @@ export const streamSimpleOpenAICompletions: StreamFunction<"openai-completions", context: Context, options?: SimpleStreamOptions, ): AssistantMessageEventStream => { - const apiKey = options?.apiKey || getEnvApiKey(model.provider); + const apiKey = options?.apiKey; if (!apiKey) { throw new Error(`No API key for provider: ${model.provider}`); } @@ -448,20 +450,11 @@ export const streamSimpleOpenAICompletions: StreamFunction<"openai-completions", function createClient( model: Model<"openai-completions">, context: Context, - apiKey?: string, + apiKey: string, optionsHeaders?: Record, sessionId?: string, compat: ResolvedOpenAICompletionsCompat = getCompat(model), ) { - if (!apiKey) { - if (!process.env.OPENAI_API_KEY) { - throw new Error( - "OpenAI API key is required. Set OPENAI_API_KEY environment variable or pass it as an argument.", - ); - } - apiKey = process.env.OPENAI_API_KEY; - } - const headers = { ...model.headers }; if (model.provider === "github-copilot") { const hasImages = hasCopilotVisionInput(context.messages); diff --git a/packages/ai/src/providers/openai-responses.ts b/packages/ai/src/providers/openai-responses.ts index acf01cf9..4b35a24c 100644 --- a/packages/ai/src/providers/openai-responses.ts +++ b/packages/ai/src/providers/openai-responses.ts @@ -1,6 +1,5 @@ import OpenAI from "openai"; import type { ResponseCreateParamsStreaming } from "openai/resources/responses/responses.js"; -import { getEnvApiKey } from "../env-api-keys.ts"; import { clampThinkingLevel } from "../models.ts"; import type { Api, @@ -107,7 +106,10 @@ export const streamOpenAIResponses: StreamFunction<"openai-responses", OpenAIRes try { // Create OpenAI client - const apiKey = options?.apiKey || getEnvApiKey(model.provider) || ""; + const apiKey = options?.apiKey; + if (!apiKey) { + throw new Error(`No API key for provider: ${model.provider}`); + } const cacheRetention = resolveCacheRetention(options?.cacheRetention); const cacheSessionId = cacheRetention === "none" ? undefined : options?.sessionId; const client = createClient(model, context, apiKey, options?.headers, cacheSessionId); @@ -161,7 +163,7 @@ export const streamSimpleOpenAIResponses: StreamFunction<"openai-responses", Sim context: Context, options?: SimpleStreamOptions, ): AssistantMessageEventStream => { - const apiKey = options?.apiKey || getEnvApiKey(model.provider); + const apiKey = options?.apiKey; if (!apiKey) { throw new Error(`No API key for provider: ${model.provider}`); } @@ -179,19 +181,10 @@ export const streamSimpleOpenAIResponses: StreamFunction<"openai-responses", Sim function createClient( model: Model<"openai-responses">, context: Context, - apiKey?: string, + apiKey: string, optionsHeaders?: Record, sessionId?: string, ) { - if (!apiKey) { - if (!process.env.OPENAI_API_KEY) { - throw new Error( - "OpenAI API key is required. Set OPENAI_API_KEY environment variable or pass it as an argument.", - ); - } - apiKey = process.env.OPENAI_API_KEY; - } - const compat = getCompat(model); const headers = { ...model.headers }; if (model.provider === "github-copilot") { diff --git a/packages/ai/src/stream.ts b/packages/ai/src/stream.ts index a38b01fd..3ae631a1 100644 --- a/packages/ai/src/stream.ts +++ b/packages/ai/src/stream.ts @@ -1,6 +1,7 @@ import "./providers/register-builtins.ts"; import { getApiProvider } from "./api-registry.ts"; +import { getEnvApiKey } from "./env-api-keys.ts"; import type { Api, AssistantMessage, @@ -14,6 +15,20 @@ import type { export { getEnvApiKey } from "./env-api-keys.ts"; +function hasExplicitApiKey(apiKey: string | undefined): apiKey is string { + return typeof apiKey === "string" && apiKey.trim().length > 0; +} + +function withEnvApiKey( + model: Model, + options: TOptions | undefined, +): TOptions | undefined { + if (hasExplicitApiKey(options?.apiKey)) return options; + const apiKey = getEnvApiKey(model.provider); + if (!apiKey) return options; + return { ...options, apiKey } as TOptions; +} + function resolveApiProvider(api: Api) { const provider = getApiProvider(api); if (!provider) { @@ -28,7 +43,7 @@ export function stream( options?: ProviderStreamOptions, ): AssistantMessageEventStream { const provider = resolveApiProvider(model.api); - return provider.stream(model, context, options as StreamOptions); + return provider.stream(model, context, withEnvApiKey(model, options) as StreamOptions); } export async function complete( @@ -46,7 +61,7 @@ export function streamSimple( options?: SimpleStreamOptions, ): AssistantMessageEventStream { const provider = resolveApiProvider(model.api); - return provider.streamSimple(model, context, options); + return provider.streamSimple(model, context, withEnvApiKey(model, options)); } export async function completeSimple(