diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md deleted file mode 100644 index 135440dbf60a..000000000000 --- a/.github/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,83 +0,0 @@ -# Contributor Covenant Code of Conduct - -## Our Pledge - -We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual orientation. - -We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. - -## Our Standards - -Examples of behavior that contributes to a positive environment for our community include: - -- Demonstrating empathy and kindness toward other people -- Being respectful of differing opinions, viewpoints, and experiences -- Giving and gracefully accepting constructive feedback -- Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience -- Focusing on what is best not just for us as individuals, but for the overall community - -Examples of unacceptable behavior include: - -- The use of sexualized language or imagery, and sexual attention or advances of any kind -- Trolling, insulting or derogatory comments, and personal or political attacks -- Public or private harassment -- Publishing others' private information, such as a physical or email address, without their explicit permission -- Other conduct which could reasonably be considered inappropriate in a professional setting - -## Enforcement Responsibilities - -Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. - -Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. - -## Scope - -This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. - -## Enforcement - -Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at: - -**Email:** support@quantumnous.com - -All complaints will be reviewed and investigated promptly and fairly. - -All community leaders are obligated to respect the privacy and security of the reporter of any incident. - -## Enforcement Guidelines - -Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: - -### 1. Correction - -**Community Impact:** Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. - -**Consequence:** A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. - -### 2. Warning - -**Community Impact:** A violation through a single incident or series of actions. - -**Consequence:** A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. - -### 3. Temporary Ban - -**Community Impact:** A serious violation of community standards, including sustained inappropriate behavior. - -**Consequence:** A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. - -### 4. Permanent Ban - -**Community Impact:** Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. - -**Consequence:** A permanent ban from any sort of public interaction within the community. - -## Attribution - -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. - -Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). - -For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. - -[homepage]: https://www.contributor-covenant.org diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index 4b16f597ee78..000000000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1,12 +0,0 @@ -# These are supported funding model platforms - -github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] -patreon: # Replace with a single Patreon username -open_collective: # Replace with a single Open Collective username -ko_fi: # Replace with a single Ko-fi username -tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel -community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry -liberapay: # Replace with a single Liberapay username -issuehunt: # Replace with a single IssueHunt username -otechie: # Replace with a single Otechie username -custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml deleted file mode 100644 index fa1029f393d5..000000000000 --- a/.github/ISSUE_TEMPLATE/config.yml +++ /dev/null @@ -1,8 +0,0 @@ -blank_issues_enabled: false -contact_links: - - name: 使用文档 / Documentation - url: https://docs.newapi.ai/ - about: 提交 issue 前请先查阅文档,确认现有说明无法解决你的问题。 - - name: 使用问题 / Usage Questions - url: https://deepwiki.com/QuantumNous/new-api - about: 使用、配置、接入等问题请优先在 DeepWiki 查询或提问。 diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 1601b86c2e0f..361cad366eed 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -1,10 +1,9 @@ -name: Publish Docker image (Multi-arch) +name: Publish Docker image on: push: tags: - '*' - - '!nightly*' workflow_dispatch: inputs: tag: @@ -13,26 +12,10 @@ on: type: string jobs: - build_single_arch: - name: Build & push (${{ matrix.arch }}) - strategy: - fail-fast: false - matrix: - include: - - arch: amd64 - platform: linux/amd64 - runner: ubuntu-latest - - arch: arm64 - platform: linux/arm64 - runner: ubuntu-24.04-arm - runs-on: ${{ matrix.runner }} - outputs: - tag: ${{ steps.version.outputs.tag }} - + build: + runs-on: ubuntu-latest permissions: - packages: write contents: read - id-token: write steps: - name: Check out @@ -41,7 +24,7 @@ jobs: fetch-depth: ${{ github.event_name == 'workflow_dispatch' && 0 || 1 }} ref: ${{ github.event.inputs.tag || github.ref }} - - name: Resolve tag & write VERSION + - name: Resolve tag id: version run: | if [ -n "${{ github.event.inputs.tag }}" ]; then @@ -56,7 +39,6 @@ jobs: echo "TAG=${TAG}" >> $GITHUB_ENV echo "tag=${TAG}" >> $GITHUB_OUTPUT echo "${TAG}" > VERSION - echo "Building tag: ${TAG} for ${{ matrix.arch }}" - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 @@ -67,75 +49,14 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Extract metadata (labels) - id: meta - uses: docker/metadata-action@v5 - with: - images: calciumion/new-api - - - name: Build & push - id: build + - name: Build & push multi-arch image uses: docker/build-push-action@v6 with: context: . - platforms: ${{ matrix.platform }} + platforms: linux/amd64,linux/arm64 push: true tags: | - calciumion/new-api:${{ env.TAG }}-${{ matrix.arch }} - calciumion/new-api:latest-${{ matrix.arch }} - labels: ${{ steps.meta.outputs.labels }} + guygubaby/new-api:${{ env.TAG }} + guygubaby/new-api:latest cache-from: type=gha cache-to: type=gha,mode=max - provenance: mode=max - sbom: true - - - name: Install cosign - uses: sigstore/cosign-installer@v3 - - - name: Sign image with cosign - run: cosign sign --yes calciumion/new-api@${{ steps.build.outputs.digest }} - - - name: Image summary - run: | - echo "### Docker Image Digest (${{ matrix.arch }})" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - echo "calciumion/new-api:${TAG}-${{ matrix.arch }}" >> $GITHUB_STEP_SUMMARY - echo "${{ steps.build.outputs.digest }}" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - - create_manifests: - name: Create multi-arch manifests - needs: [build_single_arch] - runs-on: ubuntu-latest - if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch' - - steps: - - name: Set version - run: echo "TAG=${{ needs.build_single_arch.outputs.tag }}" >> $GITHUB_ENV - - - name: Log in to Docker Hub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Create & push manifest (version) - run: | - docker buildx imagetools create \ - -t calciumion/new-api:${TAG} \ - calciumion/new-api:${TAG}-amd64 \ - calciumion/new-api:${TAG}-arm64 - - - name: Create & push manifest (latest) - run: | - docker buildx imagetools create \ - -t calciumion/new-api:latest \ - calciumion/new-api:latest-amd64 \ - calciumion/new-api:latest-arm64 - - - name: Manifest summary - run: | - echo "### Multi-arch Manifest" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - docker buildx imagetools inspect calciumion/new-api:${TAG} >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/docker-image-alpha.yml b/.github/workflows/docker-image-alpha.yml deleted file mode 100644 index 116dd1452152..000000000000 --- a/.github/workflows/docker-image-alpha.yml +++ /dev/null @@ -1,179 +0,0 @@ -name: Publish Docker image (alpha) - -on: - push: - branches: - - alpha - workflow_dispatch: - inputs: - name: - description: "reason" - required: false - -jobs: - build_single_arch: - name: Build & push (${{ matrix.arch }}) [native] - strategy: - fail-fast: false - matrix: - include: - - arch: amd64 - platform: linux/amd64 - runner: ubuntu-latest - - arch: arm64 - platform: linux/arm64 - runner: ubuntu-24.04-arm - runs-on: ${{ matrix.runner }} - permissions: - packages: write - contents: read - id-token: write - steps: - - name: Check out (shallow) - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - with: - fetch-depth: 1 - - - name: Determine alpha version - id: version - run: | - VERSION="alpha-$(date +'%Y%m%d')-$(git rev-parse --short HEAD)" - echo "$VERSION" > VERSION - echo "value=$VERSION" >> $GITHUB_OUTPUT - echo "VERSION=$VERSION" >> $GITHUB_ENV - echo "Publishing version: $VERSION for ${{ matrix.arch }}" - - - name: Normalize GHCR repository - run: echo "GHCR_REPOSITORY=${GITHUB_REPOSITORY,,}" >> $GITHUB_ENV - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 - - - name: Log in to Docker Hub - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Log in to GHCR - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata (labels) - id: meta - uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5 - with: - images: | - calciumion/new-api - ghcr.io/${{ env.GHCR_REPOSITORY }} - - - name: Build & push single-arch (to both registries) - id: build - uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6 - with: - context: . - platforms: ${{ matrix.platform }} - push: true - tags: | - calciumion/new-api:alpha-${{ matrix.arch }} - calciumion/new-api:${{ steps.version.outputs.value }}-${{ matrix.arch }} - ghcr.io/${{ env.GHCR_REPOSITORY }}:alpha-${{ matrix.arch }} - ghcr.io/${{ env.GHCR_REPOSITORY }}:${{ steps.version.outputs.value }}-${{ matrix.arch }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max - provenance: mode=max - sbom: true - - - name: Install cosign - uses: sigstore/cosign-installer@398d4b0eeef1380460a10c8013a76f728fb906ac # v3 - - - name: Sign image with cosign - run: | - cosign sign --yes calciumion/new-api@${{ steps.build.outputs.digest }} - cosign sign --yes ghcr.io/${{ env.GHCR_REPOSITORY }}@${{ steps.build.outputs.digest }} - - - name: Output digest - run: | - echo "### Docker Image Digest (${{ matrix.arch }})" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - echo "calciumion/new-api:alpha-${{ matrix.arch }}" >> $GITHUB_STEP_SUMMARY - echo "ghcr.io/${{ env.GHCR_REPOSITORY }}:alpha-${{ matrix.arch }}" >> $GITHUB_STEP_SUMMARY - echo "${{ steps.build.outputs.digest }}" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - - create_manifests: - name: Create multi-arch manifests (Docker Hub + GHCR) - needs: [build_single_arch] - runs-on: ubuntu-latest - permissions: - packages: write - contents: read - steps: - - name: Check out (shallow) - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - with: - fetch-depth: 1 - - - name: Normalize GHCR repository - run: echo "GHCR_REPOSITORY=${GITHUB_REPOSITORY,,}" >> $GITHUB_ENV - - - name: Determine alpha version - id: version - run: | - VERSION="alpha-$(date +'%Y%m%d')-$(git rev-parse --short HEAD)" - echo "value=$VERSION" >> $GITHUB_OUTPUT - echo "VERSION=$VERSION" >> $GITHUB_ENV - - - name: Log in to Docker Hub - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Create & push manifest (Docker Hub - alpha) - run: | - docker buildx imagetools create \ - -t calciumion/new-api:alpha \ - calciumion/new-api:alpha-amd64 \ - calciumion/new-api:alpha-arm64 - - - name: Create & push manifest (Docker Hub - versioned alpha) - run: | - docker buildx imagetools create \ - -t calciumion/new-api:${VERSION} \ - calciumion/new-api:${VERSION}-amd64 \ - calciumion/new-api:${VERSION}-arm64 - - - name: Log in to GHCR - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Create & push manifest (GHCR - alpha) - run: | - docker buildx imagetools create \ - -t ghcr.io/${GHCR_REPOSITORY}:alpha \ - ghcr.io/${GHCR_REPOSITORY}:alpha-amd64 \ - ghcr.io/${GHCR_REPOSITORY}:alpha-arm64 - - - name: Create & push manifest (GHCR - versioned alpha) - run: | - docker buildx imagetools create \ - -t ghcr.io/${GHCR_REPOSITORY}:${VERSION} \ - ghcr.io/${GHCR_REPOSITORY}:${VERSION}-amd64 \ - ghcr.io/${GHCR_REPOSITORY}:${VERSION}-arm64 - - - name: Output manifest digest - run: | - echo "### Multi-arch Manifest Digests" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - docker buildx imagetools inspect calciumion/new-api:alpha >> $GITHUB_STEP_SUMMARY - echo "---" >> $GITHUB_STEP_SUMMARY - docker buildx imagetools inspect ghcr.io/${GHCR_REPOSITORY}:alpha >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/docker-image-nightly.yml b/.github/workflows/docker-image-nightly.yml deleted file mode 100644 index 2125fa9dd925..000000000000 --- a/.github/workflows/docker-image-nightly.yml +++ /dev/null @@ -1,113 +0,0 @@ -name: Publish Docker image (nightly) - -on: - push: - branches: - - nightly - workflow_dispatch: - inputs: - name: - description: "reason" - required: false - -jobs: - build_single_arch: - name: Build & push (${{ matrix.arch }}) [native] - strategy: - fail-fast: false - matrix: - include: - - arch: amd64 - platform: linux/amd64 - runner: ubuntu-latest - - arch: arm64 - platform: linux/arm64 - runner: ubuntu-24.04-arm - runs-on: ${{ matrix.runner }} - - permissions: - contents: read - - steps: - - name: Check out (shallow) - uses: actions/checkout@v4 - with: - fetch-depth: 1 - - - name: Determine nightly version - id: version - run: | - VERSION="nightly-$(date +'%Y%m%d')-$(git rev-parse --short HEAD)" - echo "$VERSION" > VERSION - echo "value=$VERSION" >> $GITHUB_OUTPUT - echo "VERSION=$VERSION" >> $GITHUB_ENV - echo "Publishing version: $VERSION for ${{ matrix.arch }}" - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Log in to Docker Hub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Extract metadata (labels) - id: meta - uses: docker/metadata-action@v5 - with: - images: | - calciumion/new-api - - - name: Build & push single-arch - uses: docker/build-push-action@v6 - with: - context: . - platforms: ${{ matrix.platform }} - push: true - tags: | - calciumion/new-api:nightly-${{ matrix.arch }} - calciumion/new-api:${{ steps.version.outputs.value }}-${{ matrix.arch }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max - provenance: false - sbom: false - - create_manifests: - name: Create multi-arch manifests (Docker Hub) - needs: [build_single_arch] - runs-on: ubuntu-latest - - steps: - - name: Check out (shallow) - uses: actions/checkout@v4 - with: - fetch-depth: 1 - - - name: Determine nightly version - id: version - run: | - VERSION="nightly-$(date +'%Y%m%d')-$(git rev-parse --short HEAD)" - echo "value=$VERSION" >> $GITHUB_OUTPUT - echo "VERSION=$VERSION" >> $GITHUB_ENV - - - name: Log in to Docker Hub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Create & push manifest (Docker Hub - nightly) - run: | - docker buildx imagetools create \ - -t calciumion/new-api:nightly \ - calciumion/new-api:nightly-amd64 \ - calciumion/new-api:nightly-arm64 - - - name: Create & push manifest (Docker Hub - versioned nightly) - run: | - docker buildx imagetools create \ - -t calciumion/new-api:${VERSION} \ - calciumion/new-api:${VERSION}-amd64 \ - calciumion/new-api:${VERSION}-arm64 diff --git a/.github/workflows/electron-build.yml b/.github/workflows/electron-build.yml deleted file mode 100644 index 20113e00fe6b..000000000000 --- a/.github/workflows/electron-build.yml +++ /dev/null @@ -1,141 +0,0 @@ -name: Build Electron App - -on: - push: - tags: - - '*' # Triggers on version tags like v1.0.0 - - '!*-*' # Ignore pre-release tags like v1.0.0-beta - - '!*-alpha*' # Ignore alpha tags like v1.0.0-alpha - workflow_dispatch: # Allows manual triggering - -jobs: - build: - strategy: - matrix: - # os: [macos-latest, windows-latest] - os: [windows-latest] - - runs-on: ${{ matrix.os }} - defaults: - run: - shell: bash - - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Setup Bun - uses: oven-sh/setup-bun@v2 - with: - bun-version: latest - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '20' - - - name: Setup Go - uses: actions/setup-go@v5 - with: - go-version: '>=1.25.1' - - - name: Build frontend - env: - CI: "" - NODE_OPTIONS: "--max-old-space-size=4096" - run: | - cd web - bun install - DISABLE_ESLINT_PLUGIN='true' VITE_REACT_APP_VERSION=$(git describe --tags) bun run build - cd .. - - # - name: Build Go binary (macos/Linux) - # if: runner.os != 'Windows' - # run: | - # go mod download - # go build -ldflags "-s -w -X 'new-api/common.Version=$(git describe --tags)' -extldflags '-static'" -o new-api - - - name: Build Go binary (Windows) - if: runner.os == 'Windows' - run: | - go mod download - go build -ldflags "-s -w -X 'new-api/common.Version=$(git describe --tags)'" -o new-api.exe - - - name: Update Electron version - run: | - cd electron - VERSION=$(git describe --tags) - VERSION=${VERSION#v} # Remove 'v' prefix if present - # Convert to valid semver: take first 3 components and convert rest to prerelease format - # e.g., 0.9.3-patch.1 -> 0.9.3-patch.1 - if [[ $VERSION =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)(.*)$ ]]; then - MAJOR=${BASH_REMATCH[1]} - MINOR=${BASH_REMATCH[2]} - PATCH=${BASH_REMATCH[3]} - REST=${BASH_REMATCH[4]} - - VERSION="$MAJOR.$MINOR.$PATCH" - - # If there's extra content, append it without adding -dev - if [[ -n "$REST" ]]; then - VERSION="$VERSION$REST" - fi - fi - npm version $VERSION --no-git-tag-version --allow-same-version - - - name: Install Electron dependencies - run: | - cd electron - npm install - - # - name: Build Electron app (macOS) - # if: runner.os == 'macOS' - # run: | - # cd electron - # npm run build:mac - # env: - # CSC_IDENTITY_AUTO_DISCOVERY: false # Skip code signing - - - name: Build Electron app (Windows) - if: runner.os == 'Windows' - run: | - cd electron - npm run build:win - - # - name: Upload artifacts (macOS) - # if: runner.os == 'macOS' - # uses: actions/upload-artifact@v4 - # with: - # name: macos-build - # path: | - # electron/dist/*.dmg - # electron/dist/*.zip - - - name: Upload artifacts (Windows) - if: runner.os == 'Windows' - uses: actions/upload-artifact@v4 - with: - name: windows-build - path: | - electron/dist/*.exe - - release: - needs: build - runs-on: ubuntu-latest - if: startsWith(github.ref, 'refs/tags/') - permissions: - contents: write - - steps: - - name: Download all artifacts - uses: actions/download-artifact@v4 - - - name: Upload to Release - uses: softprops/action-gh-release@v2 - with: - files: | - windows-build/* - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml deleted file mode 100644 index 2dcda35e676e..000000000000 --- a/.github/workflows/pr-check.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: PR Check - -permissions: - contents: read - issues: read - pull-requests: read - -on: - pull_request_target: - types: [opened, reopened] - -jobs: - pr-quality: - runs-on: ubuntu-latest - steps: - - uses: peakoss/anti-slop@v0.2.1 - with: - max-failures: 4 - require-description: true - - # require-linked-issue: false - blocked-terms: | - 🤖 Generated with Claude Code - - require-pr-template: true - strict-pr-template-sections: "✅ 提交前检查项 / Checklist" - - detect-spam-usernames: true - min-account-age: 30 - - failure-add-pr-labels: "pr-check-failed" - failure-pr-message: "感谢您的提交。由于该 PR 未遵循我们的贡献模板,且被识别为缺乏人工参与的纯 AI 生成内容 (AI Slop),我们将先予以关闭。我们更欢迎经过人工审核、验证并带有个人思考的贡献。如果您认为这其中存在误解,请回复告知。/ Thank you for your submission. This PR has been closed because it does not follow our contribution template and has been identified as purely AI-generated content (AI Slop) without meaningful human involvement. We prioritize contributions that are human-verified and reflect individual effort. If you believe this is a mistake, please let us know by replying to this comment." - close-pr: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 513bbb29522d..000000000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,180 +0,0 @@ -name: Release (Linux, macOS, Windows) -permissions: - contents: write - -on: - workflow_dispatch: - inputs: - name: - description: 'reason' - required: false - push: - tags: - - '*' - - '!*-alpha*' - -jobs: - linux: - name: Linux Release - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - with: - fetch-depth: 0 - - name: Determine Version - run: | - VERSION=$(git describe --tags) - echo "VERSION=$VERSION" >> $GITHUB_ENV - - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 - with: - bun-version: latest - - name: Build Frontend (default) - env: - CI: "" - run: | - cd web/default - bun install - DISABLE_ESLINT_PLUGIN='true' VITE_REACT_APP_VERSION=$VERSION bun run build - cd ../.. - - name: Build Frontend (classic) - env: - CI: "" - run: | - cd web/classic - bun install - VITE_REACT_APP_VERSION=$VERSION bun run build - cd ../.. - - name: Set up Go - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 - with: - go-version: '>=1.25.1' - - name: Build Backend (amd64) - run: | - go mod download - go build -ldflags "-s -w -X 'new-api/common.Version=$VERSION' -extldflags '-static'" -o new-api-$VERSION - - name: Build Backend (arm64) - run: | - sudo apt-get update - DEBIAN_FRONTEND=noninteractive sudo apt-get install -y gcc-aarch64-linux-gnu - CC=aarch64-linux-gnu-gcc CGO_ENABLED=1 GOOS=linux GOARCH=arm64 go build -ldflags "-s -w -X 'new-api/common.Version=$VERSION' -extldflags '-static'" -o new-api-arm64-$VERSION - - name: Generate checksums - run: sha256sum new-api-* > checksums-linux.txt - - - name: Release - uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2 - if: startsWith(github.ref, 'refs/tags/') - with: - files: | - new-api-* - checksums-linux.txt - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - macos: - name: macOS Release - runs-on: macos-latest - steps: - - name: Checkout - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - with: - fetch-depth: 0 - - name: Determine Version - run: | - VERSION=$(git describe --tags) - echo "VERSION=$VERSION" >> $GITHUB_ENV - - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 - with: - bun-version: latest - - name: Build Frontend (default) - env: - CI: "" - NODE_OPTIONS: "--max-old-space-size=4096" - run: | - cd web/default - bun install - DISABLE_ESLINT_PLUGIN='true' VITE_REACT_APP_VERSION=$VERSION bun run build - cd ../.. - - name: Build Frontend (classic) - env: - CI: "" - run: | - cd web/classic - bun install - VITE_REACT_APP_VERSION=$VERSION bun run build - cd ../.. - - name: Set up Go - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 - with: - go-version: '>=1.25.1' - - name: Build Backend - run: | - go mod download - go build -ldflags "-X 'new-api/common.Version=$VERSION'" -o new-api-macos-$VERSION - - name: Generate checksums - run: shasum -a 256 new-api-macos-* > checksums-macos.txt - - - name: Release - uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2 - if: startsWith(github.ref, 'refs/tags/') - with: - files: | - new-api-macos-* - checksums-macos.txt - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - windows: - name: Windows Release - runs-on: windows-latest - defaults: - run: - shell: bash - steps: - - name: Checkout - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - with: - fetch-depth: 0 - - name: Determine Version - run: | - VERSION=$(git describe --tags) - echo "VERSION=$VERSION" >> $GITHUB_ENV - - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 - with: - bun-version: latest - - name: Build Frontend (default) - env: - CI: "" - run: | - cd web/default - bun install - DISABLE_ESLINT_PLUGIN='true' VITE_REACT_APP_VERSION=$VERSION bun run build - cd ../.. - - name: Build Frontend (classic) - env: - CI: "" - run: | - cd web/classic - bun install - VITE_REACT_APP_VERSION=$VERSION bun run build - cd ../.. - - name: Set up Go - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 - with: - go-version: '>=1.25.1' - - name: Build Backend - run: | - go mod download - go build -ldflags "-s -w -X 'new-api/common.Version=$VERSION'" -o new-api-$VERSION.exe - - name: Generate checksums - run: sha256sum new-api-*.exe > checksums-windows.txt - - - name: Release - uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2 - if: startsWith(github.ref, 'refs/tags/') - with: - files: | - new-api-*.exe - checksums-windows.txt - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/sync-to-gitee.yml b/.github/workflows/sync-to-gitee.yml deleted file mode 100644 index 4f515a188dbe..000000000000 --- a/.github/workflows/sync-to-gitee.yml +++ /dev/null @@ -1,91 +0,0 @@ -name: Sync Release to Gitee - -permissions: - contents: read - -on: - workflow_dispatch: - inputs: - tag_name: - description: 'Release Tag to sync (e.g. v1.0.0)' - required: true - type: string - -# 配置你的 Gitee 仓库信息 -env: - GITEE_OWNER: 'QuantumNous' # 修改为你的 Gitee 用户名 - GITEE_REPO: 'new-api' # 修改为你的 Gitee 仓库名 - -jobs: - sync-to-gitee: - runs-on: sync - steps: - - name: Checkout - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - - name: Get Release Info - id: release_info - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - TAG_NAME: ${{ github.event.inputs.tag_name }} - run: | - # 获取 release 信息 - RELEASE_INFO=$(gh release view "$TAG_NAME" --json name,body,tagName,targetCommitish) - - RELEASE_NAME=$(echo "$RELEASE_INFO" | jq -r '.name') - TARGET_COMMITISH=$(echo "$RELEASE_INFO" | jq -r '.targetCommitish') - - # 使用多行字符串输出 - { - echo "release_name=$RELEASE_NAME" - echo "target_commitish=$TARGET_COMMITISH" - echo "release_body<> $GITHUB_OUTPUT - - # 下载 release 的所有附件 - gh release download "$TAG_NAME" --dir ./release_assets || echo "No assets to download" - - # 列出下载的文件 - ls -la ./release_assets/ || echo "No assets directory" - - - name: Create Gitee Release - id: create_release - uses: nICEnnnnnnnLee/action-gitee-release@v2.0.0 - with: - gitee_action: create_release - gitee_owner: ${{ env.GITEE_OWNER }} - gitee_repo: ${{ env.GITEE_REPO }} - gitee_token: ${{ secrets.GITEE_TOKEN }} - gitee_tag_name: ${{ github.event.inputs.tag_name }} - gitee_release_name: ${{ steps.release_info.outputs.release_name }} - gitee_release_body: ${{ steps.release_info.outputs.release_body }} - gitee_target_commitish: ${{ steps.release_info.outputs.target_commitish }} - - - name: Upload Assets to Gitee - if: hashFiles('release_assets/*') != '' - uses: nICEnnnnnnnLee/action-gitee-release@v2.0.0 - with: - gitee_action: upload_asset - gitee_owner: ${{ env.GITEE_OWNER }} - gitee_repo: ${{ env.GITEE_REPO }} - gitee_token: ${{ secrets.GITEE_TOKEN }} - gitee_release_id: ${{ steps.create_release.outputs.release-id }} - gitee_upload_retry_times: 3 - gitee_files: | - release_assets/* - - - name: Cleanup - if: always() - run: | - rm -rf release_assets/ - - - name: Summary - if: success() - run: | - echo "✅ Successfully synced release ${{ github.event.inputs.tag_name }} to Gitee!" - echo "🔗 Gitee Release URL: https://gitee.com/${{ env.GITEE_OWNER }}/${{ env.GITEE_REPO }}/releases/tag/${{ github.event.inputs.tag_name }}" - diff --git a/FORK_CHANGES.md b/FORK_CHANGES.md new file mode 100644 index 000000000000..5240d7665cdd --- /dev/null +++ b/FORK_CHANGES.md @@ -0,0 +1,294 @@ +# Fork 改动记录 + +本文档记录本 fork 自 [QuantumNous/new-api](https://github.com/QuantumNous/new-api) 之后,由本 fork 自己维护的非上游改动。 + +说明: +- 只记录本 fork 自己的非 merge commit。 +- 不把同步上游 `main` 带来的 merge 内容算作自定义改动。 +- 当前工作区口径基于 `main` 分支。 +- 本文档的主要用途:后续更新/覆盖上游源代码后,可以按这里的记录重新应用本 fork 的自定义修改。 + +--- + +## 改动概览 + +| 类别 | 文件 | 说明 | +|------|------|------| +| Docker CI | `.github/workflows/docker-build.yml` | Release 镜像发布流程简化,推送到 `guygubaby/new-api` | +| Docker CI | `.github/workflows/docker-image-alpha.yml` | 已删除 alpha 独立发布流程 | +| Docker CI | `.github/workflows/docker-image-nightly.yml` | 已删除 nightly 独立发布流程 | +| Docker CI | `.github/workflows/electron-build.yml` | 已删除 Electron 构建流程 | +| Docker CI | `.github/workflows/pr-check.yml` | 已删除 PR 检查流程 | +| Docker CI | `.github/workflows/release.yml` | 已删除 release 自动发布流程 | +| Docker CI | `.github/workflows/sync-to-gitee.yml` | 已删除 Gitee 同步流程 | +| Docker Context | `.dockerignore` | 许可证文件例外规则已被上游包含,同步后只需确认仍存在 | +| GitHub Metadata | `.github/CODE_OF_CONDUCT.md` | 已删除废弃社区模板 | +| GitHub Metadata | `.github/FUNDING.yml` | 已删除 funding 配置 | +| GitHub Metadata | `.github/ISSUE_TEMPLATE/config.yml` | 已删除 issue template 配置 | +| GitHub Metadata | `.github/ISSUE_TEMPLATE/{bug_report,feature_request}*.md` | 已由上游恢复;包含受保护项目链接,不再删除 | +| GitHub Metadata | `.github/PULL_REQUEST_TEMPLATE.md` | 已由上游恢复;包含受保护项目链接,不再删除 | +| GitHub Metadata | `.github/SECURITY.md` | 已由上游恢复;包含受保护项目链接,不再删除 | +| Bug Fix | `relay/channel/claude/relay-claude.go` | Claude `required: null` 字段修复 | +| Bug Fix | `relay/channel/gemini/relay-gemini.go` | Gemini `required: null` 字段修复 | +| Test | `relay/channel/gemini/relay_gemini_usage_test.go` | 新增 Gemini required 字段测试 | +| Docs | `FORK_CHANGES.md` | 记录 fork 自定义改动 | + +--- + +## Commit 历史 + +| Commit | 说明 | +|--------|------| +| `4f329bb2` | fix: normalize required null to empty array for Gemini/Claude, simplify Docker CI workflows | +| `b81a435b` | simplify: remove per-arch tags, only push multi-arch version and latest | +| `35e3f189` | docs: add fork changes documentation | +| `cdd94481` | fix: include third-party licenses in docker context(当前已被上游包含,通常无需重新应用) | +| `6d36a339` | chore: remove unused github workflows | +| `0407cfa3` | chore: remove deprecated issue templates and security policy files(重新应用时不删除包含受保护项目链接的文件) | + +--- + +## 重新应用方式 + +### 推荐方式:按 commit 重新 cherry-pick + +如果这些 commit 仍在当前 fork 仓库历史里,更新到新的上游代码后,按顺序重新应用: + +```bash +git cherry-pick 4f329bb2 +git cherry-pick b81a435b +git cherry-pick 6d36a339 +git cherry-pick 0407cfa3 +``` + +说明: +- `35e3f189` 只新增/更新本文档,通常不需要作为业务 patch 重新应用。 +- `cdd94481` 的 `.dockerignore` 改动当前已被上游包含,只有在新上游缺少 `!THIRD-PARTY-LICENSES.md` 时才需要重新应用。 +- 如果新上游已经包含类似改动,cherry-pick 可能为空或冲突,需要按本文档逐项确认。 +- 如果使用“直接覆盖上游源码”的方式更新,建议先保留本文件,更新后再按下面的手工恢复清单操作。 + +### 手工恢复清单 + +如果不能 cherry-pick,按以下顺序手动恢复: + +1. 恢复 Docker 发布 workflow:只保留 `.github/workflows/docker-build.yml`,推送 `guygubaby/new-api:` 和 `guygubaby/new-api:latest` +2. 删除不需要的 workflow:`docker-image-alpha.yml`、`docker-image-nightly.yml`、`electron-build.yml`、`pr-check.yml`、`release.yml`、`sync-to-gitee.yml` +3. 检查 `.dockerignore`:确保存在 `!THIRD-PARTY-LICENSES.md` +4. 删除不适用的 GitHub 元数据:`CODE_OF_CONDUCT.md`、`FUNDING.yml`、`ISSUE_TEMPLATE/config.yml` +5. 保留包含受保护项目链接的上游文件:issue 模板、`PULL_REQUEST_TEMPLATE.md`、`SECURITY.md` +6. 恢复 Claude `required: null` 到 `required: []` 的处理 +7. 恢复 Gemini `normalizeRequiredArray` 递归处理和对应测试 +8. 检查 `FORK_CHANGES.md` 是否仍存在并更新为最新实际改动 + +--- + +## 1. Docker 镜像发布流程 + +### 当前保留的 Workflow + +只保留 `.github/workflows/docker-build.yml` 作为 Docker Hub 发布流程。 + +触发方式: +- 推送任意 tag:`push.tags: ['*']` +- 手动触发:`workflow_dispatch`,需要填写 `tag` + +发布镜像: +- `guygubaby/new-api:` +- `guygubaby/new-api:latest` + +平台: +- `linux/amd64` +- `linux/arm64` + +### 改动内容 + +将 Docker Hub 镜像名从上游的 `calciumion/new-api` 改为自己的 `guygubaby/new-api`。 + +```yaml +tags: | + guygubaby/new-api:${{ env.TAG }} + guygubaby/new-api:latest +``` + +### 流程简化 + +移除的内容: +- 分平台单独构建 job +- 手动创建 multi-arch manifest job +- 分平台标签:`-amd64`、`-arm64` +- GHCR 发布权限 +- cosign 镜像签名 +- SBOM 生成 +- provenance 来源证明 +- Docker metadata labels +- Job summary 输出 + +当前结构: +- 单个 `build` job +- Docker Buildx 直接构建并推送 multi-arch 镜像 +- 只依赖 Docker Hub 登录凭据:`DOCKERHUB_USERNAME`、`DOCKERHUB_TOKEN` + +### 已删除的 Workflow + +以下 workflow 已由本 fork 删除: +- `.github/workflows/docker-image-alpha.yml` +- `.github/workflows/docker-image-nightly.yml` +- `.github/workflows/electron-build.yml` +- `.github/workflows/pr-check.yml` +- `.github/workflows/release.yml` +- `.github/workflows/sync-to-gitee.yml` + +维护注意: +- `docker-image-alpha.yml` 已不存在,不再有独立 alpha 自动发布流程。 +- `.github/workflows/docker-build.yml` 当前不再排除 `nightly*` tag。 + +--- + +## 2. Docker Context 许可证文件 + +### 当前状态 + +上游当前已经包含 `!THIRD-PARTY-LICENSES.md` 例外规则。这个改动不再需要作为 fork patch 主动重新应用。 + +同步上游后仍需要确认该规则没有丢失。 + +### 原始问题 + +`.dockerignore` 默认忽略 `*.md`,如果没有例外规则,会导致 `THIRD-PARTY-LICENSES.md` 无法进入 Docker build context。 + +### 需要保留的规则 + +`.dockerignore` 中需要存在: + +```dockerignore +*.md +!THIRD-PARTY-LICENSES.md +``` + +这样 Docker 构建时可以包含第三方许可证文件。 + +--- + +## 3. GitHub 模板与元数据清理 + +### 改动内容 + +删除 fork 中不再维护或不适用的 GitHub 社区/流程文件: + +- `.github/CODE_OF_CONDUCT.md` +- `.github/FUNDING.yml` +- `.github/ISSUE_TEMPLATE/config.yml` + +以下文件曾在旧 fork 改动中删除,但上游恢复后包含 `QuantumNous/new-api` 等受保护项目链接,后续不再重新删除: + +- `.github/ISSUE_TEMPLATE/bug_report.md` +- `.github/ISSUE_TEMPLATE/bug_report_en.md` +- `.github/ISSUE_TEMPLATE/feature_request.md` +- `.github/ISSUE_TEMPLATE/feature_request_en.md` +- `.github/PULL_REQUEST_TEMPLATE.md` +- `.github/SECURITY.md` + +### 目的 + +减少 fork 维护负担,同时避免删除受保护的项目链接、归属信息和安全披露入口。 + +--- + +## 4. Gemini/Claude Required 字段修复 + +### 问题背景 + +部分上游 API 不接受 JSON Schema 中的 `required: null`。 + +本 fork 将 `required: null` 规范化为 `required: []`,避免请求被拒绝。 + +### Claude 适配 + +文件:`relay/channel/claude/relay-claude.go` + +位置:`RequestOpenAI2ClaudeMessage` + +```go +if params["required"] == nil { + claudeTool.InputSchema["required"] = []any{} +} else { + claudeTool.InputSchema["required"] = params["required"] +} +``` + +### Gemini 适配 + +文件:`relay/channel/gemini/relay-gemini.go` + +新增 `normalizeRequiredArray`,递归处理 schema 中的 `required` 字段: + +```go +func normalizeRequiredArray(schema map[string]interface{}) { + if schema == nil { + return + } + + if required, exists := schema["required"]; exists && required == nil { + schema["required"] = []interface{}{} + } + + for _, value := range schema { + switch v := value.(type) { + case map[string]interface{}: + normalizeRequiredArray(v) + case []interface{}: + for _, item := range v { + if itemMap, ok := item.(map[string]interface{}); ok { + normalizeRequiredArray(itemMap) + } + } + } + } +} +``` + +调用位置: +- `cleanFunctionParametersWithDepth` +- `cleanFunctionParametersShallow` + +调用顺序:在 `normalizeGeminiSchemaTypeAndNullable(cleanedMap)` 之后执行 `normalizeRequiredArray(cleanedMap)`。 + +### 测试覆盖 + +文件:`relay/channel/gemini/relay_gemini_usage_test.go` + +新增测试:`TestNormalizeRequiredArray` + +覆盖场景: +- nil schema +- 顶层 `required: null` +- 顶层 `required` 已是数组 +- 无 `required` 字段 +- nested schema 中的 `required: null` + +--- + +## 同步上游更新 + +当上游有新更新时,建议使用: + +```bash +git fetch upstream +git merge upstream/main +git push origin main +``` + +如果本地没有 upstream remote: + +```bash +git remote add upstream https://github.com/QuantumNous/new-api.git +``` + +同步后需要重点检查: + +1. `.github/workflows/docker-build.yml` 是否仍只推送 `guygubaby/new-api` +2. 被删除的 workflow 是否被上游重新带回 +3. `.dockerignore` 是否仍保留 `!THIRD-PARTY-LICENSES.md` +4. Gemini/Claude 参数清理逻辑是否仍会执行 `required: null` 规范化 +5. `CODE_OF_CONDUCT.md`、`FUNDING.yml`、`ISSUE_TEMPLATE/config.yml` 是否被上游重新带回 +6. issue 模板、PR 模板和 `SECURITY.md` 中的受保护项目链接是否仍保留 diff --git a/controller/pricing.go b/controller/pricing.go index 8252327244c4..7256a8218532 100644 --- a/controller/pricing.go +++ b/controller/pricing.go @@ -9,6 +9,8 @@ import ( "github.com/gin-gonic/gin" ) +const defaultGuestPricingGroup = "default" + func filterPricingByUsableGroups(pricing []model.Pricing, usableGroup map[string]string) []model.Pricing { if len(pricing) == 0 { return pricing @@ -33,24 +35,31 @@ func filterPricingByUsableGroups(pricing []model.Pricing, usableGroup map[string return filtered } +func getPricingUserGroup(c *gin.Context) string { + userId := c.GetInt("id") + if userId == 0 { + return defaultGuestPricingGroup + } + user, err := model.GetUserCache(userId) + if err != nil { + return "" + } + return user.Group +} + func GetPricing(c *gin.Context) { pricing := model.GetPricing() - userId, exists := c.Get("id") usableGroup := map[string]string{} groupRatio := map[string]float64{} for s, f := range ratio_setting.GetGroupRatioCopy() { groupRatio[s] = f } - var group string - if exists { - user, err := model.GetUserCache(userId.(int)) - if err == nil { - group = user.Group - for g := range groupRatio { - ratio, ok := ratio_setting.GetGroupGroupRatio(group, g) - if ok { - groupRatio[g] = ratio - } + group := getPricingUserGroup(c) + if group != "" { + for g := range groupRatio { + ratio, ok := ratio_setting.GetGroupGroupRatio(group, g) + if ok { + groupRatio[g] = ratio } } } diff --git a/controller/pricing_test.go b/controller/pricing_test.go new file mode 100644 index 000000000000..47207e5f8989 --- /dev/null +++ b/controller/pricing_test.go @@ -0,0 +1,44 @@ +package controller + +import ( + "net/http/httptest" + "testing" + + "github.com/QuantumNous/new-api/model" + "github.com/QuantumNous/new-api/service" + "github.com/QuantumNous/new-api/setting" + + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/require" +) + +func TestGuestPricingUsesDefaultGroupWhenSelectableGroupsEmpty(t *testing.T) { + originalUserUsableGroups := setting.UserUsableGroups2JSONString() + require.NoError(t, setting.UpdateUserUsableGroupsByJSONString(`{}`)) + t.Cleanup(func() { + require.NoError(t, setting.UpdateUserUsableGroupsByJSONString(originalUserUsableGroups)) + }) + + gin.SetMode(gin.TestMode) + ctx, _ := gin.CreateTestContext(httptest.NewRecorder()) + + group := getPricingUserGroup(ctx) + require.Equal(t, "default", group) + + usableGroup := service.GetUserUsableGroups(group) + require.Contains(t, usableGroup, "default") + + pricing := []model.Pricing{ + {ModelName: "default-model", EnableGroup: []string{"default"}}, + {ModelName: "vip-model", EnableGroup: []string{"vip"}}, + {ModelName: "all-model", EnableGroup: []string{"all"}}, + } + + filtered := filterPricingByUsableGroups(pricing, usableGroup) + names := make([]string, 0, len(filtered)) + for _, item := range filtered { + names = append(names, item.ModelName) + } + + require.ElementsMatch(t, []string{"default-model", "all-model"}, names) +} diff --git a/relay/channel/claude/relay-claude.go b/relay/channel/claude/relay-claude.go index 18d7455e9f22..db86443cda9e 100644 --- a/relay/channel/claude/relay-claude.go +++ b/relay/channel/claude/relay-claude.go @@ -58,7 +58,12 @@ func RequestOpenAI2ClaudeMessage(c *gin.Context, textRequest dto.GeneralOpenAIRe claudeTool.InputSchema["type"] = params["type"].(string) } claudeTool.InputSchema["properties"] = params["properties"] - claudeTool.InputSchema["required"] = params["required"] + // Normalize required field: convert null to empty array + if params["required"] == nil { + claudeTool.InputSchema["required"] = []any{} + } else { + claudeTool.InputSchema["required"] = params["required"] + } for s, a := range params { if s == "type" || s == "properties" || s == "required" { continue diff --git a/relay/channel/gemini/relay-gemini.go b/relay/channel/gemini/relay-gemini.go index e39826dd64e7..c14d62451061 100644 --- a/relay/channel/gemini/relay-gemini.go +++ b/relay/channel/gemini/relay-gemini.go @@ -753,6 +753,9 @@ func cleanFunctionParametersWithDepth(params interface{}, depth int) interface{} normalizeGeminiSchemaTypeAndNullable(cleanedMap) + // Normalize required field: convert null to empty array + normalizeRequiredArray(cleanedMap) + // Clean properties if props, ok := cleanedMap["properties"].(map[string]interface{}); ok && props != nil { cleanedProps := make(map[string]interface{}) @@ -806,6 +809,10 @@ func cleanFunctionParametersShallow(params interface{}) interface{} { } } normalizeGeminiSchemaTypeAndNullable(cleanedMap) + // Normalize required field: convert null to empty array + if required, exists := cleanedMap["required"]; exists && required == nil { + cleanedMap["required"] = []interface{}{} + } // Stop recursion and avoid retaining huge nested structures. delete(cleanedMap, "properties") delete(cleanedMap, "items") @@ -881,6 +888,33 @@ func normalizeGeminiSchemaTypeAndNullable(schema map[string]interface{}) { } } +// normalizeRequiredArray normalizes the "required" field in JSON Schema. +// Gemini rejects null values for "required", so we convert null to an empty array. +func normalizeRequiredArray(schema map[string]interface{}) { + if schema == nil { + return + } + + // Handle "required" field: convert null to empty array + if required, exists := schema["required"]; exists && required == nil { + schema["required"] = []interface{}{} + } + + // Recursively normalize nested schemas + for _, value := range schema { + switch v := value.(type) { + case map[string]interface{}: + normalizeRequiredArray(v) + case []interface{}: + for _, item := range v { + if itemMap, ok := item.(map[string]interface{}); ok { + normalizeRequiredArray(itemMap) + } + } + } + } +} + func removeAdditionalPropertiesWithDepth(schema interface{}, depth int) interface{} { if depth >= 5 { return schema diff --git a/relay/channel/gemini/relay_gemini_usage_test.go b/relay/channel/gemini/relay_gemini_usage_test.go index c8f9f834300c..556bd851485c 100644 --- a/relay/channel/gemini/relay_gemini_usage_test.go +++ b/relay/channel/gemini/relay_gemini_usage_test.go @@ -331,3 +331,58 @@ func TestGeminiTextGenerationHandlerUsesEstimatedPromptTokensWhenUsagePromptMiss require.Equal(t, 100, usage.CompletionTokens) require.Equal(t, 110, usage.TotalTokens) } + +func TestNormalizeRequiredArray(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + input map[string]interface{} + expected map[string]interface{} + }{ + { + name: "nil schema", + input: nil, + expected: nil, + }, + { + name: "required is null", + input: map[string]interface{}{"required": nil, "type": "object"}, + expected: map[string]interface{}{"required": []interface{}{}, "type": "object"}, + }, + { + name: "required is array", + input: map[string]interface{}{"required": []string{"name"}, "type": "object"}, + expected: map[string]interface{}{"required": []string{"name"}, "type": "object"}, + }, + { + name: "required not present", + input: map[string]interface{}{"type": "object"}, + expected: map[string]interface{}{"type": "object"}, + }, + { + name: "nested schema with null required", + input: map[string]interface{}{ + "type": "object", + "required": nil, + "properties": map[string]interface{}{"nested": map[string]interface{}{"required": nil}}, + }, + expected: map[string]interface{}{ + "type": "object", + "required": []interface{}{}, + "properties": map[string]interface{}{"nested": map[string]interface{}{"required": []interface{}{}}}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + normalizeRequiredArray(tt.input) + if tt.expected == nil { + require.Nil(t, tt.input) + return + } + require.Equal(t, tt.expected, tt.input) + }) + } +}