@inferencesh/app 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,236 @@
1
+ # @inferencesh/app — build inference.sh apps in node.js
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@inferencesh/app.svg)](https://www.npmjs.com/package/@inferencesh/app)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-blue.svg)](https://www.typescriptlang.org/)
6
+
7
+ app framework for building [inference.sh](https://inference.sh) apps in node.js — file handling, output metadata, and storage utilities.
8
+
9
+ this is the **app-side** sdk. for the **client-side** sdk (calling apps, agents, file uploads), see [@inferencesh/sdk](https://www.npmjs.com/package/@inferencesh/sdk).
10
+
11
+ ## installation
12
+
13
+ ```bash
14
+ npm install @inferencesh/app
15
+ ```
16
+
17
+ ## what's included
18
+
19
+ | export | description |
20
+ |--------|-------------|
21
+ | `File` | smart file reference — downloads urls, caches locally, resolves paths, serializes for the engine |
22
+ | `StorageDir` | standard storage directory constants (`DATA`, `TEMP`, `CACHE`) |
23
+ | `download()` | download a url to a directory with caching |
24
+ | `textMeta`, `imageMeta`, `videoMeta`, `audioMeta`, `rawMeta` | output metadata factories for usage-based pricing |
25
+
26
+ ## file handling
27
+
28
+ the `File` class handles downloading, caching, and path resolution — the node.js equivalent of the python sdk's `File` class.
29
+
30
+ ### reading input files
31
+
32
+ ```javascript
33
+ import { File } from "@inferencesh/app";
34
+
35
+ async run(inputData) {
36
+ // Input files come as URLs — File downloads and caches them
37
+ const file = await File.from(inputData.image);
38
+ console.log(file.path); // /home/.cache/inferencesh/files/abc123/image.jpg
39
+ console.log(file.contentType); // image/jpeg
40
+ console.log(file.size); // 102400
41
+ }
42
+ ```
43
+
44
+ ### returning output files
45
+
46
+ ```javascript
47
+ import { File } from "@inferencesh/app";
48
+
49
+ async run(inputData) {
50
+ const outputPath = "/tmp/result.png";
51
+ await generateImage(inputData.prompt, outputPath);
52
+
53
+ // File.fromPath is sync — no download needed for local files
54
+ return { image: File.fromPath(outputPath) };
55
+ }
56
+ ```
57
+
58
+ the engine reads `path` from the serialized output and uploads it to cdn automatically.
59
+
60
+ ### how File serializes
61
+
62
+ `File` implements `toJSON()` so it works seamlessly with `JSON.stringify`:
63
+
64
+ ```javascript
65
+ const file = File.fromPath("/tmp/output.png");
66
+ JSON.stringify(file);
67
+ // {"path":"/tmp/output.png","content_type":"image/png","size":102400,"filename":"output.png"}
68
+ ```
69
+
70
+ ### construction options
71
+
72
+ ```javascript
73
+ // From URL (downloads and caches)
74
+ const file = await File.from("https://example.com/image.jpg");
75
+
76
+ // From local path
77
+ const file = await File.from("/tmp/output.png");
78
+
79
+ // From options object
80
+ const file = await File.from({ path: "/tmp/output.png", contentType: "image/png" });
81
+
82
+ // From engine-style data (snake_case)
83
+ const file = await File.from({ uri: "https://...", content_type: "image/jpeg" });
84
+
85
+ // Sync from local path (no download)
86
+ const file = File.fromPath("/tmp/output.png");
87
+ ```
88
+
89
+ ### caching
90
+
91
+ downloaded files are cached at `~/.cache/inferencesh/files/{url_hash}/{filename}`. set `FILE_CACHE_DIR` to override.
92
+
93
+ ## storage directories
94
+
95
+ ```javascript
96
+ import { StorageDir, ensureDir } from "@inferencesh/app";
97
+
98
+ // Standard directories available on inference.sh workers
99
+ StorageDir.DATA // "/app/data" — persistent storage
100
+ StorageDir.TEMP // "/app/tmp" — cleaned between runs
101
+ StorageDir.CACHE // "/app/cache" — persists, may be evicted
102
+
103
+ // Ensure directory exists
104
+ const dataDir = ensureDir(StorageDir.DATA);
105
+ ```
106
+
107
+ ## download utility
108
+
109
+ ```javascript
110
+ import { download, StorageDir } from "@inferencesh/app";
111
+
112
+ // Download to a specific directory with caching
113
+ const modelPath = await download(
114
+ "https://huggingface.co/org/model/resolve/main/weights.bin",
115
+ StorageDir.CACHE
116
+ );
117
+ // Returns: /app/cache/{hash}/weights.bin
118
+ ```
119
+
120
+ skips download if the file already exists in the target directory (except for `TEMP`).
121
+
122
+ ## output metadata
123
+
124
+ report what your app processes and generates for usage-based pricing:
125
+
126
+ ```javascript
127
+ import { textMeta, imageMeta, videoMeta, audioMeta } from "@inferencesh/app";
128
+
129
+ // LLM app
130
+ async run(inputData) {
131
+ const result = await this.llm.generate(inputData.prompt);
132
+ return {
133
+ response: result.text,
134
+ output_meta: {
135
+ inputs: [textMeta({ tokens: result.promptTokens })],
136
+ outputs: [textMeta({ tokens: result.completionTokens })],
137
+ },
138
+ };
139
+ }
140
+
141
+ // Image generation app
142
+ async run(inputData) {
143
+ const image = await this.model.generate(inputData.prompt);
144
+ return {
145
+ image: File.fromPath(image.path),
146
+ output_meta: {
147
+ outputs: [imageMeta({ width: 1024, height: 1024, steps: 20 })],
148
+ },
149
+ };
150
+ }
151
+
152
+ // Video generation app
153
+ async run(inputData) {
154
+ return {
155
+ video: File.fromPath(videoPath),
156
+ output_meta: {
157
+ outputs: [videoMeta({ resolution: "1080p", seconds: 5.0, fps: 30 })],
158
+ },
159
+ };
160
+ }
161
+ ```
162
+
163
+ ### meta types
164
+
165
+ | factory | key fields |
166
+ |---------|-----------|
167
+ | `textMeta` | `tokens` |
168
+ | `imageMeta` | `width`, `height`, `resolution_mp`, `steps`, `count` |
169
+ | `videoMeta` | `width`, `height`, `resolution`, `seconds`, `fps` |
170
+ | `audioMeta` | `seconds`, `sample_rate` |
171
+ | `rawMeta` | `cost` (dollar cents) |
172
+
173
+ all meta types support an optional `extra` field for app-specific pricing factors.
174
+
175
+ ## full app example
176
+
177
+ ```javascript
178
+ import { z } from "zod";
179
+ import { File, textMeta } from "@inferencesh/app";
180
+
181
+ export const RunInput = z.object({
182
+ prompt: z.string().describe("Input prompt"),
183
+ image: z.string().optional().describe("Optional image URL"),
184
+ });
185
+
186
+ export const RunOutput = z.object({
187
+ result: z.string(),
188
+ processedImage: z.any().optional(),
189
+ });
190
+
191
+ export class App {
192
+ async setup(config) {
193
+ this.model = await loadModel();
194
+ }
195
+
196
+ async run(inputData) {
197
+ // Handle input files
198
+ let imageFile;
199
+ if (inputData.image) {
200
+ imageFile = await File.from(inputData.image);
201
+ }
202
+
203
+ const result = await this.model.process({
204
+ prompt: inputData.prompt,
205
+ imagePath: imageFile?.path,
206
+ });
207
+
208
+ return {
209
+ result: result.text,
210
+ processedImage: result.outputPath
211
+ ? File.fromPath(result.outputPath)
212
+ : undefined,
213
+ output_meta: {
214
+ inputs: [textMeta({ tokens: result.inputTokens })],
215
+ outputs: [textMeta({ tokens: result.outputTokens })],
216
+ },
217
+ };
218
+ }
219
+ }
220
+ ```
221
+
222
+ ## requirements
223
+
224
+ - node.js 18.0.0 or higher
225
+ - zero runtime dependencies
226
+
227
+ ## resources
228
+
229
+ - [documentation](https://inference.sh/docs) — getting started guides
230
+ - [app development guide](https://inference.sh/docs/extend/app-code) — writing app logic
231
+ - [client sdk](https://www.npmjs.com/package/@inferencesh/sdk) — calling apps from your code
232
+ - [discord](https://discord.gg/RM77SWSbyT) — community support
233
+
234
+ ## license
235
+
236
+ MIT © [inference.sh](https://inference.sh)
@@ -0,0 +1,15 @@
1
+ import { type StorageDirValue } from "./storage.js";
2
+ /**
3
+ * Download a file to a directory and return its local path.
4
+ *
5
+ * Uses the same cache as `File.from()`. If the file already exists in the
6
+ * target directory (and it's not TEMP), returns the cached copy.
7
+ *
8
+ * @example
9
+ * ```js
10
+ * import { download, StorageDir } from "@inferencesh/app";
11
+ *
12
+ * const path = await download("https://example.com/model.bin", StorageDir.CACHE);
13
+ * ```
14
+ */
15
+ export declare function download(url: string, directory: StorageDirValue | string): Promise<string>;
@@ -0,0 +1,43 @@
1
+ import { existsSync, mkdirSync, copyFileSync } from "node:fs";
2
+ import { join, basename } from "node:path";
3
+ import { createHash } from "node:crypto";
4
+ import { File } from "./file.js";
5
+ import { StorageDir } from "./storage.js";
6
+ /**
7
+ * Download a file to a directory and return its local path.
8
+ *
9
+ * Uses the same cache as `File.from()`. If the file already exists in the
10
+ * target directory (and it's not TEMP), returns the cached copy.
11
+ *
12
+ * @example
13
+ * ```js
14
+ * import { download, StorageDir } from "@inferencesh/app";
15
+ *
16
+ * const path = await download("https://example.com/model.bin", StorageDir.CACHE);
17
+ * ```
18
+ */
19
+ export async function download(url, directory) {
20
+ const dirPath = directory;
21
+ mkdirSync(dirPath, { recursive: true });
22
+ // Build output path with hash subdirectory (matches Python SDK)
23
+ const parsed = new URL(url);
24
+ let components = parsed.host + parsed.pathname;
25
+ if (parsed.search)
26
+ components += parsed.search;
27
+ const hash = createHash("sha256").update(components).digest("hex").slice(0, 12);
28
+ const filename = basename(parsed.pathname) || "download";
29
+ const hashDir = join(dirPath, hash);
30
+ mkdirSync(hashDir, { recursive: true });
31
+ const outputPath = join(hashDir, filename);
32
+ // Skip download if already in target directory (unless TEMP)
33
+ if (existsSync(outputPath) && directory !== StorageDir.TEMP) {
34
+ return outputPath;
35
+ }
36
+ // Download via File (uses File's own cache)
37
+ const file = await File.from(url);
38
+ if (file.path) {
39
+ copyFileSync(file.path, outputPath);
40
+ return outputPath;
41
+ }
42
+ throw new Error(`Failed to download ${url}`);
43
+ }
package/dist/file.d.ts ADDED
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Options for constructing a File.
3
+ */
4
+ export interface FileOptions {
5
+ uri?: string;
6
+ path?: string;
7
+ contentType?: string;
8
+ size?: number;
9
+ filename?: string;
10
+ }
11
+ /**
12
+ * Serialized File representation — what the engine sees in task output.
13
+ */
14
+ export interface FileData {
15
+ uri?: string;
16
+ path?: string;
17
+ content_type?: string;
18
+ size?: number;
19
+ filename?: string;
20
+ }
21
+ /**
22
+ * A file in the inference.sh ecosystem.
23
+ *
24
+ * Accepts a URL, local path, or options object.
25
+ * URLs are downloaded and cached locally on construction (via `await File.from()`).
26
+ * Local paths are resolved to absolute paths.
27
+ *
28
+ * In JSON output, File serializes to `{ path, uri, content_type, size, filename }`
29
+ * — the engine uploads local `path` files to CDN and replaces with `uri`.
30
+ */
31
+ export declare class File {
32
+ uri?: string;
33
+ path?: string;
34
+ contentType?: string;
35
+ size?: number;
36
+ filename?: string;
37
+ private constructor();
38
+ /**
39
+ * Create a File from a URL, local path, or options object.
40
+ * URLs are downloaded and cached automatically.
41
+ *
42
+ * @example
43
+ * ```js
44
+ * // From local path
45
+ * const file = await File.from("/tmp/output.png");
46
+ *
47
+ * // From URL (downloads and caches)
48
+ * const file = await File.from("https://example.com/image.jpg");
49
+ *
50
+ * // From options
51
+ * const file = await File.from({ path: "/tmp/output.png", contentType: "image/png" });
52
+ * ```
53
+ */
54
+ static from(input: string | FileData | FileOptions | File): Promise<File>;
55
+ /**
56
+ * Create a File from a local path (sync, no download).
57
+ */
58
+ static fromPath(localPath: string): File;
59
+ /**
60
+ * Check if the file exists on disk.
61
+ */
62
+ exists(): boolean;
63
+ /**
64
+ * Re-read metadata (contentType, size, filename) from disk.
65
+ */
66
+ refreshMetadata(): void;
67
+ /**
68
+ * Serialize to a plain object for JSON output.
69
+ * The engine reads `path` fields and uploads them to CDN.
70
+ */
71
+ toJSON(): FileData;
72
+ static getCacheDir(): string;
73
+ private _getCachePath;
74
+ private _downloadUrl;
75
+ private _populateMetadata;
76
+ }
package/dist/file.js ADDED
@@ -0,0 +1,235 @@
1
+ import { createHash } from "node:crypto";
2
+ import { createWriteStream, existsSync, mkdirSync, statSync, renameSync, unlinkSync } from "node:fs";
3
+ import { basename, resolve, join } from "node:path";
4
+ import { homedir } from "node:os";
5
+ import { get as httpsGet } from "node:https";
6
+ import { get as httpGet } from "node:http";
7
+ import { URL } from "node:url";
8
+ /**
9
+ * A file in the inference.sh ecosystem.
10
+ *
11
+ * Accepts a URL, local path, or options object.
12
+ * URLs are downloaded and cached locally on construction (via `await File.from()`).
13
+ * Local paths are resolved to absolute paths.
14
+ *
15
+ * In JSON output, File serializes to `{ path, uri, content_type, size, filename }`
16
+ * — the engine uploads local `path` files to CDN and replaces with `uri`.
17
+ */
18
+ export class File {
19
+ uri;
20
+ path;
21
+ contentType;
22
+ size;
23
+ filename;
24
+ constructor(options) {
25
+ this.uri = options.uri;
26
+ this.path = options.path;
27
+ this.contentType = options.contentType;
28
+ this.size = options.size;
29
+ this.filename = options.filename;
30
+ }
31
+ /**
32
+ * Create a File from a URL, local path, or options object.
33
+ * URLs are downloaded and cached automatically.
34
+ *
35
+ * @example
36
+ * ```js
37
+ * // From local path
38
+ * const file = await File.from("/tmp/output.png");
39
+ *
40
+ * // From URL (downloads and caches)
41
+ * const file = await File.from("https://example.com/image.jpg");
42
+ *
43
+ * // From options
44
+ * const file = await File.from({ path: "/tmp/output.png", contentType: "image/png" });
45
+ * ```
46
+ */
47
+ static async from(input) {
48
+ if (input instanceof File) {
49
+ return new File({
50
+ uri: input.uri,
51
+ path: input.path,
52
+ contentType: input.contentType,
53
+ size: input.size,
54
+ filename: input.filename,
55
+ });
56
+ }
57
+ let options;
58
+ if (typeof input === "string") {
59
+ options = { uri: input };
60
+ }
61
+ else {
62
+ const data = input;
63
+ options = {
64
+ uri: data.uri,
65
+ path: data.path,
66
+ contentType: data.content_type ?? data.contentType,
67
+ size: data.size,
68
+ filename: data.filename,
69
+ };
70
+ }
71
+ if (!options.uri && !options.path) {
72
+ throw new Error("Either 'uri' or 'path' must be provided");
73
+ }
74
+ const file = new File(options);
75
+ // Resolve URI
76
+ if (file.uri) {
77
+ if (isUrl(file.uri)) {
78
+ await file._downloadUrl(file.uri);
79
+ }
80
+ else {
81
+ // Treat as local path
82
+ file.path = resolve(file.uri);
83
+ }
84
+ }
85
+ if (file.path) {
86
+ file.path = resolve(file.path);
87
+ file._populateMetadata();
88
+ }
89
+ else {
90
+ throw new Error("Either 'uri' or 'path' must be provided and be valid");
91
+ }
92
+ return file;
93
+ }
94
+ /**
95
+ * Create a File from a local path (sync, no download).
96
+ */
97
+ static fromPath(localPath) {
98
+ const absPath = resolve(localPath);
99
+ const file = new File({ path: absPath });
100
+ file._populateMetadata();
101
+ return file;
102
+ }
103
+ /**
104
+ * Check if the file exists on disk.
105
+ */
106
+ exists() {
107
+ return this.path != null && existsSync(this.path);
108
+ }
109
+ /**
110
+ * Re-read metadata (contentType, size, filename) from disk.
111
+ */
112
+ refreshMetadata() {
113
+ this._populateMetadata();
114
+ }
115
+ /**
116
+ * Serialize to a plain object for JSON output.
117
+ * The engine reads `path` fields and uploads them to CDN.
118
+ */
119
+ toJSON() {
120
+ const result = {};
121
+ if (this.uri != null)
122
+ result.uri = this.uri;
123
+ if (this.path != null)
124
+ result.path = this.path;
125
+ if (this.contentType != null)
126
+ result.content_type = this.contentType;
127
+ if (this.size != null)
128
+ result.size = this.size;
129
+ if (this.filename != null)
130
+ result.filename = this.filename;
131
+ return result;
132
+ }
133
+ // --- Cache ---
134
+ static getCacheDir() {
135
+ const envDir = process.env.FILE_CACHE_DIR;
136
+ const dir = envDir || join(homedir(), ".cache", "inferencesh", "files");
137
+ mkdirSync(dir, { recursive: true });
138
+ return dir;
139
+ }
140
+ _getCachePath(url) {
141
+ const parsed = new URL(url);
142
+ let components = parsed.host + parsed.pathname;
143
+ if (parsed.search)
144
+ components += parsed.search;
145
+ const hash = createHash("sha256").update(components).digest("hex").slice(0, 12);
146
+ const fname = basename(parsed.pathname) || "download";
147
+ const hashDir = join(File.getCacheDir(), hash);
148
+ mkdirSync(hashDir, { recursive: true });
149
+ return join(hashDir, fname);
150
+ }
151
+ // --- Download ---
152
+ async _downloadUrl(url) {
153
+ const cachePath = this._getCachePath(url);
154
+ if (existsSync(cachePath)) {
155
+ this.path = cachePath;
156
+ return;
157
+ }
158
+ const tmpPath = cachePath + ".tmp";
159
+ try {
160
+ await downloadToFile(url, tmpPath);
161
+ renameSync(tmpPath, cachePath);
162
+ this.path = cachePath;
163
+ }
164
+ catch (err) {
165
+ try {
166
+ unlinkSync(tmpPath);
167
+ }
168
+ catch { /* ignore */ }
169
+ throw new Error(`Failed to download ${url}: ${err.message}`);
170
+ }
171
+ }
172
+ // --- Metadata ---
173
+ _populateMetadata() {
174
+ if (!this.path || !existsSync(this.path))
175
+ return;
176
+ if (!this.contentType) {
177
+ this.contentType = guessContentType(this.path);
178
+ }
179
+ if (this.size == null) {
180
+ try {
181
+ this.size = statSync(this.path).size;
182
+ }
183
+ catch { /* ignore */ }
184
+ }
185
+ if (!this.filename) {
186
+ this.filename = basename(this.path);
187
+ }
188
+ }
189
+ }
190
+ // --- Helpers ---
191
+ function isUrl(s) {
192
+ return s.startsWith("http://") || s.startsWith("https://");
193
+ }
194
+ function downloadToFile(url, destPath) {
195
+ return new Promise((resolve, reject) => {
196
+ const parsed = new URL(url);
197
+ const getter = parsed.protocol === "https:" ? httpsGet : httpGet;
198
+ const request = getter(url, (response) => {
199
+ // Follow redirects
200
+ if (response.statusCode && response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
201
+ downloadToFile(response.headers.location, destPath).then(resolve, reject);
202
+ return;
203
+ }
204
+ if (response.statusCode && response.statusCode >= 400) {
205
+ reject(new Error(`HTTP ${response.statusCode}`));
206
+ return;
207
+ }
208
+ const dir = join(destPath, "..");
209
+ mkdirSync(dir, { recursive: true });
210
+ const stream = createWriteStream(destPath);
211
+ response.pipe(stream);
212
+ stream.on("finish", () => {
213
+ stream.close();
214
+ resolve();
215
+ });
216
+ stream.on("error", reject);
217
+ });
218
+ request.on("error", reject);
219
+ });
220
+ }
221
+ const MIME_TYPES = {
222
+ ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".png": "image/png",
223
+ ".gif": "image/gif", ".webp": "image/webp", ".svg": "image/svg+xml",
224
+ ".mp4": "video/mp4", ".webm": "video/webm", ".mov": "video/quicktime",
225
+ ".mp3": "audio/mpeg", ".wav": "audio/wav", ".ogg": "audio/ogg",
226
+ ".flac": "audio/flac", ".aac": "audio/aac",
227
+ ".pdf": "application/pdf", ".json": "application/json",
228
+ ".txt": "text/plain", ".csv": "text/csv", ".html": "text/html",
229
+ ".zip": "application/zip", ".tar": "application/x-tar",
230
+ ".gz": "application/gzip",
231
+ };
232
+ function guessContentType(filePath) {
233
+ const ext = filePath.slice(filePath.lastIndexOf(".")).toLowerCase();
234
+ return MIME_TYPES[ext];
235
+ }
@@ -0,0 +1,7 @@
1
+ export { File } from "./file.js";
2
+ export type { FileOptions, FileData } from "./file.js";
3
+ export { StorageDir, ensureDir } from "./storage.js";
4
+ export type { StorageDirValue } from "./storage.js";
5
+ export { download } from "./download.js";
6
+ export { textMeta, imageMeta, videoMeta, audioMeta, rawMeta } from "./output-meta.js";
7
+ export type { OutputMeta, MetaItem, MetaItemBase, TextMeta, ImageMeta, VideoMeta, AudioMeta, RawMeta, } from "./output-meta.js";
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ // File handling
2
+ export { File } from "./file.js";
3
+ // Storage directories
4
+ export { StorageDir, ensureDir } from "./storage.js";
5
+ // Download utility
6
+ export { download } from "./download.js";
7
+ // Output metadata for usage-based pricing
8
+ export { textMeta, imageMeta, videoMeta, audioMeta, rawMeta } from "./output-meta.js";
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Output metadata types for usage-based pricing.
3
+ *
4
+ * Apps include OutputMeta in their run output to report what was consumed
5
+ * (inputs) and what was produced (outputs). The backend uses this for
6
+ * pricing calculation.
7
+ *
8
+ * @example
9
+ * ```js
10
+ * import { textMeta, imageMeta } from "@inferencesh/app";
11
+ *
12
+ * return {
13
+ * result: generatedText,
14
+ * output_meta: {
15
+ * inputs: [textMeta({ tokens: promptTokens })],
16
+ * outputs: [textMeta({ tokens: completionTokens })],
17
+ * },
18
+ * };
19
+ * ```
20
+ */
21
+ export interface MetaItemBase {
22
+ type: string;
23
+ extra?: Record<string, unknown>;
24
+ }
25
+ export interface TextMeta extends MetaItemBase {
26
+ type: "text";
27
+ tokens: number;
28
+ }
29
+ export interface ImageMeta extends MetaItemBase {
30
+ type: "image";
31
+ width?: number;
32
+ height?: number;
33
+ resolution_mp?: number;
34
+ steps?: number;
35
+ count?: number;
36
+ }
37
+ export interface VideoMeta extends MetaItemBase {
38
+ type: "video";
39
+ width?: number;
40
+ height?: number;
41
+ resolution_mp?: number;
42
+ resolution?: "480p" | "720p" | "1080p" | "1440p" | "4k";
43
+ seconds?: number;
44
+ fps?: number;
45
+ }
46
+ export interface AudioMeta extends MetaItemBase {
47
+ type: "audio";
48
+ seconds?: number;
49
+ sample_rate?: number;
50
+ }
51
+ export interface RawMeta extends MetaItemBase {
52
+ type: "raw";
53
+ cost?: number;
54
+ }
55
+ export type MetaItem = TextMeta | ImageMeta | VideoMeta | AudioMeta | RawMeta;
56
+ export interface OutputMeta {
57
+ inputs?: MetaItem[];
58
+ outputs?: MetaItem[];
59
+ }
60
+ /** Create a text metadata item. */
61
+ export declare function textMeta(opts: Omit<TextMeta, "type">): TextMeta;
62
+ /** Create an image metadata item. */
63
+ export declare function imageMeta(opts?: Omit<ImageMeta, "type">): ImageMeta;
64
+ /** Create a video metadata item. */
65
+ export declare function videoMeta(opts?: Omit<VideoMeta, "type">): VideoMeta;
66
+ /** Create an audio metadata item. */
67
+ export declare function audioMeta(opts?: Omit<AudioMeta, "type">): AudioMeta;
68
+ /** Create a raw metadata item (custom pricing). */
69
+ export declare function rawMeta(opts?: Omit<RawMeta, "type">): RawMeta;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Output metadata types for usage-based pricing.
3
+ *
4
+ * Apps include OutputMeta in their run output to report what was consumed
5
+ * (inputs) and what was produced (outputs). The backend uses this for
6
+ * pricing calculation.
7
+ *
8
+ * @example
9
+ * ```js
10
+ * import { textMeta, imageMeta } from "@inferencesh/app";
11
+ *
12
+ * return {
13
+ * result: generatedText,
14
+ * output_meta: {
15
+ * inputs: [textMeta({ tokens: promptTokens })],
16
+ * outputs: [textMeta({ tokens: completionTokens })],
17
+ * },
18
+ * };
19
+ * ```
20
+ */
21
+ // --- Factories ---
22
+ /** Create a text metadata item. */
23
+ export function textMeta(opts) {
24
+ return { type: "text", ...opts };
25
+ }
26
+ /** Create an image metadata item. */
27
+ export function imageMeta(opts = {}) {
28
+ return { type: "image", ...opts };
29
+ }
30
+ /** Create a video metadata item. */
31
+ export function videoMeta(opts = {}) {
32
+ return { type: "video", ...opts };
33
+ }
34
+ /** Create an audio metadata item. */
35
+ export function audioMeta(opts = {}) {
36
+ return { type: "audio", ...opts };
37
+ }
38
+ /** Create a raw metadata item (custom pricing). */
39
+ export function rawMeta(opts = {}) {
40
+ return { type: "raw", ...opts };
41
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Standard storage directories available to inference.sh apps at runtime.
3
+ */
4
+ export declare const StorageDir: {
5
+ /** Persistent storage — survives across runs */
6
+ readonly DATA: "/app/data";
7
+ /** Temporary storage — cleaned between runs */
8
+ readonly TEMP: "/app/tmp";
9
+ /** Cache storage — persists across runs, may be evicted */
10
+ readonly CACHE: "/app/cache";
11
+ };
12
+ export type StorageDirValue = (typeof StorageDir)[keyof typeof StorageDir];
13
+ /**
14
+ * Ensure a storage directory exists and return its path.
15
+ *
16
+ * @example
17
+ * ```js
18
+ * import { ensureDir, StorageDir } from "@inferencesh/app";
19
+ *
20
+ * const dataDir = ensureDir(StorageDir.DATA);
21
+ * ```
22
+ */
23
+ export declare function ensureDir(dir: StorageDirValue | string): string;
@@ -0,0 +1,26 @@
1
+ import { mkdirSync } from "node:fs";
2
+ /**
3
+ * Standard storage directories available to inference.sh apps at runtime.
4
+ */
5
+ export const StorageDir = {
6
+ /** Persistent storage — survives across runs */
7
+ DATA: "/app/data",
8
+ /** Temporary storage — cleaned between runs */
9
+ TEMP: "/app/tmp",
10
+ /** Cache storage — persists across runs, may be evicted */
11
+ CACHE: "/app/cache",
12
+ };
13
+ /**
14
+ * Ensure a storage directory exists and return its path.
15
+ *
16
+ * @example
17
+ * ```js
18
+ * import { ensureDir, StorageDir } from "@inferencesh/app";
19
+ *
20
+ * const dataDir = ensureDir(StorageDir.DATA);
21
+ * ```
22
+ */
23
+ export function ensureDir(dir) {
24
+ mkdirSync(dir, { recursive: true });
25
+ return dir;
26
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,70 @@
1
+ import { describe, it, before, after } from "node:test";
2
+ import assert from "node:assert";
3
+ import { writeFileSync, mkdirSync, rmSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { tmpdir } from "node:os";
6
+ import { File } from "../file.js";
7
+ const TEST_DIR = join(tmpdir(), "inferencesh-app-test-" + Date.now());
8
+ let testFile;
9
+ describe("File", () => {
10
+ before(() => {
11
+ mkdirSync(TEST_DIR, { recursive: true });
12
+ testFile = join(TEST_DIR, "hello.txt");
13
+ writeFileSync(testFile, "hello world");
14
+ });
15
+ after(() => {
16
+ rmSync(TEST_DIR, { recursive: true, force: true });
17
+ });
18
+ it("creates from local path", () => {
19
+ const file = File.fromPath(testFile);
20
+ assert.ok(file.path);
21
+ assert.ok(file.exists());
22
+ assert.strictEqual(file.filename, "hello.txt");
23
+ assert.strictEqual(file.contentType, "text/plain");
24
+ assert.strictEqual(file.size, 11);
25
+ });
26
+ it("creates from path via async from()", async () => {
27
+ const file = await File.from(testFile);
28
+ assert.ok(file.path);
29
+ assert.ok(file.exists());
30
+ assert.strictEqual(file.filename, "hello.txt");
31
+ });
32
+ it("creates from options object", async () => {
33
+ const file = await File.from({ path: testFile, contentType: "text/plain" });
34
+ assert.ok(file.exists());
35
+ assert.strictEqual(file.contentType, "text/plain");
36
+ });
37
+ it("creates from FileData with content_type (snake_case)", async () => {
38
+ const file = await File.from({ path: testFile, content_type: "application/octet-stream" });
39
+ assert.strictEqual(file.contentType, "application/octet-stream");
40
+ });
41
+ it("creates from another File", async () => {
42
+ const original = File.fromPath(testFile);
43
+ const copy = await File.from(original);
44
+ assert.strictEqual(copy.path, original.path);
45
+ assert.strictEqual(copy.filename, original.filename);
46
+ });
47
+ it("serializes to JSON with snake_case", () => {
48
+ const file = File.fromPath(testFile);
49
+ const json = file.toJSON();
50
+ assert.ok(json.path);
51
+ assert.strictEqual(json.content_type, "text/plain");
52
+ assert.strictEqual(json.size, 11);
53
+ assert.strictEqual(json.filename, "hello.txt");
54
+ assert.strictEqual(json.uri, undefined);
55
+ });
56
+ it("works with JSON.stringify", () => {
57
+ const file = File.fromPath(testFile);
58
+ const str = JSON.stringify({ image: file });
59
+ const parsed = JSON.parse(str);
60
+ assert.ok(parsed.image.path);
61
+ assert.strictEqual(parsed.image.content_type, "text/plain");
62
+ });
63
+ it("resolves relative paths to absolute", () => {
64
+ const file = File.fromPath("./package.json");
65
+ assert.ok(file.path.startsWith("/"));
66
+ });
67
+ it("throws on missing path and uri", async () => {
68
+ await assert.rejects(() => File.from({}), /Either 'uri' or 'path' must be provided/);
69
+ });
70
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,48 @@
1
+ import { describe, it } from "node:test";
2
+ import assert from "node:assert";
3
+ import { textMeta, imageMeta, videoMeta, audioMeta, rawMeta } from "../output-meta.js";
4
+ describe("OutputMeta", () => {
5
+ it("creates text meta", () => {
6
+ const meta = textMeta({ tokens: 150 });
7
+ assert.strictEqual(meta.type, "text");
8
+ assert.strictEqual(meta.tokens, 150);
9
+ });
10
+ it("creates image meta", () => {
11
+ const meta = imageMeta({ width: 1024, height: 1024, steps: 20, count: 1 });
12
+ assert.strictEqual(meta.type, "image");
13
+ assert.strictEqual(meta.width, 1024);
14
+ assert.strictEqual(meta.steps, 20);
15
+ });
16
+ it("creates video meta", () => {
17
+ const meta = videoMeta({ resolution: "1080p", seconds: 5.0 });
18
+ assert.strictEqual(meta.type, "video");
19
+ assert.strictEqual(meta.resolution, "1080p");
20
+ });
21
+ it("creates audio meta", () => {
22
+ const meta = audioMeta({ seconds: 30.0 });
23
+ assert.strictEqual(meta.type, "audio");
24
+ assert.strictEqual(meta.seconds, 30.0);
25
+ });
26
+ it("creates raw meta", () => {
27
+ const meta = rawMeta({ cost: 0.5 });
28
+ assert.strictEqual(meta.type, "raw");
29
+ assert.strictEqual(meta.cost, 0.5);
30
+ });
31
+ it("supports extra data", () => {
32
+ const meta = imageMeta({ width: 512, height: 512, extra: { model: "sdxl" } });
33
+ assert.strictEqual(meta.extra?.model, "sdxl");
34
+ });
35
+ it("composes into OutputMeta", () => {
36
+ const output = {
37
+ inputs: [textMeta({ tokens: 100 })],
38
+ outputs: [textMeta({ tokens: 500 }), imageMeta({ width: 1024, height: 1024 })],
39
+ };
40
+ assert.strictEqual(output.inputs.length, 1);
41
+ assert.strictEqual(output.outputs.length, 2);
42
+ // Serializes cleanly
43
+ const json = JSON.stringify(output);
44
+ const parsed = JSON.parse(json);
45
+ assert.strictEqual(parsed.inputs[0].type, "text");
46
+ assert.strictEqual(parsed.outputs[1].type, "image");
47
+ });
48
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,10 @@
1
+ import { describe, it } from "node:test";
2
+ import assert from "node:assert";
3
+ import { StorageDir } from "../storage.js";
4
+ describe("StorageDir", () => {
5
+ it("has correct paths", () => {
6
+ assert.strictEqual(StorageDir.DATA, "/app/data");
7
+ assert.strictEqual(StorageDir.TEMP, "/app/tmp");
8
+ assert.strictEqual(StorageDir.CACHE, "/app/cache");
9
+ });
10
+ });
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@inferencesh/app",
3
+ "version": "0.1.0",
4
+ "description": "App framework for building inference.sh apps — File handling, output metadata, storage utilities",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ }
13
+ },
14
+ "scripts": {
15
+ "build": "tsc",
16
+ "test": "node --test dist/test/*.test.js",
17
+ "clean": "rimraf dist",
18
+ "prepublishOnly": "npm run clean && npm run build && npm test"
19
+ },
20
+ "keywords": [
21
+ "inference",
22
+ "ai",
23
+ "ml",
24
+ "app",
25
+ "sdk",
26
+ "file",
27
+ "typescript"
28
+ ],
29
+ "author": "Okaris <hello@inference.sh>",
30
+ "license": "MIT",
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "git+https://github.com/inference-sh/sdk-js-app.git"
34
+ },
35
+ "homepage": "https://inference.sh",
36
+ "bugs": {
37
+ "url": "https://github.com/inference-sh/sdk-js-app/issues"
38
+ },
39
+ "engines": {
40
+ "node": ">=18.0.0"
41
+ },
42
+ "devDependencies": {
43
+ "@types/node": "^22.0.0",
44
+ "rimraf": "^6.0.1",
45
+ "typescript": "^5.8.3"
46
+ },
47
+ "files": [
48
+ "dist",
49
+ "README.md",
50
+ "LICENSE"
51
+ ]
52
+ }