Skip to content

Commit 9759c47

Browse files
jiunbaeclaude
andcommitted
Add multi-provider embedding + separate [embedding] config
- Support Gemini (gemini-embedding-001), Azure OpenAI, OpenAI embeddings - Add [embedding] section to kiwi.toml (separate from [llm] for different providers) - Semantic search uses embedding config, falls back to llm config - Fix Gemini model name: text-embedding-004 → gemini-embedding-001 - Add EmbeddingConfig interface to config.ts - 125/416 pages embedded (Gemini free tier rate limit) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 67d99cd commit 9759c47

4 files changed

Lines changed: 67 additions & 19 deletions

File tree

src/config.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,16 @@ export interface Persona {
2121
content_style: string; // injected into content generation prompts
2222
}
2323

24+
export interface EmbeddingConfig {
25+
provider: string; // "gemini" | "openai" | "azure-openai"
26+
api_key: string;
27+
}
28+
2429
export interface KiwiConfig {
2530
project: { name: string; created: string };
2631
build: { output_dir: string };
2732
llm: LLMConfig;
33+
embedding?: EmbeddingConfig; // separate config for embeddings (optional, falls back to llm)
2834
deploy: { target: string };
2935
personas?: Persona[];
3036
active_persona?: string; // name of the active persona

src/index.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -270,14 +270,17 @@ program
270270
console.log(`\x1b[32m✅ ${count}개 페이지가 빌드되었습니다!\x1b[0m`);
271271
console.log(` 출력: ${join(root, config.build.output_dir)}/`);
272272

273-
// Generate embeddings (optional — requires API key)
273+
// Generate embeddings (optional — uses [embedding] config or falls back to [llm])
274274
try {
275-
if (config.llm.api_key && config.llm.endpoint) {
275+
const embConfig = config.embedding
276+
? { ...config.llm, provider: config.embedding.provider, api_key: config.embedding.api_key }
277+
: config.llm;
278+
if (embConfig.api_key && embConfig.provider !== "demo") {
276279
const { generateMissingEmbeddings } = await import("./services/embedding");
277-
await generateMissingEmbeddings(store, config.llm, (msg) => console.log(msg));
280+
await generateMissingEmbeddings(store, embConfig, (msg) => console.log(msg));
278281
}
279-
} catch {
280-
// Embedding generation is optional
282+
} catch (e: unknown) {
283+
console.log(` ⚠ 임베딩 생성 건너뜀: ${e instanceof Error ? e.message : String(e)}`);
281284
}
282285
} catch (e: unknown) {
283286
const message = e instanceof Error ? e.message : String(e);

src/server.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -387,9 +387,13 @@ export function startServer(root: string, port: number, host: string): void {
387387
// Try semantic search first (if embeddings exist)
388388
try {
389389
const searchConfig = loadConfig(root);
390-
if (searchConfig.llm.api_key && searchConfig.llm.endpoint) {
390+
// Use embedding config if available, fall back to llm config
391+
const embeddingLlmConfig = searchConfig.embedding
392+
? { ...searchConfig.llm, provider: searchConfig.embedding.provider, api_key: searchConfig.embedding.api_key }
393+
: searchConfig.llm;
394+
if (embeddingLlmConfig.api_key) {
391395
const { semanticSearch } = await import("./services/embedding");
392-
const semanticResults = await semanticSearch(query, store, searchConfig.llm, 5);
396+
const semanticResults = await semanticSearch(query, store, embeddingLlmConfig, 5);
393397
if (semanticResults.length > 0) {
394398
return Response.json({
395399
results: semanticResults.map(r => ({

src/services/embedding.ts

Lines changed: 47 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,24 +12,59 @@ function cosineSimilarity(a: Float32Array, b: Float32Array): number {
1212
return dot / (Math.sqrt(normA) * Math.sqrt(normB));
1313
}
1414

15-
// Get embedding from Azure OpenAI
15+
// Get embedding — auto-detect provider
1616
async function getEmbedding(text: string, config: LLMConfig): Promise<Float32Array> {
17-
const { default: OpenAI } = await import("openai");
17+
const input = text.slice(0, 8000);
1818

19-
// Azure OpenAI embedding endpoint
20-
const client = new OpenAI({
21-
apiKey: config.api_key,
22-
baseURL: `${config.endpoint}/openai/deployments/text-embedding-3-small`,
23-
defaultQuery: { "api-version": "2024-06-01" },
24-
defaultHeaders: { "api-key": config.api_key },
19+
if (config.provider === "gemini") {
20+
return await geminiEmbedding(input, config);
21+
} else if (config.provider === "azure-openai") {
22+
return await azureEmbedding(input, config);
23+
} else if (config.provider === "openai") {
24+
return await openaiEmbedding(input, config);
25+
}
26+
throw new Error(`Embedding not supported for provider: ${config.provider}`);
27+
}
28+
29+
// Gemini Embedding API (free)
30+
async function geminiEmbedding(text: string, config: LLMConfig): Promise<Float32Array> {
31+
const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-embedding-001:embedContent`;
32+
const resp = await fetch(url, {
33+
method: "POST",
34+
headers: { "Content-Type": "application/json", "x-goog-api-key": config.api_key },
35+
body: JSON.stringify({
36+
model: "models/gemini-embedding-001",
37+
content: { parts: [{ text }] }
38+
})
2539
});
40+
if (!resp.ok) throw new Error(`Gemini embedding error (${resp.status})`);
41+
const data = await resp.json() as { embedding: { values: number[] } };
42+
return new Float32Array(data.embedding.values);
43+
}
2644

27-
const response = await client.embeddings.create({
28-
model: "text-embedding-3-small",
29-
input: text.slice(0, 8000), // Limit input length
45+
// Azure OpenAI Embedding
46+
async function azureEmbedding(text: string, config: LLMConfig): Promise<Float32Array> {
47+
const url = `${config.endpoint}/openai/deployments/text-embedding-3-small/embeddings?api-version=2024-06-01`;
48+
const resp = await fetch(url, {
49+
method: "POST",
50+
headers: { "Content-Type": "application/json", "api-key": config.api_key },
51+
body: JSON.stringify({ input: text, model: "text-embedding-3-small" })
3052
});
53+
if (!resp.ok) throw new Error(`Azure embedding error (${resp.status})`);
54+
const data = await resp.json() as { data: Array<{ embedding: number[] }> };
55+
return new Float32Array(data.data[0].embedding);
56+
}
3157

32-
return new Float32Array(response.data[0].embedding);
58+
// OpenAI Embedding
59+
async function openaiEmbedding(text: string, config: LLMConfig): Promise<Float32Array> {
60+
const resp = await fetch("https://api.openai.com/v1/embeddings", {
61+
method: "POST",
62+
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${config.api_key}` },
63+
body: JSON.stringify({ input: text, model: "text-embedding-3-small" })
64+
});
65+
if (!resp.ok) throw new Error(`OpenAI embedding error (${resp.status})`);
66+
const data = await resp.json() as { data: Array<{ embedding: number[] }> };
67+
return new Float32Array(data.data[0].embedding);
3368
}
3469

3570
// Generate embeddings for all pages that don't have one

0 commit comments

Comments
 (0)