From 8eeaa2bcbaf4d453944598c56f682b2ec7c35e42 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Tue, 23 Jun 2026 13:45:26 +0200 Subject: [PATCH] fix(ai): honor scoped env in compat API key injection --- packages/ai/src/compat.ts | 2 +- packages/ai/test/compat-env.test.ts | 74 +++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 packages/ai/test/compat-env.test.ts diff --git a/packages/ai/src/compat.ts b/packages/ai/src/compat.ts index 7ddbbaaf..fff4746e 100644 --- a/packages/ai/src/compat.ts +++ b/packages/ai/src/compat.ts @@ -100,7 +100,7 @@ function withEnvApiKey( options: TOptions | undefined, ): TOptions | undefined { if (hasExplicitApiKey(options?.apiKey)) return options; - const apiKey = getEnvApiKey(model.provider); + const apiKey = getEnvApiKey(model.provider, options?.env); if (!apiKey) return options; return { ...options, apiKey } as TOptions; } diff --git a/packages/ai/test/compat-env.test.ts b/packages/ai/test/compat-env.test.ts new file mode 100644 index 00000000..69a57da1 --- /dev/null +++ b/packages/ai/test/compat-env.test.ts @@ -0,0 +1,74 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { complete, registerApiProvider, resetApiProviders } from "../src/compat.ts"; +import type { AssistantMessage, Context, Model } from "../src/types.ts"; +import { AssistantMessageEventStream } from "../src/utils/event-stream.ts"; + +const context: Context = { messages: [{ role: "user", content: "hi", timestamp: Date.now() }] }; + +const model: Model<"openai-responses"> = { + id: "test-model", + name: "Test Model", + api: "openai-responses", + provider: "openai", + baseUrl: "https://example.test/v1", + reasoning: false, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 128000, + maxTokens: 4096, +}; + +function message(): AssistantMessage { + return { + role: "assistant", + content: [{ type: "text", text: "ok" }], + api: model.api, + provider: model.provider, + model: model.id, + usage: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + totalTokens: 0, + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, + }, + stopReason: "stop", + timestamp: Date.now(), + }; +} + +describe("compat env API key injection", () => { + afterEach(() => { + resetApiProviders(); + }); + + it("uses request-scoped env when injecting provider API keys", async () => { + let capturedApiKey: string | undefined; + registerApiProvider({ + api: "openai-responses", + stream: (_model, _context, options) => { + capturedApiKey = options?.apiKey; + const stream = new AssistantMessageEventStream(); + const output = message(); + stream.push({ type: "start", partial: output }); + stream.push({ type: "done", reason: "stop", message: output }); + stream.end(output); + return stream; + }, + streamSimple: (_model, _context, options) => { + capturedApiKey = options?.apiKey; + const stream = new AssistantMessageEventStream(); + const output = message(); + stream.push({ type: "start", partial: output }); + stream.push({ type: "done", reason: "stop", message: output }); + stream.end(output); + return stream; + }, + }); + + await complete(model, context, { env: { OPENAI_API_KEY: "scoped-key" } }); + + expect(capturedApiKey).toBe("scoped-key"); + }); +});