mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-09-22 16:00:51 +00:00
llmman (https://github.com/llmmanorg/llmman) is a local model runner serving Ollama- and OpenAI-compatible routes on 127.0.0.1:17434. Register it in the agent provider catalog next to Ollama and extend every Ollama special case (no API key, verification skipped, OpenClaw placeholder key, manual initial model) to cover it as well.
182 lines
5.9 KiB
Go
182 lines
5.9 KiB
Go
package provider
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/1Panel-dev/1Panel/agent/buserr"
|
|
)
|
|
|
|
type VerifyRequest struct {
|
|
Method string
|
|
URL string
|
|
Headers map[string]string
|
|
Body []byte
|
|
}
|
|
|
|
type verifyErrorResponse struct {
|
|
Error struct {
|
|
Message string `json:"message"`
|
|
} `json:"error"`
|
|
Message string `json:"message"`
|
|
}
|
|
|
|
const (
|
|
defaultVerifyTimeout = 30 * time.Second
|
|
defaultVerifyMaxTokens = 16
|
|
)
|
|
|
|
func SkipVerification(provider string) bool {
|
|
switch provider {
|
|
case "vllm", "ollama", "llmman", "kimi-coding":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func VerifyAccount(provider, apiType, authMode, baseURL, apiKey, model string) error {
|
|
req := BuildVerifyRequest(provider, apiType, authMode, baseURL, apiKey, model)
|
|
httpReq, err := http.NewRequest(req.Method, req.URL, bytes.NewReader(req.Body))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for key, value := range req.Headers {
|
|
httpReq.Header.Set(key, value)
|
|
}
|
|
httpReq.Header.Set("Accept", "application/json")
|
|
resp, err := (&http.Client{Timeout: defaultVerifyTimeout}).Do(httpReq)
|
|
if err != nil {
|
|
return buserr.WithErr("ErrAgentAccountUnavailable", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
|
|
body, readErr := io.ReadAll(io.LimitReader(resp.Body, 1024*1024))
|
|
if readErr != nil {
|
|
return buserr.WithErr("ErrAgentAccountUnavailable", readErr)
|
|
}
|
|
return buserr.WithErr("ErrAgentAccountUnavailable", errors.New(verifyHTTPError(resp.StatusCode, body)))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func BuildVerifyRequest(provider, apiType, authMode, baseURL, apiKey, model string) VerifyRequest {
|
|
baseURL = strings.TrimSpace(baseURL)
|
|
if provider != "custom" || !IsImageAPIType(apiType) {
|
|
baseURL = strings.TrimRight(baseURL, "/")
|
|
}
|
|
headers := map[string]string{"Content-Type": "application/json"}
|
|
request := VerifyRequest{Method: http.MethodPost, Headers: headers}
|
|
|
|
if provider == "gemini" {
|
|
request.URL = baseURL + "/v1beta/models/" + strings.TrimSpace(model) + ":generateContent"
|
|
headers["x-goog-api-key"] = apiKey
|
|
request.Body = mustJSON(map[string]interface{}{
|
|
"contents": []map[string]interface{}{{"parts": []map[string]string{{"text": "test"}}}},
|
|
})
|
|
return request
|
|
}
|
|
|
|
switch apiType {
|
|
case "openai-embeddings":
|
|
request.URL = embeddingVerifyURL(baseURL)
|
|
headers["Authorization"] = "Bearer " + apiKey
|
|
request.Body = mustJSON(map[string]interface{}{"model": model, "input": "ping"})
|
|
case "openai-images":
|
|
request.URL = imageVerifyURL(provider, baseURL, "/images/generations")
|
|
headers["Authorization"] = "Bearer " + apiKey
|
|
request.Body = mustJSON(map[string]interface{}{"model": model, "prompt": "test", "n": 1, "response_format": "url"})
|
|
case "dashscope-images":
|
|
request.URL = imageVerifyURL(provider, baseURL, "/api/v1/services/aigc/multimodal-generation/generation")
|
|
headers["Authorization"] = "Bearer " + apiKey
|
|
request.Body = mustJSON(map[string]interface{}{
|
|
"model": model,
|
|
"input": map[string]interface{}{"messages": []map[string]interface{}{
|
|
{"role": "user", "content": []map[string]string{{"text": "test"}}},
|
|
}},
|
|
"parameters": map[string]interface{}{"n": 1},
|
|
})
|
|
case "minimax-images":
|
|
request.URL = imageVerifyURL(provider, baseURL, "/v1/image_generation")
|
|
headers["Authorization"] = "Bearer " + apiKey
|
|
request.Body = mustJSON(map[string]interface{}{"model": model, "prompt": "test", "n": 1, "response_format": "url"})
|
|
case "openrouter-images":
|
|
request.URL = imageVerifyURL(provider, baseURL, "/api/v1/images")
|
|
headers["Authorization"] = "Bearer " + apiKey
|
|
request.Body = mustJSON(map[string]interface{}{"model": model, "prompt": "test", "n": 1, "response_format": "url"})
|
|
case "anthropic-messages":
|
|
request.URL = baseURL + "/v1/messages"
|
|
if authMode == AuthModeBearer {
|
|
headers["Authorization"] = "Bearer " + apiKey
|
|
} else {
|
|
headers["x-api-key"] = apiKey
|
|
}
|
|
headers["anthropic-version"] = "2023-06-01"
|
|
request.Body = mustJSON(map[string]interface{}{
|
|
"model": model, "max_tokens": defaultVerifyMaxTokens, "stream": false,
|
|
"messages": []map[string]interface{}{{"role": "user", "content": []map[string]string{{"type": "text", "text": "test"}}}},
|
|
})
|
|
case "openai-responses":
|
|
request.URL = baseURL + "/responses"
|
|
headers["Authorization"] = "Bearer " + apiKey
|
|
request.Body = mustJSON(map[string]interface{}{"model": model, "input": "test", "max_output_tokens": defaultVerifyMaxTokens, "stream": false})
|
|
default:
|
|
request.URL = baseURL + "/chat/completions"
|
|
if (provider != "ollama" && provider != "llmman") || strings.TrimSpace(apiKey) != "" {
|
|
headers["Authorization"] = "Bearer " + apiKey
|
|
}
|
|
request.Body = mustJSON(map[string]interface{}{
|
|
"model": model, "messages": []map[string]string{{"role": "user", "content": "test"}}, "max_tokens": defaultVerifyMaxTokens, "stream": false,
|
|
})
|
|
}
|
|
return request
|
|
}
|
|
|
|
func embeddingVerifyURL(baseURL string) string {
|
|
lowerBaseURL := strings.ToLower(baseURL)
|
|
if strings.HasSuffix(lowerBaseURL, "/embeddings") {
|
|
return baseURL
|
|
}
|
|
if strings.HasSuffix(lowerBaseURL, "/v1") {
|
|
return baseURL + "/embeddings"
|
|
}
|
|
return baseURL + "/v1/embeddings"
|
|
}
|
|
|
|
func imageVerifyURL(provider, baseURL, endpoint string) string {
|
|
if provider == "custom" || strings.HasSuffix(strings.ToLower(baseURL), endpoint) {
|
|
return baseURL
|
|
}
|
|
return baseURL + endpoint
|
|
}
|
|
|
|
func verifyHTTPError(statusCode int, body []byte) string {
|
|
message := strings.TrimSpace(string(body))
|
|
var payload verifyErrorResponse
|
|
if err := json.Unmarshal(body, &payload); err == nil {
|
|
if value := strings.TrimSpace(payload.Error.Message); value != "" {
|
|
message = value
|
|
} else if value := strings.TrimSpace(payload.Message); value != "" {
|
|
message = value
|
|
}
|
|
}
|
|
if message == "" {
|
|
return fmt.Sprintf("validation request returned status %d", statusCode)
|
|
}
|
|
return message
|
|
}
|
|
|
|
func mustJSON(value interface{}) []byte {
|
|
payload, err := json.Marshal(value)
|
|
if err != nil {
|
|
return []byte("{}")
|
|
}
|
|
return payload
|
|
}
|