-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathchat.ps1
More file actions
118 lines (105 loc) · 5.06 KB
/
Copy pathchat.ps1
File metadata and controls
118 lines (105 loc) · 5.06 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
<#
.SYNOPSIS
AI Server quickstart — PowerShell (5.1 and 7+), no modules to install.
.DESCRIPTION
Shows the client contract: Bearer auth, model discovery via GET /v1/models,
a streaming chat completion, and the responses a real AI Server can give you
(401, 429 free-tier metering, 503 backpressure with Retry-After).
.EXAMPLE
$env:AISERVER_BASE_URL = "http://192.168.1.42:11436/v1"
$env:AISERVER_API_KEY = "ai-suite_..."
.\chat.ps1 "Why is the sky blue?"
#>
[CmdletBinding()]
param(
[Parameter(ValueFromRemainingArguments = $true)]
[string[]]$Prompt
)
$ErrorActionPreference = 'Stop'
$BaseUrl = ($env:AISERVER_BASE_URL, 'http://localhost:11436/v1' -ne $null)[0].TrimEnd('/')
$ApiKey = $env:AISERVER_API_KEY
$Model = $env:AISERVER_MODEL # optional override; otherwise auto-discovered
$Text = if ($Prompt) { $Prompt -join ' ' } else { 'Say hello in one short sentence.' }
if (-not $ApiKey) {
Write-Error 'Set AISERVER_API_KEY (issue a key in AI Server -> API keys).'
exit 1
}
$Headers = @{ Authorization = "Bearer $ApiKey" }
# Turn AI Server's error responses into advice rather than a stack trace.
function Resolve-AiServerError {
param($ErrorRecord)
$resp = $ErrorRecord.Exception.Response
if (-not $resp) {
Write-Error ("Could not reach $BaseUrl : " + $ErrorRecord.Exception.Message + "`n" +
" * Is AI Server running, and does AISERVER_BASE_URL end in /v1?`n" +
" * Serving another machine? It must be bound to your network AND allowed`n" +
" through the firewall for that port.")
exit 1
}
$code = [int]$resp.StatusCode
switch ($code) {
401 {
Write-Error ("The server rejected the API key (401).`n" +
" * Issue a key in AI Server -> API keys and set AISERVER_API_KEY.`n" +
' * A server that serves a network always requires a key.')
}
429 {
# Generic (non-AI-Suite) clients are metered on the free tier: a deliberate cap,
# not a transient failure, so don't retry in a loop.
if ($resp.Headers['X-AISuite-Upgrade'] -eq '1') {
Write-Error ("Free-tier limit reached for generic API clients (429).`n" +
' Upgrade the server to Pro to lift it, or try again later.')
} else {
Write-Error "Rate limited (429). Retry-After: $($resp.Headers['Retry-After'])s"
}
}
503 { Write-Error "Server busy (503) — retry in $($resp.Headers['Retry-After'])s (a model may be loading)." }
default { Write-Error "HTTP $code from the server." }
}
exit 1
}
# ── 1. Model discovery — ids are installation-specific, so never hard-code them ──
if (-not $Model) {
try {
$models = (Invoke-RestMethod -Uri "$BaseUrl/models" -Headers $Headers).data.id
} catch { Resolve-AiServerError $_ }
if (-not $models) { Write-Error 'The server reports no models. Install one in AI Server -> Models.'; exit 1 }
# Prefer a locally-hosted model (no cloud key needed), and skip reasoning models by
# default: they emit <think> blocks that would just confuse a first run. Any of them
# still works — set AISERVER_MODEL to choose deliberately.
$local = @($models | Where-Object { $_ -notlike 'cloud-*' })
$plain = @($local | Where-Object { $_ -notmatch '(?i)-r1|r1-|qwq|reasoner|thinking' })
$Model = if ($plain.Count) { $plain[0] } elseif ($local.Count) { $local[0] } else { @($models)[0] }
Write-Host "[using model $Model; set AISERVER_MODEL to override]" -ForegroundColor DarkGray
}
# ── 2. Streaming chat completion ────────────────────────────────────────────────
# Invoke-RestMethod buffers the whole body, which would defeat streaming — so read
# the response stream directly. Each line is "data: {json}", ending with "data: [DONE]".
$body = @{
model = $Model
messages = @(@{ role = 'user'; content = $Text })
stream = $true
} | ConvertTo-Json -Depth 5 -Compress
$req = [System.Net.HttpWebRequest]::Create("$BaseUrl/chat/completions")
$req.Method = 'POST'
$req.ContentType = 'application/json'
$req.Headers.Add('Authorization', "Bearer $ApiKey")
$req.Timeout = 300000
$bytes = [Text.Encoding]::UTF8.GetBytes($body)
$req.GetRequestStream().Write($bytes, 0, $bytes.Length)
try { $response = $req.GetResponse() } catch { Resolve-AiServerError $_ }
$backend = $response.Headers['X-AISuite-Backend']
if ($backend) { Write-Host "[served by $backend]" -ForegroundColor DarkGray }
$reader = [IO.StreamReader]::new($response.GetResponseStream())
while (-not $reader.EndOfStream) {
$line = $reader.ReadLine()
if (-not $line.StartsWith('data:')) { continue }
$payload = $line.Substring(5).Trim()
if ($payload -eq '[DONE]') { break }
try {
$delta = ($payload | ConvertFrom-Json).choices[0].delta.content
if ($delta) { Write-Host -NoNewline $delta }
} catch { } # ignore keep-alive / partial frames
}
$reader.Dispose(); $response.Dispose()
Write-Host ''