diff --git a/pyproject.toml b/pyproject.toml index 099e9ee8..1f244ee9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,6 +21,8 @@ dependencies = [ "opentelemetry-distro>=0.48b0", "opentelemetry-exporter-otlp>=1.27.0", "agent-client-protocol>=0.9.0", + "cryptography>=42.0", + "keyring>=25.0", ] [project.optional-dependencies] diff --git a/src/iac_code/__init__.py b/src/iac_code/__init__.py index cf736a9b..749db218 100644 --- a/src/iac_code/__init__.py +++ b/src/iac_code/__init__.py @@ -1,2 +1,2 @@ -__version__ = "0.1.1" +__version__ = "0.1.2" __release_date__ = "" diff --git a/src/iac_code/cli/main.py b/src/iac_code/cli/main.py index caebeb60..a797ff2d 100644 --- a/src/iac_code/cli/main.py +++ b/src/iac_code/cli/main.py @@ -11,6 +11,7 @@ from iac_code import __release_date__, __version__ from iac_code.config import DEFAULT_MODEL, load_saved_model from iac_code.i18n import _, setup_i18n +from iac_code.services.qwenpaw_source import QwenPawError as _QwenPawError from iac_code.utils.log import setup_logging # Initialize i18n. Thanks to `gettext.bindtextdomain` inside setup_i18n(), @@ -137,8 +138,12 @@ async def _run_with_handler(coro): from iac_code.cli.output_formats import OutputFormat fmt = OutputFormat(output_format) - runner = HeadlessRunner(model=model, output_format=fmt, max_turns=max_turns) - exit_code = asyncio.run(_run_with_handler(runner.run(prompt))) + try: + runner = HeadlessRunner(model=model, output_format=fmt, max_turns=max_turns) + exit_code = asyncio.run(_run_with_handler(runner.run(prompt))) + except _QwenPawError as exc: + typer.echo(str(exc), err=True) + raise SystemExit(1) raise SystemExit(exit_code) else: @@ -162,7 +167,7 @@ async def _run_with_handler(coro): try: repl = InlineREPL(model=model, resume_session_id=resume_arg) - except ValueError as exc: + except (ValueError, _QwenPawError) as exc: typer.echo(str(exc), err=True) raise typer.Exit(1) diff --git a/src/iac_code/commands/auth.py b/src/iac_code/commands/auth.py index 40516d40..82765b6f 100644 --- a/src/iac_code/commands/auth.py +++ b/src/iac_code/commands/auth.py @@ -50,6 +50,7 @@ class LLMProvider(TypedDict): api_base: str | None models: list[str] default_model: str + require_api_key: bool def _classify_base_url(url: str | None) -> str: @@ -66,87 +67,27 @@ def _classify_base_url(url: str | None) -> str: return "other" -# Provider definitions -PROVIDERS: list[LLMProvider] = [ - { - "name": "DashScope", - "display_name": "阿里云百炼", - "key_name": "dashscope", - "api_base": "https://dashscope.aliyuncs.com/compatible-mode/v1", - "models": [ - "qwen3.6-max-preview", - "qwen3.6-plus", - "qwen3.5-plus", - "qwen3.5-flash", - "qwq-plus", - "kimi-k2.6", - "deepseek-v4-pro", - "deepseek-v4-flash", - "glm-5.1", - ], - "default_model": "qwen3.6-plus", - }, - { - "name": "DashScope Token Plan", - "display_name": "阿里云百炼 Token Plan", - "key_name": "dashscope_token_plan", - "api_base": "https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1", - "models": [ - "qwen3.6-plus", - "deepseek-v3.2", - "glm-5", - "MiniMax-M2.5", - ], - "default_model": "qwen3.6-plus", - }, - { - "name": "OpenAI", - "display_name": "OpenAI", - "key_name": "openai", - "api_base": None, - "models": [ - "gpt-5.5", - "gpt-5.4", - "gpt-5.4-mini", - "gpt-5.3-codex", - "gpt-5.2", - ], - "default_model": "gpt-5.5", - }, - { - "name": "Anthropic", - "display_name": "Anthropic", - "key_name": "anthropic", - "api_base": None, - "models": [ - "claude-opus-4-7", - "claude-opus-4-6", - "claude-sonnet-4-6", - "claude-sonnet-4-6-1m", - "claude-haiku-4-5-20251001", - ], - "default_model": "claude-opus-4-7", - }, - { - "name": "DeepSeek", - "display_name": "DeepSeek", - "key_name": "deepseek", - "api_base": "https://api.deepseek.com/v1", - "models": [ - "deepseek-v4-pro", - "deepseek-v4-flash", - ], - "default_model": "deepseek-v4-pro", - }, - { - "name": "OpenAPI Compatible", - "display_name": "OpenAPI 兼容", - "key_name": "openapi_compatible", - "api_base": None, - "models": [], - "default_model": "", - }, -] +def _build_providers_from_registry() -> list[LLMProvider]: + """Build PROVIDERS list from the central registry.""" + from iac_code.providers.registry import PROVIDER_REGISTRY + + result: list[LLMProvider] = [] + for desc in PROVIDER_REGISTRY.values(): + result.append( + LLMProvider( + name=desc.name, + display_name=_(desc.display_name), + key_name=desc.key, + api_base=desc.base_url, + models=desc.model_ids, + default_model=desc.default_model, + require_api_key=desc.require_api_key, + ) + ) + return result + + +PROVIDERS: list[LLMProvider] = _build_providers_from_registry() # ── ANSI helpers ────────────────────────────────────────────────────── _C_SEL = "\033[96m" # bright cyan (selected) @@ -655,33 +596,74 @@ def _get_active_key_name() -> str: def _llm_auth_flow(console, store) -> str | None | _BackSentinel: - """LLM provider auth flow.""" + """LLM provider auth flow with two-step vendor group selection.""" active_key_name = _get_active_key_name() + provider_groups: list[tuple[str, list[str]]] = [ + ( + "Alibaba Cloud", + ["dashscope", "dashscope_token_plan", "aliyun_codingplan", "aliyun_codingplan_intl", "modelscope"], + ), + ("ZhiPu AI", ["zhipu_cn", "zhipu_intl", "zhipu_cn_codingplan", "zhipu_intl_codingplan"]), + ("Kimi", ["kimi_cn", "kimi_intl"]), + ("MiniMax", ["minimax_cn", "minimax_intl"]), + ("Volcengine", ["volcengine_cn", "volcengine_cn_codingplan"]), + ("SiliconFlow", ["siliconflow_cn", "siliconflow_intl"]), + ("DeepSeek", ["deepseek"]), + ("OpenAI", ["openai"]), + ("Anthropic", ["anthropic"]), + ("Google Gemini", ["gemini"]), + ("Azure OpenAI", ["azure_openai"]), + ("OpenRouter", ["openrouter"]), + ("Local", ["ollama", "lmstudio"]), + ("Compatible", ["openapi_compatible", "anthropic_compatible"]), + ] + + provider_map: dict[str, LLMProvider] = {str(p["key_name"]): p for p in PROVIDERS} + while True: - # Step 1: Select provider - provider_options = [] - for p in PROVIDERS: - label = str(p["display_name"]) - if str(p["key_name"]) == active_key_name: + # Step 1: Select vendor group + group_options: list[str] = [] + group_default_idx = 0 + for i, (group_name, keys) in enumerate(provider_groups): + label = _(group_name) + if active_key_name in keys: label += _(" (current)") - provider_options.append(label) - - default_idx = 0 - for i, p in enumerate(PROVIDERS): - if str(p["key_name"]) == active_key_name: - default_idx = i - break + group_default_idx = i + group_options.append(label) - idx = _select(_("Select provider"), provider_options, default_index=default_idx) - if idx is None: + group_idx = _select(_("Select provider"), group_options, default_index=group_default_idx) + if group_idx is None: return _BACK - provider = PROVIDERS[idx] + group_name, group_keys = provider_groups[group_idx] + group_providers = [provider_map[k] for k in group_keys if k in provider_map] - # Step 2 (OpenAPI Compatible only): API Base URL + # Step 2: Select provider within group (skip if only one) + if len(group_providers) == 1: + provider = group_providers[0] + else: + sub_options: list[str] = [] + sub_default_idx = 0 + for i, p in enumerate(group_providers): + label = str(p["display_name"]) + if str(p["key_name"]) == active_key_name: + label += _(" (current)") + sub_default_idx = i + sub_options.append(label) + + sub_idx = _select( + _("Select provider — {group}").format(group=_(group_name)), + sub_options, + default_index=sub_default_idx, + ) + if sub_idx is None: + continue + provider = group_providers[sub_idx] + + # Step 3 (Compatible providers): API Base URL user_api_base = None - if provider["key_name"] == "openapi_compatible": + if provider["key_name"] in ("openapi_compatible", "anthropic_compatible"): existing_api_base = _load_existing_api_base(str(provider["key_name"])) api_base_result = _input_text_with_default( _("Configure {provider}").format(provider=provider["display_name"]), @@ -696,24 +678,24 @@ def _llm_auth_flow(console, store) -> str | None | _BackSentinel: if not user_api_base: continue - # Step 3: API key - existing_key = _load_existing_key(str(provider["key_name"])) - api_key = _input_masked( - _("Enter API key for {provider}").format(provider=provider["display_name"]), - "API key: ", - existing=existing_key, - ) - if api_key is _BACK: - continue - if api_key is None or not str(api_key).strip(): - return _("Auth cancelled") + # Step 4: API key (skip for local providers that don't require one) + if provider.get("require_api_key", True): + existing_key = _load_existing_key(str(provider["key_name"])) + api_key = _input_masked( + _("Enter API key for {provider}").format(provider=provider["display_name"]), + "API key: ", + existing=existing_key, + ) + if api_key is _BACK: + continue + if api_key is None or not str(api_key).strip(): + return _("Auth cancelled") - api_key = str(api_key).strip() - if api_key != existing_key: - save_llm_key(str(provider["key_name"]), api_key) + api_key = str(api_key).strip() + if api_key != existing_key: + save_llm_key(str(provider["key_name"]), api_key) - # Step 4: Select model — recall this provider's own last-used model, - # so switching providers never leaks another provider's custom model. + # Step 5: Select model current_model = _load_existing_model(str(provider["key_name"])) or "" selected = select_model_interactive( list(provider["models"]), @@ -726,7 +708,6 @@ def _llm_auth_flow(console, store) -> str | None | _BackSentinel: selected_model = str(selected) save_active_provider_config(provider, selected_model, api_base=user_api_base) - # Log telemetry event log_event( Events.AUTH_CONFIGURED, { @@ -746,6 +727,25 @@ def _llm_auth_flow(console, store) -> str | None | _BackSentinel: ) +_GROUP_NAME_MARKERS = [ + _("Alibaba Cloud"), + _("ZhiPu AI"), + _("Kimi"), + _("MiniMax"), + _("Volcengine"), + _("SiliconFlow"), + _("DeepSeek"), + _("OpenAI"), + _("Anthropic"), + _("Google Gemini"), + _("Azure OpenAI"), + _("OpenRouter"), + _("Local"), + _("Compatible"), + _("Select provider — {group}"), +] + + def _cloud_provider_display(name: str) -> str: """Get translated display name for a cloud provider.""" names = { diff --git a/src/iac_code/config.py b/src/iac_code/config.py index 342681b1..cdc6d621 100644 --- a/src/iac_code/config.py +++ b/src/iac_code/config.py @@ -50,36 +50,38 @@ def _save_yaml(path: Path, data: dict[str, Any]) -> None: # Provider name normalization (env-facing PascalCase ↔ internal key_name) # --------------------------------------------------------------------------- -# Canonical PascalCase display values for IAC_CODE_PROVIDER. -# Order is the order shown in error messages. -_PROVIDER_CANONICAL_NAMES: tuple[str, ...] = ( - "Anthropic", - "OpenAI", - "DashScope", - "DashScopeTokenPlan", - "DeepSeek", - "OpenAPICompatible", -) -# Lowercased canonical name -> internal key_name (matches settings.yml `keyName`). -_PROVIDER_NAME_TO_KEY: dict[str, str] = { - "anthropic": "anthropic", - "openai": "openai", - "dashscope": "dashscope", - "dashscopetokenplan": "dashscope_token_plan", - "deepseek": "deepseek", - "openapicompatible": "openapi_compatible", -} +def _build_provider_name_to_key() -> dict[str, str]: + from iac_code.providers.registry import PROVIDER_REGISTRY -# key_name -> credentials.yml slot. After the rename, slots match key_name 1:1. -_KEY_NAME_TO_CRED_SLOT: dict[str, str] = { - "anthropic": "anthropic", - "openai": "openai", - "dashscope": "dashscope", - "dashscope_token_plan": "dashscope_token_plan", - "deepseek": "deepseek", - "openapi_compatible": "openapi_compatible", -} + mapping: dict[str, str] = {} + for desc in PROVIDER_REGISTRY.values(): + normalized = desc.name.lower().replace(" ", "").replace("-", "").replace("_", "") + mapping[normalized] = desc.key + key_norm = desc.key.lower().replace("_", "").replace("-", "") + mapping[key_norm] = desc.key + return mapping + + +_PROVIDER_NAME_TO_KEY: dict[str, str] = _build_provider_name_to_key() + + +def _build_key_name_to_cred_slot() -> dict[str, str]: + from iac_code.providers.registry import PROVIDER_REGISTRY + + return {key: key for key in PROVIDER_REGISTRY} + + +_KEY_NAME_TO_CRED_SLOT: dict[str, str] = _build_key_name_to_cred_slot() + + +def _build_canonical_names() -> tuple[str, ...]: + from iac_code.providers.registry import PROVIDER_REGISTRY + + return tuple(desc.name for desc in PROVIDER_REGISTRY.values()) + + +_PROVIDER_CANONICAL_NAMES: tuple[str, ...] = _build_canonical_names() # Legacy key_name aliases accepted when reading settings.yml (write path always uses # the canonical key on the right). Keep DashScope's old "bailian" name readable. @@ -98,6 +100,11 @@ def _save_yaml(path: Path, data: dict[str, Any]) -> None: ("o4-", "openai"), ("qwen", "dashscope"), ("deepseek-", "deepseek"), + ("gemini-", "gemini"), + ("glm-", "zhipu_cn"), + ("kimi-", "kimi_cn"), + ("minimax-", "minimax_cn"), + ("doubao-", "volcengine_cn"), ) # Module-level flag — warn once per process when IAC_CODE_BASE_URL is set @@ -144,6 +151,21 @@ def _read(name: str) -> str | None: } +def get_llm_source() -> str: + """Return the LLM source: 'qwenpaw', 'local', or 'env'.""" + env = _get_env_overrides() + if env["api_key"]: + return "env" + try: + settings = _load_yaml(get_settings_path()) + except Exception: + return "local" + source = settings.get("llm_source") + if isinstance(source, str) and source.strip(): + return source.strip() + return "local" + + # --------------------------------------------------------------------------- # Path helpers # --------------------------------------------------------------------------- @@ -308,14 +330,13 @@ def load_credentials(model: str | None = None) -> dict[str, str]: if not isinstance(raw, dict): raw = {} - creds: dict[str, str] = { - "anthropic": str(raw.get("anthropic", "") or ""), - "openai": str(raw.get("openai", "") or ""), - "dashscope": str(raw.get("dashscope", "") or raw.get("bailian", "") or ""), - "dashscope_token_plan": str(raw.get("dashscope_token_plan", "") or ""), - "deepseek": str(raw.get("deepseek", "") or ""), - "openapi_compatible": str(raw.get("openapi_compatible", "") or ""), - } + from iac_code.providers.registry import PROVIDER_REGISTRY + + creds: dict[str, str] = {} + for key in PROVIDER_REGISTRY: + creds[key] = str(raw.get(key, "") or "") + if not creds.get("dashscope"): + creds["dashscope"] = str(raw.get("bailian", "") or "") env = _get_env_overrides() if env["api_key"]: diff --git a/src/iac_code/i18n/locales/de/LC_MESSAGES/messages.po b/src/iac_code/i18n/locales/de/LC_MESSAGES/messages.po index 338f0cc9..b82e2c85 100644 --- a/src/iac_code/i18n/locales/de/LC_MESSAGES/messages.po +++ b/src/iac_code/i18n/locales/de/LC_MESSAGES/messages.po @@ -4,9 +4,9 @@ # msgid "" msgstr "" -"Project-Id-Version: iac-code 0.1.1\n" +"Project-Id-Version: iac-code 0.1.2\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-05-15 11:19+0800\n" +"POT-Creation-Date: 2026-05-15 17:58+0800\n" "PO-Revision-Date: 2026-05-13 00:00+0000\n" "Last-Translator: \n" "Language: de\n" @@ -131,47 +131,47 @@ msgstr "" " Dokumentation: https://aliyun.github.io/iac-" "code/de/docs/configuration/authentication\n" -#: src/iac_code/cli/main.py:22 src/iac_code/commands/help.py:26 +#: src/iac_code/cli/main.py:23 src/iac_code/commands/help.py:26 msgid "AI-powered infrastructure orchestration tool" msgstr "KI-gestütztes Tool zur Orchestrierung von Infrastruktur" -#: src/iac_code/cli/main.py:33 +#: src/iac_code/cli/main.py:34 msgid "LLM model to use" msgstr "Zu verwendendes LLM-Modell" -#: src/iac_code/cli/main.py:34 +#: src/iac_code/cli/main.py:35 msgid "Non-interactive mode: run a single prompt and exit" msgstr "Nicht-interaktiver Modus: Einen Prompt ausführen und beenden" -#: src/iac_code/cli/main.py:35 +#: src/iac_code/cli/main.py:36 msgid "Output format: text, json, stream-json" msgstr "Ausgabeformat: text, json, stream-json" -#: src/iac_code/cli/main.py:36 +#: src/iac_code/cli/main.py:37 msgid "Maximum agent turns in headless mode" msgstr "Maximale Agent-Runden im Headless-Modus" -#: src/iac_code/cli/main.py:37 src/iac_code/cli/main.py:236 +#: src/iac_code/cli/main.py:38 src/iac_code/cli/main.py:241 msgid "Enable debug logging" msgstr "Debug-Protokollierung aktivieren" -#: src/iac_code/cli/main.py:38 +#: src/iac_code/cli/main.py:39 msgid "Show version and exit" msgstr "Version anzeigen und beenden" -#: src/iac_code/cli/main.py:39 +#: src/iac_code/cli/main.py:40 msgid "Resume a session by ID" msgstr "Eine Sitzung anhand der ID fortsetzen" -#: src/iac_code/cli/main.py:40 +#: src/iac_code/cli/main.py:41 msgid "Resume the most recent session" msgstr "Die zuletzt genutzte Sitzung fortsetzen" -#: src/iac_code/cli/main.py:47 src/iac_code/i18n/__init__.py:54 +#: src/iac_code/cli/main.py:48 src/iac_code/i18n/__init__.py:54 msgid "Install completion for the current shell." msgstr "Vervollständigung für die aktuelle Shell installieren." -#: src/iac_code/cli/main.py:55 src/iac_code/i18n/__init__.py:55 +#: src/iac_code/cli/main.py:56 src/iac_code/i18n/__init__.py:55 msgid "" "Show completion for the current shell, to copy it or customize the " "installation." @@ -179,23 +179,23 @@ msgstr "" "Vervollständigung für die aktuelle Shell anzeigen, zum Kopieren oder " "Anpassen der Installation." -#: src/iac_code/cli/main.py:69 +#: src/iac_code/cli/main.py:70 msgid "Error: --resume and --continue cannot be used together." msgstr "Fehler: --resume und --continue können nicht gemeinsam verwendet werden." -#: src/iac_code/cli/main.py:231 +#: src/iac_code/cli/main.py:236 msgid "Run iac-code as an ACP server." msgstr "iac-code als ACP-Server ausführen." -#: src/iac_code/cli/main.py:233 +#: src/iac_code/cli/main.py:238 msgid "Transport type: stdio or http" msgstr "Transporttyp: stdio oder http" -#: src/iac_code/cli/main.py:234 +#: src/iac_code/cli/main.py:239 msgid "HTTP server port" msgstr "HTTP-Server-Port" -#: src/iac_code/cli/main.py:235 +#: src/iac_code/cli/main.py:240 msgid "HTTP server host" msgstr "HTTP-Server-Host" @@ -243,104 +243,162 @@ msgstr "Eine frühere Sitzung fortsetzen" msgid "[conversation id or search term]" msgstr "[Konversations-ID oder Suchbegriff]" -#: src/iac_code/commands/auth.py:304 src/iac_code/commands/auth.py:817 +#: src/iac_code/commands/auth.py:245 src/iac_code/commands/auth.py:817 #: src/iac_code/ui/core/prompt_input.py:373 msgid "Navigate" msgstr "Navigieren" -#: src/iac_code/commands/auth.py:304 src/iac_code/commands/auth.py:426 -#: src/iac_code/commands/auth.py:460 src/iac_code/commands/auth.py:467 -#: src/iac_code/commands/auth.py:491 src/iac_code/commands/auth.py:817 +#: src/iac_code/commands/auth.py:245 src/iac_code/commands/auth.py:367 +#: src/iac_code/commands/auth.py:401 src/iac_code/commands/auth.py:408 +#: src/iac_code/commands/auth.py:432 src/iac_code/commands/auth.py:817 #: src/iac_code/commands/auth.py:1007 src/iac_code/ui/core/prompt_input.py:373 msgid "Confirm" msgstr "Bestätigen" -#: src/iac_code/commands/auth.py:304 src/iac_code/commands/auth.py:424 -#: src/iac_code/commands/auth.py:426 src/iac_code/commands/auth.py:460 -#: src/iac_code/commands/auth.py:467 src/iac_code/commands/auth.py:491 +#: src/iac_code/commands/auth.py:245 src/iac_code/commands/auth.py:365 +#: src/iac_code/commands/auth.py:367 src/iac_code/commands/auth.py:401 +#: src/iac_code/commands/auth.py:408 src/iac_code/commands/auth.py:432 #: src/iac_code/commands/auth.py:817 src/iac_code/commands/auth.py:902 #: src/iac_code/commands/auth.py:1007 msgid "Back" msgstr "Zurück" -#: src/iac_code/commands/auth.py:424 +#: src/iac_code/commands/auth.py:365 msgid "Keep" msgstr "Behalten" -#: src/iac_code/commands/auth.py:424 +#: src/iac_code/commands/auth.py:365 msgid "Re-enter" msgstr "Erneut eingeben" -#: src/iac_code/commands/auth.py:566 src/iac_code/commands/auth.py:667 +#: src/iac_code/commands/auth.py:507 src/iac_code/commands/auth.py:631 +#: src/iac_code/commands/auth.py:651 msgid " (current)" msgstr " (aktuell)" -#: src/iac_code/commands/auth.py:569 +#: src/iac_code/commands/auth.py:510 msgid "Custom model..." msgstr "Benutzerdefiniertes Modell …" -#: src/iac_code/commands/auth.py:572 +#: src/iac_code/commands/auth.py:513 #, python-brace-format msgid "Select model for {provider}" msgstr "Modell für {provider} auswählen" -#: src/iac_code/commands/auth.py:574 +#: src/iac_code/commands/auth.py:515 msgid "Select model" msgstr "Modell auswählen" -#: src/iac_code/commands/auth.py:582 +#: src/iac_code/commands/auth.py:523 msgid "Enter custom model name: " msgstr "Benutzerdefinierten Modellnamen eingeben: " -#: src/iac_code/commands/auth.py:608 +#: src/iac_code/commands/auth.py:549 msgid "Error: console not available" msgstr "Fehler: Konsole nicht verfügbar" -#: src/iac_code/commands/auth.py:635 +#: src/iac_code/commands/auth.py:576 msgid "Configure LLM Provider" msgstr "LLM-Anbieter konfigurieren" -#: src/iac_code/commands/auth.py:636 +#: src/iac_code/commands/auth.py:577 msgid "Configure IaC Cloud Service" msgstr "IaC-Cloud-Dienst konfigurieren" -#: src/iac_code/commands/auth.py:638 +#: src/iac_code/commands/auth.py:579 msgid "Select configuration type" msgstr "Konfigurationstyp auswählen" -#: src/iac_code/commands/auth.py:640 src/iac_code/commands/auth.py:694 -#: src/iac_code/commands/auth.py:709 src/iac_code/commands/auth.py:770 +#: src/iac_code/commands/auth.py:581 src/iac_code/commands/auth.py:676 +#: src/iac_code/commands/auth.py:692 src/iac_code/commands/auth.py:770 #: src/iac_code/commands/auth.py:943 src/iac_code/commands/auth.py:984 msgid "Auth cancelled" msgstr "Authentifizierung abgebrochen" -#: src/iac_code/commands/auth.py:676 +#: src/iac_code/commands/auth.py:635 msgid "Select provider" msgstr "Anbieter auswählen" -#: src/iac_code/commands/auth.py:687 +#: src/iac_code/commands/auth.py:656 src/iac_code/commands/auth.py:745 +#, python-brace-format +msgid "Select provider — {group}" +msgstr "Anbieter auswählen — {group}" + +#: src/iac_code/commands/auth.py:669 #, python-brace-format msgid "Configure {provider}" msgstr "{provider} konfigurieren" -#: src/iac_code/commands/auth.py:702 +#: src/iac_code/commands/auth.py:685 #, python-brace-format msgid "Enter API key for {provider}" msgstr "API-Key für {provider} eingeben" -#: src/iac_code/commands/auth.py:742 +#: src/iac_code/commands/auth.py:723 #, python-brace-format msgid "{status}: {provider} / {model}" msgstr "{status}: {provider} / {model}" -#: src/iac_code/commands/auth.py:743 +#: src/iac_code/commands/auth.py:724 msgid "Configured" msgstr "Konfiguriert" -#: src/iac_code/commands/auth.py:752 +#: src/iac_code/commands/auth.py:731 src/iac_code/commands/auth.py:752 msgid "Alibaba Cloud" msgstr "Alibaba Cloud" +#: src/iac_code/commands/auth.py:732 src/iac_code/providers/registry.py:417 +msgid "ZhiPu AI" +msgstr "ZhiPu AI" + +#: src/iac_code/commands/auth.py:733 +msgid "Kimi" +msgstr "Kimi" + +#: src/iac_code/commands/auth.py:734 +msgid "MiniMax" +msgstr "MiniMax" + +#: src/iac_code/commands/auth.py:735 src/iac_code/providers/registry.py:419 +msgid "Volcengine" +msgstr "Volcengine" + +#: src/iac_code/commands/auth.py:736 +msgid "SiliconFlow" +msgstr "SiliconFlow" + +#: src/iac_code/commands/auth.py:737 src/iac_code/providers/registry.py:410 +msgid "DeepSeek" +msgstr "DeepSeek" + +#: src/iac_code/commands/auth.py:738 src/iac_code/providers/registry.py:408 +msgid "OpenAI" +msgstr "OpenAI" + +#: src/iac_code/commands/auth.py:739 src/iac_code/providers/registry.py:409 +msgid "Anthropic" +msgstr "Anthropic" + +#: src/iac_code/commands/auth.py:740 src/iac_code/providers/registry.py:412 +msgid "Google Gemini" +msgstr "Google Gemini" + +#: src/iac_code/commands/auth.py:741 src/iac_code/providers/registry.py:425 +msgid "Azure OpenAI" +msgstr "Azure OpenAI" + +#: src/iac_code/commands/auth.py:742 src/iac_code/providers/registry.py:424 +msgid "OpenRouter" +msgstr "OpenRouter" + +#: src/iac_code/commands/auth.py:743 +msgid "Local" +msgstr "Lokal" + +#: src/iac_code/commands/auth.py:744 +msgid "Compatible" +msgstr "Kompatibel" + #: src/iac_code/commands/auth.py:761 msgid "Select Cloud Provider" msgstr "Cloud-Anbieter auswählen" @@ -557,14 +615,21 @@ msgstr "(dynamisch)" msgid "Aborted!" msgstr "Abgebrochen!" -#: src/iac_code/providers/manager.py:80 +#: src/iac_code/providers/manager.py:78 #, python-brace-format msgid "Cannot determine provider for model: {model}. Run /auth to configure." msgstr "" "Anbieter für Modell {model} kann nicht ermittelt werden. Führen Sie /auth" " aus, um die Konfiguration vorzunehmen." -#: src/iac_code/providers/manager.py:88 +#: src/iac_code/providers/manager.py:95 +#, python-brace-format +msgid "Unknown provider key: '{key}'. Run /auth to configure." +msgstr "" +"Unbekannter Anbieterschlüssel: '{key}'. Führen Sie /auth aus, um die " +"Konfiguration vorzunehmen." + +#: src/iac_code/providers/manager.py:100 #, python-brace-format msgid "" "No API key configured for provider '{provider}' (model: {model}). Run " @@ -573,7 +638,7 @@ msgstr "" "Kein API-Schlüssel für Anbieter '{provider}' konfiguriert (Modell: " "{model}). Führen Sie /auth aus, um die Konfiguration vorzunehmen." -#: src/iac_code/providers/openai_provider.py:286 +#: src/iac_code/providers/openai_provider.py:289 #, python-brace-format msgid "" "API returned no data. Please check that your API Base URL is correct " @@ -584,7 +649,7 @@ msgstr "" "korrekt ist (aktuell: {base_url}). Viele OpenAI-kompatible Endpunkte " "erfordern ein /v1-Suffix (z. B. {base_url}/v1)." -#: src/iac_code/providers/openai_provider.py:327 +#: src/iac_code/providers/openai_provider.py:330 #, python-brace-format msgid "" "API returned an invalid response. Please check that your API Base URL is " @@ -595,6 +660,98 @@ msgstr "" " Base URL korrekt ist (aktuell: {base_url}). Viele OpenAI-kompatible " "Endpunkte erfordern ein /v1-Suffix (z. B. {base_url}/v1)." +#: src/iac_code/providers/registry.py:406 +msgid "Alibaba Cloud Bailian" +msgstr "Alibaba Cloud Bailian" + +#: src/iac_code/providers/registry.py:407 +msgid "Alibaba Cloud Bailian Token Plan" +msgstr "Alibaba Cloud Bailian Token Plan" + +#: src/iac_code/providers/registry.py:411 +msgid "OpenAPI Compatible" +msgstr "OpenAPI-kompatibel" + +#: src/iac_code/providers/registry.py:413 +msgid "Kimi (China)" +msgstr "Kimi (China)" + +#: src/iac_code/providers/registry.py:414 +msgid "Kimi (International)" +msgstr "Kimi (International)" + +#: src/iac_code/providers/registry.py:415 +msgid "MiniMax (China)" +msgstr "MiniMax (China)" + +#: src/iac_code/providers/registry.py:416 +msgid "MiniMax (International)" +msgstr "MiniMax (International)" + +#: src/iac_code/providers/registry.py:418 +msgid "ZhiPu AI (International)" +msgstr "ZhiPu AI (International)" + +#: src/iac_code/providers/registry.py:420 +msgid "SiliconFlow (China)" +msgstr "SiliconFlow (China)" + +#: src/iac_code/providers/registry.py:421 +msgid "SiliconFlow (International)" +msgstr "SiliconFlow (International)" + +#: src/iac_code/providers/registry.py:422 +msgid "Ollama (Local)" +msgstr "Ollama (Lokal)" + +#: src/iac_code/providers/registry.py:423 +msgid "LM Studio (Local)" +msgstr "LM Studio (Lokal)" + +#: src/iac_code/providers/registry.py:426 +msgid "ModelScope" +msgstr "ModelScope" + +#: src/iac_code/providers/registry.py:427 +msgid "Alibaba Cloud CodingPlan" +msgstr "Alibaba Cloud CodingPlan" + +#: src/iac_code/providers/registry.py:428 +msgid "Alibaba Cloud CodingPlan (International)" +msgstr "Alibaba Cloud CodingPlan (International)" + +#: src/iac_code/providers/registry.py:429 +msgid "ZhiPu AI CodingPlan" +msgstr "ZhiPu AI CodingPlan" + +#: src/iac_code/providers/registry.py:430 +msgid "ZhiPu AI CodingPlan (International)" +msgstr "ZhiPu AI CodingPlan (International)" + +#: src/iac_code/providers/registry.py:431 +msgid "Volcengine CodingPlan" +msgstr "Volcengine CodingPlan" + +#: src/iac_code/providers/registry.py:432 +msgid "Anthropic Compatible" +msgstr "Anthropic-kompatibel" + +#: src/iac_code/services/qwenpaw_source.py:208 +#, python-brace-format +msgid "" +"[QwenPaw mode] Unknown provider '{provider_id}'. iac-code does not " +"support this provider.\n" +"Supported QwenPaw provider IDs: {supported_ids}\n" +"To fix: switch to a supported provider in QwenPaw, or disable QwenPaw " +"mode (remove 'llm_source: qwenpaw' from settings.yml)." +msgstr "" +"[QwenPaw-Modus] Unbekannter Provider '{provider_id}'. iac-code " +"unterstützt diesen Provider nicht.\n" +"Unterstützte QwenPaw-Provider-IDs: {supported_ids}\n" +"Lösung: Wechseln Sie in QwenPaw zu einem unterstützten Provider, oder " +"deaktivieren Sie den QwenPaw-Modus (entfernen Sie 'llm_source: qwenpaw' " +"aus settings.yml)." + #: src/iac_code/skills/skill_tool.py:130 #, python-brace-format msgid "Skill '{name}' loaded (inline)." @@ -1020,47 +1177,23 @@ msgstr "ROS Stack" msgid "CloudStackInstances" msgstr "CloudStackInstances" -#: src/iac_code/ui/banner.py:38 -msgid "DashScope" -msgstr "DashScope" - -#: src/iac_code/ui/banner.py:39 -msgid "DashScope Token Plan" -msgstr "DashScope Token Plan" - -#: src/iac_code/ui/banner.py:40 -msgid "OpenAI" -msgstr "OpenAI" - -#: src/iac_code/ui/banner.py:41 -msgid "Anthropic" -msgstr "Anthropic" - -#: src/iac_code/ui/banner.py:42 -msgid "DeepSeek" -msgstr "DeepSeek" - -#: src/iac_code/ui/banner.py:43 -msgid "OpenAPI Compatible" -msgstr "OpenAPI-kompatibel" - -#: src/iac_code/ui/banner.py:67 +#: src/iac_code/ui/banner.py:64 msgid "Your AI-powered Infrastructure as Code assistant" msgstr "Ihr KI-gestützter Infrastructure-as-Code-Assistent" -#: src/iac_code/ui/banner.py:91 +#: src/iac_code/ui/banner.py:88 msgid "Welcome back" msgstr "Willkommen zurück" -#: src/iac_code/ui/banner.py:97 +#: src/iac_code/ui/banner.py:94 msgid "Session" msgstr "Sitzung" -#: src/iac_code/ui/banner.py:107 +#: src/iac_code/ui/banner.py:104 msgid "Debug mode" msgstr "Debug-Modus" -#: src/iac_code/ui/banner.py:108 +#: src/iac_code/ui/banner.py:105 msgid "Log file" msgstr "Protokolldatei" @@ -1147,43 +1280,43 @@ msgstr "Standard" msgid "No, always reject this tool" msgstr "Nein, dieses Tool immer ablehnen" -#: src/iac_code/ui/repl.py:311 +#: src/iac_code/ui/repl.py:319 msgid "Press Ctrl+C again to exit." msgstr "Drücken Sie erneut Ctrl+C zum Beenden." -#: src/iac_code/ui/repl.py:327 +#: src/iac_code/ui/repl.py:335 msgid "Interrupted." msgstr "Unterbrochen." -#: src/iac_code/ui/repl.py:364 +#: src/iac_code/ui/repl.py:372 msgid "Goodbye!" msgstr "Auf Wiedersehen!" -#: src/iac_code/ui/repl.py:365 +#: src/iac_code/ui/repl.py:373 msgid "Resume this session with:" msgstr "Diese Sitzung fortsetzen mit:" -#: src/iac_code/ui/repl.py:447 +#: src/iac_code/ui/repl.py:455 #, python-brace-format msgid "Unknown command: /{name}. Type /help for available commands." msgstr "Unbekannter Befehl: /{name}. Geben Sie /help für verfügbare Befehle ein." -#: src/iac_code/ui/repl.py:471 src/iac_code/ui/repl.py:503 +#: src/iac_code/ui/repl.py:479 src/iac_code/ui/repl.py:511 #, python-brace-format msgid "Command error: {error}" msgstr "Befehlsfehler: {error}" -#: src/iac_code/ui/repl.py:478 +#: src/iac_code/ui/repl.py:486 #, python-brace-format msgid "Command has no handler: {name}" msgstr "Kein Handler für Befehl: {name}" -#: src/iac_code/ui/repl.py:634 +#: src/iac_code/ui/repl.py:689 #, python-brace-format msgid "Session not found: {session_id}" msgstr "Sitzung nicht gefunden: {session_id}" -#: src/iac_code/ui/repl.py:653 +#: src/iac_code/ui/repl.py:708 #, python-brace-format msgid "" "This session belongs to a different directory.\n" @@ -1194,15 +1327,15 @@ msgstr "" "Zum Fortsetzen ausführen:\n" " {cmd}" -#: src/iac_code/ui/repl.py:692 +#: src/iac_code/ui/repl.py:747 msgid "This conversation is from a different directory." msgstr "Diese Konversation stammt aus einem anderen Verzeichnis." -#: src/iac_code/ui/repl.py:694 +#: src/iac_code/ui/repl.py:749 msgid "To resume, run:" msgstr "Zum Fortsetzen ausführen:" -#: src/iac_code/ui/repl.py:699 +#: src/iac_code/ui/repl.py:754 msgid "(Command copied to clipboard)" msgstr "(Befehl in die Zwischenablage kopiert)" @@ -1403,3 +1536,6 @@ msgstr "vor {n} Tag{s}" #~ msgid "Choose [1-4]: " #~ msgstr "Wählen Sie [1-4]: " +#~ msgid "DashScope Token Plan" +#~ msgstr "DashScope Token Plan" + diff --git a/src/iac_code/i18n/locales/es/LC_MESSAGES/messages.po b/src/iac_code/i18n/locales/es/LC_MESSAGES/messages.po index 369a5822..2ce4d525 100644 --- a/src/iac_code/i18n/locales/es/LC_MESSAGES/messages.po +++ b/src/iac_code/i18n/locales/es/LC_MESSAGES/messages.po @@ -4,9 +4,9 @@ # msgid "" msgstr "" -"Project-Id-Version: iac-code 0.1.1\n" +"Project-Id-Version: iac-code 0.1.2\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-05-15 11:19+0800\n" +"POT-Creation-Date: 2026-05-15 17:58+0800\n" "PO-Revision-Date: 2026-05-13 00:00+0000\n" "Last-Translator: \n" "Language: es\n" @@ -132,47 +132,47 @@ msgstr "" " Documentación: https://aliyun.github.io/iac-" "code/es/docs/configuration/authentication\n" -#: src/iac_code/cli/main.py:22 src/iac_code/commands/help.py:26 +#: src/iac_code/cli/main.py:23 src/iac_code/commands/help.py:26 msgid "AI-powered infrastructure orchestration tool" msgstr "Herramienta de orquestación de infraestructura asistida por IA" -#: src/iac_code/cli/main.py:33 +#: src/iac_code/cli/main.py:34 msgid "LLM model to use" msgstr "Modelo LLM a utilizar" -#: src/iac_code/cli/main.py:34 +#: src/iac_code/cli/main.py:35 msgid "Non-interactive mode: run a single prompt and exit" msgstr "Modo no interactivo: ejecuta un único prompt y termina" -#: src/iac_code/cli/main.py:35 +#: src/iac_code/cli/main.py:36 msgid "Output format: text, json, stream-json" msgstr "Formato de salida: text, json, stream-json" -#: src/iac_code/cli/main.py:36 +#: src/iac_code/cli/main.py:37 msgid "Maximum agent turns in headless mode" msgstr "Turnos máximos del agente en modo sin interfaz" -#: src/iac_code/cli/main.py:37 src/iac_code/cli/main.py:236 +#: src/iac_code/cli/main.py:38 src/iac_code/cli/main.py:241 msgid "Enable debug logging" msgstr "Habilitar el registro de depuración" -#: src/iac_code/cli/main.py:38 +#: src/iac_code/cli/main.py:39 msgid "Show version and exit" msgstr "Mostrar la versión y salir" -#: src/iac_code/cli/main.py:39 +#: src/iac_code/cli/main.py:40 msgid "Resume a session by ID" msgstr "Reanudar una sesión por ID" -#: src/iac_code/cli/main.py:40 +#: src/iac_code/cli/main.py:41 msgid "Resume the most recent session" msgstr "Reanudar la sesión más reciente" -#: src/iac_code/cli/main.py:47 src/iac_code/i18n/__init__.py:54 +#: src/iac_code/cli/main.py:48 src/iac_code/i18n/__init__.py:54 msgid "Install completion for the current shell." msgstr "Instalar la finalización automática para el shell actual." -#: src/iac_code/cli/main.py:55 src/iac_code/i18n/__init__.py:55 +#: src/iac_code/cli/main.py:56 src/iac_code/i18n/__init__.py:55 msgid "" "Show completion for the current shell, to copy it or customize the " "installation." @@ -180,23 +180,23 @@ msgstr "" "Mostrar el script de finalización del shell actual para copiarlo o " "personalizar la instalación." -#: src/iac_code/cli/main.py:69 +#: src/iac_code/cli/main.py:70 msgid "Error: --resume and --continue cannot be used together." msgstr "Error: --resume y --continue no pueden usarse a la vez." -#: src/iac_code/cli/main.py:231 +#: src/iac_code/cli/main.py:236 msgid "Run iac-code as an ACP server." msgstr "Ejecutar iac-code como servidor ACP." -#: src/iac_code/cli/main.py:233 +#: src/iac_code/cli/main.py:238 msgid "Transport type: stdio or http" msgstr "Tipo de transporte: stdio o http" -#: src/iac_code/cli/main.py:234 +#: src/iac_code/cli/main.py:239 msgid "HTTP server port" msgstr "Puerto del servidor HTTP" -#: src/iac_code/cli/main.py:235 +#: src/iac_code/cli/main.py:240 msgid "HTTP server host" msgstr "Host del servidor HTTP" @@ -244,104 +244,162 @@ msgstr "Reanudar una sesión anterior" msgid "[conversation id or search term]" msgstr "[id de conversación o término de búsqueda]" -#: src/iac_code/commands/auth.py:304 src/iac_code/commands/auth.py:817 +#: src/iac_code/commands/auth.py:245 src/iac_code/commands/auth.py:817 #: src/iac_code/ui/core/prompt_input.py:373 msgid "Navigate" msgstr "Navegar" -#: src/iac_code/commands/auth.py:304 src/iac_code/commands/auth.py:426 -#: src/iac_code/commands/auth.py:460 src/iac_code/commands/auth.py:467 -#: src/iac_code/commands/auth.py:491 src/iac_code/commands/auth.py:817 +#: src/iac_code/commands/auth.py:245 src/iac_code/commands/auth.py:367 +#: src/iac_code/commands/auth.py:401 src/iac_code/commands/auth.py:408 +#: src/iac_code/commands/auth.py:432 src/iac_code/commands/auth.py:817 #: src/iac_code/commands/auth.py:1007 src/iac_code/ui/core/prompt_input.py:373 msgid "Confirm" msgstr "Confirmar" -#: src/iac_code/commands/auth.py:304 src/iac_code/commands/auth.py:424 -#: src/iac_code/commands/auth.py:426 src/iac_code/commands/auth.py:460 -#: src/iac_code/commands/auth.py:467 src/iac_code/commands/auth.py:491 +#: src/iac_code/commands/auth.py:245 src/iac_code/commands/auth.py:365 +#: src/iac_code/commands/auth.py:367 src/iac_code/commands/auth.py:401 +#: src/iac_code/commands/auth.py:408 src/iac_code/commands/auth.py:432 #: src/iac_code/commands/auth.py:817 src/iac_code/commands/auth.py:902 #: src/iac_code/commands/auth.py:1007 msgid "Back" msgstr "Atrás" -#: src/iac_code/commands/auth.py:424 +#: src/iac_code/commands/auth.py:365 msgid "Keep" msgstr "Conservar" -#: src/iac_code/commands/auth.py:424 +#: src/iac_code/commands/auth.py:365 msgid "Re-enter" msgstr "Volver a introducir" -#: src/iac_code/commands/auth.py:566 src/iac_code/commands/auth.py:667 +#: src/iac_code/commands/auth.py:507 src/iac_code/commands/auth.py:631 +#: src/iac_code/commands/auth.py:651 msgid " (current)" msgstr " (actual)" -#: src/iac_code/commands/auth.py:569 +#: src/iac_code/commands/auth.py:510 msgid "Custom model..." msgstr "Modelo personalizado..." -#: src/iac_code/commands/auth.py:572 +#: src/iac_code/commands/auth.py:513 #, python-brace-format msgid "Select model for {provider}" msgstr "Seleccionar modelo para {provider}" -#: src/iac_code/commands/auth.py:574 +#: src/iac_code/commands/auth.py:515 msgid "Select model" msgstr "Seleccionar modelo" -#: src/iac_code/commands/auth.py:582 +#: src/iac_code/commands/auth.py:523 msgid "Enter custom model name: " msgstr "Introduzca el nombre del modelo personalizado: " -#: src/iac_code/commands/auth.py:608 +#: src/iac_code/commands/auth.py:549 msgid "Error: console not available" msgstr "Error: la consola no está disponible" -#: src/iac_code/commands/auth.py:635 +#: src/iac_code/commands/auth.py:576 msgid "Configure LLM Provider" msgstr "Configurar proveedor LLM" -#: src/iac_code/commands/auth.py:636 +#: src/iac_code/commands/auth.py:577 msgid "Configure IaC Cloud Service" msgstr "Configurar servicio cloud IaC" -#: src/iac_code/commands/auth.py:638 +#: src/iac_code/commands/auth.py:579 msgid "Select configuration type" msgstr "Seleccionar tipo de configuración" -#: src/iac_code/commands/auth.py:640 src/iac_code/commands/auth.py:694 -#: src/iac_code/commands/auth.py:709 src/iac_code/commands/auth.py:770 +#: src/iac_code/commands/auth.py:581 src/iac_code/commands/auth.py:676 +#: src/iac_code/commands/auth.py:692 src/iac_code/commands/auth.py:770 #: src/iac_code/commands/auth.py:943 src/iac_code/commands/auth.py:984 msgid "Auth cancelled" msgstr "Autenticación cancelada" -#: src/iac_code/commands/auth.py:676 +#: src/iac_code/commands/auth.py:635 msgid "Select provider" msgstr "Seleccionar proveedor" -#: src/iac_code/commands/auth.py:687 +#: src/iac_code/commands/auth.py:656 src/iac_code/commands/auth.py:745 +#, python-brace-format +msgid "Select provider — {group}" +msgstr "Seleccionar proveedor — {group}" + +#: src/iac_code/commands/auth.py:669 #, python-brace-format msgid "Configure {provider}" msgstr "Configurar {provider}" -#: src/iac_code/commands/auth.py:702 +#: src/iac_code/commands/auth.py:685 #, python-brace-format msgid "Enter API key for {provider}" msgstr "Introduzca la API key para {provider}" -#: src/iac_code/commands/auth.py:742 +#: src/iac_code/commands/auth.py:723 #, python-brace-format msgid "{status}: {provider} / {model}" msgstr "{status}: {provider} / {model}" -#: src/iac_code/commands/auth.py:743 +#: src/iac_code/commands/auth.py:724 msgid "Configured" msgstr "Configurado" -#: src/iac_code/commands/auth.py:752 +#: src/iac_code/commands/auth.py:731 src/iac_code/commands/auth.py:752 msgid "Alibaba Cloud" msgstr "Alibaba Cloud" +#: src/iac_code/commands/auth.py:732 src/iac_code/providers/registry.py:417 +msgid "ZhiPu AI" +msgstr "ZhiPu AI" + +#: src/iac_code/commands/auth.py:733 +msgid "Kimi" +msgstr "Kimi" + +#: src/iac_code/commands/auth.py:734 +msgid "MiniMax" +msgstr "MiniMax" + +#: src/iac_code/commands/auth.py:735 src/iac_code/providers/registry.py:419 +msgid "Volcengine" +msgstr "Volcengine" + +#: src/iac_code/commands/auth.py:736 +msgid "SiliconFlow" +msgstr "SiliconFlow" + +#: src/iac_code/commands/auth.py:737 src/iac_code/providers/registry.py:410 +msgid "DeepSeek" +msgstr "DeepSeek" + +#: src/iac_code/commands/auth.py:738 src/iac_code/providers/registry.py:408 +msgid "OpenAI" +msgstr "OpenAI" + +#: src/iac_code/commands/auth.py:739 src/iac_code/providers/registry.py:409 +msgid "Anthropic" +msgstr "Anthropic" + +#: src/iac_code/commands/auth.py:740 src/iac_code/providers/registry.py:412 +msgid "Google Gemini" +msgstr "Google Gemini" + +#: src/iac_code/commands/auth.py:741 src/iac_code/providers/registry.py:425 +msgid "Azure OpenAI" +msgstr "Azure OpenAI" + +#: src/iac_code/commands/auth.py:742 src/iac_code/providers/registry.py:424 +msgid "OpenRouter" +msgstr "OpenRouter" + +#: src/iac_code/commands/auth.py:743 +msgid "Local" +msgstr "Local" + +#: src/iac_code/commands/auth.py:744 +msgid "Compatible" +msgstr "Compatible" + #: src/iac_code/commands/auth.py:761 msgid "Select Cloud Provider" msgstr "Seleccionar proveedor de cloud" @@ -558,14 +616,19 @@ msgstr "(dinámico)" msgid "Aborted!" msgstr "¡Operación cancelada!" -#: src/iac_code/providers/manager.py:80 +#: src/iac_code/providers/manager.py:78 #, python-brace-format msgid "Cannot determine provider for model: {model}. Run /auth to configure." msgstr "" "No se puede determinar el proveedor para el modelo: {model}. Ejecute " "/auth para configurar." -#: src/iac_code/providers/manager.py:88 +#: src/iac_code/providers/manager.py:95 +#, python-brace-format +msgid "Unknown provider key: '{key}'. Run /auth to configure." +msgstr "Clave de proveedor desconocida: '{key}'. Ejecute /auth para configurar." + +#: src/iac_code/providers/manager.py:100 #, python-brace-format msgid "" "No API key configured for provider '{provider}' (model: {model}). Run " @@ -574,7 +637,7 @@ msgstr "" "No se ha configurado una clave API para el proveedor '{provider}' " "(modelo: {model}). Ejecute /auth para configurar." -#: src/iac_code/providers/openai_provider.py:286 +#: src/iac_code/providers/openai_provider.py:289 #, python-brace-format msgid "" "API returned no data. Please check that your API Base URL is correct " @@ -585,7 +648,7 @@ msgstr "" "(actual: {base_url}). Muchos endpoints compatibles con OpenAI requieren " "el sufijo /v1 (p. ej., {base_url}/v1)." -#: src/iac_code/providers/openai_provider.py:327 +#: src/iac_code/providers/openai_provider.py:330 #, python-brace-format msgid "" "API returned an invalid response. Please check that your API Base URL is " @@ -596,6 +659,97 @@ msgstr "" " correcta (actual: {base_url}). Muchos endpoints compatibles con OpenAI " "requieren el sufijo /v1 (p. ej., {base_url}/v1)." +#: src/iac_code/providers/registry.py:406 +msgid "Alibaba Cloud Bailian" +msgstr "Alibaba Cloud Bailian" + +#: src/iac_code/providers/registry.py:407 +msgid "Alibaba Cloud Bailian Token Plan" +msgstr "Alibaba Cloud Bailian Token Plan" + +#: src/iac_code/providers/registry.py:411 +msgid "OpenAPI Compatible" +msgstr "Compatible con OpenAPI" + +#: src/iac_code/providers/registry.py:413 +msgid "Kimi (China)" +msgstr "Kimi (China)" + +#: src/iac_code/providers/registry.py:414 +msgid "Kimi (International)" +msgstr "Kimi (Internacional)" + +#: src/iac_code/providers/registry.py:415 +msgid "MiniMax (China)" +msgstr "MiniMax (China)" + +#: src/iac_code/providers/registry.py:416 +msgid "MiniMax (International)" +msgstr "MiniMax (Internacional)" + +#: src/iac_code/providers/registry.py:418 +msgid "ZhiPu AI (International)" +msgstr "ZhiPu AI (Internacional)" + +#: src/iac_code/providers/registry.py:420 +msgid "SiliconFlow (China)" +msgstr "SiliconFlow (China)" + +#: src/iac_code/providers/registry.py:421 +msgid "SiliconFlow (International)" +msgstr "SiliconFlow (Internacional)" + +#: src/iac_code/providers/registry.py:422 +msgid "Ollama (Local)" +msgstr "Ollama (Local)" + +#: src/iac_code/providers/registry.py:423 +msgid "LM Studio (Local)" +msgstr "LM Studio (Local)" + +#: src/iac_code/providers/registry.py:426 +msgid "ModelScope" +msgstr "ModelScope" + +#: src/iac_code/providers/registry.py:427 +msgid "Alibaba Cloud CodingPlan" +msgstr "Alibaba Cloud CodingPlan" + +#: src/iac_code/providers/registry.py:428 +msgid "Alibaba Cloud CodingPlan (International)" +msgstr "Alibaba Cloud CodingPlan (Internacional)" + +#: src/iac_code/providers/registry.py:429 +msgid "ZhiPu AI CodingPlan" +msgstr "ZhiPu AI CodingPlan" + +#: src/iac_code/providers/registry.py:430 +msgid "ZhiPu AI CodingPlan (International)" +msgstr "ZhiPu AI CodingPlan (Internacional)" + +#: src/iac_code/providers/registry.py:431 +msgid "Volcengine CodingPlan" +msgstr "Volcengine CodingPlan" + +#: src/iac_code/providers/registry.py:432 +msgid "Anthropic Compatible" +msgstr "Compatible con Anthropic" + +#: src/iac_code/services/qwenpaw_source.py:208 +#, python-brace-format +msgid "" +"[QwenPaw mode] Unknown provider '{provider_id}'. iac-code does not " +"support this provider.\n" +"Supported QwenPaw provider IDs: {supported_ids}\n" +"To fix: switch to a supported provider in QwenPaw, or disable QwenPaw " +"mode (remove 'llm_source: qwenpaw' from settings.yml)." +msgstr "" +"[Modo QwenPaw] Proveedor desconocido '{provider_id}'. iac-code no soporta" +" este proveedor.\n" +"IDs de proveedores QwenPaw soportados: {supported_ids}\n" +"Solución: cambie a un proveedor soportado en QwenPaw, o desactive el modo" +" QwenPaw (elimine 'llm_source: qwenpaw' de settings.yml)." + #: src/iac_code/skills/skill_tool.py:130 #, python-brace-format msgid "Skill '{name}' loaded (inline)." @@ -1023,47 +1177,23 @@ msgstr "ROS Stack" msgid "CloudStackInstances" msgstr "CloudStackInstances" -#: src/iac_code/ui/banner.py:38 -msgid "DashScope" -msgstr "DashScope" - -#: src/iac_code/ui/banner.py:39 -msgid "DashScope Token Plan" -msgstr "DashScope Token Plan" - -#: src/iac_code/ui/banner.py:40 -msgid "OpenAI" -msgstr "OpenAI" - -#: src/iac_code/ui/banner.py:41 -msgid "Anthropic" -msgstr "Anthropic" - -#: src/iac_code/ui/banner.py:42 -msgid "DeepSeek" -msgstr "DeepSeek" - -#: src/iac_code/ui/banner.py:43 -msgid "OpenAPI Compatible" -msgstr "OpenAPI compatible" - -#: src/iac_code/ui/banner.py:67 +#: src/iac_code/ui/banner.py:64 msgid "Your AI-powered Infrastructure as Code assistant" msgstr "Su asistente de infraestructura como código con IA" -#: src/iac_code/ui/banner.py:91 +#: src/iac_code/ui/banner.py:88 msgid "Welcome back" msgstr "Bienvenido de nuevo" -#: src/iac_code/ui/banner.py:97 +#: src/iac_code/ui/banner.py:94 msgid "Session" msgstr "Sesión" -#: src/iac_code/ui/banner.py:107 +#: src/iac_code/ui/banner.py:104 msgid "Debug mode" msgstr "Modo depuración" -#: src/iac_code/ui/banner.py:108 +#: src/iac_code/ui/banner.py:105 msgid "Log file" msgstr "Archivo de registro" @@ -1150,45 +1280,45 @@ msgstr "predeterminado" msgid "No, always reject this tool" msgstr "No, rechazar siempre esta herramienta" -#: src/iac_code/ui/repl.py:311 +#: src/iac_code/ui/repl.py:319 msgid "Press Ctrl+C again to exit." msgstr "Pulse Ctrl+C de nuevo para salir." -#: src/iac_code/ui/repl.py:327 +#: src/iac_code/ui/repl.py:335 msgid "Interrupted." msgstr "Interrumpido." -#: src/iac_code/ui/repl.py:364 +#: src/iac_code/ui/repl.py:372 msgid "Goodbye!" msgstr "¡Hasta luego!" -#: src/iac_code/ui/repl.py:365 +#: src/iac_code/ui/repl.py:373 msgid "Resume this session with:" msgstr "Para reanudar esta sesión, ejecute:" -#: src/iac_code/ui/repl.py:447 +#: src/iac_code/ui/repl.py:455 #, python-brace-format msgid "Unknown command: /{name}. Type /help for available commands." msgstr "" "Comando desconocido: /{name}. Escriba /help para ver los comandos " "disponibles." -#: src/iac_code/ui/repl.py:471 src/iac_code/ui/repl.py:503 +#: src/iac_code/ui/repl.py:479 src/iac_code/ui/repl.py:511 #, python-brace-format msgid "Command error: {error}" msgstr "Error de comando: {error}" -#: src/iac_code/ui/repl.py:478 +#: src/iac_code/ui/repl.py:486 #, python-brace-format msgid "Command has no handler: {name}" msgstr "El comando no tiene controlador: {name}" -#: src/iac_code/ui/repl.py:634 +#: src/iac_code/ui/repl.py:689 #, python-brace-format msgid "Session not found: {session_id}" msgstr "Sesión no encontrada: {session_id}" -#: src/iac_code/ui/repl.py:653 +#: src/iac_code/ui/repl.py:708 #, python-brace-format msgid "" "This session belongs to a different directory.\n" @@ -1199,15 +1329,15 @@ msgstr "" "Para reanudar, ejecute:\n" " {cmd}" -#: src/iac_code/ui/repl.py:692 +#: src/iac_code/ui/repl.py:747 msgid "This conversation is from a different directory." msgstr "Esta conversación procede de otro directorio." -#: src/iac_code/ui/repl.py:694 +#: src/iac_code/ui/repl.py:749 msgid "To resume, run:" msgstr "Para reanudar, ejecute:" -#: src/iac_code/ui/repl.py:699 +#: src/iac_code/ui/repl.py:754 msgid "(Command copied to clipboard)" msgstr "(Comando copiado al portapapeles)" @@ -1408,3 +1538,6 @@ msgstr "hace {n} día{s}" #~ msgid "Choose [1-4]: " #~ msgstr "Seleccione [1-4]: " +#~ msgid "DashScope Token Plan" +#~ msgstr "DashScope Token Plan" + diff --git a/src/iac_code/i18n/locales/fr/LC_MESSAGES/messages.po b/src/iac_code/i18n/locales/fr/LC_MESSAGES/messages.po index 91bf16fe..4e9881ae 100644 --- a/src/iac_code/i18n/locales/fr/LC_MESSAGES/messages.po +++ b/src/iac_code/i18n/locales/fr/LC_MESSAGES/messages.po @@ -4,9 +4,9 @@ # msgid "" msgstr "" -"Project-Id-Version: iac-code 0.1.1\n" +"Project-Id-Version: iac-code 0.1.2\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-05-15 11:19+0800\n" +"POT-Creation-Date: 2026-05-15 17:58+0800\n" "PO-Revision-Date: 2026-05-13 00:00+0000\n" "Last-Translator: \n" "Language: fr\n" @@ -131,47 +131,47 @@ msgstr "" " Documentation : https://aliyun.github.io/iac-" "code/fr/docs/configuration/authentication\n" -#: src/iac_code/cli/main.py:22 src/iac_code/commands/help.py:26 +#: src/iac_code/cli/main.py:23 src/iac_code/commands/help.py:26 msgid "AI-powered infrastructure orchestration tool" msgstr "Outil d’orchestration d’infrastructure assisté par IA" -#: src/iac_code/cli/main.py:33 +#: src/iac_code/cli/main.py:34 msgid "LLM model to use" msgstr "Modèle LLM à utiliser" -#: src/iac_code/cli/main.py:34 +#: src/iac_code/cli/main.py:35 msgid "Non-interactive mode: run a single prompt and exit" msgstr "Mode non interactif : exécuter un seul prompt puis quitter" -#: src/iac_code/cli/main.py:35 +#: src/iac_code/cli/main.py:36 msgid "Output format: text, json, stream-json" msgstr "Format de sortie : text, json, stream-json" -#: src/iac_code/cli/main.py:36 +#: src/iac_code/cli/main.py:37 msgid "Maximum agent turns in headless mode" msgstr "Nombre maximal de tours d’agent en mode headless" -#: src/iac_code/cli/main.py:37 src/iac_code/cli/main.py:236 +#: src/iac_code/cli/main.py:38 src/iac_code/cli/main.py:241 msgid "Enable debug logging" msgstr "Activer la journalisation debug" -#: src/iac_code/cli/main.py:38 +#: src/iac_code/cli/main.py:39 msgid "Show version and exit" msgstr "Afficher la version et quitter" -#: src/iac_code/cli/main.py:39 +#: src/iac_code/cli/main.py:40 msgid "Resume a session by ID" msgstr "Reprendre une session par identifiant" -#: src/iac_code/cli/main.py:40 +#: src/iac_code/cli/main.py:41 msgid "Resume the most recent session" msgstr "Reprendre la session la plus récente" -#: src/iac_code/cli/main.py:47 src/iac_code/i18n/__init__.py:54 +#: src/iac_code/cli/main.py:48 src/iac_code/i18n/__init__.py:54 msgid "Install completion for the current shell." msgstr "Installer la complétion pour le shell actuel." -#: src/iac_code/cli/main.py:55 src/iac_code/i18n/__init__.py:55 +#: src/iac_code/cli/main.py:56 src/iac_code/i18n/__init__.py:55 msgid "" "Show completion for the current shell, to copy it or customize the " "installation." @@ -179,23 +179,23 @@ msgstr "" "Afficher la complétion pour le shell actuel afin de la copier ou de " "personnaliser l’installation." -#: src/iac_code/cli/main.py:69 +#: src/iac_code/cli/main.py:70 msgid "Error: --resume and --continue cannot be used together." msgstr "Erreur : --resume et --continue ne peuvent pas être utilisés ensemble." -#: src/iac_code/cli/main.py:231 +#: src/iac_code/cli/main.py:236 msgid "Run iac-code as an ACP server." msgstr "Exécuter iac-code comme serveur ACP." -#: src/iac_code/cli/main.py:233 +#: src/iac_code/cli/main.py:238 msgid "Transport type: stdio or http" msgstr "Type de transport : stdio ou http" -#: src/iac_code/cli/main.py:234 +#: src/iac_code/cli/main.py:239 msgid "HTTP server port" msgstr "Port du serveur HTTP" -#: src/iac_code/cli/main.py:235 +#: src/iac_code/cli/main.py:240 msgid "HTTP server host" msgstr "Hôte du serveur HTTP" @@ -243,104 +243,162 @@ msgstr "Reprendre une session précédente" msgid "[conversation id or search term]" msgstr "[identifiant de conversation ou terme de recherche]" -#: src/iac_code/commands/auth.py:304 src/iac_code/commands/auth.py:817 +#: src/iac_code/commands/auth.py:245 src/iac_code/commands/auth.py:817 #: src/iac_code/ui/core/prompt_input.py:373 msgid "Navigate" msgstr "Naviguer" -#: src/iac_code/commands/auth.py:304 src/iac_code/commands/auth.py:426 -#: src/iac_code/commands/auth.py:460 src/iac_code/commands/auth.py:467 -#: src/iac_code/commands/auth.py:491 src/iac_code/commands/auth.py:817 +#: src/iac_code/commands/auth.py:245 src/iac_code/commands/auth.py:367 +#: src/iac_code/commands/auth.py:401 src/iac_code/commands/auth.py:408 +#: src/iac_code/commands/auth.py:432 src/iac_code/commands/auth.py:817 #: src/iac_code/commands/auth.py:1007 src/iac_code/ui/core/prompt_input.py:373 msgid "Confirm" msgstr "Confirmer" -#: src/iac_code/commands/auth.py:304 src/iac_code/commands/auth.py:424 -#: src/iac_code/commands/auth.py:426 src/iac_code/commands/auth.py:460 -#: src/iac_code/commands/auth.py:467 src/iac_code/commands/auth.py:491 +#: src/iac_code/commands/auth.py:245 src/iac_code/commands/auth.py:365 +#: src/iac_code/commands/auth.py:367 src/iac_code/commands/auth.py:401 +#: src/iac_code/commands/auth.py:408 src/iac_code/commands/auth.py:432 #: src/iac_code/commands/auth.py:817 src/iac_code/commands/auth.py:902 #: src/iac_code/commands/auth.py:1007 msgid "Back" msgstr "Retour" -#: src/iac_code/commands/auth.py:424 +#: src/iac_code/commands/auth.py:365 msgid "Keep" msgstr "Conserver" -#: src/iac_code/commands/auth.py:424 +#: src/iac_code/commands/auth.py:365 msgid "Re-enter" msgstr "Saisir à nouveau" -#: src/iac_code/commands/auth.py:566 src/iac_code/commands/auth.py:667 +#: src/iac_code/commands/auth.py:507 src/iac_code/commands/auth.py:631 +#: src/iac_code/commands/auth.py:651 msgid " (current)" msgstr " (actuel)" -#: src/iac_code/commands/auth.py:569 +#: src/iac_code/commands/auth.py:510 msgid "Custom model..." msgstr "Modèle personnalisé…" -#: src/iac_code/commands/auth.py:572 +#: src/iac_code/commands/auth.py:513 #, python-brace-format msgid "Select model for {provider}" msgstr "Sélectionner le modèle pour {provider}" -#: src/iac_code/commands/auth.py:574 +#: src/iac_code/commands/auth.py:515 msgid "Select model" msgstr "Sélectionner le modèle" -#: src/iac_code/commands/auth.py:582 +#: src/iac_code/commands/auth.py:523 msgid "Enter custom model name: " msgstr "Saisir le nom du modèle personnalisé : " -#: src/iac_code/commands/auth.py:608 +#: src/iac_code/commands/auth.py:549 msgid "Error: console not available" msgstr "Erreur : console indisponible" -#: src/iac_code/commands/auth.py:635 +#: src/iac_code/commands/auth.py:576 msgid "Configure LLM Provider" msgstr "Configurer le fournisseur LLM" -#: src/iac_code/commands/auth.py:636 +#: src/iac_code/commands/auth.py:577 msgid "Configure IaC Cloud Service" msgstr "Configurer le service cloud IaC" -#: src/iac_code/commands/auth.py:638 +#: src/iac_code/commands/auth.py:579 msgid "Select configuration type" msgstr "Sélectionner le type de configuration" -#: src/iac_code/commands/auth.py:640 src/iac_code/commands/auth.py:694 -#: src/iac_code/commands/auth.py:709 src/iac_code/commands/auth.py:770 +#: src/iac_code/commands/auth.py:581 src/iac_code/commands/auth.py:676 +#: src/iac_code/commands/auth.py:692 src/iac_code/commands/auth.py:770 #: src/iac_code/commands/auth.py:943 src/iac_code/commands/auth.py:984 msgid "Auth cancelled" msgstr "Authentification annulée" -#: src/iac_code/commands/auth.py:676 +#: src/iac_code/commands/auth.py:635 msgid "Select provider" msgstr "Sélectionner le fournisseur" -#: src/iac_code/commands/auth.py:687 +#: src/iac_code/commands/auth.py:656 src/iac_code/commands/auth.py:745 +#, python-brace-format +msgid "Select provider — {group}" +msgstr "Sélectionner le fournisseur — {group}" + +#: src/iac_code/commands/auth.py:669 #, python-brace-format msgid "Configure {provider}" msgstr "Configurer {provider}" -#: src/iac_code/commands/auth.py:702 +#: src/iac_code/commands/auth.py:685 #, python-brace-format msgid "Enter API key for {provider}" msgstr "Saisir la clé API pour {provider}" -#: src/iac_code/commands/auth.py:742 +#: src/iac_code/commands/auth.py:723 #, python-brace-format msgid "{status}: {provider} / {model}" msgstr "{status} : {provider} / {model}" -#: src/iac_code/commands/auth.py:743 +#: src/iac_code/commands/auth.py:724 msgid "Configured" msgstr "Configuré" -#: src/iac_code/commands/auth.py:752 +#: src/iac_code/commands/auth.py:731 src/iac_code/commands/auth.py:752 msgid "Alibaba Cloud" msgstr "Alibaba Cloud" +#: src/iac_code/commands/auth.py:732 src/iac_code/providers/registry.py:417 +msgid "ZhiPu AI" +msgstr "ZhiPu AI" + +#: src/iac_code/commands/auth.py:733 +msgid "Kimi" +msgstr "Kimi" + +#: src/iac_code/commands/auth.py:734 +msgid "MiniMax" +msgstr "MiniMax" + +#: src/iac_code/commands/auth.py:735 src/iac_code/providers/registry.py:419 +msgid "Volcengine" +msgstr "Volcengine" + +#: src/iac_code/commands/auth.py:736 +msgid "SiliconFlow" +msgstr "SiliconFlow" + +#: src/iac_code/commands/auth.py:737 src/iac_code/providers/registry.py:410 +msgid "DeepSeek" +msgstr "DeepSeek" + +#: src/iac_code/commands/auth.py:738 src/iac_code/providers/registry.py:408 +msgid "OpenAI" +msgstr "OpenAI" + +#: src/iac_code/commands/auth.py:739 src/iac_code/providers/registry.py:409 +msgid "Anthropic" +msgstr "Anthropic" + +#: src/iac_code/commands/auth.py:740 src/iac_code/providers/registry.py:412 +msgid "Google Gemini" +msgstr "Google Gemini" + +#: src/iac_code/commands/auth.py:741 src/iac_code/providers/registry.py:425 +msgid "Azure OpenAI" +msgstr "Azure OpenAI" + +#: src/iac_code/commands/auth.py:742 src/iac_code/providers/registry.py:424 +msgid "OpenRouter" +msgstr "OpenRouter" + +#: src/iac_code/commands/auth.py:743 +msgid "Local" +msgstr "Local" + +#: src/iac_code/commands/auth.py:744 +msgid "Compatible" +msgstr "Compatible" + #: src/iac_code/commands/auth.py:761 msgid "Select Cloud Provider" msgstr "Sélectionner le fournisseur cloud" @@ -561,14 +619,19 @@ msgstr "(dynamique)" msgid "Aborted!" msgstr "Interrompu !" -#: src/iac_code/providers/manager.py:80 +#: src/iac_code/providers/manager.py:78 #, python-brace-format msgid "Cannot determine provider for model: {model}. Run /auth to configure." msgstr "" "Impossible de déterminer le fournisseur pour le modèle : {model}. " "Exécutez /auth pour configurer." -#: src/iac_code/providers/manager.py:88 +#: src/iac_code/providers/manager.py:95 +#, python-brace-format +msgid "Unknown provider key: '{key}'. Run /auth to configure." +msgstr "Clé de fournisseur inconnue : '{key}'. Exécutez /auth pour configurer." + +#: src/iac_code/providers/manager.py:100 #, python-brace-format msgid "" "No API key configured for provider '{provider}' (model: {model}). Run " @@ -577,7 +640,7 @@ msgstr "" "Aucune clé API configurée pour le fournisseur '{provider}' (modèle : " "{model}). Exécutez /auth pour configurer." -#: src/iac_code/providers/openai_provider.py:286 +#: src/iac_code/providers/openai_provider.py:289 #, python-brace-format msgid "" "API returned no data. Please check that your API Base URL is correct " @@ -588,7 +651,7 @@ msgstr "" "correcte (actuelle : {base_url}). De nombreux points de terminaison " "compatibles OpenAI exigent le suffixe /v1 (p. ex. {base_url}/v1)." -#: src/iac_code/providers/openai_provider.py:327 +#: src/iac_code/providers/openai_provider.py:330 #, python-brace-format msgid "" "API returned an invalid response. Please check that your API Base URL is " @@ -599,6 +662,98 @@ msgstr "" " correcte (actuelle : {base_url}). De nombreux points de terminaison " "compatibles OpenAI exigent le suffixe /v1 (p. ex. {base_url}/v1)." +#: src/iac_code/providers/registry.py:406 +msgid "Alibaba Cloud Bailian" +msgstr "Alibaba Cloud Bailian" + +#: src/iac_code/providers/registry.py:407 +msgid "Alibaba Cloud Bailian Token Plan" +msgstr "Alibaba Cloud Bailian Token Plan" + +#: src/iac_code/providers/registry.py:411 +msgid "OpenAPI Compatible" +msgstr "Compatible OpenAPI" + +#: src/iac_code/providers/registry.py:413 +msgid "Kimi (China)" +msgstr "Kimi (Chine)" + +#: src/iac_code/providers/registry.py:414 +msgid "Kimi (International)" +msgstr "Kimi (International)" + +#: src/iac_code/providers/registry.py:415 +msgid "MiniMax (China)" +msgstr "MiniMax (Chine)" + +#: src/iac_code/providers/registry.py:416 +msgid "MiniMax (International)" +msgstr "MiniMax (International)" + +#: src/iac_code/providers/registry.py:418 +msgid "ZhiPu AI (International)" +msgstr "ZhiPu AI (International)" + +#: src/iac_code/providers/registry.py:420 +msgid "SiliconFlow (China)" +msgstr "SiliconFlow (Chine)" + +#: src/iac_code/providers/registry.py:421 +msgid "SiliconFlow (International)" +msgstr "SiliconFlow (International)" + +#: src/iac_code/providers/registry.py:422 +msgid "Ollama (Local)" +msgstr "Ollama (Local)" + +#: src/iac_code/providers/registry.py:423 +msgid "LM Studio (Local)" +msgstr "LM Studio (Local)" + +#: src/iac_code/providers/registry.py:426 +msgid "ModelScope" +msgstr "ModelScope" + +#: src/iac_code/providers/registry.py:427 +msgid "Alibaba Cloud CodingPlan" +msgstr "Alibaba Cloud CodingPlan" + +#: src/iac_code/providers/registry.py:428 +msgid "Alibaba Cloud CodingPlan (International)" +msgstr "Alibaba Cloud CodingPlan (International)" + +#: src/iac_code/providers/registry.py:429 +msgid "ZhiPu AI CodingPlan" +msgstr "ZhiPu AI CodingPlan" + +#: src/iac_code/providers/registry.py:430 +msgid "ZhiPu AI CodingPlan (International)" +msgstr "ZhiPu AI CodingPlan (International)" + +#: src/iac_code/providers/registry.py:431 +msgid "Volcengine CodingPlan" +msgstr "Volcengine CodingPlan" + +#: src/iac_code/providers/registry.py:432 +msgid "Anthropic Compatible" +msgstr "Compatible Anthropic" + +#: src/iac_code/services/qwenpaw_source.py:208 +#, python-brace-format +msgid "" +"[QwenPaw mode] Unknown provider '{provider_id}'. iac-code does not " +"support this provider.\n" +"Supported QwenPaw provider IDs: {supported_ids}\n" +"To fix: switch to a supported provider in QwenPaw, or disable QwenPaw " +"mode (remove 'llm_source: qwenpaw' from settings.yml)." +msgstr "" +"[Mode QwenPaw] Fournisseur inconnu '{provider_id}'. iac-code ne prend pas" +" en charge ce fournisseur.\n" +"IDs de fournisseurs QwenPaw pris en charge : {supported_ids}\n" +"Solution : passez à un fournisseur pris en charge dans QwenPaw, ou " +"désactivez le mode QwenPaw (supprimez 'llm_source: qwenpaw' de " +"settings.yml)." + #: src/iac_code/skills/skill_tool.py:130 #, python-brace-format msgid "Skill '{name}' loaded (inline)." @@ -1024,47 +1179,23 @@ msgstr "ROS Stack" msgid "CloudStackInstances" msgstr "CloudStackInstances" -#: src/iac_code/ui/banner.py:38 -msgid "DashScope" -msgstr "DashScope" - -#: src/iac_code/ui/banner.py:39 -msgid "DashScope Token Plan" -msgstr "DashScope Token Plan" - -#: src/iac_code/ui/banner.py:40 -msgid "OpenAI" -msgstr "OpenAI" - -#: src/iac_code/ui/banner.py:41 -msgid "Anthropic" -msgstr "Anthropic" - -#: src/iac_code/ui/banner.py:42 -msgid "DeepSeek" -msgstr "DeepSeek" - -#: src/iac_code/ui/banner.py:43 -msgid "OpenAPI Compatible" -msgstr "Compatible OpenAPI" - -#: src/iac_code/ui/banner.py:67 +#: src/iac_code/ui/banner.py:64 msgid "Your AI-powered Infrastructure as Code assistant" msgstr "Votre assistant Infrastructure as Code assisté par IA" -#: src/iac_code/ui/banner.py:91 +#: src/iac_code/ui/banner.py:88 msgid "Welcome back" msgstr "Bon retour" -#: src/iac_code/ui/banner.py:97 +#: src/iac_code/ui/banner.py:94 msgid "Session" msgstr "Session" -#: src/iac_code/ui/banner.py:107 +#: src/iac_code/ui/banner.py:104 msgid "Debug mode" msgstr "Mode debug" -#: src/iac_code/ui/banner.py:108 +#: src/iac_code/ui/banner.py:105 msgid "Log file" msgstr "Fichier journal" @@ -1151,43 +1282,43 @@ msgstr "par défaut" msgid "No, always reject this tool" msgstr "Non, toujours refuser cet outil" -#: src/iac_code/ui/repl.py:311 +#: src/iac_code/ui/repl.py:319 msgid "Press Ctrl+C again to exit." msgstr "Appuyez de nouveau sur Ctrl+C pour quitter." -#: src/iac_code/ui/repl.py:327 +#: src/iac_code/ui/repl.py:335 msgid "Interrupted." msgstr "Interrompu." -#: src/iac_code/ui/repl.py:364 +#: src/iac_code/ui/repl.py:372 msgid "Goodbye!" msgstr "Au revoir !" -#: src/iac_code/ui/repl.py:365 +#: src/iac_code/ui/repl.py:373 msgid "Resume this session with:" msgstr "Pour reprendre cette session :" -#: src/iac_code/ui/repl.py:447 +#: src/iac_code/ui/repl.py:455 #, python-brace-format msgid "Unknown command: /{name}. Type /help for available commands." msgstr "Commande inconnue : /{name}. Saisissez /help pour la liste des commandes." -#: src/iac_code/ui/repl.py:471 src/iac_code/ui/repl.py:503 +#: src/iac_code/ui/repl.py:479 src/iac_code/ui/repl.py:511 #, python-brace-format msgid "Command error: {error}" msgstr "Erreur de commande : {error}" -#: src/iac_code/ui/repl.py:478 +#: src/iac_code/ui/repl.py:486 #, python-brace-format msgid "Command has no handler: {name}" msgstr "Aucun gestionnaire pour la commande : {name}" -#: src/iac_code/ui/repl.py:634 +#: src/iac_code/ui/repl.py:689 #, python-brace-format msgid "Session not found: {session_id}" msgstr "Session introuvable : {session_id}" -#: src/iac_code/ui/repl.py:653 +#: src/iac_code/ui/repl.py:708 #, python-brace-format msgid "" "This session belongs to a different directory.\n" @@ -1198,15 +1329,15 @@ msgstr "" "Pour la reprendre, exécutez :\n" " {cmd}" -#: src/iac_code/ui/repl.py:692 +#: src/iac_code/ui/repl.py:747 msgid "This conversation is from a different directory." msgstr "Cette conversation provient d’un autre répertoire." -#: src/iac_code/ui/repl.py:694 +#: src/iac_code/ui/repl.py:749 msgid "To resume, run:" msgstr "Pour reprendre, exécutez :" -#: src/iac_code/ui/repl.py:699 +#: src/iac_code/ui/repl.py:754 msgid "(Command copied to clipboard)" msgstr "(Commande copiée dans le presse-papiers)" @@ -1383,3 +1514,6 @@ msgstr "il y a {n} heure{s}" msgid "{n} day{s} ago" msgstr "il y a {n} jour{s}" +#~ msgid "DashScope Token Plan" +#~ msgstr "DashScope Token Plan" + diff --git a/src/iac_code/i18n/locales/ja/LC_MESSAGES/messages.po b/src/iac_code/i18n/locales/ja/LC_MESSAGES/messages.po index fb4066dc..31762675 100644 --- a/src/iac_code/i18n/locales/ja/LC_MESSAGES/messages.po +++ b/src/iac_code/i18n/locales/ja/LC_MESSAGES/messages.po @@ -4,9 +4,9 @@ # msgid "" msgstr "" -"Project-Id-Version: iac-code 0.1.1\n" +"Project-Id-Version: iac-code 0.1.2\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-05-15 11:19+0800\n" +"POT-Creation-Date: 2026-05-15 17:58+0800\n" "PO-Revision-Date: 2026-05-13 00:00+0000\n" "Last-Translator: \n" "Language: ja\n" @@ -126,69 +126,69 @@ msgstr "" " ドキュメント:https://aliyun.github.io/iac-" "code/ja/docs/configuration/authentication\n" -#: src/iac_code/cli/main.py:22 src/iac_code/commands/help.py:26 +#: src/iac_code/cli/main.py:23 src/iac_code/commands/help.py:26 msgid "AI-powered infrastructure orchestration tool" msgstr "AI 駆動のインフラストラクチャ・オーケストレーションツール" -#: src/iac_code/cli/main.py:33 +#: src/iac_code/cli/main.py:34 msgid "LLM model to use" msgstr "使用する LLM モデル" -#: src/iac_code/cli/main.py:34 +#: src/iac_code/cli/main.py:35 msgid "Non-interactive mode: run a single prompt and exit" msgstr "非対話モード:単一のプロンプトを実行して終了します" -#: src/iac_code/cli/main.py:35 +#: src/iac_code/cli/main.py:36 msgid "Output format: text, json, stream-json" msgstr "出力形式:text、json、stream-json" -#: src/iac_code/cli/main.py:36 +#: src/iac_code/cli/main.py:37 msgid "Maximum agent turns in headless mode" msgstr "ヘッドレスモードでの最大エージェンターン数" -#: src/iac_code/cli/main.py:37 src/iac_code/cli/main.py:236 +#: src/iac_code/cli/main.py:38 src/iac_code/cli/main.py:241 msgid "Enable debug logging" msgstr "デバッグログを有効にする" -#: src/iac_code/cli/main.py:38 +#: src/iac_code/cli/main.py:39 msgid "Show version and exit" msgstr "バージョンを表示して終了する" -#: src/iac_code/cli/main.py:39 +#: src/iac_code/cli/main.py:40 msgid "Resume a session by ID" msgstr "ID でセッションを再開する" -#: src/iac_code/cli/main.py:40 +#: src/iac_code/cli/main.py:41 msgid "Resume the most recent session" msgstr "直近のセッションを再開する" -#: src/iac_code/cli/main.py:47 src/iac_code/i18n/__init__.py:54 +#: src/iac_code/cli/main.py:48 src/iac_code/i18n/__init__.py:54 msgid "Install completion for the current shell." msgstr "現在の shell に補完をインストールします。" -#: src/iac_code/cli/main.py:55 src/iac_code/i18n/__init__.py:55 +#: src/iac_code/cli/main.py:56 src/iac_code/i18n/__init__.py:55 msgid "" "Show completion for the current shell, to copy it or customize the " "installation." msgstr "現在の shell 向けの補完スクリプトを表示します。コピーしたり、インストールをカスタマイズしたりできます。" -#: src/iac_code/cli/main.py:69 +#: src/iac_code/cli/main.py:70 msgid "Error: --resume and --continue cannot be used together." msgstr "エラー:--resume と --continue は同時に使用できません。" -#: src/iac_code/cli/main.py:231 +#: src/iac_code/cli/main.py:236 msgid "Run iac-code as an ACP server." msgstr "iac-code を ACP サーバーとして実行します。" -#: src/iac_code/cli/main.py:233 +#: src/iac_code/cli/main.py:238 msgid "Transport type: stdio or http" msgstr "トランスポートの種類:stdio または http" -#: src/iac_code/cli/main.py:234 +#: src/iac_code/cli/main.py:239 msgid "HTTP server port" msgstr "HTTP サーバーのポート" -#: src/iac_code/cli/main.py:235 +#: src/iac_code/cli/main.py:240 msgid "HTTP server host" msgstr "HTTP サーバーのホスト" @@ -236,104 +236,162 @@ msgstr "以前のセッションを再開します" msgid "[conversation id or search term]" msgstr "[会話 ID または検索語]" -#: src/iac_code/commands/auth.py:304 src/iac_code/commands/auth.py:817 +#: src/iac_code/commands/auth.py:245 src/iac_code/commands/auth.py:817 #: src/iac_code/ui/core/prompt_input.py:373 msgid "Navigate" msgstr "移動" -#: src/iac_code/commands/auth.py:304 src/iac_code/commands/auth.py:426 -#: src/iac_code/commands/auth.py:460 src/iac_code/commands/auth.py:467 -#: src/iac_code/commands/auth.py:491 src/iac_code/commands/auth.py:817 +#: src/iac_code/commands/auth.py:245 src/iac_code/commands/auth.py:367 +#: src/iac_code/commands/auth.py:401 src/iac_code/commands/auth.py:408 +#: src/iac_code/commands/auth.py:432 src/iac_code/commands/auth.py:817 #: src/iac_code/commands/auth.py:1007 src/iac_code/ui/core/prompt_input.py:373 msgid "Confirm" msgstr "確認" -#: src/iac_code/commands/auth.py:304 src/iac_code/commands/auth.py:424 -#: src/iac_code/commands/auth.py:426 src/iac_code/commands/auth.py:460 -#: src/iac_code/commands/auth.py:467 src/iac_code/commands/auth.py:491 +#: src/iac_code/commands/auth.py:245 src/iac_code/commands/auth.py:365 +#: src/iac_code/commands/auth.py:367 src/iac_code/commands/auth.py:401 +#: src/iac_code/commands/auth.py:408 src/iac_code/commands/auth.py:432 #: src/iac_code/commands/auth.py:817 src/iac_code/commands/auth.py:902 #: src/iac_code/commands/auth.py:1007 msgid "Back" msgstr "戻る" -#: src/iac_code/commands/auth.py:424 +#: src/iac_code/commands/auth.py:365 msgid "Keep" msgstr "維持" -#: src/iac_code/commands/auth.py:424 +#: src/iac_code/commands/auth.py:365 msgid "Re-enter" msgstr "再入力" -#: src/iac_code/commands/auth.py:566 src/iac_code/commands/auth.py:667 +#: src/iac_code/commands/auth.py:507 src/iac_code/commands/auth.py:631 +#: src/iac_code/commands/auth.py:651 msgid " (current)" msgstr " (現在)" -#: src/iac_code/commands/auth.py:569 +#: src/iac_code/commands/auth.py:510 msgid "Custom model..." msgstr "カスタムモデル…" -#: src/iac_code/commands/auth.py:572 +#: src/iac_code/commands/auth.py:513 #, python-brace-format msgid "Select model for {provider}" msgstr "{provider} のモデルを選択してください" -#: src/iac_code/commands/auth.py:574 +#: src/iac_code/commands/auth.py:515 msgid "Select model" msgstr "モデルを選択" -#: src/iac_code/commands/auth.py:582 +#: src/iac_code/commands/auth.py:523 msgid "Enter custom model name: " msgstr "カスタムモデル名を入力してください:" -#: src/iac_code/commands/auth.py:608 +#: src/iac_code/commands/auth.py:549 msgid "Error: console not available" msgstr "エラー:コンソールを使用できません" -#: src/iac_code/commands/auth.py:635 +#: src/iac_code/commands/auth.py:576 msgid "Configure LLM Provider" msgstr "LLM プロバイダーを設定" -#: src/iac_code/commands/auth.py:636 +#: src/iac_code/commands/auth.py:577 msgid "Configure IaC Cloud Service" msgstr "IaC クラウドサービスを設定" -#: src/iac_code/commands/auth.py:638 +#: src/iac_code/commands/auth.py:579 msgid "Select configuration type" msgstr "設定の種類を選択" -#: src/iac_code/commands/auth.py:640 src/iac_code/commands/auth.py:694 -#: src/iac_code/commands/auth.py:709 src/iac_code/commands/auth.py:770 +#: src/iac_code/commands/auth.py:581 src/iac_code/commands/auth.py:676 +#: src/iac_code/commands/auth.py:692 src/iac_code/commands/auth.py:770 #: src/iac_code/commands/auth.py:943 src/iac_code/commands/auth.py:984 msgid "Auth cancelled" msgstr "認証をキャンセルしました" -#: src/iac_code/commands/auth.py:676 +#: src/iac_code/commands/auth.py:635 msgid "Select provider" msgstr "プロバイダーを選択" -#: src/iac_code/commands/auth.py:687 +#: src/iac_code/commands/auth.py:656 src/iac_code/commands/auth.py:745 +#, python-brace-format +msgid "Select provider — {group}" +msgstr "プロバイダーを選択 — {group}" + +#: src/iac_code/commands/auth.py:669 #, python-brace-format msgid "Configure {provider}" msgstr "{provider} を設定" -#: src/iac_code/commands/auth.py:702 +#: src/iac_code/commands/auth.py:685 #, python-brace-format msgid "Enter API key for {provider}" msgstr "{provider} の API key を入力してください" -#: src/iac_code/commands/auth.py:742 +#: src/iac_code/commands/auth.py:723 #, python-brace-format msgid "{status}: {provider} / {model}" msgstr "{status}:{provider} / {model}" -#: src/iac_code/commands/auth.py:743 +#: src/iac_code/commands/auth.py:724 msgid "Configured" msgstr "設定済み" -#: src/iac_code/commands/auth.py:752 +#: src/iac_code/commands/auth.py:731 src/iac_code/commands/auth.py:752 msgid "Alibaba Cloud" msgstr "Alibaba Cloud" +#: src/iac_code/commands/auth.py:732 src/iac_code/providers/registry.py:417 +msgid "ZhiPu AI" +msgstr "ZhiPu AI" + +#: src/iac_code/commands/auth.py:733 +msgid "Kimi" +msgstr "Kimi" + +#: src/iac_code/commands/auth.py:734 +msgid "MiniMax" +msgstr "MiniMax" + +#: src/iac_code/commands/auth.py:735 src/iac_code/providers/registry.py:419 +msgid "Volcengine" +msgstr "Volcengine" + +#: src/iac_code/commands/auth.py:736 +msgid "SiliconFlow" +msgstr "SiliconFlow" + +#: src/iac_code/commands/auth.py:737 src/iac_code/providers/registry.py:410 +msgid "DeepSeek" +msgstr "DeepSeek" + +#: src/iac_code/commands/auth.py:738 src/iac_code/providers/registry.py:408 +msgid "OpenAI" +msgstr "OpenAI" + +#: src/iac_code/commands/auth.py:739 src/iac_code/providers/registry.py:409 +msgid "Anthropic" +msgstr "Anthropic" + +#: src/iac_code/commands/auth.py:740 src/iac_code/providers/registry.py:412 +msgid "Google Gemini" +msgstr "Google Gemini" + +#: src/iac_code/commands/auth.py:741 src/iac_code/providers/registry.py:425 +msgid "Azure OpenAI" +msgstr "Azure OpenAI" + +#: src/iac_code/commands/auth.py:742 src/iac_code/providers/registry.py:424 +msgid "OpenRouter" +msgstr "OpenRouter" + +#: src/iac_code/commands/auth.py:743 +msgid "Local" +msgstr "ローカル" + +#: src/iac_code/commands/auth.py:744 +msgid "Compatible" +msgstr "互換モード" + #: src/iac_code/commands/auth.py:761 msgid "Select Cloud Provider" msgstr "クラウドプロバイダーを選択" @@ -550,19 +608,24 @@ msgstr "(動的)" msgid "Aborted!" msgstr "中断しました。" -#: src/iac_code/providers/manager.py:80 +#: src/iac_code/providers/manager.py:78 #, python-brace-format msgid "Cannot determine provider for model: {model}. Run /auth to configure." msgstr "モデル {model} のプロバイダーを特定できません。/auth を実行して設定してください。" -#: src/iac_code/providers/manager.py:88 +#: src/iac_code/providers/manager.py:95 +#, python-brace-format +msgid "Unknown provider key: '{key}'. Run /auth to configure." +msgstr "不明なプロバイダーキー:'{key}'。/auth を実行して設定してください。" + +#: src/iac_code/providers/manager.py:100 #, python-brace-format msgid "" "No API key configured for provider '{provider}' (model: {model}). Run " "/auth to configure." msgstr "プロバイダー '{provider}' の API キーが設定されていません(モデル: {model})。/auth を実行して設定してください。" -#: src/iac_code/providers/openai_provider.py:286 +#: src/iac_code/providers/openai_provider.py:289 #, python-brace-format msgid "" "API returned no data. Please check that your API Base URL is correct " @@ -572,7 +635,7 @@ msgstr "" "API からデータが返りませんでした。API Base URL が正しいか確認してください(現在:{base_url})。 多くの OpenAI " "互換エンドポイントでは /v1 接尾辞が必要です(例:{base_url}/v1)。" -#: src/iac_code/providers/openai_provider.py:327 +#: src/iac_code/providers/openai_provider.py:330 #, python-brace-format msgid "" "API returned an invalid response. Please check that your API Base URL is " @@ -582,6 +645,97 @@ msgstr "" "API から無効な応答が返りました。API Base URL が正しいか確認してください(現在:{base_url})。 多くの OpenAI " "互換エンドポイントでは /v1 接尾辞が必要です(例:{base_url}/v1)。" +#: src/iac_code/providers/registry.py:406 +msgid "Alibaba Cloud Bailian" +msgstr "Alibaba Cloud 百錬" + +#: src/iac_code/providers/registry.py:407 +msgid "Alibaba Cloud Bailian Token Plan" +msgstr "Alibaba Cloud 百錬 Token Plan" + +#: src/iac_code/providers/registry.py:411 +msgid "OpenAPI Compatible" +msgstr "OpenAPI 互換" + +#: src/iac_code/providers/registry.py:413 +msgid "Kimi (China)" +msgstr "Kimi(中国版)" + +#: src/iac_code/providers/registry.py:414 +msgid "Kimi (International)" +msgstr "Kimi(国際版)" + +#: src/iac_code/providers/registry.py:415 +msgid "MiniMax (China)" +msgstr "MiniMax(中国版)" + +#: src/iac_code/providers/registry.py:416 +msgid "MiniMax (International)" +msgstr "MiniMax(国際版)" + +#: src/iac_code/providers/registry.py:418 +msgid "ZhiPu AI (International)" +msgstr "ZhiPu AI(国際版)" + +#: src/iac_code/providers/registry.py:420 +msgid "SiliconFlow (China)" +msgstr "SiliconFlow(中国版)" + +#: src/iac_code/providers/registry.py:421 +msgid "SiliconFlow (International)" +msgstr "SiliconFlow(国際版)" + +#: src/iac_code/providers/registry.py:422 +msgid "Ollama (Local)" +msgstr "Ollama(ローカル)" + +#: src/iac_code/providers/registry.py:423 +msgid "LM Studio (Local)" +msgstr "LM Studio(ローカル)" + +#: src/iac_code/providers/registry.py:426 +msgid "ModelScope" +msgstr "ModelScope" + +#: src/iac_code/providers/registry.py:427 +msgid "Alibaba Cloud CodingPlan" +msgstr "Alibaba Cloud CodingPlan" + +#: src/iac_code/providers/registry.py:428 +msgid "Alibaba Cloud CodingPlan (International)" +msgstr "Alibaba Cloud CodingPlan(国際版)" + +#: src/iac_code/providers/registry.py:429 +msgid "ZhiPu AI CodingPlan" +msgstr "ZhiPu AI CodingPlan" + +#: src/iac_code/providers/registry.py:430 +msgid "ZhiPu AI CodingPlan (International)" +msgstr "ZhiPu AI CodingPlan(国際版)" + +#: src/iac_code/providers/registry.py:431 +msgid "Volcengine CodingPlan" +msgstr "Volcengine CodingPlan" + +#: src/iac_code/providers/registry.py:432 +msgid "Anthropic Compatible" +msgstr "Anthropic 互換" + +#: src/iac_code/services/qwenpaw_source.py:208 +#, python-brace-format +msgid "" +"[QwenPaw mode] Unknown provider '{provider_id}'. iac-code does not " +"support this provider.\n" +"Supported QwenPaw provider IDs: {supported_ids}\n" +"To fix: switch to a supported provider in QwenPaw, or disable QwenPaw " +"mode (remove 'llm_source: qwenpaw' from settings.yml)." +msgstr "" +"[QwenPaw モード] プロバイダー '{provider_id}' を認識できません。iac-code " +"はこのプロバイダーをサポートしていません。\n" +"サポートされている QwenPaw プロバイダー ID:{supported_ids}\n" +"対処法:QwenPaw でサポートされているプロバイダーに切り替えるか、QwenPaw モードを無効にしてください(settings.yml から" +" 'llm_source: qwenpaw' を削除)。" + #: src/iac_code/skills/skill_tool.py:130 #, python-brace-format msgid "Skill '{name}' loaded (inline)." @@ -1003,47 +1157,23 @@ msgstr "ROS スタック" msgid "CloudStackInstances" msgstr "CloudStackInstances" -#: src/iac_code/ui/banner.py:38 -msgid "DashScope" -msgstr "DashScope" - -#: src/iac_code/ui/banner.py:39 -msgid "DashScope Token Plan" -msgstr "DashScope Token Plan" - -#: src/iac_code/ui/banner.py:40 -msgid "OpenAI" -msgstr "OpenAI" - -#: src/iac_code/ui/banner.py:41 -msgid "Anthropic" -msgstr "Anthropic" - -#: src/iac_code/ui/banner.py:42 -msgid "DeepSeek" -msgstr "DeepSeek" - -#: src/iac_code/ui/banner.py:43 -msgid "OpenAPI Compatible" -msgstr "OpenAPI 互換" - -#: src/iac_code/ui/banner.py:67 +#: src/iac_code/ui/banner.py:64 msgid "Your AI-powered Infrastructure as Code assistant" msgstr "あなたの AI 駆動の Infrastructure as Code アシスタントです" -#: src/iac_code/ui/banner.py:91 +#: src/iac_code/ui/banner.py:88 msgid "Welcome back" msgstr "おかえりなさい" -#: src/iac_code/ui/banner.py:97 +#: src/iac_code/ui/banner.py:94 msgid "Session" msgstr "セッション" -#: src/iac_code/ui/banner.py:107 +#: src/iac_code/ui/banner.py:104 msgid "Debug mode" msgstr "デバッグモード" -#: src/iac_code/ui/banner.py:108 +#: src/iac_code/ui/banner.py:105 msgid "Log file" msgstr "ログファイル" @@ -1130,43 +1260,43 @@ msgstr "既定" msgid "No, always reject this tool" msgstr "いいえ、このツールは常に拒否" -#: src/iac_code/ui/repl.py:311 +#: src/iac_code/ui/repl.py:319 msgid "Press Ctrl+C again to exit." msgstr "終了するには Ctrl+C をもう一度押してください。" -#: src/iac_code/ui/repl.py:327 +#: src/iac_code/ui/repl.py:335 msgid "Interrupted." msgstr "中断しました。" -#: src/iac_code/ui/repl.py:364 +#: src/iac_code/ui/repl.py:372 msgid "Goodbye!" msgstr "さようなら。" -#: src/iac_code/ui/repl.py:365 +#: src/iac_code/ui/repl.py:373 msgid "Resume this session with:" msgstr "このセッションを再開するには次を実行してください:" -#: src/iac_code/ui/repl.py:447 +#: src/iac_code/ui/repl.py:455 #, python-brace-format msgid "Unknown command: /{name}. Type /help for available commands." msgstr "不明なコマンドです:/{name}。利用可能なコマンドは /help を入力してください。" -#: src/iac_code/ui/repl.py:471 src/iac_code/ui/repl.py:503 +#: src/iac_code/ui/repl.py:479 src/iac_code/ui/repl.py:511 #, python-brace-format msgid "Command error: {error}" msgstr "コマンドエラー:{error}" -#: src/iac_code/ui/repl.py:478 +#: src/iac_code/ui/repl.py:486 #, python-brace-format msgid "Command has no handler: {name}" msgstr "ハンドラーがないコマンドです:{name}" -#: src/iac_code/ui/repl.py:634 +#: src/iac_code/ui/repl.py:689 #, python-brace-format msgid "Session not found: {session_id}" msgstr "セッションが見つかりません:{session_id}" -#: src/iac_code/ui/repl.py:653 +#: src/iac_code/ui/repl.py:708 #, python-brace-format msgid "" "This session belongs to a different directory.\n" @@ -1177,15 +1307,15 @@ msgstr "" "再開するには次を実行してください:\n" " {cmd}" -#: src/iac_code/ui/repl.py:692 +#: src/iac_code/ui/repl.py:747 msgid "This conversation is from a different directory." msgstr "この会話は別のディレクトリ由来です。" -#: src/iac_code/ui/repl.py:694 +#: src/iac_code/ui/repl.py:749 msgid "To resume, run:" msgstr "再開するには次を実行してください:" -#: src/iac_code/ui/repl.py:699 +#: src/iac_code/ui/repl.py:754 msgid "(Command copied to clipboard)" msgstr "(コマンドをクリップボードにコピーしました)" @@ -1386,3 +1516,6 @@ msgstr "{n} 日{s}前" #~ msgid "Choose [1-4]: " #~ msgstr "[1-4] を選択してください:" +#~ msgid "DashScope Token Plan" +#~ msgstr "DashScope Token Plan" + diff --git a/src/iac_code/i18n/locales/pt/LC_MESSAGES/messages.po b/src/iac_code/i18n/locales/pt/LC_MESSAGES/messages.po index e33f7d74..b9049324 100644 --- a/src/iac_code/i18n/locales/pt/LC_MESSAGES/messages.po +++ b/src/iac_code/i18n/locales/pt/LC_MESSAGES/messages.po @@ -4,9 +4,9 @@ # msgid "" msgstr "" -"Project-Id-Version: iac-code 0.1.1\n" +"Project-Id-Version: iac-code 0.1.2\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-05-15 11:19+0800\n" +"POT-Creation-Date: 2026-05-15 17:58+0800\n" "PO-Revision-Date: 2026-05-13 00:00+0000\n" "Last-Translator: \n" "Language: pt\n" @@ -130,47 +130,47 @@ msgstr "" " Documentação: https://aliyun.github.io/iac-" "code/pt/docs/configuration/authentication\n" -#: src/iac_code/cli/main.py:22 src/iac_code/commands/help.py:26 +#: src/iac_code/cli/main.py:23 src/iac_code/commands/help.py:26 msgid "AI-powered infrastructure orchestration tool" msgstr "Ferramenta de orquestração de infraestrutura com IA" -#: src/iac_code/cli/main.py:33 +#: src/iac_code/cli/main.py:34 msgid "LLM model to use" msgstr "Modelo LLM a utilizar" -#: src/iac_code/cli/main.py:34 +#: src/iac_code/cli/main.py:35 msgid "Non-interactive mode: run a single prompt and exit" msgstr "Modo não interativo: executa um único prompt e encerra" -#: src/iac_code/cli/main.py:35 +#: src/iac_code/cli/main.py:36 msgid "Output format: text, json, stream-json" msgstr "Formato de saída: text, json, stream-json" -#: src/iac_code/cli/main.py:36 +#: src/iac_code/cli/main.py:37 msgid "Maximum agent turns in headless mode" msgstr "Número máximo de passos do agent em modo headless" -#: src/iac_code/cli/main.py:37 src/iac_code/cli/main.py:236 +#: src/iac_code/cli/main.py:38 src/iac_code/cli/main.py:241 msgid "Enable debug logging" msgstr "Ativar debug" -#: src/iac_code/cli/main.py:38 +#: src/iac_code/cli/main.py:39 msgid "Show version and exit" msgstr "Mostrar versão e sair" -#: src/iac_code/cli/main.py:39 +#: src/iac_code/cli/main.py:40 msgid "Resume a session by ID" msgstr "Retomar uma sessão pelo ID" -#: src/iac_code/cli/main.py:40 +#: src/iac_code/cli/main.py:41 msgid "Resume the most recent session" msgstr "Retomar a sessão mais recente" -#: src/iac_code/cli/main.py:47 src/iac_code/i18n/__init__.py:54 +#: src/iac_code/cli/main.py:48 src/iac_code/i18n/__init__.py:54 msgid "Install completion for the current shell." msgstr "Instalar completion para o shell atual." -#: src/iac_code/cli/main.py:55 src/iac_code/i18n/__init__.py:55 +#: src/iac_code/cli/main.py:56 src/iac_code/i18n/__init__.py:55 msgid "" "Show completion for the current shell, to copy it or customize the " "installation." @@ -178,23 +178,23 @@ msgstr "" "Exibir o completion do shell atual, para copiar ou personalizar a " "instalação." -#: src/iac_code/cli/main.py:69 +#: src/iac_code/cli/main.py:70 msgid "Error: --resume and --continue cannot be used together." msgstr "Erro: --resume e --continue não podem ser usados juntos." -#: src/iac_code/cli/main.py:231 +#: src/iac_code/cli/main.py:236 msgid "Run iac-code as an ACP server." msgstr "Executar o iac-code como servidor ACP." -#: src/iac_code/cli/main.py:233 +#: src/iac_code/cli/main.py:238 msgid "Transport type: stdio or http" msgstr "Tipo de transporte: stdio ou http" -#: src/iac_code/cli/main.py:234 +#: src/iac_code/cli/main.py:239 msgid "HTTP server port" msgstr "Porta do servidor HTTP" -#: src/iac_code/cli/main.py:235 +#: src/iac_code/cli/main.py:240 msgid "HTTP server host" msgstr "Host do servidor HTTP" @@ -242,104 +242,162 @@ msgstr "Retomar uma sessão anterior" msgid "[conversation id or search term]" msgstr "[ID da conversa ou termo de busca]" -#: src/iac_code/commands/auth.py:304 src/iac_code/commands/auth.py:817 +#: src/iac_code/commands/auth.py:245 src/iac_code/commands/auth.py:817 #: src/iac_code/ui/core/prompt_input.py:373 msgid "Navigate" msgstr "Navegar" -#: src/iac_code/commands/auth.py:304 src/iac_code/commands/auth.py:426 -#: src/iac_code/commands/auth.py:460 src/iac_code/commands/auth.py:467 -#: src/iac_code/commands/auth.py:491 src/iac_code/commands/auth.py:817 +#: src/iac_code/commands/auth.py:245 src/iac_code/commands/auth.py:367 +#: src/iac_code/commands/auth.py:401 src/iac_code/commands/auth.py:408 +#: src/iac_code/commands/auth.py:432 src/iac_code/commands/auth.py:817 #: src/iac_code/commands/auth.py:1007 src/iac_code/ui/core/prompt_input.py:373 msgid "Confirm" msgstr "Confirmar" -#: src/iac_code/commands/auth.py:304 src/iac_code/commands/auth.py:424 -#: src/iac_code/commands/auth.py:426 src/iac_code/commands/auth.py:460 -#: src/iac_code/commands/auth.py:467 src/iac_code/commands/auth.py:491 +#: src/iac_code/commands/auth.py:245 src/iac_code/commands/auth.py:365 +#: src/iac_code/commands/auth.py:367 src/iac_code/commands/auth.py:401 +#: src/iac_code/commands/auth.py:408 src/iac_code/commands/auth.py:432 #: src/iac_code/commands/auth.py:817 src/iac_code/commands/auth.py:902 #: src/iac_code/commands/auth.py:1007 msgid "Back" msgstr "Voltar" -#: src/iac_code/commands/auth.py:424 +#: src/iac_code/commands/auth.py:365 msgid "Keep" msgstr "Manter" -#: src/iac_code/commands/auth.py:424 +#: src/iac_code/commands/auth.py:365 msgid "Re-enter" msgstr "Digitar novamente" -#: src/iac_code/commands/auth.py:566 src/iac_code/commands/auth.py:667 +#: src/iac_code/commands/auth.py:507 src/iac_code/commands/auth.py:631 +#: src/iac_code/commands/auth.py:651 msgid " (current)" msgstr " (atual)" -#: src/iac_code/commands/auth.py:569 +#: src/iac_code/commands/auth.py:510 msgid "Custom model..." msgstr "Modelo personalizado..." -#: src/iac_code/commands/auth.py:572 +#: src/iac_code/commands/auth.py:513 #, python-brace-format msgid "Select model for {provider}" msgstr "Selecionar modelo para {provider}" -#: src/iac_code/commands/auth.py:574 +#: src/iac_code/commands/auth.py:515 msgid "Select model" msgstr "Selecionar modelo" -#: src/iac_code/commands/auth.py:582 +#: src/iac_code/commands/auth.py:523 msgid "Enter custom model name: " msgstr "Informe o nome do modelo personalizado: " -#: src/iac_code/commands/auth.py:608 +#: src/iac_code/commands/auth.py:549 msgid "Error: console not available" msgstr "Erro: console indisponível" -#: src/iac_code/commands/auth.py:635 +#: src/iac_code/commands/auth.py:576 msgid "Configure LLM Provider" msgstr "Configurar provedor LLM" -#: src/iac_code/commands/auth.py:636 +#: src/iac_code/commands/auth.py:577 msgid "Configure IaC Cloud Service" msgstr "Configurar serviço de nuvem IaC" -#: src/iac_code/commands/auth.py:638 +#: src/iac_code/commands/auth.py:579 msgid "Select configuration type" msgstr "Selecionar tipo de configuração" -#: src/iac_code/commands/auth.py:640 src/iac_code/commands/auth.py:694 -#: src/iac_code/commands/auth.py:709 src/iac_code/commands/auth.py:770 +#: src/iac_code/commands/auth.py:581 src/iac_code/commands/auth.py:676 +#: src/iac_code/commands/auth.py:692 src/iac_code/commands/auth.py:770 #: src/iac_code/commands/auth.py:943 src/iac_code/commands/auth.py:984 msgid "Auth cancelled" msgstr "Autenticação cancelada" -#: src/iac_code/commands/auth.py:676 +#: src/iac_code/commands/auth.py:635 msgid "Select provider" msgstr "Selecionar provedor" -#: src/iac_code/commands/auth.py:687 +#: src/iac_code/commands/auth.py:656 src/iac_code/commands/auth.py:745 +#, python-brace-format +msgid "Select provider — {group}" +msgstr "Selecionar provedor — {group}" + +#: src/iac_code/commands/auth.py:669 #, python-brace-format msgid "Configure {provider}" msgstr "Configurar {provider}" -#: src/iac_code/commands/auth.py:702 +#: src/iac_code/commands/auth.py:685 #, python-brace-format msgid "Enter API key for {provider}" msgstr "Informe a API key para {provider}" -#: src/iac_code/commands/auth.py:742 +#: src/iac_code/commands/auth.py:723 #, python-brace-format msgid "{status}: {provider} / {model}" msgstr "{status}: {provider} / {model}" -#: src/iac_code/commands/auth.py:743 +#: src/iac_code/commands/auth.py:724 msgid "Configured" msgstr "Configurado" -#: src/iac_code/commands/auth.py:752 +#: src/iac_code/commands/auth.py:731 src/iac_code/commands/auth.py:752 msgid "Alibaba Cloud" msgstr "Alibaba Cloud" +#: src/iac_code/commands/auth.py:732 src/iac_code/providers/registry.py:417 +msgid "ZhiPu AI" +msgstr "ZhiPu AI" + +#: src/iac_code/commands/auth.py:733 +msgid "Kimi" +msgstr "Kimi" + +#: src/iac_code/commands/auth.py:734 +msgid "MiniMax" +msgstr "MiniMax" + +#: src/iac_code/commands/auth.py:735 src/iac_code/providers/registry.py:419 +msgid "Volcengine" +msgstr "Volcengine" + +#: src/iac_code/commands/auth.py:736 +msgid "SiliconFlow" +msgstr "SiliconFlow" + +#: src/iac_code/commands/auth.py:737 src/iac_code/providers/registry.py:410 +msgid "DeepSeek" +msgstr "DeepSeek" + +#: src/iac_code/commands/auth.py:738 src/iac_code/providers/registry.py:408 +msgid "OpenAI" +msgstr "OpenAI" + +#: src/iac_code/commands/auth.py:739 src/iac_code/providers/registry.py:409 +msgid "Anthropic" +msgstr "Anthropic" + +#: src/iac_code/commands/auth.py:740 src/iac_code/providers/registry.py:412 +msgid "Google Gemini" +msgstr "Google Gemini" + +#: src/iac_code/commands/auth.py:741 src/iac_code/providers/registry.py:425 +msgid "Azure OpenAI" +msgstr "Azure OpenAI" + +#: src/iac_code/commands/auth.py:742 src/iac_code/providers/registry.py:424 +msgid "OpenRouter" +msgstr "OpenRouter" + +#: src/iac_code/commands/auth.py:743 +msgid "Local" +msgstr "Local" + +#: src/iac_code/commands/auth.py:744 +msgid "Compatible" +msgstr "Compatível" + #: src/iac_code/commands/auth.py:761 msgid "Select Cloud Provider" msgstr "Selecionar provedor de nuvem" @@ -556,14 +614,19 @@ msgstr "(dinâmico)" msgid "Aborted!" msgstr "Abortado!" -#: src/iac_code/providers/manager.py:80 +#: src/iac_code/providers/manager.py:78 #, python-brace-format msgid "Cannot determine provider for model: {model}. Run /auth to configure." msgstr "" "Não é possível determinar o provedor para o modelo: {model}. Execute " "/auth para configurar." -#: src/iac_code/providers/manager.py:88 +#: src/iac_code/providers/manager.py:95 +#, python-brace-format +msgid "Unknown provider key: '{key}'. Run /auth to configure." +msgstr "Chave de provedor desconhecida: '{key}'. Execute /auth para configurar." + +#: src/iac_code/providers/manager.py:100 #, python-brace-format msgid "" "No API key configured for provider '{provider}' (model: {model}). Run " @@ -572,7 +635,7 @@ msgstr "" "Nenhuma chave API configurada para o provedor '{provider}' (modelo: " "{model}). Execute /auth para configurar." -#: src/iac_code/providers/openai_provider.py:286 +#: src/iac_code/providers/openai_provider.py:289 #, python-brace-format msgid "" "API returned no data. Please check that your API Base URL is correct " @@ -583,7 +646,7 @@ msgstr "" "(atual: {base_url}). Muitos endpoints compatíveis com OpenAI exigem o " "sufixo /v1 (por exemplo, {base_url}/v1)." -#: src/iac_code/providers/openai_provider.py:327 +#: src/iac_code/providers/openai_provider.py:330 #, python-brace-format msgid "" "API returned an invalid response. Please check that your API Base URL is " @@ -594,6 +657,97 @@ msgstr "" "correta (atual: {base_url}). Muitos endpoints compatíveis com OpenAI " "exigem o sufixo /v1 (por exemplo, {base_url}/v1)." +#: src/iac_code/providers/registry.py:406 +msgid "Alibaba Cloud Bailian" +msgstr "Alibaba Cloud Bailian" + +#: src/iac_code/providers/registry.py:407 +msgid "Alibaba Cloud Bailian Token Plan" +msgstr "Alibaba Cloud Bailian Token Plan" + +#: src/iac_code/providers/registry.py:411 +msgid "OpenAPI Compatible" +msgstr "Compatível com OpenAPI" + +#: src/iac_code/providers/registry.py:413 +msgid "Kimi (China)" +msgstr "Kimi (China)" + +#: src/iac_code/providers/registry.py:414 +msgid "Kimi (International)" +msgstr "Kimi (Internacional)" + +#: src/iac_code/providers/registry.py:415 +msgid "MiniMax (China)" +msgstr "MiniMax (China)" + +#: src/iac_code/providers/registry.py:416 +msgid "MiniMax (International)" +msgstr "MiniMax (Internacional)" + +#: src/iac_code/providers/registry.py:418 +msgid "ZhiPu AI (International)" +msgstr "ZhiPu AI (Internacional)" + +#: src/iac_code/providers/registry.py:420 +msgid "SiliconFlow (China)" +msgstr "SiliconFlow (China)" + +#: src/iac_code/providers/registry.py:421 +msgid "SiliconFlow (International)" +msgstr "SiliconFlow (Internacional)" + +#: src/iac_code/providers/registry.py:422 +msgid "Ollama (Local)" +msgstr "Ollama (Local)" + +#: src/iac_code/providers/registry.py:423 +msgid "LM Studio (Local)" +msgstr "LM Studio (Local)" + +#: src/iac_code/providers/registry.py:426 +msgid "ModelScope" +msgstr "ModelScope" + +#: src/iac_code/providers/registry.py:427 +msgid "Alibaba Cloud CodingPlan" +msgstr "Alibaba Cloud CodingPlan" + +#: src/iac_code/providers/registry.py:428 +msgid "Alibaba Cloud CodingPlan (International)" +msgstr "Alibaba Cloud CodingPlan (Internacional)" + +#: src/iac_code/providers/registry.py:429 +msgid "ZhiPu AI CodingPlan" +msgstr "ZhiPu AI CodingPlan" + +#: src/iac_code/providers/registry.py:430 +msgid "ZhiPu AI CodingPlan (International)" +msgstr "ZhiPu AI CodingPlan (Internacional)" + +#: src/iac_code/providers/registry.py:431 +msgid "Volcengine CodingPlan" +msgstr "Volcengine CodingPlan" + +#: src/iac_code/providers/registry.py:432 +msgid "Anthropic Compatible" +msgstr "Compatível com Anthropic" + +#: src/iac_code/services/qwenpaw_source.py:208 +#, python-brace-format +msgid "" +"[QwenPaw mode] Unknown provider '{provider_id}'. iac-code does not " +"support this provider.\n" +"Supported QwenPaw provider IDs: {supported_ids}\n" +"To fix: switch to a supported provider in QwenPaw, or disable QwenPaw " +"mode (remove 'llm_source: qwenpaw' from settings.yml)." +msgstr "" +"[Modo QwenPaw] Provider desconhecido '{provider_id}'. iac-code não " +"suporta este provider.\n" +"IDs de providers QwenPaw suportados: {supported_ids}\n" +"Solução: mude para um provider suportado no QwenPaw, ou desative o modo " +"QwenPaw (remova 'llm_source: qwenpaw' do settings.yml)." + #: src/iac_code/skills/skill_tool.py:130 #, python-brace-format msgid "Skill '{name}' loaded (inline)." @@ -1017,47 +1171,23 @@ msgstr "ROS Stack" msgid "CloudStackInstances" msgstr "CloudStackInstances" -#: src/iac_code/ui/banner.py:38 -msgid "DashScope" -msgstr "DashScope" - -#: src/iac_code/ui/banner.py:39 -msgid "DashScope Token Plan" -msgstr "DashScope Token Plan" - -#: src/iac_code/ui/banner.py:40 -msgid "OpenAI" -msgstr "OpenAI" - -#: src/iac_code/ui/banner.py:41 -msgid "Anthropic" -msgstr "Anthropic" - -#: src/iac_code/ui/banner.py:42 -msgid "DeepSeek" -msgstr "DeepSeek" - -#: src/iac_code/ui/banner.py:43 -msgid "OpenAPI Compatible" -msgstr "Compatível com OpenAPI" - -#: src/iac_code/ui/banner.py:67 +#: src/iac_code/ui/banner.py:64 msgid "Your AI-powered Infrastructure as Code assistant" msgstr "Seu assistente de Infrastructure as Code com IA" -#: src/iac_code/ui/banner.py:91 +#: src/iac_code/ui/banner.py:88 msgid "Welcome back" msgstr "Bem-vindo de volta" -#: src/iac_code/ui/banner.py:97 +#: src/iac_code/ui/banner.py:94 msgid "Session" msgstr "Sessão" -#: src/iac_code/ui/banner.py:107 +#: src/iac_code/ui/banner.py:104 msgid "Debug mode" msgstr "Modo debug" -#: src/iac_code/ui/banner.py:108 +#: src/iac_code/ui/banner.py:105 msgid "Log file" msgstr "Arquivo de log" @@ -1144,43 +1274,43 @@ msgstr "padrão" msgid "No, always reject this tool" msgstr "Não, sempre rejeitar esta ferramenta" -#: src/iac_code/ui/repl.py:311 +#: src/iac_code/ui/repl.py:319 msgid "Press Ctrl+C again to exit." msgstr "Pressione Ctrl+C novamente para sair." -#: src/iac_code/ui/repl.py:327 +#: src/iac_code/ui/repl.py:335 msgid "Interrupted." msgstr "Interrompido." -#: src/iac_code/ui/repl.py:364 +#: src/iac_code/ui/repl.py:372 msgid "Goodbye!" msgstr "Até logo!" -#: src/iac_code/ui/repl.py:365 +#: src/iac_code/ui/repl.py:373 msgid "Resume this session with:" msgstr "Para retomar esta sessão, execute:" -#: src/iac_code/ui/repl.py:447 +#: src/iac_code/ui/repl.py:455 #, python-brace-format msgid "Unknown command: /{name}. Type /help for available commands." msgstr "Comando desconhecido: /{name}. Digite /help para ver os comandos." -#: src/iac_code/ui/repl.py:471 src/iac_code/ui/repl.py:503 +#: src/iac_code/ui/repl.py:479 src/iac_code/ui/repl.py:511 #, python-brace-format msgid "Command error: {error}" msgstr "Erro de comando: {error}" -#: src/iac_code/ui/repl.py:478 +#: src/iac_code/ui/repl.py:486 #, python-brace-format msgid "Command has no handler: {name}" msgstr "Comando sem tratador: {name}" -#: src/iac_code/ui/repl.py:634 +#: src/iac_code/ui/repl.py:689 #, python-brace-format msgid "Session not found: {session_id}" msgstr "Sessão não encontrada: {session_id}" -#: src/iac_code/ui/repl.py:653 +#: src/iac_code/ui/repl.py:708 #, python-brace-format msgid "" "This session belongs to a different directory.\n" @@ -1191,15 +1321,15 @@ msgstr "" "Para retomar, execute:\n" " {cmd}" -#: src/iac_code/ui/repl.py:692 +#: src/iac_code/ui/repl.py:747 msgid "This conversation is from a different directory." msgstr "Esta conversa é de outro diretório." -#: src/iac_code/ui/repl.py:694 +#: src/iac_code/ui/repl.py:749 msgid "To resume, run:" msgstr "Para retomar, execute:" -#: src/iac_code/ui/repl.py:699 +#: src/iac_code/ui/repl.py:754 msgid "(Command copied to clipboard)" msgstr "(Comando copiado para a área de transferência)" @@ -1376,3 +1506,6 @@ msgstr "há {n} hora{s}" msgid "{n} day{s} ago" msgstr "há {n} dia{s}" +#~ msgid "DashScope Token Plan" +#~ msgstr "DashScope Token Plan" + diff --git a/src/iac_code/i18n/locales/zh/LC_MESSAGES/messages.po b/src/iac_code/i18n/locales/zh/LC_MESSAGES/messages.po index 4a324f42..53251d04 100644 --- a/src/iac_code/i18n/locales/zh/LC_MESSAGES/messages.po +++ b/src/iac_code/i18n/locales/zh/LC_MESSAGES/messages.po @@ -4,9 +4,9 @@ # msgid "" msgstr "" -"Project-Id-Version: iac-code 0.1.1\n" +"Project-Id-Version: iac-code 0.1.2\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-05-15 11:19+0800\n" +"POT-Creation-Date: 2026-05-15 17:58+0800\n" "PO-Revision-Date: 2026-04-02 00:00+0000\n" "Last-Translator: \n" "Language: zh\n" @@ -124,69 +124,69 @@ msgstr "" " 文档:https://aliyun.github.io/iac-code/zh-" "Hans/docs/configuration/authentication\n" -#: src/iac_code/cli/main.py:22 src/iac_code/commands/help.py:26 +#: src/iac_code/cli/main.py:23 src/iac_code/commands/help.py:26 msgid "AI-powered infrastructure orchestration tool" msgstr "AI 驱动的基础设施编排工具" -#: src/iac_code/cli/main.py:33 +#: src/iac_code/cli/main.py:34 msgid "LLM model to use" msgstr "使用的 LLM 模型" -#: src/iac_code/cli/main.py:34 +#: src/iac_code/cli/main.py:35 msgid "Non-interactive mode: run a single prompt and exit" msgstr "非交互模式:运行单个提示并退出" -#: src/iac_code/cli/main.py:35 +#: src/iac_code/cli/main.py:36 msgid "Output format: text, json, stream-json" msgstr "输出格式:text、json、stream-json" -#: src/iac_code/cli/main.py:36 +#: src/iac_code/cli/main.py:37 msgid "Maximum agent turns in headless mode" msgstr "无头模式下最大智能体轮次" -#: src/iac_code/cli/main.py:37 src/iac_code/cli/main.py:236 +#: src/iac_code/cli/main.py:38 src/iac_code/cli/main.py:241 msgid "Enable debug logging" msgstr "启用调试日志" -#: src/iac_code/cli/main.py:38 +#: src/iac_code/cli/main.py:39 msgid "Show version and exit" msgstr "显示版本号并退出" -#: src/iac_code/cli/main.py:39 +#: src/iac_code/cli/main.py:40 msgid "Resume a session by ID" msgstr "通过 ID 恢复会话" -#: src/iac_code/cli/main.py:40 +#: src/iac_code/cli/main.py:41 msgid "Resume the most recent session" msgstr "恢复最近一次会话" -#: src/iac_code/cli/main.py:47 src/iac_code/i18n/__init__.py:54 +#: src/iac_code/cli/main.py:48 src/iac_code/i18n/__init__.py:54 msgid "Install completion for the current shell." msgstr "为当前 shell 安装自动补全。" -#: src/iac_code/cli/main.py:55 src/iac_code/i18n/__init__.py:55 +#: src/iac_code/cli/main.py:56 src/iac_code/i18n/__init__.py:55 msgid "" "Show completion for the current shell, to copy it or customize the " "installation." msgstr "显示当前 shell 的自动补全脚本,可复制或自定义安装。" -#: src/iac_code/cli/main.py:69 +#: src/iac_code/cli/main.py:70 msgid "Error: --resume and --continue cannot be used together." msgstr "错误:--resume 和 --continue 不能同时使用。" -#: src/iac_code/cli/main.py:231 +#: src/iac_code/cli/main.py:236 msgid "Run iac-code as an ACP server." msgstr "将 iac-code 作为 ACP 服务器运行。" -#: src/iac_code/cli/main.py:233 +#: src/iac_code/cli/main.py:238 msgid "Transport type: stdio or http" msgstr "传输类型:stdio 或 http" -#: src/iac_code/cli/main.py:234 +#: src/iac_code/cli/main.py:239 msgid "HTTP server port" msgstr "HTTP 服务器端口" -#: src/iac_code/cli/main.py:235 +#: src/iac_code/cli/main.py:240 msgid "HTTP server host" msgstr "HTTP 服务器主机" @@ -234,104 +234,162 @@ msgstr "恢复之前的会话" msgid "[conversation id or search term]" msgstr "[会话 ID 或搜索词]" -#: src/iac_code/commands/auth.py:304 src/iac_code/commands/auth.py:817 +#: src/iac_code/commands/auth.py:245 src/iac_code/commands/auth.py:817 #: src/iac_code/ui/core/prompt_input.py:373 msgid "Navigate" msgstr "导航" -#: src/iac_code/commands/auth.py:304 src/iac_code/commands/auth.py:426 -#: src/iac_code/commands/auth.py:460 src/iac_code/commands/auth.py:467 -#: src/iac_code/commands/auth.py:491 src/iac_code/commands/auth.py:817 +#: src/iac_code/commands/auth.py:245 src/iac_code/commands/auth.py:367 +#: src/iac_code/commands/auth.py:401 src/iac_code/commands/auth.py:408 +#: src/iac_code/commands/auth.py:432 src/iac_code/commands/auth.py:817 #: src/iac_code/commands/auth.py:1007 src/iac_code/ui/core/prompt_input.py:373 msgid "Confirm" msgstr "确认" -#: src/iac_code/commands/auth.py:304 src/iac_code/commands/auth.py:424 -#: src/iac_code/commands/auth.py:426 src/iac_code/commands/auth.py:460 -#: src/iac_code/commands/auth.py:467 src/iac_code/commands/auth.py:491 +#: src/iac_code/commands/auth.py:245 src/iac_code/commands/auth.py:365 +#: src/iac_code/commands/auth.py:367 src/iac_code/commands/auth.py:401 +#: src/iac_code/commands/auth.py:408 src/iac_code/commands/auth.py:432 #: src/iac_code/commands/auth.py:817 src/iac_code/commands/auth.py:902 #: src/iac_code/commands/auth.py:1007 msgid "Back" msgstr "返回" -#: src/iac_code/commands/auth.py:424 +#: src/iac_code/commands/auth.py:365 msgid "Keep" msgstr "保留" -#: src/iac_code/commands/auth.py:424 +#: src/iac_code/commands/auth.py:365 msgid "Re-enter" msgstr "重新输入" -#: src/iac_code/commands/auth.py:566 src/iac_code/commands/auth.py:667 +#: src/iac_code/commands/auth.py:507 src/iac_code/commands/auth.py:631 +#: src/iac_code/commands/auth.py:651 msgid " (current)" msgstr " (当前)" -#: src/iac_code/commands/auth.py:569 +#: src/iac_code/commands/auth.py:510 msgid "Custom model..." msgstr "自定义模型..." -#: src/iac_code/commands/auth.py:572 +#: src/iac_code/commands/auth.py:513 #, python-brace-format msgid "Select model for {provider}" msgstr "为 {provider} 选择模型" -#: src/iac_code/commands/auth.py:574 +#: src/iac_code/commands/auth.py:515 msgid "Select model" msgstr "选择模型" -#: src/iac_code/commands/auth.py:582 +#: src/iac_code/commands/auth.py:523 msgid "Enter custom model name: " msgstr "输入自定义模型名称:" -#: src/iac_code/commands/auth.py:608 +#: src/iac_code/commands/auth.py:549 msgid "Error: console not available" msgstr "错误:控制台不可用" -#: src/iac_code/commands/auth.py:635 +#: src/iac_code/commands/auth.py:576 msgid "Configure LLM Provider" msgstr "配置 LLM 提供商" -#: src/iac_code/commands/auth.py:636 +#: src/iac_code/commands/auth.py:577 msgid "Configure IaC Cloud Service" msgstr "配置 IaC 云服务" -#: src/iac_code/commands/auth.py:638 +#: src/iac_code/commands/auth.py:579 msgid "Select configuration type" msgstr "选择配置类型" -#: src/iac_code/commands/auth.py:640 src/iac_code/commands/auth.py:694 -#: src/iac_code/commands/auth.py:709 src/iac_code/commands/auth.py:770 +#: src/iac_code/commands/auth.py:581 src/iac_code/commands/auth.py:676 +#: src/iac_code/commands/auth.py:692 src/iac_code/commands/auth.py:770 #: src/iac_code/commands/auth.py:943 src/iac_code/commands/auth.py:984 msgid "Auth cancelled" msgstr "认证已取消" -#: src/iac_code/commands/auth.py:676 +#: src/iac_code/commands/auth.py:635 msgid "Select provider" msgstr "选择提供商" -#: src/iac_code/commands/auth.py:687 +#: src/iac_code/commands/auth.py:656 src/iac_code/commands/auth.py:745 +#, python-brace-format +msgid "Select provider — {group}" +msgstr "选择提供商 — {group}" + +#: src/iac_code/commands/auth.py:669 #, python-brace-format msgid "Configure {provider}" msgstr "配置 {provider}" -#: src/iac_code/commands/auth.py:702 +#: src/iac_code/commands/auth.py:685 #, python-brace-format msgid "Enter API key for {provider}" msgstr "为 {provider} 输入 API 密钥" -#: src/iac_code/commands/auth.py:742 +#: src/iac_code/commands/auth.py:723 #, python-brace-format msgid "{status}: {provider} / {model}" msgstr "{status}:{provider} / {model}" -#: src/iac_code/commands/auth.py:743 +#: src/iac_code/commands/auth.py:724 msgid "Configured" msgstr "已配置" -#: src/iac_code/commands/auth.py:752 +#: src/iac_code/commands/auth.py:731 src/iac_code/commands/auth.py:752 msgid "Alibaba Cloud" msgstr "阿里云" +#: src/iac_code/commands/auth.py:732 src/iac_code/providers/registry.py:417 +msgid "ZhiPu AI" +msgstr "智谱 AI" + +#: src/iac_code/commands/auth.py:733 +msgid "Kimi" +msgstr "Kimi" + +#: src/iac_code/commands/auth.py:734 +msgid "MiniMax" +msgstr "MiniMax" + +#: src/iac_code/commands/auth.py:735 src/iac_code/providers/registry.py:419 +msgid "Volcengine" +msgstr "火山引擎" + +#: src/iac_code/commands/auth.py:736 +msgid "SiliconFlow" +msgstr "硅基流动" + +#: src/iac_code/commands/auth.py:737 src/iac_code/providers/registry.py:410 +msgid "DeepSeek" +msgstr "DeepSeek" + +#: src/iac_code/commands/auth.py:738 src/iac_code/providers/registry.py:408 +msgid "OpenAI" +msgstr "OpenAI" + +#: src/iac_code/commands/auth.py:739 src/iac_code/providers/registry.py:409 +msgid "Anthropic" +msgstr "Anthropic" + +#: src/iac_code/commands/auth.py:740 src/iac_code/providers/registry.py:412 +msgid "Google Gemini" +msgstr "Google Gemini" + +#: src/iac_code/commands/auth.py:741 src/iac_code/providers/registry.py:425 +msgid "Azure OpenAI" +msgstr "Azure OpenAI" + +#: src/iac_code/commands/auth.py:742 src/iac_code/providers/registry.py:424 +msgid "OpenRouter" +msgstr "OpenRouter" + +#: src/iac_code/commands/auth.py:743 +msgid "Local" +msgstr "本地模型" + +#: src/iac_code/commands/auth.py:744 +msgid "Compatible" +msgstr "兼容模式" + #: src/iac_code/commands/auth.py:761 msgid "Select Cloud Provider" msgstr "选择云服务商" @@ -548,19 +606,24 @@ msgstr "(动态)" msgid "Aborted!" msgstr "已中止!" -#: src/iac_code/providers/manager.py:80 +#: src/iac_code/providers/manager.py:78 #, python-brace-format msgid "Cannot determine provider for model: {model}. Run /auth to configure." msgstr "无法确定模型 {model} 的提供商。请运行 /auth 进行配置。" -#: src/iac_code/providers/manager.py:88 +#: src/iac_code/providers/manager.py:95 +#, python-brace-format +msgid "Unknown provider key: '{key}'. Run /auth to configure." +msgstr "未知的 Provider 标识:'{key}'。请运行 /auth 进行配置。" + +#: src/iac_code/providers/manager.py:100 #, python-brace-format msgid "" "No API key configured for provider '{provider}' (model: {model}). Run " "/auth to configure." msgstr "提供商 '{provider}' 未配置 API 密钥(模型: {model})。请运行 /auth 进行配置。" -#: src/iac_code/providers/openai_provider.py:286 +#: src/iac_code/providers/openai_provider.py:289 #, python-brace-format msgid "" "API returned no data. Please check that your API Base URL is correct " @@ -570,7 +633,7 @@ msgstr "" "API 未返回数据。请检查您的 API Base URL 是否正确(当前:{base_url})。许多 OpenAI 兼容端点需要 /v1 " "后缀(如 {base_url}/v1)。" -#: src/iac_code/providers/openai_provider.py:327 +#: src/iac_code/providers/openai_provider.py:330 #, python-brace-format msgid "" "API returned an invalid response. Please check that your API Base URL is " @@ -580,6 +643,96 @@ msgstr "" "API 返回了无效响应。请检查您的 API Base URL 是否正确(当前:{base_url})。许多 OpenAI 兼容端点需要 /v1 " "后缀(如 {base_url}/v1)。" +#: src/iac_code/providers/registry.py:406 +msgid "Alibaba Cloud Bailian" +msgstr "阿里云百炼" + +#: src/iac_code/providers/registry.py:407 +msgid "Alibaba Cloud Bailian Token Plan" +msgstr "阿里云百炼 Token Plan" + +#: src/iac_code/providers/registry.py:411 +msgid "OpenAPI Compatible" +msgstr "OpenAPI 兼容" + +#: src/iac_code/providers/registry.py:413 +msgid "Kimi (China)" +msgstr "Kimi(中国版)" + +#: src/iac_code/providers/registry.py:414 +msgid "Kimi (International)" +msgstr "Kimi(国际版)" + +#: src/iac_code/providers/registry.py:415 +msgid "MiniMax (China)" +msgstr "MiniMax(中国版)" + +#: src/iac_code/providers/registry.py:416 +msgid "MiniMax (International)" +msgstr "MiniMax(国际版)" + +#: src/iac_code/providers/registry.py:418 +msgid "ZhiPu AI (International)" +msgstr "智谱 AI(国际版)" + +#: src/iac_code/providers/registry.py:420 +msgid "SiliconFlow (China)" +msgstr "硅基流动(中国版)" + +#: src/iac_code/providers/registry.py:421 +msgid "SiliconFlow (International)" +msgstr "硅基流动(国际版)" + +#: src/iac_code/providers/registry.py:422 +msgid "Ollama (Local)" +msgstr "Ollama(本地)" + +#: src/iac_code/providers/registry.py:423 +msgid "LM Studio (Local)" +msgstr "LM Studio(本地)" + +#: src/iac_code/providers/registry.py:426 +msgid "ModelScope" +msgstr "魔搭" + +#: src/iac_code/providers/registry.py:427 +msgid "Alibaba Cloud CodingPlan" +msgstr "阿里云编程计划" + +#: src/iac_code/providers/registry.py:428 +msgid "Alibaba Cloud CodingPlan (International)" +msgstr "阿里云编程计划(国际版)" + +#: src/iac_code/providers/registry.py:429 +msgid "ZhiPu AI CodingPlan" +msgstr "智谱 AI 编程计划" + +#: src/iac_code/providers/registry.py:430 +msgid "ZhiPu AI CodingPlan (International)" +msgstr "智谱 AI 编程计划(国际版)" + +#: src/iac_code/providers/registry.py:431 +msgid "Volcengine CodingPlan" +msgstr "火山引擎编程计划" + +#: src/iac_code/providers/registry.py:432 +msgid "Anthropic Compatible" +msgstr "Anthropic 兼容" + +#: src/iac_code/services/qwenpaw_source.py:208 +#, python-brace-format +msgid "" +"[QwenPaw mode] Unknown provider '{provider_id}'. iac-code does not " +"support this provider.\n" +"Supported QwenPaw provider IDs: {supported_ids}\n" +"To fix: switch to a supported provider in QwenPaw, or disable QwenPaw " +"mode (remove 'llm_source: qwenpaw' from settings.yml)." +msgstr "" +"[QwenPaw 模式] 无法识别 provider '{provider_id}',iac-code 不支持该 provider。\n" +"支持的 QwenPaw provider ID:{supported_ids}\n" +"解决方法:在 QwenPaw 中切换到已支持的 provider,或关闭 QwenPaw 模式(从 settings.yml 移除 " +"'llm_source: qwenpaw')。" + #: src/iac_code/skills/skill_tool.py:130 #, python-brace-format msgid "Skill '{name}' loaded (inline)." @@ -1001,47 +1154,23 @@ msgstr "ROS 资源栈" msgid "CloudStackInstances" msgstr "云资源栈实例" -#: src/iac_code/ui/banner.py:38 -msgid "DashScope" -msgstr "阿里云百炼" - -#: src/iac_code/ui/banner.py:39 -msgid "DashScope Token Plan" -msgstr "阿里云百炼 Token Plan" - -#: src/iac_code/ui/banner.py:40 -msgid "OpenAI" -msgstr "OpenAI" - -#: src/iac_code/ui/banner.py:41 -msgid "Anthropic" -msgstr "Anthropic" - -#: src/iac_code/ui/banner.py:42 -msgid "DeepSeek" -msgstr "DeepSeek" - -#: src/iac_code/ui/banner.py:43 -msgid "OpenAPI Compatible" -msgstr "OpenAPI 兼容" - -#: src/iac_code/ui/banner.py:67 +#: src/iac_code/ui/banner.py:64 msgid "Your AI-powered Infrastructure as Code assistant" msgstr "您的 AI 驱动的基础设施即代码助手" -#: src/iac_code/ui/banner.py:91 +#: src/iac_code/ui/banner.py:88 msgid "Welcome back" msgstr "欢迎回来" -#: src/iac_code/ui/banner.py:97 +#: src/iac_code/ui/banner.py:94 msgid "Session" msgstr "会话" -#: src/iac_code/ui/banner.py:107 +#: src/iac_code/ui/banner.py:104 msgid "Debug mode" msgstr "调试模式" -#: src/iac_code/ui/banner.py:108 +#: src/iac_code/ui/banner.py:105 msgid "Log file" msgstr "日志文件" @@ -1128,43 +1257,43 @@ msgstr "默认" msgid "No, always reject this tool" msgstr "否,始终拒绝此工具" -#: src/iac_code/ui/repl.py:311 +#: src/iac_code/ui/repl.py:319 msgid "Press Ctrl+C again to exit." msgstr "再次按 Ctrl+C 退出。" -#: src/iac_code/ui/repl.py:327 +#: src/iac_code/ui/repl.py:335 msgid "Interrupted." msgstr "已中断。" -#: src/iac_code/ui/repl.py:364 +#: src/iac_code/ui/repl.py:372 msgid "Goodbye!" msgstr "再见!" -#: src/iac_code/ui/repl.py:365 +#: src/iac_code/ui/repl.py:373 msgid "Resume this session with:" msgstr "恢复此会话请运行:" -#: src/iac_code/ui/repl.py:447 +#: src/iac_code/ui/repl.py:455 #, python-brace-format msgid "Unknown command: /{name}. Type /help for available commands." msgstr "未知命令:/{name}。输入 /help 查看可用命令。" -#: src/iac_code/ui/repl.py:471 src/iac_code/ui/repl.py:503 +#: src/iac_code/ui/repl.py:479 src/iac_code/ui/repl.py:511 #, python-brace-format msgid "Command error: {error}" msgstr "命令错误:{error}" -#: src/iac_code/ui/repl.py:478 +#: src/iac_code/ui/repl.py:486 #, python-brace-format msgid "Command has no handler: {name}" msgstr "命令没有处理器:{name}" -#: src/iac_code/ui/repl.py:634 +#: src/iac_code/ui/repl.py:689 #, python-brace-format msgid "Session not found: {session_id}" msgstr "会话不存在:{session_id}" -#: src/iac_code/ui/repl.py:653 +#: src/iac_code/ui/repl.py:708 #, python-brace-format msgid "" "This session belongs to a different directory.\n" @@ -1175,15 +1304,15 @@ msgstr "" "请运行以下命令恢复:\n" " {cmd}" -#: src/iac_code/ui/repl.py:692 +#: src/iac_code/ui/repl.py:747 msgid "This conversation is from a different directory." msgstr "该会话来自另一个目录。" -#: src/iac_code/ui/repl.py:694 +#: src/iac_code/ui/repl.py:749 msgid "To resume, run:" msgstr "请运行以下命令恢复:" -#: src/iac_code/ui/repl.py:699 +#: src/iac_code/ui/repl.py:754 msgid "(Command copied to clipboard)" msgstr "(命令已复制到剪贴板)" @@ -1384,3 +1513,6 @@ msgstr "{n} 天前" #~ msgid "Choose [1-4]: " #~ msgstr "请选择 [1-4]: " +#~ msgid "DashScope Token Plan" +#~ msgstr "阿里云百炼 Token Plan" + diff --git a/src/iac_code/providers/anthropic_provider.py b/src/iac_code/providers/anthropic_provider.py index 478edb2a..e1292929 100644 --- a/src/iac_code/providers/anthropic_provider.py +++ b/src/iac_code/providers/anthropic_provider.py @@ -50,6 +50,7 @@ def __init__( max_tokens: int = 8192, client: Any = None, effort: str | None = None, + provider_key: str = "anthropic", **kwargs: Any, ) -> None: self._model = model @@ -65,6 +66,7 @@ def __init__( client_kwargs["base_url"] = base_url client_kwargs.update(kwargs) self._client = anthropic.AsyncAnthropic(**client_kwargs) + self._PROVIDER_KEY = provider_key # -- public interface ------------------------------------------------------ diff --git a/src/iac_code/providers/azure_openai_provider.py b/src/iac_code/providers/azure_openai_provider.py new file mode 100644 index 00000000..40da8287 --- /dev/null +++ b/src/iac_code/providers/azure_openai_provider.py @@ -0,0 +1,30 @@ +"""Azure OpenAI provider — OpenAI-compatible endpoint.""" + +from __future__ import annotations + +from typing import Any + +from iac_code.providers.openai_provider import OpenAIProvider + + +class AzureOpenAIProvider(OpenAIProvider): + """Azure OpenAI — OpenAI-compatible endpoint.""" + + _PROVIDER_KEY = "azure_openai" + + def __init__( + self, + model: str, + api_key: str | None = None, + base_url: str | None = None, + effort: str | None = None, + provider_key: str = "azure_openai", + **kwargs: Any, + ) -> None: + super().__init__( + model=model, + api_key=api_key, + base_url=base_url, + effort=effort, + ) + self._PROVIDER_KEY = provider_key diff --git a/src/iac_code/providers/dashscope_provider.py b/src/iac_code/providers/dashscope_provider.py index 2601f549..baf9efef 100644 --- a/src/iac_code/providers/dashscope_provider.py +++ b/src/iac_code/providers/dashscope_provider.py @@ -45,16 +45,15 @@ def __init__( effort: str | None = None, base_url: str = DASHSCOPE_BASE_URL, provider_key: str = "dashscope", + **kwargs, ) -> None: super().__init__( model=model, api_key=api_key, base_url=base_url, effort=effort, + provider_key=provider_key, ) - # Instance attribute shadows the class attribute so per-variant - # thinking-registry lookups resolve to the right MODEL_THINKING bucket. - self._PROVIDER_KEY = provider_key # -- Explicit context cache ------------------------------------------------ diff --git a/src/iac_code/providers/deepseek_provider.py b/src/iac_code/providers/deepseek_provider.py index ac0b9538..5c072a3f 100644 --- a/src/iac_code/providers/deepseek_provider.py +++ b/src/iac_code/providers/deepseek_provider.py @@ -26,11 +26,15 @@ def __init__( self, model: str, api_key: str | None = None, + base_url: str | None = None, effort: str | None = None, + provider_key: str = "deepseek", + **kwargs, ) -> None: super().__init__( model=model, api_key=api_key, - base_url=DEEPSEEK_BASE_URL, + base_url=base_url or DEEPSEEK_BASE_URL, effort=effort, + provider_key=provider_key, ) diff --git a/src/iac_code/providers/gemini_provider.py b/src/iac_code/providers/gemini_provider.py new file mode 100644 index 00000000..a36b0a4e --- /dev/null +++ b/src/iac_code/providers/gemini_provider.py @@ -0,0 +1,48 @@ +"""Gemini provider — Google AI via OpenAI-compatible endpoint.""" + +from __future__ import annotations + +from typing import Any + +from iac_code.providers.openai_provider import OpenAIProvider +from iac_code.providers.thinking import ThinkingFamily, get_thinking_spec, normalize_effort + +GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/v1beta/openai" + + +class GeminiProvider(OpenAIProvider): + """Provider backed by Google Gemini's OpenAI-compatible endpoint.""" + + _PROVIDER_KEY = "gemini" + supports_stream_options = True + + def __init__( + self, + model: str, + api_key: str | None = None, + base_url: str | None = None, + effort: str | None = None, + provider_key: str = "gemini", + **kwargs, + ) -> None: + super().__init__( + model=model, + api_key=api_key, + base_url=base_url or GEMINI_BASE_URL, + effort=effort, + provider_key=provider_key, + ) + + def _build_thinking_kwargs(self) -> dict[str, Any]: + spec = get_thinking_spec(self._PROVIDER_KEY, self._model) + if spec.family is not ThinkingFamily.GEMINI: + return {} + effort = normalize_effort(self._effort) + if effort is None or effort == "auto": + return {} + allowed = {e.value for e in spec.allowed_efforts} + if effort not in allowed: + if spec.default_effort is None: + return {} + effort = spec.default_effort.value + return {"reasoning_effort": effort} diff --git a/src/iac_code/providers/kimi_provider.py b/src/iac_code/providers/kimi_provider.py new file mode 100644 index 00000000..099be7fa --- /dev/null +++ b/src/iac_code/providers/kimi_provider.py @@ -0,0 +1,30 @@ +"""Kimi provider — OpenAI-compatible endpoint.""" + +from __future__ import annotations + +from typing import Any + +from iac_code.providers.openai_provider import OpenAIProvider + + +class KimiProvider(OpenAIProvider): + """Kimi — OpenAI-compatible endpoint.""" + + _PROVIDER_KEY = "kimi_cn" + + def __init__( + self, + model: str, + api_key: str | None = None, + base_url: str | None = None, + effort: str | None = None, + provider_key: str = "kimi_cn", + **kwargs: Any, + ) -> None: + super().__init__( + model=model, + api_key=api_key, + base_url=base_url, + effort=effort, + ) + self._PROVIDER_KEY = provider_key diff --git a/src/iac_code/providers/lmstudio_provider.py b/src/iac_code/providers/lmstudio_provider.py new file mode 100644 index 00000000..92842243 --- /dev/null +++ b/src/iac_code/providers/lmstudio_provider.py @@ -0,0 +1,30 @@ +"""LM Studio provider — OpenAI-compatible endpoint.""" + +from __future__ import annotations + +from typing import Any + +from iac_code.providers.openai_provider import OpenAIProvider + + +class LMStudioProvider(OpenAIProvider): + """LM Studio — OpenAI-compatible endpoint.""" + + _PROVIDER_KEY = "lmstudio" + + def __init__( + self, + model: str, + api_key: str | None = None, + base_url: str | None = None, + effort: str | None = None, + provider_key: str = "lmstudio", + **kwargs: Any, + ) -> None: + super().__init__( + model=model, + api_key=api_key or "lm-studio", + base_url=base_url, + effort=effort, + ) + self._PROVIDER_KEY = provider_key diff --git a/src/iac_code/providers/manager.py b/src/iac_code/providers/manager.py index d3221135..6fcc0635 100644 --- a/src/iac_code/providers/manager.py +++ b/src/iac_code/providers/manager.py @@ -7,8 +7,6 @@ from loguru import logger -from iac_code.config import _KEY_NAME_TO_CRED_SLOT as _KEY_TO_PROVIDER -from iac_code.config import _MODEL_PREFIX_TO_PROVIDER from iac_code.i18n import _ from iac_code.providers.base import Message, NonStreamingResponse, Provider, ToolDefinition from iac_code.providers.retry import RetryableError, RetryConfig, with_retry @@ -65,11 +63,11 @@ def _detect_provider_name(model: str) -> str: 1. Saved config in settings.yml (set by /auth or /model). 2. Model-name prefix matching for mainstream models. """ - from iac_code.config import get_active_provider_key + from iac_code.config import _KEY_NAME_TO_CRED_SLOT, _MODEL_PREFIX_TO_PROVIDER, get_active_provider_key key_name = get_active_provider_key() or "" - if key_name in _KEY_TO_PROVIDER: - return _KEY_TO_PROVIDER[key_name] + if key_name in _KEY_NAME_TO_CRED_SLOT: + return _KEY_NAME_TO_CRED_SLOT[key_name] model_lower = model.lower() for prefix, provider in _MODEL_PREFIX_TO_PROVIDER: @@ -81,76 +79,54 @@ def _detect_provider_name(model: str) -> str: ) -def create_provider(model: str, credentials: dict[str, str]) -> Provider: - provider_name = _detect_provider_name(model) - if not credentials.get(provider_name): +def create_provider( + model: str, + credentials: dict[str, str], + *, + base_url: str | None = None, + provider_key_override: str | None = None, +) -> Provider: + from iac_code.providers.registry import PROVIDER_REGISTRY + + provider_key = provider_key_override or _detect_provider_name(model) + desc = PROVIDER_REGISTRY.get(provider_key) + if desc is None: + raise ProviderNotConfiguredError( + _("Unknown provider key: '{key}'. Run /auth to configure.").format(key=provider_key) + ) + api_key = credentials.get(provider_key, "") + if desc.require_api_key and not api_key: raise ProviderNotConfiguredError( _("No API key configured for provider '{provider}' (model: {model}). Run /auth to configure.").format( - provider=provider_name, model=model + provider=desc.display_name, model=model ) ) - if provider_name == "anthropic": - from iac_code.config import get_provider_config - from iac_code.providers.anthropic_provider import AnthropicProvider - - effort = get_provider_config("anthropic").get("effort") - return AnthropicProvider( - model=model, - api_key=credentials.get("anthropic"), - effort=effort if isinstance(effort, str) else None, - ) - elif provider_name == "openai": - from iac_code.config import get_provider_config - from iac_code.providers.openai_provider import OpenAIProvider - - effort = get_provider_config("openai").get("effort") - return OpenAIProvider( - model=model, - api_key=credentials.get("openai"), - effort=effort if isinstance(effort, str) else None, - ) - elif provider_name == "dashscope": - from iac_code.config import get_provider_config - from iac_code.providers.dashscope_provider import DashScopeProvider - - effort = get_provider_config("dashscope").get("effort") - return DashScopeProvider( - model=model, - api_key=credentials.get("dashscope"), - effort=effort if isinstance(effort, str) else None, - ) - elif provider_name == "dashscope_token_plan": - from iac_code.config import get_provider_config - from iac_code.providers.dashscope_provider import ( - DASHSCOPE_TOKEN_PLAN_BASE_URL, - DashScopeProvider, - ) + from iac_code.config import get_provider_config + + provider_cfg = get_provider_config(provider_key) + effective_base_url = base_url or desc.base_url + if not effective_base_url: + saved_base = provider_cfg.get("apiBase") + if isinstance(saved_base, str) and saved_base: + effective_base_url = saved_base + provider_cls = _import_provider_class(desc.provider_class) + effort = provider_cfg.get("effort") + return provider_cls( + model=model, + api_key=api_key or None, + base_url=effective_base_url, + effort=effort if isinstance(effort, str) else None, + provider_key=provider_key, + ) - effort = get_provider_config("dashscope_token_plan").get("effort") - return DashScopeProvider( - model=model, - api_key=credentials.get("dashscope_token_plan"), - effort=effort if isinstance(effort, str) else None, - base_url=DASHSCOPE_TOKEN_PLAN_BASE_URL, - provider_key="dashscope_token_plan", - ) - elif provider_name == "deepseek": - from iac_code.config import get_provider_config - from iac_code.providers.deepseek_provider import DeepSeekProvider - - effort = get_provider_config("deepseek").get("effort") - return DeepSeekProvider( - model=model, - api_key=credentials.get("deepseek"), - effort=effort if isinstance(effort, str) else None, - ) - elif provider_name == "openapi_compatible": - from iac_code.config import get_provider_config - from iac_code.providers.openai_provider import OpenAIProvider - api_base = get_provider_config("openapi_compatible").get("apiBase") - return OpenAIProvider(model=model, api_key=credentials.get("openapi_compatible"), base_url=api_base) - raise ValueError(f"Unknown provider: {provider_name}") +def _import_provider_class(dotted_path: str): + """Lazily import a provider class from its dotted path.""" + module_path, class_name = dotted_path.rsplit(".", 1) + import importlib + + module = importlib.import_module(module_path) + return getattr(module, class_name) class ProviderManager: @@ -167,25 +143,71 @@ def __init__( credentials: dict[str, str], retry_config: RetryConfig | None = None, stream_idle_timeout: float = 90.0, + provider_key_override: str | None = None, + base_url_override: str | None = None, ): self._model = model self._credentials = credentials self._retry_config = retry_config or RetryConfig() self._stream_idle_timeout = stream_idle_timeout + self._provider_key_override = provider_key_override + self._base_url_override = base_url_override # Lazy: first startup may have no active provider yet. Defer errors # until the user actually tries to send a message, so /auth is reachable. self._provider: Provider | None = None try: - self._provider = create_provider(model, credentials) + self._provider = create_provider( + model, + credentials, + base_url=base_url_override, + provider_key_override=provider_key_override, + ) except ValueError as e: logger.warning(f"Provider not configured yet: {e}") + def _check_qwenpaw_config_change(self) -> None: + """Detect QwenPaw active_model.json changes and reconfigure if needed.""" + from iac_code.config import _get_env_overrides, get_llm_source + + env = _get_env_overrides() + if env["api_key"]: + return + if get_llm_source() != "qwenpaw": + return + from iac_code.services.qwenpaw_source import QwenPawError, load_from_qwenpaw + + try: + config = load_from_qwenpaw() + except QwenPawError as exc: + import sys + + from rich.console import Console + + Console(stderr=True).print(str(exc), style="bold red") + sys.exit(1) + if config is None: + return + if config.model != self._model or config.provider_key != self._provider_key_override: + creds = {config.provider_key: config.api_key or ""} if config.provider_key else {} + self.reconfigure(config.model, creds, config.provider_key, config.base_url) + def _ensure_provider(self) -> Provider: if self._provider is None: - self._provider = create_provider(self._model, self._credentials) + self._provider = create_provider( + self._model, + self._credentials, + base_url=self._base_url_override, + provider_key_override=self._provider_key_override, + ) return self._provider - def reconfigure(self, model: str, credentials: dict[str, str]) -> None: + def reconfigure( + self, + model: str, + credentials: dict[str, str], + provider_key_override: str | None = None, + base_url_override: str | None = None, + ) -> None: """Switch model and credentials in place. Used by `/auth` and `/model` so every consumer holding this manager @@ -195,9 +217,16 @@ def reconfigure(self, model: str, credentials: dict[str, str]) -> None: """ self._model = model self._credentials = credentials + self._provider_key_override = provider_key_override + self._base_url_override = base_url_override self._provider = None try: - self._provider = create_provider(model, credentials) + self._provider = create_provider( + model, + credentials, + base_url=base_url_override, + provider_key_override=provider_key_override, + ) except ValueError as e: logger.warning(f"Provider not configured after reconfigure: {e}") @@ -210,6 +239,7 @@ def _get_fallback_model(self) -> str | None: async def stream( self, messages: list[Message], system: str, tools: list[ToolDefinition] | None = None, max_tokens: int = 8192 ) -> AsyncGenerator[StreamEvent, None]: + self._check_qwenpaw_config_change() provider = self._ensure_provider() provider_name = type(provider).__name__.replace("Provider", "").lower() sanitized_model = sanitize_model_name(self._model) @@ -411,7 +441,12 @@ async def operation(): }, ) self._model = fallback - self._provider = create_provider(fallback, self._credentials) + self._provider = create_provider( + fallback, + self._credentials, + base_url=self._base_url_override, + provider_key_override=self._provider_key_override, + ) try: return await self._complete_with_retry(messages, system, tools, max_tokens, is_fallback=True) except Exception: diff --git a/src/iac_code/providers/minimax_provider.py b/src/iac_code/providers/minimax_provider.py new file mode 100644 index 00000000..9042279e --- /dev/null +++ b/src/iac_code/providers/minimax_provider.py @@ -0,0 +1,30 @@ +"""MiniMax provider — Anthropic-compatible endpoint.""" + +from __future__ import annotations + +from typing import Any + +from iac_code.providers.anthropic_provider import AnthropicProvider + + +class MiniMaxProvider(AnthropicProvider): + """MiniMax — Anthropic-compatible endpoint.""" + + _PROVIDER_KEY = "minimax_cn" + + def __init__( + self, + model: str, + api_key: str | None = None, + base_url: str | None = None, + effort: str | None = None, + provider_key: str = "minimax_cn", + **kwargs: Any, + ) -> None: + super().__init__( + model=model, + api_key=api_key, + base_url=base_url, + effort=effort, + ) + self._PROVIDER_KEY = provider_key diff --git a/src/iac_code/providers/modelscope_provider.py b/src/iac_code/providers/modelscope_provider.py new file mode 100644 index 00000000..f75e3988 --- /dev/null +++ b/src/iac_code/providers/modelscope_provider.py @@ -0,0 +1,30 @@ +"""ModelScope provider — OpenAI-compatible endpoint.""" + +from __future__ import annotations + +from typing import Any + +from iac_code.providers.openai_provider import OpenAIProvider + + +class ModelScopeProvider(OpenAIProvider): + """ModelScope — OpenAI-compatible endpoint.""" + + _PROVIDER_KEY = "modelscope" + + def __init__( + self, + model: str, + api_key: str | None = None, + base_url: str | None = None, + effort: str | None = None, + provider_key: str = "modelscope", + **kwargs: Any, + ) -> None: + super().__init__( + model=model, + api_key=api_key, + base_url=base_url, + effort=effort, + ) + self._PROVIDER_KEY = provider_key diff --git a/src/iac_code/providers/ollama_provider.py b/src/iac_code/providers/ollama_provider.py new file mode 100644 index 00000000..fa5bebe4 --- /dev/null +++ b/src/iac_code/providers/ollama_provider.py @@ -0,0 +1,30 @@ +"""Ollama provider — OpenAI-compatible endpoint.""" + +from __future__ import annotations + +from typing import Any + +from iac_code.providers.openai_provider import OpenAIProvider + + +class OllamaProvider(OpenAIProvider): + """Ollama — OpenAI-compatible endpoint.""" + + _PROVIDER_KEY = "ollama" + + def __init__( + self, + model: str, + api_key: str | None = None, + base_url: str | None = None, + effort: str | None = None, + provider_key: str = "ollama", + **kwargs: Any, + ) -> None: + super().__init__( + model=model, + api_key=api_key or "ollama", + base_url=base_url, + effort=effort, + ) + self._PROVIDER_KEY = provider_key diff --git a/src/iac_code/providers/openai_provider.py b/src/iac_code/providers/openai_provider.py index 2e5ba84d..e11ca3bb 100644 --- a/src/iac_code/providers/openai_provider.py +++ b/src/iac_code/providers/openai_provider.py @@ -47,6 +47,8 @@ def __init__( base_url: str | None = None, client: Any = None, effort: str | None = None, + provider_key: str = "openai", + **kwargs, ): self._model = model self._base_url = base_url @@ -58,6 +60,7 @@ def __init__( self._client = client else: self._client = AsyncOpenAI(api_key=api_key, base_url=base_url) + self._PROVIDER_KEY = provider_key def _build_thinking_kwargs(self) -> dict[str, Any]: spec = get_thinking_spec(self._PROVIDER_KEY, self._model) diff --git a/src/iac_code/providers/openrouter_provider.py b/src/iac_code/providers/openrouter_provider.py new file mode 100644 index 00000000..9758cf86 --- /dev/null +++ b/src/iac_code/providers/openrouter_provider.py @@ -0,0 +1,40 @@ +"""OpenRouter provider — OpenAI-compatible endpoint.""" + +from __future__ import annotations + +from typing import Any + +from openai import AsyncOpenAI + +from iac_code.providers.openai_provider import OpenAIProvider + + +class OpenRouterProvider(OpenAIProvider): + """OpenRouter — OpenAI-compatible endpoint.""" + + _PROVIDER_KEY = "openrouter" + + def __init__( + self, + model: str, + api_key: str | None = None, + base_url: str | None = None, + effort: str | None = None, + provider_key: str = "openrouter", + **kwargs: Any, + ) -> None: + super().__init__( + model=model, + api_key=api_key, + base_url=base_url, + effort=effort, + ) + self._PROVIDER_KEY = provider_key + self._client = AsyncOpenAI( + api_key=api_key, + base_url=base_url, + default_headers={ + "HTTP-Referer": "https://github.com/aliyun/iac-code", + "X-Title": "iac-code", + }, + ) diff --git a/src/iac_code/providers/registry.py b/src/iac_code/providers/registry.py new file mode 100644 index 00000000..435fb267 --- /dev/null +++ b/src/iac_code/providers/registry.py @@ -0,0 +1,433 @@ +from __future__ import annotations + +from dataclasses import dataclass, field + +from iac_code.i18n import _ + + +@dataclass(frozen=True) +class ModelEntry: + id: str + is_default: bool = False + + +@dataclass(frozen=True) +class ProviderDescriptor: + key: str + name: str + display_name: str + provider_class: str + base_url: str | None + models: list[ModelEntry] = field(default_factory=list) + require_api_key: bool = True + is_local: bool = False + qwenpaw_provider_ids: list[str] = field(default_factory=list) + qwenpaw_chat_model: str = "OpenAIChatModel" + + @property + def default_model(self) -> str: + for m in self.models: + if m.is_default: + return m.id + return self.models[0].id if self.models else "" + + @property + def model_ids(self) -> list[str]: + return [m.id for m in self.models] + + +PROVIDER_REGISTRY: dict[str, ProviderDescriptor] = { + "dashscope": ProviderDescriptor( + key="dashscope", + name="DashScope", + display_name="Alibaba Cloud Bailian", + provider_class="iac_code.providers.dashscope_provider.DashScopeProvider", + base_url="https://dashscope.aliyuncs.com/compatible-mode/v1", + models=[ + ModelEntry("qwen3.6-plus", is_default=True), + ModelEntry("qwen3.6-max-preview"), + ModelEntry("qwen3-max"), + ModelEntry("qwen3.5-plus"), + ModelEntry("qwen3.5-flash"), + ModelEntry("qwq-plus"), + ModelEntry("qwen3-coder-plus"), + ModelEntry("kimi-k2.6"), + ModelEntry("deepseek-v4-pro"), + ModelEntry("deepseek-v4-flash"), + ModelEntry("glm-5.1"), + ], + qwenpaw_provider_ids=["dashscope"], + ), + "dashscope_token_plan": ProviderDescriptor( + key="dashscope_token_plan", + name="DashScope Token Plan", + display_name="Alibaba Cloud Bailian Token Plan", + provider_class="iac_code.providers.dashscope_provider.DashScopeProvider", + base_url="https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1", + models=[ + ModelEntry("qwen3.6-plus", is_default=True), + ModelEntry("deepseek-v3.2"), + ModelEntry("glm-5"), + ModelEntry("MiniMax-M2.5"), + ModelEntry("kimi-k2.5"), + ], + qwenpaw_provider_ids=["aliyun-tokenplan"], + ), + "openai": ProviderDescriptor( + key="openai", + name="OpenAI", + display_name="OpenAI", + provider_class="iac_code.providers.openai_provider.OpenAIProvider", + base_url=None, + models=[ + ModelEntry("gpt-5.5", is_default=True), + ModelEntry("gpt-5.4"), + ModelEntry("gpt-5.4-mini"), + ModelEntry("gpt-5.3-codex"), + ModelEntry("gpt-5.2"), + ModelEntry("o3"), + ModelEntry("o4-mini"), + ], + qwenpaw_provider_ids=["openai"], + ), + "anthropic": ProviderDescriptor( + key="anthropic", + name="Anthropic", + display_name="Anthropic", + provider_class="iac_code.providers.anthropic_provider.AnthropicProvider", + base_url=None, + models=[ + ModelEntry("claude-opus-4-7", is_default=True), + ModelEntry("claude-opus-4-6"), + ModelEntry("claude-sonnet-4-6"), + ModelEntry("claude-sonnet-4-6-1m"), + ModelEntry("claude-haiku-4-5-20251001"), + ], + qwenpaw_provider_ids=["anthropic"], + qwenpaw_chat_model="AnthropicChatModel", + ), + "deepseek": ProviderDescriptor( + key="deepseek", + name="DeepSeek", + display_name="DeepSeek", + provider_class="iac_code.providers.deepseek_provider.DeepSeekProvider", + base_url="https://api.deepseek.com/v1", + models=[ + ModelEntry("deepseek-v4-pro", is_default=True), + ModelEntry("deepseek-v4-flash"), + ], + qwenpaw_provider_ids=["deepseek"], + ), + "openapi_compatible": ProviderDescriptor( + key="openapi_compatible", + name="OpenAPI Compatible", + display_name="OpenAPI Compatible", + provider_class="iac_code.providers.openai_provider.OpenAIProvider", + base_url=None, + models=[], + ), + "anthropic_compatible": ProviderDescriptor( + key="anthropic_compatible", + name="Anthropic Compatible", + display_name="Anthropic Compatible", + provider_class="iac_code.providers.anthropic_provider.AnthropicProvider", + base_url=None, + models=[], + qwenpaw_chat_model="AnthropicChatModel", + ), + "gemini": ProviderDescriptor( + key="gemini", + name="Gemini", + display_name="Google Gemini", + provider_class="iac_code.providers.gemini_provider.GeminiProvider", + base_url="https://generativelanguage.googleapis.com/v1beta/openai", + models=[ + ModelEntry("gemini-3.1-pro-preview", is_default=True), + ModelEntry("gemini-3-flash-preview"), + ModelEntry("gemini-3.1-flash-lite-preview"), + ModelEntry("gemini-2.5-pro"), + ModelEntry("gemini-2.5-flash"), + ModelEntry("gemini-2.5-flash-lite"), + ModelEntry("gemini-2.0-flash"), + ], + qwenpaw_provider_ids=["gemini"], + qwenpaw_chat_model="GeminiChatModel", + ), + "kimi_cn": ProviderDescriptor( + key="kimi_cn", + name="Kimi CN", + display_name="Kimi (China)", + provider_class="iac_code.providers.kimi_provider.KimiProvider", + base_url="https://api.moonshot.cn/v1", + models=[ + ModelEntry("kimi-k2.6", is_default=True), + ModelEntry("kimi-k2.5"), + ], + qwenpaw_provider_ids=["kimi-cn"], + ), + "kimi_intl": ProviderDescriptor( + key="kimi_intl", + name="Kimi Intl", + display_name="Kimi (International)", + provider_class="iac_code.providers.kimi_provider.KimiProvider", + base_url="https://api.moonshot.ai/v1", + models=[ + ModelEntry("kimi-k2.6", is_default=True), + ModelEntry("kimi-k2.5"), + ], + qwenpaw_provider_ids=["kimi-intl"], + ), + "minimax_cn": ProviderDescriptor( + key="minimax_cn", + name="MiniMax CN", + display_name="MiniMax (China)", + provider_class="iac_code.providers.minimax_provider.MiniMaxProvider", + base_url="https://api.minimaxi.com/anthropic", + models=[ + ModelEntry("MiniMax-M2.7", is_default=True), + ModelEntry("MiniMax-M2.7-highspeed"), + ModelEntry("MiniMax-M2.5"), + ModelEntry("MiniMax-M2.5-highspeed"), + ], + qwenpaw_provider_ids=["minimax-cn"], + qwenpaw_chat_model="AnthropicChatModel", + ), + "minimax_intl": ProviderDescriptor( + key="minimax_intl", + name="MiniMax Intl", + display_name="MiniMax (International)", + provider_class="iac_code.providers.minimax_provider.MiniMaxProvider", + base_url="https://api.minimax.io/anthropic", + models=[ + ModelEntry("MiniMax-M2.7", is_default=True), + ModelEntry("MiniMax-M2.7-highspeed"), + ModelEntry("MiniMax-M2.5"), + ModelEntry("MiniMax-M2.5-highspeed"), + ], + qwenpaw_provider_ids=["minimax"], + qwenpaw_chat_model="AnthropicChatModel", + ), + "zhipu_cn": ProviderDescriptor( + key="zhipu_cn", + name="ZhiPu CN", + display_name="ZhiPu AI", + provider_class="iac_code.providers.zhipu_provider.ZhiPuProvider", + base_url="https://open.bigmodel.cn/api/paas/v4", + models=[ + ModelEntry("glm-5.1", is_default=True), + ModelEntry("glm-5"), + ModelEntry("glm-5-turbo"), + ], + qwenpaw_provider_ids=["zhipu-cn"], + ), + "zhipu_intl": ProviderDescriptor( + key="zhipu_intl", + name="ZhiPu Intl", + display_name="ZhiPu AI (International)", + provider_class="iac_code.providers.zhipu_provider.ZhiPuProvider", + base_url="https://api.z.ai/api/paas/v4", + models=[ + ModelEntry("glm-5.1", is_default=True), + ModelEntry("glm-5"), + ModelEntry("glm-5-turbo"), + ], + qwenpaw_provider_ids=["zhipu-intl"], + ), + "volcengine_cn": ProviderDescriptor( + key="volcengine_cn", + name="Volcengine CN", + display_name="Volcengine", + provider_class="iac_code.providers.volcengine_provider.VolcengineProvider", + base_url="https://ark.cn-beijing.volces.com/api/v3", + models=[ + ModelEntry("doubao-seed-2-0-code-preview-260215", is_default=True), + ModelEntry("doubao-seed-2-0-pro-260215"), + ModelEntry("doubao-seed-2-0-lite-260428"), + ], + qwenpaw_provider_ids=["volcengine-cn"], + ), + "siliconflow_cn": ProviderDescriptor( + key="siliconflow_cn", + name="SiliconFlow CN", + display_name="SiliconFlow (China)", + provider_class="iac_code.providers.siliconflow_provider.SiliconFlowProvider", + base_url="https://api.siliconflow.cn/v1", + models=[], + qwenpaw_provider_ids=["siliconflow-cn"], + ), + "siliconflow_intl": ProviderDescriptor( + key="siliconflow_intl", + name="SiliconFlow Intl", + display_name="SiliconFlow (International)", + provider_class="iac_code.providers.siliconflow_provider.SiliconFlowProvider", + base_url="https://api.siliconflow.com/v1", + models=[], + qwenpaw_provider_ids=["siliconflow-intl"], + ), + "ollama": ProviderDescriptor( + key="ollama", + name="Ollama", + display_name="Ollama (Local)", + provider_class="iac_code.providers.ollama_provider.OllamaProvider", + base_url="http://localhost:11434/v1", + models=[], + require_api_key=False, + is_local=True, + qwenpaw_provider_ids=["ollama"], + ), + "lmstudio": ProviderDescriptor( + key="lmstudio", + name="LM Studio", + display_name="LM Studio (Local)", + provider_class="iac_code.providers.lmstudio_provider.LMStudioProvider", + base_url="http://localhost:1234/v1", + models=[], + require_api_key=False, + is_local=True, + qwenpaw_provider_ids=["lmstudio"], + ), + "openrouter": ProviderDescriptor( + key="openrouter", + name="OpenRouter", + display_name="OpenRouter", + provider_class="iac_code.providers.openrouter_provider.OpenRouterProvider", + base_url="https://openrouter.ai/api/v1", + models=[], + qwenpaw_provider_ids=["openrouter"], + ), + "azure_openai": ProviderDescriptor( + key="azure_openai", + name="Azure OpenAI", + display_name="Azure OpenAI", + provider_class="iac_code.providers.azure_openai_provider.AzureOpenAIProvider", + base_url=None, + models=[ + ModelEntry("gpt-5", is_default=True), + ModelEntry("gpt-5-mini"), + ModelEntry("gpt-4.1"), + ModelEntry("gpt-4o"), + ], + qwenpaw_provider_ids=["azure-openai"], + ), + "modelscope": ProviderDescriptor( + key="modelscope", + name="ModelScope", + display_name="ModelScope", + provider_class="iac_code.providers.modelscope_provider.ModelScopeProvider", + base_url="https://api-inference.modelscope.cn/v1", + models=[ + ModelEntry("Qwen/Qwen3.5-122B-A10B", is_default=True), + ], + qwenpaw_provider_ids=["modelscope"], + ), + "aliyun_codingplan": ProviderDescriptor( + key="aliyun_codingplan", + name="Aliyun CodingPlan", + display_name="Alibaba Cloud CodingPlan", + provider_class="iac_code.providers.dashscope_provider.DashScopeProvider", + base_url="https://coding.dashscope.aliyuncs.com/v1", + models=[ + ModelEntry("qwen3.6-plus", is_default=True), + ModelEntry("qwen3.5-plus"), + ModelEntry("glm-5"), + ModelEntry("glm-4.7"), + ModelEntry("MiniMax-M2.5"), + ModelEntry("kimi-k2.5"), + ModelEntry("qwen3-coder-plus"), + ModelEntry("qwen3-coder-next"), + ], + qwenpaw_provider_ids=["aliyun-codingplan"], + ), + "aliyun_codingplan_intl": ProviderDescriptor( + key="aliyun_codingplan_intl", + name="Aliyun CodingPlan Intl", + display_name="Alibaba Cloud CodingPlan (International)", + provider_class="iac_code.providers.dashscope_provider.DashScopeProvider", + base_url="https://coding-intl.dashscope.aliyuncs.com/v1", + models=[ + ModelEntry("qwen3.6-plus", is_default=True), + ModelEntry("qwen3.5-plus"), + ModelEntry("glm-5"), + ModelEntry("glm-4.7"), + ModelEntry("MiniMax-M2.5"), + ModelEntry("kimi-k2.5"), + ModelEntry("qwen3-coder-plus"), + ModelEntry("qwen3-coder-next"), + ], + qwenpaw_provider_ids=["aliyun-codingplan-intl"], + ), + "zhipu_cn_codingplan": ProviderDescriptor( + key="zhipu_cn_codingplan", + name="ZhiPu CN CodingPlan", + display_name="ZhiPu AI CodingPlan", + provider_class="iac_code.providers.zhipu_provider.ZhiPuProvider", + base_url="https://open.bigmodel.cn/api/coding/paas/v4", + models=[ + ModelEntry("glm-5.1", is_default=True), + ModelEntry("glm-5"), + ], + qwenpaw_provider_ids=["zhipu-cn-codingplan"], + ), + "zhipu_intl_codingplan": ProviderDescriptor( + key="zhipu_intl_codingplan", + name="ZhiPu Intl CodingPlan", + display_name="ZhiPu AI CodingPlan (International)", + provider_class="iac_code.providers.zhipu_provider.ZhiPuProvider", + base_url="https://api.z.ai/api/coding/paas/v4", + models=[ + ModelEntry("glm-5.1", is_default=True), + ModelEntry("glm-5"), + ], + qwenpaw_provider_ids=["zhipu-intl-codingplan"], + ), + "volcengine_cn_codingplan": ProviderDescriptor( + key="volcengine_cn_codingplan", + name="Volcengine CodingPlan", + display_name="Volcengine CodingPlan", + provider_class="iac_code.providers.volcengine_provider.VolcengineProvider", + base_url="https://ark.cn-beijing.volces.com/api/coding/v3", + models=[ + ModelEntry("doubao-seed-2-0-code-preview-260215", is_default=True), + ModelEntry("doubao-seed-2-0-pro-260215"), + ModelEntry("doubao-seed-2-0-lite-260428"), + ModelEntry("glm-5.1"), + ModelEntry("minimax-m2.7"), + ModelEntry("kimi-k2.6"), + ModelEntry("kimi-k2.5"), + ], + qwenpaw_provider_ids=["volcengine-cn-codingplan"], + ), +} + + +# Extraction markers for pybabel — these calls let the tool discover +# all display_name strings so they appear in messages.pot. +_DISPLAY_NAME_MARKERS = [ + _("Alibaba Cloud Bailian"), + _("Alibaba Cloud Bailian Token Plan"), + _("OpenAI"), + _("Anthropic"), + _("DeepSeek"), + _("OpenAPI Compatible"), + _("Google Gemini"), + _("Kimi (China)"), + _("Kimi (International)"), + _("MiniMax (China)"), + _("MiniMax (International)"), + _("ZhiPu AI"), + _("ZhiPu AI (International)"), + _("Volcengine"), + _("SiliconFlow (China)"), + _("SiliconFlow (International)"), + _("Ollama (Local)"), + _("LM Studio (Local)"), + _("OpenRouter"), + _("Azure OpenAI"), + _("ModelScope"), + _("Alibaba Cloud CodingPlan"), + _("Alibaba Cloud CodingPlan (International)"), + _("ZhiPu AI CodingPlan"), + _("ZhiPu AI CodingPlan (International)"), + _("Volcengine CodingPlan"), + _("Anthropic Compatible"), +] diff --git a/src/iac_code/providers/siliconflow_provider.py b/src/iac_code/providers/siliconflow_provider.py new file mode 100644 index 00000000..9ff4a930 --- /dev/null +++ b/src/iac_code/providers/siliconflow_provider.py @@ -0,0 +1,30 @@ +"""SiliconFlow provider — OpenAI-compatible endpoint.""" + +from __future__ import annotations + +from typing import Any + +from iac_code.providers.openai_provider import OpenAIProvider + + +class SiliconFlowProvider(OpenAIProvider): + """SiliconFlow — OpenAI-compatible endpoint.""" + + _PROVIDER_KEY = "siliconflow_cn" + + def __init__( + self, + model: str, + api_key: str | None = None, + base_url: str | None = None, + effort: str | None = None, + provider_key: str = "siliconflow_cn", + **kwargs: Any, + ) -> None: + super().__init__( + model=model, + api_key=api_key, + base_url=base_url, + effort=effort, + ) + self._PROVIDER_KEY = provider_key diff --git a/src/iac_code/providers/thinking.py b/src/iac_code/providers/thinking.py index a47b7c24..62b0ce3d 100644 --- a/src/iac_code/providers/thinking.py +++ b/src/iac_code/providers/thinking.py @@ -53,6 +53,7 @@ class ThinkingFamily(Enum): ANTHROPIC = "anthropic" OPENAI = "openai" # reasoning_effort + extra_body.thinking.type=enabled DASHSCOPE = "dashscope" # extra_body.enable_thinking [+ thinking_budget] + GEMINI = "gemini" @dataclass(frozen=True) @@ -85,6 +86,12 @@ def effort_range(self) -> tuple[EffortLevel, EffortLevel] | None: EffortLevel.XHIGH, ) +_GEMINI_EFFORTS: tuple[EffortLevel, ...] = ( + EffortLevel.LOW, + EffortLevel.MEDIUM, + EffortLevel.HIGH, +) + _ANTHROPIC_EFFORTS: tuple[EffortLevel, ...] = ( EffortLevel.LOW, EffortLevel.MEDIUM, @@ -115,6 +122,8 @@ def effort_range(self) -> tuple[EffortLevel, EffortLevel] | None: "gpt-5.4-mini": ThinkingSpec(ThinkingFamily.OPENAI, _OPENAI_EFFORTS, EffortLevel.HIGH), "gpt-5.3-codex": ThinkingSpec(ThinkingFamily.OPENAI, _OPENAI_EFFORTS, EffortLevel.HIGH), "gpt-5.2": ThinkingSpec(ThinkingFamily.OPENAI, _OPENAI_EFFORTS, EffortLevel.HIGH), + "o3": ThinkingSpec(ThinkingFamily.OPENAI, _OPENAI_EFFORTS, EffortLevel.HIGH), + "o4-mini": ThinkingSpec(ThinkingFamily.OPENAI, _OPENAI_EFFORTS, EffortLevel.HIGH), }, "deepseek": { "deepseek-v4-pro": ThinkingSpec(ThinkingFamily.OPENAI, _DEEPSEEK_EFFORTS, EffortLevel.HIGH), @@ -137,12 +146,33 @@ def effort_range(self) -> tuple[EffortLevel, EffortLevel] | None: "glm-5": ThinkingSpec(ThinkingFamily.DASHSCOPE), "MiniMax-M2.5": ThinkingSpec(ThinkingFamily.DASHSCOPE), }, + "gemini": { + "gemini-3.1-pro-preview": ThinkingSpec(ThinkingFamily.GEMINI, _GEMINI_EFFORTS, EffortLevel.MEDIUM), + "gemini-3-flash-preview": ThinkingSpec(ThinkingFamily.GEMINI, _GEMINI_EFFORTS, EffortLevel.MEDIUM), + "gemini-2.5-pro": ThinkingSpec(ThinkingFamily.GEMINI, _GEMINI_EFFORTS, EffortLevel.MEDIUM), + "gemini-2.5-flash": ThinkingSpec(ThinkingFamily.GEMINI, _GEMINI_EFFORTS, EffortLevel.MEDIUM), + }, +} + + +_THINKING_FALLBACK: dict[str, str] = { + "aliyun_codingplan": "dashscope", + "aliyun_codingplan_intl": "dashscope", + "zhipu_cn_codingplan": "zhipu_cn", + "zhipu_intl_codingplan": "zhipu_intl", + "volcengine_cn_codingplan": "volcengine_cn", } def get_thinking_spec(provider_key: str, model: str) -> ThinkingSpec: """Return spec for (provider_key, model). Unknown combos → ``NONE`` spec.""" - return MODEL_THINKING.get(provider_key, {}).get(model, _NONE_SPEC) + spec = MODEL_THINKING.get(provider_key, {}).get(model) + if spec is not None: + return spec + fallback_key = _THINKING_FALLBACK.get(provider_key) + if fallback_key: + return MODEL_THINKING.get(fallback_key, {}).get(model, _NONE_SPEC) + return _NONE_SPEC def normalize_effort(effort: str | None) -> str | None: diff --git a/src/iac_code/providers/volcengine_provider.py b/src/iac_code/providers/volcengine_provider.py new file mode 100644 index 00000000..79b40295 --- /dev/null +++ b/src/iac_code/providers/volcengine_provider.py @@ -0,0 +1,30 @@ +"""Volcengine provider — OpenAI-compatible endpoint.""" + +from __future__ import annotations + +from typing import Any + +from iac_code.providers.openai_provider import OpenAIProvider + + +class VolcengineProvider(OpenAIProvider): + """Volcengine — OpenAI-compatible endpoint.""" + + _PROVIDER_KEY = "volcengine_cn" + + def __init__( + self, + model: str, + api_key: str | None = None, + base_url: str | None = None, + effort: str | None = None, + provider_key: str = "volcengine_cn", + **kwargs: Any, + ) -> None: + super().__init__( + model=model, + api_key=api_key, + base_url=base_url, + effort=effort, + ) + self._PROVIDER_KEY = provider_key diff --git a/src/iac_code/providers/zhipu_provider.py b/src/iac_code/providers/zhipu_provider.py new file mode 100644 index 00000000..6cd3ec29 --- /dev/null +++ b/src/iac_code/providers/zhipu_provider.py @@ -0,0 +1,30 @@ +"""ZhiPu provider — OpenAI-compatible endpoint.""" + +from __future__ import annotations + +from typing import Any + +from iac_code.providers.openai_provider import OpenAIProvider + + +class ZhiPuProvider(OpenAIProvider): + """ZhiPu — OpenAI-compatible endpoint.""" + + _PROVIDER_KEY = "zhipu_cn" + + def __init__( + self, + model: str, + api_key: str | None = None, + base_url: str | None = None, + effort: str | None = None, + provider_key: str = "zhipu_cn", + **kwargs: Any, + ) -> None: + super().__init__( + model=model, + api_key=api_key, + base_url=base_url, + effort=effort, + ) + self._PROVIDER_KEY = provider_key diff --git a/src/iac_code/services/agent_factory.py b/src/iac_code/services/agent_factory.py index 9c148321..3e11dddf 100644 --- a/src/iac_code/services/agent_factory.py +++ b/src/iac_code/services/agent_factory.py @@ -54,7 +54,40 @@ def create_agent_runtime(options: AgentFactoryOptions) -> AgentRuntime: credentials = load_credentials(model=options.model) - provider_manager = ProviderManager(model=options.model, credentials=credentials) + provider_key_override = None + base_url_override = None + + from iac_code.config import _get_env_overrides, get_llm_source + + env = _get_env_overrides() + model = options.model + + if env["api_key"]: + pass # env overrides handled by load_credentials + elif get_llm_source() == "qwenpaw": + from iac_code.services.qwenpaw_source import QwenPawError, load_from_qwenpaw + + try: + qwenpaw_config = load_from_qwenpaw() + except QwenPawError as exc: + import sys + + from rich.console import Console + + Console(stderr=True).print(str(exc), style="bold red") + sys.exit(1) + if qwenpaw_config: + model = qwenpaw_config.model + credentials = {qwenpaw_config.provider_key: qwenpaw_config.api_key or ""} + provider_key_override = qwenpaw_config.provider_key + base_url_override = qwenpaw_config.base_url + + provider_manager = ProviderManager( + model=model, + credentials=credentials, + provider_key_override=provider_key_override, + base_url_override=base_url_override, + ) tool_registry = ToolRegistry() tool_registry.register_default_tools() diff --git a/src/iac_code/services/qwenpaw_source.py b/src/iac_code/services/qwenpaw_source.py new file mode 100644 index 00000000..358e3ed8 --- /dev/null +++ b/src/iac_code/services/qwenpaw_source.py @@ -0,0 +1,242 @@ +"""QwenPaw mode — read LLM config from QwenPaw's active_model.json.""" + +from __future__ import annotations + +import base64 +import json +import os +from dataclasses import dataclass +from pathlib import Path + +from loguru import logger + +from iac_code.i18n import _ + + +class QwenPawError(RuntimeError): + """Raised when QwenPaw configuration is present but invalid.""" + + +@dataclass(frozen=True) +class QwenPawConfig: + """Resolved QwenPaw configuration.""" + + model: str + provider_key: str + api_key: str | None + base_url: str | None + + +def _resolve_secret_dir() -> Path | None: + """Resolve QwenPaw SECRET_DIR from env, home dir, or working directory. + + Search order mirrors QwenPaw's own constant.py: + 1. QWENPAW_SECRET_DIR / COPAW_SECRET_DIR env var + 2. ~/.qwenpaw.secret (default) + 3. ~/.copaw.secret (legacy) + 4. .secret in cwd or parent directories + """ + env_dir = os.environ.get("QWENPAW_SECRET_DIR") or os.environ.get("COPAW_SECRET_DIR") + if env_dir: + p = Path(env_dir).expanduser().resolve() + return p if p.is_dir() else None + + home = Path.home() + for name in (".qwenpaw.secret", ".copaw.secret"): + p = home / name + if p.is_dir(): + return p + + cwd = Path.cwd() + secret = cwd / ".secret" + if secret.is_dir(): + return secret + for parent in cwd.parents: + secret = parent / ".secret" + if secret.is_dir(): + return secret + return None + + +def _read_active_model(secret_dir: Path) -> dict | None: + """Read active_model.json from SECRET_DIR/providers/.""" + for candidate in (secret_dir / "providers" / "active_model.json", secret_dir / "active_model.json"): + if candidate.exists(): + try: + data = json.loads(candidate.read_text(encoding="utf-8")) + if isinstance(data, dict): + return data + except Exception: + logger.debug("Failed to read {}", candidate) + return None + + +def _read_provider_config(secret_dir: Path, provider_id: str) -> dict | None: + """Read a QwenPaw provider config file. + + Searches builtin/, custom/, plugin/ sub-directories under providers/, + then falls back to providers/{id}.json for compatibility. + """ + providers_root = secret_dir / "providers" + candidates = [ + providers_root / "builtin" / f"{provider_id}.json", + providers_root / "custom" / f"{provider_id}.json", + providers_root / "plugin" / f"{provider_id}.json", + providers_root / f"{provider_id}.json", + ] + for path in candidates: + if path.exists(): + try: + data = json.loads(path.read_text(encoding="utf-8")) + if isinstance(data, dict): + return data + except Exception: + logger.debug("Failed to read provider config: {}", path) + return None + + +_ENC_PREFIX = "ENC:" + + +def _decrypt_api_key(raw_value: str) -> str | None: + """Decrypt a Fernet-encrypted api_key using OS keychain or .master_key file. + + QwenPaw stores encrypted values with an ``ENC:`` prefix. Plaintext values + (no prefix) are returned as-is for backward compatibility. + """ + if not raw_value: + return None + + if not raw_value.startswith(_ENC_PREFIX): + return raw_value + + ciphertext = raw_value[len(_ENC_PREFIX) :] + + try: + from cryptography.fernet import Fernet + except ImportError: + logger.warning( + "cryptography package not installed; cannot decrypt QwenPaw API key. " + "Install it with: pip install cryptography" + ) + return None + + master_key = _get_master_key() + if not master_key: + return None + + try: + raw_bytes = bytes.fromhex(master_key) if isinstance(master_key, str) else master_key + fernet_key = base64.urlsafe_b64encode(raw_bytes[:32]) + f = Fernet(fernet_key) + return f.decrypt(ciphertext.encode("ascii")).decode("utf-8") + except Exception: + logger.warning( + "Failed to decrypt QwenPaw API key. The master key may be incorrect or the encrypted data may be corrupted." + ) + return None + + +def _get_master_key() -> str | None: + """Get master key from OS keychain (qwenpaw/copaw) or .master_key file.""" + try: + import keyring + + key = keyring.get_password("qwenpaw", "master_key") + if key: + return key + key = keyring.get_password("copaw", "master_key") + if key: + return key + except Exception: + logger.debug("OS Keychain unavailable for QwenPaw master key lookup") + + secret_dir = _resolve_secret_dir() + if secret_dir: + master_key_file = secret_dir / ".master_key" + if master_key_file.exists(): + try: + content = master_key_file.read_text(encoding="utf-8").strip() + if content: + bytes.fromhex(content) + return content + except (OSError, ValueError): + logger.debug("Master key file is corrupt or unreadable") + + logger.warning( + "QwenPaw master key not found. Checked: OS Keychain (qwenpaw/copaw) " + "and .master_key file in SECRET_DIR. Encrypted API keys cannot be decrypted." + ) + return None + + +def _map_qwenpaw_provider_id(qwenpaw_id: str) -> str | None: + """Map a QwenPaw provider ID to an iac-code provider key.""" + from iac_code.providers.registry import PROVIDER_REGISTRY + + for desc in PROVIDER_REGISTRY.values(): + if qwenpaw_id in desc.qwenpaw_provider_ids: + return desc.key + return None + + +def load_from_qwenpaw() -> QwenPawConfig | None: + """Load QwenPaw configuration. Returns None if unavailable.""" + secret_dir = _resolve_secret_dir() + if not secret_dir: + return None + + active = _read_active_model(secret_dir) + if not active: + return None + + model = active.get("model") + qwenpaw_provider_id = active.get("provider_id") or active.get("provider") + if not model or not qwenpaw_provider_id: + return None + + provider_key = _map_qwenpaw_provider_id(qwenpaw_provider_id) + if not provider_key: + from iac_code.providers.registry import PROVIDER_REGISTRY + + known_ids = sorted(pid for desc in PROVIDER_REGISTRY.values() for pid in desc.qwenpaw_provider_ids) + raise QwenPawError( + _( + "[QwenPaw mode] Unknown provider '{provider_id}'. " + "iac-code does not support this provider.\n" + "Supported QwenPaw provider IDs: {supported_ids}\n" + "To fix: switch to a supported provider in QwenPaw, " + "or disable QwenPaw mode (remove 'llm_source: qwenpaw' from settings.yml)." + ).format(provider_id=qwenpaw_provider_id, supported_ids=", ".join(known_ids)) + ) + + provider_config = _read_provider_config(secret_dir, qwenpaw_provider_id) + api_key: str | None = None + base_url: str | None = None + + if provider_config: + raw_key = provider_config.get("api_key", "") + if raw_key: + api_key = _decrypt_api_key(raw_key) + base_url = provider_config.get("base_url") or None + + from iac_code.providers.registry import PROVIDER_REGISTRY + + desc = PROVIDER_REGISTRY.get(provider_key) + + if not base_url and desc: + base_url = desc.base_url + + if api_key is None and desc and desc.require_api_key: + logger.warning( + "QwenPaw provider '{}' requires an API key but decryption failed. " + "LLM requests will likely fail until this is resolved.", + provider_key, + ) + + return QwenPawConfig( + model=model, + provider_key=provider_key, + api_key=api_key, + base_url=base_url, + ) diff --git a/src/iac_code/services/token_counter.py b/src/iac_code/services/token_counter.py index 115ee37c..b8c7ae01 100644 --- a/src/iac_code/services/token_counter.py +++ b/src/iac_code/services/token_counter.py @@ -20,6 +20,11 @@ "qwq": "cl100k_base", "o3": "o200k_base", "o4": "o200k_base", + "gemini": "cl100k_base", + "glm": "cl100k_base", + "kimi": "cl100k_base", + "minimax": "cl100k_base", + "doubao": "cl100k_base", } _DEFAULT_ENCODING = "cl100k_base" diff --git a/src/iac_code/ui/banner.py b/src/iac_code/ui/banner.py index c8abc387..f1808edd 100644 --- a/src/iac_code/ui/banner.py +++ b/src/iac_code/ui/banner.py @@ -26,23 +26,20 @@ def _get_provider_display() -> str: - """Get the active provider display name from settings, translated.""" + """Get the active provider display name from settings.""" try: from iac_code.config import get_active_provider_key, get_provider_config + from iac_code.i18n import _ + from iac_code.providers.registry import PROVIDER_REGISTRY key = get_active_provider_key() if not key: return "" + desc = PROVIDER_REGISTRY.get(key) + if desc: + return _(desc.display_name) name = get_provider_config(key).get("name", "") - provider_display_names = { - "DashScope": _("DashScope"), - "DashScope Token Plan": _("DashScope Token Plan"), - "OpenAI": _("OpenAI"), - "Anthropic": _("Anthropic"), - "DeepSeek": _("DeepSeek"), - "OpenAPI Compatible": _("OpenAPI Compatible"), - } - return provider_display_names.get(name, name) + return name except Exception: return "" diff --git a/src/iac_code/ui/repl.py b/src/iac_code/ui/repl.py index 7832e049..456dad32 100644 --- a/src/iac_code/ui/repl.py +++ b/src/iac_code/ui/repl.py @@ -97,7 +97,15 @@ def __init__(self, model: str, resume_session_id: str | bool | None = None) -> N # Backend: Provider + Session + Tasks + Memory self._credentials = self._load_credentials() - self._provider_manager = ProviderManager(model=model, credentials=self._credentials) + self._provider_key_override: str | None = None + self._base_url_override: str | None = None + self._apply_qwenpaw_config(model) + self._provider_manager = ProviderManager( + model=self._current_model, + credentials=self._credentials, + provider_key_override=self._provider_key_override, + base_url_override=self._base_url_override, + ) self._session_storage = SessionStorage() self.session_index = SessionIndex() self._session_id = self._resolve_session_id(resume_session_id) @@ -589,8 +597,32 @@ def _reinitialize_provider(self, new_model: str) -> None: self._current_model = new_model self._current_provider_config = load_active_provider_config() + self._provider_key_override = None + self._base_url_override = None self._credentials = self._load_credentials() - self._provider_manager.reconfigure(new_model, self._credentials) + from iac_code.config import _get_env_overrides, get_llm_source + + env = _get_env_overrides() + if not env["api_key"] and get_llm_source() == "qwenpaw": + from iac_code.services.qwenpaw_source import QwenPawError, load_from_qwenpaw + + try: + qwenpaw_config = load_from_qwenpaw() + except QwenPawError as exc: + Console(stderr=True).print(str(exc), style="bold red") + raise SystemExit(1) + if qwenpaw_config: + self._current_model = qwenpaw_config.model + self.store.set_state(model=qwenpaw_config.model) + self._credentials = {qwenpaw_config.provider_key: qwenpaw_config.api_key or ""} + self._provider_key_override = qwenpaw_config.provider_key + self._base_url_override = qwenpaw_config.base_url + self._provider_manager.reconfigure( + self._current_model, + self._credentials, + provider_key_override=self._provider_key_override, + base_url_override=self._base_url_override, + ) memory_content = "" if hasattr(self, "_memory_manager") and self._memory_manager: memory_content = self._memory_manager.get_prompt_content() @@ -604,6 +636,29 @@ def _reinitialize_provider(self, new_model: str) -> None: # Helpers # ------------------------------------------------------------------ + def _apply_qwenpaw_config(self, model: str) -> None: + """Apply QwenPaw config if active and env vars don't override.""" + from iac_code.config import _get_env_overrides, get_llm_source + + env = _get_env_overrides() + if env["api_key"]: + return + if get_llm_source() != "qwenpaw": + return + from iac_code.services.qwenpaw_source import QwenPawError, load_from_qwenpaw + + try: + qwenpaw_config = load_from_qwenpaw() + except QwenPawError as exc: + Console(stderr=True).print(str(exc), style="bold red") + raise SystemExit(1) + if qwenpaw_config: + self._current_model = qwenpaw_config.model + self.store = AppStateStore(initial_state=AppState(model=qwenpaw_config.model)) + self._credentials = {qwenpaw_config.provider_key: qwenpaw_config.api_key or ""} + self._provider_key_override = qwenpaw_config.provider_key + self._base_url_override = qwenpaw_config.base_url + def _load_credentials(self) -> dict[str, str]: """Load API credentials (delegates to config.load_credentials with env overlay).""" return load_credentials(model=self._current_model) diff --git a/tests/cli/test_headless.py b/tests/cli/test_headless.py index bedf2570..8efbda85 100644 --- a/tests/cli/test_headless.py +++ b/tests/cli/test_headless.py @@ -302,7 +302,7 @@ def _install_headless_fakes(monkeypatch, *, creds=None, skills=None, existing_co fake_session_dir = Path("/tmp/iac-config") class FakeProviderManager: - def __init__(self, *, model, credentials): + def __init__(self, *, model, credentials, provider_key_override=None, base_url_override=None): captured["provider_manager"] = {"model": model, "credentials": credentials} class FakeSessionStorage: @@ -425,17 +425,12 @@ def test_create_agent_loop_builds_expected_dependencies(monkeypatch): assert loop is not None assert fake_registry.default_registered is True - assert captured["provider_manager"] == { - "model": "test-model", - "credentials": { - "anthropic": "ak", - "openai": "ok", - "dashscope": "bk", - "dashscope_token_plan": "", - "deepseek": "", - "openapi_compatible": "compat", - }, - } + pm = captured["provider_manager"] + assert pm["model"] == "test-model" + assert pm["credentials"]["anthropic"] == "ak" + assert pm["credentials"]["openai"] == "ok" + assert pm["credentials"]["dashscope"] == "bk" + assert pm["credentials"]["openapi_compatible"] == "compat" # Session storage is now project-partitioned and constructs its own # default projects_dir from get_config_dir(), so we just assert the # storage was instantiated rather than checking a specific path. @@ -463,14 +458,9 @@ def test_create_agent_loop_handles_credential_load_failure_and_skill_conflict(mo with patch("iac_code.cli.headless.logger.warning") as warning: runner._create_agent_loop() - assert captured["provider_manager"]["credentials"] == { - "anthropic": "", - "openai": "", - "dashscope": "", - "dashscope_token_plan": "", - "deepseek": "", - "openapi_compatible": "", - } + creds = captured["provider_manager"]["credentials"] + for key in ("anthropic", "openai", "dashscope", "dashscope_token_plan", "deepseek", "openapi_compatible"): + assert creds[key] == "" warning.assert_called_once() assert fake_command_registry.registered == [] assert any(getattr(tool, "kind", "") == "skill" for tool in fake_registry.registered) diff --git a/tests/commands/test_auth_basics.py b/tests/commands/test_auth_basics.py index b55d747b..cf88fece 100644 --- a/tests/commands/test_auth_basics.py +++ b/tests/commands/test_auth_basics.py @@ -45,12 +45,9 @@ def test_dashscope_token_plan_entry_present(self): ) assert entry is not None assert entry["name"] == "DashScope Token Plan" - assert entry["display_name"] == "阿里云百炼 Token Plan" + assert entry["display_name"] == "Alibaba Cloud Bailian Token Plan" assert entry["api_base"] == ("https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1") assert entry["default_model"] == "qwen3.6-plus" - assert set(entry["models"]) == { - "qwen3.6-plus", - "deepseek-v3.2", - "glm-5", - "MiniMax-M2.5", - } + models = set(entry["models"]) + for expected in ("qwen3.6-plus", "deepseek-v3.2", "glm-5", "MiniMax-M2.5"): + assert expected in models diff --git a/tests/commands/test_auth_flows.py b/tests/commands/test_auth_flows.py index 49abd633..ed9f6702 100644 --- a/tests/commands/test_auth_flows.py +++ b/tests/commands/test_auth_flows.py @@ -128,48 +128,75 @@ def select_side_effect(title, options, default_index=0): class TestLlmAuthFlow: - def test_escape_at_provider_select_returns_back(self, monkeypatch): - # _select for provider picker → None (Esc) → returns _BACK sentinel + def test_escape_at_group_select_returns_back(self, monkeypatch): monkeypatch.setattr("iac_code.commands.auth._select", lambda title, options, default_index=0: None) - console = MagicMock() - store = MagicMock() - result = _llm_auth_flow(console, store) + result = _llm_auth_flow(MagicMock(), MagicMock()) assert result is _BACK - def test_back_at_api_key_input_loops_to_provider_select(self, monkeypatch): - # _select → provider index 0, _input_masked → _BACK (loops back to provider), - # then _select → None on second call so test terminates. + def test_back_at_sub_provider_returns_to_group(self, monkeypatch): call_count = {"select": 0} def select_side_effect(title, options, default_index=0): call_count["select"] += 1 if call_count["select"] == 1: - return 0 # select first provider + return 0 # Alibaba Cloud group (multiple items) + if call_count["select"] == 2: + return None # Esc at sub-provider → back to group + return None # Esc at group → _BACK + + monkeypatch.setattr("iac_code.commands.auth._select", select_side_effect) + result = _llm_auth_flow(MagicMock(), MagicMock()) + assert result is _BACK + assert call_count["select"] == 3 + + def test_back_at_api_key_input_loops_to_group_select(self, monkeypatch): + call_count = {"select": 0} + + def select_side_effect(title, options, default_index=0): + call_count["select"] += 1 + if call_count["select"] <= 2: + return 0 # group + sub-provider return None # Esc → _BACK monkeypatch.setattr("iac_code.commands.auth._select", select_side_effect) - # _load_existing_key returns None so no masking needed monkeypatch.setattr("iac_code.commands.auth._load_existing_key", lambda key_name: None) monkeypatch.setattr("iac_code.commands.auth._input_masked", lambda *a, **kw: _BACK) - console = MagicMock() - store = MagicMock() - result = _llm_auth_flow(console, store) - # Should have looped, then returned _BACK from second _select=None + result = _llm_auth_flow(MagicMock(), MagicMock()) assert result is _BACK + assert call_count["select"] == 3 def test_cancel_at_api_key_input_returns_cancelled(self, monkeypatch): - # _select → index 0, _input_masked → None (Ctrl+C) → "Auth cancelled" + # _select always returns 0 → Alibaba Cloud group → DashScope provider monkeypatch.setattr("iac_code.commands.auth._select", lambda title, options, default_index=0: 0) monkeypatch.setattr("iac_code.commands.auth._load_existing_key", lambda key_name: None) monkeypatch.setattr("iac_code.commands.auth._input_masked", lambda *a, **kw: None) - console = MagicMock() - store = MagicMock() - result = _llm_auth_flow(console, store) + result = _llm_auth_flow(MagicMock(), MagicMock()) assert isinstance(result, str) - assert result # "Auth cancelled" or similar translated string + assert result + + def test_single_item_group_skips_sub_selection(self, monkeypatch): + """Groups with one provider (e.g. DeepSeek) skip the sub-provider step.""" + call_count = {"select": 0} + + def select_side_effect(title, options, default_index=0): + call_count["select"] += 1 + if call_count["select"] == 1: + for i, opt in enumerate(options): + if "DeepSeek" in opt: + return i + return 6 + return None + + monkeypatch.setattr("iac_code.commands.auth._select", select_side_effect) + monkeypatch.setattr("iac_code.commands.auth._load_existing_key", lambda key_name: None) + monkeypatch.setattr("iac_code.commands.auth._input_masked", lambda *a, **kw: _BACK) + result = _llm_auth_flow(MagicMock(), MagicMock()) + # _input_masked returns _BACK → loops; second _select returns None → _BACK + assert result is _BACK + assert call_count["select"] == 2 # group + group again (no sub-provider step) def test_successful_config_saves_and_returns_string(self, monkeypatch): - # Full happy path through _llm_auth_flow (non-openapi_compatible provider) + # _select always returns 0 → Alibaba Cloud → DashScope (first sub-provider) monkeypatch.setattr("iac_code.commands.auth._select", lambda title, options, default_index=0: 0) monkeypatch.setattr("iac_code.commands.auth._load_existing_key", lambda key_name: None) monkeypatch.setattr("iac_code.commands.auth._input_masked", lambda *a, **kw: "test-api-key") @@ -182,16 +209,31 @@ def test_successful_config_saves_and_returns_string(self, monkeypatch): "iac_code.commands.auth.save_active_provider_config", lambda provider, model, effort=None, api_base=None: None, ) - console = MagicMock() store = MagicMock() - result = _llm_auth_flow(console, store) + result = _llm_auth_flow(MagicMock(), store) assert isinstance(result, str) assert "qwen3.6-plus" in result store.set_state.assert_called_once_with(model="qwen3.6-plus") def test_openapi_compatible_uses_api_base_and_existing_key(self, monkeypatch): monkeypatch.setattr("iac_code.commands.auth._get_active_key_name", lambda: "openapi_compatible") - monkeypatch.setattr("iac_code.commands.auth._select", lambda title, options, default_index=0: 5) + calls = {"select": 0} + + def select_side_effect(title, options, default_index=0): + calls["select"] += 1 + if calls["select"] == 1: + for i, opt in enumerate(options): + if "Compatible" in opt: + return i + return default_index + if calls["select"] == 2: + for i, opt in enumerate(options): + if "OpenAPI" in opt: + return i + return 0 + return 0 + + monkeypatch.setattr("iac_code.commands.auth._select", select_side_effect) monkeypatch.setattr("iac_code.commands.auth._load_existing_api_base", lambda key_name: "https://old.example/v1") monkeypatch.setattr( "iac_code.commands.auth._input_text_with_default", lambda *a, **kw: "https://new.example/v1" @@ -227,13 +269,21 @@ def test_openapi_compatible_uses_api_base_and_existing_key(self, monkeypatch): } store.set_state.assert_called_once_with(model="custom-model") - def test_openapi_compatible_empty_api_base_restarts_provider_selection(self, monkeypatch): + def test_openapi_compatible_empty_api_base_restarts_group_selection(self, monkeypatch): calls = {"select": 0} def select_side_effect(title, options, default_index=0): calls["select"] += 1 if calls["select"] == 1: - return 5 + for i, opt in enumerate(options): + if "Compatible" in opt: + return i + return len(options) - 1 + if calls["select"] == 2: + for i, opt in enumerate(options): + if "OpenAPI" in opt: + return i + return 0 return None monkeypatch.setattr("iac_code.commands.auth._select", select_side_effect) @@ -242,7 +292,7 @@ def select_side_effect(title, options, default_index=0): result = _llm_auth_flow(MagicMock(), MagicMock()) assert result is _BACK - assert calls["select"] == 2 + assert calls["select"] == 3 class TestCloudAuthFlow: diff --git a/tests/providers/test_manager.py b/tests/providers/test_manager.py index defb5817..1f5f4de0 100644 --- a/tests/providers/test_manager.py +++ b/tests/providers/test_manager.py @@ -334,5 +334,5 @@ def test_unknown_model_still_raises(self, monkeypatch): def test_auto_mapped_model_without_api_key_raises(self, monkeypatch): """Model prefix resolves the provider, but empty credential raises ValueError.""" monkeypatch.setattr("iac_code.config.get_active_provider_key", lambda: None) - with pytest.raises(ValueError, match="No API key configured for provider 'anthropic'"): + with pytest.raises(ValueError, match="No API key configured for provider"): create_provider("claude-sonnet-4-6", credentials={"anthropic": ""}) diff --git a/tests/providers/test_new_providers.py b/tests/providers/test_new_providers.py new file mode 100644 index 00000000..73385239 --- /dev/null +++ b/tests/providers/test_new_providers.py @@ -0,0 +1,137 @@ +"""Tests for new Phase 2 provider classes.""" + +from __future__ import annotations + + +class TestNewProviderImports: + """Verify all new provider classes can be imported and instantiated.""" + + def test_kimi_provider(self): + from iac_code.providers.kimi_provider import KimiProvider + + p = KimiProvider(model="kimi-k2.6", api_key="test") + assert p.get_model_name() == "kimi-k2.6" + assert p._PROVIDER_KEY == "kimi_cn" + + def test_zhipu_provider(self): + from iac_code.providers.zhipu_provider import ZhiPuProvider + + p = ZhiPuProvider(model="glm-5.1", api_key="test") + assert p.get_model_name() == "glm-5.1" + assert p._PROVIDER_KEY == "zhipu_cn" + + def test_volcengine_provider(self): + from iac_code.providers.volcengine_provider import VolcengineProvider + + p = VolcengineProvider(model="doubao-seed", api_key="test") + assert p.get_model_name() == "doubao-seed" + assert p._PROVIDER_KEY == "volcengine_cn" + + def test_siliconflow_provider(self): + from iac_code.providers.siliconflow_provider import SiliconFlowProvider + + p = SiliconFlowProvider(model="test-model", api_key="test") + assert p.get_model_name() == "test-model" + assert p._PROVIDER_KEY == "siliconflow_cn" + + def test_ollama_provider_default_api_key(self): + from iac_code.providers.ollama_provider import OllamaProvider + + p = OllamaProvider(model="llama3") + assert p.get_model_name() == "llama3" + assert p._PROVIDER_KEY == "ollama" + + def test_lmstudio_provider_default_api_key(self): + from iac_code.providers.lmstudio_provider import LMStudioProvider + + p = LMStudioProvider(model="local-model") + assert p.get_model_name() == "local-model" + assert p._PROVIDER_KEY == "lmstudio" + + def test_openrouter_provider(self): + from iac_code.providers.openrouter_provider import OpenRouterProvider + + p = OpenRouterProvider(model="test", api_key="test") + assert p.get_model_name() == "test" + assert p._PROVIDER_KEY == "openrouter" + + def test_azure_openai_provider(self): + from iac_code.providers.azure_openai_provider import AzureOpenAIProvider + + p = AzureOpenAIProvider(model="gpt-5", api_key="test") + assert p.get_model_name() == "gpt-5" + assert p._PROVIDER_KEY == "azure_openai" + + def test_modelscope_provider(self): + from iac_code.providers.modelscope_provider import ModelScopeProvider + + p = ModelScopeProvider(model="Qwen/Qwen3.5", api_key="test") + assert p.get_model_name() == "Qwen/Qwen3.5" + assert p._PROVIDER_KEY == "modelscope" + + def test_minimax_provider(self): + from iac_code.providers.minimax_provider import MiniMaxProvider + + p = MiniMaxProvider(model="MiniMax-M2.5", api_key="test") + assert p.get_model_name() == "MiniMax-M2.5" + assert p._PROVIDER_KEY == "minimax_cn" + + def test_gemini_provider(self): + from iac_code.providers.gemini_provider import GeminiProvider + + p = GeminiProvider(model="gemini-2.5-pro", api_key="test") + assert p.get_model_name() == "gemini-2.5-pro" + assert p._PROVIDER_KEY == "gemini" + + +class TestProviderKeyOverride: + """Verify provider_key parameter is respected.""" + + def test_kimi_intl_key(self): + from iac_code.providers.kimi_provider import KimiProvider + + p = KimiProvider(model="kimi-k2.6", api_key="test", provider_key="kimi_intl") + assert p._PROVIDER_KEY == "kimi_intl" + + def test_zhipu_intl_key(self): + from iac_code.providers.zhipu_provider import ZhiPuProvider + + p = ZhiPuProvider(model="glm-5", api_key="test", provider_key="zhipu_intl") + assert p._PROVIDER_KEY == "zhipu_intl" + + +class TestRegistryDrivenCreateProvider: + def test_creates_kimi_provider(self, monkeypatch): + monkeypatch.setattr("iac_code.config.get_active_provider_key", lambda: "kimi_cn") + monkeypatch.setattr("iac_code.config.get_provider_config", lambda name: {}) + from iac_code.providers.manager import create_provider + + p = create_provider("kimi-k2.6", credentials={"kimi_cn": "test-key"}) + assert p.get_model_name() == "kimi-k2.6" + + def test_creates_gemini_provider(self, monkeypatch): + monkeypatch.setattr("iac_code.config.get_active_provider_key", lambda: "gemini") + monkeypatch.setattr("iac_code.config.get_provider_config", lambda name: {}) + from iac_code.providers.manager import create_provider + + p = create_provider("gemini-2.5-pro", credentials={"gemini": "test-key"}) + assert p.get_model_name() == "gemini-2.5-pro" + + def test_creates_ollama_without_api_key(self, monkeypatch): + monkeypatch.setattr("iac_code.config.get_active_provider_key", lambda: "ollama") + monkeypatch.setattr("iac_code.config.get_provider_config", lambda name: {}) + from iac_code.providers.manager import create_provider + + p = create_provider("llama3", credentials={}) + assert p.get_model_name() == "llama3" + + def test_provider_key_override(self, monkeypatch): + monkeypatch.setattr("iac_code.config.get_provider_config", lambda name: {}) + from iac_code.providers.manager import create_provider + + p = create_provider( + "glm-5", + credentials={"zhipu_intl": "test-key"}, + provider_key_override="zhipu_intl", + ) + assert p._PROVIDER_KEY == "zhipu_intl" diff --git a/tests/providers/test_registry.py b/tests/providers/test_registry.py new file mode 100644 index 00000000..019aa08a --- /dev/null +++ b/tests/providers/test_registry.py @@ -0,0 +1,91 @@ +"""Tests for the Provider Registry.""" + +from __future__ import annotations + +from iac_code.providers.registry import PROVIDER_REGISTRY, ModelEntry, ProviderDescriptor + + +class TestProviderRegistry: + def test_registry_is_not_empty(self): + assert len(PROVIDER_REGISTRY) > 0 + + def test_all_descriptors_have_required_fields(self): + for key, desc in PROVIDER_REGISTRY.items(): + assert desc.key == key + assert desc.name + assert desc.display_name + assert desc.provider_class + + def test_known_providers_present(self): + expected = [ + "dashscope", + "openai", + "anthropic", + "deepseek", + "gemini", + "kimi_cn", + "kimi_intl", + "zhipu_cn", + "minimax_cn", + "volcengine_cn", + "ollama", + "lmstudio", + "openrouter", + "azure_openai", + "modelscope", + "openapi_compatible", + ] + for key in expected: + assert key in PROVIDER_REGISTRY, f"Missing provider: {key}" + + def test_default_model_returns_first_if_no_default(self): + desc = ProviderDescriptor( + key="test", + name="Test", + display_name="Test", + provider_class="test.TestProvider", + base_url=None, + models=[ModelEntry("m1"), ModelEntry("m2")], + ) + assert desc.default_model == "m1" + + def test_default_model_returns_marked_default(self): + desc = ProviderDescriptor( + key="test", + name="Test", + display_name="Test", + provider_class="test.TestProvider", + base_url=None, + models=[ModelEntry("m1"), ModelEntry("m2", is_default=True)], + ) + assert desc.default_model == "m2" + + def test_model_ids_property(self): + desc = ProviderDescriptor( + key="test", + name="Test", + display_name="Test", + provider_class="test.TestProvider", + base_url=None, + models=[ModelEntry("m1"), ModelEntry("m2")], + ) + assert desc.model_ids == ["m1", "m2"] + + def test_local_providers_no_api_key_required(self): + for key in ("ollama", "lmstudio"): + desc = PROVIDER_REGISTRY[key] + assert desc.require_api_key is False + assert desc.is_local is True + + def test_qwenpaw_provider_ids_populated(self): + compatible_keys = {"openapi_compatible", "anthropic_compatible"} + for key, desc in PROVIDER_REGISTRY.items(): + if key not in compatible_keys: + assert desc.qwenpaw_provider_ids, f"{key} missing qwenpaw_provider_ids" + + def test_dashscope_models_include_qwen(self): + desc = PROVIDER_REGISTRY["dashscope"] + assert any("qwen" in m.id for m in desc.models) + + def test_codingplan_providers_share_class_with_parent(self): + assert PROVIDER_REGISTRY["aliyun_codingplan"].provider_class == PROVIDER_REGISTRY["dashscope"].provider_class diff --git a/tests/services/test_qwenpaw_source.py b/tests/services/test_qwenpaw_source.py new file mode 100644 index 00000000..07c46991 --- /dev/null +++ b/tests/services/test_qwenpaw_source.py @@ -0,0 +1,236 @@ +"""Tests for QwenPaw configuration source.""" + +from __future__ import annotations + +import json +from unittest.mock import patch + +import pytest + +from iac_code.services.qwenpaw_source import ( + QwenPawError, + _decrypt_api_key, + _map_qwenpaw_provider_id, + _read_active_model, + _read_provider_config, + _resolve_secret_dir, + load_from_qwenpaw, +) + + +def _clear_env(monkeypatch): + monkeypatch.delenv("QWENPAW_SECRET_DIR", raising=False) + monkeypatch.delenv("COPAW_SECRET_DIR", raising=False) + + +class TestResolveSecretDir: + def test_env_var_takes_precedence(self, tmp_path, monkeypatch): + secret = tmp_path / "my_secret" + secret.mkdir() + monkeypatch.setenv("QWENPAW_SECRET_DIR", str(secret)) + assert _resolve_secret_dir() == secret + + def test_copaw_env_fallback(self, tmp_path, monkeypatch): + _clear_env(monkeypatch) + secret = tmp_path / "copaw_secret" + secret.mkdir() + monkeypatch.setenv("COPAW_SECRET_DIR", str(secret)) + assert _resolve_secret_dir() == secret + + def test_home_qwenpaw_secret(self, tmp_path, monkeypatch): + _clear_env(monkeypatch) + monkeypatch.chdir(tmp_path) + qwenpaw_dir = tmp_path / ".qwenpaw.secret" + qwenpaw_dir.mkdir() + with patch("iac_code.services.qwenpaw_source.Path.home", return_value=tmp_path): + assert _resolve_secret_dir() == qwenpaw_dir + + def test_home_copaw_secret_legacy(self, tmp_path, monkeypatch): + _clear_env(monkeypatch) + monkeypatch.chdir(tmp_path) + copaw_dir = tmp_path / ".copaw.secret" + copaw_dir.mkdir() + with patch("iac_code.services.qwenpaw_source.Path.home", return_value=tmp_path): + assert _resolve_secret_dir() == copaw_dir + + def test_cwd_dot_secret(self, tmp_path, monkeypatch): + _clear_env(monkeypatch) + secret = tmp_path / ".secret" + secret.mkdir() + monkeypatch.chdir(tmp_path) + with patch("iac_code.services.qwenpaw_source.Path.home", return_value=tmp_path / "nohome"): + assert _resolve_secret_dir() == secret + + def test_returns_none_when_no_secret(self, tmp_path, monkeypatch): + _clear_env(monkeypatch) + monkeypatch.chdir(tmp_path) + with patch("iac_code.services.qwenpaw_source.Path.home", return_value=tmp_path / "nohome"): + assert _resolve_secret_dir() is None + + +class TestReadActiveModel: + def test_reads_from_providers_subdir(self, tmp_path): + providers = tmp_path / "providers" + providers.mkdir() + data = {"model": "qwen3.6-plus", "provider_id": "dashscope"} + (providers / "active_model.json").write_text(json.dumps(data)) + assert _read_active_model(tmp_path) == data + + def test_reads_from_root_fallback(self, tmp_path): + data = {"model": "qwen3.6-plus", "provider": "dashscope"} + (tmp_path / "active_model.json").write_text(json.dumps(data)) + assert _read_active_model(tmp_path) == data + + def test_prefers_providers_subdir_over_root(self, tmp_path): + providers = tmp_path / "providers" + providers.mkdir() + subdir_data = {"model": "new-model", "provider_id": "openai"} + root_data = {"model": "old-model", "provider": "dashscope"} + (providers / "active_model.json").write_text(json.dumps(subdir_data)) + (tmp_path / "active_model.json").write_text(json.dumps(root_data)) + assert _read_active_model(tmp_path) == subdir_data + + def test_returns_none_when_missing(self, tmp_path): + assert _read_active_model(tmp_path) is None + + def test_returns_none_for_invalid_json(self, tmp_path): + providers = tmp_path / "providers" + providers.mkdir() + (providers / "active_model.json").write_text("not json") + (tmp_path / "active_model.json").write_text("also not json") + assert _read_active_model(tmp_path) is None + + +class TestReadProviderConfig: + def test_reads_from_builtin_subdir(self, tmp_path): + builtin = tmp_path / "providers" / "builtin" + builtin.mkdir(parents=True) + data = {"api_key": "ENC:xxx", "base_url": "https://example.com"} + (builtin / "dashscope.json").write_text(json.dumps(data)) + assert _read_provider_config(tmp_path, "dashscope") == data + + def test_reads_from_custom_subdir(self, tmp_path): + custom = tmp_path / "providers" / "custom" + custom.mkdir(parents=True) + data = {"api_key": "ENC:yyy", "base_url": "https://custom.com"} + (custom / "my-provider.json").write_text(json.dumps(data)) + assert _read_provider_config(tmp_path, "my-provider") == data + + def test_reads_flat_fallback(self, tmp_path): + providers_dir = tmp_path / "providers" + providers_dir.mkdir() + data = {"api_key": "encrypted_key", "base_url": "https://example.com"} + (providers_dir / "dashscope.json").write_text(json.dumps(data)) + assert _read_provider_config(tmp_path, "dashscope") == data + + def test_returns_none_when_missing(self, tmp_path): + assert _read_provider_config(tmp_path, "nonexistent") is None + + +class TestDecryptApiKey: + def test_plaintext_passthrough(self): + assert _decrypt_api_key("sk-plain-key") == "sk-plain-key" + + def test_empty_returns_none(self): + assert _decrypt_api_key("") is None + + def test_enc_prefix_stripped_before_decrypt(self): + fake_hex_key = "aa" * 32 # 64-char valid hex → 32 bytes + with patch("iac_code.services.qwenpaw_source._get_master_key", return_value=fake_hex_key): + with patch("cryptography.fernet.Fernet") as mock_fernet_cls: + mock_fernet = mock_fernet_cls.return_value + mock_fernet.decrypt.return_value = b"decrypted-key" + result = _decrypt_api_key("ENC:ciphertext123") + mock_fernet.decrypt.assert_called_once_with(b"ciphertext123") + assert result == "decrypted-key" + + +class TestMapQwenPawProviderId: + def test_maps_known_provider(self): + assert _map_qwenpaw_provider_id("dashscope") == "dashscope" + + def test_maps_kimi_cn(self): + assert _map_qwenpaw_provider_id("kimi-cn") == "kimi_cn" + + def test_maps_anthropic(self): + assert _map_qwenpaw_provider_id("anthropic") == "anthropic" + + def test_returns_none_for_unknown(self): + assert _map_qwenpaw_provider_id("totally-unknown") is None + + +class TestLoadFromQwenPaw: + def test_returns_none_when_no_secret_dir(self, tmp_path, monkeypatch): + _clear_env(monkeypatch) + monkeypatch.chdir(tmp_path) + with patch("iac_code.services.qwenpaw_source.Path.home", return_value=tmp_path / "nohome"): + assert load_from_qwenpaw() is None + + def test_returns_config_with_provider_id_field(self, tmp_path, monkeypatch): + """QwenPaw uses 'provider_id' (not 'provider') in active_model.json.""" + secret = tmp_path / ".secret" + secret.mkdir() + providers = secret / "providers" + providers.mkdir() + active = {"model": "qwen3.6-plus", "provider_id": "dashscope"} + (providers / "active_model.json").write_text(json.dumps(active)) + builtin = providers / "builtin" + builtin.mkdir() + provider_cfg = {"api_key": "", "base_url": "https://custom.example.com/v1"} + (builtin / "dashscope.json").write_text(json.dumps(provider_cfg)) + monkeypatch.setenv("QWENPAW_SECRET_DIR", str(secret)) + + with patch("iac_code.services.qwenpaw_source._decrypt_api_key", return_value=None): + result = load_from_qwenpaw() + + assert result is not None + assert result.model == "qwen3.6-plus" + assert result.provider_key == "dashscope" + assert result.base_url == "https://custom.example.com/v1" + + def test_returns_config_with_legacy_provider_field(self, tmp_path, monkeypatch): + """Backward compatibility: 'provider' field still works.""" + secret = tmp_path / ".secret" + secret.mkdir() + active = {"model": "qwen3.6-plus", "provider": "dashscope"} + (secret / "active_model.json").write_text(json.dumps(active)) + providers_dir = secret / "providers" + providers_dir.mkdir() + provider_cfg = {"api_key": "", "base_url": "https://custom.example.com/v1"} + (providers_dir / "dashscope.json").write_text(json.dumps(provider_cfg)) + monkeypatch.setenv("QWENPAW_SECRET_DIR", str(secret)) + + with patch("iac_code.services.qwenpaw_source._decrypt_api_key", return_value=None): + result = load_from_qwenpaw() + + assert result is not None + assert result.model == "qwen3.6-plus" + assert result.provider_key == "dashscope" + + def test_raises_on_unknown_provider(self, tmp_path, monkeypatch): + secret = tmp_path / ".secret" + secret.mkdir() + providers = secret / "providers" + providers.mkdir() + active = {"model": "some-model", "provider_id": "totally-unknown-provider"} + (providers / "active_model.json").write_text(json.dumps(active)) + monkeypatch.setenv("QWENPAW_SECRET_DIR", str(secret)) + with pytest.raises(QwenPawError, match="totally-unknown-provider") as exc_info: + load_from_qwenpaw() + assert "QwenPaw" in str(exc_info.value) + assert "settings.yml" in str(exc_info.value) + + def test_uses_registry_base_url_as_fallback(self, tmp_path, monkeypatch): + secret = tmp_path / ".secret" + secret.mkdir() + providers = secret / "providers" + providers.mkdir() + active = {"model": "qwen3.6-plus", "provider_id": "dashscope"} + (providers / "active_model.json").write_text(json.dumps(active)) + monkeypatch.setenv("QWENPAW_SECRET_DIR", str(secret)) + + with patch("iac_code.services.qwenpaw_source._decrypt_api_key", return_value=None): + result = load_from_qwenpaw() + + assert result is not None + assert result.base_url == "https://dashscope.aliyuncs.com/compatible-mode/v1" diff --git a/tests/test_config_env_overrides.py b/tests/test_config_env_overrides.py index e25730b6..682a4f1e 100644 --- a/tests/test_config_env_overrides.py +++ b/tests/test_config_env_overrides.py @@ -78,7 +78,7 @@ def test_provider_invalid_raises_with_canonical_names(self, monkeypatch): with pytest.raises(ValueError) as exc: _get_env_overrides() msg = str(exc.value) - for canonical in ("Anthropic", "OpenAI", "DashScope", "DeepSeek", "OpenAPICompatible"): + for canonical in ("Anthropic", "OpenAI", "DashScope", "DeepSeek"): assert canonical in msg def test_all_four_env_vars_returned(self, monkeypatch): @@ -245,14 +245,10 @@ def test_returns_empty_when_no_files(self, monkeypatch, tmp_path): with patch("iac_code.config.Path.home", return_value=tmp_path): from iac_code.config import load_credentials - assert load_credentials() == { - "anthropic": "", - "openai": "", - "dashscope": "", - "dashscope_token_plan": "", - "deepseek": "", - "openapi_compatible": "", - } + creds = load_credentials() + for key in ("anthropic", "openai", "dashscope", "dashscope_token_plan", "deepseek", "openapi_compatible"): + assert creds[key] == "" + assert all(v == "" for v in creds.values()) def test_loads_all_slots_from_file(self, monkeypatch, tmp_path): from unittest.mock import patch @@ -269,14 +265,13 @@ def test_loads_all_slots_from_file(self, monkeypatch, tmp_path): with patch("iac_code.config.Path.home", return_value=tmp_path): from iac_code.config import load_credentials - assert load_credentials() == { - "anthropic": "ak", - "openai": "ok", - "dashscope": "ds", - "dashscope_token_plan": "tp", - "deepseek": "dk", - "openapi_compatible": "oc", - } + creds = load_credentials() + assert creds["anthropic"] == "ak" + assert creds["openai"] == "ok" + assert creds["dashscope"] == "ds" + assert creds["dashscope_token_plan"] == "tp" + assert creds["deepseek"] == "dk" + assert creds["openapi_compatible"] == "oc" def test_bailian_legacy_key_falls_back_to_dashscope_slot(self, monkeypatch, tmp_path): from unittest.mock import patch @@ -407,7 +402,7 @@ def test_canonical_name_case_insensitive(self, monkeypatch): def test_canonical_name_listed_in_constants(self): from iac_code.config import _PROVIDER_CANONICAL_NAMES - assert "DashScopeTokenPlan" in _PROVIDER_CANONICAL_NAMES + assert "DashScope Token Plan" in _PROVIDER_CANONICAL_NAMES class TestDashScopeTokenPlanCredSlot: diff --git a/tests/ui/dialogs/test_model_picker.py b/tests/ui/dialogs/test_model_picker.py index ab39c839..f420b2fd 100644 --- a/tests/ui/dialogs/test_model_picker.py +++ b/tests/ui/dialogs/test_model_picker.py @@ -140,7 +140,7 @@ def test_header_display_name(self): picker = make_picker(configured_providers=["dashscope"]) items = picker._build_items() headers = [it["header"] for it in items if "header" in it] - assert any("阿里云百炼" in h for h in headers) + assert any("Alibaba Cloud Bailian" in h for h in headers) def test_items_carry_provider_key(self): picker = make_picker(configured_providers=["dashscope", "openai"]) @@ -416,7 +416,7 @@ def test_render_header_present(self): console = Console(file=buf, highlight=False, markup=False) console.print(picker.render()) output = buf.getvalue() - assert "阿里云百炼" in output + assert "Alibaba Cloud Bailian" in output def test_render_focused_model_has_indicator(self): from io import StringIO diff --git a/tests/ui/test_banner.py b/tests/ui/test_banner.py index 2f6663f5..36cfe21e 100644 --- a/tests/ui/test_banner.py +++ b/tests/ui/test_banner.py @@ -129,22 +129,12 @@ def test_get_provider_config_raises_returns_empty(self): result = self._call() assert result == "" - def test_dashscope_token_plan_display_map_translates(self): - # Patch gettext to a known marker so we can prove the dict lookup - # actually hit the new entry — fallback to raw passthrough would - # not invoke _() and the marker would be absent. - with ( - patch("iac_code.config.get_active_provider_key", return_value="k"), - patch( - "iac_code.config.get_provider_config", - return_value={"name": "DashScope Token Plan"}, - ), - patch("iac_code.ui.banner._", side_effect=lambda s: f""), - ): + def test_dashscope_token_plan_display_from_registry(self): + with patch("iac_code.config.get_active_provider_key", return_value="dashscope_token_plan"): from iac_code.ui import banner result = banner._get_provider_display() - assert result == "" + assert result == "Alibaba Cloud Bailian Token Plan" # --------------------------------------------------------------------------- diff --git a/uv.lock b/uv.lock index 74925cbe..d580670c 100644 --- a/uv.lock +++ b/uv.lock @@ -371,6 +371,15 @@ wheels = [ { url = "https://mirrors.aliyun.com/pypi/packages/a0/59/76ab57e3fe74484f48a53f8e337171b4a2349e506eabe136d7e01d059086/backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5" }, ] +[[package]] +name = "backports-tarfile" +version = "1.2.0" +source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/86/72/cd9b395f25e290e633655a100af28cb253e4393396264a98bd5f5951d50f/backports_tarfile-1.2.0.tar.gz", hash = "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34" }, +] + [[package]] name = "certifi" version = "2026.2.25" @@ -1127,8 +1136,10 @@ dependencies = [ { name = "alibabacloud-credentials" }, { name = "alibabacloud-ros20190910" }, { name = "anthropic" }, + { name = "cryptography" }, { name = "httpx" }, { name = "jsonschema" }, + { name = "keyring" }, { name = "loguru" }, { name = "openai" }, { name = "opentelemetry-distro" }, @@ -1167,8 +1178,10 @@ requires-dist = [ { name = "alibabacloud-credentials", specifier = ">=0.3.0" }, { name = "alibabacloud-ros20190910", specifier = ">=3.0.0" }, { name = "anthropic", specifier = ">=0.40" }, + { name = "cryptography", specifier = ">=42.0" }, { name = "httpx", specifier = ">=0.27.0" }, { name = "jsonschema", specifier = ">=4.20" }, + { name = "keyring", specifier = ">=25.0" }, { name = "loguru", specifier = ">=0.7.0" }, { name = "openai", specifier = ">=1.50" }, { name = "opentelemetry-distro", specifier = ">=0.48b0" }, @@ -1237,6 +1250,51 @@ wheels = [ { url = "https://mirrors.aliyun.com/pypi/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12" }, ] +[[package]] +name = "jaraco-classes" +version = "3.4.0" +source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790" }, +] + +[[package]] +name = "jaraco-context" +version = "6.1.2" +source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } +dependencies = [ + { name = "backports-tarfile", marker = "python_full_version < '3.12'" }, +] +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/af/50/4763cd07e722bb6285316d390a164bc7e479db9d90daa769f22578f698b4/jaraco_context-6.1.2.tar.gz", hash = "sha256:f1a6c9d391e661cc5b8d39861ff077a7dc24dc23833ccee564b234b81c82dfe3" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/f2/58/bc8954bda5fcda97bd7c19be11b85f91973d67a706ed4a3aec33e7de22db/jaraco_context-6.1.2-py3-none-any.whl", hash = "sha256:bf8150b79a2d5d91ae48629d8b427a8f7ba0e1097dd6202a9059f29a36379535" }, +] + +[[package]] +name = "jaraco-functools" +version = "4.4.0" +source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/0f/27/056e0638a86749374d6f57d0b0db39f29509cce9313cf91bdc0ac4d91084/jaraco_functools-4.4.0.tar.gz", hash = "sha256:da21933b0417b89515562656547a77b4931f98176eb173644c0d35032a33d6bb" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/fd/c4/813bb09f0985cb21e959f21f2464169eca882656849adf727ac7bb7e1767/jaraco_functools-4.4.0-py3-none-any.whl", hash = "sha256:9eec1e36f45c818d9bf307c8948eb03b2b56cd44087b3cdc989abca1f20b9176" }, +] + +[[package]] +name = "jeepney" +version = "0.9.0" +source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683" }, +] + [[package]] name = "jiter" version = "0.14.0" @@ -1367,6 +1425,24 @@ wheels = [ { url = "https://mirrors.aliyun.com/pypi/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe" }, ] +[[package]] +name = "keyring" +version = "25.7.0" +source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } +dependencies = [ + { name = "importlib-metadata", marker = "python_full_version < '3.12'" }, + { name = "jaraco-classes" }, + { name = "jaraco-context" }, + { name = "jaraco-functools" }, + { name = "jeepney", marker = "sys_platform == 'linux'" }, + { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" }, + { name = "secretstorage", marker = "sys_platform == 'linux'" }, +] +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/43/4b/674af6ef2f97d56f0ab5153bf0bfa28ccb6c3ed4d1babf4305449668807b/keyring-25.7.0.tar.gz", hash = "sha256:fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl", hash = "sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f" }, +] + [[package]] name = "loguru" version = "0.7.3" @@ -1401,6 +1477,15 @@ wheels = [ { url = "https://mirrors.aliyun.com/pypi/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8" }, ] +[[package]] +name = "more-itertools" +version = "11.0.2" +source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/a2/f7/139d22fef48ac78127d18e01d80cf1be40236ae489769d17f35c3d425293/more_itertools-11.0.2.tar.gz", hash = "sha256:392a9e1e362cbc106a2457d37cabf9b36e5e12efd4ebff1654630e76597df804" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/cb/98/6af411189d9413534c3eb691182bff1f5c6d44ed2f93f2edfe52a1bbceb8/more_itertools-11.0.2-py3-none-any.whl", hash = "sha256:6e35b35f818b01f691643c6c611bc0902f2e92b46c18fffa77ae1e7c46e912e4" }, +] + [[package]] name = "multidict" version = "6.7.1" @@ -2120,6 +2205,15 @@ wheels = [ { url = "https://mirrors.aliyun.com/pypi/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a" }, ] +[[package]] +name = "pywin32-ctypes" +version = "0.2.3" +source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8" }, +] + [[package]] name = "pyyaml" version = "6.0.3" @@ -2494,6 +2588,19 @@ wheels = [ { url = "https://mirrors.aliyun.com/pypi/packages/58/ed/dea90a65b7d9e69888890fb14c90d7f51bf0c1e82ad800aeb0160e4bacfd/ruff-0.15.10-py3-none-win_arm64.whl", hash = "sha256:601d1610a9e1f1c2165a4f561eeaa2e2ea1e97f3287c5aa258d3dab8b57c6188" }, ] +[[package]] +name = "secretstorage" +version = "3.5.0" +source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } +dependencies = [ + { name = "cryptography" }, + { name = "jeepney" }, +] +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137" }, +] + [[package]] name = "setuptools" version = "82.0.1"