Initial commit: ClawdDotNet
Import des bestehenden Projektstands in Git. - .NET 10 WinForms Anwendung (Multi-Agent / Tool-System) - .gitignore fuer Build-Artefakte, Secrets und Runtime-Daten ergaenzt Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace ClawdDotNet.Core.Api.Models;
|
||||
|
||||
public sealed class ChatMessage
|
||||
{
|
||||
[JsonPropertyName("role")]
|
||||
public string Role { get; set; } = "";
|
||||
|
||||
[JsonPropertyName("content")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? Content { get; set; }
|
||||
|
||||
[JsonPropertyName("tool_calls")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public List<ToolCall>? ToolCalls { get; set; }
|
||||
|
||||
[JsonPropertyName("tool_call_id")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? ToolCallId { get; set; }
|
||||
|
||||
public static ChatMessage System(string content) => new() { Role = "system", Content = content };
|
||||
public static ChatMessage User(string content) => new() { Role = "user", Content = content };
|
||||
public static ChatMessage Assistant(string content) => new() { Role = "assistant", Content = content };
|
||||
|
||||
public static ChatMessage AssistantWithToolCalls(List<ToolCall> toolCalls) => new()
|
||||
{
|
||||
Role = "assistant",
|
||||
ToolCalls = toolCalls
|
||||
};
|
||||
|
||||
public static ChatMessage ToolResponse(string toolCallId, string content) => new()
|
||||
{
|
||||
Role = "tool",
|
||||
ToolCallId = toolCallId,
|
||||
Content = content
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace ClawdDotNet.Core.Api.Models;
|
||||
|
||||
public sealed class ChatRequest
|
||||
{
|
||||
[JsonPropertyName("model")]
|
||||
public string Model { get; set; } = "";
|
||||
|
||||
[JsonPropertyName("messages")]
|
||||
public List<ChatMessage> Messages { get; set; } = new();
|
||||
|
||||
[JsonPropertyName("tools")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public List<ToolDefinition>? Tools { get; set; }
|
||||
|
||||
[JsonPropertyName("tool_choice")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public string? ToolChoice { get; set; }
|
||||
|
||||
[JsonPropertyName("temperature")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public double? Temperature { get; set; }
|
||||
|
||||
[JsonPropertyName("max_tokens")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public int? MaxTokens { get; set; }
|
||||
|
||||
[JsonPropertyName("stream")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
|
||||
public bool Stream { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace ClawdDotNet.Core.Api.Models;
|
||||
|
||||
public sealed class ChatResponse
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public string Id { get; set; } = "";
|
||||
|
||||
[JsonPropertyName("choices")]
|
||||
public List<Choice> Choices { get; set; } = new();
|
||||
|
||||
[JsonPropertyName("usage")]
|
||||
public Usage? Usage { get; set; }
|
||||
|
||||
[JsonPropertyName("model")]
|
||||
public string? Model { get; set; }
|
||||
|
||||
[JsonPropertyName("error")]
|
||||
public ApiError? Error { get; set; }
|
||||
}
|
||||
|
||||
public sealed class Choice
|
||||
{
|
||||
[JsonPropertyName("index")]
|
||||
public int Index { get; set; }
|
||||
|
||||
[JsonPropertyName("message")]
|
||||
public ChatMessage Message { get; set; } = new();
|
||||
|
||||
[JsonPropertyName("finish_reason")]
|
||||
public string? FinishReason { get; set; }
|
||||
}
|
||||
|
||||
public sealed class Usage
|
||||
{
|
||||
[JsonPropertyName("prompt_tokens")]
|
||||
public int PromptTokens { get; set; }
|
||||
|
||||
[JsonPropertyName("completion_tokens")]
|
||||
public int CompletionTokens { get; set; }
|
||||
|
||||
[JsonPropertyName("total_tokens")]
|
||||
public int TotalTokens { get; set; }
|
||||
}
|
||||
|
||||
public sealed class ApiError
|
||||
{
|
||||
[JsonPropertyName("message")]
|
||||
public string Message { get; set; } = "";
|
||||
|
||||
[JsonPropertyName("code")]
|
||||
public int? Code { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace ClawdDotNet.Core.Api.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Repräsentiert ein verfügbares LLM-Modell von OpenRouter.
|
||||
/// </summary>
|
||||
public sealed class ModelInfo
|
||||
{
|
||||
/// <summary>OpenRouter Modell-ID (z.B. "anthropic/claude-sonnet-4-5")</summary>
|
||||
public string Id { get; set; } = "";
|
||||
|
||||
/// <summary>Anzeigename des Modells</summary>
|
||||
public string Name { get; set; } = "";
|
||||
|
||||
public override string ToString() => Id;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace ClawdDotNet.Core.Api.Models;
|
||||
|
||||
public sealed class ToolCall
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public string Id { get; set; } = "";
|
||||
|
||||
[JsonPropertyName("type")]
|
||||
public string Type { get; set; } = "function";
|
||||
|
||||
[JsonPropertyName("function")]
|
||||
public ToolCallFunction Function { get; set; } = new();
|
||||
}
|
||||
|
||||
public sealed class ToolCallFunction
|
||||
{
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; } = "";
|
||||
|
||||
[JsonPropertyName("arguments")]
|
||||
public string Arguments { get; set; } = "";
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace ClawdDotNet.Core.Api.Models;
|
||||
|
||||
public sealed class ToolDefinition
|
||||
{
|
||||
[JsonPropertyName("type")]
|
||||
public string Type { get; set; } = "function";
|
||||
|
||||
[JsonPropertyName("function")]
|
||||
public FunctionDefinition Function { get; set; } = new();
|
||||
}
|
||||
|
||||
public sealed class FunctionDefinition
|
||||
{
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; } = "";
|
||||
|
||||
[JsonPropertyName("description")]
|
||||
public string Description { get; set; } = "";
|
||||
|
||||
[JsonPropertyName("parameters")]
|
||||
public JsonElement Parameters { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using ClawdDotNet.Core.Api.Models;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace ClawdDotNet.Core.Api;
|
||||
|
||||
public sealed class OpenRouterClient : IDisposable
|
||||
{
|
||||
private const string BaseUrl = "https://openrouter.ai/api/v1/";
|
||||
|
||||
private readonly HttpClient _http;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull
|
||||
};
|
||||
|
||||
public OpenRouterClient(string apiKey, ILogger logger, HttpClient? httpClient = null)
|
||||
{
|
||||
_logger = logger;
|
||||
_http = httpClient ?? new HttpClient();
|
||||
_http.BaseAddress = new Uri(BaseUrl);
|
||||
_http.Timeout = TimeSpan.FromMinutes(5); // LLM-Calls können bei großen Prompts lange dauern
|
||||
_http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
|
||||
_http.DefaultRequestHeaders.Add("HTTP-Referer", "ClawdDotNet");
|
||||
_http.DefaultRequestHeaders.Add("X-Title", "ClawdDotNet");
|
||||
}
|
||||
|
||||
public async Task<ChatResponse> CompleteAsync(ChatRequest request, CancellationToken ct)
|
||||
{
|
||||
request.Stream = false;
|
||||
|
||||
var json = JsonSerializer.Serialize(request, JsonOptions);
|
||||
using var content = new StringContent(json, Encoding.UTF8, "application/json");
|
||||
|
||||
_logger.LogDebug("Sending request to OpenRouter: model={Model}, messages={Count}",
|
||||
request.Model, request.Messages.Count);
|
||||
|
||||
using var response = await _http.PostAsync("chat/completions", content, ct);
|
||||
var responseBody = await response.Content.ReadAsStringAsync(ct);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
_logger.LogError("OpenRouter API error {StatusCode}: {Body}",
|
||||
(int)response.StatusCode, responseBody);
|
||||
throw new OpenRouterException(
|
||||
$"API request failed with status {(int)response.StatusCode}",
|
||||
(int)response.StatusCode,
|
||||
responseBody);
|
||||
}
|
||||
|
||||
var result = JsonSerializer.Deserialize<ChatResponse>(responseBody, JsonOptions)
|
||||
?? throw new OpenRouterException("Empty response from OpenRouter", 0, responseBody);
|
||||
|
||||
if (result.Error is not null)
|
||||
{
|
||||
_logger.LogError("OpenRouter returned error: {Error}", result.Error.Message);
|
||||
throw new OpenRouterException(result.Error.Message, result.Error.Code ?? 0, responseBody);
|
||||
}
|
||||
|
||||
_logger.LogDebug("OpenRouter response received: tokens={Tokens}",
|
||||
result.Usage?.TotalTokens ?? 0);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ruft die verfügbaren Modelle von OpenRouter ab (/models Endpoint).
|
||||
/// Gibt eine Liste von Modell-IDs zurück, sortiert nach Name.
|
||||
/// </summary>
|
||||
public async Task<List<ModelInfo>> GetAvailableModelsAsync(CancellationToken ct = default)
|
||||
{
|
||||
_logger.LogDebug("Fetching available models from OpenRouter...");
|
||||
|
||||
using var response = await _http.GetAsync("models", ct);
|
||||
var body = await response.Content.ReadAsStringAsync(ct);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
_logger.LogError("Failed to fetch models: {StatusCode} {Body}",
|
||||
(int)response.StatusCode, body);
|
||||
return [];
|
||||
}
|
||||
|
||||
var doc = JsonDocument.Parse(body);
|
||||
|
||||
if (!doc.RootElement.TryGetProperty("data", out var dataArray))
|
||||
return [];
|
||||
|
||||
var models = new List<ModelInfo>();
|
||||
|
||||
foreach (var item in dataArray.EnumerateArray())
|
||||
{
|
||||
var id = item.GetProperty("id").GetString() ?? "";
|
||||
var name = item.TryGetProperty("name", out var nameProp) ? nameProp.GetString() ?? id : id;
|
||||
|
||||
models.Add(new ModelInfo { Id = id, Name = name });
|
||||
}
|
||||
|
||||
models.Sort((a, b) => string.Compare(a.Id, b.Id, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
_logger.LogDebug("Fetched {Count} models from OpenRouter", models.Count);
|
||||
return models;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_http.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class OpenRouterException(string message, int statusCode, string responseBody)
|
||||
: Exception(message)
|
||||
{
|
||||
public int StatusCode { get; } = statusCode;
|
||||
public string ResponseBody { get; } = responseBody;
|
||||
}
|
||||
Reference in New Issue
Block a user