@axiom-lattice/gateway 2.1.12 → 2.1.13

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/src/config.ts ADDED
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Configuration service
3
+ * Manages environment variables and supports dynamic updates
4
+ */
5
+
6
+ export interface GatewayConfig {
7
+ port?: number;
8
+ queueServiceType?: string;
9
+ redisUrl?: string;
10
+ redisPassword?: string;
11
+ queueName?: string;
12
+ [key: string]: any; // Allow additional config keys
13
+ }
14
+
15
+ /**
16
+ * Get configuration from environment variables
17
+ * Supports dynamic updates via updateConfig method
18
+ */
19
+ class ConfigService {
20
+ private config: GatewayConfig;
21
+
22
+ constructor() {
23
+ this.config = this.loadFromEnv();
24
+ }
25
+
26
+ /**
27
+ * Load configuration from environment variables
28
+ */
29
+ private loadFromEnv(): GatewayConfig {
30
+ return {
31
+ port: process.env.PORT ? Number(process.env.PORT) : undefined,
32
+ queueServiceType: process.env.QUEUE_SERVICE_TYPE,
33
+ redisUrl: process.env.REDIS_URL,
34
+ redisPassword: process.env.REDIS_PASSWORD,
35
+ queueName: process.env.QUEUE_NAME,
36
+ };
37
+ }
38
+
39
+ /**
40
+ * Update configuration from JSON object
41
+ * This will update both the internal config and process.env
42
+ */
43
+ updateConfig(jsonConfig: Record<string, any>): void {
44
+ // Update process.env for all provided keys
45
+ for (const [key, value] of Object.entries(jsonConfig)) {
46
+ if (value !== null && value !== undefined) {
47
+ // Convert nested objects to environment variable format
48
+ if (typeof value === "object" && !Array.isArray(value)) {
49
+ // Handle nested objects like supabase: { url: "...", key: "..." }
50
+ for (const [nestedKey, nestedValue] of Object.entries(value)) {
51
+ const envKey = `${key.toUpperCase()}_${nestedKey.toUpperCase()}`;
52
+ process.env[envKey] = String(nestedValue);
53
+ }
54
+ } else {
55
+ // Handle flat keys
56
+ process.env[key.toUpperCase()] = String(value);
57
+ }
58
+ }
59
+ }
60
+
61
+ // Reload config from updated environment variables
62
+ this.config = this.loadFromEnv();
63
+
64
+ // Deep merge the JSON config into our config object
65
+ this.config = this.deepMerge(this.config, jsonConfig);
66
+ }
67
+
68
+ /**
69
+ * Deep merge two objects
70
+ */
71
+ private deepMerge(target: any, source: any): any {
72
+ const output = { ...target };
73
+ if (this.isObject(target) && this.isObject(source)) {
74
+ Object.keys(source).forEach((key) => {
75
+ if (this.isObject(source[key])) {
76
+ if (!(key in target)) {
77
+ Object.assign(output, { [key]: source[key] });
78
+ } else {
79
+ output[key] = this.deepMerge(target[key], source[key]);
80
+ }
81
+ } else {
82
+ Object.assign(output, { [key]: source[key] });
83
+ }
84
+ });
85
+ }
86
+ return output;
87
+ }
88
+
89
+ /**
90
+ * Check if value is a plain object
91
+ */
92
+ private isObject(item: any): boolean {
93
+ return item && typeof item === "object" && !Array.isArray(item);
94
+ }
95
+
96
+ /**
97
+ * Get current configuration
98
+ */
99
+ getConfig(): GatewayConfig {
100
+ return { ...this.config };
101
+ }
102
+ }
103
+
104
+ // Export singleton instance
105
+ export const configService = new ConfigService();
106
+
107
+ // Export config getter for backward compatibility
108
+ export const config = {
109
+ get port() {
110
+ return configService.getConfig().port;
111
+ },
112
+ get queueServiceType() {
113
+ return configService.getConfig().queueServiceType;
114
+ },
115
+ get redisUrl() {
116
+ return configService.getConfig().redisUrl;
117
+ },
118
+ get redisPassword() {
119
+ return configService.getConfig().redisPassword;
120
+ },
121
+ get queueName() {
122
+ return configService.getConfig().queueName;
123
+ },
124
+ };
@@ -0,0 +1,126 @@
1
+ import { FastifyRequest, FastifyReply } from "fastify";
2
+ import { configService } from "../config";
3
+ import {
4
+ setQueueServiceType,
5
+ QueueServiceType,
6
+ } from "../services/queue_service";
7
+
8
+ /**
9
+ * Configuration Controller
10
+ * Handles configuration updates from frontend
11
+ */
12
+
13
+ interface UpdateConfigRequest {
14
+ Body: {
15
+ config: Record<string, any>;
16
+ };
17
+ }
18
+
19
+ /**
20
+ * Update gateway configuration
21
+ * Accepts JSON config and loads it into environment variables
22
+ */
23
+ export async function updateConfig(
24
+ request: FastifyRequest<UpdateConfigRequest>,
25
+ reply: FastifyReply
26
+ ) {
27
+ try {
28
+ const { config: jsonConfig } = request.body;
29
+
30
+ if (!jsonConfig || typeof jsonConfig !== "object") {
31
+ return reply.status(400).send({
32
+ success: false,
33
+ error: "Invalid configuration: config must be an object",
34
+ });
35
+ }
36
+
37
+ // Update configuration service
38
+ configService.updateConfig(jsonConfig);
39
+
40
+ const warnings: string[] = [];
41
+ const requiresRestart: string[] = [];
42
+
43
+ // Check if port is being changed (requires restart)
44
+ if (jsonConfig.port !== undefined) {
45
+ requiresRestart.push("PORT");
46
+ warnings.push("Port change requires server restart to take effect");
47
+ }
48
+
49
+ // If queue service type is being updated, reconfigure the queue service
50
+ if (jsonConfig.queueServiceType) {
51
+ setQueueServiceType(jsonConfig.queueServiceType as QueueServiceType);
52
+ }
53
+
54
+ // If Redis configuration is being updated and queue service is Redis, reconfigure
55
+ if (
56
+ (jsonConfig.redisUrl || jsonConfig.redisPassword) &&
57
+ (process.env.QUEUE_SERVICE_TYPE === "redis" ||
58
+ jsonConfig.queueServiceType === "redis")
59
+ ) {
60
+ // Reconfigure queue service to pick up new Redis settings
61
+ const currentType =
62
+ (jsonConfig.queueServiceType as QueueServiceType) ||
63
+ (process.env.QUEUE_SERVICE_TYPE as QueueServiceType) ||
64
+ "memory";
65
+ if (currentType === "redis") {
66
+ setQueueServiceType("redis");
67
+ }
68
+ }
69
+
70
+ // Get updated config (without sensitive data)
71
+ const updatedConfig = configService.getConfig();
72
+ const safeConfig = {
73
+ ...updatedConfig,
74
+ redisPassword: updatedConfig.redisPassword
75
+ ? "***"
76
+ : updatedConfig.redisPassword,
77
+ };
78
+
79
+ return reply.send({
80
+ success: true,
81
+ message: "Configuration updated successfully",
82
+ data: safeConfig,
83
+ warnings: warnings.length > 0 ? warnings : undefined,
84
+ requiresRestart: requiresRestart.length > 0 ? requiresRestart : undefined,
85
+ });
86
+ } catch (error: any) {
87
+ console.error("Failed to update configuration", {
88
+ error: error.message,
89
+ stack: error.stack,
90
+ });
91
+ return reply.status(500).send({
92
+ success: false,
93
+ error: error.message || "Failed to update configuration",
94
+ });
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Get current gateway configuration
100
+ * Returns current configuration (with sensitive data masked)
101
+ */
102
+ export async function getConfig(request: FastifyRequest, reply: FastifyReply) {
103
+ try {
104
+ const currentConfig = configService.getConfig();
105
+ const safeConfig = {
106
+ ...currentConfig,
107
+ redisPassword: currentConfig.redisPassword
108
+ ? "***"
109
+ : currentConfig.redisPassword,
110
+ };
111
+
112
+ return reply.send({
113
+ success: true,
114
+ data: safeConfig,
115
+ });
116
+ } catch (error: any) {
117
+ console.error("Failed to get configuration", {
118
+ error: error.message,
119
+ stack: error.stack,
120
+ });
121
+ return reply.status(500).send({
122
+ success: false,
123
+ error: error.message || "Failed to get configuration",
124
+ });
125
+ }
126
+ }
@@ -0,0 +1,152 @@
1
+ import { FastifyRequest, FastifyReply } from "fastify";
2
+ import { registerModelLattice, modelLatticeManager } from "@axiom-lattice/core";
3
+ import type { LLMConfig } from "@axiom-lattice/protocols";
4
+
5
+ /**
6
+ * Models Controller
7
+ * Handles model lattice registration and management
8
+ */
9
+
10
+ interface ModelConfig {
11
+ key: string;
12
+ model: string;
13
+ provider: "azure" | "openai" | "deepseek" | "siliconcloud" | "volcengine";
14
+ streaming?: boolean;
15
+ apiKey?: string;
16
+ baseURL?: string;
17
+ maxTokens?: number;
18
+ temperature?: number;
19
+ timeout?: number;
20
+ maxRetries?: number;
21
+ }
22
+
23
+ interface UpdateModelsRequest {
24
+ Body: {
25
+ models: ModelConfig[];
26
+ };
27
+ }
28
+
29
+ /**
30
+ * Get all registered models
31
+ */
32
+ export async function getModels(request: FastifyRequest, reply: FastifyReply) {
33
+ try {
34
+ const allLattices = modelLatticeManager.getAllLattices();
35
+ const models = allLattices.map((lattice) => {
36
+ // Extract config from the lattice client
37
+ // Note: This is a simplified approach - you may need to adjust based on actual implementation
38
+ const config = (lattice.client as any).config || {};
39
+ return {
40
+ key: lattice.key,
41
+ model: config.model || "",
42
+ provider: config.provider || "openai",
43
+ streaming: config.streaming || false,
44
+ apiKey: config.apiKey || "",
45
+ baseURL: config.baseURL || "",
46
+ maxTokens: config.maxTokens,
47
+ temperature: config.temperature,
48
+ timeout: config.timeout,
49
+ maxRetries: config.maxRetries,
50
+ };
51
+ });
52
+
53
+ return reply.send({
54
+ success: true,
55
+ data: models,
56
+ });
57
+ } catch (error: any) {
58
+ console.error("Failed to get models", {
59
+ error: error.message,
60
+ stack: error.stack,
61
+ });
62
+ return reply.status(500).send({
63
+ success: false,
64
+ error: error.message || "Failed to get models",
65
+ });
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Update models configuration
71
+ * Registers or updates model lattices
72
+ */
73
+ export async function updateModels(
74
+ request: FastifyRequest<UpdateModelsRequest>,
75
+ reply: FastifyReply
76
+ ) {
77
+ try {
78
+ const { models } = request.body;
79
+
80
+ if (!models || !Array.isArray(models)) {
81
+ return reply.status(400).send({
82
+ success: false,
83
+ error: "Invalid request: models must be an array",
84
+ });
85
+ }
86
+
87
+ const registeredModels: string[] = [];
88
+ const errors: string[] = [];
89
+
90
+ for (const modelConfig of models) {
91
+ if (!modelConfig.key || !modelConfig.model || !modelConfig.provider) {
92
+ errors.push(
93
+ `Model configuration is incomplete: key, model, and provider are required`
94
+ );
95
+ continue;
96
+ }
97
+
98
+ try {
99
+ // Remove existing model with the same key if it exists
100
+ if (modelLatticeManager.hasLattice(modelConfig.key)) {
101
+ modelLatticeManager.removeLattice(modelConfig.key);
102
+ }
103
+
104
+ // Convert to LLMConfig format
105
+ const llmConfig: LLMConfig = {
106
+ provider: modelConfig.provider,
107
+ model: modelConfig.model,
108
+ streaming: modelConfig.streaming ?? false,
109
+ apiKey: modelConfig.apiKey,
110
+ baseURL: modelConfig.baseURL,
111
+ maxTokens: modelConfig.maxTokens,
112
+ temperature: modelConfig.temperature,
113
+ timeout: modelConfig.timeout,
114
+ maxRetries: modelConfig.maxRetries,
115
+ };
116
+
117
+ // Register the new model lattice
118
+ registerModelLattice(modelConfig.key, llmConfig);
119
+ registeredModels.push(modelConfig.key);
120
+ } catch (error: any) {
121
+ errors.push(
122
+ `Failed to register model ${modelConfig.key}: ${error.message}`
123
+ );
124
+ }
125
+ }
126
+
127
+ if (errors.length > 0 && registeredModels.length === 0) {
128
+ return reply.status(400).send({
129
+ success: false,
130
+ error: errors.join("; "),
131
+ });
132
+ }
133
+
134
+ return reply.send({
135
+ success: true,
136
+ message: `Successfully registered ${registeredModels.length} model(s)`,
137
+ data: {
138
+ registered: registeredModels,
139
+ errors: errors.length > 0 ? errors : undefined,
140
+ },
141
+ });
142
+ } catch (error: any) {
143
+ console.error("Failed to update models", {
144
+ error: error.message,
145
+ stack: error.stack,
146
+ });
147
+ return reply.status(500).send({
148
+ success: false,
149
+ error: error.message || "Failed to update models",
150
+ });
151
+ }
152
+ }
@@ -5,6 +5,8 @@ import * as memoryController from "../controllers/memory";
5
5
  import * as graphController from "../controllers/assistant";
6
6
  import * as agentTaskController from "../controllers/agent_task";
7
7
  import * as threadsController from "../controllers/threads";
8
+ import * as configController from "../controllers/config";
9
+ import * as modelsController from "../controllers/models";
8
10
  import {
9
11
  createRunSchema,
10
12
  getAllMemoryItemsSchema,
@@ -16,6 +18,8 @@ import {
16
18
  getAgentGraphSchema,
17
19
  resumeStreamSchema,
18
20
  triggerAgentTaskSchema,
21
+ updateConfigSchema,
22
+ getConfigSchema,
19
23
  } from "../schemas";
20
24
 
21
25
  export const registerLatticeRoutes = (app: FastifyInstance): void => {
@@ -160,4 +164,26 @@ export const registerLatticeRoutes = (app: FastifyInstance): void => {
160
164
  "/api/assistants/:assistantId/threads/:threadId",
161
165
  threadsController.deleteThread
162
166
  );
167
+
168
+ // Configuration routes
169
+ app.get(
170
+ "/api/config",
171
+ { schema: getConfigSchema },
172
+ configController.getConfig
173
+ );
174
+
175
+ app.put<{
176
+ Body: { config: Record<string, any> };
177
+ }>(
178
+ "/api/config",
179
+ { schema: updateConfigSchema },
180
+ configController.updateConfig
181
+ );
182
+
183
+ // Models routes
184
+ app.get("/api/models", modelsController.getModels);
185
+
186
+ app.put<{
187
+ Body: { models: any[] };
188
+ }>("/api/models", modelsController.updateModels);
163
189
  };
@@ -245,3 +245,77 @@ export const triggerAgentTaskSchema: FastifySchema = {
245
245
  },
246
246
  },
247
247
  };
248
+
249
+ // Configuration Schemas
250
+ export const updateConfigSchema: FastifySchema = {
251
+ description: "Update gateway configuration",
252
+ tags: ["Configuration"],
253
+ summary: "Update Configuration",
254
+ body: {
255
+ type: "object",
256
+ properties: {
257
+ config: {
258
+ type: "object",
259
+ description: "Configuration object to update",
260
+ properties: {
261
+ port: { type: "number", description: "Server port" },
262
+ queueServiceType: {
263
+ type: "string",
264
+ enum: ["memory", "redis"],
265
+ description: "Queue service type",
266
+ },
267
+ redisUrl: { type: "string", description: "Redis URL" },
268
+ redisPassword: { type: "string", description: "Redis password" },
269
+ queueName: { type: "string", description: "Queue name" },
270
+ },
271
+ },
272
+ },
273
+ required: ["config"],
274
+ },
275
+ response: {
276
+ 200: {
277
+ type: "object",
278
+ properties: {
279
+ success: { type: "boolean" },
280
+ message: { type: "string" },
281
+ data: { type: "object" },
282
+ },
283
+ },
284
+ 400: {
285
+ type: "object",
286
+ properties: {
287
+ success: { type: "boolean" },
288
+ error: { type: "string" },
289
+ },
290
+ },
291
+ 500: {
292
+ type: "object",
293
+ properties: {
294
+ success: { type: "boolean" },
295
+ error: { type: "string" },
296
+ },
297
+ },
298
+ },
299
+ };
300
+
301
+ export const getConfigSchema: FastifySchema = {
302
+ description: "Get current gateway configuration",
303
+ tags: ["Configuration"],
304
+ summary: "Get Configuration",
305
+ response: {
306
+ 200: {
307
+ type: "object",
308
+ properties: {
309
+ success: { type: "boolean" },
310
+ data: { type: "object" },
311
+ },
312
+ },
313
+ 500: {
314
+ type: "object",
315
+ properties: {
316
+ success: { type: "boolean" },
317
+ error: { type: "string" },
318
+ },
319
+ },
320
+ },
321
+ };
@@ -1,15 +1,36 @@
1
- import { createClient } from "@supabase/supabase-js";
2
- import { config } from "../config";
1
+ import { createClient, SupabaseClient } from "@supabase/supabase-js";
3
2
  // import { Database } from "@/types/database.types";
4
3
  // import { Database as PgmqPublic } from "@/types/pgmq_public.types";
5
- // 创建Supabase客户端
6
- const supabaseClient = createClient(config.supabase.url, config.supabase.key);
7
4
 
8
- export default supabaseClient;
5
+ /**
6
+ * Get Supabase client
7
+ * This function reads from environment variables directly
8
+ */
9
+ const getSupabaseClient = (): SupabaseClient => {
10
+ const url = process.env.SUPABASE_URL;
11
+ const key = process.env.SUPABASE_KEY;
12
+ if (!url || !key) {
13
+ throw new Error("Supabase URL and Key must be configured");
14
+ }
15
+ return createClient(url, key);
16
+ };
17
+
18
+ // Default export - creates client using current config
19
+ // Note: Since config uses getters, this will read the latest config values
20
+ export default getSupabaseClient();
9
21
 
22
+ /**
23
+ * Create a Supabase client with tenant headers
24
+ * This function reads from environment variables directly
25
+ */
10
26
  export const createSupabaseClient = (headers: Record<string, string>) => {
11
27
  const currentTenantId = headers["x-tenant-id"];
12
- return createClient(config.supabase.url, config.supabase.key, {
28
+ const url = process.env.SUPABASE_URL;
29
+ const key = process.env.SUPABASE_KEY;
30
+ if (!url || !key) {
31
+ throw new Error("Supabase URL and Key must be configured");
32
+ }
33
+ return createClient(url, key, {
13
34
  global: {
14
35
  fetch: async (input, init) => {
15
36
  const headers = new Headers(init?.headers);
@@ -20,7 +41,20 @@ export const createSupabaseClient = (headers: Record<string, string>) => {
20
41
  });
21
42
  };
22
43
 
23
- export const supabaseQueueClient = createClient(
24
- config.supabase.url,
25
- config.supabase.key
26
- );
44
+ /**
45
+ * Get Supabase queue client
46
+ * This function reads from environment variables directly
47
+ */
48
+ export const getSupabaseQueueClient = (): SupabaseClient => {
49
+ const url = process.env.SUPABASE_URL;
50
+ const key = process.env.SUPABASE_KEY;
51
+ if (!url || !key) {
52
+ throw new Error("Supabase URL and Key must be configured");
53
+ }
54
+ return createClient(url, key);
55
+ };
56
+
57
+ // For backward compatibility, export a client that reads config dynamically
58
+ // Note: This creates a new client each time, but Supabase clients are lightweight
59
+ // and this ensures we always use the latest config
60
+ export const supabaseQueueClient = getSupabaseQueueClient();