@harperfast/harper 5.2.0 → 5.2.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/bin/copyDb.ts +21 -4
- 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/mcp/tools/operations.ts +6 -0
- package/components/operations.js +4 -6
- package/config/configUtils.ts +12 -9
- package/config-root.schema.json +10 -0
- package/dataLayer/harperBridge/ResourceBridge.ts +26 -5
- package/dataLayer/hdbInfoController.ts +8 -0
- package/dataLayer/schemaDescribe.ts +2 -1
- package/dist/bin/copyDb.js +13 -2
- 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/mcp/tools/operations.js +6 -0
- package/dist/components/mcp/tools/operations.js.map +1 -1
- package/dist/components/operations.js +4 -6
- package/dist/components/operations.js.map +1 -1
- package/dist/config/configUtils.js +13 -9
- package/dist/config/configUtils.js.map +1 -1
- package/dist/dataLayer/harperBridge/ResourceBridge.js +12 -5
- package/dist/dataLayer/harperBridge/ResourceBridge.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 +55 -0
- package/dist/resources/DatabaseTransaction.js +282 -103
- 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 +6 -5
- package/dist/resources/Table.js +92 -15
- 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/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/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/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/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/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/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/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 +197 -193
- package/package.json +9 -7
- package/resources/DatabaseTransaction.ts +310 -101
- package/resources/ResourceInterface.ts +8 -2
- package/resources/Resources.ts +22 -4
- package/resources/Table.ts +321 -243
- package/resources/analytics/write.ts +22 -20
- package/resources/blob.ts +15 -8
- package/resources/databases.ts +123 -12
- 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/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/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-aApwhRmz.js} +2 -2
- package/studio/web/assets/{Chat-DoVWScmq.js.map → Chat-aApwhRmz.js.map} +1 -1
- package/studio/web/assets/{FloatingChat-UZ2NsUOZ.js → FloatingChat-sC0H91n9.js} +4 -4
- package/studio/web/assets/{FloatingChat-UZ2NsUOZ.js.map → FloatingChat-sC0H91n9.js.map} +1 -1
- package/studio/web/assets/{apiToken-BUI_04o7.js → apiToken-DZ8JhHOI.js} +2 -2
- package/studio/web/assets/{apiToken-BUI_04o7.js.map → apiToken-DZ8JhHOI.js.map} +1 -1
- package/studio/web/assets/{applications-D03NA7wW.js → applications-9SEVLIO5.js} +2 -2
- package/studio/web/assets/{applications-D03NA7wW.js.map → applications-9SEVLIO5.js.map} +1 -1
- package/studio/web/assets/{index-Bh_CNAHr.js → index-Dy3uDGXb.js} +6 -6
- package/studio/web/assets/{index-Bh_CNAHr.js.map → index-Dy3uDGXb.js.map} +1 -1
- package/studio/web/assets/{index.lazy-Dx3MpyDC.js → index.lazy-CN1zq4I4.js} +4 -4
- package/studio/web/assets/{index.lazy-Dx3MpyDC.js.map → index.lazy-CN1zq4I4.js.map} +1 -1
- package/studio/web/assets/{notifications-0edoFTsb.js → notifications-BFAF07xr.js} +2 -2
- package/studio/web/assets/{notifications-0edoFTsb.js.map → notifications-BFAF07xr.js.map} +1 -1
- package/studio/web/assets/{notifications-CwKhipK7.js → notifications-CKlYVvVN.js} +2 -2
- package/studio/web/assets/{notifications-CwKhipK7.js.map → notifications-CKlYVvVN.js.map} +1 -1
- package/studio/web/assets/{profile-DUfEPQtx.js → profile-A1zhEdFG.js} +2 -2
- package/studio/web/assets/{profile-DUfEPQtx.js.map → profile-A1zhEdFG.js.map} +1 -1
- package/studio/web/assets/{setComponentFile-DMPo4UjC.js → setComponentFile-SEtBt_GV.js} +2 -2
- package/studio/web/assets/{setComponentFile-DMPo4UjC.js.map → setComponentFile-SEtBt_GV.js.map} +1 -1
- package/studio/web/assets/{setup-B56Oz1_u.js → setup-BisINqdH.js} +2 -2
- package/studio/web/assets/{setup-B56Oz1_u.js.map → setup-BisINqdH.js.map} +1 -1
- package/studio/web/assets/{status-BAod7p3o.js → status-BuQoCc7l.js} +2 -2
- package/studio/web/assets/{status-BAod7p3o.js.map → status-BuQoCc7l.js.map} +1 -1
- package/studio/web/assets/{swagger-ui-react-lQrBxfwM.js → swagger-ui-react-CAi_s1PC.js} +2 -2
- package/studio/web/assets/{swagger-ui-react-lQrBxfwM.js.map → swagger-ui-react-CAi_s1PC.js.map} +1 -1
- package/studio/web/assets/{tsMode-CrHCRjTK.js → tsMode-DT74tlkM.js} +2 -2
- package/studio/web/assets/{tsMode-CrHCRjTK.js.map → tsMode-DT74tlkM.js.map} +1 -1
- package/studio/web/assets/{useEntityRestURL-DoaBMEvU.js → useEntityRestURL-CGRGc1n7.js} +2 -2
- package/studio/web/assets/{useEntityRestURL-DoaBMEvU.js.map → useEntityRestURL-CGRGc1n7.js.map} +1 -1
- package/studio/web/index.html +1 -1
- package/upgrade/upgradePrompt.ts +22 -3
- package/utility/errors/commonErrors.ts +2 -0
- package/utility/hdbTerms.ts +2 -0
- package/utility/install/installer.ts +37 -0
- package/utility/logging/harper_logger.ts +57 -26
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `POST /v1/chat/completions` — OpenAI-compatible chat endpoint (#631).
|
|
3
|
+
*
|
|
4
|
+
* SSE serving-path note: the OpenAI SDK sends `Accept: application/json` for
|
|
5
|
+
* ALL requests including streaming ones (`client.ts:1160` in the SDK source).
|
|
6
|
+
* Harper's REST layer dispatches `Accept: text/event-stream` as CONNECT, and
|
|
7
|
+
* everything else as the HTTP method. So `stream: true` from an OpenAI SDK
|
|
8
|
+
* client reaches this `post()` handler, NOT `connect()`. We detect the `stream`
|
|
9
|
+
* flag in the body and return `{ body: Readable }` which REST.ts bypasses
|
|
10
|
+
* serialisation on (REST.ts:165-193) — exactly like any SSE resource response,
|
|
11
|
+
* but initiated from `post()` rather than `connect()`.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { Readable } from 'node:stream';
|
|
15
|
+
import { contentTypes } from '../../../server/serverHelpers/contentTypes.ts';
|
|
16
|
+
import { Resource } from '../../Resource.ts';
|
|
17
|
+
import { models } from '../Models.ts';
|
|
18
|
+
import { openaiStream } from '../openaiStream.ts';
|
|
19
|
+
import { toOpenAIError, badRequest, authorizeV1Request } from './errors.ts';
|
|
20
|
+
import {
|
|
21
|
+
translateMessages,
|
|
22
|
+
translateTools,
|
|
23
|
+
toGenerateInput,
|
|
24
|
+
toGenerateOpts,
|
|
25
|
+
toChatCompletion,
|
|
26
|
+
validateChatRequest,
|
|
27
|
+
} from './translation.ts';
|
|
28
|
+
import type { OAIChatRequest } from './translation.ts';
|
|
29
|
+
|
|
30
|
+
type SseHandler = { serializeStream: (iterable: AsyncIterable<unknown>) => Readable };
|
|
31
|
+
const sseHandler = contentTypes.get('text/event-stream') as SseHandler;
|
|
32
|
+
|
|
33
|
+
// @ts-ignore — Resource base class is not typed for static dispatch; pattern mirrors login.ts
|
|
34
|
+
export class V1ChatCompletions extends Resource {
|
|
35
|
+
// Reserve this fixed route: a later app registration at the same path becomes a
|
|
36
|
+
// loud conflict (ErrorResource) instead of silently replacing the gateway and its
|
|
37
|
+
// super_user gate. See Resources.set.
|
|
38
|
+
static reservedPath = true;
|
|
39
|
+
|
|
40
|
+
static async post(_target: unknown, body: unknown, request: unknown) {
|
|
41
|
+
const authError = authorizeV1Request(request as any);
|
|
42
|
+
if (authError) return authError;
|
|
43
|
+
|
|
44
|
+
// REST.ts passes `request.data` directly, which is the (unawaited) streaming
|
|
45
|
+
// JSON deserializer's Promise — awaiting here is a no-op for callers (e.g.
|
|
46
|
+
// unit tests) that already pass a plain object. A malformed JSON body rejects
|
|
47
|
+
// this promise, which is a client error, not a 500.
|
|
48
|
+
try {
|
|
49
|
+
body = await body;
|
|
50
|
+
} catch (err) {
|
|
51
|
+
return badRequest(`Could not parse request body: ${err instanceof Error ? err.message : 'invalid JSON'}`);
|
|
52
|
+
}
|
|
53
|
+
if (!body || typeof body !== 'object' || Array.isArray(body)) {
|
|
54
|
+
return badRequest('Request body must be a JSON object');
|
|
55
|
+
}
|
|
56
|
+
const req = body as OAIChatRequest;
|
|
57
|
+
|
|
58
|
+
// Validate the nested wire shapes before mapping: the mappers assume well-formed
|
|
59
|
+
// input, so an unvalidated `messages:[null]` / `tools:[{}]` would throw a TypeError
|
|
60
|
+
// and surface as an RFC 9457 500 instead of an OpenAI 400.
|
|
61
|
+
const invalid = validateChatRequest(req);
|
|
62
|
+
if (invalid) return badRequest(invalid);
|
|
63
|
+
|
|
64
|
+
const model = typeof req.model === 'string' ? req.model : 'default';
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const messages = translateMessages(req.messages);
|
|
68
|
+
// tool_choice: 'none' means "do not call tools" — the only faithful way to honor
|
|
69
|
+
// that against a returns-tool-calls backend is to not offer the tools at all.
|
|
70
|
+
// 'required'/named selection are rejected in validateChatRequest.
|
|
71
|
+
const tools = req.tool_choice === 'none' || !req.tools?.length ? undefined : translateTools(req.tools);
|
|
72
|
+
const input = toGenerateInput(messages, tools);
|
|
73
|
+
const opts = toGenerateOpts(req);
|
|
74
|
+
if (req.stream) {
|
|
75
|
+
const tokenStream = models.generateStream(input, opts);
|
|
76
|
+
// serializeStream wraps the async iterable in a Node Readable so REST.ts
|
|
77
|
+
// can return it without re-serialising. The `body` presence on the return
|
|
78
|
+
// value skips REST.ts's own serialize() call (REST.ts:165-193).
|
|
79
|
+
// formatError reuses the non-streaming error mapping so a mid-stream backend
|
|
80
|
+
// failure reaches the client as an OpenAI-shaped SSE error frame.
|
|
81
|
+
const readable = sseHandler.serializeStream(
|
|
82
|
+
openaiStream(tokenStream, { model, formatError: (err) => toOpenAIError(err).data.error })
|
|
83
|
+
);
|
|
84
|
+
return {
|
|
85
|
+
status: 200,
|
|
86
|
+
headers: {
|
|
87
|
+
'Content-Type': 'text/event-stream',
|
|
88
|
+
'Cache-Control': 'no-cache',
|
|
89
|
+
'X-Accel-Buffering': 'no',
|
|
90
|
+
},
|
|
91
|
+
body: readable,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const result = await models.generate(input, opts);
|
|
96
|
+
return toChatCompletion(result, model);
|
|
97
|
+
} catch (err) {
|
|
98
|
+
return toOpenAIError(err);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* A client that sends an explicit `Accept: text/event-stream` with its POST is
|
|
104
|
+
* dispatched by REST as CONNECT (REST.ts), not POST. The OpenAI SDK happens to send
|
|
105
|
+
* `Accept: application/json` even when streaming, but other valid SSE clients do not.
|
|
106
|
+
*
|
|
107
|
+
* Without this override the request reached `Resource`'s default `connect`, whose
|
|
108
|
+
* instance path returns `subscribe()` — an empty `IterableEventQueue` — so the client
|
|
109
|
+
* got a 200 SSE response that stayed open forever emitting nothing, rather than an
|
|
110
|
+
* error it could act on.
|
|
111
|
+
*
|
|
112
|
+
* REST passes `null` as the CONNECT body (`resource.connect(target, null, request)`),
|
|
113
|
+
* so the parsed body is taken off the request and handed to the same `post()`
|
|
114
|
+
* implementation — one code path, identical validation and error shaping.
|
|
115
|
+
*
|
|
116
|
+
* `connect` is also reachable from the WebSocket handler with a different signature
|
|
117
|
+
* (`resourceRequest, incomingMessages, request`), where there is no `request.data`;
|
|
118
|
+
* that case is rejected as a client error rather than returning an envelope the WS
|
|
119
|
+
* path would fail to iterate.
|
|
120
|
+
*/
|
|
121
|
+
static async connect(target: unknown, _data: unknown, request: unknown) {
|
|
122
|
+
const data = (request as { data?: unknown })?.data;
|
|
123
|
+
if (data === undefined) {
|
|
124
|
+
return badRequest('This endpoint requires a JSON request body; WebSocket connections are not supported');
|
|
125
|
+
}
|
|
126
|
+
return this.post(target, data, request);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `POST /v1/embeddings` — OpenAI-compatible embedding endpoint (#631).
|
|
3
|
+
*
|
|
4
|
+
* Maps OpenAI's `{ model, input }` request body to `scope.models.embed()` and
|
|
5
|
+
* returns `{ object: 'list', data: [...], model, usage }` per the OpenAI wire spec.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Resource } from '../../Resource.ts';
|
|
9
|
+
import { models } from '../Models.ts';
|
|
10
|
+
import { toOpenAIError, badRequest, authorizeV1Request } from './errors.ts';
|
|
11
|
+
import { toEmbedOpts, toEmbedResponse } from './translation.ts';
|
|
12
|
+
|
|
13
|
+
// Cap batched input, matching OpenAI's own 2048-item limit. The endpoint is
|
|
14
|
+
// super_user-only and off by default, so this is a sanity bound (avoid an
|
|
15
|
+
// unbounded fan-out to the backend), not a security control.
|
|
16
|
+
const MAX_EMBEDDING_INPUTS = 2048;
|
|
17
|
+
|
|
18
|
+
// @ts-ignore — Resource base class is not typed for static dispatch; pattern mirrors login.ts
|
|
19
|
+
export class V1Embeddings extends Resource {
|
|
20
|
+
// Reserve this fixed route: a later app registration at the same path becomes a
|
|
21
|
+
// loud conflict (ErrorResource) instead of silently replacing the gateway and its
|
|
22
|
+
// super_user gate. See Resources.set.
|
|
23
|
+
static reservedPath = true;
|
|
24
|
+
|
|
25
|
+
static async post(_target: unknown, body: Record<string, unknown>, request: unknown) {
|
|
26
|
+
const authError = authorizeV1Request(request as any);
|
|
27
|
+
if (authError) return authError;
|
|
28
|
+
|
|
29
|
+
// REST.ts passes `request.data` directly, which is the (unawaited) streaming
|
|
30
|
+
// JSON deserializer's Promise — awaiting here is a no-op for callers (e.g.
|
|
31
|
+
// unit tests) that already pass a plain object. A malformed JSON body rejects
|
|
32
|
+
// this promise, which is a client error, not a 500 (matches chatCompletions).
|
|
33
|
+
try {
|
|
34
|
+
body = await body;
|
|
35
|
+
} catch (err) {
|
|
36
|
+
return badRequest(`Could not parse request body: ${err instanceof Error ? err.message : 'invalid JSON'}`);
|
|
37
|
+
}
|
|
38
|
+
if (!body || typeof body !== 'object' || Array.isArray(body))
|
|
39
|
+
return badRequest('Request body must be a JSON object');
|
|
40
|
+
const raw = body as Record<string, unknown>;
|
|
41
|
+
|
|
42
|
+
// Mirrors validateChatRequest: a non-string model would silently invoke the
|
|
43
|
+
// configured default rather than being rejected.
|
|
44
|
+
if (raw.model !== undefined && typeof raw.model !== 'string') return badRequest("'model' must be a string");
|
|
45
|
+
|
|
46
|
+
const input = raw.input;
|
|
47
|
+
if (input === undefined || input === null) return badRequest("'input' is required");
|
|
48
|
+
if (typeof input !== 'string' && !Array.isArray(input)) {
|
|
49
|
+
return badRequest("'input' must be a string or array of strings");
|
|
50
|
+
}
|
|
51
|
+
if (Array.isArray(input) && !input.every((v) => typeof v === 'string')) {
|
|
52
|
+
return badRequest("'input' array elements must be strings");
|
|
53
|
+
}
|
|
54
|
+
if (Array.isArray(input) && input.length > MAX_EMBEDDING_INPUTS) {
|
|
55
|
+
return badRequest(`'input' array must not exceed ${MAX_EMBEDDING_INPUTS} items`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const model = typeof raw.model === 'string' ? raw.model : 'default';
|
|
59
|
+
const opts = toEmbedOpts(raw as any);
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
// embedWithUsage, not embed(): the public facade drops the result-level usage
|
|
63
|
+
// backends report, and OpenAI clients read real token counts off the response.
|
|
64
|
+
const { vectors, usage } = await models.embedWithUsage(input as string | string[], opts);
|
|
65
|
+
return toEmbedResponse(vectors, model, usage);
|
|
66
|
+
} catch (err) {
|
|
67
|
+
return toOpenAIError(err);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAI error envelope helpers for the `/v1/*` gateway (#631).
|
|
3
|
+
*
|
|
4
|
+
* Harper's REST layer serialises uncaught errors as RFC 9457 Problem Details.
|
|
5
|
+
* Resources that need the OpenAI `{ error: { message, type, code, param } }`
|
|
6
|
+
* shape must catch errors themselves and call `toOpenAIError()` / `badRequest()`.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { ModelBackendNotFoundError } from '../backendRegistry.ts';
|
|
10
|
+
import { ModelCapabilityError } from '../Models.ts';
|
|
11
|
+
import harperLogger from '../../../utility/logging/harper_logger.ts';
|
|
12
|
+
|
|
13
|
+
type OpenAIErrorType =
|
|
14
|
+
'invalid_request_error' | 'server_error' | 'authentication_error' | 'permission_error' | 'api_error';
|
|
15
|
+
|
|
16
|
+
export interface OpenAIErrorBody {
|
|
17
|
+
message: string;
|
|
18
|
+
type: OpenAIErrorType;
|
|
19
|
+
code: string | null;
|
|
20
|
+
param: string | null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** HTTP response payload from a gateway error; resource methods return this directly. */
|
|
24
|
+
export interface OpenAIErrorResponse {
|
|
25
|
+
status: number;
|
|
26
|
+
headers: { 'Content-Type': 'application/json' };
|
|
27
|
+
data: { error: OpenAIErrorBody };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Map any thrown value to an OpenAI error envelope. Uses `statusCode` when
|
|
32
|
+
* present (Harper's `ClientError` / `ServerError` convention). Falls back to
|
|
33
|
+
* `500 server_error`. `ModelBackendNotFoundError` maps to `404 model_not_found`.
|
|
34
|
+
*/
|
|
35
|
+
export function toOpenAIError(err: unknown): OpenAIErrorResponse {
|
|
36
|
+
let status = 500;
|
|
37
|
+
let type: OpenAIErrorType = 'server_error';
|
|
38
|
+
let code: string | null = null;
|
|
39
|
+
|
|
40
|
+
if (err instanceof ModelBackendNotFoundError) {
|
|
41
|
+
status = 404;
|
|
42
|
+
type = 'invalid_request_error';
|
|
43
|
+
code = 'model_not_found';
|
|
44
|
+
} else if (err instanceof ModelCapabilityError) {
|
|
45
|
+
// Caller-driven mismatch (e.g. `tools` or streaming against a backend that
|
|
46
|
+
// doesn't support it): the request is what's wrong, not the server. It extends
|
|
47
|
+
// ServerError (statusCode 500), so this must precede the statusCode branch —
|
|
48
|
+
// falling through would report a generic sanitized 500 for a client-actionable
|
|
49
|
+
// condition. The message is safe to pass through: it names only the backend and
|
|
50
|
+
// the capability the caller asked for.
|
|
51
|
+
status = 400;
|
|
52
|
+
type = 'invalid_request_error';
|
|
53
|
+
code = 'capability_unsupported';
|
|
54
|
+
} else if (err instanceof Error && typeof (err as any).statusCode === 'number') {
|
|
55
|
+
status = (err as any).statusCode;
|
|
56
|
+
if (status === 401) {
|
|
57
|
+
type = 'authentication_error';
|
|
58
|
+
} else if (status === 403) {
|
|
59
|
+
// OpenAI semantics: 401 = bad/missing credentials, 403 = valid credentials
|
|
60
|
+
// lacking permission (matches authorizeV1Request's own 403 envelope).
|
|
61
|
+
type = 'permission_error';
|
|
62
|
+
} else if (status < 500) {
|
|
63
|
+
type = 'invalid_request_error';
|
|
64
|
+
} else {
|
|
65
|
+
type = 'server_error';
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 5xx messages stay generic: internal error strings (backend stack details, file
|
|
70
|
+
// paths) don't belong in a wire response. The real error goes to the log. 4xx
|
|
71
|
+
// messages are client-actionable and pass through.
|
|
72
|
+
let message: string;
|
|
73
|
+
if (status >= 500) {
|
|
74
|
+
harperLogger.error('v1 gateway error', err);
|
|
75
|
+
message = 'Internal server error';
|
|
76
|
+
} else {
|
|
77
|
+
message = err instanceof Error ? err.message : 'Bad request';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
status,
|
|
82
|
+
headers: { 'Content-Type': 'application/json' },
|
|
83
|
+
data: { error: { message, type, code, param: null } },
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Convenience for early request-body validation failures. */
|
|
88
|
+
export function badRequest(message: string): OpenAIErrorResponse {
|
|
89
|
+
return {
|
|
90
|
+
status: 400,
|
|
91
|
+
headers: { 'Content-Type': 'application/json' },
|
|
92
|
+
data: { error: { message, type: 'invalid_request_error', code: null, param: null } },
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Gate for the `/v1/*` handlers, since overriding the static `get`/`post` methods
|
|
98
|
+
* bypasses Resource's `transactional()` wrapper and its default `allowRead`/`allowCreate`
|
|
99
|
+
* checks (Resource.ts:685-733, 426-435) never run for these endpoints.
|
|
100
|
+
*
|
|
101
|
+
* Mirrors Resource's default gate (super_user-only) rather than introducing a new
|
|
102
|
+
* permission — see PR discussion for whether a dedicated `/v1/*` permission should
|
|
103
|
+
* replace this later.
|
|
104
|
+
*
|
|
105
|
+
* Returns an OpenAI-shape error response when access should be denied, or `null`
|
|
106
|
+
* when the request may proceed.
|
|
107
|
+
*/
|
|
108
|
+
export function authorizeV1Request(request: {
|
|
109
|
+
user?: { role?: { permission?: { super_user?: boolean } } };
|
|
110
|
+
}): OpenAIErrorResponse | null {
|
|
111
|
+
const user = request?.user;
|
|
112
|
+
if (!user) {
|
|
113
|
+
return {
|
|
114
|
+
status: 401,
|
|
115
|
+
headers: { 'Content-Type': 'application/json' },
|
|
116
|
+
data: {
|
|
117
|
+
error: {
|
|
118
|
+
message: 'You must provide valid credentials to access this endpoint.',
|
|
119
|
+
type: 'authentication_error' as const,
|
|
120
|
+
code: null,
|
|
121
|
+
param: null,
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
if (!user.role?.permission?.super_user) {
|
|
127
|
+
return {
|
|
128
|
+
status: 403,
|
|
129
|
+
headers: { 'Content-Type': 'application/json' },
|
|
130
|
+
data: {
|
|
131
|
+
error: {
|
|
132
|
+
message: 'You do not have permission to access this endpoint.',
|
|
133
|
+
type: 'permission_error' as const,
|
|
134
|
+
code: null,
|
|
135
|
+
param: null,
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
@@ -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
|
+
}
|