package api import ( "context" "encoding/json" "fmt" "io" "net/http" "strings" "sync" "time" luaVM "gitcode.com/JianFeeeee/HomeAgent/internal/lua" ) type Message struct { Role string `json:"role"` Content string `json:"content"` ReasoningContent string `json:"reasoning_content,omitempty"` ToolCallID string `json:"tool_call_id,omitempty"` ToolCalls []ToolCall `json:"-"` } func (m Message) MarshalJSON() ([]byte, error) { raw := map[string]interface{}{ "role": m.Role, "content": m.Content, } if m.ReasoningContent != "" { raw["reasoning_content"] = m.ReasoningContent } if m.ToolCallID != "" { raw["tool_call_id"] = m.ToolCallID } if len(m.ToolCalls) > 0 { apiTCs := make([]apiToolCall, len(m.ToolCalls)) for i, tc := range m.ToolCalls { argsBytes, _ := json.Marshal(tc.Arguments) apiTCs[i] = apiToolCall{ ID: tc.ID, Type: "function", Function: apiFunction{ Name: tc.Name, Arguments: string(argsBytes), }, } } raw["tool_calls"] = apiTCs } return json.Marshal(raw) } type CompletionRequest struct { Model string `json:"model,omitempty"` Messages []Message `json:"messages"` Temperature float64 `json:"temperature,omitempty"` MaxTokens int `json:"max_tokens,omitempty"` Stream bool `json:"stream,omitempty"` Tools []interface{} `json:"tools,omitempty"` ToolChoice interface{} `json:"tool_choice,omitempty"` ExtraBody map[string]interface{} `json:"-"` } func (r *CompletionRequest) MarshalJSON() ([]byte, error) { type Alias CompletionRequest data, err := json.Marshal((*Alias)(r)) if err != nil { return nil, err } if len(r.ExtraBody) == 0 { return data, nil } var raw map[string]interface{} if err := json.Unmarshal(data, &raw); err != nil { return nil, err } for k, v := range r.ExtraBody { raw[k] = v } return json.Marshal(raw) } type CompletionResponse struct { Content string `json:"content"` FinishReason string `json:"finish_reason,omitempty"` TokenUsage TokenUsage `json:"token_usage,omitempty"` ToolCalls []ToolCall `json:"tool_calls,omitempty"` } type TokenUsage struct { Prompt int `json:"prompt"` Completion int `json:"completion"` Total int `json:"total"` } type ToolCall struct { ID string `json:"id"` Type string `json:"type"` Name string `json:"name"` Arguments map[string]interface{} `json:"arguments"` } type apiToolCall struct { ID string `json:"id"` Type string `json:"type"` Function apiFunction `json:"function"` } type apiFunction struct { Name string `json:"name"` Arguments string `json:"arguments"` } type StreamChunk struct { Content string `json:"content"` Done bool `json:"done"` ToolCall *ToolCall `json:"tool_call,omitempty"` } type Provider interface { Name() string Chat(ctx context.Context, req *CompletionRequest) (*CompletionResponse, error) ChatStream(ctx context.Context, req *CompletionRequest) (<-chan StreamChunk, error) } type BaseConfig struct { Model string `json:"model"` BaseURL string `json:"base_url"` APIKey string `json:"api_key"` Temperature float64 `json:"temperature"` MaxTokens int `json:"max_tokens"` } type OpenAIProvider struct { cfg BaseConfig client *http.Client } func NewOpenAIProvider(cfg BaseConfig) *OpenAIProvider { if cfg.BaseURL == "" { cfg.BaseURL = "https://api.openai.com/v1" } if cfg.Temperature == 0 { cfg.Temperature = 0.7 } if cfg.MaxTokens == 0 { cfg.MaxTokens = 4096 } return &OpenAIProvider{ cfg: cfg, client: &http.Client{Timeout: 60 * time.Second}, } } func (p *OpenAIProvider) Name() string { return "openai" } func (p *OpenAIProvider) Chat(ctx context.Context, req *CompletionRequest) (*CompletionResponse, error) { if req.Model == "" { req.Model = p.cfg.Model } body, _ := json.Marshal(req) httpReq, _ := http.NewRequestWithContext(ctx, "POST", p.cfg.BaseURL+"/chat/completions", strings.NewReader(string(body))) httpReq.Header.Set("Content-Type", "application/json") httpReq.Header.Set("Authorization", "Bearer "+p.cfg.APIKey) resp, err := p.client.Do(httpReq) if err != nil { return nil, fmt.Errorf("api call: %w", err) } defer resp.Body.Close() if resp.StatusCode != 200 { respBody, _ := io.ReadAll(resp.Body) return nil, fmt.Errorf("api error %d: %s", resp.StatusCode, string(respBody)) } var rawResult struct { Choices []struct { Message struct { Content *string `json:"content"` ReasoningContent *string `json:"reasoning_content"` ToolCalls []rawToolCall `json:"tool_calls"` Role string `json:"role"` } `json:"message"` FinishReason string `json:"finish_reason"` } `json:"choices"` Usage struct { PromptTokens int `json:"prompt_tokens"` CompletionTokens int `json:"completion_tokens"` TotalTokens int `json:"total_tokens"` } `json:"usage"` } if err := json.NewDecoder(resp.Body).Decode(&rawResult); err != nil { return nil, fmt.Errorf("decode: %w", err) } if len(rawResult.Choices) == 0 { return nil, fmt.Errorf("no choices returned") } ch := rawResult.Choices[0] content := "" if ch.Message.Content != nil { content = *ch.Message.Content } var toolCalls []ToolCall for _, tc := range ch.Message.ToolCalls { tc := tc args := make(map[string]interface{}) if err := json.Unmarshal([]byte(tc.Function.Arguments), &args); err != nil { args["_raw"] = tc.Function.Arguments } toolCalls = append(toolCalls, ToolCall{ ID: tc.ID, Type: tc.Type, Name: tc.Function.Name, Arguments: args, }) } return &CompletionResponse{ Content: content, FinishReason: ch.FinishReason, TokenUsage: TokenUsage{ Prompt: rawResult.Usage.PromptTokens, Completion: rawResult.Usage.CompletionTokens, Total: rawResult.Usage.TotalTokens, }, ToolCalls: toolCalls, }, nil } func (p *OpenAIProvider) ChatStream(ctx context.Context, req *CompletionRequest) (<-chan StreamChunk, error) { req.Stream = true ch := make(chan StreamChunk, 64) body, _ := json.Marshal(req) httpReq, _ := http.NewRequestWithContext(ctx, "POST", p.cfg.BaseURL+"/chat/completions", strings.NewReader(string(body))) httpReq.Header.Set("Content-Type", "application/json") httpReq.Header.Set("Authorization", "Bearer "+p.cfg.APIKey) resp, err := p.client.Do(httpReq) if err != nil { return nil, fmt.Errorf("stream api: %w", err) } go func() { defer resp.Body.Close() defer close(ch) decoder := json.NewDecoder(resp.Body) for { var line struct { Choices []struct { Delta struct { Content string `json:"content"` } `json:"delta"` FinishReason *string `json:"finish_reason"` } `json:"choices"` } if err := decoder.Decode(&line); err != nil { break } if len(line.Choices) > 0 { ch <- StreamChunk{ Content: line.Choices[0].Delta.Content, Done: line.Choices[0].FinishReason != nil, } } } }() return ch, nil } type OllamaProvider struct { cfg BaseConfig client *http.Client } func NewOllamaProvider(cfg BaseConfig) *OllamaProvider { if cfg.BaseURL == "" { cfg.BaseURL = "http://localhost:11434" } if cfg.Temperature == 0 { cfg.Temperature = 0.7 } if cfg.MaxTokens == 0 { cfg.MaxTokens = 4096 } return &OllamaProvider{ cfg: cfg, client: &http.Client{Timeout: 120 * time.Second}, } } func (p *OllamaProvider) Name() string { return "ollama" } func (p *OllamaProvider) Chat(ctx context.Context, req *CompletionRequest) (*CompletionResponse, error) { ollamaReq := map[string]interface{}{ "model": req.Model, "messages": req.Messages, "stream": false, "options": map[string]interface{}{ "temperature": req.Temperature, "num_predict": req.MaxTokens, }, } body, _ := json.Marshal(ollamaReq) httpReq, _ := http.NewRequestWithContext(ctx, "POST", p.cfg.BaseURL+"/api/chat", strings.NewReader(string(body))) httpReq.Header.Set("Content-Type", "application/json") resp, err := p.client.Do(httpReq) if err != nil { return nil, fmt.Errorf("ollama chat: %w", err) } defer resp.Body.Close() var result struct { Message struct { Content string `json:"content"` } `json:"message"` DoneReason string `json:"done_reason"` } if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return nil, fmt.Errorf("decode: %w", err) } return &CompletionResponse{ Content: result.Message.Content, FinishReason: result.DoneReason, }, nil } func (p *OllamaProvider) ChatStream(ctx context.Context, req *CompletionRequest) (<-chan StreamChunk, error) { ch := make(chan StreamChunk, 64) go func() { defer close(ch) ch <- StreamChunk{Done: true} }() return ch, nil } type LuaAdaptedProvider struct { name string base Provider vm *luaVM.VM adapter string } func NewLuaAdaptedProvider(base Provider, vm *luaVM.VM, adapter string) *LuaAdaptedProvider { return &LuaAdaptedProvider{ name: fmt.Sprintf("lua_%s", adapter), base: base, vm: vm, adapter: adapter, } } func (p *LuaAdaptedProvider) Name() string { return p.name } func (p *LuaAdaptedProvider) Chat(ctx context.Context, req *CompletionRequest) (*CompletionResponse, error) { inputMap := map[string]interface{}{ "model": req.Model, "messages": messagesToMap(req.Messages), "temperature": req.Temperature, "max_tokens": req.MaxTokens, "stream": false, } transformed, err := p.vm.CallTransform(p.adapter, inputMap) if err != nil { return nil, fmt.Errorf("lua transform: %w", err) } transformedReq := &CompletionRequest{ Model: getString(transformed, "model"), Temperature: getFloat(transformed, "temperature"), MaxTokens: int(getFloat(transformed, "max_tokens")), Stream: false, } if msgs, ok := transformed["messages"].([]interface{}); ok { for _, m := range msgs { if mm, ok := m.(map[string]interface{}); ok { transformedReq.Messages = append(transformedReq.Messages, Message{ Role: getString(mm, "role"), Content: getString(mm, "content"), }) } } } resp, err := p.base.Chat(ctx, transformedReq) if err != nil { return nil, err } return resp, nil } func (p *LuaAdaptedProvider) ChatStream(ctx context.Context, req *CompletionRequest) (<-chan StreamChunk, error) { return p.base.ChatStream(ctx, req) } type ProviderManager struct { mu sync.RWMutex providers map[string]Provider default_ string } func NewProviderManager() *ProviderManager { return &ProviderManager{ providers: make(map[string]Provider), } } func (m *ProviderManager) Register(name string, p Provider) { m.mu.Lock() defer m.mu.Unlock() m.providers[name] = p if m.default_ == "" { m.default_ = name } } func (m *ProviderManager) SetDefault(name string) error { m.mu.Lock() defer m.mu.Unlock() if _, ok := m.providers[name]; !ok { return fmt.Errorf("provider %s not found", name) } m.default_ = name return nil } func (m *ProviderManager) Get(name string) Provider { m.mu.RLock() defer m.mu.RUnlock() if name == "" { name = m.default_ } return m.providers[name] } func (m *ProviderManager) Default() Provider { m.mu.RLock() defer m.mu.RUnlock() return m.providers[m.default_] } func (m *ProviderManager) List() []string { m.mu.RLock() defer m.mu.RUnlock() var names []string for n := range m.providers { names = append(names, n) } return names } type rawToolCall struct { ID string `json:"id"` Type string `json:"type"` Function struct { Name string `json:"name"` Arguments string `json:"arguments"` } `json:"function"` } func messagesToMap(msgs []Message) []interface{} { result := make([]interface{}, len(msgs)) for i, m := range msgs { result[i] = map[string]interface{}{ "role": m.Role, "content": m.Content, } } return result } func getString(m map[string]interface{}, key string) string { if v, ok := m[key]; ok { if s, ok := v.(string); ok { return s } } return "" } func getFloat(m map[string]interface{}, key string) float64 { if v, ok := m[key]; ok { switch n := v.(type) { case float64: return n case int: return float64(n) } } return 0 }