Skip to content

Commit 2f2eee3

Browse files
committed
fix: honor MEAI instructions and reject unsupported generation options
1 parent 321874a commit 2f2eee3

7 files changed

Lines changed: 70 additions & 10 deletions

File tree

‎ClaudeCodeSharpSDK.Extensions.AI/ClaudeChatClient.cs‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,9 @@ public async Task<ChatResponse> GetResponseAsync(
2525
{
2626
ArgumentNullException.ThrowIfNull(messages);
2727
ChatOptionsMapper.ValidateFunctionCallingOptions(options);
28+
ChatOptionsMapper.ValidateGenerationOptions(options);
2829

29-
var prompt = ChatMessageMapper.ToClaudeInput(messages);
30+
var prompt = ChatMessageMapper.ToClaudeInput(messages, options?.Instructions);
3031
var threadOptions = ChatOptionsMapper.ToThreadOptions(options, _options);
3132
var turnOptions = ChatOptionsMapper.ToTurnOptions(options, cancellationToken);
3233

@@ -48,8 +49,9 @@ public async IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(
4849
{
4950
ArgumentNullException.ThrowIfNull(messages);
5051
ChatOptionsMapper.ValidateFunctionCallingOptions(options);
52+
ChatOptionsMapper.ValidateGenerationOptions(options);
5153

52-
var prompt = ChatMessageMapper.ToClaudeInput(messages);
54+
var prompt = ChatMessageMapper.ToClaudeInput(messages, options?.Instructions);
5355
var threadOptions = ChatOptionsMapper.ToThreadOptions(options, _options);
5456
var turnOptions = ChatOptionsMapper.ToTurnOptions(options, cancellationToken);
5557

‎ClaudeCodeSharpSDK.Extensions.AI/Internal/ChatMessageMapper.cs‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,13 @@ internal static class ChatMessageMapper
99
private const string ParagraphSeparator = "\n\n";
1010
private const string ImageUnsupportedMessage = "Claude Code chat adapter currently supports text-only prompts.";
1111

12-
internal static string ToClaudeInput(IEnumerable<ChatMessage> messages)
12+
internal static string ToClaudeInput(IEnumerable<ChatMessage> messages, string? instructions = null)
1313
{
1414
var promptParts = new List<string>();
15+
if (!string.IsNullOrWhiteSpace(instructions))
16+
{
17+
promptParts.Add(string.Concat(SystemPrefix, instructions));
18+
}
1519

1620
foreach (var message in messages)
1721
{

‎ClaudeCodeSharpSDK.Extensions.AI/Internal/ChatOptionsMapper.cs‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ namespace ManagedCode.ClaudeCodeSharpSDK.Extensions.AI.Internal;
77

88
internal static class ChatOptionsMapper
99
{
10+
private const string GenerationOptionsUnsupportedMessage = "The Claude CLI adapter does not support the requested MEAI generation options.";
1011
private const string FunctionToolsUnsupportedMessage = "MEAI function tools are not supported by the Claude CLI adapter.";
1112
private const string ToolModeUnsupportedMessage = "Only automatic MEAI tool mode is supported by the Claude CLI adapter.";
1213
private const string InvalidValueMessagePrefix = "Invalid value for Claude chat option";
@@ -22,6 +23,17 @@ internal static class ChatOptionsMapper
2223
internal const string AppendSystemPromptKey = "claude:append_system_prompt";
2324
internal const string MaxBudgetUsdKey = "claude:max_budget_usd";
2425

26+
internal static void ValidateGenerationOptions(ChatOptions? options)
27+
{
28+
if (options is { Temperature: not null } or { TopP: not null } or { TopK: not null } or
29+
{ MaxOutputTokens: not null } or { Seed: not null } or { FrequencyPenalty: not null } or
30+
{ PresencePenalty: not null } || options?.StopSequences is { Count: > 0 } ||
31+
options?.ResponseFormat is { } format && format is not ChatResponseFormatText)
32+
{
33+
throw new NotSupportedException(GenerationOptionsUnsupportedMessage);
34+
}
35+
}
36+
2537
internal static void ValidateFunctionCallingOptions(ChatOptions? chatOptions)
2638
{
2739
if (chatOptions?.Tools is { Count: > 0 })

‎ClaudeCodeSharpSDK.Tests/MEAI/ChatMessageMapperTests.cs‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,19 @@ public class ChatMessageMapperTests
1515
private const string PngMimeType = "image/png";
1616
private const string TextOnlyPromptsMessage = "text-only prompts";
1717

18+
private const string StandardInstructions = "Follow the standard MEAI instructions.";
19+
private const string ExistingSystemText = "Existing system message.";
20+
private const string UserQuestion = "User question.";
21+
private const string ExpectedInstructionPrompt = "[System] Follow the standard MEAI instructions.\n\n[System] Existing system message.\n\nUser question.";
22+
23+
[Test]
24+
public async Task StandardInstructionsPrecedeSystemAndUserMessages()
25+
{
26+
var messages = new[] { new ChatMessage(ChatRole.System, ExistingSystemText), new ChatMessage(ChatRole.User, UserQuestion) };
27+
var prompt = ChatMessageMapper.ToClaudeInput(messages, StandardInstructions);
28+
await Assert.That(prompt).IsEqualTo(ExpectedInstructionPrompt);
29+
}
30+
1831
[Test]
1932
public async Task ToClaudeInput_MixedConversation_PreservesChronology()
2033
{

‎ClaudeCodeSharpSDK.Tests/MEAI/UnsupportedToolOptionsTests.cs‎

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,35 @@ public sealed class UnsupportedToolOptionsTests
2525
private const string NodeEvalArgument = "-e";
2626
private const string NodeMarkerScript = "require('node:fs').writeFileSync(process.argv[1], 'started');";
2727

28+
private const string GenerationStopSequence = "fixture stop";
29+
30+
[Test]
31+
[Arguments(0)]
32+
[Arguments(1)]
33+
[Arguments(2)]
34+
[Arguments(3)]
35+
[Arguments(4)]
36+
[Arguments(5)]
37+
[Arguments(6)]
38+
[Arguments(7)]
39+
[Arguments(8)]
40+
public async Task ExplicitUnsupportedGenerationOptionsFailBeforeNativeExecution(int vector)
41+
{
42+
var options = vector switch
43+
{
44+
0 => new ChatOptions { Temperature = 0.5f },
45+
1 => new ChatOptions { TopP = 0.5f },
46+
2 => new ChatOptions { TopK = 1 },
47+
3 => new ChatOptions { MaxOutputTokens = 1 },
48+
4 => new ChatOptions { Seed = 1 },
49+
5 => new ChatOptions { FrequencyPenalty = 0.5f },
50+
6 => new ChatOptions { PresencePenalty = 0.5f },
51+
7 => new ChatOptions { StopSequences = [GenerationStopSequence] },
52+
_ => new ChatOptions { ResponseFormat = ChatResponseFormat.Json }
53+
};
54+
await AssertRejectedWithoutLaunchingAsync(options);
55+
}
56+
2857
[Test]
2958
public async Task NonemptyMeaiToolsAreRejectedBeforeLaunchingOrCreatingAThread()
3059
{
@@ -78,12 +107,8 @@ private static async Task AssertRejectedWithoutLaunchingAsync(ChatOptions option
78107
await Assert.That(streamError).IsTypeOf<NotSupportedException>();
79108
await Assert.That(File.Exists(markerPath)).IsFalse();
80109

81-
var resumeOptions = new ChatOptions
82-
{
83-
ConversationId = EmptyConversationId,
84-
Tools = options.Tools,
85-
ToolMode = options.ToolMode,
86-
};
110+
var resumeOptions = options.Clone();
111+
resumeOptions.ConversationId = EmptyConversationId;
87112
var resumeError = await Assert.That(async () =>
88113
{
89114
_ = await client.GetResponseAsync(CreateMessages(), resumeOptions);

‎Directory.Build.props‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
1414
<AnalysisLevel>latest-recommended</AnalysisLevel>
1515
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
16-
<Version>2.1.292.4</Version>
16+
<Version>2.1.292.5</Version>
1717
<PackageVersion>$(Version)</PackageVersion>
1818

1919
</PropertyGroup>

‎docs/Features/meai-integration.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,3 +171,7 @@ Native provider failures stop producing response updates, but the adapter contin
171171
- DI extensions register the client correctly.
172172
- All mapper and DI tests pass.
173173
- ADR and feature docs stay aligned with the adapter surface.
174+
175+
## Standard MEAI request options
176+
177+
`ChatOptions.Instructions` is included before the supplied messages using the adapter’s existing system-message mapping in both response modes. Existing system and user messages retain their order. Model selection remains supported through `ModelId`. Explicit temperature, nucleus/top-k sampling, maximum output tokens, seed, penalties, stop sequences and non-text response formats are rejected before any native thread/process starts because this adapter does not implement those generation controls. The core typed `OutputSchema` API remains separate; arbitrary MEAI JSON schemas are not silently approximated.

0 commit comments

Comments
 (0)