mirror of
https://github.com/alibaba/higress.git
synced 2026-03-01 15:10:56 +08:00
Compare commits
91 Commits
wasm-go-ai
...
v2.1.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f5067d22f | ||
|
|
6af9587372 | ||
|
|
5812c1e734 | ||
|
|
bafbe7972d | ||
|
|
f3fbf7d6c8 | ||
|
|
1666dfb01c | ||
|
|
d2f09fe8c5 | ||
|
|
69d877c116 | ||
|
|
5bc0058779 | ||
|
|
d4e114b152 | ||
|
|
e674c780c6 | ||
|
|
26cd6837d5 | ||
|
|
5674d91a10 | ||
|
|
c78b4aaba3 | ||
|
|
0e4e8da9c1 | ||
|
|
c9ec8a12bb | ||
|
|
7484bcea62 | ||
|
|
896780b60e | ||
|
|
7b1ae49cd4 | ||
|
|
ee26baf054 | ||
|
|
33fc47cefb | ||
|
|
19946d46ca | ||
|
|
52d0212698 | ||
|
|
a73c33f1da | ||
|
|
69b755a10d | ||
|
|
52464c0e06 | ||
|
|
d7d5d1c571 | ||
|
|
ea948ee818 | ||
|
|
767f51adce | ||
|
|
168cb04c61 | ||
|
|
323aabf72b | ||
|
|
b8d75598ed | ||
|
|
b37649a62f | ||
|
|
76f76a70ab | ||
|
|
647c961f51 | ||
|
|
5a5a72a9f8 | ||
|
|
ffcf5df28a | ||
|
|
ec83623614 | ||
|
|
bf5be07d74 | ||
|
|
f6bb5d7729 | ||
|
|
031ae21caa | ||
|
|
fa3c5ea0fc | ||
|
|
93436db13c | ||
|
|
be2c6f8a4a | ||
|
|
c768973e47 | ||
|
|
8ec65ed377 | ||
|
|
675a8ce4a9 | ||
|
|
06c5ddd80b | ||
|
|
8ccc170500 | ||
|
|
ff308d5292 | ||
|
|
af8502b0b0 | ||
|
|
c683936b1c | ||
|
|
8b3f1aab1a | ||
|
|
b5eadcdbee | ||
|
|
8ca8fd27ab | ||
|
|
ab014cf912 | ||
|
|
3f67b05fab | ||
|
|
cd271c1f87 | ||
|
|
755de5ae67 | ||
|
|
40402e7dbd | ||
|
|
0a2fb35ae2 | ||
|
|
b16954d8c1 | ||
|
|
29370b18d7 | ||
|
|
c9733d405c | ||
|
|
ec6004dd27 | ||
|
|
ea9a6de8c3 | ||
|
|
5e40a700ae | ||
|
|
48b220453b | ||
|
|
489a800868 | ||
|
|
60c9f21e1c | ||
|
|
ab73f21017 | ||
|
|
806563298b | ||
|
|
02fabbb35f | ||
|
|
07154d1f49 | ||
|
|
db30c0962a | ||
|
|
731fe43d14 | ||
|
|
5bd20aa559 | ||
|
|
a2e4f944e9 | ||
|
|
7955aec639 | ||
|
|
e12feb9f57 | ||
|
|
03b4144cff | ||
|
|
c382635e7f | ||
|
|
e381806ba0 | ||
|
|
52114b37f8 | ||
|
|
b4e68c02f9 | ||
|
|
c241ccf19d | ||
|
|
e4fa1e6390 | ||
|
|
b103b9d7cb | ||
|
|
90b02a90e0 | ||
|
|
38f718b965 | ||
|
|
8752a763c2 |
112
.github/workflows/helm-docs.yaml
vendored
112
.github/workflows/helm-docs.yaml
vendored
@@ -6,11 +6,13 @@ on:
|
||||
- "*"
|
||||
paths:
|
||||
- 'helm/**'
|
||||
- '!helm/higress/README.zh.md'
|
||||
workflow_dispatch: ~
|
||||
push:
|
||||
branches: [ main ]
|
||||
paths:
|
||||
- 'helm/**'
|
||||
- '!helm/higress/README.zh.md'
|
||||
|
||||
jobs:
|
||||
helm:
|
||||
@@ -31,7 +33,7 @@ jobs:
|
||||
run: |
|
||||
GOBIN=$PWD GO111MODULE=on go install github.com/norwoodj/helm-docs/cmd/helm-docs@v1.14.2
|
||||
./helm-docs -c ${GITHUB_WORKSPACE}/helm/higress -f ../core/values.yaml
|
||||
DIFF=$(git diff ${GITHUB_WORKSPACE}/helm/higress/*md)
|
||||
DIFF=$(git diff ${GITHUB_WORKSPACE}/helm/higress/README.md)
|
||||
if [ ! -z "$DIFF" ]; then
|
||||
echo "Please use helm-docs in your clone, of your fork, of the project, and commit a updated README.md for the chart."
|
||||
fi
|
||||
@@ -55,18 +57,19 @@ jobs:
|
||||
id: compare_readme
|
||||
run: |
|
||||
cd ./helm/higress
|
||||
BASE_BRANCH=main
|
||||
UPSTREAM_REPO=https://github.com/alibaba/higress.git
|
||||
|
||||
TEMP_DIR=$(mktemp -d)
|
||||
git clone --depth 1 --branch $BASE_BRANCH $UPSTREAM_REPO $TEMP_DIR
|
||||
|
||||
if diff -q "$TEMP_DIR/README.md" README.md > /dev/null; then
|
||||
echo "README.md has no changes in comparison to base branch. Skipping translation."
|
||||
|
||||
BASE_BRANCH=${GITHUB_BASE_REF:-main}
|
||||
git fetch origin $BASE_BRANCH
|
||||
|
||||
if git diff --quiet origin/$BASE_BRANCH -- README.md; then
|
||||
echo "README.md has no local changes compared to $BASE_BRANCH. Skipping translation."
|
||||
echo "skip_translation=true" >> $GITHUB_ENV
|
||||
else
|
||||
echo "README.md has changed in comparison to base branch. Proceeding with translation."
|
||||
echo "README.md has local changes compared to $BASE_BRANCH. Proceeding with translation."
|
||||
echo "skip_translation=false" >> $GITHUB_ENV
|
||||
echo "--------- diff ---------"
|
||||
git diff origin/$BASE_BRANCH -- README.md
|
||||
echo "------------------------"
|
||||
fi
|
||||
|
||||
- name: Translate README.md to Chinese
|
||||
@@ -76,39 +79,74 @@ jobs:
|
||||
API_KEY: ${{ secrets.HIGRESS_OPENAI_API_KEY }}
|
||||
API_MODEL: ${{ secrets.HIGRESS_OPENAI_API_MODEL }}
|
||||
run: |
|
||||
cd ./helm/higress
|
||||
FILE_CONTENT=$(cat README.md)
|
||||
cat << 'EOF' > translate_readme.py
|
||||
import os
|
||||
import json
|
||||
import requests
|
||||
|
||||
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
|
||||
}')
|
||||
API_URL = os.environ["API_URL"]
|
||||
API_KEY = os.environ["API_KEY"]
|
||||
API_MODEL = os.environ["API_MODEL"]
|
||||
README_PATH = "./helm/higress/README.md"
|
||||
OUTPUT_PATH = "./helm/higress/README.zh.md"
|
||||
|
||||
RESPONSE=$(curl -s -X POST "$API_URL" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $API_KEY" \
|
||||
-d "$PAYLOAD")
|
||||
def stream_translation(api_url, api_key, payload):
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
}
|
||||
response = requests.post(api_url, headers=headers, json=payload, stream=True)
|
||||
response.raise_for_status()
|
||||
|
||||
echo "Response: $RESPONSE"
|
||||
with open(OUTPUT_PATH, "w", encoding="utf-8") as out_file:
|
||||
for line in response.iter_lines(decode_unicode=True):
|
||||
if line.strip() == "" or not line.startswith("data: "):
|
||||
continue
|
||||
data = line[6:]
|
||||
if data.strip() == "[DONE]":
|
||||
break
|
||||
try:
|
||||
chunk = json.loads(data)
|
||||
content = chunk["choices"][0]["delta"].get("content", "")
|
||||
if content:
|
||||
out_file.write(content)
|
||||
except Exception as e:
|
||||
print("Error parsing chunk:", e)
|
||||
|
||||
echo "$RESPONSE" | jq -c -r '.choices[] | .message.content' > README.zh.new.md
|
||||
def main():
|
||||
if not os.path.exists(README_PATH):
|
||||
print("README.md not found!")
|
||||
return
|
||||
|
||||
if [ -f "README.zh.new.md" ]; then
|
||||
echo "Translation completed and saved to README.zh.new.md."
|
||||
else
|
||||
echo "Translation failed or no content returned!"
|
||||
exit 1
|
||||
fi
|
||||
with open(README_PATH, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
mv README.zh.new.md README.zh.md
|
||||
payload = {
|
||||
"model": API_MODEL,
|
||||
"messages": [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "You are a translation assistant that translates English Markdown text to Chinese. Preserve original Markdown formatting and line breaks."
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": content
|
||||
}
|
||||
],
|
||||
"temperature": 0.3,
|
||||
"stream": True
|
||||
}
|
||||
|
||||
print("Streaming translation started...")
|
||||
stream_translation(API_URL, API_KEY, payload)
|
||||
print(f"Translation completed and saved to {OUTPUT_PATH}.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
EOF
|
||||
|
||||
python3 translate_readme.py
|
||||
rm -rf translate_readme.py
|
||||
|
||||
- name: Create Pull Request
|
||||
if: env.skip_translation == 'false'
|
||||
|
||||
2
.github/workflows/release-crd.yaml
vendored
2
.github/workflows/release-crd.yaml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
cat helm/core/crds/customresourcedefinitions.gen.yaml helm/core/crds/istio-envoyfilter.yaml > crd.yaml
|
||||
|
||||
- name: Upload hgctl packages to the GitHub release
|
||||
uses: softprops/action-gh-release@v2
|
||||
uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
files: |
|
||||
|
||||
6
.github/workflows/release-hgctl.yaml
vendored
6
.github/workflows/release-hgctl.yaml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
zip -q -r hgctl_${{ env.HGCTL_VERSION }}_windows_arm64.zip out/windows_arm64/
|
||||
|
||||
- name: Upload hgctl packages to the GitHub release
|
||||
uses: softprops/action-gh-release@v2
|
||||
uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
files: |
|
||||
@@ -51,7 +51,7 @@ jobs:
|
||||
tar -zcvf hgctl_${{ env.HGCTL_VERSION }}_darwin_arm64.tar.gz out/darwin_arm64/
|
||||
|
||||
- name: Upload hgctl packages to the GitHub release
|
||||
uses: softprops/action-gh-release@v2
|
||||
uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
files: |
|
||||
@@ -73,7 +73,7 @@ jobs:
|
||||
tar -zcvf hgctl_${{ env.HGCTL_VERSION }}_darwin_amd64.tar.gz out/darwin_amd64/
|
||||
|
||||
- name: Upload hgctl packages to the GitHub release
|
||||
uses: softprops/action-gh-release@v2
|
||||
uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
files: |
|
||||
|
||||
36
.github/workflows/sync-crds.yaml
vendored
Normal file
36
.github/workflows/sync-crds.yaml
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
name: "Sync CRDs to Helm Chart"
|
||||
|
||||
on:
|
||||
workflow_dispatch: ~
|
||||
push:
|
||||
branches: [ main ]
|
||||
paths:
|
||||
- 'api/kubernetes/customresourcedefinitions.gen.yaml'
|
||||
|
||||
jobs:
|
||||
sync-crds:
|
||||
name: Sync CRDs
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Copy the CRD YAML File to Helm Folder
|
||||
run: |
|
||||
cp api/kubernetes/customresourcedefinitions.gen.yaml helm/core/crds/customresourcedefinitions.gen.yaml
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
commit-message: "Update CRD file in the helm folder"
|
||||
branch: sync-crds
|
||||
title: "Update CRD file in the helm folder"
|
||||
body: |
|
||||
This PR updates CRD file in the helm folder.
|
||||
|
||||
- Automatically copied by GitHub Actions
|
||||
labels: crds, automated
|
||||
base: main
|
||||
29
.github/workflows/translate-test.yml
vendored
Normal file
29
.github/workflows/translate-test.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
name: 'Translate GitHub content into English'
|
||||
on:
|
||||
issues:
|
||||
types: [opened, edited]
|
||||
issue_comment:
|
||||
types: [created, edited]
|
||||
discussion:
|
||||
types: [created, edited]
|
||||
discussion_comment:
|
||||
types: [created, edited]
|
||||
pull_request_target:
|
||||
types: [opened, edited]
|
||||
pull_request_review_comment:
|
||||
types: [created, edited]
|
||||
|
||||
jobs:
|
||||
translate:
|
||||
permissions:
|
||||
issues: write
|
||||
discussions: write
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: lizheming/github-translate-action@main
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
APPEND_TRANSLATION: true
|
||||
@@ -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.1.4/envoy-symbol-ARCH.tar.gz
|
||||
export ENVOY_PACKAGE_URL_PATTERN?=https://github.com/higress-group/proxy/releases/download/v2.1.6/envoy-symbol-ARCH.tar.gz
|
||||
|
||||
build-envoy: prebuild
|
||||
./tools/hack/build-envoy.sh
|
||||
@@ -191,6 +191,7 @@ install: pre-install
|
||||
cd helm/higress; helm dependency build
|
||||
helm install higress helm/higress -n higress-system --create-namespace --set 'global.local=true'
|
||||
|
||||
HIGRESS_LATEST_IMAGE_TAG ?= latest
|
||||
ENVOY_LATEST_IMAGE_TAG ?= 958467a353d411ae3f06e03b096bfd342cddb2c6
|
||||
ISTIO_LATEST_IMAGE_TAG ?= d9c728d3b01f64855e012b08d136e306f1160397
|
||||
|
||||
@@ -268,10 +269,26 @@ higress-conformance-test-clean: $(tools/kind) delete-cluster
|
||||
.PHONY: higress-wasmplugin-test-prepare
|
||||
higress-wasmplugin-test-prepare: $(tools/kind) delete-cluster create-cluster docker-build kube-load-image install-dev-wasmplugin
|
||||
|
||||
# higress-wasmplugin-test-prepare-skip-docker-build prepares the environment for higress wasmplugin tests without build higress docker image.
|
||||
.PHONY: higress-wasmplugin-test-prepare-skip-docker-build
|
||||
higress-wasmplugin-test-prepare-skip-docker-build: $(tools/kind) delete-cluster create-cluster prebuild
|
||||
@export TAG="$(HIGRESS_LATEST_IMAGE_TAG)" && \
|
||||
$(MAKE) kube-load-image && \
|
||||
$(MAKE) install-dev-wasmplugin
|
||||
|
||||
# higress-wasmplugin-test runs ingress wasmplugin tests.
|
||||
.PHONY: higress-wasmplugin-test
|
||||
higress-wasmplugin-test: $(tools/kind) delete-cluster create-cluster docker-build kube-load-image install-dev-wasmplugin run-higress-e2e-test-wasmplugin delete-cluster
|
||||
|
||||
# higress-wasmplugin-test-skip-docker-build runs ingress wasmplugin tests without build higress docker image
|
||||
.PHONY: higress-wasmplugin-test-skip-docker-build
|
||||
higress-wasmplugin-test-skip-docker-build: $(tools/kind) delete-cluster create-cluster prebuild
|
||||
@export TAG="$(HIGRESS_LATEST_IMAGE_TAG)" && \
|
||||
$(MAKE) kube-load-image && \
|
||||
$(MAKE) install-dev-wasmplugin && \
|
||||
$(MAKE) run-higress-e2e-test-wasmplugin && \
|
||||
$(MAKE) delete-cluster
|
||||
|
||||
# higress-wasmplugin-test-clean cleans the environment for higress wasmplugin tests.
|
||||
.PHONY: higress-wasmplugin-test-clean
|
||||
higress-wasmplugin-test-clean: $(tools/kind) delete-cluster
|
||||
@@ -290,8 +307,12 @@ delete-cluster: $(tools/kind) ## Delete kind cluster.
|
||||
# dubbo-provider-demo和nacos-standlone-rc3的镜像已经上传到阿里云镜像库,第一次需要先拉到本地
|
||||
# docker pull registry.cn-hangzhou.aliyuncs.com/hinsteny/dubbo-provider-demo:0.0.1
|
||||
# docker pull registry.cn-hangzhou.aliyuncs.com/hinsteny/nacos-standlone-rc3:1.0.0-RC3
|
||||
# If TAG is HIGRESS_LATEST_IMAGE_TAG, means we skip building higress docker image, so we need to pull the image first.
|
||||
.PHONY: kube-load-image
|
||||
kube-load-image: $(tools/kind) ## Install the Higress image to a kind cluster using the provided $IMAGE and $TAG.
|
||||
@if [ "$(TAG)" = "$(HIGRESS_LATEST_IMAGE_TAG)" ]; then \
|
||||
tools/hack/docker-pull-image.sh higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/higress $(TAG); \
|
||||
fi
|
||||
tools/hack/kind-load-image.sh higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/higress $(TAG)
|
||||
tools/hack/docker-pull-image.sh higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/pilot $(ISTIO_LATEST_IMAGE_TAG)
|
||||
tools/hack/docker-pull-image.sh higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/gateway $(ENVOY_LATEST_IMAGE_TAG)
|
||||
|
||||
13
README.md
13
README.md
@@ -10,8 +10,10 @@
|
||||
|
||||
[](https://github.com/alibaba/higress/actions)
|
||||
[](https://www.apache.org/licenses/LICENSE-2.0.html)
|
||||
[](https://discord.gg/tSbww9VDaM)
|
||||
|
||||
<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> <a href="https://www.producthunt.com/posts/higress?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-higress" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=951287&theme=light&t=1745492822283" alt="Higress - Global APIs as MCP powered by AI Gateway | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||
|
||||
<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>
|
||||
|
||||
[**Official Site**](https://higress.ai/en/) |
|
||||
@@ -67,6 +69,10 @@ Port descriptions:
|
||||
|
||||
> All Higress Docker images use Higress's own image repository and are not affected by Docker Hub rate limits.
|
||||
> In addition, the submission and updates of the images are protected by a security scanning mechanism (powered by Alibaba Cloud ACR), making them very secure for use in production environments.
|
||||
>
|
||||
> If you experience a timeout when pulling image from `higress-registry.cn-hangzhou.cr.aliyuncs.com`, you can try replacing it with the following docker registry mirror source:
|
||||
>
|
||||
> **Southeast Asia**: `higress-registry.ap-southeast-7.cr.aliyuncs.com`
|
||||
|
||||
For other installation methods such as Helm deployment under K8s, please refer to the official [Quick Start documentation](https://higress.io/en-us/docs/user/quickstart).
|
||||
|
||||
@@ -139,7 +145,10 @@ For other installation methods such as Helm deployment under K8s, please refer t
|
||||
|
||||
## Community
|
||||
|
||||
[Slack](https://w1689142780-euk177225.slack.com/archives/C05GEL4TGTG): to get invited go [here](https://communityinviter.com/apps/w1689142780-euk177225/higress).
|
||||
Join our Discord community! This is where you can connect with developers and other enthusiastic users of Higress.
|
||||
|
||||
[](https://discord.gg/tSbww9VDaM)
|
||||
|
||||
|
||||
### Thanks
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
[](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>
|
||||
<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> <a href="https://www.producthunt.com/posts/higress?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-higress" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=951287&theme=light&t=1745492822283" alt="Higress - Global APIs as MCP powered by AI Gateway | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||
</div>
|
||||
|
||||
[**官网**](https://higress.cn/) |
|
||||
|
||||
@@ -250,6 +250,10 @@ spec:
|
||||
registries:
|
||||
items:
|
||||
properties:
|
||||
allowMcpServers:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
authSecretName:
|
||||
type: string
|
||||
consulDatacenter:
|
||||
@@ -263,6 +267,25 @@ spec:
|
||||
type: string
|
||||
domain:
|
||||
type: string
|
||||
enableMCPServer:
|
||||
type: boolean
|
||||
enableScopeMcpServers:
|
||||
type: boolean
|
||||
mcpServerBaseUrl:
|
||||
type: string
|
||||
mcpServerExportDomains:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
metadata:
|
||||
additionalProperties:
|
||||
properties:
|
||||
innerMap:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
type: object
|
||||
type: object
|
||||
nacosAccessKey:
|
||||
type: string
|
||||
nacosAddressServer:
|
||||
|
||||
@@ -26,6 +26,8 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
_ "github.com/golang/protobuf/ptypes/struct"
|
||||
wrappers "github.com/golang/protobuf/ptypes/wrappers"
|
||||
_ "google.golang.org/genproto/googleapis/api/annotations"
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
@@ -109,25 +111,31 @@ type RegistryConfig struct {
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"`
|
||||
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
|
||||
Domain string `protobuf:"bytes,3,opt,name=domain,proto3" json:"domain,omitempty"`
|
||||
Port uint32 `protobuf:"varint,4,opt,name=port,proto3" json:"port,omitempty"`
|
||||
NacosAddressServer string `protobuf:"bytes,5,opt,name=nacosAddressServer,proto3" json:"nacosAddressServer,omitempty"`
|
||||
NacosAccessKey string `protobuf:"bytes,6,opt,name=nacosAccessKey,proto3" json:"nacosAccessKey,omitempty"`
|
||||
NacosSecretKey string `protobuf:"bytes,7,opt,name=nacosSecretKey,proto3" json:"nacosSecretKey,omitempty"`
|
||||
NacosNamespaceId string `protobuf:"bytes,8,opt,name=nacosNamespaceId,proto3" json:"nacosNamespaceId,omitempty"`
|
||||
NacosNamespace string `protobuf:"bytes,9,opt,name=nacosNamespace,proto3" json:"nacosNamespace,omitempty"`
|
||||
NacosGroups []string `protobuf:"bytes,10,rep,name=nacosGroups,proto3" json:"nacosGroups,omitempty"`
|
||||
NacosRefreshInterval int64 `protobuf:"varint,11,opt,name=nacosRefreshInterval,proto3" json:"nacosRefreshInterval,omitempty"`
|
||||
ConsulNamespace string `protobuf:"bytes,12,opt,name=consulNamespace,proto3" json:"consulNamespace,omitempty"`
|
||||
ZkServicesPath []string `protobuf:"bytes,13,rep,name=zkServicesPath,proto3" json:"zkServicesPath,omitempty"`
|
||||
ConsulDatacenter string `protobuf:"bytes,14,opt,name=consulDatacenter,proto3" json:"consulDatacenter,omitempty"`
|
||||
ConsulServiceTag string `protobuf:"bytes,15,opt,name=consulServiceTag,proto3" json:"consulServiceTag,omitempty"`
|
||||
ConsulRefreshInterval int64 `protobuf:"varint,16,opt,name=consulRefreshInterval,proto3" json:"consulRefreshInterval,omitempty"`
|
||||
AuthSecretName string `protobuf:"bytes,17,opt,name=authSecretName,proto3" json:"authSecretName,omitempty"`
|
||||
Protocol string `protobuf:"bytes,18,opt,name=protocol,proto3" json:"protocol,omitempty"`
|
||||
Sni string `protobuf:"bytes,19,opt,name=sni,proto3" json:"sni,omitempty"`
|
||||
Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"`
|
||||
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
|
||||
Domain string `protobuf:"bytes,3,opt,name=domain,proto3" json:"domain,omitempty"`
|
||||
Port uint32 `protobuf:"varint,4,opt,name=port,proto3" json:"port,omitempty"`
|
||||
NacosAddressServer string `protobuf:"bytes,5,opt,name=nacosAddressServer,proto3" json:"nacosAddressServer,omitempty"`
|
||||
NacosAccessKey string `protobuf:"bytes,6,opt,name=nacosAccessKey,proto3" json:"nacosAccessKey,omitempty"`
|
||||
NacosSecretKey string `protobuf:"bytes,7,opt,name=nacosSecretKey,proto3" json:"nacosSecretKey,omitempty"`
|
||||
NacosNamespaceId string `protobuf:"bytes,8,opt,name=nacosNamespaceId,proto3" json:"nacosNamespaceId,omitempty"`
|
||||
NacosNamespace string `protobuf:"bytes,9,opt,name=nacosNamespace,proto3" json:"nacosNamespace,omitempty"`
|
||||
NacosGroups []string `protobuf:"bytes,10,rep,name=nacosGroups,proto3" json:"nacosGroups,omitempty"`
|
||||
NacosRefreshInterval int64 `protobuf:"varint,11,opt,name=nacosRefreshInterval,proto3" json:"nacosRefreshInterval,omitempty"`
|
||||
ConsulNamespace string `protobuf:"bytes,12,opt,name=consulNamespace,proto3" json:"consulNamespace,omitempty"`
|
||||
ZkServicesPath []string `protobuf:"bytes,13,rep,name=zkServicesPath,proto3" json:"zkServicesPath,omitempty"`
|
||||
ConsulDatacenter string `protobuf:"bytes,14,opt,name=consulDatacenter,proto3" json:"consulDatacenter,omitempty"`
|
||||
ConsulServiceTag string `protobuf:"bytes,15,opt,name=consulServiceTag,proto3" json:"consulServiceTag,omitempty"`
|
||||
ConsulRefreshInterval int64 `protobuf:"varint,16,opt,name=consulRefreshInterval,proto3" json:"consulRefreshInterval,omitempty"`
|
||||
AuthSecretName string `protobuf:"bytes,17,opt,name=authSecretName,proto3" json:"authSecretName,omitempty"`
|
||||
Protocol string `protobuf:"bytes,18,opt,name=protocol,proto3" json:"protocol,omitempty"`
|
||||
Sni string `protobuf:"bytes,19,opt,name=sni,proto3" json:"sni,omitempty"`
|
||||
McpServerExportDomains []string `protobuf:"bytes,20,rep,name=mcpServerExportDomains,proto3" json:"mcpServerExportDomains,omitempty"`
|
||||
McpServerBaseUrl string `protobuf:"bytes,21,opt,name=mcpServerBaseUrl,proto3" json:"mcpServerBaseUrl,omitempty"`
|
||||
EnableMCPServer *wrappers.BoolValue `protobuf:"bytes,22,opt,name=enableMCPServer,proto3" json:"enableMCPServer,omitempty"`
|
||||
EnableScopeMcpServers *wrappers.BoolValue `protobuf:"bytes,23,opt,name=enableScopeMcpServers,proto3" json:"enableScopeMcpServers,omitempty"`
|
||||
AllowMcpServers []string `protobuf:"bytes,24,rep,name=allowMcpServers,proto3" json:"allowMcpServers,omitempty"`
|
||||
Metadata map[string]*InnerMap `protobuf:"bytes,25,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
|
||||
}
|
||||
|
||||
func (x *RegistryConfig) Reset() {
|
||||
@@ -295,6 +303,95 @@ func (x *RegistryConfig) GetSni() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *RegistryConfig) GetMcpServerExportDomains() []string {
|
||||
if x != nil {
|
||||
return x.McpServerExportDomains
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *RegistryConfig) GetMcpServerBaseUrl() string {
|
||||
if x != nil {
|
||||
return x.McpServerBaseUrl
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *RegistryConfig) GetEnableMCPServer() *wrappers.BoolValue {
|
||||
if x != nil {
|
||||
return x.EnableMCPServer
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *RegistryConfig) GetEnableScopeMcpServers() *wrappers.BoolValue {
|
||||
if x != nil {
|
||||
return x.EnableScopeMcpServers
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *RegistryConfig) GetAllowMcpServers() []string {
|
||||
if x != nil {
|
||||
return x.AllowMcpServers
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *RegistryConfig) GetMetadata() map[string]*InnerMap {
|
||||
if x != nil {
|
||||
return x.Metadata
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type InnerMap struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
InnerMap map[string]string `protobuf:"bytes,1,rep,name=inner_map,json=innerMap,proto3" json:"inner_map,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
|
||||
}
|
||||
|
||||
func (x *InnerMap) Reset() {
|
||||
*x = InnerMap{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_networking_v1_mcp_bridge_proto_msgTypes[2]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *InnerMap) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*InnerMap) ProtoMessage() {}
|
||||
|
||||
func (x *InnerMap) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_networking_v1_mcp_bridge_proto_msgTypes[2]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use InnerMap.ProtoReflect.Descriptor instead.
|
||||
func (*InnerMap) Descriptor() ([]byte, []int) {
|
||||
return file_networking_v1_mcp_bridge_proto_rawDescGZIP(), []int{2}
|
||||
}
|
||||
|
||||
func (x *InnerMap) GetInnerMap() map[string]string {
|
||||
if x != nil {
|
||||
return x.InnerMap
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var File_networking_v1_mcp_bridge_proto protoreflect.FileDescriptor
|
||||
|
||||
var file_networking_v1_mcp_bridge_proto_rawDesc = []byte{
|
||||
@@ -303,61 +400,104 @@ var file_networking_v1_mcp_bridge_proto_rawDesc = []byte{
|
||||
0x12, 0x15, 0x68, 0x69, 0x67, 0x72, 0x65, 0x73, 0x73, 0x2e, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72,
|
||||
0x6b, 0x69, 0x6e, 0x67, 0x2e, 0x76, 0x31, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f,
|
||||
0x61, 0x70, 0x69, 0x2f, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x5f, 0x62, 0x65, 0x68, 0x61, 0x76, 0x69,
|
||||
0x6f, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x52, 0x0a, 0x09, 0x4d, 0x63, 0x70, 0x42,
|
||||
0x72, 0x69, 0x64, 0x67, 0x65, 0x12, 0x45, 0x0a, 0x0a, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72,
|
||||
0x69, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x68, 0x69, 0x67, 0x72,
|
||||
0x6f, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65,
|
||||
0x2f, 0x70, 0x72, 0x6f, 0x74, 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, 0x52, 0x0a, 0x09, 0x4d, 0x63, 0x70, 0x42, 0x72, 0x69,
|
||||
0x64, 0x67, 0x65, 0x12, 0x45, 0x0a, 0x0a, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x69, 0x65,
|
||||
0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x68, 0x69, 0x67, 0x72, 0x65, 0x73,
|
||||
0x73, 0x2e, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x69, 0x6e, 0x67, 0x2e, 0x76, 0x31, 0x2e,
|
||||
0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0a,
|
||||
0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x69, 0x65, 0x73, 0x22, 0xa8, 0x09, 0x0a, 0x0e, 0x52,
|
||||
0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x17, 0x0a,
|
||||
0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x03, 0xe0, 0x41, 0x02,
|
||||
0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02,
|
||||
0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1b, 0x0a, 0x06, 0x64, 0x6f,
|
||||
0x6d, 0x61, 0x69, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x03, 0xe0, 0x41, 0x02, 0x52,
|
||||
0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x17, 0x0a, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x18,
|
||||
0x04, 0x20, 0x01, 0x28, 0x0d, 0x42, 0x03, 0xe0, 0x41, 0x02, 0x52, 0x04, 0x70, 0x6f, 0x72, 0x74,
|
||||
0x12, 0x2e, 0x0a, 0x12, 0x6e, 0x61, 0x63, 0x6f, 0x73, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73,
|
||||
0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x6e, 0x61,
|
||||
0x63, 0x6f, 0x73, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72,
|
||||
0x12, 0x26, 0x0a, 0x0e, 0x6e, 0x61, 0x63, 0x6f, 0x73, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4b,
|
||||
0x65, 0x79, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x6e, 0x61, 0x63, 0x6f, 0x73, 0x41,
|
||||
0x63, 0x63, 0x65, 0x73, 0x73, 0x4b, 0x65, 0x79, 0x12, 0x26, 0x0a, 0x0e, 0x6e, 0x61, 0x63, 0x6f,
|
||||
0x73, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09,
|
||||
0x52, 0x0e, 0x6e, 0x61, 0x63, 0x6f, 0x73, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x4b, 0x65, 0x79,
|
||||
0x12, 0x2a, 0x0a, 0x10, 0x6e, 0x61, 0x63, 0x6f, 0x73, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61,
|
||||
0x63, 0x65, 0x49, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x6e, 0x61, 0x63, 0x6f,
|
||||
0x73, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x49, 0x64, 0x12, 0x26, 0x0a, 0x0e,
|
||||
0x6e, 0x61, 0x63, 0x6f, 0x73, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x09,
|
||||
0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x6e, 0x61, 0x63, 0x6f, 0x73, 0x4e, 0x61, 0x6d, 0x65, 0x73,
|
||||
0x70, 0x61, 0x63, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x6e, 0x61, 0x63, 0x6f, 0x73, 0x47, 0x72, 0x6f,
|
||||
0x75, 0x70, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0b, 0x6e, 0x61, 0x63, 0x6f, 0x73,
|
||||
0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x12, 0x32, 0x0a, 0x14, 0x6e, 0x61, 0x63, 0x6f, 0x73, 0x52,
|
||||
0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x18, 0x0b,
|
||||
0x20, 0x01, 0x28, 0x03, 0x52, 0x14, 0x6e, 0x61, 0x63, 0x6f, 0x73, 0x52, 0x65, 0x66, 0x72, 0x65,
|
||||
0x73, 0x68, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x12, 0x28, 0x0a, 0x0f, 0x63, 0x6f,
|
||||
0x6e, 0x73, 0x75, 0x6c, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x0c, 0x20,
|
||||
0x01, 0x28, 0x09, 0x52, 0x0f, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x4e, 0x61, 0x6d, 0x65, 0x73,
|
||||
0x70, 0x61, 0x63, 0x65, 0x12, 0x26, 0x0a, 0x0e, 0x7a, 0x6b, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63,
|
||||
0x65, 0x73, 0x50, 0x61, 0x74, 0x68, 0x18, 0x0d, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0e, 0x7a, 0x6b,
|
||||
0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x50, 0x61, 0x74, 0x68, 0x12, 0x2a, 0x0a, 0x10,
|
||||
0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x44, 0x61, 0x74, 0x61, 0x63, 0x65, 0x6e, 0x74, 0x65, 0x72,
|
||||
0x18, 0x0e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x44, 0x61,
|
||||
0x74, 0x61, 0x63, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x12, 0x2a, 0x0a, 0x10, 0x63, 0x6f, 0x6e, 0x73,
|
||||
0x75, 0x6c, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x54, 0x61, 0x67, 0x18, 0x0f, 0x20, 0x01,
|
||||
0x28, 0x09, 0x52, 0x10, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63,
|
||||
0x65, 0x54, 0x61, 0x67, 0x12, 0x34, 0x0a, 0x15, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x52, 0x65,
|
||||
0x66, 0x72, 0x65, 0x73, 0x68, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x18, 0x10, 0x20,
|
||||
0x01, 0x28, 0x03, 0x52, 0x15, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x52, 0x65, 0x66, 0x72, 0x65,
|
||||
0x73, 0x68, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x12, 0x26, 0x0a, 0x0e, 0x61, 0x75,
|
||||
0x74, 0x68, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x11, 0x20, 0x01,
|
||||
0x28, 0x09, 0x52, 0x0e, 0x61, 0x75, 0x74, 0x68, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x4e, 0x61,
|
||||
0x6d, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x12,
|
||||
0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x10,
|
||||
0x0a, 0x03, 0x73, 0x6e, 0x69, 0x18, 0x13, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x73, 0x6e, 0x69,
|
||||
0x12, 0x36, 0x0a, 0x16, 0x6d, 0x63, 0x70, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x45, 0x78, 0x70,
|
||||
0x6f, 0x72, 0x74, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x18, 0x14, 0x20, 0x03, 0x28, 0x09,
|
||||
0x52, 0x16, 0x6d, 0x63, 0x70, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x45, 0x78, 0x70, 0x6f, 0x72,
|
||||
0x74, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x2a, 0x0a, 0x10, 0x6d, 0x63, 0x70, 0x53,
|
||||
0x65, 0x72, 0x76, 0x65, 0x72, 0x42, 0x61, 0x73, 0x65, 0x55, 0x72, 0x6c, 0x18, 0x15, 0x20, 0x01,
|
||||
0x28, 0x09, 0x52, 0x10, 0x6d, 0x63, 0x70, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x42, 0x61, 0x73,
|
||||
0x65, 0x55, 0x72, 0x6c, 0x12, 0x44, 0x0a, 0x0f, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x4d, 0x43,
|
||||
0x50, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x18, 0x16, 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, 0x0f, 0x65, 0x6e, 0x61, 0x62, 0x6c,
|
||||
0x65, 0x4d, 0x43, 0x50, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x50, 0x0a, 0x15, 0x65, 0x6e,
|
||||
0x61, 0x62, 0x6c, 0x65, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x4d, 0x63, 0x70, 0x53, 0x65, 0x72, 0x76,
|
||||
0x65, 0x72, 0x73, 0x18, 0x17, 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, 0x15, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x53, 0x63, 0x6f,
|
||||
0x70, 0x65, 0x4d, 0x63, 0x70, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x12, 0x28, 0x0a, 0x0f,
|
||||
0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x4d, 0x63, 0x70, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x18,
|
||||
0x18, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0f, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x4d, 0x63, 0x70, 0x53,
|
||||
0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x12, 0x4f, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61,
|
||||
0x74, 0x61, 0x18, 0x19, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x33, 0x2e, 0x68, 0x69, 0x67, 0x72, 0x65,
|
||||
0x73, 0x73, 0x2e, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x69, 0x6e, 0x67, 0x2e, 0x76, 0x31,
|
||||
0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e,
|
||||
0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x6d,
|
||||
0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x1a, 0x5c, 0x0a, 0x0d, 0x4d, 0x65, 0x74, 0x61, 0x64,
|
||||
0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18,
|
||||
0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x35, 0x0a, 0x05, 0x76, 0x61,
|
||||
0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x68, 0x69, 0x67, 0x72,
|
||||
0x65, 0x73, 0x73, 0x2e, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x69, 0x6e, 0x67, 0x2e, 0x76,
|
||||
0x31, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67,
|
||||
0x52, 0x0a, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x69, 0x65, 0x73, 0x22, 0xd3, 0x05, 0x0a,
|
||||
0x0e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12,
|
||||
0x17, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x03, 0xe0,
|
||||
0x41, 0x02, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65,
|
||||
0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1b, 0x0a, 0x06,
|
||||
0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x03, 0xe0, 0x41,
|
||||
0x02, 0x52, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x17, 0x0a, 0x04, 0x70, 0x6f, 0x72,
|
||||
0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x42, 0x03, 0xe0, 0x41, 0x02, 0x52, 0x04, 0x70, 0x6f,
|
||||
0x72, 0x74, 0x12, 0x2e, 0x0a, 0x12, 0x6e, 0x61, 0x63, 0x6f, 0x73, 0x41, 0x64, 0x64, 0x72, 0x65,
|
||||
0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12,
|
||||
0x6e, 0x61, 0x63, 0x6f, 0x73, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76,
|
||||
0x65, 0x72, 0x12, 0x26, 0x0a, 0x0e, 0x6e, 0x61, 0x63, 0x6f, 0x73, 0x41, 0x63, 0x63, 0x65, 0x73,
|
||||
0x73, 0x4b, 0x65, 0x79, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x6e, 0x61, 0x63, 0x6f,
|
||||
0x73, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4b, 0x65, 0x79, 0x12, 0x26, 0x0a, 0x0e, 0x6e, 0x61,
|
||||
0x63, 0x6f, 0x73, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x18, 0x07, 0x20, 0x01,
|
||||
0x28, 0x09, 0x52, 0x0e, 0x6e, 0x61, 0x63, 0x6f, 0x73, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x4b,
|
||||
0x65, 0x79, 0x12, 0x2a, 0x0a, 0x10, 0x6e, 0x61, 0x63, 0x6f, 0x73, 0x4e, 0x61, 0x6d, 0x65, 0x73,
|
||||
0x70, 0x61, 0x63, 0x65, 0x49, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x6e, 0x61,
|
||||
0x63, 0x6f, 0x73, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x49, 0x64, 0x12, 0x26,
|
||||
0x0a, 0x0e, 0x6e, 0x61, 0x63, 0x6f, 0x73, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65,
|
||||
0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x6e, 0x61, 0x63, 0x6f, 0x73, 0x4e, 0x61, 0x6d,
|
||||
0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x6e, 0x61, 0x63, 0x6f, 0x73, 0x47,
|
||||
0x72, 0x6f, 0x75, 0x70, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0b, 0x6e, 0x61, 0x63,
|
||||
0x6f, 0x73, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x12, 0x32, 0x0a, 0x14, 0x6e, 0x61, 0x63, 0x6f,
|
||||
0x73, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c,
|
||||
0x18, 0x0b, 0x20, 0x01, 0x28, 0x03, 0x52, 0x14, 0x6e, 0x61, 0x63, 0x6f, 0x73, 0x52, 0x65, 0x66,
|
||||
0x72, 0x65, 0x73, 0x68, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x12, 0x28, 0x0a, 0x0f,
|
||||
0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18,
|
||||
0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x4e, 0x61, 0x6d,
|
||||
0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x26, 0x0a, 0x0e, 0x7a, 0x6b, 0x53, 0x65, 0x72, 0x76,
|
||||
0x69, 0x63, 0x65, 0x73, 0x50, 0x61, 0x74, 0x68, 0x18, 0x0d, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0e,
|
||||
0x7a, 0x6b, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x50, 0x61, 0x74, 0x68, 0x12, 0x2a,
|
||||
0x0a, 0x10, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x44, 0x61, 0x74, 0x61, 0x63, 0x65, 0x6e, 0x74,
|
||||
0x65, 0x72, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c,
|
||||
0x44, 0x61, 0x74, 0x61, 0x63, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x12, 0x2a, 0x0a, 0x10, 0x63, 0x6f,
|
||||
0x6e, 0x73, 0x75, 0x6c, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x54, 0x61, 0x67, 0x18, 0x0f,
|
||||
0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x53, 0x65, 0x72, 0x76,
|
||||
0x69, 0x63, 0x65, 0x54, 0x61, 0x67, 0x12, 0x34, 0x0a, 0x15, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c,
|
||||
0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x18,
|
||||
0x10, 0x20, 0x01, 0x28, 0x03, 0x52, 0x15, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x52, 0x65, 0x66,
|
||||
0x72, 0x65, 0x73, 0x68, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x12, 0x26, 0x0a, 0x0e,
|
||||
0x61, 0x75, 0x74, 0x68, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x11,
|
||||
0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x61, 0x75, 0x74, 0x68, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74,
|
||||
0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c,
|
||||
0x18, 0x12, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c,
|
||||
0x12, 0x10, 0x0a, 0x03, 0x73, 0x6e, 0x69, 0x18, 0x13, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x73,
|
||||
0x6e, 0x69, 0x42, 0x2e, 0x5a, 0x2c, 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, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x69, 0x6e, 0x67, 0x2f,
|
||||
0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
0x31, 0x2e, 0x49, 0x6e, 0x6e, 0x65, 0x72, 0x4d, 0x61, 0x70, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75,
|
||||
0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x93, 0x01, 0x0a, 0x08, 0x49, 0x6e, 0x6e, 0x65, 0x72, 0x4d,
|
||||
0x61, 0x70, 0x12, 0x4a, 0x0a, 0x09, 0x69, 0x6e, 0x6e, 0x65, 0x72, 0x5f, 0x6d, 0x61, 0x70, 0x18,
|
||||
0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x68, 0x69, 0x67, 0x72, 0x65, 0x73, 0x73, 0x2e,
|
||||
0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x69, 0x6e, 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x49, 0x6e,
|
||||
0x6e, 0x65, 0x72, 0x4d, 0x61, 0x70, 0x2e, 0x49, 0x6e, 0x6e, 0x65, 0x72, 0x4d, 0x61, 0x70, 0x45,
|
||||
0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x69, 0x6e, 0x6e, 0x65, 0x72, 0x4d, 0x61, 0x70, 0x1a, 0x3b,
|
||||
0x0a, 0x0d, 0x49, 0x6e, 0x6e, 0x65, 0x72, 0x4d, 0x61, 0x70, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12,
|
||||
0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65,
|
||||
0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09,
|
||||
0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x2e, 0x5a, 0x2c, 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, 0x6e, 0x65,
|
||||
0x74, 0x77, 0x6f, 0x72, 0x6b, 0x69, 0x6e, 0x67, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f,
|
||||
0x74, 0x6f, 0x33,
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -372,18 +512,27 @@ func file_networking_v1_mcp_bridge_proto_rawDescGZIP() []byte {
|
||||
return file_networking_v1_mcp_bridge_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_networking_v1_mcp_bridge_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
|
||||
var file_networking_v1_mcp_bridge_proto_msgTypes = make([]protoimpl.MessageInfo, 5)
|
||||
var file_networking_v1_mcp_bridge_proto_goTypes = []interface{}{
|
||||
(*McpBridge)(nil), // 0: higress.networking.v1.McpBridge
|
||||
(*RegistryConfig)(nil), // 1: higress.networking.v1.RegistryConfig
|
||||
(*McpBridge)(nil), // 0: higress.networking.v1.McpBridge
|
||||
(*RegistryConfig)(nil), // 1: higress.networking.v1.RegistryConfig
|
||||
(*InnerMap)(nil), // 2: higress.networking.v1.InnerMap
|
||||
nil, // 3: higress.networking.v1.RegistryConfig.MetadataEntry
|
||||
nil, // 4: higress.networking.v1.InnerMap.InnerMapEntry
|
||||
(*wrappers.BoolValue)(nil), // 5: google.protobuf.BoolValue
|
||||
}
|
||||
var file_networking_v1_mcp_bridge_proto_depIdxs = []int32{
|
||||
1, // 0: higress.networking.v1.McpBridge.registries:type_name -> higress.networking.v1.RegistryConfig
|
||||
1, // [1:1] is the sub-list for method output_type
|
||||
1, // [1:1] is the sub-list for method input_type
|
||||
1, // [1:1] is the sub-list for extension type_name
|
||||
1, // [1:1] is the sub-list for extension extendee
|
||||
0, // [0:1] is the sub-list for field type_name
|
||||
5, // 1: higress.networking.v1.RegistryConfig.enableMCPServer:type_name -> google.protobuf.BoolValue
|
||||
5, // 2: higress.networking.v1.RegistryConfig.enableScopeMcpServers:type_name -> google.protobuf.BoolValue
|
||||
3, // 3: higress.networking.v1.RegistryConfig.metadata:type_name -> higress.networking.v1.RegistryConfig.MetadataEntry
|
||||
4, // 4: higress.networking.v1.InnerMap.inner_map:type_name -> higress.networking.v1.InnerMap.InnerMapEntry
|
||||
2, // 5: higress.networking.v1.RegistryConfig.MetadataEntry.value:type_name -> higress.networking.v1.InnerMap
|
||||
6, // [6:6] is the sub-list for method output_type
|
||||
6, // [6:6] is the sub-list for method input_type
|
||||
6, // [6:6] is the sub-list for extension type_name
|
||||
6, // [6:6] is the sub-list for extension extendee
|
||||
0, // [0:6] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_networking_v1_mcp_bridge_proto_init() }
|
||||
@@ -416,6 +565,18 @@ func file_networking_v1_mcp_bridge_proto_init() {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_networking_v1_mcp_bridge_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*InnerMap); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
@@ -423,7 +584,7 @@ func file_networking_v1_mcp_bridge_proto_init() {
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: file_networking_v1_mcp_bridge_proto_rawDesc,
|
||||
NumEnums: 0,
|
||||
NumMessages: 2,
|
||||
NumMessages: 5,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
syntax = "proto3";
|
||||
|
||||
import "google/api/field_behavior.proto";
|
||||
import "google/protobuf/wrappers.proto";
|
||||
import "google/protobuf/struct.proto";
|
||||
|
||||
// $schema: higress.networking.v1.McpBridge
|
||||
// $title: McpBridge
|
||||
@@ -66,4 +68,14 @@ message RegistryConfig {
|
||||
string authSecretName = 17;
|
||||
string protocol = 18;
|
||||
string sni = 19;
|
||||
repeated string mcpServerExportDomains = 20;
|
||||
string mcpServerBaseUrl = 21;
|
||||
google.protobuf.BoolValue enableMCPServer = 22;
|
||||
google.protobuf.BoolValue enableScopeMcpServers = 23;
|
||||
repeated string allowMcpServers = 24;
|
||||
map<string, InnerMap> metadata = 25;
|
||||
}
|
||||
|
||||
message InnerMap {
|
||||
map<string, string> inner_map = 1;
|
||||
}
|
||||
@@ -46,3 +46,24 @@ func (in *RegistryConfig) DeepCopy() *RegistryConfig {
|
||||
func (in *RegistryConfig) DeepCopyInterface() interface{} {
|
||||
return in.DeepCopy()
|
||||
}
|
||||
|
||||
// DeepCopyInto supports using InnerMap within kubernetes types, where deepcopy-gen is used.
|
||||
func (in *InnerMap) DeepCopyInto(out *InnerMap) {
|
||||
p := proto.Clone(in).(*InnerMap)
|
||||
*out = *p
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InnerMap. Required by controller-gen.
|
||||
func (in *InnerMap) DeepCopy() *InnerMap {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(InnerMap)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInterface is an autogenerated deepcopy function, copying the receiver, creating a new InnerMap. Required by controller-gen.
|
||||
func (in *InnerMap) DeepCopyInterface() interface{} {
|
||||
return in.DeepCopy()
|
||||
}
|
||||
|
||||
@@ -28,6 +28,17 @@ func (this *RegistryConfig) UnmarshalJSON(b []byte) error {
|
||||
return McpBridgeUnmarshaler.Unmarshal(bytes.NewReader(b), this)
|
||||
}
|
||||
|
||||
// MarshalJSON is a custom marshaler for InnerMap
|
||||
func (this *InnerMap) MarshalJSON() ([]byte, error) {
|
||||
str, err := McpBridgeMarshaler.MarshalToString(this)
|
||||
return []byte(str), err
|
||||
}
|
||||
|
||||
// UnmarshalJSON is a custom unmarshaler for InnerMap
|
||||
func (this *InnerMap) UnmarshalJSON(b []byte) error {
|
||||
return McpBridgeUnmarshaler.Unmarshal(bytes.NewReader(b), this)
|
||||
}
|
||||
|
||||
var (
|
||||
McpBridgeMarshaler = &jsonpb.Marshaler{}
|
||||
McpBridgeUnmarshaler = &jsonpb.Unmarshaler{AllowUnknownFields: true}
|
||||
|
||||
Submodule envoy/envoy updated: e114a74dd8...b37a2988a1
41
go.mod
41
go.mod
@@ -31,7 +31,7 @@ require (
|
||||
github.com/hudl/fargo v1.4.0
|
||||
github.com/mholt/acmez v1.2.0
|
||||
github.com/nacos-group/nacos-sdk-go v1.0.8
|
||||
github.com/nacos-group/nacos-sdk-go/v2 v2.1.2
|
||||
github.com/nacos-group/nacos-sdk-go/v2 v2.3.2
|
||||
github.com/onsi/gomega v1.27.10
|
||||
github.com/spf13/cobra v1.8.0
|
||||
github.com/spf13/pflag v1.0.5
|
||||
@@ -39,7 +39,7 @@ require (
|
||||
github.com/tidwall/gjson v1.17.0
|
||||
go.uber.org/atomic v1.11.0
|
||||
go.uber.org/zap v1.27.0
|
||||
golang.org/x/net v0.27.0
|
||||
golang.org/x/net v0.33.0
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20230920204549-e6e6cdab5c13
|
||||
google.golang.org/grpc v1.59.0
|
||||
google.golang.org/protobuf v1.33.0
|
||||
@@ -71,7 +71,27 @@ require (
|
||||
github.com/Masterminds/sprig/v3 v3.2.3 // indirect
|
||||
github.com/alecholmes/xfccparser v0.1.0 // indirect
|
||||
github.com/alecthomas/participle v0.4.1 // indirect
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.61.1704 // indirect
|
||||
github.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6 // indirect
|
||||
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect
|
||||
github.com/alibabacloud-go/darabonba-array v0.1.0 // indirect
|
||||
github.com/alibabacloud-go/darabonba-encode-util v0.0.2 // indirect
|
||||
github.com/alibabacloud-go/darabonba-map v0.0.2 // indirect
|
||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.10 // indirect
|
||||
github.com/alibabacloud-go/darabonba-signature-util v0.0.7 // indirect
|
||||
github.com/alibabacloud-go/darabonba-string v1.0.2 // indirect
|
||||
github.com/alibabacloud-go/debug v1.0.1 // indirect
|
||||
github.com/alibabacloud-go/endpoint-util v1.1.0 // indirect
|
||||
github.com/alibabacloud-go/kms-20160120/v3 v3.2.3 // indirect
|
||||
github.com/alibabacloud-go/openapi-util v0.1.0 // indirect
|
||||
github.com/alibabacloud-go/tea v1.2.2 // indirect
|
||||
github.com/alibabacloud-go/tea-utils v1.4.4 // indirect
|
||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.7 // indirect
|
||||
github.com/alibabacloud-go/tea-xml v1.1.3 // indirect
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.61.1800 // indirect
|
||||
github.com/aliyun/alibabacloud-dkms-gcs-go-sdk v0.5.1 // indirect
|
||||
github.com/aliyun/alibabacloud-dkms-transfer-go-sdk v0.1.8 // indirect
|
||||
github.com/aliyun/aliyun-secretsmanager-client-go v1.1.5 // indirect
|
||||
github.com/aliyun/credentials-go v1.4.3 // indirect
|
||||
github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df // indirect
|
||||
github.com/armon/go-metrics v0.4.1 // indirect
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
|
||||
@@ -82,10 +102,12 @@ require (
|
||||
github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/clbanning/mxj v1.8.4 // indirect
|
||||
github.com/clbanning/mxj/v2 v2.5.5 // indirect
|
||||
github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4 // indirect
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.14.3 // indirect
|
||||
github.com/coreos/go-oidc/v3 v3.6.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/deckarep/golang-set v1.7.1 // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
|
||||
github.com/docker/cli v24.0.7+incompatible // indirect
|
||||
github.com/docker/distribution v2.8.2+incompatible // indirect
|
||||
@@ -165,6 +187,7 @@ require (
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.0-rc5 // indirect
|
||||
github.com/openshift/api v0.0.0-20230720094506-afcbe27aec7c // indirect
|
||||
github.com/orcaman/concurrent-map v0.0.0-20210501183033-44dafcb38ecc // indirect
|
||||
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
@@ -179,9 +202,11 @@ require (
|
||||
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
||||
github.com/spf13/cast v1.5.1 // indirect
|
||||
github.com/stoewer/go-strcase v1.3.0 // indirect
|
||||
github.com/stretchr/objx v0.5.0 // indirect
|
||||
github.com/tetratelabs/wazero v1.7.3 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.0 // indirect
|
||||
github.com/tjfoc/gmsm v1.4.1 // indirect
|
||||
github.com/toolkits/concurrent v0.0.0-20150624120057-a4371d70e3e3 // indirect
|
||||
github.com/vbatts/tar-split v0.11.3 // indirect
|
||||
github.com/xlab/treeprint v1.2.0 // indirect
|
||||
@@ -197,14 +222,14 @@ require (
|
||||
go.opentelemetry.io/proto/otlp v1.0.0 // indirect
|
||||
go.starlark.net v0.0.0-20230525235612-a134d8f9ddca // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/crypto v0.25.0 // indirect
|
||||
golang.org/x/crypto v0.31.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
|
||||
golang.org/x/mod v0.17.0 // indirect
|
||||
golang.org/x/oauth2 v0.13.0 // indirect
|
||||
golang.org/x/sync v0.7.0 // indirect
|
||||
golang.org/x/sys v0.22.0 // indirect
|
||||
golang.org/x/term v0.22.0 // indirect
|
||||
golang.org/x/text v0.16.0 // indirect
|
||||
golang.org/x/sync v0.10.0 // indirect
|
||||
golang.org/x/sys v0.28.0 // indirect
|
||||
golang.org/x/term v0.27.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
golang.org/x/time v0.3.0 // indirect
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
|
||||
gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
|
||||
|
||||
144
go.sum
144
go.sum
@@ -683,9 +683,68 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF
|
||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
||||
github.com/alessio/shellescape v1.2.2/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30=
|
||||
github.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6 h1:eIf+iGJxdU4U9ypaUfbtOWCsZSbTb8AUHvyPrxu6mAA=
|
||||
github.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6/go.mod h1:4EUIoxs/do24zMOGGqYVWgw0s9NtiylnJglOeEB5UJo=
|
||||
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4/go.mod h1:sCavSAvdzOjul4cEqeVtvlSaSScfNsTQ+46HwlTL1hc=
|
||||
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 h1:zE8vH9C7JiZLNJJQ5OwjU9mSi4T9ef9u3BURT6LCLC8=
|
||||
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5/go.mod h1:tWnyE9AjF8J8qqLk645oUmVUnFybApTQWklQmi5tY6g=
|
||||
github.com/alibabacloud-go/darabonba-array v0.1.0 h1:vR8s7b1fWAQIjEjWnuF0JiKsCvclSRTfDzZHTYqfufY=
|
||||
github.com/alibabacloud-go/darabonba-array v0.1.0/go.mod h1:BLKxr0brnggqOJPqT09DFJ8g3fsDshapUD3C3aOEFaI=
|
||||
github.com/alibabacloud-go/darabonba-encode-util v0.0.2 h1:1uJGrbsGEVqWcWxrS9MyC2NG0Ax+GpOM5gtupki31XE=
|
||||
github.com/alibabacloud-go/darabonba-encode-util v0.0.2/go.mod h1:JiW9higWHYXm7F4PKuMgEUETNZasrDM6vqVr/Can7H8=
|
||||
github.com/alibabacloud-go/darabonba-map v0.0.2 h1:qvPnGB4+dJbJIxOOfawxzF3hzMnIpjmafa0qOTp6udc=
|
||||
github.com/alibabacloud-go/darabonba-map v0.0.2/go.mod h1:28AJaX8FOE/ym8OUFWga+MtEzBunJwQGceGQlvaPGPc=
|
||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.9/go.mod h1:bb+Io8Sn2RuM3/Rpme6ll86jMyFSrD1bxeV/+v61KeU=
|
||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.10 h1:GEYkMApgpKEVDn6z12DcH1EGYpDYRB8JxsazM4Rywak=
|
||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.10/go.mod h1:26a14FGhZVELuz2cc2AolvW4RHmIO3/HRwsdHhaIPDE=
|
||||
github.com/alibabacloud-go/darabonba-signature-util v0.0.7 h1:UzCnKvsjPFzApvODDNEYqBHMFt1w98wC7FOo0InLyxg=
|
||||
github.com/alibabacloud-go/darabonba-signature-util v0.0.7/go.mod h1:oUzCYV2fcCH797xKdL6BDH8ADIHlzrtKVjeRtunBNTQ=
|
||||
github.com/alibabacloud-go/darabonba-string v1.0.2 h1:E714wms5ibdzCqGeYJ9JCFywE5nDyvIXIIQbZVFkkqo=
|
||||
github.com/alibabacloud-go/darabonba-string v1.0.2/go.mod h1:93cTfV3vuPhhEwGGpKKqhVW4jLe7tDpo3LUM0i0g6mA=
|
||||
github.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68/go.mod h1:6pb/Qy8c+lqua8cFpEy7g39NRRqOWc3rOwAy8m5Y2BY=
|
||||
github.com/alibabacloud-go/debug v1.0.0/go.mod h1:8gfgZCCAC3+SCzjWtY053FrOcd4/qlH6IHTI4QyICOc=
|
||||
github.com/alibabacloud-go/debug v1.0.1 h1:MsW9SmUtbb1Fnt3ieC6NNZi6aEwrXfDksD4QA6GSbPg=
|
||||
github.com/alibabacloud-go/debug v1.0.1/go.mod h1:8gfgZCCAC3+SCzjWtY053FrOcd4/qlH6IHTI4QyICOc=
|
||||
github.com/alibabacloud-go/endpoint-util v1.1.0 h1:r/4D3VSw888XGaeNpP994zDUaxdgTSHBbVfZlzf6b5Q=
|
||||
github.com/alibabacloud-go/endpoint-util v1.1.0/go.mod h1:O5FuCALmCKs2Ff7JFJMudHs0I5EBgecXXxZRyswlEjE=
|
||||
github.com/alibabacloud-go/kms-20160120/v3 v3.2.3 h1:vamGcYQFwXVqR6RWcrVTTqlIXZVsYjaA7pZbx+Xw6zw=
|
||||
github.com/alibabacloud-go/kms-20160120/v3 v3.2.3/go.mod h1:3rIyughsFDLie1ut9gQJXkWkMg/NfXBCk+OtXnPu3lw=
|
||||
github.com/alibabacloud-go/openapi-util v0.1.0 h1:0z75cIULkDrdEhkLWgi9tnLe+KhAFE/r5Pb3312/eAY=
|
||||
github.com/alibabacloud-go/openapi-util v0.1.0/go.mod h1:sQuElr4ywwFRlCCberQwKRFhRzIyG4QTP/P4y1CJ6Ws=
|
||||
github.com/alibabacloud-go/tea v1.1.0/go.mod h1:IkGyUSX4Ba1V+k4pCtJUc6jDpZLFph9QMy2VUPTwukg=
|
||||
github.com/alibabacloud-go/tea v1.1.7/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4=
|
||||
github.com/alibabacloud-go/tea v1.1.8/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4=
|
||||
github.com/alibabacloud-go/tea v1.1.11/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4=
|
||||
github.com/alibabacloud-go/tea v1.1.17/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A=
|
||||
github.com/alibabacloud-go/tea v1.1.20/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A=
|
||||
github.com/alibabacloud-go/tea v1.2.1/go.mod h1:qbzof29bM/IFhLMtJPrgTGK3eauV5J2wSyEUo4OEmnA=
|
||||
github.com/alibabacloud-go/tea v1.2.2 h1:aTsR6Rl3ANWPfqeQugPglfurloyBJY85eFy7Gc1+8oU=
|
||||
github.com/alibabacloud-go/tea v1.2.2/go.mod h1:CF3vOzEMAG+bR4WOql8gc2G9H3EkH3ZLAQdpmpXMgwk=
|
||||
github.com/alibabacloud-go/tea-utils v1.3.1/go.mod h1:EI/o33aBfj3hETm4RLiAxF/ThQdSngxrpF8rKUDJjPE=
|
||||
github.com/alibabacloud-go/tea-utils v1.4.4 h1:lxCDvNCdTo9FaXKKq45+4vGETQUKNOW/qKTcX9Sk53o=
|
||||
github.com/alibabacloud-go/tea-utils v1.4.4/go.mod h1:KNcT0oXlZZxOXINnZBs6YvgOd5aYp9U67G+E3R8fcQw=
|
||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.3/go.mod h1:sj1PbjPodAVTqGTA3olprfeeqqmwD0A5OQz94o9EuXQ=
|
||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.5/go.mod h1:dL6vbUT35E4F4bFTHL845eUloqaerYBYPsdWR2/jhe4=
|
||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.6/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I=
|
||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.7 h1:WDx5qW3Xa5ZgJ1c8NfqJkF6w+AU5wB8835UdhPr6Ax0=
|
||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.7/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I=
|
||||
github.com/alibabacloud-go/tea-xml v1.1.3 h1:7LYnm+JbOq2B+T/B0fHC4Ies4/FofC4zHzYtqw7dgt0=
|
||||
github.com/alibabacloud-go/tea-xml v1.1.3/go.mod h1:Rq08vgCcCAjHyRi/M7xlHKUykZCEtyBy9+DPF6GgEu8=
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.61.18/go.mod h1:v8ESoHo4SyHmuB4b1tJqDHxfTGEciD+yhvOU/5s1Rfk=
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.61.1704 h1:PpfENOj/vPfhhy9N2OFRjpue0hjM5XqAp2thFmkXXIk=
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.61.1704/go.mod h1:RcDobYh8k5VP6TNybz9m++gL3ijVI5wueVr0EM10VsU=
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.61.1800 h1:ie/8RxBOfKZWcrbYSJi2Z8uX8TcOlSMwPlEJh83OeOw=
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.61.1800/go.mod h1:RcDobYh8k5VP6TNybz9m++gL3ijVI5wueVr0EM10VsU=
|
||||
github.com/aliyun/alibabacloud-dkms-gcs-go-sdk v0.5.1 h1:nJYyoFP+aqGKgPs9JeZgS1rWQ4NndNR0Zfhh161ZltU=
|
||||
github.com/aliyun/alibabacloud-dkms-gcs-go-sdk v0.5.1/go.mod h1:WzGOmFFTlUzXM03CJnHWMQ85UN6QGpOXZocCjwkiyOg=
|
||||
github.com/aliyun/alibabacloud-dkms-transfer-go-sdk v0.1.8 h1:QeUdR7JF7iNCvO/81EhxEr3wDwxk4YBoYZOq6E0AjHI=
|
||||
github.com/aliyun/alibabacloud-dkms-transfer-go-sdk v0.1.8/go.mod h1:xP0KIZry6i7oGPF24vhAPr1Q8vLZRcMcxtft5xDKwCU=
|
||||
github.com/aliyun/aliyun-secretsmanager-client-go v1.1.5 h1:8S0mtD101RDYa0LXwdoqgN0RxdMmmJYjq8g2mk7/lQ4=
|
||||
github.com/aliyun/aliyun-secretsmanager-client-go v1.1.5/go.mod h1:M19fxYz3gpm0ETnoKweYyYtqrtnVtrpKFpwsghbw+cQ=
|
||||
github.com/aliyun/credentials-go v1.1.2/go.mod h1:ozcZaMR5kLM7pwtCMEpVmQ242suV6qTJya2bDq4X1Tw=
|
||||
github.com/aliyun/credentials-go v1.3.1/go.mod h1:8jKYhQuDawt8x2+fusqa1Y6mPxemTsBEN04dgcAcYz0=
|
||||
github.com/aliyun/credentials-go v1.3.6/go.mod h1:1LxUuX7L5YrZUWzBrRyk0SwSdH4OmPrib8NVePL3fxM=
|
||||
github.com/aliyun/credentials-go v1.3.10/go.mod h1:Jm6d+xIgwJVLVWT561vy67ZRP4lPTQxMbEYRuT2Ti1U=
|
||||
github.com/aliyun/credentials-go v1.4.3 h1:N3iHyvHRMyOwY1+0qBLSf3hb5JFiOujVSVuEpgeGttY=
|
||||
github.com/aliyun/credentials-go v1.4.3/go.mod h1:Jm6d+xIgwJVLVWT561vy67ZRP4lPTQxMbEYRuT2Ti1U=
|
||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
|
||||
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
||||
@@ -755,7 +814,6 @@ github.com/certifi/gocertifi v0.0.0-20191021191039-0944d244cd40/go.mod h1:sGbDF6
|
||||
github.com/certifi/gocertifi v0.0.0-20200922220541-2c3bb06c6054/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chzyer/logex v1.1.11-0.20170329064859-445be9e134b2/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
@@ -765,6 +823,8 @@ github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6D
|
||||
github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
|
||||
github.com/clbanning/mxj v1.8.4 h1:HuhwZtbyvyOw+3Z1AowPkU87JkJUSv751ELWaiTpj8I=
|
||||
github.com/clbanning/mxj v1.8.4/go.mod h1:BVjHeAH+rl9rs6f+QIpeRl0tfu10SXn1pUSa5PVGJng=
|
||||
github.com/clbanning/mxj/v2 v2.5.5 h1:oT81vUeEiQQ/DcHbzSytRngP6Ky9O+L+0Bw0zSJag9E=
|
||||
github.com/clbanning/mxj/v2 v2.5.5/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
|
||||
github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
@@ -813,6 +873,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/deckarep/golang-set v1.7.1 h1:SCQV0S6gTtp6itiFrTqI+pfmJ4LN85S1YzhDf9rTHJQ=
|
||||
github.com/deckarep/golang-set v1.7.1/go.mod h1:93vsz/8Wt4joVM7c2AVqh+YRMiUSc14yDtF28KmMOgQ=
|
||||
github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
|
||||
@@ -1162,8 +1224,9 @@ github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97Dwqy
|
||||
github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4=
|
||||
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
|
||||
github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 h1:l5lAOZEym3oK3SQ2HBHWsJUfbNBiTXJDeW2QDxw9AQ0=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||
github.com/gorilla/handlers v0.0.0-20150720190736-60c7bfde3e33/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
|
||||
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
@@ -1460,8 +1523,8 @@ github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRW
|
||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
|
||||
github.com/nacos-group/nacos-sdk-go v1.0.8 h1:8pEm05Cdav9sQgJSv5kyvlgfz0SzFUUGI3pWX6SiSnM=
|
||||
github.com/nacos-group/nacos-sdk-go v1.0.8/go.mod h1:hlAPn3UdzlxIlSILAyOXKxjFSvDJ9oLzTJ9hLAK1KzA=
|
||||
github.com/nacos-group/nacos-sdk-go/v2 v2.1.2 h1:A8GV6j0rw80I6tTKSav/pTpEgNECYXeFvZCsiLBWGnQ=
|
||||
github.com/nacos-group/nacos-sdk-go/v2 v2.1.2/go.mod h1:ys/1adWeKXXzbNWfRNbaFlX/t6HVLWdpsNDvmoWTw0g=
|
||||
github.com/nacos-group/nacos-sdk-go/v2 v2.3.2 h1:9QB2nCJzT5wkTVlxNYl3XL/7+G6p2USMi2gQh/ouQQo=
|
||||
github.com/nacos-group/nacos-sdk-go/v2 v2.3.2/go.mod h1:9FKXl6FqOiVmm72i8kADtbeK71egyG9y3uRDBg41tpQ=
|
||||
github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg=
|
||||
github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU=
|
||||
github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k=
|
||||
@@ -1517,6 +1580,8 @@ github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJ
|
||||
github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
|
||||
github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
|
||||
github.com/openzipkin/zipkin-go v0.3.0/go.mod h1:4c3sLeE8xjNqehmF5RpAFLPLJxXscc0R4l6Zg0P1tTQ=
|
||||
github.com/orcaman/concurrent-map v0.0.0-20210501183033-44dafcb38ecc h1:Ak86L+yDSOzKFa7WM5bf5itSOo1e3Xh8bm5YCMUXIjQ=
|
||||
github.com/orcaman/concurrent-map v0.0.0-20210501183033-44dafcb38ecc/go.mod h1:Lu3tH6HLW3feq74c2GC+jIMS/K2CFcDWnWD9XkenwhI=
|
||||
github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM=
|
||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY=
|
||||
@@ -1560,7 +1625,6 @@ github.com/prometheus/client_golang v1.5.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3O
|
||||
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
|
||||
github.com/prometheus/client_golang v1.9.0/go.mod h1:FqZLKOZnGdFAhOK4nqGHa7D66IdsO+O441Eve7ptJDU=
|
||||
github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
|
||||
github.com/prometheus/client_golang v1.12.2/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
|
||||
github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q=
|
||||
github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY=
|
||||
github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
@@ -1593,7 +1657,6 @@ github.com/prometheus/procfs v0.0.11/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4
|
||||
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
|
||||
github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
|
||||
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
|
||||
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
|
||||
github.com/prometheus/prometheus v0.45.0 h1:O/uG+Nw4kNxx/jDPxmjsSDd+9Ohql6E7ZSY1x5x/0KI=
|
||||
@@ -1643,8 +1706,9 @@ github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic
|
||||
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/assertions v1.1.0 h1:MkTeG1DMwsrdH7QtLXy5W+fUxWq+vmb6cLmyJ7aRtF0=
|
||||
github.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
|
||||
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
@@ -1713,6 +1777,9 @@ github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JT
|
||||
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
||||
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w=
|
||||
github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
|
||||
github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20200427203606-3cfed13b9966/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
@@ -1746,6 +1813,7 @@ github.com/yl2chen/cidranger v1.0.2 h1:lbOWZVCG1tCRX4u24kuM1Tb4nHqWkDxwLdoS+Seva
|
||||
github.com/yl2chen/cidranger v1.0.2/go.mod h1:9U1yz7WPYDwf0vpNWFaeRh0bjwz5RVgRy/9UEQfHl0g=
|
||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.30/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
@@ -1832,7 +1900,6 @@ go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0
|
||||
go.uber.org/automaxprocs v1.4.0/go.mod h1:/mTEdr7LvHhs0v7mjdxDreTz1OG5zdZGqgOnhWiR/+Q=
|
||||
go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
|
||||
go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
|
||||
go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
@@ -1849,7 +1916,6 @@ go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ=
|
||||
go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
|
||||
go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI=
|
||||
go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI=
|
||||
go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw=
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
@@ -1868,9 +1934,12 @@ golang.org/x/crypto v0.0.0-20190829043050-9756ffdc2472/go.mod h1:yigFU9vqHzYiE8U
|
||||
golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191219195013-becbf705a915/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
@@ -1882,8 +1951,13 @@ golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0
|
||||
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
|
||||
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
|
||||
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
|
||||
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
|
||||
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
|
||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 h1:MGwJjxBy0HJshjDNfLsYO8xppfqWlA5ZT9OhtUUhTNw=
|
||||
golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
|
||||
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
|
||||
@@ -1970,6 +2044,7 @@ golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
@@ -2008,8 +2083,13 @@ golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
|
||||
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
|
||||
golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
|
||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@@ -2059,8 +2139,8 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -2107,6 +2187,7 @@ golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200509044756-6aff5f38e54f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -2156,7 +2237,6 @@ golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@@ -2181,8 +2261,13 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
||||
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
@@ -2195,8 +2280,13 @@ golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk=
|
||||
golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
|
||||
golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo=
|
||||
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
||||
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
|
||||
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
|
||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||
golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -2214,8 +2304,11 @@ golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
@@ -2225,7 +2318,6 @@ golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxb
|
||||
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20220922220347-f3bd1da661af/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
|
||||
@@ -2279,6 +2371,7 @@ golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjs
|
||||
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
|
||||
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200509030707-2212a7e161a5/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
@@ -2646,6 +2739,7 @@ gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
|
||||
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.56.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
apiVersion: v2
|
||||
appVersion: 2.1.1
|
||||
appVersion: 2.1.4
|
||||
description: Helm chart for deploying higress gateways
|
||||
icon: https://higress.io/img/higress_logo_small.png
|
||||
home: http://higress.io/
|
||||
@@ -15,4 +15,4 @@ dependencies:
|
||||
repository: "file://../redis"
|
||||
version: 0.0.1
|
||||
type: application
|
||||
version: 2.1.1
|
||||
version: 2.1.4
|
||||
|
||||
@@ -250,6 +250,10 @@ spec:
|
||||
registries:
|
||||
items:
|
||||
properties:
|
||||
allowMcpServers:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
authSecretName:
|
||||
type: string
|
||||
consulDatacenter:
|
||||
@@ -263,6 +267,25 @@ spec:
|
||||
type: string
|
||||
domain:
|
||||
type: string
|
||||
enableMCPServer:
|
||||
type: boolean
|
||||
enableScopeMcpServers:
|
||||
type: boolean
|
||||
mcpServerBaseUrl:
|
||||
type: string
|
||||
mcpServerExportDomains:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
metadata:
|
||||
additionalProperties:
|
||||
properties:
|
||||
innerMap:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
type: object
|
||||
type: object
|
||||
nacosAccessKey:
|
||||
type: string
|
||||
nacosAddressServer:
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
dependencies:
|
||||
- name: higress-core
|
||||
repository: file://../core
|
||||
version: 2.1.1
|
||||
version: 2.1.4
|
||||
- name: higress-console
|
||||
repository: https://higress.io/helm-charts/
|
||||
version: 2.1.1
|
||||
digest: sha256:a40f56252d0aa995fbf62952a6d23304dd84845caf5766bc64f7270cb00f01e9
|
||||
generated: "2025-04-18T16:45:23.961507+08:00"
|
||||
version: 2.1.4
|
||||
digest: sha256:482d9c5263ed959848601ee249b4852aed842a7805f0b36e456639fd54649c45
|
||||
generated: "2025-06-10T20:57:14.150704+08:00"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
apiVersion: v2
|
||||
appVersion: 2.1.1
|
||||
appVersion: 2.1.4
|
||||
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.1.1
|
||||
version: 2.1.4
|
||||
- name: higress-console
|
||||
repository: "https://higress.io/helm-charts/"
|
||||
version: 2.1.1
|
||||
version: 2.1.4
|
||||
type: application
|
||||
version: 2.1.1
|
||||
version: 2.1.4
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
## Higress for Kubernetes
|
||||
## Higress 适用于 Kubernetes
|
||||
|
||||
Higress 是基于阿里巴巴内部网关实践构建的云原生 API 网关。
|
||||
Higress 是基于阿里巴巴内部网关实践的云原生 API 网关。
|
||||
|
||||
依托 Istio 和 Envoy,Higress 实现了流量网关、微服务网关和安全网关三重架构的融合,从而大幅降低了部署、运维成本。
|
||||
通过 Istio 和 Envoy 的支持,Higress 实现了流量网关、微服务网关和安全网关三种架构的融合,从而极大地减少了部署、运维的成本。
|
||||
|
||||
## 设置仓库信息
|
||||
|
||||
@@ -13,7 +13,7 @@ helm repo update
|
||||
|
||||
## 安装
|
||||
|
||||
以 `higress` 为发布名称安装 chart:
|
||||
使用 Helm 安装名为 `higress` 的组件:
|
||||
|
||||
```console
|
||||
helm install higress -n higress-system higress.io/higress --create-namespace --render-subchart-notes
|
||||
@@ -21,168 +21,130 @@ helm install higress -n higress-system higress.io/higress --create-namespace --r
|
||||
|
||||
## 卸载
|
||||
|
||||
要卸载/删除 higress 部署:
|
||||
删除名称为 higress 的安装:
|
||||
|
||||
```console
|
||||
helm delete higress -n higress-system
|
||||
```
|
||||
|
||||
该命令会移除与 chart 相关的所有 Kubernetes 组件,并删除发布。
|
||||
该命令将删除与组件关联的所有 Kubernetes 组件并卸载该发行版。
|
||||
|
||||
## 参数
|
||||
|
||||
## 值
|
||||
## Values
|
||||
|
||||
| 键 | 类型 | 默认值 | 描述 |
|
||||
|-----|------|---------|-------------|
|
||||
| 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 无法直接相互通信时。
|
||||
|----|------|---------|-------------|
|
||||
| clusterName | string | `""` | 集群名 |
|
||||
| controller.affinity | object | `{}` | 控制器亲和性设置 |
|
||||
| controller.automaticHttps.email | string | `""` | 自动 HTTPS 所需的邮件 |
|
||||
| controller.automaticHttps.enabled | bool | `true` | 是否启用自动 HTTPS 功能 |
|
||||
| controller.autoscaling.enabled | bool | `false` | 是否启用控制器的自动扩展功能 |
|
||||
| controller.autoscaling.maxReplicas | int | `5` | 最大副本数 |
|
||||
| controller.autoscaling.minReplicas | int | `1` | 最小副本数 |
|
||||
| controller.autoscaling.targetCPUUtilizationPercentage | int | `80` | 目标 CPU 使用率百分比 |
|
||||
| controller.env | object | `{}` | 环境变量 |
|
||||
| controller.hub | string | `"higress-registry.cn-hangzhou.cr.aliyuncs.com/higress"` | 图像库的基础地址 |
|
||||
| controller.image | string | `"higress"` | 镜像名称 |
|
||||
| controller.imagePullSecrets | list | `[]` | 拉取秘钥列表 |
|
||||
| controller.labels | object | `{}` | 标签 |
|
||||
| controller.name | string | `"higress-controller"` | 控制器名称 |
|
||||
| controller.nodeSelector | object | `{}` | 节点选择器 |
|
||||
| controller.podAnnotations | object | `{}` | Pod 注解 |
|
||||
| controller.podLabels | object | `{}` | 应用到 Pod 上的标签 |
|
||||
| controller.podSecurityContext | object | `{}` | Pod 安全上下文 |
|
||||
| controller.ports[0].name | string | `"http"` | 端口名称 |
|
||||
| controller.ports[0].port | int | `8888` | 端口编号 |
|
||||
| controller.ports[0].protocol | string | `"TCP"` | 协议类型 |
|
||||
| controller.ports[0].targetPort | int | `8888` | 目标端口 |
|
||||
| controller.ports[1].name | string | `"http-solver"` | 端口名称 |
|
||||
| controller.ports[1].port | int | `8889` | 端口编号 |
|
||||
| controller.ports[1].protocol | string | `"TCP"` | 协议类型 |
|
||||
| controller.ports[1].targetPort | int | `8889` | 目标端口 |
|
||||
| controller.ports[2].name | string | `"grpc"` | 端口名称 |
|
||||
| controller.ports[2].port | int | `15051` | 端口编号 |
|
||||
| controller.ports[2].protocol | string | `"TCP"` | 协议类型 |
|
||||
| controller.ports[2].targetPort | int | `15051` | 目标端口 |
|
||||
| controller.probe.httpGet.path | string | `"/ready"` | 运行状况检查路径 |
|
||||
| controller.probe.httpGet.port | int | `8888` | 端口运行状态检查 |
|
||||
| controller.probe.initialDelaySeconds | int | `1` | 初始延迟秒数 |
|
||||
| controller.probe.periodSeconds | int | `3` | 健康检查间隔秒数 |
|
||||
| controller.probe.timeoutSeconds | int | `5` | 超时秒数 |
|
||||
| controller.rbac.create | bool | `true` | 是否创建 RBAC 相关资源 |
|
||||
| controller.replicas | int | `1` | Higress 控制器 Pod 的数量 |
|
||||
| controller.resources.limits.cpu | string | `"1000m"` | CPU 上限 |
|
||||
| controller.resources.limits.memory | string | `"2048Mi"` | 内存上限 |
|
||||
| controller.resources.requests.cpu | string | `"500m"` | CPU 请求量 |
|
||||
| controller.resources.requests.memory | string | `"2048Mi"` | 内存请求量 |
|
||||
| controller.securityContext | object | `{}` | 安全上下文 |
|
||||
| controller.service.type | string | `"ClusterIP"` | 服务类型 |
|
||||
| controller.serviceAccount.annotations | object | `{}` | 添加到服务帐户的注解 |
|
||||
| controller.serviceAccount.create | bool | `true` | 是否创建服务帐户 |
|
||||
| controller.serviceAccount.name | string | `""` | 如果未设置且 create 为 true,则从 fullname 模板生成名称 |
|
||||
| controller.tag | string | `""` | 标记 |
|
||||
| controller.tolerations | list | `[]` | 受容容忍度列表 |
|
||||
| downstream.connectionBufferLimits | int | `32768` | 下游连接缓冲区限制(字节) |
|
||||
| downstream.http2.initialConnectionWindowSize | int | `1048576` | HTTP/2 初始连接窗口大小 |
|
||||
| downstream.http2.initialStreamWindowSize | int | `65535` | 流初始窗口大小 |
|
||||
| downstream.http2.maxConcurrentStreams | int | `100` | 并发流最大数量 |
|
||||
| downstream.idleTimeout | int | `180` | 空闲超时时间(秒) |
|
||||
| downstream.maxRequestHeadersKb | int | `60` | 最大请求头大小(KB) |
|
||||
| downstream.routeTimeout | int | `0` | 路由超时时间 |
|
||||
| gateway.affinity | object | `{}` | 网关的节点亲和性 |
|
||||
| gateway.annotations | object | `{}` | 应用于所有资源的注解 |
|
||||
| gateway.autoscaling.enabled | bool | `false` | 启用网关的自动扩展功能 |
|
||||
| gateway.autoscaling.maxReplicas | int | `5` | 最大副本数 |
|
||||
| gateway.autoscaling.minReplicas | int | `1` | 最小副本数 |
|
||||
| gateway.autoscaling.targetCPUUtilizationPercentage | int | `80` | CPU 使用率的目标百分比 |
|
||||
| gateway.containerSecurityContext | string | `nil` | 网关容器的安全配置上下文 |
|
||||
| gateway.env | object | `{}` | Pod 环境变量 |
|
||||
| gateway.hostNetwork | bool | `false` | 是否使用主机网络 |
|
||||
| gateway.httpPort | int | `80` | HTTP 服务端口 |
|
||||
| gateway.httpsPort | int | `443` | HTTPS 服务端口 |
|
||||
| gateway.hub | string | `"higress-registry.cn-hangzhou.cr.aliyuncs.com/higress"` | 网关镜像的基础域名 |
|
||||
| gateway.image | string | `"gateway"` | |
|
||||
| gateway.kind | string | `"Deployment"` | 部署类型 |
|
||||
| gateway.labels | object | `{}` | 应用于所有资源的标签 |
|
||||
| gateway.metrics.enabled | bool | `false` | 启用网关度量收集 |
|
||||
| gateway.metrics.honorLabels | bool | `false` | 是否合并现有标签 |
|
||||
| gateway.metrics.interval | string | `""` | 度量间隔时间 |
|
||||
| gateway.metrics.provider | string | `"monitoring.coreos.com"` | 定义监控提供者 |
|
||||
| gateway.metrics.rawSpec | object | `{}` | 额外的度量规范 |
|
||||
| gateway.metrics.relabelConfigs | list | `[]` | 重新标签配置 |
|
||||
| gateway.metrics.relabelings | list | `[]` | 重新标签项 |
|
||||
| gateway.metrics.scrapeTimeout | string | `""` | 抓取的超时时间 |
|
||||
| gateway.name | string | `"higress-gateway"` | 网关名称 |
|
||||
| gateway.networkGateway | string | `""` | 网络网关指定 |
|
||||
| gateway.nodeSelector | object | `{}` | 节点选择器 |
|
||||
| gateway.replicas | int | `2` | Higress Gateway pod 的数量 |
|
||||
| gateway.resources.limits.cpu | string | `"2000m"` | 容器资源限制的 CPU |
|
||||
| gateway.resources.limits.memory | string | `"2048Mi"` | 容器资源限制的内存 |
|
||||
| gateway.resources.requests.cpu | string | `"2000m"` | 容器资源请求的 CPU |
|
||||
| gateway.resources.requests.memory | string | `"2048Mi"` | 容器资源请求的内存 |
|
||||
| gateway.revision | string | `""` | 网关所属版本声明 |
|
||||
| gateway.rollingMaxSurge | string | `"100%"` | 最大激增数目百分比 |
|
||||
| gateway.rollingMaxUnavailable | string | `"25%"` | 最大不可用比例 |
|
||||
| gateway.readinessFailureThreshold | int | `30` | 成功尝试之前连续失败的最大探测次数 |
|
||||
| gateway.readinessInitialDelaySeconds | int | `1` | 初次检测推迟多少秒后开始探测存活状态 |
|
||||
| gateway.readinessPeriodSeconds | int | `2` | 存活探测间隔秒数 |
|
||||
| gateway.readinessSuccessThreshold | int | `1` | 认为成功之前连续成功最小探测次数 |
|
||||
| gateway.readinessTimeoutSeconds | int | `3` | 存活探测超时秒数 |
|
||||
| gateway.securityContext | string | `nil` | 客户豆荚的安全上下文 |
|
||||
| gateway.service.annotations | object | `{}` | 应用于服务账户的注释 |
|
||||
| gateway.service.externalTrafficPolicy | string | `""` | 外部路由策略 |
|
||||
| gateway.service.loadBalancerClass | string | `""` | 负载均衡器类别 |
|
||||
| gateway.service.loadBalancerIP | string | `""` | 负载均衡器 IP 地址 |
|
||||
| gateway.service.loadBalancerSourceRanges | list | `[]` | 允许访问负载均衡器的 CIDR 范围 |
|
||||
| gateway.service.ports[0].name | string | `"http2"` | 服务定义的端口名称 |
|
||||
| gateway.service.ports[0].port | int | `80` | 服务端口 |
|
||||
| gateway.service.ports[0].protocol | string | `"TCP"` | 协议 |
|
||||
| gateway.service.ports[0].targetPort | int | `80` | 靶向端口 |
|
||||
| gateway.service.ports[1].name | string | `"https"` | 服务定义的端口名称 |
|
||||
| gateway.service.ports[1].port | int | `443` | 服务端口 |
|
||||
| gateway.service.ports[1].protocol | string | `"TCP"` | 协议 |
|
||||
| gateway.service.ports[1].targetPort | int | `443` | 靶向端口 |
|
||||
| gateway.service.type | string | `"LoadBalancer"` | 服务类型 |
|
||||
| global.disableAlpnH2 | bool | `false` | 设置是否禁用 ALPN 中的 http/2 |
|
||||
| ... | ... | ... | ... |
|
||||
|
||||
由于内容较多,其他参数可以参考完整表。
|
||||
|
||||
12
hgctl/go.mod
12
hgctl/go.mod
@@ -242,15 +242,15 @@ require (
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
golang.org/x/crypto v0.25.0 // indirect
|
||||
golang.org/x/crypto v0.31.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
|
||||
golang.org/x/mod v0.17.0 // indirect
|
||||
golang.org/x/net v0.27.0 // indirect
|
||||
golang.org/x/net v0.33.0 // indirect
|
||||
golang.org/x/oauth2 v0.13.0 // indirect
|
||||
golang.org/x/sync v0.7.0 // indirect
|
||||
golang.org/x/sys v0.22.0 // indirect
|
||||
golang.org/x/term v0.22.0 // indirect
|
||||
golang.org/x/text v0.16.0 // indirect
|
||||
golang.org/x/sync v0.10.0 // indirect
|
||||
golang.org/x/sys v0.28.0 // indirect
|
||||
golang.org/x/term v0.27.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
golang.org/x/time v0.3.0 // indirect
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
|
||||
google.golang.org/appengine v1.6.8 // indirect
|
||||
|
||||
18
hgctl/go.sum
18
hgctl/go.sum
@@ -1789,8 +1789,9 @@ golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
|
||||
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
|
||||
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 h1:MGwJjxBy0HJshjDNfLsYO8xppfqWlA5ZT9OhtUUhTNw=
|
||||
golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
|
||||
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
|
||||
@@ -1909,8 +1910,9 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
|
||||
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
|
||||
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@@ -1961,8 +1963,9 @@ golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -2069,8 +2072,9 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
||||
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
@@ -2086,8 +2090,9 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk=
|
||||
golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
|
||||
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
|
||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||
golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -2108,8 +2113,9 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
|
||||
Submodule istio/istio updated: e6578f7dd0...1492505b14
Submodule istio/proxy updated: 862975858e...d411a4f019
@@ -22,5 +22,6 @@ var (
|
||||
GatewayName = env.RegisterStringVar("GATEWAY_NAME", "higress-gateway", "").Get()
|
||||
// Revision is the value of the Istio control plane revision, e.g. "canary",
|
||||
// and is the value used by the "istio.io/rev" label.
|
||||
Revision = env.Register("REVISION", "", "").Get()
|
||||
Revision = env.Register("REVISION", "", "").Get()
|
||||
McpServerWasmImageUrl = env.RegisterStringVar("MCP_SERVER_WASM_IMAGE_URL", "oci://higress-registry.cn-hangzhou.cr.aliyuncs.com/mcp-server/all-in-one:1.0.0", "").Get()
|
||||
)
|
||||
|
||||
@@ -63,6 +63,7 @@ import (
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/ingress"
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/ingressv1"
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/mcpbridge"
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/mcpserver"
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/secret"
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/util"
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/wasmplugin"
|
||||
@@ -158,6 +159,8 @@ type IngressConfig struct {
|
||||
|
||||
// secretConfigMgr manages secret dependencies
|
||||
secretConfigMgr *SecretConfigMgr
|
||||
|
||||
mcpServerCache mcpserver.McpServerCache
|
||||
}
|
||||
|
||||
// getSecretValue implements the getValue function for secret references
|
||||
@@ -224,6 +227,7 @@ func NewIngressConfig(localKubeClient kube.Client, xdsUpdater istiomodel.XDSUpda
|
||||
|
||||
higressConfigController := configmap.NewController(localKubeClient, clusterId, namespace)
|
||||
config.configmapMgr = configmap.NewConfigmapMgr(xdsUpdater, namespace, higressConfigController, higressConfigController.Lister())
|
||||
config.configmapMgr.RegisterMcpServerProvider(&config.mcpServerCache)
|
||||
|
||||
httpsConfigMgr, _ := cert.NewConfigMgr(namespace, localKubeClient.Kube())
|
||||
config.httpsConfigMgr = httpsConfigMgr
|
||||
@@ -421,6 +425,10 @@ func (m *IngressConfig) createWrapperConfigs(configs []config.Config) []common.W
|
||||
m.watchedSecretSet = globalContext.WatchedSecrets
|
||||
m.mutex.Unlock()
|
||||
|
||||
if m.mcpServerCache.SetMcpServers(globalContext.McpServers) {
|
||||
m.notifyXDSFullUpdate(mcpserver.GvkMcpServer, "mcp-server-annotation-change", nil)
|
||||
}
|
||||
|
||||
return wrapperConfigs
|
||||
}
|
||||
|
||||
@@ -590,6 +598,13 @@ func (m *IngressConfig) convertVirtualService(configs []common.WrapperConfig) []
|
||||
Spec: vs,
|
||||
})
|
||||
}
|
||||
// add vs from nacos3 for mcp server
|
||||
if m.RegistryReconciler != nil {
|
||||
allConfigsFromMcp := m.RegistryReconciler.GetAllConfigs(gvk.VirtualService)
|
||||
for _, cfg := range allConfigsFromMcp {
|
||||
out = append(out, *cfg)
|
||||
}
|
||||
}
|
||||
|
||||
// We generate some specific envoy filter here to avoid duplicated computation.
|
||||
m.convertEnvoyFilter(&convertOptions)
|
||||
@@ -676,6 +691,13 @@ func (m *IngressConfig) convertWasmPlugin([]common.WrapperConfig) []config.Confi
|
||||
Spec: wasmPlugin,
|
||||
})
|
||||
}
|
||||
// add wasm plugin from nacos for mcp server
|
||||
if m.RegistryReconciler != nil {
|
||||
wasmFromMcp := m.RegistryReconciler.GetAllConfigs(gvk.WasmPlugin)
|
||||
for _, cfg := range wasmFromMcp {
|
||||
out = append(out, *cfg)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -686,6 +708,7 @@ func (m *IngressConfig) convertServiceEntry([]common.WrapperConfig) []config.Con
|
||||
serviceEntries := m.RegistryReconciler.GetAllServiceWrapper()
|
||||
IngressLog.Infof("Found mcp serviceEntries %v", serviceEntries)
|
||||
out := make([]config.Config, 0, len(serviceEntries))
|
||||
hostSets := sets.Set[string]{}
|
||||
for _, se := range serviceEntries {
|
||||
out = append(out, config.Config{
|
||||
Meta: config.Meta{
|
||||
@@ -700,6 +723,15 @@ func (m *IngressConfig) convertServiceEntry([]common.WrapperConfig) []config.Con
|
||||
},
|
||||
Spec: se.ServiceEntry,
|
||||
})
|
||||
hostSets.Insert(se.ServiceEntry.Hosts[0])
|
||||
}
|
||||
// add service entry by host from nacos3 for mcp server
|
||||
seFromMcp := m.RegistryReconciler.GetAllConfigs(gvk.ServiceEntry)
|
||||
for _, cfg := range seFromMcp {
|
||||
se := cfg.Spec.(*networking.ServiceEntry)
|
||||
if !hostSets.Contains(se.Hosts[0]) {
|
||||
out = append(out, *cfg)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -770,6 +802,21 @@ func (m *IngressConfig) convertDestinationRule(configs []common.WrapperConfig) [
|
||||
if !exist {
|
||||
destinationRules[serviceName] = destinationRuleWrapper
|
||||
} else if dr.DestinationRule.TrafficPolicy != nil {
|
||||
// if the service is referenced by an sse type mcp server, an source ip based consistent hashing policy needs to be configured
|
||||
// consistent hashing policy will be generated by mcp server watcher, then if service do not have LoadBalancer settings, it will be merged
|
||||
if destinationRuleWrapper.DestinationRule.TrafficPolicy != nil && destinationRuleWrapper.DestinationRule.TrafficPolicy.LoadBalancer != nil {
|
||||
if dr.DestinationRule.TrafficPolicy.LoadBalancer == nil {
|
||||
dr.DestinationRule.TrafficPolicy.LoadBalancer = destinationRuleWrapper.DestinationRule.TrafficPolicy.LoadBalancer
|
||||
} else if dr.DestinationRule.TrafficPolicy.LoadBalancer.LbPolicy == nil {
|
||||
dr.DestinationRule.TrafficPolicy.LoadBalancer.LbPolicy = destinationRuleWrapper.DestinationRule.TrafficPolicy.LoadBalancer.LbPolicy
|
||||
}
|
||||
}
|
||||
// if the service is referenced by an https type mcp server, an client side simple mode tls policy needs to be configured
|
||||
// simple mode tls policy will be generated by mcp server watcher, then if service do not have tls settings, it will be merged
|
||||
if dr.DestinationRule.TrafficPolicy.Tls == nil && destinationRuleWrapper.DestinationRule.TrafficPolicy != nil &&
|
||||
destinationRuleWrapper.DestinationRule.TrafficPolicy.Tls != nil {
|
||||
dr.DestinationRule.TrafficPolicy.Tls = destinationRuleWrapper.DestinationRule.TrafficPolicy.Tls
|
||||
}
|
||||
portTrafficPolicy := destinationRuleWrapper.DestinationRule.TrafficPolicy.PortLevelSettings[0]
|
||||
portUpdated := false
|
||||
for _, policy := range dr.DestinationRule.TrafficPolicy.PortLevelSettings {
|
||||
@@ -1137,6 +1184,28 @@ func (m *IngressConfig) AddOrUpdateMcpBridge(clusterNamespacedName util.ClusterN
|
||||
// Set this label so that we do not compare configs and just push.
|
||||
Labels: map[string]string{constants.AlwaysPushLabel: "true"},
|
||||
}
|
||||
vsMetadata := config.Meta{
|
||||
Name: "mcpbridge-virtualservice",
|
||||
Namespace: m.namespace,
|
||||
GroupVersionKind: gvk.VirtualService,
|
||||
// Set this label so that we do not compare configs and just push.
|
||||
Labels: map[string]string{constants.AlwaysPushLabel: "true"},
|
||||
}
|
||||
wasmMetadata := config.Meta{
|
||||
Name: "mcpbridge-wasmplugin",
|
||||
Namespace: m.namespace,
|
||||
GroupVersionKind: gvk.WasmPlugin,
|
||||
// Set this label so that we do not compare configs and just push.
|
||||
Labels: map[string]string{constants.AlwaysPushLabel: "true"},
|
||||
}
|
||||
efMetadata := config.Meta{
|
||||
Name: "mcpbridge-envoyfilter",
|
||||
Namespace: m.namespace,
|
||||
GroupVersionKind: gvk.EnvoyFilter,
|
||||
// Set this label so that we do not compare configs and just push.
|
||||
Labels: map[string]string{constants.AlwaysPushLabel: "true"},
|
||||
}
|
||||
|
||||
for _, f := range m.serviceEntryHandlers {
|
||||
IngressLog.Debug("McpBridge triggerd serviceEntry update")
|
||||
f(config.Config{Meta: seMetadata}, config.Config{Meta: seMetadata}, istiomodel.EventUpdate)
|
||||
@@ -1145,7 +1214,20 @@ func (m *IngressConfig) AddOrUpdateMcpBridge(clusterNamespacedName util.ClusterN
|
||||
IngressLog.Debug("McpBridge triggerd destinationRule update")
|
||||
f(config.Config{Meta: drMetadata}, config.Config{Meta: drMetadata}, istiomodel.EventUpdate)
|
||||
}
|
||||
}, m.localKubeClient, m.namespace)
|
||||
for _, f := range m.virtualServiceHandlers {
|
||||
IngressLog.Debug("McpBridge triggerd virtualservice update")
|
||||
f(config.Config{Meta: vsMetadata}, config.Config{Meta: vsMetadata}, istiomodel.EventUpdate)
|
||||
}
|
||||
for _, f := range m.wasmPluginHandlers {
|
||||
IngressLog.Debug("McpBridge triggerd wasmplugin update")
|
||||
f(config.Config{Meta: wasmMetadata}, config.Config{Meta: wasmMetadata}, istiomodel.EventUpdate)
|
||||
}
|
||||
for _, f := range m.envoyFilterHandlers {
|
||||
IngressLog.Debug("McpBridge triggerd envoyfilter update")
|
||||
f(config.Config{Meta: efMetadata}, config.Config{Meta: efMetadata}, istiomodel.EventUpdate)
|
||||
}
|
||||
}, m.localKubeClient, m.namespace, m.clusterId.String())
|
||||
m.configmapMgr.RegisterMcpServerProvider(m.RegistryReconciler)
|
||||
}
|
||||
reconciler := m.RegistryReconciler
|
||||
err = reconciler.Reconcile(mcpbridge)
|
||||
@@ -1713,3 +1795,19 @@ func (m *IngressConfig) Patch(config.Config, config.PatchFunc) (string, error) {
|
||||
func (m *IngressConfig) Delete(config.GroupVersionKind, string, string, *string) error {
|
||||
return common.ErrUnsupportedOp
|
||||
}
|
||||
|
||||
func (m *IngressConfig) notifyXDSFullUpdate(gvk config.GroupVersionKind, reason istiomodel.TriggerReason, updatedConfigName *util.ClusterNamespacedName) {
|
||||
var configsUpdated map[istiomodel.ConfigKey]struct{}
|
||||
if updatedConfigName != nil {
|
||||
configsUpdated = map[istiomodel.ConfigKey]struct{}{{
|
||||
Kind: kind.MustFromGVK(gvk),
|
||||
Name: updatedConfigName.Name,
|
||||
Namespace: updatedConfigName.Namespace,
|
||||
}: {}}
|
||||
}
|
||||
m.XDSUpdater.ConfigUpdate(&istiomodel.PushRequest{
|
||||
Full: true,
|
||||
ConfigsUpdated: configsUpdated,
|
||||
Reason: istiomodel.NewReasonStats(reason),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@ import (
|
||||
"istio.io/istio/pkg/cluster"
|
||||
"istio.io/istio/pkg/util/sets"
|
||||
listersv1 "k8s.io/client-go/listers/core/v1"
|
||||
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/mcpserver"
|
||||
)
|
||||
|
||||
type GlobalContext struct {
|
||||
@@ -30,6 +32,8 @@ type GlobalContext struct {
|
||||
ClusterSecretLister map[cluster.ID]listersv1.SecretLister
|
||||
|
||||
ClusterServiceList map[cluster.ID]listersv1.ServiceLister
|
||||
|
||||
McpServers []*mcpserver.McpServer
|
||||
}
|
||||
|
||||
type Meta struct {
|
||||
@@ -169,6 +173,7 @@ func NewAnnotationHandlerManager() AnnotationHandler {
|
||||
match{},
|
||||
headerControl{},
|
||||
http2rpc{},
|
||||
mcpServer{},
|
||||
},
|
||||
gatewayHandlers: []GatewayHandler{
|
||||
downstreamTLS{},
|
||||
|
||||
94
pkg/ingress/kube/annotations/mcpserver.go
Normal file
94
pkg/ingress/kube/annotations/mcpserver.go
Normal file
@@ -0,0 +1,94 @@
|
||||
// Copyright (c) 2023 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 (
|
||||
"strings"
|
||||
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/mcpserver"
|
||||
"github.com/alibaba/higress/pkg/ingress/log"
|
||||
)
|
||||
|
||||
const (
|
||||
enableMcpServer = "mcp-server"
|
||||
mcpServerMatchRuleDomains = "mcp-server-match-rule-domains"
|
||||
mcpServerMatchRuleType = "mcp-server-match-rule-type"
|
||||
mcpServerMatchRuleValue = "mcp-server-match-rule-value"
|
||||
mcpServerUpstreamType = "mcp-server-upstream-type"
|
||||
mcpServerEnablePathRewrite = "mcp-server-enable-path-rewrite"
|
||||
mcpServerPathRewritePrefix = "mcp-server-path-rewrite-prefix"
|
||||
)
|
||||
|
||||
// help to conform mcpServer implements method of Parse
|
||||
var (
|
||||
_ Parser = &mcpServer{}
|
||||
)
|
||||
|
||||
type mcpServer struct{}
|
||||
|
||||
func (a mcpServer) Parse(annotations Annotations, config *Ingress, globalContext *GlobalContext) error {
|
||||
if globalContext == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
ingressKey := config.Namespace + "/" + config.Name
|
||||
|
||||
enabled, _ := annotations.ParseBoolASAP(enableMcpServer)
|
||||
if !enabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
var matchRuleDomains []string
|
||||
rawMatchRuleDomains, _ := annotations.ParseStringASAP(mcpServerMatchRuleDomains)
|
||||
if rawMatchRuleDomains == "" || rawMatchRuleDomains == "*" {
|
||||
// Match all domains. Leave an empty slice.
|
||||
} else if strings.Contains(rawMatchRuleDomains, ",") {
|
||||
matchRuleDomains = strings.Split(rawMatchRuleDomains, ",")
|
||||
} else {
|
||||
matchRuleDomains = []string{rawMatchRuleDomains}
|
||||
}
|
||||
|
||||
matchRuleType, _ := annotations.ParseStringASAP(mcpServerMatchRuleType)
|
||||
if matchRuleType == "" {
|
||||
log.IngressLog.Errorf("ingress %s: mcp-server-match-rule-path-type is empty", ingressKey)
|
||||
return nil
|
||||
} else if !mcpserver.ValidPathMatchTypes[matchRuleType] {
|
||||
log.IngressLog.Errorf("ingress %s: mcp-server-match-rule-path-type %s is not supported", ingressKey, matchRuleType)
|
||||
return nil
|
||||
}
|
||||
|
||||
matchRuleValue, _ := annotations.ParseStringASAP(mcpServerMatchRuleValue)
|
||||
|
||||
upstreamType, _ := annotations.ParseStringASAP(mcpServerUpstreamType)
|
||||
if upstreamType != "" && !mcpserver.ValidUpstreamTypes[upstreamType] {
|
||||
log.IngressLog.Errorf("mcp-server-upstream-type %s is not supported", upstreamType)
|
||||
return nil
|
||||
}
|
||||
|
||||
enablePathRewrite, _ := annotations.ParseBoolASAP(mcpServerEnablePathRewrite)
|
||||
pathRewritePrefix, _ := annotations.ParseStringASAP(mcpServerPathRewritePrefix)
|
||||
|
||||
globalContext.McpServers = append(globalContext.McpServers, &mcpserver.McpServer{
|
||||
Name: ingressKey,
|
||||
Domains: matchRuleDomains,
|
||||
PathMatchType: matchRuleType,
|
||||
PathMatchValue: matchRuleValue,
|
||||
UpstreamType: upstreamType,
|
||||
EnablePathRewrite: enablePathRewrite,
|
||||
PathRewritePrefix: pathRewritePrefix,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
257
pkg/ingress/kube/annotations/mcpserver_test.go
Normal file
257
pkg/ingress/kube/annotations/mcpserver_test.go
Normal file
@@ -0,0 +1,257 @@
|
||||
// Copyright (c) 2025 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 (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/mcpserver"
|
||||
)
|
||||
|
||||
func TestMCPServer_Parse(t *testing.T) {
|
||||
parser := mcpServer{}
|
||||
testCases := []struct {
|
||||
skip bool
|
||||
input Annotations
|
||||
expect *mcpserver.McpServer
|
||||
}{
|
||||
{
|
||||
// No annotation
|
||||
input: Annotations{},
|
||||
expect: nil,
|
||||
},
|
||||
{
|
||||
// Not enabled
|
||||
input: Annotations{
|
||||
buildHigressAnnotationKey(enableMcpServer): "false",
|
||||
buildHigressAnnotationKey(mcpServerMatchRuleDomains): "www.foo.com",
|
||||
buildHigressAnnotationKey(mcpServerMatchRuleType): "prefix",
|
||||
buildHigressAnnotationKey(mcpServerMatchRuleValue): "/mcp",
|
||||
buildHigressAnnotationKey(mcpServerUpstreamType): "rest",
|
||||
buildHigressAnnotationKey(mcpServerEnablePathRewrite): "",
|
||||
buildHigressAnnotationKey(mcpServerPathRewritePrefix): "",
|
||||
},
|
||||
expect: nil,
|
||||
},
|
||||
{
|
||||
// Enabled but no match rule type
|
||||
input: Annotations{
|
||||
buildHigressAnnotationKey(enableMcpServer): "true",
|
||||
buildHigressAnnotationKey(mcpServerMatchRuleDomains): "www.foo.com",
|
||||
buildHigressAnnotationKey(mcpServerMatchRuleValue): "/mcp",
|
||||
buildHigressAnnotationKey(mcpServerUpstreamType): "rest",
|
||||
buildHigressAnnotationKey(mcpServerEnablePathRewrite): "",
|
||||
buildHigressAnnotationKey(mcpServerPathRewritePrefix): "",
|
||||
},
|
||||
expect: nil,
|
||||
},
|
||||
{
|
||||
// Enabled but empty match rule type
|
||||
input: Annotations{
|
||||
buildHigressAnnotationKey(enableMcpServer): "true",
|
||||
buildHigressAnnotationKey(mcpServerMatchRuleDomains): "www.foo.com",
|
||||
buildHigressAnnotationKey(mcpServerMatchRuleType): "",
|
||||
buildHigressAnnotationKey(mcpServerMatchRuleValue): "/mcp",
|
||||
buildHigressAnnotationKey(mcpServerUpstreamType): "rest",
|
||||
buildHigressAnnotationKey(mcpServerEnablePathRewrite): "",
|
||||
buildHigressAnnotationKey(mcpServerPathRewritePrefix): "",
|
||||
},
|
||||
expect: nil,
|
||||
},
|
||||
{
|
||||
// Enabled but bad match rule type
|
||||
input: Annotations{
|
||||
buildHigressAnnotationKey(enableMcpServer): "true",
|
||||
buildHigressAnnotationKey(mcpServerMatchRuleDomains): "www.foo.com",
|
||||
buildHigressAnnotationKey(mcpServerMatchRuleType): "bad-type",
|
||||
buildHigressAnnotationKey(mcpServerMatchRuleValue): "/mcp",
|
||||
buildHigressAnnotationKey(mcpServerUpstreamType): "rest",
|
||||
buildHigressAnnotationKey(mcpServerEnablePathRewrite): "",
|
||||
buildHigressAnnotationKey(mcpServerPathRewritePrefix): "",
|
||||
},
|
||||
expect: nil,
|
||||
},
|
||||
{
|
||||
// Enabled but bad upstream type
|
||||
input: Annotations{
|
||||
buildHigressAnnotationKey(enableMcpServer): "true",
|
||||
buildHigressAnnotationKey(mcpServerMatchRuleDomains): "www.foo.com",
|
||||
buildHigressAnnotationKey(mcpServerMatchRuleType): "prefix",
|
||||
buildHigressAnnotationKey(mcpServerMatchRuleValue): "/mcp",
|
||||
buildHigressAnnotationKey(mcpServerUpstreamType): "bad-type",
|
||||
buildHigressAnnotationKey(mcpServerEnablePathRewrite): "",
|
||||
buildHigressAnnotationKey(mcpServerPathRewritePrefix): "",
|
||||
},
|
||||
expect: nil,
|
||||
},
|
||||
{
|
||||
// Enabled and rewrite not enabled
|
||||
input: Annotations{
|
||||
buildHigressAnnotationKey(enableMcpServer): "true",
|
||||
buildHigressAnnotationKey(mcpServerMatchRuleDomains): "www.foo.com",
|
||||
buildHigressAnnotationKey(mcpServerMatchRuleType): "prefix",
|
||||
buildHigressAnnotationKey(mcpServerMatchRuleValue): "/mcp",
|
||||
buildHigressAnnotationKey(mcpServerUpstreamType): "rest",
|
||||
buildHigressAnnotationKey(mcpServerEnablePathRewrite): "false",
|
||||
buildHigressAnnotationKey(mcpServerPathRewritePrefix): "/",
|
||||
},
|
||||
expect: &mcpserver.McpServer{
|
||||
Name: "default/route",
|
||||
Domains: []string{"www.foo.com"},
|
||||
PathMatchType: "prefix",
|
||||
PathMatchValue: "/mcp",
|
||||
UpstreamType: "rest",
|
||||
EnablePathRewrite: false,
|
||||
PathRewritePrefix: "/",
|
||||
},
|
||||
},
|
||||
{
|
||||
// Enabled and rewrite not enabled and empty domain
|
||||
input: Annotations{
|
||||
buildHigressAnnotationKey(enableMcpServer): "true",
|
||||
buildHigressAnnotationKey(mcpServerMatchRuleDomains): "",
|
||||
buildHigressAnnotationKey(mcpServerMatchRuleType): "prefix",
|
||||
buildHigressAnnotationKey(mcpServerMatchRuleValue): "/mcp",
|
||||
buildHigressAnnotationKey(mcpServerUpstreamType): "rest",
|
||||
buildHigressAnnotationKey(mcpServerEnablePathRewrite): "false",
|
||||
buildHigressAnnotationKey(mcpServerPathRewritePrefix): "/",
|
||||
},
|
||||
expect: &mcpserver.McpServer{
|
||||
Name: "default/route",
|
||||
Domains: nil,
|
||||
PathMatchType: "prefix",
|
||||
PathMatchValue: "/mcp",
|
||||
UpstreamType: "rest",
|
||||
EnablePathRewrite: false,
|
||||
PathRewritePrefix: "/",
|
||||
},
|
||||
},
|
||||
{
|
||||
// Enabled and rewrite not enabled and wildcard domain
|
||||
input: Annotations{
|
||||
buildHigressAnnotationKey(enableMcpServer): "true",
|
||||
buildHigressAnnotationKey(mcpServerMatchRuleDomains): "*",
|
||||
buildHigressAnnotationKey(mcpServerMatchRuleType): "prefix",
|
||||
buildHigressAnnotationKey(mcpServerMatchRuleValue): "/mcp",
|
||||
buildHigressAnnotationKey(mcpServerUpstreamType): "rest",
|
||||
buildHigressAnnotationKey(mcpServerEnablePathRewrite): "false",
|
||||
buildHigressAnnotationKey(mcpServerPathRewritePrefix): "/",
|
||||
},
|
||||
expect: &mcpserver.McpServer{
|
||||
Name: "default/route",
|
||||
Domains: nil,
|
||||
PathMatchType: "prefix",
|
||||
PathMatchValue: "/mcp",
|
||||
UpstreamType: "rest",
|
||||
EnablePathRewrite: false,
|
||||
PathRewritePrefix: "/",
|
||||
},
|
||||
},
|
||||
{
|
||||
// Enabled and rewrite enabled with root
|
||||
input: Annotations{
|
||||
buildHigressAnnotationKey(enableMcpServer): "true",
|
||||
buildHigressAnnotationKey(mcpServerMatchRuleDomains): "www.foo.com",
|
||||
buildHigressAnnotationKey(mcpServerMatchRuleType): "prefix",
|
||||
buildHigressAnnotationKey(mcpServerMatchRuleValue): "/mcp",
|
||||
buildHigressAnnotationKey(mcpServerUpstreamType): "rest",
|
||||
buildHigressAnnotationKey(mcpServerEnablePathRewrite): "true",
|
||||
buildHigressAnnotationKey(mcpServerPathRewritePrefix): "/",
|
||||
},
|
||||
expect: &mcpserver.McpServer{
|
||||
Name: "default/route",
|
||||
Domains: []string{"www.foo.com"},
|
||||
PathMatchType: "prefix",
|
||||
PathMatchValue: "/mcp",
|
||||
UpstreamType: "rest",
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/",
|
||||
},
|
||||
},
|
||||
{
|
||||
// Enabled and rewrite enabled with root
|
||||
input: Annotations{
|
||||
buildHigressAnnotationKey(enableMcpServer): "true",
|
||||
buildHigressAnnotationKey(mcpServerMatchRuleDomains): "www.foo.com",
|
||||
buildHigressAnnotationKey(mcpServerMatchRuleType): "prefix",
|
||||
buildHigressAnnotationKey(mcpServerMatchRuleValue): "/mcp",
|
||||
buildHigressAnnotationKey(mcpServerUpstreamType): "rest",
|
||||
buildHigressAnnotationKey(mcpServerEnablePathRewrite): "true",
|
||||
buildHigressAnnotationKey(mcpServerPathRewritePrefix): "/mcp-api",
|
||||
},
|
||||
expect: &mcpserver.McpServer{
|
||||
Name: "default/route",
|
||||
Domains: []string{"www.foo.com"},
|
||||
PathMatchType: "prefix",
|
||||
PathMatchValue: "/mcp",
|
||||
UpstreamType: "rest",
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/mcp-api",
|
||||
},
|
||||
},
|
||||
{
|
||||
// Enabled and multiple domains
|
||||
input: Annotations{
|
||||
buildHigressAnnotationKey(enableMcpServer): "true",
|
||||
buildHigressAnnotationKey(mcpServerMatchRuleDomains): "www.foo.com,www.bar.com",
|
||||
buildHigressAnnotationKey(mcpServerMatchRuleType): "exact",
|
||||
buildHigressAnnotationKey(mcpServerMatchRuleValue): "/mcp",
|
||||
buildHigressAnnotationKey(mcpServerUpstreamType): "sse",
|
||||
buildHigressAnnotationKey(mcpServerEnablePathRewrite): "true",
|
||||
buildHigressAnnotationKey(mcpServerPathRewritePrefix): "/",
|
||||
},
|
||||
expect: &mcpserver.McpServer{
|
||||
Name: "default/route",
|
||||
Domains: []string{"www.foo.com", "www.bar.com"},
|
||||
PathMatchType: "exact",
|
||||
PathMatchValue: "/mcp",
|
||||
UpstreamType: "sse",
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range testCases {
|
||||
if tt.skip {
|
||||
return
|
||||
}
|
||||
|
||||
t.Run("", func(t *testing.T) {
|
||||
config := &Ingress{Meta: Meta{
|
||||
Namespace: "default",
|
||||
Name: "route",
|
||||
}}
|
||||
globalContext := &GlobalContext{}
|
||||
_ = parser.Parse(tt.input, config, globalContext)
|
||||
if tt.expect == nil {
|
||||
if len(globalContext.McpServers) != 0 {
|
||||
t.Fatalf("globalContext.McpServers is not empty: %v", globalContext.McpServers)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if len(globalContext.McpServers) != 1 {
|
||||
t.Fatalf("globalContext.McpServers length is not 1: %v", globalContext.McpServers)
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(tt.expect, globalContext.McpServers[0]); diff != "" {
|
||||
t.Fatalf("TestMCPServer_Parse() mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
97
pkg/ingress/kube/common/model_test.go
Normal file
97
pkg/ingress/kube/common/model_test.go
Normal file
@@ -0,0 +1,97 @@
|
||||
// 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 common
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"istio.io/istio/pilot/pkg/model"
|
||||
"istio.io/istio/pkg/config"
|
||||
)
|
||||
|
||||
func TestIngressDomainCache(t *testing.T) {
|
||||
cache := NewIngressDomainCache()
|
||||
assert.NotNil(t, cache)
|
||||
assert.NotNil(t, cache.Valid)
|
||||
assert.Empty(t, cache.Invalid)
|
||||
|
||||
cache.Valid["example.com"] = &IngressDomainBuilder{
|
||||
Host: "example.com",
|
||||
Protocol: HTTP,
|
||||
ClusterId: "cluster-1",
|
||||
Ingress: &config.Config{
|
||||
Meta: config.Meta{
|
||||
Name: "test-ingress",
|
||||
Namespace: "default",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
cache.Invalid = append(cache.Invalid, model.IngressDomain{
|
||||
Host: "invalid.com",
|
||||
Error: "invalid domain",
|
||||
})
|
||||
|
||||
result := cache.Extract()
|
||||
assert.Equal(t, 1, len(result.Valid))
|
||||
assert.Equal(t, "example.com", result.Valid[0].Host)
|
||||
assert.Equal(t, string(HTTP), result.Valid[0].Protocol)
|
||||
|
||||
assert.Equal(t, 1, len(result.Invalid))
|
||||
assert.Equal(t, "invalid.com", result.Invalid[0].Host)
|
||||
}
|
||||
|
||||
func TestIngressDomainBuilder(t *testing.T) {
|
||||
builder := &IngressDomainBuilder{
|
||||
Host: "example.com",
|
||||
Protocol: HTTP,
|
||||
ClusterId: "cluster-1",
|
||||
Ingress: &config.Config{
|
||||
Meta: config.Meta{
|
||||
Name: "test-ingress",
|
||||
Namespace: "default",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
domain := builder.Build()
|
||||
assert.Equal(t, "example.com", domain.Host)
|
||||
assert.Equal(t, string(HTTP), domain.Protocol)
|
||||
|
||||
builder.Event = MissingSecret
|
||||
eventDomain := builder.Build()
|
||||
assert.Contains(t, eventDomain.Error, "misses secret")
|
||||
|
||||
builder.Event = DuplicatedTls
|
||||
builder.PreIngress = &config.Config{
|
||||
Meta: config.Meta{
|
||||
Name: "pre-ingress",
|
||||
Namespace: "default",
|
||||
},
|
||||
}
|
||||
builder.PreIngress.Meta.Annotations = map[string]string{
|
||||
ClusterIdAnnotation: "pre-cluster",
|
||||
}
|
||||
dupDomain := builder.Build()
|
||||
assert.Contains(t, dupDomain.Error, "conflicted with ingress")
|
||||
|
||||
builder.Protocol = HTTPS
|
||||
builder.SecretName = "test-secret"
|
||||
builder.Event = ""
|
||||
httpsDomain := builder.Build()
|
||||
assert.Equal(t, string(HTTPS), httpsDomain.Protocol)
|
||||
assert.Equal(t, "test-secret", httpsDomain.SecretName)
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"testing"
|
||||
|
||||
networking "istio.io/api/networking/v1alpha3"
|
||||
"istio.io/istio/pilot/pkg/model"
|
||||
"istio.io/istio/pkg/config"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
@@ -556,3 +557,514 @@ func TestSortHTTPRoutesWithMoreRules(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateBackendResource(t *testing.T) {
|
||||
groupStr := "networking.higress.io"
|
||||
testCases := []struct {
|
||||
name string
|
||||
resource *v1.TypedLocalObjectReference
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "nil resource",
|
||||
resource: nil,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "nil APIGroup",
|
||||
resource: &v1.TypedLocalObjectReference{
|
||||
APIGroup: nil,
|
||||
Kind: "McpBridge",
|
||||
Name: "default",
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "wrong APIGroup",
|
||||
resource: &v1.TypedLocalObjectReference{
|
||||
APIGroup: &groupStr,
|
||||
Kind: "McpBridge",
|
||||
Name: "wrong-name",
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "wrong Kind",
|
||||
resource: &v1.TypedLocalObjectReference{
|
||||
APIGroup: &groupStr,
|
||||
Kind: "WrongKind",
|
||||
Name: "default",
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "valid resource",
|
||||
resource: &v1.TypedLocalObjectReference{
|
||||
APIGroup: &groupStr,
|
||||
Kind: "McpBridge",
|
||||
Name: "default",
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := ValidateBackendResource(tc.resource)
|
||||
assert.Equal(t, tc.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateOrUpdateAnnotations(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
annotations map[string]string
|
||||
options Options
|
||||
expected map[string]string
|
||||
}{
|
||||
{
|
||||
name: "empty annotations",
|
||||
annotations: map[string]string{},
|
||||
options: Options{
|
||||
ClusterId: "test-cluster",
|
||||
RawClusterId: "raw-test-cluster",
|
||||
},
|
||||
expected: map[string]string{
|
||||
ClusterIdAnnotation: "test-cluster",
|
||||
RawClusterIdAnnotation: "raw-test-cluster",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "existing annotations",
|
||||
annotations: map[string]string{
|
||||
"key1": "value1",
|
||||
"key2": "value2",
|
||||
},
|
||||
options: Options{
|
||||
ClusterId: "test-cluster",
|
||||
RawClusterId: "raw-test-cluster",
|
||||
},
|
||||
expected: map[string]string{
|
||||
"key1": "value1",
|
||||
"key2": "value2",
|
||||
ClusterIdAnnotation: "test-cluster",
|
||||
RawClusterIdAnnotation: "raw-test-cluster",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "overwrite existing cluster annotations",
|
||||
annotations: map[string]string{
|
||||
ClusterIdAnnotation: "old-cluster",
|
||||
RawClusterIdAnnotation: "old-raw-cluster",
|
||||
"key1": "value1",
|
||||
},
|
||||
options: Options{
|
||||
ClusterId: "new-cluster",
|
||||
RawClusterId: "new-raw-cluster",
|
||||
},
|
||||
expected: map[string]string{
|
||||
ClusterIdAnnotation: "new-cluster",
|
||||
RawClusterIdAnnotation: "new-raw-cluster",
|
||||
"key1": "value1",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := CreateOrUpdateAnnotations(tc.annotations, tc.options)
|
||||
assert.Equal(t, tc.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetClusterId(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
annotations map[string]string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "nil annotations",
|
||||
annotations: nil,
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "empty annotations",
|
||||
annotations: map[string]string{},
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "with cluster id",
|
||||
annotations: map[string]string{
|
||||
ClusterIdAnnotation: "test-cluster",
|
||||
},
|
||||
expected: "test-cluster",
|
||||
},
|
||||
{
|
||||
name: "with other annotations",
|
||||
annotations: map[string]string{
|
||||
"key1": "value1",
|
||||
"key2": "value2",
|
||||
},
|
||||
expected: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := GetClusterId(tc.annotations)
|
||||
assert.Equal(t, tc.expected, string(result))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertToDNSLabelValidAndCleanHost(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
input string
|
||||
}{
|
||||
{
|
||||
name: "simple host",
|
||||
input: "example.com",
|
||||
},
|
||||
{
|
||||
name: "wildcard host",
|
||||
input: "*.example.com",
|
||||
},
|
||||
{
|
||||
name: "long host",
|
||||
input: "very-long-subdomain.example-service.my-namespace.svc.cluster.local",
|
||||
},
|
||||
{
|
||||
name: "empty host",
|
||||
input: "",
|
||||
},
|
||||
{
|
||||
name: "ip address",
|
||||
input: "192.168.1.1",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// Test internal convertToDNSLabelValid function (through CleanHost)
|
||||
result := CleanHost(tc.input)
|
||||
|
||||
// Validate result
|
||||
assert.NotEmpty(t, result)
|
||||
assert.Equal(t, 16, len(result)) // MD5 hash format is fixed length of 16 bytes
|
||||
|
||||
// Consistency check - same input should produce same output
|
||||
result2 := CleanHost(tc.input)
|
||||
assert.Equal(t, result, result2)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitServiceFQDN(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
fqdn string
|
||||
expectedSvc string
|
||||
expectedNs string
|
||||
expectedValid bool
|
||||
}{
|
||||
{
|
||||
name: "simple fqdn",
|
||||
fqdn: "service.namespace",
|
||||
expectedSvc: "service",
|
||||
expectedNs: "namespace",
|
||||
expectedValid: true,
|
||||
},
|
||||
{
|
||||
name: "full k8s fqdn",
|
||||
fqdn: "service.namespace.svc.cluster.local",
|
||||
expectedSvc: "service",
|
||||
expectedNs: "namespace",
|
||||
expectedValid: true,
|
||||
},
|
||||
{
|
||||
name: "just service name",
|
||||
fqdn: "service",
|
||||
expectedSvc: "",
|
||||
expectedNs: "",
|
||||
expectedValid: false,
|
||||
},
|
||||
{
|
||||
name: "empty string",
|
||||
fqdn: "",
|
||||
expectedSvc: "",
|
||||
expectedNs: "",
|
||||
expectedValid: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
svc, ns, valid := SplitServiceFQDN(tc.fqdn)
|
||||
assert.Equal(t, tc.expectedSvc, svc)
|
||||
assert.Equal(t, tc.expectedNs, ns)
|
||||
assert.Equal(t, tc.expectedValid, valid)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertBackendService(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
dest *networking.HTTPRouteDestination
|
||||
expected model.BackendService
|
||||
}{
|
||||
{
|
||||
name: "simple service",
|
||||
dest: &networking.HTTPRouteDestination{
|
||||
Destination: &networking.Destination{
|
||||
Host: "service.namespace",
|
||||
Port: &networking.PortSelector{
|
||||
Number: 80,
|
||||
},
|
||||
},
|
||||
Weight: 100,
|
||||
},
|
||||
expected: model.BackendService{
|
||||
Name: "service",
|
||||
Namespace: "namespace",
|
||||
Port: 80,
|
||||
Weight: 100,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "full k8s FQDN",
|
||||
dest: &networking.HTTPRouteDestination{
|
||||
Destination: &networking.Destination{
|
||||
Host: "service.namespace.svc.cluster.local",
|
||||
Port: &networking.PortSelector{
|
||||
Number: 8080,
|
||||
},
|
||||
},
|
||||
Weight: 50,
|
||||
},
|
||||
expected: model.BackendService{
|
||||
Name: "service",
|
||||
Namespace: "namespace",
|
||||
Port: 8080,
|
||||
Weight: 50,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := ConvertBackendService(tc.dest)
|
||||
assert.Equal(t, tc.expected.Name, result.Name)
|
||||
assert.Equal(t, tc.expected.Namespace, result.Namespace)
|
||||
assert.Equal(t, tc.expected.Port, result.Port)
|
||||
assert.Equal(t, tc.expected.Weight, result.Weight)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateConvertedName(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
items []string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "empty slice",
|
||||
items: []string{},
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "single item",
|
||||
items: []string{"example"},
|
||||
expected: "example",
|
||||
},
|
||||
{
|
||||
name: "multiple items",
|
||||
items: []string{"part1", "part2", "part3"},
|
||||
expected: "part1-part2-part3",
|
||||
},
|
||||
{
|
||||
name: "with empty strings",
|
||||
items: []string{"part1", "", "part3"},
|
||||
expected: "part1-part3",
|
||||
},
|
||||
{
|
||||
name: "all empty strings",
|
||||
items: []string{"", "", ""},
|
||||
expected: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := CreateConvertedName(tc.items...)
|
||||
assert.Equal(t, tc.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSortIngressByCreationTime(t *testing.T) {
|
||||
configs := []config.Config{
|
||||
{
|
||||
Meta: config.Meta{
|
||||
Name: "c-ingress",
|
||||
Namespace: "ns1",
|
||||
},
|
||||
},
|
||||
{
|
||||
Meta: config.Meta{
|
||||
Name: "a-ingress",
|
||||
Namespace: "ns1",
|
||||
},
|
||||
},
|
||||
{
|
||||
Meta: config.Meta{
|
||||
Name: "b-ingress",
|
||||
Namespace: "ns1",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
expected := []string{"a-ingress", "b-ingress", "c-ingress"}
|
||||
|
||||
SortIngressByCreationTime(configs)
|
||||
|
||||
var actual []string
|
||||
for _, cfg := range configs {
|
||||
actual = append(actual, cfg.Name)
|
||||
}
|
||||
|
||||
assert.Equal(t, expected, actual, "When the timestamps are the same, the configuration should be sorted by name")
|
||||
|
||||
sameNamespaceConfigs := []config.Config{
|
||||
{
|
||||
Meta: config.Meta{
|
||||
Name: "same-name",
|
||||
Namespace: "c-ns",
|
||||
},
|
||||
},
|
||||
{
|
||||
Meta: config.Meta{
|
||||
Name: "same-name",
|
||||
Namespace: "a-ns",
|
||||
},
|
||||
},
|
||||
{
|
||||
Meta: config.Meta{
|
||||
Name: "same-name",
|
||||
Namespace: "b-ns",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
expectedNamespace := []string{"a-ns", "b-ns", "c-ns"}
|
||||
|
||||
SortIngressByCreationTime(sameNamespaceConfigs)
|
||||
|
||||
var actualNamespace []string
|
||||
for _, cfg := range sameNamespaceConfigs {
|
||||
actualNamespace = append(actualNamespace, cfg.Namespace)
|
||||
}
|
||||
|
||||
assert.Equal(t, expectedNamespace, actualNamespace, "When the names are the same, the configuration should be sorted by namespace")
|
||||
}
|
||||
|
||||
func TestPartMd5(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
input string
|
||||
length int
|
||||
}{
|
||||
{
|
||||
name: "empty string",
|
||||
input: "",
|
||||
length: 8,
|
||||
},
|
||||
{
|
||||
name: "simple string",
|
||||
input: "test",
|
||||
length: 8,
|
||||
},
|
||||
{
|
||||
name: "complex string",
|
||||
input: "this-is-a-long-string-with-special-chars-!@#$%^&*()",
|
||||
length: 8,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := partMd5(tc.input)
|
||||
|
||||
// Check result format
|
||||
assert.Equal(t, tc.length, len(result), "MD5 hash excerpt should be 8 characters")
|
||||
|
||||
// Run twice to ensure deterministic output
|
||||
result2 := partMd5(tc.input)
|
||||
assert.Equal(t, result, result2, "partMd5 function should be deterministic")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetLbStatusListV1AndV1Beta1(t *testing.T) {
|
||||
clusterPrefix = "gw-123-"
|
||||
svcName := clusterPrefix
|
||||
svcList := []*v1.Service{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: svcName,
|
||||
},
|
||||
Spec: v1.ServiceSpec{
|
||||
Type: v1.ServiceTypeLoadBalancer,
|
||||
},
|
||||
Status: v1.ServiceStatus{
|
||||
LoadBalancer: v1.LoadBalancerStatus{
|
||||
Ingress: []v1.LoadBalancerIngress{
|
||||
{
|
||||
IP: "2.2.2.2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: svcName,
|
||||
},
|
||||
Spec: v1.ServiceSpec{
|
||||
Type: v1.ServiceTypeLoadBalancer,
|
||||
},
|
||||
Status: v1.ServiceStatus{
|
||||
LoadBalancer: v1.LoadBalancerStatus{
|
||||
Ingress: []v1.LoadBalancerIngress{
|
||||
{
|
||||
Hostname: "1.1.1.1" + SvcHostNameSuffix,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Test the V1 version
|
||||
t.Run("GetLbStatusListV1", func(t *testing.T) {
|
||||
lbiList := GetLbStatusListV1(svcList)
|
||||
|
||||
assert.Equal(t, 2, len(lbiList), "There should be 2 entry points")
|
||||
assert.Equal(t, "1.1.1.1", lbiList[0].IP, "The first IP should be 1.1.1.1")
|
||||
assert.Equal(t, "2.2.2.2", lbiList[1].IP, "The second IP should be 2.2.2.2")
|
||||
})
|
||||
|
||||
// Test the V1Beta1 version
|
||||
t.Run("GetLbStatusListV1Beta1", func(t *testing.T) {
|
||||
lbiList := GetLbStatusListV1Beta1(svcList)
|
||||
|
||||
assert.Equal(t, 2, len(lbiList), "There should be 2 entry points")
|
||||
assert.Equal(t, "1.1.1.1", lbiList[0].IP, "The first IP should be 1.1.1.1")
|
||||
assert.Equal(t, "2.2.2.2", lbiList[1].IP, "The second IP should be 2.2.2.2")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ import (
|
||||
"sigs.k8s.io/yaml"
|
||||
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/controller"
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/mcpserver"
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/util"
|
||||
. "github.com/alibaba/higress/pkg/ingress/log"
|
||||
)
|
||||
@@ -111,6 +112,14 @@ func (c *ConfigmapMgr) GetHigressConfig() *HigressConfig {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ConfigmapMgr) RegisterMcpServerProvider(provider mcpserver.McpServerProvider) {
|
||||
for _, itemController := range c.ItemControllers {
|
||||
if mcpRouteProviderAware, ok := itemController.(mcpserver.McpRouteProviderAware); ok {
|
||||
mcpRouteProviderAware.RegisterMcpServerProvider(provider)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *ConfigmapMgr) AddItemControllers(controllers ...ItemController) {
|
||||
c.ItemControllers = append(c.ItemControllers, controllers...)
|
||||
}
|
||||
|
||||
@@ -22,11 +22,13 @@ import (
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/util"
|
||||
. "github.com/alibaba/higress/pkg/ingress/log"
|
||||
networking "istio.io/api/networking/v1alpha3"
|
||||
"istio.io/istio/pkg/config"
|
||||
"istio.io/istio/pkg/config/schema/gvk"
|
||||
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/mcpserver"
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/util"
|
||||
. "github.com/alibaba/higress/pkg/ingress/log"
|
||||
)
|
||||
|
||||
// RedisConfig defines the configuration for Redis connection
|
||||
@@ -55,12 +57,14 @@ type MCPRatelimitConfig struct {
|
||||
type SSEServer struct {
|
||||
// The name of the SSE server
|
||||
Name string `json:"name,omitempty"`
|
||||
// The path where the SSE server will be mounted, the full path is (PATH + SsePathSuffix)
|
||||
// The path where the SSE server will be mounted, the full path is (PATH + SSEPathSuffix)
|
||||
Path string `json:"path,omitempty"`
|
||||
// The type of the SSE server
|
||||
Type string `json:"type,omitempty"`
|
||||
// Additional Config parameters for the real MCP server implementation
|
||||
Config map[string]interface{} `json:"config,omitempty"`
|
||||
// The domain list of the SSE server
|
||||
DomainList []string `json:"domain_list,omitempty"`
|
||||
}
|
||||
|
||||
// MatchRule defines a rule for matching requests
|
||||
@@ -71,6 +75,12 @@ type MatchRule struct {
|
||||
MatchRulePath string `json:"match_rule_path,omitempty"`
|
||||
// Type of match rule: exact, prefix, suffix, contains, regex
|
||||
MatchRuleType string `json:"match_rule_type,omitempty"`
|
||||
// Type of upstream(s) matched by the rule: rest (default), sse
|
||||
UpstreamType string `json:"upstream_type"`
|
||||
// Enable request path rewrite for matched routes
|
||||
EnablePathRewrite bool `json:"enable_path_rewrite"`
|
||||
// Prefix the request path would be rewritten to.
|
||||
PathRewritePrefix string `json:"path_rewrite_prefix"`
|
||||
}
|
||||
|
||||
// McpServer defines the configuration for MCP (Model Context Protocol) server
|
||||
@@ -80,7 +90,7 @@ type McpServer struct {
|
||||
// Redis Config for MCP server
|
||||
Redis *RedisConfig `json:"redis,omitempty"`
|
||||
// The suffix to be appended to SSE paths, default is "/sse"
|
||||
SsePathSuffix string `json:"sse_path_suffix,omitempty"`
|
||||
SSEPathSuffix string `json:"sse_path_suffix,omitempty"`
|
||||
// List of SSE servers Configs
|
||||
Servers []*SSEServer `json:"servers,omitempty"`
|
||||
// List of match rules for filtering requests
|
||||
@@ -115,21 +125,32 @@ func validMcpServer(m *McpServer) error {
|
||||
|
||||
// Validate match rule types
|
||||
if m.MatchList != nil {
|
||||
validTypes := map[string]bool{
|
||||
validMatchRuleTypes := map[string]bool{
|
||||
"exact": true,
|
||||
"prefix": true,
|
||||
"suffix": true,
|
||||
"contains": true,
|
||||
"regex": true,
|
||||
}
|
||||
validUpstreamTypes := map[string]bool{
|
||||
"rest": true,
|
||||
"sse": true,
|
||||
"streamable": true,
|
||||
}
|
||||
|
||||
for _, rule := range m.MatchList {
|
||||
if rule.MatchRuleType == "" {
|
||||
return errors.New("match_rule_type cannot be empty, must be one of: exact, prefix, suffix, contains, regex")
|
||||
}
|
||||
if !validTypes[rule.MatchRuleType] {
|
||||
if !validMatchRuleTypes[rule.MatchRuleType] {
|
||||
return fmt.Errorf("invalid match_rule_type: %s, must be one of: exact, prefix, suffix, contains, regex", rule.MatchRuleType)
|
||||
}
|
||||
if rule.UpstreamType != "" && !validUpstreamTypes[rule.UpstreamType] {
|
||||
return fmt.Errorf("invalid upstream_type: %s, must be one of: rest, sse, streamable", rule.UpstreamType)
|
||||
}
|
||||
if rule.EnablePathRewrite && rule.UpstreamType != "sse" {
|
||||
return errors.New("path rewrite is only supported for SSE upstream type")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,7 +192,7 @@ func deepCopyMcpServer(mcp *McpServer) (*McpServer, error) {
|
||||
WhiteList: mcp.Ratelimit.WhiteList,
|
||||
}
|
||||
}
|
||||
newMcp.SsePathSuffix = mcp.SsePathSuffix
|
||||
newMcp.SSEPathSuffix = mcp.SSEPathSuffix
|
||||
|
||||
newMcp.EnableUserLevelServer = mcp.EnableUserLevelServer
|
||||
|
||||
@@ -179,9 +200,10 @@ func deepCopyMcpServer(mcp *McpServer) (*McpServer, error) {
|
||||
newMcp.Servers = make([]*SSEServer, len(mcp.Servers))
|
||||
for i, server := range mcp.Servers {
|
||||
newServer := &SSEServer{
|
||||
Name: server.Name,
|
||||
Path: server.Path,
|
||||
Type: server.Type,
|
||||
Name: server.Name,
|
||||
Path: server.Path,
|
||||
Type: server.Type,
|
||||
DomainList: server.DomainList,
|
||||
}
|
||||
if server.Config != nil {
|
||||
newServer.Config = make(map[string]interface{})
|
||||
@@ -197,9 +219,12 @@ func deepCopyMcpServer(mcp *McpServer) (*McpServer, error) {
|
||||
newMcp.MatchList = make([]*MatchRule, len(mcp.MatchList))
|
||||
for i, rule := range mcp.MatchList {
|
||||
newMcp.MatchList[i] = &MatchRule{
|
||||
MatchRuleDomain: rule.MatchRuleDomain,
|
||||
MatchRulePath: rule.MatchRulePath,
|
||||
MatchRuleType: rule.MatchRuleType,
|
||||
MatchRuleDomain: rule.MatchRuleDomain,
|
||||
MatchRulePath: rule.MatchRulePath,
|
||||
MatchRuleType: rule.MatchRuleType,
|
||||
UpstreamType: rule.UpstreamType,
|
||||
EnablePathRewrite: rule.EnablePathRewrite,
|
||||
PathRewritePrefix: rule.PathRewritePrefix,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -208,17 +233,19 @@ func deepCopyMcpServer(mcp *McpServer) (*McpServer, error) {
|
||||
}
|
||||
|
||||
type McpServerController struct {
|
||||
Namespace string
|
||||
mcpServer atomic.Value
|
||||
Name string
|
||||
eventHandler ItemEventHandler
|
||||
Namespace string
|
||||
mcpServer atomic.Value
|
||||
Name string
|
||||
eventHandler ItemEventHandler
|
||||
mcpServerProviders map[mcpserver.McpServerProvider]bool
|
||||
}
|
||||
|
||||
func NewMcpServerController(namespace string) *McpServerController {
|
||||
mcpController := &McpServerController{
|
||||
Namespace: namespace,
|
||||
mcpServer: atomic.Value{},
|
||||
Name: "mcpServer",
|
||||
Namespace: namespace,
|
||||
Name: "mcpServer",
|
||||
mcpServer: atomic.Value{},
|
||||
mcpServerProviders: make(map[mcpserver.McpServerProvider]bool),
|
||||
}
|
||||
mcpController.SetMcpServer(NewDefaultMcpServer())
|
||||
return mcpController
|
||||
@@ -285,6 +312,13 @@ func (m *McpServerController) RegisterItemEventHandler(eventHandler ItemEventHan
|
||||
m.eventHandler = eventHandler
|
||||
}
|
||||
|
||||
func (m *McpServerController) RegisterMcpServerProvider(provider mcpserver.McpServerProvider) {
|
||||
if m.mcpServerProviders == nil {
|
||||
m.mcpServerProviders = make(map[mcpserver.McpServerProvider]bool)
|
||||
}
|
||||
m.mcpServerProviders[provider] = true
|
||||
}
|
||||
|
||||
func (m *McpServerController) ConstructEnvoyFilters() ([]*config.Config, error) {
|
||||
configs := make([]*config.Config, 0)
|
||||
mcpServer := m.GetMcpServer()
|
||||
@@ -294,88 +328,132 @@ func (m *McpServerController) ConstructEnvoyFilters() ([]*config.Config, error)
|
||||
return configs, nil
|
||||
}
|
||||
|
||||
mcpStruct := m.constructMcpServerStruct(mcpServer)
|
||||
if mcpStruct == "" {
|
||||
return configs, nil
|
||||
}
|
||||
|
||||
config := &config.Config{
|
||||
Meta: config.Meta{
|
||||
GroupVersionKind: gvk.EnvoyFilter,
|
||||
Name: higressMcpServerEnvoyFilterName,
|
||||
Namespace: namespace,
|
||||
},
|
||||
Spec: &networking.EnvoyFilter{
|
||||
ConfigPatches: []*networking.EnvoyFilter_EnvoyConfigObjectPatch{
|
||||
{
|
||||
ApplyTo: networking.EnvoyFilter_HTTP_FILTER,
|
||||
Match: &networking.EnvoyFilter_EnvoyConfigObjectMatch{
|
||||
Context: networking.EnvoyFilter_GATEWAY,
|
||||
ObjectTypes: &networking.EnvoyFilter_EnvoyConfigObjectMatch_Listener{
|
||||
Listener: &networking.EnvoyFilter_ListenerMatch{
|
||||
FilterChain: &networking.EnvoyFilter_ListenerMatch_FilterChainMatch{
|
||||
Filter: &networking.EnvoyFilter_ListenerMatch_FilterMatch{
|
||||
Name: "envoy.filters.network.http_connection_manager",
|
||||
SubFilter: &networking.EnvoyFilter_ListenerMatch_SubFilterMatch{
|
||||
Name: "envoy.filters.http.cors",
|
||||
// mcp-session envoy filter
|
||||
mcpSessionStruct := m.constructMcpSessionStruct(mcpServer)
|
||||
if mcpSessionStruct != "" {
|
||||
sessionConfig := &config.Config{
|
||||
Meta: config.Meta{
|
||||
GroupVersionKind: gvk.EnvoyFilter,
|
||||
Name: higressMcpServerEnvoyFilterName,
|
||||
Namespace: namespace,
|
||||
},
|
||||
Spec: &networking.EnvoyFilter{
|
||||
ConfigPatches: []*networking.EnvoyFilter_EnvoyConfigObjectPatch{
|
||||
{
|
||||
ApplyTo: networking.EnvoyFilter_HTTP_FILTER,
|
||||
Match: &networking.EnvoyFilter_EnvoyConfigObjectMatch{
|
||||
Context: networking.EnvoyFilter_GATEWAY,
|
||||
ObjectTypes: &networking.EnvoyFilter_EnvoyConfigObjectMatch_Listener{
|
||||
Listener: &networking.EnvoyFilter_ListenerMatch{
|
||||
FilterChain: &networking.EnvoyFilter_ListenerMatch_FilterChainMatch{
|
||||
Filter: &networking.EnvoyFilter_ListenerMatch_FilterMatch{
|
||||
Name: "envoy.filters.network.http_connection_manager",
|
||||
SubFilter: &networking.EnvoyFilter_ListenerMatch_SubFilterMatch{
|
||||
Name: "envoy.filters.http.cors",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Patch: &networking.EnvoyFilter_Patch{
|
||||
Operation: networking.EnvoyFilter_Patch_INSERT_AFTER,
|
||||
Value: util.BuildPatchStruct(mcpStruct),
|
||||
Patch: &networking.EnvoyFilter_Patch{
|
||||
Operation: networking.EnvoyFilter_Patch_INSERT_AFTER,
|
||||
Value: util.BuildPatchStruct(mcpSessionStruct),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
configs = append(configs, sessionConfig)
|
||||
}
|
||||
|
||||
// mcp-server envoy filter
|
||||
mcpServerStruct := m.constructMcpServerStruct(mcpServer)
|
||||
if mcpServerStruct != "" {
|
||||
serverConfig := &config.Config{
|
||||
Meta: config.Meta{
|
||||
GroupVersionKind: gvk.EnvoyFilter,
|
||||
Name: higressMcpServerEnvoyFilterName + "-server",
|
||||
Namespace: namespace,
|
||||
},
|
||||
Spec: &networking.EnvoyFilter{
|
||||
ConfigPatches: []*networking.EnvoyFilter_EnvoyConfigObjectPatch{
|
||||
{
|
||||
ApplyTo: networking.EnvoyFilter_HTTP_FILTER,
|
||||
Match: &networking.EnvoyFilter_EnvoyConfigObjectMatch{
|
||||
Context: networking.EnvoyFilter_GATEWAY,
|
||||
ObjectTypes: &networking.EnvoyFilter_EnvoyConfigObjectMatch_Listener{
|
||||
Listener: &networking.EnvoyFilter_ListenerMatch{
|
||||
FilterChain: &networking.EnvoyFilter_ListenerMatch_FilterChainMatch{
|
||||
Filter: &networking.EnvoyFilter_ListenerMatch_FilterMatch{
|
||||
Name: "envoy.filters.network.http_connection_manager",
|
||||
SubFilter: &networking.EnvoyFilter_ListenerMatch_SubFilterMatch{
|
||||
Name: "envoy.filters.http.router",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Patch: &networking.EnvoyFilter_Patch{
|
||||
Operation: networking.EnvoyFilter_Patch_INSERT_BEFORE,
|
||||
Value: util.BuildPatchStruct(mcpServerStruct),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
configs = append(configs, serverConfig)
|
||||
}
|
||||
|
||||
configs = append(configs, config)
|
||||
return configs, nil
|
||||
}
|
||||
|
||||
func (m *McpServerController) constructMcpServerStruct(mcp *McpServer) string {
|
||||
// Build servers configuration
|
||||
servers := "[]"
|
||||
if len(mcp.Servers) > 0 {
|
||||
serverConfigs := make([]string, len(mcp.Servers))
|
||||
for i, server := range mcp.Servers {
|
||||
serverConfig := fmt.Sprintf(`{
|
||||
"name": "%s",
|
||||
"path": "%s",
|
||||
"type": "%s"`,
|
||||
server.Name, server.Path, server.Type)
|
||||
|
||||
if len(server.Config) > 0 {
|
||||
config, _ := json.Marshal(server.Config)
|
||||
serverConfig += fmt.Sprintf(`,
|
||||
"config": %s`, string(config))
|
||||
}
|
||||
|
||||
serverConfig += "}"
|
||||
serverConfigs[i] = serverConfig
|
||||
}
|
||||
servers = fmt.Sprintf("[%s]", strings.Join(serverConfigs, ","))
|
||||
}
|
||||
|
||||
func (m *McpServerController) constructMcpSessionStruct(mcp *McpServer) string {
|
||||
// Build match_list configuration
|
||||
matchList := "[]"
|
||||
if len(mcp.MatchList) > 0 {
|
||||
matchConfigs := make([]string, len(mcp.MatchList))
|
||||
for i, rule := range mcp.MatchList {
|
||||
matchConfigs[i] = fmt.Sprintf(`{
|
||||
var matchList []*MatchRule
|
||||
matchList = append(matchList, mcp.MatchList...)
|
||||
for provider, _ := range m.mcpServerProviders {
|
||||
servers := provider.GetMcpServers()
|
||||
if len(servers) == 0 {
|
||||
continue
|
||||
}
|
||||
for _, server := range servers {
|
||||
matchRuleDomain := ""
|
||||
if len(server.Domains) != 0 {
|
||||
if len(server.Domains) > 1 {
|
||||
matchRuleDomain = fmt.Sprintf("(%s)", strings.Join(server.Domains, "|"))
|
||||
} else {
|
||||
matchRuleDomain = server.Domains[0]
|
||||
}
|
||||
}
|
||||
matchList = append(matchList, &MatchRule{
|
||||
MatchRuleDomain: matchRuleDomain,
|
||||
MatchRuleType: server.PathMatchType,
|
||||
MatchRulePath: server.PathMatchValue,
|
||||
UpstreamType: server.UpstreamType,
|
||||
EnablePathRewrite: server.EnablePathRewrite,
|
||||
PathRewritePrefix: server.PathRewritePrefix,
|
||||
})
|
||||
}
|
||||
}
|
||||
matchListConfig := "[]"
|
||||
if len(matchList) > 0 {
|
||||
matchConfigs := make([]string, 0, len(matchList))
|
||||
for _, rule := range matchList {
|
||||
matchConfigs = append(matchConfigs, fmt.Sprintf(`{
|
||||
"match_rule_domain": "%s",
|
||||
"match_rule_path": "%s",
|
||||
"match_rule_type": "%s"
|
||||
}`, rule.MatchRuleDomain, rule.MatchRulePath, rule.MatchRuleType)
|
||||
"match_rule_type": "%s",
|
||||
"upstream_type": "%s",
|
||||
"enable_path_rewrite": %t,
|
||||
"path_rewrite_prefix": "%s"
|
||||
}`, rule.MatchRuleDomain, rule.MatchRulePath, rule.MatchRuleType, rule.UpstreamType, rule.EnablePathRewrite, rule.PathRewritePrefix))
|
||||
}
|
||||
matchList = fmt.Sprintf("[%s]", strings.Join(matchConfigs, ","))
|
||||
matchListConfig = fmt.Sprintf("[%s]", strings.Join(matchConfigs, ","))
|
||||
}
|
||||
|
||||
// 构建 Redis 配置
|
||||
// Build redis configuration
|
||||
redisConfig := "null"
|
||||
if mcp.Redis != nil {
|
||||
redisConfig = fmt.Sprintf(`{
|
||||
@@ -386,7 +464,7 @@ func (m *McpServerController) constructMcpServerStruct(mcp *McpServer) string {
|
||||
}`, mcp.Redis.Address, mcp.Redis.Username, mcp.Redis.Password, mcp.Redis.DB)
|
||||
}
|
||||
|
||||
// 构建限流配置
|
||||
// Build rate limit configuration
|
||||
rateLimitConfig := "null"
|
||||
if mcp.Ratelimit != nil {
|
||||
whiteList := "[]"
|
||||
@@ -417,7 +495,6 @@ func (m *McpServerController) constructMcpServerStruct(mcp *McpServer) string {
|
||||
"rate_limit": %s,
|
||||
"sse_path_suffix": "%s",
|
||||
"match_list": %s,
|
||||
"servers": %s,
|
||||
"enable_user_level_server": %t
|
||||
}
|
||||
}
|
||||
@@ -426,8 +503,55 @@ func (m *McpServerController) constructMcpServerStruct(mcp *McpServer) string {
|
||||
}`,
|
||||
redisConfig,
|
||||
rateLimitConfig,
|
||||
mcp.SsePathSuffix,
|
||||
matchList,
|
||||
servers,
|
||||
mcp.SSEPathSuffix,
|
||||
matchListConfig,
|
||||
mcp.EnableUserLevelServer)
|
||||
}
|
||||
|
||||
func (m *McpServerController) constructMcpServerStruct(mcp *McpServer) string {
|
||||
// Build servers configuration
|
||||
servers := "[]"
|
||||
if len(mcp.Servers) > 0 {
|
||||
serverConfigs := make([]string, len(mcp.Servers))
|
||||
for i, server := range mcp.Servers {
|
||||
serverConfig := fmt.Sprintf(`{
|
||||
"name": "%s",
|
||||
"path": "%s",
|
||||
"type": "%s"`,
|
||||
server.Name, server.Path, server.Type)
|
||||
if len(server.DomainList) > 0 {
|
||||
domainList := fmt.Sprintf(`["%s"]`, strings.Join(server.DomainList, `","`))
|
||||
serverConfig += fmt.Sprintf(`,
|
||||
"domain_list": %s`, domainList)
|
||||
}
|
||||
if len(server.Config) > 0 {
|
||||
config, _ := json.Marshal(server.Config)
|
||||
serverConfig += fmt.Sprintf(`,
|
||||
"config": %s`, string(config))
|
||||
}
|
||||
serverConfig += "}"
|
||||
serverConfigs[i] = serverConfig
|
||||
}
|
||||
servers = fmt.Sprintf("[%s]", strings.Join(serverConfigs, ","))
|
||||
}
|
||||
|
||||
// Build complete configuration structure
|
||||
return fmt.Sprintf(`{
|
||||
"name": "envoy.filters.http.golang",
|
||||
"typed_config": {
|
||||
"@type": "type.googleapis.com/udpa.type.v1.TypedStruct",
|
||||
"type_url": "type.googleapis.com/envoy.extensions.filters.http.golang.v3alpha.Config",
|
||||
"value": {
|
||||
"library_id": "mcp-server",
|
||||
"library_path": "/var/lib/istio/envoy/golang-filter.so",
|
||||
"plugin_name": "mcp-server",
|
||||
"plugin_config": {
|
||||
"@type": "type.googleapis.com/xds.type.v3.TypedStruct",
|
||||
"value": {
|
||||
"servers": %s
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`, servers)
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
package configmap
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
@@ -53,6 +54,61 @@ func Test_validMcpServer(t *testing.T) {
|
||||
},
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "enabled but bad match_rule_type",
|
||||
mcp: &McpServer{
|
||||
Enable: true,
|
||||
EnableUserLevelServer: false,
|
||||
Redis: nil,
|
||||
MatchList: []*MatchRule{
|
||||
{
|
||||
MatchRuleDomain: "*",
|
||||
MatchRulePath: "/mcp",
|
||||
MatchRuleType: "bad-type",
|
||||
},
|
||||
},
|
||||
Servers: []*SSEServer{},
|
||||
},
|
||||
wantErr: errors.New("invalid match_rule_type: bad-type, must be one of: exact, prefix, suffix, contains, regex"),
|
||||
},
|
||||
{
|
||||
name: "enabled but bad upstream_type",
|
||||
mcp: &McpServer{
|
||||
Enable: true,
|
||||
EnableUserLevelServer: false,
|
||||
Redis: nil,
|
||||
MatchList: []*MatchRule{
|
||||
{
|
||||
MatchRuleDomain: "*",
|
||||
MatchRulePath: "/mcp",
|
||||
MatchRuleType: "prefix",
|
||||
UpstreamType: "bad-type",
|
||||
},
|
||||
},
|
||||
Servers: []*SSEServer{},
|
||||
},
|
||||
wantErr: errors.New("invalid upstream_type: bad-type, must be one of: rest, sse, streamable"),
|
||||
},
|
||||
{
|
||||
name: "enabled but path rewrite with unsupported upstream type",
|
||||
mcp: &McpServer{
|
||||
Enable: true,
|
||||
EnableUserLevelServer: false,
|
||||
Redis: nil,
|
||||
MatchList: []*MatchRule{
|
||||
{
|
||||
MatchRuleDomain: "*",
|
||||
MatchRulePath: "/mcp",
|
||||
MatchRuleType: "prefix",
|
||||
UpstreamType: "rest",
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/",
|
||||
},
|
||||
},
|
||||
Servers: []*SSEServer{},
|
||||
},
|
||||
wantErr: errors.New("path rewrite is only supported for SSE upstream type"),
|
||||
},
|
||||
{
|
||||
name: "enabled with user level server but no redis config",
|
||||
mcp: &McpServer{
|
||||
@@ -75,7 +131,7 @@ func Test_validMcpServer(t *testing.T) {
|
||||
Password: "password",
|
||||
DB: 0,
|
||||
},
|
||||
SsePathSuffix: "/sse",
|
||||
SSEPathSuffix: "/sse",
|
||||
MatchList: []*MatchRule{
|
||||
{
|
||||
MatchRuleDomain: "*",
|
||||
@@ -237,7 +293,7 @@ func Test_deepCopyMcpServer(t *testing.T) {
|
||||
Password: "password",
|
||||
DB: 0,
|
||||
},
|
||||
SsePathSuffix: "/sse",
|
||||
SSEPathSuffix: "/sse",
|
||||
MatchList: []*MatchRule{
|
||||
{
|
||||
MatchRuleDomain: "*",
|
||||
@@ -264,7 +320,7 @@ func Test_deepCopyMcpServer(t *testing.T) {
|
||||
Password: "password",
|
||||
DB: 0,
|
||||
},
|
||||
SsePathSuffix: "/sse",
|
||||
SSEPathSuffix: "/sse",
|
||||
MatchList: []*MatchRule{
|
||||
{
|
||||
MatchRuleDomain: "*",
|
||||
@@ -422,3 +478,342 @@ func TestMcpServerController_AddOrUpdateHigressConfig(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMcpServerController_ValidHigressConfig(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
higressConfig *HigressConfig
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "nil config",
|
||||
higressConfig: nil,
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "nil mcp server",
|
||||
higressConfig: &HigressConfig{
|
||||
McpServer: nil,
|
||||
},
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "valid config",
|
||||
higressConfig: &HigressConfig{
|
||||
McpServer: &McpServer{
|
||||
Enable: true,
|
||||
Redis: &RedisConfig{
|
||||
Address: "localhost:6379",
|
||||
},
|
||||
MatchList: []*MatchRule{},
|
||||
Servers: []*SSEServer{},
|
||||
},
|
||||
},
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "invalid config - user level server without redis",
|
||||
higressConfig: &HigressConfig{
|
||||
McpServer: &McpServer{
|
||||
Enable: true,
|
||||
EnableUserLevelServer: true,
|
||||
Redis: nil,
|
||||
MatchList: []*MatchRule{},
|
||||
Servers: []*SSEServer{},
|
||||
},
|
||||
},
|
||||
wantErr: errors.New("redis config cannot be empty when user level server is enabled"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
m := NewMcpServerController("test-namespace")
|
||||
err := m.ValidHigressConfig(tt.higressConfig)
|
||||
assert.Equal(t, tt.wantErr, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMcpServerController_ConstructEnvoyFilters(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
mcpServer *McpServer
|
||||
wantConfigs int
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "nil mcp server",
|
||||
mcpServer: nil,
|
||||
wantConfigs: 0,
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "disabled mcp server",
|
||||
mcpServer: &McpServer{
|
||||
Enable: false,
|
||||
},
|
||||
wantConfigs: 0,
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "valid mcp server with redis",
|
||||
mcpServer: &McpServer{
|
||||
Enable: true,
|
||||
Redis: &RedisConfig{
|
||||
Address: "localhost:6379",
|
||||
},
|
||||
MatchList: []*MatchRule{},
|
||||
Servers: []*SSEServer{},
|
||||
},
|
||||
wantConfigs: 2, // Both session and server filters
|
||||
wantErr: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
m := NewMcpServerController("test-namespace")
|
||||
m.mcpServer.Store(tt.mcpServer)
|
||||
configs, err := m.ConstructEnvoyFilters()
|
||||
assert.Equal(t, tt.wantErr, err)
|
||||
assert.Equal(t, tt.wantConfigs, len(configs))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMcpServerController_constructMcpSessionStruct(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
mcp *McpServer
|
||||
wantJSON string
|
||||
}{
|
||||
{
|
||||
name: "minimal config",
|
||||
mcp: &McpServer{
|
||||
Enable: true,
|
||||
Redis: &RedisConfig{
|
||||
Address: "localhost:6379",
|
||||
},
|
||||
MatchList: []*MatchRule{},
|
||||
Servers: []*SSEServer{},
|
||||
},
|
||||
wantJSON: `{
|
||||
"name": "envoy.filters.http.golang",
|
||||
"typed_config": {
|
||||
"@type": "type.googleapis.com/udpa.type.v1.TypedStruct",
|
||||
"type_url": "type.googleapis.com/envoy.extensions.filters.http.golang.v3alpha.Config",
|
||||
"value": {
|
||||
"library_id": "mcp-session",
|
||||
"library_path": "/var/lib/istio/envoy/golang-filter.so",
|
||||
"plugin_name": "mcp-session",
|
||||
"plugin_config": {
|
||||
"@type": "type.googleapis.com/xds.type.v3.TypedStruct",
|
||||
"value": {
|
||||
"redis": {
|
||||
"address": "localhost:6379",
|
||||
"username": "",
|
||||
"password": "",
|
||||
"db": 0
|
||||
},
|
||||
"rate_limit": null,
|
||||
"sse_path_suffix": "",
|
||||
"match_list": [],
|
||||
"enable_user_level_server": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
},
|
||||
{
|
||||
name: "full config",
|
||||
mcp: &McpServer{
|
||||
Enable: true,
|
||||
Redis: &RedisConfig{
|
||||
Address: "localhost:6379",
|
||||
Username: "user",
|
||||
Password: "pass",
|
||||
DB: 1,
|
||||
},
|
||||
SSEPathSuffix: "/sse",
|
||||
MatchList: []*MatchRule{
|
||||
{
|
||||
MatchRuleDomain: "*",
|
||||
MatchRulePath: "/test",
|
||||
MatchRuleType: "exact",
|
||||
},
|
||||
{
|
||||
MatchRuleDomain: "*",
|
||||
MatchRulePath: "/sse-test-1",
|
||||
MatchRuleType: "prefix",
|
||||
UpstreamType: "sse",
|
||||
},
|
||||
{
|
||||
MatchRuleDomain: "*",
|
||||
MatchRulePath: "/sse-test-2",
|
||||
MatchRuleType: "prefix",
|
||||
UpstreamType: "sse",
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/mcp",
|
||||
},
|
||||
},
|
||||
EnableUserLevelServer: true,
|
||||
Ratelimit: &MCPRatelimitConfig{
|
||||
Limit: 100,
|
||||
Window: 3600,
|
||||
WhiteList: []string{"user1", "user2"},
|
||||
},
|
||||
},
|
||||
wantJSON: `{
|
||||
"name": "envoy.filters.http.golang",
|
||||
"typed_config": {
|
||||
"@type": "type.googleapis.com/udpa.type.v1.TypedStruct",
|
||||
"type_url": "type.googleapis.com/envoy.extensions.filters.http.golang.v3alpha.Config",
|
||||
"value": {
|
||||
"library_id": "mcp-session",
|
||||
"library_path": "/var/lib/istio/envoy/golang-filter.so",
|
||||
"plugin_name": "mcp-session",
|
||||
"plugin_config": {
|
||||
"@type": "type.googleapis.com/xds.type.v3.TypedStruct",
|
||||
"value": {
|
||||
"redis": {
|
||||
"address": "localhost:6379",
|
||||
"username": "user",
|
||||
"password": "pass",
|
||||
"db": 1
|
||||
},
|
||||
"rate_limit": {
|
||||
"limit": 100,
|
||||
"window": 3600,
|
||||
"white_list": ["user1","user2"]
|
||||
},
|
||||
"sse_path_suffix": "/sse",
|
||||
"match_list": [{
|
||||
"match_rule_domain": "*",
|
||||
"match_rule_path": "/test",
|
||||
"match_rule_type": "exact",
|
||||
"upstream_type": "",
|
||||
"enable_path_rewrite": false,
|
||||
"path_rewrite_prefix": ""
|
||||
},{
|
||||
"match_rule_domain": "*",
|
||||
"match_rule_path": "/sse-test-1",
|
||||
"match_rule_type": "prefix",
|
||||
"upstream_type": "sse",
|
||||
"enable_path_rewrite": false,
|
||||
"path_rewrite_prefix": ""
|
||||
},{
|
||||
"match_rule_domain": "*",
|
||||
"match_rule_path": "/sse-test-2",
|
||||
"match_rule_type": "prefix",
|
||||
"upstream_type": "sse",
|
||||
"enable_path_rewrite": true,
|
||||
"path_rewrite_prefix": "/mcp"
|
||||
}],
|
||||
"enable_user_level_server": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
m := NewMcpServerController("test-namespace")
|
||||
got := m.constructMcpSessionStruct(tt.mcp)
|
||||
// Normalize JSON strings for comparison
|
||||
var gotJSON, wantJSON interface{}
|
||||
json.Unmarshal([]byte(got), &gotJSON)
|
||||
json.Unmarshal([]byte(tt.wantJSON), &wantJSON)
|
||||
assert.Equal(t, wantJSON, gotJSON)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMcpServerController_constructMcpServerStruct(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
mcp *McpServer
|
||||
wantJSON string
|
||||
}{
|
||||
{
|
||||
name: "no servers",
|
||||
mcp: &McpServer{
|
||||
Servers: []*SSEServer{},
|
||||
},
|
||||
wantJSON: `{
|
||||
"name": "envoy.filters.http.golang",
|
||||
"typed_config": {
|
||||
"@type": "type.googleapis.com/udpa.type.v1.TypedStruct",
|
||||
"type_url": "type.googleapis.com/envoy.extensions.filters.http.golang.v3alpha.Config",
|
||||
"value": {
|
||||
"library_id": "mcp-server",
|
||||
"library_path": "/var/lib/istio/envoy/golang-filter.so",
|
||||
"plugin_name": "mcp-server",
|
||||
"plugin_config": {
|
||||
"@type": "type.googleapis.com/xds.type.v3.TypedStruct",
|
||||
"value": {
|
||||
"servers": []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
},
|
||||
{
|
||||
name: "with servers",
|
||||
mcp: &McpServer{
|
||||
Servers: []*SSEServer{
|
||||
{
|
||||
Name: "test-server",
|
||||
Path: "/test",
|
||||
Type: "test",
|
||||
Config: map[string]interface{}{
|
||||
"key": "value",
|
||||
},
|
||||
DomainList: []string{"example.com"},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantJSON: `{
|
||||
"name": "envoy.filters.http.golang",
|
||||
"typed_config": {
|
||||
"@type": "type.googleapis.com/udpa.type.v1.TypedStruct",
|
||||
"type_url": "type.googleapis.com/envoy.extensions.filters.http.golang.v3alpha.Config",
|
||||
"value": {
|
||||
"library_id": "mcp-server",
|
||||
"library_path": "/var/lib/istio/envoy/golang-filter.so",
|
||||
"plugin_name": "mcp-server",
|
||||
"plugin_config": {
|
||||
"@type": "type.googleapis.com/xds.type.v3.TypedStruct",
|
||||
"value": {
|
||||
"servers": [{
|
||||
"name": "test-server",
|
||||
"path": "/test",
|
||||
"type": "test",
|
||||
"domain_list": ["example.com"],
|
||||
"config": {"key":"value"}
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
m := NewMcpServerController("test-namespace")
|
||||
got := m.constructMcpServerStruct(tt.mcp)
|
||||
// Normalize JSON strings for comparison
|
||||
var gotJSON, wantJSON interface{}
|
||||
json.Unmarshal([]byte(got), &gotJSON)
|
||||
json.Unmarshal([]byte(tt.wantJSON), &wantJSON)
|
||||
assert.Equal(t, wantJSON, gotJSON)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
60
pkg/ingress/kube/mcpserver/model.go
Normal file
60
pkg/ingress/kube/mcpserver/model.go
Normal file
@@ -0,0 +1,60 @@
|
||||
// Copyright (c) 2025 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 mcpserver
|
||||
|
||||
import (
|
||||
"istio.io/istio/pkg/config"
|
||||
)
|
||||
|
||||
var (
|
||||
GvkMcpServer = config.GroupVersionKind{Group: "networking.higress.io", Version: "v1alpha1", Kind: "McpServer"}
|
||||
)
|
||||
|
||||
const (
|
||||
UpstreamTypeRest string = "rest"
|
||||
UpstreamTypeSSE string = "sse"
|
||||
UpstreamTypeStreamable string = "streamable"
|
||||
|
||||
ExactMatchType string = "exact"
|
||||
PrefixMatchType string = "prefix"
|
||||
SuffixMatchType string = "suffix"
|
||||
ContainsMatchType string = "contains"
|
||||
RegexMatchType string = "regex"
|
||||
)
|
||||
|
||||
var (
|
||||
ValidUpstreamTypes = map[string]bool{
|
||||
UpstreamTypeRest: true,
|
||||
UpstreamTypeSSE: true,
|
||||
UpstreamTypeStreamable: true,
|
||||
}
|
||||
ValidPathMatchTypes = map[string]bool{
|
||||
ExactMatchType: true,
|
||||
PrefixMatchType: true,
|
||||
SuffixMatchType: true,
|
||||
ContainsMatchType: true,
|
||||
RegexMatchType: true,
|
||||
}
|
||||
)
|
||||
|
||||
type McpServer struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Domains []string `json:"domains,omitempty"`
|
||||
PathMatchType string `json:"path_match_type,omitempty"`
|
||||
PathMatchValue string `json:"path_match_value,omitempty"`
|
||||
UpstreamType string `json:"upstream_type,omitempty"`
|
||||
EnablePathRewrite bool `json:"enable_path_rewrite,omitempty"`
|
||||
PathRewritePrefix string `json:"path_rewrite_prefix,omitempty"`
|
||||
}
|
||||
70
pkg/ingress/kube/mcpserver/provider.go
Normal file
70
pkg/ingress/kube/mcpserver/provider.go
Normal file
@@ -0,0 +1,70 @@
|
||||
// Copyright (c) 2025 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 mcpserver
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type McpServerProvider interface {
|
||||
GetMcpServers() []*McpServer
|
||||
}
|
||||
|
||||
type McpRouteProviderAware interface {
|
||||
RegisterMcpServerProvider(provider McpServerProvider)
|
||||
}
|
||||
|
||||
type McpServerCache struct {
|
||||
mcpServers []*McpServer
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
func (c *McpServerCache) GetMcpServers() []*McpServer {
|
||||
c.mutex.RLock()
|
||||
defer c.mutex.RUnlock()
|
||||
return c.mcpServers
|
||||
}
|
||||
|
||||
// SetMcpServers sets the mcp servers and returns true if the cached list is changed
|
||||
func (c *McpServerCache) SetMcpServers(mcpServers []*McpServer) bool {
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
|
||||
sortedMcpServers := make([]*McpServer, 0, len(mcpServers))
|
||||
sortedMcpServers = append(sortedMcpServers, mcpServers...)
|
||||
// Sort the mcp servers by PathMatchValue in descending order
|
||||
slices.SortFunc(sortedMcpServers, func(a, b *McpServer) int {
|
||||
return strings.Compare(a.Name, b.Name)
|
||||
})
|
||||
|
||||
if len(c.mcpServers) == len(sortedMcpServers) {
|
||||
changed := false
|
||||
for i := range c.mcpServers {
|
||||
if !reflect.DeepEqual(c.mcpServers[i], sortedMcpServers[i]) {
|
||||
changed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !changed {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
c.mcpServers = sortedMcpServers
|
||||
return true
|
||||
}
|
||||
654
pkg/ingress/kube/mcpserver/provider_test.go
Normal file
654
pkg/ingress/kube/mcpserver/provider_test.go
Normal file
@@ -0,0 +1,654 @@
|
||||
// Copyright (c) 2025 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 mcpserver
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
func TestMcpServerCache_GetSet(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
skip bool
|
||||
init []*McpServer
|
||||
input []*McpServer
|
||||
expect []*McpServer
|
||||
changed bool
|
||||
}{
|
||||
{
|
||||
name: "nil",
|
||||
init: nil,
|
||||
input: nil,
|
||||
changed: false,
|
||||
expect: nil,
|
||||
},
|
||||
{
|
||||
name: "nil to non-nil",
|
||||
init: nil,
|
||||
input: []*McpServer{
|
||||
{
|
||||
Name: "test2",
|
||||
Domains: []string{"www.foo.com"},
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test2",
|
||||
UpstreamType: UpstreamTypeSSE,
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/test",
|
||||
},
|
||||
{
|
||||
Name: "test3",
|
||||
Domains: []string{"www.bar.com"},
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test3",
|
||||
UpstreamType: UpstreamTypeStreamable,
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/",
|
||||
},
|
||||
{
|
||||
Name: "test1",
|
||||
Domains: nil,
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test1",
|
||||
UpstreamType: UpstreamTypeRest,
|
||||
EnablePathRewrite: false,
|
||||
PathRewritePrefix: "",
|
||||
},
|
||||
},
|
||||
changed: true,
|
||||
expect: []*McpServer{
|
||||
{
|
||||
Name: "test1",
|
||||
Domains: nil,
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test1",
|
||||
UpstreamType: UpstreamTypeRest,
|
||||
EnablePathRewrite: false,
|
||||
PathRewritePrefix: "",
|
||||
},
|
||||
{
|
||||
Name: "test2",
|
||||
Domains: []string{"www.foo.com"},
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test2",
|
||||
UpstreamType: UpstreamTypeSSE,
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/test",
|
||||
},
|
||||
{
|
||||
Name: "test3",
|
||||
Domains: []string{"www.bar.com"},
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test3",
|
||||
UpstreamType: UpstreamTypeStreamable,
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "non-nil to non-nil (length increase)",
|
||||
init: []*McpServer{
|
||||
{
|
||||
Name: "test1",
|
||||
Domains: nil,
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test1",
|
||||
UpstreamType: UpstreamTypeRest,
|
||||
EnablePathRewrite: false,
|
||||
PathRewritePrefix: "",
|
||||
},
|
||||
{
|
||||
Name: "test2",
|
||||
Domains: []string{"www.foo.com"},
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test2",
|
||||
UpstreamType: UpstreamTypeSSE,
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/test",
|
||||
},
|
||||
},
|
||||
input: []*McpServer{
|
||||
{
|
||||
Name: "test2",
|
||||
Domains: []string{"www.foo.com"},
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test2",
|
||||
UpstreamType: UpstreamTypeSSE,
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/test",
|
||||
},
|
||||
{
|
||||
Name: "test3",
|
||||
Domains: []string{"www.bar.com"},
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test3",
|
||||
UpstreamType: UpstreamTypeStreamable,
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/",
|
||||
},
|
||||
{
|
||||
Name: "test1",
|
||||
Domains: nil,
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test1",
|
||||
UpstreamType: UpstreamTypeRest,
|
||||
EnablePathRewrite: false,
|
||||
PathRewritePrefix: "",
|
||||
},
|
||||
},
|
||||
changed: true,
|
||||
expect: []*McpServer{
|
||||
{
|
||||
Name: "test1",
|
||||
Domains: nil,
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test1",
|
||||
UpstreamType: UpstreamTypeRest,
|
||||
EnablePathRewrite: false,
|
||||
PathRewritePrefix: "",
|
||||
},
|
||||
{
|
||||
Name: "test2",
|
||||
Domains: []string{"www.foo.com"},
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test2",
|
||||
UpstreamType: UpstreamTypeSSE,
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/test",
|
||||
},
|
||||
{
|
||||
Name: "test3",
|
||||
Domains: []string{"www.bar.com"},
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test3",
|
||||
UpstreamType: UpstreamTypeStreamable,
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "non-nil to non-nil (length decrease)",
|
||||
init: []*McpServer{
|
||||
{
|
||||
Name: "test2",
|
||||
Domains: []string{"www.foo.com"},
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test2",
|
||||
UpstreamType: UpstreamTypeSSE,
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/test",
|
||||
},
|
||||
{
|
||||
Name: "test3",
|
||||
Domains: []string{"www.bar.com"},
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test3",
|
||||
UpstreamType: UpstreamTypeStreamable,
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/",
|
||||
},
|
||||
{
|
||||
Name: "test1",
|
||||
Domains: nil,
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test1",
|
||||
UpstreamType: UpstreamTypeRest,
|
||||
EnablePathRewrite: false,
|
||||
PathRewritePrefix: "",
|
||||
},
|
||||
},
|
||||
input: []*McpServer{
|
||||
{
|
||||
Name: "test3",
|
||||
Domains: []string{"www.bar.com"},
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test3",
|
||||
UpstreamType: UpstreamTypeStreamable,
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/",
|
||||
},
|
||||
{
|
||||
Name: "test2",
|
||||
Domains: []string{"www.foo.com"},
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test2",
|
||||
UpstreamType: UpstreamTypeSSE,
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/test",
|
||||
},
|
||||
},
|
||||
changed: true,
|
||||
expect: []*McpServer{
|
||||
{
|
||||
Name: "test2",
|
||||
Domains: []string{"www.foo.com"},
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test2",
|
||||
UpstreamType: UpstreamTypeSSE,
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/test",
|
||||
},
|
||||
{
|
||||
Name: "test3",
|
||||
Domains: []string{"www.bar.com"},
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test3",
|
||||
UpstreamType: UpstreamTypeStreamable,
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "non-nil to non-nil (length unchanged + name field changed)",
|
||||
init: []*McpServer{
|
||||
{
|
||||
Name: "test2",
|
||||
Domains: []string{"www.foo.com"},
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test2",
|
||||
UpstreamType: UpstreamTypeSSE,
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/test",
|
||||
},
|
||||
{
|
||||
Name: "test3",
|
||||
Domains: []string{"www.bar.com"},
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test3",
|
||||
UpstreamType: UpstreamTypeStreamable,
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/",
|
||||
},
|
||||
{
|
||||
Name: "test1",
|
||||
Domains: nil,
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test1",
|
||||
UpstreamType: UpstreamTypeRest,
|
||||
EnablePathRewrite: false,
|
||||
PathRewritePrefix: "",
|
||||
},
|
||||
},
|
||||
input: []*McpServer{
|
||||
{
|
||||
Name: "test2",
|
||||
Domains: []string{"www.foo.com"},
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test2",
|
||||
UpstreamType: UpstreamTypeSSE,
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/test",
|
||||
},
|
||||
{
|
||||
Name: "test3-1",
|
||||
Domains: []string{"www.bar.com"},
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test3",
|
||||
UpstreamType: UpstreamTypeStreamable,
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/",
|
||||
},
|
||||
{
|
||||
Name: "test1",
|
||||
Domains: nil,
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test1",
|
||||
UpstreamType: UpstreamTypeRest,
|
||||
EnablePathRewrite: false,
|
||||
PathRewritePrefix: "",
|
||||
},
|
||||
},
|
||||
changed: true,
|
||||
expect: []*McpServer{
|
||||
{
|
||||
Name: "test1",
|
||||
Domains: nil,
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test1",
|
||||
UpstreamType: UpstreamTypeRest,
|
||||
EnablePathRewrite: false,
|
||||
PathRewritePrefix: "",
|
||||
},
|
||||
{
|
||||
Name: "test2",
|
||||
Domains: []string{"www.foo.com"},
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test2",
|
||||
UpstreamType: UpstreamTypeSSE,
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/test",
|
||||
},
|
||||
{
|
||||
Name: "test3-1",
|
||||
Domains: []string{"www.bar.com"},
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test3",
|
||||
UpstreamType: UpstreamTypeStreamable,
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "non-nil to non-nil (length unchanged + non-name field changed)",
|
||||
init: []*McpServer{
|
||||
{
|
||||
Name: "test2",
|
||||
Domains: []string{"www.foo.com"},
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test2",
|
||||
UpstreamType: UpstreamTypeSSE,
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/test",
|
||||
},
|
||||
{
|
||||
Name: "test3",
|
||||
Domains: []string{"www.bar.com"},
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test3",
|
||||
UpstreamType: UpstreamTypeStreamable,
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/",
|
||||
},
|
||||
{
|
||||
Name: "test1",
|
||||
Domains: nil,
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test1",
|
||||
UpstreamType: UpstreamTypeRest,
|
||||
EnablePathRewrite: false,
|
||||
PathRewritePrefix: "",
|
||||
},
|
||||
},
|
||||
input: []*McpServer{
|
||||
{
|
||||
Name: "test2",
|
||||
Domains: []string{"www.foo.com"},
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test2",
|
||||
UpstreamType: UpstreamTypeSSE,
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/test",
|
||||
},
|
||||
{
|
||||
Name: "test3",
|
||||
Domains: []string{"www.bar-2.com"},
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test4",
|
||||
UpstreamType: UpstreamTypeStreamable,
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/",
|
||||
},
|
||||
{
|
||||
Name: "test1",
|
||||
Domains: nil,
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test1",
|
||||
UpstreamType: UpstreamTypeRest,
|
||||
EnablePathRewrite: false,
|
||||
PathRewritePrefix: "",
|
||||
},
|
||||
},
|
||||
changed: true,
|
||||
expect: []*McpServer{
|
||||
{
|
||||
Name: "test1",
|
||||
Domains: nil,
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test1",
|
||||
UpstreamType: UpstreamTypeRest,
|
||||
EnablePathRewrite: false,
|
||||
PathRewritePrefix: "",
|
||||
},
|
||||
{
|
||||
Name: "test2",
|
||||
Domains: []string{"www.foo.com"},
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test2",
|
||||
UpstreamType: UpstreamTypeSSE,
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/test",
|
||||
},
|
||||
{
|
||||
Name: "test3",
|
||||
Domains: []string{"www.bar-2.com"},
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test4",
|
||||
UpstreamType: UpstreamTypeStreamable,
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "non-nil to non-nil (content unchanged + order unchanged)",
|
||||
init: []*McpServer{
|
||||
{
|
||||
Name: "test2",
|
||||
Domains: []string{"www.foo.com"},
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test2",
|
||||
UpstreamType: UpstreamTypeSSE,
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/test",
|
||||
},
|
||||
{
|
||||
Name: "test3",
|
||||
Domains: []string{"www.bar.com"},
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test3",
|
||||
UpstreamType: UpstreamTypeStreamable,
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/",
|
||||
},
|
||||
{
|
||||
Name: "test1",
|
||||
Domains: nil,
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test1",
|
||||
UpstreamType: UpstreamTypeRest,
|
||||
EnablePathRewrite: false,
|
||||
PathRewritePrefix: "",
|
||||
},
|
||||
},
|
||||
input: []*McpServer{
|
||||
{
|
||||
Name: "test2",
|
||||
Domains: []string{"www.foo.com"},
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test2",
|
||||
UpstreamType: UpstreamTypeSSE,
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/test",
|
||||
},
|
||||
{
|
||||
Name: "test3",
|
||||
Domains: []string{"www.bar.com"},
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test3",
|
||||
UpstreamType: UpstreamTypeStreamable,
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/",
|
||||
},
|
||||
{
|
||||
Name: "test1",
|
||||
Domains: nil,
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test1",
|
||||
UpstreamType: UpstreamTypeRest,
|
||||
EnablePathRewrite: false,
|
||||
PathRewritePrefix: "",
|
||||
},
|
||||
},
|
||||
changed: false,
|
||||
expect: []*McpServer{
|
||||
{
|
||||
Name: "test1",
|
||||
Domains: nil,
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test1",
|
||||
UpstreamType: UpstreamTypeRest,
|
||||
EnablePathRewrite: false,
|
||||
PathRewritePrefix: "",
|
||||
},
|
||||
{
|
||||
Name: "test2",
|
||||
Domains: []string{"www.foo.com"},
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test2",
|
||||
UpstreamType: UpstreamTypeSSE,
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/test",
|
||||
},
|
||||
{
|
||||
Name: "test3",
|
||||
Domains: []string{"www.bar.com"},
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test3",
|
||||
UpstreamType: UpstreamTypeStreamable,
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "non-nil to non-nil (content unchanged + order changed)",
|
||||
init: []*McpServer{
|
||||
{
|
||||
Name: "test2",
|
||||
Domains: []string{"www.foo.com"},
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test2",
|
||||
UpstreamType: UpstreamTypeSSE,
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/test",
|
||||
},
|
||||
{
|
||||
Name: "test3",
|
||||
Domains: []string{"www.bar.com"},
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test3",
|
||||
UpstreamType: UpstreamTypeStreamable,
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/",
|
||||
},
|
||||
{
|
||||
Name: "test1",
|
||||
Domains: nil,
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test1",
|
||||
UpstreamType: UpstreamTypeRest,
|
||||
EnablePathRewrite: false,
|
||||
PathRewritePrefix: "",
|
||||
},
|
||||
},
|
||||
input: []*McpServer{
|
||||
{
|
||||
Name: "test3",
|
||||
Domains: []string{"www.bar.com"},
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test3",
|
||||
UpstreamType: UpstreamTypeStreamable,
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/",
|
||||
},
|
||||
{
|
||||
Name: "test1",
|
||||
Domains: nil,
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test1",
|
||||
UpstreamType: UpstreamTypeRest,
|
||||
EnablePathRewrite: false,
|
||||
PathRewritePrefix: "",
|
||||
},
|
||||
{
|
||||
Name: "test2",
|
||||
Domains: []string{"www.foo.com"},
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test2",
|
||||
UpstreamType: UpstreamTypeSSE,
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/test",
|
||||
},
|
||||
},
|
||||
changed: false,
|
||||
expect: []*McpServer{
|
||||
{
|
||||
Name: "test1",
|
||||
Domains: nil,
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test1",
|
||||
UpstreamType: UpstreamTypeRest,
|
||||
EnablePathRewrite: false,
|
||||
PathRewritePrefix: "",
|
||||
},
|
||||
{
|
||||
Name: "test2",
|
||||
Domains: []string{"www.foo.com"},
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test2",
|
||||
UpstreamType: UpstreamTypeSSE,
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/test",
|
||||
},
|
||||
{
|
||||
Name: "test3",
|
||||
Domains: []string{"www.bar.com"},
|
||||
PathMatchType: ExactMatchType,
|
||||
PathMatchValue: "/mcp/test3",
|
||||
UpstreamType: UpstreamTypeStreamable,
|
||||
EnablePathRewrite: true,
|
||||
PathRewritePrefix: "/",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range testCases {
|
||||
if tt.skip {
|
||||
continue
|
||||
}
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
provider := &McpServerCache{}
|
||||
|
||||
if provider.GetMcpServers() != nil {
|
||||
t.Fatalf("GetMcpServers doesn't return nil before testing.")
|
||||
}
|
||||
|
||||
_ = provider.SetMcpServers(tt.init)
|
||||
|
||||
changed := provider.SetMcpServers(tt.input)
|
||||
if changed != tt.changed {
|
||||
t.Fatalf("actual changed %t != expect changed %t", changed, tt.changed)
|
||||
return
|
||||
}
|
||||
|
||||
actual := provider.GetMcpServers()
|
||||
|
||||
if len(actual) != len(tt.expect) {
|
||||
t.Fatalf("actual length %d != expect length %d", len(actual), len(tt.expect))
|
||||
}
|
||||
for i := range actual {
|
||||
if diff := cmp.Diff(tt.expect[i], actual[i]); diff != "" {
|
||||
t.Fatalf("TestMcpServerCache_GetSet() mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.23-bullseye AS golang-base
|
||||
FROM golang:1.22-bullseye AS golang-base
|
||||
|
||||
ARG GOPROXY
|
||||
ARG GO_FILTER_NAME
|
||||
@@ -24,7 +24,7 @@ WORKDIR /workspace
|
||||
|
||||
COPY . .
|
||||
|
||||
WORKDIR /workspace/$GO_FILTER_NAME
|
||||
WORKDIR /workspace
|
||||
|
||||
RUN go mod tidy
|
||||
RUN if [ "$GOARCH" = "arm64" ]; then \
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
GO_FILTER_NAME ?= mcp-server
|
||||
GO_FILTER_NAME ?= golang-filter
|
||||
GOPROXY := $(shell go env GOPROXY)
|
||||
GOARCH ?= amd64
|
||||
|
||||
@@ -8,5 +8,5 @@ build:
|
||||
--build-arg GO_FILTER_NAME=${GO_FILTER_NAME} \
|
||||
--build-arg GOARCH=${GOARCH} \
|
||||
-t ${GO_FILTER_NAME} \
|
||||
--output ./${GO_FILTER_NAME} \
|
||||
--output . \
|
||||
.
|
||||
@@ -20,28 +20,42 @@ Golang HTTP Filter 允许开发者使用 Go 语言编写自定义的 Envoy Filte
|
||||
|
||||
请参考 [Envoy Golang HTTP Filter 示例](https://github.com/envoyproxy/examples/tree/main/golang-http) 了解如何开发和运行一个基本的 Golang Filter。
|
||||
|
||||
## 插件注册
|
||||
|
||||
在开发新的 Golang Filter 时,需要在`main.go` 的 `init()` 函数中注册你的插件。注册时需要提供插件名称、Filter 工厂函数和配置解析器:
|
||||
|
||||
```go
|
||||
func init() {
|
||||
envoyHttp.RegisterHttpFilterFactoryAndConfigParser(
|
||||
"your-plugin-name", // 插件名称
|
||||
yourFilterFactory, // Filter 工厂函数
|
||||
&yourConfigParser{}, // 配置解析器
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## 配置示例
|
||||
|
||||
多个 Golang Filter 插件可以共同编译到一个 `golang-filter.so` 文件中,通过 `plugin_name` 来指定要使用的插件。配置示例如下:
|
||||
|
||||
```yaml
|
||||
http_filters:
|
||||
- name: envoy.filters.http.golang
|
||||
typed_config:
|
||||
"@type": type.googleapis.com/envoy.extensions.filters.http.golang.v3alpha.Config
|
||||
library_id: my-go-filter
|
||||
library_path: "./my-go-filter.so"
|
||||
plugin_name: my-go-filter
|
||||
library_id: your-plugin-name
|
||||
library_path: "./golang-filter.so" # 包含多个插件的共享库文件
|
||||
plugin_name: your-plugin-name # 指定要使用的插件名称,需要与 init() 函数中注册的插件名称保持一致
|
||||
plugin_config:
|
||||
"@type": type.googleapis.com/xds.type.v3.TypedStruct
|
||||
value:
|
||||
your_config_here: value
|
||||
|
||||
```
|
||||
|
||||
|
||||
## 快速构建
|
||||
|
||||
使用以下命令可以快速构建 golang filter 插件:
|
||||
|
||||
```bash
|
||||
GO_FILTER_NAME=mcp-server make build
|
||||
make build
|
||||
```
|
||||
|
||||
@@ -20,16 +20,32 @@ The Golang HTTP Filter allows developers to write custom Envoy Filters using the
|
||||
|
||||
Please refer to [Envoy Golang HTTP Filter Example](https://github.com/envoyproxy/examples/tree/main/golang-http) to learn how to develop and run a basic Golang Filter.
|
||||
|
||||
## Plugin Registration
|
||||
|
||||
When developing a new Golang Filter, you need to register your plugin in the `init()` function of `main.go`. The registration requires a plugin name, Filter factory function, and configuration parser:
|
||||
|
||||
```go
|
||||
func init() {
|
||||
envoyHttp.RegisterHttpFilterFactoryAndConfigParser(
|
||||
"your-plugin-name", // Plugin name
|
||||
yourFilterFactory, // Filter factory function
|
||||
&yourConfigParser{}, // Configuration parser
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration Example
|
||||
|
||||
Multiple Golang Filter plugins can be compiled into a single `golang-filter.so` file, and the desired plugin can be specified using `plugin_name`. Here's an example configuration:
|
||||
|
||||
```yaml
|
||||
http_filters:
|
||||
- name: envoy.filters.http.golang
|
||||
typed_config:
|
||||
"@type": type.googleapis.com/envoy.extensions.filters.http.golang.v3alpha.Config
|
||||
library_id: my-go-filter
|
||||
library_path: "./my-go-filter.so"
|
||||
plugin_name: my-go-filter
|
||||
library_id: your-plugin-name
|
||||
library_path: "./golang-filter.so" # Shared library file containing multiple plugins
|
||||
plugin_name: your-plugin-name # Specify which plugin to use, must match the name registered in init()
|
||||
plugin_config:
|
||||
"@type": type.googleapis.com/xds.type.v3.TypedStruct
|
||||
value:
|
||||
@@ -41,5 +57,5 @@ http_filters:
|
||||
Use the following command to quickly build the golang filter plugin:
|
||||
|
||||
```bash
|
||||
GO_FILTER_NAME=mcp-server make build
|
||||
make build
|
||||
```
|
||||
@@ -1,6 +1,10 @@
|
||||
module github.com/alibaba/higress/plugins/golang-filter/mcp-server
|
||||
module github.com/alibaba/higress/plugins/golang-filter
|
||||
|
||||
go 1.23
|
||||
go 1.22
|
||||
|
||||
replace github.com/envoyproxy/envoy => github.com/higress-group/envoy v0.0.0-20250430151331-2c556780b65c
|
||||
|
||||
replace github.com/mark3labs/mcp-go => github.com/higress-group/mcp-go v0.0.0-20250428145706-792ce64b4b30
|
||||
|
||||
require (
|
||||
github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42
|
||||
@@ -136,8 +136,6 @@ github.com/deckarep/golang-set v1.7.1 h1:SCQV0S6gTtp6itiFrTqI+pfmJ4LN85S1YzhDf9r
|
||||
github.com/deckarep/golang-set v1.7.1/go.mod h1:93vsz/8Wt4joVM7c2AVqh+YRMiUSc14yDtF28KmMOgQ=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/envoyproxy/envoy v1.33.1-0.20250325161043-11ab50a29d99 h1:jih/Ieb7BFgVCStgvY5fXQ3mI9ByOt4wfwUF0d7qmqI=
|
||||
github.com/envoyproxy/envoy v1.33.1-0.20250325161043-11ab50a29d99/go.mod h1:x7d0dNbE0xGuDBUkBg19VGCgnPQ+lJ2k8lDzDzKExow=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
@@ -236,6 +234,10 @@ github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mO
|
||||
github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/higress-group/envoy v0.0.0-20250430151331-2c556780b65c h1:chAOZk/qEXFhLILWoNucj3X6r9xYnRR+SWFvhsOa2oo=
|
||||
github.com/higress-group/envoy v0.0.0-20250430151331-2c556780b65c/go.mod h1:SU+IJUAfh1kkZtH+u0E1dnwho8AhbGeYMgp5vvjU+Gc=
|
||||
github.com/higress-group/mcp-go v0.0.0-20250428145706-792ce64b4b30 h1:N4NMq8M1nZyyChPyzn+EUUdHi5asig2uLR5hOyRmsXI=
|
||||
github.com/higress-group/mcp-go v0.0.0-20250428145706-792ce64b4b30/go.mod h1:O9gri9UOzthw728vusc2oNu99lVh8cKCajpxNfC90gE=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
@@ -283,8 +285,6 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/luoxiner/nacos-sdk-go/v2 v2.2.9-40 h1:nzRTBplC0riQqQwEHZThw5H4/TH5LgWTQTm6A7t1lpY=
|
||||
github.com/luoxiner/nacos-sdk-go/v2 v2.2.9-40/go.mod h1:9FKXl6FqOiVmm72i8kADtbeK71egyG9y3uRDBg41tpQ=
|
||||
github.com/mark3labs/mcp-go v0.12.0 h1:Pue1Tdwqcz77GHq18uzgmLT3wmeDUxXUSAqSwhGLhVo=
|
||||
github.com/mark3labs/mcp-go v0.12.0/go.mod h1:cjMlBU0cv/cj9kjlgmRhoJ5JREdS7YX83xeIG9Ko/jE=
|
||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
25
plugins/golang-filter/main.go
Normal file
25
plugins/golang-filter/main.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
mcp_server "github.com/alibaba/higress/plugins/golang-filter/mcp-server"
|
||||
mcp_session "github.com/alibaba/higress/plugins/golang-filter/mcp-session"
|
||||
"github.com/envoyproxy/envoy/contrib/golang/common/go/api"
|
||||
envoyHttp "github.com/envoyproxy/envoy/contrib/golang/filters/http/source/go/pkg/http"
|
||||
)
|
||||
|
||||
func init() {
|
||||
envoyHttp.RegisterHttpFilterFactoryAndConfigParser(mcp_session.Name, mcp_session.FilterFactory, &mcp_session.Parser{})
|
||||
envoyHttp.RegisterHttpFilterFactoryAndConfigParser(mcp_server.Name, mcp_server.FilterFactory, &mcp_server.Parser{})
|
||||
go func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
api.LogErrorf("PProf server recovered from panic: %v", r)
|
||||
}
|
||||
}()
|
||||
api.LogError(http.ListenAndServe("localhost:6060", nil).Error())
|
||||
}()
|
||||
}
|
||||
|
||||
func main() {}
|
||||
@@ -3,27 +3,22 @@
|
||||
|
||||
## 概述
|
||||
|
||||
MCP Server 是一个基于 Envoy 的 Golang Filter 插件,用于实现服务器端事件(SSE)和消息通信功能。该插件支持多种数据库类型,并使用 Redis 作为消息队列来实现负载均衡的请求通过对应的SSE连接发送。
|
||||
MCP Server 是一个基于 Envoy 的 Golang Filter 插件,提供了统一的 MCP (Model Context Protocol) 服务接口。它支持多种后端服务的集成,包括:
|
||||
|
||||
> **注意**:MCP Server需要 Higress 2.1.0 或更高版本才能使用。
|
||||
## 项目结构
|
||||
```
|
||||
mcp-server/
|
||||
├── config.go # 配置解析相关代码
|
||||
├── filter.go # 请求处理相关代码
|
||||
├── internal/ # 内部实现逻辑
|
||||
├── servers/ # MCP 服务器实现
|
||||
├── go.mod # Go模块依赖定义
|
||||
└── go.sum # Go模块依赖校验
|
||||
```
|
||||
## MCP Server开发指南
|
||||
- 数据库服务:通过 GORM 支持多种数据库的访问和管理
|
||||
- 配置中心:支持 Nacos 配置中心的集成
|
||||
- 可扩展性:支持自定义服务器实现,方便集成其他服务
|
||||
|
||||
> **注意**:MCP Server 需要 Higress 2.1.0 或更高版本才能使用。
|
||||
|
||||
## MCP Server 开发指南
|
||||
|
||||
```go
|
||||
// 在init函数中注册你的服务器
|
||||
// 参数1: 服务器名称
|
||||
// 参数2: 配置结构体实例
|
||||
func init() {
|
||||
internal.GlobalRegistry.RegisterServer("demo", &DemoConfig{})
|
||||
common.GlobalRegistry.RegisterServer("demo", &DemoConfig{})
|
||||
}
|
||||
|
||||
// 服务器配置结构体
|
||||
@@ -43,8 +38,8 @@ func (c *DBConfig) ParseConfig(config map[string]any) error {
|
||||
// 创建新的MCP服务器实例
|
||||
// serverName: 服务器名称
|
||||
// 返回值: MCP服务器实例和可能的错误
|
||||
func (c *DBConfig) NewServer(serverName string) (*internal.MCPServer, error) {
|
||||
mcpServer := internal.NewMCPServer(serverName, Version)
|
||||
func (c *DBConfig) NewServer(serverName string) (*common.MCPServer, error) {
|
||||
mcpServer := common.NewMCPServer(serverName, Version)
|
||||
|
||||
// 添加工具方法到服务器
|
||||
// mcpServer.AddTool()
|
||||
|
||||
@@ -3,29 +3,22 @@ English | [简体中文](./README.md)
|
||||
|
||||
## Overview
|
||||
|
||||
MCP Server is a Golang Filter plugin based on Envoy, designed to implement Server-Sent Events (SSE) and message communication functionality. This plugin supports various database types and uses Redis as a message queue to enable load-balanced requests to be sent through corresponding SSE connections.
|
||||
MCP Server is a Golang Filter plugin based on Envoy that provides a unified MCP (Model Context Protocol) service interface. It supports integration with various backend services, including:
|
||||
|
||||
> **Note**: MCP Server requires Higress 2.1.0 or higher version.
|
||||
- Database Services: Supports multiple database access and management through GORM
|
||||
- Configuration Service: Supports integration with Nacos configuration service
|
||||
- Extensibility: Supports custom server implementations for easy integration with other services
|
||||
|
||||
## Project Structure
|
||||
```
|
||||
mcp-server/
|
||||
├── config.go # Configuration parsing code
|
||||
├── filter.go # Request processing code
|
||||
├── internal/ # Internal implementation logic
|
||||
├── servers/ # MCP server implementation
|
||||
├── go.mod # Go module dependency definition
|
||||
└── go.sum # Go module dependency checksum
|
||||
```
|
||||
> **Note**: MCP Server requires Higress version 2.1.0 or higher to be used.
|
||||
|
||||
## MCP Server Development Guide
|
||||
|
||||
```go
|
||||
// Register your server in the init function
|
||||
// Param 1: Server name
|
||||
// Param 2: Config struct instance
|
||||
// Parameter 1: Server name
|
||||
// Parameter 2: Configuration struct instance
|
||||
func init() {
|
||||
internal.GlobalRegistry.RegisterServer("demo", &DemoConfig{})
|
||||
common.GlobalRegistry.RegisterServer("demo", &DemoConfig{})
|
||||
}
|
||||
|
||||
// Server configuration struct
|
||||
@@ -33,7 +26,7 @@ type DemoConfig struct {
|
||||
helloworld string
|
||||
}
|
||||
|
||||
// Configuration parsing method
|
||||
// Parse configuration method
|
||||
// Parse and validate configuration items from the config map
|
||||
func (c *DBConfig) ParseConfig(config map[string]any) error {
|
||||
helloworld, ok := config["helloworld"].(string)
|
||||
@@ -45,13 +38,13 @@ func (c *DBConfig) ParseConfig(config map[string]any) error {
|
||||
// Create a new MCP server instance
|
||||
// serverName: Server name
|
||||
// Returns: MCP server instance and possible error
|
||||
func (c *DBConfig) NewServer(serverName string) (*internal.MCPServer, error) {
|
||||
mcpServer := internal.NewMCPServer(serverName, Version)
|
||||
func (c *DBConfig) NewServer(serverName string) (*common.MCPServer, error) {
|
||||
mcpServer := common.NewMCPServer(serverName, Version)
|
||||
|
||||
// Add tool methods to server
|
||||
// Add tool methods to the server
|
||||
// mcpServer.AddTool()
|
||||
|
||||
// Add resources to server
|
||||
// Add resources to the server
|
||||
// mcpServer.AddResource()
|
||||
|
||||
return mcpServer, nil
|
||||
@@ -59,7 +52,7 @@ func (c *DBConfig) NewServer(serverName string) (*internal.MCPServer, error) {
|
||||
```
|
||||
|
||||
**Note**:
|
||||
Need to use underscore import in config.go to execute the package's init function
|
||||
You need to use underscore imports in config.go to execute the package's init function
|
||||
```go
|
||||
import (
|
||||
_ "github.com/alibaba/higress/plugins/golang-filter/mcp-server/servers/gorm"
|
||||
|
||||
@@ -1,64 +1,39 @@
|
||||
package main
|
||||
package mcp_server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"net/http"
|
||||
_ "net/http/pprof"
|
||||
|
||||
xds "github.com/cncf/xds/go/xds/type/v3"
|
||||
"google.golang.org/protobuf/types/known/anypb"
|
||||
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/handler"
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/internal"
|
||||
_ "github.com/alibaba/higress/plugins/golang-filter/mcp-server/registry/nacos"
|
||||
_ "github.com/alibaba/higress/plugins/golang-filter/mcp-server/servers/gorm"
|
||||
mcp_session "github.com/alibaba/higress/plugins/golang-filter/mcp-session"
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-session/common"
|
||||
xds "github.com/cncf/xds/go/xds/type/v3"
|
||||
"github.com/envoyproxy/envoy/contrib/golang/common/go/api"
|
||||
envoyHttp "github.com/envoyproxy/envoy/contrib/golang/filters/http/source/go/pkg/http"
|
||||
"google.golang.org/protobuf/types/known/anypb"
|
||||
)
|
||||
|
||||
const Name = "mcp-session"
|
||||
const Name = "mcp-server"
|
||||
const Version = "1.0.0"
|
||||
const DefaultServerName = "defaultServer"
|
||||
const ConfigPathSuffix = "/config"
|
||||
|
||||
func init() {
|
||||
envoyHttp.RegisterHttpFilterFactoryAndConfigParser(Name, filterFactory, &parser{})
|
||||
go func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
api.LogErrorf("PProf server recovered from panic: %v", r)
|
||||
}
|
||||
}()
|
||||
api.LogError(http.ListenAndServe("localhost:6060", nil).Error())
|
||||
}()
|
||||
type SSEServerWrapper struct {
|
||||
BaseServer *common.SSEServer
|
||||
DomainList []string
|
||||
}
|
||||
|
||||
type config struct {
|
||||
ssePathSuffix string
|
||||
redisClient *internal.RedisClient
|
||||
servers []*internal.SSEServer
|
||||
defaultServer *internal.SSEServer
|
||||
matchList []internal.MatchRule
|
||||
enableUserLevelServer bool
|
||||
rateLimitConfig *handler.MCPRatelimitConfig
|
||||
servers []*SSEServerWrapper
|
||||
}
|
||||
|
||||
func (c *config) Destroy() {
|
||||
if c.redisClient != nil {
|
||||
api.LogDebug("Closing Redis client")
|
||||
c.redisClient.Close()
|
||||
}
|
||||
for _, server := range c.servers {
|
||||
server.Close()
|
||||
server.BaseServer.Close()
|
||||
}
|
||||
}
|
||||
|
||||
type parser struct {
|
||||
type Parser struct {
|
||||
}
|
||||
|
||||
// Parse the filter configuration
|
||||
func (p *parser) Parse(any *anypb.Any, callbacks api.ConfigCallbackHandler) (interface{}, error) {
|
||||
func (p *Parser) Parse(any *anypb.Any, callbacks api.ConfigCallbackHandler) (interface{}, error) {
|
||||
configStruct := &xds.TypedStruct{}
|
||||
if err := any.UnmarshalTo(configStruct); err != nil {
|
||||
return nil, err
|
||||
@@ -66,82 +41,9 @@ func (p *parser) Parse(any *anypb.Any, callbacks api.ConfigCallbackHandler) (int
|
||||
v := configStruct.Value
|
||||
|
||||
conf := &config{
|
||||
matchList: make([]internal.MatchRule, 0),
|
||||
servers: make([]*internal.SSEServer, 0),
|
||||
servers: make([]*SSEServerWrapper, 0),
|
||||
}
|
||||
|
||||
// Parse match_list if exists
|
||||
if matchList, ok := v.AsMap()["match_list"].([]interface{}); ok {
|
||||
for _, item := range matchList {
|
||||
if ruleMap, ok := item.(map[string]interface{}); ok {
|
||||
rule := internal.MatchRule{}
|
||||
if domain, ok := ruleMap["match_rule_domain"].(string); ok {
|
||||
rule.MatchRuleDomain = domain
|
||||
}
|
||||
if path, ok := ruleMap["match_rule_path"].(string); ok {
|
||||
rule.MatchRulePath = path
|
||||
}
|
||||
if ruleType, ok := ruleMap["match_rule_type"].(string); ok {
|
||||
rule.MatchRuleType = internal.RuleType(ruleType)
|
||||
}
|
||||
conf.matchList = append(conf.matchList, rule)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Redis configuration is optional
|
||||
if redisConfigMap, ok := v.AsMap()["redis"].(map[string]interface{}); ok {
|
||||
redisConfig, err := internal.ParseRedisConfig(redisConfigMap)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse redis config: %w", err)
|
||||
}
|
||||
|
||||
redisClient, err := internal.NewRedisClient(redisConfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to initialize RedisClient: %w", err)
|
||||
}
|
||||
conf.redisClient = redisClient
|
||||
api.LogDebug("Redis client initialized")
|
||||
} else {
|
||||
api.LogDebug("Redis configuration not provided, running without Redis")
|
||||
}
|
||||
|
||||
enableUserLevelServer, ok := v.AsMap()["enable_user_level_server"].(bool)
|
||||
if !ok {
|
||||
enableUserLevelServer = false
|
||||
if conf.redisClient == nil {
|
||||
return nil, fmt.Errorf("redis configuration is not provided, enable_user_level_server is true")
|
||||
}
|
||||
}
|
||||
conf.enableUserLevelServer = enableUserLevelServer
|
||||
|
||||
if rateLimit, ok := v.AsMap()["rate_limit"].(map[string]interface{}); ok {
|
||||
rateLimitConfig := &handler.MCPRatelimitConfig{}
|
||||
if limit, ok := rateLimit["limit"].(float64); ok {
|
||||
rateLimitConfig.Limit = int(limit)
|
||||
}
|
||||
if window, ok := rateLimit["window"].(float64); ok {
|
||||
rateLimitConfig.Window = int(window)
|
||||
}
|
||||
if whiteList, ok := rateLimit["white_list"].([]interface{}); ok {
|
||||
for _, item := range whiteList {
|
||||
if uid, ok := item.(string); ok {
|
||||
rateLimitConfig.Whitelist = append(rateLimitConfig.Whitelist, uid)
|
||||
}
|
||||
}
|
||||
}
|
||||
if errorText, ok := rateLimit["error_text"].(string); ok {
|
||||
rateLimitConfig.ErrorText = errorText
|
||||
}
|
||||
conf.rateLimitConfig = rateLimitConfig
|
||||
}
|
||||
|
||||
ssePathSuffix, ok := v.AsMap()["sse_path_suffix"].(string)
|
||||
if !ok || ssePathSuffix == "" {
|
||||
return nil, fmt.Errorf("sse path suffix is not set or empty")
|
||||
}
|
||||
conf.ssePathSuffix = ssePathSuffix
|
||||
|
||||
serverConfigs, ok := v.AsMap()["servers"].([]interface{})
|
||||
if !ok {
|
||||
api.LogDebug("No servers are configured")
|
||||
@@ -153,19 +55,33 @@ func (p *parser) Parse(any *anypb.Any, callbacks api.ConfigCallbackHandler) (int
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("server config must be an object")
|
||||
}
|
||||
|
||||
serverType, ok := serverConfigMap["type"].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("server type is not set")
|
||||
}
|
||||
|
||||
serverPath, ok := serverConfigMap["path"].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("server %s path is not set", serverType)
|
||||
}
|
||||
|
||||
serverDomainList := []string{}
|
||||
if domainList, ok := serverConfigMap["domain_list"].([]interface{}); ok {
|
||||
for _, domain := range domainList {
|
||||
if domainStr, ok := domain.(string); ok {
|
||||
serverDomainList = append(serverDomainList, domainStr)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
serverDomainList = []string{"*"}
|
||||
}
|
||||
|
||||
serverName, ok := serverConfigMap["name"].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("server %s name is not set", serverType)
|
||||
}
|
||||
server := internal.GlobalRegistry.GetServer(serverType)
|
||||
server := common.GlobalRegistry.GetServer(serverType)
|
||||
|
||||
if server == nil {
|
||||
return nil, fmt.Errorf("server %s is not registered", serverType)
|
||||
@@ -186,50 +102,36 @@ func (p *parser) Parse(any *anypb.Any, callbacks api.ConfigCallbackHandler) (int
|
||||
return nil, fmt.Errorf("failed to initialize DBServer: %w", err)
|
||||
}
|
||||
|
||||
conf.servers = append(conf.servers, internal.NewSSEServer(serverInstance,
|
||||
internal.WithRedisClient(conf.redisClient),
|
||||
internal.WithSSEEndpoint(fmt.Sprintf("%s%s", serverPath, ssePathSuffix)),
|
||||
internal.WithMessageEndpoint(serverPath)))
|
||||
conf.servers = append(conf.servers, &SSEServerWrapper{
|
||||
BaseServer: common.NewSSEServer(serverInstance,
|
||||
common.WithSSEEndpoint(fmt.Sprintf("%s%s", serverPath, mcp_session.GlobalSSEPathSuffix)),
|
||||
common.WithMessageEndpoint(serverPath)),
|
||||
DomainList: serverDomainList,
|
||||
})
|
||||
api.LogDebug(fmt.Sprintf("Registered MCP Server: %s", serverType))
|
||||
}
|
||||
|
||||
return conf, nil
|
||||
}
|
||||
|
||||
func (p *parser) Merge(parent interface{}, child interface{}) interface{} {
|
||||
func (p *Parser) Merge(parent interface{}, child interface{}) interface{} {
|
||||
parentConfig := parent.(*config)
|
||||
childConfig := child.(*config)
|
||||
|
||||
newConfig := *parentConfig
|
||||
if childConfig.redisClient != nil {
|
||||
newConfig.redisClient = childConfig.redisClient
|
||||
}
|
||||
if childConfig.ssePathSuffix != "" {
|
||||
newConfig.ssePathSuffix = childConfig.ssePathSuffix
|
||||
}
|
||||
if childConfig.servers != nil {
|
||||
newConfig.servers = childConfig.servers
|
||||
}
|
||||
if childConfig.defaultServer != nil {
|
||||
newConfig.defaultServer = childConfig.defaultServer
|
||||
}
|
||||
if childConfig.matchList != nil {
|
||||
newConfig.matchList = childConfig.matchList
|
||||
}
|
||||
return &newConfig
|
||||
}
|
||||
|
||||
func filterFactory(c interface{}, callbacks api.FilterCallbackHandler) api.StreamFilter {
|
||||
func FilterFactory(c interface{}, callbacks api.FilterCallbackHandler) api.StreamFilter {
|
||||
conf, ok := c.(*config)
|
||||
if !ok {
|
||||
panic("unexpected config type")
|
||||
}
|
||||
return &filter{
|
||||
callbacks: callbacks,
|
||||
config: conf,
|
||||
stopChan: make(chan struct{}),
|
||||
mcpConfigHandler: handler.NewMCPConfigHandler(conf.redisClient, callbacks),
|
||||
mcpRatelimitHandler: handler.NewMCPRatelimitHandler(conf.redisClient, callbacks, conf.rateLimitConfig),
|
||||
config: conf,
|
||||
callbacks: callbacks,
|
||||
}
|
||||
}
|
||||
|
||||
func main() {}
|
||||
|
||||
@@ -1,104 +1,44 @@
|
||||
package main
|
||||
package mcp_server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/handler"
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/internal"
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-session/common"
|
||||
"github.com/envoyproxy/envoy/contrib/golang/common/go/api"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
)
|
||||
|
||||
const (
|
||||
RedisNotEnabledResponseBody = "Redis is not enabled, SSE connection is not supported"
|
||||
)
|
||||
|
||||
// The callbacks in the filter, like `DecodeHeaders`, can be implemented on demand.
|
||||
// Because api.PassThroughStreamFilter provides a default implementation.
|
||||
type filter struct {
|
||||
api.PassThroughStreamFilter
|
||||
|
||||
callbacks api.FilterCallbackHandler
|
||||
path string
|
||||
config *config
|
||||
stopChan chan struct{}
|
||||
|
||||
req *http.Request
|
||||
serverName string
|
||||
message bool
|
||||
proxyURL *url.URL
|
||||
skip bool
|
||||
|
||||
userLevelConfig bool
|
||||
mcpConfigHandler *handler.MCPConfigHandler
|
||||
ratelimit bool
|
||||
mcpRatelimitHandler *handler.MCPRatelimitHandler
|
||||
config *config
|
||||
req *http.Request
|
||||
message bool
|
||||
path string
|
||||
}
|
||||
|
||||
type RequestURL struct {
|
||||
method string
|
||||
scheme string
|
||||
host string
|
||||
path string
|
||||
baseURL string
|
||||
parsedURL *url.URL
|
||||
internalIP bool
|
||||
}
|
||||
|
||||
func NewRequestURL(header api.RequestHeaderMap) *RequestURL {
|
||||
method, _ := header.Get(":method")
|
||||
scheme, _ := header.Get(":scheme")
|
||||
host, _ := header.Get(":authority")
|
||||
path, _ := header.Get(":path")
|
||||
internalIP, _ := header.Get("x-envoy-internal")
|
||||
baseURL := fmt.Sprintf("%s://%s", scheme, host)
|
||||
parsedURL, _ := url.Parse(path)
|
||||
api.LogDebugf("RequestURL: method=%s, scheme=%s, host=%s, path=%s", method, scheme, host, path)
|
||||
return &RequestURL{method: method, scheme: scheme, host: host, path: path, baseURL: baseURL, parsedURL: parsedURL, internalIP: internalIP == "true"}
|
||||
}
|
||||
|
||||
// Callbacks which are called in request path
|
||||
// The endStream is true if the request doesn't have body
|
||||
func (f *filter) DecodeHeaders(header api.RequestHeaderMap, endStream bool) api.StatusType {
|
||||
url := NewRequestURL(header)
|
||||
f.path = url.parsedURL.Path
|
||||
|
||||
// Check if request matches any rule in match_list
|
||||
if !internal.IsMatch(f.config.matchList, url.host, f.path) {
|
||||
f.skip = true
|
||||
api.LogDebugf("Request does not match any rule in match_list: %s", url.parsedURL.String())
|
||||
url := common.NewRequestURL(header)
|
||||
if url == nil {
|
||||
return api.Continue
|
||||
}
|
||||
f.path = url.ParsedURL.Path
|
||||
|
||||
for _, server := range f.config.servers {
|
||||
if f.path == server.GetSSEEndpoint() {
|
||||
if url.method != http.MethodGet {
|
||||
f.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusMethodNotAllowed, "Method not allowed", nil, 0, "")
|
||||
} else {
|
||||
f.serverName = server.GetServerName()
|
||||
body := "SSE connection create"
|
||||
f.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusOK, body, nil, 0, "")
|
||||
}
|
||||
api.LogDebugf("%s SSE connection started", server.GetServerName())
|
||||
return api.LocalReply
|
||||
} else if f.path == server.GetMessageEndpoint() {
|
||||
if url.method != http.MethodPost {
|
||||
if common.MatchDomainList(url.ParsedURL.Host, server.DomainList) && url.ParsedURL.Path == server.BaseServer.GetMessageEndpoint() {
|
||||
if url.Method != http.MethodPost {
|
||||
f.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusMethodNotAllowed, "Method not allowed", nil, 0, "")
|
||||
return api.LocalReply
|
||||
}
|
||||
// Create a new http.Request object
|
||||
f.req = &http.Request{
|
||||
Method: url.method,
|
||||
URL: url.parsedURL,
|
||||
Method: url.Method,
|
||||
URL: url.ParsedURL,
|
||||
Header: make(http.Header),
|
||||
}
|
||||
api.LogDebugf("Message request: %v", url.parsedURL)
|
||||
api.LogDebugf("Message request: %v", url.ParsedURL)
|
||||
// Copy headers from api.RequestHeaderMap to http.Header
|
||||
header.Range(func(key, value string) bool {
|
||||
f.req.Header.Add(key, value)
|
||||
@@ -113,209 +53,33 @@ func (f *filter) DecodeHeaders(header api.RequestHeaderMap, endStream bool) api.
|
||||
}
|
||||
}
|
||||
|
||||
f.req = &http.Request{
|
||||
Method: url.method,
|
||||
URL: url.parsedURL,
|
||||
}
|
||||
|
||||
if strings.HasSuffix(f.path, ConfigPathSuffix) && f.config.enableUserLevelServer {
|
||||
if !url.internalIP {
|
||||
api.LogWarnf("Access denied: non-internal IP address %s", url.parsedURL.String())
|
||||
f.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusForbidden, "", nil, 0, "")
|
||||
return api.LocalReply
|
||||
}
|
||||
if strings.HasSuffix(f.path, ConfigPathSuffix) && url.method == http.MethodGet {
|
||||
api.LogDebugf("Handling config request: %s", f.path)
|
||||
f.mcpConfigHandler.HandleConfigRequest(f.req, []byte{})
|
||||
return api.LocalReply
|
||||
}
|
||||
f.userLevelConfig = true
|
||||
if endStream {
|
||||
return api.Continue
|
||||
} else {
|
||||
return api.StopAndBuffer
|
||||
}
|
||||
}
|
||||
|
||||
if !strings.HasSuffix(url.parsedURL.Path, f.config.ssePathSuffix) {
|
||||
f.proxyURL = url.parsedURL
|
||||
if f.config.enableUserLevelServer {
|
||||
parts := strings.Split(url.parsedURL.Path, "/")
|
||||
if len(parts) >= 3 {
|
||||
serverName := parts[1]
|
||||
uid := parts[2]
|
||||
// Get encoded config
|
||||
encodedConfig, _ := f.mcpConfigHandler.GetEncodedConfig(serverName, uid)
|
||||
if encodedConfig != "" {
|
||||
header.Set("x-higress-mcpserver-config", encodedConfig)
|
||||
api.LogDebugf("Set x-higress-mcpserver-config Header for %s:%s", serverName, uid)
|
||||
}
|
||||
}
|
||||
f.ratelimit = true
|
||||
}
|
||||
if endStream {
|
||||
return api.Continue
|
||||
} else {
|
||||
return api.StopAndBuffer
|
||||
}
|
||||
}
|
||||
|
||||
if url.method != http.MethodGet {
|
||||
f.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusMethodNotAllowed, "Method not allowed", nil, 0, "")
|
||||
} else {
|
||||
f.config.defaultServer = internal.NewSSEServer(internal.NewMCPServer(DefaultServerName, Version),
|
||||
internal.WithSSEEndpoint(f.config.ssePathSuffix),
|
||||
internal.WithMessageEndpoint(strings.TrimSuffix(url.parsedURL.Path, f.config.ssePathSuffix)),
|
||||
internal.WithRedisClient(f.config.redisClient))
|
||||
f.serverName = f.config.defaultServer.GetServerName()
|
||||
body := "SSE connection create"
|
||||
f.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusOK, body, nil, 0, "")
|
||||
}
|
||||
return api.LocalReply
|
||||
return api.Continue
|
||||
}
|
||||
|
||||
// DecodeData might be called multiple times during handling the request body.
|
||||
// The endStream is true when handling the last piece of the body.
|
||||
func (f *filter) DecodeData(buffer api.BufferInstance, endStream bool) api.StatusType {
|
||||
if f.skip {
|
||||
return api.Continue
|
||||
}
|
||||
if !endStream {
|
||||
return api.StopAndBuffer
|
||||
}
|
||||
if f.message {
|
||||
for _, server := range f.config.servers {
|
||||
if f.path == server.GetMessageEndpoint() {
|
||||
if f.path == server.BaseServer.GetMessageEndpoint() {
|
||||
// Create a response recorder to capture the response
|
||||
recorder := httptest.NewRecorder()
|
||||
// Call the handleMessage method of SSEServer with complete body
|
||||
httpStatus := server.HandleMessage(recorder, f.req, buffer.Bytes())
|
||||
httpStatus := server.BaseServer.HandleMessage(recorder, f.req, buffer.Bytes())
|
||||
f.message = false
|
||||
f.callbacks.DecoderFilterCallbacks().SendLocalReply(httpStatus, recorder.Body.String(), recorder.Header(), 0, "")
|
||||
return api.LocalReply
|
||||
}
|
||||
}
|
||||
} else if f.userLevelConfig {
|
||||
// Handle config POST request
|
||||
api.LogDebugf("Handling config request: %s", f.path)
|
||||
f.mcpConfigHandler.HandleConfigRequest(f.req, buffer.Bytes())
|
||||
return api.LocalReply
|
||||
} else if f.ratelimit {
|
||||
if checkJSONRPCMethod(buffer.Bytes(), "tools/list") {
|
||||
api.LogDebugf("Not a tools call request, skipping ratelimit")
|
||||
return api.Continue
|
||||
}
|
||||
parts := strings.Split(f.req.URL.Path, "/")
|
||||
if len(parts) < 3 {
|
||||
api.LogWarnf("Access denied: no valid uid found")
|
||||
f.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusForbidden, "", nil, 0, "")
|
||||
return api.LocalReply
|
||||
}
|
||||
serverName := parts[1]
|
||||
uid := parts[2]
|
||||
encodedConfig, err := f.mcpConfigHandler.GetEncodedConfig(serverName, uid)
|
||||
if err != nil {
|
||||
api.LogWarnf("Access denied: no valid config found for uid %s", uid)
|
||||
f.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusForbidden, "", nil, 0, "")
|
||||
return api.LocalReply
|
||||
} else if encodedConfig == "" && checkJSONRPCMethod(buffer.Bytes(), "tools/call") {
|
||||
api.LogDebugf("Empty config found for %s:%s", serverName, uid)
|
||||
if !f.mcpRatelimitHandler.HandleRatelimit(f.req, buffer.Bytes()) {
|
||||
return api.LocalReply
|
||||
}
|
||||
}
|
||||
}
|
||||
return api.Continue
|
||||
}
|
||||
|
||||
// Callbacks which are called in response path
|
||||
// The endStream is true if the response doesn't have body
|
||||
func (f *filter) EncodeHeaders(header api.ResponseHeaderMap, endStream bool) api.StatusType {
|
||||
if f.skip {
|
||||
return api.Continue
|
||||
}
|
||||
if f.serverName != "" {
|
||||
if f.config.redisClient != nil {
|
||||
header.Set("Content-Type", "text/event-stream")
|
||||
header.Set("Cache-Control", "no-cache")
|
||||
header.Set("Connection", "keep-alive")
|
||||
header.Set("Access-Control-Allow-Origin", "*")
|
||||
header.Del("Content-Length")
|
||||
} else {
|
||||
header.Set("Content-Length", strconv.Itoa(len(RedisNotEnabledResponseBody)))
|
||||
}
|
||||
return api.Continue
|
||||
}
|
||||
return api.Continue
|
||||
}
|
||||
|
||||
// EncodeData might be called multiple times during handling the response body.
|
||||
// The endStream is true when handling the last piece of the body.
|
||||
func (f *filter) EncodeData(buffer api.BufferInstance, endStream bool) api.StatusType {
|
||||
if f.skip {
|
||||
return api.Continue
|
||||
}
|
||||
if !endStream {
|
||||
return api.StopAndBuffer
|
||||
}
|
||||
if f.proxyURL != nil && f.config.redisClient != nil {
|
||||
sessionID := f.proxyURL.Query().Get("sessionId")
|
||||
if sessionID != "" {
|
||||
channel := internal.GetSSEChannelName(sessionID)
|
||||
eventData := fmt.Sprintf("event: message\ndata: %s\n\n", buffer.String())
|
||||
publishErr := f.config.redisClient.Publish(channel, eventData)
|
||||
if publishErr != nil {
|
||||
api.LogErrorf("Failed to publish wasm mcp server message to Redis: %v", publishErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if f.serverName != "" {
|
||||
if f.config.redisClient != nil {
|
||||
// handle specific server
|
||||
for _, server := range f.config.servers {
|
||||
if f.serverName == server.GetServerName() {
|
||||
buffer.Reset()
|
||||
server.HandleSSE(f.callbacks, f.stopChan)
|
||||
return api.Running
|
||||
}
|
||||
}
|
||||
// handle default server
|
||||
if f.serverName == f.config.defaultServer.GetServerName() {
|
||||
buffer.Reset()
|
||||
f.config.defaultServer.HandleSSE(f.callbacks, f.stopChan)
|
||||
return api.Running
|
||||
}
|
||||
return api.Continue
|
||||
} else {
|
||||
buffer.SetString(RedisNotEnabledResponseBody)
|
||||
return api.Continue
|
||||
}
|
||||
}
|
||||
return api.Continue
|
||||
}
|
||||
|
||||
// OnDestroy stops the goroutine
|
||||
func (f *filter) OnDestroy(reason api.DestroyReason) {
|
||||
api.LogDebugf("OnDestroy: reason=%v", reason)
|
||||
if f.serverName != "" && f.stopChan != nil {
|
||||
select {
|
||||
case <-f.stopChan:
|
||||
return
|
||||
default:
|
||||
api.LogDebug("Stopping SSE connection")
|
||||
close(f.stopChan)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// check if the request is a tools/call request
|
||||
func checkJSONRPCMethod(body []byte, method string) bool {
|
||||
var request mcp.CallToolRequest
|
||||
if err := json.Unmarshal(body, &request); err != nil {
|
||||
api.LogWarnf("Failed to unmarshal request body: %v, not a JSON RPC request", err)
|
||||
return true
|
||||
}
|
||||
|
||||
return request.Method == method
|
||||
}
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// RuleType defines the type of matching rule
|
||||
type RuleType string
|
||||
|
||||
const (
|
||||
ExactMatch RuleType = "exact"
|
||||
PrefixMatch RuleType = "prefix"
|
||||
SuffixMatch RuleType = "suffix"
|
||||
ContainsMatch RuleType = "contains"
|
||||
RegexMatch RuleType = "regex"
|
||||
)
|
||||
|
||||
// MatchRule defines the structure for a matching rule
|
||||
type MatchRule struct {
|
||||
MatchRuleDomain string `json:"match_rule_domain"` // Domain pattern, supports wildcards
|
||||
MatchRulePath string `json:"match_rule_path"` // Path pattern to match
|
||||
MatchRuleType RuleType `json:"match_rule_type"` // Type of match rule
|
||||
}
|
||||
|
||||
// convertWildcardToRegex converts wildcard pattern to regex pattern
|
||||
func convertWildcardToRegex(pattern string) string {
|
||||
pattern = regexp.QuoteMeta(pattern)
|
||||
pattern = "^" + strings.ReplaceAll(pattern, "\\*", ".*") + "$"
|
||||
return pattern
|
||||
}
|
||||
|
||||
// matchPattern checks if the target matches the pattern based on rule type
|
||||
func matchPattern(pattern string, target string, ruleType RuleType) bool {
|
||||
if pattern == "" {
|
||||
return true
|
||||
}
|
||||
|
||||
switch ruleType {
|
||||
case ExactMatch:
|
||||
return pattern == target
|
||||
case PrefixMatch:
|
||||
return strings.HasPrefix(target, pattern)
|
||||
case SuffixMatch:
|
||||
return strings.HasSuffix(target, pattern)
|
||||
case ContainsMatch:
|
||||
return strings.Contains(target, pattern)
|
||||
case RegexMatch:
|
||||
matched, err := regexp.MatchString(pattern, target)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return matched
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// matchDomain checks if the domain matches the pattern
|
||||
func matchDomain(domain string, pattern string) bool {
|
||||
if pattern == "" || pattern == "*" {
|
||||
return true
|
||||
}
|
||||
// Convert wildcard pattern to regex pattern
|
||||
regexPattern := convertWildcardToRegex(pattern)
|
||||
matched, _ := regexp.MatchString(regexPattern, domain)
|
||||
return matched
|
||||
}
|
||||
|
||||
// matchDomainAndPath checks if both domain and path match the rule
|
||||
func matchDomainAndPath(domain, path string, rule MatchRule) bool {
|
||||
return matchDomain(domain, rule.MatchRuleDomain) &&
|
||||
matchPattern(rule.MatchRulePath, path, rule.MatchRuleType)
|
||||
}
|
||||
|
||||
// IsMatch checks if the request matches any rule in the rule list
|
||||
// Returns true if no rules are specified
|
||||
func IsMatch(rules []MatchRule, host, path string) bool {
|
||||
if len(rules) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, rule := range rules {
|
||||
if matchDomainAndPath(host, path, rule) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -5,8 +5,8 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/internal"
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/registry"
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-session/common"
|
||||
"github.com/envoyproxy/envoy/contrib/golang/common/go/api"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
"github.com/nacos-group/nacos-sdk-go/v2/clients"
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
)
|
||||
|
||||
func init() {
|
||||
internal.GlobalRegistry.RegisterServer("nacos-mcp-registry", &NacosConfig{})
|
||||
common.GlobalRegistry.RegisterServer("nacos-mcp-registry", &NacosConfig{})
|
||||
}
|
||||
|
||||
type NacosConfig struct {
|
||||
@@ -28,7 +28,7 @@ type NacosConfig struct {
|
||||
}
|
||||
|
||||
type McpServerToolsChangeListener struct {
|
||||
mcpServer *internal.MCPServer
|
||||
mcpServer *common.MCPServer
|
||||
}
|
||||
|
||||
func (l *McpServerToolsChangeListener) OnToolChanged(reg registry.McpServerRegistry) {
|
||||
@@ -137,8 +137,8 @@ func (c *NacosConfig) ParseConfig(config map[string]any) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *NacosConfig) NewServer(serverName string) (*internal.MCPServer, error) {
|
||||
mcpServer := internal.NewMCPServer(
|
||||
func (c *NacosConfig) NewServer(serverName string) (*common.MCPServer, error) {
|
||||
mcpServer := common.NewMCPServer(
|
||||
serverName,
|
||||
"1.0.0",
|
||||
)
|
||||
@@ -170,11 +170,11 @@ func (c *NacosConfig) NewServer(serverName string) (*internal.MCPServer, error)
|
||||
return mcpServer, nil
|
||||
}
|
||||
|
||||
func resetToolsToMcpServer(mcpServer *internal.MCPServer, reg registry.McpServerRegistry) {
|
||||
wrappedTools := []internal.ServerTool{}
|
||||
func resetToolsToMcpServer(mcpServer *common.MCPServer, reg registry.McpServerRegistry) {
|
||||
wrappedTools := []common.ServerTool{}
|
||||
tools := reg.ListToolsDesciption()
|
||||
for _, tool := range tools {
|
||||
wrappedTools = append(wrappedTools, internal.ServerTool{
|
||||
wrappedTools = append(wrappedTools, common.ServerTool{
|
||||
Tool: mcp.NewToolWithRawSchema(tool.Name, tool.Description, tool.InputSchema),
|
||||
Handler: registry.HandleRegistryToolsCall(reg),
|
||||
})
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/internal"
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-session/common"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
)
|
||||
|
||||
@@ -204,7 +204,7 @@ func CommonRemoteCall(reg McpServerRegistry, toolName string, parameters map[str
|
||||
return remoteHandle.HandleToolCall(ctx, parameters)
|
||||
}
|
||||
|
||||
func HandleRegistryToolsCall(reg McpServerRegistry) internal.ToolHandlerFunc {
|
||||
func HandleRegistryToolsCall(reg McpServerRegistry) common.ToolHandlerFunc {
|
||||
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
arguments := request.Params.Arguments
|
||||
return CommonRemoteCall(reg, request.Params.Name, arguments)
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/internal"
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-session/common"
|
||||
"github.com/envoyproxy/envoy/contrib/golang/common/go/api"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
)
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
const Version = "1.0.0"
|
||||
|
||||
func init() {
|
||||
internal.GlobalRegistry.RegisterServer("database", &DBConfig{})
|
||||
common.GlobalRegistry.RegisterServer("database", &DBConfig{})
|
||||
}
|
||||
|
||||
type DBConfig struct {
|
||||
@@ -41,11 +41,11 @@ func (c *DBConfig) ParseConfig(config map[string]any) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *DBConfig) NewServer(serverName string) (*internal.MCPServer, error) {
|
||||
mcpServer := internal.NewMCPServer(
|
||||
func (c *DBConfig) NewServer(serverName string) (*common.MCPServer, error) {
|
||||
mcpServer := common.NewMCPServer(
|
||||
serverName,
|
||||
Version,
|
||||
internal.WithInstructions(fmt.Sprintf("This is a %s database server", c.dbType)),
|
||||
common.WithInstructions(fmt.Sprintf("This is a %s database server", c.dbType)),
|
||||
)
|
||||
|
||||
dbClient := NewDBClient(c.dsn, c.dbType, mcpServer.GetDestoryChannel())
|
||||
|
||||
@@ -5,12 +5,12 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/internal"
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-session/common"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
)
|
||||
|
||||
// HandleQueryTool handles SQL query execution
|
||||
func HandleQueryTool(dbClient *DBClient) internal.ToolHandlerFunc {
|
||||
func HandleQueryTool(dbClient *DBClient) common.ToolHandlerFunc {
|
||||
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
arguments := request.Params.Arguments
|
||||
message, ok := arguments["sql"].(string)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package internal
|
||||
package common
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
160
plugins/golang-filter/mcp-session/common/match.go
Normal file
160
plugins/golang-filter/mcp-session/common/match.go
Normal file
@@ -0,0 +1,160 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/envoyproxy/envoy/contrib/golang/common/go/api"
|
||||
)
|
||||
|
||||
// RuleType defines the type of matching rule
|
||||
type RuleType string
|
||||
|
||||
// UpstreamType defines the type of matching rule
|
||||
type UpstreamType string
|
||||
|
||||
const (
|
||||
ExactMatch RuleType = "exact"
|
||||
PrefixMatch RuleType = "prefix"
|
||||
SuffixMatch RuleType = "suffix"
|
||||
ContainsMatch RuleType = "contains"
|
||||
RegexMatch RuleType = "regex"
|
||||
|
||||
RestUpstream UpstreamType = "rest"
|
||||
SSEUpstream UpstreamType = "sse"
|
||||
StreamableUpstream UpstreamType = "streamable"
|
||||
)
|
||||
|
||||
// MatchRule defines the structure for a matching rule
|
||||
type MatchRule struct {
|
||||
MatchRuleDomain string `json:"match_rule_domain"` // Domain pattern, supports wildcards
|
||||
MatchRulePath string `json:"match_rule_path"` // Path pattern to match
|
||||
MatchRuleType RuleType `json:"match_rule_type"` // Type of match rule
|
||||
UpstreamType UpstreamType `json:"upstream_type"` // Type of upstream(s) matched by the rule
|
||||
EnablePathRewrite bool `json:"enable_path_rewrite"` // Enable request path rewrite for matched routes
|
||||
PathRewritePrefix string `json:"path_rewrite_prefix"` // Prefix the request path would be rewritten to.
|
||||
}
|
||||
|
||||
// ParseMatchList parses the match list from the config
|
||||
func ParseMatchList(matchListConfig []interface{}) []MatchRule {
|
||||
matchList := make([]MatchRule, 0)
|
||||
for _, item := range matchListConfig {
|
||||
if ruleMap, ok := item.(map[string]interface{}); ok {
|
||||
rule := MatchRule{}
|
||||
if domain, ok := ruleMap["match_rule_domain"].(string); ok {
|
||||
rule.MatchRuleDomain = domain
|
||||
}
|
||||
if path, ok := ruleMap["match_rule_path"].(string); ok {
|
||||
rule.MatchRulePath = path
|
||||
}
|
||||
if ruleType, ok := ruleMap["match_rule_type"].(string); ok {
|
||||
rule.MatchRuleType = RuleType(ruleType)
|
||||
}
|
||||
if upstreamType, ok := ruleMap["upstream_type"].(string); ok {
|
||||
rule.UpstreamType = UpstreamType(upstreamType)
|
||||
}
|
||||
if len(rule.UpstreamType) == 0 {
|
||||
rule.UpstreamType = RestUpstream
|
||||
} else {
|
||||
switch rule.UpstreamType {
|
||||
case RestUpstream, SSEUpstream, StreamableUpstream:
|
||||
break
|
||||
default:
|
||||
api.LogWarnf("Unknown upstream type: %s", rule.UpstreamType)
|
||||
}
|
||||
}
|
||||
if enablePathRewrite, ok := ruleMap["enable_path_rewrite"].(bool); ok {
|
||||
rule.EnablePathRewrite = enablePathRewrite
|
||||
}
|
||||
if pathRewritePrefix, ok := ruleMap["path_rewrite_prefix"].(string); ok {
|
||||
rule.PathRewritePrefix = pathRewritePrefix
|
||||
}
|
||||
if rule.EnablePathRewrite {
|
||||
if rule.UpstreamType != SSEUpstream {
|
||||
api.LogWarnf("Path rewrite is only supported for SSE upstream type")
|
||||
} else if rule.MatchRuleType != PrefixMatch {
|
||||
api.LogWarnf("Path rewrite is only supported for prefix match type")
|
||||
} else if !strings.HasPrefix(rule.PathRewritePrefix, "/") {
|
||||
rule.PathRewritePrefix = "/" + rule.PathRewritePrefix
|
||||
}
|
||||
}
|
||||
matchList = append(matchList, rule)
|
||||
}
|
||||
}
|
||||
return matchList
|
||||
}
|
||||
|
||||
// convertWildcardToRegex converts wildcard pattern to regex pattern
|
||||
func convertWildcardToRegex(pattern string) string {
|
||||
pattern = regexp.QuoteMeta(pattern)
|
||||
pattern = "^" + strings.ReplaceAll(pattern, "\\*", ".*") + "$"
|
||||
return pattern
|
||||
}
|
||||
|
||||
// matchPattern checks if the target matches the pattern based on rule type
|
||||
func matchPattern(pattern string, target string, ruleType RuleType) bool {
|
||||
if pattern == "" {
|
||||
return true
|
||||
}
|
||||
|
||||
switch ruleType {
|
||||
case ExactMatch:
|
||||
return pattern == target
|
||||
case PrefixMatch:
|
||||
return strings.HasPrefix(target, pattern)
|
||||
case SuffixMatch:
|
||||
return strings.HasSuffix(target, pattern)
|
||||
case ContainsMatch:
|
||||
return strings.Contains(target, pattern)
|
||||
case RegexMatch:
|
||||
matched, err := regexp.MatchString(pattern, target)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return matched
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// matchDomain checks if the domain matches the pattern
|
||||
func matchDomain(domain string, pattern string) bool {
|
||||
if pattern == "" || pattern == "*" {
|
||||
return true
|
||||
}
|
||||
// Convert wildcard pattern to regex pattern
|
||||
regexPattern := convertWildcardToRegex(pattern)
|
||||
matched, _ := regexp.MatchString(regexPattern, domain)
|
||||
return matched
|
||||
}
|
||||
|
||||
// matchDomainAndPath checks if both domain and path match the rule
|
||||
func matchDomainAndPath(domain, path string, rule MatchRule) bool {
|
||||
return matchDomain(domain, rule.MatchRuleDomain) &&
|
||||
matchPattern(rule.MatchRulePath, path, rule.MatchRuleType)
|
||||
}
|
||||
|
||||
// IsMatch checks if the request matches any rule in the rule list
|
||||
// Returns true if no rules are specified
|
||||
func IsMatch(rules []MatchRule, host, path string) (bool, MatchRule) {
|
||||
if len(rules) == 0 {
|
||||
return true, MatchRule{}
|
||||
}
|
||||
|
||||
for _, rule := range rules {
|
||||
if matchDomainAndPath(host, path, rule) {
|
||||
return true, rule
|
||||
}
|
||||
}
|
||||
return false, MatchRule{}
|
||||
}
|
||||
|
||||
// MatchDomainList checks if the domain matches any of the domains in the list
|
||||
func MatchDomainList(domain string, domainList []string) bool {
|
||||
for _, d := range domainList {
|
||||
if matchDomain(domain, d) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package internal
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -72,9 +72,10 @@ func NewRedisClient(config *RedisConfig) (*RedisClient, error) {
|
||||
// Ping the Redis server to check the connection
|
||||
pong, err := client.Ping(context.Background()).Result()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to Redis: %w", err)
|
||||
api.LogErrorf("Failed to connect to Redis: %v", err)
|
||||
} else {
|
||||
api.LogDebugf("Connected to Redis: %s", pong)
|
||||
}
|
||||
api.LogDebugf("Connected to Redis: %s", pong)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
@@ -83,7 +84,7 @@ func NewRedisClient(config *RedisConfig) (*RedisClient, error) {
|
||||
crypto, err = NewCrypto(config.secret)
|
||||
if err != nil {
|
||||
cancel()
|
||||
return nil, err
|
||||
api.LogWarnf("Failed to initialize redis crypto: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,7 +104,7 @@ func NewRedisClient(config *RedisConfig) (*RedisClient, error) {
|
||||
|
||||
// keepAlive periodically checks Redis connection and attempts to reconnect if needed
|
||||
func (r *RedisClient) keepAlive() {
|
||||
ticker := time.NewTicker(30 * time.Second)
|
||||
ticker := time.NewTicker(5 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
@@ -249,6 +250,18 @@ func (r *RedisClient) Get(key string) (string, error) {
|
||||
return value, nil
|
||||
}
|
||||
|
||||
// Expire sets the expiration time for a key
|
||||
func (r *RedisClient) Expire(key string, expiration time.Duration) error {
|
||||
ok, err := r.client.Expire(r.ctx, key, expiration).Result()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set expiration for key: %w", err)
|
||||
}
|
||||
if !ok {
|
||||
return fmt.Errorf("key does not exist")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close closes the Redis client and stops the keepalive goroutine
|
||||
func (r *RedisClient) Close() error {
|
||||
r.cancel()
|
||||
@@ -1,4 +1,4 @@
|
||||
package internal
|
||||
package common
|
||||
|
||||
var GlobalRegistry = NewServerRegistry()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package internal
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -243,6 +243,7 @@ func (s *MCPServer) HandleMessage(
|
||||
message json.RawMessage,
|
||||
) mcp.JSONRPCMessage {
|
||||
// Add server to context
|
||||
|
||||
ctx = context.WithValue(ctx, serverKey{}, s)
|
||||
|
||||
var baseMessage struct {
|
||||
@@ -1,4 +1,4 @@
|
||||
package internal
|
||||
package common
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -210,15 +210,7 @@ func (s *SSEServer) HandleMessage(w http.ResponseWriter, r *http.Request, body j
|
||||
var status int
|
||||
// Only send response if there is one (not for notifications)
|
||||
if response != nil {
|
||||
eventData, _ := json.Marshal(response)
|
||||
|
||||
if sessionID != "" && s.redisClient != nil {
|
||||
channel := GetSSEChannelName(sessionID)
|
||||
publishErr := s.redisClient.Publish(channel, fmt.Sprintf("event: message\ndata: %s\n\n", eventData))
|
||||
|
||||
if publishErr != nil {
|
||||
api.LogErrorf("Failed to publish message to Redis: %v", publishErr)
|
||||
}
|
||||
if sessionID != ""{
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
status = http.StatusAccepted
|
||||
} else {
|
||||
34
plugins/golang-filter/mcp-session/common/utils.go
Normal file
34
plugins/golang-filter/mcp-session/common/utils.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/envoyproxy/envoy/contrib/golang/common/go/api"
|
||||
)
|
||||
|
||||
type RequestURL struct {
|
||||
Method string
|
||||
Scheme string
|
||||
Host string
|
||||
Path string
|
||||
BaseURL string
|
||||
ParsedURL *url.URL
|
||||
InternalIP bool
|
||||
}
|
||||
|
||||
func NewRequestURL(header api.RequestHeaderMap) *RequestURL {
|
||||
method, _ := header.Get(":method")
|
||||
scheme, _ := header.Get(":scheme")
|
||||
host, _ := header.Get(":authority")
|
||||
path, _ := header.Get(":path")
|
||||
internalIP, _ := header.Get("x-envoy-internal")
|
||||
baseURL := fmt.Sprintf("%s://%s", scheme, host)
|
||||
parsedURL, err := url.Parse(path)
|
||||
if err != nil {
|
||||
api.LogWarnf("url parse path:%s failed:%s", path, err)
|
||||
return nil
|
||||
}
|
||||
api.LogDebugf("RequestURL: method=%s, scheme=%s, host=%s, path=%s", method, scheme, host, path)
|
||||
return &RequestURL{Method: method, Scheme: scheme, Host: host, Path: path, BaseURL: baseURL, ParsedURL: parsedURL, InternalIP: internalIP == "true"}
|
||||
}
|
||||
145
plugins/golang-filter/mcp-session/config.go
Normal file
145
plugins/golang-filter/mcp-session/config.go
Normal file
@@ -0,0 +1,145 @@
|
||||
package mcp_session
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
_ "net/http/pprof"
|
||||
|
||||
xds "github.com/cncf/xds/go/xds/type/v3"
|
||||
"google.golang.org/protobuf/types/known/anypb"
|
||||
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-session/common"
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-session/handler"
|
||||
"github.com/envoyproxy/envoy/contrib/golang/common/go/api"
|
||||
)
|
||||
|
||||
const Name = "mcp-session"
|
||||
const Version = "1.0.0"
|
||||
const ConfigPathSuffix = "/config"
|
||||
const DefaultServerName = "higress-mcp-server"
|
||||
|
||||
var GlobalSSEPathSuffix = "/sse"
|
||||
|
||||
type config struct {
|
||||
matchList []common.MatchRule
|
||||
enableUserLevelServer bool
|
||||
rateLimitConfig *handler.MCPRatelimitConfig
|
||||
defaultServer *common.SSEServer
|
||||
redisClient *common.RedisClient
|
||||
}
|
||||
|
||||
func (c *config) Destroy() {
|
||||
if c.redisClient != nil {
|
||||
api.LogDebug("Closing Redis client")
|
||||
c.redisClient.Close()
|
||||
}
|
||||
}
|
||||
|
||||
type Parser struct {
|
||||
}
|
||||
|
||||
// Parse the filter configuration
|
||||
func (p *Parser) Parse(any *anypb.Any, callbacks api.ConfigCallbackHandler) (interface{}, error) {
|
||||
configStruct := &xds.TypedStruct{}
|
||||
if err := any.UnmarshalTo(configStruct); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
v := configStruct.Value
|
||||
|
||||
conf := &config{
|
||||
matchList: make([]common.MatchRule, 0),
|
||||
}
|
||||
|
||||
// Parse match_list if exists
|
||||
if matchList, ok := v.AsMap()["match_list"].([]interface{}); ok {
|
||||
conf.matchList = common.ParseMatchList(matchList)
|
||||
}
|
||||
|
||||
// Redis configuration is optional
|
||||
if redisConfigMap, ok := v.AsMap()["redis"].(map[string]interface{}); ok {
|
||||
redisConfig, err := common.ParseRedisConfig(redisConfigMap)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse redis config: %w", err)
|
||||
}
|
||||
|
||||
redisClient, err := common.NewRedisClient(redisConfig)
|
||||
if err != nil {
|
||||
api.LogErrorf("Failed to initialize Redis client: %w", err)
|
||||
} else {
|
||||
api.LogDebug("Redis client initialized")
|
||||
}
|
||||
conf.redisClient = redisClient
|
||||
} else {
|
||||
api.LogDebug("Redis configuration not provided, running without Redis")
|
||||
}
|
||||
|
||||
enableUserLevelServer, ok := v.AsMap()["enable_user_level_server"].(bool)
|
||||
if !ok {
|
||||
enableUserLevelServer = false
|
||||
if conf.redisClient == nil {
|
||||
return nil, fmt.Errorf("redis configuration is not provided, enable_user_level_server is true")
|
||||
}
|
||||
}
|
||||
conf.enableUserLevelServer = enableUserLevelServer
|
||||
|
||||
if rateLimit, ok := v.AsMap()["rate_limit"].(map[string]interface{}); ok {
|
||||
rateLimitConfig := &handler.MCPRatelimitConfig{}
|
||||
if limit, ok := rateLimit["limit"].(float64); ok {
|
||||
rateLimitConfig.Limit = int(limit)
|
||||
}
|
||||
if window, ok := rateLimit["window"].(float64); ok {
|
||||
rateLimitConfig.Window = int(window)
|
||||
}
|
||||
if whiteList, ok := rateLimit["white_list"].([]interface{}); ok {
|
||||
for _, item := range whiteList {
|
||||
if uid, ok := item.(string); ok {
|
||||
rateLimitConfig.Whitelist = append(rateLimitConfig.Whitelist, uid)
|
||||
}
|
||||
}
|
||||
}
|
||||
if errorText, ok := rateLimit["error_text"].(string); ok {
|
||||
rateLimitConfig.ErrorText = errorText
|
||||
}
|
||||
conf.rateLimitConfig = rateLimitConfig
|
||||
}
|
||||
|
||||
ssePathSuffix, ok := v.AsMap()["sse_path_suffix"].(string)
|
||||
if !ok || ssePathSuffix == "" {
|
||||
return nil, fmt.Errorf("sse path suffix is not set or empty")
|
||||
}
|
||||
GlobalSSEPathSuffix = ssePathSuffix
|
||||
|
||||
return conf, nil
|
||||
}
|
||||
|
||||
func (p *Parser) Merge(parent interface{}, child interface{}) interface{} {
|
||||
parentConfig := parent.(*config)
|
||||
childConfig := child.(*config)
|
||||
|
||||
newConfig := *parentConfig
|
||||
if childConfig.matchList != nil {
|
||||
newConfig.matchList = childConfig.matchList
|
||||
}
|
||||
newConfig.enableUserLevelServer = childConfig.enableUserLevelServer
|
||||
if childConfig.rateLimitConfig != nil {
|
||||
newConfig.rateLimitConfig = childConfig.rateLimitConfig
|
||||
}
|
||||
if childConfig.defaultServer != nil {
|
||||
newConfig.defaultServer = childConfig.defaultServer
|
||||
}
|
||||
return &newConfig
|
||||
}
|
||||
|
||||
func FilterFactory(c interface{}, callbacks api.FilterCallbackHandler) api.StreamFilter {
|
||||
conf, ok := c.(*config)
|
||||
if !ok {
|
||||
panic("unexpected config type")
|
||||
}
|
||||
return &filter{
|
||||
callbacks: callbacks,
|
||||
config: conf,
|
||||
stopChan: make(chan struct{}),
|
||||
mcpConfigHandler: handler.NewMCPConfigHandler(conf.redisClient, callbacks),
|
||||
mcpRatelimitHandler: handler.NewMCPRatelimitHandler(conf.redisClient, callbacks, conf.rateLimitConfig),
|
||||
}
|
||||
}
|
||||
461
plugins/golang-filter/mcp-session/filter.go
Normal file
461
plugins/golang-filter/mcp-session/filter.go
Normal file
@@ -0,0 +1,461 @@
|
||||
package mcp_session
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-session/common"
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-session/handler"
|
||||
"github.com/envoyproxy/envoy/contrib/golang/common/go/api"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
)
|
||||
|
||||
const (
|
||||
RedisNotEnabledResponseBody = "Redis is not enabled, SSE connection is not supported"
|
||||
)
|
||||
|
||||
// The callbacks in the filter, like `DecodeHeaders`, can be implemented on demand.
|
||||
// Because api.PassThroughStreamFilter provides a default implementation.
|
||||
type filter struct {
|
||||
api.PassThroughStreamFilter
|
||||
|
||||
callbacks api.FilterCallbackHandler
|
||||
path string
|
||||
config *config
|
||||
stopChan chan struct{}
|
||||
|
||||
req *http.Request
|
||||
serverName string
|
||||
proxyURL *url.URL
|
||||
matchedRule common.MatchRule
|
||||
needProcess bool
|
||||
skipRequestBody bool
|
||||
skipResponseBody bool
|
||||
cachedResponseBody []byte
|
||||
|
||||
userLevelConfig bool
|
||||
mcpConfigHandler *handler.MCPConfigHandler
|
||||
ratelimit bool
|
||||
mcpRatelimitHandler *handler.MCPRatelimitHandler
|
||||
}
|
||||
|
||||
// Callbacks which are called in request path
|
||||
// The endStream is true if the request doesn't have body
|
||||
func (f *filter) DecodeHeaders(header api.RequestHeaderMap, endStream bool) api.StatusType {
|
||||
requestUrl := common.NewRequestURL(header)
|
||||
if requestUrl == nil {
|
||||
return api.Continue
|
||||
}
|
||||
f.path = requestUrl.ParsedURL.Path
|
||||
|
||||
// Check if request matches any rule in match_list
|
||||
matched, matchedRule := common.IsMatch(f.config.matchList, requestUrl.Host, f.path)
|
||||
if !matched {
|
||||
api.LogDebugf("Request does not match any rule in match_list: %s", requestUrl.ParsedURL.String())
|
||||
return api.Continue
|
||||
}
|
||||
f.needProcess = true
|
||||
f.matchedRule = matchedRule
|
||||
|
||||
f.req = &http.Request{
|
||||
Method: requestUrl.Method,
|
||||
URL: requestUrl.ParsedURL,
|
||||
}
|
||||
|
||||
if strings.HasSuffix(f.path, ConfigPathSuffix) && f.config.enableUserLevelServer {
|
||||
if !requestUrl.InternalIP {
|
||||
api.LogWarnf("Access denied: non-Internal IP address %s", requestUrl.ParsedURL.String())
|
||||
f.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusForbidden, "", nil, 0, "")
|
||||
return api.LocalReply
|
||||
}
|
||||
if strings.HasSuffix(f.path, ConfigPathSuffix) && requestUrl.Method == http.MethodGet {
|
||||
api.LogDebugf("Handling config request: %s", f.path)
|
||||
f.mcpConfigHandler.HandleConfigRequest(f.req, []byte{})
|
||||
return api.LocalReply
|
||||
}
|
||||
f.userLevelConfig = true
|
||||
if endStream {
|
||||
return api.Continue
|
||||
} else {
|
||||
return api.StopAndBuffer
|
||||
}
|
||||
}
|
||||
|
||||
return f.processMcpRequestHeaders(header, endStream)
|
||||
}
|
||||
|
||||
func (f *filter) processMcpRequestHeaders(header api.RequestHeaderMap, endStream bool) api.StatusType {
|
||||
switch f.matchedRule.UpstreamType {
|
||||
case common.RestUpstream, common.StreamableUpstream:
|
||||
return f.processMcpRequestHeadersForRestUpstream(header, endStream)
|
||||
case common.SSEUpstream:
|
||||
return f.processMcpRequestHeadersForSSEUpstream(header, endStream)
|
||||
}
|
||||
f.needProcess = false
|
||||
return api.Continue
|
||||
}
|
||||
|
||||
func (f *filter) processMcpRequestHeadersForRestUpstream(header api.RequestHeaderMap, endStream bool) api.StatusType {
|
||||
method := f.req.Method
|
||||
requestUrl := f.req.URL
|
||||
if !strings.HasSuffix(requestUrl.Path, GlobalSSEPathSuffix) {
|
||||
f.proxyURL = requestUrl
|
||||
if f.config.enableUserLevelServer {
|
||||
parts := strings.Split(requestUrl.Path, "/")
|
||||
if len(parts) >= 3 {
|
||||
serverName := parts[1]
|
||||
uid := parts[2]
|
||||
// Get encoded config
|
||||
encodedConfig, _ := f.mcpConfigHandler.GetEncodedConfig(serverName, uid)
|
||||
if encodedConfig != "" {
|
||||
header.Set("x-higress-mcpserver-config", encodedConfig)
|
||||
api.LogDebugf("Set x-higress-mcpserver-config Header for %s:%s", serverName, uid)
|
||||
}
|
||||
}
|
||||
f.ratelimit = true
|
||||
}
|
||||
if endStream {
|
||||
return api.Continue
|
||||
} else {
|
||||
return api.StopAndBuffer
|
||||
}
|
||||
}
|
||||
|
||||
if method != http.MethodGet {
|
||||
f.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusMethodNotAllowed, "Method not allowed", nil, 0, "")
|
||||
} else {
|
||||
f.config.defaultServer = common.NewSSEServer(common.NewMCPServer(DefaultServerName, Version),
|
||||
common.WithSSEEndpoint(GlobalSSEPathSuffix),
|
||||
common.WithMessageEndpoint(strings.TrimSuffix(requestUrl.Path, GlobalSSEPathSuffix)),
|
||||
common.WithRedisClient(f.config.redisClient))
|
||||
f.serverName = f.config.defaultServer.GetServerName()
|
||||
body := "SSE connection create"
|
||||
f.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusOK, body, nil, 0, "")
|
||||
}
|
||||
return api.LocalReply
|
||||
}
|
||||
|
||||
func (f *filter) processMcpRequestHeadersForSSEUpstream(header api.RequestHeaderMap, endStream bool) api.StatusType {
|
||||
// We don't need to process the request body for SSE upstream.
|
||||
f.skipRequestBody = true
|
||||
return api.Continue
|
||||
}
|
||||
|
||||
// DecodeData might be called multiple times during handling the request body.
|
||||
// The endStream is true when handling the last piece of the body.
|
||||
func (f *filter) DecodeData(buffer api.BufferInstance, endStream bool) api.StatusType {
|
||||
if !f.needProcess || f.skipRequestBody {
|
||||
return api.Continue
|
||||
}
|
||||
if f.matchedRule.UpstreamType != common.RestUpstream && f.matchedRule.UpstreamType != common.StreamableUpstream {
|
||||
return api.Continue
|
||||
}
|
||||
if !endStream {
|
||||
return api.StopAndBuffer
|
||||
}
|
||||
if f.userLevelConfig {
|
||||
// Handle config POST request
|
||||
api.LogDebugf("Handling config request: %s", f.path)
|
||||
f.mcpConfigHandler.HandleConfigRequest(f.req, buffer.Bytes())
|
||||
return api.LocalReply
|
||||
} else if f.ratelimit {
|
||||
if checkJSONRPCMethod(buffer.Bytes(), "tools/list") {
|
||||
api.LogDebugf("Not a tools call request, skipping ratelimit")
|
||||
return api.Continue
|
||||
}
|
||||
parts := strings.Split(f.req.URL.Path, "/")
|
||||
if len(parts) < 3 {
|
||||
api.LogWarnf("Access denied: no valid uid found")
|
||||
f.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusForbidden, "", nil, 0, "")
|
||||
return api.LocalReply
|
||||
}
|
||||
serverName := parts[1]
|
||||
uid := parts[2]
|
||||
encodedConfig, err := f.mcpConfigHandler.GetEncodedConfig(serverName, uid)
|
||||
if err != nil {
|
||||
api.LogWarnf("Access denied: no valid config found for uid %s", uid)
|
||||
f.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusForbidden, "", nil, 0, "")
|
||||
return api.LocalReply
|
||||
} else if encodedConfig == "" && checkJSONRPCMethod(buffer.Bytes(), "tools/call") {
|
||||
api.LogDebugf("Empty config found for %s:%s", serverName, uid)
|
||||
if !f.mcpRatelimitHandler.HandleRatelimit(f.req, buffer.Bytes()) {
|
||||
return api.LocalReply
|
||||
}
|
||||
}
|
||||
}
|
||||
return api.Continue
|
||||
}
|
||||
|
||||
// EncodeHeaders Callbacks which are called in response path.
|
||||
// The endStream is true if the response doesn't have body.
|
||||
func (f *filter) EncodeHeaders(header api.ResponseHeaderMap, endStream bool) api.StatusType {
|
||||
if !f.needProcess {
|
||||
return api.Continue
|
||||
}
|
||||
if f.matchedRule.UpstreamType != common.RestUpstream && f.matchedRule.UpstreamType != common.StreamableUpstream {
|
||||
if contentType, ok := header.Get("content-type"); !ok || !strings.HasPrefix(contentType, "text/event-stream") {
|
||||
api.LogDebugf("Skip response body for non-SSE upstream. Content-Type: %s", contentType)
|
||||
f.skipResponseBody = true
|
||||
}
|
||||
return api.Continue
|
||||
}
|
||||
if f.serverName != "" {
|
||||
if f.config.redisClient != nil {
|
||||
header.Set("Content-Type", "text/event-stream")
|
||||
header.Set("Cache-Control", "no-cache")
|
||||
header.Set("Connection", "keep-alive")
|
||||
header.Set("Access-Control-Allow-Origin", "*")
|
||||
header.Del("Content-Length")
|
||||
} else {
|
||||
header.Set("Content-Length", strconv.Itoa(len(RedisNotEnabledResponseBody)))
|
||||
}
|
||||
return api.Continue
|
||||
}
|
||||
return api.Continue
|
||||
}
|
||||
|
||||
// EncodeData might be called multiple times during handling the response body.
|
||||
// The endStream is true when handling the last piece of the body.
|
||||
func (f *filter) EncodeData(buffer api.BufferInstance, endStream bool) api.StatusType {
|
||||
if !f.needProcess || f.skipResponseBody {
|
||||
return api.Continue
|
||||
}
|
||||
|
||||
ret := api.Continue
|
||||
api.LogDebugf("Upstream Type: %s", f.matchedRule.UpstreamType)
|
||||
switch f.matchedRule.UpstreamType {
|
||||
case common.RestUpstream, common.StreamableUpstream:
|
||||
api.LogDebugf("Encoding data from Rest upstream")
|
||||
ret = f.encodeDataFromRestUpstream(buffer, endStream)
|
||||
break
|
||||
case common.SSEUpstream:
|
||||
api.LogDebugf("Encoding data from SSE upstream")
|
||||
ret = f.encodeDataFromSSEUpstream(buffer, endStream)
|
||||
if endStream {
|
||||
// Always continue as long as the stream has ended.
|
||||
ret = api.Continue
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (f *filter) encodeDataFromRestUpstream(buffer api.BufferInstance, endStream bool) api.StatusType {
|
||||
if !f.needProcess {
|
||||
return api.Continue
|
||||
}
|
||||
if !endStream {
|
||||
return api.StopAndBuffer
|
||||
}
|
||||
if f.proxyURL != nil && f.config.redisClient != nil {
|
||||
sessionID := f.proxyURL.Query().Get("sessionId")
|
||||
if sessionID != "" {
|
||||
channel := common.GetSSEChannelName(sessionID)
|
||||
eventData := fmt.Sprintf("event: message\ndata: %s\n\n", buffer.String())
|
||||
publishErr := f.config.redisClient.Publish(channel, eventData)
|
||||
if publishErr != nil {
|
||||
api.LogErrorf("Failed to publish wasm mcp server message to Redis: %v", publishErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if f.serverName != "" {
|
||||
if f.config.redisClient != nil {
|
||||
// handle default server
|
||||
buffer.Reset()
|
||||
f.config.defaultServer.HandleSSE(f.callbacks, f.stopChan)
|
||||
return api.Running
|
||||
} else {
|
||||
_ = buffer.SetString(RedisNotEnabledResponseBody)
|
||||
return api.Continue
|
||||
}
|
||||
}
|
||||
return api.Continue
|
||||
}
|
||||
|
||||
func (f *filter) encodeDataFromSSEUpstream(buffer api.BufferInstance, endStream bool) api.StatusType {
|
||||
bufferBytes := buffer.Bytes()
|
||||
bufferData := string(bufferBytes)
|
||||
|
||||
err, endpointUrl := f.findEndpointUrl(bufferData)
|
||||
if err != nil {
|
||||
api.LogWarnf("Failed to find endpoint URL in SSE data: %v", err)
|
||||
f.needProcess = false
|
||||
return api.Continue
|
||||
}
|
||||
if endpointUrl == "" {
|
||||
// No endpoint URL found. Need to buffer and check again.
|
||||
return api.StopAndBuffer
|
||||
}
|
||||
|
||||
// Remove query string since we don't need to change it.
|
||||
queryStringIndex := strings.IndexAny(endpointUrl, "?")
|
||||
if queryStringIndex != -1 {
|
||||
endpointUrl = endpointUrl[:queryStringIndex]
|
||||
}
|
||||
|
||||
if changed, newEndpointUrl := f.rewriteEndpointUrl(endpointUrl); changed {
|
||||
api.LogDebugf("The endpoint URL is changed.\n Old: %s\n New: %s", endpointUrl, newEndpointUrl)
|
||||
|
||||
endpointUrlIndex := strings.Index(bufferData, endpointUrl)
|
||||
if endpointUrlIndex == -1 {
|
||||
api.LogWarnf("Something wrong, the previously found endpoint URL %s not found in the SSE data now", endpointUrl)
|
||||
} else {
|
||||
bufferData = bufferData[:endpointUrlIndex] + newEndpointUrl + bufferData[endpointUrlIndex+len(endpointUrl):]
|
||||
_ = buffer.SetString(bufferData)
|
||||
}
|
||||
} else {
|
||||
api.LogDebugf("The endpoint URL %s is not changed", endpointUrl)
|
||||
}
|
||||
|
||||
f.needProcess = false
|
||||
return api.Continue
|
||||
}
|
||||
|
||||
func (f *filter) rewriteEndpointUrl(endpointUrl string) (bool, string) {
|
||||
if !f.matchedRule.EnablePathRewrite {
|
||||
return false, ""
|
||||
}
|
||||
|
||||
if schemeIndex := strings.Index(endpointUrl, "://"); schemeIndex != -1 {
|
||||
endpointUrl = endpointUrl[schemeIndex+3:]
|
||||
if slashIndex := strings.Index(endpointUrl, "/"); slashIndex != -1 {
|
||||
endpointUrl = endpointUrl[slashIndex:]
|
||||
} else {
|
||||
endpointUrl = "/"
|
||||
}
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(endpointUrl, f.matchedRule.PathRewritePrefix) {
|
||||
// The endpoint URL does not match the path rewrite prefix. We are unable to rewrite it back.
|
||||
api.LogWarnf("The endpoint URL %s does not match the path rewrite prefix %s", endpointUrl, f.matchedRule.PathRewritePrefix)
|
||||
return false, ""
|
||||
}
|
||||
|
||||
suffix := endpointUrl[len(f.matchedRule.PathRewritePrefix):]
|
||||
|
||||
if len(suffix) == 0 {
|
||||
endpointUrl = f.matchedRule.MatchRulePath
|
||||
} else {
|
||||
matchPathHasTrailingSlash := strings.HasSuffix(f.matchedRule.MatchRulePath, "/")
|
||||
suffixHasLeadingSlash := strings.HasPrefix(suffix, "/")
|
||||
if matchPathHasTrailingSlash != suffixHasLeadingSlash {
|
||||
// One has, the other doesn't have.
|
||||
endpointUrl = f.matchedRule.MatchRulePath + suffix
|
||||
} else if matchPathHasTrailingSlash {
|
||||
// Both have.
|
||||
endpointUrl = f.matchedRule.MatchRulePath + suffix[1:]
|
||||
} else {
|
||||
// Neither have.
|
||||
endpointUrl = f.matchedRule.MatchRulePath + "/" + suffix
|
||||
}
|
||||
}
|
||||
|
||||
return true, endpointUrl
|
||||
}
|
||||
|
||||
func (f *filter) findNextLineBreak(bufferData string) (error, string) {
|
||||
// See https://html.spec.whatwg.org/multipage/server-sent-events.html
|
||||
crIndex := strings.IndexAny(bufferData, "\r")
|
||||
lfIndex := strings.IndexAny(bufferData, "\n")
|
||||
if crIndex == -1 && lfIndex == -1 {
|
||||
// No line break found.
|
||||
return nil, ""
|
||||
}
|
||||
lineBreak := ""
|
||||
if crIndex != -1 && lfIndex != -1 {
|
||||
if crIndex < lfIndex {
|
||||
if crIndex+1 == lfIndex {
|
||||
lineBreak = "\r\n"
|
||||
} else {
|
||||
lineBreak = "\r"
|
||||
}
|
||||
} else {
|
||||
if crIndex == lfIndex+1 {
|
||||
// Found unexpected "\n\r". Skip body processing.
|
||||
return errors.New("found unexpected LF+CR"), ""
|
||||
} else {
|
||||
lineBreak = "\n"
|
||||
}
|
||||
}
|
||||
} else if crIndex != -1 {
|
||||
lineBreak = "\r"
|
||||
} else {
|
||||
lineBreak = "\n"
|
||||
}
|
||||
return nil, lineBreak
|
||||
}
|
||||
|
||||
func (f *filter) findEndpointUrl(bufferData string) (error, string) {
|
||||
eventIndex := strings.Index(bufferData, "event:")
|
||||
if eventIndex == -1 {
|
||||
return nil, ""
|
||||
}
|
||||
bufferData = bufferData[eventIndex:]
|
||||
err, lineBreak := f.findNextLineBreak(bufferData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to find endpoint URL in SSE data: %v", err), ""
|
||||
}
|
||||
if lineBreak == "" {
|
||||
// No line break found, which means the data is not enough.
|
||||
return nil, ""
|
||||
}
|
||||
api.LogDebugf("event line break sequence: %v", []byte(lineBreak))
|
||||
eventEndIndex := strings.Index(bufferData, lineBreak)
|
||||
if eventEndIndex == -1 {
|
||||
return nil, ""
|
||||
}
|
||||
eventName := strings.TrimSpace(bufferData[len("event:"):eventEndIndex])
|
||||
if eventName != "endpoint" {
|
||||
return fmt.Errorf("the initial event [%s] is not an endpoint event. Skip processing", eventName), ""
|
||||
}
|
||||
bufferData = bufferData[eventEndIndex+len(lineBreak):]
|
||||
err, lineBreak = f.findNextLineBreak(bufferData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to find endpoint URL in SSE data: %v", err), ""
|
||||
}
|
||||
if lineBreak == "" {
|
||||
// No line break found, which means the data is not enough.
|
||||
return nil, ""
|
||||
}
|
||||
api.LogDebugf("data line break sequence: %v", []byte(lineBreak))
|
||||
dataEndIndex := strings.Index(bufferData, lineBreak)
|
||||
if dataEndIndex == -1 {
|
||||
// Data received not enough.
|
||||
return nil, ""
|
||||
}
|
||||
eventData := bufferData[:dataEndIndex]
|
||||
if !strings.HasPrefix(eventData, "data:") {
|
||||
return fmt.Errorf("an unexpected non-data field found in the event. Skip processing. Field: %s", eventData), ""
|
||||
}
|
||||
return nil, strings.TrimSpace(eventData[len("data:"):])
|
||||
}
|
||||
|
||||
// OnDestroy stops the goroutine
|
||||
func (f *filter) OnDestroy(reason api.DestroyReason) {
|
||||
api.LogDebugf("OnDestroy: reason=%v", reason)
|
||||
if f.serverName != "" && f.stopChan != nil {
|
||||
select {
|
||||
case <-f.stopChan:
|
||||
return
|
||||
default:
|
||||
api.LogDebug("Stopping SSE connection")
|
||||
close(f.stopChan)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// check if the request is a tools/call request
|
||||
func checkJSONRPCMethod(body []byte, method string) bool {
|
||||
var request mcp.CallToolRequest
|
||||
if err := json.Unmarshal(body, &request); err != nil {
|
||||
api.LogWarnf("Failed to unmarshal request body: %v, not a JSON RPC request", err)
|
||||
return true
|
||||
}
|
||||
|
||||
return request.Method == method
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/internal"
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-session/common"
|
||||
"github.com/envoyproxy/envoy/contrib/golang/common/go/api"
|
||||
)
|
||||
|
||||
@@ -18,7 +18,7 @@ type MCPConfigHandler struct {
|
||||
}
|
||||
|
||||
// NewMCPConfigHandler creates a new instance of MCP configuration handler
|
||||
func NewMCPConfigHandler(redisClient *internal.RedisClient, callbacks api.FilterCallbackHandler) *MCPConfigHandler {
|
||||
func NewMCPConfigHandler(redisClient *common.RedisClient, callbacks api.FilterCallbackHandler) *MCPConfigHandler {
|
||||
return &MCPConfigHandler{
|
||||
configStore: NewRedisConfigStore(redisClient),
|
||||
callbacks: callbacks,
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/internal"
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-session/common"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -36,11 +36,11 @@ type ConfigStore interface {
|
||||
|
||||
// RedisConfigStore implements configuration storage using Redis
|
||||
type RedisConfigStore struct {
|
||||
redisClient *internal.RedisClient
|
||||
redisClient *common.RedisClient
|
||||
}
|
||||
|
||||
// NewRedisConfigStore creates a new instance of Redis configuration storage
|
||||
func NewRedisConfigStore(redisClient *internal.RedisClient) ConfigStore {
|
||||
func NewRedisConfigStore(redisClient *common.RedisClient) ConfigStore {
|
||||
return &RedisConfigStore{
|
||||
redisClient: redisClient,
|
||||
}
|
||||
@@ -101,5 +101,11 @@ func (s *RedisConfigStore) GetConfig(serverName string, uid string) (map[string]
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Refresh TTL
|
||||
if err := s.redisClient.Expire(key, configExpiry); err != nil {
|
||||
// Log error but don't fail the request
|
||||
fmt.Printf("Failed to refresh TTL for key %s: %v\n", key, err)
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
@@ -8,13 +8,13 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-server/internal"
|
||||
"github.com/alibaba/higress/plugins/golang-filter/mcp-session/common"
|
||||
"github.com/envoyproxy/envoy/contrib/golang/common/go/api"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
)
|
||||
|
||||
type MCPRatelimitHandler struct {
|
||||
redisClient *internal.RedisClient
|
||||
redisClient *common.RedisClient
|
||||
callbacks api.FilterCallbackHandler
|
||||
limit int // Maximum requests allowed per window
|
||||
window int // Time window in seconds
|
||||
@@ -31,7 +31,7 @@ type MCPRatelimitConfig struct {
|
||||
}
|
||||
|
||||
// NewMCPRatelimitHandler creates a new rate limit handler
|
||||
func NewMCPRatelimitHandler(redisClient *internal.RedisClient, callbacks api.FilterCallbackHandler, conf *MCPRatelimitConfig) *MCPRatelimitHandler {
|
||||
func NewMCPRatelimitHandler(redisClient *common.RedisClient, callbacks api.FilterCallbackHandler, conf *MCPRatelimitConfig) *MCPRatelimitHandler {
|
||||
if conf == nil {
|
||||
conf = &MCPRatelimitConfig{
|
||||
Limit: 100,
|
||||
@@ -4,4 +4,6 @@ build:gcc --cxxopt=-std=c++17
|
||||
build:clang --action_env=CC=clang --action_env=CXX=clang++
|
||||
build:clang --action_env=BAZEL_COMPILER=clang
|
||||
build:clang --linkopt=-fuse-ld=lld
|
||||
build:clang --cxxopt=-std=c++17
|
||||
build:clang --cxxopt=-std=c++17
|
||||
|
||||
build --incompatible_use_platforms_repo_for_constraints=false
|
||||
@@ -1 +1 @@
|
||||
5.4.0
|
||||
6.0.0
|
||||
@@ -1,6 +1,13 @@
|
||||
workspace(name = "istio_ecosystem_wasm_extensions")
|
||||
|
||||
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
|
||||
|
||||
http_archive(
|
||||
name = "platforms",
|
||||
url = "https://github.com/bazelbuild/platforms/releases/download/0.0.9/platforms-0.0.9.tar.gz",
|
||||
sha256 = "5eda539c841265031c2f82d8ae7a3a6490bd62176e0c038fc469eabf91f6149b",
|
||||
)
|
||||
|
||||
load("//bazel:third_party.bzl", "wasm_extension_dependency")
|
||||
|
||||
wasm_extension_dependency()
|
||||
@@ -16,9 +23,9 @@ load("@io_bazel_rules_docker//repositories:deps.bzl", container_deps = "deps")
|
||||
|
||||
container_deps()
|
||||
|
||||
PROXY_WASM_CPP_SDK_SHA = "eaec483b5b3c7bcb89fd208b5a1fa5d79d626f61"
|
||||
PROXY_WASM_CPP_SDK_SHA = "0ceca8c81dddc4c9875cf0cb997454764905658c"
|
||||
|
||||
PROXY_WASM_CPP_SDK_SHA256 = "1140bc8114d75db56a6ca6b18423d4df50d988d40b4cec929a1eb246cf5a4a3d"
|
||||
PROXY_WASM_CPP_SDK_SHA256 = "cb010b242d49fb02b39124421b6acb69bd4ece64fb6299ba3f98f3b36eef7004"
|
||||
|
||||
http_archive(
|
||||
name = "proxy_wasm_cpp_sdk",
|
||||
|
||||
@@ -202,7 +202,7 @@ bool PluginRootContext::parsePluginConfig(const json& configuration,
|
||||
}
|
||||
item = consumer.find("keys");
|
||||
if (item == consumer.end()) {
|
||||
LOG_WARN("not found keys configuration for consumer " + c.name + ", will use global configuration to extract keys");
|
||||
LOG_DEBUG("not found keys configuration for consumer " + c.name + ", will use global configuration to extract keys");
|
||||
need_global_keys = true;
|
||||
} else {
|
||||
c.keys = std::vector<std::string>{OriginalAuthKey};
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
| `modelKey` | string | 选填 | model | 请求body中model参数的位置 |
|
||||
| `addProviderHeader` | string | 选填 | - | 从model参数中解析出的provider名字放到哪个请求header中 |
|
||||
| `modelToHeader` | string | 选填 | - | 直接将model参数放到哪个请求header中 |
|
||||
| `enableOnPathSuffix` | array of string | 选填 | ["/v1/chat/completions"] | 只对这些特定路径后缀的请求生效,可以配置为 "*" 以匹配所有路径 |
|
||||
| `enableOnPathSuffix` | array of string | 选填 | ["/completions","/embeddings","/images/generations","/audio/speech","/fine_tuning/jobs","/moderations"] | 只对这些特定路径后缀的请求生效,可以配置为 "*" 以匹配所有路径 |
|
||||
|
||||
## 运行属性
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
ARG BUILDER=higress-registry.cn-hangzhou.cr.aliyuncs.com/plugins/wasm-go-builder:go1.20.14-tinygo0.29.0-oras1.0.0
|
||||
FROM $BUILDER as builder
|
||||
FROM $BUILDER AS builder
|
||||
|
||||
|
||||
ARG GOPROXY
|
||||
@@ -26,6 +26,6 @@ RUN \
|
||||
tinygo build -o /main.wasm -scheduler=none -gc=custom -tags="custommalloc nottinygc_finalizer $EXTRA_TAGS" -target=wasi ./ ; \
|
||||
fi
|
||||
|
||||
FROM scratch as output
|
||||
FROM scratch AS output
|
||||
|
||||
COPY --from=builder /main.wasm plugin.wasm
|
||||
|
||||
@@ -90,6 +90,8 @@ func (c *PluginConfig) FromJson(json gjson.Result, log wrapper.Log) {
|
||||
|
||||
if json.Get("enableSemanticCache").Exists() {
|
||||
c.EnableSemanticCache = json.Get("enableSemanticCache").Bool()
|
||||
} else if c.GetVectorProvider() == nil {
|
||||
c.EnableSemanticCache = false // set value to false when no vector provider
|
||||
} else {
|
||||
c.EnableSemanticCache = true // set default value to true
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: AI 代理
|
||||
keywords: [ AI网关, AI代理 ]
|
||||
keywords: [AI网关, AI代理]
|
||||
description: AI 代理插件配置参考
|
||||
---
|
||||
|
||||
@@ -20,53 +20,49 @@ description: AI 代理插件配置参考
|
||||
插件执行阶段:`默认阶段`
|
||||
插件执行优先级:`100`
|
||||
|
||||
|
||||
## 配置字段
|
||||
|
||||
### 基本配置
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
|------------|--------|------|-----|------------------|
|
||||
| `provider` | object | 必填 | - | 配置目标 AI 服务提供商的信息 |
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
| ---------- | -------- | -------- | ------ | ---------------------------- |
|
||||
| `provider` | object | 必填 | - | 配置目标 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 列表 |
|
||||
| `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 |
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
| ---------------------- | ---------------------- | -------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `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. 如果映射的目标名称为空字符串 "",则表示保留原模型名称。<br/>4. 支持以 `~` 前缀使用正则匹配。例如用 "~gpt(.\*)" 匹配所有以 "gpt" 开头的模型并支持在目标模型中使用 capture group 引用匹配到的内容。示例: "~gpt(.\*): openai/gpt\$1" |
|
||||
| `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`的配置字段说明如下:
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
|---------------|--------|------|-----|----------------------------------|
|
||||
| `fileUrl` | string | 必填 | - | 保存 AI 对话上下文的文件 URL。仅支持纯文本类型的文件内容 |
|
||||
| `serviceName` | string | 必填 | - | URL 所对应的 Higress 后端服务完整名称 |
|
||||
| `servicePort` | number | 必填 | - | URL 所对应的 Higress 后端服务访问端口 |
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
| ------------- | -------- | -------- | ------ | -------------------------------------------------------- |
|
||||
| `fileUrl` | string | 必填 | - | 保存 AI 对话上下文的文件 URL。仅支持纯文本类型的文件内容 |
|
||||
| `serviceName` | string | 必填 | - | URL 所对应的 Higress 后端服务完整名称 |
|
||||
| `servicePort` | number | 必填 | - | URL 所对应的 Higress 后端服务访问端口 |
|
||||
|
||||
`customSettings`的配置字段说明如下:
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
| ----------- | --------------------- | -------- | ------ | ---------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `name` | string | 必填 | - | 想要设置的参数的名称,例如`max_tokens` |
|
||||
| `value` | string/int/float/bool | 必填 | - | 想要设置的参数的值,例如0 |
|
||||
| `value` | string/int/float/bool | 必填 | - | 想要设置的参数的值,例如 0 |
|
||||
| `mode` | string | 非必填 | "auto" | 参数设置的模式,可以设置为"auto"或者"raw",如果为"auto"则会自动根据协议对参数名做改写,如果为"raw"则不会有任何改写和限制检查 |
|
||||
| `overwrite` | bool | 非必填 | true | 如果为false则只在用户没有设置这个参数时填充参数,否则会直接覆盖用户原有的参数设置 |
|
||||
|
||||
|
||||
custom-setting会遵循如下表格,根据`name`和协议来替换对应的字段,用户需要填写表格中`settingName`列中存在的值。例如用户将`name`设置为`max_tokens`,在openai协议中会替换`max_tokens`,在gemini中会替换`maxOutputTokens`。
|
||||
`none`表示该协议不支持此参数。如果`name`不在此表格中或者对应协议不支持此参数,同时没有设置raw模式,则配置不会生效。
|
||||
| `overwrite` | bool | 非必填 | true | 如果为 false 则只在用户没有设置这个参数时填充参数,否则会直接覆盖用户原有的参数设置 |
|
||||
|
||||
custom-setting 会遵循如下表格,根据`name`和协议来替换对应的字段,用户需要填写表格中`settingName`列中存在的值。例如用户将`name`设置为`max_tokens`,在 openai 协议中会替换`max_tokens`,在 gemini 中会替换`maxOutputTokens`。
|
||||
`none`表示该协议不支持此参数。如果`name`不在此表格中或者对应协议不支持此参数,同时没有设置 raw 模式,则配置不会生效。
|
||||
|
||||
| settingName | openai | baidu | spark | qwen | gemini | hunyuan | claude | minimax |
|
||||
| ----------- | ----------- | ----------------- | ----------- | ----------- | --------------- | ----------- | ----------- | ------------------ |
|
||||
@@ -76,32 +72,31 @@ custom-setting会遵循如下表格,根据`name`和协议来替换对应的字
|
||||
| top_k | none | none | top_k | none | topK | none | top_k | none |
|
||||
| seed | seed | none | none | seed | none | none | none | none |
|
||||
|
||||
如果启用了raw模式,custom-setting会直接用输入的`name`和`value`去更改请求中的json内容,而不对参数名称做任何限制和修改。
|
||||
对于大多数协议,custom-setting都会在json内容的根路径修改或者填充参数。对于`qwen`协议,ai-proxy会在json的`parameters`子路径下做配置。对于`gemini`协议,则会在`generation_config`子路径下做配置。
|
||||
如果启用了 raw 模式,custom-setting 会直接用输入的`name`和`value`去更改请求中的 json 内容,而不对参数名称做任何限制和修改。
|
||||
对于大多数协议,custom-setting 都会在 json 内容的根路径修改或者填充参数。对于`qwen`协议,ai-proxy 会在 json 的`parameters`子路径下做配置。对于`gemini`协议,则会在`generation_config`子路径下做配置。
|
||||
|
||||
`failover` 的配置字段说明如下:
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
|------------------|--------|-----------------|-------|-----------------------------------|
|
||||
| enabled | bool | 非必填 | false | 是否启用 apiToken 的 failover 机制 |
|
||||
| failureThreshold | int | 非必填 | 3 | 触发 failover 连续请求失败的阈值(次数) |
|
||||
| successThreshold | int | 非必填 | 1 | 健康检测的成功阈值(次数) |
|
||||
| healthCheckInterval | int | 非必填 | 5000 | 健康检测的间隔时间,单位毫秒 |
|
||||
| healthCheckTimeout | int | 非必填 | 5000 | 健康检测的超时时间,单位毫秒 |
|
||||
| healthCheckModel | string | 启用 failover 时必填 | | 健康检测使用的模型 |
|
||||
| failoverOnStatus | array of string | 非必填 | ["4.*", "5.*"] | 需要进行 failover 的原始请求的状态码,支持正则表达式匹配 |
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
| ------------------- | --------------- | -------------------- | -------------- | -------------------------------------------------------- |
|
||||
| enabled | bool | 非必填 | false | 是否启用 apiToken 的 failover 机制 |
|
||||
| failureThreshold | int | 非必填 | 3 | 触发 failover 连续请求失败的阈值(次数) |
|
||||
| successThreshold | int | 非必填 | 1 | 健康检测的成功阈值(次数) |
|
||||
| healthCheckInterval | int | 非必填 | 5000 | 健康检测的间隔时间,单位毫秒 |
|
||||
| healthCheckTimeout | int | 非必填 | 5000 | 健康检测的超时时间,单位毫秒 |
|
||||
| healthCheckModel | string | 启用 failover 时必填 | | 健康检测使用的模型 |
|
||||
| failoverOnStatus | array of string | 非必填 | ["4.*", "5.*"] | 需要进行 failover 的原始请求的状态码,支持正则表达式匹配 |
|
||||
|
||||
`retryOnFailure` 的配置字段说明如下:
|
||||
|
||||
目前仅支持对非流式请求进行重试。
|
||||
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
|------------------|--------|--------|-------|---------------------------|
|
||||
| enabled | bool | 非必填 | false | 是否启用失败请求重试 |
|
||||
| maxRetries | int | 非必填 | 1 | 最大重试次数 |
|
||||
| retryTimeout | int | 非必填 | 30000 | 重试超时时间,单位毫秒 |
|
||||
| retryOnStatus | array of string | 非必填 | ["4.*", "5.*"] | 需要进行重试的原始请求的状态码,支持正则表达式匹配 |
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
| ------------- | --------------- | -------- | -------------- | -------------------------------------------------- |
|
||||
| enabled | bool | 非必填 | false | 是否启用失败请求重试 |
|
||||
| maxRetries | int | 非必填 | 1 | 最大重试次数 |
|
||||
| retryTimeout | int | 非必填 | 30000 | 重试超时时间,单位毫秒 |
|
||||
| retryOnStatus | array of string | 非必填 | ["4.*", "5.*"] | 需要进行重试的原始请求的状态码,支持正则表达式匹配 |
|
||||
|
||||
### 提供商特有配置
|
||||
|
||||
@@ -109,19 +104,18 @@ custom-setting会遵循如下表格,根据`name`和协议来替换对应的字
|
||||
|
||||
OpenAI 所对应的 `type` 为 `openai`。它特有的配置字段如下:
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
|-------------------|----------|----------|--------|-------------------------------------------------------------------------------|
|
||||
| `openaiCustomUrl` | string | 非必填 | - | 基于OpenAI协议的自定义后端URL,例如: www.example.com/myai/v1/chat/completions |
|
||||
| `responseJsonSchema` | object | 非必填 | - | 预先定义OpenAI响应需满足的Json Schema, 注意目前仅特定的几种模型支持该用法|
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
| -------------------- | -------- | -------- | ------ | ---------------------------------------------------------------------------------- |
|
||||
| `openaiCustomUrl` | string | 非必填 | - | 基于 OpenAI 协议的自定义后端 URL,例如: <www.example.com/myai/v1/chat/completions> |
|
||||
| `responseJsonSchema` | object | 非必填 | - | 预先定义 OpenAI 响应需满足的 Json Schema, 注意目前仅特定的几种模型支持该用法 |
|
||||
|
||||
#### Azure OpenAI
|
||||
|
||||
Azure OpenAI 所对应的 `type` 为 `azure`。它特有的配置字段如下:
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
|-------------------|--------|------|-----|----------------------------------------------|
|
||||
| `azureServiceUrl` | string | 必填 | - | Azure OpenAI 服务的 URL,须包含 `api-version` 查询参数。 |
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
| ----------------- | -------- | -------- | ------ | -------------------------------------------------------- |
|
||||
| `azureServiceUrl` | string | 必填 | - | Azure OpenAI 服务的 URL,须包含 `api-version` 查询参数。 |
|
||||
|
||||
**注意:** Azure OpenAI 只支持配置一个 API Token。
|
||||
|
||||
@@ -129,19 +123,19 @@ Azure OpenAI 所对应的 `type` 为 `azure`。它特有的配置字段如下:
|
||||
|
||||
月之暗面所对应的 `type` 为 `moonshot`。它特有的配置字段如下:
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
|------------------|--------|------|-----|-------------------------------------------------------------|
|
||||
| `moonshotFileId` | string | 非必填 | - | 通过文件接口上传至月之暗面的文件 ID,其内容将被用做 AI 对话的上下文。不可与 `context` 字段同时配置。 |
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
| ---------------- | -------- | -------- | ------ | ---------------------------------------------------------------------------------------------------- |
|
||||
| `moonshotFileId` | string | 非必填 | - | 通过文件接口上传至月之暗面的文件 ID,其内容将被用做 AI 对话的上下文。不可与 `context` 字段同时配置。 |
|
||||
|
||||
#### 通义千问(Qwen)
|
||||
|
||||
通义千问所对应的 `type` 为 `qwen`。它特有的配置字段如下:
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
| ---------------------- | --------------- | -------- | ------ | ------------------------------------------------------------ |
|
||||
| `qwenEnableSearch` | boolean | 非必填 | - | 是否启用通义千问内置的互联网搜索功能。 |
|
||||
| `qwenFileIds` | array of string | 非必填 | - | 通过文件接口上传至Dashscope的文件 ID,其内容将被用做 AI 对话的上下文。不可与 `context` 字段同时配置。 |
|
||||
| `qwenEnableCompatible` | boolean | 非必填 | false | 开启通义千问兼容模式。启用通义千问兼容模式后,将调用千问的兼容模式接口,同时对请求/响应不做修改。 |
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
| ---------------------- | --------------- | -------- | ------ | ------------------------------------------------------------------------------------------------------- |
|
||||
| `qwenEnableSearch` | boolean | 非必填 | - | 是否启用通义千问内置的互联网搜索功能。 |
|
||||
| `qwenFileIds` | array of string | 非必填 | - | 通过文件接口上传至 Dashscope 的文件 ID,其内容将被用做 AI 对话的上下文。不可与 `context` 字段同时配置。 |
|
||||
| `qwenEnableCompatible` | boolean | 非必填 | false | 开启通义千问兼容模式。启用通义千问兼容模式后,将调用千问的兼容模式接口,同时对请求/响应不做修改。 |
|
||||
|
||||
#### 百川智能 (Baichuan AI)
|
||||
|
||||
@@ -151,13 +145,13 @@ Azure OpenAI 所对应的 `type` 为 `azure`。它特有的配置字段如下:
|
||||
|
||||
零一万物所对应的 `type` 为 `yi`。它并无特有的配置字段。
|
||||
|
||||
#### 智谱AI(Zhipu AI)
|
||||
#### 智谱 AI(Zhipu AI)
|
||||
|
||||
智谱AI所对应的 `type` 为 `zhipuai`。它并无特有的配置字段。
|
||||
智谱 AI 所对应的 `type` 为 `zhipuai`。它并无特有的配置字段。
|
||||
|
||||
#### DeepSeek(DeepSeek)
|
||||
|
||||
DeepSeek所对应的 `type` 为 `deepseek`。它并无特有的配置字段。
|
||||
DeepSeek 所对应的 `type` 为 `deepseek`。它并无特有的配置字段。
|
||||
|
||||
#### Groq
|
||||
|
||||
@@ -167,13 +161,13 @@ Groq 所对应的 `type` 为 `groq`。它并无特有的配置字段。
|
||||
|
||||
文心一言所对应的 `type` 为 `baidu`。它并无特有的配置字段。
|
||||
|
||||
#### 360智脑
|
||||
#### 360 智脑
|
||||
|
||||
360智脑所对应的 `type` 为 `ai360`。它并无特有的配置字段。
|
||||
360 智脑所对应的 `type` 为 `ai360`。它并无特有的配置字段。
|
||||
|
||||
#### GitHub模型
|
||||
#### GitHub 模型
|
||||
|
||||
GitHub模型所对应的 `type` 为 `github`。它并无特有的配置字段。
|
||||
GitHub 模型所对应的 `type` 为 `github`。它并无特有的配置字段。
|
||||
|
||||
#### Mistral
|
||||
|
||||
@@ -181,38 +175,38 @@ Mistral 所对应的 `type` 为 `mistral`。它并无特有的配置字段。
|
||||
|
||||
#### MiniMax
|
||||
|
||||
MiniMax所对应的 `type` 为 `minimax`。它特有的配置字段如下:
|
||||
MiniMax 所对应的 `type` 为 `minimax`。它特有的配置字段如下:
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
| ---------------- | -------- | ------------------------------ | ------ |----------------------------------------------------------------|
|
||||
| `minimaxApiType` | string | v2 和 pro 中选填一项 | v2 | v2 代表 ChatCompletion v2 API,pro 代表 ChatCompletion Pro API |
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
| ---------------- | -------- | ------------------------------ | ------ | ----------------------------------------------------------------------- |
|
||||
| `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
|
||||
|
||||
Anthropic Claude 所对应的 `type` 为 `claude`。它特有的配置字段如下:
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
|-----------|--------|------|-----|----------------------------------|
|
||||
| `claudeVersion` | string | 可选 | - | Claude 服务的 API 版本,默认为 2023-06-01 |
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
| --------------- | -------- | -------- | ------ | ----------------------------------------- |
|
||||
| `claudeVersion` | string | 可选 | - | Claude 服务的 API 版本,默认为 2023-06-01 |
|
||||
|
||||
#### Ollama
|
||||
|
||||
Ollama 所对应的 `type` 为 `ollama`。它特有的配置字段如下:
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
|-------------------|--------|------|-----|----------------------------------------------|
|
||||
| `ollamaServerHost` | string | 必填 | - | Ollama 服务器的主机地址 |
|
||||
| `ollamaServerPort` | number | 必填 | - | Ollama 服务器的端口号,默认为11434 |
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
| ------------------ | -------- | -------- | ------ | ----------------------------------- |
|
||||
| `ollamaServerHost` | string | 必填 | - | Ollama 服务器的主机地址 |
|
||||
| `ollamaServerPort` | number | 必填 | - | Ollama 服务器的端口号,默认为 11434 |
|
||||
|
||||
#### 混元
|
||||
|
||||
混元所对应的 `type` 为 `hunyuan`。它特有的配置字段如下:
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
|-------------------|--------|------|-----|----------------------------------------------|
|
||||
| `hunyuanAuthId` | string | 必填 | - | 混元用于v3版本认证的id |
|
||||
| `hunyuanAuthKey` | string | 必填 | - | 混元用于v3版本认证的key |
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
| ---------------- | -------- | -------- | ------ | -------------------------- |
|
||||
| `hunyuanAuthId` | string | 必填 | - | 混元用于 v3 版本认证的 id |
|
||||
| `hunyuanAuthKey` | string | 必填 | - | 混元用于 v3 版本认证的 key |
|
||||
|
||||
#### 阶跃星辰 (Stepfun)
|
||||
|
||||
@@ -222,23 +216,24 @@ Ollama 所对应的 `type` 为 `ollama`。它特有的配置字段如下:
|
||||
|
||||
Cloudflare Workers AI 所对应的 `type` 为 `cloudflare`。它特有的配置字段如下:
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
|-------------------|--------|------|-----|----------------------------------------------------------------------------------------------------------------------------|
|
||||
| `cloudflareAccountId` | string | 必填 | - | [Cloudflare Account ID](https://developers.cloudflare.com/workers-ai/get-started/rest-api/#1-get-api-token-and-account-id) |
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
| --------------------- | -------- | -------- | ------ | -------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `cloudflareAccountId` | string | 必填 | - | [Cloudflare Account ID](https://developers.cloudflare.com/workers-ai/get-started/rest-api/#1-get-api-token-and-account-id) |
|
||||
|
||||
#### 星火 (Spark)
|
||||
|
||||
星火所对应的 `type` 为 `spark`。它并无特有的配置字段。
|
||||
|
||||
讯飞星火认知大模型的`apiTokens`字段值为`APIKey:APISecret`。即填入自己的APIKey与APISecret,并以`:`分隔。
|
||||
讯飞星火认知大模型的`apiTokens`字段值为`APIKey:APISecret`。即填入自己的 APIKey 与 APISecret,并以`:`分隔。
|
||||
|
||||
#### Gemini
|
||||
|
||||
Gemini 所对应的 `type` 为 `gemini`。它特有的配置字段如下:
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
| --------------------- | -------- | -------- |-----|-------------------------------------------------------------------------------------------------|
|
||||
| `geminiSafetySetting` | map of string | 非必填 | - | Gemini AI内容过滤和安全级别设定。参考[Safety settings](https://ai.google.dev/gemini-api/docs/safety-settings) |
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
| --------------------- | ------------- | -------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `geminiSafetySetting` | map of string | 非必填 | - | Gemini AI 内容过滤和安全级别设定。参考[Safety settings](https://ai.google.dev/gemini-api/docs/safety-settings) |
|
||||
| `apiVersion` | string | 非必填 | `v1beta` | 用于指定 API 的版本, 可选择 `v1` 或 `v1beta` 。 版本差异请参考[API versions explained](https://ai.google.dev/gemini-api/docs/api-versions)。 |
|
||||
|
||||
#### DeepL
|
||||
|
||||
@@ -253,18 +248,32 @@ 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时一起使用 |
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
| ---------------- | -------- | -------- | ------ | -------------------------------------------------------------------------------- |
|
||||
| `difyApiUrl` | string | 非必填 | - | dify 私有化部署的 url |
|
||||
| `botType` | string | 非必填 | - | dify 的应用类型,Chat/Completion/Agent/Workflow |
|
||||
| `inputVariable` | string | 非必填 | - | dify 中应用类型为 workflow 时需要设置输入变量,当 botType 为 workflow 时一起使用 |
|
||||
| `outputVariable` | string | 非必填 | - | dify 中应用类型为 workflow 时需要设置输出变量,当 botType 为 workflow 时一起使用 |
|
||||
|
||||
#### Google Vertex AI
|
||||
|
||||
Google Vertex AI 所对应的 type 为 vertex。它特有的配置字段如下:
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
|-----------------------------|---------------|--------|--------|-------------------------------------------------------------------------------|
|
||||
| `vertexAuthKey` | string | 必填 | - | 用于认证的 Google Service Account JSON Key,格式为 PEM 编码的 PKCS#8 私钥和 client_email 等信息 |
|
||||
| `vertexRegion` | string | 必填 | - | Google Cloud 区域(如 us-central1, europe-west4 等),用于构建 Vertex API 地址 |
|
||||
| `vertexProjectId` | string | 必填 | - | Google Cloud 项目 ID,用于标识目标 GCP 项目 |
|
||||
| `vertexAuthServiceName` | string | 必填 | - | 用于 OAuth2 认证的服务名称,该服务为了访问oauth2.googleapis.com |
|
||||
| `vertexGeminiSafetySetting` | map of string | 非必填 | - | Gemini 模型的内容安全过滤设置。 |
|
||||
| `vertexTokenRefreshAhead` | number | 非必填 | - | Vertex access token刷新提前时间(单位秒) |
|
||||
|
||||
## 用法示例
|
||||
|
||||
@@ -376,20 +385,20 @@ provider:
|
||||
provider:
|
||||
type: qwen
|
||||
apiTokens:
|
||||
- "YOUR_QWEN_API_TOKEN"
|
||||
- 'YOUR_QWEN_API_TOKEN'
|
||||
modelMapping:
|
||||
'gpt-3': "qwen-turbo"
|
||||
'gpt-35-turbo': "qwen-plus"
|
||||
'gpt-4-turbo': "qwen-max"
|
||||
'gpt-4-*': "qwen-max"
|
||||
'gpt-4o': "qwen-vl-plus"
|
||||
'gpt-3': 'qwen-turbo'
|
||||
'gpt-35-turbo': 'qwen-plus'
|
||||
'gpt-4-turbo': 'qwen-max'
|
||||
'gpt-4-*': 'qwen-max'
|
||||
'gpt-4o': 'qwen-vl-plus'
|
||||
'text-embedding-v1': 'text-embedding-v1'
|
||||
'*': "qwen-turbo"
|
||||
'*': 'qwen-turbo'
|
||||
```
|
||||
|
||||
**AI 对话请求示例**
|
||||
|
||||
URL: http://your-domain/v1/chat/completions
|
||||
URL: <http://your-domain/v1/chat/completions>
|
||||
|
||||
请求示例:
|
||||
|
||||
@@ -434,7 +443,7 @@ URL: http://your-domain/v1/chat/completions
|
||||
|
||||
**多模态模型 API 请求示例(适用于 `qwen-vl-plus` 和 `qwen-vl-max` 模型)**
|
||||
|
||||
URL: http://your-domain/v1/chat/completions
|
||||
URL: <http://your-domain/v1/chat/completions>
|
||||
|
||||
请求示例:
|
||||
|
||||
@@ -493,7 +502,7 @@ URL: http://your-domain/v1/chat/completions
|
||||
|
||||
**文本向量请求示例**
|
||||
|
||||
URL: http://your-domain/v1/embeddings
|
||||
URL: <http://your-domain/v1/embeddings>
|
||||
|
||||
请求示例:
|
||||
|
||||
@@ -606,12 +615,12 @@ provider:
|
||||
provider:
|
||||
type: qwen
|
||||
apiTokens:
|
||||
- "YOUR_QWEN_API_TOKEN"
|
||||
- 'YOUR_QWEN_API_TOKEN'
|
||||
modelMapping:
|
||||
"*": "qwen-long" # 通义千问的文件上下文只能在 qwen-long 模型下使用
|
||||
'*': 'qwen-long' # 通义千问的文件上下文只能在 qwen-long 模型下使用
|
||||
qwenFileIds:
|
||||
- "file-fe-xxx"
|
||||
- "file-fe-yyy"
|
||||
- 'file-fe-xxx'
|
||||
- 'file-fe-yyy'
|
||||
```
|
||||
|
||||
**请求示例**
|
||||
@@ -653,7 +662,7 @@ provider:
|
||||
}
|
||||
```
|
||||
|
||||
### 使用original协议代理百炼智能体应用
|
||||
### 使用 original 协议代理百炼智能体应用
|
||||
|
||||
**配置信息**
|
||||
|
||||
@@ -661,17 +670,18 @@ provider:
|
||||
provider:
|
||||
type: qwen
|
||||
apiTokens:
|
||||
- "YOUR_DASHSCOPE_API_TOKEN"
|
||||
- 'YOUR_DASHSCOPE_API_TOKEN'
|
||||
protocol: original
|
||||
```
|
||||
|
||||
**请求实例**
|
||||
|
||||
```json
|
||||
{
|
||||
"input": {
|
||||
"prompt": "介绍一下Dubbo"
|
||||
},
|
||||
"parameters": {},
|
||||
"parameters": {},
|
||||
"debug": {}
|
||||
}
|
||||
```
|
||||
@@ -789,7 +799,7 @@ provider:
|
||||
provider:
|
||||
type: groq
|
||||
apiTokens:
|
||||
- "YOUR_GROQ_API_TOKEN"
|
||||
- 'YOUR_GROQ_API_TOKEN'
|
||||
```
|
||||
|
||||
**请求示例**
|
||||
@@ -848,8 +858,8 @@ provider:
|
||||
provider:
|
||||
type: claude
|
||||
apiTokens:
|
||||
- "YOUR_CLAUDE_API_TOKEN"
|
||||
version: "2023-06-01"
|
||||
- 'YOUR_CLAUDE_API_TOKEN'
|
||||
version: '2023-06-01'
|
||||
```
|
||||
|
||||
**请求示例**
|
||||
@@ -899,14 +909,14 @@ provider:
|
||||
|
||||
```yaml
|
||||
provider:
|
||||
type: "hunyuan"
|
||||
hunyuanAuthKey: "<YOUR AUTH KEY>"
|
||||
type: 'hunyuan'
|
||||
hunyuanAuthKey: '<YOUR AUTH KEY>'
|
||||
apiTokens:
|
||||
- ""
|
||||
hunyuanAuthId: "<YOUR AUTH ID>"
|
||||
- ''
|
||||
hunyuanAuthId: '<YOUR AUTH ID>'
|
||||
timeout: 1200000
|
||||
modelMapping:
|
||||
"*": "hunyuan-lite"
|
||||
'*': 'hunyuan-lite'
|
||||
```
|
||||
|
||||
**请求示例**
|
||||
@@ -967,10 +977,10 @@ curl --location 'http://<your higress domain>/v1/chat/completions' \
|
||||
provider:
|
||||
type: baidu
|
||||
apiTokens:
|
||||
- "YOUR_BAIDU_API_TOKEN"
|
||||
- 'YOUR_BAIDU_API_TOKEN'
|
||||
modelMapping:
|
||||
'gpt-3': "ERNIE-4.0"
|
||||
'*': "ERNIE-4.0"
|
||||
'gpt-3': 'ERNIE-4.0'
|
||||
'*': 'ERNIE-4.0'
|
||||
```
|
||||
|
||||
**请求示例**
|
||||
@@ -1014,7 +1024,7 @@ provider:
|
||||
}
|
||||
```
|
||||
|
||||
### 使用 OpenAI 协议代理MiniMax服务
|
||||
### 使用 OpenAI 协议代理 MiniMax 服务
|
||||
|
||||
**配置信息**
|
||||
|
||||
@@ -1022,11 +1032,11 @@ provider:
|
||||
provider:
|
||||
type: minimax
|
||||
apiTokens:
|
||||
- "YOUR_MINIMAX_API_TOKEN"
|
||||
- 'YOUR_MINIMAX_API_TOKEN'
|
||||
modelMapping:
|
||||
"gpt-3": "abab6.5s-chat"
|
||||
"gpt-4": "abab6.5g-chat"
|
||||
"*": "abab6.5t-chat"
|
||||
'gpt-3': 'abab6.5s-chat'
|
||||
'gpt-4': 'abab6.5g-chat'
|
||||
'*': 'abab6.5t-chat'
|
||||
```
|
||||
|
||||
**请求示例**
|
||||
@@ -1090,12 +1100,12 @@ provider:
|
||||
provider:
|
||||
type: github
|
||||
apiTokens:
|
||||
- "YOUR_GITHUB_ACCESS_TOKEN"
|
||||
- 'YOUR_GITHUB_ACCESS_TOKEN'
|
||||
modelMapping:
|
||||
"gpt-4o": "gpt-4o"
|
||||
"gpt-4": "Phi-3.5-MoE-instruct"
|
||||
"gpt-3.5": "cohere-command-r-08-2024"
|
||||
"text-embedding-3-large": "text-embedding-3-large"
|
||||
'gpt-4o': 'gpt-4o'
|
||||
'gpt-4': 'Phi-3.5-MoE-instruct'
|
||||
'gpt-3.5': 'cohere-command-r-08-2024'
|
||||
'text-embedding-3-large': 'text-embedding-3-large'
|
||||
```
|
||||
|
||||
**请求示例**
|
||||
@@ -1121,6 +1131,7 @@ provider:
|
||||
```
|
||||
|
||||
**响应示例**
|
||||
|
||||
```json
|
||||
{
|
||||
"choices": [
|
||||
@@ -1183,7 +1194,7 @@ provider:
|
||||
}
|
||||
```
|
||||
|
||||
### 使用 OpenAI 协议代理360智脑服务
|
||||
### 使用 OpenAI 协议代理 360 智脑服务
|
||||
|
||||
**配置信息**
|
||||
|
||||
@@ -1191,13 +1202,13 @@ provider:
|
||||
provider:
|
||||
type: ai360
|
||||
apiTokens:
|
||||
- "YOUR_360_API_TOKEN"
|
||||
- 'YOUR_360_API_TOKEN'
|
||||
modelMapping:
|
||||
"gpt-4o": "360gpt-turbo-responsibility-8k"
|
||||
"gpt-4": "360gpt2-pro"
|
||||
"gpt-3.5": "360gpt-turbo"
|
||||
"text-embedding-3-small": "embedding_s1_v1.2"
|
||||
"*": "360gpt-pro"
|
||||
'gpt-4o': '360gpt-turbo-responsibility-8k'
|
||||
'gpt-4': '360gpt2-pro'
|
||||
'gpt-3.5': '360gpt-turbo'
|
||||
'text-embedding-3-small': 'embedding_s1_v1.2'
|
||||
'*': '360gpt-pro'
|
||||
```
|
||||
|
||||
**请求示例**
|
||||
@@ -1257,14 +1268,14 @@ provider:
|
||||
|
||||
**文本向量请求示例**
|
||||
|
||||
URL: http://your-domain/v1/embeddings
|
||||
URL: <http://your-domain/v1/embeddings>
|
||||
|
||||
请求示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"input":["你好"],
|
||||
"model":"text-embedding-3-small"
|
||||
"input": ["你好"],
|
||||
"model": "text-embedding-3-small"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1305,10 +1316,10 @@ URL: http://your-domain/v1/embeddings
|
||||
provider:
|
||||
type: cloudflare
|
||||
apiTokens:
|
||||
- "YOUR_WORKERS_AI_API_TOKEN"
|
||||
cloudflareAccountId: "YOUR_CLOUDFLARE_ACCOUNT_ID"
|
||||
- 'YOUR_WORKERS_AI_API_TOKEN'
|
||||
cloudflareAccountId: 'YOUR_CLOUDFLARE_ACCOUNT_ID'
|
||||
modelMapping:
|
||||
"*": "@cf/meta/llama-3-8b-instruct"
|
||||
'*': '@cf/meta/llama-3-8b-instruct'
|
||||
```
|
||||
|
||||
**请求示例**
|
||||
@@ -1348,7 +1359,7 @@ provider:
|
||||
}
|
||||
```
|
||||
|
||||
### 使用 OpenAI 协议代理Spark服务
|
||||
### 使用 OpenAI 协议代理 Spark 服务
|
||||
|
||||
**配置信息**
|
||||
|
||||
@@ -1356,11 +1367,11 @@ provider:
|
||||
provider:
|
||||
type: spark
|
||||
apiTokens:
|
||||
- "APIKey:APISecret"
|
||||
- 'APIKey:APISecret'
|
||||
modelMapping:
|
||||
"gpt-4o": "generalv3.5"
|
||||
"gpt-4": "generalv3"
|
||||
"*": "general"
|
||||
'gpt-4o': 'generalv3.5'
|
||||
'gpt-4': 'generalv3'
|
||||
'*': 'general'
|
||||
```
|
||||
|
||||
**请求示例**
|
||||
@@ -1474,8 +1485,8 @@ provider:
|
||||
provider:
|
||||
type: deepl
|
||||
apiTokens:
|
||||
- "YOUR_DEEPL_API_TOKEN"
|
||||
targetLang: "ZH"
|
||||
- 'YOUR_DEEPL_API_TOKEN'
|
||||
targetLang: 'ZH'
|
||||
```
|
||||
|
||||
**请求示例**
|
||||
@@ -1500,6 +1511,7 @@ provider:
|
||||
```
|
||||
|
||||
**响应示例**
|
||||
|
||||
```json
|
||||
{
|
||||
"choices": [
|
||||
@@ -1522,16 +1534,18 @@ provider:
|
||||
### 使用 OpenAI 协议代理 Together-AI 服务
|
||||
|
||||
**配置信息**
|
||||
|
||||
```yaml
|
||||
provider:
|
||||
type: together-ai
|
||||
apiTokens:
|
||||
- "YOUR_TOGETHER_AI_API_TOKEN"
|
||||
- 'YOUR_TOGETHER_AI_API_TOKEN'
|
||||
modelMapping:
|
||||
"*": "Qwen/Qwen2.5-72B-Instruct-Turbo"
|
||||
'*': 'Qwen/Qwen2.5-72B-Instruct-Turbo'
|
||||
```
|
||||
|
||||
**请求示例**
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "Qwen/Qwen2.5-72B-Instruct-Turbo",
|
||||
@@ -1545,6 +1559,7 @@ provider:
|
||||
```
|
||||
|
||||
**响应示例**
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "8f5809d54b73efac",
|
||||
@@ -1576,16 +1591,18 @@ provider:
|
||||
### 使用 OpenAI 协议代理 Dify 服务
|
||||
|
||||
**配置信息**
|
||||
|
||||
```yaml
|
||||
provider:
|
||||
type: dify
|
||||
apiTokens:
|
||||
- "YOUR_DIFY_API_TOKEN"
|
||||
- 'YOUR_DIFY_API_TOKEN'
|
||||
modelMapping:
|
||||
"*": "dify"
|
||||
'*': 'dify'
|
||||
```
|
||||
|
||||
**请求示例**
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "gpt-4-turbo",
|
||||
@@ -1600,6 +1617,7 @@ provider:
|
||||
```
|
||||
|
||||
**响应示例**
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "e33fc636-f9e8-4fae-8d5e-fbd0acb09401",
|
||||
@@ -1624,6 +1642,68 @@ provider:
|
||||
}
|
||||
```
|
||||
|
||||
### 使用 OpenAI 协议代理 Google Vertex 服务
|
||||
|
||||
**配置信息**
|
||||
|
||||
```yaml
|
||||
provider:
|
||||
type: vertex
|
||||
vertexAuthKey: |
|
||||
{
|
||||
"type": "service_account",
|
||||
"project_id": "your-project-id",
|
||||
"private_key_id": "your-private-key-id",
|
||||
"private_key": "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n",
|
||||
"client_email": "your-service-account@your-project.iam.gserviceaccount.com",
|
||||
"token_uri": "https://oauth2.googleapis.com/token"
|
||||
}
|
||||
vertexRegion: us-central1
|
||||
vertexProjectId: your-project-id
|
||||
vertexAuthServiceName: your-auth-service-name
|
||||
```
|
||||
|
||||
**请求示例**
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "gemini-2.0-flash-001",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "你好,你是谁?"
|
||||
}
|
||||
],
|
||||
"stream": false
|
||||
}
|
||||
```
|
||||
|
||||
**响应示例**
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "chatcmpl-0000000000000",
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "你好!我是 Vertex AI 提供的 Gemini 模型,由 Google 开发的人工智能助手。我可以回答问题、提供信息和帮助完成各种任务。有什么我可以帮您的吗?"
|
||||
},
|
||||
"finish_reason": "stop"
|
||||
}
|
||||
],
|
||||
"created": 1729986750,
|
||||
"model": "gemini-2.0-flash-001",
|
||||
"object": "chat.completion",
|
||||
"usage": {
|
||||
"prompt_tokens": 15,
|
||||
"completion_tokens": 43,
|
||||
"total_tokens": 58
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## 完整配置示例
|
||||
|
||||
@@ -1643,7 +1723,7 @@ spec:
|
||||
provider:
|
||||
type: groq
|
||||
apiTokens:
|
||||
- "YOUR_API_TOKEN"
|
||||
- 'YOUR_API_TOKEN'
|
||||
ingress:
|
||||
- groq
|
||||
url: oci://higress-registry.cn-hangzhou.cr.aliyuncs.com/plugins/ai-proxy:1.0.0
|
||||
@@ -1655,7 +1735,7 @@ metadata:
|
||||
higress.io/backend-protocol: HTTPS
|
||||
higress.io/destination: groq.dns
|
||||
higress.io/proxy-ssl-name: api.groq.com
|
||||
higress.io/proxy-ssl-server-name: "on"
|
||||
higress.io/proxy-ssl-server-name: 'on'
|
||||
labels:
|
||||
higress.io/resource-definer: higress
|
||||
name: groq
|
||||
@@ -1716,7 +1796,7 @@ services:
|
||||
networks:
|
||||
- higress-net
|
||||
ports:
|
||||
- "10000:10000"
|
||||
- '10000:10000'
|
||||
volumes:
|
||||
- ./envoy.yaml:/etc/envoy/envoy.yaml
|
||||
- ./plugin.wasm:/etc/envoy/plugin.wasm
|
||||
@@ -1745,7 +1825,7 @@ static_resources:
|
||||
- filters:
|
||||
- name: envoy.filters.network.http_connection_manager
|
||||
typed_config:
|
||||
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
|
||||
'@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
|
||||
scheme_header_transformation:
|
||||
scheme_to_overwrite: https
|
||||
stat_prefix: ingress_http
|
||||
@@ -1753,23 +1833,23 @@ static_resources:
|
||||
access_log:
|
||||
- name: envoy.access_loggers.stdout
|
||||
typed_config:
|
||||
"@type": type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StdoutAccessLog
|
||||
'@type': type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StdoutAccessLog
|
||||
# Modify as required
|
||||
route_config:
|
||||
name: local_route
|
||||
virtual_hosts:
|
||||
- name: local_service
|
||||
domains: [ "*" ]
|
||||
domains: ['*']
|
||||
routes:
|
||||
- match:
|
||||
prefix: "/"
|
||||
prefix: '/'
|
||||
route:
|
||||
cluster: claude
|
||||
timeout: 300s
|
||||
http_filters:
|
||||
- name: claude
|
||||
typed_config:
|
||||
"@type": type.googleapis.com/udpa.type.v1.TypedStruct
|
||||
'@type': type.googleapis.com/udpa.type.v1.TypedStruct
|
||||
type_url: type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm
|
||||
value:
|
||||
config:
|
||||
@@ -1780,7 +1860,7 @@ static_resources:
|
||||
local:
|
||||
filename: /etc/envoy/plugin.wasm
|
||||
configuration:
|
||||
"@type": "type.googleapis.com/google.protobuf.StringValue"
|
||||
'@type': 'type.googleapis.com/google.protobuf.StringValue'
|
||||
value: | # 插件配置
|
||||
{
|
||||
"provider": {
|
||||
@@ -1809,8 +1889,8 @@ static_resources:
|
||||
transport_socket:
|
||||
name: envoy.transport_sockets.tls
|
||||
typed_config:
|
||||
"@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
|
||||
"sni": "api.anthropic.com"
|
||||
'@type': type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
|
||||
'sni': 'api.anthropic.com'
|
||||
```
|
||||
|
||||
访问示例:
|
||||
|
||||
@@ -208,6 +208,18 @@ For DeepL, the corresponding `type` is `deepl`. Its unique configuration field i
|
||||
| ------------ | --------- | ----------- | ------- | ------------------------------------ |
|
||||
| `targetLang` | string | Required | - | The target language required by the DeepL translation service |
|
||||
|
||||
#### Google Vertex AI
|
||||
For Vertex, the corresponding `type` is `vertex`. Its unique configuration field is:
|
||||
|
||||
| Name | Data Type | Requirement | Default | Description |
|
||||
|-----------------------------|---------------|---------------| ------ |-------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `vertexAuthKey` | string | Required | - | Google Service Account JSON Key used for authentication. The format should be PEM encoded PKCS#8 private key along with client_email and other information |
|
||||
| `vertexRegion` | string | Required | - | Google Cloud region (e.g., us-central1, europe-west4) used to build the Vertex API address |
|
||||
| `vertexProjectId` | string | Required | - | Google Cloud Project ID, used to identify the target GCP project |
|
||||
| `vertexAuthServiceName` | string | Required | - | Service name for OAuth2 authentication, used to access oauth2.googleapis.com |
|
||||
| `vertexGeminiSafetySetting` | map of string | Optional | - | Gemini model content safety filtering settings. |
|
||||
| `vertexTokenRefreshAhead` | number | Optional | - | Vertex access token refresh ahead time in seconds |
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Using OpenAI Protocol Proxy for Azure OpenAI Service
|
||||
@@ -1411,6 +1423,64 @@ provider:
|
||||
}
|
||||
```
|
||||
|
||||
### Utilizing OpenAI Protocol Proxy for Google Vertex Services
|
||||
**Configuration Information**
|
||||
```yaml
|
||||
provider:
|
||||
type: vertex
|
||||
vertexAuthKey: |
|
||||
{
|
||||
"type": "service_account",
|
||||
"project_id": "your-project-id",
|
||||
"private_key_id": "your-private-key-id",
|
||||
"private_key": "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n",
|
||||
"client_email": "your-service-account@your-project.iam.gserviceaccount.com",
|
||||
"token_uri": "https://oauth2.googleapis.com/token"
|
||||
}
|
||||
vertexRegion: us-central1
|
||||
vertexProjectId: your-project-id
|
||||
vertexAuthServiceName: your-auth-service-name
|
||||
```
|
||||
|
||||
**Request Example**
|
||||
```json
|
||||
{
|
||||
"model": "gemini-2.0-flash-001",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Who are you?"
|
||||
}
|
||||
],
|
||||
"stream": false
|
||||
}
|
||||
```
|
||||
|
||||
**Response Example**
|
||||
```json
|
||||
{
|
||||
"id": "chatcmpl-0000000000000",
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "Hello! I am the Gemini model provided by Vertex AI, developed by Google. I can answer questions, provide information, and assist in completing various tasks. How can I help you today?"
|
||||
},
|
||||
"finish_reason": "stop"
|
||||
}
|
||||
],
|
||||
"created": 1729986750,
|
||||
"model": "gemini-2.0-flash-001",
|
||||
"object": "chat.completion",
|
||||
"usage": {
|
||||
"prompt_tokens": 15,
|
||||
"completion_tokens": 43,
|
||||
"total_tokens": 58
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Full Configuration Example
|
||||
|
||||
### Kubernetes Example
|
||||
|
||||
@@ -11,11 +11,11 @@ require (
|
||||
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
|
||||
github.com/wasilibs/go-re2 v1.6.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/tetratelabs/wazero v1.7.2 // indirect
|
||||
github.com/wasilibs/go-re2 v1.6.0 // indirect
|
||||
golang.org/x/sys v0.21.0 // indirect
|
||||
)
|
||||
|
||||
|
||||
@@ -6,8 +6,6 @@ github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 h1:IHDghbG
|
||||
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/magefile/mage v1.15.1-0.20230912152418-9f54e0f83e2a h1:tdPcGgyiH0K+SbsJBBm2oPyEIOTAvLBwD9TuUwVtZho=
|
||||
github.com/magefile/mage v1.15.1-0.20230912152418-9f54e0f83e2a/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
@@ -29,6 +27,7 @@ github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
github.com/wasilibs/go-re2 v1.6.0 h1:CLlhDebt38wtl/zz4ww+hkXBMcxjrKFvTDXzFW2VOz8=
|
||||
github.com/wasilibs/go-re2 v1.6.0/go.mod h1:prArCyErsypRBI/jFAFJEbzyHzjABKqkzlidF0SNA04=
|
||||
github.com/wasilibs/nottinygc v0.4.0 h1:h1TJMihMC4neN6Zq+WKpLxgd9xCFMw7O9ETLwY2exJQ=
|
||||
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
|
||||
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
|
||||
@@ -352,12 +352,33 @@ func getApiName(path string) provider.ApiName {
|
||||
if strings.HasSuffix(path, "/v1/images/generations") {
|
||||
return provider.ApiNameImageGeneration
|
||||
}
|
||||
if strings.HasSuffix(path, "/v1/images/variations") {
|
||||
return provider.ApiNameImageVariation
|
||||
}
|
||||
if strings.HasSuffix(path, "/v1/images/edits") {
|
||||
return provider.ApiNameImageEdit
|
||||
}
|
||||
if strings.HasSuffix(path, "/v1/batches") {
|
||||
return provider.ApiNameBatches
|
||||
}
|
||||
if util.RegRetrieveBatchPath.MatchString(path) {
|
||||
return provider.ApiNameRetrieveBatch
|
||||
}
|
||||
if util.RegCancelBatchPath.MatchString(path) {
|
||||
return provider.ApiNameCancelBatch
|
||||
}
|
||||
if strings.HasSuffix(path, "/v1/files") {
|
||||
return provider.ApiNameFiles
|
||||
}
|
||||
if util.RegRetrieveFilePath.MatchString(path) {
|
||||
return provider.ApiNameRetrieveFile
|
||||
}
|
||||
if util.RegRetrieveFileContentPath.MatchString(path) {
|
||||
return provider.ApiNameRetrieveFileContent
|
||||
}
|
||||
if strings.HasSuffix(path, "/v1/models") {
|
||||
return provider.ApiNameModels
|
||||
}
|
||||
// cohere style
|
||||
if strings.HasSuffix(path, "/v1/rerank") {
|
||||
return provider.ApiNameCohereV1Rerank
|
||||
|
||||
@@ -13,6 +13,8 @@ import (
|
||||
"hash/crc32"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -21,6 +23,8 @@ import (
|
||||
"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"
|
||||
"github.com/tidwall/gjson"
|
||||
"github.com/tidwall/sjson"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -32,7 +36,10 @@ const (
|
||||
bedrockChatCompletionPath = "/model/%s/converse"
|
||||
// converseStream路径 /model/{modelId}/converse-stream
|
||||
bedrockStreamChatCompletionPath = "/model/%s/converse-stream"
|
||||
bedrockSignedHeaders = "host;x-amz-date"
|
||||
// invoke_model 路径 /model/{modelId}/invoke
|
||||
bedrockInvokeModelPath = "/model/%s/invoke"
|
||||
bedrockSignedHeaders = "host;x-amz-date"
|
||||
requestIdHeader = "X-Amzn-Requestid"
|
||||
)
|
||||
|
||||
type bedrockProviderInitializer struct {
|
||||
@@ -50,7 +57,8 @@ func (b *bedrockProviderInitializer) ValidateConfig(config *ProviderConfig) erro
|
||||
|
||||
func (b *bedrockProviderInitializer) DefaultCapabilities() map[string]string {
|
||||
return map[string]string{
|
||||
string(ApiNameChatCompletion): bedrockChatCompletionPath,
|
||||
string(ApiNameChatCompletion): bedrockChatCompletionPath,
|
||||
string(ApiNameImageGeneration): bedrockInvokeModelPath,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,7 +107,7 @@ func (b *bedrockProvider) convertEventFromBedrockToOpenAI(ctx wrapper.HttpContex
|
||||
chatChoice.FinishReason = stopReasonBedrock2OpenAI(*bedrockEvent.StopReason)
|
||||
}
|
||||
choices = append(choices, chatChoice)
|
||||
requestId := ctx.GetStringContext("X-Amzn-Requestid", "")
|
||||
requestId := ctx.GetStringContext(requestIdHeader, "")
|
||||
openAIFormattedChunk := &chatCompletionResponse{
|
||||
Id: requestId,
|
||||
Created: time.Now().UnixMilli() / 1000,
|
||||
@@ -152,6 +160,74 @@ type toolUseBlockDelta struct {
|
||||
Input string `json:"input"`
|
||||
}
|
||||
|
||||
type bedrockImageGenerationResponse struct {
|
||||
Images []string `json:"images"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
type bedrockImageGenerationTextToImageParams struct {
|
||||
Text string `json:"text"`
|
||||
NegativeText string `json:"negativeText,omitempty"`
|
||||
ConditionImage string `json:"conditionImage,omitempty"`
|
||||
ControlMode string `json:"controlMode,omitempty"`
|
||||
ControlStrength float32 `json:"controlLength,omitempty"`
|
||||
}
|
||||
|
||||
type bedrockImageGenerationConfig struct {
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
Quality string `json:"quality,omitempty"`
|
||||
CfgScale float32 `json:"cfgScale,omitempty"`
|
||||
Seed int `json:"seed,omitempty"`
|
||||
NumberOfImages int `json:"numberOfImages"`
|
||||
}
|
||||
|
||||
type bedrockImageGenerationColorGuidedGenerationParams struct {
|
||||
Colors []string `json:"colors"`
|
||||
ReferenceImage string `json:"referenceImage"`
|
||||
Text string `json:"text"`
|
||||
NegativeText string `json:"negativeText,omitempty"`
|
||||
}
|
||||
|
||||
type bedrockImageGenerationImageVariationParams struct {
|
||||
Images []string `json:"images"`
|
||||
SimilarityStrength float32 `json:"similarityStrength"`
|
||||
Text string `json:"text"`
|
||||
NegativeText string `json:"negativeText,omitempty"`
|
||||
}
|
||||
|
||||
type bedrockImageGenerationInPaintingParams struct {
|
||||
Image string `json:"image"`
|
||||
MaskPrompt string `json:"maskPrompt"`
|
||||
MaskImage string `json:"maskImage"`
|
||||
Text string `json:"text"`
|
||||
NegativeText string `json:"negativeText,omitempty"`
|
||||
}
|
||||
|
||||
type bedrockImageGenerationOutPaintingParams struct {
|
||||
Image string `json:"image"`
|
||||
MaskPrompt string `json:"maskPrompt"`
|
||||
MaskImage string `json:"maskImage"`
|
||||
OutPaintingMode string `json:"outPaintingMode"`
|
||||
Text string `json:"text"`
|
||||
NegativeText string `json:"negativeText,omitempty"`
|
||||
}
|
||||
|
||||
type bedrockImageGenerationBackgroundRemovalParams struct {
|
||||
Image string `json:"image"`
|
||||
}
|
||||
|
||||
type bedrockImageGenerationRequest struct {
|
||||
TaskType string `json:"taskType"`
|
||||
ImageGenerationConfig *bedrockImageGenerationConfig `json:"imageGenerationConfig"`
|
||||
TextToImageParams *bedrockImageGenerationTextToImageParams `json:"textToImageParams,omitempty"`
|
||||
ColorGuidedGenerationParams *bedrockImageGenerationColorGuidedGenerationParams `json:"colorGuidedGenerationParams,omitempty"`
|
||||
ImageVariationParams *bedrockImageGenerationImageVariationParams `json:"imageVariationParams,omitempty"`
|
||||
InPaintingParams *bedrockImageGenerationInPaintingParams `json:"inPaintingParams,omitempty"`
|
||||
OutPaintingParams *bedrockImageGenerationOutPaintingParams `json:"outPaintingParams,omitempty"`
|
||||
BackgroundRemovalParams *bedrockImageGenerationBackgroundRemovalParams `json:"backgroundRemovalParams,omitempty"`
|
||||
}
|
||||
|
||||
func extractAmazonEventStreamEvents(ctx wrapper.HttpContext, chunk []byte) []ConverseStreamEvent {
|
||||
body := chunk
|
||||
if bufferedStreamingBody, has := ctx.GetContext(ctxKeyStreamingBody).([]byte); has {
|
||||
@@ -489,7 +565,7 @@ func validateCRC(r io.Reader, expect uint32) error {
|
||||
}
|
||||
|
||||
func (b *bedrockProvider) TransformResponseHeaders(ctx wrapper.HttpContext, apiName ApiName, headers http.Header) {
|
||||
ctx.SetContext("X-Amzn-Requestid", headers.Get("X-Amzn-Requestid"))
|
||||
ctx.SetContext(requestIdHeader, headers.Get(requestIdHeader))
|
||||
if headers.Get("Content-Type") == "application/vnd.amazon.eventstream" {
|
||||
headers.Set("Content-Type", "text/event-stream; charset=utf-8")
|
||||
}
|
||||
@@ -534,21 +610,91 @@ func (b *bedrockProvider) insertHttpContextMessage(body []byte, content string,
|
||||
}
|
||||
|
||||
func (b *bedrockProvider) TransformRequestBodyHeaders(ctx wrapper.HttpContext, apiName ApiName, body []byte, headers http.Header) ([]byte, error) {
|
||||
if gjson.GetBytes(body, "model").Exists() {
|
||||
rawModel := gjson.GetBytes(body, "model").String()
|
||||
encodedModel := url.QueryEscape(rawModel)
|
||||
body, _ = sjson.SetBytes(body, "model", encodedModel)
|
||||
}
|
||||
switch apiName {
|
||||
case ApiNameChatCompletion:
|
||||
return b.onChatCompletionRequestBody(ctx, body, headers)
|
||||
case ApiNameImageGeneration:
|
||||
return b.onImageGenerationRequestBody(ctx, body, headers)
|
||||
default:
|
||||
return b.config.defaultTransformRequestBody(ctx, apiName, body)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *bedrockProvider) TransformResponseBody(ctx wrapper.HttpContext, apiName ApiName, body []byte) ([]byte, error) {
|
||||
if apiName == ApiNameChatCompletion {
|
||||
switch apiName {
|
||||
case ApiNameChatCompletion:
|
||||
return b.onChatCompletionResponseBody(ctx, body)
|
||||
case ApiNameImageGeneration:
|
||||
return b.onImageGenerationResponseBody(ctx, body)
|
||||
}
|
||||
return nil, errUnsupportedApiName
|
||||
}
|
||||
|
||||
func (b *bedrockProvider) onImageGenerationResponseBody(ctx wrapper.HttpContext, body []byte) ([]byte, error) {
|
||||
bedrockResponse := &bedrockImageGenerationResponse{}
|
||||
if err := json.Unmarshal(body, bedrockResponse); err != nil {
|
||||
log.Errorf("unable to unmarshal bedrock image gerneration response: %v", err)
|
||||
return nil, fmt.Errorf("unable to unmarshal bedrock image generation response: %v", err)
|
||||
}
|
||||
response := b.buildBedrockImageGenerationResponse(ctx, bedrockResponse)
|
||||
return json.Marshal(response)
|
||||
}
|
||||
|
||||
func (b *bedrockProvider) onImageGenerationRequestBody(ctx wrapper.HttpContext, body []byte, headers http.Header) ([]byte, error) {
|
||||
request := &imageGenerationRequest{}
|
||||
err := b.config.parseRequestAndMapModel(ctx, request, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
headers.Set("Accept", "*/*")
|
||||
util.OverwriteRequestPathHeader(headers, fmt.Sprintf(bedrockInvokeModelPath, request.Model))
|
||||
return b.buildBedrockImageGenerationRequest(request, headers)
|
||||
}
|
||||
|
||||
func (b *bedrockProvider) buildBedrockImageGenerationRequest(origRequest *imageGenerationRequest, headers http.Header) ([]byte, error) {
|
||||
width, height := 1024, 1024
|
||||
pairs := strings.Split(origRequest.Size, "x")
|
||||
if len(pairs) == 2 {
|
||||
width, _ = strconv.Atoi(pairs[0])
|
||||
height, _ = strconv.Atoi(pairs[1])
|
||||
}
|
||||
|
||||
request := &bedrockImageGenerationRequest{
|
||||
TaskType: "TEXT_IMAGE",
|
||||
TextToImageParams: &bedrockImageGenerationTextToImageParams{
|
||||
Text: origRequest.Prompt,
|
||||
},
|
||||
ImageGenerationConfig: &bedrockImageGenerationConfig{
|
||||
NumberOfImages: origRequest.N,
|
||||
Width: width,
|
||||
Height: height,
|
||||
Quality: origRequest.Quality,
|
||||
},
|
||||
}
|
||||
util.OverwriteRequestPathHeader(headers, fmt.Sprintf(bedrockInvokeModelPath, origRequest.Model))
|
||||
requestBytes, err := json.Marshal(request)
|
||||
b.setAuthHeaders(requestBytes, headers)
|
||||
return requestBytes, err
|
||||
}
|
||||
|
||||
func (b *bedrockProvider) buildBedrockImageGenerationResponse(ctx wrapper.HttpContext, bedrockResponse *bedrockImageGenerationResponse) *imageGenerationResponse {
|
||||
data := make([]imageGenerationData, len(bedrockResponse.Images))
|
||||
for i, image := range bedrockResponse.Images {
|
||||
data[i] = imageGenerationData{
|
||||
B64: image,
|
||||
}
|
||||
}
|
||||
return &imageGenerationResponse{
|
||||
Created: time.Now().UnixMilli() / 1000,
|
||||
Data: data,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *bedrockProvider) onChatCompletionResponseBody(ctx wrapper.HttpContext, body []byte) ([]byte, error) {
|
||||
bedrockResponse := &bedrockConverseResponse{}
|
||||
if err := json.Unmarshal(body, bedrockResponse); err != nil {
|
||||
@@ -613,7 +759,7 @@ func (b *bedrockProvider) buildChatCompletionResponse(ctx wrapper.HttpContext, b
|
||||
FinishReason: stopReasonBedrock2OpenAI(bedrockResponse.StopReason),
|
||||
},
|
||||
}
|
||||
requestId := ctx.GetStringContext("X-Amzn-Requestid", "")
|
||||
requestId := ctx.GetStringContext(requestIdHeader, "")
|
||||
return &chatCompletionResponse{
|
||||
Id: requestId,
|
||||
Created: time.Now().UnixMilli() / 1000,
|
||||
@@ -763,7 +909,6 @@ func (b *bedrockProvider) setAuthHeaders(body []byte, headers http.Header) {
|
||||
|
||||
func (b *bedrockProvider) generateSignature(path, amzDate, dateStamp string, body []byte) string {
|
||||
hashedPayload := sha256Hex(body)
|
||||
path = urlEncoding(path)
|
||||
|
||||
endpoint := fmt.Sprintf(bedrockDefaultDomain, b.config.awsRegion)
|
||||
canonicalHeaders := fmt.Sprintf("host:%s\nx-amz-date:%s\n", endpoint, amzDate)
|
||||
@@ -780,16 +925,6 @@ func (b *bedrockProvider) generateSignature(path, amzDate, dateStamp string, bod
|
||||
return signature
|
||||
}
|
||||
|
||||
func urlEncoding(rawStr string) string {
|
||||
encodedStr := strings.ReplaceAll(rawStr, ":", "%3A")
|
||||
encodedStr = strings.ReplaceAll(encodedStr, "+", "%2B")
|
||||
encodedStr = strings.ReplaceAll(encodedStr, "=", "%3D")
|
||||
encodedStr = strings.ReplaceAll(encodedStr, "&", "%26")
|
||||
encodedStr = strings.ReplaceAll(encodedStr, "$", "%24")
|
||||
encodedStr = strings.ReplaceAll(encodedStr, "@", "%40")
|
||||
return encodedStr
|
||||
}
|
||||
|
||||
func getSignatureKey(key, dateStamp, region, service string) []byte {
|
||||
kDate := hmacSha256([]byte("AWS4"+key), dateStamp)
|
||||
kRegion := hmacSha256(kDate, region)
|
||||
|
||||
@@ -19,22 +19,55 @@ const (
|
||||
claudeDomain = "api.anthropic.com"
|
||||
claudeChatCompletionPath = "/v1/messages"
|
||||
claudeCompletionPath = "/v1/complete"
|
||||
defaultVersion = "2023-06-01"
|
||||
defaultMaxTokens = 4096
|
||||
claudeDefaultVersion = "2023-06-01"
|
||||
claudeDefaultMaxTokens = 4096
|
||||
)
|
||||
|
||||
type claudeProviderInitializer struct{}
|
||||
|
||||
type claudeTool struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description,omitempty"`
|
||||
InputSchema map[string]interface{} `json:"input_schema,omitempty"`
|
||||
}
|
||||
|
||||
type claudeToolChoice struct {
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name,omitempty"`
|
||||
DisableParallelToolUse bool `json:"disable_parallel_tool_use,omitempty"`
|
||||
}
|
||||
|
||||
type claudeChatMessage struct {
|
||||
Role string `json:"role"`
|
||||
Content any `json:"content"`
|
||||
}
|
||||
|
||||
type claudeChatMessageContentSource struct {
|
||||
Type string `json:"type"`
|
||||
MediaType string `json:"media_type,omitempty"`
|
||||
Data string `json:"data,omitempty"`
|
||||
Url string `json:"url,omitempty"`
|
||||
FileId string `json:"file_id,omitempty"`
|
||||
}
|
||||
|
||||
type claudeChatMessageContent struct {
|
||||
Type string `json:"type"`
|
||||
Text string `json:"text,omitempty"`
|
||||
Source *claudeChatMessageContentSource `json:"source,omitempty"`
|
||||
}
|
||||
type claudeTextGenRequest struct {
|
||||
Model string `json:"model"`
|
||||
Messages []chatMessage `json:"messages"`
|
||||
System string `json:"system,omitempty"`
|
||||
MaxTokens int `json:"max_tokens,omitempty"`
|
||||
StopSequences []string `json:"stop_sequences,omitempty"`
|
||||
Stream bool `json:"stream,omitempty"`
|
||||
Temperature float64 `json:"temperature,omitempty"`
|
||||
TopP float64 `json:"top_p,omitempty"`
|
||||
TopK int `json:"top_k,omitempty"`
|
||||
Model string `json:"model"`
|
||||
Messages []claudeChatMessage `json:"messages"`
|
||||
System string `json:"system,omitempty"`
|
||||
MaxTokens int `json:"max_tokens,omitempty"`
|
||||
StopSequences []string `json:"stop_sequences,omitempty"`
|
||||
Stream bool `json:"stream,omitempty"`
|
||||
Temperature float64 `json:"temperature,omitempty"`
|
||||
TopP float64 `json:"top_p,omitempty"`
|
||||
TopK int `json:"top_k,omitempty"`
|
||||
ToolChoice *claudeToolChoice `json:"tool_choice,omitempty"`
|
||||
Tools []claudeTool `json:"tools,omitempty"`
|
||||
ServiceTier string `json:"service_tier,omitempty"`
|
||||
}
|
||||
|
||||
type claudeTextGenResponse struct {
|
||||
@@ -50,13 +83,14 @@ type claudeTextGenResponse struct {
|
||||
}
|
||||
|
||||
type claudeTextGenContent struct {
|
||||
Type string `json:"type"`
|
||||
Type string `json:"type,omitempty"`
|
||||
Text string `json:"text,omitempty"`
|
||||
}
|
||||
|
||||
type claudeTextGenUsage struct {
|
||||
InputTokens int `json:"input_tokens"`
|
||||
OutputTokens int `json:"output_tokens"`
|
||||
InputTokens int `json:"input_tokens,omitempty"`
|
||||
OutputTokens int `json:"output_tokens,omitempty"`
|
||||
ServiceTier string `json:"service_tier,omitempty"`
|
||||
}
|
||||
|
||||
type claudeTextGenError struct {
|
||||
@@ -65,12 +99,12 @@ type claudeTextGenError struct {
|
||||
}
|
||||
|
||||
type claudeTextGenStreamResponse struct {
|
||||
Type string `json:"type"`
|
||||
Message claudeTextGenResponse `json:"message"`
|
||||
Index int `json:"index"`
|
||||
ContentBlock *claudeTextGenContent `json:"content_block"`
|
||||
Delta *claudeTextGenDelta `json:"delta"`
|
||||
Usage claudeTextGenUsage `json:"usage"`
|
||||
Type string `json:"type"`
|
||||
Message *claudeTextGenResponse `json:"message,omitempty"`
|
||||
Index int `json:"index,omitempty"`
|
||||
ContentBlock *claudeTextGenContent `json:"content_block,omitempty"`
|
||||
Delta *claudeTextGenDelta `json:"delta,omitempty"`
|
||||
Usage *claudeTextGenUsage `json:"usage,omitempty"`
|
||||
}
|
||||
|
||||
type claudeTextGenDelta struct {
|
||||
@@ -93,6 +127,7 @@ func (c *claudeProviderInitializer) DefaultCapabilities() map[string]string {
|
||||
string(ApiNameCompletion): claudeCompletionPath,
|
||||
// docs: https://docs.anthropic.com/en/docs/build-with-claude/embeddings#voyage-http-api
|
||||
string(ApiNameEmbeddings): PathOpenAIEmbeddings,
|
||||
string(ApiNameModels): PathOpenAIModels,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,6 +142,10 @@ func (c *claudeProviderInitializer) CreateProvider(config ProviderConfig) (Provi
|
||||
type claudeProvider struct {
|
||||
config ProviderConfig
|
||||
contextCache *contextCache
|
||||
|
||||
messageId string
|
||||
usage usage
|
||||
serviceTier string
|
||||
}
|
||||
|
||||
func (c *claudeProvider) GetProviderType() string {
|
||||
@@ -124,16 +163,16 @@ func (c *claudeProvider) TransformRequestHeaders(ctx wrapper.HttpContext, apiNam
|
||||
|
||||
headers.Set("x-api-key", c.config.GetApiTokenInUse(ctx))
|
||||
|
||||
if c.config.claudeVersion == "" {
|
||||
c.config.claudeVersion = defaultVersion
|
||||
if c.config.apiVersion == "" {
|
||||
c.config.apiVersion = claudeDefaultVersion
|
||||
}
|
||||
|
||||
headers.Set("anthropic-version", c.config.claudeVersion)
|
||||
headers.Set("anthropic-version", c.config.apiVersion)
|
||||
}
|
||||
|
||||
func (c *claudeProvider) OnRequestBody(ctx wrapper.HttpContext, apiName ApiName, body []byte) (types.Action, error) {
|
||||
if !c.config.isSupportedAPI(apiName) {
|
||||
return types.ActionContinue, errUnsupportedApiName
|
||||
return types.ActionContinue, nil
|
||||
}
|
||||
return c.config.handleRequestBody(c, c.contextCache, ctx, apiName, body)
|
||||
}
|
||||
@@ -205,14 +244,15 @@ func (c *claudeProvider) OnStreamingResponseBody(ctx wrapper.HttpContext, name A
|
||||
func (c *claudeProvider) buildClaudeTextGenRequest(origRequest *chatCompletionRequest) *claudeTextGenRequest {
|
||||
claudeRequest := claudeTextGenRequest{
|
||||
Model: origRequest.Model,
|
||||
MaxTokens: origRequest.MaxTokens,
|
||||
MaxTokens: origRequest.getMaxTokens(),
|
||||
StopSequences: origRequest.Stop,
|
||||
Stream: origRequest.Stream,
|
||||
Temperature: origRequest.Temperature,
|
||||
TopP: origRequest.TopP,
|
||||
// ServiceTier: origRequest.ServiceTier,
|
||||
}
|
||||
if claudeRequest.MaxTokens == 0 {
|
||||
claudeRequest.MaxTokens = defaultMaxTokens
|
||||
claudeRequest.MaxTokens = claudeDefaultMaxTokens
|
||||
}
|
||||
|
||||
for _, message := range origRequest.Messages {
|
||||
@@ -220,12 +260,80 @@ func (c *claudeProvider) buildClaudeTextGenRequest(origRequest *chatCompletionRe
|
||||
claudeRequest.System = message.StringContent()
|
||||
continue
|
||||
}
|
||||
claudeMessage := chatMessage{
|
||||
Role: message.Role,
|
||||
Content: message.Content,
|
||||
|
||||
claudeMessage := claudeChatMessage{
|
||||
Role: message.Role,
|
||||
}
|
||||
if message.IsStringContent() {
|
||||
claudeMessage.Content = message.StringContent()
|
||||
} else {
|
||||
chatMessageContents := make([]claudeChatMessageContent, 0)
|
||||
for _, messageContent := range message.ParseContent() {
|
||||
switch messageContent.Type {
|
||||
case contentTypeText:
|
||||
chatMessageContents = append(chatMessageContents, claudeChatMessageContent{
|
||||
Type: contentTypeText,
|
||||
Text: messageContent.Text,
|
||||
})
|
||||
case contentTypeImageUrl:
|
||||
if strings.HasPrefix(messageContent.ImageUrl.Url, "data:") {
|
||||
parts := strings.SplitN(messageContent.ImageUrl.Url, ";", 2)
|
||||
if len(parts) != 2 {
|
||||
log.Errorf("invalid image url format: %s", messageContent.ImageUrl.Url)
|
||||
continue
|
||||
}
|
||||
chatMessageContents = append(chatMessageContents, claudeChatMessageContent{
|
||||
Type: "image",
|
||||
Source: &claudeChatMessageContentSource{
|
||||
Type: "base64",
|
||||
MediaType: strings.TrimPrefix(parts[0], "data:"),
|
||||
Data: strings.TrimPrefix(parts[1], "base64,"),
|
||||
},
|
||||
})
|
||||
} else {
|
||||
chatMessageContents = append(chatMessageContents, claudeChatMessageContent{
|
||||
Type: "image",
|
||||
Source: &claudeChatMessageContentSource{
|
||||
Type: "url",
|
||||
Url: messageContent.ImageUrl.Url,
|
||||
},
|
||||
})
|
||||
}
|
||||
case contentTypeFile:
|
||||
chatMessageContents = append(chatMessageContents, claudeChatMessageContent{
|
||||
Type: "file",
|
||||
Source: &claudeChatMessageContentSource{
|
||||
Type: "url",
|
||||
FileId: messageContent.File.FileId,
|
||||
},
|
||||
})
|
||||
default:
|
||||
log.Errorf("Unsupported content type: %s", messageContent.Type)
|
||||
continue
|
||||
}
|
||||
}
|
||||
claudeMessage.Content = chatMessageContents
|
||||
}
|
||||
claudeRequest.Messages = append(claudeRequest.Messages, claudeMessage)
|
||||
}
|
||||
|
||||
for _, tool := range origRequest.Tools {
|
||||
claudeTool := claudeTool{
|
||||
Name: tool.Function.Name,
|
||||
Description: tool.Function.Description,
|
||||
InputSchema: tool.Function.Parameters,
|
||||
}
|
||||
claudeRequest.Tools = append(claudeRequest.Tools, claudeTool)
|
||||
}
|
||||
|
||||
if tc := origRequest.getToolChoiceObject(); tc != nil {
|
||||
claudeRequest.ToolChoice = &claudeToolChoice{
|
||||
Name: tc.Function.Name,
|
||||
Type: tc.Type,
|
||||
DisableParallelToolUse: !origRequest.ParallelToolCalls,
|
||||
}
|
||||
}
|
||||
|
||||
return &claudeRequest
|
||||
}
|
||||
|
||||
@@ -270,27 +378,50 @@ func stopReasonClaude2OpenAI(reason *string) string {
|
||||
func (c *claudeProvider) streamResponseClaude2OpenAI(ctx wrapper.HttpContext, origResponse *claudeTextGenStreamResponse) *chatCompletionResponse {
|
||||
switch origResponse.Type {
|
||||
case "message_start":
|
||||
c.messageId = origResponse.Message.Id
|
||||
c.usage = usage{
|
||||
PromptTokens: origResponse.Message.Usage.InputTokens,
|
||||
CompletionTokens: origResponse.Message.Usage.OutputTokens,
|
||||
}
|
||||
c.serviceTier = origResponse.Message.Usage.ServiceTier
|
||||
choice := chatCompletionChoice{
|
||||
Index: 0,
|
||||
Index: origResponse.Index,
|
||||
Delta: &chatMessage{Role: roleAssistant, Content: ""},
|
||||
}
|
||||
return createChatCompletionResponse(ctx, origResponse, choice)
|
||||
return c.createChatCompletionResponse(ctx, origResponse, choice)
|
||||
|
||||
case "content_block_delta":
|
||||
choice := chatCompletionChoice{
|
||||
Index: 0,
|
||||
Index: origResponse.Index,
|
||||
Delta: &chatMessage{Content: origResponse.Delta.Text},
|
||||
}
|
||||
return createChatCompletionResponse(ctx, origResponse, choice)
|
||||
return c.createChatCompletionResponse(ctx, origResponse, choice)
|
||||
|
||||
case "message_delta":
|
||||
c.usage.CompletionTokens += origResponse.Usage.OutputTokens
|
||||
c.usage.TotalTokens = c.usage.PromptTokens + c.usage.CompletionTokens
|
||||
|
||||
choice := chatCompletionChoice{
|
||||
Index: 0,
|
||||
Index: origResponse.Index,
|
||||
Delta: &chatMessage{},
|
||||
FinishReason: stopReasonClaude2OpenAI(origResponse.Delta.StopReason),
|
||||
}
|
||||
return createChatCompletionResponse(ctx, origResponse, choice)
|
||||
case "content_block_stop", "message_stop":
|
||||
return c.createChatCompletionResponse(ctx, origResponse, choice)
|
||||
case "message_stop":
|
||||
return &chatCompletionResponse{
|
||||
Id: c.messageId,
|
||||
Created: time.Now().UnixMilli() / 1000,
|
||||
Model: ctx.GetStringContext(ctxKeyFinalRequestModel, ""),
|
||||
Object: objectChatCompletionChunk,
|
||||
Choices: []chatCompletionChoice{},
|
||||
ServiceTier: c.serviceTier,
|
||||
Usage: usage{
|
||||
PromptTokens: c.usage.PromptTokens,
|
||||
CompletionTokens: c.usage.CompletionTokens,
|
||||
TotalTokens: c.usage.TotalTokens,
|
||||
},
|
||||
}
|
||||
case "content_block_stop", "ping", "content_block_start":
|
||||
log.Debugf("skip processing response type: %s", origResponse.Type)
|
||||
return nil
|
||||
default:
|
||||
@@ -299,13 +430,14 @@ func (c *claudeProvider) streamResponseClaude2OpenAI(ctx wrapper.HttpContext, or
|
||||
}
|
||||
}
|
||||
|
||||
func createChatCompletionResponse(ctx wrapper.HttpContext, response *claudeTextGenStreamResponse, choice chatCompletionChoice) *chatCompletionResponse {
|
||||
func (c *claudeProvider) createChatCompletionResponse(ctx wrapper.HttpContext, response *claudeTextGenStreamResponse, choice chatCompletionChoice) *chatCompletionResponse {
|
||||
return &chatCompletionResponse{
|
||||
Id: response.Message.Id,
|
||||
Created: time.Now().UnixMilli() / 1000,
|
||||
Model: ctx.GetStringContext(ctxKeyFinalRequestModel, ""),
|
||||
Object: objectChatCompletionChunk,
|
||||
Choices: []chatCompletionChoice{choice},
|
||||
Id: c.messageId,
|
||||
Created: time.Now().UnixMilli() / 1000,
|
||||
Model: ctx.GetStringContext(ctxKeyFinalRequestModel, ""),
|
||||
Object: objectChatCompletionChunk,
|
||||
Choices: []chatCompletionChoice{choice},
|
||||
ServiceTier: c.serviceTier,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -332,5 +464,14 @@ func (c *claudeProvider) GetApiName(path string) ApiName {
|
||||
if strings.Contains(path, claudeChatCompletionPath) {
|
||||
return ApiNameChatCompletion
|
||||
}
|
||||
if strings.Contains(path, claudeCompletionPath) {
|
||||
return ApiNameCompletion
|
||||
}
|
||||
if strings.Contains(path, PathOpenAIModels) {
|
||||
return ApiNameModels
|
||||
}
|
||||
if strings.Contains(path, PathOpenAIEmbeddings) {
|
||||
return ApiNameEmbeddings
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -6,13 +6,13 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/extensions/ai-proxy/util"
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/log"
|
||||
"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"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -138,7 +138,7 @@ func (d *difyProvider) responseDify2OpenAI(ctx wrapper.HttpContext, response *Di
|
||||
}
|
||||
return &chatCompletionResponse{
|
||||
Id: id,
|
||||
Created: time.Now().UnixMilli() / 1000,
|
||||
Created: response.CreatedAt,
|
||||
Model: ctx.GetStringContext(ctxKeyFinalRequestModel, ""),
|
||||
SystemFingerprint: "",
|
||||
Object: objectChatCompletion,
|
||||
@@ -222,7 +222,7 @@ func (d *difyProvider) streamResponseDify2OpenAI(ctx wrapper.HttpContext, respon
|
||||
}
|
||||
return &chatCompletionResponse{
|
||||
Id: id,
|
||||
Created: time.Now().UnixMilli() / 1000,
|
||||
Created: response.CreatedAt,
|
||||
Model: ctx.GetStringContext(ctxKeyFinalRequestModel, ""),
|
||||
SystemFingerprint: "",
|
||||
Object: objectChatCompletionChunk,
|
||||
@@ -309,7 +309,7 @@ type DifyChatResponse struct {
|
||||
ConversationId string `json:"conversation_id"`
|
||||
MessageId string `json:"message_id"`
|
||||
Answer string `json:"answer"`
|
||||
CreateAt int64 `json:"create_at"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
Data DifyData `json:"data"`
|
||||
MetaData DifyMetaData `json:"metadata"`
|
||||
}
|
||||
@@ -319,6 +319,7 @@ type DifyChunkChatResponse struct {
|
||||
ConversationId string `json:"conversation_id"`
|
||||
MessageId string `json:"message_id"`
|
||||
Answer string `json:"answer"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
Data DifyData `json:"data"`
|
||||
MetaData DifyMetaData `json:"metadata"`
|
||||
}
|
||||
|
||||
@@ -11,9 +11,10 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
doubaoDomain = "ark.cn-beijing.volces.com"
|
||||
doubaoChatCompletionPath = "/api/v3/chat/completions"
|
||||
doubaoEmbeddingsPath = "/api/v3/embeddings"
|
||||
doubaoDomain = "ark.cn-beijing.volces.com"
|
||||
doubaoChatCompletionPath = "/api/v3/chat/completions"
|
||||
doubaoEmbeddingsPath = "/api/v3/embeddings"
|
||||
doubaoImageGenerationPath = "/api/v3/images/generations"
|
||||
)
|
||||
|
||||
type doubaoProviderInitializer struct{}
|
||||
@@ -27,8 +28,9 @@ func (m *doubaoProviderInitializer) ValidateConfig(config *ProviderConfig) error
|
||||
|
||||
func (m *doubaoProviderInitializer) DefaultCapabilities() map[string]string {
|
||||
return map[string]string{
|
||||
string(ApiNameChatCompletion): doubaoChatCompletionPath,
|
||||
string(ApiNameEmbeddings): doubaoEmbeddingsPath,
|
||||
string(ApiNameChatCompletion): doubaoChatCompletionPath,
|
||||
string(ApiNameEmbeddings): doubaoEmbeddingsPath,
|
||||
string(ApiNameImageGeneration): doubaoImageGenerationPath,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,5 +77,8 @@ func (m *doubaoProvider) GetApiName(path string) ApiName {
|
||||
if strings.Contains(path, doubaoEmbeddingsPath) {
|
||||
return ApiNameEmbeddings
|
||||
}
|
||||
if strings.Contains(path, doubaoImageGenerationPath) {
|
||||
return ApiNameImageGeneration
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -19,14 +19,16 @@ import (
|
||||
|
||||
const (
|
||||
geminiApiKeyHeader = "x-goog-api-key"
|
||||
geminiDefaultApiVersion = "v1beta" // 可选: v1, v1beta
|
||||
geminiDomain = "generativelanguage.googleapis.com"
|
||||
geminiChatCompletionPath = "generateContent"
|
||||
geminiChatCompletionStreamPath = "streamGenerateContent?alt=sse"
|
||||
geminiEmbeddingPath = "batchEmbedContents"
|
||||
geminiModelsPath = "models"
|
||||
geminiImageGenerationPath = "predict"
|
||||
)
|
||||
|
||||
type geminiProviderInitializer struct {
|
||||
}
|
||||
type geminiProviderInitializer struct{}
|
||||
|
||||
func (g *geminiProviderInitializer) ValidateConfig(config *ProviderConfig) error {
|
||||
if config.apiTokens == nil || len(config.apiTokens) == 0 {
|
||||
@@ -36,7 +38,12 @@ func (g *geminiProviderInitializer) ValidateConfig(config *ProviderConfig) error
|
||||
}
|
||||
|
||||
func (g *geminiProviderInitializer) DefaultCapabilities() map[string]string {
|
||||
return map[string]string{}
|
||||
return map[string]string{
|
||||
string(ApiNameChatCompletion): "",
|
||||
string(ApiNameEmbeddings): "",
|
||||
string(ApiNameModels): "",
|
||||
string(ApiNameImageGeneration): "",
|
||||
}
|
||||
}
|
||||
|
||||
func (g *geminiProviderInitializer) CreateProvider(config ProviderConfig) (Provider, error) {
|
||||
@@ -65,6 +72,7 @@ func (g *geminiProvider) OnRequestHeaders(ctx wrapper.HttpContext, apiName ApiNa
|
||||
func (g *geminiProvider) TransformRequestHeaders(ctx wrapper.HttpContext, apiName ApiName, headers http.Header) {
|
||||
util.OverwriteRequestHostHeader(headers, geminiDomain)
|
||||
headers.Set(geminiApiKeyHeader, g.config.GetApiTokenInUse(ctx))
|
||||
util.OverwriteRequestAuthorizationHeader(headers, "")
|
||||
}
|
||||
|
||||
func (g *geminiProvider) OnRequestBody(ctx wrapper.HttpContext, apiName ApiName, body []byte) (types.Action, error) {
|
||||
@@ -75,11 +83,38 @@ func (g *geminiProvider) OnRequestBody(ctx wrapper.HttpContext, apiName ApiName,
|
||||
}
|
||||
|
||||
func (g *geminiProvider) TransformRequestBodyHeaders(ctx wrapper.HttpContext, apiName ApiName, body []byte, headers http.Header) ([]byte, error) {
|
||||
if apiName == ApiNameChatCompletion {
|
||||
switch apiName {
|
||||
case ApiNameChatCompletion:
|
||||
return g.onChatCompletionRequestBody(ctx, body, headers)
|
||||
} else {
|
||||
case ApiNameEmbeddings:
|
||||
return g.onEmbeddingsRequestBody(ctx, body, headers)
|
||||
case ApiNameImageGeneration:
|
||||
return g.onImageGenerationRequestBody(ctx, body, headers)
|
||||
}
|
||||
return body, nil
|
||||
}
|
||||
|
||||
func (g *geminiProvider) onImageGenerationRequestBody(ctx wrapper.HttpContext, body []byte, headers http.Header) ([]byte, error) {
|
||||
request := &imageGenerationRequest{}
|
||||
if err := g.config.parseRequestAndMapModel(ctx, request, body); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
path := g.getRequestPath(ApiNameImageGeneration, request.Model, false)
|
||||
log.Debugf("request path:%s", path)
|
||||
util.OverwriteRequestPathHeader(headers, path)
|
||||
geminiRequest := g.buildGeminiImageGenerationRequest(request)
|
||||
return json.Marshal(geminiRequest)
|
||||
}
|
||||
|
||||
func (g *geminiProvider) buildGeminiImageGenerationRequest(request *imageGenerationRequest) *geminiImageGenerationRequest {
|
||||
geminiRequest := &geminiImageGenerationRequest{
|
||||
Instances: []geminiImageGenerationInstance{{Prompt: request.Prompt}},
|
||||
Parameters: &geminiImageGenerationParameters{
|
||||
SampleCount: request.N,
|
||||
},
|
||||
}
|
||||
|
||||
return geminiRequest
|
||||
}
|
||||
|
||||
func (g *geminiProvider) onChatCompletionRequestBody(ctx wrapper.HttpContext, body []byte, headers http.Header) ([]byte, error) {
|
||||
@@ -108,7 +143,7 @@ func (g *geminiProvider) onEmbeddingsRequestBody(ctx wrapper.HttpContext, body [
|
||||
}
|
||||
|
||||
func (g *geminiProvider) OnStreamingResponseBody(ctx wrapper.HttpContext, name ApiName, chunk []byte, isLastChunk bool) ([]byte, error) {
|
||||
log.Infof("chunk body:%s", string(chunk))
|
||||
log.Debugf("chunk body:%s", string(chunk))
|
||||
if isLastChunk || len(chunk) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -144,14 +179,43 @@ func (g *geminiProvider) OnStreamingResponseBody(ctx wrapper.HttpContext, name A
|
||||
}
|
||||
|
||||
func (g *geminiProvider) TransformResponseBody(ctx wrapper.HttpContext, apiName ApiName, body []byte) ([]byte, error) {
|
||||
if apiName == ApiNameChatCompletion {
|
||||
switch apiName {
|
||||
case ApiNameChatCompletion:
|
||||
return g.onChatCompletionResponseBody(ctx, body)
|
||||
} else {
|
||||
case ApiNameEmbeddings:
|
||||
return g.onEmbeddingsResponseBody(ctx, body)
|
||||
case ApiNameImageGeneration:
|
||||
return g.onImageGenerationResponseBody(ctx, body)
|
||||
default:
|
||||
return body, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (g *geminiProvider) onImageGenerationResponseBody(ctx wrapper.HttpContext, body []byte) ([]byte, error) {
|
||||
geminiResponse := &geminiImageGenerationResponse{}
|
||||
if err := json.Unmarshal(body, geminiResponse); err != nil {
|
||||
return nil, fmt.Errorf("unable to unmarshal gemini image generation response: %v", err)
|
||||
}
|
||||
response := g.buildImageGenerationResponse(ctx, geminiResponse)
|
||||
return json.Marshal(response)
|
||||
}
|
||||
|
||||
func (g *geminiProvider) buildImageGenerationResponse(ctx wrapper.HttpContext, geminiResponse *geminiImageGenerationResponse) *imageGenerationResponse {
|
||||
data := make([]imageGenerationData, len(geminiResponse.Predictions))
|
||||
for i, prediction := range geminiResponse.Predictions {
|
||||
data[i] = imageGenerationData{
|
||||
B64: prediction.BytesBase64Encoded,
|
||||
}
|
||||
}
|
||||
response := &imageGenerationResponse{
|
||||
Created: time.Now().UnixMilli() / 1000,
|
||||
Data: data,
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
func (g *geminiProvider) onChatCompletionResponseBody(ctx wrapper.HttpContext, body []byte) ([]byte, error) {
|
||||
log.Debugf("chat completion response body:%s", string(body))
|
||||
geminiResponse := &geminiChatResponse{}
|
||||
if err := json.Unmarshal(body, geminiResponse); err != nil {
|
||||
return nil, fmt.Errorf("unable to unmarshal gemini chat response: %v", err)
|
||||
@@ -177,26 +241,37 @@ func (g *geminiProvider) onEmbeddingsResponseBody(ctx wrapper.HttpContext, body
|
||||
return json.Marshal(response)
|
||||
}
|
||||
|
||||
func (g *geminiProvider) getRequestPath(apiName ApiName, geminiModel string, stream bool) string {
|
||||
func (g *geminiProvider) getRequestPath(apiName ApiName, model string, stream bool) string {
|
||||
action := ""
|
||||
if apiName == ApiNameEmbeddings {
|
||||
action = geminiEmbeddingPath
|
||||
} else if stream {
|
||||
action = geminiChatCompletionStreamPath
|
||||
} else {
|
||||
action = geminiChatCompletionPath
|
||||
if g.config.apiVersion == "" {
|
||||
g.config.apiVersion = geminiDefaultApiVersion
|
||||
}
|
||||
return fmt.Sprintf("/v1/models/%s:%s", geminiModel, action)
|
||||
switch apiName {
|
||||
case ApiNameModels:
|
||||
return fmt.Sprintf("/%s/%s", g.config.apiVersion, geminiModelsPath)
|
||||
case ApiNameEmbeddings:
|
||||
action = geminiEmbeddingPath
|
||||
case ApiNameChatCompletion:
|
||||
if stream {
|
||||
action = geminiChatCompletionStreamPath
|
||||
} else {
|
||||
action = geminiChatCompletionPath
|
||||
}
|
||||
case ApiNameImageGeneration:
|
||||
action = geminiImageGenerationPath
|
||||
}
|
||||
return fmt.Sprintf("/%s/models/%s:%s", g.config.apiVersion, model, action)
|
||||
}
|
||||
|
||||
type geminiChatRequest struct {
|
||||
type geminiGenerationContentRequest struct {
|
||||
// Model and Stream are only used when using the gemini original protocol
|
||||
Model string `json:"model,omitempty"`
|
||||
Stream bool `json:"stream,omitempty"`
|
||||
Contents []geminiChatContent `json:"contents"`
|
||||
SafetySettings []geminiChatSafetySetting `json:"safety_settings,omitempty"`
|
||||
GenerationConfig geminiChatGenerationConfig `json:"generation_config,omitempty"`
|
||||
Tools []geminiChatTools `json:"tools,omitempty"`
|
||||
Model string `json:"model,omitempty"`
|
||||
Stream bool `json:"stream,omitempty"`
|
||||
Contents []geminiChatContent `json:"contents"`
|
||||
SystemInstruction *geminiChatContent `json:"system_instruction,omitempty"`
|
||||
SafetySettings []geminiChatSafetySetting `json:"safetySettings,omitempty"`
|
||||
GenerationConfig geminiChatGenerationConfig `json:"generationConfig,omitempty"`
|
||||
Tools []geminiChatTools `json:"tools,omitempty"`
|
||||
}
|
||||
|
||||
type geminiChatContent struct {
|
||||
@@ -209,13 +284,26 @@ type geminiChatSafetySetting struct {
|
||||
Threshold string `json:"threshold"`
|
||||
}
|
||||
|
||||
type geminiThinkingConfig struct {
|
||||
IncludeThoughts bool `json:"includeThoughts,omitempty"`
|
||||
ThinkingBudget int64 `json:"thinkingBudget,omitempty"`
|
||||
}
|
||||
|
||||
type geminiChatGenerationConfig struct {
|
||||
Temperature float64 `json:"temperature,omitempty"`
|
||||
TopP float64 `json:"topP,omitempty"`
|
||||
TopK float64 `json:"topK,omitempty"`
|
||||
MaxOutputTokens int `json:"maxOutputTokens,omitempty"`
|
||||
CandidateCount int `json:"candidateCount,omitempty"`
|
||||
StopSequences []string `json:"stopSequences,omitempty"`
|
||||
Temperature float64 `json:"temperature,omitempty"`
|
||||
TopP float64 `json:"topP,omitempty"`
|
||||
TopK int64 `json:"topK,omitempty"`
|
||||
Seed int64 `json:"seed,omitempty"`
|
||||
Logprobs bool `json:"logprobs,omitempty"`
|
||||
MaxOutputTokens int `json:"maxOutputTokens,omitempty"`
|
||||
CandidateCount int `json:"candidateCount,omitempty"`
|
||||
StopSequences []string `json:"stopSequences,omitempty"`
|
||||
PresencePenalty int64 `json:"presencePenalty,omitempty"`
|
||||
FrequencyPenalty int64 `json:"frequencyPenalty,omitempty"`
|
||||
ResponseModalities []string `json:"responseModalities,omitempty"`
|
||||
NegativePrompt string `json:"negativePrompt,omitempty"`
|
||||
ThinkingConfig *geminiThinkingConfig `json:"thinkingConfig,omitempty"`
|
||||
MediaResolution string `json:"mediaResolution,omitempty"`
|
||||
}
|
||||
|
||||
type geminiChatTools struct {
|
||||
@@ -238,25 +326,52 @@ type geminiFunctionCall struct {
|
||||
Arguments any `json:"args"`
|
||||
}
|
||||
|
||||
func (g *geminiProvider) buildGeminiChatRequest(request *chatCompletionRequest) *geminiChatRequest {
|
||||
// geminiImageGenerationRequest is the request body for generate image using Imagen 3
|
||||
type geminiImageGenerationRequest struct {
|
||||
Instances []geminiImageGenerationInstance `json:"instances"`
|
||||
Parameters *geminiImageGenerationParameters `json:"parameters,omitempty"`
|
||||
}
|
||||
|
||||
type geminiImageGenerationInstance struct {
|
||||
Prompt string `json:"prompt"`
|
||||
}
|
||||
|
||||
type geminiImageGenerationParameters struct {
|
||||
SampleCount int `json:"sampleCount,omitempty"`
|
||||
AspectRatio string `json:"aspectRatio,omitempty"`
|
||||
}
|
||||
|
||||
type geminiImageGenerationPrediction struct {
|
||||
BytesBase64Encoded string `json:"bytesBase64Encoded"`
|
||||
MimeType string `json:"mimeType"`
|
||||
}
|
||||
|
||||
type geminiImageGenerationResponse struct {
|
||||
Predictions []geminiImageGenerationPrediction `json:"predictions"`
|
||||
}
|
||||
|
||||
func (g *geminiProvider) buildGeminiChatRequest(request *chatCompletionRequest) *geminiGenerationContentRequest {
|
||||
var safetySettings []geminiChatSafetySetting
|
||||
{
|
||||
}
|
||||
for category, threshold := range g.config.geminiSafetySetting {
|
||||
safetySettings = append(safetySettings, geminiChatSafetySetting{
|
||||
Category: category,
|
||||
Threshold: threshold,
|
||||
})
|
||||
}
|
||||
geminiRequest := geminiChatRequest{
|
||||
geminiRequest := geminiGenerationContentRequest{
|
||||
Contents: make([]geminiChatContent, 0, len(request.Messages)),
|
||||
SafetySettings: safetySettings,
|
||||
GenerationConfig: geminiChatGenerationConfig{
|
||||
Temperature: request.Temperature,
|
||||
TopP: request.TopP,
|
||||
MaxOutputTokens: request.MaxTokens,
|
||||
Temperature: request.Temperature,
|
||||
TopP: request.TopP,
|
||||
MaxOutputTokens: request.MaxTokens,
|
||||
PresencePenalty: int64(request.PresencePenalty),
|
||||
FrequencyPenalty: int64(request.FrequencyPenalty),
|
||||
Logprobs: request.Logprobs,
|
||||
ResponseModalities: request.Modalities,
|
||||
},
|
||||
}
|
||||
|
||||
if request.Tools != nil {
|
||||
functions := make([]function, 0, len(request.Tools))
|
||||
for _, tool := range request.Tools {
|
||||
@@ -268,7 +383,7 @@ func (g *geminiProvider) buildGeminiChatRequest(request *chatCompletionRequest)
|
||||
},
|
||||
}
|
||||
}
|
||||
shouldAddDummyModelMessage := false
|
||||
// shouldAddDummyModelMessage := false
|
||||
for _, message := range request.Messages {
|
||||
content := geminiChatContent{
|
||||
Role: message.Role,
|
||||
@@ -280,32 +395,22 @@ func (g *geminiProvider) buildGeminiChatRequest(request *chatCompletionRequest)
|
||||
}
|
||||
|
||||
// there's no assistant role in gemini and API shall vomit if role is not user or model
|
||||
if content.Role == roleAssistant {
|
||||
switch content.Role {
|
||||
case roleSystem:
|
||||
content.Role = ""
|
||||
geminiRequest.SystemInstruction = &content
|
||||
continue
|
||||
case roleAssistant:
|
||||
content.Role = "model"
|
||||
} else if content.Role == roleSystem { // converting system prompt to prompt from user for the same reason
|
||||
content.Role = roleUser
|
||||
shouldAddDummyModelMessage = true
|
||||
}
|
||||
geminiRequest.Contents = append(geminiRequest.Contents, content)
|
||||
|
||||
// if a system message is the last message, we need to add a dummy model message to make gemini happy
|
||||
if shouldAddDummyModelMessage {
|
||||
geminiRequest.Contents = append(geminiRequest.Contents, geminiChatContent{
|
||||
Role: "model",
|
||||
Parts: []geminiPart{
|
||||
{
|
||||
Text: "Okay",
|
||||
},
|
||||
},
|
||||
})
|
||||
shouldAddDummyModelMessage = false
|
||||
}
|
||||
}
|
||||
|
||||
return &geminiRequest
|
||||
}
|
||||
|
||||
func (g *geminiProvider) setSystemContent(request *geminiChatRequest, content string) {
|
||||
func (g *geminiProvider) setSystemContent(request *geminiGenerationContentRequest, content string) {
|
||||
systemContents := []geminiChatContent{{
|
||||
Role: roleUser,
|
||||
Parts: []geminiPart{
|
||||
@@ -395,32 +500,34 @@ func (g *geminiProvider) buildChatCompletionResponse(ctx wrapper.HttpContext, re
|
||||
Object: objectChatCompletion,
|
||||
Created: time.Now().UnixMilli() / 1000,
|
||||
Model: ctx.GetStringContext(ctxKeyFinalRequestModel, ""),
|
||||
Choices: make([]chatCompletionChoice, 0, len(response.Candidates)),
|
||||
Usage: usage{
|
||||
PromptTokens: response.UsageMetadata.PromptTokenCount,
|
||||
CompletionTokens: response.UsageMetadata.CandidatesTokenCount,
|
||||
TotalTokens: response.UsageMetadata.TotalTokenCount,
|
||||
},
|
||||
}
|
||||
for i, candidate := range response.Candidates {
|
||||
choice := chatCompletionChoice{
|
||||
Index: i,
|
||||
Message: &chatMessage{
|
||||
Role: roleAssistant,
|
||||
},
|
||||
FinishReason: finishReasonStop,
|
||||
}
|
||||
if len(candidate.Content.Parts) > 0 {
|
||||
if candidate.Content.Parts[0].FunctionCall != nil {
|
||||
choice.Message.ToolCalls = g.buildToolCalls(&candidate)
|
||||
} else {
|
||||
choice.Message.Content = candidate.Content.Parts[0].Text
|
||||
choiceIndex := 0
|
||||
for _, candidate := range response.Candidates {
|
||||
for _, part := range candidate.Content.Parts {
|
||||
choice := chatCompletionChoice{
|
||||
Index: choiceIndex,
|
||||
Message: &chatMessage{
|
||||
Role: roleAssistant,
|
||||
},
|
||||
FinishReason: finishReasonStop,
|
||||
}
|
||||
} else {
|
||||
choice.Message.Content = ""
|
||||
if part.FunctionCall != nil {
|
||||
choice.Message.ToolCalls = g.buildToolCalls(&candidate)
|
||||
} else if part.InlineData != nil {
|
||||
choice.Message.Content = part.InlineData.Data
|
||||
} else {
|
||||
choice.Message.Content = part.Text
|
||||
}
|
||||
|
||||
choice.FinishReason = candidate.FinishReason
|
||||
fullTextResponse.Choices = append(fullTextResponse.Choices, choice)
|
||||
choiceIndex += 1
|
||||
}
|
||||
fullTextResponse.Choices = append(fullTextResponse.Choices, choice)
|
||||
}
|
||||
return &fullTextResponse
|
||||
}
|
||||
@@ -508,5 +615,8 @@ func (g *geminiProvider) GetApiName(path string) ApiName {
|
||||
if strings.Contains(path, geminiEmbeddingPath) {
|
||||
return ApiNameEmbeddings
|
||||
}
|
||||
if strings.Contains(path, geminiImageGenerationPath) {
|
||||
return ApiNameImageGeneration
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -42,13 +42,10 @@ const (
|
||||
hunyuanAuthIdLen = 36
|
||||
|
||||
// docs: https://cloud.tencent.com/document/product/1729/111007
|
||||
hunyuanOpenAiDomain = "api.hunyuan.cloud.tencent.com"
|
||||
hunyuanOpenAiRequestPath = "/v1/chat/completions"
|
||||
hunyuanOpenAiEmbeddings = "/v1/embeddings"
|
||||
hunyuanOpenAiDomain = "api.hunyuan.cloud.tencent.com"
|
||||
)
|
||||
|
||||
type hunyuanProviderInitializer struct {
|
||||
}
|
||||
type hunyuanProviderInitializer struct{}
|
||||
|
||||
// ref: https://console.cloud.tencent.com/api/explorer?Product=hunyuan&Version=2023-09-01&Action=ChatCompletions
|
||||
type hunyuanTextGenRequest struct {
|
||||
@@ -105,8 +102,8 @@ func (m *hunyuanProviderInitializer) ValidateConfig(config *ProviderConfig) erro
|
||||
|
||||
func (m *hunyuanProviderInitializer) DefaultCapabilities() map[string]string {
|
||||
return map[string]string{
|
||||
string(ApiNameChatCompletion): hunyuanOpenAiRequestPath,
|
||||
string(ApiNameEmbeddings): hunyuanOpenAiEmbeddings,
|
||||
string(ApiNameChatCompletion): PathOpenAIChatCompletions,
|
||||
string(ApiNameEmbeddings): PathOpenAIEmbeddings,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -324,7 +321,7 @@ func (m *hunyuanProvider) OnStreamingResponseBody(ctx wrapper.HttpContext, name
|
||||
}
|
||||
|
||||
// hunyuan的流式返回:
|
||||
//data: {"Note":"以上内容为AI生成,不代表开发者立场,请勿删除或修改本标记","Choices":[{"Delta":{"Role":"assistant","Content":"有助于"},"FinishReason":""}],"Created":1716359713,"Id":"086b6b19-8b2c-4def-a65c-db6a7bc86acd","Usage":{"PromptTokens":7,"CompletionTokens":145,"TotalTokens":152}}
|
||||
// data: {"Note":"以上内容为AI生成,不代表开发者立场,请勿删除或修改本标记","Choices":[{"Delta":{"Role":"assistant","Content":"有助于"},"FinishReason":""}],"Created":1716359713,"Id":"086b6b19-8b2c-4def-a65c-db6a7bc86acd","Usage":{"PromptTokens":7,"CompletionTokens":145,"TotalTokens":152}}
|
||||
|
||||
// openai的流式返回
|
||||
// data: {"id": "chatcmpl-7QyqpwdfhqwajicIEznoc6Q47XAyW", "object": "chat.completion.chunk", "created": 1677664795, "model": "gpt-3.5-turbo-0613", "choices": [{"delta": {"content": "The "}, "index": 0, "finish_reason": null}]}
|
||||
@@ -338,7 +335,7 @@ func (m *hunyuanProvider) OnStreamingResponseBody(ctx wrapper.HttpContext, name
|
||||
}
|
||||
|
||||
// 初始化处理下标,以及将要返回的处理过的chunks
|
||||
var newEventPivot = -1
|
||||
newEventPivot := -1
|
||||
var outputBuffer []byte
|
||||
|
||||
// 从buffer区取出若干完整的chunk,将其转为openAI格式后返回
|
||||
@@ -451,7 +448,6 @@ func (m *hunyuanProvider) TransformResponseBody(ctx wrapper.HttpContext, apiName
|
||||
}
|
||||
|
||||
func (m *hunyuanProvider) insertContextMessageIntoHunyuanRequest(request *hunyuanTextGenRequest, content string) {
|
||||
|
||||
fileMessage := hunyuanChatMessage{
|
||||
Role: roleSystem,
|
||||
Content: content,
|
||||
|
||||
@@ -20,8 +20,10 @@ const (
|
||||
|
||||
httpStatus200 = "200"
|
||||
|
||||
contentTypeText = "text"
|
||||
contentTypeImageUrl = "image_url"
|
||||
contentTypeText = "text"
|
||||
contentTypeImageUrl = "image_url"
|
||||
contentTypeInputAudio = "input_audio"
|
||||
contentTypeFile = "file"
|
||||
|
||||
reasoningStartTag = "<think>"
|
||||
reasoningEndTag = "</think>"
|
||||
@@ -53,11 +55,40 @@ type chatCompletionRequest struct {
|
||||
Temperature float64 `json:"temperature,omitempty"`
|
||||
TopP float64 `json:"top_p,omitempty"`
|
||||
Tools []tool `json:"tools,omitempty"`
|
||||
ToolChoice *toolChoice `json:"tool_choice,omitempty"`
|
||||
ToolChoice interface{} `json:"tool_choice,omitempty"`
|
||||
ParallelToolCalls bool `json:"parallel_tool_calls,omitempty"`
|
||||
User string `json:"user,omitempty"`
|
||||
}
|
||||
|
||||
func (c *chatCompletionRequest) getMaxTokens() int {
|
||||
if c.MaxCompletionTokens > 0 {
|
||||
return c.MaxCompletionTokens
|
||||
}
|
||||
return c.MaxTokens
|
||||
}
|
||||
|
||||
func (c *chatCompletionRequest) getToolChoiceString() string {
|
||||
if c.ToolChoice == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
if tc, ok := c.ToolChoice.(string); ok {
|
||||
return tc
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (c *chatCompletionRequest) getToolChoiceObject() *toolChoice {
|
||||
if c.ToolChoice == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if tc, ok := c.ToolChoice.(*toolChoice); ok {
|
||||
return tc
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type CompletionRequest struct {
|
||||
Model string `json:"model"`
|
||||
Prompt string `json:"prompt"`
|
||||
@@ -200,13 +231,26 @@ func (m *chatMessage) handleStreamingReasoningContent(ctx wrapper.HttpContext, r
|
||||
}
|
||||
}
|
||||
|
||||
type messageContent struct {
|
||||
Type string `json:"type,omitempty"`
|
||||
Text string `json:"text"`
|
||||
ImageUrl *imageUrl `json:"image_url,omitempty"`
|
||||
type chatMessageContent struct {
|
||||
Type string `json:"type,omitempty"`
|
||||
Text string `json:"text"`
|
||||
ImageUrl *chatMessageContentImageUrl `json:"image_url,omitempty"`
|
||||
File *chatMessageContentFile `json:"file,omitempty"`
|
||||
InputAudio *chatMessageContentAudio `json:"input_audio,omitempty"`
|
||||
}
|
||||
|
||||
type imageUrl struct {
|
||||
type chatMessageContentAudio struct {
|
||||
Data string `json:"data"`
|
||||
Format string `json:"format"`
|
||||
}
|
||||
|
||||
type chatMessageContentFile struct {
|
||||
FileData string `json:"file_data,omitempty"`
|
||||
FileId string `json:"file_id,omitempty"`
|
||||
FileName string `json:"file_name,omitempty"`
|
||||
}
|
||||
|
||||
type chatMessageContentImageUrl struct {
|
||||
Url string `json:"url,omitempty"`
|
||||
Detail string `json:"detail,omitempty"`
|
||||
}
|
||||
@@ -266,11 +310,11 @@ func (m *chatMessage) StringContent() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *chatMessage) ParseContent() []messageContent {
|
||||
var contentList []messageContent
|
||||
func (m *chatMessage) ParseContent() []chatMessageContent {
|
||||
var contentList []chatMessageContent
|
||||
content, ok := m.Content.(string)
|
||||
if ok {
|
||||
contentList = append(contentList, messageContent{
|
||||
contentList = append(contentList, chatMessageContent{
|
||||
Type: contentTypeText,
|
||||
Text: content,
|
||||
})
|
||||
@@ -286,18 +330,43 @@ func (m *chatMessage) ParseContent() []messageContent {
|
||||
switch contentMap["type"] {
|
||||
case contentTypeText:
|
||||
if subStr, ok := contentMap[contentTypeText].(string); ok {
|
||||
contentList = append(contentList, messageContent{
|
||||
contentList = append(contentList, chatMessageContent{
|
||||
Type: contentTypeText,
|
||||
Text: subStr,
|
||||
})
|
||||
}
|
||||
case contentTypeImageUrl:
|
||||
if subObj, ok := contentMap[contentTypeImageUrl].(map[string]any); ok {
|
||||
contentList = append(contentList, messageContent{
|
||||
msg := chatMessageContent{
|
||||
Type: contentTypeImageUrl,
|
||||
ImageUrl: &imageUrl{
|
||||
ImageUrl: &chatMessageContentImageUrl{
|
||||
Url: subObj["url"].(string),
|
||||
},
|
||||
}
|
||||
if detail, ok := subObj["detail"].(string); ok {
|
||||
msg.ImageUrl.Detail = detail
|
||||
}
|
||||
contentList = append(contentList, msg)
|
||||
}
|
||||
case contentTypeInputAudio:
|
||||
if subObj, ok := contentMap[contentTypeInputAudio].(map[string]any); ok {
|
||||
contentList = append(contentList, chatMessageContent{
|
||||
Type: contentTypeInputAudio,
|
||||
InputAudio: &chatMessageContentAudio{
|
||||
Data: subObj["data"].(string),
|
||||
Format: subObj["format"].(string),
|
||||
},
|
||||
})
|
||||
}
|
||||
case contentTypeFile:
|
||||
if subObj, ok := contentMap[contentTypeFile].(map[string]any); ok {
|
||||
contentList = append(contentList, chatMessageContent{
|
||||
Type: contentTypeFile,
|
||||
File: &chatMessageContentFile{
|
||||
FileId: subObj["file_id"].(string),
|
||||
// FileName: subObj["file_name"].(string),
|
||||
// FileData: subObj["file_data"].(string),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -356,10 +425,39 @@ func (e *StreamEvent) ToHttpString() string {
|
||||
|
||||
// https://platform.openai.com/docs/guides/images
|
||||
type imageGenerationRequest struct {
|
||||
Model string `json:"model"`
|
||||
Prompt string `json:"prompt"`
|
||||
N int `json:"n,omitempty"`
|
||||
Size string `json:"size,omitempty"`
|
||||
Model string `json:"model"`
|
||||
Prompt string `json:"prompt"`
|
||||
Background string `json:"background,omitempty"`
|
||||
Moderation string `json:"moderation,omitempty"`
|
||||
OutputCompression int `json:"output_compression,omitempty"`
|
||||
OutputFormat string `json:"output_format,omitempty"`
|
||||
Quality string `json:"quality,omitempty"`
|
||||
ResponseFormat string `json:"response_format,omitempty"`
|
||||
Style string `json:"style,omitempty"`
|
||||
N int `json:"n,omitempty"`
|
||||
Size string `json:"size,omitempty"`
|
||||
}
|
||||
|
||||
type imageGenerationData struct {
|
||||
URL string `json:"url,omitempty"`
|
||||
B64 string `json:"b64_json,omitempty"`
|
||||
RevisedPrompt string `json:"revised_prompt,omitempty"`
|
||||
}
|
||||
|
||||
type imageGenerationUsage struct {
|
||||
TotalTokens int `json:"total_tokens"`
|
||||
InputTokens int `json:"input_tokens"`
|
||||
OutputTokens int `json:"output_tokens"`
|
||||
InputTokensDetails struct {
|
||||
TextTokens int `json:"text_tokens"`
|
||||
ImageTokens int `json:"image_tokens"`
|
||||
} `json:"input_tokens_details"`
|
||||
}
|
||||
|
||||
type imageGenerationResponse struct {
|
||||
Created int64 `json:"created"`
|
||||
Data []imageGenerationData `json:"data"`
|
||||
Usage *imageGenerationUsage `json:"usage,omitempty"`
|
||||
}
|
||||
|
||||
// https://platform.openai.com/docs/guides/speech-to-text
|
||||
|
||||
@@ -18,12 +18,10 @@ import (
|
||||
// moonshotProvider is the provider for Moonshot AI service.
|
||||
|
||||
const (
|
||||
moonshotDomain = "api.moonshot.cn"
|
||||
moonshotChatCompletionPath = "/v1/chat/completions"
|
||||
moonshotDomain = "api.moonshot.cn"
|
||||
)
|
||||
|
||||
type moonshotProviderInitializer struct {
|
||||
}
|
||||
type moonshotProviderInitializer struct{}
|
||||
|
||||
func (m *moonshotProviderInitializer) ValidateConfig(config *ProviderConfig) error {
|
||||
if config.moonshotFileId != "" && config.context != nil {
|
||||
@@ -37,7 +35,8 @@ func (m *moonshotProviderInitializer) ValidateConfig(config *ProviderConfig) err
|
||||
|
||||
func (m *moonshotProviderInitializer) DefaultCapabilities() map[string]string {
|
||||
return map[string]string{
|
||||
string(ApiNameChatCompletion): moonshotChatCompletionPath,
|
||||
string(ApiNameChatCompletion): PathOpenAIChatCompletions,
|
||||
string(ApiNameModels): PathOpenAIModels,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ func (m *ollamaProviderInitializer) DefaultCapabilities() map[string]string {
|
||||
// ollama的chat接口path和OpenAI的chat接口一样
|
||||
string(ApiNameChatCompletion): PathOpenAIChatCompletions,
|
||||
string(ApiNameEmbeddings): PathOpenAIEmbeddings,
|
||||
string(ApiNameModels): PathOpenAIModels,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,16 +15,10 @@ import (
|
||||
// openaiProvider is the provider for OpenAI service.
|
||||
|
||||
const (
|
||||
defaultOpenaiDomain = "api.openai.com"
|
||||
defaultOpenaiChatCompletionPath = "/v1/chat/completions"
|
||||
defaultOpenaiCompletionPath = "/v1/completions"
|
||||
defaultOpenaiEmbeddingsPath = "/v1/embeddings"
|
||||
defaultOpenaiAudioSpeech = "/v1/audio/speech"
|
||||
defaultOpenaiImageGeneration = "/v1/images/generations"
|
||||
defaultOpenaiDomain = "api.openai.com"
|
||||
)
|
||||
|
||||
type openaiProviderInitializer struct {
|
||||
}
|
||||
type openaiProviderInitializer struct{}
|
||||
|
||||
func (m *openaiProviderInitializer) ValidateConfig(config *ProviderConfig) error {
|
||||
return nil
|
||||
@@ -32,11 +26,21 @@ func (m *openaiProviderInitializer) ValidateConfig(config *ProviderConfig) error
|
||||
|
||||
func (m *openaiProviderInitializer) DefaultCapabilities() map[string]string {
|
||||
return map[string]string{
|
||||
string(ApiNameCompletion): defaultOpenaiCompletionPath,
|
||||
string(ApiNameChatCompletion): defaultOpenaiChatCompletionPath,
|
||||
string(ApiNameEmbeddings): defaultOpenaiEmbeddingsPath,
|
||||
string(ApiNameImageGeneration): defaultOpenaiImageGeneration,
|
||||
string(ApiNameAudioSpeech): defaultOpenaiAudioSpeech,
|
||||
string(ApiNameCompletion): PathOpenAICompletions,
|
||||
string(ApiNameChatCompletion): PathOpenAIChatCompletions,
|
||||
string(ApiNameEmbeddings): PathOpenAIEmbeddings,
|
||||
string(ApiNameImageGeneration): PathOpenAIImageGeneration,
|
||||
string(ApiNameImageEdit): PathOpenAIImageEdit,
|
||||
string(ApiNameImageVariation): PathOpenAIImageVariation,
|
||||
string(ApiNameAudioSpeech): PathOpenAIAudioSpeech,
|
||||
string(ApiNameModels): PathOpenAIModels,
|
||||
string(ApiNameFiles): PathOpenAIFiles,
|
||||
string(ApiNameRetrieveFile): PathOpenAIRetrieveFile,
|
||||
string(ApiNameRetrieveFileContent): PathOpenAIRetrieveFileContent,
|
||||
string(ApiNameBatches): PathOpenAIBatches,
|
||||
string(ApiNameRetrieveBatch): PathOpenAIRetrieveBatch,
|
||||
string(ApiNameCancelBatch): PathOpenAICancelBatch,
|
||||
string(ApiNameResponses): PathOpenAIResponses,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +48,11 @@ func isDirectPath(path string) bool {
|
||||
return strings.HasSuffix(path, "/completions") ||
|
||||
strings.HasSuffix(path, "/embeddings") ||
|
||||
strings.HasSuffix(path, "/audio/speech") ||
|
||||
strings.HasSuffix(path, "/images/generations")
|
||||
strings.HasSuffix(path, "/images/generations") ||
|
||||
strings.HasSuffix(path, "/images/variations") ||
|
||||
strings.HasSuffix(path, "/images/edits") ||
|
||||
strings.HasSuffix(path, "/models") ||
|
||||
strings.HasSuffix(path, "/responses")
|
||||
}
|
||||
|
||||
func (m *openaiProviderInitializer) CreateProvider(config ProviderConfig) (Provider, error) {
|
||||
@@ -98,15 +106,14 @@ func (m *openaiProvider) OnRequestHeaders(ctx wrapper.HttpContext, apiName ApiNa
|
||||
}
|
||||
|
||||
func (m *openaiProvider) TransformRequestHeaders(ctx wrapper.HttpContext, apiName ApiName, headers http.Header) {
|
||||
if m.customPath != "" {
|
||||
if m.isDirectCustomPath || apiName == "" {
|
||||
util.OverwriteRequestPathHeader(headers, m.customPath)
|
||||
} else {
|
||||
util.OverwriteRequestPathHeaderByCapability(headers, string(apiName), m.config.capabilities)
|
||||
}
|
||||
} else {
|
||||
if m.isDirectCustomPath {
|
||||
util.OverwriteRequestPathHeader(headers, m.customPath)
|
||||
}
|
||||
|
||||
if apiName != "" {
|
||||
util.OverwriteRequestPathHeaderByCapability(headers, string(apiName), m.config.capabilities)
|
||||
}
|
||||
|
||||
if m.customDomain != "" {
|
||||
util.OverwriteRequestHostHeader(headers, m.customDomain)
|
||||
} else {
|
||||
@@ -119,7 +126,7 @@ func (m *openaiProvider) TransformRequestHeaders(ctx wrapper.HttpContext, apiNam
|
||||
}
|
||||
|
||||
func (m *openaiProvider) OnRequestBody(ctx wrapper.HttpContext, apiName ApiName, body []byte) (types.Action, error) {
|
||||
if apiName != ApiNameChatCompletion {
|
||||
if !m.config.needToProcessRequestBody(apiName) {
|
||||
// We don't need to process the request body for other APIs.
|
||||
return types.ActionContinue, nil
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/extensions/ai-proxy/util"
|
||||
@@ -16,27 +17,47 @@ import (
|
||||
"github.com/tidwall/sjson"
|
||||
)
|
||||
|
||||
type ApiName string
|
||||
type Pointcut string
|
||||
type (
|
||||
ApiName string
|
||||
Pointcut string
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
// ApiName 格式 {vendor}/{version}/{apitype}
|
||||
// 表示遵循 厂商/版本/接口类型 的格式
|
||||
// 目前openai是事实意义上的标准,但是也有其他厂商存在其他任务的一些可能的标准,比如cohere的rerank
|
||||
ApiNameCompletion ApiName = "openai/v1/completions"
|
||||
ApiNameChatCompletion ApiName = "openai/v1/chatcompletions"
|
||||
ApiNameEmbeddings ApiName = "openai/v1/embeddings"
|
||||
ApiNameImageGeneration ApiName = "openai/v1/imagegeneration"
|
||||
ApiNameAudioSpeech ApiName = "openai/v1/audiospeech"
|
||||
ApiNameFiles ApiName = "openai/v1/files"
|
||||
ApiNameBatches ApiName = "openai/v1/batches"
|
||||
ApiNameCompletion ApiName = "openai/v1/completions"
|
||||
ApiNameChatCompletion ApiName = "openai/v1/chatcompletions"
|
||||
ApiNameEmbeddings ApiName = "openai/v1/embeddings"
|
||||
ApiNameImageGeneration ApiName = "openai/v1/imagegeneration"
|
||||
ApiNameImageEdit ApiName = "openai/v1/imageedit"
|
||||
ApiNameImageVariation ApiName = "openai/v1/imagevariation"
|
||||
ApiNameAudioSpeech ApiName = "openai/v1/audiospeech"
|
||||
ApiNameFiles ApiName = "openai/v1/files"
|
||||
ApiNameRetrieveFile ApiName = "openai/v1/retrievefile"
|
||||
ApiNameRetrieveFileContent ApiName = "openai/v1/retrievefilecontent"
|
||||
ApiNameBatches ApiName = "openai/v1/batches"
|
||||
ApiNameRetrieveBatch ApiName = "openai/v1/retrievebatch"
|
||||
ApiNameCancelBatch ApiName = "openai/v1/cancelbatch"
|
||||
ApiNameModels ApiName = "openai/v1/models"
|
||||
ApiNameResponses ApiName = "openai/v1/responses"
|
||||
|
||||
PathOpenAICompletions = "/v1/completions"
|
||||
PathOpenAIChatCompletions = "/v1/chat/completions"
|
||||
PathOpenAIEmbeddings = "/v1/embeddings"
|
||||
PathOpenAIFiles = "/v1/files"
|
||||
PathOpenAIBatches = "/v1/batches"
|
||||
PathOpenAICompletions = "/v1/completions"
|
||||
PathOpenAIChatCompletions = "/v1/chat/completions"
|
||||
PathOpenAIEmbeddings = "/v1/embeddings"
|
||||
PathOpenAIFiles = "/v1/files"
|
||||
PathOpenAIRetrieveFile = "/v1/files/{file_id}"
|
||||
PathOpenAIRetrieveFileContent = "/v1/files/{file_id}/content"
|
||||
PathOpenAIBatches = "/v1/batches"
|
||||
PathOpenAIRetrieveBatch = "/v1/batches/{batch_id}"
|
||||
PathOpenAICancelBatch = "/v1/batches/{batch_id}/cancel"
|
||||
PathOpenAIModels = "/v1/models"
|
||||
PathOpenAIImageGeneration = "/v1/images/generations"
|
||||
PathOpenAIImageEdit = "/v1/images/edits"
|
||||
PathOpenAIImageVariation = "/v1/images/variations"
|
||||
PathOpenAIAudioSpeech = "/v1/audio/speech"
|
||||
PathOpenAIResponses = "/v1/responses"
|
||||
|
||||
// TODO: 以下是一些非标准的API名称,需要进一步确认是否支持
|
||||
ApiNameCohereV1Rerank ApiName = "cohere/v1/rerank"
|
||||
@@ -69,6 +90,7 @@ const (
|
||||
providerTypeTogetherAI = "together-ai"
|
||||
providerTypeDify = "dify"
|
||||
providerTypeBedrock = "bedrock"
|
||||
providerTypeVertex = "vertex"
|
||||
|
||||
protocolOpenAI = "openai"
|
||||
protocolOriginal = "original"
|
||||
@@ -140,6 +162,7 @@ var (
|
||||
providerTypeTogetherAI: &togetherAIProviderInitializer{},
|
||||
providerTypeDify: &difyProviderInitializer{},
|
||||
providerTypeBedrock: &bedrockProviderInitializer{},
|
||||
providerTypeVertex: &vertexProviderInitializer{},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -269,14 +292,29 @@ type ProviderConfig struct {
|
||||
// @Description zh-CN 配置一个外部获取对话上下文的文件来源,用于在AI请求中补充对话上下文
|
||||
context *ContextConfig `required:"false" yaml:"context" json:"context"`
|
||||
// @Title zh-CN 版本
|
||||
// @Description zh-CN 请求AI服务的版本,目前仅适用于Claude AI服务
|
||||
claudeVersion string `required:"false" yaml:"version" json:"version"`
|
||||
// @Description zh-CN 请求AI服务的版本,目前仅适用于 Gemini 和 Claude AI服务
|
||||
apiVersion string `required:"false" yaml:"apiVersion" json:"apiVersion"`
|
||||
// @Title zh-CN Cloudflare Account ID
|
||||
// @Description zh-CN 仅适用于 Cloudflare Workers AI 服务。参考:https://developers.cloudflare.com/workers-ai/get-started/rest-api/#2-run-a-model-via-api
|
||||
cloudflareAccountId string `required:"false" yaml:"cloudflareAccountId" json:"cloudflareAccountId"`
|
||||
// @Title zh-CN Gemini AI内容过滤和安全级别设定
|
||||
// @Description zh-CN 仅适用于 Gemini AI 服务。参考:https://ai.google.dev/gemini-api/docs/safety-settings
|
||||
geminiSafetySetting map[string]string `required:"false" yaml:"geminiSafetySetting" json:"geminiSafetySetting"`
|
||||
// @Title zh-CN Vertex AI访问区域
|
||||
// @Description zh-CN 仅适用于Vertex AI服务。如需查看支持的区域的完整列表,请参阅https://cloud.google.com/vertex-ai/generative-ai/docs/learn/locations?hl=zh-cn#available-regions
|
||||
vertexRegion string `required:"false" yaml:"vertexRegion" json:"vertexRegion"`
|
||||
// @Title zh-CN Vertex AI项目Id
|
||||
// @Description zh-CN 仅适用于Vertex AI服务。创建和管理项目请参阅https://cloud.google.com/resource-manager/docs/creating-managing-projects?hl=zh-cn#identifiers
|
||||
vertexProjectId string `required:"false" yaml:"vertexProjectId" json:"vertexProjectId"`
|
||||
// @Title zh-CN Vertex 认证秘钥
|
||||
// @Description zh-CN 用于Google服务账号认证的完整JSON密钥文件内容,获取可参考https://cloud.google.com/iam/docs/keys-create-delete?hl=zh-cn#iam-service-account-keys-create-console
|
||||
vertexAuthKey string `required:"false" yaml:"vertexAuthKey" json:"vertexAuthKey"`
|
||||
// @Title zh-CN Vertex 认证服务名
|
||||
// @Description zh-CN 用于Google服务账号认证的服务,DNS类型的服务名
|
||||
vertexAuthServiceName string `required:"false" yaml:"vertexAuthServiceName" json:"vertexAuthServiceName"`
|
||||
// @Title zh-CN Vertex token刷新提前时间
|
||||
// @Description zh-CN 用于Google服务账号认证,access token过期时间判定提前刷新,单位为秒,默认值为60秒
|
||||
vertexTokenRefreshAhead int64 `required:"false" yaml:"vertexTokenRefreshAhead" json:"vertexTokenRefreshAhead"`
|
||||
// @Title zh-CN 翻译服务需指定的目标语种
|
||||
// @Description zh-CN 翻译结果的语种,目前仅适用于DeepL服务。
|
||||
targetLang string `required:"false" yaml:"targetLang" json:"targetLang"`
|
||||
@@ -354,7 +392,13 @@ func (c *ProviderConfig) FromJson(json gjson.Result) {
|
||||
c.context = &ContextConfig{}
|
||||
c.context.FromJson(contextJson)
|
||||
}
|
||||
c.claudeVersion = json.Get("claudeVersion").String()
|
||||
|
||||
// 这里获取 claudeVersion 字段,与结构体中定义 yaml/json 的 tag 不一致
|
||||
c.apiVersion = json.Get("claudeVersion").String()
|
||||
if c.apiVersion == "" {
|
||||
// 增加获取 version 字段,用于适配其他模型的配置,并保持与结构体中定义的 tag 一致
|
||||
c.apiVersion = json.Get("apiVersion").String()
|
||||
}
|
||||
c.hunyuanAuthId = json.Get("hunyuanAuthId").String()
|
||||
c.hunyuanAuthKey = json.Get("hunyuanAuthKey").String()
|
||||
c.awsAccessKey = json.Get("awsAccessKey").String()
|
||||
@@ -363,12 +407,20 @@ func (c *ProviderConfig) FromJson(json gjson.Result) {
|
||||
c.minimaxApiType = json.Get("minimaxApiType").String()
|
||||
c.minimaxGroupId = json.Get("minimaxGroupId").String()
|
||||
c.cloudflareAccountId = json.Get("cloudflareAccountId").String()
|
||||
if c.typ == providerTypeGemini {
|
||||
if c.typ == providerTypeGemini || c.typ == providerTypeVertex {
|
||||
c.geminiSafetySetting = make(map[string]string)
|
||||
for k, v := range json.Get("geminiSafetySetting").Map() {
|
||||
c.geminiSafetySetting[k] = v.String()
|
||||
}
|
||||
}
|
||||
c.vertexRegion = json.Get("vertexRegion").String()
|
||||
c.vertexProjectId = json.Get("vertexProjectId").String()
|
||||
c.vertexAuthKey = json.Get("vertexAuthKey").String()
|
||||
c.vertexAuthServiceName = json.Get("vertexAuthServiceName").String()
|
||||
c.vertexTokenRefreshAhead = json.Get("vertexTokenRefreshAhead").Int()
|
||||
if c.vertexTokenRefreshAhead == 0 {
|
||||
c.vertexTokenRefreshAhead = 60
|
||||
}
|
||||
c.targetLang = json.Get("targetLang").String()
|
||||
|
||||
if schemaValue, ok := json.Get("responseJsonSchema").Value().(map[string]interface{}); ok {
|
||||
@@ -437,6 +489,8 @@ func (c *ProviderConfig) FromJson(json gjson.Result) {
|
||||
case string(ApiNameChatCompletion),
|
||||
string(ApiNameEmbeddings),
|
||||
string(ApiNameImageGeneration),
|
||||
string(ApiNameImageVariation),
|
||||
string(ApiNameImageEdit),
|
||||
string(ApiNameAudioSpeech),
|
||||
string(ApiNameCohereV1Rerank):
|
||||
c.capabilities[capability] = pathJson.String()
|
||||
@@ -532,6 +586,11 @@ func (c *ProviderConfig) parseRequestAndMapModel(ctx wrapper.HttpContext, reques
|
||||
return err
|
||||
}
|
||||
return c.setRequestModel(ctx, req)
|
||||
case *imageGenerationRequest:
|
||||
if err := decodeImageGenerationRequest(body, req); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.setRequestModel(ctx, req)
|
||||
default:
|
||||
return errors.New("unsupported request type")
|
||||
}
|
||||
@@ -545,6 +604,8 @@ func (c *ProviderConfig) setRequestModel(ctx wrapper.HttpContext, request interf
|
||||
model = &req.Model
|
||||
case *embeddingsRequest:
|
||||
model = &req.Model
|
||||
case *imageGenerationRequest:
|
||||
model = &req.Model
|
||||
default:
|
||||
return errors.New("unsupported request type")
|
||||
}
|
||||
@@ -587,13 +648,25 @@ func doGetMappedModel(model string, modelMapping map[string]string) string {
|
||||
}
|
||||
|
||||
for k, v := range modelMapping {
|
||||
if k == wildcard || !strings.HasSuffix(k, wildcard) {
|
||||
if k == wildcard {
|
||||
continue
|
||||
}
|
||||
k = strings.TrimSuffix(k, wildcard)
|
||||
if strings.HasPrefix(model, k) {
|
||||
log.Debugf("model [%s] is mapped to [%s] via prefix [%s]", model, v, k)
|
||||
return v
|
||||
if strings.HasSuffix(k, wildcard) {
|
||||
k = strings.TrimSuffix(k, wildcard)
|
||||
if strings.HasPrefix(model, k) {
|
||||
log.Debugf("model [%s] is mapped to [%s] via prefix [%s]", model, v, k)
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
if strings.HasPrefix(k, "~") {
|
||||
k = strings.TrimPrefix(k, "~")
|
||||
re := regexp.MustCompile(k)
|
||||
if re.MatchString(model) {
|
||||
v = re.ReplaceAllString(model, v)
|
||||
log.Debugf("model [%s] is mapped to [%s] via regex [%s]", model, v, k)
|
||||
return v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -694,7 +767,8 @@ func (c *ProviderConfig) setDefaultCapabilities(capabilities map[string]string)
|
||||
}
|
||||
|
||||
func (c *ProviderConfig) handleRequestBody(
|
||||
provider Provider, contextCache *contextCache, ctx wrapper.HttpContext, apiName ApiName, body []byte) (types.Action, error) {
|
||||
provider Provider, contextCache *contextCache, ctx wrapper.HttpContext, apiName ApiName, body []byte,
|
||||
) (types.Action, error) {
|
||||
// use original protocol
|
||||
if c.IsOriginal() {
|
||||
return types.ActionContinue, nil
|
||||
@@ -762,3 +836,16 @@ func (c *ProviderConfig) DefaultTransformResponseHeaders(ctx wrapper.HttpContext
|
||||
headers.Del("Content-Length")
|
||||
}
|
||||
}
|
||||
|
||||
func (c *ProviderConfig) needToProcessRequestBody(apiName ApiName) bool {
|
||||
switch apiName {
|
||||
case ApiNameChatCompletion,
|
||||
ApiNameEmbeddings,
|
||||
ApiNameImageGeneration,
|
||||
ApiNameImageEdit,
|
||||
ApiNameImageVariation,
|
||||
ApiNameAudioSpeech:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -23,13 +23,19 @@ import (
|
||||
const (
|
||||
qwenResultFormatMessage = "message"
|
||||
|
||||
qwenDefaultDomain = "dashscope.aliyuncs.com"
|
||||
qwenChatCompletionPath = "/api/v1/services/aigc/text-generation/generation"
|
||||
qwenTextEmbeddingPath = "/api/v1/services/embeddings/text-embedding/text-embedding"
|
||||
qwenChatCompatiblePath = "/compatible-mode/v1/chat/completions"
|
||||
qwenTextEmbeddingCompatiblePath = "/compatible-mode/v1/embeddings"
|
||||
qwenBailianPath = "/api/v1/apps"
|
||||
qwenMultimodalGenerationPath = "/api/v1/services/aigc/multimodal-generation/generation"
|
||||
qwenDefaultDomain = "dashscope.aliyuncs.com"
|
||||
qwenChatCompletionPath = "/api/v1/services/aigc/text-generation/generation"
|
||||
qwenTextEmbeddingPath = "/api/v1/services/embeddings/text-embedding/text-embedding"
|
||||
qwenCompatibleChatCompletionPath = "/compatible-mode/v1/chat/completions"
|
||||
qwenCompatibleCompletionsPath = "/compatible-mode/v1/completions"
|
||||
qwenCompatibleTextEmbeddingPath = "/compatible-mode/v1/embeddings"
|
||||
qwenCompatibleFilesPath = "/compatible-mode/v1/files"
|
||||
qwenCompatibleRetrieveFilePath = "/compatible-mode/v1/files/{file_id}"
|
||||
qwenCompatibleRetrieveFileContentPath = "/compatible-mode/v1/files/{file_id}/content"
|
||||
qwenCompatibleBatchesPath = "/compatible-mode/v1/batches"
|
||||
qwenCompatibleRetrieveBatchPath = "/compatible-mode/v1/batches/{batch_id}"
|
||||
qwenBailianPath = "/api/v1/apps"
|
||||
qwenMultimodalGenerationPath = "/api/v1/services/aigc/multimodal-generation/generation"
|
||||
|
||||
qwenTopPMin = 0.000001
|
||||
qwenTopPMax = 0.999999
|
||||
@@ -40,8 +46,7 @@ const (
|
||||
qwenVlModelPrefixName = "qwen-vl"
|
||||
)
|
||||
|
||||
type qwenProviderInitializer struct {
|
||||
}
|
||||
type qwenProviderInitializer struct{}
|
||||
|
||||
func (m *qwenProviderInitializer) ValidateConfig(config *ProviderConfig) error {
|
||||
if len(config.qwenFileIds) != 0 && config.context != nil {
|
||||
@@ -56,8 +61,14 @@ func (m *qwenProviderInitializer) ValidateConfig(config *ProviderConfig) error {
|
||||
func (m *qwenProviderInitializer) DefaultCapabilities(qwenEnableCompatible bool) map[string]string {
|
||||
if qwenEnableCompatible {
|
||||
return map[string]string{
|
||||
string(ApiNameChatCompletion): qwenChatCompatiblePath,
|
||||
string(ApiNameEmbeddings): qwenTextEmbeddingCompatiblePath,
|
||||
string(ApiNameChatCompletion): qwenCompatibleChatCompletionPath,
|
||||
string(ApiNameEmbeddings): qwenCompatibleTextEmbeddingPath,
|
||||
string(ApiNameCompletion): qwenCompatibleCompletionsPath,
|
||||
string(ApiNameFiles): qwenCompatibleFilesPath,
|
||||
string(ApiNameRetrieveFile): qwenCompatibleRetrieveFilePath,
|
||||
string(ApiNameRetrieveFileContent): qwenCompatibleRetrieveFileContentPath,
|
||||
string(ApiNameBatches): qwenCompatibleBatchesPath,
|
||||
string(ApiNameRetrieveBatch): qwenCompatibleRetrieveBatchPath,
|
||||
}
|
||||
} else {
|
||||
return map[string]string{
|
||||
@@ -673,10 +684,10 @@ func (m *qwenProvider) GetApiName(path string) ApiName {
|
||||
case strings.Contains(path, qwenChatCompletionPath),
|
||||
strings.Contains(path, qwenMultimodalGenerationPath),
|
||||
strings.Contains(path, qwenBailianPath),
|
||||
strings.Contains(path, qwenChatCompatiblePath):
|
||||
strings.Contains(path, qwenCompatibleChatCompletionPath):
|
||||
return ApiNameChatCompletion
|
||||
case strings.Contains(path, qwenTextEmbeddingPath),
|
||||
strings.Contains(path, qwenTextEmbeddingCompatiblePath):
|
||||
strings.Contains(path, qwenCompatibleTextEmbeddingPath):
|
||||
return ApiNameEmbeddings
|
||||
default:
|
||||
return ""
|
||||
|
||||
@@ -25,6 +25,13 @@ func decodeEmbeddingsRequest(body []byte, request *embeddingsRequest) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func decodeImageGenerationRequest(body []byte, request *imageGenerationRequest) error {
|
||||
if err := json.Unmarshal(body, request); err != nil {
|
||||
return fmt.Errorf("unable to unmarshal request: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func replaceJsonRequestBody(request interface{}) error {
|
||||
body, err := json.Marshal(request)
|
||||
if err != nil {
|
||||
|
||||
@@ -15,12 +15,10 @@ import (
|
||||
|
||||
// sparkProvider is the provider for SparkLLM AI service.
|
||||
const (
|
||||
sparkHost = "spark-api-open.xf-yun.com"
|
||||
sparkChatCompletionPath = "/v1/chat/completions"
|
||||
sparkHost = "spark-api-open.xf-yun.com"
|
||||
)
|
||||
|
||||
type sparkProviderInitializer struct {
|
||||
}
|
||||
type sparkProviderInitializer struct{}
|
||||
|
||||
type sparkProvider struct {
|
||||
config ProviderConfig
|
||||
@@ -58,7 +56,7 @@ func (i *sparkProviderInitializer) ValidateConfig(config *ProviderConfig) error
|
||||
|
||||
func (i *sparkProviderInitializer) DefaultCapabilities() map[string]string {
|
||||
return map[string]string{
|
||||
string(ApiNameChatCompletion): sparkChatCompletionPath,
|
||||
string(ApiNameChatCompletion): PathOpenAIChatCompletions,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,12 +10,10 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
stepfunDomain = "api.stepfun.com"
|
||||
stepfunChatCompletionPath = "/v1/chat/completions"
|
||||
stepfunDomain = "api.stepfun.com"
|
||||
)
|
||||
|
||||
type stepfunProviderInitializer struct {
|
||||
}
|
||||
type stepfunProviderInitializer struct{}
|
||||
|
||||
func (m *stepfunProviderInitializer) ValidateConfig(config *ProviderConfig) error {
|
||||
if config.apiTokens == nil || len(config.apiTokens) == 0 {
|
||||
@@ -27,7 +25,7 @@ func (m *stepfunProviderInitializer) ValidateConfig(config *ProviderConfig) erro
|
||||
func (m *stepfunProviderInitializer) DefaultCapabilities() map[string]string {
|
||||
return map[string]string{
|
||||
// stepfun的chat接口path和OpenAI的chat接口一样
|
||||
string(ApiNameChatCompletion): stepfunChatCompletionPath,
|
||||
string(ApiNameChatCompletion): PathOpenAIChatCompletions,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,8 +11,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
togetherAIDomain = "api.together.xyz"
|
||||
togetherAICompletionPath = "/v1/chat/completions"
|
||||
togetherAIDomain = "api.together.xyz"
|
||||
)
|
||||
|
||||
type togetherAIProviderInitializer struct{}
|
||||
@@ -26,7 +25,7 @@ func (m *togetherAIProviderInitializer) ValidateConfig(config *ProviderConfig) e
|
||||
|
||||
func (m *togetherAIProviderInitializer) DefaultCapabilities() map[string]string {
|
||||
return map[string]string{
|
||||
string(ApiNameChatCompletion): togetherAICompletionPath,
|
||||
string(ApiNameChatCompletion): PathOpenAIChatCompletions,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,7 +66,7 @@ func (m *togetherAIProvider) TransformRequestHeaders(ctx wrapper.HttpContext, ap
|
||||
}
|
||||
|
||||
func (m *togetherAIProvider) GetApiName(path string) ApiName {
|
||||
if strings.Contains(path, togetherAICompletionPath) {
|
||||
if strings.Contains(path, PathOpenAIChatCompletions) {
|
||||
return ApiNameChatCompletion
|
||||
}
|
||||
return ""
|
||||
|
||||
668
plugins/wasm-go/extensions/ai-proxy/provider/vertex.go
Normal file
668
plugins/wasm-go/extensions/ai-proxy/provider/vertex.go
Normal file
@@ -0,0 +1,668 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"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/log"
|
||||
"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"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
const (
|
||||
vertexAuthDomain = "oauth2.googleapis.com"
|
||||
vertexDomain = "{REGION}-aiplatform.googleapis.com"
|
||||
// /v1/projects/{PROJECT_ID}/locations/{REGION}/publishers/google/models/{MODEL_ID}:{ACTION}
|
||||
vertexPathTemplate = "/v1/projects/%s/locations/%s/publishers/google/models/%s:%s"
|
||||
vertexChatCompletionAction = "generateContent"
|
||||
vertexChatCompletionStreamAction = "streamGenerateContent?alt=sse"
|
||||
vertexEmbeddingAction = "predict"
|
||||
)
|
||||
|
||||
type vertexProviderInitializer struct {
|
||||
}
|
||||
|
||||
func (v *vertexProviderInitializer) ValidateConfig(config *ProviderConfig) error {
|
||||
if config.vertexAuthKey == "" {
|
||||
return errors.New("missing vertexAuthKey in vertex provider config")
|
||||
}
|
||||
if config.vertexRegion == "" || config.vertexProjectId == "" {
|
||||
return errors.New("missing vertexRegion or vertexProjectId in vertex provider config")
|
||||
}
|
||||
if config.vertexAuthServiceName == "" {
|
||||
return errors.New("missing vertexAuthServiceName in vertex provider config")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *vertexProviderInitializer) DefaultCapabilities() map[string]string {
|
||||
return map[string]string{
|
||||
string(ApiNameChatCompletion): vertexPathTemplate,
|
||||
string(ApiNameEmbeddings): vertexPathTemplate,
|
||||
}
|
||||
}
|
||||
|
||||
func (v *vertexProviderInitializer) CreateProvider(config ProviderConfig) (Provider, error) {
|
||||
config.setDefaultCapabilities(v.DefaultCapabilities())
|
||||
return &vertexProvider{
|
||||
config: config,
|
||||
client: wrapper.NewClusterClient(wrapper.DnsCluster{
|
||||
Domain: vertexAuthDomain,
|
||||
ServiceName: config.vertexAuthServiceName,
|
||||
Port: 443,
|
||||
}),
|
||||
contextCache: createContextCache(&config),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type vertexProvider struct {
|
||||
client wrapper.HttpClient
|
||||
config ProviderConfig
|
||||
contextCache *contextCache
|
||||
}
|
||||
|
||||
func (v *vertexProvider) GetProviderType() string {
|
||||
return providerTypeVertex
|
||||
}
|
||||
|
||||
func (v *vertexProvider) GetApiName(path string) ApiName {
|
||||
if strings.HasSuffix(path, vertexChatCompletionAction) || strings.HasSuffix(path, vertexChatCompletionStreamAction) {
|
||||
return ApiNameChatCompletion
|
||||
}
|
||||
if strings.HasSuffix(path, vertexEmbeddingAction) {
|
||||
return ApiNameEmbeddings
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (v *vertexProvider) OnRequestHeaders(ctx wrapper.HttpContext, apiName ApiName) error {
|
||||
v.config.handleRequestHeaders(v, ctx, apiName)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *vertexProvider) TransformRequestHeaders(ctx wrapper.HttpContext, apiName ApiName, headers http.Header) {
|
||||
vertexRegionDomain := strings.Replace(vertexDomain, "{REGION}", v.config.vertexRegion, 1)
|
||||
util.OverwriteRequestHostHeader(headers, vertexRegionDomain)
|
||||
}
|
||||
|
||||
func (v *vertexProvider) getToken() (cached bool, err error) {
|
||||
cacheKeyName := v.buildTokenKey()
|
||||
cachedAccessToken, err := v.getCachedAccessToken(cacheKeyName)
|
||||
if err == nil && cachedAccessToken != "" {
|
||||
_ = proxywasm.ReplaceHttpRequestHeader("Authorization", "Bearer "+cachedAccessToken)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
var key ServiceAccountKey
|
||||
if err := json.Unmarshal([]byte(v.config.vertexAuthKey), &key); err != nil {
|
||||
return false, fmt.Errorf("[vertex]: unable to unmarshal auth key json: %v", err)
|
||||
}
|
||||
|
||||
if key.ClientEmail == "" || key.PrivateKey == "" || key.TokenURI == "" {
|
||||
return false, fmt.Errorf("[vertex]: missing auth params")
|
||||
}
|
||||
|
||||
jwtToken, err := createJWT(&key)
|
||||
if err != nil {
|
||||
log.Errorf("[vertex]: unable to create JWT token: %v", err)
|
||||
return false, err
|
||||
}
|
||||
|
||||
err = v.getAccessToken(jwtToken)
|
||||
if err != nil {
|
||||
log.Errorf("[vertex]: unable to get access token: %v", err)
|
||||
return false, err
|
||||
}
|
||||
|
||||
return false, err
|
||||
}
|
||||
|
||||
func (v *vertexProvider) OnRequestBody(ctx wrapper.HttpContext, apiName ApiName, body []byte) (types.Action, error) {
|
||||
if !v.config.isSupportedAPI(apiName) {
|
||||
return types.ActionContinue, errUnsupportedApiName
|
||||
}
|
||||
if v.config.IsOriginal() {
|
||||
return types.ActionContinue, nil
|
||||
}
|
||||
headers := util.GetOriginalRequestHeaders()
|
||||
body, err := v.TransformRequestBodyHeaders(ctx, apiName, body, headers)
|
||||
util.ReplaceRequestHeaders(headers)
|
||||
_ = proxywasm.ReplaceHttpRequestBody(body)
|
||||
if err != nil {
|
||||
return types.ActionContinue, err
|
||||
}
|
||||
cached, err := v.getToken()
|
||||
if cached {
|
||||
return types.ActionContinue, nil
|
||||
}
|
||||
if err == nil {
|
||||
return types.ActionPause, nil
|
||||
}
|
||||
return types.ActionContinue, err
|
||||
}
|
||||
|
||||
func (v *vertexProvider) TransformRequestBodyHeaders(ctx wrapper.HttpContext, apiName ApiName, body []byte, headers http.Header) ([]byte, error) {
|
||||
if apiName == ApiNameChatCompletion {
|
||||
return v.onChatCompletionRequestBody(ctx, body, headers)
|
||||
} else {
|
||||
return v.onEmbeddingsRequestBody(ctx, body, headers)
|
||||
}
|
||||
}
|
||||
|
||||
func (v *vertexProvider) onChatCompletionRequestBody(ctx wrapper.HttpContext, body []byte, headers http.Header) ([]byte, error) {
|
||||
request := &chatCompletionRequest{}
|
||||
err := v.config.parseRequestAndMapModel(ctx, request, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
path := v.getRequestPath(ApiNameChatCompletion, request.Model, request.Stream)
|
||||
util.OverwriteRequestPathHeader(headers, path)
|
||||
|
||||
vertexRequest := v.buildVertexChatRequest(request)
|
||||
return json.Marshal(vertexRequest)
|
||||
}
|
||||
|
||||
func (v *vertexProvider) onEmbeddingsRequestBody(ctx wrapper.HttpContext, body []byte, headers http.Header) ([]byte, error) {
|
||||
request := &embeddingsRequest{}
|
||||
if err := v.config.parseRequestAndMapModel(ctx, request, body); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
path := v.getRequestPath(ApiNameEmbeddings, request.Model, false)
|
||||
util.OverwriteRequestPathHeader(headers, path)
|
||||
|
||||
vertexRequest := v.buildEmbeddingRequest(request)
|
||||
return json.Marshal(vertexRequest)
|
||||
}
|
||||
|
||||
func (v *vertexProvider) OnStreamingResponseBody(ctx wrapper.HttpContext, name ApiName, chunk []byte, isLastChunk bool) ([]byte, error) {
|
||||
log.Infof("[vertexProvider] receive chunk body: %s", string(chunk))
|
||||
if isLastChunk || len(chunk) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
if name != ApiNameChatCompletion {
|
||||
return chunk, nil
|
||||
}
|
||||
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 vertexResp vertexChatResponse
|
||||
if err := json.Unmarshal([]byte(data), &vertexResp); err != nil {
|
||||
log.Errorf("unable to unmarshal vertex response: %v", err)
|
||||
continue
|
||||
}
|
||||
response := v.buildChatCompletionStreamResponse(ctx, &vertexResp)
|
||||
responseBody, err := json.Marshal(response)
|
||||
if err != nil {
|
||||
log.Errorf("unable to marshal response: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
v.appendResponse(responseBuilder, string(responseBody))
|
||||
}
|
||||
modifiedResponseChunk := responseBuilder.String()
|
||||
log.Debugf("=== modified response chunk: %s", modifiedResponseChunk)
|
||||
return []byte(modifiedResponseChunk), nil
|
||||
}
|
||||
|
||||
func (v *vertexProvider) TransformResponseBody(ctx wrapper.HttpContext, apiName ApiName, body []byte) ([]byte, error) {
|
||||
if apiName == ApiNameChatCompletion {
|
||||
return v.onChatCompletionResponseBody(ctx, body)
|
||||
} else {
|
||||
return v.onEmbeddingsResponseBody(ctx, body)
|
||||
}
|
||||
}
|
||||
|
||||
func (v *vertexProvider) onChatCompletionResponseBody(ctx wrapper.HttpContext, body []byte) ([]byte, error) {
|
||||
vertexResponse := &vertexChatResponse{}
|
||||
if err := json.Unmarshal(body, vertexResponse); err != nil {
|
||||
return nil, fmt.Errorf("unable to unmarshal vertex chat response: %v", err)
|
||||
}
|
||||
response := v.buildChatCompletionResponse(ctx, vertexResponse)
|
||||
return json.Marshal(response)
|
||||
}
|
||||
|
||||
func (v *vertexProvider) buildChatCompletionResponse(ctx wrapper.HttpContext, response *vertexChatResponse) *chatCompletionResponse {
|
||||
fullTextResponse := chatCompletionResponse{
|
||||
Id: response.ResponseId,
|
||||
Object: objectChatCompletion,
|
||||
Created: time.Now().UnixMilli() / 1000,
|
||||
Model: ctx.GetStringContext(ctxKeyFinalRequestModel, ""),
|
||||
Choices: make([]chatCompletionChoice, 0, len(response.Candidates)),
|
||||
Usage: usage{
|
||||
PromptTokens: response.UsageMetadata.PromptTokenCount,
|
||||
CompletionTokens: response.UsageMetadata.CandidatesTokenCount,
|
||||
TotalTokens: response.UsageMetadata.TotalTokenCount,
|
||||
},
|
||||
}
|
||||
for _, candidate := range response.Candidates {
|
||||
choice := chatCompletionChoice{
|
||||
Index: candidate.Index,
|
||||
Message: &chatMessage{
|
||||
Role: roleAssistant,
|
||||
},
|
||||
FinishReason: candidate.FinishReason,
|
||||
}
|
||||
if len(candidate.Content.Parts) > 0 {
|
||||
choice.Message.Content = candidate.Content.Parts[0].Text
|
||||
} else {
|
||||
choice.Message.Content = ""
|
||||
}
|
||||
fullTextResponse.Choices = append(fullTextResponse.Choices, choice)
|
||||
}
|
||||
return &fullTextResponse
|
||||
}
|
||||
|
||||
func (v *vertexProvider) onEmbeddingsResponseBody(ctx wrapper.HttpContext, body []byte) ([]byte, error) {
|
||||
vertexResponse := &vertexEmbeddingResponse{}
|
||||
if err := json.Unmarshal(body, vertexResponse); err != nil {
|
||||
return nil, fmt.Errorf("unable to unmarshal vertex embeddings response: %v", err)
|
||||
}
|
||||
response := v.buildEmbeddingsResponse(ctx, vertexResponse)
|
||||
return json.Marshal(response)
|
||||
}
|
||||
|
||||
func (v *vertexProvider) buildEmbeddingsResponse(ctx wrapper.HttpContext, vertexResp *vertexEmbeddingResponse) *embeddingsResponse {
|
||||
response := embeddingsResponse{
|
||||
Object: "list",
|
||||
Data: make([]embedding, 0, len(vertexResp.Predictions)),
|
||||
Model: ctx.GetContext(ctxKeyFinalRequestModel).(string),
|
||||
}
|
||||
totalTokens := 0
|
||||
for _, item := range vertexResp.Predictions {
|
||||
response.Data = append(response.Data, embedding{
|
||||
Object: `embedding`,
|
||||
Index: 0,
|
||||
Embedding: item.Embeddings.Values,
|
||||
})
|
||||
if item.Embeddings.Statistics != nil {
|
||||
totalTokens += item.Embeddings.Statistics.TokenCount
|
||||
}
|
||||
}
|
||||
response.Usage.TotalTokens = totalTokens
|
||||
return &response
|
||||
}
|
||||
|
||||
func (v *vertexProvider) buildChatCompletionStreamResponse(ctx wrapper.HttpContext, vertexResp *vertexChatResponse) *chatCompletionResponse {
|
||||
var choice chatCompletionChoice
|
||||
if len(vertexResp.Candidates) > 0 && len(vertexResp.Candidates[0].Content.Parts) > 0 {
|
||||
choice.Delta = &chatMessage{Content: vertexResp.Candidates[0].Content.Parts[0].Text}
|
||||
}
|
||||
streamResponse := chatCompletionResponse{
|
||||
Id: vertexResp.ResponseId,
|
||||
Object: objectChatCompletionChunk,
|
||||
Created: time.Now().UnixMilli() / 1000,
|
||||
Model: ctx.GetStringContext(ctxKeyFinalRequestModel, ""),
|
||||
Choices: []chatCompletionChoice{choice},
|
||||
Usage: usage{
|
||||
PromptTokens: vertexResp.UsageMetadata.PromptTokenCount,
|
||||
CompletionTokens: vertexResp.UsageMetadata.CandidatesTokenCount,
|
||||
TotalTokens: vertexResp.UsageMetadata.TotalTokenCount,
|
||||
},
|
||||
}
|
||||
return &streamResponse
|
||||
}
|
||||
|
||||
func (v *vertexProvider) appendResponse(responseBuilder *strings.Builder, responseBody string) {
|
||||
responseBuilder.WriteString(fmt.Sprintf("%s %s\n\n", streamDataItemKey, responseBody))
|
||||
}
|
||||
|
||||
func (v *vertexProvider) getRequestPath(apiName ApiName, modelId string, stream bool) string {
|
||||
action := ""
|
||||
if apiName == ApiNameEmbeddings {
|
||||
action = vertexEmbeddingAction
|
||||
} else if stream {
|
||||
action = vertexChatCompletionStreamAction
|
||||
} else {
|
||||
action = vertexChatCompletionAction
|
||||
}
|
||||
return fmt.Sprintf(vertexPathTemplate, v.config.vertexProjectId, v.config.vertexRegion, modelId, action)
|
||||
}
|
||||
|
||||
func (v *vertexProvider) buildVertexChatRequest(request *chatCompletionRequest) *vertexChatRequest {
|
||||
safetySettings := make([]vertexChatSafetySetting, 0)
|
||||
for category, threshold := range v.config.geminiSafetySetting {
|
||||
safetySettings = append(safetySettings, vertexChatSafetySetting{
|
||||
Category: category,
|
||||
Threshold: threshold,
|
||||
})
|
||||
}
|
||||
vertexRequest := vertexChatRequest{
|
||||
Contents: make([]vertexChatContent, 0),
|
||||
SafetySettings: safetySettings,
|
||||
GenerationConfig: vertexChatGenerationConfig{
|
||||
Temperature: request.Temperature,
|
||||
TopP: request.TopP,
|
||||
MaxOutputTokens: request.MaxTokens,
|
||||
},
|
||||
}
|
||||
if request.Tools != nil {
|
||||
functions := make([]function, 0, len(request.Tools))
|
||||
for _, tool := range request.Tools {
|
||||
functions = append(functions, tool.Function)
|
||||
}
|
||||
vertexRequest.Tools = []vertexTool{
|
||||
{
|
||||
FunctionDeclarations: functions,
|
||||
},
|
||||
}
|
||||
}
|
||||
shouldAddDummyModelMessage := false
|
||||
for _, message := range request.Messages {
|
||||
content := vertexChatContent{
|
||||
Role: message.Role,
|
||||
Parts: []vertexPart{
|
||||
{
|
||||
Text: message.StringContent(),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// there's no assistant role in vertex and API shall vomit if role is not user or model
|
||||
if content.Role == roleAssistant {
|
||||
content.Role = "model"
|
||||
} else if content.Role == roleSystem { // converting system prompt to prompt from user for the same reason
|
||||
content.Role = roleUser
|
||||
shouldAddDummyModelMessage = true
|
||||
}
|
||||
vertexRequest.Contents = append(vertexRequest.Contents, content)
|
||||
|
||||
// if a system message is the last message, we need to add a dummy model message to make vertex happy
|
||||
if shouldAddDummyModelMessage {
|
||||
vertexRequest.Contents = append(vertexRequest.Contents, vertexChatContent{
|
||||
Role: "model",
|
||||
Parts: []vertexPart{
|
||||
{
|
||||
Text: "Okay",
|
||||
},
|
||||
},
|
||||
})
|
||||
shouldAddDummyModelMessage = false
|
||||
}
|
||||
}
|
||||
|
||||
return &vertexRequest
|
||||
}
|
||||
|
||||
func (v *vertexProvider) buildEmbeddingRequest(request *embeddingsRequest) *vertexEmbeddingRequest {
|
||||
inputs := request.ParseInput()
|
||||
instances := make([]vertexEmbeddingInstance, len(inputs))
|
||||
for i, input := range inputs {
|
||||
instances[i] = vertexEmbeddingInstance{
|
||||
Content: input,
|
||||
}
|
||||
}
|
||||
return &vertexEmbeddingRequest{Instances: instances}
|
||||
}
|
||||
|
||||
type vertexChatRequest struct {
|
||||
CachedContent string `json:"cachedContent,omitempty"`
|
||||
Contents []vertexChatContent `json:"contents"`
|
||||
SystemInstruction *vertexSystemInstruction `json:"systemInstruction,omitempty"`
|
||||
Tools []vertexTool `json:"tools,omitempty"`
|
||||
SafetySettings []vertexChatSafetySetting `json:"safetySettings,omitempty"`
|
||||
GenerationConfig vertexChatGenerationConfig `json:"generationConfig,omitempty"`
|
||||
Labels map[string]string `json:"labels,omitempty"`
|
||||
}
|
||||
|
||||
type vertexChatContent struct {
|
||||
// The producer of the content. Must be either 'user' or 'model'.
|
||||
Role string `json:"role,omitempty"`
|
||||
Parts []vertexPart `json:"parts"`
|
||||
}
|
||||
|
||||
type vertexPart struct {
|
||||
Text string `json:"text,omitempty"`
|
||||
InlineData *blob `json:"inlineData,omitempty"`
|
||||
FileData *fileData `json:"fileData,omitempty"`
|
||||
}
|
||||
|
||||
type blob struct {
|
||||
MimeType string `json:"mimeType"`
|
||||
Data string `json:"data"`
|
||||
}
|
||||
|
||||
type fileData struct {
|
||||
MimeType string `json:"mimeType"`
|
||||
FileUri string `json:"fileUri"`
|
||||
}
|
||||
|
||||
type vertexSystemInstruction struct {
|
||||
Role string `json:"role"`
|
||||
Parts []vertexPart `json:"parts"`
|
||||
}
|
||||
|
||||
type vertexTool struct {
|
||||
FunctionDeclarations any `json:"functionDeclarations"`
|
||||
}
|
||||
|
||||
type vertexChatSafetySetting struct {
|
||||
Category string `json:"category"`
|
||||
Threshold string `json:"threshold"`
|
||||
}
|
||||
|
||||
type vertexChatGenerationConfig struct {
|
||||
Temperature float64 `json:"temperature,omitempty"`
|
||||
TopP float64 `json:"topP,omitempty"`
|
||||
TopK int `json:"topK,omitempty"`
|
||||
CandidateCount int `json:"candidateCount,omitempty"`
|
||||
MaxOutputTokens int `json:"maxOutputTokens,omitempty"`
|
||||
}
|
||||
|
||||
type vertexEmbeddingRequest struct {
|
||||
Instances []vertexEmbeddingInstance `json:"instances"`
|
||||
Parameters *vertexEmbeddingParams `json:"parameters,omitempty"`
|
||||
}
|
||||
|
||||
type vertexEmbeddingInstance struct {
|
||||
TaskType string `json:"task_type"`
|
||||
Title string `json:"title,omitempty"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
type vertexEmbeddingParams struct {
|
||||
AutoTruncate bool `json:"autoTruncate,omitempty"`
|
||||
}
|
||||
|
||||
type vertexChatResponse struct {
|
||||
Candidates []vertexChatCandidate `json:"candidates"`
|
||||
ResponseId string `json:"responseId,omitempty"`
|
||||
PromptFeedback vertexChatPromptFeedback `json:"promptFeedback"`
|
||||
UsageMetadata vertexUsageMetadata `json:"usageMetadata"`
|
||||
}
|
||||
|
||||
type vertexChatCandidate struct {
|
||||
Content vertexChatContent `json:"content"`
|
||||
FinishReason string `json:"finishReason"`
|
||||
Index int `json:"index"`
|
||||
SafetyRatings []vertexChatSafetyRating `json:"safetyRatings"`
|
||||
}
|
||||
|
||||
type vertexChatSafetyRating struct {
|
||||
Category string `json:"category"`
|
||||
Probability string `json:"probability"`
|
||||
}
|
||||
|
||||
type vertexChatPromptFeedback struct {
|
||||
SafetyRatings []vertexChatSafetyRating `json:"safetyRatings"`
|
||||
}
|
||||
|
||||
type vertexUsageMetadata struct {
|
||||
PromptTokenCount int `json:"promptTokenCount,omitempty"`
|
||||
CandidatesTokenCount int `json:"candidatesTokenCount,omitempty"`
|
||||
TotalTokenCount int `json:"totalTokenCount,omitempty"`
|
||||
}
|
||||
|
||||
type vertexEmbeddingResponse struct {
|
||||
Predictions []vertexPredictions `json:"predictions"`
|
||||
}
|
||||
|
||||
type vertexPredictions struct {
|
||||
Embeddings struct {
|
||||
Values []float64 `json:"values"`
|
||||
Statistics *vertexStatistics `json:"statistics,omitempty"`
|
||||
} `json:"embeddings"`
|
||||
}
|
||||
|
||||
type vertexStatistics struct {
|
||||
TokenCount int `json:"token_count"`
|
||||
Truncated bool `json:"truncated"`
|
||||
}
|
||||
|
||||
type ServiceAccountKey struct {
|
||||
ClientEmail string `json:"client_email"`
|
||||
PrivateKeyID string `json:"private_key_id"`
|
||||
PrivateKey string `json:"private_key"`
|
||||
TokenURI string `json:"token_uri"`
|
||||
}
|
||||
|
||||
func createJWT(key *ServiceAccountKey) (string, error) {
|
||||
// 解析 PEM 格式的 RSA 私钥
|
||||
block, _ := pem.Decode([]byte(key.PrivateKey))
|
||||
if block == nil {
|
||||
return "", fmt.Errorf("invalid PEM block")
|
||||
}
|
||||
parsedKey, err := x509.ParsePKCS8PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
rsaKey := parsedKey.(*rsa.PrivateKey)
|
||||
|
||||
// 构造 JWT Header
|
||||
jwtHeader := map[string]string{
|
||||
"alg": "RS256",
|
||||
"typ": "JWT",
|
||||
"kid": key.PrivateKeyID,
|
||||
}
|
||||
headerJSON, _ := json.Marshal(jwtHeader)
|
||||
headerB64 := base64.RawURLEncoding.EncodeToString(headerJSON)
|
||||
|
||||
// 构造 JWT Claims
|
||||
now := time.Now().Unix()
|
||||
claims := map[string]interface{}{
|
||||
"iss": key.ClientEmail,
|
||||
"scope": "https://www.googleapis.com/auth/cloud-platform",
|
||||
"aud": key.TokenURI,
|
||||
"iat": now,
|
||||
"exp": now + 3600, // 1 小时有效期
|
||||
}
|
||||
claimsJSON, _ := json.Marshal(claims)
|
||||
claimsB64 := base64.RawURLEncoding.EncodeToString(claimsJSON)
|
||||
|
||||
signingInput := fmt.Sprintf("%s.%s", headerB64, claimsB64)
|
||||
hashed := sha256.Sum256([]byte(signingInput))
|
||||
signature, err := rsaKey.Sign(nil, hashed[:], crypto.SHA256)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
sigB64 := base64.RawURLEncoding.EncodeToString(signature)
|
||||
|
||||
return fmt.Sprintf("%s.%s.%s", headerB64, claimsB64, sigB64), nil
|
||||
}
|
||||
|
||||
func (v *vertexProvider) getAccessToken(jwtToken string) error {
|
||||
headers := [][2]string{
|
||||
{"Content-Type", "application/x-www-form-urlencoded"},
|
||||
}
|
||||
reqBody := "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=" + jwtToken
|
||||
err := v.client.Post("/token", headers, []byte(reqBody), func(statusCode int, responseHeaders http.Header, responseBody []byte) {
|
||||
responseString := string(responseBody)
|
||||
defer func() {
|
||||
_ = proxywasm.ResumeHttpRequest()
|
||||
}()
|
||||
if statusCode != http.StatusOK {
|
||||
log.Errorf("failed to create vertex access key, status: %d body: %s", statusCode, responseString)
|
||||
_ = util.ErrorHandler("ai-proxy.vertex.load_ak_failed", fmt.Errorf("failed to load vertex ak"))
|
||||
return
|
||||
}
|
||||
responseJson := gjson.Parse(responseString)
|
||||
accessToken := responseJson.Get("access_token").String()
|
||||
_ = proxywasm.ReplaceHttpRequestHeader("Authorization", "Bearer "+accessToken)
|
||||
|
||||
expiresIn := int64(3600)
|
||||
if expiresInVal := responseJson.Get("expires_in"); expiresInVal.Exists() {
|
||||
expiresIn = expiresInVal.Int()
|
||||
}
|
||||
expireTime := time.Now().Add(time.Duration(expiresIn) * time.Second).Unix()
|
||||
keyName := v.buildTokenKey()
|
||||
err := setCachedAccessToken(keyName, accessToken, expireTime)
|
||||
if err != nil {
|
||||
log.Errorf("[vertex]: unable to cache access token: %v", err)
|
||||
}
|
||||
}, v.config.timeout)
|
||||
return err
|
||||
}
|
||||
|
||||
func (v *vertexProvider) buildTokenKey() string {
|
||||
region := v.config.vertexRegion
|
||||
projectID := v.config.vertexProjectId
|
||||
|
||||
return fmt.Sprintf("vertex-%s-%s-access-token", region, projectID)
|
||||
}
|
||||
|
||||
type cachedAccessToken struct {
|
||||
Token string `json:"token"`
|
||||
ExpireAt int64 `json:"expireAt"`
|
||||
}
|
||||
|
||||
func (v *vertexProvider) getCachedAccessToken(key string) (string, error) {
|
||||
data, _, err := proxywasm.GetSharedData(key)
|
||||
if err != nil {
|
||||
if errors.Is(err, types.ErrorStatusNotFound) {
|
||||
return "", nil
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
if data == nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
var tokenInfo cachedAccessToken
|
||||
if err = json.Unmarshal(data, &tokenInfo); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
now := time.Now().Unix()
|
||||
refreshAhead := v.config.vertexTokenRefreshAhead
|
||||
|
||||
if tokenInfo.ExpireAt > now+refreshAhead {
|
||||
return tokenInfo.Token, nil
|
||||
}
|
||||
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func setCachedAccessToken(key string, accessToken string, expireTime int64) error {
|
||||
tokenInfo := cachedAccessToken{
|
||||
Token: accessToken,
|
||||
ExpireAt: expireTime,
|
||||
}
|
||||
|
||||
_, cas, err := proxywasm.GetSharedData(key)
|
||||
if err != nil && !errors.Is(err, types.ErrorStatusNotFound) {
|
||||
return err
|
||||
}
|
||||
|
||||
data, err := json.Marshal(tokenInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return proxywasm.SetSharedData(key, data, cas)
|
||||
}
|
||||
@@ -10,12 +10,10 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
yiDomain = "api.lingyiwanwu.com"
|
||||
yiChatCompletionPath = "/v1/chat/completions"
|
||||
yiDomain = "api.lingyiwanwu.com"
|
||||
)
|
||||
|
||||
type yiProviderInitializer struct {
|
||||
}
|
||||
type yiProviderInitializer struct{}
|
||||
|
||||
func (m *yiProviderInitializer) ValidateConfig(config *ProviderConfig) error {
|
||||
if config.apiTokens == nil || len(config.apiTokens) == 0 {
|
||||
@@ -26,7 +24,7 @@ func (m *yiProviderInitializer) ValidateConfig(config *ProviderConfig) error {
|
||||
|
||||
func (m *yiProviderInitializer) DefaultCapabilities() map[string]string {
|
||||
return map[string]string{
|
||||
string(ApiNameChatCompletion): yiChatCompletionPath,
|
||||
string(ApiNameChatCompletion): PathOpenAIChatCompletions,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,10 @@ package util
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/log"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
|
||||
)
|
||||
|
||||
@@ -13,6 +16,13 @@ const (
|
||||
MimeTypeApplicationJson = "application/json"
|
||||
)
|
||||
|
||||
var (
|
||||
RegRetrieveBatchPath = regexp.MustCompile(`^.*/v1/batches/(?P<batch_id>[^/]+)$`)
|
||||
RegCancelBatchPath = regexp.MustCompile(`^.*/v1/batches/(?P<batch_id>[^/]+)/cancel$`)
|
||||
RegRetrieveFilePath = regexp.MustCompile(`^.*/v1/files/(?P<file_id>[^/]+)$`)
|
||||
RegRetrieveFileContentPath = regexp.MustCompile(`^.*/v1/files/(?P<file_id>[^/]+)/content$`)
|
||||
)
|
||||
|
||||
type ErrorHandlerFunc func(statusCodeDetails string, err error) error
|
||||
|
||||
var ErrorHandler ErrorHandlerFunc = func(statusCodeDetails string, err error) error {
|
||||
@@ -62,10 +72,45 @@ func OverwriteRequestPathHeaderByCapability(headers http.Header, apiName string,
|
||||
if !exist {
|
||||
return
|
||||
}
|
||||
if originPath, err := proxywasm.GetHttpRequestHeader(":path"); err == nil {
|
||||
originPath, err := proxywasm.GetHttpRequestHeader(":path")
|
||||
if err == nil {
|
||||
headers.Set("X-ENVOY-ORIGINAL-PATH", originPath)
|
||||
}
|
||||
/**
|
||||
这里实现不太优雅,理应通过 apiName 来判断使用哪个正则替换
|
||||
但 ApiName 定义在 provider 中, 而 provider 中又引用了 util
|
||||
会导致循环引用
|
||||
**/
|
||||
if strings.Contains(mappedPath, "{") && strings.Contains(mappedPath, "}") {
|
||||
replacements := []struct {
|
||||
regx *regexp.Regexp
|
||||
key string
|
||||
}{
|
||||
{RegRetrieveFilePath, "file_id"},
|
||||
{RegRetrieveFileContentPath, "file_id"},
|
||||
{RegRetrieveBatchPath, "batch_id"},
|
||||
{RegCancelBatchPath, "batch_id"},
|
||||
}
|
||||
|
||||
for _, r := range replacements {
|
||||
if r.regx.MatchString(originPath) {
|
||||
subMatch := r.regx.FindStringSubmatch(originPath)
|
||||
if subMatch == nil {
|
||||
continue
|
||||
}
|
||||
index := r.regx.SubexpIndex(r.key)
|
||||
if index < 0 || index >= len(subMatch) {
|
||||
continue
|
||||
}
|
||||
id := subMatch[index]
|
||||
mappedPath = r.regx.ReplaceAllStringFunc(mappedPath, func(s string) string {
|
||||
return strings.Replace(s, "{"+r.key+"}", id, 1)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
headers.Set(":path", mappedPath)
|
||||
log.Debugf("[OverwriteRequestPath] originPath=%s, mappedPath=%s", originPath, mappedPath)
|
||||
}
|
||||
|
||||
func OverwriteRequestAuthorizationHeader(headers http.Header, credential string) {
|
||||
|
||||
@@ -289,6 +289,7 @@ func onHttpRequestHeaders(ctx wrapper.HttpContext, config Config, log wrapper.Lo
|
||||
}
|
||||
ctx.SetRequestBodyBufferLimit(DEFAULT_MAX_BODY_BYTES)
|
||||
_ = proxywasm.RemoveHttpRequestHeader("Accept-Encoding")
|
||||
_ = proxywasm.RemoveHttpRequestHeader("Content-Length")
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
@@ -362,7 +363,8 @@ func onHttpRequestBody(ctx wrapper.HttpContext, config Config, body []byte, log
|
||||
}, rewriteBody,
|
||||
func(statusCode int, responseHeaders http.Header, responseBody []byte) {
|
||||
if statusCode != http.StatusOK {
|
||||
log.Errorf("search rewrite failed, status: %d", statusCode)
|
||||
log.Errorf("search rewrite failed, status: %d, request url: %s, request cluster: %s, search rewrite model: %s",
|
||||
statusCode, searchRewrite.url, searchRewrite.client.ClusterName(), searchRewrite.modelName)
|
||||
// After a rewrite failure, no further search is performed, thus quickly identifying the failure.
|
||||
proxywasm.ResumeHttpRequest()
|
||||
return
|
||||
|
||||
@@ -27,6 +27,7 @@ func main() {
|
||||
}
|
||||
|
||||
const (
|
||||
defaultMaxBodyBytes uint32 = 100 * 1024 * 1024
|
||||
// Context consts
|
||||
StatisticsRequestStartTime = "ai-statistics-request-start-time"
|
||||
StatisticsFirstTokenTime = "ai-statistics-first-token-time"
|
||||
@@ -176,6 +177,11 @@ func onHttpRequestHeaders(ctx wrapper.HttpContext, config AIStatisticsConfig, lo
|
||||
if consumer, _ := proxywasm.GetHttpRequestHeader(ConsumerKey); consumer != "" {
|
||||
ctx.SetContext(ConsumerKey, consumer)
|
||||
}
|
||||
hasRequestBody := wrapper.HasRequestBody()
|
||||
if hasRequestBody {
|
||||
_ = proxywasm.RemoveHttpRequestHeader("Content-Length")
|
||||
ctx.SetRequestBodyBufferLimit(defaultMaxBodyBytes)
|
||||
}
|
||||
|
||||
// Set user defined log & span attributes which type is fixed_value
|
||||
setAttributeBySource(ctx, config, FixedValue, nil, log)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user