{"id":127,"date":"2026-07-23T20:35:08","date_gmt":"2026-07-23T20:35:08","guid":{"rendered":"https:\/\/www.tellpunk.it\/?p=127"},"modified":"2026-07-23T22:51:13","modified_gmt":"2026-07-23T22:51:13","slug":"azure-foundry-ai-103","status":"publish","type":"post","link":"https:\/\/www.tellpunk.it\/en\/azure-foundry-ai-103\/","title":{"rendered":"AI-103 Study Log #1: Voice Claims Intake on Microsoft Foundry"},"content":{"rendered":"<p class=\"wp-block-paragraph\"><em>Part 1 of a series documenting the road to the Azure AI Apps and Agents Developer Associate certification (Exam AI-103), one working lab at a time.<\/em><\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Why this project<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Microsoft retired exam AI-102 on June 30, 2026 and replaced it with AI-103, &#8220;Developing AI Apps and Agents on Azure&#8221; \u2014 a certification rebuilt around Microsoft Foundry, agentic workflows, and the modern Azure AI SDKs.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Rather than studying it from slides, I am preparing it by building four portfolio labs with an insurance and regulated-industry flavor: a multi-agent claims triage system, an ESG report intelligence pipeline, a visual damage assessment tool, and the subject of this article \u2014 a voice claims intake pipeline.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The voice lab is deliberately the smallest of the four. Its job is to establish the working conventions of the whole monorepo (auth, configuration, cost discipline, teardown scripts) while covering the Text Analysis &amp; Speech domain of the exam.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The plan on paper was simple: a caller reports a car accident, speech-to-text transcribes the call, a language model extracts a structured claim record, a sentiment pass scores urgency, and text-to-speech reads back a confirmation.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Almost none of it survived contact with the platform as designed \u2014 and that is exactly what makes it worth writing up.<\/p>\n\n\n\n<figure class=\"wp-block-image\"><img decoding=\"async\" src=\"https:\/\/www.tellpunk.it\/wp-content\/uploads\/2026\/07\/image-1-1024x574.png\" alt=\"Microsoft Foundry portal home page with agent demo\"\/><figcaption class=\"wp-element-caption\">The Microsoft Foundry portal home, the starting point for every lab in this series.<\/figcaption><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">Setting up from zero<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The environment started from a brand-new Azure free account: one Foundry project (<em>ai103-foundry<\/em>, Sweden Central), one resource group (<em>rg-ai103-labs<\/em>), one Speech resource on the free F0 tier.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A single Foundry project hosts all four labs \u2014 projects in Foundry are a boundary for teams, RBAC, and environments, and a solo learner on a single environment gains nothing by fragmenting quota and endpoints across several of them.<\/p>\n\n\n\n<figure class=\"wp-block-image\"><img decoding=\"async\" src=\"https:\/\/www.tellpunk.it\/wp-content\/uploads\/2026\/07\/image-4.png\" alt=\"Foundry create project dialog with custom settings\"\/><figcaption class=\"wp-element-caption\">Custom project creation: one project, one region, one resource group for all four labs.<\/figcaption><\/figure>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"665\" src=\"https:\/\/www.tellpunk.it\/wp-content\/uploads\/2026\/07\/image-23-1024x665.png\" alt=\"\" class=\"wp-image-155\" srcset=\"https:\/\/www.tellpunk.it\/wp-content\/uploads\/2026\/07\/image-23-1024x665.png 1024w, https:\/\/www.tellpunk.it\/wp-content\/uploads\/2026\/07\/image-23-300x195.png 300w, https:\/\/www.tellpunk.it\/wp-content\/uploads\/2026\/07\/image-23-768x498.png 768w, https:\/\/www.tellpunk.it\/wp-content\/uploads\/2026\/07\/image-23-18x12.png 18w, https:\/\/www.tellpunk.it\/wp-content\/uploads\/2026\/07\/image-23.png 1251w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><figcaption class=\"wp-element-caption\">The freshly created project overview, with the \/openai-capable endpoints the SDK migration will later depend on.<\/figcaption><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Authentication was keyless from day one:&nbsp;<em>az login<\/em>&nbsp;plus a&nbsp;<em>Foundry User<\/em>&nbsp;role assignment scoped to the resource group, consumed in code through&nbsp;<em>DefaultAzureCredential<\/em>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">One practical footnote for personal Microsoft accounts: role assignment by e-mail fails, because in Entra ID a Gmail-based identity lives under an external name like&nbsp;<em>user_gmail.com#EXT#@tenant.onmicrosoft.com<\/em>. Assigning by object ID (<em>az ad signed-in-user show &#8211;query id<\/em>) sidesteps the lookup entirely.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A second footnote: the RBAC roles were renamed mid-2026, so&nbsp;<em>Azure AI User<\/em>&nbsp;no longer exists \u2014 the role is now&nbsp;<em>Foundry User<\/em>.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"322\" src=\"https:\/\/www.tellpunk.it\/wp-content\/uploads\/2026\/07\/image-22-1024x322.png\" alt=\"\" class=\"wp-image-154\" srcset=\"https:\/\/www.tellpunk.it\/wp-content\/uploads\/2026\/07\/image-22-1024x322.png 1024w, https:\/\/www.tellpunk.it\/wp-content\/uploads\/2026\/07\/image-22-300x94.png 300w, https:\/\/www.tellpunk.it\/wp-content\/uploads\/2026\/07\/image-22-768x242.png 768w, https:\/\/www.tellpunk.it\/wp-content\/uploads\/2026\/07\/image-22-18x6.png 18w, https:\/\/www.tellpunk.it\/wp-content\/uploads\/2026\/07\/image-22.png 1467w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><figcaption class=\"wp-element-caption\">Role assignment by object ID \u2014 the workaround for personal-account e-mail lookups (IDs redacted).<\/figcaption><\/figure>\n\n\n\n<figure class=\"wp-block-image\"><img decoding=\"async\" src=\"https:\/\/www.tellpunk.it\/wp-content\/uploads\/2026\/07\/image-14-1024x718.png\" alt=\"Azure portal Speech service creation form, free tier\"\/><figcaption class=\"wp-element-caption\">The only non-Foundry resource: a free-tier F0 Speech service in the same resource group.<\/figcaption><\/figure>\n\n\n\n<figure class=\"wp-block-image\"><img decoding=\"async\" src=\"https:\/\/www.tellpunk.it\/wp-content\/uploads\/2026\/07\/image-16-1024x503.png\" alt=\"Azure resource group listing three resources in Sweden Central\"\/><figcaption class=\"wp-element-caption\">The entire lab footprint: one Foundry resource, one project, one F0 Speech service in rg-ai103-labs.<\/figcaption><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">The quota maze<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The first real lesson had nothing to do with AI: it was learning to read the Quota page. My initial model deployments failed with &#8220;not available due to insufficient quota,&#8221; which felt terminal until the numbers made sense.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Quota in Foundry is allocated per model, per deployment SKU, and per region \u2014 and the display reads&nbsp;<em>used\/available<\/em>. A row showing&nbsp;<em>0\/50K TPM<\/em>&nbsp;is not a disabled model; it is fifty thousand tokens per minute of untouched capacity. A row showing&nbsp;<em>0\/0<\/em>&nbsp;is the actual dead end.<\/p>\n\n\n\n<figure class=\"wp-block-image\"><img decoding=\"async\" src=\"https:\/\/www.tellpunk.it\/wp-content\/uploads\/2026\/07\/image-11-1024x242.png\" alt=\"Microsoft Foundry quota page with subscription filter\"\/><figcaption class=\"wp-element-caption\">The Foundry Quota page, where deployment failures start making sense.<\/figcaption><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">On a trial subscription the pattern was stark: every interactive chat model had zero Global Standard quota, while a handful of regional Standard SKUs and one unexpected model had real allocations.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The default deployment flow (&#8220;recommended settings&#8221;) targets Global Standard, which explains why it failed silently against models whose quota lived on the regional SKU. Custom deployment settings, with the SKU chosen deliberately and the TPM limit set low, are the way through.<\/p>\n\n\n\n<figure class=\"wp-block-image\"><img decoding=\"async\" src=\"https:\/\/www.tellpunk.it\/wp-content\/uploads\/2026\/07\/image-10.png\" alt=\"Foundry deploy menu, default versus custom settings\"\/><figcaption class=\"wp-element-caption\">The deploy menu: &#8220;recommended settings&#8221; targets Global Standard \u2014 custom settings are the way through.<\/figcaption><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">Deprecation roulette<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The second lesson was about model lifecycle. Between the writing of my study plan and the actual deployment attempt,&nbsp;<em>gpt-4o-mini<\/em>&nbsp;had entered deprecation \u2014 visible in the catalog, blocked for new deployments. Its natural successor&nbsp;<em>gpt-4.1-mini<\/em>&nbsp;had quota available but returned&nbsp;<em>ServiceModelDeprecating<\/em>&nbsp;on deployment.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The catalog&#8217;s &#8220;Lifecycle&#8221; field, not the model&#8217;s fame, is what decides whether you can build on it:&nbsp;<em>deprecated<\/em>&nbsp;means no new deployments while existing ones keep working;&nbsp;<em>retired<\/em>&nbsp;means the API returns errors. That distinction is literally exam material in the Plan &amp; Manage domain, and I got to learn it by collision.<\/p>\n\n\n\n<figure class=\"wp-block-image\"><img decoding=\"async\" src=\"https:\/\/www.tellpunk.it\/wp-content\/uploads\/2026\/07\/image-7-1024x550.png\" alt=\"Foundry model catalog with lifecycle and SKU filters\"\/><figcaption class=\"wp-element-caption\">Eleven thousand models in the catalog; the Lifecycle filter is the one that matters.<\/figcaption><\/figure>\n\n\n\n<figure class=\"wp-block-image\"><img decoding=\"async\" src=\"https:\/\/www.tellpunk.it\/wp-content\/uploads\/2026\/07\/image-8-1024x598.png\" alt=\"Model page showing deprecated banner for gpt-4o-mini\"\/><figcaption class=\"wp-element-caption\">gpt-4o-mini in the catalog: deprecated, closed to new deployments.<\/figcaption><\/figure>\n\n\n\n<figure class=\"wp-block-image\"><img decoding=\"async\" src=\"https:\/\/www.tellpunk.it\/wp-content\/uploads\/2026\/07\/image-12-1024x222.png\" alt=\"ServiceModelDeprecating error banner on gpt-4.1-mini page\"\/><figcaption class=\"wp-element-caption\">Quota available, deployment refused: ServiceModelDeprecating in the wild.<\/figcaption><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">The only model that satisfied both conditions \u2014 generally available and quota above zero \u2014 was&nbsp;<em>gpt-audio-mini<\/em>, an audio-first chat model with 30K TPM of Global Standard allocation. Not the model anyone would pick for a text pipeline. It turned out to be the most interesting decision of the lab.<\/p>\n\n\n\n<figure class=\"wp-block-image\"><img decoding=\"async\" src=\"https:\/\/www.tellpunk.it\/wp-content\/uploads\/2026\/07\/image-13-1024x517.png\" alt=\"gpt-audio-mini model page during deployment\"\/><figcaption class=\"wp-element-caption\">The unlikely winner: gpt-audio-mini, generally available and with real quota.<\/figcaption><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">When the model refuses your architecture<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">With the classic pipeline wired up (Speech STT feeding text into the model), the first end-to-end run failed with an unambiguous message:&nbsp;<em>&#8220;This model requires that either input content or output modality contain audio.&#8221;<\/em>&nbsp;An audio-native model rejects text-only requests.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The workaround would have been swapping models; the better move was restructuring the lab to match the model: the WAV file now goes directly into the chat completion as an&nbsp;<em>input_audio<\/em>&nbsp;content part, and&nbsp;<em>gpt-audio-mini<\/em>&nbsp;performs transcription, structured extraction, and sentiment scoring in a single call.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The separate STT stage remains in the repo as the &#8220;classic pipeline,&#8221; deliberately kept for a future A\/B comparison against a text model \u2014 which is precisely the kind of architectural trade-off (dedicated Speech services versus multimodal model) the exam&#8217;s speech domain asks about.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The test audio itself is synthesized: Azure Speech TTS on the free tier reads a fictional claim call transcript into a 16 kHz mono WAV. The pipeline therefore runs end to end on entirely synthetic data \u2014 a non-negotiable constraint for a portfolio that borrows its scenarios from a regulated industry.<\/p>\n\n\n\n<figure class=\"wp-block-image\"><img decoding=\"async\" src=\"https:\/\/www.tellpunk.it\/wp-content\/uploads\/2026\/07\/image-20-1024x509.png\" alt=\"Terminal output of voice claims pipeline with extracted JSON\"\/><figcaption class=\"wp-element-caption\">One call, four stages: the full pipeline run with the extracted claim JSON \u2014 mistranscribed policy number included.<\/figcaption><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">The SDK that retired under my feet<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The deepest failure was the most instructive. Authentication errors that looked like RBAC problems (&#8220;audience is incorrect&#8221;) and version errors that looked like configuration problems (&#8220;API version not supported&#8221;) both traced back to a single root cause: the lab had been scaffolded on&nbsp;<em>azure-ai-inference<\/em>, an SDK that is deprecated and scheduled for retirement on August 26, 2026, against an endpoint that had already stopped honoring its preview API dialects.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The fix was not a magic&nbsp;<em>api_version<\/em>&nbsp;string; it was migrating to the generally available OpenAI v1 surface \u2014 the standard&nbsp;<em>openai<\/em>&nbsp;Python SDK pointed at the resource&#8217;s&nbsp;<em>\/openai\/v1\/<\/em>&nbsp;endpoint, authenticated with an Entra bearer token provider instead of an API key.<\/p>\n\n\n\n<figure class=\"wp-block-image\"><img decoding=\"async\" src=\"https:\/\/www.tellpunk.it\/wp-content\/uploads\/2026\/07\/image-19-1024x214.png\" alt=\"Terminal showing BadRequest API version not supported error\"\/><figcaption class=\"wp-element-caption\">&#8220;API version not supported&#8221;: the error that unmasked a retiring SDK.<\/figcaption><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">If you are preparing AI-103 in 2026, assume that any tutorial older than a few months is describing a platform that no longer exists in that shape. Reading&nbsp;<em>ClientAuthenticationError<\/em>&nbsp;and&nbsp;<em>BadRequestError<\/em>&nbsp;bodies carefully \u2014 audience, scope, version \u2014 is a faster path to a working system than any amount of tutorial-following.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">One honest bug, kept on purpose<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The finished pipeline extracts a valid claim record: claim type, incident date, location, sentiment, an urgency score with rationale, and a spoken confirmation. It also makes one systematic mistake: the policy number&nbsp;<em>POL-4482913<\/em>, read aloud by the TTS voice, comes back transcribed as&nbsp;<em>PL4482913<\/em>. The record validates; the identifier is wrong.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">I am keeping the bug visible in the repo for now, because it is a perfect specimen of a whole class of production failures: audio fidelity on structured identifiers.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The layered fix \u2014 format constraints in the prompt, a regex-validated Pydantic field so the retry loop self-corrects, and ultimately verification against a system of record \u2014 is the next commit, and a future article of its own.<\/p>\n\n\n\n<figure class=\"wp-block-image\"><img decoding=\"async\" src=\"https:\/\/www.tellpunk.it\/wp-content\/uploads\/2026\/07\/image-21-1024x193.png\" alt=\"VS Code with generated confirmation WAV audio file\"\/><figcaption class=\"wp-element-caption\">The pipeline&#8217;s final artifact: a seven-second spoken confirmation.<\/figcaption><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">What this session actually covered<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">One evening, one working lab \u2014 and, almost incidentally, live encounters with quota management, deployment SKUs, RBAC and role renames, model lifecycle states, SDK deprecation, keyless authentication, JSON-mode extraction with validated retries, and the speech-versus-multimodal architecture decision. That is a substantial slice of the AI-103 syllabus, learned by hitting it rather than reading it.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The monorepo is public at <strong><a href=\"https:\/\/github.com\/labnova\/ai103-labs\/tree\/main\/labs\/04-voice-claims-intake\" target=\"_blank\" rel=\"noopener\">labnova\/ai103-labs \u2014 Lab 04: Voice Claims Intake<\/a><\/strong>. Next up: hardening the identifier extraction, then Lab 02 \u2014 an ESG report intelligence pipeline, where the retrieval and grounding domains get their turn.<\/p>","protected":false},"excerpt":{"rendered":"<p>Part 1 of a series documenting the road to the Azure AI Apps and Agents Developer Associate certification (Exam AI-103), one working lab at a time. Why this project Microsoft retired exam AI-102 on June 30, 2026 and replaced it [&hellip;]<\/p>","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_monsterinsights_skip_tracking":false,"footnotes":""},"categories":[25],"tags":[],"class_list":["post-127","post","type-post","status-publish","format-standard","hentry","category-research"],"aioseo_notices":[],"_links":{"self":[{"href":"https:\/\/www.tellpunk.it\/en\/wp-json\/wp\/v2\/posts\/127","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.tellpunk.it\/en\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.tellpunk.it\/en\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.tellpunk.it\/en\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.tellpunk.it\/en\/wp-json\/wp\/v2\/comments?post=127"}],"version-history":[{"count":5,"href":"https:\/\/www.tellpunk.it\/en\/wp-json\/wp\/v2\/posts\/127\/revisions"}],"predecessor-version":[{"id":182,"href":"https:\/\/www.tellpunk.it\/en\/wp-json\/wp\/v2\/posts\/127\/revisions\/182"}],"wp:attachment":[{"href":"https:\/\/www.tellpunk.it\/en\/wp-json\/wp\/v2\/media?parent=127"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.tellpunk.it\/en\/wp-json\/wp\/v2\/categories?post=127"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.tellpunk.it\/en\/wp-json\/wp\/v2\/tags?post=127"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}