Compare commits

...

1 Commits

Author SHA1 Message Date
Flatlogic Bot
19f690ae09 Autosave: 20260605-030433 2026-06-05 03:04:33 +00:00
9 changed files with 250 additions and 40 deletions

View File

@ -5,6 +5,7 @@ const path = require("path");
const http = require("http"); const http = require("http");
const https = require("https"); const https = require("https");
const { URL } = require("url"); const { URL } = require("url");
const { DEFAULT_AI_MODEL, normalizeAiModel } = require("./modelNormalizer");
let CONFIG_CACHE = null; let CONFIG_CACHE = null;
@ -46,9 +47,7 @@ async function createResponse(params, options = {}) {
} }
const cfg = config(); const cfg = config();
if (!payload.model) { payload.model = normalizeAiModel(payload.model, cfg.defaultModel);
payload.model = cfg.defaultModel;
}
const initial = await request(options.path, payload, options); const initial = await request(options.path, payload, options);
if (!initial.success) { if (!initial.success) {
@ -154,7 +153,7 @@ async function awaitResponse(aiRequestId, options = {}) {
const interval = Math.max(Number(options.interval ?? 5), 1); const interval = Math.max(Number(options.interval ?? 5), 1);
const deadline = Date.now() + Math.max(timeout, interval) * 1000; const deadline = Date.now() + Math.max(timeout, interval) * 1000;
while (true) { while (Date.now() < deadline) {
const statusResp = await fetchStatus(aiRequestId, { const statusResp = await fetchStatus(aiRequestId, {
headers: options.headers, headers: options.headers,
timeout: options.timeout_per_call, timeout: options.timeout_per_call,
@ -184,16 +183,14 @@ async function awaitResponse(aiRequestId, options = {}) {
return statusResp; return statusResp;
} }
if (Date.now() >= deadline) {
return {
success: false,
error: "timeout",
message: "Timed out waiting for AI response.",
};
}
await sleep(interval * 1000); await sleep(interval * 1000);
} }
return {
success: false,
error: "timeout",
message: "Timed out waiting for AI response.",
};
} }
function extractText(response) { function extractText(response) {
@ -283,7 +280,7 @@ function config() {
projectId, projectId,
projectUuid: process.env.PROJECT_UUID || null, projectUuid: process.env.PROJECT_UUID || null,
projectHeader: process.env.AI_PROJECT_HEADER || "project-uuid", projectHeader: process.env.AI_PROJECT_HEADER || "project-uuid",
defaultModel: process.env.AI_DEFAULT_MODEL || "gpt-5-mini", defaultModel: process.env.AI_DEFAULT_MODEL || process.env.GEMINI_MODEL || DEFAULT_AI_MODEL,
timeout, timeout,
verifyTls, verifyTls,
}; };

View File

@ -0,0 +1,109 @@
const RAW_DEFAULT_AI_MODEL = process.env.AI_DEFAULT_MODEL
|| process.env.GEMINI_MODEL
|| 'appwizzy-default';
const SUPPORTED_AI_MODELS = new Set([
'appwizzy-default',
'google/gemini-3-flash-preview',
'google/gemini-2.5-flash',
'google/gemini-2.0-flash',
'gemini/gemini-3-flash-preview',
'gemini/gemini-2.5-flash',
'gemini-3-flash-preview',
'gemini-2.5-flash',
'gemini-2.0-flash',
'gemini-1.5-flash',
'gpt-5-mini',
'gpt-5',
'gpt-5.1',
'gpt-5.1-mini',
'gpt-5.2',
'gpt-4.1',
'gpt-4.1-mini',
'gpt-4o',
'gpt-4o-mini',
'openai/gpt-4.1',
'openai/gpt-4.1-mini',
'openai/gpt-4o',
'openai/gpt-4o-mini',
]);
const MODEL_ALIASES = new Map([
['appwizzydefault', 'appwizzy-default'],
['appwizzy-default', 'appwizzy-default'],
['gpt4.1mini', 'gpt-4.1-mini'],
['gpt41mini', 'gpt-4.1-mini'],
['gpt4.1', 'gpt-4.1'],
['gpt41', 'gpt-4.1'],
['gpt5mini', 'gpt-5-mini'],
['gpt5', 'gpt-5'],
['gpt5.1mini', 'gpt-5.1-mini'],
['gpt51mini', 'gpt-5.1-mini'],
['gpt5.1', 'gpt-5.1'],
['gpt51', 'gpt-5.1'],
['gpt5.2', 'gpt-5.2'],
['gpt52', 'gpt-5.2'],
['gpt-5.5', 'appwizzy-default'],
['gpt5.5', 'appwizzy-default'],
['gpt55', 'appwizzy-default'],
]);
function normalizeModelText(value) {
if (typeof value !== 'string') {
return '';
}
const trimmed = value.trim().toLowerCase();
if (!trimmed) {
return '';
}
if (MODEL_ALIASES.has(trimmed)) {
return MODEL_ALIASES.get(trimmed);
}
const hyphenated = trimmed
.replace(/[\s_]+/g, '-')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '');
if (MODEL_ALIASES.has(hyphenated)) {
return MODEL_ALIASES.get(hyphenated);
}
return hyphenated;
}
function isSafeModelId(value) {
return /^[a-z0-9][a-z0-9._/-]*$/.test(value);
}
const DEFAULT_AI_MODEL = normalizeModelText(RAW_DEFAULT_AI_MODEL) || 'appwizzy-default';
function normalizeAiModel(value, fallback = DEFAULT_AI_MODEL) {
const normalizedFallback = normalizeModelText(fallback) || DEFAULT_AI_MODEL;
const safeFallback = isSafeModelId(normalizedFallback)
? normalizedFallback
: DEFAULT_AI_MODEL;
const normalizedModel = normalizeModelText(value);
if (!normalizedModel) {
return safeFallback;
}
if (isSafeModelId(normalizedModel)) {
return normalizedModel;
}
console.warn(
`Unsafe AI model "${value}" requested. Falling back to "${safeFallback}".`,
);
return safeFallback;
}
module.exports = {
DEFAULT_AI_MODEL,
SUPPORTED_AI_MODELS,
normalizeAiModel,
};

View File

@ -1,7 +1,6 @@
const db = require('../models'); const db = require('../models');
const FileDBApi = require('./file'); const { normalizeAiModel } = require('../../ai/modelNormalizer');
const crypto = require('crypto');
const Utils = require('../utils'); const Utils = require('../utils');
@ -71,7 +70,7 @@ module.exports = class AgentsDBApi {
null null
, ,
model: data.model model: normalizeAiModel(data.model)
|| ||
null null
, ,
@ -143,7 +142,7 @@ module.exports = class AgentsDBApi {
null null
, ,
model: item.model model: normalizeAiModel(item.model)
|| ||
null null
, ,
@ -222,7 +221,7 @@ module.exports = class AgentsDBApi {
if (data.description !== undefined) updatePayload.description = data.description; if (data.description !== undefined) updatePayload.description = data.description;
if (data.model !== undefined) updatePayload.model = data.model; if (data.model !== undefined) updatePayload.model = normalizeAiModel(data.model);
if (data.system_prompt !== undefined) updatePayload.system_prompt = data.system_prompt; if (data.system_prompt !== undefined) updatePayload.system_prompt = data.system_prompt;
@ -384,9 +383,6 @@ module.exports = class AgentsDBApi {
offset = currentPage * limit; offset = currentPage * limit;
const orderBy = null;
const transaction = (options && options.transaction) || undefined;
let include = [ let include = [

View File

@ -0,0 +1,43 @@
const { DEFAULT_AI_MODEL } = require('../../ai/modelNormalizer');
module.exports = {
async up(queryInterface) {
const transaction = await queryInterface.sequelize.transaction();
try {
await queryInterface.sequelize.query(
`
UPDATE "agents"
SET "model" = :defaultModel,
"updatedAt" = NOW()
WHERE "deletedAt" IS NULL
AND (
"model" IS NULL
OR BTRIM("model") = ''
OR LOWER(BTRIM("model")) IN (
'gpt-4.1-mini',
'gpt 4.1 mini',
'gpt_4.1_mini',
'gpt-5.5',
'gpt 5.5',
'gpt_5.5'
)
)
`,
{
replacements: { defaultModel: DEFAULT_AI_MODEL },
transaction,
},
);
await transaction.commit();
} catch (error) {
await transaction.rollback();
throw error;
}
},
async down() {
// Intentionally left as a no-op to avoid overwriting user-selected models on rollback.
},
};

View File

@ -0,0 +1,46 @@
const { DEFAULT_AI_MODEL } = require('../../ai/modelNormalizer');
module.exports = {
async up(queryInterface) {
const transaction = await queryInterface.sequelize.transaction();
try {
await queryInterface.sequelize.query(
`
UPDATE "agents"
SET "model" = :defaultModel,
"updatedAt" = NOW()
WHERE "deletedAt" IS NULL
AND (
"model" IS NULL
OR BTRIM("model") = ''
OR LOWER(BTRIM("model")) IN (
'gpt-5-mini',
'gpt 5 mini',
'gpt_5_mini',
'gpt-4.1-mini',
'gpt 4.1 mini',
'gpt_4.1_mini',
'gpt-5.5',
'gpt 5.5',
'gpt_5.5'
)
)
`,
{
replacements: { defaultModel: DEFAULT_AI_MODEL },
transaction,
},
);
await transaction.commit();
} catch (error) {
await transaction.rollback();
throw error;
}
},
async down() {
// Intentionally left as a no-op to avoid overwriting user-selected models on rollback.
},
};

View File

@ -65,7 +65,7 @@ const AgentsData = [
"model": "gpt-4.1-mini", "model": "appwizzy-default",
@ -132,7 +132,7 @@ const AgentsData = [
"model": "gpt-4.1-mini", "model": "appwizzy-default",
@ -199,7 +199,7 @@ const AgentsData = [
"model": "gpt-4.1-mini", "model": "appwizzy-default",
@ -266,7 +266,7 @@ const AgentsData = [
"model": "gpt-4.1-mini", "model": "appwizzy-default",
@ -333,7 +333,7 @@ const AgentsData = [
"model": "gpt-4.1-mini", "model": "appwizzy-default",
@ -1581,7 +1581,7 @@ const UsageEventsData = [
"model": "gpt-4.1-mini", "model": "appwizzy-default",
@ -1676,7 +1676,7 @@ const UsageEventsData = [
"model": "gpt-4.1-mini", "model": "appwizzy-default",
@ -1771,7 +1771,7 @@ const UsageEventsData = [
"model": "gpt-4.1-mini", "model": "appwizzy-default",
@ -1866,7 +1866,7 @@ const UsageEventsData = [
"model": "gpt-4.1-mini", "model": "appwizzy-default",
@ -1961,7 +1961,7 @@ const UsageEventsData = [
"model": "gpt-4.1-mini", "model": "appwizzy-default",

View File

@ -4,6 +4,7 @@ const path = require('path');
const config = require('../config'); const config = require('../config');
const db = require('../db/models'); const db = require('../db/models');
const { LocalAIApi } = require('../ai/LocalAIApi'); const { LocalAIApi } = require('../ai/LocalAIApi');
const { DEFAULT_AI_MODEL, normalizeAiModel } = require('../ai/modelNormalizer');
const AttachmentsDBApi = require('../db/api/attachments'); const AttachmentsDBApi = require('../db/api/attachments');
const ValidationError = require('./notifications/errors/validation'); const ValidationError = require('./notifications/errors/validation');
@ -16,7 +17,7 @@ const MAX_CONTEXT_MESSAGES = 12;
const MAX_ATTACHMENTS = 5; const MAX_ATTACHMENTS = 5;
const MAX_ATTACHMENT_TEXT_LENGTH = 12000; const MAX_ATTACHMENT_TEXT_LENGTH = 12000;
const MAX_INLINE_IMAGE_BYTES = 2 * 1024 * 1024; const MAX_INLINE_IMAGE_BYTES = 2 * 1024 * 1024;
const IMAGE_INPUT_MODEL = 'gpt-5.2'; const IMAGE_INPUT_MODEL = process.env.AI_IMAGE_MODEL || DEFAULT_AI_MODEL;
function normalizeText(value) { function normalizeText(value) {
if (typeof value !== 'string') { if (typeof value !== 'string') {
@ -270,6 +271,22 @@ function estimateTokens(content) {
return Math.max(1, Math.ceil(normalized.length / 4)); return Math.max(1, Math.ceil(normalized.length / 4));
} }
function buildAiProviderError(response, model) {
const reason = normalizeText(response?.error || response?.message || response?.data?.error)
|| 'AI proxy request failed.';
const modelLabel = model ? ` for model "${model}"` : '';
if (/status\s+(400|403)|unknown model/i.test(reason)) {
return [
`AI provider request failed${modelLabel}: ${reason}.`,
'Check the configured AI provider credentials and model access, or switch the agent to a model supported by this project.',
].join(' ');
}
return `AI provider request failed${modelLabel}: ${reason}`;
}
function buildAssistantFailureMessage(errorMessage) { function buildAssistantFailureMessage(errorMessage) {
const normalized = cleanMarkdownPreview(errorMessage).slice(0, 400) || 'Unknown AI error.'; const normalized = cleanMarkdownPreview(errorMessage).slice(0, 400) || 'Unknown AI error.';
@ -382,9 +399,9 @@ async function requestAssistantReply(conversationId, assistantMessageId, agent)
}; };
if (hasImageInput) { if (hasImageInput) {
payload.model = IMAGE_INPUT_MODEL; payload.model = normalizeAiModel(IMAGE_INPUT_MODEL);
} else if (agent?.model) { } else {
payload.model = agent.model; payload.model = normalizeAiModel(agent?.model, DEFAULT_AI_MODEL);
} }
if (agent?.temperature !== undefined && agent?.temperature !== null && agent.temperature !== '') { if (agent?.temperature !== undefined && agent?.temperature !== null && agent.temperature !== '') {
@ -401,7 +418,11 @@ async function requestAssistantReply(conversationId, assistantMessageId, agent)
}); });
if (!response.success) { if (!response.success) {
throw new Error(response.error || response.message || 'AI proxy request failed.'); console.error('[workspace] AI proxy request failed.', {
payload,
response,
});
throw new Error(buildAiProviderError(response, payload.model));
} }
const text = normalizeText(LocalAIApi.extractText(response)); const text = normalizeText(LocalAIApi.extractText(response));
@ -1099,7 +1120,6 @@ module.exports = class WorkspaceService {
const createTransaction = await db.sequelize.transaction(); const createTransaction = await db.sequelize.transaction();
let conversationId = null; let conversationId = null;
let assistantMessageId = null; let assistantMessageId = null;
let userMessageId = null;
let agentId = null; let agentId = null;
let agentModel = null; let agentModel = null;
let titleSeed = content || buildAttachmentTitleSeed(attachments); let titleSeed = content || buildAttachmentTitleSeed(attachments);
@ -1237,7 +1257,6 @@ module.exports = class WorkspaceService {
conversationId = conversation.id; conversationId = conversation.id;
assistantMessageId = assistantMessage.id; assistantMessageId = assistantMessage.id;
userMessageId = userMessage.id;
agentId = agent?.id || null; agentId = agent?.id || null;
agentModel = agent?.model || 'default-ai-model'; agentModel = agent?.model || 'default-ai-model';
} catch (error) { } catch (error) {

View File

@ -91,7 +91,7 @@ export default function AgentFormSections({ setFieldValue, values }: Props) {
className={inputClassName} className={inputClassName}
id="model" id="model"
name="model" name="model"
placeholder="gpt-5.5" placeholder="appwizzy-default"
/> />
</div> </div>
<div className="md:col-span-2"> <div className="md:col-span-2">

View File

@ -22,7 +22,7 @@ const initialValues = {
is_default: false, is_default: false,
max_output_tokens: '', max_output_tokens: '',
metadata_json: '', metadata_json: '',
model: '', model: 'appwizzy-default',
name: '', name: '',
system_prompt: '', system_prompt: '',
temperature: '', temperature: '',