diff --git a/.github/actions/build-windows-x64/action.yml b/.github/actions/build-windows-x64/action.yml
index d7e8599..e4035a2 100644
--- a/.github/actions/build-windows-x64/action.yml
+++ b/.github/actions/build-windows-x64/action.yml
@@ -12,23 +12,58 @@ runs:
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
- dotnet-version: '8.0.x'
+ dotnet-version: '10.0.x'
- name: Restore
shell: cmd
- run: dotnet restore BSPLocalizationTools.sln
+ run: dotnet restore BSPLocalizationTools.slnx
- name: Test
shell: cmd
- run: dotnet test BSPLocalizationTools.sln -c Release --no-restore
+ run: dotnet test BSPLocalizationTools.slnx -c Release --no-restore
- - name: Publish GUI (single file)
+ - name: Publish GUI (Native AOT)
shell: cmd
- run: dotnet publish src\BSPLocalizationTools\BSPLocalizationTools.csproj -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -p:EnableCompressionInSingleFile=true -o BSPLocalizationTools-Output\GUI
+ run: dotnet publish src\BSPLocalizationTools\BSPLocalizationTools.csproj -c Release -r win-x64 -o BSPLocalizationTools-Output\GUI
- - name: Publish CLI (single file)
+ - name: Publish CLI (Native AOT)
shell: cmd
- run: dotnet publish src\BSPLocalizationToolsCLI\BSPLocalizationToolsCLI.csproj -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -p:EnableCompressionInSingleFile=true -o BSPLocalizationTools-Output\CLI
+ run: dotnet publish src\BSPLocalizationToolsCLI\BSPLocalizationToolsCLI.csproj -c Release -r win-x64 -o BSPLocalizationTools-Output\CLI
+
+ - name: Verify publish output
+ shell: pwsh
+ run: |
+ # Native AOT discards PDBs; the release payload never ships them.
+ Get-ChildItem -LiteralPath 'BSPLocalizationTools-Output' -Filter *.pdb -Recurse | Remove-Item -Force
+
+ $cliDirectory = 'BSPLocalizationTools-Output\CLI'
+ $cliFiles = @(Get-ChildItem -LiteralPath $cliDirectory -Recurse -File)
+ if ($cliFiles.Count -ne 1 -or $cliFiles[0].Name -ne 'BSPLocalizationToolsCLI.exe') {
+ throw 'CLI publish must contain exactly one self-contained executable.'
+ }
+
+ # Native AOT cannot fold Avalonia's native backends into the executable, and
+ # satellite resource assemblies are still emitted for localized strings. The
+ # set is exact: anything else means an unintended payload crept in.
+ $exeName = 'BSPLocalizationTools.exe'
+ $nativeDlls = @('av_libglesv2.dll', 'libSkiaSharp.dll', 'libHarfBuzzSharp.dll')
+ $guiDirectory = 'BSPLocalizationTools-Output\GUI'
+ $guiFiles = @(Get-ChildItem -LiteralPath $guiDirectory -Recurse -File)
+ $unexpected = @($guiFiles | Where-Object {
+ $_.Name -ne $exeName -and $nativeDlls -notcontains $_.Name -and
+ $_.Name -ne 'BSPLocalizationTools.resources.dll'
+ })
+ if ($unexpected.Count -gt 0) {
+ throw "GUI publish contains unexpected files: $($unexpected.Name -join ', ')"
+ }
+ if (!(Test-Path -LiteralPath (Join-Path $guiDirectory $exeName) -PathType Leaf)) {
+ throw "GUI publish is missing its executable: $exeName"
+ }
+ foreach ($nativeDll in $nativeDlls) {
+ if (!(Test-Path -LiteralPath (Join-Path $guiDirectory $nativeDll) -PathType Leaf)) {
+ throw "GUI publish is missing its native backend library: $nativeDll"
+ }
+ }
- name: Copy README to output
shell: cmd
diff --git a/.gitignore b/.gitignore
index b5c4649..31a4bf4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,6 +3,8 @@
.vs/
.env
**/.env
+/BSPLocalizationTools-Output/
+/build/
# Generated at build time by Microsoft.CodeAnalysis.ResxSourceGenerator
**/Resources.Designer.cs
diff --git a/BSPLocalizationTools.sln b/BSPLocalizationTools.sln
deleted file mode 100644
index 82751cc..0000000
--- a/BSPLocalizationTools.sln
+++ /dev/null
@@ -1,84 +0,0 @@
-
-Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 17
-VisualStudioVersion = 17.0.31903.59
-MinimumVisualStudioVersion = 10.0.40219.1
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BSPLocalizationTools", "src\BSPLocalizationTools\BSPLocalizationTools.csproj", "{E465BB12-D220-4171-BEA2-73E63C0A00E1}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BSPLocalizationTools.Tests", "tests\BSPLocalizationTools.Tests\BSPLocalizationTools.Tests.csproj", "{FE47E594-ABEB-49BE-87F4-080B6359C840}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BSPLocalizationToolsCore", "src\BSPLocalizationToolsCore\BSPLocalizationToolsCore.csproj", "{3B0AE2B3-8A62-4548-84B9-518E624D8688}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BSPLocalizationToolsCLI", "src\BSPLocalizationToolsCLI\BSPLocalizationToolsCLI.csproj", "{30F77649-3BAC-4265-81A7-499A334434F2}"
-EndProject
-Global
- GlobalSection(SolutionConfigurationPlatforms) = preSolution
- Debug|Any CPU = Debug|Any CPU
- Debug|x64 = Debug|x64
- Debug|x86 = Debug|x86
- Release|Any CPU = Release|Any CPU
- Release|x64 = Release|x64
- Release|x86 = Release|x86
- EndGlobalSection
- GlobalSection(ProjectConfigurationPlatforms) = postSolution
- {E465BB12-D220-4171-BEA2-73E63C0A00E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {E465BB12-D220-4171-BEA2-73E63C0A00E1}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {E465BB12-D220-4171-BEA2-73E63C0A00E1}.Debug|x64.ActiveCfg = Debug|Any CPU
- {E465BB12-D220-4171-BEA2-73E63C0A00E1}.Debug|x64.Build.0 = Debug|Any CPU
- {E465BB12-D220-4171-BEA2-73E63C0A00E1}.Debug|x86.ActiveCfg = Debug|Any CPU
- {E465BB12-D220-4171-BEA2-73E63C0A00E1}.Debug|x86.Build.0 = Debug|Any CPU
- {E465BB12-D220-4171-BEA2-73E63C0A00E1}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {E465BB12-D220-4171-BEA2-73E63C0A00E1}.Release|Any CPU.Build.0 = Release|Any CPU
- {E465BB12-D220-4171-BEA2-73E63C0A00E1}.Release|x64.ActiveCfg = Release|Any CPU
- {E465BB12-D220-4171-BEA2-73E63C0A00E1}.Release|x64.Build.0 = Release|Any CPU
- {E465BB12-D220-4171-BEA2-73E63C0A00E1}.Release|x86.ActiveCfg = Release|Any CPU
- {E465BB12-D220-4171-BEA2-73E63C0A00E1}.Release|x86.Build.0 = Release|Any CPU
- {FE47E594-ABEB-49BE-87F4-080B6359C840}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {FE47E594-ABEB-49BE-87F4-080B6359C840}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {FE47E594-ABEB-49BE-87F4-080B6359C840}.Debug|x64.ActiveCfg = Debug|Any CPU
- {FE47E594-ABEB-49BE-87F4-080B6359C840}.Debug|x64.Build.0 = Debug|Any CPU
- {FE47E594-ABEB-49BE-87F4-080B6359C840}.Debug|x86.ActiveCfg = Debug|Any CPU
- {FE47E594-ABEB-49BE-87F4-080B6359C840}.Debug|x86.Build.0 = Debug|Any CPU
- {FE47E594-ABEB-49BE-87F4-080B6359C840}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {FE47E594-ABEB-49BE-87F4-080B6359C840}.Release|Any CPU.Build.0 = Release|Any CPU
- {FE47E594-ABEB-49BE-87F4-080B6359C840}.Release|x64.ActiveCfg = Release|Any CPU
- {FE47E594-ABEB-49BE-87F4-080B6359C840}.Release|x64.Build.0 = Release|Any CPU
- {FE47E594-ABEB-49BE-87F4-080B6359C840}.Release|x86.ActiveCfg = Release|Any CPU
- {FE47E594-ABEB-49BE-87F4-080B6359C840}.Release|x86.Build.0 = Release|Any CPU
- {3B0AE2B3-8A62-4548-84B9-518E624D8688}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {3B0AE2B3-8A62-4548-84B9-518E624D8688}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {3B0AE2B3-8A62-4548-84B9-518E624D8688}.Debug|x64.ActiveCfg = Debug|Any CPU
- {3B0AE2B3-8A62-4548-84B9-518E624D8688}.Debug|x64.Build.0 = Debug|Any CPU
- {3B0AE2B3-8A62-4548-84B9-518E624D8688}.Debug|x86.ActiveCfg = Debug|Any CPU
- {3B0AE2B3-8A62-4548-84B9-518E624D8688}.Debug|x86.Build.0 = Debug|Any CPU
- {3B0AE2B3-8A62-4548-84B9-518E624D8688}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {3B0AE2B3-8A62-4548-84B9-518E624D8688}.Release|Any CPU.Build.0 = Release|Any CPU
- {3B0AE2B3-8A62-4548-84B9-518E624D8688}.Release|x64.ActiveCfg = Release|Any CPU
- {3B0AE2B3-8A62-4548-84B9-518E624D8688}.Release|x64.Build.0 = Release|Any CPU
- {3B0AE2B3-8A62-4548-84B9-518E624D8688}.Release|x86.ActiveCfg = Release|Any CPU
- {3B0AE2B3-8A62-4548-84B9-518E624D8688}.Release|x86.Build.0 = Release|Any CPU
- {30F77649-3BAC-4265-81A7-499A334434F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {30F77649-3BAC-4265-81A7-499A334434F2}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {30F77649-3BAC-4265-81A7-499A334434F2}.Debug|x64.ActiveCfg = Debug|Any CPU
- {30F77649-3BAC-4265-81A7-499A334434F2}.Debug|x64.Build.0 = Debug|Any CPU
- {30F77649-3BAC-4265-81A7-499A334434F2}.Debug|x86.ActiveCfg = Debug|Any CPU
- {30F77649-3BAC-4265-81A7-499A334434F2}.Debug|x86.Build.0 = Debug|Any CPU
- {30F77649-3BAC-4265-81A7-499A334434F2}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {30F77649-3BAC-4265-81A7-499A334434F2}.Release|Any CPU.Build.0 = Release|Any CPU
- {30F77649-3BAC-4265-81A7-499A334434F2}.Release|x64.ActiveCfg = Release|Any CPU
- {30F77649-3BAC-4265-81A7-499A334434F2}.Release|x64.Build.0 = Release|Any CPU
- {30F77649-3BAC-4265-81A7-499A334434F2}.Release|x86.ActiveCfg = Release|Any CPU
- {30F77649-3BAC-4265-81A7-499A334434F2}.Release|x86.Build.0 = Release|Any CPU
- EndGlobalSection
- GlobalSection(SolutionProperties) = preSolution
- HideSolutionNode = FALSE
- EndGlobalSection
- GlobalSection(NestedProjects) = preSolution
- {E465BB12-D220-4171-BEA2-73E63C0A00E1} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
- {FE47E594-ABEB-49BE-87F4-080B6359C840} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
- {3B0AE2B3-8A62-4548-84B9-518E624D8688} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
- {30F77649-3BAC-4265-81A7-499A334434F2} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
- EndGlobalSection
-EndGlobal
diff --git a/BSPLocalizationTools.slnx b/BSPLocalizationTools.slnx
new file mode 100644
index 0000000..c8bc86e
--- /dev/null
+++ b/BSPLocalizationTools.slnx
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/README.md b/README.md
index d1cf1ca..2a8473c 100644
--- a/README.md
+++ b/README.md
@@ -63,3 +63,28 @@ The **Settings** tab loads and saves LLM and prompt path settings to `.env`.
## Encodings
CSV output is always written as UTF-8 with BOM.
+
+## Build
+
+Use Windows x64 with the **.NET 10 SDK**. Publishing the GUI and CLI as Native
+AOT additionally requires the Visual Studio 2022 **Desktop development with C++**
+workload (MSVC linker) and a Windows SDK.
+
+From the repository root:
+
+```powershell
+dotnet restore BSPLocalizationTools.slnx
+dotnet test BSPLocalizationTools.slnx -c Release
+dotnet run --project src\BSPLocalizationTools # Avalonia GUI
+dotnet run --project src\BSPLocalizationToolsCLI -- "-bsp=path\to\map.bsp" # CLI
+```
+
+Both executables are published as self-contained Native AOT programs, so the
+target machine does not need the .NET runtime. Native AOT cannot fold Avalonia's
+native backends into the executable, so the GUI ships as the executable plus its
+native libraries:
+
+```powershell
+dotnet publish src\BSPLocalizationTools\BSPLocalizationTools.csproj -c Release -r win-x64 -o out\GUI
+dotnet publish src\BSPLocalizationToolsCLI\BSPLocalizationToolsCLI.csproj -c Release -r win-x64 -o out\CLI
+```
diff --git a/global.json b/global.json
new file mode 100644
index 0000000..512142d
--- /dev/null
+++ b/global.json
@@ -0,0 +1,6 @@
+{
+ "sdk": {
+ "version": "10.0.100",
+ "rollForward": "latestFeature"
+ }
+}
diff --git a/src/BSPLocalizationTools/BSPLocalizationTools.csproj b/src/BSPLocalizationTools/BSPLocalizationTools.csproj
index 228887d..d3a9dcb 100644
--- a/src/BSPLocalizationTools/BSPLocalizationTools.csproj
+++ b/src/BSPLocalizationTools/BSPLocalizationTools.csproj
@@ -1,7 +1,7 @@
WinExe
- net8.0-windows10.0.17763.0
+ net10.0-windows10.0.17763.0
BSPLocalizationTools.GUI
enable
enable
@@ -10,15 +10,23 @@
true
7.0
x64
+ true
+ true
+
+ full
+ true
+ false
+ true
-
-
-
-
-
-
+
+
+
+
+
+
+
diff --git a/src/BSPLocalizationTools/Program.cs b/src/BSPLocalizationTools/Program.cs
index cb96c2a..6267dec 100644
--- a/src/BSPLocalizationTools/Program.cs
+++ b/src/BSPLocalizationTools/Program.cs
@@ -17,6 +17,6 @@ public static AppBuilder BuildAvaloniaApp()
.UsePlatformDetect()
.WithInterFont()
.LogToTrace()
- .UseReactiveUI();
+ .UseReactiveUI(_ => { });
}
}
diff --git a/src/BSPLocalizationTools/Views/MainWindow.axaml b/src/BSPLocalizationTools/Views/MainWindow.axaml
index b51c5f8..0ce081b 100644
--- a/src/BSPLocalizationTools/Views/MainWindow.axaml
+++ b/src/BSPLocalizationTools/Views/MainWindow.axaml
@@ -15,7 +15,7 @@
-
+
@@ -35,7 +35,7 @@
-
+
-
+
@@ -106,23 +106,23 @@
TextWrapping="Wrap"
MinHeight="140"
VerticalContentAlignment="Top"
- Watermark="{Binding Strings.PromptEditorWatermark}" />
+ PlaceholderText="{Binding Strings.PromptEditorWatermark}" />
-
+
-
+
-
+
-
+
diff --git a/src/BSPLocalizationToolsCLI/BSPLocalizationToolsCLI.csproj b/src/BSPLocalizationToolsCLI/BSPLocalizationToolsCLI.csproj
index daf9ae8..7a058a2 100644
--- a/src/BSPLocalizationToolsCLI/BSPLocalizationToolsCLI.csproj
+++ b/src/BSPLocalizationToolsCLI/BSPLocalizationToolsCLI.csproj
@@ -1,10 +1,12 @@
Exe
- net8.0
+ net10.0
enable
enable
latest
+ true
+ true
diff --git a/src/BSPLocalizationToolsCore/BSPLocalizationToolsCore.csproj b/src/BSPLocalizationToolsCore/BSPLocalizationToolsCore.csproj
index 6521a7a..ac80071 100644
--- a/src/BSPLocalizationToolsCore/BSPLocalizationToolsCore.csproj
+++ b/src/BSPLocalizationToolsCore/BSPLocalizationToolsCore.csproj
@@ -1,6 +1,6 @@
- net8.0
+ net10.0
enable
enable
latest
@@ -10,6 +10,5 @@
-
diff --git a/src/BSPLocalizationToolsCore/LlmRequestJson.cs b/src/BSPLocalizationToolsCore/LlmRequestJson.cs
new file mode 100644
index 0000000..e28b4c6
--- /dev/null
+++ b/src/BSPLocalizationToolsCore/LlmRequestJson.cs
@@ -0,0 +1,52 @@
+using System.Text.Json.Serialization;
+
+namespace BSPLocalizationTools;
+
+// Concrete request shapes for the OpenAI-compatible endpoints. They replace the
+// former anonymous types so Native AOT and trimming keep the serialization
+// metadata (the anonymous-type JsonSerializer overloads are not AOT-compatible).
+// Default serializer options are used, so escaping matches the previous output.
+internal sealed record ChatCompletionRequest(
+ [property: JsonPropertyName("model")] string Model,
+ [property: JsonPropertyName("messages")] ChatRequestMessage[] Messages,
+ [property: JsonPropertyName("reasoning_effort")] string ReasoningEffort,
+ [property: JsonPropertyName("temperature")] [property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] double? Temperature);
+
+internal sealed record ChatRequestMessage(
+ [property: JsonPropertyName("role")] string Role,
+ [property: JsonPropertyName("content")] string Content);
+
+internal sealed record ResponsesRequest(
+ [property: JsonPropertyName("model")] string Model,
+ [property: JsonPropertyName("input")] ResponsesInputMessage[] Input,
+ [property: JsonPropertyName("reasoning")] ResponsesReasoning Reasoning,
+ [property: JsonPropertyName("stream")] bool Stream,
+ [property: JsonPropertyName("temperature")] [property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] double? Temperature);
+
+internal sealed record ResponsesInputMessage(
+ [property: JsonPropertyName("role")] string Role,
+ [property: JsonPropertyName("content")] string Content);
+
+internal sealed record ResponsesReasoning(
+ [property: JsonPropertyName("effort")] string Effort);
+
+internal sealed record PromptPayload(
+ [property: JsonPropertyName("target_language")] string TargetLanguage,
+ [property: JsonPropertyName("output_contract")] PromptOutputContract OutputContract,
+ [property: JsonPropertyName("inputs")] PromptInput[] Inputs);
+
+internal sealed record PromptOutputContract(
+ [property: JsonPropertyName("format")] string Format,
+ [property: JsonPropertyName("schema")] string Schema,
+ [property: JsonPropertyName("rules")] string[] Rules);
+
+internal sealed record PromptInput(
+ [property: JsonPropertyName("id")] int Id,
+ [property: JsonPropertyName("text")] string Text);
+
+[JsonSerializable(typeof(ChatCompletionRequest))]
+[JsonSerializable(typeof(ResponsesRequest))]
+[JsonSerializable(typeof(PromptPayload))]
+internal sealed partial class LlmRequestJsonContext : JsonSerializerContext
+{
+}
diff --git a/src/BSPLocalizationToolsCore/OpenAICompatibleLLMClient.cs b/src/BSPLocalizationToolsCore/OpenAICompatibleLLMClient.cs
index dd52b78..89baf9e 100644
--- a/src/BSPLocalizationToolsCore/OpenAICompatibleLLMClient.cs
+++ b/src/BSPLocalizationToolsCore/OpenAICompatibleLLMClient.cs
@@ -1,6 +1,7 @@
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
+using System.Text.Json.Serialization.Metadata;
namespace BSPLocalizationTools;
@@ -43,18 +44,17 @@ private async Task CompleteViaChatAsync(
LLMOptions options,
CancellationToken cancellationToken)
{
- var body = new Dictionary
- {
- ["model"] = options.Model,
- ["messages"] = messages.Select(m => new { role = m.Role, content = m.Content }).ToArray(),
- ["reasoning_effort"] = options.Effort,
- };
- if (options.Temperature is not null)
- {
- body["temperature"] = options.Temperature;
- }
-
- using var request = CreateJsonRequest(GetBaseUrl(options) + "/chat/completions", options, body);
+ var body = new ChatCompletionRequest(
+ options.Model!,
+ messages.Select(m => new ChatRequestMessage(m.Role, m.Content)).ToArray(),
+ options.Effort,
+ options.Temperature);
+
+ using var request = CreateJsonRequest(
+ GetBaseUrl(options) + "/chat/completions",
+ options,
+ body,
+ LlmRequestJsonContext.Default.ChatCompletionRequest);
using var response = await httpClient.SendAsync(request, cancellationToken);
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync(cancellationToken);
@@ -74,22 +74,18 @@ private async Task CompleteViaResponsesAsync(
LLMOptions options,
CancellationToken cancellationToken)
{
- var body = new Dictionary
- {
- ["model"] = options.Model,
- ["input"] = new[]
- {
- new { role = "user", content = string.Join("\n\n", messages.Select(m => m.Content)) },
- },
- ["reasoning"] = new { effort = options.Effort },
- ["stream"] = true,
- };
- if (options.Temperature is not null)
- {
- body["temperature"] = options.Temperature;
- }
-
- using var request = CreateJsonRequest(GetBaseUrl(options) + "/responses", options, body);
+ var body = new ResponsesRequest(
+ options.Model!,
+ [new ResponsesInputMessage("user", string.Join("\n\n", messages.Select(m => m.Content)))],
+ new ResponsesReasoning(options.Effort),
+ true,
+ options.Temperature);
+
+ using var request = CreateJsonRequest(
+ GetBaseUrl(options) + "/responses",
+ options,
+ body,
+ LlmRequestJsonContext.Default.ResponsesRequest);
request.Headers.Accept.Clear();
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/event-stream"));
using var response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
@@ -101,12 +97,16 @@ private async Task CompleteViaResponsesAsync(
: result;
}
- private static HttpRequestMessage CreateJsonRequest(string uri, LLMOptions options, object body)
+ private static HttpRequestMessage CreateJsonRequest(
+ string uri,
+ LLMOptions options,
+ TBody body,
+ JsonTypeInfo jsonTypeInfo)
{
var request = new HttpRequestMessage(HttpMethod.Post, uri);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", options.ApiKey);
request.Headers.UserAgent.ParseAdd("BSPLocalizationTools/1.0");
- request.Content = new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json");
+ request.Content = new StringContent(JsonSerializer.Serialize(body, jsonTypeInfo), Encoding.UTF8, "application/json");
return request;
}
diff --git a/src/BSPLocalizationToolsCore/TranslationPromptBuilder.cs b/src/BSPLocalizationToolsCore/TranslationPromptBuilder.cs
index df8a1db..6be28af 100644
--- a/src/BSPLocalizationToolsCore/TranslationPromptBuilder.cs
+++ b/src/BSPLocalizationToolsCore/TranslationPromptBuilder.cs
@@ -19,27 +19,22 @@ public static IReadOnlyList Build(
var instructions = string.IsNullOrWhiteSpace(customPrompt)
? BuiltInPrompt
: customPrompt.Trim();
- var payload = new
- {
- target_language = targetLanguage,
- output_contract = new
- {
- format = "json",
- schema = """{"translations":[{"id":0,"translation":"..."}]}""",
- rules = new[]
- {
+ var payload = new PromptPayload(
+ targetLanguage,
+ new PromptOutputContract(
+ "json",
+ """{"translations":[{"id":0,"translation":"..."}]}""",
+ [
"Return only valid JSON.",
"Include every input id exactly once.",
"Keep literal \\n as backslash-n text.",
- },
- },
- inputs = sourceMessages.Select((text, id) => new { id, text }).ToArray(),
- };
+ ]),
+ sourceMessages.Select((text, id) => new PromptInput(id, text)).ToArray());
return
[
new LLMMessage("system", instructions),
- new LLMMessage("user", JsonSerializer.Serialize(payload)),
+ new LLMMessage("user", JsonSerializer.Serialize(payload, LlmRequestJsonContext.Default.PromptPayload)),
];
}
}
diff --git a/tests/BSPLocalizationTools.Tests/BSPLocalizationTools.Tests.csproj b/tests/BSPLocalizationTools.Tests/BSPLocalizationTools.Tests.csproj
index 92d06f6..05bf15e 100644
--- a/tests/BSPLocalizationTools.Tests/BSPLocalizationTools.Tests.csproj
+++ b/tests/BSPLocalizationTools.Tests/BSPLocalizationTools.Tests.csproj
@@ -1,6 +1,6 @@
- net8.0-windows10.0.17763.0
+ net10.0-windows10.0.17763.0
enable
enable
latest