smartcontext-proxy 0.1.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/PLAN.md +406 -0
- package/PROGRESS.md +60 -0
- package/README.md +99 -0
- package/SPEC.md +915 -0
- package/adapters/openclaw/embedding.d.ts +8 -0
- package/adapters/openclaw/embedding.js +16 -0
- package/adapters/openclaw/embedding.ts +15 -0
- package/adapters/openclaw/index.d.ts +18 -0
- package/adapters/openclaw/index.js +42 -0
- package/adapters/openclaw/index.ts +43 -0
- package/adapters/openclaw/session-importer.d.ts +22 -0
- package/adapters/openclaw/session-importer.js +99 -0
- package/adapters/openclaw/session-importer.ts +105 -0
- package/adapters/openclaw/storage.d.ts +26 -0
- package/adapters/openclaw/storage.js +177 -0
- package/adapters/openclaw/storage.ts +183 -0
- package/dist/adapters/openclaw/embedding.d.ts +8 -0
- package/dist/adapters/openclaw/embedding.js +16 -0
- package/dist/adapters/openclaw/index.d.ts +18 -0
- package/dist/adapters/openclaw/index.js +42 -0
- package/dist/adapters/openclaw/session-importer.d.ts +22 -0
- package/dist/adapters/openclaw/session-importer.js +99 -0
- package/dist/adapters/openclaw/storage.d.ts +26 -0
- package/dist/adapters/openclaw/storage.js +177 -0
- package/dist/config/auto-detect.d.ts +3 -0
- package/dist/config/auto-detect.js +48 -0
- package/dist/config/defaults.d.ts +2 -0
- package/dist/config/defaults.js +28 -0
- package/dist/config/schema.d.ts +30 -0
- package/dist/config/schema.js +3 -0
- package/dist/context/budget.d.ts +25 -0
- package/dist/context/budget.js +85 -0
- package/dist/context/canonical.d.ts +39 -0
- package/dist/context/canonical.js +12 -0
- package/dist/context/chunker.d.ts +9 -0
- package/dist/context/chunker.js +148 -0
- package/dist/context/optimizer.d.ts +31 -0
- package/dist/context/optimizer.js +163 -0
- package/dist/context/retriever.d.ts +29 -0
- package/dist/context/retriever.js +103 -0
- package/dist/daemon/process.d.ts +6 -0
- package/dist/daemon/process.js +76 -0
- package/dist/daemon/service.d.ts +2 -0
- package/dist/daemon/service.js +99 -0
- package/dist/embedding/ollama.d.ts +11 -0
- package/dist/embedding/ollama.js +72 -0
- package/dist/embedding/types.d.ts +6 -0
- package/dist/embedding/types.js +3 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +190 -0
- package/dist/metrics/collector.d.ts +43 -0
- package/dist/metrics/collector.js +72 -0
- package/dist/providers/anthropic.d.ts +15 -0
- package/dist/providers/anthropic.js +109 -0
- package/dist/providers/google.d.ts +13 -0
- package/dist/providers/google.js +40 -0
- package/dist/providers/ollama.d.ts +13 -0
- package/dist/providers/ollama.js +82 -0
- package/dist/providers/openai.d.ts +15 -0
- package/dist/providers/openai.js +115 -0
- package/dist/providers/types.d.ts +18 -0
- package/dist/providers/types.js +3 -0
- package/dist/proxy/router.d.ts +12 -0
- package/dist/proxy/router.js +46 -0
- package/dist/proxy/server.d.ts +25 -0
- package/dist/proxy/server.js +265 -0
- package/dist/proxy/stream.d.ts +8 -0
- package/dist/proxy/stream.js +32 -0
- package/dist/src/config/auto-detect.d.ts +3 -0
- package/dist/src/config/auto-detect.js +48 -0
- package/dist/src/config/defaults.d.ts +2 -0
- package/dist/src/config/defaults.js +28 -0
- package/dist/src/config/schema.d.ts +30 -0
- package/dist/src/config/schema.js +3 -0
- package/dist/src/context/budget.d.ts +25 -0
- package/dist/src/context/budget.js +85 -0
- package/dist/src/context/canonical.d.ts +39 -0
- package/dist/src/context/canonical.js +12 -0
- package/dist/src/context/chunker.d.ts +9 -0
- package/dist/src/context/chunker.js +148 -0
- package/dist/src/context/optimizer.d.ts +31 -0
- package/dist/src/context/optimizer.js +163 -0
- package/dist/src/context/retriever.d.ts +29 -0
- package/dist/src/context/retriever.js +103 -0
- package/dist/src/daemon/process.d.ts +6 -0
- package/dist/src/daemon/process.js +76 -0
- package/dist/src/daemon/service.d.ts +2 -0
- package/dist/src/daemon/service.js +99 -0
- package/dist/src/embedding/ollama.d.ts +11 -0
- package/dist/src/embedding/ollama.js +72 -0
- package/dist/src/embedding/types.d.ts +6 -0
- package/dist/src/embedding/types.js +3 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +190 -0
- package/dist/src/metrics/collector.d.ts +43 -0
- package/dist/src/metrics/collector.js +72 -0
- package/dist/src/providers/anthropic.d.ts +15 -0
- package/dist/src/providers/anthropic.js +109 -0
- package/dist/src/providers/google.d.ts +13 -0
- package/dist/src/providers/google.js +40 -0
- package/dist/src/providers/ollama.d.ts +13 -0
- package/dist/src/providers/ollama.js +82 -0
- package/dist/src/providers/openai.d.ts +15 -0
- package/dist/src/providers/openai.js +115 -0
- package/dist/src/providers/types.d.ts +18 -0
- package/dist/src/providers/types.js +3 -0
- package/dist/src/proxy/router.d.ts +12 -0
- package/dist/src/proxy/router.js +46 -0
- package/dist/src/proxy/server.d.ts +25 -0
- package/dist/src/proxy/server.js +265 -0
- package/dist/src/proxy/stream.d.ts +8 -0
- package/dist/src/proxy/stream.js +32 -0
- package/dist/src/storage/lancedb.d.ts +21 -0
- package/dist/src/storage/lancedb.js +158 -0
- package/dist/src/storage/types.d.ts +52 -0
- package/dist/src/storage/types.js +3 -0
- package/dist/src/test/context.test.d.ts +1 -0
- package/dist/src/test/context.test.js +141 -0
- package/dist/src/test/dashboard.test.d.ts +1 -0
- package/dist/src/test/dashboard.test.js +85 -0
- package/dist/src/test/proxy.test.d.ts +1 -0
- package/dist/src/test/proxy.test.js +188 -0
- package/dist/src/ui/dashboard.d.ts +2 -0
- package/dist/src/ui/dashboard.js +183 -0
- package/dist/storage/lancedb.d.ts +21 -0
- package/dist/storage/lancedb.js +158 -0
- package/dist/storage/types.d.ts +52 -0
- package/dist/storage/types.js +3 -0
- package/dist/test/context.test.d.ts +1 -0
- package/dist/test/context.test.js +141 -0
- package/dist/test/dashboard.test.d.ts +1 -0
- package/dist/test/dashboard.test.js +85 -0
- package/dist/test/proxy.test.d.ts +1 -0
- package/dist/test/proxy.test.js +188 -0
- package/dist/ui/dashboard.d.ts +2 -0
- package/dist/ui/dashboard.js +183 -0
- package/package.json +38 -0
- package/src/config/auto-detect.ts +51 -0
- package/src/config/defaults.ts +26 -0
- package/src/config/schema.ts +33 -0
- package/src/context/budget.ts +126 -0
- package/src/context/canonical.ts +50 -0
- package/src/context/chunker.ts +165 -0
- package/src/context/optimizer.ts +201 -0
- package/src/context/retriever.ts +123 -0
- package/src/daemon/process.ts +70 -0
- package/src/daemon/service.ts +103 -0
- package/src/embedding/ollama.ts +68 -0
- package/src/embedding/types.ts +6 -0
- package/src/index.ts +176 -0
- package/src/metrics/collector.ts +114 -0
- package/src/providers/anthropic.ts +117 -0
- package/src/providers/google.ts +42 -0
- package/src/providers/ollama.ts +87 -0
- package/src/providers/openai.ts +127 -0
- package/src/providers/types.ts +20 -0
- package/src/proxy/router.ts +48 -0
- package/src/proxy/server.ts +315 -0
- package/src/proxy/stream.ts +39 -0
- package/src/storage/lancedb.ts +169 -0
- package/src/storage/types.ts +47 -0
- package/src/test/context.test.ts +165 -0
- package/src/test/dashboard.test.ts +94 -0
- package/src/test/proxy.test.ts +218 -0
- package/src/ui/dashboard.ts +184 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MetricsCollector = void 0;
|
|
4
|
+
class MetricsCollector {
|
|
5
|
+
metrics = [];
|
|
6
|
+
startTime = Date.now();
|
|
7
|
+
record(metric) {
|
|
8
|
+
this.metrics.push(metric);
|
|
9
|
+
}
|
|
10
|
+
getRecent(limit = 50) {
|
|
11
|
+
return this.metrics.slice(-limit);
|
|
12
|
+
}
|
|
13
|
+
getStats() {
|
|
14
|
+
const total = this.metrics.length;
|
|
15
|
+
if (total === 0) {
|
|
16
|
+
return {
|
|
17
|
+
totalRequests: 0,
|
|
18
|
+
totalOriginalTokens: 0,
|
|
19
|
+
totalOptimizedTokens: 0,
|
|
20
|
+
totalSavingsPercent: 0,
|
|
21
|
+
avgLatencyOverheadMs: 0,
|
|
22
|
+
avgChunksRetrieved: 0,
|
|
23
|
+
byProvider: {},
|
|
24
|
+
byModel: {},
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
let totalOriginal = 0;
|
|
28
|
+
let totalOptimized = 0;
|
|
29
|
+
let totalLatency = 0;
|
|
30
|
+
let totalChunks = 0;
|
|
31
|
+
const byProvider = {};
|
|
32
|
+
const byModel = {};
|
|
33
|
+
for (const m of this.metrics) {
|
|
34
|
+
totalOriginal += m.originalTokens;
|
|
35
|
+
totalOptimized += m.optimizedTokens;
|
|
36
|
+
totalLatency += m.latencyOverheadMs;
|
|
37
|
+
totalChunks += m.chunksRetrieved;
|
|
38
|
+
// By provider
|
|
39
|
+
if (!byProvider[m.provider])
|
|
40
|
+
byProvider[m.provider] = { requests: 0, tokensSaved: 0, savingsPercent: 0 };
|
|
41
|
+
byProvider[m.provider].requests++;
|
|
42
|
+
byProvider[m.provider].tokensSaved += m.originalTokens - m.optimizedTokens;
|
|
43
|
+
// By model
|
|
44
|
+
if (!byModel[m.model])
|
|
45
|
+
byModel[m.model] = { requests: 0, tokensSaved: 0, savingsPercent: 0 };
|
|
46
|
+
byModel[m.model].requests++;
|
|
47
|
+
byModel[m.model].tokensSaved += m.originalTokens - m.optimizedTokens;
|
|
48
|
+
}
|
|
49
|
+
// Calculate percentages
|
|
50
|
+
for (const stats of Object.values(byProvider)) {
|
|
51
|
+
stats.savingsPercent = totalOriginal > 0 ? Math.round((stats.tokensSaved / totalOriginal) * 100) : 0;
|
|
52
|
+
}
|
|
53
|
+
for (const stats of Object.values(byModel)) {
|
|
54
|
+
stats.savingsPercent = totalOriginal > 0 ? Math.round((stats.tokensSaved / totalOriginal) * 100) : 0;
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
totalRequests: total,
|
|
58
|
+
totalOriginalTokens: totalOriginal,
|
|
59
|
+
totalOptimizedTokens: totalOptimized,
|
|
60
|
+
totalSavingsPercent: totalOriginal > 0 ? Math.round((1 - totalOptimized / totalOriginal) * 100) : 0,
|
|
61
|
+
avgLatencyOverheadMs: Math.round(totalLatency / total),
|
|
62
|
+
avgChunksRetrieved: Math.round(totalChunks / total * 10) / 10,
|
|
63
|
+
byProvider,
|
|
64
|
+
byModel,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
getUptime() {
|
|
68
|
+
return Date.now() - this.startTime;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
exports.MetricsCollector = MetricsCollector;
|
|
72
|
+
//# sourceMappingURL=collector.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ProviderAdapter } from './types.js';
|
|
2
|
+
import type { CanonicalRequest } from '../context/canonical.js';
|
|
3
|
+
export declare class AnthropicAdapter implements ProviderAdapter {
|
|
4
|
+
baseUrl: string;
|
|
5
|
+
name: string;
|
|
6
|
+
constructor(baseUrl?: string);
|
|
7
|
+
parseRequest(body: unknown, headers: Record<string, string>): CanonicalRequest;
|
|
8
|
+
serializeRequest(canonical: CanonicalRequest): unknown;
|
|
9
|
+
forwardUrl(originalPath: string): string;
|
|
10
|
+
extractApiKey(headers: Record<string, string>): string;
|
|
11
|
+
contentType(): string;
|
|
12
|
+
authHeaders(apiKey: string): Record<string, string>;
|
|
13
|
+
private parseMessage;
|
|
14
|
+
private serializeMessage;
|
|
15
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AnthropicAdapter = void 0;
|
|
4
|
+
class AnthropicAdapter {
|
|
5
|
+
baseUrl;
|
|
6
|
+
name = 'anthropic';
|
|
7
|
+
constructor(baseUrl = 'https://api.anthropic.com') {
|
|
8
|
+
this.baseUrl = baseUrl;
|
|
9
|
+
}
|
|
10
|
+
parseRequest(body, headers) {
|
|
11
|
+
const b = body;
|
|
12
|
+
const messages = [];
|
|
13
|
+
let systemPrompt;
|
|
14
|
+
// Extract system prompt
|
|
15
|
+
if (typeof b.system === 'string') {
|
|
16
|
+
systemPrompt = b.system;
|
|
17
|
+
}
|
|
18
|
+
else if (Array.isArray(b.system)) {
|
|
19
|
+
systemPrompt = b.system
|
|
20
|
+
.map((s) => s.text || '')
|
|
21
|
+
.join('\n');
|
|
22
|
+
}
|
|
23
|
+
// Convert messages
|
|
24
|
+
if (Array.isArray(b.messages)) {
|
|
25
|
+
for (const msg of b.messages) {
|
|
26
|
+
messages.push(this.parseMessage(msg));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
messages,
|
|
31
|
+
systemPrompt,
|
|
32
|
+
model: b.model || 'unknown',
|
|
33
|
+
stream: !!b.stream,
|
|
34
|
+
maxTokens: b.max_tokens,
|
|
35
|
+
temperature: b.temperature,
|
|
36
|
+
tools: b.tools,
|
|
37
|
+
rawHeaders: headers,
|
|
38
|
+
providerAuth: this.extractApiKey(headers),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
serializeRequest(canonical) {
|
|
42
|
+
const body = {
|
|
43
|
+
model: canonical.model,
|
|
44
|
+
messages: canonical.messages.map((m) => this.serializeMessage(m)),
|
|
45
|
+
stream: canonical.stream,
|
|
46
|
+
};
|
|
47
|
+
if (canonical.systemPrompt) {
|
|
48
|
+
body.system = canonical.systemPrompt;
|
|
49
|
+
}
|
|
50
|
+
if (canonical.maxTokens) {
|
|
51
|
+
body.max_tokens = canonical.maxTokens;
|
|
52
|
+
}
|
|
53
|
+
if (canonical.temperature !== undefined) {
|
|
54
|
+
body.temperature = canonical.temperature;
|
|
55
|
+
}
|
|
56
|
+
if (canonical.tools) {
|
|
57
|
+
body.tools = canonical.tools;
|
|
58
|
+
}
|
|
59
|
+
return body;
|
|
60
|
+
}
|
|
61
|
+
forwardUrl(originalPath) {
|
|
62
|
+
// /v1/anthropic/v1/messages → https://api.anthropic.com/v1/messages
|
|
63
|
+
const stripped = originalPath.replace(/^\/v1\/anthropic/, '');
|
|
64
|
+
return `${this.baseUrl}${stripped}`;
|
|
65
|
+
}
|
|
66
|
+
extractApiKey(headers) {
|
|
67
|
+
return headers['x-api-key'] || '';
|
|
68
|
+
}
|
|
69
|
+
contentType() {
|
|
70
|
+
return 'application/json';
|
|
71
|
+
}
|
|
72
|
+
authHeaders(apiKey) {
|
|
73
|
+
return {
|
|
74
|
+
'x-api-key': apiKey,
|
|
75
|
+
'anthropic-version': '2023-06-01',
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
parseMessage(msg) {
|
|
79
|
+
const role = msg.role;
|
|
80
|
+
let content;
|
|
81
|
+
if (typeof msg.content === 'string') {
|
|
82
|
+
content = msg.content;
|
|
83
|
+
}
|
|
84
|
+
else if (Array.isArray(msg.content)) {
|
|
85
|
+
content = msg.content.map((block) => {
|
|
86
|
+
if (block.type === 'text') {
|
|
87
|
+
return { type: 'text', text: block.text };
|
|
88
|
+
}
|
|
89
|
+
// Pass through tool_use, tool_result, image blocks as-is
|
|
90
|
+
return { ...block, type: block.type };
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
content = '';
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
role: role,
|
|
98
|
+
content,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
serializeMessage(msg) {
|
|
102
|
+
return {
|
|
103
|
+
role: msg.role,
|
|
104
|
+
content: msg.content,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
exports.AnthropicAdapter = AnthropicAdapter;
|
|
109
|
+
//# sourceMappingURL=anthropic.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ProviderAdapter } from './types.js';
|
|
2
|
+
import type { CanonicalRequest } from '../context/canonical.js';
|
|
3
|
+
export declare class GoogleAdapter implements ProviderAdapter {
|
|
4
|
+
baseUrl: string;
|
|
5
|
+
name: string;
|
|
6
|
+
constructor(baseUrl?: string);
|
|
7
|
+
parseRequest(body: unknown, headers: Record<string, string>): CanonicalRequest;
|
|
8
|
+
serializeRequest(canonical: CanonicalRequest): unknown;
|
|
9
|
+
forwardUrl(originalPath: string): string;
|
|
10
|
+
extractApiKey(headers: Record<string, string>): string;
|
|
11
|
+
contentType(): string;
|
|
12
|
+
authHeaders(apiKey: string): Record<string, string>;
|
|
13
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.GoogleAdapter = void 0;
|
|
4
|
+
class GoogleAdapter {
|
|
5
|
+
baseUrl;
|
|
6
|
+
name = 'google';
|
|
7
|
+
constructor(baseUrl = 'https://generativelanguage.googleapis.com') {
|
|
8
|
+
this.baseUrl = baseUrl;
|
|
9
|
+
}
|
|
10
|
+
parseRequest(body, headers) {
|
|
11
|
+
// Stub: pass through as-is for Phase 1
|
|
12
|
+
const b = body;
|
|
13
|
+
return {
|
|
14
|
+
messages: [],
|
|
15
|
+
model: 'unknown',
|
|
16
|
+
stream: !!b.stream,
|
|
17
|
+
rawHeaders: headers,
|
|
18
|
+
providerAuth: this.extractApiKey(headers),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
serializeRequest(canonical) {
|
|
22
|
+
// Stub: not implemented for Phase 1
|
|
23
|
+
return {};
|
|
24
|
+
}
|
|
25
|
+
forwardUrl(originalPath) {
|
|
26
|
+
const stripped = originalPath.replace(/^\/v1\/google/, '');
|
|
27
|
+
return `${this.baseUrl}${stripped}`;
|
|
28
|
+
}
|
|
29
|
+
extractApiKey(headers) {
|
|
30
|
+
return headers['x-goog-api-key'] || '';
|
|
31
|
+
}
|
|
32
|
+
contentType() {
|
|
33
|
+
return 'application/json';
|
|
34
|
+
}
|
|
35
|
+
authHeaders(apiKey) {
|
|
36
|
+
return { 'x-goog-api-key': apiKey };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
exports.GoogleAdapter = GoogleAdapter;
|
|
40
|
+
//# sourceMappingURL=google.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ProviderAdapter } from './types.js';
|
|
2
|
+
import type { CanonicalRequest } from '../context/canonical.js';
|
|
3
|
+
export declare class OllamaAdapter implements ProviderAdapter {
|
|
4
|
+
baseUrl: string;
|
|
5
|
+
name: string;
|
|
6
|
+
constructor(baseUrl?: string);
|
|
7
|
+
parseRequest(body: unknown, headers: Record<string, string>): CanonicalRequest;
|
|
8
|
+
serializeRequest(canonical: CanonicalRequest): unknown;
|
|
9
|
+
forwardUrl(originalPath: string): string;
|
|
10
|
+
extractApiKey(headers: Record<string, string>): string;
|
|
11
|
+
contentType(): string;
|
|
12
|
+
authHeaders(apiKey: string): Record<string, string>;
|
|
13
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.OllamaAdapter = void 0;
|
|
4
|
+
class OllamaAdapter {
|
|
5
|
+
baseUrl;
|
|
6
|
+
name = 'ollama';
|
|
7
|
+
constructor(baseUrl = 'http://localhost:11434') {
|
|
8
|
+
this.baseUrl = baseUrl;
|
|
9
|
+
}
|
|
10
|
+
parseRequest(body, headers) {
|
|
11
|
+
const b = body;
|
|
12
|
+
const messages = [];
|
|
13
|
+
let systemPrompt;
|
|
14
|
+
if (Array.isArray(b.messages)) {
|
|
15
|
+
for (const msg of b.messages) {
|
|
16
|
+
if (msg.role === 'system') {
|
|
17
|
+
systemPrompt = msg.content;
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
messages.push({
|
|
21
|
+
role: msg.role,
|
|
22
|
+
content: msg.content || '',
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
messages,
|
|
28
|
+
systemPrompt,
|
|
29
|
+
model: b.model || 'unknown',
|
|
30
|
+
stream: b.stream !== false, // Ollama streams by default
|
|
31
|
+
maxTokens: b.options ? b.options.num_predict : undefined,
|
|
32
|
+
temperature: b.options ? b.options.temperature : undefined,
|
|
33
|
+
rawHeaders: headers,
|
|
34
|
+
providerAuth: '',
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
serializeRequest(canonical) {
|
|
38
|
+
const messages = [];
|
|
39
|
+
if (canonical.systemPrompt) {
|
|
40
|
+
messages.push({ role: 'system', content: canonical.systemPrompt });
|
|
41
|
+
}
|
|
42
|
+
for (const msg of canonical.messages) {
|
|
43
|
+
messages.push({
|
|
44
|
+
role: msg.role,
|
|
45
|
+
content: typeof msg.content === 'string'
|
|
46
|
+
? msg.content
|
|
47
|
+
: msg.content.filter((b) => b.type === 'text').map((b) => b.text || '').join('\n'),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
const body = {
|
|
51
|
+
model: canonical.model,
|
|
52
|
+
messages,
|
|
53
|
+
stream: canonical.stream,
|
|
54
|
+
};
|
|
55
|
+
const options = {};
|
|
56
|
+
if (canonical.maxTokens)
|
|
57
|
+
options.num_predict = canonical.maxTokens;
|
|
58
|
+
if (canonical.temperature !== undefined)
|
|
59
|
+
options.temperature = canonical.temperature;
|
|
60
|
+
if (Object.keys(options).length > 0)
|
|
61
|
+
body.options = options;
|
|
62
|
+
return body;
|
|
63
|
+
}
|
|
64
|
+
forwardUrl(originalPath) {
|
|
65
|
+
// /v1/ollama/api/chat → http://localhost:11434/api/chat
|
|
66
|
+
const stripped = originalPath.replace(/^\/v1\/ollama/, '');
|
|
67
|
+
return `${this.baseUrl}${stripped}`;
|
|
68
|
+
}
|
|
69
|
+
extractApiKey(headers) {
|
|
70
|
+
return headers['authorization']?.replace(/^Bearer\s+/i, '') || '';
|
|
71
|
+
}
|
|
72
|
+
contentType() {
|
|
73
|
+
return 'application/json';
|
|
74
|
+
}
|
|
75
|
+
authHeaders(apiKey) {
|
|
76
|
+
if (!apiKey)
|
|
77
|
+
return {};
|
|
78
|
+
return { 'Authorization': `Bearer ${apiKey}` };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
exports.OllamaAdapter = OllamaAdapter;
|
|
82
|
+
//# sourceMappingURL=ollama.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ProviderAdapter } from './types.js';
|
|
2
|
+
import type { CanonicalRequest } from '../context/canonical.js';
|
|
3
|
+
export declare class OpenAIAdapter implements ProviderAdapter {
|
|
4
|
+
baseUrl: string;
|
|
5
|
+
name: string;
|
|
6
|
+
constructor(baseUrl?: string);
|
|
7
|
+
parseRequest(body: unknown, headers: Record<string, string>): CanonicalRequest;
|
|
8
|
+
serializeRequest(canonical: CanonicalRequest): unknown;
|
|
9
|
+
forwardUrl(originalPath: string): string;
|
|
10
|
+
extractApiKey(headers: Record<string, string>): string;
|
|
11
|
+
contentType(): string;
|
|
12
|
+
authHeaders(apiKey: string): Record<string, string>;
|
|
13
|
+
private parseMessage;
|
|
14
|
+
private serializeMessage;
|
|
15
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.OpenAIAdapter = void 0;
|
|
4
|
+
class OpenAIAdapter {
|
|
5
|
+
baseUrl;
|
|
6
|
+
name = 'openai';
|
|
7
|
+
constructor(baseUrl = 'https://api.openai.com') {
|
|
8
|
+
this.baseUrl = baseUrl;
|
|
9
|
+
}
|
|
10
|
+
parseRequest(body, headers) {
|
|
11
|
+
const b = body;
|
|
12
|
+
const messages = [];
|
|
13
|
+
let systemPrompt;
|
|
14
|
+
if (Array.isArray(b.messages)) {
|
|
15
|
+
for (const msg of b.messages) {
|
|
16
|
+
// OpenAI puts system prompt as a message with role=system
|
|
17
|
+
if (msg.role === 'system') {
|
|
18
|
+
systemPrompt = typeof msg.content === 'string'
|
|
19
|
+
? msg.content
|
|
20
|
+
: msg.content?.map((c) => c.text || '').join('\n');
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
messages.push(this.parseMessage(msg));
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
messages,
|
|
28
|
+
systemPrompt,
|
|
29
|
+
model: b.model || 'unknown',
|
|
30
|
+
stream: !!b.stream,
|
|
31
|
+
maxTokens: (b.max_tokens ?? b.max_completion_tokens),
|
|
32
|
+
temperature: b.temperature,
|
|
33
|
+
tools: b.tools,
|
|
34
|
+
rawHeaders: headers,
|
|
35
|
+
providerAuth: this.extractApiKey(headers),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
serializeRequest(canonical) {
|
|
39
|
+
const messages = [];
|
|
40
|
+
if (canonical.systemPrompt) {
|
|
41
|
+
messages.push({ role: 'system', content: canonical.systemPrompt });
|
|
42
|
+
}
|
|
43
|
+
for (const msg of canonical.messages) {
|
|
44
|
+
messages.push(this.serializeMessage(msg));
|
|
45
|
+
}
|
|
46
|
+
const body = {
|
|
47
|
+
model: canonical.model,
|
|
48
|
+
messages,
|
|
49
|
+
stream: canonical.stream,
|
|
50
|
+
};
|
|
51
|
+
if (canonical.maxTokens) {
|
|
52
|
+
body.max_tokens = canonical.maxTokens;
|
|
53
|
+
}
|
|
54
|
+
if (canonical.temperature !== undefined) {
|
|
55
|
+
body.temperature = canonical.temperature;
|
|
56
|
+
}
|
|
57
|
+
if (canonical.tools) {
|
|
58
|
+
body.tools = canonical.tools;
|
|
59
|
+
}
|
|
60
|
+
return body;
|
|
61
|
+
}
|
|
62
|
+
forwardUrl(originalPath) {
|
|
63
|
+
// /v1/openai/v1/chat/completions → https://api.openai.com/v1/chat/completions
|
|
64
|
+
const stripped = originalPath.replace(/^\/v1\/openai/, '');
|
|
65
|
+
return `${this.baseUrl}${stripped}`;
|
|
66
|
+
}
|
|
67
|
+
extractApiKey(headers) {
|
|
68
|
+
const auth = headers['authorization'] || '';
|
|
69
|
+
return auth.replace(/^Bearer\s+/i, '');
|
|
70
|
+
}
|
|
71
|
+
contentType() {
|
|
72
|
+
return 'application/json';
|
|
73
|
+
}
|
|
74
|
+
authHeaders(apiKey) {
|
|
75
|
+
return {
|
|
76
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
parseMessage(msg) {
|
|
80
|
+
const role = msg.role;
|
|
81
|
+
let content;
|
|
82
|
+
if (typeof msg.content === 'string') {
|
|
83
|
+
content = msg.content;
|
|
84
|
+
}
|
|
85
|
+
else if (Array.isArray(msg.content)) {
|
|
86
|
+
content = msg.content.map((block) => {
|
|
87
|
+
if (block.type === 'text') {
|
|
88
|
+
return { type: 'text', text: block.text };
|
|
89
|
+
}
|
|
90
|
+
return { ...block, type: block.type };
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
else if (msg.content === null || msg.content === undefined) {
|
|
94
|
+
// Assistant messages with tool_calls may have null content
|
|
95
|
+
content = '';
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
content = '';
|
|
99
|
+
}
|
|
100
|
+
const canonical = { role: role, content };
|
|
101
|
+
// Preserve tool_calls on assistant messages
|
|
102
|
+
if (msg.tool_calls) {
|
|
103
|
+
canonical.metadata = { tools: msg.tool_calls.map((t) => t.function?.name || 'unknown') };
|
|
104
|
+
}
|
|
105
|
+
return canonical;
|
|
106
|
+
}
|
|
107
|
+
serializeMessage(msg) {
|
|
108
|
+
return {
|
|
109
|
+
role: msg.role === 'tool' ? 'tool' : msg.role,
|
|
110
|
+
content: msg.content,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
exports.OpenAIAdapter = OpenAIAdapter;
|
|
115
|
+
//# sourceMappingURL=openai.js.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { CanonicalRequest } from '../context/canonical.js';
|
|
2
|
+
export interface ProviderAdapter {
|
|
3
|
+
name: string;
|
|
4
|
+
/** Base URL for this provider's API */
|
|
5
|
+
baseUrl: string;
|
|
6
|
+
/** Parse raw request body into canonical format */
|
|
7
|
+
parseRequest(body: unknown, headers: Record<string, string>): CanonicalRequest;
|
|
8
|
+
/** Convert canonical request back to provider format for forwarding */
|
|
9
|
+
serializeRequest(canonical: CanonicalRequest): unknown;
|
|
10
|
+
/** Build the forward URL from the original request path */
|
|
11
|
+
forwardUrl(originalPath: string): string;
|
|
12
|
+
/** Extract API key from request headers */
|
|
13
|
+
extractApiKey(headers: Record<string, string>): string;
|
|
14
|
+
/** Get the content-type header for forwarding */
|
|
15
|
+
contentType(): string;
|
|
16
|
+
/** Build auth headers for the forwarded request */
|
|
17
|
+
authHeaders(apiKey: string): Record<string, string>;
|
|
18
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ProviderAdapter } from '../providers/types.js';
|
|
2
|
+
import type { SmartContextConfig } from '../config/schema.js';
|
|
3
|
+
export declare class Router {
|
|
4
|
+
private adapters;
|
|
5
|
+
constructor(config: SmartContextConfig);
|
|
6
|
+
/** Extract provider name from URL path: /v1/{provider}/... */
|
|
7
|
+
resolve(path: string): {
|
|
8
|
+
adapter: ProviderAdapter;
|
|
9
|
+
providerName: string;
|
|
10
|
+
} | null;
|
|
11
|
+
getProviderNames(): string[];
|
|
12
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Router = void 0;
|
|
4
|
+
const anthropic_js_1 = require("../providers/anthropic.js");
|
|
5
|
+
const openai_js_1 = require("../providers/openai.js");
|
|
6
|
+
const ollama_js_1 = require("../providers/ollama.js");
|
|
7
|
+
const google_js_1 = require("../providers/google.js");
|
|
8
|
+
class Router {
|
|
9
|
+
adapters = new Map();
|
|
10
|
+
constructor(config) {
|
|
11
|
+
// Register adapters for detected providers
|
|
12
|
+
const providers = config.providers;
|
|
13
|
+
if (providers.anthropic) {
|
|
14
|
+
this.adapters.set('anthropic', new anthropic_js_1.AnthropicAdapter(providers.anthropic.baseUrl));
|
|
15
|
+
}
|
|
16
|
+
if (providers.openai) {
|
|
17
|
+
this.adapters.set('openai', new openai_js_1.OpenAIAdapter(providers.openai.baseUrl));
|
|
18
|
+
}
|
|
19
|
+
if (providers.ollama) {
|
|
20
|
+
this.adapters.set('ollama', new ollama_js_1.OllamaAdapter(providers.ollama.baseUrl));
|
|
21
|
+
}
|
|
22
|
+
if (providers.google) {
|
|
23
|
+
this.adapters.set('google', new google_js_1.GoogleAdapter(providers.google.baseUrl));
|
|
24
|
+
}
|
|
25
|
+
if (providers.openrouter) {
|
|
26
|
+
// OpenRouter uses OpenAI-compatible API
|
|
27
|
+
this.adapters.set('openrouter', new openai_js_1.OpenAIAdapter(providers.openrouter.baseUrl || 'https://openrouter.ai/api'));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/** Extract provider name from URL path: /v1/{provider}/... */
|
|
31
|
+
resolve(path) {
|
|
32
|
+
const match = path.match(/^\/v1\/([^/]+)/);
|
|
33
|
+
if (!match)
|
|
34
|
+
return null;
|
|
35
|
+
const providerName = match[1];
|
|
36
|
+
const adapter = this.adapters.get(providerName);
|
|
37
|
+
if (!adapter)
|
|
38
|
+
return null;
|
|
39
|
+
return { adapter, providerName };
|
|
40
|
+
}
|
|
41
|
+
getProviderNames() {
|
|
42
|
+
return Array.from(this.adapters.keys());
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
exports.Router = Router;
|
|
46
|
+
//# sourceMappingURL=router.js.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { SmartContextConfig } from '../config/schema.js';
|
|
2
|
+
import { MetricsCollector } from '../metrics/collector.js';
|
|
3
|
+
import type { EmbeddingAdapter } from '../embedding/types.js';
|
|
4
|
+
import type { StorageAdapter } from '../storage/types.js';
|
|
5
|
+
export declare class ProxyServer {
|
|
6
|
+
private server;
|
|
7
|
+
private router;
|
|
8
|
+
private config;
|
|
9
|
+
private requestCount;
|
|
10
|
+
private optimizer;
|
|
11
|
+
private metrics;
|
|
12
|
+
private paused;
|
|
13
|
+
constructor(config: SmartContextConfig, embedding?: EmbeddingAdapter, storage?: StorageAdapter);
|
|
14
|
+
start(): Promise<void>;
|
|
15
|
+
stop(): Promise<void>;
|
|
16
|
+
getProviderNames(): string[];
|
|
17
|
+
getMetrics(): MetricsCollector;
|
|
18
|
+
setPaused(paused: boolean): void;
|
|
19
|
+
isPaused(): boolean;
|
|
20
|
+
private handleRequest;
|
|
21
|
+
private handleApiRequest;
|
|
22
|
+
private proxyRequest;
|
|
23
|
+
private readBody;
|
|
24
|
+
private log;
|
|
25
|
+
}
|