mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-09 03:22:40 +00:00
fix(foundry): bind auth and thinking contracts
This commit is contained in:
@@ -91,7 +91,18 @@ function requirePrepareRuntimeAuth(
|
||||
}
|
||||
|
||||
function requireRuntimeAuthResult(
|
||||
result: { apiKey?: string; baseUrl?: string; expiresAt?: number } | undefined,
|
||||
result:
|
||||
| {
|
||||
apiKey?: string;
|
||||
baseUrl?: string;
|
||||
expiresAt?: number;
|
||||
request?: {
|
||||
auth?:
|
||||
| { mode: "authorization-bearer"; token: string }
|
||||
| { mode: "header"; headerName: string; value: string };
|
||||
};
|
||||
}
|
||||
| undefined,
|
||||
) {
|
||||
if (!result) {
|
||||
throw new Error("expected Microsoft Foundry runtime auth result");
|
||||
@@ -450,11 +461,41 @@ describe("microsoft-foundry plugin", () => {
|
||||
);
|
||||
|
||||
expect(prepared.baseUrl).toBe("https://example.services.ai.azure.com/openai/v1");
|
||||
expect(prepared.request?.auth).toEqual({
|
||||
mode: "authorization-bearer",
|
||||
token: "test-token",
|
||||
});
|
||||
expect(execFileMock.mock.calls[0]?.[1]).toEqual(
|
||||
expect.arrayContaining(["--resource", COGNITIVE_SERVICES_RESOURCE]),
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["openai-responses", "api-key"],
|
||||
["anthropic-messages", "x-api-key"],
|
||||
] as const)("binds %s API-key auth to the active profile", async (api, headerName) => {
|
||||
const provider = registerProvider();
|
||||
const prepareRuntimeAuth = requirePrepareRuntimeAuth(provider);
|
||||
|
||||
const prepared = requireRuntimeAuthResult(
|
||||
await prepareRuntimeAuth(
|
||||
buildFoundryRuntimeAuthContext({
|
||||
apiKey: "profile-api-key",
|
||||
profileId: "microsoft-foundry:default",
|
||||
model: buildFoundryModel({ api }),
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
expect(prepared).toEqual({
|
||||
apiKey: "profile-api-key",
|
||||
request: {
|
||||
auth: { mode: "header", headerName, value: "profile-api-key" },
|
||||
},
|
||||
});
|
||||
expect(execFileMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("uses active model routing when Entra metadata points at another deployment", async () => {
|
||||
const provider = registerProvider();
|
||||
const prepareRuntimeAuth = requirePrepareRuntimeAuth(provider);
|
||||
@@ -1201,7 +1242,7 @@ describe("microsoft-foundry plugin", () => {
|
||||
expect(normalized?.compat?.supportsReasoningEffort).toBe(false);
|
||||
});
|
||||
|
||||
it("writes Azure API key header overrides for API-key auth configs", () => {
|
||||
it("keeps API-key credentials scoped to auth profiles", () => {
|
||||
const result = buildFoundryAuthResult({
|
||||
profileId: "microsoft-foundry:default",
|
||||
apiKey: "test-api-key",
|
||||
@@ -1212,9 +1253,9 @@ describe("microsoft-foundry plugin", () => {
|
||||
});
|
||||
|
||||
const provider = requireFoundryProviderPatch(result);
|
||||
expect(provider.apiKey).toBe("test-api-key");
|
||||
expect(provider.authHeader).toBe(false);
|
||||
expect(provider.headers).toEqual({ "api-key": "test-api-key" });
|
||||
expect(provider.apiKey).toBeUndefined();
|
||||
expect(provider.authHeader).toBeUndefined();
|
||||
expect(provider.headers).toBeUndefined();
|
||||
});
|
||||
|
||||
it("uses the minimum supported response token count for GPT-5 connection tests", () => {
|
||||
@@ -1331,7 +1372,7 @@ describe("microsoft-foundry plugin", () => {
|
||||
const provider = result.configPatch?.models?.providers?.["microsoft-foundry"];
|
||||
expect(provider?.baseUrl).toBe("https://example.services.ai.azure.com/anthropic");
|
||||
expect(provider?.api).toBe("anthropic-messages");
|
||||
expect(provider?.authHeader).toBe(true);
|
||||
expect(provider?.authHeader).toBeUndefined();
|
||||
expect(provider?.models[0]).toMatchObject({
|
||||
id: "prod-fable",
|
||||
name: "claude-fable-5",
|
||||
@@ -1345,7 +1386,7 @@ describe("microsoft-foundry plugin", () => {
|
||||
expect(provider?.models[0]?.compat).toBeUndefined();
|
||||
});
|
||||
|
||||
it("clears stale API-key credentials when writing Entra provider patches", () => {
|
||||
it("keeps Entra credentials scoped to auth profiles", () => {
|
||||
const result = buildFoundryAuthResult({
|
||||
profileId: "microsoft-foundry:entra",
|
||||
apiKey: "__entra_id_dynamic__",
|
||||
@@ -1359,9 +1400,9 @@ describe("microsoft-foundry plugin", () => {
|
||||
const provider = result.configPatch?.models?.providers?.["microsoft-foundry"] as
|
||||
| Record<string, unknown>
|
||||
| undefined;
|
||||
expect(provider?.authHeader).toBe(true);
|
||||
expect(Object.hasOwn(provider ?? {}, "apiKey")).toBe(true);
|
||||
expect(Object.hasOwn(provider ?? {}, "headers")).toBe(true);
|
||||
expect(provider?.authHeader).toBeUndefined();
|
||||
expect(Object.hasOwn(provider ?? {}, "apiKey")).toBe(false);
|
||||
expect(Object.hasOwn(provider ?? {}, "headers")).toBe(false);
|
||||
expect(provider?.apiKey).toBeUndefined();
|
||||
expect(provider?.headers).toBeUndefined();
|
||||
});
|
||||
@@ -1463,6 +1504,28 @@ describe("microsoft-foundry plugin", () => {
|
||||
],
|
||||
});
|
||||
}
|
||||
for (const modelName of [
|
||||
"claude-opus-4-1",
|
||||
"claude-opus-4-5",
|
||||
"claude-sonnet-4-5",
|
||||
"claude-haiku-4-5",
|
||||
]) {
|
||||
expect(
|
||||
provider.resolveThinkingProfile?.({
|
||||
provider: "microsoft-foundry",
|
||||
modelId: `prod-${modelName}`,
|
||||
params: { canonicalModelId: modelName },
|
||||
}),
|
||||
).toMatchObject({
|
||||
levels: [
|
||||
{ id: "off" },
|
||||
{ id: "minimal" },
|
||||
{ id: "low" },
|
||||
{ id: "medium" },
|
||||
{ id: "high" },
|
||||
],
|
||||
});
|
||||
}
|
||||
expect(
|
||||
provider.resolveThinkingProfile?.({
|
||||
provider: "microsoft-foundry",
|
||||
@@ -1763,7 +1826,7 @@ describe("microsoft-foundry plugin", () => {
|
||||
await expect(getAccessTokenResultAsync()).rejects.toThrow("Azure CLI is not logged in");
|
||||
});
|
||||
|
||||
it("keeps Azure API key header overrides when API-key auth uses a secret ref", () => {
|
||||
it("keeps API-key secret refs scoped to auth profiles", () => {
|
||||
const secretRef = {
|
||||
source: "env" as const,
|
||||
provider: "default",
|
||||
@@ -1779,9 +1842,9 @@ describe("microsoft-foundry plugin", () => {
|
||||
});
|
||||
|
||||
const provider = requireFoundryProviderPatch(result);
|
||||
expect(provider.apiKey).toBe(secretRef);
|
||||
expect(provider.authHeader).toBe(false);
|
||||
expect(provider.headers).toEqual({ "api-key": secretRef });
|
||||
expect(provider.apiKey).toBeUndefined();
|
||||
expect(provider.authHeader).toBeUndefined();
|
||||
expect(provider.headers).toBeUndefined();
|
||||
});
|
||||
|
||||
it("moves the selected Foundry auth profile to the front of auth.order", () => {
|
||||
|
||||
@@ -51,7 +51,16 @@ async function refreshEntraToken(params?: {
|
||||
|
||||
export async function prepareFoundryRuntimeAuth(ctx: ProviderPrepareRuntimeAuthContext) {
|
||||
if (ctx.apiKey !== "__entra_id_dynamic__") {
|
||||
return null;
|
||||
return {
|
||||
apiKey: ctx.apiKey,
|
||||
request: {
|
||||
auth: {
|
||||
mode: "header" as const,
|
||||
headerName: ctx.model.api === ANTHROPIC_MESSAGES_API ? "x-api-key" : "api-key",
|
||||
value: ctx.apiKey,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
try {
|
||||
const authStore = ensureAuthProfileStore(ctx.agentDir, {
|
||||
@@ -102,6 +111,9 @@ export async function prepareFoundryRuntimeAuth(ctx: ProviderPrepareRuntimeAuthC
|
||||
apiKey: cachedToken.token,
|
||||
expiresAt: cachedToken.expiresAt,
|
||||
...(baseUrl ? { baseUrl } : {}),
|
||||
request: {
|
||||
auth: { mode: "authorization-bearer" as const, token: cachedToken.token },
|
||||
},
|
||||
};
|
||||
}
|
||||
let refreshPromise = refreshPromises.get(cacheKey);
|
||||
@@ -119,6 +131,9 @@ export async function prepareFoundryRuntimeAuth(ctx: ProviderPrepareRuntimeAuthC
|
||||
return {
|
||||
...token,
|
||||
...(baseUrl ? { baseUrl } : {}),
|
||||
request: {
|
||||
auth: { mode: "authorization-bearer" as const, token: token.apiKey },
|
||||
},
|
||||
};
|
||||
} catch (err) {
|
||||
const details = formatErrorMessage(err);
|
||||
|
||||
@@ -225,6 +225,13 @@ export function requiresFoundryMandatoryAdaptiveClaudeThinking(value?: string |
|
||||
: false;
|
||||
}
|
||||
|
||||
function supportsFoundryManualClaudeThinking(value?: string | null): boolean {
|
||||
const normalized = normalizeFoundryModelName(value)?.replace(/\./g, "-");
|
||||
return normalized
|
||||
? /(?:^|-)claude-(?:opus-4-(?:1|5)|sonnet-4-5|haiku-4-5)(?=$|[^a-z0-9])/.test(normalized)
|
||||
: false;
|
||||
}
|
||||
|
||||
function resolveFoundryModelTokenLimits(value?: string | null): {
|
||||
contextWindow: number;
|
||||
maxTokens: number;
|
||||
@@ -448,6 +455,7 @@ export function resolveFoundryModelCapabilities(
|
||||
const supportsClaudeThinking =
|
||||
isAnthropic &&
|
||||
(supportsClaudeAdaptiveThinking({ id: modelName }) ||
|
||||
supportsFoundryManualClaudeThinking(modelName) ||
|
||||
requiresFoundryMandatoryAdaptiveClaudeThinking(modelName));
|
||||
const supportsClaudeXhighThinking =
|
||||
isAnthropic && supportsClaudeNativeXhighEffort({ id: modelName });
|
||||
@@ -502,14 +510,9 @@ function buildFoundryProviderConfig(
|
||||
modelNameHint?: string | null,
|
||||
options?: {
|
||||
api?: FoundryProviderApi;
|
||||
authMethod?: "api-key" | "entra-id";
|
||||
apiKey?: SecretInput;
|
||||
deployments?: FoundryDeploymentConfigInput[];
|
||||
},
|
||||
): FoundryProviderConfigPatch {
|
||||
const runtimeApiKey = options?.authMethod === "api-key" ? options.apiKey : undefined;
|
||||
const isApiKeyAuth = options?.authMethod === "api-key";
|
||||
const isEntraIdAuth = options?.authMethod === "entra-id";
|
||||
const resolvedApi = resolveFoundryApi(modelId, modelNameHint, options?.api);
|
||||
const deployments = options?.deployments?.length
|
||||
? options.deployments
|
||||
@@ -517,16 +520,6 @@ function buildFoundryProviderConfig(
|
||||
return {
|
||||
baseUrl: buildFoundryProviderBaseUrl(endpoint, modelId, modelNameHint, resolvedApi),
|
||||
api: resolvedApi,
|
||||
...(isApiKeyAuth
|
||||
? {
|
||||
authHeader: false,
|
||||
...(runtimeApiKey !== undefined
|
||||
? { apiKey: runtimeApiKey, headers: { "api-key": runtimeApiKey } }
|
||||
: {}),
|
||||
}
|
||||
: isEntraIdAuth
|
||||
? { authHeader: true, apiKey: undefined, headers: undefined }
|
||||
: {}),
|
||||
models: deployments.map((deployment) => {
|
||||
const capabilities = resolveFoundryModelCapabilities(
|
||||
deployment.name,
|
||||
@@ -734,8 +727,6 @@ export function buildFoundryAuthResult(params: {
|
||||
params.modelNameHint,
|
||||
{
|
||||
api: params.api,
|
||||
authMethod: params.authMethod,
|
||||
apiKey: params.apiKey,
|
||||
deployments: params.deployments,
|
||||
},
|
||||
) as unknown as ModelProviderConfig,
|
||||
|
||||
Reference in New Issue
Block a user