mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-24 01:11:10 +00:00
fix(ai): skip blank environment credentials during provider auth (#109691)
* fix(ai): ignore blank environment API keys * test(ai): avoid credential-shaped fixture assignments Co-authored-by: ZengWen-DT <ceng.wen@xydigit.com> --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
@@ -109,7 +109,7 @@ function getProcEnv(key: string): string | undefined {
|
||||
}
|
||||
|
||||
function getEnvValue(key: string): string | undefined {
|
||||
return getProcessEnv()?.[key] || getProcEnv(key);
|
||||
return (getProcessEnv()?.[key] || getProcEnv(key))?.trim() || undefined;
|
||||
}
|
||||
|
||||
let cachedVertexAdcCredentialsExists: true | null = null;
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { configureAiTransportHost } from "../host.js";
|
||||
import type { Context, Model } from "../types.js";
|
||||
import { streamSimpleAzureOpenAIResponses, testing } from "./azure-openai-responses.js";
|
||||
import {
|
||||
streamAzureOpenAIResponses,
|
||||
streamSimpleAzureOpenAIResponses,
|
||||
testing,
|
||||
} from "./azure-openai-responses.js";
|
||||
|
||||
const azureResponsesModel = {
|
||||
id: "gpt-5.5",
|
||||
@@ -105,6 +109,36 @@ describe("azure-openai-responses", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects a blank environment API key before sending a request", async () => {
|
||||
const previousApiKey = process.env.AZURE_OPENAI_API_KEY;
|
||||
let fetchCalled = false;
|
||||
configureAiTransportHost({
|
||||
buildModelFetch: () => async () => {
|
||||
fetchCalled = true;
|
||||
return Response.json({ error: { message: "captured" } }, { status: 400 });
|
||||
},
|
||||
});
|
||||
process.env.AZURE_OPENAI_API_KEY = " ";
|
||||
try {
|
||||
const result = await streamAzureOpenAIResponses(
|
||||
{ ...azureResponsesModel, provider: "azure-openai-responses" },
|
||||
context,
|
||||
).result();
|
||||
|
||||
expect(fetchCalled).toBe(false);
|
||||
expect(result.errorMessage).toBe(
|
||||
"Azure OpenAI API key is required. Set AZURE_OPENAI_API_KEY environment variable or pass it as an argument.",
|
||||
);
|
||||
} finally {
|
||||
configureAiTransportHost({});
|
||||
if (previousApiKey === undefined) {
|
||||
delete process.env.AZURE_OPENAI_API_KEY;
|
||||
} else {
|
||||
process.env.AZURE_OPENAI_API_KEY = previousApiKey;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("disables response storage and clamps small output limits", async () => {
|
||||
let sentParams: { max_output_tokens?: unknown; store?: unknown } | undefined;
|
||||
const hostFetch: typeof fetch = async (input, init) => {
|
||||
|
||||
@@ -183,14 +183,11 @@ function createClient(
|
||||
apiKeyInput: string,
|
||||
options?: AzureOpenAIResponsesOptions,
|
||||
) {
|
||||
let apiKey = apiKeyInput;
|
||||
const apiKey = apiKeyInput.trim();
|
||||
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;
|
||||
throw new Error(
|
||||
"Azure OpenAI API key is required. Set AZURE_OPENAI_API_KEY environment variable or pass it as an argument.",
|
||||
);
|
||||
}
|
||||
|
||||
const headers = { ...model.headers };
|
||||
|
||||
@@ -6,12 +6,22 @@ import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { captureEnv, withEnvAsync } from "../test-utils/env.js";
|
||||
|
||||
const envKeys = [
|
||||
"ANTHROPIC_API_KEY",
|
||||
"ANTHROPIC_OAUTH_TOKEN",
|
||||
"AWS_ACCESS_KEY_ID",
|
||||
"AWS_BEARER_TOKEN_BEDROCK",
|
||||
"AWS_CONTAINER_CREDENTIALS_FULL_URI",
|
||||
"AWS_CONTAINER_CREDENTIALS_RELATIVE_URI",
|
||||
"AWS_PROFILE",
|
||||
"AWS_SECRET_ACCESS_KEY",
|
||||
"AWS_WEB_IDENTITY_TOKEN_FILE",
|
||||
"GOOGLE_APPLICATION_CREDENTIALS",
|
||||
"GOOGLE_CLOUD_LOCATION",
|
||||
"GOOGLE_CLOUD_PROJECT",
|
||||
"KIMI_API_KEY",
|
||||
"KIMICODE_API_KEY",
|
||||
"MOONSHOT_API_KEY",
|
||||
"OPENAI_API_KEY",
|
||||
] as const;
|
||||
|
||||
const originalEnv = captureEnv([...envKeys]);
|
||||
@@ -87,6 +97,72 @@ describe("getEnvApiKey", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("skips blank API keys and trims the selected fallback", async () => {
|
||||
const env = {
|
||||
ANTHROPIC_OAUTH_TOKEN: " ",
|
||||
OPENAI_API_KEY: " \t ",
|
||||
} as NodeJS.ProcessEnv;
|
||||
Reflect.set(env, "ANTHROPIC_API_KEY", " test-anthropic-key ");
|
||||
|
||||
await withEnvAsync(env, async () => {
|
||||
vi.resetModules();
|
||||
const { findEnvKeys, getEnvApiKey } = await import("@openclaw/ai/internal/runtime");
|
||||
|
||||
expect(findEnvKeys("anthropic")).toEqual(["ANTHROPIC_API_KEY"]);
|
||||
expect(getEnvApiKey("anthropic")).toBe("test-anthropic-key");
|
||||
expect(findEnvKeys("openai")).toBeUndefined();
|
||||
expect(getEnvApiKey("openai")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("does not report blank AWS credential markers as authentication", async () => {
|
||||
await withEnvAsync(
|
||||
{
|
||||
AWS_ACCESS_KEY_ID: " ",
|
||||
AWS_SECRET_ACCESS_KEY: "\t",
|
||||
AWS_PROFILE: " ",
|
||||
AWS_BEARER_TOKEN_BEDROCK: "\n",
|
||||
AWS_CONTAINER_CREDENTIALS_RELATIVE_URI: " ",
|
||||
AWS_CONTAINER_CREDENTIALS_FULL_URI: " ",
|
||||
AWS_WEB_IDENTITY_TOKEN_FILE: " ",
|
||||
},
|
||||
async () => {
|
||||
vi.resetModules();
|
||||
const { getEnvApiKey } = await import("@openclaw/ai/internal/runtime");
|
||||
|
||||
expect(getEnvApiKey("amazon-bedrock")).toBeUndefined();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps non-blank AWS profile authentication available", async () => {
|
||||
await withEnvAsync({ AWS_PROFILE: " production " }, async () => {
|
||||
vi.resetModules();
|
||||
const { getEnvApiKey } = await import("@openclaw/ai/internal/runtime");
|
||||
|
||||
expect(getEnvApiKey("amazon-bedrock")).toBe("<authenticated>");
|
||||
});
|
||||
});
|
||||
|
||||
it("requires non-blank Google Vertex project and location markers", async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), "openclaw-vertex-adc-"));
|
||||
tempDirs.push(dir);
|
||||
const credentialsPath = join(dir, "application_default_credentials.json");
|
||||
await writeFile(credentialsPath, "{}", "utf-8");
|
||||
const env = {
|
||||
GOOGLE_CLOUD_LOCATION: " ",
|
||||
GOOGLE_CLOUD_PROJECT: "\t",
|
||||
} as NodeJS.ProcessEnv;
|
||||
Reflect.set(env, "GOOGLE_APPLICATION_CREDENTIALS", credentialsPath);
|
||||
|
||||
await withEnvAsync(env, async () => {
|
||||
vi.resetModules();
|
||||
const { getEnvApiKey } = await import("@openclaw/ai/internal/runtime");
|
||||
|
||||
expect(getEnvApiKey("google-vertex")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("does not cache missing Google Vertex ADC credentials", async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), "openclaw-vertex-adc-"));
|
||||
tempDirs.push(dir);
|
||||
@@ -107,4 +183,23 @@ describe("getEnvApiKey", () => {
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("trims the Google Vertex credentials path before checking it", async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), "openclaw-vertex-adc-"));
|
||||
tempDirs.push(dir);
|
||||
const credentialsPath = join(dir, "application_default_credentials.json");
|
||||
await writeFile(credentialsPath, "{}", "utf-8");
|
||||
const env = {
|
||||
GOOGLE_CLOUD_LOCATION: "us-central1",
|
||||
GOOGLE_CLOUD_PROJECT: "vertex-project",
|
||||
} as NodeJS.ProcessEnv;
|
||||
Reflect.set(env, "GOOGLE_APPLICATION_CREDENTIALS", ` ${credentialsPath} `);
|
||||
|
||||
await withEnvAsync(env, async () => {
|
||||
vi.resetModules();
|
||||
const { getEnvApiKey } = await import("@openclaw/ai/internal/runtime");
|
||||
|
||||
expect(getEnvApiKey("google-vertex")).toBe("<authenticated>");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user