mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-10-05 15:07:51 +00:00
fix(gemini): endpoint 自相矛盾导致预置模板必失败
gemini.lua 里 adapter.endpoint = "/v1/models",而它自己的注释写的是
POST /v1/models/{model}:generateContent
两者矛盾,而 Go 侧是静态拼接(provider.URL = base_url + endpoint),拼不出
模型名。预置模板 "Google Gemini"(base_url=.../v1beta)于是会 POST 到
https://generativelanguage.googleapis.com/v1beta/v1/models
既多一段 /v1,又缺 :generateContent——那是 Gemini 的模型**列表**端点,
对 POST 返 405。所以任何用户从模板建这个源,拿到的都是必定失败的源。
实测确认影响范围:线上 21 个源里没有 gemini(openai×15 / trae / sensenova /
opencodezen / deepseek / anthropic / agentrouter),所以是潜伏缺陷。
修法:endpoint 改成模板 `/v1beta/models/{model}:generateContent`,新增
provider.ChatURL(model, stream):
- 用 **PathEscape** 替换 {model}——模型 id 进的是 URL 路径,不转义的话
一个 "/" 就会静默指向另一个资源(判据里用 RequestURI 而非 URL.Path
断言,因为后者是解码后的,看不出 %2F);
- 流式把 ":generateContent" 换成 ":streamGenerateContent"(同一个路径、
不同动词,也在路径里)。替换刻意只认这个精确后缀,免得别的适配器
仅仅提到这个词就被改写;
- source 自己设的 endpoint: 仍然优先,模板被整体跳过。
Chat / ChatStream / probeChat 三处调用点改为传本次请求真实的 model——AUTO
按槽位把 req.Model 钉死,所以 URL 必须跟随**请求**的模型,用源默认模型会让
多模型源每次都打同一个(还记到别的模型的账上)。
判定静态 endpoint 的其他 10 个适配器零影响(TestNonGeminiEndpointsAreUntouched)。
顺带:Stats 的 mutex 不是可重入的,导出方法自己加锁、*Locked 后缀要求调用
方持锁。持锁调导出方法会死锁——我的探针真卡死过一次(直到 10 分钟超时)。
补上 LOCKING 注释,并加判据把这条规则钉住(含一个 20 秒上限的行为判据,
让未来的重构撞死锁时快速失败而不是拖满整个套件)。
This commit is contained in:
@ -531,7 +531,9 @@ func isAutoID(s string) bool {
|
||||
return s == "" || strings.EqualFold(s, "AUTO")
|
||||
}
|
||||
|
||||
// Endpoint resolves the upstream chat path.
|
||||
// Endpoint resolves the upstream chat path. It may contain the placeholder
|
||||
// "{model}"; use URL (or ChatURL) rather than calling this directly when the
|
||||
// path has to be usable.
|
||||
func (p *Provider) Endpoint() string {
|
||||
if p.cfg.Endpoint != "" {
|
||||
return p.cfg.Endpoint
|
||||
@ -553,8 +555,40 @@ func (p *Provider) ImageEndpoint() string {
|
||||
return "/v1/images/generations"
|
||||
}
|
||||
|
||||
// modelPlaceholder is the marker an adapter puts in its endpoint template when
|
||||
// the upstream carries the model id in the PATH rather than in the body.
|
||||
// Gemini is the only such adapter: POST /v1beta/models/{model}:generateContent.
|
||||
// Every other adapter's endpoint is a fixed path, so substituting is a no-op
|
||||
// for them.
|
||||
const modelPlaceholder = "{model}"
|
||||
|
||||
// ChatURL resolves the full chat URL for a specific model.
|
||||
//
|
||||
// Two substitutions happen here, both driven by the adapter's endpoint template:
|
||||
//
|
||||
// - "{model}" is replaced by the model id actually being sent. Without this
|
||||
// the gateway would POST to Gemini's model-LIST endpoint, which answers 405.
|
||||
// - for a streaming request, the ":generateContent" verb becomes
|
||||
// ":streamGenerateContent". Gemini streams over the same path with a
|
||||
// different verb, and the verb is part of the path, so the two cannot both
|
||||
// be static. The rewrite is deliberately narrow: it only fires on the exact
|
||||
// ":generateContent" suffix, so an adapter whose endpoint merely mentions
|
||||
// the word keeps its path untouched.
|
||||
func (p *Provider) ChatURL(model string, stream bool) string {
|
||||
ep := p.Endpoint()
|
||||
if strings.Contains(ep, modelPlaceholder) {
|
||||
// A model id is put in a URL path, so it must be escaped: an id with a
|
||||
// slash would otherwise silently address a different resource.
|
||||
ep = strings.ReplaceAll(ep, modelPlaceholder, url.PathEscape(model))
|
||||
}
|
||||
if stream {
|
||||
ep = strings.Replace(ep, ":generateContent", ":streamGenerateContent", 1)
|
||||
}
|
||||
return strings.TrimRight(p.cfg.BaseURL, "/") + ep
|
||||
}
|
||||
|
||||
func (p *Provider) URL() string {
|
||||
return strings.TrimRight(p.cfg.BaseURL, "/") + p.Endpoint()
|
||||
return p.ChatURL("", false)
|
||||
}
|
||||
|
||||
func (p *Provider) ImageURL() string {
|
||||
@ -750,10 +784,11 @@ func (p *Provider) probeChat(ctx context.Context) (bool, string) {
|
||||
body, err := json.Marshal(probe)
|
||||
if err == nil {
|
||||
var hdr http.Header
|
||||
if hdrs, herr := p.buildHeaders(string(body), p.URL(), ""); herr == nil {
|
||||
probeURL := p.ChatURL(model, false)
|
||||
if hdrs, herr := p.buildHeaders(string(body), probeURL, ""); herr == nil {
|
||||
hdr = hdrs
|
||||
}
|
||||
raw, status, derr := p.do(ctx, p.URL(), string(body), hdr)
|
||||
raw, status, derr := p.do(ctx, probeURL, string(body), hdr)
|
||||
if derr != nil {
|
||||
msg = derr.Error()
|
||||
} else if status == 200 {
|
||||
@ -1084,11 +1119,12 @@ func (p *Provider) Chat(ctx context.Context, req *types.ChatRequest) (*types.Uni
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hdrs, err := p.buildHeaders(body, p.URL(), req.ClientSession)
|
||||
chatURL := p.ChatURL(model, false)
|
||||
hdrs, err := p.buildHeaders(body, chatURL, req.ClientSession)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
raw, status, err := p.do(ctx, p.URL(), body, hdrs)
|
||||
raw, status, err := p.do(ctx, chatURL, body, hdrs)
|
||||
if err != nil {
|
||||
// a client disconnect or cancelled context is neither a success nor
|
||||
// a failure for scheduling purposes — only upstream errors count
|
||||
@ -1144,7 +1180,10 @@ func (p *Provider) ChatStream(ctx context.Context, req *types.ChatRequest) (<-ch
|
||||
p.Release()
|
||||
return nil, err
|
||||
}
|
||||
hdrs, err := p.buildHeaders(body, p.URL(), req.ClientSession)
|
||||
// stream=true so a path-carried model plus the streaming verb is resolved
|
||||
// for THIS request's model, not the source default.
|
||||
streamURL := p.ChatURL(model, true)
|
||||
hdrs, err := p.buildHeaders(body, streamURL, req.ClientSession)
|
||||
if err != nil {
|
||||
p.Release()
|
||||
return nil, err
|
||||
@ -1156,7 +1195,7 @@ func (p *Provider) ChatStream(ctx context.Context, req *types.ChatRequest) (<-ch
|
||||
}
|
||||
rc := make(chan respOrErr, 1)
|
||||
go func() {
|
||||
resp, err := p.doRawStream(ctx, p.URL(), body, hdrs)
|
||||
resp, err := p.doRawStream(ctx, streamURL, body, hdrs)
|
||||
rc <- respOrErr{resp, err}
|
||||
}()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user