Skip to content

Commit c7233fb

Browse files
committed
Allow users to configure which model to use (vibe-kanban 8f78285b)
Users may define different models depending on the description level (project, module, class, function, etc.) as well as define a default so that they don't necessarily have to define the model for each level.
1 parent d82b2b2 commit c7233fb

10 files changed

Lines changed: 581 additions & 31 deletions

File tree

src/code_lod/cli/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
update,
1515
validate,
1616
)
17+
from code_lod.cli.config import config_set_model
1718

1819
# Configure structlog for console output
1920
structlog.configure(
@@ -41,3 +42,4 @@
4142
app.command()(hooks.uninstall_hook)
4243
app.command()(clean.clean)
4344
app.command()(config.config)
45+
app.command()(config_set_model)

src/code_lod/cli/config.py

Lines changed: 253 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,260 @@
11
"""Config command for code-lod."""
22

3+
34
import typer
45

6+
from code_lod.config import get_paths, load_config, save_config
7+
from code_lod.llm.description_generator.generator import Provider
8+
from code_lod.models import ModelConfig, Scope
9+
510

611
def config(
7-
key: str = typer.Argument(..., help="Configuration key"),
8-
value: str = typer.Argument(None, help="Configuration value (for 'set' command)"),
12+
action: str = typer.Argument(
13+
...,
14+
help="Action to perform: get, set, list, or set-model",
15+
),
16+
key: str = typer.Argument(
17+
None,
18+
help="Configuration key (e.g., 'provider', 'auto_update', or for models: 'anthropic', 'openai')",
19+
),
20+
value: str = typer.Argument(
21+
None,
22+
help="Configuration value",
23+
),
24+
) -> None:
25+
"""Get or set configuration values.
26+
27+
Actions:
28+
- get <key>: Get a configuration value
29+
- set <key> <value>: Set a configuration value
30+
- list: List all configuration
31+
- set-model <provider> <scope> <model>: Set model for provider and scope
32+
33+
Examples:
34+
code-lod config get provider
35+
code-lod config set provider openai
36+
code-lod config list
37+
code-lod config set-model anthropic function claude-sonnet-4-5-20250929
38+
code-lod config set-model openai default gpt-4o
39+
"""
40+
from code_lod.cli import app
41+
42+
log = app.log # type: ignore[attr-defined]
43+
44+
try:
45+
paths = get_paths()
46+
except FileNotFoundError:
47+
typer.echo(
48+
"Error: code-lod not initialized. Run 'code-lod init' first.", err=True
49+
)
50+
raise typer.Exit(1)
51+
52+
config_obj = load_config(paths)
53+
54+
if action == "list":
55+
_list_config(config_obj)
56+
elif action == "get":
57+
if not key:
58+
typer.echo("Error: 'get' requires a key argument", err=True)
59+
raise typer.Exit(1)
60+
_get_config(config_obj, key)
61+
elif action == "set":
62+
if not key or not value:
63+
typer.echo("Error: 'set' requires key and value arguments", err=True)
64+
raise typer.Exit(1)
65+
_set_config(config_obj, key, value, paths, log)
66+
elif action == "set-model":
67+
# set-model <provider> <scope|default> <model>
68+
if not key or not value:
69+
typer.echo(
70+
"Error: 'set-model' requires provider and scope arguments",
71+
err=True,
72+
)
73+
raise typer.Exit(1)
74+
# For set-model, we need the model name as a third arg
75+
# But typer's arg handling makes this tricky, so we'll use a different approach
76+
typer.echo(
77+
"Error: Please use: code-lod config set-model <provider> <scope> <model>",
78+
err=True,
79+
)
80+
raise typer.Exit(1)
81+
else:
82+
typer.echo(f"Error: Unknown action '{action}'", err=True)
83+
typer.echo("Valid actions: get, set, list", err=True)
84+
raise typer.Exit(1)
85+
86+
87+
def config_set_model(
88+
provider: str = typer.Argument(..., help="Provider (openai, anthropic, mock)"),
89+
scope: str = typer.Argument(
90+
...,
91+
help="Scope (default, project, package, module, class, function)",
92+
),
93+
model: str = typer.Argument(..., help="Model name"),
994
) -> None:
10-
"""Get or set configuration values."""
11-
typer.echo("Config command not yet implemented")
95+
"""Set model for a specific provider and scope.
96+
97+
Examples:
98+
code-lod config-set-model anthropic function claude-sonnet-4-5-20250929
99+
code-lod config-set-model openai default gpt-4o
100+
"""
101+
from code_lod.cli import app
102+
103+
log = app.log # type: ignore[attr-defined]
104+
105+
try:
106+
paths = get_paths()
107+
except FileNotFoundError:
108+
typer.echo(
109+
"Error: code-lod not initialized. Run 'code-lod init' first.", err=True
110+
)
111+
raise typer.Exit(1)
112+
113+
# Validate provider
114+
try:
115+
provider_enum = Provider(provider.lower())
116+
except ValueError:
117+
typer.echo(
118+
f"Error: Invalid provider '{provider}'. Valid options: {[p.value for p in Provider]}",
119+
err=True,
120+
)
121+
raise typer.Exit(1)
122+
123+
# Validate scope
124+
if scope.lower() == "default":
125+
scope_key = None # Will use default field
126+
else:
127+
try:
128+
scope_enum = Scope(scope.lower())
129+
scope_key = scope_enum
130+
except ValueError:
131+
typer.echo(
132+
f"Error: Invalid scope '{scope}'. Valid options: default, project, package, module, class, function",
133+
err=True,
134+
)
135+
raise typer.Exit(1)
136+
137+
config_obj = load_config(paths)
138+
139+
# Get or create model config for this provider
140+
if provider_enum not in config_obj.model_settings:
141+
config_obj.model_settings[provider_enum] = ModelConfig()
142+
143+
model_config = config_obj.model_settings[provider_enum]
144+
145+
# Set the model for the scope
146+
if scope_key is None:
147+
model_config.default = model
148+
log.info("model_set", provider=provider, scope="default", model=model)
149+
typer.echo(f"Set default model for {provider} to '{model}'")
150+
else:
151+
if scope_key == Scope.CLASS:
152+
model_config.class_ = model
153+
elif scope_key == Scope.FUNCTION:
154+
model_config.function = model
155+
elif scope_key == Scope.MODULE:
156+
model_config.module = model
157+
elif scope_key == Scope.PACKAGE:
158+
model_config.package = model
159+
elif scope_key == Scope.PROJECT:
160+
model_config.project = model
161+
log.info("model_set", provider=provider, scope=scope_key.value, model=model)
162+
typer.echo(f"Set {scope_key.value} model for {provider} to '{model}'")
163+
164+
save_config(config_obj, paths)
165+
166+
167+
def _list_config(config_obj) -> None:
168+
"""List all configuration."""
169+
typer.echo("Configuration:")
170+
typer.echo(f" languages: {config_obj.languages}")
171+
typer.echo(f" auto_update: {config_obj.auto_update}")
172+
typer.echo(f" fail_on_stale: {config_obj.fail_on_stale}")
173+
typer.echo(f" provider: {config_obj.provider.value}")
174+
typer.echo("\nModel settings:")
175+
if not config_obj.model_settings:
176+
typer.echo(" (none configured)")
177+
for provider, model_config in config_obj.model_settings.items():
178+
typer.echo(f" {provider.value}:")
179+
if model_config.default:
180+
typer.echo(f" default: {model_config.default}")
181+
if model_config.project:
182+
typer.echo(f" project: {model_config.project}")
183+
if model_config.package:
184+
typer.echo(f" package: {model_config.package}")
185+
if model_config.module:
186+
typer.echo(f" module: {model_config.module}")
187+
if model_config.class_:
188+
typer.echo(f" class: {model_config.class_}")
189+
if model_config.function:
190+
typer.echo(f" function: {model_config.function}")
191+
192+
193+
def _get_config(config_obj, key: str) -> None:
194+
"""Get a configuration value."""
195+
if key == "provider":
196+
typer.echo(config_obj.provider.value)
197+
elif key == "languages":
198+
typer.echo(", ".join(config_obj.languages))
199+
elif key == "auto_update":
200+
typer.echo(str(config_obj.auto_update).lower())
201+
elif key == "fail_on_stale":
202+
typer.echo(str(config_obj.fail_on_stale).lower())
203+
elif key in ["model_settings", "models"]:
204+
for provider, model_config in config_obj.model_settings.items():
205+
typer.echo(f"{provider.value}:")
206+
if model_config.default:
207+
typer.echo(f" default: {model_config.default}")
208+
if model_config.project:
209+
typer.echo(f" project: {model_config.project}")
210+
if model_config.package:
211+
typer.echo(f" package: {model_config.package}")
212+
if model_config.module:
213+
typer.echo(f" module: {model_config.module}")
214+
if model_config.class_:
215+
typer.echo(f" class: {model_config.class_}")
216+
if model_config.function:
217+
typer.echo(f" function: {model_config.function}")
218+
else:
219+
typer.echo(f"Error: Unknown key '{key}'", err=True)
220+
typer.echo(
221+
"Valid keys: provider, languages, auto_update, fail_on_stale, models",
222+
err=True,
223+
)
224+
raise typer.Exit(1)
225+
226+
227+
def _set_config(config_obj, key: str, value: str, paths, log) -> None:
228+
"""Set a configuration value."""
229+
if key == "provider":
230+
try:
231+
config_obj.provider = Provider(value.lower())
232+
log.info("config_set", key=key, value=value)
233+
except ValueError:
234+
typer.echo(
235+
f"Error: Invalid provider '{value}'. Valid options: {[p.value for p in Provider]}",
236+
err=True,
237+
)
238+
raise typer.Exit(1)
239+
elif key == "languages":
240+
config_obj.languages = [lang.strip() for lang in value.split(",")]
241+
log.info("config_set", key=key, value=config_obj.languages)
242+
elif key == "auto_update":
243+
config_obj.auto_update = value.lower() in ["true", "1", "yes", "on"]
244+
log.info("config_set", key=key, value=config_obj.auto_update)
245+
elif key == "fail_on_stale":
246+
config_obj.fail_on_stale = value.lower() in ["true", "1", "yes", "on"]
247+
log.info("config_set", key=key, value=config_obj.fail_on_stale)
248+
else:
249+
typer.echo(f"Error: Unknown key '{key}'", err=True)
250+
typer.echo(
251+
"Valid keys: provider, languages, auto_update, fail_on_stale", err=True
252+
)
253+
typer.echo(
254+
"For model settings, use: code-lod config-set-model <provider> <scope> <model>",
255+
err=True,
256+
)
257+
raise typer.Exit(1)
258+
259+
save_config(config_obj, paths)
260+
typer.echo(f"Set {key} to '{value}'")

src/code_lod/cli/generate.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import typer
66

7-
from code_lod.config import get_paths, load_config
7+
from code_lod.config import get_model_for_scope, get_paths, load_config
88
from code_lod.models import Scope
99
from code_lod.parsers.tree_sitter_parser import get_parser
1010
from code_lod.staleness import StalenessTracker
@@ -98,8 +98,15 @@ def generate(
9898
continue
9999

100100
# Generate new description
101-
log.info("generating", name=entity.name, scope=entity.scope.value)
102-
description = generator.generate(entity)
101+
# Resolve model for this entity's scope
102+
model = get_model_for_scope(config, config.provider, entity.scope)
103+
log.info(
104+
"generating",
105+
name=entity.name,
106+
scope=entity.scope.value,
107+
model=model or "default",
108+
)
109+
description = generator.generate(entity, model=model)
103110
total_generated += 1
104111
file_generated += 1
105112

src/code_lod/config.py

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,28 @@
33
import json
44
from dataclasses import dataclass, field
55
from pathlib import Path
6+
from typing import TYPE_CHECKING
67

7-
from pydantic import BaseModel
8+
from pydantic import BaseModel, Field
89

910
from code_lod.llm.description_generator.generator import Provider
11+
from code_lod.models import ModelConfig
12+
13+
if TYPE_CHECKING:
14+
from code_lod.models import Scope
1015

1116

1217
class Config(BaseModel):
1318
"""Project configuration for code-lod."""
1419

15-
languages: list[str] = field(default_factory=lambda: ["python"])
20+
languages: list[str] = Field(default_factory=lambda: ["python"])
1621
auto_update: bool = False
1722
fail_on_stale: bool = False
1823
provider: Provider = Provider.MOCK
24+
model_settings: dict[Provider, ModelConfig] = Field(
25+
default_factory=dict,
26+
description="Model configuration for each provider (openai, anthropic, etc.)",
27+
)
1928

2029

2130
@dataclass(frozen=True)
@@ -108,3 +117,28 @@ def save_config(config: Config, paths: Paths | None = None) -> None:
108117

109118
paths.config_file.parent.mkdir(parents=True, exist_ok=True)
110119
paths.config_file.write_text(config.model_dump_json(indent=2))
120+
121+
122+
def get_model_for_scope(
123+
config: Config, provider: Provider, scope: "Scope | None"
124+
) -> str | None:
125+
"""Get the configured model for a specific provider and scope.
126+
127+
Args:
128+
config: The configuration object.
129+
provider: The LLM provider.
130+
scope: The scope to get the model for. If None, returns default.
131+
132+
Returns:
133+
The configured model name, or None if not set.
134+
"""
135+
136+
if provider not in config.model_settings:
137+
return None
138+
139+
model_config = config.model_settings[provider]
140+
141+
if scope is None:
142+
return model_config.default
143+
144+
return model_config.get_model_for_scope(scope)

src/code_lod/llm/description_generator/anthropic.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
class AnthropicDescriptionGenerator(BaseLLMDescriptionGenerator):
1111
"""Description generator using Anthropic's Claude API."""
1212

13-
MODEL = "claude-sonnet-4-5-20250929"
13+
DEFAULT_MODEL = "claude-sonnet-4-5-20250929"
1414

1515
def _create_client(self, api_key: str | None):
1616
"""Create the Anthropic client.
@@ -23,18 +23,21 @@ def _create_client(self, api_key: str | None):
2323
"""
2424
return Anthropic(api_key=api_key)
2525

26-
def _make_api_request(self, prompt: str, source: str) -> str:
26+
def _make_api_request(
27+
self, prompt: str, source: str, model: str | None = None
28+
) -> str:
2729
"""Make the Anthropic API request.
2830
2931
Args:
3032
prompt: The formatted prompt.
3133
source: The source code.
34+
model: Model name to use. If None, uses self.model.
3235
3336
Returns:
3437
The generated description.
3538
"""
3639
response = self.client.messages.create(
37-
model=self.MODEL,
40+
model=model or self.model,
3841
max_tokens=1024,
3942
messages=[
4043
{

0 commit comments

Comments
 (0)