yaicli 0.6.1__py3-none-any.whl → 0.6.3__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.
pyproject.toml CHANGED
@@ -1,10 +1,10 @@
1
1
  [project]
2
2
  name = "yaicli"
3
- version = "0.6.1"
3
+ version = "0.6.3"
4
4
  description = "A simple CLI tool to interact with LLM"
5
5
  authors = [{ name = "belingud", email = "im.victor@qq.com" }]
6
6
  readme = "README.md"
7
- requires-python = ">=3.9"
7
+ requires-python = ">=3.10"
8
8
  license = { file = "LICENSE" }
9
9
  classifiers = [
10
10
  "Programming Language :: Python :: 3",
@@ -65,10 +65,16 @@ ai = "yaicli.entry:app"
65
65
  yaicli = "yaicli.entry:app"
66
66
 
67
67
  [project.optional-dependencies]
68
+ all = [
69
+ "volcengine-python-sdk>=3.0.15",
70
+ "ollama>=0.5.1",
71
+ "cohere>=5.15.0",
72
+ "google-genai>=1.20.0",
73
+ ]
68
74
  doubao = ["volcengine-python-sdk>=3.0.15"]
69
75
  ollama = ["ollama>=0.5.1"]
70
76
  cohere = ["cohere>=5.15.0"]
71
- all = ["volcengine-python-sdk>=3.0.15", "ollama>=0.5.1", "cohere>=5.15.0"]
77
+ gemini = ["google-genai>=1.20.0"]
72
78
 
73
79
  [tool.pytest.ini_options]
74
80
  testpaths = ["tests"]
@@ -91,6 +97,7 @@ dev = [
91
97
  "pytest>=8.3.5",
92
98
  "pytest-cov>=6.1.1",
93
99
  "ruff>=0.11.2",
100
+ "tox>=4.27.0",
94
101
  ]
95
102
 
96
103
  [tool.isort]
yaicli/cli.py CHANGED
@@ -384,14 +384,7 @@ class CLI:
384
384
  self._check_history_len()
385
385
 
386
386
  if self.current_mode == EXEC_MODE:
387
- # We need to extract the executable command from the last assistant message
388
- # in case of tool use.
389
- final_content = ""
390
- if self.chat.history:
391
- last_message = self.chat.history[-1]
392
- if last_message.role == "assistant":
393
- final_content = last_message.content or ""
394
- self._confirm_and_execute(final_content)
387
+ self._confirm_and_execute(content)
395
388
  return True
396
389
 
397
390
  def _confirm_and_execute(self, raw_content: str) -> None:
yaicli/const.py CHANGED
@@ -68,7 +68,7 @@ DEFAULT_JUSTIFY: JustifyMethod = "default"
68
68
  DEFAULT_ROLE_MODIFY_WARNING: BOOL_STR = "true"
69
69
  DEFAULT_ENABLE_FUNCTIONS: BOOL_STR = "true"
70
70
  DEFAULT_SHOW_FUNCTION_OUTPUT: BOOL_STR = "true"
71
- DEFAULT_REASONING_EFFORT: Optional[Literal["low", "high", "medium"]] = None
71
+ DEFAULT_REASONING_EFFORT: Optional[Literal["low", "high", "medium"]] = ""
72
72
 
73
73
 
74
74
  SHELL_PROMPT = """You are YAICLI, a shell command generator.
yaicli/llms/__init__.py CHANGED
@@ -1,13 +1,4 @@
1
- from ..config import cfg
2
1
  from .client import LLMClient
3
2
  from .provider import Provider, ProviderFactory
4
3
 
5
4
  __all__ = ["LLMClient", "Provider", "ProviderFactory"]
6
-
7
-
8
- class BaseProvider:
9
- def __init__(self) -> None:
10
- self.api_key = cfg["API_KEY"]
11
- self.model = cfg["MODEL"]
12
- self.base_url = cfg["BASE_URL"]
13
- self.timeout = cfg["TIMEOUT"]
yaicli/llms/provider.py CHANGED
@@ -9,7 +9,7 @@ class Provider(ABC):
9
9
  """Base abstract class for LLM providers"""
10
10
 
11
11
  APP_NAME = "yaicli"
12
- APPA_REFERER = "https://github.com/halfrost/yaicli"
12
+ APP_REFERER = "https://github.com/halfrost/yaicli"
13
13
 
14
14
  @abstractmethod
15
15
  def completion(
@@ -39,23 +39,26 @@ class ProviderFactory:
39
39
  """Factory to create LLM provider instances"""
40
40
 
41
41
  providers_map = {
42
- "openai": (".providers.openai_provider", "OpenAIProvider"),
43
- "modelscope": (".providers.modelscope_provider", "ModelScopeProvider"),
42
+ "ai21": (".providers.ai21_provider", "AI21Provider"),
44
43
  "chatglm": (".providers.chatglm_provider", "ChatglmProvider"),
45
- "openrouter": (".providers.openrouter_provider", "OpenRouterProvider"),
46
- "siliconflow": (".providers.siliconflow_provider", "SiliconFlowProvider"),
47
44
  "chutes": (".providers.chutes_provider", "ChutesProvider"),
48
- "infini-ai": (".providers.infiniai_provider", "InfiniAIProvider"),
49
- "yi": (".providers.yi_provider", "YiProvider"),
45
+ "cohere": (".providers.cohere_provider", "CohereProvider"),
50
46
  "deepseek": (".providers.deepseek_provider", "DeepSeekProvider"),
51
47
  "doubao": (".providers.doubao_provider", "DoubaoProvider"),
48
+ "gemini": (".providers.gemini_provider", "GeminiProvider"),
52
49
  "groq": (".providers.groq_provider", "GroqProvider"),
53
- "ai21": (".providers.ai21_provider", "AI21Provider"),
50
+ "infini-ai": (".providers.infiniai_provider", "InfiniAIProvider"),
51
+ "minimax": (".providers.minimax_provider", "MinimaxProvider"),
52
+ "modelscope": (".providers.modelscope_provider", "ModelScopeProvider"),
54
53
  "ollama": (".providers.ollama_provider", "OllamaProvider"),
55
- "cohere": (".providers.cohere_provider", "CohereProvider"),
54
+ "openai": (".providers.openai_provider", "OpenAIProvider"),
55
+ "openrouter": (".providers.openrouter_provider", "OpenRouterProvider"),
56
56
  "sambanova": (".providers.sambanova_provider", "SambanovaProvider"),
57
- "minimax": (".providers.minimax_provider", "MinimaxProvider"),
57
+ "siliconflow": (".providers.siliconflow_provider", "SiliconFlowProvider"),
58
58
  "targon": (".providers.targon_provider", "TargonProvider"),
59
+ "vertexai": (".providers.vertexai_provider", "VertexAIProvider"),
60
+ "xai": (".providers.xai_provider", "XaiProvider"),
61
+ "yi": (".providers.yi_provider", "YiProvider"),
59
62
  }
60
63
 
61
64
  @classmethod
@@ -1,4 +1,4 @@
1
- from typing import Any, Dict, Generator, Optional
1
+ from typing import Dict, Generator, Optional
2
2
 
3
3
  from openai._streaming import Stream
4
4
  from openai.types.chat.chat_completion_chunk import ChatCompletionChunk
@@ -12,10 +12,18 @@ class AI21Provider(OpenAIProvider):
12
12
 
13
13
  DEFAULT_BASE_URL = "https://api.ai21.com/studio/v1"
14
14
 
15
- def get_completion_params(self) -> Dict[str, Any]:
16
- params = super().get_completion_params()
17
- params["max_tokens"] = params.pop("max_completion_tokens")
18
- return params
15
+ def get_completion_params_keys(self) -> Dict[str, str]:
16
+ """
17
+ Customize completion parameter keys for AI21 API.
18
+ Maps 'max_completion_tokens' to 'max_tokens' for compatibility.
19
+
20
+ Returns:
21
+ Dict[str, str]: Modified parameter mapping dictionary
22
+ """
23
+ keys = super().get_completion_params_keys()
24
+ if "max_completion_tokens" in keys:
25
+ keys["max_tokens"] = keys.pop("max_completion_tokens")
26
+ return keys
19
27
 
20
28
  def _handle_stream_response(self, response: Stream[ChatCompletionChunk]) -> Generator[LLMResponse, None, None]:
21
29
  """Handle streaming response from AI21 models
@@ -1,5 +1,3 @@
1
- from typing import Any, Dict
2
-
3
1
  from .openai_provider import OpenAIProvider
4
2
 
5
3
 
@@ -8,7 +6,19 @@ class ChutesProvider(OpenAIProvider):
8
6
 
9
7
  DEFAULT_BASE_URL = "https://llm.chutes.ai/v1"
10
8
 
11
- def get_completion_params(self) -> Dict[str, Any]:
12
- params = super().get_completion_params()
13
- params["max_tokens"] = params.pop("max_completion_tokens")
14
- return params
9
+ def get_completion_params_keys(self) -> dict:
10
+ """
11
+ Customize completion parameter keys for Chutes API.
12
+ Maps 'max_completion_tokens' to 'max_tokens' and removes 'reasoning_effort'
13
+ which is not supported by this provider.
14
+
15
+ Returns:
16
+ dict: Modified parameter mapping dictionary
17
+ """
18
+ keys = super().get_completion_params_keys()
19
+ # Replace max_completion_tokens with max_tokens in the API
20
+ if "max_completion_tokens" in keys:
21
+ keys["max_tokens"] = keys.pop("max_completion_tokens")
22
+ # Remove unsupported parameters
23
+ keys.pop("reasoning_effort", None)
24
+ return keys
@@ -0,0 +1,191 @@
1
+ import json
2
+ from functools import wraps
3
+ from typing import Any, Callable, Dict, Generator, List
4
+
5
+ import google.genai as genai
6
+ from google.genai import types
7
+
8
+ from ...config import cfg
9
+ from ...console import get_console
10
+ from ...schemas import ChatMessage, LLMResponse
11
+ from ...tools import get_func_name_map
12
+ from ..provider import Provider
13
+
14
+
15
+ def wrap_function(func):
16
+ @wraps(func)
17
+ def wrapper(*args, **kwargs):
18
+ return func(*args, **kwargs)
19
+
20
+ return wrapper
21
+
22
+
23
+ class GeminiProvider(Provider):
24
+ """Gemini provider implementation based on google-genai library"""
25
+
26
+ DEFAULT_BASE_URL = "https://generativelanguage.googleapis.com/v1beta"
27
+
28
+ def __init__(self, config: dict = cfg, verbose: bool = False, **kwargs):
29
+ self.config = config
30
+ self.enable_function = self.config["ENABLE_FUNCTIONS"]
31
+ self.verbose = verbose
32
+
33
+ # Initialize client
34
+ self.client_params = self.get_client_params()
35
+ self.client = genai.Client(**self.client_params)
36
+ self.console = get_console()
37
+
38
+ def get_client_params(self) -> Dict[str, Any]:
39
+ """Get the client parameters"""
40
+ # Initialize client params
41
+ return {
42
+ "api_key": self.config["API_KEY"],
43
+ }
44
+
45
+ def get_chat_config(self):
46
+ http_options_map = {
47
+ "timeout": self.config["TIMEOUT"] * 1000, # Timeout for the request in milliseconds.
48
+ "headers": {**self.config["EXTRA_HEADERS"], "X-Client": self.APP_NAME, "Referer": self.APP_REFERER},
49
+ }
50
+ if self.config.get("BASE_URL"):
51
+ http_options_map["base_url"] = self.config["BASE_URL"]
52
+ if self.config.get("API_VERSION"):
53
+ # Specifies the version of the API to use.
54
+ http_options_map["api_version"] = self.config["API_VERSION"]
55
+ http_options = types.HttpOptions(**http_options_map)
56
+ config_map = {
57
+ "max_output_tokens": self.config["MAX_TOKENS"],
58
+ "temperature": self.config["TEMPERATURE"],
59
+ "top_p": self.config["TOP_P"],
60
+ "http_options": http_options,
61
+ }
62
+ if self.config.get("TOP_K"):
63
+ config_map["top_k"] = self.config["TOP_K"]
64
+ if self.config.get("PRESENCE_PENALTY"):
65
+ config_map["presence_penalty"] = self.config["PRESENCE_PENALTY"]
66
+ if self.config.get("FREQUENCY_PENALTY"):
67
+ config_map["frequency_penalty"] = self.config["FREQUENCY_PENALTY"]
68
+ if self.config.get("SEED"):
69
+ config_map["seed"] = self.config["SEED"]
70
+ # Indicates whether to include thoughts in the response. If true, thoughts are returned only if the model supports thought and thoughts are available.
71
+ thinking_config_map = {"include_thoughts": self.config.get("INCLUDE_THOUGHTS", True)}
72
+ if self.config.get("THINKING_BUDGET"):
73
+ thinking_config_map["thinking_budget"] = int(self.config["THINKING_BUDGET"])
74
+ config_map["thinking_config"] = types.ThinkingConfig(**thinking_config_map)
75
+ config = types.GenerateContentConfig(**config_map)
76
+ if self.enable_function:
77
+ # TODO: support disable automatic function calling
78
+ # config.automatic_function_calling = types.AutomaticFunctionCallingConfig(disable=False)
79
+ config.tools = self.gen_gemini_functions()
80
+ return config
81
+
82
+ def _convert_messages(self, messages: List[ChatMessage]) -> List[types.Content]:
83
+ """Convert a list of ChatMessage objects to a list of Gemini Content objects."""
84
+ converted_messages = []
85
+ for msg in messages:
86
+ if msg.role == "system":
87
+ continue
88
+ content = types.Content(role=self._map_role(msg.role), parts=[types.Part(text=msg.content)])
89
+ if msg.role == "tool":
90
+ content.role = "user"
91
+ content.parts = [types.Part.from_function_response(name=msg.name, response={"result": msg.content})]
92
+ converted_messages.append(content)
93
+ return converted_messages
94
+
95
+ def _map_role(self, role: str) -> str:
96
+ """Map OpenAI roles to Gemini roles"""
97
+ # Gemini uses "user", "model" instead of "user", "assistant"
98
+ if role == "assistant":
99
+ return "model"
100
+ return role
101
+
102
+ def gen_gemini_functions(self) -> List[Callable[..., Any]]:
103
+ """Wrap Gemini functions from OpenAI functions for automatic function calling"""
104
+ func_name_map = get_func_name_map()
105
+ if not func_name_map:
106
+ return []
107
+ funcs = []
108
+ for func_name, func in func_name_map.items():
109
+ wrapped_func = wrap_function(func.execute)
110
+ wrapped_func.__name__ = func_name
111
+ wrapped_func.__doc__ = func.__doc__
112
+ funcs.append(wrapped_func)
113
+ return funcs
114
+
115
+ def completion(
116
+ self,
117
+ messages: List[ChatMessage],
118
+ stream: bool = False,
119
+ ) -> Generator[LLMResponse, None, None]:
120
+ """
121
+ Send completion request to Gemini and return responses.
122
+
123
+ Args:
124
+ messages: List of chat messages to send
125
+ stream: Whether to stream the response
126
+
127
+ Yields:
128
+ LLMResponse: Response objects containing content, tool calls, etc.
129
+
130
+ Raises:
131
+ ValueError: If messages is empty or invalid
132
+ APIError: If API request fails
133
+ """
134
+ gemini_messages = self._convert_messages(messages)
135
+ if self.verbose:
136
+ self.console.print("Messages:")
137
+ self.console.print(gemini_messages)
138
+ chat_config = self.get_chat_config()
139
+ chat_config.system_instruction = messages[0].content
140
+ chat = self.client.chats.create(model=self.config["MODEL"], history=gemini_messages, config=chat_config)
141
+ message = messages[-1].content
142
+
143
+ if stream:
144
+ response = chat.send_message_stream(message=message)
145
+ yield from self._handle_stream_response(response)
146
+ else:
147
+ response = chat.send_message(message=message)
148
+ yield from self._handle_normal_response(response)
149
+
150
+ def _handle_normal_response(self, response) -> Generator[LLMResponse, None, None]:
151
+ """Handle normal (non-streaming) response"""
152
+ # TODO: support disable automatic function calling
153
+ if not response or not response.candidates:
154
+ yield LLMResponse(
155
+ content=json.dumps(response.to_json_dict()),
156
+ finish_reason="stop",
157
+ )
158
+ return
159
+ for part in response.candidates[0].content.parts:
160
+ if part.thought:
161
+ yield LLMResponse(reasoning=part.text, content=None, finish_reason="stop")
162
+ else:
163
+ yield LLMResponse(reasoning=None, content=part.text, finish_reason="stop")
164
+
165
+ def _handle_stream_response(self, response) -> Generator[LLMResponse, None, None]:
166
+ """Handle streaming response from Gemini API"""
167
+ # Initialize tool call object to accumulate tool call data across chunks
168
+ # TODO: support disable automatic function calling
169
+ tool_call = None
170
+ for chunk in response:
171
+ if not chunk.candidates:
172
+ continue
173
+ candidate = chunk.candidates[0]
174
+ finish_reason = candidate.finish_reason
175
+ for part in chunk.candidates[0].content.parts:
176
+ if part.thought:
177
+ reasoning = part.text
178
+ content = None
179
+ else:
180
+ content = part.text
181
+ reasoning = None
182
+ yield LLMResponse(
183
+ reasoning=reasoning,
184
+ content=content,
185
+ tool_call=tool_call if finish_reason == "tool_calls" else None,
186
+ finish_reason=finish_reason or None,
187
+ )
188
+
189
+ def detect_tool_role(self) -> str:
190
+ """Return the role that should be used for tool responses"""
191
+ return "user"
@@ -8,7 +8,27 @@ class GroqProvider(OpenAIProvider):
8
8
 
9
9
  DEFAULT_BASE_URL = "https://api.groq.com/openai/v1"
10
10
 
11
+ def get_completion_params_keys(self) -> Dict[str, str]:
12
+ """
13
+ Customize completion parameter keys for Groq API.
14
+ Maps 'max_completion_tokens' to 'max_tokens' for compatibility.
15
+
16
+ Returns:
17
+ Dict[str, str]: Modified parameter mapping dictionary
18
+ """
19
+ keys = super().get_completion_params_keys()
20
+ if "max_completion_tokens" in keys:
21
+ keys["max_tokens"] = keys.pop("max_completion_tokens")
22
+ return keys
23
+
11
24
  def get_completion_params(self) -> Dict[str, Any]:
25
+ """
26
+ Get completion parameters with Groq-specific adjustments.
27
+ Enforce N=1 as Groq doesn't support multiple completions.
28
+
29
+ Returns:
30
+ Dict[str, Any]: Parameters for completion API call
31
+ """
12
32
  params = super().get_completion_params()
13
33
  if self.config["EXTRA_BODY"] and "N" in self.config["EXTRA_BODY"] and self.config["EXTRA_BODY"]["N"] != 1:
14
34
  self.console.print("Groq does not support N parameter, setting N to 1 as Groq default", style="yellow")
@@ -1,4 +1,3 @@
1
- from typing import Any, Dict
2
1
  from .openai_provider import OpenAIProvider
3
2
 
4
3
 
@@ -7,7 +6,16 @@ class MinimaxProvider(OpenAIProvider):
7
6
 
8
7
  DEFAULT_BASE_URL = "https://api.minimaxi.com/v1"
9
8
 
10
- def get_completion_params(self) -> Dict[str, Any]:
11
- params = super().get_completion_params()
12
- params["max_tokens"] = params.pop("max_completion_tokens")
13
- return params
9
+ def get_completion_params_keys(self) -> dict:
10
+ """
11
+ Customize completion parameter keys for Minimax API.
12
+ Maps 'max_completion_tokens' to 'max_tokens' for compatibility.
13
+
14
+ Returns:
15
+ dict: Modified parameter mapping dictionary
16
+ """
17
+ keys = super().get_completion_params_keys()
18
+ # Replace max_completion_tokens with max_tokens in the API
19
+ if "max_completion_tokens" in keys:
20
+ keys["max_tokens"] = keys.pop("max_completion_tokens")
21
+ return keys
@@ -18,9 +18,21 @@ class OpenAIProvider(Provider):
18
18
 
19
19
  DEFAULT_BASE_URL = "https://api.openai.com/v1"
20
20
  CLIENT_CLS = openai.OpenAI
21
+ # Base mapping between config keys and API parameter names
22
+ _BASE_COMPLETION_PARAMS_KEYS = {
23
+ "model": "MODEL",
24
+ "temperature": "TEMPERATURE",
25
+ "top_p": "TOP_P",
26
+ "max_completion_tokens": "MAX_TOKENS",
27
+ "timeout": "TIMEOUT",
28
+ "extra_body": "EXTRA_BODY",
29
+ "reasoning_effort": "REASONING_EFFORT",
30
+ }
21
31
 
22
32
  def __init__(self, config: dict = cfg, verbose: bool = False, **kwargs):
23
33
  self.config = config
34
+ if not self.config.get("API_KEY"):
35
+ raise ValueError("API_KEY is required")
24
36
  self.enable_function = self.config["ENABLE_FUNCTIONS"]
25
37
  self.verbose = verbose
26
38
 
@@ -45,22 +57,32 @@ class OpenAIProvider(Provider):
45
57
  client_params["default_headers"] = {
46
58
  **self.config["EXTRA_HEADERS"],
47
59
  "X-Title": self.APP_NAME,
48
- "HTTP-Referer": self.APPA_REFERER,
60
+ "HTTP-Referer": self.APP_REFERER,
49
61
  }
50
62
  return client_params
51
63
 
64
+ def get_completion_params_keys(self) -> Dict[str, str]:
65
+ """
66
+ Get the mapping between completion parameter keys and config keys.
67
+ Subclasses can override this method to customize parameter mapping.
68
+
69
+ Returns:
70
+ Dict[str, str]: Mapping from API parameter names to config keys
71
+ """
72
+ return self._BASE_COMPLETION_PARAMS_KEYS.copy()
73
+
52
74
  def get_completion_params(self) -> Dict[str, Any]:
53
- """Get the completion parameters"""
54
- completion_params = {
55
- "model": self.config["MODEL"],
56
- "temperature": self.config["TEMPERATURE"],
57
- "top_p": self.config["TOP_P"],
58
- "max_completion_tokens": self.config["MAX_TOKENS"],
59
- "timeout": self.config["TIMEOUT"],
60
- }
61
- # Add extra body params if set
62
- if self.config["EXTRA_BODY"]:
63
- completion_params["extra_body"] = self.config["EXTRA_BODY"]
75
+ """
76
+ Get the completion parameters based on config and parameter mapping.
77
+
78
+ Returns:
79
+ Dict[str, Any]: Parameters for completion API call
80
+ """
81
+ completion_params = {}
82
+ params_keys = self.get_completion_params_keys()
83
+ for api_key, config_key in params_keys.items():
84
+ if self.config.get(config_key, None) is not None and self.config[config_key] != "":
85
+ completion_params[api_key] = self.config[config_key]
64
86
  return completion_params
65
87
 
66
88
  def _convert_messages(self, messages: List[ChatMessage]) -> List[Dict[str, Any]]:
@@ -90,7 +112,20 @@ class OpenAIProvider(Provider):
90
112
  messages: List[ChatMessage],
91
113
  stream: bool = False,
92
114
  ) -> Generator[LLMResponse, None, None]:
93
- """Send completion request to OpenAI and return responses"""
115
+ """
116
+ Send completion request to OpenAI and return responses.
117
+
118
+ Args:
119
+ messages: List of chat messages to send
120
+ stream: Whether to stream the response
121
+
122
+ Yields:
123
+ LLMResponse: Response objects containing content, tool calls, etc.
124
+
125
+ Raises:
126
+ ValueError: If messages is empty or invalid
127
+ openai.APIError: If API request fails
128
+ """
94
129
  openai_messages = self._convert_messages(messages)
95
130
  if self.verbose:
96
131
  self.console.print("Messages:")
@@ -105,12 +140,19 @@ class OpenAIProvider(Provider):
105
140
  if tools:
106
141
  params["tools"] = tools
107
142
 
108
- if stream:
109
- response = self.client.chat.completions.create(**params)
110
- yield from self._handle_stream_response(response)
111
- else:
112
- response = self.client.chat.completions.create(**params)
113
- yield from self._handle_normal_response(response)
143
+ try:
144
+ if stream:
145
+ response = self.client.chat.completions.create(**params)
146
+ yield from self._handle_stream_response(response)
147
+ else:
148
+ response = self.client.chat.completions.create(**params)
149
+ yield from self._handle_normal_response(response)
150
+ except (openai.APIStatusError, openai.APIResponseValidationError) as e:
151
+ try:
152
+ body = e.response.json()
153
+ except Exception:
154
+ body = e.response.text
155
+ self.console.print(f"Error Response: {body}")
114
156
 
115
157
  def _handle_normal_response(self, response: ChatCompletion) -> Generator[LLMResponse, None, None]:
116
158
  """Handle normal (non-streaming) response"""
@@ -154,9 +196,8 @@ class OpenAIProvider(Provider):
154
196
  if not chunk.choices:
155
197
  continue
156
198
  started = True
157
- choice = chunk.choices[0]
158
- delta = choice.delta
159
- finish_reason = choice.finish_reason
199
+ delta = chunk.choices[0].delta
200
+ finish_reason = chunk.choices[0].finish_reason
160
201
 
161
202
  # Extract content from current chunk
162
203
  content = delta.content or ""
@@ -1,4 +1,4 @@
1
- from typing import Any, Dict
1
+ from typing import Dict
2
2
 
3
3
  from .openai_provider import OpenAIProvider
4
4
 
@@ -8,7 +8,15 @@ class OpenRouterProvider(OpenAIProvider):
8
8
 
9
9
  DEFAULT_BASE_URL = "https://openrouter.ai/api/v1"
10
10
 
11
- def get_completion_params(self) -> Dict[str, Any]:
12
- params = super().get_completion_params()
13
- params["max_tokens"] = params.pop("max_completion_tokens")
14
- return params
11
+ def get_completion_params_keys(self) -> Dict[str, str]:
12
+ """
13
+ Customize completion parameter keys for OpenRouter API.
14
+ Maps 'max_completion_tokens' to 'max_tokens' for compatibility.
15
+
16
+ Returns:
17
+ Dict[str, str]: Modified parameter mapping dictionary
18
+ """
19
+ keys = super().get_completion_params_keys()
20
+ if "max_completion_tokens" in keys:
21
+ keys["max_tokens"] = keys.pop("max_completion_tokens")
22
+ return keys
@@ -16,15 +16,44 @@ class SambanovaProvider(OpenAIProvider):
16
16
  "DeepSeek-V3-0324",
17
17
  )
18
18
 
19
+ def get_completion_params_keys(self) -> Dict[str, str]:
20
+ """
21
+ Customize completion parameter keys for Sambanova API.
22
+ Maps 'max_completion_tokens' to 'max_tokens' for compatibility
23
+ and removes parameters not supported by Sambanova.
24
+
25
+ Returns:
26
+ Dict[str, str]: Modified parameter mapping dictionary
27
+ """
28
+ keys = super().get_completion_params_keys()
29
+ # Replace max_completion_tokens with max_tokens
30
+ if "max_completion_tokens" in keys:
31
+ keys["max_tokens"] = keys.pop("max_completion_tokens")
32
+ # Remove unsupported parameters
33
+ keys.pop("presence_penalty", None)
34
+ keys.pop("frequency_penalty", None)
35
+ return keys
36
+
19
37
  def get_completion_params(self) -> Dict[str, Any]:
38
+ """
39
+ Get completion parameters with Sambanova-specific adjustments.
40
+ Validate temperature range and check for function call compatibility.
41
+
42
+ Returns:
43
+ Dict[str, Any]: Parameters for completion API call
44
+ """
20
45
  params = super().get_completion_params()
21
- params.pop("presence_penalty", None)
22
- params.pop("frequency_penalty", None)
23
- if params.get("temperature") < 0 or params.get("temperature") > 1:
46
+
47
+ # Validate temperature
48
+ if params.get("temperature") is not None and (params["temperature"] < 0 or params["temperature"] > 1):
24
49
  self.console.print("Sambanova temperature must be between 0 and 1, setting to 0.4", style="yellow")
25
50
  params["temperature"] = DEFAULT_TEMPERATURE
51
+
52
+ # Check function call compatibility
26
53
  if self.enable_function and self.config["MODEL"] not in self.SUPPORT_FUNCTION_CALL_MOELS:
27
54
  self.console.print(
28
55
  f"Sambanova supports function call models: {', '.join(self.SUPPORT_FUNCTION_CALL_MOELS)}",
29
56
  style="yellow",
30
57
  )
58
+
59
+ return params
@@ -1,4 +1,4 @@
1
- from typing import Any, Dict
1
+ from typing import Dict
2
2
 
3
3
  from .openai_provider import OpenAIProvider
4
4
 
@@ -8,7 +8,15 @@ class TargonProvider(OpenAIProvider):
8
8
 
9
9
  DEFAULT_BASE_URL = "https://api.targon.com/v1"
10
10
 
11
- def get_completion_params(self) -> Dict[str, Any]:
12
- params = super().get_completion_params()
13
- params["max_tokens"] = params.pop("max_completion_tokens")
14
- return params
11
+ def get_completion_params_keys(self) -> Dict[str, str]:
12
+ """
13
+ Customize completion parameter keys for Targon API.
14
+ Maps 'max_completion_tokens' to 'max_tokens' for compatibility.
15
+
16
+ Returns:
17
+ Dict[str, str]: Modified parameter mapping dictionary
18
+ """
19
+ keys = super().get_completion_params_keys()
20
+ if "max_completion_tokens" in keys:
21
+ keys["max_tokens"] = keys.pop("max_completion_tokens")
22
+ return keys
@@ -0,0 +1,18 @@
1
+ from typing import Any, Dict
2
+
3
+ from .gemini_provider import GeminiProvider
4
+
5
+
6
+ class VertexAIProvider(GeminiProvider):
7
+ """Vertex AI provider implementation based on google-genai library"""
8
+
9
+ def get_client_params(self) -> Dict[str, Any]:
10
+ """Get the client parameters"""
11
+ # Initialize client params
12
+ if not self.config.get("PROJECT") or not self.config.get("LOCATION"):
13
+ raise ValueError("PROJECT and LOCATION are required for Vertex AI")
14
+ return {
15
+ "vertexai": True,
16
+ "project": self.config.get("PROJECT"),
17
+ "location": self.config.get("LOCATION"),
18
+ }
@@ -0,0 +1,7 @@
1
+ from .openai_provider import OpenAIProvider
2
+
3
+
4
+ class XaiProvider(OpenAIProvider):
5
+ """Xai provider implementation based on openai-compatible API"""
6
+
7
+ DEFAULT_BASE_URL = "https://api.xai.com/v1"
@@ -1,4 +1,4 @@
1
- from typing import Any, Dict
1
+ from typing import Dict
2
2
 
3
3
  from .openai_provider import OpenAIProvider
4
4
 
@@ -8,7 +8,15 @@ class YiProvider(OpenAIProvider):
8
8
 
9
9
  DEFAULT_BASE_URL = "https://api.lingyiwanwu.com/v1"
10
10
 
11
- def get_completion_params(self) -> Dict[str, Any]:
12
- params = super().get_completion_params()
13
- params["max_tokens"] = params.pop("max_completion_tokens")
14
- return params
11
+ def get_completion_params_keys(self) -> Dict[str, str]:
12
+ """
13
+ Customize completion parameter keys for Yi API.
14
+ Maps 'max_completion_tokens' to 'max_tokens' for compatibility.
15
+
16
+ Returns:
17
+ Dict[str, str]: Modified parameter mapping dictionary
18
+ """
19
+ keys = super().get_completion_params_keys()
20
+ if "max_completion_tokens" in keys:
21
+ keys["max_tokens"] = keys.pop("max_completion_tokens")
22
+ return keys
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: yaicli
3
- Version: 0.6.1
3
+ Version: 0.6.3
4
4
  Summary: A simple CLI tool to interact with LLM
5
5
  Project-URL: Homepage, https://github.com/belingud/yaicli
6
6
  Project-URL: Repository, https://github.com/belingud/yaicli
@@ -212,7 +212,7 @@ Keywords: ai,ai-assistant,ai-chat,ai-interaction,anthropic,chatgpt,claude,cli,co
212
212
  Classifier: License :: OSI Approved :: Apache Software License
213
213
  Classifier: Operating System :: OS Independent
214
214
  Classifier: Programming Language :: Python :: 3
215
- Requires-Python: >=3.9
215
+ Requires-Python: >=3.10
216
216
  Requires-Dist: click>=8.1.8
217
217
  Requires-Dist: distro>=1.9.0
218
218
  Requires-Dist: httpx>=0.28.1
@@ -225,12 +225,15 @@ Requires-Dist: socksio>=1.0.0
225
225
  Requires-Dist: typer>=0.16.0
226
226
  Provides-Extra: all
227
227
  Requires-Dist: cohere>=5.15.0; extra == 'all'
228
+ Requires-Dist: google-genai>=1.20.0; extra == 'all'
228
229
  Requires-Dist: ollama>=0.5.1; extra == 'all'
229
230
  Requires-Dist: volcengine-python-sdk>=3.0.15; extra == 'all'
230
231
  Provides-Extra: cohere
231
232
  Requires-Dist: cohere>=5.15.0; extra == 'cohere'
232
233
  Provides-Extra: doubao
233
234
  Requires-Dist: volcengine-python-sdk>=3.0.15; extra == 'doubao'
235
+ Provides-Extra: gemini
236
+ Requires-Dist: google-genai>=1.20.0; extra == 'gemini'
234
237
  Provides-Extra: ollama
235
238
  Requires-Dist: ollama>=0.5.1; extra == 'ollama'
236
239
  Description-Content-Type: text/markdown
@@ -375,7 +378,7 @@ settings, just as below:
375
378
  ```ini
376
379
  [core]
377
380
  PROVIDER=openai
378
- BASE_URL=https://api.openai.com/v1
381
+ BASE_URL=
379
382
  API_KEY=
380
383
  MODEL=gpt-4o
381
384
 
@@ -387,7 +390,7 @@ OS_NAME=auto
387
390
  STREAM=true
388
391
 
389
392
  # LLM parameters
390
- TEMPERATURE=0.5
393
+ TEMPERATURE=0.3
391
394
  TOP_P=1.0
392
395
  MAX_TOKENS=1024
393
396
  TIMEOUT=60
@@ -518,6 +521,14 @@ API_KEY=
518
521
  MODEL=llama-3.3-70b-versatile
519
522
  ```
520
523
 
524
+ #### XAI
525
+
526
+ ```ini
527
+ PROVIDER=xai
528
+ API_KEY=
529
+ MODEL=grok-3
530
+ ```
531
+
521
532
  #### Chatglm
522
533
 
523
534
  ```ini
@@ -1,10 +1,10 @@
1
- pyproject.toml,sha256=kId8ADm-DI5ydtpa9S2__9z0qnrxI7k0e1D-LPXXsKA,2434
1
+ pyproject.toml,sha256=BfvXPlqqvIqhYBItAj3HclRLukitrn0kdwIYxdUJBgU,2531
2
2
  yaicli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
3
  yaicli/chat.py,sha256=_emvZEdgMBth2nQGaNWPf0P45oW2k3bpuIwqsxFcM5A,13676
4
- yaicli/cli.py,sha256=YUKbtvRcNbe6iTmm0dx-38QDzzrAP4cOYaOChxpJ2wg,23673
4
+ yaicli/cli.py,sha256=Eu1CL9ZB8ElvXqpHqmoWoasC0Brc7-j_zd3RmPhdSEE,23310
5
5
  yaicli/config.py,sha256=HrWYcelLXE61XX719eVcuuo3292xxf1BNQznWdvjQFQ,6535
6
6
  yaicli/console.py,sha256=vARPJd-3lafutsQWrGntQVjLrYqaJD3qisN82pmuhjU,1973
7
- yaicli/const.py,sha256=jC01jLLuuYJ7K_QaekXkIN7j1bIKCoGwJeTpquJy55Q,8178
7
+ yaicli/const.py,sha256=Uvdm1rc5zhjE2r9ioCYiSzhk8cT4mfgO2Mm4mNs71Nk,8176
8
8
  yaicli/entry.py,sha256=Q1eqLE7tcHide7ooyPO7OCJpKE2YVuxR-NNFA2Pt2Hw,8693
9
9
  yaicli/exceptions.py,sha256=WBYg8OTJJzaj7lt6HE7ZyBoe5T6A3yZRNCRfWd4iN0c,372
10
10
  yaicli/history.py,sha256=s-57X9FMsaQHF7XySq1gGH_jpd_cHHTYafYu2ECuG6M,2472
@@ -16,28 +16,31 @@ yaicli/tools.py,sha256=xw8KEs_xlSf79A2Aq1rAsUWahS6A_e5QMLt7QDXL5bs,5086
16
16
  yaicli/utils.py,sha256=bpo3Xhozpxsaci3FtEIKZ32l4ZdyWMsrHjYGX0tB4J4,4541
17
17
  yaicli/functions/__init__.py,sha256=_FJooQ9GkijG8xLwuU0cr5GBrGnC9Nc6bnCeUjrsT0k,1271
18
18
  yaicli/functions/buildin/execute_shell_command.py,sha256=unl1-F8p6QZajeHdA0u5UpURMJM0WhdWMUWCCCHVRcI,1320
19
- yaicli/llms/__init__.py,sha256=cN54nu-YalZipXjLW0YAAe0rRv0tXAQ8lLi1ohuTpao,363
19
+ yaicli/llms/__init__.py,sha256=x78cJujrJkelXPnzHS6pzHkITZdgLYZqJMnrMHbptoc,134
20
20
  yaicli/llms/client.py,sha256=mkE9KHSuPcJfpNQXbzF2YXGkel3jrOW8KfQ3YYpaK4M,4453
21
- yaicli/llms/provider.py,sha256=u_pHF806rmKqkAuKfBi1vNAm2xdWfnqFPYMb6xf2wTQ,2959
22
- yaicli/llms/providers/ai21_provider.py,sha256=_RRHtj7Nopwm6t_D_jukA7VjqVhlOC_0TphRL-UxVnI,2752
21
+ yaicli/llms/provider.py,sha256=YhX6RcMQqhac4EoQTY_AXDm-jtHYfH_K7Jikqvpc3H8,3159
22
+ yaicli/llms/providers/ai21_provider.py,sha256=SvgGj9_87KEqmxCMLbtsSkT8J3rUD7Mb21UF7pMWsks,3035
23
23
  yaicli/llms/providers/chatglm_provider.py,sha256=1xP4KVAi6SDKZ-lMi2wdzywtDydsTf6jDzh3jBBGMfA,6437
24
- yaicli/llms/providers/chutes_provider.py,sha256=qaTV8863zWLUsxnjWqpOUCVtnxS4tTzFCJBo5yakvhU,431
24
+ yaicli/llms/providers/chutes_provider.py,sha256=mtvWvRRfHPH3JFfzym87wXtPNiMpLnur3805N9acx7E,882
25
25
  yaicli/llms/providers/cohere_provider.py,sha256=hc6vQxbCHz9kM2tNKK-kGkuOf4-gkskXW9ctr9V4Cxk,10837
26
26
  yaicli/llms/providers/deepseek_provider.py,sha256=VjGes_jFin5WGYNFxYKMoHwgAQX_eYbYhQKfjeh-9eI,438
27
27
  yaicli/llms/providers/doubao_provider.py,sha256=4eOdE91ITUn3uo3mvYAzdrHsuFIIBwZWib21mtZn8OY,1938
28
- yaicli/llms/providers/groq_provider.py,sha256=W87b22nSILZiFXSzMrU4aL6zdtTzDDUihJhmFvGKxAc,639
28
+ yaicli/llms/providers/gemini_provider.py,sha256=iCRDqHRBFeTD_2NQwlsAlxFU7cKK4iyjimaPSp4VySM,7923
29
+ yaicli/llms/providers/groq_provider.py,sha256=EiS1Yxw5jbAUBFCRYsJ57KYgZPk6oH-_gD72OfW8Oik,1358
29
30
  yaicli/llms/providers/infiniai_provider.py,sha256=1dseUIZiXsxYRATRtk_obFclyXMwi4glsP7l_tVtnv8,710
30
- yaicli/llms/providers/minimax_provider.py,sha256=V50Rfi_ffx7tSplPuAwjlZikSpqjdjqBVP8oSRQAQy0,435
31
+ yaicli/llms/providers/minimax_provider.py,sha256=W-j3dzrYMEv14bYt2pCPvPUxvxsUs-iMAcGB9yXakFs,744
31
32
  yaicli/llms/providers/modelscope_provider.py,sha256=BzBhYixiDEWB7gujQ0rcG__7nsv0psJRxdtYCYXBhdM,454
32
33
  yaicli/llms/providers/ollama_provider.py,sha256=pjpYjfnHWnExweZi1KGbT07JGkcxzKPhqICo8dD82D0,6967
33
- yaicli/llms/providers/openai_provider.py,sha256=5Oq2UEH2SVMbqy5oQZsC3zibzhKBehZyrtJyCVB8hsw,8589
34
- yaicli/llms/providers/openrouter_provider.py,sha256=McMBl9YWcUUWdjp9njphqCoSeHc37n-UJo8vy7EjhNg,443
35
- yaicli/llms/providers/sambanova_provider.py,sha256=ngYnOQFyYVjtplf_RKR-_DmlTNLKxK9kURxbprgyDJs,1227
34
+ yaicli/llms/providers/openai_provider.py,sha256=yl1vVKt8QzbN_dbsW_9rY8S_xkXI3Bo3Of4Cf7W3mJc,10075
35
+ yaicli/llms/providers/openrouter_provider.py,sha256=R-7FrUrCAKPZ3gbnuo0M6rPlVw1mvSBjbLGs_FtZWM0,732
36
+ yaicli/llms/providers/sambanova_provider.py,sha256=FFLrsvARt1UPAFWWgiuB6zvGzGKdtehKL58HdE1fo_M,2254
36
37
  yaicli/llms/providers/siliconflow_provider.py,sha256=7Ir73me9jGMO5TAZDjrAbX7tbb_QBmLjTGywY0yliqc,446
37
- yaicli/llms/providers/targon_provider.py,sha256=Lqj2i-YydduqKpeMED06gL-0gu-8Y548icgrSsxPVUs,432
38
- yaicli/llms/providers/yi_provider.py,sha256=UgIOmxvyq7UxnF20NBX2oFDnhRdcAKnx_qplrsbpobM,438
39
- yaicli-0.6.1.dist-info/METADATA,sha256=9UTlwCBHXS9GCOj63pnfupmLwOdUkrHV1onl-N7s0RE,53514
40
- yaicli-0.6.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
41
- yaicli-0.6.1.dist-info/entry_points.txt,sha256=iYVyQP0PJIm9tQnlQheqT435kK_xdGoi5j9aswGV9hA,66
42
- yaicli-0.6.1.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
43
- yaicli-0.6.1.dist-info/RECORD,,
38
+ yaicli/llms/providers/targon_provider.py,sha256=RQ808eS9lvsyvlzyKaQYcN0NimbpoNWgjHUzY1gLNs4,717
39
+ yaicli/llms/providers/vertexai_provider.py,sha256=_ddrse1LfXRChTgkvxUlexyfJlfr0sVJH-Rmno3djSI,636
40
+ yaicli/llms/providers/xai_provider.py,sha256=Q6iOvJZOXIAwRiiHMKEBgq8-W6SGVZ9QD1_532bNYfo,199
41
+ yaicli/llms/providers/yi_provider.py,sha256=EnTm9qTxHPnzERsKqgGnzRIVhXFcAEdYqtOra65pGmY,719
42
+ yaicli-0.6.3.dist-info/METADATA,sha256=EfU2thy5G2Ge-BaCM3RT0quP2YUPz0bBWLJjKJugf_w,53677
43
+ yaicli-0.6.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
44
+ yaicli-0.6.3.dist-info/entry_points.txt,sha256=iYVyQP0PJIm9tQnlQheqT435kK_xdGoi5j9aswGV9hA,66
45
+ yaicli-0.6.3.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
46
+ yaicli-0.6.3.dist-info/RECORD,,
File without changes