mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-09-22 18:07:58 +00:00
fix: tool call anchor & wire format, streaming chunk passthrough, WebUI narrow-screen, docs bilingual
This commit is contained in:
@ -44,10 +44,10 @@ type ChatChoice struct {
|
||||
}
|
||||
|
||||
type RespMessage struct {
|
||||
Role string `json:"role"`
|
||||
Content string `json:"content"`
|
||||
ReasoningContent string `json:"reasoning_content,omitempty"`
|
||||
ToolCalls []types.ToolCall `json:"tool_calls,omitempty"`
|
||||
Role string `json:"role,omitempty"`
|
||||
Content string `json:"content"`
|
||||
ReasoningContent string `json:"reasoning_content,omitempty"`
|
||||
ToolCalls json.RawMessage `json:"tool_calls,omitempty"`
|
||||
}
|
||||
|
||||
type ChatChunk struct {
|
||||
@ -77,13 +77,49 @@ func isAuto(m string) bool {
|
||||
}
|
||||
|
||||
// resolveCands picks the ordered candidate providers for a requested model.
|
||||
func (g *Gateway) resolveCands(model string) ([]*provider.Provider, string) {
|
||||
if model == "" || isAuto(model) {
|
||||
// toolCalling requests are anchored: they resolve to exactly one provider
|
||||
// (highest-priority available) so a tool-call round never switches models.
|
||||
func (g *Gateway) resolveCands(req *chatRequest) ([]*provider.Provider, string) {
|
||||
model := req.Model
|
||||
if model == "" {
|
||||
model = g.core.DefaultModel()
|
||||
}
|
||||
cands, effective := g.resolveByModel(model)
|
||||
if !toolRequest(req) {
|
||||
return cands, effective
|
||||
}
|
||||
// tool-call request: pin to one provider (no AUTO fallback across models)
|
||||
if len(cands) == 0 {
|
||||
return nil, effective
|
||||
}
|
||||
first := cands[0]
|
||||
eff := first.ModelFor(model)
|
||||
if eff == "" {
|
||||
eff = firstModel(first)
|
||||
}
|
||||
return []*provider.Provider{first}, eff
|
||||
}
|
||||
|
||||
func (g *Gateway) resolveByModel(model string) ([]*provider.Provider, string) {
|
||||
if isAuto(model) {
|
||||
return g.core.Registry().Resolve("AUTO"), ""
|
||||
}
|
||||
return g.core.Registry().Resolve(model), model
|
||||
}
|
||||
|
||||
// toolRequest reports whether the request participates in a tool-call round.
|
||||
func toolRequest(req *chatRequest) bool {
|
||||
if len(req.Tools) > 0 || req.ToolChoice != nil {
|
||||
return true
|
||||
}
|
||||
for _, m := range req.Messages {
|
||||
if m.Role == "tool" || len(m.ToolCalls) > 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (g *Gateway) handleChat(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "use POST")
|
||||
@ -102,7 +138,7 @@ func (g *Gateway) handleChat(w http.ResponseWriter, r *http.Request) {
|
||||
if model == "" {
|
||||
model = g.core.DefaultModel()
|
||||
}
|
||||
cands, effective := g.resolveCands(model)
|
||||
cands, effective := g.resolveCands(&req)
|
||||
if len(cands) == 0 {
|
||||
writeError(w, http.StatusServiceUnavailable, "no_provider", "no LLM source configured")
|
||||
return
|
||||
@ -159,6 +195,31 @@ func imageOnly(cands []*provider.Provider) []*provider.Provider {
|
||||
return out
|
||||
}
|
||||
|
||||
// toolCallsWire converts unified tool calls to the OpenAI wire format:
|
||||
// tool_calls:[{id,type,function:{name,arguments:StringJSON}}]. Clients expect
|
||||
// arguments to be a JSON string, not an object.
|
||||
func toolCallsWire(tcs []types.ToolCall) json.RawMessage {
|
||||
wire := make([]map[string]interface{}, 0, len(tcs))
|
||||
for _, tc := range tcs {
|
||||
args := "{}"
|
||||
if tc.Arguments != nil {
|
||||
if b, err := json.Marshal(tc.Arguments); err == nil {
|
||||
args = string(b)
|
||||
}
|
||||
}
|
||||
wire = append(wire, map[string]interface{}{
|
||||
"id": tc.ID,
|
||||
"type": tc.Type,
|
||||
"function": map[string]interface{}{
|
||||
"name": tc.Name,
|
||||
"arguments": args,
|
||||
},
|
||||
})
|
||||
}
|
||||
b, _ := json.Marshal(wire)
|
||||
return b
|
||||
}
|
||||
|
||||
func (g *Gateway) singleChat(w http.ResponseWriter, ctx context.Context, cands []*provider.Provider, req *types.ChatRequest, effective string) {
|
||||
resp, err := g.core.Scheduler().Chat(ctx, scheduler.FromRegistry(cands), req)
|
||||
if err != nil {
|
||||
@ -170,7 +231,7 @@ func (g *Gateway) singleChat(w http.ResponseWriter, ctx context.Context, cands [
|
||||
msg.ReasoningContent = resp.ReasoningContent
|
||||
}
|
||||
if len(resp.ToolCalls) > 0 {
|
||||
msg.ToolCalls = resp.ToolCalls
|
||||
msg.ToolCalls = toolCallsWire(resp.ToolCalls)
|
||||
}
|
||||
out := ChatCompletion{
|
||||
ID: newID(),
|
||||
@ -223,7 +284,7 @@ func (g *Gateway) streamChat(w http.ResponseWriter, ctx context.Context, cands [
|
||||
chunk := ChatChunk{
|
||||
ID: id, Object: "chat.completion.chunk", Created: created, Model: effective,
|
||||
}
|
||||
delta := RespMessage{Content: ck.Content}
|
||||
delta := RespMessage{Role: "assistant", Content: ck.Content}
|
||||
if ck.ReasoningContent != "" {
|
||||
delta.ReasoningContent = ck.ReasoningContent
|
||||
}
|
||||
@ -269,7 +330,7 @@ func (g *Gateway) handleImage(w http.ResponseWriter, r *http.Request) {
|
||||
if model == "" {
|
||||
model = g.core.DefaultModel()
|
||||
}
|
||||
cands, _ := g.resolveCands(model)
|
||||
cands, _ := g.resolveByModel(model)
|
||||
cands = imageOnly(cands)
|
||||
if len(cands) == 0 {
|
||||
writeError(w, http.StatusServiceUnavailable, "no_provider", "no image source configured")
|
||||
|
||||
Reference in New Issue
Block a user