apikey-0.1

This commit is contained in:
JamJan05 2026-07-09 21:52:13 +02:00
parent 5a48363e84
commit d2b2b0e9a5
6 changed files with 62 additions and 10 deletions

View file

@ -84,7 +84,7 @@ Use **Provider → Local API** to chat with models running on your own machine.
4. Set **Base URL** to `http://localhost:11434` (no `/v1` — AI-Vault uses Ollama's native endpoints).
5. Click **Refresh models** and select a model from the list.
> Web search is not available for local models. Local API requests are sent only to the Base URL you configure and never leave your machine.
> Web search is not available for Local API models. Local API requests are sent only to the Base URL you configure. If your Base URL points to a cloud service or third-party gateway, your chat messages and Local API key are sent to that endpoint. Leave **Local API key** empty for local Ollama or LM Studio.
---
@ -165,7 +165,7 @@ Data is sent to model providers only when it is part of a request, for example:
- project context,
- web-search requests.
AI-Vault does not use its own backend server. Requests go directly from Obsidian to the configured OpenAI or Anthropic API, or to the Local API Base URL you set.
AI-Vault does not use its own backend server. Requests go directly from Obsidian to the configured OpenAI or Anthropic API, or to the Local API Base URL you set. Local API can be a local server such as Ollama or LM Studio, or a user-configured authenticated Ollama/OpenAI-compatible gateway such as Ollama Cloud, LiteLLM, LocalAI, or vLLM.
---

View file

@ -130,9 +130,11 @@ export class GPTSettingsTab extends PluginSettingTab {
.onChange(async (v: boolean) => {
const oldApiKey = this.plugin.settings.apiKey;
const oldClaudeApiKey = this.plugin.settings.claudeApiKey;
const oldLocalApiKey = this.plugin.settings.localApiKey;
this.plugin.settings.apiKeysInSync = v;
this.plugin.settings.apiKey = oldApiKey;
this.plugin.settings.claudeApiKey = oldClaudeApiKey;
this.plugin.settings.localApiKey = oldLocalApiKey;
await this.plugin.saveSettings();
if (v) {
@ -146,6 +148,7 @@ export class GPTSettingsTab extends PluginSettingTab {
if (d) {
delete d.apiKey;
delete d.claudeApiKey;
delete d.localApiKey;
await this.plugin.saveData(d);
}
new Notice(t("notice_keys_moved_local"), 5000);
@ -353,6 +356,19 @@ export class GPTSettingsTab extends PluginSettingTab {
txt.inputEl.addClass("gpt-settings-input-full");
});
new Setting(el)
.setName(t("settings_local_api_key_name"))
.setDesc(t("settings_local_api_key_desc"))
.addText(txt => {
txt.inputEl.type = "password";
txt.setValue(this.plugin.settings.localApiKey ?? "")
.onChange(async (value: string) => {
this.plugin.settings.localApiKey = value.trim();
await this.plugin.saveSettings();
});
txt.inputEl.addClass("gpt-settings-input-full");
});
new Setting(el)
.setName(t("settings_local_refresh_name"))
.setDesc(t("settings_local_refresh_desc"))
@ -364,7 +380,7 @@ export class GPTSettingsTab extends PluginSettingTab {
void this.refreshLocalModelsInSelector()
.catch((err: unknown) => {
const message = err instanceof Error ? err.message : String(err);
console.error("Local API refresh failed:", err);
console.error("Local API refresh failed:", message);
new Notice(t("settings_local_refresh_fail", message), 7000);
})
.finally(() => {

View file

@ -34,6 +34,20 @@ function errorMessage(err: unknown): string {
return err instanceof Error ? err.message : String(err);
}
function isAuthenticationFailure(status: number): boolean {
return status === 401 || status === 403;
}
export function buildLocalApiHeaders(settings: PluginSettings, includeJsonContentType = false): Record<string, string> {
const headers: Record<string, string> = {};
if (includeJsonContentType) headers["Content-Type"] = "application/json";
const localApiKey = settings.localApiKey?.trim();
if (localApiKey) headers["Authorization"] = `Bearer ${localApiKey}`;
return headers;
}
async function requestLocal(options: {
url: string;
method: "GET" | "POST";
@ -95,7 +109,14 @@ export async function fetchLocalModels(settings: PluginSettings): Promise<string
const url = settings.localApiType === "ollama" ? `${base}/api/tags` : `${base}/models`;
const response = await requestLocal({ url, method: "GET" });
const response = await requestLocal({
url,
method: "GET",
headers: buildLocalApiHeaders(settings),
});
if (isAuthenticationFailure(response.status)) {
throw new Error("Authentication failed. Check your Local API key and Base URL.");
}
if (response.status < 200 || response.status >= 300) {
throw new Error(`Local API error ${response.status}: ${response.text || "Check that the Base URL and API type are correct."}`);
}
@ -109,14 +130,17 @@ export async function fetchLocalModels(settings: PluginSettings): Promise<string
// ─── Send a chat request ────────────────────────────────────────────────────────
async function postLocal(url: string, body: Record<string, unknown>): Promise<unknown> {
async function postLocal(settings: PluginSettings, url: string, body: Record<string, unknown>): Promise<unknown> {
const response = await requestLocal({
url,
method: "POST",
headers: { "Content-Type": "application/json" },
headers: buildLocalApiHeaders(settings, true),
body: JSON.stringify(body),
});
if (isAuthenticationFailure(response.status)) {
throw new Error("Authentication failed. Check your Local API key and Base URL.");
}
if (response.status < 200 || response.status >= 300) {
throw new Error(`Local API error ${response.status}: ${response.text}`);
}
@ -166,7 +190,7 @@ export async function callLocalApi(
messages: payloadMessages,
stream: false,
};
const data = await postLocal(`${base}/api/chat`, body);
const data = await postLocal(settings, `${base}/api/chat`, body);
const content = extractOllamaContent(data);
if (!content) throw new Error("Invalid Ollama response. Expected message.content.");
return content;
@ -180,7 +204,7 @@ export async function callLocalApi(
};
if (typeof options.maxTokens === "number") body.max_tokens = options.maxTokens;
const data = await postLocal(`${base}/chat/completions`, body);
const data = await postLocal(settings, `${base}/chat/completions`, body);
const content = extractOpenAIContent(data);
if (!content) throw new Error("Invalid OpenAI-compatible response. Expected choices[0].message.content.");
return content;

View file

@ -88,6 +88,8 @@ const en: TranslationDict = {
settings_local_type_name: "Local API Type",
settings_local_baseurl_name: "Base URL",
settings_local_baseurl_desc: "LM Studio usually uses http://localhost:1234/v1. Ollama usually uses http://localhost:11434.",
settings_local_api_key_name: "Local API key",
settings_local_api_key_desc: "Optional. Required only for authenticated Ollama/OpenAI-compatible endpoints such as Ollama Cloud or third-party LLM gateways. Leave empty for local Ollama or LM Studio.",
settings_local_refresh_name: "Refresh models",
settings_local_refresh_desc: "Fetch available models from your selected local server.",
settings_local_refresh_btn: "Refresh models",
@ -420,6 +422,8 @@ const pl: TranslationDict = {
settings_local_type_name: "Typ lokalnego API",
settings_local_baseurl_name: "Base URL",
settings_local_baseurl_desc: "LM Studio zwykle używa http://localhost:1234/v1. Ollama zwykle używa http://localhost:11434.",
settings_local_api_key_name: "Local API key",
settings_local_api_key_desc: "Optional. Required only for authenticated Ollama/OpenAI-compatible endpoints such as Ollama Cloud or third-party LLM gateways. Leave empty for local Ollama or LM Studio.",
settings_local_refresh_name: "Odśwież modele",
settings_local_refresh_desc: "Pobierz dostępne modele z wybranego lokalnego serwera.",
settings_local_refresh_btn: "Odśwież modele",

View file

@ -328,9 +328,11 @@ export default class GPTPlugin extends Plugin {
await this.externalStorage.writeJson(keysPath, {
apiKey: this.settings.apiKey ?? "",
claudeApiKey: this.settings.claudeApiKey ?? "",
localApiKey: this.settings.localApiKey ?? "",
});
delete toSave.apiKey;
delete toSave.claudeApiKey;
delete toSave.localApiKey;
await this.saveData(toSave);
}
}
@ -358,18 +360,21 @@ export default class GPTPlugin extends Plugin {
// keys the user has in memory/data.json (the migration branch below
// will still run for legacy data.json keys).
if (!(await this.externalStorage.exists(keysPath))) {
const hasOld = this.settings.apiKey || this.settings.claudeApiKey;
const hasOld = this.settings.apiKey || this.settings.claudeApiKey || this.settings.localApiKey;
if (hasOld) {
// Migration: data.json → keys.json
await this.externalStorage.writeJson(keysPath, {
apiKey: this.settings.apiKey ?? "",
claudeApiKey: this.settings.claudeApiKey ?? "",
localApiKey: this.settings.localApiKey ?? "",
});
delete (this.settings as unknown as Record<string, unknown>).apiKey;
delete (this.settings as unknown as Record<string, unknown>).claudeApiKey;
delete (this.settings as unknown as Record<string, unknown>).localApiKey;
await this.saveData({ ...this.settings });
this.settings.apiKey = this.settings.apiKey ?? "";
this.settings.claudeApiKey = this.settings.claudeApiKey ?? "";
this.settings.localApiKey = this.settings.localApiKey ?? "";
new Notice(t("notice_keys_migrated"), 4000);
}
return;
@ -378,7 +383,7 @@ export default class GPTPlugin extends Plugin {
// keys.json exists — read it. Only overwrite settings on a successful
// read; on a transient read failure preserve the in-memory values to
// avoid wiping the user's keys.
const stored = await this.externalStorage.readJson<{ apiKey?: string; claudeApiKey?: string } | null>(keysPath, null);
const stored = await this.externalStorage.readJson<{ apiKey?: string; claudeApiKey?: string; localApiKey?: string } | null>(keysPath, null);
if (!stored) {
console.warn("[AI-Vault] _loadApiKeys: keys.json unreadable, preserving in-memory keys");
return;
@ -386,6 +391,7 @@ export default class GPTPlugin extends Plugin {
this.settings.apiKey = stored.apiKey ?? this.settings.apiKey ?? "";
this.settings.claudeApiKey = stored.claudeApiKey ?? this.settings.claudeApiKey ?? "";
this.settings.localApiKey = stored.localApiKey ?? this.settings.localApiKey ?? "";
}
// ── Auto-migration ─────────────────────────────────────────────────────────

View file

@ -15,6 +15,7 @@ export interface PluginSettings {
// API Keys
apiKey: string;
claudeApiKey: string;
localApiKey: string;
apiKeysInSync: boolean;
// Models
@ -67,6 +68,7 @@ export const DEFAULT_SYSTEM_PROMPTS: Record<Language, string> = {
export const DEFAULT_SETTINGS: PluginSettings = {
apiKey: "",
claudeApiKey: "",
localApiKey: "",
provider: "openai",
model: "gpt-4o",
claudeModel: "claude-sonnet-4-5",