zai-cli 0.2.2__tar.gz → 0.2.3__tar.gz
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.
- {zai_cli-0.2.2 → zai_cli-0.2.3}/CHANGELOG.md +18 -0
- {zai_cli-0.2.2/zai_cli.egg-info → zai_cli-0.2.3}/PKG-INFO +7 -1
- {zai_cli-0.2.2 → zai_cli-0.2.3}/README.md +6 -0
- zai_cli-0.2.3/tests/test_fallback_diagnostics.py +177 -0
- zai_cli-0.2.3/zai/__init__.py +1 -0
- zai_cli-0.2.3/zai/core/fallback.py +257 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3/zai_cli.egg-info}/PKG-INFO +7 -1
- {zai_cli-0.2.2 → zai_cli-0.2.3}/zai_cli.egg-info/SOURCES.txt +1 -0
- zai_cli-0.2.2/zai/__init__.py +0 -1
- zai_cli-0.2.2/zai/core/fallback.py +0 -171
- {zai_cli-0.2.2 → zai_cli-0.2.3}/LICENSE +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/MANIFEST.in +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/pyproject.toml +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/scripts/release_preflight.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/setup.cfg +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/tests/test_agent.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/tests/test_auth.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/tests/test_browser.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/tests/test_code_runner.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/tests/test_config_main.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/tests/test_core.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/tests/test_errors.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/tests/test_file_mentions.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/tests/test_hooks_skills_session.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/tests/test_input.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/tests/test_integrations_cli.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/tests/test_interactive.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/tests/test_mcp.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/tests/test_plugins.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/tests/test_process.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/tests/test_project_memory.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/tests/test_project_rules.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/tests/test_providers.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/tests/test_release_preflight.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/tests/test_search.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/tests/test_security.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/tests/test_settings_cli.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/tests/test_storage.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/tests/test_streaming.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/tests/test_tokens.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/tests/test_tool_schema.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/tests/test_tools.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/tests/test_undo.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/tests/test_utilities.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/tests/test_vision.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/tests/test_watch.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/tests/test_workflows.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/zai/__main__.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/zai/cli/__init__.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/zai/cli/common.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/zai/cli/integrations.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/zai/cli/interactive.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/zai/cli/settings.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/zai/cli/setup_wizard.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/zai/cli/status.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/zai/cli/utilities.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/zai/cli/workflows.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/zai/commands/commit.md +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/zai/commands/explain.md +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/zai/commands/feature.md +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/zai/commands/fix.md +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/zai/commands/review.md +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/zai/config.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/zai/core/__init__.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/zai/core/agent.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/zai/core/auth.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/zai/core/cancellation.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/zai/core/commands.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/zai/core/compaction.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/zai/core/context.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/zai/core/errors.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/zai/core/file_mentions.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/zai/core/hooks.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/zai/core/memory.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/zai/core/process.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/zai/core/project_memory.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/zai/core/project_rules.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/zai/core/repomap.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/zai/core/runtime.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/zai/core/security.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/zai/core/session.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/zai/core/storage.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/zai/core/streaming.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/zai/core/tokens.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/zai/core/tool_schema.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/zai/core/undo.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/zai/core/watch.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/zai/main.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/zai/mcp/__init__.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/zai/mcp/client.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/zai/mcp/manager.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/zai/plugins/__init__.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/zai/plugins/base.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/zai/plugins/loader.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/zai/providers/__init__.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/zai/providers/anthropic.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/zai/providers/base.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/zai/providers/cerebras.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/zai/providers/gemini.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/zai/providers/groq.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/zai/providers/ollama.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/zai/providers/openai.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/zai/providers/openrouter.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/zai/providers/qwen.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/zai/skills/__init__.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/zai/skills/registry.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/zai/tools/__init__.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/zai/tools/browser.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/zai/tools/code_runner.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/zai/tools/files.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/zai/tools/git.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/zai/tools/search.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/zai/tools/vision.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/zai/ui/__init__.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/zai/ui/input.py +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/zai_cli.egg-info/dependency_links.txt +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/zai_cli.egg-info/entry_points.txt +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/zai_cli.egg-info/requires.txt +0 -0
- {zai_cli-0.2.2 → zai_cli-0.2.3}/zai_cli.egg-info/top_level.txt +0 -0
|
@@ -5,6 +5,24 @@ All notable changes to this project are documented in this file.
|
|
|
5
5
|
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and the project uses [Semantic Versioning](https://semver.org/).
|
|
7
7
|
|
|
8
|
+
## [0.2.3] - 2026-07-14
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Added structured fallback-attempt diagnostics with model, failure category,
|
|
13
|
+
secret-redacted detail, and a recommended next action.
|
|
14
|
+
- Added final failure summaries that list every attempted model and its result.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- Chat fallback now distinguishes missing or invalid credentials, quota limits,
|
|
19
|
+
unavailable models, timeouts, network errors, malformed responses, and other
|
|
20
|
+
provider failures instead of describing them all as network issues.
|
|
21
|
+
- Provider initialization and readiness-check failures no longer stop the
|
|
22
|
+
remaining fallback chain.
|
|
23
|
+
- Plain mode sends fallback diagnostics to stderr so successful response output
|
|
24
|
+
on stdout remains script-friendly.
|
|
25
|
+
|
|
8
26
|
## [0.2.2] - 2026-07-13
|
|
9
27
|
|
|
10
28
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: zai-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: Your personal AI CLI — free, fast, and smart
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/HumaizaNaz/zai_cli
|
|
@@ -618,6 +618,12 @@ Providers without an API key are skipped. Ollama is used only when its local
|
|
|
618
618
|
service is available. Provider quotas and model availability are controlled by
|
|
619
619
|
their respective services and can change independently of `zai`.
|
|
620
620
|
|
|
621
|
+
When an attempted model fails, `zai` reports the model, a stable failure
|
|
622
|
+
category, secret-redacted provider detail, and the recommended recovery action
|
|
623
|
+
before trying the next configured model. If every model fails, the final error
|
|
624
|
+
lists the complete attempt chain. In `--plain` mode these diagnostics go to
|
|
625
|
+
stderr, leaving successful response text on stdout.
|
|
626
|
+
|
|
621
627
|
Supported configuration keys:
|
|
622
628
|
|
|
623
629
|
```text
|
|
@@ -561,6 +561,12 @@ Providers without an API key are skipped. Ollama is used only when its local
|
|
|
561
561
|
service is available. Provider quotas and model availability are controlled by
|
|
562
562
|
their respective services and can change independently of `zai`.
|
|
563
563
|
|
|
564
|
+
When an attempted model fails, `zai` reports the model, a stable failure
|
|
565
|
+
category, secret-redacted provider detail, and the recommended recovery action
|
|
566
|
+
before trying the next configured model. If every model fails, the final error
|
|
567
|
+
lists the complete attempt chain. In `--plain` mode these diagnostics go to
|
|
568
|
+
stderr, leaving successful response text on stdout.
|
|
569
|
+
|
|
564
570
|
Supported configuration keys:
|
|
565
571
|
|
|
566
572
|
```text
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
from unittest.mock import MagicMock
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from zai.core import fallback
|
|
6
|
+
from zai.core.errors import (
|
|
7
|
+
AllModelsFailedError,
|
|
8
|
+
AuthenticationError,
|
|
9
|
+
MalformedResponseError,
|
|
10
|
+
ModelNotFoundError,
|
|
11
|
+
NetworkError,
|
|
12
|
+
NoAPIKeyError,
|
|
13
|
+
ProviderError,
|
|
14
|
+
ProviderTimeoutError,
|
|
15
|
+
QuotaError,
|
|
16
|
+
)
|
|
17
|
+
from zai.providers.base import Message, Response
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@pytest.mark.parametrize(
|
|
21
|
+
("error", "category", "guidance"),
|
|
22
|
+
[
|
|
23
|
+
(NoAPIKeyError("groq"), "missing credentials", "zai auth login groq"),
|
|
24
|
+
(AuthenticationError("groq", "bad key"), "invalid credentials", "zai auth login groq"),
|
|
25
|
+
(QuotaError("groq", "quota"), "quota/rate limit", "check quota"),
|
|
26
|
+
(ModelNotFoundError("groq", "gone"), "model unavailable", "zai model info fast"),
|
|
27
|
+
(ProviderTimeoutError("groq", "slow"), "timeout", "zai model configure fast"),
|
|
28
|
+
(NetworkError("offline"), "network error", "Check internet"),
|
|
29
|
+
(MalformedResponseError("groq", "choices"), "malformed response", "Retry once"),
|
|
30
|
+
(ProviderError("groq", "broken"), "provider error", "zai model test fast"),
|
|
31
|
+
],
|
|
32
|
+
)
|
|
33
|
+
def test_failure_attempt_has_accurate_category_and_guidance(error, category, guidance):
|
|
34
|
+
attempt = fallback._failure_attempt("fast", "groq", error)
|
|
35
|
+
|
|
36
|
+
assert attempt.category == category
|
|
37
|
+
assert guidance in attempt.next_action
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_failure_attempt_redacts_provider_secrets():
|
|
41
|
+
attempt = fallback._failure_attempt(
|
|
42
|
+
"groq",
|
|
43
|
+
"groq",
|
|
44
|
+
AuthenticationError("groq", "api_key=super-secret-provider-token"),
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
assert "super-secret-provider-token" not in attempt.detail
|
|
48
|
+
assert "[REDACTED_SECRET]" in attempt.detail
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _config(*names):
|
|
52
|
+
return {
|
|
53
|
+
"default_model": names[0],
|
|
54
|
+
"fallback_order": list(names),
|
|
55
|
+
"auto_fallback": True,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _provider(name, *, error=None, response="ok", available=True):
|
|
60
|
+
provider = MagicMock()
|
|
61
|
+
provider.name = name
|
|
62
|
+
provider.model_id = f"provider/{name}"
|
|
63
|
+
provider.context_window = 128_000
|
|
64
|
+
provider.supports_native_tools = True
|
|
65
|
+
provider.supports_streaming = False
|
|
66
|
+
provider.is_available.return_value = available
|
|
67
|
+
if error:
|
|
68
|
+
provider.chat.side_effect = error
|
|
69
|
+
else:
|
|
70
|
+
provider.chat.return_value = Response(content=response, model=provider.model_id)
|
|
71
|
+
return provider
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_chat_fallback_reports_auth_failure_and_uses_next_model(monkeypatch, capsys):
|
|
75
|
+
broken = _provider("groq", error=AuthenticationError("groq", "bad key"))
|
|
76
|
+
working = _provider("gemini", response="fallback worked")
|
|
77
|
+
providers = {"groq": broken, "gemini": working}
|
|
78
|
+
monkeypatch.setattr(fallback, "load_config", lambda: _config("groq", "gemini"))
|
|
79
|
+
monkeypatch.setattr(fallback, "get_provider", providers.get)
|
|
80
|
+
|
|
81
|
+
response, used = fallback.chat_with_fallback([Message(role="user", content="hi")])
|
|
82
|
+
|
|
83
|
+
output = capsys.readouterr().out
|
|
84
|
+
assert response.content == "fallback worked"
|
|
85
|
+
assert used == "gemini"
|
|
86
|
+
assert "groq failed" in output
|
|
87
|
+
assert "invalid credentials" in output
|
|
88
|
+
assert "Trying next model" in output
|
|
89
|
+
assert "Network issue" not in output
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_fallback_survives_provider_initialization_failure(monkeypatch, capsys):
|
|
93
|
+
working = _provider("gemini")
|
|
94
|
+
|
|
95
|
+
def get_provider(name):
|
|
96
|
+
if name == "groq":
|
|
97
|
+
raise RuntimeError("provider SDK failed to initialize")
|
|
98
|
+
return working
|
|
99
|
+
|
|
100
|
+
monkeypatch.setattr(fallback, "load_config", lambda: _config("groq", "gemini"))
|
|
101
|
+
monkeypatch.setattr(fallback, "get_provider", get_provider)
|
|
102
|
+
|
|
103
|
+
response, used = fallback.chat_with_fallback([Message(role="user", content="hi")])
|
|
104
|
+
|
|
105
|
+
assert response.content == "ok"
|
|
106
|
+
assert used == "gemini"
|
|
107
|
+
assert "provider error" in capsys.readouterr().out
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@pytest.mark.parametrize("runner", [fallback.chat_with_fallback, fallback.stream_with_fallback])
|
|
111
|
+
def test_fallback_does_not_swallow_cancellation_during_readiness(monkeypatch, runner):
|
|
112
|
+
from zai.core.cancellation import OperationCancelled
|
|
113
|
+
|
|
114
|
+
provider = _provider("groq")
|
|
115
|
+
provider.is_available.side_effect = OperationCancelled("stop now")
|
|
116
|
+
monkeypatch.setattr(fallback, "load_config", lambda: _config("groq", "gemini"))
|
|
117
|
+
monkeypatch.setattr(fallback, "get_provider", lambda name: provider)
|
|
118
|
+
|
|
119
|
+
with pytest.raises(OperationCancelled, match="stop now"):
|
|
120
|
+
runner([Message(role="user", content="hi")])
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def test_all_models_failed_lists_every_attempt(monkeypatch):
|
|
124
|
+
missing = _provider("groq", available=False)
|
|
125
|
+
limited = _provider("gemini", error=QuotaError("gemini", "quota exhausted"))
|
|
126
|
+
providers = {"groq": missing, "gemini": limited}
|
|
127
|
+
monkeypatch.setattr(fallback, "load_config", lambda: _config("groq", "gemini"))
|
|
128
|
+
monkeypatch.setattr(fallback, "get_provider", providers.get)
|
|
129
|
+
|
|
130
|
+
with pytest.raises(AllModelsFailedError) as caught:
|
|
131
|
+
fallback.chat_with_fallback([Message(role="user", content="hi")])
|
|
132
|
+
|
|
133
|
+
message = str(caught.value)
|
|
134
|
+
assert "groq (unavailable)" in message
|
|
135
|
+
assert "gemini (quota/rate limit)" in message
|
|
136
|
+
assert "zai model check" in message
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def test_stream_fallback_uses_same_diagnostics(monkeypatch, capsys):
|
|
140
|
+
broken = _provider("groq", error=NetworkError("offline"))
|
|
141
|
+
working = _provider("gemini", response="stream fallback")
|
|
142
|
+
providers = {"groq": broken, "gemini": working}
|
|
143
|
+
monkeypatch.setattr(fallback, "load_config", lambda: _config("groq", "gemini"))
|
|
144
|
+
monkeypatch.setattr(fallback, "get_provider", providers.get)
|
|
145
|
+
|
|
146
|
+
content, used = fallback.stream_with_fallback([Message(role="user", content="hi")])
|
|
147
|
+
|
|
148
|
+
output = capsys.readouterr().out
|
|
149
|
+
assert content == "stream fallback"
|
|
150
|
+
assert used == "gemini"
|
|
151
|
+
assert "network error" in output
|
|
152
|
+
assert "Check internet" in output
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def test_plain_mode_keeps_fallback_diagnostics_off_stdout(monkeypatch, capsys):
|
|
156
|
+
from zai.core.runtime import configure
|
|
157
|
+
|
|
158
|
+
broken = _provider("groq", error=NetworkError("offline"))
|
|
159
|
+
working = _provider("gemini", response="plain result")
|
|
160
|
+
providers = {"groq": broken, "gemini": working}
|
|
161
|
+
monkeypatch.setattr(fallback, "load_config", lambda: _config("groq", "gemini"))
|
|
162
|
+
monkeypatch.setattr(fallback, "get_provider", providers.get)
|
|
163
|
+
|
|
164
|
+
configure(plain=True)
|
|
165
|
+
try:
|
|
166
|
+
content, used = fallback.stream_with_fallback([
|
|
167
|
+
Message(role="user", content="hi")
|
|
168
|
+
])
|
|
169
|
+
finally:
|
|
170
|
+
configure()
|
|
171
|
+
|
|
172
|
+
captured = capsys.readouterr()
|
|
173
|
+
assert content == "plain result"
|
|
174
|
+
assert used == "gemini"
|
|
175
|
+
assert "plain result" in captured.out
|
|
176
|
+
assert "groq failed" not in captured.out
|
|
177
|
+
assert "groq failed" in captured.err
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.3"
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
from rich.console import Console
|
|
4
|
+
from ..providers import PROVIDERS
|
|
5
|
+
from ..providers.base import Message, Response
|
|
6
|
+
from ..config import get_model_config, load_config
|
|
7
|
+
from .errors import (
|
|
8
|
+
AllModelsFailedError,
|
|
9
|
+
AuthenticationError,
|
|
10
|
+
MalformedResponseError,
|
|
11
|
+
ModelNotFoundError,
|
|
12
|
+
NetworkError,
|
|
13
|
+
NoAPIKeyError,
|
|
14
|
+
ProviderError,
|
|
15
|
+
ProviderTimeoutError,
|
|
16
|
+
QuotaError,
|
|
17
|
+
RateLimitError,
|
|
18
|
+
classify_provider_error,
|
|
19
|
+
)
|
|
20
|
+
from .runtime import plain_enabled
|
|
21
|
+
from .cancellation import OperationCancelled, raise_if_cancelled
|
|
22
|
+
from .tokens import redact_secrets
|
|
23
|
+
|
|
24
|
+
console = Console()
|
|
25
|
+
diagnostic_console = Console(stderr=True)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class FallbackAttempt:
|
|
30
|
+
model: str
|
|
31
|
+
category: str
|
|
32
|
+
detail: str
|
|
33
|
+
next_action: str
|
|
34
|
+
|
|
35
|
+
def summary(self) -> str:
|
|
36
|
+
return f"{self.model} ({self.category})"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _failure_attempt(model: str, provider: str, error: Exception) -> FallbackAttempt:
|
|
40
|
+
normalized = classify_provider_error(provider, error)
|
|
41
|
+
detail = redact_secrets(str(normalized))
|
|
42
|
+
if isinstance(normalized, NoAPIKeyError):
|
|
43
|
+
category = "missing credentials"
|
|
44
|
+
next_action = f"Add credentials with: zai auth login {provider}"
|
|
45
|
+
elif isinstance(normalized, AuthenticationError):
|
|
46
|
+
category = "invalid credentials"
|
|
47
|
+
next_action = f"Update credentials with: zai auth login {provider}"
|
|
48
|
+
elif isinstance(normalized, (RateLimitError, QuotaError)):
|
|
49
|
+
category = "quota/rate limit"
|
|
50
|
+
next_action = "Wait, check quota, or choose another model."
|
|
51
|
+
elif isinstance(normalized, ModelNotFoundError):
|
|
52
|
+
category = "model unavailable"
|
|
53
|
+
next_action = f"Check configuration with: zai model info {model}"
|
|
54
|
+
elif isinstance(normalized, ProviderTimeoutError):
|
|
55
|
+
category = "timeout"
|
|
56
|
+
next_action = f"Check or increase timeout with: zai model configure {model}"
|
|
57
|
+
elif isinstance(normalized, NetworkError):
|
|
58
|
+
category = "network error"
|
|
59
|
+
next_action = "Check internet, local service, or provider status."
|
|
60
|
+
elif isinstance(normalized, MalformedResponseError):
|
|
61
|
+
category = "malformed response"
|
|
62
|
+
next_action = "Retry once, then check provider status or switch models."
|
|
63
|
+
elif isinstance(normalized, ProviderError):
|
|
64
|
+
category = "provider error"
|
|
65
|
+
next_action = f"Inspect this model with: zai model test {model}"
|
|
66
|
+
else:
|
|
67
|
+
category = "unexpected error"
|
|
68
|
+
next_action = "Retry with --debug or run: zai model check"
|
|
69
|
+
return FallbackAttempt(model, category, detail, next_action)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _unavailable_attempt(model: str) -> FallbackAttempt:
|
|
73
|
+
return FallbackAttempt(
|
|
74
|
+
model=model,
|
|
75
|
+
category="unavailable",
|
|
76
|
+
detail="Model is not configured, credentials are missing, or its local service is down.",
|
|
77
|
+
next_action=f"Inspect this model with: zai model test {model}",
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _report_attempt(attempt: FallbackAttempt, *, has_next: bool) -> None:
|
|
82
|
+
suffix = " Trying next model." if has_next else ""
|
|
83
|
+
output = diagnostic_console if plain_enabled() else console
|
|
84
|
+
output.print(
|
|
85
|
+
f"[yellow]{attempt.model} failed:[/yellow] {attempt.category}.{suffix}"
|
|
86
|
+
)
|
|
87
|
+
output.print(f"[dim]{attempt.detail}[/dim]")
|
|
88
|
+
output.print(f"[dim]{attempt.next_action}[/dim]")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _raise_all_models_failed(attempts: list[FallbackAttempt]) -> None:
|
|
92
|
+
summary = "; ".join(attempt.summary() for attempt in attempts)
|
|
93
|
+
guidance = "Run `zai model check` and `zai auth doctor`."
|
|
94
|
+
detail = f"Attempts: {summary}. {guidance}" if summary else guidance
|
|
95
|
+
raise AllModelsFailedError(detail)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def format_model_selection(name: str) -> str:
|
|
99
|
+
try:
|
|
100
|
+
model = get_model_config(name)
|
|
101
|
+
except KeyError:
|
|
102
|
+
return name
|
|
103
|
+
return f"{name} -> {model['provider']}/{model['model_id']}"
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def get_provider(name: str):
|
|
107
|
+
try:
|
|
108
|
+
model = get_model_config(name)
|
|
109
|
+
except KeyError:
|
|
110
|
+
return None
|
|
111
|
+
cls = PROVIDERS.get(model["provider"])
|
|
112
|
+
if not cls:
|
|
113
|
+
return None
|
|
114
|
+
provider = cls()
|
|
115
|
+
provider.model_id = model["model_id"]
|
|
116
|
+
provider.context_window = model["context_window"]
|
|
117
|
+
provider.timeout = model.get("timeout", 60)
|
|
118
|
+
provider.retries = model.get("retries", 2)
|
|
119
|
+
return provider
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _model_order(config: dict, preferred: str = None) -> list[str]:
|
|
123
|
+
first = preferred or config["default_model"]
|
|
124
|
+
if not config.get("auto_fallback", True):
|
|
125
|
+
return [first]
|
|
126
|
+
return [first] + [
|
|
127
|
+
model for model in config["fallback_order"]
|
|
128
|
+
if model != first
|
|
129
|
+
]
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def has_available_provider() -> bool:
|
|
133
|
+
"""Return True when any configured API provider or local provider is usable."""
|
|
134
|
+
config = load_config()
|
|
135
|
+
for model_name in _model_order(config):
|
|
136
|
+
try:
|
|
137
|
+
provider = get_provider(model_name)
|
|
138
|
+
if provider and provider.is_available():
|
|
139
|
+
return True
|
|
140
|
+
except Exception:
|
|
141
|
+
continue
|
|
142
|
+
return False
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def stream_with_fallback(messages: list[Message], system: str = "", preferred: str = None) -> tuple[str, str]:
|
|
146
|
+
"""Stream response with auto fallback. Returns (content, model_name)."""
|
|
147
|
+
config = load_config()
|
|
148
|
+
order = _model_order(config, preferred)
|
|
149
|
+
|
|
150
|
+
attempts: list[FallbackAttempt] = []
|
|
151
|
+
for index, model_name in enumerate(order):
|
|
152
|
+
raise_if_cancelled()
|
|
153
|
+
try:
|
|
154
|
+
provider = get_provider(model_name)
|
|
155
|
+
available = bool(provider and provider.is_available())
|
|
156
|
+
except OperationCancelled:
|
|
157
|
+
raise
|
|
158
|
+
except Exception as error:
|
|
159
|
+
attempt = _failure_attempt(model_name, model_name, error)
|
|
160
|
+
attempts.append(attempt)
|
|
161
|
+
_report_attempt(attempt, has_next=index < len(order) - 1)
|
|
162
|
+
continue
|
|
163
|
+
if not available:
|
|
164
|
+
attempt = _unavailable_attempt(model_name)
|
|
165
|
+
attempts.append(attempt)
|
|
166
|
+
_report_attempt(attempt, has_next=index < len(order) - 1)
|
|
167
|
+
continue
|
|
168
|
+
try:
|
|
169
|
+
from .context import compact_messages, estimate_text_tokens
|
|
170
|
+
|
|
171
|
+
prepared_messages = compact_messages(
|
|
172
|
+
messages,
|
|
173
|
+
getattr(provider, "context_window", 128_000),
|
|
174
|
+
reserve_tokens=max(4096, estimate_text_tokens(system) + 4096),
|
|
175
|
+
)
|
|
176
|
+
if provider.supports_streaming and not plain_enabled():
|
|
177
|
+
content = provider.stream_chat(prepared_messages, system=system)
|
|
178
|
+
else:
|
|
179
|
+
response = provider.chat(prepared_messages, system=system)
|
|
180
|
+
content = response.content
|
|
181
|
+
console.print(content, markup=False)
|
|
182
|
+
raise_if_cancelled()
|
|
183
|
+
return content, model_name
|
|
184
|
+
except OperationCancelled:
|
|
185
|
+
raise
|
|
186
|
+
except Exception as error:
|
|
187
|
+
attempt = _failure_attempt(model_name, provider.name, error)
|
|
188
|
+
attempts.append(attempt)
|
|
189
|
+
_report_attempt(attempt, has_next=index < len(order) - 1)
|
|
190
|
+
continue
|
|
191
|
+
|
|
192
|
+
_raise_all_models_failed(attempts)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def chat_with_fallback(
|
|
196
|
+
messages: list[Message],
|
|
197
|
+
system: str = "",
|
|
198
|
+
preferred: str = None,
|
|
199
|
+
tools: list[dict] | None = None,
|
|
200
|
+
) -> tuple[Response, str]:
|
|
201
|
+
config = load_config()
|
|
202
|
+
order = _model_order(config, preferred)
|
|
203
|
+
|
|
204
|
+
attempts: list[FallbackAttempt] = []
|
|
205
|
+
for index, model_name in enumerate(order):
|
|
206
|
+
raise_if_cancelled()
|
|
207
|
+
try:
|
|
208
|
+
provider = get_provider(model_name)
|
|
209
|
+
available = bool(provider and provider.is_available())
|
|
210
|
+
except OperationCancelled:
|
|
211
|
+
raise
|
|
212
|
+
except Exception as error:
|
|
213
|
+
attempt = _failure_attempt(model_name, model_name, error)
|
|
214
|
+
attempts.append(attempt)
|
|
215
|
+
_report_attempt(attempt, has_next=index < len(order) - 1)
|
|
216
|
+
continue
|
|
217
|
+
if not available:
|
|
218
|
+
attempt = _unavailable_attempt(model_name)
|
|
219
|
+
attempts.append(attempt)
|
|
220
|
+
_report_attempt(attempt, has_next=index < len(order) - 1)
|
|
221
|
+
continue
|
|
222
|
+
try:
|
|
223
|
+
from .context import compact_messages, estimate_text_tokens
|
|
224
|
+
|
|
225
|
+
provider_system = system
|
|
226
|
+
provider_tools = tools if provider.supports_native_tools else None
|
|
227
|
+
if tools and not provider.supports_native_tools:
|
|
228
|
+
from .tool_schema import legacy_tool_instructions
|
|
229
|
+
|
|
230
|
+
provider_system = (
|
|
231
|
+
f"{system}\n\n{legacy_tool_instructions()}"
|
|
232
|
+
)
|
|
233
|
+
tool_schema_tokens = estimate_text_tokens(str(provider_tools or ""))
|
|
234
|
+
prepared_messages = compact_messages(
|
|
235
|
+
messages,
|
|
236
|
+
getattr(provider, "context_window", 128_000),
|
|
237
|
+
reserve_tokens=max(
|
|
238
|
+
4096,
|
|
239
|
+
estimate_text_tokens(provider_system) + tool_schema_tokens + 4096,
|
|
240
|
+
),
|
|
241
|
+
)
|
|
242
|
+
response = provider.chat(
|
|
243
|
+
prepared_messages,
|
|
244
|
+
system=provider_system,
|
|
245
|
+
tools=provider_tools,
|
|
246
|
+
)
|
|
247
|
+
raise_if_cancelled()
|
|
248
|
+
return response, model_name
|
|
249
|
+
except OperationCancelled:
|
|
250
|
+
raise
|
|
251
|
+
except Exception as error:
|
|
252
|
+
attempt = _failure_attempt(model_name, provider.name, error)
|
|
253
|
+
attempts.append(attempt)
|
|
254
|
+
_report_attempt(attempt, has_next=index < len(order) - 1)
|
|
255
|
+
continue
|
|
256
|
+
|
|
257
|
+
_raise_all_models_failed(attempts)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: zai-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: Your personal AI CLI — free, fast, and smart
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/HumaizaNaz/zai_cli
|
|
@@ -618,6 +618,12 @@ Providers without an API key are skipped. Ollama is used only when its local
|
|
|
618
618
|
service is available. Provider quotas and model availability are controlled by
|
|
619
619
|
their respective services and can change independently of `zai`.
|
|
620
620
|
|
|
621
|
+
When an attempted model fails, `zai` reports the model, a stable failure
|
|
622
|
+
category, secret-redacted provider detail, and the recommended recovery action
|
|
623
|
+
before trying the next configured model. If every model fails, the final error
|
|
624
|
+
lists the complete attempt chain. In `--plain` mode these diagnostics go to
|
|
625
|
+
stderr, leaving successful response text on stdout.
|
|
626
|
+
|
|
621
627
|
Supported configuration keys:
|
|
622
628
|
|
|
623
629
|
```text
|
zai_cli-0.2.2/zai/__init__.py
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.2.2"
|
|
@@ -1,171 +0,0 @@
|
|
|
1
|
-
from rich.console import Console
|
|
2
|
-
from ..providers import PROVIDERS
|
|
3
|
-
from ..providers.base import Message, Response
|
|
4
|
-
from ..config import get_model_config, load_config
|
|
5
|
-
from .errors import (
|
|
6
|
-
AllModelsFailedError,
|
|
7
|
-
NetworkError,
|
|
8
|
-
NoAPIKeyError,
|
|
9
|
-
ProviderError,
|
|
10
|
-
RateLimitError,
|
|
11
|
-
classify_provider_error,
|
|
12
|
-
)
|
|
13
|
-
from .runtime import plain_enabled
|
|
14
|
-
from .cancellation import OperationCancelled, raise_if_cancelled
|
|
15
|
-
|
|
16
|
-
console = Console()
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def format_model_selection(name: str) -> str:
|
|
20
|
-
try:
|
|
21
|
-
model = get_model_config(name)
|
|
22
|
-
except KeyError:
|
|
23
|
-
return name
|
|
24
|
-
return f"{name} -> {model['provider']}/{model['model_id']}"
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def get_provider(name: str):
|
|
28
|
-
try:
|
|
29
|
-
model = get_model_config(name)
|
|
30
|
-
except KeyError:
|
|
31
|
-
return None
|
|
32
|
-
cls = PROVIDERS.get(model["provider"])
|
|
33
|
-
if not cls:
|
|
34
|
-
return None
|
|
35
|
-
provider = cls()
|
|
36
|
-
provider.model_id = model["model_id"]
|
|
37
|
-
provider.context_window = model["context_window"]
|
|
38
|
-
provider.timeout = model.get("timeout", 60)
|
|
39
|
-
provider.retries = model.get("retries", 2)
|
|
40
|
-
return provider
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def _model_order(config: dict, preferred: str = None) -> list[str]:
|
|
44
|
-
first = preferred or config["default_model"]
|
|
45
|
-
if not config.get("auto_fallback", True):
|
|
46
|
-
return [first]
|
|
47
|
-
return [first] + [
|
|
48
|
-
model for model in config["fallback_order"]
|
|
49
|
-
if model != first
|
|
50
|
-
]
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def has_available_provider() -> bool:
|
|
54
|
-
"""Return True when any configured API provider or local provider is usable."""
|
|
55
|
-
config = load_config()
|
|
56
|
-
for model_name in _model_order(config):
|
|
57
|
-
provider = get_provider(model_name)
|
|
58
|
-
if provider and provider.is_available():
|
|
59
|
-
return True
|
|
60
|
-
return False
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
def stream_with_fallback(messages: list[Message], system: str = "", preferred: str = None) -> tuple[str, str]:
|
|
64
|
-
"""Stream response with auto fallback. Returns (content, model_name)."""
|
|
65
|
-
config = load_config()
|
|
66
|
-
order = _model_order(config, preferred)
|
|
67
|
-
|
|
68
|
-
last_error = None
|
|
69
|
-
for model_name in order:
|
|
70
|
-
raise_if_cancelled()
|
|
71
|
-
provider = get_provider(model_name)
|
|
72
|
-
if not provider or not provider.is_available():
|
|
73
|
-
continue
|
|
74
|
-
try:
|
|
75
|
-
from .context import compact_messages, estimate_text_tokens
|
|
76
|
-
|
|
77
|
-
prepared_messages = compact_messages(
|
|
78
|
-
messages,
|
|
79
|
-
getattr(provider, "context_window", 128_000),
|
|
80
|
-
reserve_tokens=max(4096, estimate_text_tokens(system) + 4096),
|
|
81
|
-
)
|
|
82
|
-
if provider.supports_streaming and not plain_enabled():
|
|
83
|
-
content = provider.stream_chat(prepared_messages, system=system)
|
|
84
|
-
else:
|
|
85
|
-
response = provider.chat(prepared_messages, system=system)
|
|
86
|
-
content = response.content
|
|
87
|
-
console.print(content, markup=False)
|
|
88
|
-
raise_if_cancelled()
|
|
89
|
-
return content, model_name
|
|
90
|
-
except OperationCancelled:
|
|
91
|
-
raise
|
|
92
|
-
except RateLimitError as error:
|
|
93
|
-
last_error = str(error)
|
|
94
|
-
console.print(f"[yellow]⚡ {model_name} limit hit — switching...[/yellow]")
|
|
95
|
-
continue
|
|
96
|
-
except NoAPIKeyError:
|
|
97
|
-
continue
|
|
98
|
-
except (NetworkError, ProviderError) as error:
|
|
99
|
-
last_error = str(error)
|
|
100
|
-
console.print(f"[yellow]Network issue with {model_name} — switching...[/yellow]")
|
|
101
|
-
continue
|
|
102
|
-
except Exception as error:
|
|
103
|
-
normalized = classify_provider_error(provider.name, error)
|
|
104
|
-
console.print(f"[dim]Error with {model_name}: {normalized}[/dim]")
|
|
105
|
-
last_error = str(normalized)
|
|
106
|
-
continue
|
|
107
|
-
|
|
108
|
-
raise AllModelsFailedError(last_error or "")
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
def chat_with_fallback(
|
|
112
|
-
messages: list[Message],
|
|
113
|
-
system: str = "",
|
|
114
|
-
preferred: str = None,
|
|
115
|
-
tools: list[dict] | None = None,
|
|
116
|
-
) -> tuple[Response, str]:
|
|
117
|
-
config = load_config()
|
|
118
|
-
order = _model_order(config, preferred)
|
|
119
|
-
|
|
120
|
-
last_error = None
|
|
121
|
-
for model_name in order:
|
|
122
|
-
raise_if_cancelled()
|
|
123
|
-
provider = get_provider(model_name)
|
|
124
|
-
if not provider or not provider.is_available():
|
|
125
|
-
continue
|
|
126
|
-
try:
|
|
127
|
-
from .context import compact_messages, estimate_text_tokens
|
|
128
|
-
|
|
129
|
-
provider_system = system
|
|
130
|
-
provider_tools = tools if provider.supports_native_tools else None
|
|
131
|
-
if tools and not provider.supports_native_tools:
|
|
132
|
-
from .tool_schema import legacy_tool_instructions
|
|
133
|
-
|
|
134
|
-
provider_system = (
|
|
135
|
-
f"{system}\n\n{legacy_tool_instructions()}"
|
|
136
|
-
)
|
|
137
|
-
tool_schema_tokens = estimate_text_tokens(str(provider_tools or ""))
|
|
138
|
-
prepared_messages = compact_messages(
|
|
139
|
-
messages,
|
|
140
|
-
getattr(provider, "context_window", 128_000),
|
|
141
|
-
reserve_tokens=max(
|
|
142
|
-
4096,
|
|
143
|
-
estimate_text_tokens(provider_system) + tool_schema_tokens + 4096,
|
|
144
|
-
),
|
|
145
|
-
)
|
|
146
|
-
response = provider.chat(
|
|
147
|
-
prepared_messages,
|
|
148
|
-
system=provider_system,
|
|
149
|
-
tools=provider_tools,
|
|
150
|
-
)
|
|
151
|
-
raise_if_cancelled()
|
|
152
|
-
return response, model_name
|
|
153
|
-
except OperationCancelled:
|
|
154
|
-
raise
|
|
155
|
-
except RateLimitError as error:
|
|
156
|
-
console.print(f"[yellow]⚡ {model_name} limit hit — switching...[/yellow]")
|
|
157
|
-
last_error = str(error)
|
|
158
|
-
continue
|
|
159
|
-
except NoAPIKeyError:
|
|
160
|
-
continue
|
|
161
|
-
except (NetworkError, ProviderError) as error:
|
|
162
|
-
console.print(f"[yellow]Network issue with {model_name} — switching...[/yellow]")
|
|
163
|
-
last_error = str(error)
|
|
164
|
-
continue
|
|
165
|
-
except Exception as error:
|
|
166
|
-
normalized = classify_provider_error(provider.name, error)
|
|
167
|
-
console.print(f"[dim]Error with {model_name}: {normalized}[/dim]")
|
|
168
|
-
last_error = str(normalized)
|
|
169
|
-
continue
|
|
170
|
-
|
|
171
|
-
raise AllModelsFailedError(last_error or "")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|