mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-10-04 00:03:59 +00:00
llm: 统一源接入层修复(对照 llmsproxy)
- provider: 新增 OpenAI-compatible 响应/流兜底解析,Lua adapter 异常时也能解析 choices/message/tool_calls/usage(含 function.arguments 缺失、对象/字符串参数) - 过滤无效 LLM 源(<nil>/空/缺 http(s) scheme),main 与 ReloadFromConfig 均跳过, 避免 mocktest 等坏源污染 fallback 与 healthcheck - adapter(openai/deepseek/groq/mistral/github/kimicode): 修 tool_calls 对 nil function 的崩溃,兼容扁平/嵌套结构;openai 流透传 reasoning/tool_calls - config: ToConfig 探活端点过滤无效 base_url,修复 supervisor 误报 LLM unreachable
This commit is contained in:
@ -277,6 +277,10 @@ func main() {
|
|||||||
|
|
||||||
providerMgr := agentAPI.NewProviderManager()
|
providerMgr := agentAPI.NewProviderManager()
|
||||||
for _, src := range cfg.LLM.Sources {
|
for _, src := range cfg.LLM.Sources {
|
||||||
|
if !agentAPI.IsValidSourceConfig(src.Name, src.BaseURL, src.Model, src.Adapter) {
|
||||||
|
log.Printf("[homed] skip invalid llm source %q (base_url=%q model=%q adapter=%q)", src.Name, src.BaseURL, src.Model, src.Adapter)
|
||||||
|
continue
|
||||||
|
}
|
||||||
key := src.APIKey
|
key := src.APIKey
|
||||||
if key == "" {
|
if key == "" {
|
||||||
key = baseAPIKey
|
key = baseAPIKey
|
||||||
@ -513,12 +517,12 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return &ipc.Status{
|
return &ipc.Status{
|
||||||
PID: os.Getpid(),
|
PID: os.Getpid(),
|
||||||
Boot: *boot,
|
Boot: *boot,
|
||||||
UptimeSec: uptime,
|
UptimeSec: uptime,
|
||||||
LLMOK: &llmOK,
|
LLMOK: &llmOK,
|
||||||
Tools: tools,
|
Tools: tools,
|
||||||
LastDiag: lastDiagSummary(*dataDir),
|
LastDiag: lastDiagSummary(*dataDir),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if err := ipcServer.Start(); err != nil {
|
if err := ipcServer.Start(); err != nil {
|
||||||
|
|||||||
@ -129,9 +129,9 @@ type ToolCall struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type apiToolCall struct {
|
type apiToolCall struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Function apiFunction `json:"function"`
|
Function apiFunction `json:"function"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type apiFunction struct {
|
type apiFunction struct {
|
||||||
@ -140,9 +140,11 @@ type apiFunction struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type StreamChunk struct {
|
type StreamChunk struct {
|
||||||
Content string `json:"content"`
|
Content string `json:"content"`
|
||||||
Done bool `json:"done"`
|
ReasoningContent string `json:"reasoning_content,omitempty"`
|
||||||
ToolCall *ToolCall `json:"tool_call,omitempty"`
|
Done bool `json:"done"`
|
||||||
|
ToolCall *ToolCall `json:"tool_call,omitempty"`
|
||||||
|
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Provider interface {
|
type Provider interface {
|
||||||
@ -227,6 +229,19 @@ func NewLuaAdaptedProvider(cfg BaseConfig, vm *luaVM.VM, name, adapter string) *
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func IsValidSourceConfig(name, baseURL, model, adapter string) bool {
|
||||||
|
return validConfigValue(name) && validConfigValue(baseURL) && validConfigValue(model) && validConfigValue(adapter) &&
|
||||||
|
(strings.HasPrefix(baseURL, "http://") || strings.HasPrefix(baseURL, "https://"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func validConfigValue(s string) bool {
|
||||||
|
s = strings.TrimSpace(s)
|
||||||
|
if s == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return !strings.EqualFold(s, "<nil>") && !strings.EqualFold(s, "null") && !strings.EqualFold(s, "nil")
|
||||||
|
}
|
||||||
|
|
||||||
func (p *LuaAdaptedProvider) MaxContextTokens() int {
|
func (p *LuaAdaptedProvider) MaxContextTokens() int {
|
||||||
if p.cfg.ContextWindow > 0 {
|
if p.cfg.ContextWindow > 0 {
|
||||||
return p.cfg.ContextWindow
|
return p.cfg.ContextWindow
|
||||||
@ -283,17 +298,174 @@ func (p *LuaAdaptedProvider) Chat(ctx context.Context, req *CompletionRequest) (
|
|||||||
|
|
||||||
unifiedJSON, err := p.vm.CallTransformResponse(p.adapter, string(rawResp))
|
unifiedJSON, err := p.vm.CallTransformResponse(p.adapter, string(rawResp))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if parsed, perr := parseOpenAICompatibleResponse(rawResp); perr == nil {
|
||||||
|
return parsed, nil
|
||||||
|
}
|
||||||
return nil, fmt.Errorf("lua transform_response: %w", err)
|
return nil, fmt.Errorf("lua transform_response: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var result CompletionResponse
|
var result CompletionResponse
|
||||||
if err := json.Unmarshal([]byte(unifiedJSON), &result); err != nil {
|
if err := json.Unmarshal([]byte(unifiedJSON), &result); err != nil {
|
||||||
|
if parsed, perr := parseOpenAICompatibleResponse(rawResp); perr == nil {
|
||||||
|
return parsed, nil
|
||||||
|
}
|
||||||
return nil, fmt.Errorf("unmarshal unified response: %w (body: %s)", err, unifiedJSON)
|
return nil, fmt.Errorf("unmarshal unified response: %w (body: %s)", err, unifiedJSON)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &result, nil
|
return &result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseOpenAICompatibleResponse(raw []byte) (*CompletionResponse, error) {
|
||||||
|
var resp struct {
|
||||||
|
Usage struct {
|
||||||
|
PromptTokens int `json:"prompt_tokens"`
|
||||||
|
CompletionTokens int `json:"completion_tokens"`
|
||||||
|
TotalTokens int `json:"total_tokens"`
|
||||||
|
} `json:"usage"`
|
||||||
|
Choices []struct {
|
||||||
|
FinishReason string `json:"finish_reason"`
|
||||||
|
Message struct {
|
||||||
|
Content interface{} `json:"content"`
|
||||||
|
ReasoningContent string `json:"reasoning_content"`
|
||||||
|
ToolCalls []openAIToolCall `json:"tool_calls"`
|
||||||
|
} `json:"message"`
|
||||||
|
} `json:"choices"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(raw, &resp); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
out := &CompletionResponse{
|
||||||
|
TokenUsage: TokenUsage{
|
||||||
|
Prompt: resp.Usage.PromptTokens,
|
||||||
|
Completion: resp.Usage.CompletionTokens,
|
||||||
|
Total: resp.Usage.TotalTokens,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if len(resp.Choices) == 0 {
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
ch := resp.Choices[0]
|
||||||
|
out.FinishReason = ch.FinishReason
|
||||||
|
out.Content = stringifyContent(ch.Message.Content)
|
||||||
|
out.ReasoningContent = ch.Message.ReasoningContent
|
||||||
|
out.ToolCalls = normalizeOpenAIToolCalls(ch.Message.ToolCalls)
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type openAIToolCall struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Function struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Arguments interface{} `json:"arguments"`
|
||||||
|
} `json:"function"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Arguments interface{} `json:"arguments"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeOpenAIToolCalls(raw []openAIToolCall) []ToolCall {
|
||||||
|
if len(raw) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make([]ToolCall, 0, len(raw))
|
||||||
|
for _, tc := range raw {
|
||||||
|
name := tc.Function.Name
|
||||||
|
argsRaw := tc.Function.Arguments
|
||||||
|
if name == "" {
|
||||||
|
name = tc.Name
|
||||||
|
argsRaw = tc.Arguments
|
||||||
|
}
|
||||||
|
if name == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
typ := tc.Type
|
||||||
|
if typ == "" {
|
||||||
|
typ = "function"
|
||||||
|
}
|
||||||
|
out = append(out, ToolCall{
|
||||||
|
ID: tc.ID,
|
||||||
|
Type: typ,
|
||||||
|
Name: name,
|
||||||
|
Arguments: parseToolArguments(argsRaw),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseToolArguments(v interface{}) map[string]interface{} {
|
||||||
|
switch x := v.(type) {
|
||||||
|
case nil:
|
||||||
|
return map[string]interface{}{}
|
||||||
|
case map[string]interface{}:
|
||||||
|
return x
|
||||||
|
case string:
|
||||||
|
if strings.TrimSpace(x) == "" {
|
||||||
|
return map[string]interface{}{}
|
||||||
|
}
|
||||||
|
var m map[string]interface{}
|
||||||
|
if err := json.Unmarshal([]byte(x), &m); err == nil && m != nil {
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
var any interface{}
|
||||||
|
if err := json.Unmarshal([]byte(x), &any); err == nil {
|
||||||
|
return map[string]interface{}{"value": any}
|
||||||
|
}
|
||||||
|
return map[string]interface{}{"raw": x}
|
||||||
|
default:
|
||||||
|
b, _ := json.Marshal(x)
|
||||||
|
var m map[string]interface{}
|
||||||
|
if err := json.Unmarshal(b, &m); err == nil && m != nil {
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
return map[string]interface{}{"value": x}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func stringifyContent(v interface{}) string {
|
||||||
|
switch x := v.(type) {
|
||||||
|
case nil:
|
||||||
|
return ""
|
||||||
|
case string:
|
||||||
|
return x
|
||||||
|
case []interface{}:
|
||||||
|
var b strings.Builder
|
||||||
|
for _, part := range x {
|
||||||
|
if m, ok := part.(map[string]interface{}); ok {
|
||||||
|
if text, ok := m["text"].(string); ok {
|
||||||
|
b.WriteString(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return b.String()
|
||||||
|
default:
|
||||||
|
b, _ := json.Marshal(x)
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseOpenAICompatibleStreamChunk(raw []byte) (StreamChunk, bool) {
|
||||||
|
var resp struct {
|
||||||
|
Choices []struct {
|
||||||
|
Delta struct {
|
||||||
|
Content interface{} `json:"content"`
|
||||||
|
ReasoningContent string `json:"reasoning_content"`
|
||||||
|
ToolCalls []openAIToolCall `json:"tool_calls"`
|
||||||
|
} `json:"delta"`
|
||||||
|
FinishReason *string `json:"finish_reason"`
|
||||||
|
} `json:"choices"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(raw, &resp); err != nil || len(resp.Choices) == 0 {
|
||||||
|
return StreamChunk{}, false
|
||||||
|
}
|
||||||
|
choice := resp.Choices[0]
|
||||||
|
return StreamChunk{
|
||||||
|
Content: stringifyContent(choice.Delta.Content),
|
||||||
|
ReasoningContent: choice.Delta.ReasoningContent,
|
||||||
|
ToolCalls: normalizeOpenAIToolCalls(choice.Delta.ToolCalls),
|
||||||
|
Done: choice.FinishReason != nil,
|
||||||
|
}, true
|
||||||
|
}
|
||||||
|
|
||||||
func (p *LuaAdaptedProvider) ChatStream(ctx context.Context, req *CompletionRequest) (<-chan StreamChunk, error) {
|
func (p *LuaAdaptedProvider) ChatStream(ctx context.Context, req *CompletionRequest) (<-chan StreamChunk, error) {
|
||||||
req.Model = p.cfg.Model
|
req.Model = p.cfg.Model
|
||||||
req.Stream = true
|
req.Stream = true
|
||||||
@ -341,27 +513,15 @@ func (p *LuaAdaptedProvider) ChatStream(ctx context.Context, req *CompletionRequ
|
|||||||
// 尝试用 Lua 变换流块(如果 adapter 定义了 transform_stream_chunk)
|
// 尝试用 Lua 变换流块(如果 adapter 定义了 transform_stream_chunk)
|
||||||
unified, err := p.vm.CallTransformStreamChunk(p.adapter, line)
|
unified, err := p.vm.CallTransformStreamChunk(p.adapter, line)
|
||||||
if err != nil || unified == line {
|
if err != nil || unified == line {
|
||||||
// 无流变换函数或变换透传,尝试标准 SSE 解析
|
// 无流变换函数或变换透传,尝试标准 OpenAI SSE 解析
|
||||||
var raw struct {
|
chunk, ok := parseOpenAICompatibleStreamChunk([]byte(line))
|
||||||
Choices []struct {
|
if !ok {
|
||||||
Delta struct {
|
|
||||||
Content string `json:"content"`
|
|
||||||
} `json:"delta"`
|
|
||||||
FinishReason *string `json:"finish_reason"`
|
|
||||||
} `json:"choices"`
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal([]byte(unified), &raw); err != nil {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if len(raw.Choices) > 0 {
|
select {
|
||||||
select {
|
case ch <- chunk:
|
||||||
case ch <- StreamChunk{
|
case <-ctx.Done():
|
||||||
Content: raw.Choices[0].Delta.Content,
|
return
|
||||||
Done: raw.Choices[0].FinishReason != nil,
|
|
||||||
}:
|
|
||||||
case <-ctx.Done():
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -412,9 +572,9 @@ func (s *SSEScanner) Scan() bool {
|
|||||||
func (s *SSEScanner) Text() string { return s.pending }
|
func (s *SSEScanner) Text() string { return s.pending }
|
||||||
|
|
||||||
type providerStatus struct {
|
type providerStatus struct {
|
||||||
failCount int
|
failCount int
|
||||||
unavailableUntil time.Time
|
unavailableUntil time.Time
|
||||||
permanent bool // 401/403 永久不可用,不自动恢复
|
permanent bool // 401/403 永久不可用,不自动恢复
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProviderManager struct {
|
type ProviderManager struct {
|
||||||
|
|||||||
@ -695,6 +695,14 @@ func (r *ConfigRegistry) GetBool(key string, defaultVal bool) bool {
|
|||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validLLMEndpoint(s string) bool {
|
||||||
|
s = strings.TrimSpace(s)
|
||||||
|
if s == "" || strings.EqualFold(s, "<nil>") || strings.EqualFold(s, "nil") || strings.EqualFold(s, "null") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return strings.HasPrefix(s, "http://") || strings.HasPrefix(s, "https://")
|
||||||
|
}
|
||||||
|
|
||||||
// ToConfig 从 config 表重建 *types.Config
|
// ToConfig 从 config 表重建 *types.Config
|
||||||
func (r *ConfigRegistry) ToConfig() *types.Config {
|
func (r *ConfigRegistry) ToConfig() *types.Config {
|
||||||
cfg := &types.Config{}
|
cfg := &types.Config{}
|
||||||
@ -817,10 +825,12 @@ func (r *ConfigRegistry) ToConfig() *types.Config {
|
|||||||
} else {
|
} else {
|
||||||
seen := make(map[string]bool)
|
seen := make(map[string]bool)
|
||||||
for _, src := range cfg.LLM.Sources {
|
for _, src := range cfg.LLM.Sources {
|
||||||
if src.BaseURL != "" && !seen[src.BaseURL] {
|
baseURL := strings.TrimSpace(src.BaseURL)
|
||||||
seen[src.BaseURL] = true
|
if !validLLMEndpoint(baseURL) || seen[baseURL] {
|
||||||
cfg.Defaults.LLMEndpoints = append(cfg.Defaults.LLMEndpoints, src.BaseURL)
|
continue
|
||||||
}
|
}
|
||||||
|
seen[baseURL] = true
|
||||||
|
cfg.Defaults.LLMEndpoints = append(cfg.Defaults.LLMEndpoints, baseURL)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -45,14 +45,34 @@ function adapter.transform_response(raw_body)
|
|||||||
if type(ch.message.tool_calls) == "table" then
|
if type(ch.message.tool_calls) == "table" then
|
||||||
local tcs = {}
|
local tcs = {}
|
||||||
for _, tc in ipairs(ch.message.tool_calls) do
|
for _, tc in ipairs(ch.message.tool_calls) do
|
||||||
local args_ok, args = pcall(json.decode, tc["function"].arguments)
|
local fn = tc["function"]
|
||||||
if not args_ok then args = {} end
|
local name = tc.name
|
||||||
table.insert(tcs, {
|
local raw_args = tc.arguments
|
||||||
id = tc.id,
|
if type(fn) == "table" then
|
||||||
type = tc.type or "function",
|
name = fn.name or name
|
||||||
name = tc["function"].name,
|
raw_args = fn.arguments or raw_args
|
||||||
arguments = args
|
end
|
||||||
})
|
local args = {}
|
||||||
|
if type(raw_args) == "table" then
|
||||||
|
args = raw_args
|
||||||
|
elseif type(raw_args) == "string" and raw_args ~= "" then
|
||||||
|
local args_ok, decoded = pcall(json.decode, raw_args)
|
||||||
|
if args_ok and type(decoded) == "table" then
|
||||||
|
args = decoded
|
||||||
|
elseif args_ok then
|
||||||
|
args = { value = decoded }
|
||||||
|
else
|
||||||
|
args = { raw = raw_args }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if name ~= nil and name ~= "" then
|
||||||
|
table.insert(tcs, {
|
||||||
|
id = tc.id,
|
||||||
|
type = tc.type or "function",
|
||||||
|
name = name,
|
||||||
|
arguments = args
|
||||||
|
})
|
||||||
|
end
|
||||||
end
|
end
|
||||||
unified.tool_calls = tcs
|
unified.tool_calls = tcs
|
||||||
end
|
end
|
||||||
|
|||||||
@ -42,14 +42,34 @@ function adapter.transform_response(raw_body)
|
|||||||
if type(ch.message.tool_calls) == "table" then
|
if type(ch.message.tool_calls) == "table" then
|
||||||
local tcs = {}
|
local tcs = {}
|
||||||
for _, tc in ipairs(ch.message.tool_calls) do
|
for _, tc in ipairs(ch.message.tool_calls) do
|
||||||
local args_ok, args = pcall(json.decode, tc["function"].arguments)
|
local fn = tc["function"]
|
||||||
if not args_ok then args = {} end
|
local name = tc.name
|
||||||
table.insert(tcs, {
|
local raw_args = tc.arguments
|
||||||
id = tc.id,
|
if type(fn) == "table" then
|
||||||
type = tc.type or "function",
|
name = fn.name or name
|
||||||
name = tc["function"].name,
|
raw_args = fn.arguments or raw_args
|
||||||
arguments = args
|
end
|
||||||
})
|
local args = {}
|
||||||
|
if type(raw_args) == "table" then
|
||||||
|
args = raw_args
|
||||||
|
elseif type(raw_args) == "string" and raw_args ~= "" then
|
||||||
|
local args_ok, decoded = pcall(json.decode, raw_args)
|
||||||
|
if args_ok and type(decoded) == "table" then
|
||||||
|
args = decoded
|
||||||
|
elseif args_ok then
|
||||||
|
args = { value = decoded }
|
||||||
|
else
|
||||||
|
args = { raw = raw_args }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if name ~= nil and name ~= "" then
|
||||||
|
table.insert(tcs, {
|
||||||
|
id = tc.id,
|
||||||
|
type = tc.type or "function",
|
||||||
|
name = name,
|
||||||
|
arguments = args
|
||||||
|
})
|
||||||
|
end
|
||||||
end
|
end
|
||||||
unified.tool_calls = tcs
|
unified.tool_calls = tcs
|
||||||
end
|
end
|
||||||
|
|||||||
@ -41,14 +41,34 @@ function adapter.transform_response(raw_body)
|
|||||||
if type(ch.message.tool_calls) == "table" then
|
if type(ch.message.tool_calls) == "table" then
|
||||||
local tcs = {}
|
local tcs = {}
|
||||||
for _, tc in ipairs(ch.message.tool_calls) do
|
for _, tc in ipairs(ch.message.tool_calls) do
|
||||||
local args_ok, args = pcall(json.decode, tc["function"].arguments)
|
local fn = tc["function"]
|
||||||
if not args_ok then args = {} end
|
local name = tc.name
|
||||||
table.insert(tcs, {
|
local raw_args = tc.arguments
|
||||||
id = tc.id,
|
if type(fn) == "table" then
|
||||||
type = tc.type or "function",
|
name = fn.name or name
|
||||||
name = tc["function"].name,
|
raw_args = fn.arguments or raw_args
|
||||||
arguments = args
|
end
|
||||||
})
|
local args = {}
|
||||||
|
if type(raw_args) == "table" then
|
||||||
|
args = raw_args
|
||||||
|
elseif type(raw_args) == "string" and raw_args ~= "" then
|
||||||
|
local args_ok, decoded = pcall(json.decode, raw_args)
|
||||||
|
if args_ok and type(decoded) == "table" then
|
||||||
|
args = decoded
|
||||||
|
elseif args_ok then
|
||||||
|
args = { value = decoded }
|
||||||
|
else
|
||||||
|
args = { raw = raw_args }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if name ~= nil and name ~= "" then
|
||||||
|
table.insert(tcs, {
|
||||||
|
id = tc.id,
|
||||||
|
type = tc.type or "function",
|
||||||
|
name = name,
|
||||||
|
arguments = args
|
||||||
|
})
|
||||||
|
end
|
||||||
end
|
end
|
||||||
unified.tool_calls = tcs
|
unified.tool_calls = tcs
|
||||||
end
|
end
|
||||||
|
|||||||
103
internal/lua/adapters/kimicode.lua
Normal file
103
internal/lua/adapters/kimicode.lua
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
local adapter = {}
|
||||||
|
|
||||||
|
adapter.name = "kimicode"
|
||||||
|
adapter.version = "1.0.0"
|
||||||
|
adapter.endpoint = "/v1/chat/completions"
|
||||||
|
adapter.headers = {}
|
||||||
|
|
||||||
|
function adapter.transform_request(raw_body)
|
||||||
|
local ok, req = pcall(json.decode, raw_body)
|
||||||
|
if not ok then return raw_body end
|
||||||
|
req.model = req.model or "kimi-k2"
|
||||||
|
req.disable_thinking = nil
|
||||||
|
req.extra_body = nil
|
||||||
|
if req.messages then
|
||||||
|
for _, msg in ipairs(req.messages) do
|
||||||
|
msg.reasoning_content = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return json.encode(req)
|
||||||
|
end
|
||||||
|
|
||||||
|
function adapter.transform_response(raw_body)
|
||||||
|
local ok, resp = pcall(json.decode, raw_body)
|
||||||
|
if not ok or resp == nil then return raw_body end
|
||||||
|
|
||||||
|
local unified = {
|
||||||
|
content = "",
|
||||||
|
finish_reason = "",
|
||||||
|
token_usage = { prompt = 0, completion = 0, total = 0 }
|
||||||
|
}
|
||||||
|
if type(resp.usage) == "table" then
|
||||||
|
unified.token_usage.prompt = resp.usage.prompt_tokens or 0
|
||||||
|
unified.token_usage.completion = resp.usage.completion_tokens or 0
|
||||||
|
unified.token_usage.total = resp.usage.total_tokens or 0
|
||||||
|
end
|
||||||
|
if type(resp.choices) == "table" and #resp.choices > 0 then
|
||||||
|
local ch = resp.choices[1]
|
||||||
|
if type(ch.message) == "table" then
|
||||||
|
unified.content = ch.message.content or ""
|
||||||
|
if ch.message.reasoning_content then
|
||||||
|
unified.reasoning_content = ch.message.reasoning_content
|
||||||
|
end
|
||||||
|
if type(ch.message.tool_calls) == "table" then
|
||||||
|
local tcs = {}
|
||||||
|
for _, tc in ipairs(ch.message.tool_calls) do
|
||||||
|
local fn = tc["function"]
|
||||||
|
local name = tc.name
|
||||||
|
local raw_args = tc.arguments
|
||||||
|
if type(fn) == "table" then
|
||||||
|
name = fn.name or name
|
||||||
|
raw_args = fn.arguments or raw_args
|
||||||
|
end
|
||||||
|
local args = {}
|
||||||
|
if type(raw_args) == "table" then
|
||||||
|
args = raw_args
|
||||||
|
elseif type(raw_args) == "string" and raw_args ~= "" then
|
||||||
|
local args_ok, decoded = pcall(json.decode, raw_args)
|
||||||
|
if args_ok and type(decoded) == "table" then
|
||||||
|
args = decoded
|
||||||
|
elseif args_ok then
|
||||||
|
args = { value = decoded }
|
||||||
|
else
|
||||||
|
args = { raw = raw_args }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if name ~= nil and name ~= "" then
|
||||||
|
table.insert(tcs, {
|
||||||
|
id = tc.id,
|
||||||
|
type = tc.type or "function",
|
||||||
|
name = name,
|
||||||
|
arguments = args
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
unified.tool_calls = tcs
|
||||||
|
end
|
||||||
|
end
|
||||||
|
unified.finish_reason = ch.finish_reason or ""
|
||||||
|
end
|
||||||
|
return json.encode(unified)
|
||||||
|
end
|
||||||
|
|
||||||
|
function adapter.transform_stream_chunk(raw_chunk)
|
||||||
|
local ok, chunk = pcall(json.decode, raw_chunk)
|
||||||
|
if not ok then return "" end
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
||||||
@ -41,14 +41,34 @@ function adapter.transform_response(raw_body)
|
|||||||
if type(ch.message.tool_calls) == "table" then
|
if type(ch.message.tool_calls) == "table" then
|
||||||
local tcs = {}
|
local tcs = {}
|
||||||
for _, tc in ipairs(ch.message.tool_calls) do
|
for _, tc in ipairs(ch.message.tool_calls) do
|
||||||
local args_ok, args = pcall(json.decode, tc["function"].arguments)
|
local fn = tc["function"]
|
||||||
if not args_ok then args = {} end
|
local name = tc.name
|
||||||
table.insert(tcs, {
|
local raw_args = tc.arguments
|
||||||
id = tc.id,
|
if type(fn) == "table" then
|
||||||
type = tc.type or "function",
|
name = fn.name or name
|
||||||
name = tc["function"].name,
|
raw_args = fn.arguments or raw_args
|
||||||
arguments = args
|
end
|
||||||
})
|
local args = {}
|
||||||
|
if type(raw_args) == "table" then
|
||||||
|
args = raw_args
|
||||||
|
elseif type(raw_args) == "string" and raw_args ~= "" then
|
||||||
|
local args_ok, decoded = pcall(json.decode, raw_args)
|
||||||
|
if args_ok and type(decoded) == "table" then
|
||||||
|
args = decoded
|
||||||
|
elseif args_ok then
|
||||||
|
args = { value = decoded }
|
||||||
|
else
|
||||||
|
args = { raw = raw_args }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if name ~= nil and name ~= "" then
|
||||||
|
table.insert(tcs, {
|
||||||
|
id = tc.id,
|
||||||
|
type = tc.type or "function",
|
||||||
|
name = name,
|
||||||
|
arguments = args
|
||||||
|
})
|
||||||
|
end
|
||||||
end
|
end
|
||||||
unified.tool_calls = tcs
|
unified.tool_calls = tcs
|
||||||
end
|
end
|
||||||
|
|||||||
@ -45,14 +45,34 @@ function adapter.transform_response(raw_body)
|
|||||||
if type(ch.message.tool_calls) == "table" then
|
if type(ch.message.tool_calls) == "table" then
|
||||||
local tcs = {}
|
local tcs = {}
|
||||||
for _, tc in ipairs(ch.message.tool_calls) do
|
for _, tc in ipairs(ch.message.tool_calls) do
|
||||||
local args_ok, args = pcall(json.decode, tc["function"].arguments)
|
local fn = tc["function"]
|
||||||
if not args_ok then args = {} end
|
local name = tc.name
|
||||||
table.insert(tcs, {
|
local raw_args = tc.arguments
|
||||||
id = tc.id,
|
if type(fn) == "table" then
|
||||||
type = tc.type or "function",
|
name = fn.name or name
|
||||||
name = tc["function"].name,
|
raw_args = fn.arguments or raw_args
|
||||||
arguments = args
|
end
|
||||||
})
|
local args = {}
|
||||||
|
if type(raw_args) == "table" then
|
||||||
|
args = raw_args
|
||||||
|
elseif type(raw_args) == "string" and raw_args ~= "" then
|
||||||
|
local args_ok, decoded = pcall(json.decode, raw_args)
|
||||||
|
if args_ok and type(decoded) == "table" then
|
||||||
|
args = decoded
|
||||||
|
elseif args_ok then
|
||||||
|
args = { value = decoded }
|
||||||
|
else
|
||||||
|
args = { raw = raw_args }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if name ~= nil and name ~= "" then
|
||||||
|
table.insert(tcs, {
|
||||||
|
id = tc.id,
|
||||||
|
type = tc.type or "function",
|
||||||
|
name = name,
|
||||||
|
arguments = args
|
||||||
|
})
|
||||||
|
end
|
||||||
end
|
end
|
||||||
unified.tool_calls = tcs
|
unified.tool_calls = tcs
|
||||||
end
|
end
|
||||||
@ -71,10 +91,17 @@ function adapter.transform_stream_chunk(raw_chunk)
|
|||||||
local delta = chunk.choices[1].delta or {}
|
local delta = chunk.choices[1].delta or {}
|
||||||
local fr = chunk.choices[1].finish_reason
|
local fr = chunk.choices[1].finish_reason
|
||||||
|
|
||||||
return json.encode({
|
local unified = {
|
||||||
content = delta.content or "",
|
content = delta.content or "",
|
||||||
done = (fr ~= nil)
|
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
|
end
|
||||||
|
|
||||||
return adapter
|
return adapter
|
||||||
|
|||||||
@ -110,18 +110,21 @@ func (l *llmImpl) ReloadFromConfig() error {
|
|||||||
}
|
}
|
||||||
l.mgr.Reset()
|
l.mgr.Reset()
|
||||||
for _, src := range cfg.LLM.Sources {
|
for _, src := range cfg.LLM.Sources {
|
||||||
|
if !agentAPI.IsValidSourceConfig(src.Name, src.BaseURL, src.Model, src.Adapter) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
key := src.APIKey
|
key := src.APIKey
|
||||||
if key == "" {
|
if key == "" {
|
||||||
key = l.baseAPIKey
|
key = l.baseAPIKey
|
||||||
}
|
}
|
||||||
provider := agentAPI.NewLuaAdaptedProvider(agentAPI.BaseConfig{
|
provider := agentAPI.NewLuaAdaptedProvider(agentAPI.BaseConfig{
|
||||||
Model: src.Model,
|
Model: src.Model,
|
||||||
BaseURL: src.BaseURL,
|
BaseURL: src.BaseURL,
|
||||||
APIKey: key,
|
APIKey: key,
|
||||||
Temperature: cfg.LLM.Temperature,
|
Temperature: cfg.LLM.Temperature,
|
||||||
MaxTokens: cfg.LLM.MaxTokens,
|
MaxTokens: cfg.LLM.MaxTokens,
|
||||||
ContextWindow: src.ContextWindow,
|
ContextWindow: src.ContextWindow,
|
||||||
}, l.lua, src.Name, src.Adapter)
|
}, l.lua, src.Name, src.Adapter)
|
||||||
l.mgr.Register(src.Name, provider)
|
l.mgr.Register(src.Name, provider)
|
||||||
}
|
}
|
||||||
if cfg.LLM.Provider != "" {
|
if cfg.LLM.Provider != "" {
|
||||||
|
|||||||
Reference in New Issue
Block a user