Files
llmx/llmx-rs/scripts/create_github_release
Sebastian Krüger 3c7efc58c8 feat: Complete LLMX v0.1.0 - Rebrand from Codex with LiteLLM Integration
This release represents a comprehensive transformation of the codebase from Codex to LLMX,
enhanced with LiteLLM integration to support 100+ LLM providers through a unified API.

## Major Changes

### Phase 1: Repository & Infrastructure Setup
- Established new repository structure and branching strategy
- Created comprehensive project documentation (CLAUDE.md, LITELLM-SETUP.md)
- Set up development environment and tooling configuration

### Phase 2: Rust Workspace Transformation
- Renamed all Rust crates from `codex-*` to `llmx-*` (30+ crates)
- Updated package names, binary names, and workspace members
- Renamed core modules: codex.rs → llmx.rs, codex_delegate.rs → llmx_delegate.rs
- Updated all internal references, imports, and type names
- Renamed directories: codex-rs/ → llmx-rs/, codex-backend-openapi-models/ → llmx-backend-openapi-models/
- Fixed all Rust compilation errors after mass rename

### Phase 3: LiteLLM Integration
- Integrated LiteLLM for multi-provider LLM support (Anthropic, OpenAI, Azure, Google AI, AWS Bedrock, etc.)
- Implemented OpenAI-compatible Chat Completions API support
- Added model family detection and provider-specific handling
- Updated authentication to support LiteLLM API keys
- Renamed environment variables: OPENAI_BASE_URL → LLMX_BASE_URL
- Added LLMX_API_KEY for unified authentication
- Enhanced error handling for Chat Completions API responses
- Implemented fallback mechanisms between Responses API and Chat Completions API

### Phase 4: TypeScript/Node.js Components
- Renamed npm package: @codex/codex-cli → @valknar/llmx
- Updated TypeScript SDK to use new LLMX APIs and endpoints
- Fixed all TypeScript compilation and linting errors
- Updated SDK tests to support both API backends
- Enhanced mock server to handle multiple API formats
- Updated build scripts for cross-platform packaging

### Phase 5: Configuration & Documentation
- Updated all configuration files to use LLMX naming
- Rewrote README and documentation for LLMX branding
- Updated config paths: ~/.codex/ → ~/.llmx/
- Added comprehensive LiteLLM setup guide
- Updated all user-facing strings and help text
- Created release plan and migration documentation

### Phase 6: Testing & Validation
- Fixed all Rust tests for new naming scheme
- Updated snapshot tests in TUI (36 frame files)
- Fixed authentication storage tests
- Updated Chat Completions payload and SSE tests
- Fixed SDK tests for new API endpoints
- Ensured compatibility with Claude Sonnet 4.5 model
- Fixed test environment variables (LLMX_API_KEY, LLMX_BASE_URL)

### Phase 7: Build & Release Pipeline
- Updated GitHub Actions workflows for LLMX binary names
- Fixed rust-release.yml to reference llmx-rs/ instead of codex-rs/
- Updated CI/CD pipelines for new package names
- Made Apple code signing optional in release workflow
- Enhanced npm packaging resilience for partial platform builds
- Added Windows sandbox support to workspace
- Updated dotslash configuration for new binary names

### Phase 8: Final Polish
- Renamed all assets (.github images, labels, templates)
- Updated VSCode and DevContainer configurations
- Fixed all clippy warnings and formatting issues
- Applied cargo fmt and prettier formatting across codebase
- Updated issue templates and pull request templates
- Fixed all remaining UI text references

## Technical Details

**Breaking Changes:**
- Binary name changed from `codex` to `llmx`
- Config directory changed from `~/.codex/` to `~/.llmx/`
- Environment variables renamed (CODEX_* → LLMX_*)
- npm package renamed to `@valknar/llmx`

**New Features:**
- Support for 100+ LLM providers via LiteLLM
- Unified authentication with LLMX_API_KEY
- Enhanced model provider detection and handling
- Improved error handling and fallback mechanisms

**Files Changed:**
- 578 files modified across Rust, TypeScript, and documentation
- 30+ Rust crates renamed and updated
- Complete rebrand of UI, CLI, and documentation
- All tests updated and passing

**Dependencies:**
- Updated Cargo.lock with new package names
- Updated npm dependencies in llmx-cli
- Enhanced OpenAPI models for LLMX backend

This release establishes LLMX as a standalone project with comprehensive LiteLLM
integration, maintaining full backward compatibility with existing functionality
while opening support for a wide ecosystem of LLM providers.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Sebastian Krüger <support@pivoine.art>
2025-11-12 20:40:44 +01:00

376 lines
12 KiB
Python
Executable File

#!/usr/bin/env python3
import argparse
import base64
import json
import re
import subprocess
import sys
REPO = "openai/codex"
BRANCH_REF = "heads/main"
CARGO_TOML_PATH = "codex-rs/Cargo.toml"
def parse_args(argv: list[str]) -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Publish a tagged Codex release.")
parser.add_argument(
"-n",
"--dry-run",
action="store_true",
help="Print the version that would be used and exit before making changes.",
)
parser.add_argument(
"--promote-alpha",
metavar="VERSION",
help="Promote an existing alpha tag (e.g., 0.56.0-alpha.5) by using its merge-base with main as the base commit.",
)
group = parser.add_mutually_exclusive_group()
group.add_argument(
"--publish-alpha",
action="store_true",
help="Publish the next alpha release for the upcoming minor version.",
)
group.add_argument(
"--publish-release",
action="store_true",
help="Publish the next stable release by bumping the minor version.",
)
parser.add_argument(
"--emergency-version-override",
help="Publish a specific version because tag was created for the previous release but it never succeeded. Value should be semver, e.g., `0.43.0-alpha.9`.",
)
args = parser.parse_args(argv[1:])
if not (
args.publish_alpha
or args.publish_release
or args.emergency_version_override
or args.promote_alpha
):
parser.error(
"Must specify --publish-alpha, --publish-release, --promote-alpha, or --emergency-version-override."
)
return args
def main(argv: list[str]) -> int:
args = parse_args(argv)
# Strip the leading "v" if present.
promote_alpha = args.promote_alpha
if promote_alpha and promote_alpha.startswith("v"):
promote_alpha = promote_alpha[1:]
try:
if promote_alpha:
version = derive_release_version_from_alpha(promote_alpha)
elif args.emergency_version_override:
version = args.emergency_version_override
else:
version = determine_version(args)
print(f"Publishing version {version}")
if promote_alpha:
base_commit = get_promote_alpha_base_commit(promote_alpha)
if args.dry_run:
print(
f"Would publish version {version} using base commit {base_commit} derived from rust-v{promote_alpha}."
)
return 0
elif args.dry_run:
return 0
if not promote_alpha:
print("Fetching branch head...")
base_commit = get_branch_head()
print(f"Base commit: {base_commit}")
print("Fetching commit tree...")
base_tree = get_commit_tree(base_commit)
print(f"Base tree: {base_tree}")
print("Fetching Cargo.toml...")
current_contents = fetch_file_contents(base_commit)
print("Updating version...")
updated_contents = replace_version(current_contents, version)
print("Creating blob...")
blob_sha = create_blob(updated_contents)
print(f"Blob SHA: {blob_sha}")
print("Creating tree...")
tree_sha = create_tree(base_tree, blob_sha)
print(f"Tree SHA: {tree_sha}")
print("Creating commit...")
commit_sha = create_commit(version, tree_sha, base_commit)
print(f"Commit SHA: {commit_sha}")
print("Creating tag...")
tag_sha = create_tag(version, commit_sha)
print(f"Tag SHA: {tag_sha}")
print("Creating tag ref...")
create_tag_ref(version, tag_sha)
print("Done.")
except ReleaseError as error:
print(f"ERROR: {error}", file=sys.stderr)
return 1
return 0
class ReleaseError(RuntimeError):
pass
def run_gh_api(endpoint: str, *, method: str = "GET", payload: dict | None = None) -> dict:
print(f"Running gh api {method} {endpoint}")
command = [
"gh",
"api",
endpoint,
"--method",
method,
"-H",
"Accept: application/vnd.github+json",
]
json_payload = None
if payload is not None:
json_payload = json.dumps(payload)
print(f"Payload: {json_payload}")
command.extend(["-H", "Content-Type: application/json", "--input", "-"])
result = subprocess.run(command, text=True, capture_output=True, input=json_payload)
if result.returncode != 0:
message = result.stderr.strip() or result.stdout.strip() or "gh api call failed"
raise ReleaseError(message)
try:
return json.loads(result.stdout or "{}")
except json.JSONDecodeError as error:
raise ReleaseError("Failed to parse response from gh api.") from error
def get_branch_head() -> str:
response = run_gh_api(f"/repos/{REPO}/git/refs/{BRANCH_REF}")
try:
return response["object"]["sha"]
except KeyError as error:
raise ReleaseError("Unable to determine branch head.") from error
def get_promote_alpha_base_commit(alpha_version: str) -> str:
tag_name = f"rust-v{alpha_version}"
tag_commit_sha = get_tag_commit_sha(tag_name)
return get_merge_base_with_main(tag_commit_sha)
def get_tag_commit_sha(tag_name: str) -> str:
response = run_gh_api(f"/repos/{REPO}/git/refs/tags/{tag_name}")
try:
sha = response["object"]["sha"]
obj_type = response["object"]["type"]
except KeyError as error:
raise ReleaseError(f"Unable to resolve tag {tag_name}.") from error
while obj_type == "tag":
tag_response = run_gh_api(f"/repos/{REPO}/git/tags/{sha}")
try:
sha = tag_response["object"]["sha"]
obj_type = tag_response["object"]["type"]
except KeyError as error:
raise ReleaseError(f"Unable to resolve annotated tag {tag_name}.") from error
if obj_type != "commit":
raise ReleaseError(f"Tag {tag_name} does not reference a commit.")
return sha
def get_merge_base_with_main(commit_sha: str) -> str:
response = run_gh_api(f"/repos/{REPO}/compare/main...{commit_sha}")
try:
return response["merge_base_commit"]["sha"]
except KeyError as error:
raise ReleaseError("Unable to determine merge base with main.") from error
def get_commit_tree(commit_sha: str) -> str:
response = run_gh_api(f"/repos/{REPO}/git/commits/{commit_sha}")
try:
return response["tree"]["sha"]
except KeyError as error:
raise ReleaseError("Commit response missing tree SHA.") from error
def fetch_file_contents(ref_sha: str) -> str:
response = run_gh_api(f"/repos/{REPO}/contents/{CARGO_TOML_PATH}?ref={ref_sha}")
try:
encoded_content = response["content"].replace("\n", "")
encoding = response.get("encoding", "")
except KeyError as error:
raise ReleaseError("Failed to fetch Cargo.toml contents.") from error
if encoding != "base64":
raise ReleaseError(f"Unexpected Cargo.toml encoding: {encoding}")
try:
return base64.b64decode(encoded_content).decode("utf-8")
except (ValueError, UnicodeDecodeError) as error:
raise ReleaseError("Failed to decode Cargo.toml contents.") from error
def replace_version(contents: str, version: str) -> str:
updated, matches = re.subn(
r'^version = "[^"]+"', f'version = "{version}"', contents, count=1, flags=re.MULTILINE
)
if matches != 1:
raise ReleaseError("Unable to update version in Cargo.toml.")
return updated
def create_blob(content: str) -> str:
response = run_gh_api(
f"/repos/{REPO}/git/blobs",
method="POST",
payload={"content": content, "encoding": "utf-8"},
)
try:
return response["sha"]
except KeyError as error:
raise ReleaseError("Blob creation response missing SHA.") from error
def create_tree(base_tree_sha: str, blob_sha: str) -> str:
response = run_gh_api(
f"/repos/{REPO}/git/trees",
method="POST",
payload={
"base_tree": base_tree_sha,
"tree": [
{
"path": CARGO_TOML_PATH,
"mode": "100644",
"type": "blob",
"sha": blob_sha,
}
],
},
)
try:
return response["sha"]
except KeyError as error:
raise ReleaseError("Tree creation response missing SHA.") from error
def create_commit(version: str, tree_sha: str, parent_sha: str) -> str:
response = run_gh_api(
f"/repos/{REPO}/git/commits",
method="POST",
payload={
"message": f"Release {version}",
"tree": tree_sha,
"parents": [parent_sha],
},
)
try:
return response["sha"]
except KeyError as error:
raise ReleaseError("Commit creation response missing SHA.") from error
def create_tag(version: str, commit_sha: str) -> str:
tag_name = f"rust-v{version}"
response = run_gh_api(
f"/repos/{REPO}/git/tags",
method="POST",
payload={
"tag": tag_name,
"message": f"Release {version}",
"object": commit_sha,
"type": "commit",
},
)
try:
return response["sha"]
except KeyError as error:
raise ReleaseError("Tag creation response missing SHA.") from error
def create_tag_ref(version: str, tag_sha: str) -> None:
tag_ref = f"refs/tags/rust-v{version}"
run_gh_api(
f"/repos/{REPO}/git/refs",
method="POST",
payload={"ref": tag_ref, "sha": tag_sha},
)
def determine_version(args: argparse.Namespace) -> str:
latest_version = get_latest_release_version()
major, minor, patch = parse_semver(latest_version)
next_minor_version = format_version(major, minor + 1, patch)
if args.publish_release:
return next_minor_version
alpha_prefix = f"{next_minor_version}-alpha."
releases = list_releases()
highest_alpha = 0
found_alpha = False
for release in releases:
tag = release.get("tag_name", "")
candidate = strip_tag_prefix(tag)
if candidate and candidate.startswith(alpha_prefix):
suffix = candidate[len(alpha_prefix) :]
try:
alpha_number = int(suffix)
except ValueError:
continue
highest_alpha = max(highest_alpha, alpha_number)
found_alpha = True
if found_alpha:
return f"{alpha_prefix}{highest_alpha + 1}"
return f"{alpha_prefix}1"
def get_latest_release_version() -> str:
response = run_gh_api(f"/repos/{REPO}/releases/latest")
tag = response.get("tag_name")
version = strip_tag_prefix(tag)
if not version:
raise ReleaseError("Latest release tag has unexpected format.")
return version
def list_releases() -> list[dict]:
response = run_gh_api(f"/repos/{REPO}/releases?per_page=100")
if not isinstance(response, list):
raise ReleaseError("Unexpected response when listing releases.")
return response
def strip_tag_prefix(tag: str | None) -> str | None:
if not tag:
return None
prefix = "rust-v"
if not tag.startswith(prefix):
return None
return tag[len(prefix) :]
def parse_semver(version: str) -> tuple[int, int, int]:
parts = version.split(".")
if len(parts) != 3:
raise ReleaseError(f"Unexpected version format: {version}")
try:
return int(parts[0]), int(parts[1]), int(parts[2])
except ValueError as error:
raise ReleaseError(f"Version components must be integers: {version}") from error
def format_version(major: int, minor: int, patch: int) -> str:
return f"{major}.{minor}.{patch}"
def derive_release_version_from_alpha(alpha_version: str) -> str:
match = re.match(r"^(\d+)\.(\d+)\.(\d+)-alpha\.(\d+)$", alpha_version)
if match is None:
raise ReleaseError(f"Unexpected alpha version format: {alpha_version}")
return f"{match.group(1)}.{match.group(2)}.{match.group(3)}"
if __name__ == "__main__":
sys.exit(main(sys.argv))