.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.
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
- Gateway Integration
- With trace (optional)
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"}]}'
using System.Diagnostics;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
var activitySource = new ActivitySource("my-service");
using var http = new HttpClient();
using var activity = activitySource.StartActivity("gateway_call", ActivityKind.Client);
var traceparent = activity is null ? null : $"00-{activity.TraceId}-{activity.SpanId}-01";
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);
if (traceparent is not null) req.Headers.Add("traceparent", traceparent);
var res = await http.SendAsync(req);
var result = await res.Content.ReadAsStringAsync();
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
- Anthropic
- Gemini
- Groq / OpenRouter / Ollama
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.
- Gateway Integration
- With trace (optional)
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",
}],
});
var activitySource = new ActivitySource("my-service");
using var activity = activitySource.StartActivity("anthropic_call", ActivityKind.Client);
var traceparent = activity is null ? null : $"00-{activity.TraceId}-{activity.SpanId}-01";
var client = new Anthropic.AnthropicClient(new Anthropic.Core.ClientOptions
{
BaseUrl = $"{gatewayUrl}/anthropic",
ApiKey = anthropicApiKey,
ExtraHeaders = traceparent is null
? new Dictionary<string, string> { ["X-Gateway-Key"] = gatewayKey }
: new Dictionary<string, string> { ["X-Gateway-Key"] = gatewayKey, ["traceparent"] = traceparent },
});
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",
}],
});
Uses the official Google.GenAI package. Same split as Anthropic: apiKey carries the provider key (x-goog-api-key), gateway auth is a separate header on HttpOptions.
- Gateway Integration
- With trace (optional)
using GTypes = Google.GenAI.Types;
var client = new Google.GenAI.Client(
enterprise: null,
vertexAI: false,
apiKey: geminiApiKey,
credential: null,
project: null,
location: null,
httpOptions: new GTypes.HttpOptions
{
BaseUrl = $"{gatewayUrl}/gemini",
Headers = new Dictionary<string, string> { ["X-Gateway-Key"] = gatewayKey },
},
clientOptions: null);
var response = await client.Models.GenerateContentAsync(
model: "gemini-flash-lite-latest",
contents: [new GTypes.Content { Role = "user", Parts = [GTypes.Part.FromText("hello")] }],
config: null);
using GTypes = Google.GenAI.Types;
var activitySource = new ActivitySource("my-service");
using var activity = activitySource.StartActivity("gemini_call", ActivityKind.Client);
var traceparent = activity is null ? null : $"00-{activity.TraceId}-{activity.SpanId}-01";
var geminiHeaders = new Dictionary<string, string> { ["X-Gateway-Key"] = gatewayKey };
if (traceparent is not null) geminiHeaders["traceparent"] = traceparent;
var client = new Google.GenAI.Client(
enterprise: null,
vertexAI: false,
apiKey: geminiApiKey,
credential: null,
project: null,
location: null,
httpOptions: new GTypes.HttpOptions { BaseUrl = $"{gatewayUrl}/gemini", Headers = geminiHeaders },
clientOptions: null);
var response = await client.Models.GenerateContentAsync(
model: "gemini-flash-lite-latest",
contents: [new GTypes.Content { Role = "user", Parts = [GTypes.Part.FromText("hello")] }],
config: null);
These are OpenAI-compatible through the gateway - use OpenAI (the official .NET SDK) pointed at a different route and provider key, the same shape as the OpenAI route itself. X-Provider-Key isn't a client-level option on OpenAIClientOptions - add it via a custom HttpClient/DelegatingHandler passed in through Transport, the same way you'd inject any fixed header.
Without trace:
using OpenAI.Chat;
using System.ClientModel;
var client = new ChatClient(
model: "llama-3.3-70b-versatile",
credential: new ApiKeyCredential(gatewayKey),
options: new OpenAI.OpenAIClientOptions { Endpoint = new Uri($"{gatewayUrl}/groq") });
With trace, add a traceparent header the same way alongside X-Provider-Key in that same custom handler.
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.
- Microsoft Agent Framework
- Semantic Kernel
- LangChain.NET
- Google ADK-C#
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.
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;
var weatherPlugin = KernelPluginFactory.CreateFromFunctions("weather", "Weather tools", new[]
{
KernelFunctionFactory.CreateFromMethod(
(string city) => $"{city}: 72F, sunny",
functionName: "get_weather",
description: "Get the current weather for a city"),
});
var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Add("X-Provider-Key", openaiApiKey);
var builder = Kernel.CreateBuilder();
builder.AddOpenAIChatCompletion(
modelId: "gpt-4o-mini",
endpoint: new Uri($"{gatewayUrl}/openai"),
apiKey: gatewayKey,
httpClient: httpClient);
builder.Plugins.Add(weatherPlugin);
var kernel = builder.Build();
var result = await kernel.InvokePromptAsync("What's the weather in Paris?");
using LangChain.Providers;
using LangChain.Providers.OpenAI;
var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Add("X-Provider-Key", openaiApiKey);
var client = new OpenAiClient(gatewayKey, httpClient, new Uri($"{gatewayUrl}/openai/"), disposeHttpClient: true);
var provider = new OpenAiProvider(client);
var chatModel = new OpenAiChatModel(provider, "gpt-4o-mini");
var response = await chatModel.GenerateAsync("hello");
Uses NTG.Adk - a community port, Google has not published an official ADK for .NET. Its GeminiLlm takes no endpoint override, so this routes through the gateway's OpenAI-compat /gemini route via OpenAILlm instead - the compound gatewayKey||providerKey bearer syntax carries both credentials since OpenAILlm has no separate provider-key header hook.
using NTG.Adk.CoreAbstractions.Agents;
using NTG.Adk.Implementations.Models;
using NTG.Adk.Implementations.Tools;
using NTG.Adk.Operators.Agents;
using NTG.Adk.Operators.Runners;
var weatherTool = FunctionTool.Create(
(string city) => $"{city}: 72F, sunny",
"get_weather",
"Get the current weather for a city",
NTG.Adk.Boundary.Tools.GoogleLLMVariant.GeminiApi);
var llm = new OpenAILlm("gemini-flash-lite-latest", $"{gatewayKey}||{geminiApiKey}", new Uri($"{gatewayUrl}/gemini"));
var agent = new LlmAgent(llm, "gemini-flash-lite-latest", new ToolContextFactory())
{
Instruction = "You are a helpful assistant. Use the get_weather tool when asked about weather.",
Tools = [weatherTool],
EnableAutoFlow = true,
};
var runner = new InMemoryRunner(agent, "my-app", new RunConfig());
await foreach (var evt in runner.RunAsync("user-1", "session-1", "What's the weather in Paris?", null, null, null))
{
// inspect evt.Content for the tool call / final reply
}
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
- OpenAI SDK Integration - the reference JavaScript/TypeScript walkthrough.
- Other Providers - full provider/URL-segment reference.
- Gateway Authentication - key precedence and header details.
- Trace Context - how
traceparentcorrelates observations.