From cd7b7a042fecbdfc8ff86ca19ac0cf478178b7cd Mon Sep 17 00:00:00 2001 From: yetone Date: Wed, 30 Sep 2026 17:58:35 +0800 Subject: [PATCH] =?UTF-8?q?sessions:=20omp's=20local=20sessions=20are=20in?= =?UTF-8?q?=20Usage=20=E2=80=BA=20Sessions=20(#303),=20and=20the=20GLM=20C?= =?UTF-8?q?oding=20Plans=20take=20Codex's=20Responses=20as=20they=20are=20?= =?UTF-8?q?(#306)=20=E2=80=94=20omp=20(oh-my-pi)=20keeps=20Pi's=20session?= =?UTF-8?q?=20jsonl=20(v3)=20under=20~/.omp/agent/sessions=20(and=20profil?= =?UTF-8?q?es'=20and=20$XDG=5FDATA=5FHOME/omp's)=20with=20a=20title=20line?= =?UTF-8?q?,=20title=20changes,=20model=5Fusage=20for=20calls=20outside=20?= =?UTF-8?q?the=20chat,=20provider/model=20changes=20and=20subagent/advisor?= =?UTF-8?q?=20transcripts=20beside=20the=20file;=20they=20are=20read=20thr?= =?UTF-8?q?ough=20Pi's=20parser,=20subagents=20counted=20in=20their=20sess?= =?UTF-8?q?ion,=20a=20fork's=20copied=20entries=20in=20the=20original=20on?= =?UTF-8?q?ly,=20resumed=20by=20omp=20--resume=20.=20The=20zhipu=20and?= =?UTF-8?q?=20zai=20Coding=20Plan=20regions=20get=20their=20Responses=20ba?= =?UTF-8?q?se=20(open.bigmodel.cn/api/v1,=20api.z.ai/api/v1,=20per=20their?= =?UTF-8?q?=20Codex=20docs),=20so=20Codex's=20requests=20go=20there=20as?= =?UTF-8?q?=20they=20are=20instead=20of=20rebuilt=20for=20Chat;=20Chat=20a?= =?UTF-8?q?nd=20Claude=20Code=20routes=20are=20unchanged.=20TestOmp,=20Tes?= =?UTF-8?q?tDirsMore=20and=20TestGLMCodingPlanResponses=20fail=20without?= =?UTF-8?q?=20the=20change.=20go=20vet,=20go=20build=20-tags=20nogui=20(da?= =?UTF-8?q?rwin,=20GOOS=3Dlinux),=20GOOS=3Dwindows=20go=20build,=20go=20te?= =?UTF-8?q?st=20-tags=20nogui=20./...=20pass.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/gateway/glm_responses_test.go | 125 ++++++++++++++++++ internal/provider/presets.go | 8 +- internal/sessions/agents_test.go | 3 +- internal/sessions/omp.go | 86 ++++++++++++ internal/sessions/omp_test.go | 90 +++++++++++++ internal/sessions/pi.go | 22 ++- internal/sessions/sessions.go | 15 ++- internal/sessions/sessions_test.go | 4 +- ...019a0000-0000-7000-8000-00000000000a.jsonl | 11 ++ .../0-Explore.jsonl | 4 + .../0-Explore/0-Find.jsonl | 3 + .../__advisor.jsonl | 3 + .../btw-history/1790582450000.json | 1 + ...019a0000-0000-7000-8000-00000000000c.jsonl | 9 ++ ...019a0000-0000-7000-8000-00000000000d.jsonl | 6 + 15 files changed, 374 insertions(+), 16 deletions(-) create mode 100644 internal/gateway/glm_responses_test.go create mode 100644 internal/sessions/omp.go create mode 100644 internal/sessions/omp_test.go create mode 100644 internal/sessions/testdata/omp/agent/sessions/--work-omp--/2026-09-28T08-00-00-000Z_019a0000-0000-7000-8000-00000000000a.jsonl create mode 100644 internal/sessions/testdata/omp/agent/sessions/--work-omp--/2026-09-28T08-00-00-000Z_019a0000-0000-7000-8000-00000000000a/0-Explore.jsonl create mode 100644 internal/sessions/testdata/omp/agent/sessions/--work-omp--/2026-09-28T08-00-00-000Z_019a0000-0000-7000-8000-00000000000a/0-Explore/0-Find.jsonl create mode 100644 internal/sessions/testdata/omp/agent/sessions/--work-omp--/2026-09-28T08-00-00-000Z_019a0000-0000-7000-8000-00000000000a/__advisor.jsonl create mode 100644 internal/sessions/testdata/omp/agent/sessions/--work-omp--/2026-09-28T08-00-00-000Z_019a0000-0000-7000-8000-00000000000a/btw-history/1790582450000.json create mode 100644 internal/sessions/testdata/omp/agent/sessions/--work-omp--/2026-09-29T09-00-00-000Z_019a0000-0000-7000-8000-00000000000c.jsonl create mode 100644 internal/sessions/testdata/omp/agent/sessions/--work-omp--/2026-09-29T10-00-00-000Z_019a0000-0000-7000-8000-00000000000d.jsonl diff --git a/internal/gateway/glm_responses_test.go b/internal/gateway/glm_responses_test.go new file mode 100644 index 00000000..18fbb581 --- /dev/null +++ b/internal/gateway/glm_responses_test.go @@ -0,0 +1,125 @@ +package gateway + +import ( + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "strings" + "sync" + "testing" + + "github.com/yetone/magpie/internal/provider" +) + +// glmPlan plays a GLM Coding Plan: chat completions at /api/coding/paas/v4, +// the Responses API at /api/v1, Anthropic's messages at /api/anthropic. +type glmPlan struct { + mu sync.Mutex + paths []string + model []string +} + +func (g *glmPlan) ServeHTTP(w http.ResponseWriter, r *http.Request) { + body, _ := io.ReadAll(r.Body) + var v struct { + Model string `json:"model"` + } + json.Unmarshal(body, &v) + g.mu.Lock() + g.paths, g.model = append(g.paths, r.URL.Path), append(g.model, v.Model) + g.mu.Unlock() + w.Header().Set("Content-Type", "text/event-stream") + switch r.URL.Path { + case "/api/v1/responses": + io.WriteString(w, sse( + `event: response.created`+"\n"+`data: {"type":"response.created","response":{"id":"resp_1","model":"`+v.Model+`","status":"in_progress","output":[]}}`, + `event: response.output_text.delta`+"\n"+`data: {"type":"response.output_text.delta","item_id":"m1","output_index":0,"content_index":0,"delta":"from responses"}`, + `event: response.completed`+"\n"+`data: {"type":"response.completed","response":{"id":"resp_1","model":"`+v.Model+`","status":"completed","output":[{"type":"message","id":"m1","role":"assistant","content":[{"type":"output_text","text":"from responses"}]}],"usage":{"input_tokens":5,"output_tokens":2,"total_tokens":7}}}`)) + case "/api/coding/paas/v4/chat/completions": + io.WriteString(w, sse( + `data: {"id":"c1","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"role":"assistant","content":"from chat"}}]}`, + `data: {"id":"c1","object":"chat.completion.chunk","choices":[{"index":0,"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":5,"completion_tokens":2}}`, + `data: [DONE]`)) + case "/api/anthropic/v1/messages": + io.WriteString(w, sse( + `event: message_start`+"\n"+`data: {"type":"message_start","message":{"id":"msg_1","model":"`+v.Model+`","usage":{"input_tokens":5}}}`, + `data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}`, + `data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"from messages"}}`, + `data: {"type":"content_block_stop","index":0}`, + `data: {"type":"message_delta","delta":{"stop_reason":"end_turn"},"usage":{"output_tokens":2}}`, + `data: {"type":"message_stop"}`)) + default: + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusNotFound) + io.WriteString(w, `{"error":{"message":"no such route"}}`) + } +} + +// The GLM Coding Plan's Responses API (#306): the zhipu and zai presets' +// Coding Plan regions give it at /api/v1, as the plans' Codex pages set +// base_url with wire_api = "responses", so Codex's request is relayed there +// as it came rather than translated to chat; chat clients stay on the plan's +// chat completions and Claude Code on its messages. +func TestGLMCodingPlanResponses(t *testing.T) { + for _, tc := range []struct{ preset, host string }{ + {"zhipu", "https://open.bigmodel.cn"}, + {"zai", "https://api.z.ai"}, + } { + t.Run(tc.preset, func(t *testing.T) { + pr := provider.Preset(tc.preset) + var coding, payg provider.Region + for _, r := range pr.Regions { + switch r.ID { + case "coding": + coding = r + case "api": + payg = r + } + } + if coding.Responses != tc.host+"/api/v1" || coding.Chat != tc.host+"/api/coding/paas/v4" || coding.Anthropic != tc.host+"/api/anthropic" { + t.Fatalf("coding plan: %+v", coding) + } + // pay as you go is left as it was: the docs give /api/v1 for the plan + if payg.Responses != "" || pr.Responses != "" { + t.Fatalf("pay as you go: %+v / %q", payg, pr.Responses) + } + + fresh(t) + up := &glmPlan{} + srv := httptest.NewServer(up) + t.Cleanup(srv.Close) + p, err := provider.FromPreset(tc.preset) + if err != nil { + t.Fatal(err) + } + at := func(u string) string { return srv.URL + strings.TrimPrefix(u, tc.host) } + p.Key = "glm-k" + p.Chat, p.Responses, p.Anthropic = at(coding.Chat), at(coding.Responses), at(coding.Anthropic) + p.Models = []string{"glm-5.3"} + if err := provider.Save(p); err != nil { + t.Fatal(err) + } + m := tc.preset + "/glm-5.3" + for _, c := range []struct{ path, body, upstream, reply string }{ + {"/v1/responses", `{"model":"` + m + `","input":"hi","stream":true}`, "/api/v1/responses", "from responses"}, + {"/v1/chat/completions", `{"model":"` + m + `","messages":[{"role":"user","content":"hi"}],"stream":true}`, "/api/coding/paas/v4/chat/completions", "from chat"}, + {"/v1/messages", `{"model":"` + m + `","max_tokens":20,"messages":[{"role":"user","content":"hi"}],"stream":true}`, "/api/anthropic/v1/messages", "from messages"}, + } { + up.mu.Lock() + up.paths, up.model = nil, nil + up.mu.Unlock() + code, body := post(t, c.path, c.body) + if code != 200 || !strings.Contains(body, c.reply) { + t.Fatalf("%s: status %d: %s", c.path, code, body) + } + up.mu.Lock() + paths, models := up.paths, up.model + up.mu.Unlock() + if len(paths) != 1 || paths[0] != c.upstream || models[0] != "glm-5.3" { + t.Fatalf("%s asked upstream at %v %v, want %s", c.path, paths, models, c.upstream) + } + } + }) + } +} diff --git a/internal/provider/presets.go b/internal/provider/presets.go index d074933c..a12fe5cf 100644 --- a/internal/provider/presets.go +++ b/internal/provider/presets.go @@ -109,17 +109,19 @@ var presets = []PresetDef{ Chat: "https://open.bigmodel.cn/api/paas/v4", Anthropic: "https://open.bigmodel.cn/api/anthropic", Website: "https://open.bigmodel.cn", KeysURL: "https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys", // a GLM Coding Plan is served at its own OpenAI endpoint: a plan's key - // sent to the pay-as-you-go one is told it has no balance + // sent to the pay-as-you-go one is told it has no balance. The plan + // serves the Responses API at /api/v1 as well: its tool pages give + // it for Codex, wire_api = "responses" (#306) RegionLabel: "Plan", Regions: []Region{ {ID: "api", Name: "Pay as you go", Chat: "https://open.bigmodel.cn/api/paas/v4", Anthropic: "https://open.bigmodel.cn/api/anthropic"}, - {ID: "coding", Name: "Coding Plan", Chat: "https://open.bigmodel.cn/api/coding/paas/v4", Anthropic: "https://open.bigmodel.cn/api/anthropic"}, + {ID: "coding", Name: "Coding Plan", Chat: "https://open.bigmodel.cn/api/coding/paas/v4", Responses: "https://open.bigmodel.cn/api/v1", Anthropic: "https://open.bigmodel.cn/api/anthropic"}, }}, {ID: "zai", Name: "Z.ai", Icon: "zai", Kind: KindVendor, Catalog: "zhipuai", Chat: "https://api.z.ai/api/paas/v4", Anthropic: "https://api.z.ai/api/anthropic", Website: "https://z.ai", KeysURL: "https://z.ai/manage-apikey/apikey-list", RegionLabel: "Plan", Regions: []Region{ {ID: "api", Name: "Pay as you go", Chat: "https://api.z.ai/api/paas/v4", Anthropic: "https://api.z.ai/api/anthropic"}, - {ID: "coding", Name: "Coding Plan", Chat: "https://api.z.ai/api/coding/paas/v4", Anthropic: "https://api.z.ai/api/anthropic"}, + {ID: "coding", Name: "Coding Plan", Chat: "https://api.z.ai/api/coding/paas/v4", Responses: "https://api.z.ai/api/v1", Anthropic: "https://api.z.ai/api/anthropic"}, }}, {ID: "minimax", Name: "MiniMax", Icon: "minimax-color", Kind: KindVendor, Catalog: "minimax", Chat: "https://api.minimax.io/v1", Anthropic: "https://api.minimax.io/anthropic", diff --git a/internal/sessions/agents_test.go b/internal/sessions/agents_test.go index 7df2959a..24e71731 100644 --- a/internal/sessions/agents_test.go +++ b/internal/sessions/agents_test.go @@ -29,6 +29,7 @@ func setupAgents(t *testing.T) (home string) { copyTree(t, "testdata/qoder-cn", filepath.Join(home, ".qoder-cn")) copyTree(t, "testdata/grok", filepath.Join(home, ".grok")) copyTree(t, "testdata/workbuddy", filepath.Join(home, ".workbuddy")) + copyTree(t, "testdata/omp", filepath.Join(home, ".omp")) zcMakeDB(t, filepath.Join(home, ".zcode", "cli", "db", "db.sqlite")) return } @@ -272,7 +273,7 @@ func TestDirsMore(t *testing.T) { home := setupAgents(t) d := Dirs() want := []string{filepath.Join(home, ".zcode"), filepath.Join(home, ".dsh"), filepath.Join(home, ".cline", "data", "sessions"), filepath.Join(home, ".qoder"), filepath.Join(home, ".qoder-cn"), - filepath.Join(home, ".grok"), filepath.Join(home, ".workbuddy")} + filepath.Join(home, ".grok"), filepath.Join(home, ".workbuddy"), filepath.Join(home, ".omp", "agent")} if len(d) != 2+len(want) { t.Fatalf("dirs %v", d) } diff --git a/internal/sessions/omp.go b/internal/sessions/omp.go new file mode 100644 index 00000000..dc1e513d --- /dev/null +++ b/internal/sessions/omp.go @@ -0,0 +1,86 @@ +package sessions + +import ( + "io/fs" + "os" + "path/filepath" + "runtime" + "strings" +) + +// omp (oh-my-pi, a fork of Pi) keeps its sessions as Pi does, a file per +// session,