Browse Source

feat(24-01): Fetch fal.ai pricing and merge into models

Add fetchFalPricing() to batch-fetch pricing from fal.ai API:
- GET /v1/models/pricing?endpoint_id={ids}
- Maps "image" unit -> per-run, others -> per-second
- Best-effort: errors logged but don't fail model fetching

Pricing is merged into ProviderModel after fetching fal.ai models.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
handoff-20260429-1057
shrimbly 6 months ago
parent
commit
03c7ecc905
  1. 89
      src/app/api/models/route.ts

89
src/app/api/models/route.ts

@ -124,6 +124,19 @@ interface FalModel {
openapi?: Record<string, unknown>;
}
interface FalPricingResponse {
prices: FalPrice[];
has_more: boolean;
next_cursor: string | null;
}
interface FalPrice {
endpoint_id: string;
unit_price: number;
unit: string; // "image", "video", "second", etc.
currency: string;
}
// ============ Response Types ============
interface ProviderResult {
@ -280,6 +293,68 @@ function mapFalModel(model: FalModel): ProviderModel {
};
}
/**
* Fetch pricing for a list of fal.ai endpoint IDs
* Returns a Map of endpoint_id -> pricing info
* Best-effort: errors are logged but don't fail the request
*/
async function fetchFalPricing(
endpointIds: string[],
apiKey: string | null
): Promise<Map<string, ProviderModel["pricing"]>> {
const pricingMap = new Map<string, ProviderModel["pricing"]>();
if (endpointIds.length === 0) {
return pricingMap;
}
const headers: HeadersInit = {};
if (apiKey) {
headers["Authorization"] = `Key ${apiKey}`;
}
try {
// Batch endpoint IDs (API supports up to ~50 at once based on URL length limits)
const batchSize = 50;
for (let i = 0; i < endpointIds.length; i += batchSize) {
const batch = endpointIds.slice(i, i + batchSize);
const endpointIdsParam = batch.join(",");
const url = `${FAL_API_BASE}/models/pricing?endpoint_id=${encodeURIComponent(endpointIdsParam)}`;
const response = await fetch(url, { headers });
if (!response.ok) {
console.warn(`[Models] fal.ai pricing API error: ${response.status}`);
continue;
}
const data: FalPricingResponse = await response.json();
for (const price of data.prices) {
// Map fal.ai units to our pricing type
// "image" -> per-run (single generation)
// "video", "second" -> per-second (duration-based)
const pricingType: "per-run" | "per-second" =
price.unit === "image" ? "per-run" : "per-second";
pricingMap.set(price.endpoint_id, {
type: pricingType,
amount: price.unit_price,
currency: price.currency,
});
}
}
} catch (error) {
// Best-effort: log warning but don't fail
console.warn(
`[Models] Failed to fetch fal.ai pricing:`,
error instanceof Error ? error.message : "Unknown error"
);
}
return pricingMap;
}
async function fetchFalModels(
apiKey: string | null,
searchQuery?: string
@ -320,6 +395,20 @@ async function fetchFalModels(
pageCount++;
}
// Fetch pricing for all models (best-effort)
if (allModels.length > 0) {
const endpointIds = allModels.map((m) => m.id);
const pricingMap = await fetchFalPricing(endpointIds, apiKey);
// Merge pricing into models
for (const model of allModels) {
const pricing = pricingMap.get(model.id);
if (pricing) {
model.pricing = pricing;
}
}
}
return allModels;
}

Loading…
Cancel
Save