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]
### 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)).

View File

@@ -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<string, string> | 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}`);
}

View File

@@ -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) {

View File

@@ -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;
}

View File

@@ -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}`);
}

View File

@@ -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);

View File

@@ -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}`);
}

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 { 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}`);
}

View File

@@ -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<string, string>,
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);

View File

@@ -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<string, 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 headers = { ...model.headers };
if (model.provider === "github-copilot") {

View File

@@ -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<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) {
const provider = getApiProvider(api);
if (!provider) {
@@ -28,7 +43,7 @@ export function stream<TApi extends Api>(
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<TApi extends Api>(
@@ -46,7 +61,7 @@ export function streamSimple<TApi extends Api>(
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<TApi extends Api>(