mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-18 19:04:45 +00:00
fix: include checked credential source in missing auth errors
Include the checked credential source in missing API key errors so users can see which env var, profile, or config path to fix. Fixes #82785. Co-authored-by: gleb <116607327+loeclos@users.noreply.github.com>
This commit is contained in:
@@ -29,6 +29,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- Agents/auth: include the checked credential source in missing API key errors, so users can see which env var, profile, or config path to fix. Fixes #82785. Thanks @loeclos.
|
||||
- Providers/GitHub Copilot: hash Responses replay item ids with sha256 instead of a weak 32-bit hash and build same-provider Copilot tool-call ids distinctly, so concurrent tool-call replays no longer collide and reject follow-up turns.
|
||||
- Providers/Anthropic-messages: extract `reasoning_content` from `thinking` blocks during assistant replay so proxy providers that route through the Anthropic-messages transport preserve reasoning context across tool-call follow-up turns. Thanks @Sunnyone2three.
|
||||
- Agents/GitHub Copilot: normalize replayed Responses tool-call IDs before dispatch so resumed sessions with historical overlong tool IDs continue instead of failing Copilot schema validation. (#82750) Thanks @galiniliev.
|
||||
|
||||
@@ -1193,8 +1193,7 @@ export function createAnthropicMessagesTransportStreamFn(): StreamFn {
|
||||
const hasNativeAnthropicDelta =
|
||||
(delta?.type === "text_delta" && typeof delta.text === "string") ||
|
||||
(delta?.type === "thinking_delta" && typeof delta.thinking === "string") ||
|
||||
(delta?.type === "input_json_delta" &&
|
||||
typeof delta.partial_json === "string") ||
|
||||
(delta?.type === "input_json_delta" && typeof delta.partial_json === "string") ||
|
||||
(delta?.type === "signature_delta" && typeof delta.signature === "string");
|
||||
let appendedContent = false;
|
||||
if (
|
||||
|
||||
@@ -25,10 +25,14 @@ export function resolveAwsSdkEnvVarName(env: NodeJS.ProcessEnv = process.env): s
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function formatMissingAuthError(auth: ResolvedProviderAuth, provider: string): string {
|
||||
return `No API key resolved for provider "${provider}" (auth mode: ${auth.mode}, checked: ${auth.source}).`;
|
||||
}
|
||||
|
||||
export function requireApiKey(auth: ResolvedProviderAuth, provider: string): string {
|
||||
const key = normalizeSecretInput(auth.apiKey);
|
||||
if (key) {
|
||||
return key;
|
||||
}
|
||||
throw new Error(`No API key resolved for provider "${provider}" (auth mode: ${auth.mode}).`);
|
||||
throw new Error(formatMissingAuthError(auth, provider));
|
||||
}
|
||||
|
||||
@@ -123,6 +123,7 @@ vi.mock("../plugins/provider-runtime.js", async () => {
|
||||
|
||||
let applyAuthHeaderOverride: typeof import("./model-auth.js").applyAuthHeaderOverride;
|
||||
let applyLocalNoAuthHeaderOverride: typeof import("./model-auth.js").applyLocalNoAuthHeaderOverride;
|
||||
let formatMissingAuthError: typeof import("./model-auth.js").formatMissingAuthError;
|
||||
let hasUsableCustomProviderApiKey: typeof import("./model-auth.js").hasUsableCustomProviderApiKey;
|
||||
let hasSyntheticLocalProviderAuthConfig: typeof import("./model-auth.js").hasSyntheticLocalProviderAuthConfig;
|
||||
let requireApiKey: typeof import("./model-auth.js").requireApiKey;
|
||||
@@ -141,6 +142,7 @@ beforeAll(async () => {
|
||||
({
|
||||
applyAuthHeaderOverride,
|
||||
applyLocalNoAuthHeaderOverride,
|
||||
formatMissingAuthError,
|
||||
hasSyntheticLocalProviderAuthConfig,
|
||||
hasUsableCustomProviderApiKey,
|
||||
requireApiKey,
|
||||
@@ -354,6 +356,20 @@ describe("resolveModelAuthMode", () => {
|
||||
});
|
||||
|
||||
describe("requireApiKey", () => {
|
||||
it("formats missing auth errors with the checked credential source", () => {
|
||||
expect(
|
||||
formatMissingAuthError(
|
||||
{
|
||||
source: "env: OPENAI_API_KEY",
|
||||
mode: "api-key",
|
||||
},
|
||||
"openai",
|
||||
),
|
||||
).toBe(
|
||||
'No API key resolved for provider "openai" (auth mode: api-key, checked: env: OPENAI_API_KEY).',
|
||||
);
|
||||
});
|
||||
|
||||
it("normalizes line breaks in resolved API keys", () => {
|
||||
const key = requireApiKey(
|
||||
{
|
||||
@@ -376,7 +392,9 @@ describe("requireApiKey", () => {
|
||||
},
|
||||
"openai",
|
||||
),
|
||||
).toThrow('No API key resolved for provider "openai"');
|
||||
).toThrow(
|
||||
'No API key resolved for provider "openai" (auth mode: api-key, checked: env: OPENAI_API_KEY).',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -46,7 +46,11 @@ export {
|
||||
ensureAuthProfileStoreWithoutExternalProfiles,
|
||||
resolveAuthProfileOrder,
|
||||
} from "./auth-profiles.js";
|
||||
export { requireApiKey, resolveAwsSdkEnvVarName } from "./model-auth-runtime-shared.js";
|
||||
export {
|
||||
formatMissingAuthError,
|
||||
requireApiKey,
|
||||
resolveAwsSdkEnvVarName,
|
||||
} from "./model-auth-runtime-shared.js";
|
||||
export type { ResolvedProviderAuth } from "./model-auth-runtime-shared.js";
|
||||
export type ProviderCredentialPrecedence = "profile-first" | "env-first";
|
||||
|
||||
|
||||
@@ -437,7 +437,15 @@ export async function loadCompactHooksHarness(): Promise<{
|
||||
applyAuthHeaderOverride: vi.fn((model: unknown) => model),
|
||||
applyLocalNoAuthHeaderOverride: vi.fn((model: unknown) => model),
|
||||
ensureAuthProfileStoreWithoutExternalProfiles: vi.fn(() => ({})),
|
||||
getApiKeyForModel: vi.fn(async () => ({ apiKey: "test", mode: "env" })),
|
||||
formatMissingAuthError: vi.fn(
|
||||
(auth: { mode: string; source: string }, provider: string) =>
|
||||
`No API key resolved for provider "${provider}" (auth mode: ${auth.mode}, checked: ${auth.source}).`,
|
||||
),
|
||||
getApiKeyForModel: vi.fn(async () => ({
|
||||
apiKey: "test",
|
||||
mode: "env",
|
||||
source: "test harness",
|
||||
})),
|
||||
resolveModelAuthMode: vi.fn(() => "env"),
|
||||
}));
|
||||
|
||||
|
||||
@@ -63,6 +63,7 @@ import { resolveHeartbeatPromptForSystemPrompt } from "../heartbeat-system-promp
|
||||
import {
|
||||
applyAuthHeaderOverride,
|
||||
applyLocalNoAuthHeaderOverride,
|
||||
formatMissingAuthError,
|
||||
getApiKeyForModel,
|
||||
resolveModelAuthMode,
|
||||
} from "../model-auth.js";
|
||||
@@ -537,9 +538,7 @@ async function compactEmbeddedPiSessionDirectOnce(
|
||||
|
||||
if (!apiKeyInfo.apiKey) {
|
||||
if (apiKeyInfo.mode !== "aws-sdk") {
|
||||
throw new Error(
|
||||
`No API key resolved for provider "${runtimeModel.provider}" (auth mode: ${apiKeyInfo.mode}).`,
|
||||
);
|
||||
throw new Error(formatMissingAuthError(apiKeyInfo, runtimeModel.provider));
|
||||
}
|
||||
} else {
|
||||
const preparedAuth = await prepareProviderRuntimeAuth({
|
||||
|
||||
@@ -198,6 +198,30 @@ describe("createEmbeddedRunAuthController", () => {
|
||||
expect(harness.runtimeAuthState?.profileId).toBe("default");
|
||||
});
|
||||
|
||||
it("includes the checked credential source when an api key is missing", async () => {
|
||||
const harness = createMutableAuthControllerHarness();
|
||||
const setRuntimeApiKey = vi.fn<(provider: string, apiKey: string) => void>();
|
||||
|
||||
mocks.getApiKeyForModel.mockResolvedValue({
|
||||
mode: "api-key",
|
||||
source: "models.providers.custom-openai",
|
||||
});
|
||||
|
||||
const controller = createMutableEmbeddedRunAuthController({
|
||||
harness,
|
||||
setRuntimeApiKey,
|
||||
});
|
||||
|
||||
await expect(controller.initializeAuthProfile()).rejects.toThrow(
|
||||
'No API key resolved for provider "custom-openai" (auth mode: api-key, checked: models.providers.custom-openai).',
|
||||
);
|
||||
expect(setRuntimeApiKey).not.toHaveBeenCalled();
|
||||
expect(harness.apiKeyInfo).toMatchObject({
|
||||
mode: "api-key",
|
||||
source: "models.providers.custom-openai",
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects privileged runtime transport overrides on the first auth exchange", async () => {
|
||||
let runtimeModel = createTestModel();
|
||||
|
||||
|
||||
@@ -9,7 +9,11 @@ import {
|
||||
} from "../../auth-profiles.js";
|
||||
import { FailoverError, resolveFailoverStatus } from "../../failover-error.js";
|
||||
import { shouldAllowCooldownProbeForReason } from "../../failover-policy.js";
|
||||
import { getApiKeyForModel, type ResolvedProviderAuth } from "../../model-auth.js";
|
||||
import {
|
||||
formatMissingAuthError,
|
||||
getApiKeyForModel,
|
||||
type ResolvedProviderAuth,
|
||||
} from "../../model-auth.js";
|
||||
import {
|
||||
classifyFailoverReason,
|
||||
isFailoverErrorMessage,
|
||||
@@ -365,9 +369,7 @@ export function createEmbeddedRunAuthController(params: {
|
||||
if (!apiKeyInfo.apiKey) {
|
||||
if (apiKeyInfo.mode !== "aws-sdk") {
|
||||
const runtimeModel = params.getRuntimeModel();
|
||||
throw new Error(
|
||||
`No API key resolved for provider "${runtimeModel.provider}" (auth mode: ${apiKeyInfo.mode}).`,
|
||||
);
|
||||
throw new Error(formatMissingAuthError(apiKeyInfo, runtimeModel.provider));
|
||||
}
|
||||
// AWS SDK auth via IMDS / instance role / ECS task role: no explicit API
|
||||
// key is available but the SDK default credential chain can resolve
|
||||
|
||||
@@ -28,6 +28,10 @@ vi.mock("./simple-completion-transport.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("./model-auth.js", () => ({
|
||||
formatMissingAuthError: vi.fn(
|
||||
(auth: { source: string; mode: string }, provider: string) =>
|
||||
`No API key resolved for provider "${provider}" (auth mode: ${auth.mode}, checked: ${auth.source}).`,
|
||||
),
|
||||
getApiKeyForModel: hoisted.getApiKeyForModelMock,
|
||||
applyLocalNoAuthHeaderOverride: hoisted.applyLocalNoAuthHeaderOverrideMock,
|
||||
}));
|
||||
@@ -170,7 +174,8 @@ describe("prepareSimpleCompletionModel", () => {
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
error: 'No API key resolved for provider "anthropic" (auth mode: api-key).',
|
||||
error:
|
||||
'No API key resolved for provider "anthropic" (auth mode: api-key, checked: models.providers.anthropic).',
|
||||
auth: {
|
||||
source: "models.providers.anthropic",
|
||||
mode: "api-key",
|
||||
|
||||
@@ -13,6 +13,7 @@ import { DEFAULT_PROVIDER } from "./defaults.js";
|
||||
import { resolveAgentHarnessPolicy } from "./harness/policy.js";
|
||||
import {
|
||||
applyLocalNoAuthHeaderOverride,
|
||||
formatMissingAuthError,
|
||||
getApiKeyForModel,
|
||||
type ResolvedProviderAuth,
|
||||
} from "./model-auth.js";
|
||||
@@ -235,7 +236,7 @@ export async function prepareSimpleCompletionModel(params: {
|
||||
})
|
||||
) {
|
||||
return {
|
||||
error: `No API key resolved for provider "${resolved.model.provider}" (auth mode: ${auth.mode}).`,
|
||||
error: formatMissingAuthError(auth, resolved.model.provider),
|
||||
auth,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user