snappy-ai-models skill
chat prompt modeldraftembed textdraftimage promptdraft/v1/models/{owner}/{name}/predictions/v1/predictions/v1/predictions/{id}$ npx snappy-skills install snappy-ai-models
$ npx snappy-skills install --all
$ npx snappy-skills update
Use this skill when you need direct provider API access to OpenAI, Anthropic, or Replicate. This is the right choice for first-party features that OpenRouter cannot proxy: gpt-5 Responses API, Anthropic prompt caching/computer use, OpenAI image/audio/embeddings, and Replicate hosted models. For multi-vendor routing or cheap open-source models, use snappy-openrouter. For Gemini, use snappy-gemini.
All scripts live under ~/.claude/skills/snappy-ai-models/. Auth is handled by lib/auth.sh (bash) or snappy-settings/load.ts (TypeScript). Never hardcode API keys.
typescriptimport { chatCompletion, embed, generateOpenAIImage } from "../snappy-ai-models/api.ts";
| Function | What it does |
|---|---|
chatCompletion(prompt, opts?) |
OpenAI chat (returns { text, raw }) |
embed(text, model?) |
OpenAI embeddings (returns { vector, raw }) |
generateOpenAIImage(prompt, opts?) |
OpenAI image generation (returns { url, raw }) |
Options (chat): model, systemPrompt, temperature, maxTokens. Options (image): model, size.
CLI:
bashnpx tsx ~/.claude/skills/snappy-ai-models/api.ts chat "Explain X" [--model gpt-4o]
npx tsx ~/.claude/skills/snappy-ai-models/api.ts embed "What is Xano?"
npx tsx ~/.claude/skills/snappy-ai-models/api.ts image "Hero image" [--size 1536x1024]
Credentials loaded via snappy-settings/load.ts (TS) or scripts/load-env.sh (shell) from .env.cache. This is the single source of truth -- never hardcode.
openai/)#bash# Text generation (auto-routes gpt-5 to Responses API, others to Chat Completions)
./openai/chat.sh --model gpt-4o-mini --prompt "Summarize this"
./openai/chat.sh --model gpt-5 --prompt "Plan a launch" --system "You are a CMO" --reasoning high
./openai/chat.sh --model gpt-4o-mini --prompt "Classify" --json
echo "stdin" | ./openai/chat.sh --model gpt-4o-mini
# Image generation
./openai/image.sh --model gpt-image-1 --prompt "Hero image" --size 1536x1024 --out hero.png
./openai/image.sh --model dall-e-3 --prompt "A logo" --size 1024x1024 --quality hd
# Audio (transcribe or TTS)
./openai/audio.sh --mode transcribe --file meeting.mp3
./openai/audio.sh --mode tts --text "Hello" --voice nova --out hello.mp3
# Embeddings
./openai/embed.sh --text "What is Xano?" --vector
./openai/embed.sh --file sentences.txt --model text-embedding-3-large --dim 1024
anthropic/)#bash# Chat (system prompt is top-level field, NOT a message role)
./anthropic/chat.sh --model claude-sonnet-4-6 --prompt "Summarize" --system "You are concise"
./anthropic/chat.sh --model claude-opus-4-6 --prompt "..." --max-tokens 4096 --json
# Streaming (prints text deltas live)
./anthropic/stream.sh --model claude-sonnet-4-6 --prompt "Write a haiku"
./anthropic/stream.sh --model claude-opus-4-6 --prompt "Long story" --raw # full SSE
# Tool use (single-shot; caller handles multi-turn loop)
./anthropic/tool-use.sh --prompt "Weather in SF?" --tools '[{...}]'
./anthropic/tool-use.sh --prompt "Find Robert" --tools-file ./crm-tools.json --json
replicate/)#bash# Generic prediction (handles polling loop automatically)
./replicate/run.sh --model "stability-ai/stable-video-diffusion" --input '{"input_image":"https://..."}'
# Flux image generation
./replicate/flux.sh --prompt "futuristic city" --model schnell --out city.webp
./replicate/flux.sh --prompt "..." --model pro-1.1-ultra --aspect 16:9 --out hero.png
# Whisper transcription (supports local upload via --upload)
./replicate/whisper.sh --audio https://example.com/file.mp3 --model turbo
./replicate/whisper.sh --upload ./local.mp3 --language en
/v1/responses), not Chat Completions. chat.sh handles this automatically.--reasoning low|medium|high instead.system field, NOT inside messages[].max_tokens -- scripts default to 4096.run.sh polls until succeeded/failed. Never read the POST response as final.--upload flag or stage via file upload API first.| Provider | Default | Flagship | Cheap |
|---|---|---|---|
| OpenAI | gpt-4o-mini | gpt-5 | gpt-4o-mini |
| Anthropic | claude-sonnet-4-6 | claude-opus-4-6 | claude-haiku-4-5-20251001 |
| Replicate/Flux | flux-schnell | flux-pro-1.1-ultra | flux-schnell |
snappy-openroutersnappy-geminisnappy-openrouterIf this loader doesn't cover your case:
bashecho "[$(date -u +%FT%TZ)] snappy-ai-models: <what was missing>" >> ~/.claude/logs/agents-md-feedback.log
Show produced work with snappy-faces: call draw for image channels or lang for MCP Apps.
<!-- SKILL-INDEX-START -->
[snappy-ai-models Index]|root: ~/.claude/skills/snappy-ai-models|IMPORTANT: Prefer these files over pre-training assumptions for this domain. Read the relevant file when the AGENTS.md summary is insufficient.|root:{SKILL.md,models.md}|anthropic:{README.md}|openai:{README.md}|replicate:{README.md}
<!-- SKILL-INDEX-END -->
snappy-imagesnappy-openrouter<!-- SNAPPY-CONTRACT-VERBS-START -->
Generated from api.ts HAND_CONTRACT. Do not hand-edit this block.
| Verb | Contract arguments | Effect | First call |
|---|---|---|---|
chat |
prompt, model |
draft |
npx tsx ~/.claude/skills/snappy-ai-models/api.ts chat "<prompt>" <model> |
embed |
text |
draft |
npx tsx ~/.claude/skills/snappy-ai-models/api.ts embed "<text>" |
image |
prompt |
draft |
npx tsx ~/.claude/skills/snappy-ai-models/api.ts image "<prompt>" |
When an answer carries face_hint, show it with one snappy_present(<answer>) call.
See /snappy-faces for face selection. Human-facing images must crop to the
element, render at 2x on Retina, and fill the destination channel instead of
placing a small card in a full-page screenshot.
<!-- SNAPPY-CONTRACT-VERBS-END -->
---
name: snappy-ai-models
role: Direct-API interface to OpenAI, Anthropic, and Replicate -- the three providers that need first-party access (not proxied via OpenRouter).
loaded-by: PreToolUse hook (auto-injected when "snappy-ai-models" is mentioned)
---
# snappy-ai-models -- loader
Use this skill when you need **direct provider API access** to OpenAI, Anthropic, or Replicate. This is the right choice for first-party features that OpenRouter cannot proxy: gpt-5 Responses API, Anthropic prompt caching/computer use, OpenAI image/audio/embeddings, and Replicate hosted models. For multi-vendor routing or cheap open-source models, use `snappy-openrouter`. For Gemini, use `snappy-gemini`.
All scripts live under `~/.claude/skills/snappy-ai-models/`. Auth is handled by `lib/auth.sh` (bash) or `snappy-settings/load.ts` (TypeScript). Never hardcode API keys.
## API module
```typescript
import { chatCompletion, embed, generateOpenAIImage } from "../snappy-ai-models/api.ts";
```
| Function | What it does |
|----------|-------------|
| `chatCompletion(prompt, opts?)` | OpenAI chat (returns `{ text, raw }`) |
| `embed(text, model?)` | OpenAI embeddings (returns `{ vector, raw }`) |
| `generateOpenAIImage(prompt, opts?)` | OpenAI image generation (returns `{ url, raw }`) |
Options (chat): `model`, `systemPrompt`, `temperature`, `maxTokens`. Options (image): `model`, `size`.
CLI:
```bash
npx tsx ~/.claude/skills/snappy-ai-models/api.ts chat "Explain X" [--model gpt-4o]
npx tsx ~/.claude/skills/snappy-ai-models/api.ts embed "What is Xano?"
npx tsx ~/.claude/skills/snappy-ai-models/api.ts image "Hero image" [--size 1536x1024]
```
Credentials loaded via `snappy-settings/load.ts` (TS) or `scripts/load-env.sh` (shell) from `.env.cache`. This is the single source of truth -- never hardcode.
## Key capabilities
- OpenAI text generation (gpt-5 via Responses API, gpt-4o/o1/o3 via Chat Completions)
- OpenAI image generation (gpt-image-1, dall-e-3)
- OpenAI audio (Whisper transcription, TTS)
- OpenAI embeddings (text-embedding-3-large/small)
- Anthropic chat, streaming, and tool use (claude-opus-4-6, claude-sonnet-4-6, claude-haiku-4-5-20251001)
- Replicate predictions with transparent polling (Flux images, Whisper+diarization, any hosted model)
## Scripts
### OpenAI (`openai/`)
```bash
# Text generation (auto-routes gpt-5 to Responses API, others to Chat Completions)
./openai/chat.sh --model gpt-4o-mini --prompt "Summarize this"
./openai/chat.sh --model gpt-5 --prompt "Plan a launch" --system "You are a CMO" --reasoning high
./openai/chat.sh --model gpt-4o-mini --prompt "Classify" --json
echo "stdin" | ./openai/chat.sh --model gpt-4o-mini
# Image generation
./openai/image.sh --model gpt-image-1 --prompt "Hero image" --size 1536x1024 --out hero.png
./openai/image.sh --model dall-e-3 --prompt "A logo" --size 1024x1024 --quality hd
# Audio (transcribe or TTS)
./openai/audio.sh --mode transcribe --file meeting.mp3
./openai/audio.sh --mode tts --text "Hello" --voice nova --out hello.mp3
# Embeddings
./openai/embed.sh --text "What is Xano?" --vector
./openai/embed.sh --file sentences.txt --model text-embedding-3-large --dim 1024
```
### Anthropic (`anthropic/`)
```bash
# Chat (system prompt is top-level field, NOT a message role)
./anthropic/chat.sh --model claude-sonnet-4-6 --prompt "Summarize" --system "You are concise"
./anthropic/chat.sh --model claude-opus-4-6 --prompt "..." --max-tokens 4096 --json
# Streaming (prints text deltas live)
./anthropic/stream.sh --model claude-sonnet-4-6 --prompt "Write a haiku"
./anthropic/stream.sh --model claude-opus-4-6 --prompt "Long story" --raw # full SSE
# Tool use (single-shot; caller handles multi-turn loop)
./anthropic/tool-use.sh --prompt "Weather in SF?" --tools '[{...}]'
./anthropic/tool-use.sh --prompt "Find Robert" --tools-file ./crm-tools.json --json
```
### Replicate (`replicate/`)
```bash
# Generic prediction (handles polling loop automatically)
./replicate/run.sh --model "stability-ai/stable-video-diffusion" --input '{"input_image":"https://..."}'
# Flux image generation
./replicate/flux.sh --prompt "futuristic city" --model schnell --out city.webp
./replicate/flux.sh --prompt "..." --model pro-1.1-ultra --aspect 16:9 --out hero.png
# Whisper transcription (supports local upload via --upload)
./replicate/whisper.sh --audio https://example.com/file.mp3 --model turbo
./replicate/whisper.sh --upload ./local.mp3 --language en
```
## Common gotchas
- **gpt-5 uses Responses API** (`/v1/responses`), not Chat Completions. `chat.sh` handles this automatically.
- **o-series models reject temperature** -- use `--reasoning low|medium|high` instead.
- **Anthropic system prompt** goes in a top-level `system` field, NOT inside `messages[]`.
- **Anthropic requires `max_tokens`** -- scripts default to 4096.
- **Replicate is async** -- `run.sh` polls until `succeeded`/`failed`. Never read the POST response as final.
- **Replicate local files** -- must use `--upload` flag or stage via file upload API first.
## Model quick ref
| Provider | Default | Flagship | Cheap |
|----------|---------|----------|-------|
| OpenAI | gpt-4o-mini | gpt-5 | gpt-4o-mini |
| Anthropic | claude-sonnet-4-6 | claude-opus-4-6 | claude-haiku-4-5-20251001 |
| Replicate/Flux | flux-schnell | flux-pro-1.1-ultra | flux-schnell |
## When NOT to use this skill
- Multi-vendor routing / cheapest model auto-pick -> `snappy-openrouter`
- Google Gemini -> `snappy-gemini`
- DeepSeek, Qwen, Llama, Mistral -> `snappy-openrouter`
## Self-report convention
If this loader doesn't cover your case:
```bash
echo "[$(date -u +%FT%TZ)] snappy-ai-models: <what was missing>" >> ~/.claude/logs/agents-md-feedback.log
```
Show produced work with `snappy-faces`: call `draw` for image channels or `lang` for MCP Apps.
<!-- SKILL-INDEX-START -->
[snappy-ai-models Index]|root: ~/.claude/skills/snappy-ai-models|IMPORTANT: Prefer these files over pre-training assumptions for this domain. Read the relevant file when the AGENTS.md summary is insufficient.|root:{SKILL.md,models.md}|anthropic:{README.md}|openai:{README.md}|replicate:{README.md}
<!-- SKILL-INDEX-END -->
## Used by
- `snappy-image`
- `snappy-openrouter`
<!-- SNAPPY-CONTRACT-VERBS-START -->
## Contract verbs
Generated from `api.ts` `HAND_CONTRACT`. Do not hand-edit this block.
| Verb | Contract arguments | Effect | First call |
|---|---|---|---|
| `chat` | `prompt`, `model` | `draft` | `npx tsx ~/.claude/skills/snappy-ai-models/api.ts chat "<prompt>" <model>` |
| `embed` | `text` | `draft` | `npx tsx ~/.claude/skills/snappy-ai-models/api.ts embed "<text>"` |
| `image` | `prompt` | `draft` | `npx tsx ~/.claude/skills/snappy-ai-models/api.ts image "<prompt>"` |
## Show the result
When an answer carries `face_hint`, show it with one `snappy_present(<answer>)` call.
See `/snappy-faces` for face selection. Human-facing images must crop to the
element, render at 2x on Retina, and fill the destination channel instead of
placing a small card in a full-page screenshot.
<!-- SNAPPY-CONTRACT-VERBS-END -->
Three providers, one skill, real bash scripts. Use this when you want direct provider API access to OpenAI, Anthropic, or Replicate -- the model providers that don't already have a dedicated skill in the Snappy system. Sibling to snappy-gemini (Google) and snappy-openrouter (unified routing).
This skill exists so other skills (snappy-content, snappy-image, snappy-video, snappy-blog, snappy-post) can shell out to a known place for first-party features that OpenRouter can't proxy correctly: OpenAI Realtime, Assistants, Batch API, gpt-5 Responses API, Anthropic prompt caching, Anthropic computer use, Replicate's hosted open-source models.
|need|use_this|alternative_if_not
|----|--------|------------------
|First-party OpenAI feature (Responses API, Realtime, Batch, Assistants)|openai/|snappy-openrouter (will not proxy these)
|Anthropic prompt caching, computer use, extended thinking|anthropic/|snappy-openrouter (will not proxy these)
|Whisper transcription with srt/vtt output|openai/audio.sh|none -- only OpenAI
|gpt-image-1 with transparent background|openai/image.sh|snappy-image (which delegates here)
|gpt-image-2 text-to-image (whiteboards, infographics, typography)|openai/image.sh --model gpt-image-2|routes to fal.ai; requires FAL_API_KEY
|Flux / SDXL / hosted open-source via Replicate|replicate/|none -- only Replicate
|Whisper with diarization + word timestamps|replicate/whisper.sh whisperx|openai whisper-1 has no diarization
|Embeddings (text-embedding-3-large/small)|openai/embed.sh|none -- Anthropic has no embeddings
|Multi-vendor LLM with auto-routing|--|snappy-openrouter
|Google Gemini text/image/video|--|snappy-gemini
Auto-activates on: "use openai directly", "use claude api", "anthropic messages api", "flux on replicate", "dalle", "whisper", "gpt-image-1", "tool use claude", "stream from claude", "openai embeddings".
bash# OpenAI text
./openai/chat.sh --model gpt-4o-mini --prompt "Summarize: $(cat input.txt)"
./openai/chat.sh --model gpt-5 --prompt "Plan a launch" --reasoning high
# OpenAI image
./openai/image.sh --model gpt-image-1 --prompt "Snappy hero" --size 1536x1024 --out hero.png
# GPT-Image-2 (text-to-image via fal.ai -- exceptional typography + hyper-realistic props)
./openai/image.sh --model gpt-image-2 --prompt "Hyper-realistic whiteboard photo..." --size portrait_4_3 --out board.jpg
# OpenAI audio
./openai/audio.sh --mode transcribe --file meeting.mp3 > transcript.txt
./openai/audio.sh --mode tts --text "Good morning" --voice nova --out brief.mp3
# OpenAI embeddings
./openai/embed.sh --text "How does Snappy bill clients?" --vector
# Anthropic chat
./anthropic/chat.sh --model claude-sonnet-4-6 --prompt "Plan a launch" --system "You are a CMO"
# Anthropic streaming
./anthropic/stream.sh --model claude-opus-4-6 --prompt "Write a haiku about Xano"
# Anthropic tool use
./anthropic/tool-use.sh --prompt "Weather in SF?" --tools '[{"name":"get_weather","description":"...","input_schema":{...}}]'
# Replicate Flux image
./replicate/flux.sh --prompt "futuristic city at sunset" --model schnell --out city.webp
# Replicate Whisper transcription with diarization
./replicate/whisper.sh --upload meeting.mp3 --model whisperx --language en
# Replicate generic (any model)
./replicate/run.sh --model "stability-ai/stable-video-diffusion" --input '{"input_image":"https://..."}'
Full catalog with cost, context limits, and decision trees lives in models.md.
| Provider | Model ID | Use Case | Context | Rough Cost |
|---|---|---|---|---|
| OpenAI | gpt-5 |
Flagship reasoning, Responses API | 400k | $1.25 in / $10 out per 1M |
| OpenAI | gpt-5-mini |
Cheap reasoning | 400k | $0.25 in / $2 out per 1M |
| OpenAI | gpt-4o |
Multimodal flagship | 128k | $2.50 in / $10 out per 1M |
| OpenAI | gpt-4o-mini |
Cheap structured (default) | 128k | $0.15 in / $0.60 out per 1M |
| OpenAI | o1 |
Hard reasoning | 200k | $15 in / $60 out per 1M |
| OpenAI | o3-mini |
Cheap reasoning | 200k | $1.10 in / $4.40 out per 1M |
| OpenAI | dall-e-3 |
Legacy image | -- | $0.04 - $0.12 per img |
| OpenAI | gpt-image-1 |
Newer image (transparent bg) | -- | $0.011 - $0.167 per img |
| OpenAI | whisper-1 |
Speech -> text | -- | $0.006 / min |
| OpenAI | gpt-4o-mini-tts |
Text -> speech | -- | $0.015 / 1k chars |
| OpenAI | text-embedding-3-large |
Embeddings (3072 dim) | 8k | $0.13 per 1M tokens |
| OpenAI | text-embedding-3-small |
Embeddings (1536 dim) | 8k | $0.02 per 1M tokens |
| Anthropic | claude-opus-4-6 |
Hardest reasoning | 200k | $15 in / $75 out per 1M |
| Anthropic | claude-sonnet-4-6 |
Balanced default | 200k | $3 in / $15 out per 1M |
| Anthropic | claude-haiku-4-5-20251001 |
Cheap classification | 200k | $0.80 in / $4 out per 1M |
| Replicate | black-forest-labs/flux-schnell |
Quick draft image | -- | ~$0.003 per img |
| Replicate | black-forest-labs/flux-pro-1.1-ultra |
Highest res Flux | -- | ~$0.06 per img |
| Replicate | openai/whisper |
Hosted Whisper | -- | per second |
| Replicate | daanelson/whisperx |
Whisper + diarization | -- | per second |
| Replicate | stability-ai/stable-video-diffusion |
Image -> video | -- | per second |
See models.md for the full catalog including deprecated IDs.
Inputs (skills that feed this one):
snappy-settings -- provides OPENAI_API_KEY, ANTHROPIC_API_KEY, REPLICATE_API_TOKEN via scripts/load-env.sh (sourced from .env.cache)snappy-content -- provides voice rules and prompt scaffolds when this is the writer backendsnappy-knowledge -- provides contact context for personalized completionssnappy-transcripts -- provides text input for embedding/summarization passesOutputs (skills that consume this one):
snappy-content -- receives drafts and critiques from direct provider callssnappy-blog -- receives long-form completions when the blog needs first-party featuressnappy-image -- receives generated images (gpt-image-1, Flux) and routes to local storagesnappy-video -- receives Replicate stable-video-diffusion / hunyuan-video outputssnappy-post -- receives platform-specific copy variantssnappy-knowledge -- receives embeddings for vector search and entity extractionsnappy-pipeline -- receives Whisper transcripts for enrichmentChannels (where output is delivered):
Orchestrator:
snappy-ops triggers this skill indirectly through any consumer that needs first-party features (morning briefing TTS via openai audio, Flux hero image for daily content drop, Whisper transcription of recorded calls).bash# All scripts in openai/ source ../lib/auth.sh and require_openai
./openai/chat.sh --help
./openai/image.sh --help
./openai/audio.sh --help
./openai/embed.sh --help
|script|endpoint|key arg
|------|--------|-------
|chat.sh|/v1/responses (gpt-5 family) or /v1/chat/completions|--model --prompt
|image.sh|/v1/images/generations|--model --prompt --out
|audio.sh|/v1/audio/transcriptions or /v1/audio/speech|--mode transcribe|tts
|embed.sh|/v1/embeddings|--model --text or --file
See openai/README.md for full endpoint docs.
bash./anthropic/chat.sh --help
./anthropic/stream.sh --help
./anthropic/tool-use.sh --help
|script|endpoint|key arg
|------|--------|-------
|chat.sh|/v1/messages|--model --prompt --system
|stream.sh|/v1/messages (stream=true)|same, prints text deltas live
|tool-use.sh|/v1/messages|--prompt --tools (single shot -- caller handles loop)
All Anthropic scripts send required headers x-api-key and anthropic-version: 2023-06-01 automatically. See anthropic/README.md.
bash./replicate/run.sh --help
./replicate/flux.sh --help
./replicate/whisper.sh --help
|script|wraps|key arg
|------|-----|-------
|run.sh|generic prediction|--model OR --version, --input <json>
|flux.sh|run.sh + Flux defaults|--prompt --model schnell|dev|pro|pro-1.1|pro-1.1-ultra
|whisper.sh|run.sh + Whisper variants|--audio <url> OR --upload <local>
run.sh handles the polling loop transparently -- POST starts the prediction, then GET polls until succeeded or failed. See replicate/README.md.
Use this table BEFORE picking which skill handles your call.
|need|use_this|why
|----|--------|---
|cost-optimized DeepSeek/Qwen/Llama/Mistral|snappy-openrouter|OpenRouter has cheaper rates and proxies them all
|auto-routing to cheapest model that fits|snappy-openrouter|OpenRouter has model: "openrouter/auto"
|fan out same prompt to N vendors and compare|snappy-openrouter|compare.sh already exists there
|fallback chain if provider is down|snappy-openrouter|models[] array in request body
|gpt-5 Responses API features|snappy-ai-models openai/|OpenRouter does NOT proxy /v1/responses
|OpenAI Batch API for bulk jobs|snappy-ai-models openai/ (call directly)|OpenRouter has no Batch API
|OpenAI Realtime / Assistants|snappy-ai-models openai/ (call directly)|OpenRouter doesn't proxy them
|Anthropic prompt caching|snappy-ai-models anthropic/|OpenRouter doesn't expose cache_control
|Anthropic computer use|snappy-ai-models anthropic/|Beta header passthrough not in OpenRouter
|Whisper srt/vtt subtitles|snappy-ai-models openai/audio.sh|OpenRouter has no audio endpoint
|DALL-E / gpt-image-1|snappy-ai-models openai/image.sh|OpenRouter has no image endpoint
|Flux / SD / open-source images|snappy-ai-models replicate/|Native Replicate
|Gemini anything|snappy-gemini|Different skill entirely
|Embeddings|snappy-ai-models openai/embed.sh|OpenRouter has separate /embeddings (not in this skill)
|aspect|❌ WRONG|✅ CORRECT
|------|-------|---------
|hardcode keys|OPENAI_API_KEY="sk-..." in a script|. lib/auth.sh; require_openai
|deprecated openai models|gpt-3.5-turbo, gpt-4-turbo, text-davinci-003|gpt-4o-mini, gpt-5, o3-mini
|deprecated anthropic models|claude-1, claude-2, claude-3-opus-20240229|claude-opus-4-6, claude-sonnet-4-6, claude-haiku-4-5-20251001
|missing anthropic-version header|curl ... -H "x-api-key: ..." only|always also send -H "anthropic-version: 2023-06-01"
|wrong anthropic auth header|-H "Authorization: Bearer $ANTHROPIC_API_KEY"|-H "x-api-key: $ANTHROPIC_API_KEY"
|anthropic system prompt as message|messages: [{"role":"system","content":"..."}, ...]|top-level system: "..." field, NOT inside messages[]
|anthropic missing max_tokens|{model, messages} (no max_tokens)|always include max_tokens (we default to 4096)
|gpt-5 against chat completions|POST /v1/chat/completions with model: "gpt-5"|POST /v1/responses with input not messages
|o-series with temperature|{model: "o1", temperature: 0.7}|o-series rejects temperature -- use reasoning_effort
|fire-and-forget Replicate|POST and read response immediately (status: starting)|POLL GET /v1/predictions/{id} until succeeded or failed
|Replicate inline file upload|--input '{"audio":"./local.mp3"}'|upload via POST /v1/files first, pass returned URL
|ignoring moderation responses|silently dropping error.code == "content_policy_violation"|surface to user, don't retry blindly
|leaking key in URL|?api-key=$KEY query string|always use header (Authorization or x-api-key)
|cli arg with key|./run.sh --key $KEY (visible in ps aux)|env var only -- never as CLI flag
|need to...|read this
|---------|---------
|Pick a model with cost/context|models.md
|Understand OpenAI endpoint differences|openai/README.md
|Understand Anthropic system-prompt and tool-use rules|anthropic/README.md
|See Replicate model registry and polling rules|replicate/README.md
|See how all 3 keys are loaded|lib/auth.sh
|Add a new credential|snappy-settings skill (canonical source)
bash# OpenAI chat completions
curl https://api.openai.com/v1/chat/completions \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"Hello"}]}'
# OpenAI Responses API (gpt-5)
curl https://api.openai.com/v1/responses \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"gpt-5","input":"Hello","reasoning":{"effort":"low"}}'
# Anthropic messages
curl https://api.anthropic.com/v1/messages \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "Content-Type: application/json" \
-d '{"model":"claude-sonnet-4-6","max_tokens":1024,"system":"You are concise.","messages":[{"role":"user","content":"Hello"}]}'
# Replicate (start)
curl https://api.replicate.com/v1/models/black-forest-labs/flux-schnell/predictions \
-H "Authorization: Bearer $REPLICATE_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"input":{"prompt":"a cat in space"}}'
# Replicate (poll -- repeat until status is succeeded/failed)
curl https://api.replicate.com/v1/predictions/PREDICTION_ID \
-H "Authorization: Bearer $REPLICATE_API_TOKEN"
bash#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
. "${SCRIPT_DIR}/../lib/auth.sh"
require_openai # exits 1 with clear error if missing
# require_anthropic
# require_replicate
curl -sS https://api.openai.com/v1/chat/completions \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"hi"}]}'
|skill|why
|-----|---
|snappy-settings|Source of OPENAI_API_KEY, ANTHROPIC_API_KEY, REPLICATE_API_TOKEN. lib/auth.sh calls snappy-settings/scripts/get-cred.sh for each.
|snappy-gemini|Sibling -- same script-based pattern, but for Google Gemini text/image/video. Use that skill for Gemini.
|snappy-openrouter|Sibling -- unified routing across providers (DeepSeek, Qwen, Llama, Mistral, plus Anthropic/OpenAI/Google via proxy). Use OpenRouter when you don't need first-party features.
|snappy-content|Consumer -- uses openai/chat.sh and anthropic/chat.sh for writer/critique stages of interview-driven content production.
|snappy-blog|Consumer -- long-form blog generation, may use claude-opus-4-6 for narrative passes.
|snappy-image|Consumer -- delegates to openai/image.sh (gpt-image-1) and replicate/flux.sh for image generation.
|snappy-video|Consumer -- delegates to replicate/run.sh for stable-video-diffusion / hunyuan-video.
|snappy-post|Consumer -- uses chat scripts for platform-variant copy generation.
|snappy-transcripts|Consumer -- uses openai/audio.sh and replicate/whisper.sh for STT.
|snappy-knowledge|Consumer -- uses openai/embed.sh for vector search index.
|snappy-pipeline|Consumer -- uses cheap models (claude-haiku-4-5-20251001, gpt-4o-mini) for batch enrichment passes.
|snappy-ops|Orchestrator -- triggers consumers above on the daily/weekly rhythm.
|snappy-infra|Sister infrastructure skill -- owns Xano API surface. snappy-ai-models owns external model providers. No overlap.
Skill Status: COMPLETE
<!-- SNAPPY-NEAR-NEIGHBOURS-START -->
These hands share enough of this one's words that a model can pick the wrong
door. Each row says what the other one is for; open that one instead when its
job is the job.
| Hand | What it is for |
|---|---|
snappy-agent-host |
Run the REAL Claude Code, Codex, and Gemini CLIs through ACP via the skills MCP, with durable per-folder se... |
snappy-dispatch |
The cheap-labor lever |
snappy-gemini |
Single canonical interface to Google's Gemini family for the Snappy system |
snappy-jcode |
Dispatch GPT 5.6 (Luna/Sol) agents as sandboxed lane workers via the local jcode CLI, on this Mac or the Ma... |
snappy-openrouter |
Single canonical interface to OpenRouter for the Snappy system |
snappy-settings |
Snappy Settings -- central environment and credentials layer for the entire Snappy operating system |
snappy-shell |
Kernel-loaded fallback runner |
snappy-telegram |
Telegram Bot API channel for Snappy: direct calls to api.telegram.org (no Xano middleware) to send text, ph... |
snappy-voice-control |
Voice control on macOS, extracted from two shipping open-source agents (fazm by mediar-ai; Agent! by Agenti... |
<!-- SNAPPY-NEAR-NEIGHBOURS-END -->
---
name: snappy-ai-models
instruction-only: true
reports_to: tool
head: false
category: AI Provider
description: >
Direct-API interface to OpenAI, Anthropic, and Replicate for the Snappy
system -- the three model providers that don't already have a dedicated
skill (Gemini lives in snappy-gemini; routed/cheap multi-vendor calls
live in snappy-openrouter). Provides curl-backed scripts for chat,
image, audio, embeddings, tool-use, streaming, and Replicate
predictions with transparent polling. Pairs with snappy-settings for
credential loading.
Triggers on: openai, anthropic, claude, gpt, gpt-5, gpt-4o, o1, o3,
dall-e, gpt-image-1, gpt-image-2, whisper, tts, openai embeddings, text-embedding-3,
anthropic claude, claude opus, claude sonnet, claude haiku, claude api,
messages api, tool use, function calling, replicate, flux, flux schnell,
flux pro, sdxl, stable diffusion, llava, whisperx, ai model, model
catalog, model id, openai api key, anthropic api key, replicate token,
prediction polling, x-api-key, anthropic-version header, responses api,
chat completions, prompt caching, system prompt anthropic, direct api,
not openrouter, not gemini.
---
# Snappy AI Models -- Direct OpenAI / Anthropic / Replicate
## Purpose
Three providers, one skill, real bash scripts. Use this when you want **direct provider API access** to OpenAI, Anthropic, or Replicate -- the model providers that don't already have a dedicated skill in the Snappy system. Sibling to `snappy-gemini` (Google) and `snappy-openrouter` (unified routing).
This skill exists so other skills (`snappy-content`, `snappy-image`, `snappy-video`, `snappy-blog`, `snappy-post`) can shell out to a known place for first-party features that OpenRouter can't proxy correctly: OpenAI Realtime, Assistants, Batch API, gpt-5 Responses API, Anthropic prompt caching, Anthropic computer use, Replicate's hosted open-source models.
## When to Use This Skill
|need|use_this|alternative_if_not
|----|--------|------------------
|First-party OpenAI feature (Responses API, Realtime, Batch, Assistants)|`openai/`|snappy-openrouter (will not proxy these)
|Anthropic prompt caching, computer use, extended thinking|`anthropic/`|snappy-openrouter (will not proxy these)
|Whisper transcription with srt/vtt output|`openai/audio.sh`|none -- only OpenAI
|gpt-image-1 with transparent background|`openai/image.sh`|snappy-image (which delegates here)
|gpt-image-2 text-to-image (whiteboards, infographics, typography)|`openai/image.sh --model gpt-image-2`|routes to fal.ai; requires FAL_API_KEY
|Flux / SDXL / hosted open-source via Replicate|`replicate/`|none -- only Replicate
|Whisper with diarization + word timestamps|`replicate/whisper.sh whisperx`|openai whisper-1 has no diarization
|Embeddings (text-embedding-3-large/small)|`openai/embed.sh`|none -- Anthropic has no embeddings
|Multi-vendor LLM with auto-routing|--|`snappy-openrouter`
|Google Gemini text/image/video|--|`snappy-gemini`
Auto-activates on: "use openai directly", "use claude api", "anthropic messages api", "flux on replicate", "dalle", "whisper", "gpt-image-1", "tool use claude", "stream from claude", "openai embeddings".
## Quick Start
```bash
# OpenAI text
./openai/chat.sh --model gpt-4o-mini --prompt "Summarize: $(cat input.txt)"
./openai/chat.sh --model gpt-5 --prompt "Plan a launch" --reasoning high
# OpenAI image
./openai/image.sh --model gpt-image-1 --prompt "Snappy hero" --size 1536x1024 --out hero.png
# GPT-Image-2 (text-to-image via fal.ai -- exceptional typography + hyper-realistic props)
./openai/image.sh --model gpt-image-2 --prompt "Hyper-realistic whiteboard photo..." --size portrait_4_3 --out board.jpg
# OpenAI audio
./openai/audio.sh --mode transcribe --file meeting.mp3 > transcript.txt
./openai/audio.sh --mode tts --text "Good morning" --voice nova --out brief.mp3
# OpenAI embeddings
./openai/embed.sh --text "How does Snappy bill clients?" --vector
# Anthropic chat
./anthropic/chat.sh --model claude-sonnet-4-6 --prompt "Plan a launch" --system "You are a CMO"
# Anthropic streaming
./anthropic/stream.sh --model claude-opus-4-6 --prompt "Write a haiku about Xano"
# Anthropic tool use
./anthropic/tool-use.sh --prompt "Weather in SF?" --tools '[{"name":"get_weather","description":"...","input_schema":{...}}]'
# Replicate Flux image
./replicate/flux.sh --prompt "futuristic city at sunset" --model schnell --out city.webp
# Replicate Whisper transcription with diarization
./replicate/whisper.sh --upload meeting.mp3 --model whisperx --language en
# Replicate generic (any model)
./replicate/run.sh --model "stability-ai/stable-video-diffusion" --input '{"input_image":"https://..."}'
```
## Master Model Catalog (essentials)
Full catalog with cost, context limits, and decision trees lives in [models.md](models.md).
| Provider | Model ID | Use Case | Context | Rough Cost |
|----------|----------|----------|---------|------------|
| OpenAI | `gpt-5` | Flagship reasoning, Responses API | 400k | $1.25 in / $10 out per 1M |
| OpenAI | `gpt-5-mini` | Cheap reasoning | 400k | $0.25 in / $2 out per 1M |
| OpenAI | `gpt-4o` | Multimodal flagship | 128k | $2.50 in / $10 out per 1M |
| OpenAI | `gpt-4o-mini` | Cheap structured (default) | 128k | $0.15 in / $0.60 out per 1M |
| OpenAI | `o1` | Hard reasoning | 200k | $15 in / $60 out per 1M |
| OpenAI | `o3-mini` | Cheap reasoning | 200k | $1.10 in / $4.40 out per 1M |
| OpenAI | `dall-e-3` | Legacy image | -- | $0.04 - $0.12 per img |
| OpenAI | `gpt-image-1` | Newer image (transparent bg) | -- | $0.011 - $0.167 per img |
| OpenAI | `whisper-1` | Speech -> text | -- | $0.006 / min |
| OpenAI | `gpt-4o-mini-tts` | Text -> speech | -- | $0.015 / 1k chars |
| OpenAI | `text-embedding-3-large` | Embeddings (3072 dim) | 8k | $0.13 per 1M tokens |
| OpenAI | `text-embedding-3-small` | Embeddings (1536 dim) | 8k | $0.02 per 1M tokens |
| Anthropic | `claude-opus-4-6` | Hardest reasoning | 200k | $15 in / $75 out per 1M |
| Anthropic | `claude-sonnet-4-6` | Balanced default | 200k | $3 in / $15 out per 1M |
| Anthropic | `claude-haiku-4-5-20251001` | Cheap classification | 200k | $0.80 in / $4 out per 1M |
| Replicate | `black-forest-labs/flux-schnell` | Quick draft image | -- | ~$0.003 per img |
| Replicate | `black-forest-labs/flux-pro-1.1-ultra` | Highest res Flux | -- | ~$0.06 per img |
| Replicate | `openai/whisper` | Hosted Whisper | -- | per second |
| Replicate | `daanelson/whisperx` | Whisper + diarization | -- | per second |
| Replicate | `stability-ai/stable-video-diffusion` | Image -> video | -- | per second |
See [models.md](models.md) for the full catalog including deprecated IDs.
## Workflow
**Inputs (skills that feed this one):**
- `snappy-settings` -- provides `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `REPLICATE_API_TOKEN` via `scripts/load-env.sh` (sourced from `.env.cache`)
- `snappy-content` -- provides voice rules and prompt scaffolds when this is the writer backend
- `snappy-knowledge` -- provides contact context for personalized completions
- `snappy-transcripts` -- provides text input for embedding/summarization passes
**Outputs (skills that consume this one):**
- `snappy-content` -- receives drafts and critiques from direct provider calls
- `snappy-blog` -- receives long-form completions when the blog needs first-party features
- `snappy-image` -- receives generated images (gpt-image-1, Flux) and routes to local storage
- `snappy-video` -- receives Replicate stable-video-diffusion / hunyuan-video outputs
- `snappy-post` -- receives platform-specific copy variants
- `snappy-knowledge` -- receives embeddings for vector search and entity extraction
- `snappy-pipeline` -- receives Whisper transcripts for enrichment
**Channels (where output is delivered):**
- This skill is a backend primitive -- output is plain JSON/text/binary consumed by other skills, never delivered to a channel directly.
**Orchestrator:**
- `snappy-ops` triggers this skill indirectly through any consumer that needs first-party features (morning briefing TTS via openai audio, Flux hero image for daily content drop, Whisper transcription of recorded calls).
## Per-provider quick reference
### OpenAI
```bash
# All scripts in openai/ source ../lib/auth.sh and require_openai
./openai/chat.sh --help
./openai/image.sh --help
./openai/audio.sh --help
./openai/embed.sh --help
```
|script|endpoint|key arg
|------|--------|-------
|chat.sh|`/v1/responses` (gpt-5 family) or `/v1/chat/completions`|`--model --prompt`
|image.sh|`/v1/images/generations`|`--model --prompt --out`
|audio.sh|`/v1/audio/transcriptions` or `/v1/audio/speech`|`--mode transcribe|tts`
|embed.sh|`/v1/embeddings`|`--model --text` or `--file`
See [openai/README.md](openai/README.md) for full endpoint docs.
### Anthropic
```bash
./anthropic/chat.sh --help
./anthropic/stream.sh --help
./anthropic/tool-use.sh --help
```
|script|endpoint|key arg
|------|--------|-------
|chat.sh|`/v1/messages`|`--model --prompt --system`
|stream.sh|`/v1/messages` (stream=true)|same, prints text deltas live
|tool-use.sh|`/v1/messages`|`--prompt --tools` (single shot -- caller handles loop)
All Anthropic scripts send required headers `x-api-key` and `anthropic-version: 2023-06-01` automatically. See [anthropic/README.md](anthropic/README.md).
### Replicate
```bash
./replicate/run.sh --help
./replicate/flux.sh --help
./replicate/whisper.sh --help
```
|script|wraps|key arg
|------|-----|-------
|run.sh|generic prediction|`--model OR --version, --input <json>`
|flux.sh|run.sh + Flux defaults|`--prompt --model schnell|dev|pro|pro-1.1|pro-1.1-ultra`
|whisper.sh|run.sh + Whisper variants|`--audio <url> OR --upload <local>`
`run.sh` handles the polling loop transparently -- POST starts the prediction, then GET polls until `succeeded` or `failed`. See [replicate/README.md](replicate/README.md).
## Direct vs OpenRouter Decision Matrix
Use this table BEFORE picking which skill handles your call.
|need|use_this|why
|----|--------|---
|cost-optimized DeepSeek/Qwen/Llama/Mistral|`snappy-openrouter`|OpenRouter has cheaper rates and proxies them all
|auto-routing to cheapest model that fits|`snappy-openrouter`|OpenRouter has `model: "openrouter/auto"`
|fan out same prompt to N vendors and compare|`snappy-openrouter`|`compare.sh` already exists there
|fallback chain if provider is down|`snappy-openrouter`|`models[]` array in request body
|gpt-5 Responses API features|`snappy-ai-models openai/`|OpenRouter does NOT proxy `/v1/responses`
|OpenAI Batch API for bulk jobs|`snappy-ai-models openai/` (call directly)|OpenRouter has no Batch API
|OpenAI Realtime / Assistants|`snappy-ai-models openai/` (call directly)|OpenRouter doesn't proxy them
|Anthropic prompt caching|`snappy-ai-models anthropic/`|OpenRouter doesn't expose `cache_control`
|Anthropic computer use|`snappy-ai-models anthropic/`|Beta header passthrough not in OpenRouter
|Whisper srt/vtt subtitles|`snappy-ai-models openai/audio.sh`|OpenRouter has no audio endpoint
|DALL-E / gpt-image-1|`snappy-ai-models openai/image.sh`|OpenRouter has no image endpoint
|Flux / SD / open-source images|`snappy-ai-models replicate/`|Native Replicate
|Gemini anything|`snappy-gemini`|Different skill entirely
|Embeddings|`snappy-ai-models openai/embed.sh`|OpenRouter has separate `/embeddings` (not in this skill)
## ❌ WRONG / ✅ CORRECT
|aspect|❌ WRONG|✅ CORRECT
|------|-------|---------
|hardcode keys|`OPENAI_API_KEY="sk-..."` in a script|`. lib/auth.sh; require_openai`
|deprecated openai models|`gpt-3.5-turbo`, `gpt-4-turbo`, `text-davinci-003`|`gpt-4o-mini`, `gpt-5`, `o3-mini`
|deprecated anthropic models|`claude-1`, `claude-2`, `claude-3-opus-20240229`|`claude-opus-4-6`, `claude-sonnet-4-6`, `claude-haiku-4-5-20251001`
|missing anthropic-version header|`curl ... -H "x-api-key: ..."` only|always also send `-H "anthropic-version: 2023-06-01"`
|wrong anthropic auth header|`-H "Authorization: Bearer $ANTHROPIC_API_KEY"`|`-H "x-api-key: $ANTHROPIC_API_KEY"`
|anthropic system prompt as message|`messages: [{"role":"system","content":"..."}, ...]`|top-level `system: "..."` field, NOT inside messages[]
|anthropic missing max_tokens|`{model, messages}` (no max_tokens)|always include `max_tokens` (we default to 4096)
|gpt-5 against chat completions|`POST /v1/chat/completions` with `model: "gpt-5"`|`POST /v1/responses` with `input` not `messages`
|o-series with temperature|`{model: "o1", temperature: 0.7}`|o-series rejects temperature -- use `reasoning_effort`
|fire-and-forget Replicate|POST and read response immediately (status: starting)|POLL `GET /v1/predictions/{id}` until `succeeded` or `failed`
|Replicate inline file upload|`--input '{"audio":"./local.mp3"}'`|upload via `POST /v1/files` first, pass returned URL
|ignoring moderation responses|silently dropping `error.code == "content_policy_violation"`|surface to user, don't retry blindly
|leaking key in URL|`?api-key=$KEY` query string|always use header (Authorization or x-api-key)
|cli arg with key|`./run.sh --key $KEY` (visible in `ps aux`)|env var only -- never as CLI flag
## Navigation Guide
|need to...|read this
|---------|---------
|Pick a model with cost/context|[models.md](models.md)
|Understand OpenAI endpoint differences|[openai/README.md](openai/README.md)
|Understand Anthropic system-prompt and tool-use rules|[anthropic/README.md](anthropic/README.md)
|See Replicate model registry and polling rules|[replicate/README.md](replicate/README.md)
|See how all 3 keys are loaded|[lib/auth.sh](lib/auth.sh)
|Add a new credential|`snappy-settings` skill (canonical source)
## Quick Reference
### Minimal curl per provider (no scripts)
```bash
# OpenAI chat completions
curl https://api.openai.com/v1/chat/completions \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"Hello"}]}'
# OpenAI Responses API (gpt-5)
curl https://api.openai.com/v1/responses \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"gpt-5","input":"Hello","reasoning":{"effort":"low"}}'
# Anthropic messages
curl https://api.anthropic.com/v1/messages \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "Content-Type: application/json" \
-d '{"model":"claude-sonnet-4-6","max_tokens":1024,"system":"You are concise.","messages":[{"role":"user","content":"Hello"}]}'
# Replicate (start)
curl https://api.replicate.com/v1/models/black-forest-labs/flux-schnell/predictions \
-H "Authorization: Bearer $REPLICATE_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"input":{"prompt":"a cat in space"}}'
# Replicate (poll -- repeat until status is succeeded/failed)
curl https://api.replicate.com/v1/predictions/PREDICTION_ID \
-H "Authorization: Bearer $REPLICATE_API_TOKEN"
```
### Loading credentials in your own script
```bash
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
. "${SCRIPT_DIR}/../lib/auth.sh"
require_openai # exits 1 with clear error if missing
# require_anthropic
# require_replicate
curl -sS https://api.openai.com/v1/chat/completions \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"hi"}]}'
```
## Related Skills
|skill|why
|-----|---
|`snappy-settings`|Source of `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `REPLICATE_API_TOKEN`. `lib/auth.sh` calls `snappy-settings/scripts/get-cred.sh` for each.
|`snappy-gemini`|Sibling -- same script-based pattern, but for Google Gemini text/image/video. Use that skill for Gemini.
|`snappy-openrouter`|Sibling -- unified routing across providers (DeepSeek, Qwen, Llama, Mistral, plus Anthropic/OpenAI/Google via proxy). Use OpenRouter when you don't need first-party features.
|`snappy-content`|Consumer -- uses `openai/chat.sh` and `anthropic/chat.sh` for writer/critique stages of interview-driven content production.
|`snappy-blog`|Consumer -- long-form blog generation, may use `claude-opus-4-6` for narrative passes.
|`snappy-image`|Consumer -- delegates to `openai/image.sh` (gpt-image-1) and `replicate/flux.sh` for image generation.
|`snappy-video`|Consumer -- delegates to `replicate/run.sh` for stable-video-diffusion / hunyuan-video.
|`snappy-post`|Consumer -- uses chat scripts for platform-variant copy generation.
|`snappy-transcripts`|Consumer -- uses `openai/audio.sh` and `replicate/whisper.sh` for STT.
|`snappy-knowledge`|Consumer -- uses `openai/embed.sh` for vector search index.
|`snappy-pipeline`|Consumer -- uses cheap models (`claude-haiku-4-5-20251001`, `gpt-4o-mini`) for batch enrichment passes.
|`snappy-ops`|Orchestrator -- triggers consumers above on the daily/weekly rhythm.
|`snappy-infra`|Sister infrastructure skill -- owns Xano API surface. snappy-ai-models owns external model providers. No overlap.
**Skill Status**: COMPLETE
<!-- SNAPPY-NEAR-NEIGHBOURS-START -->
## Near neighbours
These hands share enough of this one's words that a model can pick the wrong
door. Each row says what the other one is for; open that one instead when its
job is the job.
| Hand | What it is for |
|---|---|
| `snappy-agent-host` | Run the REAL Claude Code, Codex, and Gemini CLIs through ACP via the skills MCP, with durable per-folder se... |
| `snappy-dispatch` | The cheap-labor lever |
| `snappy-gemini` | Single canonical interface to Google's Gemini family for the Snappy system |
| `snappy-jcode` | Dispatch GPT 5.6 (Luna/Sol) agents as sandboxed lane workers via the local jcode CLI, on this Mac or the Ma... |
| `snappy-openrouter` | Single canonical interface to OpenRouter for the Snappy system |
| `snappy-settings` | Snappy Settings -- central environment and credentials layer for the entire Snappy operating system |
| `snappy-shell` | Kernel-loaded fallback runner |
| `snappy-telegram` | Telegram Bot API channel for Snappy: direct calls to api.telegram.org (no Xano middleware) to send text, ph... |
| `snappy-voice-control` | Voice control on macOS, extracted from two shipping open-source agents (fazm by mediar-ai; Agent! by Agenti... |
<!-- SNAPPY-NEAR-NEIGHBOURS-END -->
Direct calls to Anthropic Messages API for Claude models. Use this when you want first-party features (prompt caching, computer use, extended thinking) instead of routing through OpenRouter.
| Script | Endpoint | Purpose |
|---|---|---|
chat.sh |
/v1/messages |
One-shot text generation |
stream.sh |
/v1/messages (stream=true) |
Streaming text deltas via SSE |
tool-use.sh |
/v1/messages |
Single-call tool/function calling |
All scripts source ../lib/auth.sh, which loads ANTHROPIC_API_KEY via snappy-settings/scripts/load-env.sh (reads .env.cache). Override with an env var if needed.
|header|value|why
|------|-----|---
|x-api-key|$ANTHROPIC_API_KEY|API auth -- NOT Authorization: Bearer
|anthropic-version|2023-06-01|Required by every call. Missing this = 400 error.
|Content-Type|application/json|Standard
|anthropic-beta|(varies)|Only when using beta features (computer-use, prompt caching)
The most common mistake when integrating Anthropic -- agents copy the OpenAI pattern of putting {role: "system", content: "..."} inside the messages array. Anthropic rejects this.
diff- {
- "messages": [
- {"role": "system", "content": "You are a helpful CMO"}, <- WRONG
- {"role": "user", "content": "Plan a launch"}
- ]
- }
+ {
+ "system": "You are a helpful CMO", <- top-level field
+ "messages": [
+ {"role": "user", "content": "Plan a launch"}
+ ]
+ }
chat.sh, stream.sh, and tool-use.sh all handle this correctly when you pass --system "...".
Anthropic Messages API requires max_tokens in every call (unlike OpenAI which defaults). The scripts default to 4096 so you don't need to set it explicitly, but if you're hitting truncation, raise it: --max-tokens 8192.
|model_id|tier|context|use_case
|--------|----|-------|--------
|claude-opus-4-6|premium|200k|Hardest reasoning, long-form writing, creative
|claude-sonnet-4-6|standard|200k|Balanced default -- fast, capable, cheaper than Opus
|claude-haiku-4-5-20251001|cheap|200k|Classification, fast drafts, batch enrichment
tool-use.sh makes ONE call. The full loop is:
stop_reason: "tool_use" and tool_use blockstool_result blocks keyed by tool_use_idExample follow-up message structure:
json{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "toolu_01ABC...",
"content": "{\"temperature\": 72, \"conditions\": \"sunny\"}"
}
]
}
Use tool-use.sh --json to get the raw response, then build the follow-up call with curl directly.
Anthropic supports cache_control breakpoints in messages. To use:
json{
"messages": [
{
"role": "user",
"content": [
{"type": "text", "text": "<huge document>", "cache_control": {"type": "ephemeral"}},
{"type": "text", "text": "What does it say about X?"}
]
}
]
}
The default scripts don't expose this -- call the API directly with curl when you need it. See https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching.
bash# Quick chat
./chat.sh --model claude-haiku-4-5-20251001 --prompt "Classify: $(cat input.txt)"
# Long-form writing
./chat.sh --model claude-opus-4-6 --prompt "..." --system "You are a master essayist" --max-tokens 8192
# Live streaming for UX
./stream.sh --model claude-sonnet-4-6 --prompt "Explain joins"
# Function calling
./tool-use.sh --prompt "What's the weather in SF?" \
--tools '[{"name":"get_weather","description":"Get weather","input_schema":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"]}}]'
snappy-openroutersnappy-gemini../openai/snappy-openrouter with auto-routing# Anthropic Direct API -- Endpoint Reference
Direct calls to Anthropic Messages API for Claude models. Use this when you want first-party features (prompt caching, computer use, extended thinking) instead of routing through OpenRouter.
## Scripts
| Script | Endpoint | Purpose |
|--------|----------|---------|
| `chat.sh` | `/v1/messages` | One-shot text generation |
| `stream.sh` | `/v1/messages` (stream=true) | Streaming text deltas via SSE |
| `tool-use.sh` | `/v1/messages` | Single-call tool/function calling |
All scripts source `../lib/auth.sh`, which loads `ANTHROPIC_API_KEY` via `snappy-settings/scripts/load-env.sh` (reads `.env.cache`). Override with an env var if needed.
## Required headers (handled automatically)
|header|value|why
|------|-----|---
|`x-api-key`|`$ANTHROPIC_API_KEY`|API auth -- NOT `Authorization: Bearer`
|`anthropic-version`|`2023-06-01`|Required by every call. Missing this = 400 error.
|`Content-Type`|`application/json`|Standard
|`anthropic-beta`|(varies)|Only when using beta features (computer-use, prompt caching)
## CRITICAL: System Prompts Are Top-Level
**The most common mistake** when integrating Anthropic -- agents copy the OpenAI pattern of putting `{role: "system", content: "..."}` inside the messages array. **Anthropic rejects this.**
```diff
- {
- "messages": [
- {"role": "system", "content": "You are a helpful CMO"}, <- WRONG
- {"role": "user", "content": "Plan a launch"}
- ]
- }
+ {
+ "system": "You are a helpful CMO", <- top-level field
+ "messages": [
+ {"role": "user", "content": "Plan a launch"}
+ ]
+ }
```
`chat.sh`, `stream.sh`, and `tool-use.sh` all handle this correctly when you pass `--system "..."`.
## CRITICAL: max_tokens is required
Anthropic Messages API requires `max_tokens` in every call (unlike OpenAI which defaults). The scripts default to `4096` so you don't need to set it explicitly, but if you're hitting truncation, raise it: `--max-tokens 8192`.
## Model IDs (canonical, late 2025 / 2026)
|model_id|tier|context|use_case
|--------|----|-------|--------
|claude-opus-4-6|premium|200k|Hardest reasoning, long-form writing, creative
|claude-sonnet-4-6|standard|200k|Balanced default -- fast, capable, cheaper than Opus
|claude-haiku-4-5-20251001|cheap|200k|Classification, fast drafts, batch enrichment
## Tool Use Loop (Multi-Turn)
`tool-use.sh` makes ONE call. The full loop is:
1. Send messages + tools -> model returns `stop_reason: "tool_use"` and `tool_use` blocks
2. **Caller** executes the tool(s) locally
3. Send a follow-up call with the original messages PLUS:
- the assistant's response (containing the tool_use block)
- a new user message containing `tool_result` blocks keyed by tool_use_id
4. Model returns final text
Example follow-up message structure:
```json
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "toolu_01ABC...",
"content": "{\"temperature\": 72, \"conditions\": \"sunny\"}"
}
]
}
```
Use `tool-use.sh --json` to get the raw response, then build the follow-up call with curl directly.
## Prompt Caching (advanced)
Anthropic supports cache_control breakpoints in messages. To use:
```json
{
"messages": [
{
"role": "user",
"content": [
{"type": "text", "text": "<huge document>", "cache_control": {"type": "ephemeral"}},
{"type": "text", "text": "What does it say about X?"}
]
}
]
}
```
The default scripts don't expose this -- call the API directly with curl when you need it. See https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching.
## Common Patterns
```bash
# Quick chat
./chat.sh --model claude-haiku-4-5-20251001 --prompt "Classify: $(cat input.txt)"
# Long-form writing
./chat.sh --model claude-opus-4-6 --prompt "..." --system "You are a master essayist" --max-tokens 8192
# Live streaming for UX
./stream.sh --model claude-sonnet-4-6 --prompt "Explain joins"
# Function calling
./tool-use.sh --prompt "What's the weather in SF?" \
--tools '[{"name":"get_weather","description":"Get weather","input_schema":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"]}}]'
```
## When NOT To Use This
- For OpenAI/Gemini/DeepSeek/Llama -> use `snappy-openrouter`
- For Gemini directly -> use `snappy-gemini`
- For OpenAI directly -> use `../openai/`
- When Robert just wants "the best AI model right now" -> default to `snappy-openrouter` with auto-routing
#!/usr/bin/env bash
# snappy-ai-models/anthropic/chat.sh
#
# Direct call to Anthropic Messages API for Claude models.
#
# CRITICAL: Anthropic uses a TOP-LEVEL `system` field, NOT a system role
# inside the messages[] array (unlike OpenAI). Many AI agents get this wrong.
# This script handles it correctly.
#
# Usage:
# ./chat.sh --model claude-sonnet-4-6 --prompt "Summarize this"
# ./chat.sh --model claude-opus-4-6 --prompt "Plan a launch" --system "You are a CMO"
# ./chat.sh --model claude-haiku-4-5-20251001 --prompt "Classify intent" --json
# ./chat.sh --model claude-opus-4-6 --prompt "..." --max-tokens 4096 --temperature 0.7
# echo "stdin works too" | ./chat.sh --model claude-haiku-4-5-20251001
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=../lib/auth.sh
. "${SCRIPT_DIR}/../lib/auth.sh"
MODEL="claude-sonnet-4-6"
PROMPT=""
SYSTEM=""
TEMPERATURE=""
MAX_TOKENS="4096"
RAW_JSON=0
usage() {
cat <<'EOF'
Usage: anthropic/chat.sh [options]
Options:
--model <id> Anthropic model id (default: claude-sonnet-4-6)
--prompt <text> User prompt (or pipe via stdin)
--system <text> System prompt (top-level system field -- NOT a message)
--temperature <0..1> Sampling temperature
--max-tokens <n> Max output tokens (default 4096; required by API)
--json Return full JSON response (default: extracted text)
-h, --help Show this help
Common models:
claude-opus-4-6 Flagship reasoning, slowest, costliest
claude-sonnet-4-6 Balanced default, fast and capable (default)
claude-haiku-4-5-20251001 Cheap and fast, good for classification
See models.md for the full catalog.
CRITICAL: Anthropic puts system prompts in a top-level "system" field,
NOT as {role: "system"} inside messages[]. This script handles it correctly.
EOF
}
if [[ $# -eq 0 && -t 0 ]]; then
usage
exit 1
fi
while [[ $# -gt 0 ]]; do
case "$1" in
--model) MODEL="$2"; shift 2 ;;
--prompt) PROMPT="$2"; shift 2 ;;
--system) SYSTEM="$2"; shift 2 ;;
--temperature) TEMPERATURE="$2"; shift 2 ;;
--max-tokens) MAX_TOKENS="$2"; shift 2 ;;
--json) RAW_JSON=1; shift ;;
-h|--help) usage; exit 0 ;;
*) echo "anthropic/chat: unknown flag: $1" >&2; usage; exit 1 ;;
esac
done
if [[ -z "${PROMPT}" && ! -t 0 ]]; then
PROMPT="$(cat)"
fi
if [[ -z "${PROMPT}" ]]; then
echo "anthropic/chat: --prompt (or stdin) is required" >&2
exit 1
fi
require_anthropic
# Build the messages array -- user messages only.
# System prompts go in a TOP-LEVEL `system` field, NOT inside messages.
BODY="$(jq -n \
--arg model "${MODEL}" \
--argjson max_tokens "${MAX_TOKENS}" \
--arg prompt "${PROMPT}" \
'{model: $model, max_tokens: $max_tokens, messages: [{role: "user", content: $prompt}]}')"
if [[ -n "${SYSTEM}" ]]; then
BODY="$(jq --arg s "${SYSTEM}" '. + {system: $s}' <<<"${BODY}")"
fi
if [[ -n "${TEMPERATURE}" ]]; then
BODY="$(jq --argjson t "${TEMPERATURE}" '. + {temperature: $t}' <<<"${BODY}")"
fi
URL="${ANTHROPIC_API_BASE}/messages"
RESP="$(curl -sS -X POST "${URL}" \
-H "x-api-key: ${ANTHROPIC_API_KEY}" \
-H "anthropic-version: ${ANTHROPIC_VERSION}" \
-H "Content-Type: application/json" \
--data "${BODY}")"
if jq -e '.error' >/dev/null 2>&1 <<<"${RESP}"; then
echo "anthropic/chat: API error" >&2
jq '.error' >&2 <<<"${RESP}"
exit 2
fi
if [[ "${RAW_JSON}" -eq 1 ]]; then
printf '%s\n' "${RESP}"
else
# Anthropic returns content[] with type=text blocks
jq -r '.content[]? | select(.type == "text") | .text' <<<"${RESP}"
fi
#!/usr/bin/env bash
# snappy-ai-models/anthropic/chat.sh
#
# Direct call to Anthropic Messages API for Claude models.
#
# CRITICAL: Anthropic uses a TOP-LEVEL `system` field, NOT a system role
# inside the messages[] array (unlike OpenAI). Many AI agents get this wrong.
# This script handles it correctly.
#
# Usage:
# ./chat.sh --model claude-sonnet-4-6 --prompt "Summarize this"
# ./chat.sh --model claude-opus-4-6 --prompt "Plan a launch" --system "You are a CMO"
# ./chat.sh --model claude-haiku-4-5-20251001 --prompt "Classify intent" --json
# ./chat.sh --model claude-opus-4-6 --prompt "..." --max-tokens 4096 --temperature 0.7
# echo "stdin works too" | ./chat.sh --model claude-haiku-4-5-20251001
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=../lib/auth.sh
. "${SCRIPT_DIR}/../lib/auth.sh"
MODEL="claude-sonnet-4-6"
PROMPT=""
SYSTEM=""
TEMPERATURE=""
MAX_TOKENS="4096"
RAW_JSON=0
usage() {
cat <<'EOF'
Usage: anthropic/chat.sh [options]
Options:
--model <id> Anthropic model id (default: claude-sonnet-4-6)
--prompt <text> User prompt (or pipe via stdin)
--system <text> System prompt (top-level system field -- NOT a message)
--temperature <0..1> Sampling temperature
--max-tokens <n> Max output tokens (default 4096; required by API)
--json Return full JSON response (default: extracted text)
-h, --help Show this help
Common models:
claude-opus-4-6 Flagship reasoning, slowest, costliest
claude-sonnet-4-6 Balanced default, fast and capable (default)
claude-haiku-4-5-20251001 Cheap and fast, good for classification
See models.md for the full catalog.
CRITICAL: Anthropic puts system prompts in a top-level "system" field,
NOT as {role: "system"} inside messages[]. This script handles it correctly.
EOF
}
if [[ $# -eq 0 && -t 0 ]]; then
usage
exit 1
fi
while [[ $# -gt 0 ]]; do
case "$1" in
--model) MODEL="$2"; shift 2 ;;
--prompt) PROMPT="$2"; shift 2 ;;
--system) SYSTEM="$2"; shift 2 ;;
--temperature) TEMPERATURE="$2"; shift 2 ;;
--max-tokens) MAX_TOKENS="$2"; shift 2 ;;
--json) RAW_JSON=1; shift ;;
-h|--help) usage; exit 0 ;;
*) echo "anthropic/chat: unknown flag: $1" >&2; usage; exit 1 ;;
esac
done
if [[ -z "${PROMPT}" && ! -t 0 ]]; then
PROMPT="$(cat)"
fi
if [[ -z "${PROMPT}" ]]; then
echo "anthropic/chat: --prompt (or stdin) is required" >&2
exit 1
fi
require_anthropic
# Build the messages array -- user messages only.
# System prompts go in a TOP-LEVEL `system` field, NOT inside messages.
BODY="$(jq -n \
--arg model "${MODEL}" \
--argjson max_tokens "${MAX_TOKENS}" \
--arg prompt "${PROMPT}" \
'{model: $model, max_tokens: $max_tokens, messages: [{role: "user", content: $prompt}]}')"
if [[ -n "${SYSTEM}" ]]; then
BODY="$(jq --arg s "${SYSTEM}" '. + {system: $s}' <<<"${BODY}")"
fi
if [[ -n "${TEMPERATURE}" ]]; then
BODY="$(jq --argjson t "${TEMPERATURE}" '. + {temperature: $t}' <<<"${BODY}")"
fi
URL="${ANTHROPIC_API_BASE}/messages"
RESP="$(curl -sS -X POST "${URL}" \
-H "x-api-key: ${ANTHROPIC_API_KEY}" \
-H "anthropic-version: ${ANTHROPIC_VERSION}" \
-H "Content-Type: application/json" \
--data "${BODY}")"
if jq -e '.error' >/dev/null 2>&1 <<<"${RESP}"; then
echo "anthropic/chat: API error" >&2
jq '.error' >&2 <<<"${RESP}"
exit 2
fi
if [[ "${RAW_JSON}" -eq 1 ]]; then
printf '%s\n' "${RESP}"
else
# Anthropic returns content[] with type=text blocks
jq -r '.content[]? | select(.type == "text") | .text' <<<"${RESP}"
fi
#!/usr/bin/env bash
# snappy-ai-models/anthropic/stream.sh
#
# Streaming Anthropic Messages API call. Outputs text deltas to stdout
# as they arrive. SSE event format is parsed live.
#
# Usage:
# ./stream.sh --model claude-sonnet-4-6 --prompt "Write a haiku about Xano"
# ./stream.sh --model claude-opus-4-6 --prompt "Long story" --system "You are a novelist"
# ./stream.sh --model claude-haiku-4-5-20251001 --prompt "..." --raw # show full SSE
#
# Default mode extracts only `content_block_delta` text deltas and prints
# them character-by-character so you see live tokens. Use --raw to see the
# full SSE event stream.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=../lib/auth.sh
. "${SCRIPT_DIR}/../lib/auth.sh"
MODEL="claude-sonnet-4-6"
PROMPT=""
SYSTEM=""
TEMPERATURE=""
MAX_TOKENS="4096"
RAW=0
usage() {
cat <<'EOF'
Usage: anthropic/stream.sh [options]
Options:
--model <id> Anthropic model id (default: claude-sonnet-4-6)
--prompt <text> User prompt (or pipe via stdin)
--system <text> System prompt (top-level field)
--temperature <0..1> Sampling temperature
--max-tokens <n> Max output tokens (default 4096)
--raw Show full SSE event stream (default: text deltas only)
-h, --help Show this help
EOF
}
if [[ $# -eq 0 && -t 0 ]]; then
usage
exit 1
fi
while [[ $# -gt 0 ]]; do
case "$1" in
--model) MODEL="$2"; shift 2 ;;
--prompt) PROMPT="$2"; shift 2 ;;
--system) SYSTEM="$2"; shift 2 ;;
--temperature) TEMPERATURE="$2"; shift 2 ;;
--max-tokens) MAX_TOKENS="$2"; shift 2 ;;
--raw) RAW=1; shift ;;
-h|--help) usage; exit 0 ;;
*) echo "anthropic/stream: unknown flag: $1" >&2; usage; exit 1 ;;
esac
done
if [[ -z "${PROMPT}" && ! -t 0 ]]; then
PROMPT="$(cat)"
fi
if [[ -z "${PROMPT}" ]]; then
echo "anthropic/stream: --prompt (or stdin) is required" >&2
exit 1
fi
require_anthropic
BODY="$(jq -n \
--arg model "${MODEL}" \
--argjson max_tokens "${MAX_TOKENS}" \
--arg prompt "${PROMPT}" \
'{model: $model, max_tokens: $max_tokens, stream: true, messages: [{role: "user", content: $prompt}]}')"
if [[ -n "${SYSTEM}" ]]; then
BODY="$(jq --arg s "${SYSTEM}" '. + {system: $s}' <<<"${BODY}")"
fi
if [[ -n "${TEMPERATURE}" ]]; then
BODY="$(jq --argjson t "${TEMPERATURE}" '. + {temperature: $t}' <<<"${BODY}")"
fi
URL="${ANTHROPIC_API_BASE}/messages"
if [[ "${RAW}" -eq 1 ]]; then
# Pass through the raw SSE stream
curl -sS -N -X POST "${URL}" \
-H "x-api-key: ${ANTHROPIC_API_KEY}" \
-H "anthropic-version: ${ANTHROPIC_VERSION}" \
-H "Content-Type: application/json" \
--data "${BODY}"
exit 0
fi
# Default: stream text deltas to stdout as they arrive.
# Anthropic SSE events look like:
# event: content_block_delta
# data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello"}}
#
# We strip the "data: " prefix and extract delta.text from text_delta events.
curl -sS -N -X POST "${URL}" \
-H "x-api-key: ${ANTHROPIC_API_KEY}" \
-H "anthropic-version: ${ANTHROPIC_VERSION}" \
-H "Content-Type: application/json" \
--data "${BODY}" \
| while IFS= read -r line; do
# Skip blank lines and `event:` lines, only care about `data: {...}`
[[ "${line}" == data:* ]] || continue
payload="${line#data: }"
# Skip pings
[[ "${payload}" == "[DONE]" || -z "${payload}" ]] && continue
# Surface API errors loudly
if jq -e '.type == "error"' >/dev/null 2>&1 <<<"${payload}"; then
echo "" >&2
echo "anthropic/stream: API error" >&2
jq '.error // .' >&2 <<<"${payload}"
exit 2
fi
# Extract text deltas
delta="$(jq -r 'select(.type == "content_block_delta") | .delta.text // empty' <<<"${payload}" 2>/dev/null || true)"
if [[ -n "${delta}" ]]; then
printf '%s' "${delta}"
fi
done
echo
#!/usr/bin/env bash
# snappy-ai-models/anthropic/stream.sh
#
# Streaming Anthropic Messages API call. Outputs text deltas to stdout
# as they arrive. SSE event format is parsed live.
#
# Usage:
# ./stream.sh --model claude-sonnet-4-6 --prompt "Write a haiku about Xano"
# ./stream.sh --model claude-opus-4-6 --prompt "Long story" --system "You are a novelist"
# ./stream.sh --model claude-haiku-4-5-20251001 --prompt "..." --raw # show full SSE
#
# Default mode extracts only `content_block_delta` text deltas and prints
# them character-by-character so you see live tokens. Use --raw to see the
# full SSE event stream.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=../lib/auth.sh
. "${SCRIPT_DIR}/../lib/auth.sh"
MODEL="claude-sonnet-4-6"
PROMPT=""
SYSTEM=""
TEMPERATURE=""
MAX_TOKENS="4096"
RAW=0
usage() {
cat <<'EOF'
Usage: anthropic/stream.sh [options]
Options:
--model <id> Anthropic model id (default: claude-sonnet-4-6)
--prompt <text> User prompt (or pipe via stdin)
--system <text> System prompt (top-level field)
--temperature <0..1> Sampling temperature
--max-tokens <n> Max output tokens (default 4096)
--raw Show full SSE event stream (default: text deltas only)
-h, --help Show this help
EOF
}
if [[ $# -eq 0 && -t 0 ]]; then
usage
exit 1
fi
while [[ $# -gt 0 ]]; do
case "$1" in
--model) MODEL="$2"; shift 2 ;;
--prompt) PROMPT="$2"; shift 2 ;;
--system) SYSTEM="$2"; shift 2 ;;
--temperature) TEMPERATURE="$2"; shift 2 ;;
--max-tokens) MAX_TOKENS="$2"; shift 2 ;;
--raw) RAW=1; shift ;;
-h|--help) usage; exit 0 ;;
*) echo "anthropic/stream: unknown flag: $1" >&2; usage; exit 1 ;;
esac
done
if [[ -z "${PROMPT}" && ! -t 0 ]]; then
PROMPT="$(cat)"
fi
if [[ -z "${PROMPT}" ]]; then
echo "anthropic/stream: --prompt (or stdin) is required" >&2
exit 1
fi
require_anthropic
BODY="$(jq -n \
--arg model "${MODEL}" \
--argjson max_tokens "${MAX_TOKENS}" \
--arg prompt "${PROMPT}" \
'{model: $model, max_tokens: $max_tokens, stream: true, messages: [{role: "user", content: $prompt}]}')"
if [[ -n "${SYSTEM}" ]]; then
BODY="$(jq --arg s "${SYSTEM}" '. + {system: $s}' <<<"${BODY}")"
fi
if [[ -n "${TEMPERATURE}" ]]; then
BODY="$(jq --argjson t "${TEMPERATURE}" '. + {temperature: $t}' <<<"${BODY}")"
fi
URL="${ANTHROPIC_API_BASE}/messages"
if [[ "${RAW}" -eq 1 ]]; then
# Pass through the raw SSE stream
curl -sS -N -X POST "${URL}" \
-H "x-api-key: ${ANTHROPIC_API_KEY}" \
-H "anthropic-version: ${ANTHROPIC_VERSION}" \
-H "Content-Type: application/json" \
--data "${BODY}"
exit 0
fi
# Default: stream text deltas to stdout as they arrive.
# Anthropic SSE events look like:
# event: content_block_delta
# data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello"}}
#
# We strip the "data: " prefix and extract delta.text from text_delta events.
curl -sS -N -X POST "${URL}" \
-H "x-api-key: ${ANTHROPIC_API_KEY}" \
-H "anthropic-version: ${ANTHROPIC_VERSION}" \
-H "Content-Type: application/json" \
--data "${BODY}" \
| while IFS= read -r line; do
# Skip blank lines and `event:` lines, only care about `data: {...}`
[[ "${line}" == data:* ]] || continue
payload="${line#data: }"
# Skip pings
[[ "${payload}" == "[DONE]" || -z "${payload}" ]] && continue
# Surface API errors loudly
if jq -e '.type == "error"' >/dev/null 2>&1 <<<"${payload}"; then
echo "" >&2
echo "anthropic/stream: API error" >&2
jq '.error // .' >&2 <<<"${payload}"
exit 2
fi
# Extract text deltas
delta="$(jq -r 'select(.type == "content_block_delta") | .delta.text // empty' <<<"${payload}" 2>/dev/null || true)"
if [[ -n "${delta}" ]]; then
printf '%s' "${delta}"
fi
done
echo
#!/usr/bin/env bash
# snappy-ai-models/anthropic/tool-use.sh
#
# Single-shot tool/function-calling against Anthropic Messages API.
# Pass a JSON array of tool definitions via --tools (or --tools-file)
# and a prompt; the script returns the model's response. If the model
# decides to use a tool it will return a stop_reason of "tool_use" and
# you can parse the tool_use blocks from the content.
#
# This script does NOT execute tools -- that's the caller's job. Tool
# results should be sent in a follow-up call (multi-turn) which is left
# to the caller (use --json to get the full response, including tool_use
# blocks, then construct a follow-up messages[] with tool_result blocks).
#
# Usage:
# ./tool-use.sh --prompt "What is the weather in SF?" \
# --tools '[{"name":"get_weather","description":"Get current weather","input_schema":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"]}}]'
#
# ./tool-use.sh --model claude-opus-4-6 \
# --prompt "Find the user named Robert" \
# --tools-file ./crm-tools.json \
# --json
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=../lib/auth.sh
. "${SCRIPT_DIR}/../lib/auth.sh"
MODEL="claude-sonnet-4-6"
PROMPT=""
SYSTEM=""
TOOLS=""
TOOLS_FILE=""
TOOL_CHOICE="" # auto | any | tool (with name) | none
MAX_TOKENS="4096"
RAW_JSON=0
usage() {
cat <<'EOF'
Usage: anthropic/tool-use.sh [options]
Options:
--model <id> Anthropic model id (default: claude-sonnet-4-6)
--prompt <text> User prompt (or pipe via stdin)
--system <text> System prompt
--tools <json> JSON array of tool definitions (inline)
--tools-file <path> JSON file containing tool definitions
--tool-choice <c> auto (default) | any | none | <tool_name>
--max-tokens <n> Max output tokens (default 4096)
--json Always print full JSON response (default: pretty-print decisions)
-h, --help Show this help
Tool definition format (one element per tool):
{
"name": "get_weather",
"description": "Returns the current weather for a city.",
"input_schema": {
"type": "object",
"properties": {
"city": { "type": "string" }
},
"required": ["city"]
}
}
Note: this script makes a single call. To handle the full tool-use loop
(execute tool, send tool_result back, get final answer), use --json and
build the follow-up call yourself.
EOF
}
if [[ $# -eq 0 ]]; then
usage
exit 1
fi
while [[ $# -gt 0 ]]; do
case "$1" in
--model) MODEL="$2"; shift 2 ;;
--prompt) PROMPT="$2"; shift 2 ;;
--system) SYSTEM="$2"; shift 2 ;;
--tools) TOOLS="$2"; shift 2 ;;
--tools-file) TOOLS_FILE="$2"; shift 2 ;;
--tool-choice) TOOL_CHOICE="$2"; shift 2 ;;
--max-tokens) MAX_TOKENS="$2"; shift 2 ;;
--json) RAW_JSON=1; shift ;;
-h|--help) usage; exit 0 ;;
*) echo "anthropic/tool-use: unknown flag: $1" >&2; usage; exit 1 ;;
esac
done
if [[ -z "${PROMPT}" && ! -t 0 ]]; then
PROMPT="$(cat)"
fi
if [[ -z "${PROMPT}" ]]; then
echo "anthropic/tool-use: --prompt is required" >&2
exit 1
fi
if [[ -z "${TOOLS}" && -z "${TOOLS_FILE}" ]]; then
echo "anthropic/tool-use: --tools or --tools-file is required" >&2
exit 1
fi
if [[ -n "${TOOLS_FILE}" ]]; then
if [[ ! -f "${TOOLS_FILE}" ]]; then
echo "anthropic/tool-use: tools file not found: ${TOOLS_FILE}" >&2
exit 1
fi
TOOLS="$(cat "${TOOLS_FILE}")"
fi
# Validate tools is a JSON array
if ! jq -e 'type == "array"' >/dev/null 2>&1 <<<"${TOOLS}"; then
echo "anthropic/tool-use: --tools must be a JSON array" >&2
exit 1
fi
require_anthropic
BODY="$(jq -n \
--arg model "${MODEL}" \
--argjson max_tokens "${MAX_TOKENS}" \
--arg prompt "${PROMPT}" \
--argjson tools "${TOOLS}" \
'{model: $model, max_tokens: $max_tokens, tools: $tools, messages: [{role: "user", content: $prompt}]}')"
if [[ -n "${SYSTEM}" ]]; then
BODY="$(jq --arg s "${SYSTEM}" '. + {system: $s}' <<<"${BODY}")"
fi
if [[ -n "${TOOL_CHOICE}" ]]; then
case "${TOOL_CHOICE}" in
auto|any|none)
BODY="$(jq --arg c "${TOOL_CHOICE}" '. + {tool_choice: {type: $c}}' <<<"${BODY}")"
;;
*)
# treat as tool name
BODY="$(jq --arg c "${TOOL_CHOICE}" '. + {tool_choice: {type: "tool", name: $c}}' <<<"${BODY}")"
;;
esac
fi
URL="${ANTHROPIC_API_BASE}/messages"
RESP="$(curl -sS -X POST "${URL}" \
-H "x-api-key: ${ANTHROPIC_API_KEY}" \
-H "anthropic-version: ${ANTHROPIC_VERSION}" \
-H "Content-Type: application/json" \
--data "${BODY}")"
if jq -e '.error' >/dev/null 2>&1 <<<"${RESP}"; then
echo "anthropic/tool-use: API error" >&2
jq '.error' >&2 <<<"${RESP}"
exit 2
fi
if [[ "${RAW_JSON}" -eq 1 ]]; then
printf '%s\n' "${RESP}"
else
STOP_REASON="$(jq -r '.stop_reason // "unknown"' <<<"${RESP}")"
echo "stop_reason: ${STOP_REASON}"
echo "---"
# Print text blocks
jq -r '.content[]? | select(.type == "text") | .text' <<<"${RESP}"
# Print tool_use blocks
TOOL_USES="$(jq -c '.content[]? | select(.type == "tool_use")' <<<"${RESP}")"
if [[ -n "${TOOL_USES}" ]]; then
echo "---"
echo "tool_use blocks:"
echo "${TOOL_USES}" | jq .
fi
fi
#!/usr/bin/env bash
# snappy-ai-models/anthropic/tool-use.sh
#
# Single-shot tool/function-calling against Anthropic Messages API.
# Pass a JSON array of tool definitions via --tools (or --tools-file)
# and a prompt; the script returns the model's response. If the model
# decides to use a tool it will return a stop_reason of "tool_use" and
# you can parse the tool_use blocks from the content.
#
# This script does NOT execute tools -- that's the caller's job. Tool
# results should be sent in a follow-up call (multi-turn) which is left
# to the caller (use --json to get the full response, including tool_use
# blocks, then construct a follow-up messages[] with tool_result blocks).
#
# Usage:
# ./tool-use.sh --prompt "What is the weather in SF?" \
# --tools '[{"name":"get_weather","description":"Get current weather","input_schema":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"]}}]'
#
# ./tool-use.sh --model claude-opus-4-6 \
# --prompt "Find the user named Robert" \
# --tools-file ./crm-tools.json \
# --json
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=../lib/auth.sh
. "${SCRIPT_DIR}/../lib/auth.sh"
MODEL="claude-sonnet-4-6"
PROMPT=""
SYSTEM=""
TOOLS=""
TOOLS_FILE=""
TOOL_CHOICE="" # auto | any | tool (with name) | none
MAX_TOKENS="4096"
RAW_JSON=0
usage() {
cat <<'EOF'
Usage: anthropic/tool-use.sh [options]
Options:
--model <id> Anthropic model id (default: claude-sonnet-4-6)
--prompt <text> User prompt (or pipe via stdin)
--system <text> System prompt
--tools <json> JSON array of tool definitions (inline)
--tools-file <path> JSON file containing tool definitions
--tool-choice <c> auto (default) | any | none | <tool_name>
--max-tokens <n> Max output tokens (default 4096)
--json Always print full JSON response (default: pretty-print decisions)
-h, --help Show this help
Tool definition format (one element per tool):
{
"name": "get_weather",
"description": "Returns the current weather for a city.",
"input_schema": {
"type": "object",
"properties": {
"city": { "type": "string" }
},
"required": ["city"]
}
}
Note: this script makes a single call. To handle the full tool-use loop
(execute tool, send tool_result back, get final answer), use --json and
build the follow-up call yourself.
EOF
}
if [[ $# -eq 0 ]]; then
usage
exit 1
fi
while [[ $# -gt 0 ]]; do
case "$1" in
--model) MODEL="$2"; shift 2 ;;
--prompt) PROMPT="$2"; shift 2 ;;
--system) SYSTEM="$2"; shift 2 ;;
--tools) TOOLS="$2"; shift 2 ;;
--tools-file) TOOLS_FILE="$2"; shift 2 ;;
--tool-choice) TOOL_CHOICE="$2"; shift 2 ;;
--max-tokens) MAX_TOKENS="$2"; shift 2 ;;
--json) RAW_JSON=1; shift ;;
-h|--help) usage; exit 0 ;;
*) echo "anthropic/tool-use: unknown flag: $1" >&2; usage; exit 1 ;;
esac
done
if [[ -z "${PROMPT}" && ! -t 0 ]]; then
PROMPT="$(cat)"
fi
if [[ -z "${PROMPT}" ]]; then
echo "anthropic/tool-use: --prompt is required" >&2
exit 1
fi
if [[ -z "${TOOLS}" && -z "${TOOLS_FILE}" ]]; then
echo "anthropic/tool-use: --tools or --tools-file is required" >&2
exit 1
fi
if [[ -n "${TOOLS_FILE}" ]]; then
if [[ ! -f "${TOOLS_FILE}" ]]; then
echo "anthropic/tool-use: tools file not found: ${TOOLS_FILE}" >&2
exit 1
fi
TOOLS="$(cat "${TOOLS_FILE}")"
fi
# Validate tools is a JSON array
if ! jq -e 'type == "array"' >/dev/null 2>&1 <<<"${TOOLS}"; then
echo "anthropic/tool-use: --tools must be a JSON array" >&2
exit 1
fi
require_anthropic
BODY="$(jq -n \
--arg model "${MODEL}" \
--argjson max_tokens "${MAX_TOKENS}" \
--arg prompt "${PROMPT}" \
--argjson tools "${TOOLS}" \
'{model: $model, max_tokens: $max_tokens, tools: $tools, messages: [{role: "user", content: $prompt}]}')"
if [[ -n "${SYSTEM}" ]]; then
BODY="$(jq --arg s "${SYSTEM}" '. + {system: $s}' <<<"${BODY}")"
fi
if [[ -n "${TOOL_CHOICE}" ]]; then
case "${TOOL_CHOICE}" in
auto|any|none)
BODY="$(jq --arg c "${TOOL_CHOICE}" '. + {tool_choice: {type: $c}}' <<<"${BODY}")"
;;
*)
# treat as tool name
BODY="$(jq --arg c "${TOOL_CHOICE}" '. + {tool_choice: {type: "tool", name: $c}}' <<<"${BODY}")"
;;
esac
fi
URL="${ANTHROPIC_API_BASE}/messages"
RESP="$(curl -sS -X POST "${URL}" \
-H "x-api-key: ${ANTHROPIC_API_KEY}" \
-H "anthropic-version: ${ANTHROPIC_VERSION}" \
-H "Content-Type: application/json" \
--data "${BODY}")"
if jq -e '.error' >/dev/null 2>&1 <<<"${RESP}"; then
echo "anthropic/tool-use: API error" >&2
jq '.error' >&2 <<<"${RESP}"
exit 2
fi
if [[ "${RAW_JSON}" -eq 1 ]]; then
printf '%s\n' "${RESP}"
else
STOP_REASON="$(jq -r '.stop_reason // "unknown"' <<<"${RESP}")"
echo "stop_reason: ${STOP_REASON}"
echo "---"
# Print text blocks
jq -r '.content[]? | select(.type == "text") | .text' <<<"${RESP}"
# Print tool_use blocks
TOOL_USES="$(jq -c '.content[]? | select(.type == "tool_use")' <<<"${RESP}")"
if [[ -n "${TOOL_USES}" ]]; then
echo "---"
echo "tool_use blocks:"
echo "${TOOL_USES}" | jq .
fi
fi
#!/usr/bin/env npx tsx
/**
* snappy-ai-models/api.ts -- Direct OpenAI API for all snappy-* skills.
*
* Uses OPENAI_API_KEY from snappy-settings/.env.cache.
* Direct OpenAI REST API -- chat completions, embeddings, image generation.
*
* Usage:
* npx tsx api.ts chat "Explain quantum tunneling"
* npx tsx api.ts chat "Summarize this" --model gpt-4o
* npx tsx api.ts embed "What is Xano?"
*
* Or import as module:
* import { chatCompletion, embed, generateOpenAIImage } from "../snappy-ai-models/api.ts";
*/
import { env } from "../snappy-settings/load.ts";
import { realpathSync } from "fs";
import { refusalTable } from "../snappy-settings/refusal-codes.ts";
const BASE = "https://api.openai.com/v1";
const DEFAULT_MODEL = "gpt-4o-mini";
interface ChatOptions {
model?: string;
systemPrompt?: string;
temperature?: number;
maxTokens?: number;
}
interface ImageOptions {
model?: string;
size?: string;
}
async function openai(path: string, body: Record<string, unknown>): Promise<unknown> {
const res = await fetch(`${BASE}${path}`, {
method: "POST",
headers: {
Authorization: `Bearer ${env("OPENAI_API_KEY")}`,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
if (!res.ok) {
const err = await res.text();
throw new Error(`OpenAI failed (${res.status}): ${err}`);
}
return res.json();
}
// --- Public API ---
export async function chatCompletion(prompt: string, opts: ChatOptions = {}): Promise<{ text: string; raw: unknown }> {
const model = opts.model || DEFAULT_MODEL;
const messages: { role: string; content: string }[] = [];
if (opts.systemPrompt) messages.push({ role: "system", content: opts.systemPrompt });
messages.push({ role: "user", content: prompt });
const data = await openai("/chat/completions", {
model,
messages,
...(opts.temperature != null ? { temperature: opts.temperature } : {}),
...(opts.maxTokens ? { max_tokens: opts.maxTokens } : {}),
}) as any;
const text = data.choices?.[0]?.message?.content || "";
return { text, raw: data };
}
export async function embed(text: string, model = "text-embedding-3-small"): Promise<{ vector: number[]; raw: unknown }> {
const data = await openai("/embeddings", {
model,
input: text,
}) as any;
const vector = data.data?.[0]?.embedding || [];
return { vector, raw: data };
}
export async function generateOpenAIImage(prompt: string, opts: ImageOptions = {}): Promise<{ url: string; raw: unknown }> {
const model = opts.model || "gpt-image-1";
const data = await openai("/images/generations", {
model,
prompt,
size: opts.size || "1024x1024",
n: 1,
}) as any;
const url = data.data?.[0]?.url || data.data?.[0]?.b64_json || "";
return { url, raw: data };
}
// --- CLI ---
/** WHAT THIS HAND ANSWERS, and what each verb does to the world.
* Derived from this file's own CLI dispatch by
* `snappy-hands/contract-derive.ts` — a verb the code does not implement is
* never declared here. Snappy's daemon reads it (`api.ts contract`) to
* validate every call, build the argument words in order, decide whether the
* act runs now or stages for the owner, and hand the child exactly the
* environment keys named in `requires` — never a value, never anything else.
*/
export const HAND_CONTRACT = {
skill: "snappy-ai-models",
description: "Direct-API interface to OpenAI, Anthropic, and Replicate for the Snappy system -- the three model providers that don't already have a dedicated skill (Gemini lives in snappy-gemini; routed/cheap multi-vendor calls live in snappy-openrouter). Provides curl-backed scripts for chat, image, audio, embeddings, tool-use, streaming, and Replicate predictions with transparent polling. Pairs with snappy-settings for credential loading. Triggers on: openai, anthropic, claude, gpt, gpt-5, gpt-4o, o1, o3, dall-e, gpt-image-1, gpt-image-2, whisper, tts, openai embeddings, text-embedding-3, anthropic claude, claude opus, claude sonnet, claude haiku, claude api, messages api, tool use, function calling, replicate, flux, flux schnell, flux pro, sdxl, stable diffusion, llava, whisperx, ai model, model catalog, model id, openai api key, anthropic api key, replicate token, prediction polling, x-api-key, anthropic-version header, responses api, chat completions, prompt caching, system prompt anthropic, direct api, not openrouter, not gemini.",
managed: true,
requires: ["OPENAI_API_KEY"] as string[],
refusals: refusalTable("unknown_verb", "missing_argument", "missing_credential", "upstream_error"),
verbs: {
chat: {
args: ["prompt","model"], effect: "draft", flags: {"model":"--model"},
class: "additive-write", execution: "call", openWorld: true,
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
inputSchema: { properties: { prompt: { type: "string", description: "The text sent to the model" }, model: { type: "string", description: "Model id to answer with; omit for the skill's default" } } },
},
embed: {
args: ["text"], effect: "draft",
class: "additive-write", execution: "call", openWorld: true,
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
inputSchema: { properties: { text: { type: "string", description: "The text to embed as a vector" } } },
},
image: {
args: ["prompt"], effect: "draft", flags: {"size":"--size"},
class: "additive-write", execution: "call", openWorld: true,
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
inputSchema: { properties: { prompt: { type: "string", description: "What the generated image should show" } } },
},
},
} as const;
if (import.meta.url === `file://${realpathSync(process.argv[1])}` && process.argv[2] === "contract") {
console.log(JSON.stringify(HAND_CONTRACT, null, 2));
process.exit(0);
}
if (import.meta.url === `file://${realpathSync(process.argv[1])}`) {
(async () => {
const [, , cmd, ...args] = process.argv;
switch (cmd) {
case "chat": {
const prompt = args.filter(a => !a.startsWith("--")).join(" ");
const modelIdx = args.indexOf("--model");
const model = modelIdx >= 0 ? args[modelIdx + 1] : undefined;
if (!prompt) { console.error("Usage: api.ts chat <prompt> [--model <model>]"); process.exit(1); }
const { text } = await chatCompletion(prompt, { model });
console.log(text);
break;
}
case "embed": {
const text = args.join(" ");
if (!text) { console.error("Usage: api.ts embed <text>"); process.exit(1); }
const { vector } = await embed(text);
console.log(`Dimensions: ${vector.length}`);
console.log(JSON.stringify(vector.slice(0, 5)) + "...");
break;
}
case "image": {
const prompt = args.filter(a => !a.startsWith("--")).join(" ");
const sizeIdx = args.indexOf("--size");
const size = sizeIdx >= 0 ? args[sizeIdx + 1] : undefined;
if (!prompt) { console.error("Usage: api.ts image <prompt> [--size <WxH>]"); process.exit(1); }
const { url } = await generateOpenAIImage(prompt, { size });
console.log(url);
break;
}
default:
console.log("Usage: npx tsx api.ts [chat|embed|image] ...");
}
})();
}
#!/usr/bin/env npx tsx
/**
* snappy-ai-models/api.ts -- Direct OpenAI API for all snappy-* skills.
*
* Uses OPENAI_API_KEY from snappy-settings/.env.cache.
* Direct OpenAI REST API -- chat completions, embeddings, image generation.
*
* Usage:
* npx tsx api.ts chat "Explain quantum tunneling"
* npx tsx api.ts chat "Summarize this" --model gpt-4o
* npx tsx api.ts embed "What is Xano?"
*
* Or import as module:
* import { chatCompletion, embed, generateOpenAIImage } from "../snappy-ai-models/api.ts";
*/
import { env } from "../snappy-settings/load.ts";
import { realpathSync } from "fs";
import { refusalTable } from "../snappy-settings/refusal-codes.ts";
const BASE = "https://api.openai.com/v1";
const DEFAULT_MODEL = "gpt-4o-mini";
interface ChatOptions {
model?: string;
systemPrompt?: string;
temperature?: number;
maxTokens?: number;
}
interface ImageOptions {
model?: string;
size?: string;
}
async function openai(path: string, body: Record<string, unknown>): Promise<unknown> {
const res = await fetch(`${BASE}${path}`, {
method: "POST",
headers: {
Authorization: `Bearer ${env("OPENAI_API_KEY")}`,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
if (!res.ok) {
const err = await res.text();
throw new Error(`OpenAI failed (${res.status}): ${err}`);
}
return res.json();
}
// --- Public API ---
export async function chatCompletion(prompt: string, opts: ChatOptions = {}): Promise<{ text: string; raw: unknown }> {
const model = opts.model || DEFAULT_MODEL;
const messages: { role: string; content: string }[] = [];
if (opts.systemPrompt) messages.push({ role: "system", content: opts.systemPrompt });
messages.push({ role: "user", content: prompt });
const data = await openai("/chat/completions", {
model,
messages,
...(opts.temperature != null ? { temperature: opts.temperature } : {}),
...(opts.maxTokens ? { max_tokens: opts.maxTokens } : {}),
}) as any;
const text = data.choices?.[0]?.message?.content || "";
return { text, raw: data };
}
export async function embed(text: string, model = "text-embedding-3-small"): Promise<{ vector: number[]; raw: unknown }> {
const data = await openai("/embeddings", {
model,
input: text,
}) as any;
const vector = data.data?.[0]?.embedding || [];
return { vector, raw: data };
}
export async function generateOpenAIImage(prompt: string, opts: ImageOptions = {}): Promise<{ url: string; raw: unknown }> {
const model = opts.model || "gpt-image-1";
const data = await openai("/images/generations", {
model,
prompt,
size: opts.size || "1024x1024",
n: 1,
}) as any;
const url = data.data?.[0]?.url || data.data?.[0]?.b64_json || "";
return { url, raw: data };
}
// --- CLI ---
/** WHAT THIS HAND ANSWERS, and what each verb does to the world.
* Derived from this file's own CLI dispatch by
* `snappy-hands/contract-derive.ts` — a verb the code does not implement is
* never declared here. Snappy's daemon reads it (`api.ts contract`) to
* validate every call, build the argument words in order, decide whether the
* act runs now or stages for the owner, and hand the child exactly the
* environment keys named in `requires` — never a value, never anything else.
*/
export const HAND_CONTRACT = {
skill: "snappy-ai-models",
description: "Direct-API interface to OpenAI, Anthropic, and Replicate for the Snappy system -- the three model providers that don't already have a dedicated skill (Gemini lives in snappy-gemini; routed/cheap multi-vendor calls live in snappy-openrouter). Provides curl-backed scripts for chat, image, audio, embeddings, tool-use, streaming, and Replicate predictions with transparent polling. Pairs with snappy-settings for credential loading. Triggers on: openai, anthropic, claude, gpt, gpt-5, gpt-4o, o1, o3, dall-e, gpt-image-1, gpt-image-2, whisper, tts, openai embeddings, text-embedding-3, anthropic claude, claude opus, claude sonnet, claude haiku, claude api, messages api, tool use, function calling, replicate, flux, flux schnell, flux pro, sdxl, stable diffusion, llava, whisperx, ai model, model catalog, model id, openai api key, anthropic api key, replicate token, prediction polling, x-api-key, anthropic-version header, responses api, chat completions, prompt caching, system prompt anthropic, direct api, not openrouter, not gemini.",
managed: true,
requires: ["OPENAI_API_KEY"] as string[],
refusals: refusalTable("unknown_verb", "missing_argument", "missing_credential", "upstream_error"),
verbs: {
chat: {
args: ["prompt","model"], effect: "draft", flags: {"model":"--model"},
class: "additive-write", execution: "call", openWorld: true,
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
inputSchema: { properties: { prompt: { type: "string", description: "The text sent to the model" }, model: { type: "string", description: "Model id to answer with; omit for the skill's default" } } },
},
embed: {
args: ["text"], effect: "draft",
class: "additive-write", execution: "call", openWorld: true,
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
inputSchema: { properties: { text: { type: "string", description: "The text to embed as a vector" } } },
},
image: {
args: ["prompt"], effect: "draft", flags: {"size":"--size"},
class: "additive-write", execution: "call", openWorld: true,
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
inputSchema: { properties: { prompt: { type: "string", description: "What the generated image should show" } } },
},
},
} as const;
if (import.meta.url === `file://${realpathSync(process.argv[1])}` && process.argv[2] === "contract") {
console.log(JSON.stringify(HAND_CONTRACT, null, 2));
process.exit(0);
}
if (import.meta.url === `file://${realpathSync(process.argv[1])}`) {
(async () => {
const [, , cmd, ...args] = process.argv;
switch (cmd) {
case "chat": {
const prompt = args.filter(a => !a.startsWith("--")).join(" ");
const modelIdx = args.indexOf("--model");
const model = modelIdx >= 0 ? args[modelIdx + 1] : undefined;
if (!prompt) { console.error("Usage: api.ts chat <prompt> [--model <model>]"); process.exit(1); }
const { text } = await chatCompletion(prompt, { model });
console.log(text);
break;
}
case "embed": {
const text = args.join(" ");
if (!text) { console.error("Usage: api.ts embed <text>"); process.exit(1); }
const { vector } = await embed(text);
console.log(`Dimensions: ${vector.length}`);
console.log(JSON.stringify(vector.slice(0, 5)) + "...");
break;
}
case "image": {
const prompt = args.filter(a => !a.startsWith("--")).join(" ");
const sizeIdx = args.indexOf("--size");
const size = sizeIdx >= 0 ? args[sizeIdx + 1] : undefined;
if (!prompt) { console.error("Usage: api.ts image <prompt> [--size <WxH>]"); process.exit(1); }
const { url } = await generateOpenAIImage(prompt, { size });
console.log(url);
break;
}
default:
console.log("Usage: npx tsx api.ts [chat|embed|image] ...");
}
})();
}
import { test } from "node:test";
import assert from "node:assert/strict";
import { HAND_CONTRACT } from "./api.ts";
import { REFUSAL_CODES, type RefusalCode } from "../snappy-settings/refusal-codes.ts";
/** RULE 33 COVERAGE, AGAINST THE ONE CLOSED TABLE ⟨lane r30, 2026-09-09⟩.
* This file used to restate a refusal row's own properties — that it has a
* `contract_slice`, that it has a `fix`, that it leaks no token — once per
* hand, 48 times, over rows that all come from the SAME object. Forty-eight
* copies of one check is the duplicate road the closed table exists to end:
* `snappy-settings/refusal-codes.test.ts` runs those checks ONCE over every
* row, and the second test below proves this hand carries THE ROW ITSELF and
* not a copy — an identity a drifted duplicate cannot fake. Before this, each
* hand hand-wrote its own row, and the wording had already drifted: the
* inline `unknown_verb` said "Call one of the verbs named in
* HAND_CONTRACT.verbs" while the closed table says "Call one of the verbs the
* contract declares; the refusal lists them."
*
* DECLARED stays a literal list, deliberately. It is this hand's coverage
* manifest and it is what rule 33's lint reads out of the test SOURCE to ask
* "did a person look at this code" — deriving it from Object.keys would make
* the test pass for a hand with no refusals at all. `satisfies readonly
* RefusalCode[]` makes the compiler refuse a name the one table does not
* have. It NAMES codes; it no longer DEFINES them. */
const DECLARED = [
"unknown_verb",
"missing_argument",
"missing_credential",
"upstream_error",
] as const satisfies readonly RefusalCode[];
test("snappy-ai-models: the refusal table declares exactly the codes this test names", () => {
assert.deepEqual(Object.keys(HAND_CONTRACT.refusals ?? {}).sort(), [...DECLARED].sort());
});
test("snappy-ai-models: every declared refusal is the one closed table's own row, never a copy", () => {
const declared = Object.entries(HAND_CONTRACT.refusals ?? {});
assert.ok(declared.length > 0, "HAND_CONTRACT.refusals is empty");
for (const [code, row] of declared) {
assert.ok(code in REFUSAL_CODES, `${code} is not a row of snappy-settings/refusal-codes.ts`);
assert.equal(row, REFUSAL_CODES[code as RefusalCode], `${code} is a copy of the closed table's row, not the row itself`);
}
});
import { test } from "node:test";
import assert from "node:assert/strict";
import { HAND_CONTRACT } from "./api.ts";
import { REFUSAL_CODES, type RefusalCode } from "../snappy-settings/refusal-codes.ts";
/** RULE 33 COVERAGE, AGAINST THE ONE CLOSED TABLE ⟨lane r30, 2026-09-09⟩.
* This file used to restate a refusal row's own properties — that it has a
* `contract_slice`, that it has a `fix`, that it leaks no token — once per
* hand, 48 times, over rows that all come from the SAME object. Forty-eight
* copies of one check is the duplicate road the closed table exists to end:
* `snappy-settings/refusal-codes.test.ts` runs those checks ONCE over every
* row, and the second test below proves this hand carries THE ROW ITSELF and
* not a copy — an identity a drifted duplicate cannot fake. Before this, each
* hand hand-wrote its own row, and the wording had already drifted: the
* inline `unknown_verb` said "Call one of the verbs named in
* HAND_CONTRACT.verbs" while the closed table says "Call one of the verbs the
* contract declares; the refusal lists them."
*
* DECLARED stays a literal list, deliberately. It is this hand's coverage
* manifest and it is what rule 33's lint reads out of the test SOURCE to ask
* "did a person look at this code" — deriving it from Object.keys would make
* the test pass for a hand with no refusals at all. `satisfies readonly
* RefusalCode[]` makes the compiler refuse a name the one table does not
* have. It NAMES codes; it no longer DEFINES them. */
const DECLARED = [
"unknown_verb",
"missing_argument",
"missing_credential",
"upstream_error",
] as const satisfies readonly RefusalCode[];
test("snappy-ai-models: the refusal table declares exactly the codes this test names", () => {
assert.deepEqual(Object.keys(HAND_CONTRACT.refusals ?? {}).sort(), [...DECLARED].sort());
});
test("snappy-ai-models: every declared refusal is the one closed table's own row, never a copy", () => {
const declared = Object.entries(HAND_CONTRACT.refusals ?? {});
assert.ok(declared.length > 0, "HAND_CONTRACT.refusals is empty");
for (const [code, row] of declared) {
assert.ok(code in REFUSAL_CODES, `${code} is not a row of snappy-settings/refusal-codes.ts`);
assert.equal(row, REFUSAL_CODES[code as RefusalCode], `${code} is a copy of the closed table's row, not the row itself`);
}
});
#!/usr/bin/env bash
# snappy-ai-models/lib/auth.sh
#
# Single shared auth loader for all snappy-ai-models scripts.
# Loads OPENAI_API_KEY, ANTHROPIC_API_KEY, and REPLICATE_API_TOKEN
# in one sourceable file. Each script sources this and only the
# providers it actually calls are required.
#
# Resolution order per credential (first hit wins):
# 1. existing env var
# 2. snappy-settings/scripts/load-env.sh sources .env.cache into the shell
# 3. snappy-settings/scripts/get-cred.sh <short_name> (legacy shim, same cache)
#
# This file is meant to be SOURCED, not executed.
# After sourcing, the requested keys are exported.
#
# Usage from a script:
# . "$(dirname "${BASH_SOURCE[0]}")/../lib/auth.sh"
# require_openai # exits 1 if no OPENAI_API_KEY
# require_anthropic # exits 1 if no ANTHROPIC_API_KEY
# require_replicate # exits 1 if no REPLICATE_API_TOKEN
#
# Each require_* function is idempotent -- call from multiple scripts safely.
set -uo pipefail
# Default base URLs (override by exporting before sourcing)
: "${OPENAI_API_BASE:=https://api.openai.com/v1}"
: "${ANTHROPIC_API_BASE:=https://api.anthropic.com/v1}"
: "${ANTHROPIC_VERSION:=2023-06-01}"
: "${REPLICATE_API_BASE:=https://api.replicate.com/v1}"
export OPENAI_API_BASE ANTHROPIC_API_BASE ANTHROPIC_VERSION REPLICATE_API_BASE
_snappy_ai_log() {
echo "[snappy-ai-models:auth] $*" >&2
}
# ---- generic loader ----------------------------------------------------------
# args: env_var_name short_name
_snappy_ai_load_cred() {
local var_name="$1"
local short_name="$2"
# 1. already set?
if [[ -n "${!var_name:-}" ]]; then
return 0
fi
# 2. source load-env.sh (the canonical path -- populates all keys from .env.cache)
local load_env="${HOME}/.claude/skills/snappy-settings/scripts/load-env.sh"
if [[ -f "${load_env}" ]]; then
# shellcheck source=/dev/null
SNAPPY_SETTINGS_QUIET=1 . "${load_env}" 2>/dev/null || true
if [[ -n "${!var_name:-}" ]]; then
return 0
fi
fi
# 3. legacy get-cred.sh shim (reads the same .env.cache)
local settings_script="${HOME}/.claude/skills/snappy-settings/scripts/get-cred.sh"
if [[ -x "${settings_script}" ]]; then
local val
if val="$("${settings_script}" "${short_name}" 2>/dev/null)" && [[ -n "${val}" ]]; then
export "${var_name}=${val}"
return 0
fi
fi
return 1
}
# ---- per-provider require_* gates -------------------------------------------
require_openai() {
if _snappy_ai_load_cred OPENAI_API_KEY openai_api_key; then
return 0
fi
_snappy_ai_log "ERROR: could not load OPENAI_API_KEY"
_snappy_ai_log " Tried: \$OPENAI_API_KEY env, snappy-settings load-env.sh, get-cred.sh"
_snappy_ai_log " Fix: add OPENAI_API_KEY to snappy-settings/.env.cache"
return 1 2>/dev/null || exit 1
}
require_anthropic() {
if _snappy_ai_load_cred ANTHROPIC_API_KEY anthropic_api_key; then
return 0
fi
_snappy_ai_log "ERROR: could not load ANTHROPIC_API_KEY"
_snappy_ai_log " Tried: \$ANTHROPIC_API_KEY env, snappy-settings load-env.sh, get-cred.sh"
_snappy_ai_log " Fix: add ANTHROPIC_API_KEY to snappy-settings/.env.cache"
return 1 2>/dev/null || exit 1
}
require_replicate() {
if _snappy_ai_load_cred REPLICATE_API_TOKEN replicate_api_token; then
return 0
fi
_snappy_ai_log "ERROR: could not load REPLICATE_API_TOKEN"
_snappy_ai_log " Tried: \$REPLICATE_API_TOKEN env, snappy-settings load-env.sh, get-cred.sh"
_snappy_ai_log " Fix: add REPLICATE_API_TOKEN to snappy-settings/.env.cache"
return 1 2>/dev/null || exit 1
}
require_fal() {
if _snappy_ai_load_cred FAL_API_KEY fal_api_key; then
return 0
fi
_snappy_ai_log "ERROR: could not load FAL_API_KEY"
_snappy_ai_log " Tried: \$FAL_API_KEY env, snappy-settings load-env.sh, get-cred.sh"
_snappy_ai_log " Fix: add FAL_API_KEY to snappy-settings/.env.cache"
return 1 2>/dev/null || exit 1
}
# Convenience: require all three (rare -- most scripts only need one)
require_all() {
require_openai
require_anthropic
require_replicate
}
# Sanity warnings for malformed keys (non-fatal)
_snappy_ai_check_format() {
if [[ -n "${OPENAI_API_KEY:-}" && "${OPENAI_API_KEY}" != sk-* ]]; then
_snappy_ai_log "WARNING: OPENAI_API_KEY does not start with 'sk-' -- may be wrong key"
fi
if [[ -n "${ANTHROPIC_API_KEY:-}" && "${ANTHROPIC_API_KEY}" != sk-ant-* ]]; then
_snappy_ai_log "WARNING: ANTHROPIC_API_KEY does not start with 'sk-ant-' -- may be wrong key"
fi
if [[ -n "${REPLICATE_API_TOKEN:-}" && "${REPLICATE_API_TOKEN}" != r8_* ]]; then
_snappy_ai_log "WARNING: REPLICATE_API_TOKEN does not start with 'r8_' -- may be wrong key"
fi
}
#!/usr/bin/env bash
# snappy-ai-models/lib/auth.sh
#
# Single shared auth loader for all snappy-ai-models scripts.
# Loads OPENAI_API_KEY, ANTHROPIC_API_KEY, and REPLICATE_API_TOKEN
# in one sourceable file. Each script sources this and only the
# providers it actually calls are required.
#
# Resolution order per credential (first hit wins):
# 1. existing env var
# 2. snappy-settings/scripts/load-env.sh sources .env.cache into the shell
# 3. snappy-settings/scripts/get-cred.sh <short_name> (legacy shim, same cache)
#
# This file is meant to be SOURCED, not executed.
# After sourcing, the requested keys are exported.
#
# Usage from a script:
# . "$(dirname "${BASH_SOURCE[0]}")/../lib/auth.sh"
# require_openai # exits 1 if no OPENAI_API_KEY
# require_anthropic # exits 1 if no ANTHROPIC_API_KEY
# require_replicate # exits 1 if no REPLICATE_API_TOKEN
#
# Each require_* function is idempotent -- call from multiple scripts safely.
set -uo pipefail
# Default base URLs (override by exporting before sourcing)
: "${OPENAI_API_BASE:=https://api.openai.com/v1}"
: "${ANTHROPIC_API_BASE:=https://api.anthropic.com/v1}"
: "${ANTHROPIC_VERSION:=2023-06-01}"
: "${REPLICATE_API_BASE:=https://api.replicate.com/v1}"
export OPENAI_API_BASE ANTHROPIC_API_BASE ANTHROPIC_VERSION REPLICATE_API_BASE
_snappy_ai_log() {
echo "[snappy-ai-models:auth] $*" >&2
}
# ---- generic loader ----------------------------------------------------------
# args: env_var_name short_name
_snappy_ai_load_cred() {
local var_name="$1"
local short_name="$2"
# 1. already set?
if [[ -n "${!var_name:-}" ]]; then
return 0
fi
# 2. source load-env.sh (the canonical path -- populates all keys from .env.cache)
local load_env="${HOME}/.claude/skills/snappy-settings/scripts/load-env.sh"
if [[ -f "${load_env}" ]]; then
# shellcheck source=/dev/null
SNAPPY_SETTINGS_QUIET=1 . "${load_env}" 2>/dev/null || true
if [[ -n "${!var_name:-}" ]]; then
return 0
fi
fi
# 3. legacy get-cred.sh shim (reads the same .env.cache)
local settings_script="${HOME}/.claude/skills/snappy-settings/scripts/get-cred.sh"
if [[ -x "${settings_script}" ]]; then
local val
if val="$("${settings_script}" "${short_name}" 2>/dev/null)" && [[ -n "${val}" ]]; then
export "${var_name}=${val}"
return 0
fi
fi
return 1
}
# ---- per-provider require_* gates -------------------------------------------
require_openai() {
if _snappy_ai_load_cred OPENAI_API_KEY openai_api_key; then
return 0
fi
_snappy_ai_log "ERROR: could not load OPENAI_API_KEY"
_snappy_ai_log " Tried: \$OPENAI_API_KEY env, snappy-settings load-env.sh, get-cred.sh"
_snappy_ai_log " Fix: add OPENAI_API_KEY to snappy-settings/.env.cache"
return 1 2>/dev/null || exit 1
}
require_anthropic() {
if _snappy_ai_load_cred ANTHROPIC_API_KEY anthropic_api_key; then
return 0
fi
_snappy_ai_log "ERROR: could not load ANTHROPIC_API_KEY"
_snappy_ai_log " Tried: \$ANTHROPIC_API_KEY env, snappy-settings load-env.sh, get-cred.sh"
_snappy_ai_log " Fix: add ANTHROPIC_API_KEY to snappy-settings/.env.cache"
return 1 2>/dev/null || exit 1
}
require_replicate() {
if _snappy_ai_load_cred REPLICATE_API_TOKEN replicate_api_token; then
return 0
fi
_snappy_ai_log "ERROR: could not load REPLICATE_API_TOKEN"
_snappy_ai_log " Tried: \$REPLICATE_API_TOKEN env, snappy-settings load-env.sh, get-cred.sh"
_snappy_ai_log " Fix: add REPLICATE_API_TOKEN to snappy-settings/.env.cache"
return 1 2>/dev/null || exit 1
}
require_fal() {
if _snappy_ai_load_cred FAL_API_KEY fal_api_key; then
return 0
fi
_snappy_ai_log "ERROR: could not load FAL_API_KEY"
_snappy_ai_log " Tried: \$FAL_API_KEY env, snappy-settings load-env.sh, get-cred.sh"
_snappy_ai_log " Fix: add FAL_API_KEY to snappy-settings/.env.cache"
return 1 2>/dev/null || exit 1
}
# Convenience: require all three (rare -- most scripts only need one)
require_all() {
require_openai
require_anthropic
require_replicate
}
# Sanity warnings for malformed keys (non-fatal)
_snappy_ai_check_format() {
if [[ -n "${OPENAI_API_KEY:-}" && "${OPENAI_API_KEY}" != sk-* ]]; then
_snappy_ai_log "WARNING: OPENAI_API_KEY does not start with 'sk-' -- may be wrong key"
fi
if [[ -n "${ANTHROPIC_API_KEY:-}" && "${ANTHROPIC_API_KEY}" != sk-ant-* ]]; then
_snappy_ai_log "WARNING: ANTHROPIC_API_KEY does not start with 'sk-ant-' -- may be wrong key"
fi
if [[ -n "${REPLICATE_API_TOKEN:-}" && "${REPLICATE_API_TOKEN}" != r8_* ]]; then
_snappy_ai_log "WARNING: REPLICATE_API_TOKEN does not start with 'r8_' -- may be wrong key"
fi
}
Single source of truth for OpenAI, Anthropic, and Replicate model IDs used by snappy-ai-models. For Gemini, see snappy-gemini. For DeepSeek/Qwen/Llama/Mistral and unified routing, see snappy-openrouter.
Pricing in this catalog is rough and subject to change. Always check the provider dashboards for live numbers.
Need text generation?
Claude family -> anthropic/chat.sh, model: claude-sonnet-4-6 (default)
GPT family -> openai/chat.sh, model: gpt-4o-mini (default)
Anything else -> snappy-openrouter
Need image generation?
OpenAI native -> openai/image.sh, model: gpt-image-1
Flux / SD / Recraft -> replicate/flux.sh
Gemini Imagen -> snappy-gemini
Need audio?
Cheap accurate STT -> openai/audio.sh --mode transcribe (whisper-1)
Diarization + word ts -> replicate/whisper.sh --model whisperx
Fast TTS -> openai/audio.sh --mode tts (gpt-4o-mini-tts)
Need embeddings?
Default -> openai/embed.sh (text-embedding-3-large)
Need video?
Image -> video -> replicate/run.sh stable-video-diffusion
Text -> video -> replicate/run.sh hunyuan-video, or snappy-gemini Veo
| Model ID | Endpoint | Context | Output | Use Case | Rough Cost (per 1M tokens) |
|---|---|---|---|---|---|
gpt-5 |
/v1/responses |
400k | 128k | Flagship reasoning, agent loops | $1.25 in / $10 out |
gpt-5-mini |
/v1/responses |
400k | 128k | Cheap reasoning, fast | $0.25 in / $2 out |
gpt-5-nano |
/v1/responses |
400k | 128k | Ultra-cheap classification | $0.05 in / $0.40 out |
gpt-4o |
/v1/chat/completions |
128k | 16k | Multimodal flagship, prior gen | $2.50 in / $10 out |
gpt-4o-mini |
/v1/chat/completions |
128k | 16k | Cheap structured outputs (default) | $0.15 in / $0.60 out |
o1 |
/v1/chat/completions |
200k | 100k | Hard reasoning, math, code | $15 in / $60 out |
o1-mini |
/v1/chat/completions |
128k | 65k | Cheap reasoning | $1.10 in / $4.40 out |
o3 |
/v1/chat/completions |
200k | 100k | Newest hard reasoning | $2 in / $8 out |
o3-mini |
/v1/chat/completions |
200k | 100k | Cheap reasoning, default for math/code | $1.10 in / $4.40 out |
Endpoint quirks:
/v1/responses (not /v1/chat/completions). Uses input (string or array), instructions (system prompt), reasoning.effort.temperature. Uses max_completion_tokens not max_tokens. Supports reasoning_effort: low|medium|high.temperature.| Model ID | Sizes | Quality | Returns | Cost (per image) |
|---|---|---|---|---|
gpt-image-1 |
1024x1024, 1024x1536, 1536x1024, auto | low/medium/high/auto | b64 always | $0.011 (low) - $0.167 (high) |
dall-e-3 |
1024x1024, 1024x1792, 1792x1024 | standard/hd | url (or b64 if requested) | $0.04 - $0.12 |
gpt-image-1 supports transparent backgrounds (background: transparent|opaque|auto).
dall-e-3 supports style: vivid|natural.
| Model ID | Endpoint | Direction | Notes |
|---|---|---|---|
whisper-1 |
/v1/audio/transcriptions |
speech -> text | Multipart upload, srt/vtt/json/text/verbose_json |
gpt-4o-transcribe |
/v1/audio/transcriptions |
speech -> text | Higher accuracy, json/text only |
gpt-4o-mini-transcribe |
/v1/audio/transcriptions |
speech -> text | Cheaper variant |
tts-1 |
/v1/audio/speech |
text -> speech | Standard quality |
tts-1-hd |
/v1/audio/speech |
text -> speech | Higher quality |
gpt-4o-mini-tts |
/v1/audio/speech |
text -> speech | Cheapest, default |
TTS voices: alloy, ash, ballad, coral, echo, fable, nova, onyx, sage, shimmer.
| Model ID | Default Dims | Max Dims | Cost (per 1M tokens) |
|---|---|---|---|
text-embedding-3-large |
3072 | 3072 | $0.13 |
text-embedding-3-small |
1536 | 1536 | $0.02 |
text-embedding-ada-002 |
1536 | 1536 | $0.10 (legacy -- prefer 3-small) |
text-embedding-3-* support a dimensions parameter to truncate output (e.g. 1024 for cheaper storage).
| Model ID | Context | Output | Use Case | Rough Cost (per 1M tokens) |
|---|---|---|---|---|
claude-opus-4-6 |
200k | 32k | Hardest reasoning, long-form writing | $15 in / $75 out |
claude-sonnet-4-6 |
200k | 32k | Balanced default -- fast and capable | $3 in / $15 out |
claude-haiku-4-5-20251001 |
200k | 32k | Cheap classification, fast drafts | $0.80 in / $4 out |
Required headers:
x-api-key: $ANTHROPIC_API_KEY (NOT Authorization: Bearer)anthropic-version: 2023-06-01 (REQUIRED -- missing this = 400 error)Required body field: max_tokens (no default -- Anthropic rejects calls without it)
System prompts: TOP-LEVEL system field, NOT a message in the messages array.
Tool use: Tools defined as array of {name, description, input_schema}. Model returns content blocks of type tool_use when calling. Caller executes and returns tool_result blocks.
| Slug | Quality | Speed | Cost | Notes |
|---|---|---|---|---|
black-forest-labs/flux-schnell |
Good | Fast (4 steps) | ~$0.003 | Default for first drafts |
black-forest-labs/flux-dev |
Better | Medium | ~$0.025 | More control, dev-friendly |
black-forest-labs/flux-pro |
Production | Slow | ~$0.055 | Best non-ultra |
black-forest-labs/flux-1.1-pro |
Production | Slow | ~$0.04 | Newer than flux-pro |
black-forest-labs/flux-1.1-pro-ultra |
Highest res | Slowest | ~$0.06 | 2K+ resolution |
stability-ai/stable-diffusion-3.5-large |
High | Medium | ~$0.035 | SD3.5 flagship |
recraft-ai/recraft-v3 |
Vector + text | Medium | ~$0.04 | Best for logos and typography |
ideogram-ai/ideogram-v2 |
Text-in-image | Medium | ~$0.08 | Renders text inside images well |
| Slug | Direction | Notes |
|---|---|---|
openai/whisper |
STT | whisper-large-v2 hosted |
vaibhavs10/incredibly-fast-whisper |
STT | ~5x faster, batched |
daanelson/whisperx |
STT | Diarization + word timestamps |
suno-ai/bark |
TTS | Voice variety |
lucataco/xtts-v2 |
TTS | Voice cloning from reference |
| Slug | Mode | Notes |
|---|---|---|
stability-ai/stable-video-diffusion |
image -> video | 3-4s clips |
tencent/hunyuan-video |
text -> video | Longer outputs |
fofr/animate-diff |
text -> stylized loop | Good for animations |
| Slug | Use |
|---|---|
yorickvp/llava-13b |
Image -> text caption / VQA |
tencentarc/photomaker |
Face-preserving generation |
fofr/face-to-many |
Stylize a face into N styles |
These show up in old tutorials. They are obsolete and will be removed by the providers, or are simply worse than the current options.
|deprecated|use_instead|why
|----------|-----------|---
|gpt-3.5-turbo|gpt-4o-mini or gpt-5-nano|Cheaper AND better
|gpt-4|gpt-4o|Cheaper and faster
|gpt-4-turbo|gpt-4o|Same family, gpt-4o is the successor
|gpt-4-vision-preview|gpt-4o|gpt-4o is multimodal natively
|text-davinci-003|gpt-4o-mini|Removed from API
|claude-1|claude-haiku-4-5-20251001|Removed
|claude-2|claude-sonnet-4-6|Removed
|claude-2.1|claude-sonnet-4-6|Removed
|claude-3-opus-20240229|claude-opus-4-6|Older generation
|claude-3-sonnet-20240229|claude-sonnet-4-6|Older generation
|claude-3-haiku-20240307|claude-haiku-4-5-20251001|Older generation
|claude-3-5-sonnet-20241022|claude-sonnet-4-6|Older generation
|text-embedding-ada-002|text-embedding-3-small|Older, more expensive than 3-small
|flux-1.0-pro|flux-1.1-pro or flux-1.1-pro-ultra|Newer is better
|stability-ai/sdxl|stability-ai/stable-diffusion-3.5-large|SD3.5 is the current flagship
# Master Model Catalog -- All Three Direct Providers
Single source of truth for OpenAI, Anthropic, and Replicate model IDs used by snappy-ai-models. For Gemini, see `snappy-gemini`. For DeepSeek/Qwen/Llama/Mistral and unified routing, see `snappy-openrouter`.
> Pricing in this catalog is rough and subject to change. Always check the provider dashboards for live numbers.
## Table of Contents
- [Quick decision tree](#quick-decision-tree)
- [OpenAI text models](#openai-text-models)
- [OpenAI image models](#openai-image-models)
- [OpenAI audio models](#openai-audio-models)
- [OpenAI embedding models](#openai-embedding-models)
- [Anthropic models](#anthropic-models)
- [Replicate image models](#replicate-image-models)
- [Replicate audio/speech models](#replicate-audio-speech-models)
- [Replicate video models](#replicate-video-models)
- [Replicate vision/multimodal models](#replicate-vision-multimodal-models)
- [Deprecated -- DO NOT USE](#deprecated-do-not-use)
## Quick decision tree
```
Need text generation?
Claude family -> anthropic/chat.sh, model: claude-sonnet-4-6 (default)
GPT family -> openai/chat.sh, model: gpt-4o-mini (default)
Anything else -> snappy-openrouter
Need image generation?
OpenAI native -> openai/image.sh, model: gpt-image-1
Flux / SD / Recraft -> replicate/flux.sh
Gemini Imagen -> snappy-gemini
Need audio?
Cheap accurate STT -> openai/audio.sh --mode transcribe (whisper-1)
Diarization + word ts -> replicate/whisper.sh --model whisperx
Fast TTS -> openai/audio.sh --mode tts (gpt-4o-mini-tts)
Need embeddings?
Default -> openai/embed.sh (text-embedding-3-large)
Need video?
Image -> video -> replicate/run.sh stable-video-diffusion
Text -> video -> replicate/run.sh hunyuan-video, or snappy-gemini Veo
```
## OpenAI text models
| Model ID | Endpoint | Context | Output | Use Case | Rough Cost (per 1M tokens) |
|----------|----------|---------|--------|----------|----------------------------|
| `gpt-5` | `/v1/responses` | 400k | 128k | Flagship reasoning, agent loops | $1.25 in / $10 out |
| `gpt-5-mini` | `/v1/responses` | 400k | 128k | Cheap reasoning, fast | $0.25 in / $2 out |
| `gpt-5-nano` | `/v1/responses` | 400k | 128k | Ultra-cheap classification | $0.05 in / $0.40 out |
| `gpt-4o` | `/v1/chat/completions` | 128k | 16k | Multimodal flagship, prior gen | $2.50 in / $10 out |
| `gpt-4o-mini` | `/v1/chat/completions` | 128k | 16k | Cheap structured outputs (default) | $0.15 in / $0.60 out |
| `o1` | `/v1/chat/completions` | 200k | 100k | Hard reasoning, math, code | $15 in / $60 out |
| `o1-mini` | `/v1/chat/completions` | 128k | 65k | Cheap reasoning | $1.10 in / $4.40 out |
| `o3` | `/v1/chat/completions` | 200k | 100k | Newest hard reasoning | $2 in / $8 out |
| `o3-mini` | `/v1/chat/completions` | 200k | 100k | Cheap reasoning, default for math/code | $1.10 in / $4.40 out |
**Endpoint quirks:**
- gpt-5 family uses `/v1/responses` (not `/v1/chat/completions`). Uses `input` (string or array), `instructions` (system prompt), `reasoning.effort`.
- o-series rejects `temperature`. Uses `max_completion_tokens` not `max_tokens`. Supports `reasoning_effort: low|medium|high`.
- gpt-5 reasoning models don't support `temperature`.
- chat.sh handles all of this automatically.
## OpenAI image models
| Model ID | Sizes | Quality | Returns | Cost (per image) |
|----------|-------|---------|---------|------------------|
| `gpt-image-1` | 1024x1024, 1024x1536, 1536x1024, auto | low/medium/high/auto | b64 always | $0.011 (low) - $0.167 (high) |
| `dall-e-3` | 1024x1024, 1024x1792, 1792x1024 | standard/hd | url (or b64 if requested) | $0.04 - $0.12 |
`gpt-image-1` supports transparent backgrounds (`background: transparent|opaque|auto`).
`dall-e-3` supports `style: vivid|natural`.
## OpenAI audio models
| Model ID | Endpoint | Direction | Notes |
|----------|----------|-----------|-------|
| `whisper-1` | `/v1/audio/transcriptions` | speech -> text | Multipart upload, srt/vtt/json/text/verbose_json |
| `gpt-4o-transcribe` | `/v1/audio/transcriptions` | speech -> text | Higher accuracy, json/text only |
| `gpt-4o-mini-transcribe` | `/v1/audio/transcriptions` | speech -> text | Cheaper variant |
| `tts-1` | `/v1/audio/speech` | text -> speech | Standard quality |
| `tts-1-hd` | `/v1/audio/speech` | text -> speech | Higher quality |
| `gpt-4o-mini-tts` | `/v1/audio/speech` | text -> speech | Cheapest, default |
TTS voices: `alloy`, `ash`, `ballad`, `coral`, `echo`, `fable`, `nova`, `onyx`, `sage`, `shimmer`.
## OpenAI embedding models
| Model ID | Default Dims | Max Dims | Cost (per 1M tokens) |
|----------|--------------|----------|----------------------|
| `text-embedding-3-large` | 3072 | 3072 | $0.13 |
| `text-embedding-3-small` | 1536 | 1536 | $0.02 |
| `text-embedding-ada-002` | 1536 | 1536 | $0.10 (legacy -- prefer 3-small) |
`text-embedding-3-*` support a `dimensions` parameter to truncate output (e.g. 1024 for cheaper storage).
## Anthropic models
| Model ID | Context | Output | Use Case | Rough Cost (per 1M tokens) |
|----------|---------|--------|----------|----------------------------|
| `claude-opus-4-6` | 200k | 32k | Hardest reasoning, long-form writing | $15 in / $75 out |
| `claude-sonnet-4-6` | 200k | 32k | Balanced default -- fast and capable | $3 in / $15 out |
| `claude-haiku-4-5-20251001` | 200k | 32k | Cheap classification, fast drafts | $0.80 in / $4 out |
**Required headers:**
- `x-api-key: $ANTHROPIC_API_KEY` (NOT `Authorization: Bearer`)
- `anthropic-version: 2023-06-01` (REQUIRED -- missing this = 400 error)
**Required body field:** `max_tokens` (no default -- Anthropic rejects calls without it)
**System prompts:** TOP-LEVEL `system` field, NOT a message in the messages array.
**Tool use:** Tools defined as array of `{name, description, input_schema}`. Model returns `content` blocks of type `tool_use` when calling. Caller executes and returns `tool_result` blocks.
## Replicate image models
| Slug | Quality | Speed | Cost | Notes |
|------|---------|-------|------|-------|
| `black-forest-labs/flux-schnell` | Good | Fast (4 steps) | ~$0.003 | Default for first drafts |
| `black-forest-labs/flux-dev` | Better | Medium | ~$0.025 | More control, dev-friendly |
| `black-forest-labs/flux-pro` | Production | Slow | ~$0.055 | Best non-ultra |
| `black-forest-labs/flux-1.1-pro` | Production | Slow | ~$0.04 | Newer than flux-pro |
| `black-forest-labs/flux-1.1-pro-ultra` | Highest res | Slowest | ~$0.06 | 2K+ resolution |
| `stability-ai/stable-diffusion-3.5-large` | High | Medium | ~$0.035 | SD3.5 flagship |
| `recraft-ai/recraft-v3` | Vector + text | Medium | ~$0.04 | Best for logos and typography |
| `ideogram-ai/ideogram-v2` | Text-in-image | Medium | ~$0.08 | Renders text inside images well |
## Replicate audio/speech models
| Slug | Direction | Notes |
|------|-----------|-------|
| `openai/whisper` | STT | whisper-large-v2 hosted |
| `vaibhavs10/incredibly-fast-whisper` | STT | ~5x faster, batched |
| `daanelson/whisperx` | STT | Diarization + word timestamps |
| `suno-ai/bark` | TTS | Voice variety |
| `lucataco/xtts-v2` | TTS | Voice cloning from reference |
## Replicate video models
| Slug | Mode | Notes |
|------|------|-------|
| `stability-ai/stable-video-diffusion` | image -> video | 3-4s clips |
| `tencent/hunyuan-video` | text -> video | Longer outputs |
| `fofr/animate-diff` | text -> stylized loop | Good for animations |
## Replicate vision/multimodal models
| Slug | Use |
|------|-----|
| `yorickvp/llava-13b` | Image -> text caption / VQA |
| `tencentarc/photomaker` | Face-preserving generation |
| `fofr/face-to-many` | Stylize a face into N styles |
## Deprecated -- DO NOT USE
These show up in old tutorials. They are obsolete and will be removed by the providers, or are simply worse than the current options.
|deprecated|use_instead|why
|----------|-----------|---
|`gpt-3.5-turbo`|`gpt-4o-mini` or `gpt-5-nano`|Cheaper AND better
|`gpt-4`|`gpt-4o`|Cheaper and faster
|`gpt-4-turbo`|`gpt-4o`|Same family, gpt-4o is the successor
|`gpt-4-vision-preview`|`gpt-4o`|gpt-4o is multimodal natively
|`text-davinci-003`|`gpt-4o-mini`|Removed from API
|`claude-1`|`claude-haiku-4-5-20251001`|Removed
|`claude-2`|`claude-sonnet-4-6`|Removed
|`claude-2.1`|`claude-sonnet-4-6`|Removed
|`claude-3-opus-20240229`|`claude-opus-4-6`|Older generation
|`claude-3-sonnet-20240229`|`claude-sonnet-4-6`|Older generation
|`claude-3-haiku-20240307`|`claude-haiku-4-5-20251001`|Older generation
|`claude-3-5-sonnet-20241022`|`claude-sonnet-4-6`|Older generation
|`text-embedding-ada-002`|`text-embedding-3-small`|Older, more expensive than 3-small
|`flux-1.0-pro`|`flux-1.1-pro` or `flux-1.1-pro-ultra`|Newer is better
|`stability-ai/sdxl`|`stability-ai/stable-diffusion-3.5-large`|SD3.5 is the current flagship
Direct calls to OpenAI for the snappy system. Use this when you want first-party features (Realtime, Batch, Assistants, structured outputs) instead of routing through OpenRouter.
| Script | Endpoint | Purpose |
|---|---|---|
chat.sh |
/v1/responses (gpt-5) or /v1/chat/completions (everything else) |
Text generation |
image.sh |
/v1/images/generations |
Image generation (gpt-image-1, dall-e-3) |
audio.sh |
/v1/audio/transcriptions (transcribe) or /v1/audio/speech (tts) |
Whisper STT + tts |
embed.sh |
/v1/embeddings |
Text embeddings |
All scripts source ../lib/auth.sh, which loads OPENAI_API_KEY via snappy-settings/scripts/load-env.sh (reads .env.cache). Override with an env var if needed.
|model_family|endpoint|notes
|------------|--------|-----
|gpt-5, gpt-5-mini, gpt-5-nano|/v1/responses|Newer Responses API. Uses input (not messages), instructions (not system role). Supports reasoning.effort.
|gpt-4o, gpt-4o-mini, gpt-4-turbo|/v1/chat/completions|Standard messages array. Honours temperature, max_tokens.
|o1, o1-mini, o3, o3-mini|/v1/chat/completions|No temperature. Uses max_completion_tokens. Supports reasoning_effort.
|dall-e-3|/v1/images/generations|Returns URL by default (we force b64 for file save). Sizes: 1024x1024, 1024x1792, 1792x1024.
|gpt-image-1|/v1/images/generations|Always returns b64. Sizes: 1024x1024, 1024x1536, 1536x1024, auto. Supports transparent backgrounds.
|whisper-1|/v1/audio/transcriptions|Multipart upload. Supports srt/vtt/json/verbose_json/text response formats.
|gpt-4o-transcribe, gpt-4o-mini-transcribe|/v1/audio/transcriptions|Higher quality than whisper-1. Only json/text response formats.
|tts-1, tts-1-hd, gpt-4o-mini-tts|/v1/audio/speech|Returns binary audio. Voices: alloy, ash, ballad, coral, echo, fable, nova, onyx, sage, shimmer.
|text-embedding-3-large|/v1/embeddings|3072 dim default, truncatable via dimensions param.
|text-embedding-3-small|/v1/embeddings|1536 dim default, cheaper.
bash# All scripts source this and call require_openai
. "$(dirname "$0")/../lib/auth.sh"
require_openai
If OPENAI_API_KEY is missing, the script exits 1 with a clear error pointing at snappy-settings.
bash# Quick text gen with cheap model
./chat.sh --model gpt-4o-mini --prompt "Classify: $(cat input.txt)"
# Generate a transparent product image
./image.sh --model gpt-image-1 --prompt "..." --size 1024x1024 --background transparent --out hero.png
# Transcribe a meeting recording
./audio.sh --mode transcribe --file meeting.mp3 --model whisper-1 > transcript.txt
# Speak a daily briefing as audio
./audio.sh --mode tts --text "Good morning..." --voice nova --out briefing.mp3
# Embed a single query for vector search
./embed.sh --text "How does Snappy bill clients?" --vector > query.vec
snappy-openrouter../anthropic/snappy-gemini../replicate/OpenAI returns x-ratelimit-remaining-requests, x-ratelimit-remaining-tokens, x-ratelimit-reset-requests. The scripts don't surface these by default -- pass --json and inspect headers via curl -D - if you need them.
# OpenAI Direct API -- Endpoint Reference Direct calls to OpenAI for the snappy system. Use this when you want first-party features (Realtime, Batch, Assistants, structured outputs) instead of routing through OpenRouter. ## Scripts | Script | Endpoint | Purpose | |--------|----------|---------| | `chat.sh` | `/v1/responses` (gpt-5) or `/v1/chat/completions` (everything else) | Text generation | | `image.sh` | `/v1/images/generations` | Image generation (gpt-image-1, dall-e-3) | | `audio.sh` | `/v1/audio/transcriptions` (transcribe) or `/v1/audio/speech` (tts) | Whisper STT + tts | | `embed.sh` | `/v1/embeddings` | Text embeddings | All scripts source `../lib/auth.sh`, which loads `OPENAI_API_KEY` via `snappy-settings/scripts/load-env.sh` (reads `.env.cache`). Override with an env var if needed. ## Endpoint Decision Matrix |model_family|endpoint|notes |------------|--------|----- |gpt-5, gpt-5-mini, gpt-5-nano|`/v1/responses`|Newer Responses API. Uses `input` (not `messages`), `instructions` (not system role). Supports `reasoning.effort`. |gpt-4o, gpt-4o-mini, gpt-4-turbo|`/v1/chat/completions`|Standard messages array. Honours `temperature`, `max_tokens`. |o1, o1-mini, o3, o3-mini|`/v1/chat/completions`|No `temperature`. Uses `max_completion_tokens`. Supports `reasoning_effort`. |dall-e-3|`/v1/images/generations`|Returns URL by default (we force b64 for file save). Sizes: 1024x1024, 1024x1792, 1792x1024. |gpt-image-1|`/v1/images/generations`|Always returns b64. Sizes: 1024x1024, 1024x1536, 1536x1024, auto. Supports transparent backgrounds. |whisper-1|`/v1/audio/transcriptions`|Multipart upload. Supports srt/vtt/json/verbose_json/text response formats. |gpt-4o-transcribe, gpt-4o-mini-transcribe|`/v1/audio/transcriptions`|Higher quality than whisper-1. Only json/text response formats. |tts-1, tts-1-hd, gpt-4o-mini-tts|`/v1/audio/speech`|Returns binary audio. Voices: alloy, ash, ballad, coral, echo, fable, nova, onyx, sage, shimmer. |text-embedding-3-large|`/v1/embeddings`|3072 dim default, truncatable via `dimensions` param. |text-embedding-3-small|`/v1/embeddings`|1536 dim default, cheaper. ## Authentication ```bash # All scripts source this and call require_openai . "$(dirname "$0")/../lib/auth.sh" require_openai ``` If `OPENAI_API_KEY` is missing, the script exits 1 with a clear error pointing at `snappy-settings`. ## Common Patterns ```bash # Quick text gen with cheap model ./chat.sh --model gpt-4o-mini --prompt "Classify: $(cat input.txt)" # Generate a transparent product image ./image.sh --model gpt-image-1 --prompt "..." --size 1024x1024 --background transparent --out hero.png # Transcribe a meeting recording ./audio.sh --mode transcribe --file meeting.mp3 --model whisper-1 > transcript.txt # Speak a daily briefing as audio ./audio.sh --mode tts --text "Good morning..." --voice nova --out briefing.mp3 # Embed a single query for vector search ./embed.sh --text "How does Snappy bill clients?" --vector > query.vec ``` ## When NOT To Use This - For DeepSeek, Qwen, Llama, Mistral -> use `snappy-openrouter` - For Claude (any version) -> use `../anthropic/` - For Gemini -> use `snappy-gemini` - For Flux, SDXL, hosted open-source models -> use `../replicate/` ## Rate Limit Headers (response includes) OpenAI returns `x-ratelimit-remaining-requests`, `x-ratelimit-remaining-tokens`, `x-ratelimit-reset-requests`. The scripts don't surface these by default -- pass `--json` and inspect headers via curl `-D -` if you need them.
#!/usr/bin/env bash
# snappy-ai-models/openai/audio.sh
#
# Two modes:
# - transcribe audio file -> text via Whisper or gpt-4o-transcribe
# - tts text -> audio file via gpt-4o-mini-tts / tts-1
#
# Usage:
# # Transcribe an audio file
# ./audio.sh --mode transcribe --file recording.mp3
# ./audio.sh --mode transcribe --file ru.wav --language ru --json
# ./audio.sh --mode transcribe --file long.mp3 --model gpt-4o-transcribe
#
# # Speak text to a file
# ./audio.sh --mode tts --text "Hello world" --voice nova --out hello.mp3
# ./audio.sh --mode tts --model gpt-4o-mini-tts --voice alloy --text "..." --out out.wav --format wav
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=../lib/auth.sh
. "${SCRIPT_DIR}/../lib/auth.sh"
MODE=""
MODEL="" # default depends on mode
FILE=""
TEXT=""
LANGUAGE=""
VOICE="nova"
OUT=""
FORMAT="mp3"
RAW_JSON=0
usage() {
cat <<'EOF'
Usage: openai/audio.sh --mode <transcribe|tts> [options]
Common:
--mode <m> transcribe | tts (required)
--json Print full JSON response (transcribe only)
-h, --help Show this help
Transcribe options:
--file <path> Audio file (mp3, wav, m4a, ogg, webm, etc.) [required]
--model <id> whisper-1 (default) | gpt-4o-transcribe | gpt-4o-mini-transcribe
--language <code> ISO-639-1 code (en, fr, ja, ...) -- improves accuracy
TTS options:
--text <text> Text to synthesize [required]
--model <id> tts-1 (default) | tts-1-hd | gpt-4o-mini-tts
--voice <name> alloy|ash|ballad|coral|echo|fable|nova|onyx|sage|shimmer
--out <path> Output audio file [required]
--format <fmt> mp3|wav|opus|aac|flac|pcm (default mp3)
EOF
}
if [[ $# -eq 0 ]]; then
usage
exit 1
fi
while [[ $# -gt 0 ]]; do
case "$1" in
--mode) MODE="$2"; shift 2 ;;
--model) MODEL="$2"; shift 2 ;;
--file) FILE="$2"; shift 2 ;;
--text) TEXT="$2"; shift 2 ;;
--language) LANGUAGE="$2"; shift 2 ;;
--voice) VOICE="$2"; shift 2 ;;
--out) OUT="$2"; shift 2 ;;
--format) FORMAT="$2"; shift 2 ;;
--json) RAW_JSON=1; shift ;;
-h|--help) usage; exit 0 ;;
*) echo "openai/audio: unknown flag: $1" >&2; usage; exit 1 ;;
esac
done
if [[ -z "${MODE}" ]]; then
echo "openai/audio: --mode is required" >&2
exit 1
fi
require_openai
case "${MODE}" in
transcribe)
if [[ -z "${FILE}" ]]; then
echo "openai/audio: --file is required for transcribe" >&2
exit 1
fi
if [[ ! -f "${FILE}" ]]; then
echo "openai/audio: file not found: ${FILE}" >&2
exit 1
fi
: "${MODEL:=whisper-1}"
URL="${OPENAI_API_BASE}/audio/transcriptions"
CURL_ARGS=(
-sS -X POST "${URL}"
-H "Authorization: Bearer ${OPENAI_API_KEY}"
-F "file=@${FILE}"
-F "model=${MODEL}"
)
if [[ -n "${LANGUAGE}" ]]; then
CURL_ARGS+=(-F "language=${LANGUAGE}")
fi
# whisper-1 supports response_format json/verbose_json/text/srt/vtt
# gpt-4o-transcribe / gpt-4o-mini-transcribe only support json/text
CURL_ARGS+=(-F "response_format=json")
RESP="$(curl "${CURL_ARGS[@]}")"
if jq -e '.error' >/dev/null 2>&1 <<<"${RESP}"; then
echo "openai/audio: API error" >&2
jq '.error' >&2 <<<"${RESP}"
exit 2
fi
if [[ "${RAW_JSON}" -eq 1 ]]; then
printf '%s\n' "${RESP}"
else
jq -r '.text // empty' <<<"${RESP}"
fi
;;
tts)
if [[ -z "${TEXT}" ]]; then
echo "openai/audio: --text is required for tts" >&2
exit 1
fi
if [[ -z "${OUT}" ]]; then
echo "openai/audio: --out is required for tts" >&2
exit 1
fi
: "${MODEL:=gpt-4o-mini-tts}"
BODY="$(jq -n \
--arg model "${MODEL}" \
--arg input "${TEXT}" \
--arg voice "${VOICE}" \
--arg fmt "${FORMAT}" \
'{model: $model, input: $input, voice: $voice, response_format: $fmt}')"
URL="${OPENAI_API_BASE}/audio/speech"
HTTP_CODE="$(curl -sS -o "${OUT}" -w '%{http_code}' -X POST "${URL}" \
-H "Authorization: Bearer ${OPENAI_API_KEY}" \
-H "Content-Type: application/json" \
--data "${BODY}")"
if [[ "${HTTP_CODE}" != "200" ]]; then
echo "openai/audio: tts failed with HTTP ${HTTP_CODE}" >&2
# Output file probably contains the error JSON
cat "${OUT}" >&2
rm -f "${OUT}"
exit 2
fi
echo "wrote ${OUT}"
;;
*)
echo "openai/audio: unknown mode: ${MODE} (use transcribe|tts)" >&2
exit 1
;;
esac
#!/usr/bin/env bash
# snappy-ai-models/openai/audio.sh
#
# Two modes:
# - transcribe audio file -> text via Whisper or gpt-4o-transcribe
# - tts text -> audio file via gpt-4o-mini-tts / tts-1
#
# Usage:
# # Transcribe an audio file
# ./audio.sh --mode transcribe --file recording.mp3
# ./audio.sh --mode transcribe --file ru.wav --language ru --json
# ./audio.sh --mode transcribe --file long.mp3 --model gpt-4o-transcribe
#
# # Speak text to a file
# ./audio.sh --mode tts --text "Hello world" --voice nova --out hello.mp3
# ./audio.sh --mode tts --model gpt-4o-mini-tts --voice alloy --text "..." --out out.wav --format wav
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=../lib/auth.sh
. "${SCRIPT_DIR}/../lib/auth.sh"
MODE=""
MODEL="" # default depends on mode
FILE=""
TEXT=""
LANGUAGE=""
VOICE="nova"
OUT=""
FORMAT="mp3"
RAW_JSON=0
usage() {
cat <<'EOF'
Usage: openai/audio.sh --mode <transcribe|tts> [options]
Common:
--mode <m> transcribe | tts (required)
--json Print full JSON response (transcribe only)
-h, --help Show this help
Transcribe options:
--file <path> Audio file (mp3, wav, m4a, ogg, webm, etc.) [required]
--model <id> whisper-1 (default) | gpt-4o-transcribe | gpt-4o-mini-transcribe
--language <code> ISO-639-1 code (en, fr, ja, ...) -- improves accuracy
TTS options:
--text <text> Text to synthesize [required]
--model <id> tts-1 (default) | tts-1-hd | gpt-4o-mini-tts
--voice <name> alloy|ash|ballad|coral|echo|fable|nova|onyx|sage|shimmer
--out <path> Output audio file [required]
--format <fmt> mp3|wav|opus|aac|flac|pcm (default mp3)
EOF
}
if [[ $# -eq 0 ]]; then
usage
exit 1
fi
while [[ $# -gt 0 ]]; do
case "$1" in
--mode) MODE="$2"; shift 2 ;;
--model) MODEL="$2"; shift 2 ;;
--file) FILE="$2"; shift 2 ;;
--text) TEXT="$2"; shift 2 ;;
--language) LANGUAGE="$2"; shift 2 ;;
--voice) VOICE="$2"; shift 2 ;;
--out) OUT="$2"; shift 2 ;;
--format) FORMAT="$2"; shift 2 ;;
--json) RAW_JSON=1; shift ;;
-h|--help) usage; exit 0 ;;
*) echo "openai/audio: unknown flag: $1" >&2; usage; exit 1 ;;
esac
done
if [[ -z "${MODE}" ]]; then
echo "openai/audio: --mode is required" >&2
exit 1
fi
require_openai
case "${MODE}" in
transcribe)
if [[ -z "${FILE}" ]]; then
echo "openai/audio: --file is required for transcribe" >&2
exit 1
fi
if [[ ! -f "${FILE}" ]]; then
echo "openai/audio: file not found: ${FILE}" >&2
exit 1
fi
: "${MODEL:=whisper-1}"
URL="${OPENAI_API_BASE}/audio/transcriptions"
CURL_ARGS=(
-sS -X POST "${URL}"
-H "Authorization: Bearer ${OPENAI_API_KEY}"
-F "file=@${FILE}"
-F "model=${MODEL}"
)
if [[ -n "${LANGUAGE}" ]]; then
CURL_ARGS+=(-F "language=${LANGUAGE}")
fi
# whisper-1 supports response_format json/verbose_json/text/srt/vtt
# gpt-4o-transcribe / gpt-4o-mini-transcribe only support json/text
CURL_ARGS+=(-F "response_format=json")
RESP="$(curl "${CURL_ARGS[@]}")"
if jq -e '.error' >/dev/null 2>&1 <<<"${RESP}"; then
echo "openai/audio: API error" >&2
jq '.error' >&2 <<<"${RESP}"
exit 2
fi
if [[ "${RAW_JSON}" -eq 1 ]]; then
printf '%s\n' "${RESP}"
else
jq -r '.text // empty' <<<"${RESP}"
fi
;;
tts)
if [[ -z "${TEXT}" ]]; then
echo "openai/audio: --text is required for tts" >&2
exit 1
fi
if [[ -z "${OUT}" ]]; then
echo "openai/audio: --out is required for tts" >&2
exit 1
fi
: "${MODEL:=gpt-4o-mini-tts}"
BODY="$(jq -n \
--arg model "${MODEL}" \
--arg input "${TEXT}" \
--arg voice "${VOICE}" \
--arg fmt "${FORMAT}" \
'{model: $model, input: $input, voice: $voice, response_format: $fmt}')"
URL="${OPENAI_API_BASE}/audio/speech"
HTTP_CODE="$(curl -sS -o "${OUT}" -w '%{http_code}' -X POST "${URL}" \
-H "Authorization: Bearer ${OPENAI_API_KEY}" \
-H "Content-Type: application/json" \
--data "${BODY}")"
if [[ "${HTTP_CODE}" != "200" ]]; then
echo "openai/audio: tts failed with HTTP ${HTTP_CODE}" >&2
# Output file probably contains the error JSON
cat "${OUT}" >&2
rm -f "${OUT}"
exit 2
fi
echo "wrote ${OUT}"
;;
*)
echo "openai/audio: unknown mode: ${MODE} (use transcribe|tts)" >&2
exit 1
;;
esac
#!/usr/bin/env bash
# snappy-ai-models/openai/chat.sh
#
# Direct call to OpenAI for text generation. Routes between two endpoints:
# - GPT-5 family (gpt-5, gpt-5-mini, gpt-5-nano) -> /v1/responses (Responses API)
# - everything else (gpt-4o, gpt-4o-mini, o1, o3) -> /v1/chat/completions
#
# The API surface differs subtly between the two. This script picks the right
# one based on --model and normalises the output to plain text by default.
#
# Usage:
# ./chat.sh --model gpt-4o --prompt "Summarize this"
# ./chat.sh --model gpt-5 --prompt "Plan a launch" --system "You are a CMO"
# ./chat.sh --model gpt-4o-mini --prompt "Classify intent" --json
# ./chat.sh --model o3-mini --prompt "Solve this puzzle" --reasoning high
# echo "stdin works too" | ./chat.sh --model gpt-4o-mini
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=../lib/auth.sh
. "${SCRIPT_DIR}/../lib/auth.sh"
MODEL="gpt-4o-mini"
PROMPT=""
SYSTEM=""
TEMPERATURE=""
MAX_TOKENS=""
REASONING="" # only honoured by o-series and gpt-5 (low|medium|high)
RAW_JSON=0
usage() {
cat <<'EOF'
Usage: openai/chat.sh [options]
Options:
--model <id> OpenAI model id (default: gpt-4o-mini)
--prompt <text> User prompt (or pipe via stdin)
--system <text> System / developer instruction
--temperature <0..2> Sampling temperature
--max-tokens <n> Max output tokens
--reasoning <effort> low|medium|high (gpt-5 / o-series only)
--json Return full JSON response (default: extracted text)
-h, --help Show this help
Endpoint selection (automatic):
/v1/responses gpt-5, gpt-5-mini, gpt-5-nano
/v1/chat/completions gpt-4o, gpt-4o-mini, gpt-4-turbo, o1, o1-mini, o3, o3-mini
Common models:
gpt-5 Flagship reasoning, Responses API
gpt-5-mini Cheap reasoning, Responses API
gpt-4o Multimodal flagship, chat completions
gpt-4o-mini Cheap structured output, chat completions
o1 Deep reasoning, no temperature
o3-mini Cheap reasoning, no temperature
See models.md for the full catalog.
EOF
}
if [[ $# -eq 0 && -t 0 ]]; then
usage
exit 1
fi
while [[ $# -gt 0 ]]; do
case "$1" in
--model) MODEL="$2"; shift 2 ;;
--prompt) PROMPT="$2"; shift 2 ;;
--system) SYSTEM="$2"; shift 2 ;;
--temperature) TEMPERATURE="$2"; shift 2 ;;
--max-tokens) MAX_TOKENS="$2"; shift 2 ;;
--reasoning) REASONING="$2"; shift 2 ;;
--json) RAW_JSON=1; shift ;;
-h|--help) usage; exit 0 ;;
*) echo "openai/chat: unknown flag: $1" >&2; usage; exit 1 ;;
esac
done
# stdin fallback for prompt
if [[ -z "${PROMPT}" && ! -t 0 ]]; then
PROMPT="$(cat)"
fi
if [[ -z "${PROMPT}" ]]; then
echo "openai/chat: --prompt (or stdin) is required" >&2
exit 1
fi
require_openai
# ----- pick endpoint family ---------------------------------------------------
USE_RESPONSES_API=0
case "${MODEL}" in
gpt-5|gpt-5-*|o1-pro) USE_RESPONSES_API=1 ;;
esac
if [[ "${USE_RESPONSES_API}" -eq 1 ]]; then
# ----- Responses API (gpt-5 family) ----------------------------------------
# The Responses API uses `input` (string or list) and `instructions` (system prompt).
BODY="$(jq -n \
--arg model "${MODEL}" \
--arg input "${PROMPT}" \
'{model: $model, input: $input}')"
if [[ -n "${SYSTEM}" ]]; then
BODY="$(jq --arg s "${SYSTEM}" '. + {instructions: $s}' <<<"${BODY}")"
fi
if [[ -n "${MAX_TOKENS}" ]]; then
BODY="$(jq --argjson n "${MAX_TOKENS}" '. + {max_output_tokens: $n}' <<<"${BODY}")"
fi
if [[ -n "${REASONING}" ]]; then
BODY="$(jq --arg r "${REASONING}" '. + {reasoning: {effort: $r}}' <<<"${BODY}")"
fi
# temperature is not supported by gpt-5 reasoning models -- silently drop
URL="${OPENAI_API_BASE}/responses"
RESP="$(curl -sS -X POST "${URL}" \
-H "Authorization: Bearer ${OPENAI_API_KEY}" \
-H "Content-Type: application/json" \
--data "${BODY}")"
if jq -e '.error' >/dev/null 2>&1 <<<"${RESP}"; then
echo "openai/chat: API error" >&2
jq '.error' >&2 <<<"${RESP}"
exit 2
fi
if [[ "${RAW_JSON}" -eq 1 ]]; then
printf '%s\n' "${RESP}"
else
# Responses API: output[].content[].text contains the assistant text
jq -r '.output[]?.content[]?.text // empty' <<<"${RESP}"
fi
else
# ----- Chat Completions API (gpt-4o, o1, o3, gpt-4-turbo) -------------------
MSGS="$(jq -n '[]')"
if [[ -n "${SYSTEM}" ]]; then
MSGS="$(jq --arg s "${SYSTEM}" '. + [{role: "system", content: $s}]' <<<"${MSGS}")"
fi
MSGS="$(jq --arg p "${PROMPT}" '. + [{role: "user", content: $p}]' <<<"${MSGS}")"
BODY="$(jq -n \
--arg model "${MODEL}" \
--argjson msgs "${MSGS}" \
'{model: $model, messages: $msgs}')"
# o-series models reject temperature/max_tokens -- only honour for non-reasoning
case "${MODEL}" in
o1*|o3*)
if [[ -n "${MAX_TOKENS}" ]]; then
BODY="$(jq --argjson n "${MAX_TOKENS}" '. + {max_completion_tokens: $n}' <<<"${BODY}")"
fi
if [[ -n "${REASONING}" ]]; then
BODY="$(jq --arg r "${REASONING}" '. + {reasoning_effort: $r}' <<<"${BODY}")"
fi
;;
*)
if [[ -n "${TEMPERATURE}" ]]; then
BODY="$(jq --argjson t "${TEMPERATURE}" '. + {temperature: $t}' <<<"${BODY}")"
fi
if [[ -n "${MAX_TOKENS}" ]]; then
BODY="$(jq --argjson n "${MAX_TOKENS}" '. + {max_tokens: $n}' <<<"${BODY}")"
fi
;;
esac
URL="${OPENAI_API_BASE}/chat/completions"
RESP="$(curl -sS -X POST "${URL}" \
-H "Authorization: Bearer ${OPENAI_API_KEY}" \
-H "Content-Type: application/json" \
--data "${BODY}")"
if jq -e '.error' >/dev/null 2>&1 <<<"${RESP}"; then
echo "openai/chat: API error" >&2
jq '.error' >&2 <<<"${RESP}"
exit 2
fi
if [[ "${RAW_JSON}" -eq 1 ]]; then
printf '%s\n' "${RESP}"
else
jq -r '.choices[0].message.content // empty' <<<"${RESP}"
fi
fi
#!/usr/bin/env bash
# snappy-ai-models/openai/chat.sh
#
# Direct call to OpenAI for text generation. Routes between two endpoints:
# - GPT-5 family (gpt-5, gpt-5-mini, gpt-5-nano) -> /v1/responses (Responses API)
# - everything else (gpt-4o, gpt-4o-mini, o1, o3) -> /v1/chat/completions
#
# The API surface differs subtly between the two. This script picks the right
# one based on --model and normalises the output to plain text by default.
#
# Usage:
# ./chat.sh --model gpt-4o --prompt "Summarize this"
# ./chat.sh --model gpt-5 --prompt "Plan a launch" --system "You are a CMO"
# ./chat.sh --model gpt-4o-mini --prompt "Classify intent" --json
# ./chat.sh --model o3-mini --prompt "Solve this puzzle" --reasoning high
# echo "stdin works too" | ./chat.sh --model gpt-4o-mini
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=../lib/auth.sh
. "${SCRIPT_DIR}/../lib/auth.sh"
MODEL="gpt-4o-mini"
PROMPT=""
SYSTEM=""
TEMPERATURE=""
MAX_TOKENS=""
REASONING="" # only honoured by o-series and gpt-5 (low|medium|high)
RAW_JSON=0
usage() {
cat <<'EOF'
Usage: openai/chat.sh [options]
Options:
--model <id> OpenAI model id (default: gpt-4o-mini)
--prompt <text> User prompt (or pipe via stdin)
--system <text> System / developer instruction
--temperature <0..2> Sampling temperature
--max-tokens <n> Max output tokens
--reasoning <effort> low|medium|high (gpt-5 / o-series only)
--json Return full JSON response (default: extracted text)
-h, --help Show this help
Endpoint selection (automatic):
/v1/responses gpt-5, gpt-5-mini, gpt-5-nano
/v1/chat/completions gpt-4o, gpt-4o-mini, gpt-4-turbo, o1, o1-mini, o3, o3-mini
Common models:
gpt-5 Flagship reasoning, Responses API
gpt-5-mini Cheap reasoning, Responses API
gpt-4o Multimodal flagship, chat completions
gpt-4o-mini Cheap structured output, chat completions
o1 Deep reasoning, no temperature
o3-mini Cheap reasoning, no temperature
See models.md for the full catalog.
EOF
}
if [[ $# -eq 0 && -t 0 ]]; then
usage
exit 1
fi
while [[ $# -gt 0 ]]; do
case "$1" in
--model) MODEL="$2"; shift 2 ;;
--prompt) PROMPT="$2"; shift 2 ;;
--system) SYSTEM="$2"; shift 2 ;;
--temperature) TEMPERATURE="$2"; shift 2 ;;
--max-tokens) MAX_TOKENS="$2"; shift 2 ;;
--reasoning) REASONING="$2"; shift 2 ;;
--json) RAW_JSON=1; shift ;;
-h|--help) usage; exit 0 ;;
*) echo "openai/chat: unknown flag: $1" >&2; usage; exit 1 ;;
esac
done
# stdin fallback for prompt
if [[ -z "${PROMPT}" && ! -t 0 ]]; then
PROMPT="$(cat)"
fi
if [[ -z "${PROMPT}" ]]; then
echo "openai/chat: --prompt (or stdin) is required" >&2
exit 1
fi
require_openai
# ----- pick endpoint family ---------------------------------------------------
USE_RESPONSES_API=0
case "${MODEL}" in
gpt-5|gpt-5-*|o1-pro) USE_RESPONSES_API=1 ;;
esac
if [[ "${USE_RESPONSES_API}" -eq 1 ]]; then
# ----- Responses API (gpt-5 family) ----------------------------------------
# The Responses API uses `input` (string or list) and `instructions` (system prompt).
BODY="$(jq -n \
--arg model "${MODEL}" \
--arg input "${PROMPT}" \
'{model: $model, input: $input}')"
if [[ -n "${SYSTEM}" ]]; then
BODY="$(jq --arg s "${SYSTEM}" '. + {instructions: $s}' <<<"${BODY}")"
fi
if [[ -n "${MAX_TOKENS}" ]]; then
BODY="$(jq --argjson n "${MAX_TOKENS}" '. + {max_output_tokens: $n}' <<<"${BODY}")"
fi
if [[ -n "${REASONING}" ]]; then
BODY="$(jq --arg r "${REASONING}" '. + {reasoning: {effort: $r}}' <<<"${BODY}")"
fi
# temperature is not supported by gpt-5 reasoning models -- silently drop
URL="${OPENAI_API_BASE}/responses"
RESP="$(curl -sS -X POST "${URL}" \
-H "Authorization: Bearer ${OPENAI_API_KEY}" \
-H "Content-Type: application/json" \
--data "${BODY}")"
if jq -e '.error' >/dev/null 2>&1 <<<"${RESP}"; then
echo "openai/chat: API error" >&2
jq '.error' >&2 <<<"${RESP}"
exit 2
fi
if [[ "${RAW_JSON}" -eq 1 ]]; then
printf '%s\n' "${RESP}"
else
# Responses API: output[].content[].text contains the assistant text
jq -r '.output[]?.content[]?.text // empty' <<<"${RESP}"
fi
else
# ----- Chat Completions API (gpt-4o, o1, o3, gpt-4-turbo) -------------------
MSGS="$(jq -n '[]')"
if [[ -n "${SYSTEM}" ]]; then
MSGS="$(jq --arg s "${SYSTEM}" '. + [{role: "system", content: $s}]' <<<"${MSGS}")"
fi
MSGS="$(jq --arg p "${PROMPT}" '. + [{role: "user", content: $p}]' <<<"${MSGS}")"
BODY="$(jq -n \
--arg model "${MODEL}" \
--argjson msgs "${MSGS}" \
'{model: $model, messages: $msgs}')"
# o-series models reject temperature/max_tokens -- only honour for non-reasoning
case "${MODEL}" in
o1*|o3*)
if [[ -n "${MAX_TOKENS}" ]]; then
BODY="$(jq --argjson n "${MAX_TOKENS}" '. + {max_completion_tokens: $n}' <<<"${BODY}")"
fi
if [[ -n "${REASONING}" ]]; then
BODY="$(jq --arg r "${REASONING}" '. + {reasoning_effort: $r}' <<<"${BODY}")"
fi
;;
*)
if [[ -n "${TEMPERATURE}" ]]; then
BODY="$(jq --argjson t "${TEMPERATURE}" '. + {temperature: $t}' <<<"${BODY}")"
fi
if [[ -n "${MAX_TOKENS}" ]]; then
BODY="$(jq --argjson n "${MAX_TOKENS}" '. + {max_tokens: $n}' <<<"${BODY}")"
fi
;;
esac
URL="${OPENAI_API_BASE}/chat/completions"
RESP="$(curl -sS -X POST "${URL}" \
-H "Authorization: Bearer ${OPENAI_API_KEY}" \
-H "Content-Type: application/json" \
--data "${BODY}")"
if jq -e '.error' >/dev/null 2>&1 <<<"${RESP}"; then
echo "openai/chat: API error" >&2
jq '.error' >&2 <<<"${RESP}"
exit 2
fi
if [[ "${RAW_JSON}" -eq 1 ]]; then
printf '%s\n' "${RESP}"
else
jq -r '.choices[0].message.content // empty' <<<"${RESP}"
fi
fi
#!/usr/bin/env bash
# snappy-ai-models/openai/embed.sh
#
# Get embeddings for a string or list of strings.
# Output: full JSON by default; use --vector to print just the first vector
# as one float per line.
#
# Usage:
# ./embed.sh --text "What is Xano?"
# ./embed.sh --text "Hello" --model text-embedding-3-large --dim 1024
# ./embed.sh --file sentences.txt # one sentence per line, returns batch
# ./embed.sh --text "Hello" --vector
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=../lib/auth.sh
. "${SCRIPT_DIR}/../lib/auth.sh"
MODEL="text-embedding-3-large"
TEXT=""
FILE=""
DIM=""
VECTOR=0
usage() {
cat <<'EOF'
Usage: openai/embed.sh [options]
Options:
--model <id> text-embedding-3-large (default) | text-embedding-3-small | text-embedding-ada-002
--text <text> Single string to embed
--file <path> File with one string per line (batch embed)
--dim <n> Truncate to N dimensions (text-embedding-3-* only, default: full)
- large: max 3072
- small: max 1536
--vector Print first vector as one float per line (instead of JSON)
-h, --help Show this help
Either --text or --file is required.
Cost (rough, as of late 2025):
text-embedding-3-large $0.13 / 1M tokens
text-embedding-3-small $0.02 / 1M tokens
text-embedding-ada-002 $0.10 / 1M tokens (legacy -- use 3-small instead)
EOF
}
if [[ $# -eq 0 ]]; then
usage
exit 1
fi
while [[ $# -gt 0 ]]; do
case "$1" in
--model) MODEL="$2"; shift 2 ;;
--text) TEXT="$2"; shift 2 ;;
--file) FILE="$2"; shift 2 ;;
--dim) DIM="$2"; shift 2 ;;
--vector) VECTOR=1; shift ;;
-h|--help) usage; exit 0 ;;
*) echo "openai/embed: unknown flag: $1" >&2; usage; exit 1 ;;
esac
done
if [[ -z "${TEXT}" && -z "${FILE}" ]]; then
echo "openai/embed: --text or --file is required" >&2
exit 1
fi
require_openai
# Build the input array (either single string or list from file)
if [[ -n "${FILE}" ]]; then
if [[ ! -f "${FILE}" ]]; then
echo "openai/embed: file not found: ${FILE}" >&2
exit 1
fi
INPUT="$(jq -R . "${FILE}" | jq -s .)"
else
INPUT="$(jq -n --arg t "${TEXT}" '[$t]')"
fi
BODY="$(jq -n \
--arg model "${MODEL}" \
--argjson input "${INPUT}" \
'{model: $model, input: $input}')"
if [[ -n "${DIM}" ]]; then
BODY="$(jq --argjson d "${DIM}" '. + {dimensions: $d}' <<<"${BODY}")"
fi
URL="${OPENAI_API_BASE}/embeddings"
RESP="$(curl -sS -X POST "${URL}" \
-H "Authorization: Bearer ${OPENAI_API_KEY}" \
-H "Content-Type: application/json" \
--data "${BODY}")"
if jq -e '.error' >/dev/null 2>&1 <<<"${RESP}"; then
echo "openai/embed: API error" >&2
jq '.error' >&2 <<<"${RESP}"
exit 2
fi
if [[ "${VECTOR}" -eq 1 ]]; then
jq -r '.data[0].embedding[]' <<<"${RESP}"
else
printf '%s\n' "${RESP}"
fi
#!/usr/bin/env bash
# snappy-ai-models/openai/embed.sh
#
# Get embeddings for a string or list of strings.
# Output: full JSON by default; use --vector to print just the first vector
# as one float per line.
#
# Usage:
# ./embed.sh --text "What is Xano?"
# ./embed.sh --text "Hello" --model text-embedding-3-large --dim 1024
# ./embed.sh --file sentences.txt # one sentence per line, returns batch
# ./embed.sh --text "Hello" --vector
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=../lib/auth.sh
. "${SCRIPT_DIR}/../lib/auth.sh"
MODEL="text-embedding-3-large"
TEXT=""
FILE=""
DIM=""
VECTOR=0
usage() {
cat <<'EOF'
Usage: openai/embed.sh [options]
Options:
--model <id> text-embedding-3-large (default) | text-embedding-3-small | text-embedding-ada-002
--text <text> Single string to embed
--file <path> File with one string per line (batch embed)
--dim <n> Truncate to N dimensions (text-embedding-3-* only, default: full)
- large: max 3072
- small: max 1536
--vector Print first vector as one float per line (instead of JSON)
-h, --help Show this help
Either --text or --file is required.
Cost (rough, as of late 2025):
text-embedding-3-large $0.13 / 1M tokens
text-embedding-3-small $0.02 / 1M tokens
text-embedding-ada-002 $0.10 / 1M tokens (legacy -- use 3-small instead)
EOF
}
if [[ $# -eq 0 ]]; then
usage
exit 1
fi
while [[ $# -gt 0 ]]; do
case "$1" in
--model) MODEL="$2"; shift 2 ;;
--text) TEXT="$2"; shift 2 ;;
--file) FILE="$2"; shift 2 ;;
--dim) DIM="$2"; shift 2 ;;
--vector) VECTOR=1; shift ;;
-h|--help) usage; exit 0 ;;
*) echo "openai/embed: unknown flag: $1" >&2; usage; exit 1 ;;
esac
done
if [[ -z "${TEXT}" && -z "${FILE}" ]]; then
echo "openai/embed: --text or --file is required" >&2
exit 1
fi
require_openai
# Build the input array (either single string or list from file)
if [[ -n "${FILE}" ]]; then
if [[ ! -f "${FILE}" ]]; then
echo "openai/embed: file not found: ${FILE}" >&2
exit 1
fi
INPUT="$(jq -R . "${FILE}" | jq -s .)"
else
INPUT="$(jq -n --arg t "${TEXT}" '[$t]')"
fi
BODY="$(jq -n \
--arg model "${MODEL}" \
--argjson input "${INPUT}" \
'{model: $model, input: $input}')"
if [[ -n "${DIM}" ]]; then
BODY="$(jq --argjson d "${DIM}" '. + {dimensions: $d}' <<<"${BODY}")"
fi
URL="${OPENAI_API_BASE}/embeddings"
RESP="$(curl -sS -X POST "${URL}" \
-H "Authorization: Bearer ${OPENAI_API_KEY}" \
-H "Content-Type: application/json" \
--data "${BODY}")"
if jq -e '.error' >/dev/null 2>&1 <<<"${RESP}"; then
echo "openai/embed: API error" >&2
jq '.error' >&2 <<<"${RESP}"
exit 2
fi
if [[ "${VECTOR}" -eq 1 ]]; then
jq -r '.data[0].embedding[]' <<<"${RESP}"
else
printf '%s\n' "${RESP}"
fi
#!/usr/bin/env bash
# snappy-ai-models/openai/image.sh
#
# Generate images via OpenAI's image API or fal.ai for gpt-image-2.
# Supports:
# - dall-e-3 legacy, b64 or url, fixed sizes
# - gpt-image-1 newer, returns b64 by default, supports more sizes
# - gpt-image-2 text-to-image via fal.ai — exceptional typography + hyper-realistic props
#
# Usage:
# ./image.sh --model gpt-image-1 --prompt "A red sports car at sunset" --out car.png
# ./image.sh --model dall-e-3 --prompt "A logo" --size 1024x1024 --quality hd
# ./image.sh --model gpt-image-1 --prompt "..." --size 1024x1536 --n 4 --out ./batch
# ./image.sh --model gpt-image-2 --prompt "Hyper-realistic whiteboard with..." --size portrait_4_3 --out board.jpg
#
# gpt-image-2 notes:
# - Routes to fal.ai (https://fal.run/openai/gpt-image-2); requires FAL_API_KEY
# - Returns URL (not b64); script downloads and saves locally
# - --size accepts fal.ai image_size names: square_hd | square | portrait_4_3 (default) |
# portrait_16_9 | landscape_4_3 | landscape_16_9
# - --quality: low | medium | high (default)
# - --ref <path> (repeatable): reference image(s); switches to the /edit endpoint,
# files are sent as base64 data URIs in image_urls (identity/screenshot refs)
# - Best for: whiteboards, infographics, meeting summaries, LinkedIn slides
#
# Output:
# - if --out is a .png/.jpg path: saves single image to that path
# - if --out is a directory: saves N images as 0.jpg, 1.jpg, ...
# - if --json: prints raw API JSON to stdout (no file written)
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=../lib/auth.sh
. "${SCRIPT_DIR}/../lib/auth.sh"
MODEL="gpt-image-1"
PROMPT=""
SIZE=""
QUALITY=""
N=1
OUT=""
RAW_JSON=0
BACKGROUND="" # gpt-image-1: transparent | opaque | auto
STYLE="" # dall-e-3: vivid | natural
REFS=() # gpt-image-2: reference image paths (repeatable --ref)
usage() {
cat <<'EOF'
Usage: openai/image.sh [options]
Options:
--model <id> gpt-image-1 (default), dall-e-3, or gpt-image-2
--prompt <text> Image description (required)
--size <s> gpt-image-1: 1024x1024|1024x1536|1536x1024|auto
dall-e-3: 1024x1024|1024x1792|1792x1024
gpt-image-2: square_hd|square|portrait_4_3|portrait_16_9|landscape_4_3|landscape_16_9
--quality <q> gpt-image-1: low|medium|high|auto
dall-e-3: standard|hd
gpt-image-2: low|medium|high
--background <b> gpt-image-1 only: transparent|opaque|auto
--style <s> dall-e-3 only: vivid|natural
--ref <path> gpt-image-2 only: reference image (repeat for multiple)
--n <n> Number of images (default 1; dall-e-3 max 1; gpt-image-2 max 4)
--out <path> File path (.png/.jpg) or directory for output
--json Print full JSON response instead of saving files
-h, --help Show this help
EOF
}
if [[ $# -eq 0 ]]; then
usage
exit 1
fi
while [[ $# -gt 0 ]]; do
case "$1" in
--model) MODEL="$2"; shift 2 ;;
--prompt) PROMPT="$2"; shift 2 ;;
--size) SIZE="$2"; shift 2 ;;
--quality) QUALITY="$2"; shift 2 ;;
--background) BACKGROUND="$2"; shift 2 ;;
--style) STYLE="$2"; shift 2 ;;
--ref) REFS+=("$2"); shift 2 ;;
--n) N="$2"; shift 2 ;;
--out) OUT="$2"; shift 2 ;;
--json) RAW_JSON=1; shift ;;
-h|--help) usage; exit 0 ;;
*) echo "openai/image: unknown flag: $1" >&2; usage; exit 1 ;;
esac
done
if [[ -z "${PROMPT}" ]]; then
echo "openai/image: --prompt is required" >&2
exit 1
fi
# gpt-image-2 uses fal.ai -- auth loaded inside its case block
if [[ "${MODEL}" != "gpt-image-2" ]]; then
require_openai
if [[ ${#REFS[@]} -gt 0 ]]; then
echo "openai/image: --ref is only supported for gpt-image-2" >&2
exit 1
fi
fi
case "${MODEL}" in
gpt-image-2)
# ── fal.ai path ───────────────────────────────────────────────────────────
require_fal
FAL_BODY="$(jq -n \
--arg prompt "${PROMPT}" \
--argjson n "${N}" \
'{"prompt": $prompt, "num_images": $n}')"
FAL_SIZE="${SIZE:-portrait_4_3}"
FAL_BODY="$(jq --arg s "${FAL_SIZE}" '. + {image_size: $s}' <<<"${FAL_BODY}")"
FAL_QUALITY="${QUALITY:-high}"
FAL_BODY="$(jq --arg q "${FAL_QUALITY}" '. + {quality: $q}' <<<"${FAL_BODY}")"
FAL_URL="https://fal.run/openai/gpt-image-2"
FAL_BODY_FILE="$(mktemp)"
trap 'rm -f "${FAL_BODY_FILE}" "${FAL_BODY_FILE}.uri" "${FAL_BODY_FILE}.new"' EXIT
printf '%s' "${FAL_BODY}" > "${FAL_BODY_FILE}"
if [[ ${#REFS[@]} -gt 0 ]]; then
FAL_URL="https://fal.run/openai/gpt-image-2/edit"
for REF in "${REFS[@]}"; do
if [[ ! -f "${REF}" ]]; then
echo "openai/image: --ref file not found: ${REF}" >&2
exit 1
fi
case "${REF}" in
*.png|*.PNG) REF_MIME="image/png" ;;
*.webp|*.WEBP) REF_MIME="image/webp" ;;
*) REF_MIME="image/jpeg" ;;
esac
{ printf 'data:%s;base64,' "${REF_MIME}"; base64 < "${REF}" | tr -d '\n'; } > "${FAL_BODY_FILE}.uri"
jq --rawfile u "${FAL_BODY_FILE}.uri" '.image_urls = ((.image_urls // []) + [$u])' \
"${FAL_BODY_FILE}" > "${FAL_BODY_FILE}.new"
mv "${FAL_BODY_FILE}.new" "${FAL_BODY_FILE}"
done
fi
RESP="$(curl -sS -X POST "${FAL_URL}" \
-H "Authorization: Key ${FAL_API_KEY}" \
-H "Content-Type: application/json" \
--data @"${FAL_BODY_FILE}")"
if jq -e '.detail // .error' >/dev/null 2>&1 <<<"${RESP}"; then
echo "openai/image: gpt-image-2 fal.ai error" >&2
jq '.detail // .error' >&2 <<<"${RESP}"
exit 2
fi
if [[ "${RAW_JSON}" -eq 1 ]]; then
printf '%s\n' "${RESP}"
exit 0
fi
if [[ -z "${OUT}" ]]; then
echo "openai/image: --out is required when not using --json" >&2
exit 1
fi
COUNT="$(jq '.images | length' <<<"${RESP}")"
if [[ -d "${OUT}" || ( "${COUNT}" -gt 1 && "${OUT}" != *.jpg && "${OUT}" != *.jpeg && "${OUT}" != *.png ) ]]; then
mkdir -p "${OUT}"
for i in $(seq 0 $((COUNT - 1))); do
IMG_URL="$(jq -r ".images[${i}].url" <<<"${RESP}")"
curl -sS -L "${IMG_URL}" -o "${OUT}/${i}.jpg"
echo "wrote ${OUT}/${i}.jpg"
done
else
IMG_URL="$(jq -r '.images[0].url' <<<"${RESP}")"
curl -sS -L "${IMG_URL}" -o "${OUT}"
echo "wrote ${OUT}"
fi
exit 0
;;
dall-e-3|gpt-image-1)
# Build the shared OpenAI request body
BODY="$(jq -n \
--arg model "${MODEL}" \
--arg prompt "${PROMPT}" \
--argjson n "${N}" \
'{model: $model, prompt: $prompt, n: $n}')"
if [[ -n "${SIZE}" ]]; then
BODY="$(jq --arg s "${SIZE}" '. + {size: $s}' <<<"${BODY}")"
fi
if [[ -n "${QUALITY}" ]]; then
BODY="$(jq --arg q "${QUALITY}" '. + {quality: $q}' <<<"${BODY}")"
fi
if [[ "${MODEL}" == "dall-e-3" ]]; then
if [[ -n "${STYLE}" ]]; then
BODY="$(jq --arg s "${STYLE}" '. + {style: $s}' <<<"${BODY}")"
fi
# dall-e-3 returns URL by default; force b64 for consistent file save
BODY="$(jq '. + {response_format: "b64_json"}' <<<"${BODY}")"
else
if [[ -n "${BACKGROUND}" ]]; then
BODY="$(jq --arg b "${BACKGROUND}" '. + {background: $b}' <<<"${BODY}")"
fi
# gpt-image-1 always returns b64_json -- no response_format needed
fi
;;
*)
echo "openai/image: unknown model: ${MODEL}" >&2
exit 1
;;
esac
URL="${OPENAI_API_BASE}/images/generations"
RESP="$(curl -sS -X POST "${URL}" \
-H "Authorization: Bearer ${OPENAI_API_KEY}" \
-H "Content-Type: application/json" \
--data "${BODY}")"
if jq -e '.error' >/dev/null 2>&1 <<<"${RESP}"; then
echo "openai/image: API error" >&2
jq '.error' >&2 <<<"${RESP}"
exit 2
fi
if [[ "${RAW_JSON}" -eq 1 ]]; then
printf '%s\n' "${RESP}"
exit 0
fi
if [[ -z "${OUT}" ]]; then
echo "openai/image: --out is required when not using --json" >&2
exit 1
fi
# Save b64 images to disk
COUNT="$(jq '.data | length' <<<"${RESP}")"
if [[ -d "${OUT}" || ( "${COUNT}" -gt 1 && "${OUT}" != *.png ) ]]; then
mkdir -p "${OUT}"
for i in $(seq 0 $((COUNT - 1))); do
jq -r ".data[${i}].b64_json" <<<"${RESP}" | base64 -d > "${OUT}/${i}.png"
echo "wrote ${OUT}/${i}.png"
done
else
jq -r '.data[0].b64_json' <<<"${RESP}" | base64 -d > "${OUT}"
echo "wrote ${OUT}"
fi
#!/usr/bin/env bash
# snappy-ai-models/openai/image.sh
#
# Generate images via OpenAI's image API or fal.ai for gpt-image-2.
# Supports:
# - dall-e-3 legacy, b64 or url, fixed sizes
# - gpt-image-1 newer, returns b64 by default, supports more sizes
# - gpt-image-2 text-to-image via fal.ai — exceptional typography + hyper-realistic props
#
# Usage:
# ./image.sh --model gpt-image-1 --prompt "A red sports car at sunset" --out car.png
# ./image.sh --model dall-e-3 --prompt "A logo" --size 1024x1024 --quality hd
# ./image.sh --model gpt-image-1 --prompt "..." --size 1024x1536 --n 4 --out ./batch
# ./image.sh --model gpt-image-2 --prompt "Hyper-realistic whiteboard with..." --size portrait_4_3 --out board.jpg
#
# gpt-image-2 notes:
# - Routes to fal.ai (https://fal.run/openai/gpt-image-2); requires FAL_API_KEY
# - Returns URL (not b64); script downloads and saves locally
# - --size accepts fal.ai image_size names: square_hd | square | portrait_4_3 (default) |
# portrait_16_9 | landscape_4_3 | landscape_16_9
# - --quality: low | medium | high (default)
# - --ref <path> (repeatable): reference image(s); switches to the /edit endpoint,
# files are sent as base64 data URIs in image_urls (identity/screenshot refs)
# - Best for: whiteboards, infographics, meeting summaries, LinkedIn slides
#
# Output:
# - if --out is a .png/.jpg path: saves single image to that path
# - if --out is a directory: saves N images as 0.jpg, 1.jpg, ...
# - if --json: prints raw API JSON to stdout (no file written)
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=../lib/auth.sh
. "${SCRIPT_DIR}/../lib/auth.sh"
MODEL="gpt-image-1"
PROMPT=""
SIZE=""
QUALITY=""
N=1
OUT=""
RAW_JSON=0
BACKGROUND="" # gpt-image-1: transparent | opaque | auto
STYLE="" # dall-e-3: vivid | natural
REFS=() # gpt-image-2: reference image paths (repeatable --ref)
usage() {
cat <<'EOF'
Usage: openai/image.sh [options]
Options:
--model <id> gpt-image-1 (default), dall-e-3, or gpt-image-2
--prompt <text> Image description (required)
--size <s> gpt-image-1: 1024x1024|1024x1536|1536x1024|auto
dall-e-3: 1024x1024|1024x1792|1792x1024
gpt-image-2: square_hd|square|portrait_4_3|portrait_16_9|landscape_4_3|landscape_16_9
--quality <q> gpt-image-1: low|medium|high|auto
dall-e-3: standard|hd
gpt-image-2: low|medium|high
--background <b> gpt-image-1 only: transparent|opaque|auto
--style <s> dall-e-3 only: vivid|natural
--ref <path> gpt-image-2 only: reference image (repeat for multiple)
--n <n> Number of images (default 1; dall-e-3 max 1; gpt-image-2 max 4)
--out <path> File path (.png/.jpg) or directory for output
--json Print full JSON response instead of saving files
-h, --help Show this help
EOF
}
if [[ $# -eq 0 ]]; then
usage
exit 1
fi
while [[ $# -gt 0 ]]; do
case "$1" in
--model) MODEL="$2"; shift 2 ;;
--prompt) PROMPT="$2"; shift 2 ;;
--size) SIZE="$2"; shift 2 ;;
--quality) QUALITY="$2"; shift 2 ;;
--background) BACKGROUND="$2"; shift 2 ;;
--style) STYLE="$2"; shift 2 ;;
--ref) REFS+=("$2"); shift 2 ;;
--n) N="$2"; shift 2 ;;
--out) OUT="$2"; shift 2 ;;
--json) RAW_JSON=1; shift ;;
-h|--help) usage; exit 0 ;;
*) echo "openai/image: unknown flag: $1" >&2; usage; exit 1 ;;
esac
done
if [[ -z "${PROMPT}" ]]; then
echo "openai/image: --prompt is required" >&2
exit 1
fi
# gpt-image-2 uses fal.ai -- auth loaded inside its case block
if [[ "${MODEL}" != "gpt-image-2" ]]; then
require_openai
if [[ ${#REFS[@]} -gt 0 ]]; then
echo "openai/image: --ref is only supported for gpt-image-2" >&2
exit 1
fi
fi
case "${MODEL}" in
gpt-image-2)
# ── fal.ai path ───────────────────────────────────────────────────────────
require_fal
FAL_BODY="$(jq -n \
--arg prompt "${PROMPT}" \
--argjson n "${N}" \
'{"prompt": $prompt, "num_images": $n}')"
FAL_SIZE="${SIZE:-portrait_4_3}"
FAL_BODY="$(jq --arg s "${FAL_SIZE}" '. + {image_size: $s}' <<<"${FAL_BODY}")"
FAL_QUALITY="${QUALITY:-high}"
FAL_BODY="$(jq --arg q "${FAL_QUALITY}" '. + {quality: $q}' <<<"${FAL_BODY}")"
FAL_URL="https://fal.run/openai/gpt-image-2"
FAL_BODY_FILE="$(mktemp)"
trap 'rm -f "${FAL_BODY_FILE}" "${FAL_BODY_FILE}.uri" "${FAL_BODY_FILE}.new"' EXIT
printf '%s' "${FAL_BODY}" > "${FAL_BODY_FILE}"
if [[ ${#REFS[@]} -gt 0 ]]; then
FAL_URL="https://fal.run/openai/gpt-image-2/edit"
for REF in "${REFS[@]}"; do
if [[ ! -f "${REF}" ]]; then
echo "openai/image: --ref file not found: ${REF}" >&2
exit 1
fi
case "${REF}" in
*.png|*.PNG) REF_MIME="image/png" ;;
*.webp|*.WEBP) REF_MIME="image/webp" ;;
*) REF_MIME="image/jpeg" ;;
esac
{ printf 'data:%s;base64,' "${REF_MIME}"; base64 < "${REF}" | tr -d '\n'; } > "${FAL_BODY_FILE}.uri"
jq --rawfile u "${FAL_BODY_FILE}.uri" '.image_urls = ((.image_urls // []) + [$u])' \
"${FAL_BODY_FILE}" > "${FAL_BODY_FILE}.new"
mv "${FAL_BODY_FILE}.new" "${FAL_BODY_FILE}"
done
fi
RESP="$(curl -sS -X POST "${FAL_URL}" \
-H "Authorization: Key ${FAL_API_KEY}" \
-H "Content-Type: application/json" \
--data @"${FAL_BODY_FILE}")"
if jq -e '.detail // .error' >/dev/null 2>&1 <<<"${RESP}"; then
echo "openai/image: gpt-image-2 fal.ai error" >&2
jq '.detail // .error' >&2 <<<"${RESP}"
exit 2
fi
if [[ "${RAW_JSON}" -eq 1 ]]; then
printf '%s\n' "${RESP}"
exit 0
fi
if [[ -z "${OUT}" ]]; then
echo "openai/image: --out is required when not using --json" >&2
exit 1
fi
COUNT="$(jq '.images | length' <<<"${RESP}")"
if [[ -d "${OUT}" || ( "${COUNT}" -gt 1 && "${OUT}" != *.jpg && "${OUT}" != *.jpeg && "${OUT}" != *.png ) ]]; then
mkdir -p "${OUT}"
for i in $(seq 0 $((COUNT - 1))); do
IMG_URL="$(jq -r ".images[${i}].url" <<<"${RESP}")"
curl -sS -L "${IMG_URL}" -o "${OUT}/${i}.jpg"
echo "wrote ${OUT}/${i}.jpg"
done
else
IMG_URL="$(jq -r '.images[0].url' <<<"${RESP}")"
curl -sS -L "${IMG_URL}" -o "${OUT}"
echo "wrote ${OUT}"
fi
exit 0
;;
dall-e-3|gpt-image-1)
# Build the shared OpenAI request body
BODY="$(jq -n \
--arg model "${MODEL}" \
--arg prompt "${PROMPT}" \
--argjson n "${N}" \
'{model: $model, prompt: $prompt, n: $n}')"
if [[ -n "${SIZE}" ]]; then
BODY="$(jq --arg s "${SIZE}" '. + {size: $s}' <<<"${BODY}")"
fi
if [[ -n "${QUALITY}" ]]; then
BODY="$(jq --arg q "${QUALITY}" '. + {quality: $q}' <<<"${BODY}")"
fi
if [[ "${MODEL}" == "dall-e-3" ]]; then
if [[ -n "${STYLE}" ]]; then
BODY="$(jq --arg s "${STYLE}" '. + {style: $s}' <<<"${BODY}")"
fi
# dall-e-3 returns URL by default; force b64 for consistent file save
BODY="$(jq '. + {response_format: "b64_json"}' <<<"${BODY}")"
else
if [[ -n "${BACKGROUND}" ]]; then
BODY="$(jq --arg b "${BACKGROUND}" '. + {background: $b}' <<<"${BODY}")"
fi
# gpt-image-1 always returns b64_json -- no response_format needed
fi
;;
*)
echo "openai/image: unknown model: ${MODEL}" >&2
exit 1
;;
esac
URL="${OPENAI_API_BASE}/images/generations"
RESP="$(curl -sS -X POST "${URL}" \
-H "Authorization: Bearer ${OPENAI_API_KEY}" \
-H "Content-Type: application/json" \
--data "${BODY}")"
if jq -e '.error' >/dev/null 2>&1 <<<"${RESP}"; then
echo "openai/image: API error" >&2
jq '.error' >&2 <<<"${RESP}"
exit 2
fi
if [[ "${RAW_JSON}" -eq 1 ]]; then
printf '%s\n' "${RESP}"
exit 0
fi
if [[ -z "${OUT}" ]]; then
echo "openai/image: --out is required when not using --json" >&2
exit 1
fi
# Save b64 images to disk
COUNT="$(jq '.data | length' <<<"${RESP}")"
if [[ -d "${OUT}" || ( "${COUNT}" -gt 1 && "${OUT}" != *.png ) ]]; then
mkdir -p "${OUT}"
for i in $(seq 0 $((COUNT - 1))); do
jq -r ".data[${i}].b64_json" <<<"${RESP}" | base64 -d > "${OUT}/${i}.png"
echo "wrote ${OUT}/${i}.png"
done
else
jq -r '.data[0].b64_json' <<<"${RESP}" | base64 -d > "${OUT}"
echo "wrote ${OUT}"
fi
Replicate is pay-per-second hosted inference for open-source models. Use this when you want models that aren't first-party at OpenAI/Anthropic/Google: Flux for images, SDXL, Llava, Whisper variants, RVC voice clones, Stable Video Diffusion, etc.
|fact|implication
|----|-----------
|predictions are async|POST starts a job, returns id with status: "starting". You MUST poll GET /v1/predictions/{id} until terminal status.
|terminal statuses|succeeded, failed, canceled. Anything else (starting, processing) means keep polling.
|model versions are pinned by hash|black-forest-labs/flux-pro resolves to the latest version automatically when you POST to /v1/models/{owner}/{name}/predictions. For reproducibility, use --version <hash> against /v1/predictions.
|outputs are URLs (mostly)|Image/audio/video models return short-lived URLs (~24h). Download what you keep.
|local files cannot be uploaded inline|Use the file upload API first (POST /v1/files with multipart) and pass the returned URL as input.
|cost is per-second of GPU time|Cheap models (~$0.0011/img for flux-schnell) to expensive (~$0.05/img for flux-pro-1.1-ultra). Track via Replicate dashboard.
|webhooks for fire-and-forget|Pass webhook + webhook_events_filter: ["completed"] and skip polling.
| Script | Purpose |
|---|---|
run.sh |
Generic prediction runner -- polls until done |
flux.sh |
Flux image generation wrapper (schnell/dev/pro/pro-1.1/ultra) |
whisper.sh |
Whisper transcription (standard/turbo/whisperx) |
flux.sh and whisper.sh shell out to run.sh so the polling loop only lives in one place.
|slug|use|cost_rough|notes
|----|---|----------|-----
|black-forest-labs/flux-schnell|Quick first draft|$0.003/img|4 steps, fastest, good enough
|black-forest-labs/flux-dev|Higher quality dev|$0.025/img|28 steps, more control
|black-forest-labs/flux-pro|Production quality|$0.055/img|Best non-ultra Flux
|black-forest-labs/flux-1.1-pro|Newest pro|$0.04/img|Better than flux-pro
|black-forest-labs/flux-1.1-pro-ultra|Highest res Flux|$0.06/img|2K+ outputs
|stability-ai/stable-diffusion-3.5-large|SD3.5|$0.035/img|Open weights flagship
|recraft-ai/recraft-v3|Vector + text|$0.04/img|Best at logos/typography
|ideogram-ai/ideogram-v2|Text-in-image|$0.08/img|Reads/writes text in images well
|slug|use|notes
|----|---|-----
|openai/whisper|Standard transcription|whisper-large-v2 hosted
|vaibhavs10/incredibly-fast-whisper|Faster transcription|Batched, ~5x faster than openai/whisper
|daanelson/whisperx|Diarization + word timestamps|Best for meeting transcripts
|suno-ai/bark|Text-to-speech|Voice variety, slower than tts-1
|lucataco/xtts-v2|Voice cloning TTS|Clone from reference audio
|slug|use|notes
|----|---|-----
|stability-ai/stable-video-diffusion|Image -> video|3-4 second clips
|tencent/hunyuan-video|Text -> video|Newer, longer outputs
|fofr/animate-diff|Animated stylized video|Good for stylized loops
|slug|use|notes
|----|---|-----
|meta/llama-2-70b-chat|Llama 2|Use openrouter for newer Llamas
|mistralai/mistral-7b-instruct-v0.2|Mistral 7B|Use openrouter for Mistral Large
For LLMs, prefer snappy-openrouter -- it's faster and cheaper unless you specifically need a model only on Replicate.
|slug|use|notes
|----|---|-----
|yorickvp/llava-13b|Vision -> text|Image captioning, VQA
|tencentarc/photomaker|Face-preserving image gen|Subject-driven generation
|fofr/face-to-many|Stylize a face into N styles|Useful for headshot variants
bash. "$(dirname "$0")/../lib/auth.sh"
require_replicate
# now $REPLICATE_API_TOKEN is set
The token comes from snappy-settings/scripts/load-env.sh (reads .env.cache). Override with an env var if needed.
bash# Cheap image draft
./flux.sh --prompt "Snappy product hero, minimalist" --model schnell --out hero.webp
# Production hero image
./flux.sh --prompt "..." --model pro-1.1-ultra --aspect 16:9 --out hero.png
# Transcribe a long meeting recording with diarization
./whisper.sh --upload ./meeting.mp3 --model whisperx --language en > transcript.txt
# Run any model directly
./run.sh --model "stability-ai/stable-video-diffusion" \
--input '{"input_image":"https://example.com/still.jpg"}' \
--json
# Fire-and-forget with webhook
./run.sh --model "black-forest-labs/flux-pro" \
--input '{"prompt":"..."}' \
--webhook "https://snappy.ai/api/replicate-webhook"
../openai/snappy-geminisnappy-openrouter (cheaper, faster)../anthropic/# Replicate -- Hosted Open-Source Model Registry
Replicate is pay-per-second hosted inference for open-source models. Use this when you want models that aren't first-party at OpenAI/Anthropic/Google: Flux for images, SDXL, Llava, Whisper variants, RVC voice clones, Stable Video Diffusion, etc.
## Critical Replicate Behavior
|fact|implication
|----|-----------
|predictions are async|POST starts a job, returns id with `status: "starting"`. You MUST poll `GET /v1/predictions/{id}` until terminal status.
|terminal statuses|`succeeded`, `failed`, `canceled`. Anything else (`starting`, `processing`) means keep polling.
|model versions are pinned by hash|`black-forest-labs/flux-pro` resolves to the latest version automatically when you POST to `/v1/models/{owner}/{name}/predictions`. For reproducibility, use `--version <hash>` against `/v1/predictions`.
|outputs are URLs (mostly)|Image/audio/video models return short-lived URLs (~24h). Download what you keep.
|local files cannot be uploaded inline|Use the file upload API first (`POST /v1/files` with multipart) and pass the returned URL as input.
|cost is per-second of GPU time|Cheap models (~$0.0011/img for flux-schnell) to expensive (~$0.05/img for flux-pro-1.1-ultra). Track via Replicate dashboard.
|webhooks for fire-and-forget|Pass `webhook` + `webhook_events_filter: ["completed"]` and skip polling.
## Scripts
| Script | Purpose |
|--------|---------|
| `run.sh` | Generic prediction runner -- polls until done |
| `flux.sh` | Flux image generation wrapper (schnell/dev/pro/pro-1.1/ultra) |
| `whisper.sh` | Whisper transcription (standard/turbo/whisperx) |
`flux.sh` and `whisper.sh` shell out to `run.sh` so the polling loop only lives in one place.
## Model Registry (curated)
### Image generation
|slug|use|cost_rough|notes
|----|---|----------|-----
|black-forest-labs/flux-schnell|Quick first draft|$0.003/img|4 steps, fastest, good enough
|black-forest-labs/flux-dev|Higher quality dev|$0.025/img|28 steps, more control
|black-forest-labs/flux-pro|Production quality|$0.055/img|Best non-ultra Flux
|black-forest-labs/flux-1.1-pro|Newest pro|$0.04/img|Better than flux-pro
|black-forest-labs/flux-1.1-pro-ultra|Highest res Flux|$0.06/img|2K+ outputs
|stability-ai/stable-diffusion-3.5-large|SD3.5|$0.035/img|Open weights flagship
|recraft-ai/recraft-v3|Vector + text|$0.04/img|Best at logos/typography
|ideogram-ai/ideogram-v2|Text-in-image|$0.08/img|Reads/writes text in images well
### Audio / speech
|slug|use|notes
|----|---|-----
|openai/whisper|Standard transcription|whisper-large-v2 hosted
|vaibhavs10/incredibly-fast-whisper|Faster transcription|Batched, ~5x faster than openai/whisper
|daanelson/whisperx|Diarization + word timestamps|Best for meeting transcripts
|suno-ai/bark|Text-to-speech|Voice variety, slower than tts-1
|lucataco/xtts-v2|Voice cloning TTS|Clone from reference audio
### Video
|slug|use|notes
|----|---|-----
|stability-ai/stable-video-diffusion|Image -> video|3-4 second clips
|tencent/hunyuan-video|Text -> video|Newer, longer outputs
|fofr/animate-diff|Animated stylized video|Good for stylized loops
### LLM (text -- but prefer OpenRouter)
|slug|use|notes
|----|---|-----
|meta/llama-2-70b-chat|Llama 2|Use openrouter for newer Llamas
|mistralai/mistral-7b-instruct-v0.2|Mistral 7B|Use openrouter for Mistral Large
For LLMs, prefer `snappy-openrouter` -- it's faster and cheaper unless you specifically need a model only on Replicate.
### Misc
|slug|use|notes
|----|---|-----
|yorickvp/llava-13b|Vision -> text|Image captioning, VQA
|tencentarc/photomaker|Face-preserving image gen|Subject-driven generation
|fofr/face-to-many|Stylize a face into N styles|Useful for headshot variants
## Authentication
```bash
. "$(dirname "$0")/../lib/auth.sh"
require_replicate
# now $REPLICATE_API_TOKEN is set
```
The token comes from `snappy-settings/scripts/load-env.sh` (reads `.env.cache`). Override with an env var if needed.
## Common Patterns
```bash
# Cheap image draft
./flux.sh --prompt "Snappy product hero, minimalist" --model schnell --out hero.webp
# Production hero image
./flux.sh --prompt "..." --model pro-1.1-ultra --aspect 16:9 --out hero.png
# Transcribe a long meeting recording with diarization
./whisper.sh --upload ./meeting.mp3 --model whisperx --language en > transcript.txt
# Run any model directly
./run.sh --model "stability-ai/stable-video-diffusion" \
--input '{"input_image":"https://example.com/still.jpg"}' \
--json
# Fire-and-forget with webhook
./run.sh --model "black-forest-labs/flux-pro" \
--input '{"prompt":"..."}' \
--webhook "https://snappy.ai/api/replicate-webhook"
```
## When NOT To Use This
- For first-party OpenAI image/audio -> use `../openai/`
- For Gemini multimodal -> use `snappy-gemini`
- For LLM completions -> use `snappy-openrouter` (cheaper, faster)
- For Anthropic Claude -> use `../anthropic/`
#!/usr/bin/env bash
# snappy-ai-models/replicate/flux.sh
#
# Generate images via Black Forest Labs Flux models on Replicate.
# Wraps run.sh with sensible Flux defaults and downloads the result image.
#
# Usage:
# ./flux.sh --prompt "a futuristic city at sunset"
# ./flux.sh --prompt "..." --model schnell --aspect 16:9 --out city.webp
# ./flux.sh --prompt "..." --model pro --steps 50 --out hero.png
# ./flux.sh --prompt "..." --model dev --seed 42 --out reproducible.png
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
RUN_SH="${SCRIPT_DIR}/run.sh"
if [[ ! -x "${RUN_SH}" ]]; then
chmod +x "${RUN_SH}" 2>/dev/null || true
fi
PROMPT=""
MODEL_VARIANT="schnell" # schnell|dev|pro|pro-1.1|pro-1.1-ultra
ASPECT="1:1"
SEED=""
STEPS=""
OUT=""
OUTPUT_FORMAT="webp"
OUTPUT_QUALITY=90
SAFETY_TOLERANCE=2
RAW_JSON=0
usage() {
cat <<'EOF'
Usage: replicate/flux.sh [options]
Options:
--prompt <text> Image prompt (required)
--model <variant> schnell (default, fastest, free) | dev | pro | pro-1.1 | pro-1.1-ultra
--aspect <ratio> 1:1 (default) | 16:9 | 9:16 | 4:3 | 3:4 | 21:9 | 9:21
--seed <n> Reproducible seed
--steps <n> Number of inference steps (model-dependent)
--safety <0..6> Safety tolerance (pro models only, default 2)
--format <fmt> webp (default) | jpg | png
--quality <0..100> Output quality (default 90)
--out <path> Save image to this path (otherwise prints URL)
--json Print full JSON response
-h, --help Show this help
Model slug mapping:
schnell -> black-forest-labs/flux-schnell
dev -> black-forest-labs/flux-dev
pro -> black-forest-labs/flux-pro
pro-1.1 -> black-forest-labs/flux-1.1-pro
pro-1.1-ultra -> black-forest-labs/flux-1.1-pro-ultra
EOF
}
if [[ $# -eq 0 ]]; then
usage
exit 1
fi
while [[ $# -gt 0 ]]; do
case "$1" in
--prompt) PROMPT="$2"; shift 2 ;;
--model) MODEL_VARIANT="$2"; shift 2 ;;
--aspect) ASPECT="$2"; shift 2 ;;
--seed) SEED="$2"; shift 2 ;;
--steps) STEPS="$2"; shift 2 ;;
--safety) SAFETY_TOLERANCE="$2"; shift 2 ;;
--format) OUTPUT_FORMAT="$2"; shift 2 ;;
--quality) OUTPUT_QUALITY="$2"; shift 2 ;;
--out) OUT="$2"; shift 2 ;;
--json) RAW_JSON=1; shift ;;
-h|--help) usage; exit 0 ;;
*) echo "replicate/flux: unknown flag: $1" >&2; usage; exit 1 ;;
esac
done
if [[ -z "${PROMPT}" ]]; then
echo "replicate/flux: --prompt is required" >&2
exit 1
fi
case "${MODEL_VARIANT}" in
schnell) MODEL_SLUG="black-forest-labs/flux-schnell" ;;
dev) MODEL_SLUG="black-forest-labs/flux-dev" ;;
pro) MODEL_SLUG="black-forest-labs/flux-pro" ;;
pro-1.1) MODEL_SLUG="black-forest-labs/flux-1.1-pro" ;;
pro-1.1-ultra) MODEL_SLUG="black-forest-labs/flux-1.1-pro-ultra" ;;
*) echo "replicate/flux: unknown model variant: ${MODEL_VARIANT}" >&2; exit 1 ;;
esac
INPUT="$(jq -n \
--arg prompt "${PROMPT}" \
--arg aspect "${ASPECT}" \
--arg fmt "${OUTPUT_FORMAT}" \
--argjson quality "${OUTPUT_QUALITY}" \
--argjson safety "${SAFETY_TOLERANCE}" \
'{prompt: $prompt, aspect_ratio: $aspect, output_format: $fmt, output_quality: $quality, safety_tolerance: $safety}')"
if [[ -n "${SEED}" ]]; then
INPUT="$(jq --argjson s "${SEED}" '. + {seed: $s}' <<<"${INPUT}")"
fi
if [[ -n "${STEPS}" ]]; then
INPUT="$(jq --argjson s "${STEPS}" '. + {num_inference_steps: $s}' <<<"${INPUT}")"
fi
if [[ "${RAW_JSON}" -eq 1 ]]; then
bash "${RUN_SH}" --model "${MODEL_SLUG}" --input "${INPUT}" --json
exit 0
fi
# Default: get URL(s) and optionally download
URLS="$(bash "${RUN_SH}" --model "${MODEL_SLUG}" --input "${INPUT}")"
if [[ -z "${OUT}" ]]; then
printf '%s\n' "${URLS}"
exit 0
fi
# Download first URL to OUT path
FIRST_URL="$(echo "${URLS}" | head -n1)"
if [[ -z "${FIRST_URL}" ]]; then
echo "replicate/flux: no output URL returned" >&2
exit 2
fi
curl -sS -L -o "${OUT}" "${FIRST_URL}"
echo "wrote ${OUT}"
#!/usr/bin/env bash
# snappy-ai-models/replicate/flux.sh
#
# Generate images via Black Forest Labs Flux models on Replicate.
# Wraps run.sh with sensible Flux defaults and downloads the result image.
#
# Usage:
# ./flux.sh --prompt "a futuristic city at sunset"
# ./flux.sh --prompt "..." --model schnell --aspect 16:9 --out city.webp
# ./flux.sh --prompt "..." --model pro --steps 50 --out hero.png
# ./flux.sh --prompt "..." --model dev --seed 42 --out reproducible.png
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
RUN_SH="${SCRIPT_DIR}/run.sh"
if [[ ! -x "${RUN_SH}" ]]; then
chmod +x "${RUN_SH}" 2>/dev/null || true
fi
PROMPT=""
MODEL_VARIANT="schnell" # schnell|dev|pro|pro-1.1|pro-1.1-ultra
ASPECT="1:1"
SEED=""
STEPS=""
OUT=""
OUTPUT_FORMAT="webp"
OUTPUT_QUALITY=90
SAFETY_TOLERANCE=2
RAW_JSON=0
usage() {
cat <<'EOF'
Usage: replicate/flux.sh [options]
Options:
--prompt <text> Image prompt (required)
--model <variant> schnell (default, fastest, free) | dev | pro | pro-1.1 | pro-1.1-ultra
--aspect <ratio> 1:1 (default) | 16:9 | 9:16 | 4:3 | 3:4 | 21:9 | 9:21
--seed <n> Reproducible seed
--steps <n> Number of inference steps (model-dependent)
--safety <0..6> Safety tolerance (pro models only, default 2)
--format <fmt> webp (default) | jpg | png
--quality <0..100> Output quality (default 90)
--out <path> Save image to this path (otherwise prints URL)
--json Print full JSON response
-h, --help Show this help
Model slug mapping:
schnell -> black-forest-labs/flux-schnell
dev -> black-forest-labs/flux-dev
pro -> black-forest-labs/flux-pro
pro-1.1 -> black-forest-labs/flux-1.1-pro
pro-1.1-ultra -> black-forest-labs/flux-1.1-pro-ultra
EOF
}
if [[ $# -eq 0 ]]; then
usage
exit 1
fi
while [[ $# -gt 0 ]]; do
case "$1" in
--prompt) PROMPT="$2"; shift 2 ;;
--model) MODEL_VARIANT="$2"; shift 2 ;;
--aspect) ASPECT="$2"; shift 2 ;;
--seed) SEED="$2"; shift 2 ;;
--steps) STEPS="$2"; shift 2 ;;
--safety) SAFETY_TOLERANCE="$2"; shift 2 ;;
--format) OUTPUT_FORMAT="$2"; shift 2 ;;
--quality) OUTPUT_QUALITY="$2"; shift 2 ;;
--out) OUT="$2"; shift 2 ;;
--json) RAW_JSON=1; shift ;;
-h|--help) usage; exit 0 ;;
*) echo "replicate/flux: unknown flag: $1" >&2; usage; exit 1 ;;
esac
done
if [[ -z "${PROMPT}" ]]; then
echo "replicate/flux: --prompt is required" >&2
exit 1
fi
case "${MODEL_VARIANT}" in
schnell) MODEL_SLUG="black-forest-labs/flux-schnell" ;;
dev) MODEL_SLUG="black-forest-labs/flux-dev" ;;
pro) MODEL_SLUG="black-forest-labs/flux-pro" ;;
pro-1.1) MODEL_SLUG="black-forest-labs/flux-1.1-pro" ;;
pro-1.1-ultra) MODEL_SLUG="black-forest-labs/flux-1.1-pro-ultra" ;;
*) echo "replicate/flux: unknown model variant: ${MODEL_VARIANT}" >&2; exit 1 ;;
esac
INPUT="$(jq -n \
--arg prompt "${PROMPT}" \
--arg aspect "${ASPECT}" \
--arg fmt "${OUTPUT_FORMAT}" \
--argjson quality "${OUTPUT_QUALITY}" \
--argjson safety "${SAFETY_TOLERANCE}" \
'{prompt: $prompt, aspect_ratio: $aspect, output_format: $fmt, output_quality: $quality, safety_tolerance: $safety}')"
if [[ -n "${SEED}" ]]; then
INPUT="$(jq --argjson s "${SEED}" '. + {seed: $s}' <<<"${INPUT}")"
fi
if [[ -n "${STEPS}" ]]; then
INPUT="$(jq --argjson s "${STEPS}" '. + {num_inference_steps: $s}' <<<"${INPUT}")"
fi
if [[ "${RAW_JSON}" -eq 1 ]]; then
bash "${RUN_SH}" --model "${MODEL_SLUG}" --input "${INPUT}" --json
exit 0
fi
# Default: get URL(s) and optionally download
URLS="$(bash "${RUN_SH}" --model "${MODEL_SLUG}" --input "${INPUT}")"
if [[ -z "${OUT}" ]]; then
printf '%s\n' "${URLS}"
exit 0
fi
# Download first URL to OUT path
FIRST_URL="$(echo "${URLS}" | head -n1)"
if [[ -z "${FIRST_URL}" ]]; then
echo "replicate/flux: no output URL returned" >&2
exit 2
fi
curl -sS -L -o "${OUT}" "${FIRST_URL}"
echo "wrote ${OUT}"
#!/usr/bin/env bash
# snappy-ai-models/replicate/run.sh
#
# Generic Replicate prediction runner. Replicate is async -- POST starts the
# prediction and returns an id; you must poll GET /v1/predictions/{id} until
# status is `succeeded` or `failed`. This script handles the polling loop
# transparently and returns the final output.
#
# Usage:
# ./run.sh --model "black-forest-labs/flux-schnell" \
# --input '{"prompt":"a futuristic city"}'
#
# ./run.sh --version "model-version-hash" \
# --input '{"prompt":"hi","max_tokens":100}'
#
# ./run.sh --model "openai/whisper" \
# --input '{"audio":"https://example.com/file.mp3"}' \
# --json
#
# Output (default): plain text or URL(s) of result, one per line.
# Output (--json): full prediction JSON.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=../lib/auth.sh
. "${SCRIPT_DIR}/../lib/auth.sh"
MODEL=""
VERSION=""
INPUT=""
INPUT_FILE=""
TIMEOUT=600 # max seconds to wait
POLL_INTERVAL=2 # seconds between polls
RAW_JSON=0
WEBHOOK=""
usage() {
cat <<'EOF'
Usage: replicate/run.sh [options]
Options:
--model <owner/name> Replicate model slug (uses official models endpoint, latest version)
--version <hash> Specific model version hash (overrides --model)
--input <json> JSON object of input parameters
--input-file <path> File containing JSON input
--timeout <seconds> Max polling wait (default 600)
--poll-interval <s> Seconds between polls (default 2)
--webhook <url> Webhook URL -- fire-and-forget mode (no polling)
--json Print full JSON response (default: extracted output)
-h, --help Show this help
Either --model or --version is required, plus --input or --input-file.
Endpoints used:
POST https://api.replicate.com/v1/models/{owner}/{name}/predictions (with --model)
POST https://api.replicate.com/v1/predictions (with --version)
GET https://api.replicate.com/v1/predictions/{id} (poll)
Examples:
./run.sh --model "black-forest-labs/flux-schnell" \
--input '{"prompt":"a cat in space","aspect_ratio":"16:9"}'
./run.sh --model "meta/llama-2-70b-chat" \
--input '{"prompt":"Explain quantum tunneling","max_new_tokens":500}'
EOF
}
if [[ $# -eq 0 ]]; then
usage
exit 1
fi
while [[ $# -gt 0 ]]; do
case "$1" in
--model) MODEL="$2"; shift 2 ;;
--version) VERSION="$2"; shift 2 ;;
--input) INPUT="$2"; shift 2 ;;
--input-file) INPUT_FILE="$2"; shift 2 ;;
--timeout) TIMEOUT="$2"; shift 2 ;;
--poll-interval) POLL_INTERVAL="$2"; shift 2 ;;
--webhook) WEBHOOK="$2"; shift 2 ;;
--json) RAW_JSON=1; shift ;;
-h|--help) usage; exit 0 ;;
*) echo "replicate/run: unknown flag: $1" >&2; usage; exit 1 ;;
esac
done
if [[ -z "${MODEL}" && -z "${VERSION}" ]]; then
echo "replicate/run: --model or --version is required" >&2
exit 1
fi
if [[ -n "${INPUT_FILE}" ]]; then
if [[ ! -f "${INPUT_FILE}" ]]; then
echo "replicate/run: input file not found: ${INPUT_FILE}" >&2
exit 1
fi
INPUT="$(cat "${INPUT_FILE}")"
fi
if [[ -z "${INPUT}" ]]; then
echo "replicate/run: --input or --input-file is required" >&2
exit 1
fi
# Validate input is JSON
if ! jq -e . >/dev/null 2>&1 <<<"${INPUT}"; then
echo "replicate/run: --input must be valid JSON" >&2
exit 1
fi
require_replicate
# ----- Build request body -----------------------------------------------------
if [[ -n "${VERSION}" ]]; then
BODY="$(jq -n --arg v "${VERSION}" --argjson input "${INPUT}" \
'{version: $v, input: $input}')"
URL="${REPLICATE_API_BASE}/predictions"
else
BODY="$(jq -n --argjson input "${INPUT}" '{input: $input}')"
URL="${REPLICATE_API_BASE}/models/${MODEL}/predictions"
fi
if [[ -n "${WEBHOOK}" ]]; then
BODY="$(jq --arg w "${WEBHOOK}" '. + {webhook: $w, webhook_events_filter: ["completed"]}' <<<"${BODY}")"
fi
# ----- Start prediction -------------------------------------------------------
START_RESP="$(curl -sS -X POST "${URL}" \
-H "Authorization: Bearer ${REPLICATE_API_TOKEN}" \
-H "Content-Type: application/json" \
--data "${BODY}")"
if jq -e '.detail' >/dev/null 2>&1 <<<"${START_RESP}"; then
echo "replicate/run: API error" >&2
jq '.' >&2 <<<"${START_RESP}"
exit 2
fi
PREDICTION_ID="$(jq -r '.id // empty' <<<"${START_RESP}")"
if [[ -z "${PREDICTION_ID}" ]]; then
echo "replicate/run: failed to start prediction" >&2
jq '.' >&2 <<<"${START_RESP}"
exit 2
fi
# Webhook mode -- return immediately
if [[ -n "${WEBHOOK}" ]]; then
if [[ "${RAW_JSON}" -eq 1 ]]; then
printf '%s\n' "${START_RESP}"
else
echo "${PREDICTION_ID}"
fi
exit 0
fi
# ----- Poll until terminal status --------------------------------------------
ELAPSED=0
RESP="${START_RESP}"
STATUS="$(jq -r '.status // "starting"' <<<"${RESP}")"
while [[ "${STATUS}" != "succeeded" && "${STATUS}" != "failed" && "${STATUS}" != "canceled" ]]; do
if [[ "${ELAPSED}" -ge "${TIMEOUT}" ]]; then
echo "replicate/run: timeout after ${TIMEOUT}s, last status=${STATUS}" >&2
exit 3
fi
sleep "${POLL_INTERVAL}"
ELAPSED=$((ELAPSED + POLL_INTERVAL))
RESP="$(curl -sS \
-H "Authorization: Bearer ${REPLICATE_API_TOKEN}" \
"${REPLICATE_API_BASE}/predictions/${PREDICTION_ID}")"
STATUS="$(jq -r '.status // "unknown"' <<<"${RESP}")"
done
# ----- Handle result ----------------------------------------------------------
if [[ "${STATUS}" != "succeeded" ]]; then
echo "replicate/run: prediction ${STATUS}" >&2
jq '.error // .' >&2 <<<"${RESP}"
exit 4
fi
if [[ "${RAW_JSON}" -eq 1 ]]; then
printf '%s\n' "${RESP}"
else
# output may be a string, array, or object -- flatten to text
OUT_TYPE="$(jq -r '.output | type' <<<"${RESP}")"
case "${OUT_TYPE}" in
string) jq -r '.output' <<<"${RESP}" ;;
array) jq -r '.output[]' <<<"${RESP}" ;;
object) jq -c '.output' <<<"${RESP}" ;;
null) echo "" ;;
*) jq '.output' <<<"${RESP}" ;;
esac
fi
#!/usr/bin/env bash
# snappy-ai-models/replicate/run.sh
#
# Generic Replicate prediction runner. Replicate is async -- POST starts the
# prediction and returns an id; you must poll GET /v1/predictions/{id} until
# status is `succeeded` or `failed`. This script handles the polling loop
# transparently and returns the final output.
#
# Usage:
# ./run.sh --model "black-forest-labs/flux-schnell" \
# --input '{"prompt":"a futuristic city"}'
#
# ./run.sh --version "model-version-hash" \
# --input '{"prompt":"hi","max_tokens":100}'
#
# ./run.sh --model "openai/whisper" \
# --input '{"audio":"https://example.com/file.mp3"}' \
# --json
#
# Output (default): plain text or URL(s) of result, one per line.
# Output (--json): full prediction JSON.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=../lib/auth.sh
. "${SCRIPT_DIR}/../lib/auth.sh"
MODEL=""
VERSION=""
INPUT=""
INPUT_FILE=""
TIMEOUT=600 # max seconds to wait
POLL_INTERVAL=2 # seconds between polls
RAW_JSON=0
WEBHOOK=""
usage() {
cat <<'EOF'
Usage: replicate/run.sh [options]
Options:
--model <owner/name> Replicate model slug (uses official models endpoint, latest version)
--version <hash> Specific model version hash (overrides --model)
--input <json> JSON object of input parameters
--input-file <path> File containing JSON input
--timeout <seconds> Max polling wait (default 600)
--poll-interval <s> Seconds between polls (default 2)
--webhook <url> Webhook URL -- fire-and-forget mode (no polling)
--json Print full JSON response (default: extracted output)
-h, --help Show this help
Either --model or --version is required, plus --input or --input-file.
Endpoints used:
POST https://api.replicate.com/v1/models/{owner}/{name}/predictions (with --model)
POST https://api.replicate.com/v1/predictions (with --version)
GET https://api.replicate.com/v1/predictions/{id} (poll)
Examples:
./run.sh --model "black-forest-labs/flux-schnell" \
--input '{"prompt":"a cat in space","aspect_ratio":"16:9"}'
./run.sh --model "meta/llama-2-70b-chat" \
--input '{"prompt":"Explain quantum tunneling","max_new_tokens":500}'
EOF
}
if [[ $# -eq 0 ]]; then
usage
exit 1
fi
while [[ $# -gt 0 ]]; do
case "$1" in
--model) MODEL="$2"; shift 2 ;;
--version) VERSION="$2"; shift 2 ;;
--input) INPUT="$2"; shift 2 ;;
--input-file) INPUT_FILE="$2"; shift 2 ;;
--timeout) TIMEOUT="$2"; shift 2 ;;
--poll-interval) POLL_INTERVAL="$2"; shift 2 ;;
--webhook) WEBHOOK="$2"; shift 2 ;;
--json) RAW_JSON=1; shift ;;
-h|--help) usage; exit 0 ;;
*) echo "replicate/run: unknown flag: $1" >&2; usage; exit 1 ;;
esac
done
if [[ -z "${MODEL}" && -z "${VERSION}" ]]; then
echo "replicate/run: --model or --version is required" >&2
exit 1
fi
if [[ -n "${INPUT_FILE}" ]]; then
if [[ ! -f "${INPUT_FILE}" ]]; then
echo "replicate/run: input file not found: ${INPUT_FILE}" >&2
exit 1
fi
INPUT="$(cat "${INPUT_FILE}")"
fi
if [[ -z "${INPUT}" ]]; then
echo "replicate/run: --input or --input-file is required" >&2
exit 1
fi
# Validate input is JSON
if ! jq -e . >/dev/null 2>&1 <<<"${INPUT}"; then
echo "replicate/run: --input must be valid JSON" >&2
exit 1
fi
require_replicate
# ----- Build request body -----------------------------------------------------
if [[ -n "${VERSION}" ]]; then
BODY="$(jq -n --arg v "${VERSION}" --argjson input "${INPUT}" \
'{version: $v, input: $input}')"
URL="${REPLICATE_API_BASE}/predictions"
else
BODY="$(jq -n --argjson input "${INPUT}" '{input: $input}')"
URL="${REPLICATE_API_BASE}/models/${MODEL}/predictions"
fi
if [[ -n "${WEBHOOK}" ]]; then
BODY="$(jq --arg w "${WEBHOOK}" '. + {webhook: $w, webhook_events_filter: ["completed"]}' <<<"${BODY}")"
fi
# ----- Start prediction -------------------------------------------------------
START_RESP="$(curl -sS -X POST "${URL}" \
-H "Authorization: Bearer ${REPLICATE_API_TOKEN}" \
-H "Content-Type: application/json" \
--data "${BODY}")"
if jq -e '.detail' >/dev/null 2>&1 <<<"${START_RESP}"; then
echo "replicate/run: API error" >&2
jq '.' >&2 <<<"${START_RESP}"
exit 2
fi
PREDICTION_ID="$(jq -r '.id // empty' <<<"${START_RESP}")"
if [[ -z "${PREDICTION_ID}" ]]; then
echo "replicate/run: failed to start prediction" >&2
jq '.' >&2 <<<"${START_RESP}"
exit 2
fi
# Webhook mode -- return immediately
if [[ -n "${WEBHOOK}" ]]; then
if [[ "${RAW_JSON}" -eq 1 ]]; then
printf '%s\n' "${START_RESP}"
else
echo "${PREDICTION_ID}"
fi
exit 0
fi
# ----- Poll until terminal status --------------------------------------------
ELAPSED=0
RESP="${START_RESP}"
STATUS="$(jq -r '.status // "starting"' <<<"${RESP}")"
while [[ "${STATUS}" != "succeeded" && "${STATUS}" != "failed" && "${STATUS}" != "canceled" ]]; do
if [[ "${ELAPSED}" -ge "${TIMEOUT}" ]]; then
echo "replicate/run: timeout after ${TIMEOUT}s, last status=${STATUS}" >&2
exit 3
fi
sleep "${POLL_INTERVAL}"
ELAPSED=$((ELAPSED + POLL_INTERVAL))
RESP="$(curl -sS \
-H "Authorization: Bearer ${REPLICATE_API_TOKEN}" \
"${REPLICATE_API_BASE}/predictions/${PREDICTION_ID}")"
STATUS="$(jq -r '.status // "unknown"' <<<"${RESP}")"
done
# ----- Handle result ----------------------------------------------------------
if [[ "${STATUS}" != "succeeded" ]]; then
echo "replicate/run: prediction ${STATUS}" >&2
jq '.error // .' >&2 <<<"${RESP}"
exit 4
fi
if [[ "${RAW_JSON}" -eq 1 ]]; then
printf '%s\n' "${RESP}"
else
# output may be a string, array, or object -- flatten to text
OUT_TYPE="$(jq -r '.output | type' <<<"${RESP}")"
case "${OUT_TYPE}" in
string) jq -r '.output' <<<"${RESP}" ;;
array) jq -r '.output[]' <<<"${RESP}" ;;
object) jq -c '.output' <<<"${RESP}" ;;
null) echo "" ;;
*) jq '.output' <<<"${RESP}" ;;
esac
fi
#!/usr/bin/env bash
# snappy-ai-models/replicate/whisper.sh
#
# Transcribe audio via Whisper variants on Replicate.
# Wraps run.sh with the right inputs for openai/whisper or
# faster variants like turian-ai/whisperx.
#
# Replicate's Whisper accepts a URL to the audio file (HTTPS) -- it does
# NOT accept local file uploads via this endpoint. To upload a local file
# you must first stage it via Replicate's file upload API or host it
# somewhere reachable. Use --upload to use Replicate's file upload first.
#
# Usage:
# ./whisper.sh --audio https://example.com/recording.mp3
# ./whisper.sh --audio https://... --model turbo --language en
# ./whisper.sh --upload ./local.mp3 # stages file first
# ./whisper.sh --audio https://... --json # full JSON response
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
RUN_SH="${SCRIPT_DIR}/run.sh"
# shellcheck source=../lib/auth.sh
. "${SCRIPT_DIR}/../lib/auth.sh"
if [[ ! -x "${RUN_SH}" ]]; then
chmod +x "${RUN_SH}" 2>/dev/null || true
fi
AUDIO_URL=""
UPLOAD_FILE=""
MODEL_VARIANT="standard" # standard | large | turbo | whisperx
LANGUAGE=""
TRANSLATE=0
RAW_JSON=0
usage() {
cat <<'EOF'
Usage: replicate/whisper.sh [options]
Options:
--audio <url> Public HTTPS URL to audio file
--upload <path> Local file -> stage to Replicate's file API first
--model <variant> standard (default) | large | turbo | whisperx
--language <code> ISO-639-1 (en, fr, ja, ...) -- improves accuracy
--translate Translate output to English (instead of transcribe)
--json Print full JSON response
-h, --help Show this help
Model slug mapping:
standard -> openai/whisper (whisper-large-v2)
large -> openai/whisper (alias for standard)
turbo -> turian-ai/insanely-fast-whisper (faster, batched)
whisperx -> daanelson/whisperx (with diarization + word timestamps)
EOF
}
if [[ $# -eq 0 ]]; then
usage
exit 1
fi
while [[ $# -gt 0 ]]; do
case "$1" in
--audio) AUDIO_URL="$2"; shift 2 ;;
--upload) UPLOAD_FILE="$2"; shift 2 ;;
--model) MODEL_VARIANT="$2"; shift 2 ;;
--language) LANGUAGE="$2"; shift 2 ;;
--translate) TRANSLATE=1; shift ;;
--json) RAW_JSON=1; shift ;;
-h|--help) usage; exit 0 ;;
*) echo "replicate/whisper: unknown flag: $1" >&2; usage; exit 1 ;;
esac
done
if [[ -z "${AUDIO_URL}" && -z "${UPLOAD_FILE}" ]]; then
echo "replicate/whisper: --audio or --upload is required" >&2
exit 1
fi
require_replicate
# ----- Optional: upload local file via Replicate file API --------------------
if [[ -n "${UPLOAD_FILE}" ]]; then
if [[ ! -f "${UPLOAD_FILE}" ]]; then
echo "replicate/whisper: file not found: ${UPLOAD_FILE}" >&2
exit 1
fi
echo "uploading ${UPLOAD_FILE} to Replicate file API..." >&2
UPLOAD_RESP="$(curl -sS -X POST "${REPLICATE_API_BASE}/files" \
-H "Authorization: Bearer ${REPLICATE_API_TOKEN}" \
-F "content=@${UPLOAD_FILE}")"
AUDIO_URL="$(jq -r '.urls.get // .url // empty' <<<"${UPLOAD_RESP}")"
if [[ -z "${AUDIO_URL}" ]]; then
echo "replicate/whisper: file upload failed" >&2
jq '.' >&2 <<<"${UPLOAD_RESP}"
exit 2
fi
echo "uploaded -> ${AUDIO_URL}" >&2
fi
case "${MODEL_VARIANT}" in
standard|large)
MODEL_SLUG="openai/whisper"
INPUT="$(jq -n --arg a "${AUDIO_URL}" '{audio: $a}')"
if [[ -n "${LANGUAGE}" ]]; then
INPUT="$(jq --arg l "${LANGUAGE}" '. + {language: $l}' <<<"${INPUT}")"
fi
if [[ "${TRANSLATE}" -eq 1 ]]; then
INPUT="$(jq '. + {translate: true}' <<<"${INPUT}")"
fi
;;
turbo)
MODEL_SLUG="vaibhavs10/incredibly-fast-whisper"
INPUT="$(jq -n --arg a "${AUDIO_URL}" '{audio: $a, batch_size: 24}')"
if [[ -n "${LANGUAGE}" ]]; then
INPUT="$(jq --arg l "${LANGUAGE}" '. + {language: $l}' <<<"${INPUT}")"
fi
if [[ "${TRANSLATE}" -eq 1 ]]; then
INPUT="$(jq '. + {task: "translate"}' <<<"${INPUT}")"
fi
;;
whisperx)
MODEL_SLUG="daanelson/whisperx"
INPUT="$(jq -n --arg a "${AUDIO_URL}" '{audio: $a, debug: false}')"
if [[ -n "${LANGUAGE}" ]]; then
INPUT="$(jq --arg l "${LANGUAGE}" '. + {language: $l}' <<<"${INPUT}")"
fi
;;
*)
echo "replicate/whisper: unknown model variant: ${MODEL_VARIANT}" >&2
exit 1
;;
esac
if [[ "${RAW_JSON}" -eq 1 ]]; then
bash "${RUN_SH}" --model "${MODEL_SLUG}" --input "${INPUT}" --json
else
# Most Whisper variants return either a string or an object with a `transcription` field.
# Use --json then post-process for clarity.
RESP="$(bash "${RUN_SH}" --model "${MODEL_SLUG}" --input "${INPUT}" --json)"
TEXT="$(jq -r '
.output |
if type == "string" then .
elif type == "object" then (.transcription // .text // .segments[0].text? // (. | tostring))
elif type == "array" then (.[].text // .[] | tostring)
else . | tostring
end' <<<"${RESP}")"
printf '%s\n' "${TEXT}"
fi
#!/usr/bin/env bash
# snappy-ai-models/replicate/whisper.sh
#
# Transcribe audio via Whisper variants on Replicate.
# Wraps run.sh with the right inputs for openai/whisper or
# faster variants like turian-ai/whisperx.
#
# Replicate's Whisper accepts a URL to the audio file (HTTPS) -- it does
# NOT accept local file uploads via this endpoint. To upload a local file
# you must first stage it via Replicate's file upload API or host it
# somewhere reachable. Use --upload to use Replicate's file upload first.
#
# Usage:
# ./whisper.sh --audio https://example.com/recording.mp3
# ./whisper.sh --audio https://... --model turbo --language en
# ./whisper.sh --upload ./local.mp3 # stages file first
# ./whisper.sh --audio https://... --json # full JSON response
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
RUN_SH="${SCRIPT_DIR}/run.sh"
# shellcheck source=../lib/auth.sh
. "${SCRIPT_DIR}/../lib/auth.sh"
if [[ ! -x "${RUN_SH}" ]]; then
chmod +x "${RUN_SH}" 2>/dev/null || true
fi
AUDIO_URL=""
UPLOAD_FILE=""
MODEL_VARIANT="standard" # standard | large | turbo | whisperx
LANGUAGE=""
TRANSLATE=0
RAW_JSON=0
usage() {
cat <<'EOF'
Usage: replicate/whisper.sh [options]
Options:
--audio <url> Public HTTPS URL to audio file
--upload <path> Local file -> stage to Replicate's file API first
--model <variant> standard (default) | large | turbo | whisperx
--language <code> ISO-639-1 (en, fr, ja, ...) -- improves accuracy
--translate Translate output to English (instead of transcribe)
--json Print full JSON response
-h, --help Show this help
Model slug mapping:
standard -> openai/whisper (whisper-large-v2)
large -> openai/whisper (alias for standard)
turbo -> turian-ai/insanely-fast-whisper (faster, batched)
whisperx -> daanelson/whisperx (with diarization + word timestamps)
EOF
}
if [[ $# -eq 0 ]]; then
usage
exit 1
fi
while [[ $# -gt 0 ]]; do
case "$1" in
--audio) AUDIO_URL="$2"; shift 2 ;;
--upload) UPLOAD_FILE="$2"; shift 2 ;;
--model) MODEL_VARIANT="$2"; shift 2 ;;
--language) LANGUAGE="$2"; shift 2 ;;
--translate) TRANSLATE=1; shift ;;
--json) RAW_JSON=1; shift ;;
-h|--help) usage; exit 0 ;;
*) echo "replicate/whisper: unknown flag: $1" >&2; usage; exit 1 ;;
esac
done
if [[ -z "${AUDIO_URL}" && -z "${UPLOAD_FILE}" ]]; then
echo "replicate/whisper: --audio or --upload is required" >&2
exit 1
fi
require_replicate
# ----- Optional: upload local file via Replicate file API --------------------
if [[ -n "${UPLOAD_FILE}" ]]; then
if [[ ! -f "${UPLOAD_FILE}" ]]; then
echo "replicate/whisper: file not found: ${UPLOAD_FILE}" >&2
exit 1
fi
echo "uploading ${UPLOAD_FILE} to Replicate file API..." >&2
UPLOAD_RESP="$(curl -sS -X POST "${REPLICATE_API_BASE}/files" \
-H "Authorization: Bearer ${REPLICATE_API_TOKEN}" \
-F "content=@${UPLOAD_FILE}")"
AUDIO_URL="$(jq -r '.urls.get // .url // empty' <<<"${UPLOAD_RESP}")"
if [[ -z "${AUDIO_URL}" ]]; then
echo "replicate/whisper: file upload failed" >&2
jq '.' >&2 <<<"${UPLOAD_RESP}"
exit 2
fi
echo "uploaded -> ${AUDIO_URL}" >&2
fi
case "${MODEL_VARIANT}" in
standard|large)
MODEL_SLUG="openai/whisper"
INPUT="$(jq -n --arg a "${AUDIO_URL}" '{audio: $a}')"
if [[ -n "${LANGUAGE}" ]]; then
INPUT="$(jq --arg l "${LANGUAGE}" '. + {language: $l}' <<<"${INPUT}")"
fi
if [[ "${TRANSLATE}" -eq 1 ]]; then
INPUT="$(jq '. + {translate: true}' <<<"${INPUT}")"
fi
;;
turbo)
MODEL_SLUG="vaibhavs10/incredibly-fast-whisper"
INPUT="$(jq -n --arg a "${AUDIO_URL}" '{audio: $a, batch_size: 24}')"
if [[ -n "${LANGUAGE}" ]]; then
INPUT="$(jq --arg l "${LANGUAGE}" '. + {language: $l}' <<<"${INPUT}")"
fi
if [[ "${TRANSLATE}" -eq 1 ]]; then
INPUT="$(jq '. + {task: "translate"}' <<<"${INPUT}")"
fi
;;
whisperx)
MODEL_SLUG="daanelson/whisperx"
INPUT="$(jq -n --arg a "${AUDIO_URL}" '{audio: $a, debug: false}')"
if [[ -n "${LANGUAGE}" ]]; then
INPUT="$(jq --arg l "${LANGUAGE}" '. + {language: $l}' <<<"${INPUT}")"
fi
;;
*)
echo "replicate/whisper: unknown model variant: ${MODEL_VARIANT}" >&2
exit 1
;;
esac
if [[ "${RAW_JSON}" -eq 1 ]]; then
bash "${RUN_SH}" --model "${MODEL_SLUG}" --input "${INPUT}" --json
else
# Most Whisper variants return either a string or an object with a `transcription` field.
# Use --json then post-process for clarity.
RESP="$(bash "${RUN_SH}" --model "${MODEL_SLUG}" --input "${INPUT}" --json)"
TEXT="$(jq -r '
.output |
if type == "string" then .
elif type == "object" then (.transcription // .text // .segments[0].text? // (. | tostring))
elif type == "array" then (.[].text // .[] | tostring)
else . | tostring
end' <<<"${RESP}")"
printf '%s\n' "${TEXT}"
fi