返回 DeepSeek-Reasonix
fetch_models.go
根目录 / internal / provider / openai / fetch_models.go
1 package openai
2
3 import (
4 "context"
5 "encoding/json"
6 "errors"
7 "fmt"
8 "io"
9 "net/http"
10 "sort"
11 "strings"
12 "time"
13 )
14
15 type modelFetchStatusError struct {
16 status int
17 body string
18 }
19
20 type ModelFetchAuthMode string
21
22 const (
23 ModelFetchAuthAuto ModelFetchAuthMode = ""
24 ModelFetchAuthBearer ModelFetchAuthMode = "bearer"
25 ModelFetchAuthXAPIKey ModelFetchAuthMode = "x-api-key"
26
27 // fetchModelsMaxBody caps the response body read from a model-list
28 // endpoint. Large providers like OpenRouter return ~530 KB for 338
29 // models; 2 MiB leaves headroom while keeping memory bounded.
30 fetchModelsMaxBody = 2 << 20 // 2 MiB
31 )
32
33 type FetchModelsOptions struct {
34 Headers map[string]string
35 AuthMode ModelFetchAuthMode
36 }
37
38 func (e modelFetchStatusError) Error() string {
39 return fmt.Sprintf("fetch models: status %d: %s", e.status, strings.TrimSpace(e.body))
40 }
41
42 // IsModelFetchEndpointMiss reports whether a model-list request reached a
43 // plausible endpoint path that the provider does not implement.
44 func IsModelFetchEndpointMiss(err error) bool {
45 var statusErr modelFetchStatusError
46 if !errors.As(err, &statusErr) {
47 return false
48 }
49 return statusErr.status == http.StatusNotFound || statusErr.status == http.StatusMethodNotAllowed
50 }
51
52 // FetchModels calls the OpenAI-compatible GET /models endpoint and returns the
53 // available model IDs.
54 func FetchModels(ctx context.Context, baseURL, apiKey string, headers map[string]string) ([]string, error) {
55 return FetchModelsWithOptions(ctx, baseURL, apiKey, FetchModelsOptions{Headers: headers})
56 }
57
58 // FetchModelsWithOptions calls the OpenAI-compatible GET /models endpoint and
59 // returns the available model IDs.
60 func FetchModelsWithOptions(ctx context.Context, baseURL, apiKey string, opts FetchModelsOptions) ([]string, error) {
61 cli := &http.Client{Timeout: 10 * time.Second}
62 url := strings.TrimRight(baseURL, "/")
63 if !strings.HasSuffix(url, "/models") {
64 url += "/models"
65 }
66
67 req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
68 if err != nil {
69 return nil, fmt.Errorf("fetch models: build request: %w", err)
70 }
71 applyModelFetchAPIKeyHeader(req.Header, baseURL, apiKey, opts.AuthMode)
72 req.Header.Set("Accept", "application/json")
73 applyCustomHeaders(req.Header, opts.Headers)
74
75 resp, err := cli.Do(req)
76 if err != nil {
77 return nil, fmt.Errorf("fetch models: request failed: %w", err)
78 }
79 defer resp.Body.Close()
80
81 body, err := io.ReadAll(io.LimitReader(resp.Body, fetchModelsMaxBody+1))
82 if err != nil {
83 return nil, fmt.Errorf("fetch models: read response: %w", err)
84 }
85 if len(body) > fetchModelsMaxBody {
86 return nil, fmt.Errorf("fetch models: response too large (exceeds %d bytes)", fetchModelsMaxBody)
87 }
88
89 if resp.StatusCode != http.StatusOK {
90 return nil, modelFetchStatusError{status: resp.StatusCode, body: truncateFetchBody(string(body))}
91 }
92
93 var result struct {
94 Data []struct {
95 ID string `json:"id"`
96 } `json:"data"`
97 }
98 if err := json.Unmarshal(body, &result); err != nil {
99 return nil, fmt.Errorf("fetch models: decode response: %w", err)
100 }
101
102 ids := make([]string, 0, len(result.Data))
103 for _, m := range result.Data {
104 if id := normalizeModelID(baseURL, m.ID); id != "" {
105 ids = append(ids, id)
106 }
107 }
108 sort.Strings(ids)
109 return ids, nil
110 }
111
112 func applyModelFetchAPIKeyHeader(h http.Header, baseURL, apiKey string, mode ModelFetchAuthMode) {
113 apiKey = strings.TrimSpace(apiKey)
114 if apiKey == "" {
115 return
116 }
117 switch mode {
118 case ModelFetchAuthBearer:
119 h.Set("Authorization", "Bearer "+apiKey)
120 case ModelFetchAuthXAPIKey:
121 h.Set("x-api-key", apiKey)
122 default:
123 applyAPIKeyHeader(h, baseURL, apiKey)
124 }
125 }
126
127 func truncateFetchBody(body string) string {
128 body = strings.TrimSpace(body)
129 const max = 512
130 if len([]rune(body)) <= max {
131 return body
132 }
133 r := []rune(body)
134 return string(r[:max]) + "..."
135 }
136
136 lines GO