|
|
@@ -83,6 +83,21 @@ export function resolveProviderKind(opts = {}) {
|
|
|
// Commercial-only default. Missing endpoint is handled as typed HOLD by the factory.
|
|
|
return "openai";
|
|
|
}
|
|
|
+/** Google Gemini OpenAI-compat embeddings base (qmd appends `/v1/embeddings`). */
|
|
|
+export const GEMINI_OPENAI_EMBED_ENDPOINT = "https://generativelanguage.googleapis.com/v1beta/openai";
|
|
|
+export const GEMINI_EMBED_MODEL = "gemini-embedding-001";
|
|
|
+export function isGeminiEmbeddingsHost(endpoint) {
|
|
|
+ try {
|
|
|
+ return new URL(endpoint).hostname.toLowerCase().endsWith("generativelanguage.googleapis.com");
|
|
|
+ }
|
|
|
+ catch {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+}
|
|
|
+function nativeGeminiKey(env) {
|
|
|
+ const key = (env.GEMINI_API_KEY || env.GOOGLE_API_KEY || "").trim();
|
|
|
+ return key || undefined;
|
|
|
+}
|
|
|
/**
|
|
|
* Factory entry point — returns the appropriate `EmbeddingProvider`.
|
|
|
* Throws if `openai` kind is requested but no endpoint is configured.
|
|
|
@@ -91,23 +106,36 @@ export function createEmbeddingProvider(opts = {}) {
|
|
|
const env = opts.env ?? process.env;
|
|
|
const cfg = loadConfigFile(opts.configPath);
|
|
|
resolveProviderKind(opts);
|
|
|
- // OpenAI
|
|
|
- const endpoint = opts.openai?.endpoint ??
|
|
|
+ const geminiKey = nativeGeminiKey(env);
|
|
|
+ // OpenAI (or Gemini's OpenAI-compat layer, using the native Gemini key)
|
|
|
+ let endpoint = opts.openai?.endpoint ??
|
|
|
env.QMD_EMBED_ENDPOINT ??
|
|
|
cfg.embedProvider?.endpoint;
|
|
|
if (!endpoint || endpoint.trim() === "") {
|
|
|
- throw commercialApiHold('commercial provider requires an endpoint. ' +
|
|
|
- "Set QMD_EMBED_ENDPOINT env var, or `embedProvider.endpoint` in " +
|
|
|
- "~/.config/qmd/config.json, or pass `openai.endpoint`.");
|
|
|
+ if (geminiKey) {
|
|
|
+ endpoint = GEMINI_OPENAI_EMBED_ENDPOINT;
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ throw commercialApiHold('commercial provider requires an endpoint. ' +
|
|
|
+ "Set QMD_EMBED_ENDPOINT env var, or `embedProvider.endpoint` in " +
|
|
|
+ "~/.config/qmd/config.json, or pass `openai.endpoint`. " +
|
|
|
+ "A GEMINI_API_KEY / GOOGLE_API_KEY is also enough — qmd then uses Gemini native embeddings.");
|
|
|
+ }
|
|
|
}
|
|
|
assertCommercialEndpoint(endpoint);
|
|
|
- const apiKey = opts.openai?.apiKey ??
|
|
|
+ let apiKey = opts.openai?.apiKey ??
|
|
|
env.QMD_EMBED_API_KEY ??
|
|
|
cfg.embedProvider?.apiKey;
|
|
|
- const modelId = opts.openai?.modelId ??
|
|
|
+ if (!apiKey && isGeminiEmbeddingsHost(endpoint)) {
|
|
|
+ apiKey = geminiKey;
|
|
|
+ }
|
|
|
+ let modelId = opts.openai?.modelId ??
|
|
|
env.QMD_EMBED_MODEL_ID ??
|
|
|
cfg.embedProvider?.modelId ??
|
|
|
"embeddinggemma";
|
|
|
+ if (isGeminiEmbeddingsHost(endpoint) && modelId === "embeddinggemma") {
|
|
|
+ modelId = GEMINI_EMBED_MODEL;
|
|
|
+ }
|
|
|
const upstreamModel = opts.openai?.upstreamModel ??
|
|
|
env.QMD_EMBED_UPSTREAM_MODEL ??
|
|
|
cfg.embedProvider?.upstreamModel;
|