Compare commits
3
Commits
v0.6.1
..
da7b29d2e3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da7b29d2e3 | ||
|
|
99b551401e | ||
|
|
4e2bab3032 |
+25
-9
@@ -167,21 +167,37 @@ Codex 工具兼容现状:
|
||||
|
||||
### 超大流式请求
|
||||
|
||||
上游流式断流有两个独立触发维度,状态不同:
|
||||
|
||||
> [!WARNING]
|
||||
> 2026-07 实测:OCI xAI 兼容面对**完整请求体超过约 82 KB** 的流式请求,
|
||||
> 可能在推理阶段提前断流;该现象由请求体积触发,与工具构成或字符集无关,
|
||||
> 同一请求改为非流式可正常完成。
|
||||
> **instructions + tools 合计超过约 64.5 KB** 的流式请求,上游会在推理阶段
|
||||
> 静默断连(纯 EOF,无 error / 终态事件);同请求非流式总是成功,`input`
|
||||
> 正文完全不计入。2026-07-13 定位(字节级二分),**2026-07-16 复测仍存在**
|
||||
> (70.4 KB 断 / 59.7 KB 过,Chicago,API Key 与签名行为一致)。
|
||||
|
||||
网关按协议采取不同保护:
|
||||
> [!NOTE]
|
||||
> **完整请求体超过约 82 KB**(含 input)的纯体积断流(2026-07-15 定位)
|
||||
> **已被上游修复**:2026-07-16 复核 83 KB、真实 codex 形态 104.5 KB、200 KB、
|
||||
> 400 KB 流式均正常完成(Chicago 与 Phoenix 两区、签名与 API Key 两路径对照)。
|
||||
|
||||
网关当前行为:
|
||||
|
||||
- **Responses**:兼容改写后的请求体超过 **76 KB** 时,预防性改为非流式上游
|
||||
请求,再合成最小 SSE 序列(`response.created` →
|
||||
`response.output_item.done` → `response.completed`);结果语义保留,但没有增量输出
|
||||
- **Chat Completions / Messages**:若上游在客户端收到任何内容前断流,自动用
|
||||
非流式重做,并合成对应 chunk / event 序列
|
||||
非流式重做,并合成对应 chunk / event 序列(可兜住 64.5 KB 断流)
|
||||
- **Responses**:直通协议中途无法透明重试(客户端已收到事件)。流式保险丝按
|
||||
`instructions + tools` 字节和判定:超过阈值时预防性改非流式上游 + 合成最小
|
||||
SSE 事件序列(`response.created` → `response.output_item.done` →
|
||||
`response.completed`),语义保留但无增量输出。默认开、60 KB,可在
|
||||
**设置 → AI → 流式保险丝** 调整或关闭
|
||||
- **已开始输出的流**:不能透明重试;调用日志会记录提前终止,客户端可能只拿到
|
||||
部分事件
|
||||
|
||||
### grok 服务端搜索工具默认注入
|
||||
|
||||
对 `xai.` 前缀模型的 Responses 请求,网关按开关默认注入 `web_search` /
|
||||
`x_search` 工具;请求 tools 已包含同名工具时保持原样,不覆盖参数。默认双开,
|
||||
可在 **设置 → AI → grok 服务端搜索工具** 关闭。
|
||||
|
||||
### ZDR 与文件输入
|
||||
|
||||
Responses 的 `input_file` 内容块(`file_url` / `file_data`)实测会被上游拒绝:
|
||||
@@ -239,7 +255,7 @@ Codex 工具兼容改写。请求会重新编码,不承诺字节级原样转
|
||||
| `reasoning` | ➡️ | 整个对象保留;`effort` 不校验档位 |
|
||||
| `tool_choice` | ◐ | 普通形态保留;namespace 对象会重限定,全部工具被剥离时删除 |
|
||||
| `store` | 🔄 | 无论客户端传什么,上游请求都强制改写为 `false` |
|
||||
| `stream` | ◐ | 支持 SSE;请求体超过 76 KB 时改为非流式上游并合成 SSE |
|
||||
| `stream` | ◐ | 支持 SSE;网关按 instructions+tools 字节和触发预防性非流式回退(保险丝,默认开 60 KB,见[已知限制](#limitations)) |
|
||||
| 其余标准与未知顶层字段 | ➡️ | `context_management`、`include`、`metadata`、`prompt`、`prompt_cache_key`、`service_tier`、`truncation`、`user` 等均保留,由 OCI 决定是否接受 |
|
||||
|
||||
</details>
|
||||
|
||||
+68
-1
@@ -786,6 +786,27 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/ai-model-catalog": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"AI 管理"
|
||||
],
|
||||
"summary": "聚合模型目录",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/internal_api.itemsResponse-oci-portal_internal_service_AggregatedModel"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/ai-models": {
|
||||
"get": {
|
||||
"security": [
|
||||
@@ -839,7 +860,7 @@ const docTemplate = `{
|
||||
"summary": "更新 AI 网关全局设置",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "开启后已宣布弃用(即使未退役)的模型从列表与路由中排除",
|
||||
"description": "全量提交;保险丝阈值限 1..1024 KB",
|
||||
"name": "body",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
@@ -854,6 +875,15 @@ const docTemplate = `{
|
||||
"schema": {
|
||||
"$ref": "#/definitions/internal_api.aiSettingsResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6050,7 +6080,22 @@ const docTemplate = `{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"filterDeprecated": {
|
||||
"description": "FilterDeprecated 开启后已宣布弃用(即使未退役)的模型从列表与路由中排除",
|
||||
"type": "boolean"
|
||||
},
|
||||
"grokWebSearch": {
|
||||
"description": "GrokWebSearch / GrokXSearch 是 xai. 模型服务端搜索工具默认注入开关;\n请求 tools 已包含同名工具时不覆盖",
|
||||
"type": "boolean"
|
||||
},
|
||||
"grokXSearch": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"streamGuardEnabled": {
|
||||
"description": "StreamGuardEnabled / StreamGuardKB 是 Responses 流式保险丝:\ninstructions+tools 合计超阈值(KB)的流式请求改非流式上游并合成 SSE",
|
||||
"type": "boolean"
|
||||
},
|
||||
"streamGuardKB": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -6621,6 +6666,17 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"internal_api.itemsResponse-oci-portal_internal_service_AggregatedModel": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/oci-portal_internal_service.AggregatedModel"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"internal_api.itemsResponse-oci-portal_internal_service_NotifyChannelView": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -9358,6 +9414,17 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"oci-portal_internal_service.AggregatedModel": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"capability": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"oci-portal_internal_service.AuditEventsView": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
+68
-1
@@ -779,6 +779,27 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/ai-model-catalog": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"BearerAuth": []
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"AI 管理"
|
||||
],
|
||||
"summary": "聚合模型目录",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/internal_api.itemsResponse-oci-portal_internal_service_AggregatedModel"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/ai-models": {
|
||||
"get": {
|
||||
"security": [
|
||||
@@ -832,7 +853,7 @@
|
||||
"summary": "更新 AI 网关全局设置",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "开启后已宣布弃用(即使未退役)的模型从列表与路由中排除",
|
||||
"description": "全量提交;保险丝阈值限 1..1024 KB",
|
||||
"name": "body",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
@@ -847,6 +868,15 @@
|
||||
"schema": {
|
||||
"$ref": "#/definitions/internal_api.aiSettingsResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6043,7 +6073,22 @@
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"filterDeprecated": {
|
||||
"description": "FilterDeprecated 开启后已宣布弃用(即使未退役)的模型从列表与路由中排除",
|
||||
"type": "boolean"
|
||||
},
|
||||
"grokWebSearch": {
|
||||
"description": "GrokWebSearch / GrokXSearch 是 xai. 模型服务端搜索工具默认注入开关;\n请求 tools 已包含同名工具时不覆盖",
|
||||
"type": "boolean"
|
||||
},
|
||||
"grokXSearch": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"streamGuardEnabled": {
|
||||
"description": "StreamGuardEnabled / StreamGuardKB 是 Responses 流式保险丝:\ninstructions+tools 合计超阈值(KB)的流式请求改非流式上游并合成 SSE",
|
||||
"type": "boolean"
|
||||
},
|
||||
"streamGuardKB": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -6614,6 +6659,17 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"internal_api.itemsResponse-oci-portal_internal_service_AggregatedModel": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/oci-portal_internal_service.AggregatedModel"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"internal_api.itemsResponse-oci-portal_internal_service_NotifyChannelView": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -9351,6 +9407,17 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"oci-portal_internal_service.AggregatedModel": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"capability": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"oci-portal_internal_service.AuditEventsView": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
+48
-1
@@ -49,7 +49,22 @@ definitions:
|
||||
internal_api.aiSettingsResponse:
|
||||
properties:
|
||||
filterDeprecated:
|
||||
description: FilterDeprecated 开启后已宣布弃用(即使未退役)的模型从列表与路由中排除
|
||||
type: boolean
|
||||
grokWebSearch:
|
||||
description: |-
|
||||
GrokWebSearch / GrokXSearch 是 xai. 模型服务端搜索工具默认注入开关;
|
||||
请求 tools 已包含同名工具时不覆盖
|
||||
type: boolean
|
||||
grokXSearch:
|
||||
type: boolean
|
||||
streamGuardEnabled:
|
||||
description: |-
|
||||
StreamGuardEnabled / StreamGuardKB 是 Responses 流式保险丝:
|
||||
instructions+tools 合计超阈值(KB)的流式请求改非流式上游并合成 SSE
|
||||
type: boolean
|
||||
streamGuardKB:
|
||||
type: integer
|
||||
type: object
|
||||
internal_api.attachBootVolumeRequest:
|
||||
properties:
|
||||
@@ -427,6 +442,13 @@ definitions:
|
||||
$ref: '#/definitions/oci-portal_internal_model.UserIdentity'
|
||||
type: array
|
||||
type: object
|
||||
internal_api.itemsResponse-oci-portal_internal_service_AggregatedModel:
|
||||
properties:
|
||||
items:
|
||||
items:
|
||||
$ref: '#/definitions/oci-portal_internal_service.AggregatedModel'
|
||||
type: array
|
||||
type: object
|
||||
internal_api.itemsResponse-oci-portal_internal_service_NotifyChannelView:
|
||||
properties:
|
||||
items:
|
||||
@@ -2232,6 +2254,13 @@ definitions:
|
||||
description: 卷本身的名称(attachment 名是自动生成的)
|
||||
type: string
|
||||
type: object
|
||||
oci-portal_internal_service.AggregatedModel:
|
||||
properties:
|
||||
capability:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
type: object
|
||||
oci-portal_internal_service.AuditEventsView:
|
||||
properties:
|
||||
cursor:
|
||||
@@ -3135,6 +3164,18 @@ paths:
|
||||
summary: AI 调用日志列表
|
||||
tags:
|
||||
- AI 管理
|
||||
/api/v1/ai-model-catalog:
|
||||
get:
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/internal_api.itemsResponse-oci-portal_internal_service_AggregatedModel'
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: 聚合模型目录
|
||||
tags:
|
||||
- AI 管理
|
||||
/api/v1/ai-models:
|
||||
get:
|
||||
responses:
|
||||
@@ -3161,7 +3202,7 @@ paths:
|
||||
- AI 管理
|
||||
put:
|
||||
parameters:
|
||||
- description: 开启后已宣布弃用(即使未退役)的模型从列表与路由中排除
|
||||
- description: 全量提交;保险丝阈值限 1..1024 KB
|
||||
in: body
|
||||
name: body
|
||||
required: true
|
||||
@@ -3172,6 +3213,12 @@ paths:
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/internal_api.aiSettingsResponse'
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: 更新 AI 网关全局设置
|
||||
|
||||
@@ -8,7 +8,7 @@ require (
|
||||
github.com/glebarez/sqlite v1.11.0
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/oracle/oci-go-sdk/v65 v65.120.0
|
||||
github.com/oracle/oci-go-sdk/v65 v65.121.0
|
||||
github.com/pquerna/otp v1.5.0
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/swaggo/files v1.0.1
|
||||
|
||||
@@ -1,40 +1,24 @@
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
|
||||
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
|
||||
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||
github.com/PuerkitoBio/purell v1.2.2 h1:irlAloIzZaJ5RP/+UcaT1Nw0H9on2HKWdRehCsbJWJw=
|
||||
github.com/PuerkitoBio/purell v1.2.2/go.mod h1:ZwHcC/82TOaovDi//J/804umJFFmbOHPngi8iYYv/Eo=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI=
|
||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
github.com/boombuler/barcode v1.1.0 h1:ChaYjBR63fr4LFyGn8E8nt7dBSt3MiU3zMOZqFvVkHo=
|
||||
github.com/boombuler/barcode v1.1.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
||||
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
||||
github.com/bytedance/gopkg v0.1.4 h1:oZnQwnX82KAIWb7033bEwtxvTqXcYMxDBaQxo5JJHWM=
|
||||
github.com/bytedance/gopkg v0.1.4/go.mod h1:v1zWfPm21Fb+OsyXN2VAHdL6TBb2L88anLQgdyje6R4=
|
||||
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
|
||||
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
|
||||
github.com/bytedance/sonic v1.15.2 h1:90H+rcF/FwLXwfB1cudOLq/je83n683Utf4Cbp0xHCo=
|
||||
github.com/bytedance/sonic v1.15.2/go.mod h1:mT2NbXunuaEbnZ+mRIX/vYqKISmgEuHFDI4UzmKx2SA=
|
||||
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
|
||||
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
||||
github.com/bytedance/sonic/loader v0.5.1 h1:Ygpfa9zwRCCKSlrp5bBP/b/Xzc3VxsAW+5NIYXrOOpI=
|
||||
github.com/bytedance/sonic/loader v0.5.1/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||
github.com/cloudwego/base64x v0.1.7 h1:NppS+Fgzg5ovhn4NkUXaDT3x9jldgH5ToMCqzBSi2zI=
|
||||
github.com/cloudwego/base64x v0.1.7/go.mod h1:Cu1PV9zfrSf7ET2tIbWbbEy7jO7HHJ13q4X2SQ8aWYg=
|
||||
github.com/coreos/go-oidc/v3 v3.19.0 h1:F/xyOi3x1UnG1U27YVnM1N6bHiL1K2upi6U/0qr8r+I=
|
||||
github.com/coreos/go-oidc/v3 v3.19.0/go.mod h1:DYCf24+ncYi+XkIH97GY1+dqoRlbaSI26KVTCI9SrY4=
|
||||
github.com/coreos/go-oidc/v3 v3.20.0 h1:EtE0WIBHk03N+DqGkY4+UONzzZHk7amKt6IyNd7OsZE=
|
||||
github.com/coreos/go-oidc/v3 v3.20.0/go.mod h1:DYCf24+ncYi+XkIH97GY1+dqoRlbaSI26KVTCI9SrY4=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
@@ -45,20 +29,14 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
|
||||
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
|
||||
github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
|
||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||
github.com/gin-contrib/sse v1.1.1 h1:uGYpNwTacv5R68bSGMapo62iLTRa9l5zxGCps4hK6ko=
|
||||
github.com/gin-contrib/sse v1.1.1/go.mod h1:QXzuVkA0YO7o/gun03UI1Q+FTI8ZV/n5t03kIQAI89s=
|
||||
github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
|
||||
github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
|
||||
github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
|
||||
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
|
||||
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
|
||||
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
|
||||
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
|
||||
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
|
||||
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
|
||||
@@ -66,35 +44,13 @@ github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9
|
||||
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
|
||||
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
github.com/go-openapi/jsonpointer v1.0.0 h1:kR9tHqY0CtZaOPVFm622dPVNhrvYpwr4uCxgL3h1H8s=
|
||||
github.com/go-openapi/jsonpointer v1.0.0/go.mod h1:Z3rw7dWu1p9IgitXCFamSlA5lmDiklEB6vkaxcNZW5Y=
|
||||
github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs=
|
||||
github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns=
|
||||
github.com/go-openapi/jsonreference v1.0.0 h1:jlmTr6torcd1YgDQvSfNmRtKzYDO4FGBkrAdlAVWnpY=
|
||||
github.com/go-openapi/jsonreference v1.0.0/go.mod h1:jtwdyGbJk0Xhe5Y+rwtglQP6Sb1WZST4rT32LWB+sv0=
|
||||
github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M=
|
||||
github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I=
|
||||
github.com/go-openapi/spec v0.22.6 h1:Tyy1pLaNCM8GBCFLoGYLonjJi6zykqyLCjXLc19ZPic=
|
||||
github.com/go-openapi/spec v0.22.6/go.mod h1:HZvTHat+iH0PALQRWhrqIHtU/PEqxqd89fu0MxGlMeM=
|
||||
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
|
||||
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
|
||||
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
|
||||
github.com/go-openapi/swag v0.27.0 h1:8ecSuZlh4NXc3GsmAOqECIYqDTApCWaMe3gO4gjJNEE=
|
||||
github.com/go-openapi/swag v0.27.0/go.mod h1:Kkgz9Ht0+ul9/aVdFmc9xSyPzUwf/aFF5KiFPBXfSY0=
|
||||
github.com/go-openapi/swag/conv v0.27.0 h1:EKOH4feXrvdo8DbSsXSAqRT8fz1epEnS5O2IfXUOzE8=
|
||||
github.com/go-openapi/swag/conv v0.27.0/go.mod h1:pfiv0uKQTbaGApk8Zs/lZV3uSjmSpa2FO1y183YngN8=
|
||||
github.com/go-openapi/swag/jsonname v0.27.0 h1:4QVB//CKOdE8IOiBg19JNY2wfDS48MhesIquYBy2rUE=
|
||||
github.com/go-openapi/swag/jsonname v0.27.0/go.mod h1:I1YsyvvhBuZsFXSW6I7ODfdyq13p7hDil//1T9/pFFk=
|
||||
github.com/go-openapi/swag/jsonutils v0.27.0 h1:VYtd9jEQYeU4j8q5vdn5KWotF4vKywhGdMBrALtAsfE=
|
||||
github.com/go-openapi/swag/jsonutils v0.27.0/go.mod h1:U7pb8AGuwhok3RDicHeHwSG4L3PXSq6PAL98Aon632g=
|
||||
github.com/go-openapi/swag/loading v0.27.0 h1:s8DA9aPEdFH6OluHUYUn3DnIuoTdyWs9RwffXBUfyeI=
|
||||
github.com/go-openapi/swag/loading v0.27.0/go.mod h1:VOz+Jg6UGGywcmRvYsI4fvtp+bd7NfioseGEPleYdA4=
|
||||
github.com/go-openapi/swag/stringutils v0.27.0 h1:Of7w/HljWsNZvuxsUAnw3n+hCOyI6HLJOxW2kQRAxio=
|
||||
github.com/go-openapi/swag/stringutils v0.27.0/go.mod h1:lzRN95CxXmA03XcDWHLOb6nOMcxCqR5rGY0lOgsfRoM=
|
||||
github.com/go-openapi/swag/typeutils v0.27.0 h1:aCf4MSGo8NLwZP8Q6t32DWLJSvl/WwNqgmEG+xJ6v2o=
|
||||
github.com/go-openapi/swag/typeutils v0.27.0/go.mod h1:Srm0xFNRZ1Y+vCxJclo5qzx8aj+1pAKda/YfFPrG0dQ=
|
||||
github.com/go-openapi/swag/yamlutils v0.27.0 h1:bQ6eAMil5X9tdcf7dMn4t15alzG6jddnrKPuKa/zxKM=
|
||||
github.com/go-openapi/swag/yamlutils v0.27.0/go.mod h1:yRfIo7qqVkmJRQjX8exjA3AfcI8rH1KDNPsTparoCv4=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
@@ -103,22 +59,14 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
|
||||
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
|
||||
github.com/go-playground/validator/v10 v10.30.3 h1:4MU6YkEwx7GbcPJOZxrtbu+QfF3pJLJuaYTeAH0DYy8=
|
||||
github.com/go-playground/validator/v10 v10.30.3/go.mod h1:4Axh7oCNGcoGkqLoE4YWt6n20mcEIsPRlB7vPk3lpyc=
|
||||
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||
github.com/go-sql-driver/mysql v1.10.0 h1:Q+1LV8DkHJvSYAdR83XzuhDaTykuDx0l6fkXxoWCWfw=
|
||||
github.com/go-sql-driver/mysql v1.10.0/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF0pZej+SK+nWk=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
|
||||
github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/gofrs/flock v0.10.0 h1:SHMXenfaB03KbroETaCMtbBg3Yn29v4w1r+tgy4ff4k=
|
||||
github.com/gofrs/flock v0.10.0/go.mod h1:FirDy1Ing0mI2+kB6wk+vyyAH+e6xiE+EYA0jnzV9jc=
|
||||
github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
|
||||
github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
@@ -126,11 +74,8 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||
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/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
@@ -139,8 +84,6 @@ github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7Ulw
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw=
|
||||
github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
|
||||
github.com/jackc/pgx/v5 v5.10.0 h1:VhSvgU2jSli8o3AqIEOTJr7rZwAEUVo4E4XhR94Zfr0=
|
||||
github.com/jackc/pgx/v5 v5.10.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
@@ -153,8 +96,6 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw=
|
||||
github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
@@ -168,12 +109,8 @@ github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN
|
||||
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
|
||||
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mailru/easyjson v0.9.2 h1:dX8U45hQsZpxd80nLvDGihsQ/OxlvTkVUXH2r/8cb2M=
|
||||
github.com/mailru/easyjson v0.9.2/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
|
||||
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
|
||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
@@ -181,15 +118,11 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/oracle/oci-go-sdk/v65 v65.120.0 h1:qpGdts2Yleg6TdmtxXkL8MAnsASO+SOG+iG/Nd8wUGk=
|
||||
github.com/oracle/oci-go-sdk/v65 v65.120.0/go.mod h1:Pzy+BpgkDesvGZXEHgslwhIYobHCPHg6wRta1mWnlqQ=
|
||||
github.com/oracle/oci-go-sdk/v65 v65.121.0 h1:1J+5ARgrodrx8kzFy/hxznaoUzz43jr0EestCzEaOHw=
|
||||
github.com/oracle/oci-go-sdk/v65 v65.121.0/go.mod h1:Pzy+BpgkDesvGZXEHgslwhIYobHCPHg6wRta1mWnlqQ=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pelletier/go-toml/v2 v2.4.3 h1:GTRvJQutkOSftxIFD5xw9aepkYNuPWmVJpffdDPYVpY=
|
||||
github.com/pelletier/go-toml/v2 v2.4.3/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
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/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=
|
||||
@@ -198,8 +131,6 @@ github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||
github.com/quic-go/quic-go v0.59.1 h1:0Gmua0HW1Tv7ANR7hUYwRyD0MG5OJfgvYSZasGZzBic=
|
||||
github.com/quic-go/quic-go v0.59.1/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
||||
github.com/quic-go/quic-go v0.60.0 h1:xcQioE8OM66UQLeUMHltK1CCcOu3JbVB4JAQdDQSB+0=
|
||||
github.com/quic-go/quic-go v0.60.0/go.mod h1:wpKpjmPpftl30sL6pFh7REVpjbcCVy4zt2vDyK1TuJk=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
@@ -244,27 +175,17 @@ github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfS
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
|
||||
go.mongodb.org/mongo-driver/v2 v2.8.0 h1:CxWDGQYY8QQwNjAl/aq2sfWakdnWZynnqJ9F4DhHbP8=
|
||||
go.mongodb.org/mongo-driver/v2 v2.8.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
|
||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
|
||||
golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
||||
golang.org/x/arch v0.29.0 h1:8sSET5wB0+exBm0FGmOtdHMqjlRdV2DRD3/IV6OZgho=
|
||||
golang.org/x/arch v0.29.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
|
||||
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
|
||||
golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
|
||||
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
|
||||
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
|
||||
golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk=
|
||||
golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
|
||||
@@ -272,16 +193,12 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
|
||||
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
|
||||
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
|
||||
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
|
||||
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
||||
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
|
||||
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
|
||||
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -292,14 +209,11 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
|
||||
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
|
||||
golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
|
||||
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
@@ -307,8 +221,6 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
|
||||
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
|
||||
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
|
||||
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
|
||||
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@@ -316,13 +228,9 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
|
||||
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
|
||||
golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE=
|
||||
golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
@@ -346,19 +254,11 @@ gorm.io/gorm v1.31.2 h1:3o8FXNo9v9S858gil+3LlZA1LkCOzgb4g5BL64FgaCo=
|
||||
gorm.io/gorm v1.31.2/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
||||
modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE=
|
||||
modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY=
|
||||
modernc.org/libc v1.74.1 h1:bdR4VTKFMC4966QSNZ05XLGI/VwzVa2kTUX51Dm0riQ=
|
||||
modernc.org/libc v1.74.1/go.mod h1:uH4t5bOx3G3g9Xcmj10YKlTcVISlRDwv8VoQJG9n8Os=
|
||||
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
|
||||
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
|
||||
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
|
||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||
modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM=
|
||||
modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk=
|
||||
modernc.org/sqlite v1.53.0 h1:20WG8N9q4ji/dEqGk4uiI0c6OPjSeLTNYGFCc3+7c1M=
|
||||
modernc.org/sqlite v1.53.0/go.mod h1:xoEpOIpGrgT48H5iiyt/YXPCZPEzlfmfFwtk8Lklw8s=
|
||||
sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
|
||||
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
|
||||
|
||||
+57
-5
@@ -422,9 +422,47 @@ func aiPathID(c *gin.Context) (uint, bool) {
|
||||
return uint(id), true
|
||||
}
|
||||
|
||||
// modelCatalog 返回启用渠道聚合去重后的模型目录(含能力),黑名单添加弹窗用。
|
||||
//
|
||||
// @Summary 聚合模型目录
|
||||
// @Tags AI 管理
|
||||
// @Success 200 {object} itemsResponse[service.AggregatedModel]
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/ai-model-catalog [get]
|
||||
func (h *aiAdminHandler) modelCatalog(c *gin.Context) {
|
||||
items, err := h.gw.AggregatedModels(c.Request.Context())
|
||||
if err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"items": items})
|
||||
}
|
||||
|
||||
// aiSettingsResponse 是 AI 网关全局设置(文档与响应共用)。
|
||||
type aiSettingsResponse struct {
|
||||
// FilterDeprecated 开启后已宣布弃用(即使未退役)的模型从列表与路由中排除
|
||||
FilterDeprecated bool `json:"filterDeprecated"`
|
||||
// StreamGuardEnabled / StreamGuardKB 是 Responses 流式保险丝:
|
||||
// instructions+tools 合计超阈值(KB)的流式请求改非流式上游并合成 SSE
|
||||
StreamGuardEnabled bool `json:"streamGuardEnabled"`
|
||||
StreamGuardKB int `json:"streamGuardKB"`
|
||||
// GrokWebSearch / GrokXSearch 是 xai. 模型服务端搜索工具默认注入开关;
|
||||
// 请求 tools 已包含同名工具时不覆盖
|
||||
GrokWebSearch bool `json:"grokWebSearch"`
|
||||
GrokXSearch bool `json:"grokXSearch"`
|
||||
}
|
||||
|
||||
// currentAiSettings 汇总网关运行时设置为响应体。
|
||||
func (h *aiAdminHandler) currentAiSettings() aiSettingsResponse {
|
||||
guardOn, guardKB := h.gw.StreamGuard()
|
||||
web, x := h.gw.GrokSearch()
|
||||
return aiSettingsResponse{
|
||||
FilterDeprecated: h.gw.FilterDeprecated(),
|
||||
StreamGuardEnabled: guardOn,
|
||||
StreamGuardKB: guardKB,
|
||||
GrokWebSearch: web,
|
||||
GrokXSearch: x,
|
||||
}
|
||||
}
|
||||
|
||||
// aiSettings 返回 AI 网关全局设置。
|
||||
@@ -435,15 +473,16 @@ type aiSettingsResponse struct {
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/ai-settings [get]
|
||||
func (h *aiAdminHandler) aiSettings(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, aiSettingsResponse{FilterDeprecated: h.gw.FilterDeprecated()})
|
||||
c.JSON(http.StatusOK, h.currentAiSettings())
|
||||
}
|
||||
|
||||
// updateAiSettings 更新 AI 网关全局设置(当前仅「过滤弃用模型」开关)。
|
||||
// updateAiSettings 更新 AI 网关全局设置(过滤弃用/流式保险丝/grok 搜索工具默认注入)。
|
||||
//
|
||||
// @Summary 更新 AI 网关全局设置
|
||||
// @Tags AI 管理
|
||||
// @Param body body aiSettingsResponse true "开启后已宣布弃用(即使未退役)的模型从列表与路由中排除"
|
||||
// @Param body body aiSettingsResponse true "全量提交;保险丝阈值限 1..1024 KB"
|
||||
// @Success 200 {object} aiSettingsResponse
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/ai-settings [put]
|
||||
func (h *aiAdminHandler) updateAiSettings(c *gin.Context) {
|
||||
@@ -452,9 +491,22 @@ func (h *aiAdminHandler) updateAiSettings(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if err := h.gw.SetFilterDeprecated(c.Request.Context(), req.FilterDeprecated); err != nil {
|
||||
if req.StreamGuardKB < 1 || req.StreamGuardKB > 1024 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "streamGuardKB 须在 1..1024"})
|
||||
return
|
||||
}
|
||||
ctx := c.Request.Context()
|
||||
if err := h.gw.SetFilterDeprecated(ctx, req.FilterDeprecated); err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, aiSettingsResponse{FilterDeprecated: h.gw.FilterDeprecated()})
|
||||
if err := h.gw.SetStreamGuard(ctx, req.StreamGuardEnabled, req.StreamGuardKB); err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
if err := h.gw.SetGrokSearch(ctx, req.GrokWebSearch, req.GrokXSearch); err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, h.currentAiSettings())
|
||||
}
|
||||
|
||||
@@ -587,8 +587,13 @@ func (h *aiGatewayHandler) responsesPassthrough(c *gin.Context, raw []byte, req
|
||||
return
|
||||
}
|
||||
logRespCompat(req.Model, compat)
|
||||
web, x := h.gw.GrokSearch()
|
||||
if injectedBody, injected := service.RespInjectGrokTools(body, req.Model, web, x); len(injected) > 0 {
|
||||
body = injectedBody
|
||||
log.Printf("responses 直通(model=%s): 默认注入 %s", req.Model, strings.Join(injected, ", "))
|
||||
}
|
||||
if req.Stream {
|
||||
if len(body) > service.RespStreamUpgradeLimit {
|
||||
if on, kb := h.gw.StreamGuard(); on && service.RespGuardBytes(body) > kb*1024 {
|
||||
h.responsesStreamUpgrade(c, body, req, compat)
|
||||
return
|
||||
}
|
||||
@@ -630,9 +635,9 @@ func logRespCompat(model string, compat service.RespCompat) {
|
||||
log.Printf("responses 直通(model=%s): %s", model, strings.Join(parts, "; "))
|
||||
}
|
||||
|
||||
// responsesStreamUpgrade 流式升级回退:上游对超大流式请求会在推理途中掐断
|
||||
// (实测 >~82KB,见 RespStreamUpgradeLimit),超限时改调非流式上游拿完整响应,
|
||||
// 本地合成最小 SSE 事件序列回给客户端;丢失增量输出,换会话不中断。
|
||||
// responsesStreamUpgrade 流式升级回退:instructions+tools 合计超过保险丝阈值
|
||||
// (设置页 AI Tab 配置,默认开 60KB;上游对 >≈64.5KB 会静默断流)时改调非流式
|
||||
// 上游拿完整响应,本地合成最小 SSE 事件序列回给客户端;丢失增量输出,换会话不中断。
|
||||
func (h *aiGatewayHandler) responsesStreamUpgrade(c *gin.Context, body []byte, req aiwire.RespRequest, compat service.RespCompat) {
|
||||
start := time.Now()
|
||||
nsBody, err := service.RespDisableStream(body)
|
||||
@@ -640,7 +645,8 @@ func (h *aiGatewayHandler) responsesStreamUpgrade(c *gin.Context, body []byte, r
|
||||
aiError(c, http.StatusBadRequest, "invalid_request_error", err.Error())
|
||||
return
|
||||
}
|
||||
log.Printf("responses 直通(model=%s): 请求体 %dKB 超流式安全上限,改走非流式合成 SSE", req.Model, len(body)/1024)
|
||||
log.Printf("responses 直通(model=%s): instructions+tools %dKB 超保险丝阈值,改走非流式合成 SSE",
|
||||
req.Model, service.RespGuardBytes(body)/1024)
|
||||
payload, meta, err := h.gw.RespPassthrough(c.Request.Context(), nsBody, req.Model, keyGroup(c))
|
||||
entry := h.logEntry(c, "responses", req.Model, true, meta, start)
|
||||
if err != nil {
|
||||
|
||||
@@ -39,6 +39,7 @@ func registerAiAdmin(secured *gin.RouterGroup, aiGateway *service.AiGatewayServi
|
||||
secured.GET("/ai-channels/:id/models", aiadmin.listChannelModels)
|
||||
secured.POST("/ai-channels/:id/test-model", aiadmin.testChannelModel)
|
||||
secured.GET("/ai-models", aiadmin.gatewayModels)
|
||||
secured.GET("/ai-model-catalog", aiadmin.modelCatalog)
|
||||
secured.GET("/ai-settings", aiadmin.aiSettings)
|
||||
secured.PUT("/ai-settings", aiadmin.updateAiSettings)
|
||||
secured.GET("/ai-blacklist", aiadmin.listBlacklist)
|
||||
|
||||
@@ -397,9 +397,10 @@ func fillInstanceIPs(ctx context.Context, cc core.ComputeClient, vn core.Virtual
|
||||
return
|
||||
}
|
||||
var (
|
||||
wg sync.WaitGroup
|
||||
mu sync.Mutex
|
||||
sem = make(chan struct{}, 8)
|
||||
wg sync.WaitGroup
|
||||
mu sync.Mutex
|
||||
sem = make(chan struct{}, 8)
|
||||
primarySeen = make(map[*Instance]bool)
|
||||
)
|
||||
for _, att := range attResp.Items {
|
||||
inst, ok := active[deref(att.InstanceId)]
|
||||
@@ -416,16 +417,30 @@ func fillInstanceIPs(ctx context.Context, cc core.ComputeClient, vn core.Virtual
|
||||
return
|
||||
}
|
||||
mu.Lock()
|
||||
inst.SubnetID = deref(vnicResp.SubnetId)
|
||||
inst.PrivateIP = deref(vnicResp.PrivateIp)
|
||||
inst.PublicIP = deref(vnicResp.PublicIp)
|
||||
inst.Ipv6Addresses = vnicResp.Ipv6Addresses
|
||||
applyVnicAddrs(inst, vnicResp.Vnic, primarySeen)
|
||||
mu.Unlock()
|
||||
}(att.VnicId, inst)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// applyVnicAddrs 将 VNIC 地址写入实例。多网卡实例以主网卡为准:
|
||||
// 主网卡返回前先用先到的网卡兜底,主网卡到达后覆盖并锁定,避免并发
|
||||
// 完成顺序决定展示结果。调用方需持有保护 inst 与 primarySeen 的锁。
|
||||
func applyVnicAddrs(inst *Instance, v core.Vnic, primarySeen map[*Instance]bool) {
|
||||
isPrimary := v.IsPrimary != nil && *v.IsPrimary
|
||||
if !isPrimary && (primarySeen[inst] || inst.SubnetID != "") {
|
||||
return
|
||||
}
|
||||
inst.SubnetID = deref(v.SubnetId)
|
||||
inst.PrivateIP = deref(v.PrivateIp)
|
||||
inst.PublicIP = deref(v.PublicIp)
|
||||
inst.Ipv6Addresses = v.Ipv6Addresses
|
||||
if isPrimary {
|
||||
primarySeen[inst] = true
|
||||
}
|
||||
}
|
||||
|
||||
func toInstance(inst core.Instance) Instance {
|
||||
out := Instance{
|
||||
ID: deref(inst.Id),
|
||||
|
||||
@@ -5,9 +5,51 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/oracle/oci-go-sdk/v65/common"
|
||||
"github.com/oracle/oci-go-sdk/v65/core"
|
||||
)
|
||||
|
||||
func TestApplyVnicAddrs(t *testing.T) {
|
||||
primary := core.Vnic{
|
||||
IsPrimary: common.Bool(true),
|
||||
SubnetId: common.String("sub-a"),
|
||||
PrivateIp: common.String("10.0.0.2"),
|
||||
PublicIp: common.String("1.1.1.1"),
|
||||
}
|
||||
secondary := core.Vnic{
|
||||
IsPrimary: common.Bool(false),
|
||||
SubnetId: common.String("sub-b"),
|
||||
PrivateIp: common.String("10.0.0.9"),
|
||||
}
|
||||
secondary2 := core.Vnic{
|
||||
SubnetId: common.String("sub-c"),
|
||||
PrivateIp: common.String("10.0.0.7"),
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
order []core.Vnic
|
||||
wantPrivate string
|
||||
wantSubnet string
|
||||
}{
|
||||
{name: "主卡先到不被次卡覆盖", order: []core.Vnic{primary, secondary}, wantPrivate: "10.0.0.2", wantSubnet: "sub-a"},
|
||||
{name: "次卡先兜底主卡后覆盖", order: []core.Vnic{secondary, primary}, wantPrivate: "10.0.0.2", wantSubnet: "sub-a"},
|
||||
{name: "无主卡时先到者保留", order: []core.Vnic{secondary, secondary2}, wantPrivate: "10.0.0.9", wantSubnet: "sub-b"},
|
||||
{name: "仅次卡也能兜底", order: []core.Vnic{secondary2}, wantPrivate: "10.0.0.7", wantSubnet: "sub-c"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
inst := &Instance{}
|
||||
seen := make(map[*Instance]bool)
|
||||
for _, v := range tt.order {
|
||||
applyVnicAddrs(inst, v, seen)
|
||||
}
|
||||
if inst.PrivateIP != tt.wantPrivate || inst.SubnetID != tt.wantSubnet {
|
||||
t.Errorf("got (%q, %q), want (%q, %q)", inst.PrivateIP, inst.SubnetID, tt.wantPrivate, tt.wantSubnet)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestShellSingleQuote(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
+135
-12
@@ -10,6 +10,7 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
@@ -61,20 +62,72 @@ type AiGatewayService struct {
|
||||
onChannelsChanged func(context.Context)
|
||||
// filterDeprecated 是「过滤弃用模型」开关(内存镜像,持久化在 settings 表)
|
||||
filterDeprecated atomic.Bool
|
||||
// streamGuard* 是 Responses 流式保险丝(instructions+tools 合计超阈值改非流式)
|
||||
streamGuardEnabled atomic.Bool
|
||||
streamGuardKB atomic.Int64
|
||||
// grokWebSearch / grokXSearch 是 xai. 模型服务端搜索工具默认注入开关
|
||||
grokWebSearch atomic.Bool
|
||||
grokXSearch atomic.Bool
|
||||
}
|
||||
|
||||
// NewAiGatewayService 组装依赖;调用 StartCleanup 后开始调用日志周期清理。
|
||||
func NewAiGatewayService(db *gorm.DB, configs *OciConfigService, client oci.Client) *AiGatewayService {
|
||||
s := &AiGatewayService{db: db, configs: configs, client: client, lastTouch: map[uint]time.Time{}}
|
||||
var row model.Setting
|
||||
if err := db.Where("key = ?", settingAiFilterDeprecated).First(&row).Error; err == nil {
|
||||
s.filterDeprecated.Store(row.Value == "1")
|
||||
}
|
||||
s.filterDeprecated.Store(loadBoolSetting(db, settingAiFilterDeprecated, false))
|
||||
s.streamGuardEnabled.Store(loadBoolSetting(db, settingAiStreamGuardEnabled, true))
|
||||
s.streamGuardKB.Store(int64(loadIntSetting(db, settingAiStreamGuardKB, defaultStreamGuardKB)))
|
||||
s.grokWebSearch.Store(loadBoolSetting(db, settingAiGrokWebSearch, true))
|
||||
s.grokXSearch.Store(loadBoolSetting(db, settingAiGrokXSearch, true))
|
||||
return s
|
||||
}
|
||||
|
||||
// settingAiFilterDeprecated 是「过滤弃用模型」开关的配置键,值 "1"/"0",缺省关闭。
|
||||
const settingAiFilterDeprecated = "ai_filter_deprecated"
|
||||
// AI 网关运行时设置的配置键;bool 值存 "1"/"0"。
|
||||
const (
|
||||
// settingAiFilterDeprecated 是「过滤弃用模型」开关,缺省关闭。
|
||||
settingAiFilterDeprecated = "ai_filter_deprecated"
|
||||
// settingAiStreamGuardEnabled / settingAiStreamGuardKB 是流式保险丝开关与
|
||||
// 阈值(KB),缺省开、60(上游对 instructions+tools >≈64.5KB 流式静默断流)。
|
||||
settingAiStreamGuardEnabled = "ai_stream_guard_enabled"
|
||||
settingAiStreamGuardKB = "ai_stream_guard_kb"
|
||||
// settingAiGrokWebSearch / settingAiGrokXSearch 是 grok 搜索工具默认注入
|
||||
// 开关,缺省开。
|
||||
settingAiGrokWebSearch = "ai_grok_web_search"
|
||||
settingAiGrokXSearch = "ai_grok_x_search"
|
||||
)
|
||||
|
||||
// defaultStreamGuardKB 是保险丝阈值缺省值,低于实测断流边界留余量。
|
||||
const defaultStreamGuardKB = 60
|
||||
|
||||
// loadBoolSetting 读 settings 表布尔键,无行或值非法时返回缺省。
|
||||
func loadBoolSetting(db *gorm.DB, key string, def bool) bool {
|
||||
var row model.Setting
|
||||
if err := db.Where("key = ?", key).First(&row).Error; err != nil {
|
||||
return def
|
||||
}
|
||||
return row.Value == "1"
|
||||
}
|
||||
|
||||
// loadIntSetting 读 settings 表整数键,无行或解析失败时返回缺省。
|
||||
func loadIntSetting(db *gorm.DB, key string, def int) int {
|
||||
var row model.Setting
|
||||
if err := db.Where("key = ?", key).First(&row).Error; err != nil {
|
||||
return def
|
||||
}
|
||||
n, err := strconv.Atoi(row.Value)
|
||||
if err != nil {
|
||||
return def
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// saveBoolSetting 持久化布尔键。
|
||||
func (s *AiGatewayService) saveBoolSetting(ctx context.Context, key string, on bool) error {
|
||||
value := "0"
|
||||
if on {
|
||||
value = "1"
|
||||
}
|
||||
return s.db.WithContext(ctx).Save(&model.Setting{Key: key, Value: value}).Error
|
||||
}
|
||||
|
||||
// FilterDeprecated 返回「过滤弃用模型」开关状态。
|
||||
func (s *AiGatewayService) FilterDeprecated() bool { return s.filterDeprecated.Load() }
|
||||
@@ -82,18 +135,54 @@ func (s *AiGatewayService) FilterDeprecated() bool { return s.filterDeprecated.L
|
||||
// SetFilterDeprecated 持久化并即时生效开关:开启后已宣布弃用
|
||||
// (deprecated_at 非空,即使未退役)的模型从列表与路由中排除。
|
||||
func (s *AiGatewayService) SetFilterDeprecated(ctx context.Context, on bool) error {
|
||||
value := "0"
|
||||
if on {
|
||||
value = "1"
|
||||
}
|
||||
err := s.db.WithContext(ctx).Save(&model.Setting{Key: settingAiFilterDeprecated, Value: value}).Error
|
||||
if err != nil {
|
||||
if err := s.saveBoolSetting(ctx, settingAiFilterDeprecated, on); err != nil {
|
||||
return fmt.Errorf("保存过滤弃用模型开关: %w", err)
|
||||
}
|
||||
s.filterDeprecated.Store(on)
|
||||
return nil
|
||||
}
|
||||
|
||||
// StreamGuard 返回流式保险丝开关与阈值(KB)。
|
||||
func (s *AiGatewayService) StreamGuard() (bool, int) {
|
||||
return s.streamGuardEnabled.Load(), int(s.streamGuardKB.Load())
|
||||
}
|
||||
|
||||
// SetStreamGuard 持久化并即时生效流式保险丝;kb 限定 1..1024。
|
||||
func (s *AiGatewayService) SetStreamGuard(ctx context.Context, on bool, kb int) error {
|
||||
if kb < 1 || kb > 1024 {
|
||||
return fmt.Errorf("流式保险丝阈值须在 1..1024 KB, 收到 %d", kb)
|
||||
}
|
||||
if err := s.saveBoolSetting(ctx, settingAiStreamGuardEnabled, on); err != nil {
|
||||
return fmt.Errorf("保存流式保险丝开关: %w", err)
|
||||
}
|
||||
err := s.db.WithContext(ctx).
|
||||
Save(&model.Setting{Key: settingAiStreamGuardKB, Value: strconv.Itoa(kb)}).Error
|
||||
if err != nil {
|
||||
return fmt.Errorf("保存流式保险丝阈值: %w", err)
|
||||
}
|
||||
s.streamGuardEnabled.Store(on)
|
||||
s.streamGuardKB.Store(int64(kb))
|
||||
return nil
|
||||
}
|
||||
|
||||
// GrokSearch 返回 grok 服务端搜索工具默认注入开关(web_search, x_search)。
|
||||
func (s *AiGatewayService) GrokSearch() (bool, bool) {
|
||||
return s.grokWebSearch.Load(), s.grokXSearch.Load()
|
||||
}
|
||||
|
||||
// SetGrokSearch 持久化并即时生效 grok 搜索工具默认注入开关。
|
||||
func (s *AiGatewayService) SetGrokSearch(ctx context.Context, web, x bool) error {
|
||||
if err := s.saveBoolSetting(ctx, settingAiGrokWebSearch, web); err != nil {
|
||||
return fmt.Errorf("保存 grok web_search 开关: %w", err)
|
||||
}
|
||||
if err := s.saveBoolSetting(ctx, settingAiGrokXSearch, x); err != nil {
|
||||
return fmt.Errorf("保存 grok x_search 开关: %w", err)
|
||||
}
|
||||
s.grokWebSearch.Store(web)
|
||||
s.grokXSearch.Store(x)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetOnChannelsChanged 注册渠道数量变化钩子(渠道创建/删除成功后调用)。
|
||||
func (s *AiGatewayService) SetOnChannelsChanged(fn func(context.Context)) {
|
||||
s.onChannelsChanged = fn
|
||||
@@ -661,6 +750,40 @@ func (s *AiGatewayService) GatewayModels(ctx context.Context, group string) (aiw
|
||||
return list, nil
|
||||
}
|
||||
|
||||
// AggregatedModel 是聚合模型目录条目(设置页黑名单添加弹窗用)。
|
||||
type AggregatedModel struct {
|
||||
Name string `json:"name"`
|
||||
Capability string `json:"capability"`
|
||||
}
|
||||
|
||||
// AggregatedModels 返回启用渠道去重后的模型目录(含能力);空能力归一为 CHAT。
|
||||
// 与模型列表口径一致:「过滤弃用」开启时弃用模型不出现在目录中。
|
||||
func (s *AiGatewayService) AggregatedModels(ctx context.Context) ([]AggregatedModel, error) {
|
||||
q := s.db.WithContext(ctx).
|
||||
Joins("JOIN ai_channels ON ai_channels.id = ai_model_caches.channel_id AND ai_channels.enabled = ?", true)
|
||||
if s.FilterDeprecated() {
|
||||
q = q.Where("ai_model_caches.deprecated_at IS NULL")
|
||||
}
|
||||
var rows []model.AiModelCache
|
||||
if err := q.Order("ai_model_caches.name ASC").Find(&rows).Error; err != nil {
|
||||
return nil, fmt.Errorf("聚合模型目录: %w", err)
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
out := []AggregatedModel{}
|
||||
for _, r := range rows {
|
||||
if seen[r.Name] {
|
||||
continue
|
||||
}
|
||||
seen[r.Name] = true
|
||||
cap := r.Capability
|
||||
if cap == "" {
|
||||
cap = "CHAT"
|
||||
}
|
||||
out = append(out, AggregatedModel{Name: r.Name, Capability: cap})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// DeprecatingModels 返回 within 窗口内即将退役或即将弃用的在池模型(按名称去重):
|
||||
// 退役(TimeOnDemandRetired)才导致不可调用,单独标注;已过弃用日但未到退役日的
|
||||
// 模型仍可正常调用,不再反复告警;已过退役日的在同步层剔除,不会出现在池中。
|
||||
|
||||
@@ -1060,3 +1060,67 @@ func TestRespPassthroughStreamSwitchesChannel(t *testing.T) {
|
||||
t.Errorf("流内容未透传: %s", payload)
|
||||
}
|
||||
}
|
||||
|
||||
// TestAiRuntimeSettings 断言流式保险丝与 grok 注入开关的缺省值、往返与持久化。
|
||||
func TestAiRuntimeSettings(t *testing.T) {
|
||||
gw, svc := newTestGateway(t, &gatewayStubClient{fakeClient: &fakeClient{tenancy: oci.TenancyInfo{Name: "t"}}})
|
||||
ctx := context.Background()
|
||||
|
||||
if on, kb := gw.StreamGuard(); !on || kb != 60 {
|
||||
t.Fatalf("保险丝缺省应为 开/60, got %v/%d", on, kb)
|
||||
}
|
||||
if web, x := gw.GrokSearch(); !web || !x {
|
||||
t.Fatalf("grok 注入缺省应双开, got %v/%v", web, x)
|
||||
}
|
||||
for _, bad := range []int{0, -1, 1025} {
|
||||
if err := gw.SetStreamGuard(ctx, true, bad); err == nil {
|
||||
t.Errorf("阈值 %d 应报错", bad)
|
||||
}
|
||||
}
|
||||
if err := gw.SetStreamGuard(ctx, false, 80); err != nil {
|
||||
t.Fatalf("SetStreamGuard: %v", err)
|
||||
}
|
||||
if err := gw.SetGrokSearch(ctx, false, true); err != nil {
|
||||
t.Fatalf("SetGrokSearch: %v", err)
|
||||
}
|
||||
gw2 := NewAiGatewayService(gw.db, svc, &gatewayStubClient{})
|
||||
if on, kb := gw2.StreamGuard(); on || kb != 80 {
|
||||
t.Errorf("重建后保险丝应为 关/80, got %v/%d", on, kb)
|
||||
}
|
||||
if web, x := gw2.GrokSearch(); web || !x {
|
||||
t.Errorf("重建后 grok 注入应为 关/开, got %v/%v", web, x)
|
||||
}
|
||||
}
|
||||
|
||||
// TestAggregatedModelsFilterDeprecated 断言聚合目录与模型列表口径一致:
|
||||
// 「过滤弃用」开启时弃用模型不出现,关闭时出现;空能力归一为 CHAT。
|
||||
func TestAggregatedModelsFilterDeprecated(t *testing.T) {
|
||||
gw, svc := newTestGateway(t, &gatewayStubClient{fakeClient: &fakeClient{tenancy: oci.TenancyInfo{Name: "t"}}})
|
||||
cfg := importAliveConfig(t, svc)
|
||||
ch := seedChannel(t, gw, cfg.ID, "eu-frankfurt-1", 1, 1)
|
||||
dep := time.Now().Add(-24 * time.Hour)
|
||||
old := &model.AiModelCache{ChannelID: ch.ID, ModelOcid: "ocid1..dep", Name: "meta.llama-old",
|
||||
Vendor: "meta", SyncedAt: time.Now(), DeprecatedAt: &dep}
|
||||
if err := gw.db.Create(old).Error; err != nil {
|
||||
t.Fatalf("seed deprecated cache: %v", err)
|
||||
}
|
||||
ctx := context.Background()
|
||||
|
||||
items, err := gw.AggregatedModels(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("AggregatedModels: %v", err)
|
||||
}
|
||||
if len(items) != 2 || items[0].Capability == "" {
|
||||
t.Fatalf("开关关:应含弃用模型且能力归一, got %+v", items)
|
||||
}
|
||||
if err := gw.SetFilterDeprecated(ctx, true); err != nil {
|
||||
t.Fatalf("SetFilterDeprecated: %v", err)
|
||||
}
|
||||
items, err = gw.AggregatedModels(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("AggregatedModels(过滤): %v", err)
|
||||
}
|
||||
if len(items) != 1 || items[0].Name == "meta.llama-old" {
|
||||
t.Fatalf("开关开:弃用模型应被过滤, got %+v", items)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -320,10 +320,63 @@ func respToolDrop(tool map[string]any) (string, bool) {
|
||||
return "", false
|
||||
}
|
||||
|
||||
// RespStreamUpgradeLimit 是流式直通的请求体安全上限。实测(2026-07,xai 面):
|
||||
// 超过 ~82KB 的流式请求上游会在推理阶段掐断流(非流式不受影响,纯体积触发,
|
||||
// 与工具构成无关),预留余量取 76KB;超限时网关改走非流式上游并合成 SSE。
|
||||
const RespStreamUpgradeLimit = 76 * 1024
|
||||
// RespGuardBytes 返回请求体中 instructions 与 tools 两字段的原始字节数之和,
|
||||
// 流式保险丝据此判定。上游对二者合计 >≈64.5KB 的流式请求会在推理阶段静默断流
|
||||
// (纯 EOF,input 正文不计入;2026-07-16 实测仍存在),阈值由设置页 AI Tab 配置。
|
||||
// 解析失败返回 0(放行,交由上游正常报错)。
|
||||
func RespGuardBytes(body []byte) int {
|
||||
var probe struct {
|
||||
Instructions json.RawMessage `json:"instructions"`
|
||||
Tools json.RawMessage `json:"tools"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &probe); err != nil {
|
||||
return 0
|
||||
}
|
||||
return len(probe.Instructions) + len(probe.Tools)
|
||||
}
|
||||
|
||||
// RespInjectGrokTools 为 xai. 前缀模型默认注入服务端搜索工具:开关开启且请求
|
||||
// tools 中不存在同名工具时追加 {"type":"web_search"} / {"type":"x_search"};
|
||||
// 已存在(含任意参数形态)不覆盖。返回改写后 body 与注入清单(观测日志用);
|
||||
// 模型不匹配、两开关全关或解析失败时原样返回。
|
||||
func RespInjectGrokTools(body []byte, model string, web, x bool) ([]byte, []string) {
|
||||
if !strings.HasPrefix(model, "xai.") || (!web && !x) {
|
||||
return body, nil
|
||||
}
|
||||
dec := json.NewDecoder(strings.NewReader(string(body)))
|
||||
dec.UseNumber()
|
||||
var m map[string]any
|
||||
if err := dec.Decode(&m); err != nil {
|
||||
return body, nil
|
||||
}
|
||||
tools, _ := m["tools"].([]any)
|
||||
missing := map[string]bool{"web_search": web, "x_search": x}
|
||||
for _, t := range tools {
|
||||
tool, ok := t.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if typ, _ := tool["type"].(string); missing[typ] {
|
||||
missing[typ] = false
|
||||
}
|
||||
}
|
||||
var injected []string
|
||||
for _, typ := range []string{"web_search", "x_search"} {
|
||||
if missing[typ] {
|
||||
tools = append(tools, map[string]any{"type": typ})
|
||||
injected = append(injected, typ)
|
||||
}
|
||||
}
|
||||
if len(injected) == 0 {
|
||||
return body, nil
|
||||
}
|
||||
m["tools"] = tools
|
||||
out, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
return body, nil
|
||||
}
|
||||
return out, injected
|
||||
}
|
||||
|
||||
// RespDisableStream 把请求体的 stream 改为 false(流式升级回退用),其余字段
|
||||
// 原样保留。
|
||||
|
||||
@@ -2,6 +2,7 @@ package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -329,6 +330,71 @@ func TestRespStreamCompletedUsage(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestRespGuardBytes 断言保险丝计量:instructions+tools 原始字节和,缺字段计 0,
|
||||
// 解析失败返回 0 放行。
|
||||
func TestRespGuardBytes(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name, body string
|
||||
want int
|
||||
}{
|
||||
{"双字段", `{"instructions":"abcd","tools":[{"type":"web_search"}],"input":"xxxxxxxx"}`,
|
||||
len(`"abcd"`) + len(`[{"type":"web_search"}]`)},
|
||||
{"仅 instructions", `{"instructions":"abcd"}`, len(`"abcd"`)},
|
||||
{"均缺失 input 不计", `{"input":"xxxxxxxxxxxxxxxx"}`, 0},
|
||||
{"解析失败放行", `not-json`, 0},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if got := RespGuardBytes([]byte(tc.body)); got != tc.want {
|
||||
t.Fatalf("RespGuardBytes() = %d, want %d", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestRespInjectGrokTools 断言 grok 搜索工具默认注入:仅 xai. 模型、开关可控、
|
||||
// 已带同名工具不覆盖、注入清单正确。
|
||||
func TestRespInjectGrokTools(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name, body, model string
|
||||
web, x bool
|
||||
wantInjected []string
|
||||
wantContains []string
|
||||
}{
|
||||
{"非 xai 不注入", `{"tools":[]}`, "meta.llama-3.3", true, true, nil, nil},
|
||||
{"双开无 tools 字段注入两个", `{"model":"xai.grok-4.3"}`, "xai.grok-4.3", true, true,
|
||||
[]string{"web_search", "x_search"}, []string{`"web_search"`, `"x_search"`}},
|
||||
{"已带 web_search 只注入 x_search", `{"tools":[{"type":"web_search","filters":{"x":1}}]}`,
|
||||
"xai.grok-4.3", true, true, []string{"x_search"}, []string{`"filters"`}},
|
||||
{"开关全关不注入", `{"tools":[]}`, "xai.grok-4.3", false, false, nil, nil},
|
||||
{"仅开 x_search", `{"tools":[]}`, "xai.grok-4.3", false, true, []string{"x_search"}, nil},
|
||||
{"解析失败原样", `not-json`, "xai.grok-4.3", true, true, nil, nil},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
out, injected := RespInjectGrokTools([]byte(tc.body), tc.model, tc.web, tc.x)
|
||||
if fmt.Sprint(injected) != fmt.Sprint(tc.wantInjected) {
|
||||
t.Fatalf("injected = %v, want %v", injected, tc.wantInjected)
|
||||
}
|
||||
if len(injected) == 0 && string(out) != tc.body {
|
||||
t.Fatalf("未注入时应原样返回: %s", out)
|
||||
}
|
||||
for _, sub := range append(tc.wantContains, toolTypes(injected)...) {
|
||||
if !strings.Contains(string(out), sub) {
|
||||
t.Fatalf("输出缺少 %s: %s", sub, out)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// toolTypes 把注入清单转为输出应包含的片段断言。
|
||||
func toolTypes(injected []string) []string {
|
||||
var out []string
|
||||
for _, typ := range injected {
|
||||
out = append(out, `{"type":"`+typ+`"}`)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// TestRespDisableStream 断言流式升级回退把 stream 置 false 且其余字段保留。
|
||||
func TestRespDisableStream(t *testing.T) {
|
||||
out, err := RespDisableStream([]byte(`{"model":"m","stream":true,"max_output_tokens":64}`))
|
||||
|
||||
Reference in New Issue
Block a user