diff --git a/.planning/phases/02-model-discovery/02-01-PLAN.md b/.planning/phases/02-model-discovery/02-01-PLAN.md new file mode 100644 index 00000000..2f2eb449 --- /dev/null +++ b/.planning/phases/02-model-discovery/02-01-PLAN.md @@ -0,0 +1,133 @@ +--- +phase: 02-model-discovery +plan: 01 +type: execute +--- + + +Implement Replicate provider with model discovery from their REST API. + +Purpose: Enable dynamic fetching of available models from Replicate's API, normalized to our ProviderModel interface. +Output: Working Replicate provider that registers in the provider system and an API route for client access. + + + +~/.claude/get-shit-done/workflows/execute-phase.md +~/.claude/get-shit-done/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/STATE.md +@.planning/phases/02-model-discovery/DISCOVERY.md + +# Prior phase context: +@.planning/phases/01-provider-infrastructure/01-02-SUMMARY.md + +# Key files: +@src/lib/providers/types.ts +@src/lib/providers/index.ts +@src/types/index.ts + +**Tech stack available:** +- Provider abstraction interfaces (ProviderInterface, ProviderModel) +- Provider registry with registerProvider() +- ProviderType includes "replicate" + +**Established patterns:** +- Provider types in src/lib/providers/types.ts +- Self-registration via registerProvider() +- API routes return { success, error, data } JSON + +**Constraining decisions:** +- No SDKs - use direct fetch calls +- API keys from localStorage via provider settings + + + + + + Task 1: Create Replicate provider implementation + src/lib/providers/replicate.ts + +Create Replicate provider implementing ProviderInterface: + +1. Import types from @/lib/providers and @/types +2. Implement helper function to get API key from localStorage (client) or return null (server) +3. Implement listModels(): + - Fetch GET https://api.replicate.com/v1/models with Bearer auth + - Map results to ProviderModel[] (id: `${owner}/${name}`, name, description, provider: "replicate", coverImage: cover_image_url) + - Infer capabilities from model name/description (text-to-image default, add image-to-image if "img2img" in name/description) + - Return first page only (no pagination traversal) +4. Implement searchModels(query): + - Fetch GET https://api.replicate.com/v1/search?query={query} with Bearer auth + - Map and return results same as listModels +5. Implement getModel(modelId): + - Parse modelId as owner/name + - Fetch GET https://api.replicate.com/v1/models/{owner}/{name} + - Map single result to ProviderModel +6. Implement generate() as stub returning { success: false, error: "Not implemented" } (Phase 3) +7. Implement isConfigured() checking if API key exists +8. Implement getApiKey() returning key from localStorage or null +9. Call registerProvider(replicateProvider) at module level + +Note: This runs client-side for isConfigured/getApiKey but API calls will be proxied through API route. + + TypeScript compiles without errors: npx tsc --noEmit + replicate.ts exports working provider, registered in registry when imported + + + + Task 2: Create Replicate models API route + src/app/api/providers/replicate/models/route.ts + +Create Next.js API route for fetching Replicate models server-side: + +1. Export GET handler +2. Get API key from request header or query param (client passes it) +3. Validate API key exists, return 401 if missing +4. Parse optional `search` query param +5. If search param: + - Fetch https://api.replicate.com/v1/search?query={search} +6. Else: + - Fetch https://api.replicate.com/v1/models +7. Both calls use Authorization: Bearer {apiKey} +8. Map response to ProviderModel[] using same logic as provider +9. Return { success: true, models: ProviderModel[] } +10. Handle errors: return { success: false, error: message } + +Pattern to follow from existing generate route: +- Use NextRequest, NextResponse +- Log request ID for debugging +- Handle fetch errors gracefully + + +curl -X GET "http://localhost:3000/api/providers/replicate/models" -H "X-API-Key: test" returns JSON (will error without real key, but route responds) + + API route returns model list when called with valid API key, 401 without key + + + + + +Before declaring plan complete: +- [ ] `npx tsc --noEmit` passes +- [ ] `npm run build` succeeds +- [ ] Replicate provider file exists and exports provider +- [ ] API route file exists at correct path +- [ ] Provider registers when module is imported + + + + +- Replicate provider implements ProviderInterface +- Provider self-registers via registerProvider() +- API route proxies model fetching with API key from header +- Models normalized to ProviderModel interface +- No TypeScript errors + + + +After completion, create `.planning/phases/02-model-discovery/02-01-SUMMARY.md` following the summary template. + diff --git a/.planning/phases/02-model-discovery/02-02-PLAN.md b/.planning/phases/02-model-discovery/02-02-PLAN.md new file mode 100644 index 00000000..2a17a56e --- /dev/null +++ b/.planning/phases/02-model-discovery/02-02-PLAN.md @@ -0,0 +1,140 @@ +--- +phase: 02-model-discovery +plan: 02 +type: execute +--- + + +Implement fal.ai provider with model discovery from their REST API. + +Purpose: Enable dynamic fetching of available models from fal.ai's API, normalized to our ProviderModel interface. +Output: Working fal.ai provider that registers in the provider system and an API route for client access. + + + +~/.claude/get-shit-done/workflows/execute-phase.md +~/.claude/get-shit-done/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/STATE.md +@.planning/phases/02-model-discovery/DISCOVERY.md + +# Prior plan context: +@.planning/phases/02-model-discovery/02-01-SUMMARY.md + +# Key files: +@src/lib/providers/types.ts +@src/lib/providers/index.ts +@src/lib/providers/replicate.ts +@src/types/index.ts + +**Tech stack available:** +- Provider abstraction interfaces (ProviderInterface, ProviderModel) +- Provider registry with registerProvider() +- ProviderType includes "fal" +- Replicate provider pattern established + +**Established patterns:** +- Provider implementation in src/lib/providers/{provider}.ts +- API route at src/app/api/providers/{provider}/models/route.ts +- Self-registration via registerProvider() + +**Constraining decisions:** +- No SDKs - use direct fetch calls +- API keys from localStorage via provider settings + + + + + + Task 1: Create fal.ai provider implementation + src/lib/providers/fal.ts + +Create fal.ai provider implementing ProviderInterface: + +1. Import types from @/lib/providers and @/types +2. Implement helper function to get API key from localStorage (client) or return null (server) +3. Implement listModels(): + - Fetch GET https://api.fal.ai/v1/models with optional Key auth header + - Filter to categories: text-to-image, image-to-image, text-to-video, image-to-video + - Map models to ProviderModel[]: + - id: endpoint_id + - name: metadata.display_name + - description: metadata.description + - provider: "fal" + - coverImage: metadata.thumbnail_url + - capabilities: [metadata.category as ModelCapability] + - Return first page only (no pagination traversal) +4. Implement searchModels(query): + - Fetch GET https://api.fal.ai/v1/models?q={query}&category=text-to-image (or combined categories) + - Map and return results same as listModels +5. Implement getModel(modelId): + - Fetch GET https://api.fal.ai/v1/models?endpoint_id={modelId} + - Return first result mapped to ProviderModel +6. Implement generate() as stub returning { success: false, error: "Not implemented" } (Phase 3) +7. Implement isConfigured() checking if API key exists (note: fal.ai works without key but with rate limits) +8. Implement getApiKey() returning key from localStorage or null +9. Call registerProvider(falProvider) at module level + +Note: fal.ai categories map directly to our ModelCapability type. + + TypeScript compiles without errors: npx tsc --noEmit + fal.ts exports working provider, registered in registry when imported + + + + Task 2: Create fal.ai models API route + src/app/api/providers/fal/models/route.ts + +Create Next.js API route for fetching fal.ai models server-side: + +1. Export GET handler +2. Get optional API key from request header (fal.ai works without but rate limited) +3. Parse optional `search` query param +4. Build fetch URL: + - Base: https://api.fal.ai/v1/models + - Add ?q={search} if search param provided + - Add category filter for image/video types only +5. Add Authorization header if API key provided: Key {apiKey} +6. Map response.models to ProviderModel[] using same logic as provider: + - Filter to relevant categories (text-to-image, image-to-image, text-to-video, image-to-video) + - Map endpoint_id, metadata.display_name, metadata.description, etc. +7. Return { success: true, models: ProviderModel[] } +8. Handle errors: return { success: false, error: message } + +Pattern to follow from Replicate API route (02-01). + + +curl -X GET "http://localhost:3000/api/providers/fal/models" returns JSON with models array (works without key due to fal.ai's optional auth) + + API route returns model list, works with or without API key, filters to image/video categories + + + + + +Before declaring plan complete: +- [ ] `npx tsc --noEmit` passes +- [ ] `npm run build` succeeds +- [ ] fal.ai provider file exists and exports provider +- [ ] API route file exists at correct path +- [ ] Provider registers when module is imported +- [ ] Models filtered to image/video capabilities only + + + + +- fal.ai provider implements ProviderInterface +- Provider self-registers via registerProvider() +- API route proxies model fetching (optional auth) +- Models normalized to ProviderModel interface +- Only image/video models returned (no audio, 3D, etc.) +- No TypeScript errors + + + +After completion, create `.planning/phases/02-model-discovery/02-02-SUMMARY.md` following the summary template. + diff --git a/.planning/phases/02-model-discovery/02-03-PLAN.md b/.planning/phases/02-model-discovery/02-03-PLAN.md new file mode 100644 index 00000000..edd9f123 --- /dev/null +++ b/.planning/phases/02-model-discovery/02-03-PLAN.md @@ -0,0 +1,168 @@ +--- +phase: 02-model-discovery +plan: 03 +type: execute +--- + + +Add in-memory caching for model lists and create unified models API endpoint. + +Purpose: Reduce API calls to external providers and provide single endpoint for fetching models from all configured providers. +Output: Caching layer with TTL and unified /api/models endpoint returning models from all providers. + + + +~/.claude/get-shit-done/workflows/execute-phase.md +~/.claude/get-shit-done/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/STATE.md +@.planning/phases/02-model-discovery/DISCOVERY.md + +# Prior plan context: +@.planning/phases/02-model-discovery/02-01-SUMMARY.md +@.planning/phases/02-model-discovery/02-02-SUMMARY.md + +# Key files: +@src/lib/providers/types.ts +@src/lib/providers/index.ts +@src/lib/providers/replicate.ts +@src/lib/providers/fal.ts +@src/types/index.ts + +**Tech stack available:** +- Provider abstraction interfaces +- Replicate and fal.ai providers implemented +- API routes for each provider + +**Established patterns:** +- Provider implementations in src/lib/providers/ +- API routes return { success, models/error } + +**Constraining decisions:** +- In-memory caching (no Redis) +- 5-15 minute cache TTL + + + + + + Task 1: Create model caching utility + src/lib/providers/cache.ts + +Create simple in-memory cache for model lists: + +1. Define CacheEntry interface: { data: T, timestamp: number } +2. Create cache Map: Map<string, CacheEntry<ProviderModel[]>> +3. Define DEFAULT_TTL = 10 * 60 * 1000 (10 minutes) +4. Export getCachedModels(key: string): ProviderModel[] | null + - Return null if not in cache or expired + - Return cached data if within TTL +5. Export setCachedModels(key: string, models: ProviderModel[]): void + - Store with current timestamp +6. Export invalidateCache(key?: string): void + - If key provided, delete that entry + - If no key, clear entire cache +7. Export getCacheKey(provider: ProviderType, search?: string): string + - Return `${provider}:models` or `${provider}:search:${search}` + +Keep it simple - no LRU eviction, no persistence. Server restarts clear cache. + + TypeScript compiles without errors: npx tsc --noEmit + cache.ts exports get/set/invalidate functions for model caching + + + + Task 2: Create unified models API endpoint + src/app/api/models/route.ts + +Create unified API route that fetches models from all configured providers: + +1. Export GET handler +2. Parse query params: + - provider: optional, filter to specific provider + - search: optional, search query + - refresh: optional, bypass cache if "true" +3. Get API keys from request headers: + - X-Replicate-Key: Replicate API key + - X-Fal-Key: fal.ai API key +4. Build list of providers to fetch from: + - If provider param, use only that provider (if key provided) + - Otherwise, fetch from all providers with keys +5. For each provider: + - Check cache first (unless refresh=true) + - If cache miss, fetch from provider's API route internally or call provider directly + - Store result in cache +6. Combine all provider results into single array +7. Sort by provider, then by name +8. Return { success: true, models: ProviderModel[], cached: boolean } +9. Handle partial failures: return models from successful providers, include errors array + +Response format: +{ + success: true, + models: ProviderModel[], + cached: boolean, + providers: { + replicate: { success: true, count: 50 }, + fal: { success: true, count: 100 } + }, + errors?: string[] +} + + +curl -X GET "http://localhost:3000/api/models" -H "X-Fal-Key: test" returns combined model list (fal.ai works without key) + + Unified endpoint returns models from all configured providers, uses caching, handles partial failures + + + + Task 3: Add provider module exports + src/lib/providers/index.ts + +Update provider index to export cache utilities and ensure providers auto-register: + +1. Add import for cache utilities: export * from "./cache" +2. Add dynamic imports comment explaining that providers self-register +3. Export helper function listAllModels(apiKeys: Record<string, string>): Promise<ProviderModel[]> + - Takes API keys object + - Calls listModels() on each configured provider + - Returns combined array +4. Export helper function searchAllModels(query: string, apiKeys: Record<string, string>): Promise<ProviderModel[]> + - Same pattern but calls searchModels() + +Note: These helpers can be used by the unified API route. + + TypeScript compiles without errors: npx tsc --noEmit + Provider index exports cache utilities and helper functions for multi-provider operations + + + + + +Before declaring plan complete: +- [ ] `npx tsc --noEmit` passes +- [ ] `npm run build` succeeds +- [ ] Cache utility exists with get/set/invalidate functions +- [ ] Unified /api/models endpoint works +- [ ] Caching prevents redundant API calls +- [ ] Multiple providers can be queried in single request + + + + +- In-memory cache with 10-minute TTL +- Unified /api/models endpoint aggregates all providers +- Partial failure handling (some providers fail, others succeed) +- Cache bypass option for manual refresh +- Phase 2 complete - model discovery working for both providers + + + +After completion, create `.planning/phases/02-model-discovery/02-03-SUMMARY.md` following the summary template. + +Mark Phase 2 complete in summary. + diff --git a/.planning/phases/02-model-discovery/DISCOVERY.md b/.planning/phases/02-model-discovery/DISCOVERY.md new file mode 100644 index 00000000..f0f0443f --- /dev/null +++ b/.planning/phases/02-model-discovery/DISCOVERY.md @@ -0,0 +1,197 @@ +# Phase 2 Discovery: Model Discovery APIs + +**Date:** 2026-01-09 +**Level:** 2 - Standard Research + +## Research Objectives + +1. Replicate model listing API endpoints and response schema +2. fal.ai model discovery endpoints and response schema +3. Pagination approaches for both providers +4. Authentication header formats + +## Findings + +### Replicate API + +**Endpoint:** `GET https://api.replicate.com/v1/models` + +**Authentication:** +``` +Authorization: Bearer $REPLICATE_API_TOKEN +``` + +**Query Parameters:** +- `sort_by`: Sort field (default: `latest_version_created_at`) +- `sort_direction`: `asc` or `desc` + +**Response Schema:** +```typescript +interface ReplicateModelsResponse { + next: string | null; // URL to next page + previous: string | null; // URL to previous page + results: ReplicateModel[]; +} + +interface ReplicateModel { + url: string; // Web page link + owner: string; // Username/organization + name: string; // Model identifier + description: string; // Text summary + visibility: "public" | "private"; + github_url?: string; + paper_url?: string; + license_url?: string; + run_count: number; // Usage statistics + cover_image_url?: string; // Thumbnail + default_example?: object; // Sample prediction + latest_version?: { + id: string; + openapi_schema: object; // Input/output specs + }; +} +``` + +**Pagination:** Cursor-based via `next`/`previous` URLs. Follow `next` URL until null. + +**Search Endpoint (Beta):** +``` +GET https://api.replicate.com/v1/search?query=... +``` +Returns broader search results across models. + +### fal.ai API + +**Endpoint:** `GET https://api.fal.ai/v1/models` + +**Authentication (Optional - higher rate limits):** +``` +Authorization: Key $FAL_KEY +``` + +**Query Parameters:** +| Parameter | Type | Description | +|-----------|------|-------------| +| `limit` | integer | Max items to return | +| `cursor` | string | Pagination cursor from previous response | +| `endpoint_id` | string/array | Specific endpoint ID(s) to retrieve | +| `q` | string | Free-text search query | +| `category` | string | Filter by category (e.g., 'text-to-image') | +| `status` | enum | `active` or `deprecated` | +| `expand` | string | Include `openapi-3.0` for full schema | + +**Response Schema:** +```typescript +interface FalModelsResponse { + models: FalModel[]; + next_cursor: string | null; + has_more: boolean; +} + +interface FalModel { + endpoint_id: string; // e.g., "fal-ai/flux/dev" + metadata: { + display_name: string; + category: string; // e.g., "text-to-image" + description: string; + status: "active" | "deprecated"; + tags: string[]; + updated_at: string; // ISO8601 + is_favorited: boolean | null; + thumbnail_url: string; + model_url: string; + date: string; // ISO8601 + highlighted: boolean; + pinned: boolean; + thumbnail_animated_url?: string; + github_url?: string; + license_type?: "commercial" | "research" | "private"; + }; + openapi?: object; // When expand=openapi-3.0 +} +``` + +**Pagination:** Cursor-based. Use `next_cursor` value as `cursor` param. Stop when `has_more` is false. + +**Categories of Interest:** +- `text-to-image` +- `image-to-image` +- `text-to-video` +- `image-to-video` + +## Mapping to ProviderModel + +Both APIs need to be normalized to our `ProviderModel` interface: + +```typescript +interface ProviderModel { + id: string; // Replicate: `${owner}/${name}`, fal: endpoint_id + name: string; // Replicate: name, fal: display_name + description: string | null; // Replicate: description, fal: metadata.description + provider: ProviderType; // "replicate" | "fal" + capabilities: ModelCapability[];// Derived from run type or fal category + coverImage?: string; // Replicate: cover_image_url, fal: thumbnail_url + pricing?: { type, amount, currency }; +} +``` + +**Capability Mapping:** +- fal.ai: Direct from `category` field +- Replicate: Infer from model name/description or openapi_schema input/output types + +## Implementation Approach + +### Provider Module Pattern + +Each provider implements `ProviderInterface` and self-registers: + +```typescript +// src/lib/providers/replicate.ts +const replicateProvider: ProviderInterface = { + id: "replicate", + name: "Replicate", + listModels: async () => { /* fetch and map */ }, + searchModels: async (query) => { /* use search endpoint */ }, + getModel: async (id) => { /* fetch specific model */ }, + generate: async (input) => { /* create prediction */ }, + isConfigured: () => !!getApiKey(), + getApiKey: () => /* from store or env */, +}; + +registerProvider(replicateProvider); +``` + +### API Route Pattern + +Expose via Next.js API routes for client access: + +```typescript +// src/app/api/providers/[provider]/models/route.ts +// GET /api/providers/replicate/models?search=flux +``` + +### Caching Strategy + +Cache model lists in memory with TTL: +- **Duration:** 5-15 minutes (models don't change frequently) +- **Key:** `${provider}:models` or `${provider}:search:${query}` +- **Invalidation:** Manual refresh button in UI + +## Decisions + +1. **No SDK usage** - Direct fetch calls to REST APIs as per project constraints +2. **Server-side fetching** - API routes proxy requests (hides API keys from client) +3. **Initial page only** - First API call fetches ~50-100 models, pagination on-demand for search +4. **Category filtering** - Filter to image/video capabilities only (no audio, 3D, etc.) + +## Next Steps + +1. Create Replicate provider implementation with model fetching +2. Create fal.ai provider implementation with model fetching +3. Add API routes for client access to model lists +4. Implement simple in-memory caching with TTL + +## Sources + +- [Replicate HTTP API Reference](https://replicate.com/docs/reference/http) +- [fal.ai Model Search API](https://docs.fal.ai/platform-apis/v1/models)