Skip to content

Commit 619eedd

Browse files
committed
fix: honor MEAI instructions and reject unsupported generation options
1 parent 26d2023 commit 619eedd

7 files changed

Lines changed: 70 additions & 10 deletions

File tree

‎CodexSharpSDK.Extensions.AI/CodexChatClient.cs‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,9 @@ public async Task<ChatResponse> GetResponseAsync(
2727
{
2828
ArgumentNullException.ThrowIfNull(messages);
2929
ChatOptionsMapper.ValidateFunctionCallingOptions(options);
30+
ChatOptionsMapper.ValidateGenerationOptions(options);
3031

31-
var (prompt, imageContents) = ChatMessageMapper.ToCodexInput(messages);
32+
var (prompt, imageContents) = ChatMessageMapper.ToCodexInput(messages, options?.Instructions);
3233
var threadOptions = ChatOptionsMapper.ToThreadOptions(options, _options);
3334
var turnOptions = ChatOptionsMapper.ToTurnOptions(options, cancellationToken);
3435

@@ -51,8 +52,9 @@ public async IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(
5152
{
5253
ArgumentNullException.ThrowIfNull(messages);
5354
ChatOptionsMapper.ValidateFunctionCallingOptions(options);
55+
ChatOptionsMapper.ValidateGenerationOptions(options);
5456

55-
var (prompt, imageContents) = ChatMessageMapper.ToCodexInput(messages);
57+
var (prompt, imageContents) = ChatMessageMapper.ToCodexInput(messages, options?.Instructions);
5658
var threadOptions = ChatOptionsMapper.ToThreadOptions(options, _options);
5759
var turnOptions = ChatOptionsMapper.ToTurnOptions(options, cancellationToken);
5860

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,13 @@ internal static class ChatMessageMapper
1010
private const string ParagraphSeparator = "\n\n";
1111
private const string ImageMediaPrefix = "image/";
1212

13-
internal static (string Prompt, List<DataContent> ImageContents) ToCodexInput(IEnumerable<ChatMessage> messages)
13+
internal static (string Prompt, List<DataContent> ImageContents) ToCodexInput(IEnumerable<ChatMessage> messages, string? instructions = null)
1414
{
1515
var promptParts = new List<string>();
16+
if (!string.IsNullOrWhiteSpace(instructions))
17+
{
18+
promptParts.Add(string.Concat(SystemPrefix, instructions));
19+
}
1620
var imageContents = new List<DataContent>();
1721

1822
foreach (var message in messages)

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

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

66
internal static class ChatOptionsMapper
77
{
8+
private const string GenerationOptionsUnsupportedMessage = "The Codex CLI adapter does not support the requested MEAI generation options.";
89
private const string FunctionToolsUnsupportedMessage = "MEAI function tools are not supported by the Codex CLI adapter.";
910
private const string ToolModeUnsupportedMessage = "Only automatic MEAI tool mode is supported by the Codex CLI adapter.";
1011
internal const string SandboxModeKey = "codex:sandbox_mode";
@@ -18,6 +19,17 @@ internal static class ChatOptionsMapper
1819
internal const string ProfileKey = "codex:profile";
1920
internal const string SkipGitRepoCheckKey = "codex:skip_git_repo_check";
2021

22+
internal static void ValidateGenerationOptions(ChatOptions? options)
23+
{
24+
if (options is { Temperature: not null } or { TopP: not null } or { TopK: not null } or
25+
{ MaxOutputTokens: not null } or { Seed: not null } or { FrequencyPenalty: not null } or
26+
{ PresencePenalty: not null } || options?.StopSequences is { Count: > 0 } ||
27+
options?.ResponseFormat is { } format && format is not ChatResponseFormatText)
28+
{
29+
throw new NotSupportedException(GenerationOptionsUnsupportedMessage);
30+
}
31+
}
32+
2133
internal static void ValidateFunctionCallingOptions(ChatOptions? chatOptions)
2234
{
2335
if (chatOptions?.Tools is { Count: > 0 })

‎CodexSharpSDK.Tests/MEAI/ChatMessageMapperTests.cs‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,19 @@ namespace ManagedCode.CodexSharpSDK.Extensions.AI.Tests;
66

77
public class ChatMessageMapperTests
88
{
9+
private const string StandardInstructions = "Follow the standard MEAI instructions.";
10+
private const string ExistingSystemText = "Existing system message.";
11+
private const string UserQuestion = "User question.";
12+
private const string ExpectedInstructionPrompt = "[System] Follow the standard MEAI instructions.\n\n[System] Existing system message.\n\nUser question.";
13+
14+
[Test]
15+
public async Task StandardInstructionsPrecedeSystemAndUserMessages()
16+
{
17+
var messages = new[] { new ChatMessage(ChatRole.System, ExistingSystemText), new ChatMessage(ChatRole.User, UserQuestion) };
18+
var (prompt, _) = ChatMessageMapper.ToCodexInput(messages, StandardInstructions);
19+
await Assert.That(prompt).IsEqualTo(ExpectedInstructionPrompt);
20+
}
21+
922
[Test]
1023
public async Task ToCodexInput_TextOnly_ReturnsPrompt()
1124
{

‎CodexSharpSDK.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
@@ -32,7 +32,7 @@
3232
<PublishRepositoryUrl>true</PublishRepositoryUrl>
3333
<EmbedUntrackedSources>true</EmbedUntrackedSources>
3434
<EnablePackageValidation>true</EnablePackageValidation>
35-
<Version>0.160.1.3</Version>
35+
<Version>0.160.1.4</Version>
3636
<PackageVersion>$(Version)</PackageVersion>
3737
</PropertyGroup>
3838

‎docs/Features/meai-integration.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,3 +184,7 @@ Native provider failures stop producing response updates, but the adapter contin
184184
- All mapper and DI tests pass.
185185
- ADR and feature docs created.
186186
- Architecture overview updated.
187+
188+
## Standard MEAI request options
189+
190+
`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)