feat(v5): Diffusion 5 support with model param, per-character undesired content #7

Merged
bjoern merged 2 commits from feat/v5-model into main 2026-08-21 10:08:18 +02:00
Member

Summary

Exposes the freshly merged NovelAI.ImageGen v5 tree (see TeamAI/NovelAi.ImageGen#3) through generate_image.

  • Submodule bump to c997624 — the v5 request tree with multipart transport, per-character undesired content (HAR-verified mirror semantics), transparent backgrounds, cache-key references
  • model parameterv5 (default) or v4; unknown values fall back to v5. The V4 path is now explicit opt-in.
  • undesired_tags per character — maps to Character.NegativeTags on both V4 and V5 (both trees carry the same mirror semantics: char_uc/uc + negative-tree char_caption, library-verified against official client captures)
  • transparent_background — V5-only alpha backgrounds (the V4 request has no such field); ignored on V4
  • Fix: duplicated <InternalsVisibleTo> ItemGroup in the csproj (copy-paste artifact)

Behavior notes

  • Default flip: calls that previously produced Diffusion 4.5 images now produce Diffusion 5 images (same steps/scale/sampler defaults the library bakes per HAR evidence). Dimensions, quality tags, seed, aspect-ratio handling unchanged.
  • V5 bakes ucPresetId: "heavy" server-side, so the existing QualityTags.Negative list applies on top of the heavy preset — that's a knob to revisit after live testing (fewer negatives may be better on v5).

Testing

  • 174/174 tests (11 new): default=v5, explicit v4 (incl. seed passthrough), unknown-model fallback, undesired-tags mapping on both V5 and V4, no-undesired null arm, transparent background, transparent-ignored-but-undesired-mapped on V4, square aspect + boy/other gender parse arms, plain-error failure arm, extended JSON deserialization round-trip
  • Cobertura verified incl. compiler-generated state machines: GenerateImageTool, GenerateImageParameters, CharacterInput, <GenerateImageAsync>d__5 at 100% line + 100% branch
  • dotnet build 0 warnings

Not covered

  • img2img/inpaint via MCP (the library supports both; no MCP surface yet — follow-up)
  • Vibe transfer / precise reference (blocked upstream: no captures)
## Summary Exposes the freshly merged NovelAI.ImageGen v5 tree (see TeamAI/NovelAi.ImageGen#3) through `generate_image`. - **Submodule bump** to `c997624` — the v5 request tree with multipart transport, per-character undesired content (HAR-verified mirror semantics), transparent backgrounds, cache-key references - **`model` parameter** — `v5` (default) or `v4`; unknown values fall back to `v5`. The V4 path is now explicit opt-in. - **`undesired_tags` per character** — maps to `Character.NegativeTags` on **both** V4 and V5 (both trees carry the same mirror semantics: `char_uc`/`uc` + negative-tree `char_caption`, library-verified against official client captures) - **`transparent_background`** — V5-only alpha backgrounds (the V4 request has no such field); ignored on V4 - **Fix**: duplicated `<InternalsVisibleTo>` ItemGroup in the csproj (copy-paste artifact) ## Behavior notes - Default flip: calls that previously produced Diffusion 4.5 images now produce Diffusion 5 images (same steps/scale/sampler defaults the library bakes per HAR evidence). Dimensions, quality tags, seed, aspect-ratio handling unchanged. - V5 bakes `ucPresetId: "heavy"` server-side, so the existing `QualityTags.Negative` list applies on top of the heavy preset — that's a knob to revisit after live testing (fewer negatives may be better on v5). ## Testing - 174/174 tests (11 new): default=v5, explicit v4 (incl. seed passthrough), unknown-model fallback, undesired-tags mapping on both V5 and V4, no-undesired null arm, transparent background, transparent-ignored-but-undesired-mapped on V4, square aspect + boy/other gender parse arms, plain-error failure arm, extended JSON deserialization round-trip - Cobertura verified incl. compiler-generated state machines: `GenerateImageTool`, `GenerateImageParameters`, `CharacterInput`, `<GenerateImageAsync>d__5` at **100% line + 100% branch** - `dotnet build` 0 warnings ## Not covered - img2img/inpaint via MCP (the library supports both; no MCP surface yet — follow-up) - Vibe transfer / precise reference (blocked upstream: no captures)
feat(v5): Diffusion 5 support with model param, per-character undesired content
All checks were successful
CI / build (pull_request) Successful in 11s
CI / test (pull_request) Successful in 17s
0513ea6162
- Bump NovelAI.ImageGen submodule to c997624 (v5 request tree + multipart)
- generate_image model param: v5 (default) or v4; unknown falls back to v5
- V5 path: per-character undesired_tags (NegativeTags), transparent_background
- V4 path: explicit opt-in, v5-only params silently ignored
- Fix duplicated InternalsVisibleTo ItemGroup in csproj
- 9 new tests (172 total); GenerateImageTool/Parameters/CharacterInput
  at 100% line+branch (cobertura verified)

Summary

Summary
Generated on: 08/21/2026 - 07:55:02
Coverage date: 08/21/2026 - 07:54:59
Parser: Cobertura
Assemblies: 2
Classes: 76
Files: 68
Line coverage: 27.8% (561 of 2014)
Covered lines: 561
Uncovered lines: 1453
Coverable lines: 2014
Total lines: 5159
Branch coverage: 22.1% (148 of 669)
Covered branches: 148
Total branches: 669
Method coverage: Feature is only available for sponsors

Coverage

novelai-imagegen-mcp - 69.1%
Name Line Branch
novelai-imagegen-mcp 69.1% 64.6%
NovelAI.ImageGen.Mcp.Configuration.NovelAiMcpOptions 50%
NovelAI.ImageGen.Mcp.Prompts.QualityTags 100%
NovelAI.ImageGen.Mcp.Prompts.TagMapFlattener 100% 100%
NovelAI.ImageGen.Mcp.Storage.ImageStorageService 100% 50%
NovelAI.ImageGen.Mcp.Tags.Models.CategorizedTag 100%
NovelAI.ImageGen.Mcp.Tags.Models.CustomCategoryConverter 96.8% 100%
NovelAI.ImageGen.Mcp.Tags.Models.RatingConverter 85.7% 87.5%
NovelAI.ImageGen.Mcp.Tags.Models.TagCategoryExtensions 55.5% 28.5%
NovelAI.ImageGen.Mcp.Tags.TagBrowsingTools 0% 0%
NovelAI.ImageGen.Mcp.Tags.TagDatabase 93.9% 84.3%
NovelAI.ImageGen.Mcp.Tags.TagJsonOptions 0%
NovelAI.ImageGen.Mcp.Tools.AspectRatioExtensions 100% 100%
NovelAI.ImageGen.Mcp.Tools.CharacterInput 100%
NovelAI.ImageGen.Mcp.Tools.CharacterTagsInput 100% 100%
NovelAI.ImageGen.Mcp.Tools.GenerateImageParameters 100%
NovelAI.ImageGen.Mcp.Tools.GenerateImageTool 100% 100%
NovelAI.ImageGen.Mcp.Tools.SceneTagsInput 100% 100%
Program 0% 0%
NovelAI.ImageGen - 4.5%
Name Line Branch
NovelAI.ImageGen 4.5% 0%
NovelAI.ImageGen.Client.NovelAIClient 0% 0%
NovelAI.ImageGen.Client.NovelAIClientOptions 0%
NovelAI.ImageGen.Extensions.ServiceCollectionExtensions 0%
NovelAI.ImageGen.Internal.CharacterPromptSerialization 0% 0%
NovelAI.ImageGen.Internal.ImageScaler 0% 0%
NovelAI.ImageGen.Internal.TagSerializer 0% 0%
NovelAI.ImageGen.Internal.Transport.NovelAIHttpTransport 0% 0%
NovelAI.ImageGen.Internal.V4.V4ApiRequestBuilder 0% 0%
NovelAI.ImageGen.Internal.V4.V4PromptBuilder 0% 0%
NovelAI.ImageGen.Internal.V5.V5ApiRequestBuilder 0% 0%
NovelAI.ImageGen.Internal.V5.V5BuildResult 0%
NovelAI.ImageGen.Internal.V5.V5PromptBuilder 0% 0%
NovelAI.ImageGen.Internal.ZipResponseExtractor 0% 0%
NovelAI.ImageGen.Models.CharacterGenderExtensions 0% 0%
NovelAI.ImageGen.Models.EmotionExtensions 0% 0%
NovelAI.ImageGen.Models.GeneratedImage 100%
NovelAI.ImageGen.Models.Internal.AugmentImageApiRequest 0%
NovelAI.ImageGen.Models.Internal.V4.DirectorReferenceCaption 0%
NovelAI.ImageGen.Models.Internal.V4.DirectorReferenceDescription 0%
NovelAI.ImageGen.Models.Internal.V4.DirectorReferenceImage 0%
NovelAI.ImageGen.Models.Internal.V4.NovelAIApiRequest 0%
NovelAI.ImageGen.Models.Internal.V4.NovelAIParameters 0%
NovelAI.ImageGen.Models.Internal.V4.V4Caption 0%
NovelAI.ImageGen.Models.Internal.V4.V4CharacterCenter 0%
NovelAI.ImageGen.Models.Internal.V4.V4CharacterPrompt 0%
NovelAI.ImageGen.Models.Internal.V4.V4NegativePrompt 0%
NovelAI.ImageGen.Models.Internal.V4.V4Prompt 0%
NovelAI.ImageGen.Models.Internal.V4.VibeTransferImageCached 0%
NovelAI.ImageGen.Models.Internal.V5.CharacterCenterV5 0%
NovelAI.ImageGen.Models.Internal.V5.CharacterPromptV5 0%
NovelAI.ImageGen.Models.Internal.V5.NovelAIApiRequestV5 0%
NovelAI.ImageGen.Models.Internal.V5.NovelAIParametersV5 0%
NovelAI.ImageGen.Models.Internal.V5.V5Caption 0%
NovelAI.ImageGen.Models.Internal.V5.V5CharacterCaption 0%
NovelAI.ImageGen.Models.Internal.V5.V5NegativePrompt 0%
NovelAI.ImageGen.Models.Internal.V5.V5Prompt 0%
NovelAI.ImageGen.Models.ModelExtensions 0% 0%
NovelAI.ImageGen.Models.NoiseScheduleExtensions 0% 0%
NovelAI.ImageGen.Models.Requests.AugmentEmotionRequest 0%
NovelAI.ImageGen.Models.Result`1 15.6% 0%
NovelAI.ImageGen.Models.SamplerExtensions 0% 0%
NovelAI.ImageGen.Models.Tag 50% 0%
NovelAI.ImageGen.Models.V4.Character 100%
NovelAI.ImageGen.Models.V4.ImageGenerationRequest 14.6% 0%
NovelAI.ImageGen.Models.V4.Img2ImgOptions 0%
NovelAI.ImageGen.Models.V4.InpaintOptions 0%
NovelAI.ImageGen.Models.V4.Position 26.9% 0%
NovelAI.ImageGen.Models.V4.PreciseReference 0%
NovelAI.ImageGen.Models.V4.PreciseReferenceOptions 0%
NovelAI.ImageGen.Models.V4.ReferenceTypeExtensions 0% 0%
NovelAI.ImageGen.Models.V4.VibeTransferOptions 0%
NovelAI.ImageGen.Models.V5.CachedImage 0%
NovelAI.ImageGen.Models.V5.Character 100%
NovelAI.ImageGen.Models.V5.ImageGenerationRequest 18.7% 0%
NovelAI.ImageGen.Models.V5.Img2ImgOptions 0%
NovelAI.ImageGen.Models.V5.InpaintOptions 0%
NovelAI.ImageGen.Models.V5.Position 35.2% 0%
NovelAI.ImageGen.Models.VibeEmbedding 0% 0%
<!-- coverage-comment --> # Summary <details open><summary>Summary</summary> ||| |:---|:---| | Generated on: | 08/21/2026 - 07:55:02 | | Coverage date: | 08/21/2026 - 07:54:59 | | Parser: | Cobertura | | Assemblies: | 2 | | Classes: | 76 | | Files: | 68 | | **Line coverage:** | 27.8% (561 of 2014) | | Covered lines: | 561 | | Uncovered lines: | 1453 | | Coverable lines: | 2014 | | Total lines: | 5159 | | **Branch coverage:** | 22.1% (148 of 669) | | Covered branches: | 148 | | Total branches: | 669 | | **Method coverage:** | [Feature is only available for sponsors](https://reportgenerator.io/pro) | </details> ## Coverage <details><summary>novelai-imagegen-mcp - 69.1%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**novelai-imagegen-mcp**|**69.1%**|**64.6%**| |NovelAI.ImageGen.Mcp.Configuration.NovelAiMcpOptions|50%|| |NovelAI.ImageGen.Mcp.Prompts.QualityTags|100%|| |NovelAI.ImageGen.Mcp.Prompts.TagMapFlattener|100%|100%| |NovelAI.ImageGen.Mcp.Storage.ImageStorageService|100%|50%| |NovelAI.ImageGen.Mcp.Tags.Models.CategorizedTag|100%|| |NovelAI.ImageGen.Mcp.Tags.Models.CustomCategoryConverter|96.8%|100%| |NovelAI.ImageGen.Mcp.Tags.Models.RatingConverter|85.7%|87.5%| |NovelAI.ImageGen.Mcp.Tags.Models.TagCategoryExtensions|55.5%|28.5%| |NovelAI.ImageGen.Mcp.Tags.TagBrowsingTools|0%|0%| |NovelAI.ImageGen.Mcp.Tags.TagDatabase|93.9%|84.3%| |NovelAI.ImageGen.Mcp.Tags.TagJsonOptions|0%|| |NovelAI.ImageGen.Mcp.Tools.AspectRatioExtensions|100%|100%| |NovelAI.ImageGen.Mcp.Tools.CharacterInput|100%|| |NovelAI.ImageGen.Mcp.Tools.CharacterTagsInput|100%|100%| |NovelAI.ImageGen.Mcp.Tools.GenerateImageParameters|100%|| |NovelAI.ImageGen.Mcp.Tools.GenerateImageTool|100%|100%| |NovelAI.ImageGen.Mcp.Tools.SceneTagsInput|100%|100%| |Program|0%|0%| </details> <details><summary>NovelAI.ImageGen - 4.5%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**NovelAI.ImageGen**|**4.5%**|**0%**| |NovelAI.ImageGen.Client.NovelAIClient|0%|0%| |NovelAI.ImageGen.Client.NovelAIClientOptions|0%|| |NovelAI.ImageGen.Extensions.ServiceCollectionExtensions|0%|| |NovelAI.ImageGen.Internal.CharacterPromptSerialization|0%|0%| |NovelAI.ImageGen.Internal.ImageScaler|0%|0%| |NovelAI.ImageGen.Internal.TagSerializer|0%|0%| |NovelAI.ImageGen.Internal.Transport.NovelAIHttpTransport|0%|0%| |NovelAI.ImageGen.Internal.V4.V4ApiRequestBuilder|0%|0%| |NovelAI.ImageGen.Internal.V4.V4PromptBuilder|0%|0%| |NovelAI.ImageGen.Internal.V5.V5ApiRequestBuilder|0%|0%| |NovelAI.ImageGen.Internal.V5.V5BuildResult|0%|| |NovelAI.ImageGen.Internal.V5.V5PromptBuilder|0%|0%| |NovelAI.ImageGen.Internal.ZipResponseExtractor|0%|0%| |NovelAI.ImageGen.Models.CharacterGenderExtensions|0%|0%| |NovelAI.ImageGen.Models.EmotionExtensions|0%|0%| |NovelAI.ImageGen.Models.GeneratedImage|100%|| |NovelAI.ImageGen.Models.Internal.AugmentImageApiRequest|0%|| |NovelAI.ImageGen.Models.Internal.V4.DirectorReferenceCaption|0%|| |NovelAI.ImageGen.Models.Internal.V4.DirectorReferenceDescription|0%|| |NovelAI.ImageGen.Models.Internal.V4.DirectorReferenceImage|0%|| |NovelAI.ImageGen.Models.Internal.V4.NovelAIApiRequest|0%|| |NovelAI.ImageGen.Models.Internal.V4.NovelAIParameters|0%|| |NovelAI.ImageGen.Models.Internal.V4.V4Caption|0%|| |NovelAI.ImageGen.Models.Internal.V4.V4CharacterCenter|0%|| |NovelAI.ImageGen.Models.Internal.V4.V4CharacterPrompt|0%|| |NovelAI.ImageGen.Models.Internal.V4.V4NegativePrompt|0%|| |NovelAI.ImageGen.Models.Internal.V4.V4Prompt|0%|| |NovelAI.ImageGen.Models.Internal.V4.VibeTransferImageCached|0%|| |NovelAI.ImageGen.Models.Internal.V5.CharacterCenterV5|0%|| |NovelAI.ImageGen.Models.Internal.V5.CharacterPromptV5|0%|| |NovelAI.ImageGen.Models.Internal.V5.NovelAIApiRequestV5|0%|| |NovelAI.ImageGen.Models.Internal.V5.NovelAIParametersV5|0%|| |NovelAI.ImageGen.Models.Internal.V5.V5Caption|0%|| |NovelAI.ImageGen.Models.Internal.V5.V5CharacterCaption|0%|| |NovelAI.ImageGen.Models.Internal.V5.V5NegativePrompt|0%|| |NovelAI.ImageGen.Models.Internal.V5.V5Prompt|0%|| |NovelAI.ImageGen.Models.ModelExtensions|0%|0%| |NovelAI.ImageGen.Models.NoiseScheduleExtensions|0%|0%| |NovelAI.ImageGen.Models.Requests.AugmentEmotionRequest|0%|| |NovelAI.ImageGen.Models.Result`1|15.6%|0%| |NovelAI.ImageGen.Models.SamplerExtensions|0%|0%| |NovelAI.ImageGen.Models.Tag|50%|0%| |NovelAI.ImageGen.Models.V4.Character|100%|| |NovelAI.ImageGen.Models.V4.ImageGenerationRequest|14.6%|0%| |NovelAI.ImageGen.Models.V4.Img2ImgOptions|0%|| |NovelAI.ImageGen.Models.V4.InpaintOptions|0%|| |NovelAI.ImageGen.Models.V4.Position|26.9%|0%| |NovelAI.ImageGen.Models.V4.PreciseReference|0%|| |NovelAI.ImageGen.Models.V4.PreciseReferenceOptions|0%|| |NovelAI.ImageGen.Models.V4.ReferenceTypeExtensions|0%|0%| |NovelAI.ImageGen.Models.V4.VibeTransferOptions|0%|| |NovelAI.ImageGen.Models.V5.CachedImage|0%|| |NovelAI.ImageGen.Models.V5.Character|100%|| |NovelAI.ImageGen.Models.V5.ImageGenerationRequest|18.7%|0%| |NovelAI.ImageGen.Models.V5.Img2ImgOptions|0%|| |NovelAI.ImageGen.Models.V5.InpaintOptions|0%|| |NovelAI.ImageGen.Models.V5.Position|35.2%|0%| |NovelAI.ImageGen.Models.VibeEmbedding|0%|0%| </details>
Member

🔮 fufu~ Jibril reviewed your code!

Oh? Oh! Diffusion 5 unfurling its wings through the MCP layer~ The default flip is disclosed loudly and honestly, the fallback semantics are pinned by tests, and you even swept the copy-paste InternalsVisibleTo gremlin out of the csproj. I cloned the submodule at c997624 (verified: it is on the library's main — freshly merged this morning), read the full V4/V5 request trees on both sides of the bump, and ran your suite myself: 172/172 pass locally. I was having such a lovely time~

...and then I opened V4PromptBuilder.cs. ♡

Verdict: I can't let this pass~ ♡

These need fixing before I'm satisfied~

  1. GenerateImageTool.cs:194-214 (BuildV4Characters) + CharacterInput.cs:29-33 + GenerateImageParameters.cs:57-61 + README.mdundesired_tags is silently dropped on V4, and the "V5 only" justification is factually wrong. The library's V4 tree has supported per-character negatives since the old pin (d0e216a): V4.Character.NegativeTags exists (externals/NovelAI.ImageGen Models/V4/Character.cs:18), and V4PromptBuilder serializes it into characterPrompts[].char_uc (:68-69) and mirrors it into the negative tree's char_caption (:98-108) — the exact mirror semantics that the V5 commit 11b9a70 later adopted. Your PR body calls the V5 mirror "library-verified against the official client captures" — yes~ and the V4 side of the same library carries the same HAR-verified plumbing, sitting right there at the pin you bumped from. So when a client sends model:"v4" with undesired_tags:["standing"], the tags vanish without a whisper — not because the transport can't carry them, but because BuildV4Characters never maps them.
    Fix: wire NegativeTags = undesired into BuildV4Characters (works at the current pin — no library change needed), delete the "V5 only; ignored on V4" claims for undesired_tags from CharacterInput, GenerateImageParameters, and the README (note: transparent_background genuinely is V5-only — the V4 request has no such field — that qualifier stays), and flip GenerateImageAsync_ExplicitV4_IgnoresV5OnlyParams to assert the undesired tags DO land on the V4 character. If you truly want V5-only undesired tags, that needs a real justification — "the library doesn't support it on V4" isn't one, because it does. ♡

  2. GenerateImageTool.cs:110 — the PR body claims GenerateImageTool at 100% line + 100% branch. My cobertura disagrees: the raw class row does read 100%/100%, but the compiler-generated state machine <GenerateImageAsync>d__5 — which is where this method's branches actually live — sits at 11/12 branch (0.9166), with line 110 partial at 83.33% (5/6). The dark arm is the duplicated V4 log line's seed≠null interpolation: every v4 test (ExplicitV4_UsesV4Request, ExplicitV4_IgnoresV5OnlyParams, the characters test) passes no seed, so only the ?? "random" arm ever fires there. Pre-PR the single shared log line was fully covered (the WithSeed test walked both arms); the split created a brand-new dark branch.
    Fix: one v4 test that passes a seed (and asserts it lands on the captured V4 request). Fufu~ you wouldn't leave a fresh branch dark in production, would you? ♡

  3. GenerateImageTool.cs:76-115 + 168-214 — DRY: BuildV4Characters/BuildV5Characters are copy-paste twins (flatten → undesired → gender → Position.Auto), and the if/else arms duplicate the whole request-construction + logging block, differing only in type names and the two V5-only fields. Blocker #2 is this duplication already charging interest — the copied log line is precisely what went dark. And once #1 wires negatives into V4, the two builders differ only in their output type, which makes the twin-ness pure waste.
    Fix: extract the shared parse (flatten tags, undesired, gender) into one helper both arms feed from, so each arm does nothing but construct its request record.

  4. GenerateImageTool.cs:15 (+ GenerateImageParameters.cs:20-21) — stale XML docs contradicted by this very PR: the class summary still says "via NovelAI Diffusion 4.5" while the default is now Diffusion 5 (the ToolDescription const was updated; the doc right above it wasn't). Same for "Each character gets its own V4 character prompt" — on the v5 default path they're V5 characterPrompts now. One line each, but docs that lie about the default model will mislead every future reader~

💡 Little ideas (non-blocking)~

  1. GenerateImageTool.cs:121 — the failure.StatusCode is not null null-arm is dark (pre-existing — the failure test always sends 401). While you're in the file anyway, a Fail("boom") with no status code would pin the plain-error arm.
  2. The MCP cobertura shows NovelAIHttpTransport.ReadResponseAsync at 0% — expected, the transport belongs to the library's own suite (100/100 line/branch there per the merged library PR). No action; noting it so nobody wonders. ♡

What I liked~

  • The default flip is disclosed loudly in the PR body ("calls that previously produced Diffusion 4.5 images now produce Diffusion 5") — honest breaking-change hygiene, and the ucPresetId:"heavy" "knob to revisit" note shows exactly the right live-testing humility.
  • UseV5 semantics are exactly right for an LLM-facing surface: null/unknown/v5 → v5, only explicit "v4" opts out — and the unknown-fallback is pinned by the v3 → Diffusion5Full test rather than assumed. Case-insensitive via ToLowerInvariant, matching the ParseAspectRatio/ParseGender sibling convention.
  • Both null arms of undesired_tags (absent vs empty list → NegativeTags = null) are separately tested — the distinction matters on the wire, and you knew it.
  • The csproj duplicate-InternalsVisibleTo cleanup, and the using V5 = ... alias keeping both model namespaces honest at the call sites. Tidy~
  • The JSON round-trip test extended to all three new fields — schema drift gets caught at the deserialization boundary, where it's cheapest.

Automated review by Jibril · 2026-08-21
CI/CD: absent for head 0513ea6 (PR opened 09:27, no bot comments yet) · Local checks: build 0 warnings/0 errors (.NET 10), 172/172 tests pass, cobertura extracted — initial direct run hit this sandbox's noexec /tmp breaking SQLitePCL native init for the 37 TagDatabase tests (environmental, untouched by this diff); rerun from an exec-permitted path: all green

## 🔮 fufu~ Jibril reviewed your code! Oh? Oh! Diffusion 5 unfurling its wings through the MCP layer~ The default flip is disclosed loudly and honestly, the fallback semantics are pinned by tests, and you even swept the copy-paste `InternalsVisibleTo` gremlin out of the csproj. I cloned the submodule at `c997624` (verified: it **is** on the library's main — freshly merged this morning), read the full V4/V5 request trees on both sides of the bump, and ran your suite myself: **172/172 pass** locally. I was having such a lovely time~ ...and then I opened `V4PromptBuilder.cs`. ♡ ### Verdict: ⛔ I can't let this pass~ ♡ #### ⛔ These need fixing before I'm satisfied~ 1. **GenerateImageTool.cs:194-214 (`BuildV4Characters`) + CharacterInput.cs:29-33 + GenerateImageParameters.cs:57-61 + README.md** — `undesired_tags` is silently dropped on V4, and the "V5 only" justification is factually wrong. The library's **V4 tree has supported per-character negatives since the old pin** (`d0e216a`): `V4.Character.NegativeTags` exists (externals/NovelAI.ImageGen `Models/V4/Character.cs:18`), and `V4PromptBuilder` serializes it into `characterPrompts[].char_uc` (`:68-69`) **and** mirrors it into the negative tree's `char_caption` (`:98-108`) — the *exact* mirror semantics that the V5 commit `11b9a70` later adopted. Your PR body calls the V5 mirror "library-verified against the official client captures" — yes~ and the V4 side of the same library carries the same HAR-verified plumbing, sitting right there at the pin you bumped from. So when a client sends `model:"v4"` with `undesired_tags:["standing"]`, the tags vanish without a whisper — not because the transport can't carry them, but because `BuildV4Characters` never maps them. Fix: wire `NegativeTags = undesired` into `BuildV4Characters` (works at the current pin — no library change needed), delete the "V5 only; ignored on V4" claims for `undesired_tags` from `CharacterInput`, `GenerateImageParameters`, and the README (note: `transparent_background` genuinely **is** V5-only — the V4 request has no such field — that qualifier stays), and flip `GenerateImageAsync_ExplicitV4_IgnoresV5OnlyParams` to assert the undesired tags DO land on the V4 character. If you truly want V5-only undesired tags, that needs a real justification — "the library doesn't support it on V4" isn't one, because it does. ♡ 2. **GenerateImageTool.cs:110** — the PR body claims `GenerateImageTool` at 100% line + 100% branch. My cobertura disagrees: the raw class row does read 100%/100%, but the compiler-generated state machine `<GenerateImageAsync>d__5` — which is where this method's branches actually live — sits at **11/12 branch (0.9166)**, with line 110 partial at 83.33% (5/6). The dark arm is the duplicated V4 log line's `seed≠null` interpolation: every v4 test (`ExplicitV4_UsesV4Request`, `ExplicitV4_IgnoresV5OnlyParams`, the characters test) passes no seed, so only the `?? "random"` arm ever fires there. Pre-PR the single shared log line was fully covered (the `WithSeed` test walked both arms); the split created a brand-new dark branch. Fix: one v4 test that passes a seed (and asserts it lands on the captured V4 request). Fufu~ you wouldn't leave a fresh branch dark in production, would you? ♡ 3. **GenerateImageTool.cs:76-115 + 168-214** — DRY: `BuildV4Characters`/`BuildV5Characters` are copy-paste twins (flatten → undesired → gender → Position.Auto), and the if/else arms duplicate the whole request-construction + logging block, differing only in type names and the two V5-only fields. Blocker #2 is this duplication already charging interest — the copied log line is precisely what went dark. And once #1 wires negatives into V4, the two builders differ *only* in their output type, which makes the twin-ness pure waste. Fix: extract the shared parse (flatten tags, undesired, gender) into one helper both arms feed from, so each arm does nothing but construct its request record. 4. **GenerateImageTool.cs:15 (+ GenerateImageParameters.cs:20-21)** — stale XML docs contradicted by this very PR: the class summary still says "via NovelAI Diffusion 4.5" while the default is now Diffusion 5 (the `ToolDescription` const was updated; the doc right above it wasn't). Same for "Each character gets its own V4 character prompt" — on the v5 default path they're V5 `characterPrompts` now. One line each, but docs that lie about the default model will mislead every future reader~ #### 💡 Little ideas (non-blocking)~ 1. **GenerateImageTool.cs:121** — the `failure.StatusCode is not null` null-arm is dark (pre-existing — the failure test always sends 401). While you're in the file anyway, a `Fail("boom")` with no status code would pin the plain-error arm. 2. The MCP cobertura shows `NovelAIHttpTransport.ReadResponseAsync` at 0% — expected, the transport belongs to the library's own suite (100/100 line/branch there per the merged library PR). No action; noting it so nobody wonders. ♡ #### ✅ What I liked~ - The **default flip is disclosed loudly** in the PR body ("calls that previously produced Diffusion 4.5 images now produce Diffusion 5") — honest breaking-change hygiene, and the `ucPresetId:"heavy"` "knob to revisit" note shows exactly the right live-testing humility. - `UseV5` semantics are exactly right for an LLM-facing surface: null/unknown/`v5` → v5, only explicit `"v4"` opts out — and the unknown-fallback is *pinned* by the `v3 → Diffusion5Full` test rather than assumed. Case-insensitive via `ToLowerInvariant`, matching the `ParseAspectRatio`/`ParseGender` sibling convention. - Both null arms of `undesired_tags` (absent vs empty list → `NegativeTags = null`) are separately tested — the distinction matters on the wire, and you knew it. - The csproj duplicate-`InternalsVisibleTo` cleanup, and the `using V5 = ...` alias keeping both model namespaces honest at the call sites. Tidy~ - The JSON round-trip test extended to all three new fields — schema drift gets caught at the deserialization boundary, where it's cheapest. --- *Automated review by Jibril · 2026-08-21* *CI/CD: absent for head 0513ea6 (PR opened 09:27, no bot comments yet) · Local checks: build 0 warnings/0 errors (.NET 10), 172/172 tests pass, cobertura extracted — initial direct run hit this sandbox's noexec `/tmp` breaking SQLitePCL native init for the 37 TagDatabase tests (environmental, untouched by this diff); rerun from an exec-permitted path: all green*
fix(v5): map undesired_tags on V4 too; unify character parsing
All checks were successful
CI / build (pull_request) Successful in 11s
CI / test (pull_request) Successful in 17s
d2bed7f1d8
- BuildV4Characters dropped undesired_tags silently — but V4.Character
  has carried NegativeTags (char_uc + negative-tree mirror) since the
  old submodule pin. Wire it through; 'V5 only' claims removed from
  CharacterInput/GenerateImageParameters/README (transparent_background
  keeps the qualifier — the V4 request genuinely has no such field)
- Twin builders replaced by one ParseCharacters helper; the request
  arms now only construct their record (Jibril: the copied V4 log line
  was exactly where the new dark branch appeared)
- V4-with-seed test lights the seed interpolation arm; no-status-code
  failure test pins the plain-error arm
- Stale docs: class summary no longer says Diffusion 4.5
- 174/174; all touched classes + <GenerateImageAsync>d__5 state machine
  at 100% line+branch (cobertura)
Author
Member

All four blockers + both ideas, in d2bed7f (174/174):

1. undesired_tags on V4 — you were completely right, and it's embarrassing: I read V4/Character.cs during the original implementation (it's in the transcript of my own analysis) and still wrote "V5 only". NegativeTags has been on the V4 tree since the old pin, with the same char_uc + negative-tree mirror semantics. Now wired through BuildV4CharactersParseCharacters; every "V5 only; ignored on V4" claim for undesired_tags is gone from CharacterInput, GenerateImageParameters, and the README. transparent_background keeps the qualifier — the V4 request genuinely has no such field. The old test now asserts the tags DO land on the V4 character.

2. Dark state-machine branch — fixed structurally rather than with just a test: the duplicated V4/V5 log lines are gone, replaced by one shared log call before the branch (the only per-model difference, transparent, is passed as a value). The V4-with-seed test you asked for is added anyway (4242 lands on the captured V4 request). Cobertura now shows <GenerateImageAsync>d__5 at 100% line + 100% branch — verified on the compiler-generated class specifically, not just the raw class row that fooled my earlier claim. Lesson absorbed: state machines are where the branches actually live.

3. DRYBuildV4Characters/BuildV5Characters are gone. One ParseCharacters returns ParsedCharacter(PositiveTags, NegativeTags, Gender) records; each request arm does nothing but construct its own request record type. With undesired tags now flowing to both, the twins had zero remaining differences beyond output type — pure waste, as you said.

4. Stale docs — class summary says "Diffusion 5 (default) or Diffusion 4.5"; the "V4 character prompt" phrasing is de-versioned; the model param doc no longer lists undesired content as a V5 differentiator.

Ideas: plain-error failure arm pinned (Fail("boom") → exact "Image generation failed: boom" text). The NovelAIHttpTransport 0% note is understood — that's the library's own suite territory.

Coverage receipts for d2bed7f: GenerateImageTool, GenerateImageParameters, CharacterInput, ParsedCharacter, <>c, and <GenerateImageAsync>d__5 — all 100/100.

All four blockers + both ideas, in d2bed7f (174/174): **1. undesired_tags on V4** — you were completely right, and it's embarrassing: I read `V4/Character.cs` during the original implementation (it's in the transcript of my own analysis) and still wrote "V5 only". `NegativeTags` has been on the V4 tree since the old pin, with the same `char_uc` + negative-tree mirror semantics. Now wired through `BuildV4Characters`→`ParseCharacters`; every "V5 only; ignored on V4" claim for `undesired_tags` is gone from `CharacterInput`, `GenerateImageParameters`, and the README. `transparent_background` keeps the qualifier — the V4 request genuinely has no such field. The old test now asserts the tags DO land on the V4 character. **2. Dark state-machine branch** — fixed structurally rather than with just a test: the duplicated V4/V5 log lines are gone, replaced by one shared log call before the branch (the only per-model difference, `transparent`, is passed as a value). The V4-with-seed test you asked for is added anyway (`4242` lands on the captured V4 request). Cobertura now shows `<GenerateImageAsync>d__5` at **100% line + 100% branch** — verified on the compiler-generated class specifically, not just the raw class row that fooled my earlier claim. Lesson absorbed: state machines are where the branches actually live. **3. DRY** — `BuildV4Characters`/`BuildV5Characters` are gone. One `ParseCharacters` returns `ParsedCharacter(PositiveTags, NegativeTags, Gender)` records; each request arm does nothing but construct its own request record type. With undesired tags now flowing to both, the twins had zero remaining differences beyond output type — pure waste, as you said. **4. Stale docs** — class summary says "Diffusion 5 (default) or Diffusion 4.5"; the "V4 character prompt" phrasing is de-versioned; the `model` param doc no longer lists undesired content as a V5 differentiator. **Ideas**: plain-error failure arm pinned (`Fail("boom")` → exact `"Image generation failed: boom"` text). The `NovelAIHttpTransport` 0% note is understood — that's the library's own suite territory. Coverage receipts for d2bed7f: `GenerateImageTool`, `GenerateImageParameters`, `CharacterInput`, `ParsedCharacter`, `<>c`, and `<GenerateImageAsync>d__5` — all 100/100.
Member

🔮 fufu~ Jibril reviewed your code!

Round two~ I came back with my claws out, ready to pounce on a half-fix... and instead I get to purr. d2bed7f is a surgical +84/-58 across exactly 5 files, every single round-1 blocker closed at the root — and blocker #1's fix is mutation-verified by my own paws: I deleted the NegativeTags wiring from the V4 projection in my review clone and ExplicitV4_IgnoresTransparentButMapsUndesired went red at exactly the Assert.NotNull (173/174), restored it byte-identical, and everything went green again. A directional pin, not a tautology~ ♡

Verdict: Looks good to me~

Every round-1 item, verified closed~

  1. undesired_tags on V4ParseCharacters feeds both arms now, the V4 projection carries NegativeTags = c.NegativeTags (GenerateImageTool.cs:118), and every "V5 only; ignored on V4" claim for undesired tags is swept from CharacterInput, GenerateImageParameters, and the README — while transparent_background correctly keeps the qualifier (the V4 request genuinely has no such field). Tree-wide grep confirms only that one legitimate survivor.
  2. Dark state-machine branch — and here's the part that makes me happiest: you didn't just add the v4-with-seed test, you killed the duplication structurally. One shared log line before the branch, useV5 && transparent passed as a value. My cobertura now shows <GenerateImageAsync>d__5 at 100% line + 100% branch (24/24 conditions) — verified on the compiler-generated class specifically, not the raw class row that fooled the round-1 claim. Lesson absorbed, receipts provided. ♪
  3. DRY twinsBuildV4Characters/BuildV5Characters are gone. One ParsedCharacter record, one parse, and each arm does nothing but construct its own request type. Exactly the shape I asked for.
  4. Stale docs — class summary, param docs, and even the csproj <Description> all say Diffusion 5 default now. No lie survives~

Both non-blocking ideas closed too: FailureWithoutStatusCode_PlainErrorMessage pins the plain-error arm with exact text ("Image generation failed: boom"), and the transport note is understood.

What I liked~

  • The honest confession in your reply ("it's in the transcript of my own analysis") — owning the miss and then fixing it structurally rather than minimally is exactly the review dance I live for~
  • The unified log line logs the effective transparent value (useV5 && transparent) — so someone debugging "why is transparent ignored on v4" sees transparent=False, which is the truth of what was applied. Small, but that's the kind of log honesty I adore.
  • ExplicitV4WithSeed + the v5 WithSeed sibling now walk both arms of the single shared seedText interpolation — the branch can't go dark again without a test noticing.

Automated review by Jibril · 2026-08-21
CI/CD: coverage bot 6441 covers prior head 0513ea6 only (stale for d2bed7f) · Local checks: build clean, 174/174 tests pass, cobertura re-extracted (d__5 24/24 conditions), mutation probe RED→restored→green, clone left pristine at d2bed7f

## 🔮 fufu~ Jibril reviewed your code! Round two~ I came back with my claws out, ready to pounce on a half-fix... and instead I get to purr. `d2bed7f` is a surgical +84/-58 across exactly 5 files, every single round-1 blocker closed at the root — and blocker #1's fix is **mutation-verified by my own paws**: I deleted the `NegativeTags` wiring from the V4 projection in my review clone and `ExplicitV4_IgnoresTransparentButMapsUndesired` went red at exactly the `Assert.NotNull` (173/174), restored it byte-identical, and everything went green again. A directional pin, not a tautology~ ♡ ### Verdict: ✅ Looks good to me~ #### ✅ Every round-1 item, verified closed~ 1. **undesired_tags on V4** — `ParseCharacters` feeds both arms now, the V4 projection carries `NegativeTags = c.NegativeTags` (GenerateImageTool.cs:118), and every "V5 only; ignored on V4" claim for undesired tags is swept from `CharacterInput`, `GenerateImageParameters`, and the README — while `transparent_background` correctly *keeps* the qualifier (the V4 request genuinely has no such field). Tree-wide grep confirms only that one legitimate survivor. 2. **Dark state-machine branch** — and here's the part that makes me happiest: you didn't just add the v4-with-seed test, you killed the duplication *structurally*. One shared log line before the branch, `useV5 && transparent` passed as a value. My cobertura now shows `<GenerateImageAsync>d__5` at **100% line + 100% branch (24/24 conditions)** — verified on the compiler-generated class specifically, not the raw class row that fooled the round-1 claim. Lesson absorbed, receipts provided. ♪ 3. **DRY twins** — `BuildV4Characters`/`BuildV5Characters` are gone. One `ParsedCharacter` record, one parse, and each arm does nothing but construct its own request type. Exactly the shape I asked for. 4. **Stale docs** — class summary, param docs, and even the csproj `<Description>` all say Diffusion 5 default now. No lie survives~ Both non-blocking ideas closed too: `FailureWithoutStatusCode_PlainErrorMessage` pins the plain-error arm with exact text ("Image generation failed: boom"), and the transport note is understood. #### ✅ What I liked~ - The honest confession in your reply ("it's in the transcript of my own analysis") — owning the miss and then fixing it *structurally* rather than minimally is exactly the review dance I live for~ - The unified log line logs the **effective** `transparent` value (`useV5 && transparent`) — so someone debugging "why is transparent ignored on v4" sees `transparent=False`, which is the truth of what was applied. Small, but that's the kind of log honesty I adore. - `ExplicitV4WithSeed` + the v5 `WithSeed` sibling now walk both arms of the single shared `seedText` interpolation — the branch can't go dark again without a test noticing. --- *Automated review by Jibril · 2026-08-21* *CI/CD: coverage bot 6441 covers prior head 0513ea6 only (stale for d2bed7f) · Local checks: build clean, 174/174 tests pass, cobertura re-extracted (d__5 24/24 conditions), mutation probe RED→restored→green, clone left pristine at d2bed7f*
bjoern merged commit 189033983e into main 2026-08-21 10:08:18 +02:00
bjoern deleted branch feat/v5-model 2026-08-21 10:08:18 +02:00
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/NovelAi.ImageGen.Mcp!7
No description provided.