1414import os
1515import time
1616import json
17- from typing import Dict , List , Optional , Any , Literal
17+ from typing import Dict , List , Optional , Any , Union
1818from enum import Enum
19- from dataclasses import dataclass , asdict
19+ from dataclasses import dataclass
2020from anthropic import Anthropic
2121from openai import OpenAI
2222import logging
2626logger = logging .getLogger (__name__ )
2727
2828
29- _UNSET = object ()
29+ class _UnsetType :
30+ __slots__ = ()
31+
32+ def __repr__ (self ) -> str :
33+ return "UNSET"
34+
35+
36+ _UNSET = _UnsetType ()
3037
3138
3239class TaskType (Enum ):
@@ -78,7 +85,7 @@ class LLMRouter:
7885 - Error debugging → Kimi K2 (better at technical problem-solving)
7986 - Complex installs → Kimi K2 (superior agentic capabilities)
8087
81- Includes fallback logic if primary LLM fails .
88+ Note: Fallback between providers is intentionally disabled for now .
8289 """
8390
8491 # Cost per 1M tokens (estimated, update with actual pricing)
@@ -107,8 +114,8 @@ class LLMRouter:
107114
108115 def __init__ (
109116 self ,
110- claude_api_key : Optional [str ] = _UNSET ,
111- kimi_api_key : Optional [str ] = _UNSET ,
117+ claude_api_key : Union [str , None , _UnsetType ] = _UNSET ,
118+ kimi_api_key : Union [str , None , _UnsetType ] = _UNSET ,
112119 default_provider : LLMProvider = LLMProvider .CLAUDE ,
113120 enable_fallback : bool = True ,
114121 track_costs : bool = True
@@ -133,7 +140,9 @@ def __init__(
133140 os .getenv ("MOONSHOT_API_KEY" ) if kimi_api_key is _UNSET else kimi_api_key
134141 )
135142 self .default_provider = default_provider
136- # Fallback support is intentionally disabled for now (Kimi fallback not implemented).
143+ if enable_fallback :
144+ logger .warning ("Fallback is currently disabled; enable_fallback will be ignored" )
145+ # Fallback support is intentionally disabled for now.
137146 self .enable_fallback = False
138147 self .track_costs = track_costs
139148
@@ -180,6 +189,11 @@ def route_task(
180189 RoutingDecision with provider and reasoning
181190 """
182191 if force_provider :
192+ # Forced provider still needs to be configured to avoid confusing failures later.
193+ if force_provider == LLMProvider .CLAUDE and not self .claude_client :
194+ raise RuntimeError ("Claude API not configured" )
195+ if force_provider == LLMProvider .KIMI_K2 and not self .kimi_client :
196+ raise RuntimeError ("Kimi K2 API not configured" )
183197 return RoutingDecision (
184198 provider = force_provider ,
185199 task_type = task_type ,
@@ -255,7 +269,7 @@ def complete(
255269 return response
256270
257271 except Exception as e :
258- logger .error (f"❌ Error with { routing .provider .value } : { e } " )
272+ logger .exception (f"❌ Error with { routing .provider .value } : { e } " )
259273 raise
260274
261275 def _complete_claude (
@@ -267,16 +281,26 @@ def _complete_claude(
267281 ) -> LLMResponse :
268282 """Generate completion using Claude API."""
269283 # Anthropic supports a single system prompt separate from messages.
270- system_message : Optional [str ] = None
284+ system_messages : List [str ] = []
271285 user_messages : List [Dict [str , str ]] = []
272286
273287 for message in messages :
274288 role = message .get ("role" )
289+ if role not in {"system" , "user" , "assistant" }:
290+ raise ValueError (f"Invalid role for Claude: { role !r} " )
291+
275292 content = message .get ("content" , "" )
293+ if content is None :
294+ content = ""
295+
276296 if role == "system" :
277- system_message = content
297+ if content :
298+ system_messages .append (str (content ))
278299 else :
279- user_messages .append ({"role" : role , "content" : content })
300+ user_messages .append ({"role" : role , "content" : str (content )})
301+
302+ if not user_messages :
303+ raise ValueError ("Claude requires at least one non-system message" )
280304
281305 kwargs : Dict [str , Any ] = {
282306 "model" : "claude-sonnet-4-20250514" ,
@@ -285,19 +309,22 @@ def _complete_claude(
285309 "messages" : user_messages ,
286310 }
287311
288- if system_message :
289- kwargs ["system" ] = system_message
312+ if system_messages :
313+ kwargs ["system" ] = " \n \n " . join ( system_messages )
290314
291315 if tools :
292316 kwargs ["tools" ] = tools
293317
294318 response = self .claude_client .messages .create (** kwargs )
295319
296320 # Extract content
297- content_text = ""
321+ content_parts : List [ str ] = []
298322 for block in response .content :
299- if hasattr (block , "text" ):
300- content_text += block .text
323+ text = getattr (block , "text" , None )
324+ if text :
325+ content_parts .append (text )
326+
327+ content_text = "" .join (content_parts )
301328
302329 input_tokens = response .usage .input_tokens
303330 output_tokens = response .usage .output_tokens
0 commit comments