CrossRelay stage 2: Codex /v1/responses relay, config.toml manager, Codex page

This commit is contained in:
kate522 committed 2026-10-18 18:35:01 +08:00
1 parent 404704171d
commit 5112f7ca06
30 files changed
+3539 -72

No files matched your search

+257
View File
@@ -62,6 +62,7 @@ type testEnv struct {
upstream *fakeUpstream
store *config.Store
settings string
codex string
}
func newTestEnv(t *testing.T) *testEnv {
@@ -133,6 +134,262 @@ 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.
func newCodexTestEnv(t *testing.T) *testEnv {
t.Helper()
env := newTestEnv(t)
codexHome := t.TempDir()
t.Setenv("CODEX_HOME", codexHome)
env.codex = filepath.Join(codexHome, "config.toml")
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 {
t.Fatalf("ActivateCodex: %v", err)
}
return env
}
func (e *testEnv) get(t *testing.T, path string) map[string]any {
t.Helper()
resp, err := http.Get(e.gateway.URL + path)
if err != nil {
t.Fatalf("get %s: %v", path, err)
}
defer resp.Body.Close()
var out map[string]any
_ = json.NewDecoder(resp.Body).Decode(&out)
return out
}
func responsesBody(model string, stream bool) map[string]any {
return map[string]any{
"model": model,
"instructions": "be brief",
"input": []any{map[string]any{
"type": "message", "role": "user",
"content": []any{map[string]any{"type": "input_text", "text": "hello"}},
}},
"stream": stream,
}
}
func TestResponsesNonStream(t *testing.T) {
env := newCodexTestEnv(t)
resp := env.post(t, "/v1/responses", responsesBody("gpt-5-codex", false), env.store.Config().RelayToken)
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("status = %d", resp.StatusCode)
}
var out map[string]any
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
t.Fatalf("decode: %v", err)
}
if out["object"] != "response" || out["status"] != "completed" {
t.Errorf("envelope = %v", out)
}
if out["model"] != "gpt-5-codex" {
t.Errorf("model = %v, want the requested model echoed", out["model"])
}
if out["output_text"] != "Hi there" {
t.Errorf("output_text = %v", out["output_text"])
}
usage := out["usage"].(map[string]any)
if usage["input_tokens"].(float64) != 9 || usage["output_tokens"].(float64) != 2 {
t.Errorf("usage = %v", usage)
}
if env.upstream.lastModel != "ds-chat" {
t.Errorf("upstream model = %q, want the resolved model", env.upstream.lastModel)
}
if env.upstream.lastAuth != "Bearer up-key" {
t.Errorf("upstream auth = %q", env.upstream.lastAuth)
}
msgs := env.upstream.lastRequest["messages"].([]any)
system := msgs[0].(map[string]any)
if system["role"] != "system" || system["content"] != "be brief" {
t.Errorf("system message = %v", system)
}
user := msgs[1].(map[string]any)
if user["role"] != "user" || user["content"] != "hello" {
t.Errorf("user message = %v", user)
}
}
func TestResponsesStream(t *testing.T) {
env := newCodexTestEnv(t)
resp := env.post(t, "/v1/responses", responsesBody("gpt-5-codex", true), env.store.Config().RelayToken)
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("status = %d", resp.StatusCode)
}
if ct := resp.Header.Get("Content-Type"); !strings.HasPrefix(ct, "text/event-stream") {
t.Errorf("content type = %q", ct)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatalf("read stream: %v", err)
}
var names []string
var text strings.Builder
for _, block := range strings.Split(strings.TrimSpace(string(body)), "\n\n") {
if block == "" {
continue
}
for _, line := range strings.Split(block, "\n") {
switch {
case strings.HasPrefix(line, "event: "):
names = append(names, strings.TrimPrefix(line, "event: "))
case strings.HasPrefix(line, "data: "):
var payload map[string]any
if err := json.Unmarshal([]byte(strings.TrimPrefix(line, "data: ")), &payload); err == nil {
if payload["type"] == "response.output_text.delta" {
text.WriteString(payload["delta"].(string))
}
}
}
}
}
want := []string{
"response.created", "response.in_progress",
"response.output_item.added", "response.content_part.added",
"response.output_text.delta", "response.output_text.delta",
"response.output_text.done", "response.content_part.done",
"response.output_item.done", "response.completed",
}
if len(names) != len(want) {
t.Fatalf("events = %v, want %v", names, want)
}
for i := range want {
if names[i] != want[i] {
t.Fatalf("events = %v, want %v", names, want)
}
}
if text.String() != "Hi there" {
t.Errorf("streamed text = %q", text.String())
}
}
func TestResponsesRejectsBadToken(t *testing.T) {
env := newCodexTestEnv(t)
resp := env.post(t, "/v1/responses", responsesBody("gpt-5-codex", false), "crl_wrong")
defer resp.Body.Close()
if resp.StatusCode != http.StatusUnauthorized {
t.Errorf("status = %d, want 401", resp.StatusCode)
}
}
func TestResponsesWithoutActiveProvider(t *testing.T) {
env := newTestEnv(t)
t.Setenv("CODEX_HOME", t.TempDir())
resp := env.post(t, "/v1/responses", responsesBody("gpt-5-codex", false), env.store.Config().RelayToken)
defer resp.Body.Close()
if resp.StatusCode != http.StatusBadRequest {
t.Fatalf("status = %d, want 400", resp.StatusCode)
}
var out map[string]any
_ = json.NewDecoder(resp.Body).Decode(&out)
detail, ok := out["error"].(map[string]any)
if !ok || !strings.Contains(detail["message"].(string), "no active provider") {
t.Errorf("error = %v", out["error"])
}
}
func TestApplyAndRestoreCodex(t *testing.T) {
env := newCodexTestEnv(t)
id := env.store.Config().Providers[0].ID
resp := env.post(t, "/api/providers/"+id+"/apply", map[string]any{"target": "codex"}, "")
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("apply status = %d", resp.StatusCode)
}
var out map[string]any
_ = json.NewDecoder(resp.Body).Decode(&out)
injection, ok := out["injection"].(map[string]any)
if !ok || injection["model_provider"] != "crossrelay" {
t.Fatalf("injection = %v", out["injection"])
}
table := injection["table"].(map[string]any)
if table["wire_api"] != "responses" || table["base_url"] != "http://127.0.0.1:18787" {
t.Errorf("injected table = %v", table)
}
if table["experimental_bearer_token"] != env.store.Config().RelayToken {
t.Errorf("injected token = %v", table["experimental_bearer_token"])
}
data, err := os.ReadFile(env.codex)
if err != nil {
t.Fatalf("read config.toml: %v", err)
}
got := string(data)
for _, want := range []string{
`model_provider = "crossrelay"`,
"[model_providers.crossrelay]",
"requires_openai_auth = false",
"[model_providers.custom]",
`base_url = "https://old.example.com/v1"`,
} {
if !strings.Contains(got, want) {
t.Errorf("config.toml missing %q:\n%s", want, got)
}
}
if env.store.Config().CodexPriorProvider != "custom" {
t.Errorf("prior provider = %q, want custom", env.store.Config().CodexPriorProvider)
}
if status := env.get(t, "/api/status"); status["active_codex_provider"] == nil {
t.Errorf("status lacks active_codex_provider: %v", status)
}
resp = env.post(t, "/api/restore", map[string]any{"target": "codex"}, "")
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("restore status = %d", resp.StatusCode)
}
data, _ = os.ReadFile(env.codex)
got = string(data)
if !strings.Contains(got, `model_provider = "custom"`) {
t.Errorf("prior provider not restored:\n%s", got)
}
if strings.Contains(got, "crossrelay") {
t.Errorf("managed keys survived restore:\n%s", got)
}
if !strings.Contains(got, `base_url = "https://old.example.com/v1"`) {
t.Errorf("user's provider table damaged:\n%s", got)
}
if env.store.ActiveCodex() != nil {
t.Error("active codex provider survived restore")
}
if env.store.Config().CodexPriorProvider != "" {
t.Error("prior provider record survived restore")
}
}
func TestBackupsTargetDimension(t *testing.T) {
env := newCodexTestEnv(t)
id := env.store.Config().Providers[0].ID
resp := env.post(t, "/api/providers/"+id+"/apply", map[string]any{"target": "claude"}, "")
resp.Body.Close()
resp = env.post(t, "/api/providers/"+id+"/apply", map[string]any{"target": "codex"}, "")
resp.Body.Close()
codexBackups := env.get(t, "/api/backups?target=codex")["items"].([]any)
if len(codexBackups) != 1 {
t.Fatalf("codex backups = %d, want 1", len(codexBackups))
}
if name := codexBackups[0].(map[string]any)["name"].(string); !strings.HasPrefix(name, "config-") {
t.Errorf("codex backup name = %q", name)
}
claudeBackups := env.get(t, "/api/backups")["items"].([]any)
if len(claudeBackups) != 1 {
t.Fatalf("claude backups = %d, want 1", len(claudeBackups))
}
if name := claudeBackups[0].(map[string]any)["name"].(string); !strings.HasPrefix(name, "settings-") {
t.Errorf("claude backup name = %q", name)
}
}
func TestMessagesNonStream(t *testing.T) {
env := newTestEnv(t)
resp := env.post(t, "/v1/messages", messagesBody("claude-sonnet-4-5", false), env.store.Config().RelayToken)