microsoft/ai-agents-for-beginners-azure-openai-to-responses
>- Migrate Python apps from Azure OpenAI Chat Completions to the Responses API. Covers AzureOpenAI/AsyncAzureOpenAI client migration to the v1 endpoint, streaming, tools, structured output, multi-turn, EntraID auth, and model compatibility checks. Python-focused, Azure OpenAI-specific. upgrade openai SDK, responses API migration, move from completions to responses, gpt-5 migration, azure openai python migration, chat completions to responses, AzureOpenAI to OpenAI client, python azure openai upgrade. Node/TypeScript/C#/Java/Go migrations (this skill is Python-only), Azure infrastructure setup (use azure-prepare), deploying models (use microsoft-foundry).
npx skills add https://github.com/microsoft/ai-agents-for-beginners --skill azure-openai-to-responses
> AUTHORITATIVE GUIDANCE — FOLLOW EXACTLY
>
> This skill migrates Python codebases using Azure OpenAI Chat Completions
> to the unified Responses API. Follow these instructions precisely.
> Do not improvise parameter mappings or invent API shapes.
Activate this skill when user wants to:
AzureOpenAI/AsyncAzureOpenAI to standard OpenAI/AsyncOpenAI client with the v1 endpointAzureOpenAI constructors or api_version> Before migrating, verify your Azure OpenAI deployment supports the Responses API.
import os
from openai import OpenAI
client = OpenAI(
api_key=os.environ["AZURE_OPENAI_API_KEY"],
base_url=f"{os.environ['AZURE_OPENAI_ENDPOINT'].rstrip('/')}/openai/v1/",
)
try:
resp = client.responses.create(
model=os.environ["AZURE_OPENAI_DEPLOYMENT"],
input="ping",
max_output_tokens=50,
store=False,
)
print(f"✅ Deployment supports Responses API: {resp.output_text}")
except Exception as e:
print(f"❌ Deployment does NOT support Responses API: {e}")
> Note: max_output_tokens has a minimum of 16 on Azure OpenAI. Values below 16 return a 400 error. Use 50+ for smoke tests.
If this returns a 404, the deployment's model doesn't support Responses yet — check the reference below or redeploy with a supported model.
Run the built-in model compatibility tool to see what's available with Responses API support in your specific region:
python migrate.py models --subscription YOUR_SUB_ID --location YOUR_REGION
This queries Azure ARM live and shows a compatibility matrix — which models support Responses, structured output, tools, etc. Use --filter gpt-5.1,gpt-5.2 to narrow results or --json for scripting.
python migrate.py models (see above — region-specific, always up to date)> WARNING: Older models (those predating gpt-4.1) may not support all Responses API features fully.
>
> Known limitations with older models:
> - reasoning parameter: Not supported on many non-reasoning models. Only migrate reasoning if it was already present in the original code.
> - seed parameter: Not supported in Responses API at all — remove from all requests.
> - Structured output via text.format: Older models may not enforce strict: true JSON schemas reliably.
> - Tool orchestration: GPT-5+ orchestrates tool calls as part of internal reasoning. Older models on Responses still work but lack this deep integration.
> - Temperature constraints: When migrating to gpt-5, temperature must be omitted or set to 1. Older models have no such constraint.
O-series models have unique parameter constraints. When migrating apps that target o-series models:
temperature: Must be 1 (or omitted). O-series models do not accept other values.max_completion_tokens → max_output_tokens: Apps using the Azure-specific max_completion_tokens must switch to max_output_tokens. Set high values (4096+) because reasoning tokens count against the limit.reasoning_effort: If the app uses reasoning_effort (low/medium/high), keep it — the Responses API supports this parameter for o-series models.response.output_text.delta may arrive after a longer delay than with GPT models.top_p: Not supported on o-series — remove if present.Action — proactive model advisory: During the scan phase, check which model the app targets (deployment names, env vars, config). If the model predates gpt-4.1 (not gpt-4.1+), proactively tell the user:
gpt-5.1, gpt-5.2) offer better tool orchestration, structured output enforcement, reasoning, and cross-region availability.Do not block or refuse to migrate based on model version. The advisory is informational.
> GitHub Models (models.github.ai, models.inference.ai.azure.com) does not support the Responses API.
If the codebase has a GitHub Models code path (look for base_url pointing to models.github.ai or models.inference.ai.azure.com), remove it entirely during migration. The Responses API requires Azure OpenAI, OpenAI, or a compatible local endpoint (e.g., Ollama with Responses support).
Action during scan:
Many apps use higher-level frameworks on top of OpenAI. When migrating these, the framework's own API changes — not just the underlying OpenAI calls.
Check your MAF version first — the migration depends on whether you are on MAF 1.0.0+ or a pre-1.0.0 beta/rc.
OpenAIChatClient already uses the Responses API — no migration needed. If the codebase uses the legacy OpenAIChatCompletionClient (which uses chat.completions.create), replace it with OpenAIChatClient.
| Before | After |
|--------|-------|
| from agent_framework.openai import OpenAIChatCompletionClient | from agent_framework.openai import OpenAIChatClient |
| OpenAIChatCompletionClient(...) | OpenAIChatClient(...) |
To check your version: python -c "import agent_framework_openai; print(agent_framework_openai.__version__)"
In pre-1.0.0 MAF, OpenAIChatClient used Chat Completions. Upgrade to agent-framework-openai>=1.0.0 where OpenAIChatClient uses the Responses API by default.
No other changes needed — the Agent and tool APIs remain the same.
langchain-openai)Add use_responses_api=True to ChatOpenAI(). Also update response access from .content to .text.
| Before | After |
|--------|-------|
| ChatOpenAI(model=..., base_url=..., api_key=...) | ChatOpenAI(model=..., base_url=..., api_key=..., use_responses_api=True) |
| result['messages'][-1].content | result['messages'][-1].text |
For complete before/after code examples, see cheat-sheet.md.
> The Responses API is a server-side concern. Migrate your Python backend; the frontend's HTTP contract should stay unchanged unless your backend is a thin pass-through — in that case, consider adopting the Responses request shape to eliminate a translation layer. If the frontend calls OpenAI directly with a client-side key, move those calls to a backend first.
@microsoft/ai-chat-protocol deprecationThe @microsoft/ai-chat-protocol npm package is deprecated and should be replaced with ndjson-readablestream. If you encounter it in a frontend:
<!-- Before -->
<script src="https://cdn.jsdelivr.net/npm/@microsoft/ai-chat-protocol@.../dist/iife/index.js"></script>
<!-- After -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/ndjson-readablestream.umd.js"></script>
AIChatProtocolClient instantiation (new ChatProtocol.AIChatProtocolClient("/chat")).client.getStreamedCompletion(messages) with a direct fetch() call to the backend streaming endpoint.for await (const response of result) with for await (const chunk of readNDJSONStream(response.body)).response.delta.content / response.error to chunk.delta.content / chunk.error.git add/git commit/git push; produce working-tree edits only.If the codebase uses AzureOpenAI or AsyncAzureOpenAI constructors, migrate to the standard OpenAI / AsyncOpenAI constructors first. The Azure-specific constructors are deprecated in openai>=1.108.1.
The new /openai/v1 endpoint uses the standard OpenAI() client instead of AzureOpenAI(), requires no api_version parameter, and works identically across OpenAI and Azure OpenAI. The same client code is future-proof — no version management needed.
| Before | After |
|--------|-------|
| AzureOpenAI | OpenAI |
| AsyncAzureOpenAI | AsyncOpenAI |
| azure_endpoint | base_url |
| azure_ad_token_provider | api_key |
| api_version=... | Remove entirely |
api_version argument from client construction.AZURE_OPENAI_VERSION / AZURE_OPENAI_API_VERSION environment variables from .env, app settings, and Bicep/infra files.AZURE_OPENAI_CLIENT_ID → AZURE_CLIENT_ID in .env, app settings, Bicep/infra, and test fixtures (standard Azure Identity SDK convention).openai>=1.108.1 in requirements.txt or pyproject.toml.| Old env var | Action | Notes |
|-------------|--------|-------|
| AZURE_OPENAI_VERSION | Remove | No api_version needed with v1 endpoint |
| AZURE_OPENAI_API_VERSION | Remove | Same as above |
| AZURE_OPENAI_CLIENT_ID | Rename → AZURE_CLIENT_ID | Standard Azure Identity SDK convention for ManagedIdentityCredential(client_id=...) |
| AZURE_OPENAI_ENDPOINT | Keep | Still needed for base_url construction |
| AZURE_OPENAI_CHAT_DEPLOYMENT | Keep | Used as model param in responses.create |
| AZURE_OPENAI_API_KEY | Keep | Used as api_key for key-based auth |
For client setup code examples (sync, async, EntraID, API key, multi-tenant), see cheat-sheet.md.
Run the detect_legacy.py script to find all call sites that need migration:
python skills/azure-openai-to-responses/scripts/detect_legacy.py .
Or run these searches manually — every match is a migration target:
# Legacy API calls (must rewrite)
rg "chat\.completions\.create"
rg "ChatCompletion\.create"
rg "Completion\.create"
# Deprecated Azure client constructors (must replace)
rg "AzureOpenAI\("
rg "AsyncAzureOpenAI\("
# Response shape access patterns (must update)
rg "choices\[0\]\.message\.content"
rg "choices\[0\]\.delta\.content"
rg "choices\[0\]\.message\.function_call"
rg "choices\[0\]\.message\.tool_calls"
# Tool definitions in old nested format (must flatten)
rg '"function":\s*{\s*"name"'
rg "pydantic_function_tool"
# Tool results in old format (must convert to function_call_output)
rg '"role":\s*"tool"'
rg '"tool_call_id"'
# Deprecated parameters (must remove or rename)
rg "response_format"
rg "max_tokens\b" # rename to max_output_tokens
rg "['\"]seed['\"]" # remove entirely
# Deprecated env vars (clean up)
rg "AZURE_OPENAI_API_VERSION|AZURE_OPENAI_VERSION"
rg "AZURE_OPENAI_CLIENT_ID" # should be AZURE_CLIENT_ID
# GitHub Models endpoints (must remove — Responses API not supported)
rg "models\.github\.ai|models\.inference\.ai\.azure"
# Framework-level legacy patterns (must update)
rg "OpenAIChatCompletionClient" # MAF 1.0.0+: replace with OpenAIChatClient
rg "ChatOpenAI\(" | grep -v "use_responses_api" # LangChain: needs use_responses_api=True
# Test infrastructure (must update)
rg "ChatCompletionChunk|AsyncCompletions\.create" tests/
rg "_azure_ad_token_provider" tests/
rg "prompt_filter_results|content_filter_results" tests/
rg "choices\[0\]" tests/
# Content filter error body access (must update — structure changed)
rg 'innererror.*content_filter_result|error\.body\["innererror"\]'
rg "content_filter_result\[" # old singular form — now content_filter_results (plural) inside content_filters array
# Raw HTTP calls to Chat Completions endpoint (must update URL)
rg "/openai/deployments/.*/chat/completions"
rg "api-version="
client.chat.completions.create → client.responses.create(...).AzureOpenAI(...) → OpenAI(base_url=..., api_key=...).{"type": "function", "function": {"name": ...}}) to flat Responses format ({"type": "function", "name": ...}); use tool_choice; return tool results as {"type": "function_call_output", "call_id": ..., "output": ...} items (not {"role": "tool", ...}).response.output items to the conversation (not a manual {"role": "assistant", "tool_calls": [...]} dict), then append function_call_output items for each result.{"type": "function_call", "id": "fc_...", "call_id": "fc_...", ...} + {"type": "function_call_output", ...} items. IDs must start with fc_.pydantic_function_tool(): this helper still generates the old nested format and is not compatible with responses.create(). Replace with manual tool definitions or a flattening wrapper.input items.response_format with text.format in Responses. Canonical shape: text={"format": {"type": "json_schema", "name": "Output", "strict": True, "schema": {...}}}.content[].type: "text" with Responses content[].type: "input_text" for user/system turns.content[].type: "image_url" with Responses content[].type: "input_image". The image_url field changes from a nested object {"url": "..."} to a flat string. See the cheat sheet for before/after examples.reasoning if it already exists in the original code.error.body["innererror"]["content_filter_result"] (singular); Responses API uses error.body["content_filters"][0]["content_filter_results"] (plural, inside an array). Code that accesses innererror will raise KeyError. Rewrite to use the new path.requests, httpx, etc.) using /openai/deployments/{name}/chat/completions?api-version=..., rewrite to /openai/v1/responses. The request body changes: messages → input, add max_output_tokens and store: false, remove api-version query param. The response body changes: choices[0].message.content → output[0].content[0].text (note: output_text is an SDK convenience property not present in raw REST JSON)./openai/deployments/{name}/chat/completions to /openai/v1/responses.messages → input, max_tokens → max_output_tokens. temperature remains.response_format → text.format with a proper object.content[].type: "text" with Responses content[].type: "input_text" for system/user turns.content[].type: "image_url" with Responses content[].type: "input_image". Flatten the image_url field from {"image_url": {"url": "..."}} to {"image_url": "..."} (a plain string — either an HTTPS URL or a data:image/...;base64,... data URI).| Chat Completions | Responses API |
|-----------------|---------------|
| prompt | input |
| messages | input (array of items) |
| max_tokens | max_output_tokens |
| response_format | text.format (object) |
| temperature | temperature (unchanged) |
| stop | stop (unchanged) |
| frequency_penalty | frequency_penalty (unchanged) |
| presence_penalty | presence_penalty (unchanged) |
| tools / function-calling | tools (unchanged) |
| seed | Remove (not supported) |
| store | store (set to false) |
| content[].type: "text" | content[].type: "input_text" |
| content[].type: "image_url" | content[].type: "input_image" |
| "image_url": {"url": "..."} | "image_url": "..." (flat string) |
For complete before/after code examples, see cheat-sheet.md.
For test infrastructure migration (mocks, snapshots, assertions), see test-migration.md.
For troubleshooting errors and gotchas, see troubleshooting.md.
store: false on all Responses requests.rg "chat\.completions\.create|ChatCompletion\.create|Completion\.create" in migrated files.rg "AzureOpenAI\(|AsyncAzureOpenAI\(" — all constructors use OpenAI/AsyncOpenAI with the v1 endpoint.rg "models\.github\.ai|models\.inference\.ai\.azure" — GitHub Models code paths removed.rg "OpenAIChatCompletionClient" — MAF 1.0.0+ code uses OpenAIChatClient (which uses Responses API). In pre-1.0.0, upgrade to agent-framework-openai>=1.0.0.ChatOpenAI(...) calls include use_responses_api=True.rg "choices\[0\]" — all response access uses resp.output_text or the Responses output schema.response_format at top level; all structured output uses text={"format": {...}}.openai>=1.108.1 and azure-identity in requirements.txt or pyproject.toml; dependencies reinstalled.store=False set on every responses.create call.api_version in client construction; AZURE_OPENAI_API_VERSION removed from env files and infra.rg "ChatCompletionChunk|AsyncCompletions\.create|chat\.completions" tests/.rg "_azure_ad_token_provider" tests/ — assertions updated to check isinstance(client, AsyncOpenAI) or base_url.rg "prompt_filter_results|content_filter_results" tests/ — Azure-specific filter mocks removed.kwargs.get("input") not kwargs.get("messages").choices[0], function_call, logprobs, etc.).pytest passes with zero failures after all test updates.responses.create returns non-empty output_text.response.output_text.delta events with non-empty deltas.text.format with json_schema, json.loads(resp.output_text) succeeds and matches the schema.output_text (no infinite loop).AsyncAzureOpenAI was used, AsyncOpenAI equivalent works with await.| Package | Minimum Version |
|---------|----------------|
| openai | >=1.108.1 |
| azure-identity | Latest (for EntraID auth) |
Take microsoft/ai-agents-for-beginners-azure-openai-to-responses from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.