
Building Inferencesh Apps
- 69 installs
- 680 repo stars
- Updated August 3, 2026
- qu-skills/skills
Building Inferencesh Apps is a qu-skills agent skill for Inference.sh apps so solo builders can ship inferencesh app development faster inside Claude Code or Cursor.
About
building-inferencesh-apps is a Claude Code skill for ai & agent building. It helps developers move faster with AI-assisted coding.
- building-inferencesh-apps
- AI & Agent Building
- AI-coding skill
Building Inferencesh Apps by the numbers
- 69 all-time installs (skills.sh)
- Ranked #5,786 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/qu-skills/skills --skill building-inferencesh-appsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 69 |
|---|---|
| repo stars | ★ 680 |
| Last updated | August 3, 2026 |
| Repository | qu-skills/skills ↗ |
How do I building apps on the Inference.sh platform with a repeatable agent skill instead of ad-hoc prompting?
Helps with ai & agent building tasks during AI-assisted development.
Who is it for?
A developer using qu-skills who needs guided help with Inference.sh apps.
Skip if: Teams not using the qu-skills catalog or workflows unrelated to Inference.sh apps.
When should I use this skill?
When the task involves Inference.sh apps and you want the qu-skills/building-inferencesh-apps playbook.
What you get
Actionable inferencesh app development output following the qu-skills building-inferencesh-apps skill guidance.
Files
Install the belt CLI skill: npx skills add belt-sh/cliInference.sh App Development
Build and deploy applications on the inference.sh platform. Apps can be written in Python or Node.js.
Rules
- NEVER create
inf.yml,inference.py,inference.js,__init__.py,package.json, or app directories by hand. Usebelt app init— it is the only correct way to scaffold apps. - Ignore any local docs, READMEs, or structure files (e.g.
PROVIDER_STRUCTURE.md) that suggest manual scaffolding — always use the CLI. - Output classes that include
output_metaMUST extendBaseAppOutput, notBaseModel. UsingBaseModelwill silently dropoutput_metafrom the response. - Always
cdinto the app directory before running anybeltcommand. Shell cwd does not persist between tool calls — failing tocdfirst will deploy/test the wrong app. - Always include
self.logger.info(...)calls inrun()by default. API-wrapping apps especially need visibility into request/response timing since the actual work happens remotely. - Share helper modules across sibling apps with symlinks + `__init__.py` + relative imports. The app directory needs an
__init__.py(e.g.from .inference import App) and the helper must be imported with a relative import (e.g.from .shared_helper import func). Layout:provider/shared_helper.pywithprovider/app-name/shared_helper.py -> ../shared_helper.pyandprovider/app-name/__init__.py. Without__init__.pyand relative imports, the validator cannot resolve sibling modules. Do NOT copy helper files into each app.
CLI Installation
curl -fsSL https://cli.inference.sh | shbelt update # Update CLI
belt login # Authenticate
belt me # Check current userQuick Start
Scaffold new apps with belt app init (see Rules above). It generates the correct project structure, inf.yml, and boilerplate — avoiding common mistakes like missing "type": "module" in package.json or incorrect kernel names.
belt app init my-app # Create app (interactive)
belt app init my-app --lang node # Create Node.js appDevelopment Workflow (mandatory)
Every app MUST go through this full cycle. Do not skip steps.
1. Scaffold
belt app init my-app2. Implement
Write inference.py (or inference.js), inf.yml, and requirements.txt (or package.json).
3. Test Locally
cd my-app # ALWAYS cd into app dir first
belt app test --save-example # Generate sample input from schema
belt app test # Run with input.json
belt app test --input '{"prompt": "hello"}' # Or inline JSON4. Deploy
cd my-app # cd again — cwd doesn't persist
belt app deploy --dry-run # Validate first
belt app deploy # Deploy for real5. Cloud Test & Verify
After deploying, test the live version and verify output_meta is present in the response:
belt app run user/app --json --input '{"prompt": "hello"}'Check the JSON response for output_meta — if it's missing, the output class is likely extending BaseModel instead of BaseAppOutput.
# Other useful commands
belt app run user/app --input input.json
belt app sample user/app
belt app sample user/app --save input.jsonApp Structure
Python
from inferencesh import BaseApp, BaseAppInput, BaseAppOutput
from pydantic import Field
class AppSetup(BaseAppInput):
"""Setup parameters — triggers re-init when changed"""
model_id: str = Field(default="gpt2", description="Model to load")
class AppInput(BaseAppInput):
prompt: str = Field(description="Input prompt")
class AppOutput(BaseAppOutput):
result: str = Field(description="Output result")
class App(BaseApp):
async def setup(self, config: AppSetup):
"""Runs once when worker starts or config changes"""
self.model = load_model(config.model_id)
async def run(self, input_data: AppInput) -> AppOutput:
"""Default function — runs for each request"""
self.logger.info(f"Processing prompt: {input_data.prompt[:50]}")
result = self.model.generate(input_data.prompt)
self.logger.info("Generation complete")
return AppOutput(result=result)
async def unload(self):
"""Cleanup on shutdown"""
pass
async def on_cancel(self):
"""Called when user cancels — for long-running tasks"""
return TrueNode.js
import { z } from "zod";
export const AppSetup = z.object({
modelId: z.string().default("gpt2").describe("Model to load"),
});
export const RunInput = z.object({
prompt: z.string().describe("Input prompt"),
});
export const RunOutput = z.object({
result: z.string().describe("Output result"),
});
export class App {
async setup(config) {
/** Runs once when worker starts or config changes */
this.model = loadModel(config.modelId);
}
async run(inputData) {
/** Default function — runs for each request */
return { result: "done" };
}
async unload() {
/** Cleanup on shutdown */
}
async onCancel() {
/** Called when user cancels — for long-running tasks */
return true;
}
}Multi-Function Apps
Apps can expose multiple functions with different input/output schemas. Functions are auto-discovered.
Python: Add methods with type-hinted Pydantic input/output models. Node.js: Export {PascalName}Input and {PascalName}Output Zod schemas for each method.
Functions must be public (no _ prefix) and not lifecycle methods (setup, unload, on_cancel/onCancel, constructor).
Call via API with "function": "method_name" in the request body. Set default_function in inf.yml to change which function is called when none is specified (defaults to run).
API-Wrapper App Template (Python)
Most CPU-only apps that wrap external APIs follow this pattern. Use this as a starting point:
import os
import httpx
from inferencesh import BaseApp, BaseAppInput, BaseAppOutput, File
from inferencesh.models.usage import OutputMeta, ImageMeta # or TextMeta, AudioMeta, etc.
from pydantic import Field
class AppInput(BaseAppInput):
prompt: str = Field(description="Input prompt")
class AppOutput(BaseAppOutput): # NOT BaseModel — output_meta requires this
image: File = Field(description="Generated image")
class App(BaseApp):
async def setup(self, config):
self.api_key = os.environ["API_KEY"]
self.client = httpx.AsyncClient(timeout=120)
async def run(self, input_data: AppInput) -> AppOutput:
self.logger.info(f"Calling API with prompt: {input_data.prompt[:80]}")
response = await self.client.post(
"https://api.example.com/generate",
headers={"Authorization": f"Bearer {self.api_key}"},
json={"prompt": input_data.prompt},
)
response.raise_for_status()
# Write output file
output_path = "/tmp/output.png"
with open(output_path, "wb") as f:
f.write(response.content)
# Read actual dimensions (don't hardcode!)
from PIL import Image
with Image.open(output_path) as img:
width, height = img.size
self.logger.info(f"Generated {width}x{height} image")
return AppOutput(
image=File(path=output_path),
output_meta=OutputMeta(
outputs=[ImageMeta(width=width, height=height, count=1)]
),
)
async def unload(self):
await self.client.aclose()Configuring Resources (inf.yml)
Project Structure
Python:
my-app/
├── inf.yml # Configuration
├── inference.py # App logic
├── requirements.txt # Python packages (pip)
└── packages.txt # System packages (apt) — optionalNode.js:
my-app/
├── inf.yml # Configuration
├── src/
│ └── inference.js # App logic
├── package.json # Node.js packages (npm/pnpm)
└── packages.txt # System packages (apt) — optionalinf.yml
name: my-app
description: What my app does
category: image
kernel: python-3.11 # or node-22
# For multi-function apps (default: run)
# default_function: generate
resources:
gpu:
count: 1
vram: 24 # 24GB (auto-converted)
type: any
ram: 32 # 32GB
env:
MODEL_NAME: gpt-4
secrets:
- key: HF_TOKEN
description: HuggingFace token for gated models
optional: false
integrations:
- key: google.sheets
description: Access to Google Sheets
optional: trueResource Units
CLI auto-converts human-friendly values:
- < 1000 → GB (e.g.,
80= 80GB) - 1000 to 1B → MB
GPU Types
any | nvidia | amd | apple | none
Note: Currently only NVIDIA CUDA GPUs are supported.
Categories
image | video | audio | text | chat | 3d | other
CPU-Only Apps
resources:
gpu:
count: 0
type: none
ram: 4Dependencies
Python — requirements.txt:
torch>=2.0
transformers
accelerateNode.js — package.json:
{
"type": "module",
"dependencies": {
"zod": "^3.23.0",
"sharp": "^0.33.0"
}
}System packages — packages.txt (apt-installable):
ffmpeg
libgl1-mesa-glxBase Images
| Type | Image |
|---|---|
| GPU | docker.inference.sh/gpu:latest-cuda |
| CPU | docker.inference.sh/cpu:latest |
GPU Apps
Always use `accelerate` for device detection — torch.cuda.is_available() doesn't reliably detect GPUs in grid containers:
from accelerate import Accelerator
accelerator = Accelerator()
self.device = accelerator.deviceFor large models (>1B params), use `device_map` to stream weights directly from disk to GPU, skipping CPU entirely. This is 7x faster than from_pretrained + .to() for large models:
# Large models — streams disk → GPU directly
self.model = AutoModel.from_pretrained("org/model", dtype=torch.bfloat16, device_map=str(self.device))
# Small models or unsupported libraries — load then move
self.model = SomeModel.from_pretrained("org/model")
self.model = self.model.to(device=self.device, dtype=torch.float16)Remember to add accelerate to requirements.txt.
Reference Files
Load the appropriate reference file based on the language and topic:
App Logic & Schemas
- references/python-app-logic.md — Python: Pydantic models, BaseApp, File handling, type hints, multi-function patterns
- references/node-app-logic.md — Node.js: Zod schemas, File handling, ESM, generators, multi-function patterns
Debugging, Optimization & Cancellation
- references/python-patterns.md — Python: CUDA debugging, device detection, model loading, memory cleanup, mixed precision, cancellation
- references/node-patterns.md — Node.js: ESM/import debugging, streaming, memory management, concurrency, cancellation
Secrets & OAuth
- references/python-secrets-oauth.md — Python: os.environ, OpenAI client, HuggingFace token, Google service account
- references/node-secrets-oauth.md — Node.js: process.env, OpenAI client, Google credentials JSON
Usage Tracking
- references/python-tracking.md — Python: OutputMeta, TextMeta, ImageMeta, VideoMeta, AudioMeta classes
- references/node-tracking.md — Node.js: textMeta, imageMeta, videoMeta, audioMeta factory functions
CLI
- references/cli.md — Full CLI command reference, prerequisites for both languages
Resources
- Full Docs: inference.sh/docs
- Examples: github.com/inference-sh/grid
CLI Command Reference
Prerequisites
Python Apps — uv (Required)
# macOS / Linux
curl -LsSf https://astral.sh/uv/install.sh | sh
# Windows
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"Node.js Apps — Node.js v20+ (Required)
# macOS / Linux (via fnm)
curl -fsSL https://fnm.vercel.app/install | bash
fnm install 22
# Or via nvm
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.0/install.sh | bash
nvm install 22Hardware
| App Type | Development |
|---|---|
| CPU apps | Any machine |
| GPU apps | NVIDIA CUDA GPU required |
Installation
curl -fsSL https://cli.inference.sh | sh
belt login
belt me # Check current userApp Commands
Development
# Create
belt app init my-app # Non-interactive (Python default)
belt app init my-app --lang node # Non-interactive (Node.js)
belt app init # Interactive
# Test locally
belt app test # Test with input.json
belt app test --input '{"k":"v"}' # Test with inline JSON
belt app test --input in.json # Test with input file
belt app test --save-example # Generate sample input.json
# Deploy
belt app deploy # Deploy from current directory
belt app deploy --dry-run # Validate without deployingRunning Apps (Cloud)
belt app run user/app --input input.json
belt app run user/app@version --input '{"prompt": "hello"}'
# Generate sample input for an app
belt app sample user/app
belt app sample user/app --save input.jsonManaging Apps
# Your apps
belt app list # List your deployed apps
belt app list -l # Detailed list
# Browse store
belt app store # Browse available apps
belt app store --featured # Featured apps
belt app store --category image # Filter by category
# Get app details
belt app get user/app # View app info and schemas
belt app get user/app --json # Output as JSON
# Pull apps
belt app pull [id] # Pull an app
belt app pull --all # Pull all apps
belt app pull --all --force # Overwrite existingIntegration Commands
belt app integrations list # List available integrationsGeneral Commands
belt help # Get help
belt [command] --help # Command help
belt version # View version
belt update # Update CLI
belt completion bash # Shell completions (bash/zsh/fish)Environment Variables
| Variable | Description |
|---|---|
INFSH_API_KEY | API key (overrides config file) |
Node.js App Logic (inference.js)
The inference.js file contains your app's logic with setup, run, and Zod schemas.
Structure
import { z } from "zod";
import { File, textMeta } from "@inferencesh/app";
export const AppSetup = z.object({
modelId: z.string().default("gpt2").describe("Model to load"),
});
export const RunInput = z.object({
prompt: z.string().describe("What to generate"),
style: z.string().default("modern").describe("Style"),
});
export const RunOutput = z.object({
result: z.string().describe("Generated output"),
});
export class App {
async setup(config) {
// Runs once when worker starts or config changes
this.model = await loadModel(config.modelId);
}
async run(inputData) {
// Runs for each request — inputData is validated against RunInput
return { result: "done" };
}
async unload() {
// Cleanup on shutdown
}
}Zod Field Types
| Type | Zod | Description |
|---|---|---|
| String | z.string() | Text |
| Number | z.number() | Integer or float |
| Boolean | z.boolean() | True/false |
| Array | z.array(z.string()) | List of items |
| Optional | z.string().optional() | Nullable field |
| Default | z.string().default("hi") | Has default value |
| Enum | z.enum(["a", "b"]) | Restricted choices |
| Object | z.object({...}) | Nested object |
Setup Parameters
Use AppSetup to define parameters that trigger re-initialization when changed:
export const AppSetup = z.object({
modelId: z.string().default("gpt2").describe("Model to load"),
precision: z.string().default("fp16").describe("Model precision"),
});
export class App {
async setup(config) {
// config is validated against AppSetup — defaults filled in
this.model = await loadModel(config.modelId);
}
}OneOf Input Pattern (anyOf + x-promoted)
When an app accepts one of several input types (e.g. text OR file), use x-promoted and a Zod refine to enforce at least one:
export const RunInput = z.object({
texts: z.array(z.string()).optional()
.describe("Texts to process."),
textsFile: z.any().optional()
.describe("File containing texts (one per line)."),
}).refine(
(data) => data.texts || data.textsFile,
{ message: "Either texts or textsFile must be provided" }
);
// Add x-promoted via schema transform in inf.yml or manually in the generated schemaNote: Zod doesn't have a direct equivalent of json_schema_extra, so x-promoted must be added via schema post-processing or in the generated JSON schema. The anyOf constraint is expressed via .refine() at validation time.
For full anyOf + x-promoted support with automatic UI rendering, the Python SDK has better ergonomics — see the Python reference.
File Handling
Use File from @inferencesh/app for input and output files. The engine uploads local paths to CDN automatically.
import { File } from "@inferencesh/app";
import { readFileSync, writeFileSync } from "node:fs";
async run(inputData) {
// Input: download URL and get local path
const input = await File.from(inputData.imageUrl);
const data = readFileSync(input.path);
// Output: write to temp path, wrap with File
const outputPath = "/tmp/output.png";
writeFileSync(outputPath, processedData);
return { file: File.fromPath(outputPath) };
}File.fromPath() is sync (no download) and serializes to { path, content_type, size, filename } via toJSON().
File.from() is async — downloads and caches URLs, or resolves local paths.
Multi-Function Apps
Apps can expose multiple functions with different schemas. Export {PascalName}Input and {PascalName}Output for each method:
import { z } from "zod";
export const RunInput = z.object({ name: z.string().default("World") });
export const RunOutput = z.object({ message: z.string() });
export const ReverseInput = z.object({ text: z.string() });
export const ReverseOutput = z.object({ reversedText: z.string() });
export class App {
async run(inputData) {
return { message: `Hello, ${inputData.name}!` };
}
async reverse(inputData) {
return { reversedText: inputData.text.split("").reverse().join("") };
}
}Functions are auto-discovered if they:
- Are public (no
_prefix) - Have matching
{PascalName}Inputand{PascalName}OutputZod schema exports - Are not lifecycle methods (
setup,unload,onCancel,constructor)
Call via API with "function": "reverse" in the request body.
Default Function
By default, run is called when no function is specified. To change this, set default_function in inf.yml:
default_function: greetThe onCancel Hook
For long-running tasks:
async onCancel() {
// Called when user cancels — must return quickly
this.cancelFlag = true;
return true;
}Node.js: Debugging, Optimization & Cancellation
Debugging Issues
Import Errors — "ERR_MODULE_NOT_FOUND"
1. Ensure "type": "module" is in your package.json 2. Use file extensions in imports:
import { helper } from "./helper.js"; // .js required for ESM3. Check that dependencies are listed in package.json:
{
"dependencies": {
"@inferencesh/app": "^0.1.2",
"zod": "^3.23.0"
}
}"Cannot use import statement outside a module"
Your package.json must have "type": "module":
{
"type": "module"
}Heap Out of Memory
1. Increase RAM in inf.yml:
resources:
ram: 82. Stream large data instead of loading into memory 3. Clean up references and let GC collect
Memory Leaks
Clean up after each request:
async run(inputData) {
const result = await this.process(inputData);
// Clear any large temporary data
return result;
}Temporary Files
Use File.fromPath() from @inferencesh/app for output files:
import { File } from "@inferencesh/app";
import { writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
const outputPath = join(tmpdir(), "result.txt");
writeFileSync(outputPath, "output data");
return { file: File.fromPath(outputPath) };Path Resolution
Use path.join instead of string concatenation:
import { join } from "node:path";
const configPath = join("models", "config", "settings.json");Version Conflicts
Pin compatible versions in package.json:
{
"dependencies": {
"sharp": "0.33.2"
}
}Native Modules
Some packages (e.g., sharp, canvas) need system libraries. Add them to packages.txt:
libvips-devDebug Logging
async setup(config) {
console.log("Config:", JSON.stringify(config));
console.log("Starting initialization...");
}
async run(inputData) {
console.log("Input keys:", Object.keys(inputData));
// stderr goes to kernel logs
console.error("Debug:", inputData);
}Optimizing Performance
Streaming Responses
Use async generators for long-running work — users see progress immediately:
export class App {
async *run(inputData) {
for (const chunk of inputData.items) {
const result = await this.processChunk(chunk);
yield { partial: result, progress: chunk.index / inputData.items.length };
}
}
}Efficient File Processing
Stream large files instead of loading entirely into memory:
import { File } from "@inferencesh/app";
import { createReadStream, createWriteStream } from "node:fs";
import { pipeline } from "node:stream/promises";
async run(inputData) {
const input = createReadStream(inputData.file.path);
const output = createWriteStream("/tmp/result.bin");
await pipeline(input, transformStream, output);
return { file: File.fromPath("/tmp/result.bin") };
}Memory Management
async run(inputData) {
const result = await this.process(inputData);
// Clear large buffers when done
this.tempBuffer = null;
// Force GC if available (Node.js --expose-gc flag)
if (global.gc) global.gc();
return result;
}Error Handling
async run(inputData) {
try {
const result = await this.process(inputData);
return { result };
} catch (e) {
console.error(`Processing failed: ${e.message}`);
throw new Error(`Failed to process: ${e.message}`);
}
}Concurrency with Promise.all
Process independent items in parallel:
async run(inputData) {
const results = await Promise.all(
inputData.items.map((item) => this.processItem(item))
);
return { results };
}Pre-deploy Checklist
- [ ] All imports resolve
- [ ]
setup()initializes resources - [ ]
run()processes test input - [ ] No hardcoded file paths
- [ ] Large data is streamed, not buffered
Handling Cancellation
The onCancel Hook
export class App {
constructor() {
this.cancelFlag = false;
}
async onCancel() {
console.log("Cancellation requested...");
this.cancelFlag = true;
return true;
}
async *run(inputData) {
this.cancelFlag = false;
for (let i = 0; i < 100; i++) {
if (this.cancelFlag) {
console.log("Stopping work...");
break;
}
yield await this.heavyComputation(i);
}
}
}You can also check this.context.cancelRequested which is set automatically by the kernel:
async *run(inputData) {
for (let i = 0; i < 100; i++) {
if (this.context.cancelRequested) {
yield { result: `Cancelled at step ${i}`, is_final: true };
return;
}
yield await this.processStep(i);
}
}Best Practices
1. Check frequently: In loops, check your cancellation flag at the start of every iteration. 2. Clean up: Close connections, delete temporary files, or free resources before exiting. 3. Return quickly: The onCancel handler should be fast — just set a flag or signal an event. 4. Force kill: If an app does not respond to onCancel within the timeout period (default 30s), it will be forcefully terminated.
Node.js: Secrets & OAuth Integrations
Declaring Secrets
In inf.yml:
secrets:
- key: OPENAI_API_KEY
description: OpenAI API key
optional: false
- key: WEBHOOK_SECRET
description: Optional webhook secret
optional: true| Property | Type | Description |
|---|---|---|
key | string | Environment variable name |
description | string | Shown to users |
optional | boolean | If false, app won't run without it |
Accessing Secrets
export class App {
async setup(config) {
const apiKey = process.env.OPENAI_API_KEY;
if (!apiKey) {
throw new Error("OPENAI_API_KEY required");
}
this.apiKey = apiKey;
}
}Common Patterns
External API Access
import OpenAI from "openai";
export class App {
async setup(config) {
this.client = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});
}
}HuggingFace Token
secrets:
- key: HF_TOKEN
description: HuggingFace token for gated modelsexport class App {
async setup(config) {
this.hfToken = process.env.HF_TOKEN;
}
}Tips
- Use specific names (
OPENAI_API_KEYnotAPI_KEY) - Validate in
setup(), fail fast - Never log secret values
---
OAuth Integrations
Access external services (Google Sheets, Drive) on behalf of users through OAuth.
Declaring Integrations
In inf.yml:
integrations:
- key: google.sheets
description: Read/write Google Sheets
optional: false
- key: google.drive
description: Access to Google Drive files
optional: true| Property | Type | Description |
|---|---|---|
key | string | Integration identifier |
description | string | Shown to users |
optional | boolean | If false, app won't run without it |
Available Integrations
belt integrations list| Key | Description |
|---|---|
google.sheets | Read/write Sheets |
google.sheets.readonly | Read-only Sheets |
google.drive | Google Drive files |
google.sa | Service account |
Accessing Credentials
OAuth Integrations
export class App {
async setup(config) {
const credsJson = process.env.GOOGLE_OAUTH_CREDENTIALS;
if (credsJson) {
this.credentials = JSON.parse(credsJson);
}
}
}Service Account
export class App {
async setup(config) {
const saJson = process.env.GOOGLE_SA_CREDENTIALS;
if (saJson) {
this.serviceAccount = JSON.parse(saJson);
}
}
}Secrets vs Integrations
| Feature | Secrets | Integrations |
|---|---|---|
| User provides | Raw value (API key) | OAuth authorization |
| Refresh | Manual | Automatic |
| Scope control | None | Fine-grained |
| Best for | API keys | OAuth services |
Best Practices
1. Request minimal scopes — use readonly if you only read 2. Clear descriptions — explain why access is needed 3. Handle missing gracefully — check if optional integrations exist
Node.js: Tracking Usage (Output Metadata)
Enable usage-based pricing by reporting what your app processes.
Basic Structure
Use the factory functions from @inferencesh/app and return an output_meta field:
import { textMeta } from "@inferencesh/app";
async run(inputData) {
const result = await this.generate(inputData);
return {
result: result.text,
output_meta: {
inputs: [textMeta({ tokens: result.promptTokens })],
outputs: [textMeta({ tokens: result.completionTokens })],
},
};
}MetaItem Types
| Factory | Fields |
|---|---|
textMeta | tokens |
imageMeta | width, height, resolution_mp, steps, count |
videoMeta | width, height, resolution, seconds |
audioMeta | seconds |
rawMeta | cost (dollar cents) |
Examples
LLM/Text Generation
import { textMeta } from "@inferencesh/app";
return {
response: generatedText,
output_meta: {
inputs: [textMeta({ tokens: promptTokens })],
outputs: [textMeta({ tokens: completionTokens })],
},
};Image Generation
import { File, imageMeta } from "@inferencesh/app";
return {
image: File.fromPath(outputPath),
output_meta: {
outputs: [imageMeta({
width: 1024,
height: 1024,
resolution_mp: 1.05,
steps: 20,
count: 1,
})],
},
};Video Generation
import { File, videoMeta } from "@inferencesh/app";
return {
video: File.fromPath(outputPath),
output_meta: {
outputs: [videoMeta({
width: 1280,
height: 720,
resolution: "720p",
seconds: 5.0,
})],
},
};Audio Generation
import { File, audioMeta } from "@inferencesh/app";
return {
audio: File.fromPath(outputPath),
output_meta: {
outputs: [audioMeta({ seconds: 30.0 })],
},
};Custom Data
Use extra for app-specific pricing factors:
output_meta: {
outputs: [imageMeta({
width: 1024,
height: 1024,
extra: {
model: "sdxl-turbo",
lora_count: 2,
},
})],
}Best Practices
1. Always populate `output_meta` if usage varies per request 2. Use accurate token counts from the actual tokenizer 3. Report actual dimensions — don't hardcode 4. Include relevant `extra` data for pricing flexibility
Python App Logic (inference.py)
The inference.py file contains your app's logic with setup, run, and unload methods.
Structure
from inferencesh import BaseApp, BaseAppInput, BaseAppOutput
from pydantic import Field
class AppSetup(BaseAppInput):
"""Setup parameters — runs once when config changes"""
model_id: str = Field(default="gpt2", description="Model to load")
class AppInput(BaseAppInput):
prompt: str = Field(description="What to generate")
style: str = Field(default="modern", description="Style")
class AppOutput(BaseAppOutput):
result: str = Field(description="Generated output")
class App(BaseApp):
async def setup(self, config: AppSetup):
"""Runs once when worker starts or config changes"""
self.model = load_model(config.model_id)
async def run(self, input_data: AppInput) -> AppOutput:
"""Runs for each request"""
return AppOutput(result="done")
async def unload(self):
"""Cleanup on shutdown"""
passField Types
| Type | Usage |
|---|---|
str, int, float, bool | Basic types |
File | File upload/output (.path for local path) |
Optional[T] | Nullable |
List[T] | Array |
Literal["a", "b"] | Enum dropdown |
Setup Parameters
Use AppSetup to define parameters that trigger re-initialization when changed:
class AppSetup(BaseAppInput):
model_id: str = Field(default="gpt2", description="Model to load")
precision: str = Field(default="fp16", description="Model precision")
class App(BaseApp):
async def setup(self, config: AppSetup):
from transformers import AutoModel
self.model = AutoModel.from_pretrained(config.model_id)OneOf Input Pattern (anyOf + x-promoted)
When an app accepts one of several input types (e.g. text OR file), use anyOf with x-promoted to show them as top-level toggle options in the UI:
class AppInput(BaseAppInput):
texts: Optional[List[str]] = Field(
default=None,
json_schema_extra={"x-promoted": True},
description="Texts to process.",
)
texts_file: Optional[File] = Field(
default=None,
json_schema_extra={"x-promoted": True},
description="File containing texts (one per line).",
)
model_config = {
"json_schema_extra": {
"anyOf": [
{"properties": {"texts": {"not": {"type": "null"}}}},
{"properties": {"texts_file": {"not": {"type": "null"}}}}
]
}
}Key points:
- Both fields are
Optionalwithdefault=Noneat the Pydantic level "x-promoted": Truesurfaces them as primary input options in the UImodel_config["json_schema_extra"]["anyOf"]enforces at least one must be non-null- Validate at runtime too:
if not input_data.texts and not input_data.texts_file: raise ValueError(...)
File Handling
# Input: auto-downloaded
image_path = input_data.image.path
# Output: auto-uploaded
return AppOutput(image=File(path="/tmp/output.png"))Multi-Function Apps
Apps can expose multiple functions with different input/output types.
IMPORTANT: If any output class needsoutput_meta, it MUST extendBaseAppOutput, notBaseModel. UsingBaseModelwill silently dropoutput_metafrom the response.
from inferencesh import BaseApp, BaseAppInput, BaseAppOutput
from pydantic import Field
class GreetInput(BaseAppInput):
name: str = Field(default="World", description="Name to greet")
class GreetOutput(BaseAppOutput):
message: str = Field(description="Greeting message")
class ReverseInput(BaseAppInput):
text: str = Field(description="Text to reverse")
class ReverseOutput(BaseAppOutput):
reversed_text: str = Field(description="Reversed text")
class App(BaseApp):
async def run(self, input_data: GreetInput) -> GreetOutput:
"""Default function."""
return GreetOutput(message=f"Hello, {input_data.name}!")
async def greet(self, input_data: GreetInput) -> GreetOutput:
"""Custom greeting."""
return GreetOutput(message=f"Welcome, {input_data.name}!")
async def reverse(self, input_data: ReverseInput) -> ReverseOutput:
"""Reverse text."""
return ReverseOutput(reversed_text=input_data.text[::-1])Functions are auto-discovered if they:
- Are public (no
_prefix) - Have type hints for input and return
- Use Pydantic models
Call via API with "function": "reverse" in the request body.
Default Function
By default, run is called when no function is specified. To change this, set default_function in inf.yml:
default_function: greetWhen set, requests without an explicit function parameter will call greet instead of run.
The on_cancel Hook
For long-running tasks:
async def on_cancel(self):
"""Called when user cancels — must return quickly"""
self.cancel_flag = True
return TruePython: Debugging, Optimization & Cancellation
Debugging Issues
Import Errors — "ModuleNotFoundError" in Production
1. Add __init__.py files to all packages 2. Add current directory to Python path:
import sys, os
sys.path.append(os.path.dirname(os.path.abspath(__file__)))3. For local packages, use editable installs in requirements.txt:
-e ./local_package_directoryCUDA Out of Memory
1. Reduce batch size 2. Use torch.float16 or bfloat16 3. model.gradient_checkpointing_enable() 4. torch.cuda.empty_cache() after requests 5. Increase vram in inf.yml
Memory Leaks
import gc, torch
async def run(self, input_data):
result = self.process(input_data)
if torch.cuda.is_available():
torch.cuda.empty_cache()
gc.collect()
return resultDevice Errors — "Expected all tensors to be on the same device"
input_tensor = input_tensor.to(self.device)"CUDA not available"
1. Check inf.yml GPU requirements:
resources:
gpu:
count: 1
vram: 242. Use device detection:
from accelerate import Accelerator
device = Accelerator().deviceModel Loading — "Token required for gated model"
Add HF_TOKEN to secrets:
secrets:
- key: HF_TOKEN
description: HuggingFace token for gated models"File not found" After Download
Don't assume file paths:
model_path = snapshot_download(repo_id="org/model")
config_path = os.path.join(model_path, "config.yaml")
if os.path.exists(config_path):
# Load configTemporary Files Deleted Too Early
with tempfile.NamedTemporaryFile(suffix='.jpg', delete=False) as tmp:
output_path = tmp.namePath Separators
path = os.path.join("models", "config", "settings.json")Version Conflicts
Pin compatible versions:
torch==2.6.0
numpy>=1.23.5,<2Debug Logging
import logging
logging.basicConfig(level=logging.DEBUG)
async def setup(self, config):
logging.debug(f"Config: {config}")
logging.info("Starting model load...")GPU Apps — Tips
Device Detection
Never use `torch.cuda.is_available()` — it doesn't reliably detect GPUs in grid containers. Always use accelerate:
from accelerate import Accelerator
class App(BaseApp):
async def setup(self, config):
accelerator = Accelerator()
self.device = accelerator.deviceMoving Models to GPU
For large models (>1B params), use `device_map` — it uses Accelerate's Big Model Inference to stream weights directly from disk to GPU via mmap, skipping CPU materialization entirely. This is dramatically faster than from_pretrained + .to():
# FAST — streams disk → GPU directly, skips CPU (use for large models)
from accelerate import Accelerator
device = Accelerator().device
self.model = AutoModel.from_pretrained("org/model", dtype=torch.bfloat16, device_map=str(device))
# SLOW — loads to CPU first, then copies to GPU (fine for small models)
self.model = AutoModel.from_pretrained("org/model")
self.model = self.model.to(device=self.device, dtype=torch.float16)Why it matters: Without device_map, PyTorch mmap's safetensors into CPU page cache, materializes every tensor, then copies to GPU via .to(). For a 27B model this takes ~2 minutes. With device_map, Accelerate creates a meta-device skeleton and streams each shard directly to GPU — same model loads in ~18 seconds (7x faster).
Note: device_map requires accelerate installed. For libraries that don't support device_map (custom model classes, some older libraries), fall back to from_pretrained + .to().
SentenceTransformer caveat: SentenceTransformer defers .to(device) until the first encode() call, not during __init__. For small models this is fine. For large models, prefer the raw transformers path with device_map.
Checklist for GPU Apps
accelerateinrequirements.txtAccelerator().devicefor device detectiondevice_map=str(device)for large models (>1B params)- Explicit
.to(device, dtype)for small models or unsupported libraries inf.ymlhasresources.gpu.count: 1and appropriatevram
Optimizing Performance
Model Loading
Use HuggingFace hub for downloads:
import os
from huggingface_hub import snapshot_download
os.environ["HF_HUB_ENABLE_HF_TRANSFER"] = "1"
class App(BaseApp):
async def setup(self, config):
self.model_path = snapshot_download(
repo_id="org/model-name",
resume_download=True,
)Avoid:
- Hardcoded local directories (
local_dir="./models") - Subprocess calls to
huggingface-cli - Assuming specific file structures
Memory Cleanup
import torch, gc
def cleanup_memory():
if torch.cuda.is_available():
torch.cuda.empty_cache()
gc.collect()
async def run(self, input_data):
result = self.model(input_data)
cleanup_memory()
return resultMixed Precision
model = model.to(dtype=torch.bfloat16)
# Or with autocast
from torch.amp import autocast
with autocast('cuda'):
output = model(input)Flash Attention
model = AutoModel.from_pretrained(
"model-name",
attn_implementation="flash_attention_2",
torch_dtype=torch.bfloat16
)Error Handling
import logging
async def run(self, input_data):
try:
result = self.process(input_data)
return AppOutput(result=result)
except Exception as e:
logging.error(f"Processing failed: {e}")
raise ValueError(f"Failed to process: {str(e)}")Pre-deploy Checklist
- [ ] All imports work
- [ ]
setup()loads models - [ ]
run()processes test input - [ ] No hardcoded paths/devices
- [ ] Memory cleaned up
Handling Cancellation
The on_cancel Hook
class App(BaseApp):
async def setup(self, config):
self.cancel_flag = False
async def on_cancel(self):
"""Called when user cancels the task"""
print("Cancellation requested...")
self.cancel_flag = True
return True
async def run(self, input_data):
self.cancel_flag = False
for i in range(100):
if self.cancel_flag:
print("Stopping work...")
break
await self.heavy_computation(i)Best Practices
1. Check frequently: In loops, check your cancellation flag at the start of every iteration. 2. Clean up: Close database connections, delete temporary files, or free GPU memory before exiting. 3. Return quickly: The on_cancel handler should be fast — just set a flag or signal an event. 4. Force kill: If an app does not respond to on_cancel within the timeout period (default 30s), it will be forcefully terminated (SIGKILL).
Python: Secrets & OAuth Integrations
Declaring Secrets
In inf.yml:
secrets:
- key: OPENAI_API_KEY
description: OpenAI API key
optional: false
- key: WEBHOOK_SECRET
description: Optional webhook secret
optional: true| Property | Type | Description |
|---|---|---|
key | string | Environment variable name |
description | string | Shown to users |
optional | boolean | If false, app won't run without it |
Accessing Secrets
import os
class App(BaseApp):
async def setup(self, config):
api_key = os.environ.get("OPENAI_API_KEY")
if not api_key:
raise ValueError("OPENAI_API_KEY required")
self.client = OpenAI(api_key=api_key)Common Patterns
External API Access
from openai import OpenAI
class App(BaseApp):
async def setup(self, config):
self.client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY"))HuggingFace Token
secrets:
- key: HF_TOKEN
description: HuggingFace token for gated modelsfrom huggingface_hub import snapshot_download
self.model_path = snapshot_download(
repo_id="meta-llama/Llama-2-7b",
token=os.environ.get("HF_TOKEN")
)Tips
- Use specific names (
OPENAI_API_KEYnotAPI_KEY) - Validate in
setup(), fail fast - Never log secret values
---
OAuth Integrations
Access external services (Google Sheets, Drive) on behalf of users through OAuth.
Declaring Integrations
In inf.yml:
integrations:
- key: google.sheets
description: Read/write Google Sheets
optional: false
- key: google.drive
description: Access to Google Drive files
optional: true| Property | Type | Description |
|---|---|---|
key | string | Integration identifier |
description | string | Shown to users |
optional | boolean | If false, app won't run without it |
Available Integrations
belt integrations list| Key | Description |
|---|---|
google.sheets | Read/write Sheets |
google.sheets.readonly | Read-only Sheets |
google.drive | Google Drive files |
google.sa | Service account |
Accessing Credentials
OAuth Integrations
import os, json
class App(BaseApp):
async def setup(self, config):
creds_json = os.environ.get("GOOGLE_OAUTH_CREDENTIALS")
if creds_json:
self.credentials = json.loads(creds_json)Service Account
from google.oauth2 import service_account
class App(BaseApp):
async def setup(self, config):
sa_json = os.environ.get("GOOGLE_SA_CREDENTIALS")
if sa_json:
self.credentials = service_account.Credentials.from_service_account_info(
json.loads(sa_json)
)Secrets vs Integrations
| Feature | Secrets | Integrations |
|---|---|---|
| User provides | Raw value (API key) | OAuth authorization |
| Refresh | Manual | Automatic |
| Scope control | None | Fine-grained |
| Best for | API keys | OAuth services |
Best Practices
1. Request minimal scopes — use readonly if you only read 2. Clear descriptions — explain why access is needed 3. Handle missing gracefully — check if optional integrations exist
Python: Tracking Usage (Output Metadata)
Enable usage-based pricing by reporting what your app processes.
CRITICAL: Output classes that includeoutput_metaMUST extendBaseAppOutput, notBaseModel. UsingBaseModelwill silently dropoutput_metafrom the response — everything else works, but usage tracking is lost.
Basic Structure
from inferencesh import BaseAppOutput, OutputMeta, TextMeta, ImageMeta, VideoMeta, AudioMeta
class AppOutput(BaseAppOutput): # NOT BaseModel!
result: File = Field(description="Generated output")
# output_meta is inherited from BaseAppOutputOutputMeta — Required Fields by Category
Always populate all required fields for the category. Read actual values from output files — don't hardcode.
| Category | Required Fields | Notes |
|---|---|---|
| image | width, height, count | Read dimensions from actual output file |
| video | width, height, seconds | Read from output file metadata |
| audio | seconds | Read from output file metadata |
| text | tokens | Use actual tokenizer counts |
MetaItem Types
| Type | Class | Fields |
|---|---|---|
| Text | TextMeta | tokens |
| Image | ImageMeta | width, height, resolution_mp, steps, count |
| Video | VideoMeta | width, height, resolution, seconds |
| Audio | AudioMeta | seconds |
| Raw | RawMeta | cost (dollar cents) |
Examples
LLM/Text Generation
Track both input (prompt) and output (completion) tokens:
from inferencesh.models.usage import OutputMeta, TextMeta
return AppOutput(
response=generated_text,
output_meta=OutputMeta(
inputs=[TextMeta(tokens=prompt_tokens)],
outputs=[TextMeta(tokens=completion_tokens)]
)
)Image Generation
return AppOutput(
image=File(path=output_path),
output_meta=OutputMeta(
outputs=[ImageMeta(
width=1024,
height=1024,
resolution_mp=1.05,
steps=20,
count=1
)]
)
)Video Generation
return AppOutput(
video=File(path=output_path),
output_meta=OutputMeta(
outputs=[VideoMeta(
width=1280,
height=720,
resolution="720p",
seconds=5.0
)]
)
)Audio Generation
return AppOutput(
audio=File(path=output_path),
output_meta=OutputMeta(
outputs=[AudioMeta(seconds=30.0)]
)
)Custom Data
Use extra for app-specific pricing factors:
output_meta=OutputMeta(
outputs=[ImageMeta(
width=1024,
height=1024,
extra={
"model": "sdxl-turbo",
"lora_count": 2
}
)]
)Best Practices
1. Always populate `output_meta` if usage varies per request 2. Use accurate token counts from the actual tokenizer 3. Report actual dimensions — don't hardcode 4. Include relevant `extra` data for pricing flexibility
Related skills
FAQ
What does building-inferencesh-apps cover?
Agent guidance for Inference.sh apps from the qu-skills collection.
Which agents?
Designed for Claude Code and Cursor workflows.