Add upstream model list fetching and a Codex-specific model section

This commit is contained in:
kate522 committed 2026-10-18 19:33:32 +08:00
1 parent d759fe5e5d
commit f3bf1472e7
12 files changed
+339 -16

No files matched your search

+27
View File
@@ -42,6 +42,33 @@ func (c *Client) endpoint() string {
return c.BaseURL + "/chat/completions"
}
// ListModels fetches the upstream model catalog. Not every provider
// implements it; callers should treat an error as "list unavailable"
// and fall back to manual entry.
func (c *Client) ListModels(ctx context.Context) (*ModelList, error) {
httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, c.BaseURL+"/models", nil)
if err != nil {
return nil, err
}
httpReq.Header.Set("Accept", "application/json")
if c.APIKey != "" {
httpReq.Header.Set("Authorization", "Bearer "+c.APIKey)
}
resp, err := c.HTTP.Do(httpReq)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, &UpstreamError{StatusCode: resp.StatusCode, Message: readErrorBody(resp)}
}
var out ModelList
if err := json.NewDecoder(io.LimitReader(resp.Body, 8<<20)).Decode(&out); err != nil {
return nil, fmt.Errorf("decode upstream model list: %w", err)
}
return &out, nil
}
func (c *Client) do(ctx context.Context, req *Request) (*http.Response, error) {
body, err := json.Marshal(req)
if err != nil {
+43
View File
@@ -73,6 +73,49 @@ func TestCreateStreamRejectsNon2xx(t *testing.T) {
}
}
func TestListModels(t *testing.T) {
var gotPath, gotAuth string
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotPath = r.URL.Path
gotAuth = r.Header.Get("Authorization")
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"object":"list","data":[{"id":"deepseek-chat","object":"model","owned_by":"deepseek"},{"id":"deepseek-reasoner"}]}`))
}))
defer upstream.Close()
client := NewClient(upstream.URL, "sk-up")
list, err := client.ListModels(context.Background())
if err != nil {
t.Fatalf("ListModels: %v", err)
}
if gotPath != "/models" {
t.Errorf("path = %q, want /models", gotPath)
}
if gotAuth != "Bearer sk-up" {
t.Errorf("authorization = %q", gotAuth)
}
if len(list.Data) != 2 || list.Data[0].ID != "deepseek-chat" {
t.Fatalf("models = %+v", list.Data)
}
}
func TestListModelsUpstreamError(t *testing.T) {
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusUnauthorized)
_, _ = w.Write([]byte(`{"error":{"message":"bad key"}}`))
}))
defer upstream.Close()
_, err := NewClient(upstream.URL, "sk-bad").ListModels(context.Background())
var ue *UpstreamError
if !errors.As(err, &ue) {
t.Fatalf("error = %v, want *UpstreamError", err)
}
if ue.StatusCode != http.StatusUnauthorized || ue.Message != "bad key" {
t.Errorf("upstream error = %+v", ue)
}
}
func TestStreamNext(t *testing.T) {
sse := strings.Join([]string{
`data: {"id":"1","choices":[{"index":0,"delta":{"content":"Hel"}}]}`,
+12
View File
@@ -102,6 +102,18 @@ type Error struct {
Code any `json:"code,omitempty"`
}
// ModelInfo is one entry of an upstream model catalog.
type ModelInfo struct {
ID string `json:"id"`
Object string `json:"object,omitempty"`
OwnedBy string `json:"owned_by,omitempty"`
}
// ModelList is the upstream /models response body.
type ModelList struct {
Data []ModelInfo `json:"data"`
}
// StreamChunk is one SSE data payload of a streaming response.
type StreamChunk struct {
ID string `json:"id"`