mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-08 11:02:26 +00:00
fix(auth): apply runtime request overrides everywhere
This commit is contained in:
@@ -1242,7 +1242,7 @@ describe("microsoft-foundry plugin", () => {
|
||||
expect(normalized?.compat?.supportsReasoningEffort).toBe(false);
|
||||
});
|
||||
|
||||
it("keeps API-key credentials scoped to auth profiles", () => {
|
||||
it("deletes legacy provider-level credentials for API-key profiles", () => {
|
||||
const result = buildFoundryAuthResult({
|
||||
profileId: "microsoft-foundry:default",
|
||||
apiKey: "test-api-key",
|
||||
@@ -1256,6 +1256,9 @@ describe("microsoft-foundry plugin", () => {
|
||||
expect(provider.apiKey).toBeUndefined();
|
||||
expect(provider.authHeader).toBeUndefined();
|
||||
expect(provider.headers).toBeUndefined();
|
||||
expect(Object.hasOwn(provider, "apiKey")).toBe(true);
|
||||
expect(Object.hasOwn(provider, "authHeader")).toBe(true);
|
||||
expect(Object.hasOwn(provider, "headers")).toBe(true);
|
||||
});
|
||||
|
||||
it("uses the minimum supported response token count for GPT-5 connection tests", () => {
|
||||
@@ -1386,7 +1389,7 @@ describe("microsoft-foundry plugin", () => {
|
||||
expect(provider?.models[0]?.compat).toBeUndefined();
|
||||
});
|
||||
|
||||
it("keeps Entra credentials scoped to auth profiles", () => {
|
||||
it("deletes legacy provider-level credentials for Entra profiles", () => {
|
||||
const result = buildFoundryAuthResult({
|
||||
profileId: "microsoft-foundry:entra",
|
||||
apiKey: "__entra_id_dynamic__",
|
||||
@@ -1401,8 +1404,9 @@ describe("microsoft-foundry plugin", () => {
|
||||
| Record<string, unknown>
|
||||
| undefined;
|
||||
expect(provider?.authHeader).toBeUndefined();
|
||||
expect(Object.hasOwn(provider ?? {}, "apiKey")).toBe(false);
|
||||
expect(Object.hasOwn(provider ?? {}, "headers")).toBe(false);
|
||||
expect(Object.hasOwn(provider ?? {}, "apiKey")).toBe(true);
|
||||
expect(Object.hasOwn(provider ?? {}, "authHeader")).toBe(true);
|
||||
expect(Object.hasOwn(provider ?? {}, "headers")).toBe(true);
|
||||
expect(provider?.apiKey).toBeUndefined();
|
||||
expect(provider?.headers).toBeUndefined();
|
||||
});
|
||||
@@ -1826,7 +1830,7 @@ describe("microsoft-foundry plugin", () => {
|
||||
await expect(getAccessTokenResultAsync()).rejects.toThrow("Azure CLI is not logged in");
|
||||
});
|
||||
|
||||
it("keeps API-key secret refs scoped to auth profiles", () => {
|
||||
it("deletes legacy provider-level secret refs", () => {
|
||||
const secretRef = {
|
||||
source: "env" as const,
|
||||
provider: "default",
|
||||
@@ -1845,6 +1849,9 @@ describe("microsoft-foundry plugin", () => {
|
||||
expect(provider.apiKey).toBeUndefined();
|
||||
expect(provider.authHeader).toBeUndefined();
|
||||
expect(provider.headers).toBeUndefined();
|
||||
expect(Object.hasOwn(provider, "apiKey")).toBe(true);
|
||||
expect(Object.hasOwn(provider, "authHeader")).toBe(true);
|
||||
expect(Object.hasOwn(provider, "headers")).toBe(true);
|
||||
});
|
||||
|
||||
it("moves the selected Foundry auth profile to the front of auth.order", () => {
|
||||
|
||||
@@ -520,6 +520,9 @@ function buildFoundryProviderConfig(
|
||||
return {
|
||||
baseUrl: buildFoundryProviderBaseUrl(endpoint, modelId, modelNameHint, resolvedApi),
|
||||
api: resolvedApi,
|
||||
authHeader: undefined,
|
||||
apiKey: undefined,
|
||||
headers: undefined,
|
||||
models: deployments.map((deployment) => {
|
||||
const capabilities = resolveFoundryModelCapabilities(
|
||||
deployment.name,
|
||||
|
||||
@@ -27,6 +27,7 @@ import { EmbeddedBlockChunker, type BlockReplyChunking } from "./embedded-agent-
|
||||
import { resolveModelWithRegistry } from "./embedded-agent-runner/model.js";
|
||||
import { getActiveEmbeddedRunSnapshot } from "./embedded-agent-runner/runs.js";
|
||||
import { resolveEmbeddedAgentStreamFn } from "./embedded-agent-runner/stream-resolution.js";
|
||||
import { applyPreparedRuntimeAuthToModel } from "./provider-request-config.js";
|
||||
import { resolveAvailableAgentHarnessPolicy, selectAgentHarness } from "./harness/selection.js";
|
||||
import {
|
||||
resolveImageSanitizationLimits,
|
||||
@@ -483,12 +484,7 @@ export async function runBtwSideQuestion(
|
||||
profileId: resolvedAuthProfileId,
|
||||
},
|
||||
});
|
||||
if (preparedAuth?.baseUrl) {
|
||||
runtimeModel = {
|
||||
...runtimeModel,
|
||||
baseUrl: preparedAuth.baseUrl,
|
||||
};
|
||||
}
|
||||
runtimeModel = applyPreparedRuntimeAuthToModel(runtimeModel, preparedAuth);
|
||||
if (preparedAuth?.apiKey) {
|
||||
apiKey = preparedAuth.apiKey;
|
||||
}
|
||||
|
||||
@@ -96,6 +96,7 @@ import { isFallbackSummaryError, runWithModelFallback } from "../model-fallback.
|
||||
import { supportsModelTools } from "../model-tool-support.js";
|
||||
import { ensureOpenClawModelsJson } from "../models-config.js";
|
||||
import { wrapStreamFnTextTransforms } from "../plugin-text-transforms.js";
|
||||
import { applyPreparedRuntimeAuthToModel } from "../provider-request-config.js";
|
||||
import { resolveAgentPromptSurfaceForSessionKey } from "../prompt-surface.js";
|
||||
import { registerProviderStreamForModel } from "../provider-stream.js";
|
||||
import { collectRuntimeChannelCapabilities } from "../runtime-capabilities.js";
|
||||
@@ -671,9 +672,7 @@ async function compactEmbeddedAgentSessionDirectOnce(
|
||||
profileId: apiKeyInfo.profileId,
|
||||
},
|
||||
});
|
||||
if (preparedAuth?.baseUrl) {
|
||||
runtimeModel = { ...runtimeModel, baseUrl: preparedAuth.baseUrl };
|
||||
}
|
||||
runtimeModel = applyPreparedRuntimeAuthToModel(runtimeModel, preparedAuth);
|
||||
const runtimeApiKey = preparedAuth?.apiKey ?? apiKeyInfo.apiKey;
|
||||
hasRuntimeAuthExchange = Boolean(preparedAuth?.apiKey);
|
||||
if (!runtimeApiKey) {
|
||||
|
||||
@@ -24,8 +24,8 @@ import {
|
||||
type ResolvedProviderAuth,
|
||||
} from "../../model-auth.js";
|
||||
import {
|
||||
resolveProviderRequestConfig,
|
||||
sanitizeRuntimeProviderRequestOverrides,
|
||||
applyPreparedRuntimeAuthToModel,
|
||||
type ModelProviderRequestTransportOverrides,
|
||||
} from "../../provider-request-config.js";
|
||||
import { clampRuntimeAuthRefreshDelayMs } from "../../runtime-auth-refresh.js";
|
||||
import {
|
||||
@@ -88,41 +88,22 @@ export function createEmbeddedRunAuthController(params: {
|
||||
runtimeModel: Model;
|
||||
preparedAuth: {
|
||||
baseUrl?: string;
|
||||
request?: Parameters<typeof resolveProviderRequestConfig>[0]["request"];
|
||||
request?: ModelProviderRequestTransportOverrides;
|
||||
};
|
||||
}): void => {
|
||||
if (!paramsForApply.preparedAuth.baseUrl && !paramsForApply.preparedAuth.request) {
|
||||
const runtimeModel = applyPreparedRuntimeAuthToModel(
|
||||
paramsForApply.runtimeModel,
|
||||
paramsForApply.preparedAuth,
|
||||
);
|
||||
if (runtimeModel === paramsForApply.runtimeModel) {
|
||||
return;
|
||||
}
|
||||
const runtimeRequestConfig = resolveProviderRequestConfig({
|
||||
provider: paramsForApply.runtimeModel.provider,
|
||||
api: paramsForApply.runtimeModel.api,
|
||||
baseUrl: paramsForApply.preparedAuth.baseUrl ?? paramsForApply.runtimeModel.baseUrl,
|
||||
providerHeaders:
|
||||
paramsForApply.runtimeModel.headers &&
|
||||
typeof paramsForApply.runtimeModel.headers === "object"
|
||||
? paramsForApply.runtimeModel.headers
|
||||
: undefined,
|
||||
request: sanitizeRuntimeProviderRequestOverrides(paramsForApply.preparedAuth.request),
|
||||
capability: "llm",
|
||||
transport: "stream",
|
||||
});
|
||||
// Runtime auth plugins may override baseUrl and safe request auth headers,
|
||||
// but sanitizeRuntimeProviderRequestOverrides strips privileged transport knobs.
|
||||
params.setRuntimeModel({
|
||||
...paramsForApply.runtimeModel,
|
||||
...(paramsForApply.preparedAuth.baseUrl
|
||||
? { baseUrl: paramsForApply.preparedAuth.baseUrl }
|
||||
: {}),
|
||||
...(runtimeRequestConfig.headers ? { headers: runtimeRequestConfig.headers } : {}),
|
||||
});
|
||||
params.setEffectiveModel({
|
||||
...params.getEffectiveModel(),
|
||||
...(paramsForApply.preparedAuth.baseUrl
|
||||
? { baseUrl: paramsForApply.preparedAuth.baseUrl }
|
||||
: {}),
|
||||
...(runtimeRequestConfig.headers ? { headers: runtimeRequestConfig.headers } : {}),
|
||||
});
|
||||
// while the shared applier strips privileged transport knobs.
|
||||
params.setRuntimeModel(runtimeModel);
|
||||
params.setEffectiveModel(
|
||||
applyPreparedRuntimeAuthToModel(params.getEffectiveModel(), paramsForApply.preparedAuth),
|
||||
);
|
||||
};
|
||||
|
||||
const hasRefreshableRuntimeAuth = () =>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { describe, expect, it } from "vitest";
|
||||
import type { ConfiguredProviderRequest } from "../config/types.provider-request.js";
|
||||
import type { SecretRef } from "../config/types.secrets.js";
|
||||
import {
|
||||
applyPreparedRuntimeAuthToModel,
|
||||
buildProviderRequestDispatcherPolicy,
|
||||
mergeModelProviderRequestOverrides,
|
||||
mergeProviderRequestOverrides,
|
||||
@@ -15,6 +16,33 @@ import {
|
||||
} from "./provider-request-config.js";
|
||||
|
||||
describe("provider request config", () => {
|
||||
it("applies prepared runtime auth without retaining stale credential headers", () => {
|
||||
const model = {
|
||||
provider: "microsoft-foundry",
|
||||
api: "anthropic-messages" as const,
|
||||
baseUrl: "https://example.services.ai.azure.com/anthropic",
|
||||
headers: { "X-Tenant": "tenant-a", "x-api-key": "old-key" },
|
||||
};
|
||||
|
||||
const bearerModel = applyPreparedRuntimeAuthToModel(model, {
|
||||
request: { auth: { mode: "authorization-bearer", token: "entra-token" } },
|
||||
});
|
||||
expect(bearerModel.headers).toEqual({
|
||||
"X-Tenant": "tenant-a",
|
||||
Authorization: "Bearer entra-token",
|
||||
});
|
||||
|
||||
const apiKeyModel = applyPreparedRuntimeAuthToModel(bearerModel, {
|
||||
request: {
|
||||
auth: { mode: "header", headerName: "x-api-key", value: "profile-key" },
|
||||
},
|
||||
});
|
||||
expect(apiKeyModel.headers).toEqual({
|
||||
"X-Tenant": "tenant-a",
|
||||
"x-api-key": "profile-key",
|
||||
});
|
||||
});
|
||||
|
||||
it("merges discovered, provider, and model headers in precedence order", () => {
|
||||
// Later scopes override earlier scopes: discovery < provider < model.
|
||||
const resolved = resolveProviderRequestConfig({
|
||||
|
||||
@@ -551,6 +551,47 @@ export function sanitizeRuntimeProviderRequestOverrides(
|
||||
};
|
||||
}
|
||||
|
||||
/** Applies provider-prepared runtime auth overrides to a resolved model. */
|
||||
export function applyPreparedRuntimeAuthToModel<
|
||||
T extends {
|
||||
provider: string;
|
||||
api?: RequestApi;
|
||||
baseUrl?: string;
|
||||
headers?: Record<string, string>;
|
||||
},
|
||||
>(
|
||||
model: T,
|
||||
preparedAuth:
|
||||
| { baseUrl?: string; request?: ModelProviderRequestTransportOverrides }
|
||||
| null
|
||||
| undefined,
|
||||
): T {
|
||||
if (!preparedAuth?.baseUrl && !preparedAuth?.request) {
|
||||
return model;
|
||||
}
|
||||
const providerHeaders = preparedAuth.request?.auth
|
||||
? Object.fromEntries(
|
||||
Object.entries(model.headers ?? {}).filter(
|
||||
([key]) => !["authorization", "api-key", "x-api-key"].includes(key.toLowerCase()),
|
||||
),
|
||||
)
|
||||
: model.headers;
|
||||
const requestConfig = resolveProviderRequestConfig({
|
||||
provider: model.provider,
|
||||
api: model.api,
|
||||
baseUrl: preparedAuth.baseUrl ?? model.baseUrl,
|
||||
providerHeaders,
|
||||
request: sanitizeRuntimeProviderRequestOverrides(preparedAuth.request),
|
||||
capability: "llm",
|
||||
transport: "stream",
|
||||
});
|
||||
return {
|
||||
...model,
|
||||
...(preparedAuth.baseUrl ? { baseUrl: preparedAuth.baseUrl } : {}),
|
||||
headers: requestConfig.headers,
|
||||
};
|
||||
}
|
||||
|
||||
function resolveProxyOverride(
|
||||
request: ProviderRequestTransportOverrides | undefined,
|
||||
): ResolvedProviderRequestProxyConfig {
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
type ResolvedProviderAuth,
|
||||
} from "./model-auth.js";
|
||||
import { splitTrailingAuthProfile } from "./model-ref-profile.js";
|
||||
import { applyPreparedRuntimeAuthToModel } from "./provider-request-config.js";
|
||||
import {
|
||||
buildModelAliasIndex,
|
||||
resolveDefaultModelForAgent,
|
||||
@@ -38,7 +39,7 @@ type SimpleCompletionAuthStorage = {
|
||||
|
||||
type CompletionRuntimeCredential = {
|
||||
apiKey: string;
|
||||
baseUrl?: string;
|
||||
model: Model;
|
||||
};
|
||||
|
||||
type AllowedMissingApiKeyMode = ResolvedProviderAuth["mode"];
|
||||
@@ -158,7 +159,7 @@ async function setRuntimeApiKeyForCompletion(params: {
|
||||
params.authStorage.setRuntimeApiKey(params.model.provider, copilotToken.token);
|
||||
return {
|
||||
apiKey: copilotToken.token,
|
||||
baseUrl: copilotToken.baseUrl,
|
||||
model: { ...params.model, baseUrl: copilotToken.baseUrl },
|
||||
};
|
||||
}
|
||||
const preparedAuth = await prepareProviderRuntimeAuth({
|
||||
@@ -182,7 +183,7 @@ async function setRuntimeApiKeyForCompletion(params: {
|
||||
params.authStorage.setRuntimeApiKey(params.model.provider, runtimeApiKey);
|
||||
return {
|
||||
apiKey: runtimeApiKey,
|
||||
baseUrl: preparedAuth?.baseUrl,
|
||||
model: applyPreparedRuntimeAuthToModel(params.model, preparedAuth),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -271,13 +272,7 @@ export async function prepareSimpleCompletionModel(params: {
|
||||
profileId: auth.profileId,
|
||||
});
|
||||
resolvedApiKey = runtimeCredential.apiKey;
|
||||
const runtimeBaseUrl = runtimeCredential.baseUrl?.trim();
|
||||
if (runtimeBaseUrl) {
|
||||
resolvedModel = {
|
||||
...resolvedModel,
|
||||
baseUrl: runtimeBaseUrl,
|
||||
};
|
||||
}
|
||||
resolvedModel = runtimeCredential.model;
|
||||
}
|
||||
|
||||
const resolvedAuth: ResolvedProviderAuth = {
|
||||
|
||||
Reference in New Issue
Block a user