-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathProgram.cs
More file actions
149 lines (131 loc) · 6.77 KB
/
Copy pathProgram.cs
File metadata and controls
149 lines (131 loc) · 6.77 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
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
// AI Server quickstart — C# / .NET, no NuGet packages (HttpClient + System.Text.Json).
//
// 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).
//
// set AISERVER_BASE_URL=http://192.168.1.42:11436/v1
// set AISERVER_API_KEY=ai-suite_...
// dotnet run -- "Why is the sky blue?"
using System.Net;
using System.Text;
using System.Text.Json;
var baseUrl = (Environment.GetEnvironmentVariable("AISERVER_BASE_URL") ?? "http://localhost:11436/v1").TrimEnd('/');
var apiKey = Environment.GetEnvironmentVariable("AISERVER_API_KEY") ?? "";
var modelOverride = Environment.GetEnvironmentVariable("AISERVER_MODEL");
var prompt = args.Length > 0 ? string.Join(' ', args) : "Say hello in one short sentence.";
if (string.IsNullOrWhiteSpace(apiKey))
return Fail("Set AISERVER_API_KEY (issue a key in AI Server -> API keys).");
using var http = new HttpClient { Timeout = TimeSpan.FromMinutes(5) };
http.DefaultRequestHeaders.Add("Authorization", $"Bearer {apiKey}");
// ── 1. Model discovery — ids are installation-specific, so never hard-code them ──
string model;
if (!string.IsNullOrWhiteSpace(modelOverride))
{
model = modelOverride!;
}
else
{
using var listResponse = await SendAsync(HttpMethod.Get, "/models", null);
using var doc = JsonDocument.Parse(await listResponse.Content.ReadAsStringAsync());
var ids = doc.RootElement.GetProperty("data").EnumerateArray()
.Select(m => m.GetProperty("id").GetString()!).ToList();
if (ids.Count == 0) return Fail("The server reports no models. Install one in AI Server -> Models.");
// 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.
static bool IsReasoning(string id) =>
new[] { "-r1", "r1-", "qwq", "reasoner", "thinking" }
.Any(marker => id.Contains(marker, StringComparison.OrdinalIgnoreCase));
var local = ids.Where(id => !id.StartsWith("cloud-", StringComparison.Ordinal)).ToList();
model = local.FirstOrDefault(id => !IsReasoning(id)) ?? local.FirstOrDefault() ?? ids[0];
Console.Error.WriteLine($"[using model {model}; set AISERVER_MODEL to override]");
}
// ── 2. Streaming chat completion ────────────────────────────────────────────────
var payload = JsonSerializer.Serialize(new
{
model,
messages = new[] { new { role = "user", content = prompt } },
stream = true,
});
// HttpCompletionOption.ResponseHeadersRead is what makes this actually stream —
// without it HttpClient buffers the entire body before returning.
using var response = await SendAsync(HttpMethod.Post, "/chat/completions", payload,
HttpCompletionOption.ResponseHeadersRead);
if (response.Headers.TryGetValues("X-AISuite-Backend", out var backend))
Console.Error.WriteLine($"[served by {string.Join(',', backend)}]");
// Server-sent events: each line is "data: {json}", terminated by "data: [DONE]".
using var stream = await response.Content.ReadAsStreamAsync();
using var reader = new StreamReader(stream);
while (await reader.ReadLineAsync() is { } line)
{
if (!line.StartsWith("data:", StringComparison.Ordinal)) continue;
var chunk = line[5..].Trim();
if (chunk == "[DONE]") break;
try
{
using var frame = JsonDocument.Parse(chunk);
var delta = frame.RootElement.GetProperty("choices")[0].GetProperty("delta");
if (delta.TryGetProperty("content", out var content) && content.ValueKind == JsonValueKind.String)
Console.Write(content.GetString());
}
catch (JsonException) { /* ignore keep-alive / partial frames */ }
}
Console.WriteLine();
return 0;
// One HTTP call, with the retry/error handling every client needs.
async Task<HttpResponseMessage> SendAsync(HttpMethod method, string path, string? body,
HttpCompletionOption completion = HttpCompletionOption.ResponseContentRead)
{
const int maxRetries = 3;
for (var attempt = 0; attempt < maxRetries; attempt++)
{
using var request = new HttpRequestMessage(method, baseUrl + path);
if (body is not null) request.Content = new StringContent(body, Encoding.UTF8, "application/json");
HttpResponseMessage response;
try { response = await http.SendAsync(request, completion); }
catch (HttpRequestException e)
{
Environment.Exit(Fail($"Could not reach {baseUrl}{path}: {e.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."));
throw;
}
if (response.IsSuccessStatusCode) return response;
switch (response.StatusCode)
{
case HttpStatusCode.Unauthorized:
Environment.Exit(Fail("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."));
break;
// Generic (non-AI-Suite) clients are metered on the free tier. That's a deliberate
// cap rather than a transient error, so stop instead of hammering the server.
case HttpStatusCode.TooManyRequests when response.Headers.Contains("X-AISuite-Upgrade"):
Environment.Exit(Fail("Free-tier limit reached for generic API clients (429).\n" +
" Upgrade the server to Pro to lift it, or try again later."));
break;
case HttpStatusCode.TooManyRequests:
case HttpStatusCode.ServiceUnavailable:
{
var wait = response.Headers.RetryAfter?.Delta ?? TimeSpan.FromSeconds(5);
Console.Error.WriteLine($"[warn] {(int)response.StatusCode}; retrying in {wait.TotalSeconds:0}s");
response.Dispose();
await Task.Delay(wait);
continue;
}
default:
Environment.Exit(Fail($"HTTP {(int)response.StatusCode} from {baseUrl}{path}\n" +
await response.Content.ReadAsStringAsync()));
break;
}
}
Environment.Exit(Fail($"Giving up after {maxRetries} attempts."));
throw new InvalidOperationException("unreachable");
}
static int Fail(string message)
{
Console.Error.WriteLine($"[error] {message}");
return 1;
}