insforge 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dockerignore +58 -0
- package/.env.example +49 -0
- package/.github/ISSUE_TEMPLATE/bug_report.yml +83 -0
- package/.github/ISSUE_TEMPLATE/config.yml +11 -0
- package/.github/ISSUE_TEMPLATE/feature_request.yml +79 -0
- package/.github/copilot-instructions.md +147 -0
- package/.github/workflows/build-image.yml +65 -0
- package/.github/workflows/ci-premerge-check.yml +24 -0
- package/.github/workflows/deploy-aws.yml +130 -0
- package/.github/workflows/lint-and-format.yml +33 -0
- package/.prettierignore +65 -0
- package/.prettierrc +9 -0
- package/CHANGELOG.md +3 -0
- package/CONTRIBUTING.md +126 -0
- package/Dockerfile +27 -0
- package/GITHUB_OAUTH_SETUP.md +49 -0
- package/GOOGLE_OAUTH_SETUP.md +148 -0
- package/LICENSE +201 -0
- package/README.md +134 -0
- package/assets/Dark.svg +23 -0
- package/assets/archDiagram.png +0 -0
- package/assets/banner.png +0 -0
- package/assets/mcpInstallv2.png +0 -0
- package/assets/sampleResponse.png +0 -0
- package/assets/signin.png +0 -0
- package/assets/userflow.png +0 -0
- package/backend/migrations/000_create-base-tables.sql +142 -0
- package/backend/migrations/001_create-helper-functions.sql +41 -0
- package/backend/migrations/002_rename-auth-tables.sql +30 -0
- package/backend/migrations/003_create-users-table.sql +56 -0
- package/backend/migrations/004_add-reload-postgrest-func.sql +24 -0
- package/backend/migrations/005_enable-project-admin-modify-users.sql +30 -0
- package/backend/migrations/006_modify-ai-usage-table.sql +25 -0
- package/backend/migrations/007_drop-metadata-table.sql +2 -0
- package/backend/migrations/008_add-system-tables.sql +77 -0
- package/backend/migrations/009_add-function-secrets.sql +24 -0
- package/backend/migrations/010_modify-ai-config-modalities.sql +93 -0
- package/backend/migrations/011_refactor-secrets-table.sql +15 -0
- package/backend/migrations/012_add-storage-uploaded-by.sql +8 -0
- package/backend/package.json +75 -0
- package/backend/src/api/middleware/auth.ts +240 -0
- package/backend/src/api/middleware/error.ts +231 -0
- package/backend/src/api/middleware/upload.ts +59 -0
- package/backend/src/api/routes/agent.ts +29 -0
- package/backend/src/api/routes/ai.ts +472 -0
- package/backend/src/api/routes/auth.oauth.ts +482 -0
- package/backend/src/api/routes/auth.ts +386 -0
- package/backend/src/api/routes/database.advance.ts +275 -0
- package/backend/src/api/routes/database.records.ts +246 -0
- package/backend/src/api/routes/database.tables.ts +161 -0
- package/backend/src/api/routes/docs.ts +66 -0
- package/backend/src/api/routes/functions.ts +183 -0
- package/backend/src/api/routes/logs.ts +150 -0
- package/backend/src/api/routes/metadata.ts +160 -0
- package/backend/src/api/routes/openapi.ts +82 -0
- package/backend/src/api/routes/secrets.ts +199 -0
- package/backend/src/api/routes/storage.ts +547 -0
- package/backend/src/api/routes/usage.ts +96 -0
- package/backend/src/core/ai/chat.ts +207 -0
- package/backend/src/core/ai/client.ts +242 -0
- package/backend/src/core/ai/config.ts +187 -0
- package/backend/src/core/ai/image.ts +156 -0
- package/backend/src/core/ai/model.ts +117 -0
- package/backend/src/core/ai/usage.ts +290 -0
- package/backend/src/core/auth/auth.ts +781 -0
- package/backend/src/core/auth/oauth.ts +398 -0
- package/backend/src/core/database/advance.ts +1074 -0
- package/backend/src/core/database/manager.ts +178 -0
- package/backend/src/core/database/table.ts +772 -0
- package/backend/src/core/documentation/agent.ts +689 -0
- package/backend/src/core/documentation/openapi.ts +856 -0
- package/backend/src/core/functions/functions.ts +310 -0
- package/backend/src/core/logs/analytics.ts +76 -0
- package/backend/src/core/logs/audit.ts +255 -0
- package/backend/src/core/logs/providers/base.provider.ts +83 -0
- package/backend/src/core/logs/providers/cloudwatch.provider.ts +510 -0
- package/backend/src/core/logs/providers/localdb.provider.ts +246 -0
- package/backend/src/core/secrets/encryption.ts +58 -0
- package/backend/src/core/secrets/secrets.ts +410 -0
- package/backend/src/core/socket/socket.ts +388 -0
- package/backend/src/core/socket/types.ts +79 -0
- package/backend/src/core/storage/storage.ts +923 -0
- package/backend/src/server.ts +288 -0
- package/backend/src/types/ai.ts +46 -0
- package/backend/src/types/auth.ts +90 -0
- package/backend/src/types/database.ts +136 -0
- package/backend/src/types/error-constants.ts +86 -0
- package/backend/src/types/logs.ts +47 -0
- package/backend/src/types/profile.ts +55 -0
- package/backend/src/types/storage.ts +23 -0
- package/backend/src/utils/cloud-token.ts +39 -0
- package/backend/src/utils/constants.ts +1 -0
- package/backend/src/utils/environment.ts +35 -0
- package/backend/src/utils/helpers.ts +49 -0
- package/backend/src/utils/logger.ts +13 -0
- package/backend/src/utils/response.ts +62 -0
- package/backend/src/utils/seed.ts +205 -0
- package/backend/src/utils/sql-parser.ts +63 -0
- package/backend/src/utils/uuid.ts +9 -0
- package/backend/src/utils/validations.ts +129 -0
- package/backend/tests/README.md +134 -0
- package/backend/tests/cleanup-all-test-data.sh +231 -0
- package/backend/tests/cloud/test-s3-multitenant.sh +132 -0
- package/backend/tests/local/comprehensive-curl-tests.sh +156 -0
- package/backend/tests/local/test-auth-router.sh +144 -0
- package/backend/tests/local/test-database-router.sh +222 -0
- package/backend/tests/local/test-e2e.sh +241 -0
- package/backend/tests/local/test-fk-errors.sh +97 -0
- package/backend/tests/local/test-id-field.sh +201 -0
- package/backend/tests/local/test-public-bucket.sh +265 -0
- package/backend/tests/local/test-secrets.sh +248 -0
- package/backend/tests/local/test-serverless-functions.sh.disabled +325 -0
- package/backend/tests/local/test-traditional-rest.sh +209 -0
- package/backend/tests/manual/README.md +51 -0
- package/backend/tests/manual/create-large-table-simple.sql +11 -0
- package/backend/tests/manual/seed-large-table.sql +101 -0
- package/backend/tests/manual/setup-large-table-extras.sql +34 -0
- package/backend/tests/manual/test-better-auth.sh +303 -0
- package/backend/tests/manual/test-bulk-upsert.sh +410 -0
- package/backend/tests/manual/test-database-advance.sh +297 -0
- package/backend/tests/manual/test-postgrest-stability.sh +192 -0
- package/backend/tests/manual/test-rawsql-export-import.sh +412 -0
- package/backend/tests/manual/test-universal-storage.sh +264 -0
- package/backend/tests/manual/test-users.sql +18 -0
- package/backend/tests/run-all-tests.sh +140 -0
- package/backend/tests/setup.ts +22 -0
- package/backend/tests/test-config.sh +303 -0
- package/backend/tsconfig.json +23 -0
- package/backend/tsup.config.ts +18 -0
- package/backend/vitest.config.ts +22 -0
- package/docker-compose.prod.yml +145 -0
- package/docker-compose.yml +167 -0
- package/docker-init/db/db-init.sql +125 -0
- package/docker-init/db/jwt.sql +5 -0
- package/docker-init/db/logs.sql +9 -0
- package/docker-init/db/postgresql.conf +17 -0
- package/docs/deprecated/insforge-auth-api.md +215 -0
- package/docs/deprecated/insforge-auth-sdk.md +100 -0
- package/docs/deprecated/insforge-db-api.md +359 -0
- package/docs/deprecated/insforge-db-sdk.md +140 -0
- package/docs/deprecated/insforge-debug-sdk.md +157 -0
- package/docs/deprecated/insforge-debug.md +65 -0
- package/docs/deprecated/insforge-instructions.md +124 -0
- package/docs/deprecated/insforge-project.md +118 -0
- package/docs/deprecated/insforge-storage-api.md +279 -0
- package/docs/deprecated/insforge-storage-sdk.md +159 -0
- package/docs/insforge-instructions-sdk.md +407 -0
- package/eslint.config.js +317 -0
- package/examples/oauth/frontend-oauth-example.html +251 -0
- package/examples/response-examples.md +444 -0
- package/frontend/README.md +112 -0
- package/frontend/components.json +17 -0
- package/frontend/index.html +13 -0
- package/frontend/package.json +63 -0
- package/frontend/public/favicon.ico +0 -0
- package/frontend/src/App.tsx +106 -0
- package/frontend/src/assets/icons/checkbox_checked.svg +6 -0
- package/frontend/src/assets/icons/checkbox_undetermined.svg +6 -0
- package/frontend/src/assets/icons/checked.svg +3 -0
- package/frontend/src/assets/icons/error.svg +3 -0
- package/frontend/src/assets/icons/pencil.svg +4 -0
- package/frontend/src/assets/icons/refresh.svg +4 -0
- package/frontend/src/assets/icons/step_active.svg +3 -0
- package/frontend/src/assets/icons/step_inactive.svg +11 -0
- package/frontend/src/assets/icons/warning.svg +3 -0
- package/frontend/src/assets/logos/amazon.svg +1 -0
- package/frontend/src/assets/logos/claude_code.svg +3 -0
- package/frontend/src/assets/logos/cline.svg +6 -0
- package/frontend/src/assets/logos/cursor.svg +20 -0
- package/frontend/src/assets/logos/discord.svg +9 -0
- package/frontend/src/assets/logos/gemini.svg +19 -0
- package/frontend/src/assets/logos/github.svg +5 -0
- package/frontend/src/assets/logos/google.svg +13 -0
- package/frontend/src/assets/logos/grok.svg +10 -0
- package/frontend/src/assets/logos/insforge_dark.svg +15 -0
- package/frontend/src/assets/logos/insforge_light.svg +15 -0
- package/frontend/src/assets/logos/openai.svg +10 -0
- package/frontend/src/assets/logos/roo_code.svg +9 -0
- package/frontend/src/assets/logos/trae.svg +3 -0
- package/frontend/src/assets/logos/windsurf.svg +10 -0
- package/frontend/src/components/ButtonWithLoading.tsx +27 -0
- package/frontend/src/components/Checkbox.tsx +61 -0
- package/frontend/src/components/CodeBlock.tsx +32 -0
- package/frontend/src/components/ConfirmDialog.tsx +96 -0
- package/frontend/src/components/CopyButton.tsx +69 -0
- package/frontend/src/components/DeleteActionButton.tsx +42 -0
- package/frontend/src/components/EmptyState.tsx +41 -0
- package/frontend/src/components/ErrorState.tsx +35 -0
- package/frontend/src/components/FeatureSidebar.tsx +126 -0
- package/frontend/src/components/FeatureSidebarItem.tsx +101 -0
- package/frontend/src/components/JsonHighlight.tsx +61 -0
- package/frontend/src/components/LoadingState.tsx +16 -0
- package/frontend/src/components/PaginationControls.tsx +54 -0
- package/frontend/src/components/PromptDialog.tsx +68 -0
- package/frontend/src/components/SearchInput.tsx +90 -0
- package/frontend/src/components/SelectionClearButton.tsx +26 -0
- package/frontend/src/components/Stepper.tsx +139 -0
- package/frontend/src/components/ThemeToggle.tsx +58 -0
- package/frontend/src/components/TypeBadge.tsx +20 -0
- package/frontend/src/components/datagrid/DataGrid.tsx +264 -0
- package/frontend/src/components/datagrid/DefaultCellRenderer.tsx +114 -0
- package/frontend/src/components/datagrid/IdCell.tsx +44 -0
- package/frontend/src/components/datagrid/SortableHeader.tsx +74 -0
- package/frontend/src/components/datagrid/cell-editors/BooleanCellEditor.tsx +54 -0
- package/frontend/src/components/datagrid/cell-editors/DateCellEditor.tsx +483 -0
- package/frontend/src/components/datagrid/cell-editors/JsonCellEditor.tsx +362 -0
- package/frontend/src/components/datagrid/cell-editors/TextCellEditor.tsx +38 -0
- package/frontend/src/components/datagrid/cell-editors/index.ts +14 -0
- package/frontend/src/components/datagrid/cell-editors/types.ts +43 -0
- package/frontend/src/components/datagrid/datagridTypes.tsx +72 -0
- package/frontend/src/components/datagrid/index.tsx +20 -0
- package/frontend/src/components/index.ts +39 -0
- package/frontend/src/components/layout/AppHeader.tsx +146 -0
- package/frontend/src/components/layout/AppSidebar.tsx +190 -0
- package/frontend/src/components/layout/CloudLayout.tsx +95 -0
- package/frontend/src/components/layout/Layout.tsx +43 -0
- package/frontend/src/components/radix/Alert.tsx +45 -0
- package/frontend/src/components/radix/AlertDialog.tsx +115 -0
- package/frontend/src/components/radix/Avatar.tsx +45 -0
- package/frontend/src/components/radix/Badge.tsx +33 -0
- package/frontend/src/components/radix/Button.tsx +50 -0
- package/frontend/src/components/radix/Card.tsx +58 -0
- package/frontend/src/components/radix/Dialog.tsx +98 -0
- package/frontend/src/components/radix/DropdownMenu.tsx +185 -0
- package/frontend/src/components/radix/Form.tsx +167 -0
- package/frontend/src/components/radix/Input.tsx +22 -0
- package/frontend/src/components/radix/Label.tsx +19 -0
- package/frontend/src/components/radix/Popover.tsx +29 -0
- package/frontend/src/components/radix/ScrollArea.tsx +44 -0
- package/frontend/src/components/radix/Select.tsx +151 -0
- package/frontend/src/components/radix/Separator.tsx +26 -0
- package/frontend/src/components/radix/Sheet.tsx +119 -0
- package/frontend/src/components/radix/Skeleton.tsx +7 -0
- package/frontend/src/components/radix/Switch.tsx +29 -0
- package/frontend/src/components/radix/Tabs.tsx +50 -0
- package/frontend/src/components/radix/Textarea.tsx +21 -0
- package/frontend/src/components/radix/Tooltip.tsx +28 -0
- package/frontend/src/features/ai/components/AIConfigCard.tsx +154 -0
- package/frontend/src/features/ai/components/AIConfigDialog.tsx +76 -0
- package/frontend/src/features/ai/components/AIConfigForm.tsx +222 -0
- package/frontend/src/features/ai/components/AIEmptyState.tsx +18 -0
- package/frontend/src/features/ai/components/fields/ModalityField.tsx +87 -0
- package/frontend/src/features/ai/components/fields/ModelSelectionField.tsx +134 -0
- package/frontend/src/features/ai/components/fields/SystemPromptField.tsx +33 -0
- package/frontend/src/features/ai/helpers.ts +155 -0
- package/frontend/src/features/ai/hooks/useAIConfigs.ts +221 -0
- package/frontend/src/features/ai/hooks/useAIUsage.ts +77 -0
- package/frontend/src/features/ai/page/AIPage.tsx +178 -0
- package/frontend/src/features/ai/services/ai.service.ts +148 -0
- package/frontend/src/features/auth/components/AddOAuthDialog.tsx +106 -0
- package/frontend/src/features/auth/components/AuthMethodTab.tsx +238 -0
- package/frontend/src/features/auth/components/OAuthConfigDialog.tsx +303 -0
- package/frontend/src/features/auth/components/OAuthEmptyState.tsx +15 -0
- package/frontend/src/features/auth/components/UserFormDialog.tsx +248 -0
- package/frontend/src/features/auth/components/UsersDataGrid.tsx +183 -0
- package/frontend/src/features/auth/components/UsersTab.tsx +114 -0
- package/frontend/src/features/auth/hooks/useOAuthConfig.ts +129 -0
- package/frontend/src/features/auth/hooks/useUsers.ts +57 -0
- package/frontend/src/features/auth/index.ts +9 -0
- package/frontend/src/features/auth/page/AuthenticationPage.tsx +169 -0
- package/frontend/src/features/auth/services/auth.service.ts +112 -0
- package/frontend/src/features/auth/services/oauth.service.ts +49 -0
- package/frontend/src/features/dashboard/page/DashboardPage.tsx +194 -0
- package/frontend/src/features/database/components/ColumnTypeSelect.tsx +64 -0
- package/frontend/src/features/database/components/DatabaseDataGrid.tsx +282 -0
- package/frontend/src/features/database/components/ForeignKeyCell.tsx +187 -0
- package/frontend/src/features/database/components/ForeignKeyPopover.tsx +378 -0
- package/frontend/src/features/database/components/LinkRecordModal.tsx +288 -0
- package/frontend/src/features/database/components/RecordFormDialog.tsx +164 -0
- package/frontend/src/features/database/components/RecordFormField.tsx +568 -0
- package/frontend/src/features/database/components/TableEmptyState.tsx +21 -0
- package/frontend/src/features/database/components/TableForm.tsx +656 -0
- package/frontend/src/features/database/components/TableFormColumn.tsx +137 -0
- package/frontend/src/features/database/components/TableListSkeleton.tsx +9 -0
- package/frontend/src/features/database/components/TableSidebar.tsx +47 -0
- package/frontend/src/features/database/constants.ts +26 -0
- package/frontend/src/features/database/helpers.ts +125 -0
- package/frontend/src/features/database/hooks/UseLinkModal.tsx +78 -0
- package/frontend/src/features/database/index.ts +12 -0
- package/frontend/src/features/database/page/DatabasePage.tsx +626 -0
- package/frontend/src/features/database/schema.ts +25 -0
- package/frontend/src/features/database/services/database.service.ts +216 -0
- package/frontend/src/features/functions/components/FunctionEmptyState.tsx +15 -0
- package/frontend/src/features/functions/components/FunctionRow.tsx +71 -0
- package/frontend/src/features/functions/components/FunctionViewer.tsx +46 -0
- package/frontend/src/features/functions/components/FunctionsContent.tsx +88 -0
- package/frontend/src/features/functions/components/FunctionsSidebar.tsx +56 -0
- package/frontend/src/features/functions/components/SecretEmptyState.tsx +23 -0
- package/frontend/src/features/functions/components/SecretRow.tsx +68 -0
- package/frontend/src/features/functions/components/SecretsContent.tsx +120 -0
- package/frontend/src/features/functions/hooks/useFunctions.ts +106 -0
- package/frontend/src/features/functions/page/FunctionsPage.tsx +28 -0
- package/frontend/src/features/functions/services/functions.service.ts +48 -0
- package/frontend/src/features/login/components/AuthErrorBoundary.tsx +87 -0
- package/frontend/src/features/login/components/PrivateRoute.tsx +24 -0
- package/frontend/src/features/login/page/CloudLoginPage.tsx +93 -0
- package/frontend/src/features/login/page/LoginPage.tsx +174 -0
- package/frontend/src/features/logs/components/AnalyticsLogsTable.tsx +313 -0
- package/frontend/src/features/logs/components/LogsTable.tsx +199 -0
- package/frontend/src/features/logs/hooks/useAuditLogs.ts +39 -0
- package/frontend/src/features/logs/index.ts +5 -0
- package/frontend/src/features/logs/page/AnalyticsLogsPage.tsx +530 -0
- package/frontend/src/features/logs/page/AuditsPage.tsx +192 -0
- package/frontend/src/features/logs/services/log.service.ts +171 -0
- package/frontend/src/features/metadata/hooks/useMetadata.ts +53 -0
- package/frontend/src/features/metadata/index.ts +0 -0
- package/frontend/src/features/metadata/page/MetadataPage.tsx +136 -0
- package/frontend/src/features/metadata/services/metadata.service.ts +17 -0
- package/frontend/src/features/onboard/components/CompletionCard.tsx +41 -0
- package/frontend/src/features/onboard/components/OnboardButton.tsx +84 -0
- package/frontend/src/features/onboard/components/StepContent.tsx +91 -0
- package/frontend/src/features/onboard/components/TestConnectionStep.tsx +53 -0
- package/frontend/src/features/onboard/components/mcp/CursorDeeplinkGenerator.tsx +35 -0
- package/frontend/src/features/onboard/components/mcp/McpInstallation.tsx +144 -0
- package/frontend/src/features/onboard/components/mcp/index.ts +4 -0
- package/frontend/src/features/onboard/components/mcp/mcp-helper.tsx +98 -0
- package/frontend/src/features/onboard/index.ts +3 -0
- package/frontend/src/features/onboard/page/OnBoardPage.tsx +104 -0
- package/frontend/src/features/onboard/types.ts +8 -0
- package/frontend/src/features/secrets/hooks/useSecrets.ts +139 -0
- package/frontend/src/features/secrets/services/secrets.service.ts +57 -0
- package/frontend/src/features/storage/components/BucketEmptyState.tsx +19 -0
- package/frontend/src/features/storage/components/BucketFormDialog.tsx +194 -0
- package/frontend/src/features/storage/components/BucketListSkeleton.tsx +17 -0
- package/frontend/src/features/storage/components/FilePreviewDialog.tsx +287 -0
- package/frontend/src/features/storage/components/StorageDataGrid.tsx +239 -0
- package/frontend/src/features/storage/components/StorageManager.tsx +236 -0
- package/frontend/src/features/storage/components/StorageSidebar.tsx +44 -0
- package/frontend/src/features/storage/components/UploadToast.tsx +46 -0
- package/frontend/src/features/storage/index.ts +3 -0
- package/frontend/src/features/storage/page/StoragePage.tsx +553 -0
- package/frontend/src/features/storage/services/storage.service.ts +144 -0
- package/frontend/src/features/visualizer/components/AuthNode.tsx +107 -0
- package/frontend/src/features/visualizer/components/BucketNode.tsx +34 -0
- package/frontend/src/features/visualizer/components/SchemaVisualizer.tsx +359 -0
- package/frontend/src/features/visualizer/components/TableNode.tsx +152 -0
- package/frontend/src/features/visualizer/components/VisualizerSkeleton.tsx +24 -0
- package/frontend/src/features/visualizer/components/index.ts +5 -0
- package/frontend/src/features/visualizer/page/VisualizerPage.tsx +127 -0
- package/frontend/src/index.css +248 -0
- package/frontend/src/lib/api/client.ts +163 -0
- package/frontend/src/lib/contexts/AuthContext.tsx +157 -0
- package/frontend/src/lib/contexts/OnboardStepContext.tsx +68 -0
- package/frontend/src/lib/contexts/SocketContext.tsx +303 -0
- package/frontend/src/lib/contexts/ThemeContext.tsx +125 -0
- package/frontend/src/lib/hooks/useAuth.ts +4 -0
- package/frontend/src/lib/hooks/useConfirm.ts +55 -0
- package/frontend/src/lib/hooks/useInterval.ts +27 -0
- package/frontend/src/lib/hooks/useMediaQuery.ts +59 -0
- package/frontend/src/lib/hooks/useOnboardingCompletion.ts +29 -0
- package/frontend/src/lib/hooks/usePagination.ts +27 -0
- package/frontend/src/lib/hooks/useTimeout.ts +27 -0
- package/frontend/src/lib/hooks/useToast.tsx +229 -0
- package/frontend/src/lib/utils/constants.ts +38 -0
- package/frontend/src/lib/utils/utils.ts +165 -0
- package/frontend/src/lib/utils/validation-schemas.ts +126 -0
- package/frontend/src/main.tsx +16 -0
- package/frontend/src/rdg.css +194 -0
- package/frontend/src/vite-env.d.ts +12 -0
- package/frontend/tailwind.config.js +97 -0
- package/frontend/tsconfig.json +26 -0
- package/frontend/tsconfig.node.json +10 -0
- package/frontend/vite.config.ts +37 -0
- package/frontend/vitest.config.ts +36 -0
- package/functions/deno.json +25 -0
- package/functions/server.ts +290 -0
- package/functions/worker-template.js +126 -0
- package/openapi/ai.yaml +689 -0
- package/openapi/auth.yaml +563 -0
- package/openapi/functions.yaml +476 -0
- package/openapi/health.yaml +30 -0
- package/openapi/logs.yaml +224 -0
- package/openapi/metadata.yaml +178 -0
- package/openapi/records.yaml +382 -0
- package/openapi/secrets.yaml +371 -0
- package/openapi/storage.yaml +876 -0
- package/openapi/tables.yaml +464 -0
- package/package.json +88 -0
- package/shared-schemas/package.json +31 -0
- package/shared-schemas/src/ai-api.schema.ts +167 -0
- package/shared-schemas/src/ai.schema.ts +54 -0
- package/shared-schemas/src/auth-api.schema.ts +193 -0
- package/shared-schemas/src/auth.schema.ts +94 -0
- package/shared-schemas/src/database-api.schema.ts +259 -0
- package/shared-schemas/src/database.schema.ts +69 -0
- package/shared-schemas/src/functions-api.schema.ts +25 -0
- package/shared-schemas/src/functions.schema.ts +16 -0
- package/shared-schemas/src/index.ts +13 -0
- package/shared-schemas/src/logs-api.schema.ts +49 -0
- package/shared-schemas/src/logs.schema.ts +14 -0
- package/shared-schemas/src/metadata.schema.ts +56 -0
- package/shared-schemas/src/storage-api.schema.ts +65 -0
- package/shared-schemas/src/storage.schema.ts +19 -0
- package/shared-schemas/tsconfig.json +21 -0
- package/tsconfig.json +8 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import OpenAI from 'openai';
|
|
2
|
+
import { AIUsageService } from './usage';
|
|
3
|
+
import { AIConfigService } from './config';
|
|
4
|
+
import { AIClientService } from './client';
|
|
5
|
+
import type {
|
|
6
|
+
AIConfigurationSchema,
|
|
7
|
+
ChatCompletionResponse,
|
|
8
|
+
ChatMessageSchema,
|
|
9
|
+
} from '@insforge/shared-schemas';
|
|
10
|
+
import logger from '@/utils/logger.js';
|
|
11
|
+
import { ChatCompletionOptions } from '@/types/ai';
|
|
12
|
+
|
|
13
|
+
export class ChatService {
|
|
14
|
+
private aiUsageService = new AIUsageService();
|
|
15
|
+
private aiConfigService = new AIConfigService();
|
|
16
|
+
private aiCredentialsService = AIClientService.getInstance();
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Format messages for OpenAI API with multimodal support
|
|
20
|
+
*/
|
|
21
|
+
private formatMessages(
|
|
22
|
+
messages: ChatMessageSchema[],
|
|
23
|
+
systemPrompt?: string
|
|
24
|
+
): OpenAI.Chat.ChatCompletionMessageParam[] {
|
|
25
|
+
const formattedMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [];
|
|
26
|
+
|
|
27
|
+
// Add system message if provided
|
|
28
|
+
if (systemPrompt) {
|
|
29
|
+
formattedMessages.push({ role: 'system', content: systemPrompt });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Format conversation messages
|
|
33
|
+
for (const msg of messages) {
|
|
34
|
+
// Check if message has images
|
|
35
|
+
if (msg.images && msg.images.length > 0) {
|
|
36
|
+
// Build multimodal content array
|
|
37
|
+
const content = [
|
|
38
|
+
{ type: 'text', text: msg.content },
|
|
39
|
+
...msg.images.map((image) => ({
|
|
40
|
+
type: 'image_url',
|
|
41
|
+
image_url: { url: image.url },
|
|
42
|
+
})),
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
formattedMessages.push({
|
|
46
|
+
role: msg.role as 'system' | 'user' | 'assistant',
|
|
47
|
+
content,
|
|
48
|
+
} as OpenAI.Chat.ChatCompletionMessageParam);
|
|
49
|
+
} else {
|
|
50
|
+
// Simple text message
|
|
51
|
+
formattedMessages.push({
|
|
52
|
+
role: msg.role as 'system' | 'user' | 'assistant',
|
|
53
|
+
content: msg.content,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return formattedMessages;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Validate model and get config
|
|
63
|
+
*/
|
|
64
|
+
async validateAndGetConfig(modelId: string): Promise<AIConfigurationSchema | null> {
|
|
65
|
+
const aiConfig = await this.aiConfigService.findByModelId(modelId);
|
|
66
|
+
if (!aiConfig) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
`Model ${modelId} is not enabled. Please contact your administrator to enable this model.`
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
return aiConfig;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Send a chat message to the specified model
|
|
76
|
+
* @param messages - Array of messages for conversation
|
|
77
|
+
* @param options - Chat options including model, temperature, etc.
|
|
78
|
+
*/
|
|
79
|
+
async chat(
|
|
80
|
+
messages: ChatMessageSchema[],
|
|
81
|
+
options: ChatCompletionOptions
|
|
82
|
+
): Promise<ChatCompletionResponse> {
|
|
83
|
+
try {
|
|
84
|
+
// Get the client (handles validation and initialization automatically)
|
|
85
|
+
const client = await this.aiCredentialsService.getClient();
|
|
86
|
+
|
|
87
|
+
// Validate model and get config
|
|
88
|
+
const aiConfig = await this.validateAndGetConfig(options.model);
|
|
89
|
+
|
|
90
|
+
// Apply system prompt from config if available
|
|
91
|
+
const formattedMessages = this.formatMessages(messages, aiConfig?.systemPrompt);
|
|
92
|
+
|
|
93
|
+
const response = await client.chat.completions.create({
|
|
94
|
+
model: options.model,
|
|
95
|
+
messages: formattedMessages,
|
|
96
|
+
temperature: options.temperature ?? 0.7,
|
|
97
|
+
max_tokens: options.maxTokens ?? 4096,
|
|
98
|
+
top_p: options.topP,
|
|
99
|
+
stream: false,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Extract token usage if available
|
|
103
|
+
const tokenUsage = response.usage
|
|
104
|
+
? {
|
|
105
|
+
promptTokens: response.usage.prompt_tokens,
|
|
106
|
+
completionTokens: response.usage.completion_tokens,
|
|
107
|
+
totalTokens: response.usage.total_tokens,
|
|
108
|
+
}
|
|
109
|
+
: undefined;
|
|
110
|
+
|
|
111
|
+
// Track usage if config is available
|
|
112
|
+
if (aiConfig?.id && tokenUsage) {
|
|
113
|
+
await this.aiUsageService.trackChatUsage(
|
|
114
|
+
aiConfig.id,
|
|
115
|
+
tokenUsage.promptTokens,
|
|
116
|
+
tokenUsage.completionTokens,
|
|
117
|
+
options.model
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
text: response.choices[0]?.message?.content || '',
|
|
123
|
+
metadata: {
|
|
124
|
+
model: options.model,
|
|
125
|
+
...tokenUsage,
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
} catch (error) {
|
|
129
|
+
logger.error('Chat error', { error });
|
|
130
|
+
throw new Error(
|
|
131
|
+
`Failed to get response: ${error instanceof Error ? error.message : String(error)}`
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Stream a chat response
|
|
138
|
+
* @param messages - Array of messages for conversation
|
|
139
|
+
* @param options - Chat options including model, temperature, etc.
|
|
140
|
+
*/
|
|
141
|
+
async *streamChat(
|
|
142
|
+
messages: ChatMessageSchema[],
|
|
143
|
+
options: ChatCompletionOptions
|
|
144
|
+
): AsyncGenerator<{
|
|
145
|
+
chunk?: string;
|
|
146
|
+
tokenUsage?: { promptTokens?: number; completionTokens?: number; totalTokens?: number };
|
|
147
|
+
}> {
|
|
148
|
+
try {
|
|
149
|
+
// Get the client (handles validation and initialization automatically)
|
|
150
|
+
const client = await this.aiCredentialsService.getClient();
|
|
151
|
+
|
|
152
|
+
// Validate model and get config
|
|
153
|
+
const aiConfig = await this.validateAndGetConfig(options.model);
|
|
154
|
+
|
|
155
|
+
// Apply system prompt from config if available
|
|
156
|
+
const formattedMessages = this.formatMessages(messages, aiConfig?.systemPrompt);
|
|
157
|
+
|
|
158
|
+
const stream = await client.chat.completions.create({
|
|
159
|
+
model: options.model,
|
|
160
|
+
messages: formattedMessages,
|
|
161
|
+
temperature: options.temperature ?? 0.7,
|
|
162
|
+
max_tokens: options.maxTokens ?? 4096,
|
|
163
|
+
top_p: options.topP,
|
|
164
|
+
stream: true,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const tokenUsage = {
|
|
168
|
+
promptTokens: 0,
|
|
169
|
+
completionTokens: 0,
|
|
170
|
+
totalTokens: 0,
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
for await (const chunk of stream) {
|
|
174
|
+
const content = chunk.choices[0]?.delta?.content;
|
|
175
|
+
if (content) {
|
|
176
|
+
yield { chunk: content };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Check if this chunk contains usage data
|
|
180
|
+
if (chunk.usage) {
|
|
181
|
+
// Accumulate tokens instead of replacing
|
|
182
|
+
tokenUsage.promptTokens += chunk.usage.prompt_tokens || 0;
|
|
183
|
+
tokenUsage.completionTokens += chunk.usage.completion_tokens || 0;
|
|
184
|
+
tokenUsage.totalTokens += chunk.usage.total_tokens || 0;
|
|
185
|
+
|
|
186
|
+
// Yield the accumulated usage
|
|
187
|
+
yield { tokenUsage: { ...tokenUsage } };
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Track usage after streaming completes
|
|
192
|
+
if (aiConfig?.id && tokenUsage.totalTokens > 0) {
|
|
193
|
+
await this.aiUsageService.trackChatUsage(
|
|
194
|
+
aiConfig.id,
|
|
195
|
+
tokenUsage.promptTokens,
|
|
196
|
+
tokenUsage.completionTokens,
|
|
197
|
+
options.model
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
} catch (error) {
|
|
201
|
+
logger.error('Streaming error', { error });
|
|
202
|
+
throw new Error(
|
|
203
|
+
`Failed to stream response: ${error instanceof Error ? error.message : String(error)}`
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import OpenAI from 'openai';
|
|
2
|
+
import jwt from 'jsonwebtoken';
|
|
3
|
+
import { isCloudEnvironment } from '@/utils/environment';
|
|
4
|
+
import { AppError } from '@/api/middleware/error';
|
|
5
|
+
import { ERROR_CODES } from '@/types/error-constants';
|
|
6
|
+
|
|
7
|
+
interface CloudCredentialsResponse {
|
|
8
|
+
openrouter?: {
|
|
9
|
+
api_key: string;
|
|
10
|
+
limit?: number;
|
|
11
|
+
expired_at?: string | null;
|
|
12
|
+
usage?: number;
|
|
13
|
+
limit_remaining?: number;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface CloudCredentials {
|
|
18
|
+
apiKey: string;
|
|
19
|
+
limitRemaining?: number;
|
|
20
|
+
expiredAt?: Date | null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface OpenRouterKeyInfo {
|
|
24
|
+
data: {
|
|
25
|
+
label: string;
|
|
26
|
+
usage: number;
|
|
27
|
+
limit: number | null;
|
|
28
|
+
is_free_tier: boolean;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface OpenRouterLimitation {
|
|
33
|
+
label: string;
|
|
34
|
+
limit: number | null;
|
|
35
|
+
usage: number;
|
|
36
|
+
is_provisioning_key: boolean;
|
|
37
|
+
limit_remaining: number | null;
|
|
38
|
+
is_free_tier: boolean;
|
|
39
|
+
rate_limit: {
|
|
40
|
+
requests: number;
|
|
41
|
+
interval: string;
|
|
42
|
+
note: string;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export class AIClientService {
|
|
47
|
+
private static instance: AIClientService;
|
|
48
|
+
private cloudCredentials: CloudCredentials | undefined;
|
|
49
|
+
private openRouterClient: OpenAI | null = null;
|
|
50
|
+
private currentApiKey: string | undefined;
|
|
51
|
+
|
|
52
|
+
private constructor() {}
|
|
53
|
+
|
|
54
|
+
static getInstance(): AIClientService {
|
|
55
|
+
if (!AIClientService.instance) {
|
|
56
|
+
AIClientService.instance = new AIClientService();
|
|
57
|
+
}
|
|
58
|
+
return AIClientService.instance;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Create or recreate the OpenAI client with the given API key
|
|
63
|
+
*/
|
|
64
|
+
private createClient(apiKey: string): OpenAI {
|
|
65
|
+
this.currentApiKey = apiKey;
|
|
66
|
+
return new OpenAI({
|
|
67
|
+
baseURL: 'https://openrouter.ai/api/v1',
|
|
68
|
+
apiKey,
|
|
69
|
+
defaultHeaders: {
|
|
70
|
+
'HTTP-Referer': 'https://insforge.dev',
|
|
71
|
+
'X-Title': 'InsForge',
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Get OpenRouter API key based on environment
|
|
78
|
+
* In cloud environment: fetches from cloud API with JWT authentication
|
|
79
|
+
* In local environment: returns from environment variable
|
|
80
|
+
*/
|
|
81
|
+
async getApiKey(): Promise<string> {
|
|
82
|
+
if (isCloudEnvironment()) {
|
|
83
|
+
if (
|
|
84
|
+
this.cloudCredentials &&
|
|
85
|
+
(!this.cloudCredentials.expiredAt || new Date() <= this.cloudCredentials.expiredAt)
|
|
86
|
+
) {
|
|
87
|
+
return this.cloudCredentials.apiKey;
|
|
88
|
+
} else {
|
|
89
|
+
return await this.fetchCloudApiKey();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const apiKey = process.env.OPENROUTER_API_KEY;
|
|
94
|
+
if (!apiKey) {
|
|
95
|
+
throw new AppError(
|
|
96
|
+
'OPENROUTER_API_KEY not found in environment variables',
|
|
97
|
+
500,
|
|
98
|
+
ERROR_CODES.AI_INVALID_API_KEY
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
return apiKey;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Get the OpenAI client, creating or updating it as needed
|
|
106
|
+
* This is the main method services should use
|
|
107
|
+
*/
|
|
108
|
+
async getClient(): Promise<OpenAI> {
|
|
109
|
+
if (!this.openRouterClient) {
|
|
110
|
+
this.openRouterClient = this.createClient(await this.getApiKey());
|
|
111
|
+
return this.openRouterClient;
|
|
112
|
+
}
|
|
113
|
+
if (isCloudEnvironment()) {
|
|
114
|
+
const apiKey = await this.getApiKey();
|
|
115
|
+
if (this.currentApiKey !== apiKey) {
|
|
116
|
+
this.openRouterClient = this.createClient(apiKey);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return this.openRouterClient;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Check if AI services are properly configured
|
|
124
|
+
*/
|
|
125
|
+
isConfigured(): boolean {
|
|
126
|
+
if (isCloudEnvironment()) {
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
return !!process.env.OPENROUTER_API_KEY;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Get remaining credits for the current API key from OpenRouter
|
|
134
|
+
*/
|
|
135
|
+
async getRemainingCredits(): Promise<{
|
|
136
|
+
usage: number;
|
|
137
|
+
limit: number | null;
|
|
138
|
+
remaining: number | null;
|
|
139
|
+
}> {
|
|
140
|
+
try {
|
|
141
|
+
const apiKey = await this.getApiKey();
|
|
142
|
+
|
|
143
|
+
if (isCloudEnvironment()) {
|
|
144
|
+
// Use InsForge API for cloud environment
|
|
145
|
+
const response = await fetch(
|
|
146
|
+
`https://api.insforge.dev/ai/v1/limitations?credential=${encodeURIComponent(apiKey)}`,
|
|
147
|
+
{
|
|
148
|
+
method: 'GET',
|
|
149
|
+
}
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
if (!response.ok) {
|
|
153
|
+
throw new Error(`Failed to fetch key info: ${response.statusText}`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const result = (await response.json()) as { data: OpenRouterLimitation };
|
|
157
|
+
const keyInfo = result.data;
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
usage: keyInfo.usage,
|
|
161
|
+
limit: keyInfo.limit,
|
|
162
|
+
remaining: keyInfo.limit_remaining,
|
|
163
|
+
};
|
|
164
|
+
} else {
|
|
165
|
+
// Use OpenRouter API for local environment
|
|
166
|
+
const response = await fetch('https://openrouter.ai/api/v1/key', {
|
|
167
|
+
method: 'GET',
|
|
168
|
+
headers: {
|
|
169
|
+
Authorization: `Bearer ${apiKey}`,
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
if (!response.ok) {
|
|
174
|
+
throw new AppError(
|
|
175
|
+
`Invalid OpenRouter API Key`,
|
|
176
|
+
500,
|
|
177
|
+
ERROR_CODES.AI_INVALID_API_KEY,
|
|
178
|
+
'Check your OpenRouter key and try again.'
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const keyInfo = (await response.json()) as OpenRouterKeyInfo;
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
usage: keyInfo.data.usage,
|
|
186
|
+
limit: keyInfo.data.limit,
|
|
187
|
+
remaining: keyInfo.data.limit !== null ? keyInfo.data.limit - keyInfo.data.usage : null,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
} catch (error) {
|
|
191
|
+
console.error('Failed to fetch remaining credits:', error);
|
|
192
|
+
throw error;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Fetch API key from cloud service
|
|
198
|
+
*/
|
|
199
|
+
private async fetchCloudApiKey(): Promise<string> {
|
|
200
|
+
try {
|
|
201
|
+
const projectId = process.env.PROJECT_ID;
|
|
202
|
+
if (!projectId) {
|
|
203
|
+
throw new Error('PROJECT_ID not found in environment variables');
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const jwtSecret = process.env.JWT_SECRET;
|
|
207
|
+
if (!jwtSecret) {
|
|
208
|
+
throw new Error('JWT_SECRET not found in environment variables');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Sign a token for authentication
|
|
212
|
+
const token = jwt.sign({ projectId }, jwtSecret, { expiresIn: '1h' });
|
|
213
|
+
|
|
214
|
+
// Fetch API key from cloud service with sign token as query parameter
|
|
215
|
+
const response = await fetch(
|
|
216
|
+
`https://api.insforge.dev/ai/v1/credentials/${projectId}?sign=${token}`
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
if (!response.ok) {
|
|
220
|
+
throw new Error(`Failed to fetch cloud API key: ${response.statusText}`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const data = (await response.json()) as CloudCredentialsResponse;
|
|
224
|
+
|
|
225
|
+
// Extract API key from the openrouter object in response
|
|
226
|
+
if (!data.openrouter?.api_key) {
|
|
227
|
+
throw new Error('Invalid response: missing openrouter API Key');
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Store credentials with metadata
|
|
231
|
+
this.cloudCredentials = {
|
|
232
|
+
apiKey: data.openrouter.api_key,
|
|
233
|
+
limitRemaining: data.openrouter.limit_remaining,
|
|
234
|
+
expiredAt: data.openrouter.expired_at ? new Date(data.openrouter.expired_at) : null,
|
|
235
|
+
};
|
|
236
|
+
return data.openrouter.api_key;
|
|
237
|
+
} catch (error) {
|
|
238
|
+
console.error('Failed to fetch cloud API key:', error);
|
|
239
|
+
throw error;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { Pool } from 'pg';
|
|
2
|
+
import { DatabaseManager } from '@/core/database/manager.js';
|
|
3
|
+
import logger from '@/utils/logger.js';
|
|
4
|
+
import { AIConfigurationSchema, AIConfigurationWithUsageSchema } from '@insforge/shared-schemas';
|
|
5
|
+
|
|
6
|
+
export class AIConfigService {
|
|
7
|
+
private pool: Pool | null = null;
|
|
8
|
+
|
|
9
|
+
private getPool(): Pool {
|
|
10
|
+
if (!this.pool) {
|
|
11
|
+
this.pool = DatabaseManager.getInstance().getPool();
|
|
12
|
+
}
|
|
13
|
+
return this.pool;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async create(
|
|
17
|
+
inputModality: string[],
|
|
18
|
+
outputModality: string[],
|
|
19
|
+
provider: string,
|
|
20
|
+
modelId: string,
|
|
21
|
+
systemPrompt?: string
|
|
22
|
+
): Promise<{ id: string }> {
|
|
23
|
+
const client = await this.getPool().connect();
|
|
24
|
+
try {
|
|
25
|
+
const result = await client.query(
|
|
26
|
+
`INSERT INTO _ai_configs (input_modality, output_modality, provider, model_id, system_prompt)
|
|
27
|
+
VALUES ($1, $2, $3, $4, $5)
|
|
28
|
+
RETURNING id`,
|
|
29
|
+
[inputModality, outputModality, provider, modelId, systemPrompt || null]
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
logger.info('AI configuration created', { id: result.rows[0].id });
|
|
33
|
+
return { id: result.rows[0].id };
|
|
34
|
+
} catch (error) {
|
|
35
|
+
logger.error('Failed to create AI configuration', { error });
|
|
36
|
+
throw new Error('Failed to create AI configuration');
|
|
37
|
+
} finally {
|
|
38
|
+
client.release();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async findAll(): Promise<AIConfigurationWithUsageSchema[]> {
|
|
43
|
+
const client = await this.getPool().connect();
|
|
44
|
+
try {
|
|
45
|
+
// Use a single query with aggregation to get configs with usage stats
|
|
46
|
+
const result = await client.query(
|
|
47
|
+
`SELECT
|
|
48
|
+
c.id,
|
|
49
|
+
c.input_modality as "inputModality",
|
|
50
|
+
c.output_modality as "outputModality",
|
|
51
|
+
c.provider,
|
|
52
|
+
c.model_id as "modelId",
|
|
53
|
+
c.system_prompt as "systemPrompt",
|
|
54
|
+
COALESCE(SUM(u.input_tokens), 0)::INTEGER as "totalInputTokens",
|
|
55
|
+
COALESCE(SUM(u.output_tokens), 0)::INTEGER as "totalOutputTokens",
|
|
56
|
+
COALESCE(SUM(u.input_tokens + u.output_tokens), 0)::INTEGER as "totalTokens",
|
|
57
|
+
COALESCE(SUM(u.image_count), 0)::INTEGER as "totalImageCount",
|
|
58
|
+
COALESCE(COUNT(u.id), 0)::INTEGER as "totalRequests"
|
|
59
|
+
FROM _ai_configs c
|
|
60
|
+
LEFT JOIN _ai_usage u ON c.id = u.config_id
|
|
61
|
+
GROUP BY c.id, c.input_modality, c.output_modality, c.provider, c.model_id, c.system_prompt, c.created_at
|
|
62
|
+
ORDER BY c.created_at DESC`
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
return result.rows.map((row) => ({
|
|
66
|
+
id: row.id,
|
|
67
|
+
inputModality: row.inputModality,
|
|
68
|
+
outputModality: row.outputModality,
|
|
69
|
+
provider: row.provider,
|
|
70
|
+
modelId: row.modelId,
|
|
71
|
+
systemPrompt: row.systemPrompt,
|
|
72
|
+
usageStats: {
|
|
73
|
+
totalInputTokens: row.totalInputTokens,
|
|
74
|
+
totalOutputTokens: row.totalOutputTokens,
|
|
75
|
+
totalTokens: row.totalTokens,
|
|
76
|
+
totalImageCount: row.totalImageCount,
|
|
77
|
+
totalRequests: row.totalRequests,
|
|
78
|
+
},
|
|
79
|
+
}));
|
|
80
|
+
} catch (error) {
|
|
81
|
+
logger.error('Failed to fetch AI configurations with usage', { error });
|
|
82
|
+
throw new Error('Failed to fetch AI configurations');
|
|
83
|
+
} finally {
|
|
84
|
+
client.release();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async update(id: string, systemPrompt: string | null): Promise<boolean> {
|
|
89
|
+
const client = await this.getPool().connect();
|
|
90
|
+
try {
|
|
91
|
+
const result = await client.query(
|
|
92
|
+
`UPDATE _ai_configs
|
|
93
|
+
SET system_prompt = $1, updated_at = NOW()
|
|
94
|
+
WHERE id = $2`,
|
|
95
|
+
[systemPrompt, id]
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const success = (result.rowCount ?? 0) > 0;
|
|
99
|
+
if (success) {
|
|
100
|
+
logger.info('AI configuration updated', { id });
|
|
101
|
+
}
|
|
102
|
+
return success;
|
|
103
|
+
} catch (error) {
|
|
104
|
+
logger.error('Failed to update AI configuration', { error, id });
|
|
105
|
+
throw new Error('Failed to update AI configuration');
|
|
106
|
+
} finally {
|
|
107
|
+
client.release();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async delete(id: string): Promise<boolean> {
|
|
112
|
+
const client = await this.getPool().connect();
|
|
113
|
+
try {
|
|
114
|
+
const result = await client.query('DELETE FROM _ai_configs WHERE id = $1', [id]);
|
|
115
|
+
|
|
116
|
+
const success = (result.rowCount ?? 0) > 0;
|
|
117
|
+
if (success) {
|
|
118
|
+
logger.info('AI configuration deleted', { id });
|
|
119
|
+
}
|
|
120
|
+
return success;
|
|
121
|
+
} catch (error) {
|
|
122
|
+
logger.error('Failed to delete AI configuration', { error, id });
|
|
123
|
+
throw new Error('Failed to delete AI configuration');
|
|
124
|
+
} finally {
|
|
125
|
+
client.release();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async findByModelId(modelId: string): Promise<AIConfigurationSchema | null> {
|
|
130
|
+
const client = await this.getPool().connect();
|
|
131
|
+
try {
|
|
132
|
+
const result = await client.query(
|
|
133
|
+
`SELECT id, input_modality as "inputModality", output_modality as "outputModality", provider, model_id as "modelId", system_prompt as "systemPrompt", created_at, updated_at
|
|
134
|
+
FROM _ai_configs
|
|
135
|
+
WHERE model_id = $1`,
|
|
136
|
+
[modelId]
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
if (result.rows.length === 0) {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const row = result.rows[0];
|
|
144
|
+
return {
|
|
145
|
+
id: row.id,
|
|
146
|
+
inputModality: row.inputModality,
|
|
147
|
+
outputModality: row.outputModality,
|
|
148
|
+
provider: row.provider,
|
|
149
|
+
modelId: row.modelId,
|
|
150
|
+
systemPrompt: row.systemPrompt,
|
|
151
|
+
};
|
|
152
|
+
} catch (error) {
|
|
153
|
+
logger.error('Failed to fetch AI configuration by modelId', {
|
|
154
|
+
error,
|
|
155
|
+
modelId,
|
|
156
|
+
});
|
|
157
|
+
throw new Error('Failed to fetch AI configuration');
|
|
158
|
+
} finally {
|
|
159
|
+
client.release();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Get AI metadata
|
|
165
|
+
*/
|
|
166
|
+
async getMetadata(): Promise<{
|
|
167
|
+
models: Array<{ inputModality: string[]; outputModality: string[]; modelId: string }>;
|
|
168
|
+
}> {
|
|
169
|
+
try {
|
|
170
|
+
const configs = await this.findAll();
|
|
171
|
+
|
|
172
|
+
// Map configs to simplified model metadata
|
|
173
|
+
const models = configs.map((config) => ({
|
|
174
|
+
inputModality: config.inputModality,
|
|
175
|
+
outputModality: config.outputModality,
|
|
176
|
+
modelId: config.modelId,
|
|
177
|
+
}));
|
|
178
|
+
|
|
179
|
+
return { models };
|
|
180
|
+
} catch (error) {
|
|
181
|
+
logger.error('Failed to get AI metadata', {
|
|
182
|
+
error: error instanceof Error ? error.message : String(error),
|
|
183
|
+
});
|
|
184
|
+
return { models: [] };
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|