Expands SDK support to 8 additional languages/frameworks: - Java SDK with Maven/OkHttp/Jackson - Kotlin SDK with Gradle/Ktor/kotlinx.serialization - Swift SDK with Swift Package Manager/async-await - C SDK with CMake/libcurl - C++ SDK with CMake/Modern C++20 - C# SDK with .NET 8.0/HttpClient - Ruby SDK with Bundler/Faraday - Rust SDK with Cargo/reqwest/tokio All SDKs include: - Tensor operations (matmul, conv2d, attention) - LLM inference with streaming support - Model registry, pricing, and usage APIs - Builder patterns where idiomatic - Full type safety
341 lines
10 KiB
C#
341 lines
10 KiB
C#
using System.Net.Http.Json;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
|
|
namespace SynorCompute;
|
|
|
|
/// <summary>
|
|
/// Synor Compute SDK - C# Client
|
|
///
|
|
/// Access distributed heterogeneous compute resources (CPU, GPU, TPU, NPU, LPU, FPGA, DSP)
|
|
/// for AI/ML workloads at 90% cost reduction compared to traditional cloud.
|
|
/// </summary>
|
|
/// <example>
|
|
/// <code>
|
|
/// // Create client
|
|
/// using var client = new SynorComputeClient("your-api-key");
|
|
///
|
|
/// // Matrix multiplication on GPU
|
|
/// var a = Tensor.Rand(512, 512);
|
|
/// var b = Tensor.Rand(512, 512);
|
|
/// var result = await client.MatMulAsync(a, b, new MatMulOptions
|
|
/// {
|
|
/// Processor = ProcessorType.Gpu,
|
|
/// Precision = Precision.Fp16
|
|
/// });
|
|
///
|
|
/// if (result.IsSuccess)
|
|
/// {
|
|
/// Console.WriteLine($"Time: {result.ExecutionTimeMs}ms");
|
|
/// }
|
|
///
|
|
/// // LLM inference
|
|
/// var response = await client.InferenceAsync("llama-3-70b", "Explain quantum computing");
|
|
/// Console.WriteLine(response.Result);
|
|
///
|
|
/// // Streaming inference
|
|
/// await foreach (var token in client.InferenceStreamAsync("llama-3-70b", "Write a poem"))
|
|
/// {
|
|
/// Console.Write(token);
|
|
/// }
|
|
/// </code>
|
|
/// </example>
|
|
public sealed class SynorComputeClient : IDisposable
|
|
{
|
|
public const string Version = "0.1.0";
|
|
|
|
private readonly SynorConfig _config;
|
|
private readonly HttpClient _httpClient;
|
|
private readonly JsonSerializerOptions _jsonOptions;
|
|
private bool _disposed;
|
|
|
|
public SynorComputeClient(string apiKey)
|
|
: this(new SynorConfig { ApiKey = apiKey })
|
|
{
|
|
}
|
|
|
|
public SynorComputeClient(SynorConfig config)
|
|
{
|
|
_config = config;
|
|
_httpClient = new HttpClient
|
|
{
|
|
BaseAddress = new Uri(config.BaseUrl),
|
|
Timeout = TimeSpan.FromMilliseconds(config.TimeoutMs)
|
|
};
|
|
_httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {config.ApiKey}");
|
|
_httpClient.DefaultRequestHeaders.Add("X-SDK-Version", $"csharp/{Version}");
|
|
|
|
_jsonOptions = new JsonSerializerOptions
|
|
{
|
|
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
|
PropertyNameCaseInsensitive = true
|
|
};
|
|
}
|
|
|
|
// ==================== Matrix Operations ====================
|
|
|
|
public Task<JobResult<Tensor>> MatMulAsync(Tensor a, Tensor b, CancellationToken ct = default)
|
|
=> MatMulAsync(a, b, new MatMulOptions(), ct);
|
|
|
|
public async Task<JobResult<Tensor>> MatMulAsync(
|
|
Tensor a,
|
|
Tensor b,
|
|
MatMulOptions options,
|
|
CancellationToken ct = default)
|
|
{
|
|
CheckDisposed();
|
|
|
|
var body = new
|
|
{
|
|
operation = "matmul",
|
|
a = TensorToDict(a),
|
|
b = TensorToDict(b),
|
|
precision = options.Precision.ToString().ToLower(),
|
|
processor = options.Processor.ToString().ToLower(),
|
|
priority = options.Priority.ToString().ToLower()
|
|
};
|
|
|
|
return await PostAsync<JobResult<Tensor>>("/compute", body, ct);
|
|
}
|
|
|
|
public async Task<JobResult<Tensor>> Conv2dAsync(
|
|
Tensor input,
|
|
Tensor kernel,
|
|
Conv2dOptions? options = null,
|
|
CancellationToken ct = default)
|
|
{
|
|
CheckDisposed();
|
|
options ??= new Conv2dOptions();
|
|
|
|
var body = new
|
|
{
|
|
operation = "conv2d",
|
|
input = TensorToDict(input),
|
|
kernel = TensorToDict(kernel),
|
|
stride = new[] { options.Stride.Item1, options.Stride.Item2 },
|
|
padding = new[] { options.Padding.Item1, options.Padding.Item2 },
|
|
precision = options.Precision.ToString().ToLower()
|
|
};
|
|
|
|
return await PostAsync<JobResult<Tensor>>("/compute", body, ct);
|
|
}
|
|
|
|
public async Task<JobResult<Tensor>> AttentionAsync(
|
|
Tensor query,
|
|
Tensor key,
|
|
Tensor value,
|
|
AttentionOptions? options = null,
|
|
CancellationToken ct = default)
|
|
{
|
|
CheckDisposed();
|
|
options ??= new AttentionOptions();
|
|
|
|
var body = new
|
|
{
|
|
operation = "attention",
|
|
query = TensorToDict(query),
|
|
key = TensorToDict(key),
|
|
value = TensorToDict(value),
|
|
num_heads = options.NumHeads,
|
|
flash = options.Flash,
|
|
precision = options.Precision.ToString().ToLower()
|
|
};
|
|
|
|
return await PostAsync<JobResult<Tensor>>("/compute", body, ct);
|
|
}
|
|
|
|
// ==================== LLM Inference ====================
|
|
|
|
public Task<JobResult<string>> InferenceAsync(string model, string prompt, CancellationToken ct = default)
|
|
=> InferenceAsync(model, prompt, new InferenceOptions(), ct);
|
|
|
|
public async Task<JobResult<string>> InferenceAsync(
|
|
string model,
|
|
string prompt,
|
|
InferenceOptions options,
|
|
CancellationToken ct = default)
|
|
{
|
|
CheckDisposed();
|
|
|
|
var body = new Dictionary<string, object>
|
|
{
|
|
["operation"] = "inference",
|
|
["model"] = model,
|
|
["prompt"] = prompt,
|
|
["max_tokens"] = options.MaxTokens,
|
|
["temperature"] = options.Temperature,
|
|
["top_p"] = options.TopP,
|
|
["top_k"] = options.TopK
|
|
};
|
|
|
|
if (options.Processor.HasValue)
|
|
{
|
|
body["processor"] = options.Processor.Value.ToString().ToLower();
|
|
}
|
|
|
|
return await PostAsync<JobResult<string>>("/inference", body, ct);
|
|
}
|
|
|
|
public async IAsyncEnumerable<string> InferenceStreamAsync(
|
|
string model,
|
|
string prompt,
|
|
InferenceOptions? options = null,
|
|
[EnumeratorCancellation] CancellationToken ct = default)
|
|
{
|
|
CheckDisposed();
|
|
options ??= new InferenceOptions();
|
|
|
|
var body = new Dictionary<string, object>
|
|
{
|
|
["operation"] = "inference",
|
|
["model"] = model,
|
|
["prompt"] = prompt,
|
|
["max_tokens"] = options.MaxTokens,
|
|
["temperature"] = options.Temperature,
|
|
["stream"] = true
|
|
};
|
|
|
|
var request = new HttpRequestMessage(HttpMethod.Post, "/inference/stream")
|
|
{
|
|
Content = JsonContent.Create(body, options: _jsonOptions)
|
|
};
|
|
|
|
using var response = await _httpClient.SendAsync(
|
|
request,
|
|
HttpCompletionOption.ResponseHeadersRead,
|
|
ct);
|
|
|
|
response.EnsureSuccessStatusCode();
|
|
|
|
await using var stream = await response.Content.ReadAsStreamAsync(ct);
|
|
using var reader = new StreamReader(stream);
|
|
|
|
while (!reader.EndOfStream && !ct.IsCancellationRequested)
|
|
{
|
|
var line = await reader.ReadLineAsync(ct);
|
|
if (line == null) break;
|
|
|
|
if (line.StartsWith("data: "))
|
|
{
|
|
var data = line[6..];
|
|
if (data == "[DONE]") yield break;
|
|
|
|
try
|
|
{
|
|
var json = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(data, _jsonOptions);
|
|
if (json?.TryGetValue("token", out var token) == true)
|
|
{
|
|
yield return token.GetString() ?? "";
|
|
}
|
|
}
|
|
catch (JsonException)
|
|
{
|
|
// Skip malformed JSON
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ==================== Model Registry ====================
|
|
|
|
public async Task<List<ModelInfo>> ListModelsAsync(
|
|
ModelCategory? category = null,
|
|
CancellationToken ct = default)
|
|
{
|
|
CheckDisposed();
|
|
|
|
var url = category.HasValue
|
|
? $"/models?category={category.Value.ToString().ToLower()}"
|
|
: "/models";
|
|
|
|
var response = await GetAsync<JsonElement>(url, ct);
|
|
var models = response.GetProperty("models");
|
|
|
|
return models.Deserialize<List<ModelInfo>>(_jsonOptions) ?? new List<ModelInfo>();
|
|
}
|
|
|
|
public async Task<ModelInfo> GetModelAsync(string modelId, CancellationToken ct = default)
|
|
{
|
|
CheckDisposed();
|
|
return await GetAsync<ModelInfo>($"/models/{modelId}", ct);
|
|
}
|
|
|
|
public async Task<List<ModelInfo>> SearchModelsAsync(string query, CancellationToken ct = default)
|
|
{
|
|
CheckDisposed();
|
|
var response = await GetAsync<JsonElement>($"/models/search?q={Uri.EscapeDataString(query)}", ct);
|
|
var models = response.GetProperty("models");
|
|
return models.Deserialize<List<ModelInfo>>(_jsonOptions) ?? new List<ModelInfo>();
|
|
}
|
|
|
|
// ==================== Pricing & Usage ====================
|
|
|
|
public async Task<List<PricingInfo>> GetPricingAsync(CancellationToken ct = default)
|
|
{
|
|
CheckDisposed();
|
|
var response = await GetAsync<JsonElement>("/pricing", ct);
|
|
var pricing = response.GetProperty("pricing");
|
|
return pricing.Deserialize<List<PricingInfo>>(_jsonOptions) ?? new List<PricingInfo>();
|
|
}
|
|
|
|
public async Task<UsageStats> GetUsageAsync(CancellationToken ct = default)
|
|
{
|
|
CheckDisposed();
|
|
return await GetAsync<UsageStats>("/usage", ct);
|
|
}
|
|
|
|
// ==================== Health Check ====================
|
|
|
|
public async Task<bool> HealthCheckAsync(CancellationToken ct = default)
|
|
{
|
|
try
|
|
{
|
|
var response = await GetAsync<JsonElement>("/health", ct);
|
|
return response.GetProperty("status").GetString() == "healthy";
|
|
}
|
|
catch
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// ==================== Internal Methods ====================
|
|
|
|
private async Task<T> GetAsync<T>(string path, CancellationToken ct)
|
|
{
|
|
var response = await _httpClient.GetAsync(path, ct);
|
|
response.EnsureSuccessStatusCode();
|
|
return await response.Content.ReadFromJsonAsync<T>(_jsonOptions, ct)
|
|
?? throw new SynorException("Failed to deserialize response");
|
|
}
|
|
|
|
private async Task<T> PostAsync<T>(string path, object body, CancellationToken ct)
|
|
{
|
|
var response = await _httpClient.PostAsJsonAsync(path, body, _jsonOptions, ct);
|
|
response.EnsureSuccessStatusCode();
|
|
return await response.Content.ReadFromJsonAsync<T>(_jsonOptions, ct)
|
|
?? throw new SynorException("Failed to deserialize response");
|
|
}
|
|
|
|
private static object TensorToDict(Tensor tensor) => new
|
|
{
|
|
shape = tensor.Shape,
|
|
data = tensor.Data,
|
|
dtype = tensor.Dtype.ToString().ToLower()
|
|
};
|
|
|
|
private void CheckDisposed()
|
|
{
|
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (!_disposed)
|
|
{
|
|
_disposed = true;
|
|
_httpClient.Dispose();
|
|
}
|
|
}
|
|
}
|