Isolate providers per client: add provider targets and a Codex-side form
This commit is contained in:
1 parent
adc8234cc9
commit
d759fe5e5d
13 files changed
+595
-309
No files matched your search
@@ -58,11 +58,12 @@ func writeSSELine(w http.ResponseWriter, line string) {
|
||||
}
|
||||
|
||||
type testEnv struct {
|
||||
gateway *httptest.Server
|
||||
upstream *fakeUpstream
|
||||
store *config.Store
|
||||
settings string
|
||||
codex string
|
||||
gateway *httptest.Server
|
||||
upstream *fakeUpstream
|
||||
store *config.Store
|
||||
settings string
|
||||
codex string
|
||||
upstreamURL string
|
||||
}
|
||||
|
||||
func newTestEnv(t *testing.T) *testEnv {
|
||||
@@ -87,7 +88,7 @@ func newTestEnv(t *testing.T) *testEnv {
|
||||
Sonnet: "ds-chat",
|
||||
Haiku: "ds-haiku",
|
||||
Opus: "ds-opus",
|
||||
})
|
||||
}, config.TargetClaude)
|
||||
if err != nil {
|
||||
t.Fatalf("AddProvider: %v", err)
|
||||
}
|
||||
@@ -97,7 +98,7 @@ func newTestEnv(t *testing.T) *testEnv {
|
||||
|
||||
gw := httptest.NewServer(New(store, 18787).Handler())
|
||||
t.Cleanup(gw.Close)
|
||||
return &testEnv{gateway: gw, upstream: upstream, store: store, settings: settingsPath}
|
||||
return &testEnv{gateway: gw, upstream: upstream, store: store, settings: settingsPath, upstreamURL: up.URL}
|
||||
}
|
||||
|
||||
func (e *testEnv) post(t *testing.T, path string, body any, token string) *http.Response {
|
||||
@@ -135,7 +136,7 @@ func messagesBody(model string, stream bool) map[string]any {
|
||||
}
|
||||
|
||||
// newCodexTestEnv builds the standard environment with a realistic
|
||||
// config.toml in place and the shared provider activated for Codex.
|
||||
// config.toml in place plus a dedicated Codex provider, activated.
|
||||
func newCodexTestEnv(t *testing.T) *testEnv {
|
||||
t.Helper()
|
||||
env := newTestEnv(t)
|
||||
@@ -145,12 +146,26 @@ func newCodexTestEnv(t *testing.T) *testEnv {
|
||||
if err := os.WriteFile(env.codex, []byte("model_provider = \"custom\"\n\n[model_providers.custom]\nname = \"custom\"\nwire_api = \"responses\"\nbase_url = \"https://old.example.com/v1\"\n"), 0o600); err != nil {
|
||||
t.Fatalf("write config.toml: %v", err)
|
||||
}
|
||||
if _, err := env.store.ActivateCodex(env.store.Config().Providers[0].ID); err != nil {
|
||||
p, err := env.store.AddProvider("codex-up", "", env.upstreamURL, "up-key",
|
||||
config.Models{Default: "ds-chat"}, config.TargetCodex)
|
||||
if err != nil {
|
||||
t.Fatalf("AddProvider: %v", err)
|
||||
}
|
||||
if _, err := env.store.ActivateCodex(p.ID); err != nil {
|
||||
t.Fatalf("ActivateCodex: %v", err)
|
||||
}
|
||||
return env
|
||||
}
|
||||
|
||||
func providerIDForTarget(env *testEnv, target string) string {
|
||||
for _, p := range env.store.Config().Providers {
|
||||
if p.Target == target {
|
||||
return p.ID
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (e *testEnv) get(t *testing.T, path string) map[string]any {
|
||||
t.Helper()
|
||||
resp, err := http.Get(e.gateway.URL + path)
|
||||
@@ -297,7 +312,7 @@ func TestResponsesWithoutActiveProvider(t *testing.T) {
|
||||
|
||||
func TestApplyAndRestoreCodex(t *testing.T) {
|
||||
env := newCodexTestEnv(t)
|
||||
id := env.store.Config().Providers[0].ID
|
||||
id := providerIDForTarget(env, config.TargetCodex)
|
||||
|
||||
resp := env.post(t, "/api/providers/"+id+"/apply", map[string]any{"target": "codex"}, "")
|
||||
defer resp.Body.Close()
|
||||
@@ -367,11 +382,12 @@ func TestApplyAndRestoreCodex(t *testing.T) {
|
||||
|
||||
func TestBackupsTargetDimension(t *testing.T) {
|
||||
env := newCodexTestEnv(t)
|
||||
id := env.store.Config().Providers[0].ID
|
||||
claudeID := providerIDForTarget(env, config.TargetClaude)
|
||||
codexID := providerIDForTarget(env, config.TargetCodex)
|
||||
|
||||
resp := env.post(t, "/api/providers/"+id+"/apply", map[string]any{"target": "claude"}, "")
|
||||
resp := env.post(t, "/api/providers/"+claudeID+"/apply", map[string]any{"target": "claude"}, "")
|
||||
resp.Body.Close()
|
||||
resp = env.post(t, "/api/providers/"+id+"/apply", map[string]any{"target": "codex"}, "")
|
||||
resp = env.post(t, "/api/providers/"+codexID+"/apply", map[string]any{"target": "codex"}, "")
|
||||
resp.Body.Close()
|
||||
|
||||
codexBackups := env.get(t, "/api/backups?target=codex")["items"].([]any)
|
||||
@@ -642,6 +658,11 @@ func TestProviderCRUD(t *testing.T) {
|
||||
if resp.StatusCode != http.StatusCreated {
|
||||
t.Fatalf("create status = %d", resp.StatusCode)
|
||||
}
|
||||
var created map[string]any
|
||||
_ = json.NewDecoder(resp.Body).Decode(&created)
|
||||
if created["target"] != "claude" {
|
||||
t.Errorf("created provider target = %v, want claude by default", created["target"])
|
||||
}
|
||||
if len(env.store.Config().Providers) != 2 {
|
||||
t.Fatalf("providers = %d, want 2", len(env.store.Config().Providers))
|
||||
}
|
||||
@@ -651,3 +672,40 @@ func TestProviderCRUD(t *testing.T) {
|
||||
t.Errorf("invalid create status = %d, want 400", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyRejectsWrongTarget(t *testing.T) {
|
||||
env := newTestEnv(t)
|
||||
id := providerIDForTarget(env, config.TargetClaude)
|
||||
resp := env.post(t, "/api/providers/"+id+"/apply", map[string]any{"target": "codex"}, "")
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusNotFound {
|
||||
t.Errorf("apply status = %d, want 404 for a provider of the other target", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListProvidersFiltersByTarget(t *testing.T) {
|
||||
env := newCodexTestEnv(t)
|
||||
codexItems := env.get(t, "/api/providers?target=codex")["items"].([]any)
|
||||
if len(codexItems) != 1 {
|
||||
t.Fatalf("codex providers = %d, want 1", len(codexItems))
|
||||
}
|
||||
if codexItems[0].(map[string]any)["target"] != "codex" {
|
||||
t.Errorf("codex list leaked another target: %v", codexItems[0])
|
||||
}
|
||||
claudeItems := env.get(t, "/api/providers?target=claude")["items"].([]any)
|
||||
if len(claudeItems) != 1 {
|
||||
t.Fatalf("claude providers = %d, want 1", len(claudeItems))
|
||||
}
|
||||
allItems := env.get(t, "/api/providers")["items"].([]any)
|
||||
if len(allItems) != 2 {
|
||||
t.Errorf("unfiltered providers = %d, want 2", len(allItems))
|
||||
}
|
||||
resp, err := http.Get(env.gateway.URL + "/api/providers?target=bogus")
|
||||
if err != nil {
|
||||
t.Fatalf("get: %v", err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusBadRequest {
|
||||
t.Errorf("bogus target status = %d, want 400", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ type providerInput struct {
|
||||
BaseURL string `json:"base_url"`
|
||||
APIKey string `json:"api_key"`
|
||||
Models config.Models `json:"models"`
|
||||
Target string `json:"target"`
|
||||
}
|
||||
|
||||
func (s *Server) handleStatus(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -54,7 +55,20 @@ func (s *Server) handleStatus(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (s *Server) handleListProviders(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, map[string]any{"items": s.store.Config().Providers})
|
||||
target := r.URL.Query().Get("target")
|
||||
switch target {
|
||||
case "", config.TargetClaude, config.TargetCodex:
|
||||
default:
|
||||
writeAPIError(w, http.StatusBadRequest, `target must be "claude" or "codex"`)
|
||||
return
|
||||
}
|
||||
items := []config.Provider{}
|
||||
for _, p := range s.store.Config().Providers {
|
||||
if target == "" || p.Target == target {
|
||||
items = append(items, p)
|
||||
}
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{"items": items})
|
||||
}
|
||||
|
||||
func (s *Server) handleCreateProvider(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -63,7 +77,7 @@ func (s *Server) handleCreateProvider(w http.ResponseWriter, r *http.Request) {
|
||||
writeAPIError(w, http.StatusBadRequest, "invalid request body: "+err.Error())
|
||||
return
|
||||
}
|
||||
p, err := s.store.AddProvider(in.Name, in.Preset, in.BaseURL, in.APIKey, in.Models)
|
||||
p, err := s.store.AddProvider(in.Name, in.Preset, in.BaseURL, in.APIKey, in.Models, in.Target)
|
||||
if err != nil {
|
||||
writeAPIError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
@@ -78,7 +92,7 @@ func (s *Server) handleUpdateProvider(w http.ResponseWriter, r *http.Request) {
|
||||
writeAPIError(w, http.StatusBadRequest, "invalid request body: "+err.Error())
|
||||
return
|
||||
}
|
||||
p, err := s.store.UpdateProvider(id, in.Name, in.Preset, in.BaseURL, in.APIKey, in.Models)
|
||||
p, err := s.store.UpdateProvider(id, in.Name, in.Preset, in.BaseURL, in.APIKey, in.Models, in.Target)
|
||||
if err != nil {
|
||||
writeAPIError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
|
||||
Reference in new issue
Block a user