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
152 changes: 152 additions & 0 deletions .github/workflows/docker-image-dev.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
name: Publish Docker image (dev)

# Builds the full image (Dockerfile): frontend (web/default + classic) is
# built with bun and embedded into the Go binary, so a single port (:3000)
# serves both the API and the frontend. Pushed to the GitHub Container
# Registry (ghcr.io) under this repo's owner namespace. Authenticates with
# the built-in GITHUB_TOKEN, so no Docker Hub secrets are required.
#
# Each architecture is built natively in parallel (no QEMU emulation), then
# merged into a single multi-arch manifest — much faster than a single
# emulated multi-arch build.
#
# After the first publish, set the package to "public" at
# https://github.com/users/<owner>/packages/container/new-api/settings
# so it can be pulled anonymously.

on:
push:
branches:
- feat/image-aware-model-routing
paths-ignore:
- '*.md'
- 'docs/**'
workflow_dispatch:
inputs:
name:
description: "reason"
required: false

jobs:
build_single_arch:
name: Build & push (${{ matrix.arch }}) [native]
strategy:
Comment on lines +31 to +33

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Protect :dev publishing from out-of-order workflow races.

Both jobs publish mutable :dev* tags, but there is no workflow concurrency guard. Older runs can finish later and overwrite the latest :dev manifest, which impacts deployment consumers of :dev.

Suggested minimal fix
+concurrency:
+  group: docker-image-dev-${{ github.ref }}
+  cancel-in-progress: true
+
 jobs:
   build_single_arch:

Also applies to: 98-100, 133-145

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/docker-image-dev.yml around lines 31 - 33, The Docker
image workflow lacks concurrency protection for the `:dev` tag publishing,
allowing older workflow runs to finish later and overwrite newer `:dev`
manifests. Add concurrency configuration to the build_single_arch job and any
other jobs that publish mutable `:dev*` tags to ensure only one run executes at
a time and newer runs cancel in-progress older runs. Use a stable concurrency
group identifier based on the branch or workflow context, and configure the
concurrency setting to automatically cancel previous runs when a new run starts,
preventing race conditions where stale images overwrite current ones.

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
steps:
- name: Check out (shallow)
uses: actions/checkout@v4
with:
fetch-depth: 1
Comment on lines +48 to +51

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add persist-credentials: false to limit credential exposure.

The checkout action persists Git credentials by default, which could be accessed by subsequent steps or artifacts. Since this workflow doesn't need to push commits, disable credential persistence.

       - name: Check out (shallow)
         uses: actions/checkout@v4
         with:
           fetch-depth: 1
+          persist-credentials: false
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- name: Check out (shallow)
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Check out (shallow)
uses: actions/checkout@v4
with:
fetch-depth: 1
persist-credentials: false
🧰 Tools
🪛 zizmor (1.25.2)

[warning] 34-37: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[error] 35-35: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/docker-image-dev.yml around lines 34 - 37, The checkout
action step named "Check out (shallow)" that uses actions/checkout@v4 is missing
the persist-credentials configuration. Add persist-credentials: false to the
with section of this action alongside the existing fetch-depth parameter to
prevent Git credentials from being persisted in the workflow environment.

Source: Linters/SAST tools


- name: Resolve GHCR image name
id: ghcr
run: |
OWNER_LC=$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]')
echo "image=ghcr.io/${OWNER_LC}/new-api" >> $GITHUB_OUTPUT

- name: Determine dev version
id: version
run: |
SHORT_SHA=$(git rev-parse --short HEAD)
echo "versioned=dev-${SHORT_SHA}" >> $GITHUB_OUTPUT
echo "Publishing dev image for ${{ matrix.arch }}: dev, dev-${SHORT_SHA}"

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Build & push single-arch
id: build
uses: docker/build-push-action@v6

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Unpinned action references in workflow files:"
rg -nP 'uses:\s*[^@\s]+@[vV]?\d+(\.\d+)?$' .github/workflows/*.yml .github/workflows/*.yaml || true

echo
echo "Resolve current tag SHAs (for pinning):"
for spec in \
  "actions/checkout v4" \
  "docker/build-push-action v6" \
  "docker/login-action v3" \
  "docker/setup-buildx-action v3"
do
  action="${spec% *}"
  tag="${spec##* }"
  echo "== $action@$tag =="
  gh api "repos/${action}/git/ref/tags/${tag}" --jq '.object.sha'
done

Repository: QuantumNous/new-api

Length of output: 2933


Pin GitHub Actions to immutable commit SHAs.

Lines 78, 107, 124, and 131 use floating version tags (@v6/@v4/@v3). Floating tags permit silent upstream changes in CI and violate security policy.

Resolve to commit SHAs:

  • actions/checkout@v434e114876b0b11c390a56381ad16ebd13914f8d5
  • docker/build-push-action@v610e90e3645eae34f1e60eeb005ba3a3d33f178e8
  • docker/login-action@v3c94ce9fb468520275223c153574b00df6fe4bcc9
  • docker/setup-buildx-action@v38d2750c68a42422c14e847fe6c8ac0403b4cbd6f
🧰 Tools
🪛 zizmor (1.25.2)

[error] 78-78: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/docker-image-dev.yml at line 78, Replace all floating
GitHub Actions version tags with their corresponding immutable commit SHAs in
the workflow file at lines 78, 107, 124, and 131 to prevent silent upstream
changes. Change docker/build-push-action@v6 to use SHA
10e90e3645eae34f1e60eeb005ba3a3d33f178e8, docker/login-action@v3 to
c94ce9fb468520275223c153574b00df6fe4bcc9, docker/setup-buildx-action@v3 to
8d2750c68a42422c14e847fe6c8ac0403b4cbd6f, and actions/checkout@v4 to
34e114876b0b11c390a56381ad16ebd13914f8d5 by replacing the `@vN` tag suffix with
the full commit SHA.

Source: Linters/SAST tools

with:
context: .
file: ./Dockerfile

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check Dockerfile existence and verify docker-compose.deploy.yml references

echo "=== Dockerfiles in repository ==="
fd -t f '^Dockerfile' -d 1

echo -e "\n=== docker-compose.deploy.yml references ==="
rg -n 'Dockerfile' docker-compose.deploy.yml || echo "No direct Dockerfile reference in services"

echo -e "\n=== Verify comment accuracy in docker-compose.deploy.yml ==="
rg -n -B2 -A2 'Dockerfile.dev|from Dockerfile' docker-compose.deploy.yml

Repository: QuantumNous/new-api

Length of output: 608


Verify and fix Dockerfile reference mismatch.

The workflow builds from ./Dockerfile (line 81), but docker-compose.deploy.yml documents that the CI builds from Dockerfile.dev. Both files exist in the repository. Confirm which Dockerfile should be used and update whichever is incorrect—either this workflow or the comment in docker-compose.deploy.yml (line 3).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/docker-image-dev.yml at line 81, There is a mismatch
between the Dockerfile reference in the docker-image-dev workflow and the
documentation in docker-compose.deploy.yml. The workflow file currently
references `./Dockerfile` but docker-compose.deploy.yml indicates the CI builds
from `Dockerfile.dev`. Verify which Dockerfile is the correct one to use for the
development image build, then update either the file parameter in the
docker-image-dev workflow (the `file:` field pointing to `./Dockerfile`) or the
comment in docker-compose.deploy.yml to ensure consistency across both files.

platforms: ${{ matrix.platform }}
push: true
tags: |
${{ steps.ghcr.outputs.image }}:dev-${{ matrix.arch }}
${{ steps.ghcr.outputs.image }}:${{ steps.version.outputs.versioned }}-${{ matrix.arch }}
cache-from: type=gha
cache-to: type=gha,mode=max

- name: Output digest
run: |
echo "### Dev image (${{ matrix.arch }})" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
echo "${{ steps.ghcr.outputs.image }}:dev-${{ matrix.arch }}" >> $GITHUB_STEP_SUMMARY
echo "${{ steps.build.outputs.digest }}" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY

create_manifests:
name: Create multi-arch manifest
needs: [build_single_arch]
runs-on: ubuntu-latest
permissions:
packages: write
contents: read
steps:
- name: Check out (shallow)
uses: actions/checkout@v4
with:
fetch-depth: 1

- name: Resolve GHCR image name
id: ghcr
run: |
OWNER_LC=$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]')
echo "image=ghcr.io/${OWNER_LC}/new-api" >> $GITHUB_OUTPUT

- name: Determine dev version
id: version
run: |
SHORT_SHA=$(git rev-parse --short HEAD)
echo "versioned=dev-${SHORT_SHA}" >> $GITHUB_OUTPUT

- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Create & push manifest (dev)
run: |
docker buildx imagetools create \
-t ${{ steps.ghcr.outputs.image }}:dev \
${{ steps.ghcr.outputs.image }}:dev-amd64 \
${{ steps.ghcr.outputs.image }}:dev-arm64

- name: Create & push manifest (versioned)
run: |
docker buildx imagetools create \
-t ${{ steps.ghcr.outputs.image }}:${{ steps.version.outputs.versioned }} \
${{ steps.ghcr.outputs.image }}:${{ steps.version.outputs.versioned }}-amd64 \
${{ steps.ghcr.outputs.image }}:${{ steps.version.outputs.versioned }}-arm64

- name: Output manifest digest
run: |
echo "### Multi-arch Manifest (dev)" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
docker buildx imagetools inspect ${{ steps.ghcr.outputs.image }}:dev >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
5 changes: 5 additions & 0 deletions constant/context_key.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const (
ContextKeyTokenModelLimitEnabled ContextKey = "token_model_limit_enabled"
ContextKeyTokenModelLimit ContextKey = "token_model_limit"
ContextKeyTokenCrossGroupRetry ContextKey = "token_cross_group_retry"
ContextKeyTokenModelRouteNotify ContextKey = "token_model_route_notify"

/* channel related keys */
ContextKeyChannelId ContextKey = "channel_id"
Expand All @@ -42,6 +43,10 @@ const (
ContextKeyAutoGroupIndex ContextKey = "auto_group_index"
ContextKeyAutoGroupRetryIndex ContextKey = "auto_group_retry_index"

/* image-aware routing keys */
ContextKeyImageAwareEntryModel ContextKey = "image_aware_entry_model"
ContextKeyImageAwareHasImage ContextKey = "image_aware_has_image"

/* user related keys */
ContextKeyUserId ContextKey = "id"
ContextKeyUserSetting ContextKey = "user_setting"
Expand Down
2 changes: 2 additions & 0 deletions controller/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,7 @@ func AddToken(c *gin.Context) {
AllowIps: token.AllowIps,
Group: token.Group,
CrossGroupRetry: token.CrossGroupRetry,
ModelRouteNotify: token.ModelRouteNotify,
}
err = cleanToken.Insert()
if err != nil {
Expand Down Expand Up @@ -299,6 +300,7 @@ func UpdateToken(c *gin.Context) {
cleanToken.AllowIps = token.AllowIps
cleanToken.Group = token.Group
cleanToken.CrossGroupRetry = token.CrossGroupRetry
cleanToken.ModelRouteNotify = token.ModelRouteNotify
}
err = cleanToken.Update()
if err != nil {
Expand Down
71 changes: 71 additions & 0 deletions docker-compose.deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Deployment compose - pulls the dev image published to the GitHub Container
# Registry by CI (.github/workflows/docker-image-dev.yml). The image is built
# from Dockerfile.dev (backend-only, frontend served by the image placeholder).
#
# If the GHCR package is private, log in first:
# echo $GITHUB_TOKEN | docker login ghcr.io -u <owner> --password-stdin
#
# Usage:
# 1. docker compose -f docker-compose.deploy.yml up -d
# 2. Open http://localhost:3000
#
# Stop:
# docker compose -f docker-compose.deploy.yml down
#
# Reset data:
# docker compose -f docker-compose.deploy.yml down -v

services:
new-api:
image: ghcr.io/gentle-lijie/new-api:dev
container_name: new-api-deploy
restart: unless-stopped
ports:
- "3000:3000"
volumes:
- deploy_data:/data
environment:
- SQL_DSN=postgresql://root:123456@postgres:5432/new-api
- REDIS_CONN_STRING=redis://redis
- TZ=Asia/Shanghai
- BATCH_UPDATE_ENABLED=true
depends_on:
redis:
condition: service_started
postgres:
condition: service_healthy
networks:
- deploy-network

redis:
image: redis:7-alpine
container_name: new-api-deploy-redis
restart: unless-stopped
networks:
- deploy-network

postgres:
image: postgres:15-alpine
container_name: new-api-deploy-pg
restart: unless-stopped
environment:
POSTGRES_USER: root
POSTGRES_PASSWORD: 123456
POSTGRES_DB: new-api
volumes:
- deploy_pg_data:/var/lib/postgresql/data
networks:
- deploy-network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U root -d new-api"]
interval: 5s
timeout: 3s
retries: 5

volumes:
deploy_data:
deploy_pg_data:

networks:
deploy-network:
driver: bridge
46 changes: 46 additions & 0 deletions docs/PR-description.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
> [!IMPORTANT]
> 本 PR 由 AI 辅助生成(git user `GentleLijie` 非历史核心开发者),已人工整理描述如下。

## 📝 变更描述 / Description

新增「图片感知模型路由」:配置一个**虚拟入口模型名**(如 `auto-coder`),网关在 distributor 选渠道之前,解析请求体检测**最后一条 `role=user` 消息**是否含图片(同时支持 OpenAI `image_url` 与 Claude `image` 两种 content part),据此把模型名改写为配置好的**视觉模型**或**编程模型**。改写发生在渠道选择之前,因此真实模型名会参与渠道选择、亲和性、计费与重试。

由于网关每个请求无状态,“图片轮走视觉模型、后续纯文本轮回到编程模型”由客户端每轮携带的完整对话历史天然完成,网关无需存任何状态——仅看当前轮最后一条 user 消息,避免历史残留图片误触发。

可观测性:
- 响应头注入 `X-Routed-Model` / `X-Route-Entry-Model` / `X-Route-Reason`
- Token 级 `ModelRouteNotify` 开关(默认对新 token 开启)控制是否在**响应体内**注入提示文本(如 `> [Route: auto-coder → glm-4.6v (image detected)]`),覆盖 OpenAI/Claude 客户端格式 × 流式/非流式
- 日志 `Log.Other` 写入 `image_aware_entry_model`,用量日志 Model 列显示相机图标 + 入口模型 Popover

管理后台提供向导式 Drawer 配置路由规则(入口模型 + 视觉/编程模型下拉选择)。

## 🚀 变更类型 / Type of change
- [x] ✨ 新功能 (New feature)

## 🔗 关联任务 / Related Issue
- 无对应 Issue

## ✅ 提交前检查项 / Checklist
- [x] **非重复提交:** 已确认无重复 PR
- [x] **变更理解:** 见上方描述
- [x] **范围聚焦:** 排除了无关的 `__root.tsx`(devtools 注释)与 `pnpm-lock.yaml`(npm 误生成)
- [x] **本地验证:** 后端 `go build ./...` 通过;`go test ./middleware/`(图片检测表驱动单测 12 例)通过;前端 `tsc -b` 涉及文件无类型错误
- [x] **安全合规:** 无敏感凭据;JSON 统一走 `common.*`;配置仅写 options 表字符串,三库兼容

> 注:checklist 中「人工确认」项因本 PR 为 AI 辅助生成,未勾选,已在此如实标注。

## 📸 运行证明 / Proof of Work

带图请求被正确路由到视觉模型,纯文本请求回到编程模型(`record consume log` 的 `model_name` 字段):
```text
model_name=glm-4.6v prompt_tokens=41355 image_aware_entry_model=auto-coder // 含图 → 视觉模型
model_name=glm-5 prompt_tokens=79 image_aware_entry_model=auto-coder // 纯文本 → 编程模型
model_name=glm-5 prompt_tokens=41188 image_aware_entry_model=auto-coder // 后续纯文本轮,仍回编程模型
```

路由决策日志(distributor,每请求一次,不刷屏):
```text
image_aware_routing: entry=auto-coder has_image=true -> routed=glm-4.6v notify=true
```

Token 开启 `ModelRouteNotify` 后,响应流首个内容 delta 前置提示文本,客户端可见 `> [Route: auto-coder → glm-4.6v (image detected)]`。
Loading