Skip to content

Commit 0950b4e

Browse files
committed
feat: add custom model infrastructure for OpenAI-compatible embedder
- Add manual model ID and embedding dimension configuration - Enable custom model input via text field in settings UI - Add modelDimension parameter to OpenAiCompatibleEmbedder - Update configuration management to persist dimension setting - Prioritize manual dimension over hardcoded model profiles - Add comprehensive test coverage for new functionality This allows users to specify any custom embedding model and its dimension for OpenAI-compatible providers, removing dependency on hardcoded model profiles.
1 parent f0de952 commit 0950b4e

File tree

11 files changed

+925
-44
lines changed

11 files changed

+925
-44
lines changed

packages/types/src/codebase-index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export const codebaseIndexProviderSchema = z.object({
3535
codeIndexQdrantApiKey: z.string().optional(),
3636
codebaseIndexOpenAiCompatibleBaseUrl: z.string().optional(),
3737
codebaseIndexOpenAiCompatibleApiKey: z.string().optional(),
38+
codebaseIndexOpenAiCompatibleModelDimension: z.number().optional(),
3839
})
3940

4041
export type CodebaseIndexProvider = z.infer<typeof codebaseIndexProviderSchema>

packages/types/src/provider-settings.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,7 @@ export const PROVIDER_SETTINGS_KEYS = keysOf<ProviderSettings>()([
338338
"codeIndexQdrantApiKey",
339339
"codebaseIndexOpenAiCompatibleBaseUrl",
340340
"codebaseIndexOpenAiCompatibleApiKey",
341+
"codebaseIndexOpenAiCompatibleModelDimension",
341342
// Reasoning
342343
"enableReasoningEffort",
343344
"reasoningEffort",

src/services/code-index/__tests__/config-manager.test.ts

Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,125 @@ describe("CodeIndexConfigManager", () => {
112112
})
113113
})
114114

115+
it("should load OpenAI Compatible configuration with modelDimension from globalState", async () => {
116+
const mockGlobalState = {
117+
codebaseIndexEnabled: true,
118+
codebaseIndexQdrantUrl: "http://qdrant.local",
119+
codebaseIndexEmbedderProvider: "openai-compatible",
120+
codebaseIndexEmbedderBaseUrl: "",
121+
codebaseIndexEmbedderModelId: "custom-model",
122+
}
123+
mockContextProxy.getGlobalState.mockImplementation((key: string) => {
124+
if (key === "codebaseIndexConfig") return mockGlobalState
125+
if (key === "codebaseIndexOpenAiCompatibleBaseUrl") return "https://api.example.com/v1"
126+
if (key === "codebaseIndexOpenAiCompatibleModelDimension") return 1024
127+
return undefined
128+
})
129+
mockContextProxy.getSecret.mockImplementation((key: string) => {
130+
if (key === "codeIndexQdrantApiKey") return "test-qdrant-key"
131+
if (key === "codebaseIndexOpenAiCompatibleApiKey") return "test-openai-compatible-key"
132+
return undefined
133+
})
134+
135+
const result = await configManager.loadConfiguration()
136+
137+
expect(result.currentConfig).toEqual({
138+
isEnabled: true,
139+
isConfigured: true,
140+
embedderProvider: "openai-compatible",
141+
modelId: "custom-model",
142+
openAiOptions: { openAiNativeApiKey: "" },
143+
ollamaOptions: { ollamaBaseUrl: "" },
144+
openAiCompatibleOptions: {
145+
baseUrl: "https://api.example.com/v1",
146+
apiKey: "test-openai-compatible-key",
147+
modelDimension: 1024,
148+
},
149+
qdrantUrl: "http://qdrant.local",
150+
qdrantApiKey: "test-qdrant-key",
151+
searchMinScore: 0.4,
152+
})
153+
})
154+
155+
it("should handle missing modelDimension for OpenAI Compatible configuration", async () => {
156+
const mockGlobalState = {
157+
codebaseIndexEnabled: true,
158+
codebaseIndexQdrantUrl: "http://qdrant.local",
159+
codebaseIndexEmbedderProvider: "openai-compatible",
160+
codebaseIndexEmbedderBaseUrl: "",
161+
codebaseIndexEmbedderModelId: "custom-model",
162+
}
163+
mockContextProxy.getGlobalState.mockImplementation((key: string) => {
164+
if (key === "codebaseIndexConfig") return mockGlobalState
165+
if (key === "codebaseIndexOpenAiCompatibleBaseUrl") return "https://api.example.com/v1"
166+
if (key === "codebaseIndexOpenAiCompatibleModelDimension") return undefined
167+
return undefined
168+
})
169+
mockContextProxy.getSecret.mockImplementation((key: string) => {
170+
if (key === "codeIndexQdrantApiKey") return "test-qdrant-key"
171+
if (key === "codebaseIndexOpenAiCompatibleApiKey") return "test-openai-compatible-key"
172+
return undefined
173+
})
174+
175+
const result = await configManager.loadConfiguration()
176+
177+
expect(result.currentConfig).toEqual({
178+
isEnabled: true,
179+
isConfigured: true,
180+
embedderProvider: "openai-compatible",
181+
modelId: "custom-model",
182+
openAiOptions: { openAiNativeApiKey: "" },
183+
ollamaOptions: { ollamaBaseUrl: "" },
184+
openAiCompatibleOptions: {
185+
baseUrl: "https://api.example.com/v1",
186+
apiKey: "test-openai-compatible-key",
187+
},
188+
qdrantUrl: "http://qdrant.local",
189+
qdrantApiKey: "test-qdrant-key",
190+
searchMinScore: 0.4,
191+
})
192+
})
193+
194+
it("should handle invalid modelDimension type for OpenAI Compatible configuration", async () => {
195+
const mockGlobalState = {
196+
codebaseIndexEnabled: true,
197+
codebaseIndexQdrantUrl: "http://qdrant.local",
198+
codebaseIndexEmbedderProvider: "openai-compatible",
199+
codebaseIndexEmbedderBaseUrl: "",
200+
codebaseIndexEmbedderModelId: "custom-model",
201+
}
202+
mockContextProxy.getGlobalState.mockImplementation((key: string) => {
203+
if (key === "codebaseIndexConfig") return mockGlobalState
204+
if (key === "codebaseIndexOpenAiCompatibleBaseUrl") return "https://api.example.com/v1"
205+
if (key === "codebaseIndexOpenAiCompatibleModelDimension") return "invalid-dimension"
206+
return undefined
207+
})
208+
mockContextProxy.getSecret.mockImplementation((key: string) => {
209+
if (key === "codeIndexQdrantApiKey") return "test-qdrant-key"
210+
if (key === "codebaseIndexOpenAiCompatibleApiKey") return "test-openai-compatible-key"
211+
return undefined
212+
})
213+
214+
const result = await configManager.loadConfiguration()
215+
216+
expect(result.currentConfig).toEqual({
217+
isEnabled: true,
218+
isConfigured: true,
219+
embedderProvider: "openai-compatible",
220+
modelId: "custom-model",
221+
openAiOptions: { openAiNativeApiKey: "" },
222+
ollamaOptions: { ollamaBaseUrl: "" },
223+
openAiCompatibleOptions: {
224+
baseUrl: "https://api.example.com/v1",
225+
apiKey: "test-openai-compatible-key",
226+
modelDimension: "invalid-dimension",
227+
},
228+
qdrantUrl: "http://qdrant.local",
229+
qdrantApiKey: "test-qdrant-key",
230+
searchMinScore: 0.4,
231+
})
232+
})
233+
115234
it("should detect restart requirement when provider changes", async () => {
116235
// Initial state - properly configured
117236
mockContextProxy.getGlobalState.mockReturnValue({
@@ -378,6 +497,171 @@ describe("CodeIndexConfigManager", () => {
378497
expect(result.requiresRestart).toBe(true)
379498
})
380499

500+
it("should handle OpenAI Compatible modelDimension changes", async () => {
501+
// Initial state with modelDimension
502+
mockContextProxy.getGlobalState.mockImplementation((key: string) => {
503+
if (key === "codebaseIndexConfig") {
504+
return {
505+
codebaseIndexEnabled: true,
506+
codebaseIndexQdrantUrl: "http://qdrant.local",
507+
codebaseIndexEmbedderProvider: "openai-compatible",
508+
codebaseIndexEmbedderModelId: "custom-model",
509+
}
510+
}
511+
if (key === "codebaseIndexOpenAiCompatibleBaseUrl") return "https://api.example.com/v1"
512+
if (key === "codebaseIndexOpenAiCompatibleModelDimension") return 1024
513+
return undefined
514+
})
515+
mockContextProxy.getSecret.mockImplementation((key: string) => {
516+
if (key === "codebaseIndexOpenAiCompatibleApiKey") return "test-api-key"
517+
return undefined
518+
})
519+
520+
await configManager.loadConfiguration()
521+
522+
// Change modelDimension
523+
mockContextProxy.getGlobalState.mockImplementation((key: string) => {
524+
if (key === "codebaseIndexConfig") {
525+
return {
526+
codebaseIndexEnabled: true,
527+
codebaseIndexQdrantUrl: "http://qdrant.local",
528+
codebaseIndexEmbedderProvider: "openai-compatible",
529+
codebaseIndexEmbedderModelId: "custom-model",
530+
}
531+
}
532+
if (key === "codebaseIndexOpenAiCompatibleBaseUrl") return "https://api.example.com/v1"
533+
if (key === "codebaseIndexOpenAiCompatibleModelDimension") return 2048
534+
return undefined
535+
})
536+
537+
const result = await configManager.loadConfiguration()
538+
expect(result.requiresRestart).toBe(true)
539+
})
540+
541+
it("should not require restart when modelDimension remains the same", async () => {
542+
// Initial state with modelDimension
543+
mockContextProxy.getGlobalState.mockImplementation((key: string) => {
544+
if (key === "codebaseIndexConfig") {
545+
return {
546+
codebaseIndexEnabled: true,
547+
codebaseIndexQdrantUrl: "http://qdrant.local",
548+
codebaseIndexEmbedderProvider: "openai-compatible",
549+
codebaseIndexEmbedderModelId: "custom-model",
550+
}
551+
}
552+
if (key === "codebaseIndexOpenAiCompatibleBaseUrl") return "https://api.example.com/v1"
553+
if (key === "codebaseIndexOpenAiCompatibleModelDimension") return 1024
554+
return undefined
555+
})
556+
mockContextProxy.getSecret.mockImplementation((key: string) => {
557+
if (key === "codebaseIndexOpenAiCompatibleApiKey") return "test-api-key"
558+
return undefined
559+
})
560+
561+
await configManager.loadConfiguration()
562+
563+
// Keep modelDimension the same, change unrelated setting
564+
mockContextProxy.getGlobalState.mockImplementation((key: string) => {
565+
if (key === "codebaseIndexConfig") {
566+
return {
567+
codebaseIndexEnabled: true,
568+
codebaseIndexQdrantUrl: "http://qdrant.local",
569+
codebaseIndexEmbedderProvider: "openai-compatible",
570+
codebaseIndexEmbedderModelId: "custom-model",
571+
codebaseIndexSearchMinScore: 0.5, // Changed unrelated setting
572+
}
573+
}
574+
if (key === "codebaseIndexOpenAiCompatibleBaseUrl") return "https://api.example.com/v1"
575+
if (key === "codebaseIndexOpenAiCompatibleModelDimension") return 1024
576+
return undefined
577+
})
578+
579+
const result = await configManager.loadConfiguration()
580+
expect(result.requiresRestart).toBe(false)
581+
})
582+
583+
it("should require restart when modelDimension is added", async () => {
584+
// Initial state without modelDimension
585+
mockContextProxy.getGlobalState.mockImplementation((key: string) => {
586+
if (key === "codebaseIndexConfig") {
587+
return {
588+
codebaseIndexEnabled: true,
589+
codebaseIndexQdrantUrl: "http://qdrant.local",
590+
codebaseIndexEmbedderProvider: "openai-compatible",
591+
codebaseIndexEmbedderModelId: "custom-model",
592+
}
593+
}
594+
if (key === "codebaseIndexOpenAiCompatibleBaseUrl") return "https://api.example.com/v1"
595+
if (key === "codebaseIndexOpenAiCompatibleModelDimension") return undefined
596+
return undefined
597+
})
598+
mockContextProxy.getSecret.mockImplementation((key: string) => {
599+
if (key === "codebaseIndexOpenAiCompatibleApiKey") return "test-api-key"
600+
return undefined
601+
})
602+
603+
await configManager.loadConfiguration()
604+
605+
// Add modelDimension
606+
mockContextProxy.getGlobalState.mockImplementation((key: string) => {
607+
if (key === "codebaseIndexConfig") {
608+
return {
609+
codebaseIndexEnabled: true,
610+
codebaseIndexQdrantUrl: "http://qdrant.local",
611+
codebaseIndexEmbedderProvider: "openai-compatible",
612+
codebaseIndexEmbedderModelId: "custom-model",
613+
}
614+
}
615+
if (key === "codebaseIndexOpenAiCompatibleBaseUrl") return "https://api.example.com/v1"
616+
if (key === "codebaseIndexOpenAiCompatibleModelDimension") return 1024
617+
return undefined
618+
})
619+
620+
const result = await configManager.loadConfiguration()
621+
expect(result.requiresRestart).toBe(true)
622+
})
623+
624+
it("should require restart when modelDimension is removed", async () => {
625+
// Initial state with modelDimension
626+
mockContextProxy.getGlobalState.mockImplementation((key: string) => {
627+
if (key === "codebaseIndexConfig") {
628+
return {
629+
codebaseIndexEnabled: true,
630+
codebaseIndexQdrantUrl: "http://qdrant.local",
631+
codebaseIndexEmbedderProvider: "openai-compatible",
632+
codebaseIndexEmbedderModelId: "custom-model",
633+
}
634+
}
635+
if (key === "codebaseIndexOpenAiCompatibleBaseUrl") return "https://api.example.com/v1"
636+
if (key === "codebaseIndexOpenAiCompatibleModelDimension") return 1024
637+
return undefined
638+
})
639+
mockContextProxy.getSecret.mockImplementation((key: string) => {
640+
if (key === "codebaseIndexOpenAiCompatibleApiKey") return "test-api-key"
641+
return undefined
642+
})
643+
644+
await configManager.loadConfiguration()
645+
646+
// Remove modelDimension
647+
mockContextProxy.getGlobalState.mockImplementation((key: string) => {
648+
if (key === "codebaseIndexConfig") {
649+
return {
650+
codebaseIndexEnabled: true,
651+
codebaseIndexQdrantUrl: "http://qdrant.local",
652+
codebaseIndexEmbedderProvider: "openai-compatible",
653+
codebaseIndexEmbedderModelId: "custom-model",
654+
}
655+
}
656+
if (key === "codebaseIndexOpenAiCompatibleBaseUrl") return "https://api.example.com/v1"
657+
if (key === "codebaseIndexOpenAiCompatibleModelDimension") return undefined
658+
return undefined
659+
})
660+
661+
const result = await configManager.loadConfiguration()
662+
expect(result.requiresRestart).toBe(true)
663+
})
664+
381665
it("should not require restart when disabled remains disabled", async () => {
382666
// Initial state - disabled but configured
383667
mockContextProxy.getGlobalState.mockReturnValue({

0 commit comments

Comments
 (0)