@harperfast/harper 5.2.0 → 5.2.2
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/bin/copyDb.ts +40 -11
- package/bin/harper.ts +20 -51
- package/bin/help.ts +216 -0
- package/components/Application.ts +236 -46
- package/components/ApplicationScope.ts +26 -0
- package/components/EntryHandler.ts +410 -105
- package/components/RuntimeModuleTracker.ts +189 -0
- package/components/Scope.ts +68 -24
- package/components/componentLoader.ts +70 -16
- package/components/deployLifecycle.ts +119 -33
- package/components/deploymentRecorder.ts +102 -11
- package/components/mcp/tools/operations.ts +6 -0
- package/components/operations.js +8 -6
- package/config/configUtils.ts +78 -20
- package/config/harperConfigEnvVars.ts +114 -17
- package/config-root.schema.json +10 -0
- package/dataLayer/harperBridge/ResourceBridge.ts +26 -5
- package/dataLayer/harperBridge/lmdbBridge/lmdbUtility/initializePaths.js +4 -4
- package/dataLayer/hdbInfoController.ts +8 -0
- package/dataLayer/schemaDescribe.ts +2 -1
- package/dist/bin/copyDb.js +33 -9
- package/dist/bin/copyDb.js.map +1 -1
- package/dist/bin/harper.d.ts +6 -0
- package/dist/bin/harper.js +18 -50
- package/dist/bin/harper.js.map +1 -1
- package/dist/bin/help.d.ts +8 -0
- package/dist/bin/help.js +192 -0
- package/dist/bin/help.js.map +1 -0
- package/dist/components/Application.d.ts +16 -1
- package/dist/components/Application.js +210 -38
- package/dist/components/Application.js.map +1 -1
- package/dist/components/ApplicationScope.d.ts +7 -0
- package/dist/components/ApplicationScope.js +22 -0
- package/dist/components/ApplicationScope.js.map +1 -1
- package/dist/components/EntryHandler.d.ts +4 -4
- package/dist/components/EntryHandler.js +386 -95
- package/dist/components/EntryHandler.js.map +1 -1
- package/dist/components/RuntimeModuleTracker.d.ts +11 -0
- package/dist/components/RuntimeModuleTracker.js +189 -0
- package/dist/components/RuntimeModuleTracker.js.map +1 -0
- package/dist/components/Scope.d.ts +1 -0
- package/dist/components/Scope.js +69 -22
- package/dist/components/Scope.js.map +1 -1
- package/dist/components/componentLoader.js +69 -10
- package/dist/components/componentLoader.js.map +1 -1
- package/dist/components/deployLifecycle.d.ts +6 -2
- package/dist/components/deployLifecycle.js +109 -31
- package/dist/components/deployLifecycle.js.map +1 -1
- package/dist/components/deploymentRecorder.d.ts +6 -0
- package/dist/components/deploymentRecorder.js +94 -12
- package/dist/components/deploymentRecorder.js.map +1 -1
- package/dist/components/mcp/tools/operations.js +6 -0
- package/dist/components/mcp/tools/operations.js.map +1 -1
- package/dist/components/operations.js +8 -6
- package/dist/components/operations.js.map +1 -1
- package/dist/config/configUtils.d.ts +6 -0
- package/dist/config/configUtils.js +78 -18
- package/dist/config/configUtils.js.map +1 -1
- package/dist/config/harperConfigEnvVars.js +108 -16
- package/dist/config/harperConfigEnvVars.js.map +1 -1
- package/dist/dataLayer/harperBridge/ResourceBridge.js +12 -5
- package/dist/dataLayer/harperBridge/ResourceBridge.js.map +1 -1
- package/dist/dataLayer/harperBridge/lmdbBridge/lmdbUtility/initializePaths.js +4 -4
- package/dist/dataLayer/harperBridge/lmdbBridge/lmdbUtility/initializePaths.js.map +1 -1
- package/dist/dataLayer/hdbInfoController.js +4 -0
- package/dist/dataLayer/hdbInfoController.js.map +1 -1
- package/dist/dataLayer/schemaDescribe.js +2 -1
- package/dist/dataLayer/schemaDescribe.js.map +1 -1
- package/dist/resources/DatabaseTransaction.d.ts +57 -0
- package/dist/resources/DatabaseTransaction.js +284 -104
- package/dist/resources/DatabaseTransaction.js.map +1 -1
- package/dist/resources/ResourceInterface.d.ts +8 -2
- package/dist/resources/ResourceInterface.js.map +1 -1
- package/dist/resources/Resources.js +22 -4
- package/dist/resources/Resources.js.map +1 -1
- package/dist/resources/Table.d.ts +8 -6
- package/dist/resources/Table.js +158 -31
- package/dist/resources/Table.js.map +1 -1
- package/dist/resources/analytics/write.js +6 -6
- package/dist/resources/analytics/write.js.map +1 -1
- package/dist/resources/auditStore.js +27 -1
- package/dist/resources/auditStore.js.map +1 -1
- package/dist/resources/blob.d.ts +0 -1
- package/dist/resources/blob.js +15 -7
- package/dist/resources/blob.js.map +1 -1
- package/dist/resources/databases.d.ts +22 -5
- package/dist/resources/databases.js +118 -9
- package/dist/resources/databases.js.map +1 -1
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.d.ts +15 -2
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js +128 -39
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js.map +1 -1
- package/dist/resources/jsResource.d.ts +4 -26
- package/dist/resources/jsResource.js +5 -59
- package/dist/resources/jsResource.js.map +1 -1
- package/dist/resources/models/Models.d.ts +11 -1
- package/dist/resources/models/Models.js +10 -1
- package/dist/resources/models/Models.js.map +1 -1
- package/dist/resources/models/backendRegistry.d.ts +9 -0
- package/dist/resources/models/backendRegistry.js +10 -0
- package/dist/resources/models/backendRegistry.js.map +1 -1
- package/dist/resources/models/openaiStream.d.ts +16 -1
- package/dist/resources/models/openaiStream.js +113 -21
- package/dist/resources/models/openaiStream.js.map +1 -1
- package/dist/resources/models/v1/chatCompletions.d.ts +54 -0
- package/dist/resources/models/v1/chatCompletions.js +115 -0
- package/dist/resources/models/v1/chatCompletions.js.map +1 -0
- package/dist/resources/models/v1/embeddings.d.ts +11 -0
- package/dist/resources/models/v1/embeddings.js +71 -0
- package/dist/resources/models/v1/embeddings.js.map +1 -0
- package/dist/resources/models/v1/errors.d.ts +54 -0
- package/dist/resources/models/v1/errors.js +130 -0
- package/dist/resources/models/v1/errors.js.map +1 -0
- package/dist/resources/models/v1/index.d.ts +36 -0
- package/dist/resources/models/v1/index.js +75 -0
- package/dist/resources/models/v1/index.js.map +1 -0
- package/dist/resources/models/v1/models.d.ts +26 -0
- package/dist/resources/models/v1/models.js +44 -0
- package/dist/resources/models/v1/models.js.map +1 -0
- package/dist/resources/models/v1/translation.d.ts +133 -0
- package/dist/resources/models/v1/translation.js +298 -0
- package/dist/resources/models/v1/translation.js.map +1 -0
- package/dist/resources/roles.d.ts +1 -1
- package/dist/resources/roles.js +54 -7
- package/dist/resources/roles.js.map +1 -1
- package/dist/resources/search.d.ts +1 -1
- package/dist/resources/search.js +7 -3
- package/dist/resources/search.js.map +1 -1
- package/dist/security/jsLoader.js +84 -33
- package/dist/security/jsLoader.js.map +1 -1
- package/dist/security/role.js +4 -0
- package/dist/security/role.js.map +1 -1
- package/dist/security/superUserGuard.d.ts +7 -0
- package/dist/security/superUserGuard.js +23 -0
- package/dist/security/superUserGuard.js.map +1 -0
- package/dist/security/tokenAuthentication.d.ts +0 -1
- package/dist/security/tokenAuthentication.js +6 -2
- package/dist/security/tokenAuthentication.js.map +1 -1
- package/dist/security/user.d.ts +6 -1
- package/dist/security/user.js +23 -1
- package/dist/security/user.js.map +1 -1
- package/dist/server/http.d.ts +23 -2
- package/dist/server/http.js +99 -12
- package/dist/server/http.js.map +1 -1
- package/dist/server/operationsServer.d.ts +0 -2
- package/dist/server/operationsServer.js.map +1 -1
- package/dist/server/serverHelpers/multipartParser.js +9 -0
- package/dist/server/serverHelpers/multipartParser.js.map +1 -1
- package/dist/server/serverHelpers/operationAuthorizationState.d.ts +2 -0
- package/dist/server/serverHelpers/operationAuthorizationState.js +13 -0
- package/dist/server/serverHelpers/operationAuthorizationState.js.map +1 -0
- package/dist/server/serverHelpers/registeredOperations.d.ts +3 -2
- package/dist/server/serverHelpers/registeredOperations.js +14 -13
- package/dist/server/serverHelpers/registeredOperations.js.map +1 -1
- package/dist/server/serverHelpers/serverHandlers.js +19 -3
- package/dist/server/serverHelpers/serverHandlers.js.map +1 -1
- package/dist/server/serverHelpers/serverUtilities.d.ts +1 -1
- package/dist/server/serverHelpers/serverUtilities.js +10 -7
- package/dist/server/serverHelpers/serverUtilities.js.map +1 -1
- package/dist/server/static.js +57 -18
- package/dist/server/static.js.map +1 -1
- package/dist/server/storageReclamation.d.ts +17 -0
- package/dist/server/storageReclamation.js +90 -6
- package/dist/server/storageReclamation.js.map +1 -1
- package/dist/server/threads/manageThreads.js +8 -0
- package/dist/server/threads/manageThreads.js.map +1 -1
- package/dist/server/threads/socketRouter.js +20 -0
- package/dist/server/threads/socketRouter.js.map +1 -1
- package/dist/server/threads/threadServer.js +12 -0
- package/dist/server/threads/threadServer.js.map +1 -1
- package/dist/sqlEngine/diff/differential.js +7 -3
- package/dist/sqlEngine/diff/differential.js.map +1 -1
- package/dist/sqlEngine/parser/normalizer.js +37 -9
- package/dist/sqlEngine/parser/normalizer.js.map +1 -1
- package/dist/sqlTranslator/index.js +6 -1
- package/dist/sqlTranslator/index.js.map +1 -1
- package/dist/upgrade/upgradePrompt.d.ts +2 -2
- package/dist/upgrade/upgradePrompt.js +22 -3
- package/dist/upgrade/upgradePrompt.js.map +1 -1
- package/dist/utility/environment/environmentManager.d.ts +11 -0
- package/dist/utility/environment/environmentManager.js +154 -0
- package/dist/utility/environment/environmentManager.js.map +1 -1
- package/dist/utility/errors/commonErrors.d.ts +1 -0
- package/dist/utility/errors/commonErrors.js +1 -0
- package/dist/utility/errors/commonErrors.js.map +1 -1
- package/dist/utility/hdbTerms.d.ts +2 -0
- package/dist/utility/hdbTerms.js +2 -0
- package/dist/utility/hdbTerms.js.map +1 -1
- package/dist/utility/install/installer.js +32 -0
- package/dist/utility/install/installer.js.map +1 -1
- package/dist/utility/lmdb/environmentUtility.js +2 -1
- package/dist/utility/lmdb/environmentUtility.js.map +1 -1
- package/dist/utility/logging/harper_logger.d.ts +7 -0
- package/dist/utility/logging/harper_logger.js +57 -24
- package/dist/utility/logging/harper_logger.js.map +1 -1
- package/npm-shrinkwrap.json +208 -198
- package/package.json +10 -8
- package/resources/DESIGN.md +1 -0
- package/resources/DatabaseTransaction.ts +313 -102
- package/resources/ResourceInterface.ts +8 -2
- package/resources/Resources.ts +22 -4
- package/resources/Table.ts +398 -259
- package/resources/analytics/write.ts +22 -20
- package/resources/auditStore.ts +32 -1
- package/resources/blob.ts +15 -8
- package/resources/databases.ts +123 -12
- package/resources/indexes/HierarchicalNavigableSmallWorld.ts +119 -45
- package/resources/jsResource.ts +5 -62
- package/resources/models/Models.ts +14 -1
- package/resources/models/backendRegistry.ts +10 -0
- package/resources/models/openaiStream.ts +131 -19
- package/resources/models/v1/chatCompletions.ts +128 -0
- package/resources/models/v1/embeddings.ts +70 -0
- package/resources/models/v1/errors.ts +141 -0
- package/resources/models/v1/index.ts +72 -0
- package/resources/models/v1/models.ts +53 -0
- package/resources/models/v1/translation.ts +362 -0
- package/resources/roles.ts +67 -7
- package/resources/search.ts +8 -3
- package/security/jsLoader.ts +84 -30
- package/security/role.ts +7 -0
- package/security/superUserGuard.ts +20 -0
- package/security/tokenAuthentication.ts +6 -3
- package/security/user.ts +26 -1
- package/server/DESIGN.md +45 -34
- package/server/http.ts +100 -13
- package/server/operationsServer.ts +0 -2
- package/server/serverHelpers/multipartParser.ts +9 -0
- package/server/serverHelpers/operationAuthorizationState.ts +11 -0
- package/server/serverHelpers/registeredOperations.ts +19 -15
- package/server/serverHelpers/serverHandlers.js +20 -3
- package/server/serverHelpers/serverUtilities.ts +10 -7
- package/server/static.ts +75 -20
- package/server/storageReclamation.ts +104 -8
- package/server/threads/manageThreads.js +8 -0
- package/server/threads/socketRouter.ts +20 -0
- package/server/threads/threadServer.js +11 -0
- package/sqlTranslator/index.ts +6 -1
- package/static/defaultConfig.yaml +2 -0
- package/studio/web/assets/{Chat-DoVWScmq.js → Chat-BMUjPklt.js} +5 -5
- package/studio/web/assets/{Chat-DoVWScmq.js.map → Chat-BMUjPklt.js.map} +1 -1
- package/studio/web/assets/{FloatingChat-UZ2NsUOZ.js → FloatingChat-Cbghuxe5.js} +5 -5
- package/studio/web/assets/{FloatingChat-UZ2NsUOZ.js.map → FloatingChat-Cbghuxe5.js.map} +1 -1
- package/studio/web/assets/{apiToken-BUI_04o7.js → apiToken-CkgTQAqc.js} +2 -2
- package/studio/web/assets/{apiToken-BUI_04o7.js.map → apiToken-CkgTQAqc.js.map} +1 -1
- package/studio/web/assets/{applications-D03NA7wW.js → applications-WvF4NOTN.js} +13 -13
- package/studio/web/assets/applications-WvF4NOTN.js.map +1 -0
- package/studio/web/assets/chevron-right-anDJCGlp.js +2 -0
- package/studio/web/assets/chevron-right-anDJCGlp.js.map +1 -0
- package/studio/web/assets/index-CDeqVx71.css +1 -0
- package/studio/web/assets/index-Sevt3MVx.js +819 -0
- package/studio/web/assets/index-Sevt3MVx.js.map +1 -0
- package/studio/web/assets/index.lazy-Dg5YYPGg.js +14 -0
- package/studio/web/assets/{index.lazy-Dx3MpyDC.js.map → index.lazy-Dg5YYPGg.js.map} +1 -1
- package/studio/web/assets/{jsonMode-Cjj7dtlr.js → jsonMode-CBSEleE7.js} +2 -2
- package/studio/web/assets/{jsonMode-Cjj7dtlr.js.map → jsonMode-CBSEleE7.js.map} +1 -1
- package/studio/web/assets/languageServices-gxedeeTo.js +2 -0
- package/studio/web/assets/languageServices-gxedeeTo.js.map +1 -0
- package/studio/web/assets/{notifications-0edoFTsb.js → notifications-Ap84YyQp.js} +2 -2
- package/studio/web/assets/{notifications-0edoFTsb.js.map → notifications-Ap84YyQp.js.map} +1 -1
- package/studio/web/assets/notifications-vehqyOLM.js +2 -0
- package/studio/web/assets/{notifications-CwKhipK7.js.map → notifications-vehqyOLM.js.map} +1 -1
- package/studio/web/assets/{pollUnlessForbidden-HHdQZW1N.js → pollUnlessForbidden-BhzefVRn.js} +2 -2
- package/studio/web/assets/pollUnlessForbidden-BhzefVRn.js.map +1 -0
- package/studio/web/assets/{profile-DUfEPQtx.js → profile-DzDpRLHY.js} +2 -2
- package/studio/web/assets/{profile-DUfEPQtx.js.map → profile-DzDpRLHY.js.map} +1 -1
- package/studio/web/assets/queryClient-CgtkoiCY.js +2 -0
- package/studio/web/assets/queryClient-CgtkoiCY.js.map +1 -0
- package/studio/web/assets/regions-DpJ8s7iN.js +2 -0
- package/studio/web/assets/regions-DpJ8s7iN.js.map +1 -0
- package/studio/web/assets/register-BDy01-q3.js +3 -0
- package/studio/web/assets/register-BDy01-q3.js.map +1 -0
- package/studio/web/assets/register-CQHRwNP4.js +3 -0
- package/studio/web/assets/register-CQHRwNP4.js.map +1 -0
- package/studio/web/assets/{setComponentFile-DMPo4UjC.js → setComponentFile-D0aPcI3W.js} +2 -2
- package/studio/web/assets/{setComponentFile-DMPo4UjC.js.map → setComponentFile-D0aPcI3W.js.map} +1 -1
- package/studio/web/assets/setup-I15FAF9H.js +3 -0
- package/studio/web/assets/{setup-B56Oz1_u.js.map → setup-I15FAF9H.js.map} +1 -1
- package/studio/web/assets/status-BF1eJshJ.js +62 -0
- package/studio/web/assets/status-BF1eJshJ.js.map +1 -0
- package/studio/web/assets/{swagger-ui-react-lQrBxfwM.js → swagger-ui-react-DwSPXXhl.js} +2 -2
- package/studio/web/assets/{swagger-ui-react-lQrBxfwM.js.map → swagger-ui-react-DwSPXXhl.js.map} +1 -1
- package/studio/web/assets/{textarea-DhLMZ3PA.js → table-DpOjNQHt.js} +2 -10
- package/studio/web/assets/table-DpOjNQHt.js.map +1 -0
- package/studio/web/assets/{tsMode-CrHCRjTK.js → tsMode-Bu51A-VG.js} +2 -2
- package/studio/web/assets/{tsMode-CrHCRjTK.js.map → tsMode-Bu51A-VG.js.map} +1 -1
- package/studio/web/assets/{useEntityRestURL-DoaBMEvU.js → useEntityRestURL-DrV1zMrN.js} +2 -2
- package/studio/web/assets/{useEntityRestURL-DoaBMEvU.js.map → useEntityRestURL-DrV1zMrN.js.map} +1 -1
- package/studio/web/index.html +6 -7
- package/upgrade/upgradePrompt.ts +22 -3
- package/utility/environment/environmentManager.ts +152 -0
- package/utility/errors/commonErrors.ts +2 -0
- package/utility/hdbTerms.ts +2 -0
- package/utility/install/installer.ts +37 -0
- package/utility/lmdb/environmentUtility.ts +2 -1
- package/utility/logging/harper_logger.ts +57 -26
- package/studio/web/assets/applications-D03NA7wW.js.map +0 -1
- package/studio/web/assets/index-Bh_CNAHr.js +0 -809
- package/studio/web/assets/index-Bh_CNAHr.js.map +0 -1
- package/studio/web/assets/index-C8Mv9kHV.css +0 -1
- package/studio/web/assets/index.lazy-Dx3MpyDC.js +0 -14
- package/studio/web/assets/notifications-CwKhipK7.js +0 -2
- package/studio/web/assets/pollUnlessForbidden-HHdQZW1N.js.map +0 -1
- package/studio/web/assets/queryClient-CbA8wM7J.js +0 -2
- package/studio/web/assets/queryClient-CbA8wM7J.js.map +0 -1
- package/studio/web/assets/setup-B56Oz1_u.js +0 -3
- package/studio/web/assets/status-BAod7p3o.js +0 -62
- package/studio/web/assets/status-BAod7p3o.js.map +0 -1
- package/studio/web/assets/textarea-DhLMZ3PA.js.map +0 -1
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/v1/*` OpenAI-compatible gateway (#631).
|
|
3
|
+
*
|
|
4
|
+
* Registers three REST resources on the REST port:
|
|
5
|
+
* POST /v1/embeddings → V1Embeddings
|
|
6
|
+
* POST /v1/chat/completions → V1ChatCompletions
|
|
7
|
+
* GET /v1/models → V1Models
|
|
8
|
+
*
|
|
9
|
+
* Off by default, and `defaultConfig.yaml` deliberately ships no `modelsGateway`
|
|
10
|
+
* block: with the key absent the root loader skips the component before resolving
|
|
11
|
+
* it, so none of this module graph is imported on an instance that does not use
|
|
12
|
+
* the gateway. Opt in by adding the block to `harperdb-config.yaml` with
|
|
13
|
+
* `enabled: true`, or via `set_configuration` (`modelsGateway_enabled`).
|
|
14
|
+
* `enabled: false` is honored too, for an instance that wants the block present
|
|
15
|
+
* but inert — that costs the import, which is why it is not the shipped default.
|
|
16
|
+
*
|
|
17
|
+
* Example (opt in). `rest` is required: these are REST-served resources and the
|
|
18
|
+
* gateway deliberately does not force REST to start (see `handleApplication`).
|
|
19
|
+
* Without it the resources register but every `/v1/*` path 404s.
|
|
20
|
+
*
|
|
21
|
+
* ```yaml
|
|
22
|
+
* rest: true
|
|
23
|
+
* modelsGateway:
|
|
24
|
+
* enabled: true
|
|
25
|
+
* models:
|
|
26
|
+
* generative:
|
|
27
|
+
* default:
|
|
28
|
+
* backend: ollama
|
|
29
|
+
* model: llama3.2
|
|
30
|
+
* ```
|
|
31
|
+
*
|
|
32
|
+
* All three endpoints require `super_user` permission. Anonymous or
|
|
33
|
+
* insufficient-privilege requests receive a well-formed OpenAI error envelope.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import type { Scope } from '../../../components/Scope.ts';
|
|
37
|
+
import harperLogger from '../../../utility/logging/harper_logger.ts';
|
|
38
|
+
import { getConfigObj } from '../../../config/configUtils.ts';
|
|
39
|
+
import { V1Embeddings } from './embeddings.ts';
|
|
40
|
+
import { V1ChatCompletions } from './chatCompletions.ts';
|
|
41
|
+
import { V1Models } from './models.ts';
|
|
42
|
+
|
|
43
|
+
export function handleApplication(scope: Scope): void {
|
|
44
|
+
if (!scope.options.get(['enabled'])) return;
|
|
45
|
+
// These resources are served by REST's middleware chain, so the instance must also
|
|
46
|
+
// have a `rest`/`REST` config section — the gateway deliberately does NOT force REST
|
|
47
|
+
// to start. Doing so requires reaching into REST's module state before application
|
|
48
|
+
// configs have loaded, which silently discards an app's own `rest` options (webSocket,
|
|
49
|
+
// urlPath/host, middleware ordering). Core has no supported way yet for a component to
|
|
50
|
+
// declare "I serve REST resources"; that gap is tracked separately.
|
|
51
|
+
//
|
|
52
|
+
// Warn rather than fail: an app loaded later may still declare `rest`, so absence here
|
|
53
|
+
// is not conclusive. But defaultConfig ships no `rest` section, so enabling the gateway
|
|
54
|
+
// alone yields three registered resources and a 404 on every /v1 path — worth a line in
|
|
55
|
+
// the log instead of silence.
|
|
56
|
+
const rootConfig = getConfigObj() as Record<string, unknown> | undefined;
|
|
57
|
+
if (rootConfig && !rootConfig.rest && !rootConfig.REST) {
|
|
58
|
+
harperLogger.warn(
|
|
59
|
+
'modelsGateway is enabled but no `rest` section is configured; /v1/* endpoints are only served when REST is active'
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
// Explicit protocol visibility: these are REST-only wire-protocol endpoints. Without a
|
|
63
|
+
// policy, the shared registry matches them for every protocol lookup — WS dispatch could
|
|
64
|
+
// reach V1ChatCompletions.connect() and then fail iterating its non-iterable badRequest
|
|
65
|
+
// envelope, and they would surface through MQTT/GraphQL/MCP enumeration too. `sse` stays
|
|
66
|
+
// enabled for chat only: an explicit `Accept: text/event-stream` POST is dispatched via
|
|
67
|
+
// the sse lookup (REST.ts) and is a supported streaming client shape (see connect()).
|
|
68
|
+
const restOnly = { rest: true, sse: false, ws: false, mqtt: false, graphql: false, mcp: false };
|
|
69
|
+
scope.resources.set('v1/models', V1Models, restOnly);
|
|
70
|
+
scope.resources.set('v1/embeddings', V1Embeddings, restOnly);
|
|
71
|
+
scope.resources.set('v1/chat/completions', V1ChatCompletions, { ...restOnly, sse: true });
|
|
72
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `GET /v1/models` — OpenAI-compatible model list endpoint (#631).
|
|
3
|
+
*
|
|
4
|
+
* Enumerates all registered embedding and generative backends from the
|
|
5
|
+
* process-wide `backendRegistry`. The response mirrors the OpenAI shape:
|
|
6
|
+
* `{ object: 'list', data: [{ id, object: 'model', created, owned_by }] }`.
|
|
7
|
+
*
|
|
8
|
+
* `logicalName` (not `backend.name`) is the `id` — it's what callers pass as
|
|
9
|
+
* `model` in subsequent requests.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { Resource } from '../../Resource.ts';
|
|
13
|
+
import { listBackends } from '../backendRegistry.ts';
|
|
14
|
+
import { authorizeV1Request, type OpenAIErrorResponse } from './errors.ts';
|
|
15
|
+
|
|
16
|
+
export interface OAIModelEntry {
|
|
17
|
+
id: string;
|
|
18
|
+
object: 'model';
|
|
19
|
+
created: number;
|
|
20
|
+
owned_by: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface OAIModelList {
|
|
24
|
+
object: 'list';
|
|
25
|
+
data: OAIModelEntry[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// @ts-ignore — Resource base class is not typed for static dispatch; pattern mirrors login.ts
|
|
29
|
+
export class V1Models extends Resource {
|
|
30
|
+
// Reserve this fixed route: a later app registration at the same path becomes a
|
|
31
|
+
// loud conflict (ErrorResource) instead of silently replacing the gateway and its
|
|
32
|
+
// super_user gate. See Resources.set.
|
|
33
|
+
static reservedPath = true;
|
|
34
|
+
|
|
35
|
+
static get(_target: unknown, request: unknown): OAIModelList | OpenAIErrorResponse {
|
|
36
|
+
const authError = authorizeV1Request(request as any);
|
|
37
|
+
if (authError) return authError;
|
|
38
|
+
|
|
39
|
+
const created = Math.floor(Date.now() / 1000);
|
|
40
|
+
// OpenAI model ids are unique; a logical name registered for both generative and
|
|
41
|
+
// embedding (e.g. `default` in each section) is one model id to callers.
|
|
42
|
+
const ids = new Set<string>();
|
|
43
|
+
const data: OAIModelEntry[] = [];
|
|
44
|
+
for (const kind of ['generative', 'embedding'] as const) {
|
|
45
|
+
for (const { logicalName } of listBackends(kind)) {
|
|
46
|
+
if (ids.has(logicalName)) continue;
|
|
47
|
+
ids.add(logicalName);
|
|
48
|
+
data.push({ id: logicalName, object: 'model', created, owned_by: 'harper' });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return { object: 'list', data };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAI ↔ Harper internal shape mappers for the `/v1/*` gateway (#631).
|
|
3
|
+
*
|
|
4
|
+
* All functions are pure (no I/O, no side-effects) so they can be unit-tested
|
|
5
|
+
* in isolation without a running Harper instance.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { randomUUID } from 'node:crypto';
|
|
9
|
+
import type {
|
|
10
|
+
EmbedOpts,
|
|
11
|
+
GenerateInput,
|
|
12
|
+
GenerateOpts,
|
|
13
|
+
GenerateResult,
|
|
14
|
+
Message,
|
|
15
|
+
ToolCall,
|
|
16
|
+
ToolDef,
|
|
17
|
+
TokenUsage,
|
|
18
|
+
} from '../types.ts';
|
|
19
|
+
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// OpenAI wire shapes — inlined to avoid a runtime dependency on the SDK
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
export interface OAIMessageIn {
|
|
25
|
+
role: string;
|
|
26
|
+
/** May be null for assistant messages that only contain tool_calls. */
|
|
27
|
+
content: string | null;
|
|
28
|
+
tool_calls?: Array<{
|
|
29
|
+
id: string;
|
|
30
|
+
type: string;
|
|
31
|
+
function: { name: string; arguments: string };
|
|
32
|
+
}>;
|
|
33
|
+
/** Present on role === 'tool' messages. */
|
|
34
|
+
tool_call_id?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface OAIToolIn {
|
|
38
|
+
type: 'function';
|
|
39
|
+
function: {
|
|
40
|
+
name: string;
|
|
41
|
+
description?: string;
|
|
42
|
+
parameters?: object;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface OAIChatRequest {
|
|
47
|
+
model?: string;
|
|
48
|
+
messages: OAIMessageIn[];
|
|
49
|
+
tools?: OAIToolIn[];
|
|
50
|
+
/** Subset recognised: 'none' | 'auto' | 'required' | {type:'function', function:{name}}. */
|
|
51
|
+
tool_choice?: unknown;
|
|
52
|
+
temperature?: number;
|
|
53
|
+
/** OpenAI v1 field; superseded by max_completion_tokens in v1+. */
|
|
54
|
+
max_tokens?: number;
|
|
55
|
+
/** Preferred alias; takes precedence over max_tokens when both present. */
|
|
56
|
+
max_completion_tokens?: number;
|
|
57
|
+
response_format?: { type: string; json_schema?: unknown };
|
|
58
|
+
stream?: boolean;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// OpenAI request → Harper internal
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Map an OpenAI `messages` array to Harper `Message[]`.
|
|
67
|
+
* Normalises `tool_calls[].function.arguments` from JSON strings to parsed
|
|
68
|
+
* objects (Harper's internal contract); maps `tool_call_id` to `toolCallId`.
|
|
69
|
+
*/
|
|
70
|
+
export function translateMessages(oaiMessages: OAIMessageIn[]): Message[] {
|
|
71
|
+
return oaiMessages.map((m): Message => {
|
|
72
|
+
// Content parts are flattened to the text Harper's Message.content expects; shapes
|
|
73
|
+
// that cannot be flattened are rejected up front by validateChatRequest.
|
|
74
|
+
const content = Array.isArray(m.content)
|
|
75
|
+
? (m.content as Array<{ text: string }>).map((part) => part.text).join('')
|
|
76
|
+
: (m.content ?? '');
|
|
77
|
+
const base: Message = {
|
|
78
|
+
// `developer` is OpenAI's successor to `system` (the API itself treats the two
|
|
79
|
+
// identically, converting `system` to `developer` on models that use the newer
|
|
80
|
+
// hierarchy). Harper's internal Message union carries the channel as `system`,
|
|
81
|
+
// which every provider adapter maps to its system-instruction slot — so the
|
|
82
|
+
// instruction's priority is preserved rather than degraded to `user`. Roles
|
|
83
|
+
// outside the union are rejected up front by validateChatRequest, so the cast
|
|
84
|
+
// below is over a vetted set.
|
|
85
|
+
role: m.role === 'developer' ? 'system' : (m.role as Message['role']),
|
|
86
|
+
content,
|
|
87
|
+
};
|
|
88
|
+
if (m.tool_calls?.length) {
|
|
89
|
+
base.toolCalls = m.tool_calls.map((tc): ToolCall => {
|
|
90
|
+
let args: object;
|
|
91
|
+
try {
|
|
92
|
+
args = JSON.parse(tc.function.arguments);
|
|
93
|
+
} catch {
|
|
94
|
+
// Preserve unparseable argument strings under a sentinel key rather
|
|
95
|
+
// than dropping them — backend can decide what to do.
|
|
96
|
+
args = { _raw: tc.function.arguments };
|
|
97
|
+
}
|
|
98
|
+
return { id: tc.id, name: tc.function.name, arguments: args };
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
if (m.tool_call_id) base.toolCallId = m.tool_call_id;
|
|
102
|
+
return base;
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Map OpenAI `tools[]` to Harper `ToolDef[]`. */
|
|
107
|
+
export function translateTools(oaiTools: OAIToolIn[]): ToolDef[] {
|
|
108
|
+
return oaiTools.map((t): ToolDef => ({
|
|
109
|
+
name: t.function.name,
|
|
110
|
+
description: t.function.description ?? '',
|
|
111
|
+
parameters: t.function.parameters ?? {},
|
|
112
|
+
}));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Build Harper `GenerateInput` from translated messages and tool definitions.
|
|
117
|
+
* Uses the object form `{ messages, tools }` when tools are present, so that
|
|
118
|
+
* tool definitions travel alongside messages per Harper's type contract.
|
|
119
|
+
*/
|
|
120
|
+
export function toGenerateInput(messages: Message[], tools: ToolDef[] | undefined): GenerateInput {
|
|
121
|
+
if (tools?.length) return { messages, tools };
|
|
122
|
+
return messages;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** `tool_choice` values the internal contract can faithfully represent. */
|
|
126
|
+
function isRepresentableToolChoice(choice: unknown): boolean {
|
|
127
|
+
return choice === undefined || choice === null || choice === 'auto' || choice === 'none';
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Validate the OpenAI wire shapes this gateway maps, returning a client-facing
|
|
132
|
+
* message for a 400 or `null` when the request is well-formed.
|
|
133
|
+
*
|
|
134
|
+
* Kept separate from the mappers so malformed nested input (`messages: [null]`,
|
|
135
|
+
* `tool_calls: [{}]`, `tools: [{}]`) becomes an OpenAI-shaped 400 instead of a
|
|
136
|
+
* TypeError escaping the handler as an RFC 9457 500.
|
|
137
|
+
*/
|
|
138
|
+
export function validateChatRequest(body: OAIChatRequest): string | null {
|
|
139
|
+
const req = body as any;
|
|
140
|
+
// Top-level fields that control routing and framing first: a non-string `model`
|
|
141
|
+
// would silently invoke the configured default model (potentially expensive), and a
|
|
142
|
+
// truthy non-boolean `stream` ("false") would return SSE the client didn't ask for.
|
|
143
|
+
if (req.model !== undefined && typeof req.model !== 'string') return "'model' must be a string";
|
|
144
|
+
if (req.stream !== undefined && typeof req.stream !== 'boolean') return "'stream' must be a boolean";
|
|
145
|
+
if (req.temperature !== undefined) {
|
|
146
|
+
if (typeof req.temperature !== 'number' || !Number.isFinite(req.temperature)) {
|
|
147
|
+
return "'temperature' must be a finite number";
|
|
148
|
+
}
|
|
149
|
+
if (req.temperature < 0 || req.temperature > 2) return "'temperature' must be between 0 and 2";
|
|
150
|
+
}
|
|
151
|
+
for (const field of ['max_tokens', 'max_completion_tokens']) {
|
|
152
|
+
const value = req[field];
|
|
153
|
+
if (value === undefined) continue;
|
|
154
|
+
if (typeof value !== 'number' || !Number.isInteger(value) || value < 1) {
|
|
155
|
+
return `'${field}' must be a positive integer`;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (!Array.isArray(req.messages) || req.messages.length === 0) return "'messages' must be a non-empty array";
|
|
159
|
+
for (let i = 0; i < req.messages.length; i++) {
|
|
160
|
+
const m = req.messages[i];
|
|
161
|
+
if (!m || typeof m !== 'object' || Array.isArray(m)) return `'messages[${i}]' must be an object`;
|
|
162
|
+
if (typeof m.role !== 'string') return `'messages[${i}].role' must be a string`;
|
|
163
|
+
// Closed set: translateMessages casts into Harper's Message union, so an
|
|
164
|
+
// unknown role must be a loud 400 here — previously it was cast through and
|
|
165
|
+
// provider adapters degraded it to 'user', silently changing its priority.
|
|
166
|
+
// 'developer' is accepted and carried as 'system' (see translateMessages);
|
|
167
|
+
// the deprecated 'function' role is deliberately not supported.
|
|
168
|
+
if (!['system', 'developer', 'user', 'assistant', 'tool'].includes(m.role)) {
|
|
169
|
+
return `'messages[${i}].role' must be one of 'system', 'developer', 'user', 'assistant', 'tool'`;
|
|
170
|
+
}
|
|
171
|
+
// OpenAI allows content parts: [{ type: 'text', text: '...' }, ...]. Harper's
|
|
172
|
+
// Message.content is a string, so those are flattened in translateMessages; reject
|
|
173
|
+
// shapes we cannot flatten rather than passing a non-string downstream.
|
|
174
|
+
if (m.content !== undefined && m.content !== null && typeof m.content !== 'string') {
|
|
175
|
+
if (!Array.isArray(m.content)) return `'messages[${i}].content' must be a string, array of parts, or null`;
|
|
176
|
+
for (let p = 0; p < m.content.length; p++) {
|
|
177
|
+
const part = m.content[p];
|
|
178
|
+
if (!part || typeof part !== 'object') return `'messages[${i}].content[${p}]' must be an object`;
|
|
179
|
+
if (part.type !== 'text' || typeof part.text !== 'string') {
|
|
180
|
+
return `'messages[${i}].content[${p}]' must be a text part ({ type: 'text', text: string }); other part types are not supported yet`;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (m.tool_calls !== undefined) {
|
|
185
|
+
if (!Array.isArray(m.tool_calls)) return `'messages[${i}].tool_calls' must be an array`;
|
|
186
|
+
for (let j = 0; j < m.tool_calls.length; j++) {
|
|
187
|
+
const tc = m.tool_calls[j];
|
|
188
|
+
const at = `'messages[${i}].tool_calls[${j}]`;
|
|
189
|
+
if (!tc || typeof tc !== 'object') return `${at}' must be an object`;
|
|
190
|
+
if (!tc.function || typeof tc.function !== 'object') return `${at}.function' is required`;
|
|
191
|
+
if (typeof tc.function.name !== 'string') return `${at}.function.name' must be a string`;
|
|
192
|
+
if (typeof tc.function.arguments !== 'string') return `${at}.function.arguments' must be a JSON string`;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (req.tools !== undefined) {
|
|
197
|
+
if (!Array.isArray(req.tools)) return "'tools' must be an array";
|
|
198
|
+
for (let i = 0; i < req.tools.length; i++) {
|
|
199
|
+
const t = req.tools[i];
|
|
200
|
+
if (!t || typeof t !== 'object') return `'tools[${i}]' must be an object`;
|
|
201
|
+
if (!t.function || typeof t.function !== 'object') return `'tools[${i}].function' is required`;
|
|
202
|
+
if (typeof t.function.name !== 'string') return `'tools[${i}].function.name' must be a string`;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (!isRepresentableToolChoice(req.tool_choice)) {
|
|
206
|
+
// Better a clear 400 than silently downgrading 'required'/named selection to 'auto'.
|
|
207
|
+
return "'tool_choice' supports 'auto' and 'none'; 'required' and named function selection are not supported yet";
|
|
208
|
+
}
|
|
209
|
+
if (req.response_format !== undefined) {
|
|
210
|
+
const rf = req.response_format;
|
|
211
|
+
if (!rf || typeof rf !== 'object' || Array.isArray(rf)) return "'response_format' must be an object";
|
|
212
|
+
if (rf.type === 'json_schema') {
|
|
213
|
+
const wrapper = rf.json_schema;
|
|
214
|
+
if (!wrapper || typeof wrapper !== 'object') {
|
|
215
|
+
return "'response_format.json_schema' is required when type is 'json_schema'";
|
|
216
|
+
}
|
|
217
|
+
if (!wrapper.schema || typeof wrapper.schema !== 'object') {
|
|
218
|
+
return "'response_format.json_schema.schema' must be a JSON Schema object";
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Map an OpenAI chat-completion request body to `GenerateOpts`.
|
|
227
|
+
*
|
|
228
|
+
* `tool_choice` is honored by the caller, not here: `'none'` omits tools from the
|
|
229
|
+
* generate input entirely, and unrepresentable choices are rejected up front by
|
|
230
|
+
* `validateChatRequest`. Full in-process tool-call orchestration is #612 (out of
|
|
231
|
+
* scope for #631), so tool calls are always returned to the caller to invoke.
|
|
232
|
+
*
|
|
233
|
+
* Assumes `validateChatRequest` has already passed.
|
|
234
|
+
*/
|
|
235
|
+
export function toGenerateOpts(body: OAIChatRequest): GenerateOpts {
|
|
236
|
+
const opts: GenerateOpts = { toolMode: 'return' };
|
|
237
|
+
if (typeof body.model === 'string') opts.model = body.model;
|
|
238
|
+
if (typeof body.temperature === 'number') opts.temperature = body.temperature;
|
|
239
|
+
const maxTokens = body.max_completion_tokens ?? body.max_tokens;
|
|
240
|
+
if (typeof maxTokens === 'number') opts.maxTokens = maxTokens;
|
|
241
|
+
if (body.response_format) {
|
|
242
|
+
const rf = body.response_format;
|
|
243
|
+
if (rf.type === 'json_object') {
|
|
244
|
+
opts.responseFormat = 'json';
|
|
245
|
+
} else if (rf.type === 'json_schema') {
|
|
246
|
+
// The wire value is a wrapper ({ name, strict, schema }); Harper's contract wants
|
|
247
|
+
// the JSON Schema itself. Passing the wrapper makes the backend wrap it again and
|
|
248
|
+
// send metadata where the provider expects the schema.
|
|
249
|
+
opts.responseFormat = { schema: (rf.json_schema as { schema: object }).schema };
|
|
250
|
+
} else {
|
|
251
|
+
opts.responseFormat = 'text';
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return opts;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** Map an OpenAI embeddings request body to `EmbedOpts`. */
|
|
258
|
+
export function toEmbedOpts(body: { model?: string }): EmbedOpts {
|
|
259
|
+
const opts: EmbedOpts = {};
|
|
260
|
+
if (typeof body.model === 'string') opts.model = body.model;
|
|
261
|
+
return opts;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ---------------------------------------------------------------------------
|
|
265
|
+
// Harper internal → OpenAI response shapes
|
|
266
|
+
// ---------------------------------------------------------------------------
|
|
267
|
+
|
|
268
|
+
interface OAIToolCallOut {
|
|
269
|
+
id: string;
|
|
270
|
+
type: 'function';
|
|
271
|
+
function: { name: string; arguments: string };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
interface OAIAssistantMessage {
|
|
275
|
+
role: 'assistant';
|
|
276
|
+
/** null when the message contains only tool_calls. */
|
|
277
|
+
content: string | null;
|
|
278
|
+
tool_calls?: OAIToolCallOut[];
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export interface OAIChatCompletion {
|
|
282
|
+
id: string;
|
|
283
|
+
object: 'chat.completion';
|
|
284
|
+
created: number;
|
|
285
|
+
model: string;
|
|
286
|
+
choices: Array<{
|
|
287
|
+
index: number;
|
|
288
|
+
message: OAIAssistantMessage;
|
|
289
|
+
finish_reason: string;
|
|
290
|
+
}>;
|
|
291
|
+
usage: {
|
|
292
|
+
prompt_tokens: number;
|
|
293
|
+
completion_tokens: number;
|
|
294
|
+
total_tokens: number;
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function toOAIToolCalls(toolCalls: ToolCall[]): OAIToolCallOut[] {
|
|
299
|
+
return toolCalls.map((tc) => ({
|
|
300
|
+
id: tc.id,
|
|
301
|
+
type: 'function',
|
|
302
|
+
// Guard against backends that return arguments as a pre-serialised JSON string;
|
|
303
|
+
// passing a string through JSON.stringify would double-encode it.
|
|
304
|
+
function: {
|
|
305
|
+
name: tc.name,
|
|
306
|
+
arguments: typeof tc.arguments === 'string' ? tc.arguments : JSON.stringify(tc.arguments),
|
|
307
|
+
},
|
|
308
|
+
}));
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** Map a Harper `GenerateResult` to an OpenAI `chat.completion` response object. */
|
|
312
|
+
export function toChatCompletion(result: GenerateResult, model: string, id?: string): OAIChatCompletion {
|
|
313
|
+
const completionId = id ?? `chatcmpl-${randomUUID().replaceAll('-', '')}`;
|
|
314
|
+
const hasTools = !!result.toolCalls?.length;
|
|
315
|
+
const message: OAIAssistantMessage = {
|
|
316
|
+
role: 'assistant',
|
|
317
|
+
// OpenAI sets content to null when there are tool calls and no text content.
|
|
318
|
+
content: result.content || (hasTools ? null : ''),
|
|
319
|
+
};
|
|
320
|
+
if (hasTools) message.tool_calls = toOAIToolCalls(result.toolCalls!);
|
|
321
|
+
const usage = result.usage ?? {};
|
|
322
|
+
return {
|
|
323
|
+
id: completionId,
|
|
324
|
+
object: 'chat.completion',
|
|
325
|
+
created: Math.floor(Date.now() / 1000),
|
|
326
|
+
model,
|
|
327
|
+
choices: [{ index: 0, message, finish_reason: result.finishReason }],
|
|
328
|
+
usage: {
|
|
329
|
+
prompt_tokens: usage.promptTokens ?? 0,
|
|
330
|
+
completion_tokens: usage.completionTokens ?? 0,
|
|
331
|
+
total_tokens: (usage.promptTokens ?? 0) + (usage.completionTokens ?? 0),
|
|
332
|
+
},
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export interface OAIEmbedResponse {
|
|
337
|
+
object: 'list';
|
|
338
|
+
data: Array<{ embedding: number[]; index: number; object: 'embedding' }>;
|
|
339
|
+
model: string;
|
|
340
|
+
usage: { prompt_tokens: number; total_tokens: number };
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/** Map `Float32Array[]` from `models.embed()` to an OpenAI embeddings response. */
|
|
344
|
+
export function toEmbedResponse(vecs: Float32Array[], model: string, usage?: TokenUsage): OAIEmbedResponse {
|
|
345
|
+
return {
|
|
346
|
+
object: 'list',
|
|
347
|
+
data: vecs.map((vec, index) => ({
|
|
348
|
+
embedding: Array.from(vec),
|
|
349
|
+
index,
|
|
350
|
+
object: 'embedding',
|
|
351
|
+
})),
|
|
352
|
+
model,
|
|
353
|
+
// Embeddings have no completion side, so OpenAI reports the same count in both
|
|
354
|
+
// fields. Harper backends spell it `embeddingTokens` (with `promptTokens` as a
|
|
355
|
+
// fallback for backends that report it that way); mapping must be symmetric —
|
|
356
|
+
// a backend supplying only embeddingTokens must not leave prompt_tokens at 0.
|
|
357
|
+
usage: {
|
|
358
|
+
prompt_tokens: usage?.embeddingTokens ?? usage?.promptTokens ?? 0,
|
|
359
|
+
total_tokens: usage?.embeddingTokens ?? usage?.promptTokens ?? 0,
|
|
360
|
+
},
|
|
361
|
+
};
|
|
362
|
+
}
|
package/resources/roles.ts
CHANGED
|
@@ -13,21 +13,80 @@ const USERS_NOT_DBS: readonly string[] = RESERVED_DATABASE_NAMES;
|
|
|
13
13
|
* This is the component for handling role declarations in the Harper system. This will read roles.yaml for role
|
|
14
14
|
* definitions and ensure that they are created in the system database.
|
|
15
15
|
*/
|
|
16
|
-
export function handleApplication(
|
|
17
|
-
scope
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
export function handleApplication(
|
|
17
|
+
scope: import('../components/Scope.ts').Scope,
|
|
18
|
+
ensureRoleFn: (role: any) => Promise<unknown> = ensureRole
|
|
19
|
+
) {
|
|
20
|
+
const rolesByFile = new Map<string, Set<string>>();
|
|
21
|
+
const contentsByFile = new Map<string, Buffer>();
|
|
22
|
+
let pending = Promise.resolve();
|
|
23
|
+
const enqueue = (operation: () => Promise<void>) => {
|
|
24
|
+
const queued = pending.then(operation);
|
|
25
|
+
pending = queued.catch(() => {});
|
|
26
|
+
return queued;
|
|
27
|
+
};
|
|
28
|
+
const entryHandler = scope.handleEntry((entry) =>
|
|
29
|
+
enqueue(async () => {
|
|
30
|
+
if (entry.entryType !== 'file') return;
|
|
31
|
+
const previousRoles = rolesByFile.get(entry.absolutePath) ?? new Set<string>();
|
|
32
|
+
if (entry.eventType === 'unlink') {
|
|
33
|
+
rolesByFile.delete(entry.absolutePath);
|
|
34
|
+
contentsByFile.delete(entry.absolutePath);
|
|
35
|
+
warnForStaleRoles(scope, previousRoles, rolesByFile);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
contentsByFile.set(entry.absolutePath, entry.contents);
|
|
39
|
+
const currentRoles = await handleFile(entry.contents, ensureRoleFn);
|
|
40
|
+
rolesByFile.set(entry.absolutePath, currentRoles);
|
|
41
|
+
warnForStaleRoles(
|
|
42
|
+
scope,
|
|
43
|
+
new Set([...previousRoles].filter((roleName) => !currentRoles.has(roleName))),
|
|
44
|
+
rolesByFile
|
|
45
|
+
);
|
|
46
|
+
})
|
|
47
|
+
);
|
|
48
|
+
if (!entryHandler) return;
|
|
49
|
+
scope.on('deploy:end', () => {
|
|
50
|
+
void entryHandler.ready
|
|
51
|
+
.then(() =>
|
|
52
|
+
enqueue(async () => {
|
|
53
|
+
for (const contents of contentsByFile.values()) {
|
|
54
|
+
try {
|
|
55
|
+
await handleFile(contents, ensureRoleFn);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
scope.logger.error?.('Could not reconcile a roles file after deploy:', error);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
)
|
|
62
|
+
.catch((error) => scope.logger.error?.('Could not reconcile roles after deploy:', error));
|
|
20
63
|
});
|
|
21
64
|
}
|
|
22
65
|
|
|
66
|
+
function warnForStaleRoles(
|
|
67
|
+
scope: import('../components/Scope.ts').Scope,
|
|
68
|
+
removedRoles: Set<string>,
|
|
69
|
+
rolesByFile: Map<string, Set<string>>
|
|
70
|
+
): void {
|
|
71
|
+
const stillDeclared = new Set<string>();
|
|
72
|
+
for (const roleNames of rolesByFile.values()) for (const roleName of roleNames) stillDeclared.add(roleName);
|
|
73
|
+
const staleRoles = [...removedRoles].filter((roleName) => !stillDeclared.has(roleName));
|
|
74
|
+
if (staleRoles.length === 0) return;
|
|
75
|
+
scope.logger.warn(
|
|
76
|
+
`Role definitions removed for ${staleRoles.join(', ')}; existing roles and grants remain active until explicitly altered or deleted`
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
23
80
|
/**
|
|
24
81
|
* This function will handle the roles.yaml file content that has been read, and ensure that the roles are translated to
|
|
25
82
|
* the right shape and created in the system database.
|
|
26
83
|
* @param rolesContent
|
|
27
84
|
*/
|
|
28
|
-
async function handleFile(rolesContent) {
|
|
85
|
+
async function handleFile(rolesContent, ensureRoleFn: (role: any) => Promise<unknown>) {
|
|
29
86
|
let rolesToDefine = parseDocument(rolesContent.toString(), { simpleKeys: true } as any).toJSON();
|
|
87
|
+
const roleNames = new Set<string>();
|
|
30
88
|
for (let roleName in rolesToDefine) {
|
|
89
|
+
roleNames.add(roleName);
|
|
31
90
|
let role = rolesToDefine[roleName];
|
|
32
91
|
if (!role.permission) {
|
|
33
92
|
// we allow the permission object to be collapsed into the root object for convenience
|
|
@@ -78,8 +137,9 @@ async function handleFile(rolesContent) {
|
|
|
78
137
|
}
|
|
79
138
|
}
|
|
80
139
|
role.role = role.id = roleName;
|
|
81
|
-
await
|
|
140
|
+
await ensureRoleFn(role);
|
|
82
141
|
}
|
|
142
|
+
return roleNames;
|
|
83
143
|
}
|
|
84
144
|
async function ensureRole(role) {
|
|
85
145
|
const roleTable = getDatabases().system.hdb_role;
|
|
@@ -87,10 +147,10 @@ async function ensureRole(role) {
|
|
|
87
147
|
for await (let existingRole of roleTable.search([{ attribute: 'role', value: role.role }] as any)) {
|
|
88
148
|
// use the existing role id so we can update in place. Legacy roles may have a UUID for the id instead of the role name
|
|
89
149
|
const { __createdtime__, __updatedtime__, ...existingRoleData } = existingRole;
|
|
150
|
+
role.id = existingRole.id;
|
|
90
151
|
if (isEqual(existingRoleData, role)) {
|
|
91
152
|
return;
|
|
92
153
|
}
|
|
93
|
-
role.id = existingRole.id;
|
|
94
154
|
return alterRole(role);
|
|
95
155
|
}
|
|
96
156
|
return addRole(role);
|
package/resources/search.ts
CHANGED
|
@@ -147,7 +147,8 @@ export function executeConditions(
|
|
|
147
147
|
table,
|
|
148
148
|
request.allowFullScan,
|
|
149
149
|
filtered,
|
|
150
|
-
context
|
|
150
|
+
context,
|
|
151
|
+
request.limit !== undefined ? (request.offset || 0) + request.limit : undefined
|
|
151
152
|
);
|
|
152
153
|
}
|
|
153
154
|
function mapConditionsToFilters(conditions, intersection, estimatedIncomingCount) {
|
|
@@ -247,7 +248,11 @@ export function searchByIndex(
|
|
|
247
248
|
Table: any,
|
|
248
249
|
allowFullScan?: boolean,
|
|
249
250
|
filtered?: any,
|
|
250
|
-
context?: any
|
|
251
|
+
context?: any,
|
|
252
|
+
// How many rows the query will ultimately consume (offset + limit), when it is bounded. An
|
|
253
|
+
// approximate index returns a fixed-size candidate list, so without this a query asking for more
|
|
254
|
+
// rows than that list holds silently gets a short result set. Only custom indexes read it.
|
|
255
|
+
minResults?: number
|
|
251
256
|
): AsyncIterable<Id | { key: Id; value: any }> {
|
|
252
257
|
let attribute_name = searchCondition[0] ?? searchCondition.attribute;
|
|
253
258
|
let value = searchCondition[1] ?? searchCondition.value;
|
|
@@ -505,7 +510,7 @@ export function searchByIndex(
|
|
|
505
510
|
// exploring until it has enough MATCHING results, rather than post-filtering an under-filled
|
|
506
511
|
// candidate set. Only indexes that opt in (filteredSearch) receive it; others post-filter as before.
|
|
507
512
|
const recordFilter = index.customIndex.filteredSearch ? searchCondition.recordFilter : undefined;
|
|
508
|
-
const loaded = index.customIndex.search(searchCondition, context, recordFilter).map((entry) => {
|
|
513
|
+
const loaded = index.customIndex.search(searchCondition, context, recordFilter, minResults).map((entry) => {
|
|
509
514
|
// if the custom index returns an entry with metadata, merge it with the loaded entry
|
|
510
515
|
if (typeof entry === 'object' && entry) {
|
|
511
516
|
const { key, ...otherProps } = entry;
|