janito 2.2.0__py3-none-any.whl → 2.3.0__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.
Files changed (130) hide show
  1. janito/__init__.py +6 -6
  2. janito/agent/setup_agent.py +14 -5
  3. janito/agent/templates/profiles/system_prompt_template_main.txt.j2 +3 -1
  4. janito/cli/chat_mode/bindings.py +6 -0
  5. janito/cli/chat_mode/session.py +16 -0
  6. janito/cli/chat_mode/shell/autocomplete.py +21 -21
  7. janito/cli/chat_mode/shell/commands/__init__.py +3 -0
  8. janito/cli/chat_mode/shell/commands/clear.py +12 -12
  9. janito/cli/chat_mode/shell/commands/exec.py +27 -0
  10. janito/cli/chat_mode/shell/commands/multi.py +51 -51
  11. janito/cli/chat_mode/shell/commands/tools.py +17 -6
  12. janito/cli/chat_mode/shell/input_history.py +62 -62
  13. janito/cli/chat_mode/shell/session/manager.py +1 -0
  14. janito/cli/chat_mode/toolbar.py +1 -0
  15. janito/cli/cli_commands/list_models.py +35 -35
  16. janito/cli/cli_commands/list_providers.py +9 -9
  17. janito/cli/cli_commands/list_tools.py +53 -53
  18. janito/cli/cli_commands/model_selection.py +50 -50
  19. janito/cli/cli_commands/model_utils.py +13 -2
  20. janito/cli/cli_commands/set_api_key.py +19 -19
  21. janito/cli/cli_commands/show_config.py +51 -51
  22. janito/cli/cli_commands/show_system_prompt.py +62 -62
  23. janito/cli/config.py +2 -1
  24. janito/cli/core/__init__.py +4 -4
  25. janito/cli/core/event_logger.py +59 -59
  26. janito/cli/core/getters.py +3 -1
  27. janito/cli/core/runner.py +165 -148
  28. janito/cli/core/setters.py +5 -1
  29. janito/cli/core/unsetters.py +54 -54
  30. janito/cli/main_cli.py +12 -1
  31. janito/cli/prompt_core.py +5 -2
  32. janito/cli/rich_terminal_reporter.py +22 -3
  33. janito/cli/single_shot_mode/__init__.py +6 -6
  34. janito/cli/single_shot_mode/handler.py +11 -1
  35. janito/cli/verbose_output.py +1 -1
  36. janito/config.py +5 -5
  37. janito/config_manager.py +2 -0
  38. janito/driver_events.py +14 -0
  39. janito/drivers/anthropic/driver.py +113 -113
  40. janito/drivers/azure_openai/driver.py +38 -3
  41. janito/drivers/driver_registry.py +0 -2
  42. janito/drivers/openai/driver.py +196 -36
  43. janito/formatting_token.py +54 -54
  44. janito/i18n/__init__.py +35 -35
  45. janito/i18n/messages.py +23 -23
  46. janito/i18n/pt.py +47 -47
  47. janito/llm/__init__.py +5 -5
  48. janito/llm/agent.py +443 -443
  49. janito/llm/auth.py +1 -0
  50. janito/llm/driver.py +7 -1
  51. janito/llm/driver_config.py +1 -0
  52. janito/llm/driver_config_builder.py +34 -34
  53. janito/llm/driver_input.py +12 -12
  54. janito/llm/message_parts.py +60 -60
  55. janito/llm/model.py +38 -38
  56. janito/llm/provider.py +196 -196
  57. janito/provider_config.py +7 -3
  58. janito/provider_registry.py +176 -158
  59. janito/providers/__init__.py +1 -0
  60. janito/providers/anthropic/model_info.py +22 -22
  61. janito/providers/anthropic/provider.py +2 -2
  62. janito/providers/azure_openai/model_info.py +7 -6
  63. janito/providers/azure_openai/provider.py +30 -2
  64. janito/providers/deepseek/__init__.py +1 -1
  65. janito/providers/deepseek/model_info.py +16 -16
  66. janito/providers/deepseek/provider.py +91 -91
  67. janito/providers/google/model_info.py +21 -29
  68. janito/providers/google/provider.py +49 -38
  69. janito/providers/mistralai/provider.py +2 -2
  70. janito/providers/provider_static_info.py +2 -3
  71. janito/tools/adapters/__init__.py +1 -1
  72. janito/tools/adapters/local/adapter.py +33 -11
  73. janito/tools/adapters/local/ask_user.py +102 -102
  74. janito/tools/adapters/local/copy_file.py +84 -84
  75. janito/tools/adapters/local/create_directory.py +69 -69
  76. janito/tools/adapters/local/create_file.py +82 -82
  77. janito/tools/adapters/local/delete_text_in_file.py +4 -7
  78. janito/tools/adapters/local/fetch_url.py +97 -97
  79. janito/tools/adapters/local/find_files.py +138 -138
  80. janito/tools/adapters/local/get_file_outline/__init__.py +1 -1
  81. janito/tools/adapters/local/get_file_outline/core.py +117 -117
  82. janito/tools/adapters/local/get_file_outline/java_outline.py +40 -40
  83. janito/tools/adapters/local/get_file_outline/markdown_outline.py +14 -14
  84. janito/tools/adapters/local/get_file_outline/python_outline.py +303 -303
  85. janito/tools/adapters/local/get_file_outline/python_outline_v2.py +156 -156
  86. janito/tools/adapters/local/get_file_outline/search_outline.py +33 -33
  87. janito/tools/adapters/local/move_file.py +3 -13
  88. janito/tools/adapters/local/python_code_run.py +166 -166
  89. janito/tools/adapters/local/python_command_run.py +164 -164
  90. janito/tools/adapters/local/python_file_run.py +163 -163
  91. janito/tools/adapters/local/remove_directory.py +6 -17
  92. janito/tools/adapters/local/remove_file.py +4 -10
  93. janito/tools/adapters/local/replace_text_in_file.py +6 -9
  94. janito/tools/adapters/local/run_bash_command.py +176 -176
  95. janito/tools/adapters/local/run_powershell_command.py +219 -219
  96. janito/tools/adapters/local/search_text/__init__.py +1 -1
  97. janito/tools/adapters/local/search_text/core.py +201 -201
  98. janito/tools/adapters/local/search_text/match_lines.py +1 -1
  99. janito/tools/adapters/local/search_text/pattern_utils.py +73 -73
  100. janito/tools/adapters/local/search_text/traverse_directory.py +145 -145
  101. janito/tools/adapters/local/validate_file_syntax/__init__.py +1 -1
  102. janito/tools/adapters/local/validate_file_syntax/core.py +106 -106
  103. janito/tools/adapters/local/validate_file_syntax/css_validator.py +35 -35
  104. janito/tools/adapters/local/validate_file_syntax/html_validator.py +93 -93
  105. janito/tools/adapters/local/validate_file_syntax/js_validator.py +27 -27
  106. janito/tools/adapters/local/validate_file_syntax/json_validator.py +6 -6
  107. janito/tools/adapters/local/validate_file_syntax/markdown_validator.py +109 -109
  108. janito/tools/adapters/local/validate_file_syntax/ps1_validator.py +32 -32
  109. janito/tools/adapters/local/validate_file_syntax/python_validator.py +5 -5
  110. janito/tools/adapters/local/validate_file_syntax/xml_validator.py +11 -11
  111. janito/tools/adapters/local/validate_file_syntax/yaml_validator.py +6 -6
  112. janito/tools/adapters/local/view_file.py +167 -167
  113. janito/tools/inspect_registry.py +17 -17
  114. janito/tools/tool_base.py +105 -105
  115. janito/tools/tool_events.py +58 -58
  116. janito/tools/tool_run_exception.py +12 -12
  117. janito/tools/tool_use_tracker.py +81 -81
  118. janito/tools/tool_utils.py +45 -45
  119. janito/tools/tools_adapter.py +78 -6
  120. janito/tools/tools_schema.py +104 -104
  121. janito/version.py +4 -4
  122. {janito-2.2.0.dist-info → janito-2.3.0.dist-info}/METADATA +388 -251
  123. janito-2.3.0.dist-info/RECORD +181 -0
  124. janito/drivers/google_genai/driver.py +0 -54
  125. janito/drivers/google_genai/schema_generator.py +0 -67
  126. janito-2.2.0.dist-info/RECORD +0 -182
  127. {janito-2.2.0.dist-info → janito-2.3.0.dist-info}/WHEEL +0 -0
  128. {janito-2.2.0.dist-info → janito-2.3.0.dist-info}/entry_points.txt +0 -0
  129. {janito-2.2.0.dist-info → janito-2.3.0.dist-info}/licenses/LICENSE +0 -0
  130. {janito-2.2.0.dist-info → janito-2.3.0.dist-info}/top_level.txt +0 -0
janito/llm/provider.py CHANGED
@@ -1,196 +1,196 @@
1
- from abc import ABC, abstractmethod
2
- import importlib
3
- from janito.llm.driver import LLMDriver
4
-
5
-
6
- class LLMProvider(ABC):
7
- def create_driver(self):
8
- """
9
- Returns a new instance of the configured driver for this provider.
10
- Subclasses must implement this method.
11
- """
12
- raise NotImplementedError(
13
- "LLMProvider subclasses must implement create_driver()."
14
- )
15
-
16
- """
17
- Abstract base class for Large Language Model (LLM) providers.
18
-
19
- Provider Usage and Driver Communication Flow:
20
- 1. Provider class is selected (e.g., OpenAIProvider, MistralProvider).
21
- 2. An instance of the provider is created. This instance is bound to a specific configuration (LLMDriverConfig) containing model, credentials, etc.
22
- 3. All drivers created by that provider instance are associated with the bound config.
23
- 4. To communicate with an LLM, call create_driver() on the provider instance, which yields a driver configured for the attached config. Every driver created via this method inherits the provider's configuration.
24
-
25
- Key: You do not create/configure a driver directly—always go through the provider to ensure correct configuration binding to the provider instance.
26
-
27
- Subclasses must implement the core interface for interacting with LLM APIs and define `provider_name` as a class attribute.
28
- """
29
-
30
- name: str = None # Must be set on subclasses
31
- DEFAULT_MODEL: str = None # Should be set by subclasses
32
-
33
- def __init_subclass__(cls, **kwargs):
34
- super().__init_subclass__(**kwargs)
35
- if (
36
- not hasattr(cls, "name")
37
- or not isinstance(getattr(cls, "name"), str)
38
- or not cls.name
39
- ):
40
- raise TypeError(
41
- f"Class {cls.__name__} must define a class attribute 'name' (non-empty str)"
42
- )
43
- if (
44
- not hasattr(cls, "DEFAULT_MODEL")
45
- or getattr(cls, "DEFAULT_MODEL", None) is None
46
- ):
47
- raise TypeError(
48
- f"Class {cls.__name__} must define a class attribute 'DEFAULT_MODEL' (non-empty str)"
49
- )
50
-
51
- def fill_missing_device_info(self, config):
52
- """
53
- Fill missing LLMDriverConfig fields (max_tokens, temperature, etc) from MODEL_SPECS for the chosen model.
54
- Mutates the config in place.
55
- """
56
- if not hasattr(self, "MODEL_SPECS"):
57
- return
58
- model_name = getattr(config, "model", None) or getattr(
59
- self, "DEFAULT_MODEL", None
60
- )
61
- model_info = self.MODEL_SPECS.get(model_name)
62
- if not model_info:
63
- return
64
- # Handle common fields from model_info
65
- spec_dict = (
66
- model_info.to_dict() if hasattr(model_info, "to_dict") else dict(model_info)
67
- )
68
- if (
69
- hasattr(config, "max_tokens")
70
- and getattr(config, "max_tokens", None) is None
71
- ):
72
- val = spec_dict.get("max_tokens") or spec_dict.get("max_response")
73
- if val is not None:
74
- try:
75
- config.max_tokens = int(val)
76
- except Exception:
77
- pass
78
- if (
79
- hasattr(config, "temperature")
80
- and getattr(config, "temperature", None) is None
81
- ):
82
- val = spec_dict.get("temperature")
83
- if val is None:
84
- val = spec_dict.get("default_temp")
85
- if val is not None:
86
- try:
87
- config.temperature = float(val)
88
- except Exception:
89
- pass
90
-
91
- @property
92
- @abstractmethod
93
- def driver(self) -> LLMDriver:
94
- pass
95
-
96
- def is_model_available(self, model_name):
97
- """
98
- Returns True if the given model is available for this provider.
99
- Default implementation checks MODEL_SPECS; override for dynamic providers.
100
- """
101
- if not hasattr(self, "MODEL_SPECS"):
102
- return False
103
- return model_name in self.MODEL_SPECS
104
-
105
- def get_model_info(self, model_name=None):
106
- """
107
- Return the info dict for a given model (driver, params, etc). If model_name is None, return all model info dicts.
108
- MODEL_SPECS must be dict[str, LLMModelInfo].
109
- """
110
- if not hasattr(self, "MODEL_SPECS"):
111
- raise NotImplementedError(
112
- "This provider does not have a MODEL_SPECS attribute."
113
- )
114
- if model_name is None:
115
- return {
116
- name: model_info.to_dict()
117
- for name, model_info in self.MODEL_SPECS.items()
118
- }
119
- if model_name in self.MODEL_SPECS:
120
- return self.MODEL_SPECS[model_name].to_dict()
121
- return None
122
-
123
- def _validate_model_specs(self):
124
- if not hasattr(self, "MODEL_SPECS"):
125
- raise NotImplementedError(
126
- "This provider does not have a MODEL_SPECS attribute."
127
- )
128
-
129
- def _get_model_name_from_config(self, config):
130
- return (config or {}).get("model_name", getattr(self, "DEFAULT_MODEL", None))
131
-
132
- def _get_model_spec_entry(self, model_name):
133
- spec = self.MODEL_SPECS.get(model_name, None)
134
- if spec is None:
135
- raise ValueError(f"Model '{model_name}' not found in MODEL_SPECS.")
136
- return spec
137
-
138
- def _get_driver_name_from_spec(self, spec):
139
- driver_name = None
140
- if hasattr(spec, "driver") and spec.driver:
141
- driver_name = spec.driver
142
- elif hasattr(spec, "other") and isinstance(spec.other, dict):
143
- driver_name = spec.other.get("driver", None)
144
- return driver_name
145
-
146
- def _resolve_driver_class(self, driver_name):
147
- if not driver_name:
148
- raise NotImplementedError(
149
- "No driver class found or specified for this MODEL_SPECS entry."
150
- )
151
- module_root = "janito.drivers"
152
- probable_path = None
153
- mapping = {
154
- "OpenAIResponsesModelDriver": "openai_responses.driver",
155
- "OpenAIModelDriver": "openai.driver",
156
- "AzureOpenAIModelDriver": "azure_openai.driver",
157
- "GoogleGenaiModelDriver": "google_genai.driver",
158
- }
159
- if driver_name in mapping:
160
- probable_path = mapping[driver_name]
161
- module_path = f"{module_root}.{probable_path}"
162
- mod = importlib.import_module(module_path)
163
- return getattr(mod, driver_name)
164
- # Attempt dynamic fallback based on convention
165
- if driver_name.endswith("ModelDriver"):
166
- base = driver_name[: -len("ModelDriver")]
167
- mod_name = base.replace("_", "").lower()
168
- module_path = f"{module_root}.{mod_name}.driver"
169
- try:
170
- mod = importlib.import_module(module_path)
171
- return getattr(mod, driver_name)
172
- except Exception:
173
- pass
174
- raise NotImplementedError(
175
- "No driver class found for driver_name: {}".format(driver_name)
176
- )
177
-
178
- def _validate_required_config(self, driver_class, config, driver_name):
179
- required = getattr(driver_class, "required_config", None)
180
- if required:
181
- missing = [
182
- k
183
- for k in required
184
- if not config or k not in config or config.get(k) in (None, "")
185
- ]
186
- if missing:
187
- raise ValueError(
188
- f"Missing required config for {driver_name}: {', '.join(missing)}"
189
- )
190
-
191
- def create_agent(self, tools_adapter=None, agent_name: str = None, **kwargs):
192
- from janito.llm.agent import LLMAgent
193
-
194
- # Dynamically create driver if supported, else fallback to existing.
195
- driver = self.driver
196
- return LLMAgent(self, tools_adapter, agent_name=agent_name, **kwargs)
1
+ from abc import ABC, abstractmethod
2
+ import importlib
3
+ from janito.llm.driver import LLMDriver
4
+
5
+
6
+ class LLMProvider(ABC):
7
+ def create_driver(self):
8
+ """
9
+ Returns a new instance of the configured driver for this provider.
10
+ Subclasses must implement this method.
11
+ """
12
+ raise NotImplementedError(
13
+ "LLMProvider subclasses must implement create_driver()."
14
+ )
15
+
16
+ """
17
+ Abstract base class for Large Language Model (LLM) providers.
18
+
19
+ Provider Usage and Driver Communication Flow:
20
+ 1. Provider class is selected (e.g., OpenAIProvider, MistralProvider).
21
+ 2. An instance of the provider is created. This instance is bound to a specific configuration (LLMDriverConfig) containing model, credentials, etc.
22
+ 3. All drivers created by that provider instance are associated with the bound config.
23
+ 4. To communicate with an LLM, call create_driver() on the provider instance, which yields a driver configured for the attached config. Every driver created via this method inherits the provider's configuration.
24
+
25
+ Key: You do not create/configure a driver directly—always go through the provider to ensure correct configuration binding to the provider instance.
26
+
27
+ Subclasses must implement the core interface for interacting with LLM APIs and define `provider_name` as a class attribute.
28
+ """
29
+
30
+ name: str = None # Must be set on subclasses
31
+ DEFAULT_MODEL: str = None # Should be set by subclasses
32
+
33
+ def __init_subclass__(cls, **kwargs):
34
+ super().__init_subclass__(**kwargs)
35
+ if (
36
+ not hasattr(cls, "name")
37
+ or not isinstance(getattr(cls, "name"), str)
38
+ or not cls.name
39
+ ):
40
+ raise TypeError(
41
+ f"Class {cls.__name__} must define a class attribute 'name' (non-empty str)"
42
+ )
43
+ if (
44
+ not hasattr(cls, "DEFAULT_MODEL")
45
+ or getattr(cls, "DEFAULT_MODEL", None) is None
46
+ ):
47
+ raise TypeError(
48
+ f"Class {cls.__name__} must define a class attribute 'DEFAULT_MODEL' (non-empty str)"
49
+ )
50
+
51
+ def fill_missing_device_info(self, config):
52
+ """
53
+ Fill missing LLMDriverConfig fields (max_tokens, temperature, etc) from MODEL_SPECS for the chosen model.
54
+ Mutates the config in place.
55
+ """
56
+ if not hasattr(self, "MODEL_SPECS"):
57
+ return
58
+ model_name = getattr(config, "model", None) or getattr(
59
+ self, "DEFAULT_MODEL", None
60
+ )
61
+ model_info = self.MODEL_SPECS.get(model_name)
62
+ if not model_info:
63
+ return
64
+ # Handle common fields from model_info
65
+ spec_dict = (
66
+ model_info.to_dict() if hasattr(model_info, "to_dict") else dict(model_info)
67
+ )
68
+ if (
69
+ hasattr(config, "max_tokens")
70
+ and getattr(config, "max_tokens", None) is None
71
+ ):
72
+ val = spec_dict.get("max_tokens") or spec_dict.get("max_response")
73
+ if val is not None:
74
+ try:
75
+ config.max_tokens = int(val)
76
+ except Exception:
77
+ pass
78
+ if (
79
+ hasattr(config, "temperature")
80
+ and getattr(config, "temperature", None) is None
81
+ ):
82
+ val = spec_dict.get("temperature")
83
+ if val is None:
84
+ val = spec_dict.get("default_temp")
85
+ if val is not None:
86
+ try:
87
+ config.temperature = float(val)
88
+ except Exception:
89
+ pass
90
+
91
+ @property
92
+ @abstractmethod
93
+ def driver(self) -> LLMDriver:
94
+ pass
95
+
96
+ def is_model_available(self, model_name):
97
+ """
98
+ Returns True if the given model is available for this provider.
99
+ Default implementation checks MODEL_SPECS; override for dynamic providers.
100
+ """
101
+ if not hasattr(self, "MODEL_SPECS"):
102
+ return False
103
+ return model_name in self.MODEL_SPECS
104
+
105
+ def get_model_info(self, model_name=None):
106
+ """
107
+ Return the info dict for a given model (driver, params, etc). If model_name is None, return all model info dicts.
108
+ MODEL_SPECS must be dict[str, LLMModelInfo].
109
+ """
110
+ if not hasattr(self, "MODEL_SPECS"):
111
+ raise NotImplementedError(
112
+ "This provider does not have a MODEL_SPECS attribute."
113
+ )
114
+ if model_name is None:
115
+ return {
116
+ name: model_info.to_dict()
117
+ for name, model_info in self.MODEL_SPECS.items()
118
+ }
119
+ if model_name in self.MODEL_SPECS:
120
+ return self.MODEL_SPECS[model_name].to_dict()
121
+ return None
122
+
123
+ def _validate_model_specs(self):
124
+ if not hasattr(self, "MODEL_SPECS"):
125
+ raise NotImplementedError(
126
+ "This provider does not have a MODEL_SPECS attribute."
127
+ )
128
+
129
+ def _get_model_name_from_config(self, config):
130
+ return (config or {}).get("model_name", getattr(self, "DEFAULT_MODEL", None))
131
+
132
+ def _get_model_spec_entry(self, model_name):
133
+ spec = self.MODEL_SPECS.get(model_name, None)
134
+ if spec is None:
135
+ raise ValueError(f"Model '{model_name}' not found in MODEL_SPECS.")
136
+ return spec
137
+
138
+ def _get_driver_name_from_spec(self, spec):
139
+ driver_name = None
140
+ if hasattr(spec, "driver") and spec.driver:
141
+ driver_name = spec.driver
142
+ elif hasattr(spec, "other") and isinstance(spec.other, dict):
143
+ driver_name = spec.other.get("driver", None)
144
+ return driver_name
145
+
146
+ def _resolve_driver_class(self, driver_name):
147
+ if not driver_name:
148
+ raise NotImplementedError(
149
+ "No driver class found or specified for this MODEL_SPECS entry."
150
+ )
151
+ module_root = "janito.drivers"
152
+ probable_path = None
153
+ mapping = {
154
+ "OpenAIResponsesModelDriver": "openai_responses.driver",
155
+ "OpenAIModelDriver": "openai.driver",
156
+ "AzureOpenAIModelDriver": "azure_openai.driver",
157
+ "GoogleGenaiModelDriver": "google_genai.driver",
158
+ }
159
+ if driver_name in mapping:
160
+ probable_path = mapping[driver_name]
161
+ module_path = f"{module_root}.{probable_path}"
162
+ mod = importlib.import_module(module_path)
163
+ return getattr(mod, driver_name)
164
+ # Attempt dynamic fallback based on convention
165
+ if driver_name.endswith("ModelDriver"):
166
+ base = driver_name[: -len("ModelDriver")]
167
+ mod_name = base.replace("_", "").lower()
168
+ module_path = f"{module_root}.{mod_name}.driver"
169
+ try:
170
+ mod = importlib.import_module(module_path)
171
+ return getattr(mod, driver_name)
172
+ except Exception:
173
+ pass
174
+ raise NotImplementedError(
175
+ "No driver class found for driver_name: {}".format(driver_name)
176
+ )
177
+
178
+ def _validate_required_config(self, driver_class, config, driver_name):
179
+ required = getattr(driver_class, "required_config", None)
180
+ if required:
181
+ missing = [
182
+ k
183
+ for k in required
184
+ if not config or k not in config or config.get(k) in (None, "")
185
+ ]
186
+ if missing:
187
+ raise ValueError(
188
+ f"Missing required config for {driver_name}: {', '.join(missing)}"
189
+ )
190
+
191
+ def create_agent(self, tools_adapter=None, agent_name: str = None, **kwargs):
192
+ from janito.llm.agent import LLMAgent
193
+
194
+ # Dynamically create driver if supported, else fallback to existing.
195
+ driver = self.driver
196
+ return LLMAgent(self, tools_adapter, agent_name=agent_name, **kwargs)
janito/provider_config.py CHANGED
@@ -36,6 +36,7 @@ def set_provider_config(provider, key, value):
36
36
  config.file_config["providers"] = cfg
37
37
  with open(config.config_path, "w", encoding="utf-8") as f:
38
38
  json.dump(config.file_config, f, indent=2)
39
+ f.write("\n")
39
40
 
40
41
 
41
42
  def set_provider_model_config(provider, model, key, value):
@@ -51,6 +52,7 @@ def set_provider_model_config(provider, model, key, value):
51
52
  config.file_config["providers"] = cfg
52
53
  with open(config.config_path, "w", encoding="utf-8") as f:
53
54
  json.dump(config.file_config, f, indent=2)
55
+ f.write("\n")
54
56
 
55
57
 
56
58
  def get_provider_model_config(provider, model):
@@ -71,9 +73,11 @@ def get_effective_model(provider=None, requested_model=None):
71
73
  provider_model = config.get_provider_config(provider).get("model")
72
74
  if provider_model:
73
75
  return provider_model
74
- global_model = config.get("model")
75
- if global_model:
76
- return global_model
76
+ # Only use global model if no provider is specified
77
+ if provider is None:
78
+ global_model = config.get("model")
79
+ if global_model:
80
+ return global_model
77
81
  return None
78
82
 
79
83