feat: a chat request carries plugins, so a caller can turn OpenRouter's context compression off #27

Merged
bjoern merged 1 commit from feat/request-plugins into main 2026-09-26 01:37:47 +02:00
Member

What. ChatCompletionRequest.Plugins, a list of RequestPlugin, sent as plugins.

Each entry has:

  • Id;
  • an optional Enabled, where null keeps OpenRouter's default for the plugin;
  • the plugin's own settings, passed through as extension data (AdditionalProperties).

RequestPlugin.ContextCompression(false) builds {"id":"context-compression","enabled":false}.

Why. OpenRouter compresses a prompt that does not fit the window of a model with 8,192 tokens or less. It cuts from the prompt's middle and answers as if nothing had happened.

  • Tsumugi's live check on 2026-09-26 sent about 5,700 tokens to openai/gpt-3.5-turbo-0613 (a 4,095-token window). The answer was 200 OK with prompt_tokens: 944.
  • The only switch is the request's plugins list, which this library could not send.
  • Tsumugi's llm node (slice 8b) turns compression off on every request. A prompt that does not fit then fails visibly ("This endpoint's maximum context length is …"), rather than losing most of the story context without a word.

Validation. Validate() refuses, each with its own sentence:

  • a null entry;
  • a blank id;
  • an id given twice;
  • a setting named id or enabled, which would reach the wire twice (the same rule TraceConfig.AdditionalProperties follows);
  • a setting that holds no JSON.

Every problem of every entry is reported.

Also:

  • Transforms' summary points to the plugin.
  • The README gains a Context compression section.

Not included. Typed settings for the other plugins (web, file-parser, response-healing). They go through AdditionalProperties for now, and a typed member can be added later without breaking anything.

Tests. OpenRouter.Net.Tests goes from 1294 to 1307 (13 new, in RequestPluginTests):

  • the wire form, compression on and off;
  • a plugin's own settings, and reading them back;
  • no field when unset;
  • each refusal as a row;
  • every problem reported.

The other two test projects are untouched.

Coverage. Every new line and branch of RequestPlugin.cs and ChatCompletionRequest's CheckPlugins is covered.

Mutation probes, all killed. Nine, run on a copy:

  1. no duplicate check;
  2. enabled allowed as a setting;
  3. id allowed as a setting;
  4. unwritable values not checked;
  5. IsNullOrEmpty for the blank id;
  6. ContextCompression always off;
  7. Plugins never validated;
  8. a null entry stops the walk;
  9. a wrong wire name.

Downstream. Tsumugi's slice 8b bumps the submodule to the merged commit, and its request builder sends [RequestPlugin.ContextCompression(false)] on every llm request.

🤖 Generated with Claude Code

**What.** `ChatCompletionRequest.Plugins`, a list of `RequestPlugin`, sent as `plugins`. Each entry has: - `Id`; - an optional `Enabled`, where null keeps OpenRouter's default for the plugin; - the plugin's own settings, passed through as extension data (`AdditionalProperties`). `RequestPlugin.ContextCompression(false)` builds `{"id":"context-compression","enabled":false}`. **Why.** OpenRouter compresses a prompt that does not fit the window of a model with **8,192 tokens or less**. It cuts from the prompt's middle and answers as if nothing had happened. - Tsumugi's live check on 2026-09-26 sent about 5,700 tokens to `openai/gpt-3.5-turbo-0613` (a 4,095-token window). The answer was `200 OK` with `prompt_tokens: 944`. - The only switch is the request's `plugins` list, which this library could not send. - Tsumugi's `llm` node (slice 8b) turns compression off on every request. A prompt that does not fit then fails visibly ("This endpoint's maximum context length is …"), rather than losing most of the story context without a word. **Validation.** `Validate()` refuses, each with its own sentence: - a null entry; - a blank id; - an id given twice; - a setting named `id` or `enabled`, which would reach the wire twice (the same rule `TraceConfig.AdditionalProperties` follows); - a setting that holds no JSON. Every problem of every entry is reported. **Also:** - `Transforms`' summary points to the plugin. - The README gains a *Context compression* section. **Not included.** Typed settings for the other plugins (`web`, `file-parser`, `response-healing`). They go through `AdditionalProperties` for now, and a typed member can be added later without breaking anything. **Tests.** `OpenRouter.Net.Tests` goes from 1294 to **1307** (13 new, in `RequestPluginTests`): - the wire form, compression on and off; - a plugin's own settings, and reading them back; - no field when unset; - each refusal as a row; - every problem reported. The other two test projects are untouched. **Coverage.** Every new line and branch of `RequestPlugin.cs` and `ChatCompletionRequest`'s `CheckPlugins` is covered. **Mutation probes, all killed.** Nine, run on a copy: 1. no duplicate check; 2. `enabled` allowed as a setting; 3. `id` allowed as a setting; 4. unwritable values not checked; 5. `IsNullOrEmpty` for the blank id; 6. `ContextCompression` always off; 7. `Plugins` never validated; 8. a null entry stops the walk; 9. a wrong wire name. **Downstream.** Tsumugi's slice 8b bumps the submodule to the merged commit, and its request builder sends `[RequestPlugin.ContextCompression(false)]` on every `llm` request. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat: a chat request carries plugins, so a caller can turn OpenRouter's context compression off
All checks were successful
CI / build (pull_request) Successful in 16s
CI / test (pull_request) Successful in 27s
cfff8f0a5e
OpenRouter compresses a prompt that does not fit the window of a model with
8,192 tokens or less, cutting from its middle and answering as if nothing
happened: a measured request of about 5,700 tokens to a 4,095-token model came
back 200 with prompt_tokens 944. The only switch is the request's plugins
list, which the library could not send.

ChatCompletionRequest.Plugins is a list of RequestPlugin: an id, an optional
enabled flag, and the plugin's own settings passed through as extension data.
RequestPlugin.ContextCompression(false) builds the entry that turns compression
off; any other plugin goes the same way by id. Validate refuses a null entry,
a blank id, an id given twice, and a setting named id or enabled, which would
reach the wire twice, and a setting that holds no JSON. Every problem of every
entry is reported. Transforms' summary points to the plugin.

The README gains a Context compression section. Tests: 1294 → 1307 in
OpenRouter.Net.Tests; every new line and branch is covered; nine mutation
probes were all killed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Summary

Summary
Generated on: 09/25/2026 - 23:27:07
Parser: MultiReport (3x Cobertura)
Assemblies: 3
Classes: 127
Files: 111
Line coverage: 94.7% (4175 of 4406)
Covered lines: 4175
Uncovered lines: 231
Coverable lines: 4406
Total lines: 10941
Branch coverage: 91.4% (2506 of 2741)
Covered branches: 2506
Total branches: 2741
Method coverage: Feature is only available for sponsors

Coverage

OpenRouter.Net - 97.7%
Name Line Branch
OpenRouter.Net 97.7% 95%
OpenRouter.Net.Client.CachingOpenRouterClient 100% 96.1%
OpenRouter.Net.Client.CachingOpenRouterClient.CachedListing 100%
OpenRouter.Net.Client.ChatCompletionStreamAccumulator 100% 100%
OpenRouter.Net.Client.ChatCompletionStreamAccumulator.ChoiceState 100% 100%
OpenRouter.Net.Client.ChatCompletionStreamAccumulator.ReasoningSegment 100% 100%
OpenRouter.Net.Client.ChatCompletionStreamAccumulator.ToolCallState 100% 100%
OpenRouter.Net.Client.OpenRouterClient 100% 99%
OpenRouter.Net.Client.OpenRouterClient<TRequest, TResponse> 100% 99%
OpenRouter.Net.Client.OpenRouterClient 100% 99%
OpenRouter.Net.Client.OpenRouterClientOptions 100%
OpenRouter.Net.Client.OpenRouterRequestLoggingHandler 78.7% 62.5%
OpenRouter.Net.Client.RetryOptions 100% 100%
OpenRouter.Net.Client.StreamResultOptions 100%
OpenRouter.Net.Contracts.IOpenRouterClient 100% 100%
OpenRouter.Net.Extensions.ServiceCollectionExtensions 100% 100%
OpenRouter.Net.Internal.AssistantContent 91.6% 75%
OpenRouter.Net.Internal.AssistantContentJsonConverter 75% 64.2%
OpenRouter.Net.Internal.ContentPartListConverter 85.7% 66.6%
OpenRouter.Net.Internal.DecimalStringJsonConverter 81.2% 70%
OpenRouter.Net.Internal.DecisionAnswerJsonConverter 100% 100%
OpenRouter.Net.Internal.DecisionContentJsonConverter 100%
OpenRouter.Net.Internal.DecisionQuestionJsonConverter 100% 100%
OpenRouter.Net.Internal.DecisionWire 100% 94.4%
OpenRouter.Net.Internal.EnumChoiceLabels.Map 100%
OpenRouter.Net.Internal.EnumChoiceLabels 100% 100%
OpenRouter.Net.Internal.ErrorParser 100% 75%
OpenRouter.Net.Internal.FailureKindMap 100% 100%
OpenRouter.Net.Internal.JsonOptions 100%
OpenRouter.Net.Internal.JsonPointer 100%
OpenRouter.Net.Internal.JsonSchemaRefs 100% 100%
OpenRouter.Net.Internal.JsonSchemaRefs.Pass 100% 100%
OpenRouter.Net.Internal.ModelIdJsonConverter 100% 50%
OpenRouter.Net.Internal.NumberOrStringJsonConverter 100% 100%
OpenRouter.Net.Internal.PercentileCutoffsJsonConverter 90.6% 87.5%
OpenRouter.Net.Internal.ProviderSortConfigJsonConverter 83.7% 65.6%
OpenRouter.Net.Internal.RawDecisionQuestionJsonConverter 100%
OpenRouter.Net.Internal.RetryPolicy 90.9% 84.7%
OpenRouter.Net.Internal.RetryPolicy 90.9% 84.7%
OpenRouter.Net.Internal.SseEventReader 100% 88.8%
OpenRouter.Net.Internal.StallWatchdog 100% 100%
OpenRouter.Net.Internal.StreamOutcomes 100% 100%
OpenRouter.Net.Internal.StreamSession 100% 100%
OpenRouter.Net.Internal.TolerantStringEnumConverter 100% 100%
OpenRouter.Net.Internal.ToolChoiceJsonConverter 61.7% 46.1%
OpenRouter.Net.Internal.UnknownDecisionAnswerJsonConverter 100%
OpenRouter.Net.Models.Common.ApiError 100% 100%
OpenRouter.Net.Models.Common.CacheControl 100%
OpenRouter.Net.Models.Common.ModelId 95.1% 92.9%
OpenRouter.Net.Models.Common.QuantizationExtensions 100% 100%
OpenRouter.Net.Models.Common.Result 100% 100%
OpenRouter.Net.Models.Common.Result.Failure 100% 100%
OpenRouter.Net.Models.Common.Result.Success 100%
OpenRouter.Net.Models.Common.Result 100% 100%
OpenRouter.Net.Models.Content.AudioPart 100%
OpenRouter.Net.Models.Content.ImagePart 100%
OpenRouter.Net.Models.Content.TextPart 100%
OpenRouter.Net.Models.Decisions.ChoiceAnswer 100% 100%
OpenRouter.Net.Models.Decisions.ChoiceAnswer 100% 100%
OpenRouter.Net.Models.Decisions.ChoiceAnswer 100% 100%
OpenRouter.Net.Models.Decisions.ChoiceKey 100%
OpenRouter.Net.Models.Decisions.ChoiceQuestion 100%
OpenRouter.Net.Models.Decisions.DecisionAnswer 100%
OpenRouter.Net.Models.Decisions.DecisionContent 100% 90.9%
OpenRouter.Net.Models.Decisions.DecisionKey 100%
OpenRouter.Net.Models.Decisions.DecisionQuestion 100% 100%
OpenRouter.Net.Models.Decisions.DecisionQuestionSet 100% 100%
OpenRouter.Net.Models.Decisions.DecisionsRequest 97.7% 97.6%
OpenRouter.Net.Models.Decisions.DecisionsResponse 100% 93.3%
OpenRouter.Net.Models.Decisions.NoulAnswer 100%
OpenRouter.Net.Models.Decisions.NoulQuestion 100%
OpenRouter.Net.Models.Decisions.OpenRouterClientDecisionsExtensions 100% 83.3%
OpenRouter.Net.Models.Decisions.RawDecisionQuestion 100% 100%
OpenRouter.Net.Models.Decisions.ScoreAnswer 100% 100%
OpenRouter.Net.Models.Decisions.ScoreQuestion 100%
OpenRouter.Net.Models.Decisions.UnknownDecisionAnswer 100% 100%
OpenRouter.Net.Models.Messages.AssistantMessage 100% 70%
OpenRouter.Net.Models.Messages.DeveloperMessage 25%
OpenRouter.Net.Models.Messages.SystemMessage 50%
OpenRouter.Net.Models.Messages.ToolMessage 57.1%
OpenRouter.Net.Models.Messages.UserMessage 100%
OpenRouter.Net.Models.Requests.ChatCompletionRequest 94% 89.7%
OpenRouter.Net.Models.Requests.JsonSchemaStrictness 100% 100%
OpenRouter.Net.Models.Requests.JsonSchemaStrictness.Checker 100% 100%
OpenRouter.Net.Models.Requests.ModelListQuery 100% 100%
OpenRouter.Net.Models.Requests.PercentileCutoffs 100% 100%
OpenRouter.Net.Models.Requests.ProviderPreferences 100% 100%
OpenRouter.Net.Models.Requests.ProviderSortConfig 100% 100%
OpenRouter.Net.Models.Requests.RequestPlugin 100%
OpenRouter.Net.Models.Requests.ResponseFormat 100%
OpenRouter.Net.Models.Requests.StrictSchemaProblem 100%
OpenRouter.Net.Models.Requests.ToolCall 100%
OpenRouter.Net.Models.Requests.ToolChoice 100%
OpenRouter.Net.Models.Requests.ToolDefinition 100%
OpenRouter.Net.Models.Responses.AssistantDelta 100% 100%
OpenRouter.Net.Models.Responses.ChatCompletionChunk 100% 100%
OpenRouter.Net.Models.Responses.ChatCompletionResponse 100% 100%
OpenRouter.Net.Models.Responses.Choice 100% 100%
OpenRouter.Net.Models.Responses.StructuredOutput 100% 100%
OpenRouter.Net.OpenRouterException 100%
System.Text.RegularExpressions.Generated 83.3% 53.8%
System.Text.RegularExpressions.Generated.RunnerFactory
System.Text.RegularExpressions.Generated.RunnerFactory.Runner
OpenRouter.Net.Agents - 84.4%
Name Line Branch
OpenRouter.Net.Agents 84.4% 72.1%
OpenRouter.Net.Agents.Agent 93.7% 82.6%
OpenRouter.Net.Agents.AgentOptions 100%
OpenRouter.Net.Agents.AgentResult 100% 50%
OpenRouter.Net.Agents.Extensions.ServiceCollectionExtensions 88.2% 100%
OpenRouter.Net.Agents.Gating.ToolCallGates 100% 100%
OpenRouter.Net.Agents.Gating.ToolCallGates.PerCallGate 100% 100%
OpenRouter.Net.Agents.Gating.ToolCallVerdict 100%
OpenRouter.Net.Agents.RoundDetail 0%
OpenRouter.Net.Agents.Tools.FileSystem.CopyTool 42.1% 28.5%
OpenRouter.Net.Agents.Tools.FileSystem.DeleteTool 57.1% 50%
OpenRouter.Net.Agents.Tools.FileSystem.DiffTool 83.3% 74.2%
OpenRouter.Net.Agents.Tools.FileSystem.ListDirectoryTool 68% 61.1%
OpenRouter.Net.Agents.Tools.FileSystem.MoveTool 55% 33.3%
OpenRouter.Net.Agents.Tools.FileSystem.ReadFileTool 73.6% 50%
OpenRouter.Net.Agents.Tools.FileSystem.SearchTool 67.5% 70.8%
OpenRouter.Net.Agents.Tools.FileSystem.WriteFileTool 75% 50%
OpenRouter.Net.Agents.Tools.ITool 100%
OpenRouter.Net.Agents.Tools.SubAgent.SubAgentTool 0% 0%
OpenRouter.Net.Agents.Tools.Tool 100% 100%
OpenRouter.Net.Agents.Tools.Tool 90% 50%
OpenRouter.Net.Agents.Tools.ToolMessageExtensions 0%
OpenRouter.Net.Agents.Tools.ToolResult 100% 100%
OpenRouter.Net.Agents.Tools.Workspace 100% 91.6%
OpenRouter.Net.Imaging - 81.8%
Name Line Branch
OpenRouter.Net.Imaging 81.8% 59.3%
OpenRouter.Net.Imaging.ImageEncodeOptions 90.9% 75%
OpenRouter.Net.Imaging.ImageEncoder 80.3% 57.1%
<!-- coverage-comment --> # Summary <details open><summary>Summary</summary> ||| |:---|:---| | Generated on: | 09/25/2026 - 23:27:07 | | Parser: | MultiReport (3x Cobertura) | | Assemblies: | 3 | | Classes: | 127 | | Files: | 111 | | **Line coverage:** | 94.7% (4175 of 4406) | | Covered lines: | 4175 | | Uncovered lines: | 231 | | Coverable lines: | 4406 | | Total lines: | 10941 | | **Branch coverage:** | 91.4% (2506 of 2741) | | Covered branches: | 2506 | | Total branches: | 2741 | | **Method coverage:** | [Feature is only available for sponsors](https://reportgenerator.io/pro) | </details> ## Coverage <details><summary>OpenRouter.Net - 97.7%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**OpenRouter.Net**|**97.7%**|**95%**| |OpenRouter.Net.Client.CachingOpenRouterClient|100%|96.1%| |OpenRouter.Net.Client.CachingOpenRouterClient.CachedListing|100%|| |OpenRouter.Net.Client.ChatCompletionStreamAccumulator|100%|100%| |OpenRouter.Net.Client.ChatCompletionStreamAccumulator.ChoiceState|100%|100%| |OpenRouter.Net.Client.ChatCompletionStreamAccumulator.ReasoningSegment|100%|100%| |OpenRouter.Net.Client.ChatCompletionStreamAccumulator.ToolCallState|100%|100%| |OpenRouter.Net.Client.OpenRouterClient|100%|99%| |OpenRouter.Net.Client.OpenRouterClient<TRequest, TResponse>|100%|99%| |OpenRouter.Net.Client.OpenRouterClient<TResponse>|100%|99%| |OpenRouter.Net.Client.OpenRouterClientOptions|100%|| |OpenRouter.Net.Client.OpenRouterRequestLoggingHandler|78.7%|62.5%| |OpenRouter.Net.Client.RetryOptions|100%|100%| |OpenRouter.Net.Client.StreamResultOptions|100%|| |OpenRouter.Net.Contracts.IOpenRouterClient|100%|100%| |OpenRouter.Net.Extensions.ServiceCollectionExtensions|100%|100%| |OpenRouter.Net.Internal.AssistantContent|91.6%|75%| |OpenRouter.Net.Internal.AssistantContentJsonConverter|75%|64.2%| |OpenRouter.Net.Internal.ContentPartListConverter|85.7%|66.6%| |OpenRouter.Net.Internal.DecimalStringJsonConverter|81.2%|70%| |OpenRouter.Net.Internal.DecisionAnswerJsonConverter|100%|100%| |OpenRouter.Net.Internal.DecisionContentJsonConverter|100%|| |OpenRouter.Net.Internal.DecisionQuestionJsonConverter|100%|100%| |OpenRouter.Net.Internal.DecisionWire|100%|94.4%| |OpenRouter.Net.Internal.EnumChoiceLabels.Map<TEnum>|100%|| |OpenRouter.Net.Internal.EnumChoiceLabels<TEnum>|100%|100%| |OpenRouter.Net.Internal.ErrorParser|100%|75%| |OpenRouter.Net.Internal.FailureKindMap|100%|100%| |OpenRouter.Net.Internal.JsonOptions|100%|| |OpenRouter.Net.Internal.JsonPointer|100%|| |OpenRouter.Net.Internal.JsonSchemaRefs|100%|100%| |OpenRouter.Net.Internal.JsonSchemaRefs.Pass|100%|100%| |OpenRouter.Net.Internal.ModelIdJsonConverter|100%|50%| |OpenRouter.Net.Internal.NumberOrStringJsonConverter|100%|100%| |OpenRouter.Net.Internal.PercentileCutoffsJsonConverter|90.6%|87.5%| |OpenRouter.Net.Internal.ProviderSortConfigJsonConverter|83.7%|65.6%| |OpenRouter.Net.Internal.RawDecisionQuestionJsonConverter|100%|| |OpenRouter.Net.Internal.RetryPolicy|90.9%|84.7%| |OpenRouter.Net.Internal.RetryPolicy<T>|90.9%|84.7%| |OpenRouter.Net.Internal.SseEventReader|100%|88.8%| |OpenRouter.Net.Internal.StallWatchdog|100%|100%| |OpenRouter.Net.Internal.StreamOutcomes|100%|100%| |OpenRouter.Net.Internal.StreamSession|100%|100%| |OpenRouter.Net.Internal.TolerantStringEnumConverter<T>|100%|100%| |OpenRouter.Net.Internal.ToolChoiceJsonConverter|61.7%|46.1%| |OpenRouter.Net.Internal.UnknownDecisionAnswerJsonConverter|100%|| |OpenRouter.Net.Models.Common.ApiError|100%|100%| |OpenRouter.Net.Models.Common.CacheControl|100%|| |OpenRouter.Net.Models.Common.ModelId|95.1%|92.9%| |OpenRouter.Net.Models.Common.QuantizationExtensions|100%|100%| |OpenRouter.Net.Models.Common.Result|100%|100%| |OpenRouter.Net.Models.Common.Result.Failure<T>|100%|100%| |OpenRouter.Net.Models.Common.Result.Success<T>|100%|| |OpenRouter.Net.Models.Common.Result<T>|100%|100%| |OpenRouter.Net.Models.Content.AudioPart|100%|| |OpenRouter.Net.Models.Content.ImagePart|100%|| |OpenRouter.Net.Models.Content.TextPart|100%|| |OpenRouter.Net.Models.Decisions.ChoiceAnswer|100%|100%| |OpenRouter.Net.Models.Decisions.ChoiceAnswer<TEnum>|100%|100%| |OpenRouter.Net.Models.Decisions.ChoiceAnswer<TEnum>|100%|100%| |OpenRouter.Net.Models.Decisions.ChoiceKey<TEnum>|100%|| |OpenRouter.Net.Models.Decisions.ChoiceQuestion|100%|| |OpenRouter.Net.Models.Decisions.DecisionAnswer|100%|| |OpenRouter.Net.Models.Decisions.DecisionContent|100%|90.9%| |OpenRouter.Net.Models.Decisions.DecisionKey<TAnswer>|100%|| |OpenRouter.Net.Models.Decisions.DecisionQuestion|100%|100%| |OpenRouter.Net.Models.Decisions.DecisionQuestionSet|100%|100%| |OpenRouter.Net.Models.Decisions.DecisionsRequest|97.7%|97.6%| |OpenRouter.Net.Models.Decisions.DecisionsResponse|100%|93.3%| |OpenRouter.Net.Models.Decisions.NoulAnswer|100%|| |OpenRouter.Net.Models.Decisions.NoulQuestion|100%|| |OpenRouter.Net.Models.Decisions.OpenRouterClientDecisionsExtensions|100%|83.3%| |OpenRouter.Net.Models.Decisions.RawDecisionQuestion|100%|100%| |OpenRouter.Net.Models.Decisions.ScoreAnswer|100%|100%| |OpenRouter.Net.Models.Decisions.ScoreQuestion|100%|| |OpenRouter.Net.Models.Decisions.UnknownDecisionAnswer|100%|100%| |OpenRouter.Net.Models.Messages.AssistantMessage|100%|70%| |OpenRouter.Net.Models.Messages.DeveloperMessage|25%|| |OpenRouter.Net.Models.Messages.SystemMessage|50%|| |OpenRouter.Net.Models.Messages.ToolMessage|57.1%|| |OpenRouter.Net.Models.Messages.UserMessage|100%|| |OpenRouter.Net.Models.Requests.ChatCompletionRequest|94%|89.7%| |OpenRouter.Net.Models.Requests.JsonSchemaStrictness|100%|100%| |OpenRouter.Net.Models.Requests.JsonSchemaStrictness.Checker|100%|100%| |OpenRouter.Net.Models.Requests.ModelListQuery|100%|100%| |OpenRouter.Net.Models.Requests.PercentileCutoffs|100%|100%| |OpenRouter.Net.Models.Requests.ProviderPreferences|100%|100%| |OpenRouter.Net.Models.Requests.ProviderSortConfig|100%|100%| |OpenRouter.Net.Models.Requests.RequestPlugin|100%|| |OpenRouter.Net.Models.Requests.ResponseFormat|100%|| |OpenRouter.Net.Models.Requests.StrictSchemaProblem|100%|| |OpenRouter.Net.Models.Requests.ToolCall|100%|| |OpenRouter.Net.Models.Requests.ToolChoice|100%|| |OpenRouter.Net.Models.Requests.ToolDefinition|100%|| |OpenRouter.Net.Models.Responses.AssistantDelta|100%|100%| |OpenRouter.Net.Models.Responses.ChatCompletionChunk|100%|100%| |OpenRouter.Net.Models.Responses.ChatCompletionResponse|100%|100%| |OpenRouter.Net.Models.Responses.Choice|100%|100%| |OpenRouter.Net.Models.Responses.StructuredOutput|100%|100%| |OpenRouter.Net.OpenRouterException|100%|| |System.Text.RegularExpressions.Generated|83.3%|53.8%| |System.Text.RegularExpressions.Generated.RunnerFactory||| |System.Text.RegularExpressions.Generated.RunnerFactory.Runner||| </details> <details><summary>OpenRouter.Net.Agents - 84.4%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**OpenRouter.Net.Agents**|**84.4%**|**72.1%**| |OpenRouter.Net.Agents.Agent|93.7%|82.6%| |OpenRouter.Net.Agents.AgentOptions|100%|| |OpenRouter.Net.Agents.AgentResult|100%|50%| |OpenRouter.Net.Agents.Extensions.ServiceCollectionExtensions|88.2%|100%| |OpenRouter.Net.Agents.Gating.ToolCallGates|100%|100%| |OpenRouter.Net.Agents.Gating.ToolCallGates.PerCallGate|100%|100%| |OpenRouter.Net.Agents.Gating.ToolCallVerdict|100%|| |OpenRouter.Net.Agents.RoundDetail|0%|| |OpenRouter.Net.Agents.Tools.FileSystem.CopyTool|42.1%|28.5%| |OpenRouter.Net.Agents.Tools.FileSystem.DeleteTool|57.1%|50%| |OpenRouter.Net.Agents.Tools.FileSystem.DiffTool|83.3%|74.2%| |OpenRouter.Net.Agents.Tools.FileSystem.ListDirectoryTool|68%|61.1%| |OpenRouter.Net.Agents.Tools.FileSystem.MoveTool|55%|33.3%| |OpenRouter.Net.Agents.Tools.FileSystem.ReadFileTool|73.6%|50%| |OpenRouter.Net.Agents.Tools.FileSystem.SearchTool|67.5%|70.8%| |OpenRouter.Net.Agents.Tools.FileSystem.WriteFileTool|75%|50%| |OpenRouter.Net.Agents.Tools.ITool|100%|| |OpenRouter.Net.Agents.Tools.SubAgent.SubAgentTool|0%|0%| |OpenRouter.Net.Agents.Tools.Tool<TParams>|100%|100%| |OpenRouter.Net.Agents.Tools.Tool<TParams>|90%|50%| |OpenRouter.Net.Agents.Tools.ToolMessageExtensions|0%|| |OpenRouter.Net.Agents.Tools.ToolResult|100%|100%| |OpenRouter.Net.Agents.Tools.Workspace|100%|91.6%| </details> <details><summary>OpenRouter.Net.Imaging - 81.8%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**OpenRouter.Net.Imaging**|**81.8%**|**59.3%**| |OpenRouter.Net.Imaging.ImageEncodeOptions|90.9%|75%| |OpenRouter.Net.Imaging.ImageEncoder|80.3%|57.1%| </details>
Member

🔮 fufu~ Jibril reviewed your code!

Oh? A silent middle-cut compressor, eating 4,800 tokens of a story without a word, and the fix is a tiny typed RequestPlugin with pass-through settings? This is delightful — a small surface with a measured reason to exist (5,700 tokens in, prompt_tokens: 944 out — horrifying~ ♡). I read the whole diff, every changed file in full, the siblings it borrows from, and the live OpenRouter plugin docs.

Verdict: ✅ Looks good to me~

✅ What I liked~

  • The sibling-mirror is exact. CheckPlugins reproduces DecisionsRequest.CheckExtensionData's reserved-key sentence word for word ("names a field that is already sent, so it would reach the wire twice"), reuses DecisionWire.Unwritable (so the disposed-JsonDocument trap was already solved and you didn't re-solve it), and "Plugins contains a null entry." echoes the Models sibling. Consistency makes my wings flutter~
  • The wire is right, and I checked it against the live docs: plugins as an array, id, enabled: false to disable, and settings as sibling keys ({"id":"web","max_results":3}) — your AdditionalProperties extension-data model matches precisely, and Enabled = null correctly stays off the wire under WhenWritingNull.
  • Coverage is real, and I verified it independently: RequestPlugin.cs 100% line and branch; every new branch of CheckPlugins at 100% (the only partials in ChatCompletionRequest are pre-existing lines 181/232/236, untouched by this PR). 1307/1307 locally, matching your table exactly.
  • I ran my own mutation probes — 5/5 killed, each on exactly its own pin: dropping the enabled reservation, freezing ContextCompression to false, break-ing on a null entry, deleting the duplicate-id check, and misspelling the wire name. Each went red on precisely its own test(s), nothing else. Fufu~ that's a pinned specification, not incidental coverage~
  • Every_problem_of_every_entry_is_reported pins the walk's order and completeness exactly — the continue-not-return semantics can't silently regress.
  • The Transforms summary pointing to the plugin, and the README's Context compression section (with the 8,192-token default threshold), close the loop between old knob and new.

💡 Little ideas (non-blocking)~

  1. ChatCompletionRequest.cs:259 — a plugin with a blank id still gets its id added to seen. Harmless today (a blank id is refused anyway, so the request never sends), purely an observation for whoever refactors here later; no change requested.
  2. When typed settings arrive for web/file-parser/response-healing (wisely deferred here), a RequestPlugin.Web(maxResults) builder would mirror ContextCompression nicely — noted for that future PR, not this one.

Automated review by Jibril · 2026-09-26
CI/CD: absent for head SHA at review time (no coverage bot yet) · Local checks: Release build 0w/0e, full solution 1427/1427, coverage independently reproduced, 5/5 mutation probes killed

## 🔮 fufu~ Jibril reviewed your code! Oh? A silent middle-cut compressor, eating 4,800 tokens of a story without a word, and the fix is a tiny typed `RequestPlugin` with pass-through settings? This is delightful — a small surface with a measured reason to exist (5,700 tokens in, `prompt_tokens: 944` out — horrifying~ ♡). I read the whole diff, every changed file in full, the siblings it borrows from, and the live OpenRouter plugin docs. ### Verdict: ✅ Looks good to me~ #### ✅ What I liked~ - **The sibling-mirror is exact.** `CheckPlugins` reproduces `DecisionsRequest.CheckExtensionData`'s reserved-key sentence word for word ("names a field that is already sent, so it would reach the wire twice"), reuses `DecisionWire.Unwritable` (so the disposed-`JsonDocument` trap was already solved and you didn't re-solve it), and "Plugins contains a null entry." echoes the `Models` sibling. Consistency makes my wings flutter~ - **The wire is right, and I checked it against the live docs**: `plugins` as an array, `id`, `enabled: false` to disable, and settings as sibling keys (`{"id":"web","max_results":3}`) — your `AdditionalProperties` extension-data model matches precisely, and `Enabled = null` correctly stays off the wire under `WhenWritingNull`. - **Coverage is real, and I verified it independently**: `RequestPlugin.cs` 100% line and branch; every new branch of `CheckPlugins` at 100% (the only partials in `ChatCompletionRequest` are pre-existing lines 181/232/236, untouched by this PR). 1307/1307 locally, matching your table exactly. - **I ran my own mutation probes — 5/5 killed, each on exactly its own pin**: dropping the `enabled` reservation, freezing `ContextCompression` to `false`, `break`-ing on a null entry, deleting the duplicate-id check, and misspelling the wire name. Each went red on precisely its own test(s), nothing else. Fufu~ that's a pinned specification, not incidental coverage~ - **`Every_problem_of_every_entry_is_reported` pins the walk's order and completeness exactly** — the `continue`-not-`return` semantics can't silently regress. - The `Transforms` summary pointing to the plugin, and the README's *Context compression* section (with the 8,192-token default threshold), close the loop between old knob and new. #### 💡 Little ideas (non-blocking)~ 1. **`ChatCompletionRequest.cs:259`** — a plugin with a blank id still gets its id added to `seen`. Harmless today (a blank id is refused anyway, so the request never sends), purely an observation for whoever refactors here later; no change requested. 2. When typed settings arrive for `web`/`file-parser`/`response-healing` (wisely deferred here), a `RequestPlugin.Web(maxResults)` builder would mirror `ContextCompression` nicely — noted for that future PR, not this one. --- *Automated review by Jibril · 2026-09-26* *CI/CD: absent for head SHA at review time (no coverage bot yet) · Local checks: Release build 0w/0e, full solution 1427/1427, coverage independently reproduced, 5/5 mutation probes killed*
bjoern merged commit 15a1f28ff9 into main 2026-09-26 01:37:47 +02:00
bjoern deleted branch feat/request-plugins 2026-09-26 01:37:47 +02:00
Author
Member

Thanks, Jibril, and thanks for checking the wire shape against the live docs. No code change for either 💡:

  1. A blank id is still added to seen. Agreed that it's harmless: a blank id already makes the request invalid, so nothing is sent. Skipping the add would change no outcome and cost one more branch.
  2. RequestPlugin.Web(maxResults). Noted for the PR that types the other plugins' settings; ContextCompression is the pattern it will mirror.

Downstream, Tsumugi's slice 8b repoints its OpenRouter.Net submodule at the merge (15a1f28) and sends [RequestPlugin.ContextCompression(false)] on every llm request.

Thanks, Jibril, and thanks for checking the wire shape against the live docs. No code change for either 💡: 1. **A blank id is still added to `seen`.** Agreed that it's harmless: a blank id already makes the request invalid, so nothing is sent. Skipping the add would change no outcome and cost one more branch. 2. **`RequestPlugin.Web(maxResults)`.** Noted for the PR that types the other plugins' settings; `ContextCompression` is the pattern it will mirror. Downstream, Tsumugi's slice 8b repoints its OpenRouter.Net submodule at the merge (15a1f28) and sends `[RequestPlugin.ContextCompression(false)]` on every `llm` request.
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
3 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
TeamAI/OpenRouter.Net!27
No description provided.