Mayo commited on
Commit
399a0cb
·
unverified ·
1 Parent(s): 36d8227

fix: keyring on Linux

Browse files
Cargo.lock CHANGED
@@ -1794,27 +1794,6 @@ version = "2.10.0"
1794
  source = "registry+https://github.com/rust-lang/crates.io-index"
1795
  checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea"
1796
 
1797
- [[package]]
1798
- name = "dbus"
1799
- version = "0.9.10"
1800
- source = "registry+https://github.com/rust-lang/crates.io-index"
1801
- checksum = "21b3aa68d7e7abee336255bd7248ea965cc393f3e70411135a6f6a4b651345d4"
1802
- dependencies = [
1803
- "libc",
1804
- "libdbus-sys",
1805
- "windows-sys 0.59.0",
1806
- ]
1807
-
1808
- [[package]]
1809
- name = "dbus-secret-service"
1810
- version = "4.1.0"
1811
- source = "registry+https://github.com/rust-lang/crates.io-index"
1812
- checksum = "708b509edf7889e53d7efb0ffadd994cc6c2345ccb62f55cfd6b0682165e4fa6"
1813
- dependencies = [
1814
- "dbus",
1815
- "zeroize",
1816
- ]
1817
-
1818
  [[package]]
1819
  name = "debugid"
1820
  version = "0.8.0"
@@ -4476,7 +4455,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
4476
  checksum = "eebcc3aff044e5944a8fbaf69eb277d11986064cba30c468730e8b9909fb551c"
4477
  dependencies = [
4478
  "byteorder",
4479
- "dbus-secret-service",
4480
  "log",
4481
  "security-framework 2.11.1",
4482
  "security-framework 3.7.0",
@@ -4678,7 +4656,6 @@ dependencies = [
4678
  "futures",
4679
  "image",
4680
  "imageproc",
4681
- "keyring",
4682
  "koharu-runtime",
4683
  "libloading 0.8.9",
4684
  "minijinja",
@@ -4860,15 +4837,6 @@ version = "0.2.185"
4860
  source = "registry+https://github.com/rust-lang/crates.io-index"
4861
  checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f"
4862
 
4863
- [[package]]
4864
- name = "libdbus-sys"
4865
- version = "0.2.7"
4866
- source = "registry+https://github.com/rust-lang/crates.io-index"
4867
- checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043"
4868
- dependencies = [
4869
- "pkg-config",
4870
- ]
4871
-
4872
  [[package]]
4873
  name = "libfuzzer-sys"
4874
  version = "0.4.12"
@@ -11229,20 +11197,6 @@ name = "zeroize"
11229
  version = "1.8.2"
11230
  source = "registry+https://github.com/rust-lang/crates.io-index"
11231
  checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
11232
- dependencies = [
11233
- "zeroize_derive",
11234
- ]
11235
-
11236
- [[package]]
11237
- name = "zeroize_derive"
11238
- version = "1.4.3"
11239
- source = "registry+https://github.com/rust-lang/crates.io-index"
11240
- checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e"
11241
- dependencies = [
11242
- "proc-macro2",
11243
- "quote",
11244
- "syn 2.0.117",
11245
- ]
11246
 
11247
  [[package]]
11248
  name = "zerotrie"
 
1794
  source = "registry+https://github.com/rust-lang/crates.io-index"
1795
  checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea"
1796
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1797
  [[package]]
1798
  name = "debugid"
1799
  version = "0.8.0"
 
4455
  checksum = "eebcc3aff044e5944a8fbaf69eb277d11986064cba30c468730e8b9909fb551c"
4456
  dependencies = [
4457
  "byteorder",
 
4458
  "log",
4459
  "security-framework 2.11.1",
4460
  "security-framework 3.7.0",
 
4656
  "futures",
4657
  "image",
4658
  "imageproc",
 
4659
  "koharu-runtime",
4660
  "libloading 0.8.9",
4661
  "minijinja",
 
4837
  source = "registry+https://github.com/rust-lang/crates.io-index"
4838
  checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f"
4839
 
 
 
 
 
 
 
 
 
 
4840
  [[package]]
4841
  name = "libfuzzer-sys"
4842
  version = "0.4.12"
 
11197
  version = "1.8.2"
11198
  source = "registry+https://github.com/rust-lang/crates.io-index"
11199
  checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11200
 
11201
  [[package]]
11202
  name = "zerotrie"
Dockerfile CHANGED
@@ -6,15 +6,15 @@ ARG DEBIAN_FRONTEND=noninteractive
6
 
7
  RUN apt-get update \
8
  && apt-get install -y --no-install-recommends \
9
- ca-certificates \
10
- curl \
11
- fonts-noto-cjk \
12
- libayatana-appindicator3-1 \
13
- libgomp1 \
14
- librsvg2-2 \
15
- libssl3 \
16
- libwebkit2gtk-4.1-0 \
17
- libxdo3 \
18
  && curl -fL "https://github.com/mayocream/koharu/releases/latest/download/koharu_linux_x64" -o /usr/local/bin/koharu \
19
  && chmod 0755 /usr/local/bin/koharu \
20
  && apt-get purge -y --auto-remove curl \
@@ -29,4 +29,4 @@ WORKDIR /home/koharu
29
  VOLUME ["/home/koharu/.local/share/Koharu"]
30
  EXPOSE 4000
31
 
32
- CMD ["/usr/local/bin/koharu", "--headless", "--no-keyring", "--host", "0.0.0.0", "--port", "4000"]
 
6
 
7
  RUN apt-get update \
8
  && apt-get install -y --no-install-recommends \
9
+ ca-certificates \
10
+ curl \
11
+ fonts-noto-cjk \
12
+ libayatana-appindicator3-1 \
13
+ libgomp1 \
14
+ librsvg2-2 \
15
+ libssl3 \
16
+ libwebkit2gtk-4.1-0 \
17
+ libxdo3 \
18
  && curl -fL "https://github.com/mayocream/koharu/releases/latest/download/koharu_linux_x64" -o /usr/local/bin/koharu \
19
  && chmod 0755 /usr/local/bin/koharu \
20
  && apt-get purge -y --auto-remove curl \
 
29
  VOLUME ["/home/koharu/.local/share/Koharu"]
30
  EXPOSE 4000
31
 
32
+ CMD ["/usr/local/bin/koharu", "--headless", "--host", "0.0.0.0", "--port", "4000"]
docs/en-US/how-to/run-gui-headless-and-mcp.md CHANGED
@@ -142,26 +142,8 @@ koharu.exe --debug
142
 
143
  On Windows, debug and headless runs also influence how Koharu attaches to or creates a console window.
144
 
145
- ## Running without keyring
146
 
147
- By default, Koharu stores API keys in the system keyring. In container or CI environments where there is no persistent keyring, you can pass `--no-keyring` to skip it and supply API keys through environment variables instead.
148
 
149
- The variable name for each provider follows the pattern `KOHARU_<PROVIDER>_API_KEY`:
150
-
151
- | Provider | Environment variable |
152
- | --- | --- |
153
- | OpenAI | `KOHARU_OPENAI_API_KEY` |
154
- | Gemini | `KOHARU_GEMINI_API_KEY` |
155
- | Claude | `KOHARU_CLAUDE_API_KEY` |
156
- | DeepSeek | `KOHARU_DEEPSEEK_API_KEY` |
157
- | OpenAI-compatible | `KOHARU_OPENAI_COMPATIBLE_API_KEY` |
158
-
159
- Hyphens in provider IDs are converted to underscores in the variable name.
160
-
161
- Example for a headless container run:
162
-
163
- ```bash
164
- KOHARU_OPENAI_API_KEY=sk-... koharu --port 9999 --headless --no-keyring
165
- ```
166
-
167
- When `--no-keyring` is active, calls to save an API key through the UI or HTTP API are ignored.
 
142
 
143
  On Windows, debug and headless runs also influence how Koharu attaches to or creates a console window.
144
 
145
+ ## Credential storage
146
 
147
+ By default, Koharu stores API keys outside `config.toml`. macOS and Windows use the system keyring. Linux uses Koharu's local filesystem credential store under the app data directory with owner-only file permissions; this Linux store relies on filesystem permissions rather than OS-level encryption.
148
 
149
+ Headless and container runs use the same credential storage behavior as the desktop app. For containers, keep the app data directory on a persistent volume if you want saved API keys to survive container replacement.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
docs/en-US/reference/cli.md CHANGED
@@ -31,7 +31,6 @@ koharu.exe [OPTIONS]
31
  | `--cpu` | Force CPU mode even when a GPU is available |
32
  | `-p`, `--port <PORT>` | Bind the local HTTP server to a specific `127.0.0.1` port instead of a random one |
33
  | `--headless` | Run without starting the desktop GUI |
34
- | `--no-keyring` | Run without keyring and use environment variables instead |
35
  | `--debug` | Enable debug-oriented console output |
36
 
37
  ## Behavior notes
@@ -42,7 +41,6 @@ Some flags affect more than startup appearance:
42
  - with `--headless`, Koharu skips the Tauri window but still serves the Web UI and API
43
  - with `--download`, Koharu exits after dependency prefetch and does not stay running
44
  - with `--cpu`, both the vision stack and local LLM path avoid GPU acceleration
45
- - with `--no-keyring`, Koharu skips all keyring operations, API keys must be set via environment variables
46
 
47
  When a fixed port is set, the main local endpoints are:
48
 
@@ -87,9 +85,3 @@ Start with explicit debug logging:
87
  ```bash
88
  koharu --debug
89
  ```
90
-
91
- Use without keyring:
92
-
93
- ```bash
94
- KOHARU_OPENAI_API_KEY=[key] koharu --no-keyring
95
- ```
 
31
  | `--cpu` | Force CPU mode even when a GPU is available |
32
  | `-p`, `--port <PORT>` | Bind the local HTTP server to a specific `127.0.0.1` port instead of a random one |
33
  | `--headless` | Run without starting the desktop GUI |
 
34
  | `--debug` | Enable debug-oriented console output |
35
 
36
  ## Behavior notes
 
41
  - with `--headless`, Koharu skips the Tauri window but still serves the Web UI and API
42
  - with `--download`, Koharu exits after dependency prefetch and does not stay running
43
  - with `--cpu`, both the vision stack and local LLM path avoid GPU acceleration
 
44
 
45
  When a fixed port is set, the main local endpoints are:
46
 
 
85
  ```bash
86
  koharu --debug
87
  ```
 
 
 
 
 
 
docs/en-US/reference/settings.md CHANGED
@@ -51,14 +51,18 @@ The `API Keys` tab currently covers these built-in providers:
51
 
52
  Current behavior:
53
 
54
- - provider API keys are stored through the system keyring rather than plain text in `config.toml`
 
 
55
  - provider base URLs are stored in the app config
56
  - `OpenAI Compatible` requires a custom `Base URL`
57
  - the app discovers models dynamically for `OpenAI Compatible` by querying the configured endpoint
58
- - clearing a key removes it from the keyring
59
 
60
  The API response intentionally redacts saved keys rather than returning the raw secret.
61
 
 
 
62
  ## Runtime
63
 
64
  The `Runtime` tab groups restart-required settings that affect the shared local runtime:
@@ -93,7 +97,7 @@ In packaged app mode, the version check compares the local app version against t
93
  The current settings behavior is split across storage layers:
94
 
95
  - `config.toml` stores shared app config such as `data`, `http`, `pipeline`, and provider `baseUrl`
96
- - provider API keys are stored through the system keyring
97
  - theme, language, and rendering-font preferences are stored in the frontend preferences layer
98
 
99
  That means clearing frontend preferences is not the same as clearing saved provider API keys or shared runtime config.
 
51
 
52
  Current behavior:
53
 
54
+ - provider API keys are not written to `config.toml`
55
+ - on macOS and Windows, provider API keys are stored through the system keyring
56
+ - on Linux, provider API keys are stored in Koharu's local filesystem credential store under the app data directory with owner-only file permissions
57
  - provider base URLs are stored in the app config
58
  - `OpenAI Compatible` requires a custom `Base URL`
59
  - the app discovers models dynamically for `OpenAI Compatible` by querying the configured endpoint
60
+ - clearing a key removes it from credential storage
61
 
62
  The API response intentionally redacts saved keys rather than returning the raw secret.
63
 
64
+ The Linux filesystem credential store relies on local filesystem permissions rather than OS-level encryption.
65
+
66
  ## Runtime
67
 
68
  The `Runtime` tab groups restart-required settings that affect the shared local runtime:
 
97
  The current settings behavior is split across storage layers:
98
 
99
  - `config.toml` stores shared app config such as `data`, `http`, `pipeline`, and provider `baseUrl`
100
+ - provider API keys are stored separately from `config.toml` through the platform credential store described above
101
  - theme, language, and rendering-font preferences are stored in the frontend preferences layer
102
 
103
  That means clearing frontend preferences is not the same as clearing saved provider API keys or shared runtime config.
docs/ja-JP/how-to/run-gui-headless-and-mcp.md CHANGED
@@ -141,3 +141,9 @@ koharu.exe --debug
141
  ```
142
 
143
  Windows では、debug 実行と headless 実行の組み合わせにより、Koharu がコンソールウィンドウにアタッチするか、新しく作るかが変わります。
 
 
 
 
 
 
 
141
  ```
142
 
143
  Windows では、debug 実行と headless 実行の組み合わせにより、Koharu がコンソールウィンドウにアタッチするか、新しく作るかが変わります。
144
+
145
+ ## 認証情報ストレージ
146
+
147
+ Koharu は API キーを `config.toml` の外に保存します。macOS と Windows ではシステム keyring を使います。Linux ではアプリデータディレクトリ配下の Koharu ローカルファイルシステム認証情報ストアを使い、所有ユーザーのみが読める権限を設定します。この Linux ストアは OS レベルの暗号化ではなく、ファイルシステム権限に依存します。
148
+
149
+ Headless 実行やコンテナ実行でも、デスクトップアプリと同じ認証情報ストレージを使います。コンテナを置き換えた後も保存済み API キーを維持したい場合は、アプリデータディレクトリを永続 volume に置いてください。
docs/ja-JP/reference/settings.md CHANGED
@@ -51,14 +51,18 @@ title: 設定リファレンス
51
 
52
  現在の挙動:
53
 
54
- - provider の API キーは `config.toml` ではなくシステム keyring 保存されま
 
 
55
  - provider の `Base URL` は共有アプリ設定に保存されます
56
  - `OpenAI Compatible` ではカスタム `Base URL` が必須です
57
  - `OpenAI Compatible` のモデル一覧は設定済みエンドポイントへの問い合わせで動的取得されます
58
- - キーをクリアすると keyring から削除されます
59
 
60
  API レスポンスでは保存済みキーは生値ではなく、マスク済みの値として返されます。
61
 
 
 
62
  ## Runtime
63
 
64
  `Runtime` タブでは、共有ローカルランタイムに影響する再起動必須の設定をまとめています。
@@ -93,7 +97,7 @@ API レスポンスでは保存済みキーは生値ではなく、マスク済
93
  現在の設定保存は複数の層に分かれています。
94
 
95
  - `config.toml` には `data`、`http`、`pipeline`、provider の `baseUrl` など共有設定が保存されます
96
- - provider API キーはシステkeyring に保存されます
97
  - テーマ、言語、描画フォントはフロントエンドの preferences 層に保存されます
98
 
99
  つまり、フロントエンドの preferences を消しても、保存済みの provider API キーや共有ランタイム設定までは消えません。
 
51
 
52
  現在の挙動:
53
 
54
+ - provider の API キーは `config.toml` には書き込まれません
55
+ - macOS と Windows では、provider の API キーはシステム keyring に保存されます
56
+ - Linux では、provider の API キーはアプリデータディレクトリ配下の Koharu ローカルファイルシステム認証情報ストアに保存され、所有ユーザーのみが読める権限が設定されます
57
  - provider の `Base URL` は共有アプリ設定に保存されます
58
  - `OpenAI Compatible` ではカスタム `Base URL` が必須です
59
  - `OpenAI Compatible` のモデル一覧は設定済みエンドポイントへの問い合わせで動的取得されます
60
+ - キーをクリアすると認証情報ストレージから削除されます
61
 
62
  API レスポンスでは保存済みキーは生値ではなく、マスク済みの値として返されます。
63
 
64
+ Linux のファイルシステム認証情報ストアは、OS レベルの暗号化ではなくローカルファイルシステム権限に依存します。
65
+
66
  ## Runtime
67
 
68
  `Runtime` タブでは、共有ローカルランタイムに影響する再起動必須の設定をまとめています。
 
97
  現在の設定保存は複数の層に分かれています。
98
 
99
  - `config.toml` には `data`、`http`、`pipeline`、provider の `baseUrl` など共有設定が保存されます
100
+ - provider API キーは、上記のプラットフォー認証情報ストレージを通じて `config.toml` とは別に保存されます
101
  - テーマ、言語、描画フォントはフロントエンドの preferences 層に保存されます
102
 
103
  つまり、フロントエンドの preferences を消しても、保存済みの provider API キーや共有ランタイム設定までは消えません。
docs/pt-BR/how-to/run-gui-headless-and-mcp.md CHANGED
@@ -142,26 +142,8 @@ koharu.exe --debug
142
 
143
  No Windows, execuções de debug e headless também influenciam como o Koharu anexa ou cria uma janela de console.
144
 
145
- ## Rodando sem keyring
146
 
147
- Por padrão, o Koharu armazena API keys no keyring do sistema. Em ambientes de container ou CI onde não existe um keyring persistente, você pode passar `--no-keyring` para pular essa etapa e fornecer as API keys por variáveis de ambiente.
148
 
149
- O nome da variável para cada provider segue o padrão `KOHARU_<PROVIDER>_API_KEY`:
150
-
151
- | Provider | Variável de ambiente |
152
- | --- | --- |
153
- | OpenAI | `KOHARU_OPENAI_API_KEY` |
154
- | Gemini | `KOHARU_GEMINI_API_KEY` |
155
- | Claude | `KOHARU_CLAUDE_API_KEY` |
156
- | DeepSeek | `KOHARU_DEEPSEEK_API_KEY` |
157
- | OpenAI-compatible | `KOHARU_OPENAI_COMPATIBLE_API_KEY` |
158
-
159
- Hífens nos IDs de provider são convertidos em underscores no nome da variável.
160
-
161
- Exemplo para uma execução headless em container:
162
-
163
- ```bash
164
- KOHARU_OPENAI_API_KEY=sk-... koharu --port 9999 --headless --no-keyring
165
- ```
166
-
167
- Quando `--no-keyring` está ativo, chamadas para salvar uma API key pela UI ou pela HTTP API são ignoradas.
 
142
 
143
  No Windows, execuções de debug e headless também influenciam como o Koharu anexa ou cria uma janela de console.
144
 
145
+ ## Armazenamento de credenciais
146
 
147
+ Por padrão, o Koharu armazena API keys fora de `config.toml`. macOS e Windows usam o keyring do sistema. Linux usa o armazenamento local de credenciais do Koharu no diretório de dados do app com permissões somente para o usuário dono; esse armazenamento no Linux depende das permissões do filesystem em vez de criptografia em nível de sistema operacional.
148
 
149
+ Execuções headless e em container usam o mesmo comportamento de armazenamento de credenciais do app desktop. Em containers, mantenha o diretório de dados do app em um volume persistente se quiser que as API keys salvas sobrevivam à substituição do container.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
docs/pt-BR/reference/cli.md CHANGED
@@ -31,7 +31,6 @@ koharu.exe [OPTIONS]
31
  | `--cpu` | Força o modo CPU mesmo quando uma GPU está disponível |
32
  | `-p`, `--port <PORT>` | Vincula o servidor HTTP local a uma porta `127.0.0.1` específica em vez de uma aleatória |
33
  | `--headless` | Executa sem iniciar a GUI desktop |
34
- | `--no-keyring` | Executa sem keyring e usa variáveis de ambiente no lugar |
35
  | `--debug` | Habilita saída de console orientada a debug |
36
 
37
  ## Notas de comportamento
@@ -42,7 +41,6 @@ Algumas flags afetam mais do que apenas a aparência inicial:
42
  - com `--headless`, o Koharu pula a janela do Tauri mas ainda serve a Web UI e a API
43
  - com `--download`, o Koharu encerra após o prefetch de dependências e não permanece em execução
44
  - com `--cpu`, tanto a stack de visão quanto o caminho do LLM local evitam aceleração por GPU
45
- - com `--no-keyring`, o Koharu pula todas as operações de keyring; as chaves de API devem ser definidas por variáveis de ambiente
46
 
47
  Quando uma porta fixa está definida, os principais endpoints locais são:
48
 
@@ -87,9 +85,3 @@ Iniciar com logging explícito de debug:
87
  ```bash
88
  koharu --debug
89
  ```
90
-
91
- Uso sem keyring:
92
-
93
- ```bash
94
- KOHARU_OPENAI_API_KEY=[key] koharu --no-keyring
95
- ```
 
31
  | `--cpu` | Força o modo CPU mesmo quando uma GPU está disponível |
32
  | `-p`, `--port <PORT>` | Vincula o servidor HTTP local a uma porta `127.0.0.1` específica em vez de uma aleatória |
33
  | `--headless` | Executa sem iniciar a GUI desktop |
 
34
  | `--debug` | Habilita saída de console orientada a debug |
35
 
36
  ## Notas de comportamento
 
41
  - com `--headless`, o Koharu pula a janela do Tauri mas ainda serve a Web UI e a API
42
  - com `--download`, o Koharu encerra após o prefetch de dependências e não permanece em execução
43
  - com `--cpu`, tanto a stack de visão quanto o caminho do LLM local evitam aceleração por GPU
 
44
 
45
  Quando uma porta fixa está definida, os principais endpoints locais são:
46
 
 
85
  ```bash
86
  koharu --debug
87
  ```
 
 
 
 
 
 
docs/pt-BR/reference/settings.md CHANGED
@@ -51,14 +51,18 @@ A aba `API Keys` cobre atualmente estes provedores embutidos:
51
 
52
  Comportamento atual:
53
 
54
- - as chaves de API dos provedores são armazenadas via keyring do sistema em vez de texto plano em `config.toml`
 
 
55
  - as base URLs dos provedores são armazenadas na config do app
56
  - `OpenAI Compatible` requer uma `Base URL` customizada
57
  - o app descobre modelos dinamicamente para `OpenAI Compatible` consultando o endpoint configurado
58
- - limpar uma chave a remove do keyring
59
 
60
  O response da API intencionalmente redacta as chaves salvas em vez de retornar o segredo bruto.
61
 
 
 
62
  ## Runtime
63
 
64
  A aba `Runtime` agrupa configurações que exigem reinicialização e afetam o runtime local compartilhado:
@@ -93,7 +97,7 @@ No modo de app empacotado, a verificação de versão compara a versão local do
93
  O comportamento atual das configurações é dividido em camadas de armazenamento:
94
 
95
  - `config.toml` armazena a config compartilhada do app, como `data`, `http`, `pipeline` e `baseUrl` dos provedores
96
- - as chaves de API dos provedores são armazenadas via keyring do sistema
97
  - as preferências de tema, idioma e fonte de renderização são armazenadas na camada de preferências do frontend
98
 
99
  Ou seja, limpar as preferências do frontend não é o mesmo que limpar as chaves de API salvas dos provedores ou a config compartilhada de runtime.
 
51
 
52
  Comportamento atual:
53
 
54
+ - as chaves de API dos provedores não são escritas em `config.toml`
55
+ - no macOS e no Windows, as chaves de API dos provedores são armazenadas pelo keyring do sistema
56
+ - no Linux, as chaves de API dos provedores são armazenadas no armazenamento local de credenciais do Koharu sob o diretório de dados do app com permissões somente para o usuário dono
57
  - as base URLs dos provedores são armazenadas na config do app
58
  - `OpenAI Compatible` requer uma `Base URL` customizada
59
  - o app descobre modelos dinamicamente para `OpenAI Compatible` consultando o endpoint configurado
60
+ - limpar uma chave a remove do armazenamento de credenciais
61
 
62
  O response da API intencionalmente redacta as chaves salvas em vez de retornar o segredo bruto.
63
 
64
+ O armazenamento local de credenciais no Linux depende das permissões do filesystem em vez de criptografia em nível de sistema operacional.
65
+
66
  ## Runtime
67
 
68
  A aba `Runtime` agrupa configurações que exigem reinicialização e afetam o runtime local compartilhado:
 
97
  O comportamento atual das configurações é dividido em camadas de armazenamento:
98
 
99
  - `config.toml` armazena a config compartilhada do app, como `data`, `http`, `pipeline` e `baseUrl` dos provedores
100
+ - as chaves de API dos provedores são armazenadas separadamente de `config.toml` pelo armazenamento de credenciais da plataforma descrito acima
101
  - as preferências de tema, idioma e fonte de renderização são armazenadas na camada de preferências do frontend
102
 
103
  Ou seja, limpar as preferências do frontend não é o mesmo que limpar as chaves de API salvas dos provedores ou a config compartilhada de runtime.
docs/zh-CN/how-to/run-gui-headless-and-mcp.md CHANGED
@@ -141,3 +141,9 @@ koharu.exe --debug
141
  ```
142
 
143
  在 Windows 上,debug 与 headless 运行方式还会影响 Koharu 如何附加到现有控制台,或创建新的控制台窗口。
 
 
 
 
 
 
 
141
  ```
142
 
143
  在 Windows 上,debug 与 headless 运行方式还会影响 Koharu 如何附加到现有控制台,或创建新的控制台窗口。
144
+
145
+ ## 凭据存储
146
+
147
+ Koharu 会把 API key 存储在 `config.toml` 之外。macOS 和 Windows 使用系统 keyring。Linux 使用应用数据目录下的 Koharu 本地文件系统凭据存储,并设置仅所有者可访问的文件权限;这个 Linux 存储依赖文件系统权限,而不是操作系统级加密。
148
+
149
+ Headless 和容器运行使用与桌面应用相同的凭据存储行为。如果你希望保存的 API key 在容器替换后继续存在,请把应用数据目录放在持久化 volume 上。
docs/zh-CN/reference/settings.md CHANGED
@@ -51,14 +51,18 @@ title: 设置参考
51
 
52
  当前行为:
53
 
54
- - 提供方 API key 存储在系统 keyring 中,而是明文写入 `config.toml`
 
 
55
  - 提供方的 `Base URL` 保存在共享应用配置中
56
  - `OpenAI Compatible` 需要自定义 `Base URL`
57
  - `OpenAI Compatible` 的模型列表会通过查询已配置端点动态发现
58
- - 清除密钥会把它从 keyring 中删除
59
 
60
  API 响应不会返回原始密钥,而是返回已遮罩的值。
61
 
 
 
62
  ## Runtime
63
 
64
  `Runtime` 标签页集中放置会影响共享本地运行时、且需要重启后生效的设置:
@@ -93,7 +97,7 @@ API 响应不会返回原始密钥,而是返回已遮罩的值。
93
  当前设置数据分布在多个存储层中:
94
 
95
  - `config.toml` 保存 `data`、`http`、`pipeline` 以及提供方 `baseUrl` 等共享配置
96
- - 提供方 API key 存储在系统 keyring
97
  - 主题、语言和渲染字体存储在前端 preferences 层中
98
 
99
  因此,清除前端 preferences 并不等于清除已保存的提供方 API key 或共享运行时配置。
 
51
 
52
  当前行为:
53
 
54
+ - 提供方 API key 不写入 `config.toml`
55
+ - 在 macOS 和 Windows 上,提供方 API key 存储在系统 keyring 中
56
+ - 在 Linux 上,提供方 API key 存储在应用数据目录下的 Koharu 本地文件系统凭据存储中,并使用仅所有者可访问的文件权限
57
  - 提供方的 `Base URL` 保存在共享应用配置中
58
  - `OpenAI Compatible` 需要自定义 `Base URL`
59
  - `OpenAI Compatible` 的模型列表会通过查询已配置端点动态发现
60
+ - 清除密钥会把它从凭据存储中删除
61
 
62
  API 响应不会返回原始密钥,而是返回已遮罩的值。
63
 
64
+ Linux 文件系统凭据存储依赖本地文件系统权限,而不是操作系统级加密。
65
+
66
  ## Runtime
67
 
68
  `Runtime` 标签页集中放置会影响共享本地运行时、且需要重启后生效的设置:
 
97
  当前设置数据分布在多个存储层中:
98
 
99
  - `config.toml` 保存 `data`、`http`、`pipeline` 以及提供方 `baseUrl` 等共享配置
100
+ - 提供方 API key 通过上文所述的平台凭据存储 `config.toml` 分开保存
101
  - 主题、语言和渲染字体存储在前端 preferences 层中
102
 
103
  因此,清除前端 preferences 并不等于清除已保存的提供方 API key 或共享运行时配置。
koharu-app/src/config.rs CHANGED
@@ -113,9 +113,8 @@ pub struct ProviderConfig {
113
  pub id: String,
114
  #[serde(skip_serializing_if = "Option::is_none")]
115
  pub base_url: Option<String>,
116
- /// Populated from the keyring on `load()`, never written to config.toml.
117
  /// Serializes as `"[REDACTED]"` in API responses.
118
- /// Populated from keyring on `load()`. Serializes as `"[REDACTED]"`.
119
  #[serde(default, skip_serializing_if = "Option::is_none")]
120
  #[schema(value_type = Option<String>)]
121
  pub api_key: Option<RedactedSecret>,
@@ -163,7 +162,7 @@ pub fn load() -> Result<AppConfig> {
163
  save(&config)?;
164
  }
165
 
166
- // Populate api_key from the keyring for every known provider.
167
  for provider in &mut config.providers {
168
  if let Ok(Some(key)) = get_saved_api_key(&provider.id)
169
  && !key.trim().is_empty()
@@ -348,12 +347,12 @@ fn validate_engine_name(
348
  }
349
 
350
  // ---------------------------------------------------------------------------
351
- // Secret (keyring) handling
352
  // ---------------------------------------------------------------------------
353
 
354
- /// Sync api_key fields to the keyring.
355
- /// - `Some(RedactedSecret)` with value != "[REDACTED]" → save to keyring
356
- /// - `None` → clear from keyring
357
  /// - `Some(RedactedSecret)` with value == "[REDACTED]" → unchanged
358
  pub fn sync_secrets(config: &AppConfig) -> Result<()> {
359
  for provider in &config.providers {
 
113
  pub id: String,
114
  #[serde(skip_serializing_if = "Option::is_none")]
115
  pub base_url: Option<String>,
116
+ /// Populated from credential storage on `load()`, never written to config.toml.
117
  /// Serializes as `"[REDACTED]"` in API responses.
 
118
  #[serde(default, skip_serializing_if = "Option::is_none")]
119
  #[schema(value_type = Option<String>)]
120
  pub api_key: Option<RedactedSecret>,
 
162
  save(&config)?;
163
  }
164
 
165
+ // Populate api_key from credential storage for every known provider.
166
  for provider in &mut config.providers {
167
  if let Ok(Some(key)) = get_saved_api_key(&provider.id)
168
  && !key.trim().is_empty()
 
347
  }
348
 
349
  // ---------------------------------------------------------------------------
350
+ // Secret handling
351
  // ---------------------------------------------------------------------------
352
 
353
+ /// Sync api_key fields to credential storage.
354
+ /// - `Some(RedactedSecret)` with value != "[REDACTED]" → save to credential storage
355
+ /// - `None` → clear from credential storage
356
  /// - `Some(RedactedSecret)` with value == "[REDACTED]" → unchanged
357
  pub fn sync_secrets(config: &AppConfig) -> Result<()> {
358
  for provider in &config.providers {
koharu-llm/Cargo.toml CHANGED
@@ -62,9 +62,6 @@ keyring = { workspace = true, features = ["windows-native"] }
62
  [target.'cfg(target_os = "macos")'.dependencies]
63
  keyring = { workspace = true, features = ["apple-native"] }
64
 
65
- [target.'cfg(target_os = "linux")'.dependencies]
66
- keyring = { workspace = true, features = ["sync-secret-service"] }
67
-
68
  [build-dependencies]
69
  anyhow = { workspace = true }
70
  bindgen = "0.72.1"
 
62
  [target.'cfg(target_os = "macos")'.dependencies]
63
  keyring = { workspace = true, features = ["apple-native"] }
64
 
 
 
 
65
  [build-dependencies]
66
  anyhow = { workspace = true }
67
  bindgen = "0.72.1"
koharu-llm/src/providers/credentials.rs ADDED
@@ -0,0 +1,247 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ use std::sync::Once;
2
+
3
+ use keyring::Entry;
4
+
5
+ const API_KEY_SERVICE: &str = "koharu";
6
+
7
+ static INIT_CREDENTIAL_STORE: Once = Once::new();
8
+
9
+ pub fn get_saved_api_key(provider: &str) -> anyhow::Result<Option<String>> {
10
+ let entry = provider_entry(provider)?;
11
+ match entry.get_password() {
12
+ Ok(value) => Ok(Some(value)),
13
+ Err(keyring::Error::NoEntry) => Ok(None),
14
+ Err(err) => Err(err.into()),
15
+ }
16
+ }
17
+
18
+ pub fn set_saved_api_key(provider: &str, api_key: &str) -> anyhow::Result<()> {
19
+ let entry = provider_entry(provider)?;
20
+ if api_key.trim().is_empty() {
21
+ return match entry.delete_credential() {
22
+ Ok(()) | Err(keyring::Error::NoEntry) => Ok(()),
23
+ Err(err) => Err(err.into()),
24
+ };
25
+ }
26
+
27
+ entry.set_password(api_key)?;
28
+ Ok(())
29
+ }
30
+
31
+ fn provider_entry(provider: &str) -> anyhow::Result<Entry> {
32
+ INIT_CREDENTIAL_STORE.call_once(configure_platform_store);
33
+
34
+ let username = format!("llm_provider_api_key_{provider}");
35
+ Ok(Entry::new(API_KEY_SERVICE, &username)?)
36
+ }
37
+
38
+ #[cfg(target_os = "linux")]
39
+ fn configure_platform_store() {
40
+ let root = koharu_runtime::default_app_data_root()
41
+ .as_std_path()
42
+ .join("secrets")
43
+ .join("keyring");
44
+ keyring::set_default_credential_builder(Box::new(filesystem::Builder::new(root)));
45
+ }
46
+
47
+ #[cfg(not(target_os = "linux"))]
48
+ fn configure_platform_store() {}
49
+
50
+ #[cfg(any(target_os = "linux", test))]
51
+ mod filesystem {
52
+ use std::fmt::Write as _;
53
+ use std::fs;
54
+ use std::path::{Path, PathBuf};
55
+ use std::sync::atomic::{AtomicU64, Ordering};
56
+
57
+ use keyring::credential::{Credential, CredentialApi, CredentialBuilderApi};
58
+ use keyring::{Error, Result};
59
+
60
+ static TEMP_ID: AtomicU64 = AtomicU64::new(0);
61
+
62
+ #[derive(Debug)]
63
+ pub(super) struct Builder {
64
+ root: PathBuf,
65
+ }
66
+
67
+ impl Builder {
68
+ pub(super) fn new(root: impl Into<PathBuf>) -> Self {
69
+ Self { root: root.into() }
70
+ }
71
+ }
72
+
73
+ impl CredentialBuilderApi for Builder {
74
+ fn build(
75
+ &self,
76
+ target: Option<&str>,
77
+ service: &str,
78
+ user: &str,
79
+ ) -> Result<Box<Credential>> {
80
+ Ok(Box::new(FileCredential {
81
+ root: self.root.clone(),
82
+ name: file_name(target, service, user),
83
+ }))
84
+ }
85
+
86
+ fn as_any(&self) -> &dyn std::any::Any {
87
+ self
88
+ }
89
+ }
90
+
91
+ #[derive(Debug)]
92
+ struct FileCredential {
93
+ root: PathBuf,
94
+ name: String,
95
+ }
96
+
97
+ impl FileCredential {
98
+ fn path(&self) -> PathBuf {
99
+ self.root.join(&self.name)
100
+ }
101
+
102
+ fn temp_path(&self) -> PathBuf {
103
+ self.root.join(format!(
104
+ "{}.tmp-{}-{}",
105
+ self.name,
106
+ std::process::id(),
107
+ TEMP_ID.fetch_add(1, Ordering::Relaxed)
108
+ ))
109
+ }
110
+ }
111
+
112
+ impl CredentialApi for FileCredential {
113
+ fn set_secret(&self, secret: &[u8]) -> Result<()> {
114
+ fs::create_dir_all(&self.root).map_err(storage_error)?;
115
+ set_mode(&self.root, 0o700)?;
116
+
117
+ let path = self.path();
118
+ let temp_path = self.temp_path();
119
+ fs::write(&temp_path, secret).map_err(storage_error)?;
120
+ set_mode(&temp_path, 0o600)?;
121
+
122
+ fs::rename(&temp_path, &path).or_else(|err| {
123
+ let _ = fs::remove_file(&temp_path);
124
+ Err(storage_error(err))
125
+ })?;
126
+ set_mode(&path, 0o600)
127
+ }
128
+
129
+ fn get_secret(&self) -> Result<Vec<u8>> {
130
+ fs::read(self.path()).map_err(|err| match err.kind() {
131
+ std::io::ErrorKind::NotFound => Error::NoEntry,
132
+ _ => storage_error(err),
133
+ })
134
+ }
135
+
136
+ fn delete_credential(&self) -> Result<()> {
137
+ fs::remove_file(self.path()).map_err(|err| match err.kind() {
138
+ std::io::ErrorKind::NotFound => Error::NoEntry,
139
+ _ => storage_error(err),
140
+ })
141
+ }
142
+
143
+ fn as_any(&self) -> &dyn std::any::Any {
144
+ self
145
+ }
146
+ }
147
+
148
+ #[cfg(unix)]
149
+ fn set_mode(path: &Path, mode: u32) -> Result<()> {
150
+ use std::os::unix::fs::PermissionsExt;
151
+
152
+ fs::set_permissions(path, fs::Permissions::from_mode(mode)).map_err(storage_error)
153
+ }
154
+
155
+ #[cfg(not(unix))]
156
+ fn set_mode(_path: &Path, _mode: u32) -> Result<()> {
157
+ Ok(())
158
+ }
159
+
160
+ fn storage_error(err: std::io::Error) -> Error {
161
+ Error::NoStorageAccess(Box::new(err))
162
+ }
163
+
164
+ fn file_name(target: Option<&str>, service: &str, user: &str) -> String {
165
+ let target = target
166
+ .map(|value| format!("some-{}", encode(value)))
167
+ .unwrap_or_else(|| "none".to_string());
168
+ format!(
169
+ "v1-target-{target}--service-{}--user-{}.secret",
170
+ encode(service),
171
+ encode(user)
172
+ )
173
+ }
174
+
175
+ fn encode(value: &str) -> String {
176
+ let mut encoded = String::with_capacity(value.len());
177
+ for byte in value.bytes() {
178
+ match byte {
179
+ b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' => {
180
+ encoded.push(byte as char);
181
+ }
182
+ _ => {
183
+ let _ = write!(&mut encoded, "%{byte:02X}");
184
+ }
185
+ }
186
+ }
187
+ encoded
188
+ }
189
+
190
+ #[cfg(test)]
191
+ mod tests {
192
+ use super::*;
193
+
194
+ #[test]
195
+ fn round_trips_secret_across_credentials() {
196
+ let dir = tempfile::tempdir().unwrap();
197
+ let builder = Builder::new(dir.path());
198
+
199
+ let first = builder
200
+ .build(None, "koharu", "llm_provider_api_key_openai")
201
+ .unwrap();
202
+ assert!(matches!(first.get_secret(), Err(Error::NoEntry)));
203
+ first.set_secret(b"sk-test").unwrap();
204
+
205
+ let second = builder
206
+ .build(None, "koharu", "llm_provider_api_key_openai")
207
+ .unwrap();
208
+ assert_eq!(second.get_secret().unwrap(), b"sk-test");
209
+ second.delete_credential().unwrap();
210
+ assert!(matches!(second.get_secret(), Err(Error::NoEntry)));
211
+ }
212
+
213
+ #[test]
214
+ fn file_names_escape_path_separators() {
215
+ let name = file_name(Some("target/value"), "service\\name", "user name");
216
+
217
+ assert!(name.contains("target%2Fvalue"));
218
+ assert!(name.contains("service%5Cname"));
219
+ assert!(name.contains("user%20name"));
220
+ assert!(!name.contains('/'));
221
+ assert!(!name.contains('\\'));
222
+ }
223
+
224
+ #[cfg(unix)]
225
+ #[test]
226
+ fn writes_private_permissions() {
227
+ use std::os::unix::fs::PermissionsExt;
228
+
229
+ let dir = tempfile::tempdir().unwrap();
230
+ let builder = Builder::new(dir.path());
231
+ let credential = builder
232
+ .build(None, "koharu", "llm_provider_api_key_openai")
233
+ .unwrap();
234
+
235
+ credential.set_secret(b"sk-test").unwrap();
236
+
237
+ let dir_mode = fs::metadata(dir.path()).unwrap().permissions().mode() & 0o777;
238
+ let file_path =
239
+ dir.path()
240
+ .join(file_name(None, "koharu", "llm_provider_api_key_openai"));
241
+ let file_mode = fs::metadata(file_path).unwrap().permissions().mode() & 0o777;
242
+
243
+ assert_eq!(dir_mode, 0o700);
244
+ assert_eq!(file_mode, 0o600);
245
+ }
246
+ }
247
+ }
koharu-llm/src/providers/mod.rs CHANGED
@@ -1,10 +1,8 @@
1
  use std::future::Future;
2
  use std::pin::Pin;
3
  use std::sync::Arc;
4
- use std::sync::atomic::{AtomicBool, Ordering};
5
 
6
  use anyhow::Context;
7
- use keyring::Entry;
8
  use reqwest_middleware::ClientWithMiddleware;
9
 
10
  use crate::prompt::{BLOCK_TAG_INSTRUCTIONS, system_prompt};
@@ -21,6 +19,7 @@ pub(crate) fn resolve_system_prompt(custom: Option<&str>, target_language: Langu
21
  pub mod caiyun;
22
  mod chat_completions;
23
  pub mod claude;
 
24
  pub mod deepl;
25
  pub mod deepseek;
26
  pub mod gemini;
@@ -28,10 +27,7 @@ pub mod google_translate;
28
  pub mod openai;
29
  pub mod openai_compatible;
30
 
31
- const API_KEY_SERVICE: &str = "koharu";
32
- pub const OPENAI_COMPATIBLE_ID: &str = "openai-compatible";
33
-
34
- static NO_KEYRING: AtomicBool = AtomicBool::new(false);
35
 
36
  #[derive(Debug, Clone, Copy)]
37
  pub struct ProviderModelDescriptor {
@@ -78,62 +74,6 @@ pub struct ProviderDescriptor {
78
  pub build: fn(ProviderConfig) -> anyhow::Result<Box<dyn AnyProvider>>,
79
  }
80
 
81
- pub fn disable_keyring() {
82
- NO_KEYRING.store(true, Ordering::Relaxed);
83
- }
84
-
85
- fn env_key_var(provider: &str) -> String {
86
- format!(
87
- "KOHARU_{}_API_KEY",
88
- provider.to_ascii_uppercase().replace('-', "_")
89
- )
90
- }
91
-
92
- fn provider_key_entry(provider: &str) -> anyhow::Result<Entry> {
93
- let username = format!("llm_provider_api_key_{provider}");
94
- Ok(Entry::new(API_KEY_SERVICE, &username)?)
95
- }
96
-
97
- pub fn get_saved_api_key(provider: &str) -> anyhow::Result<Option<String>> {
98
- if NO_KEYRING.load(Ordering::Relaxed) {
99
- let var = env_key_var(provider);
100
- return Ok(std::env::var(&var)
101
- .ok()
102
- .map(|v| v.trim().to_string())
103
- .filter(|v| !v.is_empty()));
104
- }
105
-
106
- let entry = provider_key_entry(provider)?;
107
- match entry.get_password() {
108
- Ok(value) => Ok(Some(value)),
109
- Err(keyring::Error::NoEntry) => Ok(None),
110
- Err(err) => Err(err.into()),
111
- }
112
- }
113
-
114
- pub fn set_saved_api_key(provider: &str, api_key: &str) -> anyhow::Result<()> {
115
- if NO_KEYRING.load(Ordering::Relaxed) {
116
- tracing::warn!(
117
- provider,
118
- "keyring is disabled; API key changes are not saved"
119
- );
120
- return Err(anyhow::anyhow!(
121
- "keyring is disabled; API key cannot be saved"
122
- ));
123
- }
124
-
125
- let entry = provider_key_entry(provider)?;
126
- if api_key.trim().is_empty() {
127
- match entry.delete_credential() {
128
- Ok(()) | Err(keyring::Error::NoEntry) => Ok(()),
129
- Err(err) => Err(err.into()),
130
- }
131
- } else {
132
- entry.set_password(api_key)?;
133
- Ok(())
134
- }
135
- }
136
-
137
  pub async fn ensure_provider_success(
138
  provider: &str,
139
  response: reqwest::Response,
@@ -273,7 +213,7 @@ const PROVIDERS: &[ProviderDescriptor] = &[
273
  build: build_caiyun_mt_provider,
274
  },
275
  ProviderDescriptor {
276
- id: OPENAI_COMPATIBLE_ID,
277
  name: "OpenAI-compatible",
278
  requires_api_key: false,
279
  requires_base_url: true,
@@ -395,7 +335,7 @@ fn build_openai_compatible_provider(
395
  ) -> anyhow::Result<Box<dyn AnyProvider>> {
396
  Ok(Box::new(openai_compatible::OpenAiCompatibleProvider {
397
  http_client: Arc::clone(&config.http_client),
398
- base_url: required_base_url(&config, OPENAI_COMPATIBLE_ID)?,
399
  api_key: config.api_key,
400
  temperature: config.temperature,
401
  max_tokens: config.max_tokens,
@@ -428,7 +368,7 @@ fn build_caiyun_mt_provider(config: ProviderConfig) -> anyhow::Result<Box<dyn An
428
 
429
  fn discover_openai_compatible_models(config: ProviderConfig) -> ProviderDiscoveryFuture {
430
  Box::pin(async move {
431
- let base_url = required_base_url(&config, OPENAI_COMPATIBLE_ID)?;
432
  let models = openai_compatible::list_models(
433
  config.http_client,
434
  &base_url,
 
1
  use std::future::Future;
2
  use std::pin::Pin;
3
  use std::sync::Arc;
 
4
 
5
  use anyhow::Context;
 
6
  use reqwest_middleware::ClientWithMiddleware;
7
 
8
  use crate::prompt::{BLOCK_TAG_INSTRUCTIONS, system_prompt};
 
19
  pub mod caiyun;
20
  mod chat_completions;
21
  pub mod claude;
22
+ mod credentials;
23
  pub mod deepl;
24
  pub mod deepseek;
25
  pub mod gemini;
 
27
  pub mod openai;
28
  pub mod openai_compatible;
29
 
30
+ pub use credentials::{get_saved_api_key, set_saved_api_key};
 
 
 
31
 
32
  #[derive(Debug, Clone, Copy)]
33
  pub struct ProviderModelDescriptor {
 
74
  pub build: fn(ProviderConfig) -> anyhow::Result<Box<dyn AnyProvider>>,
75
  }
76
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
  pub async fn ensure_provider_success(
78
  provider: &str,
79
  response: reqwest::Response,
 
213
  build: build_caiyun_mt_provider,
214
  },
215
  ProviderDescriptor {
216
+ id: "openai-compatible",
217
  name: "OpenAI-compatible",
218
  requires_api_key: false,
219
  requires_base_url: true,
 
335
  ) -> anyhow::Result<Box<dyn AnyProvider>> {
336
  Ok(Box::new(openai_compatible::OpenAiCompatibleProvider {
337
  http_client: Arc::clone(&config.http_client),
338
+ base_url: required_base_url(&config, "openai-compatible")?,
339
  api_key: config.api_key,
340
  temperature: config.temperature,
341
  max_tokens: config.max_tokens,
 
368
 
369
  fn discover_openai_compatible_models(config: ProviderConfig) -> ProviderDiscoveryFuture {
370
  Box::pin(async move {
371
+ let base_url = required_base_url(&config, "openai-compatible")?;
372
  let models = openai_compatible::list_models(
373
  config.http_client,
374
  &base_url,
koharu-ml/Cargo.toml CHANGED
@@ -34,7 +34,6 @@ num_cpus = { workspace = true }
34
  rayon = { workspace = true }
35
  libloading = { workspace = true }
36
  minijinja = { workspace = true }
37
- keyring = { workspace = true }
38
  cudarc = { workspace = true, optional = true }
39
  objc2 = { workspace = true, optional = true }
40
  objc2-metal = { workspace = true, optional = true }
@@ -42,15 +41,6 @@ objc2-metal-performance-shaders = { workspace = true, optional = true }
42
  objc2-metal-performance-shaders-graph = { workspace = true, optional = true }
43
  objc2-foundation = { workspace = true, optional = true }
44
 
45
- [target.'cfg(windows)'.dependencies]
46
- keyring = { workspace = true, features = ["windows-native"] }
47
-
48
- [target.'cfg(target_os = "macos")'.dependencies]
49
- keyring = { workspace = true, features = ["apple-native"] }
50
-
51
- [target.'cfg(target_os = "linux")'.dependencies]
52
- keyring = { workspace = true, features = ["sync-secret-service"] }
53
-
54
  [features]
55
  cuda = [
56
  "candle-core/cuda",
 
34
  rayon = { workspace = true }
35
  libloading = { workspace = true }
36
  minijinja = { workspace = true }
 
37
  cudarc = { workspace = true, optional = true }
38
  objc2 = { workspace = true, optional = true }
39
  objc2-metal = { workspace = true, optional = true }
 
41
  objc2-metal-performance-shaders-graph = { workspace = true, optional = true }
42
  objc2-foundation = { workspace = true, optional = true }
43
 
 
 
 
 
 
 
 
 
 
44
  [features]
45
  cuda = [
46
  "candle-core/cuda",
koharu/src/app.rs CHANGED
@@ -62,10 +62,6 @@ pub async fn run() -> Result<()> {
62
  .with(crate::tracing::TimingLayer::new())
63
  .init();
64
 
65
- if cli.no_keyring {
66
- koharu_llm::providers::disable_keyring();
67
- }
68
-
69
  let config: AppConfig = app_config::load()?;
70
  let http = RuntimeHttpConfig {
71
  connect_timeout_secs: config.http.connect_timeout.max(1),
 
62
  .with(crate::tracing::TimingLayer::new())
63
  .init();
64
 
 
 
 
 
65
  let config: AppConfig = app_config::load()?;
66
  let http = RuntimeHttpConfig {
67
  connect_timeout_secs: config.http.connect_timeout.max(1),
koharu/src/cli.rs CHANGED
@@ -16,8 +16,6 @@ pub(crate) struct Cli {
16
  pub(crate) host: Option<String>,
17
  #[arg(long, help = "Run without GUI")]
18
  pub(crate) headless: bool,
19
- #[arg(long, help = "Use env vars for API keys instead of keyring")]
20
- pub(crate) no_keyring: bool,
21
  #[arg(long, help = "Enable debug console output")]
22
  pub(crate) debug: bool,
23
  }
 
16
  pub(crate) host: Option<String>,
17
  #[arg(long, help = "Run without GUI")]
18
  pub(crate) headless: bool,
 
 
19
  #[arg(long, help = "Enable debug console output")]
20
  pub(crate) debug: bool,
21
  }