fix(ai): issue #141 — custom OpenAI-compatible providers work + language selection on first start in sleep mode

- callOllama: reasoning_effort only for provider=ollama (others returned 400 -> stub reply)
- endpoint normalization: base URL (Groq /openai/v1) -> /chat/completions appended
- reasoning block stripping: <thinking>/<think> tags + thinking/response markers (Groq qwen)
- handleStart: sleep mode + language_set=0 -> show language selection keyboard first
- handleSetLanguage: sleep mode -> AI dialog in chosen language after selection
This commit is contained in:
NW
2026-08-08 15:31:19 +01:00
parent b352d1434c
commit bfa21fc156
2 changed files with 76 additions and 14 deletions

View File

@@ -140,7 +140,7 @@ function generateCustomerProfile(messages: ChatMessage[]): string {
return JSON.stringify(profile);
}
// ── Ollama Cloud API call (OpenAI-compatible) ──
// ── LLM API call (OpenAI-compatible) ──
async function callOllama(
messages: { role: string; content: string }[],
endpoint: string,
@@ -148,22 +148,34 @@ async function callOllama(
model: string,
temperature: number,
maxTokens: number,
provider: string,
): Promise<string> {
const res = await fetch(endpoint, {
const body: Record<string, unknown> = {
model,
messages,
temperature,
max_tokens: maxTokens,
stream: false,
};
// reasoning_effort — параметр только Ollama Cloud; другие OpenAI-совместимые API возвращают 400
if (provider === 'ollama') {
body.reasoning_effort = 'none';
}
// Нормализация endpoint: если указан базовый URL (без /chat/completions) — добавляем
// (Groq: https://api.groq.com/openai/v1 → /openai/v1/chat/completions)
let url = endpoint;
if (!/\/chat\/completions$/i.test(url)) {
url = url.replace(/\/+$/, '') + '/chat/completions';
}
const res = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${apiKey}`,
},
body: JSON.stringify({
model,
messages,
temperature,
max_tokens: maxTokens,
stream: false,
// Отключаем reasoning-токены — возвращаем только content
reasoning_effort: 'none',
}),
body: JSON.stringify(body),
});
if (!res.ok) {
@@ -172,7 +184,28 @@ async function callOllama(
}
const data = await res.json();
return data?.choices?.[0]?.message?.content || 'Извините, не удалось получить ответ.';
let content = data?.choices?.[0]?.message?.content || 'Извините, не удалось получить ответ.';
// ── Убираем блоки рассуждений (reasoning) ──
// Форматы от разных провайдеров:
// Groq qwen: "<think>...рассуждения...</think>\n response\n<ответ>"
// Ollama: "\n thinking\n<рассуждения>\n response\n<ответ>"
// 1) Полные теги <think>...</think> / <thinking>...</thinking>
content = content.replace(/<think(?:ing)?>[\s\S]*?<\/think(?:ing)?>/gi, '');
// 2) Незакрытый <think> — вырезаем до конца (или до маркера response)
content = content.replace(/<think(?:ing)?>[\s\S]*$/gi, '');
// 3) Строка-маркер "response" — берём всё после неё
const lines = content.split('\n');
const respIdx = lines.findIndex((l) => l.trim().toLowerCase() === 'response');
if (respIdx !== -1) {
content = lines.slice(respIdx + 1).join('\n');
} else {
// 4) Нет маркера response — строка "thinking": отрезаем рассуждения
const thinkIdx = lines.findIndex((l) => l.trim().toLowerCase() === 'thinking');
if (thinkIdx !== -1) {
content = lines.slice(thinkIdx + 1).join('\n');
}
}
return content.trim();
}
export async function POST(request: NextRequest) {
@@ -429,7 +462,7 @@ export async function POST(request: NextRequest) {
if (!apiKey) {
throw new Error('Ollama API key is not configured');
}
reply = await callOllama(apiMessages, apiEndpoint, apiKey, model, temperature, maxTokens);
reply = await callOllama(apiMessages, apiEndpoint, apiKey, model, temperature, maxTokens, provider);
if (typeof reply !== 'string') {
reply = JSON.stringify(reply);
}