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
@@ -14,10 +14,17 @@
14
14
  * Run: npx a3m-router serve [--port 8787]
15
15
  */
16
16
 
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 } from "../cost/costTracker";
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
- interface RequestLog {
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 || "8787", 10);
1024
-
1025
- const server = http.createServer(async (req: http.IncomingMessage, res: http.ServerResponse) => {
1026
- const method = req.method || "GET";
1027
- const url = req.url || "/";
1028
-
1029
- // CORS preflight
1030
- if (method === "OPTIONS") {
1031
- res.writeHead(204, {
1032
- "Access-Control-Allow-Origin": "*",
1033
- "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
1034
- "Access-Control-Allow-Headers": "Content-Type, Authorization",
1035
- "Access-Control-Max-Age": "86400",
1036
- });
1037
- res.end();
1038
- return;
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
- // Route: POST /v1/chat/completions
1043
- if (method === "POST" && url === "/v1/chat/completions") {
1044
- await handleChatCompletions(req, res);
1045
- return;
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
- errorResponse(res, 500, err.message);
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(` A3M Router Proxy Server`);
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(` Endpoints:`);
1082
- console.log(` POST /v1/chat/completions (OpenAI chat)`);
1083
- console.log(` POST /v1/completions (OpenAI completions)`);
1084
- console.log(` GET /v1/models (List models)`);
1085
- console.log(` GET /health (Health check)`);
1086
- console.log(``);
1087
- console.log(` Example:`);
1088
- console.log(` curl http://localhost:${listenPort}/v1/chat/completions \\`);
1089
- console.log(` -H "Content-Type: application/json" \\`);
1090
- console.log(` -d '{"model":"auto","messages":[{"role":"user","content":"Hello"}]}'`);
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
- server.on("error", (err: any) => {
1095
- if (err.code === "EADDRINUSE") {
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-proxy] Server error: ${err.message}`);
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(`\n[a3m-proxy] Shutting down...`);
782
+ console.log('\n[a3m-router] Shutting down...');
1106
783
  server.close(() => {
1107
- console.log(`[a3m-proxy] Server closed.`);
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(`[a3m-proxy] Forced shutdown after timeout.`);
788
+ console.error('[a3m-router] Forced shutdown after timeout.');
1113
789
  process.exit(1);
1114
790
  }, 5000);
1115
791
  };
1116
792
 
1117
- process.on("SIGINT", shutdown);
1118
- process.on("SIGTERM", shutdown);
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 { CostTracker, costTracker, requestLogs };
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;