Asset Tap uses a data-driven provider system where AI providers are defined entirely through YAML configuration files. No code changes are needed to add, remove, or modify providers.

Included Providers

Asset Tap ships with pre-configured support for three providers. You only need an API key for one of them to run the full pipeline.

Asset Tap is not tied to any one service: the provider layer is a compatibility surface, support for more providers is coming, and anyone can add their own with a YAML file.

Model tables follow one ordering everywhere: model families ascend (older/lower tier first, newest last) and appear in the same order under every provider that carries them; specialized and provider-exclusive models follow their family; each provider's default is marked inline. The tables compare like-for-like at a glance.

  • fal.ai -- Pay-per-generation pricing, broadest model selection.
  • Meshy AI -- Subscription-based, credit pool; specialized in 3D.
  • Tripo3D -- Subscription-based, credit pool; specialized in 3D, including low-poly.

fal.ai

Text-to-Image Models

ModelDescription
Nano BananaGoogle Imagen 3-based image generation -- fast and affordable
Nano Banana 2Gemini 3.1 Flash Image -- reasoning-guided generation (default)
Nano Banana ProPremium Google Imagen 3 -- higher quality with aspect ratio control
FLUX.2 DevOpen-source FLUX.2 with tunable guidance and steps
FLUX.2 ProPremium FLUX.2 -- best quality, zero-config

Image-to-3D Models

ModelDescription
TRELLIS 2Native 3D generative model -- fast and versatile (default)
Hunyuan3D ProTencent Hunyuan3D v3.1 Pro -- high quality 3D generation
Meshy v6Meshy 6 proxied through fal -- pay-per-call billing
Meshy v7Meshy 7 proxied through fal -- pay-per-call billing

Tip: Your fal.ai Dashboard shows all generation requests, results, and costs. This is the source of truth for your usage and a handy way to recover past outputs.

Meshy AI

Native Meshy API -- bypasses fal's proxy markup and unlocks the full Meshy feature set. Requires MESHY_API_KEY from the Meshy API settings page.

Text-to-Image Models

Model--image-modelDescription
Nano Bananameshy/nano-bananaStandard tier (default)
Nano Banana 2meshy/nano-banana-2Mid tier
Nano Banana Promeshy/nano-banana-proHigher quality
GPT Image 2meshy/gpt-image-2The only Meshy model offering 3:2 and 2:3 (superset of the Nano Banana ratios)

Tunable parameters: aspect_ratio, generate_multi_view, pose_mode, remove_background (transparent RGBA PNG output).

Aspect ratios differ per model. The Nano Banana family accepts 1:1, 16:9, 9:16, 4:3, 3:4; GPT Image 2 accepts those plus 3:2 and 2:3. generate_multi_view cannot be combined with aspect_ratio; clear it with --param aspect_ratio= (or the (unset) entry in the GUI dropdown) when enabling multi-view.

Image-to-3D Models

Model--3d-modelDescription
Meshy 6 Litemeshy/v6-lite/image-to-3dBudget tier -- successor to the retired Meshy 5
Meshy v6meshy/v6/image-to-3dMeshy 6 -- production-ready 3D with PBR textures
Meshy v7meshy/v7/image-to-3dMeshy 7.1 -- newest generation, high-resolution geometry (default)
Smart Topologymeshy/t2/image-to-3dMeshy T2 -- clean topology, separated parts, game-ready face counts (max 15k)

Tunable parameters (6 Lite/v6/v7): topology (triangle/quad), target_polycount, enable_pbr, should_remesh, should_texture, pose_mode, texture_prompt. Smart Topology sets its face count directly with target_polycount (100-15,000) -- topology and should_remesh don't apply.

Version-specific knobs, per Meshy's own docs: v6 and v7 add texture_resolution (2k/4k/8k -- "the 4k and 8k options are unavailable with meshy-6-lite") and image_enhancement ("only available with meshy-6, meshy-7.1, or latest"); remove_lighting is v6-only; geometry_resolution (standard/2k/4k, "requires meshy-7.1 or latest") is v7-only. geometry_resolution replaces the deprecated ultra_mode, which Meshy documents as equivalent to geometry_resolution: 2k. symmetry_mode is deprecated API-wide and no longer affects output, so it is no longer exposed on any model.

Why two ways to reach Meshy? The fal.ai "Meshy v6" entry uses fal's pay-per-call billing and requires a FAL_KEY. The Meshy provider's entry uses Meshy's subscription credits and requires a MESHY_API_KEY. Pick whichever fits your billing relationship -- or keep both keys configured and switch per generation.

Tripo3D

Native Tripo v3 API. Requires TRIPO_API_KEY from the Tripo console (API Keys page).

Text-to-Image Models

Model--image-modelDescription
Seedream v4tripo/seedream-v4Balanced quality (default)
Seedream v5tripo/seedream-v5Stronger prompt following
Nano Banana Protripo/nano-banana-proHigher quality; tunable aspect_ratio
GPT Image 2tripo/gpt-image-2Tunable quality (low/medium/high)

Tripo runs one image task at a time per account by default.

Image-to-3D Models

Model--3d-modelDescription
Tripo v3.1tripo/v3.1/image-to-modelLatest H-series model, highest detail (default)
Tripo P1tripo/p1/image-to-modelLow-poly with clean topology, 50-20,000 faces

Tunable parameters (both): texture, pbr, texture_quality (standard/detailed/extreme), texture_version, delight (needs texture_version=v3.5-20260815), texture_alignment, orientation, face_limit, enable_image_autofix, auto_size, export_uv, model_seed, texture_seed. v3.1 adds geometry_quality (detailed is Ultra) and smart_low_poly. PBR forces texture on, so turning texture off needs pbr=false too.

Not exposed: quad (Tripo switches the output to FBX), compress (meshopt geometry), generate_parts, and export_orientation.

Canceling a Tripo run. Tripo's API has no cancel endpoint. Canceling in Asset Tap stops waiting for the result, but the task keeps running on Tripo's side and is billed if it succeeds.

Pricing Models

ProviderBillingHow it works
fal.aiPay-per-callCharged per generation at the model's listed cost; no monthly minimum.
Meshy AISubscriptionMonthly plan grants a credit pool; each generation deducts credits from it.
Tripo3DSubscriptionMonthly plan grants a credit pool; credits are refunded for failed tasks.

Per-generation costs are set by the providers and change without notice; check their pricing pages against your own key.


Adding Custom Providers

You can add support for any AI provider by creating a YAML configuration file. No code changes required.

Quick Start

Create a YAML file with your provider's API details:

provider:
  id: 'my-provider'
  name: 'My Provider'
  description: 'Custom AI provider'
  env_vars: ['MY_API_KEY']
  base_url: 'https://api.example.com'
  api_key_url: 'https://example.com/keys'

text_to_image:
  - id: 'my-model'
    name: 'My Model'
    description: 'Fast image generation'
    endpoint: '/generate'
    method: POST
    request:
      headers:
        Authorization: 'Bearer ${MY_API_KEY}'
        Content-Type: 'application/json'
      body:
        prompt: '${prompt}'
    response:
      response_type: json
      field: 'image_url'

Where to Put Your Config

For personal use (no rebuild needed):

Place the YAML file in your user config directory:

  • macOS: ~/Library/Application Support/asset-tap/providers/my-provider.yaml
  • Linux: ~/.config/asset-tap/providers/my-provider.yaml
  • Windows: %APPDATA%/asset-tap/providers/my-provider.yaml

Restart the application and your provider will appear automatically.

To embed in the binary (requires rebuild):

  1. Add the file to providers/my-provider.yaml in the source tree
  2. Run make build -- the include_dir! macro discovers all *.yaml files automatically

Authentication

List required environment variables in env_vars. The provider won't appear as available until all are set.

provider:
  env_vars: ['MY_API_KEY', 'MY_SECRET']

Use ${ENV_VAR} syntax in request templates:

request:
  headers:
    Authorization: 'Bearer ${MY_API_KEY}'

In the GUI, set API keys in Settings. For the CLI, use environment variables or a .env file.

Response Types

JSON -- Extract a URL from a JSON response:

response:
  response_type: json
  field: 'data.images[0].url' # JSONPath expression

Polling -- For async APIs that queue jobs:

response:
  response_type: polling
  polling:
    status_field: 'status_url' # Field in submit response containing the status check URL
    status_check_field: 'status' # Field in status response to check
    success_value: 'COMPLETED'
    failure_value: 'FAILED'
    response_url_field: 'response_url' # Field containing URL to fetch final result
    response_envelope_field: 'response' # Field in result that wraps the actual output
    result_field: 'images[0].url' # JSONPath to extract from the output
    interval_ms: 1000
    max_attempts: 120

    # Optional: build the poll URL from a task id instead of reading a full URL
    # from the initial response. Used when the API returns only {"result": "<id>"}.
    status_url_template: '/v1/jobs/${result}'

    # Optional: override the cancel HTTP method. Defaults to PUT.
    # Meshy uses DELETE for its cancel endpoint.
    cancel_method: DELETE
    cancel_url_template: '${status_url}'

    # Optional: the provider's own status vocabulary. Defaults are fal's
    # IN_QUEUE / IN_PROGRESS. Tripo uses queued / running, reports a 0-100
    # percentage, nests its failure message, and has no cancel endpoint.
    queued_value: 'queued'
    running_value: 'running'
    progress_field: 'data.progress'
    error_field: 'data.error_message'
    cancelable: false

status_url_template supports nested paths (${data.id}) and array indices (${items[0]}). Relative paths are resolved against base_url.

Binary / Base64 -- For direct file responses:

response:
  response_type: binary

# or base64-encoded in JSON:
response:
  response_type: base64
  field: 'artifacts[0].base64'

Image-to-3D Models

Image-to-3D models use ${image_url} instead of ${prompt}. Asset Tap automatically uploads the image and substitutes the public URL:

image_to_3d:
  - id: 'my-3d-model'
    name: 'My 3D Model'
    endpoint: '/3d/generate'
    method: POST
    request:
      headers:
        Authorization: 'Key ${MY_API_KEY}'
        Content-Type: 'application/json'
      body:
        image_url: '${image_url}'
    response:
      response_type: polling
      polling:
        status_field: 'id'
        status_check_field: 'status'
        success_value: 'succeeded'
        result_field: 'model_glb.url'
        interval_ms: 2000
        max_attempts: 300

Upload Configuration

Required when models use ${image_url} and the provider exposes an upload endpoint. The upload section is nested under provider::

provider:
  id: 'my-provider'
  # ... other provider fields ...
  upload:
    endpoint: '/storage/upload/initiate'
    method: POST
    request:
      type: initiate_then_put # or "multipart"
      headers:
        Authorization: 'Key ${MY_API_KEY}'
        Content-Type: 'application/json'
      initiate_body:
        file_name: 'image.png'
        content_type: 'image/png'
    response:
      upload_url_field: 'upload_url'
      file_url_field: 'file_url'

Data-URI Fallback (No Upload Endpoint)

If a provider doesn't offer an upload endpoint but accepts inline data:image/png;base64,... URIs directly (like Meshy), simply omit the upload: block from your YAML. Asset Tap automatically inlines the image as a base64 data URI wherever ${image_url} appears in the request body.

A 10 MB cap on the raw image bytes is enforced in this mode to prevent request-size failures on providers with body limits. For typical Asset Tap workflows (where the intermediate image is 1-4 MB), this is well within limits.

Testing Your Provider

# 1. Verify the provider loads and config is valid
asset-tap --list-providers

# 2. Test with the real API (validates response parsing)
asset-tap -p my-provider -y "a red cube"

Working from a source build? The repository's developer docs cover mock mode, which exercises a provider config without spending credits.


Schema Reference

Complete reference for all provider YAML fields.

Top-Level Structure

provider: # Required: Provider metadata
  id: string
  name: string
  description: string
  env_vars: [string]
  base_url: string # Optional
  api_key_url: string # Optional
  website_url: string # Optional
  docs_url: string # Optional
  upload: # Optional: File upload configuration (nested under provider)

text_to_image: # Optional: Text-to-image model list
  - id: string

image_to_3d: # Optional: Image-to-3D model list
  - id: string

Model Fields

text_to_image: # or image_to_3d
  - id: string # Unique model ID within provider
    name: string # Display name
    description: string # Model description
    endpoint: string # API endpoint (relative to base_url or absolute)
    method: string # HTTP method (default: POST)
    request:
      headers: {} # HTTP headers with ${VAR} interpolation
      body: {} # JSON body with ${prompt} or ${image_url}
    response:
      response_type: string # Json, Binary, Base64, or polling
      field: string # JSONPath for Json/Base64
      polling: # Required for polling type
        status_field: string
        status_url_template: string # Optional: build poll URL from initial response
        status_check_field: string
        success_value: string
        failure_value: string | string[] # one terminal failure status, or several
        result_field: string
        interval_ms: integer
        max_attempts: integer
        cancel_method: string # Optional: HTTP method for cancel (default PUT)
        cancel_url_template: string # Optional: template using ${status_url}
        cancelable: boolean # Optional: false if there is no cancel endpoint (default true)
        queued_value: string | string[] # Optional: queued status(es) (default IN_QUEUE)
        running_value: string | string[] # Optional: running status(es) (default IN_PROGRESS)
        progress_field: string # Optional: 0-100 percentage
        error_field: string # Optional: failure message (default top-level `error`)
    parameters: [] # Optional: user-tunable fields (see below)

Tunable Parameters

A model can declare parameters that users adjust per generation. They appear in the GUI as sliders, checkboxes, and dropdowns, and on the CLI via --param KEY=VALUE.

parameters:
  - name: 'guidance_scale' # Must match a key in request.body
    label: 'Guidance Scale' # GUI label
    description: 'Higher = stricter prompt adherence'
    type: float # float, integer, boolean, string, select
    default: 3.5
    min: 1.0
    max: 20.0
    step: 0.5
  - name: 'topology'
    label: 'Topology'
    type: select
    default: 'triangle'
    options: ['triangle', 'quad'] # Required for select; strings or numbers
  - name: 'seed'
    label: 'Seed'
    type: integer
    widget: input # Typed field instead of a slider
    default: null # Omitted from the request unless the user sets it
FieldRequiredApplies toPurpose
nameyesallMust match a request-body key
labelyesallGUI display name
descriptionnoallTooltip text
typeyesallfloat, integer, boolean, string, select
defaultyesallUsed when no override exists
min / max / stepnofloat, integerSlider bounds and increment
optionsyes for selectselectAllowed values (strings or numbers)
widgetnofloat, integer, stringslider (default) or input
allow_unsetnoselectAdds an (unset) entry that clears to null
requiresnoallSibling values this parameter needs to apply
conflicts_withnoallSibling values that make this parameter invalid

Null means "unset". A null default, a cleared input widget, or --param name= on the CLI all drop the key from the request so the provider applies its own default. A literal null is never sent.

allow_unset to ask for the provider's default. A dropdown can only write one of its options, so set allow_unset: true when a parameter must sometimes be absent.

Conditional parameters. Some knobs only apply in combination with another — Meshy ignores origin_at unless auto_size is on, and rejects aspect_ratio alongside generate_multi_view. Say so and the app enforces it everywhere:

- name: 'origin_at'
  type: select
  requires: { auto_size: true }
  default: 'bottom'
  options: ['bottom', 'center']
- name: 'aspect_ratio'
  type: select
  conflicts_with: { generate_multi_view: true }
  default: '1:1'
  options: ['1:1', '16:9']

requires needs every listed sibling to hold its value; conflicts_with fires when any does. A listed null means "set to anything". Declaring a conflict on one side covers both. When the condition isn't met, a value you set is refused up front (--param exits 2, the GUI greys the control out) and a value left at its default is simply dropped from the request.

Variable Interpolation

  • ${prompt} -- User's text prompt
  • ${image_url} -- Publicly accessible URL for the generated image. Produced by the provider's upload endpoint if configured, otherwise inlined as a data:image/png;base64,... URI.
  • ${ENV_VAR} -- Any environment variable listed in env_vars

Complete Example

provider:
  id: 'fal.ai'
  name: 'fal.ai'
  description: 'Fast, serverless AI model API'
  env_vars: ['FAL_KEY']
  base_url: 'https://queue.fal.run'
  api_key_url: 'https://fal.ai/dashboard/keys'
  upload:
    endpoint: 'https://rest.alpha.fal.ai/storage/upload/initiate?storage_type=fal-cdn-v3'
    method: POST
    request:
      type: initiate_then_put
      headers:
        Authorization: 'Key ${FAL_KEY}'
        Content-Type: 'application/json'
      initiate_body:
        file_name: 'image.png'
        content_type: 'image/png'
    response:
      upload_url_field: 'upload_url'
      file_url_field: 'file_url'

text_to_image:
  - id: 'fal-ai/nano-banana-2'
    name: 'Nano Banana 2'
    description: 'Gemini 3.1 Flash Image -- reasoning-guided generation'
    endpoint: '/fal-ai/nano-banana-2'
    method: POST
    request:
      headers:
        Authorization: 'Key ${FAL_KEY}'
        Content-Type: 'application/json'
      body:
        prompt: '${prompt}'
        resolution: '1K'
        num_images: 1
    response:
      response_type: polling
      polling:
        status_field: 'status_url'
        status_check_field: 'status'
        success_value: 'COMPLETED'
        failure_value: 'FAILED'
        response_url_field: 'response_url'
        response_envelope_field: 'response'
        result_field: 'images[0].url'
        interval_ms: 1000
        max_attempts: 120

image_to_3d:
  - id: 'fal-ai/trellis-2'
    name: 'Trellis 2'
    description: 'High quality 3D model generation'
    endpoint: '/fal-ai/trellis-2'
    method: POST
    request:
      headers:
        Authorization: 'Key ${FAL_KEY}'
        Content-Type: 'application/json'
      body:
        image_url: '${image_url}'
    response:
      response_type: polling
      polling:
        status_field: 'status_url'
        status_check_field: 'status'
        success_value: 'COMPLETED'
        failure_value: 'FAILED'
        response_url_field: 'response_url'
        response_envelope_field: 'response'
        result_field: 'model_glb.url'
        interval_ms: 2000
        max_attempts: 300

Best Practices

  • Always use HTTPS for all URLs
  • Never hardcode API keys -- use ${ENV_VAR} syntax
  • Verify JSONPath expressions against actual API responses
  • Use reasonable polling intervals to respect rate limits
  • Set adequate max_attempts based on typical operation time
  • Verify the config loads (--list-providers) before testing against the real API to validate response parsing