PowerShell и Claude API: Invoke-RestMethod, кириллица и стриминг

Как вызывать Claude из PowerShell так, чтобы кириллица не ломалась ни в запросе, ни в ответе: готовая функция для 5.1 и 7, обработка ошибок HTTP, параллельные запросы и стриминг через HttpClient.

5 мин · · разработка · автоматизация

PowerShell — естественный способ подключить Claude API к администрированию Windows: разбор журналов событий, генерация отчётов, обработка выгрузок. Сам запрос делается встроенным Invoke-RestMethod, а основные сложности связаны с кодировкой: в Windows PowerShell 5.1 кириллица легко превращается в «кракозябры» и в запросе, и в ответе. Ниже — простой пример, надёжная функция для обеих версий, обработка ошибок, параллельные запросы и стриминг.

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

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

bash
$env:ZUKKO_API_KEY = 'fetch_...'
[Environment]::SetEnvironmentVariable('ZUKKO_API_KEY', 'fetch_...', 'User')

Первая строка действует только в текущей сессии, вторая сохраняет переменную для пользователя — она появится в новых окнах PowerShell. Не вписывайте ключ в сами скрипты: их часто копируют и выкладывают в общие репозитории.

Простой запрос в PowerShell 7

Zukko принимает OpenAI-формат по адресу https://api.zukko.pro/v1. Для PowerShell 7 (лучше 7.4 и новее) достаточно такого кода:

bash
$body = @{
    model      = 'claude-sonnet-5'
    max_tokens = 400
    messages   = @(
        @{ role = 'user'; content = 'Объясни разницу между Get-Content и Get-Content -Raw' }
    )
} | ConvertTo-Json -Depth 10

$response = Invoke-RestMethod -Uri 'https://api.zukko.pro/v1/chat/completions' `
    -Method Post `
    -Headers @{ Authorization = "Bearer $env:ZUKKO_API_KEY" } `
    -ContentType 'application/json; charset=utf-8' `
    -Body ([System.Text.Encoding]::UTF8.GetBytes($body))

$response.choices[0].message.content

Две детали, на которых чаще всего ошибаются:

  • -Depth 10 у ConvertTo-Json. По умолчанию глубина равна 2, и вложенные объекты превращаются в строки вида System.Collections.Hashtable — API получит испорченный запрос.
  • Тело как байты UTF-8. [System.Text.Encoding]::UTF8.GetBytes($body) гарантирует, что кириллица уйдёт в UTF-8 независимо от версии PowerShell и его настроек кодировки.

Почему в PowerShell 5.1 ломается кириллица

В Windows PowerShell 5.1 проблема бывает с двух сторон:

  1. Запрос. Если передать в -Body строку, 5.1 может закодировать её не в UTF-8. Решение то же — передавать байты.
  2. Ответ. Если в заголовке ответа не указана кодировка, Invoke-RestMethod в 5.1 может декодировать тело не как UTF-8, и русский текст в ответе превратится в нечитаемые символы. Надёжный обход — получить ответ через Invoke-WebRequest и самому декодировать сырые байты как UTF-8.

Ещё одна мелочь: чтобы кириллица корректно отображалась в консоли 5.1, выполните [Console]::OutputEncoding = [System.Text.Encoding]::UTF8.

Функция для PowerShell 5.1 и 7

Функция ниже работает в обеих версиях: отправляет тело байтами, сама декодирует ответ, понимает системный промпт и принимает текст из конвейера.

bash
function Invoke-Zukko {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [string]$Prompt,
        [string]$Model = 'claude-sonnet-5',
        [string]$System,
        [int]$MaxTokens = 800
    )
    process {
        if (-not $env:ZUKKO_API_KEY) { throw 'Задайте переменную окружения ZUKKO_API_KEY' }

        $messages = [System.Collections.ArrayList]@()
        if ($System) { [void]$messages.Add(@{ role = 'system'; content = $System }) }

Пояснения:

  • $err = $_ сохраняет ошибку до switch: внутри switch переменная $_ означает уже проверяемое значение, а не исключение.
  • Код ответа берётся из $err.Exception.Response.StatusCode — это свойство есть и у исключения 5.1, и у исключения PowerShell 7, а приведение к [int] даёт число. Тело ответа с ошибкой лежит в $err.ErrorDetails.Message.
  • -UseBasicParsing нужен в 5.1, чтобы командлет не пытался разбирать ответ движком Internet Explorer; в PowerShell 7 параметр оставлен для совместимости и ничего не меняет.
  • Сообщения собираются в ArrayList, поэтому массив messages сериализуется в JSON корректно и с одним, и с двумя элементами.

Примеры:

bash
Invoke-Zukko 'Напиши регулярное выражение для IPv4-адреса'

Get-Content .\report.csv -Raw -Encoding UTF8 |
    Invoke-Zukko -System 'Кратко опиши, что в этой выгрузке, и найди аномалии'

Get-WinEvent -LogName System -MaxEvents 50 |
    Format-List TimeCreated, Id, LevelDisplayName, Message | Out-String |
    Invoke-Zukko -System 'Сгруппируй события и укажи, на что обратить внимание' -Model 'glm-5.3-flash'

Сохранить ответ в файл: Invoke-Zukko 'Черновик письма' | Set-Content .\draft.txt -Encoding UTF8.

Параллельные запросы

В PowerShell 7 есть ForEach-Object -Parallel. Функции из основной сессии внутри параллельного блока не видны, поэтому запрос пишем прямо в блоке, а переменные окружения доступны как обычно:

bash
Get-ChildItem .\docs -Filter *.md | ForEach-Object -Parallel {
    $body = @{
        model      = 'glm-5.3-flash'
        max_tokens = 500
        messages   = @(
            @{ role = 'system'; content = 'Перескажи текст в 5 пунктах.' },
            @{ role = 'user'; content = (Get-Content $_.FullName -Raw -Encoding UTF8) }
        )
    } | ConvertTo-Json -Depth 10

    $r = Invoke-RestMethod -Uri 'https://api.zukko.pro/v1/chat/completions' -Method Post `
        -Headers @{ Authorization = "Bearer $env:ZUKKO_API_KEY" } `
        -ContentType 'application/json; charset=utf-8' `
        -Body ([System.Text.Encoding]::UTF8.GetBytes($body))

    Set-Content -Path ($_.FullName + '.summary.txt') -Value $r.choices[0].message.content -Encoding utf8
} -ThrottleLimit 4

-ThrottleLimit 4 держит не больше четырёх запросов одновременно; если получаете 429, уменьшите значение. В Windows PowerShell 5.1 параметра -Parallel нет — там используйте Start-Job (каждое задание запускается отдельным процессом, это медленнее) или обрабатывайте файлы последовательно функцией Invoke-Zukko.

Стриминг ответа

Invoke-RestMethod ждёт ответ целиком, поэтому для вывода текста по мере генерации нужен .NET HttpClient. Сервер присылает строки data: {...}, а последней — data: [DONE]. Перед ней приходит чанк с usage и пустым массивом choices, поэтому перед обращением к choices[0] проверяем Count:

bash
Add-Type -AssemblyName System.Net.Http

$json = @{
    model      = 'claude-sonnet-5'
    max_tokens = 800
    stream     = $true
    messages   = @(@{ role = 'user'; content = 'Напиши чек-лист аудита прав доступа в Active Directory' })
} | ConvertTo-Json -Depth 10

$client = [System.Net.Http.HttpClient]::new()
$request = [System.Net.Http.HttpRequestMessage]::new([System.Net.Http.HttpMethod]::Post, 'https://api.zukko.pro/v1/chat/completions')
$request.Headers.Authorization = [System.Net.Http.Headers.AuthenticationHeaderValue]::new('Bearer', $env:ZUKKO_API_KEY)
$request.Content = [System.Net.Http.StringContent]::new($json, [System.Text.Encoding]::UTF8, 'application/json')

Синтаксис ::new() работает начиная с PowerShell 5.0, так что код подходит и для 5.1. StreamReader явно получает UTF-8, поэтому кириллица в потоке не ломается.

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

Модель передаётся параметром -Model. Цены в Zukko за 1M токенов (вход/выход): claude-sonnet-5 и claude-opus-5-5 — $0.30 / $0.30, gpt-5.6-sol — $0.30 / $0.30, gpt-5.6-luna — $0.10 / $0.10, glm-5.3-flash — $0.04 / $0.04. Актуальный список:

bash
(Invoke-RestMethod 'https://api.zukko.pro/v1/models' -Headers @{ Authorization = "Bearer $env:ZUKKO_API_KEY" }).data.id

или страница «Модели» в кабинете. Пример: ежедневный отчёт по журналу событий — 6 000 входных и 600 выходных токенов, 30 запусков в месяц. На claude-sonnet-5: 30 × (6 000 + 600) × $0.30 / 1 000 000 = $0.0594 в месяц. На glm-5.3-flash: 30 × 6 600 × $0.04 / 1 000 000 = $0.00792. Хватает ли дешёвой модели для вашего отчёта, проверьте на своих данных: сравните ответы двух моделей на одних и тех же журналах за неделю.

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

Ошибка «Не удалось создать защищённый канал SSL/TLS» в PowerShell 5.1

На старых системах .NET Framework по умолчанию может предлагать устаревшие версии TLS. Перед запросом выполните [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12. В PowerShell 7 это не требуется.

Ответ приходит в виде «РџСЂРёРІРµС‚»

Ответ декодирован не как UTF-8 — типичная ситуация для Invoke-RestMethod в 5.1. Используйте функцию Invoke-Zukko выше: она декодирует байты ответа сама. Если искажения только в консоли, а в файле текст нормальный, задайте [Console]::OutputEncoding = [System.Text.Encoding]::UTF8.

Как запускать скрипт по расписанию?

Через «Планировщик заданий»: действие powershell.exe -NoProfile -File C:\Scripts\report.ps1 (или pwsh.exe для PowerShell 7). Переменную ZUKKO_API_KEY задайте для учётной записи, от которой запускается задание. Для такого скрипта удобно завести отдельный ключ с дневным лимитом в долларах — см. лимиты ключей.

Можно ли использовать Anthropic-формат?

Да, POST https://api.zukko.pro/v1/messages с заголовком anthropic-version: 2023-06-01; текст ответа — в .content[0].text. Формат описан в справочнике Messages API, а OpenAI-формат — в справочнике chat completions.

zukko

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

за Claude Code & ChatGPT

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