Skip to content

Commit d1db0b6

Browse files
committed
support using Azure OpenAI
1 parent b4ea7ce commit d1db0b6

4 files changed

Lines changed: 77 additions & 11 deletions

File tree

scripts/release-notes-generator-readme.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ The generator does not create a complete formal release note. It does not genera
4848
export GITHUB_TOKEN=<your-github-token>
4949
```
5050

51-
- Install and log in to Codex CLI. The default `--ai-command` uses `codex exec`, so the installed Codex CLI must support `exec`, `--sandbox read-only`, `--ephemeral`, `--output-schema`, `--output-last-message`, and `-m <model>`.
51+
- Install and log in to Codex CLI. The default `--ai-command` uses `codex exec`, so the installed Codex CLI must support `exec`, `--sandbox read-only`, `--ephemeral`, `--output-schema`, `--output-last-message`, and `-m <model>`. If you use `--ai-provider azure` instead, Codex CLI is not required; set `AZURE_OPENAI_KEY` and `AZURE_OPENAI_BASE_URL` (or `OPENAI_BASE_URL`) environment variables.
5252

5353
## Typical usage
5454

@@ -67,8 +67,9 @@ python3 scripts/release_notes_generate_ai.py \
6767
| `--excel <workbook-path>` | Yes | None | `--excel /path/to/release-note-excel.xlsx` | Path to the source release note Excel file. The source workbook is not overwritten. The processed workbook is written to `<original-name>_processed.xlsx`. |
6868
| `--releases-dir <releases-dir>` | Yes | None | `--releases-dir releases` | Path to the existing English release notes directory. The script scans this directory for historical release notes and writes the generated Markdown under this directory unless `--output-release-file` is specified. |
6969
| `--sheet <sheet-name>` | No | `pr_for_release_note` | `--sheet pr_for_release_note` | Workbook sheet to process. |
70-
| `--ai-command <command>` | No | `codex --ask-for-approval never exec --sandbox read-only --ephemeral` | `--ai-command "codex --ask-for-approval never exec --sandbox read-only --ephemeral"` | Command used to invoke the AI generator. The prompt is passed through standard input. When the command is `codex exec`, the script also passes `--output-schema` and `--output-last-message`. |
71-
| `--ai-model <model>` | No | `gpt-5.4` | `--ai-model gpt-5.4` | Model name passed to `codex exec` with `-m`. |
70+
| `--ai-provider <provider>` | No | `codex` | `--ai-provider azure` | AI provider to use. `codex` runs the Codex CLI as a subprocess. `azure` calls Azure OpenAI via the OpenAI Python SDK (requires `AZURE_OPENAI_KEY` and `AZURE_OPENAI_BASE_URL` or `OPENAI_BASE_URL` environment variables). |
71+
| `--ai-command <command>` | No | `codex --ask-for-approval never exec --sandbox read-only --ephemeral` | `--ai-command "codex --ask-for-approval never exec --sandbox read-only --ephemeral"` | Command used to invoke the AI generator (only used with `--ai-provider codex`). The prompt is passed through standard input. When the command is `codex exec`, the script also passes `--output-schema` and `--output-last-message`. |
72+
| `--ai-model <model>` | No | `gpt-5.4` | `--ai-model gpt-5.4` | Model name. Passed to `codex exec` with `-m`, or used as the model parameter for Azure OpenAI. |
7273
| `--involve-ai-generation <ON-or-OFF>` | No | `ON` | `--involve-ai-generation OFF` | Whether to generate non-duplicate release notes with AI. Use `ON` to invoke AI, or `OFF` to use the source `formated_release_note` values. |
7374
| `--output-release-file <markdown-file>` | No | Conditional | `--output-release-file /path/to/release-8.5.7.md` | Write the generated Markdown to a custom path. By default, the output under `--releases-dir` is `release-<version>-updated-by-ai.md` if `release-<version>.md` already exists, otherwise `release-<version>.md`. |
7475
| `--ai-timeout <seconds>` | No | `600` | `--ai-timeout 600` | Timeout in seconds for each AI command invocation. |

scripts/release_notes_ai/ai_client.py

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,7 @@
2121

2222

2323
class AIClient:
24-
def __init__(self, command: str, model: str | None, timeout: int):
25-
self.command = shlex.split(command)
26-
self.model = model
27-
self.timeout = timeout
24+
"""Base AI client with shared generation and validation logic."""
2825

2926
def generate(self, prompt: str, expected_links: list[str], contributors: list[str]) -> GeneratedNote:
3027
result, errors = self._run_and_validate(prompt, expected_links, contributors)
@@ -47,6 +44,18 @@ def _run_and_validate(
4744
return None, [str(exc)]
4845
return validate_ai_response(data, expected_links, contributors)
4946

47+
def _run(self, prompt: str) -> str:
48+
raise NotImplementedError("Subclasses must implement _run")
49+
50+
51+
class CodexAIClient(AIClient):
52+
"""AI client that invokes the Codex CLI as a subprocess."""
53+
54+
def __init__(self, command: str, model: str | None, timeout: int):
55+
self.command = shlex.split(command)
56+
self.model = model
57+
self.timeout = timeout
58+
5059
def _run(self, prompt: str) -> str:
5160
command = list(self.command)
5261
if not command:
@@ -97,6 +106,44 @@ def _is_codex_exec(command: list[str]) -> bool:
97106
return executable == "codex" and "exec" in command[1:]
98107

99108

109+
class AzureOpenAIClient(AIClient):
110+
"""AI client that calls Azure OpenAI via the OpenAI Python SDK."""
111+
112+
DEFAULT_MODEL = "gpt-5.4"
113+
MAX_OUTPUT_TOKENS = 16384
114+
TEMPERATURE = 0.1
115+
116+
def __init__(self, model: str | None, timeout: int):
117+
from openai import OpenAI
118+
119+
key = os.environ.get("AZURE_OPENAI_KEY", "")
120+
base_url = (
121+
os.environ.get("AZURE_OPENAI_BASE_URL")
122+
or os.environ.get("OPENAI_BASE_URL", "")
123+
)
124+
if not key:
125+
raise ValueError(
126+
"AZURE_OPENAI_KEY environment variable is required "
127+
"when using --ai-provider azure"
128+
)
129+
if not base_url:
130+
raise ValueError(
131+
"AZURE_OPENAI_BASE_URL or OPENAI_BASE_URL environment variable "
132+
"is required when using --ai-provider azure"
133+
)
134+
self.client = OpenAI(api_key=key, base_url=base_url, timeout=timeout)
135+
self.model = model or self.DEFAULT_MODEL
136+
137+
def _run(self, prompt: str) -> str:
138+
response = self.client.responses.create(
139+
model=self.model,
140+
input=[{"role": "user", "content": prompt}],
141+
temperature=self.TEMPERATURE,
142+
max_output_tokens=self.MAX_OUTPUT_TOKENS,
143+
)
144+
return response.output_text.strip()
145+
146+
100147
def is_executable_available(executable: str) -> bool:
101148
if os.sep in executable or (os.altsep and os.altsep in executable):
102149
return Path(executable).exists()

scripts/release_notes_ai/cli.py

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
import openpyxl
99

10-
from .ai_client import AIClient
10+
from .ai_client import AzureOpenAIClient, CodexAIClient
1111
from .excel_workbook import (
1212
clear_output_columns,
1313
generate_notes_without_ai,
@@ -36,15 +36,26 @@ def parse_args() -> argparse.Namespace:
3636
help="Path to the existing English release notes directory.",
3737
)
3838
parser.add_argument("--sheet", default="pr_for_release_note", help="Workbook sheet name.")
39+
parser.add_argument(
40+
"--ai-provider",
41+
choices=["codex", "azure"],
42+
default="codex",
43+
help=(
44+
"AI provider to use. 'codex' runs the Codex CLI as a subprocess "
45+
"(requires codex to be installed). 'azure' calls Azure OpenAI via the "
46+
"OpenAI Python SDK (requires AZURE_OPENAI_KEY and AZURE_OPENAI_BASE_URL "
47+
"or OPENAI_BASE_URL environment variables). Default: codex."
48+
),
49+
)
3950
parser.add_argument(
4051
"--ai-command",
4152
default="codex --ask-for-approval never exec --sandbox read-only --ephemeral",
42-
help="Command-line AI command. The prompt is passed through stdin.",
53+
help="Command-line AI command (only used with --ai-provider codex). The prompt is passed through stdin.",
4354
)
4455
parser.add_argument(
4556
"--ai-model",
4657
default="gpt-5.4",
47-
help="Model name passed to codex exec with -m.",
58+
help="Model name. Passed to codex exec with -m, or used as the model parameter for Azure OpenAI.",
4859
)
4960
parser.add_argument(
5061
"--involve-ai-generation",
@@ -142,7 +153,13 @@ def main() -> int:
142153
raise SystemExit(f"error: {exc}") from None
143154
github = GitHubClient(token)
144155
involve_ai_generation = args.involve_ai_generation == "ON"
145-
ai = AIClient(args.ai_command, args.ai_model, args.ai_timeout) if involve_ai_generation else None
156+
if involve_ai_generation:
157+
if args.ai_provider == "azure":
158+
ai = AzureOpenAIClient(args.ai_model, args.ai_timeout)
159+
else:
160+
ai = CodexAIClient(args.ai_command, args.ai_model, args.ai_timeout)
161+
else:
162+
ai = None
146163

147164
output_file = (
148165
Path(args.output_release_file)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
openpyxl>=3.1
2+
openai>=1.66
23
requests>=2.31
34
urllib3>=1.26

0 commit comments

Comments
 (0)