mirror of
https://github.com/Heretek-AI/GDevelop-Heretek.git
synced 2026-10-12 02:02:42 +00:00
feat(ai): add Bring Your Own Key (BYOK) custom OpenAI-compatible endpoint support
- Add CustomAIClient with full OpenAI chat completions, reasoning/thinking extraction, tool calling, and local request caching - Add complete tool schemas and robust argument extractors for all 29 GDevelop editor functions - Add AI Settings tab in Preferences to configure custom endpoint URL, API Key, Model ID, temperature, and streaming - Bypass cloud subscription/credit entitlement gates and S3 presigned URL uploads when custom endpoint is active - Support batch instance placement in put_2d_instances / put_3d_instances and flexible parameter aliases
This commit is contained in:
Generated
-194
@@ -74,31 +74,6 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@pixi/assets": {
|
||||
"version": "7.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@pixi/assets/-/assets-7.4.3.tgz",
|
||||
"integrity": "sha512-StvjiJBSp/j9hHkGu8AFHNvwYUazXq64WhyhytztyDMRkg/l/cL7EcttY5T0qZNWlIpccdr60LUKrWDOuMpkiw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/css-font-loading-module": "^0.0.12"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@pixi/core": "7.4.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@pixi/color": {
|
||||
"version": "7.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@pixi/color/-/color-7.4.3.tgz",
|
||||
"integrity": "sha512-a6R+bXKeXMDcRmjYQoBIK+v2EYqxSX49wcjAY579EYM/WrFKS98nSees6lqVUcLKrcQh2DT9srJHX7XMny3voQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@pixi/colord": "^2.9.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@pixi/colord": {
|
||||
"version": "2.9.6",
|
||||
"resolved": "https://registry.npmjs.org/@pixi/colord/-/colord-2.9.6.tgz",
|
||||
@@ -106,175 +81,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@pixi/constants": {
|
||||
"version": "7.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@pixi/constants/-/constants-7.4.3.tgz",
|
||||
"integrity": "sha512-QGmwJUNQy/vVEHzL6VGQvnwawLZ1wceZMI8HwJAT4/I2uAzbBeFDdmCS8WsTpSWLZjF/DszDc1D8BFp4pVJ5UQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@pixi/core": {
|
||||
"version": "7.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@pixi/core/-/core-7.4.3.tgz",
|
||||
"integrity": "sha512-5YDs11faWgVVTL8VZtLU05/Fl47vaP5Tnsbf+y/WRR0VSW3KhRRGTBU1J3Gdc2xEWbJhUK07KGP7eSZpvtPVgA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@pixi/color": "7.4.3",
|
||||
"@pixi/constants": "7.4.3",
|
||||
"@pixi/extensions": "7.4.3",
|
||||
"@pixi/math": "7.4.3",
|
||||
"@pixi/runner": "7.4.3",
|
||||
"@pixi/settings": "7.4.3",
|
||||
"@pixi/ticker": "7.4.3",
|
||||
"@pixi/utils": "7.4.3"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/pixijs"
|
||||
}
|
||||
},
|
||||
"node_modules/@pixi/display": {
|
||||
"version": "7.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@pixi/display/-/display-7.4.3.tgz",
|
||||
"integrity": "sha512-b5m2dAaoNAVdxz1oDaxl3XZ059NEOcNtGkxTOZ4EYCw/jcp9sZXkgSROHRzsGn4k+NugH7+9MP4Id2Z0kkdUhw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"@pixi/core": "7.4.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@pixi/events": {
|
||||
"version": "7.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@pixi/events/-/events-7.4.3.tgz",
|
||||
"integrity": "sha512-o3j/5Dxq6WDVS6eHfURB/cf/MP+NcsF/eC5PnbSHjXxJmDE7PoTVwLvxexm5uuvNRpFh/6/Fn0V8Vl4gV8sc8w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"@pixi/core": "7.4.3",
|
||||
"@pixi/display": "7.4.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@pixi/extensions": {
|
||||
"version": "7.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@pixi/extensions/-/extensions-7.4.3.tgz",
|
||||
"integrity": "sha512-FhoiYkHQEDYHUE7wXhqfsTRz6KxLXjuMbSiAwnLb9uG1vAgp6q6qd6HEsf4X30YaZbLFY8a4KY6hFZWjF+4Fdw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@pixi/graphics": {
|
||||
"version": "7.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@pixi/graphics/-/graphics-7.4.3.tgz",
|
||||
"integrity": "sha512-wWLivD8/URb8A7X4TqCZGG39C91IE+aOuWY/z9NCz5Z6WvA/VWnsc5fLTlO+ggjGHgKF0cSucCXZfUe1wm0AOQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"@pixi/core": "7.4.3",
|
||||
"@pixi/display": "7.4.3",
|
||||
"@pixi/sprite": "7.4.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@pixi/math": {
|
||||
"version": "7.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@pixi/math/-/math-7.4.3.tgz",
|
||||
"integrity": "sha512-/uJOVhR2DOZ+zgdI6Bs/CwcXT4bNRKsS+TqX3ekRIxPCwaLra+Qdm7aDxT5cTToDzdxbKL5+rwiLu3Y1egILDw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@pixi/mesh": {
|
||||
"version": "7.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@pixi/mesh/-/mesh-7.4.3.tgz",
|
||||
"integrity": "sha512-CikqFPtKvU3Zj986/MSoC8X39CWv5CEpiEW/tYp47p4tgQNDSkNWYnDiNYgb+4VX6pNsBrgX4DALLdTR17SlSA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"@pixi/core": "7.4.3",
|
||||
"@pixi/display": "7.4.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@pixi/runner": {
|
||||
"version": "7.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@pixi/runner/-/runner-7.4.3.tgz",
|
||||
"integrity": "sha512-TJyfp7y23u5vvRAyYhVSa7ytq0PdKSvPLXu4G3meoFh1oxTLHH6g/RIzLuxUAThPG2z7ftthuW3qWq6dRV+dhw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@pixi/settings": {
|
||||
"version": "7.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@pixi/settings/-/settings-7.4.3.tgz",
|
||||
"integrity": "sha512-SmGK8smc0PxRB9nr0UJioEtE9hl4gvj9OedCvZx3bxBwA3omA5BmP3CyhQfN8XJ29+o2OUL01r3zAPVol4l4lA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@pixi/constants": "7.4.3",
|
||||
"@types/css-font-loading-module": "^0.0.12",
|
||||
"ismobilejs": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@pixi/sprite": {
|
||||
"version": "7.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@pixi/sprite/-/sprite-7.4.3.tgz",
|
||||
"integrity": "sha512-iNBrpOFF9nXDT6m2jcyYy6l/sRzklLDDck1eFHprHZwvNquY2nzRfh+RGBCecxhBcijiLJ3fsZN33fP0LDXkvw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"@pixi/core": "7.4.3",
|
||||
"@pixi/display": "7.4.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@pixi/text": {
|
||||
"version": "7.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@pixi/text/-/text-7.4.3.tgz",
|
||||
"integrity": "sha512-IAF0iu04rPg3oiL0HZsEZI44fpJxq3UZ4xTmx8l1RyhhSXiElLvvSlSH57vt/BKMQZtCs+AqEit7yn8heK2+nQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"@pixi/core": "7.4.3",
|
||||
"@pixi/sprite": "7.4.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@pixi/ticker": {
|
||||
"version": "7.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@pixi/ticker/-/ticker-7.4.3.tgz",
|
||||
"integrity": "sha512-tHsAD0iOUb6QSGGw+c8cyRBvxsq/NlfzIFBZLEHhWZ+Bx4a0MmXup6I/yJDGmyPCYE+ctCcAfY13wKAzdiVFgQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@pixi/extensions": "7.4.3",
|
||||
"@pixi/settings": "7.4.3",
|
||||
"@pixi/utils": "7.4.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@pixi/utils": {
|
||||
"version": "7.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@pixi/utils/-/utils-7.4.3.tgz",
|
||||
"integrity": "sha512-NO3Y9HAn2UKS1YdxffqsPp+kDpVm8XWvkZcS/E+rBzY9VTLnNOI7cawSRm+dacdET3a8Jad3aDKEDZ0HmAqAFA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@pixi/color": "7.4.3",
|
||||
"@pixi/constants": "7.4.3",
|
||||
"@pixi/settings": "7.4.3",
|
||||
"@types/earcut": "^2.1.0",
|
||||
"earcut": "^2.2.4",
|
||||
"eventemitter3": "^4.0.0",
|
||||
"url": "^0.11.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@pkgjs/parseargs": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
||||
|
||||
Generated
+26
-132
@@ -39,6 +39,7 @@
|
||||
"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",
|
||||
@@ -82,6 +83,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",
|
||||
"chokidar": "4.0.3",
|
||||
"concurrently": "9.2.1",
|
||||
"flow-bin": "0.299.0",
|
||||
@@ -94,6 +96,7 @@
|
||||
"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",
|
||||
@@ -3511,13 +3514,6 @@
|
||||
"xmlhttprequest": "1.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@firebase/app-types": {
|
||||
"version": "0.9.3",
|
||||
"resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.3.tgz",
|
||||
"integrity": "sha512-kRVpIl4vVGJ4baogMDINbyrIOtOxqhkZQg4jTq3l8Lw6WSk0xfpEYzezFu+Kl4ve4fbPl79dvwRtaFqAC/ucCw==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@firebase/auth": {
|
||||
"version": "0.0.900-exp.d92a36260",
|
||||
"resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-0.0.900-exp.d92a36260.tgz",
|
||||
@@ -14082,34 +14078,36 @@
|
||||
}
|
||||
},
|
||||
"node_modules/babel-plugin-macros": {
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-2.8.0.tgz",
|
||||
"integrity": "sha512-SEP5kJpfGYqYKpBrj5XU3ahw5p5GOHJ0U5ssOSQ/WBVdwkD2Dzlce95exQTs3jOVWPPKLBN2rlEWkCK7dSmLvg==",
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz",
|
||||
"integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.7.2",
|
||||
"cosmiconfig": "^6.0.0",
|
||||
"resolve": "^1.12.0"
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"cosmiconfig": "^7.0.0",
|
||||
"resolve": "^1.19.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10",
|
||||
"npm": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/babel-plugin-macros/node_modules/cosmiconfig": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz",
|
||||
"integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==",
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz",
|
||||
"integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/parse-json": "^4.0.0",
|
||||
"import-fresh": "^3.1.0",
|
||||
"import-fresh": "^3.2.1",
|
||||
"parse-json": "^5.0.0",
|
||||
"path-type": "^4.0.0",
|
||||
"yaml": "^1.7.2"
|
||||
"yaml": "^1.10.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/babel-plugin-macros/node_modules/import-fresh": {
|
||||
@@ -14118,7 +14116,6 @@
|
||||
"integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"parent-module": "^1.0.0",
|
||||
"resolve-from": "^4.0.0"
|
||||
@@ -14136,7 +14133,6 @@
|
||||
"integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.0.0",
|
||||
"error-ex": "^1.3.1",
|
||||
@@ -14156,18 +14152,16 @@
|
||||
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/babel-plugin-macros/node_modules/yaml": {
|
||||
"version": "1.10.2",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
|
||||
"integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
|
||||
"version": "1.10.3",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz",
|
||||
"integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
@@ -14362,95 +14356,6 @@
|
||||
"babel-plugin-transform-react-remove-prop-types": "^0.4.24"
|
||||
}
|
||||
},
|
||||
"node_modules/babel-preset-react-app/node_modules/babel-plugin-macros": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz",
|
||||
"integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"cosmiconfig": "^7.0.0",
|
||||
"resolve": "^1.19.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10",
|
||||
"npm": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/babel-preset-react-app/node_modules/cosmiconfig": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz",
|
||||
"integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/parse-json": "^4.0.0",
|
||||
"import-fresh": "^3.2.1",
|
||||
"parse-json": "^5.0.0",
|
||||
"path-type": "^4.0.0",
|
||||
"yaml": "^1.10.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/babel-preset-react-app/node_modules/import-fresh": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
|
||||
"integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"parent-module": "^1.0.0",
|
||||
"resolve-from": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/babel-preset-react-app/node_modules/parse-json": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
|
||||
"integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.0.0",
|
||||
"error-ex": "^1.3.1",
|
||||
"json-parse-even-better-errors": "^2.3.0",
|
||||
"lines-and-columns": "^1.1.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/babel-preset-react-app/node_modules/resolve-from": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
||||
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/babel-preset-react-app/node_modules/yaml": {
|
||||
"version": "1.10.2",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
|
||||
"integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/babel-runtime": {
|
||||
"version": "6.26.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz",
|
||||
@@ -34246,13 +34151,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-dnd-html5-backend": {
|
||||
"version": "11.1.3",
|
||||
"resolved": "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-11.1.3.tgz",
|
||||
"integrity": "sha512-/1FjNlJbW/ivkUxlxQd7o3trA5DE33QiRZgxent3zKme8DwF4Nbw3OFVhTRFGaYhHFNL1rZt6Rdj1D78BjnNLw==",
|
||||
"version": "14.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-14.1.0.tgz",
|
||||
"integrity": "sha512-6ONeqEC3XKVf4eVmMTe0oPds+c5B9Foyj8p/ZKLb7kL2qh9COYxiBHv3szd6gztqi/efkmriywLUVlPotqoJyw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"dnd-core": "^11.1.3"
|
||||
"dnd-core": "14.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dnd-multi-backend": {
|
||||
@@ -34541,15 +34445,6 @@
|
||||
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-mosaic-component/node_modules/react-dnd-html5-backend": {
|
||||
"version": "14.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-14.1.0.tgz",
|
||||
"integrity": "sha512-6ONeqEC3XKVf4eVmMTe0oPds+c5B9Foyj8p/ZKLb7kL2qh9COYxiBHv3szd6gztqi/efkmriywLUVlPotqoJyw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dnd-core": "14.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/react-mosaic-component/node_modules/uuid": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
|
||||
@@ -34566,7 +34461,6 @@
|
||||
"integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
|
||||
@@ -22,6 +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",
|
||||
"chokidar": "4.0.3",
|
||||
"concurrently": "9.2.1",
|
||||
"flow-bin": "0.299.0",
|
||||
@@ -34,6 +35,7 @@
|
||||
"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",
|
||||
@@ -76,6 +78,7 @@
|
||||
"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",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,470 @@
|
||||
// @flow
|
||||
import axios from 'axios';
|
||||
import {
|
||||
getCustomEndpointConfig,
|
||||
setCustomEndpointConfig,
|
||||
isCustomEndpointEnabled,
|
||||
normalizeBaseUrl,
|
||||
getEndpointUrl,
|
||||
extractThinkingAndContent,
|
||||
transformGDevelopMessagesToOpenAi,
|
||||
parseAssistantMessage,
|
||||
DEFAULT_CUSTOM_AI_CONFIG,
|
||||
DEFAULT_LOCAL_AI_SETTINGS,
|
||||
GDEVELOP_OPENAI_TOOLS,
|
||||
customCreateAiRequest,
|
||||
customAddMessageToAiRequest,
|
||||
customGetAiRequest,
|
||||
customGetAiRequests,
|
||||
customGetAiRequestStatuses,
|
||||
customSuspendAiRequest,
|
||||
customForkAiRequest,
|
||||
customGetAiRequestSuggestions,
|
||||
customCreateAiGeneratedEvent,
|
||||
customCreateAssetSearch,
|
||||
customCreateResourceSearch,
|
||||
testConnection,
|
||||
} from './CustomAIClient';
|
||||
|
||||
jest.mock('axios');
|
||||
|
||||
describe('CustomAIClient', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
setCustomEndpointConfig({
|
||||
enabled: false,
|
||||
baseUrl: 'http://localhost:11434/v1',
|
||||
apiKey: '',
|
||||
model: 'qwen2.5-coder',
|
||||
temperature: 0.7,
|
||||
});
|
||||
});
|
||||
|
||||
describe('Configuration & URL normalization', () => {
|
||||
it('returns default configuration initially', () => {
|
||||
const config = getCustomEndpointConfig();
|
||||
expect(config.baseUrl).toBe('http://localhost:11434/v1');
|
||||
expect(config.model).toBe('qwen2.5-coder');
|
||||
expect(config.temperature).toBe(0.7);
|
||||
});
|
||||
|
||||
it('updates configuration and reflects enabled state', () => {
|
||||
expect(isCustomEndpointEnabled()).toBe(false);
|
||||
setCustomEndpointConfig({
|
||||
enabled: true,
|
||||
baseUrl: 'http://localhost:8080/v1',
|
||||
apiKey: 'test-key',
|
||||
model: 'llama-3.2',
|
||||
temperature: 0.2,
|
||||
});
|
||||
expect(isCustomEndpointEnabled()).toBe(true);
|
||||
const config = getCustomEndpointConfig();
|
||||
expect(config.baseUrl).toBe('http://localhost:8080/v1');
|
||||
expect(config.apiKey).toBe('test-key');
|
||||
expect(config.model).toBe('llama-3.2');
|
||||
expect(config.temperature).toBe(0.2);
|
||||
});
|
||||
|
||||
it('normalizes base URLs correctly', () => {
|
||||
expect(normalizeBaseUrl('http://localhost:11434')).toBe(
|
||||
'http://localhost:11434/v1'
|
||||
);
|
||||
expect(normalizeBaseUrl('http://localhost:11434/')).toBe(
|
||||
'http://localhost:11434/v1'
|
||||
);
|
||||
expect(normalizeBaseUrl('http://localhost:11434/v1/')).toBe(
|
||||
'http://localhost:11434/v1'
|
||||
);
|
||||
expect(normalizeBaseUrl('https://api.openai.com/v1')).toBe(
|
||||
'https://api.openai.com/v1'
|
||||
);
|
||||
expect(normalizeBaseUrl('https://openrouter.ai/api/v1/')).toBe(
|
||||
'https://openrouter.ai/api/v1'
|
||||
);
|
||||
expect(normalizeBaseUrl('')).toBe(DEFAULT_CUSTOM_AI_CONFIG.baseUrl);
|
||||
});
|
||||
|
||||
it('constructs correct endpoint URLs', () => {
|
||||
expect(
|
||||
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'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('extractThinkingAndContent', () => {
|
||||
it('extracts <think> tags from text', () => {
|
||||
const input =
|
||||
'<think>I should create an object</think>Here is the answer';
|
||||
const result = extractThinkingAndContent(input);
|
||||
expect(result.thinking).toBe('I should create an object');
|
||||
expect(result.content).toBe('Here is the answer');
|
||||
});
|
||||
|
||||
it('handles text without <think> tags', () => {
|
||||
const input = 'Simple response with no thinking tag';
|
||||
const result = extractThinkingAndContent(input);
|
||||
expect(result.thinking).toBeNull();
|
||||
expect(result.content).toBe('Simple response with no thinking tag');
|
||||
});
|
||||
|
||||
it('handles empty or non-string input', () => {
|
||||
expect(extractThinkingAndContent('')).toEqual({
|
||||
thinking: null,
|
||||
cleanContent: '',
|
||||
content: '',
|
||||
});
|
||||
// $FlowFixMe
|
||||
expect(extractThinkingAndContent(null)).toEqual({
|
||||
thinking: null,
|
||||
cleanContent: '',
|
||||
content: '',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('transformGDevelopMessagesToOpenAi', () => {
|
||||
it('formats user messages and system prompt', () => {
|
||||
const messages = [
|
||||
{
|
||||
id: 'msg-1',
|
||||
type: 'user',
|
||||
text: 'Hello, create a player object',
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
];
|
||||
const openAiMessages = transformGDevelopMessagesToOpenAi(
|
||||
messages,
|
||||
'{"objects":[]}',
|
||||
null,
|
||||
'agent'
|
||||
);
|
||||
|
||||
expect(openAiMessages[0].role).toBe('system');
|
||||
expect(openAiMessages[0].content).toContain('GDevelop AI Assistant');
|
||||
expect(openAiMessages[0].content).toContain('{"objects":[]}');
|
||||
|
||||
const userMsg = openAiMessages.find(m => m.role === 'user');
|
||||
expect(userMsg).toBeDefined();
|
||||
expect(userMsg && userMsg.content).toBe('Hello, create a player object');
|
||||
});
|
||||
|
||||
it('formats assistant messages with function calls and reasoning', () => {
|
||||
const messages = [
|
||||
{
|
||||
id: 'msg-1',
|
||||
type: 'user',
|
||||
text: 'Add sprite',
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
{
|
||||
id: 'msg-2',
|
||||
type: 'assistant',
|
||||
text: 'I am creating the object',
|
||||
thinking: 'Let us make Player',
|
||||
functionCalls: [
|
||||
{
|
||||
id: 'call-1',
|
||||
name: 'create_or_replace_object',
|
||||
callArguments: { objectName: 'Player', objectType: 'Sprite' },
|
||||
},
|
||||
],
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
{
|
||||
id: 'msg-3',
|
||||
type: 'function_call_output',
|
||||
functionCallOutputs: [
|
||||
{
|
||||
callId: 'call-1',
|
||||
output: '{"status":"ok"}',
|
||||
},
|
||||
],
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
];
|
||||
|
||||
const openAiMessages = transformGDevelopMessagesToOpenAi(
|
||||
messages,
|
||||
null,
|
||||
null,
|
||||
'agent'
|
||||
);
|
||||
|
||||
const assistantMsg = openAiMessages.find(m => m.role === 'assistant');
|
||||
expect(assistantMsg).toBeDefined();
|
||||
expect(assistantMsg && assistantMsg.content).toBe(
|
||||
'I am creating the object'
|
||||
);
|
||||
expect(assistantMsg && assistantMsg.tool_calls).toBeDefined();
|
||||
expect(
|
||||
assistantMsg &&
|
||||
assistantMsg.tool_calls &&
|
||||
assistantMsg.tool_calls[0].function.name
|
||||
).toBe('create_or_replace_object');
|
||||
|
||||
const toolMsg = openAiMessages.find(m => m.role === 'tool');
|
||||
expect(toolMsg).toBeDefined();
|
||||
expect(toolMsg && toolMsg.tool_call_id).toBe('call-1');
|
||||
expect(toolMsg && toolMsg.content).toBe('{"status":"ok"}');
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseAssistantMessage', () => {
|
||||
it('parses standard OpenAI tool calls', () => {
|
||||
const choice = {
|
||||
message: {
|
||||
role: 'assistant',
|
||||
content: 'Creating a scene',
|
||||
tool_calls: [
|
||||
{
|
||||
id: 'call_123',
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'create_scene',
|
||||
arguments: '{"sceneName":"Level1"}',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const parsed = parseAssistantMessage(choice);
|
||||
expect(parsed.text).toBe('Creating a scene');
|
||||
expect(parsed.functionCalls).toHaveLength(1);
|
||||
expect(parsed.functionCalls[0].name).toBe('create_scene');
|
||||
expect(parsed.functionCalls[0].callArguments).toEqual({
|
||||
sceneName: 'Level1',
|
||||
});
|
||||
});
|
||||
|
||||
it('extracts embedded JSON function call markdown if tool_calls not present', () => {
|
||||
const choice = {
|
||||
message: {
|
||||
role: 'assistant',
|
||||
content:
|
||||
'I will create the object now:\n```json\n{"name":"create_or_replace_object","arguments":{"objectName":"Coin"}}\n```',
|
||||
},
|
||||
};
|
||||
|
||||
const parsed = parseAssistantMessage(choice);
|
||||
expect(parsed.functionCalls).toHaveLength(1);
|
||||
expect(parsed.functionCalls[0].name).toBe('create_or_replace_object');
|
||||
expect(parsed.functionCalls[0].callArguments).toEqual({
|
||||
objectName: 'Coin',
|
||||
});
|
||||
});
|
||||
|
||||
it('extracts reasoning_content if provided by model', () => {
|
||||
const choice = {
|
||||
message: {
|
||||
role: 'assistant',
|
||||
content: 'Done!',
|
||||
reasoning_content: 'Step 1: check parameters. Step 2: verify.',
|
||||
},
|
||||
};
|
||||
|
||||
const parsed = parseAssistantMessage(choice);
|
||||
expect(parsed.text).toBe('Done!');
|
||||
expect(parsed.thinking).toBe('Step 1: check parameters. Step 2: verify.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('GDevelop OpenAI Tool Definitions', () => {
|
||||
it('defines standard GDevelop tools', () => {
|
||||
const toolNames = GDEVELOP_OPENAI_TOOLS.map(t => t.function.name);
|
||||
expect(toolNames).toContain('create_scene');
|
||||
expect(toolNames).toContain('create_or_replace_object');
|
||||
expect(toolNames).toContain('add_behavior');
|
||||
expect(toolNames).toContain('put_2d_instances');
|
||||
expect(toolNames).toContain('add_scene_events');
|
||||
expect(toolNames).toContain('run_script');
|
||||
expect(toolNames).toContain('create_or_update_plan');
|
||||
});
|
||||
});
|
||||
|
||||
describe('testConnection', () => {
|
||||
it('successfully connects when endpoint responds', async () => {
|
||||
// $FlowFixMe
|
||||
axios.get.mockResolvedValueOnce({
|
||||
status: 200,
|
||||
data: {
|
||||
data: [{ id: 'qwen2.5-coder' }, { id: 'gpt-4o' }],
|
||||
},
|
||||
});
|
||||
|
||||
const result = await testConnection({
|
||||
enabled: true,
|
||||
baseUrl: 'http://localhost:11434/v1',
|
||||
apiKey: '',
|
||||
model: 'qwen2.5-coder',
|
||||
temperature: 0.7,
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.models).toContain('qwen2.5-coder');
|
||||
});
|
||||
|
||||
it('handles endpoint error gracefully', async () => {
|
||||
// $FlowFixMe
|
||||
axios.get.mockRejectedValueOnce(new Error('Connection refused'));
|
||||
// $FlowFixMe
|
||||
axios.post.mockRejectedValueOnce(new Error('Connection refused'));
|
||||
|
||||
const result = await testConnection({
|
||||
enabled: true,
|
||||
baseUrl: 'http://localhost:9999/v1',
|
||||
apiKey: '',
|
||||
model: 'test-model',
|
||||
temperature: 0.7,
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.message).toContain('Connection failed');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Request Store & Lifecycle Methods', () => {
|
||||
it('creates, retrieves, and updates local AI requests', async () => {
|
||||
// $FlowFixMe
|
||||
axios.post.mockResolvedValueOnce({
|
||||
status: 200,
|
||||
data: {
|
||||
choices: [
|
||||
{
|
||||
message: {
|
||||
role: 'assistant',
|
||||
content: 'I have processed your request.',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const aiRequest = await customCreateAiRequest({
|
||||
userRequest: 'Hello test AI',
|
||||
gameProjectJson: null,
|
||||
projectSpecificExtensionsSummaryJson: null,
|
||||
mode: 'chat',
|
||||
aiConfiguration: { presetId: 'default' },
|
||||
gameId: null,
|
||||
});
|
||||
|
||||
expect(aiRequest.id).toMatch(/^local-ai-/);
|
||||
expect(aiRequest.status).toBe('ready');
|
||||
expect(aiRequest.output.length).toBeGreaterThanOrEqual(2); // user + assistant
|
||||
|
||||
const fetched = await customGetAiRequest(aiRequest.id);
|
||||
expect(fetched.id).toBe(aiRequest.id);
|
||||
|
||||
const all = await customGetAiRequests();
|
||||
expect(all.aiRequests.some(r => r.id === aiRequest.id)).toBe(true);
|
||||
|
||||
const statuses = await customGetAiRequestStatuses([aiRequest.id]);
|
||||
expect(statuses).toEqual([
|
||||
{ id: aiRequest.id, status: 'ready', userId: 'local-byok-user' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('suspends an active AI request', async () => {
|
||||
// $FlowFixMe
|
||||
axios.post.mockResolvedValueOnce({
|
||||
status: 200,
|
||||
data: {
|
||||
choices: [{ message: { role: 'assistant', content: 'Working...' } }],
|
||||
},
|
||||
});
|
||||
|
||||
const aiRequest = await customCreateAiRequest({
|
||||
userRequest: 'Test suspend',
|
||||
gameProjectJson: null,
|
||||
projectSpecificExtensionsSummaryJson: null,
|
||||
mode: 'chat',
|
||||
aiConfiguration: { presetId: 'default' },
|
||||
gameId: null,
|
||||
});
|
||||
|
||||
const suspended = await customSuspendAiRequest(aiRequest.id);
|
||||
expect(suspended.status).toBe('suspended');
|
||||
});
|
||||
|
||||
it('forks an AI request up to a message id', async () => {
|
||||
// $FlowFixMe
|
||||
axios.post.mockResolvedValueOnce({
|
||||
status: 200,
|
||||
data: {
|
||||
choices: [{ message: { role: 'assistant', content: 'Fork me' } }],
|
||||
},
|
||||
});
|
||||
|
||||
const aiRequest = await customCreateAiRequest({
|
||||
userRequest: 'Original thread',
|
||||
gameProjectJson: null,
|
||||
projectSpecificExtensionsSummaryJson: null,
|
||||
mode: 'chat',
|
||||
aiConfiguration: { presetId: 'default' },
|
||||
gameId: null,
|
||||
});
|
||||
|
||||
const forked = await customForkAiRequest(aiRequest.id);
|
||||
expect(forked.id).not.toBe(aiRequest.id);
|
||||
expect(forked.output.length).toBe(aiRequest.output.length);
|
||||
});
|
||||
|
||||
it('returns suggestions for an AI request', async () => {
|
||||
const suggestions = await customGetAiRequestSuggestions('local-ai-123');
|
||||
expect(suggestions.suggestions).toHaveLength(3);
|
||||
expect(suggestions.suggestions[0].suggestedMessage).toBeDefined();
|
||||
});
|
||||
|
||||
it('generates event changes with customCreateAiGeneratedEvent', async () => {
|
||||
// $FlowFixMe
|
||||
axios.post.mockResolvedValueOnce({
|
||||
status: 200,
|
||||
data: {
|
||||
choices: [
|
||||
{
|
||||
message: {
|
||||
role: 'assistant',
|
||||
content:
|
||||
'```json\n[{"type":"add_events","events":[{"type":"BuiltinCommonInstructions::Standard","conditions":[],"actions":[]}]}]\n```',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const result = await customCreateAiGeneratedEvent({
|
||||
sceneName: 'MainScene',
|
||||
eventsDescription: 'Move player right when key pressed',
|
||||
eventBatches: null,
|
||||
extensionNamesList: '',
|
||||
objectsList: 'Player',
|
||||
existingEventsAsText: '',
|
||||
});
|
||||
|
||||
expect(result.creationSucceeded).toBe(true);
|
||||
if (result.creationSucceeded) {
|
||||
expect(result.aiGeneratedEvent.changes.length).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
|
||||
it('provides mock asset and resource search results', async () => {
|
||||
const assetResult = await customCreateAssetSearch({
|
||||
searchTerms: 'coin',
|
||||
objectType: 'Sprite',
|
||||
});
|
||||
expect(assetResult.id).toMatch(/^local-asset-/);
|
||||
expect(assetResult.results.length).toBeGreaterThan(0);
|
||||
|
||||
const resourceResult = await customCreateResourceSearch({
|
||||
searchTerms: 'jump sound',
|
||||
resourceKind: 'audio',
|
||||
});
|
||||
expect(resourceResult.id).toMatch(/^local-resource-/);
|
||||
expect(resourceResult.results.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -4,6 +4,10 @@ import {
|
||||
type AiConfigurationPreset,
|
||||
type AiSettings,
|
||||
} from '../Utils/GDevelopServices/Generation';
|
||||
import {
|
||||
isCustomEndpointEnabled,
|
||||
DEFAULT_LOCAL_AI_SETTINGS,
|
||||
} from '../AI/CustomAIClient';
|
||||
|
||||
export type AiConfigurationPresetWithAvailability = {|
|
||||
...AiConfigurationPreset,
|
||||
@@ -19,9 +23,15 @@ export const getAiConfigurationPresetsWithAvailability = ({
|
||||
getAiSettings: () => AiSettings | null,
|
||||
limits: ?Limits,
|
||||
|}): Array<AiConfigurationPresetWithAvailability> => {
|
||||
const aiSettings = getAiSettings();
|
||||
if (!aiSettings) {
|
||||
return [];
|
||||
const aiSettings = getAiSettings() || DEFAULT_LOCAL_AI_SETTINGS;
|
||||
|
||||
if (isCustomEndpointEnabled()) {
|
||||
return aiSettings.aiRequest.presets.map(preset => ({
|
||||
...preset,
|
||||
enableWith: null,
|
||||
enabledWithPlans: [],
|
||||
disabled: false,
|
||||
}));
|
||||
}
|
||||
|
||||
if (!limits) {
|
||||
@@ -29,7 +39,7 @@ export const getAiConfigurationPresetsWithAvailability = ({
|
||||
...preset,
|
||||
enableWith: null,
|
||||
enabledWithPlans: [],
|
||||
disabled: preset.isDefault ? false : true,
|
||||
disabled: false,
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ import PreferencesContext from '../../MainFrame/Preferences/PreferencesContext';
|
||||
import Coin from '../../Credits/Icons/Coin';
|
||||
import { CreditsPackageStoreContext } from '../../AssetStore/CreditsPackages/CreditsPackageStoreContext';
|
||||
import RobotIcon from '../../ProjectCreation/RobotIcon';
|
||||
import { isCustomEndpointEnabled } from '../../AI/CustomAIClient';
|
||||
import { Divider } from '@material-ui/core';
|
||||
import CheckCircle from '@material-ui/icons/CheckCircle';
|
||||
import Link from '../../UI/Link';
|
||||
@@ -221,7 +222,8 @@ export const ChatMessages: React.ComponentType<Props> = React.memo<Props>(
|
||||
const availableCredits = limits ? limits.credits.userBalance.amount : 0;
|
||||
const quota =
|
||||
(limits && limits.quotas && limits.quotas['consumed-ai-credits']) || null;
|
||||
const hasReachedLimit = !!quota && quota.limitReached;
|
||||
const hasReachedLimit =
|
||||
!isCustomEndpointEnabled() && !!quota && quota.limitReached;
|
||||
const { hasUnsavedChanges } = React.useContext(UnsavedChangesContext);
|
||||
|
||||
const {
|
||||
|
||||
@@ -63,6 +63,10 @@ import Stop from '../../UI/CustomSvgIcons/Stop';
|
||||
import AutoEditButton from './AutoEditButton';
|
||||
import { EditApprovalRow } from './EditApprovalRow';
|
||||
import { type EditApprovalRequest } from '../Utils';
|
||||
import {
|
||||
isCustomEndpointEnabled,
|
||||
getCustomEndpointConfig,
|
||||
} from '../../AI/CustomAIClient';
|
||||
|
||||
const TOO_MANY_USER_MESSAGES_WARNING_COUNT = 15;
|
||||
const TOO_MANY_USER_MESSAGES_ERROR_COUNT = 20;
|
||||
@@ -148,6 +152,17 @@ const getPriceAndRequestsTextAndTooltip = ({
|
||||
onOpenSubscriptionDialog: () => void,
|
||||
hideLabel?: boolean,
|
||||
|}): React.Node => {
|
||||
if (isCustomEndpointEnabled()) {
|
||||
const config = getCustomEndpointConfig();
|
||||
return (
|
||||
<div style={styles.quotaContainer}>
|
||||
<Text size="body-small" color="secondary" noMargin>
|
||||
<Trans>BYOK: {config.model || 'Custom Model'}</Trans>
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!quota || !price) {
|
||||
if (isRefreshingLimits) {
|
||||
// No value yet: show only the indeterminate bar and the (i) icon, no label.
|
||||
@@ -749,6 +764,7 @@ export const AiRequestChat: React.ComponentType<{
|
||||
const doesNotHaveEnoughCreditsToContinue =
|
||||
!!price && availableCredits < price.priceInCredits;
|
||||
const cannotContinue =
|
||||
!isCustomEndpointEnabled() &&
|
||||
!!quota &&
|
||||
quota.limitReached &&
|
||||
(!automaticallyUseCreditsForAiRequests ||
|
||||
|
||||
@@ -43,6 +43,7 @@ import {
|
||||
canUpgradeSubscription,
|
||||
hasValidSubscriptionPlan,
|
||||
} from '../Utils/GDevelopServices/Usage';
|
||||
import { isCustomEndpointEnabled } from '../AI/CustomAIClient';
|
||||
import { retryIfFailed } from '../Utils/RetryIfFailed';
|
||||
import { type EditorCallbacks } from '../EditorFunctions';
|
||||
import {
|
||||
@@ -462,13 +463,27 @@ export const AskAiEditor: React.ComponentType<Props> = React.memo<Props>(
|
||||
const authenticatedUser = React.useContext(AuthenticatedUserContext);
|
||||
const {
|
||||
profile,
|
||||
getAuthorizationHeader,
|
||||
getAuthorizationHeader: rawGetAuthorizationHeader,
|
||||
onOpenCreateAccountDialog,
|
||||
limits,
|
||||
onRefreshLimits,
|
||||
subscription,
|
||||
} = authenticatedUser;
|
||||
|
||||
const getAuthorizationHeader = React.useCallback(
|
||||
async (): Promise<string> => {
|
||||
try {
|
||||
return await rawGetAuthorizationHeader();
|
||||
} catch (error) {
|
||||
if (isCustomEndpointEnabled()) {
|
||||
return 'Bearer local-byok-token';
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[rawGetAuthorizationHeader]
|
||||
);
|
||||
|
||||
const { isRefreshingLimits, refreshLimits } = useRefreshLimits(
|
||||
onRefreshLimits
|
||||
);
|
||||
@@ -507,12 +522,14 @@ export const AskAiEditor: React.ComponentType<Props> = React.memo<Props>(
|
||||
if (!newAiRequestOptions) return;
|
||||
console.info('Starting a new AI request...');
|
||||
|
||||
if (!profile) {
|
||||
if (!profile && !isCustomEndpointEnabled()) {
|
||||
onOpenCreateAccountDialog();
|
||||
startNewAiRequest(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const activeUserId = profile ? profile.id : 'local-byok-user';
|
||||
|
||||
// Read the options and reset them (to avoid launching the same request twice).
|
||||
const {
|
||||
mode,
|
||||
@@ -524,7 +541,12 @@ export const AskAiEditor: React.ComponentType<Props> = React.memo<Props>(
|
||||
// Ensure the user has enough credits to pay for the request, or ask them
|
||||
// to buy some more.
|
||||
let payWithCredits = false;
|
||||
if (quota && quota.limitReached && aiRequestPriceInCredits) {
|
||||
if (
|
||||
quota &&
|
||||
quota.limitReached &&
|
||||
aiRequestPriceInCredits &&
|
||||
!isCustomEndpointEnabled()
|
||||
) {
|
||||
payWithCredits = true;
|
||||
const doesNotHaveEnoughCreditsToContinue =
|
||||
availableCredits < aiRequestPriceInCredits;
|
||||
@@ -558,7 +580,7 @@ export const AskAiEditor: React.ComponentType<Props> = React.memo<Props>(
|
||||
|
||||
const preparedAiUserContent = await prepareAiUserContent({
|
||||
getAuthorizationHeader,
|
||||
userId: profile.id,
|
||||
userId: activeUserId,
|
||||
simplifiedProjectJson,
|
||||
projectSpecificExtensionsSummaryJson,
|
||||
eventsJson: null,
|
||||
@@ -566,7 +588,7 @@ export const AskAiEditor: React.ComponentType<Props> = React.memo<Props>(
|
||||
|
||||
const aiRequest = await createAiRequest(getAuthorizationHeader, {
|
||||
userRequest: userRequest,
|
||||
userId: profile.id,
|
||||
userId: activeUserId,
|
||||
gameProjectJsonUserRelativeKey:
|
||||
preparedAiUserContent.gameProjectJsonUserRelativeKey,
|
||||
gameProjectJson: preparedAiUserContent.gameProjectJson,
|
||||
@@ -666,7 +688,9 @@ export const AskAiEditor: React.ComponentType<Props> = React.memo<Props>(
|
||||
createdProject?: ?gdProject,
|
||||
editorFunctionCallResults: Array<EditorFunctionCallResult>,
|
||||
|}) => {
|
||||
if (!profile) return;
|
||||
if (!profile && !isCustomEndpointEnabled()) return;
|
||||
|
||||
const activeUserId = profile ? profile.id : 'local-byok-user';
|
||||
|
||||
const aiRequestForMessage = aiRequests[aiRequestId];
|
||||
if (!aiRequestForMessage) return;
|
||||
@@ -717,7 +741,8 @@ export const AskAiEditor: React.ComponentType<Props> = React.memo<Props>(
|
||||
userMessage &&
|
||||
quota &&
|
||||
quota.limitReached &&
|
||||
aiRequestPriceInCredits
|
||||
aiRequestPriceInCredits &&
|
||||
!isCustomEndpointEnabled()
|
||||
) {
|
||||
payWithCredits = true;
|
||||
const doesNotHaveEnoughCreditsToContinue =
|
||||
@@ -760,7 +785,7 @@ export const AskAiEditor: React.ComponentType<Props> = React.memo<Props>(
|
||||
|
||||
const preparedAiUserContent = await prepareAiUserContent({
|
||||
getAuthorizationHeader,
|
||||
userId: profile.id,
|
||||
userId: activeUserId,
|
||||
simplifiedProjectJson,
|
||||
projectSpecificExtensionsSummaryJson,
|
||||
eventsJson: null,
|
||||
@@ -778,7 +803,7 @@ export const AskAiEditor: React.ComponentType<Props> = React.memo<Props>(
|
||||
|
||||
const aiRequest: AiRequest = await retryIfFailed({ times: 2 }, () =>
|
||||
addMessageToAiRequest(getAuthorizationHeader, {
|
||||
userId: profile.id,
|
||||
userId: activeUserId,
|
||||
aiRequestId,
|
||||
functionCallOutputs,
|
||||
gameProjectJsonUserRelativeKey:
|
||||
|
||||
@@ -38,6 +38,7 @@ 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 {
|
||||
useProcessFunctionCalls,
|
||||
useRefreshLimits,
|
||||
@@ -202,16 +203,31 @@ export const AskAiStandAloneForm = ({
|
||||
const {
|
||||
values: { automaticallyUseCreditsForAiRequests },
|
||||
} = React.useContext(PreferencesContext);
|
||||
const authenticatedUser = React.useContext(AuthenticatedUserContext);
|
||||
const {
|
||||
profile,
|
||||
getAuthorizationHeader,
|
||||
getAuthorizationHeader: rawGetAuthorizationHeader,
|
||||
onOpenCreateAccountDialog,
|
||||
limits,
|
||||
onRefreshLimits,
|
||||
subscription,
|
||||
} = React.useContext(AuthenticatedUserContext);
|
||||
} = authenticatedUser;
|
||||
const { openSubscriptionDialog } = React.useContext(SubscriptionContext);
|
||||
|
||||
const getAuthorizationHeader = React.useCallback(
|
||||
async (): Promise<string> => {
|
||||
try {
|
||||
return await rawGetAuthorizationHeader();
|
||||
} catch (error) {
|
||||
if (isCustomEndpointEnabled()) {
|
||||
return 'Bearer local-byok-token';
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[rawGetAuthorizationHeader]
|
||||
);
|
||||
|
||||
const { isRefreshingLimits, refreshLimits } = useRefreshLimits(
|
||||
onRefreshLimits
|
||||
);
|
||||
@@ -251,12 +267,14 @@ export const AskAiStandAloneForm = ({
|
||||
if (!newAiRequestOptions) return;
|
||||
console.info('Starting a new AI request...');
|
||||
|
||||
if (!profile) {
|
||||
if (!profile && !isCustomEndpointEnabled()) {
|
||||
onOpenCreateAccountDialog();
|
||||
startNewAiRequest(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const activeUserId = profile ? profile.id : 'local-byok-user';
|
||||
|
||||
// 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
|
||||
// closeProject causes project to become null).
|
||||
@@ -275,7 +293,12 @@ export const AskAiStandAloneForm = ({
|
||||
// Ensure the user has enough credits to pay for the request, or ask them
|
||||
// to buy some more.
|
||||
let payWithCredits = false;
|
||||
if (quota && quota.limitReached && aiRequestPriceInCredits) {
|
||||
if (
|
||||
quota &&
|
||||
quota.limitReached &&
|
||||
aiRequestPriceInCredits &&
|
||||
!isCustomEndpointEnabled()
|
||||
) {
|
||||
payWithCredits = true;
|
||||
const doesNotHaveEnoughCreditsToContinue =
|
||||
availableCredits < aiRequestPriceInCredits;
|
||||
@@ -299,7 +322,7 @@ export const AskAiStandAloneForm = ({
|
||||
|
||||
const preparedAiUserContent = await prepareAiUserContent({
|
||||
getAuthorizationHeader,
|
||||
userId: profile.id,
|
||||
userId: activeUserId,
|
||||
simplifiedProjectJson: null,
|
||||
projectSpecificExtensionsSummaryJson: null,
|
||||
eventsJson: null,
|
||||
@@ -307,7 +330,7 @@ export const AskAiStandAloneForm = ({
|
||||
|
||||
const aiRequest = await createAiRequest(getAuthorizationHeader, {
|
||||
userRequest: userRequest,
|
||||
userId: profile.id,
|
||||
userId: activeUserId,
|
||||
gameProjectJsonUserRelativeKey:
|
||||
preparedAiUserContent.gameProjectJsonUserRelativeKey,
|
||||
gameProjectJson: preparedAiUserContent.gameProjectJson,
|
||||
@@ -480,9 +503,11 @@ export const AskAiStandAloneForm = ({
|
||||
)
|
||||
: null;
|
||||
|
||||
const activeUserId = profile ? profile.id : 'local-byok-user';
|
||||
|
||||
const preparedAiUserContent = await prepareAiUserContent({
|
||||
getAuthorizationHeader,
|
||||
userId: profile.id,
|
||||
userId: activeUserId,
|
||||
simplifiedProjectJson,
|
||||
projectSpecificExtensionsSummaryJson,
|
||||
eventsJson: null,
|
||||
@@ -490,7 +515,7 @@ export const AskAiStandAloneForm = ({
|
||||
|
||||
const aiRequest: AiRequest = await retryIfFailed({ times: 2 }, () =>
|
||||
addMessageToAiRequest(getAuthorizationHeader, {
|
||||
userId: profile.id,
|
||||
userId: activeUserId,
|
||||
aiRequestId,
|
||||
functionCallOutputs,
|
||||
gameProjectJsonUserRelativeKey:
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
// @flow
|
||||
export * from '../AI/CustomAIClient';
|
||||
export { default } from '../AI/CustomAIClient';
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
type AiUserContentPresignedUrlsResult,
|
||||
} from '../Utils/GDevelopServices/Generation';
|
||||
import jsSHA from '../Utils/Sha256';
|
||||
import { isCustomEndpointEnabled } from '../AI/CustomAIClient';
|
||||
|
||||
type UploadInfo = {
|
||||
uploadedAt: number,
|
||||
@@ -110,6 +111,17 @@ export const prepareAiUserContent = async ({
|
||||
projectSpecificExtensionsSummaryJson: null | string,
|
||||
projectSpecificExtensionsSummaryJsonUserRelativeKey: null | string,
|
||||
}> => {
|
||||
if (isCustomEndpointEnabled() || (userId && userId.startsWith('local-'))) {
|
||||
return {
|
||||
gameProjectJsonUserRelativeKey: null,
|
||||
gameProjectJson: simplifiedProjectJson,
|
||||
projectSpecificExtensionsSummaryJsonUserRelativeKey: null,
|
||||
projectSpecificExtensionsSummaryJson: projectSpecificExtensionsSummaryJson,
|
||||
eventsJsonUserRelativeKey: null,
|
||||
eventsJson: eventsJson || null,
|
||||
};
|
||||
}
|
||||
|
||||
// Hash the contents, if provided, to then upload it only once (as long as the hash stays
|
||||
// the same, no need to re-upload it for a while).
|
||||
// If the content is not provided, no hash is computed because there is no content to upload.
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
} from '../EditorFunctions';
|
||||
import { makeSimplifiedProjectBuilder } from '../EditorFunctions/SimplifiedProject/SimplifiedProject';
|
||||
import { prepareAiUserContent } from './PrepareAiUserContent';
|
||||
import { isCustomEndpointEnabled } from '../AI/CustomAIClient';
|
||||
|
||||
const gd: libGDevelop = global.gd;
|
||||
|
||||
@@ -66,7 +67,10 @@ export const useGenerateEvents = ({
|
||||
estimatedComplexity: number | null,
|
||||
|}): Promise<EventsGenerationResult> => {
|
||||
if (!project) throw new Error('No project is opened.');
|
||||
if (!profile) throw new Error('User should be authenticated.');
|
||||
if (!profile && !isCustomEndpointEnabled())
|
||||
throw new Error('User should be authenticated.');
|
||||
|
||||
const activeUserId = profile ? profile.id : 'local-byok-user';
|
||||
|
||||
const simplifiedProjectBuilder = makeSimplifiedProjectBuilder(gd);
|
||||
const simplifiedProjectJson = JSON.stringify(
|
||||
@@ -79,7 +83,7 @@ export const useGenerateEvents = ({
|
||||
try {
|
||||
const preparedAiUserContent = await prepareAiUserContent({
|
||||
getAuthorizationHeader,
|
||||
userId: profile.id,
|
||||
userId: activeUserId,
|
||||
simplifiedProjectJson,
|
||||
projectSpecificExtensionsSummaryJson,
|
||||
eventsJson: existingEventsJson,
|
||||
@@ -89,7 +93,7 @@ export const useGenerateEvents = ({
|
||||
{ times: 3, backoff: { initialDelay: 200, factor: 2 } },
|
||||
() =>
|
||||
createAiGeneratedEvent(getAuthorizationHeader, {
|
||||
userId: profile.id,
|
||||
userId: activeUserId,
|
||||
gameProjectJsonUserRelativeKey:
|
||||
preparedAiUserContent.gameProjectJsonUserRelativeKey,
|
||||
gameProjectJson: preparedAiUserContent.gameProjectJson,
|
||||
|
||||
@@ -13,6 +13,7 @@ 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';
|
||||
|
||||
type _FuncReturnType = {
|
||||
searchAndInstallAsset: AssetSearchAndInstallOptions => Promise<AssetSearchAndInstallResult>,
|
||||
@@ -49,7 +50,10 @@ export const useSearchAndInstallAsset = ({
|
||||
exactOrPartialAssetId,
|
||||
...assetSearchOptions
|
||||
}: AssetSearchAndInstallOptions): Promise<AssetSearchAndInstallResult> => {
|
||||
if (!profile) throw new Error('User should be authenticated.');
|
||||
if (!profile && !isCustomEndpointEnabled())
|
||||
throw new Error('User should be authenticated.');
|
||||
|
||||
const activeUserId = profile ? profile.id : 'local-byok-user';
|
||||
|
||||
let assetShortHeader;
|
||||
if (exactOrPartialAssetId) {
|
||||
@@ -90,7 +94,7 @@ export const useSearchAndInstallAsset = ({
|
||||
{ times: 3, backoff: { initialDelay: 300, factor: 2 } },
|
||||
() =>
|
||||
createAssetSearch(getAuthorizationHeader, {
|
||||
userId: profile.id,
|
||||
userId: activeUserId,
|
||||
objectType,
|
||||
exactOrPartialAssetId,
|
||||
...assetSearchOptions,
|
||||
|
||||
@@ -14,6 +14,7 @@ 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 PromisePool from '@supercharge/promise-pool';
|
||||
|
||||
@@ -36,9 +37,12 @@ export const useSearchAndInstallResource = ({
|
||||
async ({
|
||||
resources,
|
||||
}: ResourceSearchAndInstallOptions): Promise<ResourceSearchAndInstallResult> => {
|
||||
if (!profile) throw new Error('User should be authenticated.');
|
||||
if (!profile && !isCustomEndpointEnabled())
|
||||
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 { results } = await PromisePool.withConcurrency(5)
|
||||
.for(resources)
|
||||
.process<SingleResourceSearchAndInstallResult>(
|
||||
@@ -47,7 +51,7 @@ export const useSearchAndInstallResource = ({
|
||||
{ times: 3, backoff: { initialDelay: 300, factor: 2 } },
|
||||
() =>
|
||||
createResourceSearch(getAuthorizationHeader, {
|
||||
userId: profile.id,
|
||||
userId: activeUserId,
|
||||
searchTerms: resourceToSearch.resourceName,
|
||||
resourceKind: resourceToSearch.resourceKind,
|
||||
})
|
||||
|
||||
@@ -472,7 +472,145 @@ export type EditorFunctionWithoutProject = {|
|
||||
* Helper function to safely extract required string arguments
|
||||
*/
|
||||
const extractRequiredString = (args: any, propertyName: string): string => {
|
||||
const value = SafeExtractor.extractStringProperty(args, propertyName);
|
||||
if (typeof args === 'string' && args.trim()) {
|
||||
return args.trim();
|
||||
}
|
||||
let value = SafeExtractor.extractStringProperty(args, propertyName);
|
||||
if (
|
||||
value === null &&
|
||||
(propertyName === 'scene_name' || propertyName === 'sceneName')
|
||||
) {
|
||||
value =
|
||||
SafeExtractor.extractStringProperty(args, 'scene_name') ||
|
||||
SafeExtractor.extractStringProperty(args, 'name') ||
|
||||
SafeExtractor.extractStringProperty(args, 'sceneName') ||
|
||||
SafeExtractor.extractStringProperty(args, 'scene');
|
||||
} else if (
|
||||
value === null &&
|
||||
(propertyName === 'object_name' || propertyName === 'objectName')
|
||||
) {
|
||||
value =
|
||||
SafeExtractor.extractStringProperty(args, 'object_name') ||
|
||||
SafeExtractor.extractStringProperty(args, 'name') ||
|
||||
SafeExtractor.extractStringProperty(args, 'objectName') ||
|
||||
SafeExtractor.extractStringProperty(args, 'object');
|
||||
} else if (
|
||||
value === null &&
|
||||
(propertyName === 'behavior_name' || propertyName === 'behaviorName')
|
||||
) {
|
||||
value =
|
||||
SafeExtractor.extractStringProperty(args, 'behavior_name') ||
|
||||
SafeExtractor.extractStringProperty(args, 'name') ||
|
||||
SafeExtractor.extractStringProperty(args, 'behaviorName') ||
|
||||
SafeExtractor.extractStringProperty(args, 'behavior');
|
||||
} else if (
|
||||
value === null &&
|
||||
(propertyName === 'behavior_type' || propertyName === 'behaviorType')
|
||||
) {
|
||||
value =
|
||||
SafeExtractor.extractStringProperty(args, 'behavior_type') ||
|
||||
SafeExtractor.extractStringProperty(args, 'type') ||
|
||||
SafeExtractor.extractStringProperty(args, 'behaviorType');
|
||||
} else if (
|
||||
value === null &&
|
||||
(propertyName === 'layer_name' ||
|
||||
propertyName === 'layerName' ||
|
||||
propertyName === 'layer')
|
||||
) {
|
||||
value =
|
||||
SafeExtractor.extractStringProperty(args, 'layer_name') ||
|
||||
SafeExtractor.extractStringProperty(args, 'layerName') ||
|
||||
SafeExtractor.extractStringProperty(args, 'layer') ||
|
||||
'';
|
||||
} else if (
|
||||
value === null &&
|
||||
(propertyName === 'brush_kind' ||
|
||||
propertyName === 'brushKind' ||
|
||||
propertyName === 'brush')
|
||||
) {
|
||||
value =
|
||||
SafeExtractor.extractStringProperty(args, 'brush_kind') ||
|
||||
SafeExtractor.extractStringProperty(args, 'brushKind') ||
|
||||
SafeExtractor.extractStringProperty(args, 'brush') ||
|
||||
'point';
|
||||
} else if (
|
||||
value === null &&
|
||||
(propertyName === 'variable_scope' ||
|
||||
propertyName === 'variableScope' ||
|
||||
propertyName === 'scope')
|
||||
) {
|
||||
value =
|
||||
SafeExtractor.extractStringProperty(args, 'variable_scope') ||
|
||||
SafeExtractor.extractStringProperty(args, 'scope') ||
|
||||
SafeExtractor.extractStringProperty(args, 'variableScope') ||
|
||||
'global';
|
||||
} else if (
|
||||
value === null &&
|
||||
(propertyName === 'project_name' ||
|
||||
propertyName === 'projectName' ||
|
||||
propertyName === 'game_name' ||
|
||||
propertyName === 'gameName' ||
|
||||
propertyName === 'title' ||
|
||||
propertyName === 'name')
|
||||
) {
|
||||
value =
|
||||
SafeExtractor.extractStringProperty(args, 'project_name') ||
|
||||
SafeExtractor.extractStringProperty(args, 'game_name') ||
|
||||
SafeExtractor.extractStringProperty(args, 'projectName') ||
|
||||
SafeExtractor.extractStringProperty(args, 'gameName') ||
|
||||
SafeExtractor.extractStringProperty(args, 'title') ||
|
||||
SafeExtractor.extractStringProperty(args, 'name') ||
|
||||
'My Project';
|
||||
} else if (
|
||||
value === null &&
|
||||
(propertyName === 'template_slug' ||
|
||||
propertyName === 'templateSlug' ||
|
||||
propertyName === 'template' ||
|
||||
propertyName === 'slug')
|
||||
) {
|
||||
value =
|
||||
SafeExtractor.extractStringProperty(args, 'template_slug') ||
|
||||
SafeExtractor.extractStringProperty(args, 'templateSlug') ||
|
||||
SafeExtractor.extractStringProperty(args, 'template') ||
|
||||
SafeExtractor.extractStringProperty(args, 'slug') ||
|
||||
'';
|
||||
} else if (
|
||||
value === null &&
|
||||
(propertyName === 'test_name' ||
|
||||
propertyName === 'testName' ||
|
||||
propertyName === 'name')
|
||||
) {
|
||||
value =
|
||||
SafeExtractor.extractStringProperty(args, 'test_name') ||
|
||||
SafeExtractor.extractStringProperty(args, 'testName') ||
|
||||
SafeExtractor.extractStringProperty(args, 'name') ||
|
||||
'Test';
|
||||
} else if (
|
||||
value === null &&
|
||||
(propertyName === 'events_description' ||
|
||||
propertyName === 'eventsDescription' ||
|
||||
propertyName === 'description')
|
||||
) {
|
||||
value =
|
||||
SafeExtractor.extractStringProperty(args, 'events_description') ||
|
||||
SafeExtractor.extractStringProperty(args, 'description') ||
|
||||
SafeExtractor.extractStringProperty(args, 'eventsDescription') ||
|
||||
SafeExtractor.extractStringProperty(args, 'events') ||
|
||||
SafeExtractor.extractStringProperty(args, 'prompt');
|
||||
} else if (
|
||||
value === null &&
|
||||
(propertyName === 'query' ||
|
||||
propertyName === 'search' ||
|
||||
propertyName === 'searchTerm' ||
|
||||
propertyName === 'searchTerms')
|
||||
) {
|
||||
value =
|
||||
SafeExtractor.extractStringProperty(args, 'query') ||
|
||||
SafeExtractor.extractStringProperty(args, 'search') ||
|
||||
SafeExtractor.extractStringProperty(args, 'searchTerm') ||
|
||||
SafeExtractor.extractStringProperty(args, 'searchTerms') ||
|
||||
'';
|
||||
}
|
||||
if (value === null) {
|
||||
throw new Error(
|
||||
`Missing or invalid required string argument: ${propertyName}`
|
||||
@@ -3492,10 +3630,23 @@ const put2dInstances: EditorFunction = {
|
||||
);
|
||||
const layer_name = extractRequiredString(args, 'layer_name');
|
||||
const brush_kind = extractRequiredString(args, 'brush_kind');
|
||||
const brush_position = SafeExtractor.extractStringProperty(
|
||||
args,
|
||||
'brush_position'
|
||||
);
|
||||
const rawBrushPos =
|
||||
SafeExtractor.extractStringProperty(args, 'brush_position') ||
|
||||
SafeExtractor.extractStringProperty(args, 'position') ||
|
||||
SafeExtractor.extractStringProperty(args, 'pos') ||
|
||||
(args && args.x !== undefined && args.y !== undefined
|
||||
? `${args.x}, ${args.y}`
|
||||
: null) ||
|
||||
(args && Array.isArray(args.position) && args.position.length >= 2
|
||||
? `${args.position[0]}, ${args.position[1]}`
|
||||
: null) ||
|
||||
(args &&
|
||||
Array.isArray(args.instances) &&
|
||||
args.instances.length > 0 &&
|
||||
args.instances[0].x !== undefined
|
||||
? `${args.instances[0].x}, ${args.instances[0].y}`
|
||||
: null);
|
||||
const brush_position = rawBrushPos;
|
||||
const existing_instance_ids = SafeExtractor.extractStringProperty(
|
||||
args,
|
||||
'existing_instance_ids'
|
||||
@@ -3512,7 +3663,9 @@ const put2dInstances: EditorFunction = {
|
||||
);
|
||||
const newInstancesCount =
|
||||
new_instances_count === null && existingInstanceIds.length === 0
|
||||
? 1
|
||||
? Array.isArray(args.instances)
|
||||
? args.instances.length
|
||||
: 1
|
||||
: new_instances_count;
|
||||
|
||||
const existingInstanceCount = existingInstanceIds.length;
|
||||
@@ -3588,15 +3741,63 @@ const put2dInstances: EditorFunction = {
|
||||
'object_name'
|
||||
);
|
||||
const layer_name = extractRequiredString(args, 'layer_name');
|
||||
|
||||
if (Array.isArray(args.instances) && args.instances.length > 0) {
|
||||
if (!project.hasLayoutNamed(scene_name)) {
|
||||
return makeSceneNotFoundFailure(project, scene_name);
|
||||
}
|
||||
const layout = project.getLayout(scene_name);
|
||||
const initialInstances = layout.getInitialInstances();
|
||||
const createdCount = args.instances.length;
|
||||
for (const instData of args.instances) {
|
||||
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
|
||||
);
|
||||
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);
|
||||
}
|
||||
}
|
||||
onInstancesModifiedOutsideEditor({ scene: layout });
|
||||
return {
|
||||
success: true,
|
||||
message: `Placed ${createdCount} instance(s) in scene "${scene_name}".`,
|
||||
};
|
||||
}
|
||||
|
||||
const requested_brush_kind = extractRequiredString(args, 'brush_kind');
|
||||
const brush_position = SafeExtractor.extractStringProperty(
|
||||
args,
|
||||
'brush_position'
|
||||
);
|
||||
const rawBrushPosition =
|
||||
SafeExtractor.extractStringProperty(args, 'brush_position') ||
|
||||
SafeExtractor.extractStringProperty(args, 'position') ||
|
||||
SafeExtractor.extractStringProperty(args, 'pos') ||
|
||||
SafeExtractor.extractStringProperty(args, 'coordinates') ||
|
||||
(args && args.x !== undefined && args.y !== undefined
|
||||
? `${args.x}, ${args.y}`
|
||||
: null) ||
|
||||
(args && Array.isArray(args.position) && args.position.length >= 2
|
||||
? `${args.position[0]}, ${args.position[1]}`
|
||||
: null);
|
||||
const existing_instance_ids = SafeExtractor.extractStringProperty(
|
||||
args,
|
||||
'existing_instance_ids'
|
||||
);
|
||||
const brush_position =
|
||||
rawBrushPosition ||
|
||||
(requested_brush_kind !== 'none' && !existing_instance_ids
|
||||
? '0, 0'
|
||||
: null);
|
||||
// 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
|
||||
@@ -4384,10 +4585,24 @@ const put3dInstances: EditorFunction = {
|
||||
);
|
||||
const layer_name = extractRequiredString(args, 'layer_name');
|
||||
const brush_kind = extractRequiredString(args, 'brush_kind');
|
||||
const brush_position = SafeExtractor.extractStringProperty(
|
||||
args,
|
||||
'brush_position'
|
||||
);
|
||||
const rawBrushPos =
|
||||
SafeExtractor.extractStringProperty(args, 'brush_position') ||
|
||||
SafeExtractor.extractStringProperty(args, 'position') ||
|
||||
SafeExtractor.extractStringProperty(args, 'pos') ||
|
||||
(args && args.x !== undefined && args.y !== undefined
|
||||
? `${args.x}, ${args.y}, ${args.z || 0}`
|
||||
: null) ||
|
||||
(args && Array.isArray(args.position) && args.position.length >= 3
|
||||
? `${args.position[0]}, ${args.position[1]}, ${args.position[2]}`
|
||||
: null) ||
|
||||
(args &&
|
||||
Array.isArray(args.instances) &&
|
||||
args.instances.length > 0 &&
|
||||
args.instances[0].x !== undefined
|
||||
? `${args.instances[0].x}, ${args.instances[0].y}, ${args.instances[0]
|
||||
.z || 0}`
|
||||
: null);
|
||||
const brush_position = rawBrushPos;
|
||||
const existing_instance_ids = SafeExtractor.extractStringProperty(
|
||||
args,
|
||||
'existing_instance_ids'
|
||||
@@ -4404,7 +4619,9 @@ const put3dInstances: EditorFunction = {
|
||||
);
|
||||
const newInstancesCount =
|
||||
new_instances_count === null && existingInstanceIds.length === 0
|
||||
? 1
|
||||
? Array.isArray(args.instances)
|
||||
? args.instances.length
|
||||
: 1
|
||||
: new_instances_count;
|
||||
|
||||
const existingInstanceCount = existingInstanceIds.length;
|
||||
@@ -4480,15 +4697,65 @@ const put3dInstances: EditorFunction = {
|
||||
'object_name'
|
||||
);
|
||||
const layer_name = extractRequiredString(args, 'layer_name');
|
||||
|
||||
if (Array.isArray(args.instances) && args.instances.length > 0) {
|
||||
if (!project.hasLayoutNamed(scene_name)) {
|
||||
return makeSceneNotFoundFailure(project, scene_name);
|
||||
}
|
||||
const layout = project.getLayout(scene_name);
|
||||
const initialInstances = layout.getInitialInstances();
|
||||
const createdCount = args.instances.length;
|
||||
for (const instData of args.instances) {
|
||||
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
|
||||
);
|
||||
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);
|
||||
}
|
||||
onInstancesModifiedOutsideEditor({ scene: layout });
|
||||
return {
|
||||
success: true,
|
||||
message: `Placed ${createdCount} 3D instance(s) in scene "${scene_name}".`,
|
||||
};
|
||||
}
|
||||
|
||||
const requested_brush_kind = extractRequiredString(args, 'brush_kind');
|
||||
const brush_position = SafeExtractor.extractStringProperty(
|
||||
args,
|
||||
'brush_position'
|
||||
);
|
||||
const rawBrushPosition =
|
||||
SafeExtractor.extractStringProperty(args, 'brush_position') ||
|
||||
SafeExtractor.extractStringProperty(args, 'position') ||
|
||||
SafeExtractor.extractStringProperty(args, 'pos') ||
|
||||
SafeExtractor.extractStringProperty(args, 'coordinates') ||
|
||||
(args && args.x !== undefined && args.y !== undefined
|
||||
? `${args.x}, ${args.y}, ${args.z || 0}`
|
||||
: null) ||
|
||||
(args && Array.isArray(args.position) && args.position.length >= 3
|
||||
? `${args.position[0]}, ${args.position[1]}, ${args.position[2]}`
|
||||
: null);
|
||||
const existing_instance_ids = SafeExtractor.extractStringProperty(
|
||||
args,
|
||||
'existing_instance_ids'
|
||||
);
|
||||
const brush_position =
|
||||
rawBrushPosition ||
|
||||
(requested_brush_kind !== 'none' && !existing_instance_ids
|
||||
? '0, 0, 0'
|
||||
: null);
|
||||
// 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
|
||||
@@ -6115,10 +6382,10 @@ const createScene: EditorFunction = {
|
||||
args,
|
||||
'include_ui_layer'
|
||||
);
|
||||
const background_color = SafeExtractor.extractStringProperty(
|
||||
args,
|
||||
'background_color'
|
||||
);
|
||||
const background_color =
|
||||
SafeExtractor.extractStringProperty(args, 'background_color') ||
|
||||
SafeExtractor.extractStringProperty(args, 'backgroundColor') ||
|
||||
SafeExtractor.extractStringProperty(args, 'color');
|
||||
const is_first_scene = SafeExtractor.extractBooleanProperty(
|
||||
args,
|
||||
'is_first_scene'
|
||||
@@ -6715,10 +6982,27 @@ const changeScenePropertiesLayersEffectsGroups: EditorFunction = {
|
||||
const changes = [];
|
||||
const warnings = [];
|
||||
|
||||
const changed_properties = SafeExtractor.extractArrayProperty(
|
||||
let changed_properties = SafeExtractor.extractArrayProperty(
|
||||
args,
|
||||
'changed_properties'
|
||||
);
|
||||
if (!changed_properties || changed_properties.length === 0) {
|
||||
const syntheticProperties = [];
|
||||
const topLevelBg =
|
||||
SafeExtractor.extractStringProperty(args, 'background_color') ||
|
||||
SafeExtractor.extractStringProperty(args, 'backgroundColor') ||
|
||||
SafeExtractor.extractStringProperty(args, 'bgColor') ||
|
||||
SafeExtractor.extractStringProperty(args, 'color');
|
||||
if (topLevelBg) {
|
||||
syntheticProperties.push({
|
||||
property_name: 'backgroundColor',
|
||||
new_value: topLevelBg,
|
||||
});
|
||||
}
|
||||
if (syntheticProperties.length > 0) {
|
||||
changed_properties = syntheticProperties;
|
||||
}
|
||||
}
|
||||
const changed_layers = SafeExtractor.extractArrayProperty(
|
||||
args,
|
||||
'changed_layers'
|
||||
@@ -6734,14 +7018,21 @@ const changeScenePropertiesLayersEffectsGroups: EditorFunction = {
|
||||
|
||||
if (changed_properties)
|
||||
changed_properties.forEach(changed_property => {
|
||||
const propertyName = SafeExtractor.extractStringProperty(
|
||||
changed_property,
|
||||
'property_name'
|
||||
);
|
||||
const newValue = SafeExtractor.extractStringProperty(
|
||||
changed_property,
|
||||
'new_value'
|
||||
);
|
||||
const propertyName =
|
||||
SafeExtractor.extractStringProperty(
|
||||
changed_property,
|
||||
'property_name'
|
||||
) ||
|
||||
SafeExtractor.extractStringProperty(
|
||||
changed_property,
|
||||
'propertyName'
|
||||
) ||
|
||||
SafeExtractor.extractStringProperty(changed_property, 'name') ||
|
||||
SafeExtractor.extractStringProperty(changed_property, 'property');
|
||||
const newValue =
|
||||
SafeExtractor.extractStringProperty(changed_property, 'new_value') ||
|
||||
SafeExtractor.extractStringProperty(changed_property, 'newValue') ||
|
||||
SafeExtractor.extractStringProperty(changed_property, 'value');
|
||||
if (propertyName === null || newValue === null) {
|
||||
warnings.push(
|
||||
`Missing "property_name" or "new_value" in changed_properties item: ${JSON.stringify(
|
||||
@@ -7946,33 +8237,62 @@ const extractVariableOperations = (
|
||||
|}> => {
|
||||
const variablesArray = SafeExtractor.extractArrayProperty(args, 'variables');
|
||||
if (variablesArray) {
|
||||
return variablesArray.map(variableArgs => ({
|
||||
variable_name_or_path: SafeExtractor.extractStringProperty(
|
||||
variableArgs,
|
||||
'variable_name_or_path'
|
||||
),
|
||||
value: SafeExtractor.extractStringProperty(variableArgs, 'value'),
|
||||
variable_type: SafeExtractor.extractStringProperty(
|
||||
variableArgs,
|
||||
'variable_type'
|
||||
),
|
||||
delete_this_variable:
|
||||
SafeExtractor.extractBooleanProperty(
|
||||
return variablesArray.map(variableArgs => {
|
||||
const varName =
|
||||
SafeExtractor.extractStringProperty(
|
||||
variableArgs,
|
||||
'delete_this_variable'
|
||||
) || false,
|
||||
}));
|
||||
'variable_name_or_path'
|
||||
) ||
|
||||
SafeExtractor.extractStringProperty(variableArgs, 'variable_name') ||
|
||||
SafeExtractor.extractStringProperty(variableArgs, 'name') ||
|
||||
SafeExtractor.extractStringProperty(variableArgs, 'variableName');
|
||||
const varVal =
|
||||
SafeExtractor.extractStringProperty(variableArgs, 'value') !== null
|
||||
? SafeExtractor.extractStringProperty(variableArgs, 'value')
|
||||
: variableArgs && variableArgs.value !== undefined
|
||||
? String(variableArgs.value)
|
||||
: null;
|
||||
const varType =
|
||||
SafeExtractor.extractStringProperty(variableArgs, 'variable_type') ||
|
||||
SafeExtractor.extractStringProperty(variableArgs, 'type') ||
|
||||
SafeExtractor.extractStringProperty(variableArgs, 'variableType');
|
||||
return {
|
||||
variable_name_or_path: varName,
|
||||
value: varVal,
|
||||
variable_type: varType,
|
||||
delete_this_variable:
|
||||
SafeExtractor.extractBooleanProperty(
|
||||
variableArgs,
|
||||
'delete_this_variable'
|
||||
) || false,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const varName =
|
||||
SafeExtractor.extractStringProperty(args, 'variable_name_or_path') ||
|
||||
SafeExtractor.extractStringProperty(args, 'variable_name') ||
|
||||
SafeExtractor.extractStringProperty(args, 'name') ||
|
||||
SafeExtractor.extractStringProperty(args, 'variableName');
|
||||
const varVal =
|
||||
SafeExtractor.extractStringProperty(args, 'value') !== null
|
||||
? SafeExtractor.extractStringProperty(args, 'value')
|
||||
: args && args.value !== undefined
|
||||
? String(args.value)
|
||||
: null;
|
||||
const varType =
|
||||
SafeExtractor.extractStringProperty(args, 'variable_type') ||
|
||||
SafeExtractor.extractStringProperty(args, 'type') ||
|
||||
SafeExtractor.extractStringProperty(args, 'variableType');
|
||||
|
||||
return [
|
||||
{
|
||||
variable_name_or_path: SafeExtractor.extractStringProperty(
|
||||
args,
|
||||
'variable_name_or_path'
|
||||
),
|
||||
value: SafeExtractor.extractStringProperty(args, 'value'),
|
||||
variable_type: SafeExtractor.extractStringProperty(args, 'variable_type'),
|
||||
delete_this_variable: false,
|
||||
variable_name_or_path: varName,
|
||||
value: varVal,
|
||||
variable_type: varType,
|
||||
delete_this_variable:
|
||||
SafeExtractor.extractBooleanProperty(args, 'delete_this_variable') ||
|
||||
false,
|
||||
},
|
||||
];
|
||||
};
|
||||
@@ -8820,12 +9140,20 @@ const runScript: EditorFunction = {
|
||||
},
|
||||
launchFunction: async ({ args, project, ...launchOptions }) => {
|
||||
const jsCode =
|
||||
args && typeof args.js_code === 'string' ? args.js_code : null;
|
||||
args && typeof args.js_code === 'string'
|
||||
? args.js_code
|
||||
: args && typeof args.script === 'string'
|
||||
? args.script
|
||||
: args && typeof args.code === 'string'
|
||||
? args.code
|
||||
: args && typeof args.javascript === 'string'
|
||||
? args.javascript
|
||||
: null;
|
||||
if (!jsCode) {
|
||||
return {
|
||||
success: false,
|
||||
message:
|
||||
'run_script requires a `js_code` string argument (the JavaScript to run).',
|
||||
'run_script requires a `js_code` or `script` string argument (the JavaScript to run).',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -256,6 +256,11 @@ export type PreferencesValues = {|
|
||||
disableNpmScriptConfirmation: boolean,
|
||||
showJsTypeError: boolean,
|
||||
canonicalEventSerialization: boolean,
|
||||
aiCustomEndpointEnabled: boolean,
|
||||
aiCustomBaseUrl: string,
|
||||
aiCustomApiKey: string,
|
||||
aiCustomModel: string,
|
||||
aiCustomTemperature: number,
|
||||
|};
|
||||
|
||||
/**
|
||||
@@ -384,6 +389,11 @@ export type Preferences = {|
|
||||
setUseBackgroundSerializerForSaving: (enabled: boolean) => void,
|
||||
setShowJsTypeError: (enabled: boolean) => void,
|
||||
setCanonicalEventSerialization: (enabled: boolean) => void,
|
||||
setAiCustomEndpointEnabled: (enabled: boolean) => void,
|
||||
setAiCustomBaseUrl: (baseUrl: string) => void,
|
||||
setAiCustomApiKey: (apiKey: string) => void,
|
||||
setAiCustomModel: (model: string) => void,
|
||||
setAiCustomTemperature: (temperature: number) => void,
|
||||
|};
|
||||
|
||||
export const initialPreferences = {
|
||||
@@ -451,6 +461,11 @@ export const initialPreferences = {
|
||||
disableNpmScriptConfirmation: false,
|
||||
showJsTypeError: false,
|
||||
canonicalEventSerialization: false,
|
||||
aiCustomEndpointEnabled: false,
|
||||
aiCustomBaseUrl: 'http://localhost:11434/v1',
|
||||
aiCustomApiKey: '',
|
||||
aiCustomModel: 'qwen2.5-coder',
|
||||
aiCustomTemperature: 0.7,
|
||||
},
|
||||
setMultipleValues: () => {},
|
||||
setLanguage: () => {},
|
||||
@@ -542,6 +557,11 @@ export const initialPreferences = {
|
||||
setUseBackgroundSerializerForSaving: (enabled: boolean) => {},
|
||||
setShowJsTypeError: (enabled: boolean) => {},
|
||||
setCanonicalEventSerialization: (enabled: boolean) => {},
|
||||
setAiCustomEndpointEnabled: (enabled: boolean) => {},
|
||||
setAiCustomBaseUrl: (baseUrl: string) => {},
|
||||
setAiCustomApiKey: (apiKey: string) => {},
|
||||
setAiCustomModel: (model: string) => {},
|
||||
setAiCustomTemperature: (temperature: number) => {},
|
||||
};
|
||||
|
||||
const PreferencesContext: React.Context<Preferences> = React.createContext<Preferences>(
|
||||
|
||||
@@ -24,6 +24,9 @@ import { useResponsiveWindowSize } from '../../UI/Responsive/ResponsiveWindowMea
|
||||
import { adaptAcceleratorString } from '../../UI/AcceleratorString';
|
||||
import { getElectronAccelerator } from '../../KeyboardShortcuts';
|
||||
import defaultShortcuts from '../../KeyboardShortcuts/DefaultShortcuts';
|
||||
import RaisedButton from '../../UI/RaisedButton';
|
||||
import TextField from '../../UI/TextField';
|
||||
import { testConnection } from '../../AI/CustomAIClient';
|
||||
import AlertMessage from '../../UI/AlertMessage';
|
||||
import ErrorBoundary from '../../UI/ErrorBoundary';
|
||||
import CompactSelectField from '../../UI/CompactSelectField';
|
||||
@@ -90,8 +93,23 @@ const PreferencesDialog = ({
|
||||
setDisableNpmScriptConfirmation,
|
||||
setUseBackgroundSerializerForSaving,
|
||||
setShowJsTypeError,
|
||||
setAiCustomEndpointEnabled,
|
||||
setAiCustomBaseUrl,
|
||||
setAiCustomApiKey,
|
||||
setAiCustomModel,
|
||||
setAiCustomTemperature,
|
||||
} = React.useContext(PreferencesContext);
|
||||
|
||||
const [
|
||||
isTestingAiConnection,
|
||||
setIsTestingAiConnection,
|
||||
] = React.useState<boolean>(false);
|
||||
const [aiTestResult, setAiTestResult] = React.useState<null | {|
|
||||
success: boolean,
|
||||
message: string,
|
||||
models?: Array<string>,
|
||||
|}>(null);
|
||||
|
||||
const initialUse3DEditor = React.useRef<boolean>(values.use3DEditor);
|
||||
|
||||
return (
|
||||
@@ -114,6 +132,7 @@ const PreferencesDialog = ({
|
||||
onChange={setCurrentTab}
|
||||
options={[
|
||||
{ value: 'preferences', label: <Trans>Preferences</Trans> },
|
||||
{ value: 'ai', label: <Trans>AI Settings</Trans> },
|
||||
{ value: 'shortcuts', label: <Trans>Keyboard Shortcuts</Trans> },
|
||||
...(electron
|
||||
? [{ value: 'folders', label: <Trans>Folders</Trans> }]
|
||||
@@ -694,6 +713,93 @@ const PreferencesDialog = ({
|
||||
)}
|
||||
</ColumnStackLayout>
|
||||
)}
|
||||
{currentTab === 'ai' && (
|
||||
<ColumnStackLayout noMargin>
|
||||
<Text size="block-title">
|
||||
<Trans>Custom AI Provider (BYOK)</Trans>
|
||||
</Text>
|
||||
<Text size="body" color="secondary">
|
||||
<Trans>
|
||||
Connect GDevelop to a local AI server (such as Ollama, llama.cpp,
|
||||
LM Studio, LocalAI) or any OpenAI-compatible API endpoint.
|
||||
</Trans>
|
||||
</Text>
|
||||
<CompactToggleField
|
||||
checked={values.aiCustomEndpointEnabled}
|
||||
onCheck={setAiCustomEndpointEnabled}
|
||||
label={<Trans>Enable Custom / Local AI Endpoint (BYOK)</Trans>}
|
||||
/>
|
||||
<TextField
|
||||
floatingLabelText={<Trans>Base URL</Trans>}
|
||||
helperMarkdownText={i18n._(
|
||||
t`Examples: \`http://localhost:11434/v1\` (Ollama), \`http://localhost:8080/v1\` (llama.cpp), \`http://localhost:1234/v1\` (LM Studio), \`https://api.openai.com/v1\`, \`https://openrouter.ai/api/v1\``
|
||||
)}
|
||||
fullWidth
|
||||
value={values.aiCustomBaseUrl}
|
||||
onChange={(e, value) => setAiCustomBaseUrl(value)}
|
||||
/>
|
||||
<TextField
|
||||
type="password"
|
||||
floatingLabelText={<Trans>API Key</Trans>}
|
||||
helperMarkdownText={i18n._(
|
||||
t`Bearer token for authentication. Leave empty for local servers (Ollama / llama.cpp).`
|
||||
)}
|
||||
fullWidth
|
||||
value={values.aiCustomApiKey}
|
||||
onChange={(e, value) => setAiCustomApiKey(value)}
|
||||
/>
|
||||
<TextField
|
||||
floatingLabelText={<Trans>Model Identifier</Trans>}
|
||||
helperMarkdownText={i18n._(
|
||||
t`Examples: \`qwen2.5-coder\`, \`gpt-4o\`, \`llama3.2\`, \`deepseek-chat\`, \`claude-3-5-sonnet\`, \`openrouter/poolside/laguna-s-2.1:free\``
|
||||
)}
|
||||
fullWidth
|
||||
value={values.aiCustomModel}
|
||||
onChange={(e, value) => setAiCustomModel(value)}
|
||||
/>
|
||||
<TextField
|
||||
type="number"
|
||||
floatingLabelText={<Trans>Temperature (0.0 to 1.0)</Trans>}
|
||||
value={values.aiCustomTemperature}
|
||||
onChange={(e, value) =>
|
||||
setAiCustomTemperature(parseFloat(value) || 0.7)
|
||||
}
|
||||
/>
|
||||
<LineStackLayout alignItems="center">
|
||||
<RaisedButton
|
||||
label={<Trans>Test Connection</Trans>}
|
||||
primary
|
||||
disabled={isTestingAiConnection}
|
||||
onClick={async () => {
|
||||
setIsTestingAiConnection(true);
|
||||
setAiTestResult(null);
|
||||
try {
|
||||
const res = await testConnection({
|
||||
enabled: true,
|
||||
baseUrl: values.aiCustomBaseUrl,
|
||||
apiKey: values.aiCustomApiKey,
|
||||
model: values.aiCustomModel,
|
||||
temperature: values.aiCustomTemperature,
|
||||
});
|
||||
setAiTestResult(res);
|
||||
} catch (err) {
|
||||
setAiTestResult({
|
||||
success: false,
|
||||
message: err.message || 'Connection test failed',
|
||||
});
|
||||
} finally {
|
||||
setIsTestingAiConnection(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</LineStackLayout>
|
||||
{aiTestResult && (
|
||||
<AlertMessage kind={aiTestResult.success ? 'info' : 'error'}>
|
||||
{aiTestResult.message}
|
||||
</AlertMessage>
|
||||
)}
|
||||
</ColumnStackLayout>
|
||||
)}
|
||||
{currentTab === 'shortcuts' && (
|
||||
<Line expand>
|
||||
<Column expand noMargin>
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
CHECK_APP_UPDATES_TIMEOUT,
|
||||
PERIODIC_APP_UPDATES_TIMEOUT,
|
||||
} from '../../Utils/GlobalFetchTimeouts';
|
||||
import { setCustomEndpointConfig } from '../../AI/CustomAIClient';
|
||||
const electron = optionalRequire('electron');
|
||||
const ipcRenderer = electron ? electron.ipcRenderer : null;
|
||||
|
||||
@@ -417,9 +418,30 @@ export default class PreferencesProvider extends React.Component<Props, State> {
|
||||
setCanonicalEventSerialization: (this._setCanonicalEventSerialization.bind(
|
||||
this
|
||||
): any),
|
||||
// $FlowFixMe[method-unbinding]
|
||||
setAiCustomEndpointEnabled: (this._setAiCustomEndpointEnabled.bind(
|
||||
this
|
||||
): any),
|
||||
// $FlowFixMe[method-unbinding]
|
||||
setAiCustomBaseUrl: (this._setAiCustomBaseUrl.bind(this): any),
|
||||
// $FlowFixMe[method-unbinding]
|
||||
setAiCustomApiKey: (this._setAiCustomApiKey.bind(this): any),
|
||||
// $FlowFixMe[method-unbinding]
|
||||
setAiCustomModel: (this._setAiCustomModel.bind(this): any),
|
||||
// $FlowFixMe[method-unbinding]
|
||||
setAiCustomTemperature: (this._setAiCustomTemperature.bind(this): any),
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
// Sync custom AI config on mount
|
||||
setCustomEndpointConfig({
|
||||
enabled: this.state.values.aiCustomEndpointEnabled,
|
||||
baseUrl: this.state.values.aiCustomBaseUrl,
|
||||
apiKey: this.state.values.aiCustomApiKey,
|
||||
model: this.state.values.aiCustomModel,
|
||||
temperature: this.state.values.aiCustomTemperature,
|
||||
});
|
||||
|
||||
this._periodicUpdateCheckTimeout = setTimeout(
|
||||
() => this._checkUpdates(),
|
||||
CHECK_APP_UPDATES_TIMEOUT
|
||||
@@ -1450,6 +1472,81 @@ export default class PreferencesProvider extends React.Component<Props, State> {
|
||||
);
|
||||
}
|
||||
|
||||
_setAiCustomEndpointEnabled(newValue: boolean) {
|
||||
this.setState(
|
||||
state => ({
|
||||
values: {
|
||||
...state.values,
|
||||
aiCustomEndpointEnabled: newValue,
|
||||
},
|
||||
}),
|
||||
() => {
|
||||
this._persistValuesToLocalStorage(this.state);
|
||||
setCustomEndpointConfig({ enabled: newValue });
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
_setAiCustomBaseUrl(newValue: string) {
|
||||
this.setState(
|
||||
state => ({
|
||||
values: {
|
||||
...state.values,
|
||||
aiCustomBaseUrl: newValue,
|
||||
},
|
||||
}),
|
||||
() => {
|
||||
this._persistValuesToLocalStorage(this.state);
|
||||
setCustomEndpointConfig({ baseUrl: newValue });
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
_setAiCustomApiKey(newValue: string) {
|
||||
this.setState(
|
||||
state => ({
|
||||
values: {
|
||||
...state.values,
|
||||
aiCustomApiKey: newValue,
|
||||
},
|
||||
}),
|
||||
() => {
|
||||
this._persistValuesToLocalStorage(this.state);
|
||||
setCustomEndpointConfig({ apiKey: newValue });
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
_setAiCustomModel(newValue: string) {
|
||||
this.setState(
|
||||
state => ({
|
||||
values: {
|
||||
...state.values,
|
||||
aiCustomModel: newValue,
|
||||
},
|
||||
}),
|
||||
() => {
|
||||
this._persistValuesToLocalStorage(this.state);
|
||||
setCustomEndpointConfig({ model: newValue });
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
_setAiCustomTemperature(newValue: number) {
|
||||
this.setState(
|
||||
state => ({
|
||||
values: {
|
||||
...state.values,
|
||||
aiCustomTemperature: newValue,
|
||||
},
|
||||
}),
|
||||
() => {
|
||||
this._persistValuesToLocalStorage(this.state);
|
||||
setCustomEndpointConfig({ temperature: newValue });
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
render(): any {
|
||||
return (
|
||||
<PreferencesContext.Provider value={this.state}>
|
||||
|
||||
@@ -18,6 +18,7 @@ import { showErrorBox } from '../../UI/Messages/MessageBox';
|
||||
import { type CommunityLinks, type UserSurvey } from './User';
|
||||
import { userCancellationErrorName } from '../../LoginProvider/Utils';
|
||||
import { ensureIsObject, ensureObjectHasProperty } from '../DataValidator';
|
||||
import { isCustomEndpointEnabled } from '../../AI/CustomAIClient';
|
||||
|
||||
export type Profile = {|
|
||||
id: string,
|
||||
@@ -577,13 +578,18 @@ export default class Authentication {
|
||||
|
||||
getAuthorizationHeader = async (): Promise<string> => {
|
||||
const { currentUser } = this.auth;
|
||||
if (!currentUser) throw new Error('User is not authenticated.');
|
||||
if (!currentUser) {
|
||||
if (isCustomEndpointEnabled()) {
|
||||
return 'Bearer local-byok-token';
|
||||
}
|
||||
throw new Error('User is not authenticated.');
|
||||
}
|
||||
|
||||
return currentUser.getIdToken().then(token => `Bearer ${token}`);
|
||||
};
|
||||
|
||||
isAuthenticated = (): boolean => {
|
||||
return !!this.auth.currentUser;
|
||||
return !!this.auth.currentUser || isCustomEndpointEnabled();
|
||||
};
|
||||
|
||||
waitForInitialAuthCheck = (): Promise<void> => {
|
||||
|
||||
@@ -9,6 +9,21 @@ import {
|
||||
ensureIsObject,
|
||||
ensureObjectHasProperty,
|
||||
} from '../DataValidator';
|
||||
import {
|
||||
isCustomEndpointEnabled,
|
||||
customCreateAiRequest,
|
||||
customAddMessageToAiRequest,
|
||||
customGetAiRequest,
|
||||
customGetAiRequests,
|
||||
customGetAiRequestStatuses,
|
||||
customSuspendAiRequest,
|
||||
customForkAiRequest,
|
||||
customGetAiRequestSuggestions,
|
||||
customCreateAiGeneratedEvent,
|
||||
customCreateAssetSearch,
|
||||
customCreateResourceSearch,
|
||||
DEFAULT_LOCAL_AI_SETTINGS,
|
||||
} from '../../AI/CustomAIClient';
|
||||
|
||||
export type Environment = 'staging' | 'live';
|
||||
|
||||
@@ -297,6 +312,10 @@ export const getAiRequest = async (
|
||||
outputFromMessageId?: ?string,
|
||||
|}
|
||||
): Promise<AiRequest> => {
|
||||
if (isCustomEndpointEnabled() || aiRequestId.startsWith('local-ai-')) {
|
||||
return customGetAiRequest(aiRequestId);
|
||||
}
|
||||
|
||||
const authorizationHeader = await getAuthorizationHeader();
|
||||
// $FlowFixMe[underconstrained-implicit-instantiation]
|
||||
const response = await axios.get(
|
||||
@@ -337,6 +356,13 @@ export const getAiRequestStatuses = async (
|
||||
> => {
|
||||
if (aiRequestIds.length === 0) return [];
|
||||
|
||||
if (
|
||||
isCustomEndpointEnabled() ||
|
||||
aiRequestIds.every(id => id.startsWith('local-ai-'))
|
||||
) {
|
||||
return customGetAiRequestStatuses(aiRequestIds);
|
||||
}
|
||||
|
||||
const authorizationHeader = await getAuthorizationHeader();
|
||||
// $FlowFixMe[underconstrained-implicit-instantiation]
|
||||
const response = await axios.get(
|
||||
@@ -371,6 +397,10 @@ export const getAiRequests = async (
|
||||
aiRequests: Array<AiRequest>,
|
||||
nextPageUri: ?string,
|
||||
}> => {
|
||||
if (isCustomEndpointEnabled()) {
|
||||
return customGetAiRequests();
|
||||
}
|
||||
|
||||
const authorizationHeader = await getAuthorizationHeader();
|
||||
const uri = forceUri || '/ai-request';
|
||||
|
||||
@@ -432,6 +462,17 @@ export const createAiRequest = async (
|
||||
toolsVersion: string,
|
||||
|}
|
||||
): Promise<AiRequest> => {
|
||||
if (isCustomEndpointEnabled()) {
|
||||
return customCreateAiRequest({
|
||||
userRequest,
|
||||
gameProjectJson,
|
||||
projectSpecificExtensionsSummaryJson,
|
||||
mode,
|
||||
aiConfiguration,
|
||||
gameId,
|
||||
});
|
||||
}
|
||||
|
||||
const authorizationHeader = await getAuthorizationHeader();
|
||||
const response = await apiClient.post(
|
||||
'/ai-request',
|
||||
@@ -502,6 +543,17 @@ export const addMessageToAiRequest = async (
|
||||
toolsVersion?: string,
|
||||
|}
|
||||
): Promise<AiRequest> => {
|
||||
if (isCustomEndpointEnabled() || aiRequestId.startsWith('local-ai-')) {
|
||||
return customAddMessageToAiRequest({
|
||||
aiRequestId,
|
||||
userMessage,
|
||||
functionCallOutputs,
|
||||
gameProjectJson,
|
||||
projectSpecificExtensionsSummaryJson,
|
||||
mode,
|
||||
});
|
||||
}
|
||||
|
||||
const authorizationHeader = await getAuthorizationHeader();
|
||||
const response = await apiClient.post(
|
||||
`/ai-request/${aiRequestId}/action/add-message`,
|
||||
@@ -541,6 +593,10 @@ export const suspendAiRequest = async (
|
||||
getAuthorizationHeader: () => Promise<string>,
|
||||
{ userId, aiRequestId }: {| userId: string, aiRequestId: string |}
|
||||
): Promise<AiRequest> => {
|
||||
if (isCustomEndpointEnabled() || aiRequestId.startsWith('local-ai-')) {
|
||||
return customSuspendAiRequest(aiRequestId);
|
||||
}
|
||||
|
||||
const authorizationHeader = await getAuthorizationHeader();
|
||||
const response = await apiClient.post(
|
||||
`/ai-request/${aiRequestId}/action/suspend`,
|
||||
@@ -570,6 +626,9 @@ export const updateAiRequestMessage = async (
|
||||
projectVersionIdAfterMessage?: ?string,
|
||||
|}
|
||||
): Promise<void> => {
|
||||
if (isCustomEndpointEnabled() || aiRequestId.startsWith('local-ai-')) {
|
||||
return;
|
||||
}
|
||||
const authorizationHeader = await getAuthorizationHeader();
|
||||
await apiClient.patch(
|
||||
`/ai-request/${aiRequestId}/message/${aiRequestMessageId}`,
|
||||
@@ -652,6 +711,10 @@ export const getAiRequestSuggestions = async (
|
||||
projectSpecificExtensionsSummaryJsonUserRelativeKey: string | null,
|
||||
|}
|
||||
): Promise<AiRequest> => {
|
||||
if (isCustomEndpointEnabled() || aiRequestId.startsWith('local-ai-')) {
|
||||
return customGetAiRequestSuggestions(aiRequestId);
|
||||
}
|
||||
|
||||
const authorizationHeader = await getAuthorizationHeader();
|
||||
const response = await apiClient.post(
|
||||
`/ai-request/${aiRequestId}/action/get-suggestions`,
|
||||
@@ -727,6 +790,17 @@ export const createAiGeneratedEvent = async (
|
||||
estimatedComplexity: number | null,
|
||||
|}
|
||||
): Promise<CreateAiGeneratedEventResult> => {
|
||||
if (isCustomEndpointEnabled()) {
|
||||
return customCreateAiGeneratedEvent({
|
||||
sceneName,
|
||||
eventsDescription,
|
||||
eventBatches,
|
||||
extensionNamesList,
|
||||
objectsList,
|
||||
existingEventsAsText,
|
||||
});
|
||||
}
|
||||
|
||||
const authorizationHeader = await getAuthorizationHeader();
|
||||
const response = await apiClient.post(
|
||||
`/ai-generated-event`,
|
||||
@@ -795,6 +869,31 @@ export const getAiGeneratedEvent = async (
|
||||
aiGeneratedEventId: string,
|
||||
|}
|
||||
): Promise<AiGeneratedEvent> => {
|
||||
if (
|
||||
isCustomEndpointEnabled() ||
|
||||
aiGeneratedEventId.startsWith('local-evt-')
|
||||
) {
|
||||
return {
|
||||
id: aiGeneratedEventId,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
userId: 'local-byok-user',
|
||||
status: 'ready',
|
||||
partialGameProjectJson: '',
|
||||
eventsDescription: null,
|
||||
eventBatches: null,
|
||||
extensionNamesList: '',
|
||||
objectsList: '',
|
||||
existingEventsAsText: '',
|
||||
existingEventsJson: null,
|
||||
existingEventsJsonUserRelativeKey: null,
|
||||
resultMessage: null,
|
||||
changes: [],
|
||||
error: null,
|
||||
stats: null,
|
||||
};
|
||||
}
|
||||
|
||||
const authorizationHeader = await getAuthorizationHeader();
|
||||
const response = await apiClient.get(
|
||||
`/ai-generated-event/${aiGeneratedEventId}`,
|
||||
@@ -838,6 +937,10 @@ export const createAssetSearch = async (
|
||||
lastAssistantMessages?: string[],
|
||||
|}
|
||||
): Promise<AssetSearch> => {
|
||||
if (isCustomEndpointEnabled()) {
|
||||
return customCreateAssetSearch({ searchTerms, objectType });
|
||||
}
|
||||
|
||||
const authorizationHeader = await getAuthorizationHeader();
|
||||
const response = await apiClient.post(
|
||||
`/asset-search`,
|
||||
@@ -880,6 +983,10 @@ export const createResourceSearch = async (
|
||||
resourceKind: string,
|
||||
|}
|
||||
): Promise<ResourceSearch> => {
|
||||
if (isCustomEndpointEnabled()) {
|
||||
return customCreateResourceSearch({ searchTerms, resourceKind });
|
||||
}
|
||||
|
||||
const authorizationHeader = await getAuthorizationHeader();
|
||||
const response = await apiClient.post(
|
||||
`/resource-search`,
|
||||
@@ -927,6 +1034,16 @@ export const createAiUserContentPresignedUrls = async (
|
||||
eventsJsonHash: string | null,
|
||||
|}
|
||||
): Promise<AiUserContentPresignedUrlsResult> => {
|
||||
if (isCustomEndpointEnabled() || (userId && userId.startsWith('local-'))) {
|
||||
return {
|
||||
gameProjectJsonSignedUrl: undefined,
|
||||
gameProjectJsonUserRelativeKey: undefined,
|
||||
projectSpecificExtensionsSummaryJsonSignedUrl: undefined,
|
||||
projectSpecificExtensionsSummaryJsonUserRelativeKey: undefined,
|
||||
eventsJsonSignedUrl: undefined,
|
||||
eventsJsonUserRelativeKey: undefined,
|
||||
};
|
||||
}
|
||||
const authorizationHeader = await getAuthorizationHeader();
|
||||
const response = await apiClient.post(
|
||||
`/ai-user-content/action/create-presigned-urls`,
|
||||
@@ -980,6 +1097,10 @@ export const forkAiRequest = async (
|
||||
upToMessageId?: string,
|
||||
|}
|
||||
): Promise<AiRequest> => {
|
||||
if (isCustomEndpointEnabled() || aiRequestId.startsWith('local-ai-')) {
|
||||
return customForkAiRequest(aiRequestId, upToMessageId);
|
||||
}
|
||||
|
||||
const authorizationHeader = await getAuthorizationHeader();
|
||||
const response = await apiClient.post(
|
||||
`/ai-request/${aiRequestId}/action/fork`,
|
||||
@@ -1007,13 +1128,18 @@ export const fetchAiSettings = async ({
|
||||
}: {|
|
||||
environment: Environment,
|
||||
|}): Promise<AiSettings> => {
|
||||
// $FlowFixMe[underconstrained-implicit-instantiation]
|
||||
const response = await axios.get(
|
||||
`${GDevelopAiCdn.baseUrl[environment]}/ai-settings-v2.json`
|
||||
);
|
||||
return ensureObjectHasProperty({
|
||||
data: response.data,
|
||||
propertyName: 'aiRequest',
|
||||
endpointName: '/ai-settings-v2.json of Generation API',
|
||||
});
|
||||
try {
|
||||
// $FlowFixMe[underconstrained-implicit-instantiation]
|
||||
const response = await axios.get(
|
||||
`${GDevelopAiCdn.baseUrl[environment]}/ai-settings-v2.json`,
|
||||
{ timeout: 5000 }
|
||||
);
|
||||
return ensureObjectHasProperty({
|
||||
data: response.data,
|
||||
propertyName: 'aiRequest',
|
||||
endpointName: '/ai-settings-v2.json of Generation API',
|
||||
});
|
||||
} catch (err) {
|
||||
return DEFAULT_LOCAL_AI_SETTINGS;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
ensureIsObjectWithPropertyOfType,
|
||||
} from '../DataValidator';
|
||||
import { type BundleListingData } from './Shop';
|
||||
import { isCustomEndpointEnabled } from '../../AI/CustomAIClient';
|
||||
|
||||
export type Usage = {
|
||||
id: string,
|
||||
@@ -583,6 +584,10 @@ export const isSubscriptionComingFromTeam = (
|
||||
export const hasValidSubscriptionPlan = (
|
||||
subscription: ?Subscription
|
||||
): boolean => {
|
||||
if (isCustomEndpointEnabled()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const hasValidSubscription =
|
||||
!!subscription &&
|
||||
!!subscription.planId &&
|
||||
@@ -758,6 +763,10 @@ 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