# Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] ## [0.8.0] - 2026-10-06 Provenance and connector wave. Adds two opt-in connectors (Atlas Cloud and fal.ai), experimental Mode 2 Ed25519 manifest signing, and a failed-attempt ledger that keeps fallback history in the manifest. JPEG and WebP embedding now splices XMP metadata byte-for-byte, so content binding holds for every image format. `chat()` opt-in backoff now also covers transient 5xx responses, `ObjectStorageSink` can upload from a provider's `output_dir`, the ffmpeg helpers are public for custom deterministic providers, and `MockProvider` gains opt-in input validation. Upgrade notes: `genblaze-core` no longer installs Pillow; declare `pillow` yourself if you import `PIL`. Manifests that recorded a fallback carry `failed_attempts` and need a reader at `genblaze-core` 0.3.9 or later. This heading is the release **wave** name and the git tag (`v0.8.0`); individual PyPI package versions move independently and are listed below (the umbrella `genblaze` package is `0.4.6`). Wave tags and the umbrella's PyPI versions are separate sequences that happen to look alike — don't pin `genblaze==0.8.0`. A pin on a wave tag either fails outright or, worse, resolves silently to an unrelated umbrella build from a different wave (e.g. `genblaze==0.4.0` on PyPI predates the `v0.4.0` wave). Pin the exact umbrella version above, or a lockfile for full reproducibility (the umbrella pins ranges, not exact versions, for its own dependencies). ### Released package versions - `genblaze` (umbrella) 0.4.5 → **0.4.6** - `genblaze-core` 0.3.8 → **0.3.9** - `genblaze-cli` 0.3.6 → **0.3.7** - `genblaze-s3` 0.3.6 → **0.3.7** - `genblaze-atlascloud` **0.1.0** (new) - `genblaze-fal` **0.1.0** (new) - `genblaze-assemblyai` 0.3.2 → **0.3.3** - `genblaze-gmicloud` 0.3.5 → **0.3.6** - `genblaze-google` 0.3.4 → **0.3.5** - `genblaze-nvidia` 0.3.3 → **0.3.4** - `genblaze-openai` 0.3.4 → **0.3.5** - `genblaze-runway` 0.3.3 → **0.3.4** - `@genblaze/spec` 0.4.0 → **0.4.1** ### genblaze - **Added** an `atlascloud` extra and included Atlas Cloud in the `image`, `video`, and `all` bundles. - **Added** a `fal` extra and included fal.ai in the `image`, `video`, `audio`, and `all` bundles. ### genblaze-atlascloud - **Added** an opt-in Atlas Cloud connector for asynchronous image and video generation. Submission is single-attempt to avoid duplicate billable jobs; prediction GET requests use bounded transient retries, and URL-bearing model inputs are validated before being forwarded. ### genblaze-fal - **Added** an opt-in fal.ai connector that reaches fal's image, video, and audio catalog through its queue API over plain HTTP. Ships starter defaults for FLUX, Wan video, and Stable Audio; any other endpoint id passes through. Submission is single-attempt to avoid duplicate billable jobs, and status and result GETs use the provider's bounded `RetryPolicy`. URL-bearing inputs are validated before being forwarded, `FAL_KEY` is sent only to the queue host, and checkpointed request ids can be resumed from any process (#267). ### genblaze-google - **Changed** requires `genblaze-core>=0.3.9` so the widened `chat()` retry scope (#264) is always present. - **Fixed** Veo on the Gemini Developer API (`api_key` / `GEMINI_API_KEY`) now downloads the generated video to a local file and exposes a `file://` asset URL — matching the Vertex path — instead of leaving a credentialed Files API URI that `ObjectStorageSink`/B2 could not fetch unauthenticated (#263). - **Security** `VeoProvider` no longer interpolates an unvalidated `step_id` into the local output filename. A `Step` built or deserialized with a caller-supplied `step_id` containing `../` traversal or an absolute path could previously escape the configured `output_dir` and, following an existing symlink, silently overwrite another job's output or any process-writable file. `step_id` is now validated as a UUID, the resolved path is verified to stay under `output_dir`, and the write refuses to follow symlinks or clobber an existing file — on both the Vertex and Gemini auth paths (#284). - **Fixed** README and `examples/imagen_pipeline.py` quickstarts referenced the delisted `imagen-3.0-*` slugs, which now 404 at preflight for every new user; updated to the catalog-listed `imagen-4.0-*` slugs, documented the entitlement caveat, and documented the previously-unlisted `GeminiImageProvider` as the no-entitlement alternative (#233). - **Fixed** `chat()`/`achat()` with `retry_on_rate_limit=True` (or `retry_policy=`) now retries a Gemini `503 UNAVAILABLE` / model-overloaded error under the same backoff as a 429 instead of failing on the first attempt. Requires the genblaze-core release carrying this fix (#264). ### genblaze-core - **Fixed** a step rescued by `fallback_models` no longer erases the failed primary from provenance. Each superseded failure is now recorded, oldest first, in the new `Step.failed_attempts` list (`StepAttempt`: step id, model, provider, sanitized error, error code, upstream prediction id, cost, retries, timestamps), on both `run()` and `arun()`. The field is omitted from serialization when empty, so manifests without a fallback keep their `canonical_hash` byte-for-byte. Only each attempt's `model`, `provider` and `error_code` enter the hash, so identical runs still hash identically. Failed-attempt cost is reported per attempt (usually `None`: providers price successes only) and is not added to `Step.cost_usd`. **Compatibility:** a manifest that recorded a fallback carries the new `failed_attempts` key and needs genblaze-core at this release or later to parse; older releases reject the unknown key (#239). - **Changed** `genblaze-core` no longer depends on Pillow at runtime, since JPEG/WebP embedding no longer decodes images. If your code imports `PIL`, declare `pillow` as your own dependency (#249). - **Added** `ObjectStorageSink(..., allowed_roots=[...])` to opt a provider-configured `output_dir` (e.g. `ElevenLabsTTSProvider(output_dir= "work/generated")`) into local-file asset transfer. Previously a valid generation written outside the built-in temp-dir allowlist failed only at storage finalization, after provider quota was already spent. The default stays temp-only — no behavior change without opting in — and each root is resolved and validated at construction (rejects a missing directory or the filesystem root) so a typo'd or overly broad root fails loudly immediately rather than silently widening what's readable (#247). - **Added** an opt-in `min_inputs` constructor kwarg on `MockProvider` (`genblaze_core.testing`/`genblaze_core.mocks`). The mock previously accepted any step regardless of `step.inputs`, so a pipeline step built without `external_inputs=` — input media a real provider would reject — passed a full contract-test suite and only failed on the first live run. `MockProvider(min_inputs=1)` now raises `ProviderError(INVALID_INPUT)` when `step.inputs` has fewer than the configured minimum. Default is `0`, preserving existing behavior for callers who don't opt in (#174). - **Fixed** `PromptTemplate("A {animal}")` now accepts the template positionally instead of raising `TypeError: BaseModel.__init__() takes 1 positional argument but 2 were given`. The positional spelling is the one shipped in `examples/batch_with_templates.py`, so the example crashed on its first `PromptTemplate` line. The keyword form is unchanged, and passing both raises `TypeError` (P1-02). - **Changed** the run entry points (`run`, `arun`, `batch_run`, `abatch_run`) now raise a `TypeError` that names the call site that works when a constructor- or builder-level option is passed to them. `run(cache=...)` previously produced a bare `got an unexpected keyword argument 'cache'`, which named neither where caching is configured nor how to spell it; it now adds `— caching is configured on the pipeline: .cache(StepCache(...))`. Covers `cache`, `config`, `metadata`, `tracer`, `preflight`, `tenant_id`, `project_id`, `chain`, `moderation`, `structured_log`, and `max_concurrency` (the last only on `run()`, the one entry point that does not take it). Genuinely unknown names keep CPython's exact wording (P1-03). - **Fixed** lazy top-level exports now appear in `dir(genblaze_core)` before first access, and `RunnableConfig` is available directly from `genblaze_core` (#55). - **Fixed** `BaseProvider` subclasses now fail loudly at construction with a `TypeError` if `super().__init__()` never ran — most commonly because the subclass was decorated with `@dataclass`, whose generated `__init__` silently replaces the inherited one. Previously this produced a half-initialized provider that constructed fine and then failed confusingly inside `invoke()`/`ainvoke()` with an `AttributeError` naming an unrelated private attribute (#261). - **Fixed** library loggers no longer write to stderr by default; a `NullHandler` on the `genblaze` namespace root hands the decision back to the consuming application, which opts in via its own handlers (#46). - **Added** `ModelRegistry(fallback_probe=...)` — an optional liveness probe connectors can attach for slugs that match no `ModelFamily`. Previously `validate_model()` only ever consulted a probe when a family pattern matched, so any slug covered only by the permissive fallback (a connector's mainline models included) graded `UNKNOWN_PERMISSIVE` regardless of whether it was real or fabricated. `fallback_probe` is the liveness counterpart to the existing `fallback` param-shape spec; it is opt-in and defaults to `None`, so registries that don't configure it are unaffected (#248). - **Fixed** `import genblaze_core.testing` no longer requires `pytest`. The mock providers moved to the pytest-free `genblaze_core.mocks` in 0.3.5, but `genblaze_core.testing` kept a module-level `import pytest` for `ProviderComplianceTests` — so the documented re-export path (`from genblaze_core.testing import MockVideoProvider`, used by the zero-API-key quickstart in `libs/core/README.md`) still failed with `ModuleNotFoundError: No module named 'pytest'` on a clean `pip install genblaze-core`. `pytest` is now imported inside the four compliance-harness methods that use it (P1-01). - **Fixed** JPEG and WebP embedding now splices the XMP metadata into the existing container instead of decoding and re-encoding the image through Pillow, matching the PNG and MP4 handlers. Previously every JPEG/WebP embed silently altered pixels (lossy WebP by up to ~16% of subpixels), so the delivered image no longer matched the bytes the manifest committed to. JPEG gains one XMP `APP1` segment; WebP gains one `XMP ` chunk (a simple-format `VP8`/`VP8L` file is promoted to extended `VP8X`). EXIF, ICC, third-party XMP and the compressed image data are kept byte-for-byte, re-embedding replaces the previous manifest, and files embedded by earlier versions still extract. Extraction now reads only real XMP containers, so a `` string planted in EXIF or other bytes can no longer shadow the embedded manifest. Handlers no longer transcode, so a non-JPEG/WebP source passed straight to `JpegHandler`/`WebpHandler` now raises `EmbeddingError` (`SmartEmbedder` already routes by magic bytes) (#249). - **Deprecated** `WebpHandler.embed(lossless=..., quality=...)`. Both configured the former re-encode; they are now ignored, emit a `DeprecationWarning`, and will be removed in genblaze-core 0.4.0 (#249). - **Changed** `call_with_rate_limit_retry` (behind the `retry_on_rate_limit=` flag on the `chat()` helpers) now retries `SERVER_ERROR` (5xx, e.g. Gemini `503 UNAVAILABLE`) as well as `RATE_LIMIT` by default, and honors an explicit `RetryPolicy`'s full `retryable_codes` — matching `BaseProvider`'s poll/fetch path. `TIMEOUT` retries only via an explicit policy, since a timed-out long generation can still be billed; callers already passing an explicit `RetryPolicy()` now retry `TIMEOUT` too. Pass `RetryPolicy(retryable_codes=frozenset({ProviderErrorCode.RATE_LIMIT}))` for the previous 429-only behavior (#264). - **Added** public ffmpeg helpers behind `FFmpegCompositor` / `FFmpegTransform` for custom deterministic providers: `from genblaze_core.providers import resolve_ffmpeg, resolve_input_path, run_ffmpeg, get_output_path, populate_file_asset_integrity, local_file_url, FFMPEG_TIMEOUT`. Authors no longer re-implement the security-sensitive parts (the SSRF check on `https://` inputs, presigned-URL redaction in logs and errors). As part of the promotion, `get_output_path` now rejects an `ext` that is not alphanumeric and a `step_id` containing `/`, `\`, `:` or NUL, so a crafted value cannot write outside `output_dir`; it also accepts a `str` directory and always returns an absolute path. `genblaze_core.providers._ffmpeg_utils` still imports. See the "Deterministic ffmpeg provider" section of `docs/guides/new-provider.md` (#195). - **Fixed** `SmartEmbedder`/`SidecarHandler` sidecar and pointer embed modes now copy the source media to a distinct `output=` path (streamed, so it shares MP4's 2 GB size ceiling rather than a smaller in-memory cap) before writing the sidecar, matching the inline handlers' contract. Previously, `output=` in pointer mode (or any sidecar fallback) wrote only the sidecar JSON next to the requested path and reported it in `EmbedResult.path`, even though no media file existed there (#238). ### genblaze-s3 - **Docs** clarified browser access to private B2 buckets: a durable URL (`asset.url` under the default `URLPolicy.AUTO`) carries no credentials and 401s in a browser by design — use `presigned_get`/`presigned_get_url` instead. Added regression coverage proving those methods already emit the path-style, SigV4-signed URL shape B2 requires (virtual-host-style presigns 403 against B2) with no extra `boto3.Config`, since `for_backblaze()`'s custom `endpoint_url` already selects that addressing style by default; see `docs/features/object-storage.md` (#246). ### Internal - **Security** hardened the `@genblaze/spec` TypeScript type-generation toolchain against npm supply-chain attacks (#270): `generate-types.sh` now installs `json-schema-to-typescript`/`typescript` via `npm ci --ignore-scripts` from a new committed `libs/spec/package-lock.json` instead of an unpinned `npx --yes` fetch, and the release workflow's `publish-npm` job (which holds `id-token: write`) no longer installs or executes any npm package — type generation moved to a new low-privilege `build-npm-types` job whose verified output is handed to `publish-npm` as a build artifact, and now sets `NPM_CONFIG_IGNORE_SCRIPTS=true` for the whole job. No change to the generated `genblaze.d.ts` or to the tarball's published file contents (`ts/genblaze.d.ts`, `schemas/**/*.json`, `README.md`); `package.json` itself gains the new `devDependencies`/ `scripts` fields used for local regeneration. ### genblaze-gmicloud - **Fixed** the video poll loop is now bounded by a `max_poll_seconds` ceiling. A request stuck reporting `dispatched` previously polled forever, wedging the pipeline with no way to recover; it now fails the step with a timeout once the ceiling is passed. The ceiling is tracked per prediction id and cleared on a terminal status, and can be disabled by passing `None` (#262). - **Fixed** `validate_model()` now probes slugs that match none of the connector's specialized model families (Seedream, Gemini-Flash, FLUX-Kontext, Reve create, Bria fibo, and any new GMI model) using the same empty-payload probe the specialized families already use. Previously these slugs — including a connector's actual production models — graded identically to a fabricated slug (`unknown_permissive`, no signal). A slug the probe confirms dead now raises at `Pipeline` preflight instead of failing mid-run; a slug the probe confirms live grades authoritative (#248). - **Fixed** every GMI Cloud audio model (TTS and music) was unreachable — the `gmi-audio-tts` and `gmi-audio-music` families' `param_allowlist` didn't include the API's required `text`/`lyrics` fields, so every request 400'd with "Required parameter is missing" before it reached GMI. `prompt` now aliases to `text` for TTS and to `lyrics` for music, matching the same `prompt=` idiom every other modality uses (#251). ### genblaze-openai - **Changed** requires `genblaze-core>=0.3.9` so the widened `chat()` retry scope (#264) is always present. The `openai` SDK cap widened to `<4`; verified against 3.17.0 (#274). - **Fixed** `DalleProvider` image edits from an `https://` reference image (e.g. a presigned object-storage URL) always failed upstream with `unsupported mimetype`. The download's temp file was named with a fixed `.img` suffix, so the OpenAI client inferred `Content-Type: application/octet-stream` instead of the source's real type. The suffix is now derived from the input `Asset.media_type` (falling back to `.png`) (#253). - **Fixed** `chat()`/`achat()` with `retry_on_rate_limit=True` (or `retry_policy=`) now also retries 5xx, which previously failed fast because opting in disables the OpenAI SDK's own retry. Requires the genblaze-core release carrying this fix (#264). - **Fixed** `DalleProvider.generate()` dropped the `usage` block on `gpt-image-*` responses, leaving `Step.provider_payload` empty and no way to reconcile actual token-based cost against the registry's pre-flight estimate. Input/output/total token counts are now copied into `Step.provider_payload["usage"]` before pricing runs, so a user-registered usage-based pricing recipe can read them; `dall-e-2`/`dall-e-3` responses carry no `usage` block and are unaffected (#240). ### genblaze-assemblyai - **Changed** the `assemblyai` SDK cap widened to `<2`; verified against 1.5.5 (#274). ### genblaze-nvidia - **Changed** the `[chat]` extra's `openai` cap widened to `<4`; verified against 3.17.0 (#274). ### genblaze-runway - **Changed** the `runwayml` SDK cap widened to `<6` (#243). ### genblaze-cli - **Changed** requires `genblaze-core>=0.3.9`. ## [0.7.0] - 2026-07-28 Bug-fix wave with one new opt-in feature and one new provider. Closes a regression in the ReDoS pattern-safety heuristic (both the shape #196/#200 fixed and two further false-positive/false-negative bugs those same fixes introduced), adds an opt-in rate-limit backoff for `chat()`/vision helpers, ships a native Gemini image provider, fixes Runway's image-to-video routing, and closes a `verify --fetch` SSRF gap. This heading is the release **wave** name and the git tag (`v0.7.0`); individual PyPI package versions move independently and are listed below (the umbrella `genblaze` package is `0.4.5`). Wave tags and the umbrella's PyPI versions are separate sequences that happen to look alike — don't pin `genblaze==0.7.0`. A pin on a wave tag either fails outright or, worse, resolves silently to an unrelated umbrella build from a different wave (e.g. `genblaze==0.4.0` on PyPI predates the `v0.4.0` wave). Pin the exact umbrella version above, or a lockfile for full reproducibility (the umbrella pins ranges, not exact versions, for its own dependencies). ### Released package versions - `genblaze` (umbrella) 0.4.4 → **0.4.5** - `genblaze-core` 0.3.7 → **0.3.8** - `genblaze-cli` 0.3.5 → **0.3.6** - `genblaze-assemblyai` 0.3.1 → **0.3.2** - `genblaze-decart` 0.3.2 → **0.3.3** - `genblaze-elevenlabs` 0.3.2 → **0.3.3** - `genblaze-gmicloud` 0.3.4 → **0.3.5** - `genblaze-google` 0.3.3 → **0.3.4** - `genblaze-hume` 0.3.2 → **0.3.3** - `genblaze-lmnt` 0.3.2 → **0.3.3** - `genblaze-nvidia` 0.3.2 → **0.3.3** - `genblaze-openai` 0.3.3 → **0.3.4** - `genblaze-runway` 0.3.2 → **0.3.3** - `genblaze-stability-audio` 0.3.2 → **0.3.3** ### genblaze-core - **Security** the ReDoS pattern-safety heuristic (`pattern_safety.py`) missed two more catastrophic-backtracking shapes on top of #157 (fixed in 0.6.0): a quantified alternation with overlapping branches whose textual prefixes differ, and adjacent unbounded-quantified groups whose reachable character sets actually overlap despite looking disjoint by source text alone (#196, #200). Both are now rejected by `assert_safe()`. That charset-overlap gate itself had two bugs, also closed here: an unescaped `.` no longer resolves to the literal character `'.'` (it now correctly means "matches anything"), so a pattern like `(.+)([a-z]+)$` is rejected as it should be; and a `(?:...)`/`(?P...)` group's marker prefix is now stripped before charset analysis, so a genuinely safe pattern like `(?:[a-z]+)(?:[0-9]+)$` is no longer wrongly rejected. **Upgrade impact:** the shipped connector catalog contains no pattern of either shape, so this only affects third-party or future connectors — a `ModelFamily` pattern with two or more adjacent unbounded groups where one uses `.` will now be rejected at import time (fix: separate the groups with a mandatory, non-nullable delimiter, or narrow `.` to an explicit character class); a pattern using `(?:...)`/`(?P...)` that was previously (incorrectly) rejected will now import successfully. Lookaround groups (`(?=...)`, `(?!...)`) keep their existing, unchanged handling. - **Fixed** `Mp4Handler.embed()` raised on a `str` path instead of accepting one like every other media handler, and `Pipeline.step()` accepted any object as a provider instead of validating it's a `BaseProvider` — a non-provider value previously surfaced a confusing failure deep inside `run()` instead of immediately at the call site (#224, #225). ### genblaze-openai - **Added** opt-in rate-limit backoff for `chat()`/`achat()` and the vision helpers via `retry_on_rate_limit=`/`retry_policy=` kwargs, wrapping calls in `genblaze_core.providers.retry.call_with_rate_limit_retry` (#221). A follow-up in this same release disables the SDK's own internal retry when genblaze already manages backoff on an internally-created client, closing a double-retry (multiplicative wait) bug the initial opt-in introduced; a caller-supplied `client=` keeps its own retry configuration untouched (#221, #235). The identical feature and fix landed in `genblaze-google` (see below); currently wired into `openai`/`google` only — see `docs/features/llm-calls.md`. - **Docs** confirmed `estimate_cost()` already computed per-model pricing correctly; the reported gap was a documentation error in `docs/reference/pricing-recipes.md`, now corrected, plus added test coverage pinning the existing (correct) behavior (#222, #223). No production code changed. ### genblaze-google - **Added** the same opt-in rate-limit backoff described under `genblaze-openai` above (#221, #235). - **Added** `GeminiImageProvider`, a native Gemini image-generation provider (`google-gemini-image` entry point) alongside the existing Imagen provider, sharing client construction via a new `GoogleClientMixin` (#205). - **Fixed** a probe-confirmed-`LIVE` but known-gated Imagen slug reported a misleading authoritative `OK` instead of `OK_PROVISIONAL`, mirroring the same gmicloud fix below (#206). - **Fixed** `chat()`/vision calls sent an `ImageURLContent` input as an unsupported field instead of translating it to Gemini's `inline_data`/`file_data` wire format (#217). ### genblaze-gmicloud - **Fixed** preflight validation didn't distinguish a known-catalog slug from one confirmed callable with the caller's API key, reporting a misleading authoritative `OK` for a probe-confirmed slug that isn't confirmed-callable instead of `OK_PROVISIONAL` (#193). Added a guard against a misconfigured `base_url`/`GMI_BASE_URL` pointed at the wrong endpoint shape (a silent-404 footgun), and an edit-mode fallback path. ### genblaze-runway - **Fixed** `image_to_video` on models that require an input image (e.g. `gen4_turbo`, `gen3a_turbo`) now raises a clear, actionable error instead of silently failing when no image is supplied; corrected the model catalog's ratio literals, which were wrong pixel-dimension strings for several models (#226). ### genblaze-lmnt - **Fixed** an unsupported `seed` parameter was silently forwarded to `generate_detailed` instead of being dropped; it's now dropped with a one-time warning so callers know to stop passing it (#207). ### genblaze-cli - **Fixed** `verify --fetch` accepted a `file://` URL with a remote authority (e.g. `file://evil-host/path`) instead of rejecting it, and could leak presigned-URL userinfo/credentials into output; both are now blocked/ redacted. Also dedupes redundant stream-hash computation on the same asset (#214). ### genblaze (umbrella) - **Fixed** the umbrella's lazy `__getattr__` lost the original install hint to a generic message when propagating `OptionalDependencyError` instead of preserving it (#213). - **Added** a `parquet` extra re-exposing `genblaze-core[parquet]`, so `pip install "genblaze[parquet]"` — the exact incantation `OptionalDependencyError` prints for `ParquetSink` — resolves instead of failing with an unknown-extra error. ### Packaging - **Fixed** nine connectors' (`genblaze-assemblyai`, `-decart`, `-elevenlabs`, `-google`, `-hume`, `-lmnt`, `-nvidia`, `-openai`, `-stability-audio`) `genblaze-core` dependency floor was raised to `>=0.3.7` for `local_file_url()` users, but each package's own published version wasn't bumped alongside it (#209) — a `pypi-pin-parity` trap that would have blocked (not silently shipped) this release. Closed here by bumping all nine to a new patch version. - **Fixed** `genblaze-openai` and `genblaze-google` import `call_with_rate_limit_retry`, new in `genblaze-core` 0.3.8 this release, but still declared a `genblaze-core>=0.3.7` floor — the same class of gap as the item above, catchable only because it's caught here rather than by any automated gate (`tools/prepare_release.py` doesn't check connector→core floors against the symbols a connector actually imports). Both floors are now `genblaze-core>=0.3.8,<0.4`. ### Internal - **Fixed** `install-verify`'s "Wait for PyPI to index the umbrella" pre-check only polled for the `genblaze` umbrella itself, but `genblaze[all]` also resolves the ~14 connector packages that propagate across PyPI's CDN independently — so the pre-check could pass while a connector's simple-index page was still stale, and the very next `pip install "genblaze[all]==$version"` false-red the job on a genuinely-healthy release (hit twice during the v0.6.0 release, #189). The fresh install step itself now retries up to 5 times (30s apart) so transient propagation of any package is tolerated; a truly missing/unresolvable package still fails the job after retries are exhausted. The umbrella pre-check and the import-smoke invocation are unchanged. Release tooling only — no packaged code changed. - **Fixed** the lint job's `ruff` version could drift from what contributors run locally, producing false-red CI on a clean local run; pinned to `0.14.0` (#212). - **Fixed** Dependabot could propose a major-version bump on a runtime dependency, which this repo's pinning policy caps below the next major by hand; Dependabot config now excludes major-version updates for runtime deps (#216). - **Fixed** `tools/batch_cluster.py` could drop an in-flight dependency blocker between clustering passes (#210). - **Chore** bumped the GitHub Actions dependency group (#191). ## [0.6.0] - 2026-07-22 Bug-fix and compatibility wave with one new opt-in feature. Fixes cross-platform (Windows) `file://` asset uploads, ports the elevenlabs and lmnt connectors to their SDK 2.x response shapes (lmnt's `speed` param has no 2.x equivalent and is now dropped with a warning — see genblaze-lmnt below if you set it), routes gmicloud Seedance FLF2V frames to their native slots, makes optional-dependency introspection safe, hardens the sink against B2 `HeadObject` 403s, normalizes CAS-key extension case for dedup, skips the network `HeadBucket` for foreign S3 URLs, falls back to input `char_count` in `per_input_chars` pricing, closes remaining ReDoS heuristic gaps, and adopts a mechanical dependency upper-bound policy across every package. New: `genblaze verify --fetch` performs byte-level verification of output assets against the manifest's committed digests. This heading is the release **wave** name and the git tag (`v0.6.0`); individual PyPI package versions move independently and are listed below (the umbrella `genblaze` package is `0.4.4`). Wave tags and the umbrella's PyPI versions are separate sequences that happen to look alike — don't pin `genblaze==0.6.0`. A pin on a wave tag either fails outright or, worse, resolves silently to an unrelated umbrella build from a different wave (e.g. `genblaze==0.4.0` on PyPI predates the `v0.4.0` wave). Pin the exact umbrella version above, or a lockfile for full reproducibility (the umbrella pins ranges, not exact versions, for its own dependencies). ### Released package versions - `genblaze` (umbrella) 0.4.3 → **0.4.4** - `genblaze-core` 0.3.6 → **0.3.7** - `genblaze-cli` 0.3.4 → **0.3.5** - `genblaze-s3` 0.3.5 → **0.3.6** - `genblaze-assemblyai` 0.3.0 → **0.3.1** - `genblaze-decart` 0.3.1 → **0.3.2** - `genblaze-elevenlabs` 0.3.1 → **0.3.2** - `genblaze-gmicloud` 0.3.3 → **0.3.4** - `genblaze-google` 0.3.2 → **0.3.3** - `genblaze-hume` 0.3.1 → **0.3.2** - `genblaze-langsmith` 0.3.1 → **0.3.2** - `genblaze-lmnt` 0.3.1 → **0.3.2** - `genblaze-luma` 0.3.1 → **0.3.2** - `genblaze-nvidia` 0.3.1 → **0.3.2** - `genblaze-openai` 0.3.2 → **0.3.3** - `genblaze-replicate` 0.3.3 → **0.3.4** - `genblaze-runway` 0.3.1 → **0.3.2** - `genblaze-stability-audio` 0.3.1 → **0.3.2** ### genblaze-core - **Security** the ReDoS pattern-safety heuristic (`pattern_safety.py`) missed two catastrophic-backtracking shapes: a quantified alternation with overlapping (not just byte-identical or textually-prefixed) branches, e.g. `(a|aa)+` or the character-class overlap `(?:[a-c]|[a-z]{2})+`; and adjacent unbounded-quantified groups separated by a nullable delimiter like `-?` or `\s*` (`(a+)-?(a+)`) (#157). Both are now rejected by `assert_safe()` — the alternation check adds a character-set overlap test (on top of the existing textual-prefix check) for branches that reduce to a flat sequence of simple atoms. This is hardening, not a fix for an active exploit — the shipped connector catalog contains no pattern of either shape; the exposure was a future/third-party connector pattern letting an attacker-influenced value reach `ModelFamily.matches()` and hang the worker. Residual gaps (documented in the module docstring): a branch containing a nested group isn't reduced to a character set, so overlap there still relies on the textual-prefix check only (e.g. `a(?:b)?` vs `aa`); a mandatory delimiter whose own characters coincide with a flanking group's quantified content (e.g. `(a+)a(a+)`); a nullable *group* between unbounded groups (`(a+)(?:x)?(a+)`); and backreferences, which the static heuristic doesn't model at all (pre-existing, masked by `google-re2` when installed). The alternation character-set check is a deliberate conservative over-approximation — two branches sharing any character are flagged, so a few non-ambiguous alternations (`(?:v1|v2)+`) are rejected too. On rejection, `assert_safe()` now names the specific shape and gives remediation that actually clears the check (rather than a generic list including fixes, like possessive quantifiers, that don't apply to every shape). A top-level alternation of two unbounded groups (`(a+)|(b+)`) is correctly treated as safe — the groups are in different branches, not adjacent. - **Fixed** `CONTENT_ADDRESSABLE` storage keys did not normalize the source extension's case, so byte-identical content fetched via a differently-cased extension (e.g. `IMG.PNG` vs `img.png`) hashed to the same sha256 but landed at different keys — defeating dedup and storing the bytes twice (#20). `_build_key` now lowercases the extension only for the CAS branch (the key is content-addressed, so casing is a pure accident of the origin URL); `HIERARCHICAL` keys are asset_id-based, not content-hash-based, so their extension casing is left untouched. Objects already stored under an upper-case-extension CAS key are unaffected by this change and remain reachable at their existing key — only *future* uploads of that content will resolve to the lowercase key, so a pre-existing upper-case duplicate won't retroactively merge with it. - **Fixed** `hasattr(genblaze_core, "ParquetSink")` (and `getattr(module, name, default)`, `inspect`/IDE attribute probing) raised `OptionalDependencyError` instead of returning `False`/the default when the `parquet` extra isn't installed (#165). The umbrella package's lazy `__getattr__` propagated `OptionalDependencyError` from the failed `pyarrow` import, but that error is an `ImportError`, not an `AttributeError` — the only exception type `hasattr` swallows — so capability probing crashed instead of reporting the symbol as absent. `genblaze_core.__getattr__` now catches `OptionalDependencyError` at the lazy-import call site and re-raises `AttributeError` with the original install-hint message preserved (chained via `__cause__`); direct usage (e.g. `genblaze_core.ParquetSink(...)`) still gets the actionable `pip install "genblaze[parquet]"` hint. `OptionalDependencyError` itself is unchanged (still `ImportError`-catchable) for non-attribute import paths. - **Fixed** local-output assets (`FFmpegCompositor`, `FFmpegTransform`, and every connector that built its `file://` URL via `quote()`) could never upload to B2 on Windows (#164). `file://` URLs were built as `f"file://{quote(str(path.resolve()))}"`, which percent-encodes a Windows drive colon and backslashes into `file://C%3A%5CUsers%5C...`; `urlparse` then reads the entire percent-encoded string as `netloc` and leaves `path` empty, so `_read_local_file`'s allowlist check always rejected it. A new `genblaze_core._utils.local_file_url()` helper builds these URLs with `Path.as_uri()` instead, producing the empty-netloc form (`file:///C:/Users/...`) that `_read_local_file` (via `url2pathname`) and `validate_chain_input_url` already parse correctly on every platform — the sink and validator were already fixed; only the URL construction was release-lagged. POSIX behavior is unchanged (`as_uri()` already produced the same form there). - **Fixed** `per_input_chars()` silently returned `cost_usd=None` for chain-input steps (#54). When a step is fed by an upstream step's output, `Step.prompt` is typically `None` and the text lives on `Step.inputs` instead — the strategy only read `ctx.step.prompt`, so TTS/analysis steps chained off another step looked "free" instead of "unpriceable". It now falls back to summing `metadata["char_count"]` across the step's input assets when there's no prompt text, ignoring inputs with no/invalid `char_count` (e.g. images). Returns `None` only when neither a prompt nor any usable `char_count` exists; a genuinely-present `char_count` of `0` still yields a real `0.0` cost rather than an "unknown" one. - **Fixed** an otherwise-complete run could fail when re-checking an asset already stored in the sink's backend under a B2 daily Class-B cap or a restricted key (#162). `ObjectStorageSink._asset_already_transferred` probed `backend.exists(key)`, but a B2 `HeadObject` `403` reports the key as absent, which re-scheduled a transfer of the now-durable (private) URL over an unauthenticated `GET` (`401`) — and with no source left to re-download from, that re-transfer could never succeed. The sink now trusts a backend-owned asset URL directly (valid `sha256` + non-null `size_bytes` + a resolvable backend key), since `transfer()` only rewrites `url` to the durable URL *after* a successful put. **Tradeoff:** object existence is no longer independently probed for backend-owned URLs within a run; a byte-level `genblaze verify --fetch` (below) remains available for callers that want post-hoc confirmation. ### genblaze-cli - **Added** `genblaze verify --fetch` downloads each output asset and compares its bytes against the manifest's committed `sha256` (with a `size_bytes` cross-check), closing the gap where verification stopped at declared digests. Remote fetches stream through the transfer layer's SSRF-validated, DNS-pinned path; presigned query strings are redacted from output; `file://` assets resolve against the same allowlist as the storage and ffmpeg paths, extensible via repeatable `--allowed-root `. `--fetch` and `--hash-only` are mutually exclusive. Default `verify` behavior and `Manifest.verify()` semantics are unchanged. ### genblaze-elevenlabs - **Fixed** `with_timestamps=True` TTS steps failed against elevenlabs 2.x (#163). `convert_with_timestamps` returns an `AudioWithTimestampsResponse` pydantic model, not a dict — `response.get("audio_base64", ...)` raised `'AudioWithTimestampsResponse' object has no attribute 'get'`, and even a dict-shaped stand-in would have missed the field, which is `audio_base_64` (underscores) on the parsed object. `provider.py` now reads `response.audio_base_64` and `response.alignment.characters` / `character_start_times_seconds` / `character_end_times_seconds` directly, and tolerates `alignment` being `None` (it's `Optional` on the real model). Verified against the installed `elevenlabs` 2.38.1 SDK's response types; the object-response surface has been present since 2.0.0, so no floor bump was needed — pinned to `elevenlabs>=2.0,<3` (previously unbounded) so a fresh install can't resolve an incompatible future major version. - **Fixed** `fallback_models` could never fire on ElevenLabs steps (#167). `_errors.py`'s mapper had no `MODEL_ERROR` branch, so an unknown/retired `model_id` surfaced as `INVALID_INPUT` or `UNKNOWN` and the pipeline's fallback retry (which only triggers on `MODEL_ERROR`) never engaged. A 404 status code (elevenlabs.errors.NotFoundError — raised for any missing resource, most commonly an unknown model_id or voice_id) now maps to `MODEL_ERROR`; the message-based fallback delegates to the shared `classify_api_error` classifier instead of duplicating its "model" + "not found" pattern. - **Fixed** local audio outputs (TTS + SFX) couldn't upload to B2 on Windows (#164) — see the `genblaze-core` entry above for the root cause. Both `provider.py` and `sfx.py` now build their `file://` asset URL via `genblaze_core._utils.local_file_url()`. ### genblaze-decart - **Fixed** local video/image outputs couldn't upload to B2 on Windows (#164) — see the `genblaze-core` entry above for the root cause. Both `provider.py` and `image.py` now build their `file://` asset URL via `genblaze_core._utils.local_file_url()`. ### genblaze-google - **Fixed** local video outputs (Veo inline-bytes fallback) and Imagen image outputs couldn't upload to B2 on Windows (#164) — see the `genblaze-core` entry above for the root cause. Both `provider.py` and `imagen.py` now build their `file://` asset URL via `genblaze_core._utils.local_file_url()`. ### genblaze-hume - **Fixed** local audio outputs couldn't upload to B2 on Windows (#164) — see the `genblaze-core` entry above for the root cause. `provider.py` now builds its `file://` asset URL via `genblaze_core._utils.local_file_url()`. ### genblaze-openai - **Fixed** local outputs (Sora video, DALL-E/gpt-image-1 images, TTS audio) couldn't upload to B2 on Windows (#164) — see the `genblaze-core` entry above for the root cause. `dalle.py`, `provider.py`, and `tts.py` now build their `file://` asset URL via `genblaze_core._utils.local_file_url()`. ### genblaze-stability-audio - **Fixed** local audio outputs couldn't upload to B2 on Windows (#164) — see the `genblaze-core` entry above for the root cause. `provider.py` now builds its `file://` asset URL via `genblaze_core._utils.local_file_url()`. ### genblaze-gmicloud - **Fixed** data loss on Seedance first/last-frame (FLF2V) video steps (#175). Seedance slugs (e.g. `seedance-2-0-260128`, `seedance-1-0-pro-fast-251015`) matched no dedicated family, so they fell through to the permissive fallback's `route_images(slots=("image",))` mapping — the second frame was silently dropped and the first landed in an `image` key that Seedance's API doesn't document. A new `gmi-video-seedance` family routes both frames to GMI's documented `first_frame`/`last_frame` slots (a single-image call still maps to `first_frame` for I2V). Every other video family is unaffected. ### genblaze-lmnt - **Fixed** a fresh `pip install genblaze-lmnt` couldn't run at all (#166). `provider.py` imported `from lmnt.api import Speech`, the pre-2.0 SDK layout; the unbounded `lmnt>=1.0` dependency pin let `pip` resolve the current `lmnt` 2.x release, which has no `lmnt.api` module, so every `generate()` call failed with `ModuleNotFoundError`. Ported the connector to the 2.6+ surface: the synchronous `lmnt.Lmnt` client and `client.speech.generate_detailed(..., return_timestamps=True)` (the JSON counterpart to `generate()` that also returns word-level timestamps, matching the old 1.x `synthesize()` response shape). Pinned to `lmnt>=2.6,<3`: 2.6.0 renamed this endpoint's `return_durations`/`durations` (item type `Duration`) to `return_timestamps`/`timestamps` (`Timestamp`), so the floor guarantees a single, consistent response shape across the supported range (verified against the PyPI-latest `lmnt` 2.13.0). - **Behavior change**: the 1.x `speed` step param has no 2.x equivalent (LMNT replaced it with `temperature`/`top_p`, which control expressiveness, not pacing). A step that previously set `speed` was forwarded straight to the 1.x API; it's now dropped before the call — callers get default-pace audio plus a `logging.warning` naming `temperature`/`top_p` as the closest replacement knobs, instead of the `TypeError` that forwarding an unsupported kwarg to the real 2.x SDK would raise. - **Fixed** local audio outputs couldn't upload to B2 on Windows (#164) — see the `genblaze-core` entry above for the root cause. `provider.py` now builds its `file://` asset URL via `genblaze_core._utils.local_file_url()`. ### genblaze-s3 - **Fixed** `key_from_url` forced a network `HeadBucket` (via `_ensure_region_verified()`) before comparing the URL's host to this backend's endpoint, and raised `StorageError` instead of returning `None` for a foreign URL on an unverified backend with bad credentials (#19) — breaking the documented "`None` = not mine" contract that cross-backend manifest routing (`read_manifest_for_asset`) relies on. `key_from_url` now compares the URL's host against the already-resolved endpoint first (a local attribute read, no network call) and returns `None` immediately for a clear mismatch; region verification never runs on this path. A B2 bucket that migrated to a different region after this backend was last verified is still recognized without a `HeadBucket`, since B2 bucket names are globally unique — a B2-shaped host plus an exact bucket-name match is sufficient proof of ownership on its own. ### Packaging - **Fixed** every package's `pyproject.toml` declared both the PEP 639 `license = "MIT"` SPDX expression and the legacy `License :: OSI Approved :: MIT License` trove classifier; PEP 639 says the classifier SHOULD NOT be used alongside a license expression, and setuptools >= 77 errors on the combination. Removed the redundant classifier from all 18 `pyproject.toml` files; `license = "MIT"` is unchanged (#60). - Applied a consistent upper-bound policy to third-party runtime dependencies: every dependency in `[project.dependencies]` (and non-dev-tooling `optional-dependencies` extras) across `libs/core`, every connector, `cli`, and the umbrella now caps below its next major version, matching the bounded-major convention the repo already used for `pydantic<3`, `urllib3<3`, `lmnt<3`, and `elevenlabs<3`. Packages still on a pre-1.0 line (`decart`, `langsmith`, `httpx`) cap at their next major (`<1`), matching the existing `assemblyai<1`/`hume<1` precedent rather than a next-minor rule, so pre-1.0 and post-1.0 SDKs follow one rule. Where a connector's documented floor was already several majors behind the version it resolves to today (`runwayml` floor 0.6 vs. resolved 4.x, `replicate` floor 0.25 vs. resolved 1.x, `openai` floor 1.0 vs. resolved 2.x, `genblaze-core`'s `parquet` extra's `pyarrow` floor 14.0 vs. resolved 22.x), the cap was set one major past the currently-resolving version instead of the floor's next major, so this fix doesn't reject the version already in use; the stale floors themselves are unchanged (out of scope here). Dev/test tooling extras (`pytest`, `ruff`, `mypy`, `deptry`, `hypothesis`, `jsonschema`, etc.) are unaffected — this policy applies to dependencies actually shipped to and resolved by consumers (#61). - `genblaze-assemblyai`, `genblaze-langsmith`, `genblaze-luma`, `genblaze-nvidia`, and `genblaze-runway` were bumped this wave for these packaging changes alone (no code change) — that is why they appear in the released-versions list without a per-package section above. ### Internal - **Fixed** `tools/prepare_release.py`'s `rewrite_floors` used a whole-line regex anchor that only matched a dependency alone on its own line, so the umbrella's single-line per-connector extras (`decart = ["genblaze-decart>=…"]`) were silently skipped and left on a stale floor every release. Switched to a token-level substitution that rewrites a quoted dependency anywhere on a line (inline extras, multi-line bundles, and multiple deps per line), with regression tests; this wave's floor-update count rose from 33 to 47 as a result. Release tooling only — no packaged code changed (stale floors were `>=` minimums, so installs still resolved correctly). ## [0.5.0] - 2026-07-16 Correctness and security hardening across the pipeline, provenance, streaming, and media layers, plus connector fixes (Replicate community models, OpenAI Sora on the openai SDK 2.x, Google Veo on Vertex AI). Highlights: the ReDoS guard's heuristic now always runs, manifests load tolerantly while `verify()` stays the enforcement boundary, `ParquetSink` writes are index-backed and resink-correct, stream emitters are per-instance, and a batch of connector patch-republishes. This heading is the release **wave** name and the git tag (`v0.5.0`); individual PyPI package versions move independently and are listed below (the umbrella `genblaze` package is `0.4.3`). Wave tags and the umbrella's PyPI versions are separate sequences that happen to look alike — don't pin `genblaze==0.5.0`. A pin on a wave tag either fails outright or, worse, resolves silently to an unrelated umbrella build from a different wave (e.g. `genblaze==0.4.0` on PyPI predates the `v0.4.0` wave). Pin the exact umbrella version above, or a lockfile for full reproducibility (the umbrella pins ranges, not exact versions, for its own dependencies). ### Released package versions - `genblaze` (umbrella) 0.4.1 → **0.4.3** — raises the `genblaze-core` floor to 0.3.6 and `genblaze-s3` to 0.3.5 (plus the gmicloud/google/openai/replicate extra floors) so `pip install genblaze` resolves this wave's fixes. - `genblaze-core` 0.3.4 → **0.3.6** (ReDoS heuristic always runs; per-instance stream emitters; `metadata`/`prompt_visibility` routed to `Step` fields; `max_concurrency` validation; `PipelineTemplate` param rendering; tolerant manifest load with `verify()` enforcement; `ParquetSink` run-index + resink correctness; SSRF/ffmpeg/`canonical_hash` hardening; stable ingest hash; order-preserving cache key; JPEG/WebP read cap; Windows `file://` fixes; pytest-free mocks) - `genblaze-cli` 0.3.2 → **0.3.4** (rejects directory arguments; `replay.py` Optional fixes; `verify` surfaces invalid output metadata; `extract -o`) - `genblaze-s3` 0.3.4 → **0.3.5** (widens the `aioboto3` pin to `<16`) - `genblaze-google` 0.3.1 → **0.3.2** (`VeoProvider` Vertex AI poll/fetch fix) - `genblaze-replicate` 0.3.2 → **0.3.3** (`submit()` endpoint routing for community models) - `genblaze-openai` 0.3.1 → **0.3.2** (Sora SDK 2.x submit/download; Windows `file://` in `dalle.py`) - `genblaze-gmicloud` 0.3.2 → **0.3.3** (ships the `py.typed` marker; video `duration` validation) - All other connectors unchanged — their existing `genblaze-core>=0.3.4,<0.4` floor already admits 0.3.6. ### genblaze-core - **Security** `pattern_safety.assert_safe()` no longer skips its catastrophic-backtracking heuristic when `google-re2` is installed (#148). A prior version (#80/#146) returned early once `re2.compile()` succeeded, but `re2` accepts nested-quantifier shapes like `(a+)+$` because *its own* engine matches them in linear time — `ModelFamily.matches()` always matches with stdlib `re`, which still backtracks catastrophically on the same input. Any environment with `re2` installed (CI, `[dev]`, `[re2]`) was therefore strictly weaker than the pre-#146 heuristic-only guard for exactly the shapes it targets. `re2` is now an additional gate rather than a replacement; the heuristic always runs. - **Fixed** a distinct `Pipeline`/`AgentLoop` instance run synchronously inside another instance's `stream()`/`astream()` worker (e.g. a step provider, moderation hook, or callback that drives its own sub-pipeline, or a nested `batch_run()`/`abatch_run()`) no longer cross-delivers its events into the outer consumer's queue (#151). `_emitter_slot` was a `ClassVar[EmitterSlot]` — one `contextvars.ContextVar` shared by every instance in the process — which correctly isolated concurrent `stream()`/`astream()` calls on the *same* instance (#147) but did nothing to isolate *different* instances sharing the same thread/task Context. Both classes now build their own `EmitterSlot` per instance in `__init__`; `Pipeline.__copy__`/`__getstate__`/ `__setstate__` always give a clone (including `batch_run()`/`abatch_run()` clones) a fresh, independent slot, never a shared one. - **Fixed** `Pipeline.step(metadata=..., prompt_visibility=...)` now route to the corresponding `Step` fields instead of being silently absorbed into provider `params` (#53). `prompt_visibility` is privacy-sensitive — it controls whether the prompt is persisted/cached in cleartext — so silently defaulting every step to `PUBLIC` regardless of what the caller passed was a data-exposure footgun. The reserved-name guard now also rejects `metadata=`/`prompt_visibility=` smuggled through `params={}`, and rejects caller `metadata=` keys that collide with the internal `_fallback_models`/ `_input_from` graph-bookkeeping keys. A model-fallback retry no longer wipes caller metadata (previously reassigned `Step.metadata` wholesale instead of merging). `batch_run(items=[...])` routes `metadata`/`prompt_visibility` item keys the same way and rejects the same reserved-key collisions, closing the same two gaps via the batch entry point. A step pre-failed by an invalid `input_from` reference now preserves `prompt_visibility` too (it previously defaulted back to `PUBLIC` on that path, even though the failed `Step` still carries the cleartext prompt). Added `Pipeline.metadata(**kwargs)` for run-scoped metadata (additive, merged into `Run.metadata` via `RunBuilder.meta()`). - **Fixed** `Pipeline.batch_run()`'s `max_concurrency` was a documented but dead argument — the sync implementation always ran items in a sequential loop — and `Pipeline.abatch_run(max_concurrency=0)` built an `asyncio.Semaphore` no task could ever acquire, hanging forever instead of failing fast (#83). Both APIs now validate `max_concurrency >= 1` (`GenblazeError` otherwise). `batch_run()`'s `max_concurrency` stays validated-but-inert by design — provider/sink instances are shared across batch clones and not guaranteed thread-safe under real OS-thread concurrency — but an explicit value now emits a one-time `UserWarning` pointing at `abatch_run()` for genuine concurrency; omitting it (the new `None` default) stays silent so existing call sites see no behavior change. - **Fixed** `PipelineTemplate.instantiate(variables=...)` rendered `{variable}` substitutions in `StepTemplate.prompt` only; `StepTemplate.params` passed through completely unrendered, so a template with `params={"voice": "{locale}_voice"}` reached the provider with the literal, unsubstituted string (#52). String values inside `params` — top-level or nested in `dict`/`list`/`tuple` — now render through the same `PromptTemplate` engine as `prompt`, so missing-variable behavior and doubled-brace escaping match exactly. **Behavior note:** templates that pass `variables=` AND have a literal identifier-shaped `{...}` string in `params` not meant as a placeholder must now double the braces (`{{...}}`), exactly as `prompt` already required — templates that never pass `variables=` are unaffected. - **Fixed** concurrent `stream()`/`astream()` calls on the same `Pipeline` or `AgentLoop` instance no longer cross-deliver events (#79, #84). The active emitter was a single mutable instance attribute, so a second concurrent call's install silently clobbered the first's, mixing one stream's events into the other's queue. Both classes now install the emitter on an `EmitterSlot` (`contextvars.ContextVar`-backed), which is isolated per thread/task with no additional locking required. - **Fixed** `stream()`/`astream()` no longer leak an abandoned worker's remaining events onto an undrained queue after an early break (#74). The worker used to keep running in the background for the rest of the pipeline's duration, enqueuing every subsequent event with nobody to drain it. The emitter now closes as soon as the early break is detected, so further `put()` calls become no-ops instead of buffering unboundedly. - **Fixed** `stream()`/`astream()` no longer emit a successful `pipeline.completed` terminal event for a run that aborts before reaching normal finalization — e.g. `pipeline_timeout` expiring before any step starts (#85). `all([])` treated an empty completed-steps list as "succeeded"; aborted runs are now always finalized as `FAILED` and emit `pipeline.failed`, with the abort's exception message attached. - **Fixed** concurrent `arun(fail_fast=True)` cancellation now preserves the original `step_id` for cancelled and exception-raising steps, so a `step.failed` event correlates with its own earlier `step.started` instead of minting a brand-new id (#86). - **Fixed** `step.completed` / `step.failed` stream events now carry `run_id`, matching every other pipeline-scoped event variant (#87). - **Security** `check_ssrf`/`resolve_ssrf` now catch IPv4-mapped IPv6, RFC 6052 NAT64 (`64:ff9b::/96`), and the unspecified `::/128` address, and add a property-based backstop (`is_private`/`is_loopback`/`is_link_local`/ `is_reserved`/`is_unspecified`) alongside the explicit denylist so ranges neither enumerates are still caught (#16). - **Security** ffmpeg `drawtext` overlay text now escapes `%`, neutralizing ffmpeg's text-expansion parser (`%{expr:...}`, `%{pts}`, etc., active by default) so a crafted `text` param can no longer alter the rendered filter (#17). Also fixes a pre-existing single-quote escaping bug found during review: ffmpeg's quoted values have no backslash-escape mechanism of their own, so the previous `\'` still ended the quoted string early — a `text` value like `x';scale=` spliced an attacker-chosen filter name into the graph (confirmed against real ffmpeg 7.0.1). Quotes are now escaped via close-quote/escaped-quote/reopen-quote, ffmpeg's own documented mechanism. - **Security** `.gitignore` now matches `credentials*` (previously only the literal `credentials.json`) plus `*.credentials`, `*_rsa`, `*.p12`, `*.pfx`, and `*.keystore` (#18). - **Security** ffmpeg's DEBUG command log now redacts the query string of any `http(s)` argument before logging, so a chained step's presigned object-storage URL (a bearer credential until its signature expires) no longer leaks into `genblaze.ffmpeg` DEBUG logs. Execution still uses the untouched command (#75). Also redacts the same signature from a second leak path found during review: ffmpeg's own stderr can echo the input URL verbatim on a fetch failure, and that stderr became the `ProviderError` message logged on step failure. - **Security** `pattern_safety.assert_safe()`'s `google-re2` check was silently inert — it imported `google.re2`, but the current `google-re2` distribution ships a top-level `re2` module, so the authoritative linear-time check never activated regardless of installation. Fixed the import, added a `re2` optional extra (also included in `dev`, so CI now runs the authoritative check), closed three heuristic bypasses for environments without `re2` (the `{n,}` brace-quantifier form of `+`, unbounded quantifiers nested inside a sub-group like `([a-z]+(?:x)?)+`, and 2+ adjacent parenthesized groups each carrying an unbounded quantifier like `(a+)(a+)` — confirmed exponential, ~10s to match a 100-character adversarial string under stdlib `re`), and added the performance gate (`tests/perf/test_registry_perf.py`) the module docstring already claimed existed (#80). - **Security** `canonical_hash`/`canonical_json` no longer crash with an uncaught `RecursionError` on pathologically deep `step.params` nesting (free-form, caller-supplied data hashed into every manifest and cache key). `normalize()` now raises a typed, catchable `ManifestError` past a 100-level depth cap (#81). - **Added** unit tests for the previously-untested canonical-hash normalization branches (#50): NaN/Inf floats, `Enum.value` extraction, a naive datetime raising `TypeError`, and aware-datetime timezone canonicalization (`+00:00` → `Z`, non-UTC offsets preserved and stable). `genblaze_core/canonical/_normalize.py` coverage rises from 71% to 89%. - **Fixed** `parse_manifest()` leaked a raw `AttributeError: 'list' object has no attribute 'get'` when given valid JSON whose top-level value isn't an object (e.g. a JSON array) (#64). Both the `index` and `replay` CLI commands call `parse_manifest()`, so both leaked the same internal error; it now raises a `ManifestError` naming the actual type, giving both commands the same clean error message. - **Fixed** `Asset` accepted physically impossible provenance metadata — negative `size_bytes`, negative/zero `width`/`height`, negative or non-finite `duration`, malformed `media_type` — which then became canonical hashed data (#78). Construction now rejects these via Pydantic field constraints, plus the equivalent fields on `VideoMetadata`, `AudioMetadata`, and `WordTiming` (`end >= start`, `confidence` in `[0, 1]`). `sha256` stays format-tolerant at construction by design (see #100, which already makes a malformed hash fail `Manifest.verify()`) so `parse_manifest()` keeps loading older/foreign manifests without crashing; `Asset.set_hash()` is unaffected. - **Fixed** `ParquetSink` double-counted a `run_id` when its content changed between sinks — e.g. a resume completing more steps (#72). The idempotency sentinel's partition path is content-derived, so a same-partition check alone missed a sentinel written earlier under a different partition, letting duplicate `runs`/`steps`/`assets` rows accumulate. `write_run()` now falls back to probing every partition only when the fast path misses, and removes a stale partition's files (`steps`/`assets` before `runs`, so an interrupted cleanup is safely retryable) before writing the fresh ones. - **Fixed** the numeric/`media_type` field constraints added for #78 (`width`/`height`/`bitrate`/`sample_rate`/`channels` `gt=0`, `duration`/`frame_rate` finite, `media_type` shape) were enforced on the manifest **load** path too, so an older or foreign-authored manifest carrying a previously-valid value (e.g. `width=0` as an "unknown dimensions" placeholder, or a non-MIME `media_type`) raised `ValidationError` from `parse_manifest()`, breaking `verify`/`index`/ `replay` on files that used to load fine (#149). These fields now follow the same tolerant-load pattern already established for `sha256`: constraints still reject impossible values on ordinary construction, but `parse_manifest()` validates with `context={"tolerant_load": True}` so a tolerant load never crashes. `Manifest.verify()` / `output_asset_ids_with_invalid_metadata()` is the new verification boundary that surfaces out-of-spec metadata on loaded data, mirroring `output_asset_ids_missing_sha256()`. - **Fixed** `ParquetSink.write_run()` globbed the entire `runs/` tree on every write for a brand-new `run_id`, not just the idempotent-rewrite case the #72 fix targeted — an O(number of partitions) cost paid on every single write over a long-lived dataset (#150). `write_run()` now maintains a persisted `run_id -> partition` index (one small file per `run_id`, so concurrent writers sinking different `run_id`s never race on a shared file) and only touches the file for that specific `run_id` instead of walking the tree. An existing `runs/` tree without an index yet is backfilled once, atomically (built in a temp directory and `os.replace`d into place so a crash mid-backfill can never leave a partially-populated index that a later `ParquetSink` mistakes for complete), at the first `ParquetSink` construction, not on every write. - **Fixed** the #72 idempotency fix still silently dropped a same-partition re-sink whose content changed — e.g. a resume that completes more steps without changing the modality/provider set, so the partition path (and therefore the fast-path check) is unchanged (#152). `write_run()` now compares the existing sentinel's `canonical_hash` against the new manifest's before short-circuiting; only a byte-for-byte repeat is a no-op, and a genuine content change rewrites the `runs`/`steps`/`assets` rows in place. The no-op path also refreshes the run's index entry, so a prior write that crashed between writing its sentinel and persisting its index entry self-heals on the next call for that `run_id` instead of only on a future rewrite. - **Fixed** `step_cache_key` no longer sorts `step.inputs` before hashing (#71). Providers that consume inputs positionally (multi-image edit/compose, multimodal chat) produce different output when input order changes, but the sorted key made a reordered request incorrectly reuse an earlier run's cached asset. The key now preserves input order, matching the order-preserving manifest canonical hash. Existing cache entries for multi-input steps whose inputs weren't already in sorted order miss once and recompute (one-time repopulation, no data loss). - **Fixed** `JpegHandler.extract()` and `WebpHandler.extract()` no longer buffer an unbounded amount of the source file into memory (#82). Both now read via the bounded `read_media_bytes()` helper (500 MB `MAX_FILE_BYTES` cap) already used by PNG/WAV, so a container over the cap raises `EmbeddingError` instead of being fully materialized in RAM. Since the CLI content-sniffs the handler from magic bytes rather than file extension, an oversized file with JPEG/WebP magic previously bypassed the cap that PNG/WAV/MP4 already enforced. - **Fixed** Windows `file://` URL handling across all call sites (#132). On Windows, `urlparse("file:///C:/...").path` returns `/C:/...`, and `Path("/C:/...").resolve()` produces a drive-relative path that always fails the `is_relative_to(temp_root)` allowlist check. All affected sites now use `urllib.request.url2pathname`, which strips the leading `/` before a drive letter so `Path.resolve()` gets a properly anchored path. On Unix `url2pathname` is an alias for `unquote` — no behaviour change. Fixed in: `storage/transfer.py` (`ObjectStorageSink`), `providers/_ffmpeg_utils.py` (compositor/transform), `providers/base.py` (`validate_chain_input_url`, which also replaces `startswith("/")` with cross-platform `Path.is_absolute()`). - **Fixed** 0.3.4 → 0.3.5: `MockProvider`, `MockVideoProvider`, and `MockAudioProvider` no longer require `pytest` at import. They moved to a new pytest-free `genblaze_core.mocks` module (still re-exported from `genblaze_core.testing` for backward compatibility), so `from genblaze_core import MockVideoProvider` works in a runtime-only install. - **Fixed** `Pipeline.step(..., params={...})` no longer nests the dict under a literal `"params"` key in `Step.params` (#133). The catch-all kwargs collector was itself named `params`, so a caller passing `params={"image": ..., "length": ...}` (the natural spelling, mirroring the `Step.params` field) got `step.params == {"params": {...}}` instead of the flattened dict, with no error or warning. `.step()` now accepts an explicit `params={}` dict alongside top-level kwargs; both populate `Step.params`, and a top-level kwarg wins on key collision. - **Fixed** `Step(...)` now rejects unrecognized constructor kwargs (`model_config = ConfigDict(extra="forbid")`) instead of silently discarding them — the same class of bug via direct model construction rather than `Pipeline.step()`. Provider-specific keys belong in `params={...}` (#133). - **Fixed** `examples/quickstart_local.py` printed `Verified: False` on a clean `pip install genblaze-core` (#125). The 0.3.4 hardening of `Manifest.verify()` (requires every output asset to declare a `sha256`) wasn't reflected in the example's synthetic output asset. The example now passes a `sha256` for its placeholder demo bytes, matching what a real provider/`ObjectStorageSink` would populate. - **Fixed** `Pipeline.ingest` sorted assets by the random `asset_id` before hashing, so identical fresh asset batches (new `Asset()` instances with new random ids, same content) could produce different `canonical_hash` values even though `asset_id` is excluded from the hash payload (#76). Ingest now sorts the finished steps by `asset_provenance_key` — the same content fields that feed the hash (`sha256`, `media_type`, `size_bytes`, dimensions, ...) — *after* `sink.put_asset` has populated each asset's hash, since sorting beforehand ties on the shared placeholder content every fresh batch starts with and falls back to caller input order. The determinism contract now holds for fresh asset batches and sink-populated hashes, not just permuted reuses of already-hashed objects. - **Security** `PromptTemplate` now rejects attribute and item traversal such as `{user.api_key}` and `{settings[voice]}`. Pass explicit top-level values instead, for example `{api_key}` or `{voice}`. Top-level format specs and conversions such as `{price:.2f}` and `{name!r}` remain supported (#88). ### genblaze-openai - **Fixed** same Windows `file://` drive-letter bug in `dalle.py` (`_resolve_local_file` — DALL-E image-edit local inputs) (#132). - **Fixed** `SoraProvider` image-to-video chaining (#126). `submit()` forwarded the routed image slot verbatim as `image=`, but `Videos.create()` has no such kwarg — the openai SDK 2.x start frame goes in `input_reference`, which must be an uploaded file, not a URL string. Chain inputs also arrive as local `file://` temp paths (sink upload happens later), so the URL was unusable as-is even with the right parameter name. `submit()` now materializes the routed image (local `file://` resolve, or SSRF-pinned `https://` download — reusing `DalleProvider`'s existing file-input helpers) into an open file handle before upload. Also stringifies `seconds` (`4`/`8`/`12`), which the SDK types as `Literal["4", "8", "12"]`, not `int`. - **Fixed** `SoraProvider.fetch_output()` download against openai SDK 2.x (#127). It called `videos.content()`, which the SDK renamed to `download_content()`; every completed generation failed at the download step with `'Videos' object has no attribute 'content'` — after the generation cost was already incurred. The returned binary response still supports `write_to_file()`, so the method rename is the only change needed. ### genblaze-google - **Fixed** `VeoProvider` broken in Vertex AI auth mode (`project`/`location`) (#136). `poll()`/`fetch_output()` passed the bare operation-name string returned by `submit()` straight to `client.operations.get()`, which reads `.name` off its argument and raised `AttributeError: 'str' object has no attribute 'name'` on every poll — now wrapped in `types.GenerateVideosOperation` first. `fetch_output()` also called `client.files.download()` unconditionally, which raises `ValueError` on Vertex (the Files API is Gemini-Developer-API-only); Vertex returns video bytes inline instead, so `fetch_output()` now saves those bytes to a local file (new `output_dir` constructor param, indexed per-video for `number_of_videos > 1`) and exposes a `file://` asset, matching the existing `ImagenProvider`/ `DecartVideoProvider` convention. The Gemini Developer API path is unchanged. ### genblaze-replicate - **Fixed** `ReplicateProvider.submit()` 404s for community models (#109). `predictions.create(model=)` only works for Replicate's *official* models — community slugs (e.g. `sczhou/codeformer`, `tencentarc/gfpgan`) 404 on that path. `submit()` now picks the endpoint per model: community models resolve to a published version hash and run via `predictions.create(version=)`, while official/versionless models (e.g. `black-forest-labs/flux-schnell`) keep running via the `model=` path. Resolution accepts an inline `owner/name:hash` pin, otherwise reads `client.models.get(slug).latest_version` and caches the result per-slug — hash or "official, no version" — seeded from `validate_model()`'s existing probe, so a normal `Pipeline.run()` costs no extra round-trip. ### genblaze-gmicloud - **Fixed** package now ships the `py.typed` marker its `Typing :: Typed` classifier already promised (#44). The marker file was never added, so downstream mypy/pyright silently treated every `genblaze_gmicloud` symbol as `Any` despite the package being fully annotated. It ships the same way as every other connector's marker — no `pyproject.toml` change needed, since hatchling already includes all files under the declared `[tool.hatch.build.targets.wheel] packages` directory. - **Changed** video `duration` now requires whole-second integer values from 1 to 60 seconds; fractional, zero/negative, and oversized inputs fail invalid-input validation instead of being silently truncated or forwarded (#90). ### genblaze-s3 - **Changed** widened the `aioboto3` pin in the `async` extra from `>=12,<13` to `>=12,<16`, so `genblaze-s3` installs alongside newer `aioboto3` releases (#128). ### genblaze-cli - **Fixed** `extract`, `verify`, `index`, and `replay` accepted a directory argument and failed downstream with a confusing error (e.g. `EmbeddingError: No sidecar file found at .genblaze.json` or `[Errno 21] Is a directory`) instead of a clear "expected a file" message (#64). All four file arguments now set `dir_okay=False`. - **Fixed** six mypy `str | None` errors in `replay.py` (#43), all stemming from `step.provider` not being narrowed after a provider-less step (only valid for `INGEST`/`IMPORT`, per `Step`'s own validator): `sorted()`, the provider-confirmation list, two `dict[str, ...]` key sites, and `_load_provider`'s/`Pipeline.step`'s argument types. A manifest with such a step could reach some of these with an actual `None` and raise a confusing `TypeError` instead of a clean CLI error. The provider-confirmation prompt now skips provider-less steps (they invoke no provider), and the execution loop raises a clear `ClickException` naming the offending step instead of reaching those call sites with `None`. `mypy cli/genblaze_cli/ --ignore-missing-imports` is now clean. - **Fixed** 0.3.2 → 0.3.3: `extract` now supports the `-o/--output` option to write the manifest JSON to a file, matching the documented usage. - **Changed** `--version` now reports `genblaze-cli` rather than `genblaze`, so the CLI's version is no longer mistaken for the umbrella package version. - **Fixed** `verify` reported `OK` (exit 0) for a manifest whose asset metadata is out of spec (e.g. a `width=0` value a tolerant load accepts) even though `Manifest.verify()` rejects it — the command checked only `hash_ok` and sha256, never the new `invalid_metadata_ids` boundary (#149). `verify` now fails such a manifest with a clear message, and `extract --format summary` gains an `Output metadata:` line so a `Verified: False` verdict always shows its reason. ### genblaze (umbrella) - **Changed** 0.4.1 → 0.4.3: raises its `genblaze-core` floor to 0.3.6 and `genblaze-s3` floor to 0.3.5 (and the gmicloud/google/openai/replicate extra floors to their new versions) so `pip install genblaze` resolves this wave's fixes. ### Internal - `tools/prepare_release.py` + `.claude/skills/prepare-release/SKILL.md`: a deterministic wave-level release-prep engine and the `/prepare-release` skill that drives it. Discovers every package dynamically (no hardcoded version list), bumps versions for what changed since the last tag, and resyncs `cli`'s and the umbrella's `genblaze-core`/`genblaze-s3`/connector floors to match — independent of whether those packages changed — so a pin-only change can't ship without a version bump (the drift class `tools/check_pin_parity.py` guards against). Refuses to bump core onto the reserved `raise_on_failure` default-flip version. The skill cannot tag or publish; it stops at a release PR. Repo tooling only — no packaged code changed. ### genblaze-core - **Added (experimental)** Trust Mode 2 authenticated integrity: an optional `genblaze_core.signing` module (`pip install "genblaze-core[signing]"`) with an `Ed25519Signer` that signs a manifest's existing `canonical_hash` and a `verify_signature_bundle()` verifier. API surface may still change before this is declared stable. Thanks to @Demiladepy for the original contribution. ## [0.4.0] - 2026-06-25 Security hardening (SSRF, URL-only asset verification), two new providers (Hume Octave TTS, AssemblyAI STT), and a batch of connector patch-republishes to update the `genblaze-core>=0.3.4` floor. This heading is the release **wave** name and the git tag (`v0.4.0`); individual PyPI package versions move independently and are listed below (the umbrella `genblaze` package is `0.4.1`, after the patch republish noted below). Wave tags and the umbrella's PyPI versions are separate sequences that happen to look alike — don't pin `genblaze==0.4.0`. That version *is* real and installs without error, which is the dangerous case: it's this wave's original wheel, published before the `0.4.1` patch republish fixed the connector floors, so it silently ships with the older, unfixed pins. Pin `genblaze==0.4.1` (this wave's actual umbrella), or a lockfile for full reproducibility (the umbrella pins ranges, not exact versions, for its own dependencies). ### Released package versions - `genblaze` (umbrella) 0.4.0 → **0.4.1** — patch republish to refresh extras and connector floors. PyPI cannot overwrite an existing version; all changed pins ship only when a new wheel is published. - `genblaze-core` 0.3.2 → **0.3.4** (URL-only asset verification hardening, SSRF DNS pinning + redirect guard, fan-in failure propagation, sink lifecycle fix, async preflight offload, error sanitiser, tenant-scoped step cache) - `genblaze-s3` 0.3.2 → **0.3.4** (bumps `genblaze-core` floor to 0.3.4; region-probe close on `S3StorageBackend`, backend `close()` shuts boto3 client to release thread pool connections on `run()` teardown) - `genblaze-cli` 0.3.0 → **0.3.2** (bumps `genblaze-core` floor to 0.3.4; exposes `verify_hash()` and output-asset sha256 diagnostics) - `genblaze-replicate` 0.3.0 → **0.3.2** (bumps `genblaze-core` floor to 0.3.4) - `genblaze-gmicloud` 0.3.1 → **0.3.2** (bumps `genblaze-core` floor to 0.3.4) - `genblaze-hume` — **new at 0.3.1** (Hume Octave TTS provider) - `genblaze-assemblyai` — **new at 0.3.0** (AssemblyAI speech-to-text provider) - `genblaze-openai` 0.3.0 → **0.3.1** (DALL-E URL outputs materialised locally; pinned-DNS download; bumps `genblaze-core` floor to 0.3.4) - `genblaze-google` 0.3.0 → **0.3.1** (bumps `genblaze-core` floor to 0.3.4) - `genblaze-decart` 0.3.0 → **0.3.1** (bumps `genblaze-core` floor to 0.3.4) - `genblaze-elevenlabs` 0.3.0 → **0.3.1** (bumps `genblaze-core` floor to 0.3.4) - `genblaze-langsmith` 0.3.0 → **0.3.1** (bumps `genblaze-core` floor to 0.3.4) - `genblaze-lmnt` 0.3.0 → **0.3.1** (bumps `genblaze-core` floor to 0.3.4) - `genblaze-luma` 0.3.0 → **0.3.1** (bumps `genblaze-core` floor to 0.3.4) - `genblaze-nvidia` 0.3.0 → **0.3.1** (bumps `genblaze-core` floor to 0.3.4) - `genblaze-runway` 0.3.0 → **0.3.1** (bumps `genblaze-core` floor to 0.3.4) - `genblaze-stability-audio` 0.3.0 → **0.3.1** (bumps `genblaze-core` floor to 0.3.4) ### Security - `genblaze-core` 0.3.2 → 0.3.4: `Manifest.verify()` now rejects output assets that lack `sha256` for every supported schema version, preventing a schema downgrade from bypassing declared output sha256 coverage. This is an intentional security exception to the normal patch-release compatibility policy; use `verify_hash()` for legacy hash-only checks against historical URL-only media (#77). - `genblaze-core`: `Asset.sha256` values remain loadable even when malformed, so historical and cross-producer manifests can still be inspected with `verify=False` or `allow_unverified_assets=True`. `Manifest.verify()` and `genblaze verify` treat missing, uppercase, or otherwise malformed `sha256` as unverified output coverage. They do not fetch remote asset URLs; consumers must independently hash fetched bytes before trusting those bytes (#77). - `genblaze-core`: schema 1.6 URL-only hash markers are Python read-supported. The canonical marker URL strips known credential, expiry, and response override query parameters for AWS SigV4/SigV2, GCS, B2, CloudFront, Azure SAS, and GCS V2 signed URLs (including bare `authorization` tokens) while retaining resource-identifying query parameters. Default manifest emission, storage writes, media embedding, and the published JSON Schema/TypeScript spec stay on schema 1.5 for an expand-contract rollout (#77). - `genblaze-openai`: `dall-e-2` / `dall-e-3` URL responses are now downloaded immediately to a local `file://` asset with a populated `sha256` and `size_bytes`, instead of being stored as the raw (credential-bearing, ~1-hour) Azure SAS URL. The signed URL is used only for that fetch and never reaches the manifest, step cache, or any sink. This fixes the `.run(sink=...)` transfer path (which previously received a credential-stripped, unfetchable URL) and lets DALL-E outputs verify without a storage sink (#77). - `genblaze-cli` 0.3.0 → 0.3.2: raises its `genblaze-core` floor to the first core version that exposes `verify_hash()` and output-asset sha256 diagnostics (#77). - `genblaze` umbrella package: raises its `genblaze-core` floor to 0.3.4 so umbrella installs receive the verification hardening (#77). - Provider and storage connector packages now require `genblaze-core>=0.3.4,<0.4` and carry patch version bumps so adapter-only installs receive republished wheels with the URL-only asset verification fix (#77). - `ObjectStorageSink.read_manifest_for_asset()` now requires `tenant_id`, stores tenant-scoped asset index entries, validates `asset_id` as a UUID, falls back to legacy flat index entries during migration, rejects manifest pointer substitution unless the recovered manifest references the requested asset, and applies the same staged verification behavior as `read_manifest()` (#77). - `ObjectStorageSink.write_run()` now fails the write when an asset transfer fails, instead of uploading a success-path manifest that later fails strict verification. Successful transfers from a partial failure are reused on retry and the in-memory manifest hash is recomputed before raising (#77). ### Changed - `ObjectStorageSink.read_manifest(verify=True)` verifies canonical hashes by default and logs output assets whose `sha256` is missing or malformed. Hard-failing those unverified outputs is staged behind `strict_manifest_reads=True` or `GENBLAZE_STRICT_MANIFEST_READS=true` so operators can backfill historical URL-only manifests before enabling strict read failures on hot paths (#77). - `genblaze-core` `[audio]` extra: mutagen capped at `<1.49`. Mutagen 1.48 changed m4a timescale handling in a way that broke the AAC handler test fixture; the cap prevents a future mutagen release from reintroducing the same break before it can be vetted (#108). ### Fixed - `tools/check_pin_parity.py`: extend the pre-publish drift guard to compare `[project.optional-dependencies]` as well as base `[project.dependencies]`. Previously the gate reported `0 drift` for the `genblaze` umbrella even when connector pins in the `all`, `video`, `image`, or `audio` extras diverged from the published wheel, because `Requires-Dist` extras entries were silently filtered out. The gate now groups PyPI extras by name and diffs each extra independently, naming the extra in the drift report (e.g. `[all]`). Adds `tools/tests/test_check_pin_parity.py` and wires `pytest tools/tests/` into `make test`. Updates RELEASING.md to accurately describe the gate's scope (#23). ### Added - `genblaze-hume`: new provider adapter for Hume AI **Octave TTS** (audio / text-to-speech). Synchronous `SyncProvider` that decodes the API's base64 audio to a local `file://` asset; `step.model` (`octave-1` / `octave-2`) maps to the Octave `version` field. Ships a pattern-keyed `octave-*` model family with a permissive fallback, `DiscoverySupport.NONE`, and no hardcoded pricing (register per-character rates via the recipe in `docs/reference/pricing-recipes.md`). Available as `pip install genblaze-hume` or the `genblaze[hume]` / `genblaze[audio]` extras. - `genblaze-assemblyai`: new provider adapter for **AssemblyAI** speech-to-text / transcription — the first connector that *consumes* audio and *produces* text. Async `BaseProvider` (submit / poll / fetch_output) that resolves an audio URL (`step.inputs[0]` → `params["audio_url"]` → `prompt`, SSRF-validated via `validate_chain_input_url`) and emits a hash-verified **TEXT asset** (`text:{sha256}`, `media_type="text/plain"`, transcript in `metadata["text"]`) with word-level timings on `AudioMetadata.word_timings` (converted ms → s). `step.model` is sent on the SDK's plural `speech_models` field (the live API has deprecated the singular `speech_model` field and the legacy best/nano aliases). Ships a pattern-keyed `assemblyai-speech` family (`universal-3-pro` / `universal-2`) with a permissive TEXT fallback, `DiscoverySupport.NONE`, and no hardcoded pricing (register per-minute-of-input-audio rates via the recipe in `docs/reference/pricing-recipes.md`). Available as `pip install genblaze-assemblyai` or the `genblaze[assemblyai]` extra. ### Security - `genblaze-core`: DNS pinning closes the rebinding / TOCTOU window on all outbound HTTP paths. `resolve_ssrf` resolves the hostname once, validates every returned IP, and returns the pinned address. Callers connect to that IP directly — the HTTP client never performs a second independent resolution. TLS SNI and cert verification continue to use the original hostname. Affects `storage/transfer.py` (urllib3 `HTTPSConnectionPool` per hop), and `webhooks/notifier.py` and `genblaze_openai/dalle.py` (direct `http.client.HTTPSConnection` with pre-connected pinned socket) (#9). - `genblaze-core`: SSRF guard now validates every HTTP redirect hop in the asset transfer path (`storage/transfer.py`). Previously, `check_ssrf` ran only on the initial URL; a CDN redirect to a private/loopback/IMDS address bypassed the guard entirely. Redirects are now followed manually with a bounded loop (max 5 hops); each `Location` is re-resolved and re-pinned before following, and downgrade to non-HTTPS is rejected (#9). - `genblaze-core`: Webhook delivery (`webhooks/notifier.py`) switched from `urllib.request` to `http.client.HTTPSConnection` directly. `http.client` has no redirect handler, so a 3xx response is treated as a delivery failure rather than following the `Location` header, preventing a server-side redirect to an internal host from bypassing the SSRF guard (#9). - `genblaze-openai`: `_download_https_to_temp` now uses `http.client` directly with a pinned DNS connection, closing both the redirect-bypass and DNS rebinding vectors for edit-input downloads (#9). - `genblaze-core`: IPv4-mapped IPv6 addresses (`::ffff:169.254.x.x`, `::ffff:10.x.x.x`, etc.) are now normalized to their IPv4 form before the SSRF blocklist check. Previously, a DNS response returning an IPv4-mapped address bypassed all IPv4 `BLOCKED_NETWORKS` entries (#9). - `genblaze-core`: HTTP `Location` headers in redirect chains are now resolved with `urljoin` before re-validation, so RFC-legal relative redirects work and relative redirects to private targets are still rejected (#9). - **Egress proxy note:** DNS pinning requires a direct TCP connection to the validated IP; `HTTP_PROXY` / `HTTPS_PROXY` / `NO_PROXY` env vars are ignored by design on all pinned outbound paths (transfer, webhook, dalle). Deployments that require an egress proxy should allowlist the target hosts at the proxy instead of relying on env-var forwarding. ### Fixed - `genblaze-core`: `Pipeline.arun()` no longer blocks the event loop during model preflight. The network-bound phase (`_validate_models`, which uses a `ThreadPoolExecutor` for provider discovery fetches) is now offloaded via `asyncio.to_thread`, allowing concurrent coroutines to keep running during the preflight window. Cheap capability checks (modality, chain-input) still run synchronously. `Pipeline.run()` behavior is unchanged (#56). - `genblaze-core`: `Pipeline.run()`/`arun()` now release a sink's run-scoped resources in their `finally` block (and on early preflight/validation failure), shutting down the `ObjectStorageSink` eager-upload `ThreadPoolExecutor` and releasing the backend connection pool — `S3StorageBackend.close()` now closes the boto3 client. Previously non-daemon worker threads stayed alive after `run()` returned and in-flight eager-upload futures leaked on error paths that bypassed `write_run`. Sinks declare lifecycle ownership via `BaseSink._close_with_run` (default `True`): run-scoped sinks (`ObjectStorageSink`) are closed by the pipeline and are single-use, while fire-and-forget `WebhookSink` opts out (`False`) so it is never closed — keeping webhook delivery non-blocking and the sink reusable across runs. `batch_run()`/`abatch_run()` close their shared sink once after the whole batch rather than after the first item. `BaseSink` gains `__enter__`/`__exit__` for callers that manage the lifecycle outside a `run()`. **Behavior change:** a run-scoped sink passed to `run()`/`arun()` is closed afterward — construct a fresh one per run rather than reusing it (#57). - **`genblaze-openai`**, **`genblaze-google`**: remove hardcoded USD per-token rate tables (`_RATES`) from the standalone `chat()` helpers and return `cost_usd=None` unconditionally, consistent with the 0.3.0 contract that connector modules ship zero static rate tables (Pipeline-Step providers register rates via `PricingContext`/`ModelSpec`). The standalone `chat()` helpers have no model registry, so callers that relied on `ChatResponse.cost_usd` being non-`None` for known models must now compute cost from `tokens_in`/`tokens_out` with their own rates (see `docs/reference/pricing-recipes.md`). Adds `test_pricing_phaseout.py` to `genblaze-core` — a lint-style CI guard that fails if any future connector reintroduces a `_RATES`/`_PRICING` constant (#13). - `genblaze-core`: post-submit step-level retries now resume the existing upstream prediction instead of submitting a new one, including transient checkpoint failures after `submit()` returns by replaying idempotent `on_submit(step_id, prediction_id)` callbacks before retry resume (#70). - `genblaze-core`: fan-in consumers using `input_from` now fail before provider invocation when a referenced producer step failed, produced no assets, or points at an out-of-range prior step, preventing a downstream step from reporting success with empty declared inputs (#69). Affected pipelines that previously appeared green can now report `FAILED`/`INVALID_INPUT`, which may increase status-based alerts and change manifest hashes once during rollout. Route or re-baseline those alerts using `metadata.failure_reason="input_resolution"`; these pre-failed steps also carry `metadata.provider_invoked=false` for telemetry filtering. - `genblaze-core`: failed `Step.error` values now use the shared sanitizer for OpenAI/Anthropic/Google/Replicate keys, AWS access key IDs and secret access keys, Backblaze B2 application keys, JWTs, bearer/token headers, API-key assignments, and basic-auth URL credentials, then truncate to 500 characters before manifest, log, or stream-event emission. This redaction hardening ships with #69 because fan-in pre-fail telemetry can otherwise re-surface untrusted upstream provider errors on dependent steps. - `genblaze-core`: `StepCache` now partitions the step cache key by `tenant_id` when a tenant is set (via `Pipeline(tenant_id=...)`, or passed to `StepCache.get`/`put`), so a cache shared across tenants no longer serves one tenant's cached output asset to another for an otherwise-identical step. Single-tenant keys are unchanged. A `tenant_id` in `RunnableConfig` is now rejected at runtime (it was never honored), so a dynamic caller passing it via `config()` / `invoke(config=...)` fails loudly instead of silently bypassing cache isolation (#68). - **`genblaze-replicate`** (0.3.0 → **0.3.1**): declare `httpx>=0.24` as a direct dependency. `ReplicateProvider._get_client()` imports `httpx` directly to build the client timeout, but the package declared only `replicate`, which pins `httpx>=0.21` — below the `0.22` floor where the `httpx.Timeout(connect=...)` kwarg landed. Pin aligned with the `httpx>=0.24` already used by the nvidia, gmicloud, and stability-audio connectors. Version bumped so a corrected wheel can publish past the `skip-existing` pin-parity gate (#37). - **`genblaze-core`** (0.3.2 → **0.3.3**): declare `urllib3>=1.26,<3` as a direct dependency. `storage/transfer.py` imports `urllib3` unguarded at module load (the shared `PoolManager` behind `AssetTransfer`) and `storage/__init__` imports it eagerly, so `import genblaze_core.storage` hard-required urllib3 on a clean install while core declared only `pydantic` + `pillow`. It previously arrived only transitively via a connector's boto3 stack — the same clean-install crash class as the replicate/httpx fix above. - **`genblaze-s3`** (0.3.2 → **0.3.3**): declare `botocore>=1.31` (imported directly in `backend.py`) and `aiobotocore>=2.7` in the `async` extra (imported directly in `async_backend.py`). Both always shipped transitively via boto3/aioboto3, which pin their exact versions — so these declarations add honesty for the dependency gate without changing what resolves. ### Added - `genblaze-core`: `http` (`httpx`), `otel` (`opentelemetry-api`), and `testing` (`pytest`) extras. These advertise previously-undeclared guarded/soft imports. In particular `genblaze_core.testing` (the public `MockProvider` / `ProviderComplianceTests` harness) imports `pytest` at module load and ships in the wheel, so it now installs via `pip install "genblaze-core[testing]"`. ### Changed - **Repo tooling**: new `make deptry` dependency-hygiene gate (backed by per-package `[tool.deptry]` config) fails on undeclared imports (DEP001), shipped imports of dev-only deps (DEP004), and misclassified transitive deps — the clean-install crash class above. Wired into `make lint`, `make pre-release`, and a new `deptry` CI job that also runs `pip check` against the editable workspace. `libs/meta` is excluded (umbrella metapackage: its deps are install-time bundles, not imports). ## [0.3.3] - 2026-05-26 Release-pipeline hardening + refresh of two PyPI wheels with stale `genblaze-core` pins. Closes the class of drift bug where `skip-existing` silently no-ops a package whose source pin was widened but whose `version` field was never bumped. This trap shipped twice before: `genblaze-s3` in the 0.3.0 wave (fixed in 0.3.1), and now `genblaze-langsmith` + `genblaze-cli` discovered during 0.3.2 post-publish verification. ### Released package versions - `genblaze-langsmith` 0.2.1 → **0.3.0** — patch republish to refresh the PyPI wheel. The 0.2.1 wheel pinned `genblaze-core<0.3`, so `pip install "genblaze[all]"` could not resolve any release in the 0.3.x line. Source had the corrected `<0.4` pin since the 0.3.0 prep window but `skip-existing` skipped publish on every wave. - `genblaze-cli` 0.2.0 → **0.3.0** — patch republish for the same reason. The 0.2.0 wheel on PyPI pinned `genblaze-core<0.3`; source had `<0.4`. Surfaced by the new pin-parity gate, not by any install-resolution failure (cli is not in `genblaze[all]`). - All other packages — unchanged. ### Changed - **Release pipeline**: new pre-publish parity gate (`make pypi-pin-parity`, backed by `tools/check_pin_parity.py`) fails the release if any package's `[project.dependencies]` diverges from the same-version wheel already on PyPI. Bundled into `make pre-release` and added as a `pin-parity` job in `.github/workflows/release.yml` that gates every publish job. The trap class is now closed — a future wave cannot ship a divergent wheel without an explicit version bump. - **Release pipeline**: `install-verify` now resolves `genblaze[all]==` instead of bare `genblaze==`. The workflow's own post-publish gate now exercises every connector's pin against public PyPI, matching what `make post-release` does from a maintainer's laptop and what real users see on `pip install "genblaze[all]"`. ## [0.3.2] - 2026-05-26 Storage ergonomics & GMI catalog hygiene. Closes the 2026-05-23 user feedback batch (7 items). Additive-only — no existing import paths, kwargs, or behaviors break. See [MIGRATING-0.3.2.md](MIGRATING-0.3.2.md) for the migration guide. ### Released package versions - `genblaze-core` 0.3.0 → **0.3.2** (canonical_slug field on `ModelFamily`; `URLPolicy` / `URLPolicyError` relocated from `genblaze_s3.url_policy`; `ObjectStorageSink(asset_url_policy=...)` kwarg) - `genblaze-s3` 0.3.1 → **0.3.2** (`presigned_get_url` / `presigned_put_url` companions; `for_backblaze()` 403-region probe) - `genblaze-gmicloud` 0.3.0 → **0.3.1** (canonical_slug applied to audio / Veo / new Kling V2.1 families per GMI's 2026 catalog) - All other packages — unchanged. ### Added - `genblaze-core`: `ObjectStorageSink(asset_url_policy=URLPolicy.AUTO | PUBLIC | PRESIGNED)` kwarg selects what flavor of URL gets written into `asset.url` on transfer. Default `AUTO` preserves today's durable-URL behavior; `PUBLIC` enforces that `backend.public_url_base` is configured (raises `URLPolicyError` at construction if not); `PRESIGNED` is **rejected at sink construction** — manifests must not carry SigV4 URLs (they decay before the manifest does, breaking provenance). For read-time presigned URLs use `backend.presigned_get_url(key)` directly. `AUTO` + missing `public_url_base` emits a one-time WARN per process per `(bucket, policy)` tuple pointing the caller at `public_url_base` or `presigned_get_url`. Backend-agnostic: backends that don't expose `public_url_base` skip the WARN cleanly. Addresses the 2026-05-23 feedback batch item 5. - `genblaze-s3`: `S3StorageBackend.presigned_get_url(key, ...)` and `presigned_put_url(key, ...)` — raw-`str` companions to `presigned_get` / `presigned_put`. Use at the boundary where the URL leaves the process (HTTP client call, API response). Equivalent to `backend.presigned_get(key).url` but discoverable from autocomplete alongside the wrapped form. Addresses the 2026-05-23 feedback batch item 1 — naïve `requests.get(backend.presigned_get("k"))` previously hit the redacted `__str__` and 403'd on a SigV4 mismatch. The wrapped `presigned_get` / `presigned_put` remain the safe default; the `_url` companions are the explicit raw-string escape hatch. ### Changed - `genblaze-core`: new `ModelFamily.canonical_slug: Callable[[str], str] | None` field. When set, ``family.resolve(input)`` substitutes ``canonical_slug(input)`` into the spec's ``model_id`` — the wire form the upstream HTTP client receives. ``ModelRegistry.validate()`` normalizes via the same callable before the discovery-cache check, so validation and submit agree on slug identity. ``ModelRegistry.known()`` returns canonical forms for family-matched ``example_slugs``. When the rewrite actually changes the input, a one-time INFO per ``(family, input)`` fires so callers know to migrate their call sites (instance-level dedup, fork-safe via ``fork()``). - `genblaze-gmicloud` 0.3.0 → **0.3.1**: applied the new `canonical_slug` mechanism to bridge GMICloud's per-slug casing. - **Audio families** (TTS / Voice-Clone / Music) — patterns now case-insensitive; `canonical_slug=str.lower` rewrites to GMI's published lowercase wire form (per the 2026-03-10 "Most Popular AI Models" blog: `elevenlabs-tts-v3`, `minimax-tts-speech-2.6-turbo`, `inworld-tts-1.5-mini`, `minimax-music-2.5`, `minimax-audio-voice-clone-speech-2.6-hd`). Pre-0.3.1 PascalCase callers continue to work; their input gets rewritten on the wire with a one-time INFO nudge. - **Veo family** — pattern made case-insensitive; `canonical_slug` rewrites `veo3` → `Veo3` (PascalCase per every GMI blog from 2025-12-08 onward). - **New Kling V2.1 family** — covers `Kling-Text2Video-V2.1-Master` and `Kling-Image2Video-V2.1-Master` (PascalCase wire form per the 2026-04-14 "Real-Time Video Generation Platforms" blog at $0.28/req). Accepts pre-0.3.1 lowercase callers via `canonical_slug`. Previously these slugs fell through to the permissive fallback and lost their family-specific param surface. - Cleaned up `_UNSTABLE_SLUGS` — pre-0.3.1 it carried lowercase variants of slugs whose canonical wire form is PascalCase (`kling-text2video-v2.1-master`, `minimax-hailuo-2.3-fast`). With the canonical-slug rewrite, those lowercase forms now resolve to the right wire ids and aren't unstable. Only `vidu-q1` remains (replaced by `vidu-q3-pro-i2v` per the 2026-03-04 GMI blog). - `models/voices.py`, audio README quickstart, audio example, and audio family `example_slugs` all updated to the lowercase canonical forms. - `genblaze-core` 0.3.0 → **0.3.2**: relocated `URLPolicy` / `URLPolicyError` from `genblaze_s3.url_policy` to `genblaze_core.storage.url_policy`. Required so `ObjectStorageSink` (which lives in core) can reference the enum without inverting the `genblaze-s3 → genblaze-core` dependency direction. Back-compat preserved: `from genblaze_s3.url_policy import URLPolicy` still resolves (the s3 module is now a thin re-export). New convenience: `from genblaze_core import URLPolicy`. `genblaze-s3`'s and `genblaze-gmicloud`'s minimum `genblaze-core` pin tightens to `>=0.3.2,<0.4`. ### Improved - `genblaze-s3`: `S3StorageBackend.for_backblaze()` preflight now surfaces a specific region in the error message when the bucket lives in a B2 region that returns 403 instead of 301 (e.g. `us-east-005`). On a 403 from a B2 endpoint, the preflight probes the other published B2 regions in parallel and classifies: - Exactly one other region returns 200 → *"Bucket 'X' lives in `us-east-005` — pass `region='us-east-005'` to `for_backblaze()`."* - Every probed region returns 404 → *"Bucket 'X' does not exist in any known B2 region. Verify the bucket name."* - Mixed signals → today's generic message, now with the endpoint URL we tried. Probe clients share credentials with the primary client, carry a 3-second connect+read timeout with retries disabled, and are explicitly closed via `contextlib.closing` so the error path doesn't leak sockets in long-running daemons. Probe path is gated on B2 endpoints (`backblazeb2.com`) — AWS S3 and R2 backends are unaffected. Addresses the 2026-05-23 feedback batch item 6. ### Fixed - **GMICloud README + examples: corrected Kling slug casing + Veo casing.** Verified against GMICloud's own blog posts (no third-party sources used). The newest authoritative source is the [2026-04-14 Real-Time Video Generation Platforms blog](https://www.gmicloud.ai/en/blog/real-time-video-generation-platforms); cross-checked against the [2026-03-10 Most Popular AI Models blog](https://www.gmicloud.ai/en/blog/the-most-popular-ai-models-available-today) and the [2026-03-04 Compare Generative Media blog](https://www.gmicloud.ai/en/blog/compare-generative-media-ai-platforms-video-and-image). Changes: - `kling-text2video-v1.6-pro` → `Kling-Text2Video-V2.1-Master` (the V1.6-Pro text2video slug doesn't appear in any GMICloud blog; the V2.1-Master form is explicit in the 2026-04-14 blog at $0.28/req). - `kling-image2video-v2.1-master` → `Kling-Image2Video-V2.1-Master` (PascalCase per 2026-03-04 blog; the V2.1 designation is confirmed by the 2026-04-14 blog's matching Kling-Text2Video-V2.1-Master casing). - `veo3` → `Veo3` in the connector README provider table (PascalCase per every GMICloud blog from 2025-12-08 onward). Touched: `libs/connectors/gmicloud/README.md` (provider table, video quickstart), root `README.md` (chain example), `examples/gmicloud_video_pipeline.py`. - **Rewrote the stale "Slug casing" note in `libs/connectors/gmicloud/README.md`.** The previous note claimed all GMI slugs are lowercase and that PascalCase ids rewrite via `ModelSpec.deprecated_aliases` — both false. Replaced with an accurate per-slug-casing description (GMICloud's published catalog uses lowercase for Sora/Pixverse/Seedance/Seedream/Reve/Wan/Bria/Gemini-Image and PascalCase for Kling V2.1 + Veo3, while newer Kling V2.5/V3 series use lowercase — verify on `console.gmicloud.ai` per slug). - **Repo dev install**: stripped the stub `[project]` block from the root `pyproject.toml`. The stub declared `name="genblaze" version="0.1.0"` with empty dependencies, which made `pip install -e .` from the repo root install an extras-less 0.1.0 placeholder that shadowed the real `genblaze` umbrella from `libs/meta`. Users following the README saw *"no matching distribution for genblaze-gmicloud"* on `pip install -e ".[gmicloud]"` even though the umbrella on PyPI works correctly. After the strip, `pip install -e .` from the root fails cleanly (hatchling: *"Missing 'project' metadata table"*); use `make install-dev` for editable monorepo dev installs. Addresses the 2026-05-23 feedback batch item 4. ### Internal - **Pre-release catalog verification checklist**: new [`docs/dev-workflows.md` §"Pre-release catalog verification"](docs/dev-workflows.md#pre-release-catalog-verification) with a per-provider table of upstream catalog/docs links. Maintainers click through before tagging a release to confirm that every slug in `family.example_slugs` and the README quickstarts still resolves on the provider's published catalog. Replaces an earlier scheduled CI probe design — provider catalogs rotate ~quarterly, so weekly polling was overkill and carried real audit-log + potential-paid-job cost. The existing `tools/probe_*.py` scripts remain as an optional programmatic sanity-check. ## [0.3.1] - 2026-05-18 ### Released package versions - `genblaze-s3` 0.3.0 → **0.3.1** — patch republish. The 0.3.0 wheel on PyPI was built before the `genblaze-core<0.4` widening and shipped with the stale `<0.3` constraint, which made `pip install "genblaze"` unresolvable after the 0.3.0 wave. Source already had the correct constraint; this bump forces PyPI to take the corrected wheel. - All other packages — unchanged. ## [0.3.0] - 2026-05-11 ### Released package versions - `genblaze-core` 0.2.8 → **0.3.0** (minor — breaking: `ModelRegistry(defaults=...)` removed) - `genblaze` (umbrella) 0.3.2 → **0.4.0** - `genblaze-decart` 0.2.2 → **0.3.0** - `genblaze-elevenlabs` 0.2.2 → **0.3.0** - `genblaze-gmicloud` 0.2.6 → **0.3.0** - `genblaze-google` 0.2.4 → **0.3.0** - `genblaze-lmnt` 0.2.2 → **0.3.0** - `genblaze-luma` 0.2.3 → **0.3.0** - `genblaze-nvidia` 0.2.1 → **0.3.0** - `genblaze-openai` 0.2.4 → **0.3.0** - `genblaze-replicate` 0.2.2 → **0.3.0** - `genblaze-runway` 0.2.3 → **0.3.0** - `genblaze-stability-audio` 0.2.2 → **0.3.0** - `genblaze-langsmith` — unchanged (no code edits this release) - `genblaze-s3` — unchanged - `genblaze-cli` — unchanged ### Hardening — production readiness (`model-registry-decoupling-hardening`) Closes the BLOCKER + HIGH-severity findings from the post-rollout deep review. See [`docs/exec-plans/active/model-registry-decoupling-hardening.md`](docs/exec-plans/active/model-registry-decoupling-hardening.md) for the design trail. **Security:** - `validate_chain_input_url` rewritten with multi-layer hardening: RFC 8089 netloc check; `urllib.parse.unquote()` before traversal detection (catches `..%2F` percent-encoded bypass); canonicalization via `Path.resolve()` (collapses `..`, follows symlinks, resolves macOS `/private/etc` aliases); denylist on the canonical path (`/proc`, `/dev`, `/sys`, `/etc`, `/private/etc`, `/private/var/run`, `/run/secrets`, `/var/run/secrets`); opt-in `file_root_allowlist` parameter for strict containment. Test corpus parametrized at 35 cases including symlink-resolves-outside-root, percent-encoded, and double-encoded variants. **Concurrency:** - `BaseProvider._cached_probe` cleanup moved into `try/finally` so a `BaseException` from `_invoke_family_probe` no longer orphans the in-flight `threading.Event` (would have permanently blocked all subsequent waiters for that slug). Pinned by 7 regression tests including a 20× repeat run via `pytest-repeat`. - `BaseProvider._poll_cache` reads/writes/cleanup now guarded by `threading.Lock`. Read-then-pop is atomic — concurrent `ainvoke()` callers (which dispatch via `asyncio.to_thread`) can no longer race. 5 regression tests including an 8-thread × 100-cycle stress run. - `FamilyProbe` contract docstring updated to require bounded duration via the underlying transport (`httpx.Client(timeout=...)` etc.) — the framework deliberately does not wrap probes in a separate `concurrent.futures` timeout (would add a thread layer without cancelling the in-flight HTTP request). **Registry correctness:** - `ModelRegistry.register_pricing(slug, strategy)` now falls through to `match_family()` when the slug isn't yet user-registered. The family-resolved spec (with all its param contracts) becomes the base; pricing layered on top. Previously, a bare `ModelSpec` with only `pricing` was minted, silently dropping the family's param aliases / schemas / allowlist / extras. - `ModelRegistry.register_family()` now enforces a separate `MAX_USER_FAMILIES = 32` cap on the user layer. The connector cap (`MAX_PROVIDER_FAMILIES = 32`) only counts provider families — a connector at its provider cap does not block users from registering their own families. Total scan cost stays under 64 patterns. - `ModelRegistry.fork()` now carries `_warned_deprecated` from parent to clone. Multi-tenant deployments that fork-per-request no longer spam deprecation warnings on every fork for an already-warned alias. **Connector correctness:** - `genblaze-google` Veo split into two families: `GOOGLE_VEO_LEGACY_FAMILY` (`^veo-2[.-]`, no audio) and `GOOGLE_VEO_FAMILY` (`^veo-` catch-all, `extras["has_audio"]=True`). `VeoProvider.fetch_output` reads `extras.get("has_audio")` instead of `step.model.startswith("veo-3")` — future `veo-N` slugs (N≥3) inherit synchronized-audio metadata automatically without a provider release. **API uniformity:** - LMNT, Replicate, and all NVIDIA providers (chat / audio / video / image) now accept `probe_cache_ttl` and `probe_cache_max_entries` ctor kwargs (no-ops on NATIVE / NONE providers; accepted for API uniformity). - New `ProviderComplianceTests.test_accepts_probe_cache_kwargs` conformance test calls each provider's constructor with the kwargs and asserts no `TypeError` — catches `**kwargs`-forwarders that an inspect-only check would miss. **Test coverage:** - New `tests/test_catalog_decoupling.py` for LMNT and Replicate (the two empty-registry NATIVE/NONE proof-points). **Documentation accuracy:** - Migration guide and `model-registry.md` outcome tables now match `validation.py` exactly: four `ValidationOutcome` values (`OK_AUTHORITATIVE`, `OK_PROVISIONAL`, `UNKNOWN_PERMISSIVE`, `NOT_FOUND`); five `ValidationSource` values (`USER`, `FAMILY`, `DISCOVERY`, `PROBE`, `FALLBACK`). Phantom `KNOWN_UNSTABLE` enum removed; the unstable-slug case correctly described as `OK_PROVISIONAL` with `detail="known_unstable; ..."`. - `Pipeline.preflight()` documented as a fluent setter (returns `Pipeline`); validation runs automatically inside `run()`. Use `provider.validate_model(slug)` for direct slug checks. - `discover_models()` for NONE providers documented as returning `DiscoveryResult.unsupported(...)` (not empty results). - `new-provider.md` `pyproject.toml` template constraint bumped to `>=0.3.0,<0.4`. - LMNT added to the migration guide's NONE-provider list (matches CHANGELOG and `provider-system.md`). - Deprecation horizon section added to migration guide: `BaseProvider.probe_model()` → removed in 0.4.0; `ModelRegistry(defaults=...)` already removed in 0.3.0. ### Added — model registry decoupling (PRs #1–#4) - **`genblaze-core 0.3.0` (in progress)**: introduces a new pattern-based model-catalog architecture. The SDK no longer ships hardcoded slug lists. Connectors declare: - `discovery_support: DiscoverySupport` — `NATIVE`, `PARTIAL`, or `NONE`. - `provider_families: tuple[ModelFamily, ...]` — pattern-keyed param-shape rules, replacing per-slug `defaults={...}` dicts. - Optional `family.probe` callable for `PARTIAL` providers (NIM generative endpoints use the empty-payload POST trick). See `docs/exec-plans/active/model-registry-decoupling.md` for the architecture, red-team trail, and rollout sequence. - New public types in `genblaze_core.providers`: `ModelFamily`, `FamilyMatch`, `FamilyProbe`, `LiveProbeResult`, `DiscoverySupport`, `DiscoveryResult`, `DiscoveryStatus`, `ValidationResult`, `ValidationOutcome`, `ValidationSource`, `MAX_PROVIDER_FAMILIES`, `DEFAULT_TTL_SECONDS`. - New public methods on `BaseProvider`: - `discover_models()` — snapshot the upstream catalog (NATIVE only). - `validate_model(slug, *, refresh=False)` — graded slug-validity outcome. - `_invoke_family_probe(probe, slug)` — connector-side hook for PARTIAL providers to wire their `httpx.Client` into the family probe. - New `Pipeline(preflight=True)` knob (default ON, soft-launch posture). `Pipeline.run()` validates every step's model in parallel via `ThreadPoolExecutor` before any wire calls. `NOT_FOUND` raises `ProviderError(MODEL_ERROR)`; `OK_PROVISIONAL` and `UNKNOWN_PERMISSIVE` emit one-time WARN logs. Opt out with `Pipeline(preflight=False)`. - New connector adoption (this release): - **lmnt** — `DiscoverySupport.NONE` proof-point. - **replicate** — `DiscoverySupport.NATIVE`. `discover_models()` returns the first page of `/v1/models`; `validate_model()` does per-slug `client.models.get()` lookups (cached per-process, 1-hour TTL). - **nvidia** — chat = `NATIVE` via `/v1/models`; audio/image/video = `PARTIAL` with the empty-payload `genai` probe. - **gmicloud** — audio/image/video = `PARTIAL` with the empty-payload `/requests` probe. The 2026-04 reconciliation's `suspected_dead` slugs (`veo3-fast`, `kling-text2video-v2.1-master`, `minimax-hailuo-2.3-fast`, `vidu-q1`, all 5 audio defaults) are preserved as `ModelFamily.unstable_examples` (RT-10) — the probe is the authoritative answer at runtime; users see a "known unstable" hint at preflight. PascalCase `deprecated_aliases` are removed (soft-launch clean break); pass canonical lowercase slugs. - **runway** — `DiscoverySupport.NONE` with a single `runway-gen-video` family covering `^gen\w+_turbo$` (Gen-3a, Gen-4, future variants). The runwayml SDK doesn't expose raw HTTP for the empty-payload probe pattern, so submit-time errors are the authoritative liveness signal. Pricing tables removed; recipe published at `docs/reference/pricing-recipes.md`. - **decart** — `DiscoverySupport.NONE` for both video and image providers. Two families: `decart-lucy-video` (pattern absorbs `^lucy-.*(?:2v|motion|restyle)`) and `decart-lucy-image` (`^lucy-.*2i$`). Cross-modality isolation guarded by tests so video slugs don't leak into the image registry and vice versa. Pricing removed; recipe published. - **elevenlabs** — TTS = `DiscoverySupport.NATIVE` via `client.models.get_all()` with a single `^eleven_` family pattern; SFX = `DiscoverySupport.NONE` (single-model surface, models endpoint may not enumerate SFX). TTS retired-slug verdicts surface as `NOT_FOUND` at preflight before any wire calls. Per-1K-char and duration-bucket pricing tables removed; recipes published. - **openai** — TTS, DALL-E (gpt-image + dall-e), and Sora all declare `DiscoverySupport.NATIVE` via `client.models.list()`. Each provider's fetcher filters the global OpenAI catalog by its family pattern, so chat / embeddings / Whisper slugs don't pollute cross-modality caches. Two image families (`openai-gpt-image` ^gpt-image-, `openai-dalle` ^dall-e-) keep the bespoke `_validate_params` logic in the provider while the families handle slug routing. Per-tier TTS pricing and per-(quality,size) image pricing tables removed. Sora pricing remains None — the correct formula is `(model, size, seconds)` per-second; a flat table misreports 10x+. Standalone chat module out of scope (not a Pipeline-Step provider). - **google** — Veo (`^veo-`) and Imagen (`^imagen-`) now ship as pattern-keyed families with `DiscoverySupport.PARTIAL`. Liveness is confirmed via `client.models.get(model=slug)` — the canonical "is this model available to my project?" lookup — so dead / unauthorized slugs surface as `NOT_FOUND` at preflight without enqueuing a generation. Per-second Veo and per-image Imagen pricing tables removed; recipes published. Standalone Gemini `chat()` retains its per-token table (out of scope — not a Pipeline-Step provider). - **luma** — single `^ray-` family covers `ray-2`, `ray-flash-2`, and future `ray-N` / `ray-*-N` variants. Reclassified from `PARTIAL` to `DiscoverySupport.NONE` during implementation: the lumaai SDK exposes no per-slug probe that doesn't enqueue a billable generation, and the upstream catalog is small and stable (mirrors the Runway / Decart precedent). Luma was already pricing-None — the recipe documents the canonical per-second-by-model strategy for users who want cost attribution. - **stability-audio** — single `^stable-audio-` family covers `stable-audio-2.5` and future `stable-audio-N` variants. `DiscoverySupport.NONE`: Stability ships no Python SDK or `/v1/models` endpoint for audio, the upstream catalog is effectively a single line, and submit-time errors plus the family pattern are sufficient. The hardcoded $0.01/sec rate table removed; recipe published preserving the duration-fallback behavior (probe → params). ### Removed — `ModelRegistry(defaults=...)` transitional shim - The ``defaults=`` constructor parameter is gone. PR #1 added it as a code-path migration shim alongside ``provider_families=`` so the per-connector PRs (#2-#12) could land incrementally without breaking the world. Every connector now uses ``provider_families=`` (or ``fallback=`` alone for empty-catalog providers like LMNT and Replicate). User code that previously built registries via ``ModelRegistry(defaults={"slug": spec})`` should migrate to the post-construction surface: ```python reg = ModelRegistry() reg.register(spec) # single spec reg.extend([s1, s2, ...]) # bulk ``` Resolution semantics are unchanged — the legacy ``_defaults`` dict and the user ``_user`` dict were always checked at the same precedence tier (``_user.get(slug) or _defaults.get(slug)``), so migrating to ``register()`` / ``extend()`` is a pure constructor- vs-method rephrasing. The dead-code branch in ``validate()`` that minted ``OK_AUTHORITATIVE`` with a ``"legacy defaults shim"`` detail string is also gone. - New conformance test (``libs/core/tests/unit/test_no_defaults_kwarg.py``) gates the connector tree against re-introducing the kwarg. - ``FamilyProbe`` switched from a kwarg-pinned ``Protocol`` to a ``Callable[..., LiveProbeResult]`` type alias. The old shape locked every probe's keyword argument to ``http: httpx.Client``, which excluded SDK-based probes (e.g. ``client.models.get`` for ``genblaze-google``). The new alias is honest about transport variability — connectors choose their probe's keyword shape; the contract is "first positional is slug, return is ``LiveProbeResult``." Fixes the typecheck-connectors CI failure introduced when PR #10 landed before the protocol was loosened. ### Fixed — F-2026-05-04-01 (NVIDIA `nvidia/riva-tts` 404) - The retired `nvidia/riva-tts` slug is no longer pinned in the SDK. Users who still pass it now get a deterministic `ProviderError(MODEL_ERROR)` at preflight (via the empty-payload probe) instead of a mid-pipeline 404. `nvidia/magpie-tts-multilingual` is the surfaced "Did you mean…?" hint. - New end-to-end repro test (`libs/connectors/nvidia/tests/test_catalog_decoupling.py::test_riva_tts_surfaces_at_preflight_not_mid_pipeline`) pins this regression class going forward. ### Changed — pricing phase-out - The SDK no longer ships hardcoded prices. `_PRICE_PER_CHAR`, `_COST_PER_SEC`, `_compute_time_cost`, and the per-slug pricing on connector fallback specs have been removed. `compute_cost()` and `Pipeline.estimated_cost()` return `None` for any model unless the user has registered pricing via `provider.models.register_pricing(slug, strategy)`. - New `docs/reference/pricing-recipes.md` cookbook holds the last-known prices per connector as one-shot copy-paste recipes. **Not maintained**: verify with the upstream provider before relying on the values. - `compute_cost()` and `register_pricing()` themselves are unchanged; the pricing-strategy primitives (`per_unit`, `by_param`, `bucketed_by_duration`, `per_input_chars`, `per_response_metric`, `by_model_and_param`, etc.) remain in `genblaze_core.providers.pricing`. ### Deprecated - `BaseProvider.probe_model()` is deprecated and now delegates to `validate_model(refresh=True)` with a coerced `ProbeResult` for legacy `tools/probe_models.py` consumers. Slated for removal in `genblaze-core 0.4.0`. Use `validate_model()` directly going forward. ### Documentation - New migration guide: [`docs/guides/migrating-to-0.3.md`](docs/guides/migrating-to-0.3.md) — TL;DR for callers of 0.2.x, before/after for the 4 most common registry patterns, validation-outcome handling, and the decision tree for unexpected `NOT_FOUND` outcomes. - `docs/features/model-registry.md` reorganized so `ModelFamily` leads the conceptual picture (pattern-keyed routing) with `ModelSpec` as the per-slug override path. Adds `DiscoverySupport` / `FamilyProbe` / `ValidationResult` reference sections and a `validate_model(refresh=True)` example. - `docs/features/provider-system.md` Cost Tracking section rewritten to point at [`docs/reference/pricing-recipes.md`](docs/reference/pricing-recipes.md) instead of listing per-connector strategies that no longer ship. New `DiscoverySupport` overview describing the three-tier classification. - `docs/guides/new-provider.md` updated for the families-first model: new "Declare your model families and DiscoverySupport" section with a decision tree for picking `NATIVE` / `PARTIAL` / `NONE` and worked examples for each tier's required hooks (`_fetch_models`, `_invoke_family_probe`). - `README.md` and `ARCHITECTURE.md` updated to surface the new catalog architecture and cross-link the migration guide. - `docs/reference/pricing-recipes.md` is now the single source of truth for per-provider rate sheets (LMNT, Replicate, GMICloud, Runway, Decart, ElevenLabs, OpenAI, Google, Luma, Stability-Audio). Each section is dated and linked to the upstream pricing URL. ## [0.2.9] - 2026-04-30 ### Released package versions - Code-change bumps: `genblaze-core` 0.2.8, `genblaze-s3` **0.3.0** (minor — near-total backend rewrite with async, encryption, presigned URLs). - Metadata + version-source bumps: `genblaze` (umbrella) 0.3.2 — switches `__version__` to `importlib.metadata`, adds keywords, widens `genblaze-s3` pin to `<0.4` so it can resolve the new minor. - `@genblaze/spec` (npm) 0.4.0 — `step.schema.json` contract change (already bumped locally; ready to publish). - Untouched (no republish): `genblaze-stability-audio` 0.2.2 (only internal version-source refactor, no behavior change), and every other connector + cli at their current versions. ### Added - `genblaze-core`: net-new public modules — `_optional.py` (84 lines, optional-import framework), `pipeline/ingest.py` (223 lines, ingest orchestration for standalone asset writes), `storage/config.py`, `storage/errors.py`, `storage/types.py`, `storage/key_builder.py`, `storage/_tracer.py`. `storage/base.py` and `storage/sink.py` grew substantially (+292 / +158 lines); `transfer.py` extended. - `genblaze-s3`: full async backend (`async_backend.py`, 476 lines), end-to-end encryption (`encryption.py`, 211 lines), presigned URL support (`presigned.py`, 117 lines), preflight-classification (`_preflight_classify.py`), URL-policy enforcement (`url_policy.py`), user-agent module (`_user_agent.py`). `backend.py` grew by 1018 lines to support these. Comprehensive new test coverage across phase 2A/2B/2C regression suites. - Tooling: `tools/check_pypi_metadata.py` (180 lines) for catching metadata drift before publish. ### Fixed - **Build-time / runtime version drift** (Plan 5 Phase 1A/1B): `pyproject.toml`'s literal `version = "X.Y.Z"` is now the single source of truth. Hatchling propagates it into the wheel METADATA; `_version.py` reads it back via `importlib.metadata.version(...)`. So `genblaze_core.__version__`, `pip show genblaze-core`, and the `b2ai-genblaze/{version}` user-agent header always agree with the installed wheel. No more manual edits to `_version.py` per release. Applied to `genblaze-core`, `genblaze-s3`, `genblaze-stability-audio`, and `genblaze` (umbrella). Removed all `[tool.hatch.version]` blocks and `dynamic = ["version"]` declarations. - **All 13 connectors**: ``__version__`` now reads from ``importlib.metadata`` (``genblaze-{slug}``) rather than being a hardcoded string — same drift class as the core/umbrella fix below, rolled across ``genblaze-s3``, ``genblaze-openai``, ``genblaze-google``, ``genblaze-replicate``, ``genblaze-runway``, ``genblaze-luma``, ``genblaze-decart``, ``genblaze-elevenlabs``, ``genblaze-lmnt``, ``genblaze-stability-audio``, ``genblaze-nvidia``, ``genblaze-gmicloud``, and ``genblaze-langsmith``. Pinned by ``TestConnectorVersionCoherence`` in ``test_version_coherence.py`` — adding a 14th connector that hardcodes its ``__version__`` will fail CI. - `genblaze-core`: ``__version__`` now reads from ``importlib.metadata`` rather than a hardcoded string. Closes the version-drift footgun (storage tranche bug #9): pre-fix the constant was edited per release and silently drifted out of sync with ``importlib.metadata.version("genblaze-core")`` and the ``b2ai-genblaze/{version}`` user-agent header. The smoke check on the migration confirmed the prior hardcoded ``"0.2.7"`` was already drifted from the actual installed wheel ``"0.2.3"`` — exactly the bug the plan flagged. Same fix applied to the umbrella ``genblaze`` package's ``__version__``. Test ``test_version_coherence.py`` pins the invariant going forward. - `genblaze-core`: optional-dependency import errors now raise :class:`OptionalDependencyError` with the install incantation in the message (``pip install 'genblaze[parquet]'``) — not a bare ``ModuleNotFoundError``. Closes storage tranche bug #8. ``OptionalDependencyError`` subclasses ``ImportError`` so legacy ``except ImportError:`` callers continue to catch it without code changes; new callers can be more specific via ``except OptionalDependencyError:``. Wired into ``ParquetSink``'s pyarrow gate; future optional-extra modules (``aioboto3`` for AsyncS3StorageBackend, etc.) can adopt the same pattern. ### Added - `genblaze-s3`: ``genblaze_s3._user_agent.build_user_agent(*, base=None, extra=None)`` helper. Default base is ``b2ai-genblaze/{genblaze_core.__version__}`` so the user-agent always tracks the installed core wheel — no more two-step "bump core, then bump the connector's hardcoded UA" dance. ``base`` lets forks override the prefix; ``extra`` composes with ``StorageConfig.user_agent_extra`` so application identifiers append cleanly. ``backend.py`` now wires ``_USER_AGENT = build_user_agent()`` and the legacy f-string-on-import has been removed. Pinned by ``test_user_agent.py``. - **CI gate**: ``tools/check_pypi_metadata.py`` audits every published ``pyproject.toml`` (``libs/**`` plus ``cli/``) for the metadata fields PyPI search and project pages render — ``description``, ``readme``, ``authors``, ``license``, ``requires-python>=3.11``, classifier groups (``License`` / ``Programming Language :: Python :: 3.1*`` / ``Topic`` / ``Development Status``), ``project.urls`` (Homepage / Documentation / Repository / Issues), and ``keywords``. Wired as ``make pypi-metadata-check`` (``--strict`` mode exits 1 on any miss) so a release-prep PR that adds a new package missing classifiers/keywords/etc. fails loudly rather than rendering empty on PyPI. - `genblaze-core`: ``genblaze_core._optional`` module with :class:`OptionalDependencyError` and a :func:`require(extra, package, symbol=None)` helper for module-top-level use in optional-extra-gated code. - `genblaze-core` / `@genblaze/spec`: ingest-sink tranche **Phase 2** — ``Pipeline.ingest()`` factory for non-generative bulk imports. Closes the "podcast hosting / DAM bulk import / RSS pull / UGC upload" use cases the plan called out as forced to fabricate ``SyncProvider`` shims today. - ``Pipeline.ingest(assets, *, source, source_metadata=None, sink=None, name=None, tenant_id=None, step_type=StepType.INGEST) -> PipelineResult`` — classmethod factory. Each asset becomes a :class:`Step` with ``step_type=StepType.INGEST`` (or ``IMPORT``), ``provider=None``, ``model=source``, ``modality`` inferred from ``asset.media_type``, ``status=StepStatus.SUCCEEDED``. Step metadata carries ``{"source": source, **source_metadata}``. The factory orders assets by ``asset_id`` before building steps so the resulting manifest's canonical hash is **invariant under permuted input order** — a podcast app calling ``ingest`` with feed entries in any order produces a byte-identical manifest. - When a sink and ``tenant_id`` are supplied, ``put_asset`` is called for each asset with a derived ``manifest_uri`` and tenant context so :meth:`BaseSink.read_manifest_for_asset` can later discover the manifest from that tenant's asset_id. Without ``tenant_id``, assets are uploaded without a reverse-lookup index. Sinks that don't implement ``put_asset`` (e.g. ``ParquetSink``) emit a warning and skip the upload — manifest still produced for in-memory consumers. - New module ``genblaze_core.pipeline.ingest`` houses the orchestration; ``Pipeline.ingest`` is a thin classmethod wrapper so the fluent ``.step(...)`` builder surface stays focused on generation. - `genblaze-core` / `@genblaze/spec`: **StepType.INGEST and StepType.IMPORT** — non-generative step type values. Added as the Plan 4 Phase 2 slice of the master plan's Wave 4 enum extension (``StepType.{TRANSCRIBE, CLASSIFY, ANALYZE, EXTRACT, MODERATE}`` remain in Wave 4 scope). ### Changed - **PyPI metadata sweep**: every published package's ``pyproject.toml`` now carries the full classifier set (``License :: OSI Approved :: MIT License``, ``Programming Language :: Python :: 3.{11,12,13}``, ``Topic :: Multimedia`` + ``Topic :: Software Development :: Libraries``, ``Development Status``, ``Typing :: Typed``), per-package ``keywords`` (common base — genblaze / ai / media / manifest / provenance / c2pa-ready / genai / pipeline — plus provider-specific tags), and a complete ``project.urls`` block. The ``cli`` package gained the ``Documentation`` URL it was missing. PyPI search and project pages now render rich previews instead of blank metadata. - `genblaze-core` / `@genblaze/spec`: ``Step.provider`` is now ``str | None``. A new model validator requires ``provider`` to be set unless ``step_type ∈ {INGEST, IMPORT}`` — non-generative step types may have null provider (no upstream service to attribute); every other step type continues to require provider as before. Wire schema (``manifest/v1/step.schema.json``) reflects the change: ``provider`` is now nullable and removed from the ``required`` array; the ``step_type`` enum gains ``"ingest"`` and ``"import"`` values. ``@genblaze/spec`` bumped 0.3.3 → 0.4.0 (additive minor for the enum; field-relaxation is also forward-compatible since older consumers expecting non-null provider only break for the new step types they wouldn't have emitted anyway). - `genblaze-core`: three internal call sites that read ``step.provider`` and forward it through a non-Optional surface (``StepFailedEvent`` / ``StepCompletedEvent`` provider field, OTel span attribute, ``ParquetSink`` partition path) now coerce ``None`` to a sentinel. The wire / observability shape is preserved for generative steps; ingest steps emit ``provider=""`` on the wire and skip the partition-path provider segment (filtered out before ``sorted``). ### Added - `genblaze-core`: ingest-sink tranche **Phase 1** — standalone asset writes via :class:`BaseSink`. Closes the "non-generative workflow" gap where DAM, archival, podcast-hosting, and UGC apps had to fabricate `SyncProvider` shims to seed assets through the generation-shaped pipeline. - ``BaseSink.put_asset(asset, *, manifest_uri=None, tenant_id=None) -> Asset`` — write a single asset's bytes via the sink's storage backend (no Run wrapper required). Mutates the asset in place: rewrites ``url`` to the durable backend URL, populates ``sha256`` and ``size_bytes``. Source bytes resolved from the asset's existing URL (``file://`` allowlisted dirs, or SSRF-protected ``https://``). Default impl on the ABC raises ``NotImplementedError`` so non-storage-backed sinks (``ParquetSink`` etc.) keep working. - ``BaseSink.put_assets(assets, *, manifest_uri=None, tenant_id=None) -> list[Asset]`` — bulk variant, parallelizes via ``ThreadPoolExecutor`` sized at ``min(max_upload_workers, len(assets))``. Returned list preserves input order. - ``BaseSink.read_manifest_for_asset(asset_id, *, tenant_id) -> Manifest | None`` — reverse-lookup. When ``put_asset`` is called with ``manifest_uri=`` and ``tenant_id=``, the sink writes a tenant-scoped sidecar index entry at ``{prefix}/_index/{tenant_id}/{asset_id}.json`` so future callers can discover the manifest from the asset_id inside their authorization context. Manifests for assets put without ``manifest_uri=`` and ``tenant_id=`` are not discoverable via this method (by design — opt-in). Returns ``None`` for unknown asset_ids and for foreign-backend manifest URIs that don't round-trip through ``key_from_url``. - ``ObjectStorageSink`` is the concrete implementation; reuses the existing ``AssetTransfer`` machinery (download, hash, KeyBuilder-routed put). Phase 2 (``Pipeline.ingest()`` factory) blocks on the master plan's Wave 4 ``StepType.INGEST`` enum addition. ### Fixed - `genblaze-s3`: every ``S3StorageBackend`` and ``AsyncS3StorageBackend`` operation now wraps unexpected exceptions via ``classify_botocore_error`` so the resulting ``StorageError`` carries populated ``error_code`` / ``status_code`` / ``is_retriable`` / ``operation`` / ``request_id`` fields. Pre-fix every operation raised a bare ``StorageError(f"...failed: {exc}")`` with all structured fields ``None`` — defeating the plan's primary observability acceptance gate ("StorageError round-trips request_id, status_code, is_retriable, operation, error_code"). 13 call sites updated across ``put`` / ``get`` / ``exists`` / ``delete`` / ``copy`` / ``head`` / ``list`` / ``get_range`` / ``stream`` / ``delete_many`` / ``get_url`` / ``presigned_get`` / ``presigned_put`` (sync) and ``aget`` / ``astream`` (native async). Surfaced during the cross-phase final review. - `genblaze-core`: ``StorageBackend`` ABC async pairs accept ``**kwargs`` for connector-specific options. ``aput`` / ``acopy`` / ``aget`` / ``ahead`` / ``aget_range`` now forward arbitrary kwargs through ``asyncio.to_thread`` to the matching sync method. Pre-fix the ABC defaults silently dropped ``encryption=`` / ``object_lock=`` / ``progress=`` for callers typed against the ABC — e.g. an SSE-C HEAD via the ABC async surface would treat the encrypted object's 403 as not-exist instead of decrypting. Surfaced during the cross-phase final review. ### Documentation - `docs/features/ingest-workflows.md`: NEW. Full feature doc for the ingest tranche covering Pipeline.ingest API, the four canonical use cases (podcast hosting, UGC upload, archival/DAM bulk import, cross-system/cross-tenant transfers, RTMP-style live ingest), reverse-lookup pattern via read_manifest_for_asset, manifest determinism gate (hash invariance under permuted input order), INGEST vs IMPORT semantics, composition with the generative Pipeline.step() builder, and cross-links to object-storage.md / manifest-provenance.md / moderation.md. - `examples/ingest_podcast_episode.py`: NEW runnable. Pulls episode list, ingests into B2 with CONTENT_ADDRESSABLE dedup, prints per-asset attribution + manifest hash, demonstrates reverse lookup. Notes Wave 6 Whisper transcribe chain at the bottom. - `examples/ingest_ugc_upload.py`: NEW runnable. Synthesizes a user-uploaded PNG, ingests via Pipeline.ingest with full uploader source_metadata (uploader_id / session_id / ip / user_agent), demonstrates reverse lookup, sketches the ModerationHook integration. - `docs/features/object-storage.md`: added Phase 2 (read primitives, bulk deletes, progress callbacks, per-put Object Lock) and Phase 3 (``AsyncS3StorageBackend``, native async vs threadpool-delegated methods, ``from_sync`` pattern) sections plus callouts for the ``aput``-progress-fires-on-thread-not-event-loop caveat and the SSE/Object Lock conflict-guard ``ValueError`` (raised before the network try/except, so distinct from ``StorageError``). - `libs/connectors/s3/README.md`: matching Phase 2/3 quickstart sections. - `docs/exec-plans/active/storage-backend-hardening-tranche.md`: inline annotation that ``presigned_post`` is intentionally deferred (separate ``PresignedPost`` value object needed). ### Fixed - `genblaze-s3`: ``AsyncS3StorageBackend`` async client now passes an ``AioConfig`` mirroring the sync ``BotoConfig`` — ``request_checksum_calculation="when_required"`` (B2 CRC32-trailer fix), ``connect_timeout=30``, ``read_timeout=300``, ``max_pool_connections=20``, ``user_agent_extra="b2ai-genblaze/…"``. Pre-fix the async path inherited aiobotocore's defaults, which on boto3 >= 1.36 inject the same trailer that broke async ``aget`` / ``astream`` on B2 endpoints. Surfaced during the Phase 3 final review (BLOCKING). - `genblaze-s3`: ``AsyncS3StorageBackend.__aexit__`` reorders cleanup so a failed inner ``__aexit__`` (aiohttp connector teardown exception) doesn't leave the backend holding a stale client ctx. The references are cleared upfront on a local snapshot; any exception from the inner exit propagates naturally without preventing future ``async with`` re-enters from succeeding. Surfaced during the Phase 3 final review (IMPORTANT). - `genblaze-s3`: ``AsyncS3StorageBackend.from_sync`` now carries forward ``_region_verified``, the (possibly auto-corrected) ``_region`` / ``_endpoint_url``, and ``_preflight_error`` from the source sync backend. Pre-fix the new internal sync delegate ran another preflight ``HeadBucket`` even when the source was already verified — a redundant round-trip on every ``from_sync`` call. Aio kwargs are rebuilt after the copy so endpoint/region rewrites flow through. Surfaced during the Phase 3 final review (IMPORTANT). ### Changed - `genblaze-s3`: ``AsyncS3StorageBackend.astream`` return annotation refined from ``AsyncIterator[bytes]`` to ``AsyncGenerator[bytes, None]`` so the type system signals "iterate, don't await" — callers who write ``await ab.astream(...)`` get a clearer error. ``progress`` parameter typing tightened on ``aget`` and ``astream`` from ``Any`` to ``Callable[[TransferProgress], None] | None``. Surfaced during the Phase 3 final review. ### Added - `genblaze-s3`: storage-backend hardening **Phase 3** native async via ``aioboto3`` — closes the long-deferred async-iterator gap that Phase 0 explicitly punted to here. - New ``AsyncS3StorageBackend`` class (concrete, not an ABC). Use as an async context manager:: async with AsyncS3StorageBackend.from_sync(my_sync_backend) as ab: data = await ab.aget("k") async for chunk in ab.astream("big.mp4"): ... The wrapped sync backend is exposed at ``ab.sync`` for callers who need the historical surface (lifecycle helpers, key utils) without leaving the async context. - **Native async**: ``aget`` (single-shot or chunked-progress download via ``await Body.read()``) and ``astream`` (genuine ``AsyncIterator[bytes]`` via aioboto3's ``Body.iter_chunks``). The streaming path is the headline win — Phase 0's threadpool wrap couldn't faithfully adapt a sync iterator into an ``AsyncIterator`` without buffering the whole body. - **Threadpool-delegated** (current sub-phase): ``aput``, ``ahead``, ``alist``, ``aexists``, ``adelete``, ``acopy``, ``adelete_many``, ``adelete_prefix``, ``aget_range``, ``aget_url``, ``aget_durable_url``. These dispatch to the wrapped sync backend via ``asyncio.to_thread``. Native versions are tracked as a follow-up sub-phase — ``aput`` in particular needs aioboto3-native multipart support which is more involved. - **Optional dependency**: install via ``pip install 'genblaze-s3[async]'`` (or ``aioboto3>=12,<13`` directly). ``import genblaze_s3`` works without the extra; only ``async with AsyncS3StorageBackend(...) as ab:`` requires it, and raises ``ImportError`` with the extras hint when missing. - ``AsyncS3StorageBackend.from_sync(sync_backend)`` constructs an async backend that shares an existing sync backend's settings (bucket / region / credentials) — common pattern for apps adding async to an established setup. ### Fixed - `genblaze-s3`: ``_adapt_progress_to_boto3_callback`` now lock-protects the cumulative-byte counter. boto3 invokes the ``Callback=`` from multiple part workers concurrently (default ``max_concurrency=4``); the closure's ``nonlocal cumulative += delta`` compiled to LOAD/BINARY_ADD/STORE bytecodes, and the GIL releases between bytecodes — concurrent threads could load the same stale value and silently drop deltas. Cumulative byte counts under stress now match the sum of all reported deltas. Surfaced during the Phase 2 final review; my docstring's claim that boto3 serializes Callback invocations was wrong. - `genblaze-s3`: ``_data_size`` now honors ``BytesIO.tell()`` so a partially-consumed buffer reports the correct *remaining* byte count for ``TransferProgress.total_bytes``. Pre-fix: ``data.getbuffer().nbytes`` returned the full allocated buffer regardless of read position, so callers who seeked or partially-read the BytesIO before passing it to ``put`` saw ``total_bytes`` overstated — progress UIs hit 100% well before the upload finished. Surfaced during the Phase 2 final review. - `genblaze-s3`: ``S3StorageBackend.get(progress=…)`` chunked path now pre-allocates a ``bytearray(ContentLength)`` when the total is known and writes chunks into it directly. Pre-fix: the impl appended each chunk to ``parts: list[bytes]`` and called ``b"".join(parts)`` at the end — ~2× peak memory (intermediate list + final bytes) plus one Python object per chunk. Defensive truncate when Content-Length overstates the actual body length. The fast path (``progress=None``) is unchanged. Surfaced during the Phase 2 final review. - `genblaze-s3`: ``S3StorageBackend.delete_prefix`` now surfaces partial-progress state on a mid-walk ``list()`` failure. Pre-fix: if ``list()`` raised on page N, the ``StorageError`` propagated uncaught and the already-deleted keys from pages 1..N-1 were invisible to the caller. Post-fix: the exception is captured into a synthetic ``DeleteError(key="", code="list_failed", message=…)`` on the returned ``DeleteResult``; ``result.deleted`` carries the keys actually removed before the failure. Caller can detect and recover. Surfaced during the Phase 2 final review. - `genblaze-s3`: ``S3StorageBackend.stream`` docstring documents the early-exit connection-lifecycle cost (``gen.close()`` mid-iteration discards the underlying HTTP connection rather than returning it to the urllib3 pool — botocore ``StreamingBody.close()`` doesn't drain). Negligible for typical full-stream consumers; worth knowing for high-fanout consumers that frequently abort streams. Surfaced during the Phase 2 final review. ### Added - `genblaze-core` / `genblaze-s3`: storage-backend hardening **Phase 2C** progress callbacks + per-put Object Lock — closes the last two missing-primitive rows for synchronous Phase 2. - New ``TransferProgress(bytes_transferred, total_bytes, operation, key)`` frozen dataclass. ``total_bytes=None`` is the documented "unknown total" signal for stream sources where the size would require a draining pass to determine. - ``backend.put(progress=…)`` adapts boto3's ``Callback=`` (delta bytes per multipart chunk) to a cumulative ``TransferProgress`` via a closure-cell accumulator. ``total_bytes`` is inferred automatically for ``bytes`` and ``io.BytesIO`` payloads; arbitrary ``BinaryIO`` streams pass ``None`` (boto3's transfer manager can't report the total without draining). The single-PUT path (caller pinned ``ChecksumSHA256``) silently skips progress because ``put_object`` doesn't accept a ``Callback`` parameter. - ``backend.get(progress=…)`` switches to a 1 MiB chunked-read loop that fires the callback with cumulative totals. Without ``progress=``, the historic single-call ``body.read()`` fast path is preserved — no allocation overhead for callers who don't need progress. - ``backend.stream(progress=…)`` fires per yielded chunk (``chunk_size`` defaults to 8 MiB). - ``backend.put(object_lock=ObjectLockConfig(...))`` applies per-put Object Lock retention. Useful when most uploads to a bucket don't need retention but a specific manifest does — finer granularity than the sink-wide ``manifest_lock``. - ``_build_extra_args`` adds an Object Lock conflict guard mirroring the SSE pattern: passing both ``object_lock=`` and an overlapping ``extra_args`` key (``ObjectLockMode``, ``ObjectLockRetainUntilDate``, or ``ObjectLockLegalHoldStatus``) raises ``ValueError`` rather than silently merging mismatched envelopes. - ``TransferProgress`` re-exported via ``genblaze_core.__all__``. - `genblaze-core` / `genblaze-s3`: storage-backend hardening **Phase 2B** bulk-delete primitives — ``delete_many`` and ``delete_prefix``, plus two new value-object types (``DeleteError``, ``DeleteResult``). - ``backend.delete_many(keys: Sequence[str], *, dry_run=False) -> DeleteResult`` issues batched ``DeleteObjects`` calls (chunked at 1000 keys, S3's hard cap). Per-key failures land in ``result.errors`` rather than aborting the batch — partial-success callers can salvage what worked. ``dry_run=True`` returns a preview without contacting the backend. - ``backend.delete_prefix(prefix: str, *, dry_run=True) -> DeleteResult`` walks ``list()`` pages and deletes per-page (memory bounded for prefixes matching millions of keys; no all-keys-in-RAM buffer). **Defaults to dry-run** — caller passes ``dry_run=False`` to actually delete. The asymmetry vs. ``delete_many`` (``dry_run=False`` default) is intentional: an explicit list of keys is much harder to fat-finger than a prefix that could match more than the caller expects. Empty / whitespace prefix raises ``ValueError`` rather than matching every object in the bucket. Loud INFO log per-page when actually deleting so accidental large operations leave a breadcrumb. - ``DeleteResult`` exposes ``.total`` and ``.all_succeeded`` properties for the common partial-failure inspection patterns; ``deleted`` and ``errors`` are tuples (truly immutable + hashable so a result can land in a cache). - ``DeleteError`` mirrors S3's per-key wire shape (``key``, ``code``, ``message``). - ``adelete_many`` / ``adelete_prefix`` async pairs delegate via ``asyncio.to_thread``. Native async lands in Phase 3. - ``DeleteError`` and ``DeleteResult`` re-exported via ``genblaze_core.__all__``. - `genblaze-core` / `genblaze-s3`: storage-backend hardening **Phase 2A** read primitives — ``head``, ``list``, ``get_range``, ``stream``, plus three new value-object types (``ObjectMetadata``, ``FileEntry``, ``ListPage``). Closes four of the missing-primitive rows in the storage-backend-hardening tranche. - ``backend.head(key, *, encryption=None) -> ObjectMetadata | None`` returns full per-object metadata (size, last_modified, etag, content_type, storage_class, user metadata dict). ``None`` for missing keys; tolerates 404 AND 403 the same way ``exists`` does (scoped application keys legitimately get 403 on non-existent reads). ``encryption=`` accepts the same SSE-C envelope as ``get`` — closes the head-side asymmetry that completed bug #3. - ``backend.list(prefix="", *, max_keys=1000, continuation_token=None) -> ListPage`` walks ``ListObjectsV2`` with explicit pagination. ``ListPage.entries`` is a ``tuple[FileEntry, ...]`` (truly immutable, hashable); ``ListPage.next_token`` is ``None`` once the listing is exhausted. ``FileEntry`` is the cheap shape that S3's ``ListObjectsV2`` returns natively — no per-key HEAD round-trip required to populate it. - ``backend.get_range(key, *, offset, length, encryption=None) -> bytes`` downloads a byte range via the HTTP ``Range`` header. Validates ``offset >= 0`` and ``length >= 0``; ``length=0`` short-circuits without contacting the backend. Useful for partial-file reads of multi-GB video / long-form audio. - ``backend.stream(key, *, chunk_size=8MiB, encryption=None) -> Iterator[bytes]`` lazily yields chunks via ``StreamingBody.read``. Best-effort ``body.close()`` on iterator exhaustion or ``gen.close()`` returns the HTTP connection to the pool. - ``backend.ahead`` / ``backend.aget_range`` async pairs delegate via ``asyncio.to_thread``. ``alist`` and ``astream`` are deliberately omitted from the ABC — threadpool-wrapping a sync iterator into an ``AsyncIterator`` either buffers the full result (defeating streaming) or spins up a queue per call. Phase 3 introduces native async via ``aioboto3`` for these specifically. - ``StorageBackend`` ABC defaults raise ``NotImplementedError`` for the 4 new sync methods — existing third-party subclasses that predate Phase 2A keep working at import time and opt in by overriding. - `genblaze-core`: ``ObjectMetadata``, ``FileEntry``, ``ListPage`` re-exported via ``genblaze_core.__all__`` (lazy, like the other storage value objects). ### Fixed - `genblaze-s3`: ``S3StorageBackend`` preflight no longer permanently bricks the backend on a transient upstream failure. Pre-fix, any non-redirect ``ClientError`` from ``HeadBucket`` (including 5xx, throttle, network blips) was cached as ``_preflight_error`` and re-raised on every subsequent call until the process restarted. Post-fix, the new ``_preflight_classify.is_sticky_preflight_error`` helper consults Phase 0's ``RETRYABLE_STORAGE_CODES`` to decide: retriable codes (``RATE_LIMIT`` / ``SERVER_ERROR`` / ``NETWORK`` / ``TIMEOUT``) re-raise without caching so the next call retries the HeadBucket; sticky codes (auth, missing bucket, signature mismatch) cache as before. Surfaced during the Phase 1 final review. - `genblaze-s3`: ``S3StorageBackend.put`` now detects SSE envelope conflicts between ``encryption=`` and overlapping ``extra_args`` keys and raises ``ValueError`` instead of silently encrypting the object with whichever side wins on a per-key basis. Mismatched envelopes (e.g. ``encryption=Encryption.sse_kms("A")`` plus ``extra_args={"SSEKMSKeyId": "B"}``) silently encrypted with the wrong material — S3 accepted the request, so callers wouldn't notice until they tried to decrypt. The ``ValueError`` is raised before the network try/except wrapper so caller API misuse propagates with its native exception type rather than being masked as ``StorageError``. Surfaced during the Phase 1 final review. - `genblaze-core`: ``StorageBackend.aget_url`` exposes ``policy=`` and other backend-specific kwargs to async callers via ``**kwargs`` forwarding. The async surface now reaches feature parity with the sync ``S3StorageBackend.get_url`` — ``await backend.aget_url(key, policy=URLPolicy.PUBLIC)`` and the conflict detection from Phase 1D both work. Default ``expires_in`` flipped from ``3600`` to ``None`` (sentinel meaning "don't pass") so backends that distinguish unset-vs-default see "caller didn't pass" rather than "caller passed 3600". Surfaced during the Phase 1 final review. - `genblaze-s3`: ``URLPolicy.AUTO`` docstring corrected. Pre-fix, the docstring claimed AUTO raises on explicit ``expires_in`` while ``public_url_base`` is set; the implementation is actually permissive (preserves historic silent-ignore for backward compat). Aligned the docstring with the implementation: callers wanting strict raise-on-conflict semantics pass ``policy=URLPolicy.PUBLIC`` explicitly. Surfaced during the Phase 1 final review. - `genblaze-s3`: ``S3StorageBackend.get_url(expires_in=...)`` no longer silently ignored when ``public_url_base`` is configured. The historic ``URLPolicy.AUTO`` behavior (default — public when ``public_url_base`` set, presigned otherwise) is preserved for backward compat, but explicit ``policy=URLPolicy.PUBLIC`` with an ``expires_in=`` argument now raises :class:`URLPolicyError` instead of returning a never-expiring URL. Closes bug #2 in the storage-backend-hardening tranche. - `genblaze-s3`: ``S3StorageBackend.get(key)`` and ``S3StorageBackend.copy(src, dst)`` accept ``encryption=`` and plumb the customer-key / KMS-key envelope through to the boto3 call. SSE-C uploads now round-trip cleanly — the previous read path silently dropped the customer key, so encrypted objects 4xx'd on download. Closes bug #3. - `genblaze-s3`: ``S3StorageBackend.get_url`` no longer issues a ``HeadBucket`` on the public-URL path. Public URL rendering is pure string concat against ``public_url_base`` and never needed the region verify; offline dev / CI flows that just want to compute a URL no longer require a reachable bucket. The presigned-URL path still verifies (signing endpoint must match the bucket region). Closes bug #7. ### Added - `genblaze-s3`: ``S3StorageBackend.get_url(policy=URLPolicy.AUTO)`` kwarg. Members: ``AUTO`` (default — preserves today's behavior), ``PUBLIC`` (force public, requires ``public_url_base``, conflict-with-expires_in raises :class:`URLPolicyError`), and ``PRESIGNED`` (force a SigV4 URL even when ``public_url_base`` is configured — useful for paid-feed / time-limited fetches off a public bucket). Backwards-compatible — every existing caller passing no policy or only ``expires_in`` continues to work. - `genblaze-s3`: ``S3StorageBackend.put(encryption=...)``, ``.get(encryption=...)``, ``.copy(encryption=...)`` — :class:`Encryption` value object accepted symmetrically across the three operations. Replaces the historic ``extra_args={"ServerSide…": …}`` escape hatch with typed construction; ``extra_args`` still wins on conflict so callers retain raw boto3 control when they need it. - `genblaze-s3`: ``S3StorageBackend.presigned_get(key, *, expires_in=3600) -> PresignedURL`` and ``S3StorageBackend.presigned_put(key, *, expires_in=3600, content_type=None) -> PresignedURL`` — typed presigned-URL methods returning the redaction-safe value object from Phase 1A. The URL defaults to redacted in ``repr`` / ``str`` / ``f"{...}"``; access the unredacted value via the ``.url`` attribute. Use these instead of ``get_url(policy=URLPolicy.PRESIGNED)`` when you want default redaction in logs. ``presigned_post`` deferred to a later phase (different return shape — needs a separate :class:`PresignedPost` value object covering both URL and POST-policy form fields). - `genblaze-core`: ``ObjectStorageSink(prefix="runs", key_strategy=HIERARCHICAL)`` no longer produces ``runs/runs/{tenant}/{date}/{run_id}/...`` keys. The strategy's hardcoded ``runs/`` segment is now collapsed against prefixes that already end in ``runs`` via the new ``KeyBuilder`` primitive — same fix applies to the asset-key path through ``AssetTransfer``. Closes bug #5 in the storage-backend-hardening tranche. Caller-intentional duplicates within the prefix (``"archive/archive"``) or within the strategy segments are preserved — the dedupe is seam-only, never global. ### Added - `genblaze-core`: ``genblaze_core.KeyBuilder`` — pure value-object for storage-key construction. Use ``KeyBuilder.from_prefix(s)`` to normalize a prefix (strips leading/trailing slashes, collapses consecutive separators), ``.append(*segments)`` to extend it (returns a new ``KeyBuilder``), and ``.build(*segments)`` for a terminal key string. Both ``append`` and ``build`` apply the seam-dedupe rule. Frozen ``@dataclass`` for parity with :class:`StorageConfig` and :class:`RetryPolicy`. Used internally by :class:`ObjectStorageSink` and :class:`AssetTransfer`; exposed publicly so downstream backends and custom sinks can reuse the same normalization rules. ### Security - `genblaze-s3`: ``S3StorageBackend.put`` no longer returns a presigned URL. The previous return shape leaked the access-key-id (``X-Amz-Credential`` query parameter) into anything that persisted the value — logs, manifests, DB rows — and broke canonical-hash stability for content-addressable layouts because the signature rotates per call. ``put`` now returns the storage key (a ``str``); compose with :meth:`S3StorageBackend.get_durable_url` for a credential-free, persistable URL, or call :meth:`get_url` for an explicit presigned URL when one is genuinely needed (Phase 1B forthcoming ``presigned_get``/``presigned_put``/``presigned_post`` methods will return a redaction-safe :class:`PresignedURL` value object). No deprecation shim — every internal caller in the monorepo discards ``put``'s return value already (verified across ``ObjectStorageSink`` and ``AssetTransfer``); the only callers affected are external code paths that were persisting the leaked URL, which was itself the vulnerability. Closes bug #1 in the storage-backend-hardening tranche. ### Changed - `genblaze-s3`: ``S3StorageBackend.for_backblaze(auto_lifecycle=...)`` now defaults to ``False`` (was ``True``). Construction no longer silently mutates bucket-wide lifecycle configuration. Callers that want the historic behavior pass ``auto_lifecycle=True`` explicitly, or call :meth:`ensure_lifecycle_defaults` after construction with intentional opt-in. Closes bug #4 in the storage-backend-hardening tranche. - `genblaze-s3`: ``for_backblaze`` preflight failures now **raise** instead of warning-and-returning a half-broken backend. Placeholder credentials surface immediately at construction time rather than failing on every subsequent operation with a stale "preflight skipped" warning. Use ``preflight=False`` (see ``### Added``) for offline tests that legitimately want to defer verification. - `genblaze-s3`: ``examples/quickstart.py`` and ``examples/b2_storage_pipeline.py`` updated to pass ``auto_lifecycle=True`` explicitly so the runnable examples preserve their historic behavior under the new default. ### Added - `genblaze-s3`: ``S3StorageBackend.for_backblaze(preflight=...)`` — set ``preflight=False`` to skip the construction-time HeadBucket call. Useful for offline tests with placeholder credentials. Cannot be combined with ``auto_lifecycle=True`` (lifecycle requires a verified region) — passing both raises ``ValueError``. Closes the ``preflight=False`` half of bug #4. - `genblaze-s3`: storage-backend hardening Phase 1A — additive value objects for the upcoming P0 bug fixes. None are wired into backend methods yet (Phases 1B/1D); shipping the public API first lets callers build on top while we land behavior changes behind deprecation shims. Three new symbols, one back-compatible kwarg alias. - `genblaze_s3.URLPolicy` (`StrEnum` — `AUTO` / `PUBLIC` / `PRESIGNED`) and `genblaze_s3.URLPolicyError`. Replaces the silent `expires_in`-vs-`public_url_base` precedence in `get_url` (bug #2). `AUTO` matches today's behavior; `PUBLIC` and `PRESIGNED` force the flavor. Wiring lands in Phase 1D — Phase 1A is the value-object foundation only. - `genblaze_s3.Encryption` — symmetric SSE config accepted by `put`/`get`/`copy`/`head` (bug #3). Frozen dataclass with three construction modes: `Encryption.sse_s3()`, `Encryption.sse_kms(key_id)`, `Encryption.sse_c(key_bytes)` (auto-computes key MD5). Each mode serializes to the right boto3 wire shape via `to_put_extra_args` / `to_get_extra_args` / `to_head_extra_args` / `to_copy_extra_args`. SSE-C customer keys are redacted in `__repr__` AND `__str__` — only direct attribute access reveals the raw bytes. Wiring lands in Phase 1D. - `genblaze_s3.PresignedURL` — credential-bearing URL with redaction-safe formatting. `__repr__` and `__str__` strip `X-Amz-Signature` / `X-Amz-Credential` / `X-Amz-Security-Token` (and legacy SigV2 equivalents) so `f"...{url}..."` log lines no longer leak transient credentials. The `.url` attribute returns the unredacted form for handing to HTTP clients — every leak site becomes a conscious decision rather than a default string-interpolation accident. Foundation for the Phase 1B `presigned_get`/`presigned_put`/`presigned_post` methods. - `genblaze_s3.S3StorageBackend` accepts `access_key_id` and `secret_access_key` as kwarg aliases of `aws_access_key_id` and `aws_secret_access_key` (bug #10). The README quickstart already used the short form; both names now work. Passing both names for the same credential raises `TypeError` — no silent precedence. - `genblaze-core`: storage-backend hardening Phase 0 foundation modules. Additive only — no behavior change for end-users; downstream phases land the P0 bug fixes and missing primitives (range/stream/list/head/etc.) on top of these. Net: 6 new symbols, 7 inherited async method pairs on every `StorageBackend` subclass. - `genblaze_core.StorageConfig` — frozen `@dataclass(frozen=True, slots=True)` with 8 tunable knobs (`max_pool_connections`, `connect_timeout_sec`, `read_timeout_sec`, `multipart_threshold`, `multipart_chunk_size`, `retries`, `user_agent_extra`, `signing_addressing_style`). Defaults preserve the historic `S3StorageBackend` values; `StorageConfig()` is a no-op upgrade. Mirrors the `RetryPolicy` precedent from `genblaze_core.providers.retry`. - `genblaze_core.StorageErrorCode` — typed enum (`NOT_FOUND`, `ACCESS_DENIED`, `AUTH_FAILURE`, `REGION_REDIRECT`, `RATE_LIMIT`, `SERVER_ERROR`, `NETWORK`, `TIMEOUT`, `INVALID_INPUT`, `ENCRYPTION_REQUIRED`, `OBJECT_LOCKED`, `UNKNOWN`) and `RETRYABLE_STORAGE_CODES` frozenset. Mirrors `ProviderErrorCode` shape so retry classification and observability tooling share vocabulary across the provider and storage subsystems. - `genblaze_core.classify_botocore_error(exc, *, operation, key=None)` — maps a `botocore.ClientError` (or any boto exception) to a populated `StorageError` with `error_code`, `request_id`, `status_code`, `retry_after`, `is_retriable`, and `operation` set. Lazy-imports `botocore` so core stays minimal-install clean. - `genblaze_core.exceptions.StorageError.__init__` extended with optional keyword args `error_code`, `request_id`, `status_code`, `retry_after`, `is_retriable`, `operation` — full parity with `ProviderError`. Backwards-compatible: every existing `StorageError(message)` call site in the 11 connectors works unchanged. - `genblaze_core.storage.base.StorageBackend` — async pairs for all 6 existing sync abstract methods (`aput`, `aget`, `aexists`, `adelete`, `aget_url`, `aget_durable_url`) plus `acopy` for the concrete `copy` method. Default impls delegate via `asyncio.to_thread`; backends with native async (e.g. `aioboto3`) override directly. Streaming primitives (`alist`, `astream`) are deliberately deferred to a later phase — sync iterators threadpool-wrapped into async iterators lie about back-pressure and buffer the entire result. - Internal: `genblaze_core.storage._tracer.traced(op_name)` decorator — OTel-instrumentation primitive for backend methods. Returns the wrapped function unchanged when `opentelemetry` isn't installed (zero-overhead no-op). Not yet wired to the S3 backend (Phase 1+). ## [0.2.8] - 2026-04-28 ### Released package versions - Code-change bump: `genblaze-core` 0.2.7. - Untouched (no republish): every other Python and npm package. ### Added - `genblaze-core`: `Pipeline.step(external_inputs=[Asset, ...])` — caller-held Assets seed `Step.inputs` directly, without going through `input_from=` (which only references prior pipeline steps) or `chain=True`. Closes the gap that blocked multimodal first-step calls into `NvidiaChatProvider` and any future chat / vision-analysis / image-edit step that needs a user-uploaded asset on step 0. Mutually exclusive with `input_from=`. Provider must declare `accepts_chain_input=True` in its `ProviderCapabilities` (the existing flag covers all three input mechanisms; docstring updated). Pass an `Asset` with `sha256` populated for stable cache keys and manifest canonical hashes — `WARNING` is logged otherwise. Defensive copy of the input list is taken at construction so post-construction caller mutation doesn't bleed into the deferred step. Reserved kwargs `inputs=` / `input=` raise a friendly `GenblazeError` pointing at the right name (prevents silent `**params` swallow). `Pipeline.to_template()` raises when any step uses `external_inputs=` (templates describe pipeline shape, not runtime Asset payloads). Stable additive API; Wave 4's planned `Pipeline.input(asset_or_path)` will be sugar over this primitive that adds local-path acceptance via `LocalFilesystemSink` — both APIs will coexist. ## [0.2.7] - 2026-04-24 ### Released package versions - `genblaze-core` 0.2.6, `genblaze-gmicloud` 0.2.6, `genblaze-google` 0.2.4, `genblaze-openai` 0.2.4, `genblaze-nvidia` 0.2.1, `genblaze-s3` 0.2.4, `genblaze-runway` 0.2.3, `genblaze-luma` 0.2.3. - `@genblaze/spec` (npm) 0.3.3 — adds `step-queued` event schema + queued/heartbeat/ETA/request_id fields on existing event schemas. - Untouched (no republish): `genblaze-replicate` 0.2.2, `genblaze-decart` 0.2.2, `genblaze-elevenlabs` 0.2.2, `genblaze-lmnt` 0.2.2, `genblaze-stability-audio` 0.2.2, `genblaze-langsmith` 0.2.1, `genblaze-cli` 0.2.0, `genblaze` (umbrella) 0.3.1. ### Fixed - `genblaze-core`: `StepRetriedEvent` now actually reaches `Pipeline.stream()` / `astream()` consumers. The schema shipped in `@genblaze/spec` 0.3.2 but the `_install_progress_tracer` composite only wrapped `on_progress`, so `BaseProvider._emit_retry` events fired into a void — UIs subscribed to the stream saw silence between attempt 1 and attempt N. Adds `QueueEmitter.on_retry`, extends the tracer-install composite to wrap `on_retry` with the same user-callback-then-emit pattern as `on_progress`, and exposes `on_retry=` as a kwarg on `Pipeline.run()` / `arun()`. ### Added - `genblaze-core`: typed multimodal `ChatMessage.content` — accepts structured content parts (text + image) instead of plain strings, plus a `response_format` helper for JSON-mode / structured-output across providers. The four chat-bearing connectors (`gmicloud`, `google`, `openai`, `nvidia`) all wire to the new shape. - `genblaze-nvidia`: new `chat_provider.py` (272 lines) and `models/chat.py` surface the multimodal contract end-to-end. Includes 269 lines of new test coverage. - `genblaze-core`: streaming events gain `queued` lifecycle state, heartbeats, and ETA propagation. `BaseProvider` plumbing emits these from any connector that overrides the new hooks; `Runway` and `Luma` connectors implement the emission code in this release. Spec gains `events/v1/step-queued.schema.json` and adds the relevant fields on step-started / step-progress / step-completed / step-failed. - `genblaze-s3`: `request_id` / upstream prediction tracking on the storage backend (38 lines + 74 lines of tests) for end-to-end traceability from manifest → upstream provider request. - `genblaze-core`: every media handler updated (jpeg, png, mp3, wav, webp, mp4, flac, aac, embedder, sidecar) plus comprehensive new test coverage for the media pipeline. ### Added - `genblaze-core`: `BaseProvider.poll_progress(prediction_id)` hook — connectors return mid-poll signals (`preview_url`, `progress_pct`, `message`) that the base poll loop merges into the next `step.progress` event. Default `None`; backwards compatible. Closes the long-standing spec/implementation gap where `StepProgressEvent.preview_url` shipped on the wire but no connector populated it. - `genblaze-runway`: surfaces `task.progress` and `task.thumbnail_url` / `task.preview_url` via `poll_progress()` so dashboards see live intermediate frames during Gen-4 video generation. - `genblaze-luma`: surfaces intermediate Dream Machine preview frames (`assets.preview` / `assets.image` / `assets.thumbnail`) and the current `state` string via `poll_progress()`. - `genblaze-core`: new `StepQueuedEvent` (`type="step.queued"`) — additive signal for steps waiting on capacity. Sequential pipelines emit it for every upcoming step at run start (`reason="serial"`); concurrent pipelines with `max_concurrency` emit it when a coroutine finds the semaphore locked at entry (`reason="concurrency_limit"`). `step.started` semantics are unchanged — consumers that don't care about queued state ignore the event type. `Pipeline._build_step` now accepts an optional `step_id=` so queued and started events reference the same UUID. New schema, parent stream-event union extended, TypeScript types regenerated. - `genblaze-core`: heartbeat ticks during long polls. When the adaptive poll interval grows past 15s, `BaseProvider` splits the sleep into 10s chunks and emits an `is_heartbeat=True` `step.progress` event between chunks so SSE proxies, load balancers, and impatient users see the connection is alive. New `is_heartbeat: bool` field on `StepProgressEvent` and `ProgressEvent`. `Pipeline.stream(heartbeats=False)` / `astream(heartbeats=False)` drops keepalive ticks at the emitter for high-volume deployments where the overhead outweighs the benefit. Schema updated; TypeScript types regenerated. - `genblaze-core`: `Pipeline.step(expected_duration_sec=...)` and matching `expected_duration_sec` field on `StepStartedEvent`. Caller-supplied ETA hint surfaces on the wire so progress UIs can render meaningful bars without hard-coding per-model knowledge. SDK does not synthesize the value — apps own accuracy. Schema updated; TypeScript types regenerated. - `genblaze-core`: `request_id` field on `StepProgressEvent`, `StepCompletedEvent`, and `StepFailedEvent` carries the upstream provider's prediction/job id (e.g. Replicate prediction id, Runway task id) on the wire. `BaseProvider` stashes the id in `step.metadata["upstream_id"]` as soon as `submit()` returns, then `_fire_progress` and the streaming-event translators thread it through. Lets dashboards render live debug links ("view in Replicate") without waiting for step completion. Available from any progress tick fired after submit; pre-submit ticks carry `null` (id doesn't exist yet). Schemas updated; TypeScript types regenerated. - `genblaze-core`: `RetryPolicy` frozen dataclass in `genblaze_core.providers.retry` with seven knobs (`max_attempts`, `initial_backoff_sec`, `max_backoff_sec`, `backoff_multiplier`, `jitter`, `respect_retry_after`, `retryable_codes`, `idempotency_key_strategy`) and three preset classmethods (`conservative()`, `aggressive()`, `disabled()`). Pass to any provider via `Provider(retry_policy=...)` to tune transient-retry behavior per instance. Defaults reproduce the historical `BaseProvider.poll_transient_retries=5` behavior so existing code (including instance-level `provider.poll_transient_retries = N` mutations) keeps working unchanged. **Closes the gap left by the [0.2.5] release notes** — that version's changelog promised a `RetryPolicy` users could override per-provider, but the class itself wasn't shipped. This is the actual implementation. - `genblaze-core`: `BaseProvider.IDEMPOTENCY_HEADER_NAME` opt-in class attribute and `_inject_idempotency_header(headers, step)` helper. When a provider sets the header name, retried submits carry a stable idempotency key (default: `step.step_id`) so the upstream can dedupe. Per-provider header opt-ins are individual follow-up PRs. - `genblaze-core`: cross-provider conformance test (`tests/conformance/test_provider_contract.py::test_accepts_retry_policy_kwarg`) asserts every `BaseProvider` subclass forwards `retry_policy=` to `super().__init__()` — same shape as the existing `models=` test. - `genblaze-core`: `EmbedResult.embed_error` — populated when `SmartEmbedder` falls back from inline embed to sidecar so callers can see *why* the inline path failed (previously logged at WARNING and silently swallowed). - `genblaze-core`: `WebpHandler.embed(lossless=None)` (default) auto-detects VP8L source codec and preserves losslessness. Pass `lossless=True/False` to override. - `genblaze-core`: `media.atomic_write(path)` context manager — single source of truth for atomic temp-file + rename writes, replacing eight copies of the same boilerplate across handlers. - `genblaze-core`: `media.sniff_mime(path)` — magic-byte MIME detection (PNG, JPEG, WebP, MP4, MP3, WAV, FLAC). `guess_mime()` now prefers content over extension so a misnamed file (`image.png` containing JPEG bytes) dispatches to the correct handler instead of failing inside the wrong one. - Docs: `docs/features/retry-policy.md` — when to override the default, preset chooser, idempotency-key rollout status table, migration from `poll_transient_retries`. - Docs: `docs/features/trust-modes.md` — three-mode trust model (integrity / signed / C2PA), threat model table, asset-binding caveat. - Docs: `docs/features/manifest-provenance.md` — explicit hash-payload-vs-canonical-JSON documentation for third-party verifiers. - `genblaze-core`: `ObjectStorageSink.manifest_key_for(run)` / `manifest_url_for(run)` / `read_manifest(run, *, verify=True)` — public helpers so app code can locate, link to, and re-fetch a stored manifest without re-implementing `_build_manifest_key` or parsing `manifest.manifest_uri`. `read_manifest` enforces the `MAX_MANIFEST_BYTES` cap and verifies the canonical hash by default. - `genblaze-core`: `StorageBackend.key_from_url(url)` — inverse of `get_durable_url`. Default raises `NotImplementedError` so backends opt in explicitly; the S3 implementation handles both raw-endpoint and `public_url_base` URL shapes and returns `None` for foreign URLs (different host, different bucket, malformed) so callers can route across backends without `try/except`. ### Changed - All 11 provider connectors (`genblaze-openai`, `genblaze-google`, `genblaze-runway`, `genblaze-luma`, `genblaze-decart`, `genblaze-replicate`, `genblaze-elevenlabs`, `genblaze-stability-audio`, `genblaze-lmnt`, `genblaze-gmicloud`, `genblaze-nvidia`) now accept and forward `retry_policy=` to `super().__init__()`. Backwards-compatible — providers constructed without the kwarg behave identically to prior releases. ### Corrected - The [0.2.5] release-notes entry "exposes a `RetryPolicy` the caller can override per-provider" was aspirational — only utility functions and constants shipped in that release. The class itself ships in this release (see above). No code change is required for callers who only used the default behavior; callers who tried to import `RetryPolicy` from `genblaze_core.providers` in 0.2.5 will now find it. ### Fixed - `genblaze-core`: `ObjectStorageSink.write_run` now populates `manifest.manifest_uri` even when the manifest object already exists in the backend. Previously the assignment was inside the `if not exists:` branch, so retries (or any second `write_run` call against the same run) left the in-memory `Manifest.manifest_uri` as `None`, breaking pointer-mode embedders downstream. The durable URL is now set unconditionally whenever the manifest is present in storage. - `genblaze-core`: `StepRetriedEvent` now reaches `Pipeline.stream()` / `astream()` consumers. `_install_progress_tracer` previously wrapped only `on_progress`, so retry events fired by `BaseProvider._emit_retry` never propagated to the queue. UIs subscribed to the stream now see retry attempts in real time as advertised by the 0.3.2 schema. `Pipeline.run()` / `arun()` also gain an `on_retry=` kwarg for synchronous side-effects (metrics, logs); the same event continues to flow through the stream. - `genblaze-core`: JPEG, WebP, MP3, FLAC, and AAC inline embed are now atomic (temp file + `os.replace`). A crash mid-save no longer corrupts the source file in any handler. - `genblaze-core`: `WavHandler` explicitly rejects RF64 / BW64 / RIFX variants with a clear `EmbeddingError` instead of silently misparsing them as RIFF. - `genblaze-core`: lossless WebP sources are no longer silently re-encoded as lossy when the caller doesn't pass `lossless=True`. VP8X containers with leading metadata chunks (ICCP, EXIF, XMP) are now correctly detected by walking RIFF chunks instead of peeking at a fixed offset. - `genblaze-core`: PNG embed now patches chunks directly instead of re-encoding through Pillow. Ancillary chunks (`pHYs`, `gAMA`, `cHRM`, `sRGB`, `bKGD`, `tIME`, `iCCP`, plus private/custom chunks) survive embed → extract verbatim. Also avoids decoding pixel data, faster for large images. - `genblaze-core`: JPEG/WebP extract walks every XMP packet in the file instead of returning on the first one. Files with leading non-genblaze XMP (Photoshop, Lightroom) now correctly surface a later genblaze packet rather than failing with a misleading "no manifest" error. - `genblaze-core`: FLAC and AAC extract apply `MAX_MANIFEST_BYTES` cap before parsing to prevent OOM on hostile metadata payloads, matching the existing guard in WAV/MP4. - `genblaze-core`: `MAX_FILE_BYTES` (500 MB) size cap now applies to PNG inputs (was bypassed because PNG handler used Pillow's lazy loader). ### Performance - `genblaze-core`: `SmartEmbedder.embed()` skips the JSON serialize → parse → pydantic-revalidate round-trip when applying a no-redaction policy. Hot-path win for large manifests. - `genblaze-core`: PNG embed bypasses Pillow's pixel decode/encode cycle via direct chunk-level patching — typically 5-50× faster for non-trivial images depending on size. ### Refactor - `genblaze-core`: pointer-mode embed in `SmartEmbedder` now delegates to `SidecarHandler.embed(policy=...)` rather than reimplementing the atomic write inline. - `genblaze-core`: removed `Mp4Handler._atomic_write_bytes` (subsumed by the shared `atomic_write` context manager). ### Docs - README: rewrote the provenance bullet to drop "no bolt-on signing" framing and link the new trust-modes page. Manifests are described as "SHA-256-bound" and "tamper-evident in trusted storage" — honest about what the integrity hash proves and where signing/C2PA fit on the roadmap. ## [0.2.6] - 2026-04-24 ### Released package versions - Code-change bumps: `genblaze-core` 0.2.5, `genblaze-gmicloud` 0.2.5. - Untouched (no republish): every other Python and npm package. ### Added - `genblaze-core`: `providers/probe.py` — model-probe contract for runtime capability discovery against a live provider, plus a conformance test suite (`tests/conformance/test_provider_contract.py`) that every provider adapter must pass. - `genblaze-core`: `providers/params.py` — shared parameter-standardization hooks (canonical → native rewriting at the provider boundary). - `genblaze-core`: `models/voice.py` — first-class `Voice` model for TTS providers. - `genblaze-core`: `Pipeline` gains `batch_items` / `batch_raise` / `estimated_cost` / `raise_on_failure` — execution controls covered by four new dedicated test files. - `genblaze-core`: `exceptions.py` expanded with new typed errors aligned to the probe + standardization contracts. - `genblaze-gmicloud`: model registries fully reconciled with live GMICloud catalog — image/audio/video spec rewrites, new `models/voices.py` (172 lines) for the audio voice catalog, standardization-hook tests. - Tooling: `tools/probe_models.py` and `tools/gen_model_matrix.py` for generating the model-status matrix from live API probes (not part of any published package; repo-only utilities). - Docs: `docs/reference/model-matrix.md`, `docs/reference/model-probe-status.json`. ## [0.2.5.post1] - 2026-04-24 ### Fixed - `genblaze` 0.3.1: corrects the `genblaze-nvidia` pin in the `nvidia` extra and the `video`/`image`/`audio`/`all` bundles. 0.3.0 pinned `genblaze-nvidia>=0.1.0,<0.2` but nvidia actually shipped at 0.2.0, which made `pip install "genblaze[nvidia]"` unresolvable. Widened to `>=0.2.0,<0.3`. ## [0.2.5] - 2026-04-24 ### Released package versions - **New on PyPI (first publish):** `genblaze-nvidia` 0.2.0 (NIM / build.nvidia.com adapters for video, image, audio, chat — aligned with release train), `genblaze-cli` 0.2.0 (`genblaze` CLI script for `extract` / `verify` / `replay` / `index`). - **New minor on PyPI:** `genblaze` (umbrella) 0.3.0 — now ships real code that lazily re-exports `genblaze_core`'s public API, so `from genblaze import Pipeline` works. `genblaze_core` stays the canonical import path in docs. Also exposes the `nvidia` extra and adds NVIDIA to the `video` / `image` / `audio` / `all` curated bundles. - Code-change bumps from retry-policy unification: `genblaze-core` 0.2.4, `genblaze-gmicloud` 0.2.4, `genblaze-google` 0.2.3, `genblaze-openai` 0.2.3, `genblaze-replicate` 0.2.2, `genblaze-runway` 0.2.2, `genblaze-decart` 0.2.2, `genblaze-elevenlabs` 0.2.2, `genblaze-lmnt` 0.2.2, `genblaze-luma` 0.2.2, `genblaze-stability-audio` 0.2.2. - `@genblaze/spec` (npm) 0.3.2 — adds `events/v1/step-retried.schema.json` and updates the `stream-event` union for the new event. - Untouched (no republish): `genblaze-s3` 0.2.3, `genblaze-langsmith` 0.2.1. ### Added - `genblaze-core`: `genblaze_core.providers.retry` — unified retry policy module. Every provider adapter now delegates transient-error classification + exponential backoff to this shared surface instead of rolling its own. Consistent behavior across GMICloud, OpenAI, Google, Replicate, Runway, Luma, Decart, ElevenLabs, LMNT, Stability Audio, NVIDIA. Exposes a `RetryPolicy` the caller can override per-provider. - `genblaze-core`: `BaseProvider` retry plumbing baked into `invoke` / `ainvoke` / `resume` / `aresume`. Also surfaces retry attempts as new `StepRetried` stream events for observability. - `genblaze-nvidia` (new package): video (Cosmos), image (Flux, SDXL, etc.), audio (TTS, Parakeet ASR), and chat (OpenAI-compatible NIM endpoints) providers for NVIDIA's NIM / build.nvidia.com platform. Ships with 1146 lines of tests. - `genblaze-cli` (first public publish): the `genblaze` command-line tool with `extract`, `verify`, `replay`, and `index` subcommands for manifest operations. Script entry point: `genblaze`. - `genblaze` (umbrella) 0.3.0: now ships a real Python package (was an empty metapackage). Re-exports the top-level public surface of `genblaze_core` lazily, so `from genblaze import Pipeline` works after `pip install genblaze`. `genblaze_core` remains the canonical import path used in docs and examples. Submodules (`genblaze_core.media`, `genblaze_core.canonical`) and provider adapters (`genblaze_openai`, `genblaze_google`, …) are not re-exported — import those from their own packages. Ships `py.typed` for static type-checker support. - `@genblaze/spec`: new `events/v1/step-retried.schema.json` schema + TS type for the retry event. ## [0.2.4] - 2026-04-24 ### Released package versions - Code-change bumps: `genblaze-core` 0.2.3, `genblaze-gmicloud` 0.2.3, `genblaze-google` 0.2.2, `genblaze-openai` 0.2.2. - `@genblaze/spec` (npm) 0.3.1 — minor schema + TS-type touch-up. - Untouched since last wave (no republish): `genblaze-s3` 0.2.3, `genblaze-replicate` 0.2.1, `genblaze-decart` 0.2.1, `genblaze-elevenlabs` 0.2.1, `genblaze-langsmith` 0.2.1, `genblaze-lmnt` 0.2.1, `genblaze-luma` 0.2.1, `genblaze-runway` 0.2.1, `genblaze-stability-audio` 0.2.1, `genblaze` (umbrella) 0.2.3 — its dep ranges already satisfy the new versions. ### Added - `genblaze-gmicloud`: HTTP client injection in `GMICloudBase` — providers accept an optional `client=` for deterministic tests and custom transport (proxies, retries, observability). New `test_base_client_injection.py` covers the contract. Part of the gmi-hardening exec plan. - `genblaze-google`: `_errors.py` module with explicit `ProviderErrorCode` mapping for Gemini SDK exceptions. New `test_error_mapping.py`. - `genblaze-openai`: `test_chat.py` covers the `chat()` / `achat()` wrapper paths end-to-end. - `genblaze-core`: new `Modality` enum member. ### Fixed - `genblaze-core`: OTel tracer event-type check no longer misclassifies a subset of events; poll-cache cleanup test hardened. - `genblaze-gmicloud`: media URL extraction handles a previously-missed envelope shape; chat client initialization condition corrected. ### Added - `genblaze-core`: `ChatMessage`, `ToolCall`, `ChatResponse` in `genblaze_core.models.chat` — uniform return shape for the standalone chat wrappers below. Not part of the manifest wire protocol. - `genblaze-openai`, `genblaze-google`, `genblaze-gmicloud`: standalone `chat()` / `achat()` wrappers around each provider's chat / completion endpoint. Sit outside the Pipeline / Step machinery — convenience for callers driving media steps from an LLM. See `docs/features/llm-calls.md`. - `genblaze-core`: `ProviderErrorCode.CONTENT_POLICY` — new normalized code for safety / content-policy refusals. Deterministic, never retryable. Wire schema (`step.schema.json`) and TS types regenerated. Shared `classify_api_error()` detects the common keywords; per-provider mappers (Google, GMICloud) prioritize policy detection over status-code classification so a 400 policy refusal isn't misclassified as `INVALID_INPUT`. - `genblaze-gmicloud`: `base_url=` ctor kwarg (+ `GMI_BASE_URL` env) and `http_client=` kwarg on all three provider classes. Enables staging / proxy / VPC deployments and lets multi-modality pipelines share one `httpx.Client` across video / image / audio providers. Externally supplied clients are never closed by `close()`. - Root README: provider × modality capability matrix now includes a "Chat (LLM)" column — single-page answer to "which connector does what?". ### Fixed - `genblaze-gmicloud`: `GMICloudImageProvider.fetch_output` now emits one `Asset` per URL in the `media_urls` envelope. Previously discarded all but the first when `number_of_images > 1`, silently returning one asset for an N-asset bill. The new `extract_media_urls()` helper exposes the full list; `extract_media_url()` remains as a single-output thin wrapper for the video and audio paths. ### Changed - `genblaze-core`: `ModelRegistry` now logs dropped non-allowlisted params at `INFO` (was `DEBUG`). Silent allowlist drops now surface in typical production logs without WARNING-level noise. - `genblaze-gmicloud` README: removed the SDK email/password auth claim (only API-key auth is implemented). Added a naming-reference table, the `base_url` / `http_client` injection pattern, LLM-access surface, and the canonical status-check idiom for reading `step.assets` safely. - `GMICloudAudioProvider` docstring now documents that audio input is a reference voice for cloning, not a source for speech-to-text. STT is out of scope for this class. ## [0.2.3] - 2026-04-23 ### Released package versions - **New:** `genblaze` 0.2.3 — umbrella metapackage. `pip install genblaze` installs `genblaze-core` + `genblaze-s3` by default; provider adapters are opt-in extras (e.g. `pip install "genblaze[gmicloud,video]"`). Curated bundles: `[video]`, `[image]`, `[audio]`, `[all]`. - Code-change bumps: `genblaze-core` 0.2.2, `genblaze-replicate` 0.2.1, `genblaze-s3` 0.2.3. - Metadata-only force-bumps (author + Homepage fill-in, no code changes): `genblaze-gmicloud` 0.2.2, `genblaze-openai` 0.2.1, `genblaze-google` 0.2.1, `genblaze-decart` 0.2.1, `genblaze-elevenlabs` 0.2.1, `genblaze-langsmith` 0.2.1, `genblaze-lmnt` 0.2.1, `genblaze-luma` 0.2.1, `genblaze-runway` 0.2.1, `genblaze-stability-audio` 0.2.1. - `@genblaze/spec` (npm) 0.3.0 — minor bump for new events schema namespace. ### Added - `genblaze` metapackage for discoverable `pip install genblaze` UX. - Every published Python package now has `authors` and `Homepage` URL populated, so `pip show` and the PyPI project page render correctly. - Root README: "Install" section with package-to-import mapping table (resolves hyphen/underscore confusion for new users). ### Added - `genblaze-core`: `genblaze_core.pipeline` package now uses PEP 562 module-level `__getattr__` for lazy attribute resolution (`Pipeline`, `StepCache`, `PipelineResult`, `StepCompleteEvent`). `from genblaze_core.pipeline import Pipeline` still works; loading the heavy `pipeline.py` module is deferred until first access. Lets `observability.events` import from `pipeline.result` without a circular import through `pipeline.py`. - `libs/spec/ts/genblaze.d.ts` — TypeScript type declarations generated from the JSON Schemas. Eliminates hand-rolled type drift in downstream TS consumers (studio UIs, Node backends). Regenerate via `make ts-types`. Phase 1a: committed in-repo; phase 1b will publish `@genblaze/spec` to npm. See `libs/spec/README.md`. - `libs/core/tests/unit/test_spec_conformance.py` — bidirectional conformance tests between Pydantic models and `libs/spec/schemas/`. Catches field-set drift, enum drift, missing descriptions. Runs under `make test`. - CI `ts-types` job — drift guard that regenerates the TS types and fails if the committed file would change. - `genblaze-core`: `StreamEvent` is now a Pydantic discriminated union — ten per-variant classes (`PipelineStartedEvent`, `PipelineCompletedEvent`, `PipelineFailedEvent`, `StepStartedEvent`, `StepProgressEvent`, `StepCompletedEvent`, `StepFailedEvent`, `AgentIterationStartedEvent`, `AgentIterationEvaluatedEvent`, `AgentCompletedEvent`) under a common `StreamEvent` base. `AnyStreamEvent` + `StreamEventAdapter` (a `TypeAdapter`) parse inbound event dicts into the correct variant via the `type` discriminator. - `libs/spec/schemas/events/v1/*.schema.json` — Draft 2020-12 JSON Schemas for all ten variants plus a parent `stream-event.schema.json` with `oneOf` + `discriminator`. Generated TypeScript types now include the full discriminated-union `StreamEvent` surface. - Conformance coverage extended to event variants (field-set parity, description required, `additionalProperties: false`, `type` const matches Pydantic `Literal`, round-trip validation, discriminator completeness). ### Changed - `libs/spec/schemas/manifest/v1/policy.schema.json`: `prompt_visibility` enum now includes `"encrypted"` to match the Pydantic `PromptVisibility` enum. Schema-only fix; Pydantic acceptance was already unchanged. - `Asset.url` field description now documents the durable-URL-is-the- handle invariant (no separate storage-key field; parse key from URL if needed) so the contract flows into JSON Schema and generated TypeScript JSDoc. - **Breaking (pre-1.0)**: `StreamEvent(type=..., ...)` with a raw discriminator string no longer validates — construct the specific variant class (`StepStartedEvent(...)`, etc.). `isinstance(ev, StreamEvent)` still narrows all variants; per-variant narrowing via `isinstance(ev, StepFailedEvent)` or `ev.type == "step.failed"` is now supported and produces precise field types under pyright/mypy. Agent events flatten their former `data` dict into proper fields — e.g. `event.data["iteration"]` is now `event.iteration`. - **Breaking wire format for `step.failed`**: the serialized event no longer carries a `message` key — the failure reason lives on a dedicated `error` field. Previously the failure string was duplicated into both `message` and `error`. Webhook / log / SSE consumers that key on `message` for failed steps should switch to `error`. - `StreamEvent.to_dict()` now delegates to `model_dump(mode="json", exclude_none=True)` and always emits `type`+`timestamp` plus the variant's declared fields. In-process-only fields (`step`, `result`) remain on the Python object but are excluded from the wire shape; derived `step_status`/`manifest_hash`/`run_status`/`error` fields are pre-populated at construction so consumers don't lose context. - `StepCompletedEvent.step` / `StepFailedEvent.step` are now typed as `Step | None` (was `Any`); `PipelineCompletedEvent.result`, `PipelineFailedEvent.result`, `AgentIterationEvaluatedEvent.result`, and `AgentCompletedEvent.result` are typed as `PipelineResult | None`. Pydantic passes instances through by identity (`revalidate_instances="never"`) — no copy, no perf regression. IDE autocomplete and static type checkers now narrow `event.step.assets` and `event.result.manifest` correctly. ## [0.2.2] - 2026-04-23 ### Released package versions - `genblaze-core` 0.2.1, `genblaze-gmicloud` 0.2.1, `genblaze-s3` 0.2.2. - First-time PyPI releases at 0.2.0: `genblaze-decart`, `genblaze-elevenlabs`, `genblaze-langsmith`, `genblaze-lmnt`, `genblaze-luma`, `genblaze-replicate`, `genblaze-runway`, `genblaze-stability-audio`. All pin `genblaze-core>=0.2.0,<0.3`. - `genblaze-openai` and `genblaze-google` remain at 0.2.0 — no code changes since the 0.2.0 release. ### Added - `genblaze-core`: `ModelSpec.deprecated_aliases` — old model ids keep resolving but emit a `DeprecationWarning` pointing to the canonical slug. Drop the alias after one minor version. - `genblaze-core`: `ModelRegistry.resolve_canonical(model_id)` — returns the canonical slug the upstream API expects (or passes caller input through when only the fallback spec matches). Use in connectors with case-sensitive upstream APIs instead of poking at `spec.model_id`. - `genblaze-gmicloud`: `extract_media_url()` envelope parser covering both the live `outcome.media_urls[0].url` shape and the legacy flat `*_url` keys. Image modality also falls back to `outcome.thumbnail_image_url`. - `genblaze-gmicloud`: legacy-slug and error-unwrap test coverage. ### Changed - `genblaze-gmicloud`: all image and video model ids rewritten to the live lowercase slugs the request-queue API actually accepts (`seedream-5.0-lite`, `veo3`, `wan2.6-i2v`, `kling-text2video-v1.6-pro`, etc.). Old PascalCase ids (`Seedream-5.0-Lite`, `Veo3`, `Wan-2.6-I2V`, …) still resolve via `deprecated_aliases` and will be removed in 0.4. - `genblaze-gmicloud`: submits now send the canonical slug on the wire, not the caller-supplied string — matters because the GMICloud request queue is case-sensitive. - `genblaze-gmicloud`: JSON error bodies (`{"error": "..."}`) are unwrapped before being surfaced, replacing the confusing double-encoded `GMICloud submit failed (500): {"error":"Backend error (400)..."}` message. - `genblaze-gmicloud`: new model families registered — reve-create/edit/remix (+ fast variants), bria-fibo-* (blend/relight/restore/genfill/eraser), pixverse-v5.6 i2v/t2v/transition, wan2.6/2.7 t2v/i2v/r2v. ### Fixed - `genblaze-gmicloud`: `fetch_output` parsed the wrong envelope path; live API returns `outcome.media_urls[0].url` but the connector read `outcome.*_url` keys only. Mock fixtures were aligned with the real shape in the same change. ## [0.2.1] - 2026-04-23 ### Changed - `genblaze-s3`: dependency pin widened to `genblaze-core>=0.1.0,<0.3` so the S3 backend is installable alongside core 0.2.x (ModelRegistry release). No code changes; s3 is compatible with both 0.1.x and 0.2.x core. ## [0.2.0] - 2026-04-23 ### Added - `genblaze-core`: `ModelRegistry` + `ModelSpec` — declarative, user-extensible per-model configuration across every provider connector. New public surface at `genblaze_core.providers`: `ModelSpec`, `ModelRegistry`, `PricingContext`, `PricingStrategy`, pricing helpers (`per_unit`, `per_input_chars`, `per_output_second`, `per_response_metric`, `tiered`, `bucketed_by_duration`, `by_param`, `by_model_and_param`, `first_match`), input routers (`route_images`, `route_audio`, `route_by_media_type`, `route_keyframes`, `chain_routers`), constraints (`requires_together`, `mutually_exclusive`, `required_one_of`, `implies`), and param schemas (`IntSchema`, `EnumSchema`, `StringSchema`, `BoolSchema`, `FloatSchema`, `ArraySchema`). - `genblaze-core`: `BaseProvider.create_registry()`, `models_default()`, `prepare_payload(step)`, and `models=` ctor kwarg. Registry pricing is applied automatically after `fetch_output()` when `step.cost_usd` is unset. - All 12 provider connectors (`gmicloud` video/image/audio, `openai` dalle/tts/sora, `google` imagen/veo, `elevenlabs` tts/sfx, `replicate`, `runway`, `luma`, `stability-audio`, `lmnt`, `decart` image/video) migrated to `ModelRegistry`. Inline `_PRICING` / `_MODELS` / `forward_keys` dicts removed; data now lives in spec-building functions. - Docs: `docs/features/model-registry.md`, README quickstart for custom pricing/models, runnable `examples/custom_model_registry.py` (no API keys needed). ### Fixed - `genblaze-s3`: credentials preserved across region auto-detection. Previously `_reconfigure_for_region` tried to recover credentials from `boto3.client.meta.config.__dict__`, which doesn't hold them — the reconfigured client silently lost creds and failed mid-upload with `NoCredentialsError`. Credentials are now persisted on the backend at construction and threaded through the rebuild. - `genblaze-s3`: region preflight runs on `get`/`exists`/`delete`/`get_url` (presigned), not just `put`. Previously the first `exists()` call — routinely made by `ObjectStorageSink` before `put` — could skip verification and hit the wrong region. - `genblaze-s3`: region-redirect endpoint rewrite now only fires for B2 endpoints. AWS S3 / R2 / MinIO users hitting a 301 are no longer silently retargeted at `s3.{region}.backblazeb2.com`. - `genblaze-s3`: explicit `ChecksumSHA256` in `extra_args` now routes through `put_object` (single-PUT). Whole-object SHA-256 is only valid for single-part uploads; the previous path would have let `upload_fileobj` take the multipart code path with an invalid header. - `genblaze-core`: `ObjectLockConfig` rejects naive datetimes with a clear error. S3's handling of naive timestamps is ambiguous and we refuse to silently accept multi-year retention with a wrong anchor. Past retention still allowed but logs a loud warning. - Regression tests added for every item above. ### Added - `genblaze-core`: `ObjectLockConfig` dataclass + `ObjectStorageSink(manifest_lock=...)` parameter. Applies B2 Object Lock retention (GOVERNANCE or COMPLIANCE) to manifest uploads — turns genblaze's canonical-hash provenance into an immutable, audit-grade on-disk artifact. GOVERNANCE is the default; COMPLIANCE logs a prominent warning at construction because its retention cannot be shortened, even by the account root. See `docs/features/object-storage.md` for the full recipe. - `genblaze-s3`: multipart uploads via `upload_fileobj` + `TransferConfig` — assets >16 MB now split into 16 MB parts uploaded 4-way in parallel, each part individually retryable. Transforms multi-GB video uploads from a lottery ticket into a reliable operation. - `genblaze-s3`: per-part SHA-256 integrity via `ChecksumAlgorithm=SHA256` on every upload so B2 server-side-verifies transfer integrity. - `genblaze-s3`: `S3StorageBackend.ensure_lifecycle_defaults()` helper that applies idempotent `AbortIncompleteMultipartUpload` (7 days) and `NoncurrentVersionExpiration` (30 days) rules. Called automatically by `for_backblaze(auto_lifecycle=True)` (default). - `genblaze-s3`: automatic bucket-region auto-detection — the first `put()`/`exists()` call runs a HeadBucket preflight and transparently reconfigures the client if the bucket lives in a different region. - `genblaze-s3`: `StorageBackend.put()` gains an `extra_args` passthrough for boto3-style `ExtraArgs` (Cache-Control, SSE, Object Lock, etc.). - `genblaze-core`: immutable Cache-Control on CONTENT_ADDRESSABLE uploads (`public, max-age=31536000, immutable`), shorter private TTL on HIERARCHICAL. Unlocks the B2 + Cloudflare Bandwidth Alliance zero-egress delivery pattern documented in `docs/features/object-storage.md`. - Docs: "Serving media at zero egress: B2 + Cloudflare" recipe. ### Changed - `genblaze-s3`: `for_backblaze()` raises a clear `ValueError` when both `B2_KEY_ID`/`B2_APP_KEY` env vars and explicit `key_id`/`app_key` are missing — prevents opaque mid-upload `NoCredentialsError`. - `genblaze-s3`: `BotoConfig` now pins `request_checksum_calculation="when_required"` / `response_checksum_validation="when_required"` so boto3 never sends `x-amz-sdk-checksum-algorithm` trailer headers that older B2 deployments and other S3-compat endpoints reject. Genblaze sets SHA-256 explicitly. - `genblaze-s3`: default `max_pool_connections` bumped to 20 to accommodate concurrent multipart uploads. ## [0.1.0] - 2026-04-22 ### Added - Core models: `Asset`, `Step`, `Run`, `Manifest` - Fluent builders: `StepBuilder`, `RunBuilder`, `ManifestBuilder` - Canonical JSON serialization with SHA-256 hashing - Unicode NFC normalization in canonical JSON - New enums: `RunStatus`, `StepType`, `ProviderErrorCode` - Asset fields: `width`, `height`, `duration` - Step fields: `step_type`, `model_version`, `model_hash`, `seed`, `inputs`, `provider_payload`, `retries`, `error_code` - Run fields: `status`, `project_id` - Manifest fields: `manifest_uri`, `encryption_scheme`, `signature` - `EmbedPolicy` model with `Manifest.to_embed_json()` for redaction - PNG media handler (iTXt chunk embedding) - JPEG and WebP media handlers (XMP-based embedding) - Sidecar media handler (JSON alongside media) - `SmartEmbedder` with auto-fallback to sidecar - `get_handler()` media handler registry - `MediaCapability` dataclass for handler introspection - `Runnable[In, Out]` ABC with `|` composition operator - `BaseProvider` with submit/poll/fetch_output lifecycle - `ReplicateProvider` adapter (stores `provider_payload`) - `genblaze-openai` `DalleProvider` expanded to OpenAI's full image lineup: `gpt-image-2` (free-form sizing), `gpt-image-1.5`, `gpt-image-1`, `gpt-image-1-mini`, alongside `dall-e-3` / `dall-e-2` - `/v1/images/edits` support in `DalleProvider` — routed automatically when `step.inputs` is non-empty; accepts `file://` and `https://` inputs, optional `mask`, and multi-image composites - New param passthroughs in `DalleProvider`: `output_format` (png/jpeg/webp), `output_compression`, `moderation`, `input_fidelity`, `mask` - Registry-driven model metadata (`_ImageModelSpec`) replaces scattered size/pricing/format dicts; unknown models (`chatgpt-image-latest`, dated snapshots) pass through with `cost_usd=None` - Provider error classification via `ProviderErrorCode` - `Pipeline` fluent API for multi-step generation - `PipelineResult` with `.save()` method and tuple unpacking support - Pipeline: `step_type` parameter, `sink` parameter on `.run()` - `ParquetSink` for structured run data output - Parquet sink split into `runs/`, `steps/`, `assets/` tables with idempotency - Parquet partitioning by `dt=/tenant_id=/modality=/provider=` - `pyarrow` as optional `[parquet]` extra - `StepSpan` timing context manager with `run_id`, `step_id`, `retries`, `cost` fields - `StructuredLogger` for JSON log events; `.with_context()` for correlation IDs - CLI: `extract`, `verify`, `replay`, `index` commands - Builder methods: `.step_type()`, `.seed()`, `.model_version()`, `.model_hash()`, `.input_asset()`, `.project()`, `.status()` - JSON Schema specifications (v1) - GitHub Actions CI with Python 3.11/3.12/3.13 matrix - Pre-commit hooks configuration - `py.typed` PEP 561 marker - Code coverage configuration (70% minimum) - Shared test fixtures via `conftest.py` - README.md with install, quickstart, and architecture docs