mirror of
https://github.com/alibaba/higress.git
synced 2026-02-28 06:30:49 +08:00
Compare commits
22 Commits
v1.4.2
...
wasm-go-ex
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
04a9104062 | ||
|
|
564f8c770a | ||
|
|
fec2e9dfc9 | ||
|
|
dc4ddb52ee | ||
|
|
6f221ead53 | ||
|
|
53f8410843 | ||
|
|
a17ac9e4c6 | ||
|
|
5e95f6f057 | ||
|
|
94f29e56c0 | ||
|
|
870157c576 | ||
|
|
c78ef7011d | ||
|
|
dc0dcaaaee | ||
|
|
34f5722d93 | ||
|
|
55fdddee2f | ||
|
|
980ffde244 | ||
|
|
0a578c2a04 | ||
|
|
536a3069a8 | ||
|
|
08c64ed467 | ||
|
|
cc74c0da93 | ||
|
|
210b97b06b | ||
|
|
bccfbde621 | ||
|
|
f1c6e78047 |
@@ -177,8 +177,8 @@ install: pre-install
|
||||
cd helm/higress; helm dependency build
|
||||
helm install higress helm/higress -n higress-system --create-namespace --set 'global.local=true'
|
||||
|
||||
ENVOY_LATEST_IMAGE_TAG ?= sha-63539ca
|
||||
ISTIO_LATEST_IMAGE_TAG ?= sha-63539ca
|
||||
ENVOY_LATEST_IMAGE_TAG ?= sha-59acb61
|
||||
ISTIO_LATEST_IMAGE_TAG ?= sha-59acb61
|
||||
|
||||
install-dev: pre-install
|
||||
helm install higress helm/core -n higress-system --create-namespace --set 'controller.tag=$(TAG)' --set 'gateway.replicas=1' --set 'pilot.tag=$(ISTIO_LATEST_IMAGE_TAG)' --set 'gateway.tag=$(ENVOY_LATEST_IMAGE_TAG)' --set 'global.local=true'
|
||||
|
||||
59
README.md
59
README.md
@@ -1,17 +1,17 @@
|
||||
<h1 align="center">
|
||||
<img src="https://img.alicdn.com/imgextra/i2/O1CN01NwxLDd20nxfGBjxmZ_!!6000000006895-2-tps-960-290.png" alt="Higress" width="240" height="72.5">
|
||||
<br>
|
||||
Cloud Native API Gateway
|
||||
AI Native API Gateway
|
||||
</h1>
|
||||
|
||||
[](https://github.com/alibaba/higress/actions)
|
||||
[](https://www.apache.org/licenses/LICENSE-2.0.html)
|
||||
|
||||
[**官网**](https://higress.io/) |
|
||||
[**文档**](https://higress.io/zh-cn/docs/overview/what-is-higress) |
|
||||
[**博客**](https://higress.io/zh-cn/blog) |
|
||||
[**开发指引**](https://higress.io/zh-cn/docs/developers/developers_dev) |
|
||||
[**Higress 企业版**](https://www.aliyun.com/product/aliware/mse?spm=higress-website.topbar.0.0.0)
|
||||
[**文档**](https://higress.io/docs/latest/user/quickstart/) |
|
||||
[**博客**](https://higress.io/blog/) |
|
||||
[**开发指引**](https://higress.io/docs/latest/dev/architecture/) |
|
||||
[**AI插件**](https://higress.io/plugin/)
|
||||
|
||||
|
||||
<p>
|
||||
@@ -19,9 +19,15 @@
|
||||
</p>
|
||||
|
||||
|
||||
Higress 是基于阿里内部两年多的 Envoy Gateway 实践沉淀,以开源 [Istio](https://github.com/istio/istio) 与 [Envoy](https://github.com/envoyproxy/envoy) 为核心构建的云原生 API 网关。Higress 实现了安全防护网关、流量网关、微服务网关三层网关合一,可以显著降低网关的部署和运维成本。
|
||||
Higress 是基于阿里内部多年的 Envoy Gateway 实践沉淀,以开源 [Istio](https://github.com/istio/istio) 与 [Envoy](https://github.com/envoyproxy/envoy) 为核心构建的云原生 API 网关。
|
||||
|
||||
Higress 是面向 AI 原生设计的 API 网关,在阿里内部,承载了通义千问 APP、百炼大模型 API、机器学习 PAI 平台等 AI 业务的流量。
|
||||
|
||||
Higress 能够用统一的协议对接国内外所有 LLM 模型厂商,同时具备丰富的 AI 可观测、多模型负载均衡/fallback、AI token 流控、AI 缓存等能力:
|
||||
|
||||

|
||||
|
||||
|
||||

|
||||
|
||||
## Summary
|
||||
|
||||
@@ -34,6 +40,10 @@ Higress 是基于阿里内部两年多的 Envoy Gateway 实践沉淀,以开源
|
||||
|
||||
## 使用场景
|
||||
|
||||
- **AI 网关**:
|
||||
|
||||
Higress 提供了一站式的 AI 插件集,可以增强依赖 AI 能力业务的稳定性、灵活性、可观测性,使得业务与 AI 的集成更加便捷和高效。
|
||||
|
||||
- **Kubernetes Ingress 网关**:
|
||||
|
||||
Higress 可以作为 K8s 集群的 Ingress 入口网关, 并且兼容了大量 K8s Nginx Ingress 的注解,可以从 K8s Nginx Ingress 快速平滑迁移到 Higress。
|
||||
@@ -56,27 +66,36 @@ Higress 是基于阿里内部两年多的 Envoy Gateway 实践沉淀,以开源
|
||||
|
||||
脱胎于阿里巴巴2年多生产验证的内部产品,支持每秒请求量达数十万级的大规模场景。
|
||||
|
||||
彻底摆脱 reload 引起的流量抖动,配置变更毫秒级生效且业务无感。
|
||||
|
||||
- **平滑演进**
|
||||
彻底摆脱 Nginx reload 引起的流量抖动,配置变更毫秒级生效且业务无感。对 AI 业务等长连接场景特别友好。
|
||||
|
||||
支持 Nacos/Zookeeper/Eureka 等多种注册中心,可以不依赖 K8s Service 进行服务发现,支持非容器架构平滑演进到云原生架构。
|
||||
- **流式处理**
|
||||
|
||||
支持从 Nginx Ingress Controller 平滑迁移,支持平滑过渡到 Gateway API,支持业务架构平滑演进到 ServiceMesh。
|
||||
支持真正的完全流式处理请求/响应 Body,Wasm 插件很方便地自定义处理 SSE (Server-Sent Events)等流式协议的报文。
|
||||
|
||||
- **兼收并蓄**
|
||||
|
||||
兼容 Nginx Ingress Annotation 80%+ 的使用场景,且提供功能更丰富的 Higress Annotation 注解。
|
||||
|
||||
兼容 Ingress API/Gateway API/Istio API,可以组合多种 CRD 实现流量精细化管理。
|
||||
|
||||
在 AI 业务等大带宽场景下,可以显著降低内存开销。
|
||||
|
||||
- **便于扩展**
|
||||
|
||||
提供 Wasm、Lua、进程外三种插件扩展机制,支持多语言编写插件,生效粒度支持全局级、域名级,路由级。
|
||||
提供丰富的官方插件库,涵盖 AI、流量管理、安全防护等常用功能,满足90%以上的业务场景需求。
|
||||
|
||||
主打 Wasm 插件扩展,通过沙箱隔离确保内存安全,支持多种编程语言,允许插件版本独立升级,实现流量无损热更新网关逻辑。
|
||||
|
||||
- **安全易用**
|
||||
|
||||
基于 Ingress API 和 Gateway API 标准,提供开箱即用的 UI 控制台,WAF 防护插件、IP/Cookie CC 防护插件开箱即用。
|
||||
|
||||
支持对接 Let's Encrypt 自动签发和续签免费证书,并且可以脱离 K8s 部署,一行 Docker 命令即可启动,方便个人开发者使用。
|
||||
|
||||
插件支持热更新,变更插件逻辑和配置都对流量无损。
|
||||
|
||||
## 功能展示
|
||||
|
||||
### AI 网关 Demo 展示
|
||||
|
||||
[从 OpenAI 到其他大模型,30 秒完成迁移
|
||||
](https://www.bilibili.com/video/BV1dT421a7w7/?spm_id_from=333.788.recommend_more_video.14)
|
||||
|
||||
|
||||
### Higress UI 控制台
|
||||
|
||||
- **丰富的可观测**
|
||||
|
||||
|
||||
@@ -301,6 +301,7 @@ type MatchRule struct {
|
||||
Domain []string `protobuf:"bytes,2,rep,name=domain,proto3" json:"domain,omitempty"`
|
||||
Config *types.Struct `protobuf:"bytes,3,opt,name=config,proto3" json:"config,omitempty"`
|
||||
ConfigDisable bool `protobuf:"varint,4,opt,name=config_disable,json=configDisable,proto3" json:"config_disable,omitempty"`
|
||||
Service []string `protobuf:"bytes,5,rep,name=service,proto3" json:"service,omitempty"`
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
XXX_sizecache int32 `json:"-"`
|
||||
@@ -367,6 +368,13 @@ func (m *MatchRule) GetConfigDisable() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *MatchRule) GetService() []string {
|
||||
if m != nil {
|
||||
return m.Service
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
proto.RegisterEnum("higress.extensions.v1alpha1.PluginPhase", PluginPhase_name, PluginPhase_value)
|
||||
proto.RegisterEnum("higress.extensions.v1alpha1.PullPolicy", PullPolicy_name, PullPolicy_value)
|
||||
@@ -377,46 +385,47 @@ func init() {
|
||||
func init() { proto.RegisterFile("extensions/v1alpha1/wasm.proto", fileDescriptor_4d60b240916c4e18) }
|
||||
|
||||
var fileDescriptor_4d60b240916c4e18 = []byte{
|
||||
// 619 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x7c, 0x94, 0xdd, 0x4e, 0xdb, 0x4c,
|
||||
0x10, 0x86, 0x71, 0x02, 0x81, 0x4c, 0x80, 0xcf, 0xac, 0xbe, 0xd2, 0x15, 0x54, 0x69, 0x84, 0xd4,
|
||||
0xd6, 0xe5, 0xc0, 0x16, 0xa1, 0x3f, 0x27, 0x15, 0x6a, 0x80, 0xb4, 0x44, 0x6d, 0x53, 0xcb, 0x86,
|
||||
0x56, 0xe5, 0xc4, 0xda, 0x98, 0x8d, 0xb3, 0xea, 0xfa, 0x47, 0xde, 0x35, 0x34, 0x17, 0xd2, 0x7b,
|
||||
0xea, 0x61, 0x2f, 0xa1, 0xe2, 0x2e, 0x7a, 0x56, 0x65, 0x6d, 0x43, 0x42, 0xab, 0x9c, 0xed, 0xce,
|
||||
0x3c, 0x33, 0xf3, 0xbe, 0xe3, 0x95, 0xa1, 0x49, 0xbf, 0x49, 0x1a, 0x09, 0x16, 0x47, 0xc2, 0xba,
|
||||
0xdc, 0x23, 0x3c, 0x19, 0x91, 0x3d, 0xeb, 0x8a, 0x88, 0xd0, 0x4c, 0xd2, 0x58, 0xc6, 0x68, 0x7b,
|
||||
0xc4, 0x82, 0x94, 0x0a, 0x61, 0xde, 0x72, 0x66, 0xc9, 0x6d, 0x35, 0x83, 0x38, 0x0e, 0x38, 0xb5,
|
||||
0x14, 0x3a, 0xc8, 0x86, 0xd6, 0x55, 0x4a, 0x92, 0x84, 0xa6, 0x22, 0x2f, 0xde, 0x7a, 0x70, 0x37,
|
||||
0x2f, 0x64, 0x9a, 0xf9, 0x32, 0xcf, 0xee, 0xfc, 0x5e, 0x04, 0xf8, 0x4c, 0x44, 0x68, 0xf3, 0x2c,
|
||||
0x60, 0x11, 0xd2, 0xa1, 0x9a, 0xa5, 0x1c, 0x57, 0x5a, 0x9a, 0x51, 0x77, 0x26, 0x47, 0xb4, 0x09,
|
||||
0x35, 0x31, 0x22, 0xed, 0xe7, 0x2f, 0x70, 0x55, 0x05, 0x8b, 0x1b, 0x72, 0x61, 0x83, 0x85, 0x24,
|
||||
0xa0, 0x5e, 0x92, 0x71, 0xee, 0x25, 0x31, 0x67, 0xfe, 0x18, 0x2f, 0xb6, 0x34, 0x63, 0xbd, 0xfd,
|
||||
0xc4, 0x9c, 0xa3, 0xd7, 0xb4, 0x33, 0xce, 0x6d, 0x85, 0x3b, 0xff, 0xa9, 0x0e, 0xb7, 0x01, 0xb4,
|
||||
0x3b, 0xd3, 0x54, 0x50, 0x3f, 0xa5, 0x12, 0x2f, 0xa9, 0xb9, 0xb7, 0xac, 0xab, 0xc2, 0xe8, 0x29,
|
||||
0xe8, 0x97, 0x34, 0x65, 0x43, 0xe6, 0x13, 0xc9, 0xe2, 0xc8, 0xfb, 0x4a, 0xc7, 0xb8, 0x96, 0xa3,
|
||||
0xd3, 0xf1, 0x77, 0x74, 0x8c, 0x5e, 0xc1, 0x5a, 0xa2, 0xfc, 0x79, 0x7e, 0x1c, 0x0d, 0x59, 0x80,
|
||||
0x97, 0x5b, 0x9a, 0xd1, 0x68, 0xdf, 0x37, 0xf3, 0xd5, 0x98, 0xe5, 0x6a, 0x4c, 0x57, 0xad, 0xc6,
|
||||
0x59, 0xcd, 0xe9, 0x23, 0x05, 0xa3, 0x87, 0xd0, 0x28, 0xaa, 0x23, 0x12, 0x52, 0xbc, 0xa2, 0x66,
|
||||
0x40, 0x1e, 0xea, 0x93, 0x90, 0xa2, 0x03, 0x58, 0x4a, 0x46, 0x44, 0x50, 0x5c, 0x57, 0xf6, 0x8d,
|
||||
0xf9, 0xf6, 0x55, 0x9d, 0x3d, 0xe1, 0x9d, 0xbc, 0x0c, 0xbd, 0x84, 0x95, 0x24, 0x65, 0x71, 0xca,
|
||||
0xe4, 0x18, 0x83, 0x52, 0xb6, 0xfd, 0x97, 0xb2, 0x5e, 0x24, 0xf7, 0xdb, 0x9f, 0x08, 0xcf, 0xa8,
|
||||
0x73, 0x03, 0xa3, 0x03, 0x58, 0xbf, 0xa0, 0x43, 0x92, 0x71, 0x59, 0x1a, 0xa3, 0xf3, 0x8d, 0xad,
|
||||
0x15, 0x78, 0xe1, 0xec, 0x2d, 0x34, 0x42, 0x22, 0xfd, 0x91, 0x97, 0x66, 0x9c, 0x0a, 0x3c, 0x6c,
|
||||
0x55, 0x8d, 0x46, 0xfb, 0xf1, 0x5c, 0xf9, 0x1f, 0x26, 0xbc, 0x93, 0x71, 0xea, 0x40, 0x58, 0x1e,
|
||||
0x05, 0x7a, 0x06, 0x9b, 0xb3, 0x42, 0xbc, 0x0b, 0x26, 0xc8, 0x80, 0x53, 0x1c, 0xb4, 0x34, 0x63,
|
||||
0xc5, 0xf9, 0x7f, 0x66, 0xee, 0x71, 0x9e, 0xdb, 0xf9, 0xae, 0x41, 0xfd, 0xa6, 0x1f, 0xc2, 0xb0,
|
||||
0xcc, 0x22, 0x35, 0x18, 0x6b, 0xad, 0xaa, 0x51, 0x77, 0xca, 0xeb, 0xe4, 0x09, 0x5e, 0xc4, 0x21,
|
||||
0x61, 0x11, 0xae, 0xa8, 0x44, 0x71, 0x43, 0x16, 0xd4, 0x0a, 0xdb, 0xd5, 0xf9, 0xb6, 0x0b, 0x0c,
|
||||
0x3d, 0x82, 0xf5, 0x3b, 0xf2, 0x16, 0x95, 0xbc, 0x35, 0x7f, 0x5a, 0xd7, 0x6e, 0x17, 0x1a, 0x53,
|
||||
0x5f, 0x09, 0xdd, 0x83, 0x8d, 0xb3, 0xbe, 0x6b, 0x77, 0x8f, 0x7a, 0x6f, 0x7a, 0xdd, 0x63, 0xcf,
|
||||
0x3e, 0xe9, 0xb8, 0x5d, 0x7d, 0x01, 0xd5, 0x61, 0xa9, 0x73, 0x76, 0x7a, 0xd2, 0xd7, 0xb5, 0xf2,
|
||||
0x78, 0xae, 0x57, 0x26, 0x47, 0xf7, 0xb4, 0x73, 0xea, 0xea, 0xd5, 0xdd, 0x43, 0x80, 0xa9, 0xa7,
|
||||
0xbd, 0x09, 0x68, 0xa6, 0xcb, 0xc7, 0xf7, 0xbd, 0xa3, 0x2f, 0xfa, 0x02, 0xd2, 0x61, 0xb5, 0x37,
|
||||
0xec, 0xc7, 0xd2, 0x4e, 0xa9, 0xa0, 0x91, 0xd4, 0x35, 0x04, 0x50, 0xeb, 0xf0, 0x2b, 0x32, 0x16,
|
||||
0x7a, 0xe5, 0xf0, 0xf5, 0x8f, 0xeb, 0xa6, 0xf6, 0xf3, 0xba, 0xa9, 0xfd, 0xba, 0x6e, 0x6a, 0xe7,
|
||||
0xed, 0x80, 0xc9, 0x51, 0x36, 0x30, 0xfd, 0x38, 0xb4, 0x08, 0x67, 0x03, 0x32, 0x20, 0x56, 0xf1,
|
||||
0xb1, 0x2c, 0x92, 0x30, 0xeb, 0x1f, 0xbf, 0x91, 0x41, 0x4d, 0x2d, 0x63, 0xff, 0x4f, 0x00, 0x00,
|
||||
0x00, 0xff, 0xff, 0xb9, 0xf2, 0x67, 0xbe, 0x64, 0x04, 0x00, 0x00,
|
||||
// 631 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x7c, 0x94, 0xdd, 0x6e, 0xd3, 0x4c,
|
||||
0x10, 0x86, 0xeb, 0xa4, 0x49, 0x9b, 0x49, 0xdb, 0xcf, 0x5d, 0x7d, 0x94, 0x55, 0x8b, 0x42, 0x54,
|
||||
0x09, 0x30, 0x3d, 0xb0, 0xd5, 0x94, 0x9f, 0x13, 0x54, 0x91, 0xb6, 0x81, 0x46, 0x40, 0xb0, 0xec,
|
||||
0x16, 0x44, 0x4f, 0xac, 0x8d, 0xbb, 0x71, 0x56, 0xac, 0x7f, 0xe4, 0x5d, 0xb7, 0xe4, 0xaa, 0xb8,
|
||||
0x0d, 0x0e, 0xb9, 0x04, 0xd4, 0xbb, 0xe0, 0x0c, 0x65, 0xed, 0x34, 0x49, 0x41, 0x39, 0xdb, 0x9d,
|
||||
0x79, 0x66, 0xe6, 0x7d, 0xc7, 0x2b, 0x43, 0x83, 0x7e, 0x93, 0x34, 0x12, 0x2c, 0x8e, 0x84, 0x75,
|
||||
0xb5, 0x4f, 0x78, 0x32, 0x24, 0xfb, 0xd6, 0x35, 0x11, 0xa1, 0x99, 0xa4, 0xb1, 0x8c, 0xd1, 0xce,
|
||||
0x90, 0x05, 0x29, 0x15, 0xc2, 0x9c, 0x72, 0xe6, 0x84, 0xdb, 0x6e, 0x04, 0x71, 0x1c, 0x70, 0x6a,
|
||||
0x29, 0xb4, 0x9f, 0x0d, 0xac, 0xeb, 0x94, 0x24, 0x09, 0x4d, 0x45, 0x5e, 0xbc, 0xfd, 0xe0, 0x6e,
|
||||
0x5e, 0xc8, 0x34, 0xf3, 0x65, 0x9e, 0xdd, 0xfd, 0xbd, 0x0c, 0xf0, 0x99, 0x88, 0xd0, 0xe6, 0x59,
|
||||
0xc0, 0x22, 0xa4, 0x43, 0x39, 0x4b, 0x39, 0x2e, 0x35, 0x35, 0xa3, 0xe6, 0x8c, 0x8f, 0x68, 0x0b,
|
||||
0xaa, 0x62, 0x48, 0x5a, 0xcf, 0x5f, 0xe0, 0xb2, 0x0a, 0x16, 0x37, 0xe4, 0xc2, 0x26, 0x0b, 0x49,
|
||||
0x40, 0xbd, 0x24, 0xe3, 0xdc, 0x4b, 0x62, 0xce, 0xfc, 0x11, 0x5e, 0x6e, 0x6a, 0xc6, 0x46, 0xeb,
|
||||
0x89, 0xb9, 0x40, 0xaf, 0x69, 0x67, 0x9c, 0xdb, 0x0a, 0x77, 0xfe, 0x53, 0x1d, 0xa6, 0x01, 0xb4,
|
||||
0x37, 0xd7, 0x54, 0x50, 0x3f, 0xa5, 0x12, 0x57, 0xd4, 0xdc, 0x29, 0xeb, 0xaa, 0x30, 0x7a, 0x0a,
|
||||
0xfa, 0x15, 0x4d, 0xd9, 0x80, 0xf9, 0x44, 0xb2, 0x38, 0xf2, 0xbe, 0xd2, 0x11, 0xae, 0xe6, 0xe8,
|
||||
0x6c, 0xfc, 0x1d, 0x1d, 0xa1, 0x57, 0xb0, 0x9e, 0x28, 0x7f, 0x9e, 0x1f, 0x47, 0x03, 0x16, 0xe0,
|
||||
0x95, 0xa6, 0x66, 0xd4, 0x5b, 0xf7, 0xcd, 0x7c, 0x35, 0xe6, 0x64, 0x35, 0xa6, 0xab, 0x56, 0xe3,
|
||||
0xac, 0xe5, 0xf4, 0xb1, 0x82, 0xd1, 0x43, 0xa8, 0x17, 0xd5, 0x11, 0x09, 0x29, 0x5e, 0x55, 0x33,
|
||||
0x20, 0x0f, 0xf5, 0x48, 0x48, 0xd1, 0x21, 0x54, 0x92, 0x21, 0x11, 0x14, 0xd7, 0x94, 0x7d, 0x63,
|
||||
0xb1, 0x7d, 0x55, 0x67, 0x8f, 0x79, 0x27, 0x2f, 0x43, 0x2f, 0x61, 0x35, 0x49, 0x59, 0x9c, 0x32,
|
||||
0x39, 0xc2, 0xa0, 0x94, 0xed, 0xfc, 0xa5, 0xac, 0x1b, 0xc9, 0x83, 0xd6, 0x27, 0xc2, 0x33, 0xea,
|
||||
0xdc, 0xc2, 0xe8, 0x10, 0x36, 0x2e, 0xe9, 0x80, 0x64, 0x5c, 0x4e, 0x8c, 0xd1, 0xc5, 0xc6, 0xd6,
|
||||
0x0b, 0xbc, 0x70, 0xf6, 0x16, 0xea, 0x21, 0x91, 0xfe, 0xd0, 0x4b, 0x33, 0x4e, 0x05, 0x1e, 0x34,
|
||||
0xcb, 0x46, 0xbd, 0xf5, 0x78, 0xa1, 0xfc, 0x0f, 0x63, 0xde, 0xc9, 0x38, 0x75, 0x20, 0x9c, 0x1c,
|
||||
0x05, 0x7a, 0x06, 0x5b, 0xf3, 0x42, 0xbc, 0x4b, 0x26, 0x48, 0x9f, 0x53, 0x1c, 0x34, 0x35, 0x63,
|
||||
0xd5, 0xf9, 0x7f, 0x6e, 0xee, 0x49, 0x9e, 0xdb, 0xfd, 0xae, 0x41, 0xed, 0xb6, 0x1f, 0xc2, 0xb0,
|
||||
0xc2, 0x22, 0x35, 0x18, 0x6b, 0xcd, 0xb2, 0x51, 0x73, 0x26, 0xd7, 0xf1, 0x13, 0xbc, 0x8c, 0x43,
|
||||
0xc2, 0x22, 0x5c, 0x52, 0x89, 0xe2, 0x86, 0x2c, 0xa8, 0x16, 0xb6, 0xcb, 0x8b, 0x6d, 0x17, 0x18,
|
||||
0x7a, 0x04, 0x1b, 0x77, 0xe4, 0x2d, 0x2b, 0x79, 0xeb, 0xfe, 0xac, 0xae, 0xb1, 0x12, 0x41, 0xd3,
|
||||
0x2b, 0xe6, 0x53, 0x5c, 0xc9, 0x95, 0x14, 0xd7, 0xbd, 0x0e, 0xd4, 0x67, 0xbe, 0x1f, 0xba, 0x07,
|
||||
0x9b, 0xe7, 0x3d, 0xd7, 0xee, 0x1c, 0x77, 0xdf, 0x74, 0x3b, 0x27, 0x9e, 0x7d, 0xda, 0x76, 0x3b,
|
||||
0xfa, 0x12, 0xaa, 0x41, 0xa5, 0x7d, 0x7e, 0x76, 0xda, 0xd3, 0xb5, 0xc9, 0xf1, 0x42, 0x2f, 0x8d,
|
||||
0x8f, 0xee, 0x59, 0xfb, 0xcc, 0xd5, 0xcb, 0x7b, 0x47, 0x00, 0x33, 0x8f, 0x7e, 0x0b, 0xd0, 0x5c,
|
||||
0x97, 0x8f, 0xef, 0xbb, 0xc7, 0x5f, 0xf4, 0x25, 0xa4, 0xc3, 0x5a, 0x77, 0xd0, 0x8b, 0xa5, 0x9d,
|
||||
0x52, 0x41, 0x23, 0xa9, 0x6b, 0x08, 0xa0, 0xda, 0xe6, 0xd7, 0x64, 0x24, 0xf4, 0xd2, 0xd1, 0xeb,
|
||||
0x1f, 0x37, 0x0d, 0xed, 0xe7, 0x4d, 0x43, 0xfb, 0x75, 0xd3, 0xd0, 0x2e, 0x5a, 0x01, 0x93, 0xc3,
|
||||
0xac, 0x6f, 0xfa, 0x71, 0x68, 0x11, 0xce, 0xfa, 0xa4, 0x4f, 0xac, 0xe2, 0x33, 0x5a, 0x24, 0x61,
|
||||
0xd6, 0x3f, 0x7e, 0x30, 0xfd, 0xaa, 0x5a, 0xd3, 0xc1, 0x9f, 0x00, 0x00, 0x00, 0xff, 0xff, 0x0b,
|
||||
0x3c, 0xc3, 0xcf, 0x7e, 0x04, 0x00, 0x00,
|
||||
}
|
||||
|
||||
func (m *WasmPlugin) Marshal() (dAtA []byte, err error) {
|
||||
@@ -581,6 +590,15 @@ func (m *MatchRule) MarshalToSizedBuffer(dAtA []byte) (int, error) {
|
||||
i -= len(m.XXX_unrecognized)
|
||||
copy(dAtA[i:], m.XXX_unrecognized)
|
||||
}
|
||||
if len(m.Service) > 0 {
|
||||
for iNdEx := len(m.Service) - 1; iNdEx >= 0; iNdEx-- {
|
||||
i -= len(m.Service[iNdEx])
|
||||
copy(dAtA[i:], m.Service[iNdEx])
|
||||
i = encodeVarintWasm(dAtA, i, uint64(len(m.Service[iNdEx])))
|
||||
i--
|
||||
dAtA[i] = 0x2a
|
||||
}
|
||||
}
|
||||
if m.ConfigDisable {
|
||||
i--
|
||||
if m.ConfigDisable {
|
||||
@@ -719,6 +737,12 @@ func (m *MatchRule) Size() (n int) {
|
||||
if m.ConfigDisable {
|
||||
n += 2
|
||||
}
|
||||
if len(m.Service) > 0 {
|
||||
for _, s := range m.Service {
|
||||
l = len(s)
|
||||
n += 1 + l + sovWasm(uint64(l))
|
||||
}
|
||||
}
|
||||
if m.XXX_unrecognized != nil {
|
||||
n += len(m.XXX_unrecognized)
|
||||
}
|
||||
@@ -1291,6 +1315,38 @@ func (m *MatchRule) Unmarshal(dAtA []byte) error {
|
||||
}
|
||||
}
|
||||
m.ConfigDisable = bool(v != 0)
|
||||
case 5:
|
||||
if wireType != 2 {
|
||||
return fmt.Errorf("proto: wrong wireType = %d for field Service", wireType)
|
||||
}
|
||||
var stringLen uint64
|
||||
for shift := uint(0); ; shift += 7 {
|
||||
if shift >= 64 {
|
||||
return ErrIntOverflowWasm
|
||||
}
|
||||
if iNdEx >= l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
b := dAtA[iNdEx]
|
||||
iNdEx++
|
||||
stringLen |= uint64(b&0x7F) << shift
|
||||
if b < 0x80 {
|
||||
break
|
||||
}
|
||||
}
|
||||
intStringLen := int(stringLen)
|
||||
if intStringLen < 0 {
|
||||
return ErrInvalidLengthWasm
|
||||
}
|
||||
postIndex := iNdEx + intStringLen
|
||||
if postIndex < 0 {
|
||||
return ErrInvalidLengthWasm
|
||||
}
|
||||
if postIndex > l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
m.Service = append(m.Service, string(dAtA[iNdEx:postIndex]))
|
||||
iNdEx = postIndex
|
||||
default:
|
||||
iNdEx = preIndex
|
||||
skippy, err := skipWasm(dAtA[iNdEx:])
|
||||
|
||||
@@ -114,6 +114,7 @@ message MatchRule {
|
||||
repeated string domain = 2;
|
||||
google.protobuf.Struct config = 3;
|
||||
bool config_disable = 4;
|
||||
repeated string service = 5;
|
||||
}
|
||||
|
||||
// The phase in the filter chain where the plugin will be injected.
|
||||
|
||||
@@ -64,6 +64,10 @@ spec:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
service:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
type: object
|
||||
type: array
|
||||
phase:
|
||||
|
||||
@@ -97,7 +97,7 @@ higress: {{ include "controller.name" . }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "skywalking.enabled" -}}
|
||||
{{- if and .Values.skywalking.enabled .Values.skywalking.service.address }}
|
||||
{{- if and (hasKey .Values "tracing") .Values.tracing.enable (hasKey .Values.tracing "skywalking") .Values.tracing.skywalking.service }}
|
||||
true
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
@@ -46,10 +46,6 @@
|
||||
address: {{ .Values.global.tracer.lightstep.address }}
|
||||
# Access Token used to communicate with the Satellite pool
|
||||
accessToken: {{ .Values.global.tracer.lightstep.accessToken }}
|
||||
{{- else if eq .Values.global.proxy.tracer "zipkin" }}
|
||||
zipkin:
|
||||
# Address of the Zipkin collector
|
||||
address: {{ .Values.global.tracer.zipkin.address | default (print "zipkin." .Release.Namespace ":9411") }}
|
||||
{{- else if eq .Values.global.proxy.tracer "datadog" }}
|
||||
datadog:
|
||||
# Address of the Datadog Agent
|
||||
@@ -109,7 +105,17 @@ metadata:
|
||||
labels:
|
||||
{{- include "gateway.labels" . | nindent 4 }}
|
||||
data:
|
||||
|
||||
higress: |-
|
||||
{{- $existingConfig := lookup "v1" "ConfigMap" .Release.Namespace "higress-config" }}
|
||||
{{- $existingData := dict }}
|
||||
{{- if $existingConfig }}
|
||||
{{- $existingData = index $existingConfig.data "higress" | default "{}" | fromYaml }}
|
||||
{{- end }}
|
||||
{{- $newData := dict }}
|
||||
{{- if and (hasKey .Values "tracing") .Values.tracing.enable }}
|
||||
{{- $_ := set $newData "tracing" .Values.tracing }}
|
||||
{{- end }}
|
||||
{{- toYaml (merge $existingData $newData) | nindent 4 }}
|
||||
# Configuration file for the mesh networks to be used by the Split Horizon EDS.
|
||||
meshNetworks: |-
|
||||
{{- if .Values.global.meshNetworks }}
|
||||
@@ -170,8 +176,8 @@ data:
|
||||
"endpoint": {
|
||||
"address": {
|
||||
"socket_address": {
|
||||
"address": "{{ .Values.skywalking.service.address }}",
|
||||
"port_value": "{{ .Values.skywalking.service.port }}"
|
||||
"address": "{{ .Values.tracing.skywalking.service }}",
|
||||
"port_value": "{{ .Values.tracing.skywalking.port }}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,9 +178,9 @@ global:
|
||||
# Default port for Pilot agent health checks. A value of 0 will disable health checking.
|
||||
statusPort: 15020
|
||||
|
||||
# Specify which tracer to use. One of: zipkin, lightstep, datadog, stackdriver.
|
||||
# Specify which tracer to use. One of: lightstep, datadog, stackdriver.
|
||||
# If using stackdriver tracer outside GCP, set env GOOGLE_APPLICATION_CREDENTIALS to the GCP credential file.
|
||||
tracer: "zipkin"
|
||||
tracer: ""
|
||||
|
||||
# Controls if sidecar is injected at the front of the container list and blocks the start of the other containers until the proxy is ready
|
||||
holdApplicationUntilProxyStarts: false
|
||||
@@ -330,12 +330,8 @@ global:
|
||||
maxNumberOfAnnotations: 200
|
||||
# The global default max number of attributes per span.
|
||||
maxNumberOfAttributes: 200
|
||||
zipkin:
|
||||
# Host:Port for reporting trace data in zipkin format. If not specified, will default to
|
||||
# zipkin service (port 9411) in the same namespace as the other istio components.
|
||||
address: ""
|
||||
|
||||
# Use the Mesh Control Protocol (MCP) for configuring Istiod. Requires an MCP source.
|
||||
|
||||
useMCP: false
|
||||
|
||||
# Observability (o11y) configurations
|
||||
@@ -668,9 +664,15 @@ pilot:
|
||||
podLabels: {}
|
||||
|
||||
|
||||
# Skywalking config settings
|
||||
skywalking:
|
||||
enabled: false
|
||||
service:
|
||||
address: ~
|
||||
port: 11800
|
||||
# Tracing config settings
|
||||
tracing:
|
||||
enable: false
|
||||
sampling: 100
|
||||
timeout: 500
|
||||
skywalking:
|
||||
# access_token: ""
|
||||
service: ""
|
||||
port: 11800
|
||||
# zipkin:
|
||||
# service: ""
|
||||
# port: 9411
|
||||
|
||||
@@ -32,7 +32,7 @@ func ParseProtocol(s string) Protocol {
|
||||
return TCP
|
||||
case "http":
|
||||
return HTTP
|
||||
case "grpc":
|
||||
case "grpc", "triple", "tri":
|
||||
return GRPC
|
||||
case "dubbo":
|
||||
return Dubbo
|
||||
|
||||
@@ -841,6 +841,7 @@ func (m *IngressConfig) convertIstioWasmPlugin(obj *higressext.WasmPlugin) (*ext
|
||||
StructValue: rule.Config,
|
||||
}
|
||||
var matchItems []*types.Value
|
||||
// match ingress
|
||||
for _, ing := range rule.Ingress {
|
||||
matchItems = append(matchItems, &types.Value{
|
||||
Kind: &types.Value_StringValue{
|
||||
@@ -861,6 +862,7 @@ func (m *IngressConfig) convertIstioWasmPlugin(obj *higressext.WasmPlugin) (*ext
|
||||
})
|
||||
continue
|
||||
}
|
||||
// match domain
|
||||
for _, domain := range rule.Domain {
|
||||
matchItems = append(matchItems, &types.Value{
|
||||
Kind: &types.Value_StringValue{
|
||||
@@ -868,10 +870,31 @@ func (m *IngressConfig) convertIstioWasmPlugin(obj *higressext.WasmPlugin) (*ext
|
||||
},
|
||||
})
|
||||
}
|
||||
if len(matchItems) > 0 {
|
||||
v.StructValue.Fields["_match_domain_"] = &types.Value{
|
||||
Kind: &types.Value_ListValue{
|
||||
ListValue: &types.ListValue{
|
||||
Values: matchItems,
|
||||
},
|
||||
},
|
||||
}
|
||||
ruleValues = append(ruleValues, &types.Value{
|
||||
Kind: v,
|
||||
})
|
||||
continue
|
||||
}
|
||||
// match service
|
||||
for _, service := range rule.Service {
|
||||
matchItems = append(matchItems, &types.Value{
|
||||
Kind: &types.Value_StringValue{
|
||||
StringValue: service,
|
||||
},
|
||||
})
|
||||
}
|
||||
if len(matchItems) == 0 {
|
||||
return nil, fmt.Errorf("invalid match rule has no match condition, rule:%v", rule)
|
||||
}
|
||||
v.StructValue.Fields["_match_domain_"] = &types.Value{
|
||||
v.StructValue.Fields["_match_service_"] = &types.Value{
|
||||
Kind: &types.Value_ListValue{
|
||||
ListValue: &types.ListValue{
|
||||
Values: matchItems,
|
||||
|
||||
@@ -5,7 +5,7 @@ GO_VERSION ?= 1.19
|
||||
TINYGO_VERSION ?= 0.28.1
|
||||
ORAS_VERSION ?= 1.0.0
|
||||
HIGRESS_VERSION ?= 1.0.0-rc
|
||||
USE_HIGRESS_TINYGO ?= true
|
||||
USE_HIGRESS_TINYGO ?= false
|
||||
BUILDER ?= ${BUILDER_REGISTRY}wasm-go-builder:go${GO_VERSION}-tinygo${TINYGO_VERSION}-oras${ORAS_VERSION}
|
||||
BUILD_TIME := $(shell date "+%Y%m%d-%H%M%S")
|
||||
COMMIT_ID := $(shell git rev-parse --short HEAD 2>/dev/null)
|
||||
|
||||
@@ -32,3 +32,15 @@ redis:
|
||||
serviceName: my-redis.dns
|
||||
timeout: 2000
|
||||
```
|
||||
|
||||
## 进阶用法
|
||||
|
||||
当前默认的缓存 key 是基于 GJSON PATH 的表达式:`messages.@reverse.0.content` 提取,含义是把 messages 数组反转后取第一项的 content;
|
||||
|
||||
GJSON PATH 支持条件判断语法,例如希望取最后一个 role 为 user 的 content 作为 key,可以写成: `messages.@reverse.#(role=="user").content`;
|
||||
|
||||
如果希望将所有 role 为 user 的 content 拼成一个数组作为 key,可以写成:`messages.@reverse.#(role=="user")#.content`;
|
||||
|
||||
还可以支持管道语法,例如希望取到数第二个 role 为 user 的 content 作为 key,可以写成:`messages.@reverse.#(role=="user")#.content|1`。
|
||||
|
||||
更多用法可以参考[官方文档](https://github.com/tidwall/gjson/blob/master/SYNTAX.md),可以使用 [GJSON Playground](https://gjson.dev/) 进行语法测试。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: AI 代理
|
||||
keywords: [ higress,ai,proxy,rag ]
|
||||
keywords: [ AI网关, AI代理 ]
|
||||
description: AI 代理插件配置参考
|
||||
---
|
||||
|
||||
@@ -9,6 +9,13 @@ description: AI 代理插件配置参考
|
||||
`AI 代理`插件实现了基于 OpenAI API 契约的 AI 代理功能。目前支持 OpenAI、Azure OpenAI、月之暗面(Moonshot)和通义千问等 AI
|
||||
服务提供商。
|
||||
|
||||
> **注意:**
|
||||
|
||||
> 请求路径后缀匹配 `/v1/chat/completions` 时,对应文生文场景,会用 OpenAI 的文生文协议解析请求 Body,再转换为对应 LLM 厂商的文生文协议
|
||||
|
||||
> 请求路径后缀匹配 `/v1/embeddings` 时,对应文本向量场景,会用 OpenAI 的文本向量协议解析请求 Body,再转换为对应 LLM 厂商的文本向量协议
|
||||
|
||||
|
||||
## 配置字段
|
||||
|
||||
### 基本配置
|
||||
@@ -19,14 +26,14 @@ description: AI 代理插件配置参考
|
||||
|
||||
`provider`的配置字段说明如下:
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
| -------------- | --------------- | -------- | ------ |-------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `type` | string | 必填 | - | AI 服务提供商名称 |
|
||||
| `apiTokens` | array of string | 必填 | - | 用于在访问 AI 服务时进行认证的令牌。如果配置了多个 token,插件会在请求时随机进行选择。部分服务提供商只支持配置一个 token。 |
|
||||
| `timeout` | number | 非必填 | - | 访问 AI 服务的超时时间。单位为毫秒。默认值为 120000,即 2 分钟 |
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
| -------------- | --------------- | -------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `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 对话上下文信息 |
|
||||
| `protocol` | string | 非必填 | - | 插件对外提供的 API 接口契约。目前支持以下取值:openai(默认值,使用 OpenAI 的接口契约)、original(使用目标服务提供商的原始接口契约) |
|
||||
| `context` | object | 非必填 | - | 配置 AI 对话上下文信息 |
|
||||
|
||||
`context`的配置字段说明如下:
|
||||
|
||||
@@ -40,7 +47,12 @@ description: AI 代理插件配置参考
|
||||
|
||||
#### OpenAI
|
||||
|
||||
OpenAI 所对应的 `type` 为 `openai`。它并无特有的配置字段。
|
||||
OpenAI 所对应的 `type` 为 `openai`。它特有的配置字段如下:
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
|-------------------|----------|----------|--------|-------------------------------------------------------------------------------|
|
||||
| `openaiCustomUrl` | string | 非必填 | - | 基于OpenAI协议的自定义后端URL,例如: www.example.com/myai/v1/chat/completions |
|
||||
|
||||
|
||||
#### Azure OpenAI
|
||||
|
||||
@@ -139,6 +151,19 @@ 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) |
|
||||
|
||||
#### 星火 (Spark)
|
||||
|
||||
星火所对应的 `type` 为 `spark`。它并无特有的配置字段。
|
||||
|
||||
讯飞星火认知大模型的`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) |
|
||||
|
||||
## 用法示例
|
||||
|
||||
@@ -865,6 +890,124 @@ provider:
|
||||
}
|
||||
```
|
||||
|
||||
### 使用 OpenAI 协议代理Spark服务
|
||||
|
||||
**配置信息**
|
||||
|
||||
```yaml
|
||||
provider:
|
||||
type: spark
|
||||
apiTokens:
|
||||
- "APIKey:APISecret"
|
||||
modelMapping:
|
||||
"gpt-4o": "generalv3.5"
|
||||
"gpt-4": "generalv3"
|
||||
"*": "general"
|
||||
```
|
||||
|
||||
**请求示例**
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "gpt-4o",
|
||||
"messages": [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "你是一名专业的开发人员!"
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "你好,你是谁?"
|
||||
}
|
||||
],
|
||||
"stream": false
|
||||
}
|
||||
```
|
||||
|
||||
**响应示例**
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "cha000c23c6@dx190ef0b4b96b8f2532",
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "你好!我是一名专业的开发人员,擅长编程和解决技术问题。有什么我可以帮助你的吗?"
|
||||
}
|
||||
}
|
||||
],
|
||||
"created": 1721997415,
|
||||
"model": "generalv3.5",
|
||||
"object": "chat.completion",
|
||||
"usage": {
|
||||
"prompt_tokens": 10,
|
||||
"completion_tokens": 19,
|
||||
"total_tokens": 29
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 使用 OpenAI 协议代理 gemini 服务
|
||||
|
||||
**配置信息**
|
||||
|
||||
```yaml
|
||||
provider:
|
||||
type: gemini
|
||||
apiTokens:
|
||||
- "YOUR_GEMINI_API_TOKEN"
|
||||
modelMapping:
|
||||
"*": "gemini-pro"
|
||||
geminiSafetySetting:
|
||||
"HARM_CATEGORY_SEXUALLY_EXPLICIT" :"BLOCK_NONE"
|
||||
"HARM_CATEGORY_HATE_SPEECH" :"BLOCK_NONE"
|
||||
"HARM_CATEGORY_HARASSMENT" :"BLOCK_NONE"
|
||||
"HARM_CATEGORY_DANGEROUS_CONTENT" :"BLOCK_NONE"
|
||||
```
|
||||
|
||||
**请求示例**
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "gpt-3.5",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Who are you?"
|
||||
}
|
||||
],
|
||||
"stream": false
|
||||
}
|
||||
```
|
||||
|
||||
**响应示例**
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "chatcmpl-b010867c-0d3f-40ba-95fd-4e8030551aeb",
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "I am a large multi-modal model, trained by Google. I am designed to provide information and answer questions to the best of my abilities."
|
||||
},
|
||||
"finish_reason": "stop"
|
||||
}
|
||||
],
|
||||
"created": 1722756984,
|
||||
"model": "gemini-pro",
|
||||
"object": "chat.completion",
|
||||
"usage": {
|
||||
"prompt_tokens": 5,
|
||||
"completion_tokens": 29,
|
||||
"total_tokens": 34
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 完整配置示例
|
||||
|
||||
### Kubernetes 示例
|
||||
|
||||
@@ -23,6 +23,9 @@ func (m *azureProviderInitializer) ValidateConfig(config ProviderConfig) error {
|
||||
if _, err := url.Parse(config.azureServiceUrl); err != nil {
|
||||
return fmt.Errorf("invalid azureServiceUrl: %w", err)
|
||||
}
|
||||
if config.apiTokens == nil || len(config.apiTokens) == 0 {
|
||||
return errors.New("no apiToken found in provider config")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -52,27 +55,32 @@ func (m *azureProvider) GetProviderType() string {
|
||||
}
|
||||
|
||||
func (m *azureProvider) OnRequestHeaders(ctx wrapper.HttpContext, apiName ApiName, log wrapper.Log) (types.Action, error) {
|
||||
if apiName != ApiNameChatCompletion {
|
||||
return types.ActionContinue, errUnsupportedApiName
|
||||
}
|
||||
_ = util.OverwriteRequestPath(m.serviceUrl.RequestURI())
|
||||
_ = util.OverwriteRequestHost(m.serviceUrl.Host)
|
||||
_ = proxywasm.ReplaceHttpRequestHeader("api-key", m.config.apiTokens[0])
|
||||
_ = proxywasm.RemoveHttpRequestHeader("Content-Length")
|
||||
if apiName == ApiNameChatCompletion {
|
||||
_ = proxywasm.RemoveHttpRequestHeader("Content-Length")
|
||||
} else {
|
||||
ctx.DontReadRequestBody()
|
||||
}
|
||||
return types.ActionContinue, nil
|
||||
}
|
||||
|
||||
func (m *azureProvider) OnRequestBody(ctx wrapper.HttpContext, apiName ApiName, body []byte, log wrapper.Log) (types.Action, error) {
|
||||
if apiName != ApiNameChatCompletion {
|
||||
return types.ActionContinue, errUnsupportedApiName
|
||||
}
|
||||
if m.contextCache == nil {
|
||||
// We don't need to process the request body for other APIs.
|
||||
return types.ActionContinue, nil
|
||||
}
|
||||
request := &chatCompletionRequest{}
|
||||
if err := decodeChatCompletionRequest(body, request); err != nil {
|
||||
return types.ActionContinue, err
|
||||
}
|
||||
if m.contextCache == nil {
|
||||
if err := replaceJsonRequestBody(request, log); err != nil {
|
||||
_ = util.SendResponse(500, "ai-proxy.openai.set_include_usage_failed", util.MimeTypeTextPlain, fmt.Sprintf("failed to replace request body: %v", err))
|
||||
}
|
||||
return types.ActionContinue, nil
|
||||
}
|
||||
err := m.contextCache.GetContent(func(content string, err error) {
|
||||
defer func() {
|
||||
_ = proxywasm.ResumeHttpRequest()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/extensions/ai-proxy/util"
|
||||
@@ -20,6 +21,9 @@ type baichuanProviderInitializer struct {
|
||||
}
|
||||
|
||||
func (m *baichuanProviderInitializer) ValidateConfig(config ProviderConfig) error {
|
||||
if config.apiTokens == nil || len(config.apiTokens) == 0 {
|
||||
return errors.New("no apiToken found in provider config")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -34,6 +34,9 @@ type baiduProviderInitializer struct {
|
||||
}
|
||||
|
||||
func (b *baiduProviderInitializer) ValidateConfig(config ProviderConfig) error {
|
||||
if config.apiTokens == nil || len(config.apiTokens) == 0 {
|
||||
return errors.New("no apiToken found in provider config")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -80,7 +83,7 @@ func (b *baiduProvider) OnRequestBody(ctx wrapper.HttpContext, apiName ApiName,
|
||||
return types.ActionContinue, errors.New("request model is empty")
|
||||
}
|
||||
// 根据模型重写requestPath
|
||||
path := b.GetRequestPath(request.Model)
|
||||
path := b.getRequestPath(request.Model)
|
||||
_ = util.OverwriteRequestPath(path)
|
||||
|
||||
if b.config.context == nil {
|
||||
@@ -123,7 +126,7 @@ func (b *baiduProvider) OnRequestBody(ctx wrapper.HttpContext, apiName ApiName,
|
||||
}
|
||||
request.Model = mappedModel
|
||||
ctx.SetContext(ctxKeyFinalRequestModel, request.Model)
|
||||
path := b.GetRequestPath(mappedModel)
|
||||
path := b.getRequestPath(mappedModel)
|
||||
_ = util.OverwriteRequestPath(path)
|
||||
|
||||
if b.config.context == nil {
|
||||
@@ -223,7 +226,7 @@ type baiduTextGenRequest struct {
|
||||
UserId string `json:"user_id,omitempty"`
|
||||
}
|
||||
|
||||
func (b *baiduProvider) GetRequestPath(baiduModel string) string {
|
||||
func (b *baiduProvider) getRequestPath(baiduModel string) string {
|
||||
// https://cloud.baidu.com/doc/WENXINWORKSHOP/s/clntwmv7t
|
||||
suffix, ok := baiduModelToPathSuffixMap[baiduModel]
|
||||
if !ok {
|
||||
@@ -323,7 +326,7 @@ func (b *baiduProvider) streamResponseBaidu2OpenAI(ctx wrapper.HttpContext, resp
|
||||
Created: time.Now().UnixMilli() / 1000,
|
||||
Model: ctx.GetStringContext(ctxKeyFinalRequestModel, ""),
|
||||
SystemFingerprint: "",
|
||||
Object: objectChatCompletion,
|
||||
Object: objectChatCompletionChunk,
|
||||
Choices: []chatCompletionChoice{choice},
|
||||
Usage: usage{
|
||||
PromptTokens: response.Usage.PromptTokens,
|
||||
|
||||
@@ -4,12 +4,13 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/extensions/ai-proxy/util"
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// claudeProvider is the provider for Claude service.
|
||||
@@ -78,6 +79,9 @@ type claudeTextGenDelta struct {
|
||||
}
|
||||
|
||||
func (c *claudeProviderInitializer) ValidateConfig(config ProviderConfig) error {
|
||||
if config.apiTokens == nil || len(config.apiTokens) == 0 {
|
||||
return errors.New("no apiToken found in provider config")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,9 @@ type cloudflareProviderInitializer struct {
|
||||
}
|
||||
|
||||
func (c *cloudflareProviderInitializer) ValidateConfig(config ProviderConfig) error {
|
||||
if config.apiTokens == nil || len(config.apiTokens) == 0 {
|
||||
return errors.New("no apiToken found in provider config")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/extensions/ai-proxy/util"
|
||||
@@ -20,6 +21,9 @@ type deepseekProviderInitializer struct {
|
||||
}
|
||||
|
||||
func (m *deepseekProviderInitializer) ValidateConfig(config ProviderConfig) error {
|
||||
if config.apiTokens == nil || len(config.apiTokens) == 0 {
|
||||
return errors.New("no apiToken found in provider config")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
606
plugins/wasm-go/extensions/ai-proxy/provider/gemini.go
Normal file
606
plugins/wasm-go/extensions/ai-proxy/provider/gemini.go
Normal file
@@ -0,0 +1,606 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/alibaba/higress/plugins/wasm-go/extensions/ai-proxy/util"
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
"github.com/google/uuid"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// geminiProvider is the provider for google gemini/gemini flash service.
|
||||
|
||||
const (
|
||||
geminiApiKeyHeader = "x-goog-api-key"
|
||||
geminiDomain = "generativelanguage.googleapis.com"
|
||||
)
|
||||
|
||||
type geminiProviderInitializer struct {
|
||||
}
|
||||
|
||||
func (g *geminiProviderInitializer) ValidateConfig(config ProviderConfig) error {
|
||||
if config.apiTokens == nil || len(config.apiTokens) == 0 {
|
||||
return errors.New("no apiToken found in provider config")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *geminiProviderInitializer) CreateProvider(config ProviderConfig) (Provider, error) {
|
||||
return &geminiProvider{
|
||||
config: config,
|
||||
contextCache: createContextCache(&config),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type geminiProvider struct {
|
||||
config ProviderConfig
|
||||
contextCache *contextCache
|
||||
}
|
||||
|
||||
func (g *geminiProvider) GetProviderType() string {
|
||||
return providerTypeGemini
|
||||
}
|
||||
|
||||
func (g *geminiProvider) OnRequestHeaders(ctx wrapper.HttpContext, apiName ApiName, log wrapper.Log) (types.Action, error) {
|
||||
if apiName != ApiNameChatCompletion && apiName != ApiNameEmbeddings {
|
||||
return types.ActionContinue, errUnsupportedApiName
|
||||
}
|
||||
|
||||
_ = proxywasm.ReplaceHttpRequestHeader(geminiApiKeyHeader, g.config.GetRandomToken())
|
||||
_ = util.OverwriteRequestHost(geminiDomain)
|
||||
|
||||
_ = proxywasm.RemoveHttpRequestHeader("Accept-Encoding")
|
||||
_ = proxywasm.RemoveHttpRequestHeader("Content-Length")
|
||||
|
||||
// Delay the header processing to allow changing streaming mode in OnRequestBody
|
||||
return types.HeaderStopIteration, nil
|
||||
}
|
||||
|
||||
func (g *geminiProvider) OnRequestBody(ctx wrapper.HttpContext, apiName ApiName, body []byte, log wrapper.Log) (types.Action, error) {
|
||||
if apiName == ApiNameChatCompletion {
|
||||
return g.onChatCompletionRequestBody(ctx, body, log)
|
||||
} else if apiName == ApiNameEmbeddings {
|
||||
return g.onEmbeddingsRequestBody(ctx, body, log)
|
||||
}
|
||||
return types.ActionContinue, errUnsupportedApiName
|
||||
}
|
||||
|
||||
func (g *geminiProvider) onChatCompletionRequestBody(ctx wrapper.HttpContext, body []byte, log wrapper.Log) (types.Action, error) {
|
||||
// 使用gemini接口协议
|
||||
if g.config.protocol == protocolOriginal {
|
||||
request := &geminiChatRequest{}
|
||||
if err := json.Unmarshal(body, request); err != nil {
|
||||
return types.ActionContinue, fmt.Errorf("unable to unmarshal request: %v", err)
|
||||
}
|
||||
if request.Model == "" {
|
||||
return types.ActionContinue, errors.New("request model is empty")
|
||||
}
|
||||
// 根据模型重写requestPath
|
||||
path := g.getRequestPath(ApiNameChatCompletion, request.Model, request.Stream)
|
||||
_ = util.OverwriteRequestPath(path)
|
||||
|
||||
// 移除多余的model和stream字段
|
||||
request = &geminiChatRequest{
|
||||
Contents: request.Contents,
|
||||
SafetySettings: request.SafetySettings,
|
||||
GenerationConfig: request.GenerationConfig,
|
||||
Tools: request.Tools,
|
||||
}
|
||||
if g.config.context == nil {
|
||||
return types.ActionContinue, replaceJsonRequestBody(request, log)
|
||||
}
|
||||
|
||||
err := g.contextCache.GetContent(func(content string, err error) {
|
||||
defer func() {
|
||||
_ = proxywasm.ResumeHttpRequest()
|
||||
}()
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("failed to load context file: %v", err)
|
||||
_ = util.SendResponse(500, "ai-proxy.gemini.load_ctx_failed", util.MimeTypeTextPlain, fmt.Sprintf("failed to load context file: %v", err))
|
||||
}
|
||||
g.setSystemContent(request, content)
|
||||
if err := replaceJsonRequestBody(request, log); err != nil {
|
||||
_ = util.SendResponse(500, "ai-proxy.gemini.insert_ctx_failed", util.MimeTypeTextPlain, fmt.Sprintf("failed to replace request body: %v", err))
|
||||
}
|
||||
}, log)
|
||||
if err == nil {
|
||||
return types.ActionPause, nil
|
||||
}
|
||||
return types.ActionContinue, err
|
||||
}
|
||||
request := &chatCompletionRequest{}
|
||||
if err := decodeChatCompletionRequest(body, request); err != nil {
|
||||
return types.ActionContinue, err
|
||||
}
|
||||
|
||||
// 映射模型重写requestPath
|
||||
model := request.Model
|
||||
if model == "" {
|
||||
return types.ActionContinue, errors.New("missing model in chat completion request")
|
||||
}
|
||||
ctx.SetContext(ctxKeyOriginalRequestModel, model)
|
||||
mappedModel := getMappedModel(model, g.config.modelMapping, log)
|
||||
if mappedModel == "" {
|
||||
return types.ActionContinue, errors.New("model becomes empty after applying the configured mapping")
|
||||
}
|
||||
request.Model = mappedModel
|
||||
ctx.SetContext(ctxKeyFinalRequestModel, request.Model)
|
||||
path := g.getRequestPath(ApiNameChatCompletion, mappedModel, request.Stream)
|
||||
_ = util.OverwriteRequestPath(path)
|
||||
|
||||
if g.config.context == nil {
|
||||
geminiRequest := g.buildGeminiChatRequest(request)
|
||||
return types.ActionContinue, replaceJsonRequestBody(geminiRequest, log)
|
||||
}
|
||||
|
||||
err := g.contextCache.GetContent(func(content string, err error) {
|
||||
defer func() {
|
||||
_ = proxywasm.ResumeHttpRequest()
|
||||
}()
|
||||
if err != nil {
|
||||
log.Errorf("failed to load context file: %v", err)
|
||||
_ = util.SendResponse(500, "ai-proxy.gemini.load_ctx_failed", util.MimeTypeTextPlain, fmt.Sprintf("failed to load context file: %v", err))
|
||||
}
|
||||
insertContextMessage(request, content)
|
||||
geminiRequest := g.buildGeminiChatRequest(request)
|
||||
if err := replaceJsonRequestBody(geminiRequest, log); err != nil {
|
||||
_ = util.SendResponse(500, "ai-proxy.gemini.insert_ctx_failed", util.MimeTypeTextPlain, fmt.Sprintf("failed to replace request body: %v", err))
|
||||
}
|
||||
}, log)
|
||||
if err == nil {
|
||||
return types.ActionPause, nil
|
||||
}
|
||||
return types.ActionContinue, err
|
||||
}
|
||||
|
||||
func (g *geminiProvider) onEmbeddingsRequestBody(ctx wrapper.HttpContext, body []byte, log wrapper.Log) (types.Action, error) {
|
||||
// 使用gemini接口协议
|
||||
if g.config.protocol == protocolOriginal {
|
||||
request := &geminiBatchEmbeddingRequest{}
|
||||
if err := json.Unmarshal(body, request); err != nil {
|
||||
return types.ActionContinue, fmt.Errorf("unable to unmarshal request: %v", err)
|
||||
}
|
||||
if request.Model == "" {
|
||||
return types.ActionContinue, errors.New("request model is empty")
|
||||
}
|
||||
// 根据模型重写requestPath
|
||||
path := g.getRequestPath(ApiNameEmbeddings, request.Model, false)
|
||||
_ = util.OverwriteRequestPath(path)
|
||||
|
||||
// 移除多余的model字段
|
||||
request = &geminiBatchEmbeddingRequest{
|
||||
Requests: request.Requests,
|
||||
}
|
||||
return types.ActionContinue, replaceJsonRequestBody(request, log)
|
||||
}
|
||||
request := &embeddingsRequest{}
|
||||
if err := json.Unmarshal(body, request); err != nil {
|
||||
return types.ActionContinue, fmt.Errorf("unable to unmarshal request: %v", err)
|
||||
}
|
||||
|
||||
// 映射模型重写requestPath
|
||||
model := request.Model
|
||||
if model == "" {
|
||||
return types.ActionContinue, errors.New("missing model in embeddings request")
|
||||
}
|
||||
ctx.SetContext(ctxKeyOriginalRequestModel, model)
|
||||
mappedModel := getMappedModel(model, g.config.modelMapping, log)
|
||||
if mappedModel == "" {
|
||||
return types.ActionContinue, errors.New("model becomes empty after applying the configured mapping")
|
||||
}
|
||||
request.Model = mappedModel
|
||||
ctx.SetContext(ctxKeyFinalRequestModel, request.Model)
|
||||
path := g.getRequestPath(ApiNameEmbeddings, mappedModel, false)
|
||||
_ = util.OverwriteRequestPath(path)
|
||||
|
||||
geminiRequest := g.buildBatchEmbeddingRequest(request)
|
||||
return types.ActionContinue, replaceJsonRequestBody(geminiRequest, log)
|
||||
}
|
||||
|
||||
func (g *geminiProvider) OnResponseHeaders(ctx wrapper.HttpContext, apiName ApiName, log wrapper.Log) (types.Action, error) {
|
||||
if g.config.protocol == protocolOriginal {
|
||||
ctx.DontReadResponseBody()
|
||||
return types.ActionContinue, nil
|
||||
}
|
||||
|
||||
_ = proxywasm.RemoveHttpResponseHeader("Content-Length")
|
||||
return types.ActionContinue, nil
|
||||
}
|
||||
|
||||
func (g *geminiProvider) OnStreamingResponseBody(ctx wrapper.HttpContext, name ApiName, chunk []byte, isLastChunk bool, log wrapper.Log) ([]byte, error) {
|
||||
log.Infof("chunk body:%s", string(chunk))
|
||||
if isLastChunk || len(chunk) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
// sample end event response:
|
||||
// data: {"candidates": [{"content": {"parts": [{"text": "我是 Gemini,一个大型多模态模型,由 Google 训练。我的职责是尽我所能帮助您,并尽力提供全面且信息丰富的答复。"}],"role": "model"},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}],"usageMetadata": {"promptTokenCount": 2,"candidatesTokenCount": 35,"totalTokenCount": 37}}
|
||||
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 geminiResp geminiChatResponse
|
||||
if err := json.Unmarshal([]byte(data), &geminiResp); err != nil {
|
||||
log.Errorf("unable to unmarshal gemini response: %v", err)
|
||||
continue
|
||||
}
|
||||
response := g.buildChatCompletionStreamResponse(ctx, &geminiResp)
|
||||
responseBody, err := json.Marshal(response)
|
||||
if err != nil {
|
||||
log.Errorf("unable to marshal response: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
g.appendResponse(responseBuilder, string(responseBody))
|
||||
}
|
||||
modifiedResponseChunk := responseBuilder.String()
|
||||
log.Debugf("=== modified response chunk: %s", modifiedResponseChunk)
|
||||
return []byte(modifiedResponseChunk), nil
|
||||
}
|
||||
|
||||
func (g *geminiProvider) OnResponseBody(ctx wrapper.HttpContext, apiName ApiName, body []byte, log wrapper.Log) (types.Action, error) {
|
||||
if apiName == ApiNameChatCompletion {
|
||||
return g.onChatCompletionResponseBody(ctx, body, log)
|
||||
} else if apiName == ApiNameEmbeddings {
|
||||
return g.onEmbeddingsResponseBody(ctx, body, log)
|
||||
}
|
||||
return types.ActionContinue, errUnsupportedApiName
|
||||
}
|
||||
|
||||
func (g *geminiProvider) onChatCompletionResponseBody(ctx wrapper.HttpContext, body []byte, log wrapper.Log) (types.Action, error) {
|
||||
geminiResponse := &geminiChatResponse{}
|
||||
if err := json.Unmarshal(body, geminiResponse); err != nil {
|
||||
return types.ActionContinue, fmt.Errorf("unable to unmarshal gemini chat response: %v", err)
|
||||
}
|
||||
if geminiResponse.Error != nil {
|
||||
return types.ActionContinue, fmt.Errorf("gemini chat completion response error, error_code: %d, error_status:%s, error_message: %s",
|
||||
geminiResponse.Error.Code, geminiResponse.Error.Status, geminiResponse.Error.Message)
|
||||
}
|
||||
response := g.buildChatCompletionResponse(ctx, geminiResponse)
|
||||
return types.ActionContinue, replaceJsonResponseBody(response, log)
|
||||
}
|
||||
|
||||
func (g *geminiProvider) onEmbeddingsResponseBody(ctx wrapper.HttpContext, body []byte, log wrapper.Log) (types.Action, error) {
|
||||
geminiResponse := &geminiEmbeddingResponse{}
|
||||
if err := json.Unmarshal(body, geminiResponse); err != nil {
|
||||
return types.ActionContinue, fmt.Errorf("unable to unmarshal gemini embeddings response: %v", err)
|
||||
}
|
||||
if geminiResponse.Error != nil {
|
||||
return types.ActionContinue, fmt.Errorf("gemini embeddings response error, error_code: %d, error_status:%s, error_message: %s",
|
||||
geminiResponse.Error.Code, geminiResponse.Error.Status, geminiResponse.Error.Message)
|
||||
}
|
||||
response := g.buildEmbeddingsResponse(ctx, geminiResponse)
|
||||
return types.ActionContinue, replaceJsonResponseBody(response, log)
|
||||
}
|
||||
|
||||
func (g *geminiProvider) getRequestPath(apiName ApiName, geminiModel string, stream bool) string {
|
||||
action := ""
|
||||
if apiName == ApiNameEmbeddings {
|
||||
action = "batchEmbedContents"
|
||||
} else if stream {
|
||||
action = "streamGenerateContent?alt=sse"
|
||||
} else {
|
||||
action = "generateContent"
|
||||
}
|
||||
return fmt.Sprintf("/v1/models/%s:%s", geminiModel, action)
|
||||
}
|
||||
|
||||
type geminiChatRequest 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"`
|
||||
}
|
||||
|
||||
type geminiChatContent struct {
|
||||
Role string `json:"role,omitempty"`
|
||||
Parts []geminiPart `json:"parts"`
|
||||
}
|
||||
|
||||
type geminiChatSafetySetting struct {
|
||||
Category string `json:"category"`
|
||||
Threshold string `json:"threshold"`
|
||||
}
|
||||
|
||||
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"`
|
||||
}
|
||||
|
||||
type geminiChatTools struct {
|
||||
FunctionDeclarations any `json:"function_declarations,omitempty"`
|
||||
}
|
||||
|
||||
type geminiPart struct {
|
||||
Text string `json:"text,omitempty"`
|
||||
InlineData *geminiInlineData `json:"inlineData,omitempty"`
|
||||
FunctionCall *geminiFunctionCall `json:"functionCall,omitempty"`
|
||||
}
|
||||
|
||||
type geminiInlineData struct {
|
||||
MimeType string `json:"mimeType"`
|
||||
Data string `json:"data"`
|
||||
}
|
||||
|
||||
type geminiFunctionCall struct {
|
||||
FunctionName string `json:"name"`
|
||||
Arguments any `json:"args"`
|
||||
}
|
||||
|
||||
func (g *geminiProvider) buildGeminiChatRequest(request *chatCompletionRequest) *geminiChatRequest {
|
||||
var safetySettings []geminiChatSafetySetting
|
||||
{
|
||||
}
|
||||
for category, threshold := range g.config.geminiSafetySetting {
|
||||
safetySettings = append(safetySettings, geminiChatSafetySetting{
|
||||
Category: category,
|
||||
Threshold: threshold,
|
||||
})
|
||||
}
|
||||
geminiRequest := geminiChatRequest{
|
||||
Contents: make([]geminiChatContent, 0, len(request.Messages)),
|
||||
SafetySettings: safetySettings,
|
||||
GenerationConfig: geminiChatGenerationConfig{
|
||||
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)
|
||||
}
|
||||
geminiRequest.Tools = []geminiChatTools{
|
||||
{
|
||||
FunctionDeclarations: functions,
|
||||
},
|
||||
}
|
||||
}
|
||||
shouldAddDummyModelMessage := false
|
||||
for _, message := range request.Messages {
|
||||
content := geminiChatContent{
|
||||
Role: message.Role,
|
||||
Parts: []geminiPart{
|
||||
{
|
||||
Text: message.Content,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// there's no assistant role in gemini 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
|
||||
}
|
||||
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) {
|
||||
systemContents := []geminiChatContent{{
|
||||
Role: roleUser,
|
||||
Parts: []geminiPart{
|
||||
{
|
||||
Text: content,
|
||||
},
|
||||
},
|
||||
}}
|
||||
request.Contents = append(systemContents, request.Contents...)
|
||||
}
|
||||
|
||||
type geminiBatchEmbeddingRequest struct {
|
||||
// Model are only used when using the gemini original protocol
|
||||
Model string `json:"model,omitempty"`
|
||||
Requests []geminiEmbeddingRequest `json:"requests"`
|
||||
}
|
||||
|
||||
type geminiEmbeddingRequest struct {
|
||||
Model string `json:"model"`
|
||||
Content geminiChatContent `json:"content"`
|
||||
TaskType string `json:"taskType,omitempty"`
|
||||
Title string `json:"title,omitempty"`
|
||||
OutputDimensionality int `json:"outputDimensionality,omitempty"`
|
||||
}
|
||||
|
||||
func (g *geminiProvider) buildBatchEmbeddingRequest(request *embeddingsRequest) *geminiBatchEmbeddingRequest {
|
||||
inputs := request.ParseInput()
|
||||
requests := make([]geminiEmbeddingRequest, len(inputs))
|
||||
model := fmt.Sprintf("models/%s", request.Model)
|
||||
|
||||
for i, input := range inputs {
|
||||
requests[i] = geminiEmbeddingRequest{
|
||||
Model: model,
|
||||
Content: geminiChatContent{
|
||||
Parts: []geminiPart{
|
||||
{
|
||||
Text: input,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return &geminiBatchEmbeddingRequest{
|
||||
Requests: requests,
|
||||
}
|
||||
}
|
||||
|
||||
type geminiChatResponse struct {
|
||||
Candidates []geminiChatCandidate `json:"candidates"`
|
||||
PromptFeedback geminiChatPromptFeedback `json:"promptFeedback"`
|
||||
UsageMetadata geminiUsageMetadata `json:"usageMetadata"`
|
||||
Error *geminiResponseError `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type geminiChatCandidate struct {
|
||||
Content geminiChatContent `json:"content"`
|
||||
FinishReason string `json:"finishReason"`
|
||||
Index int64 `json:"index"`
|
||||
SafetyRatings []geminiChatSafetyRating `json:"safetyRatings"`
|
||||
}
|
||||
|
||||
type geminiChatPromptFeedback struct {
|
||||
SafetyRatings []geminiChatSafetyRating `json:"safetyRatings"`
|
||||
}
|
||||
|
||||
type geminiUsageMetadata struct {
|
||||
PromptTokenCount int `json:"promptTokenCount,omitempty"`
|
||||
CandidatesTokenCount int `json:"candidatesTokenCount,omitempty"`
|
||||
TotalTokenCount int `json:"totalTokenCount,omitempty"`
|
||||
}
|
||||
|
||||
type geminiResponseError struct {
|
||||
Code int `json:"code,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
type geminiChatSafetyRating struct {
|
||||
Category string `json:"category"`
|
||||
Probability string `json:"probability"`
|
||||
}
|
||||
|
||||
func (g *geminiProvider) buildChatCompletionResponse(ctx wrapper.HttpContext, response *geminiChatResponse) *chatCompletionResponse {
|
||||
fullTextResponse := chatCompletionResponse{
|
||||
Id: fmt.Sprintf("chatcmpl-%s", uuid.New().String()),
|
||||
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
|
||||
}
|
||||
} else {
|
||||
choice.Message.Content = ""
|
||||
choice.FinishReason = candidate.FinishReason
|
||||
}
|
||||
fullTextResponse.Choices = append(fullTextResponse.Choices, choice)
|
||||
}
|
||||
return &fullTextResponse
|
||||
}
|
||||
|
||||
func (g *geminiProvider) buildToolCalls(candidate *geminiChatCandidate) []toolCall {
|
||||
var toolCalls []toolCall
|
||||
|
||||
item := candidate.Content.Parts[0]
|
||||
if item.FunctionCall != nil {
|
||||
return toolCalls
|
||||
}
|
||||
argsBytes, err := json.Marshal(item.FunctionCall.Arguments)
|
||||
if err != nil {
|
||||
proxywasm.LogErrorf("get toolCalls from gemini response failed: " + err.Error())
|
||||
return toolCalls
|
||||
}
|
||||
toolCall := toolCall{
|
||||
Id: fmt.Sprintf("call_%s", uuid.New().String()),
|
||||
Type: "function",
|
||||
Function: functionCall{
|
||||
Arguments: string(argsBytes),
|
||||
Name: item.FunctionCall.FunctionName,
|
||||
},
|
||||
}
|
||||
toolCalls = append(toolCalls, toolCall)
|
||||
return toolCalls
|
||||
}
|
||||
|
||||
func (g *geminiProvider) buildChatCompletionStreamResponse(ctx wrapper.HttpContext, geminiResp *geminiChatResponse) *chatCompletionResponse {
|
||||
var choice chatCompletionChoice
|
||||
if len(geminiResp.Candidates) > 0 && len(geminiResp.Candidates[0].Content.Parts) > 0 {
|
||||
choice.Delta = &chatMessage{Content: geminiResp.Candidates[0].Content.Parts[0].Text}
|
||||
}
|
||||
streamResponse := chatCompletionResponse{
|
||||
Id: fmt.Sprintf("chatcmpl-%s", uuid.New().String()),
|
||||
Object: objectChatCompletionChunk,
|
||||
Created: time.Now().UnixMilli() / 1000,
|
||||
Model: ctx.GetStringContext(ctxKeyFinalRequestModel, ""),
|
||||
Choices: []chatCompletionChoice{choice},
|
||||
Usage: usage{
|
||||
PromptTokens: geminiResp.UsageMetadata.PromptTokenCount,
|
||||
CompletionTokens: geminiResp.UsageMetadata.CandidatesTokenCount,
|
||||
TotalTokens: geminiResp.UsageMetadata.TotalTokenCount,
|
||||
},
|
||||
}
|
||||
return &streamResponse
|
||||
}
|
||||
|
||||
type geminiEmbeddingResponse struct {
|
||||
Embeddings []geminiEmbeddingData `json:"embeddings"`
|
||||
Error *geminiResponseError `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type geminiEmbeddingData struct {
|
||||
Values []float64 `json:"values"`
|
||||
}
|
||||
|
||||
func (g *geminiProvider) buildEmbeddingsResponse(ctx wrapper.HttpContext, geminiResp *geminiEmbeddingResponse) *embeddingsResponse {
|
||||
response := embeddingsResponse{
|
||||
Object: "list",
|
||||
Data: make([]embedding, 0, len(geminiResp.Embeddings)),
|
||||
Model: ctx.GetContext(ctxKeyFinalRequestModel).(string),
|
||||
Usage: usage{
|
||||
TotalTokens: 0,
|
||||
},
|
||||
}
|
||||
for _, item := range geminiResp.Embeddings {
|
||||
response.Data = append(response.Data, embedding{
|
||||
Object: `embedding`,
|
||||
Index: 0,
|
||||
Embedding: item.Values,
|
||||
})
|
||||
}
|
||||
return &response
|
||||
}
|
||||
|
||||
func (g *geminiProvider) appendResponse(responseBuilder *strings.Builder, responseBody string) {
|
||||
responseBuilder.WriteString(fmt.Sprintf("%s %s\n\n", streamDataItemKey, responseBody))
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/extensions/ai-proxy/util"
|
||||
@@ -18,6 +19,9 @@ const (
|
||||
type groqProviderInitializer struct{}
|
||||
|
||||
func (m *groqProviderInitializer) ValidateConfig(config ProviderConfig) error {
|
||||
if config.apiTokens == nil || len(config.apiTokens) == 0 {
|
||||
return errors.New("no apiToken found in provider config")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -52,6 +52,9 @@ func (m *minimaxProviderInitializer) ValidateConfig(config ProviderConfig) error
|
||||
}
|
||||
}
|
||||
}
|
||||
if config.apiTokens == nil || len(config.apiTokens) == 0 {
|
||||
return errors.New("no apiToken found in provider config")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -161,3 +161,22 @@ type embedding struct {
|
||||
Index int `json:"index"`
|
||||
Embedding []float64 `json:"embedding"`
|
||||
}
|
||||
|
||||
func (r embeddingsRequest) ParseInput() []string {
|
||||
if r.Input == nil {
|
||||
return nil
|
||||
}
|
||||
var input []string
|
||||
switch r.Input.(type) {
|
||||
case string:
|
||||
input = []string{r.Input.(string)}
|
||||
case []any:
|
||||
input = make([]string, 0, len(r.Input.([]any)))
|
||||
for _, item := range r.Input.([]any) {
|
||||
if str, ok := item.(string); ok {
|
||||
input = append(input, str)
|
||||
}
|
||||
}
|
||||
}
|
||||
return input
|
||||
}
|
||||
|
||||
@@ -26,6 +26,9 @@ func (m *moonshotProviderInitializer) ValidateConfig(config ProviderConfig) erro
|
||||
if config.moonshotFileId != "" && config.context != nil {
|
||||
return errors.New("moonshotFileId and context cannot be configured at the same time")
|
||||
}
|
||||
if config.apiTokens == nil || len(config.apiTokens) == 0 {
|
||||
return errors.New("no apiToken found in provider config")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package provider
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/extensions/ai-proxy/util"
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
@@ -12,9 +13,9 @@ import (
|
||||
// openaiProvider is the provider for OpenAI service.
|
||||
|
||||
const (
|
||||
openaiDomain = "api.openai.com"
|
||||
openaiChatCompletionPath = "/v1/chat/completions"
|
||||
openaiEmbeddingsPath = "/v1/chat/embeddings"
|
||||
defaultOpenaiDomain = "api.openai.com"
|
||||
defaultOpenaiChatCompletionPath = "/v1/chat/completions"
|
||||
defaultOpenaiEmbeddingsPath = "/v1/chat/embeddings"
|
||||
)
|
||||
|
||||
type openaiProviderInitializer struct {
|
||||
@@ -25,14 +26,29 @@ func (m *openaiProviderInitializer) ValidateConfig(config ProviderConfig) error
|
||||
}
|
||||
|
||||
func (m *openaiProviderInitializer) CreateProvider(config ProviderConfig) (Provider, error) {
|
||||
if config.openaiCustomUrl == "" {
|
||||
return &openaiProvider{
|
||||
config: config,
|
||||
contextCache: createContextCache(&config),
|
||||
}, nil
|
||||
}
|
||||
customUrl := strings.TrimPrefix(strings.TrimPrefix(config.openaiCustomUrl, "http://"), "https://")
|
||||
pairs := strings.SplitN(customUrl, "/", 2)
|
||||
if len(pairs) != 2 {
|
||||
return nil, fmt.Errorf("invalid openaiCustomUrl:%s", config.openaiCustomUrl)
|
||||
}
|
||||
return &openaiProvider{
|
||||
config: config,
|
||||
customDomain: pairs[0],
|
||||
customPath: "/" + pairs[1],
|
||||
contextCache: createContextCache(&config),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type openaiProvider struct {
|
||||
config ProviderConfig
|
||||
customDomain string
|
||||
customPath string
|
||||
contextCache *contextCache
|
||||
}
|
||||
|
||||
@@ -41,15 +57,25 @@ func (m *openaiProvider) GetProviderType() string {
|
||||
}
|
||||
|
||||
func (m *openaiProvider) OnRequestHeaders(ctx wrapper.HttpContext, apiName ApiName, log wrapper.Log) (types.Action, error) {
|
||||
switch apiName {
|
||||
case ApiNameChatCompletion:
|
||||
_ = util.OverwriteRequestPath(openaiChatCompletionPath)
|
||||
break
|
||||
case ApiNameEmbeddings:
|
||||
_ = util.OverwriteRequestPath(openaiEmbeddingsPath)
|
||||
break
|
||||
if m.customPath == "" {
|
||||
switch apiName {
|
||||
case ApiNameChatCompletion:
|
||||
_ = util.OverwriteRequestPath(defaultOpenaiChatCompletionPath)
|
||||
case ApiNameEmbeddings:
|
||||
ctx.DontReadRequestBody()
|
||||
_ = util.OverwriteRequestPath(defaultOpenaiEmbeddingsPath)
|
||||
}
|
||||
} else {
|
||||
_ = util.OverwriteRequestPath(m.customPath)
|
||||
}
|
||||
if m.customDomain == "" {
|
||||
_ = util.OverwriteRequestHost(defaultOpenaiDomain)
|
||||
} else {
|
||||
_ = util.OverwriteRequestHost(m.customDomain)
|
||||
}
|
||||
if len(m.config.apiTokens) > 0 {
|
||||
_ = util.OverwriteRequestAuthorization("Bearer " + m.config.GetRandomToken())
|
||||
}
|
||||
_ = util.OverwriteRequestAuthorization("Bearer " + m.config.GetRandomToken())
|
||||
_ = proxywasm.RemoveHttpRequestHeader("Content-Length")
|
||||
return types.ActionContinue, nil
|
||||
}
|
||||
@@ -63,26 +89,19 @@ func (m *openaiProvider) OnRequestBody(ctx wrapper.HttpContext, apiName ApiName,
|
||||
if err := decodeChatCompletionRequest(body, request); err != nil {
|
||||
return types.ActionContinue, err
|
||||
}
|
||||
bodyAltered := false
|
||||
if request.Stream {
|
||||
// For stream requests, we need to include usage in the response.
|
||||
if request.StreamOptions == nil {
|
||||
request.StreamOptions = &streamOptions{IncludeUsage: true}
|
||||
bodyAltered = true
|
||||
} else if !request.StreamOptions.IncludeUsage {
|
||||
request.StreamOptions.IncludeUsage = true
|
||||
bodyAltered = true
|
||||
}
|
||||
}
|
||||
if m.contextCache == nil {
|
||||
if bodyAltered {
|
||||
if err := replaceJsonRequestBody(request, log); err != nil {
|
||||
_ = util.SendResponse(500, "ai-proxy.openai.set_include_usage_failed", util.MimeTypeTextPlain, fmt.Sprintf("failed to replace request body: %v", err))
|
||||
}
|
||||
if err := replaceJsonRequestBody(request, log); err != nil {
|
||||
_ = util.SendResponse(500, "ai-proxy.openai.set_include_usage_failed", util.MimeTypeTextPlain, fmt.Sprintf("failed to replace request body: %v", err))
|
||||
}
|
||||
return types.ActionContinue, nil
|
||||
} else {
|
||||
// If context cache is configured and body has been altered, the new body will be replaced when inserting the context data.
|
||||
}
|
||||
err := m.contextCache.GetContent(func(content string, err error) {
|
||||
defer func() {
|
||||
|
||||
@@ -33,6 +33,8 @@ const (
|
||||
providerTypeStepfun = "stepfun"
|
||||
providerTypeMinimax = "minimax"
|
||||
providerTypeCloudflare = "cloudflare"
|
||||
providerTypeSpark = "spark"
|
||||
providerTypeGemini = "gemini"
|
||||
|
||||
protocolOpenAI = "openai"
|
||||
protocolOriginal = "original"
|
||||
@@ -84,6 +86,8 @@ var (
|
||||
providerTypeStepfun: &stepfunProviderInitializer{},
|
||||
providerTypeMinimax: &minimaxProviderInitializer{},
|
||||
providerTypeCloudflare: &cloudflareProviderInitializer{},
|
||||
providerTypeSpark: &sparkProviderInitializer{},
|
||||
providerTypeGemini: &geminiProviderInitializer{},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -121,6 +125,9 @@ type ProviderConfig struct {
|
||||
// @Title zh-CN 请求超时
|
||||
// @Description zh-CN 请求AI服务的超时时间,单位为毫秒。默认值为120000,即2分钟
|
||||
timeout uint32 `required:"false" yaml:"timeout" json:"timeout"`
|
||||
// @Title zh-CN 基于OpenAI协议的自定义后端URL
|
||||
// @Description zh-CN 仅适用于支持 openai 协议的服务。
|
||||
openaiCustomUrl string `required:"false" yaml:"openaiCustomUrl" json:"openaiCustomUrl"`
|
||||
// @Title zh-CN Moonshot File ID
|
||||
// @Description zh-CN 仅适用于Moonshot AI服务。Moonshot AI服务的文件ID,其内容用于补充AI请求上下文
|
||||
moonshotFileId string `required:"false" yaml:"moonshotFileId" json:"moonshotFileId"`
|
||||
@@ -163,6 +170,9 @@ type ProviderConfig struct {
|
||||
// @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"`
|
||||
}
|
||||
|
||||
func (c *ProviderConfig) FromJson(json gjson.Result) {
|
||||
@@ -175,6 +185,7 @@ func (c *ProviderConfig) FromJson(json gjson.Result) {
|
||||
if c.timeout == 0 {
|
||||
c.timeout = defaultTimeout
|
||||
}
|
||||
c.openaiCustomUrl = json.Get("openaiCustomUrl").String()
|
||||
c.moonshotFileId = json.Get("moonshotFileId").String()
|
||||
c.azureServiceUrl = json.Get("azureServiceUrl").String()
|
||||
c.qwenFileIds = make([]string, 0)
|
||||
@@ -202,12 +213,15 @@ func (c *ProviderConfig) FromJson(json gjson.Result) {
|
||||
c.hunyuanAuthKey = json.Get("hunyuanAuthKey").String()
|
||||
c.minimaxGroupId = json.Get("minimaxGroupId").String()
|
||||
c.cloudflareAccountId = json.Get("cloudflareAccountId").String()
|
||||
if c.typ == providerTypeGemini {
|
||||
c.geminiSafetySetting = make(map[string]string)
|
||||
for k, v := range json.Get("geminiSafetySetting").Map() {
|
||||
c.geminiSafetySetting[k] = v.String()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *ProviderConfig) Validate() error {
|
||||
if c.apiTokens == nil || len(c.apiTokens) == 0 {
|
||||
return errors.New("no apiToken found in provider config")
|
||||
}
|
||||
if c.timeout < 0 {
|
||||
return errors.New("invalid timeout in config")
|
||||
}
|
||||
|
||||
@@ -39,6 +39,9 @@ func (m *qwenProviderInitializer) ValidateConfig(config ProviderConfig) error {
|
||||
if len(config.qwenFileIds) != 0 && config.context != nil {
|
||||
return errors.New("qwenFileIds and context cannot be configured at the same time")
|
||||
}
|
||||
if config.apiTokens == nil || len(config.apiTokens) == 0 {
|
||||
return errors.New("no apiToken found in provider config")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ package provider
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
@@ -14,7 +13,7 @@ func decodeChatCompletionRequest(body []byte, request *chatCompletionRequest) er
|
||||
return fmt.Errorf("unable to unmarshal request: %v", err)
|
||||
}
|
||||
if request.Messages == nil || len(request.Messages) == 0 {
|
||||
return errors.New("no message found in the request body")
|
||||
return fmt.Errorf("no message found in the request body: %s", body)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
207
plugins/wasm-go/extensions/ai-proxy/provider/spark.go
Normal file
207
plugins/wasm-go/extensions/ai-proxy/provider/spark.go
Normal file
@@ -0,0 +1,207 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/extensions/ai-proxy/util"
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
)
|
||||
|
||||
// sparkProvider is the provider for SparkLLM AI service.
|
||||
const (
|
||||
sparkHost = "spark-api-open.xf-yun.com"
|
||||
sparkChatCompletionPath = "/v1/chat/completions"
|
||||
)
|
||||
|
||||
type sparkProviderInitializer struct {
|
||||
}
|
||||
|
||||
type sparkProvider struct {
|
||||
config ProviderConfig
|
||||
contextCache *contextCache
|
||||
}
|
||||
|
||||
type sparkRequest struct {
|
||||
Model string `json:"model"`
|
||||
Messages []chatMessage `json:"messages"`
|
||||
MaxTokens int `json:"max_tokens,omitempty"`
|
||||
TopK int `json:"top_k,omitempty"`
|
||||
Stream bool `json:"stream,omitempty"`
|
||||
Temperature float64 `json:"temperature,omitempty"`
|
||||
Tools []tool `json:"tools,omitempty"`
|
||||
ToolChoice string `json:"tool_choice,omitempty"`
|
||||
}
|
||||
|
||||
type sparkResponse struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Sid string `json:"sid"`
|
||||
Choices []chatCompletionChoice `json:"choices"`
|
||||
Usage usage `json:"usage,omitempty"`
|
||||
}
|
||||
|
||||
type sparkStreamResponse struct {
|
||||
sparkResponse
|
||||
Id string `json:"id"`
|
||||
Created int64 `json:"created"`
|
||||
}
|
||||
|
||||
func (i *sparkProviderInitializer) ValidateConfig(config ProviderConfig) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *sparkProviderInitializer) CreateProvider(config ProviderConfig) (Provider, error) {
|
||||
return &sparkProvider{
|
||||
config: config,
|
||||
contextCache: createContextCache(&config),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *sparkProvider) GetProviderType() string {
|
||||
return providerTypeSpark
|
||||
}
|
||||
|
||||
func (p *sparkProvider) OnRequestHeaders(ctx wrapper.HttpContext, apiName ApiName, log wrapper.Log) (types.Action, error) {
|
||||
if apiName != ApiNameChatCompletion {
|
||||
return types.ActionContinue, errUnsupportedApiName
|
||||
}
|
||||
_ = util.OverwriteRequestHost(sparkHost)
|
||||
_ = util.OverwriteRequestPath(sparkChatCompletionPath)
|
||||
_ = util.OverwriteRequestAuthorization("Bearer " + p.config.GetRandomToken())
|
||||
_ = proxywasm.RemoveHttpRequestHeader("Accept-Encoding")
|
||||
_ = proxywasm.RemoveHttpRequestHeader("Content-Length")
|
||||
return types.ActionContinue, nil
|
||||
}
|
||||
|
||||
func (p *sparkProvider) OnRequestBody(ctx wrapper.HttpContext, apiName ApiName, body []byte, log wrapper.Log) (types.Action, error) {
|
||||
if apiName != ApiNameChatCompletion {
|
||||
return types.ActionContinue, errUnsupportedApiName
|
||||
}
|
||||
// 使用Spark协议
|
||||
if p.config.protocol == protocolOriginal {
|
||||
request := &sparkRequest{}
|
||||
if err := json.Unmarshal(body, request); err != nil {
|
||||
return types.ActionContinue, fmt.Errorf("unable to unmarshal request: %v", err)
|
||||
}
|
||||
if request.Model == "" {
|
||||
return types.ActionContinue, errors.New("request model is empty")
|
||||
}
|
||||
// 目前星火在模型名称错误时,也会调用generalv3,这里还是按照输入的模型名称设置响应里的模型名称
|
||||
ctx.SetContext(ctxKeyFinalRequestModel, request.Model)
|
||||
return types.ActionContinue, replaceJsonRequestBody(request, log)
|
||||
} else {
|
||||
// 使用openai协议
|
||||
request := &chatCompletionRequest{}
|
||||
if err := decodeChatCompletionRequest(body, request); err != nil {
|
||||
return types.ActionContinue, err
|
||||
}
|
||||
if request.Model == "" {
|
||||
return types.ActionContinue, errors.New("missing model in chat completion request")
|
||||
}
|
||||
// 映射模型
|
||||
mappedModel := getMappedModel(request.Model, p.config.modelMapping, log)
|
||||
if mappedModel == "" {
|
||||
return types.ActionContinue, errors.New("model becomes empty after applying the configured mapping")
|
||||
}
|
||||
ctx.SetContext(ctxKeyFinalRequestModel, mappedModel)
|
||||
request.Model = mappedModel
|
||||
return types.ActionContinue, replaceJsonRequestBody(request, log)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *sparkProvider) OnResponseHeaders(ctx wrapper.HttpContext, apiName ApiName, log wrapper.Log) (types.Action, error) {
|
||||
_ = proxywasm.RemoveHttpResponseHeader("Content-Length")
|
||||
return types.ActionContinue, nil
|
||||
}
|
||||
|
||||
func (p *sparkProvider) OnResponseBody(ctx wrapper.HttpContext, apiName ApiName, body []byte, log wrapper.Log) (types.Action, error) {
|
||||
sparkResponse := &sparkResponse{}
|
||||
if err := json.Unmarshal(body, sparkResponse); err != nil {
|
||||
return types.ActionContinue, fmt.Errorf("unable to unmarshal spark response: %v", err)
|
||||
}
|
||||
if sparkResponse.Code != 0 {
|
||||
return types.ActionContinue, fmt.Errorf("spark response error, error_code: %d, error_message: %s", sparkResponse.Code, sparkResponse.Message)
|
||||
}
|
||||
response := p.responseSpark2OpenAI(ctx, sparkResponse)
|
||||
return types.ActionContinue, replaceJsonResponseBody(response, log)
|
||||
}
|
||||
|
||||
func (p *sparkProvider) OnStreamingResponseBody(ctx wrapper.HttpContext, name ApiName, chunk []byte, isLastChunk bool, log wrapper.Log) ([]byte, error) {
|
||||
if isLastChunk || len(chunk) == 0 {
|
||||
return nil, 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:]
|
||||
// The final response is `data: [DONE]`
|
||||
if data == "[DONE]" {
|
||||
continue
|
||||
}
|
||||
var sparkResponse sparkStreamResponse
|
||||
if err := json.Unmarshal([]byte(data), &sparkResponse); err != nil {
|
||||
log.Errorf("unable to unmarshal spark response: %v", err)
|
||||
continue
|
||||
}
|
||||
response := p.streamResponseSpark2OpenAI(ctx, &sparkResponse)
|
||||
responseBody, err := json.Marshal(response)
|
||||
if err != nil {
|
||||
log.Errorf("unable to marshal response: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
p.appendResponse(responseBuilder, string(responseBody))
|
||||
}
|
||||
modifiedResponseChunk := responseBuilder.String()
|
||||
log.Debugf("=== modified response chunk: %s", modifiedResponseChunk)
|
||||
return []byte(modifiedResponseChunk), nil
|
||||
}
|
||||
|
||||
func (p *sparkProvider) responseSpark2OpenAI(ctx wrapper.HttpContext, response *sparkResponse) *chatCompletionResponse {
|
||||
choices := make([]chatCompletionChoice, len(response.Choices))
|
||||
for idx, c := range response.Choices {
|
||||
choices[idx] = chatCompletionChoice{
|
||||
Index: c.Index,
|
||||
Message: &chatMessage{Role: c.Message.Role, Content: c.Message.Content},
|
||||
}
|
||||
}
|
||||
return &chatCompletionResponse{
|
||||
Id: response.Sid,
|
||||
Created: time.Now().UnixMilli() / 1000,
|
||||
Object: objectChatCompletion,
|
||||
Model: ctx.GetStringContext(ctxKeyFinalRequestModel, ""),
|
||||
Choices: choices,
|
||||
Usage: response.Usage,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *sparkProvider) streamResponseSpark2OpenAI(ctx wrapper.HttpContext, response *sparkStreamResponse) *chatCompletionResponse {
|
||||
choices := make([]chatCompletionChoice, len(response.Choices))
|
||||
for idx, c := range response.Choices {
|
||||
choices[idx] = chatCompletionChoice{
|
||||
Index: c.Index,
|
||||
Delta: &chatMessage{Role: c.Delta.Role, Content: c.Delta.Content},
|
||||
}
|
||||
}
|
||||
return &chatCompletionResponse{
|
||||
Id: response.Sid,
|
||||
Created: response.Created,
|
||||
Model: ctx.GetStringContext(ctxKeyFinalRequestModel, ""),
|
||||
Object: objectChatCompletion,
|
||||
Choices: choices,
|
||||
Usage: response.Usage,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *sparkProvider) appendResponse(responseBuilder *strings.Builder, responseBody string) {
|
||||
responseBuilder.WriteString(fmt.Sprintf("%s %s\n\n", streamDataItemKey, responseBody))
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/extensions/ai-proxy/util"
|
||||
@@ -18,6 +19,9 @@ type stepfunProviderInitializer struct {
|
||||
}
|
||||
|
||||
func (m *stepfunProviderInitializer) ValidateConfig(config ProviderConfig) error {
|
||||
if config.apiTokens == nil || len(config.apiTokens) == 0 {
|
||||
return errors.New("no apiToken found in provider config")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/extensions/ai-proxy/util"
|
||||
@@ -18,6 +19,9 @@ type yiProviderInitializer struct {
|
||||
}
|
||||
|
||||
func (m *yiProviderInitializer) ValidateConfig(config ProviderConfig) error {
|
||||
if config.apiTokens == nil || len(config.apiTokens) == 0 {
|
||||
return errors.New("no apiToken found in provider config")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/extensions/ai-proxy/util"
|
||||
@@ -17,6 +18,9 @@ const (
|
||||
type zhipuAiProviderInitializer struct{}
|
||||
|
||||
func (m *zhipuAiProviderInitializer) ValidateConfig(config ProviderConfig) error {
|
||||
if config.apiTokens == nil || len(config.apiTokens) == 0 {
|
||||
return errors.New("no apiToken found in provider config")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1,34 +1,42 @@
|
||||
# 简介
|
||||
通过对接阿里云向量检索服务实现LLM-RAG,流程如图所示:
|
||||
|
||||

|
||||
<img src="https://img.alicdn.com/imgextra/i1/O1CN01LuRVs41KhoeuzakeF_!!6000000001196-0-tps-1926-1316.jpg" width=600>
|
||||
|
||||
# 配置说明
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
|----------------|-----------------|------|-----|----------------------------------------------------------------------------------|
|
||||
| `dashscope.apiKey` | string | 必填 | - | 用于在访问通义千问服务时进行认证的令牌。 |
|
||||
| `dashscope.serviceName` | string | 必填 | - | 通义千问服务名 |
|
||||
| `dashscope.serviceFQDN` | string | 必填 | - | 通义千问服务名 |
|
||||
| `dashscope.servicePort` | int | 必填 | - | 通义千问服务端口 |
|
||||
| `dashscope.domain` | string | 必填 | - | 访问通义千问服务时域名 |
|
||||
| `dashscope.serviceHost` | string | 必填 | - | 访问通义千问服务时域名 |
|
||||
| `dashvector.apiKey` | string | 必填 | - | 用于在访问阿里云向量检索服务时进行认证的令牌。 |
|
||||
| `dashvector.serviceName` | string | 必填 | - | 阿里云向量检索服务名 |
|
||||
| `dashvector.serviceFQDN` | string | 必填 | - | 阿里云向量检索服务名 |
|
||||
| `dashvector.servicePort` | int | 必填 | - | 阿里云向量检索服务端口 |
|
||||
| `dashvector.domain` | string | 必填 | - | 访问阿里云向量检索服务时域名 |
|
||||
| `dashvector.serviceHost` | string | 必填 | - | 访问阿里云向量检索服务时域名 |
|
||||
| `dashvector.topk` | int | 必填 | - | 阿里云向量检索时获取向量数 |
|
||||
| `dashvector.threshold` | float | 必填 | - | 向量距离阈值,高于该阈值的文档会被过滤掉 |
|
||||
| `dashvector.field` | string | 必填 | - | 阿里云向量检索存储文档的字段名 |
|
||||
|
||||
插件开启后,在使用链路追踪功能时,会在span的attribute中添加rag检索到的文档id信息,供排查问题使用。
|
||||
|
||||
# 示例
|
||||
|
||||
```yaml
|
||||
dashscope:
|
||||
apiKey: xxxxxxxxxxxxxxx
|
||||
serviceName: dashscope
|
||||
serviceFQDN: dashscope
|
||||
servicePort: 443
|
||||
domain: dashscope.aliyuncs.com
|
||||
serviceHost: dashscope.aliyuncs.com
|
||||
dashvector:
|
||||
apiKey: xxxxxxxxxxxxxxxxxxxx
|
||||
serviceName: dashvector
|
||||
serviceFQDN: dashvector
|
||||
servicePort: 443
|
||||
domain: vrs-cn-xxxxxxxxxxxxxxx.dashvector.cn-hangzhou.aliyuncs.com
|
||||
serviceHost: vrs-cn-xxxxxxxxxxxxxxx.dashvector.cn-hangzhou.aliyuncs.com
|
||||
collection: xxxxxxxxxxxxxxx
|
||||
topk: 1
|
||||
threshold: 0.4
|
||||
field: raw
|
||||
```
|
||||
|
||||
[CEC-Corpus](https://github.com/shijiebei2009/CEC-Corpus) 数据集包含 332 篇突发事件的新闻报道的语料和标注数据,提取其原始的新闻稿文本,将其向量化后添加到阿里云向量检索服务。文本向量化的教程可以参考[《基于向量检索服务与灵积实现语义搜索》](https://help.aliyun.com/document_detail/2510234.html)。
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
github.com/alibaba/higress/plugins/wasm-go v1.3.5 h1:VOLL3m442IHCSu8mR5AZ4sc6LVT9X0w1hdqDI7oB9jY=
|
||||
github.com/alibaba/higress/plugins/wasm-go v1.3.5/go.mod h1:kr3V9Ntbspj1eSrX8rgjBsdMXkGupYEf+LM72caGPQc=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 h1:IHDghbGQ2DTIXHBHxWfqCYQW1fKjyJ/I7W1pMyUDeEA=
|
||||
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520/go.mod h1:Nz8ORLaFiLWotg6GeKlJMhv8cci8mM43uEnLA5t8iew=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240226064518-b3dc4646a35a h1:luYRvxLTE1xYxrXYj7nmjd1U0HHh8pUPiKfdZ0MhCGE=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240226064518-b3dc4646a35a/go.mod h1:hNFjhrLUIq+kJ9bOcs8QtiplSQ61GZXtd2xHKx4BYRo=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240711023527-ba358c48772f h1:ZIiIBRvIw62gA5MJhuwp1+2wWbqL9IGElQ499rUsYYg=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240711023527-ba358c48772f/go.mod h1:hNFjhrLUIq+kJ9bOcs8QtiplSQ61GZXtd2xHKx4BYRo=
|
||||
github.com/magefile/mage v1.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo=
|
||||
github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"ai-rag/dashscope"
|
||||
"ai-rag/dashvector"
|
||||
@@ -20,6 +21,7 @@ func main() {
|
||||
wrapper.ParseConfigBy(parseConfig),
|
||||
wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
|
||||
wrapper.ProcessRequestBodyBy(onHttpRequestBody),
|
||||
wrapper.ProcessResponseHeadersBy(onHttpResponseHeaders),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -29,6 +31,9 @@ type AIRagConfig struct {
|
||||
DashVectorClient wrapper.HttpClient
|
||||
DashVectorAPIKey string
|
||||
DashVectorCollection string
|
||||
DashVectorTopK int32
|
||||
DashVectorThreshold float64
|
||||
DashVectorField string
|
||||
}
|
||||
|
||||
type Request struct {
|
||||
@@ -47,29 +52,46 @@ type Message struct {
|
||||
}
|
||||
|
||||
func parseConfig(json gjson.Result, config *AIRagConfig, log wrapper.Log) error {
|
||||
checkList := []string{
|
||||
"dashscope.apiKey",
|
||||
"dashscope.serviceFQDN",
|
||||
"dashscope.servicePort",
|
||||
"dashscope.serviceHost",
|
||||
"dashvector.apiKey",
|
||||
"dashvector.collection",
|
||||
"dashvector.serviceFQDN",
|
||||
"dashvector.servicePort",
|
||||
"dashvector.serviceHost",
|
||||
"dashvector.topk",
|
||||
"dashvector.threshold",
|
||||
"dashvector.field",
|
||||
}
|
||||
for _, checkEntry := range checkList {
|
||||
if !json.Get(checkEntry).Exists() {
|
||||
return fmt.Errorf("%s not found in plugin config!", checkEntry)
|
||||
}
|
||||
}
|
||||
config.DashScopeAPIKey = json.Get("dashscope.apiKey").String()
|
||||
|
||||
config.DashScopeClient = wrapper.NewClusterClient(wrapper.DnsCluster{
|
||||
ServiceName: json.Get("dashscope.serviceName").String(),
|
||||
Port: json.Get("dashscope.servicePort").Int(),
|
||||
Domain: json.Get("dashscope.domain").String(),
|
||||
config.DashScopeClient = wrapper.NewClusterClient(wrapper.FQDNCluster{
|
||||
FQDN: json.Get("dashscope.serviceFQDN").String(),
|
||||
Port: json.Get("dashscope.servicePort").Int(),
|
||||
Host: json.Get("dashscope.serviceHost").String(),
|
||||
})
|
||||
config.DashVectorAPIKey = json.Get("dashvector.apiKey").String()
|
||||
config.DashVectorCollection = json.Get("dashvector.collection").String()
|
||||
config.DashVectorClient = wrapper.NewClusterClient(wrapper.DnsCluster{
|
||||
ServiceName: json.Get("dashvector.serviceName").String(),
|
||||
Port: json.Get("dashvector.servicePort").Int(),
|
||||
Domain: json.Get("dashvector.domain").String(),
|
||||
config.DashVectorClient = wrapper.NewClusterClient(wrapper.FQDNCluster{
|
||||
FQDN: json.Get("dashvector.serviceFQDN").String(),
|
||||
Port: json.Get("dashvector.servicePort").Int(),
|
||||
Host: json.Get("dashvector.serviceHost").String(),
|
||||
})
|
||||
config.DashVectorTopK = int32(json.Get("dashvector.topk").Int())
|
||||
config.DashVectorThreshold = json.Get("dashvector.threshold").Float()
|
||||
config.DashVectorField = json.Get("dashvector.field").String()
|
||||
return nil
|
||||
}
|
||||
|
||||
func onHttpRequestHeaders(ctx wrapper.HttpContext, config AIRagConfig, log wrapper.Log) types.Action {
|
||||
p, _ := proxywasm.GetHttpRequestHeader(":path")
|
||||
if p != "/api/openai/v1/chat/completions" {
|
||||
ctx.DontReadRequestBody()
|
||||
return types.ActionContinue
|
||||
}
|
||||
proxywasm.RemoveHttpRequestHeader("content-length")
|
||||
return types.ActionContinue
|
||||
}
|
||||
@@ -78,9 +100,12 @@ func onHttpRequestBody(ctx wrapper.HttpContext, config AIRagConfig, body []byte,
|
||||
var rawRequest Request
|
||||
_ = json.Unmarshal(body, &rawRequest)
|
||||
messageLength := len(rawRequest.Messages)
|
||||
if messageLength == 0 {
|
||||
return types.ActionContinue
|
||||
}
|
||||
rawContent := rawRequest.Messages[messageLength-1].Content
|
||||
requestEmbedding := dashscope.Request{
|
||||
Model: "text-embedding-v1",
|
||||
Model: "text-embedding-v2",
|
||||
Input: dashscope.Input{
|
||||
Texts: []string{rawContent},
|
||||
},
|
||||
@@ -90,7 +115,6 @@ func onHttpRequestBody(ctx wrapper.HttpContext, config AIRagConfig, body []byte,
|
||||
}
|
||||
headers := [][2]string{{"Content-Type", "application/json"}, {"Authorization", "Bearer " + config.DashScopeAPIKey}}
|
||||
reqEmbeddingSerialized, _ := json.Marshal(requestEmbedding)
|
||||
// log.Info(string(reqEmbeddingSerialized))
|
||||
config.DashScopeClient.Post(
|
||||
"/api/v1/services/embeddings/text-embedding/text-embedding",
|
||||
headers,
|
||||
@@ -99,8 +123,8 @@ func onHttpRequestBody(ctx wrapper.HttpContext, config AIRagConfig, body []byte,
|
||||
var responseEmbedding dashscope.Response
|
||||
_ = json.Unmarshal(responseBody, &responseEmbedding)
|
||||
requestQuery := dashvector.Request{
|
||||
TopK: 1,
|
||||
OutputFileds: []string{"raw"},
|
||||
TopK: config.DashVectorTopK,
|
||||
OutputFileds: []string{config.DashVectorField},
|
||||
Vector: responseEmbedding.Output.Embeddings[0].Embedding,
|
||||
}
|
||||
requestQuerySerialized, _ := json.Marshal(requestQuery)
|
||||
@@ -111,11 +135,27 @@ func onHttpRequestBody(ctx wrapper.HttpContext, config AIRagConfig, body []byte,
|
||||
func(statusCode int, responseHeaders http.Header, responseBody []byte) {
|
||||
var response dashvector.Response
|
||||
_ = json.Unmarshal(responseBody, &response)
|
||||
doc := response.Output[0].Fields.Raw
|
||||
rawRequest.Messages[messageLength-1].Content = fmt.Sprintf("%s\n以上是一些可能有帮助的参考信息,你可以自行选择是否使用这些参考信息,现在请回答以下问题:\n%s", doc, rawContent)
|
||||
newBody, _ := json.Marshal(rawRequest)
|
||||
// log.Info(string(newBody))
|
||||
proxywasm.ReplaceHttpRequestBody(newBody)
|
||||
recallDocIds := []string{}
|
||||
recallDocs := []string{}
|
||||
for _, output := range response.Output {
|
||||
log.Debugf("Score: %f, Doc: %s", output.Score, output.Fields.Raw)
|
||||
if output.Score <= float32(config.DashVectorThreshold) {
|
||||
recallDocs = append(recallDocs, output.Fields.Raw)
|
||||
recallDocIds = append(recallDocIds, output.ID)
|
||||
}
|
||||
}
|
||||
if len(recallDocs) > 0 {
|
||||
rawRequest.Messages = rawRequest.Messages[:messageLength-1]
|
||||
traceStr := strings.Join(recallDocIds, ", ")
|
||||
proxywasm.SetProperty([]string{"trace_span_tag.rag_docs"}, []byte(traceStr))
|
||||
for _, doc := range recallDocs {
|
||||
rawRequest.Messages = append(rawRequest.Messages, Message{"user", doc})
|
||||
}
|
||||
rawRequest.Messages = append(rawRequest.Messages, Message{"user", fmt.Sprintf("现在,请回答以下问题:\n%s", rawContent)})
|
||||
newBody, _ := json.Marshal(rawRequest)
|
||||
proxywasm.ReplaceHttpRequestBody(newBody)
|
||||
ctx.SetContext("x-envoy-rag-recall", true)
|
||||
}
|
||||
proxywasm.ResumeHttpRequest()
|
||||
},
|
||||
)
|
||||
@@ -124,3 +164,13 @@ func onHttpRequestBody(ctx wrapper.HttpContext, config AIRagConfig, body []byte,
|
||||
)
|
||||
return types.ActionPause
|
||||
}
|
||||
|
||||
func onHttpResponseHeaders(ctx wrapper.HttpContext, config AIRagConfig, log wrapper.Log) types.Action {
|
||||
recall, ok := ctx.GetContext("x-envoy-rag-recall").(bool)
|
||||
if ok && recall {
|
||||
proxywasm.AddHttpResponseHeader("x-envoy-rag-recall", "true")
|
||||
} else {
|
||||
proxywasm.AddHttpResponseHeader("x-envoy-rag-recall", "false")
|
||||
}
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
@@ -18,8 +18,9 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
regexp "github.com/wasilibs/go-re2"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -9,16 +9,19 @@ require (
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240711023527-ba358c48772f
|
||||
github.com/stretchr/testify v1.8.4
|
||||
github.com/tidwall/gjson v1.14.4
|
||||
github.com/wasilibs/go-re2 v1.6.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 // indirect
|
||||
github.com/magefile/mage v1.14.0 // indirect
|
||||
github.com/magefile/mage v1.15.1-0.20230912152418-9f54e0f83e2a // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/tetratelabs/wazero v1.7.2 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.0 // indirect
|
||||
github.com/tidwall/resp v0.1.1 // indirect
|
||||
golang.org/x/sys v0.21.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -4,17 +4,16 @@ github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 h1:IHDghbGQ2DTIXHBHxWfqCYQW1fKjyJ/I7W1pMyUDeEA=
|
||||
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520/go.mod h1:Nz8ORLaFiLWotg6GeKlJMhv8cci8mM43uEnLA5t8iew=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240226064518-b3dc4646a35a h1:luYRvxLTE1xYxrXYj7nmjd1U0HHh8pUPiKfdZ0MhCGE=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240226064518-b3dc4646a35a/go.mod h1:hNFjhrLUIq+kJ9bOcs8QtiplSQ61GZXtd2xHKx4BYRo=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240318034951-d5306e367c43/go.mod h1:hNFjhrLUIq+kJ9bOcs8QtiplSQ61GZXtd2xHKx4BYRo=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240327114451-d6b7174a84fc/go.mod h1:hNFjhrLUIq+kJ9bOcs8QtiplSQ61GZXtd2xHKx4BYRo=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240711023527-ba358c48772f h1:ZIiIBRvIw62gA5MJhuwp1+2wWbqL9IGElQ499rUsYYg=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240711023527-ba358c48772f/go.mod h1:hNFjhrLUIq+kJ9bOcs8QtiplSQ61GZXtd2xHKx4BYRo=
|
||||
github.com/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=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/tetratelabs/wazero v1.7.2 h1:1+z5nXJNwMLPAWaTePFi49SSTL0IMx/i3Fg8Yc25GDc=
|
||||
github.com/tetratelabs/wazero v1.7.2/go.mod h1:ytl6Zuh20R/eROuyDaGPkp82O9C/DJfXAwJfQ3X6/7Y=
|
||||
github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM=
|
||||
github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
@@ -23,6 +22,11 @@ github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/resp v0.1.1 h1:Ly20wkhqKTmDUPlyM1S7pWo5kk0tDu8OoC/vFArXmwE=
|
||||
github.com/tidwall/resp v0.1.1/go.mod h1:3/FrruOBAxPTPtundW0VXgmsQ4ZBA0Aw714lVYgwFa0=
|
||||
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=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
|
||||
@@ -17,8 +17,9 @@ package config
|
||||
import (
|
||||
"errors"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
regexp "github.com/wasilibs/go-re2"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -9,16 +9,19 @@ require (
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240711023527-ba358c48772f
|
||||
github.com/stretchr/testify v1.8.4
|
||||
github.com/tidwall/gjson v1.14.4
|
||||
github.com/wasilibs/go-re2 v1.6.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 // indirect
|
||||
github.com/magefile/mage v1.14.0 // indirect
|
||||
github.com/magefile/mage v1.15.1-0.20230912152418-9f54e0f83e2a // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/tetratelabs/wazero v1.7.2 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.0 // indirect
|
||||
github.com/tidwall/resp v0.1.1 // indirect
|
||||
golang.org/x/sys v0.21.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -4,17 +4,16 @@ github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 h1:IHDghbGQ2DTIXHBHxWfqCYQW1fKjyJ/I7W1pMyUDeEA=
|
||||
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520/go.mod h1:Nz8ORLaFiLWotg6GeKlJMhv8cci8mM43uEnLA5t8iew=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240226064518-b3dc4646a35a h1:luYRvxLTE1xYxrXYj7nmjd1U0HHh8pUPiKfdZ0MhCGE=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240226064518-b3dc4646a35a/go.mod h1:hNFjhrLUIq+kJ9bOcs8QtiplSQ61GZXtd2xHKx4BYRo=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240318034951-d5306e367c43/go.mod h1:hNFjhrLUIq+kJ9bOcs8QtiplSQ61GZXtd2xHKx4BYRo=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240327114451-d6b7174a84fc/go.mod h1:hNFjhrLUIq+kJ9bOcs8QtiplSQ61GZXtd2xHKx4BYRo=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240711023527-ba358c48772f h1:ZIiIBRvIw62gA5MJhuwp1+2wWbqL9IGElQ499rUsYYg=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240711023527-ba358c48772f/go.mod h1:hNFjhrLUIq+kJ9bOcs8QtiplSQ61GZXtd2xHKx4BYRo=
|
||||
github.com/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=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/tetratelabs/wazero v1.7.2 h1:1+z5nXJNwMLPAWaTePFi49SSTL0IMx/i3Fg8Yc25GDc=
|
||||
github.com/tetratelabs/wazero v1.7.2/go.mod h1:ytl6Zuh20R/eROuyDaGPkp82O9C/DJfXAwJfQ3X6/7Y=
|
||||
github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM=
|
||||
github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
@@ -23,6 +22,11 @@ github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/resp v0.1.1 h1:Ly20wkhqKTmDUPlyM1S7pWo5kk0tDu8OoC/vFArXmwE=
|
||||
github.com/tidwall/resp v0.1.1/go.mod h1:3/FrruOBAxPTPtundW0VXgmsQ4ZBA0Aw714lVYgwFa0=
|
||||
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=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
|
||||
1
plugins/wasm-go/extensions/ext-auth/.buildrc
Normal file
1
plugins/wasm-go/extensions/ext-auth/.buildrc
Normal file
@@ -0,0 +1 @@
|
||||
EXTRA_TAGS=proxy_wasm_version_0_2_100
|
||||
@@ -1,10 +1,16 @@
|
||||
# 功能说明
|
||||
---
|
||||
title: 外部认证
|
||||
keywords: [higress, auth]
|
||||
description: Ext 认证插件实现了调用外部授权服务进行认证鉴权的功能。
|
||||
---
|
||||
|
||||
## 功能说明
|
||||
|
||||
`ext-auth` 插件实现了向外部授权服务发送鉴权请求,以检查客户端请求是否得到授权。该插件实现时参考了Envoy原生的[ext_authz filter](https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/ext_authz_filter),实现了原生filter中对接HTTP服务的部分能力
|
||||
|
||||
|
||||
|
||||
# 配置字段
|
||||
## 配置字段
|
||||
|
||||
| 名称 | 数据类型 | 必填 | 默认值 | 描述 |
|
||||
| ------------------------------- | -------- | ---- | ------ |------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
@@ -17,29 +23,30 @@
|
||||
|
||||
| 名称 | 数据类型 | 必填 | 默认值 | 描述 |
|
||||
| ------------------------ | -------- | ---- | ------ | ------------------------------------- |
|
||||
| `endpoint_mode` | string | 否 | envoy | `envoy` , `forward_auth` 中选填一项 |
|
||||
| `endpoint` | object | 是 | - | 发送鉴权请求的 HTTP 服务信息 |
|
||||
| `timeout` | int | 否 | 200 | `ext-auth` 服务连接超时时间,单位毫秒 |
|
||||
| `timeout` | int | 否 | 1000 | `ext-auth` 服务连接超时时间,单位毫秒 |
|
||||
| `authorization_request` | object | 否 | - | 发送鉴权请求配置 |
|
||||
| `authorization_response` | object | 否 | - | 处理鉴权响应配置 |
|
||||
| `authorization_response` | object | 否 | - | 处理鉴权响应配置 |
|
||||
|
||||
`endpoint`中每一项的配置字段说明
|
||||
|
||||
| 名称 | 数据类型 | 必填 | 默认值 | 描述 |
|
||||
| ---------------- | -------- | ---- | ------ | --------------------------------------------------- |
|
||||
| `service_source` | string | 是 | - | 类型为固定 ip 或者 dns,输入授权服务的注册来源 |
|
||||
| `service_name` | string | 是 | - | 输入授权服务的注册名称 |
|
||||
| `service_port` | string | 是 | - | 输入授权服务的服务端口 |
|
||||
| `service_domain` | string | 否 | - | 当类型为dns时必须填写,输入 `ext-auth` 服务的domain |
|
||||
| `request_method` | string | 否 | GET | 客户端向授权服务发送请求的HTTP Method |
|
||||
| `path` | string | 是 | - | 输入授权服务的请求路径 |
|
||||
| 名称 | 数据类型 | 必填 | 默认值 | 描述 |
|
||||
| -------- | -------- | -- | ------ |-----------------------------------------------------------------------------------------|
|
||||
| `service_name` | string | 必填 | - | 输入授权服务名称,带服务类型的完整 FQDN 名称,例如 `ext-auth.dns` 、`ext-auth.my-ns.svc.cluster.local` |
|
||||
| `service_port` | int | 否 | 80 | 输入授权服务的服务端口 |
|
||||
| `path_prefix` | string | `endpoint_mode` 为`envoy`时必填 | | `endpoint_mode` 为`envoy` 时,客户端向授权服务发送请求的请求路径前缀 |
|
||||
| `request_method` | string | 否 | GET | `endpoint_mode` 为`forward_auth` 时,客户端向授权服务发送请求的HTTP Method |
|
||||
| `path` | string | `endpoint_mode` 为`forward_auth`时必填 | - | `endpoint_mode` 为`forward_auth` 时,客户端向授权服务发送请求的请求路径 |
|
||||
|
||||
`authorization_request`中每一项的配置字段说明
|
||||
|
||||
| 名称 | 数据类型 | 必填 | 默认值 | 描述 |
|
||||
| ------------------- | ---------------------- | ---- | ------ | ------------------------------------------------------------ |
|
||||
| `allowed_headers` | array of StringMatcher | 否 | - | 当设置后,具有相应匹配项的客户端请求头将添加到授权服务请求中的请求头中。除了用户自定义的头部匹配规则外,授权服务请求中会自动包含`Host`, `Method`, `Path`, `Content-Length` 和 `Authorization`这几个关键的HTTP头 |
|
||||
| `headers_to_add` | `map[string]string` | 否 | - | 设置将包含在授权服务请求中的请求头列表。请注意,同名的客户端请求头将被覆盖 |
|
||||
| `with_request_body` | bool | 否 | false | 缓冲客户端请求体,并将其发送至鉴权请求中(HTTP Method为GET、OPTIONS、HEAD请求时不生效) |
|
||||
| 名称 | 数据类型 | 必填 | 默认值 | 描述 |
|
||||
| ------------------------ | ---------------------- | ---- | ------ |---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `allowed_headers` | array of StringMatcher | 否 | - | 当设置后,具有相应匹配项的客户端请求头将添加到授权服务请求中的请求头中。除了用户自定义的头部匹配规则外,授权服务请求中会自动包含 `Authorization` 这个HTTP头( `endpoint_mode` 为 `forward_auth` 时,会把原始请求的请求路径设置到 `X-Original-Uri` ,原始请求的HTTP Method设置到 `X-Original-Method` ) |
|
||||
| `headers_to_add` | `map[string]string` | 否 | - | 设置将包含在授权服务请求中的请求头列表。请注意,同名的客户端请求头将被覆盖 |
|
||||
| `with_request_body` | bool | 否 | false | 缓冲客户端请求体,并将其发送至鉴权请求中(HTTP Method为GET、OPTIONS、HEAD请求时不生效) |
|
||||
| `max_request_body_bytes` | int | 否 | 10MB | 设置在内存中保存客户端请求体的最大尺寸。当客户端请求体达到在此字段中设置的数值时,将会返回HTTP 413状态码,并且不会启动授权过程。注意,这个设置会优先于 `failure_mode_allow` 的配置 |
|
||||
|
||||
`authorization_response`中每一项的配置字段说明
|
||||
|
||||
@@ -60,30 +67,35 @@
|
||||
|
||||
|
||||
|
||||
# 配置示例
|
||||
## 配置示例
|
||||
|
||||
下面假设 `ext-auth` 服务在Kubernetes中serviceName为 `ext-auth`,端口 `8090`,路径为 `/auth`,命名空间为 `backend`
|
||||
|
||||
## 示例1
|
||||
支持两种 `endpoint_mode`:
|
||||
|
||||
- `endpoint_mode` 为 `envoy` 时,鉴权请求会使用原始请求的HTTP Method,和配置的 `path_prefix` 作为请求路径前缀拼接上原始的请求路径
|
||||
- `endpoint_mode` 为 `forward_auth` 时,鉴权请求会使用配置的 `request_method` 作为HTTP Method,和配置的 `path` 作为请求路径
|
||||
|
||||
### endpoint_mode为envoy时
|
||||
|
||||
#### 示例1
|
||||
|
||||
`ext-auth` 插件的配置:
|
||||
|
||||
```yaml
|
||||
http_service:
|
||||
endpoint_mode: envoy
|
||||
endpoint:
|
||||
service_name: ext-auth
|
||||
namespace: backend
|
||||
service_name: ext-auth.backend.svc.cluster.local
|
||||
service_port: 8090
|
||||
service_source: k8s
|
||||
path: /auth
|
||||
request_method: POST
|
||||
timeout: 500
|
||||
path_prefix: /auth
|
||||
timeout: 1000
|
||||
```
|
||||
|
||||
使用如下请求网关,当开启 `ext-auth` 插件后:
|
||||
|
||||
```shell
|
||||
curl -i http://localhost:8082/users -X GET -H "foo: bar" -H "Authorization: xxx"
|
||||
curl -X POST http://localhost:8082/users?apikey=9a342114-ba8a-11ec-b1bf-00163e1250b5 -X GET -H "foo: bar" -H "Authorization: xxx"
|
||||
```
|
||||
|
||||
**请求 `ext-auth` 服务成功:**
|
||||
@@ -91,7 +103,7 @@ curl -i http://localhost:8082/users -X GET -H "foo: bar" -H "Authorization: xxx"
|
||||
`ext-auth` 服务将接收到如下的鉴权请求:
|
||||
|
||||
```
|
||||
POST /auth HTTP/1.1
|
||||
POST /auth/users?apikey=9a342114-ba8a-11ec-b1bf-00163e1250b5 HTTP/1.1
|
||||
Host: ext-auth
|
||||
Authorization: xxx
|
||||
Content-Length: 0
|
||||
@@ -116,8 +128,7 @@ content-length: 0
|
||||
|
||||
当 `ext-auth` 服务返回其他 HTTP 状态码时,将以返回的状态码拒绝客户端请求。如果配置了 `allowed_client_headers`,具有相应匹配项的响应头将添加到客户端的响应中
|
||||
|
||||
|
||||
## 示例2
|
||||
#### 示例2
|
||||
|
||||
`ext-auth` 插件的配置:
|
||||
|
||||
@@ -132,26 +143,24 @@ http_service:
|
||||
allowed_upstream_headers:
|
||||
- exact: x-user-id
|
||||
- exact: x-auth-version
|
||||
endpoint_mode: envoy
|
||||
endpoint:
|
||||
service_name: ext-auth
|
||||
namespace: backend
|
||||
service_name: ext-auth.backend.svc.cluster.local
|
||||
service_port: 8090
|
||||
service_source: k8s
|
||||
path: /auth
|
||||
request_method: POST
|
||||
timeout: 500
|
||||
path_prefix: /auth
|
||||
timeout: 1000
|
||||
```
|
||||
|
||||
使用如下请求网关,当开启 `ext-auth` 插件后:
|
||||
|
||||
```shell
|
||||
curl -i http://localhost:8082/users -X GET -H "foo: bar" -H "Authorization: xxx" -H "X-Auth-Version: 1.0"
|
||||
curl -X POST http://localhost:8082/users?apikey=9a342114-ba8a-11ec-b1bf-00163e1250b5 -X GET -H "foo: bar" -H "Authorization: xxx"
|
||||
```
|
||||
|
||||
`ext-auth` 服务将接收到如下的鉴权请求:
|
||||
|
||||
```
|
||||
POST /auth HTTP/1.1
|
||||
POST /auth/users?apikey=9a342114-ba8a-11ec-b1bf-00163e1250b5 HTTP/1.1
|
||||
Host: ext-auth
|
||||
Authorization: xxx
|
||||
X-Auth-Version: 1.0
|
||||
@@ -160,3 +169,105 @@ Content-Length: 0
|
||||
```
|
||||
|
||||
`ext-auth` 服务返回响应头中如果包含 `x-user-id` 和 `x-auth-version`,网关调用upstream时的请求中会带上这两个请求头
|
||||
|
||||
|
||||
|
||||
### endpoint_mode为forward_auth时
|
||||
|
||||
#### 示例1
|
||||
|
||||
`ext-auth` 插件的配置:
|
||||
|
||||
```yaml
|
||||
http_service:
|
||||
endpoint_mode: forward_auth
|
||||
endpoint:
|
||||
service_name: ext-auth.backend.svc.cluster.local
|
||||
service_port: 8090
|
||||
path: /auth
|
||||
request_method: POST
|
||||
timeout: 1000
|
||||
```
|
||||
|
||||
使用如下请求网关,当开启 `ext-auth` 插件后:
|
||||
|
||||
```shell
|
||||
curl -i http://localhost:8082/users?apikey=9a342114-ba8a-11ec-b1bf-00163e1250b5 -X GET -H "foo: bar" -H "Authorization: xxx"
|
||||
```
|
||||
|
||||
**请求 `ext-auth` 服务成功:**
|
||||
|
||||
`ext-auth` 服务将接收到如下的鉴权请求:
|
||||
|
||||
```
|
||||
POST /auth HTTP/1.1
|
||||
Host: ext-auth
|
||||
Authorization: xxx
|
||||
X-Original-Uri: /users?apikey=9a342114-ba8a-11ec-b1bf-00163e1250b5
|
||||
X-Original-Method: GET
|
||||
Content-Length: 0
|
||||
```
|
||||
|
||||
**请求 `ext-auth` 服务失败:**
|
||||
|
||||
当调用 `ext-auth` 服务响应为 5xx 时,客户端将接收到HTTP响应码403和 `ext-auth` 服务返回的全量响应头
|
||||
|
||||
假如 `ext-auth` 服务返回了 `x-auth-version: 1.0` 和 `x-auth-failed: true` 的响应头,会传递给客户端
|
||||
|
||||
```
|
||||
HTTP/1.1 403 Forbidden
|
||||
x-auth-version: 1.0
|
||||
x-auth-failed: true
|
||||
date: Tue, 16 Jul 2024 00:19:41 GMT
|
||||
server: istio-envoy
|
||||
content-length: 0
|
||||
```
|
||||
|
||||
当 `ext-auth` 无法访问或状态码为 5xx 时,将以 `status_on_error` 配置的状态码拒绝客户端请求
|
||||
|
||||
当 `ext-auth` 服务返回其他 HTTP 状态码时,将以返回的状态码拒绝客户端请求。如果配置了 `allowed_client_headers`,具有相应匹配项的响应头将添加到客户端的响应中
|
||||
|
||||
#### 示例2
|
||||
|
||||
`ext-auth` 插件的配置:
|
||||
|
||||
```yaml
|
||||
http_service:
|
||||
authorization_request:
|
||||
allowed_headers:
|
||||
- exact: x-auth-version
|
||||
headers_to_add:
|
||||
x-envoy-header: true
|
||||
authorization_response:
|
||||
allowed_upstream_headers:
|
||||
- exact: x-user-id
|
||||
- exact: x-auth-version
|
||||
endpoint_mode: forward_auth
|
||||
endpoint:
|
||||
service_name: ext-auth.backend.svc.cluster.local
|
||||
service_port: 8090
|
||||
path: /auth
|
||||
request_method: POST
|
||||
timeout: 1000
|
||||
```
|
||||
|
||||
使用如下请求网关,当开启 `ext-auth` 插件后:
|
||||
|
||||
```shell
|
||||
curl -i http://localhost:8082/users?apikey=9a342114-ba8a-11ec-b1bf-00163e1250b5 -X GET -H "foo: bar" -H "Authorization: xxx" -H "X-Auth-Version: 1.0"
|
||||
```
|
||||
|
||||
`ext-auth` 服务将接收到如下的鉴权请求:
|
||||
|
||||
```
|
||||
POST /auth HTTP/1.1
|
||||
Host: ext-auth
|
||||
Authorization: xxx
|
||||
X-Original-Uri: /users?apikey=9a342114-ba8a-11ec-b1bf-00163e1250b5
|
||||
X-Original-Method: GET
|
||||
X-Auth-Version: 1.0
|
||||
x-envoy-header: true
|
||||
Content-Length: 0
|
||||
```
|
||||
|
||||
`ext-auth` 服务返回响应头中如果包含 `x-user-id` 和 `x-auth-version`,网关调用upstream时的请求中会带上这两个请求头
|
||||
|
||||
@@ -13,7 +13,13 @@ import (
|
||||
const (
|
||||
DefaultStatusOnError uint32 = http.StatusForbidden
|
||||
|
||||
DefaultHttpServiceTimeout uint32 = 200
|
||||
DefaultHttpServiceTimeout uint32 = 1000
|
||||
|
||||
DefaultMaxRequestBodyBytes uint32 = 10 * 1024 * 1024
|
||||
|
||||
EndpointModeEnvoy = "envoy"
|
||||
|
||||
EndpointModeForwardAuth = "forward_auth"
|
||||
)
|
||||
|
||||
type ExtAuthConfig struct {
|
||||
@@ -24,8 +30,13 @@ type ExtAuthConfig struct {
|
||||
}
|
||||
|
||||
type HttpService struct {
|
||||
client wrapper.HttpClient
|
||||
requestMethod string
|
||||
endpointMode string
|
||||
client wrapper.HttpClient
|
||||
// pathPrefix is only used when endpoint_mode is envoy
|
||||
pathPrefix string
|
||||
// requestMethod is only used when endpoint_mode is forward_auth
|
||||
requestMethod string
|
||||
// path is only used when endpoint_mode is forward_auth
|
||||
path string
|
||||
timeout uint32
|
||||
authorizationRequest AuthorizationRequest
|
||||
@@ -34,10 +45,14 @@ type HttpService struct {
|
||||
|
||||
type AuthorizationRequest struct {
|
||||
// allowedHeaders In addition to the user’s supplied matchers,
|
||||
// Host, Method, Path, Content-Length, and Authorization are automatically included to the list.
|
||||
allowedHeaders expr.Matcher
|
||||
headersToAdd map[string]string
|
||||
withRequestBody bool
|
||||
// Authorization are automatically included to the list.
|
||||
// When the endpoint_mode is set to forward_auth,
|
||||
// the original request's path is set in the X-Original-Uri header,
|
||||
// and the original request's HTTP method is set in the X-Original-Method header.
|
||||
allowedHeaders expr.Matcher
|
||||
headersToAdd map[string]string
|
||||
withRequestBody bool
|
||||
maxRequestBodyBytes uint32
|
||||
}
|
||||
|
||||
type AuthorizationResponse struct {
|
||||
@@ -50,7 +65,7 @@ func parseConfig(json gjson.Result, config *ExtAuthConfig, log wrapper.Log) erro
|
||||
if !httpServiceConfig.Exists() {
|
||||
return errors.New("missing http_service in config")
|
||||
}
|
||||
err := parseHttpServiceConfig(httpServiceConfig, config)
|
||||
err := parseHttpServiceConfig(httpServiceConfig, config, log)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -65,20 +80,19 @@ func parseConfig(json gjson.Result, config *ExtAuthConfig, log wrapper.Log) erro
|
||||
config.failureModeAllowHeaderAdd = failureModeAllowHeaderAdd.Bool()
|
||||
}
|
||||
|
||||
statusOnError := json.Get("status_on_error")
|
||||
if statusOnError.Exists() {
|
||||
config.statusOnError = uint32(statusOnError.Uint())
|
||||
} else {
|
||||
config.statusOnError = DefaultStatusOnError
|
||||
statusOnError := uint32(json.Get("status_on_error").Uint())
|
||||
if statusOnError == 0 {
|
||||
statusOnError = DefaultStatusOnError
|
||||
}
|
||||
config.statusOnError = statusOnError
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseHttpServiceConfig(json gjson.Result, config *ExtAuthConfig) error {
|
||||
func parseHttpServiceConfig(json gjson.Result, config *ExtAuthConfig, log wrapper.Log) error {
|
||||
var httpService HttpService
|
||||
|
||||
if err := parseEndpointConfig(json, &httpService); err != nil {
|
||||
if err := parseEndpointConfig(json, &httpService, log); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -101,64 +115,63 @@ func parseHttpServiceConfig(json gjson.Result, config *ExtAuthConfig) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseEndpointConfig(json gjson.Result, httpService *HttpService) error {
|
||||
func parseEndpointConfig(json gjson.Result, httpService *HttpService, log wrapper.Log) error {
|
||||
endpointMode := json.Get("endpoint_mode").String()
|
||||
if endpointMode == "" {
|
||||
endpointMode = EndpointModeEnvoy
|
||||
} else if endpointMode != EndpointModeEnvoy && endpointMode != EndpointModeForwardAuth {
|
||||
return errors.New(fmt.Sprintf("endpoint_mode %s is not supported", endpointMode))
|
||||
}
|
||||
httpService.endpointMode = endpointMode
|
||||
|
||||
endpointConfig := json.Get("endpoint")
|
||||
if !endpointConfig.Exists() {
|
||||
return errors.New("missing endpoint in config")
|
||||
}
|
||||
|
||||
serviceSource := endpointConfig.Get("service_source").String()
|
||||
serviceName := endpointConfig.Get("service_name").String()
|
||||
if serviceName == "" {
|
||||
return errors.New("endpoint service name must not be empty")
|
||||
}
|
||||
servicePort := endpointConfig.Get("service_port").Int()
|
||||
if serviceName == "" || servicePort == 0 {
|
||||
return errors.New("invalid service config")
|
||||
}
|
||||
switch serviceSource {
|
||||
case "k8s":
|
||||
namespace := json.Get("namespace").String()
|
||||
httpService.client = wrapper.NewClusterClient(wrapper.K8sCluster{
|
||||
ServiceName: serviceName,
|
||||
Namespace: namespace,
|
||||
Port: servicePort,
|
||||
})
|
||||
return nil
|
||||
case "nacos":
|
||||
namespace := json.Get("namespace").String()
|
||||
httpService.client = wrapper.NewClusterClient(wrapper.NacosCluster{
|
||||
ServiceName: serviceName,
|
||||
NamespaceID: namespace,
|
||||
Port: servicePort,
|
||||
})
|
||||
return nil
|
||||
case "ip":
|
||||
httpService.client = wrapper.NewClusterClient(wrapper.StaticIpCluster{
|
||||
ServiceName: serviceName,
|
||||
Port: servicePort,
|
||||
})
|
||||
case "dns":
|
||||
domain := endpointConfig.Get("domain").String()
|
||||
httpService.client = wrapper.NewClusterClient(wrapper.DnsCluster{
|
||||
ServiceName: serviceName,
|
||||
Port: servicePort,
|
||||
Domain: domain,
|
||||
})
|
||||
default:
|
||||
return errors.New("unknown service source: " + serviceSource)
|
||||
if servicePort == 0 {
|
||||
servicePort = 80
|
||||
}
|
||||
|
||||
requestMethodConfig := endpointConfig.Get("request_method")
|
||||
if !requestMethodConfig.Exists() {
|
||||
httpService.requestMethod = http.MethodGet
|
||||
} else {
|
||||
httpService.requestMethod = strings.ToUpper(requestMethodConfig.String())
|
||||
}
|
||||
httpService.client = wrapper.NewClusterClient(wrapper.FQDNCluster{
|
||||
FQDN: serviceName,
|
||||
Port: servicePort,
|
||||
})
|
||||
|
||||
pathConfig := endpointConfig.Get("path")
|
||||
if !pathConfig.Exists() {
|
||||
return errors.New("missing path in config")
|
||||
}
|
||||
httpService.path = pathConfig.String()
|
||||
switch endpointMode {
|
||||
case EndpointModeEnvoy:
|
||||
pathPrefixConfig := endpointConfig.Get("path_prefix")
|
||||
if !pathPrefixConfig.Exists() {
|
||||
return errors.New("when endpoint_mode is envoy, endpoint path_prefix must not be empty")
|
||||
}
|
||||
httpService.pathPrefix = pathPrefixConfig.String()
|
||||
|
||||
if endpointConfig.Get("request_method").Exists() || endpointConfig.Get("path").Exists() {
|
||||
log.Warn("when endpoint_mode is envoy, endpoint request_method and path will be ignored")
|
||||
}
|
||||
case EndpointModeForwardAuth:
|
||||
requestMethodConfig := endpointConfig.Get("request_method")
|
||||
if !requestMethodConfig.Exists() {
|
||||
httpService.requestMethod = http.MethodGet
|
||||
} else {
|
||||
httpService.requestMethod = strings.ToUpper(requestMethodConfig.String())
|
||||
}
|
||||
|
||||
pathConfig := endpointConfig.Get("path")
|
||||
if !pathConfig.Exists() {
|
||||
return errors.New("when endpoint_mode is forward_auth, endpoint path must not be empty")
|
||||
}
|
||||
httpService.path = pathConfig.String()
|
||||
|
||||
if endpointConfig.Get("path_prefix").Exists() {
|
||||
log.Warn("when endpoint_mode is forward_auth, endpoint path_prefix will be ignored")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -167,6 +180,15 @@ func parseAuthorizationRequestConfig(json gjson.Result, httpService *HttpService
|
||||
if authorizationRequestConfig.Exists() {
|
||||
var authorizationRequest AuthorizationRequest
|
||||
|
||||
allowedHeaders := authorizationRequestConfig.Get("allowed_headers")
|
||||
if allowedHeaders.Exists() {
|
||||
result, err := expr.BuildRepeatedStringMatcherIgnoreCase(allowedHeaders.Array())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
authorizationRequest.allowedHeaders = result
|
||||
}
|
||||
|
||||
headersToAdd := map[string]string{}
|
||||
headersToAddConfig := authorizationRequestConfig.Get("headers_to_add")
|
||||
if headersToAddConfig.Exists() {
|
||||
@@ -186,14 +208,11 @@ func parseAuthorizationRequestConfig(json gjson.Result, httpService *HttpService
|
||||
authorizationRequest.withRequestBody = withRequestBody.Bool()
|
||||
}
|
||||
|
||||
allowedHeaders := authorizationRequestConfig.Get("allowed_headers")
|
||||
if allowedHeaders.Exists() {
|
||||
result, err := expr.BuildRepeatedStringMatcherIgnoreCase(allowedHeaders.Array())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
authorizationRequest.allowedHeaders = result
|
||||
maxRequestBodyBytes := uint32(authorizationRequestConfig.Get("max_request_body_bytes").Uint())
|
||||
if maxRequestBodyBytes == 0 {
|
||||
maxRequestBodyBytes = DefaultMaxRequestBodyBytes
|
||||
}
|
||||
authorizationRequest.maxRequestBodyBytes = maxRequestBodyBytes
|
||||
|
||||
httpService.authorizationRequest = authorizationRequest
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ package expr
|
||||
import (
|
||||
"errors"
|
||||
"github.com/tidwall/gjson"
|
||||
"regexp"
|
||||
regexp "github.com/wasilibs/go-re2"
|
||||
"strings"
|
||||
)
|
||||
|
||||
|
||||
@@ -9,16 +9,19 @@ require (
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240711023527-ba358c48772f
|
||||
github.com/stretchr/testify v1.8.4
|
||||
github.com/tidwall/gjson v1.14.3
|
||||
github.com/wasilibs/go-re2 v1.6.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 // indirect
|
||||
github.com/magefile/mage v1.14.0 // indirect
|
||||
github.com/magefile/mage v1.15.1-0.20230912152418-9f54e0f83e2a // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/tetratelabs/wazero v1.7.2 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.0 // indirect
|
||||
github.com/tidwall/resp v0.1.1 // indirect
|
||||
golang.org/x/sys v0.21.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -6,12 +6,14 @@ 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 v0.0.0-20240711023527-ba358c48772f h1:ZIiIBRvIw62gA5MJhuwp1+2wWbqL9IGElQ499rUsYYg=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240711023527-ba358c48772f/go.mod h1:hNFjhrLUIq+kJ9bOcs8QtiplSQ61GZXtd2xHKx4BYRo=
|
||||
github.com/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=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/tetratelabs/wazero v1.7.2 h1:1+z5nXJNwMLPAWaTePFi49SSTL0IMx/i3Fg8Yc25GDc=
|
||||
github.com/tetratelabs/wazero v1.7.2/go.mod h1:ytl6Zuh20R/eROuyDaGPkp82O9C/DJfXAwJfQ3X6/7Y=
|
||||
github.com/tidwall/gjson v1.14.3 h1:9jvXn7olKEHU1S9vwoMGliaT8jq1vJ7IH/n9zD9Dnlw=
|
||||
github.com/tidwall/gjson v1.14.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
@@ -20,6 +22,11 @@ github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/resp v0.1.1 h1:Ly20wkhqKTmDUPlyM1S7pWo5kk0tDu8OoC/vFArXmwE=
|
||||
github.com/tidwall/resp v0.1.1/go.mod h1:3/FrruOBAxPTPtundW0VXgmsQ4ZBA0Aw714lVYgwFa0=
|
||||
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=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -32,39 +32,39 @@ func main() {
|
||||
}
|
||||
|
||||
const (
|
||||
HeaderContentLength string = "content-length"
|
||||
HeaderAuthorization string = "authorization"
|
||||
HeaderFailureModeAllow string = "x-envoy-auth-failure-mode-allowed"
|
||||
HeaderOriginalMethod string = "x-original-method"
|
||||
HeaderOriginalUri string = "x-original-uri"
|
||||
)
|
||||
|
||||
func onHttpRequestHeaders(ctx wrapper.HttpContext, config ExtAuthConfig, log wrapper.Log) types.Action {
|
||||
contentLengthStr, _ := proxywasm.GetHttpRequestHeader(HeaderContentLength)
|
||||
hasRequestBody := false
|
||||
if contentLengthStr != "" {
|
||||
contentLength, err := strconv.Atoi(contentLengthStr)
|
||||
hasRequestBody = err == nil && contentLength > 0
|
||||
}
|
||||
// If withRequestBody is true AND the HTTP request contains a request body,
|
||||
// it will be handled in the onHttpRequestBody phase.
|
||||
if config.httpService.authorizationRequest.withRequestBody && hasRequestBody {
|
||||
// Disable the route re-calculation since the plugin may modify some headers related to the chosen route.
|
||||
ctx.DisableReroute()
|
||||
// The request has a body and requires delaying the header transmission until a cache miss occurs,
|
||||
// at which point the header should be sent.
|
||||
return types.HeaderStopIteration
|
||||
if wrapper.HasRequestBody() {
|
||||
ctx.SetRequestBodyBufferLimit(config.httpService.authorizationRequest.maxRequestBodyBytes)
|
||||
|
||||
// If withRequestBody is true AND the HTTP request contains a request body,
|
||||
// it will be handled in the onHttpRequestBody phase.
|
||||
if config.httpService.authorizationRequest.withRequestBody {
|
||||
// Disable the route re-calculation since the plugin may modify some headers related to the chosen route.
|
||||
ctx.DisableReroute()
|
||||
// The request has a body and requires delaying the header transmission until a cache miss occurs,
|
||||
// at which point the header should be sent.
|
||||
return types.HeaderStopIteration
|
||||
}
|
||||
}
|
||||
|
||||
ctx.DontReadRequestBody()
|
||||
return checkExtAuth(ctx, config, nil, log)
|
||||
return checkExtAuth(ctx, config, nil, log, types.HeaderStopAllIterationAndWatermark)
|
||||
}
|
||||
|
||||
func onHttpRequestBody(ctx wrapper.HttpContext, config ExtAuthConfig, body []byte, log wrapper.Log) types.Action {
|
||||
if config.httpService.authorizationRequest.withRequestBody {
|
||||
return checkExtAuth(ctx, config, body, log)
|
||||
return checkExtAuth(ctx, config, body, log, types.ActionPause)
|
||||
}
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
func checkExtAuth(ctx wrapper.HttpContext, config ExtAuthConfig, body []byte, log wrapper.Log) types.Action {
|
||||
func checkExtAuth(ctx wrapper.HttpContext, config ExtAuthConfig, body []byte, log wrapper.Log, pauseAction types.Action) types.Action {
|
||||
// build extAuth request headers
|
||||
extAuthReqHeaders := http.Header{}
|
||||
|
||||
@@ -90,8 +90,21 @@ func checkExtAuth(ctx wrapper.HttpContext, config ExtAuthConfig, body []byte, lo
|
||||
extAuthReqHeaders.Set(HeaderAuthorization, authorization)
|
||||
}
|
||||
|
||||
// when endpoint_mode is forward_auth, add x-original-method and x-original-uri headers
|
||||
if httpServiceConfig.endpointMode == EndpointModeForwardAuth {
|
||||
extAuthReqHeaders.Set(HeaderOriginalMethod, ctx.Method())
|
||||
extAuthReqHeaders.Set(HeaderOriginalUri, ctx.Path())
|
||||
}
|
||||
|
||||
requestMethod := httpServiceConfig.requestMethod
|
||||
requestPath := httpServiceConfig.path
|
||||
if httpServiceConfig.endpointMode == EndpointModeEnvoy {
|
||||
requestMethod = ctx.Method()
|
||||
requestPath, _ = url.JoinPath(httpServiceConfig.pathPrefix, ctx.Path())
|
||||
}
|
||||
|
||||
// call ext auth server
|
||||
err := httpServiceConfig.client.Post(httpServiceConfig.path, reconvertHeaders(extAuthReqHeaders), body,
|
||||
err := httpServiceConfig.client.Call(requestMethod, requestPath, reconvertHeaders(extAuthReqHeaders), body,
|
||||
func(statusCode int, responseHeaders http.Header, responseBody []byte) {
|
||||
defer proxywasm.ResumeHttpRequest()
|
||||
if statusCode != http.StatusOK {
|
||||
@@ -116,7 +129,7 @@ func checkExtAuth(ctx wrapper.HttpContext, config ExtAuthConfig, body []byte, lo
|
||||
callExtAuthServerErrorHandler(config, http.StatusInternalServerError, nil)
|
||||
return types.ActionContinue
|
||||
}
|
||||
return types.ActionPause
|
||||
return pauseAction
|
||||
}
|
||||
|
||||
func callExtAuthServerErrorHandler(config ExtAuthConfig, statusCode int, extAuthRespHeaders http.Header) {
|
||||
@@ -137,8 +150,8 @@ func callExtAuthServerErrorHandler(config ExtAuthConfig, statusCode int, extAuth
|
||||
}
|
||||
}
|
||||
|
||||
// Rejects client requests with statusOnError on extAuth unavailability or 5xx.
|
||||
// Otherwise, uses the extAuth's returned status code to reject requests.
|
||||
// rejects client requests with statusOnError on extAuth unavailability or 5xx.
|
||||
// otherwise, uses the extAuth's returned status code to reject requests
|
||||
statusToUse := statusCode
|
||||
if statusCode >= http.StatusInternalServerError {
|
||||
statusToUse = int(config.statusOnError)
|
||||
|
||||
@@ -8,13 +8,16 @@ require (
|
||||
github.com/alibaba/higress/plugins/wasm-go v0.0.0
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240711023527-ba358c48772f
|
||||
github.com/tidwall/gjson v1.14.3
|
||||
github.com/wasilibs/go-re2 v1.6.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 // indirect
|
||||
github.com/magefile/mage v1.14.0 // indirect
|
||||
github.com/magefile/mage v1.15.1-0.20230912152418-9f54e0f83e2a // indirect
|
||||
github.com/tetratelabs/wazero v1.7.2 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.0 // indirect
|
||||
github.com/tidwall/resp v0.1.1 // indirect
|
||||
golang.org/x/sys v0.21.0 // indirect
|
||||
)
|
||||
|
||||
@@ -3,15 +3,14 @@ github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 h1:IHDghbGQ2DTIXHBHxWfqCYQW1fKjyJ/I7W1pMyUDeEA=
|
||||
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520/go.mod h1:Nz8ORLaFiLWotg6GeKlJMhv8cci8mM43uEnLA5t8iew=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240226064518-b3dc4646a35a h1:luYRvxLTE1xYxrXYj7nmjd1U0HHh8pUPiKfdZ0MhCGE=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240226064518-b3dc4646a35a/go.mod h1:hNFjhrLUIq+kJ9bOcs8QtiplSQ61GZXtd2xHKx4BYRo=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240318034951-d5306e367c43/go.mod h1:hNFjhrLUIq+kJ9bOcs8QtiplSQ61GZXtd2xHKx4BYRo=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240327114451-d6b7174a84fc/go.mod h1:hNFjhrLUIq+kJ9bOcs8QtiplSQ61GZXtd2xHKx4BYRo=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240711023527-ba358c48772f h1:ZIiIBRvIw62gA5MJhuwp1+2wWbqL9IGElQ499rUsYYg=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240711023527-ba358c48772f/go.mod h1:hNFjhrLUIq+kJ9bOcs8QtiplSQ61GZXtd2xHKx4BYRo=
|
||||
github.com/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=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/tetratelabs/wazero v1.7.2 h1:1+z5nXJNwMLPAWaTePFi49SSTL0IMx/i3Fg8Yc25GDc=
|
||||
github.com/tetratelabs/wazero v1.7.2/go.mod h1:ytl6Zuh20R/eROuyDaGPkp82O9C/DJfXAwJfQ3X6/7Y=
|
||||
github.com/tidwall/gjson v1.14.3 h1:9jvXn7olKEHU1S9vwoMGliaT8jq1vJ7IH/n9zD9Dnlw=
|
||||
github.com/tidwall/gjson v1.14.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
@@ -20,4 +19,9 @@ github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/resp v0.1.1 h1:Ly20wkhqKTmDUPlyM1S7pWo5kk0tDu8OoC/vFArXmwE=
|
||||
github.com/tidwall/resp v0.1.1/go.mod h1:3/FrruOBAxPTPtundW0VXgmsQ4ZBA0Aw714lVYgwFa0=
|
||||
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/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
|
||||
@@ -17,12 +17,12 @@ package main
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
"github.com/tidwall/gjson"
|
||||
regexp "github.com/wasilibs/go-re2"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
)
|
||||
|
||||
@@ -12,15 +12,18 @@ require (
|
||||
github.com/tidwall/gjson v1.17.0
|
||||
github.com/tidwall/pretty v1.2.1
|
||||
github.com/tidwall/sjson v1.2.5
|
||||
github.com/wasilibs/go-re2 v1.6.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 // indirect
|
||||
github.com/magefile/mage v1.14.0 // indirect
|
||||
github.com/magefile/mage v1.15.1-0.20230912152418-9f54e0f83e2a // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/tetratelabs/wazero v1.7.2 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/resp v0.1.1 // indirect
|
||||
golang.org/x/sys v0.21.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -4,17 +4,18 @@ github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 h1:IHDghbGQ2DTIXHBHxWfqCYQW1fKjyJ/I7W1pMyUDeEA=
|
||||
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520/go.mod h1:Nz8ORLaFiLWotg6GeKlJMhv8cci8mM43uEnLA5t8iew=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240327114451-d6b7174a84fc h1:t2AT8zb6N/59Y78lyRWedVoVWHNRSCBh0oWCC+bluTQ=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240327114451-d6b7174a84fc/go.mod h1:hNFjhrLUIq+kJ9bOcs8QtiplSQ61GZXtd2xHKx4BYRo=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240711023527-ba358c48772f h1:ZIiIBRvIw62gA5MJhuwp1+2wWbqL9IGElQ499rUsYYg=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240711023527-ba358c48772f/go.mod h1:hNFjhrLUIq+kJ9bOcs8QtiplSQ61GZXtd2xHKx4BYRo=
|
||||
github.com/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/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/tetratelabs/wazero v1.7.2 h1:1+z5nXJNwMLPAWaTePFi49SSTL0IMx/i3Fg8Yc25GDc=
|
||||
github.com/tetratelabs/wazero v1.7.2/go.mod h1:ytl6Zuh20R/eROuyDaGPkp82O9C/DJfXAwJfQ3X6/7Y=
|
||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.17.0 h1:/Jocvlh98kcTfpN2+JzGQWQcqrPQwDrVEMApx/M5ZwM=
|
||||
github.com/tidwall/gjson v1.17.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
@@ -27,6 +28,11 @@ github.com/tidwall/resp v0.1.1 h1:Ly20wkhqKTmDUPlyM1S7pWo5kk0tDu8OoC/vFArXmwE=
|
||||
github.com/tidwall/resp v0.1.1/go.mod h1:3/FrruOBAxPTPtundW0VXgmsQ4ZBA0Aw714lVYgwFa0=
|
||||
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=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
|
||||
@@ -15,16 +15,16 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"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/pkg/errors"
|
||||
"github.com/tidwall/gjson"
|
||||
"github.com/tidwall/sjson"
|
||||
regexp "github.com/wasilibs/go-re2"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -291,6 +291,7 @@ func onHttpRequestHeaders(ctx wrapper.HttpContext, config TransformerConfig, log
|
||||
ctx.SetContext("path", path)
|
||||
|
||||
if config.reqTrans == nil {
|
||||
ctx.DontReadRequestBody()
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
@@ -299,15 +300,18 @@ func onHttpRequestHeaders(ctx wrapper.HttpContext, config TransformerConfig, log
|
||||
headers, err := proxywasm.GetHttpRequestHeaders()
|
||||
if err != nil {
|
||||
log.Warn("failed to get request headers")
|
||||
ctx.DontReadRequestBody()
|
||||
return types.ActionContinue
|
||||
}
|
||||
hs := convertHeaders(headers)
|
||||
if hs[":authority"] == nil {
|
||||
log.Warn(errGetRequestHost.Error())
|
||||
ctx.DontReadRequestBody()
|
||||
return types.ActionContinue
|
||||
}
|
||||
if hs[":path"] == nil {
|
||||
log.Warn(errGetRequestPath.Error())
|
||||
ctx.DontReadRequestBody()
|
||||
return types.ActionContinue
|
||||
}
|
||||
contentType := ""
|
||||
@@ -330,6 +334,7 @@ func onHttpRequestHeaders(ctx wrapper.HttpContext, config TransformerConfig, log
|
||||
qs, err := parseQueryByPath(path)
|
||||
if err != nil {
|
||||
log.Warnf("failed to parse query params by path: %v", err)
|
||||
ctx.DontReadRequestBody()
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
@@ -358,6 +363,7 @@ func onHttpRequestHeaders(ctx wrapper.HttpContext, config TransformerConfig, log
|
||||
if config.reqTrans.IsHeaderChange() {
|
||||
if err = config.reqTrans.TransformHeaders(host, path, hs, mapSourceData); err != nil {
|
||||
log.Warnf("failed to transform request headers: %v", err)
|
||||
ctx.DontReadRequestBody()
|
||||
return types.ActionContinue
|
||||
}
|
||||
}
|
||||
@@ -365,11 +371,13 @@ func onHttpRequestHeaders(ctx wrapper.HttpContext, config TransformerConfig, log
|
||||
if config.reqTrans.IsQueryChange() {
|
||||
if err = config.reqTrans.TransformQuerys(host, path, qs, mapSourceData); err != nil {
|
||||
log.Warnf("failed to transform request query params: %v", err)
|
||||
ctx.DontReadRequestBody()
|
||||
return types.ActionContinue
|
||||
}
|
||||
path, err = constructPath(path, qs)
|
||||
if err != nil {
|
||||
log.Warnf("failed to construct path: %v", err)
|
||||
ctx.DontReadRequestBody()
|
||||
return types.ActionContinue
|
||||
}
|
||||
hs[":path"] = []string{path}
|
||||
@@ -378,6 +386,7 @@ func onHttpRequestHeaders(ctx wrapper.HttpContext, config TransformerConfig, log
|
||||
headers = reconvertHeaders(hs)
|
||||
if err = proxywasm.ReplaceHttpRequestHeaders(headers); err != nil {
|
||||
log.Warnf("failed to replace request headers: %v", err)
|
||||
ctx.DontReadRequestBody()
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
@@ -507,6 +516,7 @@ func onHttpRequestBody(ctx wrapper.HttpContext, config TransformerConfig, body [
|
||||
|
||||
func onHttpResponseHeaders(ctx wrapper.HttpContext, config TransformerConfig, log wrapper.Log) types.Action {
|
||||
if config.respTrans == nil {
|
||||
ctx.DontReadResponseBody()
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
@@ -515,11 +525,13 @@ func onHttpResponseHeaders(ctx wrapper.HttpContext, config TransformerConfig, lo
|
||||
host, path, err := getHostAndPathFromHttpCtx(ctx)
|
||||
if err != nil {
|
||||
log.Warn(err.Error())
|
||||
ctx.DontReadResponseBody()
|
||||
return types.ActionContinue
|
||||
}
|
||||
headers, err := proxywasm.GetHttpResponseHeaders()
|
||||
if err != nil {
|
||||
log.Warnf("failed to get response headers: %v", err)
|
||||
ctx.DontReadResponseBody()
|
||||
return types.ActionContinue
|
||||
}
|
||||
hs := convertHeaders(headers)
|
||||
@@ -555,6 +567,7 @@ func onHttpResponseHeaders(ctx wrapper.HttpContext, config TransformerConfig, lo
|
||||
if config.respTrans.IsHeaderChange() {
|
||||
if err = config.respTrans.TransformHeaders(host, path, hs, mapSourceData); err != nil {
|
||||
log.Warnf("failed to transform response headers: %v", err)
|
||||
ctx.DontReadResponseBody()
|
||||
return types.ActionContinue
|
||||
}
|
||||
}
|
||||
@@ -562,6 +575,7 @@ func onHttpResponseHeaders(ctx wrapper.HttpContext, config TransformerConfig, lo
|
||||
headers = reconvertHeaders(hs)
|
||||
if err = proxywasm.ReplaceHttpResponseHeaders(headers); err != nil {
|
||||
log.Warnf("failed to replace response headers: %v", err)
|
||||
ctx.DontReadResponseBody()
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
|
||||
@@ -4,12 +4,7 @@ github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 h1:IHDghbGQ2DTIXHBHxWfqCYQW1fKjyJ/I7W1pMyUDeEA=
|
||||
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520/go.mod h1:Nz8ORLaFiLWotg6GeKlJMhv8cci8mM43uEnLA5t8iew=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240226064518-b3dc4646a35a h1:luYRvxLTE1xYxrXYj7nmjd1U0HHh8pUPiKfdZ0MhCGE=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240226064518-b3dc4646a35a/go.mod h1:hNFjhrLUIq+kJ9bOcs8QtiplSQ61GZXtd2xHKx4BYRo=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240318034951-d5306e367c43 h1:dCw7F/9ciw4NZN7w68wQRaygZ2zGOWMTIEoRvP1tlWs=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240318034951-d5306e367c43/go.mod h1:hNFjhrLUIq+kJ9bOcs8QtiplSQ61GZXtd2xHKx4BYRo=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240327114451-d6b7174a84fc h1:t2AT8zb6N/59Y78lyRWedVoVWHNRSCBh0oWCC+bluTQ=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240327114451-d6b7174a84fc/go.mod h1:hNFjhrLUIq+kJ9bOcs8QtiplSQ61GZXtd2xHKx4BYRo=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240711023527-ba358c48772f h1:ZIiIBRvIw62gA5MJhuwp1+2wWbqL9IGElQ499rUsYYg=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240711023527-ba358c48772f/go.mod h1:hNFjhrLUIq+kJ9bOcs8QtiplSQ61GZXtd2xHKx4BYRo=
|
||||
github.com/magefile/mage v1.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo=
|
||||
github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
||||
|
||||
@@ -123,8 +123,7 @@ func (m *RuleMatcher[PluginConfig]) ParseRuleConfig(config gjson.Result,
|
||||
if keyCount == 0 {
|
||||
// enable globally for empty config
|
||||
m.hasGlobalConfig = true
|
||||
parsePluginConfig(config, &m.globalConfig)
|
||||
return nil
|
||||
return parsePluginConfig(config, &m.globalConfig)
|
||||
}
|
||||
if rulesJson, ok := obj[RULES_KEY]; ok {
|
||||
rules = rulesJson.Array()
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
package wrapper
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
|
||||
@@ -81,3 +82,16 @@ func IsBinaryResponseBody() bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func HasRequestBody() bool {
|
||||
contentLengthStr, _ := proxywasm.GetHttpRequestHeader("content-length")
|
||||
if contentLengthStr != "" {
|
||||
contentLength, err := strconv.Atoi(contentLengthStr)
|
||||
if err == nil && contentLength > 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
transferEncodingStr, _ := proxywasm.GetHttpRequestHeader("transfer-encoding")
|
||||
return strings.Contains(transferEncodingStr, "chunked")
|
||||
}
|
||||
|
||||
@@ -165,7 +165,7 @@ impl HttpContextWrapper<RquestBlockConfig> for RquestBlock {
|
||||
}
|
||||
let mut request_url = value.unwrap().clone();
|
||||
|
||||
if config.case_sensitive {
|
||||
if !config.case_sensitive {
|
||||
request_url = request_url.to_lowercase();
|
||||
}
|
||||
for block_exact_url in &config.block_exact_urls {
|
||||
@@ -229,7 +229,7 @@ impl HttpContextWrapper<RquestBlockConfig> for RquestBlock {
|
||||
return Action::Continue;
|
||||
}
|
||||
let mut body = req_body.clone();
|
||||
if config.case_sensitive {
|
||||
if !config.case_sensitive {
|
||||
body = body.to_ascii_lowercase();
|
||||
}
|
||||
for block_body in &config.block_bodies {
|
||||
|
||||
@@ -22,10 +22,10 @@ pub trait RootContextWrapper<PluginConfig>: RootContext
|
||||
where
|
||||
PluginConfig: Default + DeserializeOwned + 'static + Clone,
|
||||
{
|
||||
// fn create_http_context(&self, _context_id: u32) -> Option<Box<dyn HttpContext>> {
|
||||
fn create_http_context_use_wrapper(&self, _context_id: u32) -> Option<Box<dyn HttpContext>> {
|
||||
// fn create_http_context(&self, context_id: u32) -> Option<Box<dyn HttpContext>> {
|
||||
fn create_http_context_use_wrapper(&self, context_id: u32) -> Option<Box<dyn HttpContext>> {
|
||||
// trait 继承没法重写 RootContext 的 create_http_context,先写个函数让上层调下吧
|
||||
match self.create_http_context_wrapper(_context_id) {
|
||||
match self.create_http_context_wrapper(context_id) {
|
||||
Some(http_context) => Some(Box::new(PluginHttpWrapper::new(
|
||||
self.rule_matcher(),
|
||||
http_context,
|
||||
@@ -58,11 +58,17 @@ pub trait HttpContextWrapper<PluginConfig>: HttpContext {
|
||||
fn on_http_response_body_ok(&mut self, _res_body: &Bytes) -> Action {
|
||||
Action::Continue
|
||||
}
|
||||
fn replace_http_request_body(&mut self, body: &[u8]) {
|
||||
self.set_http_request_body(0, i32::MAX as usize, body)
|
||||
}
|
||||
fn replace_http_response_body(&mut self, body: &[u8]) {
|
||||
self.set_http_response_body(0, i32::MAX as usize, body)
|
||||
}
|
||||
}
|
||||
pub struct PluginHttpWrapper<PluginConfig> {
|
||||
req_headers: MultiMap<String, String>,
|
||||
req_body: Bytes,
|
||||
res_body: Bytes,
|
||||
req_body_len: usize,
|
||||
res_body_len: usize,
|
||||
config: Option<PluginConfig>,
|
||||
rule_matcher: SharedRuleMatcher<PluginConfig>,
|
||||
http_content: Box<dyn HttpContextWrapper<PluginConfig>>,
|
||||
@@ -74,25 +80,58 @@ impl<PluginConfig> PluginHttpWrapper<PluginConfig> {
|
||||
) -> Self {
|
||||
PluginHttpWrapper {
|
||||
req_headers: MultiMap::new(),
|
||||
req_body: Bytes::new(),
|
||||
res_body: Bytes::new(),
|
||||
req_body_len: 0,
|
||||
res_body_len: 0,
|
||||
config: None,
|
||||
rule_matcher: rule_matcher.clone(),
|
||||
http_content,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<PluginConfig> Context for PluginHttpWrapper<PluginConfig> {}
|
||||
impl<PluginConfig> Context for PluginHttpWrapper<PluginConfig> {
|
||||
fn on_http_call_response(
|
||||
&mut self,
|
||||
token_id: u32,
|
||||
num_headers: usize,
|
||||
body_size: usize,
|
||||
num_trailers: usize,
|
||||
) {
|
||||
self.http_content
|
||||
.on_http_call_response(token_id, num_headers, body_size, num_trailers)
|
||||
}
|
||||
|
||||
fn on_grpc_call_response(&mut self, token_id: u32, status_code: u32, response_size: usize) {
|
||||
self.http_content
|
||||
.on_grpc_call_response(token_id, status_code, response_size)
|
||||
}
|
||||
fn on_grpc_stream_initial_metadata(&mut self, token_id: u32, num_elements: u32) {
|
||||
self.http_content
|
||||
.on_grpc_stream_initial_metadata(token_id, num_elements)
|
||||
}
|
||||
fn on_grpc_stream_message(&mut self, token_id: u32, message_size: usize) {
|
||||
self.http_content
|
||||
.on_grpc_stream_message(token_id, message_size)
|
||||
}
|
||||
fn on_grpc_stream_trailing_metadata(&mut self, token_id: u32, num_elements: u32) {
|
||||
self.http_content
|
||||
.on_grpc_stream_trailing_metadata(token_id, num_elements)
|
||||
}
|
||||
fn on_grpc_stream_close(&mut self, token_id: u32, status_code: u32) {
|
||||
self.http_content
|
||||
.on_grpc_stream_close(token_id, status_code)
|
||||
}
|
||||
|
||||
fn on_done(&mut self) -> bool {
|
||||
self.http_content.on_done()
|
||||
}
|
||||
}
|
||||
impl<PluginConfig> HttpContext for PluginHttpWrapper<PluginConfig>
|
||||
where
|
||||
PluginConfig: Default + DeserializeOwned + Clone,
|
||||
{
|
||||
fn on_http_request_headers(&mut self, _num_headers: usize, _end_of_stream: bool) -> Action {
|
||||
fn on_http_request_headers(&mut self, num_headers: usize, end_of_stream: bool) -> Action {
|
||||
let binding = self.rule_matcher.borrow();
|
||||
self.config = match binding.get_match_config() {
|
||||
None => None,
|
||||
Some(config) => Some(config.1.clone()),
|
||||
};
|
||||
self.config = binding.get_match_config().map(|config| config.1.clone());
|
||||
for (k, v) in self.get_http_request_headers() {
|
||||
self.req_headers.insert(k, v);
|
||||
}
|
||||
@@ -101,7 +140,7 @@ where
|
||||
}
|
||||
let ret = self
|
||||
.http_content
|
||||
.on_http_request_headers(_num_headers, _end_of_stream);
|
||||
.on_http_request_headers(num_headers, end_of_stream);
|
||||
if ret != Action::Continue {
|
||||
return ret;
|
||||
}
|
||||
@@ -109,53 +148,59 @@ where
|
||||
.on_http_request_headers_ok(&self.req_headers)
|
||||
}
|
||||
|
||||
fn on_http_request_body(&mut self, _body_size: usize, _end_of_stream: bool) -> Action {
|
||||
let mut ret = self
|
||||
fn on_http_request_body(&mut self, body_size: usize, end_of_stream: bool) -> Action {
|
||||
self.req_body_len += body_size;
|
||||
if !end_of_stream {
|
||||
return Action::Pause;
|
||||
}
|
||||
let ret = self
|
||||
.http_content
|
||||
.on_http_request_body(_body_size, _end_of_stream);
|
||||
if !self.http_content.cache_request_body() {
|
||||
.on_http_request_body(self.req_body_len, end_of_stream);
|
||||
if ret != Action::Continue || !self.http_content.cache_request_body() {
|
||||
return ret;
|
||||
}
|
||||
if _body_size > 0 {
|
||||
if let Some(body) = self.get_http_request_body(0, _body_size) {
|
||||
self.req_body.extend(body)
|
||||
let mut req_body = Bytes::new();
|
||||
if self.req_body_len > 0 {
|
||||
if let Some(body) = self.get_http_request_body(0, self.req_body_len) {
|
||||
req_body = body;
|
||||
}
|
||||
}
|
||||
if _end_of_stream && ret == Action::Continue {
|
||||
ret = self.http_content.on_http_request_body_ok(&self.req_body);
|
||||
}
|
||||
ret
|
||||
self.http_content.on_http_request_body_ok(&req_body)
|
||||
}
|
||||
|
||||
fn on_http_request_trailers(&mut self, _num_trailers: usize) -> Action {
|
||||
self.http_content.on_http_request_trailers(_num_trailers)
|
||||
fn on_http_request_trailers(&mut self, num_trailers: usize) -> Action {
|
||||
self.http_content.on_http_request_trailers(num_trailers)
|
||||
}
|
||||
|
||||
fn on_http_response_headers(&mut self, _num_headers: usize, _end_of_stream: bool) -> Action {
|
||||
fn on_http_response_headers(&mut self, num_headers: usize, end_of_stream: bool) -> Action {
|
||||
self.http_content
|
||||
.on_http_response_headers(_num_headers, _end_of_stream)
|
||||
.on_http_response_headers(num_headers, end_of_stream)
|
||||
}
|
||||
|
||||
fn on_http_response_body(&mut self, _body_size: usize, _end_of_stream: bool) -> Action {
|
||||
let mut ret = self
|
||||
fn on_http_response_body(&mut self, body_size: usize, end_of_stream: bool) -> Action {
|
||||
self.res_body_len += body_size;
|
||||
|
||||
if !end_of_stream {
|
||||
return Action::Pause;
|
||||
}
|
||||
let ret = self
|
||||
.http_content
|
||||
.on_http_response_body(_body_size, _end_of_stream);
|
||||
if !self.http_content.cache_response_body() {
|
||||
.on_http_response_body(self.res_body_len, end_of_stream);
|
||||
if ret != Action::Continue || !self.http_content.cache_response_body() {
|
||||
return ret;
|
||||
}
|
||||
if _body_size > 0 {
|
||||
if let Some(body) = self.get_http_response_body(0, _body_size) {
|
||||
self.res_body.extend(body);
|
||||
|
||||
let mut res_body = Bytes::new();
|
||||
if self.res_body_len > 0 {
|
||||
if let Some(body) = self.get_http_response_body(0, self.res_body_len) {
|
||||
res_body = body;
|
||||
}
|
||||
}
|
||||
if _end_of_stream && ret == Action::Continue {
|
||||
ret = self.http_content.on_http_response_body_ok(&self.res_body);
|
||||
}
|
||||
ret
|
||||
self.http_content.on_http_response_body_ok(&res_body)
|
||||
}
|
||||
|
||||
fn on_http_response_trailers(&mut self, _num_trailers: usize) -> Action {
|
||||
self.http_content.on_http_response_trailers(_num_trailers)
|
||||
fn on_http_response_trailers(&mut self, num_trailers: usize) -> Action {
|
||||
self.http_content.on_http_response_trailers(num_trailers)
|
||||
}
|
||||
|
||||
fn on_log(&mut self) {
|
||||
|
||||
Reference in New Issue
Block a user