weco 0.2.19__py3-none-any.whl → 0.2.22__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
weco/__init__.py CHANGED
@@ -5,11 +5,5 @@ import importlib.metadata
5
5
  __pkg_version__ = importlib.metadata.version("weco")
6
6
  __api_version__ = "v1"
7
7
 
8
- __base_url__ = f"https://api.weco.ai/{__api_version__}"
9
- # If user specifies a custom base URL, use that instead
10
- if os.environ.get("WECO_BASE_URL"):
11
- __base_url__ = os.environ.get("WECO_BASE_URL")
12
-
13
- __dashboard_url__ = "https://dashboard.weco.ai"
14
- if os.environ.get("WECO_DASHBOARD_URL"):
15
- __dashboard_url__ = os.environ.get("WECO_DASHBOARD_URL")
8
+ __base_url__ = os.environ.get("WECO_BASE_URL", f"https://api.weco.ai/{__api_version__}")
9
+ __dashboard_url__ = os.environ.get("WECO_DASHBOARD_URL", "https://dashboard.weco.ai")
weco/api.py CHANGED
@@ -1,12 +1,30 @@
1
- from typing import Dict, Any, Optional
2
- import rich
3
- import requests
4
- from weco import __pkg_version__, __base_url__
5
1
  import sys
2
+ from typing import Dict, Any, Optional, Union, Tuple, List
3
+
4
+ import requests
5
+ from requests.adapters import HTTPAdapter
6
+ from urllib3.util.retry import Retry
6
7
  from rich.console import Console
7
8
 
9
+ from weco import __pkg_version__, __base_url__
10
+
11
+
12
+ # --- Session Configuration ---
13
+ def _get_weco_session() -> requests.Session:
14
+ session = requests.Session()
15
+ retry_strategy = Retry(
16
+ total=3,
17
+ status_forcelist=[429, 500, 502, 503, 504], # Retry on these server errors and rate limiting
18
+ allowed_methods=["HEAD", "GET", "PUT", "POST", "DELETE", "OPTIONS"], # Case-insensitive
19
+ backoff_factor=1, # e.g., sleep for 0s, 2s, 4s between retries (factor * (2 ** ({number of total retries} - 1)))
20
+ )
21
+ adapter = HTTPAdapter(max_retries=retry_strategy)
22
+ session.mount("http://", adapter)
23
+ session.mount("https://", adapter)
24
+ return session
25
+
8
26
 
9
- def handle_api_error(e: requests.exceptions.HTTPError, console: rich.console.Console) -> None:
27
+ def handle_api_error(e: requests.exceptions.HTTPError, console: Console) -> None:
10
28
  """Extract and display error messages from API responses in a structured format."""
11
29
  try:
12
30
  detail = e.response.json()["detail"]
@@ -17,8 +35,8 @@ def handle_api_error(e: requests.exceptions.HTTPError, console: rich.console.Con
17
35
  # sys.exit(1)
18
36
 
19
37
 
20
- def start_optimization_session(
21
- console: rich.console.Console,
38
+ def start_optimization_run(
39
+ console: Console,
22
40
  source_code: str,
23
41
  evaluation_command: str,
24
42
  metric_name: str,
@@ -29,14 +47,15 @@ def start_optimization_session(
29
47
  search_policy_config: Dict[str, Any],
30
48
  additional_instructions: str = None,
31
49
  api_keys: Dict[str, Any] = {},
32
- auth_headers: dict = {}, # Add auth_headers
33
- timeout: int = 800,
50
+ auth_headers: dict = {},
51
+ timeout: Union[int, Tuple[int, int]] = 800,
34
52
  ) -> Dict[str, Any]:
35
- """Start the optimization session."""
53
+ """Start the optimization run."""
36
54
  with console.status("[bold green]Starting Optimization..."):
37
55
  try:
38
- response = requests.post(
39
- f"{__base_url__}/sessions", # Path is relative to base_url
56
+ session = _get_weco_session()
57
+ response = session.post(
58
+ f"{__base_url__}/runs",
40
59
  json={
41
60
  "source_code": source_code,
42
61
  "additional_instructions": additional_instructions,
@@ -49,37 +68,38 @@ def start_optimization_session(
49
68
  },
50
69
  "metadata": {"client_name": "cli", "client_version": __pkg_version__, **api_keys},
51
70
  },
52
- headers=auth_headers, # Add headers
71
+ headers=auth_headers,
53
72
  timeout=timeout,
54
73
  )
55
74
  response.raise_for_status()
56
75
  return response.json()
57
76
  except requests.exceptions.HTTPError as e:
58
77
  handle_api_error(e, console)
59
- sys.exit(1) # Exit if starting session fails
78
+ sys.exit(1)
60
79
  except requests.exceptions.RequestException as e:
61
- console.print(f"[bold red]Network Error starting session: {e}[/]")
80
+ console.print(f"[bold red]Network Error starting run: {e}[/]")
62
81
  sys.exit(1)
63
82
 
64
83
 
65
84
  def evaluate_feedback_then_suggest_next_solution(
66
- session_id: str,
85
+ run_id: str,
67
86
  execution_output: str,
68
87
  additional_instructions: str = None,
69
88
  api_keys: Dict[str, Any] = {},
70
- auth_headers: dict = {}, # Add auth_headers
71
- timeout: int = 800,
89
+ auth_headers: dict = {},
90
+ timeout: Union[int, Tuple[int, int]] = 800,
72
91
  ) -> Dict[str, Any]:
73
92
  """Evaluate the feedback and suggest the next solution."""
74
93
  try:
75
- response = requests.post(
76
- f"{__base_url__}/sessions/{session_id}/suggest", # Path is relative to base_url
94
+ session = _get_weco_session()
95
+ response = session.post(
96
+ f"{__base_url__}/runs/{run_id}/suggest",
77
97
  json={
78
98
  "execution_output": execution_output,
79
99
  "additional_instructions": additional_instructions,
80
100
  "metadata": {**api_keys},
81
101
  },
82
- headers=auth_headers, # Add headers
102
+ headers=auth_headers,
83
103
  timeout=timeout,
84
104
  )
85
105
  response.raise_for_status()
@@ -93,16 +113,14 @@ def evaluate_feedback_then_suggest_next_solution(
93
113
  raise # Re-raise the exception
94
114
 
95
115
 
96
- def get_optimization_session_status(
97
- session_id: str, include_history: bool = False, auth_headers: dict = {}, timeout: int = 800
116
+ def get_optimization_run_status(
117
+ run_id: str, include_history: bool = False, auth_headers: dict = {}, timeout: Union[int, Tuple[int, int]] = 800
98
118
  ) -> Dict[str, Any]:
99
- """Get the current status of the optimization session."""
119
+ """Get the current status of the optimization run."""
100
120
  try:
101
- response = requests.get(
102
- f"{__base_url__}/sessions/{session_id}", # Path is relative to base_url
103
- params={"include_history": include_history},
104
- headers=auth_headers,
105
- timeout=timeout,
121
+ session = _get_weco_session()
122
+ response = session.get(
123
+ f"{__base_url__}/runs/{run_id}", params={"include_history": include_history}, headers=auth_headers, timeout=timeout
106
124
  )
107
125
  response.raise_for_status()
108
126
  return response.json()
@@ -114,42 +132,37 @@ def get_optimization_session_status(
114
132
  raise # Re-raise
115
133
 
116
134
 
117
- def send_heartbeat(
118
- session_id: str,
119
- auth_headers: dict = {},
120
- timeout: int = 10, # Shorter timeout for non-critical heartbeat
121
- ) -> bool:
135
+ def send_heartbeat(run_id: str, auth_headers: dict = {}, timeout: Union[int, Tuple[int, int]] = 10) -> bool:
122
136
  """Send a heartbeat signal to the backend."""
123
137
  try:
124
- response = requests.put(f"{__base_url__}/sessions/{session_id}/heartbeat", headers=auth_headers, timeout=timeout)
125
- response.raise_for_status() # Raises HTTPError for bad responses (4xx or 5xx)
138
+ session = _get_weco_session()
139
+ response = session.put(f"{__base_url__}/runs/{run_id}/heartbeat", headers=auth_headers, timeout=timeout)
140
+ response.raise_for_status()
126
141
  return True
127
142
  except requests.exceptions.HTTPError as e:
128
- # Log non-critical errors like 409 Conflict (session not running)
129
143
  if e.response.status_code == 409:
130
- print(f"Heartbeat ignored: Session {session_id} is not running.", file=sys.stderr)
144
+ print(f"Heartbeat ignored: Run {run_id} is not running.", file=sys.stderr)
131
145
  else:
132
- print(f"Heartbeat failed for session {session_id}: HTTP {e.response.status_code}", file=sys.stderr)
133
- # Don't exit, just report failure
146
+ print(f"Heartbeat failed for run {run_id}: HTTP {e.response.status_code}", file=sys.stderr)
134
147
  return False
135
148
  except requests.exceptions.RequestException as e:
136
- # Network errors are also non-fatal for heartbeats
137
- print(f"Heartbeat network error for session {session_id}: {e}", file=sys.stderr)
149
+ print(f"Heartbeat network error for run {run_id}: {e}", file=sys.stderr)
138
150
  return False
139
151
 
140
152
 
141
153
  def report_termination(
142
- session_id: str,
154
+ run_id: str,
143
155
  status_update: str,
144
156
  reason: str,
145
157
  details: Optional[str] = None,
146
158
  auth_headers: dict = {},
147
- timeout: int = 30, # Reasonably longer timeout for important termination message
159
+ timeout: Union[int, Tuple[int, int]] = 30,
148
160
  ) -> bool:
149
161
  """Report the termination reason to the backend."""
150
162
  try:
151
- response = requests.post(
152
- f"{__base_url__}/sessions/{session_id}/terminate",
163
+ session = _get_weco_session()
164
+ response = session.post(
165
+ f"{__base_url__}/runs/{run_id}/terminate",
153
166
  json={"status_update": status_update, "termination_reason": reason, "termination_details": details},
154
167
  headers=auth_headers,
155
168
  timeout=timeout,
@@ -157,6 +170,190 @@ def report_termination(
157
170
  response.raise_for_status()
158
171
  return True
159
172
  except requests.exceptions.RequestException as e:
160
- # Log failure, but don't prevent CLI exit
161
- print(f"Warning: Failed to report termination to backend for session {session_id}: {e}", file=sys.stderr)
173
+ print(f"Warning: Failed to report termination to backend for run {run_id}: {e}", file=sys.stderr)
162
174
  return False
175
+
176
+
177
+ # --- Chatbot API Functions ---
178
+ def _determine_model_and_api_key() -> tuple[str, dict[str, str]]:
179
+ """Determine the model and API key to use based on available environment variables.
180
+
181
+ Uses the shared model selection logic to maintain consistency.
182
+ Returns (model_name, api_key_dict)
183
+ """
184
+ from .utils import read_api_keys_from_env, determine_default_model
185
+
186
+ llm_api_keys = read_api_keys_from_env()
187
+ model = determine_default_model(llm_api_keys)
188
+
189
+ # Create API key dictionary with only the key for the selected model
190
+ if model == "o4-mini":
191
+ api_key_dict = {"OPENAI_API_KEY": llm_api_keys["OPENAI_API_KEY"]}
192
+ elif model == "claude-sonnet-4-0":
193
+ api_key_dict = {"ANTHROPIC_API_KEY": llm_api_keys["ANTHROPIC_API_KEY"]}
194
+ elif model == "gemini-2.5-pro":
195
+ api_key_dict = {"GEMINI_API_KEY": llm_api_keys["GEMINI_API_KEY"]}
196
+ else:
197
+ # This should never happen if determine_default_model works correctly
198
+ raise ValueError(f"Unknown model returned: {model}")
199
+
200
+ return model, api_key_dict
201
+
202
+
203
+ def get_optimization_suggestions_from_codebase(
204
+ gitingest_summary: str,
205
+ gitingest_tree: str,
206
+ gitingest_content_str: str,
207
+ console: Console,
208
+ auth_headers: dict = {},
209
+ timeout: Union[int, Tuple[int, int]] = 800,
210
+ ) -> Optional[List[Dict[str, Any]]]:
211
+ """Analyze codebase and get optimization suggestions using the model-agnostic backend API."""
212
+ try:
213
+ model, api_key_dict = _determine_model_and_api_key()
214
+ session = _get_weco_session()
215
+ response = session.post(
216
+ f"{__base_url__}/onboard/analyze-codebase",
217
+ json={
218
+ "gitingest_summary": gitingest_summary,
219
+ "gitingest_tree": gitingest_tree,
220
+ "gitingest_content": gitingest_content_str,
221
+ "model": model,
222
+ "metadata": api_key_dict,
223
+ },
224
+ headers=auth_headers,
225
+ timeout=timeout,
226
+ )
227
+ response.raise_for_status()
228
+ result = response.json()
229
+ return [option for option in result.get("options", [])]
230
+
231
+ except requests.exceptions.HTTPError as e:
232
+ handle_api_error(e, console)
233
+ return None
234
+ except requests.exceptions.RequestException as e:
235
+ console.print(f"[bold red]Network Error getting optimization suggestions: {e}[/]")
236
+ return None
237
+ except Exception as e:
238
+ console.print(f"[bold red]Error calling backend API: {e}[/]")
239
+ return None
240
+
241
+
242
+ def generate_evaluation_script_and_metrics(
243
+ target_file: str,
244
+ description: str,
245
+ gitingest_content_str: str,
246
+ console: Console,
247
+ auth_headers: dict = {},
248
+ timeout: Union[int, Tuple[int, int]] = 800,
249
+ ) -> Tuple[Optional[str], Optional[str], Optional[str], Optional[str]]:
250
+ """Generate evaluation script and determine metrics using the model-agnostic backend API."""
251
+ try:
252
+ model, api_key_dict = _determine_model_and_api_key()
253
+ session = _get_weco_session()
254
+ response = session.post(
255
+ f"{__base_url__}/onboard/generate-script",
256
+ json={
257
+ "target_file": target_file,
258
+ "description": description,
259
+ "gitingest_content": gitingest_content_str,
260
+ "model": model,
261
+ "metadata": api_key_dict,
262
+ },
263
+ headers=auth_headers,
264
+ timeout=timeout,
265
+ )
266
+ response.raise_for_status()
267
+ result = response.json()
268
+ return result.get("script_content"), result.get("metric_name"), result.get("goal"), result.get("reasoning")
269
+
270
+ except requests.exceptions.HTTPError as e:
271
+ handle_api_error(e, console)
272
+ return None, None, None, None
273
+ except requests.exceptions.RequestException as e:
274
+ console.print(f"[bold red]Network Error generating evaluation script: {e}[/]")
275
+ return None, None, None, None
276
+ except Exception as e:
277
+ console.print(f"[bold red]Error calling backend API: {e}[/]")
278
+ return None, None, None, None
279
+
280
+
281
+ def analyze_evaluation_environment(
282
+ target_file: str,
283
+ description: str,
284
+ gitingest_summary: str,
285
+ gitingest_tree: str,
286
+ gitingest_content_str: str,
287
+ console: Console,
288
+ auth_headers: dict = {},
289
+ timeout: Union[int, Tuple[int, int]] = 800,
290
+ ) -> Optional[Dict[str, Any]]:
291
+ """Analyze existing evaluation scripts and environment using the model-agnostic backend API."""
292
+ try:
293
+ model, api_key_dict = _determine_model_and_api_key()
294
+ session = _get_weco_session()
295
+ response = session.post(
296
+ f"{__base_url__}/onboard/analyze-environment",
297
+ json={
298
+ "target_file": target_file,
299
+ "description": description,
300
+ "gitingest_summary": gitingest_summary,
301
+ "gitingest_tree": gitingest_tree,
302
+ "gitingest_content": gitingest_content_str,
303
+ "model": model,
304
+ "metadata": api_key_dict,
305
+ },
306
+ headers=auth_headers,
307
+ timeout=timeout,
308
+ )
309
+ response.raise_for_status()
310
+ return response.json()
311
+
312
+ except requests.exceptions.HTTPError as e:
313
+ handle_api_error(e, console)
314
+ return None
315
+ except requests.exceptions.RequestException as e:
316
+ console.print(f"[bold red]Network Error analyzing evaluation environment: {e}[/]")
317
+ return None
318
+ except Exception as e:
319
+ console.print(f"[bold red]Error calling backend API: {e}[/]")
320
+ return None
321
+
322
+
323
+ def analyze_script_execution_requirements(
324
+ script_content: str,
325
+ script_path: str,
326
+ target_file: str,
327
+ console: Console,
328
+ auth_headers: dict = {},
329
+ timeout: Union[int, Tuple[int, int]] = 800,
330
+ ) -> Optional[str]:
331
+ """Analyze script to determine proper execution command using the model-agnostic backend API."""
332
+ try:
333
+ model, api_key_dict = _determine_model_and_api_key()
334
+ session = _get_weco_session()
335
+ response = session.post(
336
+ f"{__base_url__}/onboard/analyze-script",
337
+ json={
338
+ "script_content": script_content,
339
+ "script_path": script_path,
340
+ "target_file": target_file,
341
+ "model": model,
342
+ "metadata": api_key_dict,
343
+ },
344
+ headers=auth_headers,
345
+ timeout=timeout,
346
+ )
347
+ response.raise_for_status()
348
+ result = response.json()
349
+ return result.get("command", f"python {script_path}")
350
+
351
+ except requests.exceptions.HTTPError as e:
352
+ handle_api_error(e, console)
353
+ return f"python {script_path}"
354
+ except requests.exceptions.RequestException as e:
355
+ console.print(f"[bold red]Network Error analyzing script execution: {e}[/]")
356
+ return f"python {script_path}"
357
+ except Exception as e:
358
+ console.print(f"[bold red]Error calling backend API: {e}[/]")
359
+ return f"python {script_path}"
weco/auth.py CHANGED
@@ -3,6 +3,13 @@ import os
3
3
  import pathlib
4
4
  import json
5
5
  import stat
6
+ import time
7
+ import requests
8
+ import webbrowser
9
+ from rich.console import Console
10
+ from rich.live import Live
11
+ from rich.prompt import Prompt
12
+ from . import __base_url__
6
13
 
7
14
  CONFIG_DIR = pathlib.Path.home() / ".config" / "weco"
8
15
  CREDENTIALS_FILE = CONFIG_DIR / "credentials.json"
@@ -27,10 +34,8 @@ def save_api_key(api_key: str):
27
34
  json.dump(credentials, f)
28
35
  # Set file permissions to read/write for owner only (600)
29
36
  os.chmod(CREDENTIALS_FILE, stat.S_IRUSR | stat.S_IWUSR)
30
- except IOError as e:
31
- print(f"Error: Could not write credentials file at {CREDENTIALS_FILE}: {e}")
32
37
  except OSError as e:
33
- print(f"Warning: Could not set permissions on {CREDENTIALS_FILE}: {e}")
38
+ print(f"Error: Could not write credentials file or set permissions on {CREDENTIALS_FILE}: {e}")
34
39
 
35
40
 
36
41
  def load_weco_api_key() -> str | None:
@@ -62,3 +67,159 @@ def clear_api_key():
62
67
  print(f"Error: Could not remove credentials file at {CREDENTIALS_FILE}: {e}")
63
68
  else:
64
69
  print("Already logged out.")
70
+
71
+
72
+ def perform_login(console: Console):
73
+ """Handles the device login flow."""
74
+ try:
75
+ # 1. Initiate device login
76
+ console.print("Initiating login...")
77
+ init_response = requests.post(f"{__base_url__}/auth/device/initiate")
78
+ init_response.raise_for_status()
79
+ init_data = init_response.json()
80
+
81
+ device_code = init_data["device_code"]
82
+ verification_uri = init_data["verification_uri"]
83
+ expires_in = init_data["expires_in"]
84
+ interval = init_data["interval"]
85
+
86
+ # 2. Display instructions
87
+ console.print("\n[bold yellow]Action Required:[/]")
88
+ console.print("Please open the following URL in your browser to authenticate:")
89
+ console.print(f"[link={verification_uri}]{verification_uri}[/link]")
90
+ console.print(f"This request will expire in {expires_in // 60} minutes.")
91
+ console.print("Attempting to open the authentication page in your default browser...") # Notify user
92
+
93
+ # Automatically open the browser
94
+ try:
95
+ if not webbrowser.open(verification_uri):
96
+ console.print("[yellow]Could not automatically open the browser. Please open the link manually.[/]")
97
+ except Exception as browser_err:
98
+ console.print(
99
+ f"[yellow]Could not automatically open the browser ({browser_err}). Please open the link manually.[/]"
100
+ )
101
+
102
+ console.print("Waiting for authentication...", end="")
103
+
104
+ # 3. Poll for token
105
+ start_time = time.time()
106
+ # Use a simple text update instead of Spinner within Live for potentially better compatibility
107
+ polling_status = "Waiting..."
108
+ with Live(polling_status, refresh_per_second=1, transient=True, console=console) as live_status:
109
+ while True:
110
+ # Check for timeout
111
+ if time.time() - start_time > expires_in:
112
+ console.print("\n[bold red]Error:[/] Login request timed out.")
113
+ return False
114
+
115
+ time.sleep(interval)
116
+ live_status.update("Waiting... (checking status)")
117
+
118
+ try:
119
+ token_response = requests.post(
120
+ f"{__base_url__}/auth/device/token",
121
+ json={"grant_type": "urn:ietf:params:oauth:grant-type:device_code", "device_code": device_code},
122
+ )
123
+
124
+ # Check for 202 Accepted - Authorization Pending
125
+ if token_response.status_code == 202:
126
+ token_data = token_response.json()
127
+ if token_data.get("error") == "authorization_pending":
128
+ live_status.update("Waiting... (authorization pending)")
129
+ continue # Continue polling
130
+ else:
131
+ # Unexpected 202 response format
132
+ console.print(f"\n[bold red]Error:[/] Received unexpected 202 response: {token_data}")
133
+ return False
134
+ # Check for standard OAuth2 errors (often 400 Bad Request)
135
+ elif token_response.status_code == 400:
136
+ token_data = token_response.json()
137
+ error_code = token_data.get("error", "unknown_error")
138
+ if error_code == "slow_down":
139
+ interval += 5 # Increase polling interval if instructed
140
+ live_status.update(f"Waiting... (slowing down polling to {interval}s)")
141
+ continue
142
+ elif error_code == "expired_token":
143
+ console.print("\n[bold red]Error:[/] Login request expired.")
144
+ return False
145
+ elif error_code == "access_denied":
146
+ console.print("\n[bold red]Error:[/] Authorization denied by user.")
147
+ return False
148
+ else: # invalid_grant, etc.
149
+ error_desc = token_data.get("error_description", "Unknown error during polling.")
150
+ console.print(f"\n[bold red]Error:[/] {error_desc} ({error_code})")
151
+ return False
152
+
153
+ # Check for other non-200/non-202/non-400 HTTP errors
154
+ token_response.raise_for_status()
155
+ # If successful (200 OK and no 'error' field)
156
+ token_data = token_response.json()
157
+ if "access_token" in token_data:
158
+ api_key = token_data["access_token"]
159
+ save_api_key(api_key)
160
+ console.print("\n[bold green]Login successful![/]")
161
+ return True
162
+ else:
163
+ # Unexpected successful response format
164
+ console.print("\n[bold red]Error:[/] Received unexpected response from server during polling.")
165
+ print(token_data)
166
+ return False
167
+ except requests.exceptions.RequestException as e:
168
+ # Handle network errors during polling gracefully
169
+ live_status.update("Waiting... (network error, retrying)")
170
+ console.print(f"\n[bold yellow]Warning:[/] Network error during polling: {e}. Retrying...")
171
+ time.sleep(interval * 2) # Simple backoff
172
+ except requests.exceptions.HTTPError as e:
173
+ from .api import handle_api_error # Import here to avoid circular imports
174
+
175
+ handle_api_error(e, console)
176
+ except requests.exceptions.RequestException as e:
177
+ # Catch other request errors
178
+ console.print(f"\n[bold red]Network Error:[/] {e}")
179
+ return False
180
+ except Exception as e:
181
+ console.print(f"\n[bold red]An unexpected error occurred during login:[/] {e}")
182
+ return False
183
+
184
+
185
+ def handle_authentication(console: Console, llm_api_keys: dict) -> tuple[str | None, dict]:
186
+ """
187
+ Handle the complete authentication flow.
188
+
189
+ Returns:
190
+ tuple: (weco_api_key, auth_headers)
191
+ """
192
+ weco_api_key = load_weco_api_key()
193
+
194
+ if not weco_api_key:
195
+ login_choice = Prompt.ask(
196
+ "Log in to Weco to save run history or use anonymously? ([bold]L[/]ogin / [bold]S[/]kip)",
197
+ choices=["l", "s"],
198
+ default="s",
199
+ ).lower()
200
+
201
+ if login_choice == "l":
202
+ console.print("[cyan]Starting login process...[/]")
203
+ if not perform_login(console):
204
+ console.print("[bold red]Login process failed or was cancelled.[/]")
205
+ return None, {}
206
+
207
+ weco_api_key = load_weco_api_key()
208
+ if not weco_api_key:
209
+ console.print("[bold red]Error: Login completed but failed to retrieve API key.[/]")
210
+ return None, {}
211
+
212
+ elif login_choice == "s":
213
+ console.print("[yellow]Proceeding anonymously. LLM API keys must be provided via environment variables.[/]")
214
+ if not llm_api_keys:
215
+ console.print(
216
+ "[bold red]Error:[/] No LLM API keys found in environment (e.g., OPENAI_API_KEY). Cannot proceed anonymously."
217
+ )
218
+ return None, {}
219
+
220
+ # Build auth headers
221
+ auth_headers = {}
222
+ if weco_api_key:
223
+ auth_headers["Authorization"] = f"Bearer {weco_api_key}"
224
+
225
+ return weco_api_key, auth_headers