Skip to content

Commit e616caa

Browse files
committed
feat: add OpenAI embedding/reranking/query expansion provider
Port PR tobi#116 (tobi/qmd) to current main, adapting to the refactored codebase. Adds OpenAI as an alternative to local GGUF models, fixing the ARM64 segfault during hybrid search (issue tobi#68). Changes: - New src/openai-llm.ts: OpenAI API client (embed, embedBatch, rerank, expandQuery) with exponential backoff and rate limiting - llm.ts: setEmbeddingConfig(), getDefaultEmbeddingLLM(), isUsingOpenAI() - collections.ts: EmbeddingProviderConfig type, getEmbeddingConfig() - store.ts: Provider-aware embedding, chunking (tiktoken), expand, rerank - qmd.ts: Startup config loading, provider-aware embed command - package.json: openai + tiktoken dependencies Config via ~/.config/qmd/index.yml: embedding: provider: openai openai: model: text-embedding-3-small Or env: QMD_OPENAI=1 + OPENAI_API_KEY
1 parent 785bbcf commit e616caa

7 files changed

Lines changed: 625 additions & 51 deletions

File tree

bun.lock

Lines changed: 42 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@
2020
"dependencies": {
2121
"@modelcontextprotocol/sdk": "^1.25.1",
2222
"node-llama-cpp": "^3.14.5",
23+
"openai": "^4.77.0",
2324
"sqlite-vec": "^0.1.7-alpha.2",
25+
"tiktoken": "^1.0.22",
2426
"yaml": "^2.8.2",
2527
"zod": "^4.2.1"
2628
},

src/collections.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,24 @@ export interface Collection {
3131
update?: string; // Optional bash command to run during qmd update
3232
}
3333

34+
/**
35+
* Embedding provider configuration (optional in config file)
36+
*/
37+
export interface EmbeddingProviderConfig {
38+
provider?: "local" | "openai"; // Default: 'local'
39+
openai?: {
40+
api_key?: string; // Falls back to OPENAI_API_KEY env var
41+
model?: string; // Default: 'text-embedding-3-small'
42+
};
43+
}
44+
3445
/**
3546
* The complete configuration file structure
3647
*/
3748
export interface CollectionConfig {
3849
global_context?: string; // Context applied to all collections
3950
collections: Record<string, Collection>; // Collection name -> config
51+
embedding?: EmbeddingProviderConfig; // Optional embedding provider settings
4052
}
4153

4254
/**
@@ -222,6 +234,14 @@ export function getGlobalContext(): string | undefined {
222234
return config.global_context;
223235
}
224236

237+
/**
238+
* Get embedding provider configuration
239+
*/
240+
export function getEmbeddingConfig(): EmbeddingProviderConfig {
241+
const config = loadConfig();
242+
return config.embedding || {};
243+
}
244+
225245
/**
226246
* Set global context
227247
*/

src/llm.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
import { homedir } from "os";
1818
import { join } from "path";
1919
import { existsSync, mkdirSync, statSync, unlinkSync, readdirSync, readFileSync, writeFileSync } from "fs";
20+
import { OpenAIEmbedding, type OpenAIConfig } from "./openai-llm.js";
2021

2122
// =============================================================================
2223
// Embedding Formatting Functions
@@ -291,6 +292,11 @@ export interface LLM {
291292
*/
292293
embed(text: string, options?: EmbedOptions): Promise<EmbeddingResult | null>;
293294

295+
/**
296+
* Get embeddings for multiple texts in a batch
297+
*/
298+
embedBatch(texts: string[]): Promise<(EmbeddingResult | null)[]>;
299+
294300
/**
295301
* Generate text completion
296302
*/
@@ -1206,3 +1212,47 @@ export async function disposeDefaultLlamaCpp(): Promise<void> {
12061212
defaultLlamaCpp = null;
12071213
}
12081214
}
1215+
1216+
// =============================================================================
1217+
// OpenAI Embedding Support
1218+
// =============================================================================
1219+
1220+
export type EmbeddingProvider = "local" | "openai";
1221+
1222+
export type EmbeddingConfig = {
1223+
provider: EmbeddingProvider;
1224+
openai?: OpenAIConfig;
1225+
};
1226+
1227+
// Default embedding config: use local llama-cpp
1228+
let embeddingConfig: EmbeddingConfig = { provider: "local" };
1229+
let openAIEmbedding: OpenAIEmbedding | null = null;
1230+
1231+
/**
1232+
* Set the embedding configuration. Call before using embeddings.
1233+
*/
1234+
export function setEmbeddingConfig(config: EmbeddingConfig): void {
1235+
embeddingConfig = config;
1236+
// Reset OpenAI instance if config changes
1237+
openAIEmbedding = null;
1238+
}
1239+
1240+
/**
1241+
* Check if using OpenAI for embeddings/query expansion/reranking.
1242+
*/
1243+
export function isUsingOpenAI(): boolean {
1244+
return embeddingConfig.provider === "openai";
1245+
}
1246+
1247+
/**
1248+
* Get the provider-aware embedding/query/rerank client.
1249+
*/
1250+
export function getDefaultEmbeddingLLM(): LLM {
1251+
if (embeddingConfig.provider === "openai") {
1252+
if (!openAIEmbedding) {
1253+
openAIEmbedding = new OpenAIEmbedding(embeddingConfig.openai);
1254+
}
1255+
return openAIEmbedding;
1256+
}
1257+
return getDefaultLlamaCpp();
1258+
}

0 commit comments

Comments
 (0)