Require explicit provider API keys

This commit is contained in:
Mario Zechner
2026-05-29 22:30:47 +02:00
parent 01a8c2d62c
commit 7921ae499a
11 changed files with 55 additions and 56 deletions

View File

@@ -2,6 +2,10 @@
## [Unreleased] ## [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
- 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)). - 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)).

View File

@@ -6,7 +6,6 @@ import type {
MessageParam, MessageParam,
RawMessageStreamEvent, RawMessageStreamEvent,
} from "@anthropic-ai/sdk/resources/messages.js"; } from "@anthropic-ai/sdk/resources/messages.js";
import { getEnvApiKey } from "../env-api-keys.ts";
import { calculateCost } from "../models.ts"; import { calculateCost } from "../models.ts";
import type { import type {
AnthropicMessagesCompat, AnthropicMessagesCompat,
@@ -479,7 +478,10 @@ export const streamAnthropic: StreamFunction<"anthropic-messages", AnthropicOpti
client = options.client; client = options.client;
isOAuth = false; isOAuth = false;
} else { } 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<string, string> | undefined; let copilotDynamicHeaders: Record<string, string> | undefined;
if (model.provider === "github-copilot") { if (model.provider === "github-copilot") {
@@ -735,7 +737,7 @@ export const streamSimpleAnthropic: StreamFunction<"anthropic-messages", SimpleS
context: Context, context: Context,
options?: SimpleStreamOptions, options?: SimpleStreamOptions,
): AssistantMessageEventStream => { ): AssistantMessageEventStream => {
const apiKey = options?.apiKey || getEnvApiKey(model.provider); const apiKey = options?.apiKey;
if (!apiKey) { if (!apiKey) {
throw new Error(`No API key for provider: ${model.provider}`); throw new Error(`No API key for provider: ${model.provider}`);
} }

View File

@@ -1,6 +1,5 @@
import { AzureOpenAI } from "openai"; import { AzureOpenAI } from "openai";
import type { ResponseCreateParamsStreaming } from "openai/resources/responses/responses.js"; import type { ResponseCreateParamsStreaming } from "openai/resources/responses/responses.js";
import { getEnvApiKey } from "../env-api-keys.ts";
import { clampThinkingLevel } from "../models.ts"; import { clampThinkingLevel } from "../models.ts";
import type { import type {
Api, Api,
@@ -101,7 +100,10 @@ export const streamAzureOpenAIResponses: StreamFunction<"azure-openai-responses"
try { try {
// Create Azure OpenAI client // 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); const client = createClient(model, apiKey, options);
let params = buildParams(model, context, options, deploymentName); let params = buildParams(model, context, options, deploymentName);
const nextParams = await options?.onPayload?.(params, model); const nextParams = await options?.onPayload?.(params, model);
@@ -150,7 +152,7 @@ export const streamSimpleAzureOpenAIResponses: StreamFunction<"azure-openai-resp
context: Context, context: Context,
options?: SimpleStreamOptions, options?: SimpleStreamOptions,
): AssistantMessageEventStream => { ): AssistantMessageEventStream => {
const apiKey = options?.apiKey || getEnvApiKey(model.provider); const apiKey = options?.apiKey;
if (!apiKey) { if (!apiKey) {
throw new Error(`No API key for provider: ${model.provider}`); 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) { 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 }; const headers = { ...model.headers };
if (options?.headers) { if (options?.headers) {

View File

@@ -395,7 +395,7 @@ function baseUrlIncludesApiVersion(baseUrl: string): boolean {
} }
function resolveApiKey(options?: GoogleVertexOptions): string | undefined { 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)) { if (!apiKey || apiKey === GCP_VERTEX_CREDENTIALS_MARKER || isPlaceholderApiKey(apiKey)) {
return undefined; return undefined;
} }

View File

@@ -4,7 +4,6 @@ import {
GoogleGenAI, GoogleGenAI,
type ThinkingConfig, type ThinkingConfig,
} from "@google/genai"; } from "@google/genai";
import { getEnvApiKey } from "../env-api-keys.ts";
import { calculateCost, clampThinkingLevel } from "../models.ts"; import { calculateCost, clampThinkingLevel } from "../models.ts";
import type { import type {
Api, Api,
@@ -72,7 +71,10 @@ export const streamGoogle: StreamFunction<"google-generative-ai", GoogleOptions>
}; };
try { 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); const client = createClient(model, apiKey, options?.headers);
let params = buildParams(model, context, options); let params = buildParams(model, context, options);
const nextParams = await options?.onPayload?.(params, model); const nextParams = await options?.onPayload?.(params, model);
@@ -280,7 +282,7 @@ export const streamSimpleGoogle: StreamFunction<"google-generative-ai", SimpleSt
context: Context, context: Context,
options?: SimpleStreamOptions, options?: SimpleStreamOptions,
): AssistantMessageEventStream => { ): AssistantMessageEventStream => {
const apiKey = options?.apiKey || getEnvApiKey(model.provider); const apiKey = options?.apiKey;
if (!apiKey) { if (!apiKey) {
throw new Error(`No API key for provider: ${model.provider}`); throw new Error(`No API key for provider: ${model.provider}`);
} }

View File

@@ -6,7 +6,6 @@ import type {
ChatCompletionContentPartText, ChatCompletionContentPartText,
ChatCompletionCreateParamsNonStreaming, ChatCompletionCreateParamsNonStreaming,
} from "openai/resources/chat/completions.js"; } from "openai/resources/chat/completions.js";
import { getEnvApiKey } from "../../env-api-keys.ts";
import type { import type {
AssistantImages, AssistantImages,
ImageContent, ImageContent,
@@ -50,9 +49,9 @@ export const generateImagesOpenRouter: ImagesFunction<"openrouter-images", Image
}; };
try { try {
const apiKey = options?.apiKey || getEnvApiKey(model.provider); const apiKey = options?.apiKey;
if (!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); const client = createClient(model, apiKey, options?.headers);
let params = buildParams(model, context); let params = buildParams(model, context);

View File

@@ -6,7 +6,6 @@ import type {
ContentChunk, ContentChunk,
FunctionTool, FunctionTool,
} from "@mistralai/mistralai/models/components"; } from "@mistralai/mistralai/models/components";
import { getEnvApiKey } from "../env-api-keys.ts";
import { calculateCost, clampThinkingLevel } from "../models.ts"; import { calculateCost, clampThinkingLevel } from "../models.ts";
import type { import type {
AssistantMessage, AssistantMessage,
@@ -57,7 +56,7 @@ export const streamMistral: StreamFunction<"mistral-conversations", MistralOptio
const output = createOutput(model); const output = createOutput(model);
try { try {
const apiKey = options?.apiKey || getEnvApiKey(model.provider); const apiKey = options?.apiKey;
if (!apiKey) { if (!apiKey) {
throw new Error(`No API key for provider: ${model.provider}`); throw new Error(`No API key for provider: ${model.provider}`);
} }
@@ -113,7 +112,7 @@ export const streamSimpleMistral: StreamFunction<"mistral-conversations", Simple
context: Context, context: Context,
options?: SimpleStreamOptions, options?: SimpleStreamOptions,
): AssistantMessageEventStream => { ): AssistantMessageEventStream => {
const apiKey = options?.apiKey || getEnvApiKey(model.provider); const apiKey = options?.apiKey;
if (!apiKey) { if (!apiKey) {
throw new Error(`No API key for provider: ${model.provider}`); throw new Error(`No API key for provider: ${model.provider}`);
} }

View File

@@ -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 { clampThinkingLevel } from "../models.ts";
import { registerSessionResourceCleanup } from "../session-resources.ts"; import { registerSessionResourceCleanup } from "../session-resources.ts";
import type { import type {
@@ -219,7 +218,7 @@ export const streamOpenAICodexResponses: StreamFunction<"openai-codex-responses"
}; };
try { try {
const apiKey = options?.apiKey || getEnvApiKey(model.provider) || ""; const apiKey = options?.apiKey;
if (!apiKey) { if (!apiKey) {
throw new Error(`No API key for provider: ${model.provider}`); throw new Error(`No API key for provider: ${model.provider}`);
} }
@@ -410,7 +409,7 @@ export const streamSimpleOpenAICodexResponses: StreamFunction<"openai-codex-resp
context: Context, context: Context,
options?: SimpleStreamOptions, options?: SimpleStreamOptions,
): AssistantMessageEventStream => { ): AssistantMessageEventStream => {
const apiKey = options?.apiKey || getEnvApiKey(model.provider); const apiKey = options?.apiKey;
if (!apiKey) { if (!apiKey) {
throw new Error(`No API key for provider: ${model.provider}`); throw new Error(`No API key for provider: ${model.provider}`);
} }

View File

@@ -10,7 +10,6 @@ import type {
ChatCompletionSystemMessageParam, ChatCompletionSystemMessageParam,
ChatCompletionToolMessageParam, ChatCompletionToolMessageParam,
} from "openai/resources/chat/completions.js"; } from "openai/resources/chat/completions.js";
import { getEnvApiKey } from "../env-api-keys.ts";
import { calculateCost, clampThinkingLevel } from "../models.ts"; import { calculateCost, clampThinkingLevel } from "../models.ts";
import type { import type {
AssistantMessage, AssistantMessage,
@@ -136,7 +135,10 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions", OpenA
}; };
try { 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 compat = getCompat(model);
const cacheRetention = resolveCacheRetention(options?.cacheRetention); const cacheRetention = resolveCacheRetention(options?.cacheRetention);
const cacheSessionId = cacheRetention === "none" ? undefined : options?.sessionId; const cacheSessionId = cacheRetention === "none" ? undefined : options?.sessionId;
@@ -428,7 +430,7 @@ export const streamSimpleOpenAICompletions: StreamFunction<"openai-completions",
context: Context, context: Context,
options?: SimpleStreamOptions, options?: SimpleStreamOptions,
): AssistantMessageEventStream => { ): AssistantMessageEventStream => {
const apiKey = options?.apiKey || getEnvApiKey(model.provider); const apiKey = options?.apiKey;
if (!apiKey) { if (!apiKey) {
throw new Error(`No API key for provider: ${model.provider}`); throw new Error(`No API key for provider: ${model.provider}`);
} }
@@ -448,20 +450,11 @@ export const streamSimpleOpenAICompletions: StreamFunction<"openai-completions",
function createClient( function createClient(
model: Model<"openai-completions">, model: Model<"openai-completions">,
context: Context, context: Context,
apiKey?: string, apiKey: string,
optionsHeaders?: Record<string, string>, optionsHeaders?: Record<string, string>,
sessionId?: string, sessionId?: string,
compat: ResolvedOpenAICompletionsCompat = getCompat(model), 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 }; const headers = { ...model.headers };
if (model.provider === "github-copilot") { if (model.provider === "github-copilot") {
const hasImages = hasCopilotVisionInput(context.messages); const hasImages = hasCopilotVisionInput(context.messages);

View File

@@ -1,6 +1,5 @@
import OpenAI from "openai"; import OpenAI from "openai";
import type { ResponseCreateParamsStreaming } from "openai/resources/responses/responses.js"; import type { ResponseCreateParamsStreaming } from "openai/resources/responses/responses.js";
import { getEnvApiKey } from "../env-api-keys.ts";
import { clampThinkingLevel } from "../models.ts"; import { clampThinkingLevel } from "../models.ts";
import type { import type {
Api, Api,
@@ -107,7 +106,10 @@ export const streamOpenAIResponses: StreamFunction<"openai-responses", OpenAIRes
try { try {
// Create OpenAI client // 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 cacheRetention = resolveCacheRetention(options?.cacheRetention);
const cacheSessionId = cacheRetention === "none" ? undefined : options?.sessionId; const cacheSessionId = cacheRetention === "none" ? undefined : options?.sessionId;
const client = createClient(model, context, apiKey, options?.headers, cacheSessionId); const client = createClient(model, context, apiKey, options?.headers, cacheSessionId);
@@ -161,7 +163,7 @@ export const streamSimpleOpenAIResponses: StreamFunction<"openai-responses", Sim
context: Context, context: Context,
options?: SimpleStreamOptions, options?: SimpleStreamOptions,
): AssistantMessageEventStream => { ): AssistantMessageEventStream => {
const apiKey = options?.apiKey || getEnvApiKey(model.provider); const apiKey = options?.apiKey;
if (!apiKey) { if (!apiKey) {
throw new Error(`No API key for provider: ${model.provider}`); throw new Error(`No API key for provider: ${model.provider}`);
} }
@@ -179,19 +181,10 @@ export const streamSimpleOpenAIResponses: StreamFunction<"openai-responses", Sim
function createClient( function createClient(
model: Model<"openai-responses">, model: Model<"openai-responses">,
context: Context, context: Context,
apiKey?: string, apiKey: string,
optionsHeaders?: Record<string, string>, optionsHeaders?: Record<string, string>,
sessionId?: string, 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 compat = getCompat(model);
const headers = { ...model.headers }; const headers = { ...model.headers };
if (model.provider === "github-copilot") { if (model.provider === "github-copilot") {

View File

@@ -1,6 +1,7 @@
import "./providers/register-builtins.ts"; import "./providers/register-builtins.ts";
import { getApiProvider } from "./api-registry.ts"; import { getApiProvider } from "./api-registry.ts";
import { getEnvApiKey } from "./env-api-keys.ts";
import type { import type {
Api, Api,
AssistantMessage, AssistantMessage,
@@ -14,6 +15,20 @@ import type {
export { getEnvApiKey } from "./env-api-keys.ts"; 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<TOptions extends StreamOptions>(
model: Model<Api>,
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) { function resolveApiProvider(api: Api) {
const provider = getApiProvider(api); const provider = getApiProvider(api);
if (!provider) { if (!provider) {
@@ -28,7 +43,7 @@ export function stream<TApi extends Api>(
options?: ProviderStreamOptions, options?: ProviderStreamOptions,
): AssistantMessageEventStream { ): AssistantMessageEventStream {
const provider = resolveApiProvider(model.api); 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<TApi extends Api>( export async function complete<TApi extends Api>(
@@ -46,7 +61,7 @@ export function streamSimple<TApi extends Api>(
options?: SimpleStreamOptions, options?: SimpleStreamOptions,
): AssistantMessageEventStream { ): AssistantMessageEventStream {
const provider = resolveApiProvider(model.api); const provider = resolveApiProvider(model.api);
return provider.streamSimple(model, context, options); return provider.streamSimple(model, context, withEnvApiKey(model, options));
} }
export async function completeSimple<TApi extends Api>( export async function completeSimple<TApi extends Api>(