agentic-server 0.21.2 → 0.22.0
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/README.md +41 -48
- package/esm/index.js +17 -17
- package/esm/providers.js +77 -0
- package/esm/router.js +236 -459
- package/esm/server.js +31 -0
- package/esm/transforms.js +111 -0
- package/esm/types.js +1 -0
- package/index.d.ts +19 -19
- package/index.js +22 -21
- package/package.json +9 -16
- package/providers.d.ts +8 -0
- package/providers.js +82 -0
- package/router.d.ts +10 -18
- package/router.js +238 -493
- package/server.d.ts +10 -0
- package/server.js +38 -0
- package/transforms.d.ts +11 -0
- package/transforms.js +117 -0
- package/types.d.ts +64 -0
- package/types.js +2 -0
package/README.md
CHANGED
|
@@ -9,29 +9,27 @@
|
|
|
9
9
|
<img height="20" src="https://github.com/constructive-io/constructive/actions/workflows/run-tests.yaml/badge.svg" />
|
|
10
10
|
</a>
|
|
11
11
|
<a href="https://github.com/constructive-io/constructive/blob/main/LICENSE"><img height="20" src="https://img.shields.io/badge/license-MIT-blue.svg"/></a>
|
|
12
|
-
<a href="https://www.npmjs.com/package/agentic-server"><img height="20" src="https://img.shields.io/github/package-json/v/constructive-io/constructive?filename=
|
|
12
|
+
<a href="https://www.npmjs.com/package/agentic-server"><img height="20" src="https://img.shields.io/github/package-json/v/constructive-io/constructive?filename=agentic%2Fagentic-server%2Fpackage.json"/></a>
|
|
13
13
|
</p>
|
|
14
14
|
|
|
15
|
-
Standalone
|
|
15
|
+
Standalone, OpenAI-compatible LLM gateway — a stateless multi-provider proxy with pluggable inference metering.
|
|
16
16
|
|
|
17
17
|
## Overview
|
|
18
18
|
|
|
19
|
-
`agentic-server` is
|
|
19
|
+
`agentic-server` is a small Express app that accepts OpenAI-compatible requests, routes them to a configured LLM provider (OpenAI, Anthropic, or Ollama), normalizes the response back to the OpenAI shape, and records usage through an injected sink. It is **backend-agnostic**: the gateway never imports a concrete telemetry/billing implementation — callers inject an `InferenceSink`, so the same gateway can meter to `compute_log`, a billing service, or nothing at all.
|
|
20
20
|
|
|
21
21
|
## Usage
|
|
22
22
|
|
|
23
23
|
```typescript
|
|
24
|
-
import
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
//
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
// Mount the agentic router
|
|
34
|
-
app.use(createAgenticRouter());
|
|
24
|
+
import { createAgenticServer } from 'agentic-server';
|
|
25
|
+
|
|
26
|
+
const app = createAgenticServer({
|
|
27
|
+
providerType: 'ollama',
|
|
28
|
+
providerBaseUrl: 'http://localhost:11434',
|
|
29
|
+
defaultModel: 'llama3',
|
|
30
|
+
// Optional: record usage. Fire-and-forget; implementations must never throw.
|
|
31
|
+
inferenceSink: { logInference: (entry) => myBackend.record(entry) },
|
|
32
|
+
});
|
|
35
33
|
|
|
36
34
|
app.listen(3001, () => {
|
|
37
35
|
console.log('agentic-server running on :3001');
|
|
@@ -42,46 +40,41 @@ app.listen(3001, () => {
|
|
|
42
40
|
|
|
43
41
|
| Method | Path | Description |
|
|
44
42
|
|--------|------|-------------|
|
|
45
|
-
| POST | `/v1/
|
|
46
|
-
| POST | `/v1/
|
|
47
|
-
| POST | `/v1/
|
|
48
|
-
|
|
|
49
|
-
|
|
|
50
|
-
|
|
51
|
-
##
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
- **Billing integration**: Automatic quota checks and usage recording (when billing module is provisioned)
|
|
69
|
-
- **Inference logging**: Automatic token usage logging (when inference_log module is provisioned)
|
|
70
|
-
- **RLS enforcement**: All database operations run within tenant-scoped RLS transactions
|
|
43
|
+
| POST | `/v1/chat/completions` | OpenAI-compatible chat completion (multi-provider) |
|
|
44
|
+
| POST | `/v1/embeddings` | OpenAI-compatible embeddings (multi-provider) |
|
|
45
|
+
| POST | `/v1/usage` | Submit an external usage report to the metering sink |
|
|
46
|
+
| GET | `/v1/providers` | List configured providers |
|
|
47
|
+
| GET | `/healthz` | Health check |
|
|
48
|
+
|
|
49
|
+
## Identity & tenancy
|
|
50
|
+
|
|
51
|
+
Requests carry tenant identity via headers: `X-Database-Id` (**required** — requests without it are rejected `400`), `X-Entity-Id`, and `X-Actor-Id`. Routing to a specific provider can be forced with `X-LLM-Provider`.
|
|
52
|
+
|
|
53
|
+
`isPublic` controls trust: when `false` (default) the server sits on a private network and trusts these headers directly; when `true` the identity headers are stripped from incoming requests (external clients cannot set tenant context), so such requests fail loud rather than being mis-attributed.
|
|
54
|
+
|
|
55
|
+
## Metering
|
|
56
|
+
|
|
57
|
+
Metering is injected, not built in:
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
interface InferenceSink {
|
|
61
|
+
logInference(entry: InferenceEntry): void; // fire-and-forget; must not throw
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The gateway calls `logInference` after each chat/embedding (and for `/v1/usage` reports). Consumers own the concrete implementation — e.g. `constructive-db` injects a `compute_log`-backed sink built on its `ModuleLoader`.
|
|
71
66
|
|
|
72
67
|
## Architecture
|
|
73
68
|
|
|
74
69
|
```
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
70
|
+
Client → POST /v1/chat/completions → agentic-server
|
|
71
|
+
│
|
|
72
|
+
┌─────────────────┼─────────────────┐
|
|
73
|
+
│ │ │
|
|
74
|
+
resolveProvider LLM provider InferenceSink
|
|
75
|
+
+ transforms (OpenAI/Ollama/…) (injected metering)
|
|
81
76
|
```
|
|
82
77
|
|
|
83
|
-
The cloud function doesn't need to know about databases, billing, or LLM providers. It just POSTs `{ messages, model }` and gets back a streamed response.
|
|
84
|
-
|
|
85
78
|
---
|
|
86
79
|
|
|
87
80
|
## Education and Tutorials
|
package/esm/index.js
CHANGED
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* agentic-server — Standalone
|
|
2
|
+
* agentic-server — Standalone OpenAI-compatible LLM gateway
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* in express-context — no custom caching here.
|
|
4
|
+
* A stateless multi-provider proxy that:
|
|
5
|
+
* 1. Accepts OpenAI-compatible requests (`/v1/chat/completions`, `/v1/embeddings`)
|
|
6
|
+
* 2. Enforces tenant isolation via X-Database-Id / X-Entity-Id / X-Actor-Id headers
|
|
7
|
+
* 3. Routes to configured LLM provider(s) (OpenAI, Anthropic, Ollama)
|
|
8
|
+
* 4. Records inference usage through an injected {@link InferenceSink}
|
|
9
|
+
* (fire-and-forget) — the gateway is backend-agnostic and never imports a
|
|
10
|
+
* concrete metering implementation.
|
|
12
11
|
*
|
|
13
12
|
* @example
|
|
14
13
|
* ```typescript
|
|
15
|
-
* import
|
|
16
|
-
* import { createContextMiddleware } from '@constructive-io/express-context';
|
|
17
|
-
* import { createAgenticRouter } from 'agentic-server';
|
|
14
|
+
* import { createAgenticServer } from 'agentic-server';
|
|
18
15
|
*
|
|
19
|
-
* const app =
|
|
20
|
-
*
|
|
21
|
-
*
|
|
16
|
+
* const app = createAgenticServer({
|
|
17
|
+
* providerType: 'ollama',
|
|
18
|
+
* providerBaseUrl: 'http://localhost:11434',
|
|
19
|
+
* inferenceSink: { logInference: (entry) => myBackend.record(entry) },
|
|
20
|
+
* });
|
|
22
21
|
* app.listen(3001);
|
|
23
22
|
* ```
|
|
24
23
|
*/
|
|
25
|
-
export {
|
|
26
|
-
export {
|
|
24
|
+
export { buildProviderHeaders, resolveProvider, resolveUpstreamUrl } from './providers';
|
|
25
|
+
export { createRouter } from './router';
|
|
26
|
+
export { createAgenticServer } from './server';
|
package/esm/providers.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve which provider to use for a given request.
|
|
3
|
+
* Priority: model-based routing → explicit provider header → default.
|
|
4
|
+
*/
|
|
5
|
+
export function resolveProvider(options, requestModel, requestProvider) {
|
|
6
|
+
const providers = options.providers || [];
|
|
7
|
+
if (providers.length > 0) {
|
|
8
|
+
if (requestProvider) {
|
|
9
|
+
const match = providers.find((p) => p.type === requestProvider);
|
|
10
|
+
if (match)
|
|
11
|
+
return match;
|
|
12
|
+
}
|
|
13
|
+
// Route by model prefix: "anthropic/claude-3" → anthropic provider
|
|
14
|
+
if (requestModel && requestModel.includes('/')) {
|
|
15
|
+
const prefix = requestModel.split('/')[0];
|
|
16
|
+
const match = providers.find((p) => p.type === prefix);
|
|
17
|
+
if (match) {
|
|
18
|
+
return { ...match, defaultModel: requestModel.split('/').slice(1).join('/') };
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
// Route by known model patterns
|
|
22
|
+
if (requestModel) {
|
|
23
|
+
if (requestModel.startsWith('claude')) {
|
|
24
|
+
const match = providers.find((p) => p.type === 'anthropic');
|
|
25
|
+
if (match)
|
|
26
|
+
return match;
|
|
27
|
+
}
|
|
28
|
+
if (requestModel.startsWith('gpt') || requestModel.startsWith('o1') || requestModel.startsWith('o3')) {
|
|
29
|
+
const match = providers.find((p) => p.type === 'openai');
|
|
30
|
+
if (match)
|
|
31
|
+
return match;
|
|
32
|
+
}
|
|
33
|
+
if (requestModel.startsWith('llama') ||
|
|
34
|
+
requestModel.startsWith('mistral') ||
|
|
35
|
+
requestModel.startsWith('gemma') ||
|
|
36
|
+
requestModel.startsWith('nomic')) {
|
|
37
|
+
const match = providers.find((p) => p.type === 'ollama');
|
|
38
|
+
if (match)
|
|
39
|
+
return match;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return providers[0];
|
|
43
|
+
}
|
|
44
|
+
// Legacy single-provider mode (default: ollama for local dev)
|
|
45
|
+
return {
|
|
46
|
+
type: options.providerType || 'ollama',
|
|
47
|
+
baseUrl: options.providerBaseUrl || 'http://localhost:11434',
|
|
48
|
+
apiKey: options.providerApiKey,
|
|
49
|
+
defaultModel: options.defaultModel
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export function resolveUpstreamUrl(provider, path) {
|
|
53
|
+
if (provider.type === 'ollama') {
|
|
54
|
+
if (path === '/v1/chat/completions')
|
|
55
|
+
return `${provider.baseUrl}/api/chat`;
|
|
56
|
+
if (path === '/v1/embeddings')
|
|
57
|
+
return `${provider.baseUrl}/api/embed`;
|
|
58
|
+
}
|
|
59
|
+
if (provider.type === 'anthropic') {
|
|
60
|
+
if (path === '/v1/chat/completions')
|
|
61
|
+
return `${provider.baseUrl}/v1/messages`;
|
|
62
|
+
}
|
|
63
|
+
return `${provider.baseUrl}${path}`;
|
|
64
|
+
}
|
|
65
|
+
export function buildProviderHeaders(provider) {
|
|
66
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
67
|
+
if (provider.apiKey) {
|
|
68
|
+
if (provider.type === 'anthropic') {
|
|
69
|
+
headers['x-api-key'] = provider.apiKey;
|
|
70
|
+
headers['anthropic-version'] = '2023-06-01';
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
headers['Authorization'] = `Bearer ${provider.apiKey}`;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return headers;
|
|
77
|
+
}
|