diff --git a/.github/workflows/ci-main.yml b/.github/workflows/ci-main.yml
index 3cb4fa2..bf9984f 100644
--- a/.github/workflows/ci-main.yml
+++ b/.github/workflows/ci-main.yml
@@ -75,9 +75,6 @@ jobs:
token: ${{ secrets.CODECOV_TOKEN }}
verbose: true
- - name: Build Next.js app
- run: bun run build
-
docker-build:
name: Docker Build
needs: verify
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 40d9438..32c7a49 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -2,6 +2,16 @@ name: Build and Release
on:
workflow_dispatch:
+ inputs:
+ version_bump:
+ description: Version increment
+ required: true
+ default: patch
+ type: choice
+ options:
+ - major
+ - minor
+ - patch
push:
branches: [main]
@@ -35,7 +45,21 @@ jobs:
set -euo pipefail
current_version="$(node -p "require('./package.json').version")"
IFS='.' read -r major minor patch <<< "$current_version"
- version="${major}.${minor}.$((patch + 1))"
+ case "${{ inputs.version_bump }}" in
+ major)
+ version="$((major + 1)).0.0"
+ ;;
+ minor)
+ version="${major}.$((minor + 1)).0"
+ ;;
+ patch)
+ version="${major}.${minor}.$((patch + 1))"
+ ;;
+ *)
+ echo 'Unsupported version increment.' >&2
+ exit 1
+ ;;
+ esac
branch="release/v${version}"
node -e "const fs=require('fs'); const p='package.json'; const x=JSON.parse(fs.readFileSync(p,'utf8')); x.version=process.argv[1]; fs.writeFileSync(p,JSON.stringify(x,null,2)+'\\n')" "$version"
git switch -c "$branch"
@@ -108,8 +132,6 @@ jobs:
- run: bun run format:check
- run: bun run typecheck
- run: bun run test:coverage
- - run: bun run build
-
publish-image:
needs: [detect-release, verify]
if: ${{ needs.detect-release.outputs.should_release == 'true' }}
@@ -173,4 +195,23 @@ jobs:
TAG: ${{ needs.detect-release.outputs.release_tag }}
IMAGE: ${{ needs.detect-release.outputs.image }}:${{ needs.detect-release.outputs.version }}
run: |
- gh release create "$TAG" --target "$GITHUB_SHA" --title "$TAG" --latest --notes "Docker image: \`$IMAGE\`"
+ set -euo pipefail
+ previous_tag="$(git tag --sort=-v:refname | grep '^v' | head -n 1 || true)"
+ if [[ -n "$previous_tag" ]]; then
+ commits="$(git log --pretty=format:'- %h %s' "${previous_tag}..${GITHUB_SHA}")"
+ else
+ commits="$(git log --pretty=format:'- %h %s' "${GITHUB_SHA}")"
+ fi
+ notes_file="$(mktemp)"
+ {
+ echo "Docker image: \`$IMAGE\`"
+ echo
+ echo '## Commits'
+ echo
+ if [[ -n "$commits" ]]; then
+ printf '%s\n' "$commits"
+ else
+ echo '- No commits found.'
+ fi
+ } > "$notes_file"
+ gh release create "$TAG" --target "$GITHUB_SHA" --title "$TAG" --latest --notes-file "$notes_file"
diff --git a/README.md b/README.md
index d4a5fc1..028392b 100644
--- a/README.md
+++ b/README.md
@@ -3,60 +3,77 @@
[](https://github.com/orangeboyChen/codebuddy2api/actions/workflows/ci-main.yml)
[](https://codecov.io/gh/orangeboyChen/codebuddy2api)
-CodeBuddy2API is a self-hosted CodeBuddy gateway with OpenAI-compatible and
-Anthropic-compatible APIs, plus an admin console for credentials, access keys,
-usage, debugging, and runtime settings.
+Proxy CodeBuddy with OpenAI-compatible and Anthropic-compatible APIs for Codex, Claude Code, and standard SDK clients.
-> Forked from [Sliverkiss/CodeBuddy2api](https://github.com/Sliverkiss/CodeBuddy2api).
+
+
+
+
+CodeBuddy2API is a self-hosted gateway with a web-based admin console for managing credentials, access keys, usage, account status, debug traces, and runtime settings.
+
+This project is a substantial refactor of [Sliverkiss/CodeBuddy2api](https://github.com/Sliverkiss/CodeBuddy2api), with a redesigned admin console, multi-protocol API support, and flexible storage backends.
## Quick Start
-The following Docker command uses SQLite, recommended for a single instance.
-Replace the encryption key with a long random value before production use.
+The following command starts a single-instance deployment with SQLite:
```bash
docker run -d \
--name codebuddy2api \
--restart unless-stopped \
-p 8001:8001 \
+ -v codebuddy2api-data:/app/.codebuddy_data \
-e CODEBUDDY_STORAGE_BACKEND=sqlite \
-e CODEBUDDY_STORAGE_ENCRYPTION_KEY='replace-with-a-long-random-secret' \
-e CODEBUDDY_STORAGE_IMPORT_LEGACY_FILES=false \
ghcr.io/orangeboychen/codebuddy2api:latest
```
-Open `http://127.0.0.1:8001/dashboard`, add a CodeBuddy credential, then create
-an access key for API clients. Use PostgreSQL instead of SQLite for multiple
-application instances.
+Open `http://127.0.0.1:8001/dashboard`, complete CodeBuddy authentication or add a credential manually, then create an access key for your clients.
-## Documentation
+## API Compatibility
-- [中文文档](https://orangeboychen.github.io/codebuddy2api/)
-- [English documentation](https://orangeboychen.github.io/codebuddy2api/en/)
-- [日本語ドキュメント](https://orangeboychen.github.io/codebuddy2api/ja/)
+The gateway exposes these endpoints under `/v1`:
-Run the documentation site locally from the repository root:
+- `POST /v1/chat/completions` — OpenAI Chat Completions
+- `POST /v1/responses` — OpenAI Responses
+- `POST /v1/messages` — Anthropic Messages
+- `GET /v1/models` — models available to the requesting access key
-```bash
-bun install
-bun install --cwd docs
-bun run docs:dev
+Authenticate inference requests with either header:
+
+```http
+Authorization: Bearer
```
-Use `bun run docs:build` to build the static site and `bun run docs:preview` to
-preview it.
+```http
+x-api-key:
+```
-## Development
+Example OpenAI-compatible request:
```bash
-bun install
-bun run lint
-bun run format:check
-bun run typecheck
-bun run test:coverage
-bunx next build --webpack
+curl http://127.0.0.1:8001/v1/chat/completions \
+ -H 'Authorization: Bearer ' \
+ -H 'Content-Type: application/json' \
+ -d '{
+ "model": "",
+ "messages": [{"role": "user", "content": "Hello"}]
+ }'
```
+## Storage
+
+- `file` — zero-configuration storage for a single instance
+- `sqlite` — encrypted SQLite storage for a single instance
+- `pg` — PostgreSQL storage for multiple instances
+
+Database backends require `CODEBUDDY_STORAGE_ENCRYPTION_KEY`. Set `DATABASE_URL` for PostgreSQL or `CODEBUDDY_STORAGE_SQLITE_PATH` for SQLite.
+
+## Documentation
+
+[Read the documentation](https://orangeboychen.github.io/codebuddy2api/)
+
## License
See [LICENSE](./LICENSE).
diff --git a/app/account-status/account-status.tsx b/app/account-status/account-status.tsx
index 42c2f92..c1aa708 100644
--- a/app/account-status/account-status.tsx
+++ b/app/account-status/account-status.tsx
@@ -124,10 +124,33 @@ const CopyableModel = ({ model }: { model: string }) => {
const [copied, setCopied] = useState(false);
const text = useTranslations('Admin');
const copy = async () => {
- if (!navigator.clipboard) return;
- await navigator.clipboard.writeText(model);
- setCopied(true);
- window.setTimeout(() => setCopied(false), 1200);
+ try {
+ let copiedWithModernApi = false;
+ if (navigator.clipboard) {
+ try {
+ await navigator.clipboard.writeText(model);
+ copiedWithModernApi = true;
+ } catch {
+ copiedWithModernApi = false;
+ }
+ }
+ if (!copiedWithModernApi) {
+ const fallback = document.createElement('textarea');
+ fallback.value = model;
+ fallback.setAttribute('readonly', '');
+ fallback.style.position = 'fixed';
+ fallback.style.opacity = '0';
+ document.body.append(fallback);
+ fallback.select();
+ const copiedWithFallback = document.execCommand('copy');
+ fallback.remove();
+ if (!copiedWithFallback) return;
+ }
+ setCopied(true);
+ window.setTimeout(() => setCopied(false), 1200);
+ } catch {
+ return;
+ }
};
return (
@@ -178,13 +201,25 @@ const AccountStatusCard = ({
padding={20}
variant="outlined"
>
-
-
+
+
- {credential.email || credential.user_id}
+
+ {credential.email || credential.user_id}
+
-
+
{credential.filename}
@@ -213,8 +248,14 @@ const AccountStatusCard = ({
{text('accountStatus.resetAt')}: {snapshot.credits.resetAt ?? '—'}
-
-
+
+
{text('accountStatus.checkin')}:{' '}
{snapshot.checkin.claimed === true
? text('accountStatus.checkedIn')
diff --git a/app/globals.scss b/app/globals.scss
index c40363a..cac0898 100644
--- a/app/globals.scss
+++ b/app/globals.scss
@@ -166,8 +166,11 @@ textarea {
}
.console-header {
- position: sticky !important;
+ left: 0;
+ position: fixed !important;
+ right: 0;
top: 0;
+ width: 100%;
z-index: 30;
}
@@ -273,7 +276,7 @@ textarea {
.console-main {
width: min(1440px, 100%);
margin: 0 auto;
- padding: 24px 32px 48px;
+ padding: 80px 32px 48px;
}
.account-status-progress {
@@ -351,6 +354,26 @@ textarea {
opacity: 0.72;
}
+.account-status-card-header,
+.account-status-card-checkin {
+ min-width: 0;
+}
+
+.account-status-card-identity {
+ flex: 1 1 auto;
+ max-width: 100%;
+ min-width: 0;
+}
+
+.account-status-card-name,
+.account-status-card-filename,
+.account-status-card-checkin-label {
+ max-width: 100%;
+ min-width: 0;
+ overflow-wrap: anywhere;
+ word-break: break-word;
+}
+
@media (max-width: 767px) {
.console-main {
padding-inline: 16px;
@@ -361,8 +384,16 @@ textarea {
gap: 8px;
}
- .account-status-card button {
- flex: 1 1 0;
+ .account-status-card-header,
+ .account-status-card-checkin {
+ align-items: flex-start !important;
+ flex-direction: column !important;
+ gap: 8px !important;
+ }
+
+ .account-status-card-header > button,
+ .account-status-card-checkin > button {
+ flex: 0 0 auto;
}
}
@@ -853,7 +884,7 @@ textarea {
@media (max-width: 640px) {
.console-main {
- padding: 20px 0 32px;
+ padding: 76px 0 32px;
}
.console-main > * {
@@ -1061,6 +1092,19 @@ textarea {
word-break: break-word;
}
+ #debug .debug-credential {
+ overflow-wrap: anywhere;
+ word-break: break-word;
+ }
+
+ #debug .debug-credential * {
+ max-width: 100%;
+ min-width: 0;
+ overflow-wrap: anywhere;
+ word-break: break-word;
+ white-space: normal;
+ }
+
#debug .debug-entry-tags > span {
flex: 0 0 auto;
}