@fungi.computer/darkroom 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,12 +1,31 @@
1
1
  import { z } from "zod";
2
2
  import * as Redacted from "effect/Redacted";
3
- import { buildProviderInput, getMediaModel, estimateImageMilliunits, normalizeMediaProviderOutput, mediaProviderWireSchema, } from "../index.js";
3
+ import { buildProviderInput, getMediaModel, estimateImageMilliunits, normalizeMediaProviderOutput, mediaProviderWireSchema, validateMediaGenerationSettings, } from "../index.js";
4
4
  import { MediaExecutionError, MediaSafetyFilterRefusal, } from "../execution-error.js";
5
5
  import { decodeMediaProviderErrorCategory, } from "@fungi.computer/agar/errors";
6
6
  import { estimateGatewayMilliunits, gatewayInput, isGatewayModel, } from "../gateway-models.js";
7
7
  import { GATEWAY_VIDEO_MAX_BYTES, gatewayOutputSchema, isMp4, } from "../gateway-wire.js";
8
8
  import { videoProviderAdapters } from "../video-providers/index.js";
9
9
  import { readResponseBytes } from "../index.js";
10
+ /** Quote the catalog defaults through the exact estimator used before dispatch. */
11
+ export function estimateMediaModel(model) {
12
+ const settings = validateMediaGenerationSettings({
13
+ model: model.id,
14
+ prompt: "x",
15
+ width: model.input.defaultWidth,
16
+ height: model.input.defaultHeight,
17
+ });
18
+ return {
19
+ /** An indicative quote, not an admitted generation receipt. */
20
+ kind: "estimate",
21
+ /** Provider cost before the host's billing policy. */
22
+ basis: "provider-cost",
23
+ /** Catalog defaults, a one-character prompt and no references. */
24
+ settings,
25
+ /** Estimated provider milliunits of a cent for these exact settings. */
26
+ costMilliunits: estimate(settings, []),
27
+ };
28
+ }
10
29
  export function estimate(settings, references) {
11
30
  const params = {
12
31
  width: settings.width,
@@ -25,15 +44,21 @@ export function estimate(settings, references) {
25
44
  }
26
45
  }
27
46
  // Workers AI prefixes upstream error messages with its numeric internal code.
28
- // Parse only that bounded identifier; the remaining message is never logged.
47
+ // Parse only that bounded identifier; printable prose is separately decoded.
29
48
  const providerFailureCode = z.lazy(() => z
30
49
  .string()
31
50
  .regex(/^\d{4,5}: /u)
32
51
  .transform((message) => Number(message.split(":", 1)[0]))
33
52
  .nullable()
34
53
  .catch(null));
54
+ // Observed in a real Flux replay: HTTP 400/code 3030. Match the complete
55
+ // bounded refusal, not 3030 itself; the code is not documented as transient.
56
+ const outputFlaggedMessage = z
57
+ .string()
58
+ .regex(/^(?:\d{4,5}: )?(?:AiError: ){0,3}Your output has been flagged\. Please choose another prompt \/ input image combination \([0-9a-f-]{36}\)$/u);
35
59
  // Recognize the refusal, not the generic Workers AI error number or prompt text.
36
60
  const safetyFilterMessage = z.lazy(() => z.union([
61
+ outputFlaggedMessage,
37
62
  z
38
63
  .string()
39
64
  .regex(/(?:^|:\s)(?:AiError:\s)?Input prompt contains NSFW content\b/iu),
@@ -78,16 +103,28 @@ function routedOptions(options, signal, input) {
78
103
  }
79
104
  /** Retain the original provider failure alongside its bounded code and category. */
80
105
  function providerFailure(model, cause) {
81
- const provider = {
82
- category: decodeMediaProviderErrorCategory(cause instanceof Error ? cause.name : undefined),
83
- code: providerFailureCode.parse(cause instanceof Error ? cause.message : undefined),
84
- };
85
- // Workers correlates this structured event with the request trace. Never log
86
- // the provider message/body: either can contain customer prompt content.
106
+ const category = decodeMediaProviderErrorCategory(cause instanceof Error ? cause.name : undefined);
107
+ const code = providerFailureCode.parse(cause instanceof Error ? cause.message : undefined);
108
+ // Observed 2026-10-27 in a real GPT Image 2 call from the Prints UI:
109
+ // "AiGatewayError: 2021: Insufficient AI Gateway credits" (no charge).
110
+ // Sources: ~/Botanical-cto/receipts/prints-e2e/REPORT.md:51 ("Follow-up 2")
111
+ // and ~/Botanical-cto/receipts/prints-e2e/gwfix/results.json (02:01 local;
112
+ // call at 2026-10-27T01:01:04.296Z). The error had only name and message;
113
+ // no HTTP status was recorded. This is observed wording, not a general
114
+ // Cloudflare code guarantee. Keep numeric classification at this owner.
115
+ const provider = code === 2021
116
+ ? { category, code, reason: "credits_exhausted" }
117
+ : { category, code };
118
+ const safeMessage = outputFlaggedMessage.safeParse(cause instanceof Error ? cause.message : undefined);
119
+ // The maintained Workers AI errors expose name/message, not a Response or
120
+ // cause chain. Only the complete, bounded refusal grammar above is safe to
121
+ // print. Unmodeled upstream prose/bodies can echo private request content;
122
+ // retain the original error (including any nested details) only as redacted.
87
123
  console.error("Media provider request failed", {
88
124
  model,
89
125
  providerCode: provider.code,
90
126
  category: provider.category,
127
+ providerMessage: safeMessage.success ? safeMessage.data : undefined,
91
128
  }, Redacted.make(cause));
92
129
  return cause instanceof Error &&
93
130
  safetyFilterMessage.safeParse(cause.message).success
@@ -145,15 +182,31 @@ export async function generate(ai, settings, references, signal, options, fetchR
145
182
  catch (cause) {
146
183
  throw new MediaExecutionError("normalization", { cause });
147
184
  }
148
- let providerValue;
149
- try {
150
- providerValue = await ai.run(settings.model, workersAiInput(providerInput), routedOptions(options, signal, {
185
+ const invoke = async () => {
186
+ return ai.run(settings.model, workersAiInput(providerInput), routedOptions(options, signal, {
151
187
  stream: providerInput.transport === "multipart",
152
188
  images: references.length,
153
189
  }));
190
+ };
191
+ let providerValue;
192
+ try {
193
+ providerValue = await invoke();
154
194
  }
155
195
  catch (cause) {
156
- throw providerFailure(settings.model, cause);
196
+ // Only 3040 is documented as capacity temporarily exceeded:
197
+ // https://developers.cloudflare.com/workers-ai/platform/errors/
198
+ // One product-call retry, never a test retry. Rebuild the multipart body
199
+ // inside invoke: a failed binding call may already have consumed it.
200
+ if (providerFailureCode.parse(cause instanceof Error ? cause.message : undefined) !== 3040 ||
201
+ signal?.aborted)
202
+ throw providerFailure(settings.model, cause);
203
+ try {
204
+ providerValue = await invoke();
205
+ }
206
+ catch (cause) {
207
+ // One terminal diagnostic for the product call, not one per attempt.
208
+ throw providerFailure(settings.model, cause);
209
+ }
157
210
  }
158
211
  try {
159
212
  return await normalizeMediaProviderOutput(mediaProviderWireSchema.parse(providerValue));
@@ -5,13 +5,13 @@ export type MediaImagePort = {
5
5
  info(input: ReadableStream<Uint8Array>): ReturnType<ImagesBinding["info"]>;
6
6
  /** Start the native resize and encode chain. */
7
7
  input(input: ReadableStream<Uint8Array>): {
8
- /** Apply the native scale-down transform. */
8
+ /** Apply the native aspect-preserving resize. */
9
9
  transform(options: {
10
10
  /** Image width in pixels. */
11
11
  width: number;
12
12
  /** Image height in pixels. */
13
13
  height: number;
14
- fit: "scale-down";
14
+ fit: "scale-down" | "contain";
15
15
  }): {
16
16
  /** Encode the transformed image through the native Images binding. */
17
17
  output(options: {
@@ -30,8 +30,8 @@ export type MediaImagePort = {
30
30
  };
31
31
  /**
32
32
  * Media's reference preparation on the Images binding: decode the real bytes,
33
- * scale an oversized image down to MAX_REFERENCE_DIMENSION as PNG, then decode
34
- * the output so its metadata describes the bytes actually sent.
33
+ * fit the selected model's reference size as PNG, then decode the output so
34
+ * its metadata describes the bytes actually sent.
35
35
  */
36
36
  export declare function createImagesReferenceFitter(images: MediaImagePort): ReferenceImageFitter;
37
37
  /**
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
- import { MAX_REFERENCE_BYTES, MAX_REFERENCE_DIMENSION, MediaSettingsError, viewDimensionSchema, } from "../index.js";
2
+ import { MAX_REFERENCE_BYTES, MediaSettingsError, viewDimensionSchema, } from "../index.js";
3
+ import { referenceImageTransform } from "../reference-image-size.js";
3
4
  /** The retained `read_image` encoding: JPEG at quality 80. */
4
5
  const VIEW_JPEG_QUALITY = 80;
5
6
  const metadata = z.lazy(() => z.object({
@@ -21,11 +22,11 @@ const stream = (bytes) => new ReadableStream({
21
22
  });
22
23
  /**
23
24
  * Media's reference preparation on the Images binding: decode the real bytes,
24
- * scale an oversized image down to MAX_REFERENCE_DIMENSION as PNG, then decode
25
- * the output so its metadata describes the bytes actually sent.
25
+ * fit the selected model's reference size as PNG, then decode the output so
26
+ * its metadata describes the bytes actually sent.
26
27
  */
27
28
  export function createImagesReferenceFitter(images) {
28
- return async (input) => {
29
+ return async (input, model) => {
29
30
  let bytes = input;
30
31
  let info;
31
32
  try {
@@ -35,15 +36,11 @@ export function createImagesReferenceFitter(images) {
35
36
  // The Images failure detail is never surfaced; the caller's bytes are.
36
37
  throw new MediaSettingsError("reference image is not a supported image");
37
38
  }
38
- if (info.width > MAX_REFERENCE_DIMENSION ||
39
- info.height > MAX_REFERENCE_DIMENSION) {
39
+ const transform = referenceImageTransform(model, info.width, info.height);
40
+ if (info.width !== transform.width || info.height !== transform.height) {
40
41
  const output = await images
41
42
  .input(stream(bytes))
42
- .transform({
43
- width: MAX_REFERENCE_DIMENSION,
44
- height: MAX_REFERENCE_DIMENSION,
45
- fit: "scale-down",
46
- })
43
+ .transform(transform)
47
44
  .output({
48
45
  format: "image/png",
49
46
  });
@@ -31,6 +31,8 @@ export type MediaFailureStage = Exclude<z.output<typeof mediaFailureStageSchema>
31
31
  export type MediaExecutionErrorOptions = ErrorOptions & {
32
32
  /** Safe provider facts, when the failure came from provider transport. */
33
33
  readonly provider?: MediaProviderDiagnostic | undefined;
34
+ /** Confirmed zero-unit customer settlement, never an upstream billing claim. */
35
+ readonly customerCharge?: "none" | undefined;
34
36
  };
35
37
  /** A bounded stage marker for failures that cross the Media HTTP boundary. */
36
38
  export declare class MediaExecutionError extends Error {
@@ -38,6 +40,8 @@ export declare class MediaExecutionError extends Error {
38
40
  readonly stage: MediaFailureStage;
39
41
  /** Safe provider facts only; the original cause remains private. */
40
42
  readonly provider: MediaProviderDiagnostic | undefined;
43
+ /** Unknown unless the execution owner confirmed zero-unit settlement. */
44
+ readonly customerCharge: "none" | "unknown";
41
45
  constructor(
42
46
  /** Execution stage where the failure occurred. */
43
47
  stage: MediaFailureStage, options?: MediaExecutionErrorOptions);
@@ -30,12 +30,15 @@ export class MediaExecutionError extends Error {
30
30
  stage;
31
31
  /** Safe provider facts only; the original cause remains private. */
32
32
  provider;
33
+ /** Unknown unless the execution owner confirmed zero-unit settlement. */
34
+ customerCharge;
33
35
  constructor(
34
36
  /** Execution stage where the failure occurred. */
35
37
  stage, options) {
36
38
  super(`Media ${stage} failed`, options);
37
39
  this.stage = stage;
38
40
  this.name = "MediaExecutionError";
41
+ this.customerCharge = options?.customerCharge ?? "unknown";
39
42
  this.provider =
40
43
  options?.provider === undefined
41
44
  ? undefined
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import * as z from "zod";
2
+ export { MAX_REFERENCE_DIMENSION } from "./reference-image-size.js";
2
3
  import { type MediaVideoReferenceRole, type MediaVideoModelId, type MediaVideoResolution, type MediaVideoAspectRatio } from "./model-catalog.js";
3
4
  export { mediaFailureStageSchema } from "./execution-error.js";
4
5
  export { MAX_MEDIA_PROMPT_LENGTH, getMediaModel, MAX_REFERENCE_COUNT, listMediaModels, mediaModelInputSchema, mediaModelSchema, mediaRequestedImageFormatSchema, mediaModels, mediaModelTierSchema, MediaSettingsError, mediaVideoModelIdSchema, mediaVideoResolutionSchema, mediaVideoAspectRatioSchema, mediaVideoReferenceRoleSchema, mediaVideoFormatSchema, mediaVideoSchema, } from "./model-catalog.js";
@@ -64,10 +65,10 @@ export type MediaProvider = (settings: MediaGenerationSettings, references: read
64
65
  /** Actual output encoding. */
65
66
  format: string;
66
67
  }>>;
67
- /** A decoded reference image, scaled within MAX_REFERENCE_DIMENSION. */
68
+ /** A decoded reference image, fitted for the selected model. */
68
69
  export type FittedReferenceImage = Omit<MediaReferenceBytes, "filename">;
69
- /** Decode one reference image and scale it within Media's dimension bound. */
70
- export type ReferenceImageFitter = (bytes: Uint8Array) => Promise<FittedReferenceImage>;
70
+ /** Decode and fit a reference: image long edge ≤512, video short edge ≥320. */
71
+ export type ReferenceImageFitter = (bytes: Uint8Array, model: string) => Promise<FittedReferenceImage>;
71
72
  /** The long-edge bounds a viewer may ask for when fitting an image to look at. */
72
73
  export declare const MIN_VIEW_DIMENSION = 256;
73
74
  /** Maximum long-edge size accepted by an image viewer, in pixels. */
@@ -80,8 +81,6 @@ export declare const viewDimensionSchema: z.ZodLazy<z.ZodNumber>;
80
81
  * a lossy JPEG, so a viewer can step the dimension down to meet a byte budget.
81
82
  */
82
83
  export type ImageViewFitter = (bytes: Uint8Array, maxDimension: number) => Promise<FittedReferenceImage>;
83
- /** Maximum reference width and height sent to inference, in pixels. */
84
- export declare const MAX_REFERENCE_DIMENSION = 512;
85
84
  /** Maximum decoded bytes per reference image. */
86
85
  export declare const MAX_REFERENCE_BYTES: number;
87
86
  /** Maximum retained still-image output bytes. */
@@ -123,7 +122,7 @@ export declare const mediaGenerationSettingsSchema: z.ZodObject<{
123
122
  }, z.core.$strict>;
124
123
  /** Caller settings before model defaults and refinements are applied. */
125
124
  export type MediaGenerationSettingsInput = z.input<typeof mediaGenerationSettingsSchema>;
126
- /** App selections contain paths; the host supplies the admitted Computer. */
125
+ /** A reference may select another Computer; identity never grants access. */
127
126
  export declare const mediaGenerationSelectionSchema: z.ZodObject<{
128
127
  model: z.ZodString;
129
128
  prompt: z.ZodString;
@@ -149,6 +148,7 @@ export declare const mediaGenerationSelectionSchema: z.ZodObject<{
149
148
  }>>;
150
149
  generateAudio: z.ZodOptional<z.ZodBoolean>;
151
150
  references: z.ZodOptional<z.ZodArray<z.ZodObject<{
151
+ computerId: z.ZodOptional<z.ZodString>;
152
152
  path: z.ZodString;
153
153
  role: z.ZodOptional<z.ZodEnum<{
154
154
  readonly first: "first-frame";
@@ -161,12 +161,14 @@ export declare const mediaGenerationSelectionSchema: z.ZodObject<{
161
161
  export type MediaGenerationSelection = z.output<typeof mediaGenerationSelectionSchema>;
162
162
  /** Validate caller settings and apply retained model defaults. */
163
163
  export declare const validateMediaGenerationSettings: (raw: MediaGenerationSettingsInput) => MediaGenerationSettings;
164
- /** Validate path-only selections without inventing a Computer identity. */
164
+ /** Validate selections without granting Computer authority. */
165
165
  export declare const validateMediaGenerationSelection: (raw: z.input<typeof mediaGenerationSelectionSchema>) => Readonly<Omit<MediaGenerationSettings, "references"> & {
166
166
  /** Host-authorized reference images. */
167
167
  references?: readonly Readonly<{
168
168
  /** Fixed destination or reference file path. */
169
169
  path: string;
170
+ /** Optional reference Computer; authorization remains host-owned. */
171
+ computerId?: string;
170
172
  /** Video-only reference role. */
171
173
  role?: MediaVideoReferenceRole;
172
174
  }>[];
@@ -247,8 +249,8 @@ export type MediaVideoSelection = Readonly<{
247
249
  export declare function validateMediaVideoSelection(raw: MediaVideoSelectionInput): MediaVideoSelection;
248
250
  /** Host-authorized reader for decoded reference images. */
249
251
  export type MediaReferenceReader = Readonly<{
250
- /** Authorize and read one reference into bounded decoded image bytes. */
251
- read: (reference: MediaReference) => Promise<MediaReferenceBytes>;
252
+ /** Authorize and fit one reference for the selected catalog model. */
253
+ read: (reference: MediaReference, model: string) => Promise<MediaReferenceBytes>;
252
254
  }>;
253
255
  /** Resolve only provider-accepted references through the caller's reader. */
254
256
  export declare const resolveMediaReferences: (settings: MediaGenerationSettings, reader: MediaReferenceReader) => Promise<readonly MediaReferenceBytes[]>;
package/dist/index.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import * as z from "zod";
2
2
  import { reportCatchFailure } from "./failure.js";
3
+ import { referenceImageSizeIsValid } from "./reference-image-size.js";
4
+ export { MAX_REFERENCE_DIMENSION } from "./reference-image-size.js";
3
5
  import { getMediaModel, MAX_REFERENCE_COUNT, MediaSettingsError, mediaVideoModelIdSchema, mediaVideoResolutionSchema, mediaVideoAspectRatioSchema, mediaVideoReferenceRoleSchema, MAX_MEDIA_PROMPT_LENGTH, } from "./model-catalog.js";
4
6
  export { mediaFailureStageSchema } from "./execution-error.js";
5
7
  export { MAX_MEDIA_PROMPT_LENGTH, getMediaModel, MAX_REFERENCE_COUNT, listMediaModels, mediaModelInputSchema, mediaModelSchema, mediaRequestedImageFormatSchema, mediaModels, mediaModelTierSchema, MediaSettingsError, mediaVideoModelIdSchema, mediaVideoResolutionSchema, mediaVideoAspectRatioSchema, mediaVideoReferenceRoleSchema, mediaVideoFormatSchema, mediaVideoSchema, } from "./model-catalog.js";
@@ -11,8 +13,6 @@ export const MAX_VIEW_DIMENSION = 512;
11
13
  export const viewDimensionSchema = z.lazy(() => z.number().int().min(MIN_VIEW_DIMENSION).max(MAX_VIEW_DIMENSION));
12
14
  /** Reference images in one video request: every verified video model's catalog value. */
13
15
  const MAX_VIDEO_REFERENCE_COUNT = 4;
14
- /** Maximum reference width and height sent to inference, in pixels. */
15
- export const MAX_REFERENCE_DIMENSION = 512;
16
16
  /** Maximum decoded bytes per reference image. */
17
17
  export const MAX_REFERENCE_BYTES = 8 * 1024 * 1024;
18
18
  /** Maximum retained still-image output bytes. */
@@ -74,12 +74,14 @@ export const mediaGenerationSettingsSchema = z
74
74
  .optional(),
75
75
  })
76
76
  .strict();
77
- /** App selections contain paths; the host supplies the admitted Computer. */
77
+ /** A reference may select another Computer; identity never grants access. */
78
78
  export const mediaGenerationSelectionSchema = mediaGenerationSettingsSchema.extend({
79
79
  /** Host-authorized reference images. */
80
80
  references: z
81
81
  .array(z
82
82
  .object({
83
+ /** Omit for the selected Computer; the host authorizes every named Computer. */
84
+ computerId: z.string().min(1).max(256).optional(),
83
85
  /** Fixed destination or reference file path. */
84
86
  path: z.string().min(1).max(4_096),
85
87
  /** Video-only reference role. */
@@ -96,15 +98,16 @@ export const validateMediaGenerationSettings = (raw) => {
96
98
  throw new MediaSettingsError("generation settings are invalid");
97
99
  return normalizeGenerationSettings(parsed.data, normalizeReference);
98
100
  };
99
- /** Validate path-only selections without inventing a Computer identity. */
101
+ /** Validate selections without granting Computer authority. */
100
102
  export const validateMediaGenerationSelection = (raw) => {
101
103
  const parsed = mediaGenerationSelectionSchema.safeParse(raw);
102
104
  if (!parsed.success)
103
105
  throw new MediaSettingsError("generation settings are invalid");
104
106
  return normalizeGenerationSettings(parsed.data, (reference) => {
105
- const selection = {
106
- path: requiredString(reference.path, "reference path"),
107
- };
107
+ const path = requiredString(reference.path, "reference path");
108
+ const selection = reference.computerId === undefined
109
+ ? { path }
110
+ : { path, computerId: reference.computerId };
108
111
  if (reference.role === undefined)
109
112
  return selection;
110
113
  return { ...selection, role: reference.role };
@@ -292,14 +295,13 @@ export const resolveMediaReferences = async (settings, reader) => {
292
295
  throw new MediaSettingsError(`${model.id} accepts at most ${String(model.input.maxReferences)} reference images`);
293
296
  const resolved = [];
294
297
  for (const reference of references) {
295
- const image = await reader.read(reference);
298
+ const image = await reader.read(reference, settings.model);
296
299
  if (!imageFormats.has(image.format) ||
297
300
  !Number.isSafeInteger(image.width) ||
298
301
  !Number.isSafeInteger(image.height) ||
299
302
  image.width < 1 ||
300
303
  image.height < 1 ||
301
- image.width > MAX_REFERENCE_DIMENSION ||
302
- image.height > MAX_REFERENCE_DIMENSION ||
304
+ !referenceImageSizeIsValid(settings.model, image.width, image.height) ||
303
305
  !(image.bytes instanceof Uint8Array) ||
304
306
  image.bytes.byteLength === 0 ||
305
307
  image.bytes.byteLength > MAX_REFERENCE_BYTES)
@@ -26,6 +26,7 @@ export declare const mediaImageBindingInputSchema: z.ZodObject<{
26
26
  width: z.ZodNumber;
27
27
  height: z.ZodNumber;
28
28
  references: z.ZodOptional<z.ZodArray<z.ZodObject<{
29
+ computerId: z.ZodOptional<z.ZodString>;
29
30
  path: z.ZodString;
30
31
  role: z.ZodOptional<z.ZodEnum<{
31
32
  readonly first: "first-frame";
@@ -112,6 +113,7 @@ export declare const mediaBindingGenerationSchema: z.ZodDiscriminatedUnion<[z.Zo
112
113
  width: z.ZodNumber;
113
114
  height: z.ZodNumber;
114
115
  references: z.ZodOptional<z.ZodArray<z.ZodObject<{
116
+ computerId: z.ZodOptional<z.ZodString>;
115
117
  path: z.ZodString;
116
118
  role: z.ZodOptional<z.ZodEnum<{
117
119
  readonly first: "first-frame";
@@ -173,7 +175,7 @@ export declare const mediaBindingGenerationSchema: z.ZodDiscriminatedUnion<[z.Zo
173
175
  }, z.core.$strict>], "kind">;
174
176
  /** Parsed generation command consumed by either host. */
175
177
  export type MediaBindingGeneration = z.output<typeof mediaBindingGenerationSchema>;
176
- /** Maintained MEDIA method grammar; guest input carries no principal or Computer. */
178
+ /** Maintained MEDIA grammar; reference selectors carry no Computer authority. */
177
179
  export declare const mediaBindingCallSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
178
180
  operation: z.ZodLiteral<"models">;
179
181
  input: z.ZodObject<{}, z.core.$strict>;
@@ -203,6 +205,7 @@ export declare const mediaBindingCallSchema: z.ZodDiscriminatedUnion<[z.ZodObjec
203
205
  width: z.ZodNumber;
204
206
  height: z.ZodNumber;
205
207
  references: z.ZodOptional<z.ZodArray<z.ZodObject<{
208
+ computerId: z.ZodOptional<z.ZodString>;
206
209
  path: z.ZodString;
207
210
  role: z.ZodOptional<z.ZodEnum<{
208
211
  readonly first: "first-frame";
@@ -286,7 +289,7 @@ export declare function parseMediaBindingCall(raw: z.core.util.JSONType): Readon
286
289
  export type MediaBindingRequest = Readonly<{
287
290
  /** Host-selected output filename within the App's library folder. */
288
291
  path: string;
289
- /** Validated selection with the host's Computer attached to references. */
292
+ /** Validated references with explicit Computer identities, authorized by the host. */
290
293
  settings: MediaGenerationSettings;
291
294
  /** Optional image encoding; video output remains MP4. */
292
295
  outputFormat?: z.output<typeof mediaRequestedImageFormatSchema>;
@@ -51,7 +51,7 @@ export const mediaBindingGenerationSchema = z.discriminatedUnion("kind", [
51
51
  mediaImageBindingInputSchema,
52
52
  mediaVideoBindingInputSchema,
53
53
  ]);
54
- /** Maintained MEDIA method grammar; guest input carries no principal or Computer. */
54
+ /** Maintained MEDIA grammar; reference selectors carry no Computer authority. */
55
55
  export const mediaBindingCallSchema = z.discriminatedUnion("operation", [
56
56
  z.strictObject({
57
57
  /** Read the model catalog without dispatching generation. */
@@ -120,7 +120,12 @@ export function mediaBindingRequest(input, computerId) {
120
120
  if (references !== undefined)
121
121
  attached = {
122
122
  ...settings,
123
- references: references.map((reference) => ({ ...reference, computerId })),
123
+ references: references.map((reference) => ({
124
+ ...reference,
125
+ computerId: "computerId" in reference
126
+ ? (reference.computerId ?? computerId)
127
+ : computerId,
128
+ })),
124
129
  };
125
130
  const request = {
126
131
  path: `${input.libraryPath}/prints-${input.requestId}.${format}`,
@@ -47,10 +47,10 @@ export function runMediaGeneration(host, request, signal) {
47
47
  generation: {
48
48
  prepare: async () => {
49
49
  const references = await resolveMediaReferences(settings, {
50
- read: (reference) => {
50
+ read: (reference, model) => {
51
51
  if (host.destination.readReference === undefined)
52
52
  throw new MediaSettingsError("reference images are unavailable");
53
- return host.destination.readReference(reference);
53
+ return host.destination.readReference(reference, model);
54
54
  },
55
55
  });
56
56
  const costMilliunits = estimate(settings, references);
@@ -1,10 +1,21 @@
1
1
  import { MediaExecutionError, MediaOutputFormatMismatch, MediaSafetyFilterRefusal, } from "./execution-error.js";
2
2
  import * as Effect from "effect/Effect";
3
+ import { reportCatchFailure } from "./failure.js";
3
4
  import { mediaUnits } from "./gateway-models.js";
4
5
  import { getMediaModel } from "./model-catalog.js";
5
6
  import { embedProvenance } from "./provenance.js";
6
7
  import { z } from "zod";
7
8
  import { mediaGenerationSettingsSchema, validateMediaGenerationSettings, } from "./index.js";
9
+ function unchargedFailure(error) {
10
+ const options = {
11
+ cause: error.cause,
12
+ provider: error.provider,
13
+ customerCharge: "none",
14
+ };
15
+ return error instanceof MediaSafetyFilterRefusal
16
+ ? new MediaSafetyFilterRefusal(options)
17
+ : new MediaExecutionError(error.stage, options);
18
+ }
8
19
  function call(body, stage) {
9
20
  return Effect.tryPromise({
10
21
  try: () => Promise.resolve(body()),
@@ -49,10 +60,10 @@ export function executeMediaOperation(input) {
49
60
  const operationId = admission.operation.operationId;
50
61
  const billed = mediaUnits(validateMediaGenerationSettings(mediaGenerationSettingsSchema.strip().parse(input.request)));
51
62
  const background = getMediaModel(input.request.model)?.kind === "video";
52
- /** Release a held price when nothing will be delivered; best effort, idempotent. */
63
+ /** Best-effort release; true proves customer settlement, not supplier billing. */
53
64
  const settleNothing = () => metering === null
54
- ? Effect.void
55
- : call(() => metering.settle({ operationId, units: 0 }), "billing").pipe(Effect.ignore);
65
+ ? Effect.succeed(false)
66
+ : call(() => metering.settle({ operationId, units: 0 }), "billing").pipe(Effect.match({ onFailure: () => false, onSuccess: () => true }));
56
67
  const claim = yield* call(() => input.operation.claim(), "operation");
57
68
  if (claim.kind === "succeeded" &&
58
69
  claim.operation.image !== null &&
@@ -71,14 +82,16 @@ export function executeMediaOperation(input) {
71
82
  }));
72
83
  }
73
84
  if (claim.kind === "interrupted") {
74
- yield* settleNothing();
85
+ const uncharged = yield* settleNothing();
75
86
  const failure = claim.operation.providerFailure;
76
- if (failure !== null)
77
- return yield* Effect.fail(failure.safetyRefused
87
+ if (failure !== null) {
88
+ const error = failure.safetyRefused
78
89
  ? new MediaSafetyFilterRefusal({ provider: failure.provider })
79
90
  : new MediaExecutionError(failure.stage, {
80
91
  provider: failure.provider,
81
- }));
92
+ });
93
+ return yield* Effect.fail(uncharged ? unchargedFailure(error) : error);
94
+ }
82
95
  }
83
96
  if (claim.kind !== "claimed" || claim.leaseId === undefined)
84
97
  return {
@@ -154,16 +167,22 @@ export function executeMediaOperation(input) {
154
167
  }, "provider").pipe(
155
168
  // A provider failure delivered nothing and wrote no output: end the
156
169
  // request now and release its hold rather than leave it unresolved.
157
- Effect.tapError((error) => {
170
+ Effect.catch((error) => Effect.gen(function* () {
158
171
  const stage = error.stage;
159
- return stage === "provider" || stage === "normalization"
160
- ? call(() => input.operation.interruptGeneration(leaseId, {
172
+ if (stage === "provider" || stage === "normalization") {
173
+ const uncharged = yield* call(() => input.operation.interruptGeneration(leaseId, {
161
174
  stage,
162
175
  safetyRefused: error instanceof MediaSafetyFilterRefusal,
163
176
  provider: error.provider,
164
- }), "operation").pipe(Effect.andThen(settleNothing()), Effect.ignore)
165
- : Effect.void;
166
- }));
177
+ }), "operation").pipe(Effect.andThen(settleNothing()), Effect.catch((cause) => {
178
+ reportCatchFailure("media-operation-interrupt", cause);
179
+ return Effect.succeed(false);
180
+ }));
181
+ if (uncharged)
182
+ return yield* Effect.fail(unchargedFailure(error));
183
+ }
184
+ return yield* Effect.fail(error);
185
+ })));
167
186
  if (generation.kind === "interrupted") {
168
187
  yield* settleNothing();
169
188
  return { kind: "interrupted" };
@@ -0,0 +1,10 @@
1
+ /** Maximum long edge for image-model references, in pixels. */
2
+ export declare const MAX_REFERENCE_DIMENSION = 512;
3
+ /** Aspect-preserving native resize; only video references may be enlarged. */
4
+ export declare function referenceImageTransform(modelId: string, width: number, height: number): {
5
+ width: number;
6
+ height: number;
7
+ fit: "contain" | "scale-down";
8
+ };
9
+ /** Validate actual decoded output, rather than trusting requested resize dimensions. */
10
+ export declare function referenceImageSizeIsValid(modelId: string, width: number, height: number): boolean;
@@ -0,0 +1,37 @@
1
+ import { getMediaModel, MediaSettingsError } from "./model-catalog.js";
2
+ /** Maximum long edge for image-model references, in pixels. */
3
+ export const MAX_REFERENCE_DIMENSION = 512;
4
+ const MIN_VIDEO_REFERENCE_DIMENSION = 320;
5
+ /** One owner for both native fitting and the resolved-reference boundary. */
6
+ function sizing(modelId) {
7
+ const model = getMediaModel(modelId);
8
+ if (model === undefined)
9
+ throw new MediaSettingsError("unknown Media model");
10
+ return model.video === null
11
+ ? { edge: "long", dimension: MAX_REFERENCE_DIMENSION }
12
+ : { edge: "short", dimension: MIN_VIDEO_REFERENCE_DIMENSION };
13
+ }
14
+ /** Aspect-preserving native resize; only video references may be enlarged. */
15
+ export function referenceImageTransform(modelId, width, height) {
16
+ const rule = sizing(modelId);
17
+ const edge = rule.edge === "short" ? Math.min(width, height) : Math.max(width, height);
18
+ const ratio = rule.edge === "short"
19
+ ? rule.dimension / edge
20
+ : Math.min(1, rule.dimension / edge);
21
+ // A video's bounding box must not round its long edge down and squeeze
22
+ // the actual decoded short edge below the provider minimum.
23
+ const round = rule.edge === "short" ? Math.ceil : Math.round;
24
+ return {
25
+ width: Math.max(1, round(width * ratio)),
26
+ height: Math.max(1, round(height * ratio)),
27
+ // Small video frames must grow; image references must never grow.
28
+ fit: rule.edge === "short" ? "contain" : "scale-down",
29
+ };
30
+ }
31
+ /** Validate actual decoded output, rather than trusting requested resize dimensions. */
32
+ export function referenceImageSizeIsValid(modelId, width, height) {
33
+ const rule = sizing(modelId);
34
+ return rule.edge === "short"
35
+ ? Math.min(width, height) >= rule.dimension
36
+ : Math.max(width, height) <= rule.dimension;
37
+ }
package/dist/server.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ export { estimateMediaModel } from "./cloudflare/provider.js";
1
2
  export { mediaPendingSchema, mediaBindingCallSchema, mediaBindingGenerationSchema, mediaImageBindingInputSchema, mediaVideoBindingInputSchema, parseMediaBindingCall, mediaBindingRequest, readMediaBindingReply, type MediaBindingCall, type MediaBindingGeneration, type MediaBindingRequest, } from "./media-binding.js";
2
3
  import { Hono } from "hono";
3
4
  import { z } from "zod";
@@ -90,8 +91,8 @@ export type MediaServiceHost = Readonly<{
90
91
  authorize: (path: string) => Promise<void>;
91
92
  /** Save exact generated bytes to the admitted destination. */
92
93
  write: (path: string, bytes: Uint8Array) => Promise<void>;
93
- /** Authorize and decode one selected reference image. */
94
- readReference?: (reference: MediaReference) => Promise<MediaReferenceBytes>;
94
+ /** Authorize and fit one selected reference image for the catalog model. */
95
+ readReference?: (reference: MediaReference, model: string) => Promise<MediaReferenceBytes>;
95
96
  }>;
96
97
  /** Translate host authority failures; undefined uses the shared Media failure. */
97
98
  onFailure?: (error: MediaExecutionError) => Response | undefined;