Skip to content

Commit a84cedd

Browse files
authored
Merge pull request #62 from zhujian0805/main
2 parents eb9dc7b + 412c829 commit a84cedd

File tree

2 files changed

+93
-22
lines changed

2 files changed

+93
-22
lines changed

code_assistant_manager/tools/opencode.py

Lines changed: 35 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,29 @@ def _process_endpoint(self, endpoint_name: str) -> Optional[List[str]]:
8585

8686
def _write_opencode_config(self, selected_models_by_endpoint: Dict[str, List[str]]) -> Path:
8787
"""Write OpenCode.ai configuration to ~/.config/opencode/opencode.json."""
88+
config_file = Path.home() / ".config" / "opencode" / "opencode.json"
89+
90+
# Load existing config to preserve all existing settings
91+
existing = {}
92+
if config_file.exists():
93+
try:
94+
existing = json.loads(config_file.read_text(encoding="utf-8"))
95+
if not isinstance(existing, dict):
96+
existing = {}
97+
except Exception:
98+
pass
99+
100+
opencode_config = {
101+
"$schema": "https://opencode.ai/config.json",
102+
"provider": existing.get("provider", {}),
103+
"mcp": existing.get("mcp", {}),
104+
}
105+
106+
# Preserve other top-level keys from existing config (e.g. theme, keybinds)
107+
for key, val in existing.items():
108+
if key not in opencode_config:
109+
opencode_config[key] = val
110+
88111
# Set default model to the first selected model with provider prefix
89112
default_model = None
90113
for endpoint_name, selected_models in selected_models_by_endpoint.items():
@@ -95,42 +118,43 @@ def _write_opencode_config(self, selected_models_by_endpoint: Dict[str, List[str
95118
default_model = f"{provider_id}/{model_key}"
96119
break
97120

98-
opencode_config = {
99-
"$schema": "https://opencode.ai/config.json",
100-
"provider": {},
101-
"mcp": {},
102-
}
103-
104121
if default_model:
105122
opencode_config["model"] = default_model
106123

107-
# Create providers from selected models
124+
# Merge providers from selected models into existing providers
108125
for endpoint_name, selected_models in selected_models_by_endpoint.items():
109126
success, endpoint_config = self.endpoint_manager.get_endpoint_config(endpoint_name)
110127
if not success:
111128
continue
112129

113130
provider_id = endpoint_name.replace(":", "-").replace("_", "-").lower()
114-
provider = {
131+
132+
# Start from existing provider entry if present, otherwise create new
133+
provider = opencode_config["provider"].get(provider_id, {
115134
"npm": "@ai-sdk/openai-compatible",
116135
"name": endpoint_config.get("description", endpoint_name),
117136
"options": {
118137
"baseURL": endpoint_config["endpoint"]
119138
},
120139
"models": {}
121-
}
140+
})
141+
142+
# Update mutable fields so they stay current
143+
provider["npm"] = "@ai-sdk/openai-compatible"
144+
provider["name"] = endpoint_config.get("description", endpoint_name)
145+
provider.setdefault("options", {})["baseURL"] = endpoint_config["endpoint"]
146+
provider.setdefault("models", {})
122147

123148
# Handle API key configuration
124149
if "api_key_env" in endpoint_config:
125150
provider["options"]["apiKey"] = f"{{env:{endpoint_config['api_key_env']}}}"
126151
elif "api_key" in endpoint_config:
127152
provider["options"]["apiKey"] = endpoint_config["api_key"]
128153

129-
# Add selected models
154+
# Append selected models (existing models are preserved)
130155
for model_name in selected_models:
131156
# Fix model name for copilot-api
132157
if endpoint_name == "copilot-api" and model_name in ["g", "r", "o", "k", "-", "c", "d", "e", "f", "a", "s", "t", "1"]:
133-
# If single letters, replace with proper model
134158
model_name = "lmstudio/google/gemma-3n-e4b"
135159
model_key = model_name.replace("/", "-").replace(":", "-").replace(".", "-").lower()
136160
provider["models"][model_key] = {
@@ -143,16 +167,6 @@ def _write_opencode_config(self, selected_models_by_endpoint: Dict[str, List[str
143167

144168
opencode_config["provider"][provider_id] = provider
145169

146-
# Preserve existing MCP servers (if any)
147-
config_file = Path.home() / ".config" / "opencode" / "opencode.json"
148-
if config_file.exists():
149-
try:
150-
existing = json.loads(config_file.read_text(encoding="utf-8"))
151-
if isinstance(existing, dict) and isinstance(existing.get("mcp"), dict):
152-
opencode_config["mcp"] = existing["mcp"]
153-
except Exception:
154-
pass
155-
156170
# Write the config
157171
config_file.parent.mkdir(parents=True, exist_ok=True)
158172
with open(config_file, "w", encoding="utf-8") as f:

tests/test_tools_opencode.py

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,4 +191,61 @@ def test_run_with_model_selection(self, mock_subprocess, mock_select_model, mock
191191
})
192192

193193
result = tool.run([])
194-
assert result == 0
194+
assert result == 0
195+
196+
def test_write_opencode_config_appends_existing(self, opencode_config):
197+
"""Test that _write_opencode_config merges into existing config instead of overwriting."""
198+
config = ConfigManager(opencode_config)
199+
tool = OpenCodeTool(config)
200+
201+
existing_config = {
202+
"$schema": "https://opencode.ai/config.json",
203+
"model": "old-provider/old-model",
204+
"provider": {
205+
"old-provider": {
206+
"npm": "@ai-sdk/openai-compatible",
207+
"name": "Old Provider",
208+
"options": {"baseURL": "https://old.example.com/v1"},
209+
"models": {
210+
"old-model": {
211+
"name": "old-model",
212+
"limit": {"context": 128000, "output": 4096}
213+
}
214+
}
215+
}
216+
},
217+
"mcp": {"my-server": {"command": "npx my-mcp-server"}},
218+
"theme": "dark",
219+
}
220+
221+
with patch("pathlib.Path.home") as mock_home:
222+
tmp_config_dir = Path(tempfile.mkdtemp())
223+
opencode_dir = tmp_config_dir / ".config" / "opencode"
224+
opencode_dir.mkdir(parents=True)
225+
config_file = opencode_dir / "opencode.json"
226+
config_file.write_text(json.dumps(existing_config), encoding="utf-8")
227+
mock_home.return_value = tmp_config_dir
228+
229+
selected_models = {"test-provider": ["gpt-4"]}
230+
231+
with patch.object(tool.endpoint_manager, "get_endpoint_config") as mock_get_config:
232+
mock_get_config.return_value = (True, {
233+
"endpoint": "https://api.test.com/v1",
234+
"description": "Test Provider",
235+
"api_key": "test-key"
236+
})
237+
238+
result_file = tool._write_opencode_config(selected_models)
239+
written = json.loads(result_file.read_text(encoding="utf-8"))
240+
241+
# Existing provider must still be present
242+
assert "old-provider" in written["provider"]
243+
assert "old-model" in written["provider"]["old-provider"]["models"]
244+
245+
# New provider must be added
246+
assert "test-provider" in written["provider"]
247+
assert "gpt-4" in written["provider"]["test-provider"]["models"]
248+
249+
# MCP and other top-level keys must be preserved
250+
assert written["mcp"] == {"my-server": {"command": "npx my-mcp-server"}}
251+
assert written.get("theme") == "dark"

0 commit comments

Comments
 (0)