insforge 1.3.0 → 1.4.8
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/CHANGELOG.md +2 -0
- package/auth/package.json +5 -3
- package/auth/src/lib/broadcastService.ts +115 -117
- package/auth/src/lib/insforge.ts +8 -0
- package/auth/src/main.tsx +2 -4
- package/auth/src/pages/SignInPage.tsx +60 -60
- package/auth/src/pages/SignUpPage.tsx +60 -60
- package/auth/src/pages/VerifyEmailPage.tsx +18 -0
- package/auth/tsconfig.json +2 -1
- package/backend/package.json +10 -6
- package/backend/src/api/middlewares/rate-limiters.ts +127 -127
- package/backend/src/api/routes/ai/index.routes.ts +475 -468
- package/backend/src/api/routes/auth/index.routes.ts +85 -32
- package/backend/src/api/routes/auth/oauth.routes.ts +11 -6
- package/backend/src/api/routes/database/index.routes.ts +2 -0
- package/backend/src/api/routes/database/records.routes.ts +39 -175
- package/backend/src/api/routes/database/rpc.routes.ts +69 -0
- package/backend/src/api/routes/deployments/index.routes.ts +192 -0
- package/backend/src/api/routes/docs/index.routes.ts +3 -2
- package/backend/src/api/routes/email/index.routes.ts +35 -35
- package/backend/src/api/routes/functions/index.routes.ts +3 -3
- package/backend/src/api/routes/metadata/index.routes.ts +26 -0
- package/backend/src/api/routes/webhooks/index.routes.ts +109 -0
- package/backend/src/infra/database/database.manager.ts +0 -10
- package/backend/src/infra/database/migrations/018_schema-rework.sql +441 -0
- package/backend/src/infra/database/migrations/019_create-deployments-table.sql +36 -0
- package/backend/src/infra/database/migrations/020_add-audio-modality.sql +11 -0
- package/backend/src/infra/database/migrations/bootstrap/bootstrap-migrations.js +103 -0
- package/backend/src/infra/security/token.manager.ts +1 -4
- package/backend/src/providers/ai/openrouter.provider.ts +12 -3
- package/backend/src/providers/database/base.provider.ts +39 -0
- package/backend/src/providers/database/cloud.provider.ts +159 -0
- package/backend/src/providers/deployments/vercel.provider.ts +516 -0
- package/backend/src/server.ts +19 -7
- package/backend/src/services/ai/ai-config.service.ts +6 -6
- package/backend/src/services/ai/ai-model.service.ts +60 -60
- package/backend/src/services/ai/ai-usage.service.ts +7 -7
- package/backend/src/services/ai/chat-completion.service.ts +415 -220
- package/backend/src/services/ai/helpers.ts +64 -64
- package/backend/src/services/ai/index.ts +13 -13
- package/backend/src/services/auth/auth-config.service.ts +4 -4
- package/backend/src/services/auth/auth-otp.service.ts +6 -6
- package/backend/src/services/auth/auth.service.ts +134 -74
- package/backend/src/services/auth/index.ts +4 -4
- package/backend/src/services/auth/oauth-config.service.ts +12 -12
- package/backend/src/services/database/database-advance.service.ts +19 -55
- package/backend/src/services/database/database-table.service.ts +38 -85
- package/backend/src/services/database/postgrest-proxy.service.ts +165 -0
- package/backend/src/services/deployments/deployment.service.ts +693 -0
- package/backend/src/services/functions/function.service.ts +61 -41
- package/backend/src/services/logs/audit.service.ts +10 -10
- package/backend/src/services/secrets/secret.service.ts +101 -27
- package/backend/src/services/storage/storage.service.ts +30 -30
- package/backend/src/services/usage/usage.service.ts +6 -6
- package/backend/src/types/ai.ts +8 -0
- package/backend/src/types/auth.ts +5 -1
- package/backend/src/types/database.ts +2 -0
- package/backend/src/types/deployments.ts +33 -0
- package/backend/src/types/storage.ts +1 -1
- package/backend/src/types/webhooks.ts +45 -0
- package/backend/src/utils/cookies.ts +34 -35
- package/backend/src/utils/environment.ts +0 -14
- package/backend/src/utils/s3-config-loader.ts +64 -64
- package/backend/src/utils/seed.ts +334 -301
- package/backend/src/utils/sql-parser.ts +126 -0
- package/backend/src/utils/utils.ts +114 -114
- package/backend/src/utils/validations.ts +10 -10
- package/backend/tests/local/test-rpc.sh +141 -0
- package/backend/tests/local/test-secrets.sh +1 -1
- package/backend/tests/manual/test-ai-model-plugins.sh +258 -0
- package/backend/tests/manual/test-rawsql-modes.sh +24 -24
- package/backend/tests/unit/database-advance.test.ts +326 -0
- package/backend/tests/unit/helpers.test.ts +2 -2
- package/claude-plugin/skills/insforge-schema-patterns/SKILL.md +13 -10
- package/docker-compose.prod.yml +1 -1
- package/docker-compose.yml +1 -1
- package/docs/agent-docs/deployment.md +79 -0
- package/docs/changelog.mdx +165 -72
- package/docs/core-concepts/ai/architecture.mdx +1 -23
- package/docs/core-concepts/ai/sdk.mdx +26 -1
- package/docs/core-concepts/authentication/architecture.mdx +6 -8
- package/docs/core-concepts/authentication/sdk.mdx +387 -91
- package/docs/core-concepts/authentication/ui-components/customization.mdx +460 -256
- package/docs/core-concepts/authentication/ui-components/nextjs.mdx +50 -24
- package/docs/core-concepts/authentication/ui-components/react-router.mdx +18 -19
- package/docs/core-concepts/authentication/ui-components/react.mdx +26 -19
- package/docs/core-concepts/database/architecture.mdx +58 -21
- package/docs/core-concepts/database/pgvector.mdx +138 -0
- package/docs/core-concepts/database/sdk.mdx +17 -17
- package/docs/core-concepts/deployments/architecture.mdx +152 -0
- package/docs/core-concepts/email/architecture.mdx +4 -2
- package/docs/core-concepts/functions/architecture.mdx +1 -1
- package/docs/core-concepts/functions/sdk.mdx +0 -1
- package/docs/core-concepts/realtime/architecture.mdx +1 -1
- package/docs/core-concepts/storage/architecture.mdx +1 -1
- package/docs/core-concepts/storage/sdk.mdx +25 -25
- package/docs/docs.json +14 -6
- package/docs/favicon.png +0 -0
- package/docs/favicon.svg +3 -18
- package/docs/images/changelog/dec-2025/apple-oauth.mp4 +0 -0
- package/docs/images/changelog/dec-2025/moreModels.png +0 -0
- package/docs/images/changelog/dec-2025/multi-region.webp +0 -0
- package/docs/images/changelog/dec-2025/postgres-connection.webp +0 -0
- package/docs/images/changelog/dec-2025/realtime2.png +0 -0
- package/docs/images/mcp-setup/CC-MCP-1.mp4 +0 -0
- package/docs/images/mcp-setup/CC-MCP-2.mp4 +0 -0
- package/docs/images/mcp-setup/Cursor-MCP-1.mp4 +0 -0
- package/docs/images/mcp-setup/Cursor-MCP-2.mp4 +0 -0
- package/docs/images/mcp-setup/Cursor-MCP-3.mp4 +0 -0
- package/docs/images/mcp-setup/claude-code-connect.png +0 -0
- package/docs/images/mcp-setup/cline-1.png +0 -0
- package/docs/images/mcp-setup/cline-2.png +0 -0
- package/docs/images/mcp-setup/cline-3.png +0 -0
- package/docs/images/mcp-setup/connect-project.png +0 -0
- package/docs/images/mcp-setup/copilot-1.png +0 -0
- package/docs/images/mcp-setup/copilot-2.png +0 -0
- package/docs/images/mcp-setup/copilot-3.png +0 -0
- package/docs/images/mcp-setup/mcp-json-1.png +0 -0
- package/docs/images/mcp-setup/mcp-json-2.png +0 -0
- package/docs/images/mcp-setup/qoder-1.png +0 -0
- package/docs/images/mcp-setup/qoder-2.png +0 -0
- package/docs/images/mcp-setup/roocode-1.png +0 -0
- package/docs/images/mcp-setup/roocode-2.png +0 -0
- package/docs/images/mcp-setup/trae-1.png +0 -0
- package/docs/images/mcp-setup/trae-2.png +0 -0
- package/docs/images/mcp-setup/trae-3.png +0 -0
- package/docs/images/mcp-setup/trae-4.png +0 -0
- package/docs/images/mcp-setup/trae-5.png +0 -0
- package/docs/images/mcp-setup/windsurf-1.png +0 -0
- package/docs/images/mcp-setup/windsurf-2.png +0 -0
- package/docs/insforge-instructions-sdk.md +7 -3
- package/docs/introduction.mdx +9 -8
- package/docs/mcp-setup.mdx +332 -0
- package/docs/oauth-server.mdx +563 -0
- package/docs/partnership.mdx +79 -10
- package/docs/quickstart.mdx +1 -1
- package/docs/vscode-extension.mdx +74 -0
- package/eslint.config.js +1 -0
- package/examples/response-examples.md +1 -1
- package/frontend/package.json +1 -1
- package/frontend/src/App.tsx +8 -3
- package/frontend/src/assets/logos/antigravity.svg +1 -0
- package/frontend/src/assets/logos/copilot.svg +10 -0
- package/frontend/src/assets/logos/deepseek.svg +139 -0
- package/frontend/src/assets/logos/kiro.svg +9 -0
- package/frontend/src/assets/logos/qoder.svg +4 -0
- package/frontend/src/assets/logos/qwen.svg +15 -0
- package/frontend/src/components/CodeBlock.tsx +2 -2
- package/frontend/src/components/ConnectCTA.tsx +3 -2
- package/frontend/src/components/datagrid/DataGrid.tsx +90 -62
- package/frontend/src/components/datagrid/datagridTypes.tsx +2 -1
- package/frontend/src/components/datagrid/index.ts +1 -1
- package/frontend/src/components/index.ts +0 -1
- package/frontend/src/components/layout/AppHeader.tsx +4 -27
- package/frontend/src/components/layout/AppSidebar.tsx +85 -100
- package/frontend/src/components/layout/Layout.tsx +34 -32
- package/frontend/src/components/layout/PrimaryMenu.tsx +12 -4
- package/frontend/src/components/radix/Select.tsx +151 -151
- package/frontend/src/features/ai/components/AIConfigCard.tsx +200 -200
- package/frontend/src/features/ai/components/AIEmptyState.tsx +23 -23
- package/frontend/src/features/ai/components/ModalityFilterSidebar.tsx +102 -101
- package/frontend/src/features/ai/components/ModelSelectionDialog.tsx +135 -135
- package/frontend/src/features/ai/components/ModelSelectionGrid.tsx +51 -51
- package/frontend/src/features/ai/components/SystemPromptDialog.tsx +118 -118
- package/frontend/src/features/ai/components/index.ts +6 -6
- package/frontend/src/features/ai/helpers.ts +147 -141
- package/frontend/src/features/ai/pages/AIPage.tsx +166 -166
- package/frontend/src/features/auth/components/AuthPreview.tsx +96 -96
- package/frontend/src/features/auth/components/UsersDataGrid.tsx +55 -31
- package/frontend/src/features/auth/components/index.ts +5 -5
- package/frontend/src/features/auth/pages/AuthMethodsPage.tsx +275 -275
- package/frontend/src/features/dashboard/pages/DashboardPage.tsx +1 -1
- package/frontend/src/features/database/components/DatabaseDataGrid.tsx +0 -2
- package/frontend/src/features/database/components/ForeignKeyCell.tsx +38 -11
- package/frontend/src/features/database/components/ForeignKeyPopover.tsx +18 -8
- package/frontend/src/features/database/components/LinkRecordModal.tsx +61 -13
- package/frontend/src/features/database/components/RecordFormField.tsx +1 -1
- package/frontend/src/features/database/components/TableSidebar.tsx +0 -3
- package/frontend/src/features/database/components/TablesEmptyState.tsx +1 -1
- package/frontend/src/features/database/components/TemplatePreview.tsx +1 -2
- package/frontend/src/features/database/constants.ts +16 -28
- package/frontend/src/features/database/hooks/useCSVImport.ts +3 -2
- package/frontend/src/features/database/hooks/useRawSQL.ts +3 -2
- package/frontend/src/features/database/hooks/useTables.ts +5 -7
- package/frontend/src/features/database/pages/FunctionsPage.tsx +0 -5
- package/frontend/src/features/database/pages/IndexesPage.tsx +0 -5
- package/frontend/src/features/database/pages/PoliciesPage.tsx +0 -5
- package/frontend/src/features/database/pages/SQLEditorPage.tsx +2 -2
- package/frontend/src/features/database/pages/TriggersPage.tsx +0 -5
- package/frontend/src/features/database/services/advance.service.ts +1 -15
- package/frontend/src/features/database/services/record.service.ts +4 -20
- package/frontend/src/features/database/services/table.service.ts +1 -4
- package/frontend/src/features/database/templates/ai-chatbot.ts +6 -6
- package/frontend/src/features/database/templates/ecommerce-platform.ts +2 -2
- package/frontend/src/features/database/templates/instagram-clone.ts +10 -10
- package/frontend/src/features/database/templates/notion-clone.ts +8 -8
- package/frontend/src/features/database/templates/reddit-clone.ts +10 -10
- package/frontend/src/features/deployments/components/DeploymentRow.tsx +93 -0
- package/frontend/src/features/deployments/components/DeploymentsEmptyState.tsx +15 -0
- package/frontend/src/features/deployments/hooks/useDeployments.ts +157 -0
- package/frontend/src/features/deployments/pages/DeploymentsPage.tsx +318 -0
- package/frontend/src/features/deployments/services/deployments.service.ts +63 -0
- package/frontend/src/features/functions/components/FunctionRow.tsx +72 -72
- package/frontend/src/features/functions/components/FunctionsSidebar.tsx +56 -56
- package/frontend/src/features/functions/components/SecretRow.tsx +3 -3
- package/frontend/src/features/functions/components/index.ts +5 -5
- package/frontend/src/features/functions/hooks/useFunctions.ts +5 -4
- package/frontend/src/features/functions/hooks/useSecrets.ts +6 -9
- package/frontend/src/features/functions/pages/SecretsPage.tsx +118 -118
- package/frontend/src/features/functions/services/function.service.ts +8 -25
- package/frontend/src/features/functions/services/secret.service.ts +23 -41
- package/frontend/src/features/login/pages/CloudLoginPage.tsx +125 -118
- package/frontend/src/features/logs/components/LogDetailPanel.tsx +41 -0
- package/frontend/src/features/logs/components/LogsDataGrid.tsx +32 -1
- package/frontend/src/features/logs/components/index.ts +1 -0
- package/frontend/src/features/logs/pages/LogsPage.tsx +36 -6
- package/frontend/src/features/onboard/components/ApiCredentialsSection.tsx +59 -0
- package/frontend/src/features/onboard/components/ConnectionStringSection.tsx +180 -0
- package/frontend/src/features/onboard/components/McpConnectionSection.tsx +159 -0
- package/frontend/src/features/onboard/components/OnboardingController.tsx +68 -0
- package/frontend/src/features/onboard/components/OnboardingModal.tsx +121 -267
- package/frontend/src/features/onboard/components/ShowPasswordButton.tsx +21 -0
- package/frontend/src/features/onboard/components/index.ts +9 -4
- package/frontend/src/features/onboard/components/mcp/CursorDeeplinkGenerator.tsx +1 -1
- package/frontend/src/features/onboard/components/mcp/QoderDeeplinkGenerator.tsx +36 -0
- package/frontend/src/features/onboard/components/mcp/helpers.tsx +123 -98
- package/frontend/src/features/onboard/components/mcp/index.ts +4 -3
- package/frontend/src/features/onboard/index.ts +17 -13
- package/frontend/src/features/settings/pages/SettingsPage.tsx +349 -0
- package/frontend/src/features/visualizer/components/AuthNode.tsx +4 -4
- package/frontend/src/features/visualizer/components/SchemaVisualizer.tsx +21 -8
- package/frontend/src/features/visualizer/pages/VisualizerPage.tsx +10 -1
- package/frontend/src/index.css +249 -249
- package/frontend/src/lib/contexts/ModalContext.tsx +35 -0
- package/frontend/src/lib/hooks/useMetadata.ts +45 -1
- package/frontend/src/lib/hooks/useModal.tsx +2 -0
- package/frontend/src/lib/routing/AppRoutes.tsx +103 -99
- package/frontend/src/lib/services/metadata.service.ts +20 -3
- package/frontend/src/lib/utils/menuItems.ts +223 -207
- package/frontend/src/lib/utils/utils.ts +196 -196
- package/functions/server.ts +315 -315
- package/functions/worker-template.js +1 -1
- package/openapi/ai.yaml +115 -5
- package/openapi/auth.yaml +97 -17
- package/openapi/logs.yaml +0 -2
- package/openapi/metadata.yaml +0 -2
- package/openapi/records.yaml +21 -21
- package/openapi/tables.yaml +1 -2
- package/package.json +1 -1
- package/shared-schemas/package.json +1 -1
- package/shared-schemas/src/ai-api.schema.ts +251 -143
- package/shared-schemas/src/ai.schema.ts +63 -63
- package/shared-schemas/src/auth-api.schema.ts +34 -6
- package/shared-schemas/src/auth.schema.ts +17 -10
- package/shared-schemas/src/cloud-events.schema.ts +26 -0
- package/shared-schemas/src/deployments-api.schema.ts +55 -0
- package/shared-schemas/src/deployments.schema.ts +30 -0
- package/shared-schemas/src/docs.schema.ts +8 -2
- package/shared-schemas/src/email-api.schema.ts +30 -30
- package/shared-schemas/src/functions-api.schema.ts +13 -4
- package/shared-schemas/src/functions.schema.ts +1 -1
- package/shared-schemas/src/index.ts +22 -18
- package/shared-schemas/src/metadata.schema.ts +30 -4
- package/shared-schemas/src/secrets-api.schema.ts +44 -0
- package/shared-schemas/src/secrets.schema.ts +15 -0
- package/zeabur/README.md +13 -0
- package/zeabur/template.yml +20 -51
- package/backend/src/types/profile.ts +0 -55
- package/frontend/src/components/ProjectInfoModal.tsx +0 -128
|
@@ -1,220 +1,415 @@
|
|
|
1
|
-
import OpenAI from 'openai';
|
|
2
|
-
import { AIUsageService } from './ai-usage.service.js';
|
|
3
|
-
import { AIConfigService } from './ai-config.service.js';
|
|
4
|
-
import { OpenRouterProvider } from '@/providers/ai/openrouter.provider.js';
|
|
5
|
-
import type {
|
|
6
|
-
AIConfigurationSchema,
|
|
7
|
-
ChatCompletionResponse,
|
|
8
|
-
ChatMessageSchema,
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
import
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
*
|
|
151
|
-
*/
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
}
|
|
1
|
+
import OpenAI from 'openai';
|
|
2
|
+
import { AIUsageService } from './ai-usage.service.js';
|
|
3
|
+
import { AIConfigService } from './ai-config.service.js';
|
|
4
|
+
import { OpenRouterProvider } from '@/providers/ai/openrouter.provider.js';
|
|
5
|
+
import type {
|
|
6
|
+
AIConfigurationSchema,
|
|
7
|
+
ChatCompletionResponse,
|
|
8
|
+
ChatMessageSchema,
|
|
9
|
+
UrlCitationAnnotation,
|
|
10
|
+
} from '@insforge/shared-schemas';
|
|
11
|
+
import logger from '@/utils/logger.js';
|
|
12
|
+
import { ChatCompletionOptions } from '@/types/ai.js';
|
|
13
|
+
|
|
14
|
+
// OpenRouter plugin type for web search
|
|
15
|
+
interface OpenRouterWebPlugin {
|
|
16
|
+
id: 'web';
|
|
17
|
+
engine?: 'native' | 'exa';
|
|
18
|
+
max_results?: number;
|
|
19
|
+
search_prompt?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// OpenRouter plugin type for file parsing (PDF processing)
|
|
23
|
+
interface OpenRouterFileParserPlugin {
|
|
24
|
+
id: 'file-parser';
|
|
25
|
+
pdf?: {
|
|
26
|
+
engine?: 'pdf-text' | 'mistral-ocr' | 'native';
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Union type for all OpenRouter plugins
|
|
31
|
+
type OpenRouterPlugin = OpenRouterWebPlugin | OpenRouterFileParserPlugin;
|
|
32
|
+
|
|
33
|
+
// Extended request type with OpenRouter-specific fields
|
|
34
|
+
interface OpenRouterChatCompletionRequest
|
|
35
|
+
extends OpenAI.Chat.ChatCompletionCreateParamsNonStreaming {
|
|
36
|
+
plugins?: OpenRouterPlugin[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface OpenRouterChatCompletionStreamingRequest
|
|
40
|
+
extends OpenAI.Chat.ChatCompletionCreateParamsStreaming {
|
|
41
|
+
plugins?: OpenRouterPlugin[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// OpenRouter annotation format from API response
|
|
45
|
+
interface OpenRouterUrlCitation {
|
|
46
|
+
type: 'url_citation';
|
|
47
|
+
url_citation: {
|
|
48
|
+
url: string;
|
|
49
|
+
title?: string;
|
|
50
|
+
content?: string;
|
|
51
|
+
start_index?: number;
|
|
52
|
+
end_index?: number;
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Message structure that may contain annotations (from OpenRouter response)
|
|
57
|
+
interface MessageWithAnnotations {
|
|
58
|
+
content?: string | null;
|
|
59
|
+
annotations?: OpenRouterUrlCitation[];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export class ChatCompletionService {
|
|
63
|
+
private static instance: ChatCompletionService;
|
|
64
|
+
private aiUsageService = AIUsageService.getInstance();
|
|
65
|
+
private aiConfigService = AIConfigService.getInstance();
|
|
66
|
+
private openRouterProvider = OpenRouterProvider.getInstance();
|
|
67
|
+
|
|
68
|
+
private constructor() {}
|
|
69
|
+
|
|
70
|
+
public static getInstance(): ChatCompletionService {
|
|
71
|
+
if (!ChatCompletionService.instance) {
|
|
72
|
+
ChatCompletionService.instance = new ChatCompletionService();
|
|
73
|
+
}
|
|
74
|
+
return ChatCompletionService.instance;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Format messages for OpenAI API with multimodal support
|
|
79
|
+
*/
|
|
80
|
+
private formatMessages(
|
|
81
|
+
messages: ChatMessageSchema[],
|
|
82
|
+
systemPrompt?: string
|
|
83
|
+
): OpenAI.Chat.ChatCompletionMessageParam[] {
|
|
84
|
+
const formattedMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [];
|
|
85
|
+
|
|
86
|
+
// Add system message if provided
|
|
87
|
+
if (systemPrompt) {
|
|
88
|
+
formattedMessages.push({ role: 'system', content: systemPrompt });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Format conversation messages
|
|
92
|
+
for (const msg of messages) {
|
|
93
|
+
// Check if message has images (legacy format), new format image is within the content array
|
|
94
|
+
if (msg.images && msg.images.length && typeof msg.content === 'string') {
|
|
95
|
+
// Build multimodal content array
|
|
96
|
+
const content = [
|
|
97
|
+
{ type: 'text', text: msg.content },
|
|
98
|
+
...msg.images.map((image) => ({
|
|
99
|
+
type: 'image_url',
|
|
100
|
+
image_url: { url: image.url },
|
|
101
|
+
})),
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
formattedMessages.push({
|
|
105
|
+
role: msg.role as 'system' | 'user' | 'assistant',
|
|
106
|
+
content,
|
|
107
|
+
} as OpenAI.Chat.ChatCompletionMessageParam);
|
|
108
|
+
} else {
|
|
109
|
+
// Simple text message or new format (content array)
|
|
110
|
+
formattedMessages.push({
|
|
111
|
+
role: msg.role as 'system' | 'user' | 'assistant',
|
|
112
|
+
content: msg.content,
|
|
113
|
+
} as OpenAI.Chat.ChatCompletionMessageParam);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return formattedMessages;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Validate model and get config
|
|
122
|
+
* For models with variants (e.g., model:thinking), first checks the full model ID
|
|
123
|
+
*/
|
|
124
|
+
async validateAndGetConfig(
|
|
125
|
+
modelId: string,
|
|
126
|
+
thinking?: boolean
|
|
127
|
+
): Promise<AIConfigurationSchema | null> {
|
|
128
|
+
// Build the actual model ID with optional :thinking suffix
|
|
129
|
+
const actualModelId = this.buildModelId(modelId, thinking);
|
|
130
|
+
const aiConfig = await this.aiConfigService.findByModelId(actualModelId);
|
|
131
|
+
if (!aiConfig) {
|
|
132
|
+
throw new Error(
|
|
133
|
+
`Model ${actualModelId} is not enabled. Please contact your administrator to enable this model.`
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
return aiConfig;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Build the model ID with optional :thinking suffix
|
|
141
|
+
*/
|
|
142
|
+
private buildModelId(model: string, thinking?: boolean): string {
|
|
143
|
+
if (thinking && model.endsWith(':thinking') === false) {
|
|
144
|
+
return `${model}:thinking`;
|
|
145
|
+
}
|
|
146
|
+
return model;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Build web search plugin configuration
|
|
151
|
+
*/
|
|
152
|
+
private buildWebSearchPlugin(
|
|
153
|
+
webSearch?: ChatCompletionOptions['webSearch']
|
|
154
|
+
): OpenRouterWebPlugin | undefined {
|
|
155
|
+
if (!webSearch?.enabled) {
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const plugin: OpenRouterWebPlugin = { id: 'web' };
|
|
160
|
+
|
|
161
|
+
if (webSearch.engine) {
|
|
162
|
+
plugin.engine = webSearch.engine;
|
|
163
|
+
}
|
|
164
|
+
if (webSearch.maxResults) {
|
|
165
|
+
plugin.max_results = webSearch.maxResults;
|
|
166
|
+
}
|
|
167
|
+
if (webSearch.searchPrompt) {
|
|
168
|
+
plugin.search_prompt = webSearch.searchPrompt;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return plugin;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Build file parser plugin configuration for PDF processing
|
|
176
|
+
*/
|
|
177
|
+
private buildFileParserPlugin(
|
|
178
|
+
fileParser?: ChatCompletionOptions['fileParser']
|
|
179
|
+
): OpenRouterFileParserPlugin | undefined {
|
|
180
|
+
if (!fileParser?.enabled) {
|
|
181
|
+
return undefined;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const plugin: OpenRouterFileParserPlugin = { id: 'file-parser' };
|
|
185
|
+
|
|
186
|
+
if (fileParser.pdf?.engine) {
|
|
187
|
+
plugin.pdf = { engine: fileParser.pdf.engine };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return plugin;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Build all plugins array from options
|
|
195
|
+
*/
|
|
196
|
+
private buildPlugins(options: ChatCompletionOptions): OpenRouterPlugin[] | undefined {
|
|
197
|
+
const plugins: OpenRouterPlugin[] = [];
|
|
198
|
+
|
|
199
|
+
const webSearchPlugin = this.buildWebSearchPlugin(options.webSearch);
|
|
200
|
+
if (webSearchPlugin) {
|
|
201
|
+
plugins.push(webSearchPlugin);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const fileParserPlugin = this.buildFileParserPlugin(options.fileParser);
|
|
205
|
+
if (fileParserPlugin) {
|
|
206
|
+
plugins.push(fileParserPlugin);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return plugins.length > 0 ? plugins : undefined;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Parse annotations from OpenRouter response to our format
|
|
214
|
+
*/
|
|
215
|
+
private parseAnnotations(
|
|
216
|
+
message: MessageWithAnnotations | undefined | null
|
|
217
|
+
): UrlCitationAnnotation[] | undefined {
|
|
218
|
+
if (!message?.annotations || !Array.isArray(message.annotations)) {
|
|
219
|
+
return undefined;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const annotations: UrlCitationAnnotation[] = [];
|
|
223
|
+
|
|
224
|
+
for (const annotation of message.annotations) {
|
|
225
|
+
if (annotation.type === 'url_citation' && annotation.url_citation) {
|
|
226
|
+
annotations.push({
|
|
227
|
+
type: 'url_citation',
|
|
228
|
+
urlCitation: {
|
|
229
|
+
url: annotation.url_citation.url,
|
|
230
|
+
title: annotation.url_citation.title,
|
|
231
|
+
content: annotation.url_citation.content,
|
|
232
|
+
startIndex: annotation.url_citation.start_index,
|
|
233
|
+
endIndex: annotation.url_citation.end_index,
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return annotations.length > 0 ? annotations : undefined;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Send a chat message to the specified model
|
|
244
|
+
* @param messages - Array of messages for conversation
|
|
245
|
+
* @param options - Chat options including model, temperature, webSearch, thinking, etc.
|
|
246
|
+
*/
|
|
247
|
+
async chat(
|
|
248
|
+
messages: ChatMessageSchema[],
|
|
249
|
+
options: ChatCompletionOptions
|
|
250
|
+
): Promise<ChatCompletionResponse> {
|
|
251
|
+
try {
|
|
252
|
+
// Validate model and get config (pass thinking option for variant checking)
|
|
253
|
+
const aiConfig = await this.validateAndGetConfig(options.model, options.thinking);
|
|
254
|
+
|
|
255
|
+
// Build model ID with optional :thinking suffix
|
|
256
|
+
const modelId = this.buildModelId(options.model, options.thinking);
|
|
257
|
+
|
|
258
|
+
// Apply system prompt from config if available
|
|
259
|
+
const formattedMessages = this.formatMessages(messages, aiConfig?.systemPrompt);
|
|
260
|
+
|
|
261
|
+
// Build request with optional plugins (web search, file parser)
|
|
262
|
+
const request: OpenRouterChatCompletionRequest = {
|
|
263
|
+
model: modelId,
|
|
264
|
+
messages: formattedMessages,
|
|
265
|
+
temperature: options.temperature ?? 0.7,
|
|
266
|
+
max_tokens: options.maxTokens ?? 4096,
|
|
267
|
+
top_p: options.topP,
|
|
268
|
+
stream: false,
|
|
269
|
+
plugins: this.buildPlugins(options),
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
// Send request with automatic renewal and retry logic
|
|
273
|
+
const response = await this.openRouterProvider.sendRequest((client) =>
|
|
274
|
+
client.chat.completions.create(
|
|
275
|
+
request as OpenAI.Chat.ChatCompletionCreateParamsNonStreaming
|
|
276
|
+
)
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
// Extract token usage if available
|
|
280
|
+
const tokenUsage = response.usage
|
|
281
|
+
? {
|
|
282
|
+
promptTokens: response.usage.prompt_tokens,
|
|
283
|
+
completionTokens: response.usage.completion_tokens,
|
|
284
|
+
totalTokens: response.usage.total_tokens,
|
|
285
|
+
}
|
|
286
|
+
: undefined;
|
|
287
|
+
|
|
288
|
+
// Track usage if config is available
|
|
289
|
+
if (aiConfig?.id && tokenUsage) {
|
|
290
|
+
await this.aiUsageService.trackChatUsage(
|
|
291
|
+
aiConfig.id,
|
|
292
|
+
tokenUsage.promptTokens,
|
|
293
|
+
tokenUsage.completionTokens,
|
|
294
|
+
modelId // pass the actual model ID used
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Parse annotations from response (for web search results)
|
|
299
|
+
const annotations = this.parseAnnotations(
|
|
300
|
+
response.choices[0]?.message as MessageWithAnnotations | undefined
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
return {
|
|
304
|
+
text: response.choices[0]?.message?.content || '',
|
|
305
|
+
annotations,
|
|
306
|
+
metadata: {
|
|
307
|
+
model: modelId,
|
|
308
|
+
usage: tokenUsage,
|
|
309
|
+
},
|
|
310
|
+
};
|
|
311
|
+
} catch (error) {
|
|
312
|
+
logger.error('Chat error', { error });
|
|
313
|
+
throw new Error(
|
|
314
|
+
`Failed to get response: ${error instanceof Error ? error.message : String(error)}`
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Stream a chat response
|
|
321
|
+
* @param messages - Array of messages for conversation
|
|
322
|
+
* @param options - Chat options including model, temperature, webSearch, thinking, etc.
|
|
323
|
+
*/
|
|
324
|
+
async *streamChat(
|
|
325
|
+
messages: ChatMessageSchema[],
|
|
326
|
+
options: ChatCompletionOptions
|
|
327
|
+
): AsyncGenerator<{
|
|
328
|
+
chunk?: string;
|
|
329
|
+
tokenUsage?: { promptTokens?: number; completionTokens?: number; totalTokens?: number };
|
|
330
|
+
annotations?: UrlCitationAnnotation[];
|
|
331
|
+
}> {
|
|
332
|
+
try {
|
|
333
|
+
// Validate model and get config (pass thinking option for variant checking)
|
|
334
|
+
const aiConfig = await this.validateAndGetConfig(options.model, options.thinking);
|
|
335
|
+
|
|
336
|
+
// Build model ID with optional :thinking suffix
|
|
337
|
+
const modelId = this.buildModelId(options.model, options.thinking);
|
|
338
|
+
|
|
339
|
+
// Apply system prompt from config if available
|
|
340
|
+
const formattedMessages = this.formatMessages(messages, aiConfig?.systemPrompt);
|
|
341
|
+
|
|
342
|
+
// Build request with optional plugins (web search, file parser)
|
|
343
|
+
const request: OpenRouterChatCompletionStreamingRequest = {
|
|
344
|
+
model: modelId,
|
|
345
|
+
messages: formattedMessages,
|
|
346
|
+
temperature: options.temperature ?? 0.7,
|
|
347
|
+
max_tokens: options.maxTokens ?? 4096,
|
|
348
|
+
top_p: options.topP,
|
|
349
|
+
stream: true,
|
|
350
|
+
plugins: this.buildPlugins(options),
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
// Send request with automatic renewal and retry logic
|
|
354
|
+
const stream = await this.openRouterProvider.sendRequest((client) =>
|
|
355
|
+
client.chat.completions.create(request as OpenAI.Chat.ChatCompletionCreateParamsStreaming)
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
const tokenUsage = {
|
|
359
|
+
promptTokens: 0,
|
|
360
|
+
completionTokens: 0,
|
|
361
|
+
totalTokens: 0,
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
// Collect annotations from streaming response
|
|
365
|
+
let collectedAnnotations: UrlCitationAnnotation[] | undefined;
|
|
366
|
+
|
|
367
|
+
for await (const chunk of stream) {
|
|
368
|
+
const content = chunk.choices[0]?.delta?.content;
|
|
369
|
+
if (content) {
|
|
370
|
+
yield { chunk: content };
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Check for annotations in the chunk (web search results)
|
|
374
|
+
const chunkAnnotations = this.parseAnnotations(
|
|
375
|
+
chunk.choices[0]?.delta as MessageWithAnnotations | undefined
|
|
376
|
+
);
|
|
377
|
+
if (chunkAnnotations) {
|
|
378
|
+
collectedAnnotations = collectedAnnotations || [];
|
|
379
|
+
collectedAnnotations.push(...chunkAnnotations);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Check if this chunk contains usage data
|
|
383
|
+
if (chunk.usage) {
|
|
384
|
+
// Accumulate tokens instead of replacing
|
|
385
|
+
tokenUsage.promptTokens += chunk.usage.prompt_tokens || 0;
|
|
386
|
+
tokenUsage.completionTokens += chunk.usage.completion_tokens || 0;
|
|
387
|
+
tokenUsage.totalTokens += chunk.usage.total_tokens || 0;
|
|
388
|
+
|
|
389
|
+
// Yield the accumulated usage
|
|
390
|
+
yield { tokenUsage: { ...tokenUsage } };
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Yield annotations at the end if present
|
|
395
|
+
if (collectedAnnotations && collectedAnnotations.length > 0) {
|
|
396
|
+
yield { annotations: collectedAnnotations };
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Track usage after streaming completes
|
|
400
|
+
if (aiConfig?.id && tokenUsage.totalTokens > 0) {
|
|
401
|
+
await this.aiUsageService.trackChatUsage(
|
|
402
|
+
aiConfig.id,
|
|
403
|
+
tokenUsage.promptTokens,
|
|
404
|
+
tokenUsage.completionTokens,
|
|
405
|
+
modelId // pass the actual model ID used
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
} catch (error) {
|
|
409
|
+
logger.error('Streaming error', { error });
|
|
410
|
+
throw new Error(
|
|
411
|
+
`Failed to stream response: ${error instanceof Error ? error.message : String(error)}`
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|