@vectoriox/iox-ui 5.54.0 → 5.56.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,5 +1,6 @@
1
- import { MotionId, MoodId, TintId, DensityId, DesignMotion, DesignChoices, DesignMode, StylePreset, AnimateId, ShapeId } from '@vectoriox/iox-ui/design-core';
2
- import { FactKey, PathIssue, SiteTemplate } from '@vectoriox/iox-ui/generation-core';
1
+ import { MotionId, MoodId, TintId, DensityId, DesignMotion, DesignChoices, DesignMode, StylePreset, ShapeId, AnimateId } from '@vectoriox/iox-ui/design-core';
2
+ import { FactKey, PathIssue, JsonSchema, SiteTemplate, ShapeEntry, AiGenerationStage, AiStep, SitePlan, PlanEdit, PlanChange } from '@vectoriox/iox-ui/generation-core';
3
+ export { JsonSchema } from '@vectoriox/iox-ui/generation-core';
3
4
 
4
5
  declare const SPEC_HARNESS_SOURCE: {
5
6
  readonly repo: "nextlevelbuilder/ui-ux-pro-max-skill";
@@ -2572,18 +2573,7 @@ interface AnalysisContext {
2572
2573
  /** At most this many moods (the closed list a preset carries too). */
2573
2574
  declare const MAX_ANALYSIS_MOODS = 3;
2574
2575
  declare function validateSiteAnalysis(value: unknown, ctx: AnalysisContext): AnalysisValidation;
2575
- /**
2576
- * The structured-output schema a model is given to write a `SiteAnalysis` (AI plan §3c/§3d, M0-5): built from the
2577
- * SAME shape table `validateSiteAnalysis` reads (keys, which are required, each key's explanation as its
2578
- * description) and the same closed lists (30 business types, needs, facts, moods, avoids) — so the shape the model
2579
- * is told about is the shape the gate refuses. Written to the strictest common subset of JSON Schema the providers'
2580
- * native strict modes accept (Anthropic `output_config.format`, OpenRouter `json_schema` + `strict`): every object
2581
- * closes with `additionalProperties: false`; no length, count or number bounds (the gate checks those). The
2582
- * schema is a hint the provider enforces; `validateSiteAnalysis` stays the gate.
2583
- */
2584
- type JsonSchema = {
2585
- [key: string]: unknown;
2586
- };
2576
+
2587
2577
  /** The name a provider shows the format under (`json_schema.name`). */
2588
2578
  declare const SITE_ANALYSIS_SCHEMA_NAME = "site_analysis";
2589
2579
  declare function siteAnalysisJsonSchema(): JsonSchema;
@@ -2673,5 +2663,314 @@ interface StructureChoice {
2673
2663
  */
2674
2664
  declare function structureFor(typeId: string | null | undefined): StructureChoice;
2675
2665
 
2676
- export { ANIMATE_ADVICE_STEPS, AnalysisIssueCode, AnalysisLocale, AvoidCheck, BUSINESS_TYPES, BUSINESS_TYPE_IDS, BookingNeed, COPY_LENGTH_ORDER, CopyLength, HARNESS_SOURCE, LITTLE_MOVEMENT_AVOID, LITTLE_MOVEMENT_CAP, LintKind, MAX_ANALYSIS_MOODS, NEEDS, NEED_IDS, NeedSource, PresetFit, RANK_WEIGHTS, READ_ANIMATE, READ_WEIGHTS, RankReason, SITE_ANALYSIS_SCHEMA_NAME, SPEC_ANALYSIS_SHAPE, SPEC_BUSINESS_TYPES, SPEC_HARNESS_EXAMPLES, SPEC_HARNESS_SOURCE, SPEC_NEEDS, SPEC_STRUCTURES, SPEC_USER_AVOIDS, STRUCTURES, StyleReadGround, StyleReadHeadings, StyleReadMotion, StyleReadSourceKind, USER_AVOID_IDS, VoiceFormality, VoicePerson, addressesIn, analysisPromptContext, businessTypeById, calmerChoice, inferNeeds, isLookRule, lintDesign, lookMatches, mapStyleRead, matchBusinessType, matchBusinessTypes, motionAdvice, motionFor, normalizeAddress, oklchHue, presetHasSerifHeadings, presetLook, presetReason, rankStylePresets, rankWithReferences, siteAnalysisJsonSchema, structureFor, userAvoidRule, validateSiteAnalysis, withoutAddress };
2677
- export type { AnalysisContext, AnalysisIssue, AnalysisValidation, AvoidRule, BusinessType, BusinessTypeMatch, CheckRule, FactRead, GoodFit, JsonSchema, LintFinding, LintSite, LookCondition, LookDesign, LookRule, MotionBasis, MotionInput, MotionStart, NeedDef, NeedId, PromptContext, PromptContextOptions, PromptSection, RankInput, RankedPreset, SiteAnalysis, SiteNeed, StructureChoice, StructureId, StyleHints, StyleRead, UserAvoidId };
2666
+ /**
2667
+ * The palette MEASURE (AI plan §3k, R-D2: a colour is measured, never asked of a model) — ONE function for every
2668
+ * reader: the browser measures a picture at upload (canvas pixels → `pixelSamples`), the server measures a picture it
2669
+ * reads and a website's CSS (declared colours, weighted by where they paint). Weighted samples in, ≤ 6 colours out,
2670
+ * by a deterministic median cut: the same input gives the same hexes on every run, in every runtime. Pure.
2671
+ */
2672
+ /** One colour seen, and how much of the reference it covers (pixels, or a CSS colour's weight). */
2673
+ interface ColourSample {
2674
+ /** 0–255 each. */
2675
+ r: number;
2676
+ g: number;
2677
+ b: number;
2678
+ weight: number;
2679
+ }
2680
+ interface PaletteColour {
2681
+ hex: string;
2682
+ /** Its part of the whole, 0–1; the palette's shares add up to 1. */
2683
+ share: number;
2684
+ }
2685
+ interface PaletteReading {
2686
+ palette: PaletteColour[];
2687
+ ground: `${StyleReadGround}`;
2688
+ /** The most frequent saturated colour — a CANDIDATE seed (the AA audit decides, `referenceSeed`). */
2689
+ brand?: string;
2690
+ }
2691
+ /** At most this many colours (the StyleRead's palette). */
2692
+ declare const PALETTE_SIZE = 6;
2693
+ /** The sampling ceiling for a picture — every n-th pixel, so a 12 MP photo costs what a thumbnail does. */
2694
+ declare const MAX_PIXEL_SAMPLES = 20000;
2695
+ /** OKLab lightness / chroma thresholds the reading speaks in. */
2696
+ declare const PALETTE_THRESHOLDS: {
2697
+ /** Above: a light colour. */
2698
+ readonly light: 0.72;
2699
+ /** Below: a dark colour. */
2700
+ readonly dark: 0.38;
2701
+ /** At least this share of light (or dark) makes the ground light (or dark); otherwise mixed. */
2702
+ readonly groundShare: 0.55;
2703
+ /** A brand candidate is at least this colourful… */
2704
+ readonly brandChroma: 0.08;
2705
+ /** …and neither near-white nor near-black. */
2706
+ readonly brandMinL: 0.25;
2707
+ readonly brandMaxL: 0.92;
2708
+ /** Two palette colours closer than this (OKLab distance) are one colour. */
2709
+ readonly mergeDistance: 0.035;
2710
+ };
2711
+ /** Every n-th opaque pixel of an RGBA buffer (canvas `ImageData.data`, a decoder's pixels), weight 1 each. */
2712
+ declare function pixelSamples(rgba: ArrayLike<number>, width: number, height: number, max?: number): ColourSample[];
2713
+ /** A palette (hex + share) back to samples — what merging two readings measures. */
2714
+ declare function paletteSamples(palette: ReadonlyArray<PaletteColour>, weight?: number): ColourSample[];
2715
+ /** Weighted samples → ≤ 6 colours, largest share first (ties by hex), shares adding up to 1. */
2716
+ declare function measurePalette(samples: ReadonlyArray<ColourSample>, size?: number): PaletteColour[];
2717
+ /** Light, dark or mixed: the share of the palette that is light (or dark). */
2718
+ declare function paletteGround(palette: ReadonlyArray<PaletteColour>): `${StyleReadGround}`;
2719
+ /** The most frequent saturated colour that is neither near-white nor near-black, or undefined. */
2720
+ declare function paletteBrand(palette: ReadonlyArray<PaletteColour>): string | undefined;
2721
+ /** The whole reading of a set of samples: the palette, its ground, its brand candidate. */
2722
+ declare function readPalette(samples: ReadonlyArray<ColourSample>): PaletteReading;
2723
+
2724
+ /**
2725
+ * Which brand colour a style starts from when the owner gave references (AI plan §3k, R4 "the AA gate decides the
2726
+ * colour"): the reading's measured `brand` — ONLY when the preset, worn in EVERY mode it offers with that colour as its
2727
+ * seed, passes the same contrast audit the contract gates every preset with (`auditDesignContrast`: every element
2728
+ * style in every section scheme, form parts, focus rings). Otherwise the preset's own seed stays and the reference
2729
+ * colour is a swatch the Colour tab marks "doesn't pass contrast here". The model never sees, picks or changes a hex.
2730
+ * Pure; the wizard (Design ▸ Colour) and the server ("Build with these") ask the same question.
2731
+ */
2732
+ interface ReferenceSeed {
2733
+ /** The seed to start from: the reference's when it passed, else the preset's. */
2734
+ seed: string;
2735
+ /** The reference colour measured, or null when there is none (no reading, nothing saturated). */
2736
+ reference: string | null;
2737
+ /** True when `seed` is the reference colour. */
2738
+ used: boolean;
2739
+ /** The modes the reference colour failed in (empty when used, or when there is no reference). */
2740
+ failsIn: Array<'light' | 'dark'>;
2741
+ }
2742
+ /** Every mode `presetId` offers passes the audit with `seed`. */
2743
+ declare function seedPassesPreset(presetId: string, seed: string): {
2744
+ passes: boolean;
2745
+ failsIn: Array<'light' | 'dark'>;
2746
+ };
2747
+ /** The seed a preset starts from, given a reading (or none). Unknown preset → null. */
2748
+ declare function referenceSeed(read: Pick<StyleRead, 'brand'> | null | undefined, presetId: string): ReferenceSeed | null;
2749
+
2750
+ /**
2751
+ * The READING call (AI plan §3d step 1, §3k R2, M1-1): ONE `text.fast` call reads the owner's words AND the pictures
2752
+ * they attached (image input) and answers with the SiteAnalysis and — when there are pictures — which ones are the
2753
+ * owner's own (Your photos) and which are pictures they like (references, style only), plus what the liked pictures
2754
+ * FEEL like: moods, the lettering, corners, spacing. Never a colour: the palette is MEASURED (`palette.ts`, R-D2).
2755
+ * The format is built from `READING_SHAPE` (keys + their explanations), the gate reads the same table. Pure.
2756
+ */
2757
+ /** Which group a picture belongs to (R-D3: from the words; own when the words don't say). */
2758
+ declare enum PictureRole {
2759
+ Own = "own",
2760
+ Reference = "reference"
2761
+ }
2762
+ /** At most this many pictures go to the reading call as image input (§3d: "up to 6 pictures"). */
2763
+ declare const MAX_READ_PICTURES = 6;
2764
+ declare const READING_SHAPE: Readonly<Record<string, ShapeEntry>>;
2765
+ interface PictureStyle {
2766
+ moods: MoodId[];
2767
+ headings?: `${StyleReadHeadings}`;
2768
+ shape?: ShapeId;
2769
+ density?: DensityId;
2770
+ }
2771
+ interface SiteReading {
2772
+ analysis: SiteAnalysis;
2773
+ /** Present when pictures were attached. */
2774
+ pictures?: {
2775
+ files: Array<{
2776
+ index: number;
2777
+ role: `${PictureRole}`;
2778
+ }>;
2779
+ style: PictureStyle;
2780
+ };
2781
+ }
2782
+ declare const SITE_READING_SCHEMA_NAME = "site_reading";
2783
+ /** The reading's format: the analysis alone when no picture was attached, else the analysis + the pictures. */
2784
+ declare function siteReadingJsonSchema(pictureCount: number): JsonSchema;
2785
+ declare enum ReadingIssueCode {
2786
+ Shape = "shape",
2787
+ UnknownKey = "unknown-key",
2788
+ Duplicate = "duplicate",
2789
+ InvalidValue = "invalid-value",
2790
+ /** A picture named twice, or one left out. */
2791
+ Pictures = "pictures"
2792
+ }
2793
+ type ReadingIssue = PathIssue<ReadingIssueCode> | AnalysisIssue;
2794
+ /** The reading's gate: the analysis by `validateSiteAnalysis` (paths under `analysis.` when pictures came too), the pictures by `READING_SHAPE`. */
2795
+ declare function validateSiteReading(value: unknown, ctx: {
2796
+ prompt: string;
2797
+ pictureCount: number;
2798
+ }): {
2799
+ valid: boolean;
2800
+ issues: ReadingIssue[];
2801
+ };
2802
+ /** A valid reading's answer → `SiteReading` (the analysis alone answers `{ analysis }`). */
2803
+ declare function decodeSiteReading(value: unknown, pictureCount: number): SiteReading;
2804
+ /** A reference picture as the server read it: its measured palette (and where it is). */
2805
+ interface MeasuredPicture {
2806
+ ref: string;
2807
+ palette: StyleRead['palette'];
2808
+ }
2809
+ /** The liked pictures as a StyleRead: their palettes measured together, what the model saw of them. */
2810
+ declare function pictureStyleRead(pictures: ReadonlyArray<MeasuredPicture>, style: PictureStyle | null | undefined): StyleRead | null;
2811
+ /**
2812
+ * Every reading of one generation (the pictures', each site's) → ONE StyleRead: palettes measured together (each
2813
+ * reading weighs the same), the ground re-read from that palette, moods by how often they were seen (≤ 3), the rest the
2814
+ * most common answer. Null when nothing was read.
2815
+ */
2816
+ declare function mergeStyleReads(reads: ReadonlyArray<StyleRead | null | undefined>): StyleRead | null;
2817
+
2818
+ /**
2819
+ * A generation as the screens read it (AI plan "M1 pipeline — the screens contract"): what iox-cms's
2820
+ * `GET …/ai/generations/:id` answers and the Thinking · Analysis · Design · Plan · Build screens render. ONE
2821
+ * definition: iox-cms builds it, cms-client types its client with it. Types and enums only — no transport.
2822
+ */
2823
+ /** Where one reader is (Thinking's three lines). */
2824
+ declare enum ReaderState {
2825
+ Wait = "wait",
2826
+ Run = "run",
2827
+ Done = "done",
2828
+ /** Nothing to read (no pictures, no address). */
2829
+ Skipped = "skipped",
2830
+ /** Refused on purpose (robots.txt, a private address…) — said in one plain line, never blocks the reading. */
2831
+ Refused = "refused",
2832
+ Failed = "failed"
2833
+ }
2834
+ /** Why a website address was not read (the screens' i18n `ai.siteRead.<reason>`). */
2835
+ declare enum SiteReadRefusal {
2836
+ /** robots.txt disallows our user agent (`IOX-Import`). */
2837
+ Robots = "robots",
2838
+ InvalidAddress = "invalid-address",
2839
+ PrivateAddress = "private-address",
2840
+ Unreachable = "unreachable",
2841
+ Timeout = "timeout",
2842
+ TooLarge = "too-large",
2843
+ NotHtml = "not-html",
2844
+ HttpError = "http-error",
2845
+ TooManyRedirects = "redirects"
2846
+ }
2847
+ /** What a sentence in the Plan prompt box did. */
2848
+ declare enum PlanChangeOutcome {
2849
+ Applied = "applied",
2850
+ /** The closed operations cannot say it — one line, the map unchanged (the call still cost). */
2851
+ Cannot = "cannot",
2852
+ /** Its edits did not make a valid plan, twice — the map unchanged. */
2853
+ Invalid = "invalid"
2854
+ }
2855
+ interface GenerationFileView {
2856
+ key: string;
2857
+ url: string;
2858
+ role: `${PictureRole}`;
2859
+ }
2860
+ interface SiteReaderView {
2861
+ address: string;
2862
+ state: `${ReaderState}`;
2863
+ reason?: `${SiteReadRefusal}`;
2864
+ }
2865
+ interface GenerationReadersView {
2866
+ words: {
2867
+ state: `${ReaderState}`;
2868
+ };
2869
+ pictures: {
2870
+ state: `${ReaderState}`;
2871
+ count: number;
2872
+ };
2873
+ site: {
2874
+ state: `${ReaderState}`;
2875
+ addresses: SiteReaderView[];
2876
+ };
2877
+ }
2878
+ interface PlanRevisionView {
2879
+ plan: SitePlan;
2880
+ at: string;
2881
+ sentence?: string;
2882
+ edits?: PlanEdit[];
2883
+ changed?: PlanChange[];
2884
+ }
2885
+ interface AiGenerationView {
2886
+ _id: string;
2887
+ organization: string;
2888
+ stage: `${AiGenerationStage}`;
2889
+ /** The model step the runner is working on; null = the owner's move. */
2890
+ task: `${AiStep}` | null;
2891
+ intake: {
2892
+ prompt: string;
2893
+ files: GenerationFileView[];
2894
+ addresses: string[];
2895
+ auto: boolean;
2896
+ };
2897
+ readers: GenerationReadersView;
2898
+ reads: {
2899
+ analysis?: {
2900
+ value: SiteAnalysis;
2901
+ model: string;
2902
+ usageIds: string[];
2903
+ };
2904
+ style?: StyleRead | null;
2905
+ };
2906
+ /** What Design and Plan use: the owner's edit, else the reading. */
2907
+ analysis: SiteAnalysis | null;
2908
+ analysisRevision: number;
2909
+ design?: {
2910
+ presetId?: string;
2911
+ design: DesignChoices;
2912
+ auto?: boolean;
2913
+ };
2914
+ plan?: {
2915
+ revisions: PlanRevisionView[];
2916
+ model: string;
2917
+ lastChange?: {
2918
+ sentence: string;
2919
+ outcome: `${PlanChangeOutcome}`;
2920
+ message?: string;
2921
+ at: string;
2922
+ };
2923
+ };
2924
+ siteBuildId?: string;
2925
+ models: Partial<Record<`${AiStep}`, string>>;
2926
+ estimate: number;
2927
+ cap: number;
2928
+ /** Derived from the ledger — the running credit total. */
2929
+ spent: number;
2930
+ error?: {
2931
+ code: string;
2932
+ detail: string;
2933
+ providerMessage?: string;
2934
+ issues?: Array<{
2935
+ code: string;
2936
+ path: string;
2937
+ }>;
2938
+ };
2939
+ }
2940
+ /** `POST …/ai/generations`. */
2941
+ interface GenerationStartRequest {
2942
+ prompt: string;
2943
+ files?: Array<{
2944
+ key: string;
2945
+ role?: `${PictureRole}`;
2946
+ }>;
2947
+ addresses?: string[];
2948
+ }
2949
+ /** The longest message, the most files and addresses a generation starts from (Start's limits). */
2950
+ declare const GENERATION_LIMITS: {
2951
+ readonly promptChars: 4000;
2952
+ readonly files: 12;
2953
+ readonly addresses: 3;
2954
+ readonly sentenceChars: 500;
2955
+ };
2956
+
2957
+ /**
2958
+ * The look a business STARTS from (AI plan §3d "Build with these", §3o, MO-D3) — ONE rule for the wizard's Design step
2959
+ * (a Look pick) and the server's "Build with these": the preset at its defaults, Scrolling · How animated · Between
2960
+ * pages started from the kind of business (and the references), the owner's own Motion choices kept, and — given
2961
+ * references — the best-ranked style with the reference colour as its seed when that colour passes AA in every mode
2962
+ * the style offers (`referenceSeed`). Pure.
2963
+ */
2964
+ /** A preset for this business: its choices, Motion from the business (`motionFor`), what the owner already set kept. */
2965
+ declare function presetDesign(presetId: string, input: MotionInput | string | null, kept?: Partial<Pick<DesignChoices, 'scroll' | 'animate' | 'between' | 'motion'>>, read?: StyleRead | null): DesignChoices | null;
2966
+ interface StartingDesign {
2967
+ presetId: string;
2968
+ design: DesignChoices;
2969
+ /** The reference colour was used as the seed (it passed AA in every mode the style offers). */
2970
+ referenceColour: boolean;
2971
+ }
2972
+ /** The look a business starts from: its best-ranked style (with the references' boost), at its defaults. */
2973
+ declare function startingDesign(input: (RankInput & MotionInput) | string | null, read?: StyleRead | null): StartingDesign | null;
2974
+
2975
+ export { ANIMATE_ADVICE_STEPS, AnalysisIssueCode, AnalysisLocale, AvoidCheck, BUSINESS_TYPES, BUSINESS_TYPE_IDS, BookingNeed, COPY_LENGTH_ORDER, CopyLength, GENERATION_LIMITS, HARNESS_SOURCE, LITTLE_MOVEMENT_AVOID, LITTLE_MOVEMENT_CAP, LintKind, MAX_ANALYSIS_MOODS, MAX_PIXEL_SAMPLES, MAX_READ_PICTURES, NEEDS, NEED_IDS, NeedSource, PALETTE_SIZE, PALETTE_THRESHOLDS, PictureRole, PlanChangeOutcome, PresetFit, RANK_WEIGHTS, READING_SHAPE, READ_ANIMATE, READ_WEIGHTS, RankReason, ReaderState, ReadingIssueCode, SITE_ANALYSIS_SCHEMA_NAME, SITE_READING_SCHEMA_NAME, SPEC_ANALYSIS_SHAPE, SPEC_BUSINESS_TYPES, SPEC_HARNESS_EXAMPLES, SPEC_HARNESS_SOURCE, SPEC_NEEDS, SPEC_STRUCTURES, SPEC_USER_AVOIDS, STRUCTURES, SiteReadRefusal, StyleReadGround, StyleReadHeadings, StyleReadMotion, StyleReadSourceKind, USER_AVOID_IDS, VoiceFormality, VoicePerson, addressesIn, analysisPromptContext, businessTypeById, calmerChoice, decodeSiteReading, inferNeeds, isLookRule, lintDesign, lookMatches, mapStyleRead, matchBusinessType, matchBusinessTypes, measurePalette, mergeStyleReads, motionAdvice, motionFor, normalizeAddress, oklchHue, paletteBrand, paletteGround, paletteSamples, pictureStyleRead, pixelSamples, presetDesign, presetHasSerifHeadings, presetLook, presetReason, rankStylePresets, rankWithReferences, readPalette, referenceSeed, seedPassesPreset, siteAnalysisJsonSchema, siteReadingJsonSchema, startingDesign, structureFor, userAvoidRule, validateSiteAnalysis, validateSiteReading, withoutAddress };
2976
+ export type { AiGenerationView, AnalysisContext, AnalysisIssue, AnalysisValidation, AvoidRule, BusinessType, BusinessTypeMatch, CheckRule, ColourSample, FactRead, GenerationFileView, GenerationReadersView, GenerationStartRequest, GoodFit, LintFinding, LintSite, LookCondition, LookDesign, LookRule, MeasuredPicture, MotionBasis, MotionInput, MotionStart, NeedDef, NeedId, PaletteColour, PaletteReading, PictureStyle, PlanRevisionView, PromptContext, PromptContextOptions, PromptSection, RankInput, RankedPreset, ReadingIssue, ReferenceSeed, SiteAnalysis, SiteNeed, SiteReaderView, SiteReading, StartingDesign, StructureChoice, StructureId, StyleHints, StyleRead, UserAvoidId };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vectoriox/iox-ui",
3
- "version": "5.54.0",
3
+ "version": "5.56.0",
4
4
  "peerDependencies": {
5
5
  "@angular/common": ">=20.0.0",
6
6
  "@angular/core": ">=20.0.0",
@@ -30,6 +30,15 @@ export enum AiStep {
30
30
 
31
31
  export const AI_STEPS: ReadonlyArray<AiStep> = Object.values(AiStep);
32
32
 
33
+ /** Each step's name on the wire (the control plane's `label`, the console) — the screens translate their own. */
34
+ export const AI_STEP_LABELS: Readonly<Record<AiStep, string>> = {
35
+ [AiStep.ReadMessage]: 'Read your message',
36
+ [AiStep.PlanSite]: 'Plan the site',
37
+ [AiStep.ChangePlan]: 'Change the plan',
38
+ [AiStep.MakePictures]: 'Make pictures',
39
+ [AiStep.NameStructures]: 'Name repeated structures',
40
+ };
41
+
33
42
  /** What a model must be able to do for a step to run on it. */
34
43
  export enum AiCapability {
35
44
  StructuredOutput = 'structuredOutputs',
@@ -50,7 +59,8 @@ export const AI_STEP_TYPICAL_TOKENS: Readonly<Record<AiStep, AiTokenCounts>> = {
50
59
  [AiStep.ReadMessage]: { input: 8_800, output: 1_500, cacheWrite: 0, cacheRead: 0 },
51
60
  [AiStep.PlanSite]: { input: 12_000, output: 7_000, cacheWrite: 0, cacheRead: 0 },
52
61
  [AiStep.ChangePlan]: { input: 6_000, output: 500, cacheWrite: 0, cacheRead: 0 },
53
- [AiStep.MakePictures]: { input: 1_120, output: 0, cacheWrite: 0, cacheRead: 0 },
62
+ // a 1 K picture is 1,120 image tokens (Google's figure for Nano Banana 2) — I-D4
63
+ [AiStep.MakePictures]: { input: 0, output: 0, cacheWrite: 0, cacheRead: 0, imageOutput: 1_120 },
54
64
  [AiStep.NameStructures]: { input: 80_000, output: 10_000, cacheWrite: 0, cacheRead: 0 },
55
65
  };
56
66
 
@@ -62,6 +72,8 @@ export interface AiTokenCounts {
62
72
  cacheWrite: number;
63
73
  /** Read from the prompt cache (priced well below input). */
64
74
  cacheRead: number;
75
+ /** Image tokens written (a picture step) — priced at `imageOutput`. */
76
+ imageOutput?: number;
65
77
  }
66
78
 
67
79
  /** US dollars per million tokens, per class. A class without a price is priced as input. */
@@ -70,6 +82,8 @@ export interface AiTokenPrice {
70
82
  output: number;
71
83
  cacheWrite?: number;
72
84
  cacheRead?: number;
85
+ /** Image-output tokens; a model without the price is priced at `output`. */
86
+ imageOutput?: number;
73
87
  }
74
88
 
75
89
  /** A model of the vetted list (served by the control plane with the entitlements, §3n) — its price travels with it. */
@@ -103,7 +117,8 @@ export function costUsd(tokens: AiTokenCounts, price: AiTokenPrice): number {
103
117
  return tokens.input * perToken(price.input, 0)
104
118
  + tokens.output * perToken(price.output, 0)
105
119
  + tokens.cacheWrite * perToken(price.cacheWrite, price.input)
106
- + tokens.cacheRead * perToken(price.cacheRead, price.input);
120
+ + tokens.cacheRead * perToken(price.cacheRead, price.input)
121
+ + (tokens.imageOutput ?? 0) * perToken(price.imageOutput, price.output);
107
122
  }
108
123
 
109
124
  /** Dollars → credits, kept to a millionth of a credit (a short read costs a fraction of one). */