Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4477c729a4 | ||
|
|
0d89a22aa0 | ||
|
|
9319602812 |
16
README.md
16
README.md
@@ -466,6 +466,7 @@ An S3-compatible object storage service can host configuration and authenticatio
|
|||||||
|
|
||||||
| Variable | Required | Default | Description |
|
| Variable | Required | Default | Description |
|
||||||
|--------------------------|----------|--------------------------------|--------------------------------------------------------------------------------------------------------------------------|
|
|--------------------------|----------|--------------------------------|--------------------------------------------------------------------------------------------------------------------------|
|
||||||
|
| `MANAGEMENT_PASSWORD` | Yes | | Password for the management web UI (required when remote management is enabled). |
|
||||||
| `OBJECTSTORE_ENDPOINT` | Yes | | Object storage endpoint. Include `http://` or `https://` to force the protocol (omitted scheme → HTTPS). |
|
| `OBJECTSTORE_ENDPOINT` | Yes | | Object storage endpoint. Include `http://` or `https://` to force the protocol (omitted scheme → HTTPS). |
|
||||||
| `OBJECTSTORE_BUCKET` | Yes | | Bucket that stores `config/config.yaml` and `auths/*.json`. |
|
| `OBJECTSTORE_BUCKET` | Yes | | Bucket that stores `config/config.yaml` and `auths/*.json`. |
|
||||||
| `OBJECTSTORE_ACCESS_KEY` | Yes | | Access key ID for the object storage account. |
|
| `OBJECTSTORE_ACCESS_KEY` | Yes | | Access key ID for the object storage account. |
|
||||||
@@ -531,21 +532,6 @@ And you can always use Gemini CLI with `CODE_ASSIST_ENDPOINT` set to `http://127
|
|||||||
|
|
||||||
The `auth-dir` parameter specifies where authentication tokens are stored. When you run the login command, the application will create JSON files in this directory containing the authentication tokens for your Google accounts. Multiple accounts can be used for load balancing.
|
The `auth-dir` parameter specifies where authentication tokens are stored. When you run the login command, the application will create JSON files in this directory containing the authentication tokens for your Google accounts. Multiple accounts can be used for load balancing.
|
||||||
|
|
||||||
### Request Authentication Providers
|
|
||||||
|
|
||||||
Configure inbound authentication through the `auth.providers` section. The built-in `config-api-key` provider works with inline keys:
|
|
||||||
|
|
||||||
```
|
|
||||||
auth:
|
|
||||||
providers:
|
|
||||||
- name: default
|
|
||||||
type: config-api-key
|
|
||||||
api-keys:
|
|
||||||
- your-api-key-1
|
|
||||||
```
|
|
||||||
|
|
||||||
Clients should send requests with an `Authorization: Bearer your-api-key-1` header (or `X-Goog-Api-Key`, `X-Api-Key`, or `?key=` as before). The legacy top-level `api-keys` array is still accepted and automatically synced to the default provider for backwards compatibility.
|
|
||||||
|
|
||||||
### Official Generative Language API
|
### Official Generative Language API
|
||||||
|
|
||||||
The `generative-language-api-key` parameter allows you to define a list of API keys that can be used to authenticate requests to the official Generative Language API.
|
The `generative-language-api-key` parameter allows you to define a list of API keys that can be used to authenticate requests to the official Generative Language API.
|
||||||
|
|||||||
18
README_CN.md
18
README_CN.md
@@ -438,7 +438,7 @@ openai-compatibility:
|
|||||||
|
|
||||||
| 变量 | 必需 | 默认值 | 描述 |
|
| 变量 | 必需 | 默认值 | 描述 |
|
||||||
|-------------------------|----|--------|----------------------------------------------------|
|
|-------------------------|----|--------|----------------------------------------------------|
|
||||||
| `MANAGEMENT_PASSWORD` | 是 | | 控制面板密码 |
|
| `MANAGEMENT_PASSWORD` | 是 | | 管理面板密码 |
|
||||||
| `GITSTORE_GIT_URL` | 是 | | 要使用的 Git 仓库的 HTTPS URL。 |
|
| `GITSTORE_GIT_URL` | 是 | | 要使用的 Git 仓库的 HTTPS URL。 |
|
||||||
| `GITSTORE_LOCAL_PATH` | 否 | 当前工作目录 | 将克隆 Git 仓库的本地路径。在 Docker 内部,此路径默认为 `/CLIProxyAPI`。 |
|
| `GITSTORE_LOCAL_PATH` | 否 | 当前工作目录 | 将克隆 Git 仓库的本地路径。在 Docker 内部,此路径默认为 `/CLIProxyAPI`。 |
|
||||||
| `GITSTORE_GIT_USERNAME` | 否 | | 用于 Git 身份验证的用户名。 |
|
| `GITSTORE_GIT_USERNAME` | 否 | | 用于 Git 身份验证的用户名。 |
|
||||||
@@ -479,6 +479,7 @@ openai-compatibility:
|
|||||||
|
|
||||||
| 变量 | 是否必填 | 默认值 | 说明 |
|
| 变量 | 是否必填 | 默认值 | 说明 |
|
||||||
|--------------------------|----------|--------------------|--------------------------------------------------------------------------------------------------------------------------|
|
|--------------------------|----------|--------------------|--------------------------------------------------------------------------------------------------------------------------|
|
||||||
|
| `MANAGEMENT_PASSWORD` | 是 | | 管理面板密码(启用远程管理时必需)。 |
|
||||||
| `OBJECTSTORE_ENDPOINT` | 是 | | 对象存储访问端点。可带 `http://` 或 `https://` 前缀指定协议(省略则默认 HTTPS)。 |
|
| `OBJECTSTORE_ENDPOINT` | 是 | | 对象存储访问端点。可带 `http://` 或 `https://` 前缀指定协议(省略则默认 HTTPS)。 |
|
||||||
| `OBJECTSTORE_BUCKET` | 是 | | 用于存放 `config/config.yaml` 与 `auths/*.json` 的 Bucket 名称。 |
|
| `OBJECTSTORE_BUCKET` | 是 | | 用于存放 `config/config.yaml` 与 `auths/*.json` 的 Bucket 名称。 |
|
||||||
| `OBJECTSTORE_ACCESS_KEY` | 是 | | 对象存储账号的访问密钥 ID。 |
|
| `OBJECTSTORE_ACCESS_KEY` | 是 | | 对象存储账号的访问密钥 ID。 |
|
||||||
@@ -539,21 +540,6 @@ openai-compatibility:
|
|||||||
|
|
||||||
`auth-dir` 参数指定身份验证令牌的存储位置。当您运行登录命令时,应用程序将在此目录中创建包含 Google 账户身份验证令牌的 JSON 文件。多个账户可用于轮询。
|
`auth-dir` 参数指定身份验证令牌的存储位置。当您运行登录命令时,应用程序将在此目录中创建包含 Google 账户身份验证令牌的 JSON 文件。多个账户可用于轮询。
|
||||||
|
|
||||||
### 请求鉴权提供方
|
|
||||||
|
|
||||||
通过 `auth.providers` 配置接入请求鉴权。内置的 `config-api-key` 提供方支持内联密钥:
|
|
||||||
|
|
||||||
```
|
|
||||||
auth:
|
|
||||||
providers:
|
|
||||||
- name: default
|
|
||||||
type: config-api-key
|
|
||||||
api-keys:
|
|
||||||
- your-api-key-1
|
|
||||||
```
|
|
||||||
|
|
||||||
调用时可在 `Authorization` 标头中携带密钥(或继续使用 `X-Goog-Api-Key`、`X-Api-Key`、查询参数 `key`)。为了兼容旧版本,顶层的 `api-keys` 字段仍然可用,并会自动同步到默认的 `config-api-key` 提供方。
|
|
||||||
|
|
||||||
### 官方生成式语言 API
|
### 官方生成式语言 API
|
||||||
|
|
||||||
`generative-language-api-key` 参数允许您定义可用于验证对官方 AIStudio Gemini API 请求的 API 密钥列表。
|
`generative-language-api-key` 参数允许您定义可用于验证对官方 AIStudio Gemini API 请求的 API 密钥列表。
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
client "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces"
|
client "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
"github.com/tidwall/sjson"
|
"github.com/tidwall/sjson"
|
||||||
)
|
)
|
||||||
@@ -36,18 +35,6 @@ import (
|
|||||||
// - []byte: The transformed request data in Gemini CLI API format
|
// - []byte: The transformed request data in Gemini CLI API format
|
||||||
func ConvertClaudeRequestToCLI(modelName string, inputRawJSON []byte, _ bool) []byte {
|
func ConvertClaudeRequestToCLI(modelName string, inputRawJSON []byte, _ bool) []byte {
|
||||||
rawJSON := bytes.Clone(inputRawJSON)
|
rawJSON := bytes.Clone(inputRawJSON)
|
||||||
var pathsToDelete []string
|
|
||||||
root := gjson.ParseBytes(rawJSON)
|
|
||||||
util.Walk(root, "", "additionalProperties", &pathsToDelete)
|
|
||||||
util.Walk(root, "", "$schema", &pathsToDelete)
|
|
||||||
|
|
||||||
var err error
|
|
||||||
for _, p := range pathsToDelete {
|
|
||||||
rawJSON, err = sjson.DeleteBytes(rawJSON, p)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
rawJSON = bytes.Replace(rawJSON, []byte(`"url":{"type":"string","format":"uri",`), []byte(`"url":{"type":"string",`), -1)
|
rawJSON = bytes.Replace(rawJSON, []byte(`"url":{"type":"string","format":"uri",`), []byte(`"url":{"type":"string",`), -1)
|
||||||
|
|
||||||
// system instruction
|
// system instruction
|
||||||
@@ -99,7 +86,7 @@ func ConvertClaudeRequestToCLI(modelName string, inputRawJSON []byte, _ bool) []
|
|||||||
functionName := contentResult.Get("name").String()
|
functionName := contentResult.Get("name").String()
|
||||||
functionArgs := contentResult.Get("input").String()
|
functionArgs := contentResult.Get("input").String()
|
||||||
var args map[string]any
|
var args map[string]any
|
||||||
if err = json.Unmarshal([]byte(functionArgs), &args); err == nil {
|
if err := json.Unmarshal([]byte(functionArgs), &args); err == nil {
|
||||||
clientContent.Parts = append(clientContent.Parts, client.Part{FunctionCall: &client.FunctionCall{Name: functionName, Args: args}})
|
clientContent.Parts = append(clientContent.Parts, client.Part{FunctionCall: &client.FunctionCall{Name: functionName, Args: args}})
|
||||||
}
|
}
|
||||||
} else if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "tool_result" {
|
} else if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "tool_result" {
|
||||||
@@ -136,18 +123,10 @@ func ConvertClaudeRequestToCLI(modelName string, inputRawJSON []byte, _ bool) []
|
|||||||
inputSchemaResult := toolResult.Get("input_schema")
|
inputSchemaResult := toolResult.Get("input_schema")
|
||||||
if inputSchemaResult.Exists() && inputSchemaResult.IsObject() {
|
if inputSchemaResult.Exists() && inputSchemaResult.IsObject() {
|
||||||
inputSchema := inputSchemaResult.Raw
|
inputSchema := inputSchemaResult.Raw
|
||||||
// Use comprehensive schema sanitization for Gemini API compatibility
|
|
||||||
if sanitizedSchema, sanitizeErr := util.SanitizeSchemaForGemini(inputSchema); sanitizeErr == nil {
|
|
||||||
inputSchema = sanitizedSchema
|
|
||||||
} else {
|
|
||||||
// Fallback to basic cleanup if sanitization fails
|
|
||||||
inputSchema, _ = sjson.Delete(inputSchema, "additionalProperties")
|
|
||||||
inputSchema, _ = sjson.Delete(inputSchema, "$schema")
|
|
||||||
}
|
|
||||||
tool, _ := sjson.Delete(toolResult.Raw, "input_schema")
|
tool, _ := sjson.Delete(toolResult.Raw, "input_schema")
|
||||||
tool, _ = sjson.SetRaw(tool, "parameters", inputSchema)
|
tool, _ = sjson.SetRaw(tool, "parametersJsonSchema", inputSchema)
|
||||||
var toolDeclaration any
|
var toolDeclaration any
|
||||||
if err = json.Unmarshal([]byte(tool), &toolDeclaration); err == nil {
|
if err := json.Unmarshal([]byte(tool), &toolDeclaration); err == nil {
|
||||||
tools[0].FunctionDeclarations = append(tools[0].FunctionDeclarations, toolDeclaration)
|
tools[0].FunctionDeclarations = append(tools[0].FunctionDeclarations, toolDeclaration)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
"github.com/tidwall/sjson"
|
"github.com/tidwall/sjson"
|
||||||
@@ -78,6 +79,24 @@ func ConvertGeminiRequestToGeminiCLI(_ string, inputRawJSON []byte, _ bool) []by
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toolsResult := gjson.GetBytes(rawJSON, "request.tools")
|
||||||
|
if toolsResult.Exists() && toolsResult.IsArray() {
|
||||||
|
toolResults := toolsResult.Array()
|
||||||
|
for i := 0; i < len(toolResults); i++ {
|
||||||
|
functionDeclarationsResult := gjson.GetBytes(rawJSON, fmt.Sprintf("request.tools.%d.function_declarations", i))
|
||||||
|
if functionDeclarationsResult.Exists() && functionDeclarationsResult.IsArray() {
|
||||||
|
functionDeclarationsResults := functionDeclarationsResult.Array()
|
||||||
|
for j := 0; j < len(functionDeclarationsResults); j++ {
|
||||||
|
parametersResult := gjson.GetBytes(rawJSON, fmt.Sprintf("request.tools.%d.function_declarations.%d.parameters", i, j))
|
||||||
|
if parametersResult.Exists() {
|
||||||
|
strJson, _ := util.RenameKey(string(rawJSON), fmt.Sprintf("request.tools.%d.function_declarations.%d.parameters", i, j), fmt.Sprintf("request.tools.%d.function_declarations.%d.parametersJsonSchema", i, j))
|
||||||
|
rawJSON = []byte(strJson)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return rawJSON
|
return rawJSON
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,21 +26,6 @@ import (
|
|||||||
// - []byte: The transformed request data in Gemini CLI API format
|
// - []byte: The transformed request data in Gemini CLI API format
|
||||||
func ConvertOpenAIRequestToGeminiCLI(modelName string, inputRawJSON []byte, _ bool) []byte {
|
func ConvertOpenAIRequestToGeminiCLI(modelName string, inputRawJSON []byte, _ bool) []byte {
|
||||||
rawJSON := bytes.Clone(inputRawJSON)
|
rawJSON := bytes.Clone(inputRawJSON)
|
||||||
var pathsToDelete []string
|
|
||||||
root := gjson.ParseBytes(rawJSON)
|
|
||||||
util.Walk(root, "", "additionalProperties", &pathsToDelete)
|
|
||||||
util.Walk(root, "", "$schema", &pathsToDelete)
|
|
||||||
util.Walk(root, "", "ref", &pathsToDelete)
|
|
||||||
util.Walk(root, "", "strict", &pathsToDelete)
|
|
||||||
|
|
||||||
var err error
|
|
||||||
for _, p := range pathsToDelete {
|
|
||||||
rawJSON, err = sjson.DeleteBytes(rawJSON, p)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Base envelope
|
// Base envelope
|
||||||
out := []byte(`{"project":"","request":{"contents":[],"generationConfig":{"thinkingConfig":{"include_thoughts":true}}},"model":"gemini-2.5-pro"}`)
|
out := []byte(`{"project":"","request":{"contents":[],"generationConfig":{"thinkingConfig":{"include_thoughts":true}}},"model":"gemini-2.5-pro"}`)
|
||||||
|
|
||||||
@@ -265,22 +250,13 @@ func ConvertOpenAIRequestToGeminiCLI(modelName string, inputRawJSON []byte, _ bo
|
|||||||
if t.Get("type").String() == "function" {
|
if t.Get("type").String() == "function" {
|
||||||
fn := t.Get("function")
|
fn := t.Get("function")
|
||||||
if fn.Exists() && fn.IsObject() {
|
if fn.Exists() && fn.IsObject() {
|
||||||
out, _ = sjson.SetRawBytes(out, fdPath+".-1", []byte(fn.Raw))
|
parametersJsonSchema, _ := util.RenameKey(fn.Raw, "parameters", "parametersJsonSchema")
|
||||||
|
out, _ = sjson.SetRawBytes(out, fdPath+".-1", []byte(parametersJsonSchema))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var pathsToType []string
|
|
||||||
root = gjson.ParseBytes(out)
|
|
||||||
util.Walk(root, "", "type", &pathsToType)
|
|
||||||
for _, p := range pathsToType {
|
|
||||||
typeResult := gjson.GetBytes(out, p)
|
|
||||||
if strings.ToLower(typeResult.String()) == "select" {
|
|
||||||
out, _ = sjson.SetBytes(out, p, "STRING")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,6 +97,7 @@ func ConvertCliResponseToOpenAI(_ context.Context, _ string, originalRequestRawJ
|
|||||||
|
|
||||||
// Process the main content part of the response.
|
// Process the main content part of the response.
|
||||||
partsResult := gjson.GetBytes(rawJSON, "response.candidates.0.content.parts")
|
partsResult := gjson.GetBytes(rawJSON, "response.candidates.0.content.parts")
|
||||||
|
hasFunctionCall := false
|
||||||
if partsResult.IsArray() {
|
if partsResult.IsArray() {
|
||||||
partResults := partsResult.Array()
|
partResults := partsResult.Array()
|
||||||
for i := 0; i < len(partResults); i++ {
|
for i := 0; i < len(partResults); i++ {
|
||||||
@@ -118,6 +119,7 @@ func ConvertCliResponseToOpenAI(_ context.Context, _ string, originalRequestRawJ
|
|||||||
template, _ = sjson.Set(template, "choices.0.delta.role", "assistant")
|
template, _ = sjson.Set(template, "choices.0.delta.role", "assistant")
|
||||||
} else if functionCallResult.Exists() {
|
} else if functionCallResult.Exists() {
|
||||||
// Handle function call content.
|
// Handle function call content.
|
||||||
|
hasFunctionCall = true
|
||||||
toolCallsResult := gjson.Get(template, "choices.0.delta.tool_calls")
|
toolCallsResult := gjson.Get(template, "choices.0.delta.tool_calls")
|
||||||
functionCallIndex := (*param).(*convertCliResponseToOpenAIChatParams).FunctionIndex
|
functionCallIndex := (*param).(*convertCliResponseToOpenAIChatParams).FunctionIndex
|
||||||
(*param).(*convertCliResponseToOpenAIChatParams).FunctionIndex++
|
(*param).(*convertCliResponseToOpenAIChatParams).FunctionIndex++
|
||||||
@@ -169,6 +171,11 @@ func ConvertCliResponseToOpenAI(_ context.Context, _ string, originalRequestRawJ
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if hasFunctionCall {
|
||||||
|
template, _ = sjson.Set(template, "choices.0.finish_reason", "tool_calls")
|
||||||
|
template, _ = sjson.Set(template, "choices.0.native_finish_reason", "tool_calls")
|
||||||
|
}
|
||||||
|
|
||||||
return []string{template}
|
return []string{template}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -11,7 +11,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
client "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces"
|
client "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
"github.com/tidwall/sjson"
|
"github.com/tidwall/sjson"
|
||||||
)
|
)
|
||||||
@@ -29,18 +28,6 @@ import (
|
|||||||
// - []byte: The transformed request in Gemini CLI format.
|
// - []byte: The transformed request in Gemini CLI format.
|
||||||
func ConvertClaudeRequestToGemini(modelName string, inputRawJSON []byte, _ bool) []byte {
|
func ConvertClaudeRequestToGemini(modelName string, inputRawJSON []byte, _ bool) []byte {
|
||||||
rawJSON := bytes.Clone(inputRawJSON)
|
rawJSON := bytes.Clone(inputRawJSON)
|
||||||
var pathsToDelete []string
|
|
||||||
root := gjson.ParseBytes(rawJSON)
|
|
||||||
util.Walk(root, "", "additionalProperties", &pathsToDelete)
|
|
||||||
util.Walk(root, "", "$schema", &pathsToDelete)
|
|
||||||
|
|
||||||
var err error
|
|
||||||
for _, p := range pathsToDelete {
|
|
||||||
rawJSON, err = sjson.DeleteBytes(rawJSON, p)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
rawJSON = bytes.Replace(rawJSON, []byte(`"url":{"type":"string","format":"uri",`), []byte(`"url":{"type":"string",`), -1)
|
rawJSON = bytes.Replace(rawJSON, []byte(`"url":{"type":"string","format":"uri",`), []byte(`"url":{"type":"string",`), -1)
|
||||||
|
|
||||||
// system instruction
|
// system instruction
|
||||||
@@ -92,7 +79,7 @@ func ConvertClaudeRequestToGemini(modelName string, inputRawJSON []byte, _ bool)
|
|||||||
functionName := contentResult.Get("name").String()
|
functionName := contentResult.Get("name").String()
|
||||||
functionArgs := contentResult.Get("input").String()
|
functionArgs := contentResult.Get("input").String()
|
||||||
var args map[string]any
|
var args map[string]any
|
||||||
if err = json.Unmarshal([]byte(functionArgs), &args); err == nil {
|
if err := json.Unmarshal([]byte(functionArgs), &args); err == nil {
|
||||||
clientContent.Parts = append(clientContent.Parts, client.Part{FunctionCall: &client.FunctionCall{Name: functionName, Args: args}})
|
clientContent.Parts = append(clientContent.Parts, client.Part{FunctionCall: &client.FunctionCall{Name: functionName, Args: args}})
|
||||||
}
|
}
|
||||||
} else if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "tool_result" {
|
} else if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "tool_result" {
|
||||||
@@ -129,18 +116,10 @@ func ConvertClaudeRequestToGemini(modelName string, inputRawJSON []byte, _ bool)
|
|||||||
inputSchemaResult := toolResult.Get("input_schema")
|
inputSchemaResult := toolResult.Get("input_schema")
|
||||||
if inputSchemaResult.Exists() && inputSchemaResult.IsObject() {
|
if inputSchemaResult.Exists() && inputSchemaResult.IsObject() {
|
||||||
inputSchema := inputSchemaResult.Raw
|
inputSchema := inputSchemaResult.Raw
|
||||||
// Use comprehensive schema sanitization for Gemini API compatibility
|
|
||||||
if sanitizedSchema, sanitizeErr := util.SanitizeSchemaForGemini(inputSchema); sanitizeErr == nil {
|
|
||||||
inputSchema = sanitizedSchema
|
|
||||||
} else {
|
|
||||||
// Fallback to basic cleanup if sanitization fails
|
|
||||||
inputSchema, _ = sjson.Delete(inputSchema, "additionalProperties")
|
|
||||||
inputSchema, _ = sjson.Delete(inputSchema, "$schema")
|
|
||||||
}
|
|
||||||
tool, _ := sjson.Delete(toolResult.Raw, "input_schema")
|
tool, _ := sjson.Delete(toolResult.Raw, "input_schema")
|
||||||
tool, _ = sjson.SetRaw(tool, "parameters", inputSchema)
|
tool, _ = sjson.SetRaw(tool, "parametersJsonSchema", inputSchema)
|
||||||
var toolDeclaration any
|
var toolDeclaration any
|
||||||
if err = json.Unmarshal([]byte(tool), &toolDeclaration); err == nil {
|
if err := json.Unmarshal([]byte(tool), &toolDeclaration); err == nil {
|
||||||
tools[0].FunctionDeclarations = append(tools[0].FunctionDeclarations, toolDeclaration)
|
tools[0].FunctionDeclarations = append(tools[0].FunctionDeclarations, toolDeclaration)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ package geminiCLI
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
"github.com/tidwall/sjson"
|
"github.com/tidwall/sjson"
|
||||||
)
|
)
|
||||||
@@ -24,5 +26,24 @@ func ConvertGeminiCLIRequestToGemini(_ string, inputRawJSON []byte, _ bool) []by
|
|||||||
rawJSON, _ = sjson.SetRawBytes(rawJSON, "system_instruction", []byte(gjson.GetBytes(rawJSON, "systemInstruction").Raw))
|
rawJSON, _ = sjson.SetRawBytes(rawJSON, "system_instruction", []byte(gjson.GetBytes(rawJSON, "systemInstruction").Raw))
|
||||||
rawJSON, _ = sjson.DeleteBytes(rawJSON, "systemInstruction")
|
rawJSON, _ = sjson.DeleteBytes(rawJSON, "systemInstruction")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toolsResult := gjson.GetBytes(rawJSON, "tools")
|
||||||
|
if toolsResult.Exists() && toolsResult.IsArray() {
|
||||||
|
toolResults := toolsResult.Array()
|
||||||
|
for i := 0; i < len(toolResults); i++ {
|
||||||
|
functionDeclarationsResult := gjson.GetBytes(rawJSON, fmt.Sprintf("tools.%d.function_declarations", i))
|
||||||
|
if functionDeclarationsResult.Exists() && functionDeclarationsResult.IsArray() {
|
||||||
|
functionDeclarationsResults := functionDeclarationsResult.Array()
|
||||||
|
for j := 0; j < len(functionDeclarationsResults); j++ {
|
||||||
|
parametersResult := gjson.GetBytes(rawJSON, fmt.Sprintf("tools.%d.function_declarations.%d.parameters", i, j))
|
||||||
|
if parametersResult.Exists() {
|
||||||
|
strJson, _ := util.RenameKey(string(rawJSON), fmt.Sprintf("tools.%d.function_declarations.%d.parameters", i, j), fmt.Sprintf("tools.%d.function_declarations.%d.parametersJsonSchema", i, j))
|
||||||
|
rawJSON = []byte(strJson)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return rawJSON
|
return rawJSON
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
"github.com/tidwall/sjson"
|
"github.com/tidwall/sjson"
|
||||||
)
|
)
|
||||||
@@ -24,6 +25,24 @@ func ConvertGeminiRequestToGemini(_ string, inputRawJSON []byte, _ bool) []byte
|
|||||||
return rawJSON
|
return rawJSON
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toolsResult := gjson.GetBytes(rawJSON, "tools")
|
||||||
|
if toolsResult.Exists() && toolsResult.IsArray() {
|
||||||
|
toolResults := toolsResult.Array()
|
||||||
|
for i := 0; i < len(toolResults); i++ {
|
||||||
|
functionDeclarationsResult := gjson.GetBytes(rawJSON, fmt.Sprintf("tools.%d.function_declarations", i))
|
||||||
|
if functionDeclarationsResult.Exists() && functionDeclarationsResult.IsArray() {
|
||||||
|
functionDeclarationsResults := functionDeclarationsResult.Array()
|
||||||
|
for j := 0; j < len(functionDeclarationsResults); j++ {
|
||||||
|
parametersResult := gjson.GetBytes(rawJSON, fmt.Sprintf("tools.%d.function_declarations.%d.parameters", i, j))
|
||||||
|
if parametersResult.Exists() {
|
||||||
|
strJson, _ := util.RenameKey(string(rawJSON), fmt.Sprintf("tools.%d.function_declarations.%d.parameters", i, j), fmt.Sprintf("tools.%d.function_declarations.%d.parametersJsonSchema", i, j))
|
||||||
|
rawJSON = []byte(strJson)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Walk contents and fix roles
|
// Walk contents and fix roles
|
||||||
out := rawJSON
|
out := rawJSON
|
||||||
prevRole := ""
|
prevRole := ""
|
||||||
|
|||||||
@@ -26,21 +26,6 @@ import (
|
|||||||
// - []byte: The transformed request data in Gemini API format
|
// - []byte: The transformed request data in Gemini API format
|
||||||
func ConvertOpenAIRequestToGemini(modelName string, inputRawJSON []byte, _ bool) []byte {
|
func ConvertOpenAIRequestToGemini(modelName string, inputRawJSON []byte, _ bool) []byte {
|
||||||
rawJSON := bytes.Clone(inputRawJSON)
|
rawJSON := bytes.Clone(inputRawJSON)
|
||||||
var pathsToDelete []string
|
|
||||||
root := gjson.ParseBytes(rawJSON)
|
|
||||||
util.Walk(root, "", "additionalProperties", &pathsToDelete)
|
|
||||||
util.Walk(root, "", "$schema", &pathsToDelete)
|
|
||||||
util.Walk(root, "", "ref", &pathsToDelete)
|
|
||||||
util.Walk(root, "", "strict", &pathsToDelete)
|
|
||||||
|
|
||||||
var err error
|
|
||||||
for _, p := range pathsToDelete {
|
|
||||||
rawJSON, err = sjson.DeleteBytes(rawJSON, p)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Base envelope
|
// Base envelope
|
||||||
out := []byte(`{"contents":[],"generationConfig":{"thinkingConfig":{"include_thoughts":true}}}`)
|
out := []byte(`{"contents":[],"generationConfig":{"thinkingConfig":{"include_thoughts":true}}}`)
|
||||||
|
|
||||||
@@ -290,22 +275,13 @@ func ConvertOpenAIRequestToGemini(modelName string, inputRawJSON []byte, _ bool)
|
|||||||
if t.Get("type").String() == "function" {
|
if t.Get("type").String() == "function" {
|
||||||
fn := t.Get("function")
|
fn := t.Get("function")
|
||||||
if fn.Exists() && fn.IsObject() {
|
if fn.Exists() && fn.IsObject() {
|
||||||
out, _ = sjson.SetRawBytes(out, fdPath+".-1", []byte(fn.Raw))
|
parametersJsonSchema, _ := util.RenameKey(fn.Raw, "parameters", "parametersJsonSchema")
|
||||||
|
out, _ = sjson.SetRawBytes(out, fdPath+".-1", []byte(parametersJsonSchema))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var pathsToType []string
|
|
||||||
root = gjson.ParseBytes(out)
|
|
||||||
util.Walk(root, "", "type", &pathsToType)
|
|
||||||
for _, p := range pathsToType {
|
|
||||||
typeResult := gjson.GetBytes(out, p)
|
|
||||||
if strings.ToLower(typeResult.String()) == "select" {
|
|
||||||
out, _ = sjson.SetBytes(out, p, "STRING")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ func ConvertGeminiResponseToOpenAI(_ context.Context, _ string, originalRequestR
|
|||||||
|
|
||||||
// Process the main content part of the response.
|
// Process the main content part of the response.
|
||||||
partsResult := gjson.GetBytes(rawJSON, "candidates.0.content.parts")
|
partsResult := gjson.GetBytes(rawJSON, "candidates.0.content.parts")
|
||||||
|
hasFunctionCall := false
|
||||||
if partsResult.IsArray() {
|
if partsResult.IsArray() {
|
||||||
partResults := partsResult.Array()
|
partResults := partsResult.Array()
|
||||||
for i := 0; i < len(partResults); i++ {
|
for i := 0; i < len(partResults); i++ {
|
||||||
@@ -121,6 +122,7 @@ func ConvertGeminiResponseToOpenAI(_ context.Context, _ string, originalRequestR
|
|||||||
template, _ = sjson.Set(template, "choices.0.delta.role", "assistant")
|
template, _ = sjson.Set(template, "choices.0.delta.role", "assistant")
|
||||||
} else if functionCallResult.Exists() {
|
} else if functionCallResult.Exists() {
|
||||||
// Handle function call content.
|
// Handle function call content.
|
||||||
|
hasFunctionCall = true
|
||||||
toolCallsResult := gjson.Get(template, "choices.0.delta.tool_calls")
|
toolCallsResult := gjson.Get(template, "choices.0.delta.tool_calls")
|
||||||
functionCallIndex := (*param).(*convertGeminiResponseToOpenAIChatParams).FunctionIndex
|
functionCallIndex := (*param).(*convertGeminiResponseToOpenAIChatParams).FunctionIndex
|
||||||
(*param).(*convertGeminiResponseToOpenAIChatParams).FunctionIndex++
|
(*param).(*convertGeminiResponseToOpenAIChatParams).FunctionIndex++
|
||||||
@@ -172,6 +174,11 @@ func ConvertGeminiResponseToOpenAI(_ context.Context, _ string, originalRequestR
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if hasFunctionCall {
|
||||||
|
template, _ = sjson.Set(template, "choices.0.finish_reason", "tool_calls")
|
||||||
|
template, _ = sjson.Set(template, "choices.0.native_finish_reason", "tool_calls")
|
||||||
|
}
|
||||||
|
|
||||||
return []string{template}
|
return []string{template}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,6 +238,7 @@ func ConvertGeminiResponseToOpenAINonStream(_ context.Context, _ string, origina
|
|||||||
|
|
||||||
// Process the main content part of the response.
|
// Process the main content part of the response.
|
||||||
partsResult := gjson.GetBytes(rawJSON, "candidates.0.content.parts")
|
partsResult := gjson.GetBytes(rawJSON, "candidates.0.content.parts")
|
||||||
|
hasFunctionCall := false
|
||||||
if partsResult.IsArray() {
|
if partsResult.IsArray() {
|
||||||
partsResults := partsResult.Array()
|
partsResults := partsResult.Array()
|
||||||
for i := 0; i < len(partsResults); i++ {
|
for i := 0; i < len(partsResults); i++ {
|
||||||
@@ -252,6 +260,7 @@ func ConvertGeminiResponseToOpenAINonStream(_ context.Context, _ string, origina
|
|||||||
template, _ = sjson.Set(template, "choices.0.message.role", "assistant")
|
template, _ = sjson.Set(template, "choices.0.message.role", "assistant")
|
||||||
} else if functionCallResult.Exists() {
|
} else if functionCallResult.Exists() {
|
||||||
// Append function call content to the tool_calls array.
|
// Append function call content to the tool_calls array.
|
||||||
|
hasFunctionCall = true
|
||||||
toolCallsResult := gjson.Get(template, "choices.0.message.tool_calls")
|
toolCallsResult := gjson.Get(template, "choices.0.message.tool_calls")
|
||||||
if !toolCallsResult.Exists() || !toolCallsResult.IsArray() {
|
if !toolCallsResult.Exists() || !toolCallsResult.IsArray() {
|
||||||
template, _ = sjson.SetRaw(template, "choices.0.message.tool_calls", `[]`)
|
template, _ = sjson.SetRaw(template, "choices.0.message.tool_calls", `[]`)
|
||||||
@@ -297,5 +306,10 @@ func ConvertGeminiResponseToOpenAINonStream(_ context.Context, _ string, origina
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if hasFunctionCall {
|
||||||
|
template, _ = sjson.Set(template, "choices.0.finish_reason", "tool_calls")
|
||||||
|
template, _ = sjson.Set(template, "choices.0.native_finish_reason", "tool_calls")
|
||||||
|
}
|
||||||
|
|
||||||
return template
|
return template
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ func ConvertOpenAIResponsesRequestToGemini(modelName string, inputRawJSON []byte
|
|||||||
if outputResult.IsObject() {
|
if outputResult.IsObject() {
|
||||||
functionResponse, _ = sjson.SetRaw(functionResponse, "functionResponse.response.content", outputResult.String())
|
functionResponse, _ = sjson.SetRaw(functionResponse, "functionResponse.response.content", outputResult.String())
|
||||||
} else {
|
} else {
|
||||||
functionResponse, _ = sjson.Set(functionResponse, "functionResponse.response.content", outputResult.String())
|
functionResponse, _ = sjson.Set(functionResponse, "functionResponse.response.content", output)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,7 +168,7 @@ func ConvertOpenAIResponsesRequestToGemini(modelName string, inputRawJSON []byte
|
|||||||
|
|
||||||
tools.ForEach(func(_, tool gjson.Result) bool {
|
tools.ForEach(func(_, tool gjson.Result) bool {
|
||||||
if tool.Get("type").String() == "function" {
|
if tool.Get("type").String() == "function" {
|
||||||
funcDecl := `{"name":"","description":"","parameters":{}}`
|
funcDecl := `{"name":"","description":"","parametersJsonSchema":{}}`
|
||||||
|
|
||||||
if name := tool.Get("name"); name.Exists() {
|
if name := tool.Get("name"); name.Exists() {
|
||||||
funcDecl, _ = sjson.Set(funcDecl, "name", name.String())
|
funcDecl, _ = sjson.Set(funcDecl, "name", name.String())
|
||||||
@@ -192,7 +192,7 @@ func ConvertOpenAIResponsesRequestToGemini(modelName string, inputRawJSON []byte
|
|||||||
}
|
}
|
||||||
// Set the overall type to OBJECT
|
// Set the overall type to OBJECT
|
||||||
cleaned, _ = sjson.Set(cleaned, "type", "OBJECT")
|
cleaned, _ = sjson.Set(cleaned, "type", "OBJECT")
|
||||||
funcDecl, _ = sjson.SetRaw(funcDecl, "parameters", cleaned)
|
funcDecl, _ = sjson.SetRaw(funcDecl, "parametersJsonSchema", cleaned)
|
||||||
}
|
}
|
||||||
|
|
||||||
geminiTools, _ = sjson.SetRaw(geminiTools, "0.functionDeclarations.-1", funcDecl)
|
geminiTools, _ = sjson.SetRaw(geminiTools, "0.functionDeclarations.-1", funcDecl)
|
||||||
@@ -261,6 +261,5 @@ func ConvertOpenAIResponsesRequestToGemini(modelName string, inputRawJSON []byte
|
|||||||
out, _ = sjson.Set(out, "generationConfig.thinkingConfig.thinkingBudget", -1)
|
out, _ = sjson.Set(out, "generationConfig.thinkingConfig.thinkingBudget", -1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return []byte(out)
|
return []byte(out)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -212,161 +212,3 @@ func FixJSON(input string) string {
|
|||||||
|
|
||||||
return out.String()
|
return out.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SanitizeSchemaForGemini removes JSON Schema fields that are incompatible with Gemini API
|
|
||||||
// to prevent "Proto field is not repeating, cannot start list" errors.
|
|
||||||
//
|
|
||||||
// Parameters:
|
|
||||||
// - schemaJSON: The JSON schema string to sanitize
|
|
||||||
//
|
|
||||||
// Returns:
|
|
||||||
// - string: The sanitized schema string
|
|
||||||
// - error: An error if the operation fails
|
|
||||||
//
|
|
||||||
// This function removes the following incompatible fields:
|
|
||||||
// - additionalProperties: Not supported in Gemini function declarations
|
|
||||||
// - $schema: JSON Schema meta-schema identifier, not needed for API
|
|
||||||
// - allOf/anyOf/oneOf: Union type constructs not supported
|
|
||||||
// - exclusiveMinimum/exclusiveMaximum: Advanced validation constraints
|
|
||||||
// - patternProperties: Advanced property pattern matching
|
|
||||||
// - dependencies: Property dependencies not supported
|
|
||||||
// - type arrays: Converts ["string", "null"] to just "string"
|
|
||||||
func SanitizeSchemaForGemini(schemaJSON string) (string, error) {
|
|
||||||
// Remove top-level incompatible fields
|
|
||||||
fieldsToRemove := []string{
|
|
||||||
"additionalProperties",
|
|
||||||
"$schema",
|
|
||||||
"allOf",
|
|
||||||
"anyOf",
|
|
||||||
"oneOf",
|
|
||||||
"exclusiveMinimum",
|
|
||||||
"exclusiveMaximum",
|
|
||||||
"patternProperties",
|
|
||||||
"dependencies",
|
|
||||||
}
|
|
||||||
|
|
||||||
result := schemaJSON
|
|
||||||
var err error
|
|
||||||
|
|
||||||
for _, field := range fieldsToRemove {
|
|
||||||
result, err = sjson.Delete(result, field)
|
|
||||||
if err != nil {
|
|
||||||
continue // Continue even if deletion fails
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle type arrays by converting them to single types
|
|
||||||
result = sanitizeTypeFields(result)
|
|
||||||
|
|
||||||
// Recursively clean nested objects
|
|
||||||
result = cleanNestedSchemas(result)
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// sanitizeTypeFields converts type arrays to single types for Gemini compatibility
|
|
||||||
func sanitizeTypeFields(jsonStr string) string {
|
|
||||||
// Parse the JSON to find all "type" fields
|
|
||||||
parsed := gjson.Parse(jsonStr)
|
|
||||||
result := jsonStr
|
|
||||||
|
|
||||||
// Walk through all paths to find type fields
|
|
||||||
var typeFields []string
|
|
||||||
walkForTypeFields(parsed, "", &typeFields)
|
|
||||||
|
|
||||||
// Process each type field
|
|
||||||
for _, path := range typeFields {
|
|
||||||
typeValue := gjson.Get(result, path)
|
|
||||||
if typeValue.IsArray() {
|
|
||||||
// Convert array to single type (prioritize string, then others)
|
|
||||||
arr := typeValue.Array()
|
|
||||||
if len(arr) > 0 {
|
|
||||||
var preferredType string
|
|
||||||
for _, t := range arr {
|
|
||||||
typeStr := t.String()
|
|
||||||
if typeStr == "string" {
|
|
||||||
preferredType = "string"
|
|
||||||
break
|
|
||||||
} else if typeStr == "number" || typeStr == "integer" {
|
|
||||||
preferredType = typeStr
|
|
||||||
} else if preferredType == "" {
|
|
||||||
preferredType = typeStr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if preferredType != "" {
|
|
||||||
result, _ = sjson.Set(result, path, preferredType)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// walkForTypeFields recursively finds all "type" field paths in the JSON
|
|
||||||
func walkForTypeFields(value gjson.Result, path string, paths *[]string) {
|
|
||||||
switch value.Type {
|
|
||||||
case gjson.JSON:
|
|
||||||
value.ForEach(func(key, val gjson.Result) bool {
|
|
||||||
var childPath string
|
|
||||||
if path == "" {
|
|
||||||
childPath = key.String()
|
|
||||||
} else {
|
|
||||||
childPath = path + "." + key.String()
|
|
||||||
}
|
|
||||||
if key.String() == "type" {
|
|
||||||
*paths = append(*paths, childPath)
|
|
||||||
}
|
|
||||||
walkForTypeFields(val, childPath, paths)
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
default:
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// cleanNestedSchemas recursively removes incompatible fields from nested schema objects
|
|
||||||
func cleanNestedSchemas(jsonStr string) string {
|
|
||||||
fieldsToRemove := []string{"allOf", "anyOf", "oneOf", "exclusiveMinimum", "exclusiveMaximum"}
|
|
||||||
|
|
||||||
// Find all nested paths that might contain these fields
|
|
||||||
var pathsToClean []string
|
|
||||||
parsed := gjson.Parse(jsonStr)
|
|
||||||
findNestedSchemaPaths(parsed, "", fieldsToRemove, &pathsToClean)
|
|
||||||
|
|
||||||
result := jsonStr
|
|
||||||
// Remove fields from all found paths
|
|
||||||
for _, path := range pathsToClean {
|
|
||||||
result, _ = sjson.Delete(result, path)
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// findNestedSchemaPaths recursively finds paths containing incompatible schema fields
|
|
||||||
func findNestedSchemaPaths(value gjson.Result, path string, fieldsToFind []string, paths *[]string) {
|
|
||||||
switch value.Type {
|
|
||||||
case gjson.JSON:
|
|
||||||
value.ForEach(func(key, val gjson.Result) bool {
|
|
||||||
var childPath string
|
|
||||||
if path == "" {
|
|
||||||
childPath = key.String()
|
|
||||||
} else {
|
|
||||||
childPath = path + "." + key.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if this key is one we want to remove
|
|
||||||
for _, field := range fieldsToFind {
|
|
||||||
if key.String() == field {
|
|
||||||
*paths = append(*paths, childPath)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
findNestedSchemaPaths(val, childPath, fieldsToFind, paths)
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
default:
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user