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:
Wynne668
2026-07-19 05:33:54 +08:00
committed by GitHub
parent 848dbcabba
commit d5b2749fd8
4 changed files with 135 additions and 9 deletions

View File

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

View File

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

View File

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

View File

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