package openai import ( "context" "net/http" "net/http/httptest" "testing" ) // TestModels_LiveDiscovery verifies the ADR-0018 tier-2 path: with // Config.LiveDiscovery=true, Models() GETs {BaseURL}/v1/models (Bearer auth) and // maps the OpenAI-compatible {data:[{id,owned_by}]} body to []flyto.ModelInfo, // stamping an empty owned_by with the "openai" provider id (mirrors lmstudio). // This is the net-new branch that makes fmlx / oMLX self-hosted discovery work; // the real fmlx endpoint (m5max) is unreachable, so an httptest server is the // deterministic proof of the happy path (not just the unreachable fail-loud). // // TestModels_LiveDiscovery 验证 ADR-0018 第二档: LiveDiscovery=true 时 Models() 打 // {BaseURL}/v1/models (Bearer 鉴权) 并把 OpenAI 兼容 body 映成 []flyto.ModelInfo, // owned_by 空时补 "openai" (镜像 lmstudio). 这是让 fmlx 自托管发现生效的 net-new 分支; // 真 fmlx 端点不可达, httptest 是 happy path 的确定性证明 (而非只证不可达 fail-loud). func TestModels_LiveDiscovery(t *testing.T) { var gotPath, gotAuth string srv := 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(`{"data":[{"id":"gemma-3-27b-it","owned_by":"","model_type":"vlm"},{"id":"qwen2.5-coder","owned_by":"mlx-community"},{"id":"qwen3-asr-1.7b","owned_by":"omlx","model_type":"audio_stt"}]}`)) })) defer srv.Close() p := New(Config{APIKey: "sk-test", BaseURL: srv.URL, LiveDiscovery: true}) models, err := p.Models(context.Background()) if err != nil { t.Fatalf("Models(LiveDiscovery): %v", err) } if gotPath != "/v1/models" { t.Errorf("hit path = %q, want /v1/models", gotPath) } if gotAuth != "Bearer sk-test" { t.Errorf("auth header = %q, want Bearer sk-test", gotAuth) } if len(models) != 3 { t.Fatalf("got %d models, want 3: %+v", len(models), models) } // owned_by empty -> stamped "openai"; owned_by present -> preserved. if models[0].ID != "gemma-3-27b-it" || models[0].Provider != "openai" { t.Errorf("model[0] = %+v, want id=gemma-3-27b-it provider=openai (empty owned_by stamped)", models[0]) } if models[1].ID != "qwen2.5-coder" || models[1].Provider != "mlx-community" { t.Errorf("model[1] = %+v, want id=qwen2.5-coder provider=mlx-community (owned_by preserved)", models[1]) } // model_type maps onto the shared capability flags (the model spec): // vlm -> SupportsVision, audio_stt -> SupportsTranscription, absent -> neither. // model_type 映射到共享能力位 (模型 spec): vlm -> SupportsVision, // audio_stt -> SupportsTranscription, 缺失 -> 都不. if !models[0].SupportsVision || models[0].SupportsTranscription { t.Errorf("model[0] caps = %+v, want vision-only", models[0]) } if models[1].SupportsVision || models[1].SupportsTranscription { t.Errorf("model[1] caps = %+v, want none (no model_type)", models[1]) } if !models[2].SupportsTranscription || models[2].SupportsVision { t.Errorf("model[2] caps = %+v, want transcription-only (audio_stt)", models[2]) } } // TestModels_LiveDiscovery_ErrorWraps verifies a non-2xx /v1/models surfaces as // a wrapped error (fail-loud), not an empty success -- so an unreachable or // broken self-hosted endpoint never looks like "zero models". // // TestModels_LiveDiscovery_ErrorWraps 验证 /v1/models 非 2xx 以包裹 error 冒出 // (fail-loud), 不是空成功 -- 不可达/坏的自托管端点绝不被当成 "零模型". func TestModels_LiveDiscovery_ErrorWraps(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Not valid JSON -> decode error -> wrapped error path. http.Error(w, "boom", http.StatusInternalServerError) })) defer srv.Close() p := New(Config{APIKey: "k", BaseURL: srv.URL, LiveDiscovery: true}) if _, err := p.Models(context.Background()); err == nil { t.Fatal("Models(LiveDiscovery) against a broken endpoint returned nil error; want fail-loud") } } // TestModels_StaticDefault verifies LiveDiscovery=false (the default) keeps the // built-in static catalog and never touches the network -- real OpenAI must not // regress to a live /v1/models pull that returns embedding/audio junk. // // TestModels_StaticDefault 验证 LiveDiscovery=false (默认) 保留内置静态目录, 绝不打 // 网络 -- 真 OpenAI 不能回归成 live /v1/models (会返 embedding/audio 垃圾). func TestModels_StaticDefault(t *testing.T) { hit := false srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { hit = true _, _ = w.Write([]byte(`{"data":[]}`)) })) defer srv.Close() p := New(Config{APIKey: "sk-test", BaseURL: srv.URL}) // LiveDiscovery defaults false models, err := p.Models(context.Background()) if err != nil { t.Fatalf("Models(static): %v", err) } if hit { t.Error("static Models() hit the network; must use the built-in catalog") } if len(models) == 0 { t.Error("static Models() returned empty; want the built-in openaiModels catalog") } }