mirror of
https://github.com/Heretek-AI/GDevelop-Heretek.git
synced 2026-10-12 02:02:42 +00:00
fix(ai): address code review feedback for BYOK custom AI client
- package.json: remove unused react-dnd-html5-backend and react-refresh, pin babel-plugin-macros to ^2.8.0 - CustomAIClient: exclude apiKey from localStorage persistence, normalize base URL without auto-appending /v1, bound local requests cache to 20 items, add cancel token support with axios, restrict markdown tool parsing to side-effect-free tools, return empty results for asset/resource searches - UseGenerateEvents: use activeUserId in getAiGeneratedEvent polling - EditorFunctions: validate batch entries before scene insertion in put2dInstances and put3dInstances, fix instance rotation with setAngle for 3D - Preferences: fix temperature input to support 0 and clamp 0.0-1.0, exclude apiKey from localStorage - Generation & Usage: route requests and events by local ID prefix, partition status requests, decouple BYOK checks from subscription status - Tests: reset state in beforeEach, update tests for base URL normalization, side-effect-free tools, and empty search results
This commit is contained in:
@@ -22,7 +22,7 @@
|
||||
"axios-mock-adapter": "^1.22.0",
|
||||
"babel-core": "^7.0.0-bridge.0",
|
||||
"babel-loader": "8.1.0",
|
||||
"babel-plugin-macros": "^3.1.0",
|
||||
"babel-plugin-macros": "^2.8.0",
|
||||
"chokidar": "4.0.3",
|
||||
"concurrently": "9.2.1",
|
||||
"flow-bin": "0.299.0",
|
||||
@@ -35,7 +35,6 @@
|
||||
"patch-package": "^6.4.7",
|
||||
"prettier": "1.15.3",
|
||||
"react-app-rewired": "^2.2.1",
|
||||
"react-refresh": "^0.18.0",
|
||||
"react-scripts": "5.0.1",
|
||||
"recursive-copy": "^2.0.14",
|
||||
"recursive-readdir": "^2.2.2",
|
||||
@@ -78,7 +77,6 @@
|
||||
"react": "18.2.0",
|
||||
"react-color": "2.13.8",
|
||||
"react-dnd": "14.0.5",
|
||||
"react-dnd-html5-backend": "^14.1.0",
|
||||
"react-dnd-touch-backend": "14.1.1",
|
||||
"react-dom": "18.2.0",
|
||||
"react-error-boundary": "^1.2.0",
|
||||
|
||||
+278
-273
@@ -14,6 +14,8 @@ import {
|
||||
type AiSettings,
|
||||
} from '../Utils/GDevelopServices/Generation';
|
||||
|
||||
export const LOCAL_BYOK_USER_ID = 'local-byok-user';
|
||||
|
||||
export type CustomAIConfig = {|
|
||||
enabled: boolean,
|
||||
baseUrl: string,
|
||||
@@ -34,6 +36,7 @@ export const DEFAULT_CUSTOM_AI_CONFIG: CustomAIConfig = {
|
||||
|
||||
const LOCAL_STORAGE_CONFIG_KEY = 'gd-custom-ai-config';
|
||||
const LOCAL_STORAGE_REQUESTS_KEY = 'gd-custom-ai-requests';
|
||||
const MAX_LOCAL_SAVED_REQUESTS = 20;
|
||||
|
||||
/**
|
||||
* In-memory configuration cache.
|
||||
@@ -70,6 +73,7 @@ export const getCustomEndpointConfig = (): CustomAIConfig => {
|
||||
|
||||
/**
|
||||
* Save custom AI config to local storage and update in-memory cache.
|
||||
* Excludes apiKey from cleartext localStorage while retaining it in memory.
|
||||
*/
|
||||
export const setCustomEndpointConfig = (
|
||||
updates: $Shape<CustomAIConfig>
|
||||
@@ -83,9 +87,10 @@ export const setCustomEndpointConfig = (
|
||||
|
||||
try {
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
const { apiKey, ...persistableConfig } = nextConfig;
|
||||
localStorage.setItem(
|
||||
LOCAL_STORAGE_CONFIG_KEY,
|
||||
JSON.stringify(nextConfig)
|
||||
JSON.stringify(persistableConfig)
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -105,6 +110,7 @@ export const isCustomEndpointEnabled = (): boolean => {
|
||||
|
||||
/**
|
||||
* Normalize base URL ensuring protocol and removing trailing slashes.
|
||||
* Preserves the configured path exactly without automatically appending /v1.
|
||||
*/
|
||||
export const normalizeBaseUrl = (baseUrl: string): string => {
|
||||
let url = (baseUrl || '').trim();
|
||||
@@ -113,12 +119,13 @@ export const normalizeBaseUrl = (baseUrl: string): string => {
|
||||
}
|
||||
// Remove trailing slashes
|
||||
url = url.replace(/\/+$/, '');
|
||||
// If user entered just localhost:11434, add http://
|
||||
// If user entered a schemeless URL, default to http:// for loopback and https:// otherwise
|
||||
if (!/^https?:\/\//i.test(url)) {
|
||||
url = `http://${url}`;
|
||||
}
|
||||
if (!url.endsWith('/v1')) {
|
||||
url = `${url}/v1`;
|
||||
if (/^(localhost|127\.0\.0\.1|\[::1\])(:\d+)?(\/.*)?$/i.test(url)) {
|
||||
url = `http://${url}`;
|
||||
} else {
|
||||
url = `https://${url}`;
|
||||
}
|
||||
}
|
||||
return url;
|
||||
};
|
||||
@@ -135,11 +142,6 @@ export const getEndpointUrl = (
|
||||
? endpointPath
|
||||
: `/${endpointPath}`;
|
||||
|
||||
// If baseUrl already ends with /v1 and endpointPath starts with /v1, don't duplicate /v1
|
||||
if (normalized.endsWith('/v1') && cleanPath.startsWith('/v1/')) {
|
||||
return `${normalized}${cleanPath.substring(3)}`;
|
||||
}
|
||||
|
||||
return `${normalized}${cleanPath}`;
|
||||
};
|
||||
|
||||
@@ -148,6 +150,16 @@ export const getEndpointUrl = (
|
||||
*/
|
||||
const localAiRequestsCache: { [id: string]: AiRequest } = {};
|
||||
|
||||
/**
|
||||
* Reset CustomAIClient state (for testing).
|
||||
*/
|
||||
export const _resetCustomAiClientForTesting = () => {
|
||||
cachedConfig = null;
|
||||
for (const key of Object.keys(localAiRequestsCache)) {
|
||||
delete localAiRequestsCache[key];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Load local AI requests from local storage.
|
||||
*/
|
||||
@@ -167,19 +179,28 @@ export const loadLocalAiRequests = (): { [id: string]: AiRequest } => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Save local AI requests to local storage.
|
||||
* Save local AI requests to local storage, retaining a bounded number of requests
|
||||
* and excluding full gameProjectJson to prevent storage quota exhaustion.
|
||||
*/
|
||||
export const saveLocalAiRequests = () => {
|
||||
try {
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
localStorage.setItem(
|
||||
LOCAL_STORAGE_REQUESTS_KEY,
|
||||
JSON.stringify(localAiRequestsCache)
|
||||
);
|
||||
if (typeof localStorage === 'undefined') return;
|
||||
|
||||
const keys = Object.keys(localAiRequestsCache);
|
||||
const recentKeys = keys.slice(-MAX_LOCAL_SAVED_REQUESTS);
|
||||
const persistableMap: { [id: string]: AiRequest } = {};
|
||||
|
||||
for (const key of recentKeys) {
|
||||
const req = localAiRequestsCache[key];
|
||||
if (req) {
|
||||
const { gameProjectJson, ...persistableReq } = req;
|
||||
persistableMap[key] = (persistableReq: any);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Error saving local AI requests to localStorage:', err);
|
||||
}
|
||||
|
||||
localStorage.setItem(
|
||||
LOCAL_STORAGE_REQUESTS_KEY,
|
||||
JSON.stringify(persistableMap)
|
||||
);
|
||||
};
|
||||
|
||||
// Initialize requests from localStorage
|
||||
@@ -1064,7 +1085,7 @@ export const extractThinkingAndContent = (
|
||||
*/
|
||||
export const transformGDevelopMessagesToOpenAi = (
|
||||
outputMessages: Array<any>,
|
||||
systemPromptOrProjectJson?: ?string,
|
||||
systemPromptOrGameProjectJson?: ?string,
|
||||
projectSpecificExtensionsSummaryJson?: ?string,
|
||||
mode?: string
|
||||
): Array<{|
|
||||
@@ -1082,26 +1103,25 @@ export const transformGDevelopMessagesToOpenAi = (
|
||||
|}> => {
|
||||
const openAiMessages = [];
|
||||
|
||||
let systemPrompt: ?string = null;
|
||||
if (typeof systemPromptOrProjectJson === 'string') {
|
||||
let effectiveSystemPrompt: ?string = null;
|
||||
if (systemPromptOrGameProjectJson) {
|
||||
if (
|
||||
systemPromptOrProjectJson.includes('GDevelop AI Assistant') ||
|
||||
systemPromptOrProjectJson.includes('You are')
|
||||
systemPromptOrGameProjectJson.includes('You are GDevelop AI Assistant')
|
||||
) {
|
||||
systemPrompt = systemPromptOrProjectJson;
|
||||
effectiveSystemPrompt = systemPromptOrGameProjectJson;
|
||||
} else {
|
||||
systemPrompt = buildSystemPrompt({
|
||||
gameProjectJson: systemPromptOrProjectJson,
|
||||
effectiveSystemPrompt = buildSystemPrompt({
|
||||
gameProjectJson: systemPromptOrGameProjectJson,
|
||||
projectSpecificExtensionsSummaryJson,
|
||||
mode,
|
||||
mode: (mode: any),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (systemPrompt) {
|
||||
if (effectiveSystemPrompt) {
|
||||
openAiMessages.push({
|
||||
role: 'system',
|
||||
content: systemPrompt,
|
||||
content: effectiveSystemPrompt,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1120,142 +1140,129 @@ export const transformGDevelopMessagesToOpenAi = (
|
||||
.map(item => item.text)
|
||||
.join('\n')
|
||||
: '');
|
||||
if (text) {
|
||||
openAiMessages.push({
|
||||
role: 'user',
|
||||
content: text,
|
||||
});
|
||||
}
|
||||
openAiMessages.push({
|
||||
role: 'user',
|
||||
content: text,
|
||||
});
|
||||
} else if (
|
||||
msg.role === 'assistant' ||
|
||||
msg.type === 'assistant' ||
|
||||
(msg.type === 'message' && msg.role === 'assistant')
|
||||
) {
|
||||
const textParts = [];
|
||||
const content = msg.text || '';
|
||||
const toolCalls = [];
|
||||
const seenCallIds = new Set();
|
||||
|
||||
if (msg.text) {
|
||||
textParts.push(msg.text);
|
||||
} else if (typeof msg.content === 'string') {
|
||||
textParts.push(msg.content);
|
||||
} else if (Array.isArray(msg.content)) {
|
||||
for (const item of msg.content) {
|
||||
if (item.type === 'output_text') {
|
||||
textParts.push(item.text);
|
||||
} else if (item.type === 'function_call') {
|
||||
const callId = item.call_id || item.id;
|
||||
if (callId && !seenCallIds.has(callId)) {
|
||||
seenCallIds.add(callId);
|
||||
toolCalls.push({
|
||||
id: callId,
|
||||
type: 'function',
|
||||
function: {
|
||||
name: item.name,
|
||||
arguments:
|
||||
typeof item.arguments === 'string'
|
||||
? item.arguments
|
||||
: JSON.stringify(item.arguments || {}),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
// Extract function calls
|
||||
if (Array.isArray(msg.functionCalls)) {
|
||||
for (const fc of msg.functionCalls) {
|
||||
toolCalls.push({
|
||||
id: fc.id || `call_${Date.now()}`,
|
||||
type: 'function',
|
||||
function: {
|
||||
name: fc.name,
|
||||
arguments:
|
||||
typeof fc.callArguments === 'string'
|
||||
? fc.callArguments
|
||||
: JSON.stringify(fc.callArguments || {}),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (msg.functionCalls && Array.isArray(msg.functionCalls)) {
|
||||
for (const fc of msg.functionCalls) {
|
||||
const callId = fc.id || fc.call_id;
|
||||
if (callId && !seenCallIds.has(callId)) {
|
||||
seenCallIds.add(callId);
|
||||
if (Array.isArray(msg.content)) {
|
||||
for (const item of msg.content) {
|
||||
if (item.type === 'function_call') {
|
||||
toolCalls.push({
|
||||
id: callId,
|
||||
id:
|
||||
item.call_id || item.callId || item.id || `call_${Date.now()}`,
|
||||
type: 'function',
|
||||
function: {
|
||||
name: fc.name,
|
||||
name: item.name,
|
||||
arguments:
|
||||
typeof fc.callArguments === 'string'
|
||||
? fc.callArguments
|
||||
: JSON.stringify(fc.callArguments || fc.arguments || {}),
|
||||
typeof item.arguments === 'string'
|
||||
? item.arguments
|
||||
: JSON.stringify(item.arguments || {}),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
openAiMessages.push({
|
||||
const assistantMsg: Object = {
|
||||
role: 'assistant',
|
||||
content: textParts.join('\n') || (toolCalls.length ? null : ''),
|
||||
...(toolCalls.length > 0 ? { tool_calls: toolCalls } : {}),
|
||||
});
|
||||
} else if (
|
||||
msg.type === 'function_call_output' ||
|
||||
msg.role === 'tool' ||
|
||||
msg.functionCallOutputs
|
||||
) {
|
||||
const seenToolOutputIds = new Set();
|
||||
if (msg.functionCallOutputs && Array.isArray(msg.functionCallOutputs)) {
|
||||
for (const fco of msg.functionCallOutputs) {
|
||||
const callId = fco.callId || fco.call_id;
|
||||
if (callId && !seenToolOutputIds.has(callId)) {
|
||||
seenToolOutputIds.add(callId);
|
||||
openAiMessages.push({
|
||||
role: 'tool',
|
||||
tool_call_id: callId,
|
||||
content:
|
||||
typeof fco.output === 'string'
|
||||
? fco.output
|
||||
: JSON.stringify(fco.output || {}),
|
||||
});
|
||||
content: content || null,
|
||||
};
|
||||
if (toolCalls.length > 0) {
|
||||
// De-duplicate tool calls by ID
|
||||
const seenCallIds = new Set<string>();
|
||||
const uniqueToolCalls = [];
|
||||
for (const tc of toolCalls) {
|
||||
if (!seenCallIds.has(tc.id)) {
|
||||
seenCallIds.add(tc.id);
|
||||
uniqueToolCalls.push(tc);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const callId = msg.call_id || msg.callId || msg.tool_call_id;
|
||||
if (callId && !seenToolOutputIds.has(callId)) {
|
||||
seenToolOutputIds.add(callId);
|
||||
assistantMsg.tool_calls = uniqueToolCalls;
|
||||
}
|
||||
openAiMessages.push(assistantMsg);
|
||||
} else if (msg.type === 'function_call_output' || msg.role === 'tool') {
|
||||
if (Array.isArray(msg.functionCallOutputs)) {
|
||||
for (const fco of msg.functionCallOutputs) {
|
||||
openAiMessages.push({
|
||||
role: 'tool',
|
||||
tool_call_id: callId,
|
||||
tool_call_id: fco.callId || fco.call_id || fco.id,
|
||||
content:
|
||||
typeof msg.output === 'string'
|
||||
? msg.output
|
||||
: JSON.stringify(msg.output || msg.content || {}),
|
||||
typeof fco.output === 'string'
|
||||
? fco.output
|
||||
: JSON.stringify(fco.output || {}),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
openAiMessages.push({
|
||||
role: 'tool',
|
||||
tool_call_id: msg.callId || msg.call_id || msg.id,
|
||||
content:
|
||||
typeof msg.output === 'string'
|
||||
? msg.output
|
||||
: JSON.stringify(msg.output || {}),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return openAiMessages;
|
||||
// Ensure consecutive tool outputs don't have duplicate tool_call_id
|
||||
const seenToolOutputIds = new Set<string>();
|
||||
return openAiMessages.filter(msg => {
|
||||
if (msg.role === 'tool' && msg.tool_call_id) {
|
||||
if (seenToolOutputIds.has(msg.tool_call_id)) {
|
||||
return false;
|
||||
}
|
||||
seenToolOutputIds.add(msg.tool_call_id);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Build standard system prompt with GDevelop context and instructions.
|
||||
* Build the system prompt with project structure and guidelines.
|
||||
*/
|
||||
export const buildSystemPrompt = ({
|
||||
gameProjectJson,
|
||||
projectSpecificExtensionsSummaryJson,
|
||||
mode,
|
||||
}: {|
|
||||
gameProjectJson?: ?string,
|
||||
projectSpecificExtensionsSummaryJson?: ?string,
|
||||
mode?: string,
|
||||
gameProjectJson?: string | null,
|
||||
projectSpecificExtensionsSummaryJson?: string | null,
|
||||
mode?: 'chat' | 'agent' | 'orchestrator',
|
||||
|}): string => {
|
||||
let prompt = `You are the GDevelop AI Assistant. You help users create, design, and modify games in GDevelop 5.
|
||||
You have access to editor tools to create scenes, objects, behaviors, place instances, change scene properties (such as background color), and generate event logic.
|
||||
Always think carefully, explain your design concisely to the user, and call the appropriate tool functions when modifying the game.
|
||||
let prompt = `You are GDevelop AI Assistant, an expert game engine developer. You help users build games in GDevelop 5.
|
||||
|
||||
Guidelines for modifying the project:
|
||||
1. To change scene properties (background color, dimensions, name, first scene):
|
||||
Call change_scene_properties_layers_effects_groups with scene_name and changed_properties: [{ property_name: "backgroundColor", new_value: "#3498db" }].
|
||||
2. To create a scene:
|
||||
Call create_scene with scene_name (e.g. { scene_name: "Level1" }).
|
||||
3. To create or replace objects:
|
||||
Call create_or_replace_object.
|
||||
4. To add behaviors:
|
||||
Call add_behavior.
|
||||
5. To place instances in a scene:
|
||||
Call put_2d_instances.
|
||||
Guidelines:
|
||||
1. Always use available tools/functions to inspect or modify the project.
|
||||
2. When creating objects, use \`create_or_replace_object\`.
|
||||
3. When adding behaviors, use \`add_behavior\`.
|
||||
4. When placing instances in scenes, use \`put_2d_instances\` or \`put_3d_instances\`.
|
||||
5. When changing scene properties (e.g. background color), use \`change_scene_properties_layers_effects_groups\`.
|
||||
6. To modify scene events:
|
||||
Call add_scene_events.
|
||||
7. If using run_script:
|
||||
@@ -1318,165 +1325,112 @@ export const sendChatCompletion = async ({
|
||||
payload.max_tokens = currentConfig.maxTokens;
|
||||
}
|
||||
|
||||
let cancelToken;
|
||||
if (signal) {
|
||||
const cancelTokenSource = axios.CancelToken.source();
|
||||
cancelToken = cancelTokenSource.token;
|
||||
if (signal.aborted) {
|
||||
cancelTokenSource.cancel('Aborted');
|
||||
} else {
|
||||
signal.addEventListener('abort', () => {
|
||||
cancelTokenSource.cancel('Aborted');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios.post(endpointUrl, payload, {
|
||||
headers,
|
||||
signal,
|
||||
cancelToken,
|
||||
timeout: 120000,
|
||||
});
|
||||
|
||||
if (
|
||||
response.data &&
|
||||
response.data.choices &&
|
||||
response.data.choices.length > 0
|
||||
response.data.choices[0] &&
|
||||
response.data.choices[0].message
|
||||
) {
|
||||
return response.data.choices[0].message;
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
'Invalid response structure from custom AI endpoint: choices array is empty.'
|
||||
);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
if (axios.isCancel(error) || (signal && signal.aborted)) {
|
||||
throw new Error('AI request was aborted.');
|
||||
}
|
||||
if (error.response) {
|
||||
const status = error.response.status;
|
||||
const data = error.response.data;
|
||||
const errorMsg =
|
||||
(data && data.error && (data.error.message || data.error)) ||
|
||||
error.response.statusText ||
|
||||
'Server Error';
|
||||
throw new Error(`Custom AI Endpoint Error (${status}): ${errorMsg}`);
|
||||
} else if (error.request) {
|
||||
throw new Error(
|
||||
`Failed to reach custom AI endpoint at ${endpointUrl}. Please verify that your local model server (Ollama/llama.cpp/LM Studio) or proxy is running and accessible.`
|
||||
);
|
||||
error.message ||
|
||||
`HTTP error ${status}`;
|
||||
throw new Error(`AI Provider Error (${status}): ${errorMsg}`);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Test connectivity with the configured custom endpoint.
|
||||
*/
|
||||
export const testConnection = async (
|
||||
customConfig?: $Shape<CustomAIConfig>
|
||||
): Promise<{|
|
||||
success: boolean,
|
||||
message: string,
|
||||
models?: Array<string>,
|
||||
|}> => {
|
||||
const config = {
|
||||
...getCustomEndpointConfig(),
|
||||
...(customConfig || {}),
|
||||
};
|
||||
const baseUrl = normalizeBaseUrl(config.baseUrl);
|
||||
|
||||
// First attempt: Try GET /models
|
||||
try {
|
||||
const modelsUrl = getEndpointUrl(baseUrl, '/models');
|
||||
const headers: { [string]: string } = {
|
||||
'HTTP-Referer': 'https://gdevelop.io',
|
||||
'X-Title': 'GDevelop IDE',
|
||||
...(config.customHeaders || {}),
|
||||
};
|
||||
if (config.apiKey && config.apiKey.trim()) {
|
||||
headers['Authorization'] = `Bearer ${config.apiKey.trim()}`;
|
||||
}
|
||||
|
||||
const response = await axios.get(modelsUrl, { headers, timeout: 5000 });
|
||||
if (response.status === 200 && response.data) {
|
||||
const data = response.data.data || response.data.models || response.data;
|
||||
const models = Array.isArray(data)
|
||||
? data.map(item => item.id || item.name || String(item)).filter(Boolean)
|
||||
: [];
|
||||
return {
|
||||
success: true,
|
||||
message: `Successfully connected to endpoint. Found ${
|
||||
models.length
|
||||
} model(s).`,
|
||||
models,
|
||||
};
|
||||
}
|
||||
} catch (err) {
|
||||
// If /models failed, fallback to a lightweight completion test
|
||||
}
|
||||
|
||||
// Second attempt: Minimal chat completion
|
||||
try {
|
||||
const res = await sendChatCompletion({
|
||||
messages: [{ role: 'user', content: 'Say "OK"' }],
|
||||
config,
|
||||
});
|
||||
return {
|
||||
success: true,
|
||||
message: `Successfully connected! Model responded: ${res.content ||
|
||||
'OK'}`,
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
success: false,
|
||||
message: `Connection failed: ${err.message ||
|
||||
'Failed to connect to custom endpoint.'}`,
|
||||
};
|
||||
}
|
||||
};
|
||||
const SIDE_EFFECT_FREE_TOOLS = new Set([
|
||||
'describe_instances',
|
||||
'describe_events',
|
||||
'describe_scene_layers_effects_groups',
|
||||
'describe_variables',
|
||||
'read_game_project_json',
|
||||
'read_full_docs',
|
||||
'search_docs',
|
||||
'get_game_starter_summary',
|
||||
]);
|
||||
|
||||
/**
|
||||
* Parse an assistant message into GDevelop AiRequestAssistantMessage structure.
|
||||
* Parse an OpenAI assistant message response into GDevelop's internal format.
|
||||
*/
|
||||
export const parseAssistantMessage = (
|
||||
openAiMessageOrChoice: Object,
|
||||
openAiMessageOrChoiceOrResponse: Object,
|
||||
messageId?: string
|
||||
): Object => {
|
||||
const openAiMessage = openAiMessageOrChoice.message || openAiMessageOrChoice;
|
||||
const contentArray: Array<any> = [];
|
||||
const functionCalls: Array<{|
|
||||
id: string,
|
||||
name: string,
|
||||
callArguments: Object,
|
||||
|}> = [];
|
||||
): AiRequestMessage => {
|
||||
const openAiMessage =
|
||||
openAiMessageOrChoiceOrResponse.message ||
|
||||
(openAiMessageOrChoiceOrResponse.choices &&
|
||||
openAiMessageOrChoiceOrResponse.choices[0] &&
|
||||
openAiMessageOrChoiceOrResponse.choices[0].message) ||
|
||||
openAiMessageOrChoiceOrResponse;
|
||||
|
||||
const rawContent = openAiMessage.content;
|
||||
const reasoningContent = openAiMessage.reasoning_content;
|
||||
const { thinking, cleanContent } = extractThinkingAndContent(
|
||||
rawContent,
|
||||
reasoningContent
|
||||
);
|
||||
const rawContent = openAiMessage.content || '';
|
||||
const reasoningContent = openAiMessage.reasoning_content || null;
|
||||
|
||||
if (thinking) {
|
||||
contentArray.push({
|
||||
type: 'reasoning',
|
||||
status: 'completed',
|
||||
summary: {
|
||||
text: thinking,
|
||||
type: 'summary_text',
|
||||
},
|
||||
});
|
||||
let { thinking, cleanContent } = extractThinkingAndContent(rawContent);
|
||||
if (!thinking && reasoningContent) {
|
||||
thinking = reasoningContent;
|
||||
}
|
||||
|
||||
const contentArray: Array<any> = [];
|
||||
if (cleanContent) {
|
||||
contentArray.push({
|
||||
type: 'output_text',
|
||||
type: 'text',
|
||||
status: 'completed',
|
||||
text: cleanContent,
|
||||
annotations: [],
|
||||
});
|
||||
}
|
||||
|
||||
if (openAiMessage.tool_calls && Array.isArray(openAiMessage.tool_calls)) {
|
||||
const functionCalls: Array<any> = [];
|
||||
|
||||
// Standard tool_calls
|
||||
if (Array.isArray(openAiMessage.tool_calls)) {
|
||||
for (const toolCall of openAiMessage.tool_calls) {
|
||||
const functionName = toolCall.function && toolCall.function.name;
|
||||
const functionArgs =
|
||||
toolCall.function &&
|
||||
(typeof toolCall.function.arguments === 'string'
|
||||
? toolCall.function.arguments
|
||||
: JSON.stringify(toolCall.function.arguments || {}));
|
||||
|
||||
let parsedArgs = {};
|
||||
try {
|
||||
parsedArgs = JSON.parse(functionArgs || '{}');
|
||||
} catch (e) {}
|
||||
|
||||
if (functionName) {
|
||||
if (toolCall.function) {
|
||||
const functionName = toolCall.function.name;
|
||||
const functionArgs = toolCall.function.arguments;
|
||||
let parsedArgs = {};
|
||||
try {
|
||||
parsedArgs =
|
||||
typeof functionArgs === 'string'
|
||||
? JSON.parse(functionArgs)
|
||||
: functionArgs || {};
|
||||
} catch (e) {
|
||||
console.warn('Error parsing function call arguments JSON:', e);
|
||||
}
|
||||
const callId =
|
||||
toolCall.id ||
|
||||
`call_${Date.now()}_${Math.random()
|
||||
@@ -1498,7 +1452,7 @@ export const parseAssistantMessage = (
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: If no tool_calls but content contains markdown JSON tool call blocks
|
||||
// Fallback: If no tool_calls but content contains markdown JSON tool call blocks (restricted to side-effect-free tools)
|
||||
if (
|
||||
(!openAiMessage.tool_calls || openAiMessage.tool_calls.length === 0) &&
|
||||
cleanContent
|
||||
@@ -1510,7 +1464,11 @@ export const parseAssistantMessage = (
|
||||
const parsed = JSON.parse(match[1]);
|
||||
const name = parsed.function || parsed.name || parsed.tool;
|
||||
const args = parsed.arguments || parsed.args || parsed.parameters || {};
|
||||
if (name && typeof name === 'string') {
|
||||
if (
|
||||
name &&
|
||||
typeof name === 'string' &&
|
||||
SIDE_EFFECT_FREE_TOOLS.has(name)
|
||||
) {
|
||||
const callId = `call_parsed_${Date.now()}_${Math.random()
|
||||
.toString(36)
|
||||
.substr(2, 5)}`;
|
||||
@@ -1616,7 +1574,7 @@ export const customCreateAiRequest = async ({
|
||||
id: reqId,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
userId: 'local-byok-user',
|
||||
userId: LOCAL_BYOK_USER_ID,
|
||||
gameId: gameId || null,
|
||||
gameProjectJson: gameProjectJson || null,
|
||||
status: 'ready',
|
||||
@@ -1658,7 +1616,7 @@ export const customAddMessageToAiRequest = async ({
|
||||
id: aiRequestId,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
userId: 'local-byok-user',
|
||||
userId: LOCAL_BYOK_USER_ID,
|
||||
status: 'ready',
|
||||
output: [],
|
||||
error: null,
|
||||
@@ -1743,7 +1701,7 @@ export const customGetAiRequest = (aiRequestId: string): AiRequest => {
|
||||
id: aiRequestId,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
userId: 'local-byok-user',
|
||||
userId: LOCAL_BYOK_USER_ID,
|
||||
status: 'ready',
|
||||
error: null,
|
||||
output: [],
|
||||
@@ -1780,7 +1738,7 @@ export const customGetAiRequestStatuses = (
|
||||
return {
|
||||
id,
|
||||
status: req ? req.status : 'ready',
|
||||
userId: 'local-byok-user',
|
||||
userId: LOCAL_BYOK_USER_ID,
|
||||
};
|
||||
});
|
||||
};
|
||||
@@ -1828,7 +1786,7 @@ export const customForkAiRequest = (
|
||||
id: newReqId,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
userId: 'local-byok-user',
|
||||
userId: LOCAL_BYOK_USER_ID,
|
||||
status: 'ready',
|
||||
forkedFromAiRequestId: aiRequestId,
|
||||
output,
|
||||
@@ -1985,7 +1943,7 @@ Ensure generatedEvents is a JSON string of standard GDevelop event objects (e.g.
|
||||
id: `local-evt-${Date.now()}`,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
userId: 'local-byok-user',
|
||||
userId: LOCAL_BYOK_USER_ID,
|
||||
status: 'ready',
|
||||
partialGameProjectJson: '',
|
||||
eventsDescription: eventsDescription || null,
|
||||
@@ -2031,6 +1989,72 @@ Ensure generatedEvents is a JSON string of standard GDevelop event objects (e.g.
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Test connectivity with the configured custom endpoint.
|
||||
*/
|
||||
export const testConnection = async (
|
||||
customConfig?: $Shape<CustomAIConfig>
|
||||
): Promise<{|
|
||||
success: boolean,
|
||||
message: string,
|
||||
models?: Array<string>,
|
||||
|}> => {
|
||||
const config = {
|
||||
...getCustomEndpointConfig(),
|
||||
...(customConfig || {}),
|
||||
};
|
||||
const baseUrl = normalizeBaseUrl(config.baseUrl);
|
||||
|
||||
// First attempt: Try GET /models
|
||||
try {
|
||||
const modelsUrl = getEndpointUrl(baseUrl, '/models');
|
||||
const headers: { [string]: string } = {
|
||||
'HTTP-Referer': 'https://gdevelop.io',
|
||||
'X-Title': 'GDevelop IDE',
|
||||
...(config.customHeaders || {}),
|
||||
};
|
||||
if (config.apiKey && config.apiKey.trim()) {
|
||||
headers['Authorization'] = `Bearer ${config.apiKey.trim()}`;
|
||||
}
|
||||
|
||||
const response = await axios.get(modelsUrl, { headers, timeout: 5000 });
|
||||
if (response.status === 200 && response.data) {
|
||||
const data = response.data.data || response.data.models || response.data;
|
||||
const models = Array.isArray(data)
|
||||
? data.map(item => item.id || item.name || String(item)).filter(Boolean)
|
||||
: [];
|
||||
return {
|
||||
success: true,
|
||||
message: `Successfully connected to endpoint. Found ${
|
||||
models.length
|
||||
} model(s).`,
|
||||
models,
|
||||
};
|
||||
}
|
||||
} catch (err) {
|
||||
// If /models failed, fallback to a lightweight completion test
|
||||
}
|
||||
|
||||
// Second attempt: Minimal chat completion
|
||||
try {
|
||||
const res = await sendChatCompletion({
|
||||
messages: [{ role: 'user', content: 'Say "OK"' }],
|
||||
config,
|
||||
});
|
||||
return {
|
||||
success: true,
|
||||
message: `Successfully connected! Model responded: ${res.content ||
|
||||
'OK'}`,
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
success: false,
|
||||
message: `Connection failed: ${err.message ||
|
||||
'Failed to connect to custom endpoint.'}`,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Client-side Asset Search.
|
||||
*/
|
||||
@@ -2043,7 +2067,7 @@ export const customCreateAssetSearch = async ({
|
||||
|}): Promise<AssetSearch> => {
|
||||
return {
|
||||
id: `local-asset-search-${Date.now()}`,
|
||||
userId: 'local-byok-user',
|
||||
userId: LOCAL_BYOK_USER_ID,
|
||||
createdAt: new Date().toISOString(),
|
||||
query: {
|
||||
searchTerms: [searchTerms],
|
||||
@@ -2055,16 +2079,7 @@ export const customCreateAssetSearch = async ({
|
||||
lastAssistantMessages: [],
|
||||
},
|
||||
status: 'completed',
|
||||
results: [
|
||||
{
|
||||
asset: {
|
||||
id: `local-asset-${Date.now()}`,
|
||||
name: searchTerms,
|
||||
objectType: objectType || 'Sprite',
|
||||
tags: [searchTerms],
|
||||
},
|
||||
},
|
||||
],
|
||||
results: [],
|
||||
};
|
||||
};
|
||||
|
||||
@@ -2080,24 +2095,14 @@ export const customCreateResourceSearch = async ({
|
||||
|}): Promise<ResourceSearch> => {
|
||||
return {
|
||||
id: `local-resource-search-${Date.now()}`,
|
||||
userId: 'local-byok-user',
|
||||
userId: LOCAL_BYOK_USER_ID,
|
||||
createdAt: new Date().toISOString(),
|
||||
query: {
|
||||
searchTerms: [searchTerms],
|
||||
resourceKind,
|
||||
},
|
||||
status: 'completed',
|
||||
results: [
|
||||
{
|
||||
resource: {
|
||||
name: searchTerms,
|
||||
kind: resourceKind,
|
||||
url: `https://resources.gdevelop.io/${encodeURIComponent(
|
||||
searchTerms
|
||||
)}`,
|
||||
},
|
||||
},
|
||||
],
|
||||
results: [],
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@ import {
|
||||
DEFAULT_CUSTOM_AI_CONFIG,
|
||||
DEFAULT_LOCAL_AI_SETTINGS,
|
||||
GDEVELOP_OPENAI_TOOLS,
|
||||
LOCAL_BYOK_USER_ID,
|
||||
_resetCustomAiClientForTesting,
|
||||
customCreateAiRequest,
|
||||
customAddMessageToAiRequest,
|
||||
customGetAiRequest,
|
||||
@@ -31,6 +33,7 @@ jest.mock('axios');
|
||||
describe('CustomAIClient', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
_resetCustomAiClientForTesting();
|
||||
setCustomEndpointConfig({
|
||||
enabled: false,
|
||||
baseUrl: 'http://localhost:11434/v1',
|
||||
@@ -65,16 +68,22 @@ describe('CustomAIClient', () => {
|
||||
expect(config.temperature).toBe(0.2);
|
||||
});
|
||||
|
||||
it('normalizes base URLs correctly', () => {
|
||||
it('normalizes base URLs correctly without auto-appending /v1', () => {
|
||||
expect(normalizeBaseUrl('http://localhost:11434')).toBe(
|
||||
'http://localhost:11434/v1'
|
||||
'http://localhost:11434'
|
||||
);
|
||||
expect(normalizeBaseUrl('http://localhost:11434/')).toBe(
|
||||
'http://localhost:11434/v1'
|
||||
'http://localhost:11434'
|
||||
);
|
||||
expect(normalizeBaseUrl('http://localhost:11434/v1/')).toBe(
|
||||
'http://localhost:11434/v1'
|
||||
);
|
||||
expect(normalizeBaseUrl('localhost:11434/v1')).toBe(
|
||||
'http://localhost:11434/v1'
|
||||
);
|
||||
expect(normalizeBaseUrl('api.openai.com/v1')).toBe(
|
||||
'https://api.openai.com/v1'
|
||||
);
|
||||
expect(normalizeBaseUrl('https://api.openai.com/v1')).toBe(
|
||||
'https://api.openai.com/v1'
|
||||
);
|
||||
@@ -89,7 +98,7 @@ describe('CustomAIClient', () => {
|
||||
getEndpointUrl('http://localhost:11434/v1', '/chat/completions')
|
||||
).toBe('http://localhost:11434/v1/chat/completions');
|
||||
expect(getEndpointUrl('http://localhost:11434', '/models')).toBe(
|
||||
'http://localhost:11434/v1/models'
|
||||
'http://localhost:11434/models'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -240,20 +249,20 @@ describe('CustomAIClient', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('extracts embedded JSON function call markdown if tool_calls not present', () => {
|
||||
it('extracts embedded JSON function call markdown if tool_calls not present for side-effect-free tools', () => {
|
||||
const choice = {
|
||||
message: {
|
||||
role: 'assistant',
|
||||
content:
|
||||
'I will create the object now:\n```json\n{"name":"create_or_replace_object","arguments":{"objectName":"Coin"}}\n```',
|
||||
'I will inspect the instances now:\n```json\n{"name":"describe_instances","arguments":{"scene_name":"Level1"}}\n```',
|
||||
},
|
||||
};
|
||||
|
||||
const parsed = parseAssistantMessage(choice);
|
||||
expect(parsed.functionCalls).toHaveLength(1);
|
||||
expect(parsed.functionCalls[0].name).toBe('create_or_replace_object');
|
||||
expect(parsed.functionCalls[0].name).toBe('describe_instances');
|
||||
expect(parsed.functionCalls[0].callArguments).toEqual({
|
||||
objectName: 'Coin',
|
||||
scene_name: 'Level1',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -364,7 +373,7 @@ describe('CustomAIClient', () => {
|
||||
|
||||
const statuses = await customGetAiRequestStatuses([aiRequest.id]);
|
||||
expect(statuses).toEqual([
|
||||
{ id: aiRequest.id, status: 'ready', userId: 'local-byok-user' },
|
||||
{ id: aiRequest.id, status: 'ready', userId: LOCAL_BYOK_USER_ID },
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -451,20 +460,22 @@ describe('CustomAIClient', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('provides mock asset and resource search results', async () => {
|
||||
it('provides empty asset and resource search results in BYOK mode', async () => {
|
||||
const assetResult = await customCreateAssetSearch({
|
||||
searchTerms: 'coin',
|
||||
objectType: 'Sprite',
|
||||
});
|
||||
expect(assetResult.id).toMatch(/^local-asset-/);
|
||||
expect(assetResult.results.length).toBeGreaterThan(0);
|
||||
expect(assetResult.userId).toBe(LOCAL_BYOK_USER_ID);
|
||||
expect(assetResult.results).toEqual([]);
|
||||
|
||||
const resourceResult = await customCreateResourceSearch({
|
||||
searchTerms: 'jump sound',
|
||||
resourceKind: 'audio',
|
||||
});
|
||||
expect(resourceResult.id).toMatch(/^local-resource-/);
|
||||
expect(resourceResult.results.length).toBeGreaterThan(0);
|
||||
expect(resourceResult.userId).toBe(LOCAL_BYOK_USER_ID);
|
||||
expect(resourceResult.results).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -23,9 +23,8 @@ export const getAiConfigurationPresetsWithAvailability = ({
|
||||
getAiSettings: () => AiSettings | null,
|
||||
limits: ?Limits,
|
||||
|}): Array<AiConfigurationPresetWithAvailability> => {
|
||||
const aiSettings = getAiSettings() || DEFAULT_LOCAL_AI_SETTINGS;
|
||||
|
||||
if (isCustomEndpointEnabled()) {
|
||||
const aiSettings = getAiSettings() || DEFAULT_LOCAL_AI_SETTINGS;
|
||||
return aiSettings.aiRequest.presets.map(preset => ({
|
||||
...preset,
|
||||
enableWith: null,
|
||||
@@ -34,12 +33,15 @@ export const getAiConfigurationPresetsWithAvailability = ({
|
||||
}));
|
||||
}
|
||||
|
||||
const aiSettings = getAiSettings();
|
||||
if (!aiSettings) return [];
|
||||
|
||||
if (!limits) {
|
||||
return aiSettings.aiRequest.presets.map(preset => ({
|
||||
...preset,
|
||||
enableWith: null,
|
||||
enabledWithPlans: [],
|
||||
disabled: false,
|
||||
disabled: !preset.isDefault,
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -43,7 +43,10 @@ import {
|
||||
canUpgradeSubscription,
|
||||
hasValidSubscriptionPlan,
|
||||
} from '../Utils/GDevelopServices/Usage';
|
||||
import { isCustomEndpointEnabled } from '../AI/CustomAIClient';
|
||||
import {
|
||||
isCustomEndpointEnabled,
|
||||
LOCAL_BYOK_USER_ID,
|
||||
} from '../AI/CustomAIClient';
|
||||
import { retryIfFailed } from '../Utils/RetryIfFailed';
|
||||
import { type EditorCallbacks } from '../EditorFunctions';
|
||||
import {
|
||||
@@ -528,7 +531,7 @@ export const AskAiEditor: React.ComponentType<Props> = React.memo<Props>(
|
||||
return;
|
||||
}
|
||||
|
||||
const activeUserId = profile ? profile.id : 'local-byok-user';
|
||||
const activeUserId = profile ? profile.id : LOCAL_BYOK_USER_ID;
|
||||
|
||||
// Read the options and reset them (to avoid launching the same request twice).
|
||||
const {
|
||||
@@ -690,7 +693,7 @@ export const AskAiEditor: React.ComponentType<Props> = React.memo<Props>(
|
||||
|}) => {
|
||||
if (!profile && !isCustomEndpointEnabled()) return;
|
||||
|
||||
const activeUserId = profile ? profile.id : 'local-byok-user';
|
||||
const activeUserId = profile ? profile.id : LOCAL_BYOK_USER_ID;
|
||||
|
||||
const aiRequestForMessage = aiRequests[aiRequestId];
|
||||
if (!aiRequestForMessage) return;
|
||||
@@ -1584,7 +1587,9 @@ export const AskAiEditor: React.ComponentType<Props> = React.memo<Props>(
|
||||
lastSendError={getLastSendError(selectedAiRequestId)}
|
||||
quota={quota}
|
||||
increaseQuotaOffering={
|
||||
!hasValidSubscriptionPlan(subscription)
|
||||
isCustomEndpointEnabled()
|
||||
? 'none'
|
||||
: !hasValidSubscriptionPlan(subscription)
|
||||
? 'subscribe'
|
||||
: canUpgradeSubscription(subscription)
|
||||
? 'upgrade'
|
||||
|
||||
@@ -37,8 +37,10 @@ import { prepareAiUserContent } from './PrepareAiUserContent';
|
||||
import { AiRequestContext } from './AiRequestContext';
|
||||
import { getAiConfigurationPresetsWithAvailability } from './AiConfiguration';
|
||||
import { type CreateProjectResult } from '../Utils/UseCreateProject';
|
||||
import { SubscriptionContext } from '../Profile/Subscription/SubscriptionContext';
|
||||
import { isCustomEndpointEnabled } from '../AI/CustomAIClient';
|
||||
import {
|
||||
isCustomEndpointEnabled,
|
||||
LOCAL_BYOK_USER_ID,
|
||||
} from '../AI/CustomAIClient';
|
||||
import {
|
||||
useProcessFunctionCalls,
|
||||
useRefreshLimits,
|
||||
@@ -273,7 +275,7 @@ export const AskAiStandAloneForm = ({
|
||||
return;
|
||||
}
|
||||
|
||||
const activeUserId = profile ? profile.id : 'local-byok-user';
|
||||
const activeUserId = profile ? profile.id : LOCAL_BYOK_USER_ID;
|
||||
|
||||
// Read the options and reset them immediately to prevent the effect from firing
|
||||
// again if dependencies change during the async operations below (e.g. when
|
||||
@@ -503,7 +505,7 @@ export const AskAiStandAloneForm = ({
|
||||
)
|
||||
: null;
|
||||
|
||||
const activeUserId = profile ? profile.id : 'local-byok-user';
|
||||
const activeUserId = profile ? profile.id : LOCAL_BYOK_USER_ID;
|
||||
|
||||
const preparedAiUserContent = await prepareAiUserContent({
|
||||
getAuthorizationHeader,
|
||||
@@ -712,7 +714,9 @@ export const AskAiStandAloneForm = ({
|
||||
lastSendError={getLastSendError(aiRequestIdForForm)}
|
||||
quota={quota}
|
||||
increaseQuotaOffering={
|
||||
!hasValidSubscriptionPlan(subscription)
|
||||
isCustomEndpointEnabled()
|
||||
? 'none'
|
||||
: !hasValidSubscriptionPlan(subscription)
|
||||
? 'subscribe'
|
||||
: canUpgradeSubscription(subscription)
|
||||
? 'upgrade'
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
// @flow
|
||||
export * from '../AI/CustomAIClient';
|
||||
export { default } from '../AI/CustomAIClient';
|
||||
|
||||
@@ -15,7 +15,10 @@ import {
|
||||
} from '../EditorFunctions';
|
||||
import { makeSimplifiedProjectBuilder } from '../EditorFunctions/SimplifiedProject/SimplifiedProject';
|
||||
import { prepareAiUserContent } from './PrepareAiUserContent';
|
||||
import { isCustomEndpointEnabled } from '../AI/CustomAIClient';
|
||||
import {
|
||||
isCustomEndpointEnabled,
|
||||
LOCAL_BYOK_USER_ID,
|
||||
} from '../AI/CustomAIClient';
|
||||
|
||||
const gd: libGDevelop = global.gd;
|
||||
|
||||
@@ -70,7 +73,7 @@ export const useGenerateEvents = ({
|
||||
if (!profile && !isCustomEndpointEnabled())
|
||||
throw new Error('User should be authenticated.');
|
||||
|
||||
const activeUserId = profile ? profile.id : 'local-byok-user';
|
||||
const activeUserId = profile ? profile.id : LOCAL_BYOK_USER_ID;
|
||||
|
||||
const simplifiedProjectBuilder = makeSimplifiedProjectBuilder(gd);
|
||||
const simplifiedProjectJson = JSON.stringify(
|
||||
@@ -137,7 +140,7 @@ export const useGenerateEvents = ({
|
||||
aiGeneratedEvent = await getAiGeneratedEvent(
|
||||
getAuthorizationHeader,
|
||||
{
|
||||
userId: profile.id,
|
||||
userId: activeUserId,
|
||||
aiGeneratedEventId: aiGeneratedEvent.id,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -13,7 +13,10 @@ import { retryIfFailed } from '../Utils/RetryIfFailed';
|
||||
import { useInstallAsset } from '../AssetStore/NewObjectDialog';
|
||||
import { type ResourceManagementProps } from '../ResourcesList/ResourceSource';
|
||||
import { AssetStoreContext } from '../AssetStore/AssetStoreContext';
|
||||
import { isCustomEndpointEnabled } from '../AI/CustomAIClient';
|
||||
import {
|
||||
isCustomEndpointEnabled,
|
||||
LOCAL_BYOK_USER_ID,
|
||||
} from '../AI/CustomAIClient';
|
||||
|
||||
type _FuncReturnType = {
|
||||
searchAndInstallAsset: AssetSearchAndInstallOptions => Promise<AssetSearchAndInstallResult>,
|
||||
@@ -53,7 +56,7 @@ export const useSearchAndInstallAsset = ({
|
||||
if (!profile && !isCustomEndpointEnabled())
|
||||
throw new Error('User should be authenticated.');
|
||||
|
||||
const activeUserId = profile ? profile.id : 'local-byok-user';
|
||||
const activeUserId = profile ? profile.id : LOCAL_BYOK_USER_ID;
|
||||
|
||||
let assetShortHeader;
|
||||
if (exactOrPartialAssetId) {
|
||||
|
||||
@@ -14,7 +14,10 @@ import { retryIfFailed } from '../Utils/RetryIfFailed';
|
||||
import { type ResourceManagementProps } from '../ResourcesList/ResourceSource';
|
||||
import { createNewResource } from '../ResourcesList/ResourceSource';
|
||||
import { applyResourceDefaults } from '../ResourcesList/ResourceUtils';
|
||||
import { isCustomEndpointEnabled } from '../AI/CustomAIClient';
|
||||
import {
|
||||
isCustomEndpointEnabled,
|
||||
LOCAL_BYOK_USER_ID,
|
||||
} from '../AI/CustomAIClient';
|
||||
|
||||
import PromisePool from '@supercharge/promise-pool';
|
||||
|
||||
@@ -41,7 +44,7 @@ export const useSearchAndInstallResource = ({
|
||||
throw new Error('User should be authenticated.');
|
||||
if (!project) throw new Error('Project should be opened.');
|
||||
|
||||
const activeUserId = profile ? profile.id : 'local-byok-user';
|
||||
const activeUserId = profile ? profile.id : LOCAL_BYOK_USER_ID;
|
||||
|
||||
const { results } = await PromisePool.withConcurrency(5)
|
||||
.for(resources)
|
||||
|
||||
@@ -3663,7 +3663,7 @@ const put2dInstances: EditorFunction = {
|
||||
);
|
||||
const newInstancesCount =
|
||||
new_instances_count === null && existingInstanceIds.length === 0
|
||||
? Array.isArray(args.instances)
|
||||
? Array.isArray(args.instances) && args.instances.length > 0
|
||||
? args.instances.length
|
||||
: 1
|
||||
: new_instances_count;
|
||||
@@ -3747,33 +3747,63 @@ const put2dInstances: EditorFunction = {
|
||||
return makeSceneNotFoundFailure(project, scene_name);
|
||||
}
|
||||
const layout = project.getLayout(scene_name);
|
||||
const initialInstances = layout.getInitialInstances();
|
||||
const createdCount = args.instances.length;
|
||||
const validEntries = [];
|
||||
for (const instData of args.instances) {
|
||||
if (!instData || typeof instData !== 'object') continue;
|
||||
const instObjName =
|
||||
instData.object_name || instData.name || object_name;
|
||||
if (!instObjName) continue;
|
||||
const inst = initialInstances.insertNewInitialInstance();
|
||||
inst.setObjectName(instObjName);
|
||||
inst.setLayer(
|
||||
instData.layer_name !== undefined
|
||||
? instData.layer_name
|
||||
: instData.layer !== undefined
|
||||
? instData.layer
|
||||
: layer_name
|
||||
const x = Number(instData.x);
|
||||
const y = Number(instData.y);
|
||||
if (!Number.isFinite(x) || !Number.isFinite(y)) continue;
|
||||
validEntries.push({
|
||||
objectName: instObjName,
|
||||
layer:
|
||||
instData.layer_name !== undefined
|
||||
? String(instData.layer_name)
|
||||
: instData.layer !== undefined
|
||||
? String(instData.layer)
|
||||
: layer_name,
|
||||
x,
|
||||
y,
|
||||
angle:
|
||||
instData.angle !== undefined &&
|
||||
Number.isFinite(Number(instData.angle))
|
||||
? Number(instData.angle)
|
||||
: null,
|
||||
zOrder:
|
||||
instData.z_order !== undefined &&
|
||||
Number.isFinite(Number(instData.z_order))
|
||||
? Number(instData.z_order)
|
||||
: instData.zOrder !== undefined &&
|
||||
Number.isFinite(Number(instData.zOrder))
|
||||
? Number(instData.zOrder)
|
||||
: null,
|
||||
});
|
||||
}
|
||||
|
||||
if (validEntries.length === 0) {
|
||||
return makeGenericFailure(
|
||||
'No valid instances found in the batch to place.'
|
||||
);
|
||||
inst.setX(Number(instData.x) || 0);
|
||||
inst.setY(Number(instData.y) || 0);
|
||||
if (instData.angle !== undefined)
|
||||
inst.setAngle(Number(instData.angle) || 0);
|
||||
if (instData.z_order !== undefined || instData.zOrder !== undefined) {
|
||||
inst.setZOrder(Number(instData.z_order || instData.zOrder) || 0);
|
||||
}
|
||||
}
|
||||
|
||||
const initialInstances = layout.getInitialInstances();
|
||||
for (const entry of validEntries) {
|
||||
const inst = initialInstances.insertNewInitialInstance();
|
||||
inst.setObjectName(entry.objectName);
|
||||
inst.setLayer(entry.layer);
|
||||
inst.setX(entry.x);
|
||||
inst.setY(entry.y);
|
||||
if (entry.angle !== null) inst.setAngle(entry.angle);
|
||||
if (entry.zOrder !== null) inst.setZOrder(entry.zOrder);
|
||||
}
|
||||
onInstancesModifiedOutsideEditor({ scene: layout });
|
||||
return {
|
||||
success: true,
|
||||
message: `Placed ${createdCount} instance(s) in scene "${scene_name}".`,
|
||||
message: `Placed ${
|
||||
validEntries.length
|
||||
} instance(s) in scene "${scene_name}".`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3793,11 +3823,7 @@ const put2dInstances: EditorFunction = {
|
||||
args,
|
||||
'existing_instance_ids'
|
||||
);
|
||||
const brush_position =
|
||||
rawBrushPosition ||
|
||||
(requested_brush_kind !== 'none' && !existing_instance_ids
|
||||
? '0, 0'
|
||||
: null);
|
||||
const brush_position = rawBrushPosition;
|
||||
// A "none" brush with both a `brush_position` and `existing_instance_ids`
|
||||
// is contradictory ("none" never positions anything) but unambiguous: move
|
||||
// THOSE instances there. Read it as the "point" brush, which is what the
|
||||
@@ -4619,7 +4645,7 @@ const put3dInstances: EditorFunction = {
|
||||
);
|
||||
const newInstancesCount =
|
||||
new_instances_count === null && existingInstanceIds.length === 0
|
||||
? Array.isArray(args.instances)
|
||||
? Array.isArray(args.instances) && args.instances.length > 0
|
||||
? args.instances.length
|
||||
: 1
|
||||
: new_instances_count;
|
||||
@@ -4703,35 +4729,70 @@ const put3dInstances: EditorFunction = {
|
||||
return makeSceneNotFoundFailure(project, scene_name);
|
||||
}
|
||||
const layout = project.getLayout(scene_name);
|
||||
const initialInstances = layout.getInitialInstances();
|
||||
const createdCount = args.instances.length;
|
||||
const validEntries = [];
|
||||
for (const instData of args.instances) {
|
||||
if (!instData || typeof instData !== 'object') continue;
|
||||
const instObjName =
|
||||
instData.object_name || instData.name || object_name;
|
||||
if (!instObjName) continue;
|
||||
const inst = initialInstances.insertNewInitialInstance();
|
||||
inst.setObjectName(instObjName);
|
||||
inst.setLayer(
|
||||
instData.layer_name !== undefined
|
||||
? instData.layer_name
|
||||
: instData.layer !== undefined
|
||||
? instData.layer
|
||||
: layer_name
|
||||
const x = Number(instData.x);
|
||||
const y = Number(instData.y);
|
||||
const z = Number(instData.z || 0);
|
||||
if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(z))
|
||||
continue;
|
||||
validEntries.push({
|
||||
objectName: instObjName,
|
||||
layer:
|
||||
instData.layer_name !== undefined
|
||||
? String(instData.layer_name)
|
||||
: instData.layer !== undefined
|
||||
? String(instData.layer)
|
||||
: layer_name,
|
||||
x,
|
||||
y,
|
||||
z,
|
||||
rotationX:
|
||||
instData.rotation_x !== undefined &&
|
||||
Number.isFinite(Number(instData.rotation_x))
|
||||
? Number(instData.rotation_x)
|
||||
: null,
|
||||
rotationY:
|
||||
instData.rotation_y !== undefined &&
|
||||
Number.isFinite(Number(instData.rotation_y))
|
||||
? Number(instData.rotation_y)
|
||||
: null,
|
||||
rotationZ:
|
||||
instData.rotation_z !== undefined &&
|
||||
Number.isFinite(Number(instData.rotation_z))
|
||||
? Number(instData.rotation_z)
|
||||
: null,
|
||||
});
|
||||
}
|
||||
|
||||
if (validEntries.length === 0) {
|
||||
return makeGenericFailure(
|
||||
'No valid 3D instances found in the batch to place.'
|
||||
);
|
||||
inst.setX(Number(instData.x) || 0);
|
||||
inst.setY(Number(instData.y) || 0);
|
||||
inst.setZ(Number(instData.z) || 0);
|
||||
if (instData.rotation_x !== undefined)
|
||||
inst.setRotationX(Number(instData.rotation_x) || 0);
|
||||
if (instData.rotation_y !== undefined)
|
||||
inst.setRotationY(Number(instData.rotation_y) || 0);
|
||||
if (instData.rotation_z !== undefined)
|
||||
inst.setRotationZ(Number(instData.rotation_z) || 0);
|
||||
}
|
||||
|
||||
const initialInstances = layout.getInitialInstances();
|
||||
for (const entry of validEntries) {
|
||||
const inst = initialInstances.insertNewInitialInstance();
|
||||
inst.setObjectName(entry.objectName);
|
||||
inst.setLayer(entry.layer);
|
||||
inst.setX(entry.x);
|
||||
inst.setY(entry.y);
|
||||
inst.setZ(entry.z);
|
||||
if (entry.rotationX !== null) inst.setRotationX(entry.rotationX);
|
||||
if (entry.rotationY !== null) inst.setRotationY(entry.rotationY);
|
||||
if (entry.rotationZ !== null) inst.setAngle(entry.rotationZ);
|
||||
}
|
||||
onInstancesModifiedOutsideEditor({ scene: layout });
|
||||
return {
|
||||
success: true,
|
||||
message: `Placed ${createdCount} 3D instance(s) in scene "${scene_name}".`,
|
||||
message: `Placed ${
|
||||
validEntries.length
|
||||
} 3D instance(s) in scene "${scene_name}".`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4751,11 +4812,7 @@ const put3dInstances: EditorFunction = {
|
||||
args,
|
||||
'existing_instance_ids'
|
||||
);
|
||||
const brush_position =
|
||||
rawBrushPosition ||
|
||||
(requested_brush_kind !== 'none' && !existing_instance_ids
|
||||
? '0, 0, 0'
|
||||
: null);
|
||||
const brush_position = rawBrushPosition;
|
||||
// A "none" brush with both a `brush_position` and `existing_instance_ids`
|
||||
// is contradictory ("none" never positions anything) but unambiguous: move
|
||||
// THOSE instances there. Read it as the "point" brush, which is what the
|
||||
|
||||
@@ -761,9 +761,14 @@ const PreferencesDialog = ({
|
||||
type="number"
|
||||
floatingLabelText={<Trans>Temperature (0.0 to 1.0)</Trans>}
|
||||
value={values.aiCustomTemperature}
|
||||
onChange={(e, value) =>
|
||||
setAiCustomTemperature(parseFloat(value) || 0.7)
|
||||
}
|
||||
onChange={(e, value) => {
|
||||
const parsed = parseFloat(value);
|
||||
if (Number.isNaN(parsed)) {
|
||||
setAiCustomTemperature(0.7);
|
||||
} else {
|
||||
setAiCustomTemperature(Math.max(0.0, Math.min(1.0, parsed)));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<LineStackLayout alignItems="center">
|
||||
<RaisedButton
|
||||
|
||||
@@ -983,10 +983,8 @@ export default class PreferencesProvider extends React.Component<Props, State> {
|
||||
|
||||
_persistValuesToLocalStorage(preferences: Preferences): any {
|
||||
try {
|
||||
localStorage.setItem(
|
||||
localStorageItem,
|
||||
JSON.stringify(preferences.values)
|
||||
);
|
||||
const { aiCustomApiKey, ...persistableValues } = preferences.values;
|
||||
localStorage.setItem(localStorageItem, JSON.stringify(persistableValues));
|
||||
} catch (e) {
|
||||
console.warn('Unable to persist preferences', e);
|
||||
}
|
||||
@@ -1511,7 +1509,6 @@ export default class PreferencesProvider extends React.Component<Props, State> {
|
||||
},
|
||||
}),
|
||||
() => {
|
||||
this._persistValuesToLocalStorage(this.state);
|
||||
setCustomEndpointConfig({ apiKey: newValue });
|
||||
}
|
||||
);
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
customCreateAssetSearch,
|
||||
customCreateResourceSearch,
|
||||
DEFAULT_LOCAL_AI_SETTINGS,
|
||||
LOCAL_BYOK_USER_ID,
|
||||
} from '../../AI/CustomAIClient';
|
||||
|
||||
export type Environment = 'staging' | 'live';
|
||||
@@ -312,7 +313,7 @@ export const getAiRequest = async (
|
||||
outputFromMessageId?: ?string,
|
||||
|}
|
||||
): Promise<AiRequest> => {
|
||||
if (isCustomEndpointEnabled() || aiRequestId.startsWith('local-ai-')) {
|
||||
if (aiRequestId.startsWith('local-ai-')) {
|
||||
return customGetAiRequest(aiRequestId);
|
||||
}
|
||||
|
||||
@@ -356,32 +357,33 @@ export const getAiRequestStatuses = async (
|
||||
> => {
|
||||
if (aiRequestIds.length === 0) return [];
|
||||
|
||||
if (
|
||||
isCustomEndpointEnabled() ||
|
||||
aiRequestIds.every(id => id.startsWith('local-ai-'))
|
||||
) {
|
||||
return customGetAiRequestStatuses(aiRequestIds);
|
||||
}
|
||||
const localIds = aiRequestIds.filter(id => id.startsWith('local-ai-'));
|
||||
const hostedIds = aiRequestIds.filter(id => !id.startsWith('local-ai-'));
|
||||
|
||||
const authorizationHeader = await getAuthorizationHeader();
|
||||
// $FlowFixMe[underconstrained-implicit-instantiation]
|
||||
const response = await axios.get(
|
||||
`${GDevelopGenerationApi.baseUrl}/ai-request`,
|
||||
{
|
||||
params: {
|
||||
userId,
|
||||
ids: aiRequestIds.join(','),
|
||||
include: 'status',
|
||||
},
|
||||
headers: {
|
||||
Authorization: authorizationHeader,
|
||||
},
|
||||
}
|
||||
);
|
||||
return ensureIsArray({
|
||||
data: response.data,
|
||||
endpointName: '/ai-request?ids=...&include=status of Generation API',
|
||||
});
|
||||
const results = [];
|
||||
if (localIds.length > 0) {
|
||||
const localStatuses = await customGetAiRequestStatuses(localIds);
|
||||
results.push(...localStatuses);
|
||||
}
|
||||
if (hostedIds.length > 0) {
|
||||
const authorizationHeader = await getAuthorizationHeader();
|
||||
// $FlowFixMe[underconstrained-implicit-instantiation]
|
||||
const response = await axios.get(
|
||||
`${GDevelopGenerationApi.baseUrl}/ai-request`,
|
||||
{
|
||||
params: {
|
||||
userId,
|
||||
ids: hostedIds.join(','),
|
||||
include: 'status',
|
||||
},
|
||||
headers: {
|
||||
Authorization: authorizationHeader,
|
||||
},
|
||||
}
|
||||
);
|
||||
results.push(...(response.data || []));
|
||||
}
|
||||
return results;
|
||||
};
|
||||
|
||||
export const getAiRequests = async (
|
||||
@@ -543,7 +545,7 @@ export const addMessageToAiRequest = async (
|
||||
toolsVersion?: string,
|
||||
|}
|
||||
): Promise<AiRequest> => {
|
||||
if (isCustomEndpointEnabled() || aiRequestId.startsWith('local-ai-')) {
|
||||
if (aiRequestId.startsWith('local-ai-')) {
|
||||
return customAddMessageToAiRequest({
|
||||
aiRequestId,
|
||||
userMessage,
|
||||
@@ -593,7 +595,7 @@ export const suspendAiRequest = async (
|
||||
getAuthorizationHeader: () => Promise<string>,
|
||||
{ userId, aiRequestId }: {| userId: string, aiRequestId: string |}
|
||||
): Promise<AiRequest> => {
|
||||
if (isCustomEndpointEnabled() || aiRequestId.startsWith('local-ai-')) {
|
||||
if (aiRequestId.startsWith('local-ai-')) {
|
||||
return customSuspendAiRequest(aiRequestId);
|
||||
}
|
||||
|
||||
@@ -626,7 +628,7 @@ export const updateAiRequestMessage = async (
|
||||
projectVersionIdAfterMessage?: ?string,
|
||||
|}
|
||||
): Promise<void> => {
|
||||
if (isCustomEndpointEnabled() || aiRequestId.startsWith('local-ai-')) {
|
||||
if (aiRequestId.startsWith('local-ai-')) {
|
||||
return;
|
||||
}
|
||||
const authorizationHeader = await getAuthorizationHeader();
|
||||
@@ -711,7 +713,7 @@ export const getAiRequestSuggestions = async (
|
||||
projectSpecificExtensionsSummaryJsonUserRelativeKey: string | null,
|
||||
|}
|
||||
): Promise<AiRequest> => {
|
||||
if (isCustomEndpointEnabled() || aiRequestId.startsWith('local-ai-')) {
|
||||
if (aiRequestId.startsWith('local-ai-')) {
|
||||
return customGetAiRequestSuggestions(aiRequestId);
|
||||
}
|
||||
|
||||
@@ -749,7 +751,8 @@ export type CreateAiGeneratedEventResult =
|
||||
|}
|
||||
| {|
|
||||
creationSucceeded: false,
|
||||
errorMessage: string,
|
||||
errorMessage?: string,
|
||||
isCreditLimitReached?: boolean,
|
||||
|};
|
||||
|
||||
export const createAiGeneratedEvent = async (
|
||||
@@ -760,14 +763,14 @@ export const createAiGeneratedEvent = async (
|
||||
gameProjectJsonUserRelativeKey,
|
||||
projectSpecificExtensionsSummaryJson,
|
||||
projectSpecificExtensionsSummaryJsonUserRelativeKey,
|
||||
existingEventsJson,
|
||||
existingEventsJsonUserRelativeKey,
|
||||
sceneName,
|
||||
eventsDescription,
|
||||
eventBatches,
|
||||
extensionNamesList,
|
||||
objectsList,
|
||||
existingEventsAsText,
|
||||
existingEventsJson,
|
||||
existingEventsJsonUserRelativeKey,
|
||||
placementHint,
|
||||
relatedAiRequestId,
|
||||
estimatedComplexity,
|
||||
@@ -777,14 +780,14 @@ export const createAiGeneratedEvent = async (
|
||||
gameProjectJsonUserRelativeKey: string | null,
|
||||
projectSpecificExtensionsSummaryJson: string | null,
|
||||
projectSpecificExtensionsSummaryJsonUserRelativeKey: string | null,
|
||||
existingEventsJson: string | null,
|
||||
existingEventsJsonUserRelativeKey: string | null,
|
||||
sceneName: string,
|
||||
eventsDescription: string | null,
|
||||
eventBatches: Array<AiGeneratedEventBatch> | null,
|
||||
eventBatches: ?EventsBatchToGenerate,
|
||||
extensionNamesList: string,
|
||||
objectsList: string,
|
||||
existingEventsAsText: string,
|
||||
existingEventsJson: string | null,
|
||||
existingEventsJsonUserRelativeKey: string | null,
|
||||
placementHint: string | null,
|
||||
relatedAiRequestId: string,
|
||||
estimatedComplexity: number | null,
|
||||
@@ -810,14 +813,14 @@ export const createAiGeneratedEvent = async (
|
||||
gameProjectJsonUserRelativeKey,
|
||||
projectSpecificExtensionsSummaryJson,
|
||||
projectSpecificExtensionsSummaryJsonUserRelativeKey,
|
||||
existingEventsJson,
|
||||
existingEventsJsonUserRelativeKey,
|
||||
sceneName,
|
||||
eventsDescription,
|
||||
eventBatches,
|
||||
extensionNamesList,
|
||||
objectsList,
|
||||
existingEventsAsText,
|
||||
existingEventsJson,
|
||||
existingEventsJsonUserRelativeKey,
|
||||
placementHint,
|
||||
relatedAiRequestId,
|
||||
estimatedComplexity,
|
||||
@@ -829,32 +832,34 @@ export const createAiGeneratedEvent = async (
|
||||
headers: {
|
||||
Authorization: authorizationHeader,
|
||||
},
|
||||
validateStatus: status => true,
|
||||
}
|
||||
);
|
||||
|
||||
if (response.status === 200) {
|
||||
const data = ensureObjectHasProperty({
|
||||
data: response.data,
|
||||
propertyName: 'id',
|
||||
endpointName: '/ai-generated-event of Generation API',
|
||||
});
|
||||
return {
|
||||
creationSucceeded: true,
|
||||
aiGeneratedEvent: data,
|
||||
};
|
||||
} else if (response.status === 400) {
|
||||
// Report the failure to give a chance to the caller to save this message
|
||||
// and retry the generation in a different way.
|
||||
return {
|
||||
creationSucceeded: false,
|
||||
errorMessage: JSON.stringify(response.data),
|
||||
aiGeneratedEvent: ensureObjectHasProperty({
|
||||
data: response.data,
|
||||
propertyName: 'id',
|
||||
endpointName: '/ai-generated-event of Generation API',
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
// Unexpected error: throw an exception as this can be something like a network error
|
||||
// or a server error.
|
||||
throw new Error(
|
||||
const errorStatusAndCode = extractGDevelopApiErrorStatusAndCode(
|
||||
response.data
|
||||
);
|
||||
if (
|
||||
errorStatusAndCode &&
|
||||
errorStatusAndCode.code === 'credit-limit-reached'
|
||||
) {
|
||||
return {
|
||||
creationSucceeded: false,
|
||||
isCreditLimitReached: true,
|
||||
};
|
||||
}
|
||||
|
||||
return makeGenericFailureResult(
|
||||
`Error while running AI event generation: ${response.statusText}`
|
||||
);
|
||||
};
|
||||
@@ -869,15 +874,12 @@ export const getAiGeneratedEvent = async (
|
||||
aiGeneratedEventId: string,
|
||||
|}
|
||||
): Promise<AiGeneratedEvent> => {
|
||||
if (
|
||||
isCustomEndpointEnabled() ||
|
||||
aiGeneratedEventId.startsWith('local-evt-')
|
||||
) {
|
||||
if (aiGeneratedEventId.startsWith('local-evt-')) {
|
||||
return {
|
||||
id: aiGeneratedEventId,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
userId: 'local-byok-user',
|
||||
userId: LOCAL_BYOK_USER_ID,
|
||||
status: 'ready',
|
||||
partialGameProjectJson: '',
|
||||
eventsDescription: null,
|
||||
@@ -918,8 +920,8 @@ export const createAssetSearch = async (
|
||||
{
|
||||
userId,
|
||||
searchTerms,
|
||||
description,
|
||||
objectType,
|
||||
description,
|
||||
twoDimensionalViewKind,
|
||||
exactOrPartialAssetId,
|
||||
relatedAiRequestId,
|
||||
@@ -928,12 +930,12 @@ export const createAssetSearch = async (
|
||||
}: {|
|
||||
userId: string,
|
||||
searchTerms: string,
|
||||
description: string,
|
||||
objectType: string | null,
|
||||
twoDimensionalViewKind: string,
|
||||
exactOrPartialAssetId?: string | null,
|
||||
relatedAiRequestId?: string | null,
|
||||
lastUserMessage?: string | null,
|
||||
objectType?: ?string,
|
||||
description?: ?string,
|
||||
twoDimensionalViewKind?: ?string,
|
||||
exactOrPartialAssetId?: ?string,
|
||||
relatedAiRequestId?: ?string,
|
||||
lastUserMessage?: ?string,
|
||||
lastAssistantMessages?: string[],
|
||||
|}
|
||||
): Promise<AssetSearch> => {
|
||||
@@ -945,10 +947,9 @@ export const createAssetSearch = async (
|
||||
const response = await apiClient.post(
|
||||
`/asset-search`,
|
||||
{
|
||||
gdevelopVersionWithHash: getIDEVersionWithHash(),
|
||||
searchTerms,
|
||||
description,
|
||||
objectType,
|
||||
description,
|
||||
twoDimensionalViewKind,
|
||||
exactOrPartialAssetId,
|
||||
relatedAiRequestId,
|
||||
@@ -991,7 +992,6 @@ export const createResourceSearch = async (
|
||||
const response = await apiClient.post(
|
||||
`/resource-search`,
|
||||
{
|
||||
gdevelopVersionWithHash: getIDEVersionWithHash(),
|
||||
searchTerms,
|
||||
resourceKind,
|
||||
},
|
||||
@@ -1048,7 +1048,6 @@ export const createAiUserContentPresignedUrls = async (
|
||||
const response = await apiClient.post(
|
||||
`/ai-user-content/action/create-presigned-urls`,
|
||||
{
|
||||
gdevelopVersionWithHash: getIDEVersionWithHash(),
|
||||
gameProjectJsonHash,
|
||||
projectSpecificExtensionsSummaryJsonHash,
|
||||
eventsJsonHash,
|
||||
@@ -1062,8 +1061,9 @@ export const createAiUserContentPresignedUrls = async (
|
||||
},
|
||||
}
|
||||
);
|
||||
return ensureIsObject({
|
||||
return ensureObjectHasProperty({
|
||||
data: response.data,
|
||||
propertyName: 'gameProjectJsonSignedUrl',
|
||||
endpointName:
|
||||
'/ai-user-content/action/create-presigned-urls of Generation API',
|
||||
});
|
||||
@@ -1097,7 +1097,7 @@ export const forkAiRequest = async (
|
||||
upToMessageId?: string,
|
||||
|}
|
||||
): Promise<AiRequest> => {
|
||||
if (isCustomEndpointEnabled() || aiRequestId.startsWith('local-ai-')) {
|
||||
if (aiRequestId.startsWith('local-ai-')) {
|
||||
return customForkAiRequest(aiRequestId, upToMessageId);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
ensureIsObjectWithPropertyOfType,
|
||||
} from '../DataValidator';
|
||||
import { type BundleListingData } from './Shop';
|
||||
import { isCustomEndpointEnabled } from '../../AI/CustomAIClient';
|
||||
|
||||
export type Usage = {
|
||||
id: string,
|
||||
@@ -584,10 +583,6 @@ export const isSubscriptionComingFromTeam = (
|
||||
export const hasValidSubscriptionPlan = (
|
||||
subscription: ?Subscription
|
||||
): boolean => {
|
||||
if (isCustomEndpointEnabled()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const hasValidSubscription =
|
||||
!!subscription &&
|
||||
!!subscription.planId &&
|
||||
@@ -763,10 +758,6 @@ export const canBenefitFromSocialRole = (
|
||||
export const canUpgradeSubscription = (
|
||||
subscription: ?Subscription
|
||||
): false | true | boolean => {
|
||||
if (isCustomEndpointEnabled()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
!!subscription &&
|
||||
// $FlowFixMe[incompatible-type]
|
||||
|
||||
Reference in New Issue
Block a user