Skip to content
Open
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
11 changes: 10 additions & 1 deletion cmd/opencodereview/config_cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ func unsetActiveProvider(configPath string) error {
}

func legacyLLMShadowWarning(provider, key string) string {
if provider == "" || !strings.HasPrefix(key, "llm.") {
if provider == "" || !strings.HasPrefix(key, "llm.") || key == "llm.prompt_caching" || key == "llm.PromptCaching" {
return ""
}
section := "custom_providers"
Expand Down Expand Up @@ -309,6 +309,8 @@ type LlmConfig struct {
TimeoutSec int `json:"timeout_sec,omitempty"` // per-request HTTP timeout in seconds
ExtraBody map[string]any `json:"extra_body,omitempty"`
ExtraHeaders map[string]string `json:"extra_headers,omitempty"`
// PromptCaching is the global Anthropic caching toggle for the active provider.
PromptCaching *bool `json:"prompt_caching,omitempty"`
}

// TelemetryConfig holds telemetry-specific settings.
Expand Down Expand Up @@ -365,6 +367,7 @@ var supportedConfigKeys = []string{
"llm.model",
"llm.protocol",
"llm.use_anthropic",
"llm.prompt_caching",
"llm.extra_body",
"llm.extra_headers",
"language",
Expand Down Expand Up @@ -476,6 +479,12 @@ func setConfigValue(cfg *Config, key, value string) error {
} else if cfg.Llm.Protocol == "" || cfg.Llm.Protocol == llm.ProtocolAnthropic || cfg.Llm.Protocol == llm.ProtocolOpenAIChatCompletions {
cfg.Llm.Protocol = llm.ProtocolOpenAIChatCompletions
}
case "llm.prompt_caching", "llm.PromptCaching":
b, err := strconv.ParseBool(value)
if err != nil {
return fmt.Errorf("invalid boolean for llm.prompt_caching: %w", err)
}
cfg.Llm.PromptCaching = &b
case "language", "Language":
cfg.Language = value
case "telemetry.enabled", "telemetry.Enabled":
Expand Down
21 changes: 20 additions & 1 deletion cmd/opencodereview/config_cmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -830,6 +830,22 @@ func TestSetConfigValueLlmUseAnthropicInvalid(t *testing.T) {
}
}

func TestSetConfigValueLlmPromptCaching(t *testing.T) {
cfg := &Config{}
if err := setConfigValue(cfg, "llm.prompt_caching", "false"); err != nil {
t.Fatalf("setConfigValue: %v", err)
}
if cfg.Llm.PromptCaching == nil || *cfg.Llm.PromptCaching {
t.Fatalf("PromptCaching = %v, want false", cfg.Llm.PromptCaching)
}
}

func TestSetConfigValueLlmPromptCachingInvalid(t *testing.T) {
if err := setConfigValue(&Config{}, "llm.prompt_caching", "sometimes"); err == nil {
t.Fatal("expected error for invalid boolean")
}
}

func TestSetConfigValueLanguage(t *testing.T) {
cfg := &Config{}
if err := setConfigValue(cfg, "language", "English"); err != nil {
Expand Down Expand Up @@ -929,7 +945,7 @@ func TestSetConfigValueUnknownKeyMessage(t *testing.T) {
t.Fatal("expected error for unknown key")
}
want := "unknown config key: bogus.key\n" +
"Supported keys: provider, model, providers.<name>.<field>, custom_providers.<name>.<field>, mcp_servers.<name>.<field>, llm.url, llm.auth_token, llm.auth_header, llm.model, llm.protocol, llm.use_anthropic, llm.extra_body, llm.extra_headers, language, telemetry.enabled, telemetry.exporter, telemetry.otlp_endpoint, telemetry.content_logging\n" +
"Supported keys: provider, model, providers.<name>.<field>, custom_providers.<name>.<field>, mcp_servers.<name>.<field>, llm.url, llm.auth_token, llm.auth_header, llm.model, llm.protocol, llm.use_anthropic, llm.prompt_caching, llm.extra_body, llm.extra_headers, language, telemetry.enabled, telemetry.exporter, telemetry.otlp_endpoint, telemetry.content_logging\n" +
"Provider fields: api_key, url, protocol, model, models, auth_header, extra_body, extra_headers\n" +
"Protocol values: anthropic, openai, openai-responses\n" +
"MCP server fields: type, command, args, env, url, headers, tools, setup"
Expand Down Expand Up @@ -998,6 +1014,9 @@ func TestLegacyLLMShadowWarning(t *testing.T) {
if got := legacyLLMShadowWarning("dashscope", "Llm.model"); got != "" {
t.Errorf("warning for invalid mixed-case legacy key = %q", got)
}
if got := legacyLLMShadowWarning("dashscope", "llm.prompt_caching"); got != "" {
t.Errorf("warning for global prompt caching setting = %q", got)
}
if got := legacyLLMShadowWarning("dashscope", "llm.model"); !strings.Contains(got, "providers.dashscope.<field>") {
t.Errorf("preset-provider warning = %q", got)
}
Expand Down
19 changes: 19 additions & 0 deletions cmd/opencodereview/provider_cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,16 @@ func removeModels(existing, toRemove []string) []string {
return result
}

func applyPromptCachingConfig(cfg *Config, protocol string, promptCaching *bool) {
if llm.NormalizeProtocol(protocol) != llm.ProtocolAnthropic {
cfg.Llm.PromptCaching = nil
return
}
if promptCaching != nil {
cfg.Llm.PromptCaching = promptCaching
}
}

func applyManualConfig(configPath string, cfg *Config, result providerTUIResult) error {
if result.url == "" {
return fmt.Errorf("URL is required for manual configuration")
Expand All @@ -120,6 +130,7 @@ func applyManualConfig(configPath string, cfg *Config, result providerTUIResult)
// older binaries pick the OpenAI auth header/endpoint instead of wrongly
// defaulting to anthropic.
protocol := llm.NormalizeProtocol(result.protocol)
applyPromptCachingConfig(cfg, protocol, result.promptCaching)
cfg.Llm.Protocol = protocol
switch protocol {
case llm.ProtocolAnthropic:
Expand Down Expand Up @@ -186,6 +197,9 @@ func applyCustomProviderConfig(configPath string, cfg *Config, result providerTU
} else {
entry.APIKey = ""
}
if !result.isEdit || cfg.Provider == result.provider || result.promptCaching != nil {
applyPromptCachingConfig(cfg, entry.Protocol, result.promptCaching)
}
cfg.CustomProviders[result.provider] = entry

if !result.isEdit {
Expand Down Expand Up @@ -234,6 +248,10 @@ func applyOfficialProviderConfig(configPath string, cfg *Config, result provider
}

preset, isPreset := llm.LookupProvider(result.provider)
protocol := result.protocol
if isPreset {
protocol = preset.Protocol
}

if result.apiKey == "" {
if isPreset && preset.EnvVar != "" {
Expand All @@ -260,6 +278,7 @@ func applyOfficialProviderConfig(configPath string, cfg *Config, result provider
// Confirmed empty key: clear saved api_key so resolver falls back to $ENV_VAR.
entry.APIKey = ""
}
applyPromptCachingConfig(cfg, protocol, result.promptCaching)
cfg.Providers[result.provider] = entry

if cfg.Provider != result.provider {
Expand Down
41 changes: 41 additions & 0 deletions cmd/opencodereview/provider_cmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
"os"
"path/filepath"
"testing"

"github.com/alibaba/open-code-review/internal/llm"
)

func TestMaskKey(t *testing.T) {
Expand Down Expand Up @@ -274,6 +276,45 @@ func TestApplyCustomProviderConfig_EmptyKeyClearsSavedAPIKey(t *testing.T) {
}
}

func TestApplyCustomProviderConfig_PersistsPromptCaching(t *testing.T) {
disabled := false
configPath := filepath.Join(t.TempDir(), "config.json")
cfg := &Config{
Provider: "gateway",
Model: "claude-test",
CustomProviders: map[string]ProviderEntry{
"gateway": {
URL: "https://gateway.example/v1",
Protocol: llm.ProtocolAnthropic,
APIKey: "test-key",
Model: "claude-test",
Models: []string{"claude-test"},
},
},
}

err := applyCustomProviderConfig(configPath, cfg, providerTUIResult{
provider: "gateway",
model: "claude-test",
models: []string{"claude-test"},
apiKey: "test-key",
isCustom: true,
url: "https://gateway.example/v1",
protocol: llm.ProtocolAnthropic,
promptCaching: &disabled,
})
if err != nil {
t.Fatalf("applyCustomProviderConfig: %v", err)
}
diskCfg, err := loadOrCreateConfig(configPath)
if err != nil {
t.Fatalf("load config: %v", err)
}
if diskCfg.Llm.PromptCaching == nil || *diskCfg.Llm.PromptCaching {
t.Fatalf("PromptCaching = %v, want false", diskCfg.Llm.PromptCaching)
}
}

func TestProviderTUIResult_ResolvedModel(t *testing.T) {
r := providerTUIResult{
provider: "baidu-qianfan",
Expand Down
Loading