diff --git a/.changeset/custom-embeddings-endpoint.md b/.changeset/custom-embeddings-endpoint.md new file mode 100644 index 00000000..2ae779d7 --- /dev/null +++ b/.changeset/custom-embeddings-endpoint.md @@ -0,0 +1,5 @@ +--- +"@inkeep/open-knowledge": patch +--- + +Configure a custom OpenAI-compatible embeddings endpoint from Settings → This project → Search instead of editing config files by hand. The semantic-search settings now expose the embeddings base URL directly, and the account copy plus `ok embeddings set-key` messaging now describe the key as belonging to the configured embeddings provider rather than only to OpenAI. diff --git a/packages/app/src/components/settings/EmbeddingsKeySection.tsx b/packages/app/src/components/settings/EmbeddingsKeySection.tsx index 3465833a..613c6710 100644 --- a/packages/app/src/components/settings/EmbeddingsKeySection.tsx +++ b/packages/app/src/components/settings/EmbeddingsKeySection.tsx @@ -57,13 +57,13 @@ export function EmbeddingsKeySection({ transport }: { transport?: EmbeddingsKeyT

- Your OpenAI API key, used only to create embeddings of your content for semantic search. - Stored once for this machine in{' '} + Your embeddings provider API key, used only to create embeddings of your content for + semantic search. Stored once for this machine in{' '} ~/.ok/secrets.yml {' '} (readable only by your user account) and shared across all projects. Turn the feature on - per project in This project → Search. + per project in This project → Search, where you can also override the endpoint.

diff --git a/packages/app/src/components/settings/SearchSection.dom.test.tsx b/packages/app/src/components/settings/SearchSection.dom.test.tsx index 29c7385c..d2dc2103 100644 --- a/packages/app/src/components/settings/SearchSection.dom.test.tsx +++ b/packages/app/src/components/settings/SearchSection.dom.test.tsx @@ -1,5 +1,10 @@ import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test'; -import type { Config, ConfigBinding, SemanticIndexStatus } from '@inkeep/open-knowledge-core'; +import { + type Config, + type ConfigBinding, + DEFAULT_EMBEDDINGS_BASE_URL, + type SemanticIndexStatus, +} from '@inkeep/open-knowledge-core'; import { cleanup, render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; @@ -44,8 +49,10 @@ mock.module('@/lib/config-provider', () => ({ const { SearchSection } = await import('./SearchSection'); -function configWithSemanticEnabled(enabled: boolean): Config { - return { search: { semantic: { enabled } } } as unknown as Config; +function configWithSemantic({ enabled, baseUrl }: { enabled: boolean; baseUrl?: string }): Config { + return { + search: { semantic: { enabled, ...(baseUrl ? { baseUrl } : {}) } }, + } as unknown as Config; } function makeBinding(): { binding: ConfigBinding; calls: unknown[] } { @@ -87,7 +94,7 @@ describe('SearchSection', () => { test('off: switch is unchecked, body says no content leaves, no coverage panel', () => { const { binding } = makeBinding(); mockProjectLocalBinding = binding; - mockProjectLocalConfig = configWithSemanticEnabled(false); + mockProjectLocalConfig = configWithSemantic({ enabled: false }); render(); @@ -115,7 +122,7 @@ describe('SearchSection', () => { const user = userEvent.setup(); const { binding, calls } = makeBinding(); mockProjectLocalBinding = binding; - mockProjectLocalConfig = configWithSemanticEnabled(false); + mockProjectLocalConfig = configWithSemantic({ enabled: false }); render(); @@ -133,7 +140,7 @@ describe('SearchSection', () => { const user = userEvent.setup(); const { binding, calls } = makeBinding(); mockProjectLocalBinding = binding; - mockProjectLocalConfig = configWithSemanticEnabled(true); + mockProjectLocalConfig = configWithSemantic({ enabled: true }); mockStatus = { enabled: true, keyPresent: true, @@ -155,7 +162,7 @@ describe('SearchSection', () => { test('on + keyed + warmed + capable: shows read-only coverage', async () => { const { binding } = makeBinding(); mockProjectLocalBinding = binding; - mockProjectLocalConfig = configWithSemanticEnabled(true); + mockProjectLocalConfig = configWithSemantic({ enabled: true }); mockStatus = { enabled: true, keyPresent: true, @@ -175,7 +182,7 @@ describe('SearchSection', () => { test('on + capable but nothing embedded yet: shows the lazy-warm hint', async () => { const { binding } = makeBinding(); mockProjectLocalBinding = binding; - mockProjectLocalConfig = configWithSemanticEnabled(true); + mockProjectLocalConfig = configWithSemantic({ enabled: true }); mockStatus = { enabled: true, keyPresent: true, @@ -195,7 +202,7 @@ describe('SearchSection', () => { test('on + NO key: shows the needs-a-key hint pointing at Account (instant, no warm)', async () => { const { binding } = makeBinding(); mockProjectLocalBinding = binding; - mockProjectLocalConfig = configWithSemanticEnabled(true); + mockProjectLocalConfig = configWithSemantic({ enabled: true }); mockStatus = { enabled: true, keyPresent: false, @@ -218,7 +225,7 @@ describe('SearchSection', () => { test('on + key present but provider rejected it: shows the provider-error hint', async () => { const { binding } = makeBinding(); mockProjectLocalBinding = binding; - mockProjectLocalConfig = configWithSemanticEnabled(true); + mockProjectLocalConfig = configWithSemantic({ enabled: true }); mockStatus = { enabled: true, keyPresent: true, @@ -239,7 +246,7 @@ describe('SearchSection', () => { test('on + keyed but not warmed: shows the pending state', async () => { const { binding } = makeBinding(); mockProjectLocalBinding = binding; - mockProjectLocalConfig = configWithSemanticEnabled(true); + mockProjectLocalConfig = configWithSemantic({ enabled: true }); mockStatus = { enabled: true, keyPresent: true, @@ -261,7 +268,7 @@ describe('SearchSection', () => { test('on but server not yet settled: shows the applying state', async () => { const { binding } = makeBinding(); mockProjectLocalBinding = binding; - mockProjectLocalConfig = configWithSemanticEnabled(true); + mockProjectLocalConfig = configWithSemantic({ enabled: true }); mockStatus = { enabled: false, keyPresent: false, @@ -285,7 +292,7 @@ describe('SearchSection', () => { const user = userEvent.setup(); const { binding, calls } = makeBinding(); mockProjectLocalBinding = binding; - mockProjectLocalConfig = configWithSemanticEnabled(false); + mockProjectLocalConfig = configWithSemantic({ enabled: false }); render(); @@ -305,7 +312,7 @@ describe('SearchSection', () => { patch: () => ({ ok: false, error: { code: 'noop', message: 'fail' } }), } as unknown as ConfigBinding; mockProjectLocalBinding = failBinding; - mockProjectLocalConfig = configWithSemanticEnabled(false); + mockProjectLocalConfig = configWithSemantic({ enabled: false }); render(); @@ -314,4 +321,54 @@ describe('SearchSection', () => { expect(await screen.findByTestId('settings-search-confirm')).toBeDefined(); }); + + test('shows the default endpoint when no custom provider is configured', () => { + const { binding } = makeBinding(); + mockProjectLocalBinding = binding; + mockProjectLocalConfig = configWithSemantic({ enabled: false }); + + render(); + + expect((screen.getByTestId('settings-search-base-url') as HTMLInputElement).value).toBe( + DEFAULT_EMBEDDINGS_BASE_URL, + ); + }); + + test('blurring the endpoint field writes the trimmed custom base URL', async () => { + const user = userEvent.setup(); + const { binding, calls } = makeBinding(); + mockProjectLocalBinding = binding; + mockProjectLocalConfig = configWithSemantic({ enabled: false }); + + render(); + + const input = screen.getByTestId('settings-search-base-url'); + await user.clear(input); + await user.type(input, ' https://azure.example.com/openai/v1/ '); + await user.tab(); + + expect(calls).toContainEqual({ + search: { semantic: { baseUrl: 'https://azure.example.com/openai/v1/' } }, + }); + }); + + test('clearing the endpoint field resets it to the default OpenAI endpoint', async () => { + const user = userEvent.setup(); + const { binding, calls } = makeBinding(); + mockProjectLocalBinding = binding; + mockProjectLocalConfig = configWithSemantic({ + enabled: false, + baseUrl: 'https://azure.example.com/openai/v1', + }); + + render(); + + const input = screen.getByTestId('settings-search-base-url'); + await user.clear(input); + await user.tab(); + + expect(calls).toContainEqual({ + search: { semantic: { baseUrl: DEFAULT_EMBEDDINGS_BASE_URL } }, + }); + }); }); diff --git a/packages/app/src/components/settings/SearchSection.tsx b/packages/app/src/components/settings/SearchSection.tsx index 99ef2fe0..95183e71 100644 --- a/packages/app/src/components/settings/SearchSection.tsx +++ b/packages/app/src/components/settings/SearchSection.tsx @@ -1,4 +1,4 @@ -import { humanFormat } from '@inkeep/open-knowledge-core'; +import { DEFAULT_EMBEDDINGS_BASE_URL, humanFormat } from '@inkeep/open-knowledge-core'; import { Trans, useLingui } from '@lingui/react/macro'; import { useEffect, useRef, useState } from 'react'; import { toast } from 'sonner'; @@ -13,6 +13,7 @@ import { Dialog as DialogRoot, DialogTitle, } from '@/components/ui/dialog'; +import { Input } from '@/components/ui/input'; import { Switch } from '@/components/ui/switch'; import { useSemanticSearchStatus } from '@/hooks/use-semantic-search-status'; import { useConfigContext } from '@/lib/config-provider'; @@ -33,6 +34,9 @@ export function SearchSection() { [], ); + const configuredBaseUrl = + projectLocalConfig?.search?.semantic?.baseUrl ?? DEFAULT_EMBEDDINGS_BASE_URL; + const enabled = projectLocalConfig?.search?.semantic?.enabled ?? false; const bindingReady = projectLocalSynced && projectLocalBinding !== null; @@ -61,6 +65,28 @@ export function SearchSection() { return true; } + function normalizeBaseUrl(next: string): string { + return next.trim() || DEFAULT_EMBEDDINGS_BASE_URL; + } + + function writeBaseUrl(next: string): boolean { + if (projectLocalBinding === null) { + toast.error(t`Search settings not yet loaded — try again in a moment`); + return false; + } + const normalized = normalizeBaseUrl(next); + if (normalized === configuredBaseUrl) return true; + const result = projectLocalBinding.patch({ search: { semantic: { baseUrl: normalized } } }); + if (!result.ok) { + const detail = humanFormat(result.error); + toast.error(t`Failed to update the embeddings endpoint — ${detail}`); + return false; + } + refresh(); + scheduleSettleRefresh(); + return true; + } + function onToggleRequest(next: boolean) { if (next) { setConfirmOpen(true); @@ -73,6 +99,11 @@ export function SearchSection() { if (write(true)) setConfirmOpen(false); } + function commitBaseUrlInput(input: HTMLInputElement): void { + const normalized = normalizeBaseUrl(input.value); + if (writeBaseUrl(input.value)) input.value = normalized; + } + const serverEnabled = status?.enabled ?? false; const keyPresent = status?.keyPresent ?? false; const ready = status?.ready ?? false; @@ -138,6 +169,41 @@ export function SearchSection() { ) : null} +
+
+ +

+ + Use the default OpenAI endpoint or override it with an OpenAI-compatible base URL for + Azure or a self-hosted provider. + +

+
+ commitBaseUrlInput(e.currentTarget)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault(); + commitBaseUrlInput(e.currentTarget); + } + }} + placeholder={DEFAULT_EMBEDDINGS_BASE_URL} + disabled={!bindingReady} + spellCheck={false} + autoComplete="off" + data-testid="settings-search-base-url" + className="h-8 font-mono text-sm" + /> +

+ Clear the field to reset back to the default OpenAI endpoint. +

+
+ ~/.ok/secrets.yml (readable only by your user account) and shared across all projects. Turn the feature on per project in This project → Search." - ], "FRYB1Q": ["/api/config returned a malformed body"], "FRqZ9v": ["Couldn't update skill: ", ["0"]], "FTUGgh": ["Enter a repository URL or owner/repo"], @@ -770,6 +767,7 @@ "KujtjC": ["Find"], "L-rMC9": ["Reset to default"], "L1ZE3M": ["Document statistics"], + "L1rRuU": ["Embeddings API endpoint"], "L3CEC1": ["this item"], "L4Cdd9": ["or create a new file <0/>"], "L6Hhh6": ["(root)"], @@ -1403,6 +1401,7 @@ "eG8Jsg": ["Toggle source tab focus mode"], "eJitUk": ["Upload from computer"], "eKThTQ": ["Failed to reorder"], + "eLl1VX": ["Failed to update the embeddings endpoint — ", ["detail"]], "eNQGRb": ["Inline LaTeX math rendered with KaTeX."], "ePK91l": ["Edit"], "eQkgKV": ["Installed"], @@ -1618,6 +1617,7 @@ "l8G3S9": ["Auto-sync is off — you don't have permission to push to this repo"], "lAiyU_": ["Only the current document uses <0>#", ["tag"], "."], "lGs_bQ": ["Page <0>*"], + "lHCR4I": ["Clear the field to reset back to the default OpenAI endpoint."], "lHwYMr": ["Server error: ", ["status"], " ", ["statusText"]], "lI0Td2": ["Use letters, digits, <0>_ and <1>- only."], "lIn9L9": ["Disable sync"], @@ -1785,6 +1785,9 @@ "pha01Y": ["Project-level pages with no incoming graph edges."], "pkAft1": ["Search failed."], "pmKdif": ["Failed: ", ["loadError"]], + "pmOGtg": [ + "Use the default OpenAI endpoint or override it with an OpenAI-compatible base URL for Azure or a self-hosted provider." + ], "pmUArF": ["Workspace"], "ppDgFi": ["Restart with this app's version"], "pzutoc": ["Italic"], @@ -2002,6 +2005,9 @@ "wXO4Tg": [["hrs"], "h ago"], "wbv9xO": ["Create new project"], "wbwHYD": ["Enable sync"], + "wccmX0": [ + "Your embeddings provider API key, used only to create embeddings of your content for semantic search. Stored once for this machine in <0>~/.ok/secrets.yml (readable only by your user account) and shared across all projects. Turn the feature on per project in This project → Search, where you can also override the endpoint." + ], "weP6me": ["Couldn't load docs — type @ again to retry"], "wlwXUk": [ "You picked ~/Downloads. Files there are usually transient — consider a stable folder instead." diff --git a/packages/app/src/locales/en/messages.po b/packages/app/src/locales/en/messages.po index f0a213a0..9025b572 100644 --- a/packages/app/src/locales/en/messages.po +++ b/packages/app/src/locales/en/messages.po @@ -1067,6 +1067,10 @@ msgstr "Clear" msgid "Clear {keyName}" msgstr "Clear {keyName}" +#: src/components/settings/SearchSection.tsx +msgid "Clear the field to reset back to the default OpenAI endpoint." +msgstr "Clear the field to reset back to the default OpenAI endpoint." + #: src/components/FolderTimelineCard.tsx msgid "cleared folder properties" msgstr "cleared folder properties" @@ -2325,6 +2329,10 @@ msgstr "Embed an external page in an inline iframe (docs, demos, Figma, CodeSand msgid "Embed an image with optional alt text." msgstr "Embed an image with optional alt text." +#: src/components/settings/SearchSection.tsx +msgid "Embeddings API endpoint" +msgstr "Embeddings API endpoint" + #: src/components/settings/SearchSection.tsx msgid "Embeddings are computed only when a search runs and are cached locally under <0>.ok/local." msgstr "Embeddings are computed only when a search runs and are cached locally under <0>.ok/local." @@ -2657,6 +2665,10 @@ msgstr "Failed to reorder" msgid "Failed to update property" msgstr "Failed to update property" +#: src/components/settings/SearchSection.tsx +msgid "Failed to update the embeddings endpoint — {detail}" +msgstr "Failed to update the embeddings endpoint — {detail}" + #: src/components/settings/SettingsDialogBody.tsx msgid "Failed to update the project sync default — {detail}" msgstr "Failed to update the project sync default — {detail}" @@ -6686,6 +6698,10 @@ msgstr "Use lowercase letters, digits, and <0>- only." msgid "Use lowercase letters, digits, and hyphens only." msgstr "Use lowercase letters, digits, and hyphens only." +#: src/components/settings/SearchSection.tsx +msgid "Use the default OpenAI endpoint or override it with an OpenAI-compatible base URL for Azure or a self-hosted provider." +msgstr "Use the default OpenAI endpoint or override it with an OpenAI-compatible base URL for Azure or a self-hosted provider." + #: src/components/settings/SettingsDialogShell.tsx msgid "User" msgstr "User" @@ -6914,6 +6930,10 @@ msgstr "You picked the filesystem root (/). Scaffolding here will scan every fil msgid "You picked your home directory. OpenKnowledge will index everything in your home tree — large and may surface personal files." msgstr "You picked your home directory. OpenKnowledge will index everything in your home tree — large and may surface personal files." +#: src/components/settings/EmbeddingsKeySection.tsx +msgid "Your embeddings provider API key, used only to create embeddings of your content for semantic search. Stored once for this machine in <0>~/.ok/secrets.yml (readable only by your user account) and shared across all projects. Turn the feature on per project in This project → Search, where you can also override the endpoint." +msgstr "Your embeddings provider API key, used only to create embeddings of your content for semantic search. Stored once for this machine in <0>~/.ok/secrets.yml (readable only by your user account) and shared across all projects. Turn the feature on per project in This project → Search, where you can also override the endpoint." + #: src/components/settings/SettingsDialogBody.tsx #: src/components/SyncStatusBadge.tsx msgid "Your GitHub session expired — sign in again to verify push access." @@ -6927,10 +6947,6 @@ msgstr "Your GitHub token is missing required scopes. Try signing in again." msgid "Your knowledge base is now on GitHub at <0>{ownerLogin}/{repoName}." msgstr "Your knowledge base is now on GitHub at <0>{ownerLogin}/{repoName}." -#: src/components/settings/EmbeddingsKeySection.tsx -msgid "Your OpenAI API key, used only to create embeddings of your content for semantic search. Stored once for this machine in <0>~/.ok/secrets.yml (readable only by your user account) and shared across all projects. Turn the feature on per project in This project → Search." -msgstr "Your OpenAI API key, used only to create embeddings of your content for semantic search. Stored once for this machine in <0>~/.ok/secrets.yml (readable only by your user account) and shared across all projects. Turn the feature on per project in This project → Search." - #: src/components/CloneDialog.tsx msgid "Your repositories" msgstr "Your repositories" diff --git a/packages/app/src/locales/pseudo/messages.json b/packages/app/src/locales/pseudo/messages.json index f3e59392..717e55fc 100644 --- a/packages/app/src/locales/pseudo/messages.json +++ b/packages/app/src/locales/pseudo/messages.json @@ -594,9 +594,6 @@ "FEr96N": ["Ţĥēḿē"], "FNTx6u": ["Śēàŕćĥ \"", ["semanticSubmitQuery"], "\" ƀŷ ḿēàńĩńĝ"], "FOGD7m": ["Ćŕēàţē śōḿēţĥĩńĝ ĝŕēàţ."], - "FOyCTr": [ - "Ŷōũŕ ŌƥēńÀĨ ÀƤĨ ķēŷ, ũśēď ōńĺŷ ţō ćŕēàţē ēḿƀēďďĩńĝś ōƒ ŷōũŕ ćōńţēńţ ƒōŕ śēḿàńţĩć śēàŕćĥ. Śţōŕēď ōńćē ƒōŕ ţĥĩś ḿàćĥĩńē ĩń <0>~/.ōķ/śēćŕēţś.ŷḿĺ (ŕēàďàƀĺē ōńĺŷ ƀŷ ŷōũŕ ũśēŕ àććōũńţ) àńď śĥàŕēď àćŕōśś àĺĺ ƥŕōĴēćţś. Ţũŕń ţĥē ƒēàţũŕē ōń ƥēŕ ƥŕōĴēćţ ĩń Ţĥĩś ƥŕōĴēćţ → Śēàŕćĥ." - ], "FRYB1Q": ["/àƥĩ/ćōńƒĩĝ ŕēţũŕńēď à ḿàĺƒōŕḿēď ƀōďŷ"], "FRqZ9v": ["Ćōũĺďń'ţ ũƥďàţē śķĩĺĺ: ", ["0"]], "FTUGgh": ["Ēńţēŕ à ŕēƥōśĩţōŕŷ ŨŔĹ ōŕ ōŵńēŕ/ŕēƥō"], @@ -770,6 +767,7 @@ "KujtjC": ["Ƒĩńď"], "L-rMC9": ["Ŕēśēţ ţō ďēƒàũĺţ"], "L1ZE3M": ["Ďōćũḿēńţ śţàţĩśţĩćś"], + "L1rRuU": ["Ēḿƀēďďĩńĝś ÀƤĨ ēńďƥōĩńţ"], "L3CEC1": ["ţĥĩś ĩţēḿ"], "L4Cdd9": ["ōŕ ćŕēàţē à ńēŵ ƒĩĺē <0/>"], "L6Hhh6": ["(ŕōōţ)"], @@ -1403,6 +1401,7 @@ "eG8Jsg": ["Ţōĝĝĺē śōũŕćē ţàƀ ƒōćũś ḿōďē"], "eJitUk": ["Ũƥĺōàď ƒŕōḿ ćōḿƥũţēŕ"], "eKThTQ": ["Ƒàĩĺēď ţō ŕēōŕďēŕ"], + "eLl1VX": ["Ƒàĩĺēď ţō ũƥďàţē ţĥē ēḿƀēďďĩńĝś ēńďƥōĩńţ — ", ["detail"]], "eNQGRb": ["Ĩńĺĩńē ĹàŢēX ḿàţĥ ŕēńďēŕēď ŵĩţĥ ĶàŢēX."], "ePK91l": ["Ēďĩţ"], "eQkgKV": ["Ĩńśţàĺĺēď"], @@ -1618,6 +1617,7 @@ "l8G3S9": ["Àũţō-śŷńć ĩś ōƒƒ — ŷōũ ďōń'ţ ĥàvē ƥēŕḿĩśśĩōń ţō ƥũśĥ ţō ţĥĩś ŕēƥō"], "lAiyU_": ["Ōńĺŷ ţĥē ćũŕŕēńţ ďōćũḿēńţ ũśēś <0>#", ["tag"], "."], "lGs_bQ": ["Ƥàĝē <0>*"], + "lHCR4I": ["Ćĺēàŕ ţĥē ƒĩēĺď ţō ŕēśēţ ƀàćķ ţō ţĥē ďēƒàũĺţ ŌƥēńÀĨ ēńďƥōĩńţ."], "lHwYMr": ["Śēŕvēŕ ēŕŕōŕ: ", ["status"], " ", ["statusText"]], "lI0Td2": ["Ũśē ĺēţţēŕś, ďĩĝĩţś, <0>_ àńď <1>- ōńĺŷ."], "lIn9L9": ["Ďĩśàƀĺē śŷńć"], @@ -1785,6 +1785,9 @@ "pha01Y": ["ƤŕōĴēćţ-ĺēvēĺ ƥàĝēś ŵĩţĥ ńō ĩńćōḿĩńĝ ĝŕàƥĥ ēďĝēś."], "pkAft1": ["Śēàŕćĥ ƒàĩĺēď."], "pmKdif": ["Ƒàĩĺēď: ", ["loadError"]], + "pmOGtg": [ + "Ũśē ţĥē ďēƒàũĺţ ŌƥēńÀĨ ēńďƥōĩńţ ōŕ ōvēŕŕĩďē ĩţ ŵĩţĥ àń ŌƥēńÀĨ-ćōḿƥàţĩƀĺē ƀàśē ŨŔĹ ƒōŕ Àźũŕē ōŕ à śēĺƒ-ĥōśţēď ƥŕōvĩďēŕ." + ], "pmUArF": ["Ŵōŕķśƥàćē"], "ppDgFi": ["Ŕēśţàŕţ ŵĩţĥ ţĥĩś àƥƥ'ś vēŕśĩōń"], "pzutoc": ["Ĩţàĺĩć"], @@ -2002,6 +2005,9 @@ "wXO4Tg": [["hrs"], "ĥ àĝō"], "wbv9xO": ["Ćŕēàţē ńēŵ ƥŕōĴēćţ"], "wbwHYD": ["Ēńàƀĺē śŷńć"], + "wccmX0": [ + "Ŷōũŕ ēḿƀēďďĩńĝś ƥŕōvĩďēŕ ÀƤĨ ķēŷ, ũśēď ōńĺŷ ţō ćŕēàţē ēḿƀēďďĩńĝś ōƒ ŷōũŕ ćōńţēńţ ƒōŕ śēḿàńţĩć śēàŕćĥ. Śţōŕēď ōńćē ƒōŕ ţĥĩś ḿàćĥĩńē ĩń <0>~/.ōķ/śēćŕēţś.ŷḿĺ (ŕēàďàƀĺē ōńĺŷ ƀŷ ŷōũŕ ũśēŕ àććōũńţ) àńď śĥàŕēď àćŕōśś àĺĺ ƥŕōĴēćţś. Ţũŕń ţĥē ƒēàţũŕē ōń ƥēŕ ƥŕōĴēćţ ĩń Ţĥĩś ƥŕōĴēćţ → Śēàŕćĥ, ŵĥēŕē ŷōũ ćàń àĺśō ōvēŕŕĩďē ţĥē ēńďƥōĩńţ." + ], "weP6me": ["Ćōũĺďń'ţ ĺōàď ďōćś — ţŷƥē @ àĝàĩń ţō ŕēţŕŷ"], "wlwXUk": [ "Ŷōũ ƥĩćķēď ~/Ďōŵńĺōàďś. Ƒĩĺēś ţĥēŕē àŕē ũśũàĺĺŷ ţŕàńśĩēńţ — ćōńśĩďēŕ à śţàƀĺē ƒōĺďēŕ ĩńśţēàď." diff --git a/packages/app/src/locales/pseudo/messages.po b/packages/app/src/locales/pseudo/messages.po index 2a401405..84d83e31 100644 --- a/packages/app/src/locales/pseudo/messages.po +++ b/packages/app/src/locales/pseudo/messages.po @@ -1062,6 +1062,10 @@ msgstr "" msgid "Clear {keyName}" msgstr "" +#: src/components/settings/SearchSection.tsx +msgid "Clear the field to reset back to the default OpenAI endpoint." +msgstr "" + #: src/components/FolderTimelineCard.tsx msgid "cleared folder properties" msgstr "" @@ -2320,6 +2324,10 @@ msgstr "" msgid "Embed an image with optional alt text." msgstr "" +#: src/components/settings/SearchSection.tsx +msgid "Embeddings API endpoint" +msgstr "" + #: src/components/settings/SearchSection.tsx msgid "Embeddings are computed only when a search runs and are cached locally under <0>.ok/local." msgstr "" @@ -2652,6 +2660,10 @@ msgstr "" msgid "Failed to update property" msgstr "" +#: src/components/settings/SearchSection.tsx +msgid "Failed to update the embeddings endpoint — {detail}" +msgstr "" + #: src/components/settings/SettingsDialogBody.tsx msgid "Failed to update the project sync default — {detail}" msgstr "" @@ -6681,6 +6693,10 @@ msgstr "" msgid "Use lowercase letters, digits, and hyphens only." msgstr "" +#: src/components/settings/SearchSection.tsx +msgid "Use the default OpenAI endpoint or override it with an OpenAI-compatible base URL for Azure or a self-hosted provider." +msgstr "" + #: src/components/settings/SettingsDialogShell.tsx msgid "User" msgstr "" @@ -6909,6 +6925,10 @@ msgstr "" msgid "You picked your home directory. OpenKnowledge will index everything in your home tree — large and may surface personal files." msgstr "" +#: src/components/settings/EmbeddingsKeySection.tsx +msgid "Your embeddings provider API key, used only to create embeddings of your content for semantic search. Stored once for this machine in <0>~/.ok/secrets.yml (readable only by your user account) and shared across all projects. Turn the feature on per project in This project → Search, where you can also override the endpoint." +msgstr "" + #: src/components/settings/SettingsDialogBody.tsx #: src/components/SyncStatusBadge.tsx msgid "Your GitHub session expired — sign in again to verify push access." @@ -6922,10 +6942,6 @@ msgstr "" msgid "Your knowledge base is now on GitHub at <0>{ownerLogin}/{repoName}." msgstr "" -#: src/components/settings/EmbeddingsKeySection.tsx -msgid "Your OpenAI API key, used only to create embeddings of your content for semantic search. Stored once for this machine in <0>~/.ok/secrets.yml (readable only by your user account) and shared across all projects. Turn the feature on per project in This project → Search." -msgstr "" - #: src/components/CloneDialog.tsx msgid "Your repositories" msgstr "" diff --git a/packages/cli/src/commands/embeddings/index.ts b/packages/cli/src/commands/embeddings/index.ts index 4f99133d..f106e1d0 100644 --- a/packages/cli/src/commands/embeddings/index.ts +++ b/packages/cli/src/commands/embeddings/index.ts @@ -23,7 +23,7 @@ async function readKey(): Promise { for await (const chunk of process.stdin) chunks.push(chunk as Buffer); return Buffer.concat(chunks).toString('utf-8').trim(); } - return (await password({ message: 'Enter OpenAI embeddings API key:' })).trim(); + return (await password({ message: 'Enter embeddings provider API key:' })).trim(); } function readSemanticConfig(projectDir: string) { @@ -57,7 +57,7 @@ async function fetchLiveCoverage( function setKeyCommand(): Command { return new Command('set-key') - .description('Store your OpenAI embeddings API key in ~/.ok/secrets.yml') + .description('Store your embeddings provider API key in ~/.ok/secrets.yml') .action(async () => { const key = await readKey(); if (!key) { @@ -67,9 +67,9 @@ function setKeyCommand(): Command { } await createEmbeddingsSecretStore().set(key); process.stderr.write( - '✓ OpenAI embeddings API key stored in ~/.ok/secrets.yml (0600, this machine only).\n' + + '✓ Embeddings provider API key stored in ~/.ok/secrets.yml (0600, this machine only).\n' + 'Now enable it per project — the easiest path is OK Desktop → Settings → This\n' + - 'project → Search (a toggle with an egress-confirmation prompt), or run\n' + + 'project → Search (toggle + endpoint settings), or run\n' + '`ok embeddings enable` in the project folder.\n', ); }); @@ -77,14 +77,14 @@ function setKeyCommand(): Command { function clearKeyCommand(): Command { return new Command('clear-key') - .description('Remove your stored OpenAI embeddings API key') + .description('Remove your stored embeddings provider API key') .action(async () => { const { touched } = await clearEmbeddingsKeyFromAllBackends(); if (touched.length === 0) { - process.stderr.write('No stored OpenAI embeddings key found.\n'); + process.stderr.write('No stored embeddings provider key found.\n'); return; } - process.stderr.write(`✓ OpenAI embeddings API key cleared (${touched.join(', ')}).\n`); + process.stderr.write(`✓ Embeddings provider API key cleared (${touched.join(', ')}).\n`); }); }