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.
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], на нём упадёт, поэтому длину массива проверяем:
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
dotnet add package OpenAIСвой адрес API задаётся через OpenAIClientOptions.Endpoint — так описано в разделе «Using a custom base URL» README пакета. ApiKeyCredential лежит в пространстве имён System.ClientModel, его легко забыть подключить.
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 перед вызовом:
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.
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.30 | 1M |
claude-opus-5-5 | $0.30 / $0.30 | 1M |
gpt-5.6-sol | $0.30 / $0.30 | 1M |
gpt-5.6-luna | $0.10 / $0.10 | 1M |
glm-5.3-flash | $0.04 / $0.04 | 128K |
Актуальный список — 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.
Похожие статьи
- Промпт-инъекция: что это и как защитить бота и агентаОбъясняем, как работает промпт-инъекция через пользовательский ввод и чужие документы, и какие меры снижают ущерб: изоляция данных, права инструментов, лимиты ключа.
- Свой прокси к нейросети на Node.js: не показывать API-ключ во фронтендеНебольшой сервер на Express между вашим фронтендом и API нейросети: ключ остаётся на сервере, а модели, длина запроса и частота ограничены.
- DeepSeek API на Python: запросы, стриминг, JSON и обработка ошибокГотовый код для DeepSeek V4.1 Flash и V4 Pro на OpenAI SDK: запрос, стриминг, разбор JSON и аккуратная обработка ошибок.
- Промпт-инжиниринг: примеры и 12 готовых шаблонов для работыДвенадцать шаблонов промптов для рабочих задач: что в них написано, почему именно так и сколько стоит один вызов по API.