feat: Support authenticating or refreshing OAuth for supported clients from within the REPL
CI / All (macos-latest) (push) Has been cancelled
CI / All (ubuntu-latest) (push) Has been cancelled
CI / All (windows-latest) (push) Has been cancelled

This commit is contained in:
2026-03-11 13:07:27 -06:00
parent c428990900
commit eb4d1c02f4
7 changed files with 44 additions and 8 deletions
+1
View File
@@ -166,6 +166,7 @@ clients:
```sh ```sh
loki --authenticate my-claude-oauth loki --authenticate my-claude-oauth
# Or via the REPL: .authenticate
``` ```
For full details, see the [authentication documentation](./docs/clients/CLIENTS.md#authentication). For full details, see the [authentication documentation](./docs/clients/CLIENTS.md#authentication).
+2 -1
View File
@@ -210,7 +210,8 @@ clients:
- type: claude - type: claude
api_base: https://api.anthropic.com/v1 # Optional api_base: https://api.anthropic.com/v1 # Optional
api_key: '{{ANTHROPIC_API_KEY}}' # You can either hard-code or inject secrets from the Loki vault api_key: '{{ANTHROPIC_API_KEY}}' # You can either hard-code or inject secrets from the Loki vault
auth: null # When set to 'oauth', Loki will use OAuth instead of an API key; Authenticate with `loki --authenticate` auth: null # When set to 'oauth', Loki will use OAuth instead of an API key
# Authenticate with `loki --authenticate` or `.authenticate` in the REPL
# See https://docs.mistral.ai/ # See https://docs.mistral.ai/
- type: openai-compatible - type: openai-compatible
+6
View File
@@ -23,6 +23,7 @@ You can enter the REPL by simply typing `loki` without any follow-up flags or ar
- [`.edit` - Modify configuration files](#edit---modify-configuration-files) - [`.edit` - Modify configuration files](#edit---modify-configuration-files)
- [`.delete` - Delete configurations from Loki](#delete---delete-configurations-from-loki) - [`.delete` - Delete configurations from Loki](#delete---delete-configurations-from-loki)
- [`.info` - Display information about the current mode](#info---display-information-about-the-current-mode) - [`.info` - Display information about the current mode](#info---display-information-about-the-current-mode)
- [`.authenticate` - Authenticate the current model client via OAuth](#authenticate---authenticate-the-current-model-client-via-oauth)
- [`.exit` - Exit an agent/role/session/rag or the Loki REPL itself](#exit---exit-an-agentrolesessionrag-or-the-loki-repl-itself) - [`.exit` - Exit an agent/role/session/rag or the Loki REPL itself](#exit---exit-an-agentrolesessionrag-or-the-loki-repl-itself)
- [`.help` - Show the help guide](#help---show-the-help-guide) - [`.help` - Show the help guide](#help---show-the-help-guide)
<!--toc:end--> <!--toc:end-->
@@ -237,6 +238,11 @@ The following entities are supported:
| `.info agent` | Display information about the active agent | | `.info agent` | Display information about the active agent |
| `.info rag` | Display information about the active RAG | | `.info rag` | Display information about the active RAG |
### `.authenticate` - Authenticate the current model client via OAuth
The `.authenticate` command will start the OAuth flow for the current model client if
* The client supports OAuth (See the [clients documentation](./clients/CLIENTS.md#providers-that-support-oauth) for supported clients)
* The client is configured in your Loki configuration to use OAuth via the `auth: oauth` property
### `.exit` - Exit an agent/role/session/rag or the Loki REPL itself ### `.exit` - Exit an agent/role/session/rag or the Loki REPL itself
The `.exit` command is used to move between modes in the Loki REPL. The `.exit` command is used to move between modes in the Loki REPL.
+6 -4
View File
@@ -129,15 +129,17 @@ Run the `--authenticate` flag with the client name:
loki --authenticate my-claude-oauth loki --authenticate my-claude-oauth
``` ```
This opens your browser for the OAuth authorization flow. After authorizing, paste the authorization code back into Or if you have only one OAuth-configured client, you can omit the name:
the terminal. Loki stores the tokens in `~/.cache/loki/oauth` and automatically refreshes them when they expire.
If you have only one OAuth-configured client, you can omit the name:
```sh ```sh
loki --authenticate loki --authenticate
``` ```
Alternatively, you can use the REPL command `.authenticate`.
This opens your browser for the OAuth authorization flow. After authorizing, paste the authorization code back into
the terminal. Loki stores the tokens in `~/.cache/loki/oauth` and automatically refreshes them when they expire.
**Step 3: Use normally** **Step 3: Use normally**
Once authenticated, the client works like any other. Loki uses the stored OAuth tokens automatically: Once authenticated, the client works like any other. Loki uses the stored OAuth tokens automatically:
+5 -1
View File
@@ -27,7 +27,7 @@ pub struct ClaudeConfig {
impl ClaudeClient { impl ClaudeClient {
config_get_fn!(api_key, get_api_key); config_get_fn!(api_key, get_api_key);
config_get_fn!(api_base, get_api_base); config_get_fn!(api_base, get_api_base);
create_oauth_supported_client_config!(); create_oauth_supported_client_config!();
} }
@@ -35,6 +35,10 @@ impl ClaudeClient {
impl Client for ClaudeClient { impl Client for ClaudeClient {
client_common_fns!(); client_common_fns!();
fn supports_oauth(&self) -> bool {
self.config.auth.as_deref() == Some("oauth")
}
async fn chat_completions_inner( async fn chat_completions_inner(
&self, &self,
client: &ReqwestClient, client: &ReqwestClient,
+4
View File
@@ -47,6 +47,10 @@ pub trait Client: Sync + Send {
fn model(&self) -> &Model; fn model(&self) -> &Model;
fn supports_oauth(&self) -> bool {
false
}
fn build_client(&self) -> Result<ReqwestClient> { fn build_client(&self) -> Result<ReqwestClient> {
let mut builder = ReqwestClient::builder(); let mut builder = ReqwestClient::builder();
let extra = self.extra_config(); let extra = self.extra_config();
+20 -2
View File
@@ -6,7 +6,7 @@ use self::completer::ReplCompleter;
use self::highlighter::ReplHighlighter; use self::highlighter::ReplHighlighter;
use self::prompt::ReplPrompt; use self::prompt::ReplPrompt;
use crate::client::{call_chat_completions, call_chat_completions_streaming}; use crate::client::{call_chat_completions, call_chat_completions_streaming, init_client, oauth};
use crate::config::{ use crate::config::{
AgentVariables, AssertState, Config, GlobalConfig, Input, LastMessage, StateFlags, AgentVariables, AssertState, Config, GlobalConfig, Input, LastMessage, StateFlags,
macro_execute, macro_execute,
@@ -17,6 +17,7 @@ use crate::utils::{
}; };
use crate::mcp::McpRegistry; use crate::mcp::McpRegistry;
use crate::resolve_oauth_client;
use anyhow::{Context, Result, bail}; use anyhow::{Context, Result, bail};
use crossterm::cursor::SetCursorStyle; use crossterm::cursor::SetCursorStyle;
use fancy_regex::Regex; use fancy_regex::Regex;
@@ -32,10 +33,15 @@ use std::{env, mem, process};
const MENU_NAME: &str = "completion_menu"; const MENU_NAME: &str = "completion_menu";
static REPL_COMMANDS: LazyLock<[ReplCommand; 37]> = LazyLock::new(|| { static REPL_COMMANDS: LazyLock<[ReplCommand; 38]> = LazyLock::new(|| {
[ [
ReplCommand::new(".help", "Show this help guide", AssertState::pass()), ReplCommand::new(".help", "Show this help guide", AssertState::pass()),
ReplCommand::new(".info", "Show system info", AssertState::pass()), ReplCommand::new(".info", "Show system info", AssertState::pass()),
ReplCommand::new(
".authenticate",
"Authenticate the current model client via OAuth (if configured)",
AssertState::pass(),
),
ReplCommand::new( ReplCommand::new(
".edit config", ".edit config",
"Modify configuration file", "Modify configuration file",
@@ -421,6 +427,18 @@ pub async fn run_repl_command(
} }
None => println!("Usage: .model <name>"), None => println!("Usage: .model <name>"),
}, },
".authenticate" => {
let client = init_client(config, None)?;
if !client.supports_oauth() {
bail!(
"Client '{}' doesn't either support OAuth or isn't configured to use it (i.e. uses an API key instead)",
client.name()
);
}
let clients = config.read().clients.clone();
let (client_name, provider) = resolve_oauth_client(Some(client.name()), &clients)?;
oauth::run_oauth_flow(&provider, &client_name).await?;
}
".prompt" => match args { ".prompt" => match args {
Some(text) => { Some(text) => {
config.write().use_prompt(text)?; config.write().use_prompt(text)?;