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.
- package/.github/ISSUE_TEMPLATE/checklist.md +35 -0
- package/.github/workflows/ci.yml +9 -6
- package/POPULARITY_BOOSTERS.md +18 -0
- package/PR_STATUS_REPORT.md +55 -148
- package/README.md +222 -1035
- package/assets/chart-accuracy-by-tier.svg +63 -0
- package/assets/chart-confusion-matrix.svg +98 -0
- package/assets/chart-cost-comparison.svg +66 -0
- package/assets/chart-latency-overhead.svg +102 -0
- package/assets/chart-routerena-leaderboard.svg +76 -0
- package/dist/analytics/costAnalytics.d.ts +1 -0
- package/dist/benchmark/comprehensive.d.ts +4 -53
- package/dist/benchmark/comprehensive.d.ts.map +1 -0
- package/dist/benchmark/comprehensive.js +112 -214
- package/dist/benchmark/comprehensive.js.map +1 -1
- package/dist/benchmark/reproducible.d.ts +1 -0
- package/dist/cache/semanticCache.d.ts +1 -0
- package/dist/cli/setupWizard.d.ts +6 -1
- package/dist/cli/setupWizard.d.ts.map +1 -1
- package/dist/cli/setupWizard.js +6 -9
- package/dist/cli/setupWizard.js.map +1 -1
- package/dist/cost/budgetEnforcer.d.ts +1 -0
- package/dist/cost/costTracker.d.ts +1 -0
- package/dist/ensemble/multiRoundDialog.d.ts +1 -0
- package/dist/ensemble/multiRoundDialog.d.ts.map +1 -0
- package/dist/ensemble/shapleyValue.d.ts +1 -0
- package/dist/ensemble/shapleyValue.d.ts.map +1 -0
- package/dist/ensemble.d.ts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/integrations/langchainAdapter.d.ts +1 -0
- package/dist/integrations/oauth.d.ts +1 -0
- package/dist/integrations/scienceAdapter.d.ts +1 -0
- package/dist/integrations/scienceAdapter.d.ts.map +1 -0
- package/dist/memory/autoFetch.d.ts +1 -0
- package/dist/memory/hybridMemory.d.ts +1 -0
- package/dist/memory/hybridMemory.d.ts.map +1 -0
- package/dist/memory/memoryTree.d.ts +1 -0
- package/dist/memory/memoryTree.d.ts.map +1 -1
- package/dist/memory/obsidianVault.d.ts +1 -0
- package/dist/memory/obsidianVault.d.ts.map +1 -1
- package/dist/memory/reasoningBank.d.ts +1 -0
- package/dist/memory/reasoningBank.d.ts.map +1 -0
- package/dist/observability/changeWatch.d.ts +1 -0
- package/dist/observability/fatigueDetector.d.ts +1 -0
- package/dist/observability/index.d.ts +1 -0
- package/dist/observability/metrics.d.ts +1 -0
- package/dist/observability/metrics.d.ts.map +1 -1
- package/dist/observability/middleware.d.ts +1 -0
- package/dist/observability/tracer.d.ts +1 -0
- package/dist/observability/tracer.d.ts.map +1 -1
- package/dist/observability/types.d.ts +1 -0
- package/dist/providers/providerConfig.d.ts +1 -0
- package/dist/providers/providerConfig.d.ts.map +1 -1
- package/dist/routing/advancedRouter.d.ts +2 -1
- package/dist/routing/advancedRouter.d.ts.map +1 -1
- package/dist/routing/crossModelValidation.d.ts +1 -0
- package/dist/routing/providerHealth.d.ts +1 -0
- package/dist/routing/providerHealth.d.ts.map +1 -1
- package/dist/routing/providerRetry.d.ts +1 -0
- package/dist/sdk.d.ts +1 -0
- package/dist/security/guardrails.d.ts +1 -0
- package/dist/security/guardrails.d.ts.map +1 -1
- package/dist/server/dashboard.d.ts +1 -0
- package/dist/server/handlers/chatHandler.d.ts +11 -0
- package/dist/server/handlers/chatHandler.d.ts.map +1 -0
- package/dist/server/handlers/chatHandler.js +159 -0
- package/dist/server/handlers/chatHandler.js.map +1 -0
- package/dist/server/handlers/completionsHandler.d.ts +10 -0
- package/dist/server/handlers/completionsHandler.d.ts.map +1 -0
- package/dist/server/handlers/completionsHandler.js +124 -0
- package/dist/server/handlers/completionsHandler.js.map +1 -0
- package/dist/server/handlers/embeddingsHandler.d.ts +17 -0
- package/dist/server/handlers/embeddingsHandler.d.ts.map +1 -0
- package/dist/server/handlers/embeddingsHandler.js +235 -0
- package/dist/server/handlers/embeddingsHandler.js.map +1 -0
- package/dist/server/handlers/healthHandler.d.ts +10 -0
- package/dist/server/handlers/healthHandler.d.ts.map +1 -0
- package/dist/server/handlers/healthHandler.js +49 -0
- package/dist/server/handlers/healthHandler.js.map +1 -0
- package/dist/server/handlers/metricsHandler.d.ts +11 -0
- package/dist/server/handlers/metricsHandler.d.ts.map +1 -0
- package/dist/server/handlers/metricsHandler.js +24 -0
- package/dist/server/handlers/metricsHandler.js.map +1 -0
- package/dist/server/handlers/modelsHandler.d.ts +10 -0
- package/dist/server/handlers/modelsHandler.d.ts.map +1 -0
- package/dist/server/handlers/modelsHandler.js +19 -0
- package/dist/server/handlers/modelsHandler.js.map +1 -0
- package/dist/server/metrics.d.ts +96 -0
- package/dist/server/metrics.d.ts.map +1 -0
- package/dist/server/metrics.js +267 -0
- package/dist/server/metrics.js.map +1 -0
- package/dist/server/modelMapper.d.ts +1 -0
- package/dist/server/proxyServer.d.ts +53 -17
- package/dist/server/proxyServer.d.ts.map +1 -1
- package/dist/server/proxyServer.js +66 -303
- package/dist/server/proxyServer.js.map +1 -1
- package/dist/server/router.d.ts +49 -0
- package/dist/server/router.d.ts.map +1 -0
- package/dist/server/router.js +120 -0
- package/dist/server/router.js.map +1 -0
- package/dist/server/state.d.ts +30 -0
- package/dist/server/state.d.ts.map +1 -0
- package/dist/server/state.js +18 -0
- package/dist/server/state.js.map +1 -0
- package/dist/skills/__tests__/skill_manager.test.d.ts +3 -0
- package/dist/skills/__tests__/skill_manager.test.d.ts.map +1 -1
- package/dist/skills/__tests__/skill_manager.test.js +3 -6
- package/dist/skills/__tests__/skill_manager.test.js.map +1 -1
- package/dist/tui/dashboard.d.ts +1 -0
- package/dist/tui/index.d.ts +1 -0
- package/dist/utils/costUtils.d.ts +1 -0
- package/dist/utils/reliability.js +4 -18
- package/dist/utils/sorting.d.ts +1 -0
- package/dist/utils/tokenUtils.d.ts +1 -0
- package/docs/assets/chart-accuracy-by-tier.svg +63 -0
- package/docs/assets/chart-confusion-matrix.svg +98 -0
- package/docs/assets/chart-cost-comparison.svg +66 -0
- package/docs/assets/chart-latency-overhead.svg +102 -0
- package/docs/assets/chart-routerena-leaderboard.svg +76 -0
- package/docs/index.html +72 -80
- package/docs/llms-full.txt +156 -184
- package/docs/llms.txt +77 -42
- package/llms-full.txt +156 -184
- package/llms.txt +77 -42
- package/mcp-server/package.json +6 -0
- package/mcp-server/tsconfig.json +3 -2
- package/package.json +4 -3
- package/scripts/postinstall-nudge.js +3 -0
- package/src/server/handlers/chatHandler.ts +173 -0
- package/src/server/handlers/completionsHandler.ts +120 -0
- package/src/server/handlers/embeddingsHandler.ts +271 -0
- package/src/server/handlers/healthHandler.ts +57 -0
- package/src/server/handlers/metricsHandler.ts +30 -0
- package/src/server/handlers/modelsHandler.ts +25 -0
- package/src/server/metrics.ts +303 -0
- package/src/server/proxyServer.ts +71 -394
- package/src/server/router.ts +157 -0
- package/src/server/state.ts +34 -0
- package/src/skills/__tests__/skill_manager.test.ts +3 -3
- package/index.html +0 -667
|
@@ -14,10 +14,17 @@
|
|
|
14
14
|
* Run: npx a3m-router serve [--port 8787]
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import * as http from
|
|
18
|
-
import { resolveModel, listAvailableModels, ModelMapping } from
|
|
19
|
-
import { getAvailableProviders, healthCheck as providerHealthCheck } from
|
|
20
|
-
import {
|
|
17
|
+
import * as http from 'http';
|
|
18
|
+
import { resolveModel, listAvailableModels, ModelMapping } from './modelMapper';
|
|
19
|
+
import { getAvailableProviders, healthCheck as providerHealthCheck } from '../providers/providerConfig';
|
|
20
|
+
import { costTracker, requestLogs, RequestLog } from './state';
|
|
21
|
+
import { registerRoute, createRequestHandler } from './router';
|
|
22
|
+
import { handleChatCompletions } from './handlers/chatHandler';
|
|
23
|
+
import { handleCompletions } from './handlers/completionsHandler';
|
|
24
|
+
import { handleModels } from './handlers/modelsHandler';
|
|
25
|
+
import { handleHealth } from './handlers/healthHandler';
|
|
26
|
+
import { handleMetrics } from './handlers/metricsHandler';
|
|
27
|
+
import { handleEmbeddings } from './handlers/embeddingsHandler';
|
|
21
28
|
|
|
22
29
|
// ============================================================
|
|
23
30
|
// TYPES
|
|
@@ -52,19 +59,7 @@ interface CompletionRequest {
|
|
|
52
59
|
stop?: string | string[];
|
|
53
60
|
}
|
|
54
61
|
|
|
55
|
-
|
|
56
|
-
id: string;
|
|
57
|
-
model: string;
|
|
58
|
-
resolvedProvider: string;
|
|
59
|
-
resolvedModel: string;
|
|
60
|
-
latencyMs: number;
|
|
61
|
-
tokensIn: number;
|
|
62
|
-
tokensOut: number;
|
|
63
|
-
cost: number;
|
|
64
|
-
status: "success" | "error";
|
|
65
|
-
error?: string;
|
|
66
|
-
timestamp: number;
|
|
67
|
-
}
|
|
62
|
+
|
|
68
63
|
|
|
69
64
|
// ============================================================
|
|
70
65
|
// HELPERS
|
|
@@ -132,7 +127,7 @@ interface ProviderCallResult {
|
|
|
132
127
|
* Call the actual LLM provider with the given messages.
|
|
133
128
|
* Handles OpenAI-compatible APIs, Anthropic, Google, and local providers.
|
|
134
129
|
*/
|
|
135
|
-
async function callProvider(
|
|
130
|
+
export async function callProvider(
|
|
136
131
|
mapping: ModelMapping,
|
|
137
132
|
messages: ChatMessage[],
|
|
138
133
|
options: { temperature?: number; max_tokens?: number; stream?: boolean; stop?: string | string[] }
|
|
@@ -395,7 +390,7 @@ async function callLocalProvider(
|
|
|
395
390
|
/**
|
|
396
391
|
* Stream a provider response as SSE chunks.
|
|
397
392
|
*/
|
|
398
|
-
async function streamProviderResponse(
|
|
393
|
+
export async function streamProviderResponse(
|
|
399
394
|
res: http.ServerResponse,
|
|
400
395
|
mapping: ModelMapping,
|
|
401
396
|
messages: ChatMessage[],
|
|
@@ -663,7 +658,7 @@ async function streamProviderResponse(
|
|
|
663
658
|
/**
|
|
664
659
|
* Try the primary mapping, then fall back to alternatives.
|
|
665
660
|
*/
|
|
666
|
-
async function callWithFallback(
|
|
661
|
+
export async function callWithFallback(
|
|
667
662
|
model: string,
|
|
668
663
|
messages: ChatMessage[],
|
|
669
664
|
options: { temperature?: number; max_tokens?: number; stream?: boolean; stop?: string | string[] },
|
|
@@ -710,419 +705,101 @@ async function callWithFallback(
|
|
|
710
705
|
throw new Error(`All providers failed for model "${model}". Check your API keys.`);
|
|
711
706
|
}
|
|
712
707
|
|
|
713
|
-
// ============================================================
|
|
714
|
-
// REQUEST HANDLERS
|
|
715
|
-
// ============================================================
|
|
716
|
-
|
|
717
|
-
const requestLogs: RequestLog[] = [];
|
|
718
|
-
const costTracker = new CostTracker();
|
|
719
|
-
|
|
720
|
-
/**
|
|
721
|
-
* Handle POST /v1/chat/completions
|
|
722
|
-
*/
|
|
723
|
-
async function handleChatCompletions(
|
|
724
|
-
req: http.IncomingMessage,
|
|
725
|
-
res: http.ServerResponse
|
|
726
|
-
): Promise<void> {
|
|
727
|
-
const body = await readBody(req);
|
|
728
|
-
let request: ChatRequest;
|
|
729
|
-
|
|
730
|
-
try {
|
|
731
|
-
request = JSON.parse(body);
|
|
732
|
-
} catch {
|
|
733
|
-
errorResponse(res, 400, "Invalid JSON in request body", "invalid_request_error");
|
|
734
|
-
return;
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
if (!request.messages || !Array.isArray(request.messages) || request.messages.length === 0) {
|
|
738
|
-
errorResponse(res, 400, "messages is required and must be a non-empty array", "invalid_request_error");
|
|
739
|
-
return;
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
const model = request.model || "auto";
|
|
743
|
-
const stream = request.stream || false;
|
|
744
|
-
const requestId = generateId();
|
|
745
|
-
|
|
746
|
-
// Build the prompt from messages for routing
|
|
747
|
-
const promptForRouting = request.messages.map((m) => m.content).join(" ");
|
|
748
|
-
|
|
749
|
-
// Resolve model
|
|
750
|
-
const mapping = resolveModel(model, promptForRouting);
|
|
751
|
-
if (!mapping) {
|
|
752
|
-
errorResponse(res, 503, `No provider available for model "${model}". Configure API keys.`, "server_error");
|
|
753
|
-
return;
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
const startTime = Date.now();
|
|
757
|
-
|
|
758
|
-
if (stream) {
|
|
759
|
-
// Streaming response
|
|
760
|
-
try {
|
|
761
|
-
await streamProviderResponse(
|
|
762
|
-
res,
|
|
763
|
-
mapping,
|
|
764
|
-
request.messages,
|
|
765
|
-
{ temperature: request.temperature, max_tokens: request.max_tokens, stop: request.stop },
|
|
766
|
-
requestId
|
|
767
|
-
);
|
|
768
|
-
|
|
769
|
-
const latencyMs = Date.now() - startTime;
|
|
770
|
-
logRequest({
|
|
771
|
-
id: requestId,
|
|
772
|
-
model,
|
|
773
|
-
resolvedProvider: mapping.providerId,
|
|
774
|
-
resolvedModel: mapping.model,
|
|
775
|
-
latencyMs,
|
|
776
|
-
tokensIn: 0,
|
|
777
|
-
tokensOut: 0,
|
|
778
|
-
cost: 0,
|
|
779
|
-
status: "success",
|
|
780
|
-
timestamp: Date.now(),
|
|
781
|
-
});
|
|
782
|
-
|
|
783
|
-
console.log(
|
|
784
|
-
`[a3m-proxy] ${requestId} stream model=${model}→${mapping.providerId}/${mapping.model} latency=${latencyMs}ms`
|
|
785
|
-
);
|
|
786
|
-
} catch (err: any) {
|
|
787
|
-
if (!res.headersSent) {
|
|
788
|
-
errorResponse(res, 500, err.message);
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
|
-
} else {
|
|
792
|
-
// Non-streaming response
|
|
793
|
-
try {
|
|
794
|
-
const { result, mapping: usedMapping } = await callWithFallback(
|
|
795
|
-
model,
|
|
796
|
-
request.messages,
|
|
797
|
-
{ temperature: request.temperature, max_tokens: request.max_tokens, stop: request.stop },
|
|
798
|
-
promptForRouting
|
|
799
|
-
);
|
|
800
|
-
|
|
801
|
-
const latencyMs = Date.now() - startTime;
|
|
802
|
-
const inputCost = (result.usage.prompt_tokens / 1000) * (usedMapping.costPerK.input);
|
|
803
|
-
const outputCost = (result.usage.completion_tokens / 1000) * (usedMapping.costPerK.output);
|
|
804
|
-
const totalCost = inputCost + outputCost;
|
|
805
|
-
|
|
806
|
-
// Track cost
|
|
807
|
-
costTracker.record(usedMapping.providerId, usedMapping.model, result.usage.prompt_tokens, result.usage.completion_tokens);
|
|
808
|
-
|
|
809
|
-
const response = {
|
|
810
|
-
id: requestId,
|
|
811
|
-
object: "chat.completion",
|
|
812
|
-
created: Math.floor(Date.now() / 1000),
|
|
813
|
-
model: result.model,
|
|
814
|
-
choices: [
|
|
815
|
-
{
|
|
816
|
-
index: 0,
|
|
817
|
-
message: { role: "assistant" as const, content: result.content },
|
|
818
|
-
finish_reason: result.finish_reason,
|
|
819
|
-
},
|
|
820
|
-
],
|
|
821
|
-
usage: result.usage,
|
|
822
|
-
};
|
|
823
|
-
|
|
824
|
-
jsonResponse(res, 200, response);
|
|
825
|
-
|
|
826
|
-
logRequest({
|
|
827
|
-
id: requestId,
|
|
828
|
-
model,
|
|
829
|
-
resolvedProvider: usedMapping.providerId,
|
|
830
|
-
resolvedModel: usedMapping.model,
|
|
831
|
-
latencyMs,
|
|
832
|
-
tokensIn: result.usage.prompt_tokens,
|
|
833
|
-
tokensOut: result.usage.completion_tokens,
|
|
834
|
-
cost: totalCost,
|
|
835
|
-
status: "success",
|
|
836
|
-
timestamp: Date.now(),
|
|
837
|
-
});
|
|
838
|
-
|
|
839
|
-
console.log(
|
|
840
|
-
`[a3m-proxy] ${requestId} model=${model}→${usedMapping.providerId}/${usedMapping.model} latency=${latencyMs}ms tokens=${result.usage.total_tokens} cost=$${totalCost.toFixed(6)}`
|
|
841
|
-
);
|
|
842
|
-
} catch (err: any) {
|
|
843
|
-
const latencyMs = Date.now() - startTime;
|
|
844
|
-
|
|
845
|
-
logRequest({
|
|
846
|
-
id: requestId,
|
|
847
|
-
model,
|
|
848
|
-
resolvedProvider: mapping.providerId,
|
|
849
|
-
resolvedModel: mapping.model,
|
|
850
|
-
latencyMs,
|
|
851
|
-
tokensIn: 0,
|
|
852
|
-
tokensOut: 0,
|
|
853
|
-
cost: 0,
|
|
854
|
-
status: "error",
|
|
855
|
-
error: err.message,
|
|
856
|
-
timestamp: Date.now(),
|
|
857
|
-
});
|
|
858
|
-
|
|
859
|
-
console.error(
|
|
860
|
-
`[a3m-proxy] ${requestId} ERROR model=${model}→${mapping.providerId}/${mapping.model} latency=${latencyMs}ms error=${err.message}`
|
|
861
|
-
);
|
|
862
|
-
|
|
863
|
-
if (!res.headersSent) {
|
|
864
|
-
errorResponse(res, 502, err.message, "upstream_error");
|
|
865
|
-
}
|
|
866
|
-
}
|
|
867
|
-
}
|
|
868
|
-
}
|
|
869
|
-
|
|
870
|
-
/**
|
|
871
|
-
* Handle POST /v1/completions
|
|
872
|
-
*/
|
|
873
|
-
async function handleCompletions(
|
|
874
|
-
req: http.IncomingMessage,
|
|
875
|
-
res: http.ServerResponse
|
|
876
|
-
): Promise<void> {
|
|
877
|
-
const body = await readBody(req);
|
|
878
|
-
let request: CompletionRequest;
|
|
879
|
-
|
|
880
|
-
try {
|
|
881
|
-
request = JSON.parse(body);
|
|
882
|
-
} catch {
|
|
883
|
-
errorResponse(res, 400, "Invalid JSON in request body", "invalid_request_error");
|
|
884
|
-
return;
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
// Convert prompt to messages format
|
|
888
|
-
const prompts = Array.isArray(request.prompt) ? request.prompt : [request.prompt || ""];
|
|
889
|
-
const messages: ChatMessage[] = prompts.map((p) => ({ role: "user" as const, content: p }));
|
|
890
|
-
|
|
891
|
-
const model = request.model || "auto";
|
|
892
|
-
const stream = request.stream || false;
|
|
893
|
-
const requestId = generateId();
|
|
894
|
-
const promptForRouting = prompts.join(" ");
|
|
895
|
-
|
|
896
|
-
const mapping = resolveModel(model, promptForRouting);
|
|
897
|
-
if (!mapping) {
|
|
898
|
-
errorResponse(res, 503, `No provider available for model "${model}".`, "server_error");
|
|
899
|
-
return;
|
|
900
|
-
}
|
|
901
|
-
|
|
902
|
-
const startTime = Date.now();
|
|
903
|
-
|
|
904
|
-
if (stream) {
|
|
905
|
-
await streamProviderResponse(
|
|
906
|
-
res, mapping, messages,
|
|
907
|
-
{ temperature: request.temperature, max_tokens: request.max_tokens, stop: request.stop },
|
|
908
|
-
requestId
|
|
909
|
-
);
|
|
910
|
-
} else {
|
|
911
|
-
try {
|
|
912
|
-
const { result, mapping: usedMapping } = await callWithFallback(
|
|
913
|
-
model, messages,
|
|
914
|
-
{ temperature: request.temperature, max_tokens: request.max_tokens, stop: request.stop },
|
|
915
|
-
promptForRouting
|
|
916
|
-
);
|
|
917
|
-
|
|
918
|
-
const latencyMs = Date.now() - startTime;
|
|
919
|
-
|
|
920
|
-
const response = {
|
|
921
|
-
id: requestId,
|
|
922
|
-
object: "text_completion",
|
|
923
|
-
created: Math.floor(Date.now() / 1000),
|
|
924
|
-
model: result.model,
|
|
925
|
-
choices: [
|
|
926
|
-
{
|
|
927
|
-
text: result.content,
|
|
928
|
-
index: 0,
|
|
929
|
-
finish_reason: result.finish_reason,
|
|
930
|
-
},
|
|
931
|
-
],
|
|
932
|
-
usage: result.usage,
|
|
933
|
-
};
|
|
934
|
-
|
|
935
|
-
jsonResponse(res, 200, response);
|
|
936
|
-
|
|
937
|
-
costTracker.record(usedMapping.providerId, usedMapping.model, result.usage.prompt_tokens, result.usage.completion_tokens);
|
|
938
|
-
|
|
939
|
-
console.log(
|
|
940
|
-
`[a3m-proxy] ${requestId} completion model=${model}→${usedMapping.providerId}/${usedMapping.model} latency=${latencyMs}ms`
|
|
941
|
-
);
|
|
942
|
-
} catch (err: any) {
|
|
943
|
-
if (!res.headersSent) {
|
|
944
|
-
errorResponse(res, 502, err.message, "upstream_error");
|
|
945
|
-
}
|
|
946
|
-
}
|
|
947
|
-
}
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
/**
|
|
951
|
-
* Handle GET /v1/models
|
|
952
|
-
*/
|
|
953
|
-
function handleModels(res: http.ServerResponse): void {
|
|
954
|
-
const models = listAvailableModels();
|
|
955
|
-
jsonResponse(res, 200, {
|
|
956
|
-
object: "list",
|
|
957
|
-
data: models,
|
|
958
|
-
});
|
|
959
|
-
}
|
|
960
|
-
|
|
961
|
-
/**
|
|
962
|
-
* Handle GET /health
|
|
963
|
-
*/
|
|
964
|
-
async function handleHealth(res: http.ServerResponse): Promise<void> {
|
|
965
|
-
const available = getAvailableProviders();
|
|
966
|
-
const providerStatus: Record<string, any> = {};
|
|
967
|
-
let healthyCount = 0;
|
|
968
|
-
|
|
969
|
-
// Quick health check — just report if API keys exist
|
|
970
|
-
for (const [id, provider] of Object.entries(available)) {
|
|
971
|
-
const hasKey = !!provider.apiKey;
|
|
972
|
-
const isAvailable = provider.type !== "api" || hasKey;
|
|
973
|
-
providerStatus[id] = {
|
|
974
|
-
name: provider.name || id,
|
|
975
|
-
type: provider.type,
|
|
976
|
-
models: provider.models?.length || 0,
|
|
977
|
-
available: isAvailable,
|
|
978
|
-
};
|
|
979
|
-
if (isAvailable) healthyCount++;
|
|
980
|
-
}
|
|
981
|
-
|
|
982
|
-
const costSummary = costTracker.getSummary();
|
|
983
|
-
|
|
984
|
-
jsonResponse(res, 200, {
|
|
985
|
-
status: "ok",
|
|
986
|
-
version: "2.0.0",
|
|
987
|
-
providers: {
|
|
988
|
-
total: Object.keys(available).length,
|
|
989
|
-
healthy: healthyCount,
|
|
990
|
-
details: providerStatus,
|
|
991
|
-
},
|
|
992
|
-
cost: {
|
|
993
|
-
total: costSummary.total_cost,
|
|
994
|
-
requests: costSummary.request_count,
|
|
995
|
-
},
|
|
996
|
-
uptime: process.uptime(),
|
|
997
|
-
recentRequests: requestLogs.slice(-20),
|
|
998
|
-
});
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
|
-
/**
|
|
1002
|
-
* Log a request for the /health endpoint.
|
|
1003
|
-
*/
|
|
1004
|
-
function logRequest(entry: RequestLog): void {
|
|
1005
|
-
requestLogs.push(entry);
|
|
1006
|
-
// Keep only the last 1000 entries
|
|
1007
|
-
if (requestLogs.length > 1000) {
|
|
1008
|
-
requestLogs.splice(0, requestLogs.length - 1000);
|
|
1009
|
-
}
|
|
1010
|
-
}
|
|
1011
|
-
|
|
1012
708
|
// ============================================================
|
|
1013
709
|
// SERVER CREATION
|
|
1014
710
|
// ============================================================
|
|
1015
711
|
|
|
1016
712
|
/**
|
|
1017
|
-
* Create and start the proxy server.
|
|
713
|
+
* Create and start the A3M Router proxy server.
|
|
714
|
+
*
|
|
715
|
+
* Uses a route table pattern (router.ts) for pluggable endpoint registration.
|
|
716
|
+
* To add a new endpoint:
|
|
717
|
+
* 1. Create src/server/handlers/yourHandler.ts
|
|
718
|
+
* 2. Import and register: registerRoute('GET', /^\/path$/, handleYourPath);
|
|
1018
719
|
*
|
|
1019
720
|
* @param port - Port to listen on (default: 8787, env: PORT)
|
|
1020
721
|
* @returns The http.Server instance
|
|
1021
722
|
*/
|
|
1022
723
|
export function createProxyServer(port?: number): http.Server {
|
|
1023
|
-
const listenPort = port || parseInt(process.env.PORT ||
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
}
|
|
1040
|
-
|
|
724
|
+
const listenPort = port || parseInt(process.env.PORT || '8787', 10);
|
|
725
|
+
|
|
726
|
+
// ── Register all routes ────────────────────────────────────────────────
|
|
727
|
+
// Routes are matched in registration order.
|
|
728
|
+
// Adding a new endpoint = add 1 import + 1 registerRoute() call.
|
|
729
|
+
registerRoute('POST', /^\/v1\/chat\/completions$/, handleChatCompletions, 'POST /v1/chat/completions');
|
|
730
|
+
registerRoute('POST', /^\/v1\/completions$/, handleCompletions, 'POST /v1/completions');
|
|
731
|
+
registerRoute('POST', /^\/v1\/embeddings$/, handleEmbeddings, 'POST /v1/embeddings');
|
|
732
|
+
registerRoute('GET', /^\/v1\/models$/, handleModels, 'GET /v1/models');
|
|
733
|
+
registerRoute('GET', /^\/health$/, handleHealth, 'GET /health');
|
|
734
|
+
registerRoute('GET', /^\/metrics$/, handleMetrics, 'GET /metrics');
|
|
735
|
+
|
|
736
|
+
// ── Create request handler ─────────────────────────────────────────────
|
|
737
|
+
const requestHandler = createRequestHandler();
|
|
738
|
+
|
|
739
|
+
const server = http.createServer(async (req, res) => {
|
|
1041
740
|
try {
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
}
|
|
1047
|
-
|
|
1048
|
-
// Route: POST /v1/completions
|
|
1049
|
-
if (method === "POST" && url === "/v1/completions") {
|
|
1050
|
-
await handleCompletions(req, res);
|
|
1051
|
-
return;
|
|
1052
|
-
}
|
|
1053
|
-
|
|
1054
|
-
// Route: GET /v1/models
|
|
1055
|
-
if (method === "GET" && url === "/v1/models") {
|
|
1056
|
-
handleModels(res);
|
|
1057
|
-
return;
|
|
1058
|
-
}
|
|
1059
|
-
|
|
1060
|
-
// Route: GET /health
|
|
1061
|
-
if (method === "GET" && url === "/health") {
|
|
1062
|
-
await handleHealth(res);
|
|
1063
|
-
return;
|
|
1064
|
-
}
|
|
1065
|
-
|
|
1066
|
-
// 404 for everything else
|
|
1067
|
-
errorResponse(res, 404, `Not found: ${method} ${url}`, "not_found");
|
|
1068
|
-
} catch (err: any) {
|
|
1069
|
-
console.error(`[a3m-proxy] Unhandled error: ${err.message}`);
|
|
741
|
+
await requestHandler(req, res);
|
|
742
|
+
} catch (err: unknown) {
|
|
743
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
744
|
+
console.error(`[a3m-router] Unhandled error: ${message}`);
|
|
1070
745
|
if (!res.headersSent) {
|
|
1071
|
-
|
|
746
|
+
res.writeHead(500, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
747
|
+
res.end(JSON.stringify({ error: { message, type: 'server_error' } }));
|
|
1072
748
|
}
|
|
1073
749
|
}
|
|
1074
750
|
});
|
|
1075
751
|
|
|
1076
752
|
server.listen(listenPort, () => {
|
|
1077
|
-
console.log(
|
|
1078
|
-
console.log(
|
|
1079
|
-
console.log(
|
|
753
|
+
console.log('');
|
|
754
|
+
console.log(' A3M Router Proxy Server');
|
|
755
|
+
console.log(' ─────────────────────────────────────────');
|
|
1080
756
|
console.log(` Listening: http://localhost:${listenPort}`);
|
|
1081
|
-
console.log(
|
|
1082
|
-
console.log(
|
|
1083
|
-
console.log(
|
|
1084
|
-
console.log(
|
|
1085
|
-
console.log(
|
|
1086
|
-
console.log(
|
|
1087
|
-
console.log(
|
|
1088
|
-
console.log(
|
|
1089
|
-
console.log(
|
|
1090
|
-
console.log(
|
|
1091
|
-
console.log(
|
|
757
|
+
console.log(' Endpoints:');
|
|
758
|
+
console.log(' POST /v1/chat/completions (OpenAI chat)');
|
|
759
|
+
console.log(' POST /v1/completions (OpenAI completions)');
|
|
760
|
+
console.log(' POST /v1/embeddings (OpenAI embeddings)');
|
|
761
|
+
console.log(' GET /v1/models (List models)');
|
|
762
|
+
console.log(' GET /health (Health check)');
|
|
763
|
+
console.log(' GET /metrics (Prometheus metrics)');
|
|
764
|
+
console.log('');
|
|
765
|
+
console.log(' Example:');
|
|
766
|
+
console.log(" curl http://localhost:" + listenPort + "/v1/chat/completions \\'");
|
|
767
|
+
console.log(' -H "Content-Type: application/json" \'');
|
|
768
|
+
console.log(" -d '{\"model\":\"auto\",\"messages\":[{\"role\":\"user\",\"content\":\"Hello\"}]}'");
|
|
769
|
+
console.log('');
|
|
1092
770
|
});
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
console.error(`[a3m-proxy] Port ${listenPort} is already in use. Use --port or PORT env var.`);
|
|
771
|
+
server.on('error', (err: NodeJS.ErrnoException) => {
|
|
772
|
+
if (err.code === 'EADDRINUSE') {
|
|
773
|
+
console.error(`[a3m-router] Port ${listenPort} is already in use. Use --port or PORT env var.`);
|
|
1097
774
|
process.exit(1);
|
|
1098
775
|
} else {
|
|
1099
|
-
console.error(`[a3m-
|
|
776
|
+
console.error(`[a3m-router] Server error: ${err.message}`);
|
|
1100
777
|
}
|
|
1101
778
|
});
|
|
1102
779
|
|
|
1103
780
|
// Graceful shutdown
|
|
1104
781
|
const shutdown = () => {
|
|
1105
|
-
console.log(
|
|
782
|
+
console.log('\n[a3m-router] Shutting down...');
|
|
1106
783
|
server.close(() => {
|
|
1107
|
-
console.log(
|
|
784
|
+
console.log('[a3m-router] Server closed.');
|
|
1108
785
|
process.exit(0);
|
|
1109
786
|
});
|
|
1110
|
-
// Force close after 5s
|
|
1111
787
|
setTimeout(() => {
|
|
1112
|
-
console.error(
|
|
788
|
+
console.error('[a3m-router] Forced shutdown after timeout.');
|
|
1113
789
|
process.exit(1);
|
|
1114
790
|
}, 5000);
|
|
1115
791
|
};
|
|
1116
792
|
|
|
1117
|
-
process.on(
|
|
1118
|
-
process.on(
|
|
793
|
+
process.on('SIGINT', shutdown);
|
|
794
|
+
process.on('SIGTERM', shutdown);
|
|
1119
795
|
|
|
1120
796
|
return server;
|
|
1121
797
|
}
|
|
1122
|
-
|
|
1123
798
|
// ============================================================
|
|
1124
799
|
// EXPORTS
|
|
1125
800
|
// ============================================================
|
|
1126
801
|
|
|
1127
|
-
export
|
|
802
|
+
// Re-export shared state and types for backward compatibility
|
|
803
|
+
export { costTracker, requestLogs } from './state';
|
|
804
|
+
export type { RequestLog } from './state';
|
|
1128
805
|
export default createProxyServer;
|