janito 2.10.0__py3-none-any.whl → 2.14.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.
@@ -0,0 +1,476 @@
1
+ import uuid
2
+ import traceback
3
+ import re
4
+ import json
5
+ import math
6
+ import time
7
+ import os
8
+ import logging
9
+ from rich import pretty
10
+ from janito.llm.driver import LLMDriver
11
+ from janito.llm.driver_input import DriverInput
12
+ from janito.driver_events import RequestFinished, RequestStatus, RateLimitRetry
13
+ from janito.llm.message_parts import TextMessagePart, FunctionCallMessagePart
14
+
15
+ try:
16
+ import openai
17
+ available = True
18
+ unavailable_reason = None
19
+ except ImportError:
20
+ available = False
21
+ unavailable_reason = "openai module not installed"
22
+
23
+
24
+ class ZAIModelDriver(LLMDriver):
25
+ available = available
26
+ unavailable_reason = unavailable_reason
27
+ def _get_message_from_result(self, result):
28
+ """Extract the message object from the provider result (Z.AI-specific)."""
29
+ if hasattr(result, "choices") and result.choices:
30
+ return result.choices[0].message
31
+ return None
32
+
33
+ """
34
+ Z.AI LLM driver (threaded, queue-based, stateless). Uses input/output queues accessible via instance attributes.
35
+ """
36
+
37
+ def __init__(self, tools_adapter=None, provider_name=None):
38
+ super().__init__(tools_adapter=tools_adapter, provider_name=provider_name)
39
+
40
+ def _prepare_api_kwargs(self, config, conversation):
41
+ """
42
+ Prepares API kwargs for Z.AI, including tool schemas if tools_adapter is present,
43
+ and Z.AI-specific arguments (model, max_tokens, temperature, etc.).
44
+ """
45
+ api_kwargs = {}
46
+ # Tool schemas (moved from base)
47
+ if self.tools_adapter:
48
+ try:
49
+ from janito.providers.zai.schema_generator import (
50
+ generate_tool_schemas,
51
+ )
52
+
53
+ tool_classes = self.tools_adapter.get_tool_classes()
54
+ tool_schemas = generate_tool_schemas(tool_classes)
55
+ api_kwargs["tools"] = tool_schemas
56
+ except Exception as e:
57
+ api_kwargs["tools"] = []
58
+ if hasattr(config, "verbose_api") and config.verbose_api:
59
+ print(f"[ZAIModelDriver] Tool schema generation failed: {e}")
60
+ # Z.AI-specific parameters
61
+ if config.model:
62
+ api_kwargs["model"] = config.model
63
+ # Use max_tokens for Z.ai SDK compatibility
64
+ if hasattr(config, "max_tokens") and config.max_tokens is not None:
65
+ api_kwargs["max_tokens"] = int(config.max_tokens)
66
+ elif (
67
+ hasattr(config, "max_completion_tokens")
68
+ and config.max_completion_tokens is not None
69
+ ):
70
+ # Fallback to max_completion_tokens if max_tokens not set
71
+ api_kwargs["max_tokens"] = int(config.max_completion_tokens)
72
+ for p in (
73
+ "temperature",
74
+ "top_p",
75
+ "presence_penalty",
76
+ "frequency_penalty",
77
+ "stop",
78
+ "reasoning_effort",
79
+ ):
80
+ v = getattr(config, p, None)
81
+ if v is not None:
82
+ api_kwargs[p] = v
83
+ api_kwargs["messages"] = conversation
84
+ api_kwargs["stream"] = False
85
+ # Always return the prepared kwargs, even if no tools are registered. The
86
+ # OpenAI Python SDK expects a **mapping** – passing *None* will raise
87
+ # ``TypeError: argument after ** must be a mapping, not NoneType``.
88
+ return api_kwargs
89
+
90
+ def _call_api(self, driver_input: DriverInput):
91
+ """Call the Z.AI-compatible chat completion endpoint with retry and error handling."""
92
+ cancel_event = getattr(driver_input, "cancel_event", None)
93
+ config = driver_input.config
94
+ conversation = self.convert_history_to_api_messages(
95
+ driver_input.conversation_history
96
+ )
97
+ request_id = getattr(config, "request_id", None)
98
+ self._print_api_call_start(config)
99
+ client = self._instantiate_zai_client(config)
100
+ api_kwargs = self._prepare_api_kwargs(config, conversation)
101
+ max_retries = getattr(config, "max_retries", 3)
102
+ attempt = 1
103
+ while True:
104
+ try:
105
+ self._print_api_attempt(config, attempt, max_retries, api_kwargs)
106
+ if self._check_cancel(cancel_event, request_id, before_call=True):
107
+ return None
108
+ result = client.chat.completions.create(**api_kwargs)
109
+ if self._check_cancel(cancel_event, request_id, before_call=False):
110
+ return None
111
+ self._handle_api_success(config, result, request_id)
112
+ return result
113
+ except Exception as e:
114
+ if self._handle_api_exception(
115
+ e, config, api_kwargs, attempt, max_retries, request_id
116
+ ):
117
+ attempt += 1
118
+ continue
119
+ raise
120
+
121
+ def _print_api_call_start(self, config):
122
+ if getattr(config, "verbose_api", False):
123
+ tool_adapter_name = (
124
+ type(self.tools_adapter).__name__ if self.tools_adapter else None
125
+ )
126
+ tool_names = []
127
+ if self.tools_adapter and hasattr(self.tools_adapter, "list_tools"):
128
+ try:
129
+ tool_names = self.tools_adapter.list_tools()
130
+ except Exception:
131
+ tool_names = ["<error retrieving tools>"]
132
+ print(
133
+ f"[verbose-api] Z.AI API call about to be sent. Model: {config.model}, max_tokens: {config.max_tokens}, tools_adapter: {tool_adapter_name}, tool_names: {tool_names}",
134
+ flush=True,
135
+ )
136
+
137
+ def _print_api_attempt(self, config, attempt, max_retries, api_kwargs):
138
+ if getattr(config, "verbose_api", False):
139
+ print(
140
+ f"[Z.AI] API CALL (attempt {attempt}/{max_retries}): chat.completions.create(**{api_kwargs})",
141
+ flush=True,
142
+ )
143
+
144
+ def _handle_api_success(self, config, result, request_id):
145
+ self._print_verbose_result(config, result)
146
+ usage_dict = self._extract_usage(result)
147
+ if getattr(config, "verbose_api", False):
148
+ print(
149
+ f"[Z.AI][DEBUG] Attaching usage info to RequestFinished: {usage_dict}",
150
+ flush=True,
151
+ )
152
+ self.output_queue.put(
153
+ RequestFinished(
154
+ driver_name=self.__class__.__name__,
155
+ request_id=request_id,
156
+ response=result,
157
+ status=RequestStatus.SUCCESS,
158
+ usage=usage_dict,
159
+ )
160
+ )
161
+ if getattr(config, "verbose_api", False):
162
+ pretty.install()
163
+ print("[Z.AI] API RESPONSE:", flush=True)
164
+ pretty.pprint(result)
165
+
166
+ def _handle_api_exception(
167
+ self, e, config, api_kwargs, attempt, max_retries, request_id
168
+ ):
169
+ status_code = getattr(e, "status_code", None)
170
+ err_str = str(e)
171
+ lower_err = err_str.lower()
172
+ is_insufficient_quota = (
173
+ "insufficient_quota" in lower_err
174
+ or "exceeded your current quota" in lower_err
175
+ )
176
+ is_rate_limit = (
177
+ status_code == 429
178
+ or "error code: 429" in lower_err
179
+ or "resource_exhausted" in lower_err
180
+ ) and not is_insufficient_quota
181
+ if not is_rate_limit or attempt > max_retries:
182
+ self._handle_fatal_exception(e, config, api_kwargs)
183
+ retry_delay = self._extract_retry_delay_seconds(e)
184
+ if retry_delay is None:
185
+ retry_delay = min(2 ** (attempt - 1), 30)
186
+ self.output_queue.put(
187
+ RateLimitRetry(
188
+ driver_name=self.__class__.__name__,
189
+ request_id=request_id,
190
+ attempt=attempt,
191
+ retry_delay=retry_delay,
192
+ error=err_str,
193
+ details={},
194
+ )
195
+ )
196
+ if getattr(config, "verbose_api", False):
197
+ print(
198
+ f"[Z.AI][RateLimit] Attempt {attempt}/{max_retries} failed with rate-limit. Waiting {retry_delay}s before retry.",
199
+ flush=True,
200
+ )
201
+ start_wait = time.time()
202
+ while time.time() - start_wait < retry_delay:
203
+ if self._check_cancel(
204
+ getattr(config, "cancel_event", None), request_id, before_call=False
205
+ ):
206
+ return False
207
+ time.sleep(0.1)
208
+ return True
209
+
210
+ def _extract_retry_delay_seconds(self, exception) -> float | None:
211
+ """Extract the retry delay in seconds from the provider error response.
212
+
213
+ Handles both the Google Gemini style ``RetryInfo`` protobuf (where it's a
214
+ ``retryDelay: '41s'`` string in JSON) and any number found after the word
215
+ ``retryDelay``. Returns ``None`` if no delay could be parsed.
216
+ """
217
+ try:
218
+ # Some SDKs expose the raw response JSON on e.args[0]
219
+ if hasattr(exception, "response") and hasattr(exception.response, "text"):
220
+ payload = exception.response.text
221
+ else:
222
+ payload = str(exception)
223
+ # Look for 'retryDelay': '41s' or similar
224
+ m = re.search(
225
+ r"retryDelay['\"]?\s*[:=]\s*['\"]?(\d+(?:\.\d+)?)(s)?", payload
226
+ )
227
+ if m:
228
+ return float(m.group(1))
229
+ # Fallback: generic number of seconds in the message
230
+ m2 = re.search(r"(\d+(?:\.\d+)?)\s*s(?:econds)?", payload)
231
+ if m2:
232
+ return float(m2.group(1))
233
+ except Exception:
234
+ pass
235
+ return None
236
+
237
+ def _handle_fatal_exception(self, e, config, api_kwargs):
238
+ """Common path for unrecoverable exceptions.
239
+
240
+ Prints diagnostics (respecting ``verbose_api``) then re-raises the
241
+ exception so standard error handling in ``LLMDriver`` continues.
242
+ """
243
+ is_verbose = getattr(config, "verbose_api", False)
244
+ if is_verbose:
245
+ print(f"[ERROR] Exception during Z.AI API call: {e}", flush=True)
246
+ print(f"[ERROR] config: {config}", flush=True)
247
+ print(
248
+ f"[ERROR] api_kwargs: {api_kwargs if 'api_kwargs' in locals() else 'N/A'}",
249
+ flush=True,
250
+ )
251
+ print("[ERROR] Full stack trace:", flush=True)
252
+ print(traceback.format_exc(), flush=True)
253
+ raise
254
+
255
+ def _instantiate_zai_client(self, config):
256
+ try:
257
+ if not config.api_key:
258
+ provider_name = getattr(self, "provider_name", "ZAI")
259
+ print(
260
+ f"[ERROR] No API key found for provider '{provider_name}'. Please set the API key using:"
261
+ )
262
+ print(f" janito --set-api-key YOUR_API_KEY -p {provider_name.lower()}")
263
+ print(
264
+ f"Or set the {provider_name.upper()}_API_KEY environment variable."
265
+ )
266
+ raise ValueError(f"API key is required for provider '{provider_name}'")
267
+
268
+ api_key_display = str(config.api_key)
269
+ if api_key_display and len(api_key_display) > 8:
270
+ api_key_display = api_key_display[:4] + "..." + api_key_display[-4:]
271
+
272
+ # HTTP debug wrapper
273
+ if os.environ.get("ZAI_DEBUG_HTTP", "0") == "1":
274
+ from http.client import HTTPConnection
275
+
276
+ HTTPConnection.debuglevel = 1
277
+ logging.basicConfig()
278
+ logging.getLogger().setLevel(logging.DEBUG)
279
+ requests_log = logging.getLogger("http.client")
280
+ requests_log.setLevel(logging.DEBUG)
281
+ requests_log.propagate = True
282
+ print(
283
+ "[ZAIModelDriver] HTTP debug enabled via ZAI_DEBUG_HTTP=1",
284
+ flush=True,
285
+ )
286
+
287
+ # Use OpenAI SDK for Z.AI API compatibility
288
+ try:
289
+ import openai
290
+ except ImportError:
291
+ raise ImportError("openai module is not available. Please install it with: pip install openai")
292
+ client = openai.OpenAI(
293
+ api_key=config.api_key, base_url="https://api.z.ai/api/paas/v4/"
294
+ )
295
+ return client
296
+ except Exception as e:
297
+ print(
298
+ f"[ERROR] Exception during Z.AI client instantiation: {e}", flush=True
299
+ )
300
+ print(traceback.format_exc(), flush=True)
301
+ raise
302
+
303
+ def _check_cancel(self, cancel_event, request_id, before_call=True):
304
+ if cancel_event is not None and cancel_event.is_set():
305
+ status = RequestStatus.CANCELLED
306
+ reason = (
307
+ "Cancelled before API call"
308
+ if before_call
309
+ else "Cancelled during API call"
310
+ )
311
+ self.output_queue.put(
312
+ RequestFinished(
313
+ driver_name=self.__class__.__name__,
314
+ request_id=request_id,
315
+ status=status,
316
+ reason=reason,
317
+ )
318
+ )
319
+ return True
320
+ return False
321
+
322
+ def _print_verbose_result(self, config, result):
323
+ if config.verbose_api:
324
+ print("[Z.AI] API RAW RESULT:", flush=True)
325
+ pretty.pprint(result)
326
+ if hasattr(result, "__dict__"):
327
+ print("[Z.AI] API RESULT __dict__:", flush=True)
328
+ pretty.pprint(result.__dict__)
329
+ try:
330
+ print("[Z.AI] API RESULT as dict:", dict(result), flush=True)
331
+ except Exception:
332
+ pass
333
+ print(
334
+ f"[Z.AI] API RESULT .usage: {getattr(result, 'usage', None)}",
335
+ flush=True,
336
+ )
337
+ try:
338
+ print(f"[Z.AI] API RESULT ['usage']: {result['usage']}", flush=True)
339
+ except Exception:
340
+ pass
341
+ if not hasattr(result, "usage") or getattr(result, "usage", None) is None:
342
+ print(
343
+ "[Z.AI][WARNING] No usage info found in API response.", flush=True
344
+ )
345
+
346
+ def _extract_usage(self, result):
347
+ usage = getattr(result, "usage", None)
348
+ if usage is not None:
349
+ usage_dict = self._usage_to_dict(usage)
350
+ if usage_dict is None:
351
+ print(
352
+ "[Z.AI][WARNING] Could not convert usage to dict, using string fallback.",
353
+ flush=True,
354
+ )
355
+ usage_dict = str(usage)
356
+ else:
357
+ usage_dict = self._extract_usage_from_result_dict(result)
358
+ return usage_dict
359
+
360
+ def _usage_to_dict(self, usage):
361
+ if hasattr(usage, "model_dump") and callable(getattr(usage, "model_dump")):
362
+ try:
363
+ return usage.model_dump()
364
+ except Exception:
365
+ pass
366
+ if hasattr(usage, "dict") and callable(getattr(usage, "dict")):
367
+ try:
368
+ return usage.dict()
369
+ except Exception:
370
+ pass
371
+ try:
372
+ return dict(usage)
373
+ except Exception:
374
+ try:
375
+ return vars(usage)
376
+ except Exception:
377
+ pass
378
+ return None
379
+
380
+ def _extract_usage_from_result_dict(self, result):
381
+ try:
382
+ return result["usage"]
383
+ except Exception:
384
+ return None
385
+
386
+ def convert_history_to_api_messages(self, conversation_history):
387
+ """
388
+ Convert LLMConversationHistory to the list of dicts required by Z.AI's API.
389
+ Handles 'tool_results' and 'tool_calls' roles for compliance.
390
+ """
391
+ api_messages = []
392
+ for msg in conversation_history.get_history():
393
+ self._append_api_message(api_messages, msg)
394
+ self._replace_none_content(api_messages)
395
+ return api_messages
396
+
397
+ def _append_api_message(self, api_messages, msg):
398
+ role = msg.get("role")
399
+ content = msg.get("content")
400
+ if role == "tool_results":
401
+ self._handle_tool_results(api_messages, content)
402
+ elif role == "tool_calls":
403
+ self._handle_tool_calls(api_messages, content)
404
+ else:
405
+ self._handle_other_roles(api_messages, msg, role, content)
406
+
407
+ def _handle_tool_results(self, api_messages, content):
408
+ try:
409
+ results = json.loads(content) if isinstance(content, str) else content
410
+ except Exception:
411
+ results = [content]
412
+ for result in results:
413
+ if isinstance(result, dict):
414
+ api_messages.append(
415
+ {
416
+ "role": "tool",
417
+ "content": result.get("content", ""),
418
+ "name": result.get("name", ""),
419
+ "tool_call_id": result.get("tool_call_id", ""),
420
+ }
421
+ )
422
+ else:
423
+ api_messages.append(
424
+ {
425
+ "role": "tool",
426
+ "content": str(result),
427
+ "name": "",
428
+ "tool_call_id": "",
429
+ }
430
+ )
431
+
432
+ def _handle_tool_calls(self, api_messages, content):
433
+ try:
434
+ tool_calls = json.loads(content) if isinstance(content, str) else content
435
+ except Exception:
436
+ tool_calls = []
437
+ api_messages.append(
438
+ {"role": "assistant", "content": "", "tool_calls": tool_calls}
439
+ )
440
+
441
+ def _handle_other_roles(self, api_messages, msg, role, content):
442
+ if role == "function":
443
+ name = ""
444
+ if isinstance(msg, dict):
445
+ metadata = msg.get("metadata", {})
446
+ name = metadata.get("name", "") if isinstance(metadata, dict) else ""
447
+ api_messages.append({"role": "tool", "content": content, "name": name})
448
+ else:
449
+ api_messages.append(msg)
450
+
451
+ def _replace_none_content(self, api_messages):
452
+ for m in api_messages:
453
+ if m.get("content", None) is None:
454
+ m["content"] = ""
455
+
456
+ def _convert_completion_message_to_parts(self, message):
457
+ """
458
+ Convert a Z.AI completion message object to a list of MessagePart objects.
459
+ Handles text, tool calls, and can be extended for other types.
460
+ """
461
+ parts = []
462
+ # Text content
463
+ content = getattr(message, "content", None)
464
+ if content:
465
+ parts.append(TextMessagePart(content=content))
466
+ # Tool calls
467
+ tool_calls = getattr(message, "tool_calls", None) or []
468
+ for tool_call in tool_calls:
469
+ parts.append(
470
+ FunctionCallMessagePart(
471
+ tool_call_id=getattr(tool_call, "id", ""),
472
+ function=getattr(tool_call, "function", None),
473
+ )
474
+ )
475
+ # Extend here for other message part types if needed
476
+ return parts
janito/mkdocs.yml CHANGED
@@ -1,6 +1,6 @@
1
1
  site_name: Janito CLI Documentation
2
2
  site_description: A powerful command-line tool for running LLM-powered workflows
3
- site_url: https://janito.readthedocs.io/
3
+ site_url: https://ikignosis.github.io/janito/
4
4
 
5
5
  nav:
6
6
  - Home: README.md
@@ -6,3 +6,4 @@ import janito.providers.anthropic.provider
6
6
  import janito.providers.deepseek.provider
7
7
  import janito.providers.moonshotai.provider
8
8
  import janito.providers.alibaba.provider
9
+ import janito.providers.zai.provider
@@ -30,4 +30,11 @@ MODEL_SPECS = {
30
30
  category="Alibaba Qwen3 Coder Plus Model (OpenAI-compatible)",
31
31
  driver="OpenAIModelDriver",
32
32
  ),
33
+ "qwen3-coder-480b-a35b-instruct": LLMModelInfo(
34
+ name="qwen3-coder-480b-a35b-instruct",
35
+ context=262144,
36
+ max_response=65536,
37
+ category="Alibaba Qwen3 Coder 480B A35B Instruct Model (OpenAI-compatible)",
38
+ driver="OpenAIModelDriver",
39
+ ),
33
40
  }
@@ -17,7 +17,7 @@ class AlibabaProvider(LLMProvider):
17
17
  NAME = "alibaba"
18
18
  MAINTAINER = "João Pinto <janito@ikignosis.org>"
19
19
  MODEL_SPECS = MODEL_SPECS
20
- DEFAULT_MODEL = "qwen-turbo" # Options: qwen-turbo, qwen-plus, qwen-max
20
+ DEFAULT_MODEL = "qwen3-coder-plus" # Options: qwen-turbo, qwen-plus, qwen-max, qwen3-coder-plus
21
21
 
22
22
  def __init__(
23
23
  self, auth_manager: LLMAuthManager = None, config: LLMDriverConfig = None
@@ -0,0 +1 @@
1
+ # Z.AI provider package
@@ -0,0 +1,38 @@
1
+ from janito.llm.model import LLMModelInfo
2
+
3
+ MODEL_SPECS = {
4
+ "glm-4.5": LLMModelInfo(
5
+ name="glm-4.5",
6
+ context=128000,
7
+ max_input=128000,
8
+ max_cot=4096,
9
+ max_response=4096,
10
+ thinking_supported=True,
11
+ other={
12
+ "description": "Z.AI's GLM-4.5 model for advanced reasoning and conversation",
13
+ "supports_tools": True,
14
+ "supports_images": True,
15
+ "supports_audio": False,
16
+ "supports_video": False,
17
+ "input_cost_per_1k": 0.0005,
18
+ "output_cost_per_1k": 0.0015,
19
+ },
20
+ ),
21
+ "glm-4.5-air": LLMModelInfo(
22
+ name="glm-4.5-air",
23
+ context=128000,
24
+ max_input=128000,
25
+ max_cot=4096,
26
+ max_response=4096,
27
+ thinking_supported=True,
28
+ other={
29
+ "description": "Z.AI's GLM-4.5-Air model - compact and efficient version",
30
+ "supports_tools": True,
31
+ "supports_images": True,
32
+ "supports_audio": False,
33
+ "supports_video": False,
34
+ "input_cost_per_1k": 0.0003,
35
+ "output_cost_per_1k": 0.0009,
36
+ },
37
+ ),
38
+ }
@@ -0,0 +1,131 @@
1
+ from janito.llm.provider import LLMProvider
2
+ from janito.llm.model import LLMModelInfo
3
+ from janito.llm.auth import LLMAuthManager
4
+ from janito.llm.driver_config import LLMDriverConfig
5
+ from janito.drivers.zai.driver import ZAIModelDriver
6
+ from janito.tools import get_local_tools_adapter
7
+ from janito.providers.registry import LLMProviderRegistry
8
+ from .model_info import MODEL_SPECS
9
+ from queue import Queue
10
+
11
+ available = ZAIModelDriver.available
12
+ unavailable_reason = ZAIModelDriver.unavailable_reason
13
+
14
+
15
+ class ZAIProvider(LLMProvider):
16
+ name = "zai"
17
+ NAME = "zai"
18
+ MAINTAINER = "João Pinto <janito@ikignosis.org>"
19
+ MODEL_SPECS = MODEL_SPECS
20
+ DEFAULT_MODEL = "glm-4.5-air" # Options: glm-4.5, glm-4.5-air
21
+
22
+ def __init__(
23
+ self, auth_manager: LLMAuthManager = None, config: LLMDriverConfig = None
24
+ ):
25
+ if not self.available:
26
+ self._setup_unavailable()
27
+ else:
28
+ self._setup_available(auth_manager, config)
29
+
30
+ def _setup_unavailable(self):
31
+ # Even when the ZAI driver is unavailable we still need a tools adapter
32
+ # so that any generic logic that expects `execute_tool()` to work does not
33
+ # crash with an AttributeError when it tries to access `self._tools_adapter`.
34
+ self._tools_adapter = get_local_tools_adapter()
35
+ self._driver = None
36
+ # Initialize _driver_config to avoid AttributeError
37
+ self._driver_config = LLMDriverConfig(model=None)
38
+
39
+ def _setup_available(self, auth_manager, config):
40
+ self.auth_manager = auth_manager or LLMAuthManager()
41
+ self._api_key = self.auth_manager.get_credentials(type(self).NAME)
42
+ if not self._api_key:
43
+ print(
44
+ f"[ERROR] No API key found for provider '{self.name}'. Please set the API key using:"
45
+ )
46
+ print(f" janito --set-api-key YOUR_API_KEY -p {self.name}")
47
+ print(f"Or set the ZAI_API_KEY environment variable.")
48
+ self._tools_adapter = get_local_tools_adapter()
49
+ return
50
+
51
+ self._tools_adapter = get_local_tools_adapter()
52
+ self._driver_config = config or LLMDriverConfig(model=None)
53
+ if not self._driver_config.model:
54
+ self._driver_config.model = self.DEFAULT_MODEL
55
+ if not self._driver_config.api_key:
56
+ self._driver_config.api_key = self._api_key
57
+
58
+ self._configure_model_tokens()
59
+ self.fill_missing_device_info(self._driver_config)
60
+ self._driver = None # to be provided by factory/agent
61
+
62
+ def _configure_model_tokens(self):
63
+ # Set only the correct token parameter for the model
64
+ model_name = self._driver_config.model
65
+ model_spec = self.MODEL_SPECS.get(model_name)
66
+ # Remove both to avoid stale values
67
+ if hasattr(self._driver_config, "max_tokens"):
68
+ self._driver_config.max_tokens = None
69
+ if hasattr(self._driver_config, "max_completion_tokens"):
70
+ self._driver_config.max_completion_tokens = None
71
+ if model_spec:
72
+ if getattr(model_spec, "thinking_supported", False):
73
+ max_cot = getattr(model_spec, "max_cot", None)
74
+ if max_cot and max_cot != "N/A":
75
+ self._driver_config.max_completion_tokens = int(max_cot)
76
+ else:
77
+ max_response = getattr(model_spec, "max_response", None)
78
+ if max_response and max_response != "N/A":
79
+ self._driver_config.max_tokens = int(max_response)
80
+
81
+ @property
82
+ def driver(self) -> ZAIModelDriver:
83
+ if not self.available:
84
+ raise ImportError(f"ZAIProvider unavailable: {self.unavailable_reason}")
85
+ return self._driver
86
+
87
+ @property
88
+ def available(self):
89
+ return available
90
+
91
+ @property
92
+ def unavailable_reason(self):
93
+ return unavailable_reason
94
+
95
+ def create_driver(self):
96
+ """
97
+ Creates and returns a new ZAIModelDriver instance with input/output queues.
98
+ """
99
+ driver = ZAIModelDriver(
100
+ tools_adapter=self._tools_adapter, provider_name=self.NAME
101
+ )
102
+ driver.config = self._driver_config
103
+ # NOTE: The caller is responsible for calling driver.start() if background processing is needed.
104
+ return driver
105
+
106
+ def create_agent(self, tools_adapter=None, agent_name: str = None, **kwargs):
107
+ from janito.llm.agent import LLMAgent
108
+
109
+ # Always create a new driver with the passed-in tools_adapter
110
+ if tools_adapter is None:
111
+ tools_adapter = get_local_tools_adapter()
112
+ # Should use new-style driver construction via queues/factory (handled elsewhere)
113
+ raise NotImplementedError(
114
+ "create_agent must be constructed via new factory using input/output queues and config."
115
+ )
116
+
117
+ @property
118
+ def model_name(self):
119
+ return self._driver_config.model
120
+
121
+ @property
122
+ def driver_config(self):
123
+ """Public, read-only access to the provider's LLMDriverConfig object."""
124
+ return self._driver_config
125
+
126
+ def execute_tool(self, tool_name: str, event_bus, *args, **kwargs):
127
+ self._tools_adapter.event_bus = event_bus
128
+ return self._tools_adapter.execute_by_name(tool_name, *args, **kwargs)
129
+
130
+
131
+ LLMProviderRegistry.register(ZAIProvider.NAME, ZAIProvider)