mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-28 07:33:38 +00:00
fix(foundry): scope Entra tokens by API
This commit is contained in:
@@ -113,19 +113,19 @@ function parseAzJson(raw: string, label: string): unknown {
|
||||
}
|
||||
|
||||
type AccessTokenParams = {
|
||||
scope?: string;
|
||||
subscriptionId?: string;
|
||||
tenantId?: string;
|
||||
};
|
||||
|
||||
function buildAccessTokenArgs(params?: AccessTokenParams): string[] {
|
||||
const args = [
|
||||
"account",
|
||||
"get-access-token",
|
||||
"--resource",
|
||||
COGNITIVE_SERVICES_RESOURCE,
|
||||
"--output",
|
||||
"json",
|
||||
];
|
||||
const args = ["account", "get-access-token"];
|
||||
if (params?.scope) {
|
||||
args.push("--scope", params.scope);
|
||||
} else {
|
||||
args.push("--resource", COGNITIVE_SERVICES_RESOURCE);
|
||||
}
|
||||
args.push("--output", "json");
|
||||
if (params?.subscriptionId) {
|
||||
args.push("--subscription", params.subscriptionId);
|
||||
} else if (params?.tenantId) {
|
||||
|
||||
@@ -14,6 +14,8 @@ import {
|
||||
} from "./onboard.js";
|
||||
import { resetFoundryRuntimeAuthCaches } from "./runtime.js";
|
||||
import {
|
||||
COGNITIVE_SERVICES_RESOURCE,
|
||||
FOUNDRY_ANTHROPIC_SCOPE,
|
||||
buildFoundryAuthResult,
|
||||
isAnthropicFoundryDeployment,
|
||||
isFoundryMaiImageModel,
|
||||
@@ -436,6 +438,9 @@ describe("microsoft-foundry plugin", () => {
|
||||
);
|
||||
|
||||
expect(prepared.baseUrl).toBe("https://example.services.ai.azure.com/openai/v1");
|
||||
expect(execFileMock.mock.calls[0]?.[1]).toEqual(
|
||||
expect.arrayContaining(["--resource", COGNITIVE_SERVICES_RESOURCE]),
|
||||
);
|
||||
});
|
||||
|
||||
it("uses active model routing when Entra metadata points at another deployment", async () => {
|
||||
@@ -466,6 +471,61 @@ describe("microsoft-foundry plugin", () => {
|
||||
);
|
||||
|
||||
expect(prepared.baseUrl).toBe("https://example.services.ai.azure.com/anthropic");
|
||||
expect(execFileMock.mock.calls[0]?.[1]).toEqual(
|
||||
expect.arrayContaining(["--scope", FOUNDRY_ANTHROPIC_SCOPE]),
|
||||
);
|
||||
});
|
||||
|
||||
it("does not reuse OpenAI Entra tokens for Anthropic Foundry deployments", async () => {
|
||||
const provider = registerProvider();
|
||||
const prepareRuntimeAuth = requirePrepareRuntimeAuth(provider);
|
||||
mockAzureCliToken({ accessToken: "gpt-token", expiresInMs: 60_000 });
|
||||
mockAzureCliToken({ accessToken: "claude-token", expiresInMs: 60_000 });
|
||||
ensureAuthProfileStoreMock.mockReturnValue(
|
||||
buildEntraProfileStore({
|
||||
endpoint: "https://example.services.ai.azure.com",
|
||||
modelId: "deployment-gpt5",
|
||||
modelName: "gpt-5.4",
|
||||
api: "openai-responses",
|
||||
}),
|
||||
);
|
||||
|
||||
const gptPrepared = requireRuntimeAuthResult(
|
||||
await prepareRuntimeAuth(
|
||||
buildFoundryRuntimeAuthContext({
|
||||
modelId: "deployment-gpt5",
|
||||
model: buildFoundryModel({
|
||||
id: "deployment-gpt5",
|
||||
name: "gpt-5.4",
|
||||
api: "openai-responses",
|
||||
baseUrl: "https://example.services.ai.azure.com/openai/v1",
|
||||
}),
|
||||
}),
|
||||
),
|
||||
);
|
||||
const claudePrepared = requireRuntimeAuthResult(
|
||||
await prepareRuntimeAuth(
|
||||
buildFoundryRuntimeAuthContext({
|
||||
modelId: "deployment-fable",
|
||||
model: buildFoundryModel({
|
||||
id: "deployment-fable",
|
||||
name: "claude-fable-5",
|
||||
api: "anthropic-messages",
|
||||
baseUrl: "https://example.services.ai.azure.com/anthropic",
|
||||
}),
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
expect(gptPrepared.apiKey).toBe("gpt-token");
|
||||
expect(claudePrepared.apiKey).toBe("claude-token");
|
||||
expect(execFileMock).toHaveBeenCalledTimes(2);
|
||||
expect(execFileMock.mock.calls[0]?.[1]).toEqual(
|
||||
expect.arrayContaining(["--resource", COGNITIVE_SERVICES_RESOURCE]),
|
||||
);
|
||||
expect(execFileMock.mock.calls[1]?.[1]).toEqual(
|
||||
expect.arrayContaining(["--scope", FOUNDRY_ANTHROPIC_SCOPE]),
|
||||
);
|
||||
});
|
||||
|
||||
it("retries Entra token refresh after a failed attempt", async () => {
|
||||
|
||||
@@ -10,7 +10,9 @@ import { ensureAuthProfileStore } from "openclaw/plugin-sdk/provider-auth";
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { getAccessTokenResultAsync } from "./cli.js";
|
||||
import {
|
||||
ANTHROPIC_MESSAGES_API,
|
||||
type CachedTokenEntry,
|
||||
FOUNDRY_ANTHROPIC_SCOPE,
|
||||
TOKEN_REFRESH_MARGIN_MS,
|
||||
buildFoundryProviderBaseUrl,
|
||||
extractFoundryEndpoint,
|
||||
@@ -29,6 +31,7 @@ export function resetFoundryRuntimeAuthCaches(): void {
|
||||
}
|
||||
|
||||
async function refreshEntraToken(params?: {
|
||||
scope?: string;
|
||||
subscriptionId?: string;
|
||||
tenantId?: string;
|
||||
}): Promise<{ apiKey: string; expiresAt: number }> {
|
||||
@@ -78,10 +81,13 @@ export async function prepareFoundryRuntimeAuth(ctx: ProviderPrepareRuntimeAuthC
|
||||
const endpoint =
|
||||
extractFoundryEndpoint(ctx.model.baseUrl ?? "") ??
|
||||
normalizeOptionalString(metadata?.endpoint);
|
||||
const tokenScope =
|
||||
configuredApi === ANTHROPIC_MESSAGES_API ? FOUNDRY_ANTHROPIC_SCOPE : undefined;
|
||||
const baseUrl = endpoint
|
||||
? buildFoundryProviderBaseUrl(endpoint, modelId, modelNameHint, configuredApi)
|
||||
: undefined;
|
||||
const cacheKey = getFoundryTokenCacheKey({
|
||||
scope: tokenScope,
|
||||
subscriptionId: metadata?.subscriptionId,
|
||||
tenantId: metadata?.tenantId,
|
||||
});
|
||||
@@ -101,6 +107,7 @@ export async function prepareFoundryRuntimeAuth(ctx: ProviderPrepareRuntimeAuthC
|
||||
let refreshPromise = refreshPromises.get(cacheKey);
|
||||
if (!refreshPromise) {
|
||||
refreshPromise = refreshEntraToken({
|
||||
scope: tokenScope,
|
||||
subscriptionId: metadata?.subscriptionId,
|
||||
tenantId: metadata?.tenantId,
|
||||
}).finally(() => {
|
||||
|
||||
@@ -3,14 +3,17 @@ export {
|
||||
TOKEN_REFRESH_MARGIN_MS,
|
||||
buildFoundryProviderBaseUrl,
|
||||
extractFoundryEndpoint,
|
||||
FOUNDRY_ANTHROPIC_SCOPE,
|
||||
isFoundryProviderApi,
|
||||
resolveConfiguredModelNameHint,
|
||||
ANTHROPIC_MESSAGES_API,
|
||||
type CachedTokenEntry,
|
||||
} from "./shared.js";
|
||||
|
||||
export function getFoundryTokenCacheKey(params?: {
|
||||
scope?: string;
|
||||
subscriptionId?: string;
|
||||
tenantId?: string;
|
||||
}): string {
|
||||
return `${params?.subscriptionId ?? ""}:${params?.tenantId ?? ""}`;
|
||||
return `${params?.scope ?? ""}:${params?.subscriptionId ?? ""}:${params?.tenantId ?? ""}`;
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ export const PROVIDER_ID = "microsoft-foundry";
|
||||
export const DEFAULT_API = "openai-completions";
|
||||
export const DEFAULT_GPT5_API = "openai-responses";
|
||||
export const COGNITIVE_SERVICES_RESOURCE = "https://cognitiveservices.azure.com";
|
||||
export const FOUNDRY_ANTHROPIC_SCOPE = "https://ai.azure.com/.default";
|
||||
export const TOKEN_REFRESH_MARGIN_MS = 5 * 60 * 1000;
|
||||
export const MAI_IMAGE_MODELS = [
|
||||
"MAI-Image-2.5-Flash",
|
||||
|
||||
Reference in New Issue
Block a user