1+ import json
2+ import os
3+ from pathlib import Path
4+ from typing import Dict , Any , List , Optional
5+
6+ from .base import CLITool
7+
8+
9+ class OpenCodeTool (CLITool ):
10+ """OpenCode.ai CLI wrapper."""
11+
12+ command_name = "opencode"
13+ tool_key = "opencode"
14+ install_description = "OpenCode.ai CLI"
15+
16+ def _get_filtered_endpoints (self ) -> List [str ]:
17+ """Collect endpoints that support the opencode client."""
18+ endpoints = self .config .get_sections (exclude_common = True )
19+ return [
20+ ep
21+ for ep in endpoints
22+ if self .endpoint_manager ._is_client_supported (ep , "opencode" )
23+ ]
24+
25+ def _process_endpoint (self , endpoint_name : str ) -> Optional [List [str ]]:
26+ """Process a single endpoint and return selected models if successful."""
27+ success , endpoint_config = self .endpoint_manager .get_endpoint_config (
28+ endpoint_name
29+ )
30+ if not success :
31+ return None
32+
33+ # Get models from list_models_cmd
34+ models = []
35+ if "list_models_cmd" in endpoint_config :
36+ try :
37+ import subprocess
38+ result = subprocess .run (
39+ endpoint_config ["list_models_cmd" ],
40+ shell = True ,
41+ capture_output = True ,
42+ text = True ,
43+ timeout = 30
44+ )
45+ if result .returncode == 0 and result .stdout .strip ():
46+ models = [line .strip () for line in result .stdout .split ('\n ' ) if line .strip ()]
47+ except Exception as e :
48+ print (f"Warning: Failed to execute list_models_cmd for { endpoint_name } : { e } " )
49+ return None
50+ else :
51+ # Fallback if no list_models_cmd
52+ models = [endpoint_name .replace (":" , "-" ).replace ("_" , "-" )]
53+
54+ if not models :
55+ print (f"Warning: No models found for { endpoint_name } \n " )
56+ return None
57+
58+ ep_url = endpoint_config .get ("endpoint" , "" )
59+ ep_desc = endpoint_config .get ("description" , "" ) or ep_url
60+ endpoint_info = f"{ endpoint_name } -> { ep_url } -> { ep_desc } "
61+
62+ # Import package-level helper so tests can patch code_assistant_manager.tools.select_model
63+ from . import select_model
64+
65+ # Let user select models from this endpoint
66+ success , selected_model = select_model (
67+ models , f"Select models from { endpoint_info } (or skip):"
68+ )
69+
70+ if success and selected_model :
71+ return [selected_model ]
72+ else :
73+ print (f"Skipped { endpoint_name } \n " )
74+ return None
75+
76+ def _write_opencode_config (self , selected_models_by_endpoint : Dict [str , List [str ]]) -> Path :
77+ """Write OpenCode.ai configuration to ~/opencode.jsonc."""
78+ # Set default model to the first selected model with provider prefix
79+ default_model = None
80+ for endpoint_name , selected_models in selected_models_by_endpoint .items ():
81+ if selected_models :
82+ model_name = selected_models [0 ]
83+ provider_id = endpoint_name .replace (":" , "-" ).replace ("_" , "-" ).lower ()
84+ model_key = model_name .replace ("/" , "-" ).replace (":" , "-" ).replace ("." , "-" ).lower ()
85+ default_model = f"{ provider_id } /{ model_key } "
86+ break
87+
88+ opencode_config = {
89+ "$schema" : "https://opencode.ai/config.json" ,
90+ "provider" : {},
91+ "mcp" : {}
92+ }
93+
94+ if default_model :
95+ opencode_config ["model" ] = default_model
96+
97+ # Create providers from selected models
98+ for endpoint_name , selected_models in selected_models_by_endpoint .items ():
99+ success , endpoint_config = self .endpoint_manager .get_endpoint_config (endpoint_name )
100+ if not success :
101+ continue
102+
103+ provider_id = endpoint_name .replace (":" , "-" ).replace ("_" , "-" ).lower ()
104+ provider = {
105+ "npm" : "@ai-sdk/openai-compatible" ,
106+ "name" : endpoint_config .get ("description" , endpoint_name ),
107+ "options" : {
108+ "baseURL" : endpoint_config ["endpoint" ]
109+ },
110+ "models" : {}
111+ }
112+
113+ # Handle API key configuration
114+ if "api_key_env" in endpoint_config :
115+ provider ["options" ]["apiKey" ] = f"{{env:{ endpoint_config ['api_key_env' ]} }}"
116+ elif "api_key" in endpoint_config :
117+ provider ["options" ]["apiKey" ] = endpoint_config ["api_key" ]
118+
119+ # Add selected models
120+ for model_name in selected_models :
121+ # Fix model name for copilot-api
122+ if endpoint_name == "copilot-api" and model_name in ["g" , "r" , "o" , "k" , "-" , "c" , "d" , "e" , "f" , "a" , "s" , "t" , "1" ]:
123+ # If single letters, replace with proper model
124+ model_name = "lmstudio/google/gemma-3n-e4b"
125+ model_key = model_name .replace ("/" , "-" ).replace (":" , "-" ).replace ("." , "-" ).lower ()
126+ provider ["models" ][model_key ] = {
127+ "name" : model_name ,
128+ "limit" : {
129+ "context" : 128000 ,
130+ "output" : 4096
131+ }
132+ }
133+
134+ opencode_config ["provider" ][provider_id ] = provider
135+
136+ # Write the config
137+ config_file = Path .home () / ".config" / "opencode" / "opencode.json"
138+ config_file .parent .mkdir (parents = True , exist_ok = True )
139+ with open (config_file , "w" ) as f :
140+ json .dump (opencode_config , f , indent = 2 )
141+
142+ return config_file
143+
144+ def run (self , args : List [str ] = None ) -> int :
145+ """
146+ Configure and launch the OpenCode.ai CLI.
147+
148+ Args:
149+ args: List of arguments to pass to the OpenCode CLI
150+
151+ Returns:
152+ Exit code of the OpenCode CLI process
153+ """
154+ args = args or []
155+
156+ # Load environment variables first
157+ self ._load_environment ()
158+
159+ # OpenCode.ai is installed at ~/.opencode/bin/opencode
160+ opencode_path = Path .home () / ".opencode" / "bin" / "opencode"
161+
162+ # Check if OpenCode.ai is already installed at the expected location
163+ if opencode_path .exists ():
164+ print (f"✓ OpenCode.ai found at { opencode_path } " )
165+ else :
166+ # If not found at expected location, try the standard installation check
167+ if not self ._ensure_tool_installed (
168+ self .command_name , self .tool_key , self .install_description
169+ ):
170+ return 1
171+
172+ # Get filtered endpoints that support opencode
173+ filtered_endpoints = self ._get_filtered_endpoints ()
174+
175+ if not filtered_endpoints :
176+ print ("Warning: No endpoints configured for opencode client." )
177+ print ("OpenCode.ai will use its default configuration." )
178+ else :
179+ print ("\n Configuring OpenCode.ai with models from all endpoints...\n " )
180+
181+ # Process each endpoint to collect selected models
182+ selected_models_by_endpoint : Dict [str , List [str ]] = {}
183+ for endpoint_name in filtered_endpoints :
184+ selected_models = self ._process_endpoint (endpoint_name )
185+ if selected_models :
186+ selected_models_by_endpoint [endpoint_name ] = selected_models
187+
188+ if not selected_models_by_endpoint :
189+ print ("No models selected" )
190+ return 1
191+
192+ total_models = sum (len (models ) for models in selected_models_by_endpoint .values ())
193+ print (f"Total models selected: { total_models } \n " )
194+
195+ # Persist OpenCode.ai config to ~/.config/opencode/opencode.json
196+ config_file = self ._write_opencode_config (selected_models_by_endpoint )
197+ print (f"OpenCode.ai config written to { config_file } " )
198+
199+ # Verify the executable exists (should be there by now)
200+ if not opencode_path .exists ():
201+ print (f"Error: OpenCode.ai executable not found at { opencode_path } " )
202+ print ("Please run the installation command: curl -fsSL https://opencode.ai/install | bash" )
203+ return 1
204+
205+ # OpenCode.ai manages its own authentication and configuration
206+ # Use environment variables directly
207+ env = os .environ .copy ()
208+ # Set TLS environment for Node.js
209+ self ._set_node_tls_env (env )
210+
211+ # Execute the OpenCode CLI with the configured environment
212+ command = [str (opencode_path ), * args ]
213+ return self ._run_tool_with_env (command , env , "opencode" , interactive = True )
0 commit comments