@fungi.computer/darkroom 0.0.0-stage → 0.1.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.
- package/LICENSE +21 -0
- package/README.md +315 -2
- package/dist/app-media-contracts.d.ts +499 -0
- package/dist/app-media-contracts.js +96 -0
- package/dist/browser-failure.d.ts +40 -0
- package/dist/browser-failure.js +39 -0
- package/dist/browser.d.ts +2 -0
- package/dist/browser.js +1 -0
- package/dist/cloudflare/provider.d.ts +13 -0
- package/dist/cloudflare/provider.js +140 -0
- package/dist/cloudflare/reference-images.d.ts +42 -0
- package/dist/cloudflare/reference-images.js +102 -0
- package/dist/cloudflare.d.ts +6 -0
- package/dist/cloudflare.js +4 -0
- package/dist/cost-estimate.d.ts +22 -0
- package/dist/cost-estimate.js +76 -0
- package/dist/effect.d.ts +2 -0
- package/dist/effect.js +2 -0
- package/dist/execution-error.d.ts +54 -0
- package/dist/execution-error.js +62 -0
- package/dist/failure.d.ts +2 -0
- package/dist/failure.js +8 -0
- package/dist/gateway-models.d.ts +23 -0
- package/dist/gateway-models.js +77 -0
- package/dist/gateway-wire.d.ts +16 -0
- package/dist/gateway-wire.js +22 -0
- package/dist/index.d.ts +361 -0
- package/dist/index.js +607 -0
- package/dist/media-binding.d.ts +291 -0
- package/dist/media-binding.js +149 -0
- package/dist/media-generation.d.ts +10 -0
- package/dist/media-generation.js +110 -0
- package/dist/media-operation-contract.d.ts +104 -0
- package/dist/media-operation-contract.js +29 -0
- package/dist/media-operation-do.d.ts +55 -0
- package/dist/media-operation-do.js +105 -0
- package/dist/media-operation-owner.d.ts +301 -0
- package/dist/media-operation-owner.js +903 -0
- package/dist/media-operation-storage.d.ts +81 -0
- package/dist/media-operation-storage.js +1 -0
- package/dist/media-operation.d.ts +33 -0
- package/dist/media-operation.js +230 -0
- package/dist/model-catalog.d.ts +456 -0
- package/dist/model-catalog.js +565 -0
- package/dist/neurons.d.ts +4 -0
- package/dist/neurons.js +4 -0
- package/dist/provenance-bytes.d.ts +3 -0
- package/dist/provenance-bytes.js +15 -0
- package/dist/provenance-export.d.ts +23 -0
- package/dist/provenance-export.js +103 -0
- package/dist/provenance-jpeg.d.ts +3 -0
- package/dist/provenance-jpeg.js +76 -0
- package/dist/provenance-png.d.ts +1 -0
- package/dist/provenance-png.js +21 -0
- package/dist/provenance-xmp.d.ts +2 -0
- package/dist/provenance-xmp.js +54 -0
- package/dist/provenance.d.ts +20 -0
- package/dist/provenance.js +58 -0
- package/dist/server.d.ts +158 -0
- package/dist/server.js +207 -0
- package/dist/video-providers/index.d.ts +14 -0
- package/dist/video-providers/index.js +14 -0
- package/dist/video-providers/minimax.d.ts +25 -0
- package/dist/video-providers/minimax.js +63 -0
- package/dist/video-providers/reference-encoding.d.ts +4 -0
- package/dist/video-providers/reference-encoding.js +11 -0
- package/dist/video-providers/types.d.ts +28 -0
- package/dist/video-providers/types.js +1 -0
- package/dist/video-providers/unified.d.ts +18 -0
- package/dist/video-providers/unified.js +52 -0
- package/docs/api/README.md +16 -0
- package/docs/api/app-media-contracts.md +507 -0
- package/docs/api/browser.md +126 -0
- package/docs/api/cloudflare.md +678 -0
- package/docs/api/effect.md +39 -0
- package/docs/api/index.md +1775 -0
- package/docs/api/modules.md +19 -0
- package/docs/api/provenance-export.md +114 -0
- package/docs/api/server.md +1672 -0
- package/package.json +110 -3
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/** SQL rows stay untrusted until the owner decodes their stored representation. */
|
|
2
|
+
export type MediaOperationStorage = {
|
|
3
|
+
/** Native synchronous SQL interface. */
|
|
4
|
+
sql: {
|
|
5
|
+
/** Execute a native SQL statement with positional bindings. */
|
|
6
|
+
exec(query: string, ...bindings: (string | number | null)[]): {
|
|
7
|
+
/** Read the native SQL rows as objects for boundary decoding. */
|
|
8
|
+
toArray(): readonly object[];
|
|
9
|
+
};
|
|
10
|
+
};
|
|
11
|
+
/** Run one atomic synchronous local mutation. */
|
|
12
|
+
transactionSync<T>(body: () => T): T;
|
|
13
|
+
/** Read the durable erasure marker. */
|
|
14
|
+
isErased(): Promise<boolean>;
|
|
15
|
+
/** Persist the durable erasure marker. */
|
|
16
|
+
markErased(): Promise<void>;
|
|
17
|
+
/** List local key-value storage keys for erasure readback. */
|
|
18
|
+
listKeys(): Promise<readonly string[]>;
|
|
19
|
+
/** Remove any scheduled background alarm. */
|
|
20
|
+
deleteAlarm(): Promise<void>;
|
|
21
|
+
/** Remove local SQL and key-value storage. */
|
|
22
|
+
deleteAll(): Promise<void>;
|
|
23
|
+
/** Read the current background alarm, or null. */
|
|
24
|
+
getAlarm(): Promise<number | null>;
|
|
25
|
+
/** Schedule background generation at the given timestamp. */
|
|
26
|
+
setAlarm(timestamp: number): Promise<void>;
|
|
27
|
+
};
|
|
28
|
+
/** The identity, serialization and storage owned by one media operation. */
|
|
29
|
+
export type MediaOperationContext = {
|
|
30
|
+
/** Durable operation identity selected by the host. */
|
|
31
|
+
id: {
|
|
32
|
+
/** Stable error name or durable object name. */
|
|
33
|
+
name: string | undefined;
|
|
34
|
+
};
|
|
35
|
+
/** Host-owned SQL, key-value storage and alarms. */
|
|
36
|
+
storage: MediaOperationStorage;
|
|
37
|
+
/** Serialize asynchronous owner initialization with requests. */
|
|
38
|
+
blockConcurrencyWhile<T>(body: () => Promise<T>): Promise<T>;
|
|
39
|
+
};
|
|
40
|
+
/** Object metadata consumed by retention and recovery; mirrors the native R2 fields used here. */
|
|
41
|
+
export type MediaOperationObject = Readonly<{
|
|
42
|
+
/** Immutable output key. */
|
|
43
|
+
key: string;
|
|
44
|
+
/** Object version captured when its exact bytes were written. */
|
|
45
|
+
version: string;
|
|
46
|
+
/** Stored byte length. */
|
|
47
|
+
size: number;
|
|
48
|
+
/** Actual retained MIME type. */
|
|
49
|
+
httpMetadata?: Readonly<{
|
|
50
|
+
/** MIME type of the retained output. */ contentType?: string;
|
|
51
|
+
}>;
|
|
52
|
+
}>;
|
|
53
|
+
/** Exact object operations needed for immutable media outputs and references. */
|
|
54
|
+
export type MediaOperationBucket = {
|
|
55
|
+
/** Conditionally retain immutable provider bytes or background reference bytes. */
|
|
56
|
+
put(key: string, bytes: Uint8Array, options?: Readonly<{
|
|
57
|
+
/** Native R2 conditional write, used once for an immutable output. */
|
|
58
|
+
onlyIf?: Readonly<{
|
|
59
|
+
/** Refuse an existing output key. */ etagDoesNotMatch: string;
|
|
60
|
+
}>;
|
|
61
|
+
/** MIME type for exact-output recovery. */
|
|
62
|
+
httpMetadata?: Readonly<{
|
|
63
|
+
/** MIME type of the retained output. */ contentType: string;
|
|
64
|
+
}>;
|
|
65
|
+
/** Decoded reference format and dimensions for background generation. */
|
|
66
|
+
customMetadata?: Readonly<Record<string, string>>;
|
|
67
|
+
}>): Promise<Readonly<{
|
|
68
|
+
/** Stored immutable object version. */ version: string;
|
|
69
|
+
}> | null>;
|
|
70
|
+
/** Read immutable object metadata without loading bytes. */
|
|
71
|
+
head(key: string): Promise<MediaOperationObject | null>;
|
|
72
|
+
/** Read retained object bytes and metadata. */
|
|
73
|
+
get(key: string): Promise<(Omit<MediaOperationObject, "httpMetadata"> & Readonly<{
|
|
74
|
+
/** Stored background reference metadata. */
|
|
75
|
+
customMetadata?: Readonly<Record<string, string>>;
|
|
76
|
+
/** Read the exact retained object bytes. */
|
|
77
|
+
arrayBuffer: () => Promise<ArrayBuffer>;
|
|
78
|
+
}>) | null>;
|
|
79
|
+
/** Delete the owner-selected object key. */
|
|
80
|
+
delete(key: string): Promise<void>;
|
|
81
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { MediaExecutionError } from "./execution-error.js";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import type { MediaExecution } from "./media-operation-contract.js";
|
|
4
|
+
/**
|
|
5
|
+
* Own one replay-safe Media execution. Network effects stay outside storage
|
|
6
|
+
* transactions; the durable operation hides its transition sequence.
|
|
7
|
+
*/
|
|
8
|
+
export declare function executeMediaOperation(input: MediaExecution): Effect.Effect<{
|
|
9
|
+
kind: "conflict";
|
|
10
|
+
bytes?: never;
|
|
11
|
+
format?: never;
|
|
12
|
+
costMilliunits?: never;
|
|
13
|
+
} | {
|
|
14
|
+
kind: "succeeded";
|
|
15
|
+
bytes: number;
|
|
16
|
+
format: string;
|
|
17
|
+
costMilliunits: number;
|
|
18
|
+
} | {
|
|
19
|
+
readonly kind: "pending" | "unavailable";
|
|
20
|
+
bytes?: never;
|
|
21
|
+
format?: never;
|
|
22
|
+
costMilliunits?: never;
|
|
23
|
+
} | {
|
|
24
|
+
kind: "accepted";
|
|
25
|
+
bytes?: never;
|
|
26
|
+
format?: never;
|
|
27
|
+
costMilliunits?: never;
|
|
28
|
+
} | {
|
|
29
|
+
kind: "interrupted";
|
|
30
|
+
bytes?: never;
|
|
31
|
+
format?: never;
|
|
32
|
+
costMilliunits?: never;
|
|
33
|
+
}, MediaExecutionError, never>;
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { MediaExecutionError, MediaOutputFormatMismatch, MediaSafetyFilterRefusal, } from "./execution-error.js";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import { mediaUnits } from "./gateway-models.js";
|
|
4
|
+
import { getMediaModel } from "./model-catalog.js";
|
|
5
|
+
import { embedProvenance } from "./provenance.js";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { mediaGenerationSettingsSchema, validateMediaGenerationSettings, } from "./index.js";
|
|
8
|
+
function call(body, stage) {
|
|
9
|
+
return Effect.tryPromise({
|
|
10
|
+
try: () => Promise.resolve(body()),
|
|
11
|
+
catch: (cause) => cause instanceof MediaExecutionError
|
|
12
|
+
? cause
|
|
13
|
+
: new MediaExecutionError(stage, {
|
|
14
|
+
cause: cause instanceof Error ? cause : new Error(String(cause)),
|
|
15
|
+
}),
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
function untilAborted(signal, body) {
|
|
19
|
+
if (signal === undefined)
|
|
20
|
+
return body();
|
|
21
|
+
if (signal.aborted)
|
|
22
|
+
return Promise.reject(new Error("Media generation was cancelled", { cause: signal.reason }));
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
const abort = () => {
|
|
25
|
+
reject(new Error("Media generation was cancelled", { cause: signal.reason }));
|
|
26
|
+
};
|
|
27
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
28
|
+
Promise.resolve()
|
|
29
|
+
.then(body)
|
|
30
|
+
.then(resolve, reject)
|
|
31
|
+
.finally(() => {
|
|
32
|
+
signal.removeEventListener("abort", abort);
|
|
33
|
+
});
|
|
34
|
+
if (signal.aborted)
|
|
35
|
+
abort();
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Own one replay-safe Media execution. Network effects stay outside storage
|
|
40
|
+
* transactions; the durable operation hides its transition sequence.
|
|
41
|
+
*/
|
|
42
|
+
export function executeMediaOperation(input) {
|
|
43
|
+
return Effect.gen(function* () {
|
|
44
|
+
const admission = yield* call(() => input.operation.admit(input.actorUserId, input.coordinates, input.requestId, input.request), "operation");
|
|
45
|
+
if (admission.kind === "conflict")
|
|
46
|
+
return { kind: "conflict" };
|
|
47
|
+
yield* call(input.destination.authorize, "destination-authorization");
|
|
48
|
+
const metering = input.metering === "unmetered" ? null : input.metering;
|
|
49
|
+
const operationId = admission.operation.operationId;
|
|
50
|
+
const billed = mediaUnits(validateMediaGenerationSettings(mediaGenerationSettingsSchema.strip().parse(input.request)));
|
|
51
|
+
const background = getMediaModel(input.request.model)?.kind === "video";
|
|
52
|
+
/** Release a held price when nothing will be delivered; best effort, idempotent. */
|
|
53
|
+
const settleNothing = () => metering === null
|
|
54
|
+
? Effect.void
|
|
55
|
+
: call(() => metering.settle({ operationId, units: 0 }), "billing").pipe(Effect.ignore);
|
|
56
|
+
const claim = yield* call(() => input.operation.claim(), "operation");
|
|
57
|
+
if (claim.kind === "succeeded" &&
|
|
58
|
+
claim.operation.image !== null &&
|
|
59
|
+
claim.operation.estimatedCostMilliunits !== null)
|
|
60
|
+
return {
|
|
61
|
+
kind: "succeeded",
|
|
62
|
+
bytes: claim.operation.image.bytes,
|
|
63
|
+
format: claim.operation.image.format,
|
|
64
|
+
costMilliunits: claim.operation.estimatedCostMilliunits,
|
|
65
|
+
};
|
|
66
|
+
if (claim.kind === "interrupted" && claim.operation.spentFailure !== null) {
|
|
67
|
+
if (metering !== null)
|
|
68
|
+
yield* call(() => metering.settle({ operationId, units: billed.units }), "billing");
|
|
69
|
+
return yield* Effect.fail(new MediaExecutionError("artifact", {
|
|
70
|
+
cause: new MediaOutputFormatMismatch(),
|
|
71
|
+
}));
|
|
72
|
+
}
|
|
73
|
+
if (claim.kind === "interrupted") {
|
|
74
|
+
yield* settleNothing();
|
|
75
|
+
if (claim.operation.providerRefusal === "safety_filter_refused")
|
|
76
|
+
return yield* Effect.fail(new MediaSafetyFilterRefusal());
|
|
77
|
+
}
|
|
78
|
+
if (claim.kind !== "claimed" || claim.leaseId === undefined)
|
|
79
|
+
return {
|
|
80
|
+
kind: claim.kind === "pending" ? "pending" : "unavailable",
|
|
81
|
+
};
|
|
82
|
+
const leaseId = claim.leaseId;
|
|
83
|
+
let costMilliunits;
|
|
84
|
+
let image;
|
|
85
|
+
if (claim.operation.state === "charging" &&
|
|
86
|
+
claim.operation.image !== null &&
|
|
87
|
+
claim.operation.estimatedCostMilliunits !== null) {
|
|
88
|
+
// The save committed; only the idempotent settlement remains.
|
|
89
|
+
image = claim.operation.image;
|
|
90
|
+
costMilliunits = claim.operation.estimatedCostMilliunits;
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
let output;
|
|
94
|
+
if (claim.operation.state === "writing" &&
|
|
95
|
+
claim.operation.image !== null &&
|
|
96
|
+
claim.operation.estimatedCostMilliunits !== null) {
|
|
97
|
+
output = {
|
|
98
|
+
bytes: yield* call(() => input.operation.readGenerated(leaseId), "artifact"),
|
|
99
|
+
format: claim.operation.image.format,
|
|
100
|
+
};
|
|
101
|
+
costMilliunits = claim.operation.estimatedCostMilliunits;
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
// A retained save never rereads reference files or recomputes its quote.
|
|
105
|
+
const releaseEstimate = () => call(() => input.operation.releaseEstimate(leaseId), "operation").pipe(Effect.ignore);
|
|
106
|
+
const prepared = yield* call(input.generation.prepare, "estimate").pipe(Effect.tapError(releaseEstimate));
|
|
107
|
+
// Billing admission is the last gate before durable dispatch. A refusal
|
|
108
|
+
// returns the request to admitted, so the same request id can retry.
|
|
109
|
+
costMilliunits =
|
|
110
|
+
metering === null
|
|
111
|
+
? prepared.costMilliunits
|
|
112
|
+
: (yield* call(() => metering.admit({
|
|
113
|
+
operationId,
|
|
114
|
+
model: input.request.model,
|
|
115
|
+
unit: billed.unit,
|
|
116
|
+
units: billed.units,
|
|
117
|
+
providerCostMilliunits: prepared.costMilliunits,
|
|
118
|
+
}), "billing").pipe(Effect.tapError(releaseEstimate))).priceMilliunits;
|
|
119
|
+
yield* call(() => input.operation.recordEstimate(leaseId, costMilliunits), "estimate");
|
|
120
|
+
if (background) {
|
|
121
|
+
// Video outlives one request: the owner's alarm generates, and a
|
|
122
|
+
// retry of this request id saves and charges the retained output.
|
|
123
|
+
yield* call(() => input.operation.dispatchInBackground(leaseId, prepared.references), "operation").pipe(Effect.tapError(() => call(() => input.operation.interruptGeneration(leaseId), "operation").pipe(Effect.andThen(settleNothing()), Effect.ignore)));
|
|
124
|
+
return { kind: "accepted" };
|
|
125
|
+
}
|
|
126
|
+
const generation = yield* call(async () => {
|
|
127
|
+
let output;
|
|
128
|
+
try {
|
|
129
|
+
output = await untilAborted(input.signal, () => prepared.run(input.signal));
|
|
130
|
+
}
|
|
131
|
+
catch (cause) {
|
|
132
|
+
if (!input.signal?.aborted)
|
|
133
|
+
throw cause;
|
|
134
|
+
await input.operation.interruptGeneration(leaseId);
|
|
135
|
+
return {
|
|
136
|
+
kind: "interrupted",
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
if (input.signal?.aborted) {
|
|
140
|
+
await input.operation.interruptGeneration(leaseId);
|
|
141
|
+
return {
|
|
142
|
+
kind: "interrupted",
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
kind: "generated",
|
|
147
|
+
output,
|
|
148
|
+
};
|
|
149
|
+
}, "provider").pipe(
|
|
150
|
+
// A provider failure delivered nothing and wrote no output: end the
|
|
151
|
+
// request now and release its hold rather than leave it unresolved.
|
|
152
|
+
Effect.tapError((error) => error.stage === "provider"
|
|
153
|
+
? call(() => input.operation.interruptGeneration(leaseId, error instanceof MediaSafetyFilterRefusal
|
|
154
|
+
? "safety_filter_refused"
|
|
155
|
+
: undefined), "operation").pipe(Effect.andThen(settleNothing()), Effect.ignore)
|
|
156
|
+
: Effect.void));
|
|
157
|
+
if (generation.kind === "interrupted") {
|
|
158
|
+
yield* settleNothing();
|
|
159
|
+
return { kind: "interrupted" };
|
|
160
|
+
}
|
|
161
|
+
output = generation.output;
|
|
162
|
+
const recorded = yield* call(() => input.operation.recordGenerated(leaseId, output.bytes, output.format), "artifact");
|
|
163
|
+
if ("kind" in recorded) {
|
|
164
|
+
// The provider ran. The owner recorded a durable spent failure before
|
|
165
|
+
// this idempotent charge, so replay cannot refund or redispatch it.
|
|
166
|
+
if (metering !== null)
|
|
167
|
+
yield* call(() => metering.settle({ operationId, units: billed.units }), "billing");
|
|
168
|
+
return yield* Effect.fail(new MediaExecutionError("artifact", {
|
|
169
|
+
cause: new MediaOutputFormatMismatch(),
|
|
170
|
+
}));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
// Embedded fresh from the recorded (unembedded) bytes on every attempt,
|
|
174
|
+
// including a retry of a "writing" lease: deterministic facts in, so a
|
|
175
|
+
// retry writes byte-identical output. A charged generation is never
|
|
176
|
+
// failed over metadata, so a malformed or truncated provider image is
|
|
177
|
+
// written as-is; embedProvenance never throws.
|
|
178
|
+
const format = z
|
|
179
|
+
.enum(["png", "jpeg", "gif", "webp", "mp4"])
|
|
180
|
+
.safeParse(output.format);
|
|
181
|
+
const provenance = format.success
|
|
182
|
+
? yield* call(() => embedProvenance(output.bytes, format.data, {
|
|
183
|
+
printId: operationId,
|
|
184
|
+
model: input.request.model,
|
|
185
|
+
prompt: input.request.prompt,
|
|
186
|
+
createdAt: admission.operation.createdAt,
|
|
187
|
+
}), "operation")
|
|
188
|
+
: {
|
|
189
|
+
bytes: output.bytes,
|
|
190
|
+
embedded: false,
|
|
191
|
+
reason: "unsupported-format",
|
|
192
|
+
};
|
|
193
|
+
if (!provenance.embedded)
|
|
194
|
+
console.warn("Media provenance not embedded", {
|
|
195
|
+
format: output.format,
|
|
196
|
+
reason: provenance.reason,
|
|
197
|
+
});
|
|
198
|
+
yield* call(() => input.destination.write(provenance.bytes), "destination-write").pipe(Effect.tapError(() => call(() => input.operation.releaseWrite(leaseId), "operation").pipe(Effect.ignore)));
|
|
199
|
+
image = {
|
|
200
|
+
bytes: provenance.bytes.byteLength,
|
|
201
|
+
format: output.format,
|
|
202
|
+
};
|
|
203
|
+
if (metering === null) {
|
|
204
|
+
yield* call(() => input.operation.recordWrittenWithoutSettlement(leaseId, "computer-write-committed", image), "operation");
|
|
205
|
+
return {
|
|
206
|
+
kind: "succeeded",
|
|
207
|
+
bytes: image.bytes,
|
|
208
|
+
format: image.format,
|
|
209
|
+
costMilliunits,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
yield* call(() => input.operation.recordWritten(leaseId, "computer-write-committed", image), "operation");
|
|
213
|
+
}
|
|
214
|
+
if (metering === null)
|
|
215
|
+
return { kind: "unavailable" };
|
|
216
|
+
yield* call(() => metering.settle({ operationId, units: billed.units }), "billing");
|
|
217
|
+
yield* call(() => input.operation.recordSucceeded(leaseId, {
|
|
218
|
+
generationId: operationId,
|
|
219
|
+
bytes: image.bytes,
|
|
220
|
+
format: image.format,
|
|
221
|
+
costMilliunits,
|
|
222
|
+
}), "operation");
|
|
223
|
+
return {
|
|
224
|
+
kind: "succeeded",
|
|
225
|
+
bytes: image.bytes,
|
|
226
|
+
format: image.format,
|
|
227
|
+
costMilliunits,
|
|
228
|
+
};
|
|
229
|
+
});
|
|
230
|
+
}
|