Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 129 additions & 0 deletions apps/sim/app/api/replicate/collections/[slug]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { getPersonalAndWorkspaceEnv } from '@/lib/environment/utils'
import { decryptSecret } from '@/lib/utils'
import { fetchWithRetry, getUserFriendlyError, parseErrorMessage } from '@/lib/api/retry'

export const dynamic = 'force-dynamic'

/**
* GET /api/replicate/collections/[slug]
* Gets models in a specific Replicate collection
*
* Path params:
* - slug: Collection slug (e.g., "text-to-image")
*
* Query params:
* - workspaceId: Optional workspace ID for environment variable resolution
*
* Returns collection details and array of models
*/
export async function GET(
request: NextRequest,
{ params }: { params: { slug: string } }
) {
const { slug } = params
const workspaceId = request.nextUrl.searchParams.get('workspaceId')
const rawApiKey = request.headers.get('x-replicate-api-key')

if (!rawApiKey) {
return NextResponse.json(
{ error: 'API key required in x-replicate-api-key header' },
{ status: 401 }
)
}

let apiKey = rawApiKey

// Resolve environment variable if needed ({{VAR}} syntax)
if (rawApiKey.match(/^\{\{[^}]+\}\}$/)) {
const session = await getSession()
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}

const varName = rawApiKey.slice(2, -2).trim()

try {
const { personalEncrypted, workspaceEncrypted } = await getPersonalAndWorkspaceEnv(
session.user.id,
workspaceId || undefined
)

const variables = { ...personalEncrypted, ...workspaceEncrypted }
const encryptedValue = variables[varName]

if (!encryptedValue) {
return NextResponse.json(
{
error: `Environment variable "${varName}" not found. Please add it in Settings → Environment.`,
},
{ status: 400 }
)
}

const { decrypted } = await decryptSecret(encryptedValue)
apiKey = decrypted
} catch (error: any) {
return NextResponse.json(
{ error: `Failed to resolve environment variable "${varName}": ${error.message}` },
{ status: 500 }
)
}
}

try {
const response = await fetchWithRetry(
`https://api.replicate.com/v1/collections/${slug}`,
{
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
},
{
maxAttempts: 2,
baseDelay: 500,
}
)

if (!response.ok) {
const errorMessage = await parseErrorMessage(response)
const userFriendlyError = getUserFriendlyError(response.status, errorMessage, 'Replicate')

return NextResponse.json({ error: userFriendlyError }, { status: response.status })
}

const data = await response.json()

// Transform and deduplicate models using Map
// Collections may contain multiple versions of the same model
const modelsMap = new Map<string, { value: string; label: string; description: string }>()

for (const model of data.models || []) {
const modelKey = `${model.owner}/${model.name}`

// Only add if not already present (keeps first occurrence)
if (!modelsMap.has(modelKey)) {
modelsMap.set(modelKey, {
value: modelKey,
label: modelKey,
description: model.description || '',
})
}
}

// Convert Map values back to array
const models = Array.from(modelsMap.values())

return NextResponse.json({
name: data.name,
slug: data.slug,
description: data.description,
models,
})
} catch (error: any) {
console.error('Collection fetch error:', error)
return NextResponse.json({ error: error.message || 'Internal server error' }, { status: 500 })
}
}
104 changes: 104 additions & 0 deletions apps/sim/app/api/replicate/collections/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { getPersonalAndWorkspaceEnv } from '@/lib/environment/utils'
import { decryptSecret } from '@/lib/utils'
import { fetchWithRetry, getUserFriendlyError, parseErrorMessage } from '@/lib/api/retry'

export const dynamic = 'force-dynamic'

/**
* GET /api/replicate/collections
* Lists all available Replicate collections
*
* Query params:
* - workspaceId: Optional workspace ID for environment variable resolution
*
* Returns array of collections with name, slug, and description
*/
export async function GET(request: NextRequest) {
const workspaceId = request.nextUrl.searchParams.get('workspaceId')
const rawApiKey = request.headers.get('x-replicate-api-key')

if (!rawApiKey) {
return NextResponse.json(
{ error: 'API key required in x-replicate-api-key header' },
{ status: 401 }
)
}

let apiKey = rawApiKey

// Resolve environment variable if needed ({{VAR}} syntax)
if (rawApiKey.match(/^\{\{[^}]+\}\}$/)) {
const session = await getSession()
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}

const varName = rawApiKey.slice(2, -2).trim()

try {
const { personalEncrypted, workspaceEncrypted } = await getPersonalAndWorkspaceEnv(
session.user.id,
workspaceId || undefined
)

const variables = { ...personalEncrypted, ...workspaceEncrypted }
const encryptedValue = variables[varName]

if (!encryptedValue) {
return NextResponse.json(
{
error: `Environment variable "${varName}" not found. Please add it in Settings → Environment.`,
},
{ status: 400 }
)
}

const { decrypted } = await decryptSecret(encryptedValue)
apiKey = decrypted
} catch (error: any) {
return NextResponse.json(
{ error: `Failed to resolve environment variable "${varName}": ${error.message}` },
{ status: 500 }
)
}
}

try {
const response = await fetchWithRetry(
'https://api.replicate.com/v1/collections',
{
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
},
{
maxAttempts: 2,
baseDelay: 500,
}
)

if (!response.ok) {
const errorMessage = await parseErrorMessage(response)
const userFriendlyError = getUserFriendlyError(response.status, errorMessage, 'Replicate')

return NextResponse.json({ error: userFriendlyError }, { status: response.status })
}

const data = await response.json()

// Transform to dropdown options format
const collections = (data.results || []).map((collection: any) => ({
value: collection.slug,
label: collection.name,
description: collection.description || '',
}))

return NextResponse.json({ collections })
} catch (error: any) {
console.error('Collections fetch error:', error)
return NextResponse.json({ error: error.message || 'Internal server error' }, { status: 500 })
}
}
141 changes: 141 additions & 0 deletions apps/sim/app/api/replicate/models/[owner]/[name]/schema/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import { NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { getPersonalAndWorkspaceEnv } from '@/lib/environment/utils'
import { dereferenceSchema } from '@/lib/response-format'
import { decryptSecret } from '@/lib/utils'
import { fetchWithRetry, getUserFriendlyError, parseErrorMessage } from '@/lib/api/retry'

export const dynamic = 'force-dynamic'

/**
* GET /api/replicate/models/[owner]/[name]/schema
* Fetches and dereferences the input/output schema for a Replicate model
* Query params:
* - version: Optional specific version ID (defaults to latest)
* - workspaceId: Optional workspace ID for environment variable resolution
*/
export async function GET(
request: NextRequest,
{ params }: { params: { owner: string; name: string } }
) {
const { owner, name } = params
const rawApiKey = request.headers.get('x-replicate-api-key')
const versionId = request.nextUrl.searchParams.get('version')
const workspaceId = request.nextUrl.searchParams.get('workspaceId')

if (!rawApiKey) {
return NextResponse.json({ error: 'API key required in x-replicate-api-key header' }, { status: 401 })
}

let apiKey = rawApiKey

// Resolve environment variable if needed ({{VAR}} syntax)
if (rawApiKey.match(/^\{\{[^}]+\}\}$/)) {
const session = await getSession()
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}

const varName = rawApiKey.slice(2, -2).trim()

try {
// Load encrypted environment variables (personal + workspace)
const { personalEncrypted, workspaceEncrypted } = await getPersonalAndWorkspaceEnv(
session.user.id,
workspaceId || undefined
)

// Merge variables (workspace overrides personal)
const variables = { ...personalEncrypted, ...workspaceEncrypted }

const encryptedValue = variables[varName]
if (!encryptedValue) {
return NextResponse.json(
{
error: `Environment variable "${varName}" not found. Please add it in Settings → Environment.`,
},
{ status: 400 }
)
}

// Decrypt the variable value
const { decrypted } = await decryptSecret(encryptedValue)
apiKey = decrypted
} catch (error: any) {
console.error('Environment variable resolution error:', error)
return NextResponse.json(
{
error: `Failed to resolve environment variable "${varName}": ${error.message}`,
},
{ status: 500 }
)
}
}

try {
// Construct URL based on whether version is specified
let url = `https://api.replicate.com/v1/models/${owner}/${name}`
if (versionId) {
url += `/versions/${versionId}`
}

const response = await fetchWithRetry(
url,
{
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
// Use Next.js caching instead of custom cache
next: { revalidate: 3600 }, // 1 hour cache
},
{
maxAttempts: 3,
baseDelay: 1000,
retryableStatusCodes: [429, 500, 502, 503, 504],
}
)

if (!response.ok) {
const errorMessage = await parseErrorMessage(response)
const userFriendlyError = getUserFriendlyError(response.status, errorMessage, 'Replicate')

return NextResponse.json({ error: userFriendlyError }, { status: response.status })
}

const data = await response.json()

// Get version data (either from specific version or latest)
const versionData = versionId ? data : data.latest_version

if (!versionData) {
return NextResponse.json({ error: 'No version data available' }, { status: 404 })
}

const fullSchema = versionData.openapi_schema

if (!fullSchema?.components?.schemas) {
return NextResponse.json({ error: 'No OpenAPI schema available for this model' }, { status: 404 })
}

// Dereference the input and output schemas
const inputSchema = fullSchema.components.schemas.Input
? dereferenceSchema(fullSchema.components.schemas.Input, fullSchema)
: null

const outputSchema = fullSchema.components.schemas.Output
? dereferenceSchema(fullSchema.components.schemas.Output, fullSchema)
: null

const schemaResponse = {
version_id: versionData.id,
input: inputSchema,
output: outputSchema,
}

return NextResponse.json(schemaResponse)
} catch (error: any) {
console.error('Schema fetch error:', error)
return NextResponse.json({ error: error.message || 'Internal server error' }, { status: 500 })
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export { LongInput } from './long-input'
export { McpDynamicArgs } from './mcp-dynamic-args/mcp-dynamic-args'
export { McpServerSelector } from './mcp-server-modal/mcp-server-selector'
export { McpToolSelector } from './mcp-server-modal/mcp-tool-selector'
export { OpenApiDynamicInputs } from './openapi-dynamic-inputs/openapi-dynamic-inputs'
export { ProjectSelectorInput } from './project-selector/project-selector-input'
export { ResponseFormat } from './response/response-format'
export { ScheduleConfig } from './schedule/schedule-config'
Expand Down
Loading