adaptive-memory-multi-model-router 2.14.59 → 2.15.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.
Files changed (140) hide show
  1. package/.github/ISSUE_TEMPLATE/checklist.md +35 -0
  2. package/.github/workflows/ci.yml +9 -6
  3. package/POPULARITY_BOOSTERS.md +18 -0
  4. package/PR_STATUS_REPORT.md +55 -148
  5. package/README.md +222 -1035
  6. package/assets/chart-accuracy-by-tier.svg +63 -0
  7. package/assets/chart-confusion-matrix.svg +98 -0
  8. package/assets/chart-cost-comparison.svg +66 -0
  9. package/assets/chart-latency-overhead.svg +102 -0
  10. package/assets/chart-routerena-leaderboard.svg +76 -0
  11. package/dist/analytics/costAnalytics.d.ts +1 -0
  12. package/dist/benchmark/comprehensive.d.ts +4 -53
  13. package/dist/benchmark/comprehensive.d.ts.map +1 -0
  14. package/dist/benchmark/comprehensive.js +112 -214
  15. package/dist/benchmark/comprehensive.js.map +1 -1
  16. package/dist/benchmark/reproducible.d.ts +1 -0
  17. package/dist/cache/semanticCache.d.ts +1 -0
  18. package/dist/cli/setupWizard.d.ts +6 -1
  19. package/dist/cli/setupWizard.d.ts.map +1 -1
  20. package/dist/cli/setupWizard.js +6 -9
  21. package/dist/cli/setupWizard.js.map +1 -1
  22. package/dist/cost/budgetEnforcer.d.ts +1 -0
  23. package/dist/cost/costTracker.d.ts +1 -0
  24. package/dist/ensemble/multiRoundDialog.d.ts +1 -0
  25. package/dist/ensemble/multiRoundDialog.d.ts.map +1 -0
  26. package/dist/ensemble/shapleyValue.d.ts +1 -0
  27. package/dist/ensemble/shapleyValue.d.ts.map +1 -0
  28. package/dist/ensemble.d.ts +1 -0
  29. package/dist/index.d.ts +1 -0
  30. package/dist/integrations/langchainAdapter.d.ts +1 -0
  31. package/dist/integrations/oauth.d.ts +1 -0
  32. package/dist/integrations/scienceAdapter.d.ts +1 -0
  33. package/dist/integrations/scienceAdapter.d.ts.map +1 -0
  34. package/dist/memory/autoFetch.d.ts +1 -0
  35. package/dist/memory/hybridMemory.d.ts +1 -0
  36. package/dist/memory/hybridMemory.d.ts.map +1 -0
  37. package/dist/memory/memoryTree.d.ts +1 -0
  38. package/dist/memory/memoryTree.d.ts.map +1 -1
  39. package/dist/memory/obsidianVault.d.ts +1 -0
  40. package/dist/memory/obsidianVault.d.ts.map +1 -1
  41. package/dist/memory/reasoningBank.d.ts +1 -0
  42. package/dist/memory/reasoningBank.d.ts.map +1 -0
  43. package/dist/observability/changeWatch.d.ts +1 -0
  44. package/dist/observability/fatigueDetector.d.ts +1 -0
  45. package/dist/observability/index.d.ts +1 -0
  46. package/dist/observability/metrics.d.ts +1 -0
  47. package/dist/observability/metrics.d.ts.map +1 -1
  48. package/dist/observability/middleware.d.ts +1 -0
  49. package/dist/observability/tracer.d.ts +1 -0
  50. package/dist/observability/tracer.d.ts.map +1 -1
  51. package/dist/observability/types.d.ts +1 -0
  52. package/dist/providers/providerConfig.d.ts +1 -0
  53. package/dist/providers/providerConfig.d.ts.map +1 -1
  54. package/dist/routing/advancedRouter.d.ts +2 -1
  55. package/dist/routing/advancedRouter.d.ts.map +1 -1
  56. package/dist/routing/crossModelValidation.d.ts +1 -0
  57. package/dist/routing/providerHealth.d.ts +1 -0
  58. package/dist/routing/providerHealth.d.ts.map +1 -1
  59. package/dist/routing/providerRetry.d.ts +1 -0
  60. package/dist/sdk.d.ts +1 -0
  61. package/dist/security/guardrails.d.ts +1 -0
  62. package/dist/security/guardrails.d.ts.map +1 -1
  63. package/dist/server/dashboard.d.ts +1 -0
  64. package/dist/server/handlers/chatHandler.d.ts +11 -0
  65. package/dist/server/handlers/chatHandler.d.ts.map +1 -0
  66. package/dist/server/handlers/chatHandler.js +159 -0
  67. package/dist/server/handlers/chatHandler.js.map +1 -0
  68. package/dist/server/handlers/completionsHandler.d.ts +10 -0
  69. package/dist/server/handlers/completionsHandler.d.ts.map +1 -0
  70. package/dist/server/handlers/completionsHandler.js +124 -0
  71. package/dist/server/handlers/completionsHandler.js.map +1 -0
  72. package/dist/server/handlers/embeddingsHandler.d.ts +17 -0
  73. package/dist/server/handlers/embeddingsHandler.d.ts.map +1 -0
  74. package/dist/server/handlers/embeddingsHandler.js +235 -0
  75. package/dist/server/handlers/embeddingsHandler.js.map +1 -0
  76. package/dist/server/handlers/healthHandler.d.ts +10 -0
  77. package/dist/server/handlers/healthHandler.d.ts.map +1 -0
  78. package/dist/server/handlers/healthHandler.js +49 -0
  79. package/dist/server/handlers/healthHandler.js.map +1 -0
  80. package/dist/server/handlers/metricsHandler.d.ts +11 -0
  81. package/dist/server/handlers/metricsHandler.d.ts.map +1 -0
  82. package/dist/server/handlers/metricsHandler.js +24 -0
  83. package/dist/server/handlers/metricsHandler.js.map +1 -0
  84. package/dist/server/handlers/modelsHandler.d.ts +10 -0
  85. package/dist/server/handlers/modelsHandler.d.ts.map +1 -0
  86. package/dist/server/handlers/modelsHandler.js +19 -0
  87. package/dist/server/handlers/modelsHandler.js.map +1 -0
  88. package/dist/server/metrics.d.ts +96 -0
  89. package/dist/server/metrics.d.ts.map +1 -0
  90. package/dist/server/metrics.js +267 -0
  91. package/dist/server/metrics.js.map +1 -0
  92. package/dist/server/modelMapper.d.ts +1 -0
  93. package/dist/server/proxyServer.d.ts +53 -17
  94. package/dist/server/proxyServer.d.ts.map +1 -1
  95. package/dist/server/proxyServer.js +66 -303
  96. package/dist/server/proxyServer.js.map +1 -1
  97. package/dist/server/router.d.ts +49 -0
  98. package/dist/server/router.d.ts.map +1 -0
  99. package/dist/server/router.js +120 -0
  100. package/dist/server/router.js.map +1 -0
  101. package/dist/server/state.d.ts +30 -0
  102. package/dist/server/state.d.ts.map +1 -0
  103. package/dist/server/state.js +18 -0
  104. package/dist/server/state.js.map +1 -0
  105. package/dist/skills/__tests__/skill_manager.test.d.ts +3 -0
  106. package/dist/skills/__tests__/skill_manager.test.d.ts.map +1 -1
  107. package/dist/skills/__tests__/skill_manager.test.js +3 -6
  108. package/dist/skills/__tests__/skill_manager.test.js.map +1 -1
  109. package/dist/tui/dashboard.d.ts +1 -0
  110. package/dist/tui/index.d.ts +1 -0
  111. package/dist/utils/costUtils.d.ts +1 -0
  112. package/dist/utils/reliability.js +4 -18
  113. package/dist/utils/sorting.d.ts +1 -0
  114. package/dist/utils/tokenUtils.d.ts +1 -0
  115. package/docs/assets/chart-accuracy-by-tier.svg +63 -0
  116. package/docs/assets/chart-confusion-matrix.svg +98 -0
  117. package/docs/assets/chart-cost-comparison.svg +66 -0
  118. package/docs/assets/chart-latency-overhead.svg +102 -0
  119. package/docs/assets/chart-routerena-leaderboard.svg +76 -0
  120. package/docs/index.html +72 -80
  121. package/docs/llms-full.txt +156 -184
  122. package/docs/llms.txt +77 -42
  123. package/llms-full.txt +156 -184
  124. package/llms.txt +77 -42
  125. package/mcp-server/package.json +6 -0
  126. package/mcp-server/tsconfig.json +3 -2
  127. package/package.json +4 -3
  128. package/scripts/postinstall-nudge.js +3 -0
  129. package/src/server/handlers/chatHandler.ts +173 -0
  130. package/src/server/handlers/completionsHandler.ts +120 -0
  131. package/src/server/handlers/embeddingsHandler.ts +271 -0
  132. package/src/server/handlers/healthHandler.ts +57 -0
  133. package/src/server/handlers/metricsHandler.ts +30 -0
  134. package/src/server/handlers/modelsHandler.ts +25 -0
  135. package/src/server/metrics.ts +303 -0
  136. package/src/server/proxyServer.ts +71 -394
  137. package/src/server/router.ts +157 -0
  138. package/src/server/state.ts +34 -0
  139. package/src/skills/__tests__/skill_manager.test.ts +3 -3
  140. package/index.html +0 -667
@@ -0,0 +1,173 @@
1
+ /**
2
+ * A3M Router - Chat Completions Handler
3
+ *
4
+ * Handles POST /v1/chat/completions
5
+ * Plug-and-play: register with router.registerRoute('POST', /^\/v1\/chat\/completions$/, handleChatCompletions);
6
+ */
7
+
8
+ import * as http from 'http';
9
+ import { RouteContext } from '../router';
10
+ import { resolveModel } from '../modelMapper';
11
+ import { costTracker } from '../state';
12
+ import { recordRequest, recordProviderError, recordActiveRequest } from '../metrics';
13
+
14
+ // ============================================================
15
+ // TYPES (local, matching OpenAI API)
16
+ // ============================================================
17
+
18
+ interface ChatMessage {
19
+ role: 'system' | 'user' | 'assistant' | 'tool';
20
+ content: string;
21
+ }
22
+
23
+ interface ChatRequest {
24
+ model: string;
25
+ messages: ChatMessage[];
26
+ stream?: boolean;
27
+ temperature?: number;
28
+ max_tokens?: number;
29
+ top_p?: number;
30
+ frequency_penalty?: number;
31
+ presence_penalty?: number;
32
+ stop?: string | string[];
33
+ n?: number;
34
+ }
35
+
36
+ // ============================================================
37
+ // HELPERS
38
+ // ============================================================
39
+
40
+ function readBody(req: http.IncomingMessage): Promise<string> {
41
+ return new Promise((resolve, reject) => {
42
+ const chunks: Buffer[] = [];
43
+ req.on('data', (chunk: Buffer) => chunks.push(chunk));
44
+ req.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
45
+ req.on('error', reject);
46
+ });
47
+ }
48
+
49
+ function jsonResponse(res: http.ServerResponse, statusCode: number, body: object): void {
50
+ res.writeHead(statusCode, {
51
+ 'Content-Type': 'application/json',
52
+ 'Access-Control-Allow-Origin': '*',
53
+ 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
54
+ 'Access-Control-Allow-Headers': 'Content-Type, Authorization',
55
+ });
56
+ res.end(JSON.stringify(body));
57
+ }
58
+
59
+ function errorResponse(res: http.ServerResponse, statusCode: number, message: string, errorType = 'server_error'): void {
60
+ jsonResponse(res, statusCode, { error: { message, type: errorType, code: statusCode } });
61
+ }
62
+
63
+ // ============================================================
64
+ // HANDLER
65
+ // ============================================================
66
+
67
+ export async function handleChatCompletions(
68
+ req: http.IncomingMessage,
69
+ res: http.ServerResponse,
70
+ ctx: RouteContext
71
+ ): Promise<void> {
72
+ recordActiveRequest(1);
73
+
74
+ const body = await readBody(req);
75
+ let request: ChatRequest;
76
+
77
+ try {
78
+ request = JSON.parse(body);
79
+ } catch {
80
+ recordActiveRequest(-1);
81
+ errorResponse(res, 400, 'Invalid JSON in request body', 'invalid_request_error');
82
+ return;
83
+ }
84
+
85
+ if (!request.messages || !Array.isArray(request.messages) || request.messages.length === 0) {
86
+ recordActiveRequest(-1);
87
+ errorResponse(res, 400, 'messages is required and must be a non-empty array', 'invalid_request_error');
88
+ return;
89
+ }
90
+
91
+ const model = request.model || 'auto';
92
+ const stream = request.stream || false;
93
+ const requestId = ctx.requestId;
94
+ const promptForRouting = request.messages.map((m) => m.content).join(' ');
95
+
96
+ const mapping = resolveModel(model, promptForRouting);
97
+ if (!mapping) {
98
+ recordActiveRequest(-1);
99
+ errorResponse(res, 503, `No provider available for model "${model}". Configure API keys.`, 'server_error');
100
+ return;
101
+ }
102
+
103
+ const startTime = Date.now();
104
+
105
+ if (stream) {
106
+ try {
107
+ const { streamProviderResponse } = await import('../proxyServer');
108
+ await streamProviderResponse(res, mapping, request.messages,
109
+ { temperature: request.temperature, max_tokens: request.max_tokens, stop: request.stop },
110
+ requestId);
111
+ const latencyMs = Date.now() - startTime;
112
+ recordRequest({ endpoint: '/v1/chat/completions', provider: mapping.providerId, status: 'success', durationMs: latencyMs, model });
113
+ recordActiveRequest(-1);
114
+ } catch (err: unknown) {
115
+ const message = err instanceof Error ? err.message : String(err);
116
+ if (!res.headersSent) errorResponse(res, 500, message);
117
+ recordProviderError(mapping.providerId, 'stream_error');
118
+ recordActiveRequest(-1);
119
+ }
120
+ return;
121
+ }
122
+
123
+ // Non-streaming path
124
+ try {
125
+ const { callWithFallback } = await import('../proxyServer');
126
+ const { result, mapping: usedMapping } = await callWithFallback(
127
+ model, request.messages,
128
+ { temperature: request.temperature, max_tokens: request.max_tokens, stop: request.stop },
129
+ promptForRouting
130
+ );
131
+
132
+ const latencyMs = Date.now() - startTime;
133
+ const inputCost = (result.usage.prompt_tokens / 1000) * usedMapping.costPerK.input;
134
+ const outputCost = (result.usage.completion_tokens / 1000) * usedMapping.costPerK.output;
135
+ const totalCost = inputCost + outputCost;
136
+
137
+ costTracker.record(usedMapping.providerId, usedMapping.model, result.usage.prompt_tokens, result.usage.completion_tokens);
138
+
139
+ const response = {
140
+ id: requestId,
141
+ object: 'chat.completion',
142
+ created: Math.floor(Date.now() / 1000),
143
+ model: result.model,
144
+ choices: [{ index: 0, message: { role: 'assistant' as const, content: result.content }, finish_reason: result.finish_reason }],
145
+ usage: result.usage,
146
+ };
147
+
148
+ jsonResponse(res, 200, response);
149
+ recordRequest({
150
+ endpoint: '/v1/chat/completions',
151
+ provider: usedMapping.providerId,
152
+ status: 'success',
153
+ durationMs: latencyMs,
154
+ tokensIn: result.usage.prompt_tokens,
155
+ tokensOut: result.usage.completion_tokens,
156
+ cost: totalCost,
157
+ model,
158
+ });
159
+
160
+ console.log(`[a3m-router] ${requestId} chat model=${model}→${usedMapping.providerId}/${usedMapping.model} latency=${latencyMs}ms tokens=${result.usage.total_tokens} cost=$${totalCost.toFixed(6)}`);
161
+ } catch (err: unknown) {
162
+ const message = err instanceof Error ? err.message : String(err);
163
+ const latencyMs = Date.now() - startTime;
164
+ recordRequest({ endpoint: '/v1/chat/completions', provider: mapping.providerId, status: 'error', durationMs: latencyMs, model });
165
+ recordProviderError(mapping.providerId, message.slice(0, 50));
166
+ console.error(`[a3m-router] ${requestId} ERROR model=${model}→${mapping.providerId}/${mapping.model} latency=${latencyMs}ms error=${message}`);
167
+ if (!res.headersSent) errorResponse(res, 502, message, 'upstream_error');
168
+ }
169
+
170
+ recordActiveRequest(-1);
171
+ }
172
+
173
+ export default handleChatCompletions;
@@ -0,0 +1,120 @@
1
+ /**
2
+ * A3M Router - Completions Handler
3
+ *
4
+ * Handles POST /v1/completions
5
+ */
6
+
7
+ import * as http from 'http';
8
+ import { RouteContext } from '../router';
9
+ import { resolveModel } from '../modelMapper';
10
+ import { recordRequest, recordActiveRequest } from '../metrics';
11
+
12
+ interface CompletionRequest {
13
+ model: string;
14
+ prompt: string | string[];
15
+ stream?: boolean;
16
+ temperature?: number;
17
+ max_tokens?: number;
18
+ top_p?: number;
19
+ echo?: boolean;
20
+ stop?: string | string[];
21
+ }
22
+
23
+ function readBody(req: http.IncomingMessage): Promise<string> {
24
+ return new Promise((resolve, reject) => {
25
+ const chunks: Buffer[] = [];
26
+ req.on('data', (chunk: Buffer) => chunks.push(chunk));
27
+ req.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
28
+ req.on('error', reject);
29
+ });
30
+ }
31
+
32
+ function jsonResponse(res: http.ServerResponse, statusCode: number, body: object): void {
33
+ res.writeHead(statusCode, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
34
+ res.end(JSON.stringify(body));
35
+ }
36
+
37
+ function errorResponse(res: http.ServerResponse, statusCode: number, message: string, errorType = 'server_error'): void {
38
+ jsonResponse(res, statusCode, { error: { message, type: errorType, code: statusCode } });
39
+ }
40
+
41
+ export async function handleCompletions(
42
+ req: http.IncomingMessage,
43
+ res: http.ServerResponse,
44
+ ctx: RouteContext
45
+ ): Promise<void> {
46
+ recordActiveRequest(1);
47
+ const body = await readBody(req);
48
+ let request: CompletionRequest;
49
+
50
+ try {
51
+ request = JSON.parse(body);
52
+ } catch {
53
+ recordActiveRequest(-1);
54
+ errorResponse(res, 400, 'Invalid JSON in request body', 'invalid_request_error');
55
+ return;
56
+ }
57
+
58
+ const prompts = Array.isArray(request.prompt) ? request.prompt : [request.prompt || ''];
59
+ const messages = prompts.map((p) => ({ role: 'user' as const, content: p }));
60
+ const model = request.model || 'auto';
61
+ const stream = request.stream || false;
62
+ const requestId = ctx.requestId;
63
+ const promptForRouting = prompts.join(' ');
64
+
65
+ const mapping = resolveModel(model, promptForRouting);
66
+ if (!mapping) {
67
+ recordActiveRequest(-1);
68
+ errorResponse(res, 503, `No provider available for model "${model}".`, 'server_error');
69
+ return;
70
+ }
71
+
72
+ const startTime = Date.now();
73
+
74
+ if (stream) {
75
+ try {
76
+ const { streamProviderResponse } = await import('../proxyServer');
77
+ await streamProviderResponse(res, mapping, messages,
78
+ { temperature: request.temperature, max_tokens: request.max_tokens, stop: request.stop },
79
+ requestId);
80
+ const latencyMs = Date.now() - startTime;
81
+ recordRequest({ endpoint: '/v1/completions', provider: mapping.providerId, status: 'success', durationMs: latencyMs, model });
82
+ recordActiveRequest(-1);
83
+ } catch (err: unknown) {
84
+ const message = err instanceof Error ? err.message : String(err);
85
+ if (!res.headersSent) errorResponse(res, 500, message);
86
+ recordActiveRequest(-1);
87
+ }
88
+ return;
89
+ }
90
+
91
+ try {
92
+ const { callWithFallback } = await import('../proxyServer');
93
+ const { result, mapping: usedMapping } = await callWithFallback(
94
+ model, messages,
95
+ { temperature: request.temperature, max_tokens: request.max_tokens, stop: request.stop },
96
+ promptForRouting
97
+ );
98
+
99
+ const latencyMs = Date.now() - startTime;
100
+ const response = {
101
+ id: requestId,
102
+ object: 'text_completion',
103
+ created: Math.floor(Date.now() / 1000),
104
+ model: result.model,
105
+ choices: [{ text: result.content, index: 0, finish_reason: result.finish_reason }],
106
+ usage: result.usage,
107
+ };
108
+
109
+ jsonResponse(res, 200, response);
110
+ recordRequest({ endpoint: '/v1/completions', provider: usedMapping.providerId, status: 'success', durationMs: latencyMs, tokensIn: result.usage.prompt_tokens, tokensOut: result.usage.completion_tokens, model });
111
+ console.log(`[a3m-router] ${requestId} completion model=${model}→${usedMapping.providerId}/${usedMapping.model} latency=${latencyMs}ms`);
112
+ } catch (err: unknown) {
113
+ const message = err instanceof Error ? err.message : String(err);
114
+ if (!res.headersSent) errorResponse(res, 502, message, 'upstream_error');
115
+ }
116
+
117
+ recordActiveRequest(-1);
118
+ }
119
+
120
+ export default handleCompletions;
@@ -0,0 +1,271 @@
1
+ /**
2
+ * A3M Router - Embeddings Handler
3
+ *
4
+ * Handles POST /v1/embeddings
5
+ * OpenAI-compatible embeddings endpoint.
6
+ *
7
+ * Routes embedding requests to the best available provider:
8
+ * - OpenAI (text-embedding-3-small, text-embedding-3-large, text-embedding-ada-002)
9
+ * - Cohere (embed-english-v3.0, embed-multilingual-v3.0)
10
+ * - Google (text-embedding-004)
11
+ * - Local Ollama (nomic-embed-text, etc.)
12
+ */
13
+
14
+ import * as http from 'http';
15
+ import { RouteContext } from '../router';
16
+ import { getAvailableProviders } from '../../providers/providerConfig';
17
+ import { recordRequest, recordProviderError, recordActiveRequest } from '../metrics';
18
+
19
+ interface EmbeddingsRequest {
20
+ model?: string;
21
+ input: string | string[];
22
+ encoding_format?: 'float' | 'base64';
23
+ dimensions?: number;
24
+ user?: string;
25
+ }
26
+
27
+ interface ProviderAdapter {
28
+ call: (url: string, apiKey: string, model: string, input: string[], dimensions?: number) => Promise<number[][]>;
29
+ urlFor: (apiKey: string, model: string) => string;
30
+ modelMap: (model: string) => string | null;
31
+ }
32
+
33
+ function readBody(req: http.IncomingMessage): Promise<string> {
34
+ return new Promise((resolve, reject) => {
35
+ const chunks: Buffer[] = [];
36
+ req.on('data', (chunk: Buffer) => chunks.push(chunk));
37
+ req.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
38
+ req.on('error', reject);
39
+ });
40
+ }
41
+
42
+ function jsonResponse(res: http.ServerResponse, statusCode: number, body: object): void {
43
+ res.writeHead(statusCode, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
44
+ res.end(JSON.stringify(body));
45
+ }
46
+
47
+ function errorResponse(res: http.ServerResponse, statusCode: number, message: string, errorType = 'server_error'): void {
48
+ jsonResponse(res, statusCode, { error: { message, type: errorType, code: statusCode } });
49
+ }
50
+
51
+ /**
52
+ * Get the best available embeddings provider.
53
+ */
54
+ function getEmbeddingsProvider(): { providerId: string; model: string; adapter: ProviderAdapter } | null {
55
+ const providers = getAvailableProviders();
56
+
57
+ // Priority: OpenAI > Cohere > Google > Ollama
58
+ const priority = ['openai', 'cohere', 'google', 'ollama'];
59
+
60
+ for (const id of priority) {
61
+ const provider = providers[id];
62
+ if (!provider || !provider.apiKey) continue;
63
+
64
+ if (id === 'openai') {
65
+ return {
66
+ providerId: 'openai',
67
+ model: 'text-embedding-3-small',
68
+ adapter: openAIAdapter,
69
+ };
70
+ }
71
+
72
+ if (id === 'cohere') {
73
+ return {
74
+ providerId: 'cohere',
75
+ model: 'embed-english-v3.0',
76
+ adapter: cohereAdapter,
77
+ };
78
+ }
79
+
80
+ if (id === 'google') {
81
+ return {
82
+ providerId: 'google',
83
+ model: 'text-embedding-004',
84
+ adapter: googleAdapter,
85
+ };
86
+ }
87
+
88
+ if (id === 'ollama' && provider.baseUrl) {
89
+ return {
90
+ providerId: 'ollama',
91
+ model: 'nomic-embed-text:latest',
92
+ adapter: ollamaAdapter,
93
+ };
94
+ }
95
+ }
96
+
97
+ return null;
98
+ }
99
+
100
+ // ============================================================
101
+ // PROVIDER ADAPTERS
102
+ // ============================================================
103
+
104
+ const openAIAdapter: ProviderAdapter = {
105
+ modelMap(model: string): string | null {
106
+ const valid = ['text-embedding-3-small', 'text-embedding-3-large', 'text-embedding-ada-002'];
107
+ return valid.includes(model) ? model : 'text-embedding-3-small';
108
+ },
109
+ urlFor(_apiKey: string, _model: string): string {
110
+ return 'https://api.openai.com/v1/embeddings';
111
+ },
112
+ async call(url, apiKey, model, input, dimensions): Promise<number[][]> {
113
+ const body: Record<string, unknown> = { model, input };
114
+ if (dimensions) body.dimensions = dimensions;
115
+ const resp = await fetch(url, {
116
+ method: 'POST',
117
+ headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` },
118
+ body: JSON.stringify(body),
119
+ });
120
+ const data = await resp.json() as any;
121
+ if (data.error) throw new Error(data.error.message || JSON.stringify(data.error));
122
+ return data.data.map((item: any) => item.embedding as number[]);
123
+ },
124
+ };
125
+
126
+ const cohereAdapter: ProviderAdapter = {
127
+ modelMap(model: string): string | null {
128
+ const valid = ['embed-english-v3.0', 'embed-multilingual-v3.0', 'embed-english-v2.0'];
129
+ return valid.includes(model) ? model : 'embed-english-v3.0';
130
+ },
131
+ urlFor(_apiKey: string, _model: string): string {
132
+ return 'https://api.cohere.ai/v2/embed';
133
+ },
134
+ async call(url, apiKey, model, input, _dimensions): Promise<number[][]> {
135
+ const resp = await fetch(url, {
136
+ method: 'POST',
137
+ headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` },
138
+ body: JSON.stringify({ model, texts: input }),
139
+ });
140
+ const data = await resp.json() as any;
141
+ if (data.error) throw new Error(data.error.message || JSON.stringify(data.error));
142
+ return data.embeddings as number[][];
143
+ },
144
+ };
145
+
146
+ const googleAdapter: ProviderAdapter = {
147
+ modelMap(model: string): string | null {
148
+ return 'text-embedding-004';
149
+ },
150
+ urlFor(apiKey: string, _model: string): string {
151
+ return `https://generativelanguage.googleapis.com/v1beta2/models/text-embedding-004:predict?key=${apiKey}`;
152
+ },
153
+ async call(url, _apiKey, model, input, _dimensions): Promise<number[][]> {
154
+ const resp = await fetch(url, {
155
+ method: 'POST',
156
+ headers: { 'Content-Type': 'application/json' },
157
+ body: JSON.stringify({
158
+ instances: input.map((text) => ({ content: text })),
159
+ parameters: { outputDimensionality: 768 },
160
+ }),
161
+ });
162
+ const data = await resp.json() as any;
163
+ if (data.error) throw new Error(data.error.message || JSON.stringify(data.error));
164
+ return data.predictions.map((p: any) => p.embeddings.values as number[]);
165
+ },
166
+ };
167
+
168
+ const ollamaAdapter: ProviderAdapter = {
169
+ modelMap(model: string): string | null {
170
+ return model || 'nomic-embed-text:latest';
171
+ },
172
+ urlFor(baseUrl: string): string {
173
+ return `${baseUrl}/api/embeddings`;
174
+ },
175
+ async call(url, _apiKey, model, input): Promise<number[][]> {
176
+ const results: number[][] = [];
177
+ for (const text of input) {
178
+ const resp = await fetch(url, {
179
+ method: 'POST',
180
+ headers: { 'Content-Type': 'application/json' },
181
+ body: JSON.stringify({ model, prompt: text }),
182
+ });
183
+ const data = await resp.json() as any;
184
+ if (data.error) throw new Error(data.error);
185
+ results.push(data.embedding as number[]);
186
+ }
187
+ return results;
188
+ },
189
+ };
190
+
191
+ // ============================================================
192
+ // HANDLER
193
+ // ============================================================
194
+
195
+ export async function handleEmbeddings(
196
+ req: http.IncomingMessage,
197
+ res: http.ServerResponse,
198
+ ctx: RouteContext
199
+ ): Promise<void> {
200
+ recordActiveRequest(1);
201
+ const body = await readBody(req);
202
+ let request: EmbeddingsRequest;
203
+
204
+ try {
205
+ request = JSON.parse(body);
206
+ } catch {
207
+ recordActiveRequest(-1);
208
+ errorResponse(res, 400, 'Invalid JSON in request body', 'invalid_request_error');
209
+ return;
210
+ }
211
+
212
+ const inputs = Array.isArray(request.input) ? request.input : [request.input];
213
+ if (inputs.length === 0) {
214
+ recordActiveRequest(-1);
215
+ errorResponse(res, 400, 'input is required', 'invalid_request_error');
216
+ return;
217
+ }
218
+
219
+ const provider = getEmbeddingsProvider();
220
+ if (!provider) {
221
+ recordActiveRequest(-1);
222
+ errorResponse(res, 503, 'No embeddings provider available. Configure API keys for OpenAI, Cohere, or Google.', 'server_error');
223
+ return;
224
+ }
225
+
226
+ const model = request.model || provider.model;
227
+ const adapter = provider.adapter;
228
+ const providers = getAvailableProviders();
229
+ const providerConfig = providers[provider.providerId];
230
+ const resolvedModel = adapter.modelMap(model) || provider.model;
231
+ const url = adapter.urlFor(providerConfig.apiKey || '', resolvedModel);
232
+
233
+ const startTime = Date.now();
234
+
235
+ try {
236
+ const embeddings = await adapter.call(url, providerConfig.apiKey || '', resolvedModel, inputs, request.dimensions);
237
+ const latencyMs = Date.now() - startTime;
238
+
239
+ const response = {
240
+ object: 'list',
241
+ data: embeddings.map((embedding, i) => ({
242
+ object: 'embedding',
243
+ embedding,
244
+ index: i,
245
+ })),
246
+ model: resolvedModel,
247
+ usage: {
248
+ prompt_tokens: 0, // Embedding APIs don't always return token counts
249
+ total_tokens: 0,
250
+ },
251
+ };
252
+
253
+ jsonResponse(res, 200, response);
254
+ recordRequest({
255
+ endpoint: '/v1/embeddings',
256
+ provider: provider.providerId,
257
+ status: 'success',
258
+ durationMs: latencyMs,
259
+ model: resolvedModel,
260
+ });
261
+ } catch (err: unknown) {
262
+ const message = err instanceof Error ? err.message : String(err);
263
+ if (!res.headersSent) errorResponse(res, 502, message, 'upstream_error');
264
+ recordProviderError(provider.providerId, 'embeddings_error');
265
+ console.error(`[a3m-router] ${ctx.requestId} embeddings ERROR ${provider.providerId}: ${message}`);
266
+ }
267
+
268
+ recordActiveRequest(-1);
269
+ }
270
+
271
+ export default handleEmbeddings;
@@ -0,0 +1,57 @@
1
+ /**
2
+ * A3M Router - Health Handler
3
+ *
4
+ * Handles GET /health
5
+ */
6
+
7
+ import * as http from 'http';
8
+ import { RouteContext } from '../router';
9
+ import { getAvailableProviders } from '../../providers/providerConfig';
10
+ import { costTracker, requestLogs } from '../state';
11
+
12
+ function jsonResponse(res: http.ServerResponse, statusCode: number, body: object): void {
13
+ res.writeHead(statusCode, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
14
+ res.end(JSON.stringify(body));
15
+ }
16
+
17
+ export async function handleHealth(
18
+ _req: http.IncomingMessage,
19
+ res: http.ServerResponse,
20
+ _ctx: RouteContext
21
+ ): Promise<void> {
22
+ const available = getAvailableProviders();
23
+ const providerStatus: Record<string, object> = {};
24
+ let healthyCount = 0;
25
+
26
+ for (const [id, provider] of Object.entries(available)) {
27
+ const hasKey = !!provider.apiKey;
28
+ const isAvailable = provider.type !== 'api' || hasKey;
29
+ providerStatus[id] = {
30
+ name: provider.name || id,
31
+ type: provider.type,
32
+ models: provider.models?.length || 0,
33
+ available: isAvailable,
34
+ };
35
+ if (isAvailable) healthyCount++;
36
+ }
37
+
38
+ const costSummary = costTracker.getSummary();
39
+
40
+ jsonResponse(res, 200, {
41
+ status: 'ok',
42
+ version: '2.14.60',
43
+ providers: {
44
+ total: Object.keys(available).length,
45
+ healthy: healthyCount,
46
+ details: providerStatus,
47
+ },
48
+ cost: {
49
+ total: costSummary.total_cost,
50
+ requests: costSummary.request_count,
51
+ },
52
+ uptime: process.uptime(),
53
+ recentRequests: requestLogs.slice(-20),
54
+ });
55
+ }
56
+
57
+ export default handleHealth;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * A3M Router - Prometheus Metrics Handler
3
+ *
4
+ * Handles GET /metrics
5
+ * Returns Prometheus-compatible metrics output.
6
+ */
7
+
8
+ import * as http from 'http';
9
+ import { RouteContext } from '../router';
10
+ import { generatePrometheusMetrics } from '../metrics';
11
+
12
+ function textResponse(res: http.ServerResponse, statusCode: number, body: string): void {
13
+ res.writeHead(statusCode, {
14
+ 'Content-Type': 'text/plain; version=0.0.4; charset=utf-8',
15
+ 'Access-Control-Allow-Origin': '*',
16
+ 'Cache-Control': 'no-cache',
17
+ });
18
+ res.end(body);
19
+ }
20
+
21
+ export function handleMetrics(
22
+ _req: http.IncomingMessage,
23
+ res: http.ServerResponse,
24
+ _ctx: RouteContext
25
+ ): void {
26
+ const output = generatePrometheusMetrics();
27
+ textResponse(res, 200, output);
28
+ }
29
+
30
+ export default handleMetrics;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * A3M Router - Models Handler
3
+ *
4
+ * Handles GET /v1/models
5
+ */
6
+
7
+ import * as http from 'http';
8
+ import { RouteContext } from '../router';
9
+ import { listAvailableModels } from '../modelMapper';
10
+
11
+ function jsonResponse(res: http.ServerResponse, statusCode: number, body: object): void {
12
+ res.writeHead(statusCode, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
13
+ res.end(JSON.stringify(body));
14
+ }
15
+
16
+ export function handleModels(
17
+ _req: http.IncomingMessage,
18
+ res: http.ServerResponse,
19
+ _ctx: RouteContext
20
+ ): void {
21
+ const models = listAvailableModels();
22
+ jsonResponse(res, 200, { object: 'list', data: models });
23
+ }
24
+
25
+ export default handleModels;