mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-28 03:00:29 -04:00
feat(copilot): enterprise configuration (#3184)
* Copilot enterprise models * Fix azure anthropic * Fix * Consolidation * Cleanup * Clean up code * Fix lint * cleanup * Fix greptile
This commit is contained in:
committed by
GitHub
parent
20b230d1aa
commit
c5dd90e79d
@@ -8,9 +8,8 @@ import { getSession } from '@/lib/auth'
|
||||
import { buildConversationHistory } from '@/lib/copilot/chat-context'
|
||||
import { resolveOrCreateChat } from '@/lib/copilot/chat-lifecycle'
|
||||
import { buildCopilotRequestPayload } from '@/lib/copilot/chat-payload'
|
||||
import { generateChatTitle } from '@/lib/copilot/chat-title'
|
||||
import { getCopilotModel } from '@/lib/copilot/config'
|
||||
import { COPILOT_MODEL_IDS, COPILOT_REQUEST_MODES } from '@/lib/copilot/models'
|
||||
import { SIM_AGENT_API_URL } from '@/lib/copilot/constants'
|
||||
import { COPILOT_REQUEST_MODES } from '@/lib/copilot/models'
|
||||
import { orchestrateCopilotStream } from '@/lib/copilot/orchestrator'
|
||||
import {
|
||||
createStreamEventWriter,
|
||||
@@ -29,6 +28,49 @@ import { resolveWorkflowIdForUser } from '@/lib/workflows/utils'
|
||||
|
||||
const logger = createLogger('CopilotChatAPI')
|
||||
|
||||
async function requestChatTitleFromCopilot(params: {
|
||||
message: string
|
||||
model: string
|
||||
provider?: string
|
||||
}): Promise<string | null> {
|
||||
const { message, model, provider } = params
|
||||
if (!message || !model) return null
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
if (env.COPILOT_API_KEY) {
|
||||
headers['x-api-key'] = env.COPILOT_API_KEY
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${SIM_AGENT_API_URL}/api/generate-chat-title`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
message,
|
||||
model,
|
||||
...(provider ? { provider } : {}),
|
||||
}),
|
||||
})
|
||||
|
||||
const payload = await response.json().catch(() => ({}))
|
||||
if (!response.ok) {
|
||||
logger.warn('Failed to generate chat title via copilot backend', {
|
||||
status: response.status,
|
||||
error: payload,
|
||||
})
|
||||
return null
|
||||
}
|
||||
|
||||
const title = typeof payload?.title === 'string' ? payload.title.trim() : ''
|
||||
return title || null
|
||||
} catch (error) {
|
||||
logger.error('Error generating chat title:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const FileAttachmentSchema = z.object({
|
||||
id: z.string(),
|
||||
key: z.string(),
|
||||
@@ -43,14 +85,14 @@ const ChatMessageSchema = z.object({
|
||||
chatId: z.string().optional(),
|
||||
workflowId: z.string().optional(),
|
||||
workflowName: z.string().optional(),
|
||||
model: z.enum(COPILOT_MODEL_IDS).optional().default('claude-4.6-opus'),
|
||||
model: z.string().optional().default('claude-opus-4-6'),
|
||||
mode: z.enum(COPILOT_REQUEST_MODES).optional().default('agent'),
|
||||
prefetch: z.boolean().optional(),
|
||||
createNewChat: z.boolean().optional().default(false),
|
||||
stream: z.boolean().optional().default(true),
|
||||
implicitFeedback: z.string().optional(),
|
||||
fileAttachments: z.array(FileAttachmentSchema).optional(),
|
||||
provider: z.string().optional().default('openai'),
|
||||
provider: z.string().optional(),
|
||||
conversationId: z.string().optional(),
|
||||
contexts: z
|
||||
.array(
|
||||
@@ -173,14 +215,14 @@ export async function POST(req: NextRequest) {
|
||||
let currentChat: any = null
|
||||
let conversationHistory: any[] = []
|
||||
let actualChatId = chatId
|
||||
const selectedModel = model || 'claude-opus-4-6'
|
||||
|
||||
if (chatId || createNewChat) {
|
||||
const defaultsForChatRow = getCopilotModel('chat')
|
||||
const chatResult = await resolveOrCreateChat({
|
||||
chatId,
|
||||
userId: authenticatedUserId,
|
||||
workflowId,
|
||||
model: defaultsForChatRow.model,
|
||||
model: selectedModel,
|
||||
})
|
||||
currentChat = chatResult.chat
|
||||
actualChatId = chatResult.chatId || chatId
|
||||
@@ -191,8 +233,6 @@ export async function POST(req: NextRequest) {
|
||||
conversationHistory = history.history
|
||||
}
|
||||
|
||||
const defaults = getCopilotModel('chat')
|
||||
const selectedModel = model || defaults.model
|
||||
const effectiveMode = mode === 'agent' ? 'build' : mode
|
||||
const effectiveConversationId =
|
||||
(currentChat?.conversationId as string | undefined) || conversationId
|
||||
@@ -205,6 +245,7 @@ export async function POST(req: NextRequest) {
|
||||
userMessageId: userMessageIdToUse,
|
||||
mode,
|
||||
model: selectedModel,
|
||||
provider,
|
||||
conversationHistory,
|
||||
contexts: agentContexts,
|
||||
fileAttachments,
|
||||
@@ -283,7 +324,7 @@ export async function POST(req: NextRequest) {
|
||||
}
|
||||
|
||||
if (actualChatId && !currentChat?.title && conversationHistory.length === 0) {
|
||||
generateChatTitle(message)
|
||||
requestChatTitleFromCopilot({ message, model: selectedModel, provider })
|
||||
.then(async (title) => {
|
||||
if (title) {
|
||||
await db
|
||||
@@ -372,10 +413,7 @@ export async function POST(req: NextRequest) {
|
||||
content: nonStreamingResult.content,
|
||||
toolCalls: nonStreamingResult.toolCalls,
|
||||
model: selectedModel,
|
||||
provider:
|
||||
(requestPayload?.provider as Record<string, unknown>)?.provider ||
|
||||
env.COPILOT_PROVIDER ||
|
||||
'openai',
|
||||
provider: typeof requestPayload?.provider === 'string' ? requestPayload.provider : undefined,
|
||||
}
|
||||
|
||||
logger.info(`[${tracker.requestId}] Non-streaming response from orchestrator:`, {
|
||||
@@ -413,7 +451,7 @@ export async function POST(req: NextRequest) {
|
||||
// Start title generation in parallel if this is first message (non-streaming)
|
||||
if (actualChatId && !currentChat.title && conversationHistory.length === 0) {
|
||||
logger.info(`[${tracker.requestId}] Starting title generation for non-streaming response`)
|
||||
generateChatTitle(message)
|
||||
requestChatTitleFromCopilot({ message, model: selectedModel, provider })
|
||||
.then(async (title) => {
|
||||
if (title) {
|
||||
await db
|
||||
|
||||
84
apps/sim/app/api/copilot/models/route.ts
Normal file
84
apps/sim/app/api/copilot/models/route.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { SIM_AGENT_API_URL } from '@/lib/copilot/constants'
|
||||
import { authenticateCopilotRequestSessionOnly } from '@/lib/copilot/request-helpers'
|
||||
import type { AvailableModel } from '@/lib/copilot/types'
|
||||
import { env } from '@/lib/core/config/env'
|
||||
|
||||
const logger = createLogger('CopilotModelsAPI')
|
||||
|
||||
interface RawAvailableModel {
|
||||
id: string
|
||||
friendlyName?: string
|
||||
displayName?: string
|
||||
provider?: string
|
||||
}
|
||||
|
||||
function isRawAvailableModel(item: unknown): item is RawAvailableModel {
|
||||
return (
|
||||
typeof item === 'object' &&
|
||||
item !== null &&
|
||||
'id' in item &&
|
||||
typeof (item as { id: unknown }).id === 'string'
|
||||
)
|
||||
}
|
||||
|
||||
export async function GET(_req: NextRequest) {
|
||||
const { userId, isAuthenticated } = await authenticateCopilotRequestSessionOnly()
|
||||
if (!isAuthenticated || !userId) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
if (env.COPILOT_API_KEY) {
|
||||
headers['x-api-key'] = env.COPILOT_API_KEY
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${SIM_AGENT_API_URL}/api/get-available-models`, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
})
|
||||
|
||||
const payload = await response.json().catch(() => ({}))
|
||||
if (!response.ok) {
|
||||
logger.warn('Failed to fetch available models from copilot backend', {
|
||||
status: response.status,
|
||||
})
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: payload?.error || 'Failed to fetch available models',
|
||||
models: [],
|
||||
},
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const rawModels = Array.isArray(payload?.models) ? payload.models : []
|
||||
const models: AvailableModel[] = rawModels
|
||||
.filter((item: unknown): item is RawAvailableModel => isRawAvailableModel(item))
|
||||
.map((item: RawAvailableModel) => ({
|
||||
id: item.id,
|
||||
friendlyName: item.friendlyName || item.displayName || item.id,
|
||||
provider: item.provider || 'unknown',
|
||||
}))
|
||||
|
||||
return NextResponse.json({ success: true, models })
|
||||
} catch (error) {
|
||||
logger.error('Error fetching available models', {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
})
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Failed to fetch available models',
|
||||
models: [],
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import type { CopilotModelId } from '@/lib/copilot/models'
|
||||
import { db } from '@/../../packages/db'
|
||||
import { settings } from '@/../../packages/db/schema'
|
||||
|
||||
const logger = createLogger('CopilotUserModelsAPI')
|
||||
|
||||
const DEFAULT_ENABLED_MODELS: Record<CopilotModelId, boolean> = {
|
||||
'gpt-4o': false,
|
||||
'gpt-4.1': false,
|
||||
'gpt-5-fast': false,
|
||||
'gpt-5': true,
|
||||
'gpt-5-medium': false,
|
||||
'gpt-5-high': false,
|
||||
'gpt-5.1-fast': false,
|
||||
'gpt-5.1': false,
|
||||
'gpt-5.1-medium': false,
|
||||
'gpt-5.1-high': false,
|
||||
'gpt-5-codex': false,
|
||||
'gpt-5.1-codex': false,
|
||||
'gpt-5.2': false,
|
||||
'gpt-5.2-codex': true,
|
||||
'gpt-5.2-pro': true,
|
||||
o3: true,
|
||||
'claude-4-sonnet': false,
|
||||
'claude-4.5-haiku': true,
|
||||
'claude-4.5-sonnet': true,
|
||||
'claude-4.6-opus': true,
|
||||
'claude-4.5-opus': true,
|
||||
'claude-4.1-opus': false,
|
||||
'gemini-3-pro': true,
|
||||
}
|
||||
|
||||
// GET - Fetch user's enabled models
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const session = await getSession()
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const userId = session.user.id
|
||||
|
||||
const [userSettings] = await db
|
||||
.select()
|
||||
.from(settings)
|
||||
.where(eq(settings.userId, userId))
|
||||
.limit(1)
|
||||
|
||||
if (userSettings) {
|
||||
const userModelsMap = (userSettings.copilotEnabledModels as Record<string, boolean>) || {}
|
||||
|
||||
const mergedModels = { ...DEFAULT_ENABLED_MODELS }
|
||||
for (const [modelId, enabled] of Object.entries(userModelsMap)) {
|
||||
if (modelId in mergedModels) {
|
||||
mergedModels[modelId as CopilotModelId] = enabled
|
||||
}
|
||||
}
|
||||
|
||||
const hasNewModels = Object.keys(DEFAULT_ENABLED_MODELS).some(
|
||||
(key) => !(key in userModelsMap)
|
||||
)
|
||||
|
||||
if (hasNewModels) {
|
||||
await db
|
||||
.update(settings)
|
||||
.set({
|
||||
copilotEnabledModels: mergedModels,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(settings.userId, userId))
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
enabledModels: mergedModels,
|
||||
})
|
||||
}
|
||||
|
||||
await db.insert(settings).values({
|
||||
id: userId,
|
||||
userId,
|
||||
copilotEnabledModels: DEFAULT_ENABLED_MODELS,
|
||||
})
|
||||
|
||||
logger.info('Created new settings record with default models', { userId })
|
||||
|
||||
return NextResponse.json({
|
||||
enabledModels: DEFAULT_ENABLED_MODELS,
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Failed to fetch user models', { error })
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
// PUT - Update user's enabled models
|
||||
export async function PUT(request: NextRequest) {
|
||||
try {
|
||||
const session = await getSession()
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const userId = session.user.id
|
||||
const body = await request.json()
|
||||
|
||||
if (!body.enabledModels || typeof body.enabledModels !== 'object') {
|
||||
return NextResponse.json({ error: 'enabledModels must be an object' }, { status: 400 })
|
||||
}
|
||||
|
||||
const [existing] = await db.select().from(settings).where(eq(settings.userId, userId)).limit(1)
|
||||
|
||||
if (existing) {
|
||||
await db
|
||||
.update(settings)
|
||||
.set({
|
||||
copilotEnabledModels: body.enabledModels,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(settings.userId, userId))
|
||||
} else {
|
||||
await db.insert(settings).values({
|
||||
id: userId,
|
||||
userId,
|
||||
copilotEnabledModels: body.enabledModels,
|
||||
})
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (error) {
|
||||
logger.error('Failed to update user models', { error })
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,6 @@ import { createLogger } from '@sim/logger'
|
||||
import { eq, sql } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { getHighestPrioritySubscription } from '@/lib/billing/core/subscription'
|
||||
import { getCopilotModel } from '@/lib/copilot/config'
|
||||
import {
|
||||
ORCHESTRATION_TIMEOUT_MS,
|
||||
SIM_AGENT_API_URL,
|
||||
@@ -39,6 +38,7 @@ import {
|
||||
|
||||
const logger = createLogger('CopilotMcpAPI')
|
||||
const mcpRateLimiter = new RateLimiter()
|
||||
const DEFAULT_COPILOT_MODEL = 'claude-opus-4-6'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
export const runtime = 'nodejs'
|
||||
@@ -627,7 +627,6 @@ async function handleBuildToolCall(
|
||||
): Promise<CallToolResult> {
|
||||
try {
|
||||
const requestText = (args.request as string) || JSON.stringify(args)
|
||||
const { model } = getCopilotModel('chat')
|
||||
const workflowId = args.workflowId as string | undefined
|
||||
|
||||
const resolved = workflowId
|
||||
@@ -666,7 +665,7 @@ async function handleBuildToolCall(
|
||||
message: requestText,
|
||||
workflowId: resolved.workflowId,
|
||||
userId,
|
||||
model,
|
||||
model: DEFAULT_COPILOT_MODEL,
|
||||
mode: 'agent',
|
||||
commands: ['fast'],
|
||||
messageId: randomUUID(),
|
||||
@@ -733,8 +732,6 @@ async function handleSubagentToolCall(
|
||||
context.plan = args.plan
|
||||
}
|
||||
|
||||
const { model } = getCopilotModel('chat')
|
||||
|
||||
const result = await orchestrateSubagentStream(
|
||||
toolDef.agentId,
|
||||
{
|
||||
@@ -742,7 +739,7 @@ async function handleSubagentToolCall(
|
||||
workflowId: args.workflowId,
|
||||
workspaceId: args.workspaceId,
|
||||
context,
|
||||
model,
|
||||
model: DEFAULT_COPILOT_MODEL,
|
||||
headless: true,
|
||||
source: 'mcp',
|
||||
},
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { getCopilotModel } from '@/lib/copilot/config'
|
||||
import { SIM_AGENT_VERSION } from '@/lib/copilot/constants'
|
||||
import { COPILOT_REQUEST_MODES } from '@/lib/copilot/models'
|
||||
import { orchestrateCopilotStream } from '@/lib/copilot/orchestrator'
|
||||
@@ -9,6 +8,7 @@ import { resolveWorkflowIdForUser } from '@/lib/workflows/utils'
|
||||
import { authenticateV1Request } from '@/app/api/v1/auth'
|
||||
|
||||
const logger = createLogger('CopilotHeadlessAPI')
|
||||
const DEFAULT_COPILOT_MODEL = 'claude-opus-4-6'
|
||||
|
||||
const RequestSchema = z.object({
|
||||
message: z.string().min(1, 'message is required'),
|
||||
@@ -42,8 +42,7 @@ export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const body = await req.json()
|
||||
const parsed = RequestSchema.parse(body)
|
||||
const defaults = getCopilotModel('chat')
|
||||
const selectedModel = parsed.model || defaults.model
|
||||
const selectedModel = parsed.model || DEFAULT_COPILOT_MODEL
|
||||
|
||||
// Resolve workflow ID
|
||||
const resolved = await resolveWorkflowIdForUser(
|
||||
|
||||
Reference in New Issue
Block a user