In this post, let's see how we can move .NET clients from consuming Azure AI Language and Azure AI Translator resource onto a single Azure AI Foundry resource.
I had some .NET code using TextAnalyticsClient (Azure.AI.TextAnalytics) and TextTranslationClient (Azure.AI.Translation.Text). These were pointing to two different Azure AI resources.
// API Kind: TextAnalytics https://lang-demo-service-001.cognitiveservices.azure.com // API Kind: TextTranslation https://trsl-demo-service-001.cognitiveservices.azure.com
These are the NuGet packages (latest as of today, it will change).
<ItemGroup> <PackageReference Include="Azure.AI.TextAnalytics" Version="5.3.0" /> <PackageReference Include="Azure.AI.Translation.Text" Version="2.0.0" /> <PackageReference Include="Azure.Identity" Version="1.21.0" /> </ItemGroup>
And this is the existing code.
using Azure; using Azure.AI.TextAnalytics; using Azure.AI.Translation.Text; using Azure.Identity; DefaultAzureCredential credentials = new DefaultAzureCredential(); // API Kind: TextAnalytics string languageEndpoint = "https://lang-demo-service-001.cognitiveservices.azure.com"; TextAnalyticsClient textAnalyticsClient = new(new Uri(languageEndpoint), credentials); Azure.AI.TextAnalytics.DetectedLanguage response = await textAnalyticsClient.DetectLanguageAsync("Hello"); Console.WriteLine(response.Name); // API Kind: TextTranslation string translatorEndpoint = "https://trsl-demo-service-001.cognitiveservices.azure.com"; TextTranslationClient textTranslationClient = new(credentials, new Uri(translatorEndpoint)); Response<IReadOnlyList<TranslatedTextItem>> translateTextResponse = await textTranslationClient.TranslateAsync("fr", "Hello", "en"); TranslatedTextItem? translatedTextItem = translateTextResponse.Value.FirstOrDefault(); Console.WriteLine(translatedTextItem?.Translations.FirstOrDefault()?.Text);
I didn't want to maintain more services when we can have single Microsoft Foundry resource (API Kind: AIServices). It's a multi-service resource, it bundles Language, Translator, Speech, Vision and more, so it's one resource, one credential and one endpoint in configuration.
I thought that should be a simple endpoint change.
string apiEndpoint = "https://aif-demo-service-001.services.ai.azure.com"; DefaultAzureCredential credentials = new DefaultAzureCredential(); // Language TextAnalyticsClient textAnalyticsClient = new(new Uri(apiEndpoint), credentials); // Translation TextTranslationClient textTranslationClient = new(credentials, new Uri(apiEndpoint)); // Omitted for brevity
TextAnalyticsClient seemed to work, it wrote out English. But TextTranslationClient didn't.
English
Unhandled exception. Azure.RequestFailedException: Resource not found Status: 404(Resource Not Found) ErrorCode: 404 Content: { "error":{ "code":"404","message": "Resource not found"} } Headers: apim - request - id: REDACTED Strict-Transport-Security: REDACTED X-Content-Type-Options: REDACTED Date: Tue, 11 Aug 2026 08:57:57 GMT Content-Length: 56 Content - Type: application / json at Azure.AI.Translation.Text.ClientPipelineExtensions.ProcessMessageAsync(HttpPipeline pipeline, HttpMessage message, RequestContext context) at Azure.AI.Translation.Text.TextTranslationClient.TranslateAsync(RequestContent content, String clientTraceId, RequestContext context) at Azure.AI.Translation.Text.TextTranslationClient.TranslateAsync(IEnumerable`1 inputs, CancellationToken cancellationToken) at Program.< Main >$(String[] args) in C: \Users\Jaliya\Desktop\ConsoleApp1\ConsoleApp1\Program.cs:line 45 at Program.<Main>(String[] args)
My first thought was that Translator simply isn't served on the services.ai.azure.com endpoint. That's not it, a raw request against the very same endpoint works.
$token = az account get-access-token ` --resource https://cognitiveservices.azure.com ` --query accessToken -o tsv curl -X POST "https://aif-demo-service-001.services.ai.azure.com/translator/text/v3.0/translate?api-version=3.0&to=fr" ` -H "Authorization: Bearer $token" ` -H "Content-Type: application/json" ` -d "[{'Text':'Hello'}]" # 200 OK #[ # { # "detectedLanguage": { # "language": "en", # "score": 1.0 # }, # "translations": [ # { # "text": "Bonjour", # "to": "fr" # } # ] # } #]
So the host is fine, and note the route, Translator lives under /translator/text. May be something fishy with the SDK.
Looking at the source of Azure.AI.Translation.Text, this is how it decides whether to add that route prefix.
private const string PLATFORM_HOST = "cognitiveservices"; internal static bool IsPlatformHost(this Uri uri) { return uri.Host?.Contains(PLATFORM_HOST) == true; }
Which is then used in the constructor.
private const string PLATFORM_PATH = "/translator/text"; if (endpoint.IsPlatformHost()) { this._endpoint = new Uri(endpoint, PLATFORM_PATH); }
There it is. The prefix is added only when the endpoint host contains the literal string cognitiveservices.
My old endpoint was trsl-demo-service-001.cognitiveservices.azure.com, which contains it, so the SDK built /translator/text/translate. The new one is aif-demo-service-001.services.ai.azure.com, which doesn't, so the SDK treated it like a global endpoint and posted to /translate. Hence the 404.
The fix is to pass the prefix ourselves when constructing the Uri.
using Azure; using Azure.AI.Translation.Text; using Azure.Identity; string apiEndpoint = "https://aif-inf-sl-tenant1-dev-001.services.ai.azure.com"; DefaultAzureCredential credentials = new DefaultAzureCredential(); // Translation TextTranslationClient textTranslationClient = new(credentials, new Uri($"{apiEndpoint}/translator/text")); Response<IReadOnlyList<TranslatedTextItem>> translateTextResponse = await textTranslationClient.TranslateAsync("fr", "Hello", "en"); TranslatedTextItem? translatedTextItem = translateTextResponse.Value.FirstOrDefault(); Console.WriteLine(translatedTextItem?.Translations.FirstOrDefault()?.Text);
And now both work off the single endpoint.
English Bonjour
One thing I wanted to confirm, what happens if a future version of the SDK starts recognizing services.ai.azure.com? Would it add the prefix on top of mine and put me back at a 404? No, because PLATFORM_PATH is rooted, so new Uri(endpoint, PLATFORM_PATH) replaces the path instead of appending to it.
Uri uri = new Uri(new Uri("https://aif-demo-service-001.services.ai.azure.com/translator/text"), "/translator/text"); Console.WriteLine(uri.ToString()); // https://aif-demo-service-001.services.ai.azure.com/translator/text
I have raised this with the SDK team as azure-sdk-for-net#61912, so the behavior might change.
More read:
Hope this helps.
Happy Coding.
Regards,
Jaliya
No comments:
Post a Comment