Connect Microsoft Agent Framework to Router One with the Chat Completions client
Microsoft Agent Framework is Microsoft's open-source SDK for building agents and workflows in Python and .NET, and the direct successor to Semantic Kernel and AutoGen; both languages reached 1.0 on April 2, 2026. Its OpenAI provider accepts any OpenAI-compatible endpoint, so pointing it at Router One reaches every chat model in the catalog served on /v1/chat/completions, with a cost and latency trace per request, while the agent loop, function tools, sessions, middleware, and workflows keep running in your process. One naming detail decides which endpoint you hit: in the 1.x Python packages OpenAIChatCompletionClient calls Chat Completions, and OpenAIChatClient, the name pre-1.0 snippets used for Chat Completions, now calls the Responses API. This guide builds an OpenAIChatCompletionClient with model, api_key, and base_url, runs an Agent with one function tool, streams a second turn on the same session, bounds the tool loop, shows the .NET equivalent through OpenAIClientOptions.Endpoint and AsAIAgent, and lists the matching settings in Semantic Kernel and AutoGen. It was checked against agent-framework 1.19.0, agent-framework-openai 1.14.4, and Microsoft.Agents.AI.OpenAI 1.22.0.
Install agent-framework-openai and set your credentials
Use Python 3.10 or newer. The OpenAI clients live in the agent-framework-openai package, which pulls in agent-framework-core and the openai Python library; pip install agent-framework installs the full standard package set instead. Released packages no longer need --pre. Importing a client from agent_framework.openai without the provider package raises ModuleNotFoundError naming agent-framework-openai. The shell example below is for macOS/Linux. Replace both placeholders before running it, using a Router One key and the exact ID of a current chat model. The ROUTER_ONE_* names belong to this example, which reads them explicitly. The client has its own fallbacks when an argument is omitted: OPENAI_API_KEY, OPENAI_BASE_URL, and for the model OPENAI_CHAT_COMPLETION_MODEL, then OPENAI_MODEL. Agent Framework does not load .env files by itself; call load_dotenv() or pass env_file_path if you keep the values in a file. Keep the same environment active when running the Python file.
python -m pip install agent-framework-openai export ROUTER_ONE_API_KEY="sk-your-router-one-key" export ROUTER_ONE_MODEL_ID="<exact-model-id-from-/models>"
Configure Microsoft Agent Framework to use the Router One base URL
Save this as maf_router_one.py and run python maf_router_one.py. OpenAIChatCompletionClient takes the catalog ID as model, unchanged, including any provider prefix that is part of the ID; base_url is the /v1 base URL and is handed to the openai library's AsyncOpenAI client, which appends /chat/completions, so every model round trip in a run is one POST /v1/chat/completions; api_key is read from ROUTER_ONE_API_KEY, and passing it explicitly keeps the client on OpenAI routing even when AZURE_OPENAI_* variables exist in the shell. The @tool decorator turns a typed Python function into a function tool: its JSON schema travels in the request's tools field, and when the model asks for it, the framework executes the function in your process and sends the result back in the next request. approval_mode is written out because the official samples do the same; never_require is the default, and always_require holds the call until explicit approval is given. function_invocation_configuration bounds that loop, here at 5 model round trips and 10 tool executions per run. Agent(client=..., instructions=..., tools=[...]) is the current constructor, and client.as_agent(...) builds the same object. agent.create_session() returns an AgentSession; with this client the history lives in the session inside your process and is resent as messages on each run. The first run returns an AgentResponse: text is the final answer, and usage_details sums the input and output token counts the API reported across the run's requests. The second run passes stream=True and iterates AgentResponseUpdate chunks; the request is the same with stream set and stream_options include_usage.
import asyncio
import os
from typing import Annotated
from agent_framework import Agent, tool
from agent_framework.openai import OpenAIChatCompletionClient
@tool(approval_mode="never_require")
def get_order_status(order_id: Annotated[str, "The order number to look up."]) -> str:
"""Look up the shipping status of an order."""
return f"Order {order_id} shipped yesterday and arrives on Friday."
async def main() -> None:
# Chat Completions client: every model round trip is one POST /v1/chat/completions
client = OpenAIChatCompletionClient(
model=os.environ["ROUTER_ONE_MODEL_ID"],
api_key=os.environ["ROUTER_ONE_API_KEY"],
base_url="https://api.router.one/v1",
)
# Bound the tool loop (defaults: max_iterations=40, max_function_calls=None)
client.function_invocation_configuration.update({"max_iterations": 5, "max_function_calls": 10})
agent = Agent(
client=client,
name="SupportAgent",
instructions="You answer order questions. Call the tool when you need order data.",
tools=[get_order_status],
)
session = agent.create_session() # history stays in this process
# 1. Non-streaming run: text is the answer, usage_details sums the reported tokens
result = await agent.run("Where is order A-1042?", session=session)
print(result.text)
print(result.usage_details)
# 2. Same session, streamed: same request with stream=true
async for update in agent.run("When does it arrive?", session=session, stream=True):
if update.text:
print(update.text, end="", flush=True)
print()
if __name__ == "__main__":
asyncio.run(main())Which Agent Framework setting sends which request
The client class picks the endpoint, and the agent decides what each request carries. The rows below give the value for each setting in this guide, the request it produces, and what to confirm before relying on it. One class in the same package is out of scope: OpenAIEmbeddingClient posts to /v1/embeddings, which Router One does not serve, so embeddings for a RAG pipeline stay on another provider or a local model.
| Agent Framework setting | Value or resulting request | What to verify |
|---|---|---|
| OpenAIChatCompletionClient → base_url (fallback: OPENAI_BASE_URL) | https://api.router.one/v1 | Handed to the openai library's AsyncOpenAI client, which appends /chat/completions; a 404 not_found on the first run means /v1 is missing or doubled |
| OpenAIChatCompletionClient → model (fallback: OPENAI_CHAT_COMPLETION_MODEL, then OPENAI_MODEL) | The exact catalog ID | The model in each Logs trace matches /models character for character; the keyword is model, and model_id from pre-1.0 snippets raises TypeError |
| OpenAIChatCompletionClient → api_key (fallback: OPENAI_API_KEY) | A Router One key created for this agent, with maxSpend | The first request appears in Dashboard → Logs under that key; an explicit key keeps the client on OpenAI routing even when AZURE_OPENAI_* variables are set |
| OpenAIChatClient with the same three arguments (model fallback: OPENAI_CHAT_MODEL) | POST /v1/responses: in the 1.x packages this class is the Responses API client | Only IDs whose detail page lists /v1/responses (the currently listed GPT-family and DeepSeek IDs); other families return 400 must be called via. With a session it continues by previous_response_id unless the options carry store=False, so confirm multi-turn on one real request |
| Agent(tools=[...]) with @tool functions | Chat Completions with a tools field (JSON schemas); the functions run in your process | Tool calling on the model detail page; get_web_search_tool() instead adds web_search_options, a provider-hosted field to confirm on one real request before relying on it |
| client.function_invocation_configuration | max_iterations (default 40) bounds model round trips; max_function_calls and max_duration_seconds default to None | When max_iterations is used up, one more request goes out with tool_choice none to write the final answer, so a run is at most max_iterations + 1 requests before retries |
| agent.run(..., stream=True) | The same request with stream=true and stream_options include_usage | Streaming on the model page; an interrupted stream is recorded as HTTP 499 client_cancelled |
| agent.create_session() | History is kept by InMemoryHistoryProvider in the session's state and resent as messages on every run | Input tokens grow turn by turn; persist with session.to_dict() and AgentSession.from_dict(). Chat Completions has no server-side conversation state, so the session is the only copy of the history |
.NET: set OpenAIClientOptions.Endpoint and call AsAIAgent
In .NET the connection belongs to the official OpenAI library, and Agent Framework adds the agent on top. Install Microsoft.Agents.AI.OpenAI; the Learn page still appends --prerelease, but NuGet lists stable 1.x releases (1.22.0 when this guide was checked), so the flag is optional. OpenAIClientOptions.Endpoint takes the URL with /v1: the library's default endpoint is https://api.openai.com/v1 and it appends /chat/completions itself, so https://api.router.one alone ends in 404 not_found. GetChatClient(model) selects Chat Completions, and the AsAIAgent extension wraps it in a ChatClientAgent; snippets that call CreateAIAgent predate the January 2026 rename to AsAIAgent. GetResponsesClient().AsAIAgent(model: ...) is the Responses variant and follows the same family rule as the Python OpenAIChatClient. Function tools are passed as tools: [AIFunctionFactory.Create(Method)] and run in your process through the FunctionInvokingChatClient of Microsoft.Extensions.AI, whose MaximumIterationsPerRequest defaults to 40. RunAsync returns the complete response, and RunStreamingAsync streams updates.
// dotnet add package Microsoft.Agents.AI.OpenAI
using System.ClientModel;
using Microsoft.Agents.AI;
using OpenAI;
using OpenAI.Chat;
var key = Environment.GetEnvironmentVariable("ROUTER_ONE_API_KEY")
?? throw new InvalidOperationException("ROUTER_ONE_API_KEY is not set.");
var model = Environment.GetEnvironmentVariable("ROUTER_ONE_MODEL_ID")
?? throw new InvalidOperationException("ROUTER_ONE_MODEL_ID is not set.");
AIAgent agent = new OpenAIClient(
new ApiKeyCredential(key),
new OpenAIClientOptions { Endpoint = new Uri("https://api.router.one/v1") })
.GetChatClient(model)
.AsAIAgent(instructions: "You answer order questions.", name: "SupportAgent");
Console.WriteLine(await agent.RunAsync("Where is order A-1042?"));Budget one agent run and reconcile its requests
One agent.run() is a loop, not one request. A plain answer is one request; each batch of tool calls adds a round trip, because the tool results go back to the model in a new request that also resends the conversation so far, so input tokens grow with every iteration. max_iterations (default 40) bounds the round trips, and when it is used up the framework sends one more request with tool_choice set to none to get a final answer; max_function_calls and max_duration_seconds are unlimited by default, and the documentation notes that they are checked only after each batch of parallel tool calls, so one batch can overshoot them. None of them is a spend cap. Below the framework, the openai library retries connection errors and 408, 409, 429, and 5xx responses 2 times by default. The framework builds that client without a max_retries argument, so to change it pass your own: async_client=AsyncOpenAI(base_url=..., api_key=..., max_retries=0). Every attempt that reaches the gateway is its own request in Dashboard → Logs with its own request_id, trace, and charge. Give each agent a dedicated key with maxSpend: a looping run then gets HTTP 402 at the cap you set, the openai library does not retry a 402, and the wallet and your other keys are untouched. Workflows built with WorkflowBuilder also run in your process, and each agent executor's model call is one more request on the same key. Agent Framework's OpenTelemetry layer is on by default but exports nothing until you configure an exporter, for example with configure_otel_providers(); it emits invoke_agent, chat, and execute_tool spans, and prompts are recorded only when sensitive data is enabled (ENABLE_SENSITIVE_DATA, off by default). Those spans are your application's traces and stay wherever you export them. result.usage_details is the framework's sum of the usage the API reported; the charge is what Logs records. Reconcile by key, time, model, and token counts, and keep the request_id when reporting a failure. Router One records model-call metadata; it does not run the agent loop, your tools, your middleware, or your workflows.
Which model ID should Microsoft Agent Framework send?
Copy the exact model ID from /models, preserving case, hyphens, and version suffixes; do not substitute a display name. Open its detail page and match the supported API endpoints, context window, and capabilities such as tool calling to the provider and features selected in Microsoft Agent Framework. A catalog listing does not mean the client can use every feature of that model. Give each tool a dedicated API key with a maxSpend cap.
Which API protocol is Microsoft Agent Framework using?
OpenAI-compatible describes an interface format; it does not make Chat Completions (/v1/chat/completions), Responses (/v1/responses), and Anthropic Messages (/v1/messages) interchangeable. Check the installed client version, provider configuration, and actual request path against the model detail page and API compatibility fact sheet. A successful plain-text chat does not establish support for hosted tools, conversation state, or file-editing features.
Verify the Microsoft Agent Framework call in your request trace
Send a simple text request from Microsoft Agent Framework, then match its trace in Dashboard → Logs by time, model, and request_id: tokens, cost, latency, and status. Next, test streaming, tool calls, and multi-turn history separately. For failures, retain the actual request path, full error message, and request_id. If there is no matching log, check client configuration and connectivity before attributing the error to the gateway or upstream.
FAQ
The first run raises ChatClientException: service failed to complete the prompt. What is the wrapped error telling me?
The client wraps whatever the openai library raised: the message ends with service failed to complete the prompt: followed by the library's own text, such as Error code: 404 and the response body, and the original exception is available as __cause__. Read the status. 404 not_found means base_url lacks /v1 or carries it twice: the openai library appends /chat/completions to whatever you pass, so https://api.router.one alone posts to /chat/completions, and the gateway's message says the base URL must end in /v1. 401 AUTH_INVALID_API_KEY means the gateway did not accept the key; check ROUTER_ONE_API_KEY in the environment that runs the file. 400 with must be called via means the ID was sent to an endpoint that does not serve it, which with this framework almost always means the code constructs OpenAIChatClient: in the 1.x packages that class calls /v1/responses, and OpenAIChatCompletionClient is the Chat Completions class. Two errors appear before any request is sent, both as SettingNotFoundError. Model must be specified via the 'model' parameter or the 'OPENAI_CHAT_COMPLETION_MODEL', 'OPENAI_MODEL' environment variable means the constructor found neither the argument nor a variable. Azure OpenAI client requires either an API key or an Azure AD token provider means no key was found: without api_key or OPENAI_API_KEY the constructor falls through to its Azure routing, so the message mentions Azure even though the fix is to supply the Router One key.
My code imports OpenAIResponsesClient, ChatAgent, or create_agent, or passes model_id. What changed?
Those are preview-era names, and the Python significant-changes page on Microsoft Learn documents the renames. In python-1.0.0rc6 the OpenAI clients moved to the agent-framework-openai package, OpenAIResponsesClient became OpenAIChatClient, the old OpenAIChatClient became OpenAIChatCompletionClient, model_id became model everywhere, and the preview-era environment variables OPENAI_CHAT_MODEL_ID and OPENAI_RESPONSES_MODEL_ID gave way to OPENAI_CHAT_MODEL, OPENAI_CHAT_COMPLETION_MODEL, and OPENAI_MODEL. Earlier betas renamed ChatAgent to Agent with client= instead of chat_client=, create_agent to as_agent, run_stream(...) to run(..., stream=True), AgentThread and get_new_thread() to AgentSession and create_session(), and @ai_function to @tool. The Assistants client was removed in python-1.0.0, and Router One does not serve the Assistants API in any case. The trap for a gateway user is the second rename: a pre-1.0 snippet that says OpenAIChatClient(base_url=...) used Chat Completions then and uses /v1/responses now, so a Claude, Gemini, or Grok ID that worked before returns 400 must be called via after an upgrade. Replace the class with OpenAIChatCompletionClient and the keyword with model.
I am migrating from Semantic Kernel or AutoGen. Which setting held the base URL there?
In Semantic Kernel for .NET it is the endpoint argument: AddOpenAIChatCompletion(modelId: ..., apiKey: ..., endpoint: new Uri(...)) with the URI set to https://api.router.one/v1, which the documentation marks experimental behind #pragma warning disable SKEXP0010; it feeds the same OpenAI library option as above, so the URL includes /v1. In Semantic Kernel for Python, OpenAIChatCompletion has no base URL argument, so you pass a client: OpenAIChatCompletion(ai_model_id=..., async_client=AsyncOpenAI(base_url=..., api_key=...)). In AutoGen it is OpenAIChatCompletionClient(model=..., base_url=..., api_key=..., model_info={...}) from autogen_ext.models.openai, where the reference states that model_info is required if the model name is not a valid OpenAI model, which is the case for prefixed catalog IDs. Agent Framework's class of the same name lives in agent_framework.openai and needs no model_info: model, api_key, and base_url are enough. One behavior difference affects the budget: the AutoGen migration guide notes that AssistantAgent is single-turn unless max_tool_iterations is raised, while an Agent Framework Agent keeps invoking tools until it has a final answer, so set function_invocation_configuration and a per-key maxSpend before moving a tool-heavy agent over.
Which models can Microsoft Agent Framework use through the gateway?
Choose a current catalog model that supports both the endpoint and the features Microsoft Agent Framework uses. Check /models and the model detail page for the exact ID, current rates, and capabilities; a family name such as GPT or Claude is not a compatibility guarantee. Seeing a model in the picker confirms discovery, so verify an actual request too.
Models are listed, but requests fail with 400 or 404. What should I check?
Record the actual request path and error message, then check the exact model ID. A 400 can indicate invalid parameters, unsupported tools, or a model/endpoint mismatch; a 404 can indicate an incorrect path or missing resource, so it does not by itself establish that a model was retired. If the error says must be called via, use the named endpoint or select a model supported on the current endpoint. Do not add or remove /v1 or /chat/completions across all clients indiscriminately.
Does this work from Mainland China?
Yes. The gateway is reachable from Mainland China without a VPN, and the configuration is identical to the global setup.
How do I debug a 401/402/403/429?
Match the request and error message in Dashboard → Logs. For 401, check whether the key was sent and is valid; for 402, check wallet balance and maxSpend; for 403, check key permissions and access restrictions. For 429, distinguish request/token limits from upstream throttling using the error details. Keep the request_id and follow the error-codes reference.