2026-01-30 13:41:32 +01:00
import OpenAI from 'openai'
2026-02-03 11:58:12 +01:00
import type { ChatCompletionCreateParamsNonStreaming } from 'openai/resources/chat/completions'
2026-02-21 17:26:59 +01:00
import Anthropic from '@anthropic-ai/sdk'
2026-01-30 13:41:32 +01:00
import { prisma } from './prisma'
2026-02-21 17:26:59 +01:00
// Hardcoded Claude model list (Anthropic API doesn't expose a models.list endpoint for all users)
export const ANTHROPIC_CLAUDE_MODELS = [
'claude-opus-4-5-20250514' ,
'claude-sonnet-4-5-20250514' ,
'claude-haiku-3-5-20241022' ,
'claude-opus-4-20250514' ,
'claude-sonnet-4-20250514' ,
] as const
/ * *
* AI client type returned by getOpenAI ( ) .
* Both the OpenAI SDK and the Anthropic adapter satisfy this interface .
* All AI services only use . chat . completions . create ( ) , so this is safe .
* /
export type AIClient = OpenAI | AnthropicClientAdapter
type AnthropicClientAdapter = {
__isAnthropicAdapter : true
chat : {
completions : {
create ( params : ChatCompletionCreateParamsNonStreaming ) : Promise < OpenAI.Chat.Completions.ChatCompletion >
}
}
}
2026-01-30 13:41:32 +01:00
// OpenAI client singleton with lazy initialization
const globalForOpenAI = globalThis as unknown as {
2026-02-21 17:26:59 +01:00
openai : AIClient | undefined
2026-01-30 13:41:32 +01:00
openaiInitialized : boolean
}
2026-02-16 15:48:34 +01:00
// ─── Provider Detection ─────────────────────────────────────────────────────
/ * *
* Get the configured AI provider from SystemSettings .
2026-02-21 17:26:59 +01:00
* Returns 'openai' ( default ) , 'litellm' ( ChatGPT subscription proxy ) , or 'anthropic' ( Claude API ) .
2026-02-16 15:48:34 +01:00
* /
2026-02-21 17:26:59 +01:00
export async function getConfiguredProvider ( ) : Promise < 'openai' | 'litellm' | 'anthropic' > {
2026-02-16 15:48:34 +01:00
try {
const setting = await prisma . systemSettings . findUnique ( {
where : { key : 'ai_provider' } ,
} )
const value = setting ? . value || 'openai'
2026-02-21 17:26:59 +01:00
if ( value === 'litellm' ) return 'litellm'
if ( value === 'anthropic' ) return 'anthropic'
return 'openai'
2026-02-16 15:48:34 +01:00
} catch {
return 'openai'
}
}
/ * *
* Check if a model ID indicates LiteLLM ChatGPT subscription routing .
* Models like 'chatgpt/gpt-5.2' use the chatgpt / prefix .
* Used by buildCompletionParams ( sync ) to strip unsupported token limit fields .
* /
export function isLiteLLMChatGPTModel ( model : string ) : boolean {
return model . toLowerCase ( ) . startsWith ( 'chatgpt/' )
}
2026-02-03 11:58:12 +01:00
// ─── Model Type Detection ────────────────────────────────────────────────────
/ * *
* Reasoning models that require different API parameters :
* - Use max_completion_tokens instead of max_tokens
* - Don ' t support response_format : json_object ( must instruct JSON in prompt )
* - Don ' t support temperature parameter
* - Don ' t support system messages ( use developer or user role instead )
* /
const REASONING_MODEL_PREFIXES = [ 'o1' , 'o3' , 'o4' ]
2026-02-03 13:08:01 +01:00
/ * *
* Models that use max_completion_tokens instead of max_tokens .
* This includes reasoning models AND newer GPT models ( GPT - 5 + ) .
* /
const NEW_TOKEN_PARAM_PREFIXES = [ 'o1' , 'o3' , 'o4' , 'gpt-5' , 'gpt-6' , 'gpt-7' ]
2026-02-03 15:04:16 +01:00
/ * *
* Models that don ' t support custom temperature values .
* These only accept the default temperature ( 1 ) .
* /
const NO_TEMPERATURE_PREFIXES = [ 'o1' , 'o3' , 'o4' , 'gpt-5' , 'gpt-6' , 'gpt-7' ]
2026-02-03 11:58:12 +01:00
/ * *
* Check if a model is a reasoning model ( o1 , o3 , o4 series )
2026-02-03 13:08:01 +01:00
* These models have additional restrictions ( no temperature , no json_object , etc . )
2026-02-03 11:58:12 +01:00
* /
export function isReasoningModel ( model : string ) : boolean {
const modelLower = model . toLowerCase ( )
return REASONING_MODEL_PREFIXES . some ( prefix = >
modelLower . startsWith ( prefix ) ||
modelLower . includes ( ` / ${ prefix } ` ) ||
modelLower . includes ( ` - ${ prefix } ` )
)
}
2026-02-03 13:08:01 +01:00
/ * *
* Check if a model requires max_completion_tokens instead of max_tokens .
* This includes reasoning models AND newer GPT models ( GPT - 5 + ) .
* /
export function usesNewTokenParam ( model : string ) : boolean {
const modelLower = model . toLowerCase ( )
return NEW_TOKEN_PARAM_PREFIXES . some ( prefix = >
modelLower . startsWith ( prefix ) ||
modelLower . includes ( ` / ${ prefix } ` ) ||
modelLower . includes ( ` - ${ prefix } ` )
)
}
2026-02-03 15:04:16 +01:00
/ * *
* Check if a model supports custom temperature values .
* Newer models ( o - series , GPT - 5 + ) only accept default temperature ( 1 ) .
* /
export function supportsTemperature ( model : string ) : boolean {
const modelLower = model . toLowerCase ( )
return ! NO_TEMPERATURE_PREFIXES . some ( prefix = >
modelLower . startsWith ( prefix ) ||
modelLower . includes ( ` / ${ prefix } ` ) ||
modelLower . includes ( ` - ${ prefix } ` )
)
}
2026-02-05 15:02:22 +01:00
/ * *
* Check if a model requires higher token limits due to reasoning overhead .
* GPT - 5 nano especially needs more tokens as reasoning consumes output budget .
* /
export function needsHigherTokenLimit ( model : string ) : boolean {
const modelLower = model . toLowerCase ( )
return modelLower . includes ( 'nano' ) || modelLower . includes ( 'gpt-5' )
}
/ * *
* Get minimum recommended max_tokens for a model .
* Reasoning models need higher limits because internal reasoning consumes tokens .
* /
export function getMinTokenLimit ( model : string , requestedLimit? : number ) : number | undefined {
// For GPT-5 nano, reasoning uses significant token budget
// If user requests < 8000, bump it up or remove limit
if ( needsHigherTokenLimit ( model ) ) {
const minLimit = 16000 // Ensure enough headroom for reasoning
if ( ! requestedLimit ) return undefined // No limit = model default
return Math . max ( requestedLimit , minLimit )
}
return requestedLimit
}
2026-02-03 11:58:12 +01:00
// ─── Chat Completion Parameter Builder ───────────────────────────────────────
type MessageRole = 'system' | 'user' | 'assistant' | 'developer'
export interface ChatCompletionOptions {
messages : Array < { role : MessageRole ; content : string } >
maxTokens? : number
temperature? : number
jsonMode? : boolean
}
/ * *
* Build chat completion parameters with correct settings for the model type .
* Handles differences between standard models and reasoning models .
* /
export function buildCompletionParams (
model : string ,
options : ChatCompletionOptions
) : ChatCompletionCreateParamsNonStreaming {
const isReasoning = isReasoningModel ( model )
// Convert messages for reasoning models (system -> developer)
const messages = options . messages . map ( msg = > {
if ( isReasoning && msg . role === 'system' ) {
return { role : 'developer' as const , content : msg.content }
}
return msg as { role : 'system' | 'user' | 'assistant' | 'developer' ; content : string }
} )
// For reasoning models requesting JSON, append JSON instruction to last user message
if ( isReasoning && options . jsonMode ) {
// Find last user message index (polyfill for findLastIndex)
let lastUserIdx = - 1
for ( let i = messages . length - 1 ; i >= 0 ; i -- ) {
if ( messages [ i ] . role === 'user' ) {
lastUserIdx = i
break
}
}
if ( lastUserIdx !== - 1 ) {
messages [ lastUserIdx ] = {
. . . messages [ lastUserIdx ] ,
content : messages [ lastUserIdx ] . content + '\n\nIMPORTANT: Respond with valid JSON only, no other text.' ,
}
}
}
const params : ChatCompletionCreateParamsNonStreaming = {
model ,
messages : messages as ChatCompletionCreateParamsNonStreaming [ 'messages' ] ,
}
// Token limit parameter differs between model types
2026-02-03 13:08:01 +01:00
// Newer models (GPT-5+, o-series) use max_completion_tokens
2026-02-05 15:02:22 +01:00
// Also ensure sufficient tokens for models with reasoning overhead (GPT-5 nano)
const effectiveMaxTokens = getMinTokenLimit ( model , options . maxTokens )
if ( effectiveMaxTokens ) {
2026-02-03 13:08:01 +01:00
if ( usesNewTokenParam ( model ) ) {
2026-02-05 15:02:22 +01:00
params . max_completion_tokens = effectiveMaxTokens
2026-02-03 11:58:12 +01:00
} else {
2026-02-05 15:02:22 +01:00
params . max_tokens = effectiveMaxTokens
2026-02-03 11:58:12 +01:00
}
}
2026-02-03 15:04:16 +01:00
// Newer models (o-series, GPT-5+) don't support custom temperature
if ( supportsTemperature ( model ) && options . temperature !== undefined ) {
2026-02-03 11:58:12 +01:00
params . temperature = options . temperature
}
// Reasoning models don't support response_format: json_object
if ( ! isReasoning && options . jsonMode ) {
params . response_format = { type : 'json_object' }
}
2026-02-16 15:48:34 +01:00
// LiteLLM ChatGPT subscription models reject token limit fields
if ( isLiteLLMChatGPTModel ( model ) ) {
delete params . max_tokens
delete params . max_completion_tokens
}
2026-02-03 11:58:12 +01:00
return params
}
2026-01-30 13:41:32 +01:00
/ * *
* Get OpenAI API key from SystemSettings
* /
async function getOpenAIApiKey ( ) : Promise < string | null > {
try {
const setting = await prisma . systemSettings . findUnique ( {
where : { key : 'openai_api_key' } ,
} )
return setting ? . value || process . env . OPENAI_API_KEY || null
} catch {
// Fall back to env var if database isn't available
return process . env . OPENAI_API_KEY || null
}
}
2026-02-21 17:26:59 +01:00
/ * *
* Get Anthropic API key from SystemSettings
* /
async function getAnthropicApiKey ( ) : Promise < string | null > {
try {
const setting = await prisma . systemSettings . findUnique ( {
where : { key : 'anthropic_api_key' } ,
} )
return setting ? . value || process . env . ANTHROPIC_API_KEY || null
} catch {
return process . env . ANTHROPIC_API_KEY || null
}
}
2026-01-30 13:41:32 +01:00
/ * *
2026-02-16 15:34:59 +01:00
* Get custom base URL for OpenAI - compatible providers .
* Supports OpenRouter , Together AI , Groq , local models , etc .
* Set via Settings → AI or OPENAI_BASE_URL env var .
* /
async function getBaseURL ( ) : Promise < string | undefined > {
try {
const setting = await prisma . systemSettings . findUnique ( {
where : { key : 'openai_base_url' } ,
} )
return setting ? . value || process . env . OPENAI_BASE_URL || undefined
} catch {
return process . env . OPENAI_BASE_URL || undefined
}
}
/ * *
* Create OpenAI client instance .
* Supports custom baseURL for OpenAI - compatible providers
* ( OpenRouter , Groq , Together AI , local models , etc . )
2026-01-30 13:41:32 +01:00
* /
async function createOpenAIClient ( ) : Promise < OpenAI | null > {
const apiKey = await getOpenAIApiKey ( )
2026-02-16 15:48:34 +01:00
const provider = await getConfiguredProvider ( )
// LiteLLM proxy may not require a real API key
const effectiveApiKey = apiKey || ( provider === 'litellm' ? 'sk-litellm' : null )
2026-01-30 13:41:32 +01:00
2026-02-16 15:48:34 +01:00
if ( ! effectiveApiKey ) {
2026-01-30 13:41:32 +01:00
console . warn ( 'OpenAI API key not configured' )
return null
}
2026-02-16 15:34:59 +01:00
const baseURL = await getBaseURL ( )
if ( baseURL ) {
2026-02-16 15:48:34 +01:00
console . log ( ` [OpenAI] Using custom base URL: ${ baseURL } (provider: ${ provider } ) ` )
2026-02-16 15:34:59 +01:00
}
2026-01-30 13:41:32 +01:00
return new OpenAI ( {
2026-02-16 15:48:34 +01:00
apiKey : effectiveApiKey ,
2026-02-16 15:34:59 +01:00
. . . ( baseURL ? { baseURL } : { } ) ,
2026-01-30 13:41:32 +01:00
} )
}
/ * *
2026-02-21 17:26:59 +01:00
* Check if a model is a Claude Opus model ( supports extended thinking ) .
2026-01-30 13:41:32 +01:00
* /
2026-02-21 17:26:59 +01:00
function isClaudeOpusModel ( model : string ) : boolean {
return model . toLowerCase ( ) . includes ( 'opus' )
}
/ * *
* Create an Anthropic adapter that wraps the Anthropic SDK behind the
* same ` .chat.completions.create() ` surface as OpenAI . This allows all
* AI service files to work with zero changes .
* /
async function createAnthropicAdapter ( ) : Promise < AnthropicClientAdapter | null > {
const apiKey = await getAnthropicApiKey ( )
if ( ! apiKey ) {
console . warn ( 'Anthropic API key not configured' )
return null
}
const baseURL = await getBaseURL ( )
const anthropic = new Anthropic ( {
apiKey ,
. . . ( baseURL ? { baseURL } : { } ) ,
} )
if ( baseURL ) {
console . log ( ` [Anthropic] Using custom base URL: ${ baseURL } ` )
}
return {
__isAnthropicAdapter : true ,
chat : {
completions : {
async create ( params : ChatCompletionCreateParamsNonStreaming ) : Promise < OpenAI.Chat.Completions.ChatCompletion > {
// Extract system messages → Anthropic's system parameter
const systemMessages : string [ ] = [ ]
const userAssistantMessages : Anthropic.MessageParam [ ] = [ ]
for ( const msg of params . messages ) {
const content = typeof msg . content === 'string' ? msg . content : ''
if ( msg . role === 'system' || msg . role === 'developer' ) {
systemMessages . push ( content )
} else {
userAssistantMessages . push ( {
role : msg.role === 'assistant' ? 'assistant' : 'user' ,
content ,
} )
}
}
// Ensure messages start with a user message (Anthropic requirement)
if ( userAssistantMessages . length === 0 || userAssistantMessages [ 0 ] . role !== 'user' ) {
userAssistantMessages . unshift ( { role : 'user' , content : 'Hello' } )
}
// Determine max_tokens (required by Anthropic, default 16384)
const maxTokens = params . max_tokens ? ? params . max_completion_tokens ? ? 16384
// Build Anthropic request
const anthropicParams : Anthropic.MessageCreateParamsNonStreaming = {
model : params.model ,
max_tokens : maxTokens ,
messages : userAssistantMessages ,
. . . ( systemMessages . length > 0 ? { system : systemMessages.join ( '\n\n' ) } : { } ) ,
}
// Add temperature if present (Anthropic supports 0-1)
if ( params . temperature !== undefined && params . temperature !== null ) {
anthropicParams . temperature = params . temperature
}
// Extended thinking for Opus models
if ( isClaudeOpusModel ( params . model ) ) {
anthropicParams . thinking = { type : 'enabled' , budget_tokens : Math.min ( 8192 , maxTokens - 1 ) }
}
// Call Anthropic API
let response = await anthropic . messages . create ( anthropicParams )
// Extract text from response (skip thinking blocks)
let responseText = response . content
. filter ( ( block ) : block is Anthropic . TextBlock = > block . type === 'text' )
. map ( ( block ) = > block . text )
. join ( '' )
// JSON retry: if response_format was set but response isn't valid JSON
const wantsJson = params . response_format && 'type' in params . response_format && params . response_format . type === 'json_object'
if ( wantsJson && responseText ) {
try {
JSON . parse ( responseText )
} catch {
// Retry once with explicit JSON instruction
const retryMessages = [ . . . userAssistantMessages ]
const lastIdx = retryMessages . length - 1
if ( lastIdx >= 0 && retryMessages [ lastIdx ] . role === 'user' ) {
retryMessages [ lastIdx ] = {
. . . retryMessages [ lastIdx ] ,
content : retryMessages [ lastIdx ] . content + '\n\nIMPORTANT: You MUST respond with valid JSON only. No markdown, no extra text, just a JSON object or array.' ,
}
}
const retryParams : Anthropic.MessageCreateParamsNonStreaming = {
. . . anthropicParams ,
messages : retryMessages ,
}
response = await anthropic . messages . create ( retryParams )
responseText = response . content
. filter ( ( block ) : block is Anthropic . TextBlock = > block . type === 'text' )
. map ( ( block ) = > block . text )
. join ( '' )
}
}
// Normalize response to OpenAI shape
return {
id : response.id ,
object : 'chat.completion' as const ,
created : Math.floor ( Date . now ( ) / 1000 ) ,
model : response.model ,
choices : [
{
index : 0 ,
message : {
role : 'assistant' as const ,
content : responseText || null ,
refusal : null ,
} ,
finish_reason : response.stop_reason === 'end_turn' || response . stop_reason === 'stop_sequence' ? 'stop' : response . stop_reason === 'max_tokens' ? 'length' : 'stop' ,
logprobs : null ,
} ,
] ,
usage : {
prompt_tokens : response.usage.input_tokens ,
completion_tokens : response.usage.output_tokens ,
total_tokens : response.usage.input_tokens + response . usage . output_tokens ,
prompt_tokens_details : undefined as any ,
completion_tokens_details : undefined as any ,
} ,
}
} ,
} ,
} ,
}
}
/ * *
* Get the AI client singleton .
* Returns an OpenAI client or an Anthropic adapter ( both expose . chat . completions . create ( ) ) .
* Returns null if the API key is not configured .
* /
export async function getOpenAI ( ) : Promise < AIClient | null > {
2026-01-30 13:41:32 +01:00
if ( globalForOpenAI . openaiInitialized ) {
return globalForOpenAI . openai || null
}
2026-02-21 17:26:59 +01:00
const provider = await getConfiguredProvider ( )
const client = provider === 'anthropic'
? await createAnthropicAdapter ( )
: await createOpenAIClient ( )
2026-01-30 13:41:32 +01:00
if ( process . env . NODE_ENV !== 'production' ) {
globalForOpenAI . openai = client || undefined
globalForOpenAI . openaiInitialized = true
}
return client
}
2026-02-16 15:34:59 +01:00
/ * *
* Reset the OpenAI client singleton ( e . g . , after settings change ) .
* Next call to getOpenAI ( ) will create a fresh client .
* /
export function resetOpenAIClient ( ) : void {
globalForOpenAI . openai = undefined
globalForOpenAI . openaiInitialized = false
}
2026-01-30 13:41:32 +01:00
/ * *
* Check if OpenAI is configured and available
* /
export async function isOpenAIConfigured ( ) : Promise < boolean > {
2026-02-16 15:48:34 +01:00
const provider = await getConfiguredProvider ( )
if ( provider === 'litellm' ) {
const baseURL = await getBaseURL ( )
return ! ! baseURL
}
2026-02-21 17:26:59 +01:00
if ( provider === 'anthropic' ) {
const apiKey = await getAnthropicApiKey ( )
return ! ! apiKey
}
2026-01-30 13:41:32 +01:00
const apiKey = await getOpenAIApiKey ( )
return ! ! apiKey
}
/ * *
2026-02-03 10:46:38 +01:00
* List available models from OpenAI API
* /
export async function listAvailableModels ( ) : Promise < {
success : boolean
models? : string [ ]
error? : string
2026-02-16 15:48:34 +01:00
manualEntry? : boolean
2026-02-03 10:46:38 +01:00
} > {
try {
2026-02-16 15:48:34 +01:00
const provider = await getConfiguredProvider ( )
// LiteLLM proxy for ChatGPT subscription doesn't support models.list()
if ( provider === 'litellm' ) {
return {
success : true ,
models : [ ] ,
manualEntry : true ,
}
}
2026-02-21 17:26:59 +01:00
// Anthropic: return hardcoded Claude model list
if ( provider === 'anthropic' ) {
const apiKey = await getAnthropicApiKey ( )
if ( ! apiKey ) {
return { success : false , error : 'Anthropic API key not configured' }
}
return {
success : true ,
models : [ . . . ANTHROPIC_CLAUDE_MODELS ] ,
}
}
2026-02-03 10:46:38 +01:00
const client = await getOpenAI ( )
if ( ! client ) {
return {
success : false ,
error : 'OpenAI API key not configured' ,
}
}
2026-02-21 17:26:59 +01:00
const response = await ( client as OpenAI ) . models . list ( )
2026-02-03 10:46:38 +01:00
const chatModels = response . data
. filter ( ( m ) = > m . id . includes ( 'gpt' ) || m . id . includes ( 'o1' ) || m . id . includes ( 'o3' ) || m . id . includes ( 'o4' ) )
. map ( ( m ) = > m . id )
. sort ( )
return {
success : true ,
models : chatModels ,
}
} catch ( error ) {
return {
success : false ,
error : error instanceof Error ? error . message : 'Unknown error' ,
}
}
}
/ * *
* Validate that a specific model is available
* /
export async function validateModel ( modelId : string ) : Promise < {
valid : boolean
error? : string
} > {
try {
const client = await getOpenAI ( )
if ( ! client ) {
return {
valid : false ,
2026-02-21 17:26:59 +01:00
error : 'AI API key not configured' ,
2026-02-03 10:46:38 +01:00
}
}
2026-02-21 17:26:59 +01:00
const provider = await getConfiguredProvider ( )
// For Anthropic, use minimal max_tokens
2026-02-03 11:58:12 +01:00
const params = buildCompletionParams ( modelId , {
2026-02-03 10:46:38 +01:00
messages : [ { role : 'user' , content : 'test' } ] ,
2026-02-21 17:26:59 +01:00
maxTokens : provider === 'anthropic' ? 16 : 1 ,
2026-02-03 10:46:38 +01:00
} )
2026-02-03 11:58:12 +01:00
await client . chat . completions . create ( params )
2026-02-03 10:46:38 +01:00
return { valid : true }
} catch ( error ) {
const message = error instanceof Error ? error . message : 'Unknown error'
// Check for specific model errors
if ( message . includes ( 'does not exist' ) || message . includes ( 'model_not_found' ) ) {
return {
valid : false ,
error : ` Model " ${ modelId } " is not available with your API key ` ,
}
}
return {
valid : false ,
error : message ,
}
}
}
/ * *
* Test OpenAI connection with the configured model
2026-01-30 13:41:32 +01:00
* /
export async function testOpenAIConnection ( ) : Promise < {
success : boolean
error? : string
model? : string
2026-02-03 10:46:38 +01:00
modelTested? : string
2026-01-30 13:41:32 +01:00
} > {
try {
const client = await getOpenAI ( )
2026-02-21 17:26:59 +01:00
const provider = await getConfiguredProvider ( )
2026-01-30 13:41:32 +01:00
if ( ! client ) {
2026-02-21 17:26:59 +01:00
const label = provider === 'anthropic' ? 'Anthropic' : 'OpenAI'
2026-01-30 13:41:32 +01:00
return {
success : false ,
2026-02-21 17:26:59 +01:00
error : ` ${ label } API key not configured ` ,
2026-01-30 13:41:32 +01:00
}
}
2026-02-03 10:46:38 +01:00
// Get the configured model
const configuredModel = await getConfiguredModel ( )
2026-02-03 11:58:12 +01:00
// Test with the configured model using correct parameters
const params = buildCompletionParams ( configuredModel , {
2026-01-30 13:41:32 +01:00
messages : [ { role : 'user' , content : 'Hello' } ] ,
2026-02-21 17:26:59 +01:00
maxTokens : provider === 'anthropic' ? 16 : 5 ,
2026-01-30 13:41:32 +01:00
} )
2026-02-03 11:58:12 +01:00
const response = await client . chat . completions . create ( params )
2026-01-30 13:41:32 +01:00
return {
success : true ,
model : response.model ,
2026-02-03 10:46:38 +01:00
modelTested : configuredModel ,
2026-01-30 13:41:32 +01:00
}
} catch ( error ) {
2026-02-03 10:46:38 +01:00
const message = error instanceof Error ? error . message : 'Unknown error'
const configuredModel = await getConfiguredModel ( )
// Check for model-specific errors
2026-02-21 17:26:59 +01:00
if ( message . includes ( 'does not exist' ) || message . includes ( 'model_not_found' ) || message . includes ( 'not_found_error' ) ) {
2026-02-03 10:46:38 +01:00
return {
success : false ,
error : ` Model " ${ configuredModel } " is not available. Check Settings → AI to select a valid model. ` ,
modelTested : configuredModel ,
}
}
2026-01-30 13:41:32 +01:00
return {
success : false ,
2026-02-03 10:46:38 +01:00
error : message ,
modelTested : configuredModel ,
2026-01-30 13:41:32 +01:00
}
}
}
// Default models for different use cases
export const AI_MODELS = {
ASSIGNMENT : 'gpt-4o' , // Best for complex reasoning
QUICK : 'gpt-4o-mini' , // Faster, cheaper for simple tasks
} as const
2026-01-30 16:24:46 +01:00
/ * *
* Get the admin - configured AI model from SystemSettings .
* Falls back to the provided default if not configured .
* /
export async function getConfiguredModel ( fallback : string = AI_MODELS . ASSIGNMENT ) : Promise < string > {
try {
const setting = await prisma . systemSettings . findUnique ( {
where : { key : 'ai_model' } ,
} )
return setting ? . value || process . env . OPENAI_MODEL || fallback
} catch {
return process . env . OPENAI_MODEL || fallback
}
}