Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions newIDE/app/scripts/import-libGD.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,9 @@ if (shell.test('-f', path.join(sourceDirectory, 'libGD.js'))) {

if (!branch) {
shell.echo(
`⚠️ Can't find the branch of the associated commit - if you're in detached HEAD, you need to be on a branch instead.`
`⚠️ Can't find the branch of the associated commit - defaulting to master.`
);
return 'unknown-branch';
return 'master';
}

return branch;
Expand Down
195 changes: 195 additions & 0 deletions newIDE/app/src/AiGeneration/AiProviderConfigurations.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
// @flow
import {
type AiProviderConfiguration,
type AiRequestCustomProviderSupport,
} from '../Utils/GDevelopServices/Generation';

export type AiProviderPreset = {|
id: string,
name: string,
baseUrl: string,
model: string,
|};

export const CUSTOM_PROVIDER_SELECTION_ID = 'custom-new';
export const PRESET_SELECTION_PREFIX = 'preset:';
export const CONFIGURATION_SELECTION_PREFIX = 'configuration:';

export const aiProviderPresets: Array<AiProviderPreset> = [
{
id: 'openai',
name: 'OpenAI',
baseUrl: 'https://api.openai.com/v1',
model: 'gpt-5.5',
},
{
id: 'openrouter',
name: 'OpenRouter',
baseUrl: 'https://openrouter.ai/api/v1',
model: 'openai/gpt-5.2',
},
{
id: 'google-gemini',
name: 'Google Gemini',
baseUrl: 'https://generativelanguage.googleapis.com/v1beta/openai/',
model: 'gemini-2.5-flash',
},
{
id: 'groq',
name: 'Groq',
baseUrl: 'https://api.groq.com/openai/v1',
model: 'openai/gpt-oss-20b',
},
{
id: 'mistral',
name: 'Mistral',
baseUrl: 'https://api.mistral.ai/v1',
model: 'codestral-latest',
},
{
id: 'deepseek',
name: 'DeepSeek',
baseUrl: 'https://api.deepseek.com',
model: 'deepseek-v4-pro',
},
{
id: 'xai',
name: 'xAI',
baseUrl: 'https://api.x.ai/v1',
model: 'grok-4.3',
},
];

export const getPresetSelectionId = (presetId: string): string =>
`${PRESET_SELECTION_PREFIX}${presetId}`;

export const getConfigurationSelectionId = (configurationId: string): string =>
`${CONFIGURATION_SELECTION_PREFIX}${configurationId}`;

export const getPresetIdFromSelectionId = (
selectionId: string
): string | null =>
selectionId.indexOf(PRESET_SELECTION_PREFIX) === 0
? selectionId.slice(PRESET_SELECTION_PREFIX.length)
: null;

export const getConfigurationIdFromSelectionId = (
selectionId: string
): string | null =>
selectionId.indexOf(CONFIGURATION_SELECTION_PREFIX) === 0
? selectionId.slice(CONFIGURATION_SELECTION_PREFIX.length)
: null;

export const configurationMatchesPreset = (
configuration: AiProviderConfiguration,
preset: AiProviderPreset
): boolean =>
configuration.name === preset.name &&
configuration.baseUrl === preset.baseUrl;

const getConfigurationUpdatedAtTimestamp = (
configuration: AiProviderConfiguration
): number => {
const updatedAtTimestamp = Date.parse(configuration.updatedAt || '');
return Number.isFinite(updatedAtTimestamp) ? updatedAtTimestamp : 0;
};

export const getPresetConfiguration = (
configurations: Array<AiProviderConfiguration>,
preset: AiProviderPreset
): AiProviderConfiguration | null =>
configurations
.filter(configuration => configurationMatchesPreset(configuration, preset))
.sort(
(configurationA, configurationB) =>
getConfigurationUpdatedAtTimestamp(configurationB) -
getConfigurationUpdatedAtTimestamp(configurationA)
)[0] || null;

export const isPresetConfiguration = (
configuration: AiProviderConfiguration
): boolean =>
aiProviderPresets.some(preset =>
configurationMatchesPreset(configuration, preset)
);

export const getSelectionIdFromAiProviderConfiguration = (
configuration: AiProviderConfiguration
): string => {
const matchingPreset =
aiProviderPresets.find(preset =>
configurationMatchesPreset(configuration, preset)
) || null;
return matchingPreset
? getPresetSelectionId(matchingPreset.id)
: getConfigurationSelectionId(configuration.id);
};

export const shouldFetchAiProviderConfigurations = ({
hasAuthenticatedUser,
customProviderSupport,
}: {|
hasAuthenticatedUser: boolean,
customProviderSupport: AiRequestCustomProviderSupport | null,
|}): boolean =>
hasAuthenticatedUser &&
!!customProviderSupport &&
customProviderSupport.enabled &&
customProviderSupport.openAiCompatible;

export const getAvailableAiProviderConfigurations = ({
aiProviderConfigurations,
customProviderSupport,
}: {|
aiProviderConfigurations: Array<AiProviderConfiguration>,
customProviderSupport: AiRequestCustomProviderSupport | null,
|}): Array<AiProviderConfiguration> => {
if (
!customProviderSupport ||
!customProviderSupport.enabled ||
!customProviderSupport.openAiCompatible
) {
return [];
}

return aiProviderConfigurations.filter(
configuration => configuration.providerType === 'openai-compatible'
);
};

export const getAiProviderConfigurationFromSelectionId = ({
selectionId,
aiProviderConfigurations,
customProviderSupport,
}: {|
selectionId: string | null,
aiProviderConfigurations: Array<AiProviderConfiguration>,
customProviderSupport: AiRequestCustomProviderSupport | null,
|}): AiProviderConfiguration | null => {
if (!selectionId) return null;

const availableAiProviderConfigurations = getAvailableAiProviderConfigurations(
{
aiProviderConfigurations,
customProviderSupport,
}
);

const configurationId = getConfigurationIdFromSelectionId(selectionId);
if (configurationId) {
return (
availableAiProviderConfigurations.find(
configuration => configuration.id === configurationId
) || null
);
}

const presetId = getPresetIdFromSelectionId(selectionId);
if (!presetId) return null;

const preset =
aiProviderPresets.find(preset => preset.id === presetId) || null;
return preset
? getPresetConfiguration(availableAiProviderConfigurations, preset)
: null;
};
163 changes: 163 additions & 0 deletions newIDE/app/src/AiGeneration/AiProviderConfigurations.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
// @flow
import {
CUSTOM_PROVIDER_SELECTION_ID,
getAvailableAiProviderConfigurations,
getAiProviderConfigurationFromSelectionId,
getConfigurationSelectionId,
getPresetSelectionId,
shouldFetchAiProviderConfigurations,
} from './AiProviderConfigurations';
import {
type AiProviderConfiguration,
type AiRequestCustomProviderSupport,
} from '../Utils/GDevelopServices/Generation';

const supportAll: AiRequestCustomProviderSupport = {
enabled: true,
openAiCompatible: true,
};

const supportDisabled: AiRequestCustomProviderSupport = {
enabled: false,
openAiCompatible: true,
};

const supportWithoutOpenAiCompatible: AiRequestCustomProviderSupport = {
enabled: true,
openAiCompatible: false,
};

const openAiCompatibleProvider: AiProviderConfiguration = {
id: 'openai-compatible-provider',
name: 'OpenAI-compatible',
providerType: 'openai-compatible',
baseUrl: 'https://api.openai.com/v1',
model: 'gpt-5.5',
hasApiKey: true,
createdAt: '',
updatedAt: '',
};

const savedOpenAiProvider: AiProviderConfiguration = {
id: 'saved-openai-provider',
name: 'OpenAI',
providerType: 'openai-compatible',
baseUrl: 'https://api.openai.com/v1',
model: 'gpt-5.5',
hasApiKey: true,
createdAt: '2026-05-15T10:00:00.000Z',
updatedAt: '2026-05-15T10:00:00.000Z',
};

const newerSavedOpenAiProvider: AiProviderConfiguration = {
...savedOpenAiProvider,
id: 'newer-saved-openai-provider',
model: 'gpt-5.5-mini',
updatedAt: '2026-05-16T10:00:00.000Z',
};

describe('AI provider configurations', () => {
it('fetches providers only for signed-in users with OpenAI-compatible support', () => {
expect(
shouldFetchAiProviderConfigurations({
hasAuthenticatedUser: true,
customProviderSupport: supportAll,
})
).toBe(true);
expect(
shouldFetchAiProviderConfigurations({
hasAuthenticatedUser: false,
customProviderSupport: supportAll,
})
).toBe(false);
expect(
shouldFetchAiProviderConfigurations({
hasAuthenticatedUser: true,
customProviderSupport: supportWithoutOpenAiCompatible,
})
).toBe(false);
});

it('skips provider fetches when custom provider support is missing or disabled', () => {
expect(
shouldFetchAiProviderConfigurations({
hasAuthenticatedUser: true,
customProviderSupport: null,
})
).toBe(false);
expect(
shouldFetchAiProviderConfigurations({
hasAuthenticatedUser: true,
customProviderSupport: supportDisabled,
})
).toBe(false);
});

it('lists OpenAI-compatible providers in the Ask AI dropdown', () => {
expect(
getAvailableAiProviderConfigurations({
aiProviderConfigurations: [openAiCompatibleProvider],
customProviderSupport: supportAll,
}).map(configuration => configuration.id)
).toEqual([openAiCompatibleProvider.id]);
});

it('honors OpenAI-compatible feature support', () => {
expect(
getAvailableAiProviderConfigurations({
aiProviderConfigurations: [openAiCompatibleProvider],
customProviderSupport: supportWithoutOpenAiCompatible,
})
).toEqual([]);
});

it('resolves a saved custom provider selection', () => {
expect(
getAiProviderConfigurationFromSelectionId({
selectionId: getConfigurationSelectionId(openAiCompatibleProvider.id),
aiProviderConfigurations: [openAiCompatibleProvider],
customProviderSupport: supportAll,
})
).toBe(openAiCompatibleProvider);
});

it('resolves a saved preset selection to the newest matching provider', () => {
expect(
getAiProviderConfigurationFromSelectionId({
selectionId: getPresetSelectionId('openai'),
aiProviderConfigurations: [
savedOpenAiProvider,
newerSavedOpenAiProvider,
],
customProviderSupport: supportAll,
})
).toBe(newerSavedOpenAiProvider);
});

it('does not resolve unsaved preset or custom-new selections', () => {
expect(
getAiProviderConfigurationFromSelectionId({
selectionId: getPresetSelectionId('openrouter'),
aiProviderConfigurations: [openAiCompatibleProvider],
customProviderSupport: supportAll,
})
).toBe(null);
expect(
getAiProviderConfigurationFromSelectionId({
selectionId: CUSTOM_PROVIDER_SELECTION_ID,
aiProviderConfigurations: [openAiCompatibleProvider],
customProviderSupport: supportAll,
})
).toBe(null);
});

it('does not resolve selections when OpenAI-compatible support is unavailable', () => {
expect(
getAiProviderConfigurationFromSelectionId({
selectionId: getConfigurationSelectionId(openAiCompatibleProvider.id),
aiProviderConfigurations: [openAiCompatibleProvider],
customProviderSupport: supportWithoutOpenAiCompatible,
})
).toBe(null);
});
});
Loading