Last reviewed: November 12, 2026 (Asia/Shanghai); primary references fetched November 11 UTC.

Direct answer

Build a CometAPI PowerShell client by serializing a request object with ConvertTo-Json, sending that JSON string through Invoke-WebRequest, and validating the response before returning assistant text. Preserve messages as an array, set serialization depth explicitly, and declare UTF-8 in the request content type. Decode with ConvertFrom-Json -AsHashtable -NoEnumerate -DateKind String so the acceptance function can inspect arrays and keep date-like text as text.

The CometAPI Chat Completions reference documents the HTTPS endpoint, bearer authentication, message fields, and completion outcomes. Microsoft’s Invoke-WebRequest reference supplies the transport options used below. This walkthrough implements one non-streaming text result with a small, explicit application contract.

Who this is for

Use this pattern in an interactive PowerShell script or a reviewed automation task that needs direct control over the request. Use PowerShell 7.5 or later with the parameters shown here; -DateKind was introduced in 7.5. Check your installed version with $PSVersionTable.PSVersion. Windows PowerShell 5.1 does not provide this command contract.

Choose a currently offered text model using the CometAPI model catalog reference . It documents public GET /api/models, exact request IDs, capability metadata, and endpoint hints. Confirm the chosen model supports Chat Completions and this minimal text request. Catalog presence alone does not establish account access. Keep optional model-specific parameters out of the first check.

If you prefer an existing terminal application, the separate LLM CLI setup guide covers endpoint registration and local prompt logging. This article builds the PowerShell request and response boundary itself.

Key takeaways

  • Construct messages with @(...), even for one message.
  • Serialize the payload before assigning Body; a PowerShell dictionary is not automatically a JSON request.
  • Set -Depth and make serialization warnings stop the request.
  • Keep JSON arrays and string types visible during response validation.
  • Pass the credential through PowerShell’s bearer-token parameter and keep request objects out of diagnostics.
  • Treat read timeouts and a post-download parsing ceiling according to their actual scope.

Build the payload and validate decoded text

Save the following as comet-client.ps1. These first two functions do not make network requests:

#requires -Version 7.5

function New-CometChatJson {
    [CmdletBinding()]
    param([string] $Prompt, [string] $Model)

    if ([string]::IsNullOrWhiteSpace($Model)) {
        throw 'invalid_model_configuration'
    }
    if ([string]::IsNullOrWhiteSpace($Prompt) -or
        [Text.Encoding]::UTF8.GetByteCount($Prompt) -gt 8000) {
        throw 'invalid_prompt'
    }
    $payload = [ordered]@{
        model = $Model
        messages = @(@{ role = 'user'; content = $Prompt })
        stream = $false
    }
    try {
        ConvertTo-Json -InputObject $payload -Depth 8 -Compress `
            -WarningAction Stop -ErrorAction Stop
    } catch {
        throw 'request_encoding_failed'
    }
}

function Get-CometChatText {
    [CmdletBinding()]
    param([AllowEmptyString()][string] $Json)

    if ([string]::IsNullOrWhiteSpace($Json)) {
        throw 'invalid_response_json'
    }
    try {
        $data = ConvertFrom-Json -InputObject $Json -AsHashtable `
            -NoEnumerate -DateKind String -Depth 32 -ErrorAction Stop
    } catch {
        throw 'invalid_response_json'
    }
    if ($data -isnot [System.Collections.IDictionary] -or
        $data['object'] -isnot [string] -or
        $data['object'] -cne 'chat.completion') {
        throw 'invalid_completion_envelope'
    }
    $choices = $data['choices']
    if ($choices -isnot [array] -or $choices.Count -ne 1) {
        throw 'invalid_completion_choices'
    }
    $choice = $choices[0]
    if ($choice -isnot [System.Collections.IDictionary] -or
        $choice['finish_reason'] -isnot [string] -or
        $choice['finish_reason'] -cne 'stop') {
        throw 'unaccepted_finish_reason'
    }
    $message = $choice['message']
    if ($message -isnot [System.Collections.IDictionary] -or
        $message['role'] -isnot [string] -or
        $message['role'] -cne 'assistant' -or
        $null -ne $message['refusal'] -or
        $null -ne $message['tool_calls']) {
        throw 'unaccepted_assistant_message'
    }
    $text = $message['content']
    if ($text -isnot [string] -or
        [string]::IsNullOrWhiteSpace($text)) {
        throw 'missing_assistant_text'
    }
    return $text
}

Microsoft’s ConvertTo-Json reference documents a default depth of two and warnings when the object exceeds the selected depth. Depth eight is a local allowance for this small payload. -WarningAction Stop prevents a later extension from silently sending a truncated representation. Keep the backtick at the end of its line, with no trailing spaces.

The outer @(...) preserves the single-message array. Do not add -AsArray to ConvertTo-Json here: that switch wraps the entire payload, whereas this operation expects a top-level object containing an array. Building an object also lets the serializer escape quotes, backslashes, and line breaks without hand-built JSON string interpolation.

Microsoft’s ConvertFrom-Json reference documents hash-table output, -NoEnumerate, and -DateKind. -NoEnumerate preserves a top-level array, allowing the envelope check to reject even a one-item array around an otherwise valid completion. -DateKind String prevents an answer consisting of an ISO timestamp from becoming a date object.

The validator checks types before comparisons. In PowerShell, comparing a collection with a value can produce a collection of matching or nonmatching elements; it is not a substitute for proving a scalar field is a string. The subsequent -cne comparisons also require the documented spelling of values such as assistant and stop.

One choice, a stop finish reason, nonblank string content, and absent or null refusal and tool-call fields are this client’s acceptance policy. Even an empty non-null tool_calls array is rejected deliberately. Extend the result type if the product needs refusals, tools, or partial text.

Send an explicit JSON request

Append this function to the same file:

function Invoke-CometChat {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string] $Prompt,
        [Parameter(Mandatory)][string] $Model,
        [Parameter(Mandatory)][securestring] $Token
    )

    if ($Token.Length -eq 0) { throw 'invalid_credential_configuration' }
    $json = New-CometChatJson -Prompt $Prompt -Model $Model
    $request = @{
        Uri = 'https://api.cometapi.com/v1/chat/completions'
        Method = 'Post'
        Authentication = 'Bearer'
        Token = $Token
        ContentType = 'application/json; charset=utf-8'
        Headers = @{ Accept = 'application/json' }
        Body = $json
        MaximumRedirection = 0
        MaximumRetryCount = 0
        ConnectionTimeoutSeconds = 15
        OperationTimeoutSeconds = 30
        SkipHttpErrorCheck = $true
        ErrorAction = 'Stop'
        Verbose = $false
        Debug = $false
        ProgressAction = 'SilentlyContinue'
    }
    try {
        $response = Invoke-WebRequest @request
    } catch {
        throw 'transport_failed'
    }
    if ($response.StatusCode -ne 200) {
        throw ('http_' + [int] $response.StatusCode)
    }
    $types = @($response.Headers['Content-Type'])
    if ($types.Count -ne 1 -or $types[0] -isnot [string] -or
        ($types[0].Split(';')[0].Trim() -ine 'application/json')) {
        throw 'unexpected_media_type'
    }
    if ($response.Content -isnot [string]) {
        throw 'unexpected_text_body'
    }
    if ([Text.Encoding]::UTF8.GetByteCount($response.Content) -gt 1048576) {
        throw 'response_exceeds_parse_limit'
    }
    return (Get-CometChatText -Json $response.Content)
}

The web-request reference says a POST defaults to a form content type when ContentType is omitted. It also documents using the declared charset to encode the body. Here, Body receives the serialized string and ContentType explicitly selects JSON and UTF-8.

Authentication = 'Bearer' and Token construct the authorization header from a SecureString. This avoids putting a literal credential in the command or source file. It does not make the token safe to export, trace, or share. Keep $request, raw response objects, and caught error records out of copied diagnostic bundles.

SkipHttpErrorCheck makes HTTP error responses available for classification; it does not approve them. The function still requires status 200, one JSON media-type value, and an accepted completion. It reports fixed categories rather than copying upstream error bodies. Redirect following and the cmdlet’s configured HTTP-status retries are disabled.

Run a small interactive check

Load the functions, enter a reviewed model ID, and obtain the token at the protected prompt:

. ./comet-client.ps1
$model = Read-Host 'Verified CometAPI Chat Completions model ID'
$token = Read-Host 'CometAPI API key' -AsSecureString
try {
    Invoke-CometChat -Model $model -Token $token `
        -Prompt 'Explain idempotency in one short sentence.'
} catch {
    Write-Error -Message 'Chat request failed; inspect privately.'
} finally {
    $token.Dispose()
    Remove-Variable token
}

Run the offline checks below before this live step. The call sends its prompt remotely and can incur usage. For unattended automation, supply a token through your existing approved secret mechanism rather than adding an interactive prompt or a literal key to a scheduled command.

Record the installed PowerShell version, selected model ID, observation time, status category, and acceptance result. Keep the prompt, answer, credential, and raw error out of shared records. Treat generated output as text for review; do not pass it into Invoke-Expression or another execution path.

Understand the timeout and size boundaries

The Invoke-WebRequest timeout descriptions distinguish request-pending time from data-read time. OperationTimeoutSeconds = 30 terminates a read that stalls too long; it is not a thirty-second wall-clock deadline for the complete response. A response that continues delivering data can take longer. DNS resolution can also affect the timing of a connection failure.

The function receives the body before checking its UTF-8 byte count. That ceiling limits decoded text admitted to JSON parsing; it does not cap bytes downloaded, decompression, or peak process memory. It also does not bound downloaded error bodies. For an exposed service or an automation job requiring a hard total deadline and transfer-size limit, implement and test those boundaries in a lower-level reader or controlled transport before adopting this compact client.

The 8,000-byte prompt allowance, parsing depth, timeouts, and one-mebibyte parsing ceiling are local choices. None establishes a CometAPI quota or token budget. The R client walkthrough explains a similar distinction between post-download checks and limits enforced during transfer in another runtime.

Check JSON behavior without a credential

Save this as check-json.ps1 beside the client and run pwsh -NoProfile -File ./check-json.ps1:

. "$PSScriptRoot/comet-client.ps1"
$ErrorActionPreference = 'Stop'

function Assert-True([bool] $Condition, [string] $Label) {
    if (-not $Condition) { throw "check_failed:$Label" }
}
function Assert-Code([scriptblock] $Action, [string] $Expected) {
    $observed = 'accepted'
    try { $null = & $Action } catch { $observed = $_.Exception.Message }
    Assert-True ($observed -ceq $Expected) $Expected
}

$prompt = 'Explain "café".' + "`n" + '你好'
$wire = New-CometChatJson -Prompt $prompt -Model 'fixture-model'
$data = ConvertFrom-Json -InputObject $wire -AsHashtable `
    -NoEnumerate -DateKind String
Assert-True ($data['messages'] -is [array]) 'messages_array'
Assert-True ($data['messages'].Count -eq 1) 'one_message'
Assert-True ($data['messages'][0]['content'] -ceq $prompt) 'prompt_round_trip'
Assert-True ($data['stream'] -is [bool] -and
    $data['stream'] -eq $false) 'stream_boolean'

$good = '{"object":"chat.completion","choices":[{"finish_reason":"stop","message":{"role":"assistant","content":"Fixture answer."}}]}'
Assert-True ((Get-CometChatText $good) -ceq 'Fixture answer.') 'accepted_text'
Assert-Code { Get-CometChatText ('[' + $good + ']') } 'invalid_completion_envelope'
Assert-Code { Get-CometChatText ($good.Replace('"stop"', '"length"')) } 'unaccepted_finish_reason'
Assert-Code { Get-CometChatText ($good.Replace('"content":"Fixture answer."', '"content":["text"]')) } 'missing_assistant_text'
$dateText = $good.Replace('Fixture answer.', '2026-11-11T00:00:00Z')
Assert-True ((Get-CometChatText $dateText) -is [string]) 'date_stays_string'
'JSON checks passed.'

These checks exercise serialization and decoded-response acceptance. During preparation, a larger offline suite passed on PowerShell 7.5.4 and 7.6.0, including scalar-versus-array fields, refusals, tool calls, malformed input, and mocked status, media-type, size, and transport failures. The mocks did not exercise a real HTTP connection. No authenticated CometAPI call, live timeout test, or deployment was performed for this article.

Contract details to verify

BoundaryFixture or observationRequired result
Request shapeOne message containing quotes, a newline, and UnicodeJSON object with a messages array and unchanged decoded text
HTTP constructionCapture a request using an isolated test transport and dummy tokenPOST, correct path, JSON content type, reviewed model, stream: false
Status and media type429 response; 200 HTML responseStatus category; unexpected_media_type
DecodingMalformed text; array-wrapped completion; date-like answerRejected syntax or envelope; accepted answer remains a string
Completion policylength, refusal, tools, or array-valued contentRejected according to the text-only contract
Timing and redirectsDelayed headers, stalled reads, and a redirectObserved transport behavior matches the installed runtime; no redirected generation
Live accessOne authorized harmless prompt after fixtures passActual account/model access and an accepted text result recorded separately

Failure modes

A request looks like JSON locally but reaches the server as form data. Pass the serialized string as Body and set ContentType. Printing a PowerShell hash table does not verify the transmitted representation.

Adding a nested option changes the payload silently. Preserve the serialization warning policy and inspect a round-trip fixture whenever the request structure changes. Raising depth without checking the new fields hides the reason the warning occurred.

A scalar field arrives as an array and passes a loose comparison. Keep the explicit type checks. Test array-valued object, role, and finish_reason fields as well as array-valued content.

A timeout triggers another paid generation automatically. This adapter has no retry loop. A local failure does not establish that remote processing stopped. Decide attempt identity, reconciliation, replay safety, and total workflow budget before adding retries around it.

FAQ

Why use Invoke-WebRequest instead of Invoke-RestMethod?

This design inspects status, media type, and decoded-body size before invoking its own JSON decoder. Invoke-WebRequest exposes the response for that sequence. A different client is suitable if it preserves the same explicit acceptance and failure decisions.

Is ConvertFrom-Json a strict duplicate-key validator?

No. Microsoft documents that duplicate keys retain the last value, and PowerShell 6 and later accept JSON comments. These examples validate the decoded structure. If a boundary must reject duplicate keys or nonstandard syntax before interpretation, use a separately reviewed strict parser.

Can this client stream answers or execute tool calls?

The request sets stream to false and the acceptance policy rejects tool-call fields. CometAPI documents streaming as SSE. Streaming and tool execution need separate state handling and, for tools, explicit authorization.

Sources checked

Five distinct primary public pages returned HTTP 200 on November 11, 2026 UTC:

These maintained references support an evergreen integration task. The Microsoft links use their fetched PowerShell 7.6 canonical view; the implementation requires features available from 7.5. Local limits and completion acceptance rules are recommendations derived from the documented interfaces.

Reader next step

Run the credential-free JSON checks, then complete the transport fixture matrix in your installed runtime. Enter one reviewed model and a token at the protected prompt for a small authorized staging call. Keep fixture evidence and that live observation separate, and resolve any required total-deadline or transfer-size controls before using the adapter in an exposed service.