Files
backend/src/common/ai-provider.util.ts
T
Alireza HassaniandCursor 23ba6344f6 Switch AI defaults to OpenAI gpt-4.1-nano.
Prefer the cheapest ChatGPT model that supports our JSON completions, and skip custom temperature for gpt-5/o-series models.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-10 20:33:43 +03:30

100 lines
2.6 KiB
TypeScript

import { ServiceUnavailableException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
export type AiProviderConfig = {
apiKey: string;
baseUrl: string;
model: string;
};
export function resolveAiProvider(config: ConfigService): AiProviderConfig {
const configured = config.get<string>('AI_PROVIDER')?.trim().toLowerCase();
const groqKey = config.get<string>('GROQ_API_KEY')?.trim();
const openAiKey = config.get<string>('OPENAI_API_KEY')?.trim();
const useGroq =
configured === 'groq' || (!configured && !!groqKey) || (!openAiKey && !!groqKey);
if (useGroq) {
if (!groqKey) {
throw new ServiceUnavailableException(
'AI is not configured. Set GROQ_API_KEY on the server.',
);
}
return {
apiKey: groqKey,
baseUrl: 'https://api.groq.com/openai/v1',
model: config.get<string>('GROQ_MODEL')?.trim() || 'llama-3.3-70b-versatile',
};
}
if (!openAiKey) {
throw new ServiceUnavailableException(
'AI is not configured. Set GROQ_API_KEY or OPENAI_API_KEY on the server.',
);
}
return {
apiKey: openAiKey,
baseUrl: 'https://api.openai.com/v1',
model: config.get<string>('OPENAI_MODEL')?.trim() || 'gpt-4.1-nano',
};
}
function supportsCustomTemperature(model: string): boolean {
// gpt-5 / o-series chat models only accept the API default temperature.
const id = model.trim().toLowerCase();
return !(
id.startsWith('gpt-5') ||
id.startsWith('o1') ||
id.startsWith('o3') ||
id.startsWith('o4')
);
}
export async function requestAiJsonCompletion(
provider: AiProviderConfig,
systemPrompt: string,
userPrompt: string,
temperature = 0.4,
): Promise<string> {
const body: Record<string, unknown> = {
model: provider.model,
response_format: { type: 'json_object' },
messages: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: userPrompt },
],
};
if (supportsCustomTemperature(provider.model)) {
body.temperature = temperature;
}
const response = await fetch(`${provider.baseUrl}/chat/completions`, {
method: 'POST',
headers: {
Authorization: `Bearer ${provider.apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
});
if (!response.ok) {
const detail = await response.text();
throw new Error(`AI provider error (${response.status}): ${detail.slice(0, 240)}`);
}
const payload = (await response.json()) as {
choices?: { message?: { content?: string } }[];
};
const content = payload.choices?.[0]?.message?.content;
if (!content) {
throw new Error('AI returned an empty response');
}
return content;
}