mirror of
https://wget.la/https://github.com/leookun/cursor-byok
synced 2026-08-17 03:27:02 +08:00
feat: add support for custom OpenAI endpoint path
- Introduced a new endpoint option for OpenAI integrations, allowing users to specify a custom path. - Updated relevant components and validation logic to accommodate the new endpoint. - Enhanced documentation and error messages to reflect the addition of the custom endpoint option.
This commit is contained in:
@@ -304,15 +304,44 @@ func OpenAIEndpointURL(baseURL string, endpoint string) string {
|
||||
if !strings.HasPrefix(normalizedEndpoint, "/") {
|
||||
normalizedEndpoint = "/" + normalizedEndpoint
|
||||
}
|
||||
// 规则0:自定义路径模式 → 直接用 baseURL 作为完整请求地址
|
||||
if normalizedEndpoint == modelchannel.OpenAIEndpointCustom {
|
||||
return base
|
||||
}
|
||||
// 规则1:baseURL 已含 endpoint 后缀 → 直接用 base
|
||||
if OpenAIEndpointFromBaseURL(base) != "" {
|
||||
return base
|
||||
}
|
||||
if strings.HasSuffix(base, "/v1") && strings.HasPrefix(normalizedEndpoint, "/v1/") {
|
||||
return base + strings.TrimPrefix(normalizedEndpoint, "/v1")
|
||||
// 规则2:通用版本段去重(/v1 /v2 /v3 /v4 ... 任意版本号)
|
||||
if version, ok := trailingVersionSegment(base); ok {
|
||||
prefix := "/" + version + "/"
|
||||
if strings.HasPrefix(normalizedEndpoint, prefix) {
|
||||
return base + strings.TrimPrefix(normalizedEndpoint, "/"+version)
|
||||
}
|
||||
}
|
||||
// 规则3:兜底原样拼接
|
||||
return base + normalizedEndpoint
|
||||
}
|
||||
|
||||
// trailingVersionSegment 检测 URL 末尾是否以 /vN 形式结尾(N 为数字),
|
||||
// 返回版本段(如 "v4")和是否匹配。用于通用版本段去重。
|
||||
func trailingVersionSegment(base string) (string, bool) {
|
||||
idx := strings.LastIndex(base, "/")
|
||||
if idx < 0 {
|
||||
return "", false
|
||||
}
|
||||
seg := base[idx+1:]
|
||||
if len(seg) < 2 || seg[0] != 'v' {
|
||||
return "", false
|
||||
}
|
||||
for i := 1; i < len(seg); i++ {
|
||||
if seg[i] < '0' || seg[i] > '9' {
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
return seg, true
|
||||
}
|
||||
|
||||
func ResolveOpenAIEndpoint(baseURL string, endpoint string) string {
|
||||
if endpointFromURL := OpenAIEndpointFromBaseURL(baseURL); endpointFromURL != "" {
|
||||
return endpointFromURL
|
||||
@@ -377,11 +406,11 @@ func (adapter *OpenAIAdapter) Stream(ctx context.Context, req StreamRequest, sin
|
||||
req.OpenAIEndpoint = endpoint
|
||||
if req.RequestKnobs != nil {
|
||||
req.RequestKnobs["openai_endpoint"] = endpoint
|
||||
if endpoint == modelchannel.OpenAIEndpointResponses {
|
||||
if modelchannel.OpenAIEndpointShape(endpoint) == "responses" {
|
||||
req.RequestKnobs["max_output_tokens"] = req.MaxTokens
|
||||
}
|
||||
}
|
||||
if endpoint == modelchannel.OpenAIEndpointResponses {
|
||||
if modelchannel.OpenAIEndpointShape(endpoint) == "responses" {
|
||||
return adapter.streamResponses(ctx, req, baseURL, apiKey, modelID, sink)
|
||||
}
|
||||
return adapter.streamChatCompletions(ctx, req, baseURL, apiKey, modelID, sink)
|
||||
@@ -425,14 +454,14 @@ func (adapter *OpenAIAdapter) streamChatCompletions(ctx context.Context, req Str
|
||||
recordLLMSummaryArtifact(req, buildLLMSummaryPayload(req, "openai", modelID, startedAt, time.Time{}, finishedAt, "", 0, 0, 0, 0, err))
|
||||
return err
|
||||
}
|
||||
applyOpenAIThinkingDisable(bodyMap, req, baseURL, modelID, modelchannel.OpenAIEndpointChatCompletions)
|
||||
applyOpenAIThinkingDisable(bodyMap, req, baseURL, modelID, req.OpenAIEndpoint)
|
||||
if err := ApplyOpenAIExtraParams(bodyMap, req.OpenAIExtraParamsEnabled, req.OpenAIExtraParamsJSON); err != nil {
|
||||
finishedAt = time.Now().UTC()
|
||||
recordLLMSummaryArtifact(req, buildLLMSummaryPayload(req, "openai", modelID, startedAt, time.Time{}, finishedAt, "", 0, 0, 0, 0, err))
|
||||
return err
|
||||
}
|
||||
body = bodyMap
|
||||
requestURL := OpenAIEndpointURL(baseURL, modelchannel.OpenAIEndpointChatCompletions)
|
||||
requestURL := OpenAIEndpointURL(baseURL, req.OpenAIEndpoint)
|
||||
recordLLMRequestArtifact(req, "openai", modelID, "POST", requestURL, body)
|
||||
|
||||
payload, err := json.Marshal(body)
|
||||
@@ -901,7 +930,7 @@ func (adapter *OpenAIAdapter) streamResponses(ctx context.Context, req StreamReq
|
||||
recordLLMSummaryArtifact(req, buildLLMSummaryPayload(req, "openai", modelID, startedAt, time.Time{}, finishedAt, "", 0, 0, 0, 0, err))
|
||||
return err
|
||||
}
|
||||
applyOpenAIThinkingDisable(bodyMap, req, baseURL, modelID, modelchannel.OpenAIEndpointResponses)
|
||||
applyOpenAIThinkingDisable(bodyMap, req, baseURL, modelID, req.OpenAIEndpoint)
|
||||
if err := ApplyOpenAIExtraParams(bodyMap, req.OpenAIExtraParamsEnabled, req.OpenAIExtraParamsJSON); err != nil {
|
||||
finishedAt = time.Now().UTC()
|
||||
recordLLMSummaryArtifact(req, buildLLMSummaryPayload(req, "openai", modelID, startedAt, time.Time{}, finishedAt, "", 0, 0, 0, 0, err))
|
||||
@@ -909,7 +938,7 @@ func (adapter *OpenAIAdapter) streamResponses(ctx context.Context, req StreamReq
|
||||
}
|
||||
body = bodyMap
|
||||
|
||||
requestURL := OpenAIEndpointURL(baseURL, modelchannel.OpenAIEndpointResponses)
|
||||
requestURL := OpenAIEndpointURL(baseURL, req.OpenAIEndpoint)
|
||||
recordLLMRequestArtifact(req, "openai", modelID, "POST", requestURL, body)
|
||||
|
||||
payload, err := json.Marshal(body)
|
||||
@@ -1819,7 +1848,7 @@ func applyOpenAIThinkingDisable(body map[string]any, req StreamRequest, baseURL
|
||||
delete(body, "reasoning_effort")
|
||||
setRequestKnob(req, "thinking_disabled_provider_param", "enable_thinking")
|
||||
case "reasoning_none":
|
||||
if endpoint == modelchannel.OpenAIEndpointResponses {
|
||||
if modelchannel.OpenAIEndpointShape(endpoint) == "responses" {
|
||||
body["reasoning"] = map[string]any{"effort": "none"}
|
||||
} else {
|
||||
body["reasoning_effort"] = "none"
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
package modeladapter
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestTrailingVersionSegment(t *testing.T) {
|
||||
cases := []struct {
|
||||
base string
|
||||
wantSeg string
|
||||
wantOK bool
|
||||
}{
|
||||
{"https://api.openai.com/v1", "v1", true},
|
||||
{"https://api.z.ai/api/coding/paas/v4", "v4", true},
|
||||
{"https://example.com/v2", "v2", true},
|
||||
{"https://example.com/v12", "v12", true},
|
||||
|
||||
// 非版本段
|
||||
{"https://api.openai.com", "", false},
|
||||
{"https://example.com/chat", "", false},
|
||||
{"https://example.com/api", "", false},
|
||||
{"https://example.com/v", "", false},
|
||||
{"https://example.com/vx", "", false},
|
||||
{"", "", false},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.base, func(t *testing.T) {
|
||||
gotSeg, gotOK := trailingVersionSegment(tc.base)
|
||||
if gotSeg != tc.wantSeg || gotOK != tc.wantOK {
|
||||
t.Fatalf("trailingVersionSegment(%q) = (%q, %v), want (%q, %v)", tc.base, gotSeg, gotOK, tc.wantSeg, tc.wantOK)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenAIEndpointURL(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
baseURL string
|
||||
endpoint string
|
||||
want string
|
||||
}{
|
||||
// === issue #166 核心场景:选"自定义路径",baseURL 填完整地址 ===
|
||||
{"custom_zai_full_chat", "https://api.z.ai/api/coding/paas/v4/chat/completions", "/custom", "https://api.z.ai/api/coding/paas/v4/chat/completions"},
|
||||
{"custom_zai_full_responses", "https://api.z.ai/api/coding/paas/v4/responses", "/custom", "https://api.z.ai/api/coding/paas/v4/responses"},
|
||||
|
||||
// === 存量场景回归:OpenAI 官方 /v1 ===
|
||||
{"openai_v1_responses_dedup", "https://api.openai.com/v1", "/v1/responses", "https://api.openai.com/v1/responses"},
|
||||
{"openai_v1_chat_dedup", "https://api.openai.com/v1", "/v1/chat/completions", "https://api.openai.com/v1/chat/completions"},
|
||||
// baseURL 不带版本号
|
||||
{"openai_noversion_chat", "https://api.openai.com", "/v1/chat/completions", "https://api.openai.com/v1/chat/completions"},
|
||||
|
||||
// === baseURL 已含完整 endpoint 后缀 → 直接用 base ===
|
||||
{"baseurl_has_chat_suffix", "https://api.openai.com/v1/chat/completions", "/v1/chat/completions", "https://api.openai.com/v1/chat/completions"},
|
||||
{"baseurl_has_responses_suffix", "https://api.openai.com/v1/responses", "/v1/responses", "https://api.openai.com/v1/responses"},
|
||||
|
||||
// === 空 endpoint 默认 responses ===
|
||||
{"empty_endpoint_defaults", "https://api.openai.com/v1", "", "https://api.openai.com/v1/responses"},
|
||||
|
||||
// === 通用版本段去重:/v2 /v3 ===
|
||||
{"v2_dedup", "https://example.com/v2", "/v2/chat/completions", "https://example.com/v2/chat/completions"},
|
||||
{"v3_dedup", "https://example.com/v3", "/v3/responses", "https://example.com/v3/responses"},
|
||||
|
||||
// === 不同版本号不去重:base /v4 + endpoint /v1 ===
|
||||
{"different_version_no_dedup", "https://example.com/v4", "/v1/chat/completions", "https://example.com/v4/v1/chat/completions"},
|
||||
|
||||
// === 尾部斜杠清理 ===
|
||||
{"trailing_slash_base", "https://api.openai.com/v1/", "/v1/responses", "https://api.openai.com/v1/responses"},
|
||||
|
||||
// === /custom + baseURL 不含已知后缀 → 直接返回 base ===
|
||||
{"custom_no_suffix", "https://api.example.com/some/path", "/custom", "https://api.example.com/some/path"},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got := OpenAIEndpointURL(tc.baseURL, tc.endpoint)
|
||||
if got != tc.want {
|
||||
t.Fatalf("OpenAIEndpointURL(%q, %q)\n got %s\n want %s", tc.baseURL, tc.endpoint, got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -154,7 +154,7 @@ func NormalizeModelAdapterConfigs(input []ModelAdapterConfig) ([]ModelAdapterCon
|
||||
case next.Type == "openai" && next.ReasoningEffort == "":
|
||||
return nil, errors.New("模型适配器 reasoningEffort 仅支持 low、medium、high、xhigh")
|
||||
case next.Type == "openai" && next.OpenAIEndpoint == "":
|
||||
return nil, errors.New("模型适配器 openAIEndpoint 仅支持 /v1/responses 或 /v1/chat/completions")
|
||||
return nil, errors.New("模型适配器 openAIEndpoint 仅支持 /v1/responses、/v1/chat/completions 或 /custom(自定义路径)")
|
||||
case next.Type == "openai" && next.OpenAIExtraParamsEnabled:
|
||||
if err := validateJSONMap(next.OpenAIExtraParamsJSON, "openAIExtraParamsJSON"); err != nil {
|
||||
return nil, err
|
||||
|
||||
Reference in New Issue
Block a user