Skip to content

Commit 854e0ab

Browse files
yroblataskbotclaude
authored
pkg/llm: implement OIDC token source and thv llm token command (#5033)
* pkg/llm: implement OIDC token source and thv llm token command Adds a three-tier TokenSource (in-memory → cached refresh token → browser OIDC+PKCE flow) for the LLM gateway. Access tokens are held in memory only; refresh tokens are persisted via ScopeLLM secrets. A 30 s preemptive refresh window avoids gateway rejections on expiry. thv llm token is now fully implemented: non-interactive, prints a fresh JWT to stdout with all other output on stderr, suitable for use as apiKeyHelper or auth.command in Claude Code / Cursor. Part of #5028 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * changes from review --------- Co-authored-by: taskbot <taskbot@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent ee6a892 commit 854e0ab

3 files changed

Lines changed: 1054 additions & 5 deletions

File tree

cmd/thv/app/llm.go

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"encoding/json"
99
"fmt"
1010
"os"
11+
"time"
1112

1213
"github.com/spf13/cobra"
1314

@@ -203,6 +204,43 @@ tokens from the secrets provider.`,
203204
}
204205
}
205206

207+
// runLLMToken prints a fresh LLM gateway access token to stdout.
208+
// All diagnostic output goes to stderr so the caller can capture the token
209+
// cleanly (e.g. apiKeyHelper or auth.command in Claude Code / Cursor).
210+
func runLLMToken(ctx context.Context) error {
211+
provider := config.NewDefaultProvider()
212+
llmCfg := provider.GetConfig().LLM
213+
214+
if !llmCfg.IsConfigured() {
215+
return fmt.Errorf("LLM gateway is not configured — run \"thv llm config set\" first")
216+
}
217+
218+
secretsProvider, err := secrets.GetSystemSecretsProvider()
219+
if err != nil {
220+
return fmt.Errorf("failed to get secrets provider: %w", err)
221+
}
222+
scoped := pkgsecrets.NewScopedProvider(secretsProvider, pkgsecrets.ScopeLLM)
223+
224+
updater := func(key string, expiry time.Time) {
225+
if err := config.UpdateConfig(func(c *config.Config) error {
226+
c.LLM.OIDC.CachedRefreshTokenRef = key
227+
c.LLM.OIDC.CachedTokenExpiry = expiry
228+
return nil
229+
}); err != nil {
230+
fmt.Fprintf(os.Stderr, "Warning: failed to persist LLM token reference: %v\n", err)
231+
}
232+
}
233+
234+
ts := llm.NewTokenSource(&llmCfg, scoped, false /* non-interactive */, updater)
235+
token, err := ts.Token(ctx)
236+
if err != nil {
237+
return err
238+
}
239+
240+
fmt.Println(token)
241+
return nil
242+
}
243+
206244
// deleteLLMSecrets removes all secrets stored under the LLM scope.
207245
func deleteLLMSecrets(ctx context.Context) error {
208246
provider, err := secrets.GetSystemSecretsProvider()
@@ -287,11 +325,8 @@ func newLLMTokenCommand() *cobra.Command {
287325
Intended for use as apiKeyHelper or auth.command in OIDC-capable AI tools.
288326
Runs non-interactively — will not launch a browser flow.`,
289327
Args: cobra.NoArgs,
290-
RunE: func(_ *cobra.Command, _ []string) error {
291-
// Print the error to stderr so it doesn't corrupt the token value
292-
// expected on stdout by callers such as apiKeyHelper or auth.command.
293-
return fmt.Errorf("thv llm token is not yet implemented — " +
294-
"run \"thv llm setup\" once it is available to configure your tools")
328+
RunE: func(cmd *cobra.Command, _ []string) error {
329+
return runLLMToken(cmd.Context())
295330
},
296331
}
297332

0 commit comments

Comments
 (0)