# Adding Providers This guide walks through integrating a new upstream API provider into ToolRouter. A provider is any external API that executes tool skills — image generation, search, transcription, etc. ## Overview Adding a provider touches 6 files: | File | Purpose | |------|---------| | `src/tools/shared/-client.ts` | API client (HTTP calls, response parsing, file downloads) | | `src/core/provider-catalog.ts` | Display metadata for `/v1/providers` endpoints | | `src/tools//models.ts` | Model registry entries (if the provider serves models) | | `src/tools//index.ts` | Tool requirement + changelog | | `src/tools//skills/.ts` | Dispatch path in the handler | | `.env.local` | Local API key for development | ## Step 1: Create the API client Create `src/tools/shared/-client.ts`. Every client follows the same pattern: ```typescript import { writeFile } from 'fs/promises'; import { join } from 'path'; import { tmpdir } from 'os'; import { randomUUID } from 'crypto'; import type { SkillContext } from '../../core/types.js'; import { CircuitBreaker } from '../../core/circuit-breaker.js'; const BASE_URL = 'https://api.example.com/v1'; const REQUEST_TIMEOUT_MS = 120_000; export const exampleBreaker = new CircuitBreaker({ name: 'example', threshold: 3, resetMs: 60_000, }); ``` ### API patterns Providers fall into three patterns: | Pattern | Description | Examples | |---------|-------------|----------| | **Synchronous** | POST → immediate result | Photalabs, Prodia | | **Async queue** | Submit → poll status → fetch result | fal.ai, Higgsfield | | **Streaming** | POST → stream chunks | (not yet used) | ### Required conventions - **Circuit breaker**: Wrap all calls in `providerBreaker.call(async () => { ... })` — trips after 3 consecutive failures, resets after 60s - **Timeout**: Use `AbortSignal.timeout(REQUEST_TIMEOUT_MS)` on all fetch calls - **Download to /tmp**: Provider URLs are ephemeral — always download media to local `/tmp` and return the path. The asset system picks up `*_path` keys and uploads to permanent storage - **File naming**: Use `__.` pattern (e.g. `phota_gen_a1b2c3d4.png`) - **Error handling**: Throw on failure — never swallow errors. Include the HTTP status and response body in error messages - **Logging**: Use `context.log()` for operational logging (goes to stderr in stdio mode) ### Auth patterns | Provider | Header format | Env var | |----------|--------------|---------| | fal.ai | `Authorization: Key ${key}` | `FAL_KEY` | | Prodia | `Authorization: Bearer ${token}` | `PRODIA_TOKEN` | | Higgsfield | `Authorization: Key ${keyId}:${keySecret}` | `HIGGSFIELD_API_KEY` | | Photalabs | `X-API-Key: ${key}` | `PHOTA_API_KEY` | ### Return types Export a result interface that includes: ```typescript export interface ExampleResult { image_path: string; // Local /tmp path (required for asset system) job_id: string; // Provider request ID or synthetic UUID content_type: string; // MIME type (e.g. 'image/png') } ``` For providers returning multiple images, use `image_paths: string[]` instead. ## Step 2: Register in provider catalog Add display metadata to `src/core/provider-catalog.ts`: ```typescript export const PROVIDER_DISPLAY: Record = { // ... existing providers example: { displayName: 'Example', description: 'What this provider does — keep it concise.', website: 'https://example.com', }, }; ``` This makes the provider appear on `/v1/providers` endpoints and the website's provider pages. ## Step 3: Add model entries (if applicable) If the provider serves models (image gen, video gen, etc.), add entries to the relevant `models.ts`: 1. Add the provider to the `ModelProvider` union type: ```typescript export type ModelProvider = 'fal' | 'prodia' | 'higgsfield' | 'example'; ``` 2. Add model entries to `MODEL_REGISTRY`: ```typescript 'example-model': { key: 'example-model', displayName: 'Example Model', falId: 'endpoint-id', // Provider-specific endpoint identifier provider: 'example', // Routes to your dispatch path capabilities: ['text-to-image'], pricing: { amount: 0.05, unit: 'per-image' }, maxImages: 4, description: 'What this model is good at.', }, ``` The `provider` field is what routes the model to your dispatch path in the handler. The `falId` field stores the provider-specific model/endpoint identifier. ## Step 4: Add tool requirement In the tool's `index.ts`, add a requirement entry so the credential system knows about the provider: ```typescript requirements: [ // ... existing requirements { name: 'example', type: 'secret', displayName: 'Example API Key', description: 'Optional: use your own Example key instead of the platform default', acquireUrl: 'https://example.com/api-keys', envFallback: 'EXAMPLE_API_KEY', }, ], ``` Also add a changelog entry: ```typescript changelog: [ // ... existing entries { date: '2026-03-27', changes: ['Added Example as a provider — X new models'] }, ], ``` ## Step 5: Add dispatch path in handler In the skill handler (e.g. `text-to-image.ts`), add a provider dispatch block. Place it before the default fal.ai path: ```typescript import { exampleGenerate } from '../../shared/example-client.js'; // Inside the handler, after model resolution: // --------------------------------------------------------------------------- // Example provider path // --------------------------------------------------------------------------- if (model?.provider === 'example') { const unavailable = requireKeyOrUnavailable(context, 'example', 'EXAMPLE_API_KEY'); if (unavailable) return unavailable; const apiKey = resolveKey(context, 'example', 'EXAMPLE_API_KEY')!; // Build provider-specific params from input // Call provider client // Calculate cost // Return { output, usage: { raw_cost } } } ``` ### Dispatch order The handler checks providers in order: 1. Prodia (explicit `provider === 'prodia'` or `inference.*` prefix) 2. Higgsfield (explicit `provider === 'higgsfield'`) 3. Photalabs (explicit `provider === 'phota'`) 4. fal.ai (default fallback) Add your provider before the fal.ai default path. ### Output structure All providers must return the same output shape: ```typescript const output: Record = { format: 'gallery', format_data: { images: [{ url, title }] }, images: [{ url, width, height, content_type }], model: modelName, model_key: model.key, seed: null, request_id: result.job_id, num_images: count, image_path: result.image_paths[0], // First image for asset system }; // Multi-image: additional paths for (let i = 1; i < result.image_paths.length; i++) { output[`image_${i + 1}_path`] = result.image_paths[i]; } return { output, usage: { raw_cost: calculatedCost } }; ``` ## Step 6: Set up credentials ### Local development Add the API key to `.env.local`: ``` EXAMPLE_API_KEY=your-key-here ``` ### Production (Railway) ```bash railway variables --set "EXAMPLE_API_KEY=your-key-here" --service ToolRouter railway variables --set "EXAMPLE_API_KEY=your-key-here" --service Worker ``` ## Pricing strategies How you calculate `raw_cost` depends on the provider: | Strategy | When to use | Example | |----------|-------------|---------| | **Live API pricing** | Provider returns cost in response | Prodia (`result.price.dollars`) | | **Pre-execution pricing** | Provider has a pricing API | fal.ai (fetch `/v1/models/pricing` before billing) | | **Fixed pricing** | Provider has static documented rates | Higgsfield ($0.09/720p), Photalabs ($0.09/1K) | **Rule**: Never silently fall back to $0 if pricing is unavailable. Either throw an error or use a static rate from the provider's documentation. The handler must always return `usage: { raw_cost }`. ## Checklist - [ ] Client file in `src/tools/shared/` with circuit breaker, timeouts, and `/tmp` downloads - [ ] Provider entry in `src/core/provider-catalog.ts` - [ ] `ModelProvider` type updated in `models.ts` - [ ] Model entries added to `MODEL_REGISTRY` - [ ] Tool requirement added in `index.ts` - [ ] Changelog entry added in `index.ts` - [ ] Dispatch path added in handler(s) before the fal.ai default - [ ] API key added to `.env.local` and production env vars - [ ] `npm run build` passes - [ ] `providers.md` doc updated with new provider details