Skip to content

Commit 15d3c61

Browse files
MajorTalclaude
andcommitted
feat(sdk,mcp): surface v1.49 gateway image variants
Adds typed AssetVariant + image-variant fields on AssetRef (width_px, height_px, blurhash, variant_spec_version, display_url, display_immutable_url, variants). Threads fields end-to-end via ResolvedAssetRef → AssetManifestEntry → buildAssetRef so r.assets.put and r.project(id).apply({ assets: { put: [...] } }) produce identical refs. Convenience getters thumbUrl/displayUrl are undefined for non-image AssetRefs (TypeScript narrows; non-images can't accidentally render as broken thumbnails). New imgTagWithSrcSet helper emits <picture> with WebP-only sources; throws at call time on missing sizes OR missing variants — no silent fallback. imgTag defaults src to display_url (HEIC-aware) and opportunistically emits width/height. MCP assets_put tool description mentions the new fields; handler surfaces Dimensions / Blurhash / Display URL / Variants in human output. CLI is JSON-only by design and passes the new fields through verbatim. Closes openspec change asset-image-variants-client (in run402-private). Refs run402#392. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 2bde2e1 commit 15d3c61

10 files changed

Lines changed: 930 additions & 26 deletions

File tree

CHANGELOG.md

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,31 @@
11
# Changelog
22

3-
All notable changes to `@run402/sdk`, `run402` (CLI), `run402-mcp`, and `@run402/functions`. Versions are kept in lockstep across the four packages.
3+
All notable changes to `@run402/sdk`, `run402` (CLI), and `run402-mcp`. Versions are kept in lockstep across the three packages in this repo. `@run402/functions` lives in the private gateway monorepo and publishes on its own cadence.
4+
5+
## 2.3.0 — unreleased
6+
7+
Surfaces the v1.49 gateway image-variant pipeline ([run402#392](https://github.com/kychee-com/run402/issues/392), parent change: [`asset-image-variants` in run402-private](https://github.com/kychee-com/run402-private/tree/main/openspec/changes/asset-image-variants)). Additive, non-breaking — old clients silently ignore the new fields.
8+
9+
### Added
10+
11+
- **`AssetVariant` interface** in `@run402/sdk` (`sdk/src/namespaces/assets.types.ts`). Shape: `{ url, cdn_url, width_px, height_px, format: 'webp' | 'jpeg', sha256 }`. Used by the new `AssetRef.variants` map.
12+
- **Typed image-variant fields on `AssetRef`**`width_px`, `height_px`, `blurhash`, `variant_spec_version`, `display_url`, `display_immutable_url`, `variants?: { thumb?, medium?, large?, display_jpeg? }`. All optional. Present only for image uploads (jpeg/png/webp/heic/heif ≥320×320) against a v1.49+ gateway. Threaded end-to-end through `ResolvedAssetRef``AssetManifestEntry``buildAssetRef`, so the same fields appear whether you upload via `r.assets.put(...)` or `r.project(id).apply({ assets: { put: [...] } })`.
13+
- **`AssetRef.thumbUrl`** convenience getter — `variants.thumb.cdn_url ?? displayUrl` for image refs, `undefined` for non-images. Single field for grid thumbnails; TypeScript narrows so a picker that does `<img src={pdfRef.thumbUrl}>` is a compile error.
14+
- **`AssetRef.displayUrl`** convenience getter — `display_url ?? cdn_url` for image refs, `undefined` for non-images. HEIC sources transparently get the JPEG transcode.
15+
- **`AssetRef.imgTagWithSrcSet(opts)`** helper — emits a `<picture>` with a WebP-only `<source>` (three sizes: 320w / 800w / 1920w) and `display_url` as the `<img>` fallback. Throws at call time on (a) missing/empty `opts.sizes` (browsers over-fetch the largest candidate without it), or (b) missing `variants` (non-image / sub-320 / pre-v1.49 ref) — no silent fallback. AVIF deferred from v1 (documented in JSDoc; `<picture>` type-precedence footgun).
16+
- **MCP `assets_put` human output** now surfaces `Dimensions: <w>×<h>`, `Blurhash: <hash>`, `Display URL` (when distinct from `cdn_url` — HEIC only), and a `Variants:` line listing kind + dimensions + format for each present variant.
17+
18+
### Changed
19+
20+
- **`AssetRef.imgTag(alt?)` defaults `<img src>` to `display_url ?? cdn_url`** (was `cdn_url`). Correct rendering for HEIC uploads without HEIC-aware caller code — for non-HEIC images `display_url === cdn_url`, so no behavior change there.
21+
- **`AssetRef.imgTag(alt?)` opportunistically emits `width`/`height` attributes** when both `width_px` and `height_px` are present on the ref. Eliminates Cumulative Layout Shift for image grids. Silently omits both attributes when either dimension is absent — never throws on absence.
22+
- **MCP `assets_put` tool description** updated to mention the new image fields and reference the SDK docs for the full AssetRef shape.
23+
24+
### Out of scope (deliberate carve-out)
25+
26+
- `@run402/functions` type updates — lives in `run402-private/packages/functions/` and co-evolves with the gateway via its own `/publish-functions` skill. The runtime returns the new fields regardless of which `@run402/functions` types are in use.
27+
- AVIF generation or AVIF-aware helpers — deferred at the gateway. When AVIF returns, it must land at all three sizes simultaneously or via a dedicated `imgTagHero()` helper.
28+
- On-demand `?w=N&fmt=webp` resize endpoint and project-configurable variant sizes.
429

530
## 2.2.0 — 2026-05-18
631

sdk/README.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,52 @@ const logo = await (await r.project(projectId)).assets.put("logo.png", { bytes }
130130

131131
`immutable: true` is the default since v1.45. The SDK always computes and sends the object SHA-256; pass `false` only when you specifically need mutable URL/cache semantics.
132132

133+
### Image variants (v1.49)
134+
135+
Image uploads (jpeg/png/webp/heic/heif) trigger automatic generation of three WebP variants — `thumb` 320w, `medium` 800w, `large` 1920w — plus dimensions, a blurhash placeholder, and (for HEIC/HEIF sources) a JPEG display variant. Everything ships on the returned `AssetRef`:
136+
137+
```ts
138+
const p = await r.project(projectId);
139+
const ref = await p.assets.put("hero.jpg", bytes, { contentType: "image/jpeg" });
140+
141+
// Image-conditional fields, undefined on non-image AssetRefs:
142+
ref.width_px; // 4032 — display-oriented (post-EXIF rotate)
143+
ref.height_px; // 3024
144+
ref.blurhash; // "LEHV6nWB2yk8pyo0adR*.7kCMdnj" — decode client-side for LQIP
145+
ref.variants?.thumb?.cdn_url; // 320w WebP — for grid thumbnails
146+
ref.variants?.medium?.cdn_url; // 800w WebP — for cards
147+
ref.variants?.large?.cdn_url; // 1920w WebP — for heroes
148+
149+
// SDK convenience fields, also undefined on non-images:
150+
ref.thumbUrl; // = variants.thumb.cdn_url ?? displayUrl (single-field thumbnail)
151+
ref.displayUrl; // = display_url ?? cdn_url (browser-renderable for any image)
152+
153+
// Render with responsive srcset (sizes is required):
154+
const html = ref.imgTagWithSrcSet({
155+
alt: "Hero",
156+
sizes: "(max-width: 800px) 100vw, 1920px",
157+
});
158+
// → <picture>
159+
// <source type="image/webp" srcset="<thumb> 320w, <medium> 800w, <large> 1920w" sizes="…">
160+
// <img src="<display_url>" alt="Hero" width="4032" height="3024" loading="lazy" decoding="async">
161+
// </picture>
162+
163+
// Quick thumbnail (TypeScript narrows thumbUrl on non-images):
164+
// <img src={ref.thumbUrl} alt={ref.key} loading="lazy" />
165+
```
166+
167+
HEIC/HEIF uploads (from iPhones) preserve the source bytes verbatim — `cdn_url` serves the original HEIC, and a JPEG display variant is generated automatically and surfaced at `display_url`. The `imgTag` / `imgTagWithSrcSet` helpers default the `<img src>` to `displayUrl` so apps render correctly without HEIC-specific code.
168+
169+
Foolproof guards keep non-images from rendering broken layouts:
170+
171+
- `thumbUrl` and `displayUrl` are `undefined` (not a fallback to `cdn_url`) on non-image AssetRefs — TypeScript narrows them, so a `<img src={pdfRef.thumbUrl}>` is a compile error rather than a broken thumbnail at runtime.
172+
- `imgTagWithSrcSet` throws at call time when `opts.sizes` is missing or empty (browsers over-fetch the largest candidate without it), AND when the AssetRef has no `variants` (use `imgTag()` instead — see the error message). No silent fallback.
173+
- `imgTag` opportunistically emits `width`/`height` attributes when present (eliminates CLS) and silently omits them on non-image refs.
174+
175+
Variants apply to BOTH write paths — single-shot `r.assets.put(...)` AND the unified apply hero `r.project(id).apply({ assets: { put: [...] } })` return the same `AssetRef` shape with variants populated.
176+
177+
AVIF was deferred from v1 — `<picture>` browsers select sources by `type` precedence, not best size, so a single 1920w AVIF would be picked for thumbnails by AVIF-capable browsers. AVIF, if it returns, will land at all three sizes simultaneously or via a separate `imgTagHero()` helper.
178+
133179
### Mixed apply — site + assets in one atomic activation
134180

135181
Drop a per-key asset put into the same release as your site files. Both promote inside the same activation transaction that flips `live_release_id`, so the asset URLs are live the moment the new release is. Source shorthand: bare strings, `Uint8Array`, or any other `ContentSource` (Blob, FsFileSource from `fileSetFromDir`, `{ data, contentType? }` wrapper). The SDK normalizer hashes once and dedups across slices — same SHA in `site` and `assets` uploads as a single byte stream.

0 commit comments

Comments
 (0)