chat-console 0.3.6__py3-none-any.whl → 0.3.7__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.
app/__init__.py CHANGED
@@ -3,4 +3,4 @@ Chat CLI
3
3
  A command-line interface for chatting with various LLM providers like ChatGPT and Claude.
4
4
  """
5
5
 
6
- __version__ = "0.3.6"
6
+ __version__ = "0.3.7"
app/api/base.py CHANGED
@@ -38,27 +38,41 @@ class BaseModelClient(ABC):
38
38
 
39
39
  logger = logging.getLogger(__name__)
40
40
 
41
+ # Safety check for None or empty string
42
+ if not model_name:
43
+ logger.warning("Empty model name passed to get_client_type_for_model")
44
+ return None
45
+
41
46
  # Get model info and provider
42
47
  model_info = CONFIG["available_models"].get(model_name)
43
48
  model_name_lower = model_name.lower()
44
49
 
50
+ # Debug log the model name
51
+ logger.info(f"Getting client type for model: {model_name}")
52
+
45
53
  # If model is in config, use its provider
46
54
  if model_info:
47
55
  provider = model_info["provider"]
56
+ logger.info(f"Found model in config with provider: {provider}")
48
57
  # For custom models, try to infer provider
49
58
  else:
50
- # First try Ollama for known model names or if selected from Ollama UI
51
- if (any(name in model_name_lower for name in ["llama", "mistral", "codellama", "gemma"]) or
52
- model_name in [m["id"] for m in CONFIG.get("ollama_models", [])]):
53
- provider = "ollama"
54
- # Then try other providers
55
- elif any(name in model_name_lower for name in ["gpt", "text-", "davinci"]):
59
+ # First check for OpenAI models - these should ALWAYS use OpenAI client
60
+ if any(name in model_name_lower for name in ["gpt", "text-", "davinci"]):
56
61
  provider = "openai"
62
+ logger.info(f"Identified as OpenAI model: {model_name}")
63
+ # Then check for Anthropic models - these should ALWAYS use Anthropic client
57
64
  elif any(name in model_name_lower for name in ["claude", "anthropic"]):
58
65
  provider = "anthropic"
66
+ logger.info(f"Identified as Anthropic model: {model_name}")
67
+ # Then try Ollama for known model names or if selected from Ollama UI
68
+ elif (any(name in model_name_lower for name in ["llama", "mistral", "codellama", "gemma"]) or
69
+ model_name in [m["id"] for m in CONFIG.get("ollama_models", [])]):
70
+ provider = "ollama"
71
+ logger.info(f"Identified as Ollama model: {model_name}")
59
72
  else:
60
73
  # Default to Ollama for unknown models
61
74
  provider = "ollama"
75
+ logger.info(f"Unknown model type, defaulting to Ollama: {model_name}")
62
76
 
63
77
  # Return appropriate client class
64
78
  if provider == "ollama":
@@ -81,6 +95,14 @@ class BaseModelClient(ABC):
81
95
 
82
96
  logger = logging.getLogger(__name__)
83
97
 
98
+ # Safety check for None or empty string
99
+ if not model_name:
100
+ logger.warning("Empty model name passed to get_client_for_model")
101
+ raise ValueError("Model name cannot be empty")
102
+
103
+ # Log the model name we're getting a client for
104
+ logger.info(f"Getting client for model: {model_name}")
105
+
84
106
  # Get model info and provider
85
107
  model_info = CONFIG["available_models"].get(model_name)
86
108
  model_name_lower = model_name.lower()
@@ -88,31 +110,35 @@ class BaseModelClient(ABC):
88
110
  # If model is in config, use its provider
89
111
  if model_info:
90
112
  provider = model_info["provider"]
113
+ logger.info(f"Found model in config with provider: {provider}")
91
114
  if not AVAILABLE_PROVIDERS[provider]:
92
115
  raise Exception(f"Provider '{provider}' is not available. Please check your configuration.")
93
116
  # For custom models, try to infer provider
94
117
  else:
95
- # First try Ollama for known model names or if selected from Ollama UI
96
- if (any(name in model_name_lower for name in ["llama", "mistral", "codellama", "gemma"]) or
97
- model_name in [m["id"] for m in CONFIG.get("ollama_models", [])]):
98
- if not AVAILABLE_PROVIDERS["ollama"]:
99
- raise Exception("Ollama server is not running. Please start Ollama and try again.")
100
- provider = "ollama"
101
- logger.info(f"Using Ollama for model: {model_name}")
102
- # Then try other providers if they're available
103
- elif any(name in model_name_lower for name in ["gpt", "text-", "davinci"]):
118
+ # First check for OpenAI models - these should ALWAYS use OpenAI client
119
+ if any(name in model_name_lower for name in ["gpt", "text-", "davinci"]):
104
120
  if not AVAILABLE_PROVIDERS["openai"]:
105
121
  raise Exception("OpenAI API key not found. Please set OPENAI_API_KEY environment variable.")
106
122
  provider = "openai"
123
+ logger.info(f"Identified as OpenAI model: {model_name}")
124
+ # Then check for Anthropic models - these should ALWAYS use Anthropic client
107
125
  elif any(name in model_name_lower for name in ["claude", "anthropic"]):
108
126
  if not AVAILABLE_PROVIDERS["anthropic"]:
109
127
  raise Exception("Anthropic API key not found. Please set ANTHROPIC_API_KEY environment variable.")
110
128
  provider = "anthropic"
129
+ logger.info(f"Identified as Anthropic model: {model_name}")
130
+ # Then try Ollama for known model names or if selected from Ollama UI
131
+ elif (any(name in model_name_lower for name in ["llama", "mistral", "codellama", "gemma"]) or
132
+ model_name in [m["id"] for m in CONFIG.get("ollama_models", [])]):
133
+ if not AVAILABLE_PROVIDERS["ollama"]:
134
+ raise Exception("Ollama server is not running. Please start Ollama and try again.")
135
+ provider = "ollama"
136
+ logger.info(f"Identified as Ollama model: {model_name}")
111
137
  else:
112
138
  # Default to Ollama for unknown models
113
139
  if AVAILABLE_PROVIDERS["ollama"]:
114
140
  provider = "ollama"
115
- logger.info(f"Defaulting to Ollama for unknown model: {model_name}")
141
+ logger.info(f"Unknown model type, defaulting to Ollama: {model_name}")
116
142
  else:
117
143
  raise Exception(f"Unknown model: {model_name}")
118
144
 
app/main.py CHANGED
@@ -707,10 +707,18 @@ class SimpleChatApp(App): # Keep SimpleChatApp class definition
707
707
  else:
708
708
  raise Exception("No valid API clients available for title generation")
709
709
 
710
- # Generate title
710
+ # Generate title - make sure we're using the right client for the model
711
711
  print(f"Calling generate_conversation_title with model: {model}")
712
712
  log(f"Calling generate_conversation_title with model: {model}")
713
- debug_log(f"Calling generate_conversation_title with model: {model}")
713
+ debug_log(f"Calling generate_conversation_title with model: {model}, client type: {type(client).__name__}")
714
+
715
+ # Double-check that we're using the right client for this model
716
+ expected_client_type = BaseModelClient.get_client_type_for_model(model)
717
+ if expected_client_type and not isinstance(client, expected_client_type):
718
+ debug_log(f"Warning: Client type mismatch. Expected {expected_client_type.__name__}, got {type(client).__name__}")
719
+ debug_log("Creating new client with correct type")
720
+ client = await BaseModelClient.get_client_for_model(model)
721
+
714
722
  title = await generate_conversation_title(content, model, client)
715
723
  debug_log(f"Generated title: {title}")
716
724
  log(f"Generated title: {title}")
@@ -729,11 +737,9 @@ class SimpleChatApp(App): # Keep SimpleChatApp class definition
729
737
  # Update conversation object
730
738
  self.current_conversation.title = title
731
739
 
732
- # IMPORTANT: Save the successful model for consistency
733
- # If the title was generated with a different model than initially selected,
734
- # update the selected_model to match so the response uses the same model
735
- debug_log(f"Using same model for chat response: '{model}'")
736
- self.selected_model = model
740
+ # DO NOT update the selected model here - keep the user's original selection
741
+ # This was causing issues with model mixing
742
+ debug_log(f"Keeping original selected model: '{self.selected_model}'")
737
743
 
738
744
  self.notify(f"Conversation title set to: {title}", severity="information", timeout=3)
739
745
 
@@ -805,17 +811,23 @@ class SimpleChatApp(App): # Keep SimpleChatApp class definition
805
811
  style = self.selected_style
806
812
 
807
813
  debug_log(f"Using model: '{model}', style: '{style}'")
814
+
815
+ # Determine the expected client type for this model
816
+ expected_client_type = BaseModelClient.get_client_type_for_model(model)
817
+ debug_log(f"Expected client type for {model}: {expected_client_type.__name__ if expected_client_type else 'None'}")
808
818
 
809
819
  # Ensure we have a valid model
810
820
  if not model:
811
821
  debug_log("Model is empty, selecting a default model")
812
- # Same fallback logic as in autotitling - this ensures consistency
822
+ # Check which providers are available and select an appropriate default
813
823
  if OPENAI_API_KEY:
814
824
  model = "gpt-3.5-turbo"
815
- debug_log("Falling back to OpenAI gpt-3.5-turbo")
825
+ expected_client_type = BaseModelClient.get_client_type_for_model(model)
826
+ debug_log(f"Falling back to OpenAI gpt-3.5-turbo with client type {expected_client_type.__name__ if expected_client_type else 'None'}")
816
827
  elif ANTHROPIC_API_KEY:
817
- model = "claude-instant-1.2"
818
- debug_log("Falling back to Anthropic claude-instant-1.2")
828
+ model = "claude-3-haiku-20240307" # Updated to newer Claude model
829
+ expected_client_type = BaseModelClient.get_client_type_for_model(model)
830
+ debug_log(f"Falling back to Anthropic Claude 3 Haiku with client type {expected_client_type.__name__ if expected_client_type else 'None'}")
819
831
  else:
820
832
  # Check for a common Ollama model
821
833
  try:
@@ -826,11 +838,13 @@ class SimpleChatApp(App): # Keep SimpleChatApp class definition
826
838
  model = models[0].get("id", "llama3")
827
839
  else:
828
840
  model = "llama3" # Common default
829
- debug_log(f"Falling back to Ollama model: {model}")
841
+ expected_client_type = BaseModelClient.get_client_type_for_model(model)
842
+ debug_log(f"Falling back to Ollama model: {model} with client type {expected_client_type.__name__ if expected_client_type else 'None'}")
830
843
  except Exception as ollama_err:
831
844
  debug_log(f"Error getting Ollama models: {str(ollama_err)}")
832
845
  model = "llama3" # Final fallback
833
- debug_log("Final fallback to llama3")
846
+ expected_client_type = BaseModelClient.get_client_type_for_model(model)
847
+ debug_log(f"Final fallback to llama3 with client type {expected_client_type.__name__ if expected_client_type else 'None'}")
834
848
 
835
849
  # Convert messages to API format with enhanced error checking
836
850
  api_messages = []
app/utils.py CHANGED
@@ -20,33 +20,63 @@ logger = logging.getLogger(__name__)
20
20
 
21
21
  async def generate_conversation_title(message: str, model: str, client: Any) -> str:
22
22
  """Generate a descriptive title for a conversation based on the first message"""
23
+ try:
24
+ from app.main import debug_log
25
+ except ImportError:
26
+ debug_log = lambda msg: None
27
+
28
+ debug_log(f"Starting title generation with model: {model}, client type: {type(client).__name__}")
29
+
23
30
  # --- Choose a specific, reliable model for title generation ---
24
- # Prefer Haiku if Anthropic is available, otherwise fallback
31
+ # First, determine if we have a valid client
32
+ if client is None:
33
+ debug_log("Client is None, will use default title")
34
+ return f"Conversation ({datetime.now().strftime('%Y-%m-%d %H:%M')})"
35
+
36
+ # Determine the best model to use for title generation
25
37
  title_model_id = None
26
- if client and isinstance(client, anthropic.AsyncAnthropic): # Check if the passed client is Anthropic
27
- # Check if Haiku is listed in the client's available models (more robust)
28
- available_anthropic_models = client.get_available_models()
29
- haiku_id = "claude-3-haiku-20240307"
30
- if any(m["id"] == haiku_id for m in available_anthropic_models):
31
- title_model_id = haiku_id
32
- logger.info(f"Using Anthropic Haiku for title generation: {title_model_id}")
33
- else:
34
- # If Haiku not found, try Sonnet
35
- sonnet_id = "claude-3-sonnet-20240229"
36
- if any(m["id"] == sonnet_id for m in available_anthropic_models):
37
- title_model_id = sonnet_id
38
- logger.info(f"Using Anthropic Sonnet for title generation: {title_model_id}")
39
- else:
40
- logger.warning(f"Neither Haiku nor Sonnet found in Anthropic client's list. Falling back.")
41
-
42
- # Fallback logic if no specific Anthropic model was found or client is not Anthropic
38
+
39
+ # Check if client is Anthropic
40
+ is_anthropic = 'anthropic' in str(type(client)).lower()
41
+ if is_anthropic:
42
+ debug_log("Using Anthropic client for title generation")
43
+ # Try to get available models safely
44
+ try:
45
+ available_anthropic_models = client.get_available_models()
46
+ debug_log(f"Found {len(available_anthropic_models)} Anthropic models")
47
+
48
+ # Try Claude 3 Haiku first (fastest)
49
+ haiku_id = "claude-3-haiku-20240307"
50
+ if any(m.get("id") == haiku_id for m in available_anthropic_models):
51
+ title_model_id = haiku_id
52
+ debug_log(f"Using Anthropic Haiku for title generation: {title_model_id}")
53
+ else:
54
+ # If Haiku not found, try Sonnet
55
+ sonnet_id = "claude-3-sonnet-20240229"
56
+ if any(m.get("id") == sonnet_id for m in available_anthropic_models):
57
+ title_model_id = sonnet_id
58
+ debug_log(f"Using Anthropic Sonnet for title generation: {title_model_id}")
59
+ else:
60
+ debug_log("Neither Haiku nor Sonnet found in Anthropic models list")
61
+ except Exception as e:
62
+ debug_log(f"Error getting Anthropic models: {str(e)}")
63
+
64
+ # Check if client is OpenAI
65
+ is_openai = 'openai' in str(type(client)).lower()
66
+ if is_openai and not title_model_id:
67
+ debug_log("Using OpenAI client for title generation")
68
+ # Use GPT-3.5 for title generation (fast and cost-effective)
69
+ title_model_id = "gpt-3.5-turbo"
70
+ debug_log(f"Using OpenAI model for title generation: {title_model_id}")
71
+
72
+ # Fallback logic if no specific model was found
43
73
  if not title_model_id:
44
- # Use the originally passed model (user's selected chat model) as the final fallback
74
+ # Use the originally passed model as the final fallback
45
75
  title_model_id = model
46
- logger.warning(f"Falling back to originally selected model for title generation: {title_model_id}")
47
- # Consider adding fallbacks to OpenAI/Ollama here if needed based on config/availability
48
-
76
+ debug_log(f"Falling back to originally selected model for title generation: {title_model_id}")
77
+
49
78
  logger.info(f"Generating title for conversation using model: {title_model_id}")
79
+ debug_log(f"Final model selected for title generation: {title_model_id}")
50
80
 
51
81
  # Create a special prompt for title generation
52
82
  title_prompt = [
@@ -65,36 +95,44 @@ async def generate_conversation_title(message: str, model: str, client: Any) ->
65
95
 
66
96
  while tries > 0:
67
97
  try:
68
- # Generate a title using the same model but with a separate request
69
- # Assuming client has a method like generate_completion or similar
70
- # Adjust the method call based on the actual client implementation
98
+ debug_log(f"Attempt {3-tries} to generate title")
99
+ # First try generate_completion if available
71
100
  if hasattr(client, 'generate_completion'):
72
- title = await client.generate_completion(
73
- messages=title_prompt,
74
- model=title_model_id, # Use the chosen title model
75
- temperature=0.7,
76
- max_tokens=60 # Titles should be short
77
- )
78
- elif hasattr(client, 'generate_stream'): # Fallback or alternative method?
79
- # If generate_completion isn't available, maybe adapt generate_stream?
80
- # This part needs clarification based on the client's capabilities.
81
- # For now, let's assume a hypothetical non-streaming call or adapt stream
82
- # Simplified adaptation: collect stream chunks
83
- title_chunks = []
84
- try:
85
- # Use the chosen title model here too
86
- async for chunk in client.generate_stream(title_prompt, title_model_id, style=""):
87
- if chunk is not None: # Ensure we only process non-None chunks
88
- title_chunks.append(chunk)
89
- title = "".join(title_chunks)
90
- # If we didn't get any content, use a default
91
- if not title.strip():
92
- title = f"Conversation ({datetime.now().strftime('%Y-%m-%d %H:%M')})"
93
- except Exception as stream_error:
94
- logger.error(f"Error during title stream processing: {str(stream_error)}")
95
- title = f"Conversation ({datetime.now().strftime('%Y-%m-%d %H:%M')})"
101
+ debug_log("Using generate_completion method")
102
+ try:
103
+ title = await client.generate_completion(
104
+ messages=title_prompt,
105
+ model=title_model_id,
106
+ temperature=0.7,
107
+ max_tokens=60 # Titles should be short
108
+ )
109
+ debug_log(f"Title generated successfully: {title}")
110
+ except Exception as completion_error:
111
+ debug_log(f"Error in generate_completion: {str(completion_error)}")
112
+ raise # Re-raise to be caught by outer try/except
113
+ # Fall back to generate_stream if completion not available
114
+ elif hasattr(client, 'generate_stream'):
115
+ debug_log("Using generate_stream method")
116
+ title_chunks = []
117
+ try:
118
+ async for chunk in client.generate_stream(title_prompt, title_model_id, style=""):
119
+ if chunk is not None:
120
+ title_chunks.append(chunk)
121
+ debug_log(f"Received chunk of length: {len(chunk)}")
122
+
123
+ title = "".join(title_chunks)
124
+ debug_log(f"Combined title from chunks: {title}")
125
+
126
+ # If we didn't get any content, use a default
127
+ if not title.strip():
128
+ debug_log("Empty title received, using default")
129
+ title = f"Conversation ({datetime.now().strftime('%Y-%m-%d %H:%M')})"
130
+ except Exception as stream_error:
131
+ debug_log(f"Error during title stream processing: {str(stream_error)}")
132
+ raise # Re-raise to be caught by outer try/except
96
133
  else:
97
- raise NotImplementedError("Client does not support a suitable method for title generation.")
134
+ debug_log("Client does not support any title generation method")
135
+ raise NotImplementedError("Client does not support a suitable method for title generation.")
98
136
 
99
137
  # Sanitize and limit the title
100
138
  title = title.strip().strip('"\'').strip()
@@ -102,20 +140,23 @@ async def generate_conversation_title(message: str, model: str, client: Any) ->
102
140
  title = title[:37] + "..."
103
141
 
104
142
  logger.info(f"Generated title: {title}")
105
- return title # Return successful title
143
+ debug_log(f"Final sanitized title: {title}")
144
+ return title # Return successful title
106
145
 
107
146
  except Exception as e:
108
147
  last_error = str(e)
109
- logger.error(f"Error generating title (tries left: {tries - 1}): {last_error}")
148
+ debug_log(f"Error generating title (tries left: {tries-1}): {last_error}")
149
+ logger.error(f"Error generating title (tries left: {tries-1}): {last_error}")
110
150
  tries -= 1
111
- if tries > 0: # Only sleep if there are more retries
151
+ if tries > 0: # Only sleep if there are more retries
112
152
  await asyncio.sleep(1) # Small delay before retry
113
153
 
114
- # If all retries fail, log the last error and return a default title
154
+ # If all retries fail, log the error and return a default title
155
+ debug_log(f"Failed to generate title after multiple retries. Using default title.")
115
156
  logger.error(f"Failed to generate title after multiple retries. Last error: {last_error}")
116
157
  return f"Conversation ({datetime.now().strftime('%Y-%m-%d %H:%M')})"
117
158
 
118
- # Make this the worker function directly
159
+ # Worker function for streaming response generation
119
160
  async def generate_streaming_response(
120
161
  app: 'SimpleChatApp',
121
162
  messages: List[Dict],
@@ -136,10 +177,12 @@ async def generate_streaming_response(
136
177
  logger.info(f"Starting streaming response with model: {model}")
137
178
  debug_log(f"Starting streaming response with model: '{model}', client type: {type(client).__name__}")
138
179
 
180
+ # Validate messages
139
181
  if not messages:
140
182
  debug_log("Error: messages list is empty")
141
183
  raise ValueError("Messages list cannot be empty")
142
184
 
185
+ # Ensure all messages have required fields
143
186
  for i, msg in enumerate(messages):
144
187
  try:
145
188
  debug_log(f"Message {i}: role={msg.get('role', 'missing')}, content_len={len(msg.get('content', ''))}")
@@ -157,14 +200,14 @@ async def generate_streaming_response(
157
200
  }
158
201
  debug_log(f"Repaired message {i}")
159
202
 
160
- import time
161
-
203
+ # Initialize variables for response tracking
162
204
  full_response = ""
163
205
  buffer = []
164
206
  last_update = time.time()
165
207
  update_interval = 0.05 # Reduced interval for more frequent updates
166
208
 
167
209
  try:
210
+ # Validate client
168
211
  if client is None:
169
212
  debug_log("Error: client is None, cannot proceed with streaming")
170
213
  raise ValueError("Model client is None, cannot proceed with streaming")
@@ -173,9 +216,15 @@ async def generate_streaming_response(
173
216
  debug_log(f"Error: client {type(client).__name__} does not have generate_stream method")
174
217
  raise ValueError(f"Client {type(client).__name__} does not support streaming")
175
218
 
219
+ # Determine client type
176
220
  is_ollama = 'ollama' in str(type(client)).lower()
177
- debug_log(f"Is Ollama client: {is_ollama}")
221
+ is_openai = 'openai' in str(type(client)).lower()
222
+ is_anthropic = 'anthropic' in str(type(client)).lower()
223
+
224
+ debug_log(f"Client types - Ollama: {is_ollama}, OpenAI: {is_openai}, Anthropic: {is_anthropic}")
178
225
 
226
+ # Only show loading indicator for Ollama (which may need to load models)
227
+ # This prevents Ollama-specific UI elements from showing when using other providers
179
228
  if is_ollama and hasattr(app, 'query_one'):
180
229
  try:
181
230
  debug_log("Showing initial model loading indicator for Ollama")
@@ -190,6 +239,7 @@ async def generate_streaming_response(
190
239
  debug_log(f"Starting stream generation with messages length: {len(messages)}")
191
240
  logger.info(f"Starting stream generation for model: {model}")
192
241
 
242
+ # Initialize stream generator
193
243
  try:
194
244
  debug_log("Calling client.generate_stream()")
195
245
  stream_generator = client.generate_stream(messages, model, style)
@@ -199,10 +249,12 @@ async def generate_streaming_response(
199
249
  logger.error(f"Error initializing stream generator: {str(stream_init_error)}")
200
250
  raise
201
251
 
202
- if hasattr(client, 'is_loading_model') and not client.is_loading_model() and hasattr(app, 'query_one'):
252
+ # Update UI if model is ready (Ollama specific)
253
+ # Only check is_loading_model for Ollama clients to prevent errors with other providers
254
+ if is_ollama and hasattr(client, 'is_loading_model') and not client.is_loading_model() and hasattr(app, 'query_one'):
203
255
  try:
204
- debug_log("Model is ready for generation, updating UI")
205
- logger.info("Model is ready for generation, updating UI")
256
+ debug_log("Ollama model is ready for generation, updating UI")
257
+ logger.info("Ollama model is ready for generation, updating UI")
206
258
  loading = app.query_one("#loading-indicator")
207
259
  loading.remove_class("model-loading")
208
260
  loading.update("▪▪▪ Generating response...")
@@ -210,9 +262,11 @@ async def generate_streaming_response(
210
262
  debug_log(f"Error updating UI after stream init: {str(e)}")
211
263
  logger.error(f"Error updating UI after stream init: {str(e)}")
212
264
 
265
+ # Process stream chunks
213
266
  debug_log("Beginning to process stream chunks")
214
267
  try:
215
268
  async for chunk in stream_generator:
269
+ # Check for task cancellation
216
270
  if asyncio.current_task().cancelled():
217
271
  debug_log("Task cancellation detected during chunk processing")
218
272
  logger.info("Task cancellation detected during chunk processing")
@@ -221,30 +275,32 @@ async def generate_streaming_response(
221
275
  await client.cancel_stream()
222
276
  raise asyncio.CancelledError()
223
277
 
224
- if hasattr(client, 'is_loading_model'):
278
+ # Handle Ollama model loading state changes - only for Ollama clients
279
+ if is_ollama and hasattr(client, 'is_loading_model'):
225
280
  try:
226
281
  model_loading = client.is_loading_model()
227
- debug_log(f"Model loading state: {model_loading}")
282
+ debug_log(f"Ollama model loading state: {model_loading}")
228
283
  if hasattr(app, 'query_one'):
229
284
  try:
230
285
  loading = app.query_one("#loading-indicator")
231
286
  if model_loading and hasattr(loading, 'has_class') and not loading.has_class("model-loading"):
232
- debug_log("Model loading started during streaming")
233
- logger.info("Model loading started during streaming")
287
+ debug_log("Ollama model loading started during streaming")
288
+ logger.info("Ollama model loading started during streaming")
234
289
  loading.add_class("model-loading")
235
290
  loading.update("⚙️ Loading Ollama model...")
236
291
  elif not model_loading and hasattr(loading, 'has_class') and loading.has_class("model-loading"):
237
- debug_log("Model loading finished during streaming")
238
- logger.info("Model loading finished during streaming")
292
+ debug_log("Ollama model loading finished during streaming")
293
+ logger.info("Ollama model loading finished during streaming")
239
294
  loading.remove_class("model-loading")
240
295
  loading.update("▪▪▪ Generating response...")
241
296
  except Exception as ui_e:
242
297
  debug_log(f"Error updating UI elements: {str(ui_e)}")
243
298
  logger.error(f"Error updating UI elements: {str(ui_e)}")
244
299
  except Exception as e:
245
- debug_log(f"Error checking model loading state: {str(e)}")
246
- logger.error(f"Error checking model loading state: {str(e)}")
300
+ debug_log(f"Error checking Ollama model loading state: {str(e)}")
301
+ logger.error(f"Error checking Ollama model loading state: {str(e)}")
247
302
 
303
+ # Process chunk content
248
304
  if chunk:
249
305
  if not isinstance(chunk, str):
250
306
  debug_log(f"WARNING: Received non-string chunk of type: {type(chunk).__name__}")
@@ -259,7 +315,8 @@ async def generate_streaming_response(
259
315
  buffer.append(chunk)
260
316
  current_time = time.time()
261
317
 
262
- # Always update immediately for the first few chunks
318
+ # Update UI with new content
319
+ # Always update immediately for the first few chunks for better responsiveness
263
320
  if (current_time - last_update >= update_interval or
264
321
  len(''.join(buffer)) > 5 or # Reduced buffer size threshold
265
322
  len(full_response) < 50): # More aggressive updates for early content
@@ -268,8 +325,10 @@ async def generate_streaming_response(
268
325
  full_response += new_content
269
326
  debug_log(f"Updating UI with content length: {len(full_response)}")
270
327
 
271
- # Print to console for debugging
272
- print(f"Streaming update: +{len(new_content)} chars, total: {len(full_response)}")
328
+ # Only print to console for debugging if not OpenAI
329
+ # This prevents Ollama debug output from appearing in OpenAI responses
330
+ if not is_openai:
331
+ print(f"Streaming update: +{len(new_content)} chars, total: {len(full_response)}")
273
332
 
274
333
  try:
275
334
  # Call the UI callback with the full response so far
@@ -282,11 +341,9 @@ async def generate_streaming_response(
282
341
  except Exception as callback_err:
283
342
  debug_log(f"Error in UI callback: {str(callback_err)}")
284
343
  logger.error(f"Error in UI callback: {str(callback_err)}")
285
- print(f"Error updating UI: {str(callback_err)}")
286
- except Exception as callback_err:
287
- debug_log(f"Error in UI callback: {str(callback_err)}")
288
- logger.error(f"Error in UI callback: {str(callback_err)}")
289
- print(f"Error updating UI: {str(callback_err)}")
344
+ # Only print error to console if not OpenAI
345
+ if not is_openai:
346
+ print(f"Error updating UI: {str(callback_err)}")
290
347
 
291
348
  buffer = []
292
349
  last_update = current_time
@@ -442,8 +499,8 @@ def resolve_model_id(model_id_or_name: str) -> str:
442
499
  """
443
500
  Resolves a potentially short model ID or display name to the full model ID
444
501
  stored in the configuration. Tries multiple matching strategies.
445
-
446
- Fix: Only apply dot-to-colon conversion for Ollama models, not for OpenAI/Anthropic/custom.
502
+
503
+ This function is critical for ensuring models are correctly identified by provider.
447
504
  """
448
505
  if not model_id_or_name:
449
506
  logger.warning("resolve_model_id called with empty input, returning empty string.")
@@ -451,6 +508,16 @@ def resolve_model_id(model_id_or_name: str) -> str:
451
508
 
452
509
  input_lower = model_id_or_name.lower().strip()
453
510
  logger.info(f"Attempting to resolve model identifier: '{input_lower}'")
511
+
512
+ # First, check if this is an OpenAI model - if so, return as-is to ensure correct provider
513
+ if any(name in input_lower for name in ["gpt", "text-", "davinci"]):
514
+ logger.info(f"Input '{input_lower}' appears to be an OpenAI model, returning as-is")
515
+ return model_id_or_name
516
+
517
+ # Next, check if this is an Anthropic model - if so, return as-is to ensure correct provider
518
+ if any(name in input_lower for name in ["claude", "anthropic"]):
519
+ logger.info(f"Input '{input_lower}' appears to be an Anthropic model, returning as-is")
520
+ return model_id_or_name
454
521
 
455
522
  available_models = CONFIG.get("available_models", {})
456
523
  if not available_models:
@@ -461,20 +528,22 @@ def resolve_model_id(model_id_or_name: str) -> str:
461
528
  provider = None
462
529
  if input_lower in available_models:
463
530
  provider = available_models[input_lower].get("provider")
531
+ logger.info(f"Found model in available_models with provider: {provider}")
464
532
  else:
465
533
  # Try to find by display name
466
534
  for model_info in available_models.values():
467
535
  if model_info.get("display_name", "").lower() == input_lower:
468
536
  provider = model_info.get("provider")
537
+ logger.info(f"Found model by display name with provider: {provider}")
469
538
  break
470
539
 
471
540
  # Special case for Ollama models with version format (model:version)
472
- if provider == "ollama" and ":" in input_lower and not input_lower.startswith("claude-"):
541
+ if (provider == "ollama" or any(name in input_lower for name in ["llama", "mistral", "codellama", "gemma"])) and ":" in input_lower and not input_lower.startswith("claude-"):
473
542
  logger.info(f"Input '{input_lower}' appears to be an Ollama model with version, returning as-is")
474
543
  return model_id_or_name
475
544
 
476
545
  # Only apply dot-to-colon for Ollama models
477
- if provider == "ollama" and "." in input_lower and not input_lower.startswith("claude-"):
546
+ if (provider == "ollama" or any(name in input_lower for name in ["llama", "mistral", "codellama", "gemma"])) and "." in input_lower and not input_lower.startswith("claude-"):
478
547
  logger.info(f"Input '{input_lower}' appears to be an Ollama model with dot notation")
479
548
  if ":" not in input_lower:
480
549
  parts = input_lower.split(".")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chat-console
3
- Version: 0.3.6
3
+ Version: 0.3.7
4
4
  Summary: A command-line interface for chatting with LLMs, storing chats and (future) rag interactions
5
5
  Home-page: https://github.com/wazacraftrfid/chat-console
6
6
  Author: Johnathan Greenaway
@@ -1,12 +1,12 @@
1
- app/__init__.py,sha256=wUvsU30dqInIPNxEtkmKfV3elJ3g5-yEF367L06eu6E,130
1
+ app/__init__.py,sha256=ZSZR6xIuPhvv1zB4p63eSeGQX8bTkhxBWk2Gn0peFaw,130
2
2
  app/config.py,sha256=KawltE7cK2bR9wbe1NSlepwWIjkiFw2bg3vbLmUnP38,7626
3
3
  app/database.py,sha256=nt8CVuDpy6zw8mOYqDcfUmNw611t7Ln7pz22M0b6-MI,9967
4
- app/main.py,sha256=aGCaQYBTgV6PRgv6ZngC-bOYAtPl8O-9V_cMOionqbk,71245
4
+ app/main.py,sha256=clcRjXwySxVjrPtqvPOIfl7r8KbHVLZ1woxyEnvl3JI,72829
5
5
  app/models.py,sha256=4-y9Lytay2exWPFi0FDlVeRL3K2-I7E-jBqNzTfokqY,2644
6
- app/utils.py,sha256=1eiwjQwZRJIaZvUPQVUmTpyEvWUh3iiKeX-vRRgyUGs,28925
6
+ app/utils.py,sha256=htktBl1JucYEHo1WBrWkfdip4yzRtvyVl24Aaj445xA,32421
7
7
  app/api/__init__.py,sha256=A8UL84ldYlv8l7O-yKzraVFcfww86SgWfpl4p7R03-w,62
8
8
  app/api/anthropic.py,sha256=UpIP3CgAOUimdVyif41MhBOCAgOyFO8mX9SFQMKRAmc,12483
9
- app/api/base.py,sha256=bqBT4jne_W6Cvj_GoWWclV4Uk95fQvt-kkYqqZFJd8M,5769
9
+ app/api/base.py,sha256=eShCiZIcW3yeZLONt1xnkP0vU6v5MEaDj3YZ3xcPle8,7294
10
10
  app/api/ollama.py,sha256=EBEEKXbgAYWEg_zF5PO_UKO5l_aoU3J_7tfCj9e-fqs,61699
11
11
  app/api/openai.py,sha256=6ORruzuuZtIjME3WK-g7kXf7cBmM4td5Njv9JLaWh7E,9557
12
12
  app/ui/__init__.py,sha256=RndfbQ1Tv47qdSiuQzvWP96lPS547SDaGE-BgOtiP_w,55
@@ -16,9 +16,9 @@ app/ui/model_browser.py,sha256=pdblLVkdyVF0_Bo02bqbErGAtieyH-y6IfhMOPEqIso,71124
16
16
  app/ui/model_selector.py,sha256=ue3rbZfjVsjli-rJN5mfSqq23Ci7NshmTb4xWS-uG5k,18685
17
17
  app/ui/search.py,sha256=b-m14kG3ovqW1-i0qDQ8KnAqFJbi5b1FLM9dOnbTyIs,9763
18
18
  app/ui/styles.py,sha256=04AhPuLrOd2yenfRySFRestPeuTPeMLzhmMB67NdGvw,5615
19
- chat_console-0.3.6.dist-info/licenses/LICENSE,sha256=srHZ3fvcAuZY1LHxE7P6XWju2njRCHyK6h_ftEbzxSE,1057
20
- chat_console-0.3.6.dist-info/METADATA,sha256=WZawRM5bbluU90n7HsSWqcu4sJhCHZbZJg9eLp7BK_Y,2921
21
- chat_console-0.3.6.dist-info/WHEEL,sha256=pxyMxgL8-pra_rKaQ4drOZAegBVuX-G_4nRHjjgWbmo,91
22
- chat_console-0.3.6.dist-info/entry_points.txt,sha256=kkVdEc22U9PAi2AeruoKklfkng_a_aHAP6VRVwrAD7c,67
23
- chat_console-0.3.6.dist-info/top_level.txt,sha256=io9g7LCbfmTG1SFKgEOGXmCFB9uMP2H5lerm0HiHWQE,4
24
- chat_console-0.3.6.dist-info/RECORD,,
19
+ chat_console-0.3.7.dist-info/licenses/LICENSE,sha256=srHZ3fvcAuZY1LHxE7P6XWju2njRCHyK6h_ftEbzxSE,1057
20
+ chat_console-0.3.7.dist-info/METADATA,sha256=eDQRUghh8Ihp8z38oAlI0___RBBDJHpLmhBGF0VgZ1w,2921
21
+ chat_console-0.3.7.dist-info/WHEEL,sha256=SmOxYU7pzNKBqASvQJ7DjX3XGUF92lrGhMb3R6_iiqI,91
22
+ chat_console-0.3.7.dist-info/entry_points.txt,sha256=kkVdEc22U9PAi2AeruoKklfkng_a_aHAP6VRVwrAD7c,67
23
+ chat_console-0.3.7.dist-info/top_level.txt,sha256=io9g7LCbfmTG1SFKgEOGXmCFB9uMP2H5lerm0HiHWQE,4
24
+ chat_console-0.3.7.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (79.0.0)
2
+ Generator: setuptools (79.0.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5