xai-review 0.32.0__py3-none-any.whl → 0.34.0__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.
Potentially problematic release.
This version of xai-review might be problematic. Click here for more details.
- ai_review/clients/openrouter/__init__.py +0 -0
- ai_review/clients/openrouter/client.py +50 -0
- ai_review/clients/openrouter/schema.py +36 -0
- ai_review/clients/openrouter/types.py +11 -0
- ai_review/libs/config/llm/base.py +8 -1
- ai_review/libs/config/llm/openrouter.py +12 -0
- ai_review/libs/constants/llm_provider.py +1 -0
- ai_review/services/artifacts/tools.py +2 -2
- ai_review/services/llm/factory.py +3 -0
- ai_review/services/llm/openrouter/__init__.py +0 -0
- ai_review/services/llm/openrouter/client.py +31 -0
- ai_review/tests/fixtures/clients/openai.py +1 -1
- ai_review/tests/fixtures/clients/openrouter.py +72 -0
- ai_review/tests/fixtures/services/artifacts.py +6 -0
- ai_review/tests/suites/clients/openrouter/__init__.py +0 -0
- ai_review/tests/suites/clients/openrouter/test_client.py +12 -0
- ai_review/tests/suites/clients/openrouter/test_schema.py +57 -0
- ai_review/tests/suites/services/artifacts/__init__.py +0 -0
- ai_review/tests/suites/services/artifacts/test_service.py +92 -0
- ai_review/tests/suites/services/artifacts/test_tools.py +28 -0
- ai_review/tests/suites/services/llm/openrouter/__init__.py +0 -0
- ai_review/tests/suites/services/llm/openrouter/test_client.py +22 -0
- ai_review/tests/suites/services/llm/test_factory.py +7 -0
- ai_review/tests/suites/services/review/gateway/test_review_comment_gateway.py +5 -5
- ai_review/tests/suites/services/review/gateway/test_review_dry_run_comment_gateway.py +5 -5
- ai_review/tests/suites/services/review/gateway/test_review_llm_gateway.py +2 -2
- ai_review/tests/suites/services/review/test_service.py +2 -2
- {xai_review-0.32.0.dist-info → xai_review-0.34.0.dist-info}/METADATA +11 -10
- {xai_review-0.32.0.dist-info → xai_review-0.34.0.dist-info}/RECORD +33 -17
- {xai_review-0.32.0.dist-info → xai_review-0.34.0.dist-info}/WHEEL +0 -0
- {xai_review-0.32.0.dist-info → xai_review-0.34.0.dist-info}/entry_points.txt +0 -0
- {xai_review-0.32.0.dist-info → xai_review-0.34.0.dist-info}/licenses/LICENSE +0 -0
- {xai_review-0.32.0.dist-info → xai_review-0.34.0.dist-info}/top_level.txt +0 -0
|
File without changes
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from httpx import AsyncClient, Response, AsyncHTTPTransport
|
|
2
|
+
|
|
3
|
+
from ai_review.clients.openrouter.schema import OpenRouterChatRequestSchema, OpenRouterChatResponseSchema
|
|
4
|
+
from ai_review.clients.openrouter.types import OpenRouterHTTPClientProtocol
|
|
5
|
+
from ai_review.config import settings
|
|
6
|
+
from ai_review.libs.http.client import HTTPClient
|
|
7
|
+
from ai_review.libs.http.event_hooks.logger import LoggerEventHook
|
|
8
|
+
from ai_review.libs.http.handlers import HTTPClientError, handle_http_error
|
|
9
|
+
from ai_review.libs.http.transports.retry import RetryTransport
|
|
10
|
+
from ai_review.libs.logger import get_logger
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class OpenRouterHTTPClientError(HTTPClientError):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class OpenRouterHTTPClient(HTTPClient, OpenRouterHTTPClientProtocol):
|
|
18
|
+
@handle_http_error(client="OpenRouterHTTPClient", exception=OpenRouterHTTPClientError)
|
|
19
|
+
async def chat_api(self, request: OpenRouterChatRequestSchema) -> Response:
|
|
20
|
+
return await self.post("/chat/completions", json=request.model_dump())
|
|
21
|
+
|
|
22
|
+
async def chat(self, request: OpenRouterChatRequestSchema) -> OpenRouterChatResponseSchema:
|
|
23
|
+
response = await self.chat_api(request)
|
|
24
|
+
return OpenRouterChatResponseSchema.model_validate_json(response.text)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_openrouter_http_client() -> OpenRouterHTTPClient:
|
|
28
|
+
logger = get_logger("OPENROUTER_HTTP_CLIENT")
|
|
29
|
+
logger_event_hook = LoggerEventHook(logger=logger)
|
|
30
|
+
retry_transport = RetryTransport(logger=logger, transport=AsyncHTTPTransport())
|
|
31
|
+
|
|
32
|
+
headers = {"Authorization": f"Bearer {settings.llm.http_client.api_token_value}"}
|
|
33
|
+
if settings.llm.meta.title:
|
|
34
|
+
headers["X-Title"] = settings.llm.meta.title
|
|
35
|
+
|
|
36
|
+
if settings.llm.meta.referer:
|
|
37
|
+
headers["Referer"] = settings.llm.meta.referer
|
|
38
|
+
|
|
39
|
+
client = AsyncClient(
|
|
40
|
+
timeout=settings.llm.http_client.timeout,
|
|
41
|
+
headers=headers,
|
|
42
|
+
base_url=settings.llm.http_client.api_url_value,
|
|
43
|
+
transport=retry_transport,
|
|
44
|
+
event_hooks={
|
|
45
|
+
"request": [logger_event_hook.request],
|
|
46
|
+
"response": [logger_event_hook.response],
|
|
47
|
+
},
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
return OpenRouterHTTPClient(client=client)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from typing import Literal
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class OpenRouterUsageSchema(BaseModel):
|
|
7
|
+
total_tokens: int
|
|
8
|
+
prompt_tokens: int
|
|
9
|
+
completion_tokens: int
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class OpenRouterMessageSchema(BaseModel):
|
|
13
|
+
role: Literal["system", "user", "assistant"]
|
|
14
|
+
content: str
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class OpenRouterChoiceSchema(BaseModel):
|
|
18
|
+
message: OpenRouterMessageSchema
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class OpenRouterChatRequestSchema(BaseModel):
|
|
22
|
+
model: str
|
|
23
|
+
messages: list[OpenRouterMessageSchema]
|
|
24
|
+
max_tokens: int
|
|
25
|
+
temperature: float
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class OpenRouterChatResponseSchema(BaseModel):
|
|
29
|
+
usage: OpenRouterUsageSchema
|
|
30
|
+
choices: list[OpenRouterChoiceSchema]
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def first_text(self) -> str:
|
|
34
|
+
if not self.choices:
|
|
35
|
+
return ""
|
|
36
|
+
return (self.choices[0].message.content or "").strip()
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from typing import Protocol
|
|
2
|
+
|
|
3
|
+
from ai_review.clients.openrouter.schema import (
|
|
4
|
+
OpenRouterChatRequestSchema,
|
|
5
|
+
OpenRouterChatResponseSchema
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class OpenRouterHTTPClientProtocol(Protocol):
|
|
10
|
+
async def chat(self, request: OpenRouterChatRequestSchema) -> OpenRouterChatResponseSchema:
|
|
11
|
+
...
|
|
@@ -8,6 +8,7 @@ from ai_review.libs.config.llm.claude import ClaudeHTTPClientConfig, ClaudeMetaC
|
|
|
8
8
|
from ai_review.libs.config.llm.gemini import GeminiHTTPClientConfig, GeminiMetaConfig
|
|
9
9
|
from ai_review.libs.config.llm.ollama import OllamaHTTPClientConfig, OllamaMetaConfig
|
|
10
10
|
from ai_review.libs.config.llm.openai import OpenAIHTTPClientConfig, OpenAIMetaConfig
|
|
11
|
+
from ai_review.libs.config.llm.openrouter import OpenRouterHTTPClientConfig, OpenRouterMetaConfig
|
|
11
12
|
from ai_review.libs.constants.llm_provider import LLMProvider
|
|
12
13
|
from ai_review.libs.resources import load_resource
|
|
13
14
|
|
|
@@ -62,7 +63,13 @@ class OllamaLLMConfig(LLMConfigBase):
|
|
|
62
63
|
http_client: OllamaHTTPClientConfig
|
|
63
64
|
|
|
64
65
|
|
|
66
|
+
class OpenRouterLLMConfig(LLMConfigBase):
|
|
67
|
+
meta: OpenRouterMetaConfig
|
|
68
|
+
provider: Literal[LLMProvider.OPENROUTER]
|
|
69
|
+
http_client: OpenRouterHTTPClientConfig
|
|
70
|
+
|
|
71
|
+
|
|
65
72
|
LLMConfig = Annotated[
|
|
66
|
-
OpenAILLMConfig | GeminiLLMConfig | ClaudeLLMConfig | OllamaLLMConfig,
|
|
73
|
+
OpenAILLMConfig | GeminiLLMConfig | ClaudeLLMConfig | OllamaLLMConfig | OpenRouterLLMConfig,
|
|
67
74
|
Field(discriminator="provider")
|
|
68
75
|
]
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from ai_review.libs.config.http import HTTPClientWithTokenConfig
|
|
2
|
+
from ai_review.libs.config.llm.meta import LLMMetaConfig
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class OpenRouterMetaConfig(LLMMetaConfig):
|
|
6
|
+
model: str = "openai/gpt-4o-mini"
|
|
7
|
+
title: str | None = None
|
|
8
|
+
referer: str | None = None
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class OpenRouterHTTPClientConfig(HTTPClientWithTokenConfig):
|
|
12
|
+
pass
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import hashlib
|
|
2
|
-
from datetime import datetime
|
|
2
|
+
from datetime import datetime, timezone
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
def make_artifact_id(text: str) -> str:
|
|
6
6
|
sha = hashlib.sha1(text.encode()).hexdigest()[:8]
|
|
7
|
-
ts = datetime.
|
|
7
|
+
ts = datetime.now(timezone.utc).strftime("%Y.%m.%d_%H-%M-%S")
|
|
8
8
|
return f"{ts}_{sha}"
|
|
@@ -4,6 +4,7 @@ from ai_review.services.llm.claude.client import ClaudeLLMClient
|
|
|
4
4
|
from ai_review.services.llm.gemini.client import GeminiLLMClient
|
|
5
5
|
from ai_review.services.llm.ollama.client import OllamaLLMClient
|
|
6
6
|
from ai_review.services.llm.openai.client import OpenAILLMClient
|
|
7
|
+
from ai_review.services.llm.openrouter.client import OpenRouterLLMClient
|
|
7
8
|
from ai_review.services.llm.types import LLMClientProtocol
|
|
8
9
|
|
|
9
10
|
|
|
@@ -17,5 +18,7 @@ def get_llm_client() -> LLMClientProtocol:
|
|
|
17
18
|
return ClaudeLLMClient()
|
|
18
19
|
case LLMProvider.OLLAMA:
|
|
19
20
|
return OllamaLLMClient()
|
|
21
|
+
case LLMProvider.OPENROUTER:
|
|
22
|
+
return OpenRouterLLMClient()
|
|
20
23
|
case _:
|
|
21
24
|
raise ValueError(f"Unsupported LLM provider: {settings.llm.provider}")
|
|
File without changes
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from ai_review.clients.openrouter.client import get_openrouter_http_client
|
|
2
|
+
from ai_review.clients.openrouter.schema import (
|
|
3
|
+
OpenRouterMessageSchema,
|
|
4
|
+
OpenRouterChatRequestSchema,
|
|
5
|
+
)
|
|
6
|
+
from ai_review.config import settings
|
|
7
|
+
from ai_review.services.llm.types import LLMClientProtocol, ChatResultSchema
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class OpenRouterLLMClient(LLMClientProtocol):
|
|
11
|
+
def __init__(self):
|
|
12
|
+
self.http_client = get_openrouter_http_client()
|
|
13
|
+
|
|
14
|
+
async def chat(self, prompt: str, prompt_system: str) -> ChatResultSchema:
|
|
15
|
+
meta = settings.llm.meta
|
|
16
|
+
request = OpenRouterChatRequestSchema(
|
|
17
|
+
model=meta.model,
|
|
18
|
+
messages=[
|
|
19
|
+
OpenRouterMessageSchema(role="system", content=prompt_system),
|
|
20
|
+
OpenRouterMessageSchema(role="user", content=prompt),
|
|
21
|
+
],
|
|
22
|
+
max_tokens=meta.max_tokens,
|
|
23
|
+
temperature=meta.temperature,
|
|
24
|
+
)
|
|
25
|
+
response = await self.http_client.chat(request)
|
|
26
|
+
return ChatResultSchema(
|
|
27
|
+
text=response.first_text,
|
|
28
|
+
total_tokens=response.usage.total_tokens,
|
|
29
|
+
prompt_tokens=response.usage.prompt_tokens,
|
|
30
|
+
completion_tokens=response.usage.completion_tokens,
|
|
31
|
+
)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from pydantic import HttpUrl, SecretStr
|
|
5
|
+
|
|
6
|
+
from ai_review.clients.openrouter.schema import (
|
|
7
|
+
OpenRouterUsageSchema,
|
|
8
|
+
OpenRouterChoiceSchema,
|
|
9
|
+
OpenRouterMessageSchema,
|
|
10
|
+
OpenRouterChatRequestSchema,
|
|
11
|
+
OpenRouterChatResponseSchema,
|
|
12
|
+
)
|
|
13
|
+
from ai_review.clients.openrouter.types import OpenRouterHTTPClientProtocol
|
|
14
|
+
from ai_review.config import settings
|
|
15
|
+
from ai_review.libs.config.llm.base import OpenRouterLLMConfig
|
|
16
|
+
from ai_review.libs.config.llm.openrouter import OpenRouterMetaConfig, OpenRouterHTTPClientConfig
|
|
17
|
+
from ai_review.libs.constants.llm_provider import LLMProvider
|
|
18
|
+
from ai_review.services.llm.openrouter.client import OpenRouterLLMClient
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class FakeOpenRouterHTTPClient(OpenRouterHTTPClientProtocol):
|
|
22
|
+
def __init__(self, responses: dict[str, Any] | None = None) -> None:
|
|
23
|
+
self.calls: list[tuple[str, dict]] = []
|
|
24
|
+
self.responses = responses or {}
|
|
25
|
+
|
|
26
|
+
async def chat(self, request: OpenRouterChatRequestSchema) -> OpenRouterChatResponseSchema:
|
|
27
|
+
self.calls.append(("chat", {"request": request}))
|
|
28
|
+
return self.responses.get(
|
|
29
|
+
"chat",
|
|
30
|
+
OpenRouterChatResponseSchema(
|
|
31
|
+
usage=OpenRouterUsageSchema(total_tokens=12, prompt_tokens=5, completion_tokens=7),
|
|
32
|
+
choices=[
|
|
33
|
+
OpenRouterChoiceSchema(
|
|
34
|
+
message=OpenRouterMessageSchema(
|
|
35
|
+
role="assistant",
|
|
36
|
+
content="FAKE_OPENROUTER_RESPONSE"
|
|
37
|
+
)
|
|
38
|
+
)
|
|
39
|
+
],
|
|
40
|
+
),
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@pytest.fixture
|
|
45
|
+
def fake_openrouter_http_client() -> FakeOpenRouterHTTPClient:
|
|
46
|
+
return FakeOpenRouterHTTPClient()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@pytest.fixture
|
|
50
|
+
def openrouter_llm_client(
|
|
51
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
52
|
+
fake_openrouter_http_client: FakeOpenRouterHTTPClient
|
|
53
|
+
) -> OpenRouterLLMClient:
|
|
54
|
+
monkeypatch.setattr(
|
|
55
|
+
"ai_review.services.llm.openrouter.client.get_openrouter_http_client",
|
|
56
|
+
lambda: fake_openrouter_http_client,
|
|
57
|
+
)
|
|
58
|
+
return OpenRouterLLMClient()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@pytest.fixture
|
|
62
|
+
def openrouter_http_client_config(monkeypatch: pytest.MonkeyPatch):
|
|
63
|
+
fake_config = OpenRouterLLMConfig(
|
|
64
|
+
meta=OpenRouterMetaConfig(),
|
|
65
|
+
provider=LLMProvider.OPENROUTER,
|
|
66
|
+
http_client=OpenRouterHTTPClientConfig(
|
|
67
|
+
timeout=10,
|
|
68
|
+
api_url=HttpUrl("https://openrouter.ai/api/v1"),
|
|
69
|
+
api_token=SecretStr("fake-token"),
|
|
70
|
+
)
|
|
71
|
+
)
|
|
72
|
+
monkeypatch.setattr(settings, "llm", fake_config)
|
|
@@ -2,6 +2,7 @@ from pathlib import Path
|
|
|
2
2
|
|
|
3
3
|
import pytest
|
|
4
4
|
|
|
5
|
+
from ai_review.services.artifacts.service import ArtifactsService
|
|
5
6
|
from ai_review.services.artifacts.types import ArtifactsServiceProtocol
|
|
6
7
|
|
|
7
8
|
|
|
@@ -49,3 +50,8 @@ class FakeArtifactsService(ArtifactsServiceProtocol):
|
|
|
49
50
|
@pytest.fixture
|
|
50
51
|
def fake_artifacts_service() -> FakeArtifactsService:
|
|
51
52
|
return FakeArtifactsService()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@pytest.fixture
|
|
56
|
+
def artifacts_service() -> ArtifactsService:
|
|
57
|
+
return ArtifactsService()
|
|
File without changes
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from httpx import AsyncClient
|
|
3
|
+
|
|
4
|
+
from ai_review.clients.openrouter.client import get_openrouter_http_client, OpenRouterHTTPClient
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@pytest.mark.usefixtures('openrouter_http_client_config')
|
|
8
|
+
def test_get_openrouter_http_client_builds_ok():
|
|
9
|
+
openrouter_http_client = get_openrouter_http_client()
|
|
10
|
+
|
|
11
|
+
assert isinstance(openrouter_http_client, OpenRouterHTTPClient)
|
|
12
|
+
assert isinstance(openrouter_http_client.client, AsyncClient)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from ai_review.clients.openrouter.schema import (
|
|
2
|
+
OpenRouterUsageSchema,
|
|
3
|
+
OpenRouterChoiceSchema,
|
|
4
|
+
OpenRouterMessageSchema,
|
|
5
|
+
OpenRouterChatRequestSchema,
|
|
6
|
+
OpenRouterChatResponseSchema,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# ---------- OpenRouterChatResponseSchema ----------
|
|
11
|
+
|
|
12
|
+
def test_first_text_returns_text():
|
|
13
|
+
resp = OpenRouterChatResponseSchema(
|
|
14
|
+
usage=OpenRouterUsageSchema(total_tokens=5, prompt_tokens=2, completion_tokens=3),
|
|
15
|
+
choices=[
|
|
16
|
+
OpenRouterChoiceSchema(
|
|
17
|
+
message=OpenRouterMessageSchema(role="assistant", content=" hello world ")
|
|
18
|
+
)
|
|
19
|
+
],
|
|
20
|
+
)
|
|
21
|
+
assert resp.first_text == "hello world"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_first_text_empty_if_no_choices():
|
|
25
|
+
resp = OpenRouterChatResponseSchema(
|
|
26
|
+
usage=OpenRouterUsageSchema(total_tokens=1, prompt_tokens=1, completion_tokens=0),
|
|
27
|
+
choices=[],
|
|
28
|
+
)
|
|
29
|
+
assert resp.first_text == ""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_first_text_strips_and_handles_empty_content():
|
|
33
|
+
resp = OpenRouterChatResponseSchema(
|
|
34
|
+
usage=OpenRouterUsageSchema(total_tokens=1, prompt_tokens=1, completion_tokens=0),
|
|
35
|
+
choices=[
|
|
36
|
+
OpenRouterChoiceSchema(
|
|
37
|
+
message=OpenRouterMessageSchema(role="assistant", content=" ")
|
|
38
|
+
)
|
|
39
|
+
],
|
|
40
|
+
)
|
|
41
|
+
assert resp.first_text == ""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# ---------- OpenRouterChatRequestSchema ----------
|
|
45
|
+
|
|
46
|
+
def test_chat_request_schema_builds_ok():
|
|
47
|
+
msg = OpenRouterMessageSchema(role="user", content="hello")
|
|
48
|
+
req = OpenRouterChatRequestSchema(
|
|
49
|
+
model="gpt-4o-mini",
|
|
50
|
+
messages=[msg],
|
|
51
|
+
max_tokens=100,
|
|
52
|
+
temperature=0.3,
|
|
53
|
+
)
|
|
54
|
+
assert req.model == "gpt-4o-mini"
|
|
55
|
+
assert req.messages[0].content == "hello"
|
|
56
|
+
assert req.max_tokens == 100
|
|
57
|
+
assert req.temperature == 0.3
|
|
File without changes
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import aiofiles
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from ai_review.config import settings
|
|
8
|
+
from ai_review.libs.config.artifacts import ArtifactsConfig
|
|
9
|
+
from ai_review.services.artifacts.service import ArtifactsService
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@pytest.mark.asyncio
|
|
13
|
+
async def test_save_llm_interaction_creates_file(
|
|
14
|
+
tmp_path: Path,
|
|
15
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
16
|
+
artifacts_service: ArtifactsService
|
|
17
|
+
):
|
|
18
|
+
"""Checks that a JSON file is created with the correct name and content when LLM saving is enabled."""
|
|
19
|
+
monkeypatch.setattr(settings, "artifacts", ArtifactsConfig(llm_dir=tmp_path, llm_enabled=True))
|
|
20
|
+
|
|
21
|
+
artifact_id = await artifacts_service.save_llm_interaction(
|
|
22
|
+
prompt="Hello world",
|
|
23
|
+
prompt_system="system prompt",
|
|
24
|
+
response="model answer"
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
assert artifact_id is not None
|
|
28
|
+
artifact_path = tmp_path / f"{artifact_id}.json"
|
|
29
|
+
assert artifact_path.exists(), "Artifact file was not created"
|
|
30
|
+
|
|
31
|
+
async with aiofiles.open(artifact_path, "r", encoding="utf-8") as file:
|
|
32
|
+
content = await file.read()
|
|
33
|
+
data = json.loads(content)
|
|
34
|
+
assert data["id"] == artifact_id
|
|
35
|
+
assert data["prompt"] == "Hello world"
|
|
36
|
+
assert data["response"] == "model answer"
|
|
37
|
+
assert data["prompt_system"] == "system prompt"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@pytest.mark.asyncio
|
|
41
|
+
async def test_save_llm_interaction_disabled(
|
|
42
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
43
|
+
artifacts_service: ArtifactsService
|
|
44
|
+
):
|
|
45
|
+
"""Checks that the method returns None and does not create a file if LLM saving is disabled."""
|
|
46
|
+
monkeypatch.setattr(settings, "artifacts", ArtifactsConfig(llm_enabled=False))
|
|
47
|
+
|
|
48
|
+
artifact_id = await artifacts_service.save_llm_interaction(
|
|
49
|
+
prompt="ignored",
|
|
50
|
+
prompt_system="ignored",
|
|
51
|
+
response="ignored"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
assert artifact_id is None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@pytest.mark.asyncio
|
|
58
|
+
async def test_save_artifact_writes_file(tmp_path: Path, artifacts_service: ArtifactsService):
|
|
59
|
+
"""Checks that save_artifact writes content to the given file path."""
|
|
60
|
+
file_path = tmp_path / "test.json"
|
|
61
|
+
content = '{"key": "value"}'
|
|
62
|
+
|
|
63
|
+
result = await artifacts_service.save_artifact(file=file_path, content=content, kind="test")
|
|
64
|
+
|
|
65
|
+
assert result == file_path
|
|
66
|
+
assert file_path.exists()
|
|
67
|
+
async with aiofiles.open(file_path, "r", encoding="utf-8") as f:
|
|
68
|
+
saved_content = await f.read()
|
|
69
|
+
assert saved_content == content
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@pytest.mark.asyncio
|
|
73
|
+
async def test_save_artifact_handles_exception(
|
|
74
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
75
|
+
artifacts_service: ArtifactsService
|
|
76
|
+
):
|
|
77
|
+
"""Checks that save_artifact gracefully returns None on write error."""
|
|
78
|
+
|
|
79
|
+
class BrokenAsyncFile:
|
|
80
|
+
async def __aenter__(self):
|
|
81
|
+
raise OSError("disk full")
|
|
82
|
+
|
|
83
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
84
|
+
return False
|
|
85
|
+
|
|
86
|
+
monkeypatch.setattr(
|
|
87
|
+
"ai_review.services.artifacts.service.aiofiles.open",
|
|
88
|
+
lambda *args, **kwargs: BrokenAsyncFile()
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
result = await artifacts_service.save_artifact(Path("/fake/path.json"), "data")
|
|
92
|
+
assert result is None
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import re
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from ai_review.services.artifacts.tools import make_artifact_id
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@pytest.mark.parametrize("text", ["hello", "some longer text", ""])
|
|
10
|
+
def test_make_artifact_id_format_and_sha(text: str):
|
|
11
|
+
"""Checks that the function returns a valid artifact ID format and correct SHA prefix."""
|
|
12
|
+
artifact_id = make_artifact_id(text)
|
|
13
|
+
|
|
14
|
+
pattern = r"^\d{4}\.\d{2}\.\d{2}_\d{2}-\d{2}-\d{2}_[0-9a-f]{8}$"
|
|
15
|
+
assert re.match(pattern, artifact_id), f"Invalid format: {artifact_id}"
|
|
16
|
+
|
|
17
|
+
expected_sha = hashlib.sha1(text.encode()).hexdigest()[:8]
|
|
18
|
+
assert artifact_id.endswith(expected_sha)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_make_artifact_id_is_deterministic_for_same_input():
|
|
22
|
+
"""Checks that the SHA part is deterministic (identical for the same input)."""
|
|
23
|
+
sha1 = hashlib.sha1("repeatable".encode()).hexdigest()[:8]
|
|
24
|
+
artifact_id1 = make_artifact_id("repeatable")
|
|
25
|
+
artifact_id2 = make_artifact_id("repeatable")
|
|
26
|
+
|
|
27
|
+
assert artifact_id1.split("_")[2] == sha1
|
|
28
|
+
assert artifact_id2.split("_")[2] == sha1
|
|
File without changes
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from ai_review.services.llm.openrouter.client import OpenRouterLLMClient
|
|
4
|
+
from ai_review.services.llm.types import ChatResultSchema
|
|
5
|
+
from ai_review.tests.fixtures.clients.openrouter import FakeOpenRouterHTTPClient
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@pytest.mark.asyncio
|
|
9
|
+
@pytest.mark.usefixtures("openrouter_http_client_config")
|
|
10
|
+
async def test_openrouter_llm_chat(
|
|
11
|
+
openrouter_llm_client: OpenRouterLLMClient,
|
|
12
|
+
fake_openrouter_http_client: FakeOpenRouterHTTPClient
|
|
13
|
+
):
|
|
14
|
+
result = await openrouter_llm_client.chat("prompt", "prompt_system")
|
|
15
|
+
|
|
16
|
+
assert isinstance(result, ChatResultSchema)
|
|
17
|
+
assert result.text == "FAKE_OPENROUTER_RESPONSE"
|
|
18
|
+
assert result.total_tokens == 12
|
|
19
|
+
assert result.prompt_tokens == 5
|
|
20
|
+
assert result.completion_tokens == 7
|
|
21
|
+
|
|
22
|
+
assert fake_openrouter_http_client.calls[0][0] == "chat"
|
|
@@ -5,6 +5,7 @@ from ai_review.services.llm.factory import get_llm_client
|
|
|
5
5
|
from ai_review.services.llm.gemini.client import GeminiLLMClient
|
|
6
6
|
from ai_review.services.llm.ollama.client import OllamaLLMClient
|
|
7
7
|
from ai_review.services.llm.openai.client import OpenAILLMClient
|
|
8
|
+
from ai_review.services.llm.openrouter.client import OpenRouterLLMClient
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
@pytest.mark.usefixtures("openai_http_client_config")
|
|
@@ -31,6 +32,12 @@ def test_get_llm_client_returns_ollama(monkeypatch: pytest.MonkeyPatch):
|
|
|
31
32
|
assert isinstance(client, OllamaLLMClient)
|
|
32
33
|
|
|
33
34
|
|
|
35
|
+
@pytest.mark.usefixtures("openrouter_http_client_config")
|
|
36
|
+
def test_get_llm_client_returns_openrouter(monkeypatch: pytest.MonkeyPatch):
|
|
37
|
+
client = get_llm_client()
|
|
38
|
+
assert isinstance(client, OpenRouterLLMClient)
|
|
39
|
+
|
|
40
|
+
|
|
34
41
|
def test_get_llm_client_unsupported_provider(monkeypatch: pytest.MonkeyPatch):
|
|
35
42
|
monkeypatch.setattr("ai_review.services.llm.factory.settings.llm.provider", "UNSUPPORTED")
|
|
36
43
|
with pytest.raises(ValueError):
|
|
@@ -72,7 +72,7 @@ async def test_get_summary_threads_filters_by_tag(
|
|
|
72
72
|
|
|
73
73
|
@pytest.mark.asyncio
|
|
74
74
|
async def test_has_existing_inline_comments_true(
|
|
75
|
-
capsys,
|
|
75
|
+
capsys: pytest.CaptureFixture,
|
|
76
76
|
fake_vcs_client: FakeVCSClient,
|
|
77
77
|
review_comment_gateway: ReviewCommentGateway,
|
|
78
78
|
):
|
|
@@ -118,7 +118,7 @@ async def test_process_inline_reply_happy_path(
|
|
|
118
118
|
|
|
119
119
|
@pytest.mark.asyncio
|
|
120
120
|
async def test_process_inline_reply_error(
|
|
121
|
-
capsys,
|
|
121
|
+
capsys: pytest.CaptureFixture,
|
|
122
122
|
fake_vcs_client: FakeVCSClient,
|
|
123
123
|
review_comment_gateway: ReviewCommentGateway,
|
|
124
124
|
):
|
|
@@ -151,7 +151,7 @@ async def test_process_summary_reply_success(
|
|
|
151
151
|
|
|
152
152
|
@pytest.mark.asyncio
|
|
153
153
|
async def test_process_summary_reply_error(
|
|
154
|
-
capsys,
|
|
154
|
+
capsys: pytest.CaptureFixture,
|
|
155
155
|
fake_vcs_client: FakeVCSClient,
|
|
156
156
|
review_comment_gateway: ReviewCommentGateway,
|
|
157
157
|
):
|
|
@@ -184,7 +184,7 @@ async def test_process_inline_comment_happy_path(
|
|
|
184
184
|
|
|
185
185
|
@pytest.mark.asyncio
|
|
186
186
|
async def test_process_inline_comment_error_fallback(
|
|
187
|
-
capsys,
|
|
187
|
+
capsys: pytest.CaptureFixture,
|
|
188
188
|
fake_vcs_client: FakeVCSClient,
|
|
189
189
|
review_comment_gateway: ReviewCommentGateway,
|
|
190
190
|
):
|
|
@@ -218,7 +218,7 @@ async def test_process_summary_comment_happy_path(
|
|
|
218
218
|
|
|
219
219
|
@pytest.mark.asyncio
|
|
220
220
|
async def test_process_summary_comment_error(
|
|
221
|
-
capsys,
|
|
221
|
+
capsys: pytest.CaptureFixture,
|
|
222
222
|
fake_vcs_client: FakeVCSClient,
|
|
223
223
|
review_comment_gateway: ReviewCommentGateway,
|
|
224
224
|
):
|
|
@@ -10,7 +10,7 @@ from ai_review.tests.fixtures.services.vcs import FakeVCSClient
|
|
|
10
10
|
|
|
11
11
|
@pytest.mark.asyncio
|
|
12
12
|
async def test_process_inline_reply_dry_run_logs_and_no_vcs_calls(
|
|
13
|
-
capsys,
|
|
13
|
+
capsys: pytest.CaptureFixture,
|
|
14
14
|
fake_vcs_client: FakeVCSClient,
|
|
15
15
|
review_dry_run_comment_gateway: ReviewDryRunCommentGateway
|
|
16
16
|
):
|
|
@@ -26,7 +26,7 @@ async def test_process_inline_reply_dry_run_logs_and_no_vcs_calls(
|
|
|
26
26
|
|
|
27
27
|
@pytest.mark.asyncio
|
|
28
28
|
async def test_process_summary_reply_dry_run_logs_and_no_vcs_calls(
|
|
29
|
-
capsys,
|
|
29
|
+
capsys: pytest.CaptureFixture,
|
|
30
30
|
fake_vcs_client: FakeVCSClient,
|
|
31
31
|
review_dry_run_comment_gateway: ReviewDryRunCommentGateway
|
|
32
32
|
):
|
|
@@ -42,7 +42,7 @@ async def test_process_summary_reply_dry_run_logs_and_no_vcs_calls(
|
|
|
42
42
|
|
|
43
43
|
@pytest.mark.asyncio
|
|
44
44
|
async def test_process_inline_comment_dry_run_logs_and_no_vcs_calls(
|
|
45
|
-
capsys,
|
|
45
|
+
capsys: pytest.CaptureFixture,
|
|
46
46
|
fake_vcs_client: FakeVCSClient,
|
|
47
47
|
review_dry_run_comment_gateway: ReviewDryRunCommentGateway
|
|
48
48
|
):
|
|
@@ -59,7 +59,7 @@ async def test_process_inline_comment_dry_run_logs_and_no_vcs_calls(
|
|
|
59
59
|
|
|
60
60
|
@pytest.mark.asyncio
|
|
61
61
|
async def test_process_summary_comment_dry_run_logs_and_no_vcs_calls(
|
|
62
|
-
capsys,
|
|
62
|
+
capsys: pytest.CaptureFixture,
|
|
63
63
|
fake_vcs_client: FakeVCSClient,
|
|
64
64
|
review_dry_run_comment_gateway: ReviewDryRunCommentGateway
|
|
65
65
|
):
|
|
@@ -75,7 +75,7 @@ async def test_process_summary_comment_dry_run_logs_and_no_vcs_calls(
|
|
|
75
75
|
|
|
76
76
|
@pytest.mark.asyncio
|
|
77
77
|
async def test_process_inline_comments_iterates_all(
|
|
78
|
-
capsys,
|
|
78
|
+
capsys: pytest.CaptureFixture,
|
|
79
79
|
fake_vcs_client: FakeVCSClient,
|
|
80
80
|
review_dry_run_comment_gateway: ReviewDryRunCommentGateway
|
|
81
81
|
):
|
|
@@ -27,7 +27,7 @@ async def test_ask_happy_path(
|
|
|
27
27
|
|
|
28
28
|
@pytest.mark.asyncio
|
|
29
29
|
async def test_ask_warns_on_empty_response(
|
|
30
|
-
capsys,
|
|
30
|
+
capsys: pytest.CaptureFixture,
|
|
31
31
|
review_llm_gateway: ReviewLLMGateway,
|
|
32
32
|
fake_llm_client: FakeLLMClient,
|
|
33
33
|
fake_cost_service: FakeCostService,
|
|
@@ -49,7 +49,7 @@ async def test_ask_warns_on_empty_response(
|
|
|
49
49
|
|
|
50
50
|
@pytest.mark.asyncio
|
|
51
51
|
async def test_ask_handles_llm_error(
|
|
52
|
-
capsys,
|
|
52
|
+
capsys: pytest.CaptureFixture,
|
|
53
53
|
fake_llm_client: FakeLLMClient,
|
|
54
54
|
review_llm_gateway: ReviewLLMGateway,
|
|
55
55
|
):
|
|
@@ -63,7 +63,7 @@ async def test_run_summary_reply_review_invokes_runner(
|
|
|
63
63
|
|
|
64
64
|
|
|
65
65
|
def test_report_total_cost_with_data(
|
|
66
|
-
capsys,
|
|
66
|
+
capsys: pytest.CaptureFixture,
|
|
67
67
|
review_service: ReviewService,
|
|
68
68
|
fake_cost_service: FakeCostService
|
|
69
69
|
):
|
|
@@ -87,7 +87,7 @@ def test_report_total_cost_with_data(
|
|
|
87
87
|
assert "0.006" in output
|
|
88
88
|
|
|
89
89
|
|
|
90
|
-
def test_report_total_cost_no_data(capsys, review_service: ReviewService):
|
|
90
|
+
def test_report_total_cost_no_data(capsys: pytest.CaptureFixture, review_service: ReviewService):
|
|
91
91
|
"""Should log message when no cost data is available."""
|
|
92
92
|
review_service.report_total_cost()
|
|
93
93
|
output = capsys.readouterr().out
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: xai-review
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: AI-powered code review tool
|
|
3
|
+
Version: 0.34.0
|
|
4
|
+
Summary: AI-powered code review tool for GitHub, GitLab, Bitbucket and Gitea — built with LLMs like OpenAI, Claude, Gemini, Ollama, and OpenRouter
|
|
5
5
|
Author-email: Nikita Filonov <nikita.filonov@example.com>
|
|
6
6
|
Maintainer-email: Nikita Filonov <nikita.filonov@example.com>
|
|
7
7
|
License: Apache-2.0
|
|
8
8
|
Project-URL: Issues, https://github.com/Nikita-Filonov/ai-review/issues
|
|
9
9
|
Project-URL: Homepage, https://github.com/Nikita-Filonov/ai-review
|
|
10
10
|
Project-URL: Repository, https://github.com/Nikita-Filonov/ai-review
|
|
11
|
-
Keywords: ai,code review,llm,openai,claude,gemini
|
|
11
|
+
Keywords: ai,code review,llm,openai,claude,gemini,ollama,openrouter,ci/cd,gitlab,github,gitea,bitbucket
|
|
12
12
|
Classifier: Programming Language :: Python :: 3
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.11
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.12
|
|
@@ -66,7 +66,8 @@ improve code quality, enforce consistency, and speed up the review process.
|
|
|
66
66
|
|
|
67
67
|
✨ Key features:
|
|
68
68
|
|
|
69
|
-
- **Multiple LLM providers** — choose between **OpenAI**, **Claude**, **Gemini**, or **
|
|
69
|
+
- **Multiple LLM providers** — choose between **OpenAI**, **Claude**, **Gemini**, **Ollama**, or **OpenRouter**, and
|
|
70
|
+
switch anytime.
|
|
70
71
|
- **VCS integration** — works out of the box with **GitLab**, **GitHub**, **Bitbucket**, and **Gitea**.
|
|
71
72
|
- **Customizable prompts** — adapt inline, context, and summary reviews to match your team’s coding guidelines.
|
|
72
73
|
- **Reply modes** — AI can now **participate in existing review threads**, adding follow-up replies in both inline and
|
|
@@ -120,7 +121,7 @@ pip install xai-review
|
|
|
120
121
|
Or run directly via Docker:
|
|
121
122
|
|
|
122
123
|
```bash
|
|
123
|
-
docker run --rm -v $(pwd):/app nikitafilonov/ai-review:latest run-summary
|
|
124
|
+
docker run --rm -v $(pwd):/app nikitafilonov/ai-review:latest ai-review run-summary
|
|
124
125
|
```
|
|
125
126
|
|
|
126
127
|
🐳 Pull from [DockerHub](https://hub.docker.com/r/nikitafilonov/ai-review)
|
|
@@ -146,8 +147,8 @@ vcs:
|
|
|
146
147
|
provider: GITLAB
|
|
147
148
|
|
|
148
149
|
pipeline:
|
|
149
|
-
project_id: 1
|
|
150
|
-
merge_request_id: 100
|
|
150
|
+
project_id: "1"
|
|
151
|
+
merge_request_id: "100"
|
|
151
152
|
|
|
152
153
|
http_client:
|
|
153
154
|
timeout: 120
|
|
@@ -176,7 +177,7 @@ for complete, ready-to-use examples.
|
|
|
176
177
|
|
|
177
178
|
Key things you can customize:
|
|
178
179
|
|
|
179
|
-
- **LLM provider** — OpenAI, Gemini, Claude, or
|
|
180
|
+
- **LLM provider** — OpenAI, Gemini, Claude, Ollama, or OpenRouter
|
|
180
181
|
- **Model settings** — model name, temperature, max tokens
|
|
181
182
|
- **VCS integration** — works out of the box with **GitLab**, **GitHub**, **Bitbucket**, and **Gitea**
|
|
182
183
|
- **Review policy** — which files to include/exclude, review modes
|
|
@@ -220,7 +221,7 @@ jobs:
|
|
|
220
221
|
with:
|
|
221
222
|
fetch-depth: 0
|
|
222
223
|
|
|
223
|
-
- uses: Nikita-Filonov/ai-review@v0.
|
|
224
|
+
- uses: Nikita-Filonov/ai-review@v0.34.0
|
|
224
225
|
with:
|
|
225
226
|
review-command: ${{ inputs.review-command }}
|
|
226
227
|
env:
|
|
@@ -298,7 +299,7 @@ AI Review does **not store**, **log**, or **transmit** your source code to any e
|
|
|
298
299
|
provider** explicitly configured in your `.ai-review.yaml`.
|
|
299
300
|
|
|
300
301
|
All data is sent **directly** from your CI/CD environment to the selected LLM API endpoint (e.g. OpenAI, Gemini,
|
|
301
|
-
Claude). No intermediary servers or storage layers are involved.
|
|
302
|
+
Claude, OpenRouter). No intermediary servers or storage layers are involved.
|
|
302
303
|
|
|
303
304
|
If you use **Ollama**, requests are sent to your **local or self-hosted Ollama runtime**
|
|
304
305
|
(by default `http://localhost:11434`). This allows you to run reviews completely **offline**, keeping all data strictly
|
|
@@ -72,6 +72,10 @@ ai_review/clients/openai/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZ
|
|
|
72
72
|
ai_review/clients/openai/client.py,sha256=jY1XG_5GtNboNjkXu3UtuXFx5V9rD6UskK7VT0lOzP8,1816
|
|
73
73
|
ai_review/clients/openai/schema.py,sha256=glxwMtBrDA6W0BQgH-ruKe0bKH3Ps1P-Y1-2jGdqaUM,764
|
|
74
74
|
ai_review/clients/openai/types.py,sha256=4VRY45ihKjii8w0d5XLnUGnHuBSh9wRsOP6lmkseC0Q,267
|
|
75
|
+
ai_review/clients/openrouter/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
76
|
+
ai_review/clients/openrouter/client.py,sha256=HfcfOuUs08w7Na344i197I_WrNNjyB1xSBzirxkPGO0,2088
|
|
77
|
+
ai_review/clients/openrouter/schema.py,sha256=U4c1wNhhAVLQ85J69IjR5g5tLly8KfrwGP7T5tsJPwI,799
|
|
78
|
+
ai_review/clients/openrouter/types.py,sha256=9CFUy52GnfjjLRufz7SwY_fnzhQnn8czLl-XLWBSKGc,303
|
|
75
79
|
ai_review/libs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
76
80
|
ai_review/libs/json.py,sha256=g-P5_pNUomQ-bGHCXASvPKj9Og0s9MaLFVEAkzqGp1A,350
|
|
77
81
|
ai_review/libs/logger.py,sha256=LbXR2Zk1btJ-83I-vHee7cUETgT1mHToSsqEI_8uM0U,370
|
|
@@ -87,12 +91,13 @@ ai_review/libs/config/logger.py,sha256=oPmjpjf6EZwW7CgOjT8mOQdGnT98CLwXepiGB_ajZ
|
|
|
87
91
|
ai_review/libs/config/prompt.py,sha256=jPBYvi75u6BUIHOZnXCg5CiL0XRONDfW5T4WaYlxPEE,6066
|
|
88
92
|
ai_review/libs/config/review.py,sha256=jCfHfGfHs7Sc6H92hVUBd8bMRbLiIiThVmk6lFNlv40,1224
|
|
89
93
|
ai_review/libs/config/llm/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
90
|
-
ai_review/libs/config/llm/base.py,sha256=
|
|
94
|
+
ai_review/libs/config/llm/base.py,sha256=yhRxizyRwwYH1swKeVzZaGAVm3I3nNME_y-h0iW5WTw,2374
|
|
91
95
|
ai_review/libs/config/llm/claude.py,sha256=MoalXkBA6pEp01znS8ohTRopfea9RUcqhZX5lOIuek8,293
|
|
92
96
|
ai_review/libs/config/llm/gemini.py,sha256=SKtlzsRuNWOlM9m3SFvcqOIjnml8lpPidp7FiGmIEz4,265
|
|
93
97
|
ai_review/libs/config/llm/meta.py,sha256=cEcAHOwy-mQBKo9_KJrQe0I7qppq6h99lSmoWX4ElJI,195
|
|
94
98
|
ai_review/libs/config/llm/ollama.py,sha256=M6aiPb5GvYvkiGcgHTsh9bOw5JsBLqmfSKoIbHCejrU,372
|
|
95
99
|
ai_review/libs/config/llm/openai.py,sha256=jGVL4gJ2wIacoKeK9Zc9LCgY95TxdeYOThdglVPErFU,262
|
|
100
|
+
ai_review/libs/config/llm/openrouter.py,sha256=6G5fApCOv0fKRHCUpsuiPOcEdyUpDe5qiUUbHjA6TbE,337
|
|
96
101
|
ai_review/libs/config/vcs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
97
102
|
ai_review/libs/config/vcs/base.py,sha256=RJZhKysD-d8oYZQ2v1H74jyqdqtOCc8zZ0n9S4ovfHk,1471
|
|
98
103
|
ai_review/libs/config/vcs/bitbucket.py,sha256=on5sQaE57kM_zSmqdDUNrttVtTPGOzqLHM5s7eFN7DA,275
|
|
@@ -101,7 +106,7 @@ ai_review/libs/config/vcs/github.py,sha256=hk-kuDLd8wecqtEb8PSqF7Yy_pkihplJhi6nB
|
|
|
101
106
|
ai_review/libs/config/vcs/gitlab.py,sha256=ecYfU158VgVlM6P5mgZn8FOqk3Xt60xx7gUqT5e22a4,252
|
|
102
107
|
ai_review/libs/config/vcs/pagination.py,sha256=S-XxWQYkIjhu3ffpHQ44d7UtRHH81fh9GaJ-xQXUUy4,175
|
|
103
108
|
ai_review/libs/constants/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
104
|
-
ai_review/libs/constants/llm_provider.py,sha256=
|
|
109
|
+
ai_review/libs/constants/llm_provider.py,sha256=_ysUQFFspG1oCuCyRwdEE9kVlqLyP1hxL_Kc-tWV2F0,173
|
|
105
110
|
ai_review/libs/constants/vcs_provider.py,sha256=7A30fTSs9GM_A4J9B84MNY78c7do0RaoKytuiRwdhDY,147
|
|
106
111
|
ai_review/libs/diff/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
107
112
|
ai_review/libs/diff/models.py,sha256=RT4YJboOPA-AjNJGRj_HIZaJLEmROOhOgMh1wIGpIwY,2344
|
|
@@ -137,7 +142,7 @@ ai_review/services/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuF
|
|
|
137
142
|
ai_review/services/artifacts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
138
143
|
ai_review/services/artifacts/schema.py,sha256=o4dqG5LFCdAQY3wjRF5rImANe-X20g4zX_bCIKiHLSk,291
|
|
139
144
|
ai_review/services/artifacts/service.py,sha256=SDHwYm9I4pSPISyNWqHEOR-wTTEa5ThsIi458C9hBt8,1789
|
|
140
|
-
ai_review/services/artifacts/tools.py,sha256=
|
|
145
|
+
ai_review/services/artifacts/tools.py,sha256=KrCQxbBEBIgqUhxZwCmbj1f07B5403gkfSgpS9qLpE4,242
|
|
141
146
|
ai_review/services/artifacts/types.py,sha256=VPEDuQQciyQL8qcmgFuZxZUuuh2-xLwqwxmNZr62F3E,448
|
|
142
147
|
ai_review/services/cost/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
143
148
|
ai_review/services/cost/schema.py,sha256=K3uCIMMxGL8AaIPh4a-d0mT5uIJuk3f805DkP8o8DtY,1323
|
|
@@ -157,7 +162,7 @@ ai_review/services/hook/constants.py,sha256=GthpBviiqcb3Oj5uhiJhyYGmWuwKgLlToC8p
|
|
|
157
162
|
ai_review/services/hook/service.py,sha256=00qai06eFqkeKtIfqxlJkWTLCpO5d2wZzc2rFsPLKTs,10680
|
|
158
163
|
ai_review/services/hook/types.py,sha256=RrGFCPwRkOOLAoHpGlkcnT6Cao0B4PyPMN-Wxj6N9Uo,2461
|
|
159
164
|
ai_review/services/llm/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
160
|
-
ai_review/services/llm/factory.py,sha256=
|
|
165
|
+
ai_review/services/llm/factory.py,sha256=VF5xkGgYQFiDJJ0WNHUoXFtP4yWK6uN5qMTFPYXdTLU,1027
|
|
161
166
|
ai_review/services/llm/types.py,sha256=OvbJWYRDThBgLhn9TWU0mliuanOW01CS3e8ermtuS-s,353
|
|
162
167
|
ai_review/services/llm/claude/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
163
168
|
ai_review/services/llm/claude/client.py,sha256=JJD0FWiXjCCpO7NW3vVoBMXhTQ9VBA4Q93QqkeQqON0,1082
|
|
@@ -167,6 +172,8 @@ ai_review/services/llm/ollama/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5N
|
|
|
167
172
|
ai_review/services/llm/ollama/client.py,sha256=817nOQRsnaVqoY6LdO95l5JkRHkGvvS8TX7hezT2gqk,1479
|
|
168
173
|
ai_review/services/llm/openai/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
169
174
|
ai_review/services/llm/openai/client.py,sha256=c3DWwLnwTheERdSGnMiQIbg5SaICouUAGClcQZSh1fE,1159
|
|
175
|
+
ai_review/services/llm/openrouter/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
176
|
+
ai_review/services/llm/openrouter/client.py,sha256=wrUEETrer3XmNRtu3YzLIRNa_3ODTzsR_LU9kDkl-7I,1212
|
|
170
177
|
ai_review/services/prompt/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
171
178
|
ai_review/services/prompt/adapter.py,sha256=9ntRxG5iON2vU54k8-BmLpiPCH5jRv2GGl01nmHj-SI,988
|
|
172
179
|
ai_review/services/prompt/schema.py,sha256=ttla_lkvBQ-jfaZXzWSAAUrJROYRsozHHd5IoZc59zA,1324
|
|
@@ -232,12 +239,13 @@ ai_review/tests/fixtures/clients/gitea.py,sha256=WQLbOyFTqqtVQGHuLFgk9qANYS03eeC
|
|
|
232
239
|
ai_review/tests/fixtures/clients/github.py,sha256=kC1L-nWZMn9O_uRfuT_B8R4sn8FRvISlBJMkRKaioS0,7814
|
|
233
240
|
ai_review/tests/fixtures/clients/gitlab.py,sha256=AD6NJOJSw76hjAEiWewQ6Vu5g-cfQn0GTtdchuDBH9o,8042
|
|
234
241
|
ai_review/tests/fixtures/clients/ollama.py,sha256=UUHDDPUraQAG8gBC-0UvftaK0BDYir5cJDlRKJymSQg,2109
|
|
235
|
-
ai_review/tests/fixtures/clients/openai.py,sha256=
|
|
242
|
+
ai_review/tests/fixtures/clients/openai.py,sha256=PfnaYdrKwGiuAx8fnSJBZamAZEYJR1y8I4oHBj2SmU4,2291
|
|
243
|
+
ai_review/tests/fixtures/clients/openrouter.py,sha256=TWCojwXP0y0_dlzFMzJra4uXSQ3Dv5wZQnm_Hbvxodg,2532
|
|
236
244
|
ai_review/tests/fixtures/libs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
237
245
|
ai_review/tests/fixtures/libs/llm/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
238
246
|
ai_review/tests/fixtures/libs/llm/output_json_parser.py,sha256=Eu0tCK5cD1d9X1829ruahih7jQoehGozIEugS71AAA8,275
|
|
239
247
|
ai_review/tests/fixtures/services/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
240
|
-
ai_review/tests/fixtures/services/artifacts.py,sha256=
|
|
248
|
+
ai_review/tests/fixtures/services/artifacts.py,sha256=ETxcGT27YUSegSjxKuuXx1B8ZCXmAm0hhUzW5Vqt7Os,1621
|
|
241
249
|
ai_review/tests/fixtures/services/cost.py,sha256=A6Ja0CtQ-k6pR2-B5LRE8EzkqPL34xHGXYtaILjhYvw,1612
|
|
242
250
|
ai_review/tests/fixtures/services/diff.py,sha256=rOLFR-giYJlE2qUYTOT9BxyJhQ-fbXDdYCw3zed4-9M,1471
|
|
243
251
|
ai_review/tests/fixtures/services/git.py,sha256=zDNNLZDoVC7r4LuF1N1MUgzhcAl2nhDdFC9olpR_PjQ,1441
|
|
@@ -291,6 +299,9 @@ ai_review/tests/suites/clients/ollama/test_schema.py,sha256=A93wCmxwGdvudfbA97VC
|
|
|
291
299
|
ai_review/tests/suites/clients/openai/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
292
300
|
ai_review/tests/suites/clients/openai/test_client.py,sha256=6Wsxw6-6Uk0uPYFkzpWSwsxfCYUZhT3UYznayo-xlPI,404
|
|
293
301
|
ai_review/tests/suites/clients/openai/test_schema.py,sha256=x1tamS4GC9pOTpjieKDbK2D73CVV4BkATppytwMevLo,1599
|
|
302
|
+
ai_review/tests/suites/clients/openrouter/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
303
|
+
ai_review/tests/suites/clients/openrouter/test_client.py,sha256=WNH0p1Bl5J6zLKB1gSQ9smQMRTOo5-U-A60iJ0n4_DI,444
|
|
304
|
+
ai_review/tests/suites/clients/openrouter/test_schema.py,sha256=9wt8-lR1u2KGvd6Iget_Yy-r33BllYLA-3AKe-S2E-c,1731
|
|
294
305
|
ai_review/tests/suites/libs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
295
306
|
ai_review/tests/suites/libs/test_json.py,sha256=q_tvFcL9EaN5jr48Ed_mxMEmW6xXS0xlZGqSooPZBa4,713
|
|
296
307
|
ai_review/tests/suites/libs/asynchronous/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -308,6 +319,9 @@ ai_review/tests/suites/libs/llm/test_output_json_parser.py,sha256=2KwnXZc3dlUWM1
|
|
|
308
319
|
ai_review/tests/suites/libs/template/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
309
320
|
ai_review/tests/suites/libs/template/test_render.py,sha256=n-ss5bd_hwc-RzYmqWmFM6KSlP1zLSnlsW1Yki12Bpw,1890
|
|
310
321
|
ai_review/tests/suites/services/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
322
|
+
ai_review/tests/suites/services/artifacts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
323
|
+
ai_review/tests/suites/services/artifacts/test_service.py,sha256=OFYHORgvKVLXrAWImTUs9nXAuAUGUVQT-QBZPU3WjVc,3054
|
|
324
|
+
ai_review/tests/suites/services/artifacts/test_tools.py,sha256=kvxGoCcL6JovW10GoVycKE5M3e-fkbVd6OpHLbPvhIA,1022
|
|
311
325
|
ai_review/tests/suites/services/cost/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
312
326
|
ai_review/tests/suites/services/cost/test_schema.py,sha256=AI3Wg1sR6nzLpkEqJGDu6nDYwiwzbbghsxhRNwRsUFA,3044
|
|
313
327
|
ai_review/tests/suites/services/cost/test_service.py,sha256=9_Mi5hu2cq3w2tIEPfhrn9x8SblCT5m1W-QUOc9BZds,3258
|
|
@@ -318,7 +332,7 @@ ai_review/tests/suites/services/diff/test_tools.py,sha256=vsOSSIDZKkuD8dMCoBBEBt
|
|
|
318
332
|
ai_review/tests/suites/services/hook/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
319
333
|
ai_review/tests/suites/services/hook/test_service.py,sha256=TjNU2xiOQfUZZa8M3L2eHbtTwxse_B7QNn2h4118z1U,6637
|
|
320
334
|
ai_review/tests/suites/services/llm/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
321
|
-
ai_review/tests/suites/services/llm/test_factory.py,sha256=
|
|
335
|
+
ai_review/tests/suites/services/llm/test_factory.py,sha256=e3pND1Rq4lIXpm_R8J1mRiEc7moEqJ9cGU3rK5tk4Bo,1685
|
|
322
336
|
ai_review/tests/suites/services/llm/claude/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
323
337
|
ai_review/tests/suites/services/llm/claude/test_client.py,sha256=ymIeuIax0Bp_CuXBSApK1RDl1JmbGc97uzXZToQOZO8,761
|
|
324
338
|
ai_review/tests/suites/services/llm/gemini/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -327,17 +341,19 @@ ai_review/tests/suites/services/llm/ollama/__init__.py,sha256=47DEQpj8HBSa-_TImW
|
|
|
327
341
|
ai_review/tests/suites/services/llm/ollama/test_client.py,sha256=Eu4OERB00SJwCKznyOCyqSFTDBp9J2Lw-BcW7sPJQM4,760
|
|
328
342
|
ai_review/tests/suites/services/llm/openai/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
329
343
|
ai_review/tests/suites/services/llm/openai/test_client.py,sha256=yzIL8GYHyX9iLKIlaF__87aue9w0cr66feoMaCv5gms,761
|
|
344
|
+
ai_review/tests/suites/services/llm/openrouter/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
345
|
+
ai_review/tests/suites/services/llm/openrouter/test_client.py,sha256=9YRwhkgeRDdRi_EMFh_T0u4wgEFj2AMgAiusrYWzeEc,813
|
|
330
346
|
ai_review/tests/suites/services/prompt/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
331
347
|
ai_review/tests/suites/services/prompt/test_adapter.py,sha256=3BhaGW4G-72dN_ajp5rJst39k1VNiKZq5FUB9LGPNVQ,2168
|
|
332
348
|
ai_review/tests/suites/services/prompt/test_schema.py,sha256=rm2__LA2_4qQwSmNAZ_Wnpy11T3yYRkYUkRUrqxUQKE,5421
|
|
333
349
|
ai_review/tests/suites/services/prompt/test_service.py,sha256=jfpmM--eZH43NylwltX-ijmF58ZJ4WA83kmGshUbJfs,8312
|
|
334
350
|
ai_review/tests/suites/services/prompt/test_tools.py,sha256=SmweFvrpxd-3RO5v18vCl7zonEqFE1n4eqHH7-6auYM,4511
|
|
335
351
|
ai_review/tests/suites/services/review/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
336
|
-
ai_review/tests/suites/services/review/test_service.py,sha256=
|
|
352
|
+
ai_review/tests/suites/services/review/test_service.py,sha256=8LYlLk4sX6R86qS9kGRPO7EJcUlXr0L1JLczADpS-nE,4252
|
|
337
353
|
ai_review/tests/suites/services/review/gateway/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
338
|
-
ai_review/tests/suites/services/review/gateway/test_review_comment_gateway.py,sha256
|
|
339
|
-
ai_review/tests/suites/services/review/gateway/test_review_dry_run_comment_gateway.py,sha256=
|
|
340
|
-
ai_review/tests/suites/services/review/gateway/test_review_llm_gateway.py,sha256=
|
|
354
|
+
ai_review/tests/suites/services/review/gateway/test_review_comment_gateway.py,sha256=-EG6bWwFX46QKROYmIxPw0WLQsYkn0munTD2Wxykm6Q,8946
|
|
355
|
+
ai_review/tests/suites/services/review/gateway/test_review_dry_run_comment_gateway.py,sha256=UzRo41pI0vBcHcr7u2hi8zH2Msd25MXhxLWXodsaNLA,3977
|
|
356
|
+
ai_review/tests/suites/services/review/gateway/test_review_llm_gateway.py,sha256=Wy9nYO_YNlBNKC2w4TvXvPqtiRUSvRfyZ7cGS-LD7Y8,2554
|
|
341
357
|
ai_review/tests/suites/services/review/internal/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
342
358
|
ai_review/tests/suites/services/review/internal/inline/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
343
359
|
ai_review/tests/suites/services/review/internal/inline/test_schema.py,sha256=2SGF8yNHsNv8lwG-MUlgO3RT4juTt0PQSl10qNhSt7o,2377
|
|
@@ -373,9 +389,9 @@ ai_review/tests/suites/services/vcs/github/test_client.py,sha256=mNt1bA6aVU3REsJ
|
|
|
373
389
|
ai_review/tests/suites/services/vcs/gitlab/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
374
390
|
ai_review/tests/suites/services/vcs/gitlab/test_adapter.py,sha256=BYBP2g1AKF_jCSJYJj16pW7M_6PprwD9reYEpdw3StU,4340
|
|
375
391
|
ai_review/tests/suites/services/vcs/gitlab/test_client.py,sha256=dnI-YxYADmVF2GS9rp6-JPkcqsn4sN8Fjbe4MkeYMaE,8476
|
|
376
|
-
xai_review-0.
|
|
377
|
-
xai_review-0.
|
|
378
|
-
xai_review-0.
|
|
379
|
-
xai_review-0.
|
|
380
|
-
xai_review-0.
|
|
381
|
-
xai_review-0.
|
|
392
|
+
xai_review-0.34.0.dist-info/licenses/LICENSE,sha256=p-v8m7Kmz4KKc7PcvsGiGEmCw9AiSXY4_ylOPy_u--Y,11343
|
|
393
|
+
xai_review-0.34.0.dist-info/METADATA,sha256=Zpuk0sHxSQKsA84Xdv4Ba1V6Seghvxd_XCh48f5BKDw,12911
|
|
394
|
+
xai_review-0.34.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
395
|
+
xai_review-0.34.0.dist-info/entry_points.txt,sha256=JyC5URanMi5io5P_PXQf7H_I1OGIpk5cZQhaPQ0g4Zs,53
|
|
396
|
+
xai_review-0.34.0.dist-info/top_level.txt,sha256=sTsZbfzLoqvRZKdKa-BcxWvjlHdrpbeJ6DrGY0EuR0E,10
|
|
397
|
+
xai_review-0.34.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|