Skip to main content

.NET Integration

Also available in: Python · PHP · Node.js · Go · Java

The "gateway base URL" is the address of your Observra Gateway (https://gateway.observra.in) - your app sends requests here instead of directly to OpenAI, Anthropic, or whichever provider you use. The "gateway key" is a per-Application credential you generate in the Observra Dashboard; it authenticates your app to the gateway and is separate from your provider's own API key, which you still keep and send yourself. See Quickstart and Gateway Authentication if you haven't set these up yet.

Try it now

Paste in a gateway URL, your gateway key, and a provider key to send a real request straight from this page - no code required yet.

Loading playground...

Environment variables

GATEWAY_URL
GATEWAY_KEY obs_...
OPENAI_API_KEY / ANTHROPIC_API_KEY / GEMINI_API_KEY / GROQ_API_KEY / OPENROUTER_API_KEY / OLLAMA_API_KEY

Install:

dotnet add package Anthropic
dotnet add package Google.GenAI

Agent framework packages (only add what you use): Microsoft.Agents.AI + Microsoft.Agents.AI.OpenAI (Microsoft Agent Framework), Microsoft.SemanticKernel, LangChain.Providers.OpenAI (LangChain.NET), NTG.Adk (a community ADK-C# port - Google has not published an official one).

Optional: distributed tracing

.NET's System.Diagnostics.Activity is a built-in, native W3C Trace Context implementation - no extra tracing package needed to generate a real traceparent header (see Trace Context):

using System.Diagnostics;

var activitySource = new ActivitySource("my-service");
using var activity = activitySource.StartActivity("gateway_call", ActivityKind.Client);
var traceparent = activity is null ? null : $"00-{activity.TraceId}-{activity.SpanId}-01";

ActivitySource only emits if something is listening - wire up OpenTelemetry.Extensions.Hosting's TracerProviderBuilder (or any other ActivityListener) if you want the spans exported anywhere, otherwise traceparent above is still generated correctly, it just isn't recorded.

Raw HTTP request

using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;

using var http = new HttpClient();
var body = JsonSerializer.Serialize(new
{
model = "gpt-4o-mini",
messages = new[] { new { role = "user", content = "hello" } },
});

var req = new HttpRequestMessage(HttpMethod.Post, $"{gatewayUrl}/openai/chat/completions")
{
Content = new StringContent(body, Encoding.UTF8, "application/json"),
};
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", gatewayKey);
req.Headers.Add("X-Provider-Key", openaiApiKey);

var res = await http.SendAsync(req);
var result = await res.Content.ReadAsStringAsync();
curl -X POST "${GATEWAY_URL}/openai/chat/completions" \
-H "Authorization: Bearer ${GATEWAY_KEY}" \
-H "X-Provider-Key: ${OPENAI_API_KEY}" \
-H "Content-Type: application/json" \
-d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"hello"}]}'

Every provider in Other Providers works the same way - swap the URL segment (/openai/anthropic, /groq, /openrouter, /ollama, ...) and the X-Provider-Key value.

Native SDKs

Uses the official Anthropic.SDK package. Its ApiKey field carries the provider key (sent as x-api-key) - gateway auth goes through a separate X-Gateway-Key header via ExtraHeaders.

var client = new Anthropic.AnthropicClient(new Anthropic.Core.ClientOptions
{
BaseUrl = $"{gatewayUrl}/anthropic",
ApiKey = anthropicApiKey,
ExtraHeaders = new Dictionary<string, string> { ["X-Gateway-Key"] = gatewayKey },
});

var response = await client.Messages.Create(new Anthropic.Models.Messages.MessageCreateParams
{
Model = "claude-3-5-haiku-20241022",
MaxTokens = 256,
Messages = [new Anthropic.Models.Messages.MessageParam
{
Role = Anthropic.Models.Messages.Role.User,
Content = "hello",
}],
});

Tool calling (native SDK)

The Gemini example above completes the full round-trip - model requests a tool, the app runs it locally, and the result is sent back as a follow-up call so the model can answer with it:

var weatherFn = new GTypes.FunctionDeclaration
{
Name = "get_weather",
Description = "Get the current weather for a city",
Parameters = new GTypes.Schema
{
Type = GTypes.Type.Object,
Properties = new Dictionary<string, GTypes.Schema> { ["city"] = new GTypes.Schema { Type = GTypes.Type.String } },
Required = ["city"],
},
};

var config = new GTypes.GenerateContentConfig { Tools = [new GTypes.Tool { FunctionDeclarations = [weatherFn] }] };
var userTurn = new GTypes.Content { Role = "user", Parts = [GTypes.Part.FromText("What's the weather in Paris right now? Use the tool.")] };

var response = await client.Models.GenerateContentAsync(model: "gemini-flash-lite-latest", contents: [userTurn], config: config);

var functionCall = response.Candidates?
.SelectMany(c => c.Content?.Parts ?? [])
.FirstOrDefault(p => p.FunctionCall?.Name == "get_weather")?.FunctionCall;

if (functionCall is not null)
{
var city = functionCall.Args?.TryGetValue("city", out var cityObj) == true ? cityObj?.ToString() ?? "Paris" : "Paris";
var modelTurn = new GTypes.Content { Role = "model", Parts = [GTypes.Part.FromFunctionCall("get_weather", functionCall.Args)] };
var toolResultTurn = new GTypes.Content
{
Role = "user",
Parts = [GTypes.Part.FromFunctionResponse("get_weather", new Dictionary<string, object> { ["result"] = $"{city}: 72F, sunny" }, null)],
};

var followUp = await client.Models.GenerateContentAsync(model: "gemini-flash-lite-latest", contents: [userTurn, modelTurn, toolResultTurn], config: config);
Console.WriteLine(followUp.Text);
}

Agent frameworks & SDKs

More advanced: these frameworks wrap the SDKs above. Each still points its underlying model client at the gateway route.

using System.ClientModel;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using OpenAI;

var options = new OpenAIClientOptions { Endpoint = new Uri($"{gatewayUrl}/openai") };
options.AddPolicy(new ExtraHeadersPolicy(openaiApiKey), PipelinePosition.PerCall);
var client = new OpenAIClient(new ApiKeyCredential(gatewayKey), options);

static string GetWeather(string city) => $"{city}: 72F, sunny";
var weatherTool = AIFunctionFactory.Create(GetWeather, name: "get_weather", description: "Get the current weather for a city");

AIAgent agent = client.GetChatClient("gpt-4o-mini").AsAIAgent(
instructions: "You are a helpful assistant. Use the get_weather tool when asked about weather.",
tools: [weatherTool]);

var result = await agent.RunAsync("What's the weather in Paris?");

ExtraHeadersPolicy is a small PipelinePolicy that adds X-Provider-Key (and optionally traceparent) to every outgoing request - the same pattern as the header injector shown in Go, adapted to System.ClientModel's pipeline API.

Every framework tab above adds tracing the same way: start an Activity as shown in the tracing section above, derive traceparent from it, and add it alongside X-Provider-Key/X-Gateway-Key in that tab's header dictionary/policy.

LlamaIndex .NET has no released package as of this writing - not covered here.

Next steps