mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-11-04 15:07:21 +00:00
Added tests which guard the JSON of providers, the runtime, and services before source generation
This commit is contained in:
31 files changed
+576
-92
No files matched your search
+17
-13
@@ -215,31 +215,35 @@ public sealed class QdrantEdgeClientImplementation(
|
|||||||
return client;
|
return client;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReSharper disable NotAccessedPositionalProperty.Local
|
//
|
||||||
private sealed record EnsureVectorStoreRequest(string StoreName, string DataSourceName, int VectorSize);
|
// The requests are internal rather than private, so that the JSON context of the runtime can name them. It
|
||||||
|
// generates the contracts they get written with, and a context sees no private type of another class.
|
||||||
|
//
|
||||||
|
// ReSharper disable NotAccessedPositionalProperty.Global
|
||||||
|
internal sealed record EnsureVectorStoreRequest(string StoreName, string DataSourceName, int VectorSize);
|
||||||
|
|
||||||
private sealed record CheckVectorStoreRequest(string StoreName);
|
internal sealed record CheckVectorStoreRequest(string StoreName);
|
||||||
|
|
||||||
private sealed record InsertEmbeddingRequest(string StoreName, IReadOnlyList<VectorStoragePoint> Points);
|
internal sealed record InsertEmbeddingRequest(string StoreName, IReadOnlyList<VectorStoragePoint> Points);
|
||||||
|
|
||||||
/// <param name="PointIds">The only points the search may return, or null for the whole store.</param>
|
/// <param name="PointIds">The only points the search may return, or null for the whole store.</param>
|
||||||
private sealed record SearchEmbeddingRequest(string StoreName, IReadOnlyList<float> Vector, int MaxMatches, IReadOnlyList<string>? PointIds, IReadOnlyList<HiddenChunks> HiddenChunks);
|
internal sealed record SearchEmbeddingRequest(string StoreName, IReadOnlyList<float> Vector, int MaxMatches, IReadOnlyList<string>? PointIds, IReadOnlyList<HiddenChunks> HiddenChunks);
|
||||||
|
|
||||||
private sealed record RetrieveVectorsRequest(string StoreName, IReadOnlyList<string> PointIds);
|
internal sealed record RetrieveVectorsRequest(string StoreName, IReadOnlyList<string> PointIds);
|
||||||
|
|
||||||
private sealed record DeleteEmbeddingByFileRequest(string StoreName, string FilePath, string ParentFileId, IReadOnlyCollection<string> KeepPointIds);
|
internal sealed record DeleteEmbeddingByFileRequest(string StoreName, string FilePath, string ParentFileId, IReadOnlyCollection<string> KeepPointIds);
|
||||||
|
|
||||||
private sealed record RelocateEmbeddingFileRequest(string StoreName, string ParentFileId, string AbsolutePath, string FileName, string RelativePath, string Fingerprint);
|
internal sealed record RelocateEmbeddingFileRequest(string StoreName, string ParentFileId, string AbsolutePath, string FileName, string RelativePath, string Fingerprint);
|
||||||
|
|
||||||
private sealed record OptimizeVectorStoreRequest(string StoreName);
|
internal sealed record OptimizeVectorStoreRequest(string StoreName);
|
||||||
|
|
||||||
private sealed record DeleteVectorStoreRequest(string StoreName);
|
internal sealed record DeleteVectorStoreRequest(string StoreName);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Preparing a backup needs nothing but the request itself.
|
/// Preparing a backup needs nothing but the request itself.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private sealed record PrepareBackupRequest;
|
internal sealed record PrepareBackupRequest;
|
||||||
|
|
||||||
private sealed record CopyStoreForBackupRequest(string BackupId, string StoreName);
|
internal sealed record CopyStoreForBackupRequest(string BackupId, string StoreName);
|
||||||
// ReSharper restore NotAccessedPositionalProperty.Local
|
// ReSharper restore NotAccessedPositionalProperty.Global
|
||||||
}
|
}
|
||||||
@@ -9,7 +9,7 @@ public abstract class ERIClientBase(IERIDataSource dataSource) : IDisposable
|
|||||||
{
|
{
|
||||||
protected readonly IERIDataSource DataSource = dataSource;
|
protected readonly IERIDataSource DataSource = dataSource;
|
||||||
|
|
||||||
protected static readonly JsonSerializerOptions JSON_OPTIONS = new()
|
internal static readonly JsonSerializerOptions JSON_OPTIONS = new()
|
||||||
{
|
{
|
||||||
WriteIndented = true,
|
WriteIndented = true,
|
||||||
AllowTrailingCommas = true,
|
AllowTrailingCommas = true,
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ public sealed partial class RustService
|
|||||||
private async Task<RequestedSecret> GetAPIKeyByKey(string secretKey, bool isTrying)
|
private async Task<RequestedSecret> GetAPIKeyByKey(string secretKey, bool isTrying)
|
||||||
{
|
{
|
||||||
var secretRequest = new SelectSecretRequest(secretKey, Environment.UserName, isTrying);
|
var secretRequest = new SelectSecretRequest(secretKey, Environment.UserName, isTrying);
|
||||||
var result = await this.http.PostAsJsonAsync("/secrets/get", secretRequest, this.jsonRustSerializerOptions);
|
var result = await this.http.PostAsJsonAsync("/secrets/get", secretRequest, JSON_OPTIONS);
|
||||||
if (!result.IsSuccessStatusCode)
|
if (!result.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
if(!isTrying)
|
if(!isTrying)
|
||||||
@@ -71,7 +71,7 @@ public sealed partial class RustService
|
|||||||
return new RequestedSecret(false, new EncryptedText(string.Empty), TB("Failed to get the API key due to an API issue."));
|
return new RequestedSecret(false, new EncryptedText(string.Empty), TB("Failed to get the API key due to an API issue."));
|
||||||
}
|
}
|
||||||
|
|
||||||
var secret = await result.Content.ReadFromJsonAsync<RequestedSecret>(this.jsonRustSerializerOptions);
|
var secret = await result.Content.ReadFromJsonAsync<RequestedSecret>(JSON_OPTIONS);
|
||||||
if (secret.Success)
|
if (secret.Success)
|
||||||
this.logger!.LogDebug($"Successfully retrieved the API key for '{secretKey}'.");
|
this.logger!.LogDebug($"Successfully retrieved the API key for '{secretKey}'.");
|
||||||
else if (isTrying)
|
else if (isTrying)
|
||||||
@@ -106,14 +106,14 @@ public sealed partial class RustService
|
|||||||
private async Task<StoreSecretResponse> StoreEncryptedAPIKeyByKey(string secretKey, EncryptedText encryptedKey)
|
private async Task<StoreSecretResponse> StoreEncryptedAPIKeyByKey(string secretKey, EncryptedText encryptedKey)
|
||||||
{
|
{
|
||||||
var request = new StoreSecretRequest(secretKey, Environment.UserName, encryptedKey);
|
var request = new StoreSecretRequest(secretKey, Environment.UserName, encryptedKey);
|
||||||
var result = await this.http.PostAsJsonAsync("/secrets/store", request, this.jsonRustSerializerOptions);
|
var result = await this.http.PostAsJsonAsync("/secrets/store", request, JSON_OPTIONS);
|
||||||
if (!result.IsSuccessStatusCode)
|
if (!result.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
this.logger!.LogError($"Failed to store the API key for '{secretKey}' due to an API issue: '{result.StatusCode}'");
|
this.logger!.LogError($"Failed to store the API key for '{secretKey}' due to an API issue: '{result.StatusCode}'");
|
||||||
return new StoreSecretResponse(false, TB("Failed to store the API key due to an API issue."));
|
return new StoreSecretResponse(false, TB("Failed to store the API key due to an API issue."));
|
||||||
}
|
}
|
||||||
|
|
||||||
var state = await result.Content.ReadFromJsonAsync<StoreSecretResponse>(this.jsonRustSerializerOptions);
|
var state = await result.Content.ReadFromJsonAsync<StoreSecretResponse>(JSON_OPTIONS);
|
||||||
if (!state.Success)
|
if (!state.Success)
|
||||||
this.logger!.LogError($"Failed to store the API key for '{secretKey}': '{state.Issue}'");
|
this.logger!.LogError($"Failed to store the API key for '{secretKey}': '{state.Issue}'");
|
||||||
else
|
else
|
||||||
@@ -149,14 +149,14 @@ public sealed partial class RustService
|
|||||||
private async Task<DeleteSecretResponse> DeleteAPIKeyByKey(string secretKey, bool isTrying = false)
|
private async Task<DeleteSecretResponse> DeleteAPIKeyByKey(string secretKey, bool isTrying = false)
|
||||||
{
|
{
|
||||||
var request = new SelectSecretRequest(secretKey, Environment.UserName, false);
|
var request = new SelectSecretRequest(secretKey, Environment.UserName, false);
|
||||||
var result = await this.http.PostAsJsonAsync("/secrets/delete", request, this.jsonRustSerializerOptions);
|
var result = await this.http.PostAsJsonAsync("/secrets/delete", request, JSON_OPTIONS);
|
||||||
if (!result.IsSuccessStatusCode)
|
if (!result.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
this.logger!.LogError($"Failed to delete the API key for '{secretKey}' due to an API issue: '{result.StatusCode}'");
|
this.logger!.LogError($"Failed to delete the API key for '{secretKey}' due to an API issue: '{result.StatusCode}'");
|
||||||
return new DeleteSecretResponse{Success = false, WasEntryFound = false, Issue = TB("Failed to delete the API key due to an API issue.")};
|
return new DeleteSecretResponse{Success = false, WasEntryFound = false, Issue = TB("Failed to delete the API key due to an API issue.")};
|
||||||
}
|
}
|
||||||
|
|
||||||
var state = await result.Content.ReadFromJsonAsync<DeleteSecretResponse>(this.jsonRustSerializerOptions);
|
var state = await result.Content.ReadFromJsonAsync<DeleteSecretResponse>(JSON_OPTIONS);
|
||||||
if (!state.Success && !isTrying)
|
if (!state.Success && !isTrying)
|
||||||
this.logger!.LogError($"Failed to delete the API key for '{secretKey}': '{state.Issue}'");
|
this.logger!.LogError($"Failed to delete the API key for '{secretKey}': '{state.Issue}'");
|
||||||
|
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ public sealed partial class RustService
|
|||||||
|
|
||||||
public async Task<RuntimeInfoResponse> GetRuntimeInfo()
|
public async Task<RuntimeInfoResponse> GetRuntimeInfo()
|
||||||
{
|
{
|
||||||
var response = await this.http.GetFromJsonAsync<RuntimeInfoResponse>("/system/runtime/info", this.jsonRustSerializerOptions);
|
var response = await this.http.GetFromJsonAsync<RuntimeInfoResponse>("/system/runtime/info", JSON_OPTIONS);
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,7 +140,7 @@ public sealed partial class RustService
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = await response.Content.ReadFromJsonAsync<AppExitResponse>(this.jsonRustSerializerOptions);
|
var result = await response.Content.ReadFromJsonAsync<AppExitResponse>(JSON_OPTIONS);
|
||||||
if (result is null || !result.Success)
|
if (result is null || !result.Success)
|
||||||
{
|
{
|
||||||
this.logger?.LogError("Failed to exit the app: {Error}", result?.ErrorMessage ?? "Unknown error");
|
this.logger?.LogError("Failed to exit the app: {Error}", result?.ErrorMessage ?? "Unknown error");
|
||||||
@@ -177,7 +177,7 @@ public sealed partial class RustService
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = await response.Content.ReadFromJsonAsync<AppRestartResponse>(this.jsonRustSerializerOptions);
|
var result = await response.Content.ReadFromJsonAsync<AppRestartResponse>(JSON_OPTIONS);
|
||||||
if (result is null || !result.Success)
|
if (result is null || !result.Success)
|
||||||
{
|
{
|
||||||
this.logger?.LogError("Failed to restart the app: {Error}", result?.ErrorMessage ?? "Unknown error");
|
this.logger?.LogError("Failed to restart the app: {Error}", result?.ErrorMessage ?? "Unknown error");
|
||||||
@@ -203,7 +203,7 @@ public sealed partial class RustService
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var response = await this.http.PostAsJsonAsync("/app/interface-size", request, this.jsonRustSerializerOptions);
|
var response = await this.http.PostAsJsonAsync("/app/interface-size", request, JSON_OPTIONS);
|
||||||
var result = await this.ReadInterfaceSizeResponse(response, "set the interface size");
|
var result = await this.ReadInterfaceSizeResponse(response, "set the interface size");
|
||||||
if (result is not null)
|
if (result is not null)
|
||||||
this.logger?.LogInformation("Set the interface sizes of {ScreenCount} screens. AI Studio shows with the size '{Size}' at {CurrentPercent} % and a zoom of {Zoom:0.###} on the screen '{ScreenKey}'.", request.Screens.Count, result.Size, result.CurrentPercent, result.Zoom, result.ScreenKey);
|
this.logger?.LogInformation("Set the interface sizes of {ScreenCount} screens. AI Studio shows with the size '{Size}' at {CurrentPercent} % and a zoom of {Zoom:0.###} on the screen '{ScreenKey}'.", request.Screens.Count, result.Size, result.CurrentPercent, result.Zoom, result.ScreenKey);
|
||||||
@@ -243,7 +243,7 @@ public sealed partial class RustService
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = await response.Content.ReadFromJsonAsync<InterfaceSizeResponse>(this.jsonRustSerializerOptions);
|
var result = await response.Content.ReadFromJsonAsync<InterfaceSizeResponse>(JSON_OPTIONS);
|
||||||
if (result is null || !result.Success)
|
if (result is null || !result.Success)
|
||||||
{
|
{
|
||||||
this.logger?.LogError("Failed to {Action}: {Error}", action, result?.ErrorMessage ?? "Unknown error");
|
this.logger?.LogError("Failed to {Action}: {Error}", action, result?.ErrorMessage ?? "Unknown error");
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ public sealed partial class RustService
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var state = await response.Content.ReadFromJsonAsync<SetClipboardResponse>(this.jsonRustSerializerOptions);
|
var state = await response.Content.ReadFromJsonAsync<SetClipboardResponse>(JSON_OPTIONS);
|
||||||
if (!state.Success)
|
if (!state.Success)
|
||||||
{
|
{
|
||||||
this.logger!.LogError("Failed to copy the text to the clipboard.");
|
this.logger!.LogError("Failed to copy the text to the clipboard.");
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ public sealed partial class RustService
|
|||||||
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||||
cts.CancelAfter(TimeSpan.FromSeconds(45));
|
cts.CancelAfter(TimeSpan.FromSeconds(45));
|
||||||
|
|
||||||
var databaseInfo = await this.http.GetFromJsonAsync<TDatabaseInfo>(infoPath, this.jsonRustSerializerOptions, cts.Token);
|
var databaseInfo = await this.http.GetFromJsonAsync<TDatabaseInfo>(infoPath, JSON_OPTIONS, cts.Token);
|
||||||
return databaseInfo ?? unavailableFactory("The database information response was empty.");
|
return databaseInfo ?? unavailableFactory("The database information response was empty.");
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||||
@@ -52,10 +52,10 @@ public sealed partial class RustService
|
|||||||
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||||
cts.CancelAfter(TimeSpan.FromMinutes(5));
|
cts.CancelAfter(TimeSpan.FromMinutes(5));
|
||||||
|
|
||||||
using var response = await this.http.PostAsJsonAsync(path, request, this.jsonRustSerializerOptions, cts.Token);
|
using var response = await this.http.PostAsJsonAsync(path, request, JSON_OPTIONS, cts.Token);
|
||||||
response.EnsureSuccessStatusCode();
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
var operation = await response.Content.ReadFromJsonAsync<DatabaseOperationResponse>(this.jsonRustSerializerOptions, cts.Token);
|
var operation = await response.Content.ReadFromJsonAsync<DatabaseOperationResponse>(JSON_OPTIONS, cts.Token);
|
||||||
if (operation is not { Success: true })
|
if (operation is not { Success: true })
|
||||||
throw CreateDatabaseException(operation?.Issue, operation?.IssueCode, $"The {databaseName} operation failed.");
|
throw CreateDatabaseException(operation?.Issue, operation?.IssueCode, $"The {databaseName} operation failed.");
|
||||||
}
|
}
|
||||||
@@ -65,10 +65,10 @@ public sealed partial class RustService
|
|||||||
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||||
cts.CancelAfter(TimeSpan.FromMinutes(5));
|
cts.CancelAfter(TimeSpan.FromMinutes(5));
|
||||||
|
|
||||||
using var response = await this.http.PostAsJsonAsync(path, request, this.jsonRustSerializerOptions, cts.Token);
|
using var response = await this.http.PostAsJsonAsync(path, request, JSON_OPTIONS, cts.Token);
|
||||||
response.EnsureSuccessStatusCode();
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
var operation = await response.Content.ReadFromJsonAsync<DatabaseQueryResponse<TResult>>(this.jsonRustSerializerOptions, cts.Token);
|
var operation = await response.Content.ReadFromJsonAsync<DatabaseQueryResponse<TResult>>(JSON_OPTIONS, cts.Token);
|
||||||
if (operation is not { Success: true })
|
if (operation is not { Success: true })
|
||||||
throw CreateDatabaseException(operation?.Issue, operation?.IssueCode, $"The {databaseName} query failed.");
|
throw CreateDatabaseException(operation?.Issue, operation?.IssueCode, $"The {databaseName} query failed.");
|
||||||
|
|
||||||
@@ -94,7 +94,10 @@ public sealed partial class RustService
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed record DatabaseOperationResponse(bool Success, string Issue, string IssueCode);
|
//
|
||||||
|
// Internal rather than private, so that the JSON context of the runtime can name them:
|
||||||
|
//
|
||||||
|
internal sealed record DatabaseOperationResponse(bool Success, string Issue, string IssueCode);
|
||||||
|
|
||||||
private sealed record DatabaseQueryResponse<TResult>(bool Success, string Issue, string IssueCode, TResult? Data);
|
internal sealed record DatabaseQueryResponse<TResult>(bool Success, string Issue, string IssueCode, TResult? Data);
|
||||||
}
|
}
|
||||||
@@ -39,7 +39,7 @@ public sealed partial class RustService
|
|||||||
throw new HttpRequestException($"Failed to query the enterprise configurations: '{result.StatusCode}'");
|
throw new HttpRequestException($"Failed to query the enterprise configurations: '{result.StatusCode}'");
|
||||||
}
|
}
|
||||||
|
|
||||||
var configs = await result.Content.ReadFromJsonAsync<List<EnterpriseConfig>>(this.jsonRustSerializerOptions);
|
var configs = await result.Content.ReadFromJsonAsync<List<EnterpriseConfig>>(JSON_OPTIONS);
|
||||||
if (configs is null)
|
if (configs is null)
|
||||||
throw new InvalidOperationException("Failed to parse the enterprise configurations from Rust.");
|
throw new InvalidOperationException("Failed to parse the enterprise configurations from Rust.");
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ public partial class RustService
|
|||||||
continue;
|
continue;
|
||||||
|
|
||||||
// Deserialize the Tauri event:
|
// Deserialize the Tauri event:
|
||||||
var tauriEvent = JsonSerializer.Deserialize<TauriEvent>(line, this.jsonRustSerializerOptions);
|
var tauriEvent = JsonSerializer.Deserialize<TauriEvent>(line, JSON_OPTIONS);
|
||||||
|
|
||||||
// Forward relevant events to the message bus:
|
// Forward relevant events to the message bus:
|
||||||
if (tauriEvent != default && tauriEvent.EventType
|
if (tauriEvent != default && tauriEvent.EventType
|
||||||
|
|||||||
@@ -16,10 +16,10 @@ public sealed partial class RustService
|
|||||||
var startDirectory = initialDirectory is null ? null : PathTools.FindNearestExistingPath(initialDirectory);
|
var startDirectory = initialDirectory is null ? null : PathTools.FindNearestExistingPath(initialDirectory);
|
||||||
var result = startDirectory is null
|
var result = startDirectory is null
|
||||||
? await this.http.PostAsync($"/select/directory?title={encodedTitle}", null)
|
? await this.http.PostAsync($"/select/directory?title={encodedTitle}", null)
|
||||||
: await this.http.PostAsJsonAsync($"/select/directory?title={encodedTitle}", new PreviousDirectory(startDirectory), this.jsonRustSerializerOptions);
|
: await this.http.PostAsJsonAsync($"/select/directory?title={encodedTitle}", new PreviousDirectory(startDirectory), JSON_OPTIONS);
|
||||||
|
|
||||||
if (result.IsSuccessStatusCode)
|
if (result.IsSuccessStatusCode)
|
||||||
return await result.Content.ReadFromJsonAsync<DirectorySelectionResponse>(this.jsonRustSerializerOptions);
|
return await result.Content.ReadFromJsonAsync<DirectorySelectionResponse>(JSON_OPTIONS);
|
||||||
|
|
||||||
this.logger!.LogError("Failed to select a directory: '{StatusCode}'", result.StatusCode);
|
this.logger!.LogError("Failed to select a directory: '{StatusCode}'", result.StatusCode);
|
||||||
return new DirectorySelectionResponse(true, string.Empty);
|
return new DirectorySelectionResponse(true, string.Empty);
|
||||||
@@ -63,9 +63,9 @@ public sealed partial class RustService
|
|||||||
Filter = FileTypes.ToFileDialogFilter(filter)
|
Filter = FileTypes.ToFileDialogFilter(filter)
|
||||||
};
|
};
|
||||||
|
|
||||||
var result = await this.http.PostAsJsonAsync("/select/file", payload, this.jsonRustSerializerOptions);
|
var result = await this.http.PostAsJsonAsync("/select/file", payload, JSON_OPTIONS);
|
||||||
if (result.IsSuccessStatusCode)
|
if (result.IsSuccessStatusCode)
|
||||||
return await result.Content.ReadFromJsonAsync<FileSelectionResponse>(this.jsonRustSerializerOptions);
|
return await result.Content.ReadFromJsonAsync<FileSelectionResponse>(JSON_OPTIONS);
|
||||||
|
|
||||||
this.logger!.LogError("Failed to select a file: '{StatusCode}'", result.StatusCode);
|
this.logger!.LogError("Failed to select a file: '{StatusCode}'", result.StatusCode);
|
||||||
return new FileSelectionResponse(true, string.Empty);
|
return new FileSelectionResponse(true, string.Empty);
|
||||||
@@ -87,9 +87,9 @@ public sealed partial class RustService
|
|||||||
Filter = FileTypes.ToFileDialogFilter(filter)
|
Filter = FileTypes.ToFileDialogFilter(filter)
|
||||||
};
|
};
|
||||||
|
|
||||||
var result = await this.http.PostAsJsonAsync("/select/files", payload, this.jsonRustSerializerOptions);
|
var result = await this.http.PostAsJsonAsync("/select/files", payload, JSON_OPTIONS);
|
||||||
if (result.IsSuccessStatusCode)
|
if (result.IsSuccessStatusCode)
|
||||||
return await result.Content.ReadFromJsonAsync<FilesSelectionResponse>(this.jsonRustSerializerOptions);
|
return await result.Content.ReadFromJsonAsync<FilesSelectionResponse>(JSON_OPTIONS);
|
||||||
|
|
||||||
this.logger!.LogError("Failed to select files: '{StatusCode}'", result.StatusCode);
|
this.logger!.LogError("Failed to select files: '{StatusCode}'", result.StatusCode);
|
||||||
return new FilesSelectionResponse(true, Array.Empty<string>());
|
return new FilesSelectionResponse(true, Array.Empty<string>());
|
||||||
@@ -118,9 +118,9 @@ public sealed partial class RustService
|
|||||||
Filter = FileTypes.ToFileDialogFilter(filter)
|
Filter = FileTypes.ToFileDialogFilter(filter)
|
||||||
};
|
};
|
||||||
|
|
||||||
var result = await this.http.PostAsJsonAsync("/save/file", payload, this.jsonRustSerializerOptions);
|
var result = await this.http.PostAsJsonAsync("/save/file", payload, JSON_OPTIONS);
|
||||||
if (result.IsSuccessStatusCode)
|
if (result.IsSuccessStatusCode)
|
||||||
return await result.Content.ReadFromJsonAsync<FileSaveResponse>(this.jsonRustSerializerOptions);
|
return await result.Content.ReadFromJsonAsync<FileSaveResponse>(JSON_OPTIONS);
|
||||||
|
|
||||||
this.logger!.LogError("Failed to select a file for writing operation: '{StatusCode}'", result.StatusCode);
|
this.logger!.LogError("Failed to select a file for writing operation: '{StatusCode}'", result.StatusCode);
|
||||||
return new FileSaveResponse(true, string.Empty);
|
return new FileSaveResponse(true, string.Empty);
|
||||||
@@ -133,7 +133,7 @@ public sealed partial class RustService
|
|||||||
HttpResponseMessage result;
|
HttpResponseMessage result;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
result = await this.http.PostAsJsonAsync("/open/path", new OpenPathRequest(path), this.jsonRustSerializerOptions);
|
result = await this.http.PostAsJsonAsync("/open/path", new OpenPathRequest(path), JSON_OPTIONS);
|
||||||
}
|
}
|
||||||
catch (HttpRequestException e)
|
catch (HttpRequestException e)
|
||||||
{
|
{
|
||||||
@@ -154,7 +154,7 @@ public sealed partial class RustService
|
|||||||
return new OpenPathResponse(false, string.Format(TB("The runtime file manager endpoint returned '{0}'."), result.StatusCode));
|
return new OpenPathResponse(false, string.Format(TB("The runtime file manager endpoint returned '{0}'."), result.StatusCode));
|
||||||
}
|
}
|
||||||
|
|
||||||
var response = await result.Content.ReadFromJsonAsync<OpenPathResponse>(this.jsonRustSerializerOptions);
|
var response = await result.Content.ReadFromJsonAsync<OpenPathResponse>(JSON_OPTIONS);
|
||||||
var normalizedResponse = response.Success
|
var normalizedResponse = response.Success
|
||||||
? response
|
? response
|
||||||
: new OpenPathResponse(false, string.IsNullOrWhiteSpace(response.Issue) ? TB("The runtime file manager endpoint failed without details.") : response.Issue);
|
: new OpenPathResponse(false, string.IsNullOrWhiteSpace(response.Issue) ? TB("The runtime file manager endpoint failed without details.") : response.Issue);
|
||||||
@@ -187,7 +187,7 @@ public sealed partial class RustService
|
|||||||
HttpResponseMessage result;
|
HttpResponseMessage result;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
result = await this.http.PostAsJsonAsync("/open/document", new OpenDocumentRequest(path, pageNumber), this.jsonRustSerializerOptions);
|
result = await this.http.PostAsJsonAsync("/open/document", new OpenDocumentRequest(path, pageNumber), JSON_OPTIONS);
|
||||||
}
|
}
|
||||||
catch (HttpRequestException e)
|
catch (HttpRequestException e)
|
||||||
{
|
{
|
||||||
@@ -208,7 +208,7 @@ public sealed partial class RustService
|
|||||||
return new OpenDocumentResponse(false, false, string.Format(TB("The runtime document endpoint returned '{0}'."), result.StatusCode));
|
return new OpenDocumentResponse(false, false, string.Format(TB("The runtime document endpoint returned '{0}'."), result.StatusCode));
|
||||||
}
|
}
|
||||||
|
|
||||||
var response = await result.Content.ReadFromJsonAsync<OpenDocumentResponse>(this.jsonRustSerializerOptions);
|
var response = await result.Content.ReadFromJsonAsync<OpenDocumentResponse>(JSON_OPTIONS);
|
||||||
if (response.Success)
|
if (response.Success)
|
||||||
{
|
{
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -20,10 +20,10 @@ public sealed partial class RustService
|
|||||||
bool optimize,
|
bool optimize,
|
||||||
CancellationToken token = default)
|
CancellationToken token = default)
|
||||||
{
|
{
|
||||||
using var response = await this.http.PostAsJsonAsync("/image/prepare", new { path, optimize }, this.jsonRustSerializerOptions, token);
|
using var response = await this.http.PostAsJsonAsync("/image/prepare", new { path, optimize }, JSON_OPTIONS, token);
|
||||||
response.EnsureSuccessStatusCode();
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
return await response.Content.ReadFromJsonAsync<ImagePrepareResponse>(this.jsonRustSerializerOptions, token)
|
return await response.Content.ReadFromJsonAsync<ImagePrepareResponse>(JSON_OPTIONS, token)
|
||||||
?? throw new InvalidDataException("The Rust image preparation returned an empty response.");
|
?? throw new InvalidDataException("The Rust image preparation returned an empty response.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,10 +44,10 @@ public sealed partial class RustService
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var response = await this.http.PostAsJsonAsync("/image/prepare-for-model", new { path, maxBytes }, this.jsonRustSerializerOptions, token);
|
using var response = await this.http.PostAsJsonAsync("/image/prepare-for-model", new { path, maxBytes }, JSON_OPTIONS, token);
|
||||||
response.EnsureSuccessStatusCode();
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
return await response.Content.ReadFromJsonAsync<PreparedModelImage>(this.jsonRustSerializerOptions, token)
|
return await response.Content.ReadFromJsonAsync<PreparedModelImage>(JSON_OPTIONS, token)
|
||||||
?? PreparedModelImage.Failed(ModelImageErrorCode.UNKNOWN);
|
?? PreparedModelImage.Failed(ModelImageErrorCode.UNKNOWN);
|
||||||
}
|
}
|
||||||
catch (Exception e) when (e is not OperationCanceledException)
|
catch (Exception e) when (e is not OperationCanceledException)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ public sealed partial class RustService
|
|||||||
/// <returns>The paths of the log files.</returns>
|
/// <returns>The paths of the log files.</returns>
|
||||||
public async Task<GetLogPathsResponse> GetLogPaths()
|
public async Task<GetLogPathsResponse> GetLogPaths()
|
||||||
{
|
{
|
||||||
return await this.http.GetFromJsonAsync<GetLogPathsResponse>("/log/paths", this.jsonRustSerializerOptions);
|
return await this.http.GetFromJsonAsync<GetLogPathsResponse>("/log/paths", JSON_OPTIONS);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -33,7 +33,7 @@ public sealed partial class RustService
|
|||||||
// feed itself. The unobserved task exception handler in Program.cs remains the safety net here.
|
// feed itself. The unobserved task exception handler in Program.cs remains the safety net here.
|
||||||
//
|
//
|
||||||
var request = new LogEventRequest(timestamp, level, category, message, exception, stackTrace);
|
var request = new LogEventRequest(timestamp, level, category, message, exception, stackTrace);
|
||||||
_ = this.http.PostAsJsonAsync("/log/event", request, this.jsonRustSerializerOptions);
|
_ = this.http.PostAsJsonAsync("/log/event", request, JSON_OPTIONS);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -18,11 +18,11 @@ public partial class RustService
|
|||||||
using var response = await this.http.PostAsJsonAsync(
|
using var response = await this.http.PostAsJsonAsync(
|
||||||
"/media/jobs",
|
"/media/jobs",
|
||||||
new CreateMediaJobRequest(inputPath, outputPath, OpusBitrateBps: opusBitrateBps),
|
new CreateMediaJobRequest(inputPath, outputPath, OpusBitrateBps: opusBitrateBps),
|
||||||
this.jsonRustSerializerOptions,
|
JSON_OPTIONS,
|
||||||
token);
|
token);
|
||||||
|
|
||||||
response.EnsureSuccessStatusCode();
|
response.EnsureSuccessStatusCode();
|
||||||
var result = await response.Content.ReadFromJsonAsync<CreateMediaJobResponse>(this.jsonRustSerializerOptions, token);
|
var result = await response.Content.ReadFromJsonAsync<CreateMediaJobResponse>(JSON_OPTIONS, token);
|
||||||
return result?.JobId ?? throw new InvalidOperationException("The Rust runtime did not return a media job ID.");
|
return result?.JobId ?? throw new InvalidOperationException("The Rust runtime did not return a media job ID.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,7 +49,7 @@ public partial class RustService
|
|||||||
continue;
|
continue;
|
||||||
|
|
||||||
var json = line["data:".Length..].Trim();
|
var json = line["data:".Length..].Trim();
|
||||||
var mediaEvent = JsonSerializer.Deserialize<MediaJobEvent>(json, this.jsonRustSerializerOptions);
|
var mediaEvent = JsonSerializer.Deserialize<MediaJobEvent>(json, JSON_OPTIONS);
|
||||||
if (mediaEvent is not null)
|
if (mediaEvent is not null)
|
||||||
yield return mediaEvent;
|
yield return mediaEvent;
|
||||||
|
|
||||||
|
|||||||
@@ -65,14 +65,14 @@ public sealed partial class RustService
|
|||||||
var secretKey = SecretKey(secretId, storeType);
|
var secretKey = SecretKey(secretId, storeType);
|
||||||
var encryptedSecret = await this.encryptor!.Encrypt(secretData);
|
var encryptedSecret = await this.encryptor!.Encrypt(secretData);
|
||||||
var request = new StoreSecretRequest(secretKey, Environment.UserName, encryptedSecret);
|
var request = new StoreSecretRequest(secretKey, Environment.UserName, encryptedSecret);
|
||||||
var result = await this.http.PostAsJsonAsync("/secrets/store", request, this.jsonRustSerializerOptions);
|
var result = await this.http.PostAsJsonAsync("/secrets/store", request, JSON_OPTIONS);
|
||||||
if (!result.IsSuccessStatusCode)
|
if (!result.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
this.logger!.LogError($"Failed to store the secret data for '{secretKey}' due to an API issue: '{result.StatusCode}'");
|
this.logger!.LogError($"Failed to store the secret data for '{secretKey}' due to an API issue: '{result.StatusCode}'");
|
||||||
return new StoreSecretResponse(false, TB("Failed to store the secret data due to an API issue."));
|
return new StoreSecretResponse(false, TB("Failed to store the secret data due to an API issue."));
|
||||||
}
|
}
|
||||||
|
|
||||||
var state = await result.Content.ReadFromJsonAsync<StoreSecretResponse>(this.jsonRustSerializerOptions);
|
var state = await result.Content.ReadFromJsonAsync<StoreSecretResponse>(JSON_OPTIONS);
|
||||||
if (!state.Success)
|
if (!state.Success)
|
||||||
this.logger!.LogError($"Failed to store the secret data for '{secretKey}': '{state.Issue}'");
|
this.logger!.LogError($"Failed to store the secret data for '{secretKey}': '{state.Issue}'");
|
||||||
|
|
||||||
@@ -104,7 +104,7 @@ public sealed partial class RustService
|
|||||||
private async Task<RequestedSecret> GetSecretByKey(string secretKey, bool isTrying)
|
private async Task<RequestedSecret> GetSecretByKey(string secretKey, bool isTrying)
|
||||||
{
|
{
|
||||||
var secretRequest = new SelectSecretRequest(secretKey, Environment.UserName, isTrying);
|
var secretRequest = new SelectSecretRequest(secretKey, Environment.UserName, isTrying);
|
||||||
var result = await this.http.PostAsJsonAsync("/secrets/get", secretRequest, this.jsonRustSerializerOptions);
|
var result = await this.http.PostAsJsonAsync("/secrets/get", secretRequest, JSON_OPTIONS);
|
||||||
if (!result.IsSuccessStatusCode)
|
if (!result.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
if(isTrying)
|
if(isTrying)
|
||||||
@@ -114,7 +114,7 @@ public sealed partial class RustService
|
|||||||
return new RequestedSecret(false, new EncryptedText(string.Empty), TB("Failed to get the secret data due to an API issue."));
|
return new RequestedSecret(false, new EncryptedText(string.Empty), TB("Failed to get the secret data due to an API issue."));
|
||||||
}
|
}
|
||||||
|
|
||||||
var state = await result.Content.ReadFromJsonAsync<RequestedSecret>(this.jsonRustSerializerOptions);
|
var state = await result.Content.ReadFromJsonAsync<RequestedSecret>(JSON_OPTIONS);
|
||||||
if (!state.Success)
|
if (!state.Success)
|
||||||
{
|
{
|
||||||
//
|
//
|
||||||
@@ -138,14 +138,14 @@ public sealed partial class RustService
|
|||||||
private async Task<DeleteSecretResponse> DeleteSecretByKey(string secretKey)
|
private async Task<DeleteSecretResponse> DeleteSecretByKey(string secretKey)
|
||||||
{
|
{
|
||||||
var request = new SelectSecretRequest(secretKey, Environment.UserName, false);
|
var request = new SelectSecretRequest(secretKey, Environment.UserName, false);
|
||||||
var result = await this.http.PostAsJsonAsync("/secrets/delete", request, this.jsonRustSerializerOptions);
|
var result = await this.http.PostAsJsonAsync("/secrets/delete", request, JSON_OPTIONS);
|
||||||
if (!result.IsSuccessStatusCode)
|
if (!result.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
this.logger!.LogError($"Failed to delete the secret data for '{secretKey}' due to an API issue: '{result.StatusCode}'");
|
this.logger!.LogError($"Failed to delete the secret data for '{secretKey}' due to an API issue: '{result.StatusCode}'");
|
||||||
return new DeleteSecretResponse{Success = false, WasEntryFound = false, Issue = TB("Failed to delete the secret data due to an API issue.")};
|
return new DeleteSecretResponse{Success = false, WasEntryFound = false, Issue = TB("Failed to delete the secret data due to an API issue.")};
|
||||||
}
|
}
|
||||||
|
|
||||||
var state = await result.Content.ReadFromJsonAsync<DeleteSecretResponse>(this.jsonRustSerializerOptions);
|
var state = await result.Content.ReadFromJsonAsync<DeleteSecretResponse>(JSON_OPTIONS);
|
||||||
if (!state.Success)
|
if (!state.Success)
|
||||||
this.logger!.LogError($"Failed to delete the secret data for '{secretKey}': '{state.Issue}'");
|
this.logger!.LogError($"Failed to delete the secret data for '{secretKey}': '{state.Issue}'");
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// ReSharper disable NotAccessedPositionalProperty.Local
|
// ReSharper disable NotAccessedPositionalProperty.Global
|
||||||
using AIStudio.Tools.Rust;
|
using AIStudio.Tools.Rust;
|
||||||
|
|
||||||
namespace AIStudio.Tools.Services;
|
namespace AIStudio.Tools.Services;
|
||||||
@@ -14,14 +14,14 @@ public sealed partial class RustService
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var response = await this.http.PostAsJsonAsync("/share/file", new ShareFileRequest(filePath), this.jsonRustSerializerOptions);
|
using var response = await this.http.PostAsJsonAsync("/share/file", new ShareFileRequest(filePath), JSON_OPTIONS);
|
||||||
if (!response.IsSuccessStatusCode)
|
if (!response.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
this.logger?.LogError($"The Rust runtime rejected the share request: {response.StatusCode}.");
|
this.logger?.LogError($"The Rust runtime rejected the share request: {response.StatusCode}.");
|
||||||
return new(ShareSheetOutcome.FAILED, $"The Rust runtime rejected the share request: {response.StatusCode}.");
|
return new(ShareSheetOutcome.FAILED, $"The Rust runtime rejected the share request: {response.StatusCode}.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = await response.Content.ReadFromJsonAsync<ShareFileResponse>(this.jsonRustSerializerOptions);
|
var result = await response.Content.ReadFromJsonAsync<ShareFileResponse>(JSON_OPTIONS);
|
||||||
if (result is null)
|
if (result is null)
|
||||||
{
|
{
|
||||||
this.logger?.LogError("The Rust runtime answered the share request without a result.");
|
this.logger?.LogError("The Rust runtime answered the share request without a result.");
|
||||||
@@ -50,5 +50,8 @@ public sealed partial class RustService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed record ShareFileRequest(string FilePath);
|
/// <remarks>
|
||||||
|
/// Internal rather than private, so that the JSON context of the runtime can name it.
|
||||||
|
/// </remarks>
|
||||||
|
internal sealed record ShareFileRequest(string FilePath);
|
||||||
}
|
}
|
||||||
@@ -18,7 +18,7 @@ public sealed partial class RustService
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var request = new RegisterShortcutRequest(shortcutId, shortcut, description, reconfigure);
|
var request = new RegisterShortcutRequest(shortcutId, shortcut, description, reconfigure);
|
||||||
var response = await this.http.PostAsJsonAsync("/shortcuts/register", request, this.jsonRustSerializerOptions);
|
var response = await this.http.PostAsJsonAsync("/shortcuts/register", request, JSON_OPTIONS);
|
||||||
|
|
||||||
if (!response.IsSuccessStatusCode)
|
if (!response.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
@@ -26,7 +26,7 @@ public sealed partial class RustService
|
|||||||
return ShortcutRegistrationResult.Failed(TB("The global shortcut could not be registered because the desktop service is unavailable."));
|
return ShortcutRegistrationResult.Failed(TB("The global shortcut could not be registered because the desktop service is unavailable."));
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = await response.Content.ReadFromJsonAsync<ShortcutRegistrationResult>(this.jsonRustSerializerOptions);
|
var result = await response.Content.ReadFromJsonAsync<ShortcutRegistrationResult>(JSON_OPTIONS);
|
||||||
if (result is null || !result.Success)
|
if (result is null || !result.Success)
|
||||||
{
|
{
|
||||||
this.logger?.LogError("Failed to register global shortcut '{ShortcutId}': {Error}", shortcutId, result?.ErrorMessage ?? "Unknown error");
|
this.logger?.LogError("Failed to register global shortcut '{ShortcutId}': {Error}", shortcutId, result?.ErrorMessage ?? "Unknown error");
|
||||||
@@ -53,7 +53,7 @@ public sealed partial class RustService
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var request = new ValidateShortcutRequest(shortcut);
|
var request = new ValidateShortcutRequest(shortcut);
|
||||||
var response = await this.http.PostAsJsonAsync("/shortcuts/validate", request, this.jsonRustSerializerOptions);
|
var response = await this.http.PostAsJsonAsync("/shortcuts/validate", request, JSON_OPTIONS);
|
||||||
|
|
||||||
if (!response.IsSuccessStatusCode)
|
if (!response.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
@@ -61,7 +61,7 @@ public sealed partial class RustService
|
|||||||
return new ShortcutValidationResult(false, "Network error during validation", false, string.Empty);
|
return new ShortcutValidationResult(false, "Network error during validation", false, string.Empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = await response.Content.ReadFromJsonAsync<ShortcutValidationResponse>(this.jsonRustSerializerOptions);
|
var result = await response.Content.ReadFromJsonAsync<ShortcutValidationResponse>(JSON_OPTIONS);
|
||||||
if (result is null)
|
if (result is null)
|
||||||
return new ShortcutValidationResult(false, "Invalid response from server", false, string.Empty);
|
return new ShortcutValidationResult(false, "Invalid response from server", false, string.Empty);
|
||||||
|
|
||||||
@@ -91,7 +91,7 @@ public sealed partial class RustService
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = await response.Content.ReadFromJsonAsync<ShortcutResponse>(this.jsonRustSerializerOptions);
|
var result = await response.Content.ReadFromJsonAsync<ShortcutResponse>(JSON_OPTIONS);
|
||||||
if (result is null || !result.Success)
|
if (result is null || !result.Success)
|
||||||
{
|
{
|
||||||
this.logger?.LogError("Failed to suspend shortcut processing: {Error}", result?.ErrorMessage ?? "Unknown error");
|
this.logger?.LogError("Failed to suspend shortcut processing: {Error}", result?.ErrorMessage ?? "Unknown error");
|
||||||
@@ -123,7 +123,7 @@ public sealed partial class RustService
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = await response.Content.ReadFromJsonAsync<ShortcutResponse>(this.jsonRustSerializerOptions);
|
var result = await response.Content.ReadFromJsonAsync<ShortcutResponse>(JSON_OPTIONS);
|
||||||
if (result is null || !result.Success)
|
if (result is null || !result.Success)
|
||||||
{
|
{
|
||||||
this.logger?.LogError("Failed to resume shortcut processing: {Error}", result?.ErrorMessage ?? "Unknown error");
|
this.logger?.LogError("Failed to resume shortcut processing: {Error}", result?.ErrorMessage ?? "Unknown error");
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ public sealed partial class RustService : ITokenizerStorage
|
|||||||
{
|
{
|
||||||
var result = await this.http.PostAsJsonAsync("/tokenizer/validate", new {
|
var result = await this.http.PostAsJsonAsync("/tokenizer/validate", new {
|
||||||
file_path = filePath,
|
file_path = filePath,
|
||||||
}, this.jsonRustSerializerOptions);
|
}, JSON_OPTIONS);
|
||||||
|
|
||||||
if (!result.IsSuccessStatusCode)
|
if (!result.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
@@ -25,7 +25,7 @@ public sealed partial class RustService : ITokenizerStorage
|
|||||||
return CreateUnavailableTokenizerResponse("An error occured while sending the path to the Rust framework for validation: "+result.StatusCode);
|
return CreateUnavailableTokenizerResponse("An error occured while sending the path to the Rust framework for validation: "+result.StatusCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
var response = await result.Content.ReadFromJsonAsync<TokenizerResponse>(this.jsonRustSerializerOptions);
|
var response = await result.Content.ReadFromJsonAsync<TokenizerResponse>(JSON_OPTIONS);
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
@@ -36,7 +36,7 @@ public sealed partial class RustService : ITokenizerStorage
|
|||||||
var result = await this.http.PostAsJsonAsync("/tokenizer/store", new {
|
var result = await this.http.PostAsJsonAsync("/tokenizer/store", new {
|
||||||
model_id = modelId,
|
model_id = modelId,
|
||||||
file_path = filePath,
|
file_path = filePath,
|
||||||
}, this.jsonRustSerializerOptions);
|
}, JSON_OPTIONS);
|
||||||
|
|
||||||
if (!result.IsSuccessStatusCode)
|
if (!result.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
@@ -44,7 +44,7 @@ public sealed partial class RustService : ITokenizerStorage
|
|||||||
return CreateUnavailableTokenizerResponse("An error occured while sending the path to the Rust framework for storing: "+result.StatusCode);
|
return CreateUnavailableTokenizerResponse("An error occured while sending the path to the Rust framework for storing: "+result.StatusCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
return await result.Content.ReadFromJsonAsync<TokenizerResponse>(this.jsonRustSerializerOptions);
|
return await result.Content.ReadFromJsonAsync<TokenizerResponse>(JSON_OPTIONS);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<TokenizerResponse> DeleteTokenizer(string modelId)
|
public async Task<TokenizerResponse> DeleteTokenizer(string modelId)
|
||||||
@@ -52,7 +52,7 @@ public sealed partial class RustService : ITokenizerStorage
|
|||||||
this.logger!.LogInformation($"Deleting tokenizer for model '{modelId}'");
|
this.logger!.LogInformation($"Deleting tokenizer for model '{modelId}'");
|
||||||
var result = await this.http.PostAsJsonAsync("/tokenizer/delete", new {
|
var result = await this.http.PostAsJsonAsync("/tokenizer/delete", new {
|
||||||
model_id = modelId,
|
model_id = modelId,
|
||||||
}, this.jsonRustSerializerOptions);
|
}, JSON_OPTIONS);
|
||||||
|
|
||||||
if (!result.IsSuccessStatusCode)
|
if (!result.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
@@ -60,7 +60,7 @@ public sealed partial class RustService : ITokenizerStorage
|
|||||||
return CreateUnavailableTokenizerResponse("An error occured while sending the tokenizer delete request to the Rust framework: "+result.StatusCode);
|
return CreateUnavailableTokenizerResponse("An error occured while sending the tokenizer delete request to the Rust framework: "+result.StatusCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
return await result.Content.ReadFromJsonAsync<TokenizerResponse>(this.jsonRustSerializerOptions);
|
return await result.Content.ReadFromJsonAsync<TokenizerResponse>(JSON_OPTIONS);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<TokenizerResponse?> GetTokenCount(AIStudio.Settings.Provider provider, string text, CancellationToken cancellationToken = default) =>
|
public Task<TokenizerResponse?> GetTokenCount(AIStudio.Settings.Provider provider, string text, CancellationToken cancellationToken = default) =>
|
||||||
@@ -74,7 +74,7 @@ public sealed partial class RustService : ITokenizerStorage
|
|||||||
var result = await this.http.PostAsJsonAsync("/tokenizer/count", new {
|
var result = await this.http.PostAsJsonAsync("/tokenizer/count", new {
|
||||||
text,
|
text,
|
||||||
tokenizer_path = tokenizerPath,
|
tokenizer_path = tokenizerPath,
|
||||||
}, this.jsonRustSerializerOptions, cancellationToken);
|
}, JSON_OPTIONS, cancellationToken);
|
||||||
|
|
||||||
if (!result.IsSuccessStatusCode)
|
if (!result.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
@@ -82,6 +82,6 @@ public sealed partial class RustService : ITokenizerStorage
|
|||||||
return CreateUnavailableTokenizerResponse("Error while getting token count from Rust service: "+result.StatusCode);
|
return CreateUnavailableTokenizerResponse("Error while getting token count from Rust service: "+result.StatusCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
return await result.Content.ReadFromJsonAsync<TokenizerResponse>(this.jsonRustSerializerOptions, cancellationToken);
|
return await result.Content.ReadFromJsonAsync<TokenizerResponse>(JSON_OPTIONS, cancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -9,7 +9,7 @@ public sealed partial class RustService
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(45));
|
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(45));
|
||||||
var response = await this.http.GetFromJsonAsync<UpdateResponse>("/updates/check", this.jsonRustSerializerOptions, cts.Token);
|
var response = await this.http.GetFromJsonAsync<UpdateResponse>("/updates/check", JSON_OPTIONS, cts.Token);
|
||||||
|
|
||||||
if (response == default)
|
if (response == default)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -34,7 +34,10 @@ public sealed partial class RustService : BackgroundService
|
|||||||
private readonly SemaphoreSlim userLanguageLock = new(1, 1);
|
private readonly SemaphoreSlim userLanguageLock = new(1, 1);
|
||||||
private readonly SemaphoreSlim userNameLock = new(1, 1);
|
private readonly SemaphoreSlim userNameLock = new(1, 1);
|
||||||
|
|
||||||
private readonly JsonSerializerOptions jsonRustSerializerOptions = new()
|
/// <summary>
|
||||||
|
/// The JSON options of nearly every call to the runtime, which names its fields in snake case.
|
||||||
|
/// </summary>
|
||||||
|
internal static readonly JsonSerializerOptions JSON_OPTIONS = new()
|
||||||
{
|
{
|
||||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||||
Converters =
|
Converters =
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
{"model":"messages-model","messages":[{"content":"What is the weather?","role":"user"},{"content":[{"type":"text","text":"Compare these pictures."},{"type":"image","source":{"type":"base64","media_type":"image/png","data":"iVBORw0KGgo="}},{"type":"image","source":{"type":"url","url":"https://example.org/picture.png"}}],"role":"user"},{"content":[{"type":"text","text":"Let me search."},{"type":"tool_use","id":"toolu_1","name":"web_search","input":{"query":"weather"}}],"role":"assistant"},{"content":[{"type":"tool_result","tool_use_id":"toolu_1","content":"It is sunny.","is_error":true}],"role":"user"}],"max_tokens":1024,"stream":true,"system":"You answer briefly.","tools":[{"name":"web_search","description":"Search the web.","strict":true,"input_schema":{"type":"object","properties":{"query":{"type":"string","description":"The search query."},"page":{"type":"integer","description":"The page, starting at 1."}},"required":["query"],"additionalProperties":false}}],"temperature":0.5,"top_k":40,"seed":12345678901,"stop":["END",7],"reasoning":{"effort":"low","summary":""},"logprobs":false}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"model":"chat-model","messages":[{"content":"You answer briefly.","role":"system"},{"content":[{"type":"text","text":"What do the pictures show?"},{"type":"image_url","image_url":{"url":"data:image/png;base64,iVBORw0KGgo="}},{"type":"image_url","image_url":"https://example.org/picture.png"}],"role":"user"},{"role":"assistant","content":null,"reasoning_content":"The weather needs a search.","tool_calls":[{"id":"call_1","type":"function","function":{"name":"web_search","arguments":"{\u0022query\u0022:\u0022weather\u0022}"},"index":0}]},{"role":"tool","content":"It is sunny.","tool_call_id":"call_1"}],"stream":true,"tools":[{"type":"function","function":{"name":"web_search","description":"Search the web.","parameters":{"type":"object","properties":{"query":{"type":"string","description":"The search query."},"page":{"type":["integer","null"],"description":"The page, starting at 1."}},"required":["query","page"],"additionalProperties":false},"strict":true}}],"parallel_tool_calls":false,"stream_options":{"include_usage":true},"temperature":0.5,"top_k":40,"seed":12345678901,"stop":["END",7],"reasoning":{"effort":"low","summary":""},"logprobs":false}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"model":"responses-model","input":[{"content":"You answer briefly.","role":"developer"},{"content":[{"type":"input_text","text":"What does the picture show?"},{"type":"input_image","image_url":"data:image/png;base64,iVBORw0KGgo="}],"role":"user"},{"type":"function_call","id":"fc_1","call_id":"call_1","name":"web_search","arguments":"{\u0022query\u0022:\u0022weather\u0022}"},{"type":"function_call_output","call_id":"call_1","output":"It is sunny."}],"stream":true,"store":false,"tools":[{"type":"function","name":"web_search","description":"Search the web.","parameters":{"type":"object","properties":{"query":{"type":"string","description":"The search query."},"page":{"type":["integer","null"],"description":"The page, starting at 1."}},"required":["query","page"],"additionalProperties":false},"strict":true}],"temperature":0.5,"top_k":40,"seed":12345678901,"stop":["END",7],"reasoning":{"effort":"low","summary":""},"logprobs":false}
|
||||||
@@ -12,6 +12,12 @@ namespace AIStudio.Tests.Serialization;
|
|||||||
public sealed class JsonContractTests
|
public sealed class JsonContractTests
|
||||||
{
|
{
|
||||||
[TestCaseSource(typeof(JsonTestRoots), nameof(JsonTestRoots.Stored))]
|
[TestCaseSource(typeof(JsonTestRoots), nameof(JsonTestRoots.Stored))]
|
||||||
|
[TestCaseSource(typeof(JsonTestRoots), nameof(JsonTestRoots.ProviderRequests))]
|
||||||
|
[TestCaseSource(typeof(JsonTestRoots), nameof(JsonTestRoots.ProviderAnswers))]
|
||||||
|
[TestCaseSource(typeof(JsonTestRoots), nameof(JsonTestRoots.RuntimeRequests))]
|
||||||
|
[TestCaseSource(typeof(JsonTestRoots), nameof(JsonTestRoots.RuntimeAnswers))]
|
||||||
|
[TestCaseSource(typeof(JsonTestRoots), nameof(JsonTestRoots.ServiceRequests))]
|
||||||
|
[TestCaseSource(typeof(JsonTestRoots), nameof(JsonTestRoots.ServiceAnswers))]
|
||||||
public void TheContractsMatchThoseOfReflection(JsonTestRoot root)
|
public void TheContractsMatchThoseOfReflection(JsonTestRoot root)
|
||||||
{
|
{
|
||||||
var baseline = JsonReflectionBaseline.Of(root.Options);
|
var baseline = JsonReflectionBaseline.Of(root.Options);
|
||||||
|
|||||||
@@ -7,11 +7,12 @@ namespace AIStudio.Tests.Serialization;
|
|||||||
/// Checks that a property missing in the JSON keeps the value a new object starts with.
|
/// Checks that a property missing in the JSON keeps the value a new object starts with.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Files written by an older version of AI Studio lack every property added since. Reading them has to give
|
/// Files written by an older version of AI Studio lack every property added since, and an answer of a
|
||||||
/// those properties their defaults, as a new object has them, e.g., a sub-object of the settings instead of
|
/// provider, of the runtime, or of a service leaves out whatever it has nothing to say about. Reading them has
|
||||||
/// null. Source generation in .NET 10 breaks this for init-only properties: it sets them all while creating
|
/// to give those properties their defaults, as a new object has them, e.g., a sub-object of the settings
|
||||||
/// the object, and a property missing in the JSON gets the default of its type instead (dotnet/runtime#84484).
|
/// instead of null. Source generation in .NET 10 breaks this for init-only properties: it sets them all while
|
||||||
/// Hence, the types AI Studio stores use set instead of init.<br/><br/>
|
/// creating the object, and a property missing in the JSON gets the default of its type instead
|
||||||
|
/// (dotnet/runtime#84484). Hence, the types AI Studio reads use set instead of init.<br/><br/>
|
||||||
///
|
///
|
||||||
/// A new object is created the way reflection creates it: with its parameterless constructor, or, for a
|
/// A new object is created the way reflection creates it: with its parameterless constructor, or, for a
|
||||||
/// struct, as the default of the struct. Source generation passes the values of a positional record struct
|
/// struct, as the default of the struct. Source generation passes the values of a positional record struct
|
||||||
@@ -21,6 +22,9 @@ namespace AIStudio.Tests.Serialization;
|
|||||||
public sealed class JsonDefaultsTests
|
public sealed class JsonDefaultsTests
|
||||||
{
|
{
|
||||||
[TestCaseSource(typeof(JsonTestRoots), nameof(JsonTestRoots.Stored))]
|
[TestCaseSource(typeof(JsonTestRoots), nameof(JsonTestRoots.Stored))]
|
||||||
|
[TestCaseSource(typeof(JsonTestRoots), nameof(JsonTestRoots.ProviderAnswers))]
|
||||||
|
[TestCaseSource(typeof(JsonTestRoots), nameof(JsonTestRoots.RuntimeAnswers))]
|
||||||
|
[TestCaseSource(typeof(JsonTestRoots), nameof(JsonTestRoots.ServiceAnswers))]
|
||||||
public void AnEmptyObjectGetsTheDefaultsOfANewOne(JsonTestRoot root)
|
public void AnEmptyObjectGetsTheDefaultsOfANewOne(JsonTestRoot root)
|
||||||
{
|
{
|
||||||
var baseline = JsonReflectionBaseline.Of(root.Options);
|
var baseline = JsonReflectionBaseline.Of(root.Options);
|
||||||
@@ -60,7 +64,14 @@ public sealed class JsonDefaultsTests
|
|||||||
Assert.That(failures, Is.Empty);
|
Assert.That(failures, Is.Empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string ValueAsJson(JsonPropertyInfo property, object instance, JsonSerializerOptions options) => JsonSerializer.Serialize(property.Get!(instance), property.PropertyType, options);
|
private static string ValueAsJson(JsonPropertyInfo property, object instance, JsonSerializerOptions options) => AsJson(property.Get!(instance), property.PropertyType, options);
|
||||||
|
|
||||||
private static string DefaultAsJson(Type type, JsonSerializerOptions options) => type.IsValueType ? JsonSerializer.Serialize(Activator.CreateInstance(type), type, options) : "null";
|
private static string DefaultAsJson(Type type, JsonSerializerOptions options) => type.IsValueType ? AsJson(Activator.CreateInstance(type), type, options) : "null";
|
||||||
|
|
||||||
|
private static string AsJson(object? value, Type type, JsonSerializerOptions options) => value switch
|
||||||
|
{
|
||||||
|
// The default of a JsonElement holds no JSON at all, so it cannot be written:
|
||||||
|
JsonElement { ValueKind: JsonValueKind.Undefined } => "no JSON",
|
||||||
|
_ => JsonSerializer.Serialize(value, type, options),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
@@ -85,5 +85,5 @@ public sealed class JsonGoldenFileTests
|
|||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Read from the source tree, the way the other fixtures are, so the file needs no entry in the project file.
|
/// Read from the source tree, the way the other fixtures are, so the file needs no entry in the project file.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
private static string ReadFixture(string fileName, [CallerFilePath] string sourceFilePath = "") => File.ReadAllText(Path.Combine(Path.GetDirectoryName(sourceFilePath)!, "Fixtures", fileName));
|
internal static string ReadFixture(string fileName, [CallerFilePath] string sourceFilePath = "") => File.ReadAllText(Path.Combine(Path.GetDirectoryName(sourceFilePath)!, "Fixtures", fileName));
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace AIStudio.Tests.Serialization;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A request in the fixtures, together with how AI Studio builds it and the options it writes it with.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="FileName">The name of the file in the folder Fixtures next to the tests, which holds the request as it got sent before.</param>
|
||||||
|
/// <param name="Create">Builds the request the way AI Studio does.</param>
|
||||||
|
/// <param name="Options">The options AI Studio writes the request with.</param>
|
||||||
|
public sealed record JsonGoldenRequest(string FileName, Func<object> Create, JsonSerializerOptions Options)
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override string ToString() => this.FileName;
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
using AIStudio.Provider;
|
||||||
|
|
||||||
|
namespace AIStudio.Tests.Serialization;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Writes requests the way AI Studio sends them, and checks that they get sent just as before.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The files in the folder Fixtures were written with the contracts of reflection, before AI Studio got
|
||||||
|
/// contracts of its own. A provider reads every byte of a request, and one which got written otherwise, e.g.,
|
||||||
|
/// with a field under another name or without the tools, gets refused or misread. The requests are made up,
|
||||||
|
/// see <see cref="JsonSampleRequests"/>.
|
||||||
|
/// </remarks>
|
||||||
|
[TestFixture]
|
||||||
|
public sealed class JsonGoldenRequestTests
|
||||||
|
{
|
||||||
|
private static IEnumerable<JsonGoldenRequest> GoldenRequests()
|
||||||
|
{
|
||||||
|
TestHost.PrepareStaticState();
|
||||||
|
yield return new("chat-completion-request.json", JsonSampleRequests.ChatCompletion, ProviderJsonOptions.OPTIONS);
|
||||||
|
yield return new("responses-request.json", JsonSampleRequests.Responses, ProviderJsonOptions.OPTIONS);
|
||||||
|
yield return new("anthropic-messages-request.json", () => JsonSampleRequests.Anthropic(), ProviderJsonOptions.OPTIONS);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCaseSource(nameof(GoldenRequests))]
|
||||||
|
public void ARequestIsSentAsBefore(JsonGoldenRequest goldenRequest)
|
||||||
|
{
|
||||||
|
var request = goldenRequest.Create();
|
||||||
|
Assert.That(JsonSerializer.Serialize(request, request.GetType(), goldenRequest.Options), Is.EqualTo(JsonGoldenFileTests.ReadFixture(goldenRequest.FileName)));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
using AIStudio.Provider;
|
||||||
|
using AIStudio.Provider.Anthropic;
|
||||||
|
using AIStudio.Provider.OpenAI;
|
||||||
|
using AIStudio.Tools.ToolCallingSystem;
|
||||||
|
|
||||||
|
using AnthropicImageUrl = AIStudio.Provider.Anthropic.SubContentImageUrl;
|
||||||
|
using OpenAIImageUrl = AIStudio.Provider.OpenAI.SubContentImageUrl;
|
||||||
|
|
||||||
|
namespace AIStudio.Tests.Serialization;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Requests to LLM providers as AI Studio builds them, with every kind of content they carry.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The values are made up. What counts is that every part of a request holds one, so that its golden file shows
|
||||||
|
/// how each part gets written.
|
||||||
|
/// </remarks>
|
||||||
|
public static class JsonSampleRequests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A tool with one required and one optional argument, the shape the web search has.
|
||||||
|
/// </summary>
|
||||||
|
public static ToolDefinition Tool() => new()
|
||||||
|
{
|
||||||
|
Id = "web_search",
|
||||||
|
Function = new()
|
||||||
|
{
|
||||||
|
Name = "web_search",
|
||||||
|
DescriptionForLLM = "Search the web.",
|
||||||
|
Strict = true,
|
||||||
|
Parameters = ToolParameterSchemaBuilder.Create()
|
||||||
|
.RequiredString("query", "The search query.")
|
||||||
|
.OptionalInteger("page", "The page, starting at 1.")
|
||||||
|
.Build(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A request to a Chat Completions API, with a tool, a call of it, and its result.
|
||||||
|
/// </summary>
|
||||||
|
public static ChatCompletionAPIRequest ChatCompletion() => new("chat-model", Messages(), true)
|
||||||
|
{
|
||||||
|
Tools = [ProviderToolAdapters.ToChatCompletionTool(Tool(), hostEnforcesStrict: true)],
|
||||||
|
ParallelToolCalls = false,
|
||||||
|
AdditionalApiParameters = ApiParameters(),
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A request to the Responses API of OpenAI, with an item of an earlier answer and the result of a tool.
|
||||||
|
/// </summary>
|
||||||
|
public static ResponsesAPIRequest Responses() => new("responses-model",
|
||||||
|
[
|
||||||
|
new TextMessage("You answer briefly.", "developer"),
|
||||||
|
new MultimodalMessage(
|
||||||
|
[
|
||||||
|
new SubContentInputText(SubContentType.INPUT_TEXT, "What does the picture show?"),
|
||||||
|
new SubContentInputImage(SubContentType.INPUT_IMAGE, "data:image/png;base64,iVBORw0KGgo="),
|
||||||
|
], "user"),
|
||||||
|
Parse("""{"type":"function_call","id":"fc_1","call_id":"call_1","name":"web_search","arguments":"{\"query\":\"weather\"}"}"""),
|
||||||
|
new ResponsesFunctionCallOutputItem { CallId = "call_1", Output = "It is sunny." },
|
||||||
|
],
|
||||||
|
true, false, [ProviderToolAdapters.ToResponsesTool(Tool())])
|
||||||
|
{
|
||||||
|
AdditionalApiParameters = ApiParameters(),
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A request to the Messages API of Anthropic, with images from a URL and as data, and a tool round.
|
||||||
|
/// </summary>
|
||||||
|
public static ChatRequest Anthropic() => new("messages-model",
|
||||||
|
[
|
||||||
|
new TextMessage("What is the weather?", "user"),
|
||||||
|
new MultimodalMessage(
|
||||||
|
[
|
||||||
|
new SubContentText(SubContentType.TEXT, "Compare these pictures."),
|
||||||
|
new SubContentImage(SubContentType.IMAGE, new SubContentBase64Image { MediaType = "image/png", Data = "iVBORw0KGgo=" }),
|
||||||
|
new SubContentImage(SubContentType.IMAGE, new AnthropicImageUrl { Url = "https://example.org/picture.png" }),
|
||||||
|
], "user"),
|
||||||
|
new AnthropicMessage(
|
||||||
|
[
|
||||||
|
Parse("""{"type":"text","text":"Let me search."}"""),
|
||||||
|
Parse("""{"type":"tool_use","id":"toolu_1","name":"web_search","input":{"query":"weather"}}"""),
|
||||||
|
]),
|
||||||
|
new AnthropicToolResultMessage([new AnthropicToolResultContent { ToolUseId = "toolu_1", Content = "It is sunny.", IsError = true }]),
|
||||||
|
],
|
||||||
|
1024, true, "You answer briefly.")
|
||||||
|
{
|
||||||
|
Tools = [ProviderToolAdapters.ToAnthropicTool(Tool())],
|
||||||
|
AdditionalApiParameters = ApiParameters(),
|
||||||
|
};
|
||||||
|
|
||||||
|
private static List<IMessageBase> Messages() =>
|
||||||
|
[
|
||||||
|
new TextMessage("You answer briefly.", "system"),
|
||||||
|
new MultimodalMessage(
|
||||||
|
[
|
||||||
|
new SubContentText(SubContentType.TEXT, "What do the pictures show?"),
|
||||||
|
new SubContentImageUrlNested(SubContentType.IMAGE_URL, new SubContentImageUrlData { Url = "data:image/png;base64,iVBORw0KGgo=" }),
|
||||||
|
new OpenAIImageUrl(SubContentType.IMAGE_URL, "https://example.org/picture.png"),
|
||||||
|
], "user"),
|
||||||
|
new AssistantToolCallMessage
|
||||||
|
{
|
||||||
|
Content = Parse("null"),
|
||||||
|
ReasoningContent = "The weather needs a search.",
|
||||||
|
ToolCalls =
|
||||||
|
[
|
||||||
|
new ChatCompletionToolCall
|
||||||
|
{
|
||||||
|
Id = "call_1",
|
||||||
|
Function = new() { Name = "web_search", Arguments = """{"query":"weather"}""" },
|
||||||
|
AdditionalMetadata = { ["index"] = Parse("0") },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
new ToolResultMessage { Content = "It is sunny.", ToolCallId = "call_1" },
|
||||||
|
];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Additional API parameters of every kind, read the way the provider settings read them.
|
||||||
|
/// </summary>
|
||||||
|
private static IDictionary<string, object> ApiParameters()
|
||||||
|
{
|
||||||
|
if (!AdditionalApiParametersParser.TryParse("""
|
||||||
|
"temperature": 0.5,
|
||||||
|
"top_k": 40,
|
||||||
|
"seed": 12345678901,
|
||||||
|
"stop": ["END", 7],
|
||||||
|
"reasoning": { "effort": "low", "summary": null },
|
||||||
|
"logprobs": false
|
||||||
|
""", out var parameters, out var errorMessage))
|
||||||
|
throw new InvalidOperationException(errorMessage);
|
||||||
|
|
||||||
|
return parameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JsonElement Parse(string json)
|
||||||
|
{
|
||||||
|
using var document = JsonDocument.Parse(json);
|
||||||
|
return document.RootElement.Clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
using AIStudio.Provider;
|
||||||
|
using AIStudio.Provider.Anthropic;
|
||||||
|
using AIStudio.Provider.Google;
|
||||||
|
using AIStudio.Provider.Groq;
|
||||||
|
using AIStudio.Provider.HuggingFace;
|
||||||
|
using AIStudio.Provider.OpenAI;
|
||||||
|
using AIStudio.Provider.OpenRouter;
|
||||||
|
using AIStudio.Provider.Requesty;
|
||||||
|
|
||||||
|
using AnthropicResponseStreamLine = AIStudio.Provider.Anthropic.ResponseStreamLine;
|
||||||
|
using FireworksResponseStreamLine = AIStudio.Provider.Fireworks.ResponseStreamLine;
|
||||||
|
using PerplexityResponseStreamLine = AIStudio.Provider.Perplexity.ResponseStreamLine;
|
||||||
|
|
||||||
|
namespace AIStudio.Tests.Serialization;
|
||||||
|
|
||||||
|
public static partial class JsonTestRoots
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// What AI Studio sends to LLM providers.
|
||||||
|
/// </summary>
|
||||||
|
public static IEnumerable<TestCaseData> ProviderRequests()
|
||||||
|
{
|
||||||
|
TestHost.PrepareStaticState();
|
||||||
|
var options = ProviderJsonOptions.OPTIONS;
|
||||||
|
|
||||||
|
yield return CaseWithNamespace(typeof(ChatCompletionAPIRequest), options);
|
||||||
|
yield return CaseWithNamespace(typeof(ResponsesAPIRequest), options);
|
||||||
|
yield return CaseWithNamespace(typeof(ChatRequest), options);
|
||||||
|
|
||||||
|
//
|
||||||
|
// The messages and their parts, which converters write as the type they are of:
|
||||||
|
//
|
||||||
|
foreach (var type in new[] { typeof(IMessageBase), typeof(ISubContent), typeof(ISubContentImageUrl), typeof(ISubContentImageSource) }.SelectMany(ImplementationsOf))
|
||||||
|
yield return CaseWithNamespace(type, options);
|
||||||
|
|
||||||
|
//
|
||||||
|
// What a request holds as an object, which gets written as the type it is of, too. The additional API
|
||||||
|
// parameters hold the values the parser of the provider settings reads, and the input of the Responses API
|
||||||
|
// holds the items of an earlier answer as they came in:
|
||||||
|
//
|
||||||
|
foreach (var type in new[] { typeof(string), typeof(int), typeof(long), typeof(double), typeof(decimal), typeof(bool), typeof(Dictionary<string, object>), typeof(List<object>), typeof(JsonElement) })
|
||||||
|
yield return Case(new(type, options), "value of a request");
|
||||||
|
|
||||||
|
yield return Case(new(ProviderToolAdapters.ToChatCompletionTool(JsonSampleRequests.Tool(), hostEnforcesStrict: true).GetType(), options), "tool of a Chat Completions API");
|
||||||
|
yield return CaseWithNamespace(typeof(ResponsesFunctionTool), options);
|
||||||
|
yield return CaseWithNamespace(typeof(ResponsesFunctionCallOutputItem), options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// What AI Studio receives from LLM providers.
|
||||||
|
/// </summary>
|
||||||
|
public static IEnumerable<TestCaseData> ProviderAnswers()
|
||||||
|
{
|
||||||
|
TestHost.PrepareStaticState();
|
||||||
|
var options = ProviderJsonOptions.OPTIONS;
|
||||||
|
|
||||||
|
// The lists of models:
|
||||||
|
yield return CaseWithNamespace(typeof(AIStudio.Provider.OpenAI.ModelsResponse), options);
|
||||||
|
yield return CaseWithNamespace(typeof(AIStudio.Provider.Mistral.ModelsResponse), options);
|
||||||
|
yield return CaseWithNamespace(typeof(AIStudio.Provider.SelfHosted.ModelsResponse), options);
|
||||||
|
yield return CaseWithNamespace(typeof(AIStudio.Provider.HuggingFace.ModelsResponse), options);
|
||||||
|
yield return Case(new(typeof(IList<HubModel>), options), "HuggingFace");
|
||||||
|
yield return CaseWithNamespace(typeof(RequestyModelsResponse), options);
|
||||||
|
yield return CaseWithNamespace(typeof(OpenRouterModelsResponse), options);
|
||||||
|
yield return CaseWithNamespace(typeof(GroqModelsResponse), options);
|
||||||
|
|
||||||
|
// The lines of streamed answers:
|
||||||
|
yield return CaseWithNamespace(typeof(ChatCompletionDeltaStreamLine), options);
|
||||||
|
yield return CaseWithNamespace(typeof(ChatCompletionAnnotationStreamLine), options);
|
||||||
|
yield return CaseWithNamespace(typeof(ChatCompletionToolStreamLine), options);
|
||||||
|
yield return CaseWithNamespace(typeof(ResponsesDeltaStreamLine), options);
|
||||||
|
yield return CaseWithNamespace(typeof(ResponsesAnnotationStreamLine), options);
|
||||||
|
yield return CaseWithNamespace(typeof(ResponsesCompletedStreamLine), options);
|
||||||
|
yield return CaseWithNamespace(typeof(AnthropicResponseStreamLine), options);
|
||||||
|
yield return CaseWithNamespace(typeof(AnthropicStreamLine), options);
|
||||||
|
yield return CaseWithNamespace(typeof(FireworksResponseStreamLine), options);
|
||||||
|
yield return CaseWithNamespace(typeof(PerplexityResponseStreamLine), options);
|
||||||
|
|
||||||
|
//
|
||||||
|
// The annotations, which a converter reads as the type their JSON tells:
|
||||||
|
//
|
||||||
|
foreach (var type in ImplementationsOf(typeof(Annotation)))
|
||||||
|
yield return CaseWithNamespace(type, options);
|
||||||
|
|
||||||
|
// Transcriptions and embeddings, where a converter reads the embeddings of Google as one or as a list:
|
||||||
|
yield return CaseWithNamespace(typeof(TranscriptionResponse), options);
|
||||||
|
yield return CaseWithNamespace(typeof(EmbeddingResponse), options);
|
||||||
|
yield return CaseWithNamespace(typeof(GoogleEmbeddingResponse), options);
|
||||||
|
yield return CaseWithNamespace(typeof(GoogleEmbedding), options);
|
||||||
|
yield return Case(new(typeof(List<GoogleEmbedding>), options), "Google");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
using AIStudio.Tools;
|
||||||
|
using AIStudio.Tools.Databases.VectorStore;
|
||||||
|
using AIStudio.Tools.Rust;
|
||||||
|
using AIStudio.Tools.Services;
|
||||||
|
|
||||||
|
namespace AIStudio.Tests.Serialization;
|
||||||
|
|
||||||
|
public static partial class JsonTestRoots
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// What AI Studio sends to its runtime.
|
||||||
|
/// </summary>
|
||||||
|
public static IEnumerable<TestCaseData> RuntimeRequests()
|
||||||
|
{
|
||||||
|
TestHost.PrepareStaticState();
|
||||||
|
var options = RustService.JSON_OPTIONS;
|
||||||
|
|
||||||
|
yield return Case(new(typeof(SelectSecretRequest), options));
|
||||||
|
yield return Case(new(typeof(StoreSecretRequest), options));
|
||||||
|
yield return Case(new(typeof(RustService.ShareFileRequest), options));
|
||||||
|
yield return Case(new(typeof(PreviousDirectory), options));
|
||||||
|
yield return Case(new(typeof(SelectFileOptions), options));
|
||||||
|
yield return Case(new(typeof(SaveFileOptions), options));
|
||||||
|
yield return Case(new(typeof(OpenPathRequest), options));
|
||||||
|
yield return Case(new(typeof(OpenDocumentRequest), options));
|
||||||
|
yield return Case(new(typeof(RegisterShortcutRequest), options));
|
||||||
|
yield return Case(new(typeof(ValidateShortcutRequest), options));
|
||||||
|
yield return Case(new(typeof(SetInterfaceSizeRequest), options));
|
||||||
|
yield return Case(new(typeof(CreateMediaJobRequest), options));
|
||||||
|
yield return Case(new(typeof(LogEventRequest), options));
|
||||||
|
|
||||||
|
// Every request of the vector store:
|
||||||
|
foreach (var type in typeof(QdrantEdgeClientImplementation).GetNestedTypes(BindingFlags.NonPublic).Where(type => type.Name.EndsWith("Request", StringComparison.Ordinal)).OrderBy(type => type.Name, StringComparer.Ordinal))
|
||||||
|
yield return Case(new(type, options));
|
||||||
|
|
||||||
|
// The prompt injection filter, which gets called with the defaults of HttpClient, i.e., with camel case:
|
||||||
|
yield return Case(new(typeof(SanitizePromptInjectionsRequest), JsonSerializerOptions.Web));
|
||||||
|
yield return Case(new(typeof(SanitizePromptInjectionsBatchRequest), JsonSerializerOptions.Web));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// What AI Studio receives from its runtime.
|
||||||
|
/// </summary>
|
||||||
|
public static IEnumerable<TestCaseData> RuntimeAnswers()
|
||||||
|
{
|
||||||
|
TestHost.PrepareStaticState();
|
||||||
|
var options = RustService.JSON_OPTIONS;
|
||||||
|
|
||||||
|
yield return Case(new(typeof(RequestedSecret), options));
|
||||||
|
yield return Case(new(typeof(StoreSecretResponse), options));
|
||||||
|
yield return Case(new(typeof(DeleteSecretResponse), options));
|
||||||
|
yield return Case(new(typeof(ShareFileResponse), options));
|
||||||
|
yield return Case(new(typeof(ImagePrepareResponse), options));
|
||||||
|
yield return Case(new(typeof(PreparedModelImage), options));
|
||||||
|
yield return Case(new(typeof(TauriEvent), options));
|
||||||
|
yield return Case(new(typeof(UpdateResponse), options));
|
||||||
|
yield return Case(new(typeof(SetClipboardResponse), options));
|
||||||
|
yield return Case(new(typeof(DirectorySelectionResponse), options));
|
||||||
|
yield return Case(new(typeof(FileSelectionResponse), options));
|
||||||
|
yield return Case(new(typeof(FilesSelectionResponse), options));
|
||||||
|
yield return Case(new(typeof(FileSaveResponse), options));
|
||||||
|
yield return Case(new(typeof(OpenPathResponse), options));
|
||||||
|
yield return Case(new(typeof(OpenDocumentResponse), options));
|
||||||
|
yield return Case(new(typeof(ShortcutRegistrationResult), options));
|
||||||
|
yield return Case(new(typeof(ShortcutValidationResponse), options));
|
||||||
|
yield return Case(new(typeof(ShortcutResponse), options));
|
||||||
|
yield return Case(new(typeof(RuntimeInfoResponse), options));
|
||||||
|
yield return Case(new(typeof(AppExitResponse), options));
|
||||||
|
yield return Case(new(typeof(AppRestartResponse), options));
|
||||||
|
yield return Case(new(typeof(InterfaceSizeResponse), options));
|
||||||
|
yield return Case(new(typeof(CreateMediaJobResponse), options));
|
||||||
|
yield return Case(new(typeof(MediaJobEvent), options));
|
||||||
|
yield return Case(new(typeof(List<EnterpriseConfig>), options));
|
||||||
|
yield return Case(new(typeof(GetLogPathsResponse), options));
|
||||||
|
yield return Case(new(typeof(TokenizerResponse), options));
|
||||||
|
|
||||||
|
// The vector store, with the result of every query its client sends:
|
||||||
|
yield return Case(new(typeof(QdrantEdgeInfo), options));
|
||||||
|
yield return Case(new(typeof(RustService.DatabaseOperationResponse), options));
|
||||||
|
yield return Case(new(typeof(RustService.DatabaseQueryResponse<VectorStoreEnsureResult>), options));
|
||||||
|
yield return Case(new(typeof(RustService.DatabaseQueryResponse<bool?>), options));
|
||||||
|
yield return Case(new(typeof(RustService.DatabaseQueryResponse<List<VectorSearchResult>>), options));
|
||||||
|
yield return Case(new(typeof(RustService.DatabaseQueryResponse<List<StoredVector>>), options));
|
||||||
|
yield return Case(new(typeof(RustService.DatabaseQueryResponse<VectorStoreBackupPreparation>), options));
|
||||||
|
yield return Case(new(typeof(RustService.DatabaseQueryResponse<long?>), options));
|
||||||
|
|
||||||
|
//
|
||||||
|
// The events of a file extraction, which get read with the defaults of System.Text.Json. A converter reads
|
||||||
|
// their metadata as the type the JSON tells:
|
||||||
|
//
|
||||||
|
yield return Case(new(typeof(ContentStreamSseEvent), JsonSerializerOptions.Default));
|
||||||
|
yield return Case(new(typeof(string), JsonSerializerOptions.Default), "error of a file extraction");
|
||||||
|
foreach (var type in ImplementationsOf(typeof(ContentStreamSseMetadata)))
|
||||||
|
yield return Case(new(type, JsonSerializerOptions.Default));
|
||||||
|
|
||||||
|
// The prompt injection filter, which gets called with the defaults of HttpClient, i.e., with camel case:
|
||||||
|
yield return Case(new(typeof(SanitizePromptInjectionsResponse), JsonSerializerOptions.Web));
|
||||||
|
yield return Case(new(typeof(SanitizePromptInjectionsBatchResponse), JsonSerializerOptions.Web));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
using AIStudio.Tools.ERIClient;
|
||||||
|
using AIStudio.Tools.ERIClient.DataModel;
|
||||||
|
using AIStudio.Tools.ToolCallingSystem.ToolCallingImplementations.WebSearch;
|
||||||
|
using AIStudio.Tools.ToolCallingSystem.ToolCallingImplementations.WebSearch.Staan;
|
||||||
|
using AIStudio.Tools.ToolCallingSystem.ToolCallingImplementations.WebSearch.Tavily;
|
||||||
|
|
||||||
|
namespace AIStudio.Tests.Serialization;
|
||||||
|
|
||||||
|
public static partial class JsonTestRoots
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// What AI Studio sends to ERI servers and to search services.
|
||||||
|
/// </summary>
|
||||||
|
public static IEnumerable<TestCaseData> ServiceRequests()
|
||||||
|
{
|
||||||
|
TestHost.PrepareStaticState();
|
||||||
|
yield return Case(new(typeof(RetrievalRequest), ERIClientBase.JSON_OPTIONS));
|
||||||
|
yield return Case(new(typeof(StaanSearchRequest), WebSearchJson.OPTIONS));
|
||||||
|
yield return Case(new(typeof(TavilySearchRequest), WebSearchJson.OPTIONS));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// What AI Studio receives from ERI servers and from search services.
|
||||||
|
/// </summary>
|
||||||
|
public static IEnumerable<TestCaseData> ServiceAnswers()
|
||||||
|
{
|
||||||
|
TestHost.PrepareStaticState();
|
||||||
|
yield return Case(new(typeof(List<AuthScheme>), ERIClientBase.JSON_OPTIONS));
|
||||||
|
yield return Case(new(typeof(AuthResponse), ERIClientBase.JSON_OPTIONS));
|
||||||
|
yield return Case(new(typeof(DataSourceInfo), ERIClientBase.JSON_OPTIONS));
|
||||||
|
yield return Case(new(typeof(List<EmbeddingInfo>), ERIClientBase.JSON_OPTIONS));
|
||||||
|
yield return Case(new(typeof(List<RetrievalInfo>), ERIClientBase.JSON_OPTIONS));
|
||||||
|
yield return Case(new(typeof(List<Context>), ERIClientBase.JSON_OPTIONS));
|
||||||
|
yield return Case(new(typeof(SecurityRequirements), ERIClientBase.JSON_OPTIONS));
|
||||||
|
yield return Case(new(typeof(StaanSearchResponse), WebSearchJson.OPTIONS));
|
||||||
|
yield return Case(new(typeof(TavilySearchResponse), WebSearchJson.OPTIONS));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
using AIStudio.Assistants.SWOTAnalysis;
|
using AIStudio.Assistants.SWOTAnalysis;
|
||||||
using AIStudio.Chat;
|
using AIStudio.Chat;
|
||||||
using AIStudio.Settings;
|
using AIStudio.Settings;
|
||||||
@@ -15,9 +17,11 @@ namespace AIStudio.Tests.Serialization;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Only the outermost types belong here: whatever they hold gets checked along with them. Add a type whenever
|
/// Only the outermost types belong here: whatever they hold gets checked along with them. Add a type whenever
|
||||||
/// AI Studio starts to store a new kind of file, with the options it stores the file with.
|
/// AI Studio starts to store a new kind of file or to exchange a new kind of message, with the options it uses
|
||||||
|
/// for them. What AI Studio sends is listed apart from what it receives: only what it reads has to keep the
|
||||||
|
/// defaults of a new object.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public static class JsonTestRoots
|
public static partial class JsonTestRoots
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The data AI Studio keeps on the disk of the user.
|
/// The data AI Studio keeps on the disk of the user.
|
||||||
@@ -67,6 +71,27 @@ public static class JsonTestRoots
|
|||||||
return new TestCaseData(root).SetArgDisplayNames(purpose is null ? root.ToString() : $"{root}, {purpose}");
|
return new TestCaseData(root).SetArgDisplayNames(purpose is null ? root.ToString() : $"{root}, {purpose}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Names the case after its type together with the last part of the namespace, which tells apart types of the
|
||||||
|
/// same name, e.g., the lists of models of two providers.
|
||||||
|
/// </summary>
|
||||||
|
private static TestCaseData CaseWithNamespace(Type type, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
var typeNamespace = type.Namespace!;
|
||||||
|
return Case(new(type, options), typeNamespace[(typeNamespace.LastIndexOf('.') + 1)..]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the classes and structs of AI Studio which are the given type.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// A converter which writes a value as the type it is of meets these types without any property naming them.
|
||||||
|
/// Listing them all here keeps a new one from going unchecked.
|
||||||
|
/// </remarks>
|
||||||
|
private static IEnumerable<Type> ImplementationsOf(Type type) => typeof(Program).Assembly.GetTypes()
|
||||||
|
.Where(candidate => candidate is { IsAbstract: false, IsInterface: false, IsGenericTypeDefinition: false } && type.IsAssignableFrom(candidate))
|
||||||
|
.OrderBy(candidate => candidate.FullName, StringComparer.Ordinal);
|
||||||
|
|
||||||
private static IEnumerable<Type> ManagedSettingValueTypes()
|
private static IEnumerable<Type> ManagedSettingValueTypes()
|
||||||
{
|
{
|
||||||
//
|
//
|
||||||
|
|||||||
Reference in new issue
Block a user