agentic-server 0.21.2 → 0.22.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/esm/server.js ADDED
@@ -0,0 +1,31 @@
1
+ import express from 'express';
2
+ import { createRouter } from './router';
3
+ /** Identity headers trusted only when isPublic === false (private network). */
4
+ const IDENTITY_HEADERS = [
5
+ 'x-database-id',
6
+ 'x-entity-id',
7
+ 'x-actor-id'
8
+ ];
9
+ /**
10
+ * Create a standalone Express app for the agentic server.
11
+ * Used by both standalone mode and integration tests.
12
+ */
13
+ export const createAgenticServer = (options) => {
14
+ const app = express();
15
+ app.use(express.json());
16
+ // When isPublic === true, strip identity headers from all incoming requests.
17
+ // Public deployments cannot trust client-supplied tenant context.
18
+ // When isPublic === false (default), the server is on a private network
19
+ // and trusts identity headers directly — same pattern as the GraphQL server
20
+ // with API_IS_PUBLIC=false.
21
+ if (options.isPublic) {
22
+ app.use((req, _res, next) => {
23
+ for (const header of IDENTITY_HEADERS) {
24
+ req.headers[header] = undefined;
25
+ }
26
+ next();
27
+ });
28
+ }
29
+ app.use(createRouter(options));
30
+ return app;
31
+ };
@@ -0,0 +1,111 @@
1
+ export function transformChatRequest(provider, body) {
2
+ let model = body.model || provider.defaultModel;
3
+ if (typeof model === 'string' && model.includes('/')) {
4
+ const prefix = model.split('/')[0];
5
+ if (prefix === provider.type) {
6
+ model = model.split('/').slice(1).join('/');
7
+ }
8
+ }
9
+ if (provider.type === 'ollama') {
10
+ return {
11
+ model: model || 'llama3',
12
+ messages: body.messages,
13
+ stream: false,
14
+ ...(body.temperature !== undefined && {
15
+ options: { temperature: body.temperature }
16
+ })
17
+ };
18
+ }
19
+ if (provider.type === 'anthropic') {
20
+ const messages = body.messages;
21
+ const systemMsg = messages?.find((m) => m.role === 'system');
22
+ const nonSystem = messages?.filter((m) => m.role !== 'system') || [];
23
+ return {
24
+ model: model || 'claude-sonnet-4-20250514',
25
+ messages: nonSystem,
26
+ max_tokens: body.max_tokens || 4096,
27
+ ...(systemMsg && { system: systemMsg.content }),
28
+ ...(body.temperature !== undefined && { temperature: body.temperature })
29
+ };
30
+ }
31
+ // OpenAI-compatible (default)
32
+ return { ...body, model: model || 'gpt-4o' };
33
+ }
34
+ export function transformEmbedRequest(provider, body) {
35
+ const model = body.model || provider.defaultModel;
36
+ if (provider.type === 'ollama') {
37
+ return {
38
+ model: model || 'nomic-embed-text',
39
+ input: body.input
40
+ };
41
+ }
42
+ return { ...body, model: model || 'text-embedding-3-small' };
43
+ }
44
+ export function transformChatResponse(data, provider) {
45
+ if (provider.type === 'ollama') {
46
+ const msg = data.message;
47
+ const promptTokens = data.prompt_eval_count || 0;
48
+ const completionTokens = data.eval_count || 0;
49
+ const usage = {
50
+ prompt_tokens: promptTokens,
51
+ completion_tokens: completionTokens,
52
+ total_tokens: promptTokens + completionTokens
53
+ };
54
+ return {
55
+ body: {
56
+ id: `chatcmpl-${Date.now()}`,
57
+ object: 'chat.completion',
58
+ choices: [{
59
+ message: msg || { role: 'assistant', content: '' },
60
+ finish_reason: 'stop',
61
+ index: 0
62
+ }],
63
+ usage
64
+ },
65
+ usage
66
+ };
67
+ }
68
+ if (provider.type === 'anthropic') {
69
+ const content = data.content;
70
+ const text = content?.find((c) => c.type === 'text')?.text || '';
71
+ const inputUsage = data.usage || {};
72
+ const usage = {
73
+ prompt_tokens: inputUsage.input_tokens || 0,
74
+ completion_tokens: inputUsage.output_tokens || 0,
75
+ total_tokens: (inputUsage.input_tokens || 0) + (inputUsage.output_tokens || 0)
76
+ };
77
+ return {
78
+ body: {
79
+ id: data.id || `chatcmpl-${Date.now()}`,
80
+ object: 'chat.completion',
81
+ choices: [{
82
+ message: { role: 'assistant', content: text },
83
+ finish_reason: 'stop',
84
+ index: 0
85
+ }],
86
+ usage
87
+ },
88
+ usage
89
+ };
90
+ }
91
+ // OpenAI — pass through
92
+ const usage = (data.usage || { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 });
93
+ return { body: data, usage };
94
+ }
95
+ export function transformEmbedResponse(data, provider) {
96
+ if (provider.type === 'ollama') {
97
+ const raw = (data.embeddings || data.embedding || []);
98
+ const embeddings = Array.isArray(raw[0]) ? raw : [raw];
99
+ const usage = { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 };
100
+ return {
101
+ body: {
102
+ object: 'list',
103
+ data: embeddings.map((emb, i) => ({ object: 'embedding', embedding: emb, index: i })),
104
+ usage
105
+ },
106
+ usage
107
+ };
108
+ }
109
+ const usage = (data.usage || { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 });
110
+ return { body: data, usage };
111
+ }
package/esm/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/index.d.ts CHANGED
@@ -1,28 +1,28 @@
1
1
  /**
2
- * agentic-server — Standalone Express LLM service
2
+ * agentic-server — Standalone OpenAI-compatible LLM gateway
3
3
  *
4
- * Express-only equivalent of graphile-llm: agent threads, chat streaming,
5
- * billing metering, and inference logging. Uses @constructive-io/express-context
6
- * for tenant-scoped database access.
7
- *
8
- * LLM provider config is resolved per-database via `ctx.useLlm()` (from the
9
- * llm_module table), falling back to env vars from @constructive-io/llm-env when the module
10
- * is not provisioned. Discovery and billing are handled by the shared loaders
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 express from 'express';
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 = express();
20
- * app.use(createContextMiddleware());
21
- * app.use(createAgenticRouter());
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 { createAgenticRouter } from './router';
26
- export type { BillingClient, InferenceLogEntry, LlmConfig, } from '@constructive-io/express-context';
27
- export type { LlmEnvOptions, LlmProviderConfig, ResolvedLlmEnvOptions } from '@constructive-io/llm-env';
28
- export { getEnvVars as getLlmEnvVars, getEnvOptions as getLlmEnvOptions, llmDefaults } from '@constructive-io/llm-env';
24
+ export { buildProviderHeaders, resolveProvider, resolveUpstreamUrl } from './providers';
25
+ export { createRouter } from './router';
26
+ export type { AgenticServerStartOptions } from './server';
27
+ export { createAgenticServer } from './server';
28
+ export type { AgenticServerOptions, InferenceEntry, InferenceSink, ProviderConfig, ResolvedProvider, UsageResult } from './types';
package/index.js CHANGED
@@ -1,33 +1,34 @@
1
1
  "use strict";
2
2
  /**
3
- * agentic-server — Standalone Express LLM service
3
+ * agentic-server — Standalone OpenAI-compatible LLM gateway
4
4
  *
5
- * Express-only equivalent of graphile-llm: agent threads, chat streaming,
6
- * billing metering, and inference logging. Uses @constructive-io/express-context
7
- * for tenant-scoped database access.
8
- *
9
- * LLM provider config is resolved per-database via `ctx.useLlm()` (from the
10
- * llm_module table), falling back to env vars from @constructive-io/llm-env when the module
11
- * is not provisioned. Discovery and billing are handled by the shared loaders
12
- * in express-context — no custom caching here.
5
+ * A stateless multi-provider proxy that:
6
+ * 1. Accepts OpenAI-compatible requests (`/v1/chat/completions`, `/v1/embeddings`)
7
+ * 2. Enforces tenant isolation via X-Database-Id / X-Entity-Id / X-Actor-Id headers
8
+ * 3. Routes to configured LLM provider(s) (OpenAI, Anthropic, Ollama)
9
+ * 4. Records inference usage through an injected {@link InferenceSink}
10
+ * (fire-and-forget) — the gateway is backend-agnostic and never imports a
11
+ * concrete metering implementation.
13
12
  *
14
13
  * @example
15
14
  * ```typescript
16
- * import express from 'express';
17
- * import { createContextMiddleware } from '@constructive-io/express-context';
18
- * import { createAgenticRouter } from 'agentic-server';
15
+ * import { createAgenticServer } from 'agentic-server';
19
16
  *
20
- * const app = express();
21
- * app.use(createContextMiddleware());
22
- * app.use(createAgenticRouter());
17
+ * const app = createAgenticServer({
18
+ * providerType: 'ollama',
19
+ * providerBaseUrl: 'http://localhost:11434',
20
+ * inferenceSink: { logInference: (entry) => myBackend.record(entry) },
21
+ * });
23
22
  * app.listen(3001);
24
23
  * ```
25
24
  */
26
25
  Object.defineProperty(exports, "__esModule", { value: true });
27
- exports.llmDefaults = exports.getLlmEnvOptions = exports.getLlmEnvVars = exports.createAgenticRouter = void 0;
26
+ exports.createAgenticServer = exports.createRouter = exports.resolveUpstreamUrl = exports.resolveProvider = exports.buildProviderHeaders = void 0;
27
+ var providers_1 = require("./providers");
28
+ Object.defineProperty(exports, "buildProviderHeaders", { enumerable: true, get: function () { return providers_1.buildProviderHeaders; } });
29
+ Object.defineProperty(exports, "resolveProvider", { enumerable: true, get: function () { return providers_1.resolveProvider; } });
30
+ Object.defineProperty(exports, "resolveUpstreamUrl", { enumerable: true, get: function () { return providers_1.resolveUpstreamUrl; } });
28
31
  var router_1 = require("./router");
29
- Object.defineProperty(exports, "createAgenticRouter", { enumerable: true, get: function () { return router_1.createAgenticRouter; } });
30
- var llm_env_1 = require("@constructive-io/llm-env");
31
- Object.defineProperty(exports, "getLlmEnvVars", { enumerable: true, get: function () { return llm_env_1.getEnvVars; } });
32
- Object.defineProperty(exports, "getLlmEnvOptions", { enumerable: true, get: function () { return llm_env_1.getEnvOptions; } });
33
- Object.defineProperty(exports, "llmDefaults", { enumerable: true, get: function () { return llm_env_1.llmDefaults; } });
32
+ Object.defineProperty(exports, "createRouter", { enumerable: true, get: function () { return router_1.createRouter; } });
33
+ var server_1 = require("./server");
34
+ Object.defineProperty(exports, "createAgenticServer", { enumerable: true, get: function () { return server_1.createAgenticServer; } });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "agentic-server",
3
- "version": "0.21.2",
4
- "description": "Standalone Express LLM service — agent threads, chat streaming, billing metering, and inference logging via express-context",
3
+ "version": "0.22.1",
4
+ "description": "Standalone OpenAI-compatible LLM gateway — multi-provider proxy with pluggable inference metering",
5
5
  "main": "index.js",
6
6
  "module": "esm/index.js",
7
7
  "types": "index.d.ts",
@@ -29,34 +29,27 @@
29
29
  "test:watch": "jest --watch"
30
30
  },
31
31
  "dependencies": {
32
- "@agentic-kit/ollama": "2.12.2",
33
- "@constructive-io/express-context": "^0.23.2",
34
- "@constructive-io/llm-env": "^0.12.2",
35
- "@pgpmjs/logger": "^2.23.2",
36
- "pg": "^8.21.0",
37
- "pg-cache": "^3.24.2"
32
+ "@pgpmjs/logger": "^2.23.3"
38
33
  },
39
34
  "devDependencies": {
40
35
  "@types/express": "^5.0.6",
41
36
  "@types/node": "^22.19.11",
42
- "@types/pg": "^8.20.0",
43
- "@types/supertest": "^7.2.0",
44
37
  "express": "^5.2.1",
45
- "makage": "^0.3.0",
46
- "supertest": "^7.2.2"
38
+ "makage": "^0.3.0"
47
39
  },
48
40
  "peerDependencies": {
49
41
  "express": "^5.0.0"
50
42
  },
51
43
  "keywords": [
52
- "agentic-kit",
53
44
  "express",
54
45
  "llm",
46
+ "gateway",
47
+ "openai",
55
48
  "ollama",
56
- "streaming",
57
- "billing",
49
+ "anthropic",
50
+ "proxy",
58
51
  "metering",
59
52
  "constructive"
60
53
  ],
61
- "gitHead": "90f447482b056a4949134565db2665f21d11aca3"
54
+ "gitHead": "c20b6be96b39bf71c0e2df0f4b51e05deacd006b"
62
55
  }
package/providers.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ import type { AgenticServerOptions, ResolvedProvider } from './types';
2
+ /**
3
+ * Resolve which provider to use for a given request.
4
+ * Priority: model-based routing → explicit provider header → default.
5
+ */
6
+ export declare function resolveProvider(options: AgenticServerOptions, requestModel?: string, requestProvider?: string): ResolvedProvider;
7
+ export declare function resolveUpstreamUrl(provider: ResolvedProvider, path: string): string;
8
+ export declare function buildProviderHeaders(provider: ResolvedProvider): Record<string, string>;
package/providers.js ADDED
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveProvider = resolveProvider;
4
+ exports.resolveUpstreamUrl = resolveUpstreamUrl;
5
+ exports.buildProviderHeaders = buildProviderHeaders;
6
+ /**
7
+ * Resolve which provider to use for a given request.
8
+ * Priority: model-based routing → explicit provider header → default.
9
+ */
10
+ function resolveProvider(options, requestModel, requestProvider) {
11
+ const providers = options.providers || [];
12
+ if (providers.length > 0) {
13
+ if (requestProvider) {
14
+ const match = providers.find((p) => p.type === requestProvider);
15
+ if (match)
16
+ return match;
17
+ }
18
+ // Route by model prefix: "anthropic/claude-3" → anthropic provider
19
+ if (requestModel && requestModel.includes('/')) {
20
+ const prefix = requestModel.split('/')[0];
21
+ const match = providers.find((p) => p.type === prefix);
22
+ if (match) {
23
+ return { ...match, defaultModel: requestModel.split('/').slice(1).join('/') };
24
+ }
25
+ }
26
+ // Route by known model patterns
27
+ if (requestModel) {
28
+ if (requestModel.startsWith('claude')) {
29
+ const match = providers.find((p) => p.type === 'anthropic');
30
+ if (match)
31
+ return match;
32
+ }
33
+ if (requestModel.startsWith('gpt') || requestModel.startsWith('o1') || requestModel.startsWith('o3')) {
34
+ const match = providers.find((p) => p.type === 'openai');
35
+ if (match)
36
+ return match;
37
+ }
38
+ if (requestModel.startsWith('llama') ||
39
+ requestModel.startsWith('mistral') ||
40
+ requestModel.startsWith('gemma') ||
41
+ requestModel.startsWith('nomic')) {
42
+ const match = providers.find((p) => p.type === 'ollama');
43
+ if (match)
44
+ return match;
45
+ }
46
+ }
47
+ return providers[0];
48
+ }
49
+ // Legacy single-provider mode (default: ollama for local dev)
50
+ return {
51
+ type: options.providerType || 'ollama',
52
+ baseUrl: options.providerBaseUrl || 'http://localhost:11434',
53
+ apiKey: options.providerApiKey,
54
+ defaultModel: options.defaultModel
55
+ };
56
+ }
57
+ function resolveUpstreamUrl(provider, path) {
58
+ if (provider.type === 'ollama') {
59
+ if (path === '/v1/chat/completions')
60
+ return `${provider.baseUrl}/api/chat`;
61
+ if (path === '/v1/embeddings')
62
+ return `${provider.baseUrl}/api/embed`;
63
+ }
64
+ if (provider.type === 'anthropic') {
65
+ if (path === '/v1/chat/completions')
66
+ return `${provider.baseUrl}/v1/messages`;
67
+ }
68
+ return `${provider.baseUrl}${path}`;
69
+ }
70
+ function buildProviderHeaders(provider) {
71
+ const headers = { 'Content-Type': 'application/json' };
72
+ if (provider.apiKey) {
73
+ if (provider.type === 'anthropic') {
74
+ headers['x-api-key'] = provider.apiKey;
75
+ headers['anthropic-version'] = '2023-06-01';
76
+ }
77
+ else {
78
+ headers['Authorization'] = `Bearer ${provider.apiKey}`;
79
+ }
80
+ }
81
+ return headers;
82
+ }
package/router.d.ts CHANGED
@@ -1,21 +1,13 @@
1
1
  /**
2
- * router — Express router for the agentic-server
3
- *
4
- * Provides REST endpoints for AI agent conversations:
5
- *
6
- * POST /v1/threads → create thread
7
- * POST /v1/threads/:thread_id/messages → send message + stream response
8
- * POST /v1/orgs/:entity_id/threads → create thread (entity-scoped)
9
- * POST /v1/orgs/:entity_id/threads/:thread_id/messages → send message (entity-scoped)
10
- * POST /v1/embed → generate embedding
11
- *
12
- * All routes require `req.constructive` (from @constructive-io/express-context).
13
- * Billing (check_quota + record_usage) and inference logging are automatic
14
- * when the billing/inference_log modules are provisioned.
15
- *
16
- * LLM provider config is resolved per-database via `ctx.useLlm()` from the
17
- * llm_module table, falling back to env vars (EMBEDDER_*, CHAT_*) when the
18
- * module is not provisioned.
2
+ * Express router — OpenAI-compatible LLM proxy with multi-provider routing.
3
+ *
4
+ * Supports:
5
+ * - Multiple providers (OpenAI, Anthropic, Ollama) configured at startup
6
+ * - Model-based routing: "anthropic/claude-3.5-sonnet" → anthropic provider
7
+ * - Header-based routing: X-LLM-Provider: ollama → ollama provider
8
+ * - Fire-and-forget inference metering via an injected InferenceSink
9
+ * - /v1/usage reporting endpoint for external usage submission
19
10
  */
20
11
  import { Router } from 'express';
21
- export declare function createAgenticRouter(): Router;
12
+ import type { AgenticServerOptions } from './types';
13
+ export declare const createRouter: (options: AgenticServerOptions) => Router;