fix(auth): apply runtime request overrides everywhere

This commit is contained in:
Vincent Koc
2026-06-11 09:09:04 +09:00
parent 25bea06596
commit abec88cfab
8 changed files with 106 additions and 56 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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({

View File

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

View File

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