Claude API в C# и .NET: HttpClient, OpenAI SDK и ASP.NET Core

Подключаем Claude к .NET-приложению двумя способами: голый HttpClient и официальный пакет OpenAI. Стриминг, обработка ошибок, таймауты и минимальный API на ASP.NET Core.

4 мин · · разработка

Если вы пишете на C#, Claude API подключается без отдельного SDK от Anthropic: Zukko принимает запросы в OpenAI-формате по адресу https://api.zukko.pro/v1, поэтому подходят и обычный HttpClient, и официальный NuGet-пакет OpenAI. Ниже — оба варианта, стриминг с корректной обработкой последнего чанка, ошибки и таймауты, а в конце — минимальный API на ASP.NET Core. Примеры рассчитаны на .NET 8 и новее.

Ключ и переменные окружения

Ключ вида fetch_... создаётся в кабинете Zukko. Полный ключ показывается один раз — сохраните его сразу. В коде ключ не храните:

  • локально — переменная окружения ZUKKO_API_KEY или dotnet user-secrets set "Zukko:ApiKey" "fetch_...";
  • на сервере — переменная окружения (для конфигурации ASP.NET Core имя Zukko__ApiKey) или хранилище секретов вашей платформы.

Вариант 1: HttpClient и System.Text.Json

Без сторонних пакетов: HttpClient, System.Net.Http.Json и JsonDocument входят в .NET.

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

var apiKey = Environment.GetEnvironmentVariable("ZUKKO_API_KEY")
    ?? throw new InvalidOperationException("ZUKKO_API_KEY не задан");

using var http = new HttpClient { BaseAddress = new Uri("https://api.zukko.pro/v1/") };
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);

var request = new
{
    model = "claude-sonnet-5",
    max_tokens = 500,

Обратите внимание на BaseAddress со слешем на конце и относительный путь chat/completions без слеша в начале: так итоговый адрес получается https://api.zukko.pro/v1/chat/completions. Если поставить /chat/completions, HttpClient отбросит /v1.

В реальном приложении не создавайте HttpClient на каждый запрос — используйте один экземпляр или IHttpClientFactory.

Стриминг через HttpClient

При "stream": true ответ приходит строками data: {...} в формате SSE, последняя строка — data: [DONE]. Важная деталь: перед [DONE] сервер присылает чанк с usage и пустым массивом choices. Код, который сразу берёт choices[0], на нём упадёт, поэтому длину массива проверяем:

bash
var streamRequest = new HttpRequestMessage(HttpMethod.Post, "chat/completions")
{
    Content = JsonContent.Create(new
    {
        model = "claude-sonnet-5",
        max_tokens = 500,
        stream = true,
        messages = new[] { new { role = "user", content = "Напиши хайку про сборщик мусора." } }
    })
};

using var streamResponse = await http.SendAsync(streamRequest, HttpCompletionOption.ResponseHeadersRead);
streamResponse.EnsureSuccessStatusCode();
using var reader = new StreamReader(await streamResponse.Content.ReadAsStreamAsync());

HttpCompletionOption.ResponseHeadersRead нужен, чтобы читать поток сразу, а не ждать, пока весь ответ загрузится в память.

Вариант 2: официальный OpenAI .NET SDK

bash
dotnet add package OpenAI

Свой адрес API задаётся через OpenAIClientOptions.Endpoint — так описано в разделе «Using a custom base URL» README пакета. ApiKeyCredential лежит в пространстве имён System.ClientModel, его легко забыть подключить.

bash
using System.ClientModel;
using OpenAI;
using OpenAI.Chat;

var client = new ChatClient(
    model: "claude-sonnet-5",
    credential: new ApiKeyCredential(Environment.GetEnvironmentVariable("ZUKKO_API_KEY")!),
    options: new OpenAIClientOptions { Endpoint = new Uri("https://api.zukko.pro/v1") });

ChatCompletion completion = await client.CompleteChatAsync(
    new SystemChatMessage("Отвечай кратко."),
    new UserChatMessage("Чем record отличается от class в C#?"));

Console.WriteLine(completion.Content[0].Text);
Console.WriteLine($"Токены: {completion.Usage.InputTokenCount} вход, {completion.Usage.OutputTokenCount} выход");

Стриминг в SDK

CompleteChatStreamingAsync возвращает AsyncCollectionResult<StreamingChatCompletionUpdate>, его перебирают через await foreach без дополнительного await перед вызовом:

bash
await foreach (StreamingChatCompletionUpdate update in client.CompleteChatStreamingAsync(
    new UserChatMessage("Перечисли пять принципов SOLID одной строкой каждый.")))
{
    if (update.ContentUpdate.Count > 0)
    {
        Console.Write(update.ContentUpdate[0].Text);
    }
    if (update.Usage is not null)
    {
        Console.WriteLine($"\nВсего токенов: {update.Usage.TotalTokenCount}");
    }
}

Проверка ContentUpdate.Count > 0 и есть защита от финального обновления без текста: в нём приходит только Usage.

Параметры, ошибки и таймауты

  • Лимит ответа задаётся через new ChatCompletionOptions { MaxOutputTokenCount = 800 } вторым аргументом вместе со списком сообщений.
  • Ошибки HTTP SDK выбрасывает как ClientResultException; код ответа — в свойстве Status. Коды Zukko: 401 — неверный ключ, 402 — не хватает баланса, 403 — модель не разрешена для ключа, 413 — слишком большой запрос, 429 — слишком частые запросы, 503 — провайдер недоступен.
  • По README SDK сам повторяет запросы при 408, 429, 500, 502, 503 и 504 до трёх раз с экспоненциальной задержкой.
  • Таймаут сетевой операции задаётся свойством NetworkTimeout у OpenAIClientOptions (оно унаследовано от ClientPipelineOptions). Для длинных ответов увеличьте его, например до трёх минут.

ASP.NET Core: минимальный API

По README клиенты SDK потокобезопасны и их можно регистрировать как singleton — так переиспользуются HTTP-соединения. Проект создаётся командой dotnet new web, затем dotnet add package OpenAI.

bash
using System.ClientModel;
using OpenAI;
using OpenAI.Chat;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSingleton(_ => new ChatClient(
    model: builder.Configuration["Zukko:Model"] ?? "claude-sonnet-5",
    credential: new ApiKeyCredential(builder.Configuration["Zukko:ApiKey"]
        ?? throw new InvalidOperationException("Zukko:ApiKey не задан")),
    options: new OpenAIClientOptions
    {
        Endpoint = new Uri("https://api.zukko.pro/v1"),
        NetworkTimeout = TimeSpan.FromMinutes(3),

Проверка: curl -X POST localhost:5000/ask -H "Content-Type: application/json" -d '{"question":"Что такое DI?"}' (порт смотрите в выводе dotnet run). Второй маршрут отдаёт текст по мере генерации. Если фронтенду нужен именно SSE, оборачивайте каждый фрагмент в data: ...\n\n и экранируйте переносы строк внутри фрагмента, например сериализуя его в JSON.

Модели и стоимость

Модель задаётся строкой model (в примере ASP.NET Core — настройкой Zukko:Model). Цены в Zukko за 1M токенов, вход/выход:

МодельЦенаКонтекст
claude-sonnet-5$0.30 / $0.301M
claude-opus-5-5$0.30 / $0.301M
gpt-5.6-sol$0.30 / $0.301M
gpt-5.6-luna$0.10 / $0.101M
glm-5.3-flash$0.04 / $0.04128K

Актуальный список — GET https://api.zukko.pro/v1/models или страница «Модели» в кабинете. Пример расчёта: эндпоинт /ask обрабатывает 10 000 вопросов в месяц, в среднем 400 входных и 300 выходных токенов. На claude-sonnet-5: 10 000 × (400 + 300) × $0.30 / 1 000 000 = $2.10 в месяц. Какая модель лучше отвечает вашим пользователям, проверьте на своих задачах: прогоните одинаковый набор реальных вопросов через две-три модели и сравните ответы.

Частые вопросы

Почему SDK отвечает 404?

Почти всегда дело в адресе. Для OpenAI SDK Endpoint должен быть https://api.zukko.pro/v1 — с /v1. В варианте с HttpClient проверьте слеш на конце BaseAddress и отсутствие слеша в начале относительного пути.

Нужен ли пакет Anthropic, чтобы работать с Claude?

Нет. Шлюз принимает OpenAI-формат для любой модели, включая Claude, и сам конвертирует запрос. Если всё же нужен Anthropic-формат (/v1/messages), его описание есть в справочнике Messages API; разница форматов — в статье какой формат выбрать.

Как ограничить расходы приложения?

Создайте для приложения отдельный ключ с дневным и месячным лимитом в долларах и списком разрешённых моделей — см. лимиты ключей. Плюс ограничивайте MaxOutputTokenCount в каждом запросе.

Как передать историю диалога?

Передавайте весь список сообщений: SystemChatMessage, затем чередующиеся UserChatMessage и AssistantChatMessage. API ничего не хранит между запросами, поэтому историю держите у себя и обрезайте старые реплики — они оплачиваются как входные токены при каждом запросе.

zukko

Платите в 25×
меньше

за Claude Code & ChatGPT

Один ключ fetch_* и base URL api.zukko.pro — в Cursor, Claude Code, Codex и любом SDK.