diff --git a/Foundational/fal/SKILL.md b/Foundational/fal/SKILL.md index b2bc869..1aabcbb 100644 --- a/Foundational/fal/SKILL.md +++ b/Foundational/fal/SKILL.md @@ -1,5 +1,5 @@ --- -name: Fal +name: fal description: > Foundational skill for the fal.ai Model API — 600+ generative media models (image, video, audio, LLMs) accessible via HTTP. Use this skill when: (1) generating images from text or other images (FLUX, SDXL, etc.), (2) generating video from text or images, (3) speech-to-text or text-to-speech, (4) running LLMs via fal, (5) any task involving fal.ai model endpoints, (6) discovering available fal models or checking pricing, (7) chaining models via fal workflows, (8) understanding fal API patterns (sync vs queue, file handling, error handling). This is the base fal skill — specialized skills may reference it for specific model categories or workflows. metadata: {"openclaw": {"emoji": "⚡", "requires": {"env": ["FAL_API_KEY"]}, "primaryEnv": "FAL_API_KEY"}} @@ -7,130 +7,142 @@ metadata: {"openclaw": {"emoji": "⚡", "requires": {"env": ["FAL_API_KEY"]}, "p # fal Model API -Access 600+ generative media models via simple HTTP requests. Two execution modes: synchronous (fast) and queue (reliable). +Access 600+ generative media models via `scripts/fal.sh`. Two execution modes: synchronous (fast) and queue (reliable). ## Authentication -All requests use the same header: +Set `FAL_API_KEY` environment variable. The wrapper script handles the auth header automatically. -``` -Authorization: Key $FAL_API_KEY -``` - -## Model Discovery - -fal has 600+ models that change frequently. Always use the live Platform API to find models and get their exact input/output schemas rather than guessing or relying on memorized model IDs. +## Wrapper Script -### Find Models +All operations go through `scripts/fal.sh`. Run `bash scripts/fal.sh --help` for full usage. -```bash -# Search by keyword (text-to-video, image generation, speech-to-text, etc.) -curl "https://api.fal.ai.cloudproxy.vibecodeapp.com/v1/models?query=text-to-video&limit=10" \ - -H "Authorization: Key $FAL_API_KEY" +### Commands -# List all available models (paginated) -curl "https://api.fal.ai.cloudproxy.vibecodeapp.com/v1/models?limit=50" \ - -H "Authorization: Key $FAL_API_KEY" +| Command | Purpose | +|---------|---------| +| `run` | Synchronous model execution (fast, simple) | +| `queue` | Queue-based async execution (reliable, recommended) | +| `status` | Poll queue request status | +| `result` | Get queue request result | +| `cancel` | Cancel a queued request | +| `search` | Search/list available models | +| `schema` | Get a model's OpenAPI input/output schema | +| `pricing` | Get model pricing | +| `estimate` | Estimate cost for model usage | -# Paginate with cursor -curl "https://api.fal.ai.cloudproxy.vibecodeapp.com/v1/models?limit=50&cursor=CURSOR_FROM_PREVIOUS" \ - -H "Authorization: Key $FAL_API_KEY" -``` - -### Get a Model's Input/Output Schema +## Model Discovery -Before calling any model, fetch its OpenAPI schema to know exactly what parameters it accepts: +fal has 600+ models that change frequently. Use the script to discover models and get their schemas rather than guessing model IDs. ```bash -curl "https://api.fal.ai.cloudproxy.vibecodeapp.com/v1/models?endpoint_id=fal-ai/flux/dev&expand=openapi-3.0" \ - -H "Authorization: Key $FAL_API_KEY" -``` +# Search by capability +bash scripts/fal.sh search --query "text-to-video" --limit 10 -This returns the full OpenAPI 3.0 spec — input parameters, types, defaults, required fields, and output schema. This is the authoritative source for any model's interface. +# List all models (paginated) +bash scripts/fal.sh search --limit 50 -### Check Pricing +# Paginate with cursor +bash scripts/fal.sh search --limit 50 --cursor CURSOR_FROM_PREVIOUS -```bash -curl "https://api.fal.ai.cloudproxy.vibecodeapp.com/v1/models/pricing?endpoint_id=fal-ai/flux/dev" \ - -H "Authorization: Key $FAL_API_KEY" -``` +# Get a model's exact input/output schema (authoritative source for parameters) +bash scripts/fal.sh schema --endpoint-id fal-ai/flux/dev -### When to Use Discovery +# Check pricing +bash scripts/fal.sh pricing --endpoint-id fal-ai/flux/dev +``` -- **No specific model requested** — search by capability (`query=text-to-image`) -- **Unknown input format** — fetch the OpenAPI schema before calling -- **Choosing between models** — compare options by searching a category -- **Specific model requested** — can skip search, but still fetch schema if unsure of params +Always fetch the schema before calling an unfamiliar model — it tells you exactly what parameters it accepts. -For a quick reference of commonly used model IDs, see [references/models.md](references/models.md). That list is a convenience starting point — the live API above is always the source of truth. +For a quick reference of commonly used model IDs, see [references/models.md](references/models.md). That list is a convenience starting point — live search is always the source of truth. ## Two Execution Modes ### Sync — Fast, Simple -`POST https://fal.run.cloudproxy.vibecodeapp.com/{model_id}` — send request, get result directly. Best for fast models (<10s). +Best for fast models (<10s). Send request, get result directly. ```bash -curl -X POST "https://fal.run.cloudproxy.vibecodeapp.com/fal-ai/flux/dev" \ - -H "Authorization: Key $FAL_API_KEY" \ - -H "Content-Type: application/json" \ - -d '{"prompt": "a cat in a spacesuit", "image_size": "landscape_4_3"}' -``` +# Quick image generation +bash scripts/fal.sh run --model fal-ai/flux/dev --prompt "a cat in a spacesuit" -Returns the model output directly (images, video URLs, text, etc.). +# With full JSON control +bash scripts/fal.sh run --model fal-ai/flux/dev \ + --data '{"prompt":"a cat","image_size":"landscape_4_3","num_images":2}' -### Queue — Reliable, Recommended +# With image input +bash scripts/fal.sh run --model fal-ai/flux/dev/image-to-image \ + --image_url "https://example.com/photo.jpg" --prompt "make it watercolor" +``` -`POST https://queue.fal.run.cloudproxy.vibecodeapp.com/{model_id}` — submit, poll, retrieve. Built-in retries, cancellation, status tracking. Use for anything slow (video gen, batch ops) or when reliability matters. +### Queue — Reliable, Recommended -**Step 1: Submit** -```bash -curl -X POST "https://queue.fal.run.cloudproxy.vibecodeapp.com/fal-ai/flux/dev" \ - -H "Authorization: Key $FAL_API_KEY" \ - -H "Content-Type: application/json" \ - -d '{"prompt": "a cat in a spacesuit"}' -``` -Returns `request_id`, `status_url`, `response_url`, `cancel_url`. +Best for slow operations (video gen, batch) or when reliability matters. Submit → poll → retrieve. -**Step 2: Poll status** -```bash -curl "https://queue.fal.run.cloudproxy.vibecodeapp.com/fal-ai/flux/dev/requests/{request_id}/status?logs=1" -``` -Status progression: `IN_QUEUE` (202) → `IN_PROGRESS` (202) → `COMPLETED` (200) +**Important:** The queue submit response returns `status_url`, `response_url`, and `cancel_url`. Use these with `--status-url`, `--response-url`, `--cancel-url` — they contain the correct normalized model path. Alternatively, pass `--model` and the script auto-normalizes sub-paths. -**Step 3: Get result** ```bash -curl "https://queue.fal.run.cloudproxy.vibecodeapp.com/fal-ai/flux/dev/requests/{request_id}" +# Step 1: Submit to queue +bash scripts/fal.sh queue --model fal-ai/wan/v2.2-a14b/text-to-video \ + --prompt "a timelapse of a flower blooming" +# Returns: request_id, status_url, response_url, cancel_url + +# Step 2: Poll status — use --status-url from queue response (preferred) +bash scripts/fal.sh status --status-url "STATUS_URL_FROM_RESPONSE" --logs +# OR use --model + --request-id (auto-normalizes sub-paths) +bash scripts/fal.sh status --model fal-ai/wan/v2.2-a14b/text-to-video \ + --request-id REQUEST_ID --logs +# Status: IN_QUEUE → IN_PROGRESS → COMPLETED + +# Step 3: Get result — use --response-url from queue response (preferred) +bash scripts/fal.sh result --response-url "RESPONSE_URL_FROM_RESPONSE" +# OR use --model + --request-id +bash scripts/fal.sh result --model fal-ai/wan/v2.2-a14b/text-to-video \ + --request-id REQUEST_ID + +# Cancel — use --cancel-url from queue response (preferred) +bash scripts/fal.sh cancel --cancel-url "CANCEL_URL_FROM_RESPONSE" +# OR use --model + --request-id +bash scripts/fal.sh cancel --model fal-ai/wan/v2.2-a14b/text-to-video \ + --request-id REQUEST_ID ``` -**Cancel** (while `IN_QUEUE`): -```bash -curl -X PUT "https://queue.fal.run.cloudproxy.vibecodeapp.com/fal-ai/flux/dev/requests/{request_id}/cancel" -``` +### Queue with webhook (no polling needed) -**SSE streaming** (real-time status updates): ```bash -curl "https://queue.fal.run.cloudproxy.vibecodeapp.com/fal-ai/flux/dev/requests/{request_id}/status/stream?logs=1" +bash scripts/fal.sh queue --model fal-ai/flux/dev \ + --prompt "a sunset" --webhook "https://your.app/webhook" ``` ## Which Mode to Use | Scenario | Mode | Why | |----------|------|-----| -| Fast image gen (FLUX schnell, SDXL) | Sync | Sub-5s, lowest latency | +| Fast image gen (FLUX schnell, SDXL) | `run` | Sub-5s, lowest latency | | High-quality image gen (FLUX dev/pro) | Either | ~5-15s, sync is simpler | -| Video generation | Queue | 30s–5min, need reliability | -| Batch operations | Queue | Track multiple requests | -| Production/critical | Queue | Auto-retries, cancellation | +| Video generation | `queue` | 30s–5min, need reliability | +| Batch operations | `queue` | Track multiple requests | +| Production/critical | `queue` | Auto-retries, cancellation | -## File Handling +## Special Headers -**Input:** Pass file URLs in the JSON body. Any public URL works. Data URIs work for small files. +Pass via script options: -```json -{"image_url": "https://example.com/photo.jpg", "prompt": "remove background"} +```bash +# Fail-fast if processing doesn't start in 30s +bash scripts/fal.sh run --model fal-ai/flux/dev --prompt "test" --request-timeout 30 + +# Disable auto-retries +bash scripts/fal.sh queue --model fal-ai/flux/dev --prompt "test" --no-retry + +# Custom output file expiration (seconds) +bash scripts/fal.sh run --model fal-ai/flux/dev --prompt "test" --expire 3600 ``` +## File Handling + +**Input:** Pass file URLs in the JSON body via `--data` or `--image_url`. Any public URL works. Data URIs work for small files. + **Output:** Models return fal CDN URLs (`https://v3.fal.media/files/...`). Guaranteed available 7 days. Download anything you need to keep longer. ## Common Response Patterns @@ -151,6 +163,16 @@ curl "https://queue.fal.run.cloudproxy.vibecodeapp.com/fal-ai/flux/dev/requests/ } ``` +## Cost Estimation + +```bash +# Get unit price +bash scripts/fal.sh pricing --endpoint-id fal-ai/flux/dev + +# Estimate cost for multiple units +bash scripts/fal.sh estimate --endpoint-id fal-ai/flux/dev --unit-quantity 10 +``` + ## Error Handling Errors return a `detail` array with `type` (machine-readable), `msg` (human-readable), `loc` (field path). @@ -161,19 +183,11 @@ Key error types: - `no_media_generated` (422) — model produced nothing, not retryable - `image_too_small` / `image_too_large` (422) — dimension issues, check `ctx` for limits -Check `X-Fal-Retryable` response header. Queue mode auto-retries server errors (503, 504, 429) up to 10 times. 5xx errors are not billed. - -## Useful Headers - -| Header | Purpose | -|--------|---------| -| `X-Fal-Request-Timeout: 30` | Fail-fast if processing doesn't start in 30s | -| `X-Fal-No-Retry: 1` | Disable auto-retries (queue mode) | -| `X-Fal-Object-Lifecycle-Preference: {"expiration_duration_seconds": 3600}` | Custom output file expiration | +Queue mode auto-retries server errors (503, 504, 429) up to 10 times. 5xx errors are not billed. ## References - **Full API reference** (all endpoints, headers, webhooks, workflows): read [references/api-reference.md](references/api-reference.md) - **Common model IDs** (convenience starting point, not exhaustive): read [references/models.md](references/models.md) - **All models (live)**: https://fal.ai/models -- **Docs**: https://docs.fal.ai/model-apis \ No newline at end of file +- **Docs**: https://docs.fal.ai/model-apis diff --git a/Foundational/fal/scripts/fal.sh b/Foundational/fal/scripts/fal.sh new file mode 100755 index 0000000..a8226a9 --- /dev/null +++ b/Foundational/fal/scripts/fal.sh @@ -0,0 +1,355 @@ +#!/usr/bin/env bash +# fal.sh — Wrapper for fal.ai Model API (600+ generative models) +# Usage: bash scripts/fal.sh [options] +# +# Commands: +# run Synchronous model execution (fast, simple) +# queue Queue-based async execution (reliable, recommended) +# status Poll queue request status +# result Get queue request result +# cancel Cancel a queued request +# search Search/list available models +# schema Get a model's OpenAPI input/output schema +# pricing Get model pricing +# estimate Estimate cost for model usage +# +# Global options: +# --key KEY Override FAL_API_KEY env var +# --raw Output raw JSON (skip jq formatting) +# --timeout SECS Curl timeout (default: 120) +# --help Show this help +# +# Model execution options (run, queue): +# --model MODEL_ID Model endpoint ID (required). e.g. fal-ai/flux/dev +# --data JSON Full JSON body as string +# --prompt TEXT Shorthand: sets {"prompt": "TEXT"} (merged with --data) +# --image_url URL Shorthand: sets {"image_url": "URL"} (merged with --data) +# +# Special headers (run, queue): +# --request-timeout S X-Fal-Request-Timeout header (fail-fast seconds) +# --no-retry X-Fal-No-Retry: 1 (disable queue auto-retries) +# --expire SECS X-Fal-Object-Lifecycle-Preference expiration_duration_seconds +# --webhook URL Webhook URL for queue completion notification +# +# Queue management options: +# --model MODEL_ID Model endpoint ID (required for status/result/cancel) +# --request-id ID Request ID from queue submit response +# --status-url URL Use exact status_url from queue response (preferred) +# --response-url URL Use exact response_url from queue response (preferred) +# --cancel-url URL Use exact cancel_url from queue response (preferred) +# --logs Include logs in status polling (adds ?logs=1) +# +# NOTE: For models with sub-paths (e.g. fal-ai/wan/v2.2-a14b/text-to-video), +# fal normalizes queue URLs to the base model (fal-ai/wan). The script handles +# this automatically, but passing --status-url/--response-url from the queue +# response is the most reliable approach. +# +# Platform API options (search, schema, pricing, estimate): +# --query TEXT Search query for model search (e.g. "text-to-video") +# --endpoint-id ID Filter by exact endpoint ID (e.g. fal-ai/flux/dev) +# --limit N Results per page (default: 20) +# --cursor TOKEN Pagination cursor from previous response +# --unit-quantity N Units for cost estimate (default: 1) +# +# Examples: +# # Quick image generation (sync) +# fal.sh run --model fal-ai/flux/dev --prompt "a cat in a spacesuit" +# +# # Image generation with full control +# fal.sh run --model fal-ai/flux/dev --data '{"prompt":"a cat","image_size":"landscape_4_3","num_images":2}' +# +# # Queue a slow video generation job +# fal.sh queue --model fal-ai/wan/v2.2-a14b/text-to-video --prompt "a timelapse of a flower blooming" +# +# # Poll queue status (with logs) +# fal.sh status --model fal-ai/wan/v2.2-a14b/text-to-video --request-id abc-123 --logs +# +# # Get queue result +# fal.sh result --model fal-ai/wan/v2.2-a14b/text-to-video --request-id abc-123 +# +# # Cancel a queued request +# fal.sh cancel --model fal-ai/wan/v2.2-a14b/text-to-video --request-id abc-123 +# +# # Search for models +# fal.sh search --query "text-to-video" --limit 10 +# +# # Get model schema (input/output specification) +# fal.sh schema --endpoint-id fal-ai/flux/dev +# +# # Get pricing for a model +# fal.sh pricing --endpoint-id fal-ai/flux/dev +# +# # Estimate cost +# fal.sh estimate --endpoint-id fal-ai/flux/dev --unit-quantity 10 + +set -euo pipefail + +API_KEY="${FAL_API_KEY:-}" +SYNC_BASE="https://fal.run.cloudproxy.vibecodeapp.com" +QUEUE_BASE="https://queue.fal.run.cloudproxy.vibecodeapp.com" +PLATFORM_BASE="https://api.fal.ai.cloudproxy.vibecodeapp.com/v1" +USE_JQ=true +TIMEOUT=120 + +die() { echo "ERROR: $*" >&2; exit 1; } + +show_help() { + sed -n '2,/^$/p' "$0" | sed 's/^# \?//' + exit 0 +} + +# ---------- Argument parsing ---------- +COMMAND="" +MODEL="" +DATA="" +PROMPT="" +IMAGE_URL="" +REQUEST_ID="" +STATUS_URL="" +RESPONSE_URL="" +CANCEL_URL="" +REQUEST_TIMEOUT="" +NO_RETRY="" +EXPIRE="" +WEBHOOK="" +QUERY="" +ENDPOINT_ID="" +LIMIT="" +CURSOR="" +UNIT_QUANTITY="" +LOGS="" + +[[ $# -eq 0 ]] && show_help + +COMMAND="$1"; shift + +while [[ $# -gt 0 ]]; do + case "$1" in + --key) API_KEY="$2"; shift 2 ;; + --raw) USE_JQ=false; shift ;; + --timeout) TIMEOUT="$2"; shift 2 ;; + --help) show_help ;; + --model) MODEL="$2"; shift 2 ;; + --data) DATA="$2"; shift 2 ;; + --prompt) PROMPT="$2"; shift 2 ;; + --image_url|--image-url) IMAGE_URL="$2"; shift 2 ;; + --request-id) REQUEST_ID="$2"; shift 2 ;; + --status-url) STATUS_URL="$2"; shift 2 ;; + --response-url) RESPONSE_URL="$2"; shift 2 ;; + --cancel-url) CANCEL_URL="$2"; shift 2 ;; + --request-timeout) REQUEST_TIMEOUT="$2"; shift 2 ;; + --no-retry) NO_RETRY=1; shift ;; + --expire) EXPIRE="$2"; shift 2 ;; + --webhook) WEBHOOK="$2"; shift 2 ;; + --query) QUERY="$2"; shift 2 ;; + --endpoint-id) ENDPOINT_ID="$2"; shift 2 ;; + --limit) LIMIT="$2"; shift 2 ;; + --cursor) CURSOR="$2"; shift 2 ;; + --unit-quantity) UNIT_QUANTITY="$2"; shift 2 ;; + --logs) LOGS=1; shift ;; + *) die "Unknown option: $1" ;; + esac +done + +[[ -z "$API_KEY" ]] && die "Set FAL_API_KEY environment variable or use --key" + +fmt() { + if $USE_JQ && command -v jq &>/dev/null; then + jq . + else + cat + fi +} + +# ---------- Build JSON body from shortcuts ---------- +build_body() { + local body="${DATA:-{\}}" + if [[ -n "$PROMPT" ]]; then + body=$(echo "$body" | jq --arg p "$PROMPT" '. + {prompt: $p}') + fi + if [[ -n "$IMAGE_URL" ]]; then + body=$(echo "$body" | jq --arg u "$IMAGE_URL" '. + {image_url: $u}') + fi + echo "$body" +} + +# ---------- Build extra headers ---------- +build_headers() { + local -a hdrs=() + if [[ -n "$REQUEST_TIMEOUT" ]]; then + hdrs+=(-H "X-Fal-Request-Timeout: $REQUEST_TIMEOUT") + fi + if [[ -n "$NO_RETRY" ]]; then + hdrs+=(-H "X-Fal-No-Retry: 1") + fi + if [[ -n "$EXPIRE" ]]; then + hdrs+=(-H "X-Fal-Object-Lifecycle-Preference: {\"expiration_duration_seconds\": $EXPIRE}") + fi + printf '%s\n' "${hdrs[@]+"${hdrs[@]}"}" +} + +# ---------- Model path normalization ---------- +# fal normalizes queue management URLs to the base model path (first 2 segments). +# e.g. fal-ai/wan/v2.2-a14b/text-to-video → fal-ai/wan +# This is required for status/result/cancel on models with sub-paths. +base_model() { + echo "$1" | cut -d'/' -f1-2 +} + +# ---------- Commands ---------- + +cmd_run() { + [[ -z "$MODEL" ]] && die "run requires --model MODEL_ID" + local body + body=$(build_body) + local -a extra_hdrs=() + while IFS= read -r h; do [[ -n "$h" ]] && extra_hdrs+=("$h"); done < <(build_headers) + + curl -sS --max-time "$TIMEOUT" \ + -X POST "${SYNC_BASE}/${MODEL}" \ + -H "Authorization: Key $API_KEY" \ + -H "Content-Type: application/json" \ + "${extra_hdrs[@]+"${extra_hdrs[@]}"}" \ + -d "$body" | fmt +} + +cmd_queue() { + [[ -z "$MODEL" ]] && die "queue requires --model MODEL_ID" + local body + body=$(build_body) + local -a extra_hdrs=() + while IFS= read -r h; do [[ -n "$h" ]] && extra_hdrs+=("$h"); done < <(build_headers) + + local url="${QUEUE_BASE}/${MODEL}" + if [[ -n "$WEBHOOK" ]]; then + url="${url}?fal_webhook=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$WEBHOOK', safe=''))" 2>/dev/null || echo "$WEBHOOK")" + fi + + curl -sS --max-time "$TIMEOUT" \ + -X POST "$url" \ + -H "Authorization: Key $API_KEY" \ + -H "Content-Type: application/json" \ + "${extra_hdrs[@]+"${extra_hdrs[@]}"}" \ + -d "$body" | fmt +} + +cmd_status() { + local url="" + if [[ -n "$STATUS_URL" ]]; then + # Prefer explicit URL from queue response + url="$STATUS_URL" + else + [[ -z "$MODEL" ]] && die "status requires --model MODEL_ID or --status-url URL" + [[ -z "$REQUEST_ID" ]] && die "status requires --request-id ID" + local bm + bm=$(base_model "$MODEL") + url="${QUEUE_BASE}/${bm}/requests/${REQUEST_ID}/status" + fi + [[ -n "$LOGS" ]] && url="${url}?logs=1" + + curl -sS --max-time "$TIMEOUT" \ + -H "Authorization: Key $API_KEY" \ + "$url" | fmt +} + +cmd_result() { + local url="" + if [[ -n "$RESPONSE_URL" ]]; then + # Prefer explicit URL from queue response + url="$RESPONSE_URL" + else + [[ -z "$MODEL" ]] && die "result requires --model MODEL_ID or --response-url URL" + [[ -z "$REQUEST_ID" ]] && die "result requires --request-id ID" + local bm + bm=$(base_model "$MODEL") + url="${QUEUE_BASE}/${bm}/requests/${REQUEST_ID}" + fi + + curl -sS --max-time "$TIMEOUT" \ + -H "Authorization: Key $API_KEY" \ + "$url" | fmt +} + +cmd_cancel() { + local url="" + if [[ -n "$CANCEL_URL" ]]; then + # Prefer explicit URL from queue response + url="$CANCEL_URL" + else + [[ -z "$MODEL" ]] && die "cancel requires --model MODEL_ID or --cancel-url URL" + [[ -z "$REQUEST_ID" ]] && die "cancel requires --request-id ID" + local bm + bm=$(base_model "$MODEL") + url="${QUEUE_BASE}/${bm}/requests/${REQUEST_ID}/cancel" + fi + + curl -sS --max-time "$TIMEOUT" \ + -X PUT \ + -H "Authorization: Key $API_KEY" \ + "$url" | fmt +} + +cmd_search() { + local params=() + [[ -n "$QUERY" ]] && params+=("query=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$QUERY'))" 2>/dev/null || echo "$QUERY")") + [[ -n "$ENDPOINT_ID" ]] && params+=("endpoint_id=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$ENDPOINT_ID'))" 2>/dev/null || echo "$ENDPOINT_ID")") + [[ -n "$LIMIT" ]] && params+=("limit=$LIMIT") + [[ -n "$CURSOR" ]] && params+=("cursor=$CURSOR") + + local qs="" + if [[ ${#params[@]} -gt 0 ]]; then + qs="?$(IFS='&'; echo "${params[*]}")" + fi + + curl -sS --max-time "$TIMEOUT" \ + -H "Authorization: Key $API_KEY" \ + "${PLATFORM_BASE}/models${qs}" | fmt +} + +cmd_schema() { + [[ -z "$ENDPOINT_ID" ]] && die "schema requires --endpoint-id MODEL_ID" + + local encoded + encoded=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$ENDPOINT_ID'))" 2>/dev/null || echo "$ENDPOINT_ID") + + curl -sS --max-time "$TIMEOUT" \ + -H "Authorization: Key $API_KEY" \ + "${PLATFORM_BASE}/models?endpoint_id=${encoded}&expand=openapi-3.0" | fmt +} + +cmd_pricing() { + [[ -z "$ENDPOINT_ID" ]] && die "pricing requires --endpoint-id MODEL_ID" + + local encoded + encoded=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$ENDPOINT_ID'))" 2>/dev/null || echo "$ENDPOINT_ID") + + curl -sS --max-time "$TIMEOUT" \ + -H "Authorization: Key $API_KEY" \ + "${PLATFORM_BASE}/models/pricing?endpoint_id=${encoded}" | fmt +} + +cmd_estimate() { + [[ -z "$ENDPOINT_ID" ]] && die "estimate requires --endpoint-id MODEL_ID" + local qty="${UNIT_QUANTITY:-1}" + + curl -sS --max-time "$TIMEOUT" \ + -X POST "${PLATFORM_BASE}/models/pricing/estimate" \ + -H "Authorization: Key $API_KEY" \ + -H "Content-Type: application/json" \ + -d "{\"estimate_type\":\"unit_price\",\"endpoints\":{\"${ENDPOINT_ID}\":{\"unit_quantity\":${qty}}}}" | fmt +} + +# ---------- Dispatch ---------- +case "$COMMAND" in + run) cmd_run ;; + queue) cmd_queue ;; + status) cmd_status ;; + result) cmd_result ;; + cancel) cmd_cancel ;; + search) cmd_search ;; + schema) cmd_schema ;; + pricing) cmd_pricing ;; + estimate) cmd_estimate ;; + --help|-h) show_help ;; + *) die "Unknown command: $COMMAND. Run with --help for usage." ;; +esac