codepp 0.0.437__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- code_puppy/__init__.py +10 -0
- code_puppy/__main__.py +10 -0
- code_puppy/agents/__init__.py +31 -0
- code_puppy/agents/agent_c_reviewer.py +155 -0
- code_puppy/agents/agent_code_puppy.py +117 -0
- code_puppy/agents/agent_code_reviewer.py +90 -0
- code_puppy/agents/agent_cpp_reviewer.py +132 -0
- code_puppy/agents/agent_creator_agent.py +638 -0
- code_puppy/agents/agent_golang_reviewer.py +151 -0
- code_puppy/agents/agent_helios.py +124 -0
- code_puppy/agents/agent_javascript_reviewer.py +160 -0
- code_puppy/agents/agent_manager.py +742 -0
- code_puppy/agents/agent_pack_leader.py +385 -0
- code_puppy/agents/agent_planning.py +165 -0
- code_puppy/agents/agent_python_programmer.py +169 -0
- code_puppy/agents/agent_python_reviewer.py +90 -0
- code_puppy/agents/agent_qa_expert.py +163 -0
- code_puppy/agents/agent_qa_kitten.py +208 -0
- code_puppy/agents/agent_scheduler.py +121 -0
- code_puppy/agents/agent_security_auditor.py +181 -0
- code_puppy/agents/agent_terminal_qa.py +323 -0
- code_puppy/agents/agent_typescript_reviewer.py +166 -0
- code_puppy/agents/base_agent.py +2156 -0
- code_puppy/agents/event_stream_handler.py +348 -0
- code_puppy/agents/json_agent.py +202 -0
- code_puppy/agents/pack/__init__.py +34 -0
- code_puppy/agents/pack/bloodhound.py +304 -0
- code_puppy/agents/pack/husky.py +327 -0
- code_puppy/agents/pack/retriever.py +393 -0
- code_puppy/agents/pack/shepherd.py +348 -0
- code_puppy/agents/pack/terrier.py +287 -0
- code_puppy/agents/pack/watchdog.py +367 -0
- code_puppy/agents/prompt_reviewer.py +145 -0
- code_puppy/agents/subagent_stream_handler.py +276 -0
- code_puppy/api/__init__.py +13 -0
- code_puppy/api/app.py +169 -0
- code_puppy/api/main.py +21 -0
- code_puppy/api/pty_manager.py +453 -0
- code_puppy/api/routers/__init__.py +12 -0
- code_puppy/api/routers/agents.py +36 -0
- code_puppy/api/routers/commands.py +217 -0
- code_puppy/api/routers/config.py +75 -0
- code_puppy/api/routers/sessions.py +234 -0
- code_puppy/api/templates/terminal.html +361 -0
- code_puppy/api/websocket.py +154 -0
- code_puppy/callbacks.py +692 -0
- code_puppy/chatgpt_codex_client.py +338 -0
- code_puppy/claude_cache_client.py +672 -0
- code_puppy/cli_runner.py +1073 -0
- code_puppy/command_line/__init__.py +1 -0
- code_puppy/command_line/add_model_menu.py +1092 -0
- code_puppy/command_line/agent_menu.py +662 -0
- code_puppy/command_line/attachments.py +395 -0
- code_puppy/command_line/autosave_menu.py +704 -0
- code_puppy/command_line/clipboard.py +527 -0
- code_puppy/command_line/colors_menu.py +532 -0
- code_puppy/command_line/command_handler.py +293 -0
- code_puppy/command_line/command_registry.py +150 -0
- code_puppy/command_line/config_commands.py +719 -0
- code_puppy/command_line/core_commands.py +867 -0
- code_puppy/command_line/diff_menu.py +865 -0
- code_puppy/command_line/file_path_completion.py +73 -0
- code_puppy/command_line/load_context_completion.py +52 -0
- code_puppy/command_line/mcp/__init__.py +10 -0
- code_puppy/command_line/mcp/base.py +32 -0
- code_puppy/command_line/mcp/catalog_server_installer.py +175 -0
- code_puppy/command_line/mcp/custom_server_form.py +688 -0
- code_puppy/command_line/mcp/custom_server_installer.py +195 -0
- code_puppy/command_line/mcp/edit_command.py +148 -0
- code_puppy/command_line/mcp/handler.py +138 -0
- code_puppy/command_line/mcp/help_command.py +147 -0
- code_puppy/command_line/mcp/install_command.py +214 -0
- code_puppy/command_line/mcp/install_menu.py +705 -0
- code_puppy/command_line/mcp/list_command.py +94 -0
- code_puppy/command_line/mcp/logs_command.py +235 -0
- code_puppy/command_line/mcp/remove_command.py +82 -0
- code_puppy/command_line/mcp/restart_command.py +100 -0
- code_puppy/command_line/mcp/search_command.py +123 -0
- code_puppy/command_line/mcp/start_all_command.py +135 -0
- code_puppy/command_line/mcp/start_command.py +117 -0
- code_puppy/command_line/mcp/status_command.py +184 -0
- code_puppy/command_line/mcp/stop_all_command.py +112 -0
- code_puppy/command_line/mcp/stop_command.py +80 -0
- code_puppy/command_line/mcp/test_command.py +107 -0
- code_puppy/command_line/mcp/utils.py +129 -0
- code_puppy/command_line/mcp/wizard_utils.py +334 -0
- code_puppy/command_line/mcp_completion.py +174 -0
- code_puppy/command_line/model_picker_completion.py +197 -0
- code_puppy/command_line/model_settings_menu.py +932 -0
- code_puppy/command_line/motd.py +96 -0
- code_puppy/command_line/onboarding_slides.py +179 -0
- code_puppy/command_line/onboarding_wizard.py +342 -0
- code_puppy/command_line/pin_command_completion.py +329 -0
- code_puppy/command_line/prompt_toolkit_completion.py +846 -0
- code_puppy/command_line/session_commands.py +302 -0
- code_puppy/command_line/shell_passthrough.py +145 -0
- code_puppy/command_line/skills_completion.py +160 -0
- code_puppy/command_line/uc_menu.py +893 -0
- code_puppy/command_line/utils.py +93 -0
- code_puppy/command_line/wiggum_state.py +78 -0
- code_puppy/config.py +1770 -0
- code_puppy/error_logging.py +134 -0
- code_puppy/gemini_code_assist.py +385 -0
- code_puppy/gemini_model.py +754 -0
- code_puppy/hook_engine/README.md +105 -0
- code_puppy/hook_engine/__init__.py +21 -0
- code_puppy/hook_engine/aliases.py +155 -0
- code_puppy/hook_engine/engine.py +221 -0
- code_puppy/hook_engine/executor.py +296 -0
- code_puppy/hook_engine/matcher.py +156 -0
- code_puppy/hook_engine/models.py +240 -0
- code_puppy/hook_engine/registry.py +106 -0
- code_puppy/hook_engine/validator.py +144 -0
- code_puppy/http_utils.py +361 -0
- code_puppy/keymap.py +128 -0
- code_puppy/main.py +10 -0
- code_puppy/mcp_/__init__.py +66 -0
- code_puppy/mcp_/async_lifecycle.py +286 -0
- code_puppy/mcp_/blocking_startup.py +469 -0
- code_puppy/mcp_/captured_stdio_server.py +275 -0
- code_puppy/mcp_/circuit_breaker.py +290 -0
- code_puppy/mcp_/config_wizard.py +507 -0
- code_puppy/mcp_/dashboard.py +308 -0
- code_puppy/mcp_/error_isolation.py +407 -0
- code_puppy/mcp_/examples/retry_example.py +226 -0
- code_puppy/mcp_/health_monitor.py +589 -0
- code_puppy/mcp_/managed_server.py +428 -0
- code_puppy/mcp_/manager.py +807 -0
- code_puppy/mcp_/mcp_logs.py +224 -0
- code_puppy/mcp_/registry.py +451 -0
- code_puppy/mcp_/retry_manager.py +337 -0
- code_puppy/mcp_/server_registry_catalog.py +1126 -0
- code_puppy/mcp_/status_tracker.py +355 -0
- code_puppy/mcp_/system_tools.py +209 -0
- code_puppy/mcp_prompts/__init__.py +1 -0
- code_puppy/mcp_prompts/hook_creator.py +103 -0
- code_puppy/messaging/__init__.py +255 -0
- code_puppy/messaging/bus.py +613 -0
- code_puppy/messaging/commands.py +167 -0
- code_puppy/messaging/markdown_patches.py +57 -0
- code_puppy/messaging/message_queue.py +361 -0
- code_puppy/messaging/messages.py +569 -0
- code_puppy/messaging/queue_console.py +271 -0
- code_puppy/messaging/renderers.py +311 -0
- code_puppy/messaging/rich_renderer.py +1158 -0
- code_puppy/messaging/spinner/__init__.py +83 -0
- code_puppy/messaging/spinner/console_spinner.py +240 -0
- code_puppy/messaging/spinner/spinner_base.py +95 -0
- code_puppy/messaging/subagent_console.py +460 -0
- code_puppy/model_factory.py +848 -0
- code_puppy/model_switching.py +63 -0
- code_puppy/model_utils.py +168 -0
- code_puppy/models.json +174 -0
- code_puppy/models_dev_api.json +1 -0
- code_puppy/models_dev_parser.py +592 -0
- code_puppy/plugins/__init__.py +186 -0
- code_puppy/plugins/agent_skills/__init__.py +22 -0
- code_puppy/plugins/agent_skills/config.py +175 -0
- code_puppy/plugins/agent_skills/discovery.py +136 -0
- code_puppy/plugins/agent_skills/downloader.py +392 -0
- code_puppy/plugins/agent_skills/installer.py +22 -0
- code_puppy/plugins/agent_skills/metadata.py +219 -0
- code_puppy/plugins/agent_skills/prompt_builder.py +60 -0
- code_puppy/plugins/agent_skills/register_callbacks.py +241 -0
- code_puppy/plugins/agent_skills/remote_catalog.py +322 -0
- code_puppy/plugins/agent_skills/skill_catalog.py +257 -0
- code_puppy/plugins/agent_skills/skills_install_menu.py +664 -0
- code_puppy/plugins/agent_skills/skills_menu.py +781 -0
- code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
- code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
- code_puppy/plugins/antigravity_oauth/antigravity_model.py +706 -0
- code_puppy/plugins/antigravity_oauth/config.py +42 -0
- code_puppy/plugins/antigravity_oauth/constants.py +133 -0
- code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
- code_puppy/plugins/antigravity_oauth/register_callbacks.py +518 -0
- code_puppy/plugins/antigravity_oauth/storage.py +288 -0
- code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
- code_puppy/plugins/antigravity_oauth/token.py +167 -0
- code_puppy/plugins/antigravity_oauth/transport.py +863 -0
- code_puppy/plugins/antigravity_oauth/utils.py +168 -0
- code_puppy/plugins/chatgpt_oauth/__init__.py +8 -0
- code_puppy/plugins/chatgpt_oauth/config.py +52 -0
- code_puppy/plugins/chatgpt_oauth/oauth_flow.py +329 -0
- code_puppy/plugins/chatgpt_oauth/register_callbacks.py +176 -0
- code_puppy/plugins/chatgpt_oauth/test_plugin.py +301 -0
- code_puppy/plugins/chatgpt_oauth/utils.py +523 -0
- code_puppy/plugins/claude_code_hooks/__init__.py +1 -0
- code_puppy/plugins/claude_code_hooks/config.py +137 -0
- code_puppy/plugins/claude_code_hooks/register_callbacks.py +175 -0
- code_puppy/plugins/claude_code_oauth/README.md +167 -0
- code_puppy/plugins/claude_code_oauth/SETUP.md +93 -0
- code_puppy/plugins/claude_code_oauth/__init__.py +25 -0
- code_puppy/plugins/claude_code_oauth/config.py +52 -0
- code_puppy/plugins/claude_code_oauth/register_callbacks.py +453 -0
- code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
- code_puppy/plugins/claude_code_oauth/token_refresh_heartbeat.py +241 -0
- code_puppy/plugins/claude_code_oauth/utils.py +640 -0
- code_puppy/plugins/customizable_commands/__init__.py +0 -0
- code_puppy/plugins/customizable_commands/register_callbacks.py +152 -0
- code_puppy/plugins/example_custom_command/README.md +280 -0
- code_puppy/plugins/example_custom_command/register_callbacks.py +51 -0
- code_puppy/plugins/file_permission_handler/__init__.py +4 -0
- code_puppy/plugins/file_permission_handler/register_callbacks.py +470 -0
- code_puppy/plugins/frontend_emitter/__init__.py +25 -0
- code_puppy/plugins/frontend_emitter/emitter.py +121 -0
- code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
- code_puppy/plugins/hook_creator/__init__.py +1 -0
- code_puppy/plugins/hook_creator/register_callbacks.py +33 -0
- code_puppy/plugins/hook_manager/__init__.py +1 -0
- code_puppy/plugins/hook_manager/config.py +290 -0
- code_puppy/plugins/hook_manager/hooks_menu.py +564 -0
- code_puppy/plugins/hook_manager/register_callbacks.py +227 -0
- code_puppy/plugins/oauth_puppy_html.py +228 -0
- code_puppy/plugins/scheduler/__init__.py +1 -0
- code_puppy/plugins/scheduler/register_callbacks.py +88 -0
- code_puppy/plugins/scheduler/scheduler_menu.py +522 -0
- code_puppy/plugins/scheduler/scheduler_wizard.py +341 -0
- code_puppy/plugins/shell_safety/__init__.py +6 -0
- code_puppy/plugins/shell_safety/agent_shell_safety.py +69 -0
- code_puppy/plugins/shell_safety/command_cache.py +156 -0
- code_puppy/plugins/shell_safety/register_callbacks.py +202 -0
- code_puppy/plugins/synthetic_status/__init__.py +1 -0
- code_puppy/plugins/synthetic_status/register_callbacks.py +132 -0
- code_puppy/plugins/synthetic_status/status_api.py +147 -0
- code_puppy/plugins/universal_constructor/__init__.py +13 -0
- code_puppy/plugins/universal_constructor/models.py +138 -0
- code_puppy/plugins/universal_constructor/register_callbacks.py +47 -0
- code_puppy/plugins/universal_constructor/registry.py +302 -0
- code_puppy/plugins/universal_constructor/sandbox.py +584 -0
- code_puppy/prompts/antigravity_system_prompt.md +1 -0
- code_puppy/pydantic_patches.py +356 -0
- code_puppy/reopenable_async_client.py +232 -0
- code_puppy/round_robin_model.py +150 -0
- code_puppy/scheduler/__init__.py +41 -0
- code_puppy/scheduler/__main__.py +9 -0
- code_puppy/scheduler/cli.py +118 -0
- code_puppy/scheduler/config.py +126 -0
- code_puppy/scheduler/daemon.py +280 -0
- code_puppy/scheduler/executor.py +155 -0
- code_puppy/scheduler/platform.py +19 -0
- code_puppy/scheduler/platform_unix.py +22 -0
- code_puppy/scheduler/platform_win.py +32 -0
- code_puppy/session_storage.py +338 -0
- code_puppy/status_display.py +257 -0
- code_puppy/summarization_agent.py +176 -0
- code_puppy/terminal_utils.py +418 -0
- code_puppy/tools/__init__.py +501 -0
- code_puppy/tools/agent_tools.py +603 -0
- code_puppy/tools/ask_user_question/__init__.py +26 -0
- code_puppy/tools/ask_user_question/constants.py +73 -0
- code_puppy/tools/ask_user_question/demo_tui.py +55 -0
- code_puppy/tools/ask_user_question/handler.py +232 -0
- code_puppy/tools/ask_user_question/models.py +304 -0
- code_puppy/tools/ask_user_question/registration.py +26 -0
- code_puppy/tools/ask_user_question/renderers.py +309 -0
- code_puppy/tools/ask_user_question/terminal_ui.py +329 -0
- code_puppy/tools/ask_user_question/theme.py +155 -0
- code_puppy/tools/ask_user_question/tui_loop.py +423 -0
- code_puppy/tools/browser/__init__.py +37 -0
- code_puppy/tools/browser/browser_control.py +289 -0
- code_puppy/tools/browser/browser_interactions.py +545 -0
- code_puppy/tools/browser/browser_locators.py +640 -0
- code_puppy/tools/browser/browser_manager.py +378 -0
- code_puppy/tools/browser/browser_navigation.py +251 -0
- code_puppy/tools/browser/browser_screenshot.py +179 -0
- code_puppy/tools/browser/browser_scripts.py +462 -0
- code_puppy/tools/browser/browser_workflows.py +221 -0
- code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
- code_puppy/tools/browser/terminal_command_tools.py +534 -0
- code_puppy/tools/browser/terminal_screenshot_tools.py +552 -0
- code_puppy/tools/browser/terminal_tools.py +525 -0
- code_puppy/tools/command_runner.py +1346 -0
- code_puppy/tools/common.py +1409 -0
- code_puppy/tools/display.py +84 -0
- code_puppy/tools/file_modifications.py +886 -0
- code_puppy/tools/file_operations.py +802 -0
- code_puppy/tools/scheduler_tools.py +412 -0
- code_puppy/tools/skills_tools.py +244 -0
- code_puppy/tools/subagent_context.py +158 -0
- code_puppy/tools/tools_content.py +51 -0
- code_puppy/tools/universal_constructor.py +889 -0
- code_puppy/uvx_detection.py +242 -0
- code_puppy/version_checker.py +82 -0
- codepp-0.0.437.dist-info/METADATA +766 -0
- codepp-0.0.437.dist-info/RECORD +288 -0
- codepp-0.0.437.dist-info/WHEEL +4 -0
- codepp-0.0.437.dist-info/entry_points.txt +3 -0
- codepp-0.0.437.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,640 @@
|
|
|
1
|
+
"""Utility helpers for the Claude Code OAuth plugin."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
import hashlib
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
9
|
+
import re
|
|
10
|
+
import secrets
|
|
11
|
+
import time
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
14
|
+
from urllib.parse import urlencode
|
|
15
|
+
|
|
16
|
+
import requests
|
|
17
|
+
|
|
18
|
+
from .config import (
|
|
19
|
+
CLAUDE_CODE_OAUTH_CONFIG,
|
|
20
|
+
get_claude_models_path,
|
|
21
|
+
get_token_storage_path,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
# Proactive refresh buffer default (seconds). Actual buffer is dynamic
|
|
25
|
+
# based on expires_in to avoid overly aggressive refreshes.
|
|
26
|
+
TOKEN_REFRESH_BUFFER_SECONDS = 300
|
|
27
|
+
MIN_REFRESH_BUFFER_SECONDS = 30
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class OAuthContext:
|
|
34
|
+
"""Runtime state for an in-progress OAuth flow."""
|
|
35
|
+
|
|
36
|
+
state: str
|
|
37
|
+
code_verifier: str
|
|
38
|
+
code_challenge: str
|
|
39
|
+
created_at: float
|
|
40
|
+
redirect_uri: Optional[str] = None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
_oauth_context: Optional[OAuthContext] = None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _urlsafe_b64encode(data: bytes) -> str:
|
|
47
|
+
return base64.urlsafe_b64encode(data).decode("utf-8").rstrip("=")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _generate_code_verifier() -> str:
|
|
51
|
+
return _urlsafe_b64encode(secrets.token_bytes(64))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _compute_code_challenge(code_verifier: str) -> str:
|
|
55
|
+
digest = hashlib.sha256(code_verifier.encode("utf-8")).digest()
|
|
56
|
+
return _urlsafe_b64encode(digest)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def prepare_oauth_context() -> OAuthContext:
|
|
60
|
+
"""Create and cache a new OAuth PKCE context."""
|
|
61
|
+
global _oauth_context
|
|
62
|
+
state = secrets.token_urlsafe(32)
|
|
63
|
+
code_verifier = _generate_code_verifier()
|
|
64
|
+
code_challenge = _compute_code_challenge(code_verifier)
|
|
65
|
+
_oauth_context = OAuthContext(
|
|
66
|
+
state=state,
|
|
67
|
+
code_verifier=code_verifier,
|
|
68
|
+
code_challenge=code_challenge,
|
|
69
|
+
created_at=time.time(),
|
|
70
|
+
)
|
|
71
|
+
return _oauth_context
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def get_oauth_context() -> Optional[OAuthContext]:
|
|
75
|
+
return _oauth_context
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def clear_oauth_context() -> None:
|
|
79
|
+
global _oauth_context
|
|
80
|
+
_oauth_context = None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def assign_redirect_uri(context: OAuthContext, port: int) -> str:
|
|
84
|
+
"""Assign redirect URI for the given OAuth context."""
|
|
85
|
+
if context is None:
|
|
86
|
+
raise RuntimeError("OAuth context cannot be None")
|
|
87
|
+
|
|
88
|
+
host = CLAUDE_CODE_OAUTH_CONFIG["redirect_host"].rstrip("/")
|
|
89
|
+
path = CLAUDE_CODE_OAUTH_CONFIG["redirect_path"].lstrip("/")
|
|
90
|
+
redirect_uri = f"{host}:{port}/{path}"
|
|
91
|
+
context.redirect_uri = redirect_uri
|
|
92
|
+
return redirect_uri
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def build_authorization_url(context: OAuthContext) -> str:
|
|
96
|
+
"""Return the Claude authorization URL with PKCE parameters."""
|
|
97
|
+
if not context.redirect_uri:
|
|
98
|
+
raise RuntimeError("Redirect URI has not been assigned for this OAuth context")
|
|
99
|
+
|
|
100
|
+
params = {
|
|
101
|
+
"response_type": "code",
|
|
102
|
+
"client_id": CLAUDE_CODE_OAUTH_CONFIG["client_id"],
|
|
103
|
+
"redirect_uri": context.redirect_uri,
|
|
104
|
+
"scope": CLAUDE_CODE_OAUTH_CONFIG["scope"],
|
|
105
|
+
"state": context.state,
|
|
106
|
+
"code": "true",
|
|
107
|
+
"code_challenge": context.code_challenge,
|
|
108
|
+
"code_challenge_method": "S256",
|
|
109
|
+
}
|
|
110
|
+
return f"{CLAUDE_CODE_OAUTH_CONFIG['auth_url']}?{urlencode(params)}"
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def parse_authorization_code(raw_input: str) -> Tuple[str, Optional[str]]:
|
|
114
|
+
value = raw_input.strip()
|
|
115
|
+
if not value:
|
|
116
|
+
raise ValueError("Authorization code cannot be empty")
|
|
117
|
+
|
|
118
|
+
if "#" in value:
|
|
119
|
+
code, state = value.split("#", 1)
|
|
120
|
+
return code.strip(), state.strip() or None
|
|
121
|
+
|
|
122
|
+
parts = value.split()
|
|
123
|
+
if len(parts) == 2:
|
|
124
|
+
return parts[0].strip(), parts[1].strip() or None
|
|
125
|
+
|
|
126
|
+
return value, None
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def load_stored_tokens() -> Optional[Dict[str, Any]]:
|
|
130
|
+
try:
|
|
131
|
+
token_path = get_token_storage_path()
|
|
132
|
+
if token_path.exists():
|
|
133
|
+
with open(token_path, "r", encoding="utf-8") as handle:
|
|
134
|
+
return json.load(handle)
|
|
135
|
+
except Exception as exc: # pragma: no cover - defensive logging
|
|
136
|
+
logger.error("Failed to load tokens: %s", exc)
|
|
137
|
+
return None
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _calculate_expires_at(expires_in: Optional[float]) -> Optional[float]:
|
|
141
|
+
if expires_in is None:
|
|
142
|
+
return None
|
|
143
|
+
try:
|
|
144
|
+
return time.time() + float(expires_in)
|
|
145
|
+
except (TypeError, ValueError):
|
|
146
|
+
return None
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _calculate_refresh_buffer(expires_in: Optional[float]) -> float:
|
|
150
|
+
default_buffer = float(TOKEN_REFRESH_BUFFER_SECONDS)
|
|
151
|
+
if expires_in is None:
|
|
152
|
+
return default_buffer
|
|
153
|
+
try:
|
|
154
|
+
expires_value = float(expires_in)
|
|
155
|
+
except (TypeError, ValueError):
|
|
156
|
+
return default_buffer
|
|
157
|
+
return min(default_buffer, max(MIN_REFRESH_BUFFER_SECONDS, expires_value * 0.1))
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _get_expires_at_value(tokens: Dict[str, Any]) -> Optional[float]:
|
|
161
|
+
expires_at = tokens.get("expires_at")
|
|
162
|
+
if expires_at is None:
|
|
163
|
+
return None
|
|
164
|
+
try:
|
|
165
|
+
return float(expires_at)
|
|
166
|
+
except (TypeError, ValueError):
|
|
167
|
+
return None
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _is_token_actually_expired(tokens: Dict[str, Any]) -> bool:
|
|
171
|
+
expires_at_value = _get_expires_at_value(tokens)
|
|
172
|
+
if expires_at_value is None:
|
|
173
|
+
return False
|
|
174
|
+
return time.time() >= expires_at_value
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def is_token_expired(tokens: Dict[str, Any]) -> bool:
|
|
178
|
+
expires_at_value = _get_expires_at_value(tokens)
|
|
179
|
+
if expires_at_value is None:
|
|
180
|
+
return False
|
|
181
|
+
buffer_seconds = _calculate_refresh_buffer(tokens.get("expires_in"))
|
|
182
|
+
return time.time() >= expires_at_value - buffer_seconds
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def update_claude_code_model_tokens(access_token: str) -> bool:
|
|
186
|
+
try:
|
|
187
|
+
claude_models = load_claude_models()
|
|
188
|
+
if not claude_models:
|
|
189
|
+
return False
|
|
190
|
+
|
|
191
|
+
updated = False
|
|
192
|
+
for config in claude_models.values():
|
|
193
|
+
if config.get("oauth_source") != "claude-code-plugin":
|
|
194
|
+
continue
|
|
195
|
+
custom_endpoint = config.get("custom_endpoint")
|
|
196
|
+
if not isinstance(custom_endpoint, dict):
|
|
197
|
+
continue
|
|
198
|
+
custom_endpoint["api_key"] = access_token
|
|
199
|
+
updated = True
|
|
200
|
+
|
|
201
|
+
if updated:
|
|
202
|
+
return save_claude_models(claude_models)
|
|
203
|
+
except Exception as exc: # pragma: no cover - defensive logging
|
|
204
|
+
logger.error("Failed to update Claude model tokens: %s", exc)
|
|
205
|
+
return False
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def refresh_access_token(force: bool = False) -> Optional[str]:
|
|
209
|
+
tokens = load_stored_tokens()
|
|
210
|
+
if not tokens:
|
|
211
|
+
return None
|
|
212
|
+
|
|
213
|
+
if not force and not is_token_expired(tokens):
|
|
214
|
+
return tokens.get("access_token")
|
|
215
|
+
|
|
216
|
+
refresh_token = tokens.get("refresh_token")
|
|
217
|
+
if not refresh_token:
|
|
218
|
+
logger.debug("No refresh_token available")
|
|
219
|
+
return None
|
|
220
|
+
|
|
221
|
+
payload = {
|
|
222
|
+
"grant_type": "refresh_token",
|
|
223
|
+
"client_id": CLAUDE_CODE_OAUTH_CONFIG["client_id"],
|
|
224
|
+
"refresh_token": refresh_token,
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
headers = {
|
|
228
|
+
"Content-Type": "application/json",
|
|
229
|
+
"Accept": "application/json",
|
|
230
|
+
"anthropic-beta": "oauth-2025-04-20",
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
try:
|
|
234
|
+
response = requests.post(
|
|
235
|
+
CLAUDE_CODE_OAUTH_CONFIG["token_url"],
|
|
236
|
+
json=payload,
|
|
237
|
+
headers=headers,
|
|
238
|
+
timeout=30,
|
|
239
|
+
)
|
|
240
|
+
if response.status_code == 200:
|
|
241
|
+
content_type = response.headers.get("content-type", "")
|
|
242
|
+
if not content_type.startswith("application/json"):
|
|
243
|
+
logger.error(
|
|
244
|
+
"Token refresh returned non-JSON response (Content-Type: %s): %s",
|
|
245
|
+
content_type,
|
|
246
|
+
response.text[:500],
|
|
247
|
+
)
|
|
248
|
+
return None
|
|
249
|
+
try:
|
|
250
|
+
new_tokens = response.json()
|
|
251
|
+
except (ValueError, json.JSONDecodeError) as e:
|
|
252
|
+
logger.error("Failed to parse token refresh response as JSON: %s", e)
|
|
253
|
+
return None
|
|
254
|
+
tokens["access_token"] = new_tokens.get("access_token")
|
|
255
|
+
tokens["refresh_token"] = new_tokens.get("refresh_token", refresh_token)
|
|
256
|
+
expires_in_value = new_tokens.get("expires_in")
|
|
257
|
+
if expires_in_value is None:
|
|
258
|
+
expires_in_value = tokens.get("expires_in")
|
|
259
|
+
if expires_in_value is not None:
|
|
260
|
+
tokens["expires_in"] = expires_in_value
|
|
261
|
+
tokens["expires_at"] = _calculate_expires_at(expires_in_value)
|
|
262
|
+
if save_tokens(tokens):
|
|
263
|
+
update_claude_code_model_tokens(tokens["access_token"])
|
|
264
|
+
return tokens["access_token"]
|
|
265
|
+
else:
|
|
266
|
+
logger.error(
|
|
267
|
+
"Token refresh failed: %s - %s", response.status_code, response.text
|
|
268
|
+
)
|
|
269
|
+
except Exception as exc: # pragma: no cover - defensive logging
|
|
270
|
+
logger.error("Token refresh error: %s", exc)
|
|
271
|
+
return None
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def get_valid_access_token() -> Optional[str]:
|
|
275
|
+
tokens = load_stored_tokens()
|
|
276
|
+
if not tokens:
|
|
277
|
+
logger.debug("No stored Claude Code OAuth tokens found")
|
|
278
|
+
return None
|
|
279
|
+
|
|
280
|
+
access_token = tokens.get("access_token")
|
|
281
|
+
if not access_token:
|
|
282
|
+
logger.debug("No access_token in stored tokens")
|
|
283
|
+
return None
|
|
284
|
+
|
|
285
|
+
if is_token_expired(tokens):
|
|
286
|
+
logger.info("Claude Code OAuth token expired, attempting refresh")
|
|
287
|
+
refreshed = refresh_access_token()
|
|
288
|
+
if refreshed:
|
|
289
|
+
return refreshed
|
|
290
|
+
if not _is_token_actually_expired(tokens):
|
|
291
|
+
logger.warning(
|
|
292
|
+
"Claude Code token refresh failed; using existing access token until expiry"
|
|
293
|
+
)
|
|
294
|
+
return access_token
|
|
295
|
+
logger.warning("Claude Code token refresh failed")
|
|
296
|
+
return None
|
|
297
|
+
|
|
298
|
+
return access_token
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def save_tokens(tokens: Dict[str, Any]) -> bool:
|
|
302
|
+
try:
|
|
303
|
+
token_path = get_token_storage_path()
|
|
304
|
+
with open(token_path, "w", encoding="utf-8") as handle:
|
|
305
|
+
json.dump(tokens, handle, indent=2)
|
|
306
|
+
token_path.chmod(0o600)
|
|
307
|
+
return True
|
|
308
|
+
except Exception as exc: # pragma: no cover - defensive logging
|
|
309
|
+
logger.error("Failed to save tokens: %s", exc)
|
|
310
|
+
return False
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def load_claude_models() -> Dict[str, Any]:
|
|
314
|
+
try:
|
|
315
|
+
models_path = get_claude_models_path()
|
|
316
|
+
if models_path.exists():
|
|
317
|
+
with open(models_path, "r", encoding="utf-8") as handle:
|
|
318
|
+
return json.load(handle)
|
|
319
|
+
except Exception as exc: # pragma: no cover - defensive logging
|
|
320
|
+
logger.error("Failed to load Claude models: %s", exc)
|
|
321
|
+
return {}
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def load_claude_models_filtered() -> Dict[str, Any]:
|
|
325
|
+
"""Load Claude models and filter to only the latest versions.
|
|
326
|
+
|
|
327
|
+
This loads the stored models and applies the same filtering logic
|
|
328
|
+
used during saving to ensure only the latest haiku, sonnet, and opus
|
|
329
|
+
models are returned.
|
|
330
|
+
"""
|
|
331
|
+
try:
|
|
332
|
+
all_models = load_claude_models()
|
|
333
|
+
if not all_models:
|
|
334
|
+
return {}
|
|
335
|
+
|
|
336
|
+
# Extract model names from the configuration
|
|
337
|
+
model_names = []
|
|
338
|
+
for name, config in all_models.items():
|
|
339
|
+
if config.get("oauth_source") == "claude-code-plugin":
|
|
340
|
+
model_names.append(config.get("name", ""))
|
|
341
|
+
else:
|
|
342
|
+
# For non-OAuth models, use the full key
|
|
343
|
+
model_names.append(name)
|
|
344
|
+
|
|
345
|
+
# Filter to only latest models
|
|
346
|
+
latest_names = set(
|
|
347
|
+
filter_latest_claude_models(
|
|
348
|
+
model_names, max_per_family={"default": 1, "opus": 3}
|
|
349
|
+
)
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
# Return only the filtered models
|
|
353
|
+
filtered_models = {}
|
|
354
|
+
for name, config in all_models.items():
|
|
355
|
+
model_name = config.get("name", name)
|
|
356
|
+
if model_name in latest_names:
|
|
357
|
+
filtered_models[name] = config
|
|
358
|
+
|
|
359
|
+
logger.info(
|
|
360
|
+
"Loaded %d models, filtered to %d latest models",
|
|
361
|
+
len(all_models),
|
|
362
|
+
len(filtered_models),
|
|
363
|
+
)
|
|
364
|
+
return filtered_models
|
|
365
|
+
|
|
366
|
+
except Exception as exc: # pragma: no cover - defensive logging
|
|
367
|
+
logger.error("Failed to load and filter Claude models: %s", exc)
|
|
368
|
+
return {}
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def save_claude_models(models: Dict[str, Any]) -> bool:
|
|
372
|
+
try:
|
|
373
|
+
models_path = get_claude_models_path()
|
|
374
|
+
with open(models_path, "w", encoding="utf-8") as handle:
|
|
375
|
+
json.dump(models, handle, indent=2)
|
|
376
|
+
return True
|
|
377
|
+
except Exception as exc: # pragma: no cover - defensive logging
|
|
378
|
+
logger.error("Failed to save Claude models: %s", exc)
|
|
379
|
+
return False
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def exchange_code_for_tokens(
|
|
383
|
+
auth_code: str, context: OAuthContext
|
|
384
|
+
) -> Optional[Dict[str, Any]]:
|
|
385
|
+
if not context.redirect_uri:
|
|
386
|
+
raise RuntimeError("Redirect URI missing from OAuth context")
|
|
387
|
+
|
|
388
|
+
payload = {
|
|
389
|
+
"grant_type": "authorization_code",
|
|
390
|
+
"client_id": CLAUDE_CODE_OAUTH_CONFIG["client_id"],
|
|
391
|
+
"code": auth_code,
|
|
392
|
+
"state": context.state,
|
|
393
|
+
"code_verifier": context.code_verifier,
|
|
394
|
+
"redirect_uri": context.redirect_uri,
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
headers = {
|
|
398
|
+
"Content-Type": "application/json",
|
|
399
|
+
"Accept": "application/json",
|
|
400
|
+
"anthropic-beta": "oauth-2025-04-20",
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
logger.info("Exchanging code for tokens: %s", CLAUDE_CODE_OAUTH_CONFIG["token_url"])
|
|
404
|
+
logger.debug("Payload keys: %s", list(payload.keys()))
|
|
405
|
+
logger.debug("Headers: %s", headers)
|
|
406
|
+
try:
|
|
407
|
+
response = requests.post(
|
|
408
|
+
CLAUDE_CODE_OAUTH_CONFIG["token_url"],
|
|
409
|
+
json=payload,
|
|
410
|
+
headers=headers,
|
|
411
|
+
timeout=30,
|
|
412
|
+
)
|
|
413
|
+
logger.info("Token exchange response: %s", response.status_code)
|
|
414
|
+
logger.debug("Response body: %s", response.text)
|
|
415
|
+
if response.status_code == 200:
|
|
416
|
+
content_type = response.headers.get("content-type", "")
|
|
417
|
+
if not content_type.startswith("application/json"):
|
|
418
|
+
logger.error(
|
|
419
|
+
"Token exchange returned non-JSON response (Content-Type: %s): %s",
|
|
420
|
+
content_type,
|
|
421
|
+
response.text[:500],
|
|
422
|
+
)
|
|
423
|
+
return None
|
|
424
|
+
try:
|
|
425
|
+
token_data = response.json()
|
|
426
|
+
except (ValueError, json.JSONDecodeError) as e:
|
|
427
|
+
logger.error("Failed to parse token exchange response as JSON: %s", e)
|
|
428
|
+
return None
|
|
429
|
+
token_data["expires_at"] = _calculate_expires_at(
|
|
430
|
+
token_data.get("expires_in")
|
|
431
|
+
)
|
|
432
|
+
return token_data
|
|
433
|
+
logger.error(
|
|
434
|
+
"Token exchange failed: %s - %s",
|
|
435
|
+
response.status_code,
|
|
436
|
+
response.text,
|
|
437
|
+
)
|
|
438
|
+
except Exception as exc: # pragma: no cover - defensive logging
|
|
439
|
+
logger.error("Token exchange error: %s", exc)
|
|
440
|
+
return None
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def filter_latest_claude_models(
|
|
444
|
+
models: List[str], max_per_family: Union[int, Dict[str, int]] = 2
|
|
445
|
+
) -> List[str]:
|
|
446
|
+
"""Filter models to keep the top N latest haiku, sonnet, and opus.
|
|
447
|
+
|
|
448
|
+
Parses model names in the format claude-{family}-{major}-{minor}-{date}
|
|
449
|
+
and returns the top ``max_per_family`` versions of each family
|
|
450
|
+
(haiku, sonnet, opus), sorted newest-first.
|
|
451
|
+
|
|
452
|
+
Args:
|
|
453
|
+
models: List of model name strings to filter.
|
|
454
|
+
max_per_family: Either a single int applied to all families, or a dict
|
|
455
|
+
mapping family name to its limit (e.g. ``{"opus": 3}``). Families
|
|
456
|
+
not present in the dict fall back to ``"default"`` key, or ``2``.
|
|
457
|
+
"""
|
|
458
|
+
# Collect all parsed models per family
|
|
459
|
+
# family -> list of (model_name, major, minor, date)
|
|
460
|
+
family_models: Dict[str, List[Tuple[str, int, int, int]]] = {}
|
|
461
|
+
|
|
462
|
+
for model_name in models:
|
|
463
|
+
if model_name == "claude-opus-4-6":
|
|
464
|
+
family_models.setdefault("opus", []).append((model_name, 4, 6, 20260205))
|
|
465
|
+
continue
|
|
466
|
+
if model_name == "claude-sonnet-4-6":
|
|
467
|
+
family_models.setdefault("sonnet", []).append((model_name, 4, 6, 20250610))
|
|
468
|
+
continue
|
|
469
|
+
# Match pattern: claude-{family}-{major}-{minor}-{date}
|
|
470
|
+
# Examples: claude-haiku-3-5-20241022, claude-sonnet-4-5-20250929
|
|
471
|
+
match = re.match(r"claude-(haiku|sonnet|opus)-(\d+)-(\d+)-(\d+)", model_name)
|
|
472
|
+
if not match:
|
|
473
|
+
# Also try pattern with dots: claude-{family}-{major}.{minor}-{date}
|
|
474
|
+
match = re.match(
|
|
475
|
+
r"claude-(haiku|sonnet|opus)-(\d+)\.(\d+)-(\d+)", model_name
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
if not match:
|
|
479
|
+
continue
|
|
480
|
+
|
|
481
|
+
family = match.group(1)
|
|
482
|
+
major = int(match.group(2))
|
|
483
|
+
minor = int(match.group(3))
|
|
484
|
+
date = int(match.group(4))
|
|
485
|
+
|
|
486
|
+
family_models.setdefault(family, []).append((model_name, major, minor, date))
|
|
487
|
+
|
|
488
|
+
# Sort each family descending and keep the top N
|
|
489
|
+
filtered: List[str] = []
|
|
490
|
+
for family, family_entries in family_models.items():
|
|
491
|
+
if isinstance(max_per_family, dict):
|
|
492
|
+
limit = max_per_family.get(family, max_per_family.get("default", 2))
|
|
493
|
+
else:
|
|
494
|
+
limit = max_per_family
|
|
495
|
+
family_entries.sort(key=lambda e: (e[1], e[2], e[3]), reverse=True)
|
|
496
|
+
for entry in family_entries[:limit]:
|
|
497
|
+
filtered.append(entry[0])
|
|
498
|
+
|
|
499
|
+
logger.info(
|
|
500
|
+
"Filtered %d models to %d latest models (max_per_family=%s): %s",
|
|
501
|
+
len(models),
|
|
502
|
+
len(filtered),
|
|
503
|
+
max_per_family,
|
|
504
|
+
filtered,
|
|
505
|
+
)
|
|
506
|
+
return filtered
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
def fetch_claude_code_models(access_token: str) -> Optional[List[str]]:
|
|
510
|
+
try:
|
|
511
|
+
api_url = f"{CLAUDE_CODE_OAUTH_CONFIG['api_base_url']}/v1/models"
|
|
512
|
+
headers = {
|
|
513
|
+
"Authorization": f"Bearer {access_token}",
|
|
514
|
+
"Content-Type": "application/json",
|
|
515
|
+
"anthropic-beta": "oauth-2025-04-20",
|
|
516
|
+
"anthropic-version": CLAUDE_CODE_OAUTH_CONFIG.get(
|
|
517
|
+
"anthropic_version", "2023-06-01"
|
|
518
|
+
),
|
|
519
|
+
}
|
|
520
|
+
response = requests.get(api_url, headers=headers, timeout=30)
|
|
521
|
+
if response.status_code == 200:
|
|
522
|
+
content_type = response.headers.get("content-type", "")
|
|
523
|
+
if not content_type.startswith("application/json"):
|
|
524
|
+
logger.error(
|
|
525
|
+
"Models fetch returned non-JSON response (Content-Type: %s): %s",
|
|
526
|
+
content_type,
|
|
527
|
+
response.text[:500],
|
|
528
|
+
)
|
|
529
|
+
return None
|
|
530
|
+
try:
|
|
531
|
+
data = response.json()
|
|
532
|
+
except (ValueError, json.JSONDecodeError) as e:
|
|
533
|
+
logger.error("Failed to parse models response as JSON: %s", e)
|
|
534
|
+
return None
|
|
535
|
+
if isinstance(data.get("data"), list):
|
|
536
|
+
models: List[str] = []
|
|
537
|
+
for model in data["data"]:
|
|
538
|
+
name = model.get("id") or model.get("name")
|
|
539
|
+
if name:
|
|
540
|
+
models.append(name)
|
|
541
|
+
return models
|
|
542
|
+
else:
|
|
543
|
+
logger.error(
|
|
544
|
+
"Failed to fetch models: %s - %s",
|
|
545
|
+
response.status_code,
|
|
546
|
+
response.text,
|
|
547
|
+
)
|
|
548
|
+
except Exception as exc: # pragma: no cover - defensive logging
|
|
549
|
+
logger.error("Error fetching Claude Code models: %s", exc)
|
|
550
|
+
return None
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def _build_model_entry(model_name: str, access_token: str, context_length: int) -> dict:
|
|
554
|
+
"""Build a single model config entry for claude_models.json."""
|
|
555
|
+
supported_settings = [
|
|
556
|
+
"temperature",
|
|
557
|
+
"extended_thinking",
|
|
558
|
+
"budget_tokens",
|
|
559
|
+
"interleaved_thinking",
|
|
560
|
+
]
|
|
561
|
+
|
|
562
|
+
# Opus 4-6 models support the effort setting
|
|
563
|
+
lower = model_name.lower()
|
|
564
|
+
if "opus-4-6" in lower or "4-6-opus" in lower:
|
|
565
|
+
supported_settings.append("effort")
|
|
566
|
+
|
|
567
|
+
return {
|
|
568
|
+
"type": "claude_code",
|
|
569
|
+
"name": model_name,
|
|
570
|
+
"custom_endpoint": {
|
|
571
|
+
"url": CLAUDE_CODE_OAUTH_CONFIG["api_base_url"],
|
|
572
|
+
"api_key": access_token,
|
|
573
|
+
"headers": {
|
|
574
|
+
"anthropic-beta": "oauth-2025-04-20,interleaved-thinking-2025-05-14",
|
|
575
|
+
"x-app": "cli",
|
|
576
|
+
"User-Agent": "claude-cli/2.0.61 (external, cli)",
|
|
577
|
+
},
|
|
578
|
+
},
|
|
579
|
+
"context_length": context_length,
|
|
580
|
+
"oauth_source": "claude-code-plugin",
|
|
581
|
+
"supported_settings": supported_settings,
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
def add_models_to_extra_config(models: List[str]) -> bool:
|
|
586
|
+
try:
|
|
587
|
+
# Filter to only latest haiku, sonnet, and opus models
|
|
588
|
+
filtered_models = filter_latest_claude_models(
|
|
589
|
+
models, max_per_family={"default": 1, "opus": 3}
|
|
590
|
+
)
|
|
591
|
+
|
|
592
|
+
# Start fresh - overwrite the file on every auth instead of loading existing
|
|
593
|
+
claude_models = {}
|
|
594
|
+
added = 0
|
|
595
|
+
access_token = get_valid_access_token() or ""
|
|
596
|
+
prefix = CLAUDE_CODE_OAUTH_CONFIG["prefix"]
|
|
597
|
+
default_ctx = CLAUDE_CODE_OAUTH_CONFIG["default_context_length"]
|
|
598
|
+
long_ctx = CLAUDE_CODE_OAUTH_CONFIG["long_context_length"]
|
|
599
|
+
long_ctx_models = CLAUDE_CODE_OAUTH_CONFIG["long_context_models"]
|
|
600
|
+
|
|
601
|
+
for model_name in filtered_models:
|
|
602
|
+
prefixed = f"{prefix}{model_name}"
|
|
603
|
+
claude_models[prefixed] = _build_model_entry(
|
|
604
|
+
model_name, access_token, default_ctx
|
|
605
|
+
)
|
|
606
|
+
added += 1
|
|
607
|
+
|
|
608
|
+
# Create a "-long" variant with extended context for eligible models
|
|
609
|
+
if model_name in long_ctx_models:
|
|
610
|
+
long_prefixed = f"{prefix}{model_name}-long"
|
|
611
|
+
claude_models[long_prefixed] = _build_model_entry(
|
|
612
|
+
model_name, access_token, long_ctx
|
|
613
|
+
)
|
|
614
|
+
added += 1
|
|
615
|
+
|
|
616
|
+
if save_claude_models(claude_models):
|
|
617
|
+
logger.info("Added %s Claude Code models", added)
|
|
618
|
+
return True
|
|
619
|
+
except Exception as exc: # pragma: no cover - defensive logging
|
|
620
|
+
logger.error("Error adding models to config: %s", exc)
|
|
621
|
+
return False
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
def remove_claude_code_models() -> int:
|
|
625
|
+
try:
|
|
626
|
+
claude_models = load_claude_models()
|
|
627
|
+
to_remove = [
|
|
628
|
+
name
|
|
629
|
+
for name, config in claude_models.items()
|
|
630
|
+
if config.get("oauth_source") == "claude-code-plugin"
|
|
631
|
+
]
|
|
632
|
+
if not to_remove:
|
|
633
|
+
return 0
|
|
634
|
+
for model_name in to_remove:
|
|
635
|
+
claude_models.pop(model_name, None)
|
|
636
|
+
if save_claude_models(claude_models):
|
|
637
|
+
return len(to_remove)
|
|
638
|
+
except Exception as exc: # pragma: no cover - defensive logging
|
|
639
|
+
logger.error("Error removing Claude Code models: %s", exc)
|
|
640
|
+
return 0
|
|
File without changes
|