Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions cmd/thv/app/llm.go
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,7 @@ func newLLMSetupCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "setup",
Short: "Configure detected AI tools to use the LLM gateway",
Long: `Detect installed AI tools (Claude Code, Gemini CLI, Cursor, VS Code, Xcode,
Long: `Detect installed AI tools (Claude Code, Gemini CLI, Pi, Cursor, VS Code, Xcode,
Claude Desktop, Codex) and patch each tool's configuration to route through the
LLM gateway.

Expand All @@ -309,6 +309,20 @@ untouched. Desktop app detection is supported on macOS and Windows; the canonica
--client target remains "codex". If the desktop app is running, fully quit and
reopen it after setup.

Pi is configured with OpenAI-compatible and Anthropic providers in
Comment thread
jerm-dro marked this conversation as resolved.
~/.pi/agent/models.json (or $PI_CODING_AGENT_DIR/models.json). It runs
"thv llm token" for each request; authenticated setup discovers models and
context sizes from both gateway APIs and Anthropic thinking support when
advertised. --models filters the discovered models. Pi does not support --lazy
because model discovery determines each model's API.
Pi defaults to short prompt caching: start Pi with PI_CACHE_RETENTION=long
in its environment for long retention (Anthropic: 1h; OpenAI: 24h).
This cannot be persisted in models.json. The gateway does not currently provide
image-input or max-output metadata, so Pi defaults to text-only input and 16K
max output tokens. OpenAI models without reasoning metadata default to no reasoning.
If Pi cannot refresh its gateway token, run "thv llm token" in a terminal to
log in again.

Proxy-mode tools (Cursor, VS Code, Xcode) are configured to send requests to
the localhost reverse proxy started by "thv llm proxy start".

Expand Down Expand Up @@ -358,8 +372,8 @@ Run "thv llm teardown" to revert all changes.`,
"Use Claude Code's five-minute prompt cache instead of ToolHive's one-hour default. Persisted, so a later "+
"plain \"thv llm setup\" keeps it; use --short-prompt-cache=false to restore one-hour caching.")
cmd.Flags().StringVar(&anthropicPathPrefix, "anthropic-path-prefix", "",
"Path prefix appended to the gateway URL when writing ANTHROPIC_BASE_URL for direct-mode tools "+
"(e.g. /anthropic). When omitted, the gateway is probed automatically.")
"Path prefix appended to the gateway URL for Anthropic API tools (e.g. /anthropic). "+
"Also used for Pi model discovery. When omitted, the gateway is probed automatically.")
cmd.Flags().StringVar(&targetClient, "client", "",
"Configure only this AI tool by name (e.g. claude-code, cursor, codex). Omit to configure all detected tools.")
cmd.Flags().BoolVar(&lazy, "lazy", false,
Expand All @@ -370,6 +384,7 @@ Run "thv llm teardown" to revert all changes.`,
cmd.Flags().StringSliceVar(&models, "models", nil,
"Model IDs to configure, comma-separated or by repeating the flag, e.g. "+
"--models=us.anthropic.claude-opus-4-8,us.anthropic.claude-sonnet-5. "+
"For Pi these filter its discovered models (Pi does not support --lazy). "+
"For credential-helper clients (Claude Desktop) these become inferenceModels. "+
"With --bedrock-compat, each ID is also mapped to a Claude Code tier by matching "+
"'haiku', 'opus', or 'sonnet' in the ID (IDs matching no tier are ignored with a "+
Expand Down
20 changes: 17 additions & 3 deletions docs/cli/thv_llm_setup.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 7 additions & 3 deletions pkg/client/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -1052,9 +1052,13 @@ var supportedClientIntegrations = []clientAppConfig{
types.TransportTypeSSE: defaultURLFieldName,
types.TransportTypeStreamableHTTP: defaultURLFieldName,
},
SupportsSkills: true,
SkillsGlobalPath: []string{".pi", "agent", skillsDirName},
SkillsProjectPath: []string{".pi", skillsDirName},
SupportsSkills: true,
SkillsGlobalPath: []string{".pi", "agent", skillsDirName},
SkillsProjectPath: []string{".pi", skillsDirName},
LLMGatewayMode: llmgateway.ModePi,
LLMBinaryName: "pi",
LLMSettingsFile: "models.json",
LLMSettingsRelPath: []string{".pi", "agent"},
Comment thread
jerm-dro marked this conversation as resolved.
},
{
// Xcode does not support MCP; it is an LLM-gateway-only entry.
Expand Down
21 changes: 21 additions & 0 deletions pkg/client/llm_gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ func (cm *ClientManager) ConfigureLLMGateway(clientType ClientApp, cfg llmgatewa
if appCfg.LLMGatewayMode == llmgateway.ModeCodexAuth {
return cm.configureCodexAuth(appCfg, cfg)
}
if appCfg.LLMGatewayMode == llmgateway.ModePi {
return cm.configurePi(appCfg, cfg)
}
if appCfg.LLMGatewayMode == llmgateway.ModeVSCode {
return cm.configureVSCode(appCfg, cfg)
}
Expand Down Expand Up @@ -192,6 +195,9 @@ func (cm *ClientManager) RevertLLMGateway(clientType ClientApp, configPath strin
if appCfg.LLMGatewayMode == llmgateway.ModeCodexAuth {
return cm.revertCodexAuth(appCfg, configPath)
}
if appCfg.LLMGatewayMode == llmgateway.ModePi {
return cm.revertPi(appCfg, configPath)
}
if appCfg.LLMGatewayMode == llmgateway.ModeVSCode {
return cm.revertVSCode(appCfg, configPath)
}
Expand Down Expand Up @@ -543,6 +549,21 @@ func (cm *ClientManager) LLMClientDetectionHint(clientType ClientApp) string {
// buildLLMSettingsPath resolves the absolute path to the LLM settings file
// for the given client using the same PlatformPrefix logic as MCP config paths.
func (cm *ClientManager) buildLLMSettingsPath(cfg *clientAppConfig) string {
if cfg.ClientType == ClientApp(Pi) {
if dir := os.Getenv("PI_CODING_AGENT_DIR"); dir != "" {
if dir == "~" {
dir = cm.homeDir
} else if strings.HasPrefix(dir, "~/") {
dir = filepath.Join(cm.homeDir, dir[2:])
}
if !filepath.IsAbs(dir) {
if absolute, err := filepath.Abs(dir); err == nil {
dir = absolute
}
}
return filepath.Join(dir, cfg.LLMSettingsFile)
}
}
return buildConfigFilePath(
cfg.LLMSettingsFile,
cfg.LLMSettingsRelPath,
Expand Down
Loading
Loading