Skip to content
Merged
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
3 changes: 0 additions & 3 deletions .github/workflows/ci-main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
49 changes: 45 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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' }}
Expand Down Expand Up @@ -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"
71 changes: 44 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,60 +3,77 @@
[![CI](https://github.com/orangeboyChen/codebuddy2api/actions/workflows/ci-main.yml/badge.svg?branch=main)](https://github.com/orangeboyChen/codebuddy2api/actions/workflows/ci-main.yml)
[![codecov](https://codecov.io/gh/orangeboyChen/codebuddy2api/graph/badge.svg?token=SJP5CBSQ16)](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).
<p align="center">
<img src="./.github/images/codebuddy2api-social.jpg" alt="CodeBuddy2API" width="600" />
</p>

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 <access-key>
```

Use `bun run docs:build` to build the static site and `bun run docs:preview` to
preview it.
```http
x-api-key: <access-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 <access-key>' \
-H 'Content-Type: application/json' \
-d '{
"model": "<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).
61 changes: 51 additions & 10 deletions app/account-status/account-status.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<Tooltip title={copied ? text('common.copy') : text('common.copy')}>
Expand Down Expand Up @@ -178,13 +201,25 @@ const AccountStatusCard = ({
padding={20}
variant="outlined"
>
<Flexbox align="flex-start" distribution="space-between" horizontal>
<Flexbox direction="vertical" gap={4}>
<Flexbox
align="flex-start"
className="account-status-card-header"
distribution="space-between"
horizontal
width="100%"
>
<Flexbox
className="account-status-card-identity"
direction="vertical"
gap={4}
>
<Tooltip title={credential.email || credential.user_id}>
<Text strong>{credential.email || credential.user_id}</Text>
<Text className="account-status-card-name" strong>
{credential.email || credential.user_id}
</Text>
</Tooltip>
<Tooltip title={credential.filename}>
<Text ellipsis type="secondary">
<Text className="account-status-card-filename" type="secondary">

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Retain filename containment above 767px

At viewport widths of 768px or more, none of the new .account-status-card-* overflow rules apply, but this line removes ellipsis unconditionally. Because /admin-api/credentials accepts caller-supplied basename-only filenames without a length limit, a long unbroken filename can retain its intrinsic width on tablet or desktop and overflow the card or displace the Refresh button; keep truncation outside the mobile breakpoint or apply the min-width/wrapping constraints at every width.

Useful? React with 👍 / 👎.

{credential.filename}
</Text>
</Tooltip>
Expand Down Expand Up @@ -213,8 +248,14 @@ const AccountStatusCard = ({
{text('accountStatus.resetAt')}: {snapshot.credits.resetAt ?? '—'}
</Text>
</Flexbox>
<Flexbox align="center" distribution="space-between" horizontal>
<Text type="secondary">
<Flexbox
align="center"
className="account-status-card-checkin"
distribution="space-between"
horizontal
width="100%"
>
<Text className="account-status-card-checkin-label" type="secondary">
{text('accountStatus.checkin')}:{' '}
{snapshot.checkin.claimed === true
? text('accountStatus.checkedIn')
Expand Down
54 changes: 49 additions & 5 deletions app/globals.scss
Original file line number Diff line number Diff line change
Expand Up @@ -166,8 +166,11 @@ textarea {
}

.console-header {
position: sticky !important;
left: 0;
position: fixed !important;
right: 0;
top: 0;
width: 100%;
z-index: 30;
}

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
Expand All @@ -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;
}
}

Expand Down Expand Up @@ -853,7 +884,7 @@ textarea {

@media (max-width: 640px) {
.console-main {
padding: 20px 0 32px;
padding: 76px 0 32px;
}

.console-main > * {
Expand Down Expand Up @@ -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;
}
Expand Down
Loading