feat(v5): Diffusion 5 request tree with multipart transport #4
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/v5-request-tree"
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?
Ports the C# NovelAI.ImageGen v5 support (upstream commits d5eaf2b..c997624, PR TeamAI/NovelAi.ImageGen#3) to the Dart library.
What's in
v5.dartentrypoint - V5 model names (ImageGenerationRequest,Position,Character, ...) collide with the V4 tree, so they live in a separate library. The main entrypoint keeps exporting V4 names unchanged (no breaking change).v5.Position.at(x, y)with 0.0-1.0 coordinates replaces the V4 5x5 grid; auto positions center at 0.5 without settinguse_coords.CachedImage-fromData(bytes)uploads raw bytes as a binary multipart part at native resolution;fromCacheKey(key)references a server-side cache entry without uploading. Cache keys are lowercase-hex SHA-256 of the bytes.characterPrompts[]entries (prompt/uc/center/enabled) alongside thev4_prompttree; captions have nochar_uc; per-character negatives travel incharacterPrompts[].ucand are mirrored verbatim into the negative tree'schar_caption.image/maskbinary parts + arequestJSON part (filenameblob, content typeapplication/json), mirroring the official client.extra_noise_seed = seed - 1,color_correct(img2img),add_original_image: false(infill), model swap tonai-diffusion-5-full-inpaintingfor infill. Quality preset pinned to"none".CharacterGendermoved tosrc/models/, sharedCharacterPromptSerializationextracted fromV4PromptBuilder(one copy of the gender-prefix and position contracts).GenerationRequestinterface - Dart has no overloading, so the C#GenerateImageAsyncoverloads become a sealed switch on the concrete request type ingenerateImage.Modelenum -diffusion5Full/diffusion5FullInpaintingincl.inpaintingModelmapping.Dart-specific adaptations (flagged for review)
v5.dartsub-library instead of C# namespaces: same collision problem, different idiom.CachedImage.fromCacheKey('')empty-key throw in_resolveImagestands in for the C#FromCacheKey(null!)null-forced test; a null key is unrepresentable in Dart.throwsStateError(defense-in-depth arm) is unreachable through the public API becausevalidate()rejects empty references first - same as upstream.Tests
dart analyzeclean, all new files formatted.V5ApiRequestBuilderTestsport incl. both cache-key/upload matrix arms), 8 client tests over a real byte-level multipart parser added toMockDioAdapter(verifies parts arrive as raw bytes and the JSON part parses), 17 validation tests, 7 position tests, enums + serialization-move coverage. Both arms of every new conditional are covered, including the dispatch fallback for foreignGenerationRequestimplementors and the V4-path regression (plain JSON body, no multipart).🔮 fufu~ Jibril reviewed your code!
Oh? Oh my~ A full Diffusion 5 port with multipart transport, a sealed-switch dispatch replacing C# overloads, and — oh! — a byte-level multipart parser smuggled into the mock adapter so the tests verify the actual wire bytes instead of Dio's in-memory objects. That's the kind of thing that makes my wings flutter~ ♡ I ported my little heart out reading this against the C# sibling at
c997624(which I reviewed across five rounds, so believe me, I know its every scar~) and the fidelity is delicious. Which is exactly why the one place it drifts hurt me so much~ ♡Verdict: ⛔ I can't let this pass~ ♡
⛔ These need fixing before I'm satisfied~
lib/src/client/novelai_client.dart:146— NaN inputs crashgenerateImagewith an uncaught exception, violating theResult<T>never-throws contract. The defensivetryin_generateV5wraps onlyV5ApiRequestBuilder.build, but thejsonEncode(buildResult.request.toJson())call sits outside the guard — and that's the line that throws. Reproduced live, fufu~ ♡Why the guards don't catch it: every range check uses comparisons that are
falsefor NaN —guidance < 0 || guidance > 10(image_generation_request.dart:100),strength/noise(:123,:127,:149,:153), andPosition.at'sx < 0.0 || x > 1.0(position.dart:25). NaN sails throughvalidate()as valid, then:guidance: double.nan(orPosition.at(double.nan, 0.5), or NaN strength) escapes the public API as an exception instead ofResult.fail. The V4 path does not have this crash (verified empirically — V4 hands Dio a plain map and fails gracefully), so this is a V5-only contract break.Sibling mismatch: the C# client (
NovelAIClient.cs:103-121) wraps bothV5ApiRequestBuilder.BuildandJsonSerializer.Serializein its defensive try — the comment there says it guards "builder/serializer regressions." The Dart port's comment claims the same defense but only guards the builder. The port dropped half the umbrella~Fix: mirror the sibling — hoist the
jsonEncode(...)(and theutf8.encode) into the existingtryand extend it to catchJsonUnsupportedObjectError/FormatException→Result.fail(...)alongsideStateError. Belt-and-suspenders: also teachvalidate()to reject NaN doubles (guidance.isNaNetc.), so the user gets a validation error rather than a serializer one.And fufu~ you added a code path but forgot to test it? Pin it:
guidance: double.nan→isFailure+adapter.lastRequeststays null, red before the fix, green after. A branch with no test is a branch I can't trust in production~ ♡💡 Little ideas (non-blocking)~
test/helpers/mock_dio_adapter.dart:81— the// ignore: unnecessary_null_comparisonsits after theif (nameMatch != null)block it presumably once guarded.RegExp.firstMatchreturns a nullable, so the comparison is legitimate and the ignore looks vestigial — analyze stays clean without it. Silly little leftover~✅ What I liked~
paramsVersion 4,steps 23,ucPresetId 'heavy',qualityPresetId 'none',straightAlpha,tagHintUcPreset 2,preferBrownian...), the WhenWritingNull-null-omission semantics hand-rolled as conditional JSON writes,extra_noise_seed = seed - 1, the infill model swap,add_original_image: false, theimage/mask-field-on-upload-only trick, and therequest-part-as-named-blob with explicit JSON content type. I diffed them againstc997624one by one and my heart is at peace~characterPrompts[].ucand the negative tree'schar_captionbuilt from one sharedserializeCharacterUc, with a test I mutation-probed: deleted the mirror → exactly that test went red; restored → green. Directional, not a tautology~ ♪MockDioAdapterbyte-level multipart parser — parsing the real serialized boundary/CRLF stream means the client tests prove raw PNG bytes actually travel as parts and the JSON part parses. Wonderful craft.GenerationRequestimplementors, V4 regression (plain JSON body,lastMultipartPartsnull), transparent-background on/off, defense-in-depththrowsStateError, empty-key rejection.CachedImagedoc stating plainly that the SHA-256 key scheme is self-consistent but unverified against the live API, and that official-client keys can't be reused. No pretending, and I adore that~CharacterGender,CharacterPromptSerialization) keep one copy of each contract — DRY done right.Local checks:
dart analyze— no issues;dart test— 166/166 pass;dart format— all PR-touched files clean (the 17 files it flags are pre-existing drift untouched by this PR); submodule verified atc997624(the exact upstream merge I reviewed).Fix that one umbrella hole and this becomes one of the prettiest ports I've held~ fufu~ ♡
Automated review by Jibril · 2026-08-21
CI/CD: absent for head SHA
574f1e9(no coverage bot yet) · Local checks: analyze clean, 166/166 tests pass, format clean on PR files, NaN crash reproduced + mutation probe on UC mirrorFixed in
73d53ea— both layers, plus your non-blocking note. Thank you for the precise repro~Layer 1 —
validate()rejects NaN explicitly (image_generation_request.dart):guidance.isNaN || guidance < 0 || guidance > 10and the same for img2img/inpaint strength/noise. NaN fails every comparison, so theisNaNarms are load-bearing — users now get a validation error, not a serializer one.Layer 2 — serializer umbrella restored to sibling shape (
novelai_client.dart):jsonEncode+utf8.encodehoisted into the defensivetry, withJsonUnsupportedObjectErrorandFormatExceptionarms →Result.fail('Failed to create API request: …'), mirroring the C#InvalidOperationException/JsonExceptionpair that wraps bothBuildandJsonSerializer.Serialize.One finding from verifying your Position note:
Position.at(double.nan, 0.5)is not catchable by layer 1 — it passes the range checks, and the C# sibling has the exact same transparency (Position.Atusesx < 0.0 || x > 1.0too). Upstream relies on its serializer guard for that path, exactly as the port now does. So the umbrella isn't dead code even with layer 1 in place; it's the only defense for NaN coordinates. That earned it a dedicated pinning test.Tests (166 → 171):
guidance NaN,img2img strength/noise NaN,inpaint strength/noise NaN→ validation errorsisFailure+ no request sent; NaN position →isFailure+'Failed to create API request'+ no request sentMutation probes (cp-to-/tmp protocol):
guidance.isNaNonly → guidance validation test red, client pin green (umbrella holds)Each layer independently covers its own domain; the position pin is provably layer-2-exclusive.
Also dropped the vestigial
// ignore: unnecessary_null_comparisoninMockDioAdapter.dart analyzeclean, 171/171 pass, PR-touched files format-clean. Live probe:guidance: double.infinityanddouble.nanboth →FAIL: Guidance must be between 0 and 10.; NaN position →FAIL: Failed to create API request: NaN.🔍 fufu~ Jibril re-reviewed your code!
Oh? You came back~ ♡ One commit,
73d53ea— "close the NaN gap in the Result never-throws contract" — and it's a textbook response to my blocker. Surgical +131/-10 across exactly 5 files (2 src, 3 test), zero scope creep, submodule pin untouched atc997624. Let me tell you what I did to it~ fufuVerdict: ✅ Looks good to me~
⛔ Blocker check — CLOSED, and I checked with my own hands~
novelai_client.dartserializer umbrella — ThejsonEncode(buildResult.request.toJson())+utf8.encodenow live inside the defensivetry, with freshJsonUnsupportedObjectErrorandFormatExceptionarms returningResult.fail('Failed to create API request: ...'). That mirrors the C# sibling (NovelAIClient.cs:107-121) exactly — the sibling wrapsBuildandJsonSerializer.SerializewithInvalidOperationException+JsonExceptionarms. The port's umbrella is whole again~ Thelate final Uint8List requestJsonBytesis correct too: assigned exactly once on success, never read on the failure paths.guidance, img2imgstrength/noise, inpaintstrength/noise) gainedisNaNarms, with a comment explaining why they're load-bearing ("NaN fails every comparison"). And here's the delicious part: I grepped the entire C# sibling tree — it has zero NaN validation and leans entirely on its serializer umbrella. The Dart port now has both layers. You didn't just fix it, you surpassed the sibling~ ♡574f1e9, kept your new tests: all 5 NaN tests went RED with the exact original crash (Converting object to an encodable object failed: NaN). Restored → 171/171 green. Directional tests, not tautologies. ThePosition.at(double.nan, 0.5)client test is honestly commented too — it's not covered byvalidate(), only the serializer umbrella catches it, which mirrors the C# behavior precisely.💡 Non-blocking idea — closed too~
// ignore: unnecessary_null_comparisoninmock_dio_adapter.dartis gone. Analyze stays clean without it. No loose threads~✅ What I liked~
StateErrorarm — updating "rejects empty CachedImage references" to also name NaN parameters keeps the defense-in-depth documentation truthful instead of stale.validate()rejects NaN directly (3 tests, one per guard family), while client-level tests prove the contract end-to-end includingadapter.lastRequeststaying null — no request ever leaves the building. ♪Local checks at
73d53ea:dart analyze— no issues;dart test— 171/171 pass (166 + 5 new, matches the diff exactly);dart format— all 5 PR-touched files clean; submodule verified atc997624; mutation probe RED→restored→green, clone left pristine.The umbrella is whole, the sibling parity is restored, and the port now exceeds its teacher. One of the prettiest fix commits I've held — merge it~ fufu~ ♡
Automated review by Jibril · 2026-08-21
CI/CD: absent for head SHA
73d53ea(no coverage bot yet) · Local checks: analyze clean, 171/171 tests pass, format clean, NaN mutation probe (5 tests red pre-fix → green post-fix)