feat: support disabling providers

This commit is contained in:
apale7 2026-04-17 14:01:28 +08:00
parent f8c0384e93
commit d585bf35c1
11 changed files with 445 additions and 69 deletions

View File

@ -101,7 +101,7 @@ olpx alias list
注意:
- target 的顺序就是失败切换顺序
- enabled alias 必须至少有一个 enabled target
- enabled alias 必须至少有一个可路由 target
- `olpx alias bind` 在 alias 不存在时会自动创建一个 enabled alias
### 3. 先做一次静态检查
@ -116,7 +116,7 @@ olpx doctor
- 本地 `olpx` 配置能不能正常加载
- alias 是否引用了不存在的 provider
- enabled alias 是否至少有一个 enabled target
- enabled alias 是否至少有一个可路由 target
- 本地代理监听地址是否合理
- 默认会同步到哪个 OpenCode 配置文件
@ -126,7 +126,7 @@ olpx doctor
olpx opencode sync
```
这个命令会做一件事:把当前 alias 列表同步进 OpenCode 的 `provider.olpx.models`
这个命令会做一件事:把当前可路由的 alias 列表同步进 OpenCode 的 `provider.olpx.models`
默认行为:
@ -262,6 +262,18 @@ olpx provider add --id <id> --base-url <url-with-/v1> --api-key <key>
olpx provider list
```
禁用 provider
```bash
olpx provider disable <id>
```
重新启用 provider
```bash
olpx provider enable <id>
```
删除 provider
```bash
@ -365,7 +377,8 @@ olpx --config /path/to/config.json doctor
"id": "codex",
"name": "Codex",
"base_url": "https://api-vip.codex-for.me/v1",
"api_key": "sk-yyy"
"api_key": "sk-yyy",
"disabled": true
}
],
"aliases": [
@ -406,7 +419,7 @@ olpx --config /path/to/config.json doctor
- alias 不存在
- alias 被禁用
- alias 没有 enabled target
- alias 没有可用 target
- 上游返回 `400`
- 上游返回 `401`
- 上游返回 `403`
@ -445,19 +458,32 @@ olpx --config /path/to/config.json doctor
1. 你是否执行过 `olpx opencode sync`
2. 你的 alias 是否是 enabled 状态
3. alias 是否至少绑定了一个 enabled target
4. OpenCode 当前实际使用的配置文件,是否就是 `olpx opencode sync` 写入的那个文件
5. 执行一次 `olpx doctor`,看输出里的 `opencode config target`
3. alias 是否至少绑定了一个可路由 target
4. alias 绑定的 provider 是否都被禁用了
5. OpenCode 当前实际使用的配置文件,是否就是 `olpx opencode sync` 写入的那个文件
6. 执行一次 `olpx doctor`,看输出里的 `opencode config target`
### 为什么 `olpx doctor` 报 alias 没有 enabled target
### 为什么 `olpx doctor` 报 alias 没有可用 target
因为当前实现要求:只要 alias 是 enabled就必须至少有一个 enabled target。
因为当前实现要求:只要 alias 是 enabled就必须至少有一个可路由的 target。
可路由的意思是同时满足:
- target 自己是 enabled
- target 引用的 provider 存在且没有被禁用
你可以:
- 给它绑定 target
- 重新启用被禁用的 provider
- 或者把这个 alias 改成 disabled 后再保存
### 为什么要禁用 provider而不是把 alias target 也改成 disabled
因为 provider 可能被多个 alias 复用。
`olpx provider disable` 只会让路由层在 failover 时自动跳过这个 provider不会改写 alias 里的 target 状态,这样重新启用 provider 时不会和 alias 上原有的启用关系打架。
### 删除 provider 后为什么还有报错?
因为 alias 里的 target 还是旧引用。需要继续执行:

View File

@ -7,6 +7,7 @@ stable model alias** routed to **multiple upstream providers** with
- Expose one custom provider `olpx` to OpenCode.
- Configure logical aliases (`olpx/gpt-5.4`, etc.).
- Each alias has an ordered list of upstream `provider/model` targets.
- Providers can be disabled without mutating alias target state.
- When the primary upstream returns `5xx`/`429`/connect error *before* any
stream bytes are flushed, `olpx` transparently retries the next target.
- Once a stream has started, the upstream is locked for the rest of that
@ -35,6 +36,9 @@ olpx alias bind --alias gpt-5.4 --provider codex --model GPT-5.4
# 3. push alias exposure into OpenCode global config
olpx opencode sync
# optional: temporarily disable one provider without editing alias targets
olpx provider disable su8
# 4. run the proxy
olpx serve
```
@ -63,11 +67,33 @@ olpx doctor
Runs structural checks only — never issues real upstream requests.
Enabled aliases must have at least one **routable** target. A target is
considered routable only when:
- the alias itself is enabled
- the target itself is enabled
- the referenced provider exists
- the referenced provider is not disabled
`olpx opencode sync` and `/v1/models` use the same routable-alias view, so
OpenCode does not see aliases that the proxy would immediately reject.
### Provider state
```bash
olpx provider disable <id>
olpx provider enable <id>
```
Disabling a provider only removes it from routing/failover consideration. It
does **not** rewrite alias target `enabled` flags in config, which avoids odd
interactions when the same provider is shared across multiple aliases.
## CLI reference
- `olpx serve` — run the proxy
- `olpx doctor` — validate config
- `olpx provider {add,list,remove,import-opencode}`
- `olpx provider {add,list,enable,disable,remove,import-opencode}`
- `olpx alias {add,list,bind,unbind,remove}`
- `olpx opencode sync [--target FILE] [--set-model ALIAS] [--set-small-model ALIAS] [--dry-run]`
@ -86,6 +112,7 @@ Every proxied response includes:
## Scope
Out of MVP: Anthropic native, multi-protocol routing, dashboard, billing,
latency-based routing, `/v1/models` discovery, full OpenCode config takeover.
latency-based routing, full `/v1/models` provider discovery, full OpenCode
config takeover.
See `.trellis/tasks/archive/2026-04/04-17-04-17-ops-mvp-design-review/prd.md`
for the authoritative design notes.

View File

@ -85,7 +85,15 @@ func newAliasListCmd() *cobra.Command {
if !t.Enabled {
mark = " "
}
fmt.Fprintf(cmd.OutOrStdout(), " [%s] %d. %s/%s\n", mark, i+1, t.Provider, t.Model)
note := ""
provider := cfg.FindProvider(t.Provider)
switch {
case provider == nil:
note = " (missing provider)"
case !provider.IsEnabled():
note = " (provider disabled)"
}
fmt.Fprintf(cmd.OutOrStdout(), " [%s] %d. %s/%s%s\n", mark, i+1, t.Provider, t.Model, note)
}
}
return nil

View File

@ -22,6 +22,7 @@ func TestProviderAddPreservesExistingFields(t *testing.T) {
BaseURL: "https://old.example.com/v1",
APIKey: "sk-old",
Headers: map[string]string{"X-Test": "1"},
Disabled: true,
})
if err := cfg.Save(); err != nil {
t.Fatalf("save config: %v", err)
@ -53,6 +54,9 @@ func TestProviderAddPreservesExistingFields(t *testing.T) {
if p.BaseURL != "https://new.example.com/v1" {
t.Fatalf("BaseURL = %q, want updated URL", p.BaseURL)
}
if !p.Disabled {
t.Fatal("Disabled = false, want true to preserve provider state")
}
}
func TestProviderAddRejectsInvalidBaseURL(t *testing.T) {
@ -116,3 +120,48 @@ func TestAliasAddPreservesExistingFields(t *testing.T) {
t.Fatalf("Targets = %#v, want preserved target", a.Targets)
}
}
func TestProviderEnableDisableCommands(t *testing.T) {
t.Setenv("OLPX_CONFIG", filepath.Join(t.TempDir(), "olpx.json"))
configPath = ""
cfg, err := loadCfg()
if err != nil {
t.Fatalf("loadCfg: %v", err)
}
cfg.UpsertProvider(config.Provider{
ID: "p1",
BaseURL: "https://example.com/v1",
})
if err := cfg.Save(); err != nil {
t.Fatalf("save config: %v", err)
}
disableCmd := newProviderDisableCmd()
disableCmd.SetArgs([]string{"p1"})
if err := disableCmd.Execute(); err != nil {
t.Fatalf("execute provider disable: %v", err)
}
cfg, err = loadCfg()
if err != nil {
t.Fatalf("reload after disable: %v", err)
}
if provider := cfg.FindProvider("p1"); provider == nil || !provider.Disabled {
t.Fatalf("provider after disable = %#v, want disabled=true", provider)
}
enableCmd := newProviderEnableCmd()
enableCmd.SetArgs([]string{"p1"})
if err := enableCmd.Execute(); err != nil {
t.Fatalf("execute provider enable: %v", err)
}
cfg, err = loadCfg()
if err != nil {
t.Fatalf("reload after enable: %v", err)
}
if provider := cfg.FindProvider("p1"); provider == nil || provider.Disabled {
t.Fatalf("provider after enable = %#v, want disabled=false", provider)
}
}

View File

@ -24,13 +24,7 @@ func newDoctorCmd() *cobra.Command {
if err != nil {
issues = append(issues, fmt.Errorf("load opencode config target: %w", err))
} else {
aliasNames := []string{}
for _, a := range cfg.Aliases {
if !a.Enabled {
continue
}
aliasNames = append(aliasNames, a.Alias)
}
aliasNames := cfg.AvailableAliasNames()
baseURL := fmt.Sprintf("http://%s:%d/v1", cfg.Server.Host, cfg.Server.Port)
opencode.EnsureOLPXProvider(raw, baseURL, cfg.Server.APIKey, aliasNames)
if err := opencode.ValidateOLPXProvider(raw, baseURL, cfg.Server.APIKey, aliasNames); err != nil {

View File

@ -51,13 +51,7 @@ or creating opencode.jsonc if none exists. It does NOT touch the top-level
if err != nil {
return err
}
aliasNames := []string{}
for _, a := range cfg.Aliases {
if !a.Enabled {
continue
}
aliasNames = append(aliasNames, a.Alias)
}
aliasNames := cfg.AvailableAliasNames()
baseURL := fmt.Sprintf("http://%s:%d/v1", cfg.Server.Host, cfg.Server.Port)
changed := opencode.EnsureOLPXProvider(raw, baseURL, cfg.Server.APIKey, aliasNames)
if setModel != "" {

View File

@ -16,13 +16,21 @@ func newProviderCmd() *cobra.Command {
Use: "provider",
Short: "Manage upstream providers",
}
c.AddCommand(newProviderAddCmd(), newProviderListCmd(), newProviderRemoveCmd(), newProviderImportCmd())
c.AddCommand(
newProviderAddCmd(),
newProviderListCmd(),
newProviderEnableCmd(),
newProviderDisableCmd(),
newProviderRemoveCmd(),
newProviderImportCmd(),
)
return c
}
func newProviderAddCmd() *cobra.Command {
var id, name, baseURL, apiKey string
var headers []string
var disabled bool
cmd := &cobra.Command{
Use: "add",
Short: "Add or update an upstream provider",
@ -51,6 +59,7 @@ func newProviderAddCmd() *cobra.Command {
BaseURL: baseURL,
APIKey: apiKey,
Headers: hdrs,
Disabled: disabled,
}
if existing := cfg.FindProvider(id); existing != nil {
if p.Name == "" {
@ -62,12 +71,19 @@ func newProviderAddCmd() *cobra.Command {
if len(headers) == 0 && len(existing.Headers) > 0 {
p.Headers = cloneHeaders(existing.Headers)
}
if !disabled {
p.Disabled = existing.Disabled
}
}
cfg.UpsertProvider(p)
if err := cfg.Save(); err != nil {
return err
}
fmt.Fprintf(cmd.OutOrStdout(), "saved provider %q → %s\n", id, baseURL)
state := "enabled"
if p.Disabled {
state = "disabled"
}
fmt.Fprintf(cmd.OutOrStdout(), "saved provider %q [%s] → %s\n", id, state, baseURL)
return nil
},
}
@ -76,6 +92,7 @@ func newProviderAddCmd() *cobra.Command {
cmd.Flags().StringVar(&baseURL, "base-url", "", "OpenAI-compatible base URL, including /v1 (required)")
cmd.Flags().StringVar(&apiKey, "api-key", "", "upstream API key")
cmd.Flags().StringArrayVar(&headers, "header", nil, "extra header KEY=VALUE (repeatable)")
cmd.Flags().BoolVar(&disabled, "disabled", false, "save provider in disabled state")
return cmd
}
@ -110,8 +127,50 @@ func newProviderListCmd() *cobra.Command {
if p.APIKey != "" {
key = maskKey(p.APIKey)
}
fmt.Fprintf(cmd.OutOrStdout(), "%-20s %s apiKey=%s\n", p.ID, p.BaseURL, key)
state := "enabled"
if !p.IsEnabled() {
state = "disabled"
}
fmt.Fprintf(cmd.OutOrStdout(), "%-20s [%s] %s apiKey=%s\n", p.ID, state, p.BaseURL, key)
}
return nil
},
}
}
func newProviderEnableCmd() *cobra.Command {
return newProviderStateCmd("enable", false)
}
func newProviderDisableCmd() *cobra.Command {
return newProviderStateCmd("disable", true)
}
func newProviderStateCmd(use string, disabled bool) *cobra.Command {
action := "enabled"
if disabled {
action = "disabled"
}
return &cobra.Command{
Use: use + " <id>",
Args: cobra.ExactArgs(1),
Short: strings.Title(action[:len(action)-1]) + " a provider without changing alias target state",
RunE: func(cmd *cobra.Command, args []string) error {
cfg, err := loadCfg()
if err != nil {
return err
}
existing := cfg.FindProvider(args[0])
if existing == nil {
return fmt.Errorf("provider %q not found", args[0])
}
updated := *existing
updated.Disabled = disabled
cfg.UpsertProvider(updated)
if err := cfg.Save(); err != nil {
return err
}
fmt.Fprintf(cmd.OutOrStdout(), "%s provider %q\n", action, args[0])
return nil
},
}
@ -179,6 +238,7 @@ func newProviderImportCmd() *cobra.Command {
Name: ip.Name,
BaseURL: ip.BaseURL,
APIKey: ip.APIKey,
Disabled: false,
})
imported++
fmt.Fprintf(cmd.OutOrStdout(), "import %q → %s (models: %s)\n", ip.ID, ip.BaseURL, strings.Join(ip.Models, ","))

View File

@ -34,6 +34,7 @@ type Provider struct {
BaseURL string `json:"base_url"`
APIKey string `json:"api_key"`
Headers map[string]string `json:"headers,omitempty"`
Disabled bool `json:"disabled,omitempty"`
}
// Server holds proxy listen settings.
@ -53,6 +54,11 @@ type Config struct {
mu sync.RWMutex
}
// IsEnabled reports whether the provider can be used for routing.
func (p Provider) IsEnabled() bool {
return !p.Disabled
}
// ValidateProviderBaseURL checks the MVP requirement that upstream base URLs
// point at an OpenAI-compatible /v1 root.
func ValidateProviderBaseURL(baseURL string) error {
@ -166,6 +172,10 @@ func (c *Config) Save() error {
func (c *Config) FindProvider(id string) *Provider {
c.mu.RLock()
defer c.mu.RUnlock()
return c.findProviderLocked(id)
}
func (c *Config) findProviderLocked(id string) *Provider {
for i := range c.Providers {
if c.Providers[i].ID == id {
return &c.Providers[i]
@ -283,6 +293,47 @@ func (c *Config) RemoveTarget(alias, provider, model string) error {
return fmt.Errorf("alias %q not found", alias)
}
// AvailableTargets returns alias targets that are individually enabled and point
// at providers that still exist and are enabled.
func (c *Config) AvailableTargets(alias Alias) []Target {
c.mu.RLock()
defer c.mu.RUnlock()
return c.availableTargetsLocked(alias)
}
func (c *Config) availableTargetsLocked(alias Alias) []Target {
targets := make([]Target, 0, len(alias.Targets))
for _, t := range alias.Targets {
if !t.Enabled {
continue
}
provider := c.findProviderLocked(t.Provider)
if provider == nil || !provider.IsEnabled() {
continue
}
targets = append(targets, t)
}
return targets
}
// AvailableAliasNames returns alias names that are enabled and still have at
// least one routable target after provider availability is applied.
func (c *Config) AvailableAliasNames() []string {
c.mu.RLock()
defer c.mu.RUnlock()
names := make([]string, 0, len(c.Aliases))
for _, a := range c.Aliases {
if !a.Enabled {
continue
}
if len(c.availableTargetsLocked(a)) == 0 {
continue
}
names = append(names, a.Alias)
}
return names
}
// Validate returns a non-nil error slice for every structural issue found.
func (c *Config) Validate() []error {
c.mu.RLock()
@ -325,12 +376,10 @@ func (c *Config) Validate() []error {
if !ids[t.Provider] {
errs = append(errs, fmt.Errorf("alias %q references unknown provider %q", a.Alias, t.Provider))
}
if t.Enabled {
enabled++
}
}
enabled = len(c.availableTargetsLocked(a))
if a.Enabled && enabled == 0 {
errs = append(errs, fmt.Errorf("alias %q has no enabled targets", a.Alias))
errs = append(errs, fmt.Errorf("alias %q has no available targets", a.Alias))
}
}
if c.Server.Port <= 0 || c.Server.Port > 65535 {

View File

@ -1,6 +1,8 @@
package config
import "testing"
import (
"testing"
)
func TestValidateProviderBaseURL(t *testing.T) {
t.Parallel()
@ -36,3 +38,75 @@ func TestValidateProviderBaseURL(t *testing.T) {
})
}
}
func TestAvailableTargetsSkipsDisabledAndMissingProviders(t *testing.T) {
t.Parallel()
cfg := &Config{
Providers: []Provider{
{ID: "p1", BaseURL: "https://p1.example.com/v1"},
{ID: "p2", BaseURL: "https://p2.example.com/v1", Disabled: true},
},
}
alias := Alias{
Alias: "gpt-5.4",
Enabled: true,
Targets: []Target{
{Provider: "p1", Model: "up-1", Enabled: true},
{Provider: "p2", Model: "up-2", Enabled: true},
{Provider: "missing", Model: "up-3", Enabled: true},
{Provider: "p1", Model: "up-4", Enabled: false},
},
}
targets := cfg.AvailableTargets(alias)
if len(targets) != 1 {
t.Fatalf("available targets = %#v, want exactly one", targets)
}
if targets[0].Provider != "p1" || targets[0].Model != "up-1" {
t.Fatalf("available target = %#v, want p1/up-1", targets[0])
}
}
func TestAvailableAliasNamesOnlyReturnsRoutableAliases(t *testing.T) {
t.Parallel()
cfg := &Config{
Providers: []Provider{
{ID: "p1", BaseURL: "https://p1.example.com/v1"},
{ID: "p2", BaseURL: "https://p2.example.com/v1", Disabled: true},
},
Aliases: []Alias{
{Alias: "ok", Enabled: true, Targets: []Target{{Provider: "p1", Model: "up-1", Enabled: true}}},
{Alias: "provider-disabled", Enabled: true, Targets: []Target{{Provider: "p2", Model: "up-2", Enabled: true}}},
{Alias: "alias-disabled", Enabled: false, Targets: []Target{{Provider: "p1", Model: "up-3", Enabled: true}}},
},
}
names := cfg.AvailableAliasNames()
if len(names) != 1 || names[0] != "ok" {
t.Fatalf("available alias names = %#v, want [ok]", names)
}
}
func TestValidateReportsAliasWithoutAvailableTargets(t *testing.T) {
t.Parallel()
cfg := &Config{
Server: Server{Host: "127.0.0.1", Port: 9982, APIKey: "olpx-local"},
Providers: []Provider{{ID: "p1", BaseURL: "https://p1.example.com/v1", Disabled: true}},
Aliases: []Alias{{
Alias: "gpt-5.4",
Enabled: true,
Targets: []Target{{Provider: "p1", Model: "up-1", Enabled: true}},
}},
}
errs := cfg.Validate()
if len(errs) != 1 {
t.Fatalf("Validate() errors = %v, want 1 error", errs)
}
if got := errs[0].Error(); got != `alias "gpt-5.4" has no available targets` {
t.Fatalf("Validate() error = %q", got)
}
}

View File

@ -105,12 +105,9 @@ func (s *Server) handleModels(w http.ResponseWriter, r *http.Request) {
return
}
data := []map[string]any{}
for _, a := range s.cfg.Aliases {
if !a.Enabled {
continue
}
for _, aliasName := range s.cfg.AvailableAliasNames() {
data = append(data, map[string]any{
"id": a.Alias,
"id": aliasName,
"object": "model",
"owned_by": "olpx",
})
@ -177,23 +174,18 @@ func (s *Server) handleResponses(w http.ResponseWriter, r *http.Request) {
writeOpenAIError(w, http.StatusNotFound, "model_not_found", fmt.Sprintf("alias %q is disabled", aliasName))
return
}
var targets []config.Target
for _, t := range alias.Targets {
if t.Enabled {
targets = append(targets, t)
}
}
targets := s.cfg.AvailableTargets(*alias)
if len(targets) == 0 {
s.logger.Printf("req=%d alias=%q has no enabled targets", reqID, aliasName)
writeOpenAIError(w, http.StatusBadRequest, "invalid_request_error", fmt.Sprintf("alias %q has no enabled targets", aliasName))
s.logger.Printf("req=%d alias=%q has no available targets", reqID, aliasName)
writeOpenAIError(w, http.StatusBadRequest, "invalid_request_error", fmt.Sprintf("alias %q has no available targets", aliasName))
return
}
failoverCount := 0
for attempt, t := range targets {
p := s.cfg.FindProvider(t.Provider)
if p == nil {
s.logger.Printf("req=%d alias=%s attempt=%d target provider %q missing, skipping", reqID, aliasName, attempt+1, t.Provider)
if p == nil || !p.IsEnabled() {
s.logger.Printf("req=%d alias=%s attempt=%d target provider %q unavailable, skipping", reqID, aliasName, attempt+1, t.Provider)
failoverCount++
continue
}

View File

@ -157,6 +157,109 @@ func TestHandleResponsesDoesNotFailOverOn400(t *testing.T) {
}
}
func TestHandleResponsesSkipsDisabledProviders(t *testing.T) {
t.Parallel()
calledDisabled := false
disabled := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
calledDisabled = true
w.WriteHeader(http.StatusOK)
}))
defer disabled.Close()
var seenModel string
enabled := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
var payload map[string]any
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
t.Fatalf("decode payload: %v", err)
}
seenModel, _ = payload["model"].(string)
w.Header().Set("Content-Type", "text/event-stream")
_, _ = w.Write([]byte("data: ok\n\n"))
}))
defer enabled.Close()
srv := New(&config.Config{
Server: config.Server{APIKey: "olpx-local"},
Providers: []config.Provider{
{ID: "p1", BaseURL: disabled.URL + "/v1", Disabled: true},
{ID: "p2", BaseURL: enabled.URL + "/v1"},
},
Aliases: []config.Alias{{
Alias: "gpt-5.4",
Enabled: true,
Targets: []config.Target{{Provider: "p1", Model: "up-1", Enabled: true}, {Provider: "p2", Model: "up-2", Enabled: true}},
}},
})
req := httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{"model":"gpt-5.4","stream":true}`))
req.Header.Set("Authorization", "Bearer olpx-local")
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "text/event-stream")
rr := httptest.NewRecorder()
srv.handleResponses(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("status = %d, want %d", rr.Code, http.StatusOK)
}
if calledDisabled {
t.Fatal("disabled provider should be skipped before any upstream call")
}
if seenModel != "up-2" {
t.Fatalf("enabled upstream model = %q, want up-2", seenModel)
}
if got := rr.Header().Get("X-OLPX-Attempt"); got != "1" {
t.Fatalf("X-OLPX-Attempt = %q, want 1", got)
}
if got := rr.Header().Get("X-OLPX-Failover-Count"); got != "0" {
t.Fatalf("X-OLPX-Failover-Count = %q, want 0", got)
}
if got := rr.Header().Get("X-OLPX-Provider"); got != "p2" {
t.Fatalf("X-OLPX-Provider = %q, want p2", got)
}
if body := rr.Body.String(); body != "data: ok\n\n" {
t.Fatalf("body = %q, want SSE payload", body)
}
}
func TestHandleModelsSkipsAliasesWithoutAvailableTargets(t *testing.T) {
t.Parallel()
srv := New(&config.Config{
Server: config.Server{APIKey: "olpx-local"},
Providers: []config.Provider{
{ID: "p1", BaseURL: "https://p1.example.com/v1"},
{ID: "p2", BaseURL: "https://p2.example.com/v1", Disabled: true},
},
Aliases: []config.Alias{
{Alias: "ok", Enabled: true, Targets: []config.Target{{Provider: "p1", Model: "up-1", Enabled: true}}},
{Alias: "no-route", Enabled: true, Targets: []config.Target{{Provider: "p2", Model: "up-2", Enabled: true}}},
{Alias: "alias-disabled", Enabled: false, Targets: []config.Target{{Provider: "p1", Model: "up-3", Enabled: true}}},
},
})
req := httptest.NewRequest(http.MethodGet, "/v1/models", nil)
req.Header.Set("Authorization", "Bearer olpx-local")
rr := httptest.NewRecorder()
srv.handleModels(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("status = %d, want %d", rr.Code, http.StatusOK)
}
if body := rr.Body.String(); !strings.Contains(body, `"id":"ok"`) {
t.Fatalf("models body = %q, want alias ok", body)
}
if body := rr.Body.String(); strings.Contains(body, `"id":"no-route"`) {
t.Fatalf("models body = %q, disabled-provider alias should be hidden", body)
}
if body := rr.Body.String(); strings.Contains(body, `"id":"alias-disabled"`) {
t.Fatalf("models body = %q, disabled alias should be hidden", body)
}
}
func TestReadFirstChunk(t *testing.T) {
t.Parallel()