Repository navigation
Step 34: ASP.NET server — OpenAI-compatible API endpoints - #85
Conversation
Implement OpenAI-compatible Minimal API server in DotLLM.Server: Endpoints: - POST /v1/chat/completions (streaming SSE + non-streaming) - POST /v1/completions (raw prompt, streaming + non-streaming) - GET /v1/models (list loaded model) - POST /v1/tokenize, POST /v1/detokenize (extension endpoints) - GET /health, GET /ready (probes) Features: - Model loading at startup via --model/--device/--quant CLI args - Chat template formatting with tool calling support - Tool call detection via IToolCallParser, finish_reason: "tool_calls" - response_format support (json_object, json_schema) - Sequential request processing via SemaphoreSlim (Step 35 adds batching) - CORS enabled for browser clients (Step 53 Chat UI) - OpenAI Python SDK compatible Infrastructure: - Request/response DTOs with [JsonPropertyName] for snake_case - RequestConverter: OpenAI DTOs ↔ engine types - ServerState: concurrency control, readiness tracking - ServerOptions: CLI arg parsing Testing: - scripts/test_server.py: 10/10 automated smoke tests - samples/server-api.http: VS Code REST Client test file Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
@gemini please review this PR thoroughly - performance issues and possible improvements, potential bugs, code quality and architecture |
|
Gemini ✦ Overall, this is a solid implementation of the OpenAI API surface. The architecture cleanly separates the DTOs from the engine types, and the SSE streaming implementation is standard. However, there are a few performance improvements that can be made to reduce GC pressure, particularly on the streaming hot path where allocations per token can quickly add up. Performance & Allocations1. Avoid string allocations in SSE streaming string json = JsonSerializer.Serialize(chunk, JsonOptions);
await ctx.Response.WriteAsync($"data: {json}\n\n", ct);This allocates multiple strings per generated token. You can eliminate these allocations by writing directly to the response body stream: private static async Task WriteSseChunk<T>(HttpContext ctx, T chunk, CancellationToken ct)
{
await ctx.Response.WriteAsync("data: ", ct);
await JsonSerializer.SerializeAsync(ctx.Response.Body, chunk, JsonOptions, ct);
await ctx.Response.WriteAsync("\n\n", ct);
await ctx.Response.Body.FlushAsync(ct);
}2. Cache static stop sequences private static List<string> BuildStopSequences(ServerState state)
{
var stops = new List<string>();
foreach (var marker in new[] { "<|im_end|>", "<|eot_id|>", "<|eom_id|>", "<|end|>", "</s>", "</tool_call>" })
stops.Add(marker);
return stops;
}Change this to a 3. Use JSON Source Generators [JsonSerializable(typeof(ChatCompletionRequest))]
[JsonSerializable(typeof(ChatCompletionResponse))]
[JsonSerializable(typeof(ChatCompletionChunk))]
// ...
internal partial class ServerJsonContext : JsonSerializerContext { }Then pass Code Quality
|
Apply Gemini review suggestions: 1. SSE streaming: eliminate per-token string allocations by using JsonSerializer.SerializeAsync directly to response body stream instead of Serialize + string interpolation. 2. Cache static stop sequences: replace per-request List<string> allocation with static readonly string[] array. 3. JSON Source Generators: add ServerJsonContext with [JsonSerializable] for all DTO types. Eliminates reflection overhead, reduces allocations, enables AOT compilation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
All three performance suggestions addressed in c866b38: 1. SSE string allocations — Fixed
2. Cache static stop sequences — FixedReplaced per-request 3. JSON Source Generators — FixedAdded 4. LINQ in Tokenize — SkippedUtility endpoint called rarely. One enumerator + one array is acceptable for readability. 5. Cancellation tokens — Already handled
All 10 server smoke tests still passing. |
Closes #84
Summary
DotLLM.Server:/v1/chat/completions(SSE streaming + non-streaming),/v1/completions,/v1/models,/v1/tokenize,/v1/detokenize, plus/healthand/readyprobes[JsonPropertyName]snake_case, streaming via Server-Sent Events,finish_reason,usagetoken counts, tool call detectionIToolCallParser, returnsfinish_reason: "tool_calls"with structuredtool_callsarrayresponse_formatsupport forjson_objectandjson_schemascripts/test_server.py(10/10 automated smoke tests),samples/server-api.http(VS Code REST Client)Technical Details
The server loads a single model at startup via
--model/--device/--quantCLI args (same pattern asRunCommand/ChatCommand). Model, tokenizer, chat template, andTextGeneratorare registered as DI singletons.Sequential request processing via
SemaphoreSlim(1,1)inServerState— Step 35 (continuous batching) will replace this withIScheduler. CORS is enabled permissively for Step 53 (Chat UI).RequestConverterhandles all DTO ↔ engine type conversions:ChatMessageDto[]→ChatMessage[],ToolDefinitionDto[]→ToolDefinition[],tool_choiceJSON →ToolChoicerecord,response_formatJSON →ResponseFormatrecord.Model path resolution is inlined (local .gguf path or
~/.dotllm/models/cache lookup) to avoid a dependency onDotLLM.Cli'sGgufFileResolver.Key Files
src/DotLLM.Server/Program.cssrc/DotLLM.Server/Endpoints/ChatCompletionEndpoint.cs/v1/chat/completions— streaming SSE + non-streaming, tool call detectionsrc/DotLLM.Server/Endpoints/CompletionEndpoint.cs/v1/completions— raw prompt completionsrc/DotLLM.Server/RequestConverter.cssrc/DotLLM.Server/Models/src/DotLLM.Server/ServerState.cssrc/DotLLM.Server/ServerOptions.csscripts/test_server.pysamples/server-api.httpTest Plan
dotnet builddotnet test tests/DotLLM.Tests.Unit/python scripts/test_server.pyopenai.ChatCompletion.create(base_url=...)🤖 Generated with Claude Code