Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
Copilot enterprise models
  • Loading branch information
Sg312 committed Feb 10, 2026
commit 59cb2f0d2db9238e724e0389e88192f471a2c83e
4 changes: 2 additions & 2 deletions apps/sim/app/api/copilot/chat/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ 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 { COPILOT_REQUEST_MODES } from '@/lib/copilot/models'
import { orchestrateCopilotStream } from '@/lib/copilot/orchestrator'
import {
createStreamEventWriter,
Expand Down Expand Up @@ -43,7 +43,7 @@ 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-4.6-opus'),
mode: z.enum(COPILOT_REQUEST_MODES).optional().default('agent'),
prefetch: z.boolean().optional(),
createNewChat: z.boolean().optional().default(false),
Expand Down
68 changes: 68 additions & 0 deletions apps/sim/app/api/copilot/models/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
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 { env } from '@/lib/core/config/env'
import type { AvailableModel } from '@/lib/copilot/types'

const logger = createLogger('CopilotModelsAPI')

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: any) => item && typeof item.id === 'string')
.map((item: any) => ({
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 }
)
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client'

import { useEffect, useRef, useState } from 'react'
import { useEffect, useMemo, useRef, useState } from 'react'
import {
Badge,
Popover,
Expand All @@ -9,8 +9,14 @@ import {
PopoverItem,
PopoverScrollArea,
} from '@/components/emcn'
import { getProviderIcon } from '@/providers/utils'
import { MODEL_OPTIONS } from '../../constants'
import {
AnthropicIcon,
AzureIcon,
BedrockIcon,
GeminiIcon,
OpenAIIcon,
} from '@/components/icons'
import { useCopilotStore } from '@/stores/panel'

interface ModelSelectorProps {
/** Currently selected model */
Expand All @@ -22,14 +28,22 @@ interface ModelSelectorProps {
}

/**
* Gets the appropriate icon component for a model
* Map a provider string (from the available-models API) to its icon component.
* Falls back to null when the provider is unrecognised.
*/
function getModelIconComponent(modelValue: string) {
const IconComponent = getProviderIcon(modelValue)
if (!IconComponent) {
return null
}
return <IconComponent className='h-3.5 w-3.5' />
const PROVIDER_ICON_MAP: Record<string, React.ComponentType<{ className?: string }>> = {
anthropic: AnthropicIcon,
openai: OpenAIIcon,
gemini: GeminiIcon,
google: GeminiIcon,
bedrock: BedrockIcon,
azure: AzureIcon,
'azure-openai': AzureIcon,
'azure-anthropic': AzureIcon,
}

function getIconForProvider(provider: string): React.ComponentType<{ className?: string }> | null {
return PROVIDER_ICON_MAP[provider] ?? null
}

/**
Expand All @@ -43,24 +57,46 @@ export function ModelSelector({ selectedModel, isNearTop, onModelSelect }: Model
const [open, setOpen] = useState(false)
const triggerRef = useRef<HTMLDivElement>(null)
const popoverRef = useRef<HTMLDivElement>(null)
const availableModels = useCopilotStore((state) => state.availableModels)

const modelOptions = useMemo(() => {
return availableModels.map((model) => ({
value: model.id,
label: model.friendlyName || model.id,
provider: model.provider,
}))
}, [availableModels])

/** Look up the provider for a model id from the available-models list */
const getProviderForModel = (modelId: string): string | undefined => {
return availableModels.find((m) => m.id === modelId)?.provider
}

const getCollapsedModeLabel = () => {
const model = MODEL_OPTIONS.find((m) => m.value === selectedModel)
return model ? model.label : 'claude-4.5-sonnet'
const model = modelOptions.find((m) => m.value === selectedModel)
return model?.label || selectedModel || 'No models available'
}

const getModelIcon = () => {
const IconComponent = getProviderIcon(selectedModel)
if (!IconComponent) {
return null
}
const provider = getProviderForModel(selectedModel)
if (!provider) return null
const IconComponent = getIconForProvider(provider)
if (!IconComponent) return null
return (
<span className='flex-shrink-0'>
<IconComponent className='h-3 w-3' />
</span>
)
}

const getModelIconComponent = (modelValue: string) => {
const provider = getProviderForModel(modelValue)
if (!provider) return null
const IconComponent = getIconForProvider(provider)
if (!IconComponent) return null
return <IconComponent className='h-3.5 w-3.5' />
}

const handleSelect = (modelValue: string) => {
onModelSelect(modelValue)
setOpen(false)
Expand Down Expand Up @@ -124,16 +160,20 @@ export function ModelSelector({ selectedModel, isNearTop, onModelSelect }: Model
onCloseAutoFocus={(e) => e.preventDefault()}
>
<PopoverScrollArea className='space-y-[2px]'>
{MODEL_OPTIONS.map((option) => (
<PopoverItem
key={option.value}
active={selectedModel === option.value}
onClick={() => handleSelect(option.value)}
>
{getModelIconComponent(option.value)}
<span>{option.label}</span>
</PopoverItem>
))}
{modelOptions.length > 0 ? (
modelOptions.map((option) => (
<PopoverItem
key={option.value}
active={selectedModel === option.value}
onClick={() => handleSelect(option.value)}
>
{getModelIconComponent(option.value)}
<span>{option.label}</span>
</PopoverItem>
))
) : (
<div className='px-2 py-2 text-xs text-[var(--text-muted)]'>No models available</div>
)}
</PopoverScrollArea>
</PopoverContent>
</Popover>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -242,19 +242,6 @@ export function getCommandDisplayLabel(commandId: string): string {
return command?.label || commandId.charAt(0).toUpperCase() + commandId.slice(1)
}

/**
* Model configuration options
*/
export const MODEL_OPTIONS = [
{ value: 'claude-4.6-opus', label: 'Claude 4.6 Opus' },
{ value: 'claude-4.5-opus', label: 'Claude 4.5 Opus' },
{ value: 'claude-4.5-sonnet', label: 'Claude 4.5 Sonnet' },
{ value: 'claude-4.5-haiku', label: 'Claude 4.5 Haiku' },
{ value: 'gpt-5.2-codex', label: 'GPT 5.2 Codex' },
{ value: 'gpt-5.2-pro', label: 'GPT 5.2 Pro' },
{ value: 'gemini-3-pro', label: 'Gemini 3 Pro' },
] as const

/**
* Threshold for considering input "near top" of viewport (in pixels)
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ export const Copilot = forwardRef<CopilotRef, CopilotProps>(({ panelWidth }, ref
closePlanTodos,
clearPlanArtifact,
savePlanArtifact,
loadAvailableModels,
loadAutoAllowedTools,
resumeActiveStream,
} = useCopilotStore()
Expand All @@ -123,6 +124,7 @@ export const Copilot = forwardRef<CopilotRef, CopilotProps>(({ panelWidth }, ref
chatsLoadedForWorkflow,
setCopilotWorkflowId,
loadChats,
loadAvailableModels,
loadAutoAllowedTools,
currentChat,
isSendingMessage,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ interface UseCopilotInitializationProps {
chatsLoadedForWorkflow: string | null
setCopilotWorkflowId: (workflowId: string | null) => Promise<void>
loadChats: (forceRefresh?: boolean) => Promise<void>
loadAvailableModels: () => Promise<void>
loadAutoAllowedTools: () => Promise<void>
currentChat: any
isSendingMessage: boolean
Expand All @@ -30,6 +31,7 @@ export function useCopilotInitialization(props: UseCopilotInitializationProps) {
chatsLoadedForWorkflow,
setCopilotWorkflowId,
loadChats,
loadAvailableModels,
loadAutoAllowedTools,
currentChat,
isSendingMessage,
Expand Down Expand Up @@ -129,6 +131,17 @@ export function useCopilotInitialization(props: UseCopilotInitializationProps) {
}
}, [loadAutoAllowedTools])

/** Load available models once on mount */
const hasLoadedModelsRef = useRef(false)
useEffect(() => {
if (!hasLoadedModelsRef.current) {
hasLoadedModelsRef.current = true
loadAvailableModels().catch((err) => {
logger.warn('[Copilot] Failed to load available models', err)
})
}
}, [loadAvailableModels])

return {
isInitialized,
}
Expand Down
50 changes: 0 additions & 50 deletions apps/sim/lib/copilot/chat-payload.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import { createLogger } from '@sim/logger'
import { processFileAttachments } from '@/lib/copilot/chat-context'
import { getCopilotModel } from '@/lib/copilot/config'
import { SIM_AGENT_VERSION } from '@/lib/copilot/constants'
import { getCredentialsServerTool } from '@/lib/copilot/tools/server/user/get-credentials'
import type { CopilotProviderConfig } from '@/lib/copilot/types'
import { env } from '@/lib/core/config/env'
import { tools } from '@/tools/registry'
import { getLatestVersionTools, stripVersionSuffix } from '@/tools/utils'

Expand Down Expand Up @@ -46,57 +43,12 @@ interface CredentialsPayload {
}
}

function buildProviderConfig(selectedModel: string): CopilotProviderConfig | undefined {
const defaults = getCopilotModel('chat')
const envModel = env.COPILOT_MODEL || defaults.model
const providerEnv = env.COPILOT_PROVIDER

if (!providerEnv) return undefined

if (providerEnv === 'azure-openai') {
return {
provider: 'azure-openai',
model: envModel,
apiKey: env.AZURE_OPENAI_API_KEY,
apiVersion: 'preview',
endpoint: env.AZURE_OPENAI_ENDPOINT,
}
}

if (providerEnv === 'azure-anthropic') {
return {
provider: 'azure-anthropic',
model: envModel,
apiKey: env.AZURE_ANTHROPIC_API_KEY,
apiVersion: env.AZURE_ANTHROPIC_API_VERSION,
endpoint: env.AZURE_ANTHROPIC_ENDPOINT,
}
}

if (providerEnv === 'vertex') {
return {
provider: 'vertex',
model: envModel,
apiKey: env.COPILOT_API_KEY,
vertexProject: env.VERTEX_PROJECT,
vertexLocation: env.VERTEX_LOCATION,
}
}

return {
provider: providerEnv as Exclude<string, 'azure-openai' | 'vertex'>,
model: selectedModel,
apiKey: env.COPILOT_API_KEY,
} as CopilotProviderConfig
}

/**
* Build the request payload for the copilot backend.
*/
export async function buildCopilotRequestPayload(
params: BuildPayloadParams,
options: {
providerConfig?: CopilotProviderConfig
selectedModel: string
}
): Promise<Record<string, unknown>> {
Expand All @@ -113,7 +65,6 @@ export async function buildCopilotRequestPayload(
} = params

const selectedModel = options.selectedModel
const providerConfig = options.providerConfig ?? buildProviderConfig(selectedModel)

const effectiveMode = mode === 'agent' ? 'build' : mode
const transportMode = effectiveMode === 'build' ? 'agent' : effectiveMode
Expand Down Expand Up @@ -198,7 +149,6 @@ export async function buildCopilotRequestPayload(
mode: transportMode,
messageId: userMessageId,
version: SIM_AGENT_VERSION,
...(providerConfig ? { provider: providerConfig } : {}),
...(contexts && contexts.length > 0 ? { context: contexts } : {}),
...(chatId ? { chatId } : {}),
...(processedFileContents.length > 0 ? { fileAttachments: processedFileContents } : {}),
Expand Down
3 changes: 3 additions & 0 deletions apps/sim/lib/copilot/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,9 @@ export const COPILOT_CHECKPOINTS_REVERT_API_PATH = '/api/copilot/checkpoints/rev
/** GET/POST/DELETE — manage auto-allowed tools. */
export const COPILOT_AUTO_ALLOWED_TOOLS_API_PATH = '/api/copilot/auto-allowed-tools'

/** GET — fetch dynamically available copilot models. */
export const COPILOT_MODELS_API_PATH = '/api/copilot/models'

/** GET — fetch user credentials for masking. */
export const COPILOT_CREDENTIALS_API_PATH = '/api/copilot/credentials'

Expand Down
2 changes: 1 addition & 1 deletion apps/sim/lib/copilot/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export const COPILOT_MODEL_IDS = [
'gemini-3-pro',
] as const

export type CopilotModelId = (typeof COPILOT_MODEL_IDS)[number]
export type CopilotModelId = string

export const COPILOT_MODES = ['ask', 'build', 'plan'] as const
export type CopilotMode = (typeof COPILOT_MODES)[number]
Expand Down
Loading