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