Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 30 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,16 @@ The server listens on `http://localhost:11434` by default (same port Ollama uses

### Configure credentials

Provide your Z.AI API key before launching the proxy:
**New users (recommended):**
```powershell
# Interactive setup - runs automatically on first use
copilot-proxy

# Or manually run setup wizard
copilot-proxy config setup
```

**Advanced users - Environment variables:**
```powershell
# PowerShell (current session only)
$env:ZAI_API_KEY = "your-zai-api-key"
Expand All @@ -58,6 +67,26 @@ Provide your Z.AI API key before launching the proxy:

You can optionally set a custom endpoint with `ZAI_API_BASE_URL`, though the default already targets the Coding Plan URL `https://api.z.ai/api/coding/paas/v4`.

**Configuration Management:**
```powershell
# Set API key in persistent config
copilot-proxy config set-api-key your-zai-api-key

# View current configuration
copilot-proxy config get-api-key

# Set custom base URL
copilot-proxy config set-base-url https://your-custom-endpoint.com

# Show config file location
copilot-proxy config show-path

# Start proxy (uses saved config automatically)
copilot-proxy serve
```

**Priority Order:** Config file > Environment variables > Default values

### Configure GitHub Copilot in VS Code

- Open the GitHub Copilot Chat panel in VS Code
Expand Down
26 changes: 21 additions & 5 deletions copilot_proxy/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
from fastapi import FastAPI, Request
from fastapi.responses import StreamingResponse

from .config import get_api_key as get_config_api_key, get_base_url as get_config_base_url

DEFAULT_BASE_URL = "https://api.z.ai/api/coding/paas/v4"
DEFAULT_MODEL = "GLM-4.6"
API_KEY_ENV_VARS = ("ZAI_API_KEY", "ZAI_CODING_API_KEY", "GLM_API_KEY")
Expand Down Expand Up @@ -62,20 +64,34 @@


def _get_api_key() -> str:
# First try to get API key from config file
config_api_key = get_config_api_key()
if config_api_key:
return config_api_key

# Fall back to environment variables
for env_var in API_KEY_ENV_VARS:
api_key = os.getenv(env_var)
if api_key:
return api_key.strip()

raise RuntimeError(
"Missing Z.AI API key. Please set one of the following environment variables: "
+ ", ".join(API_KEY_ENV_VARS)
"Missing Z.AI API key. Please set it using 'copilot-proxy config set-api-key <key>' "
f"or set one of the following environment variables: {', '.join(API_KEY_ENV_VARS)}"
)


def _get_base_url() -> str:
base_url = os.getenv(BASE_URL_ENV_VAR, DEFAULT_BASE_URL).strip()
if not base_url:
base_url = DEFAULT_BASE_URL
# First try to get base URL from config file
config_base_url = get_config_base_url()
if config_base_url:
base_url = config_base_url
else:
# Fall back to environment variable or default
base_url = os.getenv(BASE_URL_ENV_VAR, DEFAULT_BASE_URL).strip()
if not base_url:
base_url = DEFAULT_BASE_URL

if not base_url.startswith("http://") and not base_url.startswith("https://"):
base_url = f"https://{base_url}"
return base_url.rstrip("/")
Expand Down
151 changes: 139 additions & 12 deletions copilot_proxy/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,59 +2,186 @@
from __future__ import annotations

import argparse
import sys
from typing import Optional

import uvicorn

from .config import (
get_api_key, set_api_key, get_base_url, set_base_url, get_config_file,
is_first_run, ensure_complete_config
)

DEFAULT_HOST = "127.0.0.1"
DEFAULT_PORT = 11434


def interactive_setup(is_first: bool = True) -> None:
"""Run interactive setup for users."""
if is_first:
print("🚀 Welcome to Copilot Proxy!")
print("This appears to be your first time running the proxy.")
print("Let's set up your configuration.\n")
else:
print("⚙️ Copilot Proxy Setup")
print("Let's configure your settings.\n")

# Get API key
while True:
api_key = input("Please enter your Z.AI API key (or press Enter to skip): ").strip()
if not api_key:
print("⚠️ No API key provided. You can set it later with:")
print(" copilot-proxy config set-api-key <your-key>")
break

if len(api_key) < 10:
print("❌ API key seems too short. Please check your API key.")
continue

set_api_key(api_key)
print("✅ API key saved successfully!")
break

# Get optional base URL
base_url = input("\nEnter custom base URL (optional, press Enter to use default): ").strip()
if base_url:
set_base_url(base_url)
print("✅ Custom base URL saved!")

# Ensure all default values are saved to config
ensure_complete_config()

# Show config location
config_path = get_config_file()
print(f"\n📁 Configuration saved to: {config_path}")
print("\nYou're all set! You can now start the proxy with:")
print(" copilot-proxy serve")
print("\nYou can always change your configuration later using:")
print(" copilot-proxy config --help")


def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="Run the Copilot GLM proxy server.")
parser.add_argument(
subparsers = parser.add_subparsers(dest="command", help="Available commands")

# Serve command (default behavior)
serve_parser = subparsers.add_parser("serve", help="Start the proxy server")
serve_parser.add_argument(
"--host",
default=DEFAULT_HOST,
help=f"Host interface to bind (default: {DEFAULT_HOST}).",
)
parser.add_argument(
serve_parser.add_argument(
"--port",
type=int,
default=DEFAULT_PORT,
help=f"Port to bind (default: {DEFAULT_PORT}).",
)
parser.add_argument(
serve_parser.add_argument(
"--reload",
action="store_true",
help="Enable auto-reload (useful for development).",
)
parser.add_argument(
serve_parser.add_argument(
"--log-level",
default="info",
help="Log level passed to Uvicorn (default: info).",
)
parser.add_argument(
serve_parser.add_argument(
"--proxy-app",
default="copilot_proxy.app:app",
help=(
"Dotted path to the FastAPI application passed to Uvicorn "
"(default: copilot_proxy.app:app)."
),
)

# Config commands
config_parser = subparsers.add_parser("config", help="Manage configuration")
config_subparsers = config_parser.add_subparsers(dest="config_action", help="Config actions")

# Set API key
set_key_parser = config_subparsers.add_parser("set-api-key", help="Set API key in config")
set_key_parser.add_argument("api_key", help="Your Z.AI API key")

# Get API key
config_subparsers.add_parser("get-api-key", help="Show current API key from config")

# Set base URL
set_url_parser = config_subparsers.add_parser("set-base-url", help="Set base URL in config")
set_url_parser.add_argument("base_url", help="Custom base URL for API")

# Get base URL
config_subparsers.add_parser("get-base-url", help="Show current base URL from config")

# Show config path
config_subparsers.add_parser("show-path", help="Show where the config file is stored")

# Interactive setup
config_subparsers.add_parser("setup", help="Run interactive setup wizard")

return parser


def main(argv: Optional[list[str]] = None) -> None:
parser = build_parser()
args = parser.parse_args(argv)

uvicorn.run(
args.proxy_app,
host=args.host,
port=args.port,
reload=args.reload,
log_level=args.log_level,
)
# Check for first run and show interactive setup
# Only run interactive setup when no command is provided (default behavior)
if is_first_run() and args.command is None:
interactive_setup()
return

# Handle config commands
if args.command == "config":
if args.config_action == "set-api-key":
set_api_key(args.api_key)
print("API key saved to config file.")
elif args.config_action == "get-api-key":
api_key = get_api_key()
if api_key:
print(f"Current API key: {api_key}")
else:
print("No API key found in config file.")
elif args.config_action == "set-base-url":
set_base_url(args.base_url)
print("Base URL saved to config file.")
elif args.config_action == "get-base-url":
base_url = get_base_url()
if base_url:
print(f"Current base URL: {base_url}")
else:
print("No base URL found in config file.")
elif args.config_action == "show-path":
config_path = get_config_file()
print(f"Config file location: {config_path}")
print(f"Config directory: {config_path.parent}")
elif args.config_action == "setup":
interactive_setup(is_first=False)
else:
parser.print_help()
return

# Default to serve command if no command specified
if args.command is None or args.command == "serve":
# For serve command, use default values if not provided
host = getattr(args, 'host', DEFAULT_HOST)
port = getattr(args, 'port', DEFAULT_PORT)
reload = getattr(args, 'reload', False)
log_level = getattr(args, 'log_level', 'info')
proxy_app = getattr(args, 'proxy_app', 'copilot_proxy.app:app')

uvicorn.run(
proxy_app,
host=host,
port=port,
reload=reload,
log_level=log_level,
)
return

parser.print_help()


if __name__ == "__main__":
Expand Down
Loading