{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://artifacts.apis.io/creativefabrica/json-schema/creativefabrica-model-detail-schema.json",
  "title": "ModelDetail",
  "x-generated": "2026-11-04",
  "x-method": "derived",
  "x-generator": "derive-json-schema.py",
  "x-source": "openapi/creativefabrica-idea-ai-genai-openapi.yml#/components/schemas/ModelDetail",
  "allOf": [
    {
      "$ref": "#/$defs/ModelSummary"
    },
    {
      "type": "object",
      "required": [
        "capabilities"
      ],
      "properties": {
        "capabilities": {
          "type": "object",
          "description": "Per-modality capabilities, keyed by modality name (e.g.\n`textToVideo`, `mediaToVideo`). Only the modalities listed in\n`modalities` appear as keys.\n",
          "additionalProperties": {
            "$ref": "#/$defs/ModalityCapability"
          }
        }
      }
    }
  ],
  "$defs": {
    "ConstraintRule": {
      "type": "object",
      "description": "A cross-role rule. `type` discriminates which fields are set;\n`description` is always present and human-readable.\n",
      "required": [
        "type",
        "description"
      ],
      "properties": {
        "type": {
          "type": "string",
          "enum": [
            "mutuallyExclusive",
            "requires",
            "totalMax",
            "totalMaxMb",
            "totalMaxDuration",
            "conditionalMaxOption",
            "conditionalMinOption",
            "conditionalUnavailableOptions",
            "optionCountsAsRole",
            "maxCombinedDurationWithOption",
            "aspectFit"
          ]
        },
        "description": {
          "type": "string"
        },
        "groups": {
          "type": "array",
          "description": "`mutuallyExclusive`: files may appear in at most one group.",
          "items": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "role": {
          "type": "string",
          "description": "`requires`: the role that triggers the requirement. `optionCountsAsRole`: the role the option is counted against."
        },
        "requiresAll": {
          "type": "array",
          "description": "`requires`: every listed role must have a file.",
          "items": {
            "type": "string"
          }
        },
        "requiresAny": {
          "type": "array",
          "description": "`requires`: at least one listed role must have a file.",
          "items": {
            "type": "string"
          }
        },
        "roles": {
          "type": "array",
          "description": "`totalMax` / `totalMaxMb` / `totalMaxDuration` / `maxCombinedDurationWithOption`: the roles summed. `aspectFit`: the roles whose images may be re-rendered.",
          "items": {
            "type": "string"
          }
        },
        "max": {
          "type": "integer",
          "description": "`totalMax`: cap on the summed file count."
        },
        "maxMb": {
          "type": "integer",
          "description": "`totalMaxMb`: cap on the summed file size in MB."
        },
        "maxSeconds": {
          "type": "integer",
          "description": "`totalMaxDuration`: cap on the summed duration. `maxCombinedDurationWithOption`: cap on the summed duration of `roles` PLUS the value of `option` — for models whose output includes their input, such as a video extension."
        },
        "whenRole": {
          "type": "string",
          "description": "`conditionalMaxOption`: role whose presence triggers the cap. `conditionalUnavailableOptions`: role whose presence makes `options` unavailable."
        },
        "option": {
          "type": "string",
          "description": "`conditionalMaxOption`: the capped option name. `optionCountsAsRole`: the option that occupies a slot of `role`, so setting it leaves room for one fewer file in that role and subjects it to the same cross-role rules. `maxCombinedDurationWithOption`: the option added to the summed duration of `roles` before comparing against `maxSeconds`. `aspectFit`: the option holding the target aspect ratio."
        },
        "maxValue": {
          "type": "number",
          "format": "double",
          "description": "`conditionalMaxOption`: the lowered maximum."
        },
        "minValue": {
          "type": "number",
          "format": "double",
          "description": "`conditionalMinOption`: the raised minimum. A `whenRole` of `assetReference` means the floor applies because the request references a Studio Asset rather than because a file role is present."
        },
        "options": {
          "type": "array",
          "description": "`conditionalUnavailableOptions`: option names that do not apply (and should be hidden by the client) when `whenRole` is present.",
          "items": {
            "type": "string"
          }
        },
        "coinsPerInput": {
          "type": "integer",
          "format": "int64",
          "description": "`aspectFit`: coins added to the price for each re-rendered image, once per generation. An image is re-rendered when `option` holds an explicit ratio (not `auto`) that differs from the image's own ratio by more than `tolerance`. The decision needs the image's width and height: send them (input file metadata, or `widthPx`/`heightPx` when pricing) to have the re-render priced and applied. When a size is unknown nothing is re-rendered, `option` is dropped and the output follows the image."
        },
        "tolerance": {
          "type": "number",
          "format": "double",
          "description": "`aspectFit`: the relative ratio difference still treated as a match."
        }
      }
    },
    "DimensionLimits": {
      "type": "object",
      "description": "Geometric limits on visual media. Absent fields are unchecked.",
      "properties": {
        "allowedResolutions": {
          "type": "array",
          "items": {
            "type": "string"
          }
        },
        "minWidthPx": {
          "type": "integer"
        },
        "maxWidthPx": {
          "type": "integer"
        },
        "minHeightPx": {
          "type": "integer"
        },
        "maxHeightPx": {
          "type": "integer"
        },
        "minLongerSidePx": {
          "type": "integer"
        },
        "maxLongerSidePx": {
          "type": "integer"
        },
        "minShorterSidePx": {
          "type": "integer"
        },
        "maxShorterSidePx": {
          "type": "integer"
        },
        "minFramePixels": {
          "type": "integer"
        },
        "maxFramePixels": {
          "type": "integer"
        },
        "minAspectRatio": {
          "type": "number",
          "format": "double"
        },
        "maxAspectRatio": {
          "type": "number",
          "format": "double"
        }
      }
    },
    "DynamicPricing": {
      "type": "object",
      "required": [
        "type"
      ],
      "properties": {
        "type": {
          "type": "string",
          "enum": [
            "dynamic"
          ],
          "description": "The coin amount is computed per request from the validated options\nand input media (e.g. video duration × resolution). Get the exact\namount up front via `POST /v1/models/{model}/cost`.\n"
        },
        "inputs": {
          "$ref": "#/$defs/PricingInputs"
        }
      }
    },
    "FileConstraints": {
      "type": "object",
      "description": "Input file constraints. Flat fields (`maxFileCount`, `acceptedFormats`,\n...) apply to single-bucket modalities; `roles` + `rules` apply to\nrole-keyed modalities (`mediaToVideo`).\n",
      "properties": {
        "maxFileCount": {
          "type": "integer"
        },
        "acceptedFormats": {
          "type": "array",
          "items": {
            "type": "string"
          }
        },
        "maxFileSizeMb": {
          "type": "integer",
          "format": "int64"
        },
        "maxWidth": {
          "type": "integer",
          "description": "Larger inputs are downscaled to fit."
        },
        "maxHeight": {
          "type": "integer"
        },
        "roles": {
          "type": "object",
          "description": "Per-role limits, keyed by role name (`firstFrame`, `lastFrame`,\n`referenceImage`, `referenceAudio`, `referenceVideo`, `sourceVideo`).\n",
          "additionalProperties": {
            "$ref": "#/$defs/RoleConstraint"
          }
        },
        "rules": {
          "type": "array",
          "description": "Cross-role validation rules, evaluated in order.",
          "items": {
            "$ref": "#/$defs/ConstraintRule"
          }
        }
      }
    },
    "Modality": {
      "type": "string",
      "description": "What the generation transforms. Valid combinations per model are listed in the model catalog.",
      "enum": [
        "textToImage",
        "imageToImage",
        "textToAudio",
        "textToVideo",
        "mediaToVideo",
        "mediaToAudio",
        "mediaToImage",
        "imageToText",
        "mediaToText",
        "imageTo3D",
        "textTo3D",
        "textToText"
      ]
    },
    "ModalityCapability": {
      "type": "object",
      "required": [
        "outputType"
      ],
      "properties": {
        "outputType": {
          "$ref": "#/$defs/OutputType"
        },
        "inputSchema": {
          "type": "object",
          "description": "JSON Schema (draft 2020-12 compatible subset) for the `options`\nobject accepted on this model + modality: property types, enums,\nnumeric ranges, defaults and required flags.\n",
          "additionalProperties": true
        },
        "files": {
          "$ref": "#/$defs/FileConstraints"
        }
      }
    },
    "ModelSummary": {
      "type": "object",
      "required": [
        "name",
        "provider",
        "modalities",
        "pricing"
      ],
      "properties": {
        "name": {
          "type": "string",
          "description": "Model name — use as `model` in `POST /v1/generations`."
        },
        "displayName": {
          "type": "string"
        },
        "provider": {
          "type": "string"
        },
        "modalities": {
          "type": "array",
          "description": "Modalities this model supports through this API.",
          "items": {
            "$ref": "#/$defs/Modality"
          }
        },
        "pricing": {
          "$ref": "#/$defs/Pricing"
        },
        "maxPromptLength": {
          "type": "integer",
          "description": "Maximum prompt text length in characters (Unicode code points) the\nmodel accepts. Absent when the model publishes no prompt-length\nlimit. Informational — not enforced by this API.\n"
        },
        "voiceCatalog": {
          "type": "string",
          "description": "Identifier of the speaker-voice catalog this model draws from. List\nits voices with `GET /v1/voices/{voiceCatalog}` and pass a voice\n`id` as the `speaker` option. Absent when the model has no voice\nselection. A voice id is only valid on the catalog it came from — ids\nare not interchangeable between models.\n"
        }
      }
    },
    "OutputType": {
      "type": "string",
      "description": "The media type this generation produces. `layered_image` is one base image\nplus positioned transparent layers, consolidated into a single output.\n",
      "enum": [
        "image",
        "audio",
        "video",
        "text",
        "3d_model",
        "layered_image"
      ]
    },
    "Pricing": {
      "description": "How the model is priced. `standard` pricing always carries `coinAmount`;\n`dynamic` pricing never does — the amount is computed per request (e.g.\nfrom duration, resolution and input media).\n",
      "oneOf": [
        {
          "$ref": "#/$defs/StandardPricing"
        },
        {
          "$ref": "#/$defs/DynamicPricing"
        }
      ]
    },
    "PricingInputs": {
      "type": "object",
      "description": "The inputs that determine this model's price — collect exactly these\nbefore calling `POST /v1/models/{model}/cost`. All of them are required\nthere; defaults are never applied. Empty/absent arrays mean the price\ndoesn't depend on that kind of input (both empty = flat-rate dynamic).\n",
      "properties": {
        "options": {
          "type": "array",
          "description": "`options` keys that affect the price.",
          "items": {
            "type": "string"
          }
        },
        "media": {
          "type": "array",
          "description": "Input-file roles whose metadata affects the price.",
          "items": {
            "$ref": "#/$defs/PricingMediaInput"
          }
        },
        "mediaAnyOf": {
          "type": "array",
          "description": "Roles, each listed as `optional` in `media`, of which at least one\nmust be supplied: the price reads whichever one is present. Absent\nwhen every priced role stands on its own.\n",
          "items": {
            "type": "string"
          }
        }
      }
    },
    "PricingMediaInput": {
      "type": "object",
      "required": [
        "role",
        "requiredMetadata"
      ],
      "description": "Every file with this `role` must carry the listed metadata fields in the\ncost request's `inputMedia` (and as asserted metadata on the generation's\n`files`). Allowed file counts per role are in the role's catalog\nconstraints, not here.\n",
      "properties": {
        "role": {
          "type": "string"
        },
        "requiredMetadata": {
          "type": "array",
          "items": {
            "type": "string",
            "enum": [
              "durationSeconds",
              "width",
              "height",
              "fps"
            ]
          }
        },
        "optional": {
          "type": "boolean",
          "description": "When true, the role itself is optional for pricing — a quote without\nany file of this role is valid (e.g. seedance_v2's referenceVideo).\nFiles that ARE supplied must still carry the `requiredMetadata`.\nAbsent/false means at least one file of this role is required to\nprice the model.\n"
        }
      }
    },
    "RoleConstraint": {
      "type": "object",
      "required": [
        "max"
      ],
      "properties": {
        "min": {
          "type": "integer",
          "description": "Minimum number of files for this role — a value above 0 makes the\nrole mandatory. Omitted when 0 (optional role).\n"
        },
        "max": {
          "type": "integer",
          "description": "Maximum number of files for this role."
        },
        "acceptedFormats": {
          "type": "array",
          "items": {
            "type": "string"
          }
        },
        "maxFileSizeMb": {
          "type": "integer",
          "format": "int64"
        },
        "minDurationSeconds": {
          "type": "integer"
        },
        "maxDurationSeconds": {
          "type": "integer"
        },
        "minFps": {
          "type": "integer",
          "description": "Minimum frame rate for video roles. Checked against the probed frame\nrate; skipped when the frame rate cannot be determined.\n"
        },
        "maxFps": {
          "type": "integer",
          "description": "Maximum frame rate for video roles."
        },
        "dimensions": {
          "$ref": "#/$defs/DimensionLimits"
        },
        "aliasRequired": {
          "type": "boolean",
          "description": "When true, each file in this role must carry an `alias` cited in the\nprompt as `{{alias}}` (reference roles).\n"
        },
        "providerMetadata": {
          "type": "object",
          "description": "JSON Schema (draft 2020-12 compatible subset) for the\n`providerMetadata` object a file in this role accepts: property types,\nenums, ranges, length limits, defaults and required flags. Absent when\nthe role declares none.\n",
          "additionalProperties": true
        }
      }
    },
    "StandardPricing": {
      "type": "object",
      "required": [
        "type",
        "coinAmount"
      ],
      "properties": {
        "type": {
          "type": "string",
          "enum": [
            "standard"
          ]
        },
        "coinAmount": {
          "type": "integer",
          "format": "int64",
          "description": "Coins charged per generation."
        }
      }
    }
  }
}