You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
When OpenAIChatCompletionClient is constructed with credential= (keyless / Entra auth) against a Microsoft Foundry project OpenAI-compatible surface — https://<account>.services.ai.azure.com/api/projects/<project>/openai/v1 — every request fails with HTTP 401. The SDK always requests a token for the https://cognitiveservices.azure.com/.default scope, but that endpoint requires the https://ai.azure.com/.default audience. There is no public way to override the scope.
Expected behavior
Keyless auth against a Foundry project endpoint succeeds, or there is a supported parameter to set the token scope/audience.
Actual behavior
Every call 401s. The response body itself states the expected audience is https://ai.azure.com.
Minimal reproduction (steps)
Deploy or target a Foundry project endpoint (.../api/projects/<project>/openai/v1).
Build the client with credential=DefaultAzureCredential() (see Code Sample).
Call get_response(...) → 401.
Root cause
In agent_framework_openai/_shared.py the scope is a module-level constant with no override path:
line 33: AZURE_OPENAI_TOKEN_SCOPE = "https://cognitiveservices.azure.com/.default"
_resolve_azure_credential_to_token_provider() lines 350 & 352 pass this constant to get_(async_)bearer_token_provider(credential, AZURE_OPENAI_TOKEN_SCOPE) for every credential path.
Foundry account endpoints accept cognitiveservices.azure.com, but the project-scoped surface requires ai.azure.com; the two are not interchangeable (verified: ai.azure.com token → 200, cognitiveservices token → 401 on the same project URL). This hits Foundry Hosted Agents directly, since the platform injects FOUNDRY_PROJECT_ENDPOINT (a project endpoint) and the recommended keyless pattern is DefaultAzureCredential.
Requested fix
Add a public credential_scopes: Sequence[str] | None = None parameter to OpenAIChatCompletionClient (and the Azure client family), falling back to AZURE_OPENAI_TOKEN_SCOPE when unset:
For reference, openai's AsyncAzureOpenAI and azure-ai-inference both let callers choose the token scope; the hardcoding here is the outlier.
Related sub-issue (same file): a synchronous callable passed as api_key is not awaited
The natural workaround for the scope problem is to pass a bearer-token provider as a callable api_key. A synchronous provider fails with object str can't be used in 'await' expression; only the async variant (azure.identity.aio) works.
Cause: _shared.py already has _ensure_async_token_provider() (lines 314–329) that normalizes a possibly-sync provider into an awaitable, but it is applied only to the azure_ad_token_provider path (line 302), not to a user-supplied callable api_key. A sync callable api_key therefore reaches AsyncOpenAI unwrapped and gets awaited as a bare str.
Requested fix: route callable api_key values through _ensure_async_token_provider() too, so both sync and async token providers work. At minimum, document that api_key callables must return Awaitable[str].
Code Sample
# --- fails: 401, scope hardcoded to cognitiveservices ---fromagent_framework.openaiimportOpenAIChatCompletionClientfromazure.identityimportDefaultAzureCredentialclient=OpenAIChatCompletionClient(
model="gpt-5.4-mini",
base_url="https://<account>.services.ai.azure.com/api/projects/<project>/openai/v1",
credential=DefaultAzureCredential(),
)
# await client.get_response(...) -> 401# --- workaround that WORKS today (async provider as callable api_key) ---fromazure.identity.aioimportDefaultAzureCredential, get_bearer_token_providertoken_provider=get_bearer_token_provider(
DefaultAzureCredential(), "https://ai.azure.com/.default"
)
client=OpenAIChatCompletionClient(
model="gpt-5.4-mini",
base_url="https://<account>.services.ai.azure.com/api/projects/<project>/openai/v1",
api_key=token_provider, # async callable -> OK
)
# --- same workaround but SYNC provider -> raises ---fromazure.identityimportDefaultAzureCredential, get_bearer_token_providertoken_provider=get_bearer_token_provider(
DefaultAzureCredential(), "https://ai.azure.com/.default"
)
# api_key=token_provider -> object str can't be used in 'await' expression
Error Messages / Stack Traces
# 401 from the project endpoint (scope mismatch)
openai.AuthenticationError: Error code: 401 - {'statusCode': 401,
'message': 'Unauthorized. Access token is missing, invalid,
audience is incorrect (https://ai.azure.com), or have expired.'}
# sync callable api_key
agent_framework.exceptions.ChatClientException:
(... service failed to complete the prompt: object str can't be used in 'await' expression,
TypeError("object str can't be used in 'await' expression"))
Package Versions
agent-framework-openai: 1.10.1 (also reproduced on 1.4.0)
agent-framework-core: 1.11.0
azure-identity: 1.26.0b2
Python Version
3.12
Additional Context
Sanity check for an endpoint's audience: az account get-access-token --resource https://ai.azure.com vs --resource https://cognitiveservices.azure.com, then curl the /openai/v1/chat/completions path with each bearer.
agent_framework.foundry.FoundryChatClient (from agent-framework-foundry) uses the correct audience internally and is a working alternative. This issue is specifically about keeping the plain Chat Completions path via OpenAIChatCompletionClient usable with keyless auth against a project endpoint.
Endpoint host/project names are masked (<account> / <project>).
Description
High-level problem
When
OpenAIChatCompletionClientis constructed withcredential=(keyless / Entra auth) against a Microsoft Foundry project OpenAI-compatible surface —https://<account>.services.ai.azure.com/api/projects/<project>/openai/v1— every request fails with HTTP 401. The SDK always requests a token for thehttps://cognitiveservices.azure.com/.defaultscope, but that endpoint requires thehttps://ai.azure.com/.defaultaudience. There is no public way to override the scope.Expected behavior
Keyless auth against a Foundry project endpoint succeeds, or there is a supported parameter to set the token scope/audience.
Actual behavior
Every call 401s. The response body itself states the expected audience is
https://ai.azure.com.Minimal reproduction (steps)
.../api/projects/<project>/openai/v1).credential=DefaultAzureCredential()(see Code Sample).get_response(...)→ 401.Root cause
In
agent_framework_openai/_shared.pythe scope is a module-level constant with no override path:AZURE_OPENAI_TOKEN_SCOPE = "https://cognitiveservices.azure.com/.default"_resolve_azure_credential_to_token_provider()lines 350 & 352 pass this constant toget_(async_)bearer_token_provider(credential, AZURE_OPENAI_TOKEN_SCOPE)for every credential path.Foundry account endpoints accept
cognitiveservices.azure.com, but the project-scoped surface requiresai.azure.com; the two are not interchangeable (verified:ai.azure.comtoken → 200,cognitiveservicestoken → 401 on the same project URL). This hits Foundry Hosted Agents directly, since the platform injectsFOUNDRY_PROJECT_ENDPOINT(a project endpoint) and the recommended keyless pattern isDefaultAzureCredential.Requested fix
Add a public
credential_scopes: Sequence[str] | None = Noneparameter toOpenAIChatCompletionClient(and the Azure client family), falling back toAZURE_OPENAI_TOKEN_SCOPEwhen unset:For reference,
openai'sAsyncAzureOpenAIandazure-ai-inferenceboth let callers choose the token scope; the hardcoding here is the outlier.Related sub-issue (same file): a synchronous callable passed as
api_keyis not awaitedThe natural workaround for the scope problem is to pass a bearer-token provider as a callable
api_key. A synchronous provider fails withobject str can't be used in 'await' expression; only the async variant (azure.identity.aio) works.Cause:
_shared.pyalready has_ensure_async_token_provider()(lines 314–329) that normalizes a possibly-sync provider into an awaitable, but it is applied only to theazure_ad_token_providerpath (line 302), not to a user-supplied callableapi_key. A sync callableapi_keytherefore reachesAsyncOpenAIunwrapped and getsawaited as a barestr.Requested fix: route callable
api_keyvalues through_ensure_async_token_provider()too, so both sync and async token providers work. At minimum, document thatapi_keycallables must returnAwaitable[str].Code Sample
Error Messages / Stack Traces
Package Versions
Python Version
3.12
Additional Context
az account get-access-token --resource https://ai.azure.comvs--resource https://cognitiveservices.azure.com, thencurlthe/openai/v1/chat/completionspath with each bearer.agent_framework.foundry.FoundryChatClient(fromagent-framework-foundry) uses the correct audience internally and is a working alternative. This issue is specifically about keeping the plain Chat Completions path viaOpenAIChatCompletionClientusable with keyless auth against a project endpoint.<account>/<project>).agent-framework-ag-ui(Foundry project endpoint) #5941 hit the same Foundry project-endpoint environment but is about multi-turn tool-result replay, not auth.