mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-11-02 23:27:21 +00:00
Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aaabf1f436
|
||
|
|
8fa083ae24
|
||
|
|
9e78f5055e
|
||
|
|
53f358eb41
|
||
|
|
25fcabb549
|
||
|
|
c97b0f0f76
|
||
|
|
bf22cf4a78
|
||
|
|
cc6dffcba9
|
||
|
|
df09c1c753
|
||
|
|
5d80d3894b
|
||
|
|
9e5aee766c
|
No files matched your search
@@ -117,8 +117,8 @@ public sealed class AnthropicContentBlockBuilder
|
|||||||
case TYPE_TEXT:
|
case TYPE_TEXT:
|
||||||
return this.BuildFromSeed(new()
|
return this.BuildFromSeed(new()
|
||||||
{
|
{
|
||||||
["type"] = JsonSerializer.Serialize(TYPE_TEXT),
|
["type"] = JsonSerializer.Serialize(TYPE_TEXT, ProviderJsonOptions.OPTIONS),
|
||||||
["text"] = JsonSerializer.Serialize(this.text.ToString()),
|
["text"] = JsonSerializer.Serialize(this.text.ToString(), ProviderJsonOptions.OPTIONS),
|
||||||
});
|
});
|
||||||
|
|
||||||
case TYPE_THINKING:
|
case TYPE_THINKING:
|
||||||
@@ -129,9 +129,9 @@ public sealed class AnthropicContentBlockBuilder
|
|||||||
//
|
//
|
||||||
return this.BuildFromSeed(new()
|
return this.BuildFromSeed(new()
|
||||||
{
|
{
|
||||||
["type"] = JsonSerializer.Serialize(TYPE_THINKING),
|
["type"] = JsonSerializer.Serialize(TYPE_THINKING, ProviderJsonOptions.OPTIONS),
|
||||||
["thinking"] = JsonSerializer.Serialize(this.thinking.ToString()),
|
["thinking"] = JsonSerializer.Serialize(this.thinking.ToString(), ProviderJsonOptions.OPTIONS),
|
||||||
["signature"] = JsonSerializer.Serialize(this.signature),
|
["signature"] = JsonSerializer.Serialize(this.signature, ProviderJsonOptions.OPTIONS),
|
||||||
});
|
});
|
||||||
|
|
||||||
case TYPE_TOOL_USE:
|
case TYPE_TOOL_USE:
|
||||||
|
|||||||
@@ -10,4 +10,12 @@ namespace AIStudio.Provider.Anthropic;
|
|||||||
/// thinking blocks, and the thinking blocks have to return exactly as they arrived — reading and
|
/// thinking blocks, and the thinking blocks have to return exactly as they arrived — reading and
|
||||||
/// rebuilding them would risk changing them.
|
/// rebuilding them would risk changing them.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public sealed record AnthropicMessage(IList<JsonElement> Content, string Role = "assistant") : IMessage<IList<JsonElement>>;
|
public sealed record AnthropicMessage(IList<JsonElement> Content, string Role = "assistant") : IMessage<IList<JsonElement>>
|
||||||
|
{
|
||||||
|
//
|
||||||
|
// The properties of the parameters, declared again to be settable, as the messages demand it. Keep them in
|
||||||
|
// the order of the parameters, which is the order they get written in:
|
||||||
|
//
|
||||||
|
public IList<JsonElement> Content { get; set; } = Content;
|
||||||
|
public string Role { get; set; } = Role;
|
||||||
|
}
|
||||||
@@ -152,9 +152,9 @@ public sealed class AnthropicMessageStreamAccumulator
|
|||||||
this.unparsableToolArguments[builder.ToolUseId] = builder.UnparsableToolArguments;
|
this.unparsableToolArguments[builder.ToolUseId] = builder.UnparsableToolArguments;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static JsonElement EmptyTextBlock() => JsonSerializer.SerializeToElement(new
|
private static JsonElement EmptyTextBlock()
|
||||||
{
|
{
|
||||||
type = "text",
|
using var document = JsonDocument.Parse("""{"type":"text","text":""}""");
|
||||||
text = string.Empty,
|
return document.RootElement.Clone();
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
@@ -14,5 +14,5 @@ public sealed record AnthropicStreamMessage
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// What the request carried, where the stream states it.
|
/// What the request carried, where the stream states it.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public AnthropicUsage? Usage { get; init; }
|
public AnthropicUsage? Usage { get; set; }
|
||||||
}
|
}
|
||||||
@@ -11,11 +11,11 @@ namespace AIStudio.Provider.Anthropic;
|
|||||||
/// </remarks>
|
/// </remarks>
|
||||||
public sealed record AnthropicTool
|
public sealed record AnthropicTool
|
||||||
{
|
{
|
||||||
public string Name { get; init; } = string.Empty;
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
public string Description { get; init; } = string.Empty;
|
public string Description { get; set; } = string.Empty;
|
||||||
|
|
||||||
public bool Strict { get; init; }
|
public bool Strict { get; set; }
|
||||||
|
|
||||||
public JsonElement InputSchema { get; init; }
|
public JsonElement InputSchema { get; set; }
|
||||||
}
|
}
|
||||||
@@ -4,11 +4,11 @@ namespace AIStudio.Provider.Anthropic;
|
|||||||
|
|
||||||
public sealed record AnthropicToolResultContent
|
public sealed record AnthropicToolResultContent
|
||||||
{
|
{
|
||||||
public string Type { get; init; } = "tool_result";
|
public string Type { get; set; } = "tool_result";
|
||||||
|
|
||||||
public string ToolUseId { get; init; } = string.Empty;
|
public string ToolUseId { get; set; } = string.Empty;
|
||||||
|
|
||||||
public string Content { get; init; } = string.Empty;
|
public string Content { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether the tool failed rather than returning a result.
|
/// Whether the tool failed rather than returning a result.
|
||||||
@@ -18,5 +18,5 @@ public sealed record AnthropicToolResultContent
|
|||||||
/// result stays byte-identical to what earlier versions sent.
|
/// result stays byte-identical to what earlier versions sent.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
|
||||||
public bool IsError { get; init; }
|
public bool IsError { get; set; }
|
||||||
}
|
}
|
||||||
@@ -7,4 +7,12 @@ namespace AIStudio.Provider.Anthropic;
|
|||||||
/// All results of one turn belong in a single message. Splitting them across several messages
|
/// All results of one turn belong in a single message. Splitting them across several messages
|
||||||
/// teaches the model to stop asking for more than one tool at a time.
|
/// teaches the model to stop asking for more than one tool at a time.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public sealed record AnthropicToolResultMessage(IList<AnthropicToolResultContent> Content, string Role = "user") : IMessage<IList<AnthropicToolResultContent>>;
|
public sealed record AnthropicToolResultMessage(IList<AnthropicToolResultContent> Content, string Role = "user") : IMessage<IList<AnthropicToolResultContent>>
|
||||||
|
{
|
||||||
|
//
|
||||||
|
// The properties of the parameters, declared again to be settable, as the messages demand it. Keep them in
|
||||||
|
// the order of the parameters, which is the order they get written in:
|
||||||
|
//
|
||||||
|
public IList<AnthropicToolResultContent> Content { get; set; } = Content;
|
||||||
|
public string Role { get; set; } = Role;
|
||||||
|
}
|
||||||
@@ -23,17 +23,17 @@ public sealed record AnthropicUsage
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// What the request carried after its last cache breakpoint, which is all of it without caching.
|
/// What the request carried after its last cache breakpoint, which is all of it without caching.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? InputTokens { get; init; }
|
public int? InputTokens { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// What the request wrote to the cache.
|
/// What the request wrote to the cache.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? CacheCreationInputTokens { get; init; }
|
public int? CacheCreationInputTokens { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// What the request read from the cache.
|
/// What the request read from the cache.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? CacheReadInputTokens { get; init; }
|
public int? CacheReadInputTokens { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// States what this block reports, as far as it can be believed.
|
/// States what this block reports, as far as it can be believed.
|
||||||
|
|||||||
@@ -10,7 +10,11 @@ namespace AIStudio.Provider.Anthropic;
|
|||||||
/// <param name="MaxTokens">The maximum number of tokens to generate.</param>
|
/// <param name="MaxTokens">The maximum number of tokens to generate.</param>
|
||||||
/// <param name="Stream">Whether to stream the chat completion.</param>
|
/// <param name="Stream">Whether to stream the chat completion.</param>
|
||||||
/// <param name="System">The system prompt for the chat completion.</param>
|
/// <param name="System">The system prompt for the chat completion.</param>
|
||||||
public readonly record struct ChatRequest(
|
/// <remarks>
|
||||||
|
/// Not readonly, so that its properties can be settable: the source generation of System.Text.Json refuses to
|
||||||
|
/// write extension data which is init-only.
|
||||||
|
/// </remarks>
|
||||||
|
public record struct ChatRequest(
|
||||||
string Model,
|
string Model,
|
||||||
IList<IMessageBase> Messages,
|
IList<IMessageBase> Messages,
|
||||||
int MaxTokens,
|
int MaxTokens,
|
||||||
@@ -26,9 +30,12 @@ public readonly record struct ChatRequest(
|
|||||||
/// tools at all, and the final round of a tool conversation has to offer none.
|
/// tools at all, and the final round of a tool conversation has to offer none.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||||
public IList<AnthropicTool>? Tools { get; init; }
|
public IList<AnthropicTool>? Tools { get; set; }
|
||||||
|
|
||||||
// Attention: The "required" modifier is not supported for [JsonExtensionData].
|
//
|
||||||
|
// Attention: The "required" modifier is not supported for [JsonExtensionData], and neither is init: the source
|
||||||
|
// generation of System.Text.Json refuses to write the request at all then.
|
||||||
|
//
|
||||||
[JsonExtensionData]
|
[JsonExtensionData]
|
||||||
public IDictionary<string, object> AdditionalApiParameters { get; init; } = new Dictionary<string, object>();
|
public IDictionary<string, object> AdditionalApiParameters { get; set; } = new Dictionary<string, object>();
|
||||||
}
|
}
|
||||||
@@ -4,7 +4,7 @@ public record SubContentBase64Image : ISubContentImageSource
|
|||||||
{
|
{
|
||||||
public SubContentImageType Type => SubContentImageType.BASE64;
|
public SubContentImageType Type => SubContentImageType.BASE64;
|
||||||
|
|
||||||
public string MediaType { get; init; } = string.Empty;
|
public string MediaType { get; set; } = string.Empty;
|
||||||
|
|
||||||
public string Data { get; init; } = string.Empty;
|
public string Data { get; set; } = string.Empty;
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,15 @@ namespace AIStudio.Provider.Anthropic;
|
|||||||
|
|
||||||
public record SubContentImage(SubContentType Type, ISubContentImageSource Source) : ISubContent
|
public record SubContentImage(SubContentType Type, ISubContentImageSource Source) : ISubContent
|
||||||
{
|
{
|
||||||
|
//
|
||||||
|
// The properties of the parameters, declared again to be settable. The source generation of System.Text.Json
|
||||||
|
// sets every init-only property while it creates the object, also one missing in the JSON, so its value from
|
||||||
|
// the parameterless constructor would get lost. Keep them first and in the order of the parameters, which is
|
||||||
|
// the order they get written in:
|
||||||
|
//
|
||||||
|
public SubContentType Type { get; set; } = Type;
|
||||||
|
public ISubContentImageSource Source { get; set; } = Source;
|
||||||
|
|
||||||
public SubContentImage() : this(SubContentType.IMAGE, new SubContentImageUrl())
|
public SubContentImage() : this(SubContentType.IMAGE, new SubContentImageUrl())
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,5 +4,5 @@ public record SubContentImageUrl : ISubContentImageSource
|
|||||||
{
|
{
|
||||||
public SubContentImageType Type => SubContentImageType.URL;
|
public SubContentImageType Type => SubContentImageType.URL;
|
||||||
|
|
||||||
public string Url { get; init; } = string.Empty;
|
public string Url { get; set; } = string.Empty;
|
||||||
}
|
}
|
||||||
@@ -1377,7 +1377,7 @@ public abstract class BaseProvider : IProvider, ISecretId
|
|||||||
if (runnableTools.Count > 0)
|
if (runnableTools.Count > 0)
|
||||||
{
|
{
|
||||||
var adapter = new ChatCompletionToolCallingAdapter<TRequest>(requestFactory, systemPrompt, apiParameters,
|
var adapter = new ChatCompletionToolCallingAdapter<TRequest>(requestFactory, systemPrompt, apiParameters,
|
||||||
runnableTools.Select(x => ProviderToolAdapters.ToChatCompletionTool(x.Definition, enforcesStrictToolSchemas)).ToList(), mayAskForSequentialToolCalls, runnableTools,
|
runnableTools.Select(x => (object)ProviderToolAdapters.ToChatCompletionTool(x.Definition, enforcesStrictToolSchemas)).ToList(), mayAskForSequentialToolCalls, runnableTools,
|
||||||
(requestDto, requestToken) => this.StreamChatCompletionRequest(requestDto, providerName, requestPath, requestedSecret, requestHeadersAction, requestToken),
|
(requestDto, requestToken) => this.StreamChatCompletionRequest(requestDto, providerName, requestPath, requestedSecret, requestHeadersAction, requestToken),
|
||||||
ChatCompletionSourceReader.Read<TDelta, TAnnotation>,
|
ChatCompletionSourceReader.Read<TDelta, TAnnotation>,
|
||||||
this.logger);
|
this.logger);
|
||||||
@@ -1662,13 +1662,7 @@ public abstract class BaseProvider : IProvider, ISecretId
|
|||||||
modelName = "placeholder";
|
modelName = "placeholder";
|
||||||
|
|
||||||
// Prepare the HTTP embedding request:
|
// Prepare the HTTP embedding request:
|
||||||
var payload = new
|
var payload = new EmbeddingRequest(modelName, texts);
|
||||||
{
|
|
||||||
model = modelName,
|
|
||||||
input = texts,
|
|
||||||
encoding_format = "float"
|
|
||||||
};
|
|
||||||
|
|
||||||
var embeddingRequest = JsonSerializer.Serialize(payload, JSON_SERIALIZER_OPTIONS);
|
var embeddingRequest = JsonSerializer.Serialize(payload, JSON_SERIALIZER_OPTIONS);
|
||||||
using var request = new HttpRequestMessage(HttpMethod.Post, host.EmbeddingURL());
|
using var request = new HttpRequestMessage(HttpMethod.Post, host.EmbeddingURL());
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
namespace AIStudio.Provider;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A request for embeddings, in the shape of the OpenAI API, which most providers take.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="Model">The name of the embedding model.</param>
|
||||||
|
/// <param name="Input">The texts to embed.</param>
|
||||||
|
public sealed record EmbeddingRequest(string Model, IList<string> Input)
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Asks for the vectors as numbers, instead of as base64.
|
||||||
|
/// </summary>
|
||||||
|
public string EncodingFormat => "float";
|
||||||
|
}
|
||||||
@@ -2,13 +2,13 @@
|
|||||||
|
|
||||||
public sealed record EmbeddingResponse
|
public sealed record EmbeddingResponse
|
||||||
{
|
{
|
||||||
public string? Id { get; init; }
|
public string? Id { get; set; }
|
||||||
|
|
||||||
public string? Object { get; init; }
|
public string? Object { get; set; }
|
||||||
|
|
||||||
public List<EmbeddingData>? Data { get; init; }
|
public List<EmbeddingData>? Data { get; set; }
|
||||||
|
|
||||||
public string? Model { get; init; }
|
public string? Model { get; set; }
|
||||||
|
|
||||||
public EmbeddingUsage? Usage { get; init; }
|
public EmbeddingUsage? Usage { get; set; }
|
||||||
}
|
}
|
||||||
@@ -2,5 +2,5 @@
|
|||||||
|
|
||||||
public sealed record GoogleEmbedding
|
public sealed record GoogleEmbedding
|
||||||
{
|
{
|
||||||
public List<float>? Values { get; init; }
|
public List<float>? Values { get; set; }
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace AIStudio.Provider.Google;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The content of a request for an embedding to the Gemini API.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="Parts">The texts, each of them as a part of its own.</param>
|
||||||
|
public sealed record GoogleEmbeddingContent(IList<GoogleEmbeddingPart> Parts);
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace AIStudio.Provider.Google;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// One text in a request for an embedding to the Gemini API.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="Text">The text to embed.</param>
|
||||||
|
public sealed record GoogleEmbeddingPart(string Text);
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
namespace AIStudio.Provider.Google;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A request for an embedding to the Gemini API.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="Content">The texts to embed.</param>
|
||||||
|
public sealed record GoogleEmbeddingRequest(GoogleEmbeddingContent Content)
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Asks for vectors which tell how similar texts are.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Written as task_type, like every other field of our requests. The API reads both its field names in
|
||||||
|
/// snake case and in camel case.
|
||||||
|
/// </remarks>
|
||||||
|
public string TaskType => "SEMANTIC_SIMILARITY";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates the request for the given texts, each of them as a part of its own.
|
||||||
|
/// </summary>
|
||||||
|
public static GoogleEmbeddingRequest Of(IEnumerable<string> texts) => new(new GoogleEmbeddingContent([..texts.Select(text => new GoogleEmbeddingPart(text))]));
|
||||||
|
}
|
||||||
@@ -6,9 +6,13 @@ namespace AIStudio.Provider.Google;
|
|||||||
public sealed record GoogleEmbeddingResponse
|
public sealed record GoogleEmbeddingResponse
|
||||||
{
|
{
|
||||||
[JsonConverter(typeof(GoogleEmbeddingListConverter))]
|
[JsonConverter(typeof(GoogleEmbeddingListConverter))]
|
||||||
public List<GoogleEmbedding>? Embedding { get; init; }
|
public List<GoogleEmbedding>? Embedding { get; set; }
|
||||||
|
|
||||||
private sealed class GoogleEmbeddingListConverter : JsonConverter<List<GoogleEmbedding>>
|
/// <remarks>
|
||||||
|
/// Internal rather than private: the JSON context of the providers has to reach the converter, else it reads
|
||||||
|
/// the embeddings without it.
|
||||||
|
/// </remarks>
|
||||||
|
internal sealed class GoogleEmbeddingListConverter : JsonConverter<List<GoogleEmbedding>>
|
||||||
{
|
{
|
||||||
public override List<GoogleEmbedding> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
public override List<GoogleEmbedding> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -92,16 +92,7 @@ public class ProviderGoogle() : BaseProvider(LLMProviders.GOOGLE, new Uri("https
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Prepare the Google Gemini embedding request:
|
// Prepare the Google Gemini embedding request:
|
||||||
var payload = new
|
var payload = GoogleEmbeddingRequest.Of(texts);
|
||||||
{
|
|
||||||
content = new
|
|
||||||
{
|
|
||||||
parts = texts.Select(text => new { text }).ToArray()
|
|
||||||
},
|
|
||||||
|
|
||||||
taskType = "SEMANTIC_SIMILARITY"
|
|
||||||
};
|
|
||||||
|
|
||||||
var embeddingRequest = JsonSerializer.Serialize(payload, JSON_SERIALIZER_OPTIONS);
|
var embeddingRequest = JsonSerializer.Serialize(payload, JSON_SERIALIZER_OPTIONS);
|
||||||
var embedUrl = $"https://generativelanguage.googleapis.com/v1beta/models/{modelName}:embedContent";
|
var embedUrl = $"https://generativelanguage.googleapis.com/v1beta/models/{modelName}:embedContent";
|
||||||
using var request = new HttpRequestMessage(HttpMethod.Post, embedUrl);
|
using var request = new HttpRequestMessage(HttpMethod.Post, embedUrl);
|
||||||
|
|||||||
@@ -11,5 +11,5 @@ public interface IMessage<T> : IMessageBase
|
|||||||
/// The content encapsulates the core information or data being transmitted,
|
/// The content encapsulates the core information or data being transmitted,
|
||||||
/// and its type can vary based on the specific implementation or use case.
|
/// and its type can vary based on the specific implementation or use case.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public T Content { get; init; }
|
public T Content { get; set; }
|
||||||
}
|
}
|
||||||
@@ -10,5 +10,5 @@ public interface IMessageBase
|
|||||||
/// This property typically identifies whether the entity is acting
|
/// This property typically identifies whether the entity is acting
|
||||||
/// as a user, assistant, or system in the context of the interaction.
|
/// as a user, assistant, or system in the context of the interaction.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Role { get; init; }
|
public string Role { get; set; }
|
||||||
}
|
}
|
||||||
@@ -5,12 +5,12 @@ namespace AIStudio.Provider.OpenAI;
|
|||||||
|
|
||||||
public sealed record AssistantToolCallMessage : IMessageBase
|
public sealed record AssistantToolCallMessage : IMessageBase
|
||||||
{
|
{
|
||||||
public string Role { get; init; } = "assistant";
|
public string Role { get; set; } = "assistant";
|
||||||
|
|
||||||
public JsonElement? Content { get; init; }
|
public JsonElement? Content { get; set; }
|
||||||
|
|
||||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||||
public string? ReasoningContent { get; init; }
|
public string? ReasoningContent { get; set; }
|
||||||
|
|
||||||
public IList<ChatCompletionToolCall> ToolCalls { get; init; } = [];
|
public IList<ChatCompletionToolCall> ToolCalls { get; set; } = [];
|
||||||
}
|
}
|
||||||
@@ -14,15 +14,25 @@ public record ChatCompletionAPIRequest(
|
|||||||
bool Stream
|
bool Stream
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
|
//
|
||||||
|
// The properties of the parameters, declared again to be settable. The source generation of System.Text.Json
|
||||||
|
// sets every init-only property while it creates the object, also one missing in the JSON, so its value from
|
||||||
|
// the parameterless constructor would get lost. Keep them first and in the order of the parameters, which is
|
||||||
|
// the order they get written in:
|
||||||
|
//
|
||||||
|
public string Model { get; set; } = Model;
|
||||||
|
public IList<IMessageBase> Messages { get; set; } = Messages;
|
||||||
|
public bool Stream { get; set; } = Stream;
|
||||||
|
|
||||||
public ChatCompletionAPIRequest() : this(string.Empty, [], true)
|
public ChatCompletionAPIRequest() : this(string.Empty, [], true)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||||
public IList<object>? Tools { get; init; }
|
public IList<object>? Tools { get; set; }
|
||||||
|
|
||||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||||
public bool? ParallelToolCalls { get; init; }
|
public bool? ParallelToolCalls { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Asks a streamed request to end with what it cost.
|
/// Asks a streamed request to end with what it cost.
|
||||||
@@ -35,7 +45,10 @@ public record ChatCompletionAPIRequest(
|
|||||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||||
public ChatCompletionStreamOptions? StreamOptions => this.Stream ? ChatCompletionStreamOptions.INCLUDE_USAGE : null;
|
public ChatCompletionStreamOptions? StreamOptions => this.Stream ? ChatCompletionStreamOptions.INCLUDE_USAGE : null;
|
||||||
|
|
||||||
// Attention: The "required" modifier is not supported for [JsonExtensionData].
|
//
|
||||||
|
// Attention: The "required" modifier is not supported for [JsonExtensionData], and neither is init: the source
|
||||||
|
// generation of System.Text.Json refuses to write the request at all then.
|
||||||
|
//
|
||||||
[JsonExtensionData]
|
[JsonExtensionData]
|
||||||
public IDictionary<string, object> AdditionalApiParameters { get; init; } = new Dictionary<string, object>();
|
public IDictionary<string, object> AdditionalApiParameters { get; set; } = new Dictionary<string, object>();
|
||||||
}
|
}
|
||||||
@@ -7,6 +7,14 @@ namespace AIStudio.Provider.OpenAI;
|
|||||||
/// <param name="Delta">The delta text of the choice.</param>
|
/// <param name="Delta">The delta text of the choice.</param>
|
||||||
public record ChatCompletionChoice(int Index, ChatCompletionDelta Delta)
|
public record ChatCompletionChoice(int Index, ChatCompletionDelta Delta)
|
||||||
{
|
{
|
||||||
|
//
|
||||||
|
// The properties of the parameters, declared again to be settable. The source generation of System.Text.Json
|
||||||
|
// sets every init-only property while it creates the object, also one missing in the JSON, so its value from
|
||||||
|
// the parameterless constructor would get lost. Keep them first and in the order of the parameters:
|
||||||
|
//
|
||||||
|
public int Index { get; set; } = Index;
|
||||||
|
public ChatCompletionDelta Delta { get; set; } = Delta;
|
||||||
|
|
||||||
public ChatCompletionChoice() : this(0, new())
|
public ChatCompletionChoice() : this(0, new())
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ namespace AIStudio.Provider.OpenAI;
|
|||||||
public sealed record ChatCompletionDelta
|
public sealed record ChatCompletionDelta
|
||||||
{
|
{
|
||||||
[JsonPropertyName("content")]
|
[JsonPropertyName("content")]
|
||||||
public JsonElement? RawContent { get; init; }
|
public JsonElement? RawContent { get; set; }
|
||||||
|
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public string Content => ChatCompletionContent.GetText(this.RawContent) ?? string.Empty;
|
public string Content => ChatCompletionContent.GetText(this.RawContent) ?? string.Empty;
|
||||||
|
|||||||
@@ -11,6 +11,19 @@ namespace AIStudio.Provider.OpenAI;
|
|||||||
/// <param name="Choices">The choices made by the AI.</param>
|
/// <param name="Choices">The choices made by the AI.</param>
|
||||||
public record ChatCompletionDeltaStreamLine(string Id, string Object, uint Created, string Model, string SystemFingerprint, IList<ChatCompletionChoice> Choices) : IResponseStreamLine
|
public record ChatCompletionDeltaStreamLine(string Id, string Object, uint Created, string Model, string SystemFingerprint, IList<ChatCompletionChoice> Choices) : IResponseStreamLine
|
||||||
{
|
{
|
||||||
|
//
|
||||||
|
// The properties of the parameters, declared again to be settable. The source generation of System.Text.Json
|
||||||
|
// sets every init-only property while it creates the object, also one missing in the JSON, so its value from
|
||||||
|
// the parameterless constructor would get lost, e.g., the empty list of choices of the line which carries the
|
||||||
|
// usage. Keep them first and in the order of the parameters:
|
||||||
|
//
|
||||||
|
public string Id { get; set; } = Id;
|
||||||
|
public string Object { get; set; } = Object;
|
||||||
|
public uint Created { get; set; } = Created;
|
||||||
|
public string Model { get; set; } = Model;
|
||||||
|
public string SystemFingerprint { get; set; } = SystemFingerprint;
|
||||||
|
public IList<ChatCompletionChoice> Choices { get; set; } = Choices;
|
||||||
|
|
||||||
public ChatCompletionDeltaStreamLine() : this(string.Empty, string.Empty, 0, string.Empty, string.Empty, [])
|
public ChatCompletionDeltaStreamLine() : this(string.Empty, string.Empty, 0, string.Empty, string.Empty, [])
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
@@ -25,7 +38,7 @@ public record ChatCompletionDeltaStreamLine(string Id, string Object, uint Creat
|
|||||||
/// line of its own which carries no choices -- which is why the usage is read apart from the
|
/// line of its own which carries no choices -- which is why the usage is read apart from the
|
||||||
/// content rather than next to it.
|
/// content rather than next to it.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public ChatCompletionUsage? Usage { get; init; }
|
public ChatCompletionUsage? Usage { get; set; }
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public bool ContainsContent() => this.Choices.Count > 0;
|
public bool ContainsContent() => this.Choices.Count > 0;
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace AIStudio.Provider.OpenAI;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The function a tool of a Chat Completions compatible API offers to the model.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record ChatCompletionFunctionDeclaration
|
||||||
|
{
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string Description { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public JsonElement Parameters { get; set; }
|
||||||
|
|
||||||
|
public bool Strict { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
namespace AIStudio.Provider.OpenAI;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The function tool definition shape expected by Chat Completions compatible APIs, with the function nested.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record ChatCompletionFunctionTool
|
||||||
|
{
|
||||||
|
public string Type { get; set; } = "function";
|
||||||
|
|
||||||
|
public ChatCompletionFunctionDeclaration Function { get; set; } = new();
|
||||||
|
}
|
||||||
@@ -16,7 +16,7 @@ public sealed record ChatCompletionStreamDelta
|
|||||||
/// The content as it arrived: a string for most providers, a list of parts for some.
|
/// The content as it arrived: a string for most providers, a list of parts for some.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonPropertyName("content")]
|
[JsonPropertyName("content")]
|
||||||
public JsonElement? RawContent { get; init; }
|
public JsonElement? RawContent { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The text of this fragment, whichever shape it arrived in.
|
/// The text of this fragment, whichever shape it arrived in.
|
||||||
@@ -27,10 +27,10 @@ public sealed record ChatCompletionStreamDelta
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The reasoning text some providers stream next to the answer.
|
/// The reasoning text some providers stream next to the answer.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? ReasoningContent { get; init; }
|
public string? ReasoningContent { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The fragments of the tool calls the model is asking for.
|
/// The fragments of the tool calls the model is asking for.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IList<ChatCompletionToolCallDelta?>? ToolCalls { get; init; }
|
public IList<ChatCompletionToolCallDelta?>? ToolCalls { get; set; }
|
||||||
}
|
}
|
||||||
@@ -5,12 +5,12 @@ namespace AIStudio.Provider.OpenAI;
|
|||||||
|
|
||||||
public sealed record ChatCompletionToolCall
|
public sealed record ChatCompletionToolCall
|
||||||
{
|
{
|
||||||
public string? Id { get; init; }
|
public string? Id { get; set; }
|
||||||
|
|
||||||
public string? Type { get; init; } = "function";
|
public string? Type { get; set; } = "function";
|
||||||
|
|
||||||
public ChatCompletionToolFunction? Function { get; init; }
|
public ChatCompletionToolFunction? Function { get; set; }
|
||||||
|
|
||||||
[JsonExtensionData]
|
[JsonExtensionData]
|
||||||
public IDictionary<string, JsonElement> AdditionalMetadata { get; init; } = new Dictionary<string, JsonElement>();
|
public IDictionary<string, JsonElement> AdditionalMetadata { get; set; } = new Dictionary<string, JsonElement>();
|
||||||
}
|
}
|
||||||
@@ -117,7 +117,7 @@ public sealed class ChatCompletionToolCallAccumulator(Func<ServerSentEvent, ILis
|
|||||||
// No text means no content field, the way a round which only calls a tool arrives
|
// No text means no content field, the way a round which only calls a tool arrives
|
||||||
// when it is not streamed. Some providers reject an empty string in its place.
|
// when it is not streamed. Some providers reject an empty string in its place.
|
||||||
//
|
//
|
||||||
RawContent = answer.Length is 0 ? null : JsonSerializer.SerializeToElement(answer),
|
RawContent = answer.Length is 0 ? null : JsonSerializer.SerializeToElement(answer, ProviderJsonOptions.OPTIONS),
|
||||||
ReasoningContent = this.reasoning.Length is 0 ? null : this.reasoning.ToString(),
|
ReasoningContent = this.reasoning.Length is 0 ? null : this.reasoning.ToString(),
|
||||||
ToolCalls = this.toolCalls.Count is 0
|
ToolCalls = this.toolCalls.Count is 0
|
||||||
? null
|
? null
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ namespace AIStudio.Provider.OpenAI;
|
|||||||
|
|
||||||
public sealed record ChatCompletionToolFunction
|
public sealed record ChatCompletionToolFunction
|
||||||
{
|
{
|
||||||
public string? Name { get; init; }
|
public string? Name { get; set; }
|
||||||
|
|
||||||
public string? Arguments { get; init; }
|
public string? Arguments { get; set; }
|
||||||
}
|
}
|
||||||
@@ -21,5 +21,5 @@ public sealed record ChatCompletionToolStreamLine(string? Id, IList<ChatCompleti
|
|||||||
/// ChatCompletionAPIRequest asks for it, the requests of the tool rounds included. Not a
|
/// ChatCompletionAPIRequest asks for it, the requests of the tool rounds included. Not a
|
||||||
/// positional parameter, because nobody but the serializer ever builds this line.
|
/// positional parameter, because nobody but the serializer ever builds this line.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public ChatCompletionUsage? Usage { get; init; }
|
public ChatCompletionUsage? Usage { get; set; }
|
||||||
}
|
}
|
||||||
@@ -17,7 +17,7 @@ public sealed record ChatCompletionUsage
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// What everything sent to the model cost.
|
/// What everything sent to the model cost.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? PromptTokens { get; init; }
|
public int? PromptTokens { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// States what this block reports, as far as it can be believed.
|
/// States what this block reports, as far as it can be believed.
|
||||||
|
|||||||
@@ -8,5 +8,5 @@ public interface ISubContent
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The type of the sub-content.
|
/// The type of the sub-content.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public SubContentType Type { get; init; }
|
public SubContentType Type { get; set; }
|
||||||
}
|
}
|
||||||
@@ -15,5 +15,5 @@ public interface ISubContentImageUrl
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The URL or base64-encoded data URI of the image.
|
/// The URL or base64-encoded data URI of the image.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Url { get; init; }
|
public string Url { get; set; }
|
||||||
}
|
}
|
||||||
@@ -7,6 +7,15 @@ namespace AIStudio.Provider.OpenAI;
|
|||||||
/// <param name="Role">The role of the message.</param>
|
/// <param name="Role">The role of the message.</param>
|
||||||
public record MultimodalMessage(List<ISubContent> Content, string Role) : IMessage<List<ISubContent>>
|
public record MultimodalMessage(List<ISubContent> Content, string Role) : IMessage<List<ISubContent>>
|
||||||
{
|
{
|
||||||
|
//
|
||||||
|
// The properties of the parameters, declared again to be settable. The source generation of System.Text.Json
|
||||||
|
// sets every init-only property while it creates the object, also one missing in the JSON, so its value from
|
||||||
|
// the parameterless constructor would get lost. Keep them first and in the order of the parameters, which is
|
||||||
|
// the order they get written in:
|
||||||
|
//
|
||||||
|
public List<ISubContent> Content { get; set; } = Content;
|
||||||
|
public string Role { get; set; } = Role;
|
||||||
|
|
||||||
public MultimodalMessage() : this([], string.Empty)
|
public MultimodalMessage() : this([], string.Empty)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ public static class OpenAIStrictToolSchema
|
|||||||
// schema stays stable across requests, which prompt caching depends on.
|
// schema stays stable across requests, which prompt caching depends on.
|
||||||
//
|
//
|
||||||
schema["required"] = new JsonArray([..properties.Select(property => JsonValue.Create(property.Key))]);
|
schema["required"] = new JsonArray([..properties.Select(property => JsonValue.Create(property.Key))]);
|
||||||
return JsonSerializer.Deserialize<JsonElement>(schema.ToJsonString());
|
return JsonSerializer.Deserialize<JsonElement>(schema.ToJsonString(), ProviderJsonOptions.OPTIONS);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void AllowNullType(JsonObject property)
|
private static void AllowNullType(JsonObject property)
|
||||||
|
|||||||
@@ -17,11 +17,26 @@ public record ResponsesAPIRequest(
|
|||||||
bool Store,
|
bool Store,
|
||||||
IList<object> Tools)
|
IList<object> Tools)
|
||||||
{
|
{
|
||||||
|
//
|
||||||
|
// The properties of the parameters, declared again to be settable. The source generation of System.Text.Json
|
||||||
|
// sets every init-only property while it creates the object, also one missing in the JSON, so its value from
|
||||||
|
// the parameterless constructor would get lost. Keep them first and in the order of the parameters, which is
|
||||||
|
// the order they get written in:
|
||||||
|
//
|
||||||
|
public string Model { get; set; } = Model;
|
||||||
|
public IList<object> Input { get; set; } = Input;
|
||||||
|
public bool Stream { get; set; } = Stream;
|
||||||
|
public bool Store { get; set; } = Store;
|
||||||
|
public IList<object> Tools { get; set; } = Tools;
|
||||||
|
|
||||||
public ResponsesAPIRequest() : this(string.Empty, [], true, false, [])
|
public ResponsesAPIRequest() : this(string.Empty, [], true, false, [])
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attention: The "required" modifier is not supported for [JsonExtensionData].
|
//
|
||||||
|
// Attention: The "required" modifier is not supported for [JsonExtensionData], and neither is init: the source
|
||||||
|
// generation of System.Text.Json refuses to write the request at all then.
|
||||||
|
//
|
||||||
[JsonExtensionData]
|
[JsonExtensionData]
|
||||||
public IDictionary<string, object> AdditionalApiParameters { get; init; } = new Dictionary<string, object>();
|
public IDictionary<string, object> AdditionalApiParameters { get; set; } = new Dictionary<string, object>();
|
||||||
}
|
}
|
||||||
@@ -5,9 +5,9 @@ namespace AIStudio.Provider.OpenAI;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed record ResponsesFunctionCallOutputItem
|
public sealed record ResponsesFunctionCallOutputItem
|
||||||
{
|
{
|
||||||
public string Type { get; init; } = "function_call_output";
|
public string Type { get; set; } = "function_call_output";
|
||||||
|
|
||||||
public string CallId { get; init; } = string.Empty;
|
public string CallId { get; set; } = string.Empty;
|
||||||
|
|
||||||
public string Output { get; init; } = string.Empty;
|
public string Output { get; set; } = string.Empty;
|
||||||
}
|
}
|
||||||
@@ -7,13 +7,13 @@ namespace AIStudio.Provider.OpenAI;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed record ResponsesFunctionTool
|
public sealed record ResponsesFunctionTool
|
||||||
{
|
{
|
||||||
public string Type { get; init; } = "function";
|
public string Type { get; set; } = "function";
|
||||||
|
|
||||||
public string Name { get; init; } = string.Empty;
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
public string Description { get; init; } = string.Empty;
|
public string Description { get; set; } = string.Empty;
|
||||||
|
|
||||||
public JsonElement Parameters { get; init; }
|
public JsonElement Parameters { get; set; }
|
||||||
|
|
||||||
public bool Strict { get; init; }
|
public bool Strict { get; set; }
|
||||||
}
|
}
|
||||||
@@ -17,18 +17,18 @@ public sealed record ResponsesResponse
|
|||||||
/// </remarks>
|
/// </remarks>
|
||||||
private static readonly HashSet<string> OUTPUT_ITEMS_LEAVING_THE_INPUT_ALONE = ["message", "reasoning", "function_call"];
|
private static readonly HashSet<string> OUTPUT_ITEMS_LEAVING_THE_INPUT_ALONE = ["message", "reasoning", "function_call"];
|
||||||
|
|
||||||
public string Id { get; init; } = string.Empty;
|
public string Id { get; set; } = string.Empty;
|
||||||
|
|
||||||
public string Model { get; init; } = string.Empty;
|
public string Model { get; set; } = string.Empty;
|
||||||
|
|
||||||
public string? OutputText { get; init; }
|
public string? OutputText { get; set; }
|
||||||
|
|
||||||
public IList<JsonElement> Output { get; init; } = [];
|
public IList<JsonElement> Output { get; set; } = [];
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// What OpenAI says the response cost. Only the completed response carries it.
|
/// What OpenAI says the response cost. Only the completed response carries it.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ResponsesUsage? Usage { get; init; }
|
public ResponsesUsage? Usage { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// States what the request of this response carried, as far as it can be believed.
|
/// States what the request of this response carried, as far as it can be believed.
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ public sealed record ResponsesUsage
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// What everything sent to the model cost, the cached part included.
|
/// What everything sent to the model cost, the cached part included.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? InputTokens { get; init; }
|
public int? InputTokens { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// States what this block reports, as far as it can be believed.
|
/// States what this block reports, as far as it can be believed.
|
||||||
|
|||||||
@@ -5,6 +5,15 @@ namespace AIStudio.Provider.OpenAI;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public record SubContentImageUrl(SubContentType Type, string ImageUrl) : ISubContent
|
public record SubContentImageUrl(SubContentType Type, string ImageUrl) : ISubContent
|
||||||
{
|
{
|
||||||
|
//
|
||||||
|
// The properties of the parameters, declared again to be settable. The source generation of System.Text.Json
|
||||||
|
// sets every init-only property while it creates the object, also one missing in the JSON, so its value from
|
||||||
|
// the parameterless constructor would get lost. Keep them first and in the order of the parameters, which is
|
||||||
|
// the order they get written in:
|
||||||
|
//
|
||||||
|
public SubContentType Type { get; set; } = Type;
|
||||||
|
public string ImageUrl { get; set; } = ImageUrl;
|
||||||
|
|
||||||
public SubContentImageUrl() : this(SubContentType.IMAGE_URL, string.Empty)
|
public SubContentImageUrl() : this(SubContentType.IMAGE_URL, string.Empty)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,5 +13,5 @@ namespace AIStudio.Provider.OpenAI;
|
|||||||
public record SubContentImageUrlData : ISubContentImageUrl
|
public record SubContentImageUrlData : ISubContentImageUrl
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public string Url { get; init; } = string.Empty;
|
public string Url { get; set; } = string.Empty;
|
||||||
}
|
}
|
||||||
@@ -12,6 +12,15 @@ namespace AIStudio.Provider.OpenAI;
|
|||||||
/// </remarks>
|
/// </remarks>
|
||||||
public record SubContentImageUrlNested(SubContentType Type, ISubContentImageUrl ImageUrl) : ISubContent
|
public record SubContentImageUrlNested(SubContentType Type, ISubContentImageUrl ImageUrl) : ISubContent
|
||||||
{
|
{
|
||||||
|
//
|
||||||
|
// The properties of the parameters, declared again to be settable. The source generation of System.Text.Json
|
||||||
|
// sets every init-only property while it creates the object, also one missing in the JSON, so its value from
|
||||||
|
// the parameterless constructor would get lost. Keep them first and in the order of the parameters, which is
|
||||||
|
// the order they get written in:
|
||||||
|
//
|
||||||
|
public SubContentType Type { get; set; } = Type;
|
||||||
|
public ISubContentImageUrl ImageUrl { get; set; } = ImageUrl;
|
||||||
|
|
||||||
public SubContentImageUrlNested() : this(SubContentType.IMAGE_URL, new SubContentImageUrlData())
|
public SubContentImageUrlNested() : this(SubContentType.IMAGE_URL, new SubContentImageUrlData())
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,15 @@ namespace AIStudio.Provider.OpenAI;
|
|||||||
/// </remarks>
|
/// </remarks>
|
||||||
public record SubContentInputImage(SubContentType Type, string ImageUrl) : ISubContent
|
public record SubContentInputImage(SubContentType Type, string ImageUrl) : ISubContent
|
||||||
{
|
{
|
||||||
|
//
|
||||||
|
// The properties of the parameters, declared again to be settable. The source generation of System.Text.Json
|
||||||
|
// sets every init-only property while it creates the object, also one missing in the JSON, so its value from
|
||||||
|
// the parameterless constructor would get lost. Keep them first and in the order of the parameters, which is
|
||||||
|
// the order they get written in:
|
||||||
|
//
|
||||||
|
public SubContentType Type { get; set; } = Type;
|
||||||
|
public string ImageUrl { get; set; } = ImageUrl;
|
||||||
|
|
||||||
public SubContentInputImage() : this(SubContentType.INPUT_IMAGE, string.Empty)
|
public SubContentInputImage() : this(SubContentType.INPUT_IMAGE, string.Empty)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,15 @@ namespace AIStudio.Provider.OpenAI;
|
|||||||
/// </remarks>
|
/// </remarks>
|
||||||
public record SubContentInputText(SubContentType Type, string Text) : ISubContent
|
public record SubContentInputText(SubContentType Type, string Text) : ISubContent
|
||||||
{
|
{
|
||||||
|
//
|
||||||
|
// The properties of the parameters, declared again to be settable. The source generation of System.Text.Json
|
||||||
|
// sets every init-only property while it creates the object, also one missing in the JSON, so its value from
|
||||||
|
// the parameterless constructor would get lost. Keep them first and in the order of the parameters, which is
|
||||||
|
// the order they get written in:
|
||||||
|
//
|
||||||
|
public SubContentType Type { get; set; } = Type;
|
||||||
|
public string Text { get; set; } = Text;
|
||||||
|
|
||||||
public SubContentInputText() : this(SubContentType.INPUT_TEXT, string.Empty)
|
public SubContentInputText() : this(SubContentType.INPUT_TEXT, string.Empty)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,15 @@ namespace AIStudio.Provider.OpenAI;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public record SubContentText(SubContentType Type, string Text) : ISubContent
|
public record SubContentText(SubContentType Type, string Text) : ISubContent
|
||||||
{
|
{
|
||||||
|
//
|
||||||
|
// The properties of the parameters, declared again to be settable. The source generation of System.Text.Json
|
||||||
|
// sets every init-only property while it creates the object, also one missing in the JSON, so its value from
|
||||||
|
// the parameterless constructor would get lost. Keep them first and in the order of the parameters, which is
|
||||||
|
// the order they get written in:
|
||||||
|
//
|
||||||
|
public SubContentType Type { get; set; } = Type;
|
||||||
|
public string Text { get; set; } = Text;
|
||||||
|
|
||||||
public SubContentText() : this(SubContentType.TEXT, string.Empty)
|
public SubContentText() : this(SubContentType.TEXT, string.Empty)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,15 @@ namespace AIStudio.Provider.OpenAI;
|
|||||||
/// <param name="Role">The role of the message.</param>
|
/// <param name="Role">The role of the message.</param>
|
||||||
public record TextMessage(string Content, string Role) : IMessage<string>
|
public record TextMessage(string Content, string Role) : IMessage<string>
|
||||||
{
|
{
|
||||||
|
//
|
||||||
|
// The properties of the parameters, declared again to be settable. The source generation of System.Text.Json
|
||||||
|
// sets every init-only property while it creates the object, also one missing in the JSON, so its value from
|
||||||
|
// the parameterless constructor would get lost. Keep them first and in the order of the parameters, which is
|
||||||
|
// the order they get written in:
|
||||||
|
//
|
||||||
|
public string Content { get; set; } = Content;
|
||||||
|
public string Role { get; set; } = Role;
|
||||||
|
|
||||||
public TextMessage() : this(string.Empty, string.Empty)
|
public TextMessage() : this(string.Empty, string.Empty)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ namespace AIStudio.Provider.OpenAI;
|
|||||||
|
|
||||||
public sealed record ToolResultMessage : IMessage<string>
|
public sealed record ToolResultMessage : IMessage<string>
|
||||||
{
|
{
|
||||||
public string Role { get; init; } = "tool";
|
public string Role { get; set; } = "tool";
|
||||||
|
|
||||||
public string Content { get; init; } = string.Empty;
|
public string Content { get; set; } = string.Empty;
|
||||||
|
|
||||||
public string ToolCallId { get; init; } = string.Empty;
|
public string ToolCallId { get; set; } = string.Empty;
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
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 AnthropicDelta = AIStudio.Provider.Anthropic.Delta;
|
||||||
|
using AnthropicImageUrl = AIStudio.Provider.Anthropic.SubContentImageUrl;
|
||||||
|
using AnthropicResponseStreamLine = AIStudio.Provider.Anthropic.ResponseStreamLine;
|
||||||
|
using FireworksChoice = AIStudio.Provider.Fireworks.Choice;
|
||||||
|
using FireworksDelta = AIStudio.Provider.Fireworks.Delta;
|
||||||
|
using FireworksResponseStreamLine = AIStudio.Provider.Fireworks.ResponseStreamLine;
|
||||||
|
using HuggingFaceModelsResponse = AIStudio.Provider.HuggingFace.ModelsResponse;
|
||||||
|
using MistralModel = AIStudio.Provider.Mistral.Model;
|
||||||
|
using MistralModelsResponse = AIStudio.Provider.Mistral.ModelsResponse;
|
||||||
|
using OpenAIImageUrl = AIStudio.Provider.OpenAI.SubContentImageUrl;
|
||||||
|
using OpenAIModelsResponse = AIStudio.Provider.OpenAI.ModelsResponse;
|
||||||
|
using PerplexityChoice = AIStudio.Provider.Perplexity.Choice;
|
||||||
|
using PerplexityDelta = AIStudio.Provider.Perplexity.Delta;
|
||||||
|
using PerplexityResponseStreamLine = AIStudio.Provider.Perplexity.ResponseStreamLine;
|
||||||
|
using SelfHostedModel = AIStudio.Provider.SelfHosted.Model;
|
||||||
|
using SelfHostedModelsResponse = AIStudio.Provider.SelfHosted.ModelsResponse;
|
||||||
|
|
||||||
|
namespace AIStudio.Provider;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The contracts with which System.Text.Json reads and writes the requests and answers of LLM providers, built
|
||||||
|
/// at compile time.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Some types System.Text.Json meets only while it runs, since no property names them: the messages and their
|
||||||
|
/// parts, which converters write as the type they are of; the annotations, which a converter reads as the type
|
||||||
|
/// their JSON tells; and whatever a request holds as an object, i.e., the values of the additional API
|
||||||
|
/// parameters, the tools, and the items of an earlier answer. A test fails for such a type which is missing
|
||||||
|
/// here.<br/><br/>
|
||||||
|
///
|
||||||
|
/// Two types with the same name need a name of their own for their contract, else the generator builds only
|
||||||
|
/// the first of them and warns with SYSLIB1031.
|
||||||
|
/// </remarks>
|
||||||
|
[JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Metadata)]
|
||||||
|
|
||||||
|
// The requests:
|
||||||
|
[JsonSerializable(typeof(ChatCompletionAPIRequest))]
|
||||||
|
[JsonSerializable(typeof(ResponsesAPIRequest))]
|
||||||
|
[JsonSerializable(typeof(ChatRequest))]
|
||||||
|
[JsonSerializable(typeof(EmbeddingRequest))]
|
||||||
|
[JsonSerializable(typeof(GoogleEmbeddingRequest))]
|
||||||
|
|
||||||
|
// The messages and their parts:
|
||||||
|
[JsonSerializable(typeof(TextMessage))]
|
||||||
|
[JsonSerializable(typeof(MultimodalMessage))]
|
||||||
|
[JsonSerializable(typeof(AssistantToolCallMessage))]
|
||||||
|
[JsonSerializable(typeof(ToolResultMessage))]
|
||||||
|
[JsonSerializable(typeof(AnthropicMessage))]
|
||||||
|
[JsonSerializable(typeof(AnthropicToolResultMessage))]
|
||||||
|
[JsonSerializable(typeof(SubContentText))]
|
||||||
|
[JsonSerializable(typeof(OpenAIImageUrl), TypeInfoPropertyName = "OpenAISubContentImageUrl")]
|
||||||
|
[JsonSerializable(typeof(SubContentImageUrlNested))]
|
||||||
|
[JsonSerializable(typeof(SubContentImageUrlData))]
|
||||||
|
[JsonSerializable(typeof(SubContentInputText))]
|
||||||
|
[JsonSerializable(typeof(SubContentInputImage))]
|
||||||
|
[JsonSerializable(typeof(SubContentImage))]
|
||||||
|
[JsonSerializable(typeof(SubContentBase64Image))]
|
||||||
|
[JsonSerializable(typeof(AnthropicImageUrl), TypeInfoPropertyName = "AnthropicSubContentImageUrl")]
|
||||||
|
|
||||||
|
// What a request holds as an object:
|
||||||
|
[JsonSerializable(typeof(string))]
|
||||||
|
[JsonSerializable(typeof(int))]
|
||||||
|
[JsonSerializable(typeof(long))]
|
||||||
|
[JsonSerializable(typeof(double))]
|
||||||
|
[JsonSerializable(typeof(decimal))]
|
||||||
|
[JsonSerializable(typeof(bool))]
|
||||||
|
[JsonSerializable(typeof(Dictionary<string, object>))]
|
||||||
|
[JsonSerializable(typeof(List<object>))]
|
||||||
|
[JsonSerializable(typeof(JsonElement))]
|
||||||
|
[JsonSerializable(typeof(ChatCompletionFunctionTool))]
|
||||||
|
[JsonSerializable(typeof(ResponsesFunctionTool))]
|
||||||
|
[JsonSerializable(typeof(ResponsesFunctionCallOutputItem))]
|
||||||
|
|
||||||
|
// The lists of models:
|
||||||
|
[JsonSerializable(typeof(OpenAIModelsResponse), TypeInfoPropertyName = "OpenAIModelsResponse")]
|
||||||
|
[JsonSerializable(typeof(MistralModelsResponse), TypeInfoPropertyName = "MistralModelsResponse")]
|
||||||
|
[JsonSerializable(typeof(SelfHostedModelsResponse), TypeInfoPropertyName = "SelfHostedModelsResponse")]
|
||||||
|
[JsonSerializable(typeof(HuggingFaceModelsResponse), TypeInfoPropertyName = "HuggingFaceModelsResponse")]
|
||||||
|
[JsonSerializable(typeof(IList<HubModel>))]
|
||||||
|
[JsonSerializable(typeof(RequestyModelsResponse))]
|
||||||
|
[JsonSerializable(typeof(OpenRouterModelsResponse))]
|
||||||
|
[JsonSerializable(typeof(GroqModelsResponse))]
|
||||||
|
|
||||||
|
// The lines of streamed answers, with the annotations:
|
||||||
|
[JsonSerializable(typeof(ChatCompletionDeltaStreamLine))]
|
||||||
|
[JsonSerializable(typeof(ChatCompletionAnnotationStreamLine))]
|
||||||
|
[JsonSerializable(typeof(ChatCompletionToolStreamLine))]
|
||||||
|
[JsonSerializable(typeof(ResponsesDeltaStreamLine))]
|
||||||
|
[JsonSerializable(typeof(ResponsesAnnotationStreamLine))]
|
||||||
|
[JsonSerializable(typeof(ResponsesCompletedStreamLine))]
|
||||||
|
[JsonSerializable(typeof(AnthropicResponseStreamLine), TypeInfoPropertyName = "AnthropicResponseStreamLine")]
|
||||||
|
[JsonSerializable(typeof(AnthropicStreamLine))]
|
||||||
|
[JsonSerializable(typeof(FireworksResponseStreamLine), TypeInfoPropertyName = "FireworksResponseStreamLine")]
|
||||||
|
[JsonSerializable(typeof(PerplexityResponseStreamLine), TypeInfoPropertyName = "PerplexityResponseStreamLine")]
|
||||||
|
[JsonSerializable(typeof(AnnotatingUnknown))]
|
||||||
|
[JsonSerializable(typeof(ChatCompletionAnnotatingURL))]
|
||||||
|
[JsonSerializable(typeof(ResponsesAnnotatingUrlCitationData))]
|
||||||
|
|
||||||
|
// Transcriptions and embeddings:
|
||||||
|
[JsonSerializable(typeof(TranscriptionResponse))]
|
||||||
|
[JsonSerializable(typeof(EmbeddingResponse))]
|
||||||
|
[JsonSerializable(typeof(GoogleEmbeddingResponse))]
|
||||||
|
[JsonSerializable(typeof(GoogleEmbedding))]
|
||||||
|
[JsonSerializable(typeof(List<GoogleEmbedding>))]
|
||||||
|
|
||||||
|
// The types inside the answers which share their names with others:
|
||||||
|
[JsonSerializable(typeof(MistralModel), TypeInfoPropertyName = "MistralModel")]
|
||||||
|
[JsonSerializable(typeof(MistralModel[]), TypeInfoPropertyName = "MistralModelArray")]
|
||||||
|
[JsonSerializable(typeof(SelfHostedModel), TypeInfoPropertyName = "SelfHostedModel")]
|
||||||
|
[JsonSerializable(typeof(SelfHostedModel[]), TypeInfoPropertyName = "SelfHostedModelArray")]
|
||||||
|
[JsonSerializable(typeof(FireworksChoice), TypeInfoPropertyName = "FireworksChoice")]
|
||||||
|
[JsonSerializable(typeof(IList<FireworksChoice>), TypeInfoPropertyName = "IListFireworksChoice")]
|
||||||
|
[JsonSerializable(typeof(PerplexityChoice), TypeInfoPropertyName = "PerplexityChoice")]
|
||||||
|
[JsonSerializable(typeof(IList<PerplexityChoice>), TypeInfoPropertyName = "IListPerplexityChoice")]
|
||||||
|
[JsonSerializable(typeof(AnthropicDelta), TypeInfoPropertyName = "AnthropicDelta")]
|
||||||
|
[JsonSerializable(typeof(FireworksDelta), TypeInfoPropertyName = "FireworksDelta")]
|
||||||
|
[JsonSerializable(typeof(PerplexityDelta), TypeInfoPropertyName = "PerplexityDelta")]
|
||||||
|
internal sealed partial class ProviderJsonContext : JsonSerializerContext;
|
||||||
@@ -21,7 +21,7 @@ public static class ProviderJsonOptions
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The shared options.
|
/// The shared options.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static readonly JsonSerializerOptions OPTIONS = new()
|
public static readonly JsonSerializerOptions OPTIONS = new JsonSerializerOptions
|
||||||
{
|
{
|
||||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||||
Converters =
|
Converters =
|
||||||
@@ -33,6 +33,7 @@ public static class ProviderJsonOptions
|
|||||||
new SubContentImageSourceConverter(),
|
new SubContentImageSourceConverter(),
|
||||||
new SubContentImageUrlConverter(),
|
new SubContentImageUrlConverter(),
|
||||||
},
|
},
|
||||||
AllowTrailingCommas = false
|
AllowTrailingCommas = false,
|
||||||
};
|
TypeInfoResolver = ProviderJsonContext.Default,
|
||||||
|
}.AsReadOnly();
|
||||||
}
|
}
|
||||||
@@ -24,19 +24,18 @@ public static class ProviderToolAdapters
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="definition">The tool to describe.</param>
|
/// <param name="definition">The tool to describe.</param>
|
||||||
/// <param name="hostEnforcesStrict">Whether the host binds the model's tool calls to a strict schema. Only then is the tool sent in strict mode.</param>
|
/// <param name="hostEnforcesStrict">Whether the host binds the model's tool calls to a strict schema. Only then is the tool sent in strict mode.</param>
|
||||||
public static object ToChatCompletionTool(ToolDefinition definition, bool hostEnforcesStrict)
|
public static ChatCompletionFunctionTool ToChatCompletionTool(ToolDefinition definition, bool hostEnforcesStrict)
|
||||||
{
|
{
|
||||||
var isStrict = definition.Function.Strict && hostEnforcesStrict;
|
var isStrict = definition.Function.Strict && hostEnforcesStrict;
|
||||||
return new
|
return new()
|
||||||
{
|
{
|
||||||
type = "function",
|
Function = new()
|
||||||
function = new
|
|
||||||
{
|
{
|
||||||
name = definition.Function.Name,
|
Name = definition.Function.Name,
|
||||||
description = definition.Function.DescriptionForLLM,
|
Description = definition.Function.DescriptionForLLM,
|
||||||
parameters = ToOpenAIParameters(definition, isStrict),
|
Parameters = ToOpenAIParameters(definition, isStrict),
|
||||||
strict = isStrict,
|
Strict = isStrict,
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ namespace AIStudio.Tools;
|
|||||||
public sealed class ContentStreamDocumentDetails
|
public sealed class ContentStreamDocumentDetails
|
||||||
{
|
{
|
||||||
[JsonPropertyName("page_number")]
|
[JsonPropertyName("page_number")]
|
||||||
public int? PageNumber { get; init; }
|
public int? PageNumber { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("image")]
|
[JsonPropertyName("image")]
|
||||||
public ContentStreamPptxImageData? Image { get; init; }
|
public ContentStreamPptxImageData? Image { get; set; }
|
||||||
}
|
}
|
||||||
@@ -6,5 +6,5 @@ namespace AIStudio.Tools;
|
|||||||
public sealed class ContentStreamDocumentMetadata : ContentStreamSseMetadata
|
public sealed class ContentStreamDocumentMetadata : ContentStreamSseMetadata
|
||||||
{
|
{
|
||||||
[JsonPropertyName("Document")]
|
[JsonPropertyName("Document")]
|
||||||
public ContentStreamDocumentDetails? Document { get; init; }
|
public ContentStreamDocumentDetails? Document { get; set; }
|
||||||
}
|
}
|
||||||
@@ -7,23 +7,23 @@ namespace AIStudio.Tools;
|
|||||||
public sealed class ContentStreamErrorDetails
|
public sealed class ContentStreamErrorDetails
|
||||||
{
|
{
|
||||||
[JsonPropertyName("code")]
|
[JsonPropertyName("code")]
|
||||||
public string? Code { get; init; }
|
public string? Code { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("message")]
|
[JsonPropertyName("message")]
|
||||||
public string? Message { get; init; }
|
public string? Message { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The page the failure belongs to, when the failure affects a single page only.
|
/// The page the failure belongs to, when the failure affects a single page only.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonPropertyName("page_number")]
|
[JsonPropertyName("page_number")]
|
||||||
public int? PageNumber { get; init; }
|
public int? PageNumber { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The format the runtime identified by looking at the content, e.g. when it contradicts the
|
/// The format the runtime identified by looking at the content, e.g. when it contradicts the
|
||||||
/// file extension.
|
/// file extension.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonPropertyName("detected_format")]
|
[JsonPropertyName("detected_format")]
|
||||||
public string? DetectedFormat { get; init; }
|
public string? DetectedFormat { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the parsed error code.
|
/// Gets the parsed error code.
|
||||||
|
|||||||
@@ -7,5 +7,5 @@ namespace AIStudio.Tools;
|
|||||||
public sealed class ContentStreamErrorMetadata : ContentStreamSseMetadata
|
public sealed class ContentStreamErrorMetadata : ContentStreamSseMetadata
|
||||||
{
|
{
|
||||||
[JsonPropertyName("Error")]
|
[JsonPropertyName("Error")]
|
||||||
public ContentStreamErrorDetails? Error { get; init; }
|
public ContentStreamErrorDetails? Error { get; set; }
|
||||||
}
|
}
|
||||||
@@ -7,5 +7,5 @@ namespace AIStudio.Tools;
|
|||||||
public sealed class ContentStreamPdfDetails
|
public sealed class ContentStreamPdfDetails
|
||||||
{
|
{
|
||||||
[JsonPropertyName("page_number")]
|
[JsonPropertyName("page_number")]
|
||||||
public int? PageNumber { get; init; }
|
public int? PageNumber { get; set; }
|
||||||
}
|
}
|
||||||
@@ -7,5 +7,5 @@ namespace AIStudio.Tools;
|
|||||||
public sealed class ContentStreamPdfMetadata : ContentStreamSseMetadata
|
public sealed class ContentStreamPdfMetadata : ContentStreamSseMetadata
|
||||||
{
|
{
|
||||||
[JsonPropertyName("Pdf")]
|
[JsonPropertyName("Pdf")]
|
||||||
public ContentStreamPdfDetails? Pdf { get; init; }
|
public ContentStreamPdfDetails? Pdf { get; set; }
|
||||||
}
|
}
|
||||||
@@ -5,17 +5,17 @@ namespace AIStudio.Tools;
|
|||||||
public sealed class ContentStreamPptxImageData
|
public sealed class ContentStreamPptxImageData
|
||||||
{
|
{
|
||||||
[JsonPropertyName("id")]
|
[JsonPropertyName("id")]
|
||||||
public string? Id { get; init; }
|
public string? Id { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("content")]
|
[JsonPropertyName("content")]
|
||||||
public string? Content { get; init; }
|
public string? Content { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("segment")]
|
[JsonPropertyName("segment")]
|
||||||
public int? Segment { get; init; }
|
public int? Segment { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("is_end")]
|
[JsonPropertyName("is_end")]
|
||||||
public bool IsEnd { get; init; }
|
public bool IsEnd { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("media_type")]
|
[JsonPropertyName("media_type")]
|
||||||
public string? MediaType { get; init; }
|
public string? MediaType { get; set; }
|
||||||
}
|
}
|
||||||
@@ -7,8 +7,8 @@ namespace AIStudio.Tools;
|
|||||||
public sealed class ContentStreamPresentationDetails
|
public sealed class ContentStreamPresentationDetails
|
||||||
{
|
{
|
||||||
[JsonPropertyName("slide_number")]
|
[JsonPropertyName("slide_number")]
|
||||||
public int? SlideNumber { get; init; }
|
public int? SlideNumber { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("image")]
|
[JsonPropertyName("image")]
|
||||||
public ContentStreamPptxImageData? Image { get; init; }
|
public ContentStreamPptxImageData? Image { get; set; }
|
||||||
}
|
}
|
||||||
@@ -7,5 +7,5 @@ namespace AIStudio.Tools;
|
|||||||
public sealed class ContentStreamPresentationMetadata : ContentStreamSseMetadata
|
public sealed class ContentStreamPresentationMetadata : ContentStreamSseMetadata
|
||||||
{
|
{
|
||||||
[JsonPropertyName("Presentation")]
|
[JsonPropertyName("Presentation")]
|
||||||
public ContentStreamPresentationDetails? Presentation { get; init; }
|
public ContentStreamPresentationDetails? Presentation { get; set; }
|
||||||
}
|
}
|
||||||
@@ -17,12 +17,12 @@ namespace AIStudio.Tools;
|
|||||||
public sealed class ContentStreamPromptInjectionDetails
|
public sealed class ContentStreamPromptInjectionDetails
|
||||||
{
|
{
|
||||||
[JsonPropertyName("findings")]
|
[JsonPropertyName("findings")]
|
||||||
public List<PromptInjectionFinding>? Findings { get; init; }
|
public List<PromptInjectionFinding>? Findings { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// How many passages were filtered. Can exceed the number of findings, because the runtime
|
/// How many passages were filtered. Can exceed the number of findings, because the runtime
|
||||||
/// caps how many it reports in detail while it filters every single one.
|
/// caps how many it reports in detail while it filters every single one.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonPropertyName("redacted_count")]
|
[JsonPropertyName("redacted_count")]
|
||||||
public int RedactedCount { get; init; }
|
public int RedactedCount { get; set; }
|
||||||
}
|
}
|
||||||
@@ -7,5 +7,5 @@ namespace AIStudio.Tools;
|
|||||||
public sealed class ContentStreamPromptInjectionMetadata : ContentStreamSseMetadata
|
public sealed class ContentStreamPromptInjectionMetadata : ContentStreamSseMetadata
|
||||||
{
|
{
|
||||||
[JsonPropertyName("PromptInjection")]
|
[JsonPropertyName("PromptInjection")]
|
||||||
public ContentStreamPromptInjectionDetails? PromptInjection { get; init; }
|
public ContentStreamPromptInjectionDetails? PromptInjection { get; set; }
|
||||||
}
|
}
|
||||||
@@ -7,8 +7,8 @@ namespace AIStudio.Tools;
|
|||||||
public sealed class ContentStreamSpreadsheetDetails
|
public sealed class ContentStreamSpreadsheetDetails
|
||||||
{
|
{
|
||||||
[JsonPropertyName("sheet_name")]
|
[JsonPropertyName("sheet_name")]
|
||||||
public string? SheetName { get; init; }
|
public string? SheetName { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("row_number")]
|
[JsonPropertyName("row_number")]
|
||||||
public int? RowNumber { get; init; }
|
public int? RowNumber { get; set; }
|
||||||
}
|
}
|
||||||
@@ -7,5 +7,5 @@ namespace AIStudio.Tools;
|
|||||||
public sealed class ContentStreamSpreadsheetMetadata : ContentStreamSseMetadata
|
public sealed class ContentStreamSpreadsheetMetadata : ContentStreamSseMetadata
|
||||||
{
|
{
|
||||||
[JsonPropertyName("Spreadsheet")]
|
[JsonPropertyName("Spreadsheet")]
|
||||||
public ContentStreamSpreadsheetDetails? Spreadsheet { get; init; }
|
public ContentStreamSpreadsheetDetails? Spreadsheet { get; set; }
|
||||||
}
|
}
|
||||||
@@ -5,14 +5,14 @@ namespace AIStudio.Tools;
|
|||||||
public sealed class ContentStreamSseEvent
|
public sealed class ContentStreamSseEvent
|
||||||
{
|
{
|
||||||
[JsonPropertyName("content")]
|
[JsonPropertyName("content")]
|
||||||
public string? Content { get; init; }
|
public string? Content { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("stream_id")]
|
[JsonPropertyName("stream_id")]
|
||||||
public string? StreamId { get; init; }
|
public string? StreamId { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("metadata")]
|
[JsonPropertyName("metadata")]
|
||||||
public ContentStreamSseMetadata? Metadata { get; init; }
|
public ContentStreamSseMetadata? Metadata { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("token_count")]
|
[JsonPropertyName("token_count")]
|
||||||
public int? TokenCount { get; init; }
|
public int? TokenCount { get; set; }
|
||||||
}
|
}
|
||||||
@@ -6,5 +6,5 @@ namespace AIStudio.Tools;
|
|||||||
public sealed class ContentStreamTextDetails
|
public sealed class ContentStreamTextDetails
|
||||||
{
|
{
|
||||||
[JsonPropertyName("line_number")]
|
[JsonPropertyName("line_number")]
|
||||||
public int? LineNumber { get; init; }
|
public int? LineNumber { get; set; }
|
||||||
}
|
}
|
||||||
@@ -6,5 +6,5 @@ namespace AIStudio.Tools;
|
|||||||
public sealed class ContentStreamTextMetadata : ContentStreamSseMetadata
|
public sealed class ContentStreamTextMetadata : ContentStreamSseMetadata
|
||||||
{
|
{
|
||||||
[JsonPropertyName("Text")]
|
[JsonPropertyName("Text")]
|
||||||
public ContentStreamTextDetails? Text { get; init; }
|
public ContentStreamTextDetails? Text { get; set; }
|
||||||
}
|
}
|
||||||
+18
-14
@@ -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
|
||||||
}
|
}
|
||||||
@@ -27,7 +27,7 @@ public sealed record VectorSearchResult(
|
|||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// The vector store keeps only the pages of a chunk, cf. LocalIndexSearchService.SearchVectorsAsync.
|
/// The vector store keeps only the pages of a chunk, cf. LocalIndexSearchService.SearchVectorsAsync.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public ChunkLocation? StoredLocation { get; init; }
|
public ChunkLocation? StoredLocation { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Where the chunk is: what the index store keeps, or the pages alone when it could not say.
|
/// Where the chunk is: what the index store keeps, or the pages alone when it could not say.
|
||||||
|
|||||||
@@ -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 JsonSerializerOptions
|
||||||
{
|
{
|
||||||
WriteIndented = true,
|
WriteIndented = true,
|
||||||
AllowTrailingCommas = true,
|
AllowTrailingCommas = true,
|
||||||
@@ -20,8 +20,9 @@ public abstract class ERIClientBase(IERIDataSource dataSource) : IDisposable
|
|||||||
Converters =
|
Converters =
|
||||||
{
|
{
|
||||||
new JsonStringEnumConverter(JsonNamingPolicy.SnakeCaseUpper),
|
new JsonStringEnumConverter(JsonNamingPolicy.SnakeCaseUpper),
|
||||||
}
|
},
|
||||||
};
|
TypeInfoResolver = ERIClientJsonContext.Default,
|
||||||
|
}.AsReadOnly();
|
||||||
|
|
||||||
protected readonly HttpClient HttpClient = ExternalHttpClientTimeout.CreateHttpClient(new Uri($"{dataSource.Hostname}:{dataSource.Port}"), ExternalHttpTrustPolicy.ALLOW_CUSTOM_ROOTS_WHEN_HOST_WHITELISTED);
|
protected readonly HttpClient HttpClient = ExternalHttpClientTimeout.CreateHttpClient(new Uri($"{dataSource.Hostname}:{dataSource.Port}"), ExternalHttpTrustPolicy.ALLOW_CUSTOM_ROOTS_WHEN_HOST_WHITELISTED);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
using AIStudio.Tools.ERIClient.DataModel;
|
||||||
|
|
||||||
|
namespace AIStudio.Tools.ERIClient;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The contracts with which System.Text.Json reads and writes the calls to ERI servers, built at compile time.
|
||||||
|
/// </summary>
|
||||||
|
[JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Metadata)]
|
||||||
|
|
||||||
|
// The request:
|
||||||
|
[JsonSerializable(typeof(RetrievalRequest))]
|
||||||
|
|
||||||
|
// The answers:
|
||||||
|
[JsonSerializable(typeof(List<AuthScheme>))]
|
||||||
|
[JsonSerializable(typeof(AuthResponse))]
|
||||||
|
[JsonSerializable(typeof(DataSourceInfo))]
|
||||||
|
[JsonSerializable(typeof(List<EmbeddingInfo>))]
|
||||||
|
[JsonSerializable(typeof(List<RetrievalInfo>))]
|
||||||
|
[JsonSerializable(typeof(List<Context>))]
|
||||||
|
[JsonSerializable(typeof(SecurityRequirements))]
|
||||||
|
internal sealed partial class ERIClientJsonContext : JsonSerializerContext;
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace AIStudio.Tools.Rust;
|
||||||
|
|
||||||
|
/// <summary>Request body used to count the tokens of a text in the Rust runtime.</summary>
|
||||||
|
/// <param name="Text">The text to count.</param>
|
||||||
|
/// <param name="TokenizerPath">Path of the tokenizer file to count with.</param>
|
||||||
|
public sealed record CountTokensRequest(string Text, string TokenizerPath);
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
namespace AIStudio.Tools.Rust;
|
||||||
|
|
||||||
|
/// <summary>Request body used to delete the stored tokenizer of a model in the Rust runtime.</summary>
|
||||||
|
/// <param name="ModelId">The model whose tokenizer gets deleted.</param>
|
||||||
|
public sealed record DeleteTokenizerRequest(string ModelId);
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace AIStudio.Tools.Rust;
|
||||||
|
|
||||||
|
/// <summary>Request body used to prepare a local image for a model in the Rust runtime.</summary>
|
||||||
|
/// <param name="Path">Absolute path of the image.</param>
|
||||||
|
/// <param name="MaxBytes">The size the prepared image may have at most.</param>
|
||||||
|
public sealed record PrepareImageForModelRequest(string Path, long MaxBytes);
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace AIStudio.Tools.Rust;
|
||||||
|
|
||||||
|
/// <summary>Request body used to check and optionally optimize a local image in the Rust runtime.</summary>
|
||||||
|
/// <param name="Path">Absolute path of the image.</param>
|
||||||
|
/// <param name="Optimize">Whether the maximum-edge policy and re-encoding are applied.</param>
|
||||||
|
public sealed record PrepareImageRequest(string Path, bool Optimize);
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace AIStudio.Tools.Rust;
|
||||||
|
|
||||||
|
/// <summary>Request body used to store a copy of a tokenizer file for a model in the Rust runtime.</summary>
|
||||||
|
/// <param name="ModelId">The model the tokenizer belongs to.</param>
|
||||||
|
/// <param name="FilePath">Absolute path of the tokenizer file.</param>
|
||||||
|
public sealed record StoreTokenizerRequest(string ModelId, string FilePath);
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
namespace AIStudio.Tools.Rust;
|
||||||
|
|
||||||
|
/// <summary>Request body used to check a tokenizer file in the Rust runtime.</summary>
|
||||||
|
/// <param name="FilePath">Absolute path of the tokenizer file.</param>
|
||||||
|
public sealed record ValidateTokenizerRequest(string FilePath);
|
||||||
@@ -15,17 +15,17 @@ public sealed record PromptInjectionFinding
|
|||||||
/// Which rule matched, e.g. "instruction_override".
|
/// Which rule matched, e.g. "instruction_override".
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonPropertyName("rule_id")]
|
[JsonPropertyName("rule_id")]
|
||||||
public string RuleId { get; init; } = string.Empty;
|
public string RuleId { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The rule's family, e.g. "exfiltration".
|
/// The rule's family, e.g. "exfiltration".
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonPropertyName("category")]
|
[JsonPropertyName("category")]
|
||||||
public PromptInjectionFindingCategory Category { get; init; } = PromptInjectionFindingCategory.UNKNOWN;
|
public PromptInjectionFindingCategory Category { get; set; } = PromptInjectionFindingCategory.UNKNOWN;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The passage as it appeared in the content, so the user can see what was removed.
|
/// The passage as it appeared in the content, so the user can see what was removed.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonPropertyName("snippet")]
|
[JsonPropertyName("snippet")]
|
||||||
public string Snippet { get; init; } = string.Empty;
|
public string Snippet { get; set; } = string.Empty;
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace AIStudio.Tools.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The contracts with which System.Text.Json reads the events of a file extraction from the runtime, built at
|
||||||
|
/// compile time.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// A converter reads the metadata of an event as the type its JSON tells, so each of these types needs a
|
||||||
|
/// contract of its own here. A test fails for one which is missing.
|
||||||
|
/// </remarks>
|
||||||
|
[JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Metadata)]
|
||||||
|
[JsonSerializable(typeof(ContentStreamSseEvent))]
|
||||||
|
[JsonSerializable(typeof(string))]
|
||||||
|
[JsonSerializable(typeof(ContentStreamTextMetadata))]
|
||||||
|
[JsonSerializable(typeof(ContentStreamPdfMetadata))]
|
||||||
|
[JsonSerializable(typeof(ContentStreamSpreadsheetMetadata))]
|
||||||
|
[JsonSerializable(typeof(ContentStreamPresentationMetadata))]
|
||||||
|
[JsonSerializable(typeof(ContentStreamImageMetadata))]
|
||||||
|
[JsonSerializable(typeof(ContentStreamDocumentMetadata))]
|
||||||
|
[JsonSerializable(typeof(ContentStreamErrorMetadata))]
|
||||||
|
[JsonSerializable(typeof(ContentStreamPromptInjectionMetadata))]
|
||||||
|
internal sealed partial class RustContentStreamJsonContext : JsonSerializerContext;
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
using AIStudio.Tools.Databases.VectorStore;
|
||||||
|
using AIStudio.Tools.Rust;
|
||||||
|
|
||||||
|
namespace AIStudio.Tools.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The contracts with which System.Text.Json reads and writes nearly every call to the runtime, built at
|
||||||
|
/// compile time.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The calls to the vector store are generic: every query of its client needs the answer type with the type
|
||||||
|
/// of its result, e.g., DatabaseQueryResponse<bool?>. A test fails for a request or an answer which is
|
||||||
|
/// missing here.
|
||||||
|
/// </remarks>
|
||||||
|
[JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Metadata)]
|
||||||
|
|
||||||
|
// The requests:
|
||||||
|
[JsonSerializable(typeof(SelectSecretRequest))]
|
||||||
|
[JsonSerializable(typeof(StoreSecretRequest))]
|
||||||
|
[JsonSerializable(typeof(RustService.ShareFileRequest))]
|
||||||
|
[JsonSerializable(typeof(PreviousDirectory))]
|
||||||
|
[JsonSerializable(typeof(SelectFileOptions))]
|
||||||
|
[JsonSerializable(typeof(SaveFileOptions))]
|
||||||
|
[JsonSerializable(typeof(OpenPathRequest))]
|
||||||
|
[JsonSerializable(typeof(OpenDocumentRequest))]
|
||||||
|
[JsonSerializable(typeof(RegisterShortcutRequest))]
|
||||||
|
[JsonSerializable(typeof(ValidateShortcutRequest))]
|
||||||
|
[JsonSerializable(typeof(SetInterfaceSizeRequest))]
|
||||||
|
[JsonSerializable(typeof(CreateMediaJobRequest))]
|
||||||
|
[JsonSerializable(typeof(LogEventRequest))]
|
||||||
|
[JsonSerializable(typeof(ValidateTokenizerRequest))]
|
||||||
|
[JsonSerializable(typeof(StoreTokenizerRequest))]
|
||||||
|
[JsonSerializable(typeof(DeleteTokenizerRequest))]
|
||||||
|
[JsonSerializable(typeof(CountTokensRequest))]
|
||||||
|
[JsonSerializable(typeof(PrepareImageRequest))]
|
||||||
|
[JsonSerializable(typeof(PrepareImageForModelRequest))]
|
||||||
|
|
||||||
|
// The answers:
|
||||||
|
[JsonSerializable(typeof(RequestedSecret))]
|
||||||
|
[JsonSerializable(typeof(StoreSecretResponse))]
|
||||||
|
[JsonSerializable(typeof(DeleteSecretResponse))]
|
||||||
|
[JsonSerializable(typeof(ShareFileResponse))]
|
||||||
|
[JsonSerializable(typeof(ImagePrepareResponse))]
|
||||||
|
[JsonSerializable(typeof(PreparedModelImage))]
|
||||||
|
[JsonSerializable(typeof(TauriEvent))]
|
||||||
|
[JsonSerializable(typeof(UpdateResponse))]
|
||||||
|
[JsonSerializable(typeof(SetClipboardResponse))]
|
||||||
|
[JsonSerializable(typeof(DirectorySelectionResponse))]
|
||||||
|
[JsonSerializable(typeof(FileSelectionResponse))]
|
||||||
|
[JsonSerializable(typeof(FilesSelectionResponse))]
|
||||||
|
[JsonSerializable(typeof(FileSaveResponse))]
|
||||||
|
[JsonSerializable(typeof(OpenPathResponse))]
|
||||||
|
[JsonSerializable(typeof(OpenDocumentResponse))]
|
||||||
|
[JsonSerializable(typeof(ShortcutRegistrationResult))]
|
||||||
|
[JsonSerializable(typeof(ShortcutValidationResponse))]
|
||||||
|
[JsonSerializable(typeof(ShortcutResponse))]
|
||||||
|
[JsonSerializable(typeof(RuntimeInfoResponse))]
|
||||||
|
[JsonSerializable(typeof(AppExitResponse))]
|
||||||
|
[JsonSerializable(typeof(AppRestartResponse))]
|
||||||
|
[JsonSerializable(typeof(InterfaceSizeResponse))]
|
||||||
|
[JsonSerializable(typeof(CreateMediaJobResponse))]
|
||||||
|
[JsonSerializable(typeof(MediaJobEvent))]
|
||||||
|
[JsonSerializable(typeof(List<EnterpriseConfig>))]
|
||||||
|
[JsonSerializable(typeof(GetLogPathsResponse))]
|
||||||
|
[JsonSerializable(typeof(TokenizerResponse))]
|
||||||
|
|
||||||
|
// The vector store, with the result of every query its client sends:
|
||||||
|
[JsonSerializable(typeof(QdrantEdgeInfo))]
|
||||||
|
[JsonSerializable(typeof(QdrantEdgeClientImplementation.EnsureVectorStoreRequest))]
|
||||||
|
[JsonSerializable(typeof(QdrantEdgeClientImplementation.CheckVectorStoreRequest))]
|
||||||
|
[JsonSerializable(typeof(QdrantEdgeClientImplementation.InsertEmbeddingRequest))]
|
||||||
|
[JsonSerializable(typeof(QdrantEdgeClientImplementation.SearchEmbeddingRequest))]
|
||||||
|
[JsonSerializable(typeof(QdrantEdgeClientImplementation.RetrieveVectorsRequest))]
|
||||||
|
[JsonSerializable(typeof(QdrantEdgeClientImplementation.DeleteEmbeddingByFileRequest))]
|
||||||
|
[JsonSerializable(typeof(QdrantEdgeClientImplementation.RelocateEmbeddingFileRequest))]
|
||||||
|
[JsonSerializable(typeof(QdrantEdgeClientImplementation.OptimizeVectorStoreRequest))]
|
||||||
|
[JsonSerializable(typeof(QdrantEdgeClientImplementation.DeleteVectorStoreRequest))]
|
||||||
|
[JsonSerializable(typeof(QdrantEdgeClientImplementation.PrepareBackupRequest))]
|
||||||
|
[JsonSerializable(typeof(QdrantEdgeClientImplementation.CopyStoreForBackupRequest))]
|
||||||
|
[JsonSerializable(typeof(RustService.DatabaseOperationResponse))]
|
||||||
|
[JsonSerializable(typeof(RustService.DatabaseQueryResponse<VectorStoreEnsureResult>))]
|
||||||
|
[JsonSerializable(typeof(RustService.DatabaseQueryResponse<bool?>))]
|
||||||
|
[JsonSerializable(typeof(RustService.DatabaseQueryResponse<List<VectorSearchResult>>))]
|
||||||
|
[JsonSerializable(typeof(RustService.DatabaseQueryResponse<List<StoredVector>>))]
|
||||||
|
[JsonSerializable(typeof(RustService.DatabaseQueryResponse<VectorStoreBackupPreparation>))]
|
||||||
|
[JsonSerializable(typeof(RustService.DatabaseQueryResponse<long?>))]
|
||||||
|
internal sealed partial class RustJsonContext : JsonSerializerContext;
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
using AIStudio.Tools.Rust;
|
||||||
|
|
||||||
|
namespace AIStudio.Tools.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The contracts with which System.Text.Json reads and writes the calls to the prompt injection filter of the
|
||||||
|
/// runtime, built at compile time.
|
||||||
|
/// </summary>
|
||||||
|
[JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Metadata)]
|
||||||
|
[JsonSerializable(typeof(SanitizePromptInjectionsRequest))]
|
||||||
|
[JsonSerializable(typeof(SanitizePromptInjectionsResponse))]
|
||||||
|
[JsonSerializable(typeof(SanitizePromptInjectionsBatchRequest))]
|
||||||
|
[JsonSerializable(typeof(SanitizePromptInjectionsBatchResponse))]
|
||||||
|
internal sealed partial class RustPromptInjectionJsonContext : JsonSerializerContext;
|
||||||
@@ -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 PrepareImageRequest(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 PrepareImageForModelRequest(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;
|
||||||
|
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ public sealed partial class RustService
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var sseEvent = JsonSerializer.Deserialize<ContentStreamSseEvent>(jsonContent);
|
var sseEvent = JsonSerializer.Deserialize<ContentStreamSseEvent>(jsonContent, CONTENT_STREAM_JSON_OPTIONS);
|
||||||
if (sseEvent is null)
|
if (sseEvent is null)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
@@ -364,7 +364,7 @@ public sealed partial class RustService
|
|||||||
ContentStreamSseEvent? sseEvent = null;
|
ContentStreamSseEvent? sseEvent = null;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
sseEvent = JsonSerializer.Deserialize<ContentStreamSseEvent>(jsonContent);
|
sseEvent = JsonSerializer.Deserialize<ContentStreamSseEvent>(jsonContent, CONTENT_STREAM_JSON_OPTIONS);
|
||||||
}
|
}
|
||||||
catch (JsonException)
|
catch (JsonException)
|
||||||
{
|
{
|
||||||
@@ -460,7 +460,7 @@ public sealed partial class RustService
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var errorMessage = JsonSerializer.Deserialize<string>(jsonContent);
|
var errorMessage = JsonSerializer.Deserialize<string>(jsonContent, CONTENT_STREAM_JSON_OPTIONS);
|
||||||
if (string.IsNullOrWhiteSpace(errorMessage))
|
if (string.IsNullOrWhiteSpace(errorMessage))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
|||||||
@@ -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}'");
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,8 @@ public sealed partial class RustService
|
|||||||
using var response = await this.http.PostAsJsonAsync(
|
using var response = await this.http.PostAsJsonAsync(
|
||||||
"/security/prompt-injection/sanitize",
|
"/security/prompt-injection/sanitize",
|
||||||
new SanitizePromptInjectionsRequest(text),
|
new SanitizePromptInjectionsRequest(text),
|
||||||
cancellationToken: timeoutTokenSource.Token);
|
PROMPT_INJECTION_JSON_OPTIONS,
|
||||||
|
timeoutTokenSource.Token);
|
||||||
|
|
||||||
if (!response.IsSuccessStatusCode)
|
if (!response.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
@@ -48,7 +49,7 @@ public sealed partial class RustService
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return await response.Content.ReadFromJsonAsync<SanitizePromptInjectionsResponse>(timeoutTokenSource.Token);
|
return await response.Content.ReadFromJsonAsync<SanitizePromptInjectionsResponse>(PROMPT_INJECTION_JSON_OPTIONS, timeoutTokenSource.Token);
|
||||||
}
|
}
|
||||||
catch (Exception exception)
|
catch (Exception exception)
|
||||||
{
|
{
|
||||||
@@ -82,7 +83,8 @@ public sealed partial class RustService
|
|||||||
using var response = await this.http.PostAsJsonAsync(
|
using var response = await this.http.PostAsJsonAsync(
|
||||||
"/security/prompt-injection/sanitize-batch",
|
"/security/prompt-injection/sanitize-batch",
|
||||||
new SanitizePromptInjectionsBatchRequest(texts),
|
new SanitizePromptInjectionsBatchRequest(texts),
|
||||||
cancellationToken: timeoutTokenSource.Token);
|
PROMPT_INJECTION_JSON_OPTIONS,
|
||||||
|
timeoutTokenSource.Token);
|
||||||
|
|
||||||
if (!response.IsSuccessStatusCode)
|
if (!response.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
@@ -90,7 +92,7 @@ public sealed partial class RustService
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var batchResponse = await response.Content.ReadFromJsonAsync<SanitizePromptInjectionsBatchResponse>(timeoutTokenSource.Token);
|
var batchResponse = await response.Content.ReadFromJsonAsync<SanitizePromptInjectionsBatchResponse>(PROMPT_INJECTION_JSON_OPTIONS, timeoutTokenSource.Token);
|
||||||
if (batchResponse.Results is null || batchResponse.Results.Count != texts.Count)
|
if (batchResponse.Results is null || batchResponse.Results.Count != texts.Count)
|
||||||
{
|
{
|
||||||
this.logger?.LogError("The prompt injection filter answered with {ResultCount} result(s) for {TextCount} text(s).", batchResponse.Results?.Count ?? 0, texts.Count);
|
this.logger?.LogError("The prompt injection filter answered with {ResultCount} result(s) for {TextCount} text(s).", batchResponse.Results?.Count ?? 0, texts.Count);
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
Loaded 100 of 140 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user