diff --git a/cmd/opencodereview/flags.go b/cmd/opencodereview/flags.go index 6d6e64c2..9041eaf0 100644 --- a/cmd/opencodereview/flags.go +++ b/cmd/opencodereview/flags.go @@ -107,6 +107,7 @@ type reviewOptions struct { audience string // --audience: "human" (default) or "agent" background string // --background: optional requirement context backgroundFile string // --background-file: path to a Markdown file used as background + provider string // --provider: override resolved LLM provider for this review model string // --model: override resolved LLM model for this review concurrency int perFileTimeout int @@ -135,6 +136,7 @@ func parseReviewFlags(args []string) (reviewOptions, error) { a.StringVar(&opts.audience, "audience", "human", "output audience: human (show progress) or agent (summary only)") a.StringVarP(&opts.background, "background", "b", "", "optional requirement/business context for the review") a.StringVarP(&opts.backgroundFile, "background-file", "B", "", "optional requirement/business context from a Markdown file (combined with --background; inline value appears first when both are set)") + a.StringVar(&opts.provider, "provider", "", "override LLM provider for this review (e.g., openai)") a.StringVar(&opts.model, "model", "", "override LLM model for this review (e.g., claude-opus-4-6)") a.IntVar(&opts.maxTools, "max-tools", 0, "max tool call rounds per file (0 = template default; min 10)") a.IntVar(&opts.maxGitProcs, "max-git-procs", 16, "max concurrent git subprocesses") @@ -239,6 +241,7 @@ Flags: --max-git-procs int max concurrent git subprocesses (default 16) --from string source ref to start diff from (e.g., 'main') --max-tools int max tool call rounds per file (0 = template default; min 10) + --provider string override LLM provider for this review (e.g., openai) --model string override LLM model for this review (e.g., claude-opus-4-6) -p, --preview preview which files will be reviewed without running the LLM --repo string root directory of the git repository (default: current dir) diff --git a/cmd/opencodereview/flags_test.go b/cmd/opencodereview/flags_test.go index 8e259964..86bc0f6f 100644 --- a/cmd/opencodereview/flags_test.go +++ b/cmd/opencodereview/flags_test.go @@ -36,6 +36,17 @@ func TestParseReviewFlagsModelOverride(t *testing.T) { } } +func TestParseReviewFlagsProviderOverride(t *testing.T) { + opts, err := parseReviewFlags([]string{"--provider", "openai"}) + if err != nil { + t.Fatalf("parseReviewFlags: %v", err) + } + + if opts.provider != "openai" { + t.Errorf("provider = %q, want %q", opts.provider, "openai") + } +} + func TestParseReviewFlagsResume(t *testing.T) { opts, err := parseReviewFlags([]string{"--from", "main", "--to", "feature", "--resume", "session-123"}) if err != nil { diff --git a/cmd/opencodereview/review_cmd.go b/cmd/opencodereview/review_cmd.go index 8cf53275..9ce78441 100644 --- a/cmd/opencodereview/review_cmd.go +++ b/cmd/opencodereview/review_cmd.go @@ -70,7 +70,7 @@ func runReview(args []string) error { return err } - rt, err := loadLLMRuntime(cc.Template, opts.toolConfigPath, opts.model) + rt, err := loadLLMRuntime(cc.Template, opts.toolConfigPath, opts.provider, opts.model) if err != nil { return err } diff --git a/cmd/opencodereview/scan_cmd.go b/cmd/opencodereview/scan_cmd.go index 5d15f44d..69f3b722 100644 --- a/cmd/opencodereview/scan_cmd.go +++ b/cmd/opencodereview/scan_cmd.go @@ -40,6 +40,7 @@ type scanOptions struct { noSummary bool // --no-summary: skip the post-run PROJECT_SUMMARY_TASK batch string // --batch: override scan template's BATCH_STRATEGY maxTokensBudget int // --max-tokens-budget: cap total token usage; 0 = unlimited + provider string // --provider: override resolved LLM provider for this scan model string // --model: override resolved LLM model for this scan showHelp bool } @@ -66,6 +67,7 @@ func parseScanFlags(args []string) (scanOptions, error) { a.BoolVar(&opts.noSummary, "no-summary", false, "skip the post-run PROJECT_SUMMARY_TASK (no project-level markdown summary)") a.StringVar(&opts.batch, "batch", "", "override BATCH_STRATEGY from scan template: none | by-language | by-directory") a.IntVar(&opts.maxTokensBudget, "max-tokens-budget", 0, "cap total token usage (input+output); dispatch stops once exceeded (0 = unlimited)") + a.StringVar(&opts.provider, "provider", "", "override LLM provider for this scan (e.g., openai)") a.StringVar(&opts.model, "model", "", "override LLM model for this scan (e.g., claude-opus-4-6)") if err := a.Parse(args); err != nil { @@ -159,7 +161,7 @@ func runScan(args []string) error { return runScanPreview(cc, scanTpl, scanPaths) } - rt, err := loadLLMRuntime(cc.Template, opts.toolConfigPath, opts.model) + rt, err := loadLLMRuntime(cc.Template, opts.toolConfigPath, opts.provider, opts.model) if err != nil { return err } @@ -287,6 +289,7 @@ Flags: --no-summary skip the post-run PROJECT_SUMMARY_TASK --batch string override BATCH_STRATEGY: none | by-language | by-directory --max-tokens-budget int cap total token usage; dispatch stops once exceeded (0 = unlimited) + --provider string override LLM provider for this scan (e.g., openai) --model string override LLM model for this scan (e.g., claude-opus-4-6) --audience string output audience: human (show progress) or agent (summary only) (default "human") -b, --background string optional requirement/business context for the scan diff --git a/cmd/opencodereview/scan_cmd_test.go b/cmd/opencodereview/scan_cmd_test.go index 02058425..6f9371f3 100644 --- a/cmd/opencodereview/scan_cmd_test.go +++ b/cmd/opencodereview/scan_cmd_test.go @@ -180,6 +180,16 @@ func TestParseScanFlags_ModelOverride(t *testing.T) { } } +func TestParseScanFlags_ProviderOverride(t *testing.T) { + opts, err := parseScanFlags([]string{"--provider", "openai"}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if opts.provider != "openai" { + t.Errorf("provider = %q, want openai", opts.provider) + } +} + func TestParseScanFlags_AllStringFlags(t *testing.T) { opts, err := parseScanFlags([]string{ "--tools", "/tmp/tools.json", diff --git a/cmd/opencodereview/shared.go b/cmd/opencodereview/shared.go index c99c1b2c..fceb487b 100644 --- a/cmd/opencodereview/shared.go +++ b/cmd/opencodereview/shared.go @@ -139,9 +139,10 @@ type llmRuntime struct { // loadLLMRuntime loads tool defs from toolConfigPath, reads the app config // from the user's default config path (applying the configured language to // tpl — defaulting when the config file is absent), resolves the LLM -// endpoint (honoring modelOverride from --model when non-empty), and +// endpoint (honoring providerOverride from --provider and modelOverride from --model +// when non-empty), and // returns the runtime bundle. tpl is mutated in place. -func loadLLMRuntime(tpl *template.Template, toolConfigPath, modelOverride string) (*llmRuntime, error) { +func loadLLMRuntime(tpl *template.Template, toolConfigPath, providerOverride, modelOverride string) (*llmRuntime, error) { toolEntries, err := toolsconfig.Load(toolConfigPath) if err != nil { return nil, fmt.Errorf("load tools: %w", err) @@ -165,7 +166,7 @@ func loadLLMRuntime(tpl *template.Template, toolConfigPath, modelOverride string } tpl.ApplyLanguage(lang) - ep, err := llm.ResolveEndpointWithModelOverride(cfgPath, modelOverride) + ep, err := llm.ResolveEndpointWithOverrides(cfgPath, providerOverride, modelOverride) if err != nil { return nil, fmt.Errorf("resolve LLM endpoint: %w", err) } diff --git a/internal/llm/resolver.go b/internal/llm/resolver.go index 0d3bc7d9..9de729b1 100644 --- a/internal/llm/resolver.go +++ b/internal/llm/resolver.go @@ -31,6 +31,14 @@ type ResolvedEndpoint struct { // Environment variable names for OCR-specific configuration. const ( + configStrategyName = "OCR config file" + environmentStrategyName = "OCR environment" + claudeCodeStrategyName = "Claude Code environment" + shellRCStrategyName = "Shell rc file" + providerSourcePrefix = "provider:" + providersSectionName = "providers" + customProvidersSectionName = "custom_providers" + envOCRLLMURL = "OCR_LLM_URL" envOCRLLMToken = "OCR_LLM_TOKEN" envOCRLLMModel = "OCR_LLM_MODEL" @@ -66,16 +74,35 @@ func ResolveEndpoint(configPath string) (ResolvedEndpoint, error) { // but uses modelOverride as the request model when it is non-empty. The override // can also supply the otherwise required model for a configured endpoint. func ResolveEndpointWithModelOverride(configPath, modelOverride string) (ResolvedEndpoint, error) { + return ResolveEndpointWithOverrides(configPath, "", modelOverride) +} + +// ResolveEndpointWithOverrides resolves an endpoint like ResolveEndpoint, but +// applies non-empty provider and model overrides for the current command. +func ResolveEndpointWithOverrides(configPath, providerOverride, modelOverride string) (ResolvedEndpoint, error) { + providerOverride = strings.TrimSpace(providerOverride) modelOverride = strings.TrimSpace(modelOverride) + if providerOverride != "" { + ep, _, err := tryOCRConfig(configPath, providerOverride, modelOverride) + if err != nil { + return ResolvedEndpoint{}, fmt.Errorf("resolve %s: %w", configStrategyName, err) + } + ep, err = finalizeResolvedEndpoint(ep, configStrategyName) + if err != nil { + return ResolvedEndpoint{}, fmt.Errorf("resolve %s: %w", configStrategyName, err) + } + return ep, nil + } + strategies := []struct { name string fn func() (ResolvedEndpoint, bool, error) }{ - {"OCR config file", func() (ResolvedEndpoint, bool, error) { return tryOCRConfig(configPath, modelOverride) }}, - {"OCR environment", func() (ResolvedEndpoint, bool, error) { return tryOCREnv(modelOverride) }}, - {"Claude Code environment", func() (ResolvedEndpoint, bool, error) { return tryCCEnv(modelOverride) }}, - {"Shell rc file", func() (ResolvedEndpoint, bool, error) { return tryShellRC(modelOverride) }}, + {configStrategyName, func() (ResolvedEndpoint, bool, error) { return tryOCRConfig(configPath, "", modelOverride) }}, + {environmentStrategyName, func() (ResolvedEndpoint, bool, error) { return tryOCREnv(modelOverride) }}, + {claudeCodeStrategyName, func() (ResolvedEndpoint, bool, error) { return tryCCEnv(modelOverride) }}, + {shellRCStrategyName, func() (ResolvedEndpoint, bool, error) { return tryShellRC(modelOverride) }}, } for _, s := range strategies { @@ -84,36 +111,10 @@ func ResolveEndpointWithModelOverride(configPath, modelOverride string) (Resolve return ResolvedEndpoint{}, fmt.Errorf("resolve %s: %w", s.name, err) } if ok && ep.URL != "" && ep.Token != "" && ep.Model != "" { - if ep.Source == "" { - ep.Source = s.name - } - ep.Model = stripModelSuffix(ep.Model) - // OCR_LLM_TIMEOUT is a global override: applies regardless of - // which strategy resolved the endpoint, and takes precedence - // over config-file values when set. - envTimeout, ok, err := parseTimeoutEnv() + ep, err = finalizeResolvedEndpoint(ep, s.name) if err != nil { return ResolvedEndpoint{}, fmt.Errorf("resolve %s: %w", s.name, err) } - if ok { - ep.Timeout = envTimeout - } - // OCR_LLM_EXTRA_HEADERS is a global override: merges into - // extra headers regardless of which strategy resolved the - // endpoint. Env values take precedence over config-file values. - if raw := os.Getenv(envOCRLLMExtraHeaders); raw != "" { - envHeaders, err := ParseExtraHeaders(raw) - if err != nil { - return ResolvedEndpoint{}, fmt.Errorf("resolve %s: %w", s.name, err) - } - if ep.ExtraHeaders == nil { - ep.ExtraHeaders = envHeaders - } else { - for k, v := range envHeaders { - ep.ExtraHeaders[k] = v - } - } - } return ep, nil } } @@ -121,6 +122,40 @@ func ResolveEndpointWithModelOverride(configPath, modelOverride string) (Resolve return ResolvedEndpoint{}, fmt.Errorf("no valid LLM endpoint configured; one of OCR_LLM_URL/OCR_LLM_TOKEN/OCR_LLM_MODEL, ~/.opencodereview/config.json, or ANTHROPIC_BASE_URL/ANTHROPIC_AUTH_TOKEN/ANTHROPIC_MODEL must be set") } +func finalizeResolvedEndpoint(ep ResolvedEndpoint, strategyName string) (ResolvedEndpoint, error) { + if ep.Source == "" { + ep.Source = strategyName + } + ep.Model = stripModelSuffix(ep.Model) + // OCR_LLM_TIMEOUT is a global override: applies regardless of + // which strategy resolved the endpoint, and takes precedence + // over config-file values when set. + envTimeout, ok, err := parseTimeoutEnv() + if err != nil { + return ResolvedEndpoint{}, err + } + if ok { + ep.Timeout = envTimeout + } + // OCR_LLM_EXTRA_HEADERS is a global override: merges into extra headers + // regardless of which strategy resolved the endpoint. Env values take + // precedence over config-file values. + if raw := os.Getenv(envOCRLLMExtraHeaders); raw != "" { + envHeaders, err := ParseExtraHeaders(raw) + if err != nil { + return ResolvedEndpoint{}, err + } + if ep.ExtraHeaders == nil { + ep.ExtraHeaders = envHeaders + } else { + for k, v := range envHeaders { + ep.ExtraHeaders[k] = v + } + } + } + return ep, nil +} + // parseTimeoutEnv reads and validates the OCR_LLM_TIMEOUT environment variable. // Returns the parsed duration and true if set, or 0 and false if unset/empty. // Returns an error for invalid values (non-integer, negative, overflow) to give @@ -241,10 +276,13 @@ type configFile struct { } // tryOCRConfig reads the OCR config file. -func tryOCRConfig(path, modelOverride string) (ResolvedEndpoint, bool, error) { +func tryOCRConfig(path, providerOverride, modelOverride string) (ResolvedEndpoint, bool, error) { data, err := os.ReadFile(path) if err != nil { if os.IsNotExist(err) { + if providerOverride != "" { + return ResolvedEndpoint{}, false, fmt.Errorf("provider %q requires a config file", providerOverride) + } return ResolvedEndpoint{}, false, nil } return ResolvedEndpoint{}, false, err @@ -255,30 +293,33 @@ func tryOCRConfig(path, modelOverride string) (ResolvedEndpoint, bool, error) { return ResolvedEndpoint{}, false, fmt.Errorf("parse config: %w", err) } + if providerOverride != "" { + return tryProviderConfig(cfg, providerOverride, true, modelOverride) + } if cfg.Provider != "" { - return tryProviderConfig(cfg, modelOverride) + return tryProviderConfig(cfg, cfg.Provider, false, modelOverride) } return tryLegacyLlmConfig(cfg, modelOverride) } // tryProviderConfig resolves an endpoint from the provider-based configuration. -func tryProviderConfig(cfg configFile, modelOverride string) (ResolvedEndpoint, bool, error) { - preset, isPreset := LookupProvider(cfg.Provider) +func tryProviderConfig(cfg configFile, providerName string, isProviderOverride bool, modelOverride string) (ResolvedEndpoint, bool, error) { + preset, isPreset := LookupProvider(providerName) var entry providerEntryConfig var ok bool if isPreset { - entry, ok = cfg.Providers[cfg.Provider] + entry, ok = cfg.Providers[providerName] } else { - entry, ok = cfg.CustomProviders[cfg.Provider] + entry, ok = cfg.CustomProviders[providerName] } if !ok { - section := "providers" + section := providersSectionName if !isPreset { - section = "custom_providers" + section = customProvidersSectionName } - return ResolvedEndpoint{}, false, fmt.Errorf("provider %q is set but not configured in %s section", cfg.Provider, section) + return ResolvedEndpoint{}, false, fmt.Errorf("provider %q is set but not configured in %s section", providerName, section) } apiKey := entry.APIKey @@ -288,7 +329,7 @@ func tryProviderConfig(cfg configFile, modelOverride string) (ResolvedEndpoint, } } if apiKey == "" { - return ResolvedEndpoint{}, false, fmt.Errorf("provider %q has no api_key configured and no environment variable fallback found", cfg.Provider) + return ResolvedEndpoint{}, false, fmt.Errorf("provider %q has no api_key configured and no environment variable fallback found", providerName) } var url, protocol, authHeader, model string @@ -298,7 +339,7 @@ func tryProviderConfig(cfg configFile, modelOverride string) (ResolvedEndpoint, url = preset.BaseURL protocol = NormalizeProtocol(preset.Protocol) if err := ValidateProtocol(protocol); err != nil { - return ResolvedEndpoint{}, false, fmt.Errorf("provider %q: %w", cfg.Provider, err) + return ResolvedEndpoint{}, false, fmt.Errorf("provider %q: %w", providerName, err) } authHeader = preset.AuthHeader if entry.URL != "" { @@ -307,24 +348,24 @@ func tryProviderConfig(cfg configFile, modelOverride string) (ResolvedEndpoint, if entry.Protocol != "" { normalized := NormalizeProtocol(entry.Protocol) if err := ValidateProtocol(normalized); err != nil { - return ResolvedEndpoint{}, false, fmt.Errorf("provider %q: %w", cfg.Provider, err) + return ResolvedEndpoint{}, false, fmt.Errorf("provider %q: %w", providerName, err) } protocol = normalized } } else { - // Custom provider: url and protocol are required; model can come from cfg.Model. + // Custom provider: url and protocol are required. if entry.URL == "" || entry.Protocol == "" { - return ResolvedEndpoint{}, false, fmt.Errorf("custom provider %q requires url and protocol fields", cfg.Provider) + return ResolvedEndpoint{}, false, fmt.Errorf("custom provider %q requires url and protocol fields", providerName) } normalized := NormalizeProtocol(entry.Protocol) if err := ValidateProtocol(normalized); err != nil { - return ResolvedEndpoint{}, false, fmt.Errorf("custom provider %q: %w", cfg.Provider, err) + return ResolvedEndpoint{}, false, fmt.Errorf("custom provider %q: %w", providerName, err) } url = entry.URL protocol = normalized } - if cfg.Model != "" { + if cfg.Model != "" && !isProviderOverride { model = cfg.Model } if entry.Model != "" { @@ -345,7 +386,7 @@ func tryProviderConfig(cfg configFile, modelOverride string) (ResolvedEndpoint, return ResolvedEndpoint{}, false, fmt.Errorf( "model %q is not available for provider %q; available models: %s", modelOverride, - cfg.Provider, + providerName, strings.Join(availableModels, ", "), ) } @@ -354,7 +395,7 @@ func tryProviderConfig(cfg configFile, modelOverride string) (ResolvedEndpoint, } if model == "" { - return ResolvedEndpoint{}, false, fmt.Errorf("provider %q has no model configured; run 'ocr config model' to select one or pass --model", cfg.Provider) + return ResolvedEndpoint{}, false, fmt.Errorf("provider %q has no model configured; run 'ocr config model' to select one or pass --model", providerName) } if protocol == ProtocolAnthropic { @@ -368,7 +409,7 @@ func tryProviderConfig(cfg configFile, modelOverride string) (ResolvedEndpoint, } authHeader, err = NormalizeAuthHeader(ah) if err != nil { - return ResolvedEndpoint{}, false, fmt.Errorf("provider %q: %w", cfg.Provider, err) + return ResolvedEndpoint{}, false, fmt.Errorf("provider %q: %w", providerName, err) } if authHeader == "" { authHeader = defaultAuthHeader(protocol) @@ -382,7 +423,7 @@ func tryProviderConfig(cfg configFile, modelOverride string) (ResolvedEndpoint, timeout, err := validateTimeoutSec(entry.TimeoutSec) if err != nil { - return ResolvedEndpoint{}, false, fmt.Errorf("provider %q: %w", cfg.Provider, err) + return ResolvedEndpoint{}, false, fmt.Errorf("provider %q: %w", providerName, err) } if protocol == ProtocolAnthropic { @@ -395,7 +436,7 @@ func tryProviderConfig(cfg configFile, modelOverride string) (ResolvedEndpoint, Model: model, Protocol: protocol, AuthHeader: authHeader, - Source: "provider:" + cfg.Provider, + Source: providerSourcePrefix + providerName, ExtraBody: extraBody, ExtraHeaders: extraHeaders, Timeout: timeout, diff --git a/internal/llm/resolver_test.go b/internal/llm/resolver_test.go index c9953768..be33a852 100644 --- a/internal/llm/resolver_test.go +++ b/internal/llm/resolver_test.go @@ -415,6 +415,111 @@ func TestResolveEndpointWithModelOverride_CustomProviderWithoutConfiguredModel(t } } +func TestResolveEndpointWithProviderOverride_UsesConfiguredPresetProvider(t *testing.T) { + clearAllEnv(t) + + cfg := configFile{ + Provider: "anthropic", + Providers: map[string]providerEntryConfig{ + "anthropic": {APIKey: "sk-ant-test", Model: "claude-sonnet-4-6"}, + "openai": {APIKey: "sk-openai-test", Model: "gpt-4o"}, + }, + } + data, _ := json.Marshal(cfg) + cfgPath := filepath.Join(t.TempDir(), "config.json") + if err := os.WriteFile(cfgPath, data, 0644); err != nil { + t.Fatalf("write config: %v", err) + } + + ep, err := ResolveEndpointWithOverrides(cfgPath, "openai", "") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if ep.Source != "provider:openai" { + t.Errorf("Source = %q, want %q", ep.Source, "provider:openai") + } + if ep.Protocol != ProtocolOpenAIChatCompletions { + t.Errorf("Protocol = %q, want %q", ep.Protocol, ProtocolOpenAIChatCompletions) + } + if ep.Token != "sk-openai-test" { + t.Errorf("Token = %q, want %q", ep.Token, "sk-openai-test") + } + if ep.Model != "gpt-4o" { + t.Errorf("Model = %q, want %q", ep.Model, "gpt-4o") + } +} + +func TestResolveEndpointWithProviderOverride_RequiresConfiguredProvider(t *testing.T) { + clearAllEnv(t) + t.Setenv("OCR_LLM_URL", "https://api.example.com/v1") + t.Setenv("OCR_LLM_TOKEN", "env-token") + t.Setenv("OCR_LLM_MODEL", "env-model") + + cfg := configFile{ + Provider: "anthropic", + Providers: map[string]providerEntryConfig{ + "anthropic": {APIKey: "sk-ant-test", Model: "claude-sonnet-4-6"}, + }, + } + data, _ := json.Marshal(cfg) + cfgPath := filepath.Join(t.TempDir(), "config.json") + if err := os.WriteFile(cfgPath, data, 0644); err != nil { + t.Fatalf("write config: %v", err) + } + + _, err := ResolveEndpointWithOverrides(cfgPath, "openai", "") + if err == nil { + t.Fatal("expected error for unconfigured provider override") + } + if !strings.Contains(err.Error(), "provider \"openai\"") { + t.Errorf("error should mention requested provider, got: %v", err) + } + if strings.Contains(err.Error(), "OCR environment") { + t.Errorf("provider override should not fall back to OCR environment, got: %v", err) + } +} + +func TestResolveEndpointWithProviderOverride_DoesNotUseDefaultModelForSelectedProvider(t *testing.T) { + clearAllEnv(t) + + cfg := configFile{ + Provider: "anthropic", + Model: "claude-sonnet-4-6", + Providers: map[string]providerEntryConfig{ + "anthropic": {APIKey: "sk-ant-test"}, + }, + CustomProviders: map[string]providerEntryConfig{ + "my-gateway": { + APIKey: "token", + URL: "https://gateway.internal.com/v1", + Protocol: "openai", + Models: []string{"llama-3-70b", "llama-3-8b"}, + }, + }, + } + data, _ := json.Marshal(cfg) + cfgPath := filepath.Join(t.TempDir(), "config.json") + if err := os.WriteFile(cfgPath, data, 0644); err != nil { + t.Fatalf("write config: %v", err) + } + + _, err := ResolveEndpointWithOverrides(cfgPath, "my-gateway", "") + if err == nil { + t.Fatal("expected error when provider override has no configured model") + } + if !strings.Contains(err.Error(), "provider \"my-gateway\" has no model configured") { + t.Errorf("error should mention missing selected-provider model, got: %v", err) + } + + ep, err := ResolveEndpointWithOverrides(cfgPath, "my-gateway", "llama-3-8b") + if err != nil { + t.Fatalf("unexpected error with explicit model override: %v", err) + } + if ep.Model != "llama-3-8b" { + t.Errorf("Model = %q, want %q", ep.Model, "llama-3-8b") + } +} + func TestResolveEndpoint_ProviderAPIKeyEnvFallback(t *testing.T) { clearAllEnv(t) t.Setenv("ANTHROPIC_API_KEY", "env-api-key") diff --git a/pages/src/content/docs/en/cli-reference.md b/pages/src/content/docs/en/cli-reference.md index 5b6abb4d..b1c25baa 100644 --- a/pages/src/content/docs/en/cli-reference.md +++ b/pages/src/content/docs/en/cli-reference.md @@ -96,6 +96,7 @@ staged + unstaged + untracked changes in the current directory's repo. | `--timeout ` | — | `10` | Per-file deadline. `0` disables the timeout. | | `--rule ` | — | — | Path to a custom JSON review rule file. Overrides the project-level and global `rule.json`. | | `--max-tools ` | — | template default | Max tool-call rounds per file. `0` uses the template default (`30`); values 1–9 are clamped up to `10`; any value `≥ 10` overrides the template default (even if smaller than `30`). | +| `--provider ` | — | — | Override the resolved LLM provider for this review (e.g., `openai`). | | `--model ` | — | — | Override the resolved LLM model for this review (e.g., `claude-opus-4-6`). | | `--max-git-procs ` | — | `16` | Maximum number of concurrent git subprocesses. | | `--tools ` | — | embedded | Path to a custom JSON tool-config file. Overrides the embedded tool definitions. | diff --git a/pages/src/content/docs/ja/cli-reference.md b/pages/src/content/docs/ja/cli-reference.md index 47f39e42..15071751 100644 --- a/pages/src/content/docs/ja/cli-reference.md +++ b/pages/src/content/docs/ja/cli-reference.md @@ -92,6 +92,7 @@ ocr r [flags] (alias) | `--timeout ` | — | `10` | ファイルごとの締め切り時間。`0` でタイムアウトを無効化します。 | | `--rule ` | — | — | カスタム JSON レビュールールファイルのパス。プロジェクトレベルおよびグローバルの `rule.json` を上書きします。 | | `--max-tools ` | — | テンプレートのデフォルト | ファイルごとの最大ツール呼び出し回数。`0` はテンプレートのデフォルト(`30`)を使用します。1〜9 は `10` に引き上げられます。`≥ 10` の値はすべてテンプレートのデフォルトを上書きします(`30` より小さくても)。 | +| `--provider ` | — | — | 今回のレビューについて、解決済みの LLM provider を上書きします(例: `openai`)。 | | `--model ` | — | — | 今回のレビューについて、解決済みの LLM model を上書きします(例: `claude-opus-4-6`)。 | | `--max-git-procs ` | — | `16` | 並行 git サブプロセスの最大数。 | | `--tools ` | — | 埋め込み | カスタム JSON ツール設定ファイルのパス。埋め込みのツール定義を上書きします。 | diff --git a/pages/src/content/docs/zh/cli-reference.md b/pages/src/content/docs/zh/cli-reference.md index c2bc1b33..0099ab9d 100644 --- a/pages/src/content/docs/zh/cli-reference.md +++ b/pages/src/content/docs/zh/cli-reference.md @@ -93,6 +93,7 @@ unstaged + untracked 变更。 | `--timeout ` | — | `10` | 每文件截止时间。`0` 关闭超时。 | | `--rule ` | — | — | 自定义 JSON 评审规则文件路径。覆盖项目级与全局 `rule.json`。 | | `--max-tools ` | — | 模板默认 | 每文件最大工具调用轮数。`0` 用模板默认(`30`);1–9 会被上调到 `10`;任何 `≥ 10` 的值都覆盖模板默认(即使小于 `30`)。 | +| `--provider ` | — | — | 为本次评审覆盖已解析出的 LLM provider(如 `openai`)。 | | `--model ` | — | — | 为本次评审覆盖已解析出的 LLM model(如 `claude-opus-4-6`)。 | | `--max-git-procs ` | — | `16` | 并发 git 子进程的最大数。 | | `--tools ` | — | 内嵌 | 自定义 JSON 工具配置文件路径。覆盖内嵌工具定义。 |