ccproxy-api 0.1.2__py3-none-any.whl → 0.1.4__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.
- ccproxy/_version.py +2 -2
- ccproxy/adapters/openai/__init__.py +1 -2
- ccproxy/adapters/openai/adapter.py +218 -180
- ccproxy/adapters/openai/streaming.py +247 -65
- ccproxy/api/__init__.py +0 -3
- ccproxy/api/app.py +173 -40
- ccproxy/api/dependencies.py +62 -3
- ccproxy/api/middleware/errors.py +3 -7
- ccproxy/api/middleware/headers.py +0 -2
- ccproxy/api/middleware/logging.py +4 -3
- ccproxy/api/middleware/request_content_logging.py +297 -0
- ccproxy/api/middleware/request_id.py +5 -0
- ccproxy/api/middleware/server_header.py +0 -4
- ccproxy/api/routes/__init__.py +9 -1
- ccproxy/api/routes/claude.py +23 -32
- ccproxy/api/routes/health.py +58 -4
- ccproxy/api/routes/mcp.py +171 -0
- ccproxy/api/routes/metrics.py +4 -8
- ccproxy/api/routes/permissions.py +217 -0
- ccproxy/api/routes/proxy.py +0 -53
- ccproxy/api/services/__init__.py +6 -0
- ccproxy/api/services/permission_service.py +368 -0
- ccproxy/api/ui/__init__.py +6 -0
- ccproxy/api/ui/permission_handler_protocol.py +33 -0
- ccproxy/api/ui/terminal_permission_handler.py +593 -0
- ccproxy/auth/conditional.py +2 -2
- ccproxy/auth/dependencies.py +1 -1
- ccproxy/auth/oauth/models.py +0 -1
- ccproxy/auth/oauth/routes.py +1 -3
- ccproxy/auth/storage/json_file.py +0 -1
- ccproxy/auth/storage/keyring.py +0 -3
- ccproxy/claude_sdk/__init__.py +2 -0
- ccproxy/claude_sdk/client.py +91 -8
- ccproxy/claude_sdk/converter.py +405 -210
- ccproxy/claude_sdk/options.py +76 -29
- ccproxy/claude_sdk/parser.py +200 -0
- ccproxy/claude_sdk/streaming.py +286 -0
- ccproxy/cli/commands/__init__.py +5 -2
- ccproxy/cli/commands/auth.py +2 -4
- ccproxy/cli/commands/permission_handler.py +553 -0
- ccproxy/cli/commands/serve.py +30 -12
- ccproxy/cli/docker/params.py +0 -4
- ccproxy/cli/helpers.py +0 -2
- ccproxy/cli/main.py +5 -16
- ccproxy/cli/options/claude_options.py +19 -1
- ccproxy/cli/options/core_options.py +0 -3
- ccproxy/cli/options/security_options.py +0 -2
- ccproxy/cli/options/server_options.py +3 -2
- ccproxy/config/auth.py +0 -1
- ccproxy/config/claude.py +78 -2
- ccproxy/config/discovery.py +0 -1
- ccproxy/config/docker_settings.py +0 -1
- ccproxy/config/loader.py +1 -4
- ccproxy/config/scheduler.py +20 -0
- ccproxy/config/security.py +7 -2
- ccproxy/config/server.py +5 -0
- ccproxy/config/settings.py +13 -7
- ccproxy/config/validators.py +1 -1
- ccproxy/core/async_utils.py +1 -4
- ccproxy/core/errors.py +45 -1
- ccproxy/core/http_transformers.py +4 -3
- ccproxy/core/interfaces.py +2 -2
- ccproxy/core/logging.py +97 -95
- ccproxy/core/middleware.py +1 -1
- ccproxy/core/proxy.py +1 -1
- ccproxy/core/transformers.py +1 -1
- ccproxy/core/types.py +1 -1
- ccproxy/docker/models.py +1 -1
- ccproxy/docker/protocol.py +0 -3
- ccproxy/models/__init__.py +41 -0
- ccproxy/models/claude_sdk.py +420 -0
- ccproxy/models/messages.py +45 -18
- ccproxy/models/permissions.py +115 -0
- ccproxy/models/requests.py +1 -1
- ccproxy/models/responses.py +29 -2
- ccproxy/observability/access_logger.py +1 -2
- ccproxy/observability/context.py +17 -1
- ccproxy/observability/metrics.py +1 -3
- ccproxy/observability/pushgateway.py +0 -2
- ccproxy/observability/stats_printer.py +2 -4
- ccproxy/observability/storage/duckdb_simple.py +1 -1
- ccproxy/observability/storage/models.py +0 -1
- ccproxy/pricing/cache.py +0 -1
- ccproxy/pricing/loader.py +5 -21
- ccproxy/pricing/updater.py +0 -1
- ccproxy/scheduler/__init__.py +1 -0
- ccproxy/scheduler/core.py +6 -6
- ccproxy/scheduler/manager.py +35 -7
- ccproxy/scheduler/registry.py +1 -1
- ccproxy/scheduler/tasks.py +127 -2
- ccproxy/services/claude_sdk_service.py +220 -328
- ccproxy/services/credentials/manager.py +0 -1
- ccproxy/services/credentials/oauth_client.py +1 -2
- ccproxy/services/proxy_service.py +93 -222
- ccproxy/testing/config.py +1 -1
- ccproxy/testing/mock_responses.py +0 -1
- ccproxy/utils/model_mapping.py +197 -0
- ccproxy/utils/models_provider.py +150 -0
- ccproxy/utils/simple_request_logger.py +284 -0
- ccproxy/utils/version_checker.py +184 -0
- {ccproxy_api-0.1.2.dist-info → ccproxy_api-0.1.4.dist-info}/METADATA +63 -2
- ccproxy_api-0.1.4.dist-info/RECORD +166 -0
- ccproxy/cli/commands/permission.py +0 -128
- ccproxy_api-0.1.2.dist-info/RECORD +0 -150
- /ccproxy/scheduler/{exceptions.py → errors.py} +0 -0
- {ccproxy_api-0.1.2.dist-info → ccproxy_api-0.1.4.dist-info}/WHEEL +0 -0
- {ccproxy_api-0.1.2.dist-info → ccproxy_api-0.1.4.dist-info}/entry_points.txt +0 -0
- {ccproxy_api-0.1.2.dist-info → ccproxy_api-0.1.4.dist-info}/licenses/LICENSE +0 -0
ccproxy/observability/context.py
CHANGED
|
@@ -21,7 +21,8 @@ import uuid
|
|
|
21
21
|
from collections.abc import AsyncGenerator
|
|
22
22
|
from contextlib import asynccontextmanager
|
|
23
23
|
from dataclasses import dataclass, field
|
|
24
|
-
from
|
|
24
|
+
from datetime import UTC, datetime
|
|
25
|
+
from typing import Any
|
|
25
26
|
|
|
26
27
|
import structlog
|
|
27
28
|
|
|
@@ -43,6 +44,7 @@ class RequestContext:
|
|
|
43
44
|
logger: structlog.BoundLogger
|
|
44
45
|
metadata: dict[str, Any] = field(default_factory=dict)
|
|
45
46
|
storage: Any | None = None # Optional DuckDB storage instance
|
|
47
|
+
log_timestamp: datetime | None = None # Datetime for consistent logging filenames
|
|
46
48
|
|
|
47
49
|
@property
|
|
48
50
|
def duration_ms(self) -> float:
|
|
@@ -66,12 +68,25 @@ class RequestContext:
|
|
|
66
68
|
event, request_id=self.request_id, duration_ms=self.duration_ms, **kwargs
|
|
67
69
|
)
|
|
68
70
|
|
|
71
|
+
def get_log_timestamp_prefix(self) -> str:
|
|
72
|
+
"""Get timestamp prefix for consistent log filenames.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
Timestamp string in YYYYMMDDhhmmss format (UTC)
|
|
76
|
+
"""
|
|
77
|
+
if self.log_timestamp:
|
|
78
|
+
return self.log_timestamp.strftime("%Y%m%d%H%M%S")
|
|
79
|
+
else:
|
|
80
|
+
# Fallback to current time if not set
|
|
81
|
+
return datetime.now(UTC).strftime("%Y%m%d%H%M%S")
|
|
82
|
+
|
|
69
83
|
|
|
70
84
|
@asynccontextmanager
|
|
71
85
|
async def request_context(
|
|
72
86
|
request_id: str | None = None,
|
|
73
87
|
storage: Any | None = None,
|
|
74
88
|
metrics: Any | None = None,
|
|
89
|
+
log_timestamp: datetime | None = None,
|
|
75
90
|
**initial_context: Any,
|
|
76
91
|
) -> AsyncGenerator[RequestContext, None]:
|
|
77
92
|
"""
|
|
@@ -124,6 +139,7 @@ async def request_context(
|
|
|
124
139
|
logger=request_logger,
|
|
125
140
|
metadata=dict(initial_context),
|
|
126
141
|
storage=storage,
|
|
142
|
+
log_timestamp=log_timestamp,
|
|
127
143
|
)
|
|
128
144
|
|
|
129
145
|
try:
|
ccproxy/observability/metrics.py
CHANGED
|
@@ -9,10 +9,9 @@ from __future__ import annotations
|
|
|
9
9
|
|
|
10
10
|
import json
|
|
11
11
|
import time
|
|
12
|
-
from collections.abc import Callable
|
|
13
12
|
from dataclasses import dataclass
|
|
14
|
-
from datetime import datetime
|
|
15
|
-
from typing import Any
|
|
13
|
+
from datetime import datetime
|
|
14
|
+
from typing import Any
|
|
16
15
|
|
|
17
16
|
import structlog
|
|
18
17
|
|
|
@@ -524,7 +523,6 @@ class StatsCollector:
|
|
|
524
523
|
from rich import box
|
|
525
524
|
from rich.console import Console
|
|
526
525
|
from rich.table import Table
|
|
527
|
-
from rich.text import Text
|
|
528
526
|
|
|
529
527
|
output_buffer = StringIO()
|
|
530
528
|
console = Console(file=output_buffer, width=80, force_terminal=True)
|
ccproxy/pricing/cache.py
CHANGED
ccproxy/pricing/loader.py
CHANGED
|
@@ -6,6 +6,8 @@ from typing import Any
|
|
|
6
6
|
from pydantic import ValidationError
|
|
7
7
|
from structlog import get_logger
|
|
8
8
|
|
|
9
|
+
from ccproxy.utils.model_mapping import get_claude_aliases_mapping, map_model_to_claude
|
|
10
|
+
|
|
9
11
|
from .models import PricingData
|
|
10
12
|
|
|
11
13
|
|
|
@@ -15,22 +17,6 @@ logger = get_logger(__name__)
|
|
|
15
17
|
class PricingLoader:
|
|
16
18
|
"""Loads and converts pricing data from LiteLLM format to internal format."""
|
|
17
19
|
|
|
18
|
-
# Claude model name mappings for different versions
|
|
19
|
-
CLAUDE_MODEL_MAPPINGS = {
|
|
20
|
-
# Map versioned models to their canonical names
|
|
21
|
-
"claude-3-5-sonnet-latest": "claude-3-5-sonnet-20241022",
|
|
22
|
-
"claude-3-5-sonnet-20240620": "claude-3-5-sonnet-20240620",
|
|
23
|
-
"claude-3-5-sonnet-20241022": "claude-3-5-sonnet-20241022",
|
|
24
|
-
"claude-3-5-haiku-latest": "claude-3-5-haiku-20241022",
|
|
25
|
-
"claude-3-5-haiku-20241022": "claude-3-5-haiku-20241022",
|
|
26
|
-
"claude-3-opus": "claude-3-opus-20240229",
|
|
27
|
-
"claude-3-opus-20240229": "claude-3-opus-20240229",
|
|
28
|
-
"claude-3-sonnet": "claude-3-sonnet-20240229",
|
|
29
|
-
"claude-3-sonnet-20240229": "claude-3-sonnet-20240229",
|
|
30
|
-
"claude-3-haiku": "claude-3-haiku-20240307",
|
|
31
|
-
"claude-3-haiku-20240307": "claude-3-haiku-20240307",
|
|
32
|
-
}
|
|
33
|
-
|
|
34
20
|
@staticmethod
|
|
35
21
|
def extract_claude_models(
|
|
36
22
|
litellm_data: dict[str, Any], verbose: bool = True
|
|
@@ -112,9 +98,7 @@ class PricingLoader:
|
|
|
112
98
|
pricing["cache_read"] = Decimal(str(cache_read_cost * 1_000_000))
|
|
113
99
|
|
|
114
100
|
# Map to canonical model name if needed
|
|
115
|
-
canonical_name =
|
|
116
|
-
model_name, model_name
|
|
117
|
-
)
|
|
101
|
+
canonical_name = map_model_to_claude(model_name)
|
|
118
102
|
internal_format[canonical_name] = pricing
|
|
119
103
|
|
|
120
104
|
if verbose:
|
|
@@ -252,7 +236,7 @@ class PricingLoader:
|
|
|
252
236
|
Returns:
|
|
253
237
|
Dictionary mapping aliases to canonical model names
|
|
254
238
|
"""
|
|
255
|
-
return
|
|
239
|
+
return get_claude_aliases_mapping()
|
|
256
240
|
|
|
257
241
|
@staticmethod
|
|
258
242
|
def get_canonical_model_name(model_name: str) -> str:
|
|
@@ -264,4 +248,4 @@ class PricingLoader:
|
|
|
264
248
|
Returns:
|
|
265
249
|
Canonical model name
|
|
266
250
|
"""
|
|
267
|
-
return
|
|
251
|
+
return map_model_to_claude(model_name)
|
ccproxy/pricing/updater.py
CHANGED
ccproxy/scheduler/__init__.py
CHANGED
ccproxy/scheduler/core.py
CHANGED
|
@@ -5,7 +5,7 @@ from typing import Any
|
|
|
5
5
|
|
|
6
6
|
import structlog
|
|
7
7
|
|
|
8
|
-
from .
|
|
8
|
+
from .errors import (
|
|
9
9
|
SchedulerError,
|
|
10
10
|
SchedulerShutdownError,
|
|
11
11
|
TaskNotFoundError,
|
|
@@ -60,7 +60,7 @@ class Scheduler:
|
|
|
60
60
|
self._running = True
|
|
61
61
|
self._semaphore = asyncio.Semaphore(self.max_concurrent_tasks)
|
|
62
62
|
|
|
63
|
-
logger.
|
|
63
|
+
logger.debug(
|
|
64
64
|
"scheduler_starting",
|
|
65
65
|
max_concurrent_tasks=self.max_concurrent_tasks,
|
|
66
66
|
registered_tasks=self.task_registry.list_tasks(),
|
|
@@ -68,7 +68,7 @@ class Scheduler:
|
|
|
68
68
|
|
|
69
69
|
try:
|
|
70
70
|
# No automatic task creation - tasks must be explicitly added
|
|
71
|
-
logger.
|
|
71
|
+
logger.debug(
|
|
72
72
|
"scheduler_started",
|
|
73
73
|
active_tasks=len(self._tasks),
|
|
74
74
|
running_tasks=[
|
|
@@ -129,7 +129,7 @@ class Scheduler:
|
|
|
129
129
|
) from e
|
|
130
130
|
|
|
131
131
|
self._tasks.clear()
|
|
132
|
-
logger.
|
|
132
|
+
logger.debug("scheduler_stopped")
|
|
133
133
|
|
|
134
134
|
async def add_task(
|
|
135
135
|
self,
|
|
@@ -166,13 +166,13 @@ class Scheduler:
|
|
|
166
166
|
# Start the task if scheduler is running and task is enabled
|
|
167
167
|
if self._running and task_instance.enabled:
|
|
168
168
|
await task_instance.start()
|
|
169
|
-
logger.
|
|
169
|
+
logger.debug(
|
|
170
170
|
"task_added_and_started",
|
|
171
171
|
task_name=task_name,
|
|
172
172
|
task_type=task_type,
|
|
173
173
|
)
|
|
174
174
|
else:
|
|
175
|
-
logger.
|
|
175
|
+
logger.debug(
|
|
176
176
|
"task_added_not_started",
|
|
177
177
|
task_name=task_name,
|
|
178
178
|
task_type=task_type,
|
ccproxy/scheduler/manager.py
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
"""Scheduler management for FastAPI integration."""
|
|
2
2
|
|
|
3
|
-
import asyncio
|
|
4
|
-
from typing import Any
|
|
5
|
-
|
|
6
3
|
import structlog
|
|
7
4
|
|
|
8
5
|
from ccproxy.config.settings import Settings
|
|
9
6
|
|
|
10
|
-
from .core import Scheduler
|
|
7
|
+
from .core import Scheduler
|
|
11
8
|
from .registry import register_task
|
|
12
|
-
from .tasks import
|
|
9
|
+
from .tasks import (
|
|
10
|
+
PricingCacheUpdateTask,
|
|
11
|
+
PushgatewayTask,
|
|
12
|
+
StatsPrintingTask,
|
|
13
|
+
VersionUpdateCheckTask,
|
|
14
|
+
)
|
|
13
15
|
|
|
14
16
|
|
|
15
17
|
logger = structlog.get_logger(__name__)
|
|
@@ -83,7 +85,7 @@ async def setup_scheduler_tasks(scheduler: Scheduler, settings: Settings) -> Non
|
|
|
83
85
|
enabled=True,
|
|
84
86
|
force_refresh_on_startup=scheduler_config.pricing_force_refresh_on_startup,
|
|
85
87
|
)
|
|
86
|
-
logger.
|
|
88
|
+
logger.debug(
|
|
87
89
|
"pricing_update_task_added",
|
|
88
90
|
interval_hours=scheduler_config.pricing_update_interval_hours,
|
|
89
91
|
force_refresh_on_startup=scheduler_config.pricing_force_refresh_on_startup,
|
|
@@ -95,6 +97,31 @@ async def setup_scheduler_tasks(scheduler: Scheduler, settings: Settings) -> Non
|
|
|
95
97
|
error_type=type(e).__name__,
|
|
96
98
|
)
|
|
97
99
|
|
|
100
|
+
# Add version update check task if enabled
|
|
101
|
+
if scheduler_config.version_check_enabled:
|
|
102
|
+
try:
|
|
103
|
+
# Convert hours to seconds
|
|
104
|
+
interval_seconds = scheduler_config.version_check_interval_hours * 3600
|
|
105
|
+
|
|
106
|
+
await scheduler.add_task(
|
|
107
|
+
task_name="version_update_check",
|
|
108
|
+
task_type="version_update_check",
|
|
109
|
+
interval_seconds=interval_seconds,
|
|
110
|
+
enabled=True,
|
|
111
|
+
startup_max_age_hours=scheduler_config.version_check_startup_max_age_hours,
|
|
112
|
+
)
|
|
113
|
+
logger.debug(
|
|
114
|
+
"version_check_task_added",
|
|
115
|
+
interval_hours=scheduler_config.version_check_interval_hours,
|
|
116
|
+
startup_max_age_hours=scheduler_config.version_check_startup_max_age_hours,
|
|
117
|
+
)
|
|
118
|
+
except Exception as e:
|
|
119
|
+
logger.error(
|
|
120
|
+
"version_check_task_add_failed",
|
|
121
|
+
error=str(e),
|
|
122
|
+
error_type=type(e).__name__,
|
|
123
|
+
)
|
|
124
|
+
|
|
98
125
|
|
|
99
126
|
def _register_default_tasks() -> None:
|
|
100
127
|
"""Register default task types in the global registry."""
|
|
@@ -109,6 +136,8 @@ def _register_default_tasks() -> None:
|
|
|
109
136
|
register_task("stats_printing", StatsPrintingTask)
|
|
110
137
|
if not registry.is_registered("pricing_cache_update"):
|
|
111
138
|
register_task("pricing_cache_update", PricingCacheUpdateTask)
|
|
139
|
+
if not registry.is_registered("version_update_check"):
|
|
140
|
+
register_task("version_update_check", VersionUpdateCheckTask)
|
|
112
141
|
|
|
113
142
|
|
|
114
143
|
async def start_scheduler(settings: Settings) -> Scheduler | None:
|
|
@@ -177,7 +206,6 @@ async def stop_scheduler(scheduler: Scheduler | None) -> None:
|
|
|
177
206
|
|
|
178
207
|
try:
|
|
179
208
|
await scheduler.stop()
|
|
180
|
-
logger.info("scheduler_stopped")
|
|
181
209
|
except Exception as e:
|
|
182
210
|
logger.error(
|
|
183
211
|
"scheduler_stop_failed",
|
ccproxy/scheduler/registry.py
CHANGED
ccproxy/scheduler/tasks.py
CHANGED
|
@@ -5,12 +5,11 @@ import contextlib
|
|
|
5
5
|
import random
|
|
6
6
|
import time
|
|
7
7
|
from abc import ABC, abstractmethod
|
|
8
|
+
from datetime import UTC
|
|
8
9
|
from typing import Any
|
|
9
10
|
|
|
10
11
|
import structlog
|
|
11
12
|
|
|
12
|
-
from .exceptions import TaskExecutionError
|
|
13
|
-
|
|
14
13
|
|
|
15
14
|
logger = structlog.get_logger(__name__)
|
|
16
15
|
|
|
@@ -482,3 +481,129 @@ class PricingCacheUpdateTask(BaseScheduledTask):
|
|
|
482
481
|
error_type=type(e).__name__,
|
|
483
482
|
)
|
|
484
483
|
return False
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
class VersionUpdateCheckTask(BaseScheduledTask):
|
|
487
|
+
"""Task for checking version updates periodically."""
|
|
488
|
+
|
|
489
|
+
def __init__(
|
|
490
|
+
self,
|
|
491
|
+
name: str,
|
|
492
|
+
interval_seconds: float,
|
|
493
|
+
enabled: bool = True,
|
|
494
|
+
startup_max_age_hours: float = 1.0,
|
|
495
|
+
):
|
|
496
|
+
"""
|
|
497
|
+
Initialize version update check task.
|
|
498
|
+
|
|
499
|
+
Args:
|
|
500
|
+
name: Task name
|
|
501
|
+
interval_seconds: Interval between version checks
|
|
502
|
+
enabled: Whether task is enabled
|
|
503
|
+
startup_max_age_hours: Maximum age in hours before running startup check
|
|
504
|
+
"""
|
|
505
|
+
super().__init__(
|
|
506
|
+
name=name,
|
|
507
|
+
interval_seconds=interval_seconds,
|
|
508
|
+
enabled=enabled,
|
|
509
|
+
)
|
|
510
|
+
self.startup_max_age_hours = startup_max_age_hours
|
|
511
|
+
self._first_run = True
|
|
512
|
+
|
|
513
|
+
async def run(self) -> bool:
|
|
514
|
+
"""Execute version update check."""
|
|
515
|
+
try:
|
|
516
|
+
from datetime import datetime
|
|
517
|
+
|
|
518
|
+
from ccproxy.utils.version_checker import (
|
|
519
|
+
VersionCheckState,
|
|
520
|
+
compare_versions,
|
|
521
|
+
fetch_latest_github_version,
|
|
522
|
+
get_current_version,
|
|
523
|
+
get_version_check_state_path,
|
|
524
|
+
load_check_state,
|
|
525
|
+
save_check_state,
|
|
526
|
+
)
|
|
527
|
+
|
|
528
|
+
state_path = get_version_check_state_path()
|
|
529
|
+
current_time = datetime.now(UTC)
|
|
530
|
+
|
|
531
|
+
# Check if we should run based on startup logic
|
|
532
|
+
if self._first_run:
|
|
533
|
+
self._first_run = False
|
|
534
|
+
should_run_startup_check = False
|
|
535
|
+
|
|
536
|
+
# Load existing state if available
|
|
537
|
+
existing_state = await load_check_state(state_path)
|
|
538
|
+
if existing_state:
|
|
539
|
+
# Check age of last check
|
|
540
|
+
time_diff = current_time - existing_state.last_check_at
|
|
541
|
+
age_hours = time_diff.total_seconds() / 3600
|
|
542
|
+
|
|
543
|
+
if age_hours > self.startup_max_age_hours:
|
|
544
|
+
should_run_startup_check = True
|
|
545
|
+
logger.debug(
|
|
546
|
+
"version_check_startup_needed",
|
|
547
|
+
task_name=self.name,
|
|
548
|
+
age_hours=age_hours,
|
|
549
|
+
max_age_hours=self.startup_max_age_hours,
|
|
550
|
+
)
|
|
551
|
+
else:
|
|
552
|
+
logger.debug(
|
|
553
|
+
"version_check_startup_skipped",
|
|
554
|
+
task_name=self.name,
|
|
555
|
+
age_hours=age_hours,
|
|
556
|
+
max_age_hours=self.startup_max_age_hours,
|
|
557
|
+
)
|
|
558
|
+
return True # Skip this run
|
|
559
|
+
else:
|
|
560
|
+
# No previous state, run check
|
|
561
|
+
should_run_startup_check = True
|
|
562
|
+
logger.debug("version_check_startup_no_state", task_name=self.name)
|
|
563
|
+
|
|
564
|
+
if not should_run_startup_check:
|
|
565
|
+
return True
|
|
566
|
+
|
|
567
|
+
# Fetch latest version from GitHub
|
|
568
|
+
latest_version = await fetch_latest_github_version()
|
|
569
|
+
if latest_version is None:
|
|
570
|
+
logger.warning("version_check_fetch_failed", task_name=self.name)
|
|
571
|
+
return False
|
|
572
|
+
|
|
573
|
+
# Get current version
|
|
574
|
+
current_version = get_current_version()
|
|
575
|
+
|
|
576
|
+
# Save state
|
|
577
|
+
new_state = VersionCheckState(
|
|
578
|
+
last_check_at=current_time,
|
|
579
|
+
latest_version_found=latest_version,
|
|
580
|
+
)
|
|
581
|
+
await save_check_state(state_path, new_state)
|
|
582
|
+
|
|
583
|
+
# Compare versions
|
|
584
|
+
if compare_versions(current_version, latest_version):
|
|
585
|
+
logger.info(
|
|
586
|
+
"version_update_available",
|
|
587
|
+
task_name=self.name,
|
|
588
|
+
current_version=current_version,
|
|
589
|
+
latest_version=latest_version,
|
|
590
|
+
message=f"New version {latest_version} available! You are running {current_version}",
|
|
591
|
+
)
|
|
592
|
+
else:
|
|
593
|
+
logger.debug(
|
|
594
|
+
"version_check_complete_no_update",
|
|
595
|
+
task_name=self.name,
|
|
596
|
+
current_version=current_version,
|
|
597
|
+
latest_version=latest_version,
|
|
598
|
+
)
|
|
599
|
+
|
|
600
|
+
return True
|
|
601
|
+
|
|
602
|
+
except Exception as e:
|
|
603
|
+
logger.error(
|
|
604
|
+
"version_check_task_error",
|
|
605
|
+
task_name=self.name,
|
|
606
|
+
error=str(e),
|
|
607
|
+
error_type=type(e).__name__,
|
|
608
|
+
)
|
|
609
|
+
return False
|