mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-07-01 05:06:03 +00:00
195 lines
5.8 KiB
Go
195 lines
5.8 KiB
Go
package http
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/nextlevelbuilder/goclaw/internal/config"
|
|
)
|
|
|
|
// fetchAnthropicModels calls the Anthropic models API.
|
|
func fetchAnthropicModels(ctx context.Context, apiKey, apiBase string) ([]ModelInfo, error) {
|
|
base := strings.TrimRight(apiBase, "/")
|
|
if base == "" {
|
|
base = "https://api.anthropic.com/v1"
|
|
}
|
|
req, err := http.NewRequestWithContext(ctx, "GET", base+"/models", nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Set("x-api-key", apiKey)
|
|
req.Header.Set("anthropic-version", "2023-06-01")
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
body, _ := io.ReadAll(io.LimitReader(resp.Body, 1024))
|
|
return nil, fmt.Errorf("anthropic API returned %d: %s", resp.StatusCode, string(body))
|
|
}
|
|
|
|
var result struct {
|
|
Data []struct {
|
|
ID string `json:"id"`
|
|
DisplayName string `json:"display_name"`
|
|
} `json:"data"`
|
|
}
|
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
|
return nil, fmt.Errorf("failed to decode anthropic response: %w", err)
|
|
}
|
|
|
|
models := make([]ModelInfo, 0, len(result.Data))
|
|
for _, m := range result.Data {
|
|
models = append(models, ModelInfo{ID: m.ID, Name: m.DisplayName})
|
|
}
|
|
return models, nil
|
|
}
|
|
|
|
// fetchGeminiModels calls the Google Gemini models API.
|
|
// Gemini uses a different format: GET /v1beta/models?key=API_KEY
|
|
func fetchGeminiModels(ctx context.Context, apiKey string) ([]ModelInfo, error) {
|
|
req, err := http.NewRequestWithContext(ctx, "GET", "https://generativelanguage.googleapis.com/v1beta/models?key="+apiKey, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
body, _ := io.ReadAll(io.LimitReader(resp.Body, 1024))
|
|
return nil, fmt.Errorf("gemini API returned %d: %s", resp.StatusCode, string(body))
|
|
}
|
|
|
|
var result struct {
|
|
Models []struct {
|
|
Name string `json:"name"` // e.g. "models/gemini-2.0-flash"
|
|
DisplayName string `json:"displayName"` // e.g. "Gemini 2.0 Flash"
|
|
} `json:"models"`
|
|
}
|
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
|
return nil, fmt.Errorf("failed to decode gemini response: %w", err)
|
|
}
|
|
|
|
models := make([]ModelInfo, 0, len(result.Models))
|
|
for _, m := range result.Models {
|
|
// Strip "models/" prefix to get the usable model ID
|
|
id := strings.TrimPrefix(m.Name, "models/")
|
|
models = append(models, ModelInfo{ID: id, Name: m.DisplayName})
|
|
}
|
|
return models, nil
|
|
}
|
|
|
|
// fetchOpenAIModels calls an OpenAI-compatible /models endpoint.
|
|
func fetchOpenAIModels(ctx context.Context, apiBase, apiKey string, extraHeaders map[string]string) ([]ModelInfo, error) {
|
|
req, err := http.NewRequestWithContext(ctx, "GET", apiBase+"/models", nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Set("Authorization", "Bearer "+apiKey)
|
|
for k, v := range extraHeaders {
|
|
req.Header.Set(k, v)
|
|
}
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
body, _ := io.ReadAll(io.LimitReader(resp.Body, 1024))
|
|
return nil, fmt.Errorf("provider API returned %d: %s", resp.StatusCode, string(body))
|
|
}
|
|
|
|
var result struct {
|
|
Data []struct {
|
|
ID string `json:"id"`
|
|
OwnedBy string `json:"owned_by"`
|
|
} `json:"data"`
|
|
}
|
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
|
return nil, fmt.Errorf("failed to decode provider response: %w", err)
|
|
}
|
|
|
|
models := make([]ModelInfo, 0, len(result.Data))
|
|
for _, m := range result.Data {
|
|
models = append(models, ModelInfo{ID: m.ID, Name: m.ID})
|
|
}
|
|
return models, nil
|
|
}
|
|
|
|
// fetchOllamaModels calls Ollama's native /api/tags endpoint to get model metadata
|
|
// including parameter size, quantization level, and model family.
|
|
// The api_base may include a /v1 suffix (from issue #654 normalization) — strip it
|
|
// before appending /api/tags since /api/tags lives at the root, not under /v1.
|
|
func (h *ProvidersHandler) fetchOllamaModels(ctx context.Context, apiBase, apiKey string) ([]ModelInfo, error) {
|
|
// Strip /v1 suffix if present, then trim trailing slash.
|
|
base := strings.TrimRight(strings.TrimSuffix(strings.TrimRight(apiBase, "/"), "/v1"), "/")
|
|
url := config.DockerLocalhost(base + "/api/tags")
|
|
|
|
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if apiKey != "" {
|
|
req.Header.Set("Authorization", "Bearer "+apiKey)
|
|
}
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
body, _ := io.ReadAll(io.LimitReader(resp.Body, 1024))
|
|
return nil, fmt.Errorf("ollama /api/tags returned %d: %s", resp.StatusCode, string(body))
|
|
}
|
|
|
|
var result struct {
|
|
Models []struct {
|
|
Name string `json:"name"`
|
|
Details struct {
|
|
Family string `json:"family"`
|
|
ParameterSize string `json:"parameter_size"`
|
|
QuantizationLevel string `json:"quantization_level"`
|
|
} `json:"details"`
|
|
} `json:"models"`
|
|
}
|
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
|
return nil, fmt.Errorf("failed to decode ollama response: %w", err)
|
|
}
|
|
|
|
models := make([]ModelInfo, 0, len(result.Models))
|
|
for _, m := range result.Models {
|
|
// Build a human-readable display name: "family paramSize quantLevel" e.g. "gemma4 8.0B Q4_K_M"
|
|
var parts []string
|
|
if m.Details.Family != "" {
|
|
parts = append(parts, m.Details.Family)
|
|
}
|
|
if m.Details.ParameterSize != "" {
|
|
parts = append(parts, m.Details.ParameterSize)
|
|
}
|
|
if m.Details.QuantizationLevel != "" {
|
|
parts = append(parts, m.Details.QuantizationLevel)
|
|
}
|
|
name := m.Name
|
|
if len(parts) > 0 {
|
|
name = strings.Join(parts, " ")
|
|
}
|
|
models = append(models, ModelInfo{ID: m.Name, Name: name})
|
|
}
|
|
return models, nil
|
|
}
|