@clawpump/claw-agent 0.1.5 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agent/.dockerignore +67 -0
- package/agent/.envrc +1 -1
- package/agent/.gitattributes +8 -0
- package/agent/AGENTS.md +216 -4
- package/agent/CONTRIBUTING.md +46 -8
- package/agent/Dockerfile +78 -35
- package/agent/MANIFEST.in +2 -0
- package/agent/README.md +12 -5
- package/agent/README.ur-pk.md +261 -0
- package/agent/README.zh-CN.md +11 -8
- package/agent/SECURITY.md +5 -4
- package/agent/acp_adapter/provenance.py +127 -0
- package/agent/acp_adapter/server.py +112 -5
- package/agent/acp_adapter/session.py +1 -6
- package/agent/acp_registry/agent.json +2 -2
- package/agent/agent/account_usage.py +313 -1
- package/agent/agent/agent_init.py +140 -37
- package/agent/agent/agent_runtime_helpers.py +342 -83
- package/agent/agent/anthropic_adapter.py +320 -33
- package/agent/agent/auxiliary_client.py +525 -105
- package/agent/agent/background_review.py +157 -19
- package/agent/agent/bedrock_adapter.py +71 -6
- package/agent/agent/billing_view.py +295 -0
- package/agent/agent/chat_completion_helpers.py +229 -4
- package/agent/agent/codex_responses_adapter.py +86 -10
- package/agent/agent/codex_runtime.py +153 -1
- package/agent/agent/coding_context.py +738 -0
- package/agent/agent/context_compressor.py +392 -44
- package/agent/agent/context_references.py +34 -1
- package/agent/agent/conversation_compression.py +159 -22
- package/agent/agent/conversation_loop.py +643 -908
- package/agent/agent/copilot_acp_client.py +4 -11
- package/agent/agent/credential_pool.py +5 -3
- package/agent/agent/credits_tracker.py +794 -0
- package/agent/agent/curator.py +91 -18
- package/agent/agent/curator_backup.py +26 -10
- package/agent/agent/display.py +42 -1
- package/agent/agent/error_classifier.py +52 -3
- package/agent/agent/errors.py +3 -0
- package/agent/agent/file_safety.py +0 -17
- package/agent/agent/gemini_native_adapter.py +31 -1
- package/agent/agent/i18n.py +48 -4
- package/agent/agent/image_gen_provider.py +74 -5
- package/agent/agent/image_routing.py +29 -0
- package/agent/agent/insights.py +8 -17
- package/agent/agent/lsp/install.py +3 -0
- package/agent/agent/memory_manager.py +326 -31
- package/agent/agent/message_content.py +50 -0
- package/agent/agent/model_metadata.py +214 -3
- package/agent/agent/moonshot_schema.py +8 -1
- package/agent/agent/onboarding.py +60 -0
- package/agent/agent/prompt_builder.py +327 -37
- package/agent/agent/redact.py +1 -0
- package/agent/agent/runtime_cwd.py +34 -5
- package/agent/agent/secret_scope.py +205 -0
- package/agent/agent/secret_sources/bitwarden.py +34 -2
- package/agent/agent/skill_commands.py +90 -1
- package/agent/agent/skill_preprocessing.py +1 -0
- package/agent/agent/skill_utils.py +209 -36
- package/agent/agent/ssl_guard.py +94 -0
- package/agent/agent/system_prompt.py +133 -5
- package/agent/agent/tool_executor.py +496 -70
- package/agent/agent/transports/anthropic.py +83 -21
- package/agent/agent/transports/chat_completions.py +94 -5
- package/agent/agent/transports/codex.py +67 -2
- package/agent/agent/transports/codex_app_server.py +1 -0
- package/agent/agent/transports/codex_app_server_session.py +30 -0
- package/agent/agent/transports/types.py +12 -0
- package/agent/agent/turn_context.py +408 -0
- package/agent/agent/turn_finalizer.py +428 -0
- package/agent/agent/turn_retry_state.py +68 -0
- package/agent/agent/usage_pricing.py +3 -0
- package/agent/apps/bootstrap-installer/package.json +6 -5
- package/agent/apps/bootstrap-installer/src/routes/failure.tsx +12 -5
- package/agent/apps/bootstrap-installer/src/routes/progress.tsx +1 -3
- package/agent/apps/bootstrap-installer/src/store.ts +3 -2
- package/agent/apps/bootstrap-installer/src-tauri/src/bootstrap.rs +172 -7
- package/agent/apps/bootstrap-installer/src-tauri/src/events.rs +14 -1
- package/agent/apps/bootstrap-installer/src-tauri/src/paths.rs +29 -0
- package/agent/apps/bootstrap-installer/src-tauri/src/powershell.rs +93 -3
- package/agent/apps/bootstrap-installer/src-tauri/src/update.rs +695 -39
- package/agent/apps/bootstrap-installer/tsconfig.json +3 -4
- package/agent/apps/desktop/DESIGN.md +167 -0
- package/agent/apps/desktop/README.md +20 -16
- package/agent/apps/desktop/assets/icon.icns +0 -0
- package/agent/apps/desktop/assets/icon.ico +0 -0
- package/agent/apps/desktop/assets/icon.png +0 -0
- package/agent/apps/desktop/electron/backend-env.cjs +112 -0
- package/agent/apps/desktop/electron/backend-env.test.cjs +111 -0
- package/agent/apps/desktop/electron/backend-probes.test.cjs +3 -1
- package/agent/apps/desktop/electron/backend-ready.cjs +66 -0
- package/agent/apps/desktop/electron/bootstrap-platform.cjs +52 -0
- package/agent/apps/desktop/electron/bootstrap-platform.test.cjs +59 -1
- package/agent/apps/desktop/electron/bootstrap-runner.cjs +176 -38
- package/agent/apps/desktop/electron/bootstrap-runner.test.cjs +112 -1
- package/agent/apps/desktop/electron/connection-config.cjs +288 -0
- package/agent/apps/desktop/electron/connection-config.test.cjs +396 -0
- package/agent/apps/desktop/electron/dashboard-token.cjs +99 -0
- package/agent/apps/desktop/electron/dashboard-token.test.cjs +142 -0
- package/agent/apps/desktop/electron/desktop-uninstall.cjs +232 -0
- package/agent/apps/desktop/electron/desktop-uninstall.test.cjs +246 -0
- package/agent/apps/desktop/electron/entitlements.mac.inherit.plist +2 -0
- package/agent/apps/desktop/electron/fs-read-dir.cjs +109 -0
- package/agent/apps/desktop/electron/fs-read-dir.test.cjs +364 -0
- package/agent/apps/desktop/electron/gateway-ws-probe.cjs +188 -0
- package/agent/apps/desktop/electron/gateway-ws-probe.test.cjs +122 -0
- package/agent/apps/desktop/electron/git-root.cjs +54 -0
- package/agent/apps/desktop/electron/git-root.test.cjs +40 -0
- package/agent/apps/desktop/electron/git-worktrees.cjs +174 -0
- package/agent/apps/desktop/electron/hardening.cjs +123 -28
- package/agent/apps/desktop/electron/hardening.test.cjs +163 -0
- package/agent/apps/desktop/electron/main.cjs +3121 -331
- package/agent/apps/desktop/electron/oauth-net-request.cjs +20 -0
- package/agent/apps/desktop/electron/oauth-net-request.test.cjs +34 -0
- package/agent/apps/desktop/electron/preload.cjs +52 -2
- package/agent/apps/desktop/electron/session-windows.cjs +124 -0
- package/agent/apps/desktop/electron/session-windows.test.cjs +199 -0
- package/agent/apps/desktop/electron/update-rebuild.cjs +29 -0
- package/agent/apps/desktop/electron/update-rebuild.test.cjs +55 -0
- package/agent/apps/desktop/electron/update-remote.cjs +56 -0
- package/agent/apps/desktop/electron/update-remote.test.cjs +78 -0
- package/agent/apps/desktop/electron/vscode-marketplace.cjs +331 -0
- package/agent/apps/desktop/electron/vscode-marketplace.test.cjs +113 -0
- package/agent/apps/desktop/electron/windows-child-process.test.cjs +57 -0
- package/agent/apps/desktop/electron/windows-user-env.cjs +76 -0
- package/agent/apps/desktop/electron/windows-user-env.test.cjs +90 -0
- package/agent/apps/desktop/electron/workspace-cwd.cjs +38 -0
- package/agent/apps/desktop/electron/workspace-cwd.test.cjs +45 -0
- package/agent/apps/desktop/eslint.config.mjs +0 -3
- package/agent/apps/desktop/index.html +27 -2
- package/agent/apps/desktop/package.json +31 -11
- package/agent/apps/desktop/pr-assets/session-source-folders.png +0 -0
- package/agent/apps/desktop/public/apple-touch-icon.png +0 -0
- package/agent/apps/desktop/public/nous-girl.jpg +0 -0
- package/agent/apps/desktop/scripts/assert-dist-built.cjs +70 -0
- package/agent/apps/desktop/scripts/assert-dist-built.test.cjs +84 -0
- package/agent/apps/desktop/scripts/before-pack.cjs +78 -0
- package/agent/apps/desktop/scripts/before-pack.test.cjs +53 -0
- package/agent/apps/desktop/scripts/diag-scroll-reset.mjs +229 -0
- package/agent/apps/desktop/scripts/patch-electron-builder-mac-binary.cjs +64 -0
- package/agent/apps/desktop/scripts/run-electron-builder.cjs +57 -0
- package/agent/apps/desktop/src/app/agents/index.tsx +53 -45
- package/agent/apps/desktop/src/app/artifacts/index.tsx +102 -83
- package/agent/apps/desktop/src/app/chat/chat-drop-overlay.tsx +29 -8
- package/agent/apps/desktop/src/app/chat/chat-swap-overlay.tsx +47 -0
- package/agent/apps/desktop/src/app/chat/composer/attachments.tsx +81 -45
- package/agent/apps/desktop/src/app/chat/composer/completion-drawer.tsx +13 -24
- package/agent/apps/desktop/src/app/chat/composer/context-menu.tsx +138 -88
- package/agent/apps/desktop/src/app/chat/composer/controls.tsx +138 -90
- package/agent/apps/desktop/src/app/chat/composer/enter-submit-dom-race.test.tsx +218 -0
- package/agent/apps/desktop/src/app/chat/composer/focus.ts +32 -0
- package/agent/apps/desktop/src/app/chat/composer/help-hint.tsx +38 -25
- package/agent/apps/desktop/src/app/chat/composer/hooks/use-live-completion-adapter.ts +7 -0
- package/agent/apps/desktop/src/app/chat/composer/hooks/use-mic-recorder.ts +22 -12
- package/agent/apps/desktop/src/app/chat/composer/hooks/use-slash-completions.ts +142 -14
- package/agent/apps/desktop/src/app/chat/composer/hooks/use-voice-conversation.ts +14 -11
- package/agent/apps/desktop/src/app/chat/composer/hooks/use-voice-recorder.ts +9 -6
- package/agent/apps/desktop/src/app/chat/composer/ime-composition-dom-repro.test.tsx +108 -0
- package/agent/apps/desktop/src/app/chat/composer/index.tsx +930 -180
- package/agent/apps/desktop/src/app/chat/composer/inline-refs.ts +136 -32
- package/agent/apps/desktop/src/app/chat/composer/model-pill.tsx +86 -0
- package/agent/apps/desktop/src/app/chat/composer/queue-panel.tsx +54 -75
- package/agent/apps/desktop/src/app/chat/composer/rich-editor.test.ts +117 -1
- package/agent/apps/desktop/src/app/chat/composer/rich-editor.ts +117 -6
- package/agent/apps/desktop/src/app/chat/composer/slash-nav-dom-repro.test.tsx +186 -0
- package/agent/apps/desktop/src/app/chat/composer/status-stack/index.tsx +202 -0
- package/agent/apps/desktop/src/app/chat/composer/status-stack/status-row.tsx +155 -0
- package/agent/apps/desktop/src/app/chat/composer/text-utils.test.ts +104 -0
- package/agent/apps/desktop/src/app/chat/composer/text-utils.ts +37 -9
- package/agent/apps/desktop/src/app/chat/composer/trigger-popover.test.tsx +50 -0
- package/agent/apps/desktop/src/app/chat/composer/trigger-popover.tsx +105 -40
- package/agent/apps/desktop/src/app/chat/composer/types.ts +5 -0
- package/agent/apps/desktop/src/app/chat/composer/url-dialog.tsx +11 -15
- package/agent/apps/desktop/src/app/chat/composer/voice-activity.tsx +8 -4
- package/agent/apps/desktop/src/app/chat/hooks/use-composer-actions.test.ts +57 -0
- package/agent/apps/desktop/src/app/chat/hooks/use-composer-actions.ts +70 -16
- package/agent/apps/desktop/src/app/chat/hooks/use-file-drop-zone.ts +52 -16
- package/agent/apps/desktop/src/app/chat/index.tsx +234 -81
- package/agent/apps/desktop/src/app/chat/perf-probe.tsx +69 -21
- package/agent/apps/desktop/src/app/chat/right-rail/preview-console.tsx +44 -40
- package/agent/apps/desktop/src/app/chat/right-rail/preview-file.tsx +71 -25
- package/agent/apps/desktop/src/app/chat/right-rail/preview-pane.test.tsx +40 -1
- package/agent/apps/desktop/src/app/chat/right-rail/preview-pane.tsx +55 -53
- package/agent/apps/desktop/src/app/chat/right-rail/preview.tsx +35 -17
- package/agent/apps/desktop/src/app/chat/scroll-to-bottom-button.test.tsx +67 -0
- package/agent/apps/desktop/src/app/chat/scroll-to-bottom-button.tsx +74 -0
- package/agent/apps/desktop/src/app/chat/sidebar/cron-jobs-section.tsx +356 -0
- package/agent/apps/desktop/src/app/chat/sidebar/index.tsx +1189 -364
- package/agent/apps/desktop/src/app/chat/sidebar/load-more-row.tsx +30 -0
- package/agent/apps/desktop/src/app/chat/sidebar/order.test.ts +21 -0
- package/agent/apps/desktop/src/app/chat/sidebar/order.ts +17 -0
- package/agent/apps/desktop/src/app/chat/sidebar/profile-switcher.tsx +524 -0
- package/agent/apps/desktop/src/app/chat/sidebar/session-actions-menu.tsx +80 -45
- package/agent/apps/desktop/src/app/chat/sidebar/session-row.tsx +120 -25
- package/agent/apps/desktop/src/app/chat/sidebar/virtual-session-list.tsx +7 -13
- package/agent/apps/desktop/src/app/chat/sidebar/workspace-groups.test.ts +149 -0
- package/agent/apps/desktop/src/app/chat/sidebar/workspace-groups.ts +326 -0
- package/agent/apps/desktop/src/app/chat/thread-loading.ts +7 -2
- package/agent/apps/desktop/src/app/command-center/index.tsx +320 -581
- package/agent/apps/desktop/src/app/command-palette/index.tsx +681 -0
- package/agent/apps/desktop/src/app/command-palette/marketplace-theme-page.tsx +157 -0
- package/agent/apps/desktop/src/app/cron/index.tsx +392 -324
- package/agent/apps/desktop/src/app/cron/job-state.ts +29 -0
- package/agent/apps/desktop/src/app/desktop-controller.tsx +618 -123
- package/agent/apps/desktop/src/app/floating-hud.ts +22 -0
- package/agent/apps/desktop/src/app/gateway/hooks/use-gateway-boot.test.tsx +265 -0
- package/agent/apps/desktop/src/app/gateway/hooks/use-gateway-boot.ts +260 -14
- package/agent/apps/desktop/src/app/gateway/hooks/use-gateway-request.ts +48 -4
- package/agent/apps/desktop/src/app/hooks/use-keybinds.ts +270 -0
- package/agent/apps/desktop/src/app/hooks/use-refresh-hotkey.ts +45 -0
- package/agent/apps/desktop/src/app/layout-constants.ts +19 -0
- package/agent/apps/desktop/src/app/messaging/index.tsx +136 -241
- package/agent/apps/desktop/src/app/messaging/platform-icon.tsx +95 -0
- package/agent/apps/desktop/src/app/model-visibility-overlay.tsx +31 -0
- package/agent/apps/desktop/src/app/overlays/overlay-search-input.tsx +18 -62
- package/agent/apps/desktop/src/app/overlays/overlay-split-layout.tsx +59 -7
- package/agent/apps/desktop/src/app/overlays/overlay-view.tsx +9 -5
- package/agent/apps/desktop/src/app/page-search-shell.tsx +42 -20
- package/agent/apps/desktop/src/app/profiles/create-profile-dialog.tsx +165 -0
- package/agent/apps/desktop/src/app/profiles/delete-profile-dialog.tsx +65 -0
- package/agent/apps/desktop/src/app/profiles/index.tsx +174 -199
- package/agent/apps/desktop/src/app/profiles/rename-profile-dialog.tsx +125 -0
- package/agent/apps/desktop/src/app/right-sidebar/files/dnd-manager.ts +27 -0
- package/agent/apps/desktop/src/app/right-sidebar/files/ipc.test.ts +100 -0
- package/agent/apps/desktop/src/app/right-sidebar/files/ipc.ts +12 -18
- package/agent/apps/desktop/src/app/right-sidebar/files/remote-picker.tsx +177 -0
- package/agent/apps/desktop/src/app/right-sidebar/files/tree.tsx +35 -21
- package/agent/apps/desktop/src/app/right-sidebar/files/use-project-tree.test.ts +75 -3
- package/agent/apps/desktop/src/app/right-sidebar/files/use-project-tree.ts +152 -5
- package/agent/apps/desktop/src/app/right-sidebar/index.test.tsx +75 -0
- package/agent/apps/desktop/src/app/right-sidebar/index.tsx +166 -129
- package/agent/apps/desktop/src/app/right-sidebar/store.ts +19 -4
- package/agent/apps/desktop/src/app/right-sidebar/terminal/buffer.ts +65 -0
- package/agent/apps/desktop/src/app/right-sidebar/terminal/index.tsx +29 -34
- package/agent/apps/desktop/src/app/right-sidebar/terminal/persistent.tsx +18 -6
- package/agent/apps/desktop/src/app/right-sidebar/terminal/selection.ts +93 -32
- package/agent/apps/desktop/src/app/right-sidebar/terminal/use-terminal-session.ts +381 -119
- package/agent/apps/desktop/src/app/routes.ts +9 -0
- package/agent/apps/desktop/src/app/session/hooks/use-cwd-actions.ts +17 -7
- package/agent/apps/desktop/src/app/session/hooks/use-message-stream.ts +365 -47
- package/agent/apps/desktop/src/app/session/hooks/use-model-controls.test.tsx +198 -0
- package/agent/apps/desktop/src/app/session/hooks/use-model-controls.ts +70 -34
- package/agent/apps/desktop/src/app/session/hooks/use-prompt-actions.test.tsx +1061 -0
- package/agent/apps/desktop/src/app/session/hooks/use-prompt-actions.ts +1143 -165
- package/agent/apps/desktop/src/app/session/hooks/use-route-resume.test.tsx +341 -2
- package/agent/apps/desktop/src/app/session/hooks/use-route-resume.ts +176 -5
- package/agent/apps/desktop/src/app/session/hooks/use-session-actions.test.tsx +259 -0
- package/agent/apps/desktop/src/app/session/hooks/use-session-actions.ts +452 -149
- package/agent/apps/desktop/src/app/session/hooks/use-session-state-cache.test.tsx +327 -0
- package/agent/apps/desktop/src/app/session/hooks/use-session-state-cache.ts +133 -4
- package/agent/apps/desktop/src/app/session-picker-overlay.tsx +32 -0
- package/agent/apps/desktop/src/app/session-switcher.tsx +107 -0
- package/agent/apps/desktop/src/app/settings/about-settings.tsx +45 -36
- package/agent/apps/desktop/src/app/settings/appearance-settings.tsx +243 -162
- package/agent/apps/desktop/src/app/settings/config-settings.tsx +86 -66
- package/agent/apps/desktop/src/app/settings/constants.ts +459 -122
- package/agent/apps/desktop/src/app/settings/credential-key-ui.tsx +373 -0
- package/agent/apps/desktop/src/app/settings/env-credentials.tsx +198 -0
- package/agent/apps/desktop/src/app/settings/env-var-actions-menu.tsx +136 -0
- package/agent/apps/desktop/src/app/settings/field-copy.ts +56 -0
- package/agent/apps/desktop/src/app/settings/gateway-settings.tsx +385 -72
- package/agent/apps/desktop/src/app/settings/helpers.test.ts +156 -1
- package/agent/apps/desktop/src/app/settings/helpers.ts +30 -2
- package/agent/apps/desktop/src/app/settings/index.tsx +118 -84
- package/agent/apps/desktop/src/app/settings/keys-settings.tsx +62 -419
- package/agent/apps/desktop/src/app/settings/mcp-settings.tsx +65 -60
- package/agent/apps/desktop/src/app/settings/model-settings.test.tsx +129 -5
- package/agent/apps/desktop/src/app/settings/model-settings.tsx +370 -65
- package/agent/apps/desktop/src/app/settings/notifications-settings.tsx +150 -0
- package/agent/apps/desktop/src/app/settings/primitives.tsx +5 -11
- package/agent/apps/desktop/src/app/settings/provider-config-panel.test.tsx +142 -0
- package/agent/apps/desktop/src/app/settings/provider-config-panel.tsx +182 -0
- package/agent/apps/desktop/src/app/settings/providers-settings.test.tsx +171 -0
- package/agent/apps/desktop/src/app/settings/providers-settings.tsx +471 -0
- package/agent/apps/desktop/src/app/settings/sessions-settings.tsx +183 -71
- package/agent/apps/desktop/src/app/settings/toolset-config-panel.test.tsx +135 -1
- package/agent/apps/desktop/src/app/settings/toolset-config-panel.tsx +180 -57
- package/agent/apps/desktop/src/app/settings/types.ts +9 -6
- package/agent/apps/desktop/src/app/settings/uninstall-section.tsx +185 -0
- package/agent/apps/desktop/src/app/settings/use-deep-link-highlight.ts +60 -0
- package/agent/apps/desktop/src/app/shell/app-shell.tsx +59 -13
- package/agent/apps/desktop/src/app/shell/gateway-menu-panel.tsx +37 -32
- package/agent/apps/desktop/src/app/shell/hooks/use-overlay-routing.ts +6 -3
- package/agent/apps/desktop/src/app/shell/hooks/use-statusbar-items.tsx +212 -53
- package/agent/apps/desktop/src/app/shell/keybind-panel.tsx +215 -0
- package/agent/apps/desktop/src/app/shell/model-edit-submenu.test.tsx +84 -0
- package/agent/apps/desktop/src/app/shell/model-edit-submenu.tsx +244 -0
- package/agent/apps/desktop/src/app/shell/model-menu-panel.tsx +392 -0
- package/agent/apps/desktop/src/app/shell/statusbar-controls.tsx +23 -33
- package/agent/apps/desktop/src/app/shell/titlebar-controls.tsx +79 -95
- package/agent/apps/desktop/src/app/shell/titlebar.ts +8 -2
- package/agent/apps/desktop/src/app/skills/index.test.tsx +11 -0
- package/agent/apps/desktop/src/app/skills/index.tsx +79 -64
- package/agent/apps/desktop/src/app/types.ts +85 -0
- package/agent/apps/desktop/src/app/updates-overlay.tsx +110 -105
- package/agent/apps/desktop/src/components/assistant-ui/ansi-text.tsx +34 -0
- package/agent/apps/desktop/src/components/assistant-ui/block-direction.test.tsx +129 -0
- package/agent/apps/desktop/src/components/assistant-ui/clarify-tool.tsx +102 -81
- package/agent/apps/desktop/src/components/assistant-ui/directive-text.tsx +92 -15
- package/agent/apps/desktop/src/components/assistant-ui/markdown-text.test.ts +38 -0
- package/agent/apps/desktop/src/components/assistant-ui/markdown-text.tsx +304 -45
- package/agent/apps/desktop/src/components/assistant-ui/message-render-boundary.test.tsx +80 -0
- package/agent/apps/desktop/src/components/assistant-ui/message-render-boundary.tsx +48 -0
- package/agent/apps/desktop/src/components/assistant-ui/streaming.test.tsx +142 -90
- package/agent/apps/desktop/src/components/assistant-ui/thread-list.tsx +337 -0
- package/agent/apps/desktop/src/components/assistant-ui/thread.tsx +667 -190
- package/agent/apps/desktop/src/components/assistant-ui/tool-approval-group.test.tsx +299 -0
- package/agent/apps/desktop/src/components/assistant-ui/tool-approval.test.tsx +133 -0
- package/agent/apps/desktop/src/components/assistant-ui/tool-approval.tsx +239 -0
- package/agent/apps/desktop/src/components/assistant-ui/tool-fallback-model.test.ts +31 -0
- package/agent/apps/desktop/src/components/assistant-ui/tool-fallback-model.ts +152 -134
- package/agent/apps/desktop/src/components/assistant-ui/tool-fallback.tsx +142 -150
- package/agent/apps/desktop/src/components/assistant-ui/tooltip-icon-button.tsx +14 -12
- package/agent/apps/desktop/src/components/assistant-ui/user-message-edit.test.tsx +141 -0
- package/agent/apps/desktop/src/components/assistant-ui/user-message-text.tsx +152 -0
- package/agent/apps/desktop/src/components/boot-failure-overlay.tsx +150 -33
- package/agent/apps/desktop/src/components/boot-failure-reauth.test.ts +100 -0
- package/agent/apps/desktop/src/components/boot-failure-reauth.ts +81 -0
- package/agent/apps/desktop/src/components/brand-mark.tsx +19 -0
- package/agent/apps/desktop/src/components/chat/code-card.tsx +1 -1
- package/agent/apps/desktop/src/components/chat/composer-dock.ts +31 -0
- package/agent/apps/desktop/src/components/chat/diff-lines.tsx +1 -1
- package/agent/apps/desktop/src/components/chat/disclosure-row.tsx +13 -3
- package/agent/apps/desktop/src/components/chat/expandable-block.tsx +52 -0
- package/agent/apps/desktop/src/components/chat/generated-image-result.tsx +174 -0
- package/agent/apps/desktop/src/components/chat/image-generation-placeholder.tsx +70 -37
- package/agent/apps/desktop/src/components/chat/intro.tsx +8 -7
- package/agent/apps/desktop/src/components/chat/preview-attachment.tsx +4 -2
- package/agent/apps/desktop/src/components/chat/shiki-highlighter.test.ts +37 -0
- package/agent/apps/desktop/src/components/chat/shiki-highlighter.tsx +96 -22
- package/agent/apps/desktop/src/components/chat/status-row.tsx +70 -0
- package/agent/apps/desktop/src/components/chat/status-section.tsx +42 -0
- package/agent/apps/desktop/src/components/chat/terminal-output.tsx +54 -0
- package/agent/apps/desktop/src/components/chat/zoomable-image.tsx +70 -109
- package/agent/apps/desktop/src/components/desktop-install-overlay.tsx +154 -84
- package/agent/apps/desktop/src/components/desktop-onboarding-overlay.test.tsx +38 -8
- package/agent/apps/desktop/src/components/desktop-onboarding-overlay.tsx +789 -233
- package/agent/apps/desktop/src/components/error-boundary.tsx +77 -0
- package/agent/apps/desktop/src/components/gateway-connecting-overlay.test.tsx +144 -0
- package/agent/apps/desktop/src/components/gateway-connecting-overlay.tsx +7 -1
- package/agent/apps/desktop/src/components/haptics-provider.tsx +24 -0
- package/agent/apps/desktop/src/components/language-switcher.test.tsx +53 -0
- package/agent/apps/desktop/src/components/language-switcher.tsx +175 -0
- package/agent/apps/desktop/src/components/model-picker.tsx +42 -40
- package/agent/apps/desktop/src/components/model-visibility-dialog.tsx +166 -0
- package/agent/apps/desktop/src/components/notifications.tsx +48 -27
- package/agent/apps/desktop/src/components/pane-shell/index.ts +1 -1
- package/agent/apps/desktop/src/components/pane-shell/pane-shell.tsx +146 -9
- package/agent/apps/desktop/src/components/prompt-overlays.tsx +234 -0
- package/agent/apps/desktop/src/components/session-picker.tsx +108 -0
- package/agent/apps/desktop/src/components/ui/action-status.tsx +25 -0
- package/agent/apps/desktop/src/components/ui/badge.tsx +35 -0
- package/agent/apps/desktop/src/components/ui/button.tsx +37 -13
- package/agent/apps/desktop/src/components/ui/confirm-dialog.tsx +109 -0
- package/agent/apps/desktop/src/components/ui/control.ts +25 -0
- package/agent/apps/desktop/src/components/ui/copy-button.test.tsx +36 -0
- package/agent/apps/desktop/src/components/ui/copy-button.tsx +38 -27
- package/agent/apps/desktop/src/components/ui/dialog.tsx +39 -11
- package/agent/apps/desktop/src/components/ui/dropdown-menu.tsx +98 -24
- package/agent/apps/desktop/src/components/ui/error-state.tsx +50 -0
- package/agent/apps/desktop/src/components/ui/fade-text.tsx +9 -2
- package/agent/apps/desktop/src/components/ui/{braille-spinner.tsx → glyph-spinner.tsx} +15 -13
- package/agent/apps/desktop/src/components/ui/input.tsx +5 -2
- package/agent/apps/desktop/src/components/ui/kbd.tsx +83 -12
- package/agent/apps/desktop/src/components/ui/log-view.tsx +19 -0
- package/agent/apps/desktop/src/components/ui/pagination.tsx +12 -5
- package/agent/apps/desktop/src/components/ui/popover.tsx +44 -0
- package/agent/apps/desktop/src/components/ui/search-field.tsx +80 -0
- package/agent/apps/desktop/src/components/ui/segmented-control.tsx +51 -0
- package/agent/apps/desktop/src/components/ui/select.tsx +10 -3
- package/agent/apps/desktop/src/components/ui/sheet.tsx +8 -2
- package/agent/apps/desktop/src/components/ui/sidebar.tsx +18 -25
- package/agent/apps/desktop/src/components/ui/switch.tsx +38 -15
- package/agent/apps/desktop/src/components/ui/textarea.tsx +4 -11
- package/agent/apps/desktop/src/components/ui/tool-icon.tsx +65 -0
- package/agent/apps/desktop/src/components/ui/tooltip.tsx +31 -4
- package/agent/apps/desktop/src/fonts/JetBrainsMono-Bold.woff2 +0 -0
- package/agent/apps/desktop/src/fonts/JetBrainsMono-Italic.woff2 +0 -0
- package/agent/apps/desktop/src/fonts/JetBrainsMono-Regular.woff2 +0 -0
- package/agent/apps/desktop/src/global.d.ts +181 -4
- package/agent/apps/desktop/src/hermes.test.ts +60 -0
- package/agent/apps/desktop/src/hermes.ts +190 -13
- package/agent/apps/desktop/src/hooks/use-image-download.ts +85 -0
- package/agent/apps/desktop/src/hooks/use-resize-observer.ts +13 -4
- package/agent/apps/desktop/src/hooks/use-worktree-info.ts +68 -0
- package/agent/apps/desktop/src/i18n/catalog.ts +12 -0
- package/agent/apps/desktop/src/i18n/context.test.tsx +232 -0
- package/agent/apps/desktop/src/i18n/context.tsx +183 -0
- package/agent/apps/desktop/src/i18n/define-locale.ts +41 -0
- package/agent/apps/desktop/src/i18n/en.ts +1921 -0
- package/agent/apps/desktop/src/i18n/index.ts +20 -0
- package/agent/apps/desktop/src/i18n/ja.ts +2053 -0
- package/agent/apps/desktop/src/i18n/languages.test.ts +43 -0
- package/agent/apps/desktop/src/i18n/languages.ts +86 -0
- package/agent/apps/desktop/src/i18n/runtime.test.ts +75 -0
- package/agent/apps/desktop/src/i18n/runtime.ts +53 -0
- package/agent/apps/desktop/src/i18n/types.ts +1559 -0
- package/agent/apps/desktop/src/i18n/zh-hant.ts +1992 -0
- package/agent/apps/desktop/src/i18n/zh.ts +2099 -0
- package/agent/apps/desktop/src/lib/ansi.test.ts +123 -0
- package/agent/apps/desktop/src/lib/ansi.ts +186 -0
- package/agent/apps/desktop/src/lib/chat-messages.test.ts +79 -0
- package/agent/apps/desktop/src/lib/chat-messages.ts +68 -29
- package/agent/apps/desktop/src/lib/chat-runtime.test.ts +65 -1
- package/agent/apps/desktop/src/lib/chat-runtime.ts +39 -3
- package/agent/apps/desktop/src/lib/completion-sound.ts +519 -0
- package/agent/apps/desktop/src/lib/desktop-fs.test.ts +116 -0
- package/agent/apps/desktop/src/lib/desktop-fs.ts +113 -0
- package/agent/apps/desktop/src/lib/desktop-slash-commands.test.ts +89 -6
- package/agent/apps/desktop/src/lib/desktop-slash-commands.ts +270 -131
- package/agent/apps/desktop/src/lib/external-link.test.tsx +27 -0
- package/agent/apps/desktop/src/lib/external-link.tsx +9 -2
- package/agent/apps/desktop/src/lib/gateway-events.test.ts +27 -0
- package/agent/apps/desktop/src/lib/gateway-events.ts +16 -0
- package/agent/apps/desktop/src/lib/gateway-ws-url.test.ts +78 -0
- package/agent/apps/desktop/src/lib/gateway-ws-url.ts +91 -0
- package/agent/apps/desktop/src/lib/generated-images.test.ts +97 -0
- package/agent/apps/desktop/src/lib/generated-images.ts +116 -0
- package/agent/apps/desktop/src/lib/haptics.ts +17 -0
- package/agent/apps/desktop/src/lib/icons.ts +10 -2
- package/agent/apps/desktop/src/lib/keybinds/actions.ts +137 -0
- package/agent/apps/desktop/src/lib/keybinds/combo.test.ts +86 -0
- package/agent/apps/desktop/src/lib/keybinds/combo.ts +195 -0
- package/agent/apps/desktop/src/lib/local-preview.ts +23 -2
- package/agent/apps/desktop/src/lib/markdown-preprocess.ts +20 -7
- package/agent/apps/desktop/src/lib/media.remote.test.ts +90 -0
- package/agent/apps/desktop/src/lib/media.ts +40 -1
- package/agent/apps/desktop/src/lib/model-status-label.test.ts +59 -0
- package/agent/apps/desktop/src/lib/model-status-label.ts +122 -0
- package/agent/apps/desktop/src/lib/mutable-ref.ts +6 -0
- package/agent/apps/desktop/src/lib/profile-color.ts +58 -0
- package/agent/apps/desktop/src/lib/query-client.ts +13 -0
- package/agent/apps/desktop/src/lib/remend-tail.test.ts +105 -0
- package/agent/apps/desktop/src/lib/remend-tail.ts +108 -0
- package/agent/apps/desktop/src/lib/session-export.ts +6 -3
- package/agent/apps/desktop/src/lib/session-ids.test.ts +44 -0
- package/agent/apps/desktop/src/lib/session-ids.ts +26 -0
- package/agent/apps/desktop/src/lib/session-search.test.ts +66 -0
- package/agent/apps/desktop/src/lib/session-search.ts +21 -0
- package/agent/apps/desktop/src/lib/session-source.ts +126 -0
- package/agent/apps/desktop/src/lib/storage.test.ts +25 -0
- package/agent/apps/desktop/src/lib/storage.ts +35 -1
- package/agent/apps/desktop/src/lib/todos.test.ts +46 -1
- package/agent/apps/desktop/src/lib/todos.ts +37 -0
- package/agent/apps/desktop/src/lib/tool-result-summary.ts +5 -1
- package/agent/apps/desktop/src/lib/update-copy.test.ts +38 -0
- package/agent/apps/desktop/src/lib/update-copy.ts +44 -0
- package/agent/apps/desktop/src/lib/use-enter-animation.ts +2 -2
- package/agent/apps/desktop/src/lib/yolo-session.ts +50 -0
- package/agent/apps/desktop/src/main.tsx +19 -19
- package/agent/apps/desktop/src/store/boot.ts +4 -3
- package/agent/apps/desktop/src/store/clarify.test.ts +81 -0
- package/agent/apps/desktop/src/store/clarify.ts +50 -13
- package/agent/apps/desktop/src/store/command-palette.ts +20 -0
- package/agent/apps/desktop/src/store/compaction.test.ts +53 -0
- package/agent/apps/desktop/src/store/compaction.ts +38 -0
- package/agent/apps/desktop/src/store/completion-sound.ts +32 -0
- package/agent/apps/desktop/src/store/composer-input-history.test.ts +147 -0
- package/agent/apps/desktop/src/store/composer-input-history.ts +158 -0
- package/agent/apps/desktop/src/store/composer-queue.test.ts +68 -0
- package/agent/apps/desktop/src/store/composer-queue.ts +76 -0
- package/agent/apps/desktop/src/store/composer-status.test.ts +99 -0
- package/agent/apps/desktop/src/store/composer-status.ts +277 -0
- package/agent/apps/desktop/src/store/composer.test.ts +106 -0
- package/agent/apps/desktop/src/store/composer.ts +116 -0
- package/agent/apps/desktop/src/store/cron.ts +19 -0
- package/agent/apps/desktop/src/store/gateway.ts +280 -6
- package/agent/apps/desktop/src/store/keybinds.ts +143 -0
- package/agent/apps/desktop/src/store/layout.ts +107 -9
- package/agent/apps/desktop/src/store/model-presets.test.ts +51 -0
- package/agent/apps/desktop/src/store/model-presets.ts +86 -0
- package/agent/apps/desktop/src/store/model-visibility.test.ts +99 -0
- package/agent/apps/desktop/src/store/model-visibility.ts +161 -0
- package/agent/apps/desktop/src/store/native-notifications.test.ts +192 -0
- package/agent/apps/desktop/src/store/native-notifications.ts +203 -0
- package/agent/apps/desktop/src/store/notifications.ts +10 -7
- package/agent/apps/desktop/src/store/onboarding.test.ts +271 -1
- package/agent/apps/desktop/src/store/onboarding.ts +268 -38
- package/agent/apps/desktop/src/store/preview.ts +10 -1
- package/agent/apps/desktop/src/store/profile.test.ts +89 -0
- package/agent/apps/desktop/src/store/profile.ts +395 -0
- package/agent/apps/desktop/src/store/prompts.test.ts +127 -0
- package/agent/apps/desktop/src/store/prompts.ts +117 -0
- package/agent/apps/desktop/src/store/session-switcher.test.ts +115 -0
- package/agent/apps/desktop/src/store/session-switcher.ts +128 -0
- package/agent/apps/desktop/src/store/session-sync.ts +25 -0
- package/agent/apps/desktop/src/store/session.test.ts +268 -2
- package/agent/apps/desktop/src/store/session.ts +392 -18
- package/agent/apps/desktop/src/store/subagents.ts +3 -0
- package/agent/apps/desktop/src/store/system-actions.ts +48 -0
- package/agent/apps/desktop/src/store/thread-scroll.ts +58 -5
- package/agent/apps/desktop/src/store/todos.test.ts +47 -0
- package/agent/apps/desktop/src/store/todos.ts +64 -0
- package/agent/apps/desktop/src/store/tool-dismiss.ts +45 -0
- package/agent/apps/desktop/src/store/translucency.ts +38 -0
- package/agent/apps/desktop/src/store/updates.test.ts +187 -2
- package/agent/apps/desktop/src/store/updates.ts +268 -18
- package/agent/apps/desktop/src/store/windows.test.ts +143 -0
- package/agent/apps/desktop/src/store/windows.ts +115 -0
- package/agent/apps/desktop/src/styles.css +510 -119
- package/agent/apps/desktop/src/themes/color.ts +142 -0
- package/agent/apps/desktop/src/themes/context.tsx +128 -75
- package/agent/apps/desktop/src/themes/install.test.ts +119 -0
- package/agent/apps/desktop/src/themes/install.ts +95 -0
- package/agent/apps/desktop/src/themes/presets.test.ts +33 -0
- package/agent/apps/desktop/src/themes/presets.ts +13 -4
- package/agent/apps/desktop/src/themes/profile-theme.test.ts +41 -0
- package/agent/apps/desktop/src/themes/types.ts +35 -0
- package/agent/apps/desktop/src/themes/user-themes.test.ts +63 -0
- package/agent/apps/desktop/src/themes/user-themes.ts +122 -0
- package/agent/apps/desktop/src/themes/vscode.test.ts +171 -0
- package/agent/apps/desktop/src/themes/vscode.ts +343 -0
- package/agent/apps/desktop/src/types/hermes.ts +138 -1
- package/agent/apps/desktop/tsconfig.json +2 -2
- package/agent/apps/desktop/vite.config.ts +18 -0
- package/agent/apps/shared/package.json +1 -1
- package/agent/apps/shared/src/json-rpc-gateway.ts +63 -2
- package/agent/apps/shared/tsconfig.json +2 -2
- package/agent/cli-config.yaml.example +78 -1
- package/agent/cli.py +2177 -3162
- package/agent/cron/blueprint_catalog.py +713 -0
- package/agent/cron/jobs.py +226 -110
- package/agent/cron/scheduler.py +468 -193
- package/agent/cron/scheduler_provider.py +177 -0
- package/agent/cron/scripts/__init__.py +1 -0
- package/agent/cron/scripts/classify_items.py +226 -0
- package/agent/cron/suggestion_catalog.py +154 -0
- package/agent/cron/suggestions.py +257 -0
- package/agent/docs/chronos-managed-cron-contract.md +196 -0
- package/agent/docs/design/profile-builder.md +146 -0
- package/agent/docs/middleware/README.md +260 -0
- package/agent/docs/observability/README.md +316 -0
- package/agent/docs/plans/2026-06-09-003-fix-telegram-stream-overflow-continuations-plan.md +240 -0
- package/agent/docs/rca-ssl-cacert-post-git-pull.md +54 -0
- package/agent/docs/relay-connector-contract.md +285 -0
- package/agent/gateway/authz_mixin.py +536 -0
- package/agent/gateway/channel_directory.py +65 -3
- package/agent/gateway/config.py +222 -12
- package/agent/gateway/display_config.py +10 -0
- package/agent/gateway/hooks.py +17 -0
- package/agent/gateway/kanban_watchers.py +1146 -0
- package/agent/gateway/message_timestamps.py +166 -0
- package/agent/gateway/platforms/ADDING_A_PLATFORM.md +29 -0
- package/agent/gateway/platforms/api_server.py +216 -38
- package/agent/gateway/platforms/base.py +210 -58
- package/agent/gateway/platforms/email.py +122 -12
- package/agent/gateway/platforms/feishu.py +80 -11
- package/agent/gateway/platforms/feishu_meeting_invite.py +212 -0
- package/agent/gateway/platforms/matrix.py +1498 -297
- package/agent/gateway/platforms/qqbot/adapter.py +6 -0
- package/agent/gateway/platforms/signal.py +8 -0
- package/agent/gateway/platforms/slack.py +308 -12
- package/agent/gateway/platforms/telegram.py +831 -24
- package/agent/gateway/platforms/webhook.py +109 -21
- package/agent/gateway/platforms/weixin.py +113 -2
- package/agent/gateway/platforms/whatsapp.py +94 -288
- package/agent/gateway/platforms/whatsapp_cloud.py +1956 -0
- package/agent/gateway/platforms/whatsapp_common.py +367 -0
- package/agent/gateway/platforms/yuanbao.py +608 -191
- package/agent/gateway/platforms/yuanbao_proto.py +232 -23
- package/agent/gateway/relay/__init__.py +375 -0
- package/agent/gateway/relay/adapter.py +222 -0
- package/agent/gateway/relay/auth.py +168 -0
- package/agent/gateway/relay/descriptor.py +118 -0
- package/agent/gateway/relay/transport.py +101 -0
- package/agent/gateway/relay/ws_transport.py +327 -0
- package/agent/gateway/response_filters.py +53 -0
- package/agent/gateway/rich_sent_store.py +80 -0
- package/agent/gateway/run.py +2940 -5001
- package/agent/gateway/session.py +109 -8
- package/agent/gateway/session_context.py +22 -4
- package/agent/gateway/slash_commands.py +3854 -0
- package/agent/gateway/status.py +141 -21
- package/agent/gateway/stream_consumer.py +288 -31
- package/agent/hermes-already-has-routines.md +1 -1
- package/agent/hermes_cli/__init__.py +62 -17
- package/agent/hermes_cli/_parser.py +30 -0
- package/agent/hermes_cli/_subprocess_compat.py +61 -0
- package/agent/hermes_cli/active_sessions.py +320 -0
- package/agent/hermes_cli/auth.py +707 -59
- package/agent/hermes_cli/auth_commands.py +39 -22
- package/agent/hermes_cli/backup.py +109 -7
- package/agent/hermes_cli/banner.py +88 -0
- package/agent/hermes_cli/blueprint_cmd.py +318 -0
- package/agent/hermes_cli/cli_agent_setup_mixin.py +684 -0
- package/agent/hermes_cli/cli_commands_mixin.py +2293 -0
- package/agent/hermes_cli/commands.py +215 -91
- package/agent/hermes_cli/config.py +967 -130
- package/agent/hermes_cli/container_boot.py +76 -11
- package/agent/hermes_cli/cron.py +5 -11
- package/agent/hermes_cli/curator.py +21 -0
- package/agent/hermes_cli/dashboard_auth/__init__.py +2 -0
- package/agent/hermes_cli/dashboard_auth/base.py +62 -0
- package/agent/hermes_cli/dashboard_auth/cookies.py +32 -19
- package/agent/hermes_cli/dashboard_auth/login_page.py +156 -6
- package/agent/hermes_cli/dashboard_auth/middleware.py +28 -4
- package/agent/hermes_cli/dashboard_auth/prefix.py +46 -2
- package/agent/hermes_cli/dashboard_auth/public_paths.py +6 -0
- package/agent/hermes_cli/dashboard_auth/routes.py +158 -2
- package/agent/hermes_cli/dashboard_auth/ws_tickets.py +85 -11
- package/agent/hermes_cli/dashboard_register.py +427 -0
- package/agent/hermes_cli/debug.py +155 -50
- package/agent/hermes_cli/doctor.py +255 -14
- package/agent/hermes_cli/dump.py +60 -6
- package/agent/hermes_cli/env_loader.py +33 -0
- package/agent/hermes_cli/gateway.py +755 -103
- package/agent/hermes_cli/gateway_enroll.py +250 -0
- package/agent/hermes_cli/gateway_windows.py +254 -11
- package/agent/hermes_cli/gui_uninstall.py +285 -0
- package/agent/hermes_cli/inventory.py +105 -4
- package/agent/hermes_cli/kanban.py +58 -71
- package/agent/hermes_cli/kanban_db.py +391 -14
- package/agent/hermes_cli/kanban_decompose.py +2 -2
- package/agent/hermes_cli/kanban_specify.py +3 -1
- package/agent/hermes_cli/logs.py +2 -0
- package/agent/hermes_cli/main.py +2889 -5287
- package/agent/hermes_cli/managed_scope.py +214 -0
- package/agent/hermes_cli/managed_uv.py +254 -0
- package/agent/hermes_cli/mcp_catalog.py +6 -3
- package/agent/hermes_cli/mcp_config.py +145 -21
- package/agent/hermes_cli/mcp_security.py +96 -0
- package/agent/hermes_cli/mcp_startup.py +32 -3
- package/agent/hermes_cli/memory_providers.py +149 -0
- package/agent/hermes_cli/memory_setup.py +97 -42
- package/agent/hermes_cli/middleware.py +313 -0
- package/agent/hermes_cli/model_catalog.py +31 -0
- package/agent/hermes_cli/model_cost_guard.py +134 -0
- package/agent/hermes_cli/model_normalize.py +2 -1
- package/agent/hermes_cli/model_setup_flows.py +2759 -0
- package/agent/hermes_cli/model_switch.py +242 -27
- package/agent/hermes_cli/models.py +284 -44
- package/agent/hermes_cli/nous_account.py +33 -6
- package/agent/hermes_cli/nous_billing.py +406 -0
- package/agent/hermes_cli/nous_subscription.py +202 -5
- package/agent/hermes_cli/platforms.py +1 -0
- package/agent/hermes_cli/plugins.py +218 -18
- package/agent/hermes_cli/plugins_cmd.py +249 -105
- package/agent/hermes_cli/portal_cli.py +56 -16
- package/agent/hermes_cli/profile_distribution.py +6 -1
- package/agent/hermes_cli/profiles.py +283 -32
- package/agent/hermes_cli/provider_catalog.py +170 -0
- package/agent/hermes_cli/providers.py +4 -1
- package/agent/hermes_cli/pty_bridge.py +53 -4
- package/agent/hermes_cli/runtime_provider.py +216 -34
- package/agent/hermes_cli/secret_prompt.py +4 -4
- package/agent/hermes_cli/secrets_cli.py +24 -0
- package/agent/hermes_cli/send_cmd.py +28 -2
- package/agent/hermes_cli/service_manager.py +166 -19
- package/agent/hermes_cli/session_listing.py +97 -0
- package/agent/hermes_cli/setup.py +158 -94
- package/agent/hermes_cli/setup_whatsapp_cloud.py +541 -0
- package/agent/hermes_cli/skills_config.py +8 -2
- package/agent/hermes_cli/skills_hub.py +149 -7
- package/agent/hermes_cli/status.py +2 -2
- package/agent/hermes_cli/subcommands/__init__.py +18 -0
- package/agent/hermes_cli/subcommands/_shared.py +29 -0
- package/agent/hermes_cli/subcommands/acp.py +52 -0
- package/agent/hermes_cli/subcommands/auth.py +109 -0
- package/agent/hermes_cli/subcommands/backup.py +38 -0
- package/agent/hermes_cli/subcommands/claw.py +92 -0
- package/agent/hermes_cli/subcommands/config.py +49 -0
- package/agent/hermes_cli/subcommands/cron.py +163 -0
- package/agent/hermes_cli/subcommands/dashboard.py +143 -0
- package/agent/hermes_cli/subcommands/debug.py +77 -0
- package/agent/hermes_cli/subcommands/doctor.py +35 -0
- package/agent/hermes_cli/subcommands/dump.py +28 -0
- package/agent/hermes_cli/subcommands/gateway.py +332 -0
- package/agent/hermes_cli/subcommands/gui.py +63 -0
- package/agent/hermes_cli/subcommands/hooks.py +77 -0
- package/agent/hermes_cli/subcommands/import_cmd.py +31 -0
- package/agent/hermes_cli/subcommands/insights.py +25 -0
- package/agent/hermes_cli/subcommands/login.py +78 -0
- package/agent/hermes_cli/subcommands/logout.py +28 -0
- package/agent/hermes_cli/subcommands/logs.py +78 -0
- package/agent/hermes_cli/subcommands/mcp.py +108 -0
- package/agent/hermes_cli/subcommands/memory.py +53 -0
- package/agent/hermes_cli/subcommands/model.py +72 -0
- package/agent/hermes_cli/subcommands/pairing.py +36 -0
- package/agent/hermes_cli/subcommands/plugins.py +94 -0
- package/agent/hermes_cli/subcommands/postinstall.py +23 -0
- package/agent/hermes_cli/subcommands/profile.py +203 -0
- package/agent/hermes_cli/subcommands/prompt_size.py +36 -0
- package/agent/hermes_cli/subcommands/security.py +62 -0
- package/agent/hermes_cli/subcommands/setup.py +58 -0
- package/agent/hermes_cli/subcommands/skills.py +298 -0
- package/agent/hermes_cli/subcommands/slack.py +60 -0
- package/agent/hermes_cli/subcommands/status.py +28 -0
- package/agent/hermes_cli/subcommands/tools.py +95 -0
- package/agent/hermes_cli/subcommands/uninstall.py +41 -0
- package/agent/hermes_cli/subcommands/update.py +70 -0
- package/agent/hermes_cli/subcommands/version.py +18 -0
- package/agent/hermes_cli/subcommands/webhook.py +76 -0
- package/agent/hermes_cli/subcommands/whatsapp.py +22 -0
- package/agent/hermes_cli/suggestions_cmd.py +153 -0
- package/agent/hermes_cli/telegram_managed_bot.py +358 -0
- package/agent/hermes_cli/tips.py +3 -4
- package/agent/hermes_cli/tools_config.py +155 -28
- package/agent/hermes_cli/uninstall.py +231 -35
- package/agent/hermes_cli/web_server.py +6190 -973
- package/agent/hermes_cli/win_pty_bridge.py +179 -0
- package/agent/hermes_cli/write_approval_commands.py +209 -0
- package/agent/hermes_constants.py +164 -33
- package/agent/hermes_logging.py +74 -2
- package/agent/hermes_state.py +919 -106
- package/agent/hermes_time.py +20 -0
- package/agent/locales/af.yaml +23 -0
- package/agent/locales/de.yaml +23 -0
- package/agent/locales/en.yaml +20 -0
- package/agent/locales/es.yaml +23 -0
- package/agent/locales/fr.yaml +23 -0
- package/agent/locales/ga.yaml +23 -0
- package/agent/locales/hu.yaml +23 -0
- package/agent/locales/it.yaml +23 -0
- package/agent/locales/ja.yaml +23 -0
- package/agent/locales/ko.yaml +23 -0
- package/agent/locales/pt.yaml +23 -0
- package/agent/locales/ru.yaml +23 -0
- package/agent/locales/tr.yaml +23 -0
- package/agent/locales/uk.yaml +23 -0
- package/agent/locales/zh-hant.yaml +23 -0
- package/agent/locales/zh.yaml +23 -0
- package/agent/model_tools.py +204 -40
- package/agent/optional-mcps/clawpump/manifest.yaml +4 -2
- package/agent/optional-mcps/clawpump-stdio/manifest.yaml +2 -0
- package/agent/optional-mcps/unreal-engine/manifest.yaml +54 -0
- package/agent/optional-skills/blockchain/hyperliquid/SKILL.md +2 -2
- package/agent/optional-skills/blockchain/hyperliquid/scripts/hyperliquid_client.py +1 -1
- package/agent/optional-skills/creative/kanban-video-orchestrator/SKILL.md +1 -1
- package/agent/optional-skills/creative/kanban-video-orchestrator/assets/setup.sh.tmpl +4 -3
- package/agent/optional-skills/creative/kanban-video-orchestrator/references/kanban-setup.md +6 -4
- package/agent/optional-skills/creative/kanban-video-orchestrator/references/tool-matrix.md +2 -2
- package/agent/{skills/software-development → optional-skills/devops}/hermes-s6-container-supervision/SKILL.md +2 -0
- package/agent/optional-skills/devops/watchers/SKILL.md +1 -1
- package/agent/optional-skills/devops/watchers/scripts/watch_github.py +2 -1
- package/agent/optional-skills/payments/mpp-agent/SKILL.md +124 -0
- package/agent/optional-skills/payments/stripe-link-cli/SKILL.md +184 -0
- package/agent/optional-skills/payments/stripe-projects/SKILL.md +120 -0
- package/agent/optional-skills/productivity/canvas/SKILL.md +1 -1
- package/agent/optional-skills/productivity/canvas/scripts/canvas_api.py +4 -1
- package/agent/optional-skills/productivity/shop/SKILL.md +224 -0
- package/agent/optional-skills/productivity/shop/references/catalog-mcp.md +236 -0
- package/agent/optional-skills/productivity/shop/references/direct-api.md +278 -0
- package/agent/optional-skills/productivity/shop/references/legal.md +3 -0
- package/agent/optional-skills/productivity/shop/references/safety.md +36 -0
- package/agent/optional-skills/productivity/shopify/SKILL.md +1 -1
- package/agent/optional-skills/productivity/siyuan/SKILL.md +1 -1
- package/agent/optional-skills/productivity/telephony/SKILL.md +4 -4
- package/agent/optional-skills/productivity/telephony/scripts/telephony.py +15 -15
- package/agent/optional-skills/security/1password/SKILL.md +1 -1
- package/agent/{skills/red-teaming → optional-skills/security}/godmode/SKILL.md +3 -4
- package/agent/{skills/red-teaming → optional-skills/security}/godmode/scripts/auto_jailbreak.py +3 -1
- package/agent/optional-skills/software-development/rest-graphql-debug/SKILL.md +1 -1
- package/agent/{skills → optional-skills}/software-development/subagent-driven-development/SKILL.md +5 -5
- package/agent/package-lock.json +4082 -7907
- package/agent/package.json +18 -3
- package/agent/plugins/browser/firecrawl/provider.py +4 -1
- package/agent/plugins/cron/__init__.py +344 -0
- package/agent/plugins/cron/chronos/__init__.py +241 -0
- package/agent/plugins/cron/chronos/_nas_client.py +123 -0
- package/agent/plugins/cron/chronos/plugin.yaml +9 -0
- package/agent/plugins/cron/chronos/verify.py +103 -0
- package/agent/plugins/dashboard_auth/basic/__init__.py +491 -0
- package/agent/plugins/dashboard_auth/basic/plugin.yaml +7 -0
- package/agent/plugins/dashboard_auth/nous/__init__.py +12 -14
- package/agent/plugins/dashboard_auth/self_hosted/__init__.py +736 -0
- package/agent/plugins/dashboard_auth/self_hosted/plugin.yaml +8 -0
- package/agent/plugins/disk-cleanup/disk_cleanup.py +100 -20
- package/agent/plugins/google_meet/audio_bridge.py +4 -0
- package/agent/plugins/google_meet/meet_bot.py +7 -1
- package/agent/plugins/hermes-achievements/dashboard/dist/index.js +9 -15
- package/agent/plugins/image_gen/fal/__init__.py +35 -6
- package/agent/plugins/image_gen/krea/__init__.py +56 -13
- package/agent/plugins/image_gen/openai/__init__.py +122 -24
- package/agent/plugins/image_gen/openai-codex/__init__.py +28 -2
- package/agent/plugins/image_gen/xai/__init__.py +92 -12
- package/agent/plugins/kanban/dashboard/dist/index.js +63 -48
- package/agent/plugins/kanban/dashboard/plugin_api.py +39 -35
- package/agent/plugins/memory/__init__.py +48 -5
- package/agent/plugins/memory/byterover/__init__.py +1 -0
- package/agent/plugins/memory/hindsight/README.md +1 -1
- package/agent/plugins/memory/hindsight/__init__.py +138 -24
- package/agent/plugins/memory/hindsight/plugin.yaml +1 -1
- package/agent/plugins/memory/honcho/README.md +13 -10
- package/agent/plugins/memory/honcho/cli.py +247 -122
- package/agent/plugins/memory/honcho/client.py +112 -102
- package/agent/plugins/memory/openviking/README.md +12 -1
- package/agent/plugins/memory/openviking/__init__.py +2281 -107
- package/agent/plugins/memory/openviking/plugin.yaml +1 -2
- package/agent/plugins/memory/supermemory/README.md +22 -10
- package/agent/plugins/memory/supermemory/__init__.py +142 -37
- package/agent/plugins/memory/supermemory/plugin.yaml +1 -1
- package/agent/plugins/model-providers/anthropic/__init__.py +1 -0
- package/agent/plugins/model-providers/bedrock/__init__.py +1 -0
- package/agent/plugins/model-providers/copilot-acp/__init__.py +1 -0
- package/agent/plugins/model-providers/custom/__init__.py +8 -2
- package/agent/plugins/model-providers/kimi-coding/__init__.py +16 -7
- package/agent/plugins/model-providers/minimax/__init__.py +60 -8
- package/agent/plugins/model-providers/opencode-zen/__init__.py +12 -3
- package/agent/plugins/model-providers/openrouter/__init__.py +75 -4
- package/agent/plugins/model-providers/xiaomi/__init__.py +2 -0
- package/agent/plugins/model-providers/zai/__init__.py +1 -0
- package/agent/plugins/observability/langfuse/__init__.py +147 -14
- package/agent/plugins/observability/nemo_relay/README.md +559 -0
- package/agent/plugins/observability/nemo_relay/__init__.py +962 -0
- package/agent/plugins/observability/nemo_relay/plugin.yaml +20 -0
- package/agent/plugins/platforms/discord/adapter.py +932 -61
- package/agent/plugins/platforms/discord/voice_mixer.py +379 -0
- package/agent/plugins/platforms/google_chat/adapter.py +9 -3
- package/agent/plugins/platforms/google_chat/oauth.py +1 -1
- package/agent/plugins/platforms/homeassistant/__init__.py +3 -0
- package/agent/{gateway/platforms/homeassistant.py → plugins/platforms/homeassistant/adapter.py} +128 -0
- package/agent/plugins/platforms/homeassistant/plugin.yaml +22 -0
- package/agent/plugins/platforms/irc/adapter.py +4 -1
- package/agent/plugins/platforms/line/adapter.py +16 -1
- package/agent/plugins/platforms/mattermost/adapter.py +100 -24
- package/agent/plugins/platforms/photon/README.md +179 -0
- package/agent/plugins/platforms/photon/__init__.py +4 -0
- package/agent/plugins/platforms/photon/adapter.py +1586 -0
- package/agent/plugins/platforms/photon/auth.py +1046 -0
- package/agent/plugins/platforms/photon/cli.py +439 -0
- package/agent/plugins/platforms/photon/plugin.yaml +88 -0
- package/agent/plugins/platforms/photon/sidecar/README.md +52 -0
- package/agent/plugins/platforms/photon/sidecar/index.mjs +720 -0
- package/agent/plugins/platforms/photon/sidecar/package-lock.json +1730 -0
- package/agent/plugins/platforms/photon/sidecar/package.json +25 -0
- package/agent/plugins/platforms/photon/sidecar/patch-spectrum-mixed-attachments.mjs +155 -0
- package/agent/plugins/platforms/raft/__init__.py +3 -0
- package/agent/plugins/platforms/raft/adapter.py +774 -0
- package/agent/plugins/platforms/raft/plugin.yaml +19 -0
- package/agent/plugins/platforms/simplex/adapter.py +777 -220
- package/agent/plugins/platforms/simplex/plugin.yaml +21 -2
- package/agent/plugins/platforms/teams/adapter.py +175 -5
- package/agent/plugins/plugin_utils.py +135 -0
- package/agent/plugins/video_gen/fal/__init__.py +10 -3
- package/agent/plugins/web/searxng/provider.py +15 -2
- package/agent/plugins/web/xai/provider.py +2 -2
- package/agent/providers/base.py +22 -3
- package/agent/pyproject.toml +115 -21
- package/agent/run_agent.py +733 -39
- package/agent/scripts/build_skills_index.py +51 -19
- package/agent/scripts/check_subprocess_stdin.py +177 -0
- package/agent/scripts/contributor_audit.py +2 -0
- package/agent/scripts/docker_config_migrate.py +67 -0
- package/agent/scripts/install.cmd +3 -3
- package/agent/scripts/install.ps1 +580 -154
- package/agent/scripts/install.sh +402 -185
- package/agent/scripts/lib/node-bootstrap.sh +39 -4
- package/agent/scripts/release.py +183 -0
- package/agent/scripts/run_tests.sh +1 -0
- package/agent/scripts/run_tests_parallel.py +18 -23
- package/agent/scripts/whatsapp-bridge/bridge.js +25 -4
- package/agent/setup.py +59 -0
- package/agent/skills/autonomous-ai-agents/codex/SKILL.md +19 -0
- package/agent/skills/autonomous-ai-agents/hermes-agent/SKILL.md +10 -3
- package/agent/skills/{mcp/native-mcp/SKILL.md → autonomous-ai-agents/hermes-agent/references/native-mcp.md} +0 -13
- package/agent/skills/{devops/webhook-subscriptions/SKILL.md → autonomous-ai-agents/hermes-agent/references/webhooks.md} +1 -11
- package/agent/skills/clawpump/SKILL.md +4 -1
- package/agent/skills/devops/kanban-orchestrator/SKILL.md +1 -0
- package/agent/skills/devops/kanban-worker/SKILL.md +1 -0
- package/agent/skills/github/github-auth/SKILL.md +2 -2
- package/agent/skills/github/github-auth/scripts/gh-env.sh +2 -2
- package/agent/skills/github/github-code-review/SKILL.md +2 -2
- package/agent/skills/github/github-issues/SKILL.md +2 -2
- package/agent/skills/github/github-pr-workflow/SKILL.md +2 -2
- package/agent/skills/github/github-repo-management/SKILL.md +2 -2
- package/agent/skills/media/gif-search/SKILL.md +1 -1
- package/agent/skills/media/youtube-content/SKILL.md +10 -7
- package/agent/skills/media/youtube-content/scripts/fetch_transcript.py +3 -3
- package/agent/skills/note-taking/obsidian/SKILL.md +1 -1
- package/agent/skills/productivity/airtable/SKILL.md +2 -2
- package/agent/skills/productivity/google-workspace/scripts/setup.py +33 -7
- package/agent/skills/productivity/notion/SKILL.md +2 -2
- package/agent/skills/productivity/teams-meeting-pipeline/SKILL.md +1 -1
- package/agent/skills/research/llm-wiki/SKILL.md +1 -1
- package/agent/skills/social-media/xurl/SKILL.md +9 -0
- package/agent/skills/software-development/hermes-agent-skill-authoring/SKILL.md +1 -1
- package/agent/skills/software-development/plan/SKILL.md +285 -5
- package/agent/skills/software-development/requesting-code-review/SKILL.md +2 -2
- package/agent/skills/software-development/simplify-code/SKILL.md +212 -0
- package/agent/skills/software-development/spike/SKILL.md +2 -2
- package/agent/skills/software-development/systematic-debugging/SKILL.md +1 -1
- package/agent/skills/software-development/test-driven-development/SKILL.md +1 -1
- package/agent/tools/approval.py +302 -4
- package/agent/tools/async_delegation.py +386 -0
- package/agent/tools/blueprints.py +325 -0
- package/agent/tools/browser_cdp_tool.py +3 -3
- package/agent/tools/browser_tool.py +34 -6
- package/agent/tools/checkpoint_manager.py +31 -1
- package/agent/tools/clarify_tool.py +55 -5
- package/agent/tools/code_execution_tool.py +31 -14
- package/agent/tools/computer_use/cua_backend.py +81 -3
- package/agent/tools/computer_use/tool.py +79 -5
- package/agent/tools/computer_use/vision_routing.py +55 -3
- package/agent/tools/credential_files.py +31 -12
- package/agent/tools/cronjob_tools.py +30 -20
- package/agent/tools/delegate_tool.py +356 -31
- package/agent/tools/env_probe.py +1 -0
- package/agent/tools/environments/docker.py +163 -8
- package/agent/tools/environments/file_sync.py +2 -1
- package/agent/tools/environments/local.py +74 -23
- package/agent/tools/environments/singularity.py +4 -1
- package/agent/tools/environments/ssh.py +78 -11
- package/agent/tools/file_operations.py +277 -41
- package/agent/tools/file_tools.py +166 -28
- package/agent/tools/image_generation_tool.py +515 -29
- package/agent/tools/kanban_tools.py +99 -0
- package/agent/tools/lazy_deps.py +33 -2
- package/agent/tools/mcp_oauth.py +5 -5
- package/agent/tools/mcp_oauth_manager.py +7 -5
- package/agent/tools/mcp_tool.py +840 -33
- package/agent/tools/memory_tool.py +335 -38
- package/agent/tools/osv_check.py +15 -1
- package/agent/tools/process_registry.py +155 -11
- package/agent/tools/read_extract.py +248 -0
- package/agent/tools/read_terminal_tool.py +93 -0
- package/agent/tools/schema_sanitizer.py +38 -0
- package/agent/tools/send_message_tool.py +163 -49
- package/agent/tools/session_search_tool.py +189 -7
- package/agent/tools/skill_manager_tool.py +202 -3
- package/agent/tools/skill_usage.py +52 -4
- package/agent/tools/skills_hub.py +184 -44
- package/agent/tools/skills_sync.py +232 -5
- package/agent/tools/skills_tool.py +125 -11
- package/agent/tools/terminal_tool.py +148 -26
- package/agent/tools/tirith_security.py +2 -0
- package/agent/tools/todo_tool.py +32 -1
- package/agent/tools/transcription_tools.py +13 -5
- package/agent/tools/tts_tool.py +332 -38
- package/agent/tools/url_safety.py +52 -1
- package/agent/tools/vision_tools.py +124 -39
- package/agent/tools/voice_mode.py +4 -3
- package/agent/tools/web_tools.py +45 -15
- package/agent/tools/write_approval.py +493 -0
- package/agent/toolsets.py +34 -10
- package/agent/trajectory_compressor.py +81 -10
- package/agent/tui_gateway/entry.py +43 -6
- package/agent/tui_gateway/server.py +3335 -330
- package/agent/tui_gateway/slash_worker.py +61 -0
- package/agent/tui_gateway/ws.py +67 -9
- package/agent/ui-tui/eslint.config.mjs +0 -4
- package/agent/ui-tui/package.json +6 -6
- package/agent/ui-tui/packages/hermes-ink/package.json +1 -1
- package/agent/ui-tui/packages/hermes-ink/src/ink/app-mouse.test.ts +34 -1
- package/agent/ui-tui/packages/hermes-ink/src/ink/app-rawmode-mouse.test.ts +91 -0
- package/agent/ui-tui/packages/hermes-ink/src/ink/components/App.tsx +35 -2
- package/agent/ui-tui/packages/hermes-ink/src/ink/events/input-event.ts +4 -11
- package/agent/ui-tui/packages/hermes-ink/src/ink/parse-keypress.test.ts +23 -57
- package/agent/ui-tui/packages/hermes-ink/src/ink/parse-keypress.ts +11 -135
- package/agent/ui-tui/packages/hermes-ink/src/ink/termio/tokenize.test.ts +185 -0
- package/agent/ui-tui/packages/hermes-ink/src/ink/termio/tokenize.ts +37 -3
- package/agent/ui-tui/packages/hermes-ink/src/utils/execFileNoThrow.ts +5 -5
- package/agent/ui-tui/src/__tests__/appChromeStatusRule.test.tsx +217 -0
- package/agent/ui-tui/src/__tests__/appChromeStatusRuleDevCredits.test.tsx +73 -0
- package/agent/ui-tui/src/__tests__/approvalAction.test.ts +11 -0
- package/agent/ui-tui/src/__tests__/billingCommand.test.ts +301 -0
- package/agent/ui-tui/src/__tests__/blockLayout.test.ts +122 -0
- package/agent/ui-tui/src/__tests__/brandingMcpCount.test.ts +111 -0
- package/agent/ui-tui/src/__tests__/completionApply.test.ts +51 -0
- package/agent/ui-tui/src/__tests__/createGatewayEventHandler.test.ts +487 -2
- package/agent/ui-tui/src/__tests__/createSlashHandler.test.ts +54 -0
- package/agent/ui-tui/src/__tests__/creditsCommand.test.ts +144 -0
- package/agent/ui-tui/src/__tests__/gatewayClient.test.ts +120 -99
- package/agent/ui-tui/src/__tests__/gracefulExit.test.ts +11 -0
- package/agent/ui-tui/src/__tests__/memoryMonitor.test.ts +102 -0
- package/agent/ui-tui/src/__tests__/paths.test.ts +41 -1
- package/agent/ui-tui/src/__tests__/terminalModes.test.ts +22 -0
- package/agent/ui-tui/src/__tests__/text.test.ts +23 -0
- package/agent/ui-tui/src/__tests__/textInputFastEcho.test.ts +37 -0
- package/agent/ui-tui/src/__tests__/turnControllerNotice.test.ts +43 -0
- package/agent/ui-tui/src/__tests__/useInputHandlers.test.ts +38 -1
- package/agent/ui-tui/src/__tests__/virtualHeights.test.ts +8 -0
- package/agent/ui-tui/src/app/createGatewayEventHandler.ts +102 -7
- package/agent/ui-tui/src/app/interfaces.ts +64 -1
- package/agent/ui-tui/src/app/overlayStore.ts +18 -2
- package/agent/ui-tui/src/app/slash/commands/billing.ts +332 -0
- package/agent/ui-tui/src/app/slash/commands/core.ts +31 -2
- package/agent/ui-tui/src/app/slash/commands/credits.ts +57 -0
- package/agent/ui-tui/src/app/slash/commands/ops.ts +28 -0
- package/agent/ui-tui/src/app/slash/commands/session.ts +32 -4
- package/agent/ui-tui/src/app/slash/registry.ts +4 -0
- package/agent/ui-tui/src/app/turnController.ts +145 -2
- package/agent/ui-tui/src/app/uiStore.ts +2 -0
- package/agent/ui-tui/src/app/useInputHandlers.ts +42 -4
- package/agent/ui-tui/src/app/useMainApp.ts +54 -8
- package/agent/ui-tui/src/app/useSessionLifecycle.ts +40 -31
- package/agent/ui-tui/src/app/useSubmission.ts +23 -31
- package/agent/ui-tui/src/components/appChrome.tsx +112 -5
- package/agent/ui-tui/src/components/appLayout.tsx +9 -0
- package/agent/ui-tui/src/components/appOverlays.tsx +25 -1
- package/agent/ui-tui/src/components/billingOverlay.tsx +684 -0
- package/agent/ui-tui/src/components/branding.tsx +15 -3
- package/agent/ui-tui/src/components/messageLine.tsx +25 -3
- package/agent/ui-tui/src/components/pluginsHub.tsx +238 -0
- package/agent/ui-tui/src/components/prompts.tsx +31 -17
- package/agent/ui-tui/src/components/streamingAssistant.tsx +63 -55
- package/agent/ui-tui/src/components/textInput.tsx +16 -0
- package/agent/ui-tui/src/config/env.ts +12 -0
- package/agent/ui-tui/src/config/limits.ts +13 -0
- package/agent/ui-tui/src/domain/blockLayout.ts +146 -0
- package/agent/ui-tui/src/domain/paths.ts +24 -0
- package/agent/ui-tui/src/domain/slash.ts +40 -0
- package/agent/ui-tui/src/entry.tsx +35 -4
- package/agent/ui-tui/src/gatewayClient.ts +22 -10
- package/agent/ui-tui/src/gatewayTypes.ts +130 -1
- package/agent/ui-tui/src/lib/gracefulExit.ts +24 -4
- package/agent/ui-tui/src/lib/memory.test.ts +162 -0
- package/agent/ui-tui/src/lib/memory.ts +60 -1
- package/agent/ui-tui/src/lib/memoryMonitor.ts +79 -4
- package/agent/ui-tui/src/lib/osc52.ts +1 -1
- package/agent/ui-tui/src/lib/text.test.ts +32 -1
- package/agent/ui-tui/src/lib/text.ts +29 -2
- package/agent/ui-tui/src/lib/virtualHeights.ts +13 -0
- package/agent/ui-tui/src/types.ts +5 -0
- package/agent/ui-tui/tsconfig.build.json +0 -1
- package/agent/ui-tui/tsconfig.json +2 -1
- package/agent/utils.py +66 -2
- package/agent/uv.lock +300 -684
- package/agent/web/index.html +2 -2
- package/agent/web/package.json +11 -6
- package/agent/web/public/claw-bg.webp +0 -0
- package/agent/web/public/claw-logo.webp +0 -0
- package/agent/web/src/App.tsx +138 -48
- package/agent/web/src/components/AutomationBlueprints.tsx +225 -0
- package/agent/web/src/components/Backdrop.tsx +15 -0
- package/agent/web/src/components/ChatSessionList.tsx +260 -0
- package/agent/web/src/components/ChatSidebar.tsx +262 -78
- package/agent/web/src/components/ConfirmDialog.tsx +122 -0
- package/agent/web/src/components/ModelPickerDialog.tsx +111 -16
- package/agent/web/src/components/ModelReloadConfirm.tsx +40 -0
- package/agent/web/src/components/ProfileScopeBanner.tsx +30 -0
- package/agent/web/src/components/ProfileSwitcher.tsx +67 -0
- package/agent/web/src/components/ReasoningPicker.tsx +167 -0
- package/agent/web/src/components/SkillEditorDialog.tsx +215 -0
- package/agent/web/src/components/ThemeSwitcher.tsx +119 -4
- package/agent/web/src/components/ToolsetConfigDrawer.tsx +457 -0
- package/agent/web/src/contexts/PageHeaderProvider.tsx +7 -4
- package/agent/web/src/contexts/ProfileProvider.tsx +137 -0
- package/agent/web/src/contexts/SystemActions.tsx +6 -8
- package/agent/web/src/contexts/profile-context.ts +19 -0
- package/agent/web/src/contexts/useProfileScope.ts +6 -0
- package/agent/web/src/i18n/af.ts +5 -4
- package/agent/web/src/i18n/de.ts +5 -4
- package/agent/web/src/i18n/en.ts +58 -4
- package/agent/web/src/i18n/es.ts +5 -3
- package/agent/web/src/i18n/fr.ts +5 -3
- package/agent/web/src/i18n/ga.ts +5 -4
- package/agent/web/src/i18n/hu.ts +5 -4
- package/agent/web/src/i18n/it.ts +5 -4
- package/agent/web/src/i18n/ja.ts +5 -4
- package/agent/web/src/i18n/ko.ts +5 -4
- package/agent/web/src/i18n/pt.ts +5 -3
- package/agent/web/src/i18n/ru.ts +5 -4
- package/agent/web/src/i18n/tr.ts +5 -4
- package/agent/web/src/i18n/types.ts +59 -1
- package/agent/web/src/i18n/uk.ts +5 -3
- package/agent/web/src/i18n/zh-hant.ts +5 -4
- package/agent/web/src/i18n/zh.ts +5 -4
- package/agent/web/src/index.css +2 -2
- package/agent/web/src/lib/api.ts +819 -52
- package/agent/web/src/lib/dashboard-flags.ts +16 -7
- package/agent/web/src/lib/reasoning-effort.test.ts +48 -0
- package/agent/web/src/lib/reasoning-effort.ts +36 -0
- package/agent/web/src/lib/session-refresh.test.ts +21 -0
- package/agent/web/src/lib/session-refresh.ts +26 -0
- package/agent/web/src/pages/ChannelsPage.tsx +529 -68
- package/agent/web/src/pages/ChatPage.tsx +249 -56
- package/agent/web/src/pages/ConfigPage.tsx +11 -1
- package/agent/web/src/pages/CronPage.tsx +219 -31
- package/agent/web/src/pages/EnvPage.tsx +25 -6
- package/agent/web/src/pages/FilesPage.tsx +525 -0
- package/agent/web/src/pages/McpPage.tsx +80 -3
- package/agent/web/src/pages/ModelsPage.tsx +97 -12
- package/agent/web/src/pages/PluginsPage.tsx +1 -1
- package/agent/web/src/pages/ProfileBuilderPage.tsx +611 -0
- package/agent/web/src/pages/ProfilesPage.tsx +1038 -172
- package/agent/web/src/pages/SessionsPage.tsx +144 -13
- package/agent/web/src/pages/SkillsPage.tsx +851 -70
- package/agent/web/src/pages/SystemPage.tsx +340 -4
- package/agent/web/src/pages/WalletPage.tsx +401 -0
- package/agent/web/src/pages/WebhooksPage.tsx +145 -15
- package/agent/web/src/pages/X402Page.tsx +207 -0
- package/agent/web/src/plugins/registry.ts +28 -11
- package/agent/web/src/plugins/sdk.d.ts +160 -0
- package/agent/web/src/themes/context.tsx +112 -5
- package/agent/web/src/themes/fonts.ts +167 -0
- package/agent/web/src/themes/index.ts +7 -0
- package/agent/web/tsconfig.app.json +0 -1
- package/agent/web/vite.config.ts +1 -8
- package/agent/web/vitest.config.ts +16 -0
- package/package.json +1 -1
- package/agent/apps/desktop/package-lock.json +0 -18363
- package/agent/apps/desktop/src/app/chat/composer/skin-slash-popover.tsx +0 -56
- package/agent/apps/desktop/src/components/assistant-ui/thread-virtualizer.tsx +0 -382
- package/agent/apps/desktop/src/components/assistant-ui/todo-tool.tsx +0 -109
- package/agent/apps/desktop/src/components/chat/generated-image-context.tsx +0 -19
- package/agent/optional-skills/productivity/shop-app/SKILL.md +0 -340
- package/agent/skills/autonomous-ai-agents/kanban-codex-lane/SKILL.md +0 -277
- package/agent/skills/autonomous-ai-agents/kanban-codex-lane/templates/pmb-codex-lane-prompt.md +0 -57
- package/agent/skills/diagramming/DESCRIPTION.md +0 -3
- package/agent/skills/domain/DESCRIPTION.md +0 -24
- package/agent/skills/gifs/DESCRIPTION.md +0 -3
- package/agent/skills/inference-sh/DESCRIPTION.md +0 -19
- package/agent/skills/mcp/DESCRIPTION.md +0 -3
- package/agent/skills/media/spotify/SKILL.md +0 -135
- package/agent/skills/mlops/training/DESCRIPTION.md +0 -3
- package/agent/skills/mlops/vector-databases/DESCRIPTION.md +0 -3
- package/agent/skills/productivity/linear/SKILL.md +0 -380
- package/agent/skills/productivity/linear/scripts/linear_api.py +0 -445
- package/agent/skills/software-development/debugging-hermes-tui-commands/SKILL.md +0 -152
- package/agent/skills/software-development/writing-plans/SKILL.md +0 -297
- package/agent/ui-tui/package-lock.json +0 -7449
- package/agent/ui-tui/packages/hermes-ink/package-lock.json +0 -1289
- package/agent/web/package-lock.json +0 -8887
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/PORT_NOTES.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/SKILL.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/prompts/system.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/palettes/macaron.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/palettes/mono-ink.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/palettes/neon.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/palettes/warm.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/prompt-construction.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/style-presets.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/blueprint.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/chalkboard.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/editorial.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/elegant.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/fantasy-animation.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/flat-doodle.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/flat.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/ink-notes.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/intuition-machine.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/minimal.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/nature.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/notion.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/pixel-art.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/playful.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/retro.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/scientific.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/screen-print.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/sketch-notes.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/sketch.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/vector-illustration.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/vintage.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/warm.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles/watercolor.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/styles.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/usage.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-article-illustrator/references/workflow.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/PORT_NOTES.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/SKILL.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/analysis-framework.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/art-styles/chalk.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/art-styles/ink-brush.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/art-styles/ligne-claire.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/art-styles/manga.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/art-styles/minimalist.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/art-styles/realistic.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/auto-selection.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/base-prompt.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/character-template.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/cinematic.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/dense.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/four-panel.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/mixed.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/splash.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/standard.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/layouts/webtoon.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/ohmsha-guide.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/partial-workflows.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/presets/concept-story.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/presets/four-panel.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/presets/ohmsha.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/presets/shoujo.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/presets/wuxia.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/storyboard-template.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/action.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/dramatic.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/energetic.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/neutral.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/romantic.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/vintage.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/tones/warm.md +0 -0
- /package/agent/{skills → optional-skills}/creative/baoyu-comic/references/workflow.md +0 -0
- /package/agent/{skills → optional-skills}/creative/creative-ideation/SKILL.md +0 -0
- /package/agent/{skills → optional-skills}/creative/creative-ideation/references/full-prompt-library.md +0 -0
- /package/agent/{skills → optional-skills}/creative/pixel-art/ATTRIBUTION.md +0 -0
- /package/agent/{skills → optional-skills}/creative/pixel-art/SKILL.md +0 -0
- /package/agent/{skills → optional-skills}/creative/pixel-art/references/palettes.md +0 -0
- /package/agent/{skills → optional-skills}/creative/pixel-art/scripts/__init__.py +0 -0
- /package/agent/{skills → optional-skills}/creative/pixel-art/scripts/palettes.py +0 -0
- /package/agent/{skills → optional-skills}/creative/pixel-art/scripts/pixel_art.py +0 -0
- /package/agent/{skills → optional-skills}/creative/pixel-art/scripts/pixel_art_video.py +0 -0
- /package/agent/{skills/mlops/inference → optional-skills/mlops}/obliteratus/SKILL.md +0 -0
- /package/agent/{skills/mlops/inference → optional-skills/mlops}/obliteratus/references/analysis-modules.md +0 -0
- /package/agent/{skills/mlops/inference → optional-skills/mlops}/obliteratus/references/methods-guide.md +0 -0
- /package/agent/{skills/mlops/inference → optional-skills/mlops}/obliteratus/templates/abliteration-config.yaml +0 -0
- /package/agent/{skills/mlops/inference → optional-skills/mlops}/obliteratus/templates/analysis-study.yaml +0 -0
- /package/agent/{skills/mlops/inference → optional-skills/mlops}/obliteratus/templates/batch-abliteration.yaml +0 -0
- /package/agent/{skills → optional-skills}/mlops/research/DESCRIPTION.md +0 -0
- /package/agent/{skills → optional-skills}/mlops/research/dspy/SKILL.md +0 -0
- /package/agent/{skills → optional-skills}/mlops/research/dspy/references/examples.md +0 -0
- /package/agent/{skills → optional-skills}/mlops/research/dspy/references/modules.md +0 -0
- /package/agent/{skills → optional-skills}/mlops/research/dspy/references/optimizers.md +0 -0
- /package/agent/{skills/red-teaming → optional-skills/security}/godmode/references/jailbreak-templates.md +0 -0
- /package/agent/{skills/red-teaming → optional-skills/security}/godmode/references/refusal-detection.md +0 -0
- /package/agent/{skills/red-teaming → optional-skills/security}/godmode/scripts/godmode_race.py +0 -0
- /package/agent/{skills/red-teaming → optional-skills/security}/godmode/scripts/load_godmode.py +0 -0
- /package/agent/{skills/red-teaming → optional-skills/security}/godmode/scripts/parseltongue.py +0 -0
- /package/agent/{skills/red-teaming → optional-skills/security}/godmode/templates/prefill-subtle.json +0 -0
- /package/agent/{skills/red-teaming → optional-skills/security}/godmode/templates/prefill.json +0 -0
- /package/agent/{skills → optional-skills}/software-development/subagent-driven-development/references/context-budget-discipline.md +0 -0
- /package/agent/{skills → optional-skills}/software-development/subagent-driven-development/references/gates-taxonomy.md +0 -0
|
@@ -0,0 +1,2759 @@
|
|
|
1
|
+
"""Per-provider model-selection wizard flows for ``hermes setup`` / ``hermes model``.
|
|
2
|
+
|
|
3
|
+
Extracted from ``hermes_cli/main.py`` as part of the god-file decomposition
|
|
4
|
+
campaign (``~/.hermes/plans/god-file-decomposition.md``, Phase 2 — splitting
|
|
5
|
+
main.py handler/flow bodies out of the module). These 18 ``_model_flow_*``
|
|
6
|
+
functions are the interactive provider-setup branches dispatched by
|
|
7
|
+
``select_provider_and_model`` (which stays in main.py).
|
|
8
|
+
|
|
9
|
+
Behavior-neutral: each function is lifted verbatim. ``select_provider_and_model``
|
|
10
|
+
in main.py re-imports them (``from hermes_cli.model_setup_flows import *``-style
|
|
11
|
+
explicit import) so existing call sites — and test monkeypatches that target
|
|
12
|
+
``hermes_cli.main._model_flow_*`` — keep resolving against main.py's namespace.
|
|
13
|
+
|
|
14
|
+
main.py-internal helpers the flows call (``_prompt_api_key``, ``_save_custom_provider``,
|
|
15
|
+
the reasoning-effort/stepfun/qwen helpers, ``_run_anthropic_oauth_flow``, …) are
|
|
16
|
+
imported lazily inside the flows (``from hermes_cli.main import ...`` resolves at
|
|
17
|
+
call time, when main.py is fully loaded) so this module never imports
|
|
18
|
+
``hermes_cli.main`` at import time -> no import cycle.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import argparse
|
|
24
|
+
import os
|
|
25
|
+
import subprocess
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _prompt_auth_credentials_choice(title: str) -> str:
|
|
29
|
+
"""Prompt for reuse / reauthenticate / cancel with the standard radio UI.
|
|
30
|
+
|
|
31
|
+
Returns one of ``"use"``, ``"reauth"``, ``"cancel"``. Falls back to a
|
|
32
|
+
numbered prompt when curses is unavailable (piped stdin, non-TTY).
|
|
33
|
+
"""
|
|
34
|
+
choices = [
|
|
35
|
+
"Use existing credentials",
|
|
36
|
+
"Reauthenticate (new OAuth login)",
|
|
37
|
+
"Cancel",
|
|
38
|
+
]
|
|
39
|
+
try:
|
|
40
|
+
from hermes_cli.setup import _curses_prompt_choice
|
|
41
|
+
|
|
42
|
+
idx = _curses_prompt_choice(title, choices, 0)
|
|
43
|
+
if idx >= 0:
|
|
44
|
+
print()
|
|
45
|
+
return ("use", "reauth", "cancel")[idx]
|
|
46
|
+
except Exception:
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
print(title)
|
|
50
|
+
for i, label in enumerate(choices, 1):
|
|
51
|
+
marker = "→" if i == 1 else " "
|
|
52
|
+
print(f" {marker} {i}. {label}")
|
|
53
|
+
print()
|
|
54
|
+
try:
|
|
55
|
+
choice = input(" Choice [1/2/3]: ").strip()
|
|
56
|
+
except (KeyboardInterrupt, EOFError):
|
|
57
|
+
choice = "1"
|
|
58
|
+
|
|
59
|
+
if choice == "2":
|
|
60
|
+
return "reauth"
|
|
61
|
+
if choice == "3":
|
|
62
|
+
return "cancel"
|
|
63
|
+
return "use"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _model_flow_openrouter(config, current_model=""):
|
|
67
|
+
"""OpenRouter provider: ensure API key, then pick model."""
|
|
68
|
+
from hermes_cli.main import _prompt_api_key
|
|
69
|
+
from hermes_constants import OPENROUTER_BASE_URL
|
|
70
|
+
from hermes_cli.auth import (
|
|
71
|
+
ProviderConfig,
|
|
72
|
+
_prompt_model_selection,
|
|
73
|
+
_save_model_choice,
|
|
74
|
+
deactivate_provider,
|
|
75
|
+
)
|
|
76
|
+
from hermes_cli.config import get_env_value
|
|
77
|
+
|
|
78
|
+
# Route through _prompt_api_key so users can replace a stale/broken key
|
|
79
|
+
# in-flow (K/R/C) instead of having to edit ~/.hermes/.env by hand. The
|
|
80
|
+
# previous bypass-when-key-exists branch left no way to recover from a
|
|
81
|
+
# bad paste short of re-running `hermes setup` from scratch. OpenRouter
|
|
82
|
+
# isn't in PROVIDER_REGISTRY so we synthesize a minimal pconfig.
|
|
83
|
+
pconfig = ProviderConfig(
|
|
84
|
+
id="openrouter",
|
|
85
|
+
name="OpenRouter",
|
|
86
|
+
auth_type="api_key",
|
|
87
|
+
api_key_env_vars=("OPENROUTER_API_KEY",),
|
|
88
|
+
)
|
|
89
|
+
existing_key = get_env_value("OPENROUTER_API_KEY") or ""
|
|
90
|
+
if not existing_key:
|
|
91
|
+
print("Get one at: https://openrouter.ai/keys")
|
|
92
|
+
print()
|
|
93
|
+
_resolved, abort = _prompt_api_key(pconfig, existing_key, provider_id="openrouter")
|
|
94
|
+
if abort:
|
|
95
|
+
return
|
|
96
|
+
|
|
97
|
+
from hermes_cli.models import model_ids, get_pricing_for_provider
|
|
98
|
+
|
|
99
|
+
openrouter_models = model_ids(force_refresh=True)
|
|
100
|
+
|
|
101
|
+
# Fetch live pricing (non-blocking — returns empty dict on failure)
|
|
102
|
+
pricing = get_pricing_for_provider("openrouter", force_refresh=True)
|
|
103
|
+
|
|
104
|
+
selected = _prompt_model_selection(
|
|
105
|
+
openrouter_models,
|
|
106
|
+
current_model=current_model,
|
|
107
|
+
pricing=pricing,
|
|
108
|
+
confirm_provider="openrouter",
|
|
109
|
+
confirm_base_url=OPENROUTER_BASE_URL,
|
|
110
|
+
confirm_api_key=_resolved or existing_key,
|
|
111
|
+
)
|
|
112
|
+
if selected:
|
|
113
|
+
_save_model_choice(selected)
|
|
114
|
+
|
|
115
|
+
# Update config provider and deactivate any OAuth provider
|
|
116
|
+
from hermes_cli.config import load_config, save_config
|
|
117
|
+
|
|
118
|
+
cfg = load_config()
|
|
119
|
+
model = cfg.get("model")
|
|
120
|
+
if not isinstance(model, dict):
|
|
121
|
+
model = {"default": model} if model else {}
|
|
122
|
+
cfg["model"] = model
|
|
123
|
+
model["provider"] = "openrouter"
|
|
124
|
+
model["base_url"] = OPENROUTER_BASE_URL
|
|
125
|
+
model["api_mode"] = "chat_completions"
|
|
126
|
+
save_config(cfg)
|
|
127
|
+
deactivate_provider()
|
|
128
|
+
print(f"Default model set to: {selected} (via OpenRouter)")
|
|
129
|
+
else:
|
|
130
|
+
print("No change.")
|
|
131
|
+
|
|
132
|
+
def _model_flow_nous(config, current_model="", args=None):
|
|
133
|
+
"""Nous Portal provider: ensure logged in, then pick model."""
|
|
134
|
+
from hermes_cli.auth import (
|
|
135
|
+
get_provider_auth_state,
|
|
136
|
+
_prompt_model_selection,
|
|
137
|
+
_save_model_choice,
|
|
138
|
+
_update_config_for_provider,
|
|
139
|
+
resolve_nous_runtime_credentials,
|
|
140
|
+
AuthError,
|
|
141
|
+
format_auth_error,
|
|
142
|
+
_login_nous,
|
|
143
|
+
PROVIDER_REGISTRY,
|
|
144
|
+
)
|
|
145
|
+
from hermes_cli.config import (
|
|
146
|
+
get_env_value,
|
|
147
|
+
load_config,
|
|
148
|
+
save_config,
|
|
149
|
+
save_env_value,
|
|
150
|
+
)
|
|
151
|
+
from hermes_cli.nous_subscription import prompt_enable_tool_gateway
|
|
152
|
+
|
|
153
|
+
state = get_provider_auth_state("nous")
|
|
154
|
+
if not state or not state.get("access_token"):
|
|
155
|
+
print("Not logged into Nous Portal. Starting login...")
|
|
156
|
+
print()
|
|
157
|
+
try:
|
|
158
|
+
mock_args = argparse.Namespace(
|
|
159
|
+
portal_url=getattr(args, "portal_url", None),
|
|
160
|
+
inference_url=getattr(args, "inference_url", None),
|
|
161
|
+
client_id=getattr(args, "client_id", None),
|
|
162
|
+
scope=getattr(args, "scope", None),
|
|
163
|
+
no_browser=bool(getattr(args, "no_browser", False)),
|
|
164
|
+
timeout=getattr(args, "timeout", None) or 15.0,
|
|
165
|
+
ca_bundle=getattr(args, "ca_bundle", None),
|
|
166
|
+
insecure=bool(getattr(args, "insecure", False)),
|
|
167
|
+
)
|
|
168
|
+
_login_nous(mock_args, PROVIDER_REGISTRY["nous"])
|
|
169
|
+
# Offer Tool Gateway enablement for paid subscribers
|
|
170
|
+
try:
|
|
171
|
+
_refreshed = load_config() or {}
|
|
172
|
+
prompt_enable_tool_gateway(_refreshed)
|
|
173
|
+
except Exception:
|
|
174
|
+
pass
|
|
175
|
+
except SystemExit:
|
|
176
|
+
print("Login cancelled or failed.")
|
|
177
|
+
return
|
|
178
|
+
except Exception as exc:
|
|
179
|
+
print(f"Login failed: {exc}")
|
|
180
|
+
return
|
|
181
|
+
# login_nous already handles model selection + config update
|
|
182
|
+
return
|
|
183
|
+
|
|
184
|
+
# Already logged in — use curated model list (same as OpenRouter defaults).
|
|
185
|
+
# The live /models endpoint returns hundreds of models; the curated list
|
|
186
|
+
# shows only agentic models users recognize from OpenRouter.
|
|
187
|
+
from hermes_cli.models import (
|
|
188
|
+
get_curated_nous_model_ids,
|
|
189
|
+
get_pricing_for_provider,
|
|
190
|
+
check_nous_free_tier,
|
|
191
|
+
partition_nous_models_by_tier,
|
|
192
|
+
union_with_portal_free_recommendations,
|
|
193
|
+
union_with_portal_paid_recommendations,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
model_ids = get_curated_nous_model_ids()
|
|
197
|
+
if not model_ids:
|
|
198
|
+
print("No curated models available for Nous Portal.")
|
|
199
|
+
return
|
|
200
|
+
|
|
201
|
+
# Verify credentials are still valid (catches expired sessions early)
|
|
202
|
+
try:
|
|
203
|
+
creds = resolve_nous_runtime_credentials()
|
|
204
|
+
except Exception as exc:
|
|
205
|
+
relogin = isinstance(exc, AuthError) and exc.relogin_required
|
|
206
|
+
msg = format_auth_error(exc) if isinstance(exc, AuthError) else str(exc)
|
|
207
|
+
if relogin:
|
|
208
|
+
print(f"Session expired: {msg}")
|
|
209
|
+
print("Re-authenticating with Nous Portal...\n")
|
|
210
|
+
try:
|
|
211
|
+
mock_args = argparse.Namespace(
|
|
212
|
+
portal_url=None,
|
|
213
|
+
inference_url=None,
|
|
214
|
+
client_id=None,
|
|
215
|
+
scope=None,
|
|
216
|
+
no_browser=False,
|
|
217
|
+
timeout=15.0,
|
|
218
|
+
ca_bundle=None,
|
|
219
|
+
insecure=False,
|
|
220
|
+
)
|
|
221
|
+
_login_nous(mock_args, PROVIDER_REGISTRY["nous"])
|
|
222
|
+
except Exception as login_exc:
|
|
223
|
+
print(f"Re-login failed: {login_exc}")
|
|
224
|
+
return
|
|
225
|
+
print(f"Could not verify credentials: {msg}")
|
|
226
|
+
return
|
|
227
|
+
|
|
228
|
+
# Fetch live pricing (non-blocking — returns empty dict on failure)
|
|
229
|
+
pricing = get_pricing_for_provider("nous")
|
|
230
|
+
|
|
231
|
+
# Force fresh account data for model selection so recent credit purchases
|
|
232
|
+
# are reflected immediately.
|
|
233
|
+
free_tier = check_nous_free_tier(force_fresh=True)
|
|
234
|
+
if not free_tier:
|
|
235
|
+
try:
|
|
236
|
+
refreshed_creds = resolve_nous_runtime_credentials(
|
|
237
|
+
force_refresh=True,
|
|
238
|
+
)
|
|
239
|
+
if refreshed_creds:
|
|
240
|
+
creds = refreshed_creds
|
|
241
|
+
except Exception:
|
|
242
|
+
# Runtime inference has its own paid-entitlement recovery path; do
|
|
243
|
+
# not block model selection if this opportunistic refresh fails.
|
|
244
|
+
pass
|
|
245
|
+
|
|
246
|
+
# Resolve portal URL early — needed both for upgrade links and for the
|
|
247
|
+
# freeRecommendedModels endpoint below.
|
|
248
|
+
_nous_portal_url = ""
|
|
249
|
+
try:
|
|
250
|
+
_nous_state = get_provider_auth_state("nous")
|
|
251
|
+
if _nous_state:
|
|
252
|
+
_nous_portal_url = _nous_state.get("portal_base_url", "")
|
|
253
|
+
except Exception:
|
|
254
|
+
pass
|
|
255
|
+
|
|
256
|
+
# For free users: partition models into selectable/unavailable based on
|
|
257
|
+
# whether they are free per the Portal-reported pricing. First augment
|
|
258
|
+
# with the Portal's freeRecommendedModels list so newly-launched free
|
|
259
|
+
# models show up even if this CLI build's hardcoded curated list and
|
|
260
|
+
# docs-hosted manifest haven't caught up yet.
|
|
261
|
+
#
|
|
262
|
+
# For paid users: mirror the same idea with paidRecommendedModels so
|
|
263
|
+
# newly-launched paid models surface in the picker too — independent
|
|
264
|
+
# of CLI release cadence.
|
|
265
|
+
unavailable_models: list[str] = []
|
|
266
|
+
unavailable_message = ""
|
|
267
|
+
if free_tier:
|
|
268
|
+
try:
|
|
269
|
+
from hermes_cli.nous_account import (
|
|
270
|
+
format_nous_portal_entitlement_message,
|
|
271
|
+
get_nous_portal_account_info,
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
_account_info = get_nous_portal_account_info(force_fresh=True)
|
|
275
|
+
unavailable_message = (
|
|
276
|
+
format_nous_portal_entitlement_message(
|
|
277
|
+
_account_info,
|
|
278
|
+
capability="paid Nous models",
|
|
279
|
+
)
|
|
280
|
+
or ""
|
|
281
|
+
)
|
|
282
|
+
except Exception:
|
|
283
|
+
unavailable_message = ""
|
|
284
|
+
model_ids, pricing = union_with_portal_free_recommendations(
|
|
285
|
+
model_ids, pricing, _nous_portal_url,
|
|
286
|
+
)
|
|
287
|
+
model_ids, unavailable_models = partition_nous_models_by_tier(
|
|
288
|
+
model_ids, pricing, free_tier=True
|
|
289
|
+
)
|
|
290
|
+
else:
|
|
291
|
+
model_ids, pricing = union_with_portal_paid_recommendations(
|
|
292
|
+
model_ids, pricing, _nous_portal_url,
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
if not model_ids and not unavailable_models:
|
|
296
|
+
print("No models available for Nous Portal after filtering.")
|
|
297
|
+
return
|
|
298
|
+
|
|
299
|
+
if free_tier and not model_ids:
|
|
300
|
+
print("No free models currently available.")
|
|
301
|
+
if unavailable_models:
|
|
302
|
+
from hermes_cli.auth import DEFAULT_NOUS_PORTAL_URL
|
|
303
|
+
|
|
304
|
+
_url = (_nous_portal_url or DEFAULT_NOUS_PORTAL_URL).rstrip("/")
|
|
305
|
+
print(unavailable_message or f"Upgrade at {_url} to access paid models.")
|
|
306
|
+
return
|
|
307
|
+
|
|
308
|
+
print(
|
|
309
|
+
f'Showing {len(model_ids)} curated models — use "Enter custom model name" for others.'
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
selected = _prompt_model_selection(
|
|
313
|
+
model_ids,
|
|
314
|
+
current_model=current_model,
|
|
315
|
+
pricing=pricing,
|
|
316
|
+
unavailable_models=unavailable_models,
|
|
317
|
+
portal_url=_nous_portal_url,
|
|
318
|
+
unavailable_message=unavailable_message,
|
|
319
|
+
confirm_provider="nous",
|
|
320
|
+
confirm_base_url=creds.get("base_url", ""),
|
|
321
|
+
confirm_api_key=creds.get("api_key", ""),
|
|
322
|
+
)
|
|
323
|
+
if selected:
|
|
324
|
+
_save_model_choice(selected)
|
|
325
|
+
# Reactivate Nous as the provider and update config
|
|
326
|
+
inference_url = creds.get("base_url", "")
|
|
327
|
+
_update_config_for_provider("nous", inference_url)
|
|
328
|
+
current_model_cfg = config.get("model")
|
|
329
|
+
if isinstance(current_model_cfg, dict):
|
|
330
|
+
model_cfg = dict(current_model_cfg)
|
|
331
|
+
elif isinstance(current_model_cfg, str) and current_model_cfg.strip():
|
|
332
|
+
model_cfg = {"default": current_model_cfg.strip()}
|
|
333
|
+
else:
|
|
334
|
+
model_cfg = {}
|
|
335
|
+
model_cfg["provider"] = "nous"
|
|
336
|
+
model_cfg["default"] = selected
|
|
337
|
+
if inference_url and inference_url.strip():
|
|
338
|
+
model_cfg["base_url"] = inference_url.rstrip("/")
|
|
339
|
+
else:
|
|
340
|
+
model_cfg.pop("base_url", None)
|
|
341
|
+
config["model"] = model_cfg
|
|
342
|
+
# Clear any custom endpoint that might conflict
|
|
343
|
+
if get_env_value("OPENAI_BASE_URL"):
|
|
344
|
+
save_env_value("OPENAI_BASE_URL", "")
|
|
345
|
+
save_env_value("OPENAI_API_KEY", "")
|
|
346
|
+
save_config(config)
|
|
347
|
+
print(f"Default model set to: {selected} (via Nous Portal)")
|
|
348
|
+
# Offer Tool Gateway enablement for paid subscribers
|
|
349
|
+
prompt_enable_tool_gateway(config)
|
|
350
|
+
else:
|
|
351
|
+
print("No change.")
|
|
352
|
+
|
|
353
|
+
def _model_flow_openai_codex(config, current_model=""):
|
|
354
|
+
"""OpenAI Codex provider: ensure logged in, then pick model."""
|
|
355
|
+
from hermes_cli.auth import (
|
|
356
|
+
get_codex_auth_status,
|
|
357
|
+
_prompt_model_selection,
|
|
358
|
+
_save_model_choice,
|
|
359
|
+
_update_config_for_provider,
|
|
360
|
+
_login_openai_codex,
|
|
361
|
+
PROVIDER_REGISTRY,
|
|
362
|
+
DEFAULT_CODEX_BASE_URL,
|
|
363
|
+
)
|
|
364
|
+
from hermes_cli.codex_models import get_codex_model_ids
|
|
365
|
+
|
|
366
|
+
status = get_codex_auth_status()
|
|
367
|
+
if status.get("logged_in"):
|
|
368
|
+
print(" OpenAI Codex credentials: ✓")
|
|
369
|
+
print()
|
|
370
|
+
choice = _prompt_auth_credentials_choice("OpenAI Codex credentials:")
|
|
371
|
+
|
|
372
|
+
if choice == "reauth":
|
|
373
|
+
print("Starting a fresh OpenAI Codex login...")
|
|
374
|
+
print()
|
|
375
|
+
try:
|
|
376
|
+
mock_args = argparse.Namespace()
|
|
377
|
+
_login_openai_codex(
|
|
378
|
+
mock_args,
|
|
379
|
+
PROVIDER_REGISTRY["openai-codex"],
|
|
380
|
+
force_new_login=True,
|
|
381
|
+
)
|
|
382
|
+
except SystemExit:
|
|
383
|
+
print("Login cancelled or failed.")
|
|
384
|
+
return
|
|
385
|
+
except Exception as exc:
|
|
386
|
+
print(f"Login failed: {exc}")
|
|
387
|
+
return
|
|
388
|
+
status = get_codex_auth_status()
|
|
389
|
+
if not status.get("logged_in"):
|
|
390
|
+
print("Login failed.")
|
|
391
|
+
return
|
|
392
|
+
elif choice == "cancel":
|
|
393
|
+
return
|
|
394
|
+
else:
|
|
395
|
+
print("Not logged into OpenAI Codex. Starting login...")
|
|
396
|
+
print()
|
|
397
|
+
try:
|
|
398
|
+
mock_args = argparse.Namespace()
|
|
399
|
+
_login_openai_codex(mock_args, PROVIDER_REGISTRY["openai-codex"])
|
|
400
|
+
except SystemExit:
|
|
401
|
+
print("Login cancelled or failed.")
|
|
402
|
+
return
|
|
403
|
+
except Exception as exc:
|
|
404
|
+
print(f"Login failed: {exc}")
|
|
405
|
+
return
|
|
406
|
+
|
|
407
|
+
_codex_token = None
|
|
408
|
+
# Prefer credential pool (where `hermes auth` stores device_code tokens),
|
|
409
|
+
# fall back to legacy provider state.
|
|
410
|
+
try:
|
|
411
|
+
_codex_status = get_codex_auth_status()
|
|
412
|
+
if _codex_status.get("logged_in"):
|
|
413
|
+
_codex_token = _codex_status.get("api_key")
|
|
414
|
+
except Exception:
|
|
415
|
+
pass
|
|
416
|
+
if not _codex_token:
|
|
417
|
+
try:
|
|
418
|
+
from hermes_cli.auth import resolve_codex_runtime_credentials
|
|
419
|
+
|
|
420
|
+
_codex_creds = resolve_codex_runtime_credentials()
|
|
421
|
+
_codex_token = _codex_creds.get("api_key")
|
|
422
|
+
except Exception:
|
|
423
|
+
pass
|
|
424
|
+
|
|
425
|
+
codex_models = get_codex_model_ids(access_token=_codex_token)
|
|
426
|
+
|
|
427
|
+
selected = _prompt_model_selection(
|
|
428
|
+
codex_models,
|
|
429
|
+
current_model=current_model,
|
|
430
|
+
confirm_provider="openai-codex",
|
|
431
|
+
confirm_base_url=DEFAULT_CODEX_BASE_URL,
|
|
432
|
+
confirm_api_key=_codex_token or "",
|
|
433
|
+
)
|
|
434
|
+
if selected:
|
|
435
|
+
_save_model_choice(selected)
|
|
436
|
+
_update_config_for_provider("openai-codex", DEFAULT_CODEX_BASE_URL)
|
|
437
|
+
print(f"Default model set to: {selected} (via OpenAI Codex)")
|
|
438
|
+
else:
|
|
439
|
+
print("No change.")
|
|
440
|
+
|
|
441
|
+
def _model_flow_xai_oauth(_config, current_model="", *, args=None):
|
|
442
|
+
"""xAI Grok OAuth (SuperGrok / Premium+) provider: ensure logged in, then pick model."""
|
|
443
|
+
from hermes_cli.auth import (
|
|
444
|
+
get_xai_oauth_auth_status,
|
|
445
|
+
_prompt_model_selection,
|
|
446
|
+
_save_model_choice,
|
|
447
|
+
_update_config_for_provider,
|
|
448
|
+
resolve_xai_oauth_runtime_credentials,
|
|
449
|
+
_login_xai_oauth,
|
|
450
|
+
DEFAULT_XAI_OAUTH_BASE_URL,
|
|
451
|
+
PROVIDER_REGISTRY,
|
|
452
|
+
)
|
|
453
|
+
from hermes_cli.models import _PROVIDER_MODELS
|
|
454
|
+
|
|
455
|
+
status = get_xai_oauth_auth_status()
|
|
456
|
+
if status.get("logged_in"):
|
|
457
|
+
print(" xAI Grok OAuth (SuperGrok / Premium+) credentials: ✓")
|
|
458
|
+
print()
|
|
459
|
+
choice = _prompt_auth_credentials_choice(
|
|
460
|
+
"xAI Grok OAuth (SuperGrok / Premium+) credentials:"
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
if choice == "reauth":
|
|
464
|
+
print("Starting a fresh xAI OAuth login...")
|
|
465
|
+
print()
|
|
466
|
+
try:
|
|
467
|
+
# Forward CLI flags from ``hermes model --manual-paste``
|
|
468
|
+
# / ``--no-browser`` / ``--timeout`` into the loopback
|
|
469
|
+
# login. Without this, browser-only remotes (#26923)
|
|
470
|
+
# can't reach the manual-paste path via ``hermes model``.
|
|
471
|
+
mock_args = argparse.Namespace(
|
|
472
|
+
manual_paste=bool(getattr(args, "manual_paste", False)),
|
|
473
|
+
no_browser=bool(getattr(args, "no_browser", False)),
|
|
474
|
+
timeout=getattr(args, "timeout", None),
|
|
475
|
+
)
|
|
476
|
+
_login_xai_oauth(
|
|
477
|
+
mock_args,
|
|
478
|
+
PROVIDER_REGISTRY["xai-oauth"],
|
|
479
|
+
force_new_login=True,
|
|
480
|
+
)
|
|
481
|
+
except SystemExit:
|
|
482
|
+
print("Login cancelled or failed.")
|
|
483
|
+
return
|
|
484
|
+
except Exception as exc:
|
|
485
|
+
print(f"Login failed: {exc}")
|
|
486
|
+
return
|
|
487
|
+
elif choice == "cancel":
|
|
488
|
+
return
|
|
489
|
+
else:
|
|
490
|
+
print("Not logged into xAI Grok OAuth (SuperGrok / Premium+). Starting login...")
|
|
491
|
+
print()
|
|
492
|
+
try:
|
|
493
|
+
mock_args = argparse.Namespace(
|
|
494
|
+
manual_paste=bool(getattr(args, "manual_paste", False)),
|
|
495
|
+
no_browser=bool(getattr(args, "no_browser", False)),
|
|
496
|
+
timeout=getattr(args, "timeout", None),
|
|
497
|
+
)
|
|
498
|
+
_login_xai_oauth(mock_args, PROVIDER_REGISTRY["xai-oauth"])
|
|
499
|
+
except SystemExit:
|
|
500
|
+
print("Login cancelled or failed.")
|
|
501
|
+
return
|
|
502
|
+
except Exception as exc:
|
|
503
|
+
print(f"Login failed: {exc}")
|
|
504
|
+
return
|
|
505
|
+
|
|
506
|
+
# Resolve a usable base URL. ``resolve_xai_oauth_runtime_credentials``
|
|
507
|
+
# only reads from the auth.json singleton — but credentials may legitimately
|
|
508
|
+
# live only in the pool (e.g. after ``hermes auth add xai-oauth``). Fall
|
|
509
|
+
# back to the default base URL in that case so the model picker still
|
|
510
|
+
# completes successfully instead of bailing out with
|
|
511
|
+
# ``Could not resolve xAI OAuth credentials``.
|
|
512
|
+
base_url = DEFAULT_XAI_OAUTH_BASE_URL
|
|
513
|
+
try:
|
|
514
|
+
creds = resolve_xai_oauth_runtime_credentials()
|
|
515
|
+
base_url = (creds.get("base_url") or "").strip().rstrip("/") or base_url
|
|
516
|
+
except Exception:
|
|
517
|
+
pass
|
|
518
|
+
|
|
519
|
+
models = list(_PROVIDER_MODELS.get("xai-oauth") or _PROVIDER_MODELS.get("xai") or [])
|
|
520
|
+
selected = _prompt_model_selection(models, current_model=current_model or (models[0] if models else "grok-build-0.1"))
|
|
521
|
+
if selected:
|
|
522
|
+
_save_model_choice(selected)
|
|
523
|
+
_update_config_for_provider("xai-oauth", base_url)
|
|
524
|
+
print(f"Default model set to: {selected} (via xAI Grok OAuth — SuperGrok / Premium+)")
|
|
525
|
+
else:
|
|
526
|
+
print("No change.")
|
|
527
|
+
|
|
528
|
+
def _model_flow_qwen_oauth(_config, current_model=""):
|
|
529
|
+
"""Qwen OAuth provider: reuse local Qwen CLI login, then pick model."""
|
|
530
|
+
from hermes_cli.main import _DEFAULT_QWEN_PORTAL_MODELS
|
|
531
|
+
from hermes_cli.auth import (
|
|
532
|
+
get_qwen_auth_status,
|
|
533
|
+
resolve_qwen_runtime_credentials,
|
|
534
|
+
_prompt_model_selection,
|
|
535
|
+
_save_model_choice,
|
|
536
|
+
_update_config_for_provider,
|
|
537
|
+
DEFAULT_QWEN_BASE_URL,
|
|
538
|
+
)
|
|
539
|
+
from hermes_cli.models import fetch_api_models
|
|
540
|
+
|
|
541
|
+
status = get_qwen_auth_status()
|
|
542
|
+
if not status.get("logged_in"):
|
|
543
|
+
print("Not logged into Qwen CLI OAuth.")
|
|
544
|
+
print("Run: qwen auth qwen-oauth")
|
|
545
|
+
auth_file = status.get("auth_file")
|
|
546
|
+
if auth_file:
|
|
547
|
+
print(f"Expected credentials file: {auth_file}")
|
|
548
|
+
if status.get("error"):
|
|
549
|
+
print(f"Error: {status.get('error')}")
|
|
550
|
+
return
|
|
551
|
+
|
|
552
|
+
# Try live model discovery, fall back to curated list.
|
|
553
|
+
models = None
|
|
554
|
+
try:
|
|
555
|
+
creds = resolve_qwen_runtime_credentials(refresh_if_expiring=True)
|
|
556
|
+
models = fetch_api_models(creds["api_key"], creds["base_url"])
|
|
557
|
+
except Exception:
|
|
558
|
+
pass
|
|
559
|
+
if not models:
|
|
560
|
+
models = list(_DEFAULT_QWEN_PORTAL_MODELS)
|
|
561
|
+
|
|
562
|
+
default = current_model or (models[0] if models else "qwen3-coder-plus")
|
|
563
|
+
selected = _prompt_model_selection(
|
|
564
|
+
models,
|
|
565
|
+
current_model=default,
|
|
566
|
+
confirm_provider="qwen-oauth",
|
|
567
|
+
confirm_base_url=DEFAULT_QWEN_BASE_URL,
|
|
568
|
+
)
|
|
569
|
+
if selected:
|
|
570
|
+
_save_model_choice(selected)
|
|
571
|
+
_update_config_for_provider("qwen-oauth", DEFAULT_QWEN_BASE_URL)
|
|
572
|
+
print(f"Default model set to: {selected} (via Qwen OAuth)")
|
|
573
|
+
else:
|
|
574
|
+
print("No change.")
|
|
575
|
+
|
|
576
|
+
def _model_flow_minimax_oauth(config, current_model="", args=None):
|
|
577
|
+
"""MiniMax OAuth provider: ensure logged in, then pick model."""
|
|
578
|
+
from hermes_cli.auth import (
|
|
579
|
+
get_provider_auth_state,
|
|
580
|
+
_prompt_model_selection,
|
|
581
|
+
_save_model_choice,
|
|
582
|
+
_update_config_for_provider,
|
|
583
|
+
resolve_minimax_oauth_runtime_credentials,
|
|
584
|
+
AuthError,
|
|
585
|
+
format_auth_error,
|
|
586
|
+
_login_minimax_oauth,
|
|
587
|
+
PROVIDER_REGISTRY,
|
|
588
|
+
)
|
|
589
|
+
|
|
590
|
+
state = get_provider_auth_state("minimax-oauth")
|
|
591
|
+
if not state or not state.get("access_token"):
|
|
592
|
+
print("Not logged into MiniMax. Starting OAuth login...")
|
|
593
|
+
print()
|
|
594
|
+
try:
|
|
595
|
+
mock_args = argparse.Namespace(
|
|
596
|
+
region=getattr(args, "region", None) or "global",
|
|
597
|
+
no_browser=bool(getattr(args, "no_browser", False)),
|
|
598
|
+
timeout=getattr(args, "timeout", None) or 15.0,
|
|
599
|
+
)
|
|
600
|
+
_login_minimax_oauth(mock_args, PROVIDER_REGISTRY["minimax-oauth"])
|
|
601
|
+
except SystemExit:
|
|
602
|
+
print("Login cancelled or failed.")
|
|
603
|
+
return
|
|
604
|
+
except Exception as exc:
|
|
605
|
+
print(f"Login failed: {exc}")
|
|
606
|
+
return
|
|
607
|
+
|
|
608
|
+
try:
|
|
609
|
+
creds = resolve_minimax_oauth_runtime_credentials()
|
|
610
|
+
except AuthError as exc:
|
|
611
|
+
print(format_auth_error(exc))
|
|
612
|
+
return
|
|
613
|
+
|
|
614
|
+
from hermes_cli.models import _PROVIDER_MODELS
|
|
615
|
+
|
|
616
|
+
model_ids = _PROVIDER_MODELS.get("minimax-oauth", [])
|
|
617
|
+
selected = _prompt_model_selection(
|
|
618
|
+
model_ids,
|
|
619
|
+
current_model,
|
|
620
|
+
confirm_provider="minimax-oauth",
|
|
621
|
+
confirm_base_url=creds["base_url"],
|
|
622
|
+
)
|
|
623
|
+
if not selected:
|
|
624
|
+
return
|
|
625
|
+
_save_model_choice(selected)
|
|
626
|
+
_update_config_for_provider("minimax-oauth", creds["base_url"])
|
|
627
|
+
print(f"\u2713 Using MiniMax model: {selected}")
|
|
628
|
+
|
|
629
|
+
def _model_flow_google_gemini_cli(_config, current_model=""):
|
|
630
|
+
"""Google Gemini OAuth (PKCE) via Cloud Code Assist — supports free AND paid tiers.
|
|
631
|
+
|
|
632
|
+
Flow:
|
|
633
|
+
1. Show upfront warning about Google's ToS stance (per opencode-gemini-auth).
|
|
634
|
+
2. If creds missing, run PKCE browser OAuth via agent.google_oauth.
|
|
635
|
+
3. Resolve project context (env -> config -> auto-discover -> free tier).
|
|
636
|
+
4. Prompt user to pick a model.
|
|
637
|
+
5. Save to ~/.hermes/config.yaml.
|
|
638
|
+
"""
|
|
639
|
+
from hermes_cli.auth import (
|
|
640
|
+
DEFAULT_GEMINI_CLOUDCODE_BASE_URL,
|
|
641
|
+
get_gemini_oauth_auth_status,
|
|
642
|
+
resolve_gemini_oauth_runtime_credentials,
|
|
643
|
+
_prompt_model_selection,
|
|
644
|
+
_save_model_choice,
|
|
645
|
+
_update_config_for_provider,
|
|
646
|
+
)
|
|
647
|
+
from hermes_cli.models import _PROVIDER_MODELS
|
|
648
|
+
|
|
649
|
+
print()
|
|
650
|
+
print("⚠ Google considers using the Gemini CLI OAuth client with third-party")
|
|
651
|
+
print(" software a policy violation. Some users have reported account")
|
|
652
|
+
print(" restrictions. You can use your own API key via 'gemini' provider")
|
|
653
|
+
print(" for the lowest-risk experience.")
|
|
654
|
+
print()
|
|
655
|
+
try:
|
|
656
|
+
proceed = input("Continue with OAuth login? [y/N]: ").strip().lower()
|
|
657
|
+
except (EOFError, KeyboardInterrupt):
|
|
658
|
+
print("Cancelled.")
|
|
659
|
+
return
|
|
660
|
+
if proceed not in {"y", "yes"}:
|
|
661
|
+
print("Cancelled.")
|
|
662
|
+
return
|
|
663
|
+
|
|
664
|
+
status = get_gemini_oauth_auth_status()
|
|
665
|
+
if not status.get("logged_in"):
|
|
666
|
+
try:
|
|
667
|
+
from agent.google_oauth import resolve_project_id_from_env, start_oauth_flow
|
|
668
|
+
|
|
669
|
+
env_project = resolve_project_id_from_env()
|
|
670
|
+
start_oauth_flow(force_relogin=True, project_id=env_project)
|
|
671
|
+
except Exception as exc:
|
|
672
|
+
print(f"OAuth login failed: {exc}")
|
|
673
|
+
return
|
|
674
|
+
|
|
675
|
+
# Verify creds resolve + trigger project discovery
|
|
676
|
+
try:
|
|
677
|
+
creds = resolve_gemini_oauth_runtime_credentials(force_refresh=False)
|
|
678
|
+
project_id = creds.get("project_id", "")
|
|
679
|
+
if project_id:
|
|
680
|
+
print(f" Using GCP project: {project_id}")
|
|
681
|
+
else:
|
|
682
|
+
print(
|
|
683
|
+
" No GCP project configured — free tier will be auto-provisioned on first request."
|
|
684
|
+
)
|
|
685
|
+
except Exception as exc:
|
|
686
|
+
print(f"Failed to resolve Gemini credentials: {exc}")
|
|
687
|
+
return
|
|
688
|
+
|
|
689
|
+
models = list(_PROVIDER_MODELS.get("google-gemini-cli") or [])
|
|
690
|
+
default = current_model or (models[0] if models else "gemini-3-flash-preview")
|
|
691
|
+
selected = _prompt_model_selection(
|
|
692
|
+
models,
|
|
693
|
+
current_model=default,
|
|
694
|
+
confirm_provider="google-gemini-cli",
|
|
695
|
+
confirm_base_url=DEFAULT_GEMINI_CLOUDCODE_BASE_URL,
|
|
696
|
+
)
|
|
697
|
+
if selected:
|
|
698
|
+
_save_model_choice(selected)
|
|
699
|
+
_update_config_for_provider(
|
|
700
|
+
"google-gemini-cli", DEFAULT_GEMINI_CLOUDCODE_BASE_URL
|
|
701
|
+
)
|
|
702
|
+
print(
|
|
703
|
+
f"Default model set to: {selected} (via Google Gemini OAuth / Code Assist)"
|
|
704
|
+
)
|
|
705
|
+
else:
|
|
706
|
+
print("No change.")
|
|
707
|
+
|
|
708
|
+
def _model_flow_custom(config):
|
|
709
|
+
"""Custom endpoint: collect URL, API key, and model name.
|
|
710
|
+
|
|
711
|
+
Automatically saves the endpoint to ``custom_providers`` in config.yaml
|
|
712
|
+
so it appears in the provider menu on subsequent runs.
|
|
713
|
+
"""
|
|
714
|
+
from hermes_cli.main import _auto_provider_name, _prompt_custom_api_mode_selection, _save_custom_provider
|
|
715
|
+
from hermes_cli.auth import _save_model_choice, deactivate_provider
|
|
716
|
+
from hermes_cli.config import get_env_value, load_config, save_config
|
|
717
|
+
from hermes_cli.secret_prompt import masked_secret_prompt
|
|
718
|
+
|
|
719
|
+
current_url = get_env_value("OPENAI_BASE_URL") or ""
|
|
720
|
+
current_key = get_env_value("OPENAI_API_KEY") or ""
|
|
721
|
+
|
|
722
|
+
print("Custom OpenAI-compatible endpoint configuration:")
|
|
723
|
+
if current_url:
|
|
724
|
+
print(f" Current URL: {current_url}")
|
|
725
|
+
if current_key:
|
|
726
|
+
print(f" Current key: {current_key[:8]}...")
|
|
727
|
+
print()
|
|
728
|
+
|
|
729
|
+
try:
|
|
730
|
+
base_url = input(
|
|
731
|
+
f"API base URL [{current_url or 'e.g. https://api.example.com/v1'}]: "
|
|
732
|
+
).strip()
|
|
733
|
+
api_key = masked_secret_prompt(
|
|
734
|
+
f"API key [{current_key[:8] + '...' if current_key else 'optional'}]: "
|
|
735
|
+
).strip()
|
|
736
|
+
except (KeyboardInterrupt, EOFError):
|
|
737
|
+
print("\nCancelled.")
|
|
738
|
+
return
|
|
739
|
+
|
|
740
|
+
if not base_url and not current_url:
|
|
741
|
+
print("No URL provided. Cancelled.")
|
|
742
|
+
return
|
|
743
|
+
|
|
744
|
+
# Validate URL format
|
|
745
|
+
effective_url = base_url or current_url
|
|
746
|
+
if not effective_url.startswith(("http://", "https://")):
|
|
747
|
+
print(f"Invalid URL: {effective_url} (must start with http:// or https://)")
|
|
748
|
+
return
|
|
749
|
+
|
|
750
|
+
effective_key = api_key or current_key
|
|
751
|
+
|
|
752
|
+
# Hint: most local model servers (Ollama, vLLM, llama.cpp) require /v1
|
|
753
|
+
# in the base URL for OpenAI-compatible chat completions. Prompt the
|
|
754
|
+
# user if the URL looks like a local server without /v1.
|
|
755
|
+
_url_lower = effective_url.rstrip("/").lower()
|
|
756
|
+
_looks_local = any(
|
|
757
|
+
h in _url_lower
|
|
758
|
+
for h in ("localhost", "127.0.0.1", "0.0.0.0", ":11434", ":8080", ":5000")
|
|
759
|
+
)
|
|
760
|
+
if _looks_local and not _url_lower.endswith("/v1"):
|
|
761
|
+
print()
|
|
762
|
+
print(f" Hint: Did you mean to add /v1 at the end?")
|
|
763
|
+
print(f" Most local model servers (Ollama, vLLM, llama.cpp) require it.")
|
|
764
|
+
print(f" e.g. {effective_url.rstrip('/')}/v1")
|
|
765
|
+
try:
|
|
766
|
+
_add_v1 = input(" Add /v1? [Y/n]: ").strip().lower()
|
|
767
|
+
except (KeyboardInterrupt, EOFError):
|
|
768
|
+
_add_v1 = "n"
|
|
769
|
+
if _add_v1 in {"", "y", "yes"}:
|
|
770
|
+
effective_url = effective_url.rstrip("/") + "/v1"
|
|
771
|
+
if base_url:
|
|
772
|
+
base_url = effective_url
|
|
773
|
+
print(f" Updated URL: {effective_url}")
|
|
774
|
+
print()
|
|
775
|
+
|
|
776
|
+
from hermes_cli.models import probe_api_models
|
|
777
|
+
|
|
778
|
+
probe = probe_api_models(effective_key, effective_url)
|
|
779
|
+
if probe.get("used_fallback") and probe.get("resolved_base_url"):
|
|
780
|
+
print(
|
|
781
|
+
f"Warning: endpoint verification worked at {probe['resolved_base_url']}/models, "
|
|
782
|
+
f"not the exact URL you entered. Saving the working base URL instead."
|
|
783
|
+
)
|
|
784
|
+
effective_url = probe["resolved_base_url"]
|
|
785
|
+
if base_url:
|
|
786
|
+
base_url = effective_url
|
|
787
|
+
elif probe.get("models") is not None:
|
|
788
|
+
print(
|
|
789
|
+
f"Verified endpoint via {probe.get('probed_url')} "
|
|
790
|
+
f"({len(probe.get('models') or [])} model(s) visible)"
|
|
791
|
+
)
|
|
792
|
+
else:
|
|
793
|
+
print(
|
|
794
|
+
f"Warning: could not verify this endpoint via {probe.get('probed_url')}. "
|
|
795
|
+
f"Hermes will still save it."
|
|
796
|
+
)
|
|
797
|
+
if probe.get("suggested_base_url"):
|
|
798
|
+
suggested = probe["suggested_base_url"]
|
|
799
|
+
if suggested.endswith("/v1"):
|
|
800
|
+
print(
|
|
801
|
+
f" If this server expects /v1 in the path, try base URL: {suggested}"
|
|
802
|
+
)
|
|
803
|
+
else:
|
|
804
|
+
print(f" If /v1 should not be in the base URL, try: {suggested}")
|
|
805
|
+
|
|
806
|
+
# Prompt for API compatibility mode explicitly so codex-compatible custom
|
|
807
|
+
# providers don't silently fall back to chat_completions.
|
|
808
|
+
current_model_cfg = config.get("model")
|
|
809
|
+
current_api_mode = ""
|
|
810
|
+
if isinstance(current_model_cfg, dict):
|
|
811
|
+
current_api_mode = str(current_model_cfg.get("api_mode") or "").strip()
|
|
812
|
+
api_mode = _prompt_custom_api_mode_selection(
|
|
813
|
+
effective_url,
|
|
814
|
+
current_api_mode=current_api_mode,
|
|
815
|
+
)
|
|
816
|
+
if api_mode:
|
|
817
|
+
print(f" API mode: {api_mode}")
|
|
818
|
+
else:
|
|
819
|
+
print(" API mode: auto-detect")
|
|
820
|
+
|
|
821
|
+
# Select model — use probe results when available, fall back to manual input
|
|
822
|
+
model_name = ""
|
|
823
|
+
detected_models = probe.get("models") or []
|
|
824
|
+
try:
|
|
825
|
+
if len(detected_models) == 1:
|
|
826
|
+
print(f" Detected model: {detected_models[0]}")
|
|
827
|
+
confirm = input(" Use this model? [Y/n]: ").strip().lower()
|
|
828
|
+
if confirm in {"", "y", "yes"}:
|
|
829
|
+
model_name = detected_models[0]
|
|
830
|
+
else:
|
|
831
|
+
model_name = input("Model name (e.g. gpt-4, llama-3-70b): ").strip()
|
|
832
|
+
elif len(detected_models) > 1:
|
|
833
|
+
print(" Available models:")
|
|
834
|
+
for i, m in enumerate(detected_models, 1):
|
|
835
|
+
print(f" {i}. {m}")
|
|
836
|
+
pick = input(
|
|
837
|
+
f" Select model [1-{len(detected_models)}] or type name: "
|
|
838
|
+
).strip()
|
|
839
|
+
if pick.isdigit() and 1 <= int(pick) <= len(detected_models):
|
|
840
|
+
model_name = detected_models[int(pick) - 1]
|
|
841
|
+
elif pick:
|
|
842
|
+
model_name = pick
|
|
843
|
+
else:
|
|
844
|
+
model_name = input("Model name (e.g. gpt-4, llama-3-70b): ").strip()
|
|
845
|
+
|
|
846
|
+
context_length_str = input(
|
|
847
|
+
"Context length in tokens [leave blank for auto-detect]: "
|
|
848
|
+
).strip()
|
|
849
|
+
|
|
850
|
+
# Prompt for a display name — shown in the provider menu on future runs
|
|
851
|
+
default_name = _auto_provider_name(effective_url)
|
|
852
|
+
display_name = input(f"Display name [{default_name}]: ").strip() or default_name
|
|
853
|
+
except (KeyboardInterrupt, EOFError):
|
|
854
|
+
print("\nCancelled.")
|
|
855
|
+
return
|
|
856
|
+
|
|
857
|
+
context_length = None
|
|
858
|
+
if context_length_str:
|
|
859
|
+
try:
|
|
860
|
+
context_length = int(
|
|
861
|
+
context_length_str.replace(",", "")
|
|
862
|
+
.replace("k", "000")
|
|
863
|
+
.replace("K", "000")
|
|
864
|
+
)
|
|
865
|
+
if context_length <= 0:
|
|
866
|
+
context_length = None
|
|
867
|
+
except ValueError:
|
|
868
|
+
print(f"Invalid context length: {context_length_str} — will auto-detect.")
|
|
869
|
+
context_length = None
|
|
870
|
+
|
|
871
|
+
if model_name:
|
|
872
|
+
_save_model_choice(model_name)
|
|
873
|
+
|
|
874
|
+
# Update config and deactivate any OAuth provider
|
|
875
|
+
cfg = load_config()
|
|
876
|
+
model = cfg.get("model")
|
|
877
|
+
if not isinstance(model, dict):
|
|
878
|
+
model = {"default": model} if model else {}
|
|
879
|
+
cfg["model"] = model
|
|
880
|
+
model["provider"] = "custom"
|
|
881
|
+
model["base_url"] = effective_url
|
|
882
|
+
if effective_key:
|
|
883
|
+
model["api_key"] = effective_key
|
|
884
|
+
if api_mode:
|
|
885
|
+
model["api_mode"] = api_mode
|
|
886
|
+
else:
|
|
887
|
+
model.pop("api_mode", None)
|
|
888
|
+
save_config(cfg)
|
|
889
|
+
deactivate_provider()
|
|
890
|
+
|
|
891
|
+
# Sync the caller's config dict so the setup wizard's final
|
|
892
|
+
# save_config(config) preserves our model settings. Without
|
|
893
|
+
# this, the wizard overwrites model.provider/base_url with
|
|
894
|
+
# the stale values from its own config dict (#4172).
|
|
895
|
+
config["model"] = dict(model)
|
|
896
|
+
|
|
897
|
+
print(f"Default model set to: {model_name} (via {effective_url})")
|
|
898
|
+
else:
|
|
899
|
+
if base_url or api_key:
|
|
900
|
+
deactivate_provider()
|
|
901
|
+
# Even without a model name, persist the custom endpoint on the
|
|
902
|
+
# caller's config dict so the setup wizard doesn't lose it.
|
|
903
|
+
_caller_model = config.get("model")
|
|
904
|
+
if not isinstance(_caller_model, dict):
|
|
905
|
+
_caller_model = {"default": _caller_model} if _caller_model else {}
|
|
906
|
+
_caller_model["provider"] = "custom"
|
|
907
|
+
_caller_model["base_url"] = effective_url
|
|
908
|
+
if effective_key:
|
|
909
|
+
_caller_model["api_key"] = effective_key
|
|
910
|
+
if api_mode:
|
|
911
|
+
_caller_model["api_mode"] = api_mode
|
|
912
|
+
else:
|
|
913
|
+
_caller_model.pop("api_mode", None)
|
|
914
|
+
config["model"] = _caller_model
|
|
915
|
+
print("Endpoint saved. Use `/model` in chat or `hermes model` to set a model.")
|
|
916
|
+
|
|
917
|
+
# Auto-save to custom_providers so it appears in the menu next time
|
|
918
|
+
_save_custom_provider(
|
|
919
|
+
effective_url,
|
|
920
|
+
effective_key,
|
|
921
|
+
model_name or "",
|
|
922
|
+
context_length=context_length,
|
|
923
|
+
name=display_name,
|
|
924
|
+
api_mode=api_mode,
|
|
925
|
+
)
|
|
926
|
+
|
|
927
|
+
def _model_flow_azure_foundry(config, current_model=""):
|
|
928
|
+
"""Azure Foundry provider: configure endpoint, auth mode, API mode, and model.
|
|
929
|
+
|
|
930
|
+
Azure Foundry supports both OpenAI-style (``/v1/chat/completions``) and
|
|
931
|
+
Anthropic-style (``/v1/messages``) endpoints, and two authentication
|
|
932
|
+
modes:
|
|
933
|
+
|
|
934
|
+
* **API key** (default) — uses ``AZURE_FOUNDRY_API_KEY`` from .env.
|
|
935
|
+
* **Microsoft Entra ID** — keyless, RBAC-based auth via the
|
|
936
|
+
``azure-identity`` SDK (Managed Identity / Workload Identity / az
|
|
937
|
+
login / VS Code / azd / service principal env vars). Works on both
|
|
938
|
+
OpenAI-style and Anthropic-style endpoints — Microsoft RBAC is
|
|
939
|
+
per-resource and the same ``Azure AI User`` role grants
|
|
940
|
+
both. For OpenAI-style the OpenAI SDK's native callable
|
|
941
|
+
``api_key=`` contract is used; for Anthropic-style an
|
|
942
|
+
``httpx.Client`` with a request event hook (built by
|
|
943
|
+
:func:`agent.azure_identity_adapter.build_bearer_http_client`)
|
|
944
|
+
mints a fresh JWT per request because the Anthropic SDK does not
|
|
945
|
+
accept a callable ``auth_token`` natively.
|
|
946
|
+
|
|
947
|
+
The wizard auto-detects the transport and available models when
|
|
948
|
+
possible:
|
|
949
|
+
|
|
950
|
+
* URLs ending in ``/anthropic`` → Anthropic Messages API.
|
|
951
|
+
* Successful ``GET <base>/models`` probe → OpenAI-style + populates
|
|
952
|
+
a picker with the returned deployment / model IDs.
|
|
953
|
+
* Anthropic Messages probe fallback when ``/models`` fails.
|
|
954
|
+
* Manual entry when every probe fails (private endpoints, etc.).
|
|
955
|
+
|
|
956
|
+
Context lengths for the chosen model are resolved via the standard
|
|
957
|
+
:func:`agent.model_metadata.get_model_context_length` chain
|
|
958
|
+
(models.dev, provider metadata, hardcoded family fallbacks).
|
|
959
|
+
"""
|
|
960
|
+
from hermes_cli.auth import _save_model_choice, deactivate_provider # noqa: F401
|
|
961
|
+
from hermes_cli.config import (
|
|
962
|
+
get_env_value,
|
|
963
|
+
save_env_value,
|
|
964
|
+
load_config,
|
|
965
|
+
save_config,
|
|
966
|
+
)
|
|
967
|
+
from hermes_cli import azure_detect
|
|
968
|
+
|
|
969
|
+
# ── Load current Azure Foundry configuration ─────────────────────
|
|
970
|
+
model_cfg = config.get("model", {})
|
|
971
|
+
if isinstance(model_cfg, dict) and model_cfg.get("provider") == "azure-foundry":
|
|
972
|
+
current_base_url = str(model_cfg.get("base_url", "") or "")
|
|
973
|
+
current_api_mode = str(model_cfg.get("api_mode", "") or "")
|
|
974
|
+
current_auth_mode = str(model_cfg.get("auth_mode") or "api_key").strip().lower() or "api_key"
|
|
975
|
+
_cur_entra = model_cfg.get("entra") or {}
|
|
976
|
+
current_entra = _cur_entra if isinstance(_cur_entra, dict) else {}
|
|
977
|
+
else:
|
|
978
|
+
current_base_url = ""
|
|
979
|
+
current_api_mode = ""
|
|
980
|
+
current_auth_mode = "api_key"
|
|
981
|
+
current_entra = {}
|
|
982
|
+
|
|
983
|
+
current_api_key = get_env_value("AZURE_FOUNDRY_API_KEY") or ""
|
|
984
|
+
|
|
985
|
+
print()
|
|
986
|
+
print("Azure Foundry Configuration")
|
|
987
|
+
print("=" * 50)
|
|
988
|
+
print()
|
|
989
|
+
print("Azure Foundry can host models with either OpenAI-style or")
|
|
990
|
+
print("Anthropic-style API endpoints. Hermes will probe your")
|
|
991
|
+
print("endpoint to auto-detect the transport and the deployed")
|
|
992
|
+
print("models when possible.")
|
|
993
|
+
print()
|
|
994
|
+
|
|
995
|
+
if current_base_url:
|
|
996
|
+
print(f" Current endpoint: {current_base_url}")
|
|
997
|
+
if current_api_mode:
|
|
998
|
+
_lbl = (
|
|
999
|
+
"OpenAI-style"
|
|
1000
|
+
if current_api_mode == "chat_completions"
|
|
1001
|
+
else "Anthropic-style"
|
|
1002
|
+
)
|
|
1003
|
+
print(f" Current API mode: {_lbl}")
|
|
1004
|
+
if current_auth_mode == "entra_id":
|
|
1005
|
+
print(f" Current auth mode: Microsoft Entra ID (keyless)")
|
|
1006
|
+
elif current_api_key:
|
|
1007
|
+
print(f" Current auth mode: API key ({current_api_key[:8]}...)")
|
|
1008
|
+
print()
|
|
1009
|
+
|
|
1010
|
+
# ── Step 1: endpoint URL ─────────────────────────────────────────
|
|
1011
|
+
try:
|
|
1012
|
+
_placeholder = (
|
|
1013
|
+
current_base_url
|
|
1014
|
+
or "e.g. https://<resource>.openai.azure.com/openai/v1 "
|
|
1015
|
+
"or https://<resource>.services.ai.azure.com/anthropic"
|
|
1016
|
+
)
|
|
1017
|
+
base_url = input(
|
|
1018
|
+
f"API endpoint URL [{_placeholder}]: "
|
|
1019
|
+
).strip()
|
|
1020
|
+
except (KeyboardInterrupt, EOFError):
|
|
1021
|
+
print("\nCancelled.")
|
|
1022
|
+
return
|
|
1023
|
+
|
|
1024
|
+
effective_url = (base_url or current_base_url).rstrip("/")
|
|
1025
|
+
if not effective_url:
|
|
1026
|
+
print("No endpoint URL provided. Cancelled.")
|
|
1027
|
+
return
|
|
1028
|
+
if not effective_url.startswith(("http://", "https://")):
|
|
1029
|
+
print(f"Invalid URL: {effective_url} (must start with http:// or https://)")
|
|
1030
|
+
return
|
|
1031
|
+
|
|
1032
|
+
# ── Step 2: authentication mode ──────────────────────────────────
|
|
1033
|
+
print()
|
|
1034
|
+
print("Authentication:")
|
|
1035
|
+
print(" 1. API key (AZURE_FOUNDRY_API_KEY in .env)")
|
|
1036
|
+
print(" 2. Microsoft Entra ID (managed identity / workload identity / az login)")
|
|
1037
|
+
print(" Recommended by Microsoft. Works for both OpenAI-style and Anthropic-style endpoints.")
|
|
1038
|
+
print(" Requires the 'Azure AI User' role on the Foundry resource.")
|
|
1039
|
+
try:
|
|
1040
|
+
_auth_default = "2" if current_auth_mode == "entra_id" else "1"
|
|
1041
|
+
auth_choice = (
|
|
1042
|
+
input(f"Authentication mode [1/2] ({_auth_default}): ").strip()
|
|
1043
|
+
or _auth_default
|
|
1044
|
+
)
|
|
1045
|
+
except (KeyboardInterrupt, EOFError):
|
|
1046
|
+
print("\nCancelled.")
|
|
1047
|
+
return
|
|
1048
|
+
use_entra = auth_choice == "2"
|
|
1049
|
+
auth_mode_label = "entra_id" if use_entra else "api_key"
|
|
1050
|
+
|
|
1051
|
+
# ── Step 3: credentials (key OR Entra preflight) ─────────────────
|
|
1052
|
+
effective_key: str = ""
|
|
1053
|
+
entra_overrides: dict = {}
|
|
1054
|
+
token_provider = None # callable when entra
|
|
1055
|
+
entra_scope = ""
|
|
1056
|
+
|
|
1057
|
+
if use_entra:
|
|
1058
|
+
try:
|
|
1059
|
+
from agent.azure_identity_adapter import (
|
|
1060
|
+
EntraIdentityConfig,
|
|
1061
|
+
SCOPE_AI_AZURE_DEFAULT,
|
|
1062
|
+
build_token_provider,
|
|
1063
|
+
describe_active_credential,
|
|
1064
|
+
has_azure_identity_installed,
|
|
1065
|
+
)
|
|
1066
|
+
except ImportError as exc:
|
|
1067
|
+
print()
|
|
1068
|
+
print(f"⚠ Could not import azure-identity adapter: {exc}")
|
|
1069
|
+
print(" Falling back to API key auth.")
|
|
1070
|
+
use_entra = False
|
|
1071
|
+
auth_mode_label = "api_key"
|
|
1072
|
+
|
|
1073
|
+
if use_entra:
|
|
1074
|
+
print()
|
|
1075
|
+
if not has_azure_identity_installed():
|
|
1076
|
+
print("◐ The 'azure-identity' package is not installed yet.")
|
|
1077
|
+
print(
|
|
1078
|
+
" Hermes will install it now (the preflight below "
|
|
1079
|
+
"triggers the lazy-install). To skip lazy installs, "
|
|
1080
|
+
"run: pip install azure-identity"
|
|
1081
|
+
)
|
|
1082
|
+
|
|
1083
|
+
# Preserve only the optional scope override. Identity selection
|
|
1084
|
+
# (tenant, user-assigned MI, workload identity, service principal)
|
|
1085
|
+
# stays in Azure SDK env vars such as AZURE_CLIENT_ID.
|
|
1086
|
+
_persisted_scope_override = str(current_entra.get("scope") or "").strip()
|
|
1087
|
+
entra_scope = _persisted_scope_override or SCOPE_AI_AZURE_DEFAULT
|
|
1088
|
+
|
|
1089
|
+
entra_overrides = {}
|
|
1090
|
+
if _persisted_scope_override:
|
|
1091
|
+
entra_overrides["scope"] = _persisted_scope_override
|
|
1092
|
+
|
|
1093
|
+
print()
|
|
1094
|
+
print("◐ Probing Microsoft Entra ID credential chain (up to 10s)...")
|
|
1095
|
+
_config = EntraIdentityConfig(
|
|
1096
|
+
scope=entra_scope,
|
|
1097
|
+
)
|
|
1098
|
+
info = describe_active_credential(config=_config, timeout_seconds=10.0)
|
|
1099
|
+
if info.get("ok"):
|
|
1100
|
+
env_sources = info.get("env_sources") or []
|
|
1101
|
+
tag = ", ".join(env_sources) if env_sources else "default chain"
|
|
1102
|
+
print(f"✓ Entra ID token acquired ({tag}, scope={entra_scope})")
|
|
1103
|
+
else:
|
|
1104
|
+
err = info.get("error") or "credential chain exhausted"
|
|
1105
|
+
hint = info.get("hint") or (
|
|
1106
|
+
"Run `az login`, attach a managed identity to this VM, or "
|
|
1107
|
+
"set AZURE_TENANT_ID/AZURE_CLIENT_ID/AZURE_CLIENT_SECRET."
|
|
1108
|
+
)
|
|
1109
|
+
print(f"⚠ {err}")
|
|
1110
|
+
print(f" Hint: {hint}")
|
|
1111
|
+
try:
|
|
1112
|
+
ans = input("Save Entra config anyway and validate later? [Y/n]: ").strip().lower()
|
|
1113
|
+
except (KeyboardInterrupt, EOFError):
|
|
1114
|
+
print("\nCancelled.")
|
|
1115
|
+
return
|
|
1116
|
+
if ans and ans not in ("y", "yes"):
|
|
1117
|
+
print("Cancelled.")
|
|
1118
|
+
return
|
|
1119
|
+
|
|
1120
|
+
# Build the token provider for the detection probe (best-effort —
|
|
1121
|
+
# if the credential chain failed above, this will silently return
|
|
1122
|
+
# None inside azure_detect and the probe falls back to manual).
|
|
1123
|
+
try:
|
|
1124
|
+
token_provider = build_token_provider(config=_config)
|
|
1125
|
+
except Exception as exc:
|
|
1126
|
+
print(f"⚠ Could not build token provider for probing: {exc}")
|
|
1127
|
+
token_provider = None
|
|
1128
|
+
else:
|
|
1129
|
+
print()
|
|
1130
|
+
from hermes_cli.secret_prompt import masked_secret_prompt
|
|
1131
|
+
|
|
1132
|
+
try:
|
|
1133
|
+
api_key = masked_secret_prompt(
|
|
1134
|
+
f"API key [{current_api_key[:8] + '...' if current_api_key else 'required'}]: "
|
|
1135
|
+
).strip()
|
|
1136
|
+
except (KeyboardInterrupt, EOFError):
|
|
1137
|
+
print("\nCancelled.")
|
|
1138
|
+
return
|
|
1139
|
+
|
|
1140
|
+
effective_key = api_key or current_api_key
|
|
1141
|
+
if not effective_key:
|
|
1142
|
+
print("No API key provided. Cancelled.")
|
|
1143
|
+
return
|
|
1144
|
+
|
|
1145
|
+
# ── Step 4: auto-detect transport + models ───────────────────────
|
|
1146
|
+
print()
|
|
1147
|
+
print("◐ Probing endpoint to auto-detect transport and models...")
|
|
1148
|
+
detection = azure_detect.detect(
|
|
1149
|
+
effective_url,
|
|
1150
|
+
api_key=effective_key,
|
|
1151
|
+
token_provider=token_provider,
|
|
1152
|
+
)
|
|
1153
|
+
|
|
1154
|
+
discovered_models: list[str] = list(detection.models)
|
|
1155
|
+
api_mode: str = detection.api_mode or ""
|
|
1156
|
+
|
|
1157
|
+
if api_mode:
|
|
1158
|
+
mode_label = (
|
|
1159
|
+
"OpenAI-style" if api_mode == "chat_completions" else "Anthropic-style"
|
|
1160
|
+
)
|
|
1161
|
+
print(f"✓ Detected API transport: {mode_label}")
|
|
1162
|
+
if detection.reason:
|
|
1163
|
+
print(f" ({detection.reason})")
|
|
1164
|
+
if discovered_models:
|
|
1165
|
+
print(
|
|
1166
|
+
f"✓ Found {len(discovered_models)} deployed model(s) on this endpoint"
|
|
1167
|
+
)
|
|
1168
|
+
else:
|
|
1169
|
+
print(f"⚠ Auto-detection incomplete: {detection.reason}")
|
|
1170
|
+
print()
|
|
1171
|
+
print("Select the API format your Azure Foundry endpoint uses:")
|
|
1172
|
+
print(" 1. OpenAI-style (POST /v1/chat/completions)")
|
|
1173
|
+
print(" For: GPT models, Llama, Mistral, and most open models")
|
|
1174
|
+
print(" 2. Anthropic-style (POST /v1/messages)")
|
|
1175
|
+
print(" For: Claude models deployed via Anthropic API format")
|
|
1176
|
+
try:
|
|
1177
|
+
default_choice = "2" if current_api_mode == "anthropic_messages" else "1"
|
|
1178
|
+
mode_choice = (
|
|
1179
|
+
input(f"API format [1/2] ({default_choice}): ").strip()
|
|
1180
|
+
or default_choice
|
|
1181
|
+
)
|
|
1182
|
+
except (KeyboardInterrupt, EOFError):
|
|
1183
|
+
print("\nCancelled.")
|
|
1184
|
+
return
|
|
1185
|
+
api_mode = "anthropic_messages" if mode_choice == "2" else "chat_completions"
|
|
1186
|
+
|
|
1187
|
+
# ── Step 5: model name ───────────────────────────────────────────
|
|
1188
|
+
print()
|
|
1189
|
+
effective_model = ""
|
|
1190
|
+
if discovered_models:
|
|
1191
|
+
print("Available models on this endpoint:")
|
|
1192
|
+
for i, mid in enumerate(discovered_models[:30], start=1):
|
|
1193
|
+
print(f" {i:>2}. {mid}")
|
|
1194
|
+
if len(discovered_models) > 30:
|
|
1195
|
+
print(
|
|
1196
|
+
f" ... and {len(discovered_models) - 30} more (type name manually if not shown)"
|
|
1197
|
+
)
|
|
1198
|
+
print()
|
|
1199
|
+
try:
|
|
1200
|
+
pick = input(
|
|
1201
|
+
f"Pick by number, or type a deployment name [{current_model or discovered_models[0]}]: "
|
|
1202
|
+
).strip()
|
|
1203
|
+
except (KeyboardInterrupt, EOFError):
|
|
1204
|
+
print("\nCancelled.")
|
|
1205
|
+
return
|
|
1206
|
+
if not pick:
|
|
1207
|
+
effective_model = current_model or discovered_models[0]
|
|
1208
|
+
elif pick.isdigit() and 1 <= int(pick) <= min(len(discovered_models), 30):
|
|
1209
|
+
effective_model = discovered_models[int(pick) - 1]
|
|
1210
|
+
else:
|
|
1211
|
+
effective_model = pick
|
|
1212
|
+
else:
|
|
1213
|
+
try:
|
|
1214
|
+
model_name = input(
|
|
1215
|
+
f"Model / deployment name [{current_model or 'e.g. gpt-5.4, claude-sonnet-4-6'}]: "
|
|
1216
|
+
).strip()
|
|
1217
|
+
except (KeyboardInterrupt, EOFError):
|
|
1218
|
+
print("\nCancelled.")
|
|
1219
|
+
return
|
|
1220
|
+
effective_model = model_name or current_model
|
|
1221
|
+
|
|
1222
|
+
if not effective_model:
|
|
1223
|
+
print("No model name provided. Cancelled.")
|
|
1224
|
+
return
|
|
1225
|
+
|
|
1226
|
+
# ── Step 6: context-length lookup ────────────────────────────────
|
|
1227
|
+
ctx_len = azure_detect.lookup_context_length(
|
|
1228
|
+
effective_model,
|
|
1229
|
+
effective_url,
|
|
1230
|
+
api_key=effective_key,
|
|
1231
|
+
token_provider=token_provider,
|
|
1232
|
+
)
|
|
1233
|
+
|
|
1234
|
+
# ── Step 7: persist ──────────────────────────────────────────────
|
|
1235
|
+
if not use_entra:
|
|
1236
|
+
save_env_value("AZURE_FOUNDRY_API_KEY", effective_key)
|
|
1237
|
+
|
|
1238
|
+
cfg = load_config()
|
|
1239
|
+
model = cfg.get("model")
|
|
1240
|
+
if not isinstance(model, dict):
|
|
1241
|
+
model = {"default": model} if model else {}
|
|
1242
|
+
cfg["model"] = model
|
|
1243
|
+
|
|
1244
|
+
model["provider"] = "azure-foundry"
|
|
1245
|
+
model["base_url"] = effective_url
|
|
1246
|
+
model["api_mode"] = api_mode
|
|
1247
|
+
model["default"] = effective_model
|
|
1248
|
+
model["auth_mode"] = auth_mode_label
|
|
1249
|
+
if use_entra:
|
|
1250
|
+
# Persist only the non-default Entra scope so config.yaml stays tidy.
|
|
1251
|
+
# Azure identity selection stays in standard AZURE_* env vars.
|
|
1252
|
+
clean_entra: dict = {}
|
|
1253
|
+
for key in ("scope",):
|
|
1254
|
+
val = entra_overrides.get(key)
|
|
1255
|
+
if val:
|
|
1256
|
+
clean_entra[key] = val
|
|
1257
|
+
if clean_entra:
|
|
1258
|
+
model["entra"] = clean_entra
|
|
1259
|
+
elif "entra" in model:
|
|
1260
|
+
del model["entra"]
|
|
1261
|
+
else:
|
|
1262
|
+
if "entra" in model:
|
|
1263
|
+
del model["entra"]
|
|
1264
|
+
if ctx_len:
|
|
1265
|
+
model["context_length"] = ctx_len
|
|
1266
|
+
|
|
1267
|
+
save_config(cfg)
|
|
1268
|
+
deactivate_provider()
|
|
1269
|
+
config["model"] = dict(model)
|
|
1270
|
+
|
|
1271
|
+
# Clear any conflicting env vars so auxiliary clients don't poison
|
|
1272
|
+
# themselves with a stale OpenAI base URL / key.
|
|
1273
|
+
if get_env_value("OPENAI_BASE_URL"):
|
|
1274
|
+
save_env_value("OPENAI_BASE_URL", "")
|
|
1275
|
+
if get_env_value("OPENAI_API_KEY"):
|
|
1276
|
+
save_env_value("OPENAI_API_KEY", "")
|
|
1277
|
+
|
|
1278
|
+
mode_label = "OpenAI-style" if api_mode == "chat_completions" else "Anthropic-style"
|
|
1279
|
+
auth_label = (
|
|
1280
|
+
"Microsoft Entra ID (keyless)" if use_entra else "API key"
|
|
1281
|
+
)
|
|
1282
|
+
print()
|
|
1283
|
+
print("✓ Azure Foundry configured:")
|
|
1284
|
+
print(f" Endpoint: {effective_url}")
|
|
1285
|
+
print(f" API mode: {mode_label}")
|
|
1286
|
+
print(f" Auth: {auth_label}")
|
|
1287
|
+
print(f" Model: {effective_model}")
|
|
1288
|
+
if ctx_len:
|
|
1289
|
+
print(f" Context length: {ctx_len:,} tokens")
|
|
1290
|
+
else:
|
|
1291
|
+
print(" Context length: not auto-detected (will fall back at runtime)")
|
|
1292
|
+
print()
|
|
1293
|
+
|
|
1294
|
+
def _model_flow_named_custom(config, provider_info):
|
|
1295
|
+
"""Handle a named custom provider from config.yaml custom_providers list.
|
|
1296
|
+
|
|
1297
|
+
Always probes the endpoint's /models API to let the user pick a model.
|
|
1298
|
+
If a model was previously saved, it is pre-selected in the menu.
|
|
1299
|
+
Falls back to the saved model if probing fails.
|
|
1300
|
+
"""
|
|
1301
|
+
from hermes_cli.main import _custom_provider_api_key_config_value, _custom_provider_base_url_config_value, _save_custom_provider
|
|
1302
|
+
from hermes_cli.auth import _save_model_choice, deactivate_provider
|
|
1303
|
+
from hermes_cli.config import load_config, save_config
|
|
1304
|
+
from hermes_cli.models import fetch_api_models
|
|
1305
|
+
|
|
1306
|
+
name = provider_info["name"]
|
|
1307
|
+
base_url = provider_info["base_url"]
|
|
1308
|
+
api_mode = provider_info.get("api_mode", "")
|
|
1309
|
+
api_key = provider_info.get("api_key", "")
|
|
1310
|
+
key_env = provider_info.get("key_env", "")
|
|
1311
|
+
saved_model = provider_info.get("model", "")
|
|
1312
|
+
provider_key = (provider_info.get("provider_key") or "").strip()
|
|
1313
|
+
|
|
1314
|
+
# Resolve key from env var if api_key not set directly
|
|
1315
|
+
if not api_key and key_env:
|
|
1316
|
+
api_key = os.environ.get(key_env, "")
|
|
1317
|
+
config_api_key = _custom_provider_api_key_config_value(provider_info, api_key)
|
|
1318
|
+
|
|
1319
|
+
# Honor ``discover_models: false`` (default True) — when discovery is
|
|
1320
|
+
# disabled, use the configured ``models:`` list verbatim and skip the
|
|
1321
|
+
# live /models probe. This lets operators restrict the picker to the
|
|
1322
|
+
# subset their plan actually serves instead of the endpoint's full
|
|
1323
|
+
# catalog (#18726: Baidu Qianfan returns 100+ models for a 2-3 model
|
|
1324
|
+
# plan). Same semantics as the slash-command picker (model_switch.py
|
|
1325
|
+
# sections 3 & 4): default discovers, false keeps the explicit list.
|
|
1326
|
+
discover = provider_info.get("discover_models", True)
|
|
1327
|
+
if isinstance(discover, str):
|
|
1328
|
+
discover = discover.lower() not in {"false", "no", "0"}
|
|
1329
|
+
configured_models: list[str] = []
|
|
1330
|
+
cfg_models = provider_info.get("models", {})
|
|
1331
|
+
if isinstance(cfg_models, dict):
|
|
1332
|
+
configured_models = [str(m) for m in cfg_models if str(m).strip()]
|
|
1333
|
+
elif isinstance(cfg_models, list):
|
|
1334
|
+
configured_models = [
|
|
1335
|
+
str(m) for m in cfg_models if isinstance(m, str) and m.strip()
|
|
1336
|
+
]
|
|
1337
|
+
|
|
1338
|
+
print(f" Provider: {name}")
|
|
1339
|
+
print(f" URL: {base_url}")
|
|
1340
|
+
if saved_model:
|
|
1341
|
+
print(f" Current: {saved_model}")
|
|
1342
|
+
print()
|
|
1343
|
+
|
|
1344
|
+
if not discover and configured_models:
|
|
1345
|
+
# Discovery disabled with an explicit list — use it verbatim, no probe.
|
|
1346
|
+
print(f"Using configured models (discover_models: false): {len(configured_models)}")
|
|
1347
|
+
models = configured_models
|
|
1348
|
+
else:
|
|
1349
|
+
print("Fetching available models...")
|
|
1350
|
+
fetch_kwargs = {"timeout": 8.0}
|
|
1351
|
+
if api_mode:
|
|
1352
|
+
fetch_kwargs["api_mode"] = api_mode
|
|
1353
|
+
models = fetch_api_models(api_key, base_url, **fetch_kwargs)
|
|
1354
|
+
# If the probe came back empty but the operator configured an explicit
|
|
1355
|
+
# list, fall back to it rather than forcing manual entry.
|
|
1356
|
+
if not models and configured_models:
|
|
1357
|
+
models = configured_models
|
|
1358
|
+
|
|
1359
|
+
if models:
|
|
1360
|
+
default_idx = 0
|
|
1361
|
+
if saved_model and saved_model in models:
|
|
1362
|
+
default_idx = models.index(saved_model)
|
|
1363
|
+
|
|
1364
|
+
print(f"Found {len(models)} model(s):\n")
|
|
1365
|
+
try:
|
|
1366
|
+
from hermes_cli.curses_ui import curses_radiolist
|
|
1367
|
+
|
|
1368
|
+
menu_items = [
|
|
1369
|
+
f"{m} (current)" if m == saved_model else m for m in models
|
|
1370
|
+
] + ["Cancel"]
|
|
1371
|
+
idx = curses_radiolist(
|
|
1372
|
+
f"Select model from {name}:",
|
|
1373
|
+
menu_items,
|
|
1374
|
+
selected=default_idx,
|
|
1375
|
+
cancel_returns=-1,
|
|
1376
|
+
searchable=True,
|
|
1377
|
+
)
|
|
1378
|
+
print()
|
|
1379
|
+
if idx < 0 or idx >= len(models):
|
|
1380
|
+
print("Cancelled.")
|
|
1381
|
+
return
|
|
1382
|
+
model_name = models[idx]
|
|
1383
|
+
except (ImportError, NotImplementedError, OSError, subprocess.SubprocessError):
|
|
1384
|
+
for i, m in enumerate(models, 1):
|
|
1385
|
+
suffix = " (current)" if m == saved_model else ""
|
|
1386
|
+
print(f" {i}. {m}{suffix}")
|
|
1387
|
+
print(f" {len(models) + 1}. Cancel")
|
|
1388
|
+
print()
|
|
1389
|
+
try:
|
|
1390
|
+
val = input(f"Choice [1-{len(models) + 1}]: ").strip()
|
|
1391
|
+
if not val:
|
|
1392
|
+
print("Cancelled.")
|
|
1393
|
+
return
|
|
1394
|
+
idx = int(val) - 1
|
|
1395
|
+
if idx < 0 or idx >= len(models):
|
|
1396
|
+
print("Cancelled.")
|
|
1397
|
+
return
|
|
1398
|
+
model_name = models[idx]
|
|
1399
|
+
except (ValueError, KeyboardInterrupt, EOFError):
|
|
1400
|
+
print("\nCancelled.")
|
|
1401
|
+
return
|
|
1402
|
+
elif saved_model:
|
|
1403
|
+
print("Could not fetch models from endpoint.")
|
|
1404
|
+
try:
|
|
1405
|
+
model_name = input(f"Model name [{saved_model}]: ").strip() or saved_model
|
|
1406
|
+
except (KeyboardInterrupt, EOFError):
|
|
1407
|
+
print("\nCancelled.")
|
|
1408
|
+
return
|
|
1409
|
+
else:
|
|
1410
|
+
print("Could not fetch models from endpoint. Enter model name manually.")
|
|
1411
|
+
try:
|
|
1412
|
+
model_name = input("Model name: ").strip()
|
|
1413
|
+
except (KeyboardInterrupt, EOFError):
|
|
1414
|
+
print("\nCancelled.")
|
|
1415
|
+
return
|
|
1416
|
+
if not model_name:
|
|
1417
|
+
print("No model specified. Cancelled.")
|
|
1418
|
+
return
|
|
1419
|
+
|
|
1420
|
+
# Activate and save the model to the custom_providers entry
|
|
1421
|
+
_save_model_choice(model_name)
|
|
1422
|
+
|
|
1423
|
+
cfg = load_config()
|
|
1424
|
+
model = cfg.get("model")
|
|
1425
|
+
if not isinstance(model, dict):
|
|
1426
|
+
model = {"default": model} if model else {}
|
|
1427
|
+
cfg["model"] = model
|
|
1428
|
+
if provider_key:
|
|
1429
|
+
model["provider"] = provider_key
|
|
1430
|
+
model.pop("base_url", None)
|
|
1431
|
+
model.pop("api_key", None)
|
|
1432
|
+
else:
|
|
1433
|
+
model["provider"] = "custom"
|
|
1434
|
+
model["base_url"] = _custom_provider_base_url_config_value(
|
|
1435
|
+
provider_info, base_url
|
|
1436
|
+
)
|
|
1437
|
+
if config_api_key:
|
|
1438
|
+
model["api_key"] = config_api_key
|
|
1439
|
+
# Apply api_mode from custom_providers entry, or clear stale value
|
|
1440
|
+
custom_api_mode = provider_info.get("api_mode", "")
|
|
1441
|
+
if custom_api_mode:
|
|
1442
|
+
model["api_mode"] = custom_api_mode
|
|
1443
|
+
else:
|
|
1444
|
+
model.pop("api_mode", None) # let runtime auto-detect from URL
|
|
1445
|
+
save_config(cfg)
|
|
1446
|
+
deactivate_provider()
|
|
1447
|
+
|
|
1448
|
+
# Persist the selected model back to whichever schema owns this endpoint.
|
|
1449
|
+
if provider_key:
|
|
1450
|
+
cfg = load_config()
|
|
1451
|
+
providers_cfg = cfg.get("providers")
|
|
1452
|
+
if isinstance(providers_cfg, dict):
|
|
1453
|
+
provider_entry = providers_cfg.get(provider_key)
|
|
1454
|
+
if isinstance(provider_entry, dict):
|
|
1455
|
+
provider_entry["default_model"] = model_name
|
|
1456
|
+
# Only persist an inline api_key when the user originally had
|
|
1457
|
+
# one (either a literal secret or a ``${VAR}`` template). When
|
|
1458
|
+
# the entry relies on ``key_env``, do not synthesize a
|
|
1459
|
+
# ``${key_env}`` api_key — the runtime already resolves the
|
|
1460
|
+
# key from ``key_env`` directly, and writing the resolved
|
|
1461
|
+
# secret (or even a synthesized template) would silently
|
|
1462
|
+
# downgrade credential hygiene on entries that intentionally
|
|
1463
|
+
# keep plaintext out of ``config.yaml``. See issue #15803.
|
|
1464
|
+
original_api_key_ref = str(
|
|
1465
|
+
provider_info.get("api_key_ref", "") or ""
|
|
1466
|
+
).strip()
|
|
1467
|
+
original_api_key = str(provider_info.get("api_key", "") or "").strip()
|
|
1468
|
+
had_inline_api_key = bool(original_api_key_ref or original_api_key)
|
|
1469
|
+
if (
|
|
1470
|
+
had_inline_api_key
|
|
1471
|
+
and config_api_key
|
|
1472
|
+
and not str(provider_entry.get("api_key", "") or "").strip()
|
|
1473
|
+
):
|
|
1474
|
+
provider_entry["api_key"] = config_api_key
|
|
1475
|
+
if key_env and not str(provider_entry.get("key_env", "") or "").strip():
|
|
1476
|
+
provider_entry["key_env"] = key_env
|
|
1477
|
+
cfg["providers"] = providers_cfg
|
|
1478
|
+
save_config(cfg)
|
|
1479
|
+
else:
|
|
1480
|
+
# Save model name to the custom_providers entry for next time
|
|
1481
|
+
_save_custom_provider(base_url, config_api_key, model_name, api_mode=api_mode)
|
|
1482
|
+
|
|
1483
|
+
print(f"\n✅ Model set to: {model_name}")
|
|
1484
|
+
print(f" Provider: {name} ({base_url})")
|
|
1485
|
+
|
|
1486
|
+
def _model_flow_copilot(config, current_model=""):
|
|
1487
|
+
"""GitHub Copilot flow using env vars, gh CLI, or OAuth device code."""
|
|
1488
|
+
from hermes_cli.main import _current_reasoning_effort, _prompt_reasoning_effort_selection, _set_reasoning_effort
|
|
1489
|
+
from hermes_cli.auth import (
|
|
1490
|
+
PROVIDER_REGISTRY,
|
|
1491
|
+
_prompt_model_selection,
|
|
1492
|
+
_save_model_choice,
|
|
1493
|
+
deactivate_provider,
|
|
1494
|
+
resolve_api_key_provider_credentials,
|
|
1495
|
+
)
|
|
1496
|
+
from hermes_cli.config import save_env_value, load_config, save_config
|
|
1497
|
+
from hermes_cli.models import (
|
|
1498
|
+
_PROVIDER_MODELS,
|
|
1499
|
+
fetch_api_models,
|
|
1500
|
+
fetch_github_model_catalog,
|
|
1501
|
+
github_model_reasoning_efforts,
|
|
1502
|
+
copilot_model_api_mode,
|
|
1503
|
+
normalize_copilot_model_id,
|
|
1504
|
+
)
|
|
1505
|
+
|
|
1506
|
+
provider_id = "copilot"
|
|
1507
|
+
pconfig = PROVIDER_REGISTRY[provider_id]
|
|
1508
|
+
|
|
1509
|
+
creds = resolve_api_key_provider_credentials(provider_id)
|
|
1510
|
+
api_key = creds.get("api_key", "")
|
|
1511
|
+
source = creds.get("source", "")
|
|
1512
|
+
|
|
1513
|
+
if not api_key:
|
|
1514
|
+
print("No GitHub token configured for GitHub Copilot.")
|
|
1515
|
+
print()
|
|
1516
|
+
print(" Supported token types:")
|
|
1517
|
+
print(
|
|
1518
|
+
" → OAuth token (gho_*) via `copilot login` or device code flow"
|
|
1519
|
+
)
|
|
1520
|
+
print(" → Fine-grained PAT (github_pat_*) with Copilot Requests permission")
|
|
1521
|
+
print(" → GitHub App token (ghu_*) via environment variable")
|
|
1522
|
+
print(" ✗ Classic PAT (ghp_*) NOT supported by Copilot API")
|
|
1523
|
+
print()
|
|
1524
|
+
print(" Options:")
|
|
1525
|
+
print(" 1. Login with GitHub (OAuth device code flow)")
|
|
1526
|
+
print(" 2. Enter a token manually")
|
|
1527
|
+
print(" 3. Cancel")
|
|
1528
|
+
print()
|
|
1529
|
+
try:
|
|
1530
|
+
choice = input(" Choice [1-3]: ").strip()
|
|
1531
|
+
except (KeyboardInterrupt, EOFError):
|
|
1532
|
+
print()
|
|
1533
|
+
return
|
|
1534
|
+
|
|
1535
|
+
if choice == "1":
|
|
1536
|
+
try:
|
|
1537
|
+
from hermes_cli.copilot_auth import copilot_device_code_login
|
|
1538
|
+
|
|
1539
|
+
token = copilot_device_code_login()
|
|
1540
|
+
if token:
|
|
1541
|
+
save_env_value("COPILOT_GITHUB_TOKEN", token)
|
|
1542
|
+
print(" Copilot token saved.")
|
|
1543
|
+
print()
|
|
1544
|
+
else:
|
|
1545
|
+
print(" Login cancelled or failed.")
|
|
1546
|
+
return
|
|
1547
|
+
except Exception as exc:
|
|
1548
|
+
print(f" Login failed: {exc}")
|
|
1549
|
+
return
|
|
1550
|
+
elif choice == "2":
|
|
1551
|
+
from hermes_cli.secret_prompt import masked_secret_prompt
|
|
1552
|
+
|
|
1553
|
+
try:
|
|
1554
|
+
new_key = masked_secret_prompt(" Token (COPILOT_GITHUB_TOKEN): ").strip()
|
|
1555
|
+
except (KeyboardInterrupt, EOFError):
|
|
1556
|
+
print()
|
|
1557
|
+
return
|
|
1558
|
+
if not new_key:
|
|
1559
|
+
print(" Cancelled.")
|
|
1560
|
+
return
|
|
1561
|
+
# Validate token type
|
|
1562
|
+
try:
|
|
1563
|
+
from hermes_cli.copilot_auth import validate_copilot_token
|
|
1564
|
+
|
|
1565
|
+
valid, msg = validate_copilot_token(new_key)
|
|
1566
|
+
if not valid:
|
|
1567
|
+
print(f" ✗ {msg}")
|
|
1568
|
+
return
|
|
1569
|
+
except ImportError:
|
|
1570
|
+
pass
|
|
1571
|
+
save_env_value("COPILOT_GITHUB_TOKEN", new_key)
|
|
1572
|
+
print(" Token saved.")
|
|
1573
|
+
print()
|
|
1574
|
+
else:
|
|
1575
|
+
print(" Cancelled.")
|
|
1576
|
+
return
|
|
1577
|
+
|
|
1578
|
+
creds = resolve_api_key_provider_credentials(provider_id)
|
|
1579
|
+
api_key = creds.get("api_key", "")
|
|
1580
|
+
source = creds.get("source", "")
|
|
1581
|
+
else:
|
|
1582
|
+
if source in {"GITHUB_TOKEN", "GH_TOKEN"}:
|
|
1583
|
+
from hermes_cli.env_loader import format_secret_source_suffix
|
|
1584
|
+
bw_suffix = format_secret_source_suffix(source)
|
|
1585
|
+
print(f" GitHub token: {api_key[:8]}... ✓ ({source}{bw_suffix})")
|
|
1586
|
+
elif source == "gh auth token":
|
|
1587
|
+
print(" GitHub token: ✓ (from `gh auth token`)")
|
|
1588
|
+
else:
|
|
1589
|
+
print(" GitHub token: ✓")
|
|
1590
|
+
print()
|
|
1591
|
+
|
|
1592
|
+
effective_base = pconfig.inference_base_url
|
|
1593
|
+
|
|
1594
|
+
catalog = fetch_github_model_catalog(api_key)
|
|
1595
|
+
live_models = (
|
|
1596
|
+
[item.get("id", "") for item in catalog if item.get("id")]
|
|
1597
|
+
if catalog
|
|
1598
|
+
else fetch_api_models(api_key, effective_base)
|
|
1599
|
+
)
|
|
1600
|
+
normalized_current_model = (
|
|
1601
|
+
normalize_copilot_model_id(
|
|
1602
|
+
current_model,
|
|
1603
|
+
catalog=catalog,
|
|
1604
|
+
api_key=api_key,
|
|
1605
|
+
)
|
|
1606
|
+
or current_model
|
|
1607
|
+
)
|
|
1608
|
+
if live_models:
|
|
1609
|
+
model_list = [model_id for model_id in live_models if model_id]
|
|
1610
|
+
print(f" Found {len(model_list)} model(s) from GitHub Copilot")
|
|
1611
|
+
else:
|
|
1612
|
+
model_list = _PROVIDER_MODELS.get(provider_id, [])
|
|
1613
|
+
if model_list:
|
|
1614
|
+
print(
|
|
1615
|
+
" ⚠ Could not auto-detect models from GitHub Copilot — showing defaults."
|
|
1616
|
+
)
|
|
1617
|
+
print(' Use "Enter custom model name" if you do not see your model.')
|
|
1618
|
+
|
|
1619
|
+
if model_list:
|
|
1620
|
+
selected = _prompt_model_selection(
|
|
1621
|
+
model_list,
|
|
1622
|
+
current_model=normalized_current_model,
|
|
1623
|
+
confirm_provider=provider_id,
|
|
1624
|
+
confirm_base_url=effective_base,
|
|
1625
|
+
confirm_api_key=api_key,
|
|
1626
|
+
)
|
|
1627
|
+
else:
|
|
1628
|
+
try:
|
|
1629
|
+
selected = input("Model name: ").strip()
|
|
1630
|
+
except (KeyboardInterrupt, EOFError):
|
|
1631
|
+
selected = None
|
|
1632
|
+
|
|
1633
|
+
if selected:
|
|
1634
|
+
selected = (
|
|
1635
|
+
normalize_copilot_model_id(
|
|
1636
|
+
selected,
|
|
1637
|
+
catalog=catalog,
|
|
1638
|
+
api_key=api_key,
|
|
1639
|
+
)
|
|
1640
|
+
or selected
|
|
1641
|
+
)
|
|
1642
|
+
initial_cfg = load_config()
|
|
1643
|
+
current_effort = _current_reasoning_effort(initial_cfg)
|
|
1644
|
+
reasoning_efforts = github_model_reasoning_efforts(
|
|
1645
|
+
selected,
|
|
1646
|
+
catalog=catalog,
|
|
1647
|
+
api_key=api_key,
|
|
1648
|
+
)
|
|
1649
|
+
selected_effort = None
|
|
1650
|
+
if reasoning_efforts:
|
|
1651
|
+
print(f" {selected} supports reasoning controls.")
|
|
1652
|
+
selected_effort = _prompt_reasoning_effort_selection(
|
|
1653
|
+
reasoning_efforts, current_effort=current_effort
|
|
1654
|
+
)
|
|
1655
|
+
|
|
1656
|
+
_save_model_choice(selected)
|
|
1657
|
+
|
|
1658
|
+
cfg = load_config()
|
|
1659
|
+
model = cfg.get("model")
|
|
1660
|
+
if not isinstance(model, dict):
|
|
1661
|
+
model = {"default": model} if model else {}
|
|
1662
|
+
cfg["model"] = model
|
|
1663
|
+
model["provider"] = provider_id
|
|
1664
|
+
model["base_url"] = effective_base
|
|
1665
|
+
model["api_mode"] = copilot_model_api_mode(
|
|
1666
|
+
selected,
|
|
1667
|
+
catalog=catalog,
|
|
1668
|
+
api_key=api_key,
|
|
1669
|
+
)
|
|
1670
|
+
if selected_effort is not None:
|
|
1671
|
+
_set_reasoning_effort(cfg, selected_effort)
|
|
1672
|
+
save_config(cfg)
|
|
1673
|
+
deactivate_provider()
|
|
1674
|
+
|
|
1675
|
+
print(f"Default model set to: {selected} (via {pconfig.name})")
|
|
1676
|
+
if reasoning_efforts:
|
|
1677
|
+
if selected_effort == "none":
|
|
1678
|
+
print("Reasoning disabled for this model.")
|
|
1679
|
+
elif selected_effort:
|
|
1680
|
+
print(f"Reasoning effort set to: {selected_effort}")
|
|
1681
|
+
else:
|
|
1682
|
+
print("No change.")
|
|
1683
|
+
|
|
1684
|
+
def _model_flow_copilot_acp(config, current_model=""):
|
|
1685
|
+
"""GitHub Copilot ACP flow using the local Copilot CLI."""
|
|
1686
|
+
from hermes_cli.auth import (
|
|
1687
|
+
PROVIDER_REGISTRY,
|
|
1688
|
+
_prompt_model_selection,
|
|
1689
|
+
_save_model_choice,
|
|
1690
|
+
deactivate_provider,
|
|
1691
|
+
get_external_process_provider_status,
|
|
1692
|
+
resolve_api_key_provider_credentials,
|
|
1693
|
+
resolve_external_process_provider_credentials,
|
|
1694
|
+
)
|
|
1695
|
+
from hermes_cli.models import (
|
|
1696
|
+
_PROVIDER_MODELS,
|
|
1697
|
+
fetch_github_model_catalog,
|
|
1698
|
+
normalize_copilot_model_id,
|
|
1699
|
+
)
|
|
1700
|
+
from hermes_cli.config import load_config, save_config
|
|
1701
|
+
|
|
1702
|
+
del config
|
|
1703
|
+
|
|
1704
|
+
provider_id = "copilot-acp"
|
|
1705
|
+
pconfig = PROVIDER_REGISTRY[provider_id]
|
|
1706
|
+
|
|
1707
|
+
status = get_external_process_provider_status(provider_id)
|
|
1708
|
+
resolved_command = (
|
|
1709
|
+
status.get("resolved_command") or status.get("command") or "copilot"
|
|
1710
|
+
)
|
|
1711
|
+
effective_base = status.get("base_url") or pconfig.inference_base_url
|
|
1712
|
+
|
|
1713
|
+
print(" GitHub Copilot ACP delegates Hermes turns to `copilot --acp`.")
|
|
1714
|
+
print(" Hermes currently starts its own ACP subprocess for each request.")
|
|
1715
|
+
print(" Hermes uses your selected model as a hint for the Copilot ACP session.")
|
|
1716
|
+
print(f" Command: {resolved_command}")
|
|
1717
|
+
print(f" Backend marker: {effective_base}")
|
|
1718
|
+
print()
|
|
1719
|
+
|
|
1720
|
+
try:
|
|
1721
|
+
creds = resolve_external_process_provider_credentials(provider_id)
|
|
1722
|
+
except Exception as exc:
|
|
1723
|
+
print(f" ⚠ {exc}")
|
|
1724
|
+
print(
|
|
1725
|
+
" Set HERMES_COPILOT_ACP_COMMAND or COPILOT_CLI_PATH if Copilot CLI is installed elsewhere."
|
|
1726
|
+
)
|
|
1727
|
+
return
|
|
1728
|
+
|
|
1729
|
+
effective_base = creds.get("base_url") or effective_base
|
|
1730
|
+
|
|
1731
|
+
catalog_api_key = ""
|
|
1732
|
+
try:
|
|
1733
|
+
catalog_creds = resolve_api_key_provider_credentials("copilot")
|
|
1734
|
+
catalog_api_key = catalog_creds.get("api_key", "")
|
|
1735
|
+
except Exception:
|
|
1736
|
+
pass
|
|
1737
|
+
|
|
1738
|
+
catalog = fetch_github_model_catalog(catalog_api_key)
|
|
1739
|
+
normalized_current_model = (
|
|
1740
|
+
normalize_copilot_model_id(
|
|
1741
|
+
current_model,
|
|
1742
|
+
catalog=catalog,
|
|
1743
|
+
api_key=catalog_api_key,
|
|
1744
|
+
)
|
|
1745
|
+
or current_model
|
|
1746
|
+
)
|
|
1747
|
+
|
|
1748
|
+
if catalog:
|
|
1749
|
+
model_list = [item.get("id", "") for item in catalog if item.get("id")]
|
|
1750
|
+
print(f" Found {len(model_list)} model(s) from GitHub Copilot")
|
|
1751
|
+
else:
|
|
1752
|
+
model_list = _PROVIDER_MODELS.get("copilot", [])
|
|
1753
|
+
if model_list:
|
|
1754
|
+
print(
|
|
1755
|
+
" ⚠ Could not auto-detect models from GitHub Copilot — showing defaults."
|
|
1756
|
+
)
|
|
1757
|
+
print(' Use "Enter custom model name" if you do not see your model.')
|
|
1758
|
+
|
|
1759
|
+
if model_list:
|
|
1760
|
+
selected = _prompt_model_selection(
|
|
1761
|
+
model_list,
|
|
1762
|
+
current_model=normalized_current_model,
|
|
1763
|
+
confirm_provider=provider_id,
|
|
1764
|
+
confirm_base_url=effective_base,
|
|
1765
|
+
confirm_api_key=catalog_api_key,
|
|
1766
|
+
)
|
|
1767
|
+
else:
|
|
1768
|
+
try:
|
|
1769
|
+
selected = input("Model name: ").strip()
|
|
1770
|
+
except (KeyboardInterrupt, EOFError):
|
|
1771
|
+
selected = None
|
|
1772
|
+
|
|
1773
|
+
if not selected:
|
|
1774
|
+
print("No change.")
|
|
1775
|
+
return
|
|
1776
|
+
|
|
1777
|
+
selected = (
|
|
1778
|
+
normalize_copilot_model_id(
|
|
1779
|
+
selected,
|
|
1780
|
+
catalog=catalog,
|
|
1781
|
+
api_key=catalog_api_key,
|
|
1782
|
+
)
|
|
1783
|
+
or selected
|
|
1784
|
+
)
|
|
1785
|
+
_save_model_choice(selected)
|
|
1786
|
+
|
|
1787
|
+
cfg = load_config()
|
|
1788
|
+
model = cfg.get("model")
|
|
1789
|
+
if not isinstance(model, dict):
|
|
1790
|
+
model = {"default": model} if model else {}
|
|
1791
|
+
cfg["model"] = model
|
|
1792
|
+
model["provider"] = provider_id
|
|
1793
|
+
model["base_url"] = effective_base
|
|
1794
|
+
model["api_mode"] = "chat_completions"
|
|
1795
|
+
save_config(cfg)
|
|
1796
|
+
deactivate_provider()
|
|
1797
|
+
|
|
1798
|
+
print(f"Default model set to: {selected} (via {pconfig.name})")
|
|
1799
|
+
|
|
1800
|
+
def _model_flow_kimi(config, current_model=""):
|
|
1801
|
+
"""Kimi / Moonshot model selection with automatic endpoint routing.
|
|
1802
|
+
|
|
1803
|
+
- sk-kimi-* keys → api.kimi.com/coding/v1 (Kimi Coding Plan)
|
|
1804
|
+
- Other keys → api.moonshot.ai/v1 (legacy Moonshot)
|
|
1805
|
+
|
|
1806
|
+
No manual base URL prompt — endpoint is determined by key prefix.
|
|
1807
|
+
"""
|
|
1808
|
+
from hermes_cli.main import _prompt_api_key
|
|
1809
|
+
from hermes_cli.auth import (
|
|
1810
|
+
PROVIDER_REGISTRY,
|
|
1811
|
+
KIMI_CODE_BASE_URL,
|
|
1812
|
+
_prompt_model_selection,
|
|
1813
|
+
_save_model_choice,
|
|
1814
|
+
deactivate_provider,
|
|
1815
|
+
)
|
|
1816
|
+
from hermes_cli.config import (
|
|
1817
|
+
get_env_value,
|
|
1818
|
+
save_env_value,
|
|
1819
|
+
load_config,
|
|
1820
|
+
save_config,
|
|
1821
|
+
)
|
|
1822
|
+
from hermes_cli.models import _PROVIDER_MODELS
|
|
1823
|
+
|
|
1824
|
+
provider_id = "kimi-coding"
|
|
1825
|
+
pconfig = PROVIDER_REGISTRY[provider_id]
|
|
1826
|
+
key_env = pconfig.api_key_env_vars[0] if pconfig.api_key_env_vars else ""
|
|
1827
|
+
base_url_env = pconfig.base_url_env_var or ""
|
|
1828
|
+
|
|
1829
|
+
# Step 1: Check / prompt for API key
|
|
1830
|
+
existing_key = ""
|
|
1831
|
+
for ev in pconfig.api_key_env_vars:
|
|
1832
|
+
existing_key = get_env_value(ev) or os.getenv(ev, "")
|
|
1833
|
+
if existing_key:
|
|
1834
|
+
break
|
|
1835
|
+
|
|
1836
|
+
existing_key, abort = _prompt_api_key(
|
|
1837
|
+
pconfig, existing_key, provider_id=provider_id
|
|
1838
|
+
)
|
|
1839
|
+
if abort:
|
|
1840
|
+
return
|
|
1841
|
+
|
|
1842
|
+
# Step 2: Auto-detect endpoint from key prefix
|
|
1843
|
+
is_coding_plan = existing_key.startswith("sk-kimi-")
|
|
1844
|
+
if is_coding_plan:
|
|
1845
|
+
effective_base = KIMI_CODE_BASE_URL
|
|
1846
|
+
print(f" Detected Kimi Coding Plan key → {effective_base}")
|
|
1847
|
+
else:
|
|
1848
|
+
effective_base = pconfig.inference_base_url
|
|
1849
|
+
print(f" Using Moonshot endpoint → {effective_base}")
|
|
1850
|
+
# Clear any manual base URL override so auto-detection works at runtime
|
|
1851
|
+
if base_url_env and get_env_value(base_url_env):
|
|
1852
|
+
save_env_value(base_url_env, "")
|
|
1853
|
+
print()
|
|
1854
|
+
|
|
1855
|
+
# Step 3: Model selection — show appropriate models for the endpoint
|
|
1856
|
+
model_list = _PROVIDER_MODELS.get("kimi-coding" if is_coding_plan else "moonshot", [])
|
|
1857
|
+
|
|
1858
|
+
if model_list:
|
|
1859
|
+
selected = _prompt_model_selection(
|
|
1860
|
+
model_list,
|
|
1861
|
+
current_model=current_model,
|
|
1862
|
+
confirm_provider=provider_id,
|
|
1863
|
+
confirm_base_url=effective_base,
|
|
1864
|
+
confirm_api_key=existing_key,
|
|
1865
|
+
)
|
|
1866
|
+
else:
|
|
1867
|
+
try:
|
|
1868
|
+
selected = input("Enter model name: ").strip()
|
|
1869
|
+
except (KeyboardInterrupt, EOFError):
|
|
1870
|
+
selected = None
|
|
1871
|
+
|
|
1872
|
+
if selected:
|
|
1873
|
+
_save_model_choice(selected)
|
|
1874
|
+
|
|
1875
|
+
# Update config with provider and base URL
|
|
1876
|
+
cfg = load_config()
|
|
1877
|
+
model = cfg.get("model")
|
|
1878
|
+
if not isinstance(model, dict):
|
|
1879
|
+
model = {"default": model} if model else {}
|
|
1880
|
+
cfg["model"] = model
|
|
1881
|
+
model["provider"] = provider_id
|
|
1882
|
+
model["base_url"] = effective_base
|
|
1883
|
+
model.pop("api_mode", None) # let runtime auto-detect from URL
|
|
1884
|
+
save_config(cfg)
|
|
1885
|
+
deactivate_provider()
|
|
1886
|
+
|
|
1887
|
+
endpoint_label = "Kimi Coding" if is_coding_plan else "Moonshot"
|
|
1888
|
+
print(f"Default model set to: {selected} (via {endpoint_label})")
|
|
1889
|
+
else:
|
|
1890
|
+
print("No change.")
|
|
1891
|
+
|
|
1892
|
+
def _model_flow_stepfun(config, current_model=""):
|
|
1893
|
+
"""StepFun Step Plan flow with region-specific endpoints."""
|
|
1894
|
+
from hermes_cli.main import _infer_stepfun_region, _prompt_api_key, _prompt_provider_choice, _stepfun_base_url_for_region
|
|
1895
|
+
from hermes_cli.auth import (
|
|
1896
|
+
PROVIDER_REGISTRY,
|
|
1897
|
+
_prompt_model_selection,
|
|
1898
|
+
_save_model_choice,
|
|
1899
|
+
deactivate_provider,
|
|
1900
|
+
)
|
|
1901
|
+
from hermes_cli.config import (
|
|
1902
|
+
get_env_value,
|
|
1903
|
+
save_env_value,
|
|
1904
|
+
load_config,
|
|
1905
|
+
save_config,
|
|
1906
|
+
)
|
|
1907
|
+
from hermes_cli.models import _PROVIDER_MODELS, fetch_api_models
|
|
1908
|
+
|
|
1909
|
+
provider_id = "stepfun"
|
|
1910
|
+
pconfig = PROVIDER_REGISTRY[provider_id]
|
|
1911
|
+
key_env = pconfig.api_key_env_vars[0] if pconfig.api_key_env_vars else ""
|
|
1912
|
+
base_url_env = pconfig.base_url_env_var or ""
|
|
1913
|
+
|
|
1914
|
+
existing_key = ""
|
|
1915
|
+
for ev in pconfig.api_key_env_vars:
|
|
1916
|
+
existing_key = get_env_value(ev) or os.getenv(ev, "")
|
|
1917
|
+
if existing_key:
|
|
1918
|
+
break
|
|
1919
|
+
|
|
1920
|
+
existing_key, abort = _prompt_api_key(
|
|
1921
|
+
pconfig, existing_key, provider_id=provider_id
|
|
1922
|
+
)
|
|
1923
|
+
if abort:
|
|
1924
|
+
return
|
|
1925
|
+
|
|
1926
|
+
current_base = ""
|
|
1927
|
+
if base_url_env:
|
|
1928
|
+
current_base = get_env_value(base_url_env) or os.getenv(base_url_env, "")
|
|
1929
|
+
if not current_base:
|
|
1930
|
+
model_cfg = config.get("model")
|
|
1931
|
+
if isinstance(model_cfg, dict):
|
|
1932
|
+
current_base = str(model_cfg.get("base_url") or "").strip()
|
|
1933
|
+
current_region = _infer_stepfun_region(current_base or pconfig.inference_base_url)
|
|
1934
|
+
|
|
1935
|
+
region_choices = [
|
|
1936
|
+
(
|
|
1937
|
+
"international",
|
|
1938
|
+
f"International ({_stepfun_base_url_for_region('international')})",
|
|
1939
|
+
),
|
|
1940
|
+
("china", f"China ({_stepfun_base_url_for_region('china')})"),
|
|
1941
|
+
]
|
|
1942
|
+
ordered_regions = []
|
|
1943
|
+
for region_key, label in region_choices:
|
|
1944
|
+
if region_key == current_region:
|
|
1945
|
+
ordered_regions.insert(0, (region_key, f"{label} ← currently active"))
|
|
1946
|
+
else:
|
|
1947
|
+
ordered_regions.append((region_key, label))
|
|
1948
|
+
ordered_regions.append(("cancel", "Cancel"))
|
|
1949
|
+
|
|
1950
|
+
region_idx = _prompt_provider_choice([label for _, label in ordered_regions])
|
|
1951
|
+
if region_idx is None or ordered_regions[region_idx][0] == "cancel":
|
|
1952
|
+
print("No change.")
|
|
1953
|
+
return
|
|
1954
|
+
|
|
1955
|
+
selected_region = ordered_regions[region_idx][0]
|
|
1956
|
+
effective_base = _stepfun_base_url_for_region(selected_region)
|
|
1957
|
+
if base_url_env:
|
|
1958
|
+
save_env_value(base_url_env, effective_base)
|
|
1959
|
+
|
|
1960
|
+
live_models = fetch_api_models(existing_key, effective_base)
|
|
1961
|
+
if live_models:
|
|
1962
|
+
model_list = live_models
|
|
1963
|
+
print(f" Found {len(model_list)} model(s) from {pconfig.name} API")
|
|
1964
|
+
else:
|
|
1965
|
+
model_list = _PROVIDER_MODELS.get(provider_id, [])
|
|
1966
|
+
if model_list:
|
|
1967
|
+
print(
|
|
1968
|
+
f" Could not auto-detect models from {pconfig.name} API — "
|
|
1969
|
+
"showing Step Plan fallback catalog."
|
|
1970
|
+
)
|
|
1971
|
+
|
|
1972
|
+
if model_list:
|
|
1973
|
+
selected = _prompt_model_selection(
|
|
1974
|
+
model_list,
|
|
1975
|
+
current_model=current_model,
|
|
1976
|
+
confirm_provider=provider_id,
|
|
1977
|
+
confirm_base_url=effective_base,
|
|
1978
|
+
confirm_api_key=existing_key,
|
|
1979
|
+
)
|
|
1980
|
+
else:
|
|
1981
|
+
try:
|
|
1982
|
+
selected = input("Model name: ").strip()
|
|
1983
|
+
except (KeyboardInterrupt, EOFError):
|
|
1984
|
+
selected = None
|
|
1985
|
+
|
|
1986
|
+
if selected:
|
|
1987
|
+
_save_model_choice(selected)
|
|
1988
|
+
|
|
1989
|
+
cfg = load_config()
|
|
1990
|
+
model = cfg.get("model")
|
|
1991
|
+
if not isinstance(model, dict):
|
|
1992
|
+
model = {"default": model} if model else {}
|
|
1993
|
+
cfg["model"] = model
|
|
1994
|
+
model["provider"] = provider_id
|
|
1995
|
+
model["base_url"] = effective_base
|
|
1996
|
+
model.pop("api_mode", None)
|
|
1997
|
+
save_config(cfg)
|
|
1998
|
+
deactivate_provider()
|
|
1999
|
+
|
|
2000
|
+
config["model"] = dict(model)
|
|
2001
|
+
print(f"Default model set to: {selected} (via {pconfig.name})")
|
|
2002
|
+
else:
|
|
2003
|
+
print("No change.")
|
|
2004
|
+
|
|
2005
|
+
def _model_flow_bedrock_api_key(config, region, current_model=""):
|
|
2006
|
+
"""Bedrock API Key mode — uses the OpenAI-compatible bedrock-mantle endpoint.
|
|
2007
|
+
|
|
2008
|
+
For developers who don't have an AWS account but received a Bedrock API Key
|
|
2009
|
+
from their AWS admin. Works like any OpenAI-compatible endpoint.
|
|
2010
|
+
"""
|
|
2011
|
+
from hermes_cli.auth import (
|
|
2012
|
+
_prompt_model_selection,
|
|
2013
|
+
_save_model_choice,
|
|
2014
|
+
deactivate_provider,
|
|
2015
|
+
)
|
|
2016
|
+
from hermes_cli.config import (
|
|
2017
|
+
load_config,
|
|
2018
|
+
save_config,
|
|
2019
|
+
get_env_value,
|
|
2020
|
+
save_env_value,
|
|
2021
|
+
)
|
|
2022
|
+
from hermes_cli.models import _PROVIDER_MODELS
|
|
2023
|
+
|
|
2024
|
+
mantle_base_url = f"https://bedrock-mantle.{region}.api.aws/v1"
|
|
2025
|
+
|
|
2026
|
+
# Prompt for API key
|
|
2027
|
+
existing_key = get_env_value("AWS_BEARER_TOKEN_BEDROCK") or ""
|
|
2028
|
+
if existing_key:
|
|
2029
|
+
from hermes_cli.env_loader import format_secret_source_suffix
|
|
2030
|
+
source_suffix = format_secret_source_suffix("AWS_BEARER_TOKEN_BEDROCK")
|
|
2031
|
+
print(f" Bedrock API Key: {existing_key[:12]}... ✓{source_suffix}")
|
|
2032
|
+
else:
|
|
2033
|
+
print(f" Endpoint: {mantle_base_url}")
|
|
2034
|
+
print()
|
|
2035
|
+
from hermes_cli.secret_prompt import masked_secret_prompt
|
|
2036
|
+
|
|
2037
|
+
try:
|
|
2038
|
+
api_key = masked_secret_prompt(" Bedrock API Key: ").strip()
|
|
2039
|
+
except (KeyboardInterrupt, EOFError):
|
|
2040
|
+
print()
|
|
2041
|
+
return
|
|
2042
|
+
if not api_key:
|
|
2043
|
+
print(" Cancelled.")
|
|
2044
|
+
return
|
|
2045
|
+
save_env_value("AWS_BEARER_TOKEN_BEDROCK", api_key)
|
|
2046
|
+
existing_key = api_key
|
|
2047
|
+
print(" ✓ API key saved.")
|
|
2048
|
+
print()
|
|
2049
|
+
|
|
2050
|
+
# Model selection — use static list (mantle doesn't need boto3 for discovery)
|
|
2051
|
+
model_list = _PROVIDER_MODELS.get("bedrock", [])
|
|
2052
|
+
print(f" Showing {len(model_list)} curated models")
|
|
2053
|
+
|
|
2054
|
+
if model_list:
|
|
2055
|
+
selected = _prompt_model_selection(
|
|
2056
|
+
model_list,
|
|
2057
|
+
current_model=current_model,
|
|
2058
|
+
confirm_provider="custom",
|
|
2059
|
+
confirm_base_url=mantle_base_url,
|
|
2060
|
+
confirm_api_key=existing_key,
|
|
2061
|
+
)
|
|
2062
|
+
else:
|
|
2063
|
+
try:
|
|
2064
|
+
selected = input(" Model ID: ").strip()
|
|
2065
|
+
except (KeyboardInterrupt, EOFError):
|
|
2066
|
+
selected = None
|
|
2067
|
+
|
|
2068
|
+
if selected:
|
|
2069
|
+
_save_model_choice(selected)
|
|
2070
|
+
|
|
2071
|
+
# Save as custom provider pointing to bedrock-mantle
|
|
2072
|
+
cfg = load_config()
|
|
2073
|
+
model = cfg.get("model")
|
|
2074
|
+
if not isinstance(model, dict):
|
|
2075
|
+
model = {"default": model} if model else {}
|
|
2076
|
+
cfg["model"] = model
|
|
2077
|
+
model["provider"] = "custom"
|
|
2078
|
+
model["base_url"] = mantle_base_url
|
|
2079
|
+
model.pop("api_mode", None) # chat_completions is the default
|
|
2080
|
+
|
|
2081
|
+
# Also save region in bedrock config for reference
|
|
2082
|
+
bedrock_cfg = cfg.get("bedrock", {})
|
|
2083
|
+
if not isinstance(bedrock_cfg, dict):
|
|
2084
|
+
bedrock_cfg = {}
|
|
2085
|
+
bedrock_cfg["region"] = region
|
|
2086
|
+
cfg["bedrock"] = bedrock_cfg
|
|
2087
|
+
|
|
2088
|
+
# Save the API key env var name so hermes knows where to find it
|
|
2089
|
+
save_env_value("OPENAI_API_KEY", existing_key)
|
|
2090
|
+
save_env_value("OPENAI_BASE_URL", mantle_base_url)
|
|
2091
|
+
|
|
2092
|
+
save_config(cfg)
|
|
2093
|
+
deactivate_provider()
|
|
2094
|
+
|
|
2095
|
+
print(f" Default model set to: {selected} (via Bedrock API Key, {region})")
|
|
2096
|
+
print(f" Endpoint: {mantle_base_url}")
|
|
2097
|
+
else:
|
|
2098
|
+
print(" No change.")
|
|
2099
|
+
|
|
2100
|
+
def _model_flow_bedrock(config, current_model=""):
|
|
2101
|
+
"""AWS Bedrock provider: verify credentials, pick region, discover models.
|
|
2102
|
+
|
|
2103
|
+
Uses the native Converse API via boto3 — not the OpenAI-compatible endpoint.
|
|
2104
|
+
Auth is handled by the AWS SDK default credential chain (env vars, profile,
|
|
2105
|
+
instance role), so no API key prompt is needed.
|
|
2106
|
+
"""
|
|
2107
|
+
from hermes_cli.auth import (
|
|
2108
|
+
_prompt_model_selection,
|
|
2109
|
+
_save_model_choice,
|
|
2110
|
+
deactivate_provider,
|
|
2111
|
+
)
|
|
2112
|
+
from hermes_cli.config import load_config, save_config
|
|
2113
|
+
from hermes_cli.models import _PROVIDER_MODELS
|
|
2114
|
+
|
|
2115
|
+
# 1. Check for AWS credentials
|
|
2116
|
+
try:
|
|
2117
|
+
from agent.bedrock_adapter import (
|
|
2118
|
+
has_aws_credentials,
|
|
2119
|
+
resolve_aws_auth_env_var,
|
|
2120
|
+
resolve_bedrock_region,
|
|
2121
|
+
discover_bedrock_models,
|
|
2122
|
+
)
|
|
2123
|
+
except ImportError:
|
|
2124
|
+
print(" ✗ boto3 is not installed. Install it with:")
|
|
2125
|
+
print(" pip install boto3")
|
|
2126
|
+
print()
|
|
2127
|
+
return
|
|
2128
|
+
|
|
2129
|
+
if not has_aws_credentials():
|
|
2130
|
+
print(" ⚠ No AWS credentials detected via environment variables.")
|
|
2131
|
+
print(" Bedrock will use boto3's default credential chain (IMDS, SSO, etc.)")
|
|
2132
|
+
print()
|
|
2133
|
+
|
|
2134
|
+
auth_var = resolve_aws_auth_env_var()
|
|
2135
|
+
if auth_var:
|
|
2136
|
+
print(f" AWS credentials: {auth_var} ✓")
|
|
2137
|
+
else:
|
|
2138
|
+
print(" AWS credentials: boto3 default chain (instance role / SSO)")
|
|
2139
|
+
print()
|
|
2140
|
+
|
|
2141
|
+
# 2. Region selection
|
|
2142
|
+
current_region = resolve_bedrock_region()
|
|
2143
|
+
try:
|
|
2144
|
+
region_input = input(f" AWS Region [{current_region}]: ").strip()
|
|
2145
|
+
except (KeyboardInterrupt, EOFError):
|
|
2146
|
+
print()
|
|
2147
|
+
return
|
|
2148
|
+
region = region_input or current_region
|
|
2149
|
+
|
|
2150
|
+
# 2b. Authentication mode
|
|
2151
|
+
print(" Choose authentication method:")
|
|
2152
|
+
print()
|
|
2153
|
+
print(" 1. IAM credential chain (recommended)")
|
|
2154
|
+
print(" Works with EC2 instance roles, SSO, env vars, aws configure")
|
|
2155
|
+
print(" 2. Bedrock API Key")
|
|
2156
|
+
print(" Enter your Bedrock API Key directly — also supports")
|
|
2157
|
+
print(" team scenarios where an admin distributes keys")
|
|
2158
|
+
print()
|
|
2159
|
+
try:
|
|
2160
|
+
auth_choice = input(" Choice [1]: ").strip()
|
|
2161
|
+
except (KeyboardInterrupt, EOFError):
|
|
2162
|
+
print()
|
|
2163
|
+
return
|
|
2164
|
+
|
|
2165
|
+
if auth_choice == "2":
|
|
2166
|
+
_model_flow_bedrock_api_key(config, region, current_model)
|
|
2167
|
+
return
|
|
2168
|
+
|
|
2169
|
+
# 3. Model discovery — try live API first, fall back to static list
|
|
2170
|
+
print(f" Discovering models in {region}...")
|
|
2171
|
+
live_models = discover_bedrock_models(region)
|
|
2172
|
+
|
|
2173
|
+
if live_models:
|
|
2174
|
+
_EXCLUDE_PREFIXES = (
|
|
2175
|
+
"stability.",
|
|
2176
|
+
"cohere.embed",
|
|
2177
|
+
"twelvelabs.",
|
|
2178
|
+
"us.stability.",
|
|
2179
|
+
"us.cohere.embed",
|
|
2180
|
+
"us.twelvelabs.",
|
|
2181
|
+
"global.cohere.embed",
|
|
2182
|
+
"global.twelvelabs.",
|
|
2183
|
+
)
|
|
2184
|
+
_EXCLUDE_SUBSTRINGS = ("safeguard", "voxtral", "palmyra-vision")
|
|
2185
|
+
filtered = []
|
|
2186
|
+
for m in live_models:
|
|
2187
|
+
mid = m["id"]
|
|
2188
|
+
if any(mid.startswith(p) for p in _EXCLUDE_PREFIXES):
|
|
2189
|
+
continue
|
|
2190
|
+
if any(s in mid.lower() for s in _EXCLUDE_SUBSTRINGS):
|
|
2191
|
+
continue
|
|
2192
|
+
filtered.append(m)
|
|
2193
|
+
|
|
2194
|
+
# Deduplicate: prefer inference profiles (us.*, global.*) over bare
|
|
2195
|
+
# foundation model IDs.
|
|
2196
|
+
profile_base_ids = set()
|
|
2197
|
+
for m in filtered:
|
|
2198
|
+
mid = m["id"]
|
|
2199
|
+
if mid.startswith(("us.", "global.")):
|
|
2200
|
+
base = mid.split(".", 1)[1] if "." in mid[3:] else mid
|
|
2201
|
+
profile_base_ids.add(base)
|
|
2202
|
+
|
|
2203
|
+
deduped = []
|
|
2204
|
+
for m in filtered:
|
|
2205
|
+
mid = m["id"]
|
|
2206
|
+
if not mid.startswith(("us.", "global.")) and mid in profile_base_ids:
|
|
2207
|
+
continue
|
|
2208
|
+
deduped.append(m)
|
|
2209
|
+
|
|
2210
|
+
_RECOMMENDED = [
|
|
2211
|
+
"us.anthropic.claude-sonnet-4-6",
|
|
2212
|
+
"us.anthropic.claude-opus-4-6",
|
|
2213
|
+
"us.anthropic.claude-haiku-4-5",
|
|
2214
|
+
"us.amazon.nova-pro",
|
|
2215
|
+
"us.amazon.nova-lite",
|
|
2216
|
+
"us.amazon.nova-micro",
|
|
2217
|
+
"deepseek.v3",
|
|
2218
|
+
"us.meta.llama4-maverick",
|
|
2219
|
+
"us.meta.llama4-scout",
|
|
2220
|
+
]
|
|
2221
|
+
|
|
2222
|
+
def _sort_key(m):
|
|
2223
|
+
mid = m["id"]
|
|
2224
|
+
for i, rec in enumerate(_RECOMMENDED):
|
|
2225
|
+
if mid.startswith(rec):
|
|
2226
|
+
return (0, i, mid)
|
|
2227
|
+
if mid.startswith("global."):
|
|
2228
|
+
return (1, 0, mid)
|
|
2229
|
+
return (2, 0, mid)
|
|
2230
|
+
|
|
2231
|
+
deduped.sort(key=_sort_key)
|
|
2232
|
+
model_list = [m["id"] for m in deduped]
|
|
2233
|
+
print(
|
|
2234
|
+
f" Found {len(model_list)} text model(s) (filtered from {len(live_models)} total)"
|
|
2235
|
+
)
|
|
2236
|
+
else:
|
|
2237
|
+
model_list = _PROVIDER_MODELS.get("bedrock", [])
|
|
2238
|
+
if model_list:
|
|
2239
|
+
print(
|
|
2240
|
+
f" Using {len(model_list)} curated models (live discovery unavailable)"
|
|
2241
|
+
)
|
|
2242
|
+
else:
|
|
2243
|
+
print(
|
|
2244
|
+
" No models found. Check IAM permissions for bedrock:ListFoundationModels."
|
|
2245
|
+
)
|
|
2246
|
+
return
|
|
2247
|
+
|
|
2248
|
+
# 4. Model selection
|
|
2249
|
+
if model_list:
|
|
2250
|
+
selected = _prompt_model_selection(
|
|
2251
|
+
model_list,
|
|
2252
|
+
current_model=current_model,
|
|
2253
|
+
confirm_provider="bedrock",
|
|
2254
|
+
confirm_base_url=f"https://bedrock-runtime.{region}.amazonaws.com",
|
|
2255
|
+
)
|
|
2256
|
+
else:
|
|
2257
|
+
try:
|
|
2258
|
+
selected = input(" Model ID: ").strip()
|
|
2259
|
+
except (KeyboardInterrupt, EOFError):
|
|
2260
|
+
selected = None
|
|
2261
|
+
|
|
2262
|
+
if selected:
|
|
2263
|
+
_save_model_choice(selected)
|
|
2264
|
+
|
|
2265
|
+
cfg = load_config()
|
|
2266
|
+
model = cfg.get("model")
|
|
2267
|
+
if not isinstance(model, dict):
|
|
2268
|
+
model = {"default": model} if model else {}
|
|
2269
|
+
cfg["model"] = model
|
|
2270
|
+
model["provider"] = "bedrock"
|
|
2271
|
+
model["base_url"] = f"https://bedrock-runtime.{region}.amazonaws.com"
|
|
2272
|
+
model.pop("api_mode", None) # bedrock_converse is auto-detected
|
|
2273
|
+
|
|
2274
|
+
bedrock_cfg = cfg.get("bedrock", {})
|
|
2275
|
+
if not isinstance(bedrock_cfg, dict):
|
|
2276
|
+
bedrock_cfg = {}
|
|
2277
|
+
bedrock_cfg["region"] = region
|
|
2278
|
+
cfg["bedrock"] = bedrock_cfg
|
|
2279
|
+
|
|
2280
|
+
save_config(cfg)
|
|
2281
|
+
deactivate_provider()
|
|
2282
|
+
|
|
2283
|
+
print(f" Default model set to: {selected} (via AWS Bedrock, {region})")
|
|
2284
|
+
else:
|
|
2285
|
+
print(" No change.")
|
|
2286
|
+
|
|
2287
|
+
def _model_flow_api_key_provider(config, provider_id, current_model=""):
|
|
2288
|
+
"""Generic flow for API-key providers (z.ai, MiniMax, OpenCode, etc.)."""
|
|
2289
|
+
from hermes_cli.main import _prompt_api_key
|
|
2290
|
+
from hermes_cli.auth import (
|
|
2291
|
+
PROVIDER_REGISTRY,
|
|
2292
|
+
_prompt_model_selection,
|
|
2293
|
+
_save_model_choice,
|
|
2294
|
+
deactivate_provider,
|
|
2295
|
+
)
|
|
2296
|
+
from hermes_cli.config import (
|
|
2297
|
+
get_env_value,
|
|
2298
|
+
save_env_value,
|
|
2299
|
+
load_config,
|
|
2300
|
+
save_config,
|
|
2301
|
+
)
|
|
2302
|
+
from hermes_cli.models import (
|
|
2303
|
+
_PROVIDER_MODELS,
|
|
2304
|
+
fetch_api_models,
|
|
2305
|
+
opencode_model_api_mode,
|
|
2306
|
+
normalize_opencode_model_id,
|
|
2307
|
+
)
|
|
2308
|
+
|
|
2309
|
+
pconfig = PROVIDER_REGISTRY[provider_id]
|
|
2310
|
+
key_env = pconfig.api_key_env_vars[0] if pconfig.api_key_env_vars else ""
|
|
2311
|
+
base_url_env = pconfig.base_url_env_var or ""
|
|
2312
|
+
|
|
2313
|
+
# Check / prompt for API key
|
|
2314
|
+
existing_key = ""
|
|
2315
|
+
for ev in pconfig.api_key_env_vars:
|
|
2316
|
+
existing_key = get_env_value(ev) or os.getenv(ev, "")
|
|
2317
|
+
if existing_key:
|
|
2318
|
+
break
|
|
2319
|
+
|
|
2320
|
+
existing_key, abort = _prompt_api_key(
|
|
2321
|
+
pconfig, existing_key, provider_id=provider_id
|
|
2322
|
+
)
|
|
2323
|
+
if abort:
|
|
2324
|
+
return
|
|
2325
|
+
|
|
2326
|
+
# Gemini free-tier gate: free-tier daily quotas (<= 250 RPD for Flash)
|
|
2327
|
+
# are exhausted in a handful of agent turns, so refuse to wire up the
|
|
2328
|
+
# provider with a free-tier key. Probe is best-effort; network or auth
|
|
2329
|
+
# errors fall through without blocking.
|
|
2330
|
+
if provider_id == "gemini" and existing_key:
|
|
2331
|
+
try:
|
|
2332
|
+
from agent.gemini_native_adapter import probe_gemini_tier
|
|
2333
|
+
except Exception:
|
|
2334
|
+
probe_gemini_tier = None
|
|
2335
|
+
if probe_gemini_tier is not None:
|
|
2336
|
+
print(" Checking Gemini API tier...")
|
|
2337
|
+
probe_base = (
|
|
2338
|
+
(get_env_value(base_url_env) if base_url_env else "")
|
|
2339
|
+
or os.getenv(base_url_env or "", "")
|
|
2340
|
+
or pconfig.inference_base_url
|
|
2341
|
+
)
|
|
2342
|
+
tier = probe_gemini_tier(existing_key, probe_base)
|
|
2343
|
+
if tier == "free":
|
|
2344
|
+
print()
|
|
2345
|
+
print(
|
|
2346
|
+
"❌ This Google API key is on the free tier "
|
|
2347
|
+
"(<= 250 requests/day for gemini-2.5-flash)."
|
|
2348
|
+
)
|
|
2349
|
+
print(
|
|
2350
|
+
" Hermes typically makes 3-10 API calls per user turn "
|
|
2351
|
+
"(tool iterations + auxiliary tasks),"
|
|
2352
|
+
)
|
|
2353
|
+
print(
|
|
2354
|
+
" so the free tier is exhausted after a handful of "
|
|
2355
|
+
"messages and cannot sustain"
|
|
2356
|
+
)
|
|
2357
|
+
print(" an agent session.")
|
|
2358
|
+
print()
|
|
2359
|
+
print(
|
|
2360
|
+
" To use Gemini with Hermes, enable billing on your "
|
|
2361
|
+
"Google Cloud project and regenerate"
|
|
2362
|
+
)
|
|
2363
|
+
print(
|
|
2364
|
+
" the key in a billing-enabled project: "
|
|
2365
|
+
"https://aistudio.google.com/apikey"
|
|
2366
|
+
)
|
|
2367
|
+
print()
|
|
2368
|
+
print(
|
|
2369
|
+
" Alternatives with workable free usage: DeepSeek, "
|
|
2370
|
+
"OpenRouter (free models), Groq, Nous."
|
|
2371
|
+
)
|
|
2372
|
+
print()
|
|
2373
|
+
print("Not saving Gemini as the default provider.")
|
|
2374
|
+
return
|
|
2375
|
+
if tier == "paid":
|
|
2376
|
+
print(" Tier check: paid ✓")
|
|
2377
|
+
else:
|
|
2378
|
+
# "unknown" -- network issue, auth problem, unexpected response.
|
|
2379
|
+
# Don't block; the runtime 429 handler will surface free-tier
|
|
2380
|
+
# guidance if the key turns out to be free tier.
|
|
2381
|
+
print(" Tier check: could not verify (proceeding anyway).")
|
|
2382
|
+
print()
|
|
2383
|
+
|
|
2384
|
+
# Optional base URL override.
|
|
2385
|
+
# Precedence: env var → config.yaml model.base_url → registry default.
|
|
2386
|
+
# Reading config.yaml prevents silently overwriting a saved remote URL
|
|
2387
|
+
# (e.g. a remote LM Studio endpoint) with localhost when the user just
|
|
2388
|
+
# presses Enter at the prompt below.
|
|
2389
|
+
current_base = ""
|
|
2390
|
+
if base_url_env:
|
|
2391
|
+
current_base = get_env_value(base_url_env) or os.getenv(base_url_env, "")
|
|
2392
|
+
if not current_base:
|
|
2393
|
+
try:
|
|
2394
|
+
_m = load_config().get("model") or {}
|
|
2395
|
+
if str(_m.get("provider") or "").strip().lower() == provider_id:
|
|
2396
|
+
current_base = str(_m.get("base_url") or "").strip()
|
|
2397
|
+
except Exception:
|
|
2398
|
+
pass
|
|
2399
|
+
effective_base = current_base or pconfig.inference_base_url
|
|
2400
|
+
|
|
2401
|
+
if provider_id == "usepod":
|
|
2402
|
+
# UsePod authenticates by the token embedded in the URL path, so there
|
|
2403
|
+
# is no base URL for the user to type — it is derived from the pasted
|
|
2404
|
+
# key. Skip the prompt; honour USEPOD_BASE_URL only for self-hosting.
|
|
2405
|
+
from hermes_cli.auth import _resolve_usepod_base_url
|
|
2406
|
+
|
|
2407
|
+
key_for_probe = existing_key or (get_env_value(key_env) if key_env else "")
|
|
2408
|
+
env_override = ""
|
|
2409
|
+
if base_url_env:
|
|
2410
|
+
env_override = get_env_value(base_url_env) or os.getenv(base_url_env, "")
|
|
2411
|
+
effective_base = _resolve_usepod_base_url(key_for_probe, env_override)
|
|
2412
|
+
else:
|
|
2413
|
+
try:
|
|
2414
|
+
override = input(f"Base URL [{effective_base}]: ").strip()
|
|
2415
|
+
except (KeyboardInterrupt, EOFError):
|
|
2416
|
+
print()
|
|
2417
|
+
override = ""
|
|
2418
|
+
if override and base_url_env:
|
|
2419
|
+
if not override.startswith(("http://", "https://")):
|
|
2420
|
+
print(
|
|
2421
|
+
" Invalid URL — must start with http:// or https://. Keeping current value."
|
|
2422
|
+
)
|
|
2423
|
+
else:
|
|
2424
|
+
save_env_value(base_url_env, override)
|
|
2425
|
+
effective_base = override
|
|
2426
|
+
|
|
2427
|
+
# Model selection — resolution order:
|
|
2428
|
+
# 1. models.dev registry (cached, filtered for agentic/tool-capable models)
|
|
2429
|
+
# 2. Curated static fallback list (offline insurance)
|
|
2430
|
+
# 3. Live /models endpoint probe (small providers without models.dev data)
|
|
2431
|
+
#
|
|
2432
|
+
# LM Studio: live /api/v1/models probe (no models.dev catalog).
|
|
2433
|
+
# Ollama Cloud: merged discovery (live API + models.dev + disk cache).
|
|
2434
|
+
if provider_id == "lmstudio":
|
|
2435
|
+
from hermes_cli.auth import AuthError
|
|
2436
|
+
from hermes_cli.models import fetch_lmstudio_models
|
|
2437
|
+
|
|
2438
|
+
api_key_for_probe = existing_key or (get_env_value(key_env) if key_env else "")
|
|
2439
|
+
try:
|
|
2440
|
+
model_list = fetch_lmstudio_models(
|
|
2441
|
+
api_key=api_key_for_probe, base_url=effective_base
|
|
2442
|
+
)
|
|
2443
|
+
except AuthError as exc:
|
|
2444
|
+
print(f" LM Studio rejected the request: {exc}")
|
|
2445
|
+
print(" Set LM_API_KEY (or update it) to match the server's bearer token.")
|
|
2446
|
+
model_list = []
|
|
2447
|
+
if model_list:
|
|
2448
|
+
print(f" Found {len(model_list)} model(s) from LM Studio")
|
|
2449
|
+
elif provider_id == "ollama-cloud":
|
|
2450
|
+
from hermes_cli.models import fetch_ollama_cloud_models
|
|
2451
|
+
|
|
2452
|
+
api_key_for_probe = existing_key or (get_env_value(key_env) if key_env else "")
|
|
2453
|
+
# During setup, force a live refresh so the picker reflects newly
|
|
2454
|
+
# released models (e.g. deepseek v4 flash, kimi k2.6) the moment
|
|
2455
|
+
# the user enters their key — not an hour later when the disk
|
|
2456
|
+
# cache TTL expires.
|
|
2457
|
+
model_list = fetch_ollama_cloud_models(
|
|
2458
|
+
api_key=api_key_for_probe,
|
|
2459
|
+
base_url=effective_base,
|
|
2460
|
+
force_refresh=True,
|
|
2461
|
+
)
|
|
2462
|
+
if model_list:
|
|
2463
|
+
print(f" Found {len(model_list)} model(s) from Ollama Cloud")
|
|
2464
|
+
elif provider_id == "usepod":
|
|
2465
|
+
from hermes_cli.models import fetch_api_models
|
|
2466
|
+
from providers import get_provider_profile
|
|
2467
|
+
|
|
2468
|
+
api_key_for_probe = existing_key or (get_env_value(key_env) if key_env else "")
|
|
2469
|
+
live_models = fetch_api_models(api_key_for_probe, effective_base)
|
|
2470
|
+
if live_models:
|
|
2471
|
+
model_list = live_models
|
|
2472
|
+
print(f" Found {len(model_list)} model(s) from {pconfig.name} API")
|
|
2473
|
+
else:
|
|
2474
|
+
_pp = get_provider_profile("usepod")
|
|
2475
|
+
model_list = list(getattr(_pp, "fallback_models", ()) or [])
|
|
2476
|
+
if model_list:
|
|
2477
|
+
print(
|
|
2478
|
+
f' Showing {len(model_list)} curated models — use "Enter custom model name" for others.'
|
|
2479
|
+
)
|
|
2480
|
+
elif provider_id == "novita":
|
|
2481
|
+
from hermes_cli.models import fetch_api_models
|
|
2482
|
+
|
|
2483
|
+
api_key_for_probe = existing_key or (get_env_value(key_env) if key_env else "")
|
|
2484
|
+
curated = _PROVIDER_MODELS.get(provider_id, [])
|
|
2485
|
+
live_models = fetch_api_models(api_key_for_probe, effective_base)
|
|
2486
|
+
if live_models:
|
|
2487
|
+
model_list = live_models
|
|
2488
|
+
print(f" Found {len(model_list)} model(s) from {pconfig.name} API")
|
|
2489
|
+
else:
|
|
2490
|
+
mdev_models: list = []
|
|
2491
|
+
try:
|
|
2492
|
+
from agent.models_dev import list_agentic_models
|
|
2493
|
+
|
|
2494
|
+
mdev_models = list_agentic_models(provider_id)
|
|
2495
|
+
except Exception:
|
|
2496
|
+
pass
|
|
2497
|
+
if mdev_models:
|
|
2498
|
+
seen = {m.lower() for m in mdev_models}
|
|
2499
|
+
model_list = list(mdev_models)
|
|
2500
|
+
for m in curated:
|
|
2501
|
+
if m.lower() not in seen:
|
|
2502
|
+
model_list.append(m)
|
|
2503
|
+
seen.add(m.lower())
|
|
2504
|
+
print(f" Found {len(model_list)} model(s) from models.dev registry")
|
|
2505
|
+
else:
|
|
2506
|
+
model_list = curated
|
|
2507
|
+
if model_list:
|
|
2508
|
+
print(
|
|
2509
|
+
f' Showing {len(model_list)} curated models — use "Enter custom model name" for others.'
|
|
2510
|
+
)
|
|
2511
|
+
else:
|
|
2512
|
+
curated = _PROVIDER_MODELS.get(provider_id, [])
|
|
2513
|
+
|
|
2514
|
+
# Try models.dev first — returns tool-capable models, filtered for noise
|
|
2515
|
+
mdev_models: list = []
|
|
2516
|
+
try:
|
|
2517
|
+
from agent.models_dev import list_agentic_models
|
|
2518
|
+
|
|
2519
|
+
mdev_models = list_agentic_models(provider_id)
|
|
2520
|
+
except Exception:
|
|
2521
|
+
pass
|
|
2522
|
+
|
|
2523
|
+
if mdev_models:
|
|
2524
|
+
# Merge models.dev with curated list so newly added models
|
|
2525
|
+
# (not yet in models.dev) still appear in the picker.
|
|
2526
|
+
if curated:
|
|
2527
|
+
seen = {m.lower() for m in mdev_models}
|
|
2528
|
+
merged = list(mdev_models)
|
|
2529
|
+
for m in curated:
|
|
2530
|
+
if m.lower() not in seen:
|
|
2531
|
+
merged.append(m)
|
|
2532
|
+
seen.add(m.lower())
|
|
2533
|
+
model_list = merged
|
|
2534
|
+
else:
|
|
2535
|
+
model_list = mdev_models
|
|
2536
|
+
print(f" Found {len(model_list)} model(s) from models.dev registry")
|
|
2537
|
+
elif curated and len(curated) >= 8:
|
|
2538
|
+
# Curated list is substantial — use it directly, skip live probe
|
|
2539
|
+
model_list = curated
|
|
2540
|
+
print(
|
|
2541
|
+
f' Showing {len(model_list)} curated models — use "Enter custom model name" for others.'
|
|
2542
|
+
)
|
|
2543
|
+
else:
|
|
2544
|
+
api_key_for_probe = existing_key or (
|
|
2545
|
+
get_env_value(key_env) if key_env else ""
|
|
2546
|
+
)
|
|
2547
|
+
live_models = fetch_api_models(api_key_for_probe, effective_base)
|
|
2548
|
+
if live_models and len(live_models) >= len(curated):
|
|
2549
|
+
model_list = live_models
|
|
2550
|
+
print(f" Found {len(model_list)} model(s) from {pconfig.name} API")
|
|
2551
|
+
else:
|
|
2552
|
+
model_list = curated
|
|
2553
|
+
if model_list:
|
|
2554
|
+
print(
|
|
2555
|
+
f' Showing {len(model_list)} curated models — use "Enter custom model name" for others.'
|
|
2556
|
+
)
|
|
2557
|
+
# else: no defaults either, will fall through to raw input
|
|
2558
|
+
|
|
2559
|
+
if provider_id in {"opencode-zen", "opencode-go"}:
|
|
2560
|
+
model_list = [
|
|
2561
|
+
normalize_opencode_model_id(provider_id, mid) for mid in model_list
|
|
2562
|
+
]
|
|
2563
|
+
current_model = normalize_opencode_model_id(provider_id, current_model)
|
|
2564
|
+
model_list = list(dict.fromkeys(mid for mid in model_list if mid))
|
|
2565
|
+
|
|
2566
|
+
if model_list:
|
|
2567
|
+
selected = _prompt_model_selection(
|
|
2568
|
+
model_list,
|
|
2569
|
+
current_model=current_model,
|
|
2570
|
+
confirm_provider=provider_id,
|
|
2571
|
+
confirm_base_url=effective_base,
|
|
2572
|
+
confirm_api_key=existing_key,
|
|
2573
|
+
)
|
|
2574
|
+
else:
|
|
2575
|
+
try:
|
|
2576
|
+
selected = input("Model name: ").strip()
|
|
2577
|
+
except (KeyboardInterrupt, EOFError):
|
|
2578
|
+
selected = None
|
|
2579
|
+
|
|
2580
|
+
if selected:
|
|
2581
|
+
if provider_id in {"opencode-zen", "opencode-go"}:
|
|
2582
|
+
selected = normalize_opencode_model_id(provider_id, selected)
|
|
2583
|
+
|
|
2584
|
+
_save_model_choice(selected)
|
|
2585
|
+
|
|
2586
|
+
# Update config with provider, base URL, and provider-specific API mode
|
|
2587
|
+
cfg = load_config()
|
|
2588
|
+
model = cfg.get("model")
|
|
2589
|
+
if not isinstance(model, dict):
|
|
2590
|
+
model = {"default": model} if model else {}
|
|
2591
|
+
cfg["model"] = model
|
|
2592
|
+
model["provider"] = provider_id
|
|
2593
|
+
if provider_id == "usepod":
|
|
2594
|
+
# The effective base URL embeds the token; persisting it would bake
|
|
2595
|
+
# the secret into config.yaml and go stale on key rotation. Runtime
|
|
2596
|
+
# always re-derives it from the current USEPOD_API_KEY.
|
|
2597
|
+
model.pop("base_url", None)
|
|
2598
|
+
else:
|
|
2599
|
+
model["base_url"] = effective_base
|
|
2600
|
+
if provider_id in {"opencode-zen", "opencode-go"}:
|
|
2601
|
+
model["api_mode"] = opencode_model_api_mode(provider_id, selected)
|
|
2602
|
+
else:
|
|
2603
|
+
model.pop("api_mode", None)
|
|
2604
|
+
save_config(cfg)
|
|
2605
|
+
deactivate_provider()
|
|
2606
|
+
|
|
2607
|
+
print(f"Default model set to: {selected} (via {pconfig.name})")
|
|
2608
|
+
else:
|
|
2609
|
+
print("No change.")
|
|
2610
|
+
|
|
2611
|
+
def _model_flow_anthropic(config, current_model=""):
|
|
2612
|
+
"""Flow for Anthropic provider — OAuth subscription, API key, or Claude Code creds."""
|
|
2613
|
+
from hermes_cli.main import _run_anthropic_oauth_flow
|
|
2614
|
+
from hermes_cli.auth import (
|
|
2615
|
+
_prompt_model_selection,
|
|
2616
|
+
_save_model_choice,
|
|
2617
|
+
deactivate_provider,
|
|
2618
|
+
)
|
|
2619
|
+
from hermes_cli.config import (
|
|
2620
|
+
save_env_value,
|
|
2621
|
+
load_config,
|
|
2622
|
+
save_config,
|
|
2623
|
+
save_anthropic_api_key,
|
|
2624
|
+
)
|
|
2625
|
+
from hermes_cli.models import _PROVIDER_MODELS
|
|
2626
|
+
|
|
2627
|
+
# Check ALL credential sources
|
|
2628
|
+
from hermes_cli.auth import get_anthropic_key
|
|
2629
|
+
|
|
2630
|
+
existing_key = get_anthropic_key()
|
|
2631
|
+
cc_available = False
|
|
2632
|
+
try:
|
|
2633
|
+
from agent.anthropic_adapter import (
|
|
2634
|
+
read_claude_code_credentials,
|
|
2635
|
+
is_claude_code_token_valid,
|
|
2636
|
+
_is_oauth_token,
|
|
2637
|
+
)
|
|
2638
|
+
|
|
2639
|
+
cc_creds = read_claude_code_credentials()
|
|
2640
|
+
if cc_creds and is_claude_code_token_valid(cc_creds):
|
|
2641
|
+
cc_available = True
|
|
2642
|
+
except Exception:
|
|
2643
|
+
pass
|
|
2644
|
+
|
|
2645
|
+
# Stale-OAuth guard: if the only existing cred is an expired OAuth token
|
|
2646
|
+
# (no valid cc_creds to fall back on), treat it as missing so the re-auth
|
|
2647
|
+
# path is offered instead of silently accepting a broken token.
|
|
2648
|
+
existing_is_stale_oauth = False
|
|
2649
|
+
if existing_key and _is_oauth_token(existing_key) and not cc_available:
|
|
2650
|
+
existing_is_stale_oauth = True
|
|
2651
|
+
|
|
2652
|
+
has_creds = (bool(existing_key) and not existing_is_stale_oauth) or cc_available
|
|
2653
|
+
needs_auth = not has_creds
|
|
2654
|
+
|
|
2655
|
+
if has_creds:
|
|
2656
|
+
# Show what we found
|
|
2657
|
+
if existing_key:
|
|
2658
|
+
from hermes_cli.env_loader import format_secret_source_suffix
|
|
2659
|
+
from hermes_cli.auth import PROVIDER_REGISTRY
|
|
2660
|
+
|
|
2661
|
+
# Surface which env var supplied the key so users with
|
|
2662
|
+
# Bitwarden see "(from Bitwarden)" — without this, a detected
|
|
2663
|
+
# BSM key looks identical to a key in .env and users assume
|
|
2664
|
+
# nothing is wired up.
|
|
2665
|
+
source_suffix = ""
|
|
2666
|
+
for var in PROVIDER_REGISTRY["anthropic"].api_key_env_vars:
|
|
2667
|
+
if os.getenv(var, "").strip() == existing_key:
|
|
2668
|
+
source_suffix = format_secret_source_suffix(var)
|
|
2669
|
+
if source_suffix:
|
|
2670
|
+
break
|
|
2671
|
+
print(
|
|
2672
|
+
f" Anthropic credentials: {existing_key[:12]}... ✓{source_suffix}"
|
|
2673
|
+
)
|
|
2674
|
+
elif cc_available:
|
|
2675
|
+
print(" Claude Code credentials: ✓ (auto-detected)")
|
|
2676
|
+
print()
|
|
2677
|
+
choice = _prompt_auth_credentials_choice("Anthropic credentials:")
|
|
2678
|
+
|
|
2679
|
+
if choice == "reauth":
|
|
2680
|
+
needs_auth = True
|
|
2681
|
+
elif choice == "cancel":
|
|
2682
|
+
return
|
|
2683
|
+
# choice == "use" or default: use existing, proceed to model selection
|
|
2684
|
+
|
|
2685
|
+
if needs_auth:
|
|
2686
|
+
# Show auth method choice
|
|
2687
|
+
print()
|
|
2688
|
+
print(" Choose authentication method:")
|
|
2689
|
+
print()
|
|
2690
|
+
print(" 1. Claude Pro/Max subscription (OAuth login)")
|
|
2691
|
+
print(" 2. Anthropic API key (pay-per-token)")
|
|
2692
|
+
print(" 3. Cancel")
|
|
2693
|
+
print()
|
|
2694
|
+
try:
|
|
2695
|
+
choice = input(" Choice [1/2/3]: ").strip()
|
|
2696
|
+
except (KeyboardInterrupt, EOFError):
|
|
2697
|
+
print()
|
|
2698
|
+
return
|
|
2699
|
+
|
|
2700
|
+
if choice == "1":
|
|
2701
|
+
if not _run_anthropic_oauth_flow(save_env_value):
|
|
2702
|
+
return
|
|
2703
|
+
|
|
2704
|
+
elif choice == "2":
|
|
2705
|
+
print()
|
|
2706
|
+
print(" Get an API key at: https://platform.claude.com/settings/keys")
|
|
2707
|
+
print()
|
|
2708
|
+
from hermes_cli.secret_prompt import masked_secret_prompt
|
|
2709
|
+
|
|
2710
|
+
try:
|
|
2711
|
+
api_key = masked_secret_prompt(" API key (sk-ant-...): ").strip()
|
|
2712
|
+
except (KeyboardInterrupt, EOFError):
|
|
2713
|
+
print()
|
|
2714
|
+
return
|
|
2715
|
+
if not api_key:
|
|
2716
|
+
print(" Cancelled.")
|
|
2717
|
+
return
|
|
2718
|
+
save_anthropic_api_key(api_key, save_fn=save_env_value)
|
|
2719
|
+
print(" ✓ API key saved.")
|
|
2720
|
+
|
|
2721
|
+
else:
|
|
2722
|
+
print(" No change.")
|
|
2723
|
+
return
|
|
2724
|
+
print()
|
|
2725
|
+
|
|
2726
|
+
# Model selection
|
|
2727
|
+
model_list = _PROVIDER_MODELS.get("anthropic", [])
|
|
2728
|
+
if model_list:
|
|
2729
|
+
selected = _prompt_model_selection(
|
|
2730
|
+
model_list,
|
|
2731
|
+
current_model=current_model,
|
|
2732
|
+
confirm_provider="anthropic",
|
|
2733
|
+
)
|
|
2734
|
+
else:
|
|
2735
|
+
try:
|
|
2736
|
+
selected = input("Model name (e.g., claude-sonnet-4-20250514): ").strip()
|
|
2737
|
+
except (KeyboardInterrupt, EOFError):
|
|
2738
|
+
selected = None
|
|
2739
|
+
|
|
2740
|
+
if selected:
|
|
2741
|
+
_save_model_choice(selected)
|
|
2742
|
+
|
|
2743
|
+
# Update config with provider — clear base_url since
|
|
2744
|
+
# resolve_runtime_provider() always hardcodes Anthropic's URL.
|
|
2745
|
+
# Leaving a stale base_url in config can contaminate other
|
|
2746
|
+
# providers if the user switches without running 'hermes model'.
|
|
2747
|
+
cfg = load_config()
|
|
2748
|
+
model = cfg.get("model")
|
|
2749
|
+
if not isinstance(model, dict):
|
|
2750
|
+
model = {"default": model} if model else {}
|
|
2751
|
+
cfg["model"] = model
|
|
2752
|
+
model["provider"] = "anthropic"
|
|
2753
|
+
model.pop("base_url", None)
|
|
2754
|
+
save_config(cfg)
|
|
2755
|
+
deactivate_provider()
|
|
2756
|
+
|
|
2757
|
+
print(f"Default model set to: {selected} (via Anthropic)")
|
|
2758
|
+
else:
|
|
2759
|
+
print("No change.")
|