octo-agent 0.11.2
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.
- checksums.yaml +7 -0
- data/.clacky/skills/commit/SKILL.md +423 -0
- data/.clacky/skills/gem-release/SKILL.md +199 -0
- data/.clacky/skills/gem-release/scripts/release.sh +304 -0
- data/.clacky/skills/oss-upload/SKILL.md +47 -0
- data/.octorules +106 -0
- data/.rspec +3 -0
- data/.rubocop.yml +8 -0
- data/CHANGELOG.md +76 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/CONTRIBUTING.md +92 -0
- data/Dockerfile +28 -0
- data/LICENSE.txt +22 -0
- data/POSITIONING.md +46 -0
- data/README.md +134 -0
- data/README_CN.md +134 -0
- data/Rakefile +34 -0
- data/benchmark/fixtures/sample_project/Gemfile +3 -0
- data/benchmark/fixtures/sample_project/lib/api_handler.rb +32 -0
- data/benchmark/fixtures/sample_project/lib/order_calculator.rb +23 -0
- data/benchmark/fixtures/sample_project/lib/user_renderer.rb +20 -0
- data/benchmark/fixtures/sample_project/spec/order_calculator_spec.rb +20 -0
- data/benchmark/results/EVALUATION_REPORT.md +165 -0
- data/benchmark/results/baseline_20260511_174424.json +128 -0
- data/benchmark/results/report_20260511_175256.json +271 -0
- data/benchmark/results/report_20260511_175444.json +271 -0
- data/benchmark/results/treatment_20260511_175103.json +130 -0
- data/benchmark/runner.rb +441 -0
- data/bin/octo +7 -0
- data/docs/agent-first-ui-design.md +77 -0
- data/docs/billing-system.md +318 -0
- data/docs/channel-architecture.md +235 -0
- data/docs/engineering-article.md +343 -0
- data/docs/session-skill-invocation.md +69 -0
- data/docs/time_machine_design.md +247 -0
- data/docs/ui2-architecture.md +124 -0
- data/homebrew/README.md +96 -0
- data/homebrew/openocto.rb +24 -0
- data/lib/octo/agent/hook_manager.rb +61 -0
- data/lib/octo/agent/llm_caller.rb +800 -0
- data/lib/octo/agent/memory_updater.rb +246 -0
- data/lib/octo/agent/message_compressor.rb +225 -0
- data/lib/octo/agent/message_compressor_helper.rb +869 -0
- data/lib/octo/agent/next_message_suggester.rb +215 -0
- data/lib/octo/agent/session_serializer.rb +685 -0
- data/lib/octo/agent/skill_auto_creator.rb +114 -0
- data/lib/octo/agent/skill_evolution.rb +61 -0
- data/lib/octo/agent/skill_manager.rb +466 -0
- data/lib/octo/agent/skill_reflector.rb +89 -0
- data/lib/octo/agent/system_prompt_builder.rb +101 -0
- data/lib/octo/agent/time_machine.rb +214 -0
- data/lib/octo/agent/tool_executor.rb +454 -0
- data/lib/octo/agent/tool_registry.rb +150 -0
- data/lib/octo/agent.rb +2180 -0
- data/lib/octo/agent_config.rb +989 -0
- data/lib/octo/agent_profile.rb +112 -0
- data/lib/octo/anthropic_stream_aggregator.rb +137 -0
- data/lib/octo/background_task_registry.rb +324 -0
- data/lib/octo/banner.rb +34 -0
- data/lib/octo/bedrock_stream_aggregator.rb +137 -0
- data/lib/octo/block_font.rb +331 -0
- data/lib/octo/cli.rb +968 -0
- data/lib/octo/client.rb +623 -0
- data/lib/octo/default_agents/SOUL.md +3 -0
- data/lib/octo/default_agents/USER.md +1 -0
- data/lib/octo/default_agents/base_prompt.md +66 -0
- data/lib/octo/default_agents/coding/profile.yml +2 -0
- data/lib/octo/default_agents/coding/system_prompt.md +67 -0
- data/lib/octo/default_agents/general/profile.yml +2 -0
- data/lib/octo/default_agents/general/system_prompt.md +16 -0
- data/lib/octo/default_parsers/doc_parser.rb +69 -0
- data/lib/octo/default_parsers/docx_parser.rb +188 -0
- data/lib/octo/default_parsers/pdf_parser.rb +120 -0
- data/lib/octo/default_parsers/pdf_parser_ocr.py +103 -0
- data/lib/octo/default_parsers/pdf_parser_plumber.py +62 -0
- data/lib/octo/default_parsers/pptx_parser.rb +140 -0
- data/lib/octo/default_parsers/xlsx_parser.rb +121 -0
- data/lib/octo/default_skills/browser-setup/SKILL.md +426 -0
- data/lib/octo/default_skills/channel-manager/SKILL.md +623 -0
- data/lib/octo/default_skills/channel-manager/dingtalk_setup.rb +191 -0
- data/lib/octo/default_skills/channel-manager/discord_setup.rb +199 -0
- data/lib/octo/default_skills/channel-manager/feishu_setup.rb +574 -0
- data/lib/octo/default_skills/channel-manager/import_lark_skills.rb +97 -0
- data/lib/octo/default_skills/channel-manager/install_feishu_skills.rb +105 -0
- data/lib/octo/default_skills/channel-manager/weixin_setup.rb +274 -0
- data/lib/octo/default_skills/code-explorer/SKILL.md +36 -0
- data/lib/octo/default_skills/cron-task-creator/SKILL.md +257 -0
- data/lib/octo/default_skills/cron-task-creator/evals/evals.json +38 -0
- data/lib/octo/default_skills/onboard/SKILL.md +578 -0
- data/lib/octo/default_skills/onboard/scripts/import_external_skills.rb +413 -0
- data/lib/octo/default_skills/onboard/scripts/install_builtin_skills.rb +97 -0
- data/lib/octo/default_skills/persist-memory/SKILL.md +59 -0
- data/lib/octo/default_skills/personal-website/SKILL.md +113 -0
- data/lib/octo/default_skills/personal-website/publish.rb +235 -0
- data/lib/octo/default_skills/product-help/SKILL.md +123 -0
- data/lib/octo/default_skills/product-help/docs/agent-config.md +74 -0
- data/lib/octo/default_skills/product-help/docs/best-practices.md +49 -0
- data/lib/octo/default_skills/product-help/docs/browser-tool.md +53 -0
- data/lib/octo/default_skills/product-help/docs/built-in-skills.md +43 -0
- data/lib/octo/default_skills/product-help/docs/cli-reference.md +82 -0
- data/lib/octo/default_skills/product-help/docs/create-your-first-skill.md +47 -0
- data/lib/octo/default_skills/product-help/docs/faq.md +98 -0
- data/lib/octo/default_skills/product-help/docs/how-to-use-a-skill.md +58 -0
- data/lib/octo/default_skills/product-help/docs/installation.md +59 -0
- data/lib/octo/default_skills/product-help/docs/memory-system.md +61 -0
- data/lib/octo/default_skills/product-help/docs/octorules.md +62 -0
- data/lib/octo/default_skills/product-help/docs/session-management.md +63 -0
- data/lib/octo/default_skills/product-help/docs/skill-basics.md +55 -0
- data/lib/octo/default_skills/product-help/docs/skill-frontmatter.md +61 -0
- data/lib/octo/default_skills/product-help/docs/web-server.md +49 -0
- data/lib/octo/default_skills/product-help/docs/what-is-octo.md +37 -0
- data/lib/octo/default_skills/product-help/docs/windows-installation.md +36 -0
- data/lib/octo/default_skills/product-help/docs/writing-tips.md +53 -0
- data/lib/octo/default_skills/recall-memory/SKILL.md +65 -0
- data/lib/octo/default_skills/skill-add/SKILL.md +59 -0
- data/lib/octo/default_skills/skill-add/scripts/install_from_zip.rb +295 -0
- data/lib/octo/default_skills/skill-creator/SKILL.md +602 -0
- data/lib/octo/default_skills/skill-creator/agents/analyzer.md +274 -0
- data/lib/octo/default_skills/skill-creator/agents/comparator.md +202 -0
- data/lib/octo/default_skills/skill-creator/agents/grader.md +223 -0
- data/lib/octo/default_skills/skill-creator/eval-viewer/generate_review.py +471 -0
- data/lib/octo/default_skills/skill-creator/eval-viewer/viewer.html +1325 -0
- data/lib/octo/default_skills/skill-creator/references/schemas.md +430 -0
- data/lib/octo/default_skills/skill-creator/scripts/__init__.py +0 -0
- data/lib/octo/default_skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
- data/lib/octo/default_skills/skill-creator/scripts/generate_report.py +326 -0
- data/lib/octo/default_skills/skill-creator/scripts/improve_description.py +310 -0
- data/lib/octo/default_skills/skill-creator/scripts/quick_validate.py +103 -0
- data/lib/octo/default_skills/skill-creator/scripts/run_eval.py +317 -0
- data/lib/octo/default_skills/skill-creator/scripts/run_loop.py +331 -0
- data/lib/octo/default_skills/skill-creator/scripts/utils.py +47 -0
- data/lib/octo/default_skills/skill-creator/scripts/validate_skill_frontmatter.rb +143 -0
- data/lib/octo/idle_compression_timer.rb +115 -0
- data/lib/octo/json_ui_controller.rb +204 -0
- data/lib/octo/message_format/anthropic.rb +409 -0
- data/lib/octo/message_format/bedrock.rb +361 -0
- data/lib/octo/message_format/open_ai.rb +222 -0
- data/lib/octo/message_history.rb +373 -0
- data/lib/octo/openai_stream_aggregator.rb +130 -0
- data/lib/octo/plain_ui_controller.rb +166 -0
- data/lib/octo/providers.rb +534 -0
- data/lib/octo/server/browser_manager.rb +397 -0
- data/lib/octo/server/channel/adapters/base.rb +82 -0
- data/lib/octo/server/channel/adapters/dingtalk/adapter.rb +314 -0
- data/lib/octo/server/channel/adapters/dingtalk/api_client.rb +391 -0
- data/lib/octo/server/channel/adapters/dingtalk/stream_client.rb +203 -0
- data/lib/octo/server/channel/adapters/discord/adapter.rb +229 -0
- data/lib/octo/server/channel/adapters/discord/api_client.rb +107 -0
- data/lib/octo/server/channel/adapters/discord/gateway_client.rb +270 -0
- data/lib/octo/server/channel/adapters/feishu/adapter.rb +320 -0
- data/lib/octo/server/channel/adapters/feishu/bot.rb +478 -0
- data/lib/octo/server/channel/adapters/feishu/file_processor.rb +36 -0
- data/lib/octo/server/channel/adapters/feishu/message_parser.rb +129 -0
- data/lib/octo/server/channel/adapters/feishu/ws_client.rb +423 -0
- data/lib/octo/server/channel/adapters/telegram/adapter.rb +375 -0
- data/lib/octo/server/channel/adapters/telegram/api_client.rb +205 -0
- data/lib/octo/server/channel/adapters/wecom/adapter.rb +148 -0
- data/lib/octo/server/channel/adapters/wecom/media_downloader.rb +115 -0
- data/lib/octo/server/channel/adapters/wecom/ws_client.rb +395 -0
- data/lib/octo/server/channel/adapters/weixin/adapter.rb +692 -0
- data/lib/octo/server/channel/adapters/weixin/api_client.rb +402 -0
- data/lib/octo/server/channel/channel_config.rb +178 -0
- data/lib/octo/server/channel/channel_manager.rb +468 -0
- data/lib/octo/server/channel/channel_ui_controller.rb +224 -0
- data/lib/octo/server/channel.rb +33 -0
- data/lib/octo/server/discover.rb +77 -0
- data/lib/octo/server/epipe_safe_io.rb +105 -0
- data/lib/octo/server/http_server.rb +3554 -0
- data/lib/octo/server/scheduler.rb +317 -0
- data/lib/octo/server/server_master.rb +325 -0
- data/lib/octo/server/session_registry.rb +431 -0
- data/lib/octo/server/web_ui_controller.rb +487 -0
- data/lib/octo/session_manager.rb +385 -0
- data/lib/octo/skill.rb +466 -0
- data/lib/octo/skill_loader.rb +328 -0
- data/lib/octo/tools/base.rb +118 -0
- data/lib/octo/tools/browser.rb +625 -0
- data/lib/octo/tools/edit.rb +165 -0
- data/lib/octo/tools/file_reader.rb +549 -0
- data/lib/octo/tools/glob.rb +162 -0
- data/lib/octo/tools/grep.rb +356 -0
- data/lib/octo/tools/invoke_skill.rb +96 -0
- data/lib/octo/tools/list_tasks.rb +54 -0
- data/lib/octo/tools/redo_task.rb +41 -0
- data/lib/octo/tools/request_user_feedback.rb +84 -0
- data/lib/octo/tools/security.rb +333 -0
- data/lib/octo/tools/terminal/output_cleaner.rb +63 -0
- data/lib/octo/tools/terminal/persistent_session.rb +268 -0
- data/lib/octo/tools/terminal/safe_rm.sh +106 -0
- data/lib/octo/tools/terminal/session_manager.rb +213 -0
- data/lib/octo/tools/terminal.rb +1828 -0
- data/lib/octo/tools/todo_manager.rb +374 -0
- data/lib/octo/tools/trash_manager.rb +388 -0
- data/lib/octo/tools/undo_task.rb +35 -0
- data/lib/octo/tools/web_fetch.rb +242 -0
- data/lib/octo/tools/web_search.rb +260 -0
- data/lib/octo/tools/write.rb +77 -0
- data/lib/octo/ui2/block_font.rb +10 -0
- data/lib/octo/ui2/components/base_component.rb +163 -0
- data/lib/octo/ui2/components/command_suggestions.rb +290 -0
- data/lib/octo/ui2/components/common_component.rb +96 -0
- data/lib/octo/ui2/components/inline_input.rb +226 -0
- data/lib/octo/ui2/components/input_area.rb +1338 -0
- data/lib/octo/ui2/components/message_component.rb +99 -0
- data/lib/octo/ui2/components/modal_component.rb +419 -0
- data/lib/octo/ui2/components/todo_area.rb +149 -0
- data/lib/octo/ui2/components/tool_component.rb +107 -0
- data/lib/octo/ui2/components/welcome_banner.rb +139 -0
- data/lib/octo/ui2/layout_manager.rb +807 -0
- data/lib/octo/ui2/line_editor.rb +363 -0
- data/lib/octo/ui2/markdown_renderer.rb +100 -0
- data/lib/octo/ui2/output_buffer.rb +370 -0
- data/lib/octo/ui2/progress_handle.rb +362 -0
- data/lib/octo/ui2/progress_indicator.rb +55 -0
- data/lib/octo/ui2/screen_buffer.rb +273 -0
- data/lib/octo/ui2/terminal_detector.rb +119 -0
- data/lib/octo/ui2/theme_manager.rb +85 -0
- data/lib/octo/ui2/themes/base_theme.rb +105 -0
- data/lib/octo/ui2/themes/hacker_theme.rb +62 -0
- data/lib/octo/ui2/themes/minimal_theme.rb +56 -0
- data/lib/octo/ui2/thinking_verbs.rb +26 -0
- data/lib/octo/ui2/ui_controller.rb +1625 -0
- data/lib/octo/ui2/view_renderer.rb +177 -0
- data/lib/octo/ui2.rb +40 -0
- data/lib/octo/ui_interface.rb +154 -0
- data/lib/octo/utils/arguments_parser.rb +191 -0
- data/lib/octo/utils/browser_detector.rb +195 -0
- data/lib/octo/utils/encoding.rb +92 -0
- data/lib/octo/utils/environment_detector.rb +140 -0
- data/lib/octo/utils/file_ignore_helper.rb +170 -0
- data/lib/octo/utils/file_processor.rb +601 -0
- data/lib/octo/utils/gitignore_parser.rb +154 -0
- data/lib/octo/utils/limit_stack.rb +152 -0
- data/lib/octo/utils/logger.rb +124 -0
- data/lib/octo/utils/login_shell.rb +72 -0
- data/lib/octo/utils/model_pricing.rb +646 -0
- data/lib/octo/utils/parser_manager.rb +165 -0
- data/lib/octo/utils/path_helper.rb +15 -0
- data/lib/octo/utils/scripts_manager.rb +59 -0
- data/lib/octo/utils/string_matcher.rb +158 -0
- data/lib/octo/utils/trash_directory.rb +112 -0
- data/lib/octo/utils/workspace_rules.rb +46 -0
- data/lib/octo/version.rb +5 -0
- data/lib/octo/web/app.css +7141 -0
- data/lib/octo/web/app.js +543 -0
- data/lib/octo/web/apple-touch-icon.png +0 -0
- data/lib/octo/web/auth.js +150 -0
- data/lib/octo/web/channels.js +276 -0
- data/lib/octo/web/datepicker.js +205 -0
- data/lib/octo/web/favicon.png +0 -0
- data/lib/octo/web/i18n.js +1073 -0
- data/lib/octo/web/icon-512.png +0 -0
- data/lib/octo/web/icon-dark.svg +25 -0
- data/lib/octo/web/icon.svg +29 -0
- data/lib/octo/web/index.html +871 -0
- data/lib/octo/web/marked.min.js +69 -0
- data/lib/octo/web/onboard.js +491 -0
- data/lib/octo/web/profile.js +442 -0
- data/lib/octo/web/sessions.js +4421 -0
- data/lib/octo/web/settings.js +913 -0
- data/lib/octo/web/sidebar.js +32 -0
- data/lib/octo/web/skills.js +885 -0
- data/lib/octo/web/tasks.js +297 -0
- data/lib/octo/web/theme.js +105 -0
- data/lib/octo/web/trash.js +343 -0
- data/lib/octo/web/vendor/hljs/highlight.min.js +1244 -0
- data/lib/octo/web/vendor/hljs/hljs-theme.css +95 -0
- data/lib/octo/web/vendor/katex/auto-render.min.js +1 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/katex.min.css +1 -0
- data/lib/octo/web/vendor/katex/katex.min.js +1 -0
- data/lib/octo/web/version.js +449 -0
- data/lib/octo/web/weixin-qr.html +209 -0
- data/lib/octo/web/ws-dispatcher.js +357 -0
- data/lib/octo/web/ws.js +128 -0
- data/lib/octo.rb +145 -0
- data/scripts/build/build.sh +329 -0
- data/scripts/build/lib/apt.sh +56 -0
- data/scripts/build/lib/brew.sh +89 -0
- data/scripts/build/lib/colors.sh +17 -0
- data/scripts/build/lib/gem.sh +95 -0
- data/scripts/build/lib/mise.sh +125 -0
- data/scripts/build/lib/network.sh +157 -0
- data/scripts/build/lib/os.sh +57 -0
- data/scripts/build/lib/shell.sh +37 -0
- data/scripts/build/src/install.sh.cc +174 -0
- data/scripts/build/src/install_browser.sh.cc +101 -0
- data/scripts/build/src/install_full.sh.cc +290 -0
- data/scripts/build/src/install_rails_deps.sh.cc +145 -0
- data/scripts/build/src/install_system_deps.sh.cc +123 -0
- data/scripts/build/src/uninstall.sh.cc +101 -0
- data/scripts/install.ps1 +532 -0
- data/scripts/install.sh +567 -0
- data/scripts/install_browser.sh +479 -0
- data/scripts/install_full.sh +838 -0
- data/scripts/install_rails_deps.sh +746 -0
- data/scripts/install_system_deps.sh +518 -0
- data/scripts/uninstall.sh +287 -0
- data/sig/octo.rbs +4 -0
- metadata +614 -0
|
@@ -0,0 +1,534 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Octo
|
|
4
|
+
# Built-in model provider presets
|
|
5
|
+
# Provides default configurations for supported AI model providers
|
|
6
|
+
module Providers
|
|
7
|
+
# Provider preset definitions
|
|
8
|
+
# Each preset includes:
|
|
9
|
+
# - name: Human-readable provider name
|
|
10
|
+
# - base_url: Default API endpoint
|
|
11
|
+
# - api: API type (anthropic-messages, openai-responses, openai-completions)
|
|
12
|
+
# - default_model: Recommended default model
|
|
13
|
+
# - capabilities (optional): provider-level capability hash (e.g.
|
|
14
|
+
# { "vision" => false }). Applies to all models under this provider
|
|
15
|
+
# unless overridden by model_capabilities below.
|
|
16
|
+
# - model_capabilities (optional): per-model capability override map,
|
|
17
|
+
# { "<model_name>" => { "<cap>" => bool, ... } }. Use this when a
|
|
18
|
+
# single provider hosts models with different capabilities (e.g.
|
|
19
|
+
# octo hosts both vision-capable Claude and text-only DeepSeek).
|
|
20
|
+
# - model_api_overrides (optional): per-model API-type override map,
|
|
21
|
+
# { <Regexp|String> => "anthropic-messages" | "openai-completions" | ... }.
|
|
22
|
+
# Keys can be a plain model name or a Regexp matched against the model.
|
|
23
|
+
# The first key that matches wins; if none match, the provider's top-level
|
|
24
|
+
# "api" is used. Used so e.g. OpenRouter can keep "openai-responses" as
|
|
25
|
+
# its default while routing Claude models through the native Anthropic
|
|
26
|
+
# endpoint (which preserves cache_control fidelity).
|
|
27
|
+
PRESETS = {
|
|
28
|
+
"openrouter" => {
|
|
29
|
+
"name" => "OpenRouter",
|
|
30
|
+
"base_url" => "https://openrouter.ai/api/v1",
|
|
31
|
+
"api" => "openai-responses",
|
|
32
|
+
"default_model" => "anthropic/claude-sonnet-4-6",
|
|
33
|
+
# Curated default lineup. OpenRouter's full catalogue is enormous
|
|
34
|
+
# (hundreds of models) and the live /models endpoint isn't always
|
|
35
|
+
# reachable from every region — shipping a small list of the
|
|
36
|
+
# mainstream Claude + GPT entries gives users a working dropdown
|
|
37
|
+
# out of the box. Users can still type any other OpenRouter model
|
|
38
|
+
# ID manually; this list only seeds the picker.
|
|
39
|
+
"models" => [
|
|
40
|
+
"anthropic/claude-sonnet-4-6",
|
|
41
|
+
"anthropic/claude-opus-4-7",
|
|
42
|
+
"anthropic/claude-opus-4-6",
|
|
43
|
+
"anthropic/claude-haiku-4-5",
|
|
44
|
+
"openai/gpt-5.5",
|
|
45
|
+
"openai/gpt-5.4",
|
|
46
|
+
"openai/gpt-5.4-mini"
|
|
47
|
+
],
|
|
48
|
+
# Per-primary lite pairing — Claude family pairs with Haiku, GPT
|
|
49
|
+
# family pairs with the mini variant. Mirrors the octo and
|
|
50
|
+
# openai presets above so subagents on OpenRouter get a sensible
|
|
51
|
+
# cheap/fast sidekick automatically.
|
|
52
|
+
"lite_models" => {
|
|
53
|
+
"anthropic/claude-sonnet-4-6" => "anthropic/claude-haiku-4-5",
|
|
54
|
+
"anthropic/claude-opus-4-7" => "anthropic/claude-haiku-4-5",
|
|
55
|
+
"anthropic/claude-opus-4-6" => "anthropic/claude-haiku-4-5",
|
|
56
|
+
"openai/gpt-5.5" => "openai/gpt-5.4-mini",
|
|
57
|
+
"openai/gpt-5.4" => "openai/gpt-5.4-mini"
|
|
58
|
+
},
|
|
59
|
+
# Fallback chain for degraded-endpoint scenarios. When the primary
|
|
60
|
+
# returns repeated 503/429 errors, the agent temporarily switches to
|
|
61
|
+
# the fallback model to keep sessions alive.
|
|
62
|
+
"fallback_models" => {
|
|
63
|
+
"anthropic/claude-sonnet-4-6" => "anthropic/claude-haiku-4-5",
|
|
64
|
+
"anthropic/claude-opus-4-7" => "anthropic/claude-sonnet-4-6",
|
|
65
|
+
"anthropic/claude-opus-4-6" => "anthropic/claude-sonnet-4-6",
|
|
66
|
+
"openai/gpt-5.5" => "openai/gpt-5.4-mini",
|
|
67
|
+
"openai/gpt-5.4" => "openai/gpt-5.4-mini"
|
|
68
|
+
},
|
|
69
|
+
# Per-model API type overrides. Matched by Regexp against the model name.
|
|
70
|
+
# Why this exists: OpenRouter proxies Claude via both its OpenAI-compatible
|
|
71
|
+
# /chat/completions endpoint AND a native Anthropic /v1/messages endpoint.
|
|
72
|
+
# The OpenAI shim is lossy for Claude's cache_control semantics — prefix
|
|
73
|
+
# rewrites inside the proxy cause ~10% prompt-cache misses. Pinning
|
|
74
|
+
# "anthropic/*" (and any direct "claude-*" alias) to the native Anthropic
|
|
75
|
+
# endpoint preserves cache_control byte-for-byte and matches what Claude
|
|
76
|
+
# Code CLI does internally. Non-Claude models (Gemini, GPT, etc.) keep
|
|
77
|
+
# the OpenAI shim — that's what OpenRouter documents as their primary.
|
|
78
|
+
"model_api_overrides" => {
|
|
79
|
+
/\Aanthropic\// => "anthropic-messages",
|
|
80
|
+
/\Aclaude[-.]/ => "anthropic-messages"
|
|
81
|
+
}.freeze,
|
|
82
|
+
"website_url" => "https://openrouter.ai/keys"
|
|
83
|
+
}.freeze,
|
|
84
|
+
|
|
85
|
+
"deepseekv4" => {
|
|
86
|
+
"name" => "DeepSeek V4",
|
|
87
|
+
# DeepSeek API is compatible with both OpenAI and Anthropic formats.
|
|
88
|
+
# We use the OpenAI-compatible endpoint here (matches kimi/minimax/glm style).
|
|
89
|
+
# For Anthropic-format usage, point base_url at https://api.deepseek.com/anthropic
|
|
90
|
+
# and change "api" to "anthropic-messages".
|
|
91
|
+
"base_url" => "https://api.deepseek.com",
|
|
92
|
+
"api" => "openai-completions",
|
|
93
|
+
"default_model" => "deepseek-v4-pro",
|
|
94
|
+
"lite_model" => "deepseek-v4-flash",
|
|
95
|
+
# Note: deepseek-chat and deepseek-reasoner are legacy aliases being
|
|
96
|
+
# deprecated on 2026-07-24; they map to deepseek-v4-flash's non-thinking
|
|
97
|
+
# and thinking modes respectively. Prefer deepseek-v4-flash / deepseek-v4-pro.
|
|
98
|
+
"models" => [
|
|
99
|
+
"deepseek-v4-flash",
|
|
100
|
+
"deepseek-v4-pro",
|
|
101
|
+
],
|
|
102
|
+
# DeepSeek V4 API does not accept image inputs — text-only across all models.
|
|
103
|
+
"capabilities" => { "vision" => false }.freeze,
|
|
104
|
+
"website_url" => "https://platform.deepseek.com/api_keys"
|
|
105
|
+
}.freeze,
|
|
106
|
+
|
|
107
|
+
"minimax" => {
|
|
108
|
+
"name" => "Minimax",
|
|
109
|
+
"base_url" => "https://api.minimaxi.com/v1",
|
|
110
|
+
"api" => "openai-completions",
|
|
111
|
+
"default_model" => "MiniMax-M2.7",
|
|
112
|
+
"models" => ["MiniMax-M2.5", "MiniMax-M2.7"],
|
|
113
|
+
# MiniMax operates two regional endpoints with identical APIs & model
|
|
114
|
+
# lineup — mainland China (.com) and international (.io). Listing both
|
|
115
|
+
# lets find_by_base_url identify either one as provider "minimax",
|
|
116
|
+
# so capability checks (vision=false) fire correctly regardless of
|
|
117
|
+
# which endpoint the user configured.
|
|
118
|
+
"endpoint_variants" => [
|
|
119
|
+
{ "label" => "Mainland China", "label_key" => "settings.models.baseurl.variant.mainland_cn", "base_url" => "https://api.minimaxi.com/v1", "region" => "cn" }.freeze,
|
|
120
|
+
{ "label" => "International", "label_key" => "settings.models.baseurl.variant.international", "base_url" => "https://api.minimax.io/v1", "region" => "intl" }.freeze
|
|
121
|
+
].freeze,
|
|
122
|
+
# MiniMax M2.x does not support multimodal/vision input on this endpoint.
|
|
123
|
+
"capabilities" => { "vision" => false }.freeze,
|
|
124
|
+
"website_url" => "https://www.minimaxi.com/user-center/basic-information/interface-key"
|
|
125
|
+
}.freeze,
|
|
126
|
+
|
|
127
|
+
"kimi" => {
|
|
128
|
+
"name" => "Kimi (Moonshot)",
|
|
129
|
+
"base_url" => "https://api.moonshot.cn/v1",
|
|
130
|
+
"api" => "openai-completions",
|
|
131
|
+
"default_model" => "kimi-k2.6",
|
|
132
|
+
"models" => ["kimi-k2.6", "kimi-k2.5"],
|
|
133
|
+
# Moonshot operates two regional endpoints with identical APIs & model
|
|
134
|
+
# lineup — mainland China (.cn) and international (.ai). These are the
|
|
135
|
+
# pay-as-you-go Open Platform endpoints; the subscription-billed
|
|
136
|
+
# Coding Plan lives at api.kimi.com/coding with the unified
|
|
137
|
+
# `kimi-for-coding` model alias and is exposed as a separate
|
|
138
|
+
# top-level "kimi-coding" preset (different domain, distinct billing
|
|
139
|
+
# model, marketed by Moonshot as the standalone Kimi Code product).
|
|
140
|
+
# Listing both PAYG variants here lets find_by_base_url identify
|
|
141
|
+
# either one as provider "kimi", so downstream capability checks,
|
|
142
|
+
# fallback chains, and provider-specific behaviours work regardless
|
|
143
|
+
# of which endpoint the user configured.
|
|
144
|
+
"endpoint_variants" => [
|
|
145
|
+
{ "label" => "Mainland China", "label_key" => "settings.models.baseurl.variant.mainland_cn", "base_url" => "https://api.moonshot.cn/v1", "region" => "cn" }.freeze,
|
|
146
|
+
{ "label" => "International", "label_key" => "settings.models.baseurl.variant.international", "base_url" => "https://api.moonshot.ai/v1", "region" => "intl" }.freeze
|
|
147
|
+
].freeze,
|
|
148
|
+
# k2.5 / k2.6 are multimodal; legacy k2 text-only models need model_capabilities override if added.
|
|
149
|
+
"capabilities" => { "vision" => true }.freeze,
|
|
150
|
+
"website_url" => "https://platform.moonshot.cn/console/api-keys"
|
|
151
|
+
}.freeze,
|
|
152
|
+
|
|
153
|
+
"kimi-coding" => {
|
|
154
|
+
"name" => "Kimi Code (Coding Plan)",
|
|
155
|
+
# Subscription-billed Kimi Code endpoint — separate product from the
|
|
156
|
+
# PAYG Moonshot Open Platform (api.moonshot.cn/v1 / .ai/v1). Uses the
|
|
157
|
+
# unified `kimi-for-coding` model alias which the Coding Plan backend
|
|
158
|
+
# routes to the appropriate K2 variant (Kimi-k2.6 today; 262K context,
|
|
159
|
+
# 32K max output, supports vision/video/reasoning).
|
|
160
|
+
#
|
|
161
|
+
# Why anthropic-messages: Moonshot exposes the Coding Plan via two
|
|
162
|
+
# URLs on the same domain — an Anthropic-format endpoint at
|
|
163
|
+
# api.kimi.com/coding/ (used by Claude Code via ANTHROPIC_BASE_URL)
|
|
164
|
+
# and an OpenAI-compatible endpoint at api.kimi.com/coding/v1 (used
|
|
165
|
+
# by Roo Code etc.). We route through anthropic-messages so
|
|
166
|
+
# cache_control fields round-trip byte-for-byte (the OpenAI shim is
|
|
167
|
+
# lossy for cache_control semantics — see OpenRouter preset above
|
|
168
|
+
# for the same reason). Verified against the live endpoint: response
|
|
169
|
+
# payload includes cache_creation_input_tokens / cache_read_input_tokens,
|
|
170
|
+
# so the cache layer is real on this backend.
|
|
171
|
+
#
|
|
172
|
+
# User-Agent gate: this endpoint enforces a UA-prefix whitelist
|
|
173
|
+
# limited to first-party coding agents (Kimi CLI, Claude Code, Roo
|
|
174
|
+
# Code, Kilo Code, ...). Requests carrying octo's default
|
|
175
|
+
# Faraday UA are rejected with HTTP 403 access_terminated_error.
|
|
176
|
+
# Client#anthropic_connection injects a Claude Code-shaped UA when
|
|
177
|
+
# @provider_id == "kimi-coding" — see the comment in client.rb for
|
|
178
|
+
# the policy rationale.
|
|
179
|
+
#
|
|
180
|
+
# Source: https://www.kimi.com/code/docs/third-party-tools/other-coding-agents.html
|
|
181
|
+
"base_url" => "https://api.kimi.com/coding",
|
|
182
|
+
"api" => "anthropic-messages",
|
|
183
|
+
"default_model" => "kimi-for-coding",
|
|
184
|
+
"models" => ["kimi-for-coding"],
|
|
185
|
+
# K2.6 backend behind the alias is multimodal (image + video input,
|
|
186
|
+
# reasoning). Same vision capability as the PAYG kimi preset.
|
|
187
|
+
"capabilities" => { "vision" => true }.freeze,
|
|
188
|
+
"website_url" => "https://www.kimi.com/code"
|
|
189
|
+
}.freeze,
|
|
190
|
+
|
|
191
|
+
"anthropic" => {
|
|
192
|
+
"name" => "Anthropic (Claude)",
|
|
193
|
+
"base_url" => "https://api.anthropic.com",
|
|
194
|
+
"api" => "anthropic-messages",
|
|
195
|
+
"default_model" => "claude-sonnet-4.6",
|
|
196
|
+
"models" => ["claude-opus-4-7", "claude-opus-4-6", "claude-sonnet-4.6", "claude-haiku-4.5"],
|
|
197
|
+
"website_url" => "https://console.anthropic.com/settings/keys"
|
|
198
|
+
}.freeze,
|
|
199
|
+
|
|
200
|
+
"mimo" => {
|
|
201
|
+
"name" => "MiMo (Xiaomi)",
|
|
202
|
+
"base_url" => "https://api.xiaomimimo.com/v1",
|
|
203
|
+
"api" => "openai-completions",
|
|
204
|
+
"default_model" => "mimo-v2.5-pro",
|
|
205
|
+
"models" => ["mimo-v2.5-pro", "mimo-v2-pro", "mimo-v2-omni"],
|
|
206
|
+
# MiMo-V2-Pro is text-only; MiMo-V2-Omni supports vision (omni = multimodal).
|
|
207
|
+
"capabilities" => { "vision" => false }.freeze,
|
|
208
|
+
"model_capabilities" => {
|
|
209
|
+
"mimo-v2-omni" => { "vision" => true }.freeze
|
|
210
|
+
}.freeze,
|
|
211
|
+
"website_url" => "https://platform.xiaomimimo.com/"
|
|
212
|
+
}.freeze,
|
|
213
|
+
|
|
214
|
+
"glm" => {
|
|
215
|
+
"name" => "GLM (Z.ai / Zhipu)",
|
|
216
|
+
"base_url" => "https://open.bigmodel.cn/api/paas/v4",
|
|
217
|
+
"api" => "openai-completions",
|
|
218
|
+
"default_model" => "glm-5.1",
|
|
219
|
+
"models" => ["glm-5.1", "glm-5", "glm-5-turbo", "glm-5v-turbo", "glm-4.7"],
|
|
220
|
+
# Zhipu / Z.ai expose four functionally-equivalent endpoints:
|
|
221
|
+
# two regional sites (mainland open.bigmodel.cn + international api.z.ai)
|
|
222
|
+
# each with a general-billing and a Coding-Plan subpath. They share the
|
|
223
|
+
# same model lineup & identical capability profile, so a single preset
|
|
224
|
+
# with endpoint_variants is the right shape — one source of truth for
|
|
225
|
+
# vision/model_capabilities, four URLs recognised by find_by_base_url.
|
|
226
|
+
# Without this, users pointing at api.z.ai or the /coding/ path fell
|
|
227
|
+
# through to the conservative "assume vision=true" default and got
|
|
228
|
+
# hallucinated image descriptions on text-only GLM models (C-5563).
|
|
229
|
+
"endpoint_variants" => [
|
|
230
|
+
{ "label" => "Mainland · Pay-as-you-go", "label_key" => "settings.models.baseurl.variant.mainland_cn_payg", "base_url" => "https://open.bigmodel.cn/api/paas/v4", "region" => "cn" }.freeze,
|
|
231
|
+
{ "label" => "Mainland · Coding Plan", "label_key" => "settings.models.baseurl.variant.mainland_cn_coding", "base_url" => "https://open.bigmodel.cn/api/coding/paas/v4", "region" => "cn" }.freeze,
|
|
232
|
+
{ "label" => "International · Pay-as-you-go", "label_key" => "settings.models.baseurl.variant.international_payg", "base_url" => "https://api.z.ai/api/paas/v4", "region" => "intl" }.freeze,
|
|
233
|
+
{ "label" => "International · Coding Plan", "label_key" => "settings.models.baseurl.variant.international_coding","base_url" => "https://api.z.ai/api/coding/paas/v4", "region" => "intl" }.freeze
|
|
234
|
+
].freeze,
|
|
235
|
+
# GLM models are text-only except glm-5v-turbo which is vision-capable ("v" = visual).
|
|
236
|
+
"capabilities" => { "vision" => false }.freeze,
|
|
237
|
+
"model_capabilities" => {
|
|
238
|
+
"glm-5v-turbo" => { "vision" => true }.freeze
|
|
239
|
+
}.freeze,
|
|
240
|
+
"website_url" => "https://open.bigmodel.cn/usercenter/apikeys"
|
|
241
|
+
}.freeze,
|
|
242
|
+
|
|
243
|
+
"openai" => {
|
|
244
|
+
"name" => "OpenAI (GPT)",
|
|
245
|
+
"base_url" => "https://api.openai.com/v1",
|
|
246
|
+
"api" => "openai-completions",
|
|
247
|
+
"default_model" => "gpt-5.5",
|
|
248
|
+
"models" => [
|
|
249
|
+
"gpt-5.5",
|
|
250
|
+
"gpt-5.4",
|
|
251
|
+
"gpt-5.4-mini",
|
|
252
|
+
"gpt-5.4-nano",
|
|
253
|
+
"o4-mini",
|
|
254
|
+
"o3"
|
|
255
|
+
],
|
|
256
|
+
# GPT-5.x and o-series models are multimodal (text + image input).
|
|
257
|
+
"capabilities" => { "vision" => true }.freeze,
|
|
258
|
+
# Per-primary lite pairing: subagents use mini/nano for cheap/fast work.
|
|
259
|
+
# o4-mini and o3 are reasoning models without a lite-tier sibling here.
|
|
260
|
+
"lite_models" => {
|
|
261
|
+
"gpt-5.5" => "gpt-5.4-mini",
|
|
262
|
+
"gpt-5.4" => "gpt-5.4-mini"
|
|
263
|
+
},
|
|
264
|
+
"website_url" => "https://platform.openai.com/api-keys"
|
|
265
|
+
}.freeze,
|
|
266
|
+
|
|
267
|
+
"qwen" => {
|
|
268
|
+
"name" => "Qwen (Alibaba)",
|
|
269
|
+
"base_url" => "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
|
270
|
+
"api" => "openai-completions",
|
|
271
|
+
"default_model" => "qwen3.6-plus",
|
|
272
|
+
"models" => [
|
|
273
|
+
"qwen3.6-plus",
|
|
274
|
+
"qwen3.6-max",
|
|
275
|
+
"qwen3.6-27b",
|
|
276
|
+
"qwen3.6-flash",
|
|
277
|
+
"qwen-plus-latest",
|
|
278
|
+
"qwen-vl-plus",
|
|
279
|
+
"qwen-vl-max"
|
|
280
|
+
],
|
|
281
|
+
"endpoint_variants" => [
|
|
282
|
+
{ "label" => "Mainland China", "label_key" => "settings.models.baseurl.variant.mainland_cn", "base_url" => "https://dashscope.aliyuncs.com/compatible-mode/v1", "region" => "cn" }.freeze,
|
|
283
|
+
{ "label" => "Singapore", "label_key" => "settings.models.baseurl.variant.international", "base_url" => "https://dashscope-intl.aliyuncs.com/compatible-mode/v1", "region" => "intl" }.freeze,
|
|
284
|
+
{ "label" => "US (Virginia)", "label_key" => "settings.models.baseurl.variant.us", "base_url" => "https://dashscope-us.aliyuncs.com/compatible-mode/v1", "region" => "us" }.freeze
|
|
285
|
+
].freeze,
|
|
286
|
+
"capabilities" => { "vision" => false }.freeze,
|
|
287
|
+
"model_capabilities" => {
|
|
288
|
+
"qwen3.6-27b" => { "vision" => true }.freeze,
|
|
289
|
+
"qwen-vl-plus" => { "vision" => true }.freeze,
|
|
290
|
+
"qwen-vl-max" => { "vision" => true }.freeze
|
|
291
|
+
}.freeze,
|
|
292
|
+
"lite_models" => {
|
|
293
|
+
"qwen3.6-plus" => "qwen3.6-flash",
|
|
294
|
+
"qwen3.6-max" => "qwen3.6-flash",
|
|
295
|
+
"qwen3.6-27b" => "qwen3.6-flash",
|
|
296
|
+
"qwen-plus-latest" => "qwen3.6-flash"
|
|
297
|
+
},
|
|
298
|
+
"website_url" => "https://bailian.console.aliyun.com/?apiKey=1"
|
|
299
|
+
}.freeze
|
|
300
|
+
|
|
301
|
+
}.freeze
|
|
302
|
+
|
|
303
|
+
class << self
|
|
304
|
+
# Check if a provider preset exists
|
|
305
|
+
# @param provider_id [String] The provider identifier (e.g., "anthropic", "openrouter")
|
|
306
|
+
# @return [Boolean] True if the preset exists
|
|
307
|
+
def exists?(provider_id)
|
|
308
|
+
PRESETS.key?(provider_id)
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
# Get a provider preset by ID
|
|
312
|
+
# @param provider_id [String] The provider identifier
|
|
313
|
+
# @return [Hash, nil] The preset configuration or nil if not found
|
|
314
|
+
def get(provider_id)
|
|
315
|
+
PRESETS[provider_id]
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# Get the default model for a provider
|
|
319
|
+
# @param provider_id [String] The provider identifier
|
|
320
|
+
# @return [String, nil] The default model name or nil if provider not found
|
|
321
|
+
def default_model(provider_id)
|
|
322
|
+
preset = PRESETS[provider_id]
|
|
323
|
+
preset&.dig("default_model")
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
# Get the base URL for a provider
|
|
327
|
+
# @param provider_id [String] The provider identifier
|
|
328
|
+
# @return [String, nil] The base URL or nil if provider not found
|
|
329
|
+
def base_url(provider_id)
|
|
330
|
+
preset = PRESETS[provider_id]
|
|
331
|
+
preset&.dig("base_url")
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
# Get the API type for a provider
|
|
335
|
+
# @param provider_id [String] The provider identifier
|
|
336
|
+
# @return [String, nil] The API type or nil if provider not found
|
|
337
|
+
def api_type(provider_id)
|
|
338
|
+
preset = PRESETS[provider_id]
|
|
339
|
+
preset&.dig("api")
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
# Resolve the API type for a specific provider+model pair.
|
|
343
|
+
#
|
|
344
|
+
# Resolution order:
|
|
345
|
+
# 1. PRESETS[provider_id]["model_api_overrides"] — first key (String or
|
|
346
|
+
# Regexp) that matches the model name wins.
|
|
347
|
+
# 2. PRESETS[provider_id]["api"] — the provider-wide default.
|
|
348
|
+
# 3. nil — unknown provider.
|
|
349
|
+
#
|
|
350
|
+
# Use this instead of api_type when you need the precise transport for a
|
|
351
|
+
# given model (e.g. routing OpenRouter's Claude requests to the native
|
|
352
|
+
# /v1/messages endpoint to preserve prompt-cache fidelity).
|
|
353
|
+
#
|
|
354
|
+
# @param provider_id [String] The provider identifier
|
|
355
|
+
# @param model_name [String, nil] The specific model name
|
|
356
|
+
# @return [String, nil] The API type (e.g. "anthropic-messages")
|
|
357
|
+
def api_type_for_model(provider_id, model_name)
|
|
358
|
+
preset = PRESETS[provider_id]
|
|
359
|
+
return nil unless preset
|
|
360
|
+
|
|
361
|
+
overrides = preset["model_api_overrides"]
|
|
362
|
+
if overrides.is_a?(Hash) && model_name
|
|
363
|
+
name = model_name.to_s
|
|
364
|
+
matched = overrides.find do |pattern, _api|
|
|
365
|
+
case pattern
|
|
366
|
+
when Regexp then pattern.match?(name)
|
|
367
|
+
when String then pattern == name
|
|
368
|
+
else false
|
|
369
|
+
end
|
|
370
|
+
end
|
|
371
|
+
return matched[1] if matched
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
preset["api"]
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
# Returns true when the provider+model should be talked to using the
|
|
378
|
+
# native Anthropic /v1/messages format. This is the single source of
|
|
379
|
+
# truth for deciding anthropic_format at Client construction time.
|
|
380
|
+
# @param provider_id [String] The provider identifier
|
|
381
|
+
# @param model_name [String, nil] The specific model name
|
|
382
|
+
# @return [Boolean]
|
|
383
|
+
def anthropic_format_for_model?(provider_id, model_name)
|
|
384
|
+
api_type_for_model(provider_id, model_name) == "anthropic-messages"
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
# List all available provider IDs
|
|
388
|
+
# @return [Array<String>] List of provider identifiers
|
|
389
|
+
def provider_ids
|
|
390
|
+
PRESETS.keys
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
# List all available providers with their names
|
|
394
|
+
# @return [Array<Array(String, String)>] Array of [id, name] pairs
|
|
395
|
+
def list
|
|
396
|
+
PRESETS.map { |id, config| [id, config["name"]] }
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
# Get available models for a provider
|
|
400
|
+
# @param provider_id [String] The provider identifier
|
|
401
|
+
# @return [Array<String>] List of model names (empty if dynamic)
|
|
402
|
+
def models(provider_id)
|
|
403
|
+
preset = PRESETS[provider_id]
|
|
404
|
+
preset&.dig("models") || []
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
# Get the lite model for a provider.
|
|
408
|
+
# @param provider_id [String] The provider identifier
|
|
409
|
+
# @param primary_model [String, nil] The currently-selected primary model name.
|
|
410
|
+
# When given, look it up in the provider's `lite_models` table first
|
|
411
|
+
# (so one provider can host multiple model families, each with its own
|
|
412
|
+
# lite sidekick — e.g. Claude Opus/Sonnet → Haiku, DeepSeek Pro → Flash).
|
|
413
|
+
# Falls back to the global `lite_model` field for old-style presets
|
|
414
|
+
# (e.g. deepseekv4) that declare a single provider-wide lite.
|
|
415
|
+
# @return [String, nil] The lite model name, or nil when the primary is
|
|
416
|
+
# already lite-class (no entry) and no global `lite_model` is defined.
|
|
417
|
+
def lite_model(provider_id, primary_model = nil)
|
|
418
|
+
preset = PRESETS[provider_id]
|
|
419
|
+
return nil unless preset
|
|
420
|
+
|
|
421
|
+
if primary_model && preset["lite_models"].is_a?(Hash)
|
|
422
|
+
mapped = preset["lite_models"][primary_model]
|
|
423
|
+
return mapped if mapped
|
|
424
|
+
# When a `lite_models` table is defined but the current primary
|
|
425
|
+
# isn't listed, it means the primary is already a lite-class model
|
|
426
|
+
# (e.g. haiku / v4-flash) — do NOT fall back to the legacy single
|
|
427
|
+
# field, because that would incorrectly inject a lite for a model
|
|
428
|
+
# that doesn't need one.
|
|
429
|
+
return nil if preset["lite_models"].any?
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
preset["lite_model"]
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
# Get the fallback model for a given model within a provider.
|
|
436
|
+
# Returns nil if no fallback is defined for that model.
|
|
437
|
+
# @param provider_id [String] The provider identifier
|
|
438
|
+
# @param model [String] The primary model name
|
|
439
|
+
# @return [String, nil] The fallback model name or nil
|
|
440
|
+
def fallback_model(provider_id, model)
|
|
441
|
+
preset = PRESETS[provider_id]
|
|
442
|
+
preset&.dig("fallback_models", model)
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
# Find provider ID by base URL.
|
|
446
|
+
# Matches if the given URL starts with the provider's base_url (after normalisation),
|
|
447
|
+
# so both exact matches and sub-path variants (e.g. "/v1") are recognised.
|
|
448
|
+
#
|
|
449
|
+
# Also scans `endpoint_variants` (when present) so providers that operate
|
|
450
|
+
# multiple regional / billing-plan endpoints under the same identity
|
|
451
|
+
# (e.g. GLM on open.bigmodel.cn + api.z.ai, MiniMax on .com + .io) are
|
|
452
|
+
# all recognised as that single provider — one capability definition,
|
|
453
|
+
# N entry URLs. Without this, users configured with a non-default
|
|
454
|
+
# variant fall back to the "unknown provider" path and miss capability
|
|
455
|
+
# enforcement (see C-5563).
|
|
456
|
+
# @param base_url [String] The base URL to look up
|
|
457
|
+
# @return [String, nil] The provider ID or nil if not found
|
|
458
|
+
def find_by_base_url(base_url)
|
|
459
|
+
return nil if base_url.nil? || base_url.empty?
|
|
460
|
+
normalized = base_url.to_s.chomp("/")
|
|
461
|
+
PRESETS.find do |_id, preset|
|
|
462
|
+
# Collect every URL this preset claims: the canonical base_url plus
|
|
463
|
+
# any declared endpoint_variants. Dedup so the canonical one showing
|
|
464
|
+
# up in both lists doesn't change behaviour.
|
|
465
|
+
candidates = [preset["base_url"]]
|
|
466
|
+
variants = preset["endpoint_variants"]
|
|
467
|
+
if variants.is_a?(Array)
|
|
468
|
+
variants.each { |v| candidates << v["base_url"] if v.is_a?(Hash) }
|
|
469
|
+
end
|
|
470
|
+
candidates.compact.uniq.any? do |candidate|
|
|
471
|
+
preset_base = candidate.to_s.chomp("/")
|
|
472
|
+
next false if preset_base.empty?
|
|
473
|
+
normalized == preset_base || normalized.start_with?("#{preset_base}/")
|
|
474
|
+
end
|
|
475
|
+
end&.first
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
# Resolve the provider id for a model entry by base_url.
|
|
479
|
+
#
|
|
480
|
+
# @param base_url [String, nil] the configured base_url
|
|
481
|
+
# @param api_key [String, nil] unused, kept for API compatibility
|
|
482
|
+
# @return [String, nil] provider id or nil if unresolvable
|
|
483
|
+
def resolve_provider(base_url: nil, api_key: nil)
|
|
484
|
+
find_by_base_url(base_url)
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
# Resolve the capabilities hash for a given provider+model.
|
|
488
|
+
#
|
|
489
|
+
# Resolution order (most specific wins):
|
|
490
|
+
# 1. PRESETS[provider_id]["model_capabilities"][model_name] — per-model
|
|
491
|
+
# override, used when a single provider hosts a mix of capabilities
|
|
492
|
+
# (e.g. OpenRouter serves both Claude [vision] and DeepSeek [text]).
|
|
493
|
+
# 2. PRESETS[provider_id]["capabilities"] — provider-wide defaults,
|
|
494
|
+
# used when the whole lineup shares the same capabilities.
|
|
495
|
+
# 3. {} — no declaration; callers get the conservative default (true)
|
|
496
|
+
# via `supports?`.
|
|
497
|
+
#
|
|
498
|
+
# Returns a plain Hash (always safe to inspect; never nil).
|
|
499
|
+
# @param provider_id [String] The provider identifier
|
|
500
|
+
# @param model_name [String, nil] Optional specific model for override lookup
|
|
501
|
+
# @return [Hash] capabilities mapping (e.g. { "vision" => true })
|
|
502
|
+
def capabilities(provider_id, model_name: nil)
|
|
503
|
+
preset = PRESETS[provider_id]
|
|
504
|
+
return {} unless preset
|
|
505
|
+
|
|
506
|
+
provider_caps = preset["capabilities"] || {}
|
|
507
|
+
return provider_caps.dup unless model_name
|
|
508
|
+
|
|
509
|
+
model_caps = preset.dig("model_capabilities", model_name) || {}
|
|
510
|
+
provider_caps.merge(model_caps)
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
# Check if a provider+model supports a capability.
|
|
514
|
+
# Unknown provider / missing capability declaration → returns true
|
|
515
|
+
# (conservative default: assume supported unless we explicitly say otherwise).
|
|
516
|
+
# This keeps custom base_urls working and avoids over-aggressive downgrades.
|
|
517
|
+
#
|
|
518
|
+
# @param provider_id [String] The provider identifier
|
|
519
|
+
# @param capability [String, Symbol] The capability name (e.g. :vision, "vision")
|
|
520
|
+
# @param model_name [String, nil] Optional specific model name
|
|
521
|
+
# @return [Boolean] true unless the preset explicitly says false
|
|
522
|
+
def supports?(provider_id, capability, model_name: nil)
|
|
523
|
+
preset = PRESETS[provider_id]
|
|
524
|
+
return true unless preset
|
|
525
|
+
|
|
526
|
+
key = capability.to_s
|
|
527
|
+
caps = capabilities(provider_id, model_name: model_name)
|
|
528
|
+
# When the capability is not declared at either level, default to true.
|
|
529
|
+
return true unless caps.key?(key)
|
|
530
|
+
caps[key] != false
|
|
531
|
+
end
|
|
532
|
+
end
|
|
533
|
+
end
|
|
534
|
+
end
|