package anthropic import ( "context" "encoding/json" "fmt" "strings" "github.com/rose_cat707/luminary/internal/model" "github.com/rose_cat707/luminary/internal/provider" ) type Client struct { http *provider.HTTPClientWrapper } func New() *Client { return &Client{http: provider.NewHTTPClientWrapper()} } func (c *Client) Type() string { return "anthropic" } func (c *Client) ListModels(ctx context.Context, apiKey, baseURL string) ([]provider.ModelInfo, error) { return c.ListModelsWithConfig(ctx, apiKey, provider.ProviderConfig{ Type: model.ProviderAnthropic, BaseURL: provider.NormalizeBaseURL(baseURL, model.ProviderAnthropic), }) } func (c *Client) ListModelsWithConfig(ctx context.Context, apiKey string, cfg provider.ProviderConfig) ([]provider.ModelInfo, error) { cfg.Type = model.ProviderAnthropic cfg.BaseURL = provider.NormalizeBaseURL(cfg.BaseURL, model.ProviderAnthropic) url := cfg.ResolveEndpointURL(provider.EndpointListModels) resp, err := provider.ForwardHTTP(ctx, c.http.Client, "GET", url, apiKey, model.ProviderAnthropic, nil, nil) if err != nil { return nil, err } if resp.StatusCode >= 400 { return nil, fmt.Errorf("anthropic list models: status %d: %s", resp.StatusCode, string(resp.Body)) } var out struct { Data []struct { ID string `json:"id"` Name string `json:"display_name"` } `json:"data"` } if err := json.Unmarshal(resp.Body, &out); err != nil { return nil, err } models := make([]provider.ModelInfo, 0, len(out.Data)) for _, m := range out.Data { name := m.Name if name == "" { name = m.ID } models = append(models, provider.ModelInfo{ID: m.ID, DisplayName: name}) } return models, nil } func (c *Client) ChatCompletion(ctx context.Context, apiKey, baseURL string, req *provider.ChatRequest) (*provider.ChatResponse, error) { cfg := provider.ProviderConfig{ Type: model.ProviderAnthropic, BaseURL: provider.NormalizeBaseURL(baseURL, model.ProviderAnthropic), } anthropicReq, err := convertOpenAIToAnthropic(req) if err != nil { return nil, err } body, err := json.Marshal(anthropicReq) if err != nil { return nil, err } url := cfg.ResolveEndpointURL(provider.EndpointChatCompletion) resp, err := provider.ForwardHTTP(ctx, c.http.Client, "POST", url, apiKey, model.ProviderAnthropic, body, nil) if err != nil { return nil, err } if resp.StatusCode >= 400 { return &provider.ChatResponse{StatusCode: resp.StatusCode, Body: resp.Body}, nil } openAIResp, tokensIn, tokensOut := convertAnthropicToOpenAI(resp.Body, req.Model) return &provider.ChatResponse{ StatusCode: 200, Body: openAIResp, TokensIn: tokensIn, TokensOut: tokensOut, }, nil } func (c *Client) ForwardWithConfig(ctx context.Context, apiKey string, cfg provider.ProviderConfig, endpoint provider.EndpointType, body []byte) (*provider.ChatResponse, error) { cfg.Type = model.ProviderAnthropic cfg.BaseURL = provider.NormalizeBaseURL(cfg.BaseURL, model.ProviderAnthropic) if endpoint == provider.EndpointChatCompletion { var payload map[string]any if err := json.Unmarshal(body, &payload); err == nil { modelName, _ := payload["model"].(string) anthropicReq, err := convertOpenAIToAnthropic(&provider.ChatRequest{Model: modelName, Raw: body}) if err == nil { body, _ = json.Marshal(anthropicReq) } } } url := cfg.ResolveEndpointURL(endpoint) resp, err := provider.ForwardHTTP(ctx, c.http.Client, "POST", url, apiKey, model.ProviderAnthropic, body, nil) if err != nil { return nil, err } if endpoint == provider.EndpointChatCompletion && resp.StatusCode < 400 { var payload map[string]any modelName := "" _ = json.Unmarshal(body, &payload) if m, ok := payload["model"].(string); ok { modelName = m } openAIResp, tokensIn, tokensOut := convertAnthropicToOpenAI(resp.Body, modelName) return &provider.ChatResponse{StatusCode: 200, Body: openAIResp, TokensIn: tokensIn, TokensOut: tokensOut}, nil } return &provider.ChatResponse{StatusCode: resp.StatusCode, Body: resp.Body, TokensIn: resp.TokensIn, TokensOut: resp.TokensOut}, nil } func (c *Client) HealthCheck(ctx context.Context, apiKey, baseURL string) error { _, err := c.ListModels(ctx, apiKey, baseURL) return err } func (c *Client) HealthCheckWithConfig(ctx context.Context, apiKey string, cfg provider.ProviderConfig) error { _, err := c.ListModelsWithConfig(ctx, apiKey, cfg) return err } // convertOpenAIToAnthropic and convertAnthropicToOpenAI unchanged below func convertOpenAIToAnthropic(req *provider.ChatRequest) (map[string]any, error) { var payload map[string]any raw := req.Raw if len(raw) == 0 { b, _ := json.Marshal(req) raw = b } if err := json.Unmarshal(raw, &payload); err != nil { return nil, err } model, _ := payload["model"].(string) if model == "" { model = req.Model } msgs, _ := payload["messages"].([]any) var system string var anthropicMsgs []map[string]any for _, m := range msgs { msg, _ := m.(map[string]any) role, _ := msg["role"].(string) if role == "system" { system = extractOpenAIContent(msg["content"]) continue } if role != "assistant" { role = "user" } content := convertOpenAIContentBlocks(msg["content"]) anthropicMsgs = append(anthropicMsgs, map[string]any{"role": role, "content": content}) } out := map[string]any{ "model": model, "max_tokens": 4096, "messages": anthropicMsgs, } if system != "" { out["system"] = system } if maxTok, ok := payload["max_tokens"].(float64); ok { out["max_tokens"] = int(maxTok) } return out, nil } func extractOpenAIContent(content any) string { switch v := content.(type) { case string: return v case []any: var parts []string for _, p := range v { part, ok := p.(map[string]any) if !ok { continue } if text, ok := part["text"].(string); ok { parts = append(parts, text) } } return strings.Join(parts, "\n") default: return "" } } func convertOpenAIContentBlocks(content any) any { switch v := content.(type) { case string: if v == "" { return "" } return v case []any: blocks := make([]map[string]any, 0, len(v)) for _, p := range v { part, ok := p.(map[string]any) if !ok { continue } partType, _ := part["type"].(string) switch partType { case "text": if text, ok := part["text"].(string); ok { blocks = append(blocks, map[string]any{"type": "text", "text": text}) } case "image_url": if imageURL, ok := part["image_url"].(map[string]any); ok { if url, ok := imageURL["url"].(string); ok && url != "" { blocks = append(blocks, map[string]any{ "type": "image", "source": map[string]any{ "type": "url", "url": url, }, }) } } case "image": if source, ok := part["source"].(map[string]any); ok { blocks = append(blocks, map[string]any{"type": "image", "source": source}) } default: if text, ok := part["text"].(string); ok { blocks = append(blocks, map[string]any{"type": "text", "text": text}) } } } if len(blocks) == 1 { if text, ok := blocks[0]["text"].(string); ok && blocks[0]["type"] == "text" { return text } } if len(blocks) > 0 { return blocks } return "" default: return "" } } func convertAnthropicToOpenAI(body []byte, model string) ([]byte, int, int) { var resp struct { Content []struct { Text string `json:"text"` } `json:"content"` Usage struct { InputTokens int `json:"input_tokens"` OutputTokens int `json:"output_tokens"` } `json:"usage"` } _ = json.Unmarshal(body, &resp) text := "" if len(resp.Content) > 0 { text = resp.Content[0].Text } openAI := map[string]any{ "id": "chatcmpl-luminary", "object": "chat.completion", "model": model, "choices": []any{map[string]any{"index": 0, "message": map[string]string{"role": "assistant", "content": text}, "finish_reason": "stop"}}, "usage": map[string]int{ "prompt_tokens": resp.Usage.InputTokens, "completion_tokens": resp.Usage.OutputTokens, "total_tokens": resp.Usage.InputTokens + resp.Usage.OutputTokens, }, } b, _ := json.Marshal(openAI) return b, resp.Usage.InputTokens, resp.Usage.OutputTokens }