Files
2026-07-13 13:00:08 +08:00

128 lines
3.3 KiB
Go

package openai
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"sort"
"strings"
"time"
)
type modelFetchStatusError struct {
status int
body string
}
type ModelFetchAuthMode string
const (
ModelFetchAuthAuto ModelFetchAuthMode = ""
ModelFetchAuthBearer ModelFetchAuthMode = "bearer"
ModelFetchAuthXAPIKey ModelFetchAuthMode = "x-api-key"
)
type FetchModelsOptions struct {
Headers map[string]string
AuthMode ModelFetchAuthMode
}
func (e modelFetchStatusError) Error() string {
return fmt.Sprintf("fetch models: status %d: %s", e.status, strings.TrimSpace(e.body))
}
// IsModelFetchEndpointMiss reports whether a model-list request reached a
// plausible endpoint path that the provider does not implement.
func IsModelFetchEndpointMiss(err error) bool {
var statusErr modelFetchStatusError
if !errors.As(err, &statusErr) {
return false
}
return statusErr.status == http.StatusNotFound || statusErr.status == http.StatusMethodNotAllowed
}
// FetchModels calls the OpenAI-compatible GET /models endpoint and returns the
// available model IDs.
func FetchModels(ctx context.Context, baseURL, apiKey string, headers map[string]string) ([]string, error) {
return FetchModelsWithOptions(ctx, baseURL, apiKey, FetchModelsOptions{Headers: headers})
}
// FetchModelsWithOptions calls the OpenAI-compatible GET /models endpoint and
// returns the available model IDs.
func FetchModelsWithOptions(ctx context.Context, baseURL, apiKey string, opts FetchModelsOptions) ([]string, error) {
cli := &http.Client{Timeout: 10 * time.Second}
url := strings.TrimRight(baseURL, "/")
if !strings.HasSuffix(url, "/models") {
url += "/models"
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, fmt.Errorf("fetch models: build request: %w", err)
}
applyModelFetchAPIKeyHeader(req.Header, baseURL, apiKey, opts.AuthMode)
req.Header.Set("Accept", "application/json")
applyCustomHeaders(req.Header, opts.Headers)
resp, err := cli.Do(req)
if err != nil {
return nil, fmt.Errorf("fetch models: request failed: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(io.LimitReader(resp.Body, 256*1024))
if err != nil {
return nil, fmt.Errorf("fetch models: read response: %w", err)
}
if resp.StatusCode != http.StatusOK {
return nil, modelFetchStatusError{status: resp.StatusCode, body: truncateFetchBody(string(body))}
}
var result struct {
Data []struct {
ID string `json:"id"`
} `json:"data"`
}
if err := json.Unmarshal(body, &result); err != nil {
return nil, fmt.Errorf("fetch models: decode response: %w", err)
}
ids := make([]string, 0, len(result.Data))
for _, m := range result.Data {
if m.ID != "" {
ids = append(ids, m.ID)
}
}
sort.Strings(ids)
return ids, nil
}
func applyModelFetchAPIKeyHeader(h http.Header, baseURL, apiKey string, mode ModelFetchAuthMode) {
apiKey = strings.TrimSpace(apiKey)
if apiKey == "" {
return
}
switch mode {
case ModelFetchAuthBearer:
h.Set("Authorization", "Bearer "+apiKey)
case ModelFetchAuthXAPIKey:
h.Set("x-api-key", apiKey)
default:
applyAPIKeyHeader(h, baseURL, apiKey)
}
}
func truncateFetchBody(body string) string {
body = strings.TrimSpace(body)
const max = 512
if len([]rune(body)) <= max {
return body
}
r := []rune(body)
return string(r[:max]) + "..."
}