mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-09-20 00:48:00 +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")
|
||||
|
||||
@ -155,6 +155,50 @@ html[data-theme="dark"] .dropzone.dragover, html[data-theme="dark"] .dropzone:ho
|
||||
.empty { color:var(--muted); text-align:center; padding:24px 0; }
|
||||
#modal-wrap { position:fixed; inset:0; background:rgba(15,22,44,.45); display:flex; align-items:flex-start;
|
||||
justify-content:center; overflow:auto; padding:48px 20px; z-index:50; }
|
||||
.twrap { overflow-x:auto; -webkit-overflow-scrolling:touch; }
|
||||
|
||||
/* ---------- responsive / narrow screens ---------- */
|
||||
@media (max-width: 900px) {
|
||||
header { padding:12px 16px; }
|
||||
nav { padding:12px 16px 0; overflow-x:auto; }
|
||||
nav button { padding:7px 12px; white-space:nowrap; }
|
||||
main { padding:16px 16px 40px; }
|
||||
.card { padding:16px; }
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
header { gap:8px; padding:10px 12px; }
|
||||
.brand h1 { font-size:15px; }
|
||||
.brand .sub { display:none; }
|
||||
.hd-actions button { padding:5px 9px; }
|
||||
nav { gap:4px; padding:10px 12px 0; }
|
||||
nav button { padding:6px 10px; font-size:13px; }
|
||||
main { padding:12px 12px 32px; }
|
||||
.card { padding:13px; border-radius:12px; margin-bottom:14px; }
|
||||
.card h2 { font-size:13px; }
|
||||
th,td { padding:8px 10px; }
|
||||
.row { flex-direction:column; gap:0; }
|
||||
.model-row { flex-wrap:wrap; }
|
||||
.model-row input { flex:1 1 120px; }
|
||||
.model-row select { flex:0 0 auto; }
|
||||
.tab-chat { height:calc(100vh - 150px); }
|
||||
.msg { gap:7px; }
|
||||
.avatar { width:24px; height:24px; font-size:11px; }
|
||||
.bubble { max-width:90%; padding:8px 11px; font-size:13px; }
|
||||
.chat-log { padding:14px 12px 6px; gap:14px; }
|
||||
.chat-tools { flex-wrap:wrap; gap:8px; }
|
||||
.chat-tools .tl { display:none; }
|
||||
.chat-tools select { flex:1 1 auto; min-width:0; }
|
||||
.chat-box { gap:7px; }
|
||||
.chat-box .sendbtn { padding:10px 13px; }
|
||||
.attach-btn { width:38px; height:38px; }
|
||||
.chat-box textarea { font-size:13.5px; }
|
||||
.chat-composer { padding:8px 9px 10px; }
|
||||
#modal-wrap { padding:14px 10px; }
|
||||
.dropzone { padding:18px 14px; }
|
||||
#toast { left:12px; right:12px; bottom:12px; text-align:center; }
|
||||
pre.configbox { font-size:11.5px; padding:11px; }
|
||||
.att img { height:52px; max-width:90px; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@ -103,12 +103,41 @@ function adapter.transform_stream_chunk(raw_chunk)
|
||||
if chunk.type == "message_delta" then
|
||||
return json.encode({ content = "", done = (chunk.delta and chunk.delta.stop_reason ~= nil) })
|
||||
end
|
||||
if chunk.type == "content_block_start" and chunk.content_block
|
||||
and chunk.content_block.type == "tool_use" then
|
||||
-- first fragment of a tool call: emit index + id + name, empty args
|
||||
return json.encode({
|
||||
content = "", done = false,
|
||||
tool_calls = { {
|
||||
index = chunk.index or 0,
|
||||
id = chunk.content_block.id or "",
|
||||
type = "function",
|
||||
["function"] = { name = chunk.content_block.name or "", arguments = "" }
|
||||
} }
|
||||
})
|
||||
end
|
||||
if chunk.type == "content_block_delta" and chunk.delta then
|
||||
if chunk.delta.type == "input_json_delta" then
|
||||
-- incremental JSON fragment; clients accumulate across chunks
|
||||
local unified = { content = "", done = false, tool_calls = { {
|
||||
index = chunk.index or 0,
|
||||
id = "",
|
||||
type = "function",
|
||||
["function"] = { name = "", arguments = chunk.delta.partial_json or "" }
|
||||
} } }
|
||||
return json.encode(unified)
|
||||
end
|
||||
if chunk.delta.type == "thinking_delta" and chunk.delta.thinking then
|
||||
return json.encode({ content = "", done = false, reasoning_content = chunk.delta.thinking })
|
||||
end
|
||||
return json.encode({ content = chunk.delta.text or "", done = false })
|
||||
end
|
||||
if chunk.type == "message_stop" then
|
||||
return json.encode({ content = "", done = true })
|
||||
end
|
||||
if chunk.type == "content_block_stop" then
|
||||
return json.encode({ content = "", done = false })
|
||||
end
|
||||
return ""
|
||||
end
|
||||
|
||||
|
||||
@ -20,6 +20,18 @@ function adapter.transform_request(raw_body)
|
||||
req.extra_body.thinking = { type = "disabled" }
|
||||
end
|
||||
req.disable_thinking = nil
|
||||
|
||||
-- V4 thinking 模式要求:带 tool_calls 的 assistant 消息必须回传 reasoning_content。
|
||||
-- OpenAI 兼容客户端不会发该字段,补空串即可通过校验。
|
||||
if req.messages then
|
||||
for _, msg in ipairs(req.messages) do
|
||||
if msg.role == "assistant" and msg.tool_calls and msg.tool_calls[1] then
|
||||
if msg.reasoning_content == nil then
|
||||
msg.reasoning_content = ""
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return json.encode(req)
|
||||
end
|
||||
|
||||
@ -73,10 +85,18 @@ function adapter.transform_stream_chunk(raw_chunk)
|
||||
if not chunk.choices or #chunk.choices == 0 then return "" end
|
||||
local delta = chunk.choices[1].delta or {}
|
||||
local fr = chunk.choices[1].finish_reason
|
||||
return json.encode({
|
||||
|
||||
local unified = {
|
||||
content = delta.content or "",
|
||||
done = (fr ~= nil)
|
||||
})
|
||||
}
|
||||
if delta.reasoning_content then
|
||||
unified.reasoning_content = delta.reasoning_content
|
||||
end
|
||||
if delta.tool_calls then
|
||||
unified.tool_calls = delta.tool_calls
|
||||
end
|
||||
return json.encode(unified)
|
||||
end
|
||||
|
||||
return adapter
|
||||
|
||||
@ -93,16 +93,31 @@ function adapter.transform_stream_chunk(raw_chunk)
|
||||
|
||||
if not chunk.candidates or #chunk.candidates == 0 then return "" end
|
||||
local cand = chunk.candidates[1]
|
||||
local content = ""
|
||||
local unified = { content = "", done = (cand.finishReason ~= nil) }
|
||||
local reasoning = ""
|
||||
local tools = {}
|
||||
if cand.content and cand.content.parts then
|
||||
for _, part in ipairs(cand.content.parts) do
|
||||
content = content .. (part.text or "")
|
||||
if part.text then
|
||||
unified.content = (unified.content or "") .. part.text
|
||||
elseif part.reasoning_content then
|
||||
reasoning = reasoning .. part.reasoning_content
|
||||
elseif part.functionCall then
|
||||
table.insert(tools, {
|
||||
index = #tools,
|
||||
id = part.functionCall.id or ("call_" .. #tools),
|
||||
type = "function",
|
||||
["function"] = {
|
||||
name = part.functionCall.name or "",
|
||||
arguments = part.functionCall.args or "{}"
|
||||
}
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
return json.encode({
|
||||
content = content,
|
||||
done = (cand.finishReason ~= nil)
|
||||
})
|
||||
if reasoning ~= "" then unified.reasoning_content = reasoning end
|
||||
if #tools > 0 then unified.tool_calls = tools end
|
||||
return json.encode(unified)
|
||||
end
|
||||
|
||||
return adapter
|
||||
|
||||
@ -66,10 +66,18 @@ function adapter.transform_stream_chunk(raw_chunk)
|
||||
if not chunk.choices or #chunk.choices == 0 then return "" end
|
||||
local delta = chunk.choices[1].delta or {}
|
||||
local fr = chunk.choices[1].finish_reason
|
||||
return json.encode({
|
||||
|
||||
local unified = {
|
||||
content = delta.content or "",
|
||||
done = (fr ~= nil)
|
||||
})
|
||||
}
|
||||
if delta.reasoning_content then
|
||||
unified.reasoning_content = delta.reasoning_content
|
||||
end
|
||||
if delta.tool_calls then
|
||||
unified.tool_calls = delta.tool_calls
|
||||
end
|
||||
return json.encode(unified)
|
||||
end
|
||||
|
||||
return adapter
|
||||
|
||||
@ -65,10 +65,18 @@ function adapter.transform_stream_chunk(raw_chunk)
|
||||
if not chunk.choices or #chunk.choices == 0 then return "" end
|
||||
local delta = chunk.choices[1].delta or {}
|
||||
local fr = chunk.choices[1].finish_reason
|
||||
return json.encode({
|
||||
|
||||
local unified = {
|
||||
content = delta.content or "",
|
||||
done = (fr ~= nil)
|
||||
})
|
||||
}
|
||||
if delta.reasoning_content then
|
||||
unified.reasoning_content = delta.reasoning_content
|
||||
end
|
||||
if delta.tool_calls then
|
||||
unified.tool_calls = delta.tool_calls
|
||||
end
|
||||
return json.encode(unified)
|
||||
end
|
||||
|
||||
return adapter
|
||||
|
||||
@ -98,10 +98,18 @@ function adapter.transform_stream_chunk(raw_chunk)
|
||||
if not chunk.choices or #chunk.choices == 0 then return "" end
|
||||
local delta = chunk.choices[1].delta or {}
|
||||
local fr = chunk.choices[1].finish_reason
|
||||
return json.encode({
|
||||
|
||||
local unified = {
|
||||
content = delta.content or "",
|
||||
done = (fr ~= nil)
|
||||
})
|
||||
}
|
||||
if delta.reasoning_content then
|
||||
unified.reasoning_content = delta.reasoning_content
|
||||
end
|
||||
if delta.tool_calls then
|
||||
unified.tool_calls = delta.tool_calls
|
||||
end
|
||||
return json.encode(unified)
|
||||
end
|
||||
|
||||
return adapter
|
||||
@ -65,10 +65,18 @@ function adapter.transform_stream_chunk(raw_chunk)
|
||||
if not chunk.choices or #chunk.choices == 0 then return "" end
|
||||
local delta = chunk.choices[1].delta or {}
|
||||
local fr = chunk.choices[1].finish_reason
|
||||
return json.encode({
|
||||
|
||||
local unified = {
|
||||
content = delta.content or "",
|
||||
done = (fr ~= nil)
|
||||
})
|
||||
}
|
||||
if delta.reasoning_content then
|
||||
unified.reasoning_content = delta.reasoning_content
|
||||
end
|
||||
if delta.tool_calls then
|
||||
unified.tool_calls = delta.tool_calls
|
||||
end
|
||||
return json.encode(unified)
|
||||
end
|
||||
|
||||
return adapter
|
||||
|
||||
@ -72,10 +72,29 @@ function adapter.transform_stream_chunk(raw_chunk)
|
||||
if not ok then return "" end
|
||||
if not chunk.message then return "" end
|
||||
|
||||
return json.encode({
|
||||
local unified = {
|
||||
content = chunk.message.content or "",
|
||||
done = chunk.done or false
|
||||
})
|
||||
}
|
||||
if chunk.message.reasoning_content then
|
||||
unified.reasoning_content = chunk.message.reasoning_content
|
||||
end
|
||||
if chunk.message.tool_calls then
|
||||
local tools = {}
|
||||
for _, tc in ipairs(chunk.message.tool_calls) do
|
||||
table.insert(tools, {
|
||||
index = #tools,
|
||||
id = tc.id or ("call_" .. #tools),
|
||||
type = "function",
|
||||
["function"] = {
|
||||
name = tc["function"] and tc["function"].name or "",
|
||||
arguments = tc["function"] and (tc["function"].arguments or "{}") or "{}"
|
||||
}
|
||||
})
|
||||
end
|
||||
unified.tool_calls = tools
|
||||
end
|
||||
return json.encode(unified)
|
||||
end
|
||||
|
||||
return adapter
|
||||
|
||||
@ -71,10 +71,18 @@ function adapter.transform_stream_chunk(raw_chunk)
|
||||
local delta = chunk.choices[1].delta or {}
|
||||
local fr = chunk.choices[1].finish_reason
|
||||
|
||||
return json.encode({
|
||||
local unified = {
|
||||
content = delta.content or "",
|
||||
done = (fr ~= nil)
|
||||
})
|
||||
}
|
||||
if delta.reasoning_content then
|
||||
unified.reasoning_content = delta.reasoning_content
|
||||
end
|
||||
if delta.tool_calls then
|
||||
-- pass raw streaming fragments through; OpenAI clients accumulate index+id+name+arguments
|
||||
unified.tool_calls = delta.tool_calls
|
||||
end
|
||||
return json.encode(unified)
|
||||
end
|
||||
|
||||
return adapter
|
||||
|
||||
@ -100,6 +100,43 @@ func (p *Provider) ModelByID(id string) *config.Model {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ModelFor resolves the model name this provider should send upstream.
|
||||
// If the requested model is not owned by this provider (e.g. an AUTO chain
|
||||
// fallback), it returns this provider's highest-priority chat model instead.
|
||||
func (p *Provider) ModelFor(reqModel string) string {
|
||||
if reqModel == "" || isAutoID(reqModel) {
|
||||
return p.bestChatModel()
|
||||
}
|
||||
if p.ModelByID(reqModel) != nil {
|
||||
return reqModel
|
||||
}
|
||||
return p.bestChatModel()
|
||||
}
|
||||
|
||||
// bestChatModel returns the highest-priority chat-kind model of this source.
|
||||
func (p *Provider) bestChatModel() string {
|
||||
bestID, bestPrio := "", -1
|
||||
for _, m := range p.cfg.Models {
|
||||
if m.Kind != "" && m.Kind != "chat" {
|
||||
continue
|
||||
}
|
||||
if m.Priority > bestPrio {
|
||||
bestPrio = m.Priority
|
||||
bestID = m.ID
|
||||
}
|
||||
}
|
||||
if bestID == "" && len(p.cfg.Models) > 0 {
|
||||
bestID = p.cfg.Models[0].ID
|
||||
}
|
||||
return bestID
|
||||
}
|
||||
|
||||
// IsAutoID reports whether s is an AUTO routing placeholder.
|
||||
func isAutoID(s string) bool {
|
||||
s = strings.TrimSpace(s)
|
||||
return s == "" || strings.EqualFold(s, "AUTO")
|
||||
}
|
||||
|
||||
// Endpoint resolves the upstream chat path.
|
||||
func (p *Provider) Endpoint() string {
|
||||
if p.cfg.Endpoint != "" {
|
||||
|
||||
@ -28,6 +28,7 @@ func New(maxRetries int) *Scheduler {
|
||||
type Provider interface {
|
||||
Name() string
|
||||
Available() bool
|
||||
ModelFor(reqModel string) string
|
||||
Chat(ctx context.Context, req *types.ChatRequest) (*types.UnifiedResponse, error)
|
||||
ChatStream(ctx context.Context, req *types.ChatRequest) (<-chan types.UnifiedChunk, error)
|
||||
Image(ctx context.Context, req *types.ImageGenRequest) (*types.UnifiedResponse, error)
|
||||
@ -42,13 +43,18 @@ func FromRegistry(ps []*provider.Provider) []Provider {
|
||||
return out
|
||||
}
|
||||
|
||||
// Chat runs a chat request across cands, falling back on failure.
|
||||
// Chat runs a chat request across cands, falling back on failure. Each
|
||||
// candidate receives a request pinned to its own model (ModelFor), so an AUTO
|
||||
// chain fallback switches the model id per provider instead of reusing the
|
||||
// first candidate's model name.
|
||||
func (s *Scheduler) Chat(ctx context.Context, cands []Provider, req *types.ChatRequest) (*types.UnifiedResponse, error) {
|
||||
attempts := s.MaxRetries + 1
|
||||
var lastErr error
|
||||
for i := 0; i < attempts && i < len(cands); i++ {
|
||||
p := cands[i]
|
||||
resp, err := p.Chat(ctx, req)
|
||||
r := *req
|
||||
r.Model = p.ModelFor(req.Model)
|
||||
resp, err := p.Chat(ctx, &r)
|
||||
if ctx.Err() != nil {
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
@ -68,13 +74,16 @@ func (s *Scheduler) Chat(ctx context.Context, cands []Provider, req *types.ChatR
|
||||
return nil, lastErr
|
||||
}
|
||||
|
||||
// ChatStream runs a streaming chat across cands, falling back early on connect errors.
|
||||
// ChatStream runs a streaming chat across cands, falling back early on connect
|
||||
// errors. The request model is pinned per candidate like Chat.
|
||||
func (s *Scheduler) ChatStream(ctx context.Context, cands []Provider, req *types.ChatRequest) (<-chan types.UnifiedChunk, error) {
|
||||
attempts := s.MaxRetries + 1
|
||||
var lastErr error
|
||||
for i := 0; i < attempts && i < len(cands); i++ {
|
||||
p := cands[i]
|
||||
resp, err := p.ChatStream(ctx, req)
|
||||
r := *req
|
||||
r.Model = p.ModelFor(req.Model)
|
||||
resp, err := p.ChatStream(ctx, &r)
|
||||
if err == nil {
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
@ -47,7 +47,7 @@ type ChatMessage struct {
|
||||
Content json.RawMessage `json:"content,omitempty"`
|
||||
ReasoningContent string `json:"reasoning_content,omitempty"`
|
||||
ToolCallID string `json:"tool_call_id,omitempty"`
|
||||
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
|
||||
ToolCalls json.RawMessage `json:"tool_calls,omitempty"`
|
||||
}
|
||||
|
||||
func StringContent(s string) json.RawMessage { b, _ := json.Marshal(s); return b }
|
||||
@ -100,11 +100,14 @@ type ImageGenResponse struct {
|
||||
|
||||
// ---- Unified streaming chunk produced by adapters ----
|
||||
|
||||
// UnifiedChunk is one streamed delta. ToolCalls carries the raw upstream
|
||||
// streaming tool_calls array (incremental fragments with an index field), which
|
||||
// OpenAI-compatible clients accumulate themselves.
|
||||
type UnifiedChunk struct {
|
||||
Content string `json:"content"`
|
||||
Done bool `json:"done"`
|
||||
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
|
||||
ReasoningContent string `json:"reasoning_content,omitempty"`
|
||||
Content string `json:"content"`
|
||||
Done bool `json:"done"`
|
||||
ToolCalls json.RawMessage `json:"tool_calls,omitempty"`
|
||||
ReasoningContent string `json:"reasoning_content,omitempty"`
|
||||
}
|
||||
|
||||
// Meta passed to Lua build_headers hook
|
||||
|
||||
Reference in New Issue
Block a user