mirror of
https://github.com/alibaba/higress.git
synced 2026-03-19 09:47:25 +08:00
Compare commits
117 Commits
wasm-go-ai
...
rust_wasm_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
102596360a | ||
|
|
7354d80263 | ||
|
|
3646502248 | ||
|
|
8be591d052 | ||
|
|
45fdd95a9c | ||
|
|
d3afe345ad | ||
|
|
90ca903d2e | ||
|
|
2d8a8f26da | ||
|
|
9ea2410388 | ||
|
|
9e1792c245 | ||
|
|
3eda7def89 | ||
|
|
1787553294 | ||
|
|
f6c48415d1 | ||
|
|
e27d3d0971 | ||
|
|
49617c7a98 | ||
|
|
53a015d8fe | ||
|
|
e711e9f997 | ||
|
|
8530742472 | ||
|
|
c0c1f5113a | ||
|
|
2e6ddd7e35 | ||
|
|
2328e19c9d | ||
|
|
fabc22f218 | ||
|
|
2986e1911d | ||
|
|
a566f7257d | ||
|
|
3dbd1b2731 | ||
|
|
7f23980bf5 | ||
|
|
6fb0684c39 | ||
|
|
dfac9fa5e6 | ||
|
|
bfd9e3026d | ||
|
|
49aad4152c | ||
|
|
94aacf5153 | ||
|
|
efcfdbf36e | ||
|
|
2dbde1833f | ||
|
|
7272eff8b6 | ||
|
|
a84a382f1d | ||
|
|
477e44b9f1 | ||
|
|
512385d225 | ||
|
|
b997e6fd26 | ||
|
|
fab3ebb35a | ||
|
|
1431ff9cfe | ||
|
|
fac2c3e7a3 | ||
|
|
574d1aa36a | ||
|
|
7840167c4a | ||
|
|
9d8e78dae3 | ||
|
|
133a30b8d5 | ||
|
|
ce94c6e62d | ||
|
|
05f251e627 | ||
|
|
0259eaddbb | ||
|
|
cfa3baddf8 | ||
|
|
b1f625a652 | ||
|
|
fd1eb54f25 | ||
|
|
c7550e2d49 | ||
|
|
ba74f4bbb9 | ||
|
|
9e418dafd9 | ||
|
|
95523a1bc7 | ||
|
|
dcd8466127 | ||
|
|
cceae6ad2a | ||
|
|
32f9a5ff32 | ||
|
|
6f95297b80 | ||
|
|
95426d5ccf | ||
|
|
a05b6b1e9d | ||
|
|
d0628344da | ||
|
|
a1bf315b13 | ||
|
|
b3d9123d59 | ||
|
|
817061c6cc | ||
|
|
ea0d5e7564 | ||
|
|
2a89c3bb70 | ||
|
|
a570c72504 | ||
|
|
ab1316dfe1 | ||
|
|
e97448b71b | ||
|
|
6820a06a99 | ||
|
|
4733af849d | ||
|
|
1c2330e33b | ||
|
|
61fef0ecf8 | ||
|
|
d29b8d7ca8 | ||
|
|
2501895b66 | ||
|
|
187a7b5408 | ||
|
|
00be491d02 | ||
|
|
2d74c48e8a | ||
|
|
6dc4d43df5 | ||
|
|
2a4e55d46f | ||
|
|
579c986915 | ||
|
|
380717ae3d | ||
|
|
8f3723f554 | ||
|
|
909cc0f088 | ||
|
|
4eaf204737 | ||
|
|
748bcb083a | ||
|
|
39c007d045 | ||
|
|
d74d327b68 | ||
|
|
be27726721 | ||
|
|
34cc1c0632 | ||
|
|
5694475872 | ||
|
|
2f5709a93e | ||
|
|
2a200cdd42 | ||
|
|
ec39d56731 | ||
|
|
8544fa604d | ||
|
|
0ba63e5dd4 | ||
|
|
441408c593 | ||
|
|
be57960c22 | ||
|
|
f32020068a | ||
|
|
1a8fce48f0 | ||
|
|
85c7b1f501 | ||
|
|
8f660211e3 | ||
|
|
433227323d | ||
|
|
b36e5ea26b | ||
|
|
ce66ff68ce | ||
|
|
d026f0fca5 | ||
|
|
22790aa149 | ||
|
|
7ce6d7aba1 | ||
|
|
e705a0344f | ||
|
|
d6094974c2 | ||
|
|
6187be97e5 | ||
|
|
bb64b43f23 | ||
|
|
ca7458cf1c | ||
|
|
ee2dd76ae1 | ||
|
|
8154cf95f1 | ||
|
|
a7593381e1 |
@@ -133,8 +133,13 @@ jobs:
|
||||
command="
|
||||
set -e
|
||||
cd /workspace/plugins/wasm-rust/extensions/${PLUGIN_NAME}
|
||||
cargo build --target wasm32-wasi --release
|
||||
cp target/wasm32-wasi/release/*.wasm plugin.wasm
|
||||
if [ -f ./.prebuild ]; then
|
||||
echo 'Found .prebuild file, sourcing it...'
|
||||
. ./.prebuild
|
||||
fi
|
||||
rustup target add wasm32-wasip1
|
||||
cargo build --target wasm32-wasip1 --release
|
||||
cp target/wasm32-wasip1/release/*.wasm plugin.wasm
|
||||
tar czvf plugin.tar.gz plugin.wasm
|
||||
echo ${{ secrets.REGISTRY_PASSWORD }} | oras login -u ${{ secrets.REGISTRY_USERNAME }} --password-stdin ${{ env.IMAGE_REGISTRY_SERVICE }}
|
||||
oras push ${target_image} ${push_command}
|
||||
|
||||
4
.github/workflows/build-and-test-plugin.yaml
vendored
4
.github/workflows/build-and-test-plugin.yaml
vendored
@@ -6,11 +6,15 @@ on:
|
||||
paths:
|
||||
- 'plugins/**'
|
||||
- 'test/**'
|
||||
- 'helm/**'
|
||||
- 'Makefile.core.mk'
|
||||
pull_request:
|
||||
branches: [ "*" ]
|
||||
paths:
|
||||
- 'plugins/**'
|
||||
- 'test/**'
|
||||
- 'helm/**'
|
||||
- 'Makefile.core.mk'
|
||||
workflow_dispatch: ~
|
||||
|
||||
jobs:
|
||||
|
||||
487
.github/workflows/build-image-and-push.yaml
vendored
487
.github/workflows/build-image-and-push.yaml
vendored
@@ -1,229 +1,258 @@
|
||||
name: Build Docker Images and Push to Image Registry
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*.*.*"
|
||||
workflow_dispatch: ~
|
||||
|
||||
jobs:
|
||||
build-controller-image:
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: image-registry-controller
|
||||
env:
|
||||
CONTROLLER_IMAGE_REGISTRY: ${{ vars.IMAGE_REGISTRY || 'higress-registry.cn-hangzhou.cr.aliyuncs.com' }}
|
||||
CONTROLLER_IMAGE_NAME: ${{ vars.CONTROLLER_IMAGE_NAME || 'higress/higress' }}
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }}"
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Free Up GitHub Actions Ubuntu Runner Disk Space 🔧
|
||||
uses: jlumbroso/free-disk-space@main
|
||||
with:
|
||||
tool-cache: false
|
||||
android: true
|
||||
dotnet: true
|
||||
haskell: true
|
||||
large-packages: true
|
||||
swap-storage: true
|
||||
|
||||
- name: "Setup Go"
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.21.5
|
||||
|
||||
- name: Setup Golang Caches
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |-
|
||||
~/.cache/go-build
|
||||
~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ github.run_id }}
|
||||
restore-keys: ${{ runner.os }}-go
|
||||
|
||||
- name: Calculate Docker metadata
|
||||
id: docker-meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
${{ env.CONTROLLER_IMAGE_REGISTRY }}/${{ env.CONTROLLER_IMAGE_NAME }}
|
||||
tags: |
|
||||
type=sha
|
||||
type=ref,event=tag
|
||||
type=semver,pattern={{version}}
|
||||
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'main') }}
|
||||
|
||||
- name: Login to Docker Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.CONTROLLER_IMAGE_REGISTRY }}
|
||||
username: ${{ secrets.REGISTRY_USERNAME }}
|
||||
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
|
||||
- name: Build Docker Image and Push
|
||||
run: |
|
||||
GOPROXY="https://proxy.golang.org,direct" make docker-buildx-push
|
||||
BUILT_IMAGE="higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/higress"
|
||||
readarray -t IMAGES <<< "${{ steps.docker-meta.outputs.tags }}"
|
||||
for image in ${IMAGES[@]}; do
|
||||
echo "Image: $image"
|
||||
docker buildx imagetools create $BUILT_IMAGE:$GITHUB_SHA --tag $image
|
||||
done
|
||||
|
||||
build-pilot-image:
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: image-registry-pilot
|
||||
env:
|
||||
PILOT_IMAGE_REGISTRY: ${{ vars.IMAGE_REGISTRY || 'higress-registry.cn-hangzhou.cr.aliyuncs.com' }}
|
||||
PILOT_IMAGE_NAME: ${{ vars.PILOT_IMAGE_NAME || 'higress/pilot' }}
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }}"
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Free Up GitHub Actions Ubuntu Runner Disk Space 🔧
|
||||
uses: jlumbroso/free-disk-space@main
|
||||
with:
|
||||
tool-cache: false
|
||||
android: true
|
||||
dotnet: true
|
||||
haskell: true
|
||||
large-packages: true
|
||||
swap-storage: true
|
||||
|
||||
- name: "Setup Go"
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.21.5
|
||||
|
||||
- name: Setup Golang Caches
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |-
|
||||
~/.cache/go-build
|
||||
~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ github.run_id }}
|
||||
restore-keys: ${{ runner.os }}-go
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
||||
- name: Cache Docker layers
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: /tmp/.buildx-cache
|
||||
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-buildx-
|
||||
|
||||
- name: Calculate Docker metadata
|
||||
id: docker-meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
${{ env.PILOT_IMAGE_REGISTRY }}/${{ env.PILOT_IMAGE_NAME }}
|
||||
tags: |
|
||||
type=sha
|
||||
type=ref,event=tag
|
||||
type=semver,pattern={{version}}
|
||||
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'main') }}
|
||||
|
||||
- name: Login to Docker Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.PILOT_IMAGE_REGISTRY }}
|
||||
username: ${{ secrets.REGISTRY_USERNAME }}
|
||||
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
|
||||
- name: Build Pilot-Discovery Image and Push
|
||||
run: |
|
||||
GOPROXY="https://proxy.golang.org,direct" make build-istio
|
||||
BUILT_IMAGE="higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/pilot"
|
||||
readarray -t IMAGES <<< "${{ steps.docker-meta.outputs.tags }}"
|
||||
for image in ${IMAGES[@]}; do
|
||||
echo "Image: $image"
|
||||
docker buildx imagetools create $BUILT_IMAGE:$GITHUB_SHA --tag $image
|
||||
done
|
||||
|
||||
|
||||
build-gateway-image:
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: image-registry-pilot
|
||||
env:
|
||||
GATEWAY_IMAGE_REGISTRY: ${{ vars.IMAGE_REGISTRY || 'higress-registry.cn-hangzhou.cr.aliyuncs.com' }}
|
||||
GATEWAY_IMAGE_NAME: ${{ vars.GATEWAY_IMAGE_NAME || 'higress/gateway' }}
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }}"
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Free Up GitHub Actions Ubuntu Runner Disk Space 🔧
|
||||
uses: jlumbroso/free-disk-space@main
|
||||
with:
|
||||
tool-cache: false
|
||||
android: true
|
||||
dotnet: true
|
||||
haskell: true
|
||||
large-packages: true
|
||||
swap-storage: true
|
||||
|
||||
- name: "Setup Go"
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.21.5
|
||||
|
||||
- name: Setup Golang Caches
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |-
|
||||
~/.cache/go-build
|
||||
~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ github.run_id }}
|
||||
restore-keys: ${{ runner.os }}-go
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
||||
- name: Cache Docker layers
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: /tmp/.buildx-cache
|
||||
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-buildx-
|
||||
|
||||
- name: Calculate Docker metadata
|
||||
id: docker-meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
${{ env.GATEWAY_IMAGE_REGISTRY }}/${{ env.GATEWAY_IMAGE_NAME }}
|
||||
tags: |
|
||||
type=sha
|
||||
type=ref,event=tag
|
||||
type=semver,pattern={{version}}
|
||||
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'main') }}
|
||||
|
||||
- name: Login to Docker Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.GATEWAY_IMAGE_REGISTRY }}
|
||||
username: ${{ secrets.REGISTRY_USERNAME }}
|
||||
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
|
||||
- name: Build Gateway Image and Push
|
||||
run: |
|
||||
GOPROXY="https://proxy.golang.org,direct" make build-gateway
|
||||
BUILT_IMAGE="higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/proxyv2"
|
||||
readarray -t IMAGES <<< "${{ steps.docker-meta.outputs.tags }}"
|
||||
for image in ${IMAGES[@]}; do
|
||||
echo "Image: $image"
|
||||
docker buildx imagetools create $BUILT_IMAGE:$GITHUB_SHA --tag $image
|
||||
done
|
||||
name: Build Docker Images and Push to Image Registry
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*.*.*"
|
||||
workflow_dispatch: ~
|
||||
|
||||
jobs:
|
||||
build-controller-image:
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: image-registry-controller
|
||||
env:
|
||||
CONTROLLER_IMAGE_REGISTRY: ${{ vars.IMAGE_REGISTRY || 'higress-registry.cn-hangzhou.cr.aliyuncs.com' }}
|
||||
CONTROLLER_IMAGE_NAME: ${{ vars.CONTROLLER_IMAGE_NAME || 'higress/higress' }}
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }}"
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Free Up GitHub Actions Ubuntu Runner Disk Space 🔧
|
||||
uses: jlumbroso/free-disk-space@main
|
||||
with:
|
||||
tool-cache: false
|
||||
android: true
|
||||
dotnet: true
|
||||
haskell: true
|
||||
large-packages: true
|
||||
swap-storage: true
|
||||
|
||||
- name: "Setup Go"
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.21.5
|
||||
|
||||
- name: Setup Golang Caches
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |-
|
||||
~/.cache/go-build
|
||||
~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ github.run_id }}
|
||||
restore-keys: ${{ runner.os }}-go
|
||||
|
||||
- name: Calculate Docker metadata
|
||||
id: docker-meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
${{ env.CONTROLLER_IMAGE_REGISTRY }}/${{ env.CONTROLLER_IMAGE_NAME }}
|
||||
tags: |
|
||||
type=sha
|
||||
type=ref,event=tag
|
||||
type=semver,pattern={{version}}
|
||||
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'main') }}
|
||||
|
||||
- name: Login to Docker Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.CONTROLLER_IMAGE_REGISTRY }}
|
||||
username: ${{ secrets.REGISTRY_USERNAME }}
|
||||
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
|
||||
- name: Build Docker Image and Push
|
||||
run: |
|
||||
BUILT_IMAGE=""
|
||||
readarray -t IMAGES <<< "${{ steps.docker-meta.outputs.tags }}"
|
||||
for image in ${IMAGES[@]}; do
|
||||
echo "Image: $image"
|
||||
if [ "$BUILT_IMAGE" == "" ]; then
|
||||
GOPROXY="https://proxy.golang.org,direct" IMG_URL="$image" make docker-buildx-push
|
||||
BUILT_IMAGE="$image"
|
||||
else
|
||||
docker buildx imagetools create $BUILT_IMAGE --tag $image
|
||||
fi
|
||||
done
|
||||
|
||||
build-pilot-image:
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: image-registry-pilot
|
||||
env:
|
||||
PILOT_IMAGE_REGISTRY: ${{ vars.IMAGE_REGISTRY || 'higress-registry.cn-hangzhou.cr.aliyuncs.com' }}
|
||||
PILOT_IMAGE_NAME: ${{ vars.PILOT_IMAGE_NAME || 'higress/pilot' }}
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }}"
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Free Up GitHub Actions Ubuntu Runner Disk Space 🔧
|
||||
uses: jlumbroso/free-disk-space@main
|
||||
with:
|
||||
tool-cache: false
|
||||
android: true
|
||||
dotnet: true
|
||||
haskell: true
|
||||
large-packages: true
|
||||
swap-storage: true
|
||||
|
||||
- name: "Setup Go"
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.21.5
|
||||
|
||||
- name: Setup Golang Caches
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |-
|
||||
~/.cache/go-build
|
||||
~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ github.run_id }}
|
||||
restore-keys: ${{ runner.os }}-go
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
with:
|
||||
image: tonistiigi/binfmt:qemu-v7.0.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Cache Docker layers
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: /tmp/.buildx-cache
|
||||
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-buildx-
|
||||
|
||||
- name: Calculate Docker metadata
|
||||
id: docker-meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
${{ env.PILOT_IMAGE_REGISTRY }}/${{ env.PILOT_IMAGE_NAME }}
|
||||
tags: |
|
||||
type=sha
|
||||
type=ref,event=tag
|
||||
type=semver,pattern={{version}}
|
||||
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'main') }}
|
||||
|
||||
- name: Login to Docker Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.PILOT_IMAGE_REGISTRY }}
|
||||
username: ${{ secrets.REGISTRY_USERNAME }}
|
||||
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
|
||||
- name: Build Pilot-Discovery Image and Push
|
||||
run: |
|
||||
BUILT_IMAGE=""
|
||||
readarray -t IMAGES <<< "${{ steps.docker-meta.outputs.tags }}"
|
||||
for image in ${IMAGES[@]}; do
|
||||
echo "Image: $image"
|
||||
if [ "$BUILT_IMAGE" == "" ]; then
|
||||
TAG=${image#*:}
|
||||
HUB=${image%:*}
|
||||
HUB=${HUB%/*}
|
||||
BUILT_IMAGE="$HUB/pilot:$TAG"
|
||||
GOPROXY="https://proxy.golang.org,direct" IMG_URL="$BUILT_IMAGE" make build-istio
|
||||
fi
|
||||
if [ "$BUILT_IMAGE" != "$image" ]; then
|
||||
docker buildx imagetools create $BUILT_IMAGE --tag $image
|
||||
fi
|
||||
done
|
||||
|
||||
build-gateway-image:
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: image-registry-gateway
|
||||
env:
|
||||
GATEWAY_IMAGE_REGISTRY: ${{ vars.IMAGE_REGISTRY || 'higress-registry.cn-hangzhou.cr.aliyuncs.com' }}
|
||||
GATEWAY_IMAGE_NAME: ${{ vars.GATEWAY_IMAGE_NAME || 'higress/gateway' }}
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }}"
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Free Up GitHub Actions Ubuntu Runner Disk Space 🔧
|
||||
uses: jlumbroso/free-disk-space@main
|
||||
with:
|
||||
tool-cache: false
|
||||
android: true
|
||||
dotnet: true
|
||||
haskell: true
|
||||
large-packages: true
|
||||
swap-storage: true
|
||||
|
||||
- name: "Setup Go"
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.21.5
|
||||
|
||||
- name: Setup Golang Caches
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |-
|
||||
~/.cache/go-build
|
||||
~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ github.run_id }}
|
||||
restore-keys: ${{ runner.os }}-go
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
with:
|
||||
image: tonistiigi/binfmt:qemu-v7.0.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Cache Docker layers
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: /tmp/.buildx-cache
|
||||
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-buildx-
|
||||
|
||||
- name: Calculate Docker metadata
|
||||
id: docker-meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
${{ env.GATEWAY_IMAGE_REGISTRY }}/${{ env.GATEWAY_IMAGE_NAME }}
|
||||
tags: |
|
||||
type=sha
|
||||
type=ref,event=tag
|
||||
type=semver,pattern={{version}}
|
||||
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'main') }}
|
||||
|
||||
- name: Login to Docker Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.GATEWAY_IMAGE_REGISTRY }}
|
||||
username: ${{ secrets.REGISTRY_USERNAME }}
|
||||
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
|
||||
- name: Build Gateway Image and Push
|
||||
run: |
|
||||
BUILT_IMAGE=""
|
||||
readarray -t IMAGES <<< "${{ steps.docker-meta.outputs.tags }}"
|
||||
for image in ${IMAGES[@]}; do
|
||||
echo "Image: $image"
|
||||
if [ "$BUILT_IMAGE" == "" ]; then
|
||||
TAG=${image#*:}
|
||||
HUB=${image%:*}
|
||||
HUB=${HUB%/*}
|
||||
BUILT_IMAGE="$HUB/proxyv2:$TAG"
|
||||
GOPROXY="https://proxy.golang.org,direct" IMG_URL="$BUILT_IMAGE" make build-gateway
|
||||
fi
|
||||
if [ "$BUILT_IMAGE" != "$image" ]; then
|
||||
docker buildx imagetools create $BUILT_IMAGE --tag $image
|
||||
fi
|
||||
done
|
||||
2
.github/workflows/deploy-to-oss.yaml
vendored
2
.github/workflows/deploy-to-oss.yaml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
- name: Download Helm Charts Index
|
||||
uses: doggycool/ossutil-github-action@master
|
||||
with:
|
||||
ossArgs: 'cp -r -u oss://higress-website-cn-hongkong/helm-charts/index.yaml ./artifact/'
|
||||
ossArgs: 'cp oss://higress-website-cn-hongkong/helm-charts/index.yaml ./artifact/'
|
||||
accessKey: ${{ secrets.ACCESS_KEYID }}
|
||||
accessSecret: ${{ secrets.ACCESS_KEYSECRET }}
|
||||
endpoint: oss-cn-hongkong.aliyuncs.com
|
||||
|
||||
85
.github/workflows/helm-docs.yaml
vendored
85
.github/workflows/helm-docs.yaml
vendored
@@ -4,11 +4,15 @@ on:
|
||||
pull_request:
|
||||
branches:
|
||||
- "*"
|
||||
|
||||
paths:
|
||||
- 'helm/**'
|
||||
workflow_dispatch: ~
|
||||
push:
|
||||
branches: [ main ]
|
||||
paths:
|
||||
- 'helm/**'
|
||||
|
||||
jobs:
|
||||
|
||||
helm:
|
||||
name: Helm Docs
|
||||
runs-on: ubuntu-latest
|
||||
@@ -32,4 +36,79 @@ jobs:
|
||||
echo "Please use helm-docs in your clone, of your fork, of the project, and commit a updated README.md for the chart."
|
||||
fi
|
||||
git diff --exit-code
|
||||
rm -f ./helm-docs
|
||||
rm -f ./helm-docs
|
||||
|
||||
translate-readme:
|
||||
needs: helm
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y jq
|
||||
|
||||
- name: Translate README.md to Chinese
|
||||
env:
|
||||
API_URL: ${{ secrets.HIGRESS_OPENAI_API_URL }}
|
||||
API_KEY: ${{ secrets.HIGRESS_OPENAI_API_KEY }}
|
||||
API_MODEL: ${{ secrets.HIGRESS_OPENAI_API_MODEL }}
|
||||
run: |
|
||||
cd ./helm/higress
|
||||
FILE_CONTENT=$(cat README.md)
|
||||
|
||||
PAYLOAD=$(jq -n \
|
||||
--arg model "$API_MODEL" \
|
||||
--arg content "$FILE_CONTENT" \
|
||||
'{
|
||||
model: $model,
|
||||
messages: [
|
||||
{"role": "system", "content": "You are a translation assistant that translates English Markdown text to Chinese."},
|
||||
{"role": "user", "content": $content}
|
||||
],
|
||||
temperature: 1.1,
|
||||
stream: false
|
||||
}')
|
||||
|
||||
RESPONSE=$(curl -s -X POST "$API_URL" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $API_KEY" \
|
||||
-d "$PAYLOAD")
|
||||
|
||||
echo "response: $RESPONSE"
|
||||
|
||||
TRANSLATED_CONTENT=$(echo "$RESPONSE" | jq -r '.choices[0].message.content')
|
||||
|
||||
if [ -z "$TRANSLATED_CONTENT" ]; then
|
||||
echo "Translation failed! Response: $RESPONSE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "$TRANSLATED_CONTENT" > README.zh.new.md
|
||||
echo "Translation completed and saved to README.zh.new.md."
|
||||
|
||||
- name: Compare README.zh.md
|
||||
id: compare
|
||||
run: |
|
||||
cd ./helm/higress
|
||||
NEW_README_ZH="README.zh.new.md"
|
||||
EXISTING_README_ZH="README.zh.md"
|
||||
|
||||
if [ ! -f "$EXISTING_README_ZH" ]; then
|
||||
echo "Add README.zh.md."
|
||||
mv "$NEW_README_ZH" "$EXISTING_README_ZH"
|
||||
echo "updated=true" >> $GITHUB_ENV
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ! diff -q "$NEW_README_ZH" "$EXISTING_README_ZH"; then
|
||||
echo "Files are different. Updating README.zh.md."
|
||||
mv "$NEW_README_ZH" "$EXISTING_README_ZH"
|
||||
echo "updated=true" >> $GITHUB_ENV
|
||||
else
|
||||
echo "Files are identical. No update needed."
|
||||
echo "updated=false" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
2
.github/workflows/release-hgctl.yaml
vendored
2
.github/workflows/release-hgctl.yaml
vendored
@@ -58,7 +58,7 @@ jobs:
|
||||
hgctl_${{ env.HGCTL_VERSION }}_darwin_arm64.tar.gz
|
||||
|
||||
release-hgctl-macos-amd64:
|
||||
runs-on: macos-12
|
||||
runs-on: macos-14
|
||||
env:
|
||||
HGCTL_VERSION: ${{github.ref_name}}
|
||||
steps:
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -17,4 +17,3 @@ target/
|
||||
tools/hack/cluster.conf
|
||||
envoy/1.20
|
||||
istio/1.12
|
||||
Cargo.lock
|
||||
|
||||
@@ -12,6 +12,7 @@ header:
|
||||
- 'LICENSE'
|
||||
- 'api/**'
|
||||
- 'samples/**'
|
||||
- 'docs/**'
|
||||
- '.github/**'
|
||||
- '.licenserc.yaml'
|
||||
- 'helm/**'
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
/envoy @gengleilei @johnlanni
|
||||
/istio @SpecialYang @johnlanni
|
||||
/pkg @SpecialYang @johnlanni @CH3CHO
|
||||
/plugins @johnlanni @WeixinX @CH3CHO
|
||||
/plugins @johnlanni @CH3CHO @rinfx
|
||||
/plugins/wasm-go/extensions/ai-proxy @cr7258 @CH3CHO @rinfx
|
||||
/plugins/wasm-rust @007gzs @jizhuozhi
|
||||
/registry @NameHaibinZhang @2456868764 @johnlanni
|
||||
/test @Xunzhuo @2456868764 @CH3CHO
|
||||
|
||||
@@ -144,7 +144,7 @@ docker-buildx-push: clean-env docker.higress-buildx
|
||||
export PARENT_GIT_TAG:=$(shell cat VERSION)
|
||||
export PARENT_GIT_REVISION:=$(TAG)
|
||||
|
||||
export ENVOY_PACKAGE_URL_PATTERN?=https://github.com/higress-group/proxy/releases/download/v2.0.0/envoy-symbol-ARCH.tar.gz
|
||||
export ENVOY_PACKAGE_URL_PATTERN?=https://github.com/higress-group/proxy/releases/download/v2.1.1/envoy-symbol-ARCH.tar.gz
|
||||
|
||||
build-envoy: prebuild
|
||||
./tools/hack/build-envoy.sh
|
||||
@@ -162,13 +162,13 @@ buildx-prepare:
|
||||
build-gateway: prebuild buildx-prepare
|
||||
USE_REAL_USER=1 TARGET_ARCH=amd64 DOCKER_TARGETS="docker.proxyv2" ./tools/hack/build-istio-image.sh init
|
||||
USE_REAL_USER=1 TARGET_ARCH=arm64 DOCKER_TARGETS="docker.proxyv2" ./tools/hack/build-istio-image.sh init
|
||||
DOCKER_TARGETS="docker.proxyv2" ./tools/hack/build-istio-image.sh docker.buildx
|
||||
DOCKER_TARGETS="docker.proxyv2" IMG_URL="${IMG_URL}" ./tools/hack/build-istio-image.sh docker.buildx
|
||||
|
||||
build-gateway-local: prebuild
|
||||
TARGET_ARCH=${TARGET_ARCH} DOCKER_TARGETS="docker.proxyv2" ./tools/hack/build-istio-image.sh docker
|
||||
|
||||
build-istio: prebuild buildx-prepare
|
||||
DOCKER_TARGETS="docker.pilot" ./tools/hack/build-istio-image.sh docker.buildx
|
||||
DOCKER_TARGETS="docker.pilot" IMG_URL="${IMG_URL}" ./tools/hack/build-istio-image.sh docker.buildx
|
||||
|
||||
build-istio-local: prebuild
|
||||
TARGET_ARCH=${TARGET_ARCH} DOCKER_TARGETS="docker.pilot" ./tools/hack/build-istio-image.sh docker
|
||||
@@ -187,8 +187,8 @@ install: pre-install
|
||||
cd helm/higress; helm dependency build
|
||||
helm install higress helm/higress -n higress-system --create-namespace --set 'global.local=true'
|
||||
|
||||
ENVOY_LATEST_IMAGE_TAG ?= 2.0.1
|
||||
ISTIO_LATEST_IMAGE_TAG ?= 2.0.1
|
||||
ENVOY_LATEST_IMAGE_TAG ?= 958467a353d411ae3f06e03b096bfd342cddb2c6
|
||||
ISTIO_LATEST_IMAGE_TAG ?= d9c728d3b01f64855e012b08d136e306f1160397
|
||||
|
||||
install-dev: pre-install
|
||||
helm install higress helm/core -n higress-system --create-namespace --set 'controller.tag=$(TAG)' --set 'gateway.replicas=1' --set 'pilot.tag=$(ISTIO_LATEST_IMAGE_TAG)' --set 'gateway.tag=$(ENVOY_LATEST_IMAGE_TAG)' --set 'global.local=true'
|
||||
@@ -299,7 +299,7 @@ kube-load-image: $(tools/kind) ## Install the Higress image to a kind cluster us
|
||||
tools/hack/docker-pull-image.sh higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/echo-server 1.3.0
|
||||
tools/hack/docker-pull-image.sh higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/echo-server v1.0
|
||||
tools/hack/docker-pull-image.sh higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/echo-body 1.0.0
|
||||
tools/hack/docker-pull-image.sh openpolicyagent/opa latest
|
||||
tools/hack/docker-pull-image.sh openpolicyagent/opa 0.61.0
|
||||
tools/hack/docker-pull-image.sh curlimages/curl latest
|
||||
tools/hack/docker-pull-image.sh registry.cn-hangzhou.aliyuncs.com/2456868764/httpbin 1.0.2
|
||||
tools/hack/docker-pull-image.sh registry.cn-hangzhou.aliyuncs.com/hinsteny/nacos-standlone-rc3 1.0.0-RC3
|
||||
@@ -312,7 +312,7 @@ kube-load-image: $(tools/kind) ## Install the Higress image to a kind cluster us
|
||||
tools/hack/kind-load-image.sh higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/echo-server 1.3.0
|
||||
tools/hack/kind-load-image.sh higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/echo-server v1.0
|
||||
tools/hack/kind-load-image.sh higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/echo-body 1.0.0
|
||||
tools/hack/kind-load-image.sh openpolicyagent/opa latest
|
||||
tools/hack/kind-load-image.sh openpolicyagent/opa 0.61.0
|
||||
tools/hack/kind-load-image.sh curlimages/curl latest
|
||||
tools/hack/kind-load-image.sh registry.cn-hangzhou.aliyuncs.com/2456868764/httpbin 1.0.2
|
||||
tools/hack/kind-load-image.sh registry.cn-hangzhou.aliyuncs.com/hinsteny/nacos-standlone-rc3 1.0.0-RC3
|
||||
|
||||
10
README.md
10
README.md
@@ -6,9 +6,14 @@
|
||||
</h1>
|
||||
<h4 align="center"> AI Native API Gateway </h4>
|
||||
|
||||
<div align="center">
|
||||
|
||||
[](https://github.com/alibaba/higress/actions)
|
||||
[](https://www.apache.org/licenses/LICENSE-2.0.html)
|
||||
|
||||
<a href="https://trendshift.io/repositories/10918" target="_blank"><img src="https://trendshift.io/api/badge/repositories/10918" alt="alibaba%2Fhigress | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||
</div>
|
||||
|
||||
[**官网**](https://higress.cn/) |
|
||||
[**文档**](https://higress.cn/docs/latest/overview/what-is-higress/) |
|
||||
[**博客**](https://higress.cn/blog/) |
|
||||
@@ -17,6 +22,7 @@
|
||||
[**AI插件**](https://higress.cn/plugin/)
|
||||
|
||||
|
||||
|
||||
<p>
|
||||
<a href="README_EN.md"> English <a/>| 中文 | <a href="README_JP.md"> 日本語 <a/>
|
||||
</p>
|
||||
@@ -28,7 +34,7 @@ Higress 在阿里内部为解决 Tengine reload 对长连接业务有损,以
|
||||
|
||||
阿里云基于 Higress 构建了云原生 API 网关产品,为大量企业客户提供 99.99% 的网关高可用保障服务能力。
|
||||
|
||||
Higress 基于 AI 网关能力,支撑了通义千问 APP、百炼大模型 API、机器学习 PAI 平台等 AI 业务。同时服务国内头部的 AIGC 企业(如零一万物),以及 AI 产品(如 FastGPT)
|
||||
Higress 的 AI 网关能力支持国内外所有[主流模型供应商](https://github.com/alibaba/higress/tree/main/plugins/wasm-go/extensions/ai-proxy/provider)和基于 vllm/ollama 等自建的 DeepSeek 模型;在阿里云内部支撑了通义千问 APP、百炼大模型 API、机器学习 PAI 平台等 AI 业务。同时服务国内头部的 AIGC 企业(如零一万物),以及 AI 产品(如 FastGPT)
|
||||
|
||||

|
||||
|
||||
@@ -180,7 +186,7 @@ K8s 下使用 Helm 部署等其他安装方式可以参考官网 [Quick Start
|
||||
|
||||
### 交流群
|
||||
|
||||

|
||||

|
||||
|
||||
### 技术分享
|
||||
|
||||
|
||||
@@ -341,7 +341,7 @@ type WasmPlugin struct {
|
||||
// Extended by Higress, matching rules take effect
|
||||
MatchRules []*MatchRule `protobuf:"bytes,102,rep,name=match_rules,json=matchRules,proto3" json:"match_rules,omitempty"`
|
||||
// disable the default config
|
||||
DefaultConfigDisable bool `protobuf:"varint,103,opt,name=default_config_disable,json=defaultConfigDisable,proto3" json:"default_config_disable,omitempty"`
|
||||
DefaultConfigDisable *wrappers.BoolValue `protobuf:"bytes,103,opt,name=default_config_disable,json=defaultConfigDisable,proto3" json:"default_config_disable,omitempty"`
|
||||
}
|
||||
|
||||
func (x *WasmPlugin) Reset() {
|
||||
@@ -467,11 +467,11 @@ func (x *WasmPlugin) GetMatchRules() []*MatchRule {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *WasmPlugin) GetDefaultConfigDisable() bool {
|
||||
func (x *WasmPlugin) GetDefaultConfigDisable() *wrappers.BoolValue {
|
||||
if x != nil {
|
||||
return x.DefaultConfigDisable
|
||||
}
|
||||
return false
|
||||
return nil
|
||||
}
|
||||
|
||||
// Extended by Higress
|
||||
@@ -480,11 +480,11 @@ type MatchRule struct {
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Ingress []string `protobuf:"bytes,1,rep,name=ingress,proto3" json:"ingress,omitempty"`
|
||||
Domain []string `protobuf:"bytes,2,rep,name=domain,proto3" json:"domain,omitempty"`
|
||||
Config *_struct.Struct `protobuf:"bytes,3,opt,name=config,proto3" json:"config,omitempty"`
|
||||
ConfigDisable bool `protobuf:"varint,4,opt,name=config_disable,json=configDisable,proto3" json:"config_disable,omitempty"`
|
||||
Service []string `protobuf:"bytes,5,rep,name=service,proto3" json:"service,omitempty"`
|
||||
Ingress []string `protobuf:"bytes,1,rep,name=ingress,proto3" json:"ingress,omitempty"`
|
||||
Domain []string `protobuf:"bytes,2,rep,name=domain,proto3" json:"domain,omitempty"`
|
||||
Config *_struct.Struct `protobuf:"bytes,3,opt,name=config,proto3" json:"config,omitempty"`
|
||||
ConfigDisable *wrappers.BoolValue `protobuf:"bytes,4,opt,name=config_disable,json=configDisable,proto3" json:"config_disable,omitempty"`
|
||||
Service []string `protobuf:"bytes,5,rep,name=service,proto3" json:"service,omitempty"`
|
||||
}
|
||||
|
||||
func (x *MatchRule) Reset() {
|
||||
@@ -540,11 +540,11 @@ func (x *MatchRule) GetConfig() *_struct.Struct {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *MatchRule) GetConfigDisable() bool {
|
||||
func (x *MatchRule) GetConfigDisable() *wrappers.BoolValue {
|
||||
if x != nil {
|
||||
return x.ConfigDisable
|
||||
}
|
||||
return false
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *MatchRule) GetService() []string {
|
||||
@@ -686,7 +686,7 @@ var file_extensions_v1alpha1_wasmplugin_proto_rawDesc = []byte{
|
||||
0x6f, 0x62, 0x75, 0x66, 0x2f, 0x77, 0x72, 0x61, 0x70, 0x70, 0x65, 0x72, 0x73, 0x2e, 0x70, 0x72,
|
||||
0x6f, 0x74, 0x6f, 0x1a, 0x1c, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74,
|
||||
0x6f, 0x62, 0x75, 0x66, 0x2f, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74,
|
||||
0x6f, 0x22, 0x8d, 0x06, 0x0a, 0x0a, 0x57, 0x61, 0x73, 0x6d, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e,
|
||||
0x6f, 0x22, 0xa9, 0x06, 0x0a, 0x0a, 0x57, 0x61, 0x73, 0x6d, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e,
|
||||
0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75,
|
||||
0x72, 0x6c, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x68, 0x61, 0x32, 0x35, 0x36, 0x18, 0x03, 0x20, 0x01,
|
||||
0x28, 0x09, 0x52, 0x06, 0x73, 0x68, 0x61, 0x32, 0x35, 0x36, 0x12, 0x53, 0x0a, 0x11, 0x69, 0x6d,
|
||||
@@ -731,52 +731,55 @@ var file_extensions_v1alpha1_wasmplugin_proto_rawDesc = []byte{
|
||||
0x73, 0x18, 0x66, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x68, 0x69, 0x67, 0x72, 0x65, 0x73,
|
||||
0x73, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x61,
|
||||
0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x52, 0x75, 0x6c, 0x65, 0x52,
|
||||
0x0a, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x12, 0x34, 0x0a, 0x16, 0x64,
|
||||
0x0a, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x12, 0x50, 0x0a, 0x16, 0x64,
|
||||
0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x5f, 0x64, 0x69,
|
||||
0x73, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x67, 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x64, 0x65, 0x66,
|
||||
0x61, 0x75, 0x6c, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x44, 0x69, 0x73, 0x61, 0x62, 0x6c,
|
||||
0x65, 0x22, 0xaf, 0x01, 0x0a, 0x09, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x52, 0x75, 0x6c, 0x65, 0x12,
|
||||
0x18, 0x0a, 0x07, 0x69, 0x6e, 0x67, 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09,
|
||||
0x52, 0x07, 0x69, 0x6e, 0x67, 0x72, 0x65, 0x73, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x6f, 0x6d,
|
||||
0x61, 0x69, 0x6e, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69,
|
||||
0x6e, 0x12, 0x2f, 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28,
|
||||
0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
|
||||
0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66,
|
||||
0x69, 0x67, 0x12, 0x25, 0x0a, 0x0e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x5f, 0x64, 0x69, 0x73,
|
||||
0x61, 0x62, 0x6c, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x63, 0x6f, 0x6e, 0x66,
|
||||
0x69, 0x67, 0x44, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x65, 0x72,
|
||||
0x76, 0x69, 0x63, 0x65, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x73, 0x65, 0x72, 0x76,
|
||||
0x69, 0x63, 0x65, 0x22, 0x41, 0x0a, 0x08, 0x56, 0x6d, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12,
|
||||
0x35, 0x0a, 0x03, 0x65, 0x6e, 0x76, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x68,
|
||||
0x69, 0x67, 0x72, 0x65, 0x73, 0x73, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e,
|
||||
0x73, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x45, 0x6e, 0x76, 0x56, 0x61,
|
||||
0x72, 0x52, 0x03, 0x65, 0x6e, 0x76, 0x22, 0x7e, 0x0a, 0x06, 0x45, 0x6e, 0x76, 0x56, 0x61, 0x72,
|
||||
0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04,
|
||||
0x6e, 0x61, 0x6d, 0x65, 0x12, 0x4a, 0x0a, 0x0a, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x5f, 0x66, 0x72,
|
||||
0x6f, 0x6d, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2b, 0x2e, 0x68, 0x69, 0x67, 0x72, 0x65,
|
||||
0x73, 0x73, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31,
|
||||
0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x45, 0x6e, 0x76, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x53,
|
||||
0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x09, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x46, 0x72, 0x6f, 0x6d,
|
||||
0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52,
|
||||
0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x2a, 0x45, 0x0a, 0x0b, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e,
|
||||
0x50, 0x68, 0x61, 0x73, 0x65, 0x12, 0x15, 0x0a, 0x11, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49,
|
||||
0x46, 0x49, 0x45, 0x44, 0x5f, 0x50, 0x48, 0x41, 0x53, 0x45, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05,
|
||||
0x41, 0x55, 0x54, 0x48, 0x4e, 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, 0x41, 0x55, 0x54, 0x48, 0x5a,
|
||||
0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x53, 0x54, 0x41, 0x54, 0x53, 0x10, 0x03, 0x2a, 0x42, 0x0a,
|
||||
0x0a, 0x50, 0x75, 0x6c, 0x6c, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x12, 0x16, 0x0a, 0x12, 0x55,
|
||||
0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x5f, 0x50, 0x4f, 0x4c, 0x49, 0x43,
|
||||
0x59, 0x10, 0x00, 0x12, 0x10, 0x0a, 0x0c, 0x49, 0x66, 0x4e, 0x6f, 0x74, 0x50, 0x72, 0x65, 0x73,
|
||||
0x65, 0x6e, 0x74, 0x10, 0x01, 0x12, 0x0a, 0x0a, 0x06, 0x41, 0x6c, 0x77, 0x61, 0x79, 0x73, 0x10,
|
||||
0x02, 0x2a, 0x26, 0x0a, 0x0e, 0x45, 0x6e, 0x76, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x53, 0x6f, 0x75,
|
||||
0x72, 0x63, 0x65, 0x12, 0x0a, 0x0a, 0x06, 0x49, 0x4e, 0x4c, 0x49, 0x4e, 0x45, 0x10, 0x00, 0x12,
|
||||
0x08, 0x0a, 0x04, 0x48, 0x4f, 0x53, 0x54, 0x10, 0x01, 0x2a, 0x2d, 0x0a, 0x0c, 0x46, 0x61, 0x69,
|
||||
0x6c, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x12, 0x0e, 0x0a, 0x0a, 0x46, 0x41, 0x49,
|
||||
0x4c, 0x5f, 0x43, 0x4c, 0x4f, 0x53, 0x45, 0x10, 0x00, 0x12, 0x0d, 0x0a, 0x09, 0x46, 0x41, 0x49,
|
||||
0x4c, 0x5f, 0x4f, 0x50, 0x45, 0x4e, 0x10, 0x01, 0x42, 0x34, 0x5a, 0x32, 0x67, 0x69, 0x74, 0x68,
|
||||
0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x61, 0x6c, 0x69, 0x62, 0x61, 0x62, 0x61, 0x2f, 0x68,
|
||||
0x69, 0x67, 0x72, 0x65, 0x73, 0x73, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x65, 0x78, 0x74, 0x65, 0x6e,
|
||||
0x73, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x62, 0x06,
|
||||
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
0x73, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x67, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f,
|
||||
0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x6f,
|
||||
0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x14, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74,
|
||||
0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x44, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x22, 0xcb, 0x01,
|
||||
0x0a, 0x09, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x69,
|
||||
0x6e, 0x67, 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x69, 0x6e,
|
||||
0x67, 0x72, 0x65, 0x73, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18,
|
||||
0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x2f, 0x0a,
|
||||
0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e,
|
||||
0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e,
|
||||
0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x41,
|
||||
0x0a, 0x0e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x5f, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65,
|
||||
0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e,
|
||||
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c,
|
||||
0x75, 0x65, 0x52, 0x0d, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x44, 0x69, 0x73, 0x61, 0x62, 0x6c,
|
||||
0x65, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x18, 0x05, 0x20, 0x03,
|
||||
0x28, 0x09, 0x52, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x22, 0x41, 0x0a, 0x08, 0x56,
|
||||
0x6d, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x35, 0x0a, 0x03, 0x65, 0x6e, 0x76, 0x18, 0x01,
|
||||
0x20, 0x03, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x68, 0x69, 0x67, 0x72, 0x65, 0x73, 0x73, 0x2e, 0x65,
|
||||
0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68,
|
||||
0x61, 0x31, 0x2e, 0x45, 0x6e, 0x76, 0x56, 0x61, 0x72, 0x52, 0x03, 0x65, 0x6e, 0x76, 0x22, 0x7e,
|
||||
0x0a, 0x06, 0x45, 0x6e, 0x76, 0x56, 0x61, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65,
|
||||
0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x4a, 0x0a, 0x0a,
|
||||
0x76, 0x61, 0x6c, 0x75, 0x65, 0x5f, 0x66, 0x72, 0x6f, 0x6d, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e,
|
||||
0x32, 0x2b, 0x2e, 0x68, 0x69, 0x67, 0x72, 0x65, 0x73, 0x73, 0x2e, 0x65, 0x78, 0x74, 0x65, 0x6e,
|
||||
0x73, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2e, 0x45,
|
||||
0x6e, 0x76, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x09, 0x76,
|
||||
0x61, 0x6c, 0x75, 0x65, 0x46, 0x72, 0x6f, 0x6d, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75,
|
||||
0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x2a, 0x45,
|
||||
0x0a, 0x0b, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x50, 0x68, 0x61, 0x73, 0x65, 0x12, 0x15, 0x0a,
|
||||
0x11, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x5f, 0x50, 0x48, 0x41,
|
||||
0x53, 0x45, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x41, 0x55, 0x54, 0x48, 0x4e, 0x10, 0x01, 0x12,
|
||||
0x09, 0x0a, 0x05, 0x41, 0x55, 0x54, 0x48, 0x5a, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x53, 0x54,
|
||||
0x41, 0x54, 0x53, 0x10, 0x03, 0x2a, 0x42, 0x0a, 0x0a, 0x50, 0x75, 0x6c, 0x6c, 0x50, 0x6f, 0x6c,
|
||||
0x69, 0x63, 0x79, 0x12, 0x16, 0x0a, 0x12, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49,
|
||||
0x45, 0x44, 0x5f, 0x50, 0x4f, 0x4c, 0x49, 0x43, 0x59, 0x10, 0x00, 0x12, 0x10, 0x0a, 0x0c, 0x49,
|
||||
0x66, 0x4e, 0x6f, 0x74, 0x50, 0x72, 0x65, 0x73, 0x65, 0x6e, 0x74, 0x10, 0x01, 0x12, 0x0a, 0x0a,
|
||||
0x06, 0x41, 0x6c, 0x77, 0x61, 0x79, 0x73, 0x10, 0x02, 0x2a, 0x26, 0x0a, 0x0e, 0x45, 0x6e, 0x76,
|
||||
0x56, 0x61, 0x6c, 0x75, 0x65, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x0a, 0x0a, 0x06, 0x49,
|
||||
0x4e, 0x4c, 0x49, 0x4e, 0x45, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x48, 0x4f, 0x53, 0x54, 0x10,
|
||||
0x01, 0x2a, 0x2d, 0x0a, 0x0c, 0x46, 0x61, 0x69, 0x6c, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67,
|
||||
0x79, 0x12, 0x0e, 0x0a, 0x0a, 0x46, 0x41, 0x49, 0x4c, 0x5f, 0x43, 0x4c, 0x4f, 0x53, 0x45, 0x10,
|
||||
0x00, 0x12, 0x0d, 0x0a, 0x09, 0x46, 0x41, 0x49, 0x4c, 0x5f, 0x4f, 0x50, 0x45, 0x4e, 0x10, 0x01,
|
||||
0x42, 0x34, 0x5a, 0x32, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x61,
|
||||
0x6c, 0x69, 0x62, 0x61, 0x62, 0x61, 0x2f, 0x68, 0x69, 0x67, 0x72, 0x65, 0x73, 0x73, 0x2f, 0x61,
|
||||
0x70, 0x69, 0x2f, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x76, 0x31,
|
||||
0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -804,6 +807,7 @@ var file_extensions_v1alpha1_wasmplugin_proto_goTypes = []interface{}{
|
||||
(*EnvVar)(nil), // 7: higress.extensions.v1alpha1.EnvVar
|
||||
(*_struct.Struct)(nil), // 8: google.protobuf.Struct
|
||||
(*wrappers.Int32Value)(nil), // 9: google.protobuf.Int32Value
|
||||
(*wrappers.BoolValue)(nil), // 10: google.protobuf.BoolValue
|
||||
}
|
||||
var file_extensions_v1alpha1_wasmplugin_proto_depIdxs = []int32{
|
||||
1, // 0: higress.extensions.v1alpha1.WasmPlugin.image_pull_policy:type_name -> higress.extensions.v1alpha1.PullPolicy
|
||||
@@ -814,14 +818,16 @@ var file_extensions_v1alpha1_wasmplugin_proto_depIdxs = []int32{
|
||||
6, // 5: higress.extensions.v1alpha1.WasmPlugin.vm_config:type_name -> higress.extensions.v1alpha1.VmConfig
|
||||
8, // 6: higress.extensions.v1alpha1.WasmPlugin.default_config:type_name -> google.protobuf.Struct
|
||||
5, // 7: higress.extensions.v1alpha1.WasmPlugin.match_rules:type_name -> higress.extensions.v1alpha1.MatchRule
|
||||
8, // 8: higress.extensions.v1alpha1.MatchRule.config:type_name -> google.protobuf.Struct
|
||||
7, // 9: higress.extensions.v1alpha1.VmConfig.env:type_name -> higress.extensions.v1alpha1.EnvVar
|
||||
2, // 10: higress.extensions.v1alpha1.EnvVar.value_from:type_name -> higress.extensions.v1alpha1.EnvValueSource
|
||||
11, // [11:11] is the sub-list for method output_type
|
||||
11, // [11:11] is the sub-list for method input_type
|
||||
11, // [11:11] is the sub-list for extension type_name
|
||||
11, // [11:11] is the sub-list for extension extendee
|
||||
0, // [0:11] is the sub-list for field type_name
|
||||
10, // 8: higress.extensions.v1alpha1.WasmPlugin.default_config_disable:type_name -> google.protobuf.BoolValue
|
||||
8, // 9: higress.extensions.v1alpha1.MatchRule.config:type_name -> google.protobuf.Struct
|
||||
10, // 10: higress.extensions.v1alpha1.MatchRule.config_disable:type_name -> google.protobuf.BoolValue
|
||||
7, // 11: higress.extensions.v1alpha1.VmConfig.env:type_name -> higress.extensions.v1alpha1.EnvVar
|
||||
2, // 12: higress.extensions.v1alpha1.EnvVar.value_from:type_name -> higress.extensions.v1alpha1.EnvValueSource
|
||||
13, // [13:13] is the sub-list for method output_type
|
||||
13, // [13:13] is the sub-list for method input_type
|
||||
13, // [13:13] is the sub-list for extension type_name
|
||||
13, // [13:13] is the sub-list for extension extendee
|
||||
0, // [0:13] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_extensions_v1alpha1_wasmplugin_proto_init() }
|
||||
|
||||
@@ -112,7 +112,7 @@ message WasmPlugin {
|
||||
// Extended by Higress, matching rules take effect
|
||||
repeated MatchRule match_rules = 102;
|
||||
// disable the default config
|
||||
bool default_config_disable = 103;
|
||||
google.protobuf.BoolValue default_config_disable = 103;
|
||||
}
|
||||
|
||||
// Extended by Higress
|
||||
@@ -120,7 +120,7 @@ message MatchRule {
|
||||
repeated string ingress = 1;
|
||||
repeated string domain = 2;
|
||||
google.protobuf.Struct config = 3;
|
||||
bool config_disable = 4;
|
||||
google.protobuf.BoolValue config_disable = 4;
|
||||
repeated string service = 5;
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,8 @@ DOCKER_ALL_VARIANTS ?= debug distroless
|
||||
INCLUDE_UNTAGGED_DEFAULT ?= false
|
||||
DEFAULT_DISTRIBUTION=debug
|
||||
|
||||
HIGRESS_DOCKER_BUILDX_RULE ?= $(foreach VARIANT,$(DOCKER_BUILD_VARIANTS), time (mkdir -p $(HIGRESS_DOCKER_BUILD_TOP)/$@ && TARGET_ARCH=$(TARGET_ARCH) ./docker/docker-copy.sh $^ $(HIGRESS_DOCKER_BUILD_TOP)/$@ && cd $(HIGRESS_DOCKER_BUILD_TOP)/$@ $(BUILD_PRE) && docker buildx create --name higress --node higress0 --platform linux/amd64,linux/arm64 --use && docker buildx build --no-cache --platform linux/amd64,linux/arm64 $(BUILD_ARGS) --build-arg BASE_DISTRIBUTION=$(call normalize-tag,$(VARIANT)) -t $(HUB)/higress:$(TAG)$(call variant-tag,$(VARIANT)) -f Dockerfile.higress . --push ); )
|
||||
HIGRESS_DOCKER_RULE ?= $(foreach VARIANT,$(DOCKER_BUILD_VARIANTS), time (mkdir -p $(HIGRESS_DOCKER_BUILD_TOP)/$@ && TARGET_ARCH=$(TARGET_ARCH) ./docker/docker-copy.sh $^ $(HIGRESS_DOCKER_BUILD_TOP)/$@ && cd $(HIGRESS_DOCKER_BUILD_TOP)/$@ $(BUILD_PRE) && docker build $(BUILD_ARGS) --build-arg BASE_DISTRIBUTION=$(call normalize-tag,$(VARIANT)) -t $(HUB)/higress:$(TAG)$(call variant-tag,$(VARIANT)) -f Dockerfile.higress . ); )
|
||||
IMG ?= higress
|
||||
IMG_URL ?= $(HUB)/$(IMG):$(TAG)
|
||||
|
||||
HIGRESS_DOCKER_BUILDX_RULE ?= $(foreach VARIANT,$(DOCKER_BUILD_VARIANTS), time (mkdir -p $(HIGRESS_DOCKER_BUILD_TOP)/$@ && TARGET_ARCH=$(TARGET_ARCH) ./docker/docker-copy.sh $^ $(HIGRESS_DOCKER_BUILD_TOP)/$@ && cd $(HIGRESS_DOCKER_BUILD_TOP)/$@ $(BUILD_PRE) && docker buildx create --name higress --node higress0 --platform linux/amd64,linux/arm64 --use && docker buildx build --no-cache --platform linux/amd64,linux/arm64 $(BUILD_ARGS) --build-arg BASE_DISTRIBUTION=$(call normalize-tag,$(VARIANT)) -t $(IMG_URL)$(call variant-tag,$(VARIANT)) -f Dockerfile.higress . --push ); )
|
||||
HIGRESS_DOCKER_RULE ?= $(foreach VARIANT,$(DOCKER_BUILD_VARIANTS), time (mkdir -p $(HIGRESS_DOCKER_BUILD_TOP)/$@ && TARGET_ARCH=$(TARGET_ARCH) ./docker/docker-copy.sh $^ $(HIGRESS_DOCKER_BUILD_TOP)/$@ && cd $(HIGRESS_DOCKER_BUILD_TOP)/$@ $(BUILD_PRE) && docker build $(BUILD_ARGS) --build-arg BASE_DISTRIBUTION=$(call normalize-tag,$(VARIANT)) -t $(IMG_URL)$(call variant-tag,$(VARIANT)) -f Dockerfile.higress . ); )
|
||||
|
||||
143
docs/architecture.md
Normal file
143
docs/architecture.md
Normal file
@@ -0,0 +1,143 @@
|
||||
# Higress 核心组件和原理
|
||||
|
||||
Higress 是基于 Envoy 和 Istio 进行二次定制化开发构建和功能增强,同时利用 Envoy 和 Istio 一些插件机制,实现了一个轻量级的网关服务。其包括 3 个核心组件:Higress Controller(控制器)、Higress Gateway(网关)和 Higress Console(控制台)。
|
||||
下图概况了其核心工作流程:
|
||||
|
||||

|
||||
|
||||
本章将重点介绍 Higress 的两个核心组件:Higress Controller 和 Higress Gateway。
|
||||
|
||||
## 1 Higress Console
|
||||
|
||||
Higress Console 是 Higress 网关的管理控制台,主要功能是管理 Higress 网关的路由配置、插件配置等。
|
||||
|
||||
### 1.1 Higress Admin SDK
|
||||
|
||||
Higress Admin SDK 脱胎于 Higress Console。起初,它作为 Higress Console 的一部分,为前端界面提供实际的功能支持。后来考虑到对接外部系统等需求,将配置管理的部分剥离出来,形成一个独立的逻辑组件,便于和各个系统进行对接。目前支持服务来源管理、服务管理、路由管理、域名管理、证书管理、插件管理等功能。
|
||||
Higress Admin SDK 现在只提供 Java 版本,且要求 JDK 版本不低于 17。具体如何集成请参考 Higress 官方 BLOG [如何使用 Higress Admin SDK 进行配置管理](https://higress.io/zh-cn/blog/admin-sdk-intro)。
|
||||
|
||||
## 2 Higress Controller
|
||||
|
||||
Higress Controller(控制器) 是 Higress 的核心组件,其功能主要是实现 Higress 网关的服务发现、动态配置管理,以及动态下发配置给数据面。Higress Controller 内部包含两个子组件:Discovery 和 Higress Core。
|
||||
|
||||
### 2.1 Discovery 组件
|
||||
|
||||
Discovery 组件(Istio Pilot-Discovery)是 Istio 的核心组件,负责服务发现、配置管理、证书签发、控制面和数据面之间的通讯和配置下发等。Discovery 内部结构比较复杂,本文只介绍 Discovery 配置管理和服务发现的基本原理,其核心功能的详细介绍可以参考赵化冰老师的 BLOG [Istio Pilot 组件介绍](https://www.zhaohuabing.com/post/2019-10-21-pilot-discovery-code-analysis/)。
|
||||
Discovery 将 Kubernetes Service、Gateway API 配置等转换成 Istio 配置,然后将所有 Istio 配置合并转成符合 xDS 接口规范的数据结构,通过 GRPC 下发到数据面的 Envoy。其工作原理如下图:
|
||||
|
||||

|
||||
|
||||
#### 2.1.1 Config Controller
|
||||
|
||||
Discovery 为了更好管理 Istio 配置来源,提供 `Config Controller` 用于管理各种配置来源,目前支持 4 种类型的 `Config Controller`:
|
||||
|
||||
- Kubernetes:使用 Kubernetes 作为配置信息来源,该方式的直接依赖 Kubernetes 强大的 CRD 机制来存储配置信息,简单方便,是 Istio 最开始使用的配置信息存储方案, 其中包括 `Kubernetes Controller` 和 `Gateway API Controller` 两个实现。
|
||||
- MCP(Mesh Configuration Protocol):使用 Kubernetes 存储配置数据导致了 Istio 和 Kubernetes 的耦合,限制了 Istio 在非 Kubernetes 环境下的运用。为了解决该耦合,Istio 社区提出了 MCP。
|
||||
- Memory:一个基于内存的 Config Controller 实现,主要用于测试。
|
||||
- File:一个基于文件的 Config Controller 实现,主要用于测试。
|
||||
|
||||
1. Istio 配置
|
||||
|
||||
Istio 配置包括:`Gateway`、`VirtualService`、`DestinationRule`、`ServiceEntry`、`EnvoyFilter`、`WasmPlugin`、`WorkloadEntry`、`WorkloadGroup` 等,可以参考 Istio 官方文档[流量管理](https://istio.io/latest/zh/docs/reference/config/networking/)了解更多配置信息。
|
||||
|
||||
2. Gateway API 配置
|
||||
|
||||
Gateway API 配置包括:`GatewayClass`、`Gateway`、`HttpRoute`、`TCPRoute`、`GRPCRoute` 等, 可以参考 Gateway API 官方文档 [Gateway API](https://gateway-api.sigs.k8s.io/api-types/gateway/) 了解更多配置信息。
|
||||
|
||||
3. MCP over xDS
|
||||
|
||||
Discovery 作为 MCP Client,任何实现了 MCP 协议的 Server 都可以通过 MCP 协议向 Discovery 下发配置信息,从而消除了 Istio 和 Kubernetes 之间的耦合, 同时使 Istio 的配置信息处理更加灵活和可扩展。
|
||||
同时 MCP 是一种基于 xDS 协议的配置管理协议,Higress Core 通过实现 MCP 协议,使 Higress Core 成为 Discovery 的 Istio 配置来源。
|
||||
|
||||
4. Config Controller 来源配置
|
||||
|
||||
在 `higress-system` 命名空间中,名为 `higress-config` 的 Configmap 中,`mesh` 配置项包含一个 `configSources` 属性用于配置来源。其 Configmap 部分配置项如下:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: higress-config
|
||||
namespace: higress-system
|
||||
data:
|
||||
mesh: |-
|
||||
accessLogEncoding: TEXT
|
||||
...
|
||||
configSources:
|
||||
- address: xds://127.0.0.1:15051
|
||||
- address: k8s://
|
||||
...
|
||||
meshNetworks: "networks: {}"
|
||||
```
|
||||
|
||||
#### 2.1.2 Service Controller
|
||||
|
||||
`Service Controller` 用于管理各种 `Service Registry`,提供服务发现数据,目前 Istio 支持的 `Service Registry` 主要包括:
|
||||
|
||||
- Kubernetes:对接 Kubernetes Registry,可以将 Kubernetes 中定义的 Service 和 Endpoint 采集到 Istio 中。
|
||||
- Memory:一个基于内存的 Service Controller 实现,主要用于测试。
|
||||
|
||||
### 2.2 Higress Core 组件
|
||||
|
||||
Higress Core 核心逻辑如下图:
|
||||
|
||||

|
||||
|
||||
|
||||
Higress Core 内部包含两个核心子组件: Ingress Config 和 Cert Server。
|
||||
|
||||
#### 2.2.1 Ingress Config
|
||||
|
||||
Ingress Config 包含 6 个控制器,各自负责不同的功能:
|
||||
|
||||
- Ingress Controller:监听 Ingress 资源,将 Ingress 转换为 Istio 的 Gateway、VirtualService、DestinationRule 等资源。
|
||||
- Gateway Controller:监听 Gateway、VirtualService、DestinationRule 等资源。
|
||||
- McpBridge Controller:根据 McpBridge 的配置,将来自 Nacos、Eureka、Consul、Zookeeper 等外部注册中心或 DNS 的服务信息转换成 Istio ServiceEntry 资源。
|
||||
- Http2Rpc Controller:监听 Http2Rpc 资源,实现 HTTP 协议到 RPC 协议的转换。用户可以通过配置协议转换,将 RPC 服务以 HTTP 接口的形式暴露,从而使用 HTTP 请求调用 RPC 接口。
|
||||
- WasmPlugin Controller:监听 WasmPlugin 资源,将 Higress WasmPlugin 转化为 Istio WasmPlugin。Higress WasmPlugin 在 Istio WasmPlugin 的基础上进行了扩展,支持全局、路由、域名、服务级别的配置。
|
||||
- ConfigmapMgr:监听 Higress 的全局配置 `higress-config` ConfigMap,可以根据 tracing、gzip 等配置构造 EnvoyFilter。
|
||||
|
||||
#### 2.2.2 Cert Server
|
||||
|
||||
Cert Server 管理 Secret 资源和证书自动签发。
|
||||
|
||||
## 3 Higress Gateway
|
||||
|
||||
Higress Gateway 内部包含两个子组件:Pilot Agent 和 Envoy。Pilot Agent 主要负责 Envoy 的启动和配置,同时代理 Envoy xDS 请求到 Discovery。 Envoy 作为数据面,负责接收控制面的配置下发,并代理请求到业务服务。 Pilot Agent 和 Envoy 之间通讯协议是使用 xDS 协议, 通过 Unix Domain Socket(UDS)进行通信。
|
||||
Envoy 核心架构如下图:
|
||||
|
||||

|
||||
|
||||
### 1 Envoy 核心组件
|
||||
|
||||
- 下游(Downstream):
|
||||
下游是 Envoy 的客户端,它们负责发起请求并接收 Envoy 的响应。下游通常是最终用户的设备或服务,它们通过 Envoy 代理与后端服务进行通信。
|
||||
|
||||
- 上游(Upstream):
|
||||
上游是 Envoy 的后端服务器,它们接收 Envoy 代理的连接和请求。上游提供服务或数据,对来自下游客户端的请求进行处理并返回响应。
|
||||
|
||||
- 监听器(Listener):
|
||||
监听器是可以接受来自下游客户端连接的网络地址(如 IP 地址和端口,Unix Domain Socket 等)。Envoy 支持在单个进程中配置任意数量的监听器。监听器可以通过 `Listener Discovery Service(LDS)`来动态发现和更新。
|
||||
|
||||
- 路由(Router):
|
||||
路由器是 Envoy 中连接下游和上游的桥梁。它负责决定如何将监听器接收到的请求路由到适当的集群。路由器根据配置的路由规则,如路径、HTTP 标头 等,来确定请求的目标集群,从而实现精确的流量控制和路由。路由器可以通过 `Route Discovery Service(RDS)`来动态发现和更新。
|
||||
|
||||
- 集群(Cluster):
|
||||
集群是一组逻辑上相似的服务提供者的集合。集群成员的选择由负载均衡策略决定,确保请求能够均匀或按需分配到不同的服务实例。集群可以通过 `Cluster Discovery Service(CDS)`来动态发现和更新。
|
||||
|
||||
- 端点(Endpoint):
|
||||
端点是上游集群中的具体服务实例,可以是 IP 地址和端口号的组合。端点可以通过 `Endpoint Discovery Service(EDS)`来动态发现和更新。
|
||||
|
||||
- SSL/TLS:
|
||||
Envoy 可以通过 `Secret Discovery Service (SDS)` 动态获取监听器和集群所需的 TLS 证书、私钥以及信任的根证书和撤销机制等配置信息。
|
||||
|
||||
通过这些组件的协同工作,Envoy 能够高效地处理网络请求,提供流量管理、负载均衡、服务发现和动态路由等关键功能。
|
||||
要详细了解 Envoy 的工作原理,可以参考[Envoy 官方文档](https://www.envoyproxy.io/docs/envoy/latest/intro/intro),最佳的方式可以通过一个请求通过 [Envoy 代理的生命周期](https://www.envoyproxy.io/docs/envoy/latest/intro/life_of_a_request)事件的过程来理解 Envoy 的工作原理。
|
||||
|
||||
|
||||
## 参考
|
||||
|
||||
- [1] [Istio Pilot 组件介绍](https://www.zhaohuabing.com/post/2019-10-21-pilot-discovery-code-analysis/)
|
||||
- [2] [Istio 服务注册插件机制代码解析](https://www.zhaohuabing.com/post/2019-02-18-pilot-service-registry-code-analysis/)
|
||||
- [3] [Istio Pilot代码深度解析](https://www.zhaohuabing.com/post/2019-10-21-pilot-discovery-code-analysis/)
|
||||
- [4] [Envoy 官方文档](https://www.envoyproxy.io/docs/envoy/latest/intro/intro)
|
||||
BIN
docs/images/img_02_01.png
Normal file
BIN
docs/images/img_02_01.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 119 KiB |
BIN
docs/images/img_02_02.png
Normal file
BIN
docs/images/img_02_02.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 131 KiB |
BIN
docs/images/img_02_03.png
Normal file
BIN
docs/images/img_02_03.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 167 KiB |
BIN
docs/images/img_02_04.png
Normal file
BIN
docs/images/img_02_04.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 127 KiB |
Submodule envoy/envoy updated: e9302f5574...c2471f1247
@@ -1,5 +1,5 @@
|
||||
apiVersion: v2
|
||||
appVersion: 2.0.3
|
||||
appVersion: 2.0.7
|
||||
description: Helm chart for deploying higress gateways
|
||||
icon: https://higress.io/img/higress_logo_small.png
|
||||
home: http://higress.io/
|
||||
@@ -10,4 +10,4 @@ name: higress-core
|
||||
sources:
|
||||
- http://github.com/alibaba/higress
|
||||
type: application
|
||||
version: 2.0.3
|
||||
version: 2.0.7
|
||||
|
||||
@@ -7,9 +7,6 @@ Rendering the pod template of gateway component.
|
||||
template:
|
||||
metadata:
|
||||
annotations:
|
||||
{{- if .Values.global.enableHigressIstio }}
|
||||
"enableHigressIstio": "true"
|
||||
{{- end }}
|
||||
{{- if .Values.gateway.podAnnotations }}
|
||||
{{- toYaml .Values.gateway.podAnnotations | nindent 6 }}
|
||||
{{- end }}
|
||||
@@ -131,7 +128,7 @@ template:
|
||||
- name: ISTIO_META_REQUESTED_NETWORK_VIEW
|
||||
value: "{{.}}"
|
||||
{{- end }}
|
||||
{{- range $key, $val := .Values.env }}
|
||||
{{- range $key, $val := .Values.gateway.env }}
|
||||
- name: {{ $key }}
|
||||
value: {{ $val | quote }}
|
||||
{{- end }}
|
||||
@@ -268,11 +265,7 @@ template:
|
||||
{{- end }}
|
||||
- name: higress-ca-root-cert
|
||||
configMap:
|
||||
{{- if .Values.global.enableHigressIstio }}
|
||||
name: istio-ca-root-cert
|
||||
{{- else }}
|
||||
name: higress-ca-root-cert
|
||||
{{- end }}
|
||||
- name: config
|
||||
configMap:
|
||||
name: higress-config
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
accessLogFile: "/dev/stdout"
|
||||
{{- end }}
|
||||
ingressControllerMode: "OFF"
|
||||
accessLogFormat: '{"authority":"%REQ(X-ENVOY-ORIGINAL-HOST?:AUTHORITY)%","bytes_received":"%BYTES_RECEIVED%","bytes_sent":"%BYTES_SENT%","downstream_local_address":"%DOWNSTREAM_LOCAL_ADDRESS%","downstream_remote_address":"%DOWNSTREAM_REMOTE_ADDRESS%","duration":"%DURATION%","istio_policy_status":"%DYNAMIC_METADATA(istio.mixer:status)%","method":"%REQ(:METHOD)%","path":"%REQ(X-ENVOY-ORIGINAL-PATH?:PATH)%","protocol":"%PROTOCOL%","request_id":"%REQ(X-REQUEST-ID)%","requested_server_name":"%REQUESTED_SERVER_NAME%","response_code":"%RESPONSE_CODE%","response_flags":"%RESPONSE_FLAGS%","route_name":"%ROUTE_NAME%","start_time":"%START_TIME%","trace_id":"%REQ(X-B3-TRACEID)%","upstream_cluster":"%UPSTREAM_CLUSTER%","upstream_host":"%UPSTREAM_HOST%","upstream_local_address":"%UPSTREAM_LOCAL_ADDRESS%","upstream_service_time":"%RESP(X-ENVOY-UPSTREAM-SERVICE-TIME)%","upstream_transport_failure_reason":"%UPSTREAM_TRANSPORT_FAILURE_REASON%","user_agent":"%REQ(USER-AGENT)%","x_forwarded_for":"%REQ(X-FORWARDED-FOR)%","response_code_details":"%RESPONSE_CODE_DETAILS%"}
|
||||
accessLogFormat: '{"ai_log":"%FILTER_STATE(wasm.ai_log:PLAIN)%","authority":"%REQ(X-ENVOY-ORIGINAL-HOST?:AUTHORITY)%","bytes_received":"%BYTES_RECEIVED%","bytes_sent":"%BYTES_SENT%","downstream_local_address":"%DOWNSTREAM_LOCAL_ADDRESS%","downstream_remote_address":"%DOWNSTREAM_REMOTE_ADDRESS%","duration":"%DURATION%","istio_policy_status":"%DYNAMIC_METADATA(istio.mixer:status)%","method":"%REQ(:METHOD)%","path":"%REQ(X-ENVOY-ORIGINAL-PATH?:PATH)%","protocol":"%PROTOCOL%","request_id":"%REQ(X-REQUEST-ID)%","requested_server_name":"%REQUESTED_SERVER_NAME%","response_code":"%RESPONSE_CODE%","response_flags":"%RESPONSE_FLAGS%","route_name":"%ROUTE_NAME%","start_time":"%START_TIME%","trace_id":"%REQ(X-B3-TRACEID)%","upstream_cluster":"%UPSTREAM_CLUSTER%","upstream_host":"%UPSTREAM_HOST%","upstream_local_address":"%UPSTREAM_LOCAL_ADDRESS%","upstream_service_time":"%RESP(X-ENVOY-UPSTREAM-SERVICE-TIME)%","upstream_transport_failure_reason":"%UPSTREAM_TRANSPORT_FAILURE_REASON%","user_agent":"%REQ(USER-AGENT)%","x_forwarded_for":"%REQ(X-FORWARDED-FOR)%","response_code_details":"%RESPONSE_CODE_DETAILS%"}
|
||||
|
||||
'
|
||||
dnsRefreshRate: 200s
|
||||
@@ -20,11 +20,7 @@
|
||||
# When processing a leaf namespace Istio will search for declarations in that namespace first
|
||||
# and if none are found it will search in the root namespace. Any matching declaration found in the root namespace
|
||||
# is processed as if it were declared in the leaf namespace.
|
||||
{{- if .Values.global.enableHigressIstio }}
|
||||
rootNamespace: {{ .Values.meshConfig.rootNamespace | default .Values.global.istioNamespace }}
|
||||
{{- else }}
|
||||
rootNamespace: {{ .Release.Namespace }}
|
||||
{{- end }}
|
||||
|
||||
configSources:
|
||||
- address: "xds://127.0.0.1:15051"
|
||||
@@ -85,12 +81,8 @@
|
||||
discoveryAddress: {{ printf "istiod.%s.svc" .Release.Namespace }}:15012
|
||||
{{- end }}
|
||||
{{- else }}
|
||||
{{- if .Values.global.enableHigressIstio }}
|
||||
discoveryAddress: {{ printf "istiod.%s.svc" .Values.global.istioNamespace }}:15012
|
||||
{{- else }}
|
||||
discoveryAddress: {{ include "controller.name" . }}.{{.Release.Namespace}}.svc:15012
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
proxyStatsMatcher:
|
||||
inclusionRegexps:
|
||||
- ".*"
|
||||
|
||||
@@ -96,7 +96,6 @@ spec:
|
||||
volumeMounts:
|
||||
- name: log
|
||||
mountPath: /var/log
|
||||
{{- if not .Values.global.enableHigressIstio }}
|
||||
- name: discovery
|
||||
image: "{{ .Values.pilot.hub | default .Values.global.hub }}/{{ .Values.pilot.image | default "pilot" }}:{{ .Values.pilot.tag | default .Chart.AppVersion }}"
|
||||
{{- if .Values.global.imagePullPolicy }}
|
||||
@@ -137,6 +136,10 @@ spec:
|
||||
periodSeconds: 3
|
||||
timeoutSeconds: 5
|
||||
env:
|
||||
- name: ENABLE_PUSH_ALL_MCP_CLUSTERS
|
||||
value: "{{ .Values.global.enablePushAllMCPClusters }}"
|
||||
- name: PILOT_ENABLE_LDS_CACHE
|
||||
value: "{{ .Values.global.enableLDSCache }}"
|
||||
- name: PILOT_ENABLE_QUIC_LISTENERS
|
||||
value: "true"
|
||||
- name: VALIDATION_WEBHOOK_CONFIG_NAME
|
||||
@@ -229,10 +232,8 @@ spec:
|
||||
value: "false"
|
||||
- name: PILOT_ENABLE_GATEWAY_API_DEPLOYMENT_CONTROLLER
|
||||
value: "false"
|
||||
{{- if not .Values.global.enableHigressIstio }}
|
||||
- name: CUSTOM_CA_CERT_NAME
|
||||
value: "higress-ca-root-cert"
|
||||
{{- end }}
|
||||
{{- if not (or .Values.global.local .Values.global.kind) }}
|
||||
resources:
|
||||
{{- if .Values.pilot.resources }}
|
||||
@@ -269,7 +270,6 @@ spec:
|
||||
- name: extracacerts
|
||||
mountPath: /cacerts
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- with .Values.controller.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
@@ -285,7 +285,6 @@ spec:
|
||||
volumes:
|
||||
- name: log
|
||||
emptyDir: {}
|
||||
{{- if not .Values.global.enableHigressIstio }}
|
||||
- name: config
|
||||
configMap:
|
||||
name: higress-config
|
||||
@@ -317,4 +316,3 @@ spec:
|
||||
configMap:
|
||||
name: pilot-jwks-extra-cacerts{{- if not (eq .Values.revision "") }}-{{ .Values.revision }}{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
@@ -9,7 +9,6 @@ spec:
|
||||
type: {{ .Values.controller.service.type }}
|
||||
ports:
|
||||
{{- toYaml .Values.controller.ports | nindent 4 }}
|
||||
{{- if not .Values.global.enableHigressIstio }}
|
||||
- port: 15010
|
||||
name: grpc-xds # plaintext
|
||||
protocol: TCP
|
||||
@@ -23,6 +22,5 @@ spec:
|
||||
- port: 15014
|
||||
name: http-monitoring # prometheus stats
|
||||
protocol: TCP
|
||||
{{- end }}
|
||||
selector:
|
||||
{{- include "controller.selectorLabels" . | nindent 4 }}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
{{- if eq .Values.gateway.kind "DaemonSet" -}}
|
||||
{{- $o11y := .Values.global.o11y }}
|
||||
{{- $unprivilegedPortSupported := true }}
|
||||
{{- range $index, $node := (lookup "v1" "Node" "default" "").items }}
|
||||
{{- if eq .Values.gateway.unprivilegedPortSupported nil -}}
|
||||
{{- $unprivilegedPortSupported := true }}
|
||||
{{- range $index, $node := (lookup "v1" "Node" "default" "").items }}
|
||||
{{- $kernelVersion := $node.status.nodeInfo.kernelVersion }}
|
||||
{{- if $kernelVersion }}
|
||||
{{- $kernelVersion = regexFind "^(\\d+\\.\\d+\\.\\d+)" $kernelVersion }}
|
||||
@@ -9,8 +10,9 @@
|
||||
{{- $unprivilegedPortSupported = false }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end -}}
|
||||
{{- $_ := set .Values.gateway "unprivilegedPortSupported" $unprivilegedPortSupported -}}
|
||||
{{- end -}}
|
||||
{{- $_ := set .Values.gateway "unprivilegedPortSupported" $unprivilegedPortSupported -}}
|
||||
|
||||
apiVersion: apps/v1
|
||||
kind: DaemonSet
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{{- if eq .Values.gateway.kind "Deployment" -}}
|
||||
{{- $unprivilegedPortSupported := true }}
|
||||
{{- range $index, $node := (lookup "v1" "Node" "default" "").items }}
|
||||
{{- if eq .Values.gateway.unprivilegedPortSupported nil -}}
|
||||
{{- $unprivilegedPortSupported := true }}
|
||||
{{- range $index, $node := (lookup "v1" "Node" "default" "").items }}
|
||||
{{- $kernelVersion := $node.status.nodeInfo.kernelVersion }}
|
||||
{{- if $kernelVersion }}
|
||||
{{- $kernelVersion = regexFind "^(\\d+\\.\\d+\\.\\d+)" $kernelVersion }}
|
||||
@@ -8,8 +9,9 @@
|
||||
{{- $unprivilegedPortSupported = false }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end -}}
|
||||
{{- $_ := set .Values.gateway "unprivilegedPortSupported" $unprivilegedPortSupported -}}
|
||||
{{- end -}}
|
||||
{{- $_ := set .Values.gateway "unprivilegedPortSupported" $unprivilegedPortSupported -}}
|
||||
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
|
||||
@@ -3,7 +3,9 @@ global:
|
||||
enableH3: false
|
||||
enableIPv6: false
|
||||
enableProxyProtocol: false
|
||||
liteMetrics: true
|
||||
enableLDSCache: false
|
||||
enablePushAllMCPClusters: true
|
||||
liteMetrics: false
|
||||
xdsMaxRecvMsgSize: "104857600"
|
||||
defaultUpstreamConcurrencyThreshold: 10000
|
||||
enableSRDS: true
|
||||
@@ -40,8 +42,6 @@ global:
|
||||
enableIstioAPI: true
|
||||
# -- If true, Higress Controller will monitor Gateway API resources as well
|
||||
enableGatewayAPI: false
|
||||
# Deprecated
|
||||
enableHigressIstio: false
|
||||
# -- Used to locate istiod.
|
||||
istioNamespace: istio-system
|
||||
# -- enable pod disruption budget for the control plane, which is used to
|
||||
@@ -467,6 +467,7 @@ gateway:
|
||||
# On Kubernetes 1.22+, this only requires the `net.ipv4.ip_unprivileged_port_start` sysctl.
|
||||
securityContext: ~
|
||||
containerSecurityContext: ~
|
||||
unprivilegedPortSupported: ~
|
||||
|
||||
service:
|
||||
# -- Type of service. Set to "None" to disable the service entirely
|
||||
@@ -487,6 +488,7 @@ gateway:
|
||||
externalTrafficPolicy: ""
|
||||
|
||||
rollingMaxSurge: 100%
|
||||
# -- If global.local is true, the default value is 100%, otherwise it is 25%
|
||||
rollingMaxUnavailable: 25%
|
||||
|
||||
resources:
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
dependencies:
|
||||
- name: higress-core
|
||||
repository: file://../core
|
||||
version: 2.0.3
|
||||
version: 2.0.7
|
||||
- name: higress-console
|
||||
repository: https://higress.io/helm-charts/
|
||||
version: 1.4.5
|
||||
digest: sha256:74b772113264168483961f5d0424459fd7359adc509a4b50400229581d7cddbf
|
||||
generated: "2024-11-08T14:06:51.871719+08:00"
|
||||
version: 2.0.4
|
||||
digest: sha256:ca9cc8bdac0488d79c20e7a4e3d7b3a436a59b697a37728daa462601b4d1ea65
|
||||
generated: "2025-02-19T16:23:39.424987+08:00"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
apiVersion: v2
|
||||
appVersion: 2.0.3
|
||||
appVersion: 2.0.7
|
||||
description: Helm chart for deploying Higress gateways
|
||||
icon: https://higress.io/img/higress_logo_small.png
|
||||
home: http://higress.io/
|
||||
@@ -12,9 +12,9 @@ sources:
|
||||
dependencies:
|
||||
- name: higress-core
|
||||
repository: "file://../core"
|
||||
version: 2.0.3
|
||||
version: 2.0.7
|
||||
- name: higress-console
|
||||
repository: "https://higress.io/helm-charts/"
|
||||
version: 1.4.5
|
||||
version: 2.0.4
|
||||
type: application
|
||||
version: 2.0.3
|
||||
version: 2.0.7
|
||||
|
||||
@@ -128,7 +128,7 @@ The command removes all the Kubernetes components associated with the chart and
|
||||
| gateway.resources.requests.memory | string | `"2048Mi"` | |
|
||||
| gateway.revision | string | `""` | revision declares which revision this gateway is a part of |
|
||||
| gateway.rollingMaxSurge | string | `"100%"` | |
|
||||
| gateway.rollingMaxUnavailable | string | `"25%"` | |
|
||||
| gateway.rollingMaxUnavailable | string | `"25%"` | If global.local is true, the default value is 100%, otherwise it is 25% |
|
||||
| gateway.securityContext | string | `nil` | Define the security context for the pod. If unset, this will be automatically set to the minimum privileges required to bind to port 80 and 443. On Kubernetes 1.22+, this only requires the `net.ipv4.ip_unprivileged_port_start` sysctl. |
|
||||
| gateway.service.annotations | object | `{}` | |
|
||||
| gateway.service.externalTrafficPolicy | string | `""` | |
|
||||
@@ -149,6 +149,7 @@ The command removes all the Kubernetes components associated with the chart and
|
||||
| gateway.serviceAccount.name | string | `""` | The name of the service account to use. If not set, the release name is used |
|
||||
| gateway.tag | string | `""` | |
|
||||
| gateway.tolerations | list | `[]` | |
|
||||
| gateway.unprivilegedPortSupported | string | `nil` | |
|
||||
| global.autoscalingv2API | bool | `true` | whether to use autoscaling/v2 template for HPA settings for internal usage only, not to be configured by users. |
|
||||
| global.caAddress | string | `""` | The customized CA address to retrieve certificates for the pods in the cluster. CSR clients such as the Istio Agent and ingress gateways can use this to specify the CA endpoint. If not set explicitly, default to the Istio discovery address. |
|
||||
| global.caName | string | `""` | The name of the CA for workload certificates. For example, when caName=GkeWorkloadCertificate, GKE workload certificates will be used as the certificates for workloads. The default value is "" and when caName="", the CA will be configured by other mechanisms (e.g., environmental variable CA_PROVIDER). |
|
||||
@@ -159,10 +160,11 @@ The command removes all the Kubernetes components associated with the chart and
|
||||
| global.disableAlpnH2 | bool | `false` | Whether to disable HTTP/2 in ALPN |
|
||||
| global.enableGatewayAPI | bool | `false` | If true, Higress Controller will monitor Gateway API resources as well |
|
||||
| global.enableH3 | bool | `false` | |
|
||||
| global.enableHigressIstio | bool | `false` | |
|
||||
| global.enableIPv6 | bool | `false` | |
|
||||
| global.enableIstioAPI | bool | `true` | If true, Higress Controller will monitor istio resources as well |
|
||||
| global.enableLDSCache | bool | `false` | |
|
||||
| global.enableProxyProtocol | bool | `false` | |
|
||||
| global.enablePushAllMCPClusters | bool | `true` | |
|
||||
| global.enableSRDS | bool | `true` | |
|
||||
| global.enableStatus | bool | `true` | If true, Higress Controller will update the status field of Ingress resources. When migrating from Nginx Ingress, in order to avoid status field of Ingress objects being overwritten, this parameter needs to be set to false, so Higress won't write the entry IP to the status field of the corresponding Ingress object. |
|
||||
| global.externalIstiod | bool | `false` | Configure a remote cluster data plane controlled by an external istiod. When set to true, istiod is not deployed locally and only a subset of the other discovery charts are enabled. |
|
||||
@@ -175,7 +177,7 @@ The command removes all the Kubernetes components associated with the chart and
|
||||
| global.istiod | object | `{"enableAnalysis":false}` | Enabled by default in master for maximising testing. |
|
||||
| global.jwtPolicy | string | `"third-party-jwt"` | Configure the policy for validating JWT. Currently, two options are supported: "third-party-jwt" and "first-party-jwt". |
|
||||
| global.kind | bool | `false` | |
|
||||
| global.liteMetrics | bool | `true` | |
|
||||
| global.liteMetrics | bool | `false` | |
|
||||
| global.local | bool | `false` | When deploying to a local cluster (e.g.: kind cluster), set this to true. |
|
||||
| global.logAsJson | bool | `false` | |
|
||||
| global.logging | object | `{"level":"default:info"}` | Comma-separated minimum per-scope logging level of messages to output, in the form of <scope>:<level>,<scope>:<level> The control plane has different scopes depending on component, but can configure default log level across all components If empty, default scope and level will be used as configured in code |
|
||||
|
||||
188
helm/higress/README.zh.md
Normal file
188
helm/higress/README.zh.md
Normal file
@@ -0,0 +1,188 @@
|
||||
## Higress for Kubernetes
|
||||
|
||||
Higress 是基于阿里巴巴内部网关实践构建的云原生 API 网关。
|
||||
|
||||
依托 Istio 和 Envoy,Higress 实现了流量网关、微服务网关和安全网关三重架构的融合,从而大幅降低了部署、运维成本。
|
||||
|
||||
## 设置仓库信息
|
||||
|
||||
```console
|
||||
helm repo add higress.io https://higress.io/helm-charts
|
||||
helm repo update
|
||||
```
|
||||
|
||||
## 安装
|
||||
|
||||
以 `higress` 为发布名称安装 chart:
|
||||
|
||||
```console
|
||||
helm install higress -n higress-system higress.io/higress --create-namespace --render-subchart-notes
|
||||
```
|
||||
|
||||
## 卸载
|
||||
|
||||
要卸载/删除 higress 部署:
|
||||
|
||||
```console
|
||||
helm delete higress -n higress-system
|
||||
```
|
||||
|
||||
该命令会移除与 chart 相关的所有 Kubernetes 组件,并删除发布。
|
||||
|
||||
## 参数
|
||||
|
||||
## 值
|
||||
|
||||
| 键 | 类型 | 默认值 | 描述 |
|
||||
|-----|------|---------|-------------|
|
||||
| clusterName | 字符串 | `""` | |
|
||||
| controller.affinity | 对象 | `{}` | |
|
||||
| controller.automaticHttps.email | 字符串 | `""` | |
|
||||
| controller.automaticHttps.enabled | 布尔值 | `true` | |
|
||||
| controller.autoscaling.enabled | 布尔值 | `false` | |
|
||||
| controller.autoscaling.maxReplicas | 整数 | `5` | |
|
||||
| controller.autoscaling.minReplicas | 整数 | `1` | |
|
||||
| controller.autoscaling.targetCPUUtilizationPercentage | 整数 | `80` | |
|
||||
| controller.env | 对象 | `{}` | |
|
||||
| controller.hub | 字符串 | `"higress-registry.cn-hangzhou.cr.aliyuncs.com/higress"` | |
|
||||
| controller.image | 字符串 | `"higress"` | |
|
||||
| controller.imagePullSecrets | 列表 | `[]` | |
|
||||
| controller.labels | 对象 | `{}` | |
|
||||
| controller.name | 字符串 | `"higress-controller"` | |
|
||||
| controller.nodeSelector | 对象 | `{}` | |
|
||||
| controller.podAnnotations | 对象 | `{}` | |
|
||||
| controller.podSecurityContext | 对象 | `{}` | |
|
||||
| controller.ports[0].name | 字符串 | `"http"` | |
|
||||
| controller.ports[0].port | 整数 | `8888` | |
|
||||
| controller.ports[0].protocol | 字符串 | `"TCP"` | |
|
||||
| controller.ports[0].targetPort | 整数 | `8888` | |
|
||||
| controller.ports[1].name | 字符串 | `"http-solver"` | |
|
||||
| controller.ports[1].port | 整数 | `8889` | |
|
||||
| controller.ports[1].protocol | 字符串 | `"TCP"` | |
|
||||
| controller.ports[1].targetPort | 整数 | `8889` | |
|
||||
| controller.ports[2].name | 字符串 | `"grpc"` | |
|
||||
| controller.ports[2].port | 整数 | `15051` | |
|
||||
| controller.ports[2].protocol | 字符串 | `"TCP"` | |
|
||||
| controller.ports[2].targetPort | 整数 | `15051` | |
|
||||
| controller.probe.httpGet.path | 字符串 | `"/ready"` | |
|
||||
| controller.probe.httpGet.port | 整数 | `8888` | |
|
||||
| controller.probe.initialDelaySeconds | 整数 | `1` | |
|
||||
| controller.probe.periodSeconds | 整数 | `3` | |
|
||||
| controller.probe.timeoutSeconds | 整数 | `5` | |
|
||||
| controller.rbac.create | 布尔值 | `true` | |
|
||||
| controller.replicas | 整数 | `1` | Higress Controller 的 Pod 数量 |
|
||||
| controller.resources.limits.cpu | 字符串 | `"1000m"` | |
|
||||
| controller.resources.limits.memory | 字符串 | `"2048Mi"` | |
|
||||
| controller.resources.requests.cpu | 字符串 | `"500m"` | |
|
||||
| controller.resources.requests.memory | 字符串 | `"2048Mi"` | |
|
||||
| controller.securityContext | 对象 | `{}` | |
|
||||
| controller.service.type | 字符串 | `"ClusterIP"` | |
|
||||
| controller.serviceAccount.annotations | 对象 | `{}` | 添加到服务账户的注解 |
|
||||
| controller.serviceAccount.create | 布尔值 | `true` | 指定是否创建服务账户 |
|
||||
| controller.serviceAccount.name | 字符串 | `""` | 如果未设置且 create 为 true,则使用 fullname 模板生成名称 |
|
||||
| controller.tag | 字符串 | `""` | |
|
||||
| controller.tolerations | 列表 | `[]` | |
|
||||
| downstream | 对象 | `{"connectionBufferLimits":32768,"http2":{"initialConnectionWindowSize":1048576,"initialStreamWindowSize":65535,"maxConcurrentStreams":100},"idleTimeout":180,"maxRequestHeadersKb":60,"routeTimeout":0}` | 下游配置设置 |
|
||||
| gateway.affinity | 对象 | `{}` | |
|
||||
| gateway.annotations | 对象 | `{}` | 应用到所有资源的注解 |
|
||||
| gateway.autoscaling.enabled | 布尔值 | `false` | |
|
||||
| gateway.autoscaling.maxReplicas | 整数 | `5` | |
|
||||
| gateway.autoscaling.minReplicas | 整数 | `1` | |
|
||||
| gateway.autoscaling.targetCPUUtilizationPercentage | 整数 | `80` | |
|
||||
| gateway.containerSecurityContext | 字符串 | `nil` | |
|
||||
| gateway.env | 对象 | `{}` | Pod 环境变量 |
|
||||
| gateway.hostNetwork | 布尔值 | `false` | |
|
||||
| gateway.httpPort | 整数 | `80` | |
|
||||
| gateway.httpsPort | 整数 | `443` | |
|
||||
| gateway.hub | 字符串 | `"higress-registry.cn-hangzhou.cr.aliyuncs.com/higress"` | |
|
||||
| gateway.image | 字符串 | `"gateway"` | |
|
||||
| gateway.kind | 字符串 | `"Deployment"` | 使用 `DaemonSet` 或 `Deployment` |
|
||||
| gateway.labels | 对象 | `{}` | 应用到所有资源的标签 |
|
||||
| gateway.metrics.enabled | 布尔值 | `false` | 如果为 true,则为网关创建 PodMonitor 或 VMPodScrape |
|
||||
| gateway.metrics.honorLabels | 布尔值 | `false` | |
|
||||
| gateway.metrics.interval | 字符串 | `""` | |
|
||||
| gateway.metrics.metricRelabelConfigs | 列表 | `[]` | 用于 operator.victoriametrics.com/v1beta1.VMPodScrape |
|
||||
| gateway.metrics.metricRelabelings | 列表 | `[]` | 用于 monitoring.coreos.com/v1.PodMonitor |
|
||||
| gateway.metrics.provider | 字符串 | `"monitoring.coreos.com"` | CustomResourceDefinition 的提供者组名,可以是 monitoring.coreos.com 或 operator.victoriametrics.com |
|
||||
| gateway.metrics.rawSpec | 对象 | `{}` | 更多原始的 podMetricsEndpoints 规范 |
|
||||
| gateway.metrics.relabelConfigs | 列表 | `[]` | |
|
||||
| gateway.metrics.relabelings | 列表 | `[]` | |
|
||||
| gateway.metrics.scrapeTimeout | 字符串 | `""` | |
|
||||
| gateway.name | 字符串 | `"higress-gateway"` | |
|
||||
| gateway.networkGateway | 字符串 | `""` | 如果指定,网关将作为给定网络的网络网关。 |
|
||||
| gateway.nodeSelector | 对象 | `{}` | |
|
||||
| gateway.podAnnotations."prometheus.io/path" | 字符串 | `"/stats/prometheus"` | |
|
||||
| gateway.podAnnotations."prometheus.io/port" | 字符串 | `"15020"` | |
|
||||
| gateway.podAnnotations."prometheus.io/scrape" | 字符串 | `"true"` | |
|
||||
| gateway.podAnnotations."sidecar.istio.io/inject" | 字符串 | `"false"` | |
|
||||
| gateway.rbac.enabled | 布尔值 | `true` | 如果启用,将创建角色以启用从网关访问证书。当使用 http://gateway-api.org/ 时不需要。 |
|
||||
| gateway.readinessFailureThreshold | 整数 | `30` | 指示准备失败前的连续失败探测次数。 |
|
||||
| gateway.readinessInitialDelaySeconds | 整数 | `1` | 准备探测的初始延迟秒数。 |
|
||||
| gateway.readinessPeriodSeconds | 整数 | `2` | 准备探测之间的间隔。 |
|
||||
| gateway.readinessSuccessThreshold | 整数 | `1` | 指示准备成功前的连续成功探测次数。 |
|
||||
| gateway.readinessTimeoutSeconds | 整数 | `3` | 准备探测的超时秒数 |
|
||||
| gateway.replicas | 整数 | `2` | Higress Gateway 的 Pod 数量 |
|
||||
| gateway.resources.limits.cpu | 字符串 | `"2000m"` | |
|
||||
| gateway.resources.limits.memory | 字符串 | `"2048Mi"` | |
|
||||
| gateway.resources.requests.cpu | 字符串 | `"2000m"` | |
|
||||
| gateway.resources.requests.memory | 字符串 | `"2048Mi"` | |
|
||||
| gateway.revision | 字符串 | `""` | 修订声明此网关属于哪个修订 |
|
||||
| gateway.rollingMaxSurge | 字符串 | `"100%"` | |
|
||||
| gateway.rollingMaxUnavailable | 字符串 | `"25%"` | |
|
||||
| gateway.securityContext | 字符串 | `nil` | 定义 Pod 的安全上下文。如果未设置,将自动设置为绑定到端口 80 和 443 所需的最小权限。在 Kubernetes 1.22+ 上,这只需要 `net.ipv4.ip_unprivileged_port_start` 系统调用。 |
|
||||
| gateway.service.annotations | 对象 | `{}` | |
|
||||
| gateway.service.externalTrafficPolicy | 字符串 | `""` | |
|
||||
| gateway.service.loadBalancerClass | 字符串 | `""` | |
|
||||
| gateway.service.loadBalancerIP | 字符串 | `""` | |
|
||||
| gateway.service.loadBalancerSourceRanges | 列表 | `[]` | |
|
||||
| gateway.service.ports[0].name | 字符串 | `"http2"` | |
|
||||
| gateway.service.ports[0].port | 整数 | `80` | |
|
||||
| gateway.service.ports[0].protocol | 字符串 | `"TCP"` | |
|
||||
| gateway.service.ports[0].targetPort | 整数 | `80` | |
|
||||
| gateway.service.ports[1].name | 字符串 | `"https"` | |
|
||||
| gateway.service.ports[1].port | 整数 | `443` | |
|
||||
| gateway.service.ports[1].protocol | 字符串 | `"TCP"` | |
|
||||
| gateway.service.ports[1].targetPort | 整数 | `443` | |
|
||||
| gateway.service.type | 字符串 | `"LoadBalancer"` | 服务类型。设置为 "None" 以完全禁用服务 |
|
||||
| gateway.serviceAccount.annotations | 对象 | `{}` | 添加到服务账户的注解 |
|
||||
| gateway.serviceAccount.create | 布尔值 | `true` | 如果设置,将创建服务账户。否则,使用默认值 |
|
||||
| gateway.serviceAccount.name | 字符串 | `""` | 要使用的服务账户名称。如果未设置,则使用发布名称 |
|
||||
| gateway.tag | 字符串 | `""` | |
|
||||
| gateway.tolerations | 列表 | `[]` | |
|
||||
| gateway.unprivilegedPortSupported | 字符串 | `nil` | |
|
||||
| global.autoscalingv2API | 布尔值 | `true` | 是否使用 autoscaling/v2 模板进行 HPA 设置,仅供内部使用,用户不应配置。 |
|
||||
| global.caAddress | 字符串 | `""` | 自定义的 CA 地址,用于为集群中的 Pod 检索证书。CSR 客户端(如 Istio Agent 和 ingress gateways)可以使用此地址指定 CA 端点。如果未明确设置,则默认为 Istio 发现地址。 |
|
||||
| global.caName | 字符串 | `""` | 工作负载证书的 CA 名称。例如,当 caName=GkeWorkloadCertificate 时,GKE 工作负载证书将用作工作负载的证书。默认值为 "",当 caName="" 时,CA 将通过其他机制(如环境变量 CA_PROVIDER)配置。 |
|
||||
| global.configCluster | 布尔值 | `false` | 将远程集群配置为外部 istiod 的配置集群。 |
|
||||
| global.defaultPodDisruptionBudget | 对象 | `{"enabled":false}` | 为控制平面启用 Pod 中断预算,用于确保 Istio 控制平面组件逐步升级或恢复。 |
|
||||
| global.defaultResources | 对象 | `{"requests":{"cpu":"10m"}}` | 应用于所有部署的最小请求资源集,以便 Horizontal Pod Autoscaler 能够正常工作(如果设置)。每个组件可以通过在相关部分添加自己的资源块并设置所需的资源值来覆盖这些默认值。 |
|
||||
| global.defaultUpstreamConcurrencyThreshold | 整数 | `10000` | |
|
||||
| global.disableAlpnH2 | 布尔值 | `false` | 是否在 ALPN 中禁用 HTTP/2 |
|
||||
| global.enableGatewayAPI | 布尔值 | `false` | 如果为 true,Higress Controller 还将监控 Gateway API 资源 |
|
||||
| global.enableH3 | 布尔值 | `false` | |
|
||||
| global.enableIPv6 | 布尔值 | `false` | |
|
||||
| global.enableIstioAPI | 布尔值 | `true` | 如果为 true,Higress Controller 还将监控 istio 资源 |
|
||||
| global.enableLDSCache | 布尔值 | `true` | |
|
||||
| global.enableProxyProtocol | 布尔值 | `false` | |
|
||||
| global.enablePushAllMCPClusters | 布尔值 | `true` | |
|
||||
| global.enableSRDS | 布尔值 | `true` | |
|
||||
| global.enableStatus | 布尔值 | `true` | 如果为 true,Higress Controller 将更新 Ingress 资源的状态字段。从 Nginx Ingress 迁移时,为了避免 Ingress 对象的状态字段被覆盖,需要将此参数设置为 false,以便 Higress 不会将入口 IP 写入相应 Ingress 对象的状态字段。 |
|
||||
| global.externalIstiod | 布尔值 | `false` | 配置由外部 istiod 控制的远程集群数据平面。当设置为 true 时,本地不部署 istiod,仅启用其他发现 chart 的子集。 |
|
||||
| global.hostRDSMergeSubset | 布尔值 | `false` | |
|
||||
| global.hub | 字符串 | `"higress-registry.cn-hangzhou.cr.aliyuncs.com/higress"` | Istio 镜像的默认仓库。发布版本发布到 docker hub 的 'istio' 项目下。来自 prow 的开发构建位于 gcr.io |
|
||||
| global.imagePullPolicy | 字符串 | `""` | 如果不需要默认行为,则指定镜像拉取策略。默认行为:最新镜像将始终拉取,否则 IfNotPresent。 |
|
||||
| global.imagePullSecrets | 列表 | `[]` | 所有 ServiceAccount 的 ImagePullSecrets,用于引用此 ServiceAccount 的 Pod 拉取任何镜像的同一命名空间中的秘密列表。对于不使用 ServiceAccount 的组件(即 grafana、servicegraph、tracing),ImagePullSecrets 将添加到相应的 Deployment(StatefulSet) 对象中。对于配置了私有 docker 注册表的任何集群,必须设置。 |
|
||||
| global.ingressClass | 字符串 | `"higress"` | IngressClass 过滤 higress controller 监听的 ingress 资源。默认的 ingress class 是 higress。有一些特殊情况用于特殊的 ingress class。1. 当 ingress class 设置为 nginx 时,higress controller 将监听带有 nginx ingress class 或没有任何 ingress class 的 ingress 资源。2. 当 ingress class 设置为空时,higress controller 将监听 k8s 集群中的所有 ingress 资源。 |
|
||||
| global.istioNamespace | 字符串 | `"istio-system"` | 用于定位 istiod。 |
|
||||
| global.istiod | 对象 | `{"enableAnalysis":false}` | 默认在主分支中启用以最大化测试。 |
|
||||
| global.jwtPolicy | 字符串 | `"third-party-jwt"` | 配置验证 JWT 的策略。目前支持两个选项:"third-party-jwt" 和 "first-party-jwt"。 |
|
||||
| global.kind | 布尔值 | `false` | |
|
||||
| global.liteMetrics | 布尔值 | `false` | |
|
||||
| global.local | 布尔值 | `false` | 当部署到本地集群(如:kind 集群)时,将此设置为 true。 |
|
||||
| global.logAsJson | 布尔值 | `false` | |
|
||||
| global.logging | 对象 | `{"level":"default:info"}` | 以逗号分隔的每个范围的最小日志级别,格式为 <scope>:<level>,<scope>:<level> 控制平面根据组件不同有不同的范围,但可以配置所有组件的默认日志级别 如果为空,将使用代码中配置的默认范围和级别 |
|
||||
| global.meshID | 字符串 | `""` | 如果网格管理员未指定值,Istio 将使用网格的信任域的值。最佳实践是选择一个合适的信任域值。 |
|
||||
| global.meshNetworks | 对象 | `{}` | |
|
||||
| global.mountMtlsCerts | 布尔值 | `false` | 使用用户指定的、挂载的密钥和证书用于 Pilot 和工作负载。 |
|
||||
| global.multiCluster.clusterName | 字符串 | `""` | 应设置为此安装运行的集群的名称。这是为了正确标记代理的 sidecar 注入所必需的 |
|
||||
| global.multiCluster.enabled | 布尔值 | `true` | 设置为 true 以通过各自的 ingressgateway 服务连接两个 kubernetes 集群,当每个集群中的 Pod 无法直接相互通信时。
|
||||
Submodule istio/istio updated: 1dbd773596...5a5aa495c7
Submodule istio/proxy updated: 2a5416fcfa...8a85c12b89
@@ -41,11 +41,11 @@ import (
|
||||
"istio.io/istio/pkg/config/schema/kind"
|
||||
"istio.io/istio/pkg/keepalive"
|
||||
istiokube "istio.io/istio/pkg/kube"
|
||||
"istio.io/istio/pkg/log"
|
||||
"istio.io/istio/pkg/security"
|
||||
"istio.io/istio/security/pkg/server/ca/authenticate"
|
||||
"istio.io/istio/security/pkg/server/ca/authenticate/kubeauth"
|
||||
"istio.io/pkg/ledger"
|
||||
"istio.io/pkg/log"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
|
||||
|
||||
@@ -173,7 +173,7 @@ func (s *CertMgr) Reconcile(ctx context.Context, oldConfig *Config, newConfig *C
|
||||
s.cache.Start()
|
||||
// sync domains
|
||||
s.configMgr.SetConfig(newConfig)
|
||||
CertLog.Infof("certMgr start to manageSync domains:+v%", newDomains)
|
||||
CertLog.Infof("certMgr start to manageSync domains: %+v", newDomains)
|
||||
s.manageSync(context.Background(), newDomains)
|
||||
CertLog.Infof("certMgr manageSync domains done")
|
||||
} else {
|
||||
|
||||
@@ -14,6 +14,6 @@
|
||||
|
||||
package cert
|
||||
|
||||
import "istio.io/pkg/log"
|
||||
import "istio.io/istio/pkg/log"
|
||||
|
||||
var CertLog = log.RegisterScope("cert", "Higress Cert process.", 0)
|
||||
var CertLog = log.RegisterScope("cert", "Higress Cert process.")
|
||||
|
||||
@@ -25,7 +25,7 @@ import (
|
||||
"istio.io/istio/pkg/config/constants"
|
||||
"istio.io/istio/pkg/env"
|
||||
"istio.io/istio/pkg/keepalive"
|
||||
"istio.io/pkg/log"
|
||||
"istio.io/istio/pkg/log"
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
@@ -303,21 +303,21 @@ func (m *IngressConfig) listFromIngressControllers(typ config.GroupVersionKind,
|
||||
common.SortIngressByCreationTime(configs)
|
||||
wrapperConfigs := m.createWrapperConfigs(configs)
|
||||
|
||||
IngressLog.Infof("resource type %s, configs number %d", typ, len(wrapperConfigs))
|
||||
var result []config.Config
|
||||
switch typ {
|
||||
case gvk.Gateway:
|
||||
return m.convertGateways(wrapperConfigs)
|
||||
result = m.convertGateways(wrapperConfigs)
|
||||
case gvk.VirtualService:
|
||||
return m.convertVirtualService(wrapperConfigs)
|
||||
result = m.convertVirtualService(wrapperConfigs)
|
||||
case gvk.DestinationRule:
|
||||
return m.convertDestinationRule(wrapperConfigs)
|
||||
result = m.convertDestinationRule(wrapperConfigs)
|
||||
case gvk.ServiceEntry:
|
||||
return m.convertServiceEntry(wrapperConfigs)
|
||||
result = m.convertServiceEntry(wrapperConfigs)
|
||||
case gvk.WasmPlugin:
|
||||
return m.convertWasmPlugin(wrapperConfigs)
|
||||
result = m.convertWasmPlugin(wrapperConfigs)
|
||||
}
|
||||
|
||||
return nil
|
||||
IngressLog.Infof("resource type %s, ingress number %d, convert configs number %d", typ, len(configs), len(result))
|
||||
return result
|
||||
}
|
||||
|
||||
func (m *IngressConfig) listFromGatewayControllers(typ config.GroupVersionKind, namespace string) []config.Config {
|
||||
@@ -712,7 +712,6 @@ func (m *IngressConfig) convertDestinationRule(configs []common.WrapperConfig) [
|
||||
|
||||
if m.RegistryReconciler != nil {
|
||||
drws := m.RegistryReconciler.GetAllDestinationRuleWrapper()
|
||||
IngressLog.Infof("Found mcp destinationRules: %v", drws)
|
||||
for _, destinationRuleWrapper := range drws {
|
||||
serviceName := destinationRuleWrapper.ServiceKey.ServiceFQDN
|
||||
dr, exist := destinationRules[serviceName]
|
||||
@@ -882,7 +881,7 @@ func (m *IngressConfig) convertIstioWasmPlugin(obj *higressext.WasmPlugin) (*ext
|
||||
if result.PluginConfig != nil {
|
||||
return result, nil
|
||||
}
|
||||
if !obj.DefaultConfigDisable {
|
||||
if !isBoolValueTrue(obj.DefaultConfigDisable) {
|
||||
result.PluginConfig = obj.DefaultConfig
|
||||
}
|
||||
hasValidRule := false
|
||||
@@ -894,7 +893,7 @@ func (m *IngressConfig) convertIstioWasmPlugin(obj *higressext.WasmPlugin) (*ext
|
||||
}
|
||||
var ruleValues []*_struct.Value
|
||||
for _, rule := range obj.MatchRules {
|
||||
if rule.ConfigDisable {
|
||||
if isBoolValueTrue(rule.ConfigDisable) {
|
||||
continue
|
||||
}
|
||||
if rule.Config == nil {
|
||||
@@ -906,6 +905,7 @@ func (m *IngressConfig) convertIstioWasmPlugin(obj *higressext.WasmPlugin) (*ext
|
||||
StructValue: rule.Config,
|
||||
}
|
||||
|
||||
validRule := false
|
||||
var matchItems []*_struct.Value
|
||||
// match ingress
|
||||
for _, ing := range rule.Ingress {
|
||||
@@ -916,6 +916,7 @@ func (m *IngressConfig) convertIstioWasmPlugin(obj *higressext.WasmPlugin) (*ext
|
||||
})
|
||||
}
|
||||
if len(matchItems) > 0 {
|
||||
validRule = true
|
||||
v.StructValue.Fields["_match_route_"] = &_struct.Value{
|
||||
Kind: &_struct.Value_ListValue{
|
||||
ListValue: &_struct.ListValue{
|
||||
@@ -923,12 +924,9 @@ func (m *IngressConfig) convertIstioWasmPlugin(obj *higressext.WasmPlugin) (*ext
|
||||
},
|
||||
},
|
||||
}
|
||||
ruleValues = append(ruleValues, &_struct.Value{
|
||||
Kind: v,
|
||||
})
|
||||
continue
|
||||
}
|
||||
// match service
|
||||
matchItems = nil
|
||||
for _, service := range rule.Service {
|
||||
matchItems = append(matchItems, &_struct.Value{
|
||||
Kind: &_struct.Value_StringValue{
|
||||
@@ -937,6 +935,7 @@ func (m *IngressConfig) convertIstioWasmPlugin(obj *higressext.WasmPlugin) (*ext
|
||||
})
|
||||
}
|
||||
if len(matchItems) > 0 {
|
||||
validRule = true
|
||||
v.StructValue.Fields["_match_service_"] = &_struct.Value{
|
||||
Kind: &_struct.Value_ListValue{
|
||||
ListValue: &_struct.ListValue{
|
||||
@@ -944,12 +943,9 @@ func (m *IngressConfig) convertIstioWasmPlugin(obj *higressext.WasmPlugin) (*ext
|
||||
},
|
||||
},
|
||||
}
|
||||
ruleValues = append(ruleValues, &_struct.Value{
|
||||
Kind: v,
|
||||
})
|
||||
continue
|
||||
}
|
||||
// match domain
|
||||
matchItems = nil
|
||||
for _, domain := range rule.Domain {
|
||||
matchItems = append(matchItems, &_struct.Value{
|
||||
Kind: &_struct.Value_StringValue{
|
||||
@@ -957,19 +953,23 @@ func (m *IngressConfig) convertIstioWasmPlugin(obj *higressext.WasmPlugin) (*ext
|
||||
},
|
||||
})
|
||||
}
|
||||
if len(matchItems) == 0 {
|
||||
if len(matchItems) > 0 {
|
||||
validRule = true
|
||||
v.StructValue.Fields["_match_domain_"] = &_struct.Value{
|
||||
Kind: &_struct.Value_ListValue{
|
||||
ListValue: &_struct.ListValue{
|
||||
Values: matchItems,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
if validRule {
|
||||
ruleValues = append(ruleValues, &_struct.Value{
|
||||
Kind: v,
|
||||
})
|
||||
} else {
|
||||
return nil, fmt.Errorf("invalid match rule has no match condition, rule:%v", rule)
|
||||
}
|
||||
v.StructValue.Fields["_match_domain_"] = &_struct.Value{
|
||||
Kind: &_struct.Value_ListValue{
|
||||
ListValue: &_struct.ListValue{
|
||||
Values: matchItems,
|
||||
},
|
||||
},
|
||||
}
|
||||
ruleValues = append(ruleValues, &_struct.Value{
|
||||
Kind: v,
|
||||
})
|
||||
}
|
||||
if len(ruleValues) > 0 {
|
||||
hasValidRule = true
|
||||
@@ -982,13 +982,17 @@ func (m *IngressConfig) convertIstioWasmPlugin(obj *higressext.WasmPlugin) (*ext
|
||||
}
|
||||
}
|
||||
}
|
||||
if !hasValidRule && obj.DefaultConfigDisable {
|
||||
if !hasValidRule && isBoolValueTrue(obj.DefaultConfigDisable) {
|
||||
return nil, nil
|
||||
}
|
||||
return result, nil
|
||||
|
||||
}
|
||||
|
||||
func isBoolValueTrue(b *wrappers.BoolValue) bool {
|
||||
return b != nil && b.Value
|
||||
}
|
||||
|
||||
func (m *IngressConfig) AddOrUpdateWasmPlugin(clusterNamespacedName util.ClusterNamespacedName) {
|
||||
if clusterNamespacedName.Namespace != m.namespace {
|
||||
return
|
||||
|
||||
@@ -493,7 +493,7 @@ func (m *KIngressConfig) HasSynced() bool {
|
||||
defer m.mutex.RUnlock()
|
||||
|
||||
for _, remoteIngressController := range m.remoteIngressControllers {
|
||||
IngressLog.Info("In Kingress Synced.", remoteIngressController)
|
||||
IngressLog.Info("In Kingress Synced.")
|
||||
if !remoteIngressController.HasSynced() {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -15,12 +15,6 @@
|
||||
package annotations
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/util"
|
||||
. "github.com/alibaba/higress/pkg/ingress/log"
|
||||
)
|
||||
@@ -57,101 +51,10 @@ func (a auth) Parse(annotations Annotations, config *Ingress, globalContext *Glo
|
||||
if !needAuthConfig(annotations) {
|
||||
return nil
|
||||
}
|
||||
|
||||
authConfig := &AuthConfig{
|
||||
AuthType: defaultAuthType,
|
||||
}
|
||||
|
||||
// Check auth type
|
||||
authType, err := annotations.ParseStringASAP(authType)
|
||||
if err != nil {
|
||||
IngressLog.Errorf("Parse auth type error %v within ingress %/%s", err, config.Namespace, config.Name)
|
||||
return nil
|
||||
}
|
||||
if authType != defaultAuthType {
|
||||
IngressLog.Errorf("Auth type %s within ingress %/%s is not supported yet.", authType, config.Namespace, config.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
secretName, _ := annotations.ParseStringASAP(authSecretAnn)
|
||||
namespaced := util.SplitNamespacedName(secretName)
|
||||
if namespaced.Name == "" {
|
||||
IngressLog.Errorf("Auth secret name within ingress %s/%s is invalid", config.Namespace, config.Name)
|
||||
return nil
|
||||
}
|
||||
if namespaced.Namespace == "" {
|
||||
namespaced.Namespace = config.Namespace
|
||||
}
|
||||
|
||||
configKey := util.ClusterNamespacedName{
|
||||
NamespacedName: namespaced,
|
||||
ClusterId: config.ClusterId,
|
||||
}
|
||||
authConfig.AuthSecret = configKey
|
||||
|
||||
// Subscribe secret
|
||||
globalContext.WatchedSecrets.Insert(configKey.String())
|
||||
|
||||
secretType := authFileAuthSecretType
|
||||
if rawSecretType, err := annotations.ParseStringASAP(authSecretTypeAnn); err == nil {
|
||||
resultAuthSecretType := authSecretType(rawSecretType)
|
||||
if resultAuthSecretType == authFileAuthSecretType || resultAuthSecretType == authMapAuthSecretType {
|
||||
secretType = resultAuthSecretType
|
||||
}
|
||||
}
|
||||
|
||||
authConfig.AuthRealm, _ = annotations.ParseStringASAP(authRealm)
|
||||
|
||||
// Process credentials.
|
||||
secretLister, exist := globalContext.ClusterSecretLister[config.ClusterId]
|
||||
if !exist {
|
||||
IngressLog.Errorf("secret lister of cluster %s doesn't exist", config.ClusterId)
|
||||
return nil
|
||||
}
|
||||
authSecret, err := secretLister.Secrets(namespaced.Namespace).Get(namespaced.Name)
|
||||
if err != nil {
|
||||
IngressLog.Errorf("Secret %s within ingress %s/%s is not found",
|
||||
namespaced.String(), config.Namespace, config.Name)
|
||||
return nil
|
||||
}
|
||||
credentials, err := convertCredentials(secretType, authSecret)
|
||||
if err != nil {
|
||||
IngressLog.Errorf("Parse auth secret fail, err %v", err)
|
||||
return nil
|
||||
}
|
||||
authConfig.Credentials = credentials
|
||||
|
||||
config.Auth = authConfig
|
||||
IngressLog.Error("The annotation nginx.ingress.kubernetes.io/auth-type is no longer supported after version 2.0.0, please use the higress wasm plugin (e.g., basic-auth) as an alternative.")
|
||||
return nil
|
||||
}
|
||||
|
||||
func convertCredentials(secretType authSecretType, secret *corev1.Secret) ([]string, error) {
|
||||
var result []string
|
||||
switch secretType {
|
||||
case authFileAuthSecretType:
|
||||
users, exist := secret.Data[authFileKey]
|
||||
if !exist {
|
||||
return nil, errors.New("the auth file type must has auth key in secret data")
|
||||
}
|
||||
userList := strings.Split(string(users), "\n")
|
||||
for _, item := range userList {
|
||||
if !strings.Contains(item, ":") {
|
||||
continue
|
||||
}
|
||||
result = append(result, item)
|
||||
}
|
||||
case authMapAuthSecretType:
|
||||
for name, password := range secret.Data {
|
||||
result = append(result, name+":"+string(password))
|
||||
}
|
||||
}
|
||||
sort.SliceStable(result, func(i, j int) bool {
|
||||
return result[i] < result[j]
|
||||
})
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func needAuthConfig(annotations Annotations) bool {
|
||||
return annotations.HasASAP(authType) &&
|
||||
annotations.HasASAP(authSecretAnn)
|
||||
|
||||
@@ -1,197 +0,0 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package annotations
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"istio.io/istio/pkg/cluster"
|
||||
"istio.io/istio/pkg/util/sets"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/informers"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
listerv1 "k8s.io/client-go/listers/core/v1"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/util"
|
||||
)
|
||||
|
||||
func TestAuthParse(t *testing.T) {
|
||||
auth := auth{}
|
||||
inputCases := []struct {
|
||||
input map[string]string
|
||||
secret *v1.Secret
|
||||
expect *AuthConfig
|
||||
watchedSecret string
|
||||
}{
|
||||
{
|
||||
secret: &v1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "bar",
|
||||
Namespace: "foo",
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"auth": []byte("A:a\nB:b"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: map[string]string{
|
||||
buildNginxAnnotationKey(authType): "digest",
|
||||
},
|
||||
expect: nil,
|
||||
secret: &v1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "bar",
|
||||
Namespace: "foo",
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"auth": []byte("A:a\nB:b"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: map[string]string{
|
||||
buildNginxAnnotationKey(authType): defaultAuthType,
|
||||
buildHigressAnnotationKey(authSecretAnn): "foo/bar",
|
||||
},
|
||||
secret: &v1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "bar",
|
||||
Namespace: "foo",
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"auth": []byte("A:a\nB:b"),
|
||||
},
|
||||
},
|
||||
expect: &AuthConfig{
|
||||
AuthType: defaultAuthType,
|
||||
AuthSecret: util.ClusterNamespacedName{
|
||||
NamespacedName: types.NamespacedName{
|
||||
Namespace: "foo",
|
||||
Name: "bar",
|
||||
},
|
||||
ClusterId: "cluster",
|
||||
},
|
||||
Credentials: []string{"A:a", "B:b"},
|
||||
},
|
||||
watchedSecret: "cluster/foo/bar",
|
||||
},
|
||||
{
|
||||
input: map[string]string{
|
||||
buildNginxAnnotationKey(authType): defaultAuthType,
|
||||
buildHigressAnnotationKey(authSecretAnn): "foo/bar",
|
||||
buildNginxAnnotationKey(authSecretTypeAnn): string(authMapAuthSecretType),
|
||||
},
|
||||
secret: &v1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "bar",
|
||||
Namespace: "foo",
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"A": []byte("a"),
|
||||
"B": []byte("b"),
|
||||
},
|
||||
},
|
||||
expect: &AuthConfig{
|
||||
AuthType: defaultAuthType,
|
||||
AuthSecret: util.ClusterNamespacedName{
|
||||
NamespacedName: types.NamespacedName{
|
||||
Namespace: "foo",
|
||||
Name: "bar",
|
||||
},
|
||||
ClusterId: "cluster",
|
||||
},
|
||||
Credentials: []string{"A:a", "B:b"},
|
||||
},
|
||||
watchedSecret: "cluster/foo/bar",
|
||||
},
|
||||
{
|
||||
input: map[string]string{
|
||||
buildNginxAnnotationKey(authType): defaultAuthType,
|
||||
buildHigressAnnotationKey(authSecretAnn): "bar",
|
||||
buildNginxAnnotationKey(authSecretTypeAnn): string(authFileAuthSecretType),
|
||||
},
|
||||
secret: &v1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "bar",
|
||||
Namespace: "default",
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"auth": []byte("A:a\nB:b"),
|
||||
},
|
||||
},
|
||||
expect: &AuthConfig{
|
||||
AuthType: defaultAuthType,
|
||||
AuthSecret: util.ClusterNamespacedName{
|
||||
NamespacedName: types.NamespacedName{
|
||||
Namespace: "default",
|
||||
Name: "bar",
|
||||
},
|
||||
ClusterId: "cluster",
|
||||
},
|
||||
Credentials: []string{"A:a", "B:b"},
|
||||
},
|
||||
watchedSecret: "cluster/default/bar",
|
||||
},
|
||||
}
|
||||
|
||||
for _, inputCase := range inputCases {
|
||||
t.Run("", func(t *testing.T) {
|
||||
config := &Ingress{
|
||||
Meta: Meta{
|
||||
Namespace: "default",
|
||||
ClusterId: "cluster",
|
||||
},
|
||||
}
|
||||
|
||||
globalContext, cancel := initGlobalContext(inputCase.secret)
|
||||
defer cancel()
|
||||
|
||||
_ = auth.Parse(inputCase.input, config, globalContext)
|
||||
if !reflect.DeepEqual(inputCase.expect, config.Auth) {
|
||||
t.Fatal("Should be equal")
|
||||
}
|
||||
|
||||
if inputCase.watchedSecret != "" {
|
||||
if !globalContext.WatchedSecrets.Contains(inputCase.watchedSecret) {
|
||||
t.Fatalf("Should watch secret %s", inputCase.watchedSecret)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func initGlobalContext(secret *v1.Secret) (*GlobalContext, context.CancelFunc) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
client := fake.NewSimpleClientset(secret)
|
||||
informerFactory := informers.NewSharedInformerFactory(client, time.Hour)
|
||||
secretInformer := informerFactory.Core().V1().Secrets()
|
||||
go secretInformer.Informer().Run(ctx.Done())
|
||||
cache.WaitForCacheSync(ctx.Done(), secretInformer.Informer().HasSynced)
|
||||
|
||||
return &GlobalContext{
|
||||
WatchedSecrets: sets.New[string](),
|
||||
ClusterSecretLister: map[cluster.ID]listerv1.SecretLister{
|
||||
"cluster": secretInformer.Lister(),
|
||||
},
|
||||
}, cancel
|
||||
}
|
||||
@@ -15,6 +15,7 @@
|
||||
package annotations
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
networking "istio.io/api/networking/v1alpha3"
|
||||
@@ -27,9 +28,11 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
authTLSSecret = "auth-tls-secret"
|
||||
sslCipher = "ssl-cipher"
|
||||
gatewaySdsCaSuffix = "-cacert"
|
||||
authTLSSecret = "auth-tls-secret"
|
||||
sslCipher = "ssl-cipher"
|
||||
gatewaySdsCaSuffix = "-cacert"
|
||||
annotationMinTLSVersion = "tls-min-protocol-version"
|
||||
annotationMaxTLSVersion = "tls-max-protocol-version"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -41,6 +44,8 @@ type DownstreamTLSConfig struct {
|
||||
CipherSuites []string
|
||||
Mode networking.ServerTLSSettings_TLSmode
|
||||
CASecretName types.NamespacedName
|
||||
MinVersion string
|
||||
MaxVersion string
|
||||
}
|
||||
|
||||
type downstreamTLS struct{}
|
||||
@@ -82,6 +87,14 @@ func (d downstreamTLS) Parse(annotations Annotations, config *Ingress, _ *Global
|
||||
downstreamTLSConfig.CipherSuites = validCipherSuite
|
||||
}
|
||||
|
||||
if minVersion, err := annotations.ParseStringASAP(annotationMinTLSVersion); err == nil {
|
||||
downstreamTLSConfig.MinVersion = minVersion
|
||||
}
|
||||
|
||||
if maxVersion, err := annotations.ParseStringASAP(annotationMaxTLSVersion); err == nil {
|
||||
downstreamTLSConfig.MaxVersion = maxVersion
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -107,11 +120,44 @@ func (d downstreamTLS) ApplyGateway(gateway *networking.Gateway, config *Ingress
|
||||
if len(downstreamTLSConfig.CipherSuites) != 0 {
|
||||
server.Tls.CipherSuites = downstreamTLSConfig.CipherSuites
|
||||
}
|
||||
|
||||
if downstreamTLSConfig.MinVersion != "" {
|
||||
if version, err := convertTLSVersion(downstreamTLSConfig.MinVersion); err != nil {
|
||||
IngressLog.Errorf("Invalid minimum TLS version: %v", err)
|
||||
} else {
|
||||
server.Tls.MinProtocolVersion = version
|
||||
}
|
||||
}
|
||||
|
||||
if downstreamTLSConfig.MaxVersion != "" {
|
||||
if version, err := convertTLSVersion(downstreamTLSConfig.MaxVersion); err != nil {
|
||||
IngressLog.Errorf("Invalid maximum TLS version: %v", err)
|
||||
} else {
|
||||
server.Tls.MaxProtocolVersion = version
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func needDownstreamTLS(annotations Annotations) bool {
|
||||
return annotations.HasASAP(sslCipher) ||
|
||||
annotations.HasASAP(authTLSSecret)
|
||||
annotations.HasASAP(authTLSSecret) ||
|
||||
annotations.HasASAP(annotationMinTLSVersion) ||
|
||||
annotations.HasASAP(annotationMaxTLSVersion)
|
||||
}
|
||||
|
||||
func convertTLSVersion(version string) (networking.ServerTLSSettings_TLSProtocol, error) {
|
||||
switch version {
|
||||
case "TLSv1.0":
|
||||
return networking.ServerTLSSettings_TLSV1_0, nil
|
||||
case "TLSv1.1":
|
||||
return networking.ServerTLSSettings_TLSV1_1, nil
|
||||
case "TLSv1.2":
|
||||
return networking.ServerTLSSettings_TLSV1_2, nil
|
||||
case "TLSv1.3":
|
||||
return networking.ServerTLSSettings_TLSV1_3, nil
|
||||
}
|
||||
return networking.ServerTLSSettings_TLS_AUTO, fmt.Errorf("invalid TLS version: %s. Valid values are: TLSv1.0, TLSv1.1, TLSv1.2, TLSv1.3", version)
|
||||
}
|
||||
|
||||
@@ -26,11 +26,15 @@ var parser = downstreamTLS{}
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
input map[string]string
|
||||
expect *DownstreamTLSConfig
|
||||
}{
|
||||
{},
|
||||
{
|
||||
name: "empty config",
|
||||
},
|
||||
{
|
||||
name: "ssl cipher only",
|
||||
input: map[string]string{
|
||||
buildNginxAnnotationKey(sslCipher): "ECDHE-RSA-AES256-GCM-SHA384:AES128-SHA",
|
||||
},
|
||||
@@ -40,9 +44,24 @@ func TestParse(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with TLS version config",
|
||||
input: map[string]string{
|
||||
buildNginxAnnotationKey(authTLSSecret): "test",
|
||||
buildNginxAnnotationKey(sslCipher): "ECDHE-RSA-AES256-GCM-SHA384:AES128-SHA",
|
||||
buildNginxAnnotationKey(annotationMinTLSVersion): "TLSv1.2",
|
||||
buildNginxAnnotationKey(annotationMaxTLSVersion): "TLSv1.3",
|
||||
},
|
||||
expect: &DownstreamTLSConfig{
|
||||
Mode: networking.ServerTLSSettings_SIMPLE,
|
||||
MinVersion: "TLSv1.2",
|
||||
MaxVersion: "TLSv1.3",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "complete config",
|
||||
input: map[string]string{
|
||||
buildNginxAnnotationKey(authTLSSecret): "test",
|
||||
buildNginxAnnotationKey(sslCipher): "ECDHE-RSA-AES256-GCM-SHA384:AES128-SHA",
|
||||
buildNginxAnnotationKey(annotationMinTLSVersion): "TLSv1.2",
|
||||
buildNginxAnnotationKey(annotationMaxTLSVersion): "TLSv1.3",
|
||||
},
|
||||
expect: &DownstreamTLSConfig{
|
||||
CASecretName: types.NamespacedName{
|
||||
@@ -51,34 +70,79 @@ func TestParse(t *testing.T) {
|
||||
},
|
||||
Mode: networking.ServerTLSSettings_MUTUAL,
|
||||
CipherSuites: []string{"ECDHE-RSA-AES256-GCM-SHA384", "AES128-SHA"},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: map[string]string{
|
||||
buildHigressAnnotationKey(authTLSSecret): "test/foo",
|
||||
DefaultAnnotationsPrefix + "/" + sslCipher: "ECDHE-RSA-AES256-GCM-SHA384:AES128-SHA",
|
||||
},
|
||||
expect: &DownstreamTLSConfig{
|
||||
CASecretName: types.NamespacedName{
|
||||
Namespace: "test",
|
||||
Name: "foo",
|
||||
},
|
||||
Mode: networking.ServerTLSSettings_MUTUAL,
|
||||
CipherSuites: []string{"ECDHE-RSA-AES256-GCM-SHA384", "AES128-SHA"},
|
||||
MinVersion: "TLSv1.2",
|
||||
MaxVersion: "TLSv1.3",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run("", func(t *testing.T) {
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
config := &Ingress{
|
||||
Meta: Meta{
|
||||
Namespace: "foo",
|
||||
},
|
||||
}
|
||||
_ = parser.Parse(testCase.input, config, nil)
|
||||
if !reflect.DeepEqual(testCase.expect, config.DownstreamTLS) {
|
||||
t.Fatalf("Should be equal")
|
||||
err := parser.Parse(tc.input, config, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Parse failed: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(tc.expect, config.DownstreamTLS) {
|
||||
t.Fatalf("Parse result mismatch:\nExpect: %+v\nGot: %+v", tc.expect, config.DownstreamTLS)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertTLSVersion(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
version string
|
||||
expect networking.ServerTLSSettings_TLSProtocol
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "TLS 1.0",
|
||||
version: "TLSv1.0",
|
||||
expect: networking.ServerTLSSettings_TLSV1_0,
|
||||
},
|
||||
{
|
||||
name: "TLS 1.1",
|
||||
version: "TLSv1.1",
|
||||
expect: networking.ServerTLSSettings_TLSV1_1,
|
||||
},
|
||||
{
|
||||
name: "TLS 1.2",
|
||||
version: "TLSv1.2",
|
||||
expect: networking.ServerTLSSettings_TLSV1_2,
|
||||
},
|
||||
{
|
||||
name: "TLS 1.3",
|
||||
version: "TLSv1.3",
|
||||
expect: networking.ServerTLSSettings_TLSV1_3,
|
||||
},
|
||||
{
|
||||
name: "invalid version",
|
||||
version: "invalid",
|
||||
expect: networking.ServerTLSSettings_TLS_AUTO,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result, err := convertTLSVersion(tc.version)
|
||||
if tc.wantErr {
|
||||
if err == nil {
|
||||
t.Error("Expected error but got none")
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
if result != tc.expect {
|
||||
t.Errorf("Expected %v but got %v", tc.expect, result)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -86,11 +150,13 @@ func TestParse(t *testing.T) {
|
||||
|
||||
func TestApplyGateway(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
input *networking.Gateway
|
||||
config *Ingress
|
||||
expect *networking.Gateway
|
||||
}{
|
||||
{
|
||||
name: "apply TLS version",
|
||||
input: &networking.Gateway{
|
||||
Servers: []*networking.Server{
|
||||
{
|
||||
@@ -105,7 +171,8 @@ func TestApplyGateway(t *testing.T) {
|
||||
},
|
||||
config: &Ingress{
|
||||
DownstreamTLS: &DownstreamTLSConfig{
|
||||
CipherSuites: []string{"ECDHE-RSA-AES256-GCM-SHA384"},
|
||||
MinVersion: "TLSv1.2",
|
||||
MaxVersion: "TLSv1.3",
|
||||
},
|
||||
},
|
||||
expect: &networking.Gateway{
|
||||
@@ -115,14 +182,16 @@ func TestApplyGateway(t *testing.T) {
|
||||
Protocol: "HTTPS",
|
||||
},
|
||||
Tls: &networking.ServerTLSSettings{
|
||||
Mode: networking.ServerTLSSettings_SIMPLE,
|
||||
CipherSuites: []string{"ECDHE-RSA-AES256-GCM-SHA384"},
|
||||
Mode: networking.ServerTLSSettings_SIMPLE,
|
||||
MinProtocolVersion: networking.ServerTLSSettings_TLSV1_2,
|
||||
MaxProtocolVersion: networking.ServerTLSSettings_TLSV1_3,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "complete config",
|
||||
input: &networking.Gateway{
|
||||
Servers: []*networking.Server{
|
||||
{
|
||||
@@ -144,24 +213,28 @@ func TestApplyGateway(t *testing.T) {
|
||||
},
|
||||
Mode: networking.ServerTLSSettings_MUTUAL,
|
||||
CipherSuites: []string{"ECDHE-RSA-AES256-GCM-SHA384"},
|
||||
MinVersion: "TLSv1.2",
|
||||
MaxVersion: "TLSv1.3",
|
||||
},
|
||||
},
|
||||
expect: &networking.Gateway{
|
||||
Servers: []*networking.Server{
|
||||
{
|
||||
Port: &networking.Port{
|
||||
Protocol: "HTTPS",
|
||||
},
|
||||
{Port: &networking.Port{
|
||||
Protocol: "HTTPS",
|
||||
},
|
||||
Tls: &networking.ServerTLSSettings{
|
||||
CredentialName: "kubernetes-ingress://cluster/foo/bar",
|
||||
Mode: networking.ServerTLSSettings_MUTUAL,
|
||||
CipherSuites: []string{"ECDHE-RSA-AES256-GCM-SHA384"},
|
||||
CredentialName: "kubernetes-ingress://cluster/foo/bar",
|
||||
Mode: networking.ServerTLSSettings_MUTUAL,
|
||||
CipherSuites: []string{"ECDHE-RSA-AES256-GCM-SHA384"},
|
||||
MinProtocolVersion: networking.ServerTLSSettings_TLSV1_2,
|
||||
MaxProtocolVersion: networking.ServerTLSSettings_TLSV1_3,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid TLS version",
|
||||
input: &networking.Gateway{
|
||||
Servers: []*networking.Server{
|
||||
{
|
||||
@@ -169,20 +242,15 @@ func TestApplyGateway(t *testing.T) {
|
||||
Protocol: "HTTPS",
|
||||
},
|
||||
Tls: &networking.ServerTLSSettings{
|
||||
Mode: networking.ServerTLSSettings_SIMPLE,
|
||||
CredentialName: "kubernetes-ingress://cluster/foo/bar",
|
||||
Mode: networking.ServerTLSSettings_SIMPLE,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
config: &Ingress{
|
||||
DownstreamTLS: &DownstreamTLSConfig{
|
||||
CASecretName: types.NamespacedName{
|
||||
Namespace: "foo",
|
||||
Name: "bar-cacert",
|
||||
},
|
||||
Mode: networking.ServerTLSSettings_MUTUAL,
|
||||
CipherSuites: []string{"ECDHE-RSA-AES256-GCM-SHA384"},
|
||||
MinVersion: "invalid",
|
||||
MaxVersion: "invalid",
|
||||
},
|
||||
},
|
||||
expect: &networking.Gateway{
|
||||
@@ -192,48 +260,10 @@ func TestApplyGateway(t *testing.T) {
|
||||
Protocol: "HTTPS",
|
||||
},
|
||||
Tls: &networking.ServerTLSSettings{
|
||||
CredentialName: "kubernetes-ingress://cluster/foo/bar",
|
||||
Mode: networking.ServerTLSSettings_MUTUAL,
|
||||
CipherSuites: []string{"ECDHE-RSA-AES256-GCM-SHA384"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: &networking.Gateway{
|
||||
Servers: []*networking.Server{
|
||||
{
|
||||
Port: &networking.Port{
|
||||
Protocol: "HTTPS",
|
||||
},
|
||||
Tls: &networking.ServerTLSSettings{
|
||||
Mode: networking.ServerTLSSettings_SIMPLE,
|
||||
CredentialName: "kubernetes-ingress://cluster/foo/bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
config: &Ingress{
|
||||
DownstreamTLS: &DownstreamTLSConfig{
|
||||
CASecretName: types.NamespacedName{
|
||||
Namespace: "bar",
|
||||
Name: "foo",
|
||||
},
|
||||
Mode: networking.ServerTLSSettings_MUTUAL,
|
||||
CipherSuites: []string{"ECDHE-RSA-AES256-GCM-SHA384"},
|
||||
},
|
||||
},
|
||||
expect: &networking.Gateway{
|
||||
Servers: []*networking.Server{
|
||||
{
|
||||
Port: &networking.Port{
|
||||
Protocol: "HTTPS",
|
||||
},
|
||||
Tls: &networking.ServerTLSSettings{
|
||||
CredentialName: "kubernetes-ingress://cluster/foo/bar",
|
||||
Mode: networking.ServerTLSSettings_SIMPLE,
|
||||
CipherSuites: []string{"ECDHE-RSA-AES256-GCM-SHA384"},
|
||||
Mode: networking.ServerTLSSettings_SIMPLE,
|
||||
// Invalid versions should default to TLS_AUTO
|
||||
MinProtocolVersion: networking.ServerTLSSettings_TLS_AUTO,
|
||||
MaxProtocolVersion: networking.ServerTLSSettings_TLS_AUTO,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -241,11 +271,59 @@ func TestApplyGateway(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run("", func(t *testing.T) {
|
||||
parser.ApplyGateway(testCase.input, testCase.config)
|
||||
if !reflect.DeepEqual(testCase.input, testCase.expect) {
|
||||
t.Fatalf("Should be equal")
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
parser.ApplyGateway(tc.input, tc.config)
|
||||
if !reflect.DeepEqual(tc.input, tc.expect) {
|
||||
t.Fatalf("ApplyGateway result mismatch for %s:\nExpect: %+v\nGot: %+v",
|
||||
tc.name, tc.expect, tc.input)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNeedDownstreamTLS(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
annotations map[string]string
|
||||
expect bool
|
||||
}{
|
||||
{
|
||||
name: "empty annotations",
|
||||
annotations: map[string]string{},
|
||||
expect: false,
|
||||
},
|
||||
{
|
||||
name: "with ssl cipher",
|
||||
annotations: map[string]string{
|
||||
buildNginxAnnotationKey(sslCipher): "ECDHE-RSA-AES256-GCM-SHA384",
|
||||
},
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
name: "with TLS version",
|
||||
annotations: map[string]string{
|
||||
buildNginxAnnotationKey(annotationMinTLSVersion): "TLSv1.2",
|
||||
},
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
name: "with multiple TLS configs",
|
||||
annotations: map[string]string{
|
||||
buildNginxAnnotationKey(sslCipher): "ECDHE-RSA-AES256-GCM-SHA384",
|
||||
buildNginxAnnotationKey(annotationMinTLSVersion): "TLSv1.2",
|
||||
buildNginxAnnotationKey(annotationMaxTLSVersion): "TLSv1.3",
|
||||
},
|
||||
expect: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := needDownstreamTLS(tc.annotations)
|
||||
if result != tc.expect {
|
||||
t.Errorf("needDownstreamTLS() for %s = %v, want %v",
|
||||
tc.name, result, tc.expect)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -81,8 +81,6 @@ func (s *statusSyncer) runUpdateStatus() error {
|
||||
return err
|
||||
}
|
||||
|
||||
IngressLog.Debugf("found number %d of svc", len(svcList))
|
||||
|
||||
lbStatusList := common.GetLbStatusListV1Beta1(svcList)
|
||||
if len(lbStatusList) == 0 {
|
||||
return nil
|
||||
|
||||
@@ -162,6 +162,7 @@ func (c *controller) onEvent(namespacedName types.NamespacedName) error {
|
||||
delete(c.ingresses, namespacedName.String())
|
||||
c.mutex.Unlock()
|
||||
} else {
|
||||
IngressLog.Warnf("ingressLister Get failed, ingress: %s, err: %v", namespacedName, err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -171,7 +172,7 @@ func (c *controller) onEvent(namespacedName types.NamespacedName) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
IngressLog.Debugf("ingress: %s, event: %s", namespacedName, event)
|
||||
IngressLog.Infof("ingress: %s, event: %s", namespacedName, event)
|
||||
|
||||
// we should check need process only when event is not delete,
|
||||
// if it is delete event, and previously processed, we need to process too.
|
||||
@@ -181,7 +182,7 @@ func (c *controller) onEvent(namespacedName types.NamespacedName) error {
|
||||
return err
|
||||
}
|
||||
if !shouldProcess {
|
||||
IngressLog.Infof("no need process, ingress %s", namespacedName)
|
||||
IngressLog.Infof("no need process, ingress: %s", namespacedName)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -279,10 +280,17 @@ func (c *controller) List() []config.Config {
|
||||
for _, raw := range c.ingressInformer.Informer.GetStore().List() {
|
||||
ing, ok := raw.(*ingress.Ingress)
|
||||
if !ok {
|
||||
IngressLog.Warnf("get ingress from informer failed: %v", raw)
|
||||
continue
|
||||
}
|
||||
|
||||
if should, err := c.shouldProcessIngress(ing); !should || err != nil {
|
||||
should, err := c.shouldProcessIngress(ing)
|
||||
if err != nil {
|
||||
IngressLog.Warnf("check should process ingress failed: %v", err)
|
||||
continue
|
||||
}
|
||||
if !should {
|
||||
IngressLog.Debugf("no need process ingress: %s/%s", ing.Namespace, ing.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
@@ -81,8 +81,6 @@ func (s *statusSyncer) runUpdateStatus() error {
|
||||
return err
|
||||
}
|
||||
|
||||
IngressLog.Debugf("found number %d of svc", len(svcList))
|
||||
|
||||
lbStatusList := common.GetLbStatusListV1(svcList)
|
||||
if len(lbStatusList) == 0 {
|
||||
return nil
|
||||
|
||||
@@ -77,7 +77,6 @@ func (s *statusSyncer) runUpdateStatus() error {
|
||||
return err
|
||||
}
|
||||
|
||||
IngressLog.Debugf("found number %d of svc", len(svcList))
|
||||
lbStatusList := common2.GetLbStatusList(svcList)
|
||||
return s.updateStatus(lbStatusList)
|
||||
}
|
||||
|
||||
@@ -14,6 +14,6 @@
|
||||
|
||||
package log
|
||||
|
||||
import "istio.io/pkg/log"
|
||||
import "istio.io/istio/pkg/log"
|
||||
|
||||
var IngressLog = log.RegisterScope("ingress", "Higress Ingress process.", 0)
|
||||
var IngressLog = log.RegisterScope("ingress", "Higress Ingress process.")
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
## 功能说明
|
||||
# 功能说明
|
||||
`model-mapper`插件实现了基于LLM协议中的model参数路由的功能
|
||||
|
||||
## 配置字段
|
||||
# 配置字段
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
| ----------- | --------------- | ----------------------- | ------ | ------------------------------------------- |
|
||||
| `modelKey` | string | 选填 | model | 请求body中model参数的位置 |
|
||||
| `modelMapping` | map of string | 选填 | - | AI 模型映射表,用于将请求中的模型名称映射为服务提供商支持模型名称。<br/>1. 支持前缀匹配。例如用 "gpt-3-*" 匹配所有名称以“gpt-3-”开头的模型;<br/>2. 支持使用 "*" 为键来配置通用兜底映射关系;<br/>3. 如果映射的目标名称为空字符串 "",则表示保留原模型名称。 |
|
||||
| `enableOnPathSuffix` | array of string | 选填 | ["/v1/chat/completions"] | 只对这些特定路径后缀的请求生效 ## 运行属性
|
||||
| `enableOnPathSuffix` | array of string | 选填 | ["/v1/chat/completions"] | 只对这些特定路径后缀的请求生效 |
|
||||
|
||||
|
||||
插件执行阶段:认证阶段
|
||||
插件执行优先级:800
|
||||
|
|
||||
## 效果说明
|
||||
|
||||
如下配置
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
## 功能说明
|
||||
# 功能说明
|
||||
`model-router`插件实现了基于LLM协议中的model参数路由的功能
|
||||
|
||||
## 配置字段
|
||||
# 配置字段
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
| ----------- | --------------- | ----------------------- | ------ | ------------------------------------------- |
|
||||
|
||||
@@ -26,6 +26,7 @@ proxy_wasm_cc_binary(
|
||||
"@com_google_absl//absl/time",
|
||||
"//common:json_util",
|
||||
"//common:http_util",
|
||||
"//common:regex_util",
|
||||
"//common:rule_util",
|
||||
],
|
||||
)
|
||||
@@ -44,6 +45,7 @@ cc_library(
|
||||
"//common:json_util",
|
||||
"@proxy_wasm_cpp_host//:lib",
|
||||
"//common:http_util_nullvm",
|
||||
"//common:regex_util",
|
||||
"//common:rule_util_nullvm",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -1,31 +1,22 @@
|
||||
---
|
||||
title: 请求屏蔽
|
||||
keywords: [higress,request block]
|
||||
description: 请求屏蔽插件配置参考
|
||||
---
|
||||
|
||||
## 功能说明
|
||||
# 功能说明
|
||||
`request-block`插件实现了基于 URL、请求头等特征屏蔽 HTTP 请求,可以用于防护部分站点资源不对外部暴露
|
||||
|
||||
## 运行属性
|
||||
# 配置字段
|
||||
|
||||
插件执行阶段:`鉴权阶段`
|
||||
插件执行优先级:`320`
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
| -------- | -------- | -------- | -------- | -------- |
|
||||
| block_urls | array of string | 选填,`block_urls`,`block_exact_urls`,`block_regexp_urls`,`block_headers`,`block_bodies` 中至少必填一项 | - | 配置用于匹配需要屏蔽 URL 的字符串 |
|
||||
| block_exact_urls | array of string | 选填,`block_urls`,`block_exact_urls`,`block_regexp_urls`,`block_headers`,`block_bodies` 中至少必填一项 | - | 配置用于匹配需要精确屏蔽 URL 的字符串 |
|
||||
| block_regexp_urls | array of string | 选填,`block_urls`,`block_exact_urls`,`block_regexp_urls`,`block_headers`,`block_bodies` 中至少必填一项 | - | 配置用于匹配需要屏蔽 URL 的正则表达式 |
|
||||
| block_headers | array of string | 选填,`block_urls`,`block_exact_urls`,`block_regexp_urls`,`block_headers`,`block_bodies` 中至少必填一项 | - | 配置用于匹配需要屏蔽请求 Header 的字符串 |
|
||||
| block_bodies | array of string | 选填,`block_urls`,`block_exact_urls`,`block_regexp_urls`,`block_headers`,`block_bodies` 中至少必填一项 | - | 配置用于匹配需要屏蔽请求 Body 的字符串 |
|
||||
| blocked_code | number | 选填 | 403 | 配置请求被屏蔽时返回的 HTTP 状态码 |
|
||||
| blocked_message | string | 选填 | - | 配置请求被屏蔽时返回的 HTTP 应答 Body |
|
||||
| case_sensitive | bool | 选填 | true | 配置匹配时是否区分大小写,默认区分 |
|
||||
|
||||
## 配置字段
|
||||
# 配置示例
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
| -------- | -------- | -------- | -------- | -------- |
|
||||
| block_urls | array of string | 选填,`block_urls`,`block_headers`,`block_bodies` 中至少必填一项 | - | 配置用于匹配需要屏蔽 URL 的字符串 |
|
||||
| block_headers | array of string | 选填,`block_urls`,`block_headers`,`block_bodies` 中至少必填一项 | - | 配置用于匹配需要屏蔽请求 Header 的字符串 |
|
||||
| block_bodies | array of string | 选填,`block_urls`,`block_headers`,`block_bodies` 中至少必填一项 | - | 配置用于匹配需要屏蔽请求 Body 的字符串 |
|
||||
| blocked_code | number | 选填 | 403 | 配置请求被屏蔽时返回的 HTTP 状态码 |
|
||||
| blocked_message | string | 选填 | - | 配置请求被屏蔽时返回的 HTTP 应答 Body |
|
||||
| case_sensitive | bool | 选填 | true | 配置匹配时是否区分大小写,默认区分 |
|
||||
|
||||
## 配置示例
|
||||
|
||||
### 屏蔽请求 url 路径
|
||||
## 屏蔽请求 url 路径
|
||||
```yaml
|
||||
block_urls:
|
||||
- swagger.html
|
||||
@@ -40,7 +31,36 @@ curl http://example.com?foo=Bar
|
||||
curl http://exmaple.com/Swagger.html
|
||||
```
|
||||
|
||||
### 屏蔽请求 header
|
||||
## 屏蔽精确匹配的请求 url 路径
|
||||
|
||||
```yaml
|
||||
block_exact_urls:
|
||||
- /swagger.html?foo=bar
|
||||
case_sensitive: false
|
||||
```
|
||||
|
||||
根据该配置,下列请求将被禁止访问:
|
||||
|
||||
```bash
|
||||
curl http://exmaple.com/Swagger.html?foo=Bar
|
||||
```
|
||||
|
||||
## 屏蔽正则匹配的请求 url 路径
|
||||
|
||||
```yaml
|
||||
block_exact_urls:
|
||||
- .*swagger.*
|
||||
case_sensitive: false
|
||||
```
|
||||
|
||||
根据该配置,下列请求将被禁止访问:
|
||||
|
||||
```bash
|
||||
curl http://exmaple.com/Swagger.html?foo=Bar
|
||||
```
|
||||
|
||||
|
||||
## 屏蔽请求 header
|
||||
```yaml
|
||||
block_headers:
|
||||
- example-key
|
||||
@@ -54,9 +74,9 @@ curl http://example.com -H 'example-key: 123'
|
||||
curl http://exmaple.com -H 'my-header: example-value'
|
||||
```
|
||||
|
||||
### 屏蔽请求 body
|
||||
## 屏蔽请求 body
|
||||
```yaml
|
||||
block_bodies:
|
||||
block_bodys:
|
||||
- "hello world"
|
||||
case_sensitive: false
|
||||
```
|
||||
@@ -68,8 +88,30 @@ curl http://example.com -d 'Hello World'
|
||||
curl http://exmaple.com -d 'hello world'
|
||||
```
|
||||
|
||||
## 对特定路由或域名开启
|
||||
```yaml
|
||||
# 使用 _rules_ 字段进行细粒度规则配置
|
||||
_rules_:
|
||||
# 规则一:按路由名称匹配生效
|
||||
- _match_route_:
|
||||
- route-a
|
||||
- route-b
|
||||
block_bodys:
|
||||
- "hello world"
|
||||
# 规则二:按域名匹配生效
|
||||
- _match_domain_:
|
||||
- "*.example.com"
|
||||
- test.com
|
||||
block_urls:
|
||||
- "swagger.html"
|
||||
block_bodys:
|
||||
- "hello world"
|
||||
```
|
||||
此例 `_match_route_` 中指定的 `route-a` 和 `route-b` 即在创建网关路由时填写的路由名称,当匹配到这两个路由时,将使用此段配置;
|
||||
此例 `_match_domain_` 中指定的 `*.example.com` 和 `test.com` 用于匹配请求的域名,当发现域名匹配时,将使用此段配置;
|
||||
配置的匹配生效顺序,将按照 `_rules_` 下规则的排列顺序,匹配第一个规则后生效对应配置,后续规则将被忽略。
|
||||
|
||||
## 请求 Body 大小限制
|
||||
# 请求 Body 大小限制
|
||||
|
||||
当配置了 `block_bodies` 时,仅支持小于 32 MB 的请求 Body 进行匹配。若请求 Body 大于此限制,并且不存在匹配到的 `block_urls` 和 `block_headers` 项时,不会对该请求执行屏蔽操作
|
||||
当配置了 `block_bodies` 时,若请求 Body 超过全局配置 DownstreamConnectionBufferLimits,将返回 `413 Payload Too Large`
|
||||
当配置了 `block_bodys` 时,仅支持小于 32 MB 的请求 Body 进行匹配。若请求 Body 大于此限制,并且不存在匹配到的 `block_urls` 和 `block_headers` 项时,不会对该请求执行屏蔽操作
|
||||
当配置了 `block_bodys` 时,若请求 Body 超过全局配置 DownstreamConnectionBufferLimits,将返回 `413 Payload Too Large`, 请在参数配置页调高此项。注意调高此参数配置后,网关内存使用将有显著增加。
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include "extensions/request_block/plugin.h"
|
||||
|
||||
#include <array>
|
||||
#include <memory>
|
||||
|
||||
#include "absl/strings/str_cat.h"
|
||||
#include "absl/strings/str_join.h"
|
||||
@@ -89,6 +90,48 @@ bool PluginRootContext::parsePluginConfig(const json& configuration,
|
||||
LOG_WARN("failed to parse configuration for block_urls.");
|
||||
return false;
|
||||
}
|
||||
if (!JsonArrayIterate(
|
||||
configuration, "block_exact_urls", [&](const json& item) -> bool {
|
||||
auto url = JsonValueAs<std::string>(item);
|
||||
if (url.second != Wasm::Common::JsonParserResultDetail::OK) {
|
||||
LOG_WARN("cannot parse block_exact_urls");
|
||||
return false;
|
||||
}
|
||||
if (rule.case_sensitive) {
|
||||
rule.block_exact_urls.push_back(std::move(url.first.value()));
|
||||
} else {
|
||||
rule.block_exact_urls.push_back(
|
||||
absl::AsciiStrToLower(url.first.value()));
|
||||
}
|
||||
return true;
|
||||
})) {
|
||||
LOG_WARN("failed to parse configuration for block_exact_urls.");
|
||||
return false;
|
||||
}
|
||||
if (!JsonArrayIterate(
|
||||
configuration, "block_regexp_urls", [&](const json& item) -> bool {
|
||||
auto url = JsonValueAs<std::string>(item);
|
||||
if (url.second != Wasm::Common::JsonParserResultDetail::OK) {
|
||||
LOG_WARN("cannot parse block_regexp_urls");
|
||||
return false;
|
||||
}
|
||||
std::string regex;
|
||||
if (rule.case_sensitive) {
|
||||
regex = url.first.value();
|
||||
} else {
|
||||
regex = absl::AsciiStrToLower(url.first.value());
|
||||
}
|
||||
auto re = std::make_unique<ReMatcher>(regex);
|
||||
if (!re->error().empty()) {
|
||||
LOG_WARN(re->error());
|
||||
return false;
|
||||
}
|
||||
rule.block_regexp_urls.push_back(std::move(re));
|
||||
return true;
|
||||
})) {
|
||||
LOG_WARN("failed to parse configuration for block_regexp_urls.");
|
||||
return false;
|
||||
}
|
||||
if (!JsonArrayIterate(
|
||||
configuration, "block_headers", [&](const json& item) -> bool {
|
||||
auto header = JsonValueAs<std::string>(item);
|
||||
@@ -125,8 +168,28 @@ bool PluginRootContext::parsePluginConfig(const json& configuration,
|
||||
LOG_WARN("failed to parse configuration for block_bodys.");
|
||||
return false;
|
||||
}
|
||||
// compatiable
|
||||
if (!JsonArrayIterate(
|
||||
configuration, "block_bodies", [&](const json& item) -> bool {
|
||||
auto body = JsonValueAs<std::string>(item);
|
||||
if (body.second != Wasm::Common::JsonParserResultDetail::OK) {
|
||||
LOG_WARN("cannot parse block_bodies");
|
||||
return false;
|
||||
}
|
||||
if (rule.case_sensitive) {
|
||||
rule.block_bodys.push_back(std::move(body.first.value()));
|
||||
} else {
|
||||
rule.block_bodys.push_back(
|
||||
absl::AsciiStrToLower(body.first.value()));
|
||||
}
|
||||
return true;
|
||||
})) {
|
||||
LOG_WARN("failed to parse configuration for block_bodies.");
|
||||
return false;
|
||||
}
|
||||
if (rule.block_bodys.empty() && rule.block_headers.empty() &&
|
||||
rule.block_urls.empty()) {
|
||||
rule.block_urls.empty() && rule.block_exact_urls.empty() &&
|
||||
rule.block_regexp_urls.empty()) {
|
||||
LOG_WARN("there is no block rules");
|
||||
return false;
|
||||
}
|
||||
@@ -172,6 +235,18 @@ bool PluginRootContext::checkHeader(const RequestBlockConfigRule& rule,
|
||||
urlstr = absl::AsciiStrToLower(request_url);
|
||||
url = urlstr;
|
||||
}
|
||||
for (const auto& block_url : rule.block_exact_urls) {
|
||||
if (url == block_url) {
|
||||
sendLocalResponse(rule.blocked_code, "", rule.blocked_message, {});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for (const auto& block_url : rule.block_regexp_urls) {
|
||||
if (block_url->match(url)) {
|
||||
sendLocalResponse(rule.blocked_code, "", rule.blocked_message, {});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for (const auto& block_url : rule.block_urls) {
|
||||
if (absl::StrContains(url, block_url)) {
|
||||
sendLocalResponse(rule.blocked_code, "", rule.blocked_message, {});
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include <unordered_map>
|
||||
|
||||
#include "common/http_util.h"
|
||||
#include "common/regex.h"
|
||||
#include "common/route_rule_matcher.h"
|
||||
#define ASSERT(_X) assert(_X)
|
||||
|
||||
@@ -39,11 +40,16 @@ namespace request_block {
|
||||
|
||||
#endif
|
||||
|
||||
using ReMatcher = Wasm::Common::Regex::CompiledGoogleReMatcher;
|
||||
using ReMatcherPtr = std::unique_ptr<ReMatcher>;
|
||||
|
||||
struct RequestBlockConfigRule {
|
||||
int blocked_code = 403;
|
||||
std::string blocked_message;
|
||||
bool case_sensitive = true;
|
||||
std::vector<std::string> block_urls;
|
||||
std::vector<std::string> block_exact_urls;
|
||||
std::vector<ReMatcherPtr> block_regexp_urls;
|
||||
std::vector<std::string> block_headers;
|
||||
std::vector<std::string> block_bodys;
|
||||
};
|
||||
|
||||
@@ -127,6 +127,8 @@ TEST_F(RequestBlockTest, CaseSensitive) {
|
||||
std::string configuration = R"(
|
||||
{
|
||||
"block_urls": ["?foo=bar", "swagger.html"],
|
||||
"block_exact_urls": ["/hello.html?abc=123"],
|
||||
"block_regexp_urls": [".*monkey.*"],
|
||||
"block_headers": ["headerKey", "headerValue"],
|
||||
"block_bodys": ["Hello World"]
|
||||
})";
|
||||
@@ -150,6 +152,22 @@ TEST_F(RequestBlockTest, CaseSensitive) {
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::StopIteration);
|
||||
|
||||
path_ = "/hello.html?abc=123";
|
||||
EXPECT_CALL(*mock_context_, sendLocalResponse(403, testing::_, testing::_,
|
||||
testing::_, testing::_));
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::StopIteration);
|
||||
|
||||
path_ = "/black/Monkey";
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::Continue);
|
||||
|
||||
path_ = "/black/monkey";
|
||||
EXPECT_CALL(*mock_context_, sendLocalResponse(403, testing::_, testing::_,
|
||||
testing::_, testing::_));
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::StopIteration);
|
||||
|
||||
path_ = "";
|
||||
headers_ = {{"headerKey", "123"}};
|
||||
EXPECT_CALL(*mock_context_, sendLocalResponse(403, testing::_, testing::_,
|
||||
@@ -188,6 +206,8 @@ TEST_F(RequestBlockTest, CaseInsensitive) {
|
||||
"blocked_code": 404,
|
||||
"block_urls": ["?foo=bar", "swagger.html"],
|
||||
"block_headers": ["headerKey", "headerValue"],
|
||||
"block_exact_urls": ["/hello.html?abc=123"],
|
||||
"block_regexp_urls": [".*monkey.*"],
|
||||
"block_bodys": ["Hello World"]
|
||||
})";
|
||||
|
||||
@@ -206,6 +226,24 @@ TEST_F(RequestBlockTest, CaseInsensitive) {
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::StopIteration);
|
||||
|
||||
path_ = "/Hello.html?abc=123";
|
||||
EXPECT_CALL(*mock_context_, sendLocalResponse(404, testing::_, testing::_,
|
||||
testing::_, testing::_));
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::StopIteration);
|
||||
|
||||
path_ = "/black/Monkey";
|
||||
EXPECT_CALL(*mock_context_, sendLocalResponse(404, testing::_, testing::_,
|
||||
testing::_, testing::_));
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::StopIteration);
|
||||
|
||||
path_ = "/black/monkey";
|
||||
EXPECT_CALL(*mock_context_, sendLocalResponse(404, testing::_, testing::_,
|
||||
testing::_, testing::_));
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::StopIteration);
|
||||
|
||||
path_ = "";
|
||||
headers_ = {{"headerkey", "123"}};
|
||||
EXPECT_CALL(*mock_context_, sendLocalResponse(404, testing::_, testing::_,
|
||||
@@ -232,6 +270,26 @@ TEST_F(RequestBlockTest, CaseInsensitive) {
|
||||
FilterDataStatus::StopIterationNoBuffer);
|
||||
}
|
||||
|
||||
TEST_F(RequestBlockTest, Bodies) {
|
||||
std::string configuration = R"(
|
||||
{
|
||||
"case_sensitive": false,
|
||||
"blocked_code": 404,
|
||||
"block_bodies": ["Hello World"]
|
||||
})";
|
||||
|
||||
config_.set({configuration.data(), configuration.size()});
|
||||
EXPECT_TRUE(root_context_->configure(configuration.size()));
|
||||
|
||||
body_.set("hello world");
|
||||
EXPECT_EQ(context_->onRequestHeaders(0, false),
|
||||
FilterHeadersStatus::Continue);
|
||||
EXPECT_CALL(*mock_context_, sendLocalResponse(404, testing::_, testing::_,
|
||||
testing::_, testing::_));
|
||||
EXPECT_EQ(context_->onRequestBody(11, true),
|
||||
FilterDataStatus::StopIterationNoBuffer);
|
||||
}
|
||||
|
||||
} // namespace request_block
|
||||
} // namespace null_plugin
|
||||
} // namespace proxy_wasm
|
||||
|
||||
68
plugins/wasm-go/examples/custom-log/config.yaml
Normal file
68
plugins/wasm-go/examples/custom-log/config.yaml
Normal file
@@ -0,0 +1,68 @@
|
||||
static_resources:
|
||||
listeners:
|
||||
- name: listener_0
|
||||
address:
|
||||
socket_address:
|
||||
protocol: TCP
|
||||
address: 0.0.0.0
|
||||
port_value: 8080
|
||||
filter_chains:
|
||||
- filters:
|
||||
- name: envoy.filters.network.http_connection_manager
|
||||
typed_config:
|
||||
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
|
||||
stat_prefix: ingress_http
|
||||
access_log:
|
||||
- name: envoy.access_loggers.file
|
||||
typed_config:
|
||||
"@type": type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog
|
||||
log_format:
|
||||
text_format_source:
|
||||
inline_string: "{\"custom_log\":\"%FILTER_STATE(wasm.custom_log:PLAIN)%\",\"ai_log\":\"%FILTER_STATE(wasm.ai_log:PLAIN)%\"}
|
||||
|
||||
"
|
||||
path: /dev/stdout
|
||||
route_config:
|
||||
name: local_route
|
||||
virtual_hosts:
|
||||
- name: local_service
|
||||
domains: ["*"]
|
||||
routes:
|
||||
- name: get
|
||||
match:
|
||||
prefix: "/get"
|
||||
route:
|
||||
cluster: httpbin
|
||||
http_filters:
|
||||
- name: test
|
||||
typed_config:
|
||||
"@type": type.googleapis.com/udpa.type.v1.TypedStruct
|
||||
type_url: type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm
|
||||
value:
|
||||
config:
|
||||
name: test
|
||||
vm_config:
|
||||
runtime: envoy.wasm.runtime.v8
|
||||
code:
|
||||
local:
|
||||
filename: main.wasm
|
||||
configuration:
|
||||
"@type": "type.googleapis.com/google.protobuf.StringValue"
|
||||
value: {}
|
||||
- name: envoy.filters.http.router
|
||||
typed_config:
|
||||
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
|
||||
clusters:
|
||||
- name: httpbin
|
||||
connect_timeout: 600s
|
||||
type: STRICT_DNS
|
||||
lb_policy: ROUND_ROBIN
|
||||
load_assignment:
|
||||
cluster_name: httpbin
|
||||
endpoints:
|
||||
- lb_endpoints:
|
||||
- endpoint:
|
||||
address:
|
||||
socket_address:
|
||||
address: httpbin.org
|
||||
port_value: 80
|
||||
20
plugins/wasm-go/examples/custom-log/go.mod
Normal file
20
plugins/wasm-go/examples/custom-log/go.mod
Normal file
@@ -0,0 +1,20 @@
|
||||
module github.com/alibaba/higress/plugins/wasm-go/extensions/custom-logs
|
||||
|
||||
go 1.18
|
||||
|
||||
replace github.com/alibaba/higress/plugins/wasm-go => ../..
|
||||
|
||||
require (
|
||||
github.com/alibaba/higress/plugins/wasm-go v0.0.0
|
||||
github.com/higress-group/proxy-wasm-go-sdk v1.0.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 // indirect
|
||||
github.com/magefile/mage v1.14.0 // indirect
|
||||
github.com/tidwall/gjson v1.17.3 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.0 // indirect
|
||||
github.com/tidwall/resp v0.1.1 // indirect
|
||||
)
|
||||
20
plugins/wasm-go/examples/custom-log/go.sum
Normal file
20
plugins/wasm-go/examples/custom-log/go.sum
Normal file
@@ -0,0 +1,20 @@
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 h1:IHDghbGQ2DTIXHBHxWfqCYQW1fKjyJ/I7W1pMyUDeEA=
|
||||
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520/go.mod h1:Nz8ORLaFiLWotg6GeKlJMhv8cci8mM43uEnLA5t8iew=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v1.0.0 h1:BZRNf4R7jr9hwRivg/E29nkVaKEak5MWjBDhWjuHijU=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v1.0.0/go.mod h1:iiSyFbo+rAtbtGt/bsefv8GU57h9CCLYGJA74/tF5/0=
|
||||
github.com/magefile/mage v1.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo=
|
||||
github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/tidwall/gjson v1.17.3 h1:bwWLZU7icoKRG+C+0PNwIKC6FCJO/Q3p2pZvuP0jN94=
|
||||
github.com/tidwall/gjson v1.17.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/resp v0.1.1 h1:Ly20wkhqKTmDUPlyM1S7pWo5kk0tDu8OoC/vFArXmwE=
|
||||
github.com/tidwall/resp v0.1.1/go.mod h1:3/FrruOBAxPTPtundW0VXgmsQ4ZBA0Aw714lVYgwFa0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
67
plugins/wasm-go/examples/custom-log/main.go
Normal file
67
plugins/wasm-go/examples/custom-log/main.go
Normal file
@@ -0,0 +1,67 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
)
|
||||
|
||||
func main() {
|
||||
wrapper.SetCtx(
|
||||
"custom-log",
|
||||
wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
|
||||
)
|
||||
}
|
||||
|
||||
type CustomLogConfig struct {
|
||||
}
|
||||
|
||||
// Method 1: write custom log
|
||||
func writeLog(ctx wrapper.HttpContext) {
|
||||
ctx.SetUserAttribute("question", "当然可以。在Python中,你可以创建一个函数来计算一系列数字的和。下面是一个简单的例子,该函数接受一个数字列表作为输入,并返回它们的总和。\n\n```python\ndef sum_of_numbers(numbers):\n \"\"\"\n 计算列表中所有数字的和。\n \n 参数:\n numbers (list of int or float): 一个包含数字的列表。\n \n 返回:\n int or float: 列表中所有数字的总和。\n \"\"\"\n total_sum = sum(numbers) # 使用Python内置的sum函数计算总和\n return total_sum\n\n# 示例使用\nnumbers_list = [1, 2, 3, 4, 5]\nprint(\"The sum is:\", sum_of_numbers(numbers_list)) # 输出:The sum is: 15\n```\n\n在这段代码中,我们定义了一个名为 `sum_of_numbers` 的函数,它接收一个参数 `numbers`,这是一个包含整数或浮点数的列表。函数内部使用了Python的内置函数 `sum()` 来计算这些数字的总和,并将结果返回。\n\n你也可以手动实现求和逻辑,而不是使用内置的 `sum()` 函数,如下所示:\n\n```python\ndef sum_of_numbers_manual(numbers):\n \"\"\"\n 手动计算列表中所有数字的和。\n \n 参数:\n numbers (list of int or float): 一个包含数字的列表。\n \n 返回:\n int or float: 列表中所有数字的总和。\n \"\"\"\n total_sum = 0\n for number in numbers:\n total_sum += number\n return total_sum\n\n# 示例使用\nnumbers_list = [1, 2, 3, 4, 5]\nprint(\"The sum is:\", sum_of_numbers_manual(numbers_list)) # 输出:The sum is: 15\n```\n\n在这个版本中,我们初始化 `total_sum` 为0,然后遍历列表中的每个元素,并将其加到 `total_sum` 上。最后返回这个累加的结果。这两种方法都可以达到相同的目的,但是使用内置函数通常更简洁且效率更高。")
|
||||
ctx.SetUserAttribute("k2", 2213.22)
|
||||
ctx.WriteUserAttributeToLog()
|
||||
}
|
||||
|
||||
// Methods 2: write custom log with specific key
|
||||
func writeLogWithKey(ctx wrapper.HttpContext, key string) {
|
||||
ctx.SetUserAttribute("k2", 2213.22)
|
||||
_ = ctx.WriteUserAttributeToLogWithKey(key)
|
||||
ctx.SetUserAttribute("k2", 212939.22)
|
||||
ctx.SetUserAttribute("k3", 123)
|
||||
_ = ctx.WriteUserAttributeToLogWithKey(key)
|
||||
}
|
||||
|
||||
// Methods 2: write custom log with specific key
|
||||
func writeTraceAttribute(ctx wrapper.HttpContext) {
|
||||
ctx.SetUserAttribute("question", "当然可以。在Python中,你可以创建一个函数来计算一系列数字的和。下面是一个简单的例子,该函数接受一个数字列表作为输入,并返回它们的总和。\n\n```python\ndef sum_of_numbers(numbers):\n \"\"\"\n 计算列表中所有数字的和。\n \n 参数:\n numbers (list of int or float): 一个包含数字的列表。\n \n 返回:\n int or float: 列表中所有数字的总和。\n \"\"\"\n total_sum = sum(numbers) # 使用Python内置的sum函数计算总和\n return total_sum\n\n# 示例使用\nnumbers_list = [1, 2, 3, 4, 5]\nprint(\"The sum is:\", sum_of_numbers(numbers_list)) # 输出:The sum is: 15\n```\n\n在这段代码中,我们定义了一个名为 `sum_of_numbers` 的函数,它接收一个参数 `numbers`,这是一个包含整数或浮点数的列表。函数内部使用了Python的内置函数 `sum()` 来计算这些数字的总和,并将结果返回。\n\n你也可以手动实现求和逻辑,而不是使用内置的 `sum()` 函数,如下所示:\n\n```python\ndef sum_of_numbers_manual(numbers):\n \"\"\"\n 手动计算列表中所有数字的和。\n \n 参数:\n numbers (list of int or float): 一个包含数字的列表。\n \n 返回:\n int or float: 列表中所有数字的总和。\n \"\"\"\n total_sum = 0\n for number in numbers:\n total_sum += number\n return total_sum\n\n# 示例使用\nnumbers_list = [1, 2, 3, 4, 5]\nprint(\"The sum is:\", sum_of_numbers_manual(numbers_list)) # 输出:The sum is: 15\n```\n\n在这个版本中,我们初始化 `total_sum` 为0,然后遍历列表中的每个元素,并将其加到 `total_sum` 上。最后返回这个累加的结果。这两种方法都可以达到相同的目的,但是使用内置函数通常更简洁且效率更高。")
|
||||
ctx.SetUserAttribute("k2", 2213.22)
|
||||
ctx.WriteUserAttributeToTrace()
|
||||
}
|
||||
|
||||
func onHttpRequestHeaders(ctx wrapper.HttpContext, config CustomLogConfig, log wrapper.Log) types.Action {
|
||||
if rand.Intn(10)%3 == 1 {
|
||||
writeLog(ctx)
|
||||
} else if rand.Intn(10)%3 == 2 {
|
||||
writeLogWithKey(ctx, "ai_log")
|
||||
} else {
|
||||
writeTraceAttribute(ctx)
|
||||
}
|
||||
return types.ActionContinue
|
||||
}
|
||||
@@ -30,7 +30,7 @@ LLM 结果缓存插件,默认配置方式可以直接用于 openai 协议的
|
||||
|
||||
## 配置说明
|
||||
|
||||
本插件同时支持基于向量数据库的语义化缓存和基于字符串匹配的缓存方法,如果同时配置了向量数据库和缓存数据库,优先使用向量数据库。
|
||||
本插件同时支持基于向量数据库的语义化缓存和基于字符串匹配的缓存方法,如果同时配置了向量数据库和缓存数据库,优先使用缓存数据库,未命中场景下使用向量数据库能力。
|
||||
|
||||
*Note*: 向量数据库(vector) 和 缓存数据库(cache) 不能同时为空,否则本插件无法提供缓存服务。
|
||||
|
||||
@@ -86,7 +86,8 @@ LLM 结果缓存插件,默认配置方式可以直接用于 openai 协议的
|
||||
| cache.password | string | optional | "" | 缓存服务密码 |
|
||||
| cache.timeout | uint32 | optional | 10000 | 缓存服务的超时时间,单位为毫秒。默认值是10000,即10秒 |
|
||||
| cache.cacheTTL | int | optional | 0 | 缓存过期时间,单位为秒。默认值是 0,即 永不过期|
|
||||
| cacheKeyPrefix | string | optional | "higress-ai-cache:" | 缓存 Key 的前缀,默认值为 "higress-ai-cache:" |
|
||||
| cache.cacheKeyPrefix | string | optional | "higress-ai-cache:" | 缓存 Key 的前缀,默认值为 "higress-ai-cache:" |
|
||||
| cache.database | int | optional | 0 | 使用的数据库id,仅限redis,例如配置为1,对应`SELECT 1` |
|
||||
|
||||
|
||||
## 其他配置
|
||||
@@ -168,6 +169,7 @@ redis:
|
||||
serviceName: my_redis.dns
|
||||
servicePort: 6379
|
||||
timeout: 100
|
||||
database: 1
|
||||
```
|
||||
|
||||
## 进阶用法
|
||||
|
||||
@@ -15,26 +15,29 @@ Plugin Execution Phase: `Authentication Phase`
|
||||
Plugin Execution Priority: `10`
|
||||
|
||||
## Configuration Description
|
||||
| Name | Type | Requirement | Default | Description |
|
||||
| -------- | -------- | -------- | -------- | -------- |
|
||||
| cacheKeyFrom.requestBody | string | optional | "messages.@reverse.0.content" | Extracts a string from the request Body based on [GJSON PATH](https://github.com/tidwall/gjson/blob/master/SYNTAX.md) syntax |
|
||||
| cacheValueFrom.responseBody | string | optional | "choices.0.message.content" | Extracts a string from the response Body based on [GJSON PATH](https://github.com/tidwall/gjson/blob/master/SYNTAX.md) syntax |
|
||||
| Name | Type | Requirement | Default | Description |
|
||||
| -------- | -------- | -------- | -------- | -------- |
|
||||
| cacheKeyFrom.requestBody | string | optional | "messages.@reverse.0.content" | Extracts a string from the request Body based on [GJSON PATH](https://github.com/tidwall/gjson/blob/master/SYNTAX.md) syntax |
|
||||
| cacheValueFrom.responseBody | string | optional | "choices.0.message.content" | Extracts a string from the response Body based on [GJSON PATH](https://github.com/tidwall/gjson/blob/master/SYNTAX.md) syntax |
|
||||
| cacheStreamValueFrom.responseBody | string | optional | "choices.0.delta.content" | Extracts a string from the streaming response Body based on [GJSON PATH](https://github.com/tidwall/gjson/blob/master/SYNTAX.md) syntax |
|
||||
| cacheKeyPrefix | string | optional | "higress-ai-cache:" | Prefix for the Redis cache key |
|
||||
| cacheTTL | integer | optional | 0 | Cache expiration time in seconds, default value is 0, which means never expire |
|
||||
| redis.serviceName | string | required | - | The complete FQDN name of the Redis service, including the service type, e.g., my-redis.dns, redis.my-ns.svc.cluster.local |
|
||||
| redis.servicePort | integer | optional | 6379 | Redis service port |
|
||||
| redis.timeout | integer | optional | 1000 | Timeout for requests to Redis, in milliseconds |
|
||||
| redis.username | string | optional | - | Username for logging into Redis |
|
||||
| redis.password | string | optional | - | Password for logging into Redis |
|
||||
| returnResponseTemplate | string | optional | `{"id":"from-cache","choices":[%s],"model":"gpt-4o","object":"chat.completion","usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}` | Template for returning HTTP response, with %s marking the part to be replaced by cache value |
|
||||
| returnStreamResponseTemplate | string | optional | `data:{"id":"from-cache","choices":[{"index":0,"delta":{"role":"assistant","content":"%s"},"finish_reason":"stop"}],"model":"gpt-4o","object":"chat.completion","usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}\n\ndata:[DONE]\n\n` | Template for returning streaming HTTP response, with %s marking the part to be replaced by cache value |
|
||||
| cacheKeyPrefix | string | optional | "higress-ai-cache:" | Prefix for the Redis cache key |
|
||||
| cacheTTL | integer | optional | 0 | Cache expiration time in seconds, default value is 0, which means never expire |
|
||||
| redis.serviceName | string | required | - | The complete FQDN name of the Redis service, including the service type, e.g., my-redis.dns, redis.my-ns.svc.cluster.local |
|
||||
| redis.servicePort | integer | optional | 6379 | Redis service port |
|
||||
| redis.timeout | integer | optional | 1000 | Timeout for requests to Redis, in milliseconds |
|
||||
| redis.username | string | optional | - | Username for logging into Redis |
|
||||
| redis.database | int | optional | 0 | The database ID used, limited to Redis, for example, configured as 1, corresponds to `SELECT 1`. |
|
||||
| redis.password | string | optional | - | Password for logging into Redis |
|
||||
| returnResponseTemplate | string | optional | `{"id":"from-cache","choices":[%s],"model":"gpt-4o","object":"chat.completion","usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}` | Template for returning HTTP response, with %s marking the part to be replaced by cache value |
|
||||
| returnStreamResponseTemplate | string | optional | `data:{"id":"from-cache","choices":[{"index":0,"delta":{"role":"assistant","content":"%s"},"finish_reason":"stop"}],"model":"gpt-4o","object":"chat.completion","usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}\n\ndata:[DONE]\n\n` | Template for returning streaming HTTP response, with %s marking the part to be replaced by cache value |
|
||||
|
||||
## Configuration Example
|
||||
```yaml
|
||||
redis:
|
||||
serviceName: my-redis.dns
|
||||
timeout: 2000
|
||||
servicePort: 6379
|
||||
database: 1
|
||||
```
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
@@ -2,6 +2,7 @@ package cache
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
"github.com/tidwall/gjson"
|
||||
@@ -51,6 +52,9 @@ type ProviderConfig struct {
|
||||
// @Title 缓存 Key 前缀
|
||||
// @Description 缓存 Key 的前缀,默认值为 "higressAiCache:"
|
||||
cacheKeyPrefix string
|
||||
// @Title redis database
|
||||
// @Description 指定 redis 的 database,默认使用0
|
||||
database int
|
||||
}
|
||||
|
||||
func (c *ProviderConfig) GetProviderType() string {
|
||||
@@ -62,7 +66,12 @@ func (c *ProviderConfig) FromJson(json gjson.Result) {
|
||||
c.serviceName = json.Get("serviceName").String()
|
||||
c.servicePort = int(json.Get("servicePort").Int())
|
||||
if !json.Get("servicePort").Exists() {
|
||||
c.servicePort = 6379
|
||||
if strings.HasSuffix(c.serviceName, ".static") {
|
||||
// use default logic port which is 80 for static service
|
||||
c.servicePort = 80
|
||||
} else {
|
||||
c.servicePort = 6379
|
||||
}
|
||||
}
|
||||
c.serviceHost = json.Get("serviceHost").String()
|
||||
c.username = json.Get("username").String()
|
||||
@@ -73,6 +82,7 @@ func (c *ProviderConfig) FromJson(json gjson.Result) {
|
||||
if !json.Get("password").Exists() {
|
||||
c.password = ""
|
||||
}
|
||||
c.database = int(json.Get("database").Int())
|
||||
c.timeout = uint32(json.Get("timeout").Int())
|
||||
if !json.Get("timeout").Exists() {
|
||||
c.timeout = 10000
|
||||
|
||||
@@ -38,7 +38,7 @@ func (rp *redisProvider) GetProviderType() string {
|
||||
}
|
||||
|
||||
func (rp *redisProvider) Init(username string, password string, timeout uint32) error {
|
||||
return rp.client.Init(rp.config.username, rp.config.password, int64(rp.config.timeout))
|
||||
return rp.client.Init(rp.config.username, rp.config.password, int64(rp.config.timeout), wrapper.WithDataBase(rp.config.database))
|
||||
}
|
||||
|
||||
func (rp *redisProvider) Get(key string, cb wrapper.RedisResponseCallback) error {
|
||||
|
||||
@@ -28,9 +28,9 @@ type PluginConfig struct {
|
||||
embeddingProvider embedding.Provider
|
||||
vectorProvider vector.Provider
|
||||
|
||||
embeddingProviderConfig embedding.ProviderConfig
|
||||
vectorProviderConfig vector.ProviderConfig
|
||||
cacheProviderConfig cache.ProviderConfig
|
||||
embeddingProviderConfig *embedding.ProviderConfig
|
||||
vectorProviderConfig *vector.ProviderConfig
|
||||
cacheProviderConfig *cache.ProviderConfig
|
||||
|
||||
CacheKeyFrom string
|
||||
CacheValueFrom string
|
||||
@@ -47,7 +47,9 @@ type PluginConfig struct {
|
||||
}
|
||||
|
||||
func (c *PluginConfig) FromJson(json gjson.Result, log wrapper.Log) {
|
||||
|
||||
c.embeddingProviderConfig = &embedding.ProviderConfig{}
|
||||
c.vectorProviderConfig = &vector.ProviderConfig{}
|
||||
c.cacheProviderConfig = &cache.ProviderConfig{}
|
||||
c.vectorProviderConfig.FromJson(json.Get("vector"))
|
||||
c.embeddingProviderConfig.FromJson(json.Get("embedding"))
|
||||
c.cacheProviderConfig.FromJson(json.Get("cache"))
|
||||
@@ -79,11 +81,11 @@ func (c *PluginConfig) FromJson(json gjson.Result, log wrapper.Log) {
|
||||
|
||||
c.StreamResponseTemplate = json.Get("streamResponseTemplate").String()
|
||||
if c.StreamResponseTemplate == "" {
|
||||
c.StreamResponseTemplate = `data:{"id":"from-cache","choices":[{"index":0,"delta":{"role":"assistant","content":"%s"},"finish_reason":"stop"}],"model":"gpt-4o","object":"chat.completion","usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}` + "\n\ndata:[DONE]\n\n"
|
||||
c.StreamResponseTemplate = `data:{"id":"from-cache","choices":[{"index":0,"delta":{"role":"assistant","content":"%s"},"finish_reason":"stop"}],"model":"from-cache","object":"chat.completion","usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}` + "\n\ndata:[DONE]\n\n"
|
||||
}
|
||||
c.ResponseTemplate = json.Get("responseTemplate").String()
|
||||
if c.ResponseTemplate == "" {
|
||||
c.ResponseTemplate = `{"id":"from-cache","choices":[{"index":0,"message":{"role":"assistant","content":"%s"},"finish_reason":"stop"}],"model":"gpt-4o","object":"chat.completion","usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}`
|
||||
c.ResponseTemplate = `{"id":"from-cache","choices":[{"index":0,"message":{"role":"assistant","content":"%s"},"finish_reason":"stop"}],"model":"from-cache","object":"chat.completion","usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}`
|
||||
}
|
||||
|
||||
if json.Get("enableSemanticCache").Exists() {
|
||||
@@ -142,7 +144,7 @@ func (c *PluginConfig) Complete(log wrapper.Log) error {
|
||||
var err error
|
||||
if c.embeddingProviderConfig.GetProviderType() != "" {
|
||||
log.Debugf("embedding provider is set to %s", c.embeddingProviderConfig.GetProviderType())
|
||||
c.embeddingProvider, err = embedding.CreateProvider(c.embeddingProviderConfig)
|
||||
c.embeddingProvider, err = embedding.CreateProvider(*c.embeddingProviderConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -152,7 +154,7 @@ func (c *PluginConfig) Complete(log wrapper.Log) error {
|
||||
}
|
||||
if c.cacheProviderConfig.GetProviderType() != "" {
|
||||
log.Debugf("cache provider is set to %s", c.cacheProviderConfig.GetProviderType())
|
||||
c.cacheProvider, err = cache.CreateProvider(c.cacheProviderConfig)
|
||||
c.cacheProvider, err = cache.CreateProvider(*c.cacheProviderConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -162,7 +164,7 @@ func (c *PluginConfig) Complete(log wrapper.Log) error {
|
||||
}
|
||||
if c.vectorProviderConfig.GetProviderType() != "" {
|
||||
log.Debugf("vector provider is set to %s", c.vectorProviderConfig.GetProviderType())
|
||||
c.vectorProvider, err = vector.CreateProvider(c.vectorProviderConfig)
|
||||
c.vectorProvider, err = vector.CreateProvider(*c.vectorProviderConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -182,7 +184,7 @@ func (c *PluginConfig) GetVectorProvider() vector.Provider {
|
||||
}
|
||||
|
||||
func (c *PluginConfig) GetVectorProviderConfig() vector.ProviderConfig {
|
||||
return c.vectorProviderConfig
|
||||
return *c.vectorProviderConfig
|
||||
}
|
||||
|
||||
func (c *PluginConfig) GetCacheProvider() cache.Provider {
|
||||
|
||||
@@ -74,6 +74,9 @@ func processCacheHit(key string, response string, stream bool, ctx wrapper.HttpC
|
||||
|
||||
ctx.SetContext(CACHE_KEY_CONTEXT_KEY, nil)
|
||||
|
||||
ctx.SetUserAttribute("cache_status", "hit")
|
||||
ctx.WriteUserAttributeToLogWithKey(wrapper.AILogKey)
|
||||
|
||||
if stream {
|
||||
proxywasm.SendHttpResponseWithDetail(200, "ai-cache.hit", [][2]string{{"content-type", "text/event-stream; charset=utf-8"}}, []byte(fmt.Sprintf(c.StreamResponseTemplate, escapedResponse)), -1)
|
||||
} else {
|
||||
|
||||
158
plugins/wasm-go/extensions/ai-cache/embedding/cohere.go
Normal file
158
plugins/wasm-go/extensions/ai-cache/embedding/cohere.go
Normal file
@@ -0,0 +1,158 @@
|
||||
package embedding
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
const (
|
||||
COHERE_DOMAIN = "api.cohere.com"
|
||||
COHERE_PORT = 443
|
||||
COHERE_DEFAULT_MODEL_NAME = "embed-english-v2.0"
|
||||
COHERE_ENDPOINT = "/v2/embed"
|
||||
)
|
||||
|
||||
type cohereProviderInitializer struct {
|
||||
}
|
||||
|
||||
var cohereConfig cohereProviderConfig
|
||||
|
||||
type cohereProviderConfig struct {
|
||||
// @Title zh-CN 文本特征提取服务 API Key
|
||||
// @Description zh-CN 文本特征提取服务 API Key
|
||||
apiKey string
|
||||
}
|
||||
|
||||
func (c *cohereProviderInitializer) InitConfig(json gjson.Result) {
|
||||
cohereConfig.apiKey = json.Get("apiKey").String()
|
||||
}
|
||||
func (c *cohereProviderInitializer) ValidateConfig() error {
|
||||
if cohereConfig.apiKey == "" {
|
||||
return errors.New("[Cohere] apiKey is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *cohereProviderInitializer) CreateProvider(c ProviderConfig) (Provider, error) {
|
||||
if c.servicePort == 0 {
|
||||
c.servicePort = COHERE_PORT
|
||||
}
|
||||
if c.serviceHost == "" {
|
||||
c.serviceHost = COHERE_DOMAIN
|
||||
}
|
||||
return &CohereProvider{
|
||||
config: c,
|
||||
client: wrapper.NewClusterClient(wrapper.FQDNCluster{
|
||||
FQDN: c.serviceName,
|
||||
Host: c.serviceHost,
|
||||
Port: int64(c.servicePort),
|
||||
}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type cohereResponse struct {
|
||||
Embeddings cohereEmbeddings `json:"embeddings"`
|
||||
}
|
||||
|
||||
type cohereEmbeddings struct {
|
||||
FloatTypeEebedding [][]float64 `json:"float"`
|
||||
}
|
||||
|
||||
type cohereEmbeddingRequest struct {
|
||||
Texts []string `json:"texts"`
|
||||
Model string `json:"model"`
|
||||
InputType string `json:"input_type"`
|
||||
EmbeddingTypes []string `json:"embedding_types"`
|
||||
}
|
||||
|
||||
type CohereProvider struct {
|
||||
config ProviderConfig
|
||||
client wrapper.HttpClient
|
||||
}
|
||||
|
||||
func (t *CohereProvider) GetProviderType() string {
|
||||
return PROVIDER_TYPE_COHERE
|
||||
}
|
||||
func (t *CohereProvider) constructParameters(texts []string, log wrapper.Log) (string, [][2]string, []byte, error) {
|
||||
model := t.config.model
|
||||
|
||||
if model == "" {
|
||||
model = COHERE_DEFAULT_MODEL_NAME
|
||||
}
|
||||
data := cohereEmbeddingRequest{
|
||||
Texts: texts,
|
||||
Model: model,
|
||||
InputType: "search_document",
|
||||
EmbeddingTypes: []string{"float"},
|
||||
}
|
||||
|
||||
requestBody, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
log.Errorf("failed to marshal request data: %v", err)
|
||||
return "", nil, nil, err
|
||||
}
|
||||
|
||||
headers := [][2]string{
|
||||
{"Authorization", fmt.Sprintf("BEARER %s", cohereConfig.apiKey)},
|
||||
{"Content-Type", "application/json"},
|
||||
}
|
||||
|
||||
return COHERE_ENDPOINT, headers, requestBody, nil
|
||||
}
|
||||
|
||||
func (t *CohereProvider) parseTextEmbedding(responseBody []byte) (*cohereResponse, error) {
|
||||
var resp cohereResponse
|
||||
err := json.Unmarshal(responseBody, &resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
func (t *CohereProvider) GetEmbedding(
|
||||
queryString string,
|
||||
ctx wrapper.HttpContext,
|
||||
log wrapper.Log,
|
||||
callback func(emb []float64, err error)) error {
|
||||
embUrl, embHeaders, embRequestBody, err := t.constructParameters([]string{queryString}, log)
|
||||
if err != nil {
|
||||
log.Errorf("failed to construct parameters: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
var resp *cohereResponse
|
||||
err = t.client.Post(embUrl, embHeaders, embRequestBody,
|
||||
func(statusCode int, responseHeaders http.Header, responseBody []byte) {
|
||||
|
||||
if statusCode != http.StatusOK {
|
||||
err = errors.New("failed to get embedding due to status code: " + strconv.Itoa(statusCode))
|
||||
callback(nil, err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Debugf("get embedding response: %d, %s", statusCode, responseBody)
|
||||
|
||||
resp, err = t.parseTextEmbedding(responseBody)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("failed to parse response: %v", err)
|
||||
callback(nil, err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(resp.Embeddings.FloatTypeEebedding) == 0 {
|
||||
err = errors.New("no embedding found in response")
|
||||
callback(nil, err)
|
||||
return
|
||||
}
|
||||
|
||||
callback(resp.Embeddings.FloatTypeEebedding[0], nil)
|
||||
|
||||
}, t.config.timeout)
|
||||
return err
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"strconv"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -17,11 +18,22 @@ const (
|
||||
DASHSCOPE_ENDPOINT = "/api/v1/services/embeddings/text-embedding/text-embedding"
|
||||
)
|
||||
|
||||
var dashScopeConfig dashScopeProviderConfig
|
||||
|
||||
type dashScopeProviderInitializer struct {
|
||||
}
|
||||
type dashScopeProviderConfig struct {
|
||||
// @Title zh-CN 文本特征提取服务 API Key
|
||||
// @Description zh-CN 文本特征提取服务 API Key
|
||||
apiKey string
|
||||
}
|
||||
|
||||
func (d *dashScopeProviderInitializer) ValidateConfig(config ProviderConfig) error {
|
||||
if config.apiKey == "" {
|
||||
func (c *dashScopeProviderInitializer) InitConfig(json gjson.Result) {
|
||||
dashScopeConfig.apiKey = json.Get("apiKey").String()
|
||||
}
|
||||
|
||||
func (c *dashScopeProviderInitializer) ValidateConfig() error {
|
||||
if dashScopeConfig.apiKey == "" {
|
||||
return errors.New("[DashScope] apiKey is required")
|
||||
}
|
||||
return nil
|
||||
@@ -114,14 +126,14 @@ func (d *DSProvider) constructParameters(texts []string, log wrapper.Log) (strin
|
||||
return "", nil, nil, err
|
||||
}
|
||||
|
||||
if d.config.apiKey == "" {
|
||||
if dashScopeConfig.apiKey == "" {
|
||||
err := errors.New("dashScopeKey is empty")
|
||||
log.Errorf("failed to construct headers: %v", err)
|
||||
return "", nil, nil, err
|
||||
}
|
||||
|
||||
headers := [][2]string{
|
||||
{"Authorization", "Bearer " + d.config.apiKey},
|
||||
{"Authorization", "Bearer " + dashScopeConfig.apiKey},
|
||||
{"Content-Type", "application/json"},
|
||||
}
|
||||
|
||||
|
||||
151
plugins/wasm-go/extensions/ai-cache/embedding/ollama.go
Normal file
151
plugins/wasm-go/extensions/ai-cache/embedding/ollama.go
Normal file
@@ -0,0 +1,151 @@
|
||||
package embedding
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
"github.com/tidwall/gjson"
|
||||
"net/http"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
const (
|
||||
OLLAMA_DOMAIN = "localhost"
|
||||
OLLAMA_PORT = 11434
|
||||
OLLAMA_DEFAULT_MODEL_NAME = "llama3.2"
|
||||
OLLAMA_ENDPOINT = "/api/embed"
|
||||
)
|
||||
|
||||
type ollamaProviderInitializer struct {
|
||||
}
|
||||
|
||||
func (c *ollamaProviderInitializer) InitConfig(json gjson.Result) {}
|
||||
|
||||
func (c *ollamaProviderInitializer) ValidateConfig() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type ollamaProvider struct {
|
||||
config ProviderConfig
|
||||
client *wrapper.ClusterClient[wrapper.FQDNCluster]
|
||||
}
|
||||
|
||||
func (t *ollamaProviderInitializer) CreateProvider(c ProviderConfig) (Provider, error) {
|
||||
if c.servicePort == 0 {
|
||||
c.servicePort = OLLAMA_PORT
|
||||
}
|
||||
if c.serviceHost == "" {
|
||||
c.serviceHost = OLLAMA_DOMAIN
|
||||
}
|
||||
if c.model == "" {
|
||||
c.model = OLLAMA_DEFAULT_MODEL_NAME
|
||||
}
|
||||
|
||||
return &ollamaProvider{
|
||||
config: c,
|
||||
client: wrapper.NewClusterClient(wrapper.FQDNCluster{
|
||||
FQDN: c.serviceName,
|
||||
Host: c.serviceHost,
|
||||
Port: c.servicePort,
|
||||
}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t *ollamaProvider) GetProviderType() string {
|
||||
return PROVIDER_TYPE_OLLAMA
|
||||
}
|
||||
|
||||
type ollamaResponse struct {
|
||||
Model string `json:"model"`
|
||||
Embeddings [][]float64 `json:"embeddings"`
|
||||
TotalDuration int64 `json:"total_duration"`
|
||||
LoadDuration int64 `json:"load_duration"`
|
||||
PromptEvalCount int64 `json:"prompt_eval_count"`
|
||||
}
|
||||
|
||||
type ollamaEmbeddingRequest struct {
|
||||
Input string `json:"input"`
|
||||
Model string `json:"model"`
|
||||
}
|
||||
|
||||
func (t *ollamaProvider) constructParameters(text string, log wrapper.Log) (string, [][2]string, []byte, error) {
|
||||
if text == "" {
|
||||
err := errors.New("queryString text cannot be empty")
|
||||
return "", nil, nil, err
|
||||
}
|
||||
|
||||
data := ollamaEmbeddingRequest{
|
||||
Input: text,
|
||||
Model: t.config.model,
|
||||
}
|
||||
|
||||
requestBody, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
log.Errorf("failed to marshal request data: %v", err)
|
||||
return "", nil, nil, err
|
||||
}
|
||||
|
||||
headers := [][2]string{
|
||||
{"Content-Type", "application/json"},
|
||||
}
|
||||
log.Debugf("constructParameters: %s", string(requestBody))
|
||||
|
||||
return OLLAMA_ENDPOINT, headers, requestBody, err
|
||||
}
|
||||
|
||||
func (t *ollamaProvider) parseTextEmbedding(responseBody []byte) (*ollamaResponse, error) {
|
||||
var resp ollamaResponse
|
||||
if err := json.Unmarshal(responseBody, &resp); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse response: %w", err)
|
||||
}
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
func (t *ollamaProvider) GetEmbedding(
|
||||
queryString string,
|
||||
ctx wrapper.HttpContext,
|
||||
log wrapper.Log,
|
||||
callback func(emb []float64, err error)) error {
|
||||
embUrl, embHeaders, embRequestBody, err := t.constructParameters(queryString, log)
|
||||
if err != nil {
|
||||
log.Errorf("failed to construct parameters: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
var resp *ollamaResponse
|
||||
|
||||
defer func() {
|
||||
if err != nil {
|
||||
callback(nil, err)
|
||||
}
|
||||
}()
|
||||
err = t.client.Post(embUrl, embHeaders, embRequestBody,
|
||||
func(statusCode int, responseHeaders http.Header, responseBody []byte) {
|
||||
|
||||
if statusCode != http.StatusOK {
|
||||
err = errors.New("failed to get embedding due to status code: " + strconv.Itoa(statusCode))
|
||||
callback(nil, err)
|
||||
return
|
||||
}
|
||||
|
||||
resp, err = t.parseTextEmbedding(responseBody)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("failed to parse response: %v", err)
|
||||
callback(nil, err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Debugf("get embedding response: %d, %s", statusCode, responseBody)
|
||||
|
||||
if len(resp.Embeddings) == 0 {
|
||||
err = errors.New("no embedding found in response")
|
||||
callback(nil, err)
|
||||
return
|
||||
}
|
||||
|
||||
callback(resp.Embeddings[0], nil)
|
||||
|
||||
}, t.config.timeout)
|
||||
return err
|
||||
}
|
||||
170
plugins/wasm-go/extensions/ai-cache/embedding/openai.go
Normal file
170
plugins/wasm-go/extensions/ai-cache/embedding/openai.go
Normal file
@@ -0,0 +1,170 @@
|
||||
package embedding
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
const (
|
||||
OPENAI_DOMAIN = "api.openai.com"
|
||||
OPENAI_PORT = 443
|
||||
OPENAI_DEFAULT_MODEL_NAME = "text-embedding-3-small"
|
||||
OPENAI_ENDPOINT = "/v1/embeddings"
|
||||
)
|
||||
|
||||
type openAIProviderInitializer struct {
|
||||
}
|
||||
|
||||
var openAIConfig openAIProviderConfig
|
||||
|
||||
type openAIProviderConfig struct {
|
||||
// @Title zh-CN 文本特征提取服务 API Key
|
||||
// @Description zh-CN 文本特征提取服务 API Key
|
||||
apiKey string
|
||||
}
|
||||
|
||||
func (c *openAIProviderInitializer) InitConfig(json gjson.Result) {
|
||||
openAIConfig.apiKey = json.Get("apiKey").String()
|
||||
}
|
||||
|
||||
func (c *openAIProviderInitializer) ValidateConfig() error {
|
||||
if openAIConfig.apiKey == "" {
|
||||
return errors.New("[openAI] apiKey is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *openAIProviderInitializer) CreateProvider(c ProviderConfig) (Provider, error) {
|
||||
if c.servicePort == 0 {
|
||||
c.servicePort = OPENAI_PORT
|
||||
}
|
||||
if c.serviceHost == "" {
|
||||
c.serviceHost = OPENAI_DOMAIN
|
||||
}
|
||||
if c.model == "" {
|
||||
c.model = OPENAI_DEFAULT_MODEL_NAME
|
||||
}
|
||||
return &OpenAIProvider{
|
||||
config: c,
|
||||
client: wrapper.NewClusterClient(wrapper.FQDNCluster{
|
||||
FQDN: c.serviceName,
|
||||
Host: c.serviceHost,
|
||||
Port: c.servicePort,
|
||||
}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t *OpenAIProvider) GetProviderType() string {
|
||||
return PROVIDER_TYPE_OPENAI
|
||||
}
|
||||
|
||||
type OpenAIResponse struct {
|
||||
Object string `json:"object"`
|
||||
Data []OpenAIResult `json:"data"`
|
||||
Model string `json:"model"`
|
||||
Error *OpenAIError `json:"error"`
|
||||
}
|
||||
|
||||
type OpenAIResult struct {
|
||||
Object string `json:"object"`
|
||||
Embedding []float64 `json:"embedding"`
|
||||
Index int `json:"index"`
|
||||
}
|
||||
|
||||
type OpenAIError struct {
|
||||
Message string `json:"prompt_tokens"`
|
||||
Type string `json:"type"`
|
||||
Code string `json:"code"`
|
||||
Param string `json:"param"`
|
||||
}
|
||||
|
||||
type OpenAIEmbeddingRequest struct {
|
||||
Input string `json:"input"`
|
||||
Model string `json:"model"`
|
||||
}
|
||||
|
||||
type OpenAIProvider struct {
|
||||
config ProviderConfig
|
||||
client wrapper.HttpClient
|
||||
}
|
||||
|
||||
func (t *OpenAIProvider) constructParameters(text string, log wrapper.Log) (string, [][2]string, []byte, error) {
|
||||
if text == "" {
|
||||
err := errors.New("queryString text cannot be empty")
|
||||
return "", nil, nil, err
|
||||
}
|
||||
|
||||
data := OpenAIEmbeddingRequest{
|
||||
Input: text,
|
||||
Model: t.config.model,
|
||||
}
|
||||
|
||||
requestBody, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
log.Errorf("failed to marshal request data: %v", err)
|
||||
return "", nil, nil, err
|
||||
}
|
||||
|
||||
headers := [][2]string{
|
||||
{"Authorization", fmt.Sprintf("Bearer %s", openAIConfig.apiKey)},
|
||||
{"Content-Type", "application/json"},
|
||||
}
|
||||
|
||||
return OPENAI_ENDPOINT, headers, requestBody, err
|
||||
}
|
||||
|
||||
func (t *OpenAIProvider) parseTextEmbedding(responseBody []byte) (*OpenAIResponse, error) {
|
||||
var resp OpenAIResponse
|
||||
err := json.Unmarshal(responseBody, &resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
func (t *OpenAIProvider) GetEmbedding(
|
||||
queryString string,
|
||||
ctx wrapper.HttpContext,
|
||||
log wrapper.Log,
|
||||
callback func(emb []float64, err error)) error {
|
||||
embUrl, embHeaders, embRequestBody, err := t.constructParameters(queryString, log)
|
||||
if err != nil {
|
||||
log.Errorf("failed to construct parameters: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
var resp *OpenAIResponse
|
||||
err = t.client.Post(embUrl, embHeaders, embRequestBody,
|
||||
func(statusCode int, responseHeaders http.Header, responseBody []byte) {
|
||||
|
||||
if statusCode != http.StatusOK {
|
||||
err = fmt.Errorf("failed to get embedding due to status code: %d, resp: %s", statusCode, responseBody)
|
||||
callback(nil, err)
|
||||
return
|
||||
}
|
||||
|
||||
resp, err = t.parseTextEmbedding(responseBody)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("failed to parse response: %v", err)
|
||||
callback(nil, err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Debugf("get embedding response: %d, %s", statusCode, responseBody)
|
||||
|
||||
if len(resp.Data) == 0 {
|
||||
err = errors.New("no embedding found in response")
|
||||
callback(nil, err)
|
||||
return
|
||||
}
|
||||
|
||||
callback(resp.Data[0].Embedding, nil)
|
||||
|
||||
}, t.config.timeout)
|
||||
return err
|
||||
}
|
||||
@@ -10,10 +10,14 @@ import (
|
||||
const (
|
||||
PROVIDER_TYPE_DASHSCOPE = "dashscope"
|
||||
PROVIDER_TYPE_TEXTIN = "textin"
|
||||
PROVIDER_TYPE_COHERE = "cohere"
|
||||
PROVIDER_TYPE_OPENAI = "openai"
|
||||
PROVIDER_TYPE_OLLAMA = "ollama"
|
||||
)
|
||||
|
||||
type providerInitializer interface {
|
||||
ValidateConfig(ProviderConfig) error
|
||||
InitConfig(json gjson.Result)
|
||||
ValidateConfig() error
|
||||
CreateProvider(ProviderConfig) (Provider, error)
|
||||
}
|
||||
|
||||
@@ -21,6 +25,9 @@ var (
|
||||
providerInitializers = map[string]providerInitializer{
|
||||
PROVIDER_TYPE_DASHSCOPE: &dashScopeProviderInitializer{},
|
||||
PROVIDER_TYPE_TEXTIN: &textInProviderInitializer{},
|
||||
PROVIDER_TYPE_COHERE: &cohereProviderInitializer{},
|
||||
PROVIDER_TYPE_OPENAI: &openAIProviderInitializer{},
|
||||
PROVIDER_TYPE_OLLAMA: &ollamaProviderInitializer{},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -37,35 +44,26 @@ type ProviderConfig struct {
|
||||
// @Title zh-CN 文本特征提取服务端口
|
||||
// @Description zh-CN 文本特征提取服务端口
|
||||
servicePort int64
|
||||
// @Title zh-CN 文本特征提取服务 API Key
|
||||
// @Description zh-CN 文本特征提取服务 API Key
|
||||
apiKey string
|
||||
//@Title zh-CN TextIn x-ti-app-id
|
||||
// @Description zh-CN 仅适用于 TextIn 服务。参考 https://www.textin.com/document/acge_text_embedding
|
||||
textinAppId string
|
||||
//@Title zh-CN TextIn x-ti-secret-code
|
||||
// @Description zh-CN 仅适用于 TextIn 服务。参考 https://www.textin.com/document/acge_text_embedding
|
||||
textinSecretCode string
|
||||
//@Title zh-CN TextIn request matryoshka_dim
|
||||
// @Description zh-CN 仅适用于 TextIn 服务, 指定返回的向量维度。参考 https://www.textin.com/document/acge_text_embedding
|
||||
textinMatryoshkaDim int
|
||||
// @Title zh-CN 文本特征提取服务超时时间
|
||||
// @Description zh-CN 文本特征提取服务超时时间
|
||||
timeout uint32
|
||||
// @Title zh-CN 文本特征提取服务使用的模型
|
||||
// @Description zh-CN 用于文本特征提取的模型名称, 在 DashScope 中默认为 "text-embedding-v1"
|
||||
model string
|
||||
|
||||
initializer providerInitializer
|
||||
}
|
||||
|
||||
func (c *ProviderConfig) FromJson(json gjson.Result) {
|
||||
c.typ = json.Get("type").String()
|
||||
i, has := providerInitializers[c.typ]
|
||||
if has {
|
||||
i.InitConfig(json)
|
||||
c.initializer = i
|
||||
}
|
||||
c.serviceName = json.Get("serviceName").String()
|
||||
c.serviceHost = json.Get("serviceHost").String()
|
||||
c.servicePort = json.Get("servicePort").Int()
|
||||
c.apiKey = json.Get("apiKey").String()
|
||||
c.textinAppId = json.Get("textinAppId").String()
|
||||
c.textinSecretCode = json.Get("textinSecretCode").String()
|
||||
c.textinMatryoshkaDim = int(json.Get("textinMatryoshkaDim").Int())
|
||||
c.timeout = uint32(json.Get("timeout").Int())
|
||||
c.model = json.Get("model").String()
|
||||
if c.timeout == 0 {
|
||||
@@ -80,11 +78,10 @@ func (c *ProviderConfig) Validate() error {
|
||||
if c.typ == "" {
|
||||
return errors.New("embedding service type is required")
|
||||
}
|
||||
initializer, has := providerInitializers[c.typ]
|
||||
if !has {
|
||||
if c.initializer == nil {
|
||||
return errors.New("unknown embedding service provider type: " + c.typ)
|
||||
}
|
||||
if err := initializer.ValidateConfig(*c); err != nil {
|
||||
if err := c.initializer.ValidateConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"strconv"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -20,14 +21,34 @@ const (
|
||||
type textInProviderInitializer struct {
|
||||
}
|
||||
|
||||
func (t *textInProviderInitializer) ValidateConfig(config ProviderConfig) error {
|
||||
if config.textinAppId == "" {
|
||||
return errors.New("embedding service TextIn App ID is required")
|
||||
var textInConfig textInProviderConfig
|
||||
|
||||
type textInProviderConfig struct {
|
||||
//@Title zh-CN TextIn x-ti-app-id
|
||||
// @Description zh-CN 仅适用于 TextIn 服务。参考 https://www.textin.com/document/acge_text_embedding
|
||||
textinAppId string
|
||||
//@Title zh-CN TextIn x-ti-secret-code
|
||||
// @Description zh-CN 仅适用于 TextIn 服务。参考 https://www.textin.com/document/acge_text_embedding
|
||||
textinSecretCode string
|
||||
//@Title zh-CN TextIn request matryoshka_dim
|
||||
// @Description zh-CN 仅适用于 TextIn 服务, 指定返回的向量维度。参考 https://www.textin.com/document/acge_text_embedding
|
||||
textinMatryoshkaDim int
|
||||
}
|
||||
|
||||
func (c *textInProviderInitializer) InitConfig(json gjson.Result) {
|
||||
textInConfig.textinAppId = json.Get("textinAppId").String()
|
||||
textInConfig.textinSecretCode = json.Get("textinSecretCode").String()
|
||||
textInConfig.textinMatryoshkaDim = int(json.Get("textinMatryoshkaDim").Int())
|
||||
}
|
||||
|
||||
func (c *textInProviderInitializer) ValidateConfig() error {
|
||||
if textInConfig.textinAppId == "" {
|
||||
return errors.New("textinAppId is required")
|
||||
}
|
||||
if config.textinSecretCode == "" {
|
||||
return errors.New("embedding service TextIn Secret Code is required")
|
||||
if textInConfig.textinSecretCode == "" {
|
||||
return errors.New("textinSecretCode is required")
|
||||
}
|
||||
if config.textinMatryoshkaDim == 0 {
|
||||
if textInConfig.textinMatryoshkaDim == 0 {
|
||||
return errors.New("embedding service TextIn Matryoshka Dim is required")
|
||||
}
|
||||
return nil
|
||||
@@ -62,7 +83,7 @@ type TextInResponse struct {
|
||||
}
|
||||
|
||||
type TextInResult struct {
|
||||
Embeddings [][]float64 `json:"embedding"`
|
||||
Embeddings [][]float64 `json:"embedding"`
|
||||
MatryoshkaDim int `json:"matryoshka_dim"`
|
||||
}
|
||||
|
||||
@@ -80,7 +101,7 @@ func (t *TIProvider) constructParameters(texts []string, log wrapper.Log) (strin
|
||||
|
||||
data := TextInEmbeddingRequest{
|
||||
Input: texts,
|
||||
MatryoshkaDim: t.config.textinMatryoshkaDim,
|
||||
MatryoshkaDim: textInConfig.textinMatryoshkaDim,
|
||||
}
|
||||
|
||||
requestBody, err := json.Marshal(data)
|
||||
@@ -89,20 +110,20 @@ func (t *TIProvider) constructParameters(texts []string, log wrapper.Log) (strin
|
||||
return "", nil, nil, err
|
||||
}
|
||||
|
||||
if t.config.textinAppId == "" {
|
||||
if textInConfig.textinAppId == "" {
|
||||
err := errors.New("textinAppId is empty")
|
||||
log.Errorf("failed to construct headers: %v", err)
|
||||
return "", nil, nil, err
|
||||
}
|
||||
if t.config.textinSecretCode == "" {
|
||||
if textInConfig.textinSecretCode == "" {
|
||||
err := errors.New("textinSecretCode is empty")
|
||||
log.Errorf("failed to construct headers: %v", err)
|
||||
return "", nil, nil, err
|
||||
}
|
||||
|
||||
headers := [][2]string{
|
||||
{"x-ti-app-id", t.config.textinAppId},
|
||||
{"x-ti-secret-code", t.config.textinSecretCode},
|
||||
{"x-ti-app-id", textInConfig.textinAppId},
|
||||
{"x-ti-secret-code", textInConfig.textinSecretCode},
|
||||
{"Content-Type", "application/json"},
|
||||
}
|
||||
|
||||
|
||||
@@ -8,14 +8,14 @@ replace github.com/alibaba/higress/plugins/wasm-go => ../..
|
||||
|
||||
require (
|
||||
github.com/alibaba/higress/plugins/wasm-go v1.4.2
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240711023527-ba358c48772f
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/higress-group/proxy-wasm-go-sdk v1.0.0
|
||||
github.com/tidwall/gjson v1.17.3
|
||||
github.com/tidwall/resp v0.1.1
|
||||
// github.com/weaviate/weaviate-go-client/v4 v4.15.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 // indirect
|
||||
github.com/magefile/mage v1.14.0 // indirect
|
||||
github.com/stretchr/testify v1.9.0 // indirect
|
||||
|
||||
@@ -3,8 +3,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 h1:IHDghbGQ2DTIXHBHxWfqCYQW1fKjyJ/I7W1pMyUDeEA=
|
||||
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520/go.mod h1:Nz8ORLaFiLWotg6GeKlJMhv8cci8mM43uEnLA5t8iew=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240711023527-ba358c48772f h1:ZIiIBRvIw62gA5MJhuwp1+2wWbqL9IGElQ499rUsYYg=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240711023527-ba358c48772f/go.mod h1:hNFjhrLUIq+kJ9bOcs8QtiplSQ61GZXtd2xHKx4BYRo=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v1.0.0 h1:BZRNf4R7jr9hwRivg/E29nkVaKEak5MWjBDhWjuHijU=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v1.0.0/go.mod h1:iiSyFbo+rAtbtGt/bsefv8GU57h9CCLYGJA74/tF5/0=
|
||||
github.com/magefile/mage v1.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo=
|
||||
github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
|
||||
@@ -22,6 +22,8 @@ const (
|
||||
STREAM_CONTEXT_KEY = "stream"
|
||||
SKIP_CACHE_HEADER = "x-higress-skip-ai-cache"
|
||||
ERROR_PARTIAL_MESSAGE_KEY = "errorPartialMessage"
|
||||
|
||||
DEFAULT_MAX_BODY_BYTES uint32 = 100 * 1024 * 1024
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -69,6 +71,7 @@ func onHttpRequestHeaders(ctx wrapper.HttpContext, c config.PluginConfig, log wr
|
||||
ctx.DontReadRequestBody()
|
||||
return types.ActionContinue
|
||||
}
|
||||
ctx.SetRequestBodyBufferLimit(DEFAULT_MAX_BODY_BYTES)
|
||||
_ = proxywasm.RemoveHttpRequestHeader("Accept-Encoding")
|
||||
// The request has a body and requires delaying the header transmission until a cache miss occurs,
|
||||
// at which point the header should be sent.
|
||||
@@ -101,11 +104,11 @@ func onHttpRequestBody(ctx wrapper.HttpContext, c config.PluginConfig, body []by
|
||||
key = strings.Join(userMessages, "\n")
|
||||
} else if c.CacheKeyStrategy == config.CACHE_KEY_STRATEGY_DISABLED {
|
||||
log.Info("[onHttpRequestBody] cache key strategy is disabled")
|
||||
ctx.DontReadRequestBody()
|
||||
ctx.DontReadResponseBody()
|
||||
return types.ActionContinue
|
||||
} else {
|
||||
log.Warnf("[onHttpRequestBody] unknown cache key strategy: %s", c.CacheKeyStrategy)
|
||||
ctx.DontReadRequestBody()
|
||||
ctx.DontReadResponseBody()
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
@@ -128,17 +131,20 @@ func onHttpRequestBody(ctx wrapper.HttpContext, c config.PluginConfig, body []by
|
||||
func onHttpResponseHeaders(ctx wrapper.HttpContext, c config.PluginConfig, log wrapper.Log) types.Action {
|
||||
skipCache := ctx.GetContext(SKIP_CACHE_HEADER)
|
||||
if skipCache != nil {
|
||||
ctx.SetUserAttribute("cache_status", "skip")
|
||||
ctx.WriteUserAttributeToLogWithKey(wrapper.AILogKey)
|
||||
ctx.DontReadResponseBody()
|
||||
return types.ActionContinue
|
||||
}
|
||||
if ctx.GetContext(CACHE_KEY_CONTEXT_KEY) != nil {
|
||||
ctx.SetUserAttribute("cache_status", "miss")
|
||||
ctx.WriteUserAttributeToLogWithKey(wrapper.AILogKey)
|
||||
}
|
||||
contentType, _ := proxywasm.GetHttpResponseHeader("content-type")
|
||||
if strings.Contains(contentType, "text/event-stream") {
|
||||
ctx.SetContext(STREAM_CONTEXT_KEY, struct{}{})
|
||||
}
|
||||
|
||||
if ctx.GetContext(ERROR_PARTIAL_MESSAGE_KEY) != nil {
|
||||
ctx.DontReadResponseBody()
|
||||
return types.ActionContinue
|
||||
} else {
|
||||
ctx.SetResponseBodyBufferLimit(DEFAULT_MAX_BODY_BYTES)
|
||||
}
|
||||
|
||||
return types.ActionContinue
|
||||
@@ -148,7 +154,7 @@ func onHttpResponseBody(ctx wrapper.HttpContext, c config.PluginConfig, chunk []
|
||||
log.Debugf("[onHttpResponseBody] is last chunk: %v", isLastChunk)
|
||||
log.Debugf("[onHttpResponseBody] chunk: %s", string(chunk))
|
||||
|
||||
if ctx.GetContext(TOOL_CALLS_CONTEXT_KEY) != nil {
|
||||
if ctx.GetContext(TOOL_CALLS_CONTEXT_KEY) != nil || ctx.GetContext(ERROR_PARTIAL_MESSAGE_KEY) != nil {
|
||||
return chunk
|
||||
}
|
||||
|
||||
@@ -158,22 +164,26 @@ func onHttpResponseBody(ctx wrapper.HttpContext, c config.PluginConfig, chunk []
|
||||
return chunk
|
||||
}
|
||||
|
||||
stream := ctx.GetContext(STREAM_CONTEXT_KEY)
|
||||
var err error
|
||||
if !isLastChunk {
|
||||
if err := handleNonLastChunk(ctx, c, chunk, log); err != nil {
|
||||
if stream == nil {
|
||||
err = handleNonStreamChunk(ctx, c, chunk, log)
|
||||
} else {
|
||||
err = handleStreamChunk(ctx, c, unifySSEChunk(chunk), log)
|
||||
}
|
||||
if err != nil {
|
||||
log.Errorf("[onHttpResponseBody] handle non last chunk failed, error: %v", err)
|
||||
// Set an empty struct in the context to indicate an error in processing the partial message
|
||||
ctx.SetContext(ERROR_PARTIAL_MESSAGE_KEY, struct{}{})
|
||||
}
|
||||
return chunk
|
||||
}
|
||||
|
||||
stream := ctx.GetContext(STREAM_CONTEXT_KEY)
|
||||
var value string
|
||||
var err error
|
||||
if stream == nil {
|
||||
value, err = processNonStreamLastChunk(ctx, c, chunk, log)
|
||||
} else {
|
||||
value, err = processStreamLastChunk(ctx, c, chunk, log)
|
||||
value, err = processStreamLastChunk(ctx, c, unifySSEChunk(chunk), log)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
@@ -9,17 +10,6 @@ import (
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
func handleNonLastChunk(ctx wrapper.HttpContext, c config.PluginConfig, chunk []byte, log wrapper.Log) error {
|
||||
stream := ctx.GetContext(STREAM_CONTEXT_KEY)
|
||||
err := error(nil)
|
||||
if stream == nil {
|
||||
err = handleNonStreamChunk(ctx, c, chunk, log)
|
||||
} else {
|
||||
err = handleStreamChunk(ctx, c, chunk, log)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func handleNonStreamChunk(ctx wrapper.HttpContext, c config.PluginConfig, chunk []byte, log wrapper.Log) error {
|
||||
tempContentI := ctx.GetContext(CACHE_CONTENT_CONTEXT_KEY)
|
||||
if tempContentI == nil {
|
||||
@@ -32,6 +22,12 @@ func handleNonStreamChunk(ctx wrapper.HttpContext, c config.PluginConfig, chunk
|
||||
return nil
|
||||
}
|
||||
|
||||
func unifySSEChunk(data []byte) []byte {
|
||||
data = bytes.ReplaceAll(data, []byte("\r\n"), []byte("\n"))
|
||||
data = bytes.ReplaceAll(data, []byte("\r"), []byte("\n"))
|
||||
return data
|
||||
}
|
||||
|
||||
func handleStreamChunk(ctx wrapper.HttpContext, c config.PluginConfig, chunk []byte, log wrapper.Log) error {
|
||||
var partialMessage []byte
|
||||
partialMessageI := ctx.GetContext(PARTIAL_MESSAGE_CONTEXT_KEY)
|
||||
@@ -101,55 +97,54 @@ func processStreamLastChunk(ctx wrapper.HttpContext, c config.PluginConfig, chun
|
||||
}
|
||||
|
||||
func processSSEMessage(ctx wrapper.HttpContext, c config.PluginConfig, sseMessage string, log wrapper.Log) (string, error) {
|
||||
subMessages := strings.Split(sseMessage, "\n")
|
||||
var message string
|
||||
for _, msg := range subMessages {
|
||||
if strings.HasPrefix(msg, "data:") {
|
||||
message = msg
|
||||
break
|
||||
content := ""
|
||||
for _, chunk := range strings.Split(sseMessage, "\n\n") {
|
||||
log.Debugf("single sse message: %s", chunk)
|
||||
subMessages := strings.Split(chunk, "\n")
|
||||
var message string
|
||||
for _, msg := range subMessages {
|
||||
if strings.HasPrefix(msg, "data:") {
|
||||
message = msg
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(message) < 6 {
|
||||
return "", fmt.Errorf("[processSSEMessage] invalid message: %s", message)
|
||||
}
|
||||
|
||||
// skip the prefix "data:"
|
||||
bodyJson := message[5:]
|
||||
|
||||
if strings.TrimSpace(bodyJson) == "[DONE]" {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// Extract values from JSON fields
|
||||
responseBody := gjson.Get(bodyJson, c.CacheStreamValueFrom)
|
||||
toolCalls := gjson.Get(bodyJson, c.CacheToolCallsFrom)
|
||||
|
||||
if toolCalls.Exists() {
|
||||
// TODO: Temporarily store the tool_calls value in the context for processing
|
||||
ctx.SetContext(TOOL_CALLS_CONTEXT_KEY, toolCalls.String())
|
||||
}
|
||||
|
||||
// Check if the ResponseBody field exists
|
||||
if !responseBody.Exists() {
|
||||
if ctx.GetContext(CACHE_CONTENT_CONTEXT_KEY) != nil {
|
||||
log.Debugf("[processSSEMessage] unable to extract content from message; cache content is not nil: %s", message)
|
||||
return "", nil
|
||||
if len(message) < 6 {
|
||||
return content, fmt.Errorf("[processSSEMessage] invalid message: %s", message)
|
||||
}
|
||||
return "", fmt.Errorf("[processSSEMessage] unable to extract content from message; cache content is nil: %s", message)
|
||||
} else {
|
||||
tempContentI := ctx.GetContext(CACHE_CONTENT_CONTEXT_KEY)
|
||||
|
||||
// If there is no content in the cache, initialize and set the content
|
||||
if tempContentI == nil {
|
||||
content := responseBody.String()
|
||||
ctx.SetContext(CACHE_CONTENT_CONTEXT_KEY, content)
|
||||
// skip the prefix "data:"
|
||||
bodyJson := message[5:]
|
||||
|
||||
if strings.TrimSpace(bodyJson) == "[DONE]" {
|
||||
return content, nil
|
||||
}
|
||||
|
||||
// Update the content in the cache
|
||||
appendMsg := responseBody.String()
|
||||
content := tempContentI.(string) + appendMsg
|
||||
ctx.SetContext(CACHE_CONTENT_CONTEXT_KEY, content)
|
||||
return content, nil
|
||||
// Extract values from JSON fields
|
||||
responseBody := gjson.Get(bodyJson, c.CacheStreamValueFrom)
|
||||
toolCalls := gjson.Get(bodyJson, c.CacheToolCallsFrom)
|
||||
|
||||
if toolCalls.Exists() {
|
||||
// TODO: Temporarily store the tool_calls value in the context for processing
|
||||
ctx.SetContext(TOOL_CALLS_CONTEXT_KEY, toolCalls.String())
|
||||
}
|
||||
|
||||
// Check if the ResponseBody field exists
|
||||
if !responseBody.Exists() {
|
||||
if ctx.GetContext(CACHE_CONTENT_CONTEXT_KEY) != nil {
|
||||
log.Debugf("[processSSEMessage] unable to extract content from message; cache content is not nil: %s", message)
|
||||
return content, nil
|
||||
}
|
||||
return content, fmt.Errorf("[processSSEMessage] unable to extract content from message; cache content is nil: %s", message)
|
||||
} else {
|
||||
content += responseBody.String()
|
||||
}
|
||||
}
|
||||
tempContentI := ctx.GetContext(CACHE_CONTENT_CONTEXT_KEY)
|
||||
// If there is no content in the cache, initialize and set the content
|
||||
if tempContentI == nil {
|
||||
ctx.SetContext(CACHE_CONTENT_CONTEXT_KEY, content)
|
||||
} else {
|
||||
ctx.SetContext(CACHE_CONTENT_CONTEXT_KEY, tempContentI.(string)+content)
|
||||
}
|
||||
return content, nil
|
||||
}
|
||||
|
||||
@@ -20,17 +20,18 @@ description: AI 历史对话插件配置参考
|
||||
|
||||
## 配置字段
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | Description |
|
||||
|-------------------|---------|----------|-----------------------|---------------------------------------------------------------------------|
|
||||
| identityHeader | string | optional | "Authorization" | 身份解析对应的请求头,可用 Authorization,X-Mse-Consumer等 |
|
||||
| fillHistoryCnt | integer | optional | 3 | 默认填充历史对话轮次 |
|
||||
| cacheKeyPrefix | string | optional | "higress-ai-history:" | Redis缓存Key的前缀 |
|
||||
| cacheTTL | integer | optional | 0 | 缓存的过期时间,单位是秒,默认值为0,即永不过期 |
|
||||
| redis.serviceName | string | required | - | redis 服务名称,带服务类型的完整 FQDN 名称,例如 my-redis.dns、redis.my-ns.svc.cluster.local |
|
||||
| redis.servicePort | integer | optional | 6379 | redis 服务端口 |
|
||||
| redis.timeout | integer | optional | 1000 | 请求 redis 的超时时间,单位为毫秒 |
|
||||
| redis.username | string | optional | - | 登陆 redis 的用户名 |
|
||||
| redis.password | string | optional | - | 登陆 redis 的密码 |
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | Description |
|
||||
|-------------------|----------|----------|-----------------------|----------------------------------------------------------------------------------------------|
|
||||
| identityHeader | string | optional | "Authorization" | 身份解析对应的请求头,可用 Authorization,X-Mse-Consumer等 |
|
||||
| fillHistoryCnt | integer | optional | 3 | 默认填充历史对话轮次 |
|
||||
| cacheKeyPrefix | string | optional | "higress-ai-history:" | Redis缓存Key的前缀 |
|
||||
| cacheTTL | integer | optional | 0 | 缓存的过期时间,单位是秒,默认值为0,即永不过期 |
|
||||
| redis.serviceName | string | required | - | redis 服务名称,带服务类型的完整 FQDN 名称,例如 my-redis.dns、redis.my-ns.svc.cluster.local |
|
||||
| redis.servicePort | integer | optional | 6379 | redis 服务端口 |
|
||||
| redis.timeout | integer | optional | 1000 | 请求 redis 的超时时间,单位为毫秒 |
|
||||
| redis.username | string | optional | - | 登陆 redis 的用户名 |
|
||||
| redis.password | string | optional | - | 登陆 redis 的密码 |
|
||||
| redis.database | int | optional | 0 | 使用的数据库id,例如配置为1,对应`SELECT 1` |
|
||||
|
||||
## 用法示例
|
||||
|
||||
|
||||
@@ -15,17 +15,19 @@ Plugin Execution Phase: `Default Phase`
|
||||
Plugin Execution Priority: `650`
|
||||
|
||||
## Configuration Fields
|
||||
| Name | Data Type | Required | Default Value | Description |
|
||||
|-------------------|---------|----------|-----------------------|---------------------------------------------------------------------------|
|
||||
| identityHeader | string | optional | "Authorization" | The request header for identity resolution, can be Authorization, X-Mse-Consumer, etc. |
|
||||
| fillHistoryCnt | integer | optional | 3 | Default number of historical dialogues to be filled. |
|
||||
| cacheKeyPrefix | string | optional | "higress-ai-history:" | Prefix for Redis cache key. |
|
||||
| cacheTTL | integer | optional | 0 | Cache expiration time in seconds, default value is 0, meaning it never expires. |
|
||||
| redis.serviceName | string | required | - | Redis service name, full FQDN name with service type, e.g., my-redis.dns, redis.my-ns.svc.cluster.local |
|
||||
| redis.servicePort | integer | optional | 6379 | Redis service port. |
|
||||
| redis.timeout | integer | optional | 1000 | Timeout for requests to Redis, in milliseconds. |
|
||||
| redis.username | string | optional | - | Username for logging into Redis. |
|
||||
| redis.password | string | optional | - | Password for logging into Redis. |
|
||||
| Name | Data Type | Required | Default Value | Description |
|
||||
|-------------------|-----------|----------|-----------------------|---------------------------------------------------------------------------------------------------------|
|
||||
| identityHeader | string | optional | "Authorization" | The request header for identity resolution, can be Authorization, X-Mse-Consumer, etc. |
|
||||
| fillHistoryCnt | integer | optional | 3 | Default number of historical dialogues to be filled. |
|
||||
| cacheKeyPrefix | string | optional | "higress-ai-history:" | Prefix for Redis cache key. |
|
||||
| cacheTTL | integer | optional | 0 | Cache expiration time in seconds, default value is 0, meaning it never expires. |
|
||||
| redis.serviceName | string | required | - | Redis service name, full FQDN name with service type, e.g., my-redis.dns, redis.my-ns.svc.cluster.local |
|
||||
| redis.servicePort | integer | optional | 6379 | Redis service port. |
|
||||
| redis.timeout | integer | optional | 1000 | Timeout for requests to Redis, in milliseconds. |
|
||||
| redis.username | string | optional | - | Username for logging into Redis. |
|
||||
| redis.password | string | optional | - | Password for logging into Redis. |
|
||||
| redis.database | int | optional | 0 | The database ID used, for example, configured as 1, corresponds to `SELECT 1`. |
|
||||
|
||||
|
||||
## Usage Example
|
||||
### Configuration Information
|
||||
|
||||
@@ -3,15 +3,13 @@ github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 h1:IHDghbGQ2DTIXHBHxWfqCYQW1fKjyJ/I7W1pMyUDeEA=
|
||||
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520/go.mod h1:Nz8ORLaFiLWotg6GeKlJMhv8cci8mM43uEnLA5t8iew=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240711023527-ba358c48772f h1:ZIiIBRvIw62gA5MJhuwp1+2wWbqL9IGElQ499rUsYYg=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240711023527-ba358c48772f/go.mod h1:hNFjhrLUIq+kJ9bOcs8QtiplSQ61GZXtd2xHKx4BYRo=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v1.0.0 h1:BZRNf4R7jr9hwRivg/E29nkVaKEak5MWjBDhWjuHijU=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v1.0.0/go.mod h1:iiSyFbo+rAtbtGt/bsefv8GU57h9CCLYGJA74/tF5/0=
|
||||
github.com/magefile/mage v1.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo=
|
||||
github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/tidwall/gjson v1.14.3 h1:9jvXn7olKEHU1S9vwoMGliaT8jq1vJ7IH/n9zD9Dnlw=
|
||||
github.com/tidwall/gjson v1.14.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.17.3 h1:bwWLZU7icoKRG+C+0PNwIKC6FCJO/Q3p2pZvuP0jN94=
|
||||
github.com/tidwall/gjson v1.17.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
|
||||
@@ -76,6 +76,9 @@ type RedisInfo struct {
|
||||
// @Title zh-CN 请求超时
|
||||
// @Description zh-CN 请求 redis 的超时时间,单位为毫秒。默认值是1000,即1秒
|
||||
Timeout int `required:"false" yaml:"timeout" json:"timeout"`
|
||||
// @Title zh-CN Database
|
||||
// @Description zh-CN redis database
|
||||
Database int `required:"false" yaml:"database" json:"database"`
|
||||
}
|
||||
|
||||
type KVExtractor struct {
|
||||
@@ -138,6 +141,7 @@ func parseConfig(json gjson.Result, c *PluginConfig, log wrapper.Log) error {
|
||||
if c.RedisInfo.Timeout == 0 {
|
||||
c.RedisInfo.Timeout = 1000
|
||||
}
|
||||
c.RedisInfo.Database = int(json.Get("redis.database").Int())
|
||||
c.QuestionFrom.RequestBody = "messages.@reverse.0.content"
|
||||
c.AnswerValueFrom.ResponseBody = "choices.0.message.content"
|
||||
c.AnswerStreamValueFrom.ResponseBody = "choices.0.delta.content"
|
||||
@@ -159,7 +163,7 @@ func parseConfig(json gjson.Result, c *PluginConfig, log wrapper.Log) error {
|
||||
FQDN: c.RedisInfo.ServiceName,
|
||||
Port: int64(c.RedisInfo.ServicePort),
|
||||
})
|
||||
return c.redisClient.Init(c.RedisInfo.Username, c.RedisInfo.Password, int64(c.RedisInfo.Timeout))
|
||||
return c.redisClient.Init(c.RedisInfo.Username, c.RedisInfo.Password, int64(c.RedisInfo.Timeout), wrapper.WithDataBase(c.RedisInfo.Database))
|
||||
}
|
||||
|
||||
func onHttpRequestHeaders(ctx wrapper.HttpContext, config PluginConfig, log wrapper.Log) types.Action {
|
||||
@@ -194,6 +198,12 @@ func onHttpRequestBody(ctx wrapper.HttpContext, config PluginConfig, body []byte
|
||||
ctx.SetContext(StreamContextKey, struct{}{})
|
||||
}
|
||||
identityKey := ctx.GetStringContext(IdentityKey, "")
|
||||
question := TrimQuote(bodyJson.Get(config.QuestionFrom.RequestBody).String())
|
||||
if question == "" {
|
||||
log.Debug("parse question from request body failed")
|
||||
return types.ActionContinue
|
||||
}
|
||||
ctx.SetContext(QuestionContextKey, question)
|
||||
err := config.redisClient.Get(config.CacheKeyPrefix+identityKey, func(response resp.Value) {
|
||||
if err := response.Error(); err != nil {
|
||||
log.Errorf("redis get failed, err:%v", err)
|
||||
@@ -230,13 +240,6 @@ func onHttpRequestBody(ctx wrapper.HttpContext, config PluginConfig, body []byte
|
||||
_ = proxywasm.SendHttpResponseWithDetail(200, "OK", [][2]string{{"content-type", "application/json; charset=utf-8"}}, res, -1)
|
||||
return
|
||||
}
|
||||
question := TrimQuote(bodyJson.Get(config.QuestionFrom.RequestBody).String())
|
||||
if question == "" {
|
||||
log.Debug("parse question from request body failed")
|
||||
_ = proxywasm.ResumeHttpRequest()
|
||||
return
|
||||
}
|
||||
ctx.SetContext(QuestionContextKey, question)
|
||||
fillHistoryCnt := getIntQueryParameter("fill_history_cnt", path, config.FillHistoryCnt) * 2
|
||||
currJson := bodyJson.Get("messages").String()
|
||||
var currMessage []ChatHistory
|
||||
@@ -317,38 +320,39 @@ func getIntQueryParameter(name string, path string, defaultValue int) int {
|
||||
}
|
||||
|
||||
func processSSEMessage(ctx wrapper.HttpContext, config PluginConfig, sseMessage string, log wrapper.Log) string {
|
||||
subMessages := strings.Split(sseMessage, "\n")
|
||||
var message string
|
||||
for _, msg := range subMessages {
|
||||
if strings.HasPrefix(msg, "data:") {
|
||||
message = msg
|
||||
break
|
||||
content := ""
|
||||
for _, chunk := range strings.Split(sseMessage, "\n\n") {
|
||||
subMessages := strings.Split(chunk, "\n")
|
||||
var message string
|
||||
for _, msg := range subMessages {
|
||||
if strings.HasPrefix(msg, "data:") {
|
||||
message = msg
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(message) < 6 {
|
||||
log.Errorf("invalid message:%s", message)
|
||||
return ""
|
||||
}
|
||||
// skip the prefix "data:"
|
||||
bodyJson := message[5:]
|
||||
if gjson.Get(bodyJson, config.AnswerStreamValueFrom.ResponseBody).Exists() {
|
||||
tempContentI := ctx.GetContext(AnswerContentContextKey)
|
||||
if tempContentI == nil {
|
||||
content := TrimQuote(gjson.Get(bodyJson, config.AnswerStreamValueFrom.ResponseBody).Raw)
|
||||
ctx.SetContext(AnswerContentContextKey, content)
|
||||
if len(message) < 6 {
|
||||
log.Errorf("invalid message:%s", message)
|
||||
return content
|
||||
}
|
||||
append := TrimQuote(gjson.Get(bodyJson, config.AnswerStreamValueFrom.ResponseBody).Raw)
|
||||
content := tempContentI.(string) + append
|
||||
ctx.SetContext(AnswerContentContextKey, content)
|
||||
return content
|
||||
} else if gjson.Get(bodyJson, "choices.0.delta.content.tool_calls").Exists() {
|
||||
// TODO: compatible with other providers
|
||||
ctx.SetContext(ToolCallsContextKey, struct{}{})
|
||||
return ""
|
||||
// skip the prefix "data:"
|
||||
bodyJson := message[5:]
|
||||
if gjson.Get(bodyJson, config.AnswerStreamValueFrom.ResponseBody).Exists() {
|
||||
tempContentI := ctx.GetContext(AnswerContentContextKey)
|
||||
if tempContentI == nil {
|
||||
content = TrimQuote(gjson.Get(bodyJson, config.AnswerStreamValueFrom.ResponseBody).Raw)
|
||||
ctx.SetContext(AnswerContentContextKey, content)
|
||||
} else {
|
||||
append := TrimQuote(gjson.Get(bodyJson, config.AnswerStreamValueFrom.ResponseBody).Raw)
|
||||
content = tempContentI.(string) + append
|
||||
ctx.SetContext(AnswerContentContextKey, content)
|
||||
}
|
||||
} else if gjson.Get(bodyJson, "choices.0.delta.content.tool_calls").Exists() {
|
||||
// TODO: compatible with other providers
|
||||
ctx.SetContext(ToolCallsContextKey, struct{}{})
|
||||
}
|
||||
log.Debugf("unknown message:%s", bodyJson)
|
||||
}
|
||||
log.Debugf("unknown message:%s", bodyJson)
|
||||
return ""
|
||||
return content
|
||||
}
|
||||
|
||||
func onHttpResponseHeaders(ctx wrapper.HttpContext, config PluginConfig, log wrapper.Log) types.Action {
|
||||
|
||||
@@ -34,7 +34,7 @@ func parseConfig(json gjson.Result, config *AIPromptTemplateConfig, log wrapper.
|
||||
|
||||
func onHttpRequestHeaders(ctx wrapper.HttpContext, config AIPromptTemplateConfig, log wrapper.Log) types.Action {
|
||||
templateEnable, _ := proxywasm.GetHttpRequestHeader("template-enable")
|
||||
if templateEnable != "true" {
|
||||
if templateEnable == "false" {
|
||||
ctx.DontReadRequestBody()
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
@@ -31,16 +31,19 @@ description: AI 代理插件配置参考
|
||||
|
||||
`provider`的配置字段说明如下:
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
|------------------| --------------- | -------- | ------ |-----------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `type` | string | 必填 | - | AI 服务提供商名称 |
|
||||
| `apiTokens` | array of string | 非必填 | - | 用于在访问 AI 服务时进行认证的令牌。如果配置了多个 token,插件会在请求时随机进行选择。部分服务提供商只支持配置一个 token。 |
|
||||
| `timeout` | number | 非必填 | - | 访问 AI 服务的超时时间。单位为毫秒。默认值为 120000,即 2 分钟 |
|
||||
| `modelMapping` | map of string | 非必填 | - | AI 模型映射表,用于将请求中的模型名称映射为服务提供商支持模型名称。<br/>1. 支持前缀匹配。例如用 "gpt-3-*" 匹配所有名称以“gpt-3-”开头的模型;<br/>2. 支持使用 "*" 为键来配置通用兜底映射关系;<br/>3. 如果映射的目标名称为空字符串 "",则表示保留原模型名称。 |
|
||||
| `protocol` | string | 非必填 | - | 插件对外提供的 API 接口契约。目前支持以下取值:openai(默认值,使用 OpenAI 的接口契约)、original(使用目标服务提供商的原始接口契约) |
|
||||
| `context` | object | 非必填 | - | 配置 AI 对话上下文信息 |
|
||||
| `customSettings` | array of customSetting | 非必填 | - | 为AI请求指定覆盖或者填充参数 |
|
||||
| `failover` | object | 非必填 | - | 配置 apiToken 的 failover 策略,当 apiToken 不可用时,将其移出 apiToken 列表,待健康检测通过后重新添加回 apiToken 列表 |
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
|------------------| --------------- | -------- | ------ |-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `type` | string | 必填 | - | AI 服务提供商名称 |
|
||||
| `apiTokens` | array of string | 非必填 | - | 用于在访问 AI 服务时进行认证的令牌。如果配置了多个 token,插件会在请求时随机进行选择。部分服务提供商只支持配置一个 token。 |
|
||||
| `timeout` | number | 非必填 | - | 访问 AI 服务的超时时间。单位为毫秒。默认值为 120000,即 2 分钟 |
|
||||
| `modelMapping` | map of string | 非必填 | - | AI 模型映射表,用于将请求中的模型名称映射为服务提供商支持模型名称。<br/>1. 支持前缀匹配。例如用 "gpt-3-*" 匹配所有名称以“gpt-3-”开头的模型;<br/>2. 支持使用 "*" 为键来配置通用兜底映射关系;<br/>3. 如果映射的目标名称为空字符串 "",则表示保留原模型名称。 |
|
||||
| `protocol` | string | 非必填 | - | 插件对外提供的 API 接口契约。目前支持以下取值:openai(默认值,使用 OpenAI 的接口契约)、original(使用目标服务提供商的原始接口契约) |
|
||||
| `context` | object | 非必填 | - | 配置 AI 对话上下文信息 |
|
||||
| `customSettings` | array of customSetting | 非必填 | - | 为AI请求指定覆盖或者填充参数 |
|
||||
| `failover` | object | 非必填 | - | 配置 apiToken 的 failover 策略,当 apiToken 不可用时,将其移出 apiToken 列表,待健康检测通过后重新添加回 apiToken 列表 |
|
||||
| `retryOnFailure` | object | 非必填 | - | 当请求失败时立即进行重试 |
|
||||
| `reasoningContentMode` | string | 非必填 | - | 如何处理大模型服务返回的推理内容。目前支持以下取值:passthrough(正常输出推理内容)、ignore(不输出推理内容)、concat(将推理内容拼接在常规输出内容之前)。默认为 passthrough。仅支持通义千问服务。 |
|
||||
| `capabilities` | map of string | 非必填 | - | 部分provider的部分ai能力原生兼容openai/v1格式,不需要重写,可以直接转发,通过此配置项指定来开启转发, key表示的是采用的厂商协议能力,values表示的真实的厂商该能力的api path, 厂商协议能力当前支持: openai/v1/chatcompletions, openai/v1/embeddings, openai/v1/imagegeneration, openai/v1/audiospeech, cohere/v1/rerank |
|
||||
|
||||
`context`的配置字段说明如下:
|
||||
|
||||
@@ -78,14 +81,22 @@ custom-setting会遵循如下表格,根据`name`和协议来替换对应的字
|
||||
|
||||
`failover` 的配置字段说明如下:
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
|------------------|--------|------|-------|-----------------------------|
|
||||
| enabled | bool | 非必填 | false | 是否启用 apiToken 的 failover 机制 |
|
||||
| failureThreshold | int | 非必填 | 3 | 触发 failover 连续请求失败的阈值(次数) |
|
||||
| successThreshold | int | 非必填 | 1 | 健康检测的成功阈值(次数) |
|
||||
| healthCheckInterval | int | 非必填 | 5000 | 健康检测的间隔时间,单位毫秒 |
|
||||
| healthCheckTimeout | int | 非必填 | 5000 | 健康检测的超时时间,单位毫秒 |
|
||||
| healthCheckModel | string | 必填 | | 健康检测使用的模型 |
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
|------------------|--------|-----------------|-------|-----------------------------|
|
||||
| enabled | bool | 非必填 | false | 是否启用 apiToken 的 failover 机制 |
|
||||
| failureThreshold | int | 非必填 | 3 | 触发 failover 连续请求失败的阈值(次数) |
|
||||
| successThreshold | int | 非必填 | 1 | 健康检测的成功阈值(次数) |
|
||||
| healthCheckInterval | int | 非必填 | 5000 | 健康检测的间隔时间,单位毫秒 |
|
||||
| healthCheckTimeout | int | 非必填 | 5000 | 健康检测的超时时间,单位毫秒 |
|
||||
| healthCheckModel | string | 启用 failover 时必填 | | 健康检测使用的模型 |
|
||||
|
||||
`retryOnFailure` 的配置字段说明如下:
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
|------------------|--------|-----------------|-------|-------------|
|
||||
| enabled | bool | 非必填 | false | 是否启用失败请求重试 |
|
||||
| maxRetries | int | 非必填 | 1 | 最大重试次数 |
|
||||
| retryTimeout | int | 非必填 | 30000 | 重试超时时间,单位毫秒 |
|
||||
|
||||
### 提供商特有配置
|
||||
|
||||
@@ -121,10 +132,11 @@ Azure OpenAI 所对应的 `type` 为 `azure`。它特有的配置字段如下:
|
||||
|
||||
通义千问所对应的 `type` 为 `qwen`。它特有的配置字段如下:
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
|--------------------|-----------------|------|-----|------------------------------------------------------------------|
|
||||
| `qwenEnableSearch` | boolean | 非必填 | - | 是否启用通义千问内置的互联网搜索功能。 |
|
||||
| `qwenFileIds` | array of string | 非必填 | - | 通过文件接口上传至Dashscope的文件 ID,其内容将被用做 AI 对话的上下文。不可与 `context` 字段同时配置。 |
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
| ---------------------- | --------------- | -------- | ------ | ------------------------------------------------------------ |
|
||||
| `qwenEnableSearch` | boolean | 非必填 | - | 是否启用通义千问内置的互联网搜索功能。 |
|
||||
| `qwenFileIds` | array of string | 非必填 | - | 通过文件接口上传至Dashscope的文件 ID,其内容将被用做 AI 对话的上下文。不可与 `context` 字段同时配置。 |
|
||||
| `qwenEnableCompatible` | boolean | 非必填 | false | 开启通义千问兼容模式。启用通义千问兼容模式后,将调用千问的兼容模式接口,同时对请求/响应不做修改。 |
|
||||
|
||||
#### 百川智能 (Baichuan AI)
|
||||
|
||||
@@ -166,9 +178,10 @@ Mistral 所对应的 `type` 为 `mistral`。它并无特有的配置字段。
|
||||
|
||||
MiniMax所对应的 `type` 为 `minimax`。它特有的配置字段如下:
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
| ---------------- | -------- | ------------------------------------------------------------ | ------ | ------------------------------------------------------------ |
|
||||
| `minimaxGroupId` | string | 当使用`abab6.5-chat`, `abab6.5s-chat`, `abab5.5s-chat`, `abab5.5-chat`四种模型时必填 | - | 当使用`abab6.5-chat`, `abab6.5s-chat`, `abab5.5s-chat`, `abab5.5-chat`四种模型时会使用ChatCompletion Pro,需要设置groupID |
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
| ---------------- | -------- | ------------------------------ | ------ |----------------------------------------------------------------|
|
||||
| `minimaxApiType` | string | v2 和 pro 中选填一项 | v2 | v2 代表 ChatCompletion v2 API,pro 代表 ChatCompletion Pro API |
|
||||
| `minimaxGroupId` | string | `minimaxApiType` 为 pro 时必填 | - | `minimaxApiType` 为 pro 时使用 ChatCompletion Pro API,需要设置 groupID |
|
||||
|
||||
#### Anthropic Claude
|
||||
|
||||
@@ -234,6 +247,20 @@ DeepL 所对应的 `type` 为 `deepl`。它特有的配置字段如下:
|
||||
|
||||
Cohere 所对应的 `type` 为 `cohere`。它并无特有的配置字段。
|
||||
|
||||
#### Together-AI
|
||||
Together-AI 所对应的 `type` 为 `together-ai`。它并无特有的配置字段。
|
||||
|
||||
#### Dify
|
||||
Dify 所对应的 `type` 为 `dify`。它特有的配置字段如下:
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
| -- | -------- |------| ------ | ---------------------------- |
|
||||
| `difyApiUrl` | string | 非必填 | - | dify私有化部署的url |
|
||||
| `botType` | string | 非必填 | - | dify的应用类型,Chat/Completion/Agent/Workflow |
|
||||
| `inputVariable` | string | 非必填 | - | dify中应用类型为workflow时需要设置输入变量,当botType为workflow时一起使用 |
|
||||
| `outputVariable` | string | 非必填 | - | dify中应用类型为workflow时需要设置输出变量,当botType为workflow时一起使用 |
|
||||
|
||||
|
||||
## 用法示例
|
||||
|
||||
### 使用 OpenAI 协议代理 Azure OpenAI 服务
|
||||
@@ -408,25 +435,25 @@ URL: http://your-domain/v1/chat/completions
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "gpt-4o",
|
||||
"messages": [
|
||||
"model": "gpt-4o",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "image_url",
|
||||
"image_url": {
|
||||
"url": "https://dashscope.oss-cn-beijing.aliyuncs.com/images/dog_and_girl.jpeg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"text": "这个图片是哪里?"
|
||||
}
|
||||
]
|
||||
"type": "image_url",
|
||||
"image_url": {
|
||||
"url": "https://dashscope.oss-cn-beijing.aliyuncs.com/images/dog_and_girl.jpeg"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"text": "这个图片是哪里?"
|
||||
}
|
||||
],
|
||||
"temperature": 0.3
|
||||
]
|
||||
}
|
||||
],
|
||||
"temperature": 0.3
|
||||
}
|
||||
```
|
||||
|
||||
@@ -434,28 +461,28 @@ URL: http://your-domain/v1/chat/completions
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "17c5955d-af9c-9f28-bbde-293a9c9a3515",
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{
|
||||
"text": "这张照片显示的是一位女士和一只狗在海滩上。由于我无法获取具体的地理位置信息,所以不能确定这是哪个地方的海滩。但是从视觉内容来看,它可能是一个位于沿海地区的沙滩海岸线,并且有海浪拍打着岸边。这样的场景在全球许多美丽的海滨地区都可以找到。如果您需要更精确的信息,请提供更多的背景或细节描述。"
|
||||
}
|
||||
]
|
||||
},
|
||||
"finish_reason": "stop"
|
||||
}
|
||||
],
|
||||
"created": 1723949230,
|
||||
"model": "qwen-vl-plus",
|
||||
"object": "chat.completion",
|
||||
"usage": {
|
||||
"prompt_tokens": 1279,
|
||||
"completion_tokens": 78
|
||||
"id": "17c5955d-af9c-9f28-bbde-293a9c9a3515",
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{
|
||||
"text": "这张照片显示的是一位女士和一只狗在海滩上。由于我无法获取具体的地理位置信息,所以不能确定这是哪个地方的海滩。但是从视觉内容来看,它可能是一个位于沿海地区的沙滩海岸线,并且有海浪拍打着岸边。这样的场景在全球许多美丽的海滨地区都可以找到。如果您需要更精确的信息,请提供更多的背景或细节描述。"
|
||||
}
|
||||
]
|
||||
},
|
||||
"finish_reason": "stop"
|
||||
}
|
||||
],
|
||||
"created": 1723949230,
|
||||
"model": "qwen-vl-plus",
|
||||
"object": "chat.completion",
|
||||
"usage": {
|
||||
"prompt_tokens": 1279,
|
||||
"completion_tokens": 78
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -578,8 +605,8 @@ provider:
|
||||
modelMapping:
|
||||
"*": "qwen-long" # 通义千问的文件上下文只能在 qwen-long 模型下使用
|
||||
qwenFileIds:
|
||||
- "file-fe-xxx"
|
||||
- "file-fe-yyy"
|
||||
- "file-fe-xxx"
|
||||
- "file-fe-yyy"
|
||||
```
|
||||
|
||||
**请求示例**
|
||||
@@ -637,7 +664,7 @@ provider:
|
||||
```json
|
||||
{
|
||||
"input": {
|
||||
"prompt": "介绍一下Dubbo"
|
||||
"prompt": "介绍一下Dubbo"
|
||||
},
|
||||
"parameters": {},
|
||||
"debug": {}
|
||||
@@ -648,21 +675,21 @@ provider:
|
||||
|
||||
```json
|
||||
{
|
||||
"output": {
|
||||
"finish_reason": "stop",
|
||||
"session_id": "677e7e8fbb874e1b84792b65042e1599",
|
||||
"text": "Apache Dubbo 是一个..."
|
||||
},
|
||||
"usage": {
|
||||
"models": [
|
||||
{
|
||||
"output_tokens": 449,
|
||||
"model_id": "qwen-max",
|
||||
"input_tokens": 282
|
||||
}
|
||||
]
|
||||
},
|
||||
"request_id": "b59e45e3-5af4-91df-b7c6-9d746fd3297c"
|
||||
"output": {
|
||||
"finish_reason": "stop",
|
||||
"session_id": "677e7e8fbb874e1b84792b65042e1599",
|
||||
"text": "Apache Dubbo 是一个..."
|
||||
},
|
||||
"usage": {
|
||||
"models": [
|
||||
{
|
||||
"output_tokens": 449,
|
||||
"model_id": "qwen-max",
|
||||
"input_tokens": 282
|
||||
}
|
||||
]
|
||||
},
|
||||
"request_id": "b59e45e3-5af4-91df-b7c6-9d746fd3297c"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -905,25 +932,25 @@ curl --location 'http://<your higress domain>/v1/chat/completions' \
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "fd140c3e-0b69-4b19-849b-d354d32a6162",
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"delta": {
|
||||
"role": "assistant",
|
||||
"content": "你好!我是一名专业的开发人员。"
|
||||
},
|
||||
"finish_reason": "stop"
|
||||
}
|
||||
],
|
||||
"created": 1717493117,
|
||||
"model": "hunyuan-lite",
|
||||
"object": "chat.completion",
|
||||
"usage": {
|
||||
"prompt_tokens": 15,
|
||||
"completion_tokens": 9,
|
||||
"total_tokens": 24
|
||||
"id": "fd140c3e-0b69-4b19-849b-d354d32a6162",
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"delta": {
|
||||
"role": "assistant",
|
||||
"content": "你好!我是一名专业的开发人员。"
|
||||
},
|
||||
"finish_reason": "stop"
|
||||
}
|
||||
],
|
||||
"created": 1717493117,
|
||||
"model": "hunyuan-lite",
|
||||
"object": "chat.completion",
|
||||
"usage": {
|
||||
"prompt_tokens": 15,
|
||||
"completion_tokens": 9,
|
||||
"total_tokens": 24
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -945,14 +972,14 @@ provider:
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "gpt-4-turbo",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "你好,你是谁?"
|
||||
}
|
||||
],
|
||||
"stream": false
|
||||
"model": "gpt-4-turbo",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "你好,你是谁?"
|
||||
}
|
||||
],
|
||||
"stream": false
|
||||
}
|
||||
```
|
||||
|
||||
@@ -960,25 +987,25 @@ provider:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "as-e90yfg1pk1",
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "你好,我是文心一言,英文名是ERNIE Bot。我能够与人对话互动,回答问题,协助创作,高效便捷地帮助人们获取信息、知识和灵感。"
|
||||
},
|
||||
"finish_reason": "stop"
|
||||
}
|
||||
],
|
||||
"created": 1717251488,
|
||||
"model": "ERNIE-4.0",
|
||||
"object": "chat.completion",
|
||||
"usage": {
|
||||
"prompt_tokens": 4,
|
||||
"completion_tokens": 33,
|
||||
"total_tokens": 37
|
||||
"id": "as-e90yfg1pk1",
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "你好,我是文心一言,英文名是ERNIE Bot。我能够与人对话互动,回答问题,协助创作,高效便捷地帮助人们获取信息、知识和灵感。"
|
||||
},
|
||||
"finish_reason": "stop"
|
||||
}
|
||||
],
|
||||
"created": 1717251488,
|
||||
"model": "ERNIE-4.0",
|
||||
"object": "chat.completion",
|
||||
"usage": {
|
||||
"prompt_tokens": 4,
|
||||
"completion_tokens": 33,
|
||||
"total_tokens": 37
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -992,24 +1019,23 @@ provider:
|
||||
apiTokens:
|
||||
- "YOUR_MINIMAX_API_TOKEN"
|
||||
modelMapping:
|
||||
"gpt-3": "abab6.5g-chat"
|
||||
"gpt-4": "abab6.5-chat"
|
||||
"*": "abab6.5g-chat"
|
||||
minimaxGroupId: "YOUR_MINIMAX_GROUP_ID"
|
||||
"gpt-3": "abab6.5s-chat"
|
||||
"gpt-4": "abab6.5g-chat"
|
||||
"*": "abab6.5t-chat"
|
||||
```
|
||||
|
||||
**请求示例**
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "gpt-4-turbo",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "你好,你是谁?"
|
||||
}
|
||||
],
|
||||
"stream": false
|
||||
"model": "gpt-3",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "你好,你是谁?"
|
||||
}
|
||||
],
|
||||
"stream": false
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1017,31 +1043,37 @@ provider:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "02b2251f8c6c09d68c1743f07c72afd7",
|
||||
"choices": [
|
||||
{
|
||||
"finish_reason": "stop",
|
||||
"index": 0,
|
||||
"message": {
|
||||
"content": "你好!我是MM智能助理,一款由MiniMax自研的大型语言模型。我可以帮助你解答问题,提供信息,进行对话等。有什么可以帮助你的吗?",
|
||||
"role": "assistant"
|
||||
}
|
||||
}
|
||||
],
|
||||
"created": 1717760544,
|
||||
"model": "abab6.5s-chat",
|
||||
"object": "chat.completion",
|
||||
"usage": {
|
||||
"total_tokens": 106
|
||||
},
|
||||
"input_sensitive": false,
|
||||
"output_sensitive": false,
|
||||
"input_sensitive_type": 0,
|
||||
"output_sensitive_type": 0,
|
||||
"base_resp": {
|
||||
"status_code": 0,
|
||||
"status_msg": ""
|
||||
"id": "03ac4fcfe1c6cc9c6a60f9d12046e2b4",
|
||||
"choices": [
|
||||
{
|
||||
"finish_reason": "stop",
|
||||
"index": 0,
|
||||
"message": {
|
||||
"content": "你好,我是一个由MiniMax公司研发的大型语言模型,名为MM智能助理。我可以帮助回答问题、提供信息、进行对话和执行多种语言处理任务。如果你有任何问题或需要帮助,请随时告诉我!",
|
||||
"role": "assistant",
|
||||
"name": "MM智能助理",
|
||||
"audio_content": ""
|
||||
}
|
||||
}
|
||||
],
|
||||
"created": 1734155471,
|
||||
"model": "abab6.5s-chat",
|
||||
"object": "chat.completion",
|
||||
"usage": {
|
||||
"total_tokens": 116,
|
||||
"total_characters": 0,
|
||||
"prompt_tokens": 70,
|
||||
"completion_tokens": 46
|
||||
},
|
||||
"input_sensitive": false,
|
||||
"output_sensitive": false,
|
||||
"input_sensitive_type": 0,
|
||||
"output_sensitive_type": 0,
|
||||
"output_sensitive_int": 0,
|
||||
"base_resp": {
|
||||
"status_code": 0,
|
||||
"status_msg": ""
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1330,18 +1362,18 @@ provider:
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "gpt-4o",
|
||||
"messages": [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "你是一名专业的开发人员!"
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "你好,你是谁?"
|
||||
}
|
||||
],
|
||||
"stream": false
|
||||
"model": "gpt-4o",
|
||||
"messages": [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "你是一名专业的开发人员!"
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "你好,你是谁?"
|
||||
}
|
||||
],
|
||||
"stream": false
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1349,24 +1381,24 @@ provider:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "cha000c23c6@dx190ef0b4b96b8f2532",
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "你好!我是一名专业的开发人员,擅长编程和解决技术问题。有什么我可以帮助你的吗?"
|
||||
}
|
||||
}
|
||||
],
|
||||
"created": 1721997415,
|
||||
"model": "generalv3.5",
|
||||
"object": "chat.completion",
|
||||
"usage": {
|
||||
"prompt_tokens": 10,
|
||||
"completion_tokens": 19,
|
||||
"total_tokens": 29
|
||||
"id": "cha000c23c6@dx190ef0b4b96b8f2532",
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "你好!我是一名专业的开发人员,擅长编程和解决技术问题。有什么我可以帮助你的吗?"
|
||||
}
|
||||
}
|
||||
],
|
||||
"created": 1721997415,
|
||||
"model": "generalv3.5",
|
||||
"object": "chat.completion",
|
||||
"usage": {
|
||||
"prompt_tokens": 10,
|
||||
"completion_tokens": 19,
|
||||
"total_tokens": 29
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1392,14 +1424,14 @@ provider:
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "gpt-3.5",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Who are you?"
|
||||
}
|
||||
],
|
||||
"stream": false
|
||||
"model": "gpt-3.5",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Who are you?"
|
||||
}
|
||||
],
|
||||
"stream": false
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1407,25 +1439,25 @@ provider:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "chatcmpl-b010867c-0d3f-40ba-95fd-4e8030551aeb",
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "I am a large multi-modal model, trained by Google. I am designed to provide information and answer questions to the best of my abilities."
|
||||
},
|
||||
"finish_reason": "stop"
|
||||
}
|
||||
],
|
||||
"created": 1722756984,
|
||||
"model": "gemini-pro",
|
||||
"object": "chat.completion",
|
||||
"usage": {
|
||||
"prompt_tokens": 5,
|
||||
"completion_tokens": 29,
|
||||
"total_tokens": 34
|
||||
"id": "chatcmpl-b010867c-0d3f-40ba-95fd-4e8030551aeb",
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "I am a large multi-modal model, trained by Google. I am designed to provide information and answer questions to the best of my abilities."
|
||||
},
|
||||
"finish_reason": "stop"
|
||||
}
|
||||
],
|
||||
"created": 1722756984,
|
||||
"model": "gemini-pro",
|
||||
"object": "chat.completion",
|
||||
"usage": {
|
||||
"prompt_tokens": 5,
|
||||
"completion_tokens": 29,
|
||||
"total_tokens": 34
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1482,6 +1514,112 @@ provider:
|
||||
}
|
||||
```
|
||||
|
||||
### 使用 OpenAI 协议代理 Together-AI 服务
|
||||
|
||||
**配置信息**
|
||||
```yaml
|
||||
provider:
|
||||
type: together-ai
|
||||
apiTokens:
|
||||
- "YOUR_TOGETHER_AI_API_TOKEN"
|
||||
modelMapping:
|
||||
"*": "Qwen/Qwen2.5-72B-Instruct-Turbo"
|
||||
```
|
||||
|
||||
**请求示例**
|
||||
```json
|
||||
{
|
||||
"model": "Qwen/Qwen2.5-72B-Instruct-Turbo",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Who are you?"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**响应示例**
|
||||
```json
|
||||
{
|
||||
"id": "8f5809d54b73efac",
|
||||
"object": "chat.completion",
|
||||
"created": 1734785851,
|
||||
"model": "Qwen/Qwen2.5-72B-Instruct-Turbo",
|
||||
"prompt": [],
|
||||
"choices": [
|
||||
{
|
||||
"finish_reason": "eos",
|
||||
"seed": 12830868308626506000,
|
||||
"logprobs": null,
|
||||
"index": 0,
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "I am Qwen, a large language model created by Alibaba Cloud. I am designed to assist users in generating various types of text, such as articles, stories, poems, and more, as well as answering questions and providing information on a wide range of topics. How can I assist you today?",
|
||||
"tool_calls": []
|
||||
}
|
||||
}
|
||||
],
|
||||
"usage": {
|
||||
"prompt_tokens": 33,
|
||||
"completion_tokens": 61,
|
||||
"total_tokens": 94
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 使用 OpenAI 协议代理 Dify 服务
|
||||
|
||||
**配置信息**
|
||||
```yaml
|
||||
provider:
|
||||
type: dify
|
||||
apiTokens:
|
||||
- "YOUR_DIFY_API_TOKEN"
|
||||
modelMapping:
|
||||
"*": "dify"
|
||||
```
|
||||
|
||||
**请求示例**
|
||||
```json
|
||||
{
|
||||
"model": "gpt-4-turbo",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "你好,你是谁?"
|
||||
}
|
||||
],
|
||||
"stream": false
|
||||
}
|
||||
```
|
||||
|
||||
**响应示例**
|
||||
```json
|
||||
{
|
||||
"id": "e33fc636-f9e8-4fae-8d5e-fbd0acb09401",
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "你好!我是ChatGPT,由OpenAI开发的人工智能语言模型。我可以帮助回答问题、提供建议或进行各种对话。如果你有任何需要,随时告诉我哦!"
|
||||
},
|
||||
"finish_reason": "stop"
|
||||
}
|
||||
],
|
||||
"created": 1736657752,
|
||||
"model": "dify",
|
||||
"object": "chat.completion",
|
||||
"usage": {
|
||||
"prompt_tokens": 16,
|
||||
"completion_tokens": 243,
|
||||
"total_tokens": 259
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## 完整配置示例
|
||||
|
||||
### Kubernetes 示例
|
||||
@@ -1496,13 +1634,13 @@ metadata:
|
||||
namespace: higress-system
|
||||
spec:
|
||||
matchRules:
|
||||
- config:
|
||||
provider:
|
||||
type: groq
|
||||
apiTokens:
|
||||
- "YOUR_API_TOKEN"
|
||||
ingress:
|
||||
- groq
|
||||
- config:
|
||||
provider:
|
||||
type: groq
|
||||
apiTokens:
|
||||
- "YOUR_API_TOKEN"
|
||||
ingress:
|
||||
- groq
|
||||
url: oci://higress-registry.cn-hangzhou.cr.aliyuncs.com/plugins/ai-proxy:1.0.0
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
@@ -1520,16 +1658,16 @@ metadata:
|
||||
spec:
|
||||
ingressClassName: higress
|
||||
rules:
|
||||
- host: <YOUR-DOMAIN>
|
||||
http:
|
||||
paths:
|
||||
- backend:
|
||||
resource:
|
||||
apiGroup: networking.higress.io
|
||||
kind: McpBridge
|
||||
name: default
|
||||
path: /
|
||||
pathType: Prefix
|
||||
- host: <YOUR-DOMAIN>
|
||||
http:
|
||||
paths:
|
||||
- backend:
|
||||
resource:
|
||||
apiGroup: networking.higress.io
|
||||
kind: McpBridge
|
||||
name: default
|
||||
path: /
|
||||
pathType: Prefix
|
||||
---
|
||||
apiVersion: networking.higress.io/v1
|
||||
kind: McpBridge
|
||||
@@ -1538,10 +1676,10 @@ metadata:
|
||||
namespace: higress-system
|
||||
spec:
|
||||
registries:
|
||||
- domain: api.groq.com
|
||||
name: groq
|
||||
port: 443
|
||||
type: dns
|
||||
- domain: api.groq.com
|
||||
name: groq
|
||||
port: 443
|
||||
type: dns
|
||||
```
|
||||
|
||||
访问示例:
|
||||
|
||||
@@ -106,6 +106,7 @@ For Qwen (Tongyi Qwen), the corresponding `type` is `qwen`. Its unique configura
|
||||
|--------------------|-----------------|----------------------|---------------|------------------------------------------------------------------------------------------------------------------------|
|
||||
| `qwenEnableSearch` | boolean | Optional | - | Whether to enable the built-in Internet search function provided by Qwen. |
|
||||
| `qwenFileIds` | array of string | Optional | - | The file IDs uploaded via the Dashscope file interface, whose content will be used as context for AI conversations. Cannot be configured with the `context` field. |
|
||||
| `qwenEnableCompatible` | boolean | Optional | false | Enable Qwen compatibility mode. When Qwen compatibility mode is enabled, the compatible mode interface of Qwen will be called, and the request/response will not be modified. |
|
||||
|
||||
#### Baichuan AI
|
||||
|
||||
@@ -1356,6 +1357,60 @@ Here, `model` denotes the service tier of DeepL and can only be either `Free` or
|
||||
}
|
||||
```
|
||||
|
||||
### Utilizing OpenAI Protocol Proxy for Together-AI Services
|
||||
|
||||
**Configuration Information**
|
||||
```yaml
|
||||
provider:
|
||||
type: together-ai
|
||||
apiTokens:
|
||||
- "YOUR_TOGETHER_AI_API_TOKEN"
|
||||
modelMapping:
|
||||
"*": "meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo"
|
||||
```
|
||||
|
||||
**Request Example**
|
||||
```json
|
||||
{
|
||||
"model": "Qwen/Qwen2.5-72B-Instruct-Turbo",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Who are you?"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Response Example**
|
||||
```json
|
||||
{
|
||||
"id": "8f5809d54b73efac",
|
||||
"object": "chat.completion",
|
||||
"created": 1734785851,
|
||||
"model": "Qwen/Qwen2.5-72B-Instruct-Turbo",
|
||||
"prompt": [],
|
||||
"choices": [
|
||||
{
|
||||
"finish_reason": "eos",
|
||||
"seed": 12830868308626506000,
|
||||
"logprobs": null,
|
||||
"index": 0,
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "I am Qwen, a large language model created by Alibaba Cloud. I am designed to assist users in generating various types of text, such as articles, stories, poems, and more, as well as answering questions and providing information on a wide range of topics. How can I assist you today?",
|
||||
"tool_calls": []
|
||||
}
|
||||
}
|
||||
],
|
||||
"usage": {
|
||||
"prompt_tokens": 33,
|
||||
"completion_tokens": 61,
|
||||
"total_tokens": 94
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Full Configuration Example
|
||||
|
||||
### Kubernetes Example
|
||||
|
||||
1
plugins/wasm-go/extensions/ai-proxy/VERSION
Normal file
1
plugins/wasm-go/extensions/ai-proxy/VERSION
Normal file
@@ -0,0 +1 @@
|
||||
1.0.0-alpha
|
||||
@@ -80,13 +80,16 @@ func (c *PluginConfig) Complete(log wrapper.Log) error {
|
||||
c.activeProvider = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
var err error
|
||||
|
||||
c.activeProvider, err = provider.CreateProvider(*c.activeProviderConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
providerConfig := c.GetProviderConfig()
|
||||
err = providerConfig.SetApiTokensFailover(log, c.activeProvider)
|
||||
|
||||
return err
|
||||
return providerConfig.SetApiTokensFailover(log, c.activeProvider)
|
||||
}
|
||||
|
||||
func (c *PluginConfig) GetProvider() provider.Provider {
|
||||
|
||||
@@ -8,7 +8,7 @@ replace github.com/alibaba/higress/plugins/wasm-go => ../..
|
||||
|
||||
require (
|
||||
github.com/alibaba/higress/plugins/wasm-go v0.0.0
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240711023527-ba358c48772f
|
||||
github.com/higress-group/proxy-wasm-go-sdk v1.0.0
|
||||
github.com/stretchr/testify v1.8.4
|
||||
github.com/tidwall/gjson v1.17.3
|
||||
)
|
||||
|
||||
@@ -4,8 +4,8 @@ github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 h1:IHDghbGQ2DTIXHBHxWfqCYQW1fKjyJ/I7W1pMyUDeEA=
|
||||
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520/go.mod h1:Nz8ORLaFiLWotg6GeKlJMhv8cci8mM43uEnLA5t8iew=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240711023527-ba358c48772f h1:ZIiIBRvIw62gA5MJhuwp1+2wWbqL9IGElQ499rUsYYg=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240711023527-ba358c48772f/go.mod h1:hNFjhrLUIq+kJ9bOcs8QtiplSQ61GZXtd2xHKx4BYRo=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v1.0.0 h1:BZRNf4R7jr9hwRivg/E29nkVaKEak5MWjBDhWjuHijU=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v1.0.0/go.mod h1:iiSyFbo+rAtbtGt/bsefv8GU57h9CCLYGJA74/tF5/0=
|
||||
github.com/magefile/mage v1.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo=
|
||||
github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
|
||||
@@ -15,14 +15,13 @@ import (
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
"github.com/tidwall/gjson"
|
||||
"github.com/tidwall/sjson"
|
||||
)
|
||||
|
||||
const (
|
||||
pluginName = "ai-proxy"
|
||||
|
||||
ctxKeyApiName = "apiName"
|
||||
|
||||
defaultMaxBodyBytes uint32 = 10 * 1024 * 1024
|
||||
defaultMaxBodyBytes uint32 = 100 * 1024 * 1024
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -42,9 +41,11 @@ func parseGlobalConfig(json gjson.Result, pluginConfig *config.PluginConfig, log
|
||||
|
||||
pluginConfig.FromJson(json)
|
||||
if err := pluginConfig.Validate(); err != nil {
|
||||
log.Errorf("global rule config is invalid: %v", err)
|
||||
return err
|
||||
}
|
||||
if err := pluginConfig.Complete(log); err != nil {
|
||||
log.Errorf("failed to apply global rule config: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -58,9 +59,11 @@ func parseOverrideRuleConfig(json gjson.Result, global config.PluginConfig, plug
|
||||
|
||||
pluginConfig.FromJson(json)
|
||||
if err := pluginConfig.Validate(); err != nil {
|
||||
log.Errorf("overriden rule config is invalid: %v", err)
|
||||
return err
|
||||
}
|
||||
if err := pluginConfig.Complete(log); err != nil {
|
||||
log.Errorf("failed to apply overriden rule config: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -80,7 +83,7 @@ func onHttpRequestHeader(ctx wrapper.HttpContext, pluginConfig config.PluginConf
|
||||
|
||||
rawPath := ctx.Path()
|
||||
path, _ := url.Parse(rawPath)
|
||||
apiName := getOpenAiApiName(path.Path)
|
||||
apiName := getApiName(path.Path)
|
||||
providerConfig := pluginConfig.GetProviderConfig()
|
||||
if providerConfig.IsOriginal() {
|
||||
if handler, ok := activeProvider.(provider.ApiNameHandler); ok {
|
||||
@@ -89,32 +92,39 @@ func onHttpRequestHeader(ctx wrapper.HttpContext, pluginConfig config.PluginConf
|
||||
}
|
||||
|
||||
if apiName == "" {
|
||||
log.Debugf("[onHttpRequestHeader] unsupported path: %s", path.Path)
|
||||
// _ = util.SendResponse(404, "ai-proxy.unknown_api", util.MimeTypeTextPlain, "API not found: "+path.Path)
|
||||
log.Debugf("[onHttpRequestHeader] no send response")
|
||||
return types.ActionContinue
|
||||
ctx.DontReadRequestBody()
|
||||
ctx.DontReadResponseBody()
|
||||
log.Warnf("[onHttpRequestHeader] unsupported path: %s, will not process http path and body", path.Path)
|
||||
}
|
||||
ctx.SetContext(ctxKeyApiName, apiName)
|
||||
|
||||
ctx.SetContext(provider.CtxKeyApiName, apiName)
|
||||
// Disable the route re-calculation since the plugin may modify some headers related to the chosen route.
|
||||
ctx.DisableReroute()
|
||||
|
||||
// Always remove the Accept-Encoding header to prevent the LLM from sending compressed responses,
|
||||
// allowing plugins to inspect or modify the response correctly
|
||||
_ = proxywasm.RemoveHttpRequestHeader("Accept-Encoding")
|
||||
|
||||
if handler, ok := activeProvider.(provider.RequestHeadersHandler); ok {
|
||||
// Disable the route re-calculation since the plugin may modify some headers related to the chosen route.
|
||||
ctx.DisableReroute()
|
||||
// Set the apiToken for the current request.
|
||||
providerConfig.SetApiTokenInUse(ctx, log)
|
||||
// Set available apiTokens of current request in the context, will be used in the retryOnFailure
|
||||
providerConfig.SetAvailableApiTokens(ctx, log)
|
||||
|
||||
hasRequestBody := wrapper.HasRequestBody()
|
||||
action, err := handler.OnRequestHeaders(ctx, apiName, log)
|
||||
if err == nil {
|
||||
if hasRequestBody {
|
||||
ctx.SetRequestBodyBufferLimit(defaultMaxBodyBytes)
|
||||
// Always return types.HeaderStopIteration to support fallback routing,
|
||||
// as long as onHttpRequestBody can be called.
|
||||
return types.HeaderStopIteration
|
||||
}
|
||||
return action
|
||||
err := handler.OnRequestHeaders(ctx, apiName, log)
|
||||
if err != nil {
|
||||
_ = util.ErrorHandler("ai-proxy.proc_req_headers_failed", fmt.Errorf("failed to process request headers: %v", err))
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
_ = util.SendResponse(500, "ai-proxy.proc_req_headers_failed", util.MimeTypeTextPlain, fmt.Sprintf("failed to process request headers: %v", err))
|
||||
hasRequestBody := wrapper.HasRequestBody()
|
||||
if hasRequestBody {
|
||||
_ = proxywasm.RemoveHttpRequestHeader("Content-Length")
|
||||
ctx.SetRequestBodyBufferLimit(defaultMaxBodyBytes)
|
||||
// Delay the header processing to allow changing in OnRequestBody
|
||||
return types.HeaderStopIteration
|
||||
}
|
||||
ctx.DontReadRequestBody()
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
@@ -132,21 +142,22 @@ func onHttpRequestBody(ctx wrapper.HttpContext, pluginConfig config.PluginConfig
|
||||
log.Debugf("[onHttpRequestBody] provider=%s", activeProvider.GetProviderType())
|
||||
|
||||
if handler, ok := activeProvider.(provider.RequestBodyHandler); ok {
|
||||
apiName, _ := ctx.GetContext(ctxKeyApiName).(provider.ApiName)
|
||||
|
||||
newBody, settingErr := pluginConfig.GetProviderConfig().ReplaceByCustomSettings(body)
|
||||
apiName, _ := ctx.GetContext(provider.CtxKeyApiName).(provider.ApiName)
|
||||
providerConfig := pluginConfig.GetProviderConfig()
|
||||
newBody, settingErr := providerConfig.ReplaceByCustomSettings(body)
|
||||
if settingErr != nil {
|
||||
_ = util.SendResponse(500, "ai-proxy.proc_req_body_failed", util.MimeTypeTextPlain, fmt.Sprintf("failed to rewrite request body by custom settings: %v", settingErr))
|
||||
return types.ActionContinue
|
||||
log.Errorf("failed to replace request body by custom settings: %v", settingErr)
|
||||
}
|
||||
if providerConfig.IsOpenAIProtocol() {
|
||||
newBody = normalizeOpenAiRequestBody(newBody, log)
|
||||
}
|
||||
|
||||
log.Debugf("[onHttpRequestBody] newBody=%s", newBody)
|
||||
body = newBody
|
||||
action, err := handler.OnRequestBody(ctx, apiName, body, log)
|
||||
if err == nil {
|
||||
return action
|
||||
}
|
||||
_ = util.SendResponse(500, "ai-proxy.proc_req_body_failed", util.MimeTypeTextPlain, fmt.Sprintf("failed to process request body: %v", err))
|
||||
_ = util.ErrorHandler("ai-proxy.proc_req_body_failed", fmt.Errorf("failed to process request body: %v", err))
|
||||
}
|
||||
return types.ActionContinue
|
||||
}
|
||||
@@ -170,6 +181,7 @@ func onHttpResponseHeaders(ctx wrapper.HttpContext, pluginConfig config.PluginCo
|
||||
|
||||
providerConfig := pluginConfig.GetProviderConfig()
|
||||
apiTokenInUse := providerConfig.GetApiTokenInUse(ctx)
|
||||
apiTokens := providerConfig.GetAvailableApiToken(ctx)
|
||||
|
||||
status, err := proxywasm.GetHttpResponseHeader(":status")
|
||||
if err != nil || status != "200" {
|
||||
@@ -177,29 +189,29 @@ func onHttpResponseHeaders(ctx wrapper.HttpContext, pluginConfig config.PluginCo
|
||||
log.Errorf("unable to load :status header from response: %v", err)
|
||||
}
|
||||
ctx.DontReadResponseBody()
|
||||
providerConfig.OnRequestFailed(ctx, apiTokenInUse, log)
|
||||
|
||||
return types.ActionContinue
|
||||
return providerConfig.OnRequestFailed(activeProvider, ctx, apiTokenInUse, apiTokens, log)
|
||||
}
|
||||
|
||||
// Reset ctxApiTokenRequestFailureCount if the request is successful,
|
||||
// the apiToken is removed only when the number of consecutive request failures exceeds the threshold.
|
||||
providerConfig.ResetApiTokenRequestFailureCount(apiTokenInUse, log)
|
||||
|
||||
if handler, ok := activeProvider.(provider.ResponseHeadersHandler); ok {
|
||||
apiName, _ := ctx.GetContext(ctxKeyApiName).(provider.ApiName)
|
||||
action, err := handler.OnResponseHeaders(ctx, apiName, log)
|
||||
if err == nil {
|
||||
checkStream(&ctx, &log)
|
||||
return action
|
||||
}
|
||||
_ = util.SendResponse(500, "ai-proxy.proc_resp_headers_failed", util.MimeTypeTextPlain, fmt.Sprintf("failed to process response headers: %v", err))
|
||||
return types.ActionContinue
|
||||
headers := util.GetOriginalResponseHeaders()
|
||||
if handler, ok := activeProvider.(provider.TransformResponseHeadersHandler); ok {
|
||||
apiName, _ := ctx.GetContext(provider.CtxKeyApiName).(provider.ApiName)
|
||||
handler.TransformResponseHeaders(ctx, apiName, headers, log)
|
||||
} else {
|
||||
providerConfig.DefaultTransformResponseHeaders(ctx, headers)
|
||||
}
|
||||
util.ReplaceResponseHeaders(headers)
|
||||
|
||||
checkStream(&ctx, &log)
|
||||
_, needHandleBody := activeProvider.(provider.ResponseBodyHandler)
|
||||
_, needHandleStreamingBody := activeProvider.(provider.StreamingResponseBodyHandler)
|
||||
checkStream(ctx, log)
|
||||
_, needHandleBody := activeProvider.(provider.TransformResponseBodyHandler)
|
||||
var needHandleStreamingBody bool
|
||||
_, needHandleStreamingBody = activeProvider.(provider.StreamingResponseBodyHandler)
|
||||
if !needHandleStreamingBody {
|
||||
_, needHandleStreamingBody = activeProvider.(provider.StreamingEventHandler)
|
||||
}
|
||||
if !needHandleBody && !needHandleStreamingBody {
|
||||
ctx.DontReadResponseBody()
|
||||
} else if !needHandleStreamingBody {
|
||||
@@ -218,16 +230,48 @@ func onStreamingResponseBody(ctx wrapper.HttpContext, pluginConfig config.Plugin
|
||||
}
|
||||
|
||||
log.Debugf("[onStreamingResponseBody] provider=%s", activeProvider.GetProviderType())
|
||||
log.Debugf("isLastChunk=%v chunk: %s", isLastChunk, string(chunk))
|
||||
log.Debugf("[onStreamingResponseBody] isLastChunk=%v chunk: %s", isLastChunk, string(chunk))
|
||||
|
||||
if handler, ok := activeProvider.(provider.StreamingResponseBodyHandler); ok {
|
||||
apiName, _ := ctx.GetContext(ctxKeyApiName).(provider.ApiName)
|
||||
apiName, _ := ctx.GetContext(provider.CtxKeyApiName).(provider.ApiName)
|
||||
modifiedChunk, err := handler.OnStreamingResponseBody(ctx, apiName, chunk, isLastChunk, log)
|
||||
if err == nil && modifiedChunk != nil {
|
||||
return modifiedChunk
|
||||
}
|
||||
return chunk
|
||||
}
|
||||
if handler, ok := activeProvider.(provider.StreamingEventHandler); ok {
|
||||
apiName, _ := ctx.GetContext(provider.CtxKeyApiName).(provider.ApiName)
|
||||
events := provider.ExtractStreamingEvents(ctx, chunk, log)
|
||||
log.Debugf("[onStreamingResponseBody] %d events received", len(events))
|
||||
if len(events) == 0 {
|
||||
// No events are extracted, return the original chunk
|
||||
return chunk
|
||||
}
|
||||
var responseBuilder strings.Builder
|
||||
for _, event := range events {
|
||||
log.Debugf("processing event: %v", event)
|
||||
|
||||
if event.IsEndData() {
|
||||
responseBuilder.WriteString(event.ToHttpString())
|
||||
continue
|
||||
}
|
||||
|
||||
outputEvents, err := handler.OnStreamingEvent(ctx, apiName, event, log)
|
||||
if err != nil {
|
||||
log.Errorf("[onStreamingResponseBody] failed to process streaming event: %v\n%s", err, chunk)
|
||||
return chunk
|
||||
}
|
||||
if outputEvents == nil || len(outputEvents) == 0 {
|
||||
responseBuilder.WriteString(event.ToHttpString())
|
||||
} else {
|
||||
for _, outputEvent := range outputEvents {
|
||||
responseBuilder.WriteString(outputEvent.ToHttpString())
|
||||
}
|
||||
}
|
||||
}
|
||||
return []byte(responseBuilder.String())
|
||||
}
|
||||
return chunk
|
||||
}
|
||||
|
||||
@@ -240,36 +284,64 @@ func onHttpResponseBody(ctx wrapper.HttpContext, pluginConfig config.PluginConfi
|
||||
}
|
||||
|
||||
log.Debugf("[onHttpResponseBody] provider=%s", activeProvider.GetProviderType())
|
||||
//log.Debugf("response body: %s", string(body))
|
||||
|
||||
if handler, ok := activeProvider.(provider.ResponseBodyHandler); ok {
|
||||
apiName, _ := ctx.GetContext(ctxKeyApiName).(provider.ApiName)
|
||||
action, err := handler.OnResponseBody(ctx, apiName, body, log)
|
||||
if err == nil {
|
||||
return action
|
||||
if handler, ok := activeProvider.(provider.TransformResponseBodyHandler); ok {
|
||||
apiName, _ := ctx.GetContext(provider.CtxKeyApiName).(provider.ApiName)
|
||||
body, err := handler.TransformResponseBody(ctx, apiName, body, log)
|
||||
if err != nil {
|
||||
_ = util.ErrorHandler("ai-proxy.proc_resp_body_failed", fmt.Errorf("failed to process response body: %v", err))
|
||||
return types.ActionContinue
|
||||
}
|
||||
if err = provider.ReplaceResponseBody(body, log); err != nil {
|
||||
_ = util.ErrorHandler("ai-proxy.replace_resp_body_failed", fmt.Errorf("failed to replace response body: %v", err))
|
||||
}
|
||||
_ = util.SendResponse(500, "ai-proxy.proc_resp_body_failed", util.MimeTypeTextPlain, fmt.Sprintf("failed to process response body: %v", err))
|
||||
return types.ActionContinue
|
||||
}
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
func checkStream(ctx *wrapper.HttpContext, log *wrapper.Log) {
|
||||
func normalizeOpenAiRequestBody(body []byte, log wrapper.Log) []byte {
|
||||
var err error
|
||||
// Default setting include_usage.
|
||||
if gjson.GetBytes(body, "stream").Bool() {
|
||||
body, err = sjson.SetBytes(body, "stream_options.include_usage", true)
|
||||
if err != nil {
|
||||
log.Errorf("set include_usage failed, err:%s", err)
|
||||
}
|
||||
}
|
||||
return body
|
||||
}
|
||||
|
||||
func checkStream(ctx wrapper.HttpContext, log wrapper.Log) {
|
||||
contentType, err := proxywasm.GetHttpResponseHeader("Content-Type")
|
||||
if err != nil || !strings.HasPrefix(contentType, "text/event-stream") {
|
||||
if err != nil {
|
||||
log.Errorf("unable to load content-type header from response: %v", err)
|
||||
}
|
||||
(*ctx).BufferResponseBody()
|
||||
ctx.BufferResponseBody()
|
||||
ctx.SetResponseBodyBufferLimit(defaultMaxBodyBytes)
|
||||
}
|
||||
}
|
||||
|
||||
func getOpenAiApiName(path string) provider.ApiName {
|
||||
func getApiName(path string) provider.ApiName {
|
||||
// openai style
|
||||
if strings.HasSuffix(path, "/v1/completions") {
|
||||
return provider.ApiNameCompletion
|
||||
}
|
||||
if strings.HasSuffix(path, "/v1/chat/completions") {
|
||||
return provider.ApiNameChatCompletion
|
||||
}
|
||||
if strings.HasSuffix(path, "/v1/embeddings") {
|
||||
return provider.ApiNameEmbeddings
|
||||
}
|
||||
if strings.HasSuffix(path, "/v1/audio/speech") {
|
||||
return provider.ApiNameAudioSpeech
|
||||
}
|
||||
if strings.HasSuffix(path, "/v1/images/generations") {
|
||||
return provider.ApiNameImageGeneration
|
||||
}
|
||||
// cohere style
|
||||
if strings.HasSuffix(path, "/v1/rerank") {
|
||||
return provider.ApiNameCohereV1Rerank
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -22,7 +22,14 @@ type ai360Provider struct {
|
||||
contextCache *contextCache
|
||||
}
|
||||
|
||||
func (m *ai360ProviderInitializer) ValidateConfig(config ProviderConfig) error {
|
||||
func (m *ai360ProviderInitializer) DefaultCapabilities() map[string]string {
|
||||
return map[string]string{
|
||||
string(ApiNameChatCompletion): PathOpenAIChatCompletions,
|
||||
string(ApiNameEmbeddings): PathOpenAIEmbeddings,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *ai360ProviderInitializer) ValidateConfig(config *ProviderConfig) error {
|
||||
if config.apiTokens == nil || len(config.apiTokens) == 0 {
|
||||
return errors.New("no apiToken found in provider config")
|
||||
}
|
||||
@@ -30,6 +37,7 @@ func (m *ai360ProviderInitializer) ValidateConfig(config ProviderConfig) error {
|
||||
}
|
||||
|
||||
func (m *ai360ProviderInitializer) CreateProvider(config ProviderConfig) (Provider, error) {
|
||||
config.setDefaultCapabilities(m.DefaultCapabilities())
|
||||
return &ai360Provider{
|
||||
config: config,
|
||||
contextCache: createContextCache(&config),
|
||||
@@ -40,17 +48,14 @@ func (m *ai360Provider) GetProviderType() string {
|
||||
return providerTypeAi360
|
||||
}
|
||||
|
||||
func (m *ai360Provider) OnRequestHeaders(ctx wrapper.HttpContext, apiName ApiName, log wrapper.Log) (types.Action, error) {
|
||||
if apiName != ApiNameChatCompletion && apiName != ApiNameEmbeddings {
|
||||
return types.ActionContinue, errUnsupportedApiName
|
||||
}
|
||||
func (m *ai360Provider) OnRequestHeaders(ctx wrapper.HttpContext, apiName ApiName, log wrapper.Log) error {
|
||||
m.config.handleRequestHeaders(m, ctx, apiName, log)
|
||||
// Delay the header processing to allow changing streaming mode in OnRequestBody
|
||||
return types.HeaderStopIteration, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *ai360Provider) OnRequestBody(ctx wrapper.HttpContext, apiName ApiName, body []byte, log wrapper.Log) (types.Action, error) {
|
||||
if apiName != ApiNameChatCompletion && apiName != ApiNameEmbeddings {
|
||||
if !m.config.isSupportedAPI(apiName) {
|
||||
return types.ActionContinue, errUnsupportedApiName
|
||||
}
|
||||
return m.config.handleRequestBody(m, m.contextCache, ctx, apiName, body, log)
|
||||
@@ -58,7 +63,6 @@ func (m *ai360Provider) OnRequestBody(ctx wrapper.HttpContext, apiName ApiName,
|
||||
|
||||
func (m *ai360Provider) TransformRequestHeaders(ctx wrapper.HttpContext, apiName ApiName, headers http.Header, log wrapper.Log) {
|
||||
util.OverwriteRequestHostHeader(headers, ai360Domain)
|
||||
util.OverwriteRequestAuthorizationHeader(headers, "Authorization "+m.config.GetApiTokenInUse(ctx))
|
||||
headers.Del("Accept-Encoding")
|
||||
headers.Del("Content-Length")
|
||||
util.OverwriteRequestPathHeaderByCapability(headers, string(apiName), m.config.capabilities)
|
||||
util.OverwriteRequestAuthorizationHeader(headers, m.config.GetApiTokenInUse(ctx))
|
||||
}
|
||||
|
||||
@@ -15,7 +15,15 @@ import (
|
||||
type azureProviderInitializer struct {
|
||||
}
|
||||
|
||||
func (m *azureProviderInitializer) ValidateConfig(config ProviderConfig) error {
|
||||
func (m *azureProviderInitializer) DefaultCapabilities() map[string]string {
|
||||
return map[string]string{
|
||||
// TODO: azure's pattern is the same as openai, just need to handle the prefix, can be done in TransformRequestHeaders to support general capabilities
|
||||
string(ApiNameChatCompletion): PathOpenAIChatCompletions,
|
||||
string(ApiNameEmbeddings): PathOpenAIEmbeddings,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *azureProviderInitializer) ValidateConfig(config *ProviderConfig) error {
|
||||
if config.azureServiceUrl == "" {
|
||||
return errors.New("missing azureServiceUrl in provider config")
|
||||
}
|
||||
@@ -35,6 +43,7 @@ func (m *azureProviderInitializer) CreateProvider(config ProviderConfig) (Provid
|
||||
} else {
|
||||
serviceUrl = u
|
||||
}
|
||||
config.setDefaultCapabilities(m.DefaultCapabilities())
|
||||
return &azureProvider{
|
||||
config: config,
|
||||
serviceUrl: serviceUrl,
|
||||
@@ -53,24 +62,38 @@ func (m *azureProvider) GetProviderType() string {
|
||||
return providerTypeAzure
|
||||
}
|
||||
|
||||
func (m *azureProvider) OnRequestHeaders(ctx wrapper.HttpContext, apiName ApiName, log wrapper.Log) (types.Action, error) {
|
||||
if apiName != ApiNameChatCompletion {
|
||||
return types.ActionContinue, errUnsupportedApiName
|
||||
}
|
||||
func (m *azureProvider) OnRequestHeaders(ctx wrapper.HttpContext, apiName ApiName, log wrapper.Log) error {
|
||||
m.config.handleRequestHeaders(m, ctx, apiName, log)
|
||||
return types.ActionContinue, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *azureProvider) OnRequestBody(ctx wrapper.HttpContext, apiName ApiName, body []byte, log wrapper.Log) (types.Action, error) {
|
||||
if apiName != ApiNameChatCompletion {
|
||||
if !m.config.isSupportedAPI(apiName) {
|
||||
return types.ActionContinue, errUnsupportedApiName
|
||||
}
|
||||
return m.config.handleRequestBody(m, m.contextCache, ctx, apiName, body, log)
|
||||
}
|
||||
|
||||
func (m *azureProvider) TransformRequestHeaders(ctx wrapper.HttpContext, apiName ApiName, headers http.Header, log wrapper.Log) {
|
||||
util.OverwriteRequestPathHeader(headers, m.serviceUrl.RequestURI())
|
||||
if apiName != "" {
|
||||
u, e := url.Parse(ctx.Path())
|
||||
if e == nil {
|
||||
customApiVersion := u.Query().Get("api-version")
|
||||
if customApiVersion == "" {
|
||||
util.OverwriteRequestPathHeader(headers, m.serviceUrl.RequestURI())
|
||||
} else {
|
||||
q := m.serviceUrl.Query()
|
||||
q.Set("api-version", customApiVersion)
|
||||
newUrl := *m.serviceUrl
|
||||
newUrl.RawQuery = q.Encode()
|
||||
util.OverwriteRequestPathHeader(headers, newUrl.RequestURI())
|
||||
}
|
||||
} else {
|
||||
log.Errorf("failed to parse request path: %v", e)
|
||||
util.OverwriteRequestPathHeader(headers, m.serviceUrl.RequestURI())
|
||||
}
|
||||
}
|
||||
util.OverwriteRequestHostHeader(headers, m.serviceUrl.Host)
|
||||
util.OverwriteRequestAuthorizationHeader(headers, "api-key "+m.config.GetApiTokenInUse(ctx))
|
||||
headers.Set("api-key", m.config.GetApiTokenInUse(ctx))
|
||||
headers.Del("Content-Length")
|
||||
}
|
||||
|
||||
@@ -12,21 +12,28 @@ import (
|
||||
// baichuanProvider is the provider for baichuan Ai service.
|
||||
|
||||
const (
|
||||
baichuanDomain = "api.baichuan-ai.com"
|
||||
baichuanChatCompletionPath = "/v1/chat/completions"
|
||||
baichuanDomain = "api.baichuan-ai.com"
|
||||
)
|
||||
|
||||
type baichuanProviderInitializer struct {
|
||||
}
|
||||
|
||||
func (m *baichuanProviderInitializer) ValidateConfig(config ProviderConfig) error {
|
||||
func (m *baichuanProviderInitializer) ValidateConfig(config *ProviderConfig) error {
|
||||
if config.apiTokens == nil || len(config.apiTokens) == 0 {
|
||||
return errors.New("no apiToken found in provider config")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *baichuanProviderInitializer) DefaultCapabilities() map[string]string {
|
||||
return map[string]string{
|
||||
string(ApiNameChatCompletion): PathOpenAIChatCompletions,
|
||||
string(ApiNameEmbeddings): PathOpenAIEmbeddings,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *baichuanProviderInitializer) CreateProvider(config ProviderConfig) (Provider, error) {
|
||||
config.setDefaultCapabilities(m.DefaultCapabilities())
|
||||
return &baichuanProvider{
|
||||
config: config,
|
||||
contextCache: createContextCache(&config),
|
||||
@@ -42,23 +49,20 @@ func (m *baichuanProvider) GetProviderType() string {
|
||||
return providerTypeBaichuan
|
||||
}
|
||||
|
||||
func (m *baichuanProvider) OnRequestHeaders(ctx wrapper.HttpContext, apiName ApiName, log wrapper.Log) (types.Action, error) {
|
||||
if apiName != ApiNameChatCompletion {
|
||||
return types.ActionContinue, errUnsupportedApiName
|
||||
}
|
||||
func (m *baichuanProvider) OnRequestHeaders(ctx wrapper.HttpContext, apiName ApiName, log wrapper.Log) error {
|
||||
m.config.handleRequestHeaders(m, ctx, apiName, log)
|
||||
return types.ActionContinue, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *baichuanProvider) OnRequestBody(ctx wrapper.HttpContext, apiName ApiName, body []byte, log wrapper.Log) (types.Action, error) {
|
||||
if apiName != ApiNameChatCompletion {
|
||||
if !m.config.isSupportedAPI(apiName) {
|
||||
return types.ActionContinue, errUnsupportedApiName
|
||||
}
|
||||
return m.config.handleRequestBody(m, m.contextCache, ctx, apiName, body, log)
|
||||
}
|
||||
|
||||
func (m *baichuanProvider) TransformRequestHeaders(ctx wrapper.HttpContext, apiName ApiName, headers http.Header, log wrapper.Log) {
|
||||
util.OverwriteRequestPathHeader(headers, baichuanChatCompletionPath)
|
||||
util.OverwriteRequestPathHeaderByCapability(headers, string(apiName), m.config.capabilities)
|
||||
util.OverwriteRequestHostHeader(headers, baichuanDomain)
|
||||
util.OverwriteRequestAuthorizationHeader(headers, "Bearer "+m.config.GetApiTokenInUse(ctx))
|
||||
headers.Del("Content-Length")
|
||||
|
||||
@@ -1,48 +1,40 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/extensions/ai-proxy/util"
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
)
|
||||
|
||||
// baiduProvider is the provider for baidu ernie bot service.
|
||||
|
||||
// baiduProvider is the provider for baidu service.
|
||||
const (
|
||||
baiduDomain = "aip.baidubce.com"
|
||||
baiduChatCompletionPath = "/chat"
|
||||
baiduDomain = "qianfan.baidubce.com"
|
||||
baiduChatCompletionPath = "/v2/chat/completions"
|
||||
baiduEmbeddings = "/v2/embeddings"
|
||||
)
|
||||
|
||||
var baiduModelToPathSuffixMap = map[string]string{
|
||||
"ERNIE-4.0-8K": "completions_pro",
|
||||
"ERNIE-3.5-8K": "completions",
|
||||
"ERNIE-3.5-128K": "ernie-3.5-128k",
|
||||
"ERNIE-Speed-8K": "ernie_speed",
|
||||
"ERNIE-Speed-128K": "ernie-speed-128k",
|
||||
"ERNIE-Tiny-8K": "ernie-tiny-8k",
|
||||
"ERNIE-Bot-8K": "ernie_bot_8k",
|
||||
"BLOOMZ-7B": "bloomz_7b1",
|
||||
}
|
||||
type baiduProviderInitializer struct{}
|
||||
|
||||
type baiduProviderInitializer struct {
|
||||
}
|
||||
|
||||
func (b *baiduProviderInitializer) ValidateConfig(config ProviderConfig) error {
|
||||
func (g *baiduProviderInitializer) ValidateConfig(config *ProviderConfig) error {
|
||||
if config.apiTokens == nil || len(config.apiTokens) == 0 {
|
||||
return errors.New("no apiToken found in provider config")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *baiduProviderInitializer) CreateProvider(config ProviderConfig) (Provider, error) {
|
||||
func (g *baiduProviderInitializer) DefaultCapabilities() map[string]string {
|
||||
return map[string]string{
|
||||
string(ApiNameChatCompletion): baiduChatCompletionPath,
|
||||
string(ApiNameEmbeddings): baiduEmbeddings,
|
||||
}
|
||||
}
|
||||
|
||||
func (g *baiduProviderInitializer) CreateProvider(config ProviderConfig) (Provider, error) {
|
||||
config.setDefaultCapabilities(g.DefaultCapabilities())
|
||||
return &baiduProvider{
|
||||
config: config,
|
||||
contextCache: createContextCache(&config),
|
||||
@@ -54,232 +46,30 @@ type baiduProvider struct {
|
||||
contextCache *contextCache
|
||||
}
|
||||
|
||||
func (b *baiduProvider) GetProviderType() string {
|
||||
func (g *baiduProvider) GetProviderType() string {
|
||||
return providerTypeBaidu
|
||||
}
|
||||
|
||||
func (b *baiduProvider) OnRequestHeaders(ctx wrapper.HttpContext, apiName ApiName, log wrapper.Log) (types.Action, error) {
|
||||
if apiName != ApiNameChatCompletion {
|
||||
return types.ActionContinue, errUnsupportedApiName
|
||||
}
|
||||
b.config.handleRequestHeaders(b, ctx, apiName, log)
|
||||
// Delay the header processing to allow changing streaming mode in OnRequestBody
|
||||
return types.HeaderStopIteration, nil
|
||||
func (g *baiduProvider) OnRequestHeaders(ctx wrapper.HttpContext, apiName ApiName, log wrapper.Log) error {
|
||||
g.config.handleRequestHeaders(g, ctx, apiName, log)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *baiduProvider) TransformRequestHeaders(ctx wrapper.HttpContext, apiName ApiName, headers http.Header, log wrapper.Log) {
|
||||
func (g *baiduProvider) OnRequestBody(ctx wrapper.HttpContext, apiName ApiName, body []byte, log wrapper.Log) (types.Action, error) {
|
||||
if !g.config.isSupportedAPI(apiName) {
|
||||
return types.ActionContinue, errUnsupportedApiName
|
||||
}
|
||||
return g.config.handleRequestBody(g, g.contextCache, ctx, apiName, body, log)
|
||||
}
|
||||
|
||||
func (g *baiduProvider) TransformRequestHeaders(ctx wrapper.HttpContext, apiName ApiName, headers http.Header, log wrapper.Log) {
|
||||
util.OverwriteRequestPathHeaderByCapability(headers, string(apiName), g.config.capabilities)
|
||||
util.OverwriteRequestHostHeader(headers, baiduDomain)
|
||||
headers.Del("Accept-Encoding")
|
||||
util.OverwriteRequestAuthorizationHeader(headers, "Bearer "+g.config.GetApiTokenInUse(ctx))
|
||||
headers.Del("Content-Length")
|
||||
}
|
||||
|
||||
func (b *baiduProvider) OnRequestBody(ctx wrapper.HttpContext, apiName ApiName, body []byte, log wrapper.Log) (types.Action, error) {
|
||||
if apiName != ApiNameChatCompletion {
|
||||
return types.ActionContinue, errUnsupportedApiName
|
||||
}
|
||||
return b.config.handleRequestBody(b, b.contextCache, ctx, apiName, body, log)
|
||||
}
|
||||
|
||||
func (b *baiduProvider) TransformRequestBodyHeaders(ctx wrapper.HttpContext, apiName ApiName, body []byte, headers http.Header, log wrapper.Log) ([]byte, error) {
|
||||
request := &chatCompletionRequest{}
|
||||
err := b.config.parseRequestAndMapModel(ctx, request, body, log)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
path := b.getRequestPath(ctx, request.Model)
|
||||
util.OverwriteRequestPathHeader(headers, path)
|
||||
|
||||
baiduRequest := b.baiduTextGenRequest(request)
|
||||
return json.Marshal(baiduRequest)
|
||||
}
|
||||
|
||||
func (b *baiduProvider) OnResponseHeaders(ctx wrapper.HttpContext, apiName ApiName, log wrapper.Log) (types.Action, error) {
|
||||
// 使用文心一言接口协议,跳过OnStreamingResponseBody()和OnResponseBody()
|
||||
if b.config.protocol == protocolOriginal {
|
||||
ctx.DontReadResponseBody()
|
||||
return types.ActionContinue, nil
|
||||
}
|
||||
|
||||
_ = proxywasm.RemoveHttpResponseHeader("Content-Length")
|
||||
return types.ActionContinue, nil
|
||||
}
|
||||
|
||||
func (b *baiduProvider) OnStreamingResponseBody(ctx wrapper.HttpContext, name ApiName, chunk []byte, isLastChunk bool, log wrapper.Log) ([]byte, error) {
|
||||
if isLastChunk || len(chunk) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
// sample event response:
|
||||
// data: {"id":"as-vb0m37ti8y","object":"chat.completion","created":1709089502,"sentence_id":0,"is_end":false,"is_truncated":false,"result":"当然可以,","need_clear_history":false,"finish_reason":"normal","usage":{"prompt_tokens":5,"completion_tokens":2,"total_tokens":7}}
|
||||
|
||||
// sample end event response:
|
||||
// data: {"id":"as-vb0m37ti8y","object":"chat.completion","created":1709089531,"sentence_id":20,"is_end":true,"is_truncated":false,"result":"","need_clear_history":false,"finish_reason":"normal","usage":{"prompt_tokens":5,"completion_tokens":420,"total_tokens":425}}
|
||||
responseBuilder := &strings.Builder{}
|
||||
lines := strings.Split(string(chunk), "\n")
|
||||
for _, data := range lines {
|
||||
if len(data) < 6 {
|
||||
// ignore blank line or wrong format
|
||||
continue
|
||||
}
|
||||
data = data[6:]
|
||||
var baiduResponse baiduTextGenStreamResponse
|
||||
if err := json.Unmarshal([]byte(data), &baiduResponse); err != nil {
|
||||
log.Errorf("unable to unmarshal baidu response: %v", err)
|
||||
continue
|
||||
}
|
||||
response := b.streamResponseBaidu2OpenAI(ctx, &baiduResponse)
|
||||
responseBody, err := json.Marshal(response)
|
||||
if err != nil {
|
||||
log.Errorf("unable to marshal response: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
b.appendResponse(responseBuilder, string(responseBody))
|
||||
}
|
||||
modifiedResponseChunk := responseBuilder.String()
|
||||
log.Debugf("=== modified response chunk: %s", modifiedResponseChunk)
|
||||
return []byte(modifiedResponseChunk), nil
|
||||
}
|
||||
|
||||
func (b *baiduProvider) OnResponseBody(ctx wrapper.HttpContext, apiName ApiName, body []byte, log wrapper.Log) (types.Action, error) {
|
||||
baiduResponse := &baiduTextGenResponse{}
|
||||
if err := json.Unmarshal(body, baiduResponse); err != nil {
|
||||
return types.ActionContinue, fmt.Errorf("unable to unmarshal baidu response: %v", err)
|
||||
}
|
||||
if baiduResponse.ErrorMsg != "" {
|
||||
return types.ActionContinue, fmt.Errorf("baidu response error, error_code: %d, error_message: %s", baiduResponse.ErrorCode, baiduResponse.ErrorMsg)
|
||||
}
|
||||
response := b.responseBaidu2OpenAI(ctx, baiduResponse)
|
||||
return types.ActionContinue, replaceJsonResponseBody(response, log)
|
||||
}
|
||||
|
||||
type baiduTextGenRequest struct {
|
||||
Model string `json:"model"`
|
||||
Messages []chatMessage `json:"messages"`
|
||||
Temperature float64 `json:"temperature,omitempty"`
|
||||
TopP float64 `json:"top_p,omitempty"`
|
||||
PenaltyScore float64 `json:"penalty_score,omitempty"`
|
||||
Stream bool `json:"stream,omitempty"`
|
||||
System string `json:"system,omitempty"`
|
||||
DisableSearch bool `json:"disable_search,omitempty"`
|
||||
EnableCitation bool `json:"enable_citation,omitempty"`
|
||||
MaxOutputTokens int `json:"max_output_tokens,omitempty"`
|
||||
UserId string `json:"user_id,omitempty"`
|
||||
}
|
||||
|
||||
func (b *baiduProvider) getRequestPath(ctx wrapper.HttpContext, baiduModel string) string {
|
||||
// https://cloud.baidu.com/doc/WENXINWORKSHOP/s/clntwmv7t
|
||||
suffix, ok := baiduModelToPathSuffixMap[baiduModel]
|
||||
if !ok {
|
||||
suffix = baiduModel
|
||||
}
|
||||
return fmt.Sprintf("/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/%s?access_token=%s", suffix, b.config.GetApiTokenInUse(ctx))
|
||||
}
|
||||
|
||||
func (b *baiduProvider) setSystemContent(request *baiduTextGenRequest, content string) {
|
||||
request.System = content
|
||||
}
|
||||
|
||||
func (b *baiduProvider) baiduTextGenRequest(request *chatCompletionRequest) *baiduTextGenRequest {
|
||||
baiduRequest := baiduTextGenRequest{
|
||||
Messages: make([]chatMessage, 0, len(request.Messages)),
|
||||
Temperature: request.Temperature,
|
||||
TopP: request.TopP,
|
||||
PenaltyScore: request.FrequencyPenalty,
|
||||
Stream: request.Stream,
|
||||
DisableSearch: false,
|
||||
EnableCitation: false,
|
||||
MaxOutputTokens: request.MaxTokens,
|
||||
UserId: request.User,
|
||||
}
|
||||
for _, message := range request.Messages {
|
||||
if message.Role == roleSystem {
|
||||
baiduRequest.System = message.StringContent()
|
||||
} else {
|
||||
baiduRequest.Messages = append(baiduRequest.Messages, chatMessage{
|
||||
Role: message.Role,
|
||||
Content: message.Content,
|
||||
})
|
||||
}
|
||||
}
|
||||
return &baiduRequest
|
||||
}
|
||||
|
||||
type baiduTextGenResponse struct {
|
||||
Id string `json:"id"`
|
||||
Object string `json:"object"`
|
||||
Created int64 `json:"created"`
|
||||
Result string `json:"result"`
|
||||
IsTruncated bool `json:"is_truncated"`
|
||||
NeedClearHistory bool `json:"need_clear_history"`
|
||||
Usage baiduTextGenResponseUsage `json:"usage"`
|
||||
baiduTextGenResponseError
|
||||
}
|
||||
|
||||
type baiduTextGenResponseError struct {
|
||||
ErrorCode int `json:"error_code"`
|
||||
ErrorMsg string `json:"error_msg"`
|
||||
}
|
||||
|
||||
type baiduTextGenStreamResponse struct {
|
||||
baiduTextGenResponse
|
||||
SentenceId int `json:"sentence_id"`
|
||||
IsEnd bool `json:"is_end"`
|
||||
}
|
||||
|
||||
type baiduTextGenResponseUsage struct {
|
||||
PromptTokens int `json:"prompt_tokens"`
|
||||
CompletionTokens int `json:"completion_tokens"`
|
||||
TotalTokens int `json:"total_tokens"`
|
||||
}
|
||||
|
||||
func (b *baiduProvider) responseBaidu2OpenAI(ctx wrapper.HttpContext, response *baiduTextGenResponse) *chatCompletionResponse {
|
||||
choice := chatCompletionChoice{
|
||||
Index: 0,
|
||||
Message: &chatMessage{Role: roleAssistant, Content: response.Result},
|
||||
FinishReason: finishReasonStop,
|
||||
}
|
||||
return &chatCompletionResponse{
|
||||
Id: response.Id,
|
||||
Created: time.Now().UnixMilli() / 1000,
|
||||
Model: ctx.GetStringContext(ctxKeyFinalRequestModel, ""),
|
||||
SystemFingerprint: "",
|
||||
Object: objectChatCompletion,
|
||||
Choices: []chatCompletionChoice{choice},
|
||||
Usage: usage{
|
||||
PromptTokens: response.Usage.PromptTokens,
|
||||
CompletionTokens: response.Usage.CompletionTokens,
|
||||
TotalTokens: response.Usage.TotalTokens,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (b *baiduProvider) streamResponseBaidu2OpenAI(ctx wrapper.HttpContext, response *baiduTextGenStreamResponse) *chatCompletionResponse {
|
||||
choice := chatCompletionChoice{
|
||||
Index: 0,
|
||||
Message: &chatMessage{Role: roleAssistant, Content: response.Result},
|
||||
}
|
||||
if response.IsEnd {
|
||||
choice.FinishReason = finishReasonStop
|
||||
}
|
||||
return &chatCompletionResponse{
|
||||
Id: response.Id,
|
||||
Created: time.Now().UnixMilli() / 1000,
|
||||
Model: ctx.GetStringContext(ctxKeyFinalRequestModel, ""),
|
||||
SystemFingerprint: "",
|
||||
Object: objectChatCompletionChunk,
|
||||
Choices: []chatCompletionChoice{choice},
|
||||
Usage: usage{
|
||||
PromptTokens: response.Usage.PromptTokens,
|
||||
CompletionTokens: response.Usage.CompletionTokens,
|
||||
TotalTokens: response.Usage.TotalTokens,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (b *baiduProvider) appendResponse(responseBuilder *strings.Builder, responseBody string) {
|
||||
responseBuilder.WriteString(fmt.Sprintf("%s %s\n\n", streamDataItemKey, responseBody))
|
||||
}
|
||||
|
||||
func (b *baiduProvider) GetApiName(path string) ApiName {
|
||||
func (g *baiduProvider) GetApiName(path string) ApiName {
|
||||
if strings.Contains(path, baiduChatCompletionPath) {
|
||||
return ApiNameChatCompletion
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/extensions/ai-proxy/util"
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
)
|
||||
|
||||
@@ -18,6 +17,7 @@ import (
|
||||
const (
|
||||
claudeDomain = "api.anthropic.com"
|
||||
claudeChatCompletionPath = "/v1/messages"
|
||||
claudeCompletionPath = "/v1/complete"
|
||||
defaultVersion = "2023-06-01"
|
||||
defaultMaxTokens = 4096
|
||||
)
|
||||
@@ -79,14 +79,24 @@ type claudeTextGenDelta struct {
|
||||
StopSequence *string `json:"stop_sequence"`
|
||||
}
|
||||
|
||||
func (c *claudeProviderInitializer) ValidateConfig(config ProviderConfig) error {
|
||||
func (c *claudeProviderInitializer) ValidateConfig(config *ProviderConfig) error {
|
||||
if config.apiTokens == nil || len(config.apiTokens) == 0 {
|
||||
return errors.New("no apiToken found in provider config")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *claudeProviderInitializer) DefaultCapabilities() map[string]string {
|
||||
return map[string]string{
|
||||
string(ApiNameChatCompletion): claudeChatCompletionPath,
|
||||
string(ApiNameCompletion): claudeCompletionPath,
|
||||
// docs: https://docs.anthropic.com/en/docs/build-with-claude/embeddings#voyage-http-api
|
||||
string(ApiNameEmbeddings): PathOpenAIEmbeddings,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *claudeProviderInitializer) CreateProvider(config ProviderConfig) (Provider, error) {
|
||||
config.setDefaultCapabilities(c.DefaultCapabilities())
|
||||
return &claudeProvider{
|
||||
config: config,
|
||||
contextCache: createContextCache(&config),
|
||||
@@ -102,37 +112,35 @@ func (c *claudeProvider) GetProviderType() string {
|
||||
return providerTypeClaude
|
||||
}
|
||||
|
||||
func (c *claudeProvider) OnRequestHeaders(ctx wrapper.HttpContext, apiName ApiName, log wrapper.Log) (types.Action, error) {
|
||||
if apiName != ApiNameChatCompletion {
|
||||
return types.ActionContinue, errUnsupportedApiName
|
||||
}
|
||||
func (c *claudeProvider) OnRequestHeaders(ctx wrapper.HttpContext, apiName ApiName, log wrapper.Log) error {
|
||||
c.config.handleRequestHeaders(c, ctx, apiName, log)
|
||||
return types.ActionContinue, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *claudeProvider) TransformRequestHeaders(ctx wrapper.HttpContext, apiName ApiName, headers http.Header, log wrapper.Log) {
|
||||
util.OverwriteRequestPathHeader(headers, claudeChatCompletionPath)
|
||||
util.OverwriteRequestPathHeaderByCapability(headers, string(apiName), c.config.capabilities)
|
||||
util.OverwriteRequestHostHeader(headers, claudeDomain)
|
||||
|
||||
headers.Add("x-api-key", c.config.GetApiTokenInUse(ctx))
|
||||
headers.Set("x-api-key", c.config.GetApiTokenInUse(ctx))
|
||||
|
||||
if c.config.claudeVersion == "" {
|
||||
c.config.claudeVersion = defaultVersion
|
||||
}
|
||||
|
||||
headers.Add("anthropic-version", c.config.claudeVersion)
|
||||
headers.Del("Accept-Encoding")
|
||||
headers.Del("Content-Length")
|
||||
headers.Set("anthropic-version", c.config.claudeVersion)
|
||||
}
|
||||
|
||||
func (c *claudeProvider) OnRequestBody(ctx wrapper.HttpContext, apiName ApiName, body []byte, log wrapper.Log) (types.Action, error) {
|
||||
if apiName != ApiNameChatCompletion {
|
||||
if !c.config.isSupportedAPI(apiName) {
|
||||
return types.ActionContinue, errUnsupportedApiName
|
||||
}
|
||||
return c.config.handleRequestBody(c, c.contextCache, ctx, apiName, body, log)
|
||||
}
|
||||
|
||||
func (c *claudeProvider) TransformRequestBody(ctx wrapper.HttpContext, apiName ApiName, body []byte, log wrapper.Log) ([]byte, error) {
|
||||
if apiName != ApiNameChatCompletion {
|
||||
return c.config.defaultTransformRequestBody(ctx, apiName, body, log)
|
||||
}
|
||||
request := &chatCompletionRequest{}
|
||||
if err := c.config.parseRequestAndMapModel(ctx, request, body, log); err != nil {
|
||||
return nil, err
|
||||
@@ -141,33 +149,29 @@ func (c *claudeProvider) TransformRequestBody(ctx wrapper.HttpContext, apiName A
|
||||
return json.Marshal(claudeRequest)
|
||||
}
|
||||
|
||||
func (c *claudeProvider) OnResponseBody(ctx wrapper.HttpContext, apiName ApiName, body []byte, log wrapper.Log) (types.Action, error) {
|
||||
func (c *claudeProvider) TransformResponseBody(ctx wrapper.HttpContext, apiName ApiName, body []byte, log wrapper.Log) ([]byte, error) {
|
||||
if apiName != ApiNameChatCompletion {
|
||||
return body, nil
|
||||
}
|
||||
claudeResponse := &claudeTextGenResponse{}
|
||||
if err := json.Unmarshal(body, claudeResponse); err != nil {
|
||||
return types.ActionContinue, fmt.Errorf("unable to unmarshal claude response: %v", err)
|
||||
return nil, fmt.Errorf("unable to unmarshal claude response: %v", err)
|
||||
}
|
||||
if claudeResponse.Error != nil {
|
||||
return types.ActionContinue, fmt.Errorf("claude response error, error_type: %s, error_message: %s", claudeResponse.Error.Type, claudeResponse.Error.Message)
|
||||
return nil, fmt.Errorf("claude response error, error_type: %s, error_message: %s", claudeResponse.Error.Type, claudeResponse.Error.Message)
|
||||
}
|
||||
response := c.responseClaude2OpenAI(ctx, claudeResponse)
|
||||
return types.ActionContinue, replaceJsonResponseBody(response, log)
|
||||
}
|
||||
|
||||
func (c *claudeProvider) OnResponseHeaders(ctx wrapper.HttpContext, apiName ApiName, log wrapper.Log) (types.Action, error) {
|
||||
// use original protocol, skip OnStreamingResponseBody() and OnResponseBody()
|
||||
if c.config.protocol == protocolOriginal {
|
||||
ctx.DontReadResponseBody()
|
||||
return types.ActionContinue, nil
|
||||
}
|
||||
|
||||
_ = proxywasm.RemoveHttpResponseHeader("Content-Length")
|
||||
return types.ActionContinue, nil
|
||||
return json.Marshal(response)
|
||||
}
|
||||
|
||||
func (c *claudeProvider) OnStreamingResponseBody(ctx wrapper.HttpContext, name ApiName, chunk []byte, isLastChunk bool, log wrapper.Log) ([]byte, error) {
|
||||
if isLastChunk || len(chunk) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
// only process the response from chat completion, skip other responses
|
||||
if name != ApiNameChatCompletion {
|
||||
return chunk, nil
|
||||
}
|
||||
|
||||
responseBuilder := &strings.Builder{}
|
||||
lines := strings.Split(string(chunk), "\n")
|
||||
|
||||
@@ -19,14 +19,20 @@ const (
|
||||
type cloudflareProviderInitializer struct {
|
||||
}
|
||||
|
||||
func (c *cloudflareProviderInitializer) ValidateConfig(config ProviderConfig) error {
|
||||
func (c *cloudflareProviderInitializer) ValidateConfig(config *ProviderConfig) error {
|
||||
if config.apiTokens == nil || len(config.apiTokens) == 0 {
|
||||
return errors.New("no apiToken found in provider config")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (c *cloudflareProviderInitializer) DefaultCapabilities() map[string]string {
|
||||
return map[string]string{
|
||||
string(ApiNameChatCompletion): cloudflareChatCompletionPath,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *cloudflareProviderInitializer) CreateProvider(config ProviderConfig) (Provider, error) {
|
||||
config.setDefaultCapabilities(c.DefaultCapabilities())
|
||||
return &cloudflareProvider{
|
||||
config: config,
|
||||
contextCache: createContextCache(&config),
|
||||
@@ -42,16 +48,13 @@ func (c *cloudflareProvider) GetProviderType() string {
|
||||
return providerTypeCloudflare
|
||||
}
|
||||
|
||||
func (c *cloudflareProvider) OnRequestHeaders(ctx wrapper.HttpContext, apiName ApiName, log wrapper.Log) (types.Action, error) {
|
||||
if apiName != ApiNameChatCompletion {
|
||||
return types.ActionContinue, errUnsupportedApiName
|
||||
}
|
||||
func (c *cloudflareProvider) OnRequestHeaders(ctx wrapper.HttpContext, apiName ApiName, log wrapper.Log) error {
|
||||
c.config.handleRequestHeaders(c, ctx, apiName, log)
|
||||
return types.ActionContinue, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *cloudflareProvider) OnRequestBody(ctx wrapper.HttpContext, apiName ApiName, body []byte, log wrapper.Log) (types.Action, error) {
|
||||
if apiName != ApiNameChatCompletion {
|
||||
if !c.config.isSupportedAPI(apiName) {
|
||||
return types.ActionContinue, errUnsupportedApiName
|
||||
}
|
||||
return c.config.handleRequestBody(c, c.contextCache, ctx, apiName, body, log)
|
||||
@@ -61,6 +64,4 @@ func (c *cloudflareProvider) TransformRequestHeaders(ctx wrapper.HttpContext, ap
|
||||
util.OverwriteRequestPathHeader(headers, strings.Replace(cloudflareChatCompletionPath, "{account_id}", c.config.cloudflareAccountId, 1))
|
||||
util.OverwriteRequestHostHeader(headers, cloudflareDomain)
|
||||
util.OverwriteRequestAuthorizationHeader(headers, "Bearer "+c.config.GetApiTokenInUse(ctx))
|
||||
headers.Del("Accept-Encoding")
|
||||
headers.Del("Content-Length")
|
||||
}
|
||||
|
||||
@@ -3,28 +3,39 @@ package provider
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/extensions/ai-proxy/util"
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
cohereDomain = "api.cohere.com"
|
||||
cohereDomain = "api.cohere.com"
|
||||
// TODO: support more capabilities, upgrade to v2, docs: https://docs.cohere.com/v2/reference/chat
|
||||
cohereChatCompletionPath = "/v1/chat"
|
||||
cohereRerankPath = "/v1/rerank"
|
||||
)
|
||||
|
||||
type cohereProviderInitializer struct{}
|
||||
|
||||
func (m *cohereProviderInitializer) ValidateConfig(config ProviderConfig) error {
|
||||
func (m *cohereProviderInitializer) ValidateConfig(config *ProviderConfig) error {
|
||||
if config.apiTokens == nil || len(config.apiTokens) == 0 {
|
||||
return errors.New("no apiToken found in provider config")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *cohereProviderInitializer) DefaultCapabilities() map[string]string {
|
||||
return map[string]string{
|
||||
string(ApiNameChatCompletion): cohereChatCompletionPath,
|
||||
string(ApiNameCohereV1Rerank): cohereRerankPath,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *cohereProviderInitializer) CreateProvider(config ProviderConfig) (Provider, error) {
|
||||
config.setDefaultCapabilities(m.DefaultCapabilities())
|
||||
return &cohereProvider{
|
||||
config: config,
|
||||
contextCache: createContextCache(&config),
|
||||
@@ -54,16 +65,13 @@ func (m *cohereProvider) GetProviderType() string {
|
||||
return providerTypeCohere
|
||||
}
|
||||
|
||||
func (m *cohereProvider) OnRequestHeaders(ctx wrapper.HttpContext, apiName ApiName, log wrapper.Log) (types.Action, error) {
|
||||
if apiName != ApiNameChatCompletion {
|
||||
return types.ActionContinue, errUnsupportedApiName
|
||||
}
|
||||
func (m *cohereProvider) OnRequestHeaders(ctx wrapper.HttpContext, apiName ApiName, log wrapper.Log) error {
|
||||
m.config.handleRequestHeaders(m, ctx, apiName, log)
|
||||
return types.ActionContinue, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *cohereProvider) OnRequestBody(ctx wrapper.HttpContext, apiName ApiName, body []byte, log wrapper.Log) (types.Action, error) {
|
||||
if apiName != ApiNameChatCompletion {
|
||||
if !m.config.isSupportedAPI(apiName) {
|
||||
return types.ActionContinue, errUnsupportedApiName
|
||||
}
|
||||
return m.config.handleRequestBody(m, m.contextCache, ctx, apiName, body, log)
|
||||
@@ -89,13 +97,16 @@ func (m *cohereProvider) buildCohereRequest(origin *chatCompletionRequest) *cohe
|
||||
}
|
||||
|
||||
func (m *cohereProvider) TransformRequestHeaders(ctx wrapper.HttpContext, apiName ApiName, headers http.Header, log wrapper.Log) {
|
||||
util.OverwriteRequestPathHeader(headers, cohereChatCompletionPath)
|
||||
util.OverwriteRequestPathHeaderByCapability(headers, string(apiName), m.config.capabilities)
|
||||
util.OverwriteRequestHostHeader(headers, cohereDomain)
|
||||
util.OverwriteRequestAuthorizationHeader(headers, "Bearer "+m.config.GetApiTokenInUse(ctx))
|
||||
headers.Del("Content-Length")
|
||||
}
|
||||
|
||||
func (m *cohereProvider) TransformRequestBody(ctx wrapper.HttpContext, apiName ApiName, body []byte, log wrapper.Log) ([]byte, error) {
|
||||
if apiName != ApiNameChatCompletion {
|
||||
return m.config.defaultTransformRequestBody(ctx, apiName, body, log)
|
||||
}
|
||||
request := &chatCompletionRequest{}
|
||||
if err := m.config.parseRequestAndMapModel(ctx, request, body, log); err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -139,7 +139,7 @@ func insertContext(provider Provider, content string, err error, body []byte, lo
|
||||
typ := provider.GetProviderType()
|
||||
if err != nil {
|
||||
log.Errorf("failed to load context file: %v", err)
|
||||
_ = util.SendResponse(500, fmt.Sprintf("ai-proxy.%s.load_ctx_failed", typ), util.MimeTypeTextPlain, fmt.Sprintf("failed to load context file: %v", err))
|
||||
util.ErrorHandler(fmt.Sprintf("ai-proxy.%s.load_ctx_failed", typ), fmt.Errorf("failed to load context file: %v", err))
|
||||
}
|
||||
|
||||
if inserter, ok := provider.(ContextInserter); ok {
|
||||
@@ -149,10 +149,10 @@ func insertContext(provider Provider, content string, err error, body []byte, lo
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
_ = util.SendResponse(500, fmt.Sprintf("ai-proxy.%s.insert_ctx_failed", typ), util.MimeTypeTextPlain, fmt.Sprintf("failed to insert context message: %v", err))
|
||||
util.ErrorHandler(fmt.Sprintf("ai-proxy.%s.insert_ctx_failed", typ), fmt.Errorf("failed to insert context message: %v", err))
|
||||
}
|
||||
if err := replaceHttpJsonRequestBody(body, log); err != nil {
|
||||
_ = util.SendResponse(500, fmt.Sprintf("ai-proxy.%s.replace_request_body_failed", typ), util.MimeTypeTextPlain, fmt.Sprintf("failed to replace request body: %v", err))
|
||||
if err := replaceRequestBody(body, log); err != nil {
|
||||
util.ErrorHandler(fmt.Sprintf("ai-proxy.%s.replace_request_body_failed", typ), fmt.Errorf("failed to replace request body: %v", err))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/extensions/ai-proxy/util"
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -15,14 +14,19 @@ const (
|
||||
|
||||
type cozeProviderInitializer struct{}
|
||||
|
||||
func (m *cozeProviderInitializer) ValidateConfig(config ProviderConfig) error {
|
||||
func (m *cozeProviderInitializer) ValidateConfig(config *ProviderConfig) error {
|
||||
if config.apiTokens == nil || len(config.apiTokens) == 0 {
|
||||
return errors.New("no apiToken found in provider config")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *cozeProviderInitializer) DefaultCapabilities() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (m *cozeProviderInitializer) CreateProvider(config ProviderConfig) (Provider, error) {
|
||||
config.setDefaultCapabilities(m.DefaultCapabilities())
|
||||
return &cozeProvider{
|
||||
config: config,
|
||||
contextCache: createContextCache(&config),
|
||||
@@ -38,9 +42,9 @@ func (m *cozeProvider) GetProviderType() string {
|
||||
return providerTypeCoze
|
||||
}
|
||||
|
||||
func (m *cozeProvider) OnRequestHeaders(ctx wrapper.HttpContext, apiName ApiName, log wrapper.Log) (types.Action, error) {
|
||||
func (m *cozeProvider) OnRequestHeaders(ctx wrapper.HttpContext, apiName ApiName, log wrapper.Log) error {
|
||||
m.config.handleRequestHeaders(m, ctx, apiName, log)
|
||||
return types.ActionContinue, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *cozeProvider) TransformRequestHeaders(ctx wrapper.HttpContext, apiName ApiName, headers http.Header, log wrapper.Log) {
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/extensions/ai-proxy/util"
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
)
|
||||
|
||||
@@ -58,14 +57,21 @@ type deeplResponseTranslation struct {
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
func (d *deeplProviderInitializer) ValidateConfig(config ProviderConfig) error {
|
||||
func (d *deeplProviderInitializer) ValidateConfig(config *ProviderConfig) error {
|
||||
if config.targetLang == "" {
|
||||
return errors.New("missing targetLang in deepl provider config")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *deeplProviderInitializer) DefaultCapabilities() map[string]string {
|
||||
return map[string]string{
|
||||
string(ApiNameChatCompletion): deeplChatCompletionPath,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *deeplProviderInitializer) CreateProvider(config ProviderConfig) (Provider, error) {
|
||||
config.setDefaultCapabilities(d.DefaultCapabilities())
|
||||
return &deeplProvider{
|
||||
config: config,
|
||||
contextCache: createContextCache(&config),
|
||||
@@ -76,23 +82,22 @@ func (d *deeplProvider) GetProviderType() string {
|
||||
return providerTypeDeepl
|
||||
}
|
||||
|
||||
func (d *deeplProvider) OnRequestHeaders(ctx wrapper.HttpContext, apiName ApiName, log wrapper.Log) (types.Action, error) {
|
||||
if apiName != ApiNameChatCompletion {
|
||||
return types.ActionContinue, errUnsupportedApiName
|
||||
}
|
||||
func (d *deeplProvider) OnRequestHeaders(ctx wrapper.HttpContext, apiName ApiName, log wrapper.Log) error {
|
||||
d.config.handleRequestHeaders(d, ctx, apiName, log)
|
||||
return types.HeaderStopIteration, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *deeplProvider) TransformRequestHeaders(ctx wrapper.HttpContext, apiName ApiName, headers http.Header, log wrapper.Log) {
|
||||
util.OverwriteRequestPathHeader(headers, deeplChatCompletionPath)
|
||||
if apiName != "" {
|
||||
util.OverwriteRequestPathHeader(headers, deeplChatCompletionPath)
|
||||
}
|
||||
// TODO: Support default host through configuration
|
||||
util.OverwriteRequestHostHeader(headers, deeplHostFree)
|
||||
util.OverwriteRequestAuthorizationHeader(headers, "DeepL-Auth-Key "+d.config.GetApiTokenInUse(ctx))
|
||||
headers.Del("Content-Length")
|
||||
headers.Del("Accept-Encoding")
|
||||
}
|
||||
|
||||
func (d *deeplProvider) OnRequestBody(ctx wrapper.HttpContext, apiName ApiName, body []byte, log wrapper.Log) (types.Action, error) {
|
||||
if apiName != ApiNameChatCompletion {
|
||||
if !d.config.isSupportedAPI(apiName) {
|
||||
return types.ActionContinue, errUnsupportedApiName
|
||||
}
|
||||
return d.config.handleRequestBody(d, d.contextCache, ctx, apiName, body, log)
|
||||
@@ -114,18 +119,16 @@ func (d *deeplProvider) TransformRequestBodyHeaders(ctx wrapper.HttpContext, api
|
||||
return json.Marshal(baiduRequest)
|
||||
}
|
||||
|
||||
func (d *deeplProvider) OnResponseHeaders(ctx wrapper.HttpContext, apiName ApiName, log wrapper.Log) (types.Action, error) {
|
||||
_ = proxywasm.RemoveHttpResponseHeader("Content-Length")
|
||||
return types.ActionContinue, nil
|
||||
}
|
||||
|
||||
func (d *deeplProvider) OnResponseBody(ctx wrapper.HttpContext, apiName ApiName, body []byte, log wrapper.Log) (types.Action, error) {
|
||||
func (d *deeplProvider) TransformResponseBody(ctx wrapper.HttpContext, apiName ApiName, body []byte, log wrapper.Log) ([]byte, error) {
|
||||
if apiName != ApiNameChatCompletion {
|
||||
return body, nil
|
||||
}
|
||||
deeplResponse := &deeplResponse{}
|
||||
if err := json.Unmarshal(body, deeplResponse); err != nil {
|
||||
return types.ActionContinue, fmt.Errorf("unable to unmarshal deepl response: %v", err)
|
||||
return nil, fmt.Errorf("unable to unmarshal deepl response: %v", err)
|
||||
}
|
||||
response := d.responseDeepl2OpenAI(ctx, deeplResponse)
|
||||
return types.ActionContinue, replaceJsonResponseBody(response, log)
|
||||
return json.Marshal(response)
|
||||
}
|
||||
|
||||
func (d *deeplProvider) responseDeepl2OpenAI(ctx wrapper.HttpContext, deeplResponse *deeplResponse) *chatCompletionResponse {
|
||||
|
||||
@@ -2,30 +2,40 @@ package provider
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/extensions/ai-proxy/util"
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// deepseekProvider is the provider for deepseek Ai service.
|
||||
|
||||
const (
|
||||
deepseekDomain = "api.deepseek.com"
|
||||
deepseekDomain = "api.deepseek.com"
|
||||
// TODO: docs: https://api-docs.deepseek.com/api/create-chat-completion
|
||||
// accourding to the docs, the path should be /chat/completions, need to be verified
|
||||
deepseekChatCompletionPath = "/v1/chat/completions"
|
||||
)
|
||||
|
||||
type deepseekProviderInitializer struct {
|
||||
}
|
||||
|
||||
func (m *deepseekProviderInitializer) ValidateConfig(config ProviderConfig) error {
|
||||
func (m *deepseekProviderInitializer) ValidateConfig(config *ProviderConfig) error {
|
||||
if config.apiTokens == nil || len(config.apiTokens) == 0 {
|
||||
return errors.New("no apiToken found in provider config")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *deepseekProviderInitializer) DefaultCapabilities() map[string]string {
|
||||
return map[string]string{
|
||||
string(ApiNameChatCompletion): deepseekChatCompletionPath,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *deepseekProviderInitializer) CreateProvider(config ProviderConfig) (Provider, error) {
|
||||
config.setDefaultCapabilities(m.DefaultCapabilities())
|
||||
return &deepseekProvider{
|
||||
config: config,
|
||||
contextCache: createContextCache(&config),
|
||||
@@ -41,23 +51,20 @@ func (m *deepseekProvider) GetProviderType() string {
|
||||
return providerTypeDeepSeek
|
||||
}
|
||||
|
||||
func (m *deepseekProvider) OnRequestHeaders(ctx wrapper.HttpContext, apiName ApiName, log wrapper.Log) (types.Action, error) {
|
||||
if apiName != ApiNameChatCompletion {
|
||||
return types.ActionContinue, errUnsupportedApiName
|
||||
}
|
||||
func (m *deepseekProvider) OnRequestHeaders(ctx wrapper.HttpContext, apiName ApiName, log wrapper.Log) error {
|
||||
m.config.handleRequestHeaders(m, ctx, apiName, log)
|
||||
return types.ActionContinue, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *deepseekProvider) OnRequestBody(ctx wrapper.HttpContext, apiName ApiName, body []byte, log wrapper.Log) (types.Action, error) {
|
||||
if apiName != ApiNameChatCompletion {
|
||||
if !m.config.isSupportedAPI(apiName) {
|
||||
return types.ActionContinue, errUnsupportedApiName
|
||||
}
|
||||
return m.config.handleRequestBody(m, m.contextCache, ctx, apiName, body, log)
|
||||
}
|
||||
|
||||
func (m *deepseekProvider) TransformRequestHeaders(ctx wrapper.HttpContext, apiName ApiName, headers http.Header, log wrapper.Log) {
|
||||
util.OverwriteRequestPathHeader(headers, deepseekChatCompletionPath)
|
||||
util.OverwriteRequestPathHeaderByCapability(headers, string(apiName), m.config.capabilities)
|
||||
util.OverwriteRequestHostHeader(headers, deepseekDomain)
|
||||
util.OverwriteRequestAuthorizationHeader(headers, "Bearer "+m.config.GetApiTokenInUse(ctx))
|
||||
headers.Del("Content-Length")
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user