From 5a1b01b9eb2fc17ee1746a33271a22cb45a1f836 Mon Sep 17 00:00:00 2001 From: Ahmet Ildirim Date: Thu, 26 Feb 2026 20:25:18 +0100 Subject: [PATCH] feat: add Grok provider integration and update settings --- README.md | 2 +- package-lock.json | 4 +- src/providers/factory.ts | 5 +- src/providers/grok/index.ts | 2 + src/providers/grok/provider.ts | 83 ++++++++++++++++++++++++++++++++++ src/providers/grok/settings.ts | 11 +++++ src/providers/index.ts | 4 +- src/providers/provider.ts | 3 +- src/settings/provider.ts | 26 ++++++++++- src/settings/settings.ts | 13 +++++- src/settings/tab.ts | 10 ++++ 11 files changed, 155 insertions(+), 8 deletions(-) create mode 100644 src/providers/grok/index.ts create mode 100644 src/providers/grok/provider.ts create mode 100644 src/providers/grok/settings.ts diff --git a/README.md b/README.md index 7a9f963..bf69154 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ Inscribe is an AI-powered inline autocompletion plugin for Obsidian. It will gen Inscribe supports following providers: - Ollama (local) - Gemini +- Grok - OpenAI - OpenAI compatible @@ -48,4 +49,3 @@ Using per-path profile config, you can enable completions only for specific path If you find this plugin useful: [![Buy Me A Coffee](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://buymeacoffee.com/ahmetildirim) - diff --git a/package-lock.json b/package-lock.json index 0addf4a..d1979ac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "obsidian-inscribe", - "version": "1.1.3", + "version": "1.1.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "obsidian-inscribe", - "version": "1.1.3", + "version": "1.1.5", "license": "MIT", "dependencies": { "@google/genai": "^1.41.0", diff --git a/src/providers/factory.ts b/src/providers/factory.ts index 4195696..40fba44 100644 --- a/src/providers/factory.ts +++ b/src/providers/factory.ts @@ -5,12 +5,14 @@ import { OllamaProvider } from "./ollama"; import { OpenAIProvider } from "./openai"; import { OpenAICompatibleProvider } from "./openai-compat"; import { GeminiProvider } from "./gemini"; +import { GrokProvider } from "./grok"; interface Providers { [ProviderType.OLLAMA]: OllamaProvider, [ProviderType.OPENAI]: OpenAIProvider, [ProviderType.OPENAI_COMPATIBLE]: OpenAICompatibleProvider, [ProviderType.GEMINI]: GeminiProvider, + [ProviderType.GROK]: GrokProvider, } export class ProviderFactory { @@ -44,6 +46,7 @@ export class ProviderFactory { [ProviderType.OPENAI]: new OpenAIProvider(settings.providers.openai), [ProviderType.OPENAI_COMPATIBLE]: new OpenAICompatibleProvider(settings.providers.openai_compatible), [ProviderType.GEMINI]: new GeminiProvider(settings.providers.gemini), + [ProviderType.GROK]: new GrokProvider(settings.providers.grok), } } -} \ No newline at end of file +} diff --git a/src/providers/grok/index.ts b/src/providers/grok/index.ts new file mode 100644 index 0000000..d2a3b0e --- /dev/null +++ b/src/providers/grok/index.ts @@ -0,0 +1,2 @@ +export * from "./provider"; +export * from "./settings"; diff --git a/src/providers/grok/provider.ts b/src/providers/grok/provider.ts new file mode 100644 index 0000000..699b08d --- /dev/null +++ b/src/providers/grok/provider.ts @@ -0,0 +1,83 @@ +import { Provider } from ".."; +import { Editor } from "obsidian"; +import OpenAI from "openai"; +import { ProfileOptions } from "src/settings/settings"; +import { GrokSettings } from "."; + +export class GrokProvider implements Provider { + private static readonly BASE_URL = "https://api.x.ai/v1"; + client: OpenAI; + settings: GrokSettings; + aborted: boolean = false; + abortcontroller?: AbortController; + + constructor(settings: GrokSettings) { + this.settings = settings; + this.client = new OpenAI({ + baseURL: GrokProvider.BASE_URL, + apiKey: this.settings.apiKey, + dangerouslyAllowBrowser: true, + }); + } + + async *generate(editor: Editor, prompt: string, options: ProfileOptions): AsyncGenerator { + this.aborted = false; + this.abortcontroller = new AbortController(); + + const initialPosition = editor.getCursor(); + const stream = await this.client.chat.completions.create({ + model: options.model, + messages: [ + { role: "system", content: options.systemPrompt }, + { role: "user", content: prompt } + ], + temperature: options.temperature, + stream: true, + }, { signal: this.abortcontroller.signal }); + + let completion = ""; + for await (const chunk of stream) { + if (this.aborted) { + return; + } + if (this.cursorMoved(editor, initialPosition)) { + this.abort(); + return; + } + + const content = chunk.choices[0]?.delta?.content || ""; + completion += content; + yield completion; + } + } + + async abort() { + if (this.aborted) return; + this.aborted = true; + this.abortcontroller?.abort(); + } + + async fetchModels(): Promise { + if (!this.client) { + return this.settings.models; + } + + const models = await this.client.models.list(); + return models.data.map(model => model.id); + } + + async connectionTest(): Promise { + try { + await this.client.models.list(); + return true; + } catch (error) { + console.error("Error testing connection:", error); + return false; + } + } + + private cursorMoved(editor: Editor, initialPosition: { line: number, ch: number }): boolean { + const currentPosition = editor.getCursor(); + return currentPosition.line !== initialPosition.line || currentPosition.ch !== initialPosition.ch; + } +} diff --git a/src/providers/grok/settings.ts b/src/providers/grok/settings.ts new file mode 100644 index 0000000..3372680 --- /dev/null +++ b/src/providers/grok/settings.ts @@ -0,0 +1,11 @@ +import { ProviderType } from ".."; + +export interface GrokSettings { + integration: ProviderType; + name: string; + description: string; + apiKey: string; + models: string[]; + configured: boolean; + temperature_range: { min: number; max: number }; +} diff --git a/src/providers/index.ts b/src/providers/index.ts index d90798e..553d8f5 100644 --- a/src/providers/index.ts +++ b/src/providers/index.ts @@ -2,5 +2,7 @@ export * from "./provider"; export * from "./factory"; export * from "./ollama"; export * from "./openai"; - +export * from "./openai-compat"; +export * from "./gemini"; +export * from "./grok"; diff --git a/src/providers/provider.ts b/src/providers/provider.ts index 677c2dd..b11997c 100644 --- a/src/providers/provider.ts +++ b/src/providers/provider.ts @@ -6,6 +6,7 @@ export enum ProviderType { OPENAI = "openai", OPENAI_COMPATIBLE = "openai_compatible", GEMINI = "gemini", + GROK = "grok", } // Completer interface for ai integrations @@ -15,4 +16,4 @@ export interface Provider { abort: () => Promise; fetchModels(): Promise | string[]; connectionTest(): Promise; -} \ No newline at end of file +} diff --git a/src/settings/provider.ts b/src/settings/provider.ts index 918aba3..b50dd73 100644 --- a/src/settings/provider.ts +++ b/src/settings/provider.ts @@ -36,6 +36,9 @@ export class ProviderSettingsModal extends Modal { case ProviderType.GEMINI: this.renderGeminiSettings(); break; + case ProviderType.GROK: + this.renderGrokSettings(); + break; default: contentEl.createEl('h2', { text: 'Unknown provider type' }); break; @@ -137,6 +140,27 @@ export class ProviderSettingsModal extends Modal { this.renderConnectionStatus(this.plugin.settings.providers.gemini); } + async renderGrokSettings() { + const { contentEl } = this; + contentEl.empty(); + this.setTitle('Grok'); + + new Setting(contentEl) + .setName("Grok API key") + .setDesc("The API key for xAI Grok") + .addText((text) => { + text + .setValue(this.plugin.settings.providers.grok.apiKey) + .onChange(async (value) => { + this.plugin.settings.providers.grok.apiKey = value; + await this.plugin.saveSettings(); + }); + }); + + this.renderModelSettings(this.plugin.settings.providers.grok); + this.renderConnectionStatus(this.plugin.settings.providers.grok); + } + private renderModelSettings(provider: { models: string[] }) { new Setting(this.contentEl) .setName("Available models") @@ -192,4 +216,4 @@ export class ProviderSettingsModal extends Modal { contentEl.empty(); this.onCloseCallback(); } -} \ No newline at end of file +} diff --git a/src/settings/settings.ts b/src/settings/settings.ts index 47a671c..69730ce 100644 --- a/src/settings/settings.ts +++ b/src/settings/settings.ts @@ -4,6 +4,7 @@ import { OllamaSettings } from "src/providers/ollama"; import { OpenAISettings } from "src/providers/openai"; import { OpenAICompatibleSettings } from "src/providers/openai-compat"; import { GeminiSettings } from "src/providers/gemini"; +import { GrokSettings } from "src/providers/grok"; // Completion options for a profile export interface ProfileOptions { @@ -54,6 +55,7 @@ export interface Settings { openai: OpenAISettings, openai_compatible: OpenAICompatibleSettings, gemini: GeminiSettings, + grok: GrokSettings, }, // profiles profiles: Profiles, @@ -121,6 +123,15 @@ export const DEFAULT_SETTINGS: Settings = { configured: false, temperature_range: { min: 0, max: 2 }, }, + grok: { + integration: ProviderType.GROK, + name: "Grok", + description: "Use xAI Grok models to generate text.", + apiKey: "api-key", + models: ["grok-3-mini", "grok-3"], + configured: false, + temperature_range: { min: 0, max: 1 }, + }, }, profiles: { default: { @@ -186,4 +197,4 @@ export function resetSettings(settings: Settings): void { export const isDefaultProfile = (profile: ProfileId): boolean => { return profile === DEFAULT_PROFILE; -} \ No newline at end of file +} diff --git a/src/settings/tab.ts b/src/settings/tab.ts index 80d9cf2..dd4efd5 100644 --- a/src/settings/tab.ts +++ b/src/settings/tab.ts @@ -369,6 +369,15 @@ class ProvidersSection { ProviderType.GEMINI, this.plugin.settings.providers.gemini.configured)); + // Grok Provider + new Setting(this.container) + .setName("Grok") + .setDesc("xAI Grok API") + .addButton((button: ButtonComponent) => this.createConfigureButton( + button, + ProviderType.GROK, + this.plugin.settings.providers.grok.configured)); + // OpenAI Compatible Provider new Setting(this.container) .setName("OpenAI compatible API") @@ -444,6 +453,7 @@ class ProfilesSection { .addOption(ProviderType.OLLAMA, "Ollama") .addOption(ProviderType.OPENAI, "OpenAI") .addOption(ProviderType.GEMINI, "Gemini") + .addOption(ProviderType.GROK, "Grok") .addOption(ProviderType.OPENAI_COMPATIBLE, "OpenAI Compatible") .setValue(profile.provider) .onChange(async (value: ProviderType) => {