DPDK-dev Archive on lore.kernel.org
 help / color / mirror / Atom feed
From: Stephen Hemminger <stephen@networkplumber.org>
To: dev@dpdk.org
Cc: Stephen Hemminger <stephen@networkplumber.org>,
	Aaron Conole <aconole@redhat.com>
Subject: [PATCH v15 2/7] devtools: add common infrastructure for AI review scripts
Date: Thu, 21 May 2026 15:44:34 -0700	[thread overview]
Message-ID: <20260521224550.927338-3-stephen@networkplumber.org> (raw)
In-Reply-To: <20260521224550.927338-1-stephen@networkplumber.org>

The AI review scripts share similar options and infrastructure.

Signed-off-by: Stephen Hemminger <stephen@networkplumber.org>
---
 devtools/ai/_common.py | 246 +++++++++++++++++++++++++++++++++++++++++
 1 file changed, 246 insertions(+)
 create mode 100644 devtools/ai/_common.py

diff --git a/devtools/ai/_common.py b/devtools/ai/_common.py
new file mode 100644
index 0000000000..69982cbda5
--- /dev/null
+++ b/devtools/ai/_common.py
@@ -0,0 +1,246 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: BSD-3-Clause
+# Copyright(c) 2026 Stephen Hemminger
+
+"""Common utilities shared by the DPDK AI review scripts."""
+
+import argparse
+import json
+import subprocess
+import sys
+from dataclasses import dataclass
+from typing import Any, NoReturn
+from urllib.error import HTTPError, URLError
+from urllib.request import Request, urlopen
+
+# Provider configurations (model defaults; override with --model).
+PROVIDERS: dict[str, dict[str, str]] = {
+    "anthropic": {
+        "name": "Claude",
+        "endpoint": "https://api.anthropic.com/v1/messages",
+        "default_model": "claude-sonnet-4-5-20250929",
+        "env_var": "ANTHROPIC_API_KEY",
+    },
+    "openai": {
+        "name": "ChatGPT",
+        "endpoint": "https://api.openai.com/v1/chat/completions",
+        "default_model": "gpt-4.1",
+        "env_var": "OPENAI_API_KEY",
+    },
+    "xai": {
+        "name": "Grok",
+        "endpoint": "https://api.x.ai/v1/chat/completions",
+        "default_model": "grok-4-1-fast-non-reasoning",
+        "env_var": "XAI_API_KEY",
+    },
+    "google": {
+        "name": "Gemini",
+        "endpoint": "https://generativelanguage.googleapis.com/v1beta/models",
+        "default_model": "gemini-3-flash-preview",
+        "env_var": "GOOGLE_API_KEY",
+    },
+}
+
+
+def error(msg: str) -> NoReturn:
+    """Print error message to stderr and exit with status 1."""
+    print(f"Error: {msg}", file=sys.stderr)
+    sys.exit(1)
+
+
+def get_git_config(key: str) -> str | None:
+    """Return the value of a git config key, or None if unset/git missing."""
+    try:
+        result = subprocess.run(
+            ["git", "config", "--get", key],
+            capture_output=True,
+            text=True,
+            check=True,
+        )
+        return result.stdout.strip()
+    except (subprocess.CalledProcessError, FileNotFoundError):
+        return None
+
+
+def list_providers() -> NoReturn:
+    """Print available providers and exit."""
+    print("Available AI Providers:\n")
+    print(f"{'Provider':<12} {'Default Model':<30} {'API Key Variable'}")
+    print(f"{'--------':<12} {'-------------':<30} {'----------------'}")
+    for name, config in PROVIDERS.items():
+        print(f"{name:<12} {config['default_model']:<30} {config['env_var']}")
+    sys.exit(0)
+
+
+@dataclass
+class TokenUsage:
+    """Accumulated token usage across API calls."""
+
+    input_tokens: int = 0
+    output_tokens: int = 0
+    cache_creation_tokens: int = 0
+    cache_read_tokens: int = 0
+    api_calls: int = 0
+
+    def add(self, other: "TokenUsage") -> None:
+        """Accumulate usage from another TokenUsage."""
+        self.input_tokens += other.input_tokens
+        self.output_tokens += other.output_tokens
+        self.cache_creation_tokens += other.cache_creation_tokens
+        self.cache_read_tokens += other.cache_read_tokens
+        self.api_calls += other.api_calls
+
+
+def format_token_summary(usage: TokenUsage, provider: str, model: str) -> str:
+    """Format a token usage summary string."""
+    provider_label = PROVIDERS.get(provider, {}).get("name", provider)
+    lines = ["=== Token Usage Summary ==="]
+    lines.append(f"Provider:      {provider_label} ({model})")
+    lines.append(f"API calls:     {usage.api_calls}")
+    lines.append(f"Input tokens:  {usage.input_tokens:,}")
+    lines.append(f"Output tokens: {usage.output_tokens:,}")
+    if usage.cache_creation_tokens:
+        lines.append(f"Cache write:   {usage.cache_creation_tokens:,}")
+    if usage.cache_read_tokens:
+        lines.append(f"Cache read:    {usage.cache_read_tokens:,}")
+    total = usage.input_tokens + usage.output_tokens
+    lines.append(f"Total tokens:  {total:,}")
+    lines.append("=" * 27)
+    return "\n".join(lines)
+
+
+def add_token_args(parser: argparse.ArgumentParser) -> None:
+    """Add the --show-tokens flag to an ArgumentParser."""
+    parser.add_argument(
+        "--show-tokens",
+        action="store_true",
+        help="Show token usage summary on stderr after the run",
+    )
+
+
+def print_token_summary(
+    usage: TokenUsage, provider: str, model: str, show: bool
+) -> None:
+    """Print token usage summary to stderr if requested and any calls were made."""
+    if not show or usage.api_calls == 0:
+        return
+    print("", file=sys.stderr)
+    print(format_token_summary(usage, provider, model), file=sys.stderr)
+
+
+def _build_request_meta(
+    provider: str, api_key: str, model: str
+) -> tuple[str, dict[str, str]]:
+    """Return (url, headers) for a provider request."""
+    config = PROVIDERS[provider]
+    if provider == "anthropic":
+        return config["endpoint"], {
+            "Content-Type": "application/json",
+            "x-api-key": api_key,
+            "anthropic-version": "2023-06-01",
+        }
+    if provider == "google":
+        url = f"{config['endpoint']}/{model}:generateContent?key={api_key}"
+        return url, {"Content-Type": "application/json"}
+    # openai, xai
+    return config["endpoint"], {
+        "Content-Type": "application/json",
+        "Authorization": f"Bearer {api_key}",
+    }
+
+
+def _extract_usage(provider: str, result: dict[str, Any]) -> TokenUsage:
+    """Extract TokenUsage from a provider response."""
+    usage = TokenUsage(api_calls=1)
+    if provider == "anthropic":
+        raw = result.get("usage", {})
+        usage.input_tokens = raw.get("input_tokens", 0)
+        usage.output_tokens = raw.get("output_tokens", 0)
+        usage.cache_creation_tokens = raw.get("cache_creation_input_tokens", 0)
+        usage.cache_read_tokens = raw.get("cache_read_input_tokens", 0)
+    elif provider == "google":
+        raw = result.get("usageMetadata", {})
+        usage.input_tokens = raw.get("promptTokenCount", 0)
+        usage.output_tokens = raw.get("candidatesTokenCount", 0)
+    else:  # openai, xai
+        raw = result.get("usage", {})
+        usage.input_tokens = raw.get("prompt_tokens", 0)
+        usage.output_tokens = raw.get("completion_tokens", 0)
+        cache_details = raw.get("prompt_tokens_details", {})
+        if cache_details:
+            usage.cache_read_tokens = cache_details.get("cached_tokens", 0)
+    return usage
+
+
+def _extract_text(provider: str, result: dict[str, Any]) -> str:
+    """Extract response text from a provider response. Calls error() on failure."""
+    if "error" in result:
+        error(f"API error: {result['error'].get('message', result)}")
+    if provider == "anthropic":
+        content = result.get("content", [])
+        return "".join(
+            block.get("text", "") for block in content if block.get("type") == "text"
+        )
+    if provider == "google":
+        candidates = result.get("candidates", [])
+        if not candidates:
+            error("No response from Gemini")
+        parts = candidates[0].get("content", {}).get("parts", [])
+        return "".join(part.get("text", "") for part in parts)
+    # openai, xai
+    choices = result.get("choices", [])
+    if not choices:
+        error("No response from API")
+    return choices[0].get("message", {}).get("content", "")
+
+
+def _print_verbose_usage(usage: TokenUsage) -> None:
+    """Print per-call token details to stderr."""
+    print("=== Token Usage ===", file=sys.stderr)
+    print(f"Input tokens: {usage.input_tokens:,}", file=sys.stderr)
+    print(f"Output tokens: {usage.output_tokens:,}", file=sys.stderr)
+    if usage.cache_creation_tokens:
+        print(f"Cache creation: {usage.cache_creation_tokens:,}", file=sys.stderr)
+    if usage.cache_read_tokens:
+        print(f"Cache read: {usage.cache_read_tokens:,}", file=sys.stderr)
+    print("===================", file=sys.stderr)
+
+
+def send_request(
+    provider: str,
+    api_key: str,
+    model: str,
+    request_data: dict[str, Any],
+    *,
+    timeout: int = 120,
+    verbose: bool = False,
+) -> tuple[str, TokenUsage]:
+    """Send a prebuilt request to a provider and return (response_text, usage).
+
+    The caller assembles the provider-specific request body via its own
+    build_*_request helpers (the prompts differ per script). This function
+    handles transport, error reporting, and token-usage extraction.
+    """
+    url, headers = _build_request_meta(provider, api_key, model)
+    body = json.dumps(request_data).encode("utf-8")
+    req = Request(url, data=body, headers=headers)
+
+    try:
+        with urlopen(req, timeout=timeout) as response:
+            result = json.loads(response.read().decode("utf-8"))
+    except HTTPError as e:
+        error_body = e.read().decode("utf-8")
+        try:
+            error_data = json.loads(error_body)
+            error(f"API error: {error_data.get('error', error_body)}")
+        except json.JSONDecodeError:
+            error(f"API error ({e.code}): {error_body}")
+    except URLError as e:
+        if isinstance(e.reason, TimeoutError):
+            error(f"Request timed out after {timeout} seconds")
+        error(f"Connection error: {e.reason}")
+
+    usage = _extract_usage(provider, result)
+    if verbose:
+        _print_verbose_usage(usage)
+    return _extract_text(provider, result), usage
-- 
2.53.0


  parent reply	other threads:[~2026-05-21 22:46 UTC|newest]

Thread overview: 76+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
     [not found] <0260109014106.398156-1-stephen@networkplumber.org>
2026-01-26 18:40 ` [PATCH v7 0/4] devtools: add AI-assisted code review tools Stephen Hemminger
2026-01-26 18:40   ` [PATCH v7 1/4] doc: add AGENTS.md for AI-powered " Stephen Hemminger
2026-01-30 23:49     ` Stephen Hemminger
2026-01-26 18:40   ` [PATCH v7 2/4] devtools: add multi-provider AI patch review script Stephen Hemminger
2026-01-26 18:40   ` [PATCH v7 3/4] devtools: add compare-reviews.sh for multi-provider analysis Stephen Hemminger
2026-01-26 18:40   ` [PATCH v7 4/4] devtools: add multi-provider AI documentation review script Stephen Hemminger
2026-02-09 19:48   ` [PATCH v8 0/6] add AGENTS.md and scripts for AI code review Stephen Hemminger
2026-02-09 19:48     ` [PATCH v8 1/6] doc: add AGENTS.md for AI code review tools Stephen Hemminger
2026-02-09 19:48     ` [PATCH v8 2/6] devtools: add multi-provider AI patch review script Stephen Hemminger
2026-02-09 19:48     ` [PATCH v8 3/6] devtools: add compare-reviews.sh for multi-provider analysis Stephen Hemminger
2026-02-09 19:48     ` [PATCH v8 4/6] devtools: add multi-provider AI documentation review script Stephen Hemminger
2026-02-09 19:48     ` [PATCH v8 5/6] doc: add AI-assisted patch review to contributing guide Stephen Hemminger
2026-02-09 19:48     ` [PATCH v8 6/6] MAINTAINERS: add section for AI review tools Stephen Hemminger
2026-03-04 17:59   ` [PATCH v9 0/6] add AGENTS.md and scripts for AI code review Stephen Hemminger
2026-03-04 17:59     ` [PATCH v9 1/6] doc: add AGENTS.md for AI code review tools Stephen Hemminger
2026-03-04 17:59     ` [PATCH v9 2/6] devtools: add multi-provider AI patch review script Stephen Hemminger
2026-03-04 17:59     ` [PATCH v9 3/6] devtools: add compare-reviews.sh for multi-provider analysis Stephen Hemminger
2026-03-04 17:59     ` [PATCH v9 4/6] devtools: add multi-provider AI documentation review script Stephen Hemminger
2026-03-04 17:59     ` [PATCH v9 5/6] doc: add AI-assisted patch review to contributing guide Stephen Hemminger
2026-03-04 17:59     ` [PATCH v9 6/6] MAINTAINERS: add section for AI review tools Stephen Hemminger
2026-03-10  1:57   ` [PATCH v10 0/6] Add AGENTS and scripts for AI code review Stephen Hemminger
2026-03-10  1:57     ` [PATCH v10 1/6] doc: add AGENTS.md for AI code review tools Stephen Hemminger
2026-03-10  1:57     ` [PATCH v10 2/6] devtools: add multi-provider AI patch review script Stephen Hemminger
2026-03-10  1:57     ` [PATCH v10 3/6] devtools: add compare-reviews.sh for multi-provider analysis Stephen Hemminger
2026-03-10  1:57     ` [PATCH v10 4/6] devtools: add multi-provider AI documentation review script Stephen Hemminger
2026-03-10  1:57     ` [PATCH v10 5/6] doc: add AI-assisted patch review to contributing guide Stephen Hemminger
2026-03-10  1:57     ` [PATCH v10 6/6] MAINTAINERS: add section for AI review tools Stephen Hemminger
2026-03-27 15:41   ` [PATCH v11 0/6] Add AGENTS.md and scripts for AI code review Stephen Hemminger
2026-03-27 15:41     ` [PATCH v11 1/6] doc: add AGENTS.md for AI code review tools Stephen Hemminger
2026-03-27 15:41     ` [PATCH v11 2/6] devtools: add multi-provider AI patch review script Stephen Hemminger
2026-03-27 15:41     ` [PATCH v11 3/6] devtools: add compare-reviews.sh for multi-provider analysis Stephen Hemminger
2026-03-27 15:41     ` [PATCH v11 4/6] devtools: add multi-provider AI documentation review script Stephen Hemminger
2026-03-27 15:41     ` [PATCH v11 5/6] doc: add AI-assisted patch review to contributing guide Stephen Hemminger
2026-03-27 15:41     ` [PATCH v11 6/6] MAINTAINERS: add section for AI review tools Stephen Hemminger
2026-04-01 15:38   ` [PATCH v12 0/6] Add AGENTS.md and scripts for AI code review Stephen Hemminger
2026-04-01 15:38     ` [PATCH v12 1/6] doc: add AGENTS.md for AI code review tools Stephen Hemminger
2026-04-01 15:38     ` [PATCH v12 2/6] devtools: add multi-provider AI patch review script Stephen Hemminger
2026-04-02  4:00       ` sunyuechi
2026-04-01 15:38     ` [PATCH v12 3/6] devtools: add compare-reviews.sh for multi-provider analysis Stephen Hemminger
2026-04-01 15:38     ` [PATCH v12 4/6] devtools: add multi-provider AI documentation review script Stephen Hemminger
2026-04-02  4:05       ` sunyuechi
2026-04-01 15:38     ` [PATCH v12 5/6] doc: add AI-assisted patch review to contributing guide Stephen Hemminger
2026-04-01 15:38     ` [PATCH v12 6/6] MAINTAINERS: add section for AI review tools Stephen Hemminger
2026-04-02 19:44   ` [PATCH v13 0/6] Add AGENTS.md and scripts for AI code review Stephen Hemminger
2026-04-02 19:44     ` [PATCH v13 1/6] doc: add AGENTS.md for AI code review tools Stephen Hemminger
2026-04-14 19:54       ` Aaron Conole
2026-04-14 20:45         ` Stephen Hemminger
2026-04-02 19:44     ` [PATCH v13 2/6] devtools: add multi-provider AI patch review script Stephen Hemminger
2026-04-14 19:57       ` Aaron Conole
2026-04-02 19:44     ` [PATCH v13 3/6] devtools: add compare-reviews.sh for multi-provider analysis Stephen Hemminger
2026-04-14 19:58       ` Aaron Conole
2026-04-02 19:44     ` [PATCH v13 4/6] devtools: add multi-provider AI documentation review script Stephen Hemminger
2026-04-14 20:05       ` Aaron Conole
2026-04-02 19:44     ` [PATCH v13 5/6] doc: add AI-assisted patch review to contributing guide Stephen Hemminger
2026-04-14 20:06       ` Aaron Conole
2026-04-02 19:44     ` [PATCH v13 6/6] MAINTAINERS: add section for AI review tools Stephen Hemminger
2026-04-14 20:07       ` Aaron Conole
2026-04-14 21:08   ` [PATCH v14 0/6] Add AGENTS.md and scripts for AI code review Stephen Hemminger
2026-04-14 21:08     ` [PATCH v14 1/6] doc: add AGENTS.md for AI code review tools Stephen Hemminger
2026-05-21 14:15       ` Thomas Monjalon
2026-04-14 21:08     ` [PATCH v14 2/6] devtools: add multi-provider AI patch review script Stephen Hemminger
2026-04-14 21:08     ` [PATCH v14 3/6] devtools: add compare-reviews.sh for multi-provider analysis Stephen Hemminger
2026-05-21 14:17       ` Thomas Monjalon
2026-04-14 21:08     ` [PATCH v14 4/6] devtools: add multi-provider AI documentation review script Stephen Hemminger
2026-04-14 21:08     ` [PATCH v14 5/6] doc: add AI-assisted patch review to contributing guide Stephen Hemminger
2026-04-14 21:08     ` [PATCH v14 6/6] MAINTAINERS: add section for AI review tools Stephen Hemminger
2026-05-21 14:12     ` [PATCH v14 0/6] Add AGENTS.md and scripts for AI code review Thomas Monjalon
2026-05-21 22:44   ` [PATCH v15 0/6] Add AI code review tools and AGENTS Stephen Hemminger
2026-05-21 22:44     ` [PATCH v15 1/7] doc: add AGENTS.md for AI code review tools Stephen Hemminger
2026-05-21 22:44     ` Stephen Hemminger [this message]
2026-05-21 22:44     ` [PATCH v15 3/7] devtools: add multi-provider AI patch review script Stephen Hemminger
2026-05-21 22:44     ` [PATCH v15 4/7] devtools: add compare-patch-reviews.sh for multi-provider analysis Stephen Hemminger
2026-05-21 22:44     ` [PATCH v15 5/7] devtools: add multi-provider AI documentation review script Stephen Hemminger
2026-05-21 22:44     ` [PATCH v15 6/7] doc: add AI-assisted patch review to contributing guide Stephen Hemminger
2026-05-21 22:44     ` [PATCH v15 7/7] MAINTAINERS: add section for AI review tools Stephen Hemminger
2026-05-28 18:09     ` [PATCH v15 0/6] Add AI code review tools and AGENTS Thomas Monjalon

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260521224550.927338-3-stephen@networkplumber.org \
    --to=stephen@networkplumber.org \
    --cc=aconole@redhat.com \
    --cc=dev@dpdk.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox