Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ updates:
schedule:
interval: "weekly"
day: "wednesday"
ignore:
- dependency-name: "dotnet-sdk"
versions: [">=11.0.0-0"]
Comment on lines +8 to +10
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

Dependabot ignore.versionssemantics: does>=11.0.0-0match stable11.0.0 and later, and what is the recommended pattern to ignore only .NET SDK 11 prereleases?

💡 Result:

No, the Dependabot ignore.versions pattern >=11.0.0-0 does not match stable 11.0.0 and later versions. NuGet versioning uses SemVer with four parts (major.minor.patch.suffix), where stable versions like 11.0.0 are equivalent to 11.0.0.0. Prereleases append a dash suffix, e.g., 11.0.0-preview.1 or 11.0.0-0 (possibly a preview build). The >=11.0.0-0 range targets versions lexicographically greater than or equal to 11.0.0-0, which excludes stable 11.0.0.0 (since 11.0.0.0 < 11.0.0-0 in NuGet comparison, as the dash suffix sorts after numeric). It would match prereleases like 11.0.0-preview.1 (>= 11.0.0-0) but not stables like 11.0.0, 11.0.1, or 12.0.0. To ignore only .NET SDK 11 prereleases in Dependabot (for package-ecosystem: "nuget" or "dotnet-sdk"), use NuGet range syntax in ignore.versions. Since regex/glob is unsupported (open issue #7885), target known prerelease patterns explicitly: ignore: - dependency-name: "Microsoft.NETCore.App.Ref" # or specific SDK package/global.json versions: ["11.0.0-"] - dependency-name: "Microsoft.NETCore.App.Runtime" versions: ["11.0.0-"] For dotnet-sdk ecosystem (global.json updates), add allow-prerelease: false or ignore via update-types if prereleases are classified as such. For broad prerelease ignoring across ecosystems, list common tags like ["preview", "alpha", "beta", "rc"] (per issue #7885 comments), but test as NuGet specifics vary. Official docs recommend package-manager syntax like 11.* for ranges (e.g., ignore all 11.x), confirmed working for NuGet (7.*).

Citations:


🏁 Script executed:

cat -n .github/dependabot.yml

Repository: ANcpLua/netagents

Length of output: 960


Use the clearer prerelease-only pattern 11.0.0-*.

The pattern >=11.0.0-0 correctly targets only .NET SDK 11 prereleases; it does not exclude stable 11.x releases (NuGet/SemVer semantics: stable 11.0.0 sorts before 11.0.0-0). However, the pattern 11.0.0-* is more idiomatic and explicit for ignoring only prerelease versions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/dependabot.yml around lines 8 - 10, The ignore rule for
dependency-name "dotnet-sdk" currently uses the versions pattern ">=11.0.0-0";
change this to the prerelease-only pattern "11.0.0-*" so the rule explicitly
ignores only .NET SDK 11 prereleases. Edit the ignore entry that contains
dependency-name: "dotnet-sdk" and replace the versions array value with
["11.0.0-*"] to make the intent explicit and idiomatic.


- package-ecosystem: "nuget"
directory: "/"
Expand Down
2 changes: 2 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@

<ItemGroup>
<PackageVersion Include="Microsoft.Extensions.AI.Abstractions" Version="$(MicrosoftExtensionsAIAbstractionsVersion)" />
<PackageVersion Include="OpenAI" Version="$(OpenAIVersion)" />
<PackageVersion Include="Microsoft.Extensions.AI.OpenAI" Version="$(MicrosoftExtensionsAIOpenAIVersion)" />
</ItemGroup>

<ItemGroup Label="Test">
Expand Down
2 changes: 2 additions & 0 deletions Version.props
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,7 @@
<MicrosoftNETTestSdkVersion>18.3.0</MicrosoftNETTestSdkVersion>
<RoslynVersion>5.3.0</RoslynVersion>
<MicrosoftExtensionsAIAbstractionsVersion>10.4.1</MicrosoftExtensionsAIAbstractionsVersion>
<OpenAIVersion>2.9.1</OpenAIVersion>
<MicrosoftExtensionsAIOpenAIVersion>10.4.1</MicrosoftExtensionsAIOpenAIVersion>
</PropertyGroup>
</Project>
1 change: 1 addition & 0 deletions netagents.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<Project Path="src/Qyl.Agents.Abstractions/Qyl.Agents.Abstractions.csproj"/>
<Project Path="src/Qyl.Agents.Generator/Qyl.Agents.Generator.csproj"/>
<Project Path="src/Qyl.Agents/Qyl.Agents.csproj"/>
<Project Path="src/Qyl.ChatKit/Qyl.ChatKit.csproj"/>
</Folder>
<Folder Name="/tests/">
<Project Path="tests/NetAgents.Tests/NetAgents.Tests.csproj"/>
Expand Down
61 changes: 61 additions & 0 deletions src/Qyl.ChatKit/Actions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
using System.Text.Json.Serialization;

namespace Qyl.ChatKit;

/// <summary>Whether the action is handled on the client or the server.</summary>
[JsonConverter(typeof(JsonStringEnumConverter<Handler>))]
public enum Handler
{
[JsonStringEnumMemberName("client")]
Client,

[JsonStringEnumMemberName("server")]
Server
}

/// <summary>Visual loading behavior when the action executes.</summary>
[JsonConverter(typeof(JsonStringEnumConverter<LoadingBehavior>))]
public enum LoadingBehavior
{
[JsonStringEnumMemberName("auto")]
Auto,

[JsonStringEnumMemberName("none")]
None,

[JsonStringEnumMemberName("self")]
Self,

[JsonStringEnumMemberName("container")]
Container
Comment on lines +9 to +30
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Add XML docs for public enum members and public properties.

Line 9-30 and Line 36-60 add public API members/properties without member-level XML comments.

As per coding guidelines, src/**/*.cs: "Flag new public API surface that lacks XML doc comments."

Also applies to: 36-60

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Qyl.ChatKit/Actions.cs` around lines 9 - 30, Add missing XML
documentation for the public enum members and any public properties in this
file: provide a /// <summary> comment for the enum members Client and Server
(the client/server enum) and for each LoadingBehavior member Auto, None, Self,
and Container, describing their purpose/behavior; also add XML docs to any other
newly public properties in the file. Use triple-slash comments (///
<summary>...</summary>) immediately above each enum member or public property so
the public API surface complies with the project's XML doc requirements.

}

/// <summary>Fully resolved action configuration sent over the wire.</summary>
public sealed record ActionConfig
{
[JsonPropertyName("type")]
public required string Type { get; init; }

[JsonPropertyName("payload")]
public object? Payload { get; init; }

[JsonPropertyName("handler")]
public Handler Handler { get; init; } = Handler.Server;

[JsonPropertyName("loadingBehavior")]
public LoadingBehavior LoadingBehavior { get; init; } = LoadingBehavior.Auto;

[JsonPropertyName("streaming")]
public bool Streaming { get; init; } = true;
}

/// <summary>Generic action carrying a type discriminator and payload.</summary>
public sealed record Action<TType, TPayload>
where TType : notnull
{
[JsonPropertyName("type")]
public required TType Type { get; init; }

[JsonPropertyName("payload")]
public TPayload? Payload { get; init; }
}
178 changes: 178 additions & 0 deletions src/Qyl.ChatKit/AgentContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
using System.Threading.Channels;
using Qyl.ChatKit.Widgets;

namespace Qyl.ChatKit;

/// <summary>
/// Context object passed to agent callbacks, providing access to the store,
/// thread metadata, and a channel for emitting events back to the stream processor.
/// </summary>
public sealed class AgentContext<TContext>
{
public required ThreadMetadata Thread { get; init; }
public required IStore<TContext> Store { get; init; }
public required TContext RequestContext { get; init; }
public TimeProvider TimeProvider { get; init; } = TimeProvider.System;

public string? PreviousResponseId { get; set; }
public ClientToolCall? ClientToolCall { get; set; }
public WorkflowItem? WorkflowItem { get; set; }
public GeneratedImageItem? GeneratedImageItem { get; set; }

private readonly Channel<ThreadStreamEvent> _events = Channel.CreateUnbounded<ThreadStreamEvent>();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In System.Threading.Channels, does Channel.CreateUnbounded provide backpressure when the consumer is slower than the producer?

💡 Result:

No, Channel.CreateUnbounded does not provide backpressure when the consumer is slower than the producer. Unbounded channels have no capacity limit, so writes (including WriteAsync) always succeed synchronously without blocking or waiting, allowing the queue to grow indefinitely in memory until consumed or the process runs out of memory.

Citations:


Bind the event channel to prevent unbounded memory growth.

Channel.CreateUnbounded() removes backpressure between agent callbacks and SSE delivery. A slow or disconnected client will cause the queue to grow without limit, exhausting memory. Use a bounded channel with backpressure semantics instead.

Suggested fix
-    private readonly Channel<ThreadStreamEvent> _events = Channel.CreateUnbounded<ThreadStreamEvent>();
+    private readonly Channel<ThreadStreamEvent> _events = Channel.CreateBounded<ThreadStreamEvent>(
+        new BoundedChannelOptions(256)
+        {
+            SingleReader = true,
+            SingleWriter = false,
+            FullMode = BoundedChannelFullMode.Wait,
+        });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private readonly Channel<ThreadStreamEvent> _events = Channel.CreateUnbounded<ThreadStreamEvent>();
private readonly Channel<ThreadStreamEvent> _events = Channel.CreateBounded<ThreadStreamEvent>(
new BoundedChannelOptions(256)
{
SingleReader = true,
SingleWriter = false,
FullMode = BoundedChannelFullMode.Wait,
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Qyl.ChatKit/AgentContext.cs` at line 22, The field _events currently uses
Channel.CreateUnbounded<ThreadStreamEvent>(), which allows unlimited queue
growth; change it to a bounded channel to apply backpressure (replace
Channel.CreateUnbounded with Channel.CreateBounded using a BoundedChannelOptions
instance), pick a sensible capacity (e.g., 100) and set SingleReader = true /
SingleWriter = true plus BoundedChannelFullMode.Wait so slow/disconnected SSE
clients block the producer rather than exhausting memory; update any
producer/consumer usage of _events to handle the bounded write semantics (e.g.,
awaiting WriteAsync) in the ThreadStreamEvent producers and the SSE consumer.


/// <summary>Reader for the event channel, consumed by the stream processor.</summary>
internal ChannelReader<ThreadStreamEvent> EventReader => _events.Reader;

/// <summary>Signal that no more events will be written.</summary>
internal void Complete() => _events.Writer.TryComplete();

/// <summary>Generate a new store-backed id for the given item type.</summary>
public string GenerateId(StoreItemType type, ThreadMetadata? thread = null) =>
type == StoreItemType.Thread
? Store.GenerateThreadId(RequestContext)
: Store.GenerateItemId(type, thread ?? Thread, RequestContext);

/// <summary>Stream a widget into the thread by enqueueing widget events.</summary>
public async ValueTask StreamWidgetAsync(
WidgetRoot widget, string? copyText = null, CancellationToken ct = default)
{
await foreach (var evt in WidgetDiff.StreamWidgetAsync(
Thread, widget, copyText,
t => Store.GenerateItemId(t, Thread, RequestContext),
TimeProvider, ct))
{
await _events.Writer.WriteAsync(evt, ct);
}
}

/// <summary>Stream an async sequence of widget roots into the thread.</summary>
public async ValueTask StreamWidgetAsync(
IAsyncEnumerable<WidgetRoot> widgetStream, string? copyText = null,
CancellationToken ct = default)
{
await foreach (var evt in WidgetDiff.StreamWidgetAsync(
Thread, widgetStream, copyText,
t => Store.GenerateItemId(t, Thread, RequestContext),
TimeProvider, ct))
{
await _events.Writer.WriteAsync(evt, ct);
}
}

/// <summary>Begin streaming a new workflow item.</summary>
public async ValueTask StartWorkflowAsync(Workflow workflow, CancellationToken ct = default)
{
WorkflowItem = new WorkflowItem
{
Id = GenerateId(StoreItemType.Workflow),
CreatedAt = TimeProvider.GetUtcNow().UtcDateTime,
Workflow = workflow,
ThreadId = Thread.Id,
};

if (workflow.Type != "reasoning" && workflow.Tasks.Count == 0)
return; // Defer sending added event until we have tasks

await StreamAsync(new ThreadItemAddedEvent { Item = WorkflowItem }, ct);
}
Comment on lines +64 to +78
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Do not emit thread.item.done for workflows the client never saw.

StartWorkflowAsync defers ThreadItemAddedEvent for empty non-reasoning workflows, but EndWorkflowAsync always sends ThreadItemDoneEvent. An empty custom workflow will therefore complete an unknown item on the client.

One way to fix it
 public sealed class AgentContext<TContext>
 {
+    private bool _workflowAdded;
+
     public async ValueTask StartWorkflowAsync(Workflow workflow, CancellationToken ct = default)
     {
         WorkflowItem = new WorkflowItem
         {
             Id = GenerateId(StoreItemType.Workflow),
@@
             ThreadId = Thread.Id,
         };
 
-        if (workflow.Type != "reasoning" && workflow.Tasks.Count == 0)
+        _workflowAdded = workflow.Type == "reasoning" || workflow.Tasks.Count > 0;
+        if (!_workflowAdded)
             return; // Defer sending added event until we have tasks
 
         await StreamAsync(new ThreadItemAddedEvent { Item = WorkflowItem }, ct);
     }
@@
     public async ValueTask EndWorkflowAsync(
         WorkflowSummary? summary = null, bool expanded = false, CancellationToken ct = default)
     {
         if (WorkflowItem is null)
             return;
@@
         WorkflowItem = WorkflowItem with
         {
             Workflow = WorkflowItem.Workflow with
             {
                 Summary = finalSummary,
                 Expanded = expanded,
             },
         };
 
+        if (!_workflowAdded)
+            await StreamAsync(new ThreadItemAddedEvent { Item = WorkflowItem }, ct);
+
         await StreamAsync(new ThreadItemDoneEvent { Item = WorkflowItem }, ct);
+        _workflowAdded = false;
         WorkflowItem = null;
     }
 }

Also applies to: 141-165

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Qyl.ChatKit/AgentContext.cs` around lines 64 - 78, StartWorkflowAsync
currently defers emitting ThreadItemAddedEvent for empty non-"reasoning"
workflows but EndWorkflowAsync always emits ThreadItemDoneEvent, causing clients
to see completions for items they never saw; modify the logic so
EndWorkflowAsync only emits ThreadItemDoneEvent for WorkflowItem instances that
were previously announced. Concretely, add a Boolean flag/property (e.g.,
WasAnnounced) to WorkflowItem when you call StreamAsync(new ThreadItemAddedEvent
{...}) in StartWorkflowAsync (or set it to true when workflow.Type ==
"reasoning" or workflow.Tasks.Count > 0), and in EndWorkflowAsync check that
flag (or the same condition) before sending ThreadItemDoneEvent; apply the same
pattern to the analogous block referenced at the 141-165 region.


/// <summary>Append a workflow task and stream the appropriate event.</summary>
public async ValueTask AddWorkflowTaskAsync(ChatKitTask task, CancellationToken ct = default)
{
WorkflowItem ??= new WorkflowItem
{
Id = GenerateId(StoreItemType.Workflow),
CreatedAt = TimeProvider.GetUtcNow().UtcDateTime,
Workflow = new Workflow { Type = "custom", Tasks = [] },
ThreadId = Thread.Id,
};

var tasks = WorkflowItem.Workflow.Tasks.ToList();
tasks.Add(task);
WorkflowItem = WorkflowItem with
{
Workflow = WorkflowItem.Workflow with { Tasks = tasks },
};

if (WorkflowItem.Workflow.Type != "reasoning" && tasks.Count == 1)
{
await StreamAsync(new ThreadItemAddedEvent { Item = WorkflowItem }, ct);
}
else
{
await StreamAsync(new ThreadItemUpdatedEvent
{
ItemId = WorkflowItem.Id,
Update = new WorkflowTaskAdded
{
Task = task,
TaskIndex = tasks.Count - 1,
},
}, ct);
}
}

/// <summary>Update an existing workflow task and stream the delta.</summary>
public async ValueTask UpdateWorkflowTaskAsync(
ChatKitTask task, int taskIndex, CancellationToken ct = default)
{
if (WorkflowItem is null)
throw new InvalidOperationException("Workflow is not set");

var tasks = WorkflowItem.Workflow.Tasks.ToList();
tasks[taskIndex] = task;
WorkflowItem = WorkflowItem with
{
Workflow = WorkflowItem.Workflow with { Tasks = tasks },
};

await StreamAsync(new ThreadItemUpdatedEvent
{
ItemId = WorkflowItem.Id,
Update = new WorkflowTaskUpdated
{
Task = task,
TaskIndex = taskIndex,
},
}, ct);
}

/// <summary>Finalize the active workflow item, optionally attaching a summary.</summary>
public async ValueTask EndWorkflowAsync(
WorkflowSummary? summary = null, bool expanded = false, CancellationToken ct = default)
{
if (WorkflowItem is null)
return;

var finalSummary = summary ?? WorkflowItem.Workflow.Summary;
if (finalSummary is null)
{
var delta = TimeProvider.GetUtcNow().UtcDateTime - WorkflowItem.CreatedAt;
finalSummary = new DurationSummary { Duration = (int)delta.TotalSeconds };
}

WorkflowItem = WorkflowItem with
{
Workflow = WorkflowItem.Workflow with
{
Summary = finalSummary,
Expanded = expanded,
},
};

await StreamAsync(new ThreadItemDoneEvent { Item = WorkflowItem }, ct);
WorkflowItem = null;
}

/// <summary>Enqueue a ThreadStreamEvent for downstream processing.</summary>
public async ValueTask StreamAsync(ThreadStreamEvent evt, CancellationToken ct = default) =>
await _events.Writer.WriteAsync(evt, ct);
}

/// <summary>Returned from tool methods to indicate a client-side tool call.</summary>
public sealed record ClientToolCall
{
public required string Name { get; init; }
public required Dictionary<string, object?> Arguments { get; init; }
}
101 changes: 101 additions & 0 deletions src/Qyl.ChatKit/Attachments.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
using System.Text.Json.Serialization;

namespace Qyl.ChatKit;

/// <summary>Base metadata shared by all attachments.</summary>
[JsonPolymorphic(TypeDiscriminatorPropertyName = "type")]
[JsonDerivedType(typeof(FileAttachment), "file")]
[JsonDerivedType(typeof(ImageAttachment), "image")]
public abstract record AttachmentBase
{
[JsonPropertyName("id")]
public required string Id { get; init; }

[JsonPropertyName("name")]
public required string Name { get; init; }

[JsonPropertyName("mime_type")]
public required string MimeType { get; init; }

/// <summary>
/// Two-phase upload instructions.
/// Should be set to null after upload is complete or when using direct upload.
/// </summary>
[JsonPropertyName("upload_descriptor")]
public AttachmentUploadDescriptor? UploadDescriptor { get; init; }

/// <summary>
/// The thread the attachment belongs to, if any.
/// Added when the user message that contains the attachment is saved to store.
/// </summary>
[JsonPropertyName("thread_id")]
public string? ThreadId { get; init; }

/// <summary>
/// Integration-only metadata stored with the attachment.
/// Ignored by ChatKit and not returned in server responses.
/// </summary>
[JsonPropertyName("metadata")]
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The XML doc on AttachmentBase.Metadata says it is “ignored by ChatKit and not returned in server responses,” but the property is currently serialized as metadata (and ProcessNonStreamingAsync returns Serialize(attachment) for attachment creation). If this metadata must never go over the wire, mark it [JsonIgnore] (and/or use a separate internal model) to prevent accidental exposure.

Suggested change
[JsonPropertyName("metadata")]
[JsonIgnore]

Copilot uses AI. Check for mistakes.
public Dictionary<string, object?>? Metadata { get; init; }
}

/// <summary>Attachment representing a generic file.</summary>
public sealed record FileAttachment : AttachmentBase;

/// <summary>Attachment representing an image resource.</summary>
public sealed record ImageAttachment : AttachmentBase
{
[JsonPropertyName("preview_url")]
public required Uri PreviewUrl { get; init; }
}

/// <summary>Two-phase upload instructions.</summary>
public sealed record AttachmentUploadDescriptor
{
[JsonPropertyName("url")]
public required Uri Url { get; init; }

/// <summary>The HTTP method to use when uploading the file for two-phase upload.</summary>
[JsonPropertyName("method")]
public required string Method { get; init; }

/// <summary>Optional headers to include in the upload request.</summary>
[JsonPropertyName("headers")]
public Dictionary<string, string> Headers { get; init; } = new();
}

/// <summary>Metadata needed to initialize an attachment.</summary>
public sealed record AttachmentCreateParams
{
[JsonPropertyName("name")]
public required string Name { get; init; }

[JsonPropertyName("size")]
public required int Size { get; init; }
Comment on lines +73 to +74
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

int Size limits attachments to ~2GB.

File sizes routinely exceed 2GB. Use long for size fields to match standard .NET file APIs (FileInfo.Length, Stream.Length).

Proposed fix
     [JsonPropertyName("size")]
-    public required int Size { get; init; }
+    public required long Size { get; init; }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
[JsonPropertyName("size")]
public required int Size { get; init; }
[JsonPropertyName("size")]
public required long Size { get; init; }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Qyl.ChatKit/Attachments.cs` around lines 73 - 74, Change the Size
property's type from int to long to support files >2GB: update the declaration
public required int Size { get; init; } to use long (public required long Size {
get; init; }), then search for and update all usages, method signatures,
serialization/deserialization expectations, and any comparisons or math that
assume int (e.g., code referencing Size, constructors, DTO mappings, and unit
tests) so they accept long and preserve JSON name "size" via the existing
JsonPropertyName attribute.


[JsonPropertyName("mime_type")]
public required string MimeType { get; init; }
}

/// <summary>Audio input data for transcription.</summary>
public sealed record AudioInput
{
/// <summary>Audio data bytes.</summary>
[JsonPropertyName("data")]
public required byte[] Data { get; init; }

/// <summary>Raw MIME type for the audio payload, e.g. "audio/webm;codecs=opus".</summary>
[JsonPropertyName("mime_type")]
public required string MimeType { get; init; }

/// <summary>Media type for the audio payload, e.g. "audio/webm".</summary>
[JsonIgnore]
public string MediaType => MimeType.Split(';', 2)[0];
}

/// <summary>Input speech transcription result.</summary>
public sealed record TranscriptionResult
{
[JsonPropertyName("text")]
public required string Text { get; init; }
}
14 changes: 14 additions & 0 deletions src/Qyl.ChatKit/ChatKitJsonOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System.Text.Json;
using System.Text.Json.Serialization;

namespace Qyl.ChatKit;

/// <summary>Shared JSON serializer options for the ChatKit wire protocol.</summary>
internal static class ChatKitJsonOptions
{
public static JsonSerializerOptions Default { get; } = new()
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
};
}
Loading