@tonyclaw/llm-inspector 1.7.3 → 1.7.5

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.
@@ -31,82 +31,70 @@ type ProvidersStore = z.infer<typeof ProvidersStoreSchema>;
31
31
 
32
32
  // Using conf for storage - works in any Node.js environment without Electron
33
33
  // Note: conf stores data in plain JSON. For production, consider additional encryption.
34
+ const configPath = process.env["LLM_INSPECTOR_CONFIG_PATH"];
34
35
  export const store = new Conf<ProvidersStore>({
35
36
  projectName: "llm-inspector",
36
37
  defaults: {
37
38
  providers: [],
38
39
  },
40
+ ...(configPath !== undefined ? { path: configPath } : {}),
39
41
  });
40
42
 
41
43
  /**
42
- * Migrates existing provider configs to preserve both Anthropic and OpenAI URLs.
43
- * Old configs had anthropicBaseUrl/openaiBaseUrl, we now keep both.
44
+ * Migrates a single provider config to preserve both Anthropic and OpenAI URLs.
44
45
  */
45
- function migrateProviders(): void {
46
- const providers = store.get("providers", []);
47
- let migrated = false;
48
-
49
- const updated = providers.map((p) => {
50
- // Helper to safely get old URL properties using Object.getOwnPropertyDescriptor
51
- function getOldUrl(obj: unknown, key: string): string {
52
- if (obj !== null && typeof obj === "object") {
53
- const desc = Object.getOwnPropertyDescriptor(obj, key);
54
- if (desc !== undefined && typeof desc.value === "string") {
55
- return desc.value;
56
- }
57
- }
58
- return "";
59
- }
60
-
61
- const oldAnthropicBaseUrl = getOldUrl(p, "anthropicBaseUrl");
62
- const oldOpenaiBaseUrl = getOldUrl(p, "openaiBaseUrl");
63
- const currentAnthropicUrl = p.anthropicBaseUrl ?? "";
64
- const currentOpenaiUrl = p.openaiBaseUrl ?? "";
65
-
66
- // If already migrated (has format field and at least one URL), skip
67
- if (p.format !== undefined && (currentAnthropicUrl !== "" || currentOpenaiUrl !== "")) {
68
- return p;
69
- }
46
+ export function migrateProvider(p: ProviderConfig): ProviderConfig {
47
+ const currentAnthropicUrl = p.anthropicBaseUrl ?? "";
48
+ const currentOpenaiUrl = p.openaiBaseUrl ?? "";
49
+
50
+ // If already migrated (has at least one URL set), skip
51
+ // format may be undefined but if URL is set, it's already migrated
52
+ if (currentAnthropicUrl !== "" || currentOpenaiUrl !== "") {
53
+ return p;
54
+ }
70
55
 
71
- // Preserve both URLs
72
- const newAnthropicUrl = oldAnthropicBaseUrl !== "" ? oldAnthropicBaseUrl : currentAnthropicUrl;
73
- const newOpenaiUrl = oldOpenaiBaseUrl !== "" ? oldOpenaiBaseUrl : currentOpenaiUrl;
74
-
75
- // Determine primary format based on which URL is set
76
- let format: "anthropic" | "openai" | undefined;
77
- let baseUrl: string | undefined;
78
-
79
- if (newAnthropicUrl !== "" && newOpenaiUrl !== "") {
80
- // Both URLs set - prefer anthropic as default, use baseUrl for backward compat
81
- format = p.format ?? "anthropic";
82
- baseUrl = p.baseUrl !== undefined && p.baseUrl !== "" ? p.baseUrl : newAnthropicUrl;
83
- } else if (newOpenaiUrl !== "") {
84
- format = "openai";
85
- baseUrl = newOpenaiUrl;
86
- } else if (newAnthropicUrl !== "") {
87
- format = "anthropic";
88
- baseUrl = newAnthropicUrl;
89
- } else if (p.format !== undefined && p.baseUrl !== undefined && p.baseUrl !== "") {
90
- // Only baseUrl is set (legacy config) - migrate based on format
91
- format = p.format;
92
- baseUrl = p.baseUrl;
93
- if (format === "openai") {
94
- return { ...p, format, baseUrl, anthropicBaseUrl: "", openaiBaseUrl: p.baseUrl };
95
- } else {
96
- return { ...p, format, baseUrl, anthropicBaseUrl: p.baseUrl, openaiBaseUrl: "" };
97
- }
56
+ // Determine primary format based on which URL is set
57
+ let format: "anthropic" | "openai" | undefined;
58
+ let baseUrl: string | undefined;
59
+
60
+ if (currentAnthropicUrl !== "" && currentOpenaiUrl !== "") {
61
+ // Both URLs set - prefer anthropic as default, use baseUrl for backward compat
62
+ format = p.format ?? "anthropic";
63
+ baseUrl = p.baseUrl !== undefined && p.baseUrl !== "" ? p.baseUrl : currentAnthropicUrl;
64
+ } else if (currentOpenaiUrl !== "") {
65
+ format = "openai";
66
+ baseUrl = currentOpenaiUrl;
67
+ } else if (currentAnthropicUrl !== "") {
68
+ format = "anthropic";
69
+ baseUrl = currentAnthropicUrl;
70
+ } else if (p.format !== undefined && p.baseUrl !== undefined && p.baseUrl !== "") {
71
+ // Only baseUrl is set (legacy config) - migrate based on format
72
+ format = p.format;
73
+ baseUrl = p.baseUrl;
74
+ if (format === "openai") {
75
+ return { ...p, format, baseUrl, anthropicBaseUrl: "", openaiBaseUrl: p.baseUrl };
76
+ } else {
77
+ return { ...p, format, baseUrl, anthropicBaseUrl: p.baseUrl, openaiBaseUrl: "" };
98
78
  }
79
+ }
99
80
 
100
- migrated = true;
81
+ return {
82
+ ...p,
83
+ format,
84
+ baseUrl,
85
+ anthropicBaseUrl: currentAnthropicUrl,
86
+ openaiBaseUrl: currentOpenaiUrl,
87
+ };
88
+ }
101
89
 
102
- return {
103
- ...p,
104
- format,
105
- baseUrl,
106
- anthropicBaseUrl: newAnthropicUrl,
107
- openaiBaseUrl: newOpenaiUrl,
108
- };
109
- });
90
+ /**
91
+ * Migrates existing provider configs to preserve both Anthropic and OpenAI URLs.
92
+ * Old configs had anthropicBaseUrl/openaiBaseUrl, we now keep both.
93
+ */
94
+ function migrateProviders(): void {
95
+ const providers = store.get("providers", []);
96
+ const updated = providers.map(migrateProvider);
97
+ const migrated = updated.some((p, i) => JSON.stringify(p) !== JSON.stringify(providers[i]));
110
98
 
111
99
  if (migrated) {
112
100
  store.set("providers", updated);
@@ -116,6 +104,21 @@ function migrateProviders(): void {
116
104
  // Run migration on module load
117
105
  migrateProviders();
118
106
 
107
+ // Override with JSON env var if provided (for testing)
108
+ const providersJson = process.env["LLM_INSPECTOR_PROVIDERS_JSON"];
109
+ if (providersJson !== undefined) {
110
+ try {
111
+ const parsed = ProviderConfigSchema.array().safeParse(JSON.parse(providersJson));
112
+ if (parsed.success) {
113
+ // Apply migration to JSON providers (e.g., convert baseUrl to openaiBaseUrl/anthropicBaseUrl)
114
+ const migrated = parsed.data.map(migrateProvider);
115
+ store.set("providers", migrated);
116
+ }
117
+ } catch {
118
+ // Ignore invalid JSON
119
+ }
120
+ }
121
+
119
122
  export function getProviders(): ProviderConfig[] {
120
123
  return store.get("providers", []);
121
124
  }
@@ -128,7 +131,7 @@ export function getProvider(id: string): ProviderConfig | undefined {
128
131
  /**
129
132
  * Normalizes an API key by stripping "Bearer " prefix if present.
130
133
  */
131
- function normalizeApiKey(apiKey: string): string {
134
+ export function normalizeApiKey(apiKey: string): string {
132
135
  return apiKey.replace(/^Bearer\s+/i, "").trim();
133
136
  }
134
137
 
@@ -152,6 +155,8 @@ export function addProvider(
152
155
  authHeader: authHeader ?? "bearer",
153
156
  createdAt: now,
154
157
  updatedAt: now,
158
+ anthropicBaseUrl: format === "anthropic" && baseUrl !== undefined ? baseUrl : "",
159
+ openaiBaseUrl: format === "openai" && baseUrl !== undefined ? baseUrl : "",
155
160
  };
156
161
  providers.push(newProvider);
157
162
  store.set("providers", providers);
@@ -176,6 +181,11 @@ export function updateProvider(
176
181
  authHeader: updates.authHeader ?? existing.authHeader,
177
182
  createdAt: existing.createdAt,
178
183
  updatedAt: new Date().toISOString(),
184
+ // Handle format-specific URLs
185
+ anthropicBaseUrl:
186
+ updates.anthropicBaseUrl !== undefined ? updates.anthropicBaseUrl : existing.anthropicBaseUrl,
187
+ openaiBaseUrl:
188
+ updates.openaiBaseUrl !== undefined ? updates.openaiBaseUrl : existing.openaiBaseUrl,
179
189
  };
180
190
  const index = providers.findIndex((p) => p.id === id);
181
191
  providers[index] = updated;
@@ -9,6 +9,8 @@ const ProviderUpdateSchema = z.object({
9
9
  baseUrl: z.string().min(1, "Base URL is required").optional(),
10
10
  model: z.string().min(1, "Model is required").optional(),
11
11
  authHeader: z.enum(["bearer", "x-api-key"]).optional(),
12
+ anthropicBaseUrl: z.string().optional(),
13
+ openaiBaseUrl: z.string().optional(),
12
14
  });
13
15
 
14
16
  export const Route = createFileRoute("/api/providers/$providerId")({