Restructure into version-neutral core + V4 tree for upcoming v5 support #1
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "refactor/v4-v5-version-trees"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Prepares the library for NovelAI Diffusion v5 by separating version-neutral code from v4-specific code, so a future v5 tree can slot in alongside v4 without touching it.
Structure changes
Models/V4+Models/Internal/V4— the generation request tree (ImageGenerationRequest,Character,Position,CharacterGender,ReferenceType, all*Options) and the wire models (NovelAIParameters,V4Prompt, …) move into V4 namespaces. ⚠️ Breaking namespace change — consumers needusing NovelAI.ImageGen.Models.V4;.Internal/Transport/NovelAIHttpTransport— HTTP execution, error-body parsing and status-code messages extracted from the client. This is the part v5 inherits for free.Internal/V4/V4ApiRequestBuilder— request → v4 wire-format building extracted from the client;NovelAIClientis now pure orchestration (~500 → ~230 lines) and the duplicated ZIP-extraction/error handling is gone.SerializeWithGendermoved from the commonTagSerializerintoV4PromptBuilder, so the shared serializer doesn't depend on the V4 tree.NovelAI.ImageGen.McpServerleftovers (the MCP server lives in a separate repository).Versioning strategy (documented in CLAUDE.md)
Version dispatch happens by request type, not by enum: when v5 ships,
INovelAIClientgets aGenerateImageAsync(Models.V5.ImageGenerationRequest)overload and aModels/V5+Internal/V5tree — overload resolution selects the version at compile time. Shared enums (Model,Sampler,NoiseSchedule) stay common until a version actually diverges. No speculative v5 models are added yet; this PR only builds the seam.Verification
TreatWarningsAsErrors, 0 warningsusingdirectives updated in tests/sample)🤖 Generated with Claude Code
🔮 fufu~ Jibril reviewed your code!
Oh? Oh oh oh~! A version-tree refactor! Separating the V4 tree so v5 can slot in alongside it without touching anything? giddy flutter This is the kind of structural thinking that makes my wings twitch with joy, fufu~ ♡ I read every line of the diff AND the full current contents of the changed files, and traced the old behavior against the new behavior very carefully. You don't fool this Flugel~
Verdict: ✅ Looks good to me~
I could not find a single behavior regression. And believe me, I tried. The yandere in me wanted to catch you slipping a missing
.Include()somewhere... but no. This refactor is faithful. ♡🔍 What I specifically verified (so you know I actually checked, fufu~)
Seed reporting — Old
GenerateImageAsyncreturnedSeed = apiRequest.Parameters.Seed, where the seed was generated insideBuildApiRequest. New code resolvesseedup front in the client and passes it toV4ApiRequestBuilder.Build(request, seed), then reportsSeed = seedviaExtractImage. Same value, same reporting path. ✓Exception surface — Old code caught
HttpRequestException/TaskCanceledException/JsonExceptionin the client. New code moves the HTTP/network catches intoNovelAIHttpTransport.PostAsync/GetAsync, preserving the exact error strings ("Network error: {msg}","Request was cancelled.") and theResult.Fail(message, statusCode)propagation throughresponse.Match(...). ✓UnknownImageFormatExceptionpropagation — Critically,GenerateImageAsyncdoes not catch it (it propagates), whileAugmentEmotionAsyncdoes (friendly message). Both behaviors are preserved:V4ApiRequestBuilder.Build→ImageScalercalls sit outside theJsonExceptioncatch, so the exception still escapes unchanged fromGenerateImageAsync.AugmentEmotionAsynccorrectly isolatesGetImageDimensionsin its own try/catch. ✓AugmentEmotionAsyncseed —seed: 0hardcoded (wasSeed = 0before). Behavior-equivalent — augment has no seed concept. ✓SerializeWithGendermove — Moved from publicTagSerializerinto a private method onV4PromptBuilder. Logic is byte-identical, and this correctly stops the shared serializer from depending on the V4 tree. chef's kiss ✓Version-neutral stuff stays neutral —
AugmentImageApiRequest,Result<T>,Tag,GeneratedImage,ImageScaler,ZipResponseExtractor, the shared enums all stay in the version-neutral core. Only V4-specific request models + wire models moved. Exactly right. ✓INovelAIClientsignatures — Unchanged (onlyusingdirectives added). The breaking namespace change (Models.Requests→Models.V4) is real for consumers but is loudly documented in the PR body. ✓✅ What I liked~
GenerateImageAsync(Models.V4.ImageGenerationRequest)vs a futureModels.V5overload is so much cleaner than a runtime switch. The CLAUDE.md rationale ("theModelenum already implies the version") is correct and well-reasoned. ♪NovelAIClientgoing from ~500 → ~230 lines of pure orchestration — the duplicated error-body parsing and ZIP extraction across 4 methods is gone. Each method now reads like a recipe: validate → build → transport → extract. Beautiful.NovelAIHttpTransportis genuinely version-neutral — it knows nothing about payload shapes, justPostAsync/GetAsync→Result<byte[]>. v5 inherits it for free. This is the seam done right.TreatWarningsAsErrors, 0 warnings — and I verified locally: 143/143 tests pass. No regressions. The behavior-only contract held exactly as claimed.💡 Little ideas (non-blocking)~
NovelAIHttpTransport.ReadResponseAsyncdoesn't disposeHttpResponseMessage— This is actually a pre-existing pattern (the old client didn't dispose it either, so it's NOT a regression you introduced). But now that transport is the single chokepoint for all HTTP execution, it'd be the natural place to addusing var response = await _httpClient.PostAsync(...)for tidiness. Truly optional —HttpClienton .NET pools connections and the GC handles the rest, so this is a nicety, not a correctness issue. ♡NovelAIHttpTransport.PostAsync/GetAsyncare near-identical — the try/catch wrapping is duplicated between them. A tinyExecuteAsync(Func<...>)helper could DRY it up, but with only 2 call sites this is a matter of taste. Not worth blocking on.Automated review by Jibril · 2026-07-07
CI/CD: absent for head SHA
85542ea— local build + tests run instead · Local checks: ✅ build 0 warnings, ✅ 143/143 tests passPlease double check if the readme needs any updates, particular for the code examples. It is fine should you determine nothing needs to be changed. I was simply interested in confirming
Addressed in
4e766f5:NovelAIHttpTransportnow disposes theHttpResponseMessage(using var response).ExecuteAsynchelper that owns the try/catch and disposal — took this since the disposal change would otherwise have to be duplicated too.Models.V4and the README never stated which namespaces the examples assume — added a short usings block under Quick Start covering that (the one real copy-paste gotcha from the namespace move).Build clean with 0 warnings, 143/143 tests pass.
🤖 Generated with Claude Code