pixelkiln 0.13.0 → 0.14.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/dist/cli.js CHANGED
@@ -2315,7 +2315,494 @@ function retroError(value) {
2315
2315
  return JSON.stringify(value);
2316
2316
  }
2317
2317
 
2318
+ // src/providers/scenario.ts
2319
+ var DEFAULT_BASE_URL3 = "https://api.cloud.scenario.com/v1";
2320
+ var SOURCE_PROTOCOL2 = "scenario:";
2321
+ var PROTECTED_PARAMETERS = /* @__PURE__ */ new Set([
2322
+ "dryRun",
2323
+ "height",
2324
+ "maxComputeUnits",
2325
+ "numOutputs",
2326
+ "projectId",
2327
+ "prompt",
2328
+ "seed",
2329
+ "width"
2330
+ ]);
2331
+ var ScenarioClient = class {
2332
+ constructor(apiKey, apiSecret, baseUrl = DEFAULT_BASE_URL3, request = fetch) {
2333
+ this.apiKey = apiKey;
2334
+ this.apiSecret = apiSecret;
2335
+ this.baseUrl = baseUrl;
2336
+ this.request = request;
2337
+ }
2338
+ apiKey;
2339
+ apiSecret;
2340
+ baseUrl;
2341
+ request;
2342
+ async quote(modelId, body, projectId) {
2343
+ const response = await this.call(
2344
+ `/generate/custom/${encodeURIComponent(modelId)}`,
2345
+ { method: "POST", body: JSON.stringify(body) },
2346
+ { dryRun: "true", projectId }
2347
+ );
2348
+ const creativeUnitsCost = finiteNonnegative(
2349
+ response?.creativeUnitsCost,
2350
+ "Scenario dry run returned an invalid creativeUnitsCost"
2351
+ );
2352
+ const costDetails = numericRecord(response?.costDetails);
2353
+ const ipDetectionCost = optionalFiniteNonnegative(
2354
+ response?.ipDetection?.creativeUnitsCharged,
2355
+ "Scenario dry run returned an invalid IP-detection cost"
2356
+ );
2357
+ return {
2358
+ total: creativeUnitsCost + ipDetectionCost,
2359
+ creativeUnitsCost,
2360
+ costDetails,
2361
+ ipDetectionCost
2362
+ };
2363
+ }
2364
+ async submit(modelId, body, projectId) {
2365
+ const response = await this.call(
2366
+ `/generate/custom/${encodeURIComponent(modelId)}`,
2367
+ { method: "POST", body: JSON.stringify(body) },
2368
+ { projectId }
2369
+ );
2370
+ const job = response?.job;
2371
+ const jobId = readJobId(job);
2372
+ if (!jobId) throw new Error("Scenario did not return an async job id");
2373
+ return jobId;
2374
+ }
2375
+ async job(id, projectId) {
2376
+ const response = await this.call(`/jobs/${encodeURIComponent(id)}`, {}, { projectId });
2377
+ const raw = response?.job;
2378
+ if (!raw || typeof raw !== "object") throw new Error("Scenario returned an invalid job");
2379
+ const record = raw;
2380
+ const jobId = readJobId(raw);
2381
+ if (!jobId || typeof record.status !== "string") {
2382
+ throw new Error("Scenario returned an invalid job");
2383
+ }
2384
+ return {
2385
+ jobId,
2386
+ status: record.status,
2387
+ ...typeof record.progress === "number" ? { progress: record.progress } : {},
2388
+ ...record.metadata && typeof record.metadata === "object" ? { metadata: record.metadata } : {},
2389
+ ...record.billing && typeof record.billing === "object" ? { billing: record.billing } : {}
2390
+ };
2391
+ }
2392
+ async asset(id, projectId) {
2393
+ const response = await this.call(
2394
+ `/assets/${encodeURIComponent(id)}`,
2395
+ {},
2396
+ { originalAssets: "true", projectId }
2397
+ );
2398
+ const raw = response?.asset;
2399
+ if (!raw || typeof raw !== "object") throw new Error(`Scenario returned an invalid asset ${id}`);
2400
+ const asset = raw;
2401
+ const assetId = typeof asset.id === "string" && asset.id ? asset.id : id;
2402
+ const url = typeof asset.url === "string" ? asset.url : "";
2403
+ const originalFileUrl = typeof asset.originalFileUrl === "string" ? asset.originalFileUrl : void 0;
2404
+ if (!originalFileUrl && !url) throw new Error(`Scenario asset ${assetId} has no download URL`);
2405
+ const metadata = isRecord(asset.metadata) ? asset.metadata : {};
2406
+ const rawOutputIndex = asset.outputIndex ?? metadata.outputIndex;
2407
+ const outputIndex = Number.isInteger(rawOutputIndex) && Number(rawOutputIndex) >= 0 ? Number(rawOutputIndex) : Number.MAX_SAFE_INTEGER;
2408
+ const originalMimeType = typeof asset.originalMimeType === "string" ? asset.originalMimeType : typeof asset.mimeType === "string" ? asset.mimeType : void 0;
2409
+ return {
2410
+ id: assetId,
2411
+ url,
2412
+ ...originalFileUrl ? { originalFileUrl } : {},
2413
+ ...originalMimeType ? { originalMimeType } : {},
2414
+ outputIndex
2415
+ };
2416
+ }
2417
+ async download(url) {
2418
+ const parsed = new URL(url);
2419
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
2420
+ throw new Error("Scenario downloads must use HTTP or HTTPS");
2421
+ }
2422
+ const response = await this.request(parsed);
2423
+ if (!response.ok) throw new Error(`Scenario download failed (${response.status})`);
2424
+ const declared = Number(response.headers.get("content-length"));
2425
+ if (Number.isFinite(declared) && declared > MAX_DOWNLOAD_BYTES) {
2426
+ throw new Error(`Scenario download exceeds the ${MAX_DOWNLOAD_BYTES}-byte safety limit`);
2427
+ }
2428
+ const bytes = Buffer.from(await response.arrayBuffer());
2429
+ if (bytes.length > MAX_DOWNLOAD_BYTES) {
2430
+ throw new Error(`Scenario download exceeds the ${MAX_DOWNLOAD_BYTES}-byte safety limit`);
2431
+ }
2432
+ return bytes;
2433
+ }
2434
+ async checkConnection() {
2435
+ await this.call("/models", {}, { pageSize: "1" });
2436
+ }
2437
+ async call(pathname, init = {}, query = {}) {
2438
+ if (!this.apiKey || !this.apiSecret) {
2439
+ const missing = [
2440
+ !this.apiKey ? "SCENARIO_SDK_API_KEY" : null,
2441
+ !this.apiSecret ? "SCENARIO_SDK_API_SECRET" : null
2442
+ ].filter(Boolean);
2443
+ throw new Error(`${missing.join(" and ")} ${missing.length > 1 ? "are" : "is"} not set`);
2444
+ }
2445
+ const url = new URL(`${this.baseUrl.replace(/\/$/, "")}${pathname}`);
2446
+ for (const [key, value2] of Object.entries(query)) {
2447
+ if (value2 != null) url.searchParams.set(key, value2);
2448
+ }
2449
+ const auth = Buffer.from(`${this.apiKey}:${this.apiSecret}`).toString("base64");
2450
+ const response = await this.request(url, {
2451
+ ...init,
2452
+ headers: {
2453
+ Accept: "application/json",
2454
+ Authorization: `Basic ${auth}`,
2455
+ ...init.body ? { "Content-Type": "application/json" } : {},
2456
+ ...init.headers
2457
+ }
2458
+ });
2459
+ const text = await response.text();
2460
+ let value = null;
2461
+ try {
2462
+ value = text ? JSON.parse(text) : null;
2463
+ } catch {
2464
+ value = text;
2465
+ }
2466
+ if (!response.ok) {
2467
+ const retry = response.headers.get("retry-after");
2468
+ const detail = redactScenarioError(value, [this.apiKey, this.apiSecret, auth]);
2469
+ throw new Error(
2470
+ `Scenario request failed (${response.status}): ${detail}` + (retry ? `; retry after ${retry}s` : "")
2471
+ );
2472
+ }
2473
+ return value;
2474
+ }
2475
+ };
2476
+ var ScenarioProvider = class _ScenarioProvider {
2477
+ constructor(client) {
2478
+ this.client = client;
2479
+ }
2480
+ client;
2481
+ id = "scenario";
2482
+ static fromEnv() {
2483
+ return new _ScenarioProvider(new ScenarioClient(
2484
+ process.env.SCENARIO_SDK_API_KEY,
2485
+ process.env.SCENARIO_SDK_API_SECRET,
2486
+ scenarioBaseUrl(process.env.SCENARIO_API_BASE_URL ?? DEFAULT_BASE_URL3)
2487
+ ));
2488
+ }
2489
+ static forOffline() {
2490
+ return new _ScenarioProvider(new ScenarioClient(void 0, void 0));
2491
+ }
2492
+ static forDownloads() {
2493
+ return _ScenarioProvider.fromEnv();
2494
+ }
2495
+ supports(generator) {
2496
+ return generator === "map";
2497
+ }
2498
+ estimate(spec) {
2499
+ const options = scenarioOptions(spec);
2500
+ return {
2501
+ unit: "compute-units",
2502
+ amount: options.maxComputeUnits,
2503
+ candidates: options.numOutputs ?? 1
2504
+ };
2505
+ }
2506
+ validate(spec, styleImages) {
2507
+ const options = scenarioOptions(spec);
2508
+ if (typeof options.modelId !== "string" || !options.modelId.trim()) {
2509
+ throw new Error("Scenario modelId is required");
2510
+ }
2511
+ if (!Number.isFinite(options.maxComputeUnits) || options.maxComputeUnits <= 0) {
2512
+ throw new Error("Scenario maxComputeUnits must be a positive number");
2513
+ }
2514
+ const count = options.numOutputs ?? 1;
2515
+ if (!Number.isInteger(count) || count < 1 || count > 4) {
2516
+ throw new Error("Scenario numOutputs must be a whole number from 1 to 4");
2517
+ }
2518
+ if (spec.width < 128 || spec.width > 2048 || spec.width % 16 !== 0 || spec.height < 128 || spec.height > 2048 || spec.height % 16 !== 0) {
2519
+ throw new Error("Scenario width and height must be multiples of 16 from 128 to 2048");
2520
+ }
2521
+ if (styleImages.length) {
2522
+ throw new Error("Scenario styleImages are not supported yet; use a reusable Scenario model");
2523
+ }
2524
+ if (options.projectId != null && (typeof options.projectId !== "string" || !options.projectId.trim())) {
2525
+ throw new Error("Scenario projectId must be a non-empty string");
2526
+ }
2527
+ validateParameters(options.parameters ?? {});
2528
+ }
2529
+ rateLimit() {
2530
+ return { spacingMs: 1e3, maxInFlight: 4 };
2531
+ }
2532
+ async submit(spec, styleImages) {
2533
+ this.validate(spec, styleImages);
2534
+ const options = scenarioOptions(spec);
2535
+ const body = requestBody(spec, options);
2536
+ const quote = await this.client.quote(options.modelId, body, options.projectId);
2537
+ if (quote.total > options.maxComputeUnits + Number.EPSILON) {
2538
+ throw new Error(
2539
+ `Scenario quoted ${quote.total} compute-units, above maxComputeUnits ${options.maxComputeUnits}; no paid request was sent`
2540
+ );
2541
+ }
2542
+ const apiJobId = await this.client.submit(options.modelId, body, options.projectId);
2543
+ return {
2544
+ jobId: jobRef(apiJobId, options.projectId),
2545
+ metadata: {
2546
+ modelId: options.modelId,
2547
+ projectId: options.projectId ?? null,
2548
+ maxComputeUnits: options.maxComputeUnits,
2549
+ quotedComputeUnits: quote.total,
2550
+ creativeUnitsCost: quote.creativeUnitsCost,
2551
+ costDetails: quote.costDetails,
2552
+ ipDetectionCost: quote.ipDetectionCost
2553
+ }
2554
+ };
2555
+ }
2556
+ async poll(jobReference, _generator, context) {
2557
+ const reference = parseJobRef(jobReference);
2558
+ const job = await this.client.job(reference.jobId, reference.projectId);
2559
+ const metadata = jobMetadata(job, context?.spec ? scenarioOptions(context.spec) : void 0);
2560
+ if (["pending", "queued", "warming-up", "in-progress", "finalizing", "processing"].includes(job.status)) {
2561
+ return {
2562
+ status: "processing",
2563
+ progressPercent: validProgress(job.progress)
2564
+ };
2565
+ }
2566
+ if (["failure", "failed", "canceled"].includes(job.status)) {
2567
+ return { status: "failed", error: scenarioJobError(job) };
2568
+ }
2569
+ if (job.status !== "success") {
2570
+ return { status: "failed", error: `Scenario returned unknown job status "${job.status}"` };
2571
+ }
2572
+ const assetIds = scenarioAssetIds(job);
2573
+ if (!assetIds.length) return { status: "failed", error: "Scenario job returned no asset IDs" };
2574
+ const assets = await this.assets(assetIds, reference.projectId);
2575
+ if (assets.length > 1) {
2576
+ return {
2577
+ status: "review",
2578
+ candidateUrls: assets.map(assetDownloadUrl),
2579
+ metadata: { ...metadata, assetIds: assets.map((asset2) => asset2.id) }
2580
+ };
2581
+ }
2582
+ const asset = assets[0];
2583
+ return {
2584
+ status: "ready",
2585
+ objectId: asset.id,
2586
+ sourceUrl: assetRef(asset.id, reference.projectId),
2587
+ sources: [{ url: assetRef(asset.id, reference.projectId), mediaType: MediaType.PNG }],
2588
+ metadata: {
2589
+ ...metadata,
2590
+ assetId: asset.id,
2591
+ outputIndex: asset.outputIndex === Number.MAX_SAFE_INTEGER ? null : asset.outputIndex
2592
+ }
2593
+ };
2594
+ }
2595
+ async selectCandidate(jobReference, index) {
2596
+ const reference = parseJobRef(jobReference);
2597
+ const job = await this.client.job(reference.jobId, reference.projectId);
2598
+ if (job.status !== "success") throw new Error(`Scenario job ${reference.jobId} is not ready`);
2599
+ const assets = await this.assets(scenarioAssetIds(job), reference.projectId);
2600
+ const asset = assets[index];
2601
+ if (!asset) throw new Error(`Scenario job ${reference.jobId} has no candidate at index ${index}`);
2602
+ return {
2603
+ objectId: asset.id,
2604
+ sourceUrl: assetRef(asset.id, reference.projectId),
2605
+ metadata: {
2606
+ ...jobMetadata(job),
2607
+ assetId: asset.id,
2608
+ outputIndex: asset.outputIndex === Number.MAX_SAFE_INTEGER ? null : asset.outputIndex
2609
+ }
2610
+ };
2611
+ }
2612
+ async download(source) {
2613
+ if (!source.startsWith(`${SOURCE_PROTOCOL2}//`)) return this.client.download(source);
2614
+ const reference = parseAssetRef(source);
2615
+ const asset = await this.client.asset(reference.assetId, reference.projectId);
2616
+ assertPngAsset(asset);
2617
+ return this.client.download(assetDownloadUrl(asset));
2618
+ }
2619
+ async checkConnection() {
2620
+ await this.client.checkConnection();
2621
+ }
2622
+ async assets(ids, projectId) {
2623
+ const assets = await Promise.all(ids.map((id) => this.client.asset(id, projectId)));
2624
+ const jobOrder = new Map(ids.map((id, index) => [id, index]));
2625
+ for (const asset of assets) assertPngAsset(asset);
2626
+ return assets.sort((left, right) => left.outputIndex - right.outputIndex || (jobOrder.get(left.id) ?? Number.MAX_SAFE_INTEGER) - (jobOrder.get(right.id) ?? Number.MAX_SAFE_INTEGER) || left.id.localeCompare(right.id));
2627
+ }
2628
+ };
2629
+ function scenarioOptions(spec) {
2630
+ return spec.providerOptions;
2631
+ }
2632
+ function scenarioBaseUrl(value) {
2633
+ let url;
2634
+ try {
2635
+ url = new URL(value);
2636
+ } catch {
2637
+ throw new Error("SCENARIO_API_BASE_URL must be an absolute HTTP(S) URL");
2638
+ }
2639
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
2640
+ throw new Error("SCENARIO_API_BASE_URL must use HTTP or HTTPS");
2641
+ }
2642
+ return url.href.replace(/\/$/, "");
2643
+ }
2644
+ function requestBody(spec, options) {
2645
+ return {
2646
+ ...options.parameters ?? {},
2647
+ prompt: spec.prompt,
2648
+ width: spec.width,
2649
+ height: spec.height,
2650
+ numOutputs: options.numOutputs ?? 1,
2651
+ ...spec.seed != null ? { seed: spec.seed } : {}
2652
+ };
2653
+ }
2654
+ function validateParameters(parameters) {
2655
+ if (!parameters || typeof parameters !== "object" || Array.isArray(parameters)) {
2656
+ throw new Error("Scenario parameters must be a JSON object");
2657
+ }
2658
+ for (const key of Object.keys(parameters)) {
2659
+ if (PROTECTED_PARAMETERS.has(key)) {
2660
+ throw new Error(`Scenario parameters.${key} is managed by PixelKiln and cannot be overridden`);
2661
+ }
2662
+ }
2663
+ assertJsonValue(parameters, "Scenario parameters");
2664
+ }
2665
+ function assertJsonValue(value, label, seen = /* @__PURE__ */ new Set()) {
2666
+ if (value == null || typeof value === "string" || typeof value === "boolean") return;
2667
+ if (typeof value === "number") {
2668
+ if (!Number.isFinite(value)) throw new Error(`${label} contains a non-finite number`);
2669
+ return;
2670
+ }
2671
+ if (typeof value !== "object") throw new Error(`${label} must contain only JSON values`);
2672
+ if (!Array.isArray(value)) {
2673
+ const prototype = Object.getPrototypeOf(value);
2674
+ if (prototype !== Object.prototype && prototype !== null) {
2675
+ throw new Error(`${label} must contain only plain JSON objects`);
2676
+ }
2677
+ }
2678
+ if (seen.has(value)) throw new Error(`${label} contains a circular value`);
2679
+ seen.add(value);
2680
+ if (Array.isArray(value)) {
2681
+ value.forEach((item, index) => assertJsonValue(item, `${label}[${index}]`, seen));
2682
+ } else {
2683
+ for (const [key, item] of Object.entries(value)) {
2684
+ assertJsonValue(item, `${label}.${key}`, seen);
2685
+ }
2686
+ }
2687
+ seen.delete(value);
2688
+ }
2689
+ function jobRef(jobId, projectId) {
2690
+ const url = new URL("scenario://job");
2691
+ url.searchParams.set("id", jobId);
2692
+ if (projectId) url.searchParams.set("projectId", projectId);
2693
+ return url.href;
2694
+ }
2695
+ function parseJobRef(value) {
2696
+ if (!value.startsWith(`${SOURCE_PROTOCOL2}//`)) return { jobId: value };
2697
+ const url = new URL(value);
2698
+ const jobId = url.searchParams.get("id");
2699
+ if (url.protocol !== SOURCE_PROTOCOL2 || url.hostname !== "job" || !jobId) {
2700
+ throw new Error("Invalid durable Scenario job reference");
2701
+ }
2702
+ const projectId = url.searchParams.get("projectId") || void 0;
2703
+ return { jobId, ...projectId ? { projectId } : {} };
2704
+ }
2705
+ function assetRef(assetId, projectId) {
2706
+ const url = new URL("scenario://asset");
2707
+ url.searchParams.set("id", assetId);
2708
+ if (projectId) url.searchParams.set("projectId", projectId);
2709
+ return url.href;
2710
+ }
2711
+ function parseAssetRef(value) {
2712
+ const url = new URL(value);
2713
+ const assetId = url.searchParams.get("id");
2714
+ if (url.protocol !== SOURCE_PROTOCOL2 || url.hostname !== "asset" || !assetId) {
2715
+ throw new Error("Invalid durable Scenario asset reference");
2716
+ }
2717
+ const projectId = url.searchParams.get("projectId") || void 0;
2718
+ return { assetId, ...projectId ? { projectId } : {} };
2719
+ }
2720
+ function readJobId(value) {
2721
+ if (!value || typeof value !== "object") return null;
2722
+ const job = value;
2723
+ const id = job.jobId ?? job.id;
2724
+ return typeof id === "string" && id ? id : null;
2725
+ }
2726
+ function isRecord(value) {
2727
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
2728
+ }
2729
+ function scenarioAssetIds(job) {
2730
+ const ids = job.metadata?.assetIds;
2731
+ return Array.isArray(ids) ? [...new Set(ids.filter((id) => typeof id === "string" && id.length > 0))] : [];
2732
+ }
2733
+ function assetDownloadUrl(asset) {
2734
+ return asset.originalFileUrl || asset.url;
2735
+ }
2736
+ function assertPngAsset(asset) {
2737
+ if (asset.originalMimeType && asset.originalMimeType !== MediaType.PNG) {
2738
+ throw new Error(
2739
+ `Scenario asset ${asset.id} is ${asset.originalMimeType}; PixelKiln's Scenario MVP requires PNG output`
2740
+ );
2741
+ }
2742
+ }
2743
+ function jobMetadata(job, options) {
2744
+ const billing = job.billing;
2745
+ const billedMain = optionalFiniteNonnegative(billing?.cuCost, "Scenario job returned invalid CU billing");
2746
+ const billedAddons = Object.values(numericRecord(billing?.cuCostDetails)).reduce((sum, amount) => sum + amount, 0);
2747
+ return {
2748
+ ...options ? {
2749
+ modelId: options.modelId,
2750
+ projectId: options.projectId ?? null,
2751
+ maxComputeUnits: options.maxComputeUnits
2752
+ } : {},
2753
+ apiJobId: job.jobId,
2754
+ billedComputeUnits: billedMain + billedAddons,
2755
+ billing: billing ?? null
2756
+ };
2757
+ }
2758
+ function scenarioJobError(job) {
2759
+ const raw = job.metadata?.error;
2760
+ const detail = typeof raw === "string" ? raw : raw && typeof raw === "object" && typeof raw.message === "string" ? String(raw.message) : "job did not complete";
2761
+ return `Scenario job ${job.status}: ${detail.slice(0, 500)}`;
2762
+ }
2763
+ function validProgress(progress) {
2764
+ if (typeof progress !== "number" || !Number.isFinite(progress) || progress < 0 || progress > 1) {
2765
+ return null;
2766
+ }
2767
+ return Math.round(progress * 100);
2768
+ }
2769
+ function finiteNonnegative(value, message5) {
2770
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) throw new Error(message5);
2771
+ return value;
2772
+ }
2773
+ function optionalFiniteNonnegative(value, message5) {
2774
+ if (value == null) return 0;
2775
+ return finiteNonnegative(value, message5);
2776
+ }
2777
+ function numericRecord(value) {
2778
+ if (value == null) return {};
2779
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
2780
+ throw new Error("Scenario returned invalid cost details");
2781
+ }
2782
+ const out = {};
2783
+ for (const [key, amount] of Object.entries(value)) {
2784
+ out[key] = finiteNonnegative(amount, `Scenario returned invalid cost detail "${key}"`);
2785
+ }
2786
+ return out;
2787
+ }
2788
+ function redactScenarioError(value, secrets) {
2789
+ let detail;
2790
+ if (typeof value === "string") detail = value;
2791
+ else if (value && typeof value === "object") {
2792
+ const record = value;
2793
+ const nested = record.error && typeof record.error === "object" ? record.error.message : null;
2794
+ detail = typeof record.message === "string" ? record.message : typeof nested === "string" ? nested : typeof record.detail === "string" ? record.detail : "request failed";
2795
+ } else detail = "request failed";
2796
+ for (const secret of secrets) {
2797
+ if (secret) detail = detail.split(secret).join("[redacted]");
2798
+ }
2799
+ return detail.slice(0, 500);
2800
+ }
2801
+
2318
2802
  // src/providers/registry.ts
2803
+ function providerCredentialEnvs(factory) {
2804
+ return factory.credentialEnvs ?? (factory.credentialEnv ? [factory.credentialEnv] : []);
2805
+ }
2319
2806
  var factories = /* @__PURE__ */ new Map();
2320
2807
  function registerProvider(factory) {
2321
2808
  const id = factory.id.trim();
@@ -2360,6 +2847,15 @@ registerProvider({
2360
2847
  return ComfyUIProvider.forOffline();
2361
2848
  }
2362
2849
  });
2850
+ registerProvider({
2851
+ id: "scenario",
2852
+ credentialEnvs: ["SCENARIO_SDK_API_KEY", "SCENARIO_SDK_API_SECRET"],
2853
+ create(mode2) {
2854
+ if (mode2 === "online") return ScenarioProvider.fromEnv();
2855
+ if (mode2 === "downloads") return ScenarioProvider.forDownloads();
2856
+ return ScenarioProvider.forOffline();
2857
+ }
2858
+ });
2363
2859
 
2364
2860
  // src/manifest.ts
2365
2861
  import { readFile as readFile2 } from "fs/promises";
@@ -3333,13 +3829,14 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
3333
3829
  lastSubmitAt = Date.now();
3334
3830
  try {
3335
3831
  const refs = styleImages.get(spec.styleId) ?? [];
3336
- const { jobId } = await provider.submit(spec, refs);
3832
+ const { jobId, metadata } = await provider.submit(spec, refs);
3337
3833
  upsert(lock, key, {
3338
3834
  jobId,
3339
3835
  // A multi-candidate generator routes through review; record the parent
3340
3836
  // so `pick` knows where to look.
3341
3837
  reviewObjectId: estimate.candidates > 1 && !spec.tileFeature ? jobId : null,
3342
- status: "processing"
3838
+ status: "processing",
3839
+ providerMetadata: metadata ? { ...lock.entries[key].providerMetadata, [provider.id]: metadata } : lock.entries[key].providerMetadata
3343
3840
  });
3344
3841
  inFlight.set(jobId, spec);
3345
3842
  submitted++;
@@ -3389,7 +3886,13 @@ async function poll(provider, lock, lockPath, opts = {}) {
3389
3886
  upsert(lock, key, {
3390
3887
  status: "review",
3391
3888
  reviewObjectId: entry.jobId,
3392
- providerMetadata: state.metadata ? { ...entry.providerMetadata, [provider.id]: state.metadata } : entry.providerMetadata
3889
+ providerMetadata: state.metadata ? {
3890
+ ...entry.providerMetadata,
3891
+ [provider.id]: {
3892
+ ...entry.providerMetadata[provider.id],
3893
+ ...state.metadata
3894
+ }
3895
+ } : entry.providerMetadata
3393
3896
  });
3394
3897
  result.review++;
3395
3898
  log2(` review ${key} (${state.candidateUrls.length} candidates)`);
@@ -3400,7 +3903,13 @@ async function poll(provider, lock, lockPath, opts = {}) {
3400
3903
  objectId: state.objectId,
3401
3904
  sourceUrl: sourceUrls[0]?.url ?? null,
3402
3905
  sourceUrls,
3403
- providerMetadata: state.metadata ? { ...entry.providerMetadata, [provider.id]: state.metadata } : entry.providerMetadata,
3906
+ providerMetadata: state.metadata ? {
3907
+ ...entry.providerMetadata,
3908
+ [provider.id]: {
3909
+ ...entry.providerMetadata[provider.id],
3910
+ ...state.metadata
3911
+ }
3912
+ } : entry.providerMetadata,
3404
3913
  error: null
3405
3914
  });
3406
3915
  result.completed++;
@@ -3760,7 +4269,7 @@ async function doctor(loaded, specs, lock, lockPath, opts = {}) {
3760
4269
  add(
3761
4270
  checkId,
3762
4271
  "error",
3763
- !target.apiKeyPresent ? `${target.credentialEnv ?? (legacyTarget ? "PIXELLAB_API_KEY" : `${target.id} credential`)} is not configured` : `${target.id} is not configured`
4272
+ !target.apiKeyPresent ? `${target.missingCredentialEnvs?.length ? target.missingCredentialEnvs.join(", ") : target.credentialEnv ?? (legacyTarget ? "PIXELLAB_API_KEY" : `${target.id} credential`)} ${target.missingCredentialEnvs && target.missingCredentialEnvs.length > 1 ? "are" : "is"} not configured` : `${target.id} is not configured`
3764
4273
  );
3765
4274
  } else if (!target.provider.balance && !target.provider.checkConnection) {
3766
4275
  add(checkId, "ok", `${target.id} configured; connectivity check unavailable`);
@@ -4381,7 +4890,7 @@ async function runPicker(provider, lock, lockPath, opts = {}) {
4381
4890
  const entry = lock.entries[key];
4382
4891
  if (!group || !entry?.reviewObjectId) continue;
4383
4892
  if (!Number.isInteger(index) || index < 0 || index >= group.frameUrls.length) continue;
4384
- const { objectId, sourceUrl } = await requireSelectCandidate(provider)(
4893
+ const { objectId, sourceUrl, metadata } = await requireSelectCandidate(provider)(
4385
4894
  entry.reviewObjectId,
4386
4895
  index,
4387
4896
  `asset:${entry.assetId}`,
@@ -4393,7 +4902,14 @@ async function runPicker(provider, lock, lockPath, opts = {}) {
4393
4902
  candidateIndex: index,
4394
4903
  sourceUrl: sourceUrl ?? group.frameUrls[index] ?? null,
4395
4904
  sourceUrls: [],
4396
- provider: provider.id
4905
+ provider: provider.id,
4906
+ providerMetadata: metadata ? {
4907
+ ...entry.providerMetadata,
4908
+ [provider.id]: {
4909
+ ...entry.providerMetadata[provider.id],
4910
+ ...metadata
4911
+ }
4912
+ } : entry.providerMetadata
4397
4913
  });
4398
4914
  selected++;
4399
4915
  log2(` picked ${key} \u2192 candidate ${index + 1}`);
@@ -6038,16 +6554,16 @@ async function approveQualityRecord(recordPath, approval) {
6038
6554
 
6039
6555
  // src/pipeline/tileset-export.ts
6040
6556
  function normalizeTileRules(raw) {
6041
- if (!isRecord(raw)) return null;
6557
+ if (!isRecord2(raw)) return null;
6042
6558
  const ruleType = raw.rule_type;
6043
6559
  const arity = raw.arity;
6044
6560
  if (ruleType !== "edge" && ruleType !== "corner" && ruleType !== "outline") return null;
6045
6561
  if (arity !== 4 && arity !== 6) return null;
6046
6562
  const masks = {};
6047
- if (isRecord(raw.tiles)) {
6563
+ if (isRecord2(raw.tiles)) {
6048
6564
  for (const [key, value] of Object.entries(raw.tiles)) {
6049
6565
  const index = tileIndex(key);
6050
- const mask = typeof value === "number" ? value : isRecord(value) && typeof value.bitmask === "number" ? value.bitmask : isRecord(value) && typeof value.mask === "number" ? value.mask : null;
6566
+ const mask = typeof value === "number" ? value : isRecord2(value) && typeof value.bitmask === "number" ? value.bitmask : isRecord2(value) && typeof value.mask === "number" ? value.mask : null;
6051
6567
  if (index !== null && mask !== null && Number.isInteger(mask) && mask >= 0) masks[index] = mask;
6052
6568
  }
6053
6569
  }
@@ -6078,7 +6594,7 @@ function exportTileset(entry, spec, options) {
6078
6594
  );
6079
6595
  }
6080
6596
  const metadata = entry.providerMetadata?.[entry.provider];
6081
- const rawRules = isRecord(metadata) ? metadata.tileRules : void 0;
6597
+ const rawRules = isRecord2(metadata) ? metadata.tileRules : void 0;
6082
6598
  const rules = normalizeTileRules(rawRules);
6083
6599
  const frames = packed.atlas.frames;
6084
6600
  const generic = buildGeneric(
@@ -6086,7 +6602,7 @@ function exportTileset(entry, spec, options) {
6086
6602
  options.imageName,
6087
6603
  packed.atlas,
6088
6604
  frames,
6089
- isRecord(rawRules) ? rawRules : null,
6605
+ isRecord2(rawRules) ? rawRules : null,
6090
6606
  rules
6091
6607
  );
6092
6608
  if (options.format === "generic") {
@@ -6288,7 +6804,7 @@ function tileIndex(key) {
6288
6804
  const match = key.match(/(?:^|[_-])(\d+)$/);
6289
6805
  return match ? Number(match[1]) : null;
6290
6806
  }
6291
- function isRecord(value) {
6807
+ function isRecord2(value) {
6292
6808
  return typeof value === "object" && value !== null && !Array.isArray(value);
6293
6809
  }
6294
6810
 
@@ -8330,11 +8846,14 @@ async function main() {
8330
8846
  )].sort();
8331
8847
  const providers2 = providerIds.map((id) => {
8332
8848
  const factory = providerFactory(id);
8333
- const apiKeyPresent = !factory.credentialEnv || Boolean(process.env[factory.credentialEnv]);
8849
+ const credentialEnvs = providerCredentialEnvs(factory);
8850
+ const missingCredentialEnvs = credentialEnvs.filter((name) => !process.env[name]);
8851
+ const apiKeyPresent = missingCredentialEnvs.length === 0;
8334
8852
  return {
8335
8853
  id,
8336
8854
  apiKeyPresent,
8337
8855
  credentialEnv: factory.credentialEnv,
8856
+ missingCredentialEnvs,
8338
8857
  provider: !args.dryRun && apiKeyPresent ? createProvider(id, "online") : void 0
8339
8858
  };
8340
8859
  });